NodeHistoryPanel.tsx 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177
  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. const formatKbps = (v: number) => v.toLocaleString(undefined, { maximumFractionDigits: 1 });
  23. export default function NodeHistoryPanel({ node, bucket = 30 }: NodeHistoryPanelProps) {
  24. const { t } = useTranslation();
  25. const [cpuPoints, setCpuPoints] = useState<number[]>([]);
  26. const [cpuLabels, setCpuLabels] = useState<string[]>([]);
  27. const [memPoints, setMemPoints] = useState<number[]>([]);
  28. const [memLabels, setMemLabels] = useState<string[]>([]);
  29. const [netUpPoints, setNetUpPoints] = useState<number[]>([]);
  30. const [netUpLabels, setNetUpLabels] = useState<string[]>([]);
  31. const [netDownPoints, setNetDownPoints] = useState<number[]>([]);
  32. const [netDownLabels, setNetDownLabels] = useState<string[]>([]);
  33. const lastNodeId = useRef<number>(node.id);
  34. useEffect(() => {
  35. let cancelled = false;
  36. const bucketLabel = (unixSec: number) => {
  37. const d = new Date(unixSec * 1000);
  38. const hh = String(d.getHours()).padStart(2, '0');
  39. const mm = String(d.getMinutes()).padStart(2, '0');
  40. if (bucket >= 60) return `${hh}:${mm}`;
  41. const ss = String(d.getSeconds()).padStart(2, '0');
  42. return `${hh}:${mm}:${ss}`;
  43. };
  44. // cpu/mem are percentages (clamp 0-100); net throughput is bytes/sec shown
  45. // as KB/s, which must opt out of Sparkline's 0-100 "%" defaults.
  46. const fetchSeries = async (metric: string, kind: 'pct' | 'rate') => {
  47. try {
  48. const url = `/panel/api/nodes/history/${node.id}/${metric}/${bucket}`;
  49. const msg = (await HttpUtil.get(url)) as ApiMsg<SeriesPoint[]>;
  50. if (msg?.success && Array.isArray(msg.obj)) {
  51. const vals: number[] = [];
  52. const labs: string[] = [];
  53. for (const p of msg.obj) {
  54. labs.push(bucketLabel(p.t));
  55. const n = Number(p.v) || 0;
  56. vals.push(kind === 'pct' ? Math.max(0, Math.min(100, n)) : Math.max(0, n / 1024));
  57. }
  58. return { vals, labs };
  59. }
  60. } catch (e) {
  61. console.error('node history fetch failed', metric, e);
  62. }
  63. return { vals: [] as number[], labs: [] as string[] };
  64. };
  65. const refresh = async () => {
  66. const [cpu, mem, netUp, netDown] = await Promise.all([
  67. fetchSeries('cpu', 'pct'),
  68. fetchSeries('mem', 'pct'),
  69. fetchSeries('netUp', 'rate'),
  70. fetchSeries('netDown', 'rate'),
  71. ]);
  72. if (cancelled) return;
  73. setCpuPoints(cpu.vals);
  74. setCpuLabels(cpu.labs);
  75. setMemPoints(mem.vals);
  76. setMemLabels(mem.labs);
  77. setNetUpPoints(netUp.vals);
  78. setNetUpLabels(netUp.labs);
  79. setNetDownPoints(netDown.vals);
  80. setNetDownLabels(netDown.labs);
  81. };
  82. refresh();
  83. const timer = window.setInterval(refresh, REFRESH_MS);
  84. lastNodeId.current = node.id;
  85. return () => {
  86. cancelled = true;
  87. window.clearInterval(timer);
  88. };
  89. }, [node.id, bucket]);
  90. return (
  91. <div className="node-history-panel">
  92. <div className="series">
  93. <div className="series-title">{t('pages.nodes.cpu')}</div>
  94. <Sparkline
  95. data={cpuPoints}
  96. labels={cpuLabels}
  97. height={120}
  98. stroke="#008771"
  99. showGrid
  100. showAxes
  101. tickCountX={4}
  102. maxPoints={cpuPoints.length || 1}
  103. fillOpacity={0.18}
  104. markerRadius={2.6}
  105. showTooltip
  106. />
  107. </div>
  108. <div className="series">
  109. <div className="series-title">{t('pages.nodes.mem')}</div>
  110. <Sparkline
  111. data={memPoints}
  112. labels={memLabels}
  113. height={120}
  114. stroke="#7c4dff"
  115. showGrid
  116. showAxes
  117. tickCountX={4}
  118. maxPoints={memPoints.length || 1}
  119. fillOpacity={0.18}
  120. markerRadius={2.6}
  121. showTooltip
  122. />
  123. </div>
  124. <div className="series">
  125. <div className="series-title">{t('pages.nodes.netUp')}</div>
  126. <Sparkline
  127. data={netUpPoints}
  128. labels={netUpLabels}
  129. height={120}
  130. stroke="#1677ff"
  131. showGrid
  132. showAxes
  133. tickCountX={4}
  134. maxPoints={netUpPoints.length || 1}
  135. fillOpacity={0.18}
  136. markerRadius={2.6}
  137. showTooltip
  138. valueMax={null}
  139. yFormatter={formatKbps}
  140. />
  141. </div>
  142. <div className="series">
  143. <div className="series-title">{t('pages.nodes.netDown')}</div>
  144. <Sparkline
  145. data={netDownPoints}
  146. labels={netDownLabels}
  147. height={120}
  148. stroke="#fa8c16"
  149. showGrid
  150. showAxes
  151. tickCountX={4}
  152. maxPoints={netDownPoints.length || 1}
  153. fillOpacity={0.18}
  154. markerRadius={2.6}
  155. showTooltip
  156. valueMax={null}
  157. yFormatter={formatKbps}
  158. />
  159. </div>
  160. </div>
  161. );
  162. }