diff --git a/ui/src/components/LayoutShell.tsx b/ui/src/components/LayoutShell.tsx index 1551ecfc..c166a16f 100644 --- a/ui/src/components/LayoutShell.tsx +++ b/ui/src/components/LayoutShell.tsx @@ -19,6 +19,8 @@ import { Box, Settings, FileText, ChevronRight, Square, Pause, Star, X } from 'l import { useRouteCatalog } from '../api/queries/catalog'; import { useAgents } from '../api/queries/agents'; import { useSearchExecutions, useAttributeKeys } from '../api/queries/executions'; +import { useUsers, useGroups, useRoles } from '../api/queries/admin/rbac'; +import type { UserDetail, GroupDetail, RoleDetail } from '../api/queries/admin/rbac'; import { useAuthStore } from '../auth/auth-store'; import { useState, useMemo, useCallback, useEffect, useRef, createElement } from 'react'; import type { ReactNode } from 'react'; @@ -95,6 +97,52 @@ function buildSearchData( return results; } +function buildAdminSearchData( + users: UserDetail[] | undefined, + groups: GroupDetail[] | undefined, + roles: RoleDetail[] | undefined, +): SearchResult[] { + const results: SearchResult[] = []; + + if (users) { + for (const u of users) { + results.push({ + id: `user:${u.userId}`, + category: 'user', + title: u.displayName || u.userId, + meta: u.userId, + path: '/admin/rbac', + }); + } + } + + if (groups) { + for (const g of groups) { + results.push({ + id: `group:${g.id}`, + category: 'group', + title: g.name, + meta: g.parentGroupId ? `parent: ${g.parentGroupId}` : 'top-level group', + path: '/admin/rbac', + }); + } + } + + if (roles) { + for (const r of roles) { + results.push({ + id: `role:${r.id}`, + category: 'role', + title: r.name, + meta: r.scope, + path: '/admin/rbac', + }); + } + } + + return results; +} + function healthToSearchColor(health: string): string { switch (health) { case 'live': return 'success'; @@ -226,6 +274,12 @@ function LayoutContent() { const { data: catalog } = useRouteCatalog(timeRange.start.toISOString(), timeRange.end.toISOString()); const { data: agents } = useAgents(); const { data: attributeKeys } = useAttributeKeys(); + + // --- Admin search data (only fetched on admin pages) ---------------- + const { data: adminUsers } = useUsers(); + const { data: adminGroups } = useGroups(); + const { data: adminRoles } = useRoles(); + const { username, logout } = useAuthStore(); const { open: paletteOpen, setOpen: setPaletteOpen } = useCommandPalette(); const { scope, setTab } = useScope(); @@ -365,7 +419,14 @@ function LayoutContent() { catalogRef.current = catalogData; } - const searchData: SearchResult[] = useMemo(() => { + const adminSearchData: SearchResult[] = useMemo( + () => buildAdminSearchData(adminUsers, adminGroups, adminRoles), + [adminUsers, adminGroups, adminRoles], + ); + + const operationalSearchData: SearchResult[] = useMemo(() => { + if (isAdminPage) return []; + const exchangeItems: SearchResult[] = (exchangeResults?.data || []).map((e: any) => ({ id: e.executionId, category: 'exchange' as const, @@ -399,7 +460,9 @@ function LayoutContent() { } return [...catalogRef.current, ...exchangeItems, ...attributeItems]; - }, [catalogRef.current, exchangeResults, debouncedQuery]); + }, [isAdminPage, catalogRef.current, exchangeResults, debouncedQuery]); + + const searchData = isAdminPage ? adminSearchData : operationalSearchData; // --- Breadcrumb --------------------------------------------------- const breadcrumb = useMemo(() => {