jettask 0.2.7__py3-none-any.whl → 0.2.9__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- jettask/core/cli.py +242 -0
- jettask/pg_consumer/sql/add_execution_time_field.sql +29 -0
- jettask/pg_consumer/sql/create_new_tables.sql +137 -0
- jettask/pg_consumer/sql/create_tables_v3.sql +175 -0
- jettask/pg_consumer/sql/migrate_to_new_structure.sql +179 -0
- jettask/pg_consumer/sql/modify_time_fields.sql +69 -0
- jettask/webui/frontend/package.json +30 -0
- jettask/webui/frontend/src/App.css +109 -0
- jettask/webui/frontend/src/App.jsx +66 -0
- jettask/webui/frontend/src/components/NamespaceSelector.jsx +166 -0
- jettask/webui/frontend/src/components/QueueBacklogChart.jsx +298 -0
- jettask/webui/frontend/src/components/QueueBacklogTrend.jsx +638 -0
- jettask/webui/frontend/src/components/QueueDetailsTable.css +65 -0
- jettask/webui/frontend/src/components/QueueDetailsTable.jsx +487 -0
- jettask/webui/frontend/src/components/QueueDetailsTableV2.jsx +465 -0
- jettask/webui/frontend/src/components/ScheduledTaskFilter.jsx +423 -0
- jettask/webui/frontend/src/components/TaskFilter.jsx +425 -0
- jettask/webui/frontend/src/components/TimeRangeSelector.css +21 -0
- jettask/webui/frontend/src/components/TimeRangeSelector.jsx +160 -0
- jettask/webui/frontend/src/components/charts/QueueChart.jsx +111 -0
- jettask/webui/frontend/src/components/charts/QueueTrendChart.jsx +115 -0
- jettask/webui/frontend/src/components/charts/WorkerChart.jsx +40 -0
- jettask/webui/frontend/src/components/common/StatsCard.jsx +18 -0
- jettask/webui/frontend/src/components/layout/AppLayout.css +95 -0
- jettask/webui/frontend/src/components/layout/AppLayout.jsx +49 -0
- jettask/webui/frontend/src/components/layout/Header.css +106 -0
- jettask/webui/frontend/src/components/layout/Header.jsx +106 -0
- jettask/webui/frontend/src/components/layout/SideMenu.css +137 -0
- jettask/webui/frontend/src/components/layout/SideMenu.jsx +209 -0
- jettask/webui/frontend/src/components/layout/TabsNav.css +244 -0
- jettask/webui/frontend/src/components/layout/TabsNav.jsx +206 -0
- jettask/webui/frontend/src/components/layout/UserInfo.css +197 -0
- jettask/webui/frontend/src/components/layout/UserInfo.jsx +197 -0
- jettask/webui/frontend/src/contexts/LoadingContext.jsx +27 -0
- jettask/webui/frontend/src/contexts/NamespaceContext.jsx +72 -0
- jettask/webui/frontend/src/contexts/TabsContext.backup.jsx +245 -0
- jettask/webui/frontend/src/index.css +114 -0
- jettask/webui/frontend/src/main.jsx +20 -0
- jettask/webui/frontend/src/pages/Alerts.jsx +684 -0
- jettask/webui/frontend/src/pages/Dashboard/index.css +35 -0
- jettask/webui/frontend/src/pages/Dashboard/index.jsx +281 -0
- jettask/webui/frontend/src/pages/Dashboard.jsx +1330 -0
- jettask/webui/frontend/src/pages/QueueDetail.jsx +1117 -0
- jettask/webui/frontend/src/pages/QueueMonitor.jsx +527 -0
- jettask/webui/frontend/src/pages/Queues.jsx +12 -0
- jettask/webui/frontend/src/pages/ScheduledTasks.jsx +809 -0
- jettask/webui/frontend/src/pages/Settings.jsx +800 -0
- jettask/webui/frontend/src/pages/Workers.jsx +12 -0
- jettask/webui/frontend/src/services/api.js +114 -0
- jettask/webui/frontend/src/services/queueTrend.js +152 -0
- jettask/webui/frontend/src/utils/suppressWarnings.js +22 -0
- jettask/webui/frontend/src/utils/userPreferences.js +154 -0
- jettask/webui/frontend/vite.config.js +26 -0
- {jettask-0.2.7.dist-info → jettask-0.2.9.dist-info}/METADATA +1 -1
- {jettask-0.2.7.dist-info → jettask-0.2.9.dist-info}/RECORD +59 -14
- jettask/webui/static/dist/assets/index-7129cfe1.css +0 -1
- jettask/webui/static/dist/assets/index-8d1935cc.js +0 -774
- jettask/webui/static/dist/index.html +0 -15
- jettask/webui/static/index.html +0 -1734
- jettask/webui/static/queue.html +0 -981
- jettask/webui/static/queues.html +0 -549
- jettask/webui/static/workers.html +0 -734
- {jettask-0.2.7.dist-info → jettask-0.2.9.dist-info}/WHEEL +0 -0
- {jettask-0.2.7.dist-info → jettask-0.2.9.dist-info}/entry_points.txt +0 -0
- {jettask-0.2.7.dist-info → jettask-0.2.9.dist-info}/licenses/LICENSE +0 -0
- {jettask-0.2.7.dist-info → jettask-0.2.9.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,809 @@
|
|
1
|
+
import { useState, useEffect, useRef } from 'react';
|
2
|
+
import { useSearchParams } from 'react-router-dom';
|
3
|
+
import { Card, Button, Space, Tag, message, Modal, Form, Input, Select, Switch, Tooltip, Row, Col, Statistic, InputNumber, Empty } from 'antd';
|
4
|
+
import { PlusOutlined, EditOutlined, DeleteOutlined, PlayCircleOutlined, HistoryOutlined, ShareAltOutlined, FileTextOutlined, TagsOutlined, DatabaseOutlined } from '@ant-design/icons';
|
5
|
+
import ProTable from '@ant-design/pro-table';
|
6
|
+
import ScheduledTaskFilter from '../components/ScheduledTaskFilter';
|
7
|
+
import dayjs from 'dayjs';
|
8
|
+
import axios from 'axios';
|
9
|
+
import { useNamespace } from '../contexts/NamespaceContext';
|
10
|
+
|
11
|
+
const { Option } = Select;
|
12
|
+
const { TextArea } = Input;
|
13
|
+
|
14
|
+
// 任务类型配置
|
15
|
+
const TASK_TYPE_CONFIG = {
|
16
|
+
'cron': { label: 'Cron表达式', color: 'blue' },
|
17
|
+
'interval': { label: '间隔执行', color: 'green' },
|
18
|
+
'once': { label: '单次执行', color: 'orange' },
|
19
|
+
};
|
20
|
+
|
21
|
+
// 任务状态配置
|
22
|
+
const TASK_STATUS_CONFIG = {
|
23
|
+
'active': { label: '活跃', color: 'green' },
|
24
|
+
'paused': { label: '暂停', color: 'orange' },
|
25
|
+
'completed': { label: '已完成', color: 'gray' },
|
26
|
+
'error': { label: '错误', color: 'red' },
|
27
|
+
};
|
28
|
+
|
29
|
+
function ScheduledTasks() {
|
30
|
+
const [searchParams, setSearchParams] = useSearchParams();
|
31
|
+
const { currentNamespace } = useNamespace();
|
32
|
+
|
33
|
+
// 从URL参数初始化状态
|
34
|
+
const getInitialState = () => {
|
35
|
+
const urlFilters = searchParams.get('filters');
|
36
|
+
|
37
|
+
let initialFilters = [];
|
38
|
+
if (urlFilters) {
|
39
|
+
try {
|
40
|
+
initialFilters = JSON.parse(decodeURIComponent(urlFilters));
|
41
|
+
} catch (e) {
|
42
|
+
console.error('Failed to parse filters from URL:', e);
|
43
|
+
}
|
44
|
+
}
|
45
|
+
|
46
|
+
return { initialFilters };
|
47
|
+
};
|
48
|
+
|
49
|
+
const { initialFilters } = getInitialState();
|
50
|
+
|
51
|
+
const [selectedTask, setSelectedTask] = useState(null);
|
52
|
+
const [modalVisible, setModalVisible] = useState(false);
|
53
|
+
const [isEditMode, setIsEditMode] = useState(false);
|
54
|
+
const [form] = Form.useForm();
|
55
|
+
const [statistics, setStatistics] = useState({
|
56
|
+
total: 0,
|
57
|
+
active: 0,
|
58
|
+
todayExecutions: 0,
|
59
|
+
successRate: 0,
|
60
|
+
});
|
61
|
+
const [filters, setFilters] = useState(initialFilters);
|
62
|
+
const [detailModalVisible, setDetailModalVisible] = useState(false);
|
63
|
+
const [selectedTaskDetail, setSelectedTaskDetail] = useState(null);
|
64
|
+
const [selectedDetailField, setSelectedDetailField] = useState(null);
|
65
|
+
|
66
|
+
// ProTable相关
|
67
|
+
const actionRef = useRef();
|
68
|
+
const [tableHeight, setTableHeight] = useState(400);
|
69
|
+
|
70
|
+
// 计算表格高度
|
71
|
+
useEffect(() => {
|
72
|
+
const updateTableHeight = () => {
|
73
|
+
// 视窗高度 - 统计卡片(约120px) - 页边距(约50px) - ProTable工具栏和分页(约150px)
|
74
|
+
const availableHeight = window.innerHeight - 320;
|
75
|
+
const minHeight = 300; // 最小高度
|
76
|
+
const calculatedHeight = Math.max(availableHeight, minHeight);
|
77
|
+
setTableHeight(calculatedHeight);
|
78
|
+
};
|
79
|
+
|
80
|
+
// 初始计算
|
81
|
+
updateTableHeight();
|
82
|
+
|
83
|
+
// 监听窗口大小变化
|
84
|
+
window.addEventListener('resize', updateTableHeight);
|
85
|
+
return () => window.removeEventListener('resize', updateTableHeight);
|
86
|
+
}, []);
|
87
|
+
|
88
|
+
// ProTable的请求函数
|
89
|
+
const request = async (params) => {
|
90
|
+
// 如果没有选择命名空间,返回空数据
|
91
|
+
if (!currentNamespace) {
|
92
|
+
return {
|
93
|
+
data: [],
|
94
|
+
success: true,
|
95
|
+
total: 0,
|
96
|
+
};
|
97
|
+
}
|
98
|
+
|
99
|
+
try {
|
100
|
+
const requestParams = {
|
101
|
+
limit: params.pageSize,
|
102
|
+
offset: (params.current - 1) * params.pageSize,
|
103
|
+
};
|
104
|
+
|
105
|
+
const response = await axios.get(
|
106
|
+
`http://localhost:8001/api/data/scheduled-tasks/${currentNamespace}`,
|
107
|
+
{ params: requestParams }
|
108
|
+
);
|
109
|
+
|
110
|
+
if (response.data) {
|
111
|
+
calculateStatistics(response.data.tasks || []);
|
112
|
+
return {
|
113
|
+
data: response.data.tasks || [],
|
114
|
+
success: true,
|
115
|
+
total: response.data.total || 0,
|
116
|
+
};
|
117
|
+
}
|
118
|
+
return {
|
119
|
+
data: [],
|
120
|
+
success: false,
|
121
|
+
total: 0,
|
122
|
+
};
|
123
|
+
} catch (error) {
|
124
|
+
console.error('Failed to fetch scheduled tasks:', error);
|
125
|
+
return {
|
126
|
+
data: [],
|
127
|
+
success: false,
|
128
|
+
total: 0,
|
129
|
+
};
|
130
|
+
}
|
131
|
+
};
|
132
|
+
|
133
|
+
|
134
|
+
|
135
|
+
// 获取统计数据
|
136
|
+
const fetchStatistics = async () => {
|
137
|
+
if (!currentNamespace) {
|
138
|
+
return;
|
139
|
+
}
|
140
|
+
|
141
|
+
try {
|
142
|
+
const response = await axios.get(`/api/scheduled-tasks/statistics/${currentNamespace}`);
|
143
|
+
if (response.data) {
|
144
|
+
setStatistics(response.data);
|
145
|
+
}
|
146
|
+
} catch (error) {
|
147
|
+
console.error('Failed to fetch statistics:', error);
|
148
|
+
}
|
149
|
+
};
|
150
|
+
|
151
|
+
// 计算统计数据(保留以兼容旧逻辑)
|
152
|
+
const calculateStatistics = (taskList) => {
|
153
|
+
// 调用新的统计API
|
154
|
+
fetchStatistics();
|
155
|
+
};
|
156
|
+
|
157
|
+
// 筛选变化时刷新ProTable并保存状态
|
158
|
+
useEffect(() => {
|
159
|
+
if (actionRef.current) {
|
160
|
+
actionRef.current.reload();
|
161
|
+
}
|
162
|
+
}, [filters]);
|
163
|
+
|
164
|
+
// 组件加载时获取统计数据,命名空间变化时重新获取
|
165
|
+
useEffect(() => {
|
166
|
+
if (currentNamespace) {
|
167
|
+
fetchStatistics();
|
168
|
+
}
|
169
|
+
}, [currentNamespace]);
|
170
|
+
|
171
|
+
|
172
|
+
// 处理添加/编辑任务
|
173
|
+
const handleAddOrEditTask = () => {
|
174
|
+
form.validateFields().then(async (values) => {
|
175
|
+
try {
|
176
|
+
const url = isEditMode
|
177
|
+
? `/api/scheduled-tasks/${selectedTask.id}`
|
178
|
+
: '/api/scheduled-tasks';
|
179
|
+
const method = isEditMode ? 'put' : 'post';
|
180
|
+
|
181
|
+
// 处理task_data
|
182
|
+
let taskData = {};
|
183
|
+
if (values.task_data) {
|
184
|
+
try {
|
185
|
+
taskData = JSON.parse(values.task_data);
|
186
|
+
} catch (e) {
|
187
|
+
message.error('任务数据格式错误,请输入有效的JSON');
|
188
|
+
return;
|
189
|
+
}
|
190
|
+
}
|
191
|
+
|
192
|
+
// 构建请求数据
|
193
|
+
const requestData = {
|
194
|
+
namespace: currentNamespace,
|
195
|
+
name: values.name,
|
196
|
+
queue_name: values.queue_name,
|
197
|
+
schedule_type: values.schedule_type,
|
198
|
+
is_active: values.is_active !== false,
|
199
|
+
description: values.description,
|
200
|
+
task_data: taskData,
|
201
|
+
schedule_config: {}
|
202
|
+
};
|
203
|
+
|
204
|
+
// 根据schedule_type设置schedule_config
|
205
|
+
if (values.schedule_type === 'cron') {
|
206
|
+
requestData.schedule_config = { cron_expression: values.cron_expression };
|
207
|
+
} else if (values.schedule_type === 'interval') {
|
208
|
+
requestData.schedule_config = { seconds: values.interval_seconds || 60 };
|
209
|
+
}
|
210
|
+
|
211
|
+
const response = await axios[method](url, requestData);
|
212
|
+
if (response.data.success) {
|
213
|
+
message.success(isEditMode ? '任务更新成功' : '任务创建成功');
|
214
|
+
setModalVisible(false);
|
215
|
+
form.resetFields();
|
216
|
+
if (actionRef.current) {
|
217
|
+
actionRef.current.reload();
|
218
|
+
}
|
219
|
+
}
|
220
|
+
} catch (error) {
|
221
|
+
message.error(isEditMode ? '更新任务失败' : '创建任务失败');
|
222
|
+
console.error('Failed to save task:', error);
|
223
|
+
}
|
224
|
+
});
|
225
|
+
};
|
226
|
+
|
227
|
+
// 处理删除任务
|
228
|
+
const handleDeleteTask = (task) => {
|
229
|
+
Modal.confirm({
|
230
|
+
title: '确认删除',
|
231
|
+
content: `确定要删除定时任务 "${task.name}" 吗?`,
|
232
|
+
onOk: async () => {
|
233
|
+
try {
|
234
|
+
const response = await axios.delete(`/api/scheduled-tasks/${task.id}`);
|
235
|
+
if (response.data.success) {
|
236
|
+
message.success('任务删除成功');
|
237
|
+
if (actionRef.current) {
|
238
|
+
actionRef.current.reload();
|
239
|
+
}
|
240
|
+
}
|
241
|
+
} catch (error) {
|
242
|
+
message.error('删除任务失败');
|
243
|
+
console.error('Failed to delete task:', error);
|
244
|
+
}
|
245
|
+
},
|
246
|
+
});
|
247
|
+
};
|
248
|
+
|
249
|
+
// 处理启用/禁用任务
|
250
|
+
const handleToggleTask = async (task) => {
|
251
|
+
try {
|
252
|
+
const response = await axios.post(`/api/scheduled-tasks/${task.id}/toggle`);
|
253
|
+
if (response.data.success) {
|
254
|
+
message.success(task.is_active ? '任务已暂停' : '任务已启用');
|
255
|
+
if (actionRef.current) {
|
256
|
+
actionRef.current.reload();
|
257
|
+
}
|
258
|
+
}
|
259
|
+
} catch (error) {
|
260
|
+
message.error('操作失败');
|
261
|
+
console.error('Failed to toggle task:', error);
|
262
|
+
}
|
263
|
+
};
|
264
|
+
|
265
|
+
// 处理立即执行
|
266
|
+
const handleExecuteNow = async (task) => {
|
267
|
+
try {
|
268
|
+
const response = await axios.post(`/api/scheduled-tasks/${task.id}/execute`);
|
269
|
+
if (response.data.success) {
|
270
|
+
message.success('任务已触发执行');
|
271
|
+
}
|
272
|
+
} catch (error) {
|
273
|
+
message.error('执行失败');
|
274
|
+
console.error('Failed to execute task:', error);
|
275
|
+
}
|
276
|
+
};
|
277
|
+
|
278
|
+
// 打开添加/编辑模态框
|
279
|
+
const openModal = (task = null) => {
|
280
|
+
setIsEditMode(!!task);
|
281
|
+
setSelectedTask(task);
|
282
|
+
if (task) {
|
283
|
+
// 转换后端数据格式为表单格式
|
284
|
+
const formValues = {
|
285
|
+
name: task.name,
|
286
|
+
queue_name: task.queue_name,
|
287
|
+
schedule_type: task.schedule_type,
|
288
|
+
is_active: task.is_active,
|
289
|
+
description: task.description,
|
290
|
+
task_data: task.task_data ? JSON.stringify(task.task_data) : '{}',
|
291
|
+
};
|
292
|
+
|
293
|
+
// 根据schedule_type设置相应的配置字段
|
294
|
+
if (task.schedule_type === 'cron') {
|
295
|
+
formValues.cron_expression = task.schedule_config?.cron_expression;
|
296
|
+
} else if (task.schedule_type === 'interval') {
|
297
|
+
formValues.interval_seconds = task.schedule_config?.seconds;
|
298
|
+
}
|
299
|
+
|
300
|
+
form.setFieldsValue(formValues);
|
301
|
+
} else {
|
302
|
+
form.resetFields();
|
303
|
+
}
|
304
|
+
setModalVisible(true);
|
305
|
+
};
|
306
|
+
|
307
|
+
// 打开历史记录模态框
|
308
|
+
const openHistoryModal = (task) => {
|
309
|
+
// 跳转到任务队列详情页面,带上 scheduled_task_id 筛选参数
|
310
|
+
window.location.href = `/queue/${task.queue_name}?scheduled_task_id=${task.id}`;
|
311
|
+
};
|
312
|
+
|
313
|
+
// 查看任务详情
|
314
|
+
const handleViewDetail = (task, field) => {
|
315
|
+
setSelectedTaskDetail(task);
|
316
|
+
setSelectedDetailField(field);
|
317
|
+
setDetailModalVisible(true);
|
318
|
+
};
|
319
|
+
|
320
|
+
|
321
|
+
// ProTable列定义
|
322
|
+
const columns = [
|
323
|
+
{
|
324
|
+
title: 'ID',
|
325
|
+
dataIndex: 'id',
|
326
|
+
key: 'id',
|
327
|
+
width: 50,
|
328
|
+
ellipsis: true,
|
329
|
+
search: false,
|
330
|
+
},
|
331
|
+
{
|
332
|
+
title: '任务名称',
|
333
|
+
dataIndex: 'name',
|
334
|
+
key: 'name',
|
335
|
+
width: 200,
|
336
|
+
ellipsis: true,
|
337
|
+
search: false,
|
338
|
+
},
|
339
|
+
{
|
340
|
+
title: '队列名称',
|
341
|
+
dataIndex: 'queue_name',
|
342
|
+
key: 'queue_name',
|
343
|
+
width: 120,
|
344
|
+
search: false,
|
345
|
+
},
|
346
|
+
{
|
347
|
+
title: '调度类型',
|
348
|
+
dataIndex: 'schedule_type',
|
349
|
+
key: 'schedule_type',
|
350
|
+
width: 100,
|
351
|
+
valueType: 'select',
|
352
|
+
valueEnum: {
|
353
|
+
cron: { text: 'Cron表达式', status: 'Default' },
|
354
|
+
interval: { text: '间隔执行', status: 'Processing' },
|
355
|
+
once: { text: '单次执行', status: 'Warning' },
|
356
|
+
},
|
357
|
+
render: (type) => {
|
358
|
+
const config = TASK_TYPE_CONFIG[type] || { label: type, color: 'default' };
|
359
|
+
return <Tag color={config.color}>{config.label}</Tag>;
|
360
|
+
},
|
361
|
+
search: false,
|
362
|
+
},
|
363
|
+
{
|
364
|
+
title: '状态',
|
365
|
+
dataIndex: 'is_active',
|
366
|
+
key: 'is_active',
|
367
|
+
width: 60,
|
368
|
+
valueType: 'select',
|
369
|
+
valueEnum: {
|
370
|
+
true: { text: '启用', status: 'Success' },
|
371
|
+
false: { text: '暂停', status: 'Default' },
|
372
|
+
},
|
373
|
+
render: (_, record) => (
|
374
|
+
<Switch
|
375
|
+
checked={record.is_active}
|
376
|
+
size="small"
|
377
|
+
onChange={() => handleToggleTask(record)}
|
378
|
+
/>
|
379
|
+
),
|
380
|
+
search: false,
|
381
|
+
},
|
382
|
+
{
|
383
|
+
title: '调度配置',
|
384
|
+
key: 'schedule',
|
385
|
+
width: 80,
|
386
|
+
search: false,
|
387
|
+
render: (_, record) => {
|
388
|
+
if (record.schedule_type === 'cron') {
|
389
|
+
return <code>{record.schedule_config?.cron_expression || '-'}</code>;
|
390
|
+
} else if (record.schedule_type === 'interval') {
|
391
|
+
const seconds = record.schedule_config?.seconds || record.schedule_config?.minutes * 60;
|
392
|
+
return `每 ${seconds} 秒`;
|
393
|
+
} else {
|
394
|
+
return '-';
|
395
|
+
}
|
396
|
+
},
|
397
|
+
},
|
398
|
+
{
|
399
|
+
title: '描述',
|
400
|
+
dataIndex: 'description',
|
401
|
+
key: 'description',
|
402
|
+
width: 140,
|
403
|
+
ellipsis: true,
|
404
|
+
search: false,
|
405
|
+
render: (text) => (
|
406
|
+
text || '-'
|
407
|
+
),
|
408
|
+
},
|
409
|
+
{
|
410
|
+
title: '执行次数',
|
411
|
+
dataIndex: 'execution_count',
|
412
|
+
key: 'execution_count',
|
413
|
+
width: 60,
|
414
|
+
ellipsis: true,
|
415
|
+
search: false,
|
416
|
+
render: (text) => text || 0,
|
417
|
+
},
|
418
|
+
{
|
419
|
+
title: '上次执行',
|
420
|
+
dataIndex: 'last_run',
|
421
|
+
key: 'last_run',
|
422
|
+
width: 130,
|
423
|
+
search: false,
|
424
|
+
render: (_, record) => {
|
425
|
+
const time = record.last_run;
|
426
|
+
if (!time) return '-';
|
427
|
+
const date = dayjs(time);
|
428
|
+
return date.isValid() ? date.format('YYYY-MM-DD HH:mm:ss') : '-';
|
429
|
+
},
|
430
|
+
},
|
431
|
+
{
|
432
|
+
title: '下次执行',
|
433
|
+
dataIndex: 'next_run',
|
434
|
+
key: 'next_run',
|
435
|
+
width: 130,
|
436
|
+
search: false,
|
437
|
+
render: (_, record) => {
|
438
|
+
const time = record.next_run;
|
439
|
+
if (!time) return '-';
|
440
|
+
const date = dayjs(time);
|
441
|
+
return date.isValid() ? date.format('YYYY-MM-DD HH:mm:ss') : '-';
|
442
|
+
},
|
443
|
+
},
|
444
|
+
{
|
445
|
+
title: '创建时间',
|
446
|
+
dataIndex: 'created_at',
|
447
|
+
key: 'created_at',
|
448
|
+
width: 130,
|
449
|
+
search: false,
|
450
|
+
render: (_, record) => {
|
451
|
+
const text = record.created_at;
|
452
|
+
if (!text) return '-';
|
453
|
+
const date = dayjs(text);
|
454
|
+
return date.isValid() ? date.format('YYYY-MM-DD HH:mm:ss') : '-';
|
455
|
+
},
|
456
|
+
},
|
457
|
+
{
|
458
|
+
title: '操作',
|
459
|
+
key: 'actions',
|
460
|
+
width: 190,
|
461
|
+
fixed: 'right',
|
462
|
+
search: false,
|
463
|
+
render: (_, record) => (
|
464
|
+
<Space size="small">
|
465
|
+
<Tooltip title="查看任务参数">
|
466
|
+
<Button
|
467
|
+
type="link"
|
468
|
+
size="small"
|
469
|
+
icon={<FileTextOutlined />}
|
470
|
+
onClick={() => handleViewDetail(record, 'task_data')}
|
471
|
+
/>
|
472
|
+
</Tooltip>
|
473
|
+
<Tooltip title={record.tags ? "查看标签" : "无标签"}>
|
474
|
+
<Button
|
475
|
+
type="link"
|
476
|
+
size="small"
|
477
|
+
icon={<TagsOutlined />}
|
478
|
+
onClick={() => handleViewDetail(record, 'tags')}
|
479
|
+
disabled={!record.tags}
|
480
|
+
style={{ color: record.tags ? '#722ed1' : undefined }}
|
481
|
+
/>
|
482
|
+
</Tooltip>
|
483
|
+
<Tooltip title={record.metadata ? "查看元数据" : "无元数据"}>
|
484
|
+
<Button
|
485
|
+
type="link"
|
486
|
+
size="small"
|
487
|
+
icon={<DatabaseOutlined />}
|
488
|
+
onClick={() => handleViewDetail(record, 'metadata')}
|
489
|
+
disabled={!record.metadata}
|
490
|
+
style={{ color: record.metadata ? '#13c2c2' : undefined }}
|
491
|
+
/>
|
492
|
+
</Tooltip>
|
493
|
+
<Tooltip title="立即执行">
|
494
|
+
<Button
|
495
|
+
type="link"
|
496
|
+
size="small"
|
497
|
+
icon={<PlayCircleOutlined />}
|
498
|
+
onClick={() => handleExecuteNow(record)}
|
499
|
+
disabled={!record.is_active}
|
500
|
+
/>
|
501
|
+
</Tooltip>
|
502
|
+
<Tooltip title="历史">
|
503
|
+
<Button
|
504
|
+
type="link"
|
505
|
+
size="small"
|
506
|
+
icon={<HistoryOutlined />}
|
507
|
+
onClick={() => openHistoryModal(record)}
|
508
|
+
/>
|
509
|
+
</Tooltip>
|
510
|
+
<Tooltip title="编辑">
|
511
|
+
<Button
|
512
|
+
type="link"
|
513
|
+
size="small"
|
514
|
+
icon={<EditOutlined />}
|
515
|
+
onClick={() => {
|
516
|
+
setSelectedTask(record);
|
517
|
+
setIsEditMode(true);
|
518
|
+
openModal(record);
|
519
|
+
}}
|
520
|
+
/>
|
521
|
+
</Tooltip>
|
522
|
+
<Tooltip title="删除">
|
523
|
+
<Button
|
524
|
+
type="link"
|
525
|
+
size="small"
|
526
|
+
danger
|
527
|
+
icon={<DeleteOutlined />}
|
528
|
+
onClick={() => handleDeleteTask(record)}
|
529
|
+
/>
|
530
|
+
</Tooltip>
|
531
|
+
</Space>
|
532
|
+
),
|
533
|
+
},
|
534
|
+
];
|
535
|
+
|
536
|
+
|
537
|
+
// 如果没有选择命名空间,显示提示
|
538
|
+
if (!currentNamespace) {
|
539
|
+
return (
|
540
|
+
<div className="page-wrapper" style={{
|
541
|
+
display: 'flex',
|
542
|
+
alignItems: 'center',
|
543
|
+
justifyContent: 'center'
|
544
|
+
}}>
|
545
|
+
<Empty
|
546
|
+
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
547
|
+
description={
|
548
|
+
<span>
|
549
|
+
请先在右上角选择一个命名空间
|
550
|
+
<br />
|
551
|
+
<span style={{ color: '#999', fontSize: '12px' }}>
|
552
|
+
选择命名空间后才能查看该空间的定时任务
|
553
|
+
</span>
|
554
|
+
</span>
|
555
|
+
}
|
556
|
+
/>
|
557
|
+
</div>
|
558
|
+
);
|
559
|
+
}
|
560
|
+
|
561
|
+
return (
|
562
|
+
<div className="page-wrapper">
|
563
|
+
{/* 统计卡片 */}
|
564
|
+
<Card style={{ marginBottom: 16 }} bodyStyle={{ padding: '16px' }}>
|
565
|
+
<Row gutter={16}>
|
566
|
+
<Col span={6}>
|
567
|
+
<Statistic title="总任务数" value={statistics.total} />
|
568
|
+
</Col>
|
569
|
+
<Col span={6}>
|
570
|
+
<Statistic
|
571
|
+
title="活跃任务"
|
572
|
+
value={statistics.active}
|
573
|
+
valueStyle={{ color: '#3f8600' }}
|
574
|
+
/>
|
575
|
+
</Col>
|
576
|
+
<Col span={6}>
|
577
|
+
<Statistic
|
578
|
+
title="今日执行次数"
|
579
|
+
value={statistics.todayExecutions}
|
580
|
+
valueStyle={{ color: '#1890ff' }}
|
581
|
+
/>
|
582
|
+
</Col>
|
583
|
+
<Col span={6}>
|
584
|
+
<Statistic
|
585
|
+
title="成功率"
|
586
|
+
value={statistics.successRate}
|
587
|
+
precision={1}
|
588
|
+
suffix="%"
|
589
|
+
valueStyle={{ color: statistics.successRate > 90 ? '#3f8600' : '#cf1322' }}
|
590
|
+
/>
|
591
|
+
</Col>
|
592
|
+
</Row>
|
593
|
+
</Card>
|
594
|
+
|
595
|
+
{/* ProTable卡片 */}
|
596
|
+
<Card style={{ marginBottom: 0 }}>
|
597
|
+
<ProTable
|
598
|
+
columns={columns}
|
599
|
+
actionRef={actionRef}
|
600
|
+
request={request}
|
601
|
+
rowKey="id"
|
602
|
+
pagination={{
|
603
|
+
showQuickJumper: true,
|
604
|
+
showSizeChanger: true,
|
605
|
+
defaultPageSize: 20,
|
606
|
+
}}
|
607
|
+
search={false}
|
608
|
+
dateFormatter="string"
|
609
|
+
headerTitle="定时任务列表"
|
610
|
+
scroll={{
|
611
|
+
y: tableHeight,
|
612
|
+
x: 1680,
|
613
|
+
}}
|
614
|
+
size="small"
|
615
|
+
options={{
|
616
|
+
reload: true,
|
617
|
+
density: true,
|
618
|
+
fullScreen: true,
|
619
|
+
setting: true,
|
620
|
+
}}
|
621
|
+
toolbar={{
|
622
|
+
title: (
|
623
|
+
<Space>
|
624
|
+
<ScheduledTaskFilter
|
625
|
+
filters={filters}
|
626
|
+
onFiltersChange={(newFilters) => {
|
627
|
+
setFilters(newFilters);
|
628
|
+
if (actionRef.current) {
|
629
|
+
actionRef.current.reload();
|
630
|
+
}
|
631
|
+
}}
|
632
|
+
/>
|
633
|
+
</Space>
|
634
|
+
),
|
635
|
+
actions: [
|
636
|
+
<Button
|
637
|
+
key="add"
|
638
|
+
type="primary"
|
639
|
+
icon={<PlusOutlined />}
|
640
|
+
onClick={() => openModal()}
|
641
|
+
>
|
642
|
+
新建任务
|
643
|
+
</Button>,
|
644
|
+
<Button
|
645
|
+
key="share"
|
646
|
+
icon={<ShareAltOutlined />}
|
647
|
+
onClick={() => {
|
648
|
+
if (filters && filters.length > 0) {
|
649
|
+
const params = new URLSearchParams();
|
650
|
+
params.set('filters', encodeURIComponent(JSON.stringify(filters)));
|
651
|
+
const shareUrl = `${window.location.origin}${window.location.pathname}?${params.toString()}`;
|
652
|
+
navigator.clipboard.writeText(shareUrl);
|
653
|
+
message.success('分享链接已复制到剪贴板');
|
654
|
+
} else {
|
655
|
+
message.info('当前没有筛选条件可分享');
|
656
|
+
}
|
657
|
+
}}
|
658
|
+
>
|
659
|
+
分享筛选
|
660
|
+
</Button>,
|
661
|
+
],
|
662
|
+
}}
|
663
|
+
params={{
|
664
|
+
filters,
|
665
|
+
currentNamespace,
|
666
|
+
}}
|
667
|
+
/>
|
668
|
+
</Card>
|
669
|
+
|
670
|
+
{/* 详情查看弹窗 */}
|
671
|
+
<Modal
|
672
|
+
title={`${
|
673
|
+
selectedDetailField === 'task_data' ? '任务参数' :
|
674
|
+
selectedDetailField === 'tags' ? '标签' :
|
675
|
+
selectedDetailField === 'metadata' ? '元数据' : ''
|
676
|
+
} - ${selectedTaskDetail?.name || ''}`}
|
677
|
+
open={detailModalVisible}
|
678
|
+
onCancel={() => {
|
679
|
+
setDetailModalVisible(false);
|
680
|
+
setSelectedTaskDetail(null);
|
681
|
+
setSelectedDetailField(null);
|
682
|
+
}}
|
683
|
+
width={800}
|
684
|
+
footer={null}
|
685
|
+
>
|
686
|
+
<div style={{ maxHeight: '60vh', overflowY: 'auto' }}>
|
687
|
+
<pre style={{
|
688
|
+
backgroundColor: '#f5f5f5',
|
689
|
+
padding: '16px',
|
690
|
+
borderRadius: '4px',
|
691
|
+
fontSize: '12px',
|
692
|
+
lineHeight: '1.5',
|
693
|
+
overflowX: 'auto'
|
694
|
+
}}>
|
695
|
+
{selectedTaskDetail && selectedDetailField ?
|
696
|
+
JSON.stringify(selectedTaskDetail[selectedDetailField], null, 2) :
|
697
|
+
'无数据'
|
698
|
+
}
|
699
|
+
</pre>
|
700
|
+
</div>
|
701
|
+
</Modal>
|
702
|
+
|
703
|
+
{/* 添加/编辑任务模态框 */}
|
704
|
+
<Modal
|
705
|
+
title={isEditMode ? '编辑定时任务' : '添加定时任务'}
|
706
|
+
open={modalVisible}
|
707
|
+
onOk={handleAddOrEditTask}
|
708
|
+
onCancel={() => {
|
709
|
+
setModalVisible(false);
|
710
|
+
form.resetFields();
|
711
|
+
}}
|
712
|
+
width={600}
|
713
|
+
>
|
714
|
+
<Form
|
715
|
+
form={form}
|
716
|
+
layout="vertical"
|
717
|
+
initialValues={{
|
718
|
+
schedule_type: 'interval',
|
719
|
+
is_active: true,
|
720
|
+
task_data: '{}',
|
721
|
+
}}
|
722
|
+
>
|
723
|
+
<Form.Item
|
724
|
+
name="name"
|
725
|
+
label="任务名称"
|
726
|
+
rules={[{ required: true, message: '请输入任务名称' }]}
|
727
|
+
>
|
728
|
+
<Input placeholder="任务的描述性名称" />
|
729
|
+
</Form.Item>
|
730
|
+
|
731
|
+
<Form.Item
|
732
|
+
name="schedule_type"
|
733
|
+
label="调度类型"
|
734
|
+
rules={[{ required: true, message: '请选择调度类型' }]}
|
735
|
+
>
|
736
|
+
<Select>
|
737
|
+
<Option value="interval">间隔执行</Option>
|
738
|
+
<Option value="cron">Cron表达式</Option>
|
739
|
+
<Option value="once">单次执行</Option>
|
740
|
+
</Select>
|
741
|
+
</Form.Item>
|
742
|
+
|
743
|
+
<Form.Item noStyle shouldUpdate={(prevValues, currentValues) => prevValues.schedule_type !== currentValues.schedule_type}>
|
744
|
+
{({ getFieldValue }) => {
|
745
|
+
const scheduleType = getFieldValue('schedule_type');
|
746
|
+
if (scheduleType === 'cron') {
|
747
|
+
return (
|
748
|
+
<Form.Item
|
749
|
+
name="cron_expression"
|
750
|
+
label="Cron表达式"
|
751
|
+
rules={[{ required: true, message: '请输入Cron表达式' }]}
|
752
|
+
extra="例如: 0 0 * * * (每天零点)"
|
753
|
+
>
|
754
|
+
<Input placeholder="* * * * *" />
|
755
|
+
</Form.Item>
|
756
|
+
);
|
757
|
+
} else if (scheduleType === 'interval') {
|
758
|
+
return (
|
759
|
+
<Form.Item
|
760
|
+
name="interval_seconds"
|
761
|
+
label="执行间隔(秒)"
|
762
|
+
rules={[{ required: true, message: '请输入执行间隔' }]}
|
763
|
+
>
|
764
|
+
<InputNumber min={1} style={{ width: '100%' }} placeholder="60" />
|
765
|
+
</Form.Item>
|
766
|
+
);
|
767
|
+
}
|
768
|
+
return null;
|
769
|
+
}}
|
770
|
+
</Form.Item>
|
771
|
+
|
772
|
+
<Form.Item
|
773
|
+
name="queue_name"
|
774
|
+
label="目标队列"
|
775
|
+
rules={[{ required: true, message: '请输入目标队列' }]}
|
776
|
+
>
|
777
|
+
<Input placeholder="default" />
|
778
|
+
</Form.Item>
|
779
|
+
|
780
|
+
<Form.Item
|
781
|
+
name="task_data"
|
782
|
+
label="任务数据 (JSON格式)"
|
783
|
+
extra={'例如: {"args": [], "kwargs": {"key": "value"}}'}
|
784
|
+
>
|
785
|
+
<TextArea rows={3} placeholder="{}" />
|
786
|
+
</Form.Item>
|
787
|
+
|
788
|
+
|
789
|
+
<Form.Item
|
790
|
+
name="description"
|
791
|
+
label="任务描述"
|
792
|
+
>
|
793
|
+
<TextArea rows={2} placeholder="任务的详细描述" />
|
794
|
+
</Form.Item>
|
795
|
+
|
796
|
+
<Form.Item
|
797
|
+
name="is_active"
|
798
|
+
label="启用状态"
|
799
|
+
valuePropName="checked"
|
800
|
+
>
|
801
|
+
<Switch checkedChildren="启用" unCheckedChildren="暂停" />
|
802
|
+
</Form.Item>
|
803
|
+
</Form>
|
804
|
+
</Modal>
|
805
|
+
</div>
|
806
|
+
);
|
807
|
+
}
|
808
|
+
|
809
|
+
export default ScheduledTasks;
|