changed styling

This commit is contained in:
Oliver Kieselbach
2026-03-23 05:29:28 +01:00
parent 4d71260e2e
commit ec6eb2f273
5 changed files with 122 additions and 63 deletions
@@ -4,11 +4,12 @@ import { useMemo } from "react";
import { usePageSections } from "../../hooks/usePageSections";
import { PageSectionItem } from "../../contexts/SidebarContext";
import {
GearIcon, FolderIcon, ArrowPathIcon,
NoSymbolIcon, KeyIcon,
DocumentTextIcon, SparklesIcon, ArrowDownTrayIcon,
GearIcon,
NoSymbolIcon,
DocumentTextIcon,
ChartBarIcon,
BuildingOfficeIcon,
ShieldCheckIcon,
} from "../../lib/sidebarIcons";
// Inline icon (same as MetricsSidebar had)
@@ -46,33 +47,33 @@ function GlobeIcon({ className = "w-5 h-5" }: { className?: string }) {
export function AdminPageSections() {
const items: PageSectionItem[] = useMemo(() => [
// Tenants
{ id: "management", label: "Tenant Management", icon: <BuildingOfficeIcon />, href: "/admin/tenants/management", group: "Tenants" },
{ id: "config-report", label: "Config Report", icon: <DocumentTextIcon />, href: "/admin/tenants/config-report", group: "Tenants" },
{ id: "management", label: "Tenant Management", href: "/admin/tenants/management", group: "Tenants", groupIcon: <BuildingOfficeIcon /> },
{ id: "config-report", label: "Config Report", href: "/admin/tenants/config-report", group: "Tenants" },
// Metrics
{ id: "agent-metrics", label: "Agent Metrics", icon: <ChartBarIcon />, href: "/admin/metrics/agent-metrics", group: "Metrics" },
{ id: "usage", label: "Platform Usage", icon: <TrendingUpIcon />, href: "/admin/metrics/usage", group: "Metrics" },
{ id: "agent-metrics", label: "Agent Metrics", href: "/admin/metrics/agent-metrics", group: "Metrics", groupIcon: <ChartBarIcon /> },
{ id: "usage", label: "Platform Usage", href: "/admin/metrics/usage", group: "Metrics" },
// Reports
{ id: "session-reports", label: "Session Reports", icon: <DocumentTextIcon />, href: "/admin/reports/session-reports", group: "Reports" },
{ id: "user-feedback", label: "User Feedback", icon: <SparklesIcon />, href: "/admin/reports/user-feedback", group: "Reports" },
{ id: "session-export", label: "Session Export", icon: <ArrowDownTrayIcon />, href: "/admin/reports/session-export", group: "Reports" },
{ id: "session-reports", label: "Session Reports", href: "/admin/reports/session-reports", group: "Reports", groupIcon: <DocumentTextIcon /> },
{ id: "user-feedback", label: "User Feedback", href: "/admin/reports/user-feedback", group: "Reports" },
{ id: "session-export", label: "Session Export", href: "/admin/reports/session-export", group: "Reports" },
// Security
{ id: "device-block", label: "Device Block", icon: <NoSymbolIcon />, href: "/admin/security/device-block", group: "Security" },
{ id: "version-block", label: "Version Block", icon: <NoSymbolIcon />, href: "/admin/security/version-block", group: "Security" },
{ id: "vulnerability-data", label: "Vulnerability Data", icon: <KeyIcon />, href: "/admin/security/vulnerability-data", group: "Security" },
{ id: "device-block", label: "Device Block", href: "/admin/security/device-block", group: "Security", groupIcon: <ShieldCheckIcon /> },
{ id: "version-block", label: "Version Block", href: "/admin/security/version-block", group: "Security" },
{ id: "vulnerability-data", label: "Vulnerability Data", href: "/admin/security/vulnerability-data", group: "Security" },
// Settings
{ id: "global", label: "Global Settings", icon: <GearIcon />, href: "/admin/settings/global", group: "Settings" },
{ id: "diagnostics-log-paths", label: "Diagnostics Log Paths", icon: <FolderIcon />, href: "/admin/settings/diagnostics-log-paths", group: "Settings" },
{ id: "config-reseed", label: "Config Reseed", icon: <ArrowPathIcon />, href: "/admin/settings/config-reseed", group: "Settings" },
{ id: "global", label: "Global Settings", href: "/admin/settings/global", group: "Settings", groupIcon: <GearIcon /> },
{ id: "diagnostics-log-paths", label: "Diagnostics Log Paths", href: "/admin/settings/diagnostics-log-paths", group: "Settings" },
{ id: "config-reseed", label: "Config Reseed", href: "/admin/settings/config-reseed", group: "Settings" },
// Ops (single page, no sub-items — still in a group for consistency)
{ id: "ops", label: "Maintenance", icon: <WrenchIcon />, href: "/admin/ops", group: "Ops" },
// Ops (single page)
{ id: "ops", label: "Maintenance", href: "/admin/ops", group: "Ops", groupIcon: <WrenchIcon /> },
// Software (single page)
{ id: "software", label: "Software Mapping", icon: <GlobeIcon />, href: "/admin/software", group: "Software" },
{ id: "software", label: "Software Mapping", href: "/admin/software", group: "Software", groupIcon: <GlobeIcon /> },
], []);
usePageSections(items, "Galactic Admin", "route");
@@ -5,68 +5,66 @@ import { usePageSections } from "../../hooks/usePageSections";
import { PageSectionItem } from "../../contexts/SidebarContext";
import { useTenantConfig } from "./TenantConfigContext";
import {
ShieldCheckIcon, CpuChipIcon, GearIcon, MagnifyingGlassIcon,
LockClosedIcon, UsersIcon, KeyIcon, BellIcon, CloudArrowUpIcon,
CircleStackIcon, ArrowRightOnRectangleIcon,
ShieldCheckIcon, GearIcon, UsersIcon, BellIcon, CircleStackIcon,
} from "../../lib/sidebarIcons";
/**
* Registers all tenant settings sections in the global sidebar
* with group information for expandable rendering.
* Placed inside TenantConfigProvider so it can read config for conditional sections.
* with group information for expandable rendering (GitHub-style).
*/
export function SettingsPageSections() {
const { config, user } = useTenantConfig();
const items: PageSectionItem[] = useMemo(() => {
const sections: PageSectionItem[] = [];
const isAdmin = user?.isTenantAdmin || user?.isGalacticAdmin;
// 1. Validation
if (user?.isTenantAdmin || user?.isGalacticAdmin) {
if (isAdmin) {
sections.push(
{ id: "autopilot", label: "Autopilot Validation", icon: <ShieldCheckIcon />, href: "/settings/validation/autopilot", group: "Validation" },
{ id: "hardware-whitelist", label: "Hardware Whitelist", icon: <CpuChipIcon />, href: "/settings/validation/hardware-whitelist", group: "Validation" },
{ 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" },
);
}
// 2. Agent
if (user?.isTenantAdmin || user?.isGalacticAdmin) {
if (isAdmin) {
sections.push(
{ id: "settings", label: "Agent Settings", icon: <GearIcon />, href: "/settings/agent/settings", group: "Agent" },
{ id: "analyzers", label: "Agent Analyzers", icon: <MagnifyingGlassIcon />, href: "/settings/agent/analyzers", group: "Agent" },
{ id: "settings", label: "Agent Settings", href: "/settings/agent/settings", group: "Agent", groupIcon: <GearIcon /> },
{ id: "analyzers", label: "Agent Analyzers", href: "/settings/agent/analyzers", group: "Agent" },
);
if (config?.unrestrictedModeEnabled) {
sections.push(
{ id: "unrestricted-mode", label: "Unrestricted Mode", icon: <LockClosedIcon />, href: "/settings/agent/unrestricted-mode", group: "Agent" },
{ id: "unrestricted-mode", label: "Unrestricted Mode", href: "/settings/agent/unrestricted-mode", group: "Agent" },
);
}
}
// 3. Access
if (user?.isTenantAdmin || user?.isGalacticAdmin) {
if (isAdmin) {
sections.push(
{ id: "admin-management", label: "Admin Management", icon: <UsersIcon />, href: "/settings/access/admin-management", group: "Access" },
{ id: "admin-management", label: "Admin Management", href: "/settings/access/admin-management", group: "Access", groupIcon: <UsersIcon /> },
);
}
if (config?.bootstrapTokenEnabled && (user?.isTenantAdmin || user?.isGalacticAdmin || user?.canManageBootstrapTokens)) {
if (config?.bootstrapTokenEnabled && (isAdmin || user?.canManageBootstrapTokens)) {
sections.push(
{ id: "bootstrap-sessions", label: "Bootstrap Sessions", icon: <KeyIcon />, href: "/settings/access/bootstrap-sessions", group: "Access" },
{ id: "bootstrap-sessions", label: "Bootstrap Sessions", href: "/settings/access/bootstrap-sessions", group: "Access", ...(!isAdmin ? { groupIcon: <UsersIcon /> } : {}) },
);
}
// 4. Integrations
if (user?.isTenantAdmin || user?.isGalacticAdmin) {
if (isAdmin) {
sections.push(
{ id: "notifications", label: "Notifications", icon: <BellIcon />, href: "/settings/integrations/notifications", group: "Integrations" },
{ id: "diagnostics", label: "Diagnostics", icon: <CloudArrowUpIcon />, href: "/settings/integrations/diagnostics", group: "Integrations" },
{ id: "notifications", label: "Notifications", href: "/settings/integrations/notifications", group: "Integrations", groupIcon: <BellIcon /> },
{ id: "diagnostics", label: "Diagnostics", href: "/settings/integrations/diagnostics", group: "Integrations" },
);
}
// 5. Management
if (user?.isTenantAdmin || user?.isGalacticAdmin) {
if (isAdmin) {
sections.push(
{ id: "data", label: "Data Management", icon: <CircleStackIcon />, href: "/settings/management/data", group: "Management" },
{ id: "offboarding", label: "Offboarding", icon: <ArrowRightOnRectangleIcon />, href: "/settings/management/offboarding", group: "Management" },
{ id: "data", label: "Data Management", href: "/settings/management/data", group: "Management", groupIcon: <CircleStackIcon /> },
{ id: "offboarding", label: "Offboarding", href: "/settings/management/offboarding", group: "Management" },
);
}
@@ -129,13 +129,13 @@ export function GlobalSidebar({ children }: { children: ReactNode }) {
// Group items by their group name (preserving order of first appearance)
const groupedSections = useMemo(() => {
if (!hasGroups) return null;
const groups: { name: string; items: PageSectionItem[] }[] = [];
const groups: { name: string; icon?: ReactNode; items: PageSectionItem[] }[] = [];
const seen = new Map<string, number>();
for (const item of pageSections) {
const g = item.group ?? "";
if (!seen.has(g)) {
seen.set(g, groups.length);
groups.push({ name: g, items: [] });
groups.push({ name: g, icon: item.groupIcon, items: [] });
}
groups[seen.get(g)!].items.push(item);
}
@@ -401,52 +401,110 @@ export function GlobalSidebar({ children }: { children: ReactNode }) {
</p>
)}
{/* Grouped sections with expand/collapse */}
{/* Grouped sections with expand/collapse (GitHub-style) */}
{groupedSections ? (
groupedSections.map((group) => {
<ul className="space-y-0.5">
{groupedSections.map((group) => {
const isExpanded = expandedGroups.has(group.name);
const groupHasActive = group.items.some((item) => item.id === activeSectionId);
const firstHref = group.items[0]?.href;
if (collapseState === "icons" && !isMobile) {
// In icons mode, show all items flat (no headers)
// In icons mode: show group icon as a link to the first item
return (
<ul key={group.name} className="space-y-0.5">
{group.items.map(renderSectionItem)}
</ul>
<li key={group.name}>
<Link
href={firstHref ?? "#"}
onClick={() => setMobileDrawerOpen(false)}
className={`flex items-center justify-center px-2 py-2 rounded-md text-sm transition-colors relative group ${
groupHasActive
? "bg-blue-50 text-blue-700 font-semibold dark:bg-blue-900/30 dark:text-blue-300"
: "text-gray-600 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-gray-200"
}`}
title={group.name}
>
<span className="shrink-0 w-4.5 h-4.5">
{group.icon ? group.icon : renderIcon(undefined, "w-4.5 h-4.5")}
</span>
<span className="absolute left-full ml-2 px-2 py-1 rounded bg-gray-900 text-white text-xs whitespace-nowrap opacity-0 pointer-events-none group-hover:opacity-100 transition-opacity z-50 dark:bg-gray-700">
{group.name}
</span>
</Link>
</li>
);
}
// Full mode: GitHub-style with icon + label + chevron
return (
<div key={group.name} className="mt-1">
{/* Group header — clickable to expand/collapse */}
<li key={group.name} className="mt-0.5">
<button
onClick={() => toggleGroup(group.name)}
className={`w-full flex items-center gap-1.5 px-3 py-1 text-[11px] font-semibold uppercase tracking-wider transition-colors rounded-md ${
className={`w-full flex items-center gap-2.5 px-3 py-1.5 rounded-md text-sm transition-colors ${
groupHasActive
? "text-blue-600 dark:text-blue-400"
: "text-gray-400 hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300"
? "text-blue-700 font-semibold dark:text-blue-300"
: "text-gray-600 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-gray-200"
}`}
>
{/* Group icon */}
<span className="shrink-0 w-4 h-4">
{group.icon ? group.icon : renderIcon(undefined, "w-4 h-4")}
</span>
{/* Group label */}
<span className="truncate flex-1 text-left">{group.name}</span>
{/* Chevron */}
<svg
className={`w-3 h-3 transition-transform duration-150 ${isExpanded ? "rotate-90" : ""}`}
className={`w-3.5 h-3.5 shrink-0 text-gray-400 transition-transform duration-150 ${isExpanded ? "rotate-90" : ""}`}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2.5}
strokeWidth={2}
>
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
</svg>
<span>{group.name}</span>
</button>
{/* Group items — shown when expanded */}
{/* Sub-items — smaller, indented, no icons (GitHub-style) */}
{isExpanded && (
<ul className="space-y-0.5 mt-0.5">
{group.items.map(renderSectionItem)}
<ul className="mt-0.5 space-y-px">
{group.items.map((item) => {
const active = activeSectionId === item.id;
if (pageSectionsMode === "route" && item.href) {
return (
<li key={item.id}>
<Link
href={item.href}
onClick={() => setMobileDrawerOpen(false)}
className={`block pl-10 pr-3 py-1 rounded-md text-[13px] transition-colors ${
active
? "bg-blue-50 text-blue-700 font-medium dark:bg-blue-900/30 dark:text-blue-300"
: "text-gray-500 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-gray-200"
}`}
>
{item.label}
</Link>
</li>
);
}
return (
<li key={item.id}>
<button
onClick={() => scrollTo(item.id)}
className={`w-full text-left block pl-10 pr-3 py-1 rounded-md text-[13px] transition-colors ${
active
? "bg-blue-50 text-blue-700 font-medium dark:bg-blue-900/30 dark:text-blue-300"
: "text-gray-500 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-gray-200"
}`}
>
{item.label}
</button>
</li>
);
})}
</ul>
)}
</div>
</li>
);
})
})}
</ul>
) : (
/* Flat (ungrouped) sections — existing behavior */
<ul className="space-y-0.5">
@@ -10,6 +10,8 @@ export interface PageSectionItem {
href?: string;
/** Optional group name — items sharing the same group are rendered under a collapsible header */
group?: string;
/** Icon for the group header (only needs to be set on the first item of a group) */
groupIcon?: ReactNode;
}
interface SidebarContextValue {
File diff suppressed because one or more lines are too long