161 lines
4.7 KiB
TypeScript
161 lines
4.7 KiB
TypeScript
|
|
import type { ReactNode } from 'react';
|
||
|
|
import type { SidebarTreeNode } from '@cameleer/design-system';
|
||
|
|
|
||
|
|
/* ------------------------------------------------------------------ */
|
||
|
|
/* Domain types (moved out of DS — no longer exported there) */
|
||
|
|
/* ------------------------------------------------------------------ */
|
||
|
|
|
||
|
|
export interface SidebarRoute {
|
||
|
|
id: string;
|
||
|
|
name: string;
|
||
|
|
exchangeCount: number;
|
||
|
|
}
|
||
|
|
|
||
|
|
export interface SidebarAgent {
|
||
|
|
id: string;
|
||
|
|
name: string;
|
||
|
|
status: 'live' | 'stale' | 'dead';
|
||
|
|
tps?: number;
|
||
|
|
}
|
||
|
|
|
||
|
|
export interface SidebarApp {
|
||
|
|
id: string;
|
||
|
|
name: string;
|
||
|
|
health: 'live' | 'stale' | 'dead';
|
||
|
|
exchangeCount: number;
|
||
|
|
routes: SidebarRoute[];
|
||
|
|
agents: SidebarAgent[];
|
||
|
|
}
|
||
|
|
|
||
|
|
/* ------------------------------------------------------------------ */
|
||
|
|
/* Formatting helpers */
|
||
|
|
/* ------------------------------------------------------------------ */
|
||
|
|
|
||
|
|
export function formatCount(n: number): string {
|
||
|
|
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}m`;
|
||
|
|
if (n >= 1_000) return `${(n / 1_000).toFixed(1)}k`;
|
||
|
|
return String(n);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* ------------------------------------------------------------------ */
|
||
|
|
/* localStorage collapse helpers */
|
||
|
|
/* ------------------------------------------------------------------ */
|
||
|
|
|
||
|
|
export function readCollapsed(key: string, defaultValue: boolean): boolean {
|
||
|
|
try {
|
||
|
|
const raw = localStorage.getItem(key);
|
||
|
|
if (raw === null) return defaultValue;
|
||
|
|
return raw === 'true';
|
||
|
|
} catch {
|
||
|
|
return defaultValue;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
export function writeCollapsed(key: string, value: boolean): void {
|
||
|
|
try {
|
||
|
|
localStorage.setItem(key, String(value));
|
||
|
|
} catch {
|
||
|
|
// ignore quota errors
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/* ------------------------------------------------------------------ */
|
||
|
|
/* Tree builders */
|
||
|
|
/* ------------------------------------------------------------------ */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Apps tree — one node per app, routes as children.
|
||
|
|
* Paths: /apps/{appId}, /apps/{appId}/{routeId}
|
||
|
|
*/
|
||
|
|
export function buildAppTreeNodes(
|
||
|
|
apps: SidebarApp[],
|
||
|
|
statusDot: (health: string) => ReactNode,
|
||
|
|
chevron: () => ReactNode,
|
||
|
|
): SidebarTreeNode[] {
|
||
|
|
return apps.map((app) => ({
|
||
|
|
id: app.id,
|
||
|
|
label: app.name,
|
||
|
|
icon: statusDot(app.health),
|
||
|
|
badge: formatCount(app.exchangeCount),
|
||
|
|
path: `/apps/${app.id}`,
|
||
|
|
starrable: true,
|
||
|
|
starKey: `app:${app.id}`,
|
||
|
|
children: app.routes.map((r) => ({
|
||
|
|
id: `${app.id}/${r.id}`,
|
||
|
|
label: r.name,
|
||
|
|
icon: chevron(),
|
||
|
|
badge: formatCount(r.exchangeCount),
|
||
|
|
path: `/apps/${app.id}/${r.id}`,
|
||
|
|
starrable: true,
|
||
|
|
starKey: `route:${app.id}/${r.id}`,
|
||
|
|
})),
|
||
|
|
}));
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Agents tree — one node per app, agents as children.
|
||
|
|
* Paths: /agents/{appId}, /agents/{appId}/{agentId}
|
||
|
|
* Badge shows "N/M live".
|
||
|
|
*/
|
||
|
|
export function buildAgentTreeNodes(
|
||
|
|
apps: SidebarApp[],
|
||
|
|
statusDot: (health: string) => ReactNode,
|
||
|
|
): SidebarTreeNode[] {
|
||
|
|
return apps.map((app) => {
|
||
|
|
const liveCount = app.agents.filter((a) => a.status === 'live').length;
|
||
|
|
return {
|
||
|
|
id: `agent:${app.id}`,
|
||
|
|
label: app.name,
|
||
|
|
icon: statusDot(app.health),
|
||
|
|
badge: `${liveCount}/${app.agents.length} live`,
|
||
|
|
path: `/agents/${app.id}`,
|
||
|
|
children: app.agents.map((a) => ({
|
||
|
|
id: `agent:${app.id}/${a.id}`,
|
||
|
|
label: a.name,
|
||
|
|
icon: statusDot(a.status),
|
||
|
|
badge: a.tps != null ? `${a.tps.toFixed(1)} msg/s` : undefined,
|
||
|
|
path: `/agents/${app.id}/${a.id}`,
|
||
|
|
})),
|
||
|
|
};
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Routes stats tree — one node per app, routes as children.
|
||
|
|
* Paths: /routes/{appId}, /routes/{appId}/{routeId}
|
||
|
|
*/
|
||
|
|
export function buildRouteTreeNodes(
|
||
|
|
apps: SidebarApp[],
|
||
|
|
statusDot: (health: string) => ReactNode,
|
||
|
|
chevron: () => ReactNode,
|
||
|
|
): SidebarTreeNode[] {
|
||
|
|
return apps.map((app) => ({
|
||
|
|
id: `route:${app.id}`,
|
||
|
|
label: app.name,
|
||
|
|
icon: statusDot(app.health),
|
||
|
|
badge: `${app.routes.length} routes`,
|
||
|
|
path: `/routes/${app.id}`,
|
||
|
|
children: app.routes.map((r) => ({
|
||
|
|
id: `route:${app.id}/${r.id}`,
|
||
|
|
label: r.name,
|
||
|
|
icon: chevron(),
|
||
|
|
badge: formatCount(r.exchangeCount),
|
||
|
|
path: `/routes/${app.id}/${r.id}`,
|
||
|
|
})),
|
||
|
|
}));
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Admin tree — static 6 nodes.
|
||
|
|
*/
|
||
|
|
export function buildAdminTreeNodes(): SidebarTreeNode[] {
|
||
|
|
return [
|
||
|
|
{ id: 'admin:rbac', label: 'Users & Roles', path: '/admin/rbac' },
|
||
|
|
{ id: 'admin:audit', label: 'Audit Log', path: '/admin/audit' },
|
||
|
|
{ id: 'admin:oidc', label: 'OIDC', path: '/admin/oidc' },
|
||
|
|
{ id: 'admin:appconfig', label: 'App Config', path: '/admin/appconfig' },
|
||
|
|
{ id: 'admin:database', label: 'Database', path: '/admin/database' },
|
||
|
|
{ id: 'admin:clickhouse', label: 'ClickHouse', path: '/admin/clickhouse' },
|
||
|
|
];
|
||
|
|
}
|