Add Cmd+K command palette for searching executions and agents
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:
@@ -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;
|
||||
|
||||
@@ -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}>⌘K</span>
|
||||
</div>
|
||||
<button className={styles.btnPrimary}>Search</button>
|
||||
</div>
|
||||
|
||||
{/* Row 2: Status chips + date + duration */}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user