mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-05 20:54:14 +00:00
Step 4 of the planned vue->react migration. The nodes entry brings in
the largest shared-infrastructure batch so far — every authenticated
react page from here on can lean on these.
New shared pieces (live alongside their .vue counterparts during
coexistence):
* hooks/useMediaQuery.ts — useState + resize listener
* hooks/useWebSocket.ts — wraps WebSocketClient, subscribes on mount
and unsubscribes on unmount. The underlying client is a single
module-level instance so multiple components on the same page
share one socket.
* hooks/useNodes.ts — node list state + CRUD + probe/test, including
the totals memo (online/offline/avgLatency) used by the summary card.
applyNodesEvent is the entry point for the heartbeat-pushed list.
* components/CustomStatistic.tsx — thin Statistic wrapper, prefix +
suffix slots become props.
* components/Sparkline.tsx — the SVG line chart with measured-width
axis scaling, gradient fill, tooltip overlay, and per-instance
gradient id from React.useId. ResizeObserver lifecycle is in
useEffect; the math is unchanged.
Pages:
* NodesPage — wires hooks + WebSocket together, renders summary card
+ NodeList, hosts the form modal. Uses Modal.useModal() for the
delete confirm so the dialog inherits ConfigProvider theming.
* NodeList — desktop renders a Table with expandable history rows;
mobile flips to a vertical card list whose actions live in a
bottom-right Dropdown. The IP-blur eye toggle persists across both.
* NodeFormModal — controlled form (useState object, single setForm
per change). The reset-on-open effect computes the next state
once and applies it with eslint-disable to satisfy the new
react-hooks/set-state-in-effect rule on a legitimate pattern.
* NodeHistoryPanel — polls /panel/api/nodes/history/{id}/{metric}/
{bucket} every 15s, renders cpu+mem sparklines side-by-side.
30 lines
586 B
CSS
30 lines
586 B
CSS
.sparkline-svg {
|
|
display: block;
|
|
width: 100%;
|
|
}
|
|
|
|
.sparkline-svg .cpu-grid-y-text,
|
|
.sparkline-svg .cpu-grid-x-text {
|
|
fill: rgba(0, 0, 0, 0.65);
|
|
}
|
|
|
|
.sparkline-svg .cpu-grid-text {
|
|
fill: rgba(0, 0, 0, 0.88);
|
|
}
|
|
|
|
body.dark .sparkline-svg .cpu-grid-y-text,
|
|
body.dark .sparkline-svg .cpu-grid-x-text {
|
|
fill: rgba(255, 255, 255, 0.85);
|
|
}
|
|
|
|
body.dark .sparkline-svg .cpu-grid-text {
|
|
fill: rgba(255, 255, 255, 0.95);
|
|
}
|
|
|
|
body.dark .sparkline-svg .cpu-grid-line {
|
|
stroke: rgba(255, 255, 255, 0.12);
|
|
}
|
|
|
|
body.dark .sparkline-svg .cpu-grid-h-line {
|
|
stroke: rgba(255, 255, 255, 0.35);
|
|
}
|