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
@@ -7,6 +7,9 @@ import {
|
||||
} from 'lucide-react';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { toast } from 'sonner';
|
||||
import { approvalWorkflowApi, userApi } from '@/api';
|
||||
import { useConfirm } from '@/components/ui/confirm-dialog';
|
||||
@@ -66,7 +69,30 @@ const STEP_TYPE_ICONS: Record<string, React.ElementType> = {
|
||||
execute: Play,
|
||||
};
|
||||
|
||||
/** 解析审批人 ID 列表 // Logan.Li */
|
||||
function parseApproverIds(ids: string | null): number[] {
|
||||
if (!ids) return [];
|
||||
try {
|
||||
const p = JSON.parse(ids);
|
||||
return Array.isArray(p) ? p : [];
|
||||
} catch { return []; }
|
||||
}
|
||||
|
||||
/** 查找 admin 用户 ID,用于新建流程默认审批/执行人 // Logan.Li */
|
||||
function resolveAdminId(users: UserItem[]): number[] {
|
||||
const admin = users.find((u) => u.username === 'admin');
|
||||
return admin ? [admin.id] : [];
|
||||
}
|
||||
|
||||
/** 新建流程默认步骤:提交 → admin 审批 → admin 执行 // Logan.Li */
|
||||
function buildDefaultSteps(users: UserItem[], t: (key: string) => string): StepDraft[] {
|
||||
const adminIds = resolveAdminId(users);
|
||||
return [
|
||||
{ name: t('approval.detail.submitNode'), step_type: 'submit', approver_ids: [] },
|
||||
{ name: t('approval.step.auditShort'), step_type: 'audit', approver_ids: adminIds },
|
||||
{ name: t('approval.step.executeShort'), step_type: 'execute', approver_ids: adminIds },
|
||||
];
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* UserSelectInline */
|
||||
@@ -210,25 +236,13 @@ function WorkflowEditorModal({ workflow, users, onClose, onSaved }: EditorProps)
|
||||
step_type: (s.step_type === 'serial' || s.step_type === 'parallel')
|
||||
? ('audit' as const)
|
||||
: (s.step_type as 'submit' | 'audit' | 'execute'),
|
||||
approver_ids: parseIds(s.approver_ids),
|
||||
approver_ids: parseApproverIds(s.approver_ids),
|
||||
}))
|
||||
);
|
||||
} else {
|
||||
setSteps([
|
||||
{ name: t('approval.detail.submitNode'), step_type: 'submit', approver_ids: [] },
|
||||
{ name: t('approval.step.auditShort'), step_type: 'audit', approver_ids: [] },
|
||||
]);
|
||||
setSteps(buildDefaultSteps(users, t));
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [workflow]);
|
||||
|
||||
const parseIds = (ids: string | null): number[] => {
|
||||
if (!ids) return [];
|
||||
try {
|
||||
const p = JSON.parse(ids);
|
||||
return Array.isArray(p) ? p : [];
|
||||
} catch { return []; }
|
||||
};
|
||||
}, [workflow, users, t]);
|
||||
|
||||
const addStep = (type: 'submit' | 'audit' | 'execute') => {
|
||||
const labels: Record<string, string> = {
|
||||
@@ -322,19 +336,18 @@ function WorkflowEditorModal({ workflow, users, onClose, onSaved }: EditorProps)
|
||||
const inputCls = 'w-full px-3 py-2 bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded-lg text-sm focus:border-blue-400 focus:outline-none transition-colors text-[var(--dg-text-primary)]';
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||
<div className="absolute inset-0 bg-black/50" onClick={onClose} />
|
||||
<div className="relative bg-[var(--dg-bg-secondary)] rounded-xl shadow-2xl w-full max-w-2xl max-h-[90vh] overflow-y-auto m-4">
|
||||
<div className="flex items-center justify-between px-5 py-4 border-b border-[var(--dg-border)]">
|
||||
<h2 className="text-base font-semibold text-[var(--dg-text-primary)]">
|
||||
<Dialog open onOpenChange={(open) => !open && onClose()}>
|
||||
<DialogContent className="dg-card sm:max-w-2xl max-h-[90vh] overflow-hidden flex flex-col p-0 gap-0">
|
||||
<DialogHeader className="px-5 py-4 border-b border-[var(--dg-border)] shrink-0">
|
||||
<DialogTitle className="text-base font-semibold text-[var(--dg-text-primary)]">
|
||||
{isEdit ? t('approval.editor.editTitle') : t('approval.editor.newTitle')}
|
||||
</h2>
|
||||
<button onClick={onClose} className="p-1.5 rounded-lg hover:bg-[var(--dg-bg-hover)] text-[var(--dg-text-secondary)] transition-colors">
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</DialogTitle>
|
||||
<DialogDescription className="sr-only">
|
||||
{isEdit ? t('approval.editor.editTitle') : t('approval.editor.newTitle')}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="px-5 pb-5 space-y-4 pt-4">
|
||||
<div className="px-5 pb-5 space-y-4 pt-4 overflow-y-auto flex-1 min-h-0">
|
||||
{error && <div className="px-3 py-2 text-sm dg-text-red bg-red-500/10 rounded-lg">{error}</div>}
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
@@ -431,17 +444,17 @@ function WorkflowEditorModal({ workflow, users, onClose, onSaved }: EditorProps)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-end gap-3 px-5 py-4 border-t border-[var(--dg-border)]">
|
||||
<button onClick={onClose} className="px-4 py-2 text-sm text-[var(--dg-text-secondary)] hover:bg-[var(--dg-bg-hover)] rounded-lg transition-colors">
|
||||
<DialogFooter className="px-5 py-4 border-t border-[var(--dg-border)] shrink-0">
|
||||
<Button variant="outline" className="dg-btn-secondary" onClick={onClose}>
|
||||
{t('common.cancel')}
|
||||
</button>
|
||||
<Button onClick={handleSubmit} disabled={submitting}>
|
||||
</Button>
|
||||
<Button className="dg-btn-primary" onClick={handleSubmit} disabled={submitting}>
|
||||
{submitting && <Loader2 className="w-4 h-4 animate-spin mr-1" />}
|
||||
{isEdit ? t('common.save') : t('common.create')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -491,10 +504,14 @@ export default function ApprovalWorkflowPage() {
|
||||
const res: any = await userApi.getUsers();
|
||||
const data = Array.isArray(res?.list) ? res.list : (Array.isArray(res) ? res : []);
|
||||
// Normalize: extract role code from roles array
|
||||
const normalized = data.map((u: any) => ({
|
||||
...u,
|
||||
role: u.role || (Array.isArray(u.roles) && u.roles.length > 0 ? u.roles[0].code : ''),
|
||||
}));
|
||||
const normalized = data.map((u: any) => {
|
||||
let role = u.role ?? '';
|
||||
if (!role && Array.isArray(u.roles) && u.roles.length > 0) {
|
||||
const first = u.roles[0];
|
||||
role = typeof first === 'string' ? first : (first?.code ?? first?.name ?? '');
|
||||
}
|
||||
return { ...u, role };
|
||||
});
|
||||
setUsers(normalized);
|
||||
} catch { /* ignore */ }
|
||||
}, []);
|
||||
@@ -547,6 +564,7 @@ export default function ApprovalWorkflowPage() {
|
||||
<>
|
||||
<PageShell
|
||||
variant="list"
|
||||
maxWidth="xl"
|
||||
title={t('approval.workflowTitle')}
|
||||
subtitle={t('approval.workflowSubtitle')}
|
||||
icon={<GitBranch className="h-5 w-5" />}
|
||||
@@ -577,13 +595,13 @@ export default function ApprovalWorkflowPage() {
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
<div className="grid gap-4">
|
||||
{items.map((wf) => {
|
||||
const isExpanded = expandedId === wf.id;
|
||||
return (
|
||||
<Card key={wf.id} className={`dg-card overflow-hidden ${!wf.is_active ? 'opacity-50' : ''}`}>
|
||||
<CardContent className="p-5">
|
||||
<div className="flex items-start justify-between">
|
||||
<Card key={wf.id} className={`dg-card overflow-hidden ${!wf.is_active ? 'opacity-60' : ''}`}>
|
||||
<CardContent className="p-4 sm:p-5">
|
||||
<div className="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-3">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-3 mb-2 flex-wrap">
|
||||
<h3 className="text-lg font-semibold text-[var(--dg-text-primary)]">{wf.name}</h3>
|
||||
@@ -610,7 +628,7 @@ export default function ApprovalWorkflowPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1 flex-shrink-0 ml-4">
|
||||
<div className="flex items-center gap-1 flex-shrink-0 sm:ml-4 self-end sm:self-start">
|
||||
<button onClick={() => setExpandedId(isExpanded ? null : wf.id)}
|
||||
className="p-1.5 rounded hover:bg-[var(--dg-bg-hover)] text-[var(--dg-text-muted)] transition-colors">
|
||||
{isExpanded ? <ChevronUp className="w-4 h-4" /> : <ChevronDown className="w-4 h-4" />}
|
||||
@@ -640,7 +658,7 @@ export default function ApprovalWorkflowPage() {
|
||||
{isExpanded && (
|
||||
<div className="mt-4 pt-4 border-t border-[var(--dg-border)] space-y-3">
|
||||
{wf.steps?.map((s, i) => {
|
||||
const ids = (() => { try { const p = JSON.parse(s.approver_ids || '[]'); return Array.isArray(p) ? p : []; } catch { return []; } })();
|
||||
const ids = parseApproverIds(s.approver_ids);
|
||||
return (
|
||||
<div key={i} className="flex items-start gap-3 p-3 bg-[var(--dg-bg-primary)] rounded-lg">
|
||||
<div className="w-6 h-6 rounded-full bg-[var(--dg-bg-tertiary)] flex items-center justify-center text-xs font-medium text-[var(--dg-text-secondary)] flex-shrink-0 mt-0.5">
|
||||
|
||||
Reference in new issue
Block a user