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.
Files changed (66) hide show
  1. jettask/core/cli.py +242 -0
  2. jettask/pg_consumer/sql/add_execution_time_field.sql +29 -0
  3. jettask/pg_consumer/sql/create_new_tables.sql +137 -0
  4. jettask/pg_consumer/sql/create_tables_v3.sql +175 -0
  5. jettask/pg_consumer/sql/migrate_to_new_structure.sql +179 -0
  6. jettask/pg_consumer/sql/modify_time_fields.sql +69 -0
  7. jettask/webui/frontend/package.json +30 -0
  8. jettask/webui/frontend/src/App.css +109 -0
  9. jettask/webui/frontend/src/App.jsx +66 -0
  10. jettask/webui/frontend/src/components/NamespaceSelector.jsx +166 -0
  11. jettask/webui/frontend/src/components/QueueBacklogChart.jsx +298 -0
  12. jettask/webui/frontend/src/components/QueueBacklogTrend.jsx +638 -0
  13. jettask/webui/frontend/src/components/QueueDetailsTable.css +65 -0
  14. jettask/webui/frontend/src/components/QueueDetailsTable.jsx +487 -0
  15. jettask/webui/frontend/src/components/QueueDetailsTableV2.jsx +465 -0
  16. jettask/webui/frontend/src/components/ScheduledTaskFilter.jsx +423 -0
  17. jettask/webui/frontend/src/components/TaskFilter.jsx +425 -0
  18. jettask/webui/frontend/src/components/TimeRangeSelector.css +21 -0
  19. jettask/webui/frontend/src/components/TimeRangeSelector.jsx +160 -0
  20. jettask/webui/frontend/src/components/charts/QueueChart.jsx +111 -0
  21. jettask/webui/frontend/src/components/charts/QueueTrendChart.jsx +115 -0
  22. jettask/webui/frontend/src/components/charts/WorkerChart.jsx +40 -0
  23. jettask/webui/frontend/src/components/common/StatsCard.jsx +18 -0
  24. jettask/webui/frontend/src/components/layout/AppLayout.css +95 -0
  25. jettask/webui/frontend/src/components/layout/AppLayout.jsx +49 -0
  26. jettask/webui/frontend/src/components/layout/Header.css +106 -0
  27. jettask/webui/frontend/src/components/layout/Header.jsx +106 -0
  28. jettask/webui/frontend/src/components/layout/SideMenu.css +137 -0
  29. jettask/webui/frontend/src/components/layout/SideMenu.jsx +209 -0
  30. jettask/webui/frontend/src/components/layout/TabsNav.css +244 -0
  31. jettask/webui/frontend/src/components/layout/TabsNav.jsx +206 -0
  32. jettask/webui/frontend/src/components/layout/UserInfo.css +197 -0
  33. jettask/webui/frontend/src/components/layout/UserInfo.jsx +197 -0
  34. jettask/webui/frontend/src/contexts/LoadingContext.jsx +27 -0
  35. jettask/webui/frontend/src/contexts/NamespaceContext.jsx +72 -0
  36. jettask/webui/frontend/src/contexts/TabsContext.backup.jsx +245 -0
  37. jettask/webui/frontend/src/index.css +114 -0
  38. jettask/webui/frontend/src/main.jsx +20 -0
  39. jettask/webui/frontend/src/pages/Alerts.jsx +684 -0
  40. jettask/webui/frontend/src/pages/Dashboard/index.css +35 -0
  41. jettask/webui/frontend/src/pages/Dashboard/index.jsx +281 -0
  42. jettask/webui/frontend/src/pages/Dashboard.jsx +1330 -0
  43. jettask/webui/frontend/src/pages/QueueDetail.jsx +1117 -0
  44. jettask/webui/frontend/src/pages/QueueMonitor.jsx +527 -0
  45. jettask/webui/frontend/src/pages/Queues.jsx +12 -0
  46. jettask/webui/frontend/src/pages/ScheduledTasks.jsx +809 -0
  47. jettask/webui/frontend/src/pages/Settings.jsx +800 -0
  48. jettask/webui/frontend/src/pages/Workers.jsx +12 -0
  49. jettask/webui/frontend/src/services/api.js +114 -0
  50. jettask/webui/frontend/src/services/queueTrend.js +152 -0
  51. jettask/webui/frontend/src/utils/suppressWarnings.js +22 -0
  52. jettask/webui/frontend/src/utils/userPreferences.js +154 -0
  53. jettask/webui/frontend/vite.config.js +26 -0
  54. {jettask-0.2.7.dist-info → jettask-0.2.9.dist-info}/METADATA +1 -1
  55. {jettask-0.2.7.dist-info → jettask-0.2.9.dist-info}/RECORD +59 -14
  56. jettask/webui/static/dist/assets/index-7129cfe1.css +0 -1
  57. jettask/webui/static/dist/assets/index-8d1935cc.js +0 -774
  58. jettask/webui/static/dist/index.html +0 -15
  59. jettask/webui/static/index.html +0 -1734
  60. jettask/webui/static/queue.html +0 -981
  61. jettask/webui/static/queues.html +0 -549
  62. jettask/webui/static/workers.html +0 -734
  63. {jettask-0.2.7.dist-info → jettask-0.2.9.dist-info}/WHEEL +0 -0
  64. {jettask-0.2.7.dist-info → jettask-0.2.9.dist-info}/entry_points.txt +0 -0
  65. {jettask-0.2.7.dist-info → jettask-0.2.9.dist-info}/licenses/LICENSE +0 -0
  66. {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;