Skip to content

Commit 4616358

Browse files
committed
feature: add the collection task executions page and the collection template page
1 parent 2b97a8c commit 4616358

17 files changed

Lines changed: 1159 additions & 563 deletions

File tree

frontend/src/pages/DataCollection/Create/CreateTask.tsx

Lines changed: 240 additions & 326 deletions
Large diffs are not rendered by default.

frontend/src/pages/DataCollection/Home/DataCollectionPage.tsx

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,27 @@
1-
import { useState } from "react";
1+
import { useEffect, useState } from "react";
22
import { Button, Tabs } from "antd";
33
import { PlusOutlined } from "@ant-design/icons";
44
import TaskManagement from "./TaskManagement";
5-
import ExecutionLog from "./ExecutionLog";
6-
import { useNavigate } from "react-router";
5+
import Execution from "./Execution.tsx";
6+
import TemplateManagement from "./TemplateManagement";
7+
import { useLocation, useNavigate } from "react-router";
78

89
export default function DataCollection() {
910
const navigate = useNavigate();
11+
const location = useLocation();
1012
const [activeTab, setActiveTab] = useState("task-management");
13+
const [taskId, setTaskId] = useState<string | undefined>(undefined);
14+
15+
useEffect(() => {
16+
const params = new URLSearchParams(location.search);
17+
const tab = params.get("tab") || undefined;
18+
const nextTaskId = params.get("taskId") || undefined;
19+
20+
if (tab === "task-execution" || tab === "task-management" || tab === "task-template") {
21+
setActiveTab(tab);
22+
}
23+
setTaskId(nextTaskId);
24+
}, [location.search]);
1125

1226
return (
1327
<div className="gap-4 h-full flex flex-col">
@@ -29,13 +43,20 @@ export default function DataCollection() {
2943
activeKey={activeTab}
3044
items={[
3145
{ label: "任务管理", key: "task-management" },
32-
// { label: "执行日志", key: "execution-log" },
46+
{ label: "执行记录", key: "task-execution" },
47+
{ label: "模板管理", key: "task-template" },
3348
]}
3449
onChange={(tab) => {
3550
setActiveTab(tab);
51+
setTaskId(undefined);
52+
const params = new URLSearchParams();
53+
params.set("tab", tab);
54+
navigate({ pathname: location.pathname, search: params.toString() }, { replace: true });
3655
}}
3756
/>
38-
{activeTab === "task-management" ? <TaskManagement /> : <ExecutionLog />}
57+
{activeTab === "task-management" ? <TaskManagement /> : null}
58+
{activeTab === "task-execution" ? <Execution taskId={taskId} /> : null}
59+
{activeTab === "task-template" ? <TemplateManagement /> : null}
3960
</div>
4061
);
4162
}
Lines changed: 291 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,291 @@
1+
import {Card, Badge, Button, Modal, Table, Tag} from "antd";
2+
import type { ColumnsType } from "antd/es/table";
3+
import { SearchControls } from "@/components/SearchControls";
4+
import { queryExecutionLogUsingPost } from "../collection.apis";
5+
import useFetchData from "@/hooks/useFetchData";
6+
import { useEffect, useState } from "react";
7+
import {TaskExecution} from "@/pages/DataCollection/collection.model.ts";
8+
import {mapTaskExecution} from "@/pages/DataCollection/collection.const.ts";
9+
import { queryExecutionLogFileByIdUsingGet } from "../collection.apis";
10+
import { FileTextOutlined } from "@ant-design/icons";
11+
12+
const filterOptions = [
13+
{
14+
key: "status",
15+
label: "状态筛选",
16+
options: [
17+
{ value: "all", label: "全部状态" },
18+
{ value: "RUNNING", label: "运行中" },
19+
{ value: "SUCCESS", label: "成功" },
20+
{ value: "FAILED", label: "失败" },
21+
{ value: "STOPPED", label: "停止" },
22+
],
23+
},
24+
];
25+
26+
export default function Execution({ taskId }: { taskId?: string }) {
27+
const [dateRange, setDateRange] = useState<[any, any] | null>(null);
28+
const [logOpen, setLogOpen] = useState(false);
29+
const [logLoading, setLogLoading] = useState(false);
30+
const [logTitle, setLogTitle] = useState<string>("");
31+
const [logContent, setLogContent] = useState<string>("");
32+
const [logFilename, setLogFilename] = useState<string>("");
33+
const [logBlobUrl, setLogBlobUrl] = useState<string>("");
34+
35+
const formatDuration = (seconds?: number) => {
36+
if (seconds === undefined || seconds === null) return "-";
37+
const total = Math.max(0, Math.floor(seconds));
38+
if (total < 60) return `${total}s`;
39+
const min = Math.floor(total / 60);
40+
const sec = total % 60;
41+
return `${min}min${sec}s`;
42+
};
43+
44+
const handleReset = () => {
45+
setSearchParams({
46+
keyword: "",
47+
filter: {
48+
type: [],
49+
status: [],
50+
tags: [],
51+
},
52+
current: 1,
53+
pageSize: 10,
54+
});
55+
setDateRange(null);
56+
};
57+
58+
const {
59+
loading,
60+
tableData,
61+
pagination,
62+
searchParams,
63+
setSearchParams,
64+
handleFiltersChange,
65+
handleKeywordChange,
66+
} = useFetchData<TaskExecution>(
67+
(params) => {
68+
const { keyword, start_time, end_time, ...rest } = params || {};
69+
return queryExecutionLogUsingPost({
70+
...rest,
71+
task_id: taskId || undefined,
72+
task_name: keyword || undefined,
73+
start_time,
74+
end_time,
75+
});
76+
},
77+
mapTaskExecution,
78+
30000,
79+
false,
80+
[],
81+
0
82+
);
83+
84+
useEffect(() => {
85+
setSearchParams((prev) => ({
86+
...prev,
87+
current: 1,
88+
}));
89+
}, [taskId, setSearchParams]);
90+
91+
const handleViewLog = async (record: TaskExecution) => {
92+
setLogOpen(true);
93+
setLogLoading(true);
94+
setLogTitle(`${record.taskName} / ${record.id}`);
95+
setLogContent("");
96+
setLogFilename("");
97+
if (logBlobUrl) {
98+
URL.revokeObjectURL(logBlobUrl);
99+
setLogBlobUrl("");
100+
}
101+
try {
102+
const { blob, filename } = await queryExecutionLogFileByIdUsingGet(record.id);
103+
setLogFilename(filename);
104+
const url = URL.createObjectURL(blob);
105+
setLogBlobUrl(url);
106+
const text = await blob.text();
107+
setLogContent(text);
108+
} catch (e: any) {
109+
setLogContent(e?.data?.detail || e?.message || "Failed to load log");
110+
} finally {
111+
setLogLoading(false);
112+
}
113+
};
114+
115+
const columns: ColumnsType<any> = [
116+
{
117+
title: "任务名称",
118+
dataIndex: "taskName",
119+
key: "taskName",
120+
fixed: "left",
121+
render: (text: string) => (
122+
<span style={{ fontWeight: 500 }}>{text}</span>
123+
),
124+
},
125+
{
126+
title: "状态",
127+
dataIndex: "status",
128+
key: "status",
129+
render: (status: any) => ((
130+
<Tag color={status.color}>{status.label}</Tag>
131+
)
132+
),
133+
},
134+
{
135+
title: "开始时间",
136+
dataIndex: "startedAt",
137+
key: "startedAt",
138+
},
139+
{
140+
title: "结束时间",
141+
dataIndex: "completedAt",
142+
key: "completedAt",
143+
},
144+
{
145+
title: "执行时长",
146+
dataIndex: "durationSeconds",
147+
key: "durationSeconds",
148+
render: (v?: number) => formatDuration(v),
149+
},
150+
{
151+
title: "错误信息",
152+
dataIndex: "errorMessage",
153+
key: "errorMessage",
154+
render: (msg?: string) =>
155+
msg ? (
156+
<span style={{ color: "#f5222d" }} title={msg}>
157+
{msg}
158+
</span>
159+
) : (
160+
<span style={{ color: "#bbb" }}>-</span>
161+
),
162+
},
163+
{
164+
title: "操作",
165+
key: "action",
166+
fixed: "right",
167+
width: 120,
168+
render: (_: any, record: TaskExecution) => (
169+
<Button
170+
type="link"
171+
icon={<FileTextOutlined />}
172+
onClick={() => handleViewLog(record)}
173+
>
174+
查看日志
175+
</Button>
176+
),
177+
},
178+
];
179+
180+
return (
181+
<div className="flex flex-col gap-4">
182+
{/* Filter Controls */}
183+
<div className="flex items-center justify-between gap-4">
184+
<SearchControls
185+
searchTerm={searchParams.keyword}
186+
onSearchChange={handleKeywordChange}
187+
filters={filterOptions}
188+
onFiltersChange={handleFiltersChange}
189+
showViewToggle={false}
190+
onClearFilters={() =>
191+
setSearchParams((prev) => ({
192+
...prev,
193+
filter: { ...prev.filter, status: [] },
194+
current: 1,
195+
}))
196+
}
197+
showDatePicker
198+
dateRange={dateRange as any}
199+
onDateChange={(date) => {
200+
setDateRange(date as any);
201+
const start = (date?.[0] as any)?.toISOString?.() || undefined;
202+
const end = (date?.[1] as any)?.toISOString?.() || undefined;
203+
setSearchParams((prev) => ({
204+
...prev,
205+
current: 1,
206+
start_time: start,
207+
end_time: end,
208+
}));
209+
}}
210+
onReload={handleReset}
211+
searchPlaceholder="搜索任务名称..."
212+
className="flex-1"
213+
/>
214+
</div>
215+
<Card>
216+
<Table
217+
loading={loading}
218+
columns={columns}
219+
dataSource={tableData}
220+
rowKey="id"
221+
pagination={pagination}
222+
scroll={{ x: "max-content" }}
223+
/>
224+
</Card>
225+
226+
<Modal
227+
title={logTitle || "执行日志"}
228+
open={logOpen}
229+
onCancel={() => {
230+
setLogOpen(false);
231+
if (logBlobUrl) {
232+
URL.revokeObjectURL(logBlobUrl);
233+
setLogBlobUrl("");
234+
}
235+
}}
236+
footer={
237+
<div style={{ display: "flex", justifyContent: "space-between", width: "100%" }}>
238+
<div style={{ color: "#6b7280", fontSize: 12 }}>{logFilename || ""}</div>
239+
<div style={{ display: "flex", gap: 8 }}>
240+
{logBlobUrl ? (
241+
<Button
242+
onClick={() => {
243+
const a = document.createElement("a");
244+
a.href = logBlobUrl;
245+
a.download = logFilename || "execution.log";
246+
document.body.appendChild(a);
247+
a.click();
248+
document.body.removeChild(a);
249+
}}
250+
>
251+
下载日志
252+
</Button>
253+
) : null}
254+
<Button
255+
type="primary"
256+
onClick={() => {
257+
setLogOpen(false);
258+
if (logBlobUrl) {
259+
URL.revokeObjectURL(logBlobUrl);
260+
setLogBlobUrl("");
261+
}
262+
}}
263+
>
264+
关闭
265+
</Button>
266+
</div>
267+
</div>
268+
}
269+
width={900}
270+
>
271+
<div
272+
style={{
273+
background: "#0b1020",
274+
color: "#e5e7eb",
275+
borderRadius: 8,
276+
padding: 12,
277+
maxHeight: "60vh",
278+
overflow: "auto",
279+
fontFamily: "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace",
280+
fontSize: 12,
281+
lineHeight: 1.5,
282+
whiteSpace: "pre-wrap",
283+
wordBreak: "break-word",
284+
}}
285+
>
286+
{logLoading ? "Loading..." : (logContent || "(empty)")}
287+
</div>
288+
</Modal>
289+
</div>
290+
);
291+
}

0 commit comments

Comments
 (0)