mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-10-24 20:53:06 +00:00
re-org sidebar
This commit is contained in:
@@ -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
Reference in New Issue
Block a user