jettask 0.2.7__py3-none-any.whl → 0.2.9__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- jettask/core/cli.py +242 -0
- jettask/pg_consumer/sql/add_execution_time_field.sql +29 -0
- jettask/pg_consumer/sql/create_new_tables.sql +137 -0
- jettask/pg_consumer/sql/create_tables_v3.sql +175 -0
- jettask/pg_consumer/sql/migrate_to_new_structure.sql +179 -0
- jettask/pg_consumer/sql/modify_time_fields.sql +69 -0
- jettask/webui/frontend/package.json +30 -0
- jettask/webui/frontend/src/App.css +109 -0
- jettask/webui/frontend/src/App.jsx +66 -0
- jettask/webui/frontend/src/components/NamespaceSelector.jsx +166 -0
- jettask/webui/frontend/src/components/QueueBacklogChart.jsx +298 -0
- jettask/webui/frontend/src/components/QueueBacklogTrend.jsx +638 -0
- jettask/webui/frontend/src/components/QueueDetailsTable.css +65 -0
- jettask/webui/frontend/src/components/QueueDetailsTable.jsx +487 -0
- jettask/webui/frontend/src/components/QueueDetailsTableV2.jsx +465 -0
- jettask/webui/frontend/src/components/ScheduledTaskFilter.jsx +423 -0
- jettask/webui/frontend/src/components/TaskFilter.jsx +425 -0
- jettask/webui/frontend/src/components/TimeRangeSelector.css +21 -0
- jettask/webui/frontend/src/components/TimeRangeSelector.jsx +160 -0
- jettask/webui/frontend/src/components/charts/QueueChart.jsx +111 -0
- jettask/webui/frontend/src/components/charts/QueueTrendChart.jsx +115 -0
- jettask/webui/frontend/src/components/charts/WorkerChart.jsx +40 -0
- jettask/webui/frontend/src/components/common/StatsCard.jsx +18 -0
- jettask/webui/frontend/src/components/layout/AppLayout.css +95 -0
- jettask/webui/frontend/src/components/layout/AppLayout.jsx +49 -0
- jettask/webui/frontend/src/components/layout/Header.css +106 -0
- jettask/webui/frontend/src/components/layout/Header.jsx +106 -0
- jettask/webui/frontend/src/components/layout/SideMenu.css +137 -0
- jettask/webui/frontend/src/components/layout/SideMenu.jsx +209 -0
- jettask/webui/frontend/src/components/layout/TabsNav.css +244 -0
- jettask/webui/frontend/src/components/layout/TabsNav.jsx +206 -0
- jettask/webui/frontend/src/components/layout/UserInfo.css +197 -0
- jettask/webui/frontend/src/components/layout/UserInfo.jsx +197 -0
- jettask/webui/frontend/src/contexts/LoadingContext.jsx +27 -0
- jettask/webui/frontend/src/contexts/NamespaceContext.jsx +72 -0
- jettask/webui/frontend/src/contexts/TabsContext.backup.jsx +245 -0
- jettask/webui/frontend/src/index.css +114 -0
- jettask/webui/frontend/src/main.jsx +20 -0
- jettask/webui/frontend/src/pages/Alerts.jsx +684 -0
- jettask/webui/frontend/src/pages/Dashboard/index.css +35 -0
- jettask/webui/frontend/src/pages/Dashboard/index.jsx +281 -0
- jettask/webui/frontend/src/pages/Dashboard.jsx +1330 -0
- jettask/webui/frontend/src/pages/QueueDetail.jsx +1117 -0
- jettask/webui/frontend/src/pages/QueueMonitor.jsx +527 -0
- jettask/webui/frontend/src/pages/Queues.jsx +12 -0
- jettask/webui/frontend/src/pages/ScheduledTasks.jsx +809 -0
- jettask/webui/frontend/src/pages/Settings.jsx +800 -0
- jettask/webui/frontend/src/pages/Workers.jsx +12 -0
- jettask/webui/frontend/src/services/api.js +114 -0
- jettask/webui/frontend/src/services/queueTrend.js +152 -0
- jettask/webui/frontend/src/utils/suppressWarnings.js +22 -0
- jettask/webui/frontend/src/utils/userPreferences.js +154 -0
- jettask/webui/frontend/vite.config.js +26 -0
- {jettask-0.2.7.dist-info → jettask-0.2.9.dist-info}/METADATA +1 -1
- {jettask-0.2.7.dist-info → jettask-0.2.9.dist-info}/RECORD +59 -14
- jettask/webui/static/dist/assets/index-7129cfe1.css +0 -1
- jettask/webui/static/dist/assets/index-8d1935cc.js +0 -774
- jettask/webui/static/dist/index.html +0 -15
- jettask/webui/static/index.html +0 -1734
- jettask/webui/static/queue.html +0 -981
- jettask/webui/static/queues.html +0 -549
- jettask/webui/static/workers.html +0 -734
- {jettask-0.2.7.dist-info → jettask-0.2.9.dist-info}/WHEEL +0 -0
- {jettask-0.2.7.dist-info → jettask-0.2.9.dist-info}/entry_points.txt +0 -0
- {jettask-0.2.7.dist-info → jettask-0.2.9.dist-info}/licenses/LICENSE +0 -0
- {jettask-0.2.7.dist-info → jettask-0.2.9.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,1117 @@
|
|
1
|
+
import { useState, useEffect, useRef, useCallback } from 'react';
|
2
|
+
import { useParams, useNavigate, useSearchParams } from 'react-router-dom';
|
3
|
+
import { Card, Space, Button, Tag, message, Breadcrumb, Spin, Input, Empty, Select, Tooltip, Row, Col, Modal, Radio } from 'antd';
|
4
|
+
import { QuestionCircleOutlined, ShareAltOutlined, CopyOutlined, ClearOutlined, FileTextOutlined, CheckCircleOutlined, ExclamationCircleOutlined, LineChartOutlined, AreaChartOutlined, EyeOutlined } from '@ant-design/icons';
|
5
|
+
import ProTable from '@ant-design/pro-table';
|
6
|
+
import { Line } from '@ant-design/plots';
|
7
|
+
import { G2 } from "@ant-design/plots";
|
8
|
+
import TimeRangeSelector from '../components/TimeRangeSelector';
|
9
|
+
import TaskFilter from '../components/TaskFilter';
|
10
|
+
import QueueBacklogTrend from '../components/QueueBacklogTrend';
|
11
|
+
import { getQueueFilters, saveQueueFilters, clearQueueFilters } from '../utils/userPreferences';
|
12
|
+
import { useNamespace } from '../contexts/NamespaceContext';
|
13
|
+
import { useLoading } from '../contexts/LoadingContext';
|
14
|
+
import dayjs from 'dayjs';
|
15
|
+
import axios from 'axios';
|
16
|
+
|
17
|
+
const { Search } = Input;
|
18
|
+
|
19
|
+
// 任务状态配置
|
20
|
+
const STATUS_CONFIG = {
|
21
|
+
'pending': { color: 'gold', label: '待处理' },
|
22
|
+
'running': { color: 'blue', label: '运行中' },
|
23
|
+
'success': { color: 'green', label: '成功' },
|
24
|
+
'error': { color: 'red', label: '失败' },
|
25
|
+
'rejected': { color: 'purple', label: '拒绝' },
|
26
|
+
};
|
27
|
+
|
28
|
+
function QueueDetail() {
|
29
|
+
const { queueName } = useParams();
|
30
|
+
const navigate = useNavigate();
|
31
|
+
const [searchParams, setSearchParams] = useSearchParams();
|
32
|
+
const { currentNamespace } = useNamespace();
|
33
|
+
const { setLoading: setGlobalLoading } = useLoading();
|
34
|
+
const decodedQueueName = decodeURIComponent(queueName);
|
35
|
+
|
36
|
+
// 初始化状态:优先使用localStorage记忆,其次使用URL参数
|
37
|
+
const getInitialState = () => {
|
38
|
+
// 先尝试从localStorage获取记忆的设置
|
39
|
+
const savedSettings = getQueueFilters(decodedQueueName);
|
40
|
+
|
41
|
+
// 检查URL参数
|
42
|
+
const urlFilters = searchParams.get('filters');
|
43
|
+
const urlTimeRange = searchParams.get('timeRange');
|
44
|
+
const urlCustomStart = searchParams.get('startTime');
|
45
|
+
const urlCustomEnd = searchParams.get('endTime');
|
46
|
+
const scheduledTaskId = searchParams.get('scheduled_task_id');
|
47
|
+
|
48
|
+
// 如果有scheduled_task_id参数,创建对应的筛选条件
|
49
|
+
let initialFilters = [];
|
50
|
+
if (scheduledTaskId) {
|
51
|
+
initialFilters = [{
|
52
|
+
field: 'scheduled_task_id',
|
53
|
+
operator: 'eq',
|
54
|
+
value: parseInt(scheduledTaskId)
|
55
|
+
}];
|
56
|
+
} else if (urlFilters) {
|
57
|
+
try {
|
58
|
+
initialFilters = JSON.parse(decodeURIComponent(urlFilters));
|
59
|
+
} catch (e) {
|
60
|
+
console.error('Failed to parse filters from URL:', e);
|
61
|
+
}
|
62
|
+
} else {
|
63
|
+
initialFilters = savedSettings.filters;
|
64
|
+
}
|
65
|
+
|
66
|
+
// 如果有URL参数,优先使用URL参数(分享链接场景)
|
67
|
+
if (urlFilters || urlTimeRange || (urlCustomStart && urlCustomEnd) || scheduledTaskId) {
|
68
|
+
let initialTimeRange = urlTimeRange || savedSettings.timeRange || '1h';
|
69
|
+
let initialCustomTimeRange = null;
|
70
|
+
|
71
|
+
if (urlCustomStart && urlCustomEnd) {
|
72
|
+
initialCustomTimeRange = [dayjs(urlCustomStart), dayjs(urlCustomEnd)];
|
73
|
+
initialTimeRange = 'custom';
|
74
|
+
}
|
75
|
+
|
76
|
+
return { initialFilters, initialTimeRange, initialCustomTimeRange };
|
77
|
+
}
|
78
|
+
|
79
|
+
// 否则使用localStorage中的记忆设置
|
80
|
+
let initialCustomTimeRange = null;
|
81
|
+
if (savedSettings.customTimeRange && savedSettings.customTimeRange.length === 2) {
|
82
|
+
initialCustomTimeRange = [
|
83
|
+
dayjs(savedSettings.customTimeRange[0]),
|
84
|
+
dayjs(savedSettings.customTimeRange[1])
|
85
|
+
];
|
86
|
+
}
|
87
|
+
|
88
|
+
return {
|
89
|
+
initialFilters: savedSettings.filters,
|
90
|
+
initialTimeRange: savedSettings.timeRange,
|
91
|
+
initialCustomTimeRange
|
92
|
+
};
|
93
|
+
};
|
94
|
+
|
95
|
+
const { initialFilters, initialTimeRange, initialCustomTimeRange } = getInitialState();
|
96
|
+
|
97
|
+
const [loading, setLoading] = useState(false);
|
98
|
+
const [timeRange, setTimeRange] = useState(initialTimeRange);
|
99
|
+
const [customTimeRange, setCustomTimeRange] = useState(initialCustomTimeRange);
|
100
|
+
const [chartData, setChartData] = useState([]);
|
101
|
+
const [granularity, setGranularity] = useState('');
|
102
|
+
const [refreshing, setRefreshing] = useState(false);
|
103
|
+
const [tableKey, setTableKey] = useState(0); // 用于强制刷新表格
|
104
|
+
const [chartType, setChartType] = useState('flowRate'); // 'flowRate' | 'backlog'
|
105
|
+
|
106
|
+
// 用于防抖的 ref
|
107
|
+
const fetchTimeoutRef = useRef(null);
|
108
|
+
const isBrushingRef = useRef(false);
|
109
|
+
|
110
|
+
// 筛选条件状态
|
111
|
+
const [filterState, setFilterState] = useState({
|
112
|
+
status: null,
|
113
|
+
taskId: '',
|
114
|
+
workerId: '',
|
115
|
+
});
|
116
|
+
const [filters, setFilters] = useState(initialFilters);
|
117
|
+
|
118
|
+
// 任务详细数据相关状态
|
119
|
+
const [taskDetailsCache, setTaskDetailsCache] = useState({});
|
120
|
+
const [loadingTaskDetails, setLoadingTaskDetails] = useState({});
|
121
|
+
const [detailModalVisible, setDetailModalVisible] = useState(false);
|
122
|
+
const [selectedTaskDetail, setSelectedTaskDetail] = useState(null);
|
123
|
+
const [selectedTaskId, setSelectedTaskId] = useState(null);
|
124
|
+
|
125
|
+
// 计算表格高度的hooks
|
126
|
+
const [tableHeight, setTableHeight] = useState(400);
|
127
|
+
const actionRef = useRef();
|
128
|
+
|
129
|
+
useEffect(() => {
|
130
|
+
const updateTableHeight = () => {
|
131
|
+
// 视窗高度 - 面包屑(约50px) - 图表卡片(约350px) - 页边距(约50px) - ProTable工具栏和分页(约150px)
|
132
|
+
const availableHeight = window.innerHeight - 540;
|
133
|
+
const minHeight = 300; // 最小高度
|
134
|
+
const calculatedHeight = Math.max(availableHeight, minHeight);
|
135
|
+
setTableHeight(calculatedHeight);
|
136
|
+
};
|
137
|
+
|
138
|
+
// 初始计算
|
139
|
+
updateTableHeight();
|
140
|
+
|
141
|
+
// 监听窗口大小变化
|
142
|
+
window.addEventListener('resize', updateTableHeight);
|
143
|
+
return () => window.removeEventListener('resize', updateTableHeight);
|
144
|
+
}, []);
|
145
|
+
|
146
|
+
// 获取队列流量速率数据
|
147
|
+
const fetchQueueTimeline = useCallback(async () => {
|
148
|
+
if (!decodedQueueName || !currentNamespace) {
|
149
|
+
return;
|
150
|
+
}
|
151
|
+
|
152
|
+
setLoading(true);
|
153
|
+
try {
|
154
|
+
// 构建筛选条件(合并旧的搜索参数和新的筛选条件)
|
155
|
+
const combinedFilters = [...filters];
|
156
|
+
|
157
|
+
// 添加旧的搜索参数作为筛选条件(为了兼容性)
|
158
|
+
if (filterState.status) {
|
159
|
+
combinedFilters.push({ field: 'status', operator: 'eq', value: filterState.status });
|
160
|
+
}
|
161
|
+
if (filterState.taskId) {
|
162
|
+
combinedFilters.push({ field: 'id', operator: 'contains', value: filterState.taskId });
|
163
|
+
}
|
164
|
+
if (filterState.workerId) {
|
165
|
+
combinedFilters.push({ field: 'worker_id', operator: 'contains', value: filterState.workerId });
|
166
|
+
}
|
167
|
+
|
168
|
+
const params = {
|
169
|
+
namespace: currentNamespace || 'default', // 添加命名空间
|
170
|
+
queues: [decodedQueueName],
|
171
|
+
time_range: timeRange,
|
172
|
+
filters: combinedFilters, // 传递筛选条件
|
173
|
+
};
|
174
|
+
|
175
|
+
if (customTimeRange && customTimeRange.length === 2) {
|
176
|
+
params.start_time = customTimeRange[0].toISOString();
|
177
|
+
params.end_time = customTimeRange[1].toISOString();
|
178
|
+
}
|
179
|
+
|
180
|
+
// 使用支持命名空间的新接口
|
181
|
+
const namespace = currentNamespace || 'default';
|
182
|
+
const response = await axios.post(`/api/data/queue-flow-rates/${namespace}`, params);
|
183
|
+
const { data, granularity: dataGranularity } = response.data;
|
184
|
+
|
185
|
+
|
186
|
+
// 打印前几个数据点来调试
|
187
|
+
if (data && data.length > 0) {
|
188
|
+
// 检查数据一致性
|
189
|
+
const metrics = [...new Set(data.map(d => d.metric))];
|
190
|
+
|
191
|
+
// 按metric分组统计
|
192
|
+
const metricCounts = {};
|
193
|
+
metrics.forEach(metric => {
|
194
|
+
metricCounts[metric] = data.filter(d => d.metric === metric).length;
|
195
|
+
});
|
196
|
+
}
|
197
|
+
|
198
|
+
setChartData(data || []);
|
199
|
+
setGranularity(dataGranularity || 'minute');
|
200
|
+
} catch (error) {
|
201
|
+
console.error('Failed to fetch queue timeline:', error);
|
202
|
+
|
203
|
+
// 显示服务端返回的错误消息
|
204
|
+
const errorMessage = error.response?.data?.detail || error.message || '获取队列趋势数据失败';
|
205
|
+
message.error(errorMessage);
|
206
|
+
} finally {
|
207
|
+
setLoading(false);
|
208
|
+
isBrushingRef.current = false;
|
209
|
+
}
|
210
|
+
}, [decodedQueueName, timeRange, customTimeRange, filters, filterState]);
|
211
|
+
|
212
|
+
// ProTable的请求函数
|
213
|
+
const request = async (params, sort, filter) => {
|
214
|
+
setRefreshing(true);
|
215
|
+
try {
|
216
|
+
// 构建筛选条件(向后兼容旧的搜索参数)
|
217
|
+
const combinedFilters = [...filters];
|
218
|
+
|
219
|
+
// 添加旧的搜索参数作为筛选条件
|
220
|
+
if (filterState.status) {
|
221
|
+
combinedFilters.push({ field: 'status', operator: 'eq', value: filterState.status });
|
222
|
+
}
|
223
|
+
if (filterState.taskId) {
|
224
|
+
combinedFilters.push({ field: 'id', operator: 'contains', value: filterState.taskId });
|
225
|
+
}
|
226
|
+
if (filterState.workerId) {
|
227
|
+
combinedFilters.push({ field: 'worker_id', operator: 'contains', value: filterState.workerId });
|
228
|
+
}
|
229
|
+
|
230
|
+
// 构建请求参数,包含时间范围
|
231
|
+
const requestParams = {
|
232
|
+
namespace: currentNamespace || 'default', // 添加命名空间
|
233
|
+
queue_name: decodedQueueName,
|
234
|
+
page: params.current,
|
235
|
+
page_size: params.pageSize,
|
236
|
+
filters: combinedFilters,
|
237
|
+
time_range: timeRange, // 传递时间范围
|
238
|
+
};
|
239
|
+
|
240
|
+
// 如果有自定义时间范围,使用它
|
241
|
+
if (customTimeRange && customTimeRange.length === 2) {
|
242
|
+
requestParams.start_time = customTimeRange[0].toISOString();
|
243
|
+
requestParams.end_time = customTimeRange[1].toISOString();
|
244
|
+
}
|
245
|
+
|
246
|
+
// 处理排序
|
247
|
+
if (sort) {
|
248
|
+
const sortField = Object.keys(sort)[0];
|
249
|
+
if (sortField) {
|
250
|
+
requestParams.sort_field = sortField;
|
251
|
+
requestParams.sort_order = sort[sortField] === 'ascend' ? 'asc' : 'desc';
|
252
|
+
}
|
253
|
+
}
|
254
|
+
|
255
|
+
// 使用支持命名空间的新接口
|
256
|
+
const namespace = requestParams.namespace || 'default';
|
257
|
+
const response = await axios.post(`/api/data/tasks/${namespace}`, requestParams);
|
258
|
+
|
259
|
+
if (response.data.success) {
|
260
|
+
// 确保即使是空数据也能正确更新
|
261
|
+
const responseData = response.data.data || [];
|
262
|
+
console.log(`任务列表请求返回 ${responseData.length} 条数据`);
|
263
|
+
return {
|
264
|
+
data: responseData,
|
265
|
+
success: true,
|
266
|
+
total: response.data.total || 0,
|
267
|
+
};
|
268
|
+
}
|
269
|
+
console.log('任务列表请求失败');
|
270
|
+
return {
|
271
|
+
data: [],
|
272
|
+
success: true, // 改为true,确保ProTable接受空数据
|
273
|
+
total: 0,
|
274
|
+
};
|
275
|
+
} catch (error) {
|
276
|
+
console.error('Failed to fetch tasks:', error);
|
277
|
+
|
278
|
+
// 显示服务端返回的错误消息
|
279
|
+
const errorMessage = error.response?.data?.detail || error.message || '获取任务列表失败';
|
280
|
+
message.error(errorMessage);
|
281
|
+
|
282
|
+
return {
|
283
|
+
data: [],
|
284
|
+
success: true, // 即使出错也返回true,让ProTable清空数据
|
285
|
+
total: 0,
|
286
|
+
};
|
287
|
+
} finally {
|
288
|
+
setRefreshing(false);
|
289
|
+
}
|
290
|
+
};
|
291
|
+
|
292
|
+
// 初始化加载
|
293
|
+
// 合并的初始化和命名空间切换处理
|
294
|
+
useEffect(() => {
|
295
|
+
if (decodedQueueName && currentNamespace) {
|
296
|
+
// 根据当前视图类型决定刷新哪些数据
|
297
|
+
// if (chartType === 'flow') {
|
298
|
+
fetchQueueTimeline();
|
299
|
+
// }
|
300
|
+
// chartType === 'backlog' 时,QueueBacklogTrend组件会自己处理刷新
|
301
|
+
|
302
|
+
// 刷新表格
|
303
|
+
if (actionRef.current) {
|
304
|
+
actionRef.current.reload();
|
305
|
+
}
|
306
|
+
}
|
307
|
+
}, [decodedQueueName, currentNamespace]);
|
308
|
+
|
309
|
+
// 时间范围变化时重新加载(添加防抖)
|
310
|
+
useEffect(() => {
|
311
|
+
if (decodedQueueName) {
|
312
|
+
// 清除之前的定时器
|
313
|
+
if (fetchTimeoutRef.current) {
|
314
|
+
clearTimeout(fetchTimeoutRef.current);
|
315
|
+
}
|
316
|
+
|
317
|
+
// 如果是刷选触发的自定义时间范围,延迟一点获取数据
|
318
|
+
const delay = isBrushingRef.current ? 300 : 0;
|
319
|
+
|
320
|
+
fetchTimeoutRef.current = setTimeout(() => {
|
321
|
+
// 根据当前视图类型决定刷新哪些数据
|
322
|
+
if (chartType === 'flow') {
|
323
|
+
fetchQueueTimeline();
|
324
|
+
}
|
325
|
+
// chartType === 'backlog' 时,QueueBacklogTrend组件会自己处理刷新
|
326
|
+
|
327
|
+
// 刷新ProTable
|
328
|
+
if (actionRef.current) {
|
329
|
+
actionRef.current.reload();
|
330
|
+
}
|
331
|
+
}, delay);
|
332
|
+
}
|
333
|
+
|
334
|
+
// 清理函数
|
335
|
+
return () => {
|
336
|
+
if (fetchTimeoutRef.current) {
|
337
|
+
clearTimeout(fetchTimeoutRef.current);
|
338
|
+
}
|
339
|
+
};
|
340
|
+
}, [timeRange, customTimeRange, chartType]);
|
341
|
+
|
342
|
+
// 更新URL参数的函数
|
343
|
+
const updateURLParams = useCallback(() => {
|
344
|
+
const params = new URLSearchParams();
|
345
|
+
|
346
|
+
// 保存筛选条件
|
347
|
+
if (filters && filters.length > 0) {
|
348
|
+
params.set('filters', encodeURIComponent(JSON.stringify(filters)));
|
349
|
+
}
|
350
|
+
|
351
|
+
// 保存时间范围
|
352
|
+
if (timeRange !== '1h') {
|
353
|
+
params.set('timeRange', timeRange);
|
354
|
+
}
|
355
|
+
|
356
|
+
// 保存自定义时间范围
|
357
|
+
if (customTimeRange && customTimeRange.length === 2) {
|
358
|
+
params.set('startTime', customTimeRange[0].toISOString());
|
359
|
+
params.set('endTime', customTimeRange[1].toISOString());
|
360
|
+
}
|
361
|
+
|
362
|
+
// 更新URL但不触发导航
|
363
|
+
setSearchParams(params, { replace: true });
|
364
|
+
}, [filters, timeRange, customTimeRange, setSearchParams]);
|
365
|
+
|
366
|
+
// 生成分享链接
|
367
|
+
const generateShareLink = () => {
|
368
|
+
const currentURL = new URL(window.location.href);
|
369
|
+
const params = new URLSearchParams();
|
370
|
+
|
371
|
+
// 添加筛选条件
|
372
|
+
if (filters && filters.length > 0) {
|
373
|
+
params.set('filters', encodeURIComponent(JSON.stringify(filters)));
|
374
|
+
}
|
375
|
+
|
376
|
+
// 添加时间范围
|
377
|
+
if (timeRange !== '1h') {
|
378
|
+
params.set('timeRange', timeRange);
|
379
|
+
}
|
380
|
+
|
381
|
+
// 添加自定义时间范围
|
382
|
+
if (customTimeRange && customTimeRange.length === 2) {
|
383
|
+
params.set('startTime', customTimeRange[0].toISOString());
|
384
|
+
params.set('endTime', customTimeRange[1].toISOString());
|
385
|
+
}
|
386
|
+
|
387
|
+
currentURL.search = params.toString();
|
388
|
+
return currentURL.toString();
|
389
|
+
};
|
390
|
+
|
391
|
+
// 复制分享链接
|
392
|
+
const handleCopyShareLink = () => {
|
393
|
+
const shareLink = generateShareLink();
|
394
|
+
navigator.clipboard.writeText(shareLink).then(() => {
|
395
|
+
message.success('分享链接已复制到剪贴板');
|
396
|
+
}).catch(() => {
|
397
|
+
message.error('复制失败,请手动复制');
|
398
|
+
Modal.info({
|
399
|
+
title: '分享链接',
|
400
|
+
content: shareLink,
|
401
|
+
okText: '关闭',
|
402
|
+
});
|
403
|
+
});
|
404
|
+
};
|
405
|
+
|
406
|
+
// 分享筛选条件(ProTable用)
|
407
|
+
const shareFilters = () => {
|
408
|
+
handleCopyShareLink();
|
409
|
+
};
|
410
|
+
|
411
|
+
// 保存筛选条件(ProTable用)
|
412
|
+
const handleSaveFilters = () => {
|
413
|
+
const settings = {
|
414
|
+
filters,
|
415
|
+
timeRange,
|
416
|
+
customTimeRange: customTimeRange ? customTimeRange.map(t => t.toISOString()) : null,
|
417
|
+
};
|
418
|
+
saveQueueFilters(decodedQueueName, settings);
|
419
|
+
message.success('筛选条件已保存');
|
420
|
+
};
|
421
|
+
|
422
|
+
// 清除筛选条件(ProTable用)
|
423
|
+
const handleClearFilters = () => {
|
424
|
+
setFilters([]);
|
425
|
+
setTimeRange('1h');
|
426
|
+
setCustomTimeRange(null);
|
427
|
+
clearQueueFilters(decodedQueueName);
|
428
|
+
if (actionRef.current) {
|
429
|
+
actionRef.current.reload();
|
430
|
+
}
|
431
|
+
message.success('筛选条件已清除');
|
432
|
+
};
|
433
|
+
|
434
|
+
// 筛选条件或时间范围变化时更新URL和保存到localStorage
|
435
|
+
useEffect(() => {
|
436
|
+
updateURLParams();
|
437
|
+
|
438
|
+
// 保存到localStorage(用于记忆)
|
439
|
+
if (decodedQueueName) {
|
440
|
+
// 准备要保存的自定义时间范围
|
441
|
+
let customTimeRangeToSave = null;
|
442
|
+
if (customTimeRange && customTimeRange.length === 2) {
|
443
|
+
customTimeRangeToSave = [
|
444
|
+
customTimeRange[0].toISOString(),
|
445
|
+
customTimeRange[1].toISOString()
|
446
|
+
];
|
447
|
+
}
|
448
|
+
|
449
|
+
saveQueueFilters(decodedQueueName, {
|
450
|
+
filters,
|
451
|
+
timeRange,
|
452
|
+
customTimeRange: customTimeRangeToSave
|
453
|
+
});
|
454
|
+
}
|
455
|
+
}, [filters, timeRange, customTimeRange, updateURLParams, decodedQueueName]);
|
456
|
+
|
457
|
+
// 搜索参数或筛选条件变化时重新加载
|
458
|
+
useEffect(() => {
|
459
|
+
if (decodedQueueName) {
|
460
|
+
if (actionRef.current) {
|
461
|
+
actionRef.current.reload();
|
462
|
+
}
|
463
|
+
// 根据当前视图类型决定是否刷新图表
|
464
|
+
if (chartType === 'flow') {
|
465
|
+
fetchQueueTimeline();
|
466
|
+
}
|
467
|
+
}
|
468
|
+
}, [filterState, filters, chartType]);
|
469
|
+
|
470
|
+
|
471
|
+
// 处理刷新
|
472
|
+
const handleRefresh = () => {
|
473
|
+
// 根据当前视图类型决定刷新哪些数据
|
474
|
+
if (chartType === 'flow') {
|
475
|
+
fetchQueueTimeline();
|
476
|
+
}
|
477
|
+
// chartType === 'backlog' 时,QueueBacklogTrend组件会自己处理刷新
|
478
|
+
|
479
|
+
if (actionRef.current) {
|
480
|
+
actionRef.current.reload();
|
481
|
+
}
|
482
|
+
};
|
483
|
+
|
484
|
+
// 清除当前队列的记忆设置
|
485
|
+
const handleClearMemory = () => {
|
486
|
+
Modal.confirm({
|
487
|
+
title: '清除筛选记忆',
|
488
|
+
content: `确定要清除队列 "${decodedQueueName}" 的所有筛选记忆吗?这将重置所有筛选条件和时间范围。`,
|
489
|
+
okText: '确定',
|
490
|
+
cancelText: '取消',
|
491
|
+
onOk: () => {
|
492
|
+
clearQueueFilters(decodedQueueName);
|
493
|
+
// 重置为默认值
|
494
|
+
setFilters([]);
|
495
|
+
setTimeRange('1h');
|
496
|
+
setCustomTimeRange(null);
|
497
|
+
message.success('筛选记忆已清除');
|
498
|
+
// 刷新数据
|
499
|
+
fetchQueueTimeline();
|
500
|
+
if (actionRef.current) {
|
501
|
+
actionRef.current.reload();
|
502
|
+
}
|
503
|
+
}
|
504
|
+
});
|
505
|
+
};
|
506
|
+
|
507
|
+
|
508
|
+
// 获取单个任务的详细数据
|
509
|
+
const fetchTaskDetails = async (taskId, consumerGroup) => {
|
510
|
+
// 使用taskId和consumerGroup作为缓存键
|
511
|
+
const cacheKey = consumerGroup ? `${taskId}_${consumerGroup}` : taskId;
|
512
|
+
|
513
|
+
// 如果已经在缓存中,直接使用
|
514
|
+
if (taskDetailsCache[cacheKey]) {
|
515
|
+
return taskDetailsCache[cacheKey];
|
516
|
+
}
|
517
|
+
|
518
|
+
// 如果正在加载,避免重复请求
|
519
|
+
if (loadingTaskDetails[cacheKey]) {
|
520
|
+
return;
|
521
|
+
}
|
522
|
+
|
523
|
+
setLoadingTaskDetails(prev => ({ ...prev, [cacheKey]: true }));
|
524
|
+
|
525
|
+
try {
|
526
|
+
// 构建URL,如果有consumerGroup则添加为查询参数
|
527
|
+
const url = consumerGroup
|
528
|
+
? `/api/task/${taskId}/details?consumer_group=${encodeURIComponent(consumerGroup)}`
|
529
|
+
: `/api/task/${taskId}/details`;
|
530
|
+
const response = await axios.get(url);
|
531
|
+
|
532
|
+
if (response.data.success) {
|
533
|
+
const details = response.data.data;
|
534
|
+
setTaskDetailsCache(prev => ({ ...prev, [cacheKey]: details }));
|
535
|
+
return details;
|
536
|
+
}
|
537
|
+
} catch (error) {
|
538
|
+
message.error('获取任务详细数据失败');
|
539
|
+
console.error('Failed to fetch task details:', error);
|
540
|
+
} finally {
|
541
|
+
setLoadingTaskDetails(prev => ({ ...prev, [cacheKey]: false }));
|
542
|
+
}
|
543
|
+
};
|
544
|
+
|
545
|
+
// 处理查看任务详情
|
546
|
+
const handleViewTaskDetail = async (taskId, field, consumerGroup) => {
|
547
|
+
const details = await fetchTaskDetails(taskId, consumerGroup);
|
548
|
+
if (details) {
|
549
|
+
setSelectedTaskId(taskId);
|
550
|
+
let data;
|
551
|
+
if (field === 'task_data') {
|
552
|
+
data = details.task_data;
|
553
|
+
} else if (field === 'result') {
|
554
|
+
data = details.result;
|
555
|
+
} else if (field === 'error_message') {
|
556
|
+
data = details.error_message;
|
557
|
+
}
|
558
|
+
setSelectedTaskDetail({
|
559
|
+
field: field,
|
560
|
+
data: data,
|
561
|
+
consumerGroup: consumerGroup
|
562
|
+
});
|
563
|
+
setDetailModalVisible(true);
|
564
|
+
}
|
565
|
+
};
|
566
|
+
|
567
|
+
// 图表配置 - 显示三条流量速率线
|
568
|
+
const chartConfig = {
|
569
|
+
data: chartData,
|
570
|
+
xField: (d) => new Date(d.time),
|
571
|
+
yField: 'value',
|
572
|
+
colorField: 'metric', // 使用metric字段区分不同的线
|
573
|
+
smooth: true,
|
574
|
+
animate: false,
|
575
|
+
connectNulls: {
|
576
|
+
connect: true,
|
577
|
+
connectStroke: '#aaa',
|
578
|
+
},
|
579
|
+
axis: {
|
580
|
+
x: {
|
581
|
+
labelFormatter: (text) => {
|
582
|
+
const date = dayjs(text);
|
583
|
+
switch (granularity) {
|
584
|
+
case 'second':
|
585
|
+
return date.format('HH:mm:ss');
|
586
|
+
case 'minute':
|
587
|
+
return date.format('HH:mm');
|
588
|
+
case 'hour':
|
589
|
+
return date.format('MM-DD HH:mm');
|
590
|
+
case 'day':
|
591
|
+
return date.format('YYYY-MM-DD');
|
592
|
+
default:
|
593
|
+
return date.format('MM-DD HH:mm');
|
594
|
+
}
|
595
|
+
},
|
596
|
+
labelAutoRotate: true,
|
597
|
+
},
|
598
|
+
y: {
|
599
|
+
title: '任务数量',
|
600
|
+
titleFontSize: 14,
|
601
|
+
},
|
602
|
+
},
|
603
|
+
// 配置颜色映射
|
604
|
+
scale: {
|
605
|
+
color: {
|
606
|
+
domain: ['入队速率', '完成速率', '失败数'],
|
607
|
+
range: ['#1890ff', '#52c41a', '#ff4d4f'], // 蓝色(入队)、绿色(完成)、红色(失败)
|
608
|
+
},
|
609
|
+
y: { nice: true },
|
610
|
+
},
|
611
|
+
legend: {
|
612
|
+
position: 'top',
|
613
|
+
itemName: {
|
614
|
+
style: {
|
615
|
+
fontSize: 12,
|
616
|
+
},
|
617
|
+
},
|
618
|
+
},
|
619
|
+
tooltip: {
|
620
|
+
title: (title) => {
|
621
|
+
const date = dayjs(title.time);
|
622
|
+
switch (granularity) {
|
623
|
+
case 'second':
|
624
|
+
return date.format('YYYY-MM-DD HH:mm:ss');
|
625
|
+
case 'minute':
|
626
|
+
return date.format('YYYY-MM-DD HH:mm');
|
627
|
+
case 'hour':
|
628
|
+
return date.format('YYYY-MM-DD HH:mm');
|
629
|
+
case 'day':
|
630
|
+
return date.format('YYYY-MM-DD');
|
631
|
+
default:
|
632
|
+
return date.format('YYYY-MM-DD HH:mm');
|
633
|
+
}
|
634
|
+
},
|
635
|
+
items: [
|
636
|
+
{
|
637
|
+
field: 'value',
|
638
|
+
name: (datum) => datum.metric,
|
639
|
+
valueFormatter: (value) => {
|
640
|
+
return `${value} 个`;
|
641
|
+
},
|
642
|
+
},
|
643
|
+
],
|
644
|
+
},
|
645
|
+
style: {
|
646
|
+
lineWidth: 2,
|
647
|
+
},
|
648
|
+
// point: {
|
649
|
+
// size: 3,
|
650
|
+
// shape: 'circle',
|
651
|
+
// },
|
652
|
+
height: 240,
|
653
|
+
interaction: {
|
654
|
+
brushXFilter: true // 启用横向筛选
|
655
|
+
},
|
656
|
+
// 监听brush事件,实现框选后自动请求数据
|
657
|
+
onReady: (plot) => {
|
658
|
+
|
659
|
+
// 获取图表实例
|
660
|
+
const chart = plot.chart;
|
661
|
+
|
662
|
+
chart.on("brush:filter", (e) => {
|
663
|
+
console.log('Brush filter 事件:', e);
|
664
|
+
|
665
|
+
// 获取刷选的数据范围
|
666
|
+
if (e.data && e.data.selection) {
|
667
|
+
const selection = e.data.selection;
|
668
|
+
console.log('Selection 数据:', selection);
|
669
|
+
|
670
|
+
// selection[0] 是选中的时间数组
|
671
|
+
if (selection && selection[0] && selection[0].length > 0) {
|
672
|
+
const selectedTimes = selection[0];
|
673
|
+
|
674
|
+
// 获取选中时间的起止
|
675
|
+
const startTime = dayjs(selectedTimes[0]);
|
676
|
+
const endTime = dayjs(selectedTimes[selectedTimes.length - 1]);
|
677
|
+
|
678
|
+
console.log('刷选范围:', startTime.format(), endTime.format());
|
679
|
+
|
680
|
+
// 设置刷选标志
|
681
|
+
isBrushingRef.current = true;
|
682
|
+
|
683
|
+
// 更新UI状态,这会触发 useEffect 重新获取数据
|
684
|
+
setTimeRange('custom');
|
685
|
+
setCustomTimeRange([startTime, endTime]);
|
686
|
+
}
|
687
|
+
}
|
688
|
+
});
|
689
|
+
},
|
690
|
+
};
|
691
|
+
|
692
|
+
// 生成GCS日志系统的链接
|
693
|
+
const generateGCSLogLink = (taskId, task) => {
|
694
|
+
// 如果task_name是unknown,只筛选task_id
|
695
|
+
const query = task.task_name && task.task_name !== 'unknown'
|
696
|
+
? `jsonPayload.event_id%3D%22${taskId}%22%20and%20jsonPayload.task_name%3D%22${task.task_name}%22`
|
697
|
+
: `jsonPayload.event_id%3D%22${taskId}%22`;
|
698
|
+
const baseUrl = 'https://console.cloud.google.com/logs/query';
|
699
|
+
const storageScope = 'logScope,projects%2Ftap-testing-env%2Flocations%2Fglobal%2FlogScopes%2Fall-project-log';
|
700
|
+
const currentTime = new Date().toISOString();
|
701
|
+
|
702
|
+
// 检查任务是否为最终状态
|
703
|
+
const finalStates = ['success', 'error', 'cancel'];
|
704
|
+
const isTaskCompleted = finalStates.includes(task.status);
|
705
|
+
|
706
|
+
if (isTaskCompleted) {
|
707
|
+
// 任务已完成,使用精确的时间范围
|
708
|
+
let startTime, endTime;
|
709
|
+
|
710
|
+
if (task.created_at) {
|
711
|
+
// 任务创建时间前5分钟
|
712
|
+
const createdAt = new Date(task.created_at);
|
713
|
+
startTime = new Date(createdAt.getTime() - 5 * 60 * 1000).toISOString();
|
714
|
+
} else {
|
715
|
+
// 如果没有创建时间,使用当前时间前1小时
|
716
|
+
startTime = new Date(Date.now() - 60 * 60 * 1000).toISOString();
|
717
|
+
}
|
718
|
+
|
719
|
+
if (task.completed_at) {
|
720
|
+
// 任务完成时间后5分钟
|
721
|
+
const completedAt = new Date(task.completed_at);
|
722
|
+
endTime = new Date(completedAt.getTime() + 5 * 60 * 1000).toISOString();
|
723
|
+
} else if (task.started_at) {
|
724
|
+
// 如果没有完成时间但有开始时间,使用开始时间后30分钟(假设任务最多运行30分钟)
|
725
|
+
const startedAt = new Date(task.started_at);
|
726
|
+
endTime = new Date(startedAt.getTime() + 30 * 60 * 1000).toISOString();
|
727
|
+
} else {
|
728
|
+
// 如果都没有,使用当前时间
|
729
|
+
endTime = currentTime;
|
730
|
+
}
|
731
|
+
|
732
|
+
return `${baseUrl};query=${query};storageScope=${storageScope};cursorTimestamp=${currentTime};startTime=${startTime};endTime=${endTime}?project=tap-testing-env`;
|
733
|
+
} else {
|
734
|
+
// 任务未完成,使用最近一天的日志
|
735
|
+
return `${baseUrl};query=${query};storageScope=${storageScope};cursorTimestamp=${currentTime};duration=P1D?project=tap-testing-env`;
|
736
|
+
}
|
737
|
+
};
|
738
|
+
|
739
|
+
// 处理查看日志
|
740
|
+
const handleViewLogs = (taskId, task) => {
|
741
|
+
const logUrl = generateGCSLogLink(taskId, task);
|
742
|
+
window.open(logUrl, '_blank');
|
743
|
+
};
|
744
|
+
|
745
|
+
// ProTable列定义
|
746
|
+
const columns = [
|
747
|
+
{
|
748
|
+
title: '任务ID',
|
749
|
+
dataIndex: 'id',
|
750
|
+
key: 'id',
|
751
|
+
width: 120,
|
752
|
+
fixed: 'left',
|
753
|
+
ellipsis: true,
|
754
|
+
copyable: true,
|
755
|
+
search: false,
|
756
|
+
render: (text) => <span style={{ fontFamily: 'monospace', fontSize: '12px' }}>{text}</span>,
|
757
|
+
},
|
758
|
+
{
|
759
|
+
title: '任务名称',
|
760
|
+
dataIndex: 'task_name',
|
761
|
+
key: 'task_name',
|
762
|
+
width: 150,
|
763
|
+
ellipsis: true,
|
764
|
+
copyable: true,
|
765
|
+
search: false,
|
766
|
+
},
|
767
|
+
// {
|
768
|
+
// title: '消费者组',
|
769
|
+
// dataIndex: 'consumer_group',
|
770
|
+
// key: 'consumer_group',
|
771
|
+
// width: 180,
|
772
|
+
// ellipsis: true,
|
773
|
+
// copyable: true,
|
774
|
+
// search: false,
|
775
|
+
// render: (text) => text || '-',
|
776
|
+
// },
|
777
|
+
{
|
778
|
+
title: '状态',
|
779
|
+
dataIndex: 'status',
|
780
|
+
key: 'status',
|
781
|
+
width: 100,
|
782
|
+
valueType: 'select',
|
783
|
+
valueEnum: {
|
784
|
+
pending: { text: '待处理', status: 'Warning' },
|
785
|
+
running: { text: '运行中', status: 'Processing' },
|
786
|
+
success: { text: '成功', status: 'Success' },
|
787
|
+
error: { text: '失败', status: 'Error' },
|
788
|
+
rejected: { text: '拒绝', status: 'Default' },
|
789
|
+
},
|
790
|
+
render: (status) => {
|
791
|
+
const config = STATUS_CONFIG[status] || { color: 'default', label: status };
|
792
|
+
return <Tag color={config.color}>{config.label}</Tag>;
|
793
|
+
},
|
794
|
+
search: false,
|
795
|
+
},
|
796
|
+
// {
|
797
|
+
// title: '优先级',
|
798
|
+
// dataIndex: 'priority',
|
799
|
+
// key: 'priority',
|
800
|
+
// width: 80,
|
801
|
+
// render: (priority) => priority ?? '-',
|
802
|
+
// },
|
803
|
+
// {
|
804
|
+
// title: '重试次数',
|
805
|
+
// dataIndex: 'retry_count',
|
806
|
+
// key: 'retry_count',
|
807
|
+
// width: 100,
|
808
|
+
// render: (count, record) => `${count || 0}/${record.max_retry || 0}`,
|
809
|
+
// },
|
810
|
+
{
|
811
|
+
title: '创建时间',
|
812
|
+
dataIndex: 'created_at',
|
813
|
+
key: 'created_at',
|
814
|
+
width: 160,
|
815
|
+
sorter: true,
|
816
|
+
search: false,
|
817
|
+
render: (_, record) => {
|
818
|
+
const text = record.created_at;
|
819
|
+
if (!text) return '-';
|
820
|
+
const date = dayjs(text);
|
821
|
+
return date.isValid() ? date.format('YYYY-MM-DD HH:mm:ss') : '-';
|
822
|
+
},
|
823
|
+
},
|
824
|
+
{
|
825
|
+
title: '开始时间',
|
826
|
+
dataIndex: 'started_at',
|
827
|
+
key: 'started_at',
|
828
|
+
width: 160,
|
829
|
+
search: false,
|
830
|
+
render: (_, record) => {
|
831
|
+
const text = record.started_at;
|
832
|
+
if (!text) return '-';
|
833
|
+
const date = dayjs(text);
|
834
|
+
return date.isValid() ? date.format('YYYY-MM-DD HH:mm:ss') : '-';
|
835
|
+
},
|
836
|
+
},
|
837
|
+
{
|
838
|
+
title: '完成时间',
|
839
|
+
dataIndex: 'completed_at',
|
840
|
+
key: 'completed_at',
|
841
|
+
width: 160,
|
842
|
+
search: false,
|
843
|
+
render: (_, record) => {
|
844
|
+
const text = record.completed_at;
|
845
|
+
if (!text) return '-';
|
846
|
+
const date = dayjs(text);
|
847
|
+
return date.isValid() ? date.format('YYYY-MM-DD HH:mm:ss') : '-';
|
848
|
+
},
|
849
|
+
},
|
850
|
+
{
|
851
|
+
title: '耗时',
|
852
|
+
key: 'duration',
|
853
|
+
width: 75,
|
854
|
+
search: false,
|
855
|
+
render: (_, record) => {
|
856
|
+
const text = record.duration;
|
857
|
+
if (text === null || text === undefined) return '-';
|
858
|
+
if (typeof text === 'number') {
|
859
|
+
return `${text.toFixed(2)}s`;
|
860
|
+
}
|
861
|
+
const num = parseFloat(text);
|
862
|
+
return !isNaN(num) ? `${num.toFixed(2)}s` : '-';
|
863
|
+
},
|
864
|
+
},
|
865
|
+
{
|
866
|
+
title: '执行时间',
|
867
|
+
dataIndex: 'execution_time',
|
868
|
+
key: 'execution_time',
|
869
|
+
width: 100,
|
870
|
+
align: 'right',
|
871
|
+
search: false,
|
872
|
+
render: (text) => {
|
873
|
+
if (text === null || text === undefined) return '-';
|
874
|
+
if (typeof text === 'number') {
|
875
|
+
return `${text.toFixed(5)}s`;
|
876
|
+
}
|
877
|
+
const num = parseFloat(text);
|
878
|
+
return !isNaN(num) ? `${num.toFixed(3)}s` : '-';
|
879
|
+
},
|
880
|
+
},
|
881
|
+
{
|
882
|
+
title: 'Worker',
|
883
|
+
dataIndex: 'worker_id',
|
884
|
+
key: 'worker_id',
|
885
|
+
width: 200,
|
886
|
+
ellipsis: true,
|
887
|
+
search: false,
|
888
|
+
},
|
889
|
+
{
|
890
|
+
title: '操作',
|
891
|
+
key: 'actions',
|
892
|
+
width: 150,
|
893
|
+
align: 'center',
|
894
|
+
fixed: 'right',
|
895
|
+
search: false,
|
896
|
+
render: (_, record) => (
|
897
|
+
<Space size="small">
|
898
|
+
<Tooltip title="查看任务参数">
|
899
|
+
<Button
|
900
|
+
type="link"
|
901
|
+
size="small"
|
902
|
+
icon={<FileTextOutlined />}
|
903
|
+
loading={loadingTaskDetails[`${record.id}_${record.consumer_group || 'none'}`]}
|
904
|
+
onClick={() => handleViewTaskDetail(record.id, 'task_data', record.consumer_group)}
|
905
|
+
/>
|
906
|
+
</Tooltip>
|
907
|
+
<Tooltip title="查看执行结果">
|
908
|
+
<Button
|
909
|
+
type="link"
|
910
|
+
size="small"
|
911
|
+
icon={<CheckCircleOutlined />}
|
912
|
+
loading={loadingTaskDetails[`${record.id}_${record.consumer_group || 'none'}`]}
|
913
|
+
onClick={() => handleViewTaskDetail(record.id, 'result', record.consumer_group)}
|
914
|
+
style={{ color: '#52c41a' }}
|
915
|
+
/>
|
916
|
+
</Tooltip>
|
917
|
+
<Tooltip title={record.status === 'error' ? "查看错误信息" : "无错误信息"}>
|
918
|
+
<Button
|
919
|
+
type="link"
|
920
|
+
size="small"
|
921
|
+
icon={<ExclamationCircleOutlined />}
|
922
|
+
loading={loadingTaskDetails[`${record.id}_${record.consumer_group || 'none'}`]}
|
923
|
+
onClick={() => handleViewTaskDetail(record.id, 'error_message', record.consumer_group)}
|
924
|
+
disabled={record.status !== 'error'}
|
925
|
+
danger={record.status === 'error'}
|
926
|
+
/>
|
927
|
+
</Tooltip>
|
928
|
+
<Tooltip title="查看GCS日志">
|
929
|
+
<Button
|
930
|
+
type="link"
|
931
|
+
size="small"
|
932
|
+
icon={<EyeOutlined />}
|
933
|
+
onClick={() => handleViewLogs(record.id, record)}
|
934
|
+
style={{ color: '#1890ff' }}
|
935
|
+
/>
|
936
|
+
</Tooltip>
|
937
|
+
</Space>
|
938
|
+
),
|
939
|
+
},
|
940
|
+
];
|
941
|
+
|
942
|
+
return (
|
943
|
+
<div style={{ padding: '0px', height: '94.5vh', overflow: 'hidden' }}>
|
944
|
+
{/* 面包屑导航 */}
|
945
|
+
{/* <Breadcrumb
|
946
|
+
style={{ marginBottom: 16 }}
|
947
|
+
items={[
|
948
|
+
{
|
949
|
+
title: <a onClick={() => navigate('/queues')}>队列管理</a>
|
950
|
+
},
|
951
|
+
{
|
952
|
+
title: decodedQueueName
|
953
|
+
}
|
954
|
+
]}
|
955
|
+
/> */}
|
956
|
+
{/* 趋势图 */}
|
957
|
+
{/* 任务列表 */}
|
958
|
+
<Card
|
959
|
+
style={{ marginBottom: 0 }}
|
960
|
+
>
|
961
|
+
<div style={{ height: 240 }}>
|
962
|
+
<Spin spinning={loading}>
|
963
|
+
{chartData.length > 0 ? (
|
964
|
+
<Line {...chartConfig} />
|
965
|
+
) : (
|
966
|
+
<Empty description="暂无数据" />
|
967
|
+
)}
|
968
|
+
</Spin>
|
969
|
+
</div>
|
970
|
+
|
971
|
+
|
972
|
+
|
973
|
+
<ProTable
|
974
|
+
key={tableKey}
|
975
|
+
columns={columns}
|
976
|
+
actionRef={actionRef}
|
977
|
+
request={request}
|
978
|
+
rowKey={(record) => `${record.id}_${record.consumer_group || 'none'}`}
|
979
|
+
pagination={{
|
980
|
+
showQuickJumper: true,
|
981
|
+
showSizeChanger: true,
|
982
|
+
defaultPageSize: 20,
|
983
|
+
}}
|
984
|
+
search={false}
|
985
|
+
dateFormatter="string"
|
986
|
+
headerTitle="任务列表"
|
987
|
+
scroll={{
|
988
|
+
y: tableHeight,
|
989
|
+
x: 1300,
|
990
|
+
}}
|
991
|
+
size="small"
|
992
|
+
options={{
|
993
|
+
reload: async () => {
|
994
|
+
try {
|
995
|
+
console.log('刷新数据...');
|
996
|
+
setRefreshing(true);
|
997
|
+
setGlobalLoading(true, '刷新数据中...');
|
998
|
+
|
999
|
+
// 先刷新图表
|
1000
|
+
await fetchQueueTimeline();
|
1001
|
+
|
1002
|
+
// 然后通过修改key强制刷新表格
|
1003
|
+
// 不要调用action?.reload(),避免循环
|
1004
|
+
setTableKey(prev => prev + 1);
|
1005
|
+
|
1006
|
+
// 给一点时间让表格重新渲染
|
1007
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
1008
|
+
|
1009
|
+
} catch (error) {
|
1010
|
+
console.error('刷新失败:', error);
|
1011
|
+
message.error('刷新数据失败');
|
1012
|
+
} finally {
|
1013
|
+
setRefreshing(false);
|
1014
|
+
setGlobalLoading(false);
|
1015
|
+
}
|
1016
|
+
},
|
1017
|
+
density: true,
|
1018
|
+
fullScreen: true,
|
1019
|
+
setting: true,
|
1020
|
+
}}
|
1021
|
+
loading={refreshing}
|
1022
|
+
toolbar={{
|
1023
|
+
title: (
|
1024
|
+
<Space>
|
1025
|
+
<TaskFilter
|
1026
|
+
filters={filters}
|
1027
|
+
onFiltersChange={(newFilters) => {
|
1028
|
+
setFilters(newFilters);
|
1029
|
+
if (actionRef.current) {
|
1030
|
+
actionRef.current.reload();
|
1031
|
+
}
|
1032
|
+
}}
|
1033
|
+
/>
|
1034
|
+
</Space>
|
1035
|
+
),
|
1036
|
+
actions: [
|
1037
|
+
<TimeRangeSelector
|
1038
|
+
value={timeRange}
|
1039
|
+
onChange={setTimeRange}
|
1040
|
+
customValue={customTimeRange}
|
1041
|
+
onCustomChange={setCustomTimeRange}
|
1042
|
+
/>,
|
1043
|
+
<Tooltip key="share" title="分享当前筛选条件">
|
1044
|
+
<Button icon={<ShareAltOutlined />} onClick={shareFilters}>
|
1045
|
+
分享
|
1046
|
+
</Button>
|
1047
|
+
</Tooltip>,
|
1048
|
+
// <Tooltip key="clear" title="清除所有筛选条件">
|
1049
|
+
// <Button icon={<ClearOutlined />} onClick={handleClearFilters}>
|
1050
|
+
// 清除
|
1051
|
+
// </Button>
|
1052
|
+
// </Tooltip>,
|
1053
|
+
],
|
1054
|
+
}}
|
1055
|
+
params={{
|
1056
|
+
timeRange,
|
1057
|
+
customTimeRange,
|
1058
|
+
filters,
|
1059
|
+
}}
|
1060
|
+
/>
|
1061
|
+
</Card>
|
1062
|
+
|
1063
|
+
|
1064
|
+
{/* JSON数据展示弹窗 */}
|
1065
|
+
<Modal
|
1066
|
+
title={`${selectedTaskDetail?.field === 'task_data' ? '任务参数' :
|
1067
|
+
selectedTaskDetail?.field === 'result' ? '执行结果' :
|
1068
|
+
selectedTaskDetail?.field === 'error_message' ? '错误信息' : ''
|
1069
|
+
} - ${selectedTaskId}`}
|
1070
|
+
open={detailModalVisible}
|
1071
|
+
onCancel={() => {
|
1072
|
+
setDetailModalVisible(false);
|
1073
|
+
setSelectedTaskDetail(null);
|
1074
|
+
setSelectedTaskId(null);
|
1075
|
+
}}
|
1076
|
+
width={800}
|
1077
|
+
footer={null}
|
1078
|
+
>
|
1079
|
+
<div style={{ maxHeight: '60vh', overflowY: 'auto' }}>
|
1080
|
+
{selectedTaskDetail?.field === 'error_message' ? (
|
1081
|
+
// 错误信息直接显示为文本
|
1082
|
+
<div style={{
|
1083
|
+
backgroundColor: '#fff2f0',
|
1084
|
+
padding: '16px',
|
1085
|
+
borderRadius: '4px',
|
1086
|
+
border: '1px solid #ffccc7',
|
1087
|
+
color: '#cf1322',
|
1088
|
+
fontSize: '13px',
|
1089
|
+
lineHeight: '1.5',
|
1090
|
+
whiteSpace: 'pre-wrap',
|
1091
|
+
wordBreak: 'break-word'
|
1092
|
+
}}>
|
1093
|
+
{selectedTaskDetail?.data || '无错误信息'}
|
1094
|
+
</div>
|
1095
|
+
) : (
|
1096
|
+
// JSON数据格式化显示
|
1097
|
+
<pre style={{
|
1098
|
+
backgroundColor: '#f5f5f5',
|
1099
|
+
padding: '16px',
|
1100
|
+
borderRadius: '4px',
|
1101
|
+
fontSize: '12px',
|
1102
|
+
lineHeight: '1.5',
|
1103
|
+
overflowX: 'auto'
|
1104
|
+
}}>
|
1105
|
+
{selectedTaskDetail?.data ?
|
1106
|
+
JSON.stringify(selectedTaskDetail.data, null, 2) :
|
1107
|
+
'无数据'
|
1108
|
+
}
|
1109
|
+
</pre>
|
1110
|
+
)}
|
1111
|
+
</div>
|
1112
|
+
</Modal>
|
1113
|
+
</div >
|
1114
|
+
);
|
1115
|
+
}
|
1116
|
+
|
1117
|
+
export default QueueDetail;
|