Files
worldquant-alpha-system/frontend/src/components/JobPanel.tsx
T
2026-09-09 20:10:20 +08:00

194 lines
6.1 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 { useState } from "react";
import {
Button,
Empty,
Progress,
SideSheet,
Tag,
Toast,
} from "@douyinfe/semi-ui-19";
import { api, formatTime, jobLabels, jobStateLabels, post } from "../api";
import type { Job } from "../types";
export function JobPanel({
visible,
jobs,
timezone,
onClose,
onChange,
onAccount,
chatOffset = 0,
}: {
visible: boolean;
jobs: Job[];
timezone?: string;
onClose: () => void;
onChange: () => void;
onAccount: () => void;
chatOffset?: number;
}) {
const [errors, setErrors] = useState<
Record<string, { alpha_id: string; error: string }[]>
>({});
const [busy, setBusy] = useState("");
async function action(job: Job, name: string) {
setBusy(job.id);
try {
await post(`/sync-jobs/${job.id}/${name}`);
onChange();
} catch (e) {
Toast.error((e as Error).message);
} finally {
setBusy("");
}
}
return (
<SideSheet
closeOnEsc
className="job-panel"
title="同步任务"
visible={visible}
width={Math.min(520, window.innerWidth - chatOffset)}
style={{ right: chatOffset }}
maskStyle={{ right: chatOffset }}
onCancel={onClose}
>
<p className="muted">同步进度保存在服务端,关闭此面板不会中断任务。</p>
{!jobs.length && <Empty description="暂时没有同步任务" />}
{jobs.map((job) => (
<article className="job-card" key={job.id}>
<div className="job-header">
<strong>{jobLabels[job.kind] ?? job.kind}</strong>
<Tag
color={
job.status === "completed"
? "green"
: ["failed", "completed_with_errors"].includes(job.status)
? "red"
: "blue"
}
>
{jobStateLabels[job.status] ?? job.status}
</Tag>
</div>
{job.payload?.scope && (
<p>
{job.payload.dataset_id || "数据集目录"} ·{" "}
{job.payload.scope.region} · {job.payload.scope.universe} · Delay{" "}
{job.payload.scope.delay}
</p>
)}
<p className="muted">{formatTime(job.created_at, timezone)}</p>
{job.payload?.submission && (
<p className="muted">
{job.payload.submission === "SUBMITTED" ? "已提交" : "待提交"}
{job.payload.date_from
? ` · ${job.payload.date_from} 至 ${job.payload.date_to}(UTC)`
: " · 全量"}
</p>
)}
{job.checkpoint?.date && (
<p className="muted">
同步日期:{job.checkpoint.date} · 已完成{" "}
{job.checkpoint.dates_completed} / {job.checkpoint.dates_total} 天
</p>
)}
{job.kind === "pnl_backfill" && (
<p className="muted">
{job.total === 0
? "已提交 Alpha 的 PnL 已齐全,无需补取"
: `仅补取缺失的 PnL${job.checkpoint?.alpha_id ? ` · ${job.checkpoint.alpha_id}` : ""}`}
</p>
)}
{job.kind === "self_correlation" && job.checkpoint?.alpha_id && (
<p className="muted">
{job.checkpoint.alpha_id} ·{" "}
{job.checkpoint.phase === "calculating"
? "计算相关性"
: `准备 PnL:${job.checkpoint.references_loaded ?? 0} / ${job.checkpoint.references_total ?? 0} 个基准`}
</p>
)}
<div className="job-numbers">
<span>
已处理 <b>{job.processed}</b>
</span>
<span>
失败 <b>{job.failed}</b>
</span>
<span>
总数 <b>{job.total ?? "计算中"}</b>
</span>
</div>
{job.total !== null && job.total > 0 && (
<Progress
percent={Math.min(
100,
Math.round(((job.processed + job.failed) / job.total) * 100),
)}
showInfo={false}
/>
)}
{job.error && <p className="error-text">{job.error}</p>}
{job.next_retry_at && (
<p className="muted">
下次重试:{formatTime(job.next_retry_at, timezone)}
</p>
)}
<div className="inline-actions">
{[
"queued",
"running",
"waiting_auth",
"waiting_connection",
].includes(job.status) && (
<Button
size="small"
loading={busy === job.id}
onClick={() => void action(job, "cancel")}
>
取消
</Button>
)}
{["failed", "cancelled", "completed_with_errors"].includes(
job.status,
) && (
<Button
size="small"
loading={busy === job.id}
onClick={() => void action(job, "retry")}
>
重试未完成部分
</Button>
)}
{["waiting_auth", "waiting_connection"].includes(job.status) && (
<Button size="small" onClick={onAccount}>
处理账户连接
</Button>
)}
{job.failed > 0 && (
<Button
size="small"
theme="borderless"
onClick={() =>
api<{ alpha_id: string; error: string }[]>(
`/sync-jobs/${job.id}/errors`,
)
.then((rows) => setErrors({ ...errors, [job.id]: rows }))
.catch((e) => Toast.error(e.message))
}
>
查看失败记录
</Button>
)}
</div>
{errors[job.id]?.map((e) => (
<p className="error-text" key={e.alpha_id}>
{e.alpha_id}:{e.error}
</p>
))}
</article>
))}
</SideSheet>
);
}