feat: 监控告警体系统一重构 + 自动化任务调度引擎 + 监控大屏拆分
告警中心 - 新增统一告警(alert_unified)、监控告警规则(monitor_alert_rule)、指标目录(alert_metric_catalog) - 通知策略与消息分发(alert_notify_policy/dispatch/message)、静默规则 - 告警引擎按阈值/SNMP 拆分(alert_engine_monitor/snmp) 监控采集 - SNMP 常量/模板与历史指标(snmp templates/constants、snmp_metrics_history、snmp_collect) - 中间件监控 Redis 采集与种子(middleware_monitor_redis/seed) - 站点探测 pkg(siteprobe) + 站点视图(site_view) 自动化任务 - cron 调度器(cronscheduler)、脚本运行器(scriptrunner)、系统任务(systask)及运行记录(cron_job_run、system_task) 审计与凭证 - 审计设置(audit_setting) + 历史清理(auditcleaner) 前端 - 监控页面(host/middleware/site/snmp)拆分为子模块并大屏化 - 告警中心多 Tab(规则/指标目录/通知策略/静默)、任务管理组件重构、Agent CLI Markdown 渲染 - 移除旧脚本卡片/详情/日志组件,统一为 ScriptTable/ScriptCodeEditor/TerminalOutput Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
1 parent
092d0e4b9e
commit
471353e3d7
190 files changed
+16640
-3484
No files matched your search
@@ -1,8 +1,9 @@
|
||||
// 告警中心主组件 — TanStack Query 编排 // Logan.Li
|
||||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import {
|
||||
AlertTriangle, CheckCircle, Search, RefreshCw,
|
||||
Eye, Check, VolumeX, Plus, Rocket,
|
||||
Eye, Check, VolumeX, Plus,
|
||||
} from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
@@ -12,23 +13,24 @@ import { StatStrip } from '@/components/ui/stat-strip';
|
||||
import {
|
||||
Select, SelectContent, SelectItem, SelectTrigger, SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { toast } from 'sonner';
|
||||
import { PageShell } from '@/components/layout/PageShell';
|
||||
import { PageEmpty } from '@/components/layout/PageEmpty';
|
||||
import { PageLoading } from '@/components/layout/PageLoading';
|
||||
import { useConfirm } from '@/components/ui/confirm-dialog';
|
||||
import { alertCenterApi, alertApi } from '@/api';
|
||||
import { useAlerts, useAgentAlertRules, useAlertMutations } from '@/hooks/api';
|
||||
import { useAlerts, useUnifiedAlertRules, useMetricCatalog, useAlertMutations } from '@/hooks/api';
|
||||
import {
|
||||
type Alert, type AlertRule, getSeverityBadge, getStatusBadge,
|
||||
getChannelTypeLabel,
|
||||
type Alert, getSeverityBadge, getStatusBadge, getChannelTypeLabel,
|
||||
} from './alert-center-helpers';
|
||||
import { AlertDetailDialog, AlertSilenceDialog } from './AlertCenterDetail';
|
||||
import { AlertRuleDialog } from '@/components/monitor/AlertRuleDialog';
|
||||
import { MetricCatalogTab } from './MetricCatalogTab';
|
||||
import { AlertRulesTab } from './AlertRulesTab';
|
||||
import { ContactDialog } from './ContactDialog';
|
||||
import { ContactGroupDialog } from './ContactGroupDialog';
|
||||
import SilenceRuleTab from './SilenceRuleTab';
|
||||
import { NotifyPolicyTab } from './NotifyPolicyTab';
|
||||
|
||||
import type { UnifiedAlertRule } from '@/api';
|
||||
import type { Contact, ContactGroup } from './alert-center-helpers';
|
||||
import type { JsonData } from '@/types/common';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -39,13 +41,11 @@ const AlertCenter: React.FC = () => {
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [severityFilter, setSeverityFilter] = useState('all');
|
||||
const [statusFilter, setStatusFilter] = useState('all');
|
||||
const [sourceFilter, setSourceFilter] = useState('all');
|
||||
const [activeTab, setActiveTab] = useState('alerts');
|
||||
const [selectedAlert, setSelectedAlert] = useState<Alert | null>(null);
|
||||
const [detailOpen, setDetailOpen] = useState(false);
|
||||
const [silenceOpen, setSilenceOpen] = useState(false);
|
||||
// 规则弹框
|
||||
const [ruleDialogOpen, setRuleDialogOpen] = useState(false);
|
||||
const [editingRule, setEditingRule] = useState<JsonData>(null);
|
||||
// 联系人 & 联系组
|
||||
const [contacts, setContacts] = useState<Contact[]>([]);
|
||||
const [contactGroups, setContactGroups] = useState<ContactGroup[]>([]);
|
||||
@@ -58,20 +58,17 @@ const AlertCenter: React.FC = () => {
|
||||
const [pageSize] = useState(10);
|
||||
|
||||
const alertParams = useMemo(() => ({
|
||||
page,
|
||||
page_size: pageSize,
|
||||
severity: severityFilter,
|
||||
status: statusFilter,
|
||||
}), [page, pageSize, severityFilter, statusFilter]);
|
||||
page, page_size: pageSize,
|
||||
severity: severityFilter, status: statusFilter, source_type: sourceFilter,
|
||||
}), [page, pageSize, severityFilter, statusFilter, sourceFilter]);
|
||||
|
||||
const { data: alertData, isLoading: loading, refetch: refetchAlerts } = useAlerts(alertParams);
|
||||
const { data: rules = [], refetch: refetchRules } = useAgentAlertRules();
|
||||
const { data: alertData, isLoading: loading, isFetching: fetchingAlerts, refetch: refetchAlerts } = useAlerts(alertParams);
|
||||
const { data: rules = [], isFetching: fetchingRules, refetch: refetchRules } = useUnifiedAlertRules('all');
|
||||
const { data: catalog = [] } = useMetricCatalog();
|
||||
const {
|
||||
ackMutation,
|
||||
resolveMutation,
|
||||
quickSetupMutation,
|
||||
toggleAgentRuleMutation,
|
||||
deleteAgentRuleMutation,
|
||||
ackMutation, resolveMutation, quickSetupMutation, monitorQuickSetupMutation,
|
||||
toggleAgentRuleMutation, deleteAgentRuleMutation,
|
||||
deleteMonitorRuleMutation, toggleMonitorRuleMutation,
|
||||
} = useAlertMutations();
|
||||
|
||||
const alerts = alertData?.list ?? [];
|
||||
@@ -86,15 +83,15 @@ const AlertCenter: React.FC = () => {
|
||||
|
||||
const fetchContacts = async () => {
|
||||
try {
|
||||
const res: JsonData = await alertApi.getContacts();
|
||||
setContacts((res?.data?.list || res?.list || []) as Contact[]);
|
||||
const res = await alertApi.getContacts() as { list?: Contact[] };
|
||||
setContacts(res?.list || []);
|
||||
} catch { /* silent */ }
|
||||
};
|
||||
|
||||
const fetchContactGroups = async () => {
|
||||
try {
|
||||
const res: JsonData = await alertApi.getContactGroups();
|
||||
setContactGroups((res?.data?.list || res?.list || []) as ContactGroup[]);
|
||||
const res = await alertApi.getContactGroups() as { list?: ContactGroup[] };
|
||||
setContactGroups(res?.list || []);
|
||||
} catch { /* silent */ }
|
||||
};
|
||||
|
||||
@@ -140,19 +137,6 @@ const AlertCenter: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const toggleRule = async (ruleId: string, enabled: boolean) => {
|
||||
try {
|
||||
await toggleAgentRuleMutation.mutateAsync({ id: Number(ruleId), enabled });
|
||||
toast.success(t('alert.ruleUpdated'));
|
||||
} catch {
|
||||
toast.error(t('alert.updateFailed'));
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpenAddRule = () => { setEditingRule(null); setRuleDialogOpen(true); };
|
||||
const handleEditRule = (rule: AlertRule) => { setEditingRule(rule); setRuleDialogOpen(true); };
|
||||
const handleRuleDialogClose = () => { setRuleDialogOpen(false); setEditingRule(null); };
|
||||
|
||||
const handleQuickSetup = async () => {
|
||||
const ok = await _confirm({
|
||||
title: t('alert.quickSetup'),
|
||||
@@ -168,24 +152,32 @@ const AlertCenter: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteRule = async (rule: AlertRule) => {
|
||||
const handleDeleteAgentRule = async (rule: UnifiedAlertRule) => {
|
||||
const ok = await _confirm({ title: t('alert.deleteRule'), description: `${rule.name}`, confirmText: t('common.delete'), variant: 'destructive' });
|
||||
if (!ok) return;
|
||||
try { await deleteAgentRuleMutation.mutateAsync(Number(rule.id)); toast.success(t('alert.deleted')); } catch { toast.error(t('alert.deleteFailed')); }
|
||||
try { await deleteAgentRuleMutation.mutateAsync(rule.id); toast.success(t('alert.deleted')); } catch { toast.error(t('alert.deleteFailed')); }
|
||||
};
|
||||
|
||||
const handleFilterChange = (type: 'severity' | 'status', value: string) => {
|
||||
const handleDeleteMonitorRule = async (rule: UnifiedAlertRule) => {
|
||||
const ok = await _confirm({ title: t('alert.deleteRule'), description: `${rule.name}`, confirmText: t('common.delete'), variant: 'destructive' });
|
||||
if (!ok) return;
|
||||
try { await deleteMonitorRuleMutation.mutateAsync(rule.id); toast.success(t('alert.deleted')); } catch { toast.error(t('alert.deleteFailed')); }
|
||||
};
|
||||
|
||||
const handleFilterChange = (type: 'severity' | 'status' | 'source', value: string) => {
|
||||
if (type === 'severity') setSeverityFilter(value);
|
||||
else setStatusFilter(value);
|
||||
setPage(1); // 过滤条件变化时重置到第一页
|
||||
else if (type === 'status') setStatusFilter(value);
|
||||
else setSourceFilter(value);
|
||||
setPage(1);
|
||||
};
|
||||
|
||||
const pageTabs = [
|
||||
{ id: 'alerts', label: t('alert.tab.alerts') },
|
||||
{ id: 'rules', label: t('alert.tab.rules') },
|
||||
{ id: 'silences', label: t('alert.tab.silences') },
|
||||
{ id: 'metrics', label: '指标目录' },
|
||||
{ id: 'contacts', label: t('alert.tab.contacts') },
|
||||
{ id: 'groups', label: t('alert.tab.groups') },
|
||||
{ id: 'notify', label: '通知策略' },
|
||||
];
|
||||
|
||||
const alertsToolbar = activeTab === 'alerts' ? (
|
||||
@@ -222,11 +214,24 @@ const AlertCenter: React.FC = () => {
|
||||
<SelectItem value="resolved">{t('alert.status.resolved')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Select value={sourceFilter} onValueChange={(v) => handleFilterChange('source', v)}>
|
||||
<SelectTrigger className="w-[120px] h-8 text-xs">
|
||||
<SelectValue placeholder="来源" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">全部来源</SelectItem>
|
||||
<SelectItem value="host">主机监控</SelectItem>
|
||||
<SelectItem value="middleware">中间件监控</SelectItem>
|
||||
<SelectItem value="snmp">SNMP 监控</SelectItem>
|
||||
<SelectItem value="site">站点拨测</SelectItem>
|
||||
<SelectItem value="log">日志告警</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 text-xs dg-text-muted">
|
||||
<span>{t('alert.totalItems', { count: total })}</span>
|
||||
<Button variant="outline" size="sm" onClick={() => refetchAlerts()} className="h-7 text-xs px-2 dg-btn-secondary">
|
||||
<RefreshCw className="w-3 h-3 mr-1" />{t('common.refresh')}
|
||||
<Button variant="outline" size="sm" onClick={() => refetchAlerts()} disabled={fetchingAlerts} className="h-7 text-xs px-2 dg-btn-secondary">
|
||||
<RefreshCw className={cn('w-3 h-3 mr-1', fetchingAlerts && 'animate-spin')} />{t('common.refresh')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -253,18 +258,9 @@ const AlertCenter: React.FC = () => {
|
||||
/>
|
||||
)}
|
||||
actions={activeTab === 'rules' ? (
|
||||
<>
|
||||
<Button className="dg-btn-primary" onClick={handleOpenAddRule}>
|
||||
<Plus className="w-4 h-4 mr-1" />{t('alert.addRule')}
|
||||
</Button>
|
||||
<Button variant="secondary" className="dg-btn-secondary" onClick={handleQuickSetup} disabled={quickSetupMutation.isPending}>
|
||||
<Rocket className="w-4 h-4 mr-1" />
|
||||
{quickSetupMutation.isPending ? t('alert.quickSetupCreating') : t('alert.quickSetup')}
|
||||
</Button>
|
||||
<Button variant="outline" className="dg-btn-secondary" onClick={() => refetchRules()}>
|
||||
<RefreshCw className="w-4 h-4 mr-2" />{t('common.refresh')}
|
||||
</Button>
|
||||
</>
|
||||
<Button variant="outline" className="dg-btn-secondary" onClick={() => refetchRules()} disabled={fetchingRules}>
|
||||
<RefreshCw className={cn('w-4 h-4 mr-2', fetchingRules && 'animate-spin')} />{t('common.refresh')}
|
||||
</Button>
|
||||
) : activeTab === 'contacts' ? (
|
||||
<Button size="sm" className="dg-btn-primary" onClick={() => { setEditingContact(null); setContactDialogOpen(true); }}>
|
||||
<Plus className="w-4 h-4 mr-1" />添加联系人
|
||||
@@ -393,66 +389,22 @@ const AlertCenter: React.FC = () => {
|
||||
)}
|
||||
|
||||
{activeTab === 'rules' && (
|
||||
rules.length === 0 ? (
|
||||
<PageEmpty
|
||||
icon={<AlertTriangle className="w-10 h-10" />}
|
||||
title={t('alert.noRules')}
|
||||
description={t('alert.noRulesHint')}
|
||||
/>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
{rules.map((rule) => (
|
||||
<Card key={rule.id}>
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className="font-medium">{rule.name}</span>
|
||||
{getSeverityBadge(rule.severity)}
|
||||
<Badge variant={rule.enabled ? 'default' : 'secondary'}>
|
||||
{rule.enabled ? t('alert.enabled') : t('alert.disabled')}
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground mb-2">{rule.description}</p>
|
||||
<div className="flex items-center gap-4 text-xs text-muted-foreground">
|
||||
{rule.expression && (
|
||||
<span className="font-mono bg-muted px-2 py-1 rounded">{rule.expression}</span>
|
||||
)}
|
||||
{rule.duration ? <span>{t('alert.durationSeconds', { count: rule.duration })}</span> : null}
|
||||
</div>
|
||||
{(() => {
|
||||
let chs: string[] = [];
|
||||
try {
|
||||
const raw = rule.notify_channels;
|
||||
if (raw) chs = Array.isArray(raw) ? raw : JSON.parse(raw);
|
||||
} catch { /* ignore */ }
|
||||
if (!chs.length) return null;
|
||||
return (
|
||||
<div className="flex gap-1 mt-2">
|
||||
{chs.map(ch => (
|
||||
<Badge key={ch} variant="outline" className="text-xs">{ch}</Badge>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="ghost" size="sm" onClick={() => handleEditRule(rule)}>{t('common.edit')}</Button>
|
||||
<Button variant="ghost" size="sm" className="dg-text-red" onClick={() => handleDeleteRule(rule)}>{t('common.delete')}</Button>
|
||||
<Switch
|
||||
checked={rule.enabled}
|
||||
onCheckedChange={(checked) => toggleRule(String(rule.id), checked)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
<AlertRulesTab
|
||||
rules={rules}
|
||||
loading={fetchingRules}
|
||||
catalog={catalog}
|
||||
onRefresh={() => refetchRules()}
|
||||
onToggleAgent={(id, enabled) => toggleAgentRuleMutation.mutate({ id, enabled })}
|
||||
onDeleteAgent={handleDeleteAgentRule}
|
||||
onDeleteMonitor={handleDeleteMonitorRule}
|
||||
onToggleMonitor={(id, enabled) => toggleMonitorRuleMutation.mutate({ id, enabled })}
|
||||
onQuickSetupHost={handleQuickSetup}
|
||||
onQuickSetupMonitor={(cat) => monitorQuickSetupMutation.mutate(cat)}
|
||||
quickSetupPending={quickSetupMutation.isPending || monitorQuickSetupMutation.isPending}
|
||||
/>
|
||||
)}
|
||||
|
||||
{activeTab === 'silences' && <SilenceRuleTab />}
|
||||
{activeTab === 'metrics' && <MetricCatalogTab />}
|
||||
|
||||
{activeTab === 'contacts' && (
|
||||
contacts.length === 0 ? (
|
||||
@@ -503,6 +455,8 @@ const AlertCenter: React.FC = () => {
|
||||
)
|
||||
)}
|
||||
|
||||
{activeTab === 'notify' && <NotifyPolicyTab />}
|
||||
|
||||
{activeTab === 'groups' && (
|
||||
contactGroups.length === 0 ? (
|
||||
<PageEmpty title="暂无报警联系组" description="点击「添加联系组」创建" />
|
||||
@@ -564,13 +518,6 @@ const AlertCenter: React.FC = () => {
|
||||
onOpenChange={setSilenceOpen}
|
||||
onSilence={handleSilence}
|
||||
/>
|
||||
<AlertRuleDialog
|
||||
open={ruleDialogOpen}
|
||||
onOpenChange={handleRuleDialogClose}
|
||||
editingRule={editingRule}
|
||||
notifyChannels={[]}
|
||||
onSaved={() => { handleRuleDialogClose(); refetchRules(); }}
|
||||
/>
|
||||
<ContactDialog
|
||||
open={contactDialogOpen}
|
||||
onOpenChange={setContactDialogOpen}
|
||||
|
||||
Reference in new issue
Block a user