148 lines
4.8 KiB
TypeScript
148 lines
4.8 KiB
TypeScript
import { Suspense } from "react";
|
|
import { getServerSession } from "next-auth";
|
|
import { redirect } from "next/navigation";
|
|
import {
|
|
Activity,
|
|
Database,
|
|
MessageCircle,
|
|
Music2,
|
|
RadioTower,
|
|
} from "lucide-react";
|
|
import { AdminShellServer } from "@/components/admin-shell-server";
|
|
import { Skeleton } from "@/components/ui/skeleton";
|
|
import {
|
|
SidebarGroup,
|
|
SidebarGroupContent,
|
|
SidebarMenu,
|
|
SidebarMenuButton,
|
|
SidebarMenuItem,
|
|
} from "@/components/ui/sidebar";
|
|
import {
|
|
Tooltip,
|
|
TooltipContent,
|
|
TooltipTrigger,
|
|
} from "@/components/ui/tooltip";
|
|
import { getAdminDiscordIds, getSessionDiscordId } from "@/lib/auth/auth";
|
|
import { authOptions } from "@/lib/auth/auth-options";
|
|
import { getAdminServiceHealth } from "@/lib/admin/service-health";
|
|
import { RobloxIcon, YoutubeIcon } from "@/lib/icons";
|
|
import { cn } from "@/lib/utils";
|
|
|
|
const iconByService = {
|
|
database: Database,
|
|
redis: RadioTower,
|
|
youtube: YoutubeIcon,
|
|
tiktok: Music2,
|
|
discord: MessageCircle,
|
|
roblox: RobloxIcon,
|
|
} as const;
|
|
|
|
async function AdminHealthStatus() {
|
|
const snapshot = await getAdminServiceHealth();
|
|
const unavailable = snapshot.services.filter(
|
|
(service) => service.status === "unavailable",
|
|
).length;
|
|
const summary = unavailable
|
|
? `${unavailable} ${unavailable === 1 ? "service" : "services"} unavailable`
|
|
: "All systems operational";
|
|
|
|
return (
|
|
<SidebarGroup className="pb-0">
|
|
<SidebarGroupContent>
|
|
<SidebarMenu>
|
|
<SidebarMenuItem>
|
|
<SidebarMenuButton size="lg" tooltip={summary} aria-label={summary}>
|
|
<Activity
|
|
className={cn(
|
|
"hidden group-data-[collapsible=icon]:block",
|
|
unavailable ? "text-destructive" : "text-emerald-500",
|
|
)}
|
|
/>
|
|
<div className="flex min-w-0 flex-1 items-center justify-between gap-2 group-data-[collapsible=icon]:hidden">
|
|
<div className="flex flex-col gap-1">
|
|
{(["core", "integration"] as const).map((category) => (
|
|
<div key={category} className="flex items-center gap-1">
|
|
{snapshot.services
|
|
.filter((service) => service.category === category)
|
|
.map((service) => {
|
|
const Icon =
|
|
iconByService[
|
|
service.id as keyof typeof iconByService
|
|
];
|
|
return (
|
|
<Tooltip key={service.id}>
|
|
<TooltipTrigger asChild>
|
|
<span
|
|
className={cn(
|
|
"flex [&>svg]:shrink-0",
|
|
category === "core"
|
|
? "[&>svg]:size-5"
|
|
: "[&>svg]:size-4",
|
|
service.status === "operational"
|
|
? "text-emerald-500"
|
|
: "text-destructive",
|
|
)}
|
|
aria-label={`${service.label}: ${service.status}`}
|
|
>
|
|
<Icon />
|
|
</span>
|
|
</TooltipTrigger>
|
|
<TooltipContent side="bottom">
|
|
{service.label}: {service.status}
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
);
|
|
})}
|
|
</div>
|
|
))}
|
|
</div>
|
|
<span className="truncate text-xs">{summary}</span>
|
|
</div>
|
|
</SidebarMenuButton>
|
|
</SidebarMenuItem>
|
|
</SidebarMenu>
|
|
</SidebarGroupContent>
|
|
</SidebarGroup>
|
|
);
|
|
}
|
|
|
|
function AdminHealthStatusSkeleton() {
|
|
return (
|
|
<SidebarGroup className="pb-0">
|
|
<SidebarGroupContent>
|
|
<SidebarMenu>
|
|
<SidebarMenuItem className="flex h-12 items-center gap-2 px-2">
|
|
<Skeleton className="size-4" />
|
|
<Skeleton className="h-3 flex-1" />
|
|
</SidebarMenuItem>
|
|
</SidebarMenu>
|
|
</SidebarGroupContent>
|
|
</SidebarGroup>
|
|
);
|
|
}
|
|
|
|
export default async function AdminLayout({
|
|
children,
|
|
}: {
|
|
children: React.ReactNode;
|
|
}) {
|
|
const session = await getServerSession(authOptions);
|
|
const discordId = getSessionDiscordId(session);
|
|
|
|
if (!discordId || !getAdminDiscordIds().includes(discordId)) {
|
|
redirect("/");
|
|
}
|
|
|
|
return (
|
|
<AdminShellServer
|
|
healthStatus={
|
|
<Suspense fallback={<AdminHealthStatusSkeleton />}>
|
|
<AdminHealthStatus />
|
|
</Suspense>
|
|
}
|
|
>
|
|
{children}
|
|
</AdminShellServer>
|
|
);
|
|
}
|