feat: migrate UI to @cameleer/design-system, add backend endpoints
Backend: - Add agent_events table (V5) and lifecycle event recording - Add route catalog endpoint (GET /routes/catalog) - Add route metrics endpoint (GET /routes/metrics) - Add agent events endpoint (GET /agents/events-log) - Enrich AgentInstanceResponse with tps, errorRate, activeRoutes, uptimeSeconds - Add TimescaleDB retention/compression policies (V6) Frontend: - Replace custom Mission Control UI with @cameleer/design-system components - Rebuild all pages: Dashboard, ExchangeDetail, RoutesMetrics, AgentHealth, AgentInstance, RBAC, AuditLog, OIDC, DatabaseAdmin, OpenSearchAdmin, Swagger - New LayoutShell with design system AppShell, Sidebar, TopBar, CommandPalette - Consume design system from Gitea npm registry (@cameleer/design-system@0.0.1) - Add .npmrc for scoped registry, update Dockerfile with REGISTRY_TOKEN arg CI: - Pass REGISTRY_TOKEN build-arg to UI Docker build step Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
84
ui/src/components/LayoutShell.tsx
Normal file
84
ui/src/components/LayoutShell.tsx
Normal file
@@ -0,0 +1,84 @@
|
||||
import { Outlet, useNavigate, useLocation } from 'react-router';
|
||||
import { AppShell, Sidebar, TopBar, CommandPalette, CommandPaletteProvider, GlobalFilterProvider, useCommandPalette } from '@cameleer/design-system';
|
||||
import { useRouteCatalog } from '../api/queries/catalog';
|
||||
import { useAuthStore } from '../auth/auth-store';
|
||||
import { useMemo, useCallback } from 'react';
|
||||
import type { SidebarApp } from '@cameleer/design-system';
|
||||
|
||||
function LayoutContent() {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const { data: catalog } = useRouteCatalog();
|
||||
const { username, roles } = useAuthStore();
|
||||
const { open: paletteOpen, setOpen: setPaletteOpen } = useCommandPalette();
|
||||
|
||||
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 breadcrumb = useMemo(() => {
|
||||
const parts = location.pathname.split('/').filter(Boolean);
|
||||
return parts.map((part, i) => ({
|
||||
label: part,
|
||||
href: '/' + parts.slice(0, i + 1).join('/'),
|
||||
}));
|
||||
}, [location.pathname]);
|
||||
|
||||
const handlePaletteSelect = useCallback((result: any) => {
|
||||
if (result.path) navigate(result.path);
|
||||
setPaletteOpen(false);
|
||||
}, [navigate, setPaletteOpen]);
|
||||
|
||||
const isAdmin = roles.includes('ADMIN');
|
||||
|
||||
return (
|
||||
<AppShell
|
||||
sidebar={
|
||||
<Sidebar
|
||||
apps={sidebarApps}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<TopBar
|
||||
breadcrumb={breadcrumb}
|
||||
user={username ? { name: username } : undefined}
|
||||
/>
|
||||
<CommandPalette
|
||||
open={paletteOpen}
|
||||
onClose={() => setPaletteOpen(false)}
|
||||
onSelect={handlePaletteSelect}
|
||||
data={[]}
|
||||
/>
|
||||
<main style={{ flex: 1, overflow: 'auto', padding: '1.5rem' }}>
|
||||
<Outlet />
|
||||
</main>
|
||||
</AppShell>
|
||||
);
|
||||
}
|
||||
|
||||
export function LayoutShell() {
|
||||
return (
|
||||
<CommandPaletteProvider>
|
||||
<GlobalFilterProvider>
|
||||
<LayoutContent />
|
||||
</GlobalFilterProvider>
|
||||
</CommandPaletteProvider>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user