告警中心 - 新增统一告警(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>
219 lines
8.6 KiB
TypeScript
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>
|
|
);
|
|
} |