Files
web2app/frontend/src/pages/JobStatus.tsx
shumengya f6fa4bb1ca feat(frontend): show build duration on job status and list
Display elapsed time while building and total duration from start to completion for success and failure.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-29 13:57:31 +08:00

181 lines
5.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useEffect, useState } from "react";
import { Link, useParams } from "react-router-dom";
import { fetchBuild, statusLabel, type Build } from "../api/client";
import {
formatDateTime,
getBuildDuration,
isBuildFinished,
} from "../lib/duration";
export default function JobStatus() {
const { id } = useParams<{ id: string }>();
const [build, setBuild] = useState<Build | null>(null);
const [error, setError] = useState<string | null>(null);
const [now, setNow] = useState(() => Date.now());
useEffect(() => {
if (!id) return;
let cancelled = false;
async function poll() {
try {
const data = await fetchBuild(id!);
if (cancelled) return;
setBuild(data);
setError(null);
if (data.status === "completed" || data.status === "failed") {
return;
}
window.setTimeout(poll, 5000);
} catch (err) {
if (cancelled) return;
setError(err instanceof Error ? err.message : "加载失败");
window.setTimeout(poll, 5000);
}
}
poll();
return () => {
cancelled = true;
};
}, [id]);
useEffect(() => {
if (!build || isBuildFinished(build.status)) return;
const timer = window.setInterval(() => setNow(Date.now()), 1000);
return () => window.clearInterval(timer);
}, [build?.status, build?.id]);
const duration = build ? getBuildDuration(build, now) : null;
if (!id) {
return <p className="error-text"> ID</p>;
}
return (
<>
<div className="status-toolbar">
<h1 className="page-title" style={{ margin: 0 }}>
</h1>
<Link to="/"></Link>
</div>
{error ? <p className="error-text">{error}</p> : null}
<section className="doc-section">
{build ? (
<>
<dl className="meta-list">
<div>
<dt> ID</dt>
<dd>
<code>{build.id}</code>
</dd>
</div>
<div>
<dt></dt>
<dd>{build.appName}</dd>
</div>
<div>
<dt></dt>
<dd>{build.appNameEn}</dd>
</div>
<div>
<dt>Bundle ID</dt>
<dd>
<code>{build.appIdentifier}</code>
</dd>
</div>
<div>
<dt></dt>
<dd>
<code>{build.appVersion}</code>
</dd>
</div>
<div>
<dt></dt>
<dd>
<span className={`badge badge-${build.status}`}>
{statusLabel(build.status)}
</span>
</dd>
</div>
<div>
<dt></dt>
<dd>{formatDateTime(build.createdAt)}</dd>
</div>
{duration?.finished ? (
<div>
<dt></dt>
<dd>{formatDateTime(build.updatedAt)}</dd>
</div>
) : null}
<div>
<dt>{duration?.finished ? "构建总耗时" : "已用时间"}</dt>
<dd>
<strong>{duration?.text ?? "—"}</strong>
</dd>
</div>
</dl>
{build.status === "completed" ? (
<>
<h3></h3>
<div className="download-row">
{build.windowsUrl ? (
<a
className="btn btn-primary"
href={build.windowsUrl}
target="_blank"
rel="noreferrer"
>
Windows
</a>
) : (
<span className="muted">Windows </span>
)}
{build.androidUrl ? (
<a
className="btn btn-secondary"
href={build.androidUrl}
target="_blank"
rel="noreferrer"
>
Android
</a>
) : (
<span className="muted">Android </span>
)}
</div>
</>
) : null}
{build.status === "failed" ? (
<div className="error-box">
<p>{build.error ?? "构建失败,请查看 GitHub Actions 日志。"}</p>
{build.actionsUrl ? (
<a href={build.actionsUrl} target="_blank" rel="noreferrer">
Actions
</a>
) : null}
</div>
) : null}
{build.status !== "completed" && build.status !== "failed" ? (
<p className="prose" style={{ marginTop: "1rem" }}>
GitHub Actions
</p>
) : null}
</>
) : (
<p className="prose"></p>
)}
</section>
</>
);
}