194 lines
6.1 KiB
TypeScript
194 lines
6.1 KiB
TypeScript
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>
|
||
);
|
||
}
|