From 5b3beb517d52f8573ca52e7087339b43fa3ba530 Mon Sep 17 00:00:00 2001 From: ZonForge DevOps Date: Wed, 17 Jun 2026 00:19:12 -0400 Subject: [PATCH] Polish enterprise header and profile menu --- apps/web-dashboard/src/App.tsx | 4 +- .../brand/EnterpriseProfileMenu.tsx | 147 ++++++++++++++++++ .../src/components/brand/ZonForgeBrand.tsx | 79 ++++++++++ .../components/customer/CustomerLayout.tsx | 78 ++++++++-- .../components/customer/CustomerSidebar.tsx | 17 +- .../src/components/shared/ui.tsx | 4 + .../web-dashboard/src/layouts/AdminLayout.tsx | 38 +++-- .../src/styles/admin-workspace.css | 17 +- 8 files changed, 342 insertions(+), 42 deletions(-) create mode 100644 apps/web-dashboard/src/components/brand/EnterpriseProfileMenu.tsx create mode 100644 apps/web-dashboard/src/components/brand/ZonForgeBrand.tsx diff --git a/apps/web-dashboard/src/App.tsx b/apps/web-dashboard/src/App.tsx index 8021c4d9..586f99c2 100644 --- a/apps/web-dashboard/src/App.tsx +++ b/apps/web-dashboard/src/App.tsx @@ -5,6 +5,7 @@ import { api } from '@/lib/api' import CustomerLayout from '@/layouts/CustomerLayout' import AdminLayout from '@/layouts/AdminLayout' import SuperAdminLayout from '@/layouts/SuperAdminLayout' +import { ZonForgeBrand } from '@/components/brand/ZonForgeBrand' import { APP_ROOT_ROUTE, DEFAULT_AUTHENTICATED_ROUTE, FORBIDDEN_ROUTE, NOT_FOUND_ROUTE, ONBOARDING_ROUTE, SUPER_ADMIN_ROUTE, resolvePostLoginRedirect, resolveProtectedRouteRedirect, resolveRoleHomeRoute } from '@/lib/auth-routing' import { useAuthStore } from '@/stores/auth.store' @@ -188,8 +189,9 @@ function PageLoader() { return (
+
- Loading… + Loading ZonForge Sentinel...
) diff --git a/apps/web-dashboard/src/components/brand/EnterpriseProfileMenu.tsx b/apps/web-dashboard/src/components/brand/EnterpriseProfileMenu.tsx new file mode 100644 index 00000000..4e1f9610 --- /dev/null +++ b/apps/web-dashboard/src/components/brand/EnterpriseProfileMenu.tsx @@ -0,0 +1,147 @@ +import { useEffect, useRef, useState } from 'react' +import { Link } from 'react-router-dom' +import { + Building2, + ChevronDown, + CircleHelp, + CreditCard, + KeyRound, + LogOut, + ScrollText, + Settings, + UserCircle, + Users, +} from 'lucide-react' + +type ProfileLink = { + label: string + to: string + icon: 'profile' | 'organization' | 'team' | 'keys' | 'audit' | 'billing' | 'support' +} + +const iconMap = { + profile: UserCircle, + organization: Building2, + team: Users, + keys: KeyRound, + audit: ScrollText, + billing: CreditCard, + support: CircleHelp, +} + +export function EnterpriseProfileMenu({ + displayName, + role, + links, + onSignOut, + align = 'right', +}: { + displayName: string + role: string + links: ProfileLink[] + onSignOut: () => void + align?: 'left' | 'right' +}) { + const [open, setOpen] = useState(false) + const menuRef = useRef(null) + const initials = displayName + .split(/\s+/) + .filter(Boolean) + .map((part) => part[0]) + .join('') + .slice(0, 2) + .toUpperCase() || 'ZF' + + useEffect(() => { + if (!open) return + + function handlePointerDown(event: PointerEvent) { + if (!menuRef.current?.contains(event.target as Node)) { + setOpen(false) + } + } + + function handleKeyDown(event: KeyboardEvent) { + if (event.key === 'Escape') setOpen(false) + } + + document.addEventListener('pointerdown', handlePointerDown) + document.addEventListener('keydown', handleKeyDown) + return () => { + document.removeEventListener('pointerdown', handlePointerDown) + document.removeEventListener('keydown', handleKeyDown) + } + }, [open]) + + return ( +
+ + + {open ? ( +
+
+

{displayName}

+

ZonForge Sentinel

+

+ AI-Native Cybersecurity Platform +

+
+
+ {links.map((item) => { + const Icon = iconMap[item.icon] + return ( + setOpen(false)} + > + + {item.label} + + ) + })} +
+
+ +
+
+ ) : null} +
+ ) +} + +export type { ProfileLink } diff --git a/apps/web-dashboard/src/components/brand/ZonForgeBrand.tsx b/apps/web-dashboard/src/components/brand/ZonForgeBrand.tsx new file mode 100644 index 00000000..b26b56bb --- /dev/null +++ b/apps/web-dashboard/src/components/brand/ZonForgeBrand.tsx @@ -0,0 +1,79 @@ +import type { SVGProps } from 'react' + +type ZonForgeMarkProps = SVGProps & { + title?: string +} + +export function ZonForgeMark({ title = 'ZonForge', className = '', ...props }: ZonForgeMarkProps) { + return ( + + + + + + + + + + + ) +} + +type ZonForgeWordmarkProps = { + compact?: boolean + className?: string + subtitleClassName?: string +} + +export function ZonForgeWordmark({ + compact = false, + className = '', + subtitleClassName = '', +}: ZonForgeWordmarkProps) { + return ( +
+
+ ZonForge + Sentinel +
+ {!compact ? ( +

+ AI-Native Cybersecurity Platform +

+ ) : null} +
+ ) +} + +export function ZonForgeBrand({ + compact = false, + className = '', +}: { + compact?: boolean + className?: string +}) { + return ( +
+
+ +
+ +
+ ) +} diff --git a/apps/web-dashboard/src/components/customer/CustomerLayout.tsx b/apps/web-dashboard/src/components/customer/CustomerLayout.tsx index 4429170e..abbc303d 100644 --- a/apps/web-dashboard/src/components/customer/CustomerLayout.tsx +++ b/apps/web-dashboard/src/components/customer/CustomerLayout.tsx @@ -1,13 +1,14 @@ import React, { useMemo, useState } from "react"; import { Outlet, useLocation, useNavigate } from "react-router-dom"; +import { Bell, CircleHelp, Search, Settings } from "lucide-react"; import { useAuthStore } from "@/stores/auth.store"; import { useQuery } from "@tanstack/react-query"; import { api } from "@/lib/api"; import type { BillingSubscriptionResponse } from "@/lib/api"; +import { EnterpriseProfileMenu, type ProfileLink } from "@/components/brand/EnterpriseProfileMenu"; +import { ZonForgeBrand } from "@/components/brand/ZonForgeBrand"; import CustomerSidebar from "./CustomerSidebar"; import { UpgradeBanner } from "@/components/billing/UpgradeBanner"; -import { TrialCountdown } from "@/components/billing/TrialCountdown"; -import { PlanBadge } from "@/components/billing/PlanBadge"; type StoredAuth = { state?: { @@ -99,6 +100,7 @@ export default function CustomerLayout() { const location = useLocation(); const navigate = useNavigate(); const user = useAuthStore((state) => state.user); + const clearAuth = useAuthStore((state) => state.clearAuth); const { data: subData } = useQuery({ queryKey: ['customer', 'subscription', 'layout'], @@ -114,6 +116,26 @@ export default function CustomerLayout() { const role = user?.membership?.role || user?.role || auth?.user?.role || "viewer"; const isSocWorkspace = location.pathname === '/dashboard' || location.pathname.startsWith('/dashboard/') || location.pathname.startsWith('/app/customer-investigations'); + const profileLinks: ProfileLink[] = [ + { label: "My Profile", to: "/app/customer-settings", icon: "profile" }, + { label: "Organization Settings", to: "/app/customer-settings", icon: "organization" }, + { label: "Team Members", to: "/app/customer-settings/team", icon: "team" }, + { label: "API Keys", to: "/app/customer-settings", icon: "keys" }, + { label: "Audit Logs", to: "/app/reports", icon: "audit" }, + { label: "Billing", to: "/app/billing", icon: "billing" }, + { label: "Support", to: "/app/customer-settings", icon: "support" }, + ]; + + async function signOut() { + try { + await api.auth.logout(); + } catch { + // Logout must remain available even if the session is already expired. + } finally { + clearAuth(); + navigate("/login", { replace: true }); + } + } return (
@@ -131,8 +153,8 @@ export default function CustomerLayout() {
-
-
+
+
+

- {isSocWorkspace ? 'Security Operations' : 'Executive Overview'} + ZonForge Sentinel + {isSocWorkspace ? 'Security Operations' : 'Executive Overview'}

-

- {pageTitle(location.pathname)} -

+
+

+ {pageTitle(location.pathname)} +

+ + ZonForge Sentinel + +
+

AI-Native Cybersecurity Platform

-
-
-

{displayName}

-

{role}

-
-
- {displayName.slice(0, 2).toUpperCase()} -
+
+ + + + +
diff --git a/apps/web-dashboard/src/components/customer/CustomerSidebar.tsx b/apps/web-dashboard/src/components/customer/CustomerSidebar.tsx index 8036aa8f..00c2f406 100644 --- a/apps/web-dashboard/src/components/customer/CustomerSidebar.tsx +++ b/apps/web-dashboard/src/components/customer/CustomerSidebar.tsx @@ -2,6 +2,7 @@ import React from "react"; import { NavLink, useLocation } from "react-router-dom"; import { PlanBadge } from "@/components/billing/PlanBadge"; import { TrialCountdown } from "@/components/billing/TrialCountdown"; +import { ZonForgeBrand } from "@/components/brand/ZonForgeBrand"; type CustomerSidebarProps = { open: boolean; @@ -192,16 +193,12 @@ function SidebarContent({ return (
-
-
- ZF -
-
-

- Customer Workspace -

-

{workspaceName}

-
+ +
+

+ Customer Workspace +

+

{workspaceName}

diff --git a/apps/web-dashboard/src/components/shared/ui.tsx b/apps/web-dashboard/src/components/shared/ui.tsx index 45522dc3..c6cc3911 100644 --- a/apps/web-dashboard/src/components/shared/ui.tsx +++ b/apps/web-dashboard/src/components/shared/ui.tsx @@ -1,6 +1,7 @@ import { type CSSProperties, type ReactNode, forwardRef } from 'react' import { clsx } from 'clsx' import { Loader2 } from 'lucide-react' +import { ZonForgeMark } from '@/components/brand/ZonForgeBrand' // ───────────────────────────────────────────── // BADGE @@ -153,6 +154,9 @@ export function EmptyState({ }) { return (
+
+ +
diff --git a/apps/web-dashboard/src/layouts/AdminLayout.tsx b/apps/web-dashboard/src/layouts/AdminLayout.tsx index 7774b503..6be7e8ac 100644 --- a/apps/web-dashboard/src/layouts/AdminLayout.tsx +++ b/apps/web-dashboard/src/layouts/AdminLayout.tsx @@ -1,8 +1,10 @@ import { useEffect, useState } from "react"; -import { Bell, CircleHelp, Command, LogOut, Menu, Search, Settings, X } from "lucide-react"; +import { Bell, CircleHelp, Menu, Search, Settings, X } from "lucide-react"; import { NavLink, Outlet, useLocation, useNavigate } from "react-router-dom"; import { apiClient } from "../api/client"; import { useAuthStore } from "../store/authStore"; +import { EnterpriseProfileMenu, type ProfileLink } from "@/components/brand/EnterpriseProfileMenu"; +import { ZonForgeBrand, ZonForgeMark } from "@/components/brand/ZonForgeBrand"; const groups = [ { label: "Administration", items: [["/app/admin", "Overview", "M4 13h6V4H4v9Zm10 7h6v-9h-6v9ZM4 20h6v-3H4v3Zm10-13h6V4h-6v3Z", true], ["/app/admin/users", "Users", "M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2M9 11a4 4 0 1 0 0-8 4 4 0 0 0 0 8Zm13 10v-2a4 4 0 0 0-3-3.87M16 3.13a4 4 0 0 1 0 7.75"], ["/app/admin/access-review", "Access Review", "M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10Zm-3-10 2 2 4-4"]] }, @@ -18,7 +20,17 @@ export default function AdminLayout() { const navigate = useNavigate(); const location = useLocation(); const [mobileNavOpen, setMobileNavOpen] = useState(false); - const initials = (user?.name ?? "Admin Tenant").split(" ").map((part) => part[0]).join("").slice(0, 2).toUpperCase(); + const displayName = user?.name ?? "Admin Tenant"; + const roleLabel = user?.role === "TENANT_ADMIN" ? "Tenant Admin" : user?.role?.replace(/_/g, " ") ?? "Administrator"; + const profileLinks: ProfileLink[] = [ + { label: "My Profile", to: "/app/admin/settings", icon: "profile" }, + { label: "Organization Settings", to: "/app/admin/settings", icon: "organization" }, + { label: "Team Members", to: "/app/admin/users", icon: "team" }, + { label: "API Keys", to: "/app/admin/api-keys", icon: "keys" }, + { label: "Audit Logs", to: "/app/admin/audit", icon: "audit" }, + { label: "Billing", to: "/app/admin/billing", icon: "billing" }, + { label: "Support", to: "/app/admin/settings", icon: "support" }, + ]; useEffect(() => setMobileNavOpen(false), [location.pathname]); @@ -29,7 +41,10 @@ export default function AdminLayout() { return (