mirror of
https://github.com/priyanshujain/sanderling.git
synced 2026-10-02 19:17:10 +00:00
* 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.
103 lines
2.8 KiB
TypeScript
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>
|
|
);
|
|
}
|