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
@@ -1,4 +1,4 @@
|
||||
// ScriptManager — 脚本库与执行记录主组件 // Logan.Li
|
||||
// ScriptManager — 脚本库主组件 // Logan.Li
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Plus, Search, RotateCcw, Trash2, FileCode } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
@@ -9,16 +9,16 @@ import {
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { toast } from 'sonner';
|
||||
import { useConfirm } from '@/components/ui/confirm-dialog';
|
||||
import { scriptApi, taskExecutionApi } from '@/api';
|
||||
import { scriptApi } from '@/api';
|
||||
import type { ScriptRuntimeInfo } from '@/api/task/script';
|
||||
import { usePermission } from '@/hooks/usePermission';
|
||||
import {
|
||||
type Script, type ExecutionRecord, mapScript, scriptTypes, genCode,
|
||||
type Script, mapScript, scriptTypes, genCode, availableScriptTypes,
|
||||
} from './script-helpers';
|
||||
import { ScriptRuntimeBanner } from './ScriptRuntimeBanner';
|
||||
import type { ScriptFormData } from './ScriptFormDialog';
|
||||
import ScriptFormDialog from './ScriptFormDialog';
|
||||
import ScriptDetailDialog from './ScriptDetailDialog';
|
||||
import ScriptLogsDialog from './ScriptLogsDialog';
|
||||
import ScriptCard from './ScriptCard';
|
||||
import ScriptExecutionsTable from './ScriptExecutionsTable';
|
||||
import { ScriptTable } from './ScriptTable';
|
||||
import { PageShell } from '@/components/layout/PageShell';
|
||||
import { ContentPanel } from '@/components/layout/ContentPanel';
|
||||
import { PageEmpty } from '@/components/layout/PageEmpty';
|
||||
@@ -26,24 +26,20 @@ import type { JsonData } from '@/types/common';
|
||||
|
||||
export function ScriptManager() {
|
||||
const _confirm = useConfirm();
|
||||
const { hasAnyPermission, isAdmin } = usePermission();
|
||||
const canWrite = isAdmin || hasAnyPermission(['auto:script', 'auto:cron', 'auto:ansible']);
|
||||
const [scripts, setScripts] = useState<Script[]>([]);
|
||||
const [executions, setExecutions] = useState<ExecutionRecord[]>([]);
|
||||
const [runtime, setRuntime] = useState<ScriptRuntimeInfo | null>(null);
|
||||
const [runtimeLoading, setRuntimeLoading] = useState(true);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [executing, setExecuting] = useState(false);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [typeFilter, setTypeFilter] = useState<string>('all');
|
||||
const [activeTab, setActiveTab] = useState('scripts');
|
||||
const [selectedIds, setSelectedIds] = useState<number[]>([]);
|
||||
|
||||
// 弹窗状态
|
||||
const [editorOpen, setEditorOpen] = useState(false);
|
||||
const [detailOpen, setDetailOpen] = useState(false);
|
||||
const [logsOpen, setLogsOpen] = useState(false);
|
||||
const [editingId, setEditingId] = useState<number | null>(null);
|
||||
const [selectedScript, setSelectedScript] = useState<Script | null>(null);
|
||||
const [selectedExec, setSelectedExec] = useState<ExecutionRecord | null>(null);
|
||||
|
||||
// 表单状态
|
||||
const [formData, setFormData] = useState<ScriptFormData>({
|
||||
name: '', description: '', content: '', type: 'shell', timeout: 300,
|
||||
});
|
||||
@@ -61,26 +57,30 @@ export function ScriptManager() {
|
||||
}
|
||||
};
|
||||
|
||||
const fetchExecutions = async () => {
|
||||
const fetchRuntime = async () => {
|
||||
setRuntimeLoading(true);
|
||||
try {
|
||||
const res = await taskExecutionApi.getExecutions({ task_type: 'script', page_size: 50 });
|
||||
const items = (res as JsonData)?.list || [];
|
||||
setExecutions(items);
|
||||
const rt = await scriptApi.getRuntime();
|
||||
setRuntime(rt);
|
||||
} catch {
|
||||
// 静默失败
|
||||
} finally {
|
||||
setRuntimeLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchScripts();
|
||||
fetchExecutions();
|
||||
fetchRuntime();
|
||||
}, []);
|
||||
|
||||
const openCreate = () => {
|
||||
const types = availableScriptTypes(runtime);
|
||||
const first = types.find((t) => !t.disabled) ?? types[0] ?? scriptTypes[0];
|
||||
setEditingId(null);
|
||||
setFormData({
|
||||
name: '', description: '', content: scriptTypes[0].sample,
|
||||
type: 'shell', timeout: 300,
|
||||
name: '', description: '', content: first.sample,
|
||||
type: first.value, timeout: 300,
|
||||
});
|
||||
setEditorOpen(true);
|
||||
};
|
||||
@@ -96,7 +96,6 @@ export function ScriptManager() {
|
||||
timeout: sc.timeout,
|
||||
});
|
||||
setEditorOpen(true);
|
||||
setDetailOpen(false);
|
||||
};
|
||||
|
||||
const handleTypeChange = (t: string) => {
|
||||
@@ -134,8 +133,8 @@ export function ScriptManager() {
|
||||
}
|
||||
setEditorOpen(false);
|
||||
fetchScripts();
|
||||
} catch {
|
||||
toast.error(editingId !== null ? '更新失败' : '创建失败');
|
||||
} catch (e) {
|
||||
toast.error(e instanceof Error ? e.message : (editingId !== null ? '更新失败' : '创建失败'));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -151,8 +150,8 @@ export function ScriptManager() {
|
||||
});
|
||||
toast.success('复制成功');
|
||||
fetchScripts();
|
||||
} catch {
|
||||
toast.error('复制失败');
|
||||
} catch (e) {
|
||||
toast.error(e instanceof Error ? e.message : '复制失败');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -168,43 +167,15 @@ export function ScriptManager() {
|
||||
await scriptApi.deleteScript(sc.id);
|
||||
toast.success('删除成功');
|
||||
fetchScripts();
|
||||
} catch {
|
||||
toast.error('删除失败');
|
||||
}
|
||||
};
|
||||
|
||||
const handleExecute = async (sc: Script) => {
|
||||
const ok = await _confirm({
|
||||
title: '执行脚本',
|
||||
description: `将在 AIOps 主机上本地执行「${sc.name}」(${sc.type})。`,
|
||||
confirmText: '开始执行',
|
||||
});
|
||||
if (!ok) return;
|
||||
setExecuting(true);
|
||||
try {
|
||||
const res = await scriptApi.executeScript(sc.id);
|
||||
const exec = (res as JsonData)?.data ?? res;
|
||||
if (exec?.status === 'success') {
|
||||
toast.success(`执行成功,耗时 ${exec.duration ?? 0}ms`);
|
||||
} else {
|
||||
toast.error(`执行失败,耗时 ${exec?.duration ?? 0}ms`, {
|
||||
description: ((exec?.output || '') as string).slice(0, 120),
|
||||
});
|
||||
}
|
||||
fetchScripts();
|
||||
fetchExecutions();
|
||||
} catch {
|
||||
toast.error('执行请求失败');
|
||||
} finally {
|
||||
setExecuting(false);
|
||||
} catch (e) {
|
||||
toast.error(e instanceof Error ? e.message : '删除失败');
|
||||
}
|
||||
};
|
||||
|
||||
const filteredScripts = scripts.filter(sc => {
|
||||
const q = searchQuery.toLowerCase();
|
||||
const matchSearch = sc.name.toLowerCase().includes(q) ||
|
||||
sc.description.toLowerCase().includes(q) ||
|
||||
sc.code.toLowerCase().includes(q);
|
||||
sc.description.toLowerCase().includes(q);
|
||||
const matchType = typeFilter === 'all' || sc.type === typeFilter;
|
||||
return matchSearch && matchType;
|
||||
});
|
||||
@@ -233,24 +204,18 @@ export function ScriptManager() {
|
||||
toast.success(`已删除 ${selectedIds.length} 个脚本`);
|
||||
setSelectedIds([]);
|
||||
fetchScripts();
|
||||
} catch {
|
||||
toast.error('批量删除失败');
|
||||
} catch (e) {
|
||||
toast.error(e instanceof Error ? e.message : '批量删除失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 页面级 Tab 配置 // Logan.Li
|
||||
const pageTabs = [
|
||||
{ id: 'scripts', label: '脚本库' },
|
||||
{ id: 'jobs', label: '执行记录' },
|
||||
];
|
||||
|
||||
const scriptsToolbar = (
|
||||
const toolbar = (
|
||||
<div className="flex flex-wrap items-center justify-between gap-4">
|
||||
<div className="flex items-center gap-2 flex-1 min-w-[300px]">
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 dg-text-muted" />
|
||||
<Input
|
||||
placeholder="搜索名称、编码、描述..."
|
||||
placeholder="搜索名称、描述..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-10 dg-input"
|
||||
@@ -276,103 +241,81 @@ export function ScriptManager() {
|
||||
<PageShell
|
||||
variant="list"
|
||||
title="脚本库"
|
||||
subtitle="脚本管理与执行记录"
|
||||
subtitle="脚本编辑与管理"
|
||||
icon={<FileCode className="h-5 w-5" />}
|
||||
tabs={pageTabs}
|
||||
activeTab={activeTab}
|
||||
onTabChange={(id) => {
|
||||
setActiveTab(id);
|
||||
if (id === 'jobs') fetchExecutions();
|
||||
}}
|
||||
toolbar={activeTab === 'scripts' ? scriptsToolbar : undefined}
|
||||
loading={activeTab === 'scripts' && loading}
|
||||
actions={activeTab === 'scripts' ? (
|
||||
toolbar={toolbar}
|
||||
loading={loading}
|
||||
actions={(
|
||||
<>
|
||||
<Button variant="outline" className="dg-btn-secondary" onClick={fetchScripts}>
|
||||
<RotateCcw className="w-4 h-4 mr-2" />
|
||||
刷新
|
||||
</Button>
|
||||
<Button className="dg-btn-primary" onClick={openCreate}>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
新建脚本
|
||||
</Button>
|
||||
{canWrite && (
|
||||
<Button className="dg-btn-primary" onClick={openCreate}>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
新建脚本
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<Button variant="outline" className="dg-btn-secondary" onClick={fetchExecutions}>
|
||||
<RotateCcw className="w-4 h-4 mr-2" />
|
||||
刷新
|
||||
</Button>
|
||||
)}
|
||||
>
|
||||
{activeTab === 'scripts' ? (
|
||||
<>
|
||||
{selectedIds.length > 0 && (
|
||||
<div className="flex items-center justify-between gap-4 px-4 py-2.5 rounded-lg border border-[var(--dg-border)] bg-[var(--dg-bg-secondary)] mb-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<Checkbox
|
||||
checked={allSelected}
|
||||
onCheckedChange={toggleSelectAll}
|
||||
className="border-[var(--dg-border)]"
|
||||
/>
|
||||
<span className="text-sm dg-text-secondary">
|
||||
已选择 {selectedIds.length} 项 / 共 {filteredScripts.length} 项
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="outline" size="sm" className="dg-btn-secondary" onClick={() => setSelectedIds([])}>
|
||||
取消选择
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="dg-btn-danger dg-btn-sm"
|
||||
onClick={handleBatchDelete}
|
||||
>
|
||||
<Trash2 className="w-4 h-4 mr-2" />
|
||||
批量删除
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{filteredScripts.length === 0 ? (
|
||||
<PageEmpty
|
||||
title={scripts.length === 0 ? '暂无脚本' : '未匹配到符合条件的脚本'}
|
||||
description={scripts.length === 0 ? '点击「新建脚本」创建第一个' : undefined}
|
||||
action={scripts.length === 0 ? (
|
||||
<Button className="dg-btn-primary" onClick={openCreate}>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
新建脚本
|
||||
</Button>
|
||||
) : undefined}
|
||||
<div className="mb-4">
|
||||
<ScriptRuntimeBanner runtime={runtime} loading={runtimeLoading} />
|
||||
</div>
|
||||
{selectedIds.length > 0 && (
|
||||
<div className="flex items-center justify-between gap-4 px-4 py-2.5 rounded-lg border border-[var(--dg-border)] bg-[var(--dg-bg-secondary)] mb-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<Checkbox
|
||||
checked={allSelected}
|
||||
onCheckedChange={toggleSelectAll}
|
||||
className="border-[var(--dg-border)]"
|
||||
/>
|
||||
) : (
|
||||
<ContentPanel>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{filteredScripts.map((sc) => (
|
||||
<ScriptCard
|
||||
key={sc.id}
|
||||
script={sc}
|
||||
selected={selectedIds.includes(sc.id)}
|
||||
executing={executing}
|
||||
onToggleSelect={() => toggleSelect(sc.id)}
|
||||
onView={() => { setSelectedScript(sc); setDetailOpen(true); }}
|
||||
onExecute={() => handleExecute(sc)}
|
||||
onEdit={() => openEdit(sc)}
|
||||
onClone={() => handleClone(sc)}
|
||||
onDelete={() => handleDelete(sc)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</ContentPanel>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<ScriptExecutionsTable
|
||||
executions={executions}
|
||||
onRefresh={fetchExecutions}
|
||||
onViewLogs={(job) => { setSelectedExec(job); setLogsOpen(true); }}
|
||||
<span className="text-sm dg-text-secondary">
|
||||
已选择 {selectedIds.length} 项 / 共 {filteredScripts.length} 项
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="outline" size="sm" className="dg-btn-secondary" onClick={() => setSelectedIds([])}>
|
||||
取消选择
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="dg-btn-danger dg-btn-sm"
|
||||
onClick={handleBatchDelete}
|
||||
>
|
||||
<Trash2 className="w-4 h-4 mr-2" />
|
||||
批量删除
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{filteredScripts.length === 0 ? (
|
||||
<PageEmpty
|
||||
title={scripts.length === 0 ? '暂无脚本' : '未匹配到符合条件的脚本'}
|
||||
description={scripts.length === 0 ? '点击「新建脚本」创建第一个' : undefined}
|
||||
action={scripts.length === 0 && canWrite ? (
|
||||
<Button className="dg-btn-primary" onClick={openCreate}>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
新建脚本
|
||||
</Button>
|
||||
) : undefined}
|
||||
/>
|
||||
) : (
|
||||
<ContentPanel>
|
||||
<ScriptTable
|
||||
scripts={filteredScripts}
|
||||
selectedIds={selectedIds}
|
||||
onToggleSelect={toggleSelect}
|
||||
onToggleSelectAll={toggleSelectAll}
|
||||
allSelected={allSelected}
|
||||
onEdit={openEdit}
|
||||
onClone={handleClone}
|
||||
onDelete={handleDelete}
|
||||
/>
|
||||
</ContentPanel>
|
||||
)}
|
||||
</PageShell>
|
||||
|
||||
@@ -385,19 +328,7 @@ export function ScriptManager() {
|
||||
setFormData={setFormData}
|
||||
onTypeChange={handleTypeChange}
|
||||
onSave={handleSave}
|
||||
/>
|
||||
|
||||
<ScriptDetailDialog
|
||||
open={detailOpen}
|
||||
onOpenChange={setDetailOpen}
|
||||
script={selectedScript}
|
||||
onEdit={openEdit}
|
||||
/>
|
||||
|
||||
<ScriptLogsDialog
|
||||
open={logsOpen}
|
||||
onOpenChange={setLogsOpen}
|
||||
exec={selectedExec}
|
||||
runtime={runtime}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user