154 lines
4.4 KiB
TypeScript
154 lines
4.4 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,
|
|||
|
|
}: {
|
|||
|
|
visible: boolean;
|
|||
|
|
jobs: Job[];
|
|||
|
|
timezone?: string;
|
|||
|
|
onClose: () => void;
|
|||
|
|
onChange: () => void;
|
|||
|
|
onAccount: () => void;
|
|||
|
|
}) {
|
|||
|
|
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={520}
|
|||
|
|
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>
|
|||
|
|
<p className="muted">{formatTime(job.created_at, timezone)}</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>
|
|||
|
|
);
|
|||
|
|
}
|