Files
aiops/frontend/src/components/task/SystemTasksPanel.tsx
T
LoganandClaude 471353e3d7 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>
2026-07-01 23:44:45 +08:00

219 lines
8.6 KiB
TypeScript

// 内置系统任务面板 — 表格式紧凑布局 // 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>
);
}