|
|
@@ -1,6 +1,16 @@
|
|
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
|
+import type { ReactNode } from 'react';
|
|
|
import { useTranslation } from 'react-i18next';
|
|
|
import { Modal, Select, Tabs } from 'antd';
|
|
|
+import {
|
|
|
+ DashboardOutlined,
|
|
|
+ DatabaseOutlined,
|
|
|
+ DeploymentUnitOutlined,
|
|
|
+ GlobalOutlined,
|
|
|
+ HddOutlined,
|
|
|
+ LineChartOutlined,
|
|
|
+ TeamOutlined,
|
|
|
+} from '@ant-design/icons';
|
|
|
|
|
|
import { HttpUtil, SizeFormatter } from '@/utils';
|
|
|
import { Sparkline } from '@/components/viz';
|
|
|
@@ -17,26 +27,38 @@ interface SystemHistoryModalProps {
|
|
|
interface MetricDef {
|
|
|
key: string;
|
|
|
tab: string;
|
|
|
+ tabKey?: string;
|
|
|
+ title: string;
|
|
|
+ icon: ReactNode;
|
|
|
valueMax: number | null;
|
|
|
unit: string;
|
|
|
stroke: string;
|
|
|
+ key2?: string;
|
|
|
+ stroke2?: string;
|
|
|
+ name1?: string;
|
|
|
+ name2?: string;
|
|
|
+ key3?: string;
|
|
|
+ stroke3?: string;
|
|
|
+ name3?: string;
|
|
|
}
|
|
|
|
|
|
const METRICS: MetricDef[] = [
|
|
|
- { key: 'cpu', tab: 'CPU', valueMax: 100, unit: '%', stroke: '' },
|
|
|
- { key: 'mem', tab: 'RAM', valueMax: 100, unit: '%', stroke: '#7c4dff' },
|
|
|
- { key: 'netUp', tab: 'Net Up', valueMax: null, unit: 'B/s', stroke: '#1890ff' },
|
|
|
- { key: 'netDown', tab: 'Net Down', valueMax: null, unit: 'B/s', stroke: '#13c2c2' },
|
|
|
- { key: 'online', tab: 'Online', valueMax: null, unit: '', stroke: '#52c41a' },
|
|
|
- { key: 'load1', tab: 'Load 1m', valueMax: null, unit: '', stroke: '#fa8c16' },
|
|
|
- { key: 'load5', tab: 'Load 5m', valueMax: null, unit: '', stroke: '#f5222d' },
|
|
|
- { key: 'load15', tab: 'Load 15m', valueMax: null, unit: '', stroke: '#a0d911' },
|
|
|
+ { key: 'cpu', tab: 'CPU', title: 'pages.index.historyTitleCpu', icon: <DashboardOutlined />, valueMax: 100, unit: '%', stroke: '' },
|
|
|
+ { key: 'mem', tab: 'RAM', title: 'pages.index.historyTitleMem', icon: <DatabaseOutlined />, valueMax: 100, unit: '%', stroke: '#7c4dff' },
|
|
|
+ { key: 'netUp', tab: 'Bandwidth', tabKey: 'pages.index.historyTabBandwidth', title: 'pages.index.historyTitleNetwork', icon: <GlobalOutlined />, valueMax: null, unit: 'B/s', stroke: '#1890ff', key2: 'netDown', stroke2: '#13c2c2', name1: 'Up', name2: 'Down' },
|
|
|
+ { key: 'pktUp', tab: 'Packets', tabKey: 'pages.index.historyTabPackets', title: 'pages.index.historyTitlePackets', icon: <DeploymentUnitOutlined />, valueMax: null, unit: 'pkt/s', stroke: '#2f54eb', key2: 'pktDown', stroke2: '#36cfc9', name1: 'Up', name2: 'Down' },
|
|
|
+ { key: 'diskRead', tab: 'Disk I/O', tabKey: 'pages.index.historyTabDisk', title: 'pages.index.historyTitleDisk', icon: <HddOutlined />, valueMax: null, unit: 'B/s', stroke: '#eb2f96', key2: 'diskWrite', stroke2: '#722ed1', name1: 'Read', name2: 'Write' },
|
|
|
+ { key: 'online', tab: 'Online', tabKey: 'pages.index.historyTabOnline', title: 'pages.index.historyTitleOnline', icon: <TeamOutlined />, valueMax: null, unit: '', stroke: '#52c41a' },
|
|
|
+ { key: 'load1', tab: 'Load', tabKey: 'pages.index.historyTabLoad', title: 'pages.index.historyTitleLoad', icon: <LineChartOutlined />, valueMax: null, unit: '', stroke: '#fa8c16', key2: 'load5', stroke2: '#f5222d', name1: '1m', name2: '5m', key3: 'load15', stroke3: '#a0d911', name3: '15m' },
|
|
|
];
|
|
|
|
|
|
function unitFormatter(unit: string, activeKey: string): (v: number) => string {
|
|
|
if (unit === 'B/s') {
|
|
|
return (v) => `${SizeFormatter.sizeFormat(Math.max(0, Number(v) || 0)).replace(/\.\d+/, '')}/s`;
|
|
|
}
|
|
|
+ if (unit === 'pkt/s') {
|
|
|
+ return (v) => `${Math.round(Math.max(0, Number(v) || 0)).toLocaleString()}/s`;
|
|
|
+ }
|
|
|
if (unit === '%') {
|
|
|
return (v) => `${Number(v).toFixed(1)}%`;
|
|
|
}
|
|
|
@@ -69,6 +91,8 @@ export default function SystemHistoryModal({ open, status, onClose }: SystemHist
|
|
|
const [activeKey, setActiveKey] = useState('cpu');
|
|
|
const [bucket, setBucket] = useState(2);
|
|
|
const [points, setPoints] = useState<number[]>([]);
|
|
|
+ const [points2, setPoints2] = useState<number[]>([]);
|
|
|
+ const [points3, setPoints3] = useState<number[]>([]);
|
|
|
const [labels, setLabels] = useState<string[]>([]);
|
|
|
const [timestamps, setTimestamps] = useState<number[]>([]);
|
|
|
|
|
|
@@ -116,15 +140,32 @@ export default function SystemHistoryModal({ open, status, onClose }: SystemHist
|
|
|
setLabels(labs);
|
|
|
setPoints(vals);
|
|
|
setTimestamps(tss);
|
|
|
+
|
|
|
+ const fetchAligned = async (key?: string): Promise<number[]> => {
|
|
|
+ if (!key) return [];
|
|
|
+ const m = await HttpUtil.get(`/panel/api/server/history/${key}/${bucket}`);
|
|
|
+ if (m?.success && Array.isArray(m.obj)) {
|
|
|
+ const byTs = new Map<number, number>();
|
|
|
+ for (const p of m.obj) byTs.set(Number(p.t) || 0, Number(p.v) || 0);
|
|
|
+ return tss.map((ts) => byTs.get(ts) ?? 0);
|
|
|
+ }
|
|
|
+ return [];
|
|
|
+ };
|
|
|
+ setPoints2(await fetchAligned(activeMetric.key2));
|
|
|
+ setPoints3(await fetchAligned(activeMetric.key3));
|
|
|
} else {
|
|
|
setLabels([]);
|
|
|
setPoints([]);
|
|
|
+ setPoints2([]);
|
|
|
+ setPoints3([]);
|
|
|
setTimestamps([]);
|
|
|
}
|
|
|
} catch (e) {
|
|
|
console.error('Failed to fetch history bucket', e);
|
|
|
setLabels([]);
|
|
|
setPoints([]);
|
|
|
+ setPoints2([]);
|
|
|
+ setPoints3([]);
|
|
|
setTimestamps([]);
|
|
|
}
|
|
|
}, [activeMetric, bucket]);
|
|
|
@@ -168,12 +209,26 @@ export default function SystemHistoryModal({ open, status, onClose }: SystemHist
|
|
|
onChange={setActiveKey}
|
|
|
size="small"
|
|
|
className="history-tabs"
|
|
|
- items={METRICS.map((m) => ({ key: m.key, label: m.tab }))}
|
|
|
+ items={METRICS.map((m) => {
|
|
|
+ const tabLabel = m.tabKey ? t(m.tabKey) : m.tab;
|
|
|
+ return {
|
|
|
+ key: m.key,
|
|
|
+ label: isMobile ? <span title={tabLabel} aria-label={tabLabel}>{m.icon}</span> : tabLabel,
|
|
|
+ };
|
|
|
+ })}
|
|
|
/>
|
|
|
|
|
|
<div className="cpu-chart-wrap">
|
|
|
+ {activeMetric?.title && <div className="history-chart-title">{t(activeMetric.title)}</div>}
|
|
|
<Sparkline
|
|
|
data={points}
|
|
|
+ data2={activeMetric?.key2 ? points2 : undefined}
|
|
|
+ data3={activeMetric?.key3 ? points3 : undefined}
|
|
|
+ stroke2={activeMetric?.stroke2}
|
|
|
+ stroke3={activeMetric?.stroke3}
|
|
|
+ name1={activeMetric?.name1}
|
|
|
+ name2={activeMetric?.name2}
|
|
|
+ name3={activeMetric?.name3}
|
|
|
labels={labels}
|
|
|
height={260}
|
|
|
stroke={strokeColor}
|
|
|
@@ -189,7 +244,7 @@ export default function SystemHistoryModal({ open, status, onClose }: SystemHist
|
|
|
valueMax={activeMetric?.valueMax ?? null}
|
|
|
yFormatter={yFormatter}
|
|
|
tooltipLabelFormatter={tooltipLabelFormatter}
|
|
|
- extrema={{ show: true, formatter: yFormatter }}
|
|
|
+ extrema={{ show: !activeMetric?.key2, formatter: yFormatter }}
|
|
|
/>
|
|
|
</div>
|
|
|
</Modal>
|