Server-side sorting for execution search results
All checks were successful
CI / build (push) Successful in 1m12s
CI / docker (push) Successful in 50s
CI / deploy (push) Successful in 33s

Sorting now applies to the entire result set via ClickHouse ORDER BY
instead of only sorting the current page client-side. Default sort
order is timestamp descending. Supported sort columns: startTime,
status, agentId, routeId, correlationId, durationMs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-03-14 19:34:22 +01:00
parent 31b8695420
commit b64edaa16f
7 changed files with 83 additions and 44 deletions

View File

@@ -255,6 +255,22 @@
"format": "int32",
"default": 50
}
},
{
"name": "sortField",
"in": "query",
"required": false,
"schema": {
"type": "string"
}
},
{
"name": "sortDir",
"in": "query",
"required": false,
"schema": {
"type": "string"
}
}
],
"responses": {
@@ -1500,6 +1516,12 @@
"limit": {
"type": "integer",
"format": "int32"
},
"sortField": {
"type": "string"
},
"sortDir": {
"type": "string"
}
}
},

View File

@@ -562,6 +562,8 @@ export interface components {
offset?: number;
/** Format: int32 */
limit?: number;
sortField?: string;
sortDir?: string;
};
ExecutionSummary: {
executionId: string;
@@ -915,6 +917,8 @@ export interface operations {
processorType?: string;
offset?: number;
limit?: number;
sortField?: string;
sortDir?: string;
};
header?: never;
path?: never;

View File

@@ -1,10 +1,11 @@
import { useState, useMemo } from 'react';
import { useState } from 'react';
import type { ExecutionSummary } from '../../api/types';
import { StatusPill } from '../../components/shared/StatusPill';
import { DurationBar } from '../../components/shared/DurationBar';
import { AppBadge } from '../../components/shared/AppBadge';
import { ProcessorTree } from './ProcessorTree';
import { ExchangeDetail } from './ExchangeDetail';
import { useExecutionSearch } from './use-execution-search';
import styles from './ResultsTable.module.css';
interface ResultsTableProps {
@@ -24,31 +25,6 @@ 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;
@@ -76,21 +52,12 @@ function SortableTh({ label, column, activeColumn, direction, onSort, style }: S
export function ResultsTable({ results, loading }: ResultsTableProps) {
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]);
const sortColumn = useExecutionSearch((s) => s.sortField);
const sortDir = useExecutionSearch((s) => s.sortDir);
const setSort = useExecutionSearch((s) => s.setSort);
function handleSort(col: SortColumn) {
if (sortColumn === col) {
setSortDir((d) => (d === 'asc' ? 'desc' : 'asc'));
} else {
setSortColumn(col);
setSortDir('desc');
}
setSort(col);
}
if (loading && results.length === 0) {
@@ -124,7 +91,7 @@ export function ResultsTable({ results, loading }: ResultsTableProps) {
</tr>
</thead>
<tbody>
{sortedResults.map((exec) => {
{results.map((exec) => {
const isExpanded = expandedId === exec.executionId;
return (
<ResultRow

View File

@@ -9,6 +9,9 @@ function todayMidnight(): string {
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T00:00`;
}
type SortColumn = 'startTime' | 'status' | 'agentId' | 'routeId' | 'correlationId' | 'durationMs';
type SortDir = 'asc' | 'desc';
interface ExecutionSearchState {
status: string[];
timeFrom: string;
@@ -22,6 +25,8 @@ interface ExecutionSearchState {
live: boolean;
offset: number;
limit: number;
sortField: SortColumn;
sortDir: SortDir;
toggleLive: () => void;
setStatus: (statuses: string[]) => void;
@@ -35,6 +40,7 @@ interface ExecutionSearchState {
setAgentId: (v: string) => void;
setProcessorType: (v: string) => void;
setOffset: (v: number) => void;
setSort: (col: SortColumn) => void;
clearAll: () => void;
toSearchRequest: () => SearchRequest;
}
@@ -52,6 +58,8 @@ export const useExecutionSearch = create<ExecutionSearchState>((set, get) => ({
live: true,
offset: 0,
limit: 25,
sortField: 'startTime',
sortDir: 'desc',
toggleLive: () => set((state) => ({ live: !state.live })),
setStatus: (statuses) => set({ status: statuses, offset: 0 }),
@@ -71,6 +79,12 @@ export const useExecutionSearch = create<ExecutionSearchState>((set, get) => ({
setAgentId: (v) => set({ agentId: v, offset: 0 }),
setProcessorType: (v) => set({ processorType: v, offset: 0 }),
setOffset: (v) => set({ offset: v }),
setSort: (col) =>
set((state) => ({
sortField: col,
sortDir: state.sortField === col && state.sortDir === 'desc' ? 'asc' : 'desc',
offset: 0,
})),
clearAll: () =>
set({
status: ['COMPLETED', 'FAILED', 'RUNNING'],
@@ -83,6 +97,8 @@ export const useExecutionSearch = create<ExecutionSearchState>((set, get) => ({
agentId: '',
processorType: '',
offset: 0,
sortField: 'startTime',
sortDir: 'desc',
}),
toSearchRequest: (): SearchRequest => {
@@ -102,6 +118,8 @@ export const useExecutionSearch = create<ExecutionSearchState>((set, get) => ({
processorType: s.processorType || undefined,
offset: s.offset,
limit: s.limit,
sortField: s.sortField,
sortDir: s.sortDir,
};
},
}));