deepdata: core product code (backend, frontend, runtime, deepflow module)

This commit is contained in:
figmar
2026-08-30 19:23:38 +08:00
commit bd3db30be9
1405 changed files with 271727 additions and 0 deletions
@@ -0,0 +1,311 @@
import { Card, Badge, Button, Modal, Table, Tag } from "antd";
import type { ColumnsType } from "antd/es/table";
import { SearchControls } from "@/components/SearchControls";
import { queryExecutionLogUsingPost } from "../collection.apis";
import useFetchData from "@/hooks/useFetchData";
import { useEffect, useState } from "react";
import { TaskExecution } from "@/pages/DataCollection/collection.model.ts";
import { mapTaskExecution } from "@/pages/DataCollection/collection.const.ts";
import { queryExecutionLogFileByIdUsingGet } from "../collection.apis";
import { FileTextOutlined } from "@ant-design/icons";
import { useTranslation } from "react-i18next";
export default function Execution({ taskId }: { taskId?: string }) {
const { t } = useTranslation();
const [dateRange, setDateRange] = useState<[any, any] | null>(null);
const [logOpen, setLogOpen] = useState(false);
const [logLoading, setLogLoading] = useState(false);
const [logTitle, setLogTitle] = useState<string>("");
const [logContent, setLogContent] = useState<string>("");
const [logFilename, setLogFilename] = useState<string>("");
const [logBlobUrl, setLogBlobUrl] = useState<string>("");
const filterOptions = [
{
key: "status",
label: t("dataCollection.execution.filters.statusFilter"),
options: [
{ value: "all", label: t("dataCollection.execution.filters.allStatus") },
{ value: "RUNNING", label: t("dataCollection.execution.filters.running") },
{ value: "COMPLETED", label: t("dataCollection.execution.filters.success") },
{ value: "FAILED", label: t("dataCollection.execution.filters.failed") },
],
},
];
const formatDuration = (seconds?: number) => {
if (seconds === undefined || seconds === null) return t("common.placeholders.empty");
const total = Math.max(0, Math.floor(seconds));
if (total < 60) return `${total}${t("dataCollection.execution.duration.secondsSuffix")}`;
const min = Math.floor(total / 60);
const sec = total % 60;
return `${min}${t("dataCollection.execution.duration.minutesSuffix")}${sec}${t("dataCollection.execution.duration.secondsSuffix")}`;
};
const handleReset = () => {
setDateRange(null);
setSearchParams({
keyword: "",
filter: {
type: [],
status: [],
tags: [],
},
current: 1,
pageSize: 10,
start_time: undefined,
end_time: undefined,
});
};
const {
loading,
tableData,
pagination,
searchParams,
setSearchParams,
handleFiltersChange,
handleKeywordChange,
} = useFetchData<TaskExecution>(
(params) => {
const { keyword, start_time, end_time, status, ...rest } = params || {};
return queryExecutionLogUsingPost({
...rest,
task_id: taskId || undefined,
task_name: keyword || undefined,
status: status || undefined,
start_time,
end_time,
});
},
(execution) => mapTaskExecution(execution, t),
30000,
false,
[],
0
);
useEffect(() => {
setSearchParams((prev) => ({
...prev,
current: 1,
}));
}, [taskId, setSearchParams, t]);
const handleViewLog = async (record: TaskExecution) => {
setLogOpen(true);
setLogLoading(true);
setLogTitle(`${record.taskName} / ${record.id}`);
setLogContent("");
setLogFilename("");
if (logBlobUrl) {
URL.revokeObjectURL(logBlobUrl);
setLogBlobUrl("");
}
try {
const { blob, filename } = await queryExecutionLogFileByIdUsingGet(record.id);
setLogFilename(filename);
const url = URL.createObjectURL(blob);
setLogBlobUrl(url);
const text = await blob.text();
setLogContent(text);
} catch (e: any) {
setLogContent(e?.data?.detail || e?.message || t("dataCollection.execution.messages.loadLogFailed"));
} finally {
setLogLoading(false);
}
};
const columns: ColumnsType<any> = [
{
title: t("dataCollection.execution.columns.taskName"),
dataIndex: "taskName",
key: "taskName",
fixed: "left",
render: (text: string) => (
<span style={{ fontWeight: 500 }}>{text}</span>
),
},
{
title: t("dataCollection.execution.columns.status"),
dataIndex: "status",
key: "status",
render: (status: any) => ((
<Tag color={status.color}>{status.label}</Tag>
)
),
},
{
title: t("dataCollection.execution.columns.startTime"),
dataIndex: "startedAt",
key: "startedAt",
},
{
title: t("dataCollection.execution.columns.endTime"),
dataIndex: "completedAt",
key: "completedAt",
},
{
title: t("dataCollection.execution.columns.duration"),
dataIndex: "durationSeconds",
key: "durationSeconds",
render: (v?: number) => formatDuration(v),
},
{
title: t("dataCollection.execution.columns.errorMessage"),
dataIndex: "errorMessage",
key: "errorMessage",
render: (msg?: string) =>
msg ? (
<span style={{ color: "#f5222d" }} title={msg}>
{msg}
</span>
) : (
<span style={{ color: "#bbb" }}>{t("common.placeholders.empty")}</span>
),
},
{
title: t("dataCollection.execution.columns.actions"),
key: "action",
fixed: "right",
width: 120,
render: (_: any, record: TaskExecution) => (
<Button
type="link"
icon={<FileTextOutlined />}
onClick={() => handleViewLog(record)}
>
{t("dataCollection.execution.actions.viewLog")}
</Button>
),
},
];
return (
<div className="flex flex-col gap-4">
{/* Filter Controls */}
<div className="flex items-center justify-between gap-4">
<SearchControls
searchTerm={searchParams.keyword}
onSearchChange={handleKeywordChange}
filters={filterOptions}
selectedFilters={searchParams.filter}
onFiltersChange={handleFiltersChange}
showViewToggle={false}
onClearFilters={() => {
setDateRange(null);
setSearchParams((prev) => ({
...prev,
filter: { ...prev.filter, status: [] },
current: 1,
keyword: "",
start_time: undefined,
end_time: undefined,
}));
}}
showDatePicker
dateRange={dateRange as any}
onDateChange={(date) => {
// 自动修正日期顺序:确保开始时间 <= 结束时间
if (date && date[0] && date[1] && date[0].isAfter(date[1])) {
// 如果开始时间晚于结束时间,交换它们
date = [date[1], date[0]];
}
setDateRange(date as any);
// 转换为不带时区的 ISO 格式字符串(YYYY-MM-DDTHH:mm:ss)
const toLocalISOString = (d: any) => {
if (!d) return undefined;
const pad = (n: number) => String(n).padStart(2, '0');
return `${d.year()}-${pad(d.month() + 1)}-${pad(d.date())}T${pad(d.hour())}:${pad(d.minute())}:${pad(d.second())}`;
};
setSearchParams((prev) => ({
...prev,
current: 1,
start_time: toLocalISOString(date?.[0]),
end_time: toLocalISOString(date?.[1]),
}));
}}
onReload={handleReset}
searchPlaceholder={t("dataCollection.execution.filters.searchPlaceholder")}
className="flex-1"
/>
</div>
<Card>
<Table
loading={loading}
columns={columns}
dataSource={tableData}
rowKey="id"
pagination={pagination}
scroll={{ x: "max-content" }}
/>
</Card>
<Modal
title={logTitle || t("dataCollection.execution.modal.title")}
open={logOpen}
onCancel={() => {
setLogOpen(false);
if (logBlobUrl) {
URL.revokeObjectURL(logBlobUrl);
setLogBlobUrl("");
}
}}
footer={
<div style={{ display: "flex", justifyContent: "space-between", width: "100%" }}>
<div style={{ color: "#6b7280", fontSize: 12 }}>{logFilename || ""}</div>
<div style={{ display: "flex", gap: 8 }}>
{logBlobUrl ? (
<Button
onClick={() => {
const a = document.createElement("a");
a.href = logBlobUrl;
a.download = logFilename || "execution.log";
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
}}
>
{t("dataCollection.execution.modal.downloadLog")}
</Button>
) : null}
<Button
type="primary"
onClick={() => {
setLogOpen(false);
if (logBlobUrl) {
URL.revokeObjectURL(logBlobUrl);
setLogBlobUrl("");
}
}}
>
{t("dataCollection.execution.modal.close")}
</Button>
</div>
</div>
}
width={900}
>
<div
style={{
background: "#0b1020",
color: "#e5e7eb",
borderRadius: 8,
padding: 12,
maxHeight: "60vh",
overflow: "auto",
fontFamily: "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace",
fontSize: 12,
lineHeight: 1.5,
whiteSpace: "pre-wrap",
wordBreak: "break-word",
}}
>
{logLoading
? t("dataCollection.execution.modal.loading")
: (logContent || t("dataCollection.execution.modal.empty"))}
</div>
</Modal>
</div>
);
}