2026-04-09 08:00:54 +02:00
|
|
|
import { createElement, type ReactNode } from 'react';
|
2026-04-02 18:29:22 +02:00
|
|
|
import type { SidebarTreeNode } from '@cameleer/design-system';
|
2026-04-20 13:46:49 +02:00
|
|
|
import { AlertTriangle, Inbox, List, ScrollText, BellOff } from 'lucide-react';
|
2026-04-02 18:29:22 +02:00
|
|
|
|
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
|
/* Domain types (moved out of DS — no longer exported there) */
|
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
|
|
|
|
|
|
export interface SidebarRoute {
|
|
|
|
|
id: string;
|
|
|
|
|
name: string;
|
|
|
|
|
exchangeCount: number;
|
2026-04-02 19:15:46 +02:00
|
|
|
routeState?: 'stopped' | 'suspended';
|
2026-04-02 18:29:22 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface SidebarApp {
|
|
|
|
|
id: string;
|
|
|
|
|
name: string;
|
2026-04-09 08:00:54 +02:00
|
|
|
health: 'live' | 'stale' | 'dead' | 'running' | 'error';
|
|
|
|
|
healthTooltip?: string;
|
2026-04-02 18:29:22 +02:00
|
|
|
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.
|
2026-04-06 21:57:29 +02:00
|
|
|
* Paths: /exchanges/{appId}, /exchanges/{appId}/{routeId}
|
2026-04-02 18:29:22 +02:00
|
|
|
*/
|
|
|
|
|
export function buildAppTreeNodes(
|
|
|
|
|
apps: SidebarApp[],
|
|
|
|
|
statusDot: (health: string) => ReactNode,
|
|
|
|
|
chevron: () => ReactNode,
|
2026-04-02 19:15:46 +02:00
|
|
|
stopIcon?: () => ReactNode,
|
|
|
|
|
pauseIcon?: () => ReactNode,
|
2026-04-02 18:29:22 +02:00
|
|
|
): SidebarTreeNode[] {
|
|
|
|
|
return apps.map((app) => ({
|
|
|
|
|
id: app.id,
|
|
|
|
|
label: app.name,
|
2026-04-09 08:00:54 +02:00
|
|
|
icon: app.healthTooltip
|
|
|
|
|
? createElement('span', { title: app.healthTooltip }, statusDot(app.health))
|
|
|
|
|
: statusDot(app.health),
|
2026-04-02 18:29:22 +02:00
|
|
|
badge: formatCount(app.exchangeCount),
|
2026-04-06 21:57:29 +02:00
|
|
|
path: `/exchanges/${app.id}`,
|
2026-04-02 18:29:22 +02:00
|
|
|
starrable: true,
|
|
|
|
|
starKey: `app:${app.id}`,
|
|
|
|
|
children: app.routes.map((r) => ({
|
|
|
|
|
id: `${app.id}/${r.id}`,
|
|
|
|
|
label: r.name,
|
2026-04-02 19:15:46 +02:00
|
|
|
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),
|
2026-04-16 12:42:01 +02:00
|
|
|
path: `/exchanges/${app.id}/${r.id}`,
|
2026-04-02 18:29:22 +02:00
|
|
|
starrable: true,
|
|
|
|
|
starKey: `route:${app.id}/${r.id}`,
|
|
|
|
|
})),
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2026-04-08 16:23:30 +02:00
|
|
|
* Admin tree — static nodes, alphabetically sorted.
|
2026-04-02 18:29:22 +02:00
|
|
|
*/
|
2026-04-11 23:12:30 +02:00
|
|
|
export function buildAdminTreeNodes(opts?: { infrastructureEndpoints?: boolean }): SidebarTreeNode[] {
|
|
|
|
|
const showInfra = opts?.infrastructureEndpoints !== false;
|
|
|
|
|
const nodes: SidebarTreeNode[] = [
|
2026-04-02 18:29:22 +02:00
|
|
|
{ id: 'admin:audit', label: 'Audit Log', path: '/admin/audit' },
|
2026-04-11 23:12:30 +02:00
|
|
|
...(showInfra ? [{ id: 'admin:clickhouse', label: 'ClickHouse', path: '/admin/clickhouse' }] : []),
|
|
|
|
|
...(showInfra ? [{ id: 'admin:database', label: 'Database', path: '/admin/database' }] : []),
|
2026-04-08 16:23:30 +02:00
|
|
|
{ id: 'admin:environments', label: 'Environments', path: '/admin/environments' },
|
|
|
|
|
{ id: 'admin:oidc', label: 'OIDC', path: '/admin/oidc' },
|
2026-04-19 16:55:35 +02:00
|
|
|
{ id: 'admin:outbound-connections', label: 'Outbound Connections', path: '/admin/outbound-connections' },
|
2026-04-14 18:24:13 +02:00
|
|
|
{ id: 'admin:sensitive-keys', label: 'Sensitive Keys', path: '/admin/sensitive-keys' },
|
2026-04-08 16:23:30 +02:00
|
|
|
{ id: 'admin:rbac', label: 'Users & Roles', path: '/admin/rbac' },
|
2026-04-02 18:29:22 +02:00
|
|
|
];
|
2026-04-11 23:12:30 +02:00
|
|
|
return nodes;
|
2026-04-02 18:29:22 +02:00
|
|
|
}
|
2026-04-20 13:46:49 +02:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Alerts tree — static nodes for the alerting section.
|
|
|
|
|
* Paths: /alerts/{inbox|all|rules|silences|history}
|
|
|
|
|
*/
|
|
|
|
|
export function buildAlertsTreeNodes(): SidebarTreeNode[] {
|
|
|
|
|
const icon = (el: ReactNode) => el;
|
|
|
|
|
return [
|
|
|
|
|
{ id: 'alerts-inbox', label: 'Inbox', path: '/alerts/inbox', icon: icon(createElement(Inbox, { size: 14 })) },
|
|
|
|
|
{ id: 'alerts-all', label: 'All', path: '/alerts/all', icon: icon(createElement(List, { size: 14 })) },
|
|
|
|
|
{ id: 'alerts-rules', label: 'Rules', path: '/alerts/rules', icon: icon(createElement(AlertTriangle, { size: 14 })) },
|
|
|
|
|
{ id: 'alerts-silences', label: 'Silences', path: '/alerts/silences', icon: icon(createElement(BellOff, { size: 14 })) },
|
|
|
|
|
{ id: 'alerts-history', label: 'History', path: '/alerts/history', icon: icon(createElement(ScrollText, { size: 14 })) },
|
|
|
|
|
];
|
|
|
|
|
}
|