jettask 0.2.7__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.7.dist-info → jettask-0.2.8.dist-info}/METADATA +1 -1
  55. {jettask-0.2.7.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.7.dist-info → jettask-0.2.8.dist-info}/WHEEL +0 -0
  64. {jettask-0.2.7.dist-info → jettask-0.2.8.dist-info}/entry_points.txt +0 -0
  65. {jettask-0.2.7.dist-info → jettask-0.2.8.dist-info}/licenses/LICENSE +0 -0
  66. {jettask-0.2.7.dist-info → jettask-0.2.8.dist-info}/top_level.txt +0 -0
@@ -1,981 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="zh-CN">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>队列详情 - Jettask Monitor</title>
7
-
8
- <!-- React -->
9
- <script crossorigin src="https://unpkg.com/react@17/umd/react.production.min.js"></script>
10
- <script crossorigin src="https://unpkg.com/react-dom@17/umd/react-dom.production.min.js"></script>
11
-
12
- <!-- Babel for JSX -->
13
- <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
14
-
15
- <!-- Ant Design -->
16
- <link rel="stylesheet" href="https://unpkg.com/antd@4.24.13/dist/antd.min.css" />
17
- <script src="https://unpkg.com/moment@2.29.4/moment.js"></script>
18
- <script src="https://unpkg.com/antd@4.24.13/dist/antd.min.js"></script>
19
-
20
- <!-- Chart.js -->
21
- <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
22
-
23
- <!-- Icons -->
24
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
25
-
26
- <style>
27
- * {
28
- margin: 0;
29
- padding: 0;
30
- box-sizing: border-box;
31
- }
32
-
33
- :root {
34
- --primary-color: #177ddc;
35
- --primary-gradient: linear-gradient(135deg, #1f1f1f 0%, #434343 100%);
36
- --success-color: #49aa19;
37
- --warning-color: #d89614;
38
- --error-color: #a61d24;
39
- --info-color: #177ddc;
40
- --bg-primary: #000000;
41
- --bg-secondary: #141414;
42
- --bg-card: #1f1f1f;
43
- --text-primary: #rgba(255, 255, 255, 0.85);
44
- --text-secondary: rgba(255, 255, 255, 0.65);
45
- --border-color: rgba(255, 255, 255, 0.12);
46
- }
47
-
48
- body {
49
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
50
- background: #000000;
51
- min-height: 100vh;
52
- position: relative;
53
- color: rgba(255, 255, 255, 0.85);
54
- }
55
-
56
- body::before {
57
- content: '';
58
- position: fixed;
59
- top: 0;
60
- left: 0;
61
- right: 0;
62
- bottom: 0;
63
- background:
64
- radial-gradient(circle at 20% 50%, rgba(23, 125, 220, 0.1) 0%, transparent 50%),
65
- radial-gradient(circle at 80% 20%, rgba(73, 170, 25, 0.1) 0%, transparent 50%),
66
- radial-gradient(circle at 40% 80%, rgba(216, 150, 20, 0.1) 0%, transparent 50%);
67
- pointer-events: none;
68
- z-index: 0;
69
- }
70
-
71
- #root {
72
- position: relative;
73
- z-index: 1;
74
- }
75
-
76
- .app-container {
77
- min-height: 100vh;
78
- backdrop-filter: blur(10px);
79
- background: rgba(0, 0, 0, 0.5);
80
- }
81
-
82
-
83
- .main-content {
84
- padding: 30px;
85
- max-width: 1400px;
86
- margin: 0 auto;
87
- }
88
-
89
- .gradient-text {
90
- background: linear-gradient(135deg, #177ddc 0%, #69c0ff 100%);
91
- -webkit-background-clip: text;
92
- -webkit-text-fill-color: transparent;
93
- background-clip: text;
94
- }
95
-
96
- /* Ant Design 暗色主题覆盖 */
97
- .ant-card {
98
- background: rgba(31, 31, 31, 0.95) !important;
99
- border-color: rgba(255, 255, 255, 0.08) !important;
100
- }
101
-
102
- .ant-statistic-title {
103
- color: rgba(255, 255, 255, 0.65) !important;
104
- }
105
-
106
- .ant-table {
107
- background: transparent !important;
108
- }
109
-
110
- .ant-table-thead > tr > th {
111
- background: rgba(255, 255, 255, 0.04) !important;
112
- border-bottom-color: rgba(255, 255, 255, 0.08) !important;
113
- }
114
-
115
- .ant-table-tbody > tr > td {
116
- border-bottom-color: rgba(255, 255, 255, 0.08) !important;
117
- }
118
-
119
- .ant-table-tbody > tr:hover > td {
120
- background: rgba(255, 255, 255, 0.04) !important;
121
- }
122
-
123
- .ant-modal-content {
124
- background: rgba(31, 31, 31, 0.95) !important;
125
- }
126
-
127
- .ant-modal-header {
128
- background: transparent !important;
129
- border-bottom-color: rgba(255, 255, 255, 0.08) !important;
130
- }
131
-
132
- .ant-modal-title {
133
- color: rgba(255, 255, 255, 0.85) !important;
134
- }
135
-
136
- /* 修复导航按钮样式 */
137
- .ant-btn-text {
138
- color: rgba(255, 255, 255, 0.65) !important;
139
- }
140
-
141
- .ant-btn-text:hover {
142
- color: rgba(255, 255, 255, 0.85) !important;
143
- background: rgba(255, 255, 255, 0.08) !important;
144
- }
145
-
146
- .ant-btn-primary {
147
- background: #177ddc !important;
148
- border-color: #177ddc !important;
149
- }
150
-
151
- .ant-btn-primary:hover {
152
- background: #1890ff !important;
153
- border-color: #1890ff !important;
154
- }
155
-
156
- /* 分页器样式 */
157
- .ant-pagination-item {
158
- background: rgba(255, 255, 255, 0.08) !important;
159
- border-color: rgba(255, 255, 255, 0.12) !important;
160
- }
161
-
162
- .ant-pagination-item a {
163
- color: rgba(255, 255, 255, 0.85) !important;
164
- }
165
-
166
- .ant-pagination-item-active {
167
- background: #177ddc !important;
168
- border-color: #177ddc !important;
169
- }
170
-
171
- /* Input输入框样式 */
172
- .ant-input {
173
- background: rgba(255, 255, 255, 0.08) !important;
174
- border-color: rgba(255, 255, 255, 0.12) !important;
175
- color: rgba(255, 255, 255, 0.85) !important;
176
- }
177
-
178
- .ant-input::placeholder {
179
- color: rgba(255, 255, 255, 0.45) !important;
180
- }
181
-
182
- /* Select下拉框样式 */
183
- .ant-select-selector {
184
- background: rgba(255, 255, 255, 0.08) !important;
185
- border-color: rgba(255, 255, 255, 0.12) !important;
186
- color: rgba(255, 255, 255, 0.85) !important;
187
- }
188
-
189
- /* 响应式设计 */
190
- @media (max-width: 1024px) {
191
- .main-content {
192
- padding: 16px;
193
- }
194
- }
195
-
196
- /* 自定义滚动条 */
197
- ::-webkit-scrollbar {
198
- width: 6px;
199
- }
200
-
201
- ::-webkit-scrollbar-track {
202
- background: rgba(255, 255, 255, 0.1);
203
- border-radius: 3px;
204
- }
205
-
206
- ::-webkit-scrollbar-thumb {
207
- background: rgba(255, 255, 255, 0.3);
208
- border-radius: 3px;
209
- }
210
-
211
- ::-webkit-scrollbar-thumb:hover {
212
- background: rgba(255, 255, 255, 0.5);
213
- }
214
-
215
- </style>
216
- </head>
217
- <body>
218
- <div id="root"></div>
219
-
220
- <script type="text/babel">
221
- const { useState, useEffect, useMemo, useRef } = React;
222
- const { Card, Row, Col, Statistic, Table, Input, Select, Button, Tag, Modal, Descriptions, Space, Divider, Pagination, Spin, message, BackTop, ConfigProvider } = antd;
223
- // Icons (using span with text as fallback)
224
- const SearchIcon = () => React.createElement('span', null, '🔍');
225
- const ReloadIcon = () => React.createElement('span', null, '🔄');
226
- const ArrowLeftIcon = () => React.createElement('span', null, '←');
227
- const MonitorIcon = () => React.createElement('span', null, '📺');
228
- const LineChartIcon = () => React.createElement('span', null, '📈');
229
-
230
- function QueueDetail() {
231
- // 从URL获取队列名称
232
- const urlParams = new URLSearchParams(window.location.search);
233
- const queueName = urlParams.get('name') || '';
234
-
235
- // 状态管理
236
- const [loading, setLoading] = useState(false);
237
- const [taskSearch, setTaskSearch] = useState('');
238
- const [statusFilter, setStatusFilter] = useState('');
239
- const [timeRange, setTimeRange] = useState('1h');
240
- const [showAllStats, setShowAllStats] = useState(true);
241
- const [currentPage, setCurrentPage] = useState(1);
242
- const [pageSize, setPageSize] = useState(50);
243
- const [totalTasks, setTotalTasks] = useState(0);
244
- const [taskDetailVisible, setTaskDetailVisible] = useState(false);
245
- const [selectedTask, setSelectedTask] = useState(null);
246
-
247
- // 数据状态
248
- const [queueStats, setQueueStats] = useState({
249
- messages: 0,
250
- messages_ready: 0,
251
- messages_unacknowledged: 0,
252
- consumers: 0,
253
- length: 0
254
- });
255
-
256
- const [messageStats, setMessageStats] = useState({
257
- publish: 0,
258
- deliver_get: 0,
259
- ack: 0
260
- });
261
-
262
- const [workerSummary, setWorkerSummary] = useState({
263
- total_success_count: 0,
264
- total_failed_count: 0,
265
- total_running_tasks: 0,
266
- avg_processing_time: 0
267
- });
268
-
269
- const [workers, setWorkers] = useState([]);
270
- const [tasks, setTasks] = useState([]);
271
-
272
- const chartRef = useRef(null);
273
- const trendChart = useRef(null);
274
-
275
- // 计算属性
276
- const onlineWorkers = useMemo(() =>
277
- workers.filter(w => w.is_alive).length, [workers]
278
- );
279
-
280
- const filteredTasks = useMemo(() => {
281
- let filtered = tasks;
282
-
283
- if (taskSearch) {
284
- filtered = filtered.filter(task =>
285
- (task.task_name || '').toLowerCase().includes(taskSearch.toLowerCase()) ||
286
- (task.event_id || '').toLowerCase().includes(taskSearch.toLowerCase())
287
- );
288
- }
289
-
290
- if (statusFilter) {
291
- filtered = filtered.filter(task => {
292
- const status = task.parsed_status?.status || '';
293
- return status.includes(statusFilter);
294
- });
295
- }
296
-
297
- const start = (currentPage - 1) * pageSize;
298
- const end = start + pageSize;
299
- setTotalTasks(filtered.length);
300
- return filtered.slice(start, end);
301
- }, [tasks, taskSearch, statusFilter, currentPage, pageSize]);
302
-
303
- // 辅助函数
304
- const formatTime = (seconds) => {
305
- if (!seconds || seconds === 0) return '-';
306
- if (seconds < 1) return `${Math.round(seconds * 1000)}ms`;
307
- if (seconds < 60) return `${seconds.toFixed(2)}s`;
308
- const minutes = Math.floor(seconds / 60);
309
- const remainingSeconds = (seconds % 60).toFixed(1);
310
- return `${minutes}m${remainingSeconds}s`;
311
- };
312
-
313
- const formatDateTime = (timestamp) => {
314
- if (!timestamp) return '-';
315
- return new Date(parseFloat(timestamp) * 1000).toLocaleString('zh-CN');
316
- };
317
-
318
- const getStatusType = (status) => {
319
- if (!status) return 'default';
320
- if (status.includes('completed') || status.includes('成功')) return 'success';
321
- if (status.includes('failed') || status.includes('失败')) return 'error';
322
- if (status.includes('running') || status.includes('执行')) return 'warning';
323
- return 'default';
324
- };
325
-
326
- const getStatusText = (status) => {
327
- if (!status) return '未知';
328
- if (status.includes('completed') || status.includes('成功')) return '已完成';
329
- if (status.includes('failed') || status.includes('失败')) return '失败';
330
- if (status.includes('running') || status.includes('执行')) return '执行中';
331
- if (status.includes('pending') || status.includes('等待')) return '等待中';
332
- return status;
333
- };
334
-
335
- const goBack = () => {
336
- window.location.href = '/';
337
- };
338
-
339
- const viewTaskDetail = (task) => {
340
- setSelectedTask(task);
341
- setTaskDetailVisible(true);
342
- };
343
-
344
- // 数据加载函数
345
- const loadQueueStats = async () => {
346
- try {
347
- const response = await fetch(`/api/queue/${encodeURIComponent(queueName)}/stats`);
348
- if (response.ok) {
349
- const stats = await response.json();
350
- setQueueStats(stats);
351
- setMessageStats(stats.message_stats || {});
352
- }
353
- } catch (error) {
354
- console.error('加载队列统计失败:', error);
355
- }
356
- };
357
-
358
- const loadWorkers = async () => {
359
- try {
360
- const [workersResponse, summaryResponse] = await Promise.all([
361
- fetch(`/api/queue/${encodeURIComponent(queueName)}/workers`),
362
- fetch(`/api/queue/${encodeURIComponent(queueName)}/worker-summary`)
363
- ]);
364
-
365
- if (workersResponse.ok) {
366
- const data = await workersResponse.json();
367
- setWorkers(data.workers || []);
368
- }
369
-
370
- if (summaryResponse.ok) {
371
- const summaryData = await summaryResponse.json();
372
- setWorkerSummary(summaryData.summary || {});
373
- }
374
- } catch (error) {
375
- console.error('加载Workers失败:', error);
376
- }
377
- };
378
-
379
- const loadTasks = async () => {
380
- try {
381
- setLoading(true);
382
- const response = await fetch(`/api/queue/${encodeURIComponent(queueName)}/tasks?limit=1000`);
383
- if (response.ok) {
384
- const data = await response.json();
385
- setTasks(data.tasks || []);
386
- }
387
- } catch (error) {
388
- console.error('加载任务失败:', error);
389
- message.error('加载任务失败');
390
- } finally {
391
- setLoading(false);
392
- }
393
- };
394
-
395
- const refreshData = async () => {
396
- await Promise.all([
397
- loadQueueStats(),
398
- loadWorkers(),
399
- loadTasks()
400
- ]);
401
- };
402
-
403
- const initChart = () => {
404
- if (chartRef.current && !trendChart.current) {
405
- const ctx = chartRef.current.getContext('2d');
406
- trendChart.current = new Chart(ctx, {
407
- type: 'line',
408
- data: {
409
- labels: Array.from({length: 12}, (_, i) => `${i * 2}:00`),
410
- datasets: [
411
- {
412
- label: '成功任务',
413
- data: Array.from({length: 12}, () => Math.floor(Math.random() * 50)),
414
- borderColor: '#52c41a',
415
- backgroundColor: 'rgba(82, 196, 26, 0.1)',
416
- fill: true,
417
- tension: 0.4
418
- },
419
- {
420
- label: '失败任务',
421
- data: Array.from({length: 12}, () => Math.floor(Math.random() * 10)),
422
- borderColor: '#f5222d',
423
- backgroundColor: 'rgba(245, 34, 45, 0.1)',
424
- fill: true,
425
- tension: 0.4
426
- }
427
- ]
428
- },
429
- options: {
430
- responsive: true,
431
- maintainAspectRatio: false,
432
- plugins: {
433
- legend: {
434
- position: 'top',
435
- labels: {
436
- color: 'rgba(255, 255, 255, 0.85)'
437
- }
438
- }
439
- },
440
- scales: {
441
- x: {
442
- grid: {
443
- color: 'rgba(255, 255, 255, 0.08)'
444
- },
445
- ticks: {
446
- color: 'rgba(255, 255, 255, 0.65)'
447
- }
448
- },
449
- y: {
450
- beginAtZero: true,
451
- grid: {
452
- color: 'rgba(255, 255, 255, 0.08)'
453
- },
454
- ticks: {
455
- color: 'rgba(255, 255, 255, 0.65)'
456
- }
457
- }
458
- }
459
- }
460
- });
461
- }
462
- };
463
-
464
- // 生命周期
465
- useEffect(() => {
466
- if (!queueName) {
467
- message.error('队列名称不能为空');
468
- return;
469
- }
470
-
471
- refreshData();
472
- setTimeout(initChart, 100);
473
-
474
- return () => {
475
- if (trendChart.current) {
476
- trendChart.current.destroy();
477
- }
478
- };
479
- }, []);
480
-
481
- // 表格列定义
482
- const columns = [
483
- {
484
- title: '任务ID',
485
- dataIndex: 'event_id',
486
- key: 'event_id',
487
- width: 200,
488
- render: (text) => (
489
- <code style={{fontSize: '0.75rem', background: '#f7fafc', padding: '4px 8px', borderRadius: '4px'}}>
490
- {text ? text.substring(0, 16) + '...' : 'N/A'}
491
- </code>
492
- ),
493
- },
494
- {
495
- title: '任务名称',
496
- dataIndex: 'task_name',
497
- key: 'task_name',
498
- minWidth: 120,
499
- render: (text) => (
500
- <div style={{display: 'flex', alignItems: 'center'}}>
501
- <Tag color="blue" size="small" style={{marginRight: '8px'}}>T</Tag>
502
- <strong>{text || 'unknown'}</strong>
503
- </div>
504
- ),
505
- },
506
- {
507
- title: '消费者',
508
- dataIndex: 'consumer',
509
- key: 'consumer',
510
- width: 150,
511
- render: (text) => <code style={{fontSize: '0.7rem'}}>{text || '-'}</code>,
512
- },
513
- {
514
- title: '状态',
515
- dataIndex: 'parsed_status',
516
- key: 'status',
517
- width: 100,
518
- align: 'center',
519
- render: (parsed_status) => (
520
- <Tag color={getStatusType(parsed_status?.status)} size="small">
521
- {getStatusText(parsed_status?.status)}
522
- </Tag>
523
- ),
524
- },
525
- {
526
- title: '创建时间',
527
- dataIndex: 'trigger_time',
528
- key: 'trigger_time',
529
- width: 160,
530
- render: (text) => formatDateTime(text),
531
- },
532
- {
533
- title: '参数',
534
- dataIndex: 'params_str',
535
- key: 'params_str',
536
- minWidth: 150,
537
- render: (text) => {
538
- const content = text || '无参数';
539
- return (
540
- <span style={{fontSize: '0.8rem', color: '#718096'}} title={content}>
541
- {content.length > 20 ? content.substring(0, 20) + '...' : content}
542
- </span>
543
- );
544
- },
545
- },
546
- {
547
- title: '操作',
548
- key: 'action',
549
- width: 120,
550
- align: 'center',
551
- render: (_, record) => (
552
- <Button
553
- type="primary"
554
- size="small"
555
- onClick={() => viewTaskDetail(record)}
556
- style={{borderRadius: '6px'}}
557
- >
558
- 详情
559
- </Button>
560
- ),
561
- },
562
- ];
563
-
564
- return (
565
- <ConfigProvider>
566
- <div className="app-container">
567
- {/* 导航栏 */}
568
- <div style={{
569
- background: 'rgba(20, 20, 20, 0.8)',
570
- backdropFilter: 'blur(20px)',
571
- borderBottom: '1px solid rgba(255, 255, 255, 0.08)',
572
- padding: '16px 0',
573
- position: 'sticky',
574
- top: 0,
575
- zIndex: 1000
576
- }}>
577
- <div style={{
578
- maxWidth: 1400,
579
- margin: '0 auto',
580
- padding: '0 30px',
581
- display: 'flex',
582
- alignItems: 'center',
583
- justifyContent: 'space-between'
584
- }}>
585
- <div style={{
586
- color: 'white',
587
- fontSize: '1.5rem',
588
- fontWeight: 700,
589
- display: 'flex',
590
- alignItems: 'center',
591
- gap: 12
592
- }}>
593
- <i className="fas fa-rocket" style={{fontSize: '1.5rem'}} />
594
- Jettask Monitor
595
- </div>
596
- <Space size="large">
597
- <Button type="text" size="large" onClick={() => window.location.href = '/'}>
598
- 概览
599
- </Button>
600
- <Button type="primary" size="large">
601
- 队列
602
- </Button>
603
- <Button type="text" size="large" onClick={() => window.location.href = '/workers.html'}>
604
- Workers
605
- </Button>
606
- </Space>
607
- </div>
608
- </div>
609
-
610
- {/* 队列标题 */}
611
- <div style={{
612
- background: 'rgba(20, 20, 20, 0.5)',
613
- padding: '20px 0',
614
- textAlign: 'center'
615
- }}>
616
- <h1 style={{
617
- color: 'white',
618
- fontSize: '2rem',
619
- fontWeight: 700,
620
- margin: 0
621
- }}>
622
- 📊 队列详情 - {queueName}
623
- </h1>
624
- <div style={{
625
- color: 'rgba(255, 255, 255, 0.65)',
626
- marginTop: 8,
627
- fontSize: '0.9rem'
628
- }}>
629
- 实时监控队列状态和任务执行情况
630
- </div>
631
- </div>
632
-
633
- {/* 主内容区域 */}
634
- <div className="main-content">
635
- <Row gutter={[24, 24]}>
636
- {/* 左侧统计面板 */}
637
- <Col xs={24} lg={8}>
638
- <Space direction="vertical" size="large" style={{width: '100%'}}>
639
- {/* 队列概览 */}
640
- <Card
641
- title={
642
- <span style={{background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)', WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent', fontSize: '1.2rem', fontWeight: 700}}>
643
- 📊 队列概览
644
- </span>
645
- }
646
- style={{
647
- background: 'rgba(255, 255, 255, 0.95)',
648
- backdropFilter: 'blur(20px)',
649
- borderRadius: '16px',
650
- border: '1px solid rgba(255, 255, 255, 0.3)'
651
- }}
652
- >
653
- <Row gutter={[12, 12]}>
654
- <Col span={12}>
655
- <Statistic title="队列名称" value={queueName} />
656
- </Col>
657
- <Col span={12}>
658
- <Statistic
659
- title="消息总数"
660
- value={queueStats.messages || 0}
661
- valueStyle={{color: '#1890ff'}}
662
- />
663
- </Col>
664
- <Col span={12}>
665
- <Statistic
666
- title="就绪消息"
667
- value={queueStats.messages_ready || 0}
668
- valueStyle={{color: '#52c41a'}}
669
- />
670
- </Col>
671
- <Col span={12}>
672
- <Statistic
673
- title="未确认消息"
674
- value={queueStats.messages_unacknowledged || 0}
675
- valueStyle={{color: '#faad14'}}
676
- />
677
- </Col>
678
- <Col span={12}>
679
- <Statistic
680
- title="消费者数量"
681
- value={queueStats.consumers || 0}
682
- valueStyle={{color: '#722ed1'}}
683
- />
684
- </Col>
685
- </Row>
686
-
687
- <Divider>消息统计</Divider>
688
- <Row gutter={[12, 12]}>
689
- <Col span={8}>
690
- <Statistic
691
- title="发布数"
692
- value={messageStats.publish || 0}
693
- valueStyle={{color: '#9b59b6'}}
694
- />
695
- </Col>
696
- <Col span={8}>
697
- <Statistic
698
- title="消费数"
699
- value={messageStats.deliver_get || 0}
700
- valueStyle={{color: '#3498db'}}
701
- />
702
- </Col>
703
- <Col span={8}>
704
- <Statistic
705
- title="确认数"
706
- value={messageStats.ack || 0}
707
- valueStyle={{color: '#27ae60'}}
708
- />
709
- </Col>
710
- </Row>
711
-
712
- <Divider>任务统计</Divider>
713
- <Row gutter={[12, 12]}>
714
- <Col span={12}>
715
- <Statistic
716
- title="成功数"
717
- value={workerSummary.total_success_count || 0}
718
- valueStyle={{color: '#52c41a'}}
719
- />
720
- </Col>
721
- <Col span={12}>
722
- <Statistic
723
- title="失败数"
724
- value={workerSummary.total_failed_count || 0}
725
- valueStyle={{color: '#f5222d'}}
726
- />
727
- </Col>
728
- <Col span={12}>
729
- <Statistic
730
- title="执行中"
731
- value={workerSummary.total_running_tasks || 0}
732
- valueStyle={{color: '#faad14'}}
733
- />
734
- </Col>
735
- <Col span={12}>
736
- <Statistic
737
- title="平均耗时"
738
- value={formatTime(workerSummary.avg_processing_time)}
739
- />
740
- </Col>
741
- </Row>
742
- </Card>
743
-
744
- {/* Workers信息 */}
745
- <Card
746
- title={
747
- <span style={{background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)', WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent', fontSize: '1.2rem', fontWeight: 700}}>
748
- 👥 Workers ({onlineWorkers}/{workers.length})
749
- </span>
750
- }
751
- style={{
752
- background: 'rgba(255, 255, 255, 0.95)',
753
- backdropFilter: 'blur(20px)',
754
- borderRadius: '16px',
755
- border: '1px solid rgba(255, 255, 255, 0.3)'
756
- }}
757
- >
758
- {workers.length === 0 ? (
759
- <div style={{textAlign: 'center', color: '#999', padding: '20px'}}>
760
- 暂无Worker
761
- </div>
762
- ) : (
763
- <Space direction="vertical" size="small" style={{width: '100%'}}>
764
- {workers.slice(0, 5).map(worker => (
765
- <div
766
- key={worker.consumer_id}
767
- style={{
768
- padding: '12px',
769
- background: 'rgba(255, 255, 255, 0.5)',
770
- borderRadius: '8px',
771
- border: '1px solid rgba(255, 255, 255, 0.3)'
772
- }}
773
- >
774
- <div style={{display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '8px'}}>
775
- <div style={{display: 'flex', alignItems: 'center'}}>
776
- <Tag color={worker.is_alive ? 'success' : 'error'} size="small" style={{marginRight: '8px'}}>
777
- {worker.is_alive ? '在线' : '离线'}
778
- </Tag>
779
- <code style={{fontSize: '0.7rem', background: '#f7fafc', padding: '2px 6px', borderRadius: '3px'}}>
780
- {(worker.consumer_id || 'unknown').substring(0, 12)}...
781
- </code>
782
- </div>
783
- </div>
784
- <div style={{fontSize: '0.75rem', color: '#718096'}}>
785
- {worker.host} | 成功: {worker.success_count || 0} | 失败: {worker.failed_count || 0}
786
- </div>
787
- </div>
788
- ))}
789
- {workers.length > 5 && (
790
- <Button type="link" size="small">
791
- 查看全部 {workers.length} 个Workers
792
- </Button>
793
- )}
794
- </Space>
795
- )}
796
- </Card>
797
-
798
- {/* 任务趋势图 */}
799
- <Card
800
- title={
801
- <span style={{background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)', WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent', fontSize: '1.2rem', fontWeight: 700}}>
802
- 📈 任务趋势
803
- </span>
804
- }
805
- style={{
806
- background: 'rgba(255, 255, 255, 0.95)',
807
- backdropFilter: 'blur(20px)',
808
- borderRadius: '16px',
809
- border: '1px solid rgba(255, 255, 255, 0.3)'
810
- }}
811
- >
812
- <div style={{height: '250px'}}>
813
- <canvas ref={chartRef}></canvas>
814
- </div>
815
- </Card>
816
- </Space>
817
- </Col>
818
-
819
- {/* 右侧任务列表 */}
820
- <Col xs={24} lg={16}>
821
- <Card
822
- title={
823
- <span style={{background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)', WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent', fontSize: '1.5rem', fontWeight: 700}}>
824
- 📝 任务列表
825
- </span>
826
- }
827
- extra={
828
- <Space>
829
- <Input
830
- placeholder="搜索任务..."
831
- prefix={<SearchIcon />}
832
- value={taskSearch}
833
- onChange={(e) => setTaskSearch(e.target.value)}
834
- style={{width: 200}}
835
- />
836
- <Select
837
- placeholder="状态筛选"
838
- value={statusFilter}
839
- onChange={setStatusFilter}
840
- style={{width: 120}}
841
- allowClear
842
- >
843
- <Select.Option value="">全部</Select.Option>
844
- <Select.Option value="pending">等待中</Select.Option>
845
- <Select.Option value="running">执行中</Select.Option>
846
- <Select.Option value="completed">已完成</Select.Option>
847
- <Select.Option value="failed">失败</Select.Option>
848
- </Select>
849
- <Button.Group>
850
- <Button
851
- type={timeRange === '1h' ? 'primary' : 'default'}
852
- onClick={() => setTimeRange('1h')}
853
- >
854
- 1小时
855
- </Button>
856
- <Button
857
- type={timeRange === '24h' ? 'primary' : 'default'}
858
- onClick={() => setTimeRange('24h')}
859
- >
860
- 24小时
861
- </Button>
862
- <Button
863
- type={timeRange === '7d' ? 'primary' : 'default'}
864
- onClick={() => setTimeRange('7d')}
865
- >
866
- 7天
867
- </Button>
868
- </Button.Group>
869
- <Button
870
- type="primary"
871
- icon={<ReloadIcon />}
872
- loading={loading}
873
- onClick={refreshData}
874
- >
875
- 刷新
876
- </Button>
877
- </Space>
878
- }
879
- style={{
880
- background: 'rgba(255, 255, 255, 0.95)',
881
- backdropFilter: 'blur(20px)',
882
- borderRadius: '20px',
883
- border: '1px solid rgba(255, 255, 255, 0.3)'
884
- }}
885
- >
886
- <Table
887
- columns={columns}
888
- dataSource={filteredTasks}
889
- rowKey="event_id"
890
- loading={loading}
891
- pagination={false}
892
- scroll={{y: 500}}
893
- size="middle"
894
- />
895
-
896
- <div style={{padding: '16px 0', display: 'flex', justifyContent: 'center'}}>
897
- <Pagination
898
- current={currentPage}
899
- pageSize={pageSize}
900
- total={totalTasks}
901
- showSizeChanger
902
- showQuickJumper
903
- showTotal={(total, range) => `第 ${range[0]}-${range[1]} 条,共 ${total} 条`}
904
- pageSizeOptions={['20', '50', '100', '200']}
905
- onChange={(page, size) => {
906
- setCurrentPage(page);
907
- setPageSize(size);
908
- }}
909
- />
910
- </div>
911
- </Card>
912
- </Col>
913
- </Row>
914
- </div>
915
-
916
- {/* 任务详情模态框 */}
917
- <Modal
918
- title="任务详情"
919
- open={taskDetailVisible}
920
- onCancel={() => {
921
- setTaskDetailVisible(false);
922
- setSelectedTask(null);
923
- }}
924
- footer={null}
925
- width="60%"
926
- >
927
- {selectedTask && (
928
- <>
929
- <Descriptions title="基本信息" column={2} bordered>
930
- <Descriptions.Item label="任务ID">
931
- <code>{selectedTask.event_id}</code>
932
- </Descriptions.Item>
933
- <Descriptions.Item label="任务名称">
934
- {selectedTask.task_name}
935
- </Descriptions.Item>
936
- <Descriptions.Item label="状态">
937
- <Tag color={getStatusType(selectedTask.parsed_status?.status)}>
938
- {getStatusText(selectedTask.parsed_status?.status)}
939
- </Tag>
940
- </Descriptions.Item>
941
- <Descriptions.Item label="消费者">
942
- {selectedTask.consumer}
943
- </Descriptions.Item>
944
- <Descriptions.Item label="创建时间">
945
- {formatDateTime(selectedTask.trigger_time)}
946
- </Descriptions.Item>
947
- <Descriptions.Item label="队列">
948
- {selectedTask.queue}
949
- </Descriptions.Item>
950
- </Descriptions>
951
-
952
- <Divider>参数详情</Divider>
953
- <pre style={{
954
- background: '#f5f5f5',
955
- padding: '16px',
956
- borderRadius: '8px',
957
- overflowX: 'auto',
958
- fontSize: '0.9rem'
959
- }}>
960
- {selectedTask.params_str || '无参数'}
961
- </pre>
962
- </>
963
- )}
964
- </Modal>
965
-
966
- <BackTop />
967
- </div>
968
- </ConfigProvider>
969
- );
970
- }
971
-
972
- // 主应用组件
973
- function App() {
974
- return React.createElement(QueueDetail);
975
- }
976
-
977
- // 渲染应用
978
- ReactDOM.render(React.createElement(App), document.getElementById('root'));
979
- </script>
980
- </body>
981
- </html>