From 8219c544229686c2dc94003560b5bc3b0036cf3e Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Sat, 28 Mar 2026 13:55:08 +0100 Subject: [PATCH] feat(ui): add ExchangeList compact component for 3-column layout Co-Authored-By: Claude Sonnet 4.6 --- .../pages/Exchanges/ExchangeList.module.css | 74 +++++++++++++++++++ ui/src/pages/Exchanges/ExchangeList.tsx | 56 ++++++++++++++ 2 files changed, 130 insertions(+) create mode 100644 ui/src/pages/Exchanges/ExchangeList.module.css create mode 100644 ui/src/pages/Exchanges/ExchangeList.tsx diff --git a/ui/src/pages/Exchanges/ExchangeList.module.css b/ui/src/pages/Exchanges/ExchangeList.module.css new file mode 100644 index 00000000..7dfec372 --- /dev/null +++ b/ui/src/pages/Exchanges/ExchangeList.module.css @@ -0,0 +1,74 @@ +.list { + display: flex; + flex-direction: column; + overflow-y: auto; + height: 100%; + border-right: 1px solid var(--border); + background: var(--surface); +} + +.item { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.625rem 0.75rem; + cursor: pointer; + border-bottom: 1px solid var(--border-light); + font-size: 0.8125rem; + transition: background 0.1s; +} + +.item:hover { + background: var(--surface-hover); +} + +.itemSelected { + background: var(--surface-active); + border-left: 3px solid var(--amber); + padding-left: calc(0.75rem - 3px); +} + +.dot { + width: 8px; + height: 8px; + border-radius: 50%; + flex-shrink: 0; +} + +.dotOk { background: var(--success); } +.dotErr { background: var(--error); } +.dotRun { background: var(--running); } + +.meta { + flex: 1; + min-width: 0; +} + +.exchangeId { + font-family: var(--font-mono); + font-size: 0.6875rem; + color: var(--text-muted); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.duration { + font-family: var(--font-mono); + font-size: 0.75rem; + color: var(--text-secondary); + flex-shrink: 0; +} + +.timestamp { + font-size: 0.6875rem; + color: var(--text-muted); + flex-shrink: 0; +} + +.empty { + padding: 2rem; + text-align: center; + color: var(--text-muted); + font-size: 0.8125rem; +} diff --git a/ui/src/pages/Exchanges/ExchangeList.tsx b/ui/src/pages/Exchanges/ExchangeList.tsx new file mode 100644 index 00000000..0cc83d6f --- /dev/null +++ b/ui/src/pages/Exchanges/ExchangeList.tsx @@ -0,0 +1,56 @@ +import type { ExecutionSummary } from '../../api/types'; +import styles from './ExchangeList.module.css'; + +interface ExchangeListProps { + exchanges: ExecutionSummary[]; + selectedId?: string; + onSelect: (exchange: ExecutionSummary) => void; +} + +function formatDuration(ms: number): string { + if (ms >= 60_000) return `${(ms / 1000).toFixed(0)}s`; + if (ms >= 1000) return `${(ms / 1000).toFixed(2)}s`; + return `${ms}ms`; +} + +function formatTime(iso: string): string { + const d = new Date(iso); + const h = String(d.getHours()).padStart(2, '0'); + const m = String(d.getMinutes()).padStart(2, '0'); + const s = String(d.getSeconds()).padStart(2, '0'); + return `${h}:${m}:${s}`; +} + +function dotClass(status: string): string { + switch (status) { + case 'COMPLETED': return styles.dotOk; + case 'FAILED': return styles.dotErr; + case 'RUNNING': return styles.dotRun; + default: return styles.dotOk; + } +} + +export function ExchangeList({ exchanges, selectedId, onSelect }: ExchangeListProps) { + if (exchanges.length === 0) { + return
No exchanges found
; + } + + return ( +
+ {exchanges.map((ex) => ( +
onSelect(ex)} + > + +
+
{ex.executionId.slice(0, 12)}
+
+ {formatDuration(ex.durationMs)} + {formatTime(ex.startTime)} +
+ ))} +
+ ); +}