import { Outlet, useNavigate, useLocation } from 'react-router'; import { AppShell, Sidebar, TopBar, CommandPalette, CommandPaletteProvider, GlobalFilterProvider, ToastProvider, BreadcrumbProvider, useCommandPalette, useGlobalFilters } from '@cameleer/design-system'; import type { SidebarApp, SearchResult } from '@cameleer/design-system'; import { useRouteCatalog } from '../api/queries/catalog'; import { useAgents } from '../api/queries/agents'; import { useSearchExecutions } from '../api/queries/executions'; import { useAuthStore } from '../auth/auth-store'; import { useState, useMemo, useCallback, useEffect } from 'react'; function healthToColor(health: string): string { switch (health) { case 'live': return 'success'; case 'stale': return 'warning'; case 'dead': return 'error'; default: return 'auto'; } } function buildSearchData( catalog: any[] | undefined, agents: any[] | undefined, ): SearchResult[] { if (!catalog) return []; const results: SearchResult[] = []; for (const app of catalog) { const liveAgents = (app.agents || []).filter((a: any) => a.status === 'live').length; results.push({ id: app.appId, category: 'application', title: app.appId, badges: [{ label: (app.health || 'unknown').toUpperCase(), color: healthToColor(app.health) }], meta: `${(app.routes || []).length} routes · ${(app.agents || []).length} agents (${liveAgents} live) · ${(app.exchangeCount ?? 0).toLocaleString()} exchanges`, path: `/apps/${app.appId}`, }); for (const route of (app.routes || [])) { results.push({ id: route.routeId, category: 'route', title: route.routeId, badges: [{ label: app.appId }], meta: `${(route.exchangeCount ?? 0).toLocaleString()} exchanges`, path: `/apps/${app.appId}/${route.routeId}`, }); } } if (agents) { for (const agent of agents) { results.push({ id: agent.id, category: 'agent', title: agent.name, badges: [{ label: (agent.state || 'unknown').toUpperCase(), color: healthToColor((agent.state || '').toLowerCase()) }], meta: `${agent.application} · ${agent.version || ''}${agent.agentTps != null ? ` · ${agent.agentTps.toFixed(1)} msg/s` : ''}`, path: `/agents/${agent.application}/${agent.id}`, }); } } return results; } function formatDuration(ms: number): string { if (ms >= 60_000) return `${(ms / 1000).toFixed(0)}s`; if (ms >= 1000) return `${(ms / 1000).toFixed(2)}s`; return `${ms}ms`; } function statusToColor(status: string): string { switch (status) { case 'COMPLETED': return 'success'; case 'FAILED': return 'error'; case 'RUNNING': return 'running'; default: return 'warning'; } } function useDebouncedValue(value: T, delayMs: number): T { const [debounced, setDebounced] = useState(value); useEffect(() => { const timer = setTimeout(() => setDebounced(value), delayMs); return () => clearTimeout(timer); }, [value, delayMs]); return debounced; } function LayoutContent() { const navigate = useNavigate(); const location = useLocation(); const { timeRange } = useGlobalFilters(); const { data: catalog } = useRouteCatalog(timeRange.start.toISOString(), timeRange.end.toISOString()); const { data: agents } = useAgents(); const { username, logout } = useAuthStore(); const { open: paletteOpen, setOpen: setPaletteOpen } = useCommandPalette(); // Exchange full-text search via command palette const [paletteQuery, setPaletteQuery] = useState(''); const debouncedQuery = useDebouncedValue(paletteQuery, 300); const { data: exchangeResults } = useSearchExecutions( { text: debouncedQuery || undefined, offset: 0, limit: 10 }, false, ); const sidebarApps: SidebarApp[] = useMemo(() => { if (!catalog) return []; return catalog.map((app: any) => ({ id: app.appId, name: app.appId, health: app.health as 'live' | 'stale' | 'dead', exchangeCount: app.exchangeCount, routes: (app.routes || []).map((r: any) => ({ id: r.routeId, name: r.routeId, exchangeCount: r.exchangeCount, })), agents: (app.agents || []).map((a: any) => ({ id: a.id, name: a.name, status: a.status as 'live' | 'stale' | 'dead', tps: a.tps, })), })); }, [catalog]); const catalogData = useMemo( () => buildSearchData(catalog, agents as any[]), [catalog, agents], ); const searchData: SearchResult[] = useMemo(() => { const exchangeItems: SearchResult[] = (exchangeResults?.data || []).map((e: any) => ({ id: e.executionId, category: 'exchange' as const, title: e.executionId, badges: [{ label: e.status, color: statusToColor(e.status) }], meta: `${e.routeId} · ${e.applicationName ?? ''} · ${formatDuration(e.durationMs)}`, path: `/exchanges/${e.executionId}`, serverFiltered: true, matchContext: e.highlight ?? undefined, })); const attributeItems: SearchResult[] = []; if (debouncedQuery) { const q = debouncedQuery.toLowerCase(); for (const e of exchangeResults?.data || []) { if (!e.attributes) continue; for (const [key, value] of Object.entries(e.attributes as Record)) { if (key.toLowerCase().includes(q) || String(value).toLowerCase().includes(q)) { attributeItems.push({ id: `${e.executionId}-attr-${key}`, category: 'attribute' as const, title: `${key} = "${value}"`, badges: [{ label: e.status, color: statusToColor(e.status) }], meta: `${e.executionId} · ${e.routeId} · ${e.applicationName ?? ''}`, path: `/exchanges/${e.executionId}`, serverFiltered: true, }); } } } } return [...catalogData, ...exchangeItems, ...attributeItems]; }, [catalogData, exchangeResults, debouncedQuery]); const breadcrumb = useMemo(() => { const LABELS: Record = { apps: 'Applications', agents: 'Agents', exchanges: 'Exchanges', routes: 'Routes', admin: 'Admin', 'api-docs': 'API Docs', rbac: 'Users & Roles', audit: 'Audit Log', oidc: 'OIDC', database: 'Database', opensearch: 'OpenSearch', appconfig: 'App Config', }; const parts = location.pathname.split('/').filter(Boolean); return parts.map((part, i) => ({ label: LABELS[part] ?? part, ...(i < parts.length - 1 ? { href: '/' + parts.slice(0, i + 1).join('/') } : {}), })); }, [location.pathname]); const handleLogout = useCallback(() => { logout(); navigate('/login'); }, [logout, navigate]); const handlePaletteSelect = useCallback((result: any) => { if (result.path) { navigate(result.path, { state: result.path ? { sidebarReveal: result.path } : undefined }); } setPaletteOpen(false); }, [navigate, setPaletteOpen]); return ( } > setPaletteOpen(false)} onOpen={() => setPaletteOpen(true)} onSelect={handlePaletteSelect} onQueryChange={setPaletteQuery} data={searchData} />
); } export function LayoutShell() { return ( ); }