feat: show distinct attribute keys in cmd-k Attributes tab
All checks were successful
CI / build (push) Successful in 1m58s
CI / cleanup-branch (push) Has been skipped
CI / docker (push) Successful in 1m46s
CI / deploy (push) Successful in 47s
CI / deploy-feature (push) Has been skipped

Add GET /search/attributes/keys endpoint that queries distinct
attribute key names from ClickHouse using JSONExtractKeys. Attribute
keys appear in the cmd-k Attributes tab alongside attribute value
matches from exchange results.

- SearchIndex.distinctAttributeKeys() interface method
- ClickHouseSearchIndex implementation using arrayJoin(JSONExtractKeys)
- SearchController /attributes/keys endpoint
- useAttributeKeys() React Query hook
- buildSearchData includes attribute keys as 'attribute' category items

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-04-01 21:39:27 +02:00
parent 9057981cf7
commit f3feaddbfe
6 changed files with 62 additions and 3 deletions

View File

@@ -3,7 +3,7 @@ import { AppShell, Sidebar, TopBar, CommandPalette, CommandPaletteProvider, Glob
import type { SidebarApp, SearchResult } from '@cameleer/design-system';
import { useRouteCatalog } from '../api/queries/catalog';
import { useAgents } from '../api/queries/agents';
import { useSearchExecutions } from '../api/queries/executions';
import { useSearchExecutions, useAttributeKeys } from '../api/queries/executions';
import { useAuthStore } from '../auth/auth-store';
import { useState, useMemo, useCallback, useEffect, useRef } from 'react';
import { ContentTabs } from './ContentTabs';
@@ -21,6 +21,7 @@ function healthToColor(health: string): string {
function buildSearchData(
catalog: any[] | undefined,
agents: any[] | undefined,
attrKeys: string[] | undefined,
): SearchResult[] {
if (!catalog) return [];
const results: SearchResult[] = [];
@@ -61,6 +62,17 @@ function buildSearchData(
}
}
if (attrKeys) {
for (const key of attrKeys) {
results.push({
id: `attr-key-${key}`,
category: 'attribute',
title: key,
meta: 'attribute key',
});
}
}
return results;
}
@@ -94,6 +106,7 @@ function LayoutContent() {
const { timeRange } = useGlobalFilters();
const { data: catalog } = useRouteCatalog(timeRange.start.toISOString(), timeRange.end.toISOString());
const { data: agents } = useAgents();
const { data: attributeKeys } = useAttributeKeys();
const { username, logout } = useAuthStore();
const { open: paletteOpen, setOpen: setPaletteOpen } = useCommandPalette();
const { scope, setTab } = useScope();
@@ -141,8 +154,8 @@ function LayoutContent() {
}, [catalog]);
const catalogData = useMemo(
() => buildSearchData(catalog, agents as any[]),
[catalog, agents],
() => buildSearchData(catalog, agents as any[], attributeKeys),
[catalog, agents, attributeKeys],
);
// Stable reference for catalog data — only changes when catalog/agents actually change,