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
@@ -0,0 +1,219 @@
|
||||
// 内置系统任务面板 — 表格式紧凑布局 // Logan.Li
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { ContentPanel } from '@/components/layout/ContentPanel';
|
||||
import { PageEmpty } from '@/components/layout/PageEmpty';
|
||||
import { systemTaskApi, type SystemTaskItem } from '@/api';
|
||||
import { usePermission } from '@/hooks/usePermission';
|
||||
import { useConfirm } from '@/components/ui/confirm-dialog';
|
||||
import { toast } from 'sonner';
|
||||
import { formatRelativeTime } from './cronJobHelpers';
|
||||
import {
|
||||
ServerCog, Play, Loader2, Shield, Trash2, Clock, CheckCircle, XCircle, AlertCircle,
|
||||
} from 'lucide-react';
|
||||
|
||||
const categoryLabel: Record<string, string> = {
|
||||
monitor: '监控',
|
||||
cleanup: '清理',
|
||||
};
|
||||
|
||||
function statusBadge(status?: string) {
|
||||
switch (status) {
|
||||
case 'success':
|
||||
return (
|
||||
<Badge variant="outline" className="border-emerald-500/30 dg-text-green bg-emerald-500/10 gap-1 h-5 text-[11px] px-1.5">
|
||||
<CheckCircle className="h-2.5 w-2.5" />成功
|
||||
</Badge>
|
||||
);
|
||||
case 'failed':
|
||||
return (
|
||||
<Badge variant="outline" className="border-red-500/30 dg-text-red bg-red-500/10 gap-1 h-5 text-[11px] px-1.5">
|
||||
<XCircle className="h-2.5 w-2.5" />失败
|
||||
</Badge>
|
||||
);
|
||||
case 'running':
|
||||
return (
|
||||
<Badge variant="outline" className="border-blue-500/30 dg-text-blue bg-blue-500/10 gap-1 h-5 text-[11px] px-1.5">
|
||||
<Loader2 className="h-2.5 w-2.5 animate-spin" />运行中
|
||||
</Badge>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<Badge variant="outline" className="border-[var(--dg-border)] dg-text-muted gap-1 h-5 text-[11px] px-1.5">
|
||||
<AlertCircle className="h-2.5 w-2.5" />待执行
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
interface Props {
|
||||
refreshing?: boolean;
|
||||
}
|
||||
|
||||
export function SystemTasksPanel({ refreshing }: Props) {
|
||||
const _confirm = useConfirm();
|
||||
const { hasAnyPermission, isAdmin } = usePermission();
|
||||
const canRun = isAdmin || hasAnyPermission(['auto:script', 'auto:cron', 'auto:ansible']);
|
||||
const [tasks, setTasks] = useState<SystemTaskItem[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [runningCode, setRunningCode] = useState<string | null>(null);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
try {
|
||||
const res = await systemTaskApi.list();
|
||||
setTasks(res.list || []);
|
||||
} catch (e: unknown) {
|
||||
toast.error(e instanceof Error ? e.message : '加载系统任务失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
load();
|
||||
}, [load, refreshing]);
|
||||
|
||||
const handleRun = async (task: SystemTaskItem) => {
|
||||
const ok = await _confirm({
|
||||
title: '立即执行',
|
||||
description: `确定手动执行系统任务「${task.name}」吗?`,
|
||||
confirmText: '执行',
|
||||
});
|
||||
if (!ok) return;
|
||||
setRunningCode(task.code);
|
||||
try {
|
||||
const res = await systemTaskApi.run(task.code);
|
||||
toast.success(res.last_message || '执行完成');
|
||||
await load();
|
||||
} catch (e: unknown) {
|
||||
toast.error(e instanceof Error ? e.message : '执行失败');
|
||||
} finally {
|
||||
setRunningCode(null);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex justify-center py-12">
|
||||
<Loader2 className="h-6 w-6 animate-spin dg-text-muted" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (tasks.length === 0) {
|
||||
return <PageEmpty title="暂无系统任务" />;
|
||||
}
|
||||
|
||||
const failedCount = tasks.filter((t) => t.last_status === 'failed').length;
|
||||
|
||||
return (
|
||||
<ContentPanel>
|
||||
<div className="flex items-center justify-between gap-3 mb-3 px-0.5">
|
||||
<p className="text-xs dg-text-muted">
|
||||
进程内置调度,不可编辑;清理类任务支持手动触发
|
||||
</p>
|
||||
<div className="flex items-center gap-2 text-[11px] shrink-0">
|
||||
<span className="dg-text-muted">{tasks.length} 个任务</span>
|
||||
{failedCount > 0 && (
|
||||
<Badge variant="outline" className="border-red-500/30 dg-text-red bg-red-500/10 h-5 text-[10px]">
|
||||
{failedCount} 个失败
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border border-[var(--dg-border)] overflow-hidden">
|
||||
{/* 表头 */}
|
||||
<div className="hidden md:grid md:grid-cols-[minmax(0,1.2fr)_minmax(0,1.6fr)_minmax(0,1fr)_72px] gap-3 px-3 py-2 bg-[var(--dg-bg-secondary)] border-b border-[var(--dg-border)] text-[11px] dg-text-muted">
|
||||
<span>任务</span>
|
||||
<span>调度 / 说明</span>
|
||||
<span>最近执行</span>
|
||||
<span className="text-right">操作</span>
|
||||
</div>
|
||||
|
||||
<div className="divide-y divide-[var(--dg-border)]">
|
||||
{tasks.map((task) => (
|
||||
<div
|
||||
key={task.code}
|
||||
className="grid grid-cols-1 md:grid-cols-[minmax(0,1.2fr)_minmax(0,1.6fr)_minmax(0,1fr)_72px] gap-2 md:gap-3 px-3 py-2 items-center hover:bg-[var(--dg-bg-hover)]/40 transition-colors"
|
||||
>
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<ServerCog className="h-3.5 w-3.5 dg-text-cyan shrink-0" />
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-center gap-1 flex-wrap">
|
||||
<span className="text-sm font-medium text-[var(--dg-text-primary)] truncate">{task.name}</span>
|
||||
<Badge variant="outline" className="border-cyan-500/30 text-cyan-500 bg-cyan-500/10 h-5 text-[10px] px-1">
|
||||
内置
|
||||
</Badge>
|
||||
<Badge variant="outline" className="border-[var(--dg-border)] dg-text-secondary h-5 text-[10px] px-1">
|
||||
{categoryLabel[task.category] || task.category}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="mt-0.5">{statusBadge(task.last_status)}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="min-w-0 text-xs dg-text-secondary pl-5 md:pl-0">
|
||||
<p className="truncate" title={task.description}>{task.description}</p>
|
||||
<div className="flex flex-wrap items-center gap-x-2.5 mt-0.5 dg-text-muted text-[11px]">
|
||||
<span className="inline-flex items-center gap-1">
|
||||
<Clock className="h-3 w-3" />
|
||||
{task.schedule}
|
||||
</span>
|
||||
{task.category === 'cleanup' && task.code === 'audit_log_cleanup' && task.extra?.retention_days != null && (
|
||||
<span className="inline-flex items-center gap-1">
|
||||
<Trash2 className="h-3 w-3" />
|
||||
保留 {String(task.extra.retention_days)} 天
|
||||
</span>
|
||||
)}
|
||||
{task.code === 'alert_engine' && task.extra && (
|
||||
<span className="inline-flex items-center gap-1">
|
||||
<Shield className="h-3 w-3" />
|
||||
{(task.extra as { is_leader?: boolean }).is_leader ? 'Leader' : 'Follower'}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="min-w-0 text-[11px] dg-text-muted pl-5 md:pl-0">
|
||||
<div>{formatRelativeTime(task.last_run_at ?? undefined)}</div>
|
||||
<div className="flex items-center gap-2 mt-0.5">
|
||||
{task.last_duration_ms != null && task.last_duration_ms > 0 && (
|
||||
<span>{task.last_duration_ms}ms</span>
|
||||
)}
|
||||
<span>{task.run_count ?? 0} 次</span>
|
||||
</div>
|
||||
{task.last_message && (
|
||||
<p className="truncate font-mono mt-0.5 max-w-full" title={task.last_message}>
|
||||
{task.last_message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-start md:justify-end pl-5 md:pl-0">
|
||||
{task.runnable && canRun ? (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="border-emerald-600 dg-text-green hover:bg-emerald-600/20 h-7 px-2 text-xs"
|
||||
disabled={runningCode === task.code}
|
||||
onClick={() => handleRun(task)}
|
||||
>
|
||||
{runningCode === task.code ? (
|
||||
<Loader2 className="h-3 w-3 animate-spin" />
|
||||
) : (
|
||||
<Play className="h-3 w-3" />
|
||||
)}
|
||||
</Button>
|
||||
) : (
|
||||
<span className="text-[11px] dg-text-muted">自动</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</ContentPanel>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user