Files
UHD_Media_Tool/apps/web/app/components/tasks/legacy-task.tsx
T

170 lines
5.8 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 { useQuery } from "@tanstack/react-query";
import { AssetOperation, DownloadTask, Task } from "@umt/contracts";
import { useEffect, useRef } from "react";
import { z } from "zod";
import { apiParsed, isMissingEndpoint } from "~/api/parse.ts";
import { DetailSection, DetailSheet } from "~/components/console/detail-sheet.tsx";
import { ErrorState, LoadingState } from "~/components/console/empty-state.tsx";
import { RecordLink, recordPath } from "~/components/console/record-sheet.tsx";
import type { StatusTone } from "~/components/console/status-badge.tsx";
import { Timeline } from "~/components/console/timeline.tsx";
import { TASK_KIND_LABEL, TaskActions, TaskStatusLine, TaskSummary } from "~/components/downloads/business-tasks.tsx";
import { TTL_SECOND } from "~/lib/api.ts";
import { useNavigate } from "~/lib/router-compat.tsx";
// 日志页任务抽屉打开的 id 不是引擎运行时(旧链接 /console/tasks/:id):先当旧业务任务查,
// 有引擎运行就换成那条;没有就在抽屉里显示旧任务的概况与文件操作;再查不到当下载任务 id,换到下载面板的抽屉。
const OP_TONE: Record<AssetOperation["status"], StatusTone> = {
planned: "neutral",
started: "neutral",
succeeded: "success",
failed: "destructive",
skipped: "neutral",
compensated: "warning",
};
const OP_STATUS: Record<AssetOperation["status"], string> = {
planned: "待执行",
started: "进行中",
succeeded: "成功",
failed: "失败",
skipped: "已跳过",
compensated: "已回滚",
};
const OP_PHASE: Record<AssetOperation["phase"], string> = {
decision: "判定",
prepare: "准备",
transfer: "传输",
verify: "校验",
commit: "提交",
cleanup: "清理",
compensate: "回滚",
};
const OP_KIND: Record<AssetOperation["kind"], string> = {
copy: "复制",
move: "移动",
rename: "改名",
mkdir: "建目录",
hardlink: "硬链接",
delete_source: "删除源文件",
delete_target: "删除目标文件",
verify: "校验",
register: "登记",
qb_pause: "暂停下载",
qb_resume: "恢复下载",
qb_remove: "移除下载",
qb_set_tags: "改下载标签",
other: "其他",
};
function useTask(id: string) {
return useQuery({
queryKey: ["tasks", "one", id],
retry: false,
queryFn: ({ signal }) => apiParsed(Task, "/api/v1/tasks/:id", { pathParams: { id }, signal }),
refetchInterval: (q) =>
q.state.data && ["pending", "running", "waiting"].includes(q.state.data.status) ? 5 * TTL_SECOND : false,
});
}
function useLegacyDownload(id: string, enabled: boolean) {
return useQuery({
queryKey: ["downloads", "legacy-probe", id],
enabled,
retry: false,
queryFn: ({ signal }) => apiParsed(DownloadTask, "/api/v1/download-tasks/:id", { pathParams: { id }, signal }),
});
}
function Operations({ taskId }: { taskId: string }) {
const ops = useQuery({
queryKey: ["tasks", "operations", taskId],
queryFn: ({ signal }) =>
apiParsed(z.object({ items: z.array(AssetOperation) }), "/api/v1/tasks/:id/operations", {
pathParams: { id: taskId },
signal,
}),
});
if (ops.isPending) return <LoadingState rows={3} />;
if (ops.error) return <ErrorState plain error={ops.error} onRetry={() => void ops.refetch()} />;
if (!ops.data.items.length) return <p className="text-caption text-muted-foreground">这个任务没有文件操作。</p>;
return (
<Timeline
items={ops.data.items.map((o) => ({
id: o.id,
at: o.startedAt ?? o.createdAt,
tone: OP_TONE[o.status],
title: `${OP_PHASE[o.phase]} · ${OP_KIND[o.kind]}:${OP_STATUS[o.status]}`,
meta: o.attempts > 1 ? `${o.attempts} 次` : undefined,
description: o.organizationRecordId ? (
<RecordLink page="organize" id={o.organizationRecordId} className="underline underline-offset-4">
整理记录 {o.organizationRecordId.slice(0, 8)}
</RecordLink>
) : o.downloadTaskId ? (
<RecordLink page="downloads" id={o.downloadTaskId} className="underline underline-offset-4">
下载任务 {o.downloadTaskId.slice(0, 8)}
</RecordLink>
) : undefined,
}))}
/>
);
}
/** 引擎运行查不到时的任务抽屉 */
export function LegacyTaskSheet({
id,
runError,
onOpenRun,
onClose,
}: {
id: string;
/** 引擎运行查询的错误(不是「不存在」时直接显示) */
runError: unknown;
onOpenRun: (runId: string) => void;
onClose: () => void;
}) {
const navigate = useNavigate();
const runMissing = isMissingEndpoint(runError);
const task = useTask(id);
const notFound = !!task.error && isMissingEndpoint(task.error);
const legacy = useLegacyDownload(id, runMissing && notFound);
const t = task.data;
const engineRunId = t?.engineRunId;
const openRun = useRef(onOpenRun);
openRun.current = onOpenRun;
useEffect(() => {
if (engineRunId) openRun.current(engineRunId);
}, [engineRunId]);
const downloadId = legacy.data?.id;
useEffect(() => {
if (downloadId) navigate(recordPath("downloads", downloadId), { replace: true });
}, [downloadId, navigate]);
return (
<DetailSheet
open
onOpenChange={(o) => !o && onClose()}
width="lg"
title={t ? (TASK_KIND_LABEL[t.kind] ?? t.kind) : "任务"}
description={t ? <TaskStatusLine t={t} /> : undefined}
footer={t ? <TaskActions t={t} /> : null}
>
{!runMissing ? (
<ErrorState plain error={runError} />
) : task.isPending || (notFound && legacy.isPending) || engineRunId || downloadId ? (
<LoadingState rows={5} />
) : !t ? (
<ErrorState plain error={task.error} onRetry={() => void task.refetch()} />
) : (
<>
<TaskSummary t={t} />
<DetailSection title="文件操作">
<Operations taskId={t.id} />
</DetailSection>
</>
)}
</DetailSheet>
);
}