Add sortable table columns, pre-populate date filter, inline command palette
- 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:
@@ -1,94 +1,11 @@
|
|||||||
import { useEffect, useCallback } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { createPortal } from 'react-dom';
|
import { useCommandPalette } from './use-command-palette';
|
||||||
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'];
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Headless component: only registers the global Cmd+K / Ctrl+K keyboard shortcut.
|
||||||
|
* The palette UI itself is rendered inline within SearchFilters.
|
||||||
|
*/
|
||||||
export function CommandPalette() {
|
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(() => {
|
useEffect(() => {
|
||||||
function onKeyDown(e: KeyboardEvent) {
|
function onKeyDown(e: KeyboardEvent) {
|
||||||
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
|
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
|
||||||
@@ -106,28 +23,5 @@ export function CommandPalette() {
|
|||||||
return () => document.removeEventListener('keydown', onKeyDown);
|
return () => document.removeEventListener('keydown', onKeyDown);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Keyboard handling when open
|
return null;
|
||||||
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,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,35 @@
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.thSortable {
|
||||||
|
cursor: pointer;
|
||||||
|
transition: color 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thSortable:hover {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.thActive {
|
||||||
|
color: var(--amber);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sortArrow {
|
||||||
|
display: inline-block;
|
||||||
|
margin-left: 4px;
|
||||||
|
font-size: 9px;
|
||||||
|
opacity: 0.3;
|
||||||
|
transition: opacity 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thSortable:hover .sortArrow {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thActive .sortArrow {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
.row {
|
.row {
|
||||||
border-bottom: 1px solid var(--border-subtle);
|
border-bottom: 1px solid var(--border-subtle);
|
||||||
transition: background 0.1s;
|
transition: background 0.1s;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState } from 'react';
|
import { useState, useMemo } from 'react';
|
||||||
import type { ExecutionSummary } from '../../api/schema';
|
import type { ExecutionSummary } from '../../api/schema';
|
||||||
import { StatusPill } from '../../components/shared/StatusPill';
|
import { StatusPill } from '../../components/shared/StatusPill';
|
||||||
import { DurationBar } from '../../components/shared/DurationBar';
|
import { DurationBar } from '../../components/shared/DurationBar';
|
||||||
@@ -12,6 +12,9 @@ interface ResultsTableProps {
|
|||||||
loading: boolean;
|
loading: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SortColumn = 'startTime' | 'status' | 'agentId' | 'routeId' | 'correlationId' | 'durationMs';
|
||||||
|
type SortDir = 'asc' | 'desc';
|
||||||
|
|
||||||
function formatTime(iso: string) {
|
function formatTime(iso: string) {
|
||||||
return new Date(iso).toLocaleTimeString('en-GB', {
|
return new Date(iso).toLocaleTimeString('en-GB', {
|
||||||
hour: '2-digit',
|
hour: '2-digit',
|
||||||
@@ -21,8 +24,74 @@ function formatTime(iso: string) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function compareFn(a: ExecutionSummary, b: ExecutionSummary, col: SortColumn, dir: SortDir): number {
|
||||||
|
let cmp = 0;
|
||||||
|
switch (col) {
|
||||||
|
case 'startTime':
|
||||||
|
cmp = a.startTime.localeCompare(b.startTime);
|
||||||
|
break;
|
||||||
|
case 'status':
|
||||||
|
cmp = a.status.localeCompare(b.status);
|
||||||
|
break;
|
||||||
|
case 'agentId':
|
||||||
|
cmp = a.agentId.localeCompare(b.agentId);
|
||||||
|
break;
|
||||||
|
case 'routeId':
|
||||||
|
cmp = a.routeId.localeCompare(b.routeId);
|
||||||
|
break;
|
||||||
|
case 'correlationId':
|
||||||
|
cmp = (a.correlationId ?? '').localeCompare(b.correlationId ?? '');
|
||||||
|
break;
|
||||||
|
case 'durationMs':
|
||||||
|
cmp = a.durationMs - b.durationMs;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return dir === 'asc' ? cmp : -cmp;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SortableThProps {
|
||||||
|
label: string;
|
||||||
|
column: SortColumn;
|
||||||
|
activeColumn: SortColumn | null;
|
||||||
|
direction: SortDir;
|
||||||
|
onSort: (col: SortColumn) => void;
|
||||||
|
style?: React.CSSProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
function SortableTh({ label, column, activeColumn, direction, onSort, style }: SortableThProps) {
|
||||||
|
const isActive = activeColumn === column;
|
||||||
|
return (
|
||||||
|
<th
|
||||||
|
className={`${styles.th} ${styles.thSortable} ${isActive ? styles.thActive : ''}`}
|
||||||
|
style={style}
|
||||||
|
onClick={() => onSort(column)}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
<span className={styles.sortArrow}>
|
||||||
|
{isActive ? (direction === 'asc' ? '\u25B2' : '\u25BC') : '\u25B4'}
|
||||||
|
</span>
|
||||||
|
</th>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function ResultsTable({ results, loading }: ResultsTableProps) {
|
export function ResultsTable({ results, loading }: ResultsTableProps) {
|
||||||
const [expandedId, setExpandedId] = useState<string | null>(null);
|
const [expandedId, setExpandedId] = useState<string | null>(null);
|
||||||
|
const [sortColumn, setSortColumn] = useState<SortColumn | null>(null);
|
||||||
|
const [sortDir, setSortDir] = useState<SortDir>('desc');
|
||||||
|
|
||||||
|
const sortedResults = useMemo(() => {
|
||||||
|
if (!sortColumn) return results;
|
||||||
|
return [...results].sort((a, b) => compareFn(a, b, sortColumn, sortDir));
|
||||||
|
}, [results, sortColumn, sortDir]);
|
||||||
|
|
||||||
|
function handleSort(col: SortColumn) {
|
||||||
|
if (sortColumn === col) {
|
||||||
|
setSortDir((d) => (d === 'asc' ? 'desc' : 'asc'));
|
||||||
|
} else {
|
||||||
|
setSortColumn(col);
|
||||||
|
setSortDir('desc');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (loading && results.length === 0) {
|
if (loading && results.length === 0) {
|
||||||
return (
|
return (
|
||||||
@@ -46,16 +115,16 @@ export function ResultsTable({ results, loading }: ResultsTableProps) {
|
|||||||
<thead className={styles.thead}>
|
<thead className={styles.thead}>
|
||||||
<tr>
|
<tr>
|
||||||
<th className={styles.th} style={{ width: 32 }} />
|
<th className={styles.th} style={{ width: 32 }} />
|
||||||
<th className={styles.th}>Timestamp</th>
|
<SortableTh label="Timestamp" column="startTime" activeColumn={sortColumn} direction={sortDir} onSort={handleSort} />
|
||||||
<th className={styles.th}>Status</th>
|
<SortableTh label="Status" column="status" activeColumn={sortColumn} direction={sortDir} onSort={handleSort} />
|
||||||
<th className={styles.th}>Application</th>
|
<SortableTh label="Application" column="agentId" activeColumn={sortColumn} direction={sortDir} onSort={handleSort} />
|
||||||
<th className={styles.th}>Route</th>
|
<SortableTh label="Route" column="routeId" activeColumn={sortColumn} direction={sortDir} onSort={handleSort} />
|
||||||
<th className={styles.th}>Correlation ID</th>
|
<SortableTh label="Correlation ID" column="correlationId" activeColumn={sortColumn} direction={sortDir} onSort={handleSort} />
|
||||||
<th className={styles.th}>Duration</th>
|
<SortableTh label="Duration" column="durationMs" activeColumn={sortColumn} direction={sortDir} onSort={handleSort} />
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{results.map((exec) => {
|
{sortedResults.map((exec) => {
|
||||||
const isExpanded = expandedId === exec.executionId;
|
const isExpanded = expandedId === exec.executionId;
|
||||||
return (
|
return (
|
||||||
<ResultRow
|
<ResultRow
|
||||||
|
|||||||
@@ -182,7 +182,32 @@
|
|||||||
|
|
||||||
.clearAll:hover { color: var(--rose); }
|
.clearAll:hover { color: var(--rose); }
|
||||||
|
|
||||||
|
/* ── Inline Palette ── */
|
||||||
|
.searchAnchor {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 300px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.paletteInline {
|
||||||
|
background: var(--bg-base);
|
||||||
|
border: 1px solid var(--amber-dim);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.25), 0 0 0 3px var(--amber-glow);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
max-height: 480px;
|
||||||
|
overflow: hidden;
|
||||||
|
animation: paletteExpand 0.15s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes paletteExpand {
|
||||||
|
from { opacity: 0; max-height: 44px; }
|
||||||
|
to { opacity: 1; max-height: 480px; }
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.filterRow { flex-direction: column; align-items: stretch; }
|
.filterRow { flex-direction: column; align-items: stretch; }
|
||||||
.searchInputWrap { min-width: unset; }
|
.searchInputWrap { min-width: unset; }
|
||||||
|
.searchAnchor { min-width: unset; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
|
import { useRef, useEffect, useCallback } from 'react';
|
||||||
import { useExecutionSearch } from './use-execution-search';
|
import { useExecutionSearch } from './use-execution-search';
|
||||||
import { useCommandPalette } from '../../components/command-palette/use-command-palette';
|
import { useCommandPalette } from '../../components/command-palette/use-command-palette';
|
||||||
|
import { usePaletteSearch, type PaletteResult } from '../../components/command-palette/use-palette-search';
|
||||||
|
import { PaletteInput } from '../../components/command-palette/PaletteInput';
|
||||||
|
import { ScopeTabs } from '../../components/command-palette/ScopeTabs';
|
||||||
|
import { ResultsList } from '../../components/command-palette/ResultsList';
|
||||||
|
import { PaletteFooter } from '../../components/command-palette/PaletteFooter';
|
||||||
import { FilterChip } from '../../components/shared/FilterChip';
|
import { FilterChip } from '../../components/shared/FilterChip';
|
||||||
|
import type { ExecutionSummary, AgentInstance } from '../../api/schema';
|
||||||
import styles from './SearchFilters.module.css';
|
import styles from './SearchFilters.module.css';
|
||||||
|
|
||||||
export function SearchFilters() {
|
export function SearchFilters() {
|
||||||
@@ -16,7 +23,90 @@ export function SearchFilters() {
|
|||||||
clearAll,
|
clearAll,
|
||||||
} = useExecutionSearch();
|
} = useExecutionSearch();
|
||||||
|
|
||||||
|
const execSearch = useExecutionSearch();
|
||||||
|
const { isOpen, close, scope, setScope, selectedIndex, setSelectedIndex, reset, filters } =
|
||||||
|
useCommandPalette();
|
||||||
const openPalette = useCommandPalette((s) => s.open);
|
const openPalette = useCommandPalette((s) => s.open);
|
||||||
|
const { results, executionCount, agentCount, isLoading } = usePaletteSearch();
|
||||||
|
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
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('');
|
||||||
|
}
|
||||||
|
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],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Close on click outside
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isOpen) return;
|
||||||
|
function onClickOutside(e: MouseEvent) {
|
||||||
|
if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) {
|
||||||
|
close();
|
||||||
|
reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
document.addEventListener('mousedown', onClickOutside);
|
||||||
|
return () => document.removeEventListener('mousedown', onClickOutside);
|
||||||
|
}, [isOpen, close, reset]);
|
||||||
|
|
||||||
|
// Keyboard handling when open
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isOpen) return;
|
||||||
|
const SCOPES = ['all', 'executions', 'agents'] as const;
|
||||||
|
function handleKeyDown(e: KeyboardEvent) {
|
||||||
|
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 as typeof SCOPES[number]);
|
||||||
|
setScope(SCOPES[(idx + 1) % SCOPES.length]);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
document.addEventListener('keydown', handleKeyDown);
|
||||||
|
return () => document.removeEventListener('keydown', handleKeyDown);
|
||||||
|
}, [isOpen, close, reset, selectedIndex, setSelectedIndex, results, handleSelect, scope, setScope]);
|
||||||
|
|
||||||
const activeTags: { label: string; onRemove: () => void }[] = [];
|
const activeTags: { label: string; onRemove: () => void }[] = [];
|
||||||
if (text) activeTags.push({ label: `text:"${text}"`, onRemove: () => setText('') });
|
if (text) activeTags.push({ label: `text:"${text}"`, onRemove: () => setText('') });
|
||||||
@@ -31,19 +121,30 @@ export function SearchFilters() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`${styles.filterBar} animate-in delay-3`}>
|
<div className={`${styles.filterBar} animate-in delay-3`}>
|
||||||
{/* Row 1: Search trigger (opens command palette) */}
|
{/* Row 1: Search bar with inline palette */}
|
||||||
<div className={styles.filterRow}>
|
<div className={styles.filterRow}>
|
||||||
<div className={styles.searchInputWrap} onClick={openPalette} role="button" tabIndex={0} onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') openPalette(); }}>
|
<div className={styles.searchAnchor} ref={dropdownRef}>
|
||||||
<svg className={styles.searchIcon} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
{isOpen ? (
|
||||||
<circle cx="11" cy="11" r="8" />
|
<div className={styles.paletteInline}>
|
||||||
<path d="M21 21l-4.35-4.35" />
|
<PaletteInput />
|
||||||
</svg>
|
<ScopeTabs executionCount={executionCount} agentCount={agentCount} />
|
||||||
<span className={styles.searchPlaceholder}>
|
<ResultsList results={results} isLoading={isLoading} onSelect={handleSelect} />
|
||||||
{text || routeId || agentId || processorType
|
<PaletteFooter />
|
||||||
? [text, routeId && `route:${routeId}`, agentId && `agent:${agentId}`, processorType && `processor:${processorType}`].filter(Boolean).join(' ')
|
</div>
|
||||||
: 'Search by correlation ID, error message, route ID...'}
|
) : (
|
||||||
</span>
|
<div className={styles.searchInputWrap} onClick={openPalette} role="button" tabIndex={0} onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') openPalette(); }}>
|
||||||
<span className={styles.searchHint}>⌘K</span>
|
<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>
|
||||||
|
<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>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,14 @@
|
|||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
import type { SearchRequest } from '../../api/schema';
|
import type { SearchRequest } from '../../api/schema';
|
||||||
|
|
||||||
|
function todayMidnight(): string {
|
||||||
|
const d = new Date();
|
||||||
|
d.setHours(0, 0, 0, 0);
|
||||||
|
// Format as datetime-local value: YYYY-MM-DDTHH:mm
|
||||||
|
const pad = (n: number) => n.toString().padStart(2, '0');
|
||||||
|
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T00:00`;
|
||||||
|
}
|
||||||
|
|
||||||
interface ExecutionSearchState {
|
interface ExecutionSearchState {
|
||||||
status: string[];
|
status: string[];
|
||||||
timeFrom: string;
|
timeFrom: string;
|
||||||
@@ -31,7 +39,7 @@ interface ExecutionSearchState {
|
|||||||
|
|
||||||
export const useExecutionSearch = create<ExecutionSearchState>((set, get) => ({
|
export const useExecutionSearch = create<ExecutionSearchState>((set, get) => ({
|
||||||
status: ['COMPLETED', 'FAILED'],
|
status: ['COMPLETED', 'FAILED'],
|
||||||
timeFrom: '',
|
timeFrom: todayMidnight(),
|
||||||
timeTo: '',
|
timeTo: '',
|
||||||
durationMin: null,
|
durationMin: null,
|
||||||
durationMax: null,
|
durationMax: null,
|
||||||
@@ -62,7 +70,7 @@ export const useExecutionSearch = create<ExecutionSearchState>((set, get) => ({
|
|||||||
clearAll: () =>
|
clearAll: () =>
|
||||||
set({
|
set({
|
||||||
status: ['COMPLETED', 'FAILED', 'RUNNING'],
|
status: ['COMPLETED', 'FAILED', 'RUNNING'],
|
||||||
timeFrom: '',
|
timeFrom: todayMidnight(),
|
||||||
timeTo: '',
|
timeTo: '',
|
||||||
durationMin: null,
|
durationMin: null,
|
||||||
durationMax: null,
|
durationMax: null,
|
||||||
|
|||||||
Reference in New Issue
Block a user