re-org sidebar

This commit is contained in:
Oliver Kieselbach
2026-03-23 05:59:28 +01:00
parent d59ce5b574
commit 5226df08d1
7 changed files with 291 additions and 256 deletions
@@ -5,7 +5,6 @@ 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();
@@ -22,7 +21,6 @@ export default function AdminLayout({ children }: { children: React.ReactNode })
return (
<ProtectedRoute requireGalacticAdmin>
<AdminConfigProvider>
<AdminPageSections />
{children}
</AdminConfigProvider>
</ProtectedRoute>
@@ -1,22 +1,14 @@
"use client";
import { useEffect, useState, useRef } from "react";
import { useTenant } from "../../../contexts/TenantContext";
import { useAuth } from "../../../contexts/AuthContext";
import { API_BASE_URL } from "@/lib/config";
import { authenticatedFetch, TokenExpiredError } from "@/lib/authenticatedFetch";
export function SettingsMenu({
apiStatus,
onCheckHealth,
adminMode,
onAdminModeChange,
galacticAdminMode,
onGalacticAdminModeChange,
user,
}: {
apiStatus: "unchecked" | "checking" | "healthy" | "error";
onCheckHealth: () => void;
adminMode: boolean;
onAdminModeChange: (enabled: boolean) => void;
galacticAdminMode: boolean;
@@ -24,53 +16,7 @@ export function SettingsMenu({
user: { displayName: string; email: string; isGalacticAdmin?: boolean } | null;
}) {
const [isOpen, setIsOpen] = useState(false);
const [showAuditLog, setShowAuditLog] = useState(false);
const [auditLogs, setAuditLogs] = useState<any[]>([]);
const [loadingLogs, setLoadingLogs] = useState(false);
const menuRef = useRef<HTMLDivElement>(null);
const { tenantId } = useTenant();
const { getAccessToken } = useAuth();
const fetchAuditLogs = async () => {
setLoadingLogs(true);
try {
const response = await authenticatedFetch(`${API_BASE_URL}/api/audit/logs?tenantId=${tenantId}`, getAccessToken);
if (response.ok) {
const data = await response.json();
setAuditLogs(data.logs || []);
}
} catch (error) {
if (error instanceof TokenExpiredError) {
console.error('Session expired:', error.message);
} else {
console.error("Failed to fetch audit logs:", error);
}
} finally {
setLoadingLogs(false);
}
};
const statusColors = {
unchecked: "text-gray-500",
checking: "text-yellow-600",
healthy: "text-green-600",
error: "text-red-600",
};
const statusIcons = {
unchecked: "🔘",
checking: "🔄",
healthy: "✅",
error: "❌",
};
const statusText = {
unchecked: "Not checked",
checking: "Checking...",
healthy: "Connected",
error: "Not connected",
};
// Close dropdown when clicking outside
useEffect(() => {
@@ -165,146 +111,6 @@ export function SettingsMenu({
</div>
)}
{/* API Status Section - Clickable */}
<div className="border-t border-gray-200 pt-3">
<button
onClick={onCheckHealth}
disabled={apiStatus === "checking"}
className="w-full p-3 text-left rounded-lg hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
title={apiStatus === "unchecked" ? "Click to check API status" : undefined}
>
<div className="flex items-center justify-between">
<span className="text-sm font-medium text-gray-700">API Status</span>
<span className={`text-sm font-medium ${statusColors[apiStatus]}`}>
{statusIcons[apiStatus]} {statusText[apiStatus]}
</span>
</div>
</button>
</div>
{/* Configuration Section */}
<div className="border-t border-gray-200 pt-3 mt-3">
<a
href="/settings"
className="block w-full p-3 text-left rounded-lg hover:bg-gray-50 transition-colors"
>
<div className="flex items-center justify-between">
<span className="text-sm font-medium text-gray-700">Configuration</span>
<svg className="h-5 w-5 text-gray-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
</svg>
</div>
</a>
</div>
{/* Usage Metrics Section - Always visible for tenant */}
<div className="border-t border-gray-200 pt-3">
<a
href="/usage-metrics"
className="block w-full p-3 text-left rounded-lg hover:bg-gray-50 transition-colors"
>
<div className="flex items-center justify-between">
<span className="text-sm font-medium text-gray-700">Usage Metrics</span>
<svg className="h-5 w-5 text-gray-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
</svg>
</div>
</a>
</div>
{/* Platform Usage Metrics Section - Galactic Admin Only */}
{galacticAdminMode && (
<div className="border-t border-gray-200 pt-3">
<a
href="/admin/metrics/usage"
className="block w-full p-3 text-left rounded-lg hover:bg-gray-50 transition-colors"
>
<div className="flex items-center justify-between">
<span className="text-sm font-medium text-gray-700">Platform Usage Metrics</span>
<svg className="h-5 w-5 text-gray-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3.055 11H5a2 2 0 012 2v1a2 2 0 002 2 2 2 0 012 2v2.945M8 3.935V5.5A2.5 2.5 0 0010.5 8h.5a2 2 0 012 2 2 2 0 104 0 2 2 0 012-2h1.064M15 20.488V18a2 2 0 012-2h3.064M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
</a>
</div>
)}
{/* Audit Log Section */}
<div className="border-t border-gray-200 pt-3">
<button
onClick={() => {
setShowAuditLog(true);
fetchAuditLogs();
}}
className="w-full p-3 text-left rounded-lg hover:bg-gray-50 transition-colors"
>
<div className="flex items-center justify-between">
<span className="text-sm font-medium text-gray-700">Audit Log</span>
<svg className="h-5 w-5 text-gray-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
</div>
</button>
</div>
</div>
</div>
)}
{/* Audit Log Modal */}
{showAuditLog && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-[60]" onClick={() => setShowAuditLog(false)}>
<div className="bg-white rounded-lg shadow-xl max-w-4xl w-full max-h-[80vh] overflow-hidden" onClick={(e) => e.stopPropagation()}>
<div className="p-6 border-b border-gray-200">
<div className="flex items-center justify-between">
<h2 className="text-xl font-semibold text-gray-900">Audit Log</h2>
<button
onClick={() => setShowAuditLog(false)}
className="text-gray-400 hover:text-gray-600"
>
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
</div>
<div className="p-6 overflow-y-auto max-h-[60vh]">
{loadingLogs ? (
<div className="text-center py-8 text-gray-500">Lade Audit Logs...</div>
) : auditLogs.length === 0 ? (
<div className="text-center py-8 text-gray-500">Keine Audit Logs vorhanden</div>
) : (
<div className="space-y-4">
{auditLogs.map((log) => (
<div key={log.id} className="border border-gray-200 rounded-lg p-4 hover:bg-gray-50">
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center gap-3">
<span className={`px-2 py-1 text-xs font-semibold rounded ${
log.action === 'DELETE' ? 'bg-red-100 text-red-800' : 'bg-blue-100 text-blue-800'
}`}>
{log.action}
</span>
<span className="text-sm font-medium text-gray-900">{log.entityType}</span>
<span className="text-sm text-gray-500 font-mono">{log.entityId}</span>
</div>
<div className="mt-2 text-sm text-gray-600">
von <span className="font-medium">{log.performedBy}</span>
</div>
{log.details && log.details !== '{}' && (
<div className="mt-2 text-xs text-gray-500 bg-gray-50 p-2 rounded">
{log.details}
</div>
)}
</div>
<div className="text-xs text-gray-500 text-right">
{new Date(log.timestamp).toLocaleString('de-DE')}
</div>
</div>
</div>
))}
</div>
)}
</div>
</div>
</div>
)}
@@ -5,7 +5,6 @@ import { useEffect } from "react";
import { ProtectedRoute } from "../../components/ProtectedRoute";
import { useAuth } from "../../contexts/AuthContext";
import { TenantConfigProvider } from "./TenantConfigContext";
import { SettingsPageSections } from "./SettingsPageSections";
export default function SettingsLayout({ children }: { children: React.ReactNode }) {
const { user } = useAuth();
@@ -34,7 +33,6 @@ export default function SettingsLayout({ children }: { children: React.ReactNode
return (
<ProtectedRoute>
<TenantConfigProvider>
<SettingsPageSections />
<div className="min-h-screen bg-gray-50">
{children}
</div>
@@ -7,7 +7,7 @@ import { useAuth } from "../contexts/AuthContext";
import { useSidebar, PageSectionItem } from "../contexts/SidebarContext";
import { CollapseState } from "../hooks/useSidebarState";
import { DefaultSectionIcon, BookOpenIcon, RocketLaunchIcon, InformationCircleIcon, DocumentTextIcon, ShieldCheckIcon } from "../lib/sidebarIcons";
import { DASHBOARD_ITEM, NAV_GROUPS, REGULAR_USER_ITEMS, NavItem, NavGroup } from "../lib/globalNavConfig";
import { DASHBOARD_ITEM, NAV_GROUPS, EXPANDABLE_NAV_GROUPS, REGULAR_USER_ITEMS, NavItem, NavGroup, ExpandableNavGroup, ExpandableNavItem } from "../lib/globalNavConfig";
import { PublicSiteNavbar } from "./PublicSiteNavbar";
// Sidebar pixel widths
@@ -176,7 +176,7 @@ export function GlobalSidebar({ children }: { children: ReactNode }) {
const isAdminOrOperator = isTenantAdmin || isOperator;
const isGalacticAdmin = user?.isGalacticAdmin ?? false;
const isGroupVisible = (group: NavGroup): boolean => {
const isGroupVisible = (group: NavGroup | ExpandableNavGroup): boolean => {
switch (group.visibility) {
case "all": return true;
case "adminOrOperator": return isAdminOrOperator;
@@ -185,6 +185,26 @@ export function GlobalSidebar({ children }: { children: ReactNode }) {
}
};
// Visible expandable groups
const visibleExpandableGroups = EXPANDABLE_NAV_GROUPS.filter(isGroupVisible);
// Auto-expand the group containing the current pathname
useEffect(() => {
for (const group of visibleExpandableGroups) {
for (const item of group.items) {
if (item.items.some((sub) => pathname === sub.href || pathname.startsWith(sub.href + "/"))) {
setExpandedGroups((prev) => {
if (prev.has(item.id)) return prev;
const next = new Set(prev);
next.add(item.id);
return next;
});
return;
}
}
}
}, [pathname]); // eslint-disable-line react-hooks/exhaustive-deps
// Docs pages are always accessible (public + authenticated)
const isDocsPage = pathname.startsWith("/docs");
@@ -343,10 +363,9 @@ export function GlobalSidebar({ children }: { children: ReactNode }) {
</div>
)}
{/* Admin/Operator groups */}
{/* Admin/Operator flat groups */}
{!isRegularUser && visibleGroups.map((group) => (
<div key={group.id} className={`${collapseState === "full" || isMobile ? "mt-4" : "mt-1"}`}>
{/* Group header (full mode only) */}
{(collapseState === "full" || isMobile) && (
<p className={`text-[11px] font-semibold uppercase tracking-wider mb-1 px-3 ${
group.style === "galactic"
@@ -356,7 +375,6 @@ export function GlobalSidebar({ children }: { children: ReactNode }) {
{group.label}
</p>
)}
{/* Divider in icons mode */}
{collapseState === "icons" && !isMobile && (
<hr className="mx-2 my-1.5 border-gray-200 dark:border-gray-700" />
)}
@@ -365,6 +383,115 @@ export function GlobalSidebar({ children }: { children: ReactNode }) {
</ul>
</div>
))}
{/* Expandable groups (GitHub-style) — Configuration, Galactic Admin */}
{!isRegularUser && visibleExpandableGroups.map((group) => (
<div key={group.id} className={`${collapseState === "full" || isMobile ? "mt-4" : "mt-1"}`}>
{/* Group label */}
{(collapseState === "full" || isMobile) && (
<p className={`text-[11px] font-semibold uppercase tracking-wider mb-1 px-3 ${
group.style === "galactic"
? "text-purple-500 dark:text-purple-400"
: "text-gray-400 dark:text-gray-500"
}`}>
{group.label}
</p>
)}
{collapseState === "icons" && !isMobile && (
<hr className={`mx-2 my-1.5 ${
group.style === "galactic"
? "border-purple-200 dark:border-purple-800"
: "border-gray-200 dark:border-gray-700"
}`} />
)}
<ul className="space-y-0.5">
{group.items.map((expandItem) => {
const isExpanded = expandedGroups.has(expandItem.id);
const itemHasActive = expandItem.items.some((sub) => pathname === sub.href || pathname.startsWith(sub.href + "/"));
const firstHref = expandItem.items[0]?.href ?? "#";
const isGalactic = group.style === "galactic";
if (collapseState === "icons" && !isMobile) {
// Icons mode: show group icon as link
return (
<li key={expandItem.id}>
<Link
href={firstHref}
onClick={() => setMobileDrawerOpen(false)}
className={`flex items-center justify-center px-2 py-2 rounded-md text-sm transition-colors relative group ${
itemHasActive
? isGalactic
? "bg-purple-50 text-purple-700 font-semibold dark:bg-purple-900/30 dark:text-purple-300"
: "bg-blue-50 text-blue-700 font-semibold dark:bg-blue-900/30 dark:text-blue-300"
: isGalactic
? "text-purple-600 hover:bg-purple-50 hover:text-purple-800 dark:text-purple-400 dark:hover:bg-purple-900/20"
: "text-gray-600 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-gray-200"
}`}
title={expandItem.label}
>
<span className="shrink-0 w-4.5 h-4.5">{expandItem.icon}</span>
<span className="absolute left-full ml-2 px-2 py-1 rounded bg-gray-900 text-white text-xs whitespace-nowrap opacity-0 pointer-events-none group-hover:opacity-100 transition-opacity z-50 dark:bg-gray-700">
{expandItem.label}
</span>
</Link>
</li>
);
}
// Full mode: GitHub-style expandable item
return (
<li key={expandItem.id}>
<button
onClick={() => toggleGroup(expandItem.id)}
className={`w-full flex items-center gap-2.5 px-3 py-1.5 rounded-md text-sm transition-colors ${
itemHasActive
? isGalactic
? "text-purple-700 font-semibold dark:text-purple-300"
: "text-blue-700 font-semibold dark:text-blue-300"
: isGalactic
? "text-purple-600 hover:bg-purple-50 hover:text-purple-800 dark:text-purple-400 dark:hover:bg-purple-900/20"
: "text-gray-600 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-gray-200"
}`}
>
<span className="shrink-0 w-4 h-4">{expandItem.icon}</span>
<span className="truncate flex-1 text-left">{expandItem.label}</span>
<svg
className={`w-3.5 h-3.5 shrink-0 text-gray-400 transition-transform duration-150 ${isExpanded ? "rotate-90" : ""}`}
fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}
>
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
</svg>
</button>
{isExpanded && (
<ul className="mt-0.5 space-y-px">
{expandItem.items.map((sub) => {
const subActive = pathname === sub.href || pathname.startsWith(sub.href + "/");
return (
<li key={sub.id}>
<Link
href={sub.href}
onClick={() => setMobileDrawerOpen(false)}
className={`block pl-10 pr-3 py-1 rounded-md text-[13px] transition-colors ${
subActive
? isGalactic
? "bg-purple-50 text-purple-700 font-medium dark:bg-purple-900/30 dark:text-purple-300"
: "bg-blue-50 text-blue-700 font-medium dark:bg-blue-900/30 dark:text-blue-300"
: "text-gray-500 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-gray-200"
}`}
>
{sub.label}
</Link>
</li>
);
})}
</ul>
)}
</li>
);
})}
</ul>
</div>
))}
</>
)}
@@ -435,45 +435,6 @@ export default function Navbar() {
</div>
)}
{/* CONFIGURATION */}
{(isTenantAdmin || (isOperator && user?.canManageBootstrapTokens)) && (
<>
<p className="text-[11px] font-semibold uppercase tracking-wider text-gray-400 mt-3 mb-1.5">Configuration</p>
<Link href="/settings" className="flex items-center gap-2 py-2 px-2.5 rounded-md hover:bg-gray-50 transition-colors" onClick={() => setShowSettings(false)}>
<svg className="w-4 h-4 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4" />
</svg>
<span className="text-sm text-gray-700">Tenant Settings</span>
</Link>
</>
)}
{(isTenantAdmin || isOperator) && (
<>
<Link href="/gather-rules" className="flex items-center gap-2 py-2 px-2.5 rounded-md hover:bg-gray-50 transition-colors" onClick={() => setShowSettings(false)}>
<svg className="w-4 h-4 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
</svg>
<span className="text-sm text-gray-700">Gather Rules</span>
</Link>
<Link href="/analyze-rules" className="flex items-center gap-2 py-2 px-2.5 rounded-md hover:bg-gray-50 transition-colors" onClick={() => setShowSettings(false)}>
<svg className="w-4 h-4 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" />
</svg>
<span className="text-sm text-gray-700">Analyze Rules</span>
</Link>
<Link href="/ime-log-patterns" className="flex items-center gap-2 py-2 px-2.5 rounded-md hover:bg-gray-50 transition-colors" onClick={() => setShowSettings(false)}>
<svg className="w-4 h-4 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
<span className="text-sm text-gray-700">IME Log Patterns</span>
</Link>
</>
)}
</div>
</div>
)}
@@ -1,12 +1,21 @@
import {
ChartBarIcon,
GearIcon,
ComputerDesktopIcon,
DocumentTextIcon,
ShieldCheckIcon,
UsersIcon,
BellIcon,
CircleStackIcon,
BuildingOfficeIcon,
NoSymbolIcon,
KeyIcon,
SparklesIcon,
ArrowDownTrayIcon,
ArrowPathIcon,
FolderIcon,
} from "./sidebarIcons";
// --- New icons for global nav (added inline here, monochrome outline style) ---
// --- Icons defined inline (not in sidebarIcons) ---
function HomeIcon({ className = "w-5 h-5" }: { className?: string }) {
return (
@@ -40,14 +49,6 @@ function MonitorIcon({ className = "w-5 h-5" }: { className?: string }) {
);
}
function DocumentMagnifyingGlassIcon({ 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="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m5.231 13.481L15 17.25m-4.5-15H5.625c-.621 0-1.125.504-1.125 1.125v16.5c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9zm3.75 11.625a2.625 2.625 0 11-5.25 0 2.625 2.625 0 015.25 0z" />
</svg>
);
}
function ClipboardDocumentIcon({ className = "w-5 h-5" }: { className?: string }) {
return (
<svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
@@ -64,6 +65,14 @@ function HeartIcon({ className = "w-5 h-5" }: { className?: string }) {
);
}
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>
);
}
// --- Types ---
export interface NavItem {
@@ -73,6 +82,21 @@ export interface NavItem {
icon: React.ReactNode;
}
/** A sub-item inside an expandable group (GitHub-style, no icon) */
export interface ExpandableSubItem {
id: string;
label: string;
href: string;
}
/** An expandable group with icon + chevron, containing sub-items */
export interface ExpandableNavItem {
id: string;
label: string;
icon: React.ReactNode;
items: ExpandableSubItem[];
}
export interface NavGroup {
id: string;
label: string;
@@ -81,6 +105,15 @@ export interface NavGroup {
style?: "galactic";
}
/** A group rendered as expandable GitHub-style items */
export interface ExpandableNavGroup {
id: string;
label: string;
items: ExpandableNavItem[];
visibility: "all" | "adminOrOperator" | "galacticAdmin";
style?: "galactic";
}
// --- Dashboard item (standalone, above all groups) ---
export const DASHBOARD_ITEM: NavItem = {
@@ -90,7 +123,7 @@ export const DASHBOARD_ITEM: NavItem = {
icon: <HomeIcon />,
};
// --- Global navigation groups ---
// --- Global navigation groups (flat items) ---
export const NAV_GROUPS: NavGroup[] = [
{
@@ -104,6 +137,16 @@ export const NAV_GROUPS: NavGroup[] = [
{ id: "progress", label: "Progress Portal", href: "/progress", icon: <MonitorIcon /> },
],
},
{
id: "rules",
label: "Rules",
visibility: "adminOrOperator",
items: [
{ id: "gather-rules", label: "Gather Rules", href: "/gather-rules", icon: <FolderIcon /> },
{ id: "analyze-rules", label: "Analyze Rules", href: "/analyze-rules", icon: <SparklesIcon /> },
{ id: "ime-log-patterns", label: "IME Log Patterns", href: "/ime-log-patterns", icon: <DocumentTextIcon /> },
],
},
{
id: "operations",
label: "Operations",
@@ -113,8 +156,110 @@ export const NAV_GROUPS: NavGroup[] = [
{ id: "health-check", label: "System Health", href: "/health-check", icon: <HeartIcon /> },
],
},
// Galactic Admin items are registered dynamically via AdminPageSections
// when the user navigates to /admin/* routes.
];
// --- Expandable navigation groups (GitHub-style with sub-items) ---
export const EXPANDABLE_NAV_GROUPS: ExpandableNavGroup[] = [
{
id: "configuration",
label: "Configuration",
visibility: "adminOrOperator",
items: [
{
id: "cfg-validation", label: "Validation", icon: <ShieldCheckIcon />,
items: [
{ id: "cfg-autopilot", label: "Autopilot Validation", href: "/settings/validation/autopilot" },
{ id: "cfg-hardware", label: "Hardware Whitelist", href: "/settings/validation/hardware-whitelist" },
],
},
{
id: "cfg-agent", label: "Agent", icon: <GearIcon />,
items: [
{ id: "cfg-agent-settings", label: "Agent Settings", href: "/settings/agent/settings" },
{ id: "cfg-agent-analyzers", label: "Agent Analyzers", href: "/settings/agent/analyzers" },
],
},
{
id: "cfg-access", label: "Access", icon: <UsersIcon />,
items: [
{ id: "cfg-admin-mgmt", label: "Admin Management", href: "/settings/access/admin-management" },
],
},
{
id: "cfg-integrations", label: "Integrations", icon: <BellIcon />,
items: [
{ id: "cfg-notifications", label: "Notifications", href: "/settings/integrations/notifications" },
{ id: "cfg-diagnostics", label: "Diagnostics", href: "/settings/integrations/diagnostics" },
],
},
{
id: "cfg-management", label: "Management", icon: <CircleStackIcon />,
items: [
{ id: "cfg-data", label: "Data Management", href: "/settings/management/data" },
{ id: "cfg-offboarding", label: "Offboarding", href: "/settings/management/offboarding" },
],
},
],
},
{
id: "galactic-admin",
label: "Galactic Admin",
visibility: "galacticAdmin",
style: "galactic",
items: [
{
id: "ga-tenants", label: "Tenants", icon: <BuildingOfficeIcon />,
items: [
{ id: "ga-tenant-mgmt", label: "Tenant Management", href: "/admin/tenants/management" },
{ id: "ga-config-report", label: "Config Report", href: "/admin/tenants/config-report" },
],
},
{
id: "ga-metrics", label: "Metrics", icon: <ChartBarIcon />,
items: [
{ id: "ga-agent-metrics", label: "Agent Metrics", href: "/admin/metrics/agent-metrics" },
{ id: "ga-usage", label: "Platform Usage", href: "/admin/metrics/usage" },
],
},
{
id: "ga-reports", label: "Reports", icon: <DocumentTextIcon />,
items: [
{ id: "ga-session-reports", label: "Session Reports", href: "/admin/reports/session-reports" },
{ id: "ga-user-feedback", label: "User Feedback", href: "/admin/reports/user-feedback" },
{ id: "ga-session-export", label: "Session Export", href: "/admin/reports/session-export" },
],
},
{
id: "ga-security", label: "Security", icon: <ShieldCheckIcon />,
items: [
{ id: "ga-device-block", label: "Device Block", href: "/admin/security/device-block" },
{ id: "ga-version-block", label: "Version Block", href: "/admin/security/version-block" },
{ id: "ga-vulnerability", label: "Vulnerability Data", href: "/admin/security/vulnerability-data" },
],
},
{
id: "ga-settings", label: "Settings", icon: <GearIcon />,
items: [
{ id: "ga-global", label: "Global Settings", href: "/admin/settings/global" },
{ id: "ga-diag-paths", label: "Diagnostics Log Paths", href: "/admin/settings/diagnostics-log-paths" },
{ id: "ga-config-reseed", label: "Config Reseed", href: "/admin/settings/config-reseed" },
],
},
{
id: "ga-ops", label: "Ops", icon: <WrenchIcon />,
items: [
{ id: "ga-maintenance", label: "Maintenance", href: "/admin/ops" },
],
},
{
id: "ga-software", label: "Software", icon: <GlobeAltIcon />,
items: [
{ id: "ga-software-mapping", label: "Software Mapping", href: "/admin/software" },
],
},
],
},
];
// --- Regular user nav (minimal) ---
File diff suppressed because one or more lines are too long