sidebar new structure (more logical), top navbar mobile view fix

This commit is contained in:
Oliver Kieselbach
2026-03-30 14:34:24 +02:00
parent 0af770599f
commit 0955fd79dc
47 changed files with 359 additions and 298 deletions
@@ -276,8 +276,8 @@ export function SectionSettings() {
</SettingsRow>
</SettingsGroup>
{/* Team Management */}
<SettingsGroup title="Team Management" color="bg-gray-100 text-gray-800" borderColor="border-gray-200">
{/* Access Management */}
<SettingsGroup title="Access Management" color="bg-gray-100 text-gray-800" borderColor="border-gray-200">
<SettingsRow name="Team Members &amp; Roles">
Manage who has access to the portal and at what level. New members are added by entering their UPN
(e.g. <span className="font-mono text-xs bg-gray-100 px-1 py-0.5 rounded">user@contoso.com</span>).
@@ -4,8 +4,9 @@ import { useMemo } from "react";
import { usePageSections } from "../../hooks/usePageSections";
import { PageSectionItem } from "../../contexts/SidebarContext";
import { useTenantConfig } from "./TenantConfigContext";
import { useAuth } from "../../contexts/AuthContext";
import {
ShieldCheckIcon, GearIcon, UsersIcon, BellIcon, CircleStackIcon, ChartBarIcon,
BuildingOfficeIcon, GearIcon, WrenchScrewdriverIcon, ChartBarIcon,
} from "../../lib/sidebarIcons";
/**
@@ -14,16 +15,24 @@ import {
*/
export function SettingsPageSections() {
const { config, user } = useTenantConfig();
const { user: authUser } = useAuth();
const items: PageSectionItem[] = useMemo(() => {
const sections: PageSectionItem[] = [];
const isAdmin = user?.isTenantAdmin || user?.isGlobalAdmin;
// 1. Validation
// 1. Tenant
if (isAdmin) {
sections.push(
{ id: "autopilot", label: "Autopilot Validation", href: "/settings/validation/autopilot", group: "Validation", groupIcon: <ShieldCheckIcon /> },
{ id: "hardware-whitelist", label: "Hardware Whitelist", href: "/settings/validation/hardware-whitelist", group: "Validation" },
{ id: "autopilot", label: "Autopilot Validation", href: "/settings/tenant/autopilot", group: "Tenant", groupIcon: <BuildingOfficeIcon /> },
{ id: "hardware-whitelist", label: "Hardware Whitelist", href: "/settings/tenant/hardware-whitelist", group: "Tenant" },
{ id: "notifications", label: "Notifications", href: "/settings/tenant/notifications", group: "Tenant" },
{ id: "access-management", label: "Access Management", href: "/settings/tenant/access-management", group: "Tenant" },
);
}
if (config?.bootstrapTokenEnabled && (isAdmin || user?.canManageBootstrapTokens)) {
sections.push(
{ id: "bootstrap-sessions", label: "Bootstrap Sessions", href: "/settings/tenant/bootstrap-sessions", group: "Tenant", ...(!isAdmin ? { groupIcon: <BuildingOfficeIcon /> } : {}) },
);
}
@@ -32,6 +41,7 @@ export function SettingsPageSections() {
sections.push(
{ id: "settings", label: "Agent Settings", href: "/settings/agent/settings", group: "Agent", groupIcon: <GearIcon /> },
{ id: "analyzers", label: "Agent Analyzers", href: "/settings/agent/analyzers", group: "Agent" },
{ id: "diagnostics", label: "Diagnostics Package", href: "/settings/agent/diagnostics", group: "Agent" },
);
if (config?.unrestrictedModeEnabled) {
sections.push(
@@ -40,41 +50,23 @@ export function SettingsPageSections() {
}
}
// 3. Access
// 3. Maintenance
if (isAdmin) {
sections.push(
{ id: "admin-management", label: "Admin Management", href: "/settings/access/admin-management", group: "Access", groupIcon: <UsersIcon /> },
);
}
if (config?.bootstrapTokenEnabled && (isAdmin || user?.canManageBootstrapTokens)) {
sections.push(
{ id: "bootstrap-sessions", label: "Bootstrap Sessions", href: "/settings/access/bootstrap-sessions", group: "Access", ...(!isAdmin ? { groupIcon: <UsersIcon /> } : {}) },
{ id: "data", label: "Data Management", href: "/settings/management/data", group: "Maintenance", groupIcon: <WrenchScrewdriverIcon /> },
{ id: "offboarding", label: "Offboarding", href: "/settings/management/offboarding", group: "Maintenance" },
);
}
// 4. Integrations
if (isAdmin) {
// 4. Reporting (only visible if user has MCP access)
if (authUser?.hasMcpAccess) {
sections.push(
{ id: "notifications", label: "Notifications", href: "/settings/integrations/notifications", group: "Integrations", groupIcon: <BellIcon /> },
{ id: "diagnostics", label: "Diagnostics", href: "/settings/integrations/diagnostics", group: "Integrations" },
{ id: "mcp-usage", label: "MCP Usage", href: "/settings/reporting/mcp-usage", group: "Reporting", groupIcon: <ChartBarIcon /> },
);
}
// 5. Management
if (isAdmin) {
sections.push(
{ id: "data", label: "Data Management", href: "/settings/management/data", group: "Management", groupIcon: <CircleStackIcon /> },
{ id: "offboarding", label: "Offboarding", href: "/settings/management/offboarding", group: "Management" },
);
}
// 6. MCP (visible to any authenticated user — the API itself checks MCP access)
sections.push(
{ id: "mcp-usage", label: "Usage", href: "/settings/mcp/usage", group: "MCP", groupIcon: <ChartBarIcon /> },
);
return sections;
}, [user, config?.unrestrictedModeEnabled, config?.bootstrapTokenEnabled]);
}, [user, config?.unrestrictedModeEnabled, config?.bootstrapTokenEnabled, authUser?.hasMcpAccess]);
usePageSections(items, "Configuration", "route");
@@ -355,7 +355,7 @@ export function TenantConfigProvider({ children }: { children: React.ReactNode }
'Diagnostics SAS URL Expired',
`The Diagnostics SAS URL expired on ${expiry.toLocaleDateString()}. Diagnostics upload is non-functional.`,
'diagnostics-sas-expiry',
'/settings/integrations/diagnostics'
'/settings/agent/diagnostics'
);
} else if (daysRemaining <= 7) {
addNotification(
@@ -363,7 +363,7 @@ export function TenantConfigProvider({ children }: { children: React.ReactNode }
'Diagnostics SAS URL Expiring Soon',
`The Diagnostics SAS URL expires on ${expiry.toLocaleDateString()} (${daysRemaining} day${daysRemaining === 1 ? '' : 's'} remaining). Please update it soon.`,
'diagnostics-sas-expiry',
'/settings/integrations/diagnostics'
'/settings/agent/diagnostics'
);
}
}
@@ -569,7 +569,7 @@ export function TenantConfigProvider({ children }: { children: React.ReactNode }
setError(null);
setSuccessMessage(null);
const redirectUri = `${window.location.origin}/settings/validation/autopilot`;
const redirectUri = `${window.location.origin}/settings/tenant/autopilot`;
const response = await authenticatedFetch(
api.config.autopilotConsentUrl(tenantId, redirectUri),
getAccessToken,
@@ -625,7 +625,7 @@ export function TenantConfigProvider({ children }: { children: React.ReactNode }
: consentError;
setError(`Admin consent failed: ${errorText}`);
setAutopilotConsentInProgress(false);
router.replace("/settings/validation/autopilot");
router.replace("/settings/tenant/autopilot");
return;
}
@@ -654,7 +654,7 @@ export function TenantConfigProvider({ children }: { children: React.ReactNode }
await saveConfiguration("autopilotValidation", { validateAutopilotDevice: true });
setSuccessMessage("Autopilot Device Validation enabled. Backend agent endpoints are now unlocked for this tenant.");
}
router.replace("/settings/validation/autopilot");
router.replace("/settings/tenant/autopilot");
} catch (err) {
if (err instanceof TokenExpiredError) {
addNotification('error', 'Session Expired', err.message, 'session-expired-error');
@@ -1,20 +0,0 @@
"use client";
import { useParams } from "next/navigation";
import { notFound } from "next/navigation";
import { type AccessSectionId } from "../accessNavSections";
import { SectionAdminManagement } from "../sections/SectionAdminManagement";
import { SectionBootstrapSessions } from "../sections/SectionBootstrapSessions";
const SECTION_COMPONENTS: Record<AccessSectionId, React.ComponentType> = {
"admin-management": SectionAdminManagement,
"bootstrap-sessions": SectionBootstrapSessions,
};
export default function AccessSectionPage() {
const params = useParams();
const section = params.section as string;
const SectionContent = SECTION_COMPONENTS[section as AccessSectionId];
if (!SectionContent) notFound();
return <SectionContent />;
}
@@ -1,6 +0,0 @@
export const ACCESS_NAV_SECTIONS = [
{ id: "admin-management", label: "Admin Management", description: "Manage tenant admins and operators" },
{ id: "bootstrap-sessions", label: "Bootstrap Sessions", description: "Create and manage bootstrap tokens" },
] as const;
export type AccessSectionId = (typeof ACCESS_NAV_SECTIONS)[number]["id"];
@@ -1,5 +0,0 @@
import { AccessSidebar } from "./AccessSidebar";
export default function AccessLayout({ children }: { children: React.ReactNode }) {
return <AccessSidebar>{children}</AccessSidebar>;
}
@@ -1,5 +0,0 @@
import { redirect } from "next/navigation";
export default function AccessPage() {
redirect("/settings/access/admin-management");
}
@@ -6,7 +6,7 @@ export function AgentSidebar({ children }: { children: React.ReactNode }) {
<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">Tenant Configuration</h1>
<p className="text-sm text-gray-500 mt-1">Agent collector and behavior configuration</p>
<p className="text-sm text-gray-500 mt-1">Agent behavior, analyzers, and diagnostics package</p>
</div>
</header>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8 space-y-8">
@@ -5,11 +5,13 @@ import { notFound } from "next/navigation";
import { type AgentSectionId } from "../agentNavSections";
import { SectionAgentSettings } from "../sections/SectionAgentSettings";
import { SectionAgentAnalyzers } from "../sections/SectionAgentAnalyzers";
import { SectionDiagnostics } from "../sections/SectionDiagnostics";
import { SectionUnrestrictedMode } from "../sections/SectionUnrestrictedMode";
const SECTION_COMPONENTS: Record<AgentSectionId, React.ComponentType> = {
"settings": SectionAgentSettings,
"analyzers": SectionAgentAnalyzers,
"diagnostics": SectionDiagnostics,
"unrestricted-mode": SectionUnrestrictedMode,
};
@@ -1,6 +1,7 @@
export const AGENT_NAV_SECTIONS = [
{ id: "settings", label: "Agent Settings", description: "Agent collector and behavior configuration" },
{ id: "analyzers", label: "Agent Analyzers", description: "Local admin and software inventory analyzers" },
{ id: "diagnostics", label: "Diagnostics Package", description: "Diagnostics upload and log path configuration" },
{ id: "unrestricted-mode", label: "Unrestricted Mode", description: "Unrestricted mode configuration" },
] as const;
@@ -81,7 +81,7 @@ export default function AdminManagementSection({
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z" />
</svg>
<div>
<h2 className="text-xl font-semibold text-gray-900">Team Management</h2>
<h2 className="text-xl font-semibold text-gray-900">Access Management</h2>
<p className="text-sm text-gray-500 mt-1">Manage team members and their roles for this tenant</p>
</div>
</div>
@@ -1,17 +0,0 @@
"use client";
export function IntegrationsSidebar({ children }: { children: React.ReactNode }) {
return (
<>
<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">Tenant Configuration</h1>
<p className="text-sm text-gray-500 mt-1">Webhook notifications and diagnostics upload</p>
</div>
</header>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8 space-y-8">
{children}
</div>
</>
);
}
@@ -1,20 +0,0 @@
"use client";
import { useParams } from "next/navigation";
import { notFound } from "next/navigation";
import { type IntegrationsSectionId } from "../integrationsNavSections";
import { SectionNotifications } from "../sections/SectionNotifications";
import { SectionDiagnostics } from "../sections/SectionDiagnostics";
const SECTION_COMPONENTS: Record<IntegrationsSectionId, React.ComponentType> = {
"notifications": SectionNotifications,
"diagnostics": SectionDiagnostics,
};
export default function IntegrationsSectionPage() {
const params = useParams();
const section = params.section as string;
const SectionContent = SECTION_COMPONENTS[section as IntegrationsSectionId];
if (!SectionContent) notFound();
return <SectionContent />;
}
@@ -1,6 +0,0 @@
export const INTEGRATIONS_NAV_SECTIONS = [
{ id: "notifications", label: "Notifications", description: "Webhook notification configuration" },
{ id: "diagnostics", label: "Diagnostics", description: "Diagnostics upload and log path configuration" },
] as const;
export type IntegrationsSectionId = (typeof INTEGRATIONS_NAV_SECTIONS)[number]["id"];
@@ -1,5 +0,0 @@
import { IntegrationsSidebar } from "./IntegrationsSidebar";
export default function IntegrationsLayout({ children }: { children: React.ReactNode }) {
return <IntegrationsSidebar>{children}</IntegrationsSidebar>;
}
@@ -1,5 +0,0 @@
import { redirect } from "next/navigation";
export default function IntegrationsPage() {
redirect("/settings/integrations/notifications");
}
@@ -1,5 +0,0 @@
import { McpSidebar } from "./McpSidebar";
export default function McpLayout({ children }: { children: React.ReactNode }) {
return <McpSidebar>{children}</McpSidebar>;
}
@@ -1,5 +0,0 @@
export const MCP_NAV_SECTIONS = [
{ id: "usage", label: "Usage", description: "Your MCP API usage statistics" },
] as const;
export type McpSectionId = (typeof MCP_NAV_SECTIONS)[number]["id"];
@@ -1,5 +0,0 @@
import { redirect } from "next/navigation";
export default function McpPage() {
redirect("/settings/mcp/usage");
}
@@ -13,12 +13,12 @@ export default function SettingsPage() {
// Operators with bootstrap permission only → go directly to bootstrap sessions
if (user.role === "Operator" && !user.isTenantAdmin && !user.isGlobalAdmin && user.canManageBootstrapTokens) {
router.replace("/settings/access/bootstrap-sessions");
router.replace("/settings/tenant/bootstrap-sessions");
return;
}
// Everyone else (Tenant Admins, Global Admins) → validation (first in onboarding flow)
router.replace("/settings/validation/autopilot");
// Everyone else (Tenant Admins, Global Admins) → tenant settings (first in onboarding flow)
router.replace("/settings/tenant/autopilot");
}, [user, router]);
return null;
@@ -1,11 +1,11 @@
"use client";
export function McpSidebar({ children }: { children: React.ReactNode }) {
export function ReportingSidebar({ children }: { children: React.ReactNode }) {
return (
<>
<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">MCP</h1>
<h1 className="text-2xl font-normal text-gray-900">Reporting</h1>
<p className="text-sm text-gray-500 mt-1">AI agent access and usage statistics</p>
</div>
</header>
@@ -2,17 +2,17 @@
import { useParams } from "next/navigation";
import { notFound } from "next/navigation";
import { type McpSectionId } from "../mcpNavSections";
import { type ReportingSectionId } from "../reportingNavSections";
import { SectionMcpUsage } from "../sections/SectionMcpUsage";
const SECTION_COMPONENTS: Record<McpSectionId, React.ComponentType> = {
"usage": SectionMcpUsage,
const SECTION_COMPONENTS: Record<ReportingSectionId, React.ComponentType> = {
"mcp-usage": SectionMcpUsage,
};
export default function McpSectionPage() {
export default function ReportingSectionPage() {
const params = useParams();
const section = params.section as string;
const SectionContent = SECTION_COMPONENTS[section as McpSectionId];
const SectionContent = SECTION_COMPONENTS[section as ReportingSectionId];
if (!SectionContent) notFound();
return <SectionContent />;
}
@@ -0,0 +1,5 @@
import { ReportingSidebar } from "./ReportingSidebar";
export default function ReportingLayout({ children }: { children: React.ReactNode }) {
return <ReportingSidebar>{children}</ReportingSidebar>;
}
@@ -0,0 +1,5 @@
import { redirect } from "next/navigation";
export default function ReportingPage() {
redirect("/settings/reporting/mcp-usage");
}
@@ -0,0 +1,5 @@
export const REPORTING_NAV_SECTIONS = [
{ id: "mcp-usage", label: "MCP Usage", description: "Your MCP API usage statistics" },
] as const;
export type ReportingSectionId = (typeof REPORTING_NAV_SECTIONS)[number]["id"];
@@ -1,12 +1,12 @@
"use client";
export function AccessSidebar({ children }: { children: React.ReactNode }) {
export function TenantSidebar({ children }: { children: React.ReactNode }) {
return (
<>
<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">Tenant Configuration</h1>
<p className="text-sm text-gray-500 mt-1">Admin management and bootstrap sessions</p>
<p className="text-sm text-gray-500 mt-1">Validation, notifications, and access management</p>
</div>
</header>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8 space-y-8">
@@ -0,0 +1,26 @@
"use client";
import { useParams } from "next/navigation";
import { notFound } from "next/navigation";
import { type TenantSectionId } from "../tenantNavSections";
import { SectionAutopilotValidation } from "../sections/SectionAutopilotValidation";
import { SectionHardwareWhitelist } from "../sections/SectionHardwareWhitelist";
import { SectionNotifications } from "../sections/SectionNotifications";
import { SectionAccessManagement } from "../sections/SectionAccessManagement";
import { SectionBootstrapSessions } from "../sections/SectionBootstrapSessions";
const SECTION_COMPONENTS: Record<TenantSectionId, React.ComponentType> = {
"autopilot": SectionAutopilotValidation,
"hardware-whitelist": SectionHardwareWhitelist,
"notifications": SectionNotifications,
"access-management": SectionAccessManagement,
"bootstrap-sessions": SectionBootstrapSessions,
};
export default function TenantSectionPage() {
const params = useParams();
const section = params.section as string;
const SectionContent = SECTION_COMPONENTS[section as TenantSectionId];
if (!SectionContent) notFound();
return <SectionContent />;
}
@@ -0,0 +1,5 @@
import { TenantSidebar } from "./TenantSidebar";
export default function TenantLayout({ children }: { children: React.ReactNode }) {
return <TenantSidebar>{children}</TenantSidebar>;
}
@@ -0,0 +1,5 @@
import { redirect } from "next/navigation";
export default function TenantPage() {
redirect("/settings/tenant/autopilot");
}
@@ -4,7 +4,7 @@ import { useTenantConfig } from "../../TenantConfigContext";
import { TenantNotifications } from "../../TenantNotifications";
import AdminManagementSection from "../../components/AdminManagementSection";
export function SectionAdminManagement() {
export function SectionAccessManagement() {
const {
admins, loadingAdmins,
newAdminEmail, setNewAdminEmail,
@@ -0,0 +1,9 @@
export const TENANT_NAV_SECTIONS = [
{ id: "autopilot", label: "Autopilot Validation", description: "Autopilot device and corporate identifier validation" },
{ id: "hardware-whitelist", label: "Hardware Whitelist", description: "Manufacturer and model whitelist configuration" },
{ id: "notifications", label: "Notifications", description: "Webhook notification configuration" },
{ id: "access-management", label: "Access Management", description: "Manage tenant admins and operators" },
{ id: "bootstrap-sessions", label: "Bootstrap Sessions", description: "Create and manage bootstrap tokens" },
] as const;
export type TenantSectionId = (typeof TENANT_NAV_SECTIONS)[number]["id"];
@@ -1,17 +0,0 @@
"use client";
export function ValidationSidebar({ children }: { children: React.ReactNode }) {
return (
<>
<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">Tenant Configuration</h1>
<p className="text-sm text-gray-500 mt-1">Autopilot validation and hardware whitelist</p>
</div>
</header>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8 space-y-8">
{children}
</div>
</>
);
}
@@ -1,20 +0,0 @@
"use client";
import { useParams } from "next/navigation";
import { notFound } from "next/navigation";
import { VALIDATION_NAV_SECTIONS, type ValidationSectionId } from "../validationNavSections";
import { SectionAutopilotValidation } from "../sections/SectionAutopilotValidation";
import { SectionHardwareWhitelist } from "../sections/SectionHardwareWhitelist";
const SECTION_COMPONENTS: Record<ValidationSectionId, React.ComponentType> = {
"autopilot": SectionAutopilotValidation,
"hardware-whitelist": SectionHardwareWhitelist,
};
export default function ValidationSectionPage() {
const params = useParams();
const section = params.section as string;
const SectionContent = SECTION_COMPONENTS[section as ValidationSectionId];
if (!SectionContent) notFound();
return <SectionContent />;
}
@@ -1,5 +0,0 @@
import { ValidationSidebar } from "./ValidationSidebar";
export default function ValidationLayout({ children }: { children: React.ReactNode }) {
return <ValidationSidebar>{children}</ValidationSidebar>;
}
@@ -1,5 +0,0 @@
import { redirect } from "next/navigation";
export default function ValidationPage() {
redirect("/settings/validation/autopilot");
}
@@ -1,6 +0,0 @@
export const VALIDATION_NAV_SECTIONS = [
{ id: "autopilot", label: "Autopilot Validation", description: "Autopilot device and corporate identifier validation" },
{ id: "hardware-whitelist", label: "Hardware Whitelist", description: "Manufacturer and model whitelist configuration" },
] as const;
export type ValidationSectionId = (typeof VALIDATION_NAV_SECTIONS)[number]["id"];
@@ -183,7 +183,7 @@ export function GlobalSidebar({ children }: { children: ReactNode }) {
.map((group) => {
// Filter out MCP item if user doesn't have MCP access
const filteredItems = group.items.filter((item) => {
if (item.id === "cfg-mcp") return hasMcpAccess;
if (item.id === "cfg-reporting") return hasMcpAccess;
return true;
});
return { ...group, items: filteredItems };
@@ -23,6 +23,7 @@ export default function Navbar() {
const [showSettings, setShowSettings] = useState(false);
const [showHelp, setShowHelp] = useState(false);
const [showOverflow, setShowOverflow] = useState(false);
const [overflowSubmenu, setOverflowSubmenu] = useState<'help' | 'settings' | 'notifications' | null>(null);
const { adminMode, setAdminMode, globalAdminMode, setGlobalAdminMode } = useAdminMode();
const [previewMode, setPreviewMode] = useState(() => {
if (typeof window !== 'undefined') {
@@ -85,6 +86,7 @@ export default function Navbar() {
}
if (overflowRef.current && !overflowRef.current.contains(event.target as Node)) {
setShowOverflow(false);
setOverflowSubmenu(null);
}
}
@@ -496,7 +498,7 @@ export default function Navbar() {
{/* Overflow Menu (...) — visible only on <sm */}
<div className="sm:hidden relative" ref={overflowRef}>
<button
onClick={() => setShowOverflow(!showOverflow)}
onClick={() => { setShowOverflow(!showOverflow); setOverflowSubmenu(null); }}
className="p-2 rounded-lg hover:bg-gray-100 transition-colors"
title="More"
>
@@ -505,74 +507,247 @@ export default function Navbar() {
</svg>
</button>
{showOverflow && (
<div className="absolute right-0 mt-2 w-56 bg-white rounded-lg shadow-lg border border-gray-200 z-50 py-1">
{/* Dark Mode Toggle */}
<button
onClick={() => { toggleTheme(); setShowOverflow(false); }}
className="w-full flex items-center justify-between px-3 py-2 text-sm text-gray-700 hover:bg-gray-50 transition-colors"
>
<div className="flex items-center gap-2.5">
{theme === 'dark' ? (
<svg className="w-4 h-4 text-yellow-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
</svg>
) : (
<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="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
</svg>
<div className="absolute right-0 mt-2 w-56 bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 z-50 py-1">
{/* ── Main overflow menu ── */}
{!overflowSubmenu && (
<>
{/* Dark Mode Toggle */}
<button
onClick={() => { toggleTheme(); setShowOverflow(false); }}
className="w-full flex items-center justify-between px-3 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
>
<div className="flex items-center gap-2.5">
{theme === 'dark' ? (
<svg className="w-4 h-4 text-yellow-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
</svg>
) : (
<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="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
</svg>
)}
<span>{theme === 'dark' ? 'Light Mode' : 'Dark Mode'}</span>
</div>
</button>
{/* Notifications — opens submenu */}
<button
onClick={() => setOverflowSubmenu('notifications')}
className="w-full flex items-center justify-between px-3 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
>
<div className="flex items-center gap-2.5">
<svg className="w-4 h-4 text-gray-400" fill="none" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" viewBox="0 0 24 24" stroke="currentColor">
<path d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"></path>
</svg>
<span>Notifications</span>
</div>
<div className="flex items-center gap-1.5">
{(() => {
const globalCount = (user?.isGlobalAdmin && globalAdminMode) ? globalNotifications.length : 0;
const totalUnread = unreadCount + globalCount;
return totalUnread > 0 ? (
<span className="inline-flex items-center justify-center w-5 h-5 text-[10px] font-bold text-white bg-red-600 rounded-full">
{totalUnread > 9 ? '9+' : totalUnread}
</span>
) : null;
})()}
<svg className="w-3.5 h-3.5 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
</svg>
</div>
</button>
<div className="border-t border-gray-100 dark:border-gray-700 my-1"></div>
{/* Settings — opens submenu */}
{isTenantAdmin && (
<button
onClick={() => setOverflowSubmenu('settings')}
className="w-full flex items-center justify-between px-3 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
>
<div className="flex items-center gap-2.5">
<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="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
<path strokeLinecap="round" strokeLinejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
<span>Settings</span>
</div>
<svg className="w-3.5 h-3.5 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
</svg>
</button>
)}
<span>{theme === 'dark' ? 'Light Mode' : 'Dark Mode'}</span>
</div>
</button>
{/* Notifications */}
<button
onClick={() => { setShowOverflow(false); setShowNotifications(!showNotifications); }}
className="w-full flex items-center justify-between px-3 py-2 text-sm text-gray-700 hover:bg-gray-50 transition-colors"
>
<div className="flex items-center gap-2.5">
<svg className="w-4 h-4 text-gray-400" fill="none" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" viewBox="0 0 24 24" stroke="currentColor">
<path d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"></path>
</svg>
<span>Notifications</span>
</div>
{(() => {
const globalCount = (user?.isGlobalAdmin && globalAdminMode) ? globalNotifications.length : 0;
const totalUnread = unreadCount + globalCount;
return totalUnread > 0 ? (
<span className="inline-flex items-center justify-center w-5 h-5 text-[10px] font-bold text-white bg-red-600 rounded-full">
{totalUnread > 9 ? '9+' : totalUnread}
</span>
) : null;
})()}
</button>
<div className="border-t border-gray-100 my-1"></div>
{/* Settings */}
{isTenantAdmin && (
<button
onClick={() => { setShowOverflow(false); setShowSettings(!showSettings); }}
className="w-full flex items-center gap-2.5 px-3 py-2 text-sm text-gray-700 hover:bg-gray-50 transition-colors"
>
<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="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
<path strokeLinecap="round" strokeLinejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
<span>Settings</span>
</button>
{/* Help — opens submenu */}
<button
onClick={() => setOverflowSubmenu('help')}
className="w-full flex items-center justify-between px-3 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
>
<div className="flex items-center gap-2.5">
<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.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span>Help</span>
</div>
<svg className="w-3.5 h-3.5 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
</svg>
</button>
</>
)}
{/* ── Notifications submenu ── */}
{overflowSubmenu === 'notifications' && (() => {
const showGlobal = user?.isGlobalAdmin && globalAdminMode;
const visibleGlobal = showGlobal ? globalNotifications : [];
const hasAny = notifications.length > 0 || visibleGlobal.length > 0;
return (
<>
<button
onClick={() => setOverflowSubmenu(null)}
className="w-full flex items-center gap-2 px-3 py-2 text-sm font-medium text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
>
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M15 19l-7-7 7-7" />
</svg>
Notifications
</button>
<div className="border-t border-gray-100 dark:border-gray-700 my-1"></div>
<div className="max-h-72 overflow-y-auto">
{!hasAny ? (
<p className="px-3 py-4 text-xs text-gray-400 text-center">No notifications</p>
) : (
<>
{/* Global admin notifications */}
{visibleGlobal.map((n) => (
<div key={n.id} className="flex items-start gap-2 px-3 py-2 hover:bg-gray-50 dark:hover:bg-gray-700">
<span className="shrink-0 mt-0.5 inline-flex items-center justify-center w-5 h-5 rounded text-[9px] font-bold text-white bg-purple-600">GA</span>
<div className="flex-1 min-w-0">
<p className="text-xs font-medium text-gray-800 dark:text-gray-200 truncate">{n.title}</p>
<p className="text-[11px] text-gray-500 dark:text-gray-400 line-clamp-2">{n.message}</p>
</div>
<button onClick={() => dismissGlobal(n.id)} className="shrink-0 text-gray-300 hover:text-gray-500">
<svg className="w-3.5 h-3.5" fill="none" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" viewBox="0 0 24 24" stroke="currentColor"><path d="M6 18L18 6M6 6l12 12"></path></svg>
</button>
</div>
))}
{/* User notifications */}
{notifications.map((n) => (
<div key={n.id} className="flex items-start gap-2 px-3 py-2 hover:bg-gray-50 dark:hover:bg-gray-700" onClick={() => markAsRead(n.id)}>
<div className="flex-1 min-w-0">
<p className="text-xs font-medium text-gray-800 dark:text-gray-200 truncate">{n.title}</p>
<p className="text-[11px] text-gray-500 dark:text-gray-400 line-clamp-2">{n.message}</p>
</div>
<button onClick={(e) => { e.stopPropagation(); removeNotification(n.id); }} className="shrink-0 text-gray-300 hover:text-gray-500">
<svg className="w-3.5 h-3.5" fill="none" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" viewBox="0 0 24 24" stroke="currentColor"><path d="M6 18L18 6M6 6l12 12"></path></svg>
</button>
</div>
))}
</>
)}
</div>
{hasAny && (
<>
<div className="border-t border-gray-100 dark:border-gray-700 my-1"></div>
<div className="flex items-center justify-between px-3 py-1.5">
<button onClick={() => { markAllAsRead(); }} className="text-[11px] text-blue-600 hover:text-blue-800">Mark all read</button>
<button onClick={() => { clearAll(); if (showGlobal) dismissAllGlobal(); }} className="text-[11px] text-gray-400 hover:text-gray-600">Clear all</button>
</div>
</>
)}
</>
);
})()}
{/* ── Settings submenu ── */}
{overflowSubmenu === 'settings' && (
<>
<button
onClick={() => setOverflowSubmenu(null)}
className="w-full flex items-center gap-2 px-3 py-2 text-sm font-medium text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
>
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M15 19l-7-7 7-7" />
</svg>
Settings
</button>
<div className="border-t border-gray-100 dark:border-gray-700 my-1"></div>
<div className="px-3 py-2">
<p className="text-[11px] font-semibold uppercase tracking-wider text-gray-400 mb-2">Administration</p>
{/* Admin Mode Toggle */}
{isTenantAdmin && (
<div className="flex items-center justify-between py-2 px-2.5 rounded-md bg-gray-50 dark:bg-gray-700 mb-1">
<div className="flex items-center gap-1.5">
<svg className="w-4 h-4 text-gray-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
</svg>
<span className="text-sm text-gray-700 dark:text-gray-200">Admin Mode</span>
{adminMode && <span className="text-[10px] text-amber-600 font-semibold">ON</span>}
</div>
<button onClick={() => { trackEvent("admin_mode_toggled", { enabled: !adminMode }); setAdminMode(!adminMode); }} className={`relative inline-flex h-5 w-9 items-center rounded-full transition-colors ${adminMode ? 'bg-amber-500' : 'bg-gray-300'}`}>
<span className={`inline-block h-3.5 w-3.5 transform rounded-full bg-white transition-transform ${adminMode ? 'translate-x-[18px]' : 'translate-x-[3px]'}`} />
</button>
</div>
)}
{/* Global Admin Toggle */}
{user?.isGlobalAdmin && (
<div className="flex items-center justify-between py-2 px-2.5 rounded-md bg-purple-50 dark:bg-purple-900/30 mb-1">
<div className="flex items-center gap-1.5">
<svg className="w-4 h-4 text-purple-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" 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>
<span className="text-sm text-gray-700 dark:text-gray-200">Global Admin</span>
{globalAdminMode && <span className="text-[10px] text-purple-700 font-semibold">ON</span>}
</div>
<button onClick={() => { trackEvent("admin_mode_toggled", { enabled: !globalAdminMode, isGlobal: true }); setGlobalAdminMode(!globalAdminMode); }} className={`relative inline-flex h-5 w-9 items-center rounded-full transition-colors ${globalAdminMode ? 'bg-purple-600' : 'bg-gray-300'}`}>
<span className={`inline-block h-3.5 w-3.5 transform rounded-full bg-white transition-transform ${globalAdminMode ? 'translate-x-[18px]' : 'translate-x-[3px]'}`} />
</button>
</div>
)}
</div>
</>
)}
{/* ── Help submenu ── */}
{overflowSubmenu === 'help' && (
<>
<button
onClick={() => setOverflowSubmenu(null)}
className="w-full flex items-center gap-2 px-3 py-2 text-sm font-medium text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
>
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M15 19l-7-7 7-7" />
</svg>
Help
</button>
<div className="border-t border-gray-100 dark:border-gray-700 my-1"></div>
<Link href="/docs" className="w-full flex items-center gap-2.5 px-3 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors" onClick={() => { setShowOverflow(false); setOverflowSubmenu(null); }}>
<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 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" /></svg>
<span>Documentation</span>
</Link>
<Link href="/roadmap" className="w-full flex items-center gap-2.5 px-3 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors" onClick={() => { setShowOverflow(false); setOverflowSubmenu(null); }}>
<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 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 01.553-.894L9 2m0 18l6-3m-6 3V2m6 15l5.447-2.724A1 1 0 0021 13.382V2.618a1 1 0 00-.553-.894L15 2m0 15V2" /></svg>
<span>Roadmap</span>
</Link>
<Link href="/changelog" className="w-full flex items-center gap-2.5 px-3 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors" onClick={() => { setShowOverflow(false); setOverflowSubmenu(null); }}>
<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 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" /></svg>
<span>Changelog</span>
</Link>
<div className="border-t border-gray-100 dark:border-gray-700 my-1"></div>
<Link href="/privacy" className="w-full flex items-center gap-2.5 px-3 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors" onClick={() => { setShowOverflow(false); setOverflowSubmenu(null); }}>
<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 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" /></svg>
<span>Privacy Policy</span>
</Link>
<Link href="/terms" className="w-full flex items-center gap-2.5 px-3 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors" onClick={() => { setShowOverflow(false); setOverflowSubmenu(null); }}>
<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 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>
<span>Terms of Use</span>
</Link>
</>
)}
{/* Help */}
<button
onClick={() => { setShowOverflow(false); setShowHelp(!showHelp); }}
className="w-full flex items-center gap-2.5 px-3 py-2 text-sm text-gray-700 hover:bg-gray-50 transition-colors"
>
<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.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span>Help</span>
</button>
</div>
)}
</div>
@@ -3,9 +3,6 @@ import {
GearIcon,
DocumentTextIcon,
ShieldCheckIcon,
UsersIcon,
BellIcon,
CircleStackIcon,
BuildingOfficeIcon,
NoSymbolIcon,
KeyIcon,
@@ -13,6 +10,7 @@ import {
ArrowDownTrayIcon,
ArrowPathIcon,
FolderIcon,
WrenchScrewdriverIcon,
} from "./sidebarIcons";
// --- Icons defined inline (not in sidebarIcons) ---
@@ -167,10 +165,12 @@ export const EXPANDABLE_NAV_GROUPS: ExpandableNavGroup[] = [
visibility: "adminOrOperator",
items: [
{
id: "cfg-validation", label: "Validation", icon: <ShieldCheckIcon />,
id: "cfg-tenant", label: "Tenant", icon: <BuildingOfficeIcon />,
items: [
{ id: "cfg-autopilot", label: "Autopilot Validation", href: "/settings/validation/autopilot" },
{ id: "cfg-hardware", label: "Hardware Whitelist", href: "/settings/validation/hardware-whitelist" },
{ id: "cfg-autopilot", label: "Autopilot Validation", href: "/settings/tenant/autopilot" },
{ id: "cfg-hardware", label: "Hardware Whitelist", href: "/settings/tenant/hardware-whitelist" },
{ id: "cfg-notifications", label: "Notifications", href: "/settings/tenant/notifications" },
{ id: "cfg-access-mgmt", label: "Access Management", href: "/settings/tenant/access-management" },
],
},
{
@@ -178,32 +178,20 @@ export const EXPANDABLE_NAV_GROUPS: ExpandableNavGroup[] = [
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-diagnostics", label: "Diagnostics Package", href: "/settings/agent/diagnostics" },
],
},
{
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 />,
id: "cfg-maintenance", label: "Maintenance", icon: <WrenchScrewdriverIcon />,
items: [
{ id: "cfg-data", label: "Data Management", href: "/settings/management/data" },
{ id: "cfg-offboarding", label: "Offboarding", href: "/settings/management/offboarding" },
],
},
{
id: "cfg-mcp", label: "MCP", icon: <ChartBarIcon />,
id: "cfg-reporting", label: "Reporting", icon: <ChartBarIcon />,
items: [
{ id: "cfg-mcp-usage", label: "Usage", href: "/settings/mcp/usage" },
{ id: "cfg-mcp-usage", label: "MCP Usage", href: "/settings/reporting/mcp-usage" },
],
},
],
File diff suppressed because one or more lines are too long