diff --git a/ui/src/api/queries/capabilities.ts b/ui/src/api/queries/capabilities.ts new file mode 100644 index 00000000..9466c92b --- /dev/null +++ b/ui/src/api/queries/capabilities.ts @@ -0,0 +1,29 @@ +import { useQuery } from '@tanstack/react-query'; +import { config } from '../../config'; + +interface HealthResponse { + status: string; + components?: { + serverCapabilities?: { + details?: { + infrastructureEndpoints?: boolean; + }; + }; + }; +} + +export function useServerCapabilities() { + return useQuery<{ infrastructureEndpoints: boolean }>({ + queryKey: ['server-capabilities'], + queryFn: async () => { + const res = await fetch(config.apiBaseUrl + '/health'); + if (!res.ok) return { infrastructureEndpoints: true }; + const data: HealthResponse = await res.json(); + return { + infrastructureEndpoints: + data.components?.serverCapabilities?.details?.infrastructureEndpoints ?? true, + }; + }, + staleTime: Infinity, + }); +} diff --git a/ui/src/components/LayoutShell.tsx b/ui/src/components/LayoutShell.tsx index 41b60146..16b7ca46 100644 --- a/ui/src/components/LayoutShell.tsx +++ b/ui/src/components/LayoutShell.tsx @@ -46,6 +46,7 @@ import { readCollapsed, writeCollapsed, } from './sidebar-utils'; +import { useServerCapabilities } from '../api/queries/capabilities'; import type { SidebarApp } from './sidebar-utils'; /* ------------------------------------------------------------------ */ @@ -290,6 +291,9 @@ function LayoutContent() { const globalFilters = useGlobalFilters(); const { timeRange, autoRefresh, refreshTimeRange } = globalFilters; + // --- Server capabilities ------------------------------------------ + const { data: capabilities } = useServerCapabilities(); + // --- Role checks ---------------------------------------------------- const isAdmin = useIsAdmin(); @@ -432,8 +436,8 @@ function LayoutContent() { ); const adminTreeNodes: SidebarTreeNode[] = useMemo( - () => buildAdminTreeNodes(), - [], + () => buildAdminTreeNodes({ infrastructureEndpoints: capabilities?.infrastructureEndpoints }), + [capabilities?.infrastructureEndpoints], ); // --- Starred items ------------------------------------------------ diff --git a/ui/src/components/sidebar-utils.ts b/ui/src/components/sidebar-utils.ts index 41a34dce..2a3bbd79 100644 --- a/ui/src/components/sidebar-utils.ts +++ b/ui/src/components/sidebar-utils.ts @@ -99,13 +99,15 @@ export function buildAppTreeNodes( /** * Admin tree — static nodes, alphabetically sorted. */ -export function buildAdminTreeNodes(): SidebarTreeNode[] { - return [ +export function buildAdminTreeNodes(opts?: { infrastructureEndpoints?: boolean }): SidebarTreeNode[] { + const showInfra = opts?.infrastructureEndpoints !== false; + const nodes: SidebarTreeNode[] = [ { id: 'admin:audit', label: 'Audit Log', path: '/admin/audit' }, - { id: 'admin:clickhouse', label: 'ClickHouse', path: '/admin/clickhouse' }, - { id: 'admin:database', label: 'Database', path: '/admin/database' }, + ...(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; }