Files
sanderling/replay-ui/src/routes/RunList.tsx
T
pj b44077afde replay ui fix (#56)
* refactor: rename inspect to replay across the codebase

Renames inspect-ui/ to replay-ui/, internal/inspect/ to internal/replay/,
the CLI subcommand from `sanderling inspect` to `sanderling replay`, and
updates all references in docs, Makefile, README, and Go comments.

* feat(replay-ui): show spec filename with full path on hover

RunList and RunDetail now render the basename of spec_path (e.g.
login.spec.ts) with the full path available as a title tooltip.
2026-06-03 16:17:26 +05:30

103 lines
2.8 KiB
TypeScript

import { useCallback, useEffect, useState } from "react";
import { Link } from "react-router-dom";
import { listRuns } from "../api";
import type { RunSummary } from "../types";
import { useSse } from "../hooks/useSse";
function formatDuration(milliseconds?: number): string {
if (!milliseconds) {
return "-";
}
if (milliseconds < 1000) {
return `${milliseconds}ms`;
}
const seconds = milliseconds / 1000;
if (seconds < 60) {
return `${seconds.toFixed(1)}s`;
}
const minutes = Math.floor(seconds / 60);
const remainingSeconds = Math.round(seconds - minutes * 60);
return `${minutes}m${remainingSeconds}s`;
}
function formatStartedAt(value: string): string {
const date = new Date(value);
if (Number.isNaN(date.getTime())) {
return value;
}
return date.toLocaleString();
}
function basename(specPath: string): string {
const index = specPath.lastIndexOf("/");
return index >= 0 ? specPath.slice(index + 1) : specPath;
}
export default function RunList() {
const [runs, setRuns] = useState<RunSummary[] | null>(null);
const [error, setError] = useState<string | null>(null);
const refresh = useCallback(() => {
listRuns()
.then(setRuns)
.catch((failure: unknown) => {
setError(failure instanceof Error ? failure.message : String(failure));
});
}, []);
useEffect(() => {
refresh();
}, [refresh]);
useSse("runs.changed", refresh);
if (error) {
return <div className="status-block status-error">failed to load runs: {error}</div>;
}
if (!runs) {
return <div className="status-block">loading runs...</div>;
}
if (runs.length === 0) {
return <div className="status-block">no runs yet. run `sanderling test` to produce some.</div>;
}
return (
<table className="run-table">
<thead>
<tr>
<th>started</th>
<th>spec</th>
<th>seed</th>
<th>platform</th>
<th>duration</th>
<th>steps</th>
<th>violations</th>
<th></th>
</tr>
</thead>
<tbody>
{runs.map((run) => (
<tr key={run.id}>
<td>
<Link to={`/runs/${run.id}`}>{formatStartedAt(run.started_at)}</Link>
</td>
<td title={run.spec_path}>{basename(run.spec_path)}</td>
<td>{run.seed}</td>
<td>{run.platform}</td>
<td>{formatDuration(run.duration_millis)}</td>
<td>{run.step_count}</td>
<td>
{run.violation_count > 0 ? (
<span className="chip chip-violation">{run.violation_count}</span>
) : (
"0"
)}
</td>
<td>{run.in_progress ? <span className="chip chip-progress">in progress</span> : null}</td>
</tr>
))}
</tbody>
</table>
);
}