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:
LoganandClaude committed 2026-07-01 23:44:45 +08:00
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}