MicProtocol / apps /admin /src /components /shell /AdminSidebar.tsx
Benaah's picture
Upload folder using huggingface_hub
fecff63 verified
Raw History Blame Contribute Delete
8.78 kB
"use client";
import React from "react";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { AdminPermission } from "@micprotocol/shared-types";
import { trpc } from "@/lib/trpc";
import {
LayoutDashboard,
Server,
Radio,
Layers,
BrainCircuit,
Plug,
Webhook,
Activity,
Gauge,
TerminalSquare,
Bug,
Building2,
ShieldCheck,
ShieldAlert,
Users,
MessageSquare,
FlaskConical,
Microscope,
ChevronDown,
BarChart,
FileText,
Sliders,
} from "lucide-react";
interface NavItem {
href: string;
label: string;
icon: React.ComponentType<{ className?: string }>;
badge?: string;
/** Platform permission required to see this entry (absent = always visible). */
permission?: AdminPermission;
children?: NavItem[];
}
const primaryNav: NavItem[] = [
{ href: "/", label: "Overview", icon: LayoutDashboard, permission: AdminPermission.ANALYTICS_READ },
{ href: "/infrastructure", label: "Infrastructure", icon: Server, permission: AdminPermission.OPS_READ },
{ href: "/realtime", label: "Realtime Engine", icon: Radio, permission: AdminPermission.OPS_READ },
{ href: "/queues", label: "Queues", icon: Layers, permission: AdminPermission.OPS_READ },
{
href: "/ai-engines",
label: "AI Engines",
icon: BrainCircuit,
permission: AdminPermission.OPS_READ,
children: [
{ href: "/ai-engines", label: "Overview", icon: Microscope, permission: AdminPermission.OPS_READ },
{ href: "/ai-engines/clip-scorer", label: "CLIP Scorer", icon: FlaskConical, permission: AdminPermission.OPS_READ },
],
},
{
href: "/integrations",
label: "Integrations",
icon: Plug,
permission: AdminPermission.SETTINGS_READ,
children: [
{ href: "/integrations", label: "Services", icon: Plug, permission: AdminPermission.SETTINGS_READ },
{ href: "/integrations/credentials", label: "Credentials", icon: ShieldCheck, permission: AdminPermission.SETTINGS_READ },
],
},
{ href: "/webhooks", label: "Webhooks", icon: Webhook, permission: AdminPermission.OPS_READ },
{
href: "/observability",
label: "Observability",
icon: Activity,
permission: AdminPermission.OPS_READ,
children: [
{ href: "/observability/traces", label: "Traces", icon: Activity, permission: AdminPermission.OPS_READ },
{ href: "/observability/metrics", label: "Metrics", icon: Gauge, permission: AdminPermission.OPS_READ },
{ href: "/observability/logs", label: "Live Logs", icon: TerminalSquare, permission: AdminPermission.OPS_READ },
{ href: "/observability/errors", label: "Errors", icon: Bug, permission: AdminPermission.OPS_READ },
],
},
{ href: "/tenants", label: "Tenants", icon: Building2, permission: AdminPermission.WORKSPACES_READ },
{ href: "/verification", label: "Verification", icon: ShieldCheck, permission: AdminPermission.MODERATION_READ },
{
href: "/crm",
label: "CRM",
icon: Users,
permission: AdminPermission.USERS_READ,
children: [
{ href: "/crm/users", label: "Users", icon: Users, permission: AdminPermission.USERS_READ },
{ href: "/crm/guests", label: "Guests", icon: Users, permission: AdminPermission.WORKSPACES_READ },
{ href: "/crm/inquiries", label: "Inquiries", icon: MessageSquare, permission: AdminPermission.SUPPORT_READ },
],
},
{
href: "/users",
label: "Platform Users",
icon: Users,
permission: AdminPermission.USERS_READ,
},
{
href: "/workspaces",
label: "Platform Workspaces",
icon: Building2,
permission: AdminPermission.WORKSPACES_READ,
},
{ href: "/settings", label: "Settings & Flags", icon: Sliders, permission: AdminPermission.SETTINGS_READ },
{ href: "/moderation", label: "Moderation", icon: ShieldAlert, permission: AdminPermission.MODERATION_READ },
{ href: "/supervision", label: "Supervision", icon: Gauge, permission: AdminPermission.SUPERVISION_READ },
{ href: "/synthetic-agents", label: "Synthetic Agents", icon: FlaskConical, permission: AdminPermission.OPS_READ },
{ href: "/audit-logs", label: "Audit Logs", icon: FileText, permission: AdminPermission.AUDIT_READ },
{ href: "/security-policy", label: "Security Policy", icon: ShieldAlert, permission: AdminPermission.SECURITY_POLICY_READ },
{ href: "/ratings", label: "Ratings", icon: BarChart, permission: AdminPermission.MODERATION_READ },
{ href: "/surveys", label: "Surveys", icon: FileText, permission: AdminPermission.ANNOUNCEMENTS_READ },
{ href: "/support", label: "Support", icon: MessageSquare, permission: AdminPermission.SUPPORT_READ },
];
function filterNav(items: NavItem[], has: (p: AdminPermission) => boolean): NavItem[] {
const out: NavItem[] = [];
for (const item of items) {
const ownOk = !item.permission || has(item.permission);
if (item.children) {
const children = filterNav(item.children, has);
if (children.length > 0) {
out.push({ ...item, children });
} else if (ownOk) {
const { children: _dropped, ...rest } = item;
out.push(rest);
}
} else if (ownOk) {
out.push(item);
}
}
return out;
}
function NavItemLink({ item, depth = 0 }: { item: NavItem; depth?: number }) {
const pathname = usePathname();
const [open, setOpen] = React.useState(
() => (item.children?.some((c) => pathname.startsWith(c.href)) ?? false),
);
const active = pathname === item.href || (item.href !== "/" && pathname.startsWith(item.href));
if (item.children) {
const hasActiveChild = item.children.some((c) => pathname.startsWith(c.href));
return (
<div className={hasActiveChild || open ? "" : ""}>
<button
onClick={() => setOpen((o) => !o)}
className={`group flex w-full items-center gap-3 rounded-lg px-3 py-2 text-sm transition-colors ${
hasActiveChild
? "bg-[var(--bg-tertiary)] text-[var(--color-primary)]"
: "text-[var(--text-secondary)] hover:bg-[var(--bg-tertiary)] hover:text-[var(--text-primary)]"
}`}
>
<item.icon className="size-4 shrink-0" />
<span className="flex-1 text-left">{item.label}</span>
<ChevronDown className={`size-4 transition-transform ${open ? "rotate-180" : ""}`} />
</button>
{open && (
<div className="mt-1 space-y-1 border-l border-[var(--border-color)] pl-3 ml-3">
{item.children.map((child) => (
<NavItemLink key={child.href} item={child} depth={depth + 1} />
))}
</div>
)}
</div>
);
}
return (
<Link
href={item.href}
className={`flex items-center gap-3 rounded-lg px-3 py-2 text-sm transition-colors ${
active
? "bg-[var(--bg-tertiary)] text-[var(--color-primary)]"
: "text-[var(--text-secondary)] hover:bg-[var(--bg-tertiary)] hover:text-[var(--text-primary)]"
}`}
>
<item.icon className="size-4 shrink-0" />
<span className="flex-1">{item.label}</span>
</Link>
);
}
export function AdminSidebar() {
const meQuery = trpc.admin.me.useQuery(undefined, { retry: false });
const permissions = meQuery.data?.permissions;
const permsSet = React.useMemo(
() => (permissions ? new Set<string>(permissions) : null),
[permissions],
);
const nav = React.useMemo(
() =>
permsSet
? filterNav(primaryNav, (p) => permsSet.has(p))
: primaryNav,
[permsSet],
);
return (
<aside className="flex h-full w-60 shrink-0 flex-col border-r border-[var(--border-color)] bg-[var(--bg-secondary)]">
<div className="flex h-16 items-center gap-2.5 border-b border-[var(--border-color)] px-5">
<img src="/favicon.svg" alt="MicProtocol logo" className="size-7" />
<div>
<div className="font-display text-sm font-semibold text-[var(--text-primary)]">MicProtocol</div>
<div className="text-[11px] uppercase tracking-widest text-[var(--color-primary)]">Admin</div>
</div>
</div>
<nav className="flex-1 space-y-1 overflow-y-auto px-3 py-4">
{nav.map((item) => (
<NavItemLink key={item.href} item={item} />
))}
{permsSet && nav.length === 0 && (
<p className="px-3 py-2 text-xs text-[var(--text-tertiary)]">
Your role has no console permissions assigned. Contact a SUPER_ADMIN.
</p>
)}
</nav>
<div className="border-t border-[var(--border-color)] p-4 text-xs text-[var(--text-tertiary)]">
MicProtocol Admin Panel
<br />
v0.1.0
{meQuery.data?.role && (
<>
<br />
Role: {meQuery.data.role}
</>
)}
</div>
</aside>
);
}