mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-10-24 12:42:38 +00:00
sidebar new structure (more logical), top navbar mobile view fix
This commit is contained in:
+1
-1
Submodule .claude/commands updated: d1435f61db...490e728f68
@@ -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 & 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;
|
||||
|
||||
+2
-2
@@ -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>
|
||||
+5
-5
@@ -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"];
|
||||
+2
-2
@@ -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");
|
||||
}
|
||||
+1
-1
@@ -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
Reference in New Issue
Block a user