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

commit e2f33b28568759ce8304427c30e7d5e838e65e64
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 3959a7c9..e21f40c5 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 00000000..b8ac8929
--- /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 00000000..469b5f91
--- /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 b3d0930a..7928b720 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 00000000..dbdd6df3
--- /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;

Reply via email to