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,684 @@
|
|
1
|
+
import { useState, useEffect } from 'react';
|
2
|
+
import { Card, Table, Button, Space, Tag, message, Modal, Form, Input, Select, Switch, InputNumber, Row, Col, Statistic, Tabs, Timeline, Badge } from 'antd';
|
3
|
+
import { PlusOutlined, EditOutlined, DeleteOutlined, ReloadOutlined, BellOutlined, WarningOutlined, CheckCircleOutlined, CloseCircleOutlined, HistoryOutlined, ExpandOutlined } from '@ant-design/icons';
|
4
|
+
import dayjs from 'dayjs';
|
5
|
+
import axios from 'axios';
|
6
|
+
|
7
|
+
const { Option } = Select;
|
8
|
+
const { TextArea } = Input;
|
9
|
+
const { TabPane } = Tabs;
|
10
|
+
|
11
|
+
// 告警级别配置
|
12
|
+
const ALERT_LEVEL_CONFIG = {
|
13
|
+
'critical': { label: '严重', color: 'red', icon: <CloseCircleOutlined /> },
|
14
|
+
'warning': { label: '警告', color: 'orange', icon: <WarningOutlined /> },
|
15
|
+
'info': { label: '信息', color: 'blue', icon: <BellOutlined /> },
|
16
|
+
};
|
17
|
+
|
18
|
+
// 告警类型配置
|
19
|
+
const ALERT_TYPE_CONFIG = {
|
20
|
+
'error_rate': { label: '错误率', description: '任务错误率超过阈值' },
|
21
|
+
'queue_length': { label: '队列长度', description: '队列积压任务超过阈值' },
|
22
|
+
'worker_count': { label: 'Worker数量', description: 'Worker数量低于阈值' },
|
23
|
+
'execution_time': { label: '执行时长', description: '任务执行时间超过阈值' },
|
24
|
+
'failure_count': { label: '失败次数', description: '连续失败次数超过阈值' },
|
25
|
+
'throughput': { label: '吞吐量', description: '任务处理速率低于阈值' },
|
26
|
+
'idle_time': { label: '空闲时间', description: 'Worker空闲时间超过阈值' },
|
27
|
+
'memory_usage': { label: '内存使用', description: 'Worker内存使用超过阈值' },
|
28
|
+
};
|
29
|
+
|
30
|
+
// 比较操作符
|
31
|
+
const OPERATORS = {
|
32
|
+
'gt': '大于',
|
33
|
+
'gte': '大于等于',
|
34
|
+
'lt': '小于',
|
35
|
+
'lte': '小于等于',
|
36
|
+
'eq': '等于',
|
37
|
+
'ne': '不等于',
|
38
|
+
};
|
39
|
+
|
40
|
+
function Alerts() {
|
41
|
+
const [loading, setLoading] = useState(false);
|
42
|
+
const [alertRules, setAlertRules] = useState([]);
|
43
|
+
const [alertHistory, setAlertHistory] = useState([]);
|
44
|
+
const [modalVisible, setModalVisible] = useState(false);
|
45
|
+
const [historyModalVisible, setHistoryModalVisible] = useState(false);
|
46
|
+
const [isEditMode, setIsEditMode] = useState(false);
|
47
|
+
const [selectedRule, setSelectedRule] = useState(null);
|
48
|
+
const [form] = Form.useForm();
|
49
|
+
const [statistics, setStatistics] = useState({
|
50
|
+
totalRules: 0,
|
51
|
+
activeRules: 0,
|
52
|
+
todayAlerts: 0,
|
53
|
+
criticalAlerts: 0,
|
54
|
+
});
|
55
|
+
const [activeTab, setActiveTab] = useState('rules');
|
56
|
+
|
57
|
+
// 获取告警规则列表
|
58
|
+
const fetchAlertRules = async () => {
|
59
|
+
setLoading(true);
|
60
|
+
try {
|
61
|
+
const response = await axios.get('/api/alert-rules');
|
62
|
+
if (response.data.success) {
|
63
|
+
setAlertRules(response.data.data);
|
64
|
+
calculateStatistics(response.data.data);
|
65
|
+
}
|
66
|
+
} catch (error) {
|
67
|
+
message.error('获取告警规则失败');
|
68
|
+
console.error('Failed to fetch alert rules:', error);
|
69
|
+
} finally {
|
70
|
+
setLoading(false);
|
71
|
+
}
|
72
|
+
};
|
73
|
+
|
74
|
+
// 获取告警历史
|
75
|
+
const fetchAlertHistory = async (ruleId = null) => {
|
76
|
+
try {
|
77
|
+
// 使用正确的API端点
|
78
|
+
if (ruleId) {
|
79
|
+
const response = await axios.get(`/api/alert-rules/${ruleId}/history`);
|
80
|
+
if (response.data.success) {
|
81
|
+
setAlertHistory(response.data.data);
|
82
|
+
}
|
83
|
+
} else {
|
84
|
+
// 暂时设置为空,因为后端还没有全局历史API
|
85
|
+
setAlertHistory([]);
|
86
|
+
}
|
87
|
+
} catch (error) {
|
88
|
+
message.error('获取告警历史失败');
|
89
|
+
console.error('Failed to fetch alert history:', error);
|
90
|
+
}
|
91
|
+
};
|
92
|
+
|
93
|
+
// 计算统计数据
|
94
|
+
const calculateStatistics = (rules) => {
|
95
|
+
const stats = {
|
96
|
+
totalRules: rules.length,
|
97
|
+
activeRules: rules.filter(r => r.is_active).length,
|
98
|
+
todayAlerts: 0, // 需要从后端获取
|
99
|
+
criticalAlerts: 0, // 需要从后端获取
|
100
|
+
};
|
101
|
+
setStatistics(stats);
|
102
|
+
};
|
103
|
+
|
104
|
+
// 初始化加载
|
105
|
+
useEffect(() => {
|
106
|
+
fetchAlertRules();
|
107
|
+
fetchAlertHistory();
|
108
|
+
}, []);
|
109
|
+
|
110
|
+
// 处理添加/编辑规则
|
111
|
+
const handleAddOrEditRule = () => {
|
112
|
+
form.validateFields().then(async (values) => {
|
113
|
+
try {
|
114
|
+
// 处理webhook_headers
|
115
|
+
if (values.webhook_headers) {
|
116
|
+
try {
|
117
|
+
values.webhook_headers = JSON.parse(values.webhook_headers);
|
118
|
+
} catch (e) {
|
119
|
+
message.error('Webhook头部格式错误,请输入有效的JSON');
|
120
|
+
return;
|
121
|
+
}
|
122
|
+
}
|
123
|
+
|
124
|
+
// 处理额外配置
|
125
|
+
if (values.extra_config) {
|
126
|
+
try {
|
127
|
+
values.extra_config = JSON.parse(values.extra_config);
|
128
|
+
} catch (e) {
|
129
|
+
message.error('额外配置格式错误,请输入有效的JSON');
|
130
|
+
return;
|
131
|
+
}
|
132
|
+
}
|
133
|
+
|
134
|
+
const url = isEditMode
|
135
|
+
? `/api/alert-rules/${selectedRule.id}`
|
136
|
+
: '/api/alert-rules';
|
137
|
+
const method = isEditMode ? 'put' : 'post';
|
138
|
+
|
139
|
+
const response = await axios[method](url, values);
|
140
|
+
if (response.data.success) {
|
141
|
+
message.success(isEditMode ? '规则更新成功' : '规则创建成功');
|
142
|
+
setModalVisible(false);
|
143
|
+
form.resetFields();
|
144
|
+
fetchAlertRules();
|
145
|
+
}
|
146
|
+
} catch (error) {
|
147
|
+
message.error(isEditMode ? '更新规则失败' : '创建规则失败');
|
148
|
+
console.error('Failed to save rule:', error);
|
149
|
+
}
|
150
|
+
});
|
151
|
+
};
|
152
|
+
|
153
|
+
// 处理删除规则
|
154
|
+
const handleDeleteRule = (rule) => {
|
155
|
+
Modal.confirm({
|
156
|
+
title: '确认删除',
|
157
|
+
content: `确定要删除告警规则 "${rule.name}" 吗?`,
|
158
|
+
onOk: async () => {
|
159
|
+
try {
|
160
|
+
const response = await axios.delete(`/api/alert-rules/${rule.id}`);
|
161
|
+
if (response.data.success) {
|
162
|
+
message.success('规则删除成功');
|
163
|
+
fetchAlertRules();
|
164
|
+
}
|
165
|
+
} catch (error) {
|
166
|
+
message.error('删除规则失败');
|
167
|
+
console.error('Failed to delete rule:', error);
|
168
|
+
}
|
169
|
+
},
|
170
|
+
});
|
171
|
+
};
|
172
|
+
|
173
|
+
// 处理启用/禁用规则
|
174
|
+
const handleToggleRule = async (rule) => {
|
175
|
+
try {
|
176
|
+
const response = await axios.put(`/api/alert-rules/${rule.id}/toggle`, {
|
177
|
+
enabled: !rule.enabled,
|
178
|
+
});
|
179
|
+
if (response.data.success) {
|
180
|
+
message.success(rule.enabled ? '规则已禁用' : '规则已启用');
|
181
|
+
fetchAlertRules();
|
182
|
+
}
|
183
|
+
} catch (error) {
|
184
|
+
message.error('操作失败');
|
185
|
+
console.error('Failed to toggle rule:', error);
|
186
|
+
}
|
187
|
+
};
|
188
|
+
|
189
|
+
// 测试告警规则
|
190
|
+
const handleTestRule = async (rule) => {
|
191
|
+
try {
|
192
|
+
const response = await axios.post(`/api/alert-rules/${rule.id}/test`);
|
193
|
+
if (response.data.success) {
|
194
|
+
message.success('测试告警已发送');
|
195
|
+
}
|
196
|
+
} catch (error) {
|
197
|
+
message.error('测试失败');
|
198
|
+
console.error('Failed to test rule:', error);
|
199
|
+
}
|
200
|
+
};
|
201
|
+
|
202
|
+
// 打开添加/编辑模态框
|
203
|
+
const openModal = (rule = null) => {
|
204
|
+
setIsEditMode(!!rule);
|
205
|
+
setSelectedRule(rule);
|
206
|
+
if (rule) {
|
207
|
+
form.setFieldsValue({
|
208
|
+
...rule,
|
209
|
+
webhook_headers: rule.webhook_headers ? JSON.stringify(rule.webhook_headers) : '{}',
|
210
|
+
extra_config: rule.extra_config ? JSON.stringify(rule.extra_config) : '{}',
|
211
|
+
});
|
212
|
+
} else {
|
213
|
+
form.resetFields();
|
214
|
+
}
|
215
|
+
setModalVisible(true);
|
216
|
+
};
|
217
|
+
|
218
|
+
// 查看规则历史
|
219
|
+
const viewRuleHistory = (rule) => {
|
220
|
+
setSelectedRule(rule);
|
221
|
+
fetchAlertHistory(rule.id);
|
222
|
+
setHistoryModalVisible(true);
|
223
|
+
};
|
224
|
+
|
225
|
+
// 规则表格列定义
|
226
|
+
const ruleColumns = [
|
227
|
+
{
|
228
|
+
title: '规则名称',
|
229
|
+
dataIndex: 'name',
|
230
|
+
key: 'name',
|
231
|
+
width: 200,
|
232
|
+
},
|
233
|
+
{
|
234
|
+
title: '类型',
|
235
|
+
dataIndex: 'alert_type',
|
236
|
+
key: 'alert_type',
|
237
|
+
width: 120,
|
238
|
+
render: (type) => {
|
239
|
+
const config = ALERT_TYPE_CONFIG[type] || { label: type };
|
240
|
+
return config.label;
|
241
|
+
},
|
242
|
+
},
|
243
|
+
{
|
244
|
+
title: '级别',
|
245
|
+
dataIndex: 'level',
|
246
|
+
key: 'level',
|
247
|
+
width: 80,
|
248
|
+
render: (level) => {
|
249
|
+
const config = ALERT_LEVEL_CONFIG[level] || { label: level, color: 'default' };
|
250
|
+
return <Tag color={config.color}>{config.label}</Tag>;
|
251
|
+
},
|
252
|
+
},
|
253
|
+
{
|
254
|
+
title: '条件',
|
255
|
+
key: 'condition',
|
256
|
+
width: 200,
|
257
|
+
render: (_, record) => {
|
258
|
+
const operator = OPERATORS[record.operator] || record.operator;
|
259
|
+
const scope = record.scope === 'queue' ? `队列: ${record.queue_name || '全部'}` : '全局';
|
260
|
+
return (
|
261
|
+
<div>
|
262
|
+
<div>{scope}</div>
|
263
|
+
<div style={{ fontSize: 12, color: '#666' }}>
|
264
|
+
{`${ALERT_TYPE_CONFIG[record.alert_type]?.label} ${operator} ${record.threshold}`}
|
265
|
+
{record.time_window && ` (${record.time_window}秒内)`}
|
266
|
+
</div>
|
267
|
+
</div>
|
268
|
+
);
|
269
|
+
},
|
270
|
+
},
|
271
|
+
{
|
272
|
+
title: 'Webhook',
|
273
|
+
dataIndex: 'webhook_url',
|
274
|
+
key: 'webhook_url',
|
275
|
+
width: 200,
|
276
|
+
ellipsis: true,
|
277
|
+
render: (url) => url ? <span style={{ fontSize: 12 }}>{url}</span> : '-',
|
278
|
+
},
|
279
|
+
{
|
280
|
+
title: '状态',
|
281
|
+
dataIndex: 'enabled',
|
282
|
+
key: 'enabled',
|
283
|
+
width: 80,
|
284
|
+
render: (enabled) => (
|
285
|
+
<Badge status={enabled ? 'success' : 'default'} text={enabled ? '启用' : '禁用'} />
|
286
|
+
),
|
287
|
+
},
|
288
|
+
{
|
289
|
+
title: '最后触发',
|
290
|
+
dataIndex: 'last_triggered',
|
291
|
+
key: 'last_triggered',
|
292
|
+
width: 150,
|
293
|
+
render: (time) => time ? dayjs(time).format('MM-DD HH:mm:ss') : '-',
|
294
|
+
},
|
295
|
+
{
|
296
|
+
title: '操作',
|
297
|
+
key: 'actions',
|
298
|
+
width: 200,
|
299
|
+
fixed: 'right',
|
300
|
+
render: (_, record) => (
|
301
|
+
<Space size="small">
|
302
|
+
<Button
|
303
|
+
type="link"
|
304
|
+
size="small"
|
305
|
+
onClick={() => handleToggleRule(record)}
|
306
|
+
>
|
307
|
+
{record.enabled ? '禁用' : '启用'}
|
308
|
+
</Button>
|
309
|
+
<Button
|
310
|
+
type="link"
|
311
|
+
size="small"
|
312
|
+
onClick={() => handleTestRule(record)}
|
313
|
+
>
|
314
|
+
测试
|
315
|
+
</Button>
|
316
|
+
<Button
|
317
|
+
type="link"
|
318
|
+
size="small"
|
319
|
+
onClick={() => viewRuleHistory(record)}
|
320
|
+
>
|
321
|
+
历史
|
322
|
+
</Button>
|
323
|
+
<Button
|
324
|
+
type="link"
|
325
|
+
size="small"
|
326
|
+
onClick={() => openModal(record)}
|
327
|
+
>
|
328
|
+
编辑
|
329
|
+
</Button>
|
330
|
+
<Button
|
331
|
+
type="link"
|
332
|
+
size="small"
|
333
|
+
danger
|
334
|
+
onClick={() => handleDeleteRule(record)}
|
335
|
+
>
|
336
|
+
删除
|
337
|
+
</Button>
|
338
|
+
</Space>
|
339
|
+
),
|
340
|
+
},
|
341
|
+
];
|
342
|
+
|
343
|
+
// 告警历史表格列
|
344
|
+
const historyColumns = [
|
345
|
+
{
|
346
|
+
title: '触发时间',
|
347
|
+
dataIndex: 'triggered_at',
|
348
|
+
key: 'triggered_at',
|
349
|
+
width: 150,
|
350
|
+
render: (time) => dayjs(time).format('YYYY-MM-DD HH:mm:ss'),
|
351
|
+
},
|
352
|
+
{
|
353
|
+
title: '规则名称',
|
354
|
+
dataIndex: 'rule_name',
|
355
|
+
key: 'rule_name',
|
356
|
+
width: 200,
|
357
|
+
},
|
358
|
+
{
|
359
|
+
title: '级别',
|
360
|
+
dataIndex: 'level',
|
361
|
+
key: 'level',
|
362
|
+
width: 80,
|
363
|
+
render: (level) => {
|
364
|
+
const config = ALERT_LEVEL_CONFIG[level] || { label: level, color: 'default' };
|
365
|
+
return <Tag color={config.color} icon={config.icon}>{config.label}</Tag>;
|
366
|
+
},
|
367
|
+
},
|
368
|
+
{
|
369
|
+
title: '告警内容',
|
370
|
+
dataIndex: 'message',
|
371
|
+
key: 'message',
|
372
|
+
ellipsis: true,
|
373
|
+
},
|
374
|
+
{
|
375
|
+
title: '通知状态',
|
376
|
+
dataIndex: 'notification_status',
|
377
|
+
key: 'notification_status',
|
378
|
+
width: 100,
|
379
|
+
render: (status) => (
|
380
|
+
<Tag color={status === 'success' ? 'green' : 'red'}>
|
381
|
+
{status === 'success' ? '已发送' : '发送失败'}
|
382
|
+
</Tag>
|
383
|
+
),
|
384
|
+
},
|
385
|
+
];
|
386
|
+
|
387
|
+
return (
|
388
|
+
<div className="page-wrapper">
|
389
|
+
|
390
|
+
{/* 统计卡片 */}
|
391
|
+
<Row gutter={16} style={{ marginBottom: 16 }}>
|
392
|
+
<Col span={6}>
|
393
|
+
<Card>
|
394
|
+
<Statistic title="规则总数" value={statistics.totalRules} />
|
395
|
+
</Card>
|
396
|
+
</Col>
|
397
|
+
<Col span={6}>
|
398
|
+
<Card>
|
399
|
+
<Statistic
|
400
|
+
title="活跃规则"
|
401
|
+
value={statistics.activeRules}
|
402
|
+
valueStyle={{ color: '#3f8600' }}
|
403
|
+
/>
|
404
|
+
</Card>
|
405
|
+
</Col>
|
406
|
+
<Col span={6}>
|
407
|
+
<Card>
|
408
|
+
<Statistic
|
409
|
+
title="今日告警"
|
410
|
+
value={statistics.todayAlerts}
|
411
|
+
suffix="次"
|
412
|
+
valueStyle={{ color: '#ff9800' }}
|
413
|
+
/>
|
414
|
+
</Card>
|
415
|
+
</Col>
|
416
|
+
<Col span={6}>
|
417
|
+
<Card>
|
418
|
+
<Statistic
|
419
|
+
title="严重告警"
|
420
|
+
value={statistics.criticalAlerts}
|
421
|
+
suffix="个"
|
422
|
+
valueStyle={{ color: '#f50' }}
|
423
|
+
/>
|
424
|
+
</Card>
|
425
|
+
</Col>
|
426
|
+
</Row>
|
427
|
+
|
428
|
+
{/* 主内容区域 */}
|
429
|
+
<Card>
|
430
|
+
<Tabs activeKey={activeTab} onChange={setActiveTab}>
|
431
|
+
<TabPane tab="告警规则" key="rules">
|
432
|
+
<div style={{ marginBottom: 16 }}>
|
433
|
+
<Space>
|
434
|
+
<Button
|
435
|
+
icon={<ReloadOutlined />}
|
436
|
+
onClick={fetchAlertRules}
|
437
|
+
>
|
438
|
+
刷新
|
439
|
+
</Button>
|
440
|
+
<Button
|
441
|
+
type="primary"
|
442
|
+
icon={<PlusOutlined />}
|
443
|
+
onClick={() => openModal()}
|
444
|
+
>
|
445
|
+
添加规则
|
446
|
+
</Button>
|
447
|
+
</Space>
|
448
|
+
</div>
|
449
|
+
<Table
|
450
|
+
columns={ruleColumns}
|
451
|
+
dataSource={alertRules}
|
452
|
+
rowKey="id"
|
453
|
+
loading={loading}
|
454
|
+
scroll={{ x: 1300 }}
|
455
|
+
pagination={{
|
456
|
+
pageSize: 10,
|
457
|
+
showSizeChanger: true,
|
458
|
+
showTotal: (total) => `共 ${total} 条`,
|
459
|
+
}}
|
460
|
+
/>
|
461
|
+
</TabPane>
|
462
|
+
|
463
|
+
<TabPane tab="告警历史" key="history">
|
464
|
+
<div style={{ marginBottom: 16 }}>
|
465
|
+
<Button
|
466
|
+
icon={<ReloadOutlined />}
|
467
|
+
onClick={() => fetchAlertHistory()}
|
468
|
+
>
|
469
|
+
刷新
|
470
|
+
</Button>
|
471
|
+
</div>
|
472
|
+
<Table
|
473
|
+
columns={historyColumns}
|
474
|
+
dataSource={alertHistory}
|
475
|
+
rowKey="id"
|
476
|
+
pagination={{
|
477
|
+
pageSize: 20,
|
478
|
+
showSizeChanger: true,
|
479
|
+
showTotal: (total) => `共 ${total} 条`,
|
480
|
+
}}
|
481
|
+
/>
|
482
|
+
</TabPane>
|
483
|
+
</Tabs>
|
484
|
+
</Card>
|
485
|
+
|
486
|
+
{/* 添加/编辑规则模态框 */}
|
487
|
+
<Modal
|
488
|
+
title={isEditMode ? '编辑告警规则' : '添加告警规则'}
|
489
|
+
open={modalVisible}
|
490
|
+
onOk={handleAddOrEditRule}
|
491
|
+
onCancel={() => {
|
492
|
+
setModalVisible(false);
|
493
|
+
form.resetFields();
|
494
|
+
}}
|
495
|
+
width={700}
|
496
|
+
>
|
497
|
+
<Form
|
498
|
+
form={form}
|
499
|
+
layout="vertical"
|
500
|
+
initialValues={{
|
501
|
+
enabled: true,
|
502
|
+
level: 'warning',
|
503
|
+
scope: 'global',
|
504
|
+
operator: 'gt',
|
505
|
+
time_window: 300,
|
506
|
+
webhook_headers: '{}',
|
507
|
+
extra_config: '{}',
|
508
|
+
}}
|
509
|
+
>
|
510
|
+
<Row gutter={16}>
|
511
|
+
<Col span={12}>
|
512
|
+
<Form.Item
|
513
|
+
name="name"
|
514
|
+
label="规则名称"
|
515
|
+
rules={[{ required: true, message: '请输入规则名称' }]}
|
516
|
+
>
|
517
|
+
<Input placeholder="如:队列积压告警" />
|
518
|
+
</Form.Item>
|
519
|
+
</Col>
|
520
|
+
<Col span={12}>
|
521
|
+
<Form.Item
|
522
|
+
name="level"
|
523
|
+
label="告警级别"
|
524
|
+
rules={[{ required: true, message: '请选择告警级别' }]}
|
525
|
+
>
|
526
|
+
<Select>
|
527
|
+
<Option value="info">信息</Option>
|
528
|
+
<Option value="warning">警告</Option>
|
529
|
+
<Option value="critical">严重</Option>
|
530
|
+
</Select>
|
531
|
+
</Form.Item>
|
532
|
+
</Col>
|
533
|
+
</Row>
|
534
|
+
|
535
|
+
<Row gutter={16}>
|
536
|
+
<Col span={12}>
|
537
|
+
<Form.Item
|
538
|
+
name="alert_type"
|
539
|
+
label="告警类型"
|
540
|
+
rules={[{ required: true, message: '请选择告警类型' }]}
|
541
|
+
>
|
542
|
+
<Select>
|
543
|
+
{Object.entries(ALERT_TYPE_CONFIG).map(([key, config]) => (
|
544
|
+
<Option key={key} value={key}>
|
545
|
+
{config.label}
|
546
|
+
</Option>
|
547
|
+
))}
|
548
|
+
</Select>
|
549
|
+
</Form.Item>
|
550
|
+
</Col>
|
551
|
+
<Col span={12}>
|
552
|
+
<Form.Item
|
553
|
+
name="scope"
|
554
|
+
label="监控范围"
|
555
|
+
rules={[{ required: true, message: '请选择监控范围' }]}
|
556
|
+
>
|
557
|
+
<Select onChange={(value) => {
|
558
|
+
if (value === 'global') {
|
559
|
+
form.setFieldsValue({ queue_name: undefined });
|
560
|
+
}
|
561
|
+
}}>
|
562
|
+
<Option value="global">全局</Option>
|
563
|
+
<Option value="queue">指定队列</Option>
|
564
|
+
</Select>
|
565
|
+
</Form.Item>
|
566
|
+
</Col>
|
567
|
+
</Row>
|
568
|
+
|
569
|
+
<Form.Item noStyle shouldUpdate={(prevValues, currentValues) => prevValues.scope !== currentValues.scope}>
|
570
|
+
{({ getFieldValue }) => {
|
571
|
+
return getFieldValue('scope') === 'queue' ? (
|
572
|
+
<Form.Item
|
573
|
+
name="queue_name"
|
574
|
+
label="队列名称"
|
575
|
+
rules={[{ required: true, message: '请输入队列名称' }]}
|
576
|
+
>
|
577
|
+
<Input placeholder="输入要监控的队列名称" />
|
578
|
+
</Form.Item>
|
579
|
+
) : null;
|
580
|
+
}}
|
581
|
+
</Form.Item>
|
582
|
+
|
583
|
+
<Row gutter={16}>
|
584
|
+
<Col span={8}>
|
585
|
+
<Form.Item
|
586
|
+
name="operator"
|
587
|
+
label="比较操作"
|
588
|
+
rules={[{ required: true, message: '请选择比较操作' }]}
|
589
|
+
>
|
590
|
+
<Select>
|
591
|
+
{Object.entries(OPERATORS).map(([key, label]) => (
|
592
|
+
<Option key={key} value={key}>{label}</Option>
|
593
|
+
))}
|
594
|
+
</Select>
|
595
|
+
</Form.Item>
|
596
|
+
</Col>
|
597
|
+
<Col span={8}>
|
598
|
+
<Form.Item
|
599
|
+
name="threshold"
|
600
|
+
label="阈值"
|
601
|
+
rules={[{ required: true, message: '请输入阈值' }]}
|
602
|
+
>
|
603
|
+
<InputNumber style={{ width: '100%' }} placeholder="如:80" />
|
604
|
+
</Form.Item>
|
605
|
+
</Col>
|
606
|
+
<Col span={8}>
|
607
|
+
<Form.Item
|
608
|
+
name="time_window"
|
609
|
+
label="时间窗口(秒)"
|
610
|
+
rules={[{ required: true, message: '请输入时间窗口' }]}
|
611
|
+
>
|
612
|
+
<InputNumber min={1} style={{ width: '100%' }} placeholder="300" />
|
613
|
+
</Form.Item>
|
614
|
+
</Col>
|
615
|
+
</Row>
|
616
|
+
|
617
|
+
<Form.Item
|
618
|
+
name="description"
|
619
|
+
label="规则描述"
|
620
|
+
>
|
621
|
+
<TextArea rows={2} placeholder="描述这个告警规则的用途" />
|
622
|
+
</Form.Item>
|
623
|
+
|
624
|
+
<Form.Item
|
625
|
+
name="webhook_url"
|
626
|
+
label="Webhook URL"
|
627
|
+
rules={[{ type: 'url', message: '请输入有效的URL' }]}
|
628
|
+
>
|
629
|
+
<Input placeholder="https://example.com/webhook" />
|
630
|
+
</Form.Item>
|
631
|
+
|
632
|
+
<Form.Item
|
633
|
+
name="webhook_headers"
|
634
|
+
label="Webhook Headers (JSON)"
|
635
|
+
extra={'例如: {"Authorization": "Bearer token"}'}
|
636
|
+
>
|
637
|
+
<TextArea rows={2} placeholder="{}" />
|
638
|
+
</Form.Item>
|
639
|
+
|
640
|
+
<Form.Item
|
641
|
+
name="extra_config"
|
642
|
+
label="额外配置 (JSON)"
|
643
|
+
extra="其他自定义配置项"
|
644
|
+
>
|
645
|
+
<TextArea rows={2} placeholder="{}" />
|
646
|
+
</Form.Item>
|
647
|
+
|
648
|
+
<Form.Item
|
649
|
+
name="enabled"
|
650
|
+
label="启用状态"
|
651
|
+
valuePropName="checked"
|
652
|
+
>
|
653
|
+
<Switch checkedChildren="启用" unCheckedChildren="禁用" />
|
654
|
+
</Form.Item>
|
655
|
+
</Form>
|
656
|
+
</Modal>
|
657
|
+
|
658
|
+
{/* 规则历史模态框 */}
|
659
|
+
<Modal
|
660
|
+
title={`告警历史 - ${selectedRule?.name}`}
|
661
|
+
open={historyModalVisible}
|
662
|
+
onCancel={() => {
|
663
|
+
setHistoryModalVisible(false);
|
664
|
+
setAlertHistory([]);
|
665
|
+
}}
|
666
|
+
width={900}
|
667
|
+
footer={null}
|
668
|
+
>
|
669
|
+
<Table
|
670
|
+
columns={historyColumns}
|
671
|
+
dataSource={alertHistory}
|
672
|
+
rowKey="id"
|
673
|
+
pagination={{
|
674
|
+
pageSize: 10,
|
675
|
+
showSizeChanger: true,
|
676
|
+
}}
|
677
|
+
/>
|
678
|
+
</Modal>
|
679
|
+
|
680
|
+
</div>
|
681
|
+
);
|
682
|
+
}
|
683
|
+
|
684
|
+
export default Alerts;
|