Adds a useServerCapabilities hook that fetches /api/v1/health once per session (staleTime: Infinity) and extracts the infrastructureEndpoints flag. buildAdminTreeNodes now accepts an opts parameter so ClickHouse and Database tabs are hidden when the server reports infra endpoints as disabled. LayoutShell wires the hook result into the admin tree memo. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
114 lines
3.8 KiB
TypeScript
114 lines
3.8 KiB
TypeScript
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: `/apps/${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:rbac', label: 'Users & Roles', path: '/admin/rbac' },
|
|
];
|
|
return nodes;
|
|
}
|