Add Cmd+K command palette for searching executions and agents
All checks were successful
CI / build (push) Successful in 59s
CI / docker (push) Successful in 56s
CI / deploy (push) Successful in 26s

Backend: add routeId, agentId, processorType filter fields to SearchRequest
and ClickHouseSearchEngine. Expand global text search to match route_id and
agent_id columns.

Frontend: new command palette component (portal overlay, Zustand store,
TanStack Query search hook with 300ms debounce, filter chip parsing,
keyboard navigation, scope tabs). Search bar in SearchFilters and TopNav
now open the palette. Selecting a result writes filters to the execution
search store to drive the results table.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-03-13 16:28:16 +01:00
parent 6f415cb017
commit 64b03a4e2f
20 changed files with 1348 additions and 67 deletions

View File

@@ -20,6 +20,18 @@
flex: 1;
min-width: 300px;
position: relative;
display: flex;
align-items: center;
background: var(--bg-base);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
cursor: pointer;
transition: border-color 0.2s, box-shadow 0.2s;
}
.searchInputWrap:hover {
border-color: var(--amber-dim);
box-shadow: 0 0 0 3px var(--amber-glow);
}
.searchIcon {
@@ -32,27 +44,18 @@
color: var(--text-muted);
}
.searchInput {
width: 100%;
background: var(--bg-base);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
.searchPlaceholder {
flex: 1;
padding: 10px 14px 10px 40px;
color: var(--text-primary);
font-family: var(--font-mono);
font-size: 13px;
outline: none;
transition: border-color 0.2s, box-shadow 0.2s;
}
.searchInput::placeholder {
color: var(--text-muted);
font-family: var(--font-body);
font-size: 13px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.searchInput:focus {
border-color: var(--amber-dim);
box-shadow: 0 0 0 3px var(--amber-glow);
.searchInputWrap:hover .searchPlaceholder {
color: var(--text-secondary);
}
.searchHint {
@@ -136,35 +139,6 @@
min-width: 50px;
}
.btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 8px 16px;
border-radius: var(--radius-sm);
border: 1px solid var(--border);
background: var(--bg-raised);
color: var(--text-secondary);
font-family: var(--font-body);
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: all 0.15s;
white-space: nowrap;
}
.btn:hover { background: var(--bg-hover); color: var(--text-primary); border-color: var(--text-muted); }
.btnPrimary {
composes: btn;
background: var(--amber);
color: #0a0e17;
border-color: var(--amber);
font-weight: 600;
}
.btnPrimary:hover { background: var(--amber-hover); border-color: var(--amber-hover); color: #0a0e17; }
.filterTags {
display: flex;
gap: 6px;

View File

@@ -1,5 +1,5 @@
import { useRef, useCallback } from 'react';
import { useExecutionSearch } from './use-execution-search';
import { useCommandPalette } from '../../components/command-palette/use-command-palette';
import { FilterChip } from '../../components/shared/FilterChip';
import styles from './SearchFilters.module.css';
@@ -10,21 +10,19 @@ export function SearchFilters() {
timeTo, setTimeTo,
durationMax, setDurationMax,
text, setText,
routeId, setRouteId,
agentId, setAgentId,
processorType, setProcessorType,
clearAll,
} = useExecutionSearch();
const debounceRef = useRef<ReturnType<typeof setTimeout>>(undefined);
const handleTextChange = useCallback(
(value: string) => {
clearTimeout(debounceRef.current);
debounceRef.current = setTimeout(() => setText(value), 300);
},
[setText],
);
const openPalette = useCommandPalette((s) => s.open);
const activeTags: { label: string; onRemove: () => void }[] = [];
if (text) activeTags.push({ label: `text:"${text}"`, onRemove: () => setText('') });
if (routeId) activeTags.push({ label: `route:${routeId}`, onRemove: () => setRouteId('') });
if (agentId) activeTags.push({ label: `agent:${agentId}`, onRemove: () => setAgentId('') });
if (processorType) activeTags.push({ label: `processor:${processorType}`, onRemove: () => setProcessorType('') });
if (timeFrom) activeTags.push({ label: `from:${timeFrom}`, onRemove: () => setTimeFrom('') });
if (timeTo) activeTags.push({ label: `to:${timeTo}`, onRemove: () => setTimeTo('') });
if (durationMax && durationMax < 5000) {
@@ -33,23 +31,20 @@ export function SearchFilters() {
return (
<div className={`${styles.filterBar} animate-in delay-3`}>
{/* Row 1: Search */}
{/* Row 1: Search trigger (opens command palette) */}
<div className={styles.filterRow}>
<div className={styles.searchInputWrap}>
<div className={styles.searchInputWrap} onClick={openPalette} role="button" tabIndex={0} onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') openPalette(); }}>
<svg className={styles.searchIcon} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<circle cx="11" cy="11" r="8" />
<path d="M21 21l-4.35-4.35" />
</svg>
<input
className={styles.searchInput}
type="text"
placeholder="Search by correlation ID, error message, route ID..."
defaultValue={text}
onChange={(e) => handleTextChange(e.target.value)}
/>
<span className={styles.searchPlaceholder}>
{text || routeId || agentId || processorType
? [text, routeId && `route:${routeId}`, agentId && `agent:${agentId}`, processorType && `processor:${processorType}`].filter(Boolean).join(' ')
: 'Search by correlation ID, error message, route ID...'}
</span>
<span className={styles.searchHint}>&#8984;K</span>
</div>
<button className={styles.btnPrimary}>Search</button>
</div>
{/* Row 2: Status chips + date + duration */}

View File

@@ -8,6 +8,9 @@ interface ExecutionSearchState {
durationMin: number | null;
durationMax: number | null;
text: string;
routeId: string;
agentId: string;
processorType: string;
offset: number;
limit: number;
@@ -18,6 +21,9 @@ interface ExecutionSearchState {
setDurationMin: (v: number | null) => void;
setDurationMax: (v: number | null) => void;
setText: (v: string) => void;
setRouteId: (v: string) => void;
setAgentId: (v: string) => void;
setProcessorType: (v: string) => void;
setOffset: (v: number) => void;
clearAll: () => void;
toSearchRequest: () => SearchRequest;
@@ -30,6 +36,9 @@ export const useExecutionSearch = create<ExecutionSearchState>((set, get) => ({
durationMin: null,
durationMax: null,
text: '',
routeId: '',
agentId: '',
processorType: '',
offset: 0,
limit: 25,
@@ -46,6 +55,9 @@ export const useExecutionSearch = create<ExecutionSearchState>((set, get) => ({
setDurationMin: (v) => set({ durationMin: v, offset: 0 }),
setDurationMax: (v) => set({ durationMax: v, offset: 0 }),
setText: (v) => set({ text: v, offset: 0 }),
setRouteId: (v) => set({ routeId: v, offset: 0 }),
setAgentId: (v) => set({ agentId: v, offset: 0 }),
setProcessorType: (v) => set({ processorType: v, offset: 0 }),
setOffset: (v) => set({ offset: v }),
clearAll: () =>
set({
@@ -55,6 +67,9 @@ export const useExecutionSearch = create<ExecutionSearchState>((set, get) => ({
durationMin: null,
durationMax: null,
text: '',
routeId: '',
agentId: '',
processorType: '',
offset: 0,
}),
@@ -70,6 +85,9 @@ export const useExecutionSearch = create<ExecutionSearchState>((set, get) => ({
durationMin: s.durationMin,
durationMax: s.durationMax,
text: s.text || undefined,
routeId: s.routeId || undefined,
agentId: s.agentId || undefined,
processorType: s.processorType || undefined,
offset: s.offset,
limit: s.limit,
};