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
@@ -1,1734 +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
- .stats-card {
90
- background: rgba(31, 31, 31, 0.95);
91
- backdrop-filter: blur(20px);
92
- border-radius: 16px;
93
- border: 1px solid rgba(255, 255, 255, 0.1);
94
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
95
- overflow: hidden;
96
- position: relative;
97
- height: 100%;
98
- }
99
-
100
- .stats-card::before {
101
- content: '';
102
- position: absolute;
103
- top: 0;
104
- left: 0;
105
- right: 0;
106
- height: 4px;
107
- background: var(--primary-gradient);
108
- }
109
-
110
- .stats-card:hover {
111
- transform: translateY(-8px);
112
- box-shadow: 0 20px 40px rgba(0, 0, 0, 0.5);
113
- background: rgba(31, 31, 31, 1);
114
- border-color: rgba(255, 255, 255, 0.2);
115
- }
116
-
117
- .chart-card {
118
- background: rgba(31, 31, 31, 0.95);
119
- backdrop-filter: blur(20px);
120
- border-radius: 20px;
121
- border: 1px solid rgba(255, 255, 255, 0.1);
122
- transition: all 0.3s ease;
123
- }
124
-
125
- .chart-card:hover {
126
- box-shadow: 0 20px 40px rgba(0, 0, 0, 0.5);
127
- background: rgba(31, 31, 31, 1);
128
- border-color: rgba(255, 255, 255, 0.2);
129
- }
130
-
131
- .table-card {
132
- background: rgba(255, 255, 255, 0.95);
133
- backdrop-filter: blur(20px);
134
- border-radius: 20px;
135
- border: 1px solid rgba(255, 255, 255, 0.3);
136
- overflow: hidden;
137
- transition: all 0.3s ease;
138
- }
139
-
140
- .table-card:hover {
141
- box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15);
142
- background: rgba(255, 255, 255, 1);
143
- }
144
-
145
- .gradient-text {
146
- background: linear-gradient(135deg, #177ddc 0%, #69c0ff 100%);
147
- -webkit-background-clip: text;
148
- -webkit-text-fill-color: transparent;
149
- background-clip: text;
150
- font-weight: 700;
151
- }
152
-
153
- /* 暗色主题卡片样式 */
154
- .stats-card {
155
- background: rgba(31, 31, 31, 0.8) !important;
156
- backdrop-filter: blur(20px);
157
- border-radius: 20px;
158
- border: 1px solid rgba(255, 255, 255, 0.08);
159
- transition: all 0.3s ease;
160
- overflow: hidden;
161
- position: relative;
162
- }
163
-
164
- .stats-card:hover {
165
- transform: translateY(-5px);
166
- box-shadow: 0 20px 40px rgba(0, 0, 0, 0.5);
167
- background: rgba(31, 31, 31, 0.95) !important;
168
- border-color: rgba(255, 255, 255, 0.12);
169
- }
170
-
171
- .chart-card {
172
- background: rgba(31, 31, 31, 0.8) !important;
173
- backdrop-filter: blur(20px);
174
- border-radius: 20px;
175
- border: 1px solid rgba(255, 255, 255, 0.08);
176
- overflow: visible;
177
- transition: all 0.3s ease;
178
- }
179
-
180
- .chart-card:hover {
181
- box-shadow: 0 20px 40px rgba(0, 0, 0, 0.5);
182
- background: rgba(31, 31, 31, 0.95) !important;
183
- border-color: rgba(255, 255, 255, 0.12);
184
- }
185
-
186
- .table-card {
187
- background: rgba(31, 31, 31, 0.8) !important;
188
- backdrop-filter: blur(20px);
189
- border-radius: 20px;
190
- border: 1px solid rgba(255, 255, 255, 0.08);
191
- overflow: hidden;
192
- transition: all 0.3s ease;
193
- }
194
-
195
- .table-card:hover {
196
- box-shadow: 0 20px 40px rgba(0, 0, 0, 0.5);
197
- background: rgba(31, 31, 31, 0.95) !important;
198
- border-color: rgba(255, 255, 255, 0.12);
199
- }
200
-
201
- /* Ant Design 组件暗色覆盖 */
202
- .ant-card {
203
- background: rgba(31, 31, 31, 0.95) !important;
204
- border-color: rgba(255, 255, 255, 0.08) !important;
205
- }
206
-
207
- .ant-statistic-title {
208
- color: rgba(255, 255, 255, 0.65) !important;
209
- }
210
-
211
- .ant-table {
212
- background: transparent !important;
213
- }
214
-
215
- .ant-table-thead > tr > th {
216
- background: rgba(255, 255, 255, 0.04) !important;
217
- border-bottom-color: rgba(255, 255, 255, 0.08) !important;
218
- }
219
-
220
- .ant-table-tbody > tr > td {
221
- border-bottom-color: rgba(255, 255, 255, 0.08) !important;
222
- }
223
-
224
- .ant-table-tbody > tr:hover > td {
225
- background: rgba(255, 255, 255, 0.04) !important;
226
- }
227
-
228
- /* 表格行条纹效果 */
229
- .table-row-light {
230
- background: rgba(255, 255, 255, 0.02);
231
- }
232
-
233
- .table-row-dark {
234
- background: rgba(255, 255, 255, 0.04);
235
- }
236
-
237
- /* 修复导航按钮样式 */
238
- .ant-btn-text {
239
- color: rgba(255, 255, 255, 0.65) !important;
240
- }
241
-
242
- .ant-btn-text:hover {
243
- color: rgba(255, 255, 255, 0.85) !important;
244
- background: rgba(255, 255, 255, 0.08) !important;
245
- }
246
-
247
- .ant-btn-primary {
248
- background: #177ddc !important;
249
- border-color: #177ddc !important;
250
- }
251
-
252
- .ant-btn-primary:hover {
253
- background: #1890ff !important;
254
- border-color: #1890ff !important;
255
- }
256
-
257
- /* 分页器样式 */
258
- .ant-pagination-item {
259
- background: rgba(255, 255, 255, 0.08) !important;
260
- border-color: rgba(255, 255, 255, 0.12) !important;
261
- }
262
-
263
- .ant-pagination-item a {
264
- color: rgba(255, 255, 255, 0.85) !important;
265
- }
266
-
267
- .ant-pagination-item-active {
268
- background: #177ddc !important;
269
- border-color: #177ddc !important;
270
- }
271
-
272
- .ant-pagination-item-active a {
273
- color: white !important;
274
- }
275
-
276
- /* Select下拉框样式 */
277
- .ant-select-selector {
278
- background: rgba(255, 255, 255, 0.08) !important;
279
- border-color: rgba(255, 255, 255, 0.12) !important;
280
- color: rgba(255, 255, 255, 0.85) !important;
281
- }
282
-
283
- /* Input输入框样式 */
284
- .ant-input {
285
- background: rgba(255, 255, 255, 0.08) !important;
286
- border-color: rgba(255, 255, 255, 0.12) !important;
287
- color: rgba(255, 255, 255, 0.85) !important;
288
- }
289
-
290
- .ant-input::placeholder {
291
- color: rgba(255, 255, 255, 0.45) !important;
292
- }
293
-
294
- /* Switch开关样式 */
295
- .ant-switch {
296
- background: rgba(255, 255, 255, 0.25) !important;
297
- }
298
-
299
- .ant-switch-checked {
300
- background: #177ddc !important;
301
- }
302
-
303
- .connection-status {
304
- position: fixed;
305
- bottom: 24px;
306
- right: 24px;
307
- z-index: 1000;
308
- }
309
-
310
- .chart-container {
311
- height: 300px;
312
- padding: 16px;
313
- }
314
-
315
- .ant-table-thead > tr > th {
316
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
317
- color: white !important;
318
- border-bottom: none !important;
319
- font-weight: 600 !important;
320
- font-size: 0.8rem !important;
321
- text-transform: uppercase !important;
322
- letter-spacing: 0.05em !important;
323
- }
324
-
325
- .ant-table-tbody > tr:hover > td {
326
- background: rgba(24, 144, 255, 0.04) !important;
327
- }
328
-
329
- .ant-card {
330
- background: rgba(31, 31, 31, 0.95) !important;
331
- border: 1px solid rgba(255, 255, 255, 0.1) !important;
332
- box-shadow: none !important;
333
- }
334
-
335
- /* 修复时间选择器被遮挡的问题 */
336
- .chart-card .ant-card {
337
- overflow: visible !important;
338
- }
339
- .chart-card .ant-card-head {
340
- overflow: visible !important;
341
- }
342
- .chart-card .ant-card-body {
343
- overflow: visible !important;
344
- }
345
-
346
- .ant-card-head {
347
- border-bottom-color: rgba(255, 255, 255, 0.1) !important;
348
- color: rgba(255, 255, 255, 0.85) !important;
349
- }
350
-
351
- .ant-card-body {
352
- color: rgba(255, 255, 255, 0.85) !important;
353
- }
354
-
355
- .ant-statistic-title {
356
- color: rgba(255, 255, 255, 0.65) !important;
357
- font-size: 0.875rem !important;
358
- font-weight: 600 !important;
359
- text-transform: uppercase !important;
360
- letter-spacing: 0.05em !important;
361
- margin-bottom: 12px !important;
362
- }
363
-
364
- .ant-statistic-content-value {
365
- color: rgba(255, 255, 255, 0.85) !important;
366
- font-size: 2.25rem !important;
367
- font-weight: 700 !important;
368
- line-height: 1.2 !important;
369
- }
370
-
371
- /* 响应式设计 */
372
- @media (max-width: 768px) {
373
- .main-content {
374
- padding: 16px;
375
- }
376
- .main-header {
377
- font-size: 2rem;
378
- }
379
- }
380
-
381
- /* 确保所有文字都清晰可见 */
382
- .ant-typography {
383
- color: rgba(255, 255, 255, 0.85) !important;
384
- }
385
-
386
- .ant-tag {
387
- background: rgba(255, 255, 255, 0.1) !important;
388
- border-color: rgba(255, 255, 255, 0.2) !important;
389
- color: rgba(255, 255, 255, 0.85) !important;
390
- }
391
-
392
- /* 特定颜色的标签保持原色 */
393
- .ant-tag-success {
394
- background: rgba(73, 170, 25, 0.2) !important;
395
- border-color: #49aa19 !important;
396
- color: #49aa19 !important;
397
- }
398
-
399
- .ant-tag-error {
400
- background: rgba(166, 29, 36, 0.2) !important;
401
- border-color: #a61d24 !important;
402
- color: #ff4d4f !important;
403
- }
404
-
405
- .ant-tag-warning {
406
- background: rgba(216, 150, 20, 0.2) !important;
407
- border-color: #d89614 !important;
408
- color: #faad14 !important;
409
- }
410
-
411
- .ant-tag-blue {
412
- background: rgba(24, 144, 255, 0.2) !important;
413
- border-color: #1890ff !important;
414
- color: #1890ff !important;
415
- }
416
-
417
- .ant-btn {
418
- color: rgba(255, 255, 255, 0.85) !important;
419
- border-color: rgba(255, 255, 255, 0.2) !important;
420
- }
421
-
422
- .ant-btn:hover {
423
- color: rgba(255, 255, 255, 1) !important;
424
- border-color: rgba(255, 255, 255, 0.4) !important;
425
- }
426
-
427
- .ant-btn-primary {
428
- background: #177ddc !important;
429
- border-color: #177ddc !important;
430
- color: white !important;
431
- }
432
-
433
- .ant-btn-primary:hover {
434
- background: #1890ff !important;
435
- border-color: #1890ff !important;
436
- }
437
-
438
- /* 表格文字颜色 */
439
- .ant-table {
440
- color: rgba(255, 255, 255, 0.85) !important;
441
- }
442
-
443
- .ant-table-thead > tr > th {
444
- color: rgba(255, 255, 255, 0.85) !important;
445
- background: rgba(255, 255, 255, 0.04) !important;
446
- }
447
-
448
- .ant-table-tbody > tr > td {
449
- color: rgba(255, 255, 255, 0.85) !important;
450
- }
451
-
452
- /* 确保标题可见 */
453
- h1, h2, h3, h4, h5, h6 {
454
- color: rgba(255, 255, 255, 0.85) !important;
455
- }
456
-
457
- /* 修复按钮文字 */
458
- .ant-btn-text {
459
- color: rgba(255, 255, 255, 0.65) !important;
460
- background: transparent !important;
461
- }
462
-
463
- .ant-btn-text:hover {
464
- color: rgba(255, 255, 255, 0.85) !important;
465
- background: rgba(255, 255, 255, 0.08) !important;
466
- }
467
-
468
- /* 分页器 */
469
- .ant-pagination-item {
470
- background: rgba(255, 255, 255, 0.08) !important;
471
- border-color: rgba(255, 255, 255, 0.12) !important;
472
- }
473
-
474
- .ant-pagination-item a {
475
- color: rgba(255, 255, 255, 0.85) !important;
476
- }
477
-
478
- .ant-pagination-item-active {
479
- background: #177ddc !important;
480
- border-color: #177ddc !important;
481
- }
482
-
483
- /* 输入框 */
484
- .ant-input {
485
- background: rgba(255, 255, 255, 0.08) !important;
486
- border-color: rgba(255, 255, 255, 0.12) !important;
487
- color: rgba(255, 255, 255, 0.85) !important;
488
- }
489
-
490
- .ant-input::placeholder {
491
- color: rgba(255, 255, 255, 0.45) !important;
492
- }
493
-
494
- /* 开关 */
495
- .ant-switch {
496
- background: rgba(255, 255, 255, 0.25) !important;
497
- }
498
-
499
- .ant-switch-checked {
500
- background: #177ddc !important;
501
- }
502
-
503
- /* 动画效果 */
504
- @keyframes fadeInUp {
505
- from {
506
- opacity: 0;
507
- transform: translateY(30px);
508
- }
509
- to {
510
- opacity: 1;
511
- transform: translateY(0);
512
- }
513
- }
514
-
515
- .fade-in-up {
516
- animation: fadeInUp 0.6s ease-out;
517
- }
518
- </style>
519
- </head>
520
- <body>
521
- <div id="root"></div>
522
-
523
- <script type="text/babel">
524
- const { useState, useEffect, useRef, useCallback, useMemo } = React;
525
- const {
526
- Layout, Card, Row, Col, Table, Tag, Button, Input, Select,
527
- Statistic, Progress, Badge, Space, Typography, Tooltip,
528
- Switch, message, Spin, Modal, Descriptions, Divider, ConfigProvider,
529
- DatePicker, Radio
530
- } = antd;
531
-
532
- const { Header, Content } = Layout;
533
- const { Title, Text } = Typography;
534
- const { Search } = Input;
535
- const { Option } = Select;
536
-
537
- // 时间工具函数
538
- const formatUTCToLocal = (utcTimeStr) => {
539
- // 将 UTC 时间转换为本地时间显示
540
- if (!utcTimeStr) return '-';
541
- const date = new Date(utcTimeStr);
542
- // 使用 moment 进行格式化,自动转换为本地时区
543
- return moment(date).format('YYYY-MM-DD HH:mm:ss');
544
- };
545
-
546
- const getUTCNow = () => {
547
- // 获取当前 UTC 时间
548
- return new Date().toISOString();
549
- };
550
-
551
- const toUTCString = (localDate) => {
552
- // 将本地时间转换为 UTC ISO 字符串
553
- return new Date(localDate).toISOString();
554
- };
555
-
556
- // 主应用组件
557
- const App = () => {
558
- const [loading, setLoading] = useState(false);
559
- const [connectionStatus, setConnectionStatus] = useState('disconnected');
560
- const [searchKeyword, setSearchKeyword] = useState('');
561
- const [showAllStats, setShowAllStats] = useState(true);
562
- const [chartLoading, setChartLoading] = useState(false);
563
-
564
- // 图表时间范围选择
565
- const [chartTimeRange, setChartTimeRange] = useState('1h'); // 默认1小时
566
- const [customTimeRange, setCustomTimeRange] = useState(null); // 自定义时间范围
567
- const [showCustomPicker, setShowCustomPicker] = useState(false); // 是否显示自定义时间选择器
568
- const [pickerPosition, setPickerPosition] = useState({ top: 0, left: 0 }); // 时间选择器位置
569
-
570
- const [globalStats, setGlobalStats] = useState({
571
- total_queues: 0,
572
- messages: 0,
573
- messages_ready: 0,
574
- messages_unacknowledged: 0,
575
- consumers: 0,
576
- total_count: 0,
577
- total_success_count: 0,
578
- total_failed_count: 0,
579
- total_running_tasks: 0,
580
- online_workers: 0,
581
- total_workers: 0,
582
- message_stats: {
583
- publish: 0,
584
- deliver_get: 0,
585
- ack: 0
586
- }
587
- });
588
-
589
- const [queues, setQueues] = useState([]);
590
- const [workers, setWorkers] = useState([]);
591
-
592
- const wsRef = useRef(null);
593
- const tasksChartRef = useRef(null);
594
- const tasksChartInstance = useRef(null);
595
-
596
- // 格式化时间
597
- const formatTime = useCallback((seconds) => {
598
- if (!seconds || seconds === 0) return '-';
599
- if (seconds < 1) return `${Math.round(seconds * 1000)}ms`;
600
- if (seconds < 60) return `${seconds.toFixed(2)}s`;
601
- const minutes = Math.floor(seconds / 60);
602
- const remainingSeconds = (seconds % 60).toFixed(1);
603
- return `${minutes}m${remainingSeconds}s`;
604
- }, []);
605
-
606
- // 统计卡片数据
607
- const statsCards = useMemo(() => [
608
- {
609
- title: '总队列数',
610
- value: globalStats.total_queues,
611
- icon: <i className="fas fa-layer-group" style={{color: '#1890ff'}}/>,
612
- color: '#1890ff',
613
- trend: { type: 'success', text: '活跃', desc: '队列运行正常' }
614
- },
615
- {
616
- title: '消息总数',
617
- value: globalStats.messages,
618
- icon: <i className="fas fa-envelope" style={{color: '#722ed1'}}/>,
619
- color: '#722ed1',
620
- suffix: (
621
- <div style={{fontSize: '0.75rem', color: '#666', marginTop: '4px'}}>
622
- 就绪: <Text type="secondary">{globalStats.messages_ready}</Text> |
623
- 未确认: <Text type="warning">{globalStats.messages_unacknowledged}</Text>
624
- </div>
625
- )
626
- },
627
- {
628
- title: '任务统计',
629
- value: globalStats.total_count,
630
- icon: <i className="fas fa-tasks" style={{color: '#52c41a'}}/>,
631
- color: '#52c41a',
632
- suffix: (
633
- <div style={{fontSize: '0.75rem', color: '#666', marginTop: '4px'}}>
634
- 成功: <Text type="success">{globalStats.total_success_count}</Text> |
635
- 失败: <Text type="danger">{globalStats.total_failed_count}</Text>
636
- </div>
637
- )
638
- },
639
- {
640
- title: '消费者数量',
641
- value: globalStats.consumers,
642
- icon: <i className="fas fa-users" style={{color: '#faad14'}}/>,
643
- color: '#faad14',
644
- suffix: (
645
- <div style={{fontSize: '0.75rem', color: '#666', marginTop: '4px'}}>
646
- 在线: <Text type="success">{globalStats.online_workers}</Text> |
647
- 离线: <Text type="secondary">{globalStats.total_workers - globalStats.online_workers}</Text>
648
- </div>
649
- )
650
- },
651
- {
652
- title: '执行中任务',
653
- value: globalStats.total_running_tasks,
654
- icon: <i className="fas fa-spinner" style={{color: '#faad14'}}/>,
655
- color: '#faad14'
656
- }
657
- ], [globalStats]);
658
-
659
- // 过滤后的队列
660
- const filteredQueues = useMemo(() => {
661
- if (!searchKeyword) return queues;
662
- return queues.filter(queue =>
663
- queue.name.toLowerCase().includes(searchKeyword.toLowerCase())
664
- );
665
- }, [queues, searchKeyword]);
666
-
667
- // 在线Workers数量
668
- const onlineWorkersCount = useMemo(() =>
669
- workers.filter(w => w.is_alive).length, [workers]
670
- );
671
-
672
- // 队列表格列定义
673
- const queueColumns = [
674
- {
675
- title: '队列名称',
676
- dataIndex: 'name',
677
- key: 'name',
678
- render: (name) => (
679
- <Space>
680
- <Tag color="blue">Q</Tag>
681
- <Text strong style={{color: '#1890ff'}}>{name}</Text>
682
- </Space>
683
- ),
684
- },
685
- {
686
- title: '消息数',
687
- dataIndex: 'messages',
688
- key: 'messages',
689
- align: 'center',
690
- render: (value) => <Tag color="blue">{value || 0}</Tag>,
691
- },
692
- {
693
- title: '就绪',
694
- dataIndex: 'messages_ready',
695
- key: 'messages_ready',
696
- align: 'center',
697
- render: (value) => <Tag color="cyan">{value || 0}</Tag>,
698
- },
699
- {
700
- title: '未确认',
701
- dataIndex: 'messages_unacknowledged',
702
- key: 'messages_unacknowledged',
703
- align: 'center',
704
- render: (value) => <Tag color="orange">{value || 0}</Tag>,
705
- },
706
- {
707
- title: '成功',
708
- dataIndex: 'success_count',
709
- key: 'success_count',
710
- align: 'center',
711
- render: (value) => <Tag color="green">{value || 0}</Tag>,
712
- },
713
- {
714
- title: '失败',
715
- dataIndex: 'failed_count',
716
- key: 'failed_count',
717
- align: 'center',
718
- render: (value) => <Tag color="red">{value || 0}</Tag>,
719
- },
720
- {
721
- title: '执行中',
722
- dataIndex: 'running_tasks',
723
- key: 'running_tasks',
724
- align: 'center',
725
- render: (value) => <Tag color="orange">{value || 0}</Tag>,
726
- },
727
- {
728
- title: '消费者',
729
- dataIndex: 'consumers',
730
- key: 'consumers',
731
- align: 'center',
732
- render: (value) => (
733
- <Space>
734
- <Badge
735
- status={value > 0 ? 'success' : 'error'}
736
- color={value > 0 ? '#52c41a' : '#f5222d'}
737
- />
738
- {value || 0}
739
- </Space>
740
- ),
741
- },
742
- {
743
- title: '操作',
744
- key: 'action',
745
- align: 'center',
746
- render: (_, record) => (
747
- <Button
748
- type="primary"
749
- size="small"
750
- onClick={() => viewQueueDetails(record)}
751
- style={{borderRadius: '8px'}}
752
- >
753
- 查看详情
754
- </Button>
755
- ),
756
- },
757
- ];
758
-
759
- // Workers表格列定义
760
- const workerColumns = [
761
- {
762
- title: '状态',
763
- dataIndex: 'is_alive',
764
- key: 'is_alive',
765
- align: 'center',
766
- render: (isAlive) => (
767
- <Tag color={isAlive ? 'green' : 'red'}>
768
- {isAlive ? '在线' : '离线'}
769
- </Tag>
770
- ),
771
- },
772
- {
773
- title: 'Worker ID',
774
- dataIndex: 'consumer_id',
775
- key: 'consumer_id',
776
- render: (id) => (
777
- <code style={{
778
- fontSize: '0.75rem',
779
- background: '#f7fafc',
780
- padding: '4px 8px',
781
- borderRadius: '4px'
782
- }}>
783
- {(id || 'unknown').substring(0, 20)}...
784
- </code>
785
- ),
786
- },
787
- {
788
- title: '主机',
789
- dataIndex: 'host',
790
- key: 'host',
791
- },
792
- {
793
- title: '队列',
794
- dataIndex: 'queue',
795
- key: 'queue',
796
- },
797
- {
798
- title: '成功',
799
- dataIndex: 'success_count',
800
- key: 'success_count',
801
- align: 'center',
802
- render: (value) => <Tag color="green">{value || 0}</Tag>,
803
- },
804
- {
805
- title: '失败',
806
- dataIndex: 'failed_count',
807
- key: 'failed_count',
808
- align: 'center',
809
- render: (value) => <Tag color="red">{value || 0}</Tag>,
810
- },
811
- {
812
- title: '执行中',
813
- dataIndex: 'running_tasks',
814
- key: 'running_tasks',
815
- align: 'center',
816
- render: (value) => <Tag color="orange">{value || 0}</Tag>,
817
- },
818
- {
819
- title: '平均耗时',
820
- dataIndex: 'avg_processing_time',
821
- key: 'avg_processing_time',
822
- align: 'center',
823
- render: (value) => formatTime(value),
824
- },
825
- ];
826
-
827
- // 获取队列时间线数据 - 完全使用PostgreSQL
828
- const fetchQueueTimelines = useCallback(async () => {
829
- try {
830
- console.log('fetchQueueTimelines called with chartTimeRange:', chartTimeRange);
831
-
832
- // 计算时间范围
833
- let startTime, endTime;
834
- const now = new Date();
835
-
836
- if (customTimeRange) {
837
- // 使用自定义时间范围
838
- startTime = customTimeRange[0].toISOString();
839
- endTime = customTimeRange[1].toISOString();
840
- } else {
841
- // 使用预设时间范围
842
- endTime = now.toISOString();
843
- let millisAgo;
844
- switch (chartTimeRange) {
845
- case '15m': millisAgo = 15 * 60 * 1000; break;
846
- case '30m': millisAgo = 30 * 60 * 1000; break;
847
- case '1h': millisAgo = 60 * 60 * 1000; break;
848
- case '3h': millisAgo = 3 * 60 * 60 * 1000; break;
849
- case '6h': millisAgo = 6 * 60 * 60 * 1000; break;
850
- case '12h': millisAgo = 12 * 60 * 60 * 1000; break;
851
- case '24h': millisAgo = 24 * 60 * 60 * 1000; break;
852
- case '3d': millisAgo = 3 * 24 * 60 * 60 * 1000; break;
853
- case '7d': millisAgo = 7 * 24 * 60 * 60 * 1000; break;
854
- case '30d': millisAgo = 30 * 24 * 60 * 60 * 1000; break;
855
- default: millisAgo = 60 * 60 * 1000; // 默认1小时
856
- }
857
- startTime = new Date(now.getTime() - millisAgo).toISOString();
858
-
859
- console.log('Time range:', { startTime, endTime, chartTimeRange, millisAgo });
860
- }
861
-
862
- // 根据时间范围选择合适的间隔
863
- let interval = '5m';
864
- const minutes = (new Date(endTime) - new Date(startTime)) / (1000 * 60);
865
- if (minutes <= 30) interval = '1m';
866
- else if (minutes <= 60) interval = '5m';
867
- else if (minutes <= 180) interval = '10m';
868
- else if (minutes <= 360) interval = '15m';
869
- else if (minutes <= 720) interval = '30m';
870
- else if (minutes <= 1440) interval = '1h';
871
- else if (minutes <= 4320) interval = '3h';
872
- else if (minutes <= 10080) interval = '6h';
873
- else interval = '24h';
874
-
875
- // 只使用PostgreSQL API获取多个队列的数据
876
- const queueNames = queues.slice(0, 5).map(q => q.name).join(',');
877
- const response = await fetch(
878
- `/api/queues/timeline/pg?queues=${encodeURIComponent(queueNames)}&start_time=${startTime}&end_time=${endTime}&interval=${interval}`
879
- );
880
-
881
- if (!response.ok) {
882
- const errorData = await response.json();
883
- if (errorData.error === "PostgreSQL connection not configured") {
884
- message.error('PostgreSQL未配置,请设置数据库连接信息');
885
- } else {
886
- message.error('获取数据失败: ' + (errorData.error || 'Unknown error'));
887
- }
888
- throw new Error(errorData.error || 'Failed to fetch timeline data');
889
- }
890
-
891
- const data = await response.json();
892
- console.log('Timeline data from PostgreSQL:', data);
893
-
894
- if (data.error) {
895
- message.warning('部分数据获取失败: ' + data.error);
896
- }
897
-
898
- return data.queues || [];
899
- } catch (error) {
900
- console.error('Failed to fetch queue timelines:', error);
901
- message.error('获取队列趋势数据失败');
902
- return [];
903
- }
904
- }, [chartTimeRange, customTimeRange, queues]);
905
-
906
- // 初始化图表
907
- const initCharts = useCallback(async () => {
908
- // 任务趋势图表
909
- if (tasksChartRef.current) {
910
- const ctx = tasksChartRef.current.getContext('2d');
911
-
912
- if (tasksChartInstance.current) {
913
- tasksChartInstance.current.destroy();
914
- }
915
-
916
- // 生成队列颜色
917
- const colors = [
918
- '#1890ff', '#52c41a', '#faad14', '#f5222d', '#722ed1',
919
- '#13c2c2', '#eb2f96', '#fa8c16', '#a0d911', '#fa541c'
920
- ];
921
-
922
- // 固定为1小时,每5分钟一个点
923
- const dataPoints = 12;
924
- const now = new Date();
925
- const labels = Array.from({length: dataPoints}, (_, i) => {
926
- const time = new Date(now.getTime() - (60 - i * 5) * 60 * 1000);
927
- return `${time.getHours().toString().padStart(2, '0')}:${time.getMinutes().toString().padStart(2, '0')}`;
928
- });
929
-
930
- // 获取真实的时间线数据
931
- const timelines = await fetchQueueTimelines();
932
-
933
- // 处理时间线数据
934
- const queueDatasets = timelines.map((item, index) => {
935
- if (!item.timeline) {
936
- console.warn(`No timeline data for queue ${item.queue}`);
937
- return null;
938
- }
939
-
940
- const timelinePoints = item.timeline.timeline || [];
941
- console.log(`Processing ${item.queue}: ${timelinePoints.length} points`);
942
-
943
- // 提取数据点
944
- const dataPoints = timelinePoints.map(point => point.count || 0);
945
- console.log(`Data points for ${item.queue}:`, dataPoints);
946
-
947
- // 如果数据点不足,补充0
948
- while (dataPoints.length < labels.length) {
949
- dataPoints.unshift(0);
950
- }
951
-
952
- // 如果数据点过多,截取最后的数据
953
- if (dataPoints.length > labels.length) {
954
- dataPoints.splice(0, dataPoints.length - labels.length);
955
- }
956
-
957
- return {
958
- label: item.queue,
959
- data: dataPoints,
960
- borderColor: colors[index % colors.length],
961
- backgroundColor: 'transparent',
962
- fill: false,
963
- tension: 0.4,
964
- borderWidth: 2,
965
- pointRadius: 3,
966
- pointHoverRadius: 5
967
- };
968
- }).filter(dataset => dataset !== null);
969
-
970
- // 自定义插件:显示垂直线
971
- const crosshairPlugin = {
972
- id: 'crosshair',
973
- afterDraw: (chart) => {
974
- if (chart.tooltip._active && chart.tooltip._active.length) {
975
- const activePoint = chart.tooltip._active[0];
976
- const ctx = chart.ctx;
977
- const x = activePoint.element.x;
978
- const topY = chart.scales.y.top;
979
- const bottomY = chart.scales.y.bottom;
980
-
981
- // 绘制垂直线
982
- ctx.save();
983
- ctx.beginPath();
984
- ctx.moveTo(x, topY);
985
- ctx.lineTo(x, bottomY);
986
- ctx.lineWidth = 1;
987
- ctx.strokeStyle = 'rgba(255, 255, 255, 0.3)';
988
- ctx.stroke();
989
- ctx.restore();
990
- }
991
- },
992
- afterEvent: (chart, args) => {
993
- const {inChartArea} = args;
994
- chart.canvas.style.cursor = inChartArea ? 'crosshair' : 'default';
995
- }
996
- };
997
-
998
- tasksChartInstance.current = new Chart(ctx, {
999
- type: 'line',
1000
- data: {
1001
- labels: labels,
1002
- datasets: queueDatasets
1003
- },
1004
- plugins: [crosshairPlugin],
1005
- options: {
1006
- responsive: true,
1007
- maintainAspectRatio: false,
1008
- interaction: {
1009
- mode: 'index',
1010
- intersect: false,
1011
- },
1012
- animation: false, // 完全禁用动画
1013
- transitions: {
1014
- active: {
1015
- animation: {
1016
- duration: 0
1017
- }
1018
- }
1019
- },
1020
- plugins: {
1021
- tooltip: {
1022
- mode: 'index',
1023
- intersect: false,
1024
- backgroundColor: 'rgba(0, 0, 0, 0.8)',
1025
- titleColor: 'rgba(255, 255, 255, 0.9)',
1026
- bodyColor: 'rgba(255, 255, 255, 0.8)',
1027
- borderColor: 'rgba(255, 255, 255, 0.2)',
1028
- borderWidth: 1,
1029
- cornerRadius: 4,
1030
- displayColors: true,
1031
- callbacks: {
1032
- title: function(context) {
1033
- return context[0].label;
1034
- },
1035
- label: function(context) {
1036
- return context.dataset.label + ': ' + context.parsed.y + ' 任务';
1037
- }
1038
- }
1039
- },
1040
- legend: {
1041
- position: 'top',
1042
- labels: {
1043
- color: 'rgba(255, 255, 255, 0.85)'
1044
- }
1045
- }
1046
- },
1047
- scales: {
1048
- x: {
1049
- grid: {
1050
- color: 'rgba(255, 255, 255, 0.08)'
1051
- },
1052
- ticks: {
1053
- color: 'rgba(255, 255, 255, 0.65)'
1054
- }
1055
- },
1056
- y: {
1057
- beginAtZero: true,
1058
- min: 0,
1059
- suggestedMax: 10, // 设置一个最小的Y轴范围,即使数据为0也能看到线条
1060
- grid: {
1061
- color: 'rgba(255, 255, 255, 0.08)'
1062
- },
1063
- ticks: {
1064
- color: 'rgba(255, 255, 255, 0.65)'
1065
- }
1066
- }
1067
- }
1068
- }
1069
- });
1070
- }
1071
-
1072
- }, [globalStats.online_workers, globalStats.total_workers, queues]);
1073
-
1074
- // 更新图表数据
1075
- const updateChartsData = useCallback(async () => {
1076
- console.log('updateChartsData called, current chartTimeRange:', chartTimeRange);
1077
-
1078
- // 更新队列处理趋势图
1079
- if (tasksChartInstance.current && queues.length > 0) {
1080
- const colors = [
1081
- '#1890ff', '#52c41a', '#faad14', '#f5222d', '#722ed1',
1082
- '#13c2c2', '#eb2f96', '#fa8c16', '#a0d911', '#fa541c'
1083
- ];
1084
-
1085
- // 获取真实的时间线数据
1086
- const timelines = await fetchQueueTimelines();
1087
-
1088
- if (timelines.length === 0) {
1089
- console.warn('No timeline data available');
1090
- return;
1091
- }
1092
-
1093
- // 从第一个队列的时间线数据生成时间标签
1094
- let labels = [];
1095
- const firstTimeline = timelines[0]?.timeline?.timeline || [];
1096
- if (firstTimeline.length > 0) {
1097
- labels = firstTimeline.map(point => {
1098
- const time = new Date(point.time);
1099
- return `${time.getHours().toString().padStart(2, '0')}:${time.getMinutes().toString().padStart(2, '0')}`;
1100
- });
1101
- } else {
1102
- // 如果没有数据,生成默认标签
1103
- const dataPoints = 12;
1104
- const now = new Date();
1105
- labels = Array.from({length: dataPoints}, (_, i) => {
1106
- const time = new Date(now.getTime() - (60 - i * 5) * 60 * 1000);
1107
- return `${time.getHours().toString().padStart(2, '0')}:${time.getMinutes().toString().padStart(2, '0')}`;
1108
- });
1109
- }
1110
-
1111
- // 处理时间线数据
1112
- const queueDatasets = timelines.map((item, index) => {
1113
- if (!item.timeline || !item.timeline.timeline) {
1114
- console.warn(`No timeline data for queue ${item.queue}`);
1115
- return null;
1116
- }
1117
-
1118
- const timelinePoints = item.timeline.timeline || [];
1119
- console.log(`Processing ${item.queue}: ${timelinePoints.length} points`);
1120
-
1121
- // 提取数据点
1122
- const dataPoints = timelinePoints.map(point => point.count || 0);
1123
- console.log(`Data points for ${item.queue}:`, dataPoints);
1124
-
1125
- return {
1126
- label: item.queue,
1127
- data: dataPoints,
1128
- borderColor: colors[index % colors.length],
1129
- backgroundColor: 'transparent',
1130
- fill: false,
1131
- tension: 0.4,
1132
- borderWidth: 2,
1133
- pointRadius: 3,
1134
- pointHoverRadius: 5
1135
- };
1136
- }).filter(dataset => dataset !== null);
1137
-
1138
- tasksChartInstance.current.data.labels = labels;
1139
- tasksChartInstance.current.data.datasets = queueDatasets;
1140
- tasksChartInstance.current.update('none'); // 使用 'none' 模式避免动画
1141
- }
1142
- }, [globalStats.online_workers, globalStats.total_workers, queues, fetchQueueTimelines, chartTimeRange, customTimeRange]);
1143
-
1144
- // WebSocket连接
1145
- const connectWebSocket = useCallback(() => {
1146
- const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
1147
- wsRef.current = new WebSocket(`${protocol}//${window.location.host}/ws`);
1148
-
1149
- wsRef.current.onopen = () => {
1150
- setConnectionStatus('connected');
1151
- message.success('连接已建立');
1152
- };
1153
-
1154
- wsRef.current.onmessage = (event) => {
1155
- try {
1156
- const data = JSON.parse(event.data);
1157
- updateData(data);
1158
- } catch (error) {
1159
- console.error('WebSocket数据解析失败:', error);
1160
- }
1161
- };
1162
-
1163
- wsRef.current.onclose = () => {
1164
- setConnectionStatus('disconnected');
1165
- message.warning('连接已断开,正在重连...');
1166
- setTimeout(connectWebSocket, 5000);
1167
- };
1168
-
1169
- wsRef.current.onerror = (error) => {
1170
- console.error('WebSocket错误:', error);
1171
- };
1172
- }, []);
1173
-
1174
- // 更新数据
1175
- const updateData = async (data) => {
1176
- if (data.workers) {
1177
- // 处理队列数据
1178
- const allQueues = {};
1179
- Object.entries(data.workers).forEach(([queueName, queueWorkers]) => {
1180
- allQueues[queueName] = queueWorkers;
1181
- });
1182
-
1183
- // 更新队列列表
1184
- const queuePromises = Object.keys(allQueues).map(async (queueName) => {
1185
- try {
1186
- const [statsResponse, summaryResponse] = await Promise.all([
1187
- fetch(`/api/queue/${encodeURIComponent(queueName)}/stats`),
1188
- fetch(`/api/queue/${encodeURIComponent(queueName)}/worker-summary`)
1189
- ]);
1190
-
1191
- let queueData = {
1192
- key: queueName,
1193
- name: queueName,
1194
- messages: 0,
1195
- messages_ready: 0,
1196
- messages_unacknowledged: 0,
1197
- consumers: 0,
1198
- success_count: 0,
1199
- failed_count: 0,
1200
- running_tasks: 0
1201
- };
1202
-
1203
- if (statsResponse.ok) {
1204
- const stats = await statsResponse.json();
1205
- Object.assign(queueData, stats);
1206
- }
1207
-
1208
- if (summaryResponse.ok) {
1209
- const summary = await summaryResponse.json();
1210
- Object.assign(queueData, summary.summary);
1211
- }
1212
-
1213
- return queueData;
1214
- } catch (error) {
1215
- console.error(`获取队列 ${queueName} 数据失败:`, error);
1216
- return {
1217
- key: queueName,
1218
- name: queueName,
1219
- messages: 0,
1220
- messages_ready: 0,
1221
- messages_unacknowledged: 0,
1222
- consumers: 0,
1223
- success_count: 0,
1224
- failed_count: 0,
1225
- running_tasks: 0
1226
- };
1227
- }
1228
- });
1229
-
1230
- const queuesData = await Promise.all(queuePromises);
1231
- setQueues(queuesData);
1232
-
1233
- // 更新Worker列表
1234
- const allWorkers = [];
1235
- Object.entries(allQueues).forEach(([queueName, queueWorkers]) => {
1236
- queueWorkers.forEach((worker, index) => {
1237
- allWorkers.push({
1238
- key: `${queueName}-${index}`,
1239
- ...worker,
1240
- queue: queueName
1241
- });
1242
- });
1243
- });
1244
- setWorkers(allWorkers);
1245
- }
1246
-
1247
- // 更新全局统计
1248
- try {
1249
- const globalResponse = await fetch('/api/global-stats');
1250
- if (globalResponse.ok) {
1251
- const stats = await globalResponse.json();
1252
- setGlobalStats(stats);
1253
- updateChartsData();
1254
- }
1255
- } catch (error) {
1256
- console.error('获取全局统计失败:', error);
1257
- }
1258
- };
1259
-
1260
- // 刷新数据
1261
- const refreshData = async () => {
1262
- setLoading(true);
1263
- try {
1264
- // 获取队列列表
1265
- const queuesResponse = await fetch('/api/queues');
1266
- const queuesData = await queuesResponse.json();
1267
-
1268
- // 为每个队列获取详细信息
1269
- const detailedQueues = await Promise.all(
1270
- (queuesData.queues || []).map(async (queueName) => {
1271
- const [workersResponse, statsResponse, summaryResponse] = await Promise.all([
1272
- fetch(`/api/queue/${encodeURIComponent(queueName)}/workers`),
1273
- fetch(`/api/queue/${encodeURIComponent(queueName)}/stats`),
1274
- fetch(`/api/queue/${encodeURIComponent(queueName)}/worker-summary`)
1275
- ]);
1276
-
1277
- let queueData = { key: queueName, name: queueName, workers: [] };
1278
-
1279
- if (workersResponse.ok) {
1280
- const workersData = await workersResponse.json();
1281
- queueData.workers = workersData.workers || [];
1282
- }
1283
-
1284
- if (statsResponse.ok) {
1285
- const stats = await statsResponse.json();
1286
- Object.assign(queueData, stats);
1287
- }
1288
-
1289
- if (summaryResponse.ok) {
1290
- const summary = await summaryResponse.json();
1291
- Object.assign(queueData, summary.summary);
1292
- }
1293
-
1294
- return queueData;
1295
- })
1296
- );
1297
-
1298
- setQueues(detailedQueues);
1299
-
1300
- // 汇总所有Worker
1301
- const allWorkers = [];
1302
- detailedQueues.forEach(queue => {
1303
- queue.workers.forEach((worker, index) => {
1304
- allWorkers.push({
1305
- key: `${queue.name}-${index}`,
1306
- ...worker,
1307
- queue: queue.name
1308
- });
1309
- });
1310
- });
1311
- setWorkers(allWorkers);
1312
-
1313
- // 获取全局统计
1314
- const globalResponse = await fetch('/api/global-stats');
1315
- if (globalResponse.ok) {
1316
- const stats = await globalResponse.json();
1317
- setGlobalStats(stats);
1318
- }
1319
-
1320
- } catch (error) {
1321
- console.error('刷新数据失败:', error);
1322
- message.error('数据加载失败');
1323
- } finally {
1324
- setLoading(false);
1325
- }
1326
- };
1327
-
1328
- // 查看队列详情
1329
- const viewQueueDetails = (queue) => {
1330
- window.location.href = `queue.html?name=${encodeURIComponent(queue.name)}`;
1331
- };
1332
-
1333
- // 切换统计模式
1334
- const toggleStatsMode = (checked) => {
1335
- setShowAllStats(checked);
1336
- localStorage.setItem('statsMode', checked ? 'all' : 'online');
1337
- refreshData();
1338
- };
1339
-
1340
-
1341
- // 显示Worker历史
1342
- const showWorkerHistory = () => {
1343
- Modal.info({
1344
- title: '提示',
1345
- content: 'Worker历史记录功能开发中...',
1346
- });
1347
- };
1348
-
1349
- // 组件初始化
1350
- useEffect(() => {
1351
- // 读取用户设置
1352
- setShowAllStats(localStorage.getItem('statsMode') !== 'online');
1353
-
1354
- // 连接WebSocket
1355
- connectWebSocket();
1356
-
1357
- // 初始化数据
1358
- refreshData();
1359
-
1360
- // 初始化图表
1361
- const timer = setTimeout(initCharts, 100);
1362
-
1363
- return () => {
1364
- clearTimeout(timer);
1365
- if (wsRef.current) {
1366
- wsRef.current.close();
1367
- }
1368
- if (tasksChartInstance.current) {
1369
- tasksChartInstance.current.destroy();
1370
- }
1371
- };
1372
- }, []);
1373
-
1374
-
1375
- // 初始化图表并设置定期更新
1376
- useEffect(() => {
1377
- if (queues.length > 0) {
1378
- // 初次初始化图表
1379
- const initTimer = setTimeout(async () => {
1380
- await initCharts();
1381
- }, 100);
1382
-
1383
- // 设置定期更新队列趋势图(每30秒更新一次)
1384
- const updateInterval = setInterval(async () => {
1385
- if (tasksChartInstance.current && !chartLoading) {
1386
- try {
1387
- const timelines = await fetchQueueTimelines();
1388
-
1389
- // 处理时间线数据并直接更新数据点
1390
- timelines.forEach((item, index) => {
1391
- if (!item.timeline || !tasksChartInstance.current.data.datasets[index]) return;
1392
-
1393
- const timelinePoints = item.timeline.timeline || [];
1394
- let dataPoints = timelinePoints.map(point => point.count || 0);
1395
-
1396
- // 调整数据点数量
1397
- const labels = tasksChartInstance.current.data.labels;
1398
- while (dataPoints.length < labels.length) {
1399
- dataPoints.unshift(0);
1400
- }
1401
- if (dataPoints.length > labels.length) {
1402
- dataPoints.splice(0, dataPoints.length - labels.length);
1403
- }
1404
-
1405
- // 直接更新现有数据集的数据,而不是替换整个数据集
1406
- tasksChartInstance.current.data.datasets[index].data = dataPoints;
1407
- });
1408
-
1409
- // 使用最小化的更新方式
1410
- tasksChartInstance.current.update('none');
1411
- } catch (error) {
1412
- console.error('Failed to update queue timeline:', error);
1413
- }
1414
- }
1415
- }, 30000); // 30秒更新一次
1416
-
1417
- return () => {
1418
- clearTimeout(initTimer);
1419
- clearInterval(updateInterval);
1420
- };
1421
- }
1422
- }, [queues.length]); // 只依赖队列数量变化
1423
-
1424
- // 监听时间范围变化并更新图表
1425
- useEffect(() => {
1426
- if (queues.length > 0 && tasksChartInstance.current) {
1427
- const updateChartData = async () => {
1428
- setChartLoading(true);
1429
- try {
1430
- await updateChartsData();
1431
- } finally {
1432
- setChartLoading(false);
1433
- }
1434
- };
1435
- updateChartData();
1436
- }
1437
- }, [chartTimeRange, customTimeRange, queues.length]);
1438
-
1439
- // 点击外部关闭时间选择器
1440
- useEffect(() => {
1441
- const handleClickOutside = (event) => {
1442
- if (showCustomPicker && !event.target.closest('.time-picker-container')) {
1443
- setShowCustomPicker(false);
1444
- }
1445
- };
1446
-
1447
- document.addEventListener('click', handleClickOutside);
1448
- return () => {
1449
- document.removeEventListener('click', handleClickOutside);
1450
- };
1451
- }, [showCustomPicker]);
1452
-
1453
- return (
1454
- <ConfigProvider>
1455
- <div className="app-container">
1456
- <Layout style={{ minHeight: '100vh', background: 'transparent' }}>
1457
- {/* 导航栏 */}
1458
- <div style={{
1459
- background: 'rgba(20, 20, 20, 0.8)',
1460
- backdropFilter: 'blur(20px)',
1461
- borderBottom: '1px solid rgba(255, 255, 255, 0.08)',
1462
- padding: '16px 0',
1463
- position: 'sticky',
1464
- top: 0,
1465
- zIndex: 1000
1466
- }}>
1467
- <div style={{
1468
- maxWidth: 1400,
1469
- margin: '0 auto',
1470
- padding: '0 30px',
1471
- display: 'flex',
1472
- alignItems: 'center',
1473
- justifyContent: 'space-between'
1474
- }}>
1475
- <div style={{
1476
- color: 'white',
1477
- fontSize: '1.5rem',
1478
- fontWeight: 700,
1479
- display: 'flex',
1480
- alignItems: 'center',
1481
- gap: 12
1482
- }}>
1483
- <i className="fas fa-rocket" style={{fontSize: '1.5rem'}} />
1484
- Jettask Monitor
1485
- </div>
1486
- <Space size="large">
1487
- <Space size="large">
1488
- <Button type="primary" size="large">
1489
- 概览
1490
- </Button>
1491
- <Button type="text" size="large" onClick={() => window.location.href = '/queues.html'}>
1492
- 队列
1493
- </Button>
1494
- <Button type="text" size="large" onClick={() => window.location.href = '/workers.html'}>
1495
- Workers
1496
- </Button>
1497
- </Space>
1498
- <Button
1499
- type="default"
1500
- size="large"
1501
- icon={<i className="fas fa-sync-alt" style={{marginRight: 8}} />}
1502
- onClick={refreshData}
1503
- loading={loading}
1504
- style={{
1505
- background: 'rgba(255, 255, 255, 0.1)',
1506
- border: '1px solid rgba(255, 255, 255, 0.2)',
1507
- color: 'rgba(255, 255, 255, 0.85)'
1508
- }}
1509
- >
1510
- 刷新
1511
- </Button>
1512
- </Space>
1513
- </div>
1514
- </div>
1515
-
1516
- <Content className="main-content">
1517
- {/* 统计概览卡片 */}
1518
- <Row gutter={[24, 24]} style={{ marginBottom: '40px' }}>
1519
- {statsCards.map((stat, index) => (
1520
- <Col key={index} xs={24} sm={12} md={8} lg={6}>
1521
- <div className="stats-card fade-in-up" style={{ animationDelay: `${index * 100}ms` }}>
1522
- <Card style={{ height: '100%', border: 'none' }}>
1523
- <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '16px' }}>
1524
- <div style={{ flex: 1 }}>
1525
- <Statistic
1526
- title={stat.title}
1527
- value={stat.value}
1528
- valueStyle={{ color: stat.color }}
1529
- suffix={stat.suffix}
1530
- />
1531
- </div>
1532
- <div style={{
1533
- background: stat.color,
1534
- borderRadius: '12px',
1535
- padding: '12px',
1536
- color: 'white',
1537
- fontSize: '24px',
1538
- boxShadow: `0 4px 12px ${stat.color}30`
1539
- }}>
1540
- {stat.icon}
1541
- </div>
1542
- </div>
1543
- {stat.trend && (
1544
- <div style={{ display: 'flex', alignItems: 'center', fontSize: '0.75rem' }}>
1545
- <Tag color={stat.trend.type} size="small" style={{ marginRight: '8px' }}>
1546
- {stat.trend.text}
1547
- </Tag>
1548
- <Text type="secondary">{stat.trend.desc}</Text>
1549
- </div>
1550
- )}
1551
- </Card>
1552
- </div>
1553
- </Col>
1554
- ))}
1555
- </Row>
1556
-
1557
- {/* 图表区域 */}
1558
- <Row gutter={[24, 24]} style={{ marginBottom: '40px' }}>
1559
- {/* 任务趋势图表 */}
1560
- <Col xs={24}>
1561
- <div className="chart-card">
1562
- <Card
1563
- title={
1564
- <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
1565
- <Title level={4} className="gradient-text">
1566
- 📈 队列处理趋势
1567
- </Title>
1568
- <div className="time-picker-container" style={{ display: 'flex', alignItems: 'center', gap: '8px', position: 'relative', zIndex: 1050, overflow: 'visible' }}>
1569
- <Button
1570
- size="small"
1571
- icon={<i className="far fa-clock" />}
1572
- onClick={(e) => {
1573
- const rect = e.currentTarget.getBoundingClientRect();
1574
- setPickerPosition({
1575
- top: rect.bottom + 10,
1576
- left: rect.right - 320 // 320是下拉框的宽度
1577
- });
1578
- setShowCustomPicker(!showCustomPicker);
1579
- }}
1580
- style={{
1581
- borderRadius: '4px',
1582
- padding: '4px 12px',
1583
- display: 'flex',
1584
- alignItems: 'center',
1585
- gap: '6px',
1586
- backgroundColor: customTimeRange ? '#1890ff' : 'transparent',
1587
- color: customTimeRange ? '#fff' : 'inherit',
1588
- border: '1px solid rgba(255, 255, 255, 0.2)'
1589
- }}
1590
- >
1591
- {customTimeRange ?
1592
- `${moment(customTimeRange[0]).local().format('MM-DD HH:mm')} ~ ${moment(customTimeRange[1]).local().format('MM-DD HH:mm')}` :
1593
- (chartTimeRange === '1h' ? '最近1小时' :
1594
- chartTimeRange === '3h' ? '最近3小时' :
1595
- chartTimeRange === '6h' ? '最近6小时' :
1596
- chartTimeRange === '12h' ? '最近12小时' :
1597
- chartTimeRange === '24h' ? '最近24小时' :
1598
- chartTimeRange === '3d' ? '最近3天' :
1599
- chartTimeRange === '7d' ? '最近7天' : '选择时间')
1600
- }
1601
- <i className="fas fa-chevron-down" style={{ fontSize: '10px' }} />
1602
- </Button>
1603
-
1604
- {showCustomPicker && ReactDOM.createPortal(
1605
- <div style={{
1606
- position: 'fixed',
1607
- top: `${pickerPosition.top}px`,
1608
- left: `${pickerPosition.left}px`,
1609
- zIndex: 99999,
1610
- backgroundColor: 'rgba(31, 31, 31, 0.98)',
1611
- border: '1px solid rgba(255, 255, 255, 0.2)',
1612
- borderRadius: '8px',
1613
- padding: '16px',
1614
- boxShadow: '0 8px 32px rgba(0, 0, 0, 0.8)',
1615
- minWidth: '320px',
1616
- backdropFilter: 'blur(10px)'
1617
- }}>
1618
- <div style={{ marginBottom: '12px' }}>
1619
- <Text style={{ color: 'rgba(255, 255, 255, 0.65)', fontSize: '12px' }}>快速选择</Text>
1620
- <div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px', marginTop: '8px' }}>
1621
- {[
1622
- { value: '15m', label: '最近15分钟' },
1623
- { value: '30m', label: '最近30分钟' },
1624
- { value: '1h', label: '最近1小时' },
1625
- { value: '3h', label: '最近3小时' },
1626
- { value: '6h', label: '最近6小时' },
1627
- { value: '12h', label: '最近12小时' },
1628
- { value: '24h', label: '最近24小时' },
1629
- { value: '3d', label: '最近3天' },
1630
- { value: '7d', label: '最近7天' },
1631
- { value: '30d', label: '最近30天' }
1632
- ].map(item => (
1633
- <Button
1634
- key={item.value}
1635
- size="small"
1636
- type={chartTimeRange === item.value && !customTimeRange ? 'primary' : 'default'}
1637
- onClick={() => {
1638
- setChartTimeRange(item.value);
1639
- setCustomTimeRange(null);
1640
- setShowCustomPicker(false);
1641
- }}
1642
- style={{
1643
- fontSize: '12px',
1644
- padding: '2px 8px',
1645
- height: '24px'
1646
- }}
1647
- >
1648
- {item.label}
1649
- </Button>
1650
- ))}
1651
- </div>
1652
- </div>
1653
-
1654
- <Divider style={{ margin: '12px 0' }} />
1655
-
1656
- <div>
1657
- <Text style={{ color: 'rgba(255, 255, 255, 0.65)', fontSize: '12px' }}>自定义时间范围</Text>
1658
- <DatePicker.RangePicker
1659
- showTime={{
1660
- format: 'HH:mm'
1661
- }}
1662
- format="YYYY-MM-DD HH:mm"
1663
- size="small"
1664
- placeholder={['开始时间', '结束时间']}
1665
- value={customTimeRange}
1666
- onChange={(dates) => {
1667
- setCustomTimeRange(dates);
1668
- if (dates) {
1669
- setChartTimeRange('custom');
1670
- setShowCustomPicker(false);
1671
- }
1672
- }}
1673
- style={{ marginTop: '8px', width: '100%' }}
1674
- />
1675
- </div>
1676
- </div>,
1677
- document.body
1678
- )}
1679
- </div>
1680
- </div>
1681
- }
1682
- style={{ height: '100%', overflow: 'visible' }}
1683
- >
1684
- <div className="chart-container" style={{ position: 'relative', minHeight: '400px' }}>
1685
- <canvas ref={tasksChartRef} />
1686
- {chartLoading && (
1687
- <div style={{
1688
- position: 'absolute',
1689
- top: 0,
1690
- left: 0,
1691
- right: 0,
1692
- bottom: 0,
1693
- backgroundColor: 'rgba(0, 0, 0, 0.6)',
1694
- display: 'flex',
1695
- alignItems: 'center',
1696
- justifyContent: 'center',
1697
- zIndex: 10
1698
- }}>
1699
- <Spin size="large" tip="加载中..." />
1700
- </div>
1701
- )}
1702
- </div>
1703
- </Card>
1704
- </div>
1705
- </Col>
1706
-
1707
- </Row>
1708
-
1709
- </Content>
1710
-
1711
- {/* 连接状态指示器 */}
1712
- <div className="connection-status">
1713
- <Tag
1714
- color={connectionStatus === 'connected' ? 'green' : 'red'}
1715
- icon={connectionStatus === 'connected' ?
1716
- <i className="fas fa-check-circle" /> :
1717
- <i className="fas fa-times-circle" />
1718
- }
1719
- style={{ fontSize: '14px', padding: '8px 16px' }}
1720
- >
1721
- {connectionStatus === 'connected' ? '已连接' : '连接断开'}
1722
- </Tag>
1723
- </div>
1724
- </Layout>
1725
- </div>
1726
- </ConfigProvider>
1727
- );
1728
- };
1729
-
1730
- // 渲染应用
1731
- ReactDOM.render(React.createElement(App), document.getElementById('root'));
1732
- </script>
1733
- </body>
1734
- </html>