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:
@@ -1,135 +0,0 @@
|
||||
import { useState, useMemo, useCallback, useEffect } from 'react';
|
||||
import type { ExecutionDetail, ProcessorNode } from '../api/types';
|
||||
|
||||
export interface IterationData {
|
||||
count: number;
|
||||
current: number;
|
||||
}
|
||||
|
||||
export interface OverlayState {
|
||||
isActive: boolean;
|
||||
toggle: () => void;
|
||||
executedNodes: Set<string>;
|
||||
executedEdges: Set<string>;
|
||||
durations: Map<string, number>;
|
||||
sequences: Map<string, number>;
|
||||
statuses: Map<string, string>;
|
||||
iterationData: Map<string, IterationData>;
|
||||
selectedNodeId: string | null;
|
||||
selectNode: (nodeId: string | null) => void;
|
||||
setIteration: (nodeId: string, iteration: number) => void;
|
||||
}
|
||||
|
||||
/** Walk the processor tree and collect execution data keyed by diagramNodeId */
|
||||
function collectProcessorData(
|
||||
processors: ProcessorNode[],
|
||||
executedNodes: Set<string>,
|
||||
durations: Map<string, number>,
|
||||
sequences: Map<string, number>,
|
||||
statuses: Map<string, string>,
|
||||
counter: { seq: number },
|
||||
) {
|
||||
for (const proc of processors) {
|
||||
const nodeId = proc.diagramNodeId;
|
||||
if (nodeId) {
|
||||
executedNodes.add(nodeId);
|
||||
durations.set(nodeId, proc.durationMs ?? 0);
|
||||
sequences.set(nodeId, ++counter.seq);
|
||||
if (proc.status) statuses.set(nodeId, proc.status);
|
||||
}
|
||||
if (proc.children && proc.children.length > 0) {
|
||||
collectProcessorData(proc.children, executedNodes, durations, sequences, statuses, counter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Determine which edges are executed (both source and target are executed) */
|
||||
function computeExecutedEdges(
|
||||
executedNodes: Set<string>,
|
||||
edges: Array<{ sourceId?: string; targetId?: string }>,
|
||||
): Set<string> {
|
||||
const result = new Set<string>();
|
||||
for (const edge of edges) {
|
||||
if (edge.sourceId && edge.targetId
|
||||
&& executedNodes.has(edge.sourceId) && executedNodes.has(edge.targetId)) {
|
||||
result.add(`${edge.sourceId}->${edge.targetId}`);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function useExecutionOverlay(
|
||||
execution: ExecutionDetail | null | undefined,
|
||||
edges: Array<{ sourceId?: string; targetId?: string }> = [],
|
||||
): OverlayState {
|
||||
const [isActive, setIsActive] = useState(!!execution);
|
||||
const [selectedNodeId, setSelectedNodeId] = useState<string | null>(null);
|
||||
const [iterations, setIterations] = useState<Map<string, number>>(new Map());
|
||||
|
||||
// Activate overlay when an execution is loaded
|
||||
useEffect(() => {
|
||||
if (execution) setIsActive(true);
|
||||
}, [execution]);
|
||||
|
||||
const { executedNodes, durations, sequences, statuses, iterationData } = useMemo(() => {
|
||||
const en = new Set<string>();
|
||||
const dur = new Map<string, number>();
|
||||
const seq = new Map<string, number>();
|
||||
const st = new Map<string, string>();
|
||||
const iter = new Map<string, IterationData>();
|
||||
|
||||
if (!execution?.processors) {
|
||||
return { executedNodes: en, durations: dur, sequences: seq, statuses: st, iterationData: iter };
|
||||
}
|
||||
|
||||
collectProcessorData(execution.processors, en, dur, seq, st, { seq: 0 });
|
||||
|
||||
return { executedNodes: en, durations: dur, sequences: seq, statuses: st, iterationData: iter };
|
||||
}, [execution]);
|
||||
|
||||
const executedEdges = useMemo(
|
||||
() => computeExecutedEdges(executedNodes, edges),
|
||||
[executedNodes, edges],
|
||||
);
|
||||
|
||||
const toggle = useCallback(() => setIsActive((v) => !v), []);
|
||||
const selectNode = useCallback((nodeId: string | null) => setSelectedNodeId(nodeId), []);
|
||||
const setIteration = useCallback((nodeId: string, iteration: number) => {
|
||||
setIterations((prev) => {
|
||||
const next = new Map(prev);
|
||||
next.set(nodeId, iteration);
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
|
||||
// Keyboard shortcut: E to toggle overlay
|
||||
useEffect(() => {
|
||||
function handleKey(e: KeyboardEvent) {
|
||||
if (e.key === 'e' || e.key === 'E') {
|
||||
const target = e.target as HTMLElement;
|
||||
if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.tagName === 'SELECT') return;
|
||||
e.preventDefault();
|
||||
setIsActive((v) => !v);
|
||||
}
|
||||
}
|
||||
window.addEventListener('keydown', handleKey);
|
||||
return () => window.removeEventListener('keydown', handleKey);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
isActive,
|
||||
toggle,
|
||||
executedNodes,
|
||||
executedEdges,
|
||||
durations,
|
||||
sequences,
|
||||
statuses,
|
||||
iterationData: new Map([...iterationData].map(([k, v]) => {
|
||||
const current = iterations.get(k) ?? v.current;
|
||||
return [k, { ...v, current }];
|
||||
})),
|
||||
selectedNodeId,
|
||||
selectNode,
|
||||
setIteration,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user