misc changes
This commit is contained in:
@ -10,7 +10,7 @@ import UserSettingsHeader from "@/components/dashboard/user/user-settings-header
|
|||||||
* @return the page jsx
|
* @return the page jsx
|
||||||
*/
|
*/
|
||||||
const UserBillingPage = (): ReactElement => (
|
const UserBillingPage = (): ReactElement => (
|
||||||
<main className="w-[47rem] p-10 flex flex-col gap-5">
|
<main className="w-[47rem] px-10 py-7 flex flex-col gap-5">
|
||||||
<UserSettingsHeader title="Billing" />
|
<UserSettingsHeader title="Billing" />
|
||||||
|
|
||||||
{/* Content */}
|
{/* Content */}
|
||||||
|
@ -14,7 +14,7 @@ import UserSettingsHeader from "@/components/dashboard/user/user-settings-header
|
|||||||
* @return the page jsx
|
* @return the page jsx
|
||||||
*/
|
*/
|
||||||
const UserProfilePage = (): ReactElement => (
|
const UserProfilePage = (): ReactElement => (
|
||||||
<main className="w-[47rem] p-10 flex flex-col gap-5">
|
<main className="w-[47rem] px-10 py-7 flex flex-col gap-5">
|
||||||
<UserSettingsHeader title="My Profile" />
|
<UserSettingsHeader title="My Profile" />
|
||||||
|
|
||||||
{/* Content */}
|
{/* Content */}
|
||||||
|
@ -12,7 +12,7 @@ import DevicesSetting from "@/components/dashboard/user/settings/device/devices-
|
|||||||
* @return the page jsx
|
* @return the page jsx
|
||||||
*/
|
*/
|
||||||
const UserSettingsPage = (): ReactElement => (
|
const UserSettingsPage = (): ReactElement => (
|
||||||
<main className="w-[47rem] p-10 flex flex-col gap-5">
|
<main className="w-[47rem] px-10 py-7 flex flex-col gap-5">
|
||||||
<UserSettingsHeader title="Settings" />
|
<UserSettingsHeader title="Settings" />
|
||||||
|
|
||||||
{/* Content */}
|
{/* Content */}
|
||||||
|
@ -37,20 +37,24 @@ const OrganizationProvider = ({ children }: { children: ReactNode }) => {
|
|||||||
* Fetch the organizations for the logged in user.
|
* Fetch the organizations for the logged in user.
|
||||||
*/
|
*/
|
||||||
const fetchOrganizations = useCallback(async () => {
|
const fetchOrganizations = useCallback(async () => {
|
||||||
let selectedOrganization: string | null = localStorage.getItem(
|
|
||||||
"selected-organization"
|
|
||||||
);
|
|
||||||
const { data, error } = await apiRequest<Organization[]>({
|
const { data, error } = await apiRequest<Organization[]>({
|
||||||
endpoint: "/organization/@me",
|
endpoint: "/organization/@me",
|
||||||
session,
|
session,
|
||||||
});
|
});
|
||||||
|
const selectedOrgSnowflake: string | null = localStorage.getItem(
|
||||||
|
"selected-organization"
|
||||||
|
);
|
||||||
const organizations: Organization[] = data as Organization[];
|
const organizations: Organization[] = data as Organization[];
|
||||||
if (!selectedOrganization && organizations.length > 0) {
|
let selected: Organization | undefined;
|
||||||
selectedOrganization = organizations[0].snowflake;
|
if (!selected && organizations.length > 0) {
|
||||||
|
selected = organizations[0];
|
||||||
|
} else {
|
||||||
|
selected = organizations.find(
|
||||||
|
(organization: Organization) =>
|
||||||
|
organization.snowflake === selectedOrgSnowflake
|
||||||
|
);
|
||||||
}
|
}
|
||||||
storeRef.current
|
storeRef.current?.getState().update(organizations, selected);
|
||||||
?.getState()
|
|
||||||
.update(selectedOrganization || undefined, organizations);
|
|
||||||
}, [session]);
|
}, [session]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -13,15 +13,15 @@ export const OrganizationContext = createContext<OrganizationStore | null>(
|
|||||||
* The props in this store.
|
* The props in this store.
|
||||||
*/
|
*/
|
||||||
export type OrganizationStoreProps = {
|
export type OrganizationStoreProps = {
|
||||||
/**
|
|
||||||
* The currently selected organization.
|
|
||||||
*/
|
|
||||||
selected: string | undefined;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The organization's the user has.
|
* The organization's the user has.
|
||||||
*/
|
*/
|
||||||
organizations: Organization[];
|
organizations: Organization[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The currently selected organization.
|
||||||
|
*/
|
||||||
|
selected: Organization | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -31,12 +31,12 @@ export type OrganizationState = OrganizationStoreProps & {
|
|||||||
/**
|
/**
|
||||||
* Update the state.
|
* Update the state.
|
||||||
*
|
*
|
||||||
* @param selected the selected organization
|
|
||||||
* @param organizations the user's organizations
|
* @param organizations the user's organizations
|
||||||
|
* @param selected the selected organization
|
||||||
*/
|
*/
|
||||||
update: (
|
update: (
|
||||||
selected: string | undefined,
|
organizations: Organization[],
|
||||||
organizations: Organization[]
|
selected: Organization | undefined
|
||||||
) => void;
|
) => void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -44,7 +44,7 @@ export type OrganizationState = OrganizationStoreProps & {
|
|||||||
*
|
*
|
||||||
* @param selected the selected organization
|
* @param selected the selected organization
|
||||||
*/
|
*/
|
||||||
setSelected: (selected: string | undefined) => void;
|
setSelected: (selected: Organization | undefined) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -62,9 +62,11 @@ const createOrganizationStore = () => {
|
|||||||
};
|
};
|
||||||
return createStore<OrganizationState>()((set) => ({
|
return createStore<OrganizationState>()((set) => ({
|
||||||
...defaultProps,
|
...defaultProps,
|
||||||
update: (selected: string | undefined, organizations: Organization[]) =>
|
update: (
|
||||||
set(() => ({ selected, organizations })),
|
organizations: Organization[],
|
||||||
setSelected: (selected: string | undefined) =>
|
selected: Organization | undefined
|
||||||
|
) => set(() => ({ organizations, selected })),
|
||||||
|
setSelected: (selected: Organization | undefined) =>
|
||||||
set(() => ({ selected })),
|
set(() => ({ selected })),
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
@ -29,36 +29,14 @@ import OrganizationLogo from "@/components/org/organization-logo";
|
|||||||
* @return the selector jsx
|
* @return the selector jsx
|
||||||
*/
|
*/
|
||||||
const OrganizationSelector = (): ReactElement => {
|
const OrganizationSelector = (): ReactElement => {
|
||||||
const selectedOrganization: string | undefined = useOrganizationContext(
|
|
||||||
(state: OrganizationState) => state.selected
|
|
||||||
);
|
|
||||||
const setSelectedOrganization = useOrganizationContext(
|
|
||||||
(state) => state.setSelected
|
|
||||||
);
|
|
||||||
const organizations: Organization[] = useOrganizationContext(
|
const organizations: Organization[] = useOrganizationContext(
|
||||||
(state: OrganizationState) => state.organizations
|
(state: OrganizationState) => state.organizations
|
||||||
);
|
);
|
||||||
|
const selected: Organization | undefined = useOrganizationContext(
|
||||||
|
(state: OrganizationState) => state.selected
|
||||||
|
);
|
||||||
|
const setSelected = useOrganizationContext((state) => state.setSelected);
|
||||||
const [open, setOpen] = useState<boolean>(false);
|
const [open, setOpen] = useState<boolean>(false);
|
||||||
const [selected, setSelected] = useState<Organization | undefined>();
|
|
||||||
|
|
||||||
// Set the selected organization
|
|
||||||
useEffect(() => {
|
|
||||||
const toSelect: Organization | undefined = organizations?.find(
|
|
||||||
(organization: Organization) => {
|
|
||||||
return organization.snowflake === selectedOrganization;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
// Update the state for this page
|
|
||||||
setSelected(
|
|
||||||
toSelect ||
|
|
||||||
(organizations?.length > 0 ? organizations[0] : undefined)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Update the state for all pages
|
|
||||||
if (!toSelect && organizations?.length > 0) {
|
|
||||||
setSelectedOrganization(organizations[0].snowflake);
|
|
||||||
}
|
|
||||||
}, [organizations, selectedOrganization, setSelectedOrganization]);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle selecting an organization.
|
* Handle selecting an organization.
|
||||||
@ -68,7 +46,6 @@ const OrganizationSelector = (): ReactElement => {
|
|||||||
const selectOrganization = (organization: Organization) => {
|
const selectOrganization = (organization: Organization) => {
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
setSelected(organization);
|
setSelected(organization);
|
||||||
setSelectedOrganization(organization.snowflake);
|
|
||||||
localStorage.setItem("selected-organization", organization.snowflake);
|
localStorage.setItem("selected-organization", organization.snowflake);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -122,8 +99,7 @@ const OrganizationSelector = (): ReactElement => {
|
|||||||
size="sm"
|
size="sm"
|
||||||
/>
|
/>
|
||||||
{organization.name}
|
{organization.name}
|
||||||
{organization.snowflake ===
|
{organization === selected && (
|
||||||
selectedOrganization && (
|
|
||||||
<CheckIcon className="absolute right-3.5 w-4 h-4" />
|
<CheckIcon className="absolute right-3.5 w-4 h-4" />
|
||||||
)}
|
)}
|
||||||
</CommandItem>
|
</CommandItem>
|
||||||
|
@ -17,6 +17,7 @@ import {
|
|||||||
import { useOrganizationContext } from "@/app/provider/organization-provider";
|
import { useOrganizationContext } from "@/app/provider/organization-provider";
|
||||||
import { OrganizationState } from "@/app/store/organization-store";
|
import { OrganizationState } from "@/app/store/organization-store";
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
|
import { Organization } from "@/app/types/org/organization";
|
||||||
|
|
||||||
const links: SidebarLink[] = [
|
const links: SidebarLink[] = [
|
||||||
{
|
{
|
||||||
@ -27,32 +28,32 @@ const links: SidebarLink[] = [
|
|||||||
{
|
{
|
||||||
name: "Status Pages",
|
name: "Status Pages",
|
||||||
icon: <ClipboardIcon />,
|
icon: <ClipboardIcon />,
|
||||||
href: "/dashboard/{org}/status-pages",
|
href: "/dashboard/status-pages",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Automations",
|
name: "Automations",
|
||||||
icon: <WrenchIcon />,
|
icon: <WrenchIcon />,
|
||||||
href: "/dashboard/{org}/automations",
|
href: "/dashboard/automations",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Incidents",
|
name: "Incidents",
|
||||||
icon: <FireIcon />,
|
icon: <FireIcon />,
|
||||||
href: "/dashboard/{org}/incidents",
|
href: "/dashboard/incidents",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Insights",
|
name: "Insights",
|
||||||
icon: <ChartBarSquareIcon />,
|
icon: <ChartBarSquareIcon />,
|
||||||
href: "/dashboard/{org}/insights",
|
href: "/dashboard/insights",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Audit Logs",
|
name: "Audit Logs",
|
||||||
icon: <PencilSquareIcon />,
|
icon: <PencilSquareIcon />,
|
||||||
href: "/dashboard/{org}/audit",
|
href: "/dashboard/audit",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Settings",
|
name: "Settings",
|
||||||
icon: <Cog6ToothIcon />,
|
icon: <Cog6ToothIcon />,
|
||||||
href: "/dashboard/{org}/settings",
|
href: "/dashboard/settings",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -63,18 +64,18 @@ const links: SidebarLink[] = [
|
|||||||
* @return the links jsx
|
* @return the links jsx
|
||||||
*/
|
*/
|
||||||
const Links = (): ReactElement => {
|
const Links = (): ReactElement => {
|
||||||
const selectedOrganization: string | undefined = useOrganizationContext(
|
const selectedOrganization: Organization | undefined =
|
||||||
(state: OrganizationState) => state.selected
|
useOrganizationContext((state: OrganizationState) => state.selected);
|
||||||
);
|
|
||||||
const path: string = usePathname();
|
const path: string = usePathname();
|
||||||
return (
|
return (
|
||||||
<div className="mt-3.5 w-full flex flex-col gap-0.5 select-none">
|
<div className="mt-3.5 w-full flex flex-col gap-0.5 select-none">
|
||||||
{links.map((link: SidebarLink, index: number) => {
|
{links
|
||||||
const href: string = link.href.replace(
|
.filter(
|
||||||
"{org}",
|
(link: SidebarLink, index: number) =>
|
||||||
selectedOrganization as string
|
index === 0 || (index > 0 && selectedOrganization)
|
||||||
);
|
)
|
||||||
const active: boolean = path.startsWith(href);
|
.map((link: SidebarLink, index: number) => {
|
||||||
|
const active: boolean = path.startsWith(link.href);
|
||||||
return (
|
return (
|
||||||
<SimpleTooltip
|
<SimpleTooltip
|
||||||
key={index}
|
key={index}
|
||||||
@ -86,9 +87,11 @@ const Links = (): ReactElement => {
|
|||||||
"px-3 py-2 flex gap-2 items-center text-sm rounded-lg hover:bg-zinc-800 transition-all transform-gpu",
|
"px-3 py-2 flex gap-2 items-center text-sm rounded-lg hover:bg-zinc-800 transition-all transform-gpu",
|
||||||
active && "font-medium bg-zinc-800"
|
active && "font-medium bg-zinc-800"
|
||||||
)}
|
)}
|
||||||
href={href}
|
href={link.href}
|
||||||
>
|
>
|
||||||
<div className="relative w-5 h-5">{link.icon}</div>
|
<div className="relative w-5 h-5">
|
||||||
|
{link.icon}
|
||||||
|
</div>
|
||||||
{link.name}
|
{link.name}
|
||||||
</Link>
|
</Link>
|
||||||
</SimpleTooltip>
|
</SimpleTooltip>
|
||||||
|
@ -34,7 +34,7 @@ const Device = ({
|
|||||||
setTimeSinceFirstLogin(
|
setTimeSinceFirstLogin(
|
||||||
DateTime.fromISO(device.firstLogin.toString()).toRelative()
|
DateTime.fromISO(device.firstLogin.toString()).toRelative()
|
||||||
);
|
);
|
||||||
}, 60000);
|
}, 1000);
|
||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
}, [device.firstLogin]);
|
}, [device.firstLogin]);
|
||||||
|
|
||||||
|
@ -9,6 +9,7 @@ import { Device as DeviceType } from "@/app/types/user/device";
|
|||||||
import { apiRequest } from "@/lib/api";
|
import { apiRequest } from "@/lib/api";
|
||||||
import Device from "@/components/dashboard/user/settings/device/device";
|
import Device from "@/components/dashboard/user/settings/device/device";
|
||||||
import { DateTime } from "luxon";
|
import { DateTime } from "luxon";
|
||||||
|
import DevicesSkeleton from "@/components/dashboard/user/settings/device/devices-skeleton";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The setting that allows a
|
* The setting that allows a
|
||||||
@ -52,23 +53,34 @@ const DevicesSetting = (): ReactElement => {
|
|||||||
|
|
||||||
{/* Setting */}
|
{/* Setting */}
|
||||||
<div className="w-[27.7rem] flex flex-col gap-2">
|
<div className="w-[27.7rem] flex flex-col gap-2">
|
||||||
{devices
|
{!devices ? (
|
||||||
?.sort(
|
<>
|
||||||
|
{Array.from({ length: 4 }, (_, index) => (
|
||||||
|
<DevicesSkeleton key={index} index={index} />
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
devices
|
||||||
|
.sort(
|
||||||
(a: DeviceType, b: DeviceType) =>
|
(a: DeviceType, b: DeviceType) =>
|
||||||
DateTime.fromISO(
|
DateTime.fromISO(
|
||||||
b.firstLogin.toString()
|
b.firstLogin.toString()
|
||||||
).toMillis() -
|
).toMillis() -
|
||||||
DateTime.fromISO(a.firstLogin.toString()).toMillis()
|
DateTime.fromISO(
|
||||||
|
a.firstLogin.toString()
|
||||||
|
).toMillis()
|
||||||
)
|
)
|
||||||
.map((device: DeviceType, index: number) => (
|
.map((device: DeviceType, index: number) => (
|
||||||
<Device
|
<Device
|
||||||
key={index}
|
key={index}
|
||||||
device={device}
|
device={device}
|
||||||
current={
|
current={
|
||||||
session?.snowflake === device.sessionSnowflake
|
session?.snowflake ===
|
||||||
|
device.sessionSnowflake
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
))}
|
))
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -0,0 +1,16 @@
|
|||||||
|
import { ReactElement } from "react";
|
||||||
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The skeleton to indicate the
|
||||||
|
* loading of the user's devices.
|
||||||
|
*
|
||||||
|
* @param index the skeleton index
|
||||||
|
* @return the skeleton jsx
|
||||||
|
*/
|
||||||
|
const DevicesSkeleton = ({ index }: { index: number }): ReactElement => (
|
||||||
|
<div style={{ opacity: 0.5 - 0.14 * index }}>
|
||||||
|
<Skeleton className="h-[4rem] rounded-lg" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
export default DevicesSkeleton;
|
15
src/components/ui/skeleton.tsx
Normal file
15
src/components/ui/skeleton.tsx
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
function Skeleton({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.HTMLAttributes<HTMLDivElement>) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn("animate-pulse rounded-md bg-zinc-300/10", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Skeleton };
|
Reference in New Issue
Block a user