Add structured job progress tracking

This commit is contained in:
2026-06-30 14:18:02 +08:00
parent 93af8bcd3a
commit 442b521705
8 changed files with 454 additions and 9 deletions

View File

@@ -25,6 +25,16 @@ import "./styles.css";
const API_BASE = import.meta.env.VITE_API_BASE ?? "http://localhost:8010";
type JobProgress = {
percent: number | null;
label: string;
stage: string;
current: number | null;
total: number | null;
unit: string | null;
source: string;
};
type Job = {
id: string;
type: string;
@@ -35,6 +45,7 @@ type Job = {
finished_at?: string;
log_tail?: string;
params: Record<string, unknown>;
progress?: JobProgress;
};
type Catalog = {
@@ -273,6 +284,21 @@ function StatusPill({ status }: { status: string }) {
return <span className={`pill pill-${status}`}>{status}</span>;
}
function JobProgressBar({ progress }: { progress?: JobProgress }) {
const percent = typeof progress?.percent === "number" ? Math.max(0, Math.min(100, progress.percent)) : 0;
return (
<div className="progressBox" data-stage={progress?.stage ?? "unknown"}>
<div className="progressTrack" aria-label={progress?.label ?? "job progress"}>
<span style={{ width: `${percent}%` }} />
</div>
<div className="progressMeta">
<span>{progress?.label ?? "等待日志"}</span>
<strong>{typeof progress?.percent === "number" ? `${progress.percent.toFixed(progress.percent % 1 ? 1 : 0)}%` : "..."}</strong>
</div>
</div>
);
}
function App() {
const { catalog, gpus, jobs, results, curves, datasets, datasetValidations, coverage, acceptance, deepAcceptance, error, refresh } = useData();
const [taskType, setTaskType] = useState("mock.echo");
@@ -553,9 +579,12 @@ function App() {
<div className="jobList">
{jobs.slice(0, 12).map((job) => (
<button key={job.id} className="jobRow" onClick={() => inspectJob(job)}>
<span>{job.type}</span>
<StatusPill status={job.status} />
<div className="jobRowTop">
<span>{job.type}</span>
<StatusPill status={job.status} />
</div>
<small>{job.description || job.id.slice(0, 8)}</small>
<JobProgressBar progress={job.progress} />
</button>
))}
</div>
@@ -836,6 +865,7 @@ function App() {
<Square size={18} />
</button>
</div>
{selectedJob && <JobProgressBar progress={selectedJob.progress} />}
<pre>{log || "No log selected."}</pre>
</div>

View File

@@ -631,8 +631,87 @@ textarea {
color: var(--ink);
}
.jobRow {
grid-template-columns: 1fr;
}
.jobRowTop {
min-width: 0;
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
align-items: center;
gap: 8px;
}
.jobRowTop span:first-child {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.jobRow small {
grid-column: 1 / -1;
display: block;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.progressBox {
display: grid;
gap: 5px;
}
.progressTrack {
width: 100%;
height: 7px;
overflow: hidden;
border-radius: 999px;
background: #080a08;
border: 1px solid rgba(238, 242, 232, 0.1);
}
.progressTrack span {
display: block;
min-width: 0;
height: 100%;
border-radius: inherit;
background: var(--cyan);
transition: width 180ms ease;
}
.progressBox[data-stage="completed"] .progressTrack span,
.progressBox[data-stage="saving"] .progressTrack span {
background: var(--green);
}
.progressBox[data-stage="failed"] .progressTrack span {
background: var(--red);
}
.progressBox[data-stage="cancelled"] .progressTrack span {
background: var(--amber);
}
.progressMeta {
min-width: 0;
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
align-items: center;
gap: 8px;
color: var(--muted);
font-size: 11px;
}
.progressMeta span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.progressMeta strong {
color: var(--ink);
font-size: 11px;
}
.pill {
@@ -695,7 +774,7 @@ meter {
min-height: 340px;
max-height: 520px;
overflow: auto;
margin: 0;
margin: 12px 0 0;
padding: 14px;
color: #dbe7d8;
background: #080a08;