feat(ui): add ExchangeList compact component for 3-column layout

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-03-28 13:55:08 +01:00
parent c1b156bdb4
commit 8219c54422
2 changed files with 130 additions and 0 deletions

View File

@@ -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;
}

View File

@@ -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 <div className={styles.empty}>No exchanges found</div>;
}
return (
<div className={styles.list}>
{exchanges.map((ex) => (
<div
key={ex.executionId}
className={`${styles.item} ${selectedId === ex.executionId ? styles.itemSelected : ''}`}
onClick={() => onSelect(ex)}
>
<span className={`${styles.dot} ${dotClass(ex.status)}`} />
<div className={styles.meta}>
<div className={styles.exchangeId}>{ex.executionId.slice(0, 12)}</div>
</div>
<span className={styles.duration}>{formatDuration(ex.durationMs)}</span>
<span className={styles.timestamp}>{formatTime(ex.startTime)}</span>
</div>
))}
</div>
);
}