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