Files
cameleer-server/ui/src/pages/executions/ResultsTable.tsx
hsiegeln 50bb22d6f6
All checks were successful
CI / build (push) Successful in 1m8s
CI / docker (push) Successful in 51s
CI / deploy (push) Successful in 29s
Add OIDC logout, fix OpenAPI schema types, expose end_session_endpoint
Backend:
- Expose end_session_endpoint from OIDC provider metadata in /auth/oidc/config
- Add getEndSessionEndpoint() to OidcTokenExchanger

Frontend:
- On OIDC logout, redirect to provider's end_session_endpoint to clear SSO session
- Strip /api/v1 prefix from OpenAPI paths to match client baseUrl convention
- Add schema-types.ts with convenience type re-exports from generated schema
- Fix all type imports to use schema-types instead of raw generated schema
- Fix optional field access (processors, children, duration) with proper typing
- Fix AgentInstance.state → status field name

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 14:43:18 +01:00

188 lines
6.0 KiB
TypeScript

import { useState, useMemo } from 'react';
import type { ExecutionSummary } from '../../api/schema-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 styles from './ResultsTable.module.css';
interface ResultsTableProps {
results: ExecutionSummary[];
loading: boolean;
}
type SortColumn = 'startTime' | 'status' | 'agentId' | 'routeId' | 'correlationId' | 'durationMs';
type SortDir = 'asc' | 'desc';
function formatTime(iso: string) {
return new Date(iso).toLocaleTimeString('en-GB', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
fractionalSecondDigits: 3,
});
}
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) {
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) {
return (
<div className={styles.tableWrap}>
<div className={styles.loadingOverlay}>Loading executions...</div>
</div>
);
}
if (results.length === 0) {
return (
<div className={styles.tableWrap}>
<div className={styles.emptyState}>No executions found matching your filters.</div>
</div>
);
}
return (
<div className={styles.tableWrap}>
<table className={styles.table}>
<thead className={styles.thead}>
<tr>
<th className={styles.th} style={{ width: 32 }} />
<SortableTh label="Timestamp" column="startTime" activeColumn={sortColumn} direction={sortDir} onSort={handleSort} />
<SortableTh label="Status" column="status" activeColumn={sortColumn} direction={sortDir} onSort={handleSort} />
<SortableTh label="Application" column="agentId" activeColumn={sortColumn} direction={sortDir} onSort={handleSort} />
<SortableTh label="Route" column="routeId" activeColumn={sortColumn} direction={sortDir} onSort={handleSort} />
<SortableTh label="Correlation ID" column="correlationId" activeColumn={sortColumn} direction={sortDir} onSort={handleSort} />
<SortableTh label="Duration" column="durationMs" activeColumn={sortColumn} direction={sortDir} onSort={handleSort} />
</tr>
</thead>
<tbody>
{sortedResults.map((exec) => {
const isExpanded = expandedId === exec.executionId;
return (
<ResultRow
key={exec.executionId}
exec={exec}
isExpanded={isExpanded}
onToggle={() => setExpandedId(isExpanded ? null : exec.executionId)}
/>
);
})}
</tbody>
</table>
</div>
);
}
function ResultRow({
exec,
isExpanded,
onToggle,
}: {
exec: ExecutionSummary;
isExpanded: boolean;
onToggle: () => void;
}) {
return (
<>
<tr
className={`${styles.row} ${isExpanded ? styles.expanded : ''}`}
onClick={onToggle}
>
<td className={`${styles.td} ${styles.tdExpand}`}>&rsaquo;</td>
<td className={`${styles.td} mono`}>{formatTime(exec.startTime)}</td>
<td className={styles.td}>
<StatusPill status={exec.status} />
</td>
<td className={styles.td}>
<AppBadge name={exec.agentId} />
</td>
<td className={`${styles.td} mono text-secondary`}>{exec.routeId}</td>
<td className={`${styles.td} mono text-muted ${styles.correlationId}`} title={exec.correlationId ?? ''}>
{exec.correlationId ?? '-'}
</td>
<td className={styles.td}>
<DurationBar duration={exec.durationMs} />
</td>
</tr>
{isExpanded && (
<tr className={styles.detailRowVisible}>
<td className={styles.detailCell} colSpan={7}>
<div className={styles.detailContent}>
<ProcessorTree executionId={exec.executionId} />
<ExchangeDetail execution={exec} />
</div>
</td>
</tr>
)}
</>
);
}