Add sortable table columns, pre-populate date filter, inline command palette
All checks were successful
CI / build (push) Successful in 1m1s
CI / docker (push) Successful in 46s
CI / deploy (push) Successful in 32s

- Table headers are now clickable to sort by column (client-side)
- From date picker defaults to today 00:00 instead of empty
- Command palette expands inline from search bar instead of modal dialog

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-03-13 18:03:37 +01:00
parent d78b283567
commit c3cfb39f81
6 changed files with 261 additions and 135 deletions

View File

@@ -1,94 +1,11 @@
import { useEffect, useCallback } from 'react';
import { createPortal } from 'react-dom';
import { useCommandPalette, type PaletteScope } from './use-command-palette';
import { usePaletteSearch, type PaletteResult } from './use-palette-search';
import { useExecutionSearch } from '../../pages/executions/use-execution-search';
import { PaletteInput } from './PaletteInput';
import { ScopeTabs } from './ScopeTabs';
import { ResultsList } from './ResultsList';
import { PaletteFooter } from './PaletteFooter';
import type { ExecutionSummary, AgentInstance } from '../../api/schema';
import styles from './CommandPalette.module.css';
const SCOPES: PaletteScope[] = ['all', 'executions', 'agents'];
import { useEffect } from 'react';
import { useCommandPalette } from './use-command-palette';
/**
* Headless component: only registers the global Cmd+K / Ctrl+K keyboard shortcut.
* The palette UI itself is rendered inline within SearchFilters.
*/
export function CommandPalette() {
const { isOpen, close, scope, setScope, selectedIndex, setSelectedIndex, reset, filters } =
useCommandPalette();
const { results, executionCount, agentCount, isLoading } = usePaletteSearch();
const execSearch = useExecutionSearch();
const handleSelect = useCallback(
(result: PaletteResult) => {
if (result.type === 'execution') {
const exec = result.data as ExecutionSummary;
execSearch.setStatus(['COMPLETED', 'FAILED', 'RUNNING']);
execSearch.setText(exec.executionId);
execSearch.setRouteId('');
execSearch.setAgentId('');
execSearch.setProcessorType('');
} else if (result.type === 'agent') {
const agent = result.data as AgentInstance;
execSearch.setStatus(['COMPLETED', 'FAILED', 'RUNNING']);
execSearch.setAgentId(agent.id);
execSearch.setText('');
execSearch.setRouteId('');
execSearch.setProcessorType('');
}
// Apply any active palette filters to the execution search
for (const f of filters) {
if (f.key === 'status') execSearch.setStatus([f.value.toUpperCase()]);
if (f.key === 'route') execSearch.setRouteId(f.value);
if (f.key === 'agent') execSearch.setAgentId(f.value);
if (f.key === 'processor') execSearch.setProcessorType(f.value);
}
close();
reset();
},
[close, reset, execSearch, filters],
);
const handleKeyDown = useCallback(
(e: KeyboardEvent) => {
if (!isOpen) return;
switch (e.key) {
case 'Escape':
e.preventDefault();
close();
reset();
break;
case 'ArrowDown':
e.preventDefault();
setSelectedIndex(
results.length > 0 ? (selectedIndex + 1) % results.length : 0,
);
break;
case 'ArrowUp':
e.preventDefault();
setSelectedIndex(
results.length > 0
? (selectedIndex - 1 + results.length) % results.length
: 0,
);
break;
case 'Enter':
e.preventDefault();
if (results[selectedIndex]) {
handleSelect(results[selectedIndex]);
}
break;
case 'Tab':
e.preventDefault();
const idx = SCOPES.indexOf(scope);
setScope(SCOPES[(idx + 1) % SCOPES.length]);
break;
}
},
[isOpen, close, reset, selectedIndex, setSelectedIndex, results, handleSelect, scope, setScope],
);
// Global Cmd+K / Ctrl+K listener
useEffect(() => {
function onKeyDown(e: KeyboardEvent) {
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
@@ -106,28 +23,5 @@ export function CommandPalette() {
return () => document.removeEventListener('keydown', onKeyDown);
}, []);
// Keyboard handling when open
useEffect(() => {
document.addEventListener('keydown', handleKeyDown);
return () => document.removeEventListener('keydown', handleKeyDown);
}, [handleKeyDown]);
if (!isOpen) return null;
return createPortal(
<div className={styles.overlay} onClick={(e) => {
if (e.target === e.currentTarget) {
close();
reset();
}
}}>
<div className={styles.modal}>
<PaletteInput />
<ScopeTabs executionCount={executionCount} agentCount={agentCount} />
<ResultsList results={results} isLoading={isLoading} onSelect={handleSelect} />
<PaletteFooter />
</div>
</div>,
document.body,
);
return null;
}