mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-26 02:06:49 +00:00
d95e154aeb
Replace the static SVG fleet topology with a ReactFlow canvas laid out via dagre. Each node renders as a rack card with status pill, type badge, CPU/MEM/DISK bars, and stack/running counts. Pan, zoom, drag, and minimap are enabled; user-dragged positions persist across the 30-second poll so live metric updates no longer reset layout.
91 lines
2.6 KiB
TypeScript
91 lines
2.6 KiB
TypeScript
import dagre from '@dagrejs/dagre';
|
|
import type { Edge, Node } from '@xyflow/react';
|
|
|
|
export interface FleetTopologyNode {
|
|
id: number;
|
|
name: string;
|
|
type: 'local' | 'remote';
|
|
status: 'online' | 'offline' | 'unknown';
|
|
cpuPercent: number;
|
|
memPercent: number;
|
|
diskPercent: number;
|
|
stackCount: number;
|
|
runningCount: number;
|
|
critical: boolean;
|
|
}
|
|
|
|
export interface FleetNodeData extends Record<string, unknown> {
|
|
node: FleetTopologyNode;
|
|
}
|
|
|
|
const NODE_WIDTH = 240;
|
|
const NODE_HEIGHT = 150;
|
|
|
|
// ReactFlow inline styles cannot resolve CSS custom properties, so raw oklch
|
|
// values are used. Values match the design tokens in frontend/src/index.css.
|
|
const EDGE_BRAND = 'oklch(0.78 0.11 195)';
|
|
const EDGE_WARNING = 'oklch(0.75 0.14 75)';
|
|
const EDGE_MUTED = 'oklch(0.55 0 0)';
|
|
|
|
function edgeStyle(remote: FleetTopologyNode): {
|
|
stroke: string;
|
|
strokeWidth: number;
|
|
strokeDasharray?: string;
|
|
} {
|
|
if (remote.status !== 'online') {
|
|
return { stroke: EDGE_MUTED, strokeWidth: 1, strokeDasharray: '4 4' };
|
|
}
|
|
if (remote.critical) {
|
|
return { stroke: EDGE_WARNING, strokeWidth: 1.5 };
|
|
}
|
|
return { stroke: EDGE_BRAND, strokeWidth: 1.5 };
|
|
}
|
|
|
|
export function layoutFleetGraph(
|
|
fleetNodes: FleetTopologyNode[],
|
|
): { nodes: Node[]; edges: Edge[] } {
|
|
if (fleetNodes.length === 0) return { nodes: [], edges: [] };
|
|
|
|
const local = fleetNodes.find(n => n.type === 'local') ?? null;
|
|
const remotes = fleetNodes.filter(n => n.type !== 'local');
|
|
|
|
const g = new dagre.graphlib.Graph();
|
|
g.setGraph({ rankdir: 'LR', ranksep: 160, nodesep: 32, marginx: 20, marginy: 20 });
|
|
g.setDefaultEdgeLabel(() => ({}));
|
|
|
|
for (const n of fleetNodes) {
|
|
g.setNode(String(n.id), { width: NODE_WIDTH, height: NODE_HEIGHT });
|
|
}
|
|
|
|
if (local) {
|
|
for (const r of remotes) {
|
|
g.setEdge(String(local.id), String(r.id));
|
|
}
|
|
}
|
|
|
|
dagre.layout(g);
|
|
|
|
const flowNodes: Node[] = fleetNodes.map(n => {
|
|
const pos = g.node(String(n.id));
|
|
return {
|
|
id: String(n.id),
|
|
type: 'fleetNode',
|
|
position: { x: pos.x - pos.width / 2, y: pos.y - pos.height / 2 },
|
|
data: { node: n } satisfies FleetNodeData,
|
|
draggable: true,
|
|
};
|
|
});
|
|
|
|
const flowEdges: Edge[] = local
|
|
? remotes.map(r => ({
|
|
id: `edge-${local.id}-${r.id}`,
|
|
source: String(local.id),
|
|
target: String(r.id),
|
|
style: edgeStyle(r),
|
|
animated: false,
|
|
}))
|
|
: [];
|
|
|
|
return { nodes: flowNodes, edges: flowEdges };
|
|
}
|