feat(ui/alerts): CMD-K sources for alerts + alert rules

Extends operationalSearchData with open alerts (FIRING|ACKNOWLEDGED) and
all rules. Badges convey severity + state. Selecting an alert navigates to
/alerts/inbox/{id}; a rule navigates to /alerts/rules/{id}. Uses the
existing CommandPalette extension point — no new registry.
This commit is contained in:
hsiegeln
2026-04-20 14:09:39 +02:00
parent 8689643e11
commit f4c2cb120b

View File

@@ -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<string>();
@@ -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<string, string> = { 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(':');