diff --git a/ui/src/components/LayoutShell.tsx b/ui/src/components/LayoutShell.tsx index 407a79ef..5f017d21 100644 --- a/ui/src/components/LayoutShell.tsx +++ b/ui/src/components/LayoutShell.tsx @@ -31,6 +31,8 @@ import { useAgents } from '../api/queries/agents'; import { useSearchExecutions, useAttributeKeys } from '../api/queries/executions'; import { useUsers, useGroups, useRoles } from '../api/queries/admin/rbac'; import { useEnvironments } from '../api/queries/admin/environments'; +import { useAlerts } from '../api/queries/alerts'; +import { useAlertRules } from '../api/queries/alertRules'; import type { UserDetail, GroupDetail, RoleDetail } from '../api/queries/admin/rbac'; import { useAuthStore, useIsAdmin, useCanControl } from '../auth/auth-store'; import { useEnvironmentStore } from '../api/environment-store'; @@ -161,6 +163,58 @@ function buildAdminSearchData( return results; } +function buildAlertSearchData( + alerts: any[] | undefined, + rules: any[] | undefined, +): SearchResult[] { + const results: SearchResult[] = []; + if (alerts) { + for (const a of alerts) { + results.push({ + id: `alert:${a.id}`, + category: 'alert', + title: a.title ?? '(untitled)', + badges: [ + { label: a.severity, color: severityToSearchColor(a.severity) }, + { label: a.state, color: stateToSearchColor(a.state) }, + ], + meta: `${a.firedAt ?? ''}${a.silenced ? ' · silenced' : ''}`, + path: `/alerts/inbox/${a.id}`, + }); + } + } + if (rules) { + for (const r of rules) { + results.push({ + id: `rule:${r.id}`, + category: 'alertRule', + title: r.name, + badges: [ + { label: r.severity, color: severityToSearchColor(r.severity) }, + { label: r.conditionKind, color: 'auto' }, + ...(r.enabled ? [] : [{ label: 'DISABLED', color: 'warning' as const }]), + ], + meta: `${r.evaluationIntervalSeconds}s · ${r.targets?.length ?? 0} targets`, + path: `/alerts/rules/${r.id}`, + }); + } + } + return results; +} + +function severityToSearchColor(s: string): string { + if (s === 'CRITICAL') return 'error'; + if (s === 'WARNING') return 'warning'; + return 'auto'; +} + +function stateToSearchColor(s: string): string { + if (s === 'FIRING') return 'error'; + if (s === 'ACKNOWLEDGED') return 'warning'; + if (s === 'RESOLVED') return 'success'; + return 'auto'; +} + function healthToSearchColor(health: string): string { switch (health) { case 'live': return 'success'; @@ -313,6 +367,10 @@ function LayoutContent() { const { data: attributeKeys } = useAttributeKeys(); const { data: envRecords = [] } = useEnvironments(); + // Open alerts + rules for CMD-K (env-scoped). + const { data: cmdkAlerts } = useAlerts({ state: ['FIRING', 'ACKNOWLEDGED'], limit: 100 }); + const { data: cmdkRules } = useAlertRules(); + // Merge environments from both the environments table and agent heartbeats const environments: string[] = useMemo(() => { const envSet = new Set(); @@ -569,6 +627,11 @@ function LayoutContent() { [adminUsers, adminGroups, adminRoles], ); + const alertingSearchData: SearchResult[] = useMemo( + () => buildAlertSearchData(cmdkAlerts, cmdkRules), + [cmdkAlerts, cmdkRules], + ); + const operationalSearchData: SearchResult[] = useMemo(() => { if (isAdminPage) return []; @@ -604,8 +667,8 @@ function LayoutContent() { } } - return [...catalogRef.current, ...exchangeItems, ...attributeItems]; - }, [isAdminPage, catalogRef.current, exchangeResults, debouncedQuery]); + return [...catalogRef.current, ...exchangeItems, ...attributeItems, ...alertingSearchData]; + }, [isAdminPage, catalogRef.current, exchangeResults, debouncedQuery, alertingSearchData]); const searchData = isAdminPage ? adminSearchData : operationalSearchData; @@ -653,6 +716,11 @@ function LayoutContent() { const ADMIN_TAB_MAP: Record = { user: 'users', group: 'groups', role: 'roles' }; const handlePaletteSelect = useCallback((result: any) => { + if (result.category === 'alert' || result.category === 'alertRule') { + if (result.path) navigate(result.path); + setPaletteOpen(false); + return; + } if (result.path) { if (ADMIN_CATEGORIES.has(result.category)) { const itemId = result.id.split(':').slice(1).join(':');