mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-10-24 20:53:06 +00:00
admin settings expandable pattern
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo } from "react";
|
||||
import { usePageSections } from "../../hooks/usePageSections";
|
||||
import { PageSectionItem } from "../../contexts/SidebarContext";
|
||||
import {
|
||||
GearIcon, FolderIcon, ArrowPathIcon,
|
||||
NoSymbolIcon, KeyIcon,
|
||||
DocumentTextIcon, SparklesIcon, ArrowDownTrayIcon,
|
||||
ChartBarIcon,
|
||||
BuildingOfficeIcon,
|
||||
} from "../../lib/sidebarIcons";
|
||||
|
||||
// Inline icon (same as MetricsSidebar had)
|
||||
function TrendingUpIcon({ className = "w-5 h-5" }: { className?: string }) {
|
||||
return (
|
||||
<svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M2.25 18L9 11.25l4.306 4.307a11.95 11.95 0 015.814-5.519l2.74-1.22m0 0l-5.94-2.28m5.94 2.28l-2.28 5.941" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
// Inline icon for Ops (wrench)
|
||||
function WrenchIcon({ className = "w-5 h-5" }: { className?: string }) {
|
||||
return (
|
||||
<svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M11.42 15.17L17.25 21A2.652 2.652 0 0021 17.25l-5.877-5.877M11.42 15.17l2.496-3.03c.317-.384.74-.626 1.208-.766M11.42 15.17l-4.655 5.653a2.548 2.548 0 11-3.586-3.586l6.837-5.63m5.108-.233c.55-.164 1.163-.188 1.743-.14a4.5 4.5 0 004.486-6.336l-3.276 3.277a3.004 3.004 0 01-2.25-2.25l3.276-3.276a4.5 4.5 0 00-6.336 4.486c.091 1.076-.071 2.264-.904 2.95l-.102.085m-1.745 1.437L5.909 7.5H4.5L2.25 3.75l1.5-1.5L7.5 4.5v1.409l4.26 4.26m-1.745 1.437l1.745-1.437" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
// Inline icon for Software (globe)
|
||||
function GlobeIcon({ className = "w-5 h-5" }: { className?: string }) {
|
||||
return (
|
||||
<svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M12 21a9.004 9.004 0 008.716-6.747M12 21a9.004 9.004 0 01-8.716-6.747M12 21c2.485 0 4.5-4.03 4.5-9S14.485 3 12 3m0 18c-2.485 0-4.5-4.03-4.5-9S9.515 3 12 3m0 0a8.997 8.997 0 017.843 4.582M12 3a8.997 8.997 0 00-7.843 4.582m15.686 0A11.953 11.953 0 0112 10.5c-2.998 0-5.74-1.1-7.843-2.918m15.686 0A8.959 8.959 0 0121 12c0 .778-.099 1.533-.284 2.253m0 0A17.919 17.919 0 0112 16.5c-3.162 0-6.133-.815-8.716-2.247m0 0A9.015 9.015 0 013 12c0-1.605.42-3.113 1.157-4.418" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers all admin sections in the global sidebar
|
||||
* with group information for expandable rendering.
|
||||
* Placed inside AdminConfigProvider in the admin layout.
|
||||
*/
|
||||
export function AdminPageSections() {
|
||||
const items: PageSectionItem[] = useMemo(() => [
|
||||
// Tenants
|
||||
{ id: "management", label: "Tenant Management", icon: <BuildingOfficeIcon />, href: "/admin/tenants/management", group: "Tenants" },
|
||||
{ id: "config-report", label: "Config Report", icon: <DocumentTextIcon />, href: "/admin/tenants/config-report", group: "Tenants" },
|
||||
|
||||
// Metrics
|
||||
{ id: "agent-metrics", label: "Agent Metrics", icon: <ChartBarIcon />, href: "/admin/metrics/agent-metrics", group: "Metrics" },
|
||||
{ id: "usage", label: "Platform Usage", icon: <TrendingUpIcon />, href: "/admin/metrics/usage", group: "Metrics" },
|
||||
|
||||
// Reports
|
||||
{ id: "session-reports", label: "Session Reports", icon: <DocumentTextIcon />, href: "/admin/reports/session-reports", group: "Reports" },
|
||||
{ id: "user-feedback", label: "User Feedback", icon: <SparklesIcon />, href: "/admin/reports/user-feedback", group: "Reports" },
|
||||
{ id: "session-export", label: "Session Export", icon: <ArrowDownTrayIcon />, href: "/admin/reports/session-export", group: "Reports" },
|
||||
|
||||
// Security
|
||||
{ id: "device-block", label: "Device Block", icon: <NoSymbolIcon />, href: "/admin/security/device-block", group: "Security" },
|
||||
{ id: "version-block", label: "Version Block", icon: <NoSymbolIcon />, href: "/admin/security/version-block", group: "Security" },
|
||||
{ id: "vulnerability-data", label: "Vulnerability Data", icon: <KeyIcon />, href: "/admin/security/vulnerability-data", group: "Security" },
|
||||
|
||||
// Settings
|
||||
{ id: "global", label: "Global Settings", icon: <GearIcon />, href: "/admin/settings/global", group: "Settings" },
|
||||
{ id: "diagnostics-log-paths", label: "Diagnostics Log Paths", icon: <FolderIcon />, href: "/admin/settings/diagnostics-log-paths", group: "Settings" },
|
||||
{ id: "config-reseed", label: "Config Reseed", icon: <ArrowPathIcon />, href: "/admin/settings/config-reseed", group: "Settings" },
|
||||
|
||||
// Ops (single page, no sub-items — still in a group for consistency)
|
||||
{ id: "ops", label: "Maintenance", icon: <WrenchIcon />, href: "/admin/ops", group: "Ops" },
|
||||
|
||||
// Software (single page)
|
||||
{ id: "software", label: "Software Mapping", icon: <GlobeIcon />, href: "/admin/software", group: "Software" },
|
||||
], []);
|
||||
|
||||
usePageSections(items, "Galactic Admin", "route");
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import { useEffect } from "react";
|
||||
import { ProtectedRoute } from "../../components/ProtectedRoute";
|
||||
import { useAuth } from "../../contexts/AuthContext";
|
||||
import { AdminConfigProvider } from "./AdminConfigContext";
|
||||
import { AdminPageSections } from "./AdminPageSections";
|
||||
|
||||
export default function AdminLayout({ children }: { children: React.ReactNode }) {
|
||||
const { user } = useAuth();
|
||||
@@ -21,6 +22,7 @@ export default function AdminLayout({ children }: { children: React.ReactNode })
|
||||
return (
|
||||
<ProtectedRoute requireGalacticAdmin>
|
||||
<AdminConfigProvider>
|
||||
<AdminPageSections />
|
||||
{children}
|
||||
</AdminConfigProvider>
|
||||
</ProtectedRoute>
|
||||
|
||||
@@ -1,44 +1,14 @@
|
||||
"use client";
|
||||
|
||||
import { usePageSections } from "../../../hooks/usePageSections";
|
||||
import { PageSectionItem } from "../../../contexts/SidebarContext";
|
||||
import { METRICS_NAV_SECTIONS } from "./metricsNavSections";
|
||||
import { ChartBarIcon } from "../../../lib/sidebarIcons";
|
||||
|
||||
function TrendingUpIcon({ className = "w-5 h-5" }: { className?: string }) {
|
||||
return (
|
||||
<svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M2.25 18L9 11.25l4.306 4.307a11.95 11.95 0 015.814-5.519l2.74-1.22m0 0l-5.94-2.28m5.94 2.28l-2.28 5.941" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
const SECTION_ICONS: Record<string, React.ReactNode> = {
|
||||
"agent-metrics": <ChartBarIcon />,
|
||||
"usage": <TrendingUpIcon />,
|
||||
};
|
||||
|
||||
const metricsItems: PageSectionItem[] = METRICS_NAV_SECTIONS.map((s) => ({
|
||||
id: s.id,
|
||||
label: s.label,
|
||||
icon: SECTION_ICONS[s.id],
|
||||
href: `/admin/metrics/${s.id}`,
|
||||
}));
|
||||
|
||||
export function MetricsSidebar({ children }: { children: React.ReactNode }) {
|
||||
usePageSections(metricsItems, "Metrics", "route");
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Page header */}
|
||||
<header className="bg-white shadow">
|
||||
<div className="py-6 px-4 sm:px-6 lg:px-8">
|
||||
<h1 className="text-2xl font-normal text-gray-900">Metrics</h1>
|
||||
<p className="text-sm text-gray-500 mt-1">Platform-wide analytics</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Content */}
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8 space-y-8">
|
||||
{children}
|
||||
</div>
|
||||
|
||||
@@ -1,37 +1,14 @@
|
||||
"use client";
|
||||
|
||||
import { usePageSections } from "../../../hooks/usePageSections";
|
||||
import { PageSectionItem } from "../../../contexts/SidebarContext";
|
||||
import { REPORTS_NAV_SECTIONS } from "./reportsNavSections";
|
||||
import { DocumentTextIcon, SparklesIcon, ArrowDownTrayIcon } from "../../../lib/sidebarIcons";
|
||||
|
||||
const SECTION_ICONS: Record<string, React.ReactNode> = {
|
||||
"session-reports": <DocumentTextIcon />,
|
||||
"user-feedback": <SparklesIcon />,
|
||||
"session-export": <ArrowDownTrayIcon />,
|
||||
};
|
||||
|
||||
const reportsItems: PageSectionItem[] = REPORTS_NAV_SECTIONS.map((s) => ({
|
||||
id: s.id,
|
||||
label: s.label,
|
||||
icon: SECTION_ICONS[s.id],
|
||||
href: `/admin/reports/${s.id}`,
|
||||
}));
|
||||
|
||||
export function ReportsSidebar({ children }: { children: React.ReactNode }) {
|
||||
usePageSections(reportsItems, "Reports", "route");
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Page header */}
|
||||
<header className="bg-white shadow">
|
||||
<div className="py-6 px-4 sm:px-6 lg:px-8">
|
||||
<h1 className="text-2xl font-normal text-gray-900">Reports</h1>
|
||||
<p className="text-sm text-gray-500 mt-1">Session reports and user feedback</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Content */}
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8 space-y-8">
|
||||
{children}
|
||||
</div>
|
||||
|
||||
@@ -1,37 +1,14 @@
|
||||
"use client";
|
||||
|
||||
import { usePageSections } from "../../../hooks/usePageSections";
|
||||
import { PageSectionItem } from "../../../contexts/SidebarContext";
|
||||
import { SECURITY_NAV_SECTIONS } from "./securityNavSections";
|
||||
import { NoSymbolIcon, KeyIcon } from "../../../lib/sidebarIcons";
|
||||
|
||||
const SECTION_ICONS: Record<string, React.ReactNode> = {
|
||||
"device-block": <NoSymbolIcon />,
|
||||
"version-block": <NoSymbolIcon />,
|
||||
"vulnerability-data": <KeyIcon />,
|
||||
};
|
||||
|
||||
const securityItems: PageSectionItem[] = SECURITY_NAV_SECTIONS.map((s) => ({
|
||||
id: s.id,
|
||||
label: s.label,
|
||||
icon: SECTION_ICONS[s.id],
|
||||
href: `/admin/security/${s.id}`,
|
||||
}));
|
||||
|
||||
export function SecuritySidebar({ children }: { children: React.ReactNode }) {
|
||||
usePageSections(securityItems, "Security", "route");
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Page header */}
|
||||
<header className="bg-white shadow">
|
||||
<div className="py-6 px-4 sm:px-6 lg:px-8">
|
||||
<h1 className="text-2xl font-normal text-gray-900">Security</h1>
|
||||
<p className="text-sm text-gray-500 mt-1">Device blocking and vulnerability management</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Content */}
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8 space-y-8">
|
||||
{children}
|
||||
</div>
|
||||
|
||||
@@ -1,37 +1,14 @@
|
||||
"use client";
|
||||
|
||||
import { usePageSections } from "../../../hooks/usePageSections";
|
||||
import { PageSectionItem } from "../../../contexts/SidebarContext";
|
||||
import { SETTINGS_NAV_SECTIONS } from "./settingsNavSections";
|
||||
import { GearIcon, FolderIcon, ArrowPathIcon } from "../../../lib/sidebarIcons";
|
||||
|
||||
const SECTION_ICONS: Record<string, React.ReactNode> = {
|
||||
"global": <GearIcon />,
|
||||
"diagnostics-log-paths": <FolderIcon />,
|
||||
"config-reseed": <ArrowPathIcon />,
|
||||
};
|
||||
|
||||
const settingsItems: PageSectionItem[] = SETTINGS_NAV_SECTIONS.map((s) => ({
|
||||
id: s.id,
|
||||
label: s.label,
|
||||
icon: SECTION_ICONS[s.id],
|
||||
href: `/admin/settings/${s.id}`,
|
||||
}));
|
||||
|
||||
export function SettingsSidebar({ children }: { children: React.ReactNode }) {
|
||||
usePageSections(settingsItems, "Settings", "route");
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Page header */}
|
||||
<header className="bg-white shadow">
|
||||
<div className="py-6 px-4 sm:px-6 lg:px-8">
|
||||
<h1 className="text-2xl font-normal text-gray-900">Settings</h1>
|
||||
<p className="text-sm text-gray-500 mt-1">Global platform configuration</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Content */}
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8 space-y-8">
|
||||
{children}
|
||||
</div>
|
||||
|
||||
@@ -1,39 +1,14 @@
|
||||
"use client";
|
||||
|
||||
import { usePageSections } from "../../../hooks/usePageSections";
|
||||
import { PageSectionItem } from "../../../contexts/SidebarContext";
|
||||
import { TENANTS_NAV_SECTIONS } from "./tenantsNavSections";
|
||||
import {
|
||||
BuildingOfficeIcon,
|
||||
DocumentTextIcon,
|
||||
} from "../../../lib/sidebarIcons";
|
||||
|
||||
const SECTION_ICONS: Record<string, React.ReactNode> = {
|
||||
"management": <BuildingOfficeIcon />,
|
||||
"config-report": <DocumentTextIcon />,
|
||||
};
|
||||
|
||||
const tenantsItems: PageSectionItem[] = TENANTS_NAV_SECTIONS.map((s) => ({
|
||||
id: s.id,
|
||||
label: s.label,
|
||||
icon: SECTION_ICONS[s.id],
|
||||
href: `/admin/tenants/${s.id}`,
|
||||
}));
|
||||
|
||||
export function TenantsSidebar({ children }: { children: React.ReactNode }) {
|
||||
usePageSections(tenantsItems, "Tenants", "route");
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Page header */}
|
||||
<header className="bg-white shadow">
|
||||
<div className="py-6 px-4 sm:px-6 lg:px-8">
|
||||
<h1 className="text-2xl font-normal text-gray-900">Tenants</h1>
|
||||
<p className="mt-1 text-sm text-gray-500">Tenant management and configuration</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Content */}
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8 space-y-8">
|
||||
{children}
|
||||
</div>
|
||||
|
||||
@@ -102,9 +102,15 @@ export function GlobalSidebar({ children }: { children: ReactNode }) {
|
||||
// Route mode for page sections
|
||||
useEffect(() => {
|
||||
if (pageSectionsMode !== "route" || pageSections.length === 0) return;
|
||||
const segment = pathname.split("/").pop() ?? "";
|
||||
const match = pageSections.find((item) => item.id === segment);
|
||||
if (match) setActiveSectionId(match.id);
|
||||
// Match by href first (more precise), fall back to last segment matching id
|
||||
const hrefMatch = pageSections.find((item) => item.href && pathname === item.href);
|
||||
if (hrefMatch) {
|
||||
setActiveSectionId(hrefMatch.id);
|
||||
} else {
|
||||
const segment = pathname.split("/").pop() ?? "";
|
||||
const match = pageSections.find((item) => item.id === segment);
|
||||
if (match) setActiveSectionId(match.id);
|
||||
}
|
||||
}, [pathname, pageSections, pageSectionsMode]);
|
||||
|
||||
const scrollTo = useCallback((id: string) => {
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user