misc changes
This commit is contained in:
@ -10,7 +10,7 @@ import UserSettingsHeader from "@/components/dashboard/user/user-settings-header
|
||||
* @return the page jsx
|
||||
*/
|
||||
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" />
|
||||
|
||||
{/* Content */}
|
||||
|
@ -14,7 +14,7 @@ import UserSettingsHeader from "@/components/dashboard/user/user-settings-header
|
||||
* @return the page jsx
|
||||
*/
|
||||
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" />
|
||||
|
||||
{/* Content */}
|
||||
|
@ -12,7 +12,7 @@ import DevicesSetting from "@/components/dashboard/user/settings/device/devices-
|
||||
* @return the page jsx
|
||||
*/
|
||||
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" />
|
||||
|
||||
{/* Content */}
|
||||
|
@ -37,20 +37,24 @@ const OrganizationProvider = ({ children }: { children: ReactNode }) => {
|
||||
* Fetch the organizations for the logged in user.
|
||||
*/
|
||||
const fetchOrganizations = useCallback(async () => {
|
||||
let selectedOrganization: string | null = localStorage.getItem(
|
||||
"selected-organization"
|
||||
);
|
||||
const { data, error } = await apiRequest<Organization[]>({
|
||||
endpoint: "/organization/@me",
|
||||
session,
|
||||
});
|
||||
const selectedOrgSnowflake: string | null = localStorage.getItem(
|
||||
"selected-organization"
|
||||
);
|
||||
const organizations: Organization[] = data as Organization[];
|
||||
if (!selectedOrganization && organizations.length > 0) {
|
||||
selectedOrganization = organizations[0].snowflake;
|
||||
let selected: Organization | undefined;
|
||||
if (!selected && organizations.length > 0) {
|
||||
selected = organizations[0];
|
||||
} else {
|
||||
selected = organizations.find(
|
||||
(organization: Organization) =>
|
||||
organization.snowflake === selectedOrgSnowflake
|
||||
);
|
||||
}
|
||||
storeRef.current
|
||||
?.getState()
|
||||
.update(selectedOrganization || undefined, organizations);
|
||||
storeRef.current?.getState().update(organizations, selected);
|
||||
}, [session]);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -13,15 +13,15 @@ export const OrganizationContext = createContext<OrganizationStore | null>(
|
||||
* The props in this store.
|
||||
*/
|
||||
export type OrganizationStoreProps = {
|
||||
/**
|
||||
* The currently selected organization.
|
||||
*/
|
||||
selected: string | undefined;
|
||||
|
||||
/**
|
||||
* The organization's the user has.
|
||||
*/
|
||||
organizations: Organization[];
|
||||
|
||||
/**
|
||||
* The currently selected organization.
|
||||
*/
|
||||
selected: Organization | undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
@ -31,12 +31,12 @@ export type OrganizationState = OrganizationStoreProps & {
|
||||
/**
|
||||
* Update the state.
|
||||
*
|
||||
* @param selected the selected organization
|
||||
* @param organizations the user's organizations
|
||||
* @param selected the selected organization
|
||||
*/
|
||||
update: (
|
||||
selected: string | undefined,
|
||||
organizations: Organization[]
|
||||
organizations: Organization[],
|
||||
selected: Organization | undefined
|
||||
) => void;
|
||||
|
||||
/**
|
||||
@ -44,7 +44,7 @@ export type OrganizationState = OrganizationStoreProps & {
|
||||
*
|
||||
* @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) => ({
|
||||
...defaultProps,
|
||||
update: (selected: string | undefined, organizations: Organization[]) =>
|
||||
set(() => ({ selected, organizations })),
|
||||
setSelected: (selected: string | undefined) =>
|
||||
update: (
|
||||
organizations: Organization[],
|
||||
selected: Organization | undefined
|
||||
) => set(() => ({ organizations, selected })),
|
||||
setSelected: (selected: Organization | undefined) =>
|
||||
set(() => ({ selected })),
|
||||
}));
|
||||
};
|
||||
|
@ -29,36 +29,14 @@ import OrganizationLogo from "@/components/org/organization-logo";
|
||||
* @return the selector jsx
|
||||
*/
|
||||
const OrganizationSelector = (): ReactElement => {
|
||||
const selectedOrganization: string | undefined = useOrganizationContext(
|
||||
(state: OrganizationState) => state.selected
|
||||
);
|
||||
const setSelectedOrganization = useOrganizationContext(
|
||||
(state) => state.setSelected
|
||||
);
|
||||
const organizations: Organization[] = useOrganizationContext(
|
||||
(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 [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.
|
||||
@ -68,7 +46,6 @@ const OrganizationSelector = (): ReactElement => {
|
||||
const selectOrganization = (organization: Organization) => {
|
||||
setOpen(false);
|
||||
setSelected(organization);
|
||||
setSelectedOrganization(organization.snowflake);
|
||||
localStorage.setItem("selected-organization", organization.snowflake);
|
||||
};
|
||||
|
||||
@ -122,8 +99,7 @@ const OrganizationSelector = (): ReactElement => {
|
||||
size="sm"
|
||||
/>
|
||||
{organization.name}
|
||||
{organization.snowflake ===
|
||||
selectedOrganization && (
|
||||
{organization === selected && (
|
||||
<CheckIcon className="absolute right-3.5 w-4 h-4" />
|
||||
)}
|
||||
</CommandItem>
|
||||
|
@ -17,6 +17,7 @@ import {
|
||||
import { useOrganizationContext } from "@/app/provider/organization-provider";
|
||||
import { OrganizationState } from "@/app/store/organization-store";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { Organization } from "@/app/types/org/organization";
|
||||
|
||||
const links: SidebarLink[] = [
|
||||
{
|
||||
@ -27,32 +28,32 @@ const links: SidebarLink[] = [
|
||||
{
|
||||
name: "Status Pages",
|
||||
icon: <ClipboardIcon />,
|
||||
href: "/dashboard/{org}/status-pages",
|
||||
href: "/dashboard/status-pages",
|
||||
},
|
||||
{
|
||||
name: "Automations",
|
||||
icon: <WrenchIcon />,
|
||||
href: "/dashboard/{org}/automations",
|
||||
href: "/dashboard/automations",
|
||||
},
|
||||
{
|
||||
name: "Incidents",
|
||||
icon: <FireIcon />,
|
||||
href: "/dashboard/{org}/incidents",
|
||||
href: "/dashboard/incidents",
|
||||
},
|
||||
{
|
||||
name: "Insights",
|
||||
icon: <ChartBarSquareIcon />,
|
||||
href: "/dashboard/{org}/insights",
|
||||
href: "/dashboard/insights",
|
||||
},
|
||||
{
|
||||
name: "Audit Logs",
|
||||
icon: <PencilSquareIcon />,
|
||||
href: "/dashboard/{org}/audit",
|
||||
href: "/dashboard/audit",
|
||||
},
|
||||
{
|
||||
name: "Settings",
|
||||
icon: <Cog6ToothIcon />,
|
||||
href: "/dashboard/{org}/settings",
|
||||
href: "/dashboard/settings",
|
||||
},
|
||||
];
|
||||
|
||||
@ -63,37 +64,39 @@ const links: SidebarLink[] = [
|
||||
* @return the links jsx
|
||||
*/
|
||||
const Links = (): ReactElement => {
|
||||
const selectedOrganization: string | undefined = useOrganizationContext(
|
||||
(state: OrganizationState) => state.selected
|
||||
);
|
||||
const selectedOrganization: Organization | undefined =
|
||||
useOrganizationContext((state: OrganizationState) => state.selected);
|
||||
const path: string = usePathname();
|
||||
return (
|
||||
<div className="mt-3.5 w-full flex flex-col gap-0.5 select-none">
|
||||
{links.map((link: SidebarLink, index: number) => {
|
||||
const href: string = link.href.replace(
|
||||
"{org}",
|
||||
selectedOrganization as string
|
||||
);
|
||||
const active: boolean = path.startsWith(href);
|
||||
return (
|
||||
<SimpleTooltip
|
||||
key={index}
|
||||
content={`Visit ${link.name}`}
|
||||
side="right"
|
||||
>
|
||||
<Link
|
||||
className={cn(
|
||||
"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"
|
||||
)}
|
||||
href={href}
|
||||
{links
|
||||
.filter(
|
||||
(link: SidebarLink, index: number) =>
|
||||
index === 0 || (index > 0 && selectedOrganization)
|
||||
)
|
||||
.map((link: SidebarLink, index: number) => {
|
||||
const active: boolean = path.startsWith(link.href);
|
||||
return (
|
||||
<SimpleTooltip
|
||||
key={index}
|
||||
content={`Visit ${link.name}`}
|
||||
side="right"
|
||||
>
|
||||
<div className="relative w-5 h-5">{link.icon}</div>
|
||||
{link.name}
|
||||
</Link>
|
||||
</SimpleTooltip>
|
||||
);
|
||||
})}
|
||||
<Link
|
||||
className={cn(
|
||||
"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"
|
||||
)}
|
||||
href={link.href}
|
||||
>
|
||||
<div className="relative w-5 h-5">
|
||||
{link.icon}
|
||||
</div>
|
||||
{link.name}
|
||||
</Link>
|
||||
</SimpleTooltip>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -34,7 +34,7 @@ const Device = ({
|
||||
setTimeSinceFirstLogin(
|
||||
DateTime.fromISO(device.firstLogin.toString()).toRelative()
|
||||
);
|
||||
}, 60000);
|
||||
}, 1000);
|
||||
return () => clearInterval(interval);
|
||||
}, [device.firstLogin]);
|
||||
|
||||
|
@ -9,6 +9,7 @@ import { Device as DeviceType } from "@/app/types/user/device";
|
||||
import { apiRequest } from "@/lib/api";
|
||||
import Device from "@/components/dashboard/user/settings/device/device";
|
||||
import { DateTime } from "luxon";
|
||||
import DevicesSkeleton from "@/components/dashboard/user/settings/device/devices-skeleton";
|
||||
|
||||
/**
|
||||
* The setting that allows a
|
||||
@ -52,23 +53,34 @@ const DevicesSetting = (): ReactElement => {
|
||||
|
||||
{/* Setting */}
|
||||
<div className="w-[27.7rem] flex flex-col gap-2">
|
||||
{devices
|
||||
?.sort(
|
||||
(a: DeviceType, b: DeviceType) =>
|
||||
DateTime.fromISO(
|
||||
b.firstLogin.toString()
|
||||
).toMillis() -
|
||||
DateTime.fromISO(a.firstLogin.toString()).toMillis()
|
||||
)
|
||||
.map((device: DeviceType, index: number) => (
|
||||
<Device
|
||||
key={index}
|
||||
device={device}
|
||||
current={
|
||||
session?.snowflake === device.sessionSnowflake
|
||||
}
|
||||
/>
|
||||
))}
|
||||
{!devices ? (
|
||||
<>
|
||||
{Array.from({ length: 4 }, (_, index) => (
|
||||
<DevicesSkeleton key={index} index={index} />
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
devices
|
||||
.sort(
|
||||
(a: DeviceType, b: DeviceType) =>
|
||||
DateTime.fromISO(
|
||||
b.firstLogin.toString()
|
||||
).toMillis() -
|
||||
DateTime.fromISO(
|
||||
a.firstLogin.toString()
|
||||
).toMillis()
|
||||
)
|
||||
.map((device: DeviceType, index: number) => (
|
||||
<Device
|
||||
key={index}
|
||||
device={device}
|
||||
current={
|
||||
session?.snowflake ===
|
||||
device.sessionSnowflake
|
||||
}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</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