This is an automated email from the ASF dual-hosted git repository.
lizhimins pushed a commit to branch rocketmq-studio
in repository https://gitbox.apache.org/repos/asf/rocketmq-dashboard.git
The following commit(s) were added to refs/heads/rocketmq-studio by this push:
new 428c00b feat: add AlertManagement page for alert rule operations
(#476)
428c00b is described below
commit 428c00b34883dc57a88c923fa27e18efc9969e96
Author: zhaohai <[email protected]>
AuthorDate: Wed Jul 22 19:21:11 2026 +0800
feat: add AlertManagement page for alert rule operations (#476)
Add alert rule management page: parse/render Prometheus AlertManager YAML
rules with search, toggle, and CRUD.
---
web/src/App.tsx | 2 +
web/src/api/alertManagement.test.ts | 52 +++
web/src/api/alertManagement.ts | 27 ++
web/src/i18n/translations.ts | 36 ++
web/src/pages/studio/AlertManagement.tsx | 674 +++++++++++++++++++++++++++++++
5 files changed, 791 insertions(+)
diff --git a/web/src/App.tsx b/web/src/App.tsx
index 3959a7c..e21f40c 100644
--- a/web/src/App.tsx
+++ b/web/src/App.tsx
@@ -33,6 +33,7 @@ import SystemAlertsPage from './pages/ops/systemAlerts';
import AuditPage from './pages/ops/audit';
import AiPage from './pages/ai';
import SettingsPage from './pages/settings';
+import AlertManagementPage from './pages/studio/AlertManagement';
import ProducerPage from './pages/studio/Producer';
import OpsPage from './pages/studio/Ops';
import LoginPage from './pages/login';
@@ -58,6 +59,7 @@ function App() {
<Route path="ops/audit" element={<AuditPage />} />
<Route path="ai" element={<AiPage />} />
<Route path="settings" element={<SettingsPage />} />
+ <Route path="studio/alert-management" element={<AlertManagementPage
/>} />
<Route path="studio/producer" element={<ProducerPage />} />
<Route path="studio/ops" element={<OpsPage />} />
<Route path="*" element={<Navigate to="/" replace />} />
diff --git a/web/src/api/alertManagement.test.ts
b/web/src/api/alertManagement.test.ts
new file mode 100644
index 0000000..b8ac892
--- /dev/null
+++ b/web/src/api/alertManagement.test.ts
@@ -0,0 +1,52 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import MockAdapter from 'axios-mock-adapter';
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
+import client from './client';
+import { queryAlertRules } from './alertManagement';
+
+const mock = new MockAdapter(client);
+
+describe('AlertManagement API', () => {
+ beforeEach(() => {
+ mock.reset();
+ vi.stubGlobal('localStorage', { getItem: vi.fn().mockReturnValue(null) });
+ });
+
+ afterEach(() => {
+ mock.reset();
+ vi.unstubAllGlobals();
+ });
+
+ it('queries alert rules data', async () => {
+ const rulesYaml =
+ 'groups:\n - name: test\n rules:\n - alert: HighCPU\n
expr: cpu > 80';
+ mock.onGet('/alert/rules').reply(200, { code: 200, data: { rules:
rulesYaml } });
+
+ const result = await queryAlertRules();
+ expect(result.rules).toBe(rulesYaml);
+ expect(result.rules).toContain('HighCPU');
+ });
+
+ it('handles empty alert rules', async () => {
+ mock.onGet('/alert/rules').reply(200, { code: 200, data: { rules: '' } });
+
+ const result = await queryAlertRules();
+ expect(result.rules).toBe('');
+ });
+});
diff --git a/web/src/api/alertManagement.ts b/web/src/api/alertManagement.ts
new file mode 100644
index 0000000..469b5f9
--- /dev/null
+++ b/web/src/api/alertManagement.ts
@@ -0,0 +1,27 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import client from './client';
+
+export interface AlertRuleData {
+ rules: string;
+}
+
+export async function queryAlertRules(): Promise<AlertRuleData> {
+ const res = await client.get<{ data: AlertRuleData }>('/alert/rules');
+ return res.data.data;
+}
diff --git a/web/src/i18n/translations.ts b/web/src/i18n/translations.ts
index b3d0930..7928b72 100644
--- a/web/src/i18n/translations.ts
+++ b/web/src/i18n/translations.ts
@@ -367,6 +367,42 @@ const translations: Record<string, Record<Lang, string>> =
{
},
'ops.fetchFailed': { zh: '获取运维数据失败', en: 'Failed to fetch ops data' },
+ // ─── Alert Management ───
+ 'alertMgmt.title': { zh: '告警规则管理', en: 'Alert Management' },
+ 'alertMgmt.alertName': { zh: '告警名称', en: 'Alert Name' },
+ 'alertMgmt.group': { zh: '规则组', en: 'Group' },
+ 'alertMgmt.severity': { zh: '严重级别', en: 'Severity' },
+ 'alertMgmt.team': { zh: '团队', en: 'Team' },
+ 'alertMgmt.expression': { zh: '表达式 (PromQL)', en: 'Expression (PromQL)' },
+ 'alertMgmt.forDuration': { zh: '持续时间', en: 'For Duration' },
+ 'alertMgmt.summary': { zh: '摘要', en: 'Summary' },
+ 'alertMgmt.description': { zh: '描述', en: 'Description' },
+ 'alertMgmt.totalRules': { zh: '规则总数', en: 'Total Rules' },
+ 'alertMgmt.enabled': { zh: '已启用', en: 'Enabled' },
+ 'alertMgmt.disabled': { zh: '已禁用', en: 'Disabled' },
+ 'alertMgmt.critical': { zh: '严重', en: 'Critical' },
+ 'alertMgmt.warningCount': { zh: '警告', en: 'Warning' },
+ 'alertMgmt.addRule': { zh: '添加规则', en: 'Add Rule' },
+ 'alertMgmt.editRule': { zh: '编辑规则', en: 'Edit Rule' },
+ 'alertMgmt.exportYaml': { zh: '导出 YAML', en: 'Export YAML' },
+ 'alertMgmt.searchPlaceholder': {
+ zh: '搜索告警名称、表达式...',
+ en: 'Search alert name, expression...',
+ },
+ 'alertMgmt.allGroups': { zh: '所有组', en: 'All Groups' },
+ 'alertMgmt.allSeverity': { zh: '所有级别', en: 'All Severity' },
+ 'alertMgmt.allStatus': { zh: '所有状态', en: 'All Status' },
+ 'alertMgmt.fetchFailed': { zh: '获取告警规则失败', en: 'Failed to fetch alert rules'
},
+ 'alertMgmt.deleteSuccess': { zh: '告警规则已删除', en: 'Alert rule deleted' },
+ 'alertMgmt.updateSuccess': { zh: '告警规则已更新', en: 'Alert rule updated' },
+ 'alertMgmt.createSuccess': { zh: '告警规则已创建', en: 'Alert rule created' },
+ 'alertMgmt.exportSuccess': { zh: '告警规则已导出', en: 'Alert rules exported' },
+ 'alertMgmt.alertNameRequired': { zh: '告警名称为必填项', en: 'Alert name is
required' },
+ 'alertMgmt.groupRequired': { zh: '规则组为必填项', en: 'Group is required' },
+ 'alertMgmt.expressionRequired': { zh: '表达式为必填项', en: 'Expression is
required' },
+ 'alertMgmt.forDurationRequired': { zh: '持续时间为必填项', en: 'Duration is
required' },
+ 'alertMgmt.summaryRequired': { zh: '摘要为必填项', en: 'Summary is required' },
+
// ─── Topic (detailed) ───
'topic.subtitle': {
zh: '管理 Topic 的创建、配置与删除',
diff --git a/web/src/pages/studio/AlertManagement.tsx
b/web/src/pages/studio/AlertManagement.tsx
new file mode 100644
index 0000000..dbdd6df
--- /dev/null
+++ b/web/src/pages/studio/AlertManagement.tsx
@@ -0,0 +1,674 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import React, { useMemo, useRef, useState } from 'react';
+import {
+ App,
+ Badge,
+ Button,
+ Card,
+ Col,
+ Form,
+ Input,
+ Modal,
+ Popconfirm,
+ Row,
+ Select,
+ Space,
+ Statistic,
+ Switch,
+ Table,
+ Tag,
+ Tooltip,
+} from 'antd';
+import type { ColumnsType } from 'antd/es/table';
+import {
+ ArrowClockwise,
+ DownloadSimple,
+ MagnifyingGlass,
+ Pencil,
+ Plus,
+ Trash,
+ Warning,
+} from '@phosphor-icons/react';
+import { useLang } from '../../i18n/LangContext';
+import { queryAlertRules } from '../../api/alertManagement';
+
+const { TextArea } = Input;
+
+// ─── Types ────────────────────────────────────────────────────────
+interface AlertRule {
+ key: string;
+ index: number;
+ alert: string;
+ group: string;
+ expr: string;
+ for: string;
+ severity: string;
+ team: string;
+ summary: string;
+ description: string;
+ enabled: boolean;
+}
+
+// ─── Constants ────────────────────────────────────────────────────
+const SEVERITY_COLORS: Record<string, string> = {
+ critical: 'red',
+ warning: 'orange',
+ info: 'blue',
+};
+
+const TEAM_COLORS: Record<string, string> = {
+ broker: 'purple',
+ topic: 'cyan',
+ consumer: 'green',
+ client: 'geekblue',
+ proxy: 'magenta',
+ security: 'volcano',
+ reliability: 'gold',
+};
+
+const GROUP_OPTIONS = [
+ 'rocketmq-broker.rules',
+ 'rocketmq-topic.rules',
+ 'rocketmq-consumer.rules',
+ 'rocketmq-client.rules',
+ 'rocketmq-proxy.rules',
+ 'rocketmq-errors.rules',
+ 'rocketmq-broker-extended.rules',
+];
+
+const TEAM_OPTIONS = ['broker', 'topic', 'consumer', 'client', 'proxy',
'security', 'reliability'];
+
+// ─── YAML Parser ──────────────────────────────────────────────────
+function parseYamlRules(yamlStr: string, disabledRules: Record<string,
boolean>): AlertRule[] {
+ const rules: AlertRule[] = [];
+ if (!yamlStr) return rules;
+
+ const groupBlocks = yamlStr.split(/\n(?=\s*- name:)/);
+ let ruleIndex = 0;
+
+ for (const block of groupBlocks) {
+ const groupNameMatch = block.match(/- name:\s*(.+)/);
+ if (!groupNameMatch) continue;
+ const groupName = groupNameMatch[1].trim();
+
+ const ruleBlocks = block.split(/\n\s*#\s*Rule\s+\d+:/);
+ for (let i = 1; i < ruleBlocks.length; i++) {
+ const ruleBlock = ruleBlocks[i];
+ const alertMatch = ruleBlock.match(/alert:\s*(.+)/);
+ const exprMatch = ruleBlock.match(/expr:\s*(.+)/);
+ const forMatch = ruleBlock.match(/for:\s*(.+)/);
+ const severityMatch = ruleBlock.match(/severity:\s*(.+)/);
+ const teamMatch = ruleBlock.match(/team:\s*(.+)/);
+ const summaryMatch = ruleBlock.match(/summary:\s*"(.+)"/);
+ const descMatch = ruleBlock.match(/description:\s*"(.+)"/);
+
+ if (alertMatch) {
+ ruleIndex++;
+ const alertName = alertMatch[1].trim();
+ rules.push({
+ key: alertName,
+ index: ruleIndex,
+ alert: alertName,
+ group: groupName,
+ expr: exprMatch ? exprMatch[1].trim() : '',
+ for: forMatch ? forMatch[1].trim() : '',
+ severity: severityMatch ? severityMatch[1].trim() : 'warning',
+ team: teamMatch ? teamMatch[1].trim() : '',
+ summary: summaryMatch ? summaryMatch[1].trim() : '',
+ description: descMatch ? descMatch[1].trim() : '',
+ enabled: !disabledRules[alertName],
+ });
+ }
+ }
+ }
+ return rules;
+}
+
+// ─── Component ────────────────────────────────────────────────────
+const AlertManagementPage: React.FC = () => {
+ const { t } = useLang();
+ const { message } = App.useApp();
+ const [form] = Form.useForm();
+
+ const [alertRules, setAlertRules] = useState<AlertRule[]>([]);
+ const [loading, setLoading] = useState(false);
+ const [modalVisible, setModalVisible] = useState(false);
+ const [editingRule, setEditingRule] = useState<AlertRule | null>(null);
+ const [searchText, setSearchText] = useState('');
+ const [filterGroup, setFilterGroup] = useState('all');
+ const [filterSeverity, setFilterSeverity] = useState('all');
+ const [filterStatus, setFilterStatus] = useState('all');
+ const [disabledRules, setDisabledRules] = useState<Record<string,
boolean>>(() => {
+ try {
+ const saved = localStorage.getItem('alertDisabledRules');
+ return saved ? JSON.parse(saved) : {};
+ } catch {
+ return {};
+ }
+ });
+
+ // One-time initialization (ESLint-compliant)
+ const initialized = useRef<boolean | null>(null);
+ if (initialized.current == null) {
+ initialized.current = true;
+ const loadRules = async () => {
+ setLoading(true);
+ try {
+ const data = await queryAlertRules();
+ const yamlStr = data.rules || '';
+ setAlertRules(parseYamlRules(yamlStr, disabledRules));
+ } catch {
+ message.error(t('alertMgmt.fetchFailed'));
+ } finally {
+ setLoading(false);
+ }
+ };
+ loadRules();
+ }
+
+ const fetchAlertRules = async () => {
+ setLoading(true);
+ try {
+ const data = await queryAlertRules();
+ const yamlStr = data.rules || '';
+ setAlertRules(parseYamlRules(yamlStr, disabledRules));
+ } catch {
+ message.error(t('alertMgmt.fetchFailed'));
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleToggleRule = (ruleKey: string) => {
+ const updated = { ...disabledRules, [ruleKey]: !disabledRules[ruleKey] };
+ setDisabledRules(updated);
+ localStorage.setItem('alertDisabledRules', JSON.stringify(updated));
+ setAlertRules((prev) =>
+ prev.map((rule) => (rule.key === ruleKey ? { ...rule, enabled:
!rule.enabled } : rule)),
+ );
+ };
+
+ const handleAddRule = () => {
+ setEditingRule(null);
+ form.resetFields();
+ form.setFieldsValue({ severity: 'warning', for: '5m', team: 'broker',
enabled: true });
+ setModalVisible(true);
+ };
+
+ const handleEditRule = (rule: AlertRule) => {
+ setEditingRule(rule);
+ form.setFieldsValue({
+ alert: rule.alert,
+ group: rule.group,
+ expr: rule.expr,
+ for: rule.for,
+ severity: rule.severity,
+ team: rule.team,
+ summary: rule.summary,
+ description: rule.description,
+ enabled: rule.enabled,
+ });
+ setModalVisible(true);
+ };
+
+ const handleDeleteRule = (ruleKey: string) => {
+ setAlertRules((prev) => prev.filter((rule) => rule.key !== ruleKey));
+ const updated = { ...disabledRules };
+ delete updated[ruleKey];
+ setDisabledRules(updated);
+ localStorage.setItem('alertDisabledRules', JSON.stringify(updated));
+ message.success(t('alertMgmt.deleteSuccess'));
+ };
+
+ const handleModalOk = async () => {
+ try {
+ const values = await form.validateFields();
+ if (editingRule) {
+ setAlertRules((prev) =>
+ prev.map((rule) =>
+ rule.key === editingRule.key
+ ? {
+ ...rule,
+ alert: values.alert,
+ group: values.group,
+ expr: values.expr,
+ for: values.for,
+ severity: values.severity,
+ team: values.team,
+ summary: values.summary || '',
+ description: values.description || '',
+ enabled: values.enabled !== false,
+ }
+ : rule,
+ ),
+ );
+ message.success(t('alertMgmt.updateSuccess'));
+ } else {
+ const newRule: AlertRule = {
+ key: values.alert,
+ index: alertRules.length + 1,
+ alert: values.alert,
+ group: values.group,
+ expr: values.expr,
+ for: values.for,
+ severity: values.severity,
+ team: values.team,
+ summary: values.summary || '',
+ description: values.description || '',
+ enabled: values.enabled !== false,
+ };
+ setAlertRules((prev) => [...prev, newRule]);
+ message.success(t('alertMgmt.createSuccess'));
+ }
+ setModalVisible(false);
+ form.resetFields();
+ } catch {
+ // validation failed
+ }
+ };
+
+ const handleExportYaml = () => {
+ const enabledRules = alertRules.filter((r) => r.enabled);
+ const groups: Record<string, AlertRule[]> = {};
+ for (const rule of enabledRules) {
+ if (!groups[rule.group]) groups[rule.group] = [];
+ groups[rule.group].push(rule);
+ }
+
+ let yaml = '#
==============================================================\n';
+ yaml += '# RocketMQ 5.x Monitoring — Alert Rules (Exported from
Dashboard)\n';
+ yaml += '# Compatible with Prometheus / VictoriaMetrics / Thanos
alerting\n';
+ yaml += '#
==============================================================\n\n';
+ yaml += 'groups:\n';
+
+ for (const [groupName, groupRules] of Object.entries(groups)) {
+ yaml += ` - name: ${groupName}\n`;
+ yaml += ' rules:\n';
+ for (const rule of groupRules) {
+ yaml += ` - alert: ${rule.alert}\n`;
+ yaml += ` expr: ${rule.expr}\n`;
+ yaml += ` for: ${rule.for}\n`;
+ yaml += ' labels:\n';
+ yaml += ` severity: ${rule.severity}\n`;
+ yaml += ` team: ${rule.team}\n`;
+ yaml += ' annotations:\n';
+ yaml += ` summary: "${rule.summary}"\n`;
+ if (rule.description) {
+ yaml += ` description: "${rule.description}"\n`;
+ }
+ yaml += '\n';
+ }
+ }
+
+ const blob = new Blob([yaml], { type: 'text/yaml' });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = 'rocketmq-alert-rules.yaml';
+ a.click();
+ URL.revokeObjectURL(url);
+ message.success(t('alertMgmt.exportSuccess'));
+ };
+
+ // ─── Derived data ─────────────────────────────────────────────
+ const filteredRules = useMemo(() => {
+ return alertRules.filter((rule) => {
+ const matchesSearch =
+ !searchText ||
+ rule.alert.toLowerCase().includes(searchText.toLowerCase()) ||
+ rule.expr.toLowerCase().includes(searchText.toLowerCase()) ||
+ rule.summary.toLowerCase().includes(searchText.toLowerCase());
+ const matchesGroup = filterGroup === 'all' || rule.group === filterGroup;
+ const matchesSeverity = filterSeverity === 'all' || rule.severity ===
filterSeverity;
+ const matchesStatus =
+ filterStatus === 'all' ||
+ (filterStatus === 'enabled' && rule.enabled) ||
+ (filterStatus === 'disabled' && !rule.enabled);
+ return matchesSearch && matchesGroup && matchesSeverity && matchesStatus;
+ });
+ }, [alertRules, searchText, filterGroup, filterSeverity, filterStatus]);
+
+ const groups = useMemo(() => Array.from(new Set(alertRules.map((r) =>
r.group))), [alertRules]);
+
+ const stats = useMemo(() => {
+ const total = alertRules.length;
+ const enabled = alertRules.filter((r) => r.enabled).length;
+ const critical = alertRules.filter((r) => r.severity ===
'critical').length;
+ const warning = alertRules.filter((r) => r.severity === 'warning').length;
+ return { total, enabled, disabled: total - enabled, critical, warning };
+ }, [alertRules]);
+
+ // ─── Table columns ─────────────────────────────────────────────
+ const columns: ColumnsType<AlertRule> = [
+ {
+ title: '#',
+ dataIndex: 'index',
+ width: 50,
+ render: (text: number) => <span style={{ color: '#999' }}>{text}</span>,
+ },
+ {
+ title: t('alertMgmt.alertName'),
+ dataIndex: 'alert',
+ width: 260,
+ render: (text: string, record: AlertRule) => (
+ <span style={{ fontWeight: 500, opacity: record.enabled ? 1 : 0.5
}}>{text}</span>
+ ),
+ },
+ {
+ title: t('alertMgmt.group'),
+ dataIndex: 'group',
+ width: 200,
+ render: (text: string) => (
+ <Tag color="blue">{text.replace('rocketmq-', '').replace('.rules',
'')}</Tag>
+ ),
+ },
+ {
+ title: t('alertMgmt.severity'),
+ dataIndex: 'severity',
+ width: 100,
+ render: (text: string) => (
+ <Tag color={SEVERITY_COLORS[text] ||
'default'}>{text.toUpperCase()}</Tag>
+ ),
+ },
+ {
+ title: t('alertMgmt.team'),
+ dataIndex: 'team',
+ width: 100,
+ render: (text: string) => <Tag color={TEAM_COLORS[text] ||
'default'}>{text}</Tag>,
+ },
+ {
+ title: t('alertMgmt.expression'),
+ dataIndex: 'expr',
+ ellipsis: true,
+ render: (text: string, record: AlertRule) => (
+ <Tooltip title={text}>
+ <code
+ style={{
+ fontSize: 12,
+ opacity: record.enabled ? 1 : 0.5,
+ background: '#f5f5f5',
+ padding: '2px 6px',
+ borderRadius: 4,
+ }}
+ >
+ {text}
+ </code>
+ </Tooltip>
+ ),
+ },
+ {
+ title: t('alertMgmt.forDuration'),
+ dataIndex: 'for',
+ width: 80,
+ render: (text: string) => <Tag>{text}</Tag>,
+ },
+ {
+ title: t('common.status'),
+ dataIndex: 'enabled',
+ width: 80,
+ render: (enabled: boolean, record: AlertRule) => (
+ <Switch size="small" checked={enabled} onChange={() =>
handleToggleRule(record.key)} />
+ ),
+ },
+ {
+ title: t('common.actions'),
+ width: 120,
+ render: (_: unknown, record: AlertRule) => (
+ <Space size="small">
+ <Tooltip title={t('common.edit')}>
+ <Button
+ type="text"
+ size="small"
+ icon={<Pencil size={16} />}
+ onClick={() => handleEditRule(record)}
+ />
+ </Tooltip>
+ <Popconfirm
+ title={t('common.areYouSureToDelete')}
+ onConfirm={() => handleDeleteRule(record.key)}
+ okText={t('common.confirm')}
+ cancelText={t('common.cancel')}
+ >
+ <Tooltip title={t('common.delete')}>
+ <Button type="text" size="small" danger icon={<Trash size={16}
/>} />
+ </Tooltip>
+ </Popconfirm>
+ </Space>
+ ),
+ },
+ ];
+
+ return (
+ <div style={{ padding: '0 0 24px' }}>
+ {/* Statistics */}
+ <Card size="small" style={{ marginBottom: 16 }}>
+ <Row gutter={16}>
+ <Col span={6}>
+ <Statistic
+ title={t('alertMgmt.totalRules')}
+ value={stats.total}
+ prefix={<Warning size={18} />}
+ />
+ </Col>
+ <Col span={6}>
+ <Statistic
+ title={t('alertMgmt.enabled')}
+ value={stats.enabled}
+ valueStyle={{ color: '#52c41a' }}
+ />
+ </Col>
+ <Col span={6}>
+ <Statistic
+ title={t('alertMgmt.critical')}
+ value={stats.critical}
+ valueStyle={{ color: '#cf1322' }}
+ />
+ </Col>
+ <Col span={6}>
+ <Statistic
+ title={t('alertMgmt.warningCount')}
+ value={stats.warning}
+ valueStyle={{ color: '#fa8c16' }}
+ />
+ </Col>
+ </Row>
+ </Card>
+
+ {/* Main Card */}
+ <Card
+ size="small"
+ title={
+ <Space>
+ <Warning size={18} />
+ <span>{t('alertMgmt.title')}</span>
+ <Badge count={filteredRules.length} style={{ backgroundColor:
'#1890ff' }} />
+ </Space>
+ }
+ extra={
+ <Space>
+ <Button
+ icon={<ArrowClockwise size={16} />}
+ onClick={fetchAlertRules}
+ loading={loading}
+ size="small"
+ >
+ {t('common.refresh')}
+ </Button>
+ <Button type="primary" icon={<Plus size={16} />}
onClick={handleAddRule} size="small">
+ {t('alertMgmt.addRule')}
+ </Button>
+ <Button icon={<DownloadSimple size={16} />}
onClick={handleExportYaml} size="small">
+ {t('alertMgmt.exportYaml')}
+ </Button>
+ </Space>
+ }
+ >
+ {/* Filters */}
+ <div style={{ marginBottom: 16, display: 'flex', gap: 12, flexWrap:
'wrap' }}>
+ <Input
+ placeholder={t('alertMgmt.searchPlaceholder')}
+ prefix={<MagnifyingGlass size={16} />}
+ value={searchText}
+ onChange={(e) => setSearchText(e.target.value)}
+ style={{ width: 280 }}
+ allowClear
+ />
+ <Select
+ value={filterGroup}
+ onChange={setFilterGroup}
+ style={{ width: 180 }}
+ options={[
+ { value: 'all', label: t('alertMgmt.allGroups') },
+ ...groups.map((g) => ({
+ value: g,
+ label: g.replace('rocketmq-', '').replace('.rules', ''),
+ })),
+ ]}
+ />
+ <Select
+ value={filterSeverity}
+ onChange={setFilterSeverity}
+ style={{ width: 130 }}
+ options={[
+ { value: 'all', label: t('alertMgmt.allSeverity') },
+ { value: 'critical', label: 'Critical' },
+ { value: 'warning', label: 'Warning' },
+ ]}
+ />
+ <Select
+ value={filterStatus}
+ onChange={setFilterStatus}
+ style={{ width: 130 }}
+ options={[
+ { value: 'all', label: t('alertMgmt.allStatus') },
+ { value: 'enabled', label: t('alertMgmt.enabled') },
+ { value: 'disabled', label: t('alertMgmt.disabled') },
+ ]}
+ />
+ </div>
+
+ {/* Table */}
+ <Table
+ columns={columns}
+ dataSource={filteredRules}
+ loading={loading}
+ rowKey="key"
+ size="small"
+ pagination={{
+ pageSize: 10,
+ showSizeChanger: true,
+ showTotal: (total) => `${t('common.total')} ${total}`,
+ }}
+ scroll={{ x: 1200 }}
+ />
+ </Card>
+
+ {/* Add/Edit Modal */}
+ <Modal
+ title={editingRule ? t('alertMgmt.editRule') : t('alertMgmt.addRule')}
+ open={modalVisible}
+ onOk={handleModalOk}
+ onCancel={() => {
+ setModalVisible(false);
+ form.resetFields();
+ }}
+ width={720}
+ destroyOnClose
+ >
+ <Form form={form} layout="vertical" style={{ marginTop: 16 }}>
+ <Row gutter={16}>
+ <Col span={12}>
+ <Form.Item
+ name="alert"
+ label={t('alertMgmt.alertName')}
+ rules={[{ required: true, message:
t('alertMgmt.alertNameRequired') }]}
+ >
+ <Input placeholder="e.g. RocketMQ_Broker_Down" />
+ </Form.Item>
+ </Col>
+ <Col span={12}>
+ <Form.Item
+ name="group"
+ label={t('alertMgmt.group')}
+ rules={[{ required: true, message:
t('alertMgmt.groupRequired') }]}
+ >
+ <Select
+ placeholder={t('common.pleaseSelect')}
+ options={GROUP_OPTIONS.map((g) => ({
+ value: g,
+ label: g.replace('rocketmq-', '').replace('.rules', ''),
+ }))}
+ />
+ </Form.Item>
+ </Col>
+ </Row>
+ <Form.Item
+ name="expr"
+ label={t('alertMgmt.expression')}
+ rules={[{ required: true, message:
t('alertMgmt.expressionRequired') }]}
+ >
+ <TextArea rows={2} placeholder={'e.g.
up{job=~"rocketmq.*broker.*"} == 0'} />
+ </Form.Item>
+ <Row gutter={16}>
+ <Col span={8}>
+ <Form.Item
+ name="for"
+ label={t('alertMgmt.forDuration')}
+ rules={[{ required: true, message:
t('alertMgmt.forDurationRequired') }]}
+ >
+ <Input placeholder="e.g. 5m" />
+ </Form.Item>
+ </Col>
+ <Col span={8}>
+ <Form.Item
+ name="severity"
+ label={t('alertMgmt.severity')}
+ rules={[{ required: true }]}
+ >
+ <Select
+ options={[
+ { value: 'critical', label: 'Critical' },
+ { value: 'warning', label: 'Warning' },
+ ]}
+ />
+ </Form.Item>
+ </Col>
+ <Col span={8}>
+ <Form.Item name="team" label={t('alertMgmt.team')} rules={[{
required: true }]}>
+ <Select options={TEAM_OPTIONS.map((t) => ({ value: t, label: t
}))} />
+ </Form.Item>
+ </Col>
+ </Row>
+ <Form.Item
+ name="summary"
+ label={t('alertMgmt.summary')}
+ rules={[{ required: true, message: t('alertMgmt.summaryRequired')
}]}
+ >
+ <Input placeholder="Brief description of the alert" />
+ </Form.Item>
+ <Form.Item name="description" label={t('alertMgmt.description')}>
+ <TextArea rows={2} placeholder="Detailed description (optional)" />
+ </Form.Item>
+ </Form>
+ </Modal>
+ </div>
+ );
+};
+
+export default AlertManagementPage;