Files
cameleer-server/ui/src/components/sidebar-utils.ts

116 lines
4.0 KiB
TypeScript
Raw Normal View History

import { createElement, 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;
routeState?: 'stopped' | 'suspended';
}
export interface SidebarApp {
id: string;
name: string;
health: 'live' | 'stale' | 'dead' | 'running' | 'error';
healthTooltip?: string;
exchangeCount: number;
routes: SidebarRoute[];
}
/* ------------------------------------------------------------------ */
/* 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: /exchanges/{appId}, /exchanges/{appId}/{routeId}
*/
export function buildAppTreeNodes(
apps: SidebarApp[],
statusDot: (health: string) => ReactNode,
chevron: () => ReactNode,
stopIcon?: () => ReactNode,
pauseIcon?: () => ReactNode,
): SidebarTreeNode[] {
return apps.map((app) => ({
id: app.id,
label: app.name,
icon: app.healthTooltip
? createElement('span', { title: app.healthTooltip }, statusDot(app.health))
: statusDot(app.health),
badge: formatCount(app.exchangeCount),
path: `/exchanges/${app.id}`,
starrable: true,
starKey: `app:${app.id}`,
children: app.routes.map((r) => ({
id: `${app.id}/${r.id}`,
label: r.name,
icon: r.routeState === 'stopped' && stopIcon
? stopIcon()
: r.routeState === 'suspended' && pauseIcon
? pauseIcon()
: chevron(),
badge: r.routeState
? `${r.routeState.toUpperCase()} \u00b7 ${formatCount(r.exchangeCount)}`
: formatCount(r.exchangeCount),
path: `/exchanges/${app.id}/${r.id}`,
starrable: true,
starKey: `route:${app.id}/${r.id}`,
})),
}));
}
/**
* Admin tree static nodes, alphabetically sorted.
*/
export function buildAdminTreeNodes(opts?: { infrastructureEndpoints?: boolean }): SidebarTreeNode[] {
const showInfra = opts?.infrastructureEndpoints !== false;
const nodes: SidebarTreeNode[] = [
{ id: 'admin:audit', label: 'Audit Log', path: '/admin/audit' },
...(showInfra ? [{ id: 'admin:clickhouse', label: 'ClickHouse', path: '/admin/clickhouse' }] : []),
...(showInfra ? [{ id: 'admin:database', label: 'Database', path: '/admin/database' }] : []),
{ id: 'admin:environments', label: 'Environments', path: '/admin/environments' },
{ id: 'admin:oidc', label: 'OIDC', path: '/admin/oidc' },
{ id: 'admin:outbound-connections', label: 'Outbound Connections', path: '/admin/outbound-connections' },
{ id: 'admin:sensitive-keys', label: 'Sensitive Keys', path: '/admin/sensitive-keys' },
{ id: 'admin:rbac', label: 'Users & Roles', path: '/admin/rbac' },
];
return nodes;
}