NodeHistoryPanel.tsx 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123
  1. import { useEffect, useRef, useState } from 'react';
  2. import { useTranslation } from 'react-i18next';
  3. import { HttpUtil } from '@/utils';
  4. import { Sparkline } from '@/components/viz';
  5. import './NodeHistoryPanel.css';
  6. interface NodeRef {
  7. id: number;
  8. }
  9. interface NodeHistoryPanelProps {
  10. node: NodeRef;
  11. bucket?: number;
  12. }
  13. interface SeriesPoint {
  14. t: number;
  15. v: number;
  16. }
  17. interface ApiMsg<T = unknown> {
  18. success?: boolean;
  19. obj?: T;
  20. }
  21. const REFRESH_MS = 15000;
  22. export default function NodeHistoryPanel({ node, bucket = 30 }: NodeHistoryPanelProps) {
  23. const { t } = useTranslation();
  24. const [cpuPoints, setCpuPoints] = useState<number[]>([]);
  25. const [cpuLabels, setCpuLabels] = useState<string[]>([]);
  26. const [memPoints, setMemPoints] = useState<number[]>([]);
  27. const [memLabels, setMemLabels] = useState<string[]>([]);
  28. const lastNodeId = useRef<number>(node.id);
  29. useEffect(() => {
  30. let cancelled = false;
  31. const bucketLabel = (unixSec: number) => {
  32. const d = new Date(unixSec * 1000);
  33. const hh = String(d.getHours()).padStart(2, '0');
  34. const mm = String(d.getMinutes()).padStart(2, '0');
  35. if (bucket >= 60) return `${hh}:${mm}`;
  36. const ss = String(d.getSeconds()).padStart(2, '0');
  37. return `${hh}:${mm}:${ss}`;
  38. };
  39. const fetchSeries = async (metric: 'cpu' | 'mem') => {
  40. try {
  41. const url = `/panel/api/nodes/history/${node.id}/${metric}/${bucket}`;
  42. const msg = await HttpUtil.get(url) as ApiMsg<SeriesPoint[]>;
  43. if (msg?.success && Array.isArray(msg.obj)) {
  44. const vals: number[] = [];
  45. const labs: string[] = [];
  46. for (const p of msg.obj) {
  47. labs.push(bucketLabel(p.t));
  48. vals.push(Math.max(0, Math.min(100, Number(p.v) || 0)));
  49. }
  50. return { vals, labs };
  51. }
  52. } catch (e) {
  53. console.error('node history fetch failed', metric, e);
  54. }
  55. return { vals: [] as number[], labs: [] as string[] };
  56. };
  57. const refresh = async () => {
  58. const [cpu, mem] = await Promise.all([fetchSeries('cpu'), fetchSeries('mem')]);
  59. if (cancelled) return;
  60. setCpuPoints(cpu.vals);
  61. setCpuLabels(cpu.labs);
  62. setMemPoints(mem.vals);
  63. setMemLabels(mem.labs);
  64. };
  65. refresh();
  66. const timer = window.setInterval(refresh, REFRESH_MS);
  67. lastNodeId.current = node.id;
  68. return () => {
  69. cancelled = true;
  70. window.clearInterval(timer);
  71. };
  72. }, [node.id, bucket]);
  73. return (
  74. <div className="node-history-panel">
  75. <div className="series">
  76. <div className="series-title">{t('pages.nodes.cpu')}</div>
  77. <Sparkline
  78. data={cpuPoints}
  79. labels={cpuLabels}
  80. height={120}
  81. stroke="#008771"
  82. showGrid
  83. showAxes
  84. tickCountX={4}
  85. maxPoints={cpuPoints.length || 1}
  86. fillOpacity={0.18}
  87. markerRadius={2.6}
  88. showTooltip
  89. />
  90. </div>
  91. <div className="series">
  92. <div className="series-title">{t('pages.nodes.mem')}</div>
  93. <Sparkline
  94. data={memPoints}
  95. labels={memLabels}
  96. height={120}
  97. stroke="#7c4dff"
  98. showGrid
  99. showAxes
  100. tickCountX={4}
  101. maxPoints={memPoints.length || 1}
  102. fillOpacity={0.18}
  103. markerRadius={2.6}
  104. showTooltip
  105. />
  106. </div>
  107. </div>
  108. );
  109. }