jettask 0.2.6__py3-none-any.whl → 0.2.8__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 +152 -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.6.dist-info → jettask-0.2.8.dist-info}/METADATA +70 -2
  55. {jettask-0.2.6.dist-info → jettask-0.2.8.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.6.dist-info → jettask-0.2.8.dist-info}/WHEEL +0 -0
  64. {jettask-0.2.6.dist-info → jettask-0.2.8.dist-info}/entry_points.txt +0 -0
  65. {jettask-0.2.6.dist-info → jettask-0.2.8.dist-info}/licenses/LICENSE +0 -0
  66. {jettask-0.2.6.dist-info → jettask-0.2.8.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,487 @@
1
+ import { useState, useEffect, forwardRef, useImperativeHandle } from 'react';
2
+ import { Table, Tag, Tooltip, message, Button, Space, Popconfirm, Modal, InputNumber } from 'antd';
3
+ import { InfoCircleOutlined, ScissorOutlined, DeleteOutlined, EyeOutlined } from '@ant-design/icons';
4
+ import { useNavigate } from 'react-router-dom';
5
+ import axios from 'axios';
6
+ import dayjs from 'dayjs';
7
+ import { getPreference, setPreference, PREFERENCE_KEYS } from '../utils/userPreferences';
8
+ import { useNamespace } from '../contexts/NamespaceContext';
9
+ import './QueueDetailsTable.css';
10
+
11
+ const QueueDetailsTable = forwardRef(({
12
+ autoRefresh = false,
13
+ refreshInterval = 5000,
14
+ timeRange: parentTimeRange,
15
+ customTimeRange,
16
+ selectedQueues = []
17
+ }, ref) => {
18
+ const navigate = useNavigate();
19
+ const { currentNamespace } = useNamespace();
20
+ const [loading, setLoading] = useState(false);
21
+ const [data, setData] = useState([]);
22
+ const [trimModalVisible, setTrimModalVisible] = useState(false);
23
+ const [trimQueue, setTrimQueue] = useState(null);
24
+ const [trimCount, setTrimCount] = useState(100);
25
+
26
+ // 从本地存储恢复分页大小
27
+ const [pageSize, setPageSize] = useState(
28
+ getPreference(PREFERENCE_KEYS.QUEUE_DETAILS_PAGE_SIZE, 10)
29
+ );
30
+
31
+ // 计算实际的时间范围(分钟)
32
+ const calculateTimeRangeMinutes = () => {
33
+ if (customTimeRange && customTimeRange.length === 2) {
34
+ // 自定义时间范围,计算分钟差
35
+ const start = dayjs(customTimeRange[0]);
36
+ const end = dayjs(customTimeRange[1]);
37
+ const diffMinutes = Math.ceil(end.diff(start, 'minute'));
38
+ return Math.max(1, diffMinutes); // 至少1分钟
39
+ }
40
+
41
+ // 预设时间范围
42
+ const timeRangeMap = {
43
+ '15m': 15,
44
+ '30m': 30,
45
+ '1h': 60,
46
+ '3h': 180,
47
+ '6h': 360,
48
+ '12h': 720,
49
+ '24h': 1440,
50
+ '7d': 10080,
51
+ '30d': 43200,
52
+ };
53
+
54
+ return timeRangeMap[parentTimeRange] || 15; // 默认15分钟
55
+ };
56
+
57
+ const timeRangeMinutes = calculateTimeRangeMinutes();
58
+
59
+ // 暴露刷新方法给父组件
60
+ useImperativeHandle(ref, () => ({
61
+ refresh: fetchQueueDetails
62
+ }));
63
+
64
+ // 获取队列详细信息
65
+ const fetchQueueDetails = async () => {
66
+ if (!currentNamespace) {
67
+ return;
68
+ }
69
+
70
+ setLoading(true);
71
+ try {
72
+ // 构建与queue-timeline相同格式的请求参数
73
+ const params = {
74
+ namespace: currentNamespace,
75
+ time_range: parentTimeRange || '15m', // 添加默认值
76
+ queues: selectedQueues // 传递选中的队列
77
+ };
78
+
79
+ // 如果有自定义时间范围
80
+ if (customTimeRange && customTimeRange.length === 2) {
81
+ params.start_time = customTimeRange[0].toISOString();
82
+ params.end_time = customTimeRange[1].toISOString();
83
+ }
84
+
85
+ console.log('[QueueDetailsTable] 请求参数:', params, 'parentTimeRange:', parentTimeRange);
86
+
87
+ const response = await axios.post(`/api/data/queue-details/${currentNamespace}`, params);
88
+ if (response.data.success) {
89
+ setData(response.data.data);
90
+ } else {
91
+ message.error('获取队列详情失败');
92
+ }
93
+ } catch (error) {
94
+ console.error('Failed to fetch queue details:', error);
95
+ message.error('获取队列详情失败');
96
+ } finally {
97
+ setLoading(false);
98
+ }
99
+ };
100
+
101
+ // 初始化加载数据
102
+ useEffect(() => {
103
+ // 只有当有选中的队列时才加载数据
104
+ if (selectedQueues && selectedQueues.length > 0) {
105
+ fetchQueueDetails();
106
+ } else {
107
+ setData([]); // 没有选中队列时清空数据
108
+ }
109
+ }, [parentTimeRange, customTimeRange, selectedQueues]); // 当时间范围或选中队列变化时重新加载
110
+
111
+ // 自动刷新
112
+ useEffect(() => {
113
+ if (autoRefresh) {
114
+ const timer = setInterval(() => {
115
+ fetchQueueDetails();
116
+ }, refreshInterval);
117
+
118
+ return () => clearInterval(timer);
119
+ }
120
+ }, [autoRefresh, refreshInterval]);
121
+
122
+ // 格式化消费速度
123
+ const formatConsumptionRate = (rate) => {
124
+ if (rate === null || rate === undefined) return '-';
125
+ if (rate === 0) return '0 任务/分钟';
126
+ if (rate < 1) return `${(rate * 60).toFixed(1)} 任务/小时`;
127
+ return `${rate.toFixed(1)} 任务/分钟`;
128
+ };
129
+
130
+ // 处理分页变化
131
+ const handleTableChange = (pagination) => {
132
+ if (pagination.pageSize !== pageSize) {
133
+ setPageSize(pagination.pageSize);
134
+ // 保存到本地存储
135
+ setPreference(PREFERENCE_KEYS.QUEUE_DETAILS_PAGE_SIZE, pagination.pageSize);
136
+ }
137
+ };
138
+
139
+ // 处理删除队列
140
+ const handleDeleteQueue = async (queueName) => {
141
+ try {
142
+ const response = await axios.delete(`/api/queue/${queueName}`);
143
+ if (response.data.success) {
144
+ message.success(`队列 ${queueName} 已删除`);
145
+ fetchQueueDetails(); // 刷新数据
146
+ } else {
147
+ message.error(response.data.message || '删除失败');
148
+ }
149
+ } catch (error) {
150
+ console.error('Failed to delete queue:', error);
151
+ message.error('删除队列失败');
152
+ }
153
+ };
154
+
155
+ // 处理裁剪队列
156
+ const handleTrimQueue = async () => {
157
+ if (!trimQueue || !trimCount) return;
158
+
159
+ try {
160
+ const response = await axios.post(`/api/queue/${trimQueue}/trim`, {
161
+ max_length: trimCount
162
+ });
163
+ if (response.data.success) {
164
+ message.success(`队列 ${trimQueue} 已裁剪至 ${trimCount} 条消息`);
165
+ setTrimModalVisible(false);
166
+ fetchQueueDetails(); // 刷新数据
167
+ } else {
168
+ message.error(response.data.message || '裁剪失败');
169
+ }
170
+ } catch (error) {
171
+ console.error('Failed to trim queue:', error);
172
+ message.error('裁剪队列失败');
173
+ }
174
+ };
175
+
176
+ // 查看队列详情
177
+ const handleViewDetails = (queueName) => {
178
+ navigate(`/queue/${encodeURIComponent(queueName)}`);
179
+ };
180
+
181
+ // 显示裁剪模态框
182
+ const showTrimModal = (queueName) => {
183
+ setTrimQueue(queueName);
184
+ setTrimModalVisible(true);
185
+ };
186
+
187
+ // 表格列定义
188
+ const columns = [
189
+ {
190
+ title: '队列名称',
191
+ dataIndex: 'queue_name',
192
+ key: 'queue_name',
193
+ fixed: 'left',
194
+ width: 150,
195
+ render: (text) => <strong>{text}</strong>,
196
+ },
197
+ {
198
+ title: '消息数量',
199
+ dataIndex: 'message_count',
200
+ key: 'message_count',
201
+ width: 100,
202
+ align: 'right',
203
+ sorter: (a, b) => a.message_count - b.message_count,
204
+ render: (value) => value?.toLocaleString() || '0',
205
+ },
206
+ {
207
+ title: '可见消息',
208
+ dataIndex: 'visible_messages',
209
+ key: 'visible_messages',
210
+ width: 100,
211
+ align: 'right',
212
+ sorter: (a, b) => a.visible_messages - b.visible_messages,
213
+ render: (value) => {
214
+ if (value > 0) {
215
+ return <Tag color="orange">{value.toLocaleString()}</Tag>;
216
+ }
217
+ return <span style={{ color: '#999' }}>0</span>;
218
+ },
219
+ },
220
+ {
221
+ title: '不可见消息',
222
+ dataIndex: 'invisible_messages',
223
+ key: 'invisible_messages',
224
+ width: 120,
225
+ align: 'right',
226
+ sorter: (a, b) => a.invisible_messages - b.invisible_messages,
227
+ // render: (value) => {
228
+ // if (value > 0) {
229
+ // return <Tag color="blue">{value.toLocaleString()}</Tag>;
230
+ // }
231
+ // return <span style={{ color: '#999' }}>0</span>;
232
+ // },
233
+ },
234
+ {
235
+ title: '成功',
236
+ dataIndex: 'completed',
237
+ key: 'completed',
238
+ width: 100,
239
+ align: 'right',
240
+ sorter: (a, b) => a.completed - b.completed,
241
+ render: (value) => {
242
+ if (value > 0) {
243
+ return <Tag color="green">{value.toLocaleString()}</Tag>;
244
+ }
245
+ return <span style={{ color: '#999' }}>0</span>;
246
+ },
247
+ },
248
+ {
249
+ title: '失败',
250
+ dataIndex: 'failed',
251
+ key: 'failed',
252
+ width: 100,
253
+ align: 'right',
254
+ sorter: (a, b) => a.failed - b.failed,
255
+ render: (value) => {
256
+ if (value > 0) {
257
+ return <Tag color="red">{value.toLocaleString()}</Tag>;
258
+ }
259
+ return <span style={{ color: '#999' }}>0</span>;
260
+ },
261
+ },
262
+ {
263
+ title: (
264
+ <Tooltip title={`基于最近${timeRangeMinutes}分钟的平均处理速度`}>
265
+ <span>
266
+ 消费速度 <InfoCircleOutlined />
267
+ </span>
268
+ </Tooltip>
269
+ ),
270
+ dataIndex: 'consumption_rate',
271
+ key: 'consumption_rate',
272
+ width: 150,
273
+ align: 'right',
274
+ sorter: (a, b) => (a.consumption_rate || 0) - (b.consumption_rate || 0),
275
+ render: (value) => {
276
+ const formatted = formatConsumptionRate(value);
277
+ if (value > 10) {
278
+ return <span style={{ color: '#52c41a', fontWeight: 'bold' }}>{formatted}</span>;
279
+ } else if (value > 0) {
280
+ return <span style={{ color: '#1890ff' }}>{formatted}</span>;
281
+ }
282
+ return <span style={{ color: '#999' }}>{formatted}</span>;
283
+ },
284
+ },
285
+ {
286
+ title: '在线Workers',
287
+ dataIndex: 'active_workers',
288
+ key: 'active_workers',
289
+ width: 120,
290
+ align: 'center',
291
+ sorter: (a, b) => a.active_workers - b.active_workers,
292
+ render: (value) => {
293
+ if (value > 0) {
294
+ return (
295
+ <Tag color="green">
296
+ <span style={{ fontSize: '14px' }}>👥 {value}</span>
297
+ </Tag>
298
+ );
299
+ }
300
+ return <Tag color="default">无</Tag>;
301
+ },
302
+ },
303
+ {
304
+ title: (
305
+ <Tooltip title={`基于最近${timeRangeMinutes}分钟的成功率`}>
306
+ <span>
307
+ 成功率 <InfoCircleOutlined />
308
+ </span>
309
+ </Tooltip>
310
+ ),
311
+ dataIndex: 'success_rate',
312
+ key: 'success_rate',
313
+ width: 100,
314
+ align: 'center',
315
+ sorter: (a, b) => (a.success_rate || 0) - (b.success_rate || 0),
316
+ render: (value) => {
317
+ if (!value && value !== 0) return <span style={{ color: '#999' }}>-</span>;
318
+
319
+ let color = '#52c41a';
320
+ if (value < 50) color = '#ff4d4f';
321
+ else if (value < 90) color = '#faad14';
322
+
323
+ return (
324
+ <span style={{ color, fontWeight: 'bold' }}>
325
+ {value.toFixed(1)}%
326
+ </span>
327
+ );
328
+ },
329
+ },
330
+ {
331
+ title: '队列状态',
332
+ dataIndex: 'queue_status',
333
+ key: 'queue_status',
334
+ width: 100,
335
+ align: 'center',
336
+ render: (value) => {
337
+ const statusConfig = {
338
+ 'active': { color: 'green', label: '活跃' },
339
+ 'idle': { color: 'blue', label: '空闲' },
340
+ 'unknown': { color: 'default', label: '未知' }
341
+ };
342
+ const config = statusConfig[value] || statusConfig['unknown'];
343
+ return <Tag color={config.color}>{config.label}</Tag>;
344
+ },
345
+ },
346
+ {
347
+ title: '操作',
348
+ key: 'action',
349
+ width: 200,
350
+ align: 'center',
351
+ fixed: 'right',
352
+ render: (_, record) => (
353
+ <Space size="small">
354
+ <Tooltip title="查看详情">
355
+ <Button
356
+ type="link"
357
+ size="small"
358
+ icon={<EyeOutlined />}
359
+ onClick={() => handleViewDetails(record.queue_name)}
360
+ />
361
+ </Tooltip>
362
+ <Tooltip title="裁剪消息">
363
+ <Button
364
+ type="link"
365
+ size="small"
366
+ icon={<ScissorOutlined />}
367
+ onClick={() => showTrimModal(record.queue_name)}
368
+ disabled={record.message_count === 0}
369
+ />
370
+ </Tooltip>
371
+ <Popconfirm
372
+ title="确定要删除这个队列吗?"
373
+ description="此操作不可恢复"
374
+ onConfirm={() => handleDeleteQueue(record.queue_name)}
375
+ okText="确定"
376
+ cancelText="取消"
377
+ >
378
+ <Tooltip title="删除队列">
379
+ <Button
380
+ type="link"
381
+ size="small"
382
+ danger
383
+ icon={<DeleteOutlined />}
384
+ />
385
+ </Tooltip>
386
+ </Popconfirm>
387
+ </Space>
388
+ ),
389
+ },
390
+ ];
391
+
392
+ return (
393
+ <>
394
+ <div className="compact-table">
395
+ <Table
396
+ columns={columns}
397
+ dataSource={data}
398
+ rowKey="queue_name"
399
+ loading={loading}
400
+ onChange={handleTableChange}
401
+ pagination={{
402
+ pageSize: pageSize,
403
+ pageSizeOptions: ['5', '6', '7', '8', '10', '20', '50', '100'],
404
+ showTotal: (total) => `共 ${total} 个队列`,
405
+ showSizeChanger: true,
406
+ showQuickJumper: true,
407
+ }}
408
+ scroll={{ x: 1300 }}
409
+ size="small"
410
+ summary={(pageData) => {
411
+ const totals = pageData.reduce(
412
+ (acc, row) => ({
413
+ message_count: acc.message_count + (row.message_count || 0),
414
+ visible: acc.visible + (row.visible_messages || 0),
415
+ invisible: acc.invisible + (row.invisible_messages || 0),
416
+ completed: acc.completed + (row.completed || 0),
417
+ failed: acc.failed + (row.failed || 0),
418
+ workers: acc.workers + (row.active_workers || 0),
419
+ }),
420
+ { message_count: 0, visible: 0, invisible: 0, completed: 0, failed: 0, workers: 0 }
421
+ );
422
+
423
+ return (
424
+ <Table.Summary.Row style={{ background: '#fafafa', fontWeight: 'bold' }}>
425
+ <Table.Summary.Cell index={0}>总计</Table.Summary.Cell>
426
+ <Table.Summary.Cell index={1} align="right">
427
+ {totals.message_count.toLocaleString()}
428
+ </Table.Summary.Cell>
429
+ <Table.Summary.Cell index={2} align="right">
430
+ {totals.visible.toLocaleString()}
431
+ </Table.Summary.Cell>
432
+ <Table.Summary.Cell index={3} align="right">
433
+ {totals.invisible.toLocaleString()}
434
+ </Table.Summary.Cell>
435
+ <Table.Summary.Cell index={4} align="right">
436
+ {totals.completed.toLocaleString()}
437
+ </Table.Summary.Cell>
438
+ <Table.Summary.Cell index={5} align="right">
439
+ {totals.failed.toLocaleString()}
440
+ </Table.Summary.Cell>
441
+ <Table.Summary.Cell index={6} align="right">-</Table.Summary.Cell>
442
+ <Table.Summary.Cell index={7} align="center">
443
+ {totals.workers}
444
+ </Table.Summary.Cell>
445
+ <Table.Summary.Cell index={8} align="center">
446
+ {(totals.completed + totals.failed) > 0
447
+ ? `${((totals.completed / (totals.completed + totals.failed)) * 100).toFixed(1)}%`
448
+ : '-'}
449
+ </Table.Summary.Cell>
450
+ <Table.Summary.Cell index={9} align="center">-</Table.Summary.Cell>
451
+ </Table.Summary.Row>
452
+ );
453
+ }}
454
+ rowHoverable
455
+ />
456
+ </div>
457
+
458
+ {/* 裁剪队列模态框 */}
459
+ <Modal
460
+ title={`裁剪队列: ${trimQueue}`}
461
+ open={trimModalVisible}
462
+ onOk={handleTrimQueue}
463
+ onCancel={() => setTrimModalVisible(false)}
464
+ okText="确定"
465
+ cancelText="取消"
466
+ >
467
+ <div style={{ marginBottom: 16 }}>
468
+ <span>保留最新的消息数量:</span>
469
+ <InputNumber
470
+ min={0}
471
+ max={10000}
472
+ value={trimCount}
473
+ onChange={setTrimCount}
474
+ style={{ width: 150, marginLeft: 8 }}
475
+ />
476
+ </div>
477
+ <div style={{ color: '#ff4d4f' }}>
478
+ 注意:裁剪操作不可恢复,将永久删除旧消息
479
+ </div>
480
+ </Modal>
481
+ </>
482
+ );
483
+ });
484
+
485
+ QueueDetailsTable.displayName = 'QueueDetailsTable';
486
+
487
+ export default QueueDetailsTable;