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,361 @@
|
||||
// 可缩放面积图 — 光标拖拽选区 / 宽屏弹框展开 // Logan.Li
|
||||
import {
|
||||
useState, useMemo, useCallback, useId, useEffect,
|
||||
forwardRef, useImperativeHandle,
|
||||
} from 'react';
|
||||
import {
|
||||
AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip,
|
||||
ResponsiveContainer, ReferenceArea, ReferenceLine,
|
||||
} from 'recharts';
|
||||
import {
|
||||
Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { ChartToolbar } from './ChartToolbar';
|
||||
import { MonitorChartTooltip } from '../shared/monitorChartTheme';
|
||||
import { ChartRangeHint } from '../shared/ChartRangeHint';
|
||||
|
||||
export interface ChartSeries {
|
||||
dataKey: string;
|
||||
color: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export interface ChartReferenceLine {
|
||||
value: number;
|
||||
label?: string;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
export interface ZoomableChartHandle {
|
||||
expand: () => void;
|
||||
resetZoom: () => void;
|
||||
}
|
||||
|
||||
interface ZoomRange {
|
||||
left: string;
|
||||
right: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
data: Record<string, unknown>[];
|
||||
series: ChartSeries[];
|
||||
xDataKey?: string;
|
||||
height?: number | string;
|
||||
unit?: string;
|
||||
yDomain?: [number, number | 'auto'];
|
||||
chartId: string;
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
showHint?: boolean;
|
||||
/** header=按钮由父组件放在卡片标题栏;overlay=浮在图表右上角(无卡片场景) // Logan.Li */
|
||||
toolbarPlacement?: 'header' | 'overlay';
|
||||
onZoomedChange?: (zoomed: boolean) => void;
|
||||
referenceLines?: ChartReferenceLine[];
|
||||
}
|
||||
|
||||
const dialogClass =
|
||||
'!max-w-[min(96vw,88rem)] sm:!max-w-[min(96vw,88rem)] w-[min(96vw,88rem)] max-h-[92vh] flex flex-col gap-2 p-4 sm:p-5 bg-[var(--dg-bg-primary)] border-[var(--dg-border)] overflow-hidden';
|
||||
|
||||
function ChartBody({
|
||||
displayData,
|
||||
series,
|
||||
xDataKey,
|
||||
height,
|
||||
unit,
|
||||
yDomain,
|
||||
gradPrefix,
|
||||
zoomRange,
|
||||
refLeft,
|
||||
refRight,
|
||||
onMouseDown,
|
||||
onMouseMove,
|
||||
onMouseUp,
|
||||
compact,
|
||||
referenceLines,
|
||||
}: {
|
||||
displayData: Record<string, unknown>[];
|
||||
series: ChartSeries[];
|
||||
xDataKey: string;
|
||||
height: number | string;
|
||||
unit: string;
|
||||
yDomain: [number, number | 'auto'];
|
||||
gradPrefix: string;
|
||||
zoomRange: ZoomRange | null;
|
||||
refLeft: string;
|
||||
refRight: string;
|
||||
onMouseDown: (e: { activeLabel?: string }) => void;
|
||||
onMouseMove: (e: { activeLabel?: string }) => void;
|
||||
onMouseUp: () => void;
|
||||
compact?: boolean;
|
||||
referenceLines?: ChartReferenceLine[];
|
||||
}) {
|
||||
return (
|
||||
<div style={{ height }} className="select-none w-full min-h-0">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<AreaChart
|
||||
data={displayData}
|
||||
margin={{ top: 6, right: compact ? 12 : 8, bottom: compact ? 12 : 8, left: 2 }}
|
||||
onMouseDown={onMouseDown}
|
||||
onMouseMove={onMouseMove}
|
||||
onMouseUp={onMouseUp}
|
||||
style={{ cursor: 'crosshair' }}
|
||||
>
|
||||
<defs>
|
||||
{series.map((s) => (
|
||||
<linearGradient key={s.dataKey} id={`${gradPrefix}-${s.dataKey}`} x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor={s.color} stopOpacity={0.35} />
|
||||
<stop offset="95%" stopColor={s.color} stopOpacity={0} />
|
||||
</linearGradient>
|
||||
))}
|
||||
</defs>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="var(--dg-border)" strokeOpacity={0.6} />
|
||||
<XAxis
|
||||
dataKey={xDataKey}
|
||||
tick={{ fontSize: compact ? 11 : 10, fill: 'var(--dg-text-muted)' }}
|
||||
tickLine={false}
|
||||
axisLine={{ stroke: 'var(--dg-border)' }}
|
||||
interval="preserveStartEnd"
|
||||
minTickGap={compact ? 56 : 40}
|
||||
/>
|
||||
<YAxis
|
||||
tick={{ fontSize: compact ? 11 : 10, fill: 'var(--dg-text-muted)' }}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
width={compact ? 44 : 36}
|
||||
domain={yDomain[1] === 'auto' ? [0, (max: number) => max * 1.15] : yDomain as [number, number]}
|
||||
tickFormatter={(v: number) => (v >= 1000 ? `${(v / 1000).toFixed(1)}k` : String(Math.round(v * 10) / 10))}
|
||||
/>
|
||||
<Tooltip
|
||||
content={(
|
||||
<MonitorChartTooltip
|
||||
nameFormatter={(name) => series.find((s) => s.dataKey === name)?.label || name}
|
||||
valueFormatter={(v) => `${Number(v).toFixed(2)}${unit ? ` ${unit}` : ''}`}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{referenceLines?.map((line) => (
|
||||
<ReferenceLine
|
||||
key={`ref-${line.value}-${line.label || ''}`}
|
||||
y={line.value}
|
||||
stroke={line.color || 'var(--dg-yellow)'}
|
||||
strokeDasharray="4 4"
|
||||
strokeOpacity={0.75}
|
||||
label={{
|
||||
value: line.label || String(line.value),
|
||||
position: 'insideTopRight',
|
||||
fontSize: compact ? 10 : 9,
|
||||
fill: line.color || 'var(--dg-yellow)',
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
{series.map((s) => (
|
||||
<Area
|
||||
key={s.dataKey}
|
||||
type="monotone"
|
||||
dataKey={s.dataKey}
|
||||
stroke={s.color}
|
||||
strokeWidth={compact ? 2 : 1.5}
|
||||
fill={`url(#${gradPrefix}-${s.dataKey})`}
|
||||
name={s.dataKey}
|
||||
activeDot={{ r: compact ? 5 : 4, strokeWidth: 0 }}
|
||||
isAnimationActive={false}
|
||||
/>
|
||||
))}
|
||||
{refLeft && refRight ? (
|
||||
<ReferenceArea
|
||||
x1={refLeft}
|
||||
x2={refRight}
|
||||
strokeOpacity={0.4}
|
||||
fill="var(--dg-blue)"
|
||||
fillOpacity={0.15}
|
||||
stroke="var(--dg-blue)"
|
||||
/>
|
||||
) : null}
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const ZoomableAreaChart = forwardRef<ZoomableChartHandle, Props>(function ZoomableAreaChart({
|
||||
data,
|
||||
series,
|
||||
xDataKey = 'time',
|
||||
height = 192,
|
||||
unit = '',
|
||||
yDomain = [0, 'auto'],
|
||||
chartId,
|
||||
title,
|
||||
subtitle,
|
||||
showHint = true,
|
||||
toolbarPlacement = 'header',
|
||||
onZoomedChange,
|
||||
referenceLines,
|
||||
}, ref) {
|
||||
const uid = useId().replace(/:/g, '');
|
||||
const gradPrefix = `grad-${chartId}-${uid}`;
|
||||
|
||||
const [zoomRange, setZoomRange] = useState<ZoomRange | null>(null);
|
||||
const [refLeft, setRefLeft] = useState('');
|
||||
const [refRight, setRefRight] = useState('');
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
|
||||
const dataFingerprint = useMemo(() => {
|
||||
if (data.length === 0) return 'empty';
|
||||
const first = data[0]?.[xDataKey];
|
||||
const last = data[data.length - 1]?.[xDataKey];
|
||||
return `${data.length}|${String(first)}|${String(last)}`;
|
||||
}, [data, xDataKey]);
|
||||
|
||||
useEffect(() => {
|
||||
setZoomRange(null);
|
||||
setRefLeft('');
|
||||
setRefRight('');
|
||||
setExpanded(false);
|
||||
}, [dataFingerprint]);
|
||||
|
||||
useEffect(() => {
|
||||
onZoomedChange?.(!!zoomRange);
|
||||
}, [zoomRange, onZoomedChange]);
|
||||
|
||||
const displayData = useMemo(() => {
|
||||
if (!zoomRange || data.length === 0) return data;
|
||||
const li = data.findIndex((d) => d[xDataKey] === zoomRange.left);
|
||||
const ri = data.findIndex((d) => d[xDataKey] === zoomRange.right);
|
||||
if (li < 0 || ri < 0) return data;
|
||||
const start = Math.min(li, ri);
|
||||
const end = Math.max(li, ri);
|
||||
if (end - start < 2) return data;
|
||||
return data.slice(start, end + 1);
|
||||
}, [data, zoomRange, xDataKey]);
|
||||
|
||||
const zoomOut = useCallback(() => {
|
||||
setZoomRange(null);
|
||||
setRefLeft('');
|
||||
setRefRight('');
|
||||
}, []);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
expand: () => setExpanded(true),
|
||||
resetZoom: zoomOut,
|
||||
}), [zoomOut]);
|
||||
|
||||
const onMouseDown = useCallback((e: { activeLabel?: string }) => {
|
||||
if (!e?.activeLabel) return;
|
||||
setRefLeft(String(e.activeLabel));
|
||||
setRefRight(String(e.activeLabel));
|
||||
}, []);
|
||||
|
||||
const onMouseMove = useCallback((e: { activeLabel?: string }) => {
|
||||
if (!refLeft || !e?.activeLabel) return;
|
||||
setRefRight(String(e.activeLabel));
|
||||
}, [refLeft]);
|
||||
|
||||
const onMouseUp = useCallback(() => {
|
||||
if (!refLeft || !refRight || refLeft === refRight) {
|
||||
setRefLeft('');
|
||||
setRefRight('');
|
||||
return;
|
||||
}
|
||||
setZoomRange({ left: refLeft, right: refRight });
|
||||
setRefLeft('');
|
||||
setRefRight('');
|
||||
}, [refLeft, refRight]);
|
||||
|
||||
const chartProps = {
|
||||
displayData,
|
||||
series,
|
||||
xDataKey,
|
||||
unit,
|
||||
yDomain,
|
||||
gradPrefix,
|
||||
zoomRange,
|
||||
refLeft,
|
||||
refRight,
|
||||
onMouseDown,
|
||||
onMouseMove,
|
||||
onMouseUp,
|
||||
referenceLines,
|
||||
};
|
||||
|
||||
if (data.length === 0) {
|
||||
return (
|
||||
<div
|
||||
className="flex items-center justify-center text-xs dg-text-muted border border-dashed border-[var(--dg-border)] rounded-md w-full"
|
||||
style={{ height }}
|
||||
>
|
||||
暂无数据
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="relative w-full">
|
||||
{toolbarPlacement === 'overlay' && (
|
||||
<div className="absolute top-1 right-1 z-10">
|
||||
<ChartToolbar
|
||||
zoomed={!!zoomRange}
|
||||
onExpand={() => setExpanded(true)}
|
||||
onResetZoom={zoomOut}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<ChartBody {...chartProps} height={height} />
|
||||
{showHint && toolbarPlacement === 'header' && (
|
||||
<ChartRangeHint
|
||||
label={zoomRange ? `${zoomRange.left} — ${zoomRange.right}` : null}
|
||||
zoomed={!!zoomRange}
|
||||
onReset={zoomOut}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Dialog open={expanded} onOpenChange={setExpanded}>
|
||||
<DialogContent className={dialogClass}>
|
||||
<DialogHeader className="shrink-0 flex-row items-start justify-between gap-4 space-y-0 pr-10">
|
||||
<div className="min-w-0">
|
||||
<DialogTitle className="text-base truncate">{title || '指标详情'}</DialogTitle>
|
||||
{subtitle && (
|
||||
<p className="text-xs dg-text-muted mt-0.5 truncate">{subtitle}</p>
|
||||
)}
|
||||
<DialogDescription className="sr-only">放大查看监控指标趋势</DialogDescription>
|
||||
</div>
|
||||
<ChartToolbar
|
||||
size="md"
|
||||
zoomed={!!zoomRange}
|
||||
onExpand={() => setExpanded(true)}
|
||||
onResetZoom={zoomOut}
|
||||
/>
|
||||
</DialogHeader>
|
||||
|
||||
{series.length > 1 && (
|
||||
<div className="flex flex-wrap items-center gap-4 text-xs shrink-0 px-0.5">
|
||||
{series.map((s) => (
|
||||
<span key={s.dataKey} className="inline-flex items-center gap-1.5 dg-text-secondary">
|
||||
<span className="w-3 h-0.5 rounded-full" style={{ backgroundColor: s.color }} />
|
||||
{s.label}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 宽屏趋势区:21:8 比例,监控弹框标准形态 // Logan.Li */}
|
||||
<div className="w-full flex-1 min-h-[260px] max-h-[min(48vh,480px)] aspect-[21/8]">
|
||||
<ChartBody {...chartProps} height="100%" compact />
|
||||
</div>
|
||||
|
||||
{zoomRange && (
|
||||
<div className="shrink-0 border-t border-[var(--dg-border)] pt-2">
|
||||
<ChartRangeHint
|
||||
label={`${zoomRange.left} — ${zoomRange.right}`}
|
||||
zoomed
|
||||
onReset={zoomOut}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
});
|
||||
Reference in new issue
Block a user