From 886368ec447c184d7db32eda132447853f383579 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Sun, 9 Nov 2025 22:31:35 +0000 Subject: [PATCH] feat: add sparklines view mode for metrics visualization Add comprehensive sparkline chart support as an alternative to progress bars for CPU, Memory, and Disk metrics across all tables. Features: - Toggle between bars/trends view modes (persisted to localStorage) - 30-second sampling with 2-hour retention window using ring buffer - Canvas-based rendering with shared requestAnimationFrame for efficiency - Hover tooltips showing exact values and timestamps - Threshold reference lines (warning/critical) for context - localStorage persistence survives page refreshes (12-hour max age) - Dynamic width adaptation to column size - Namespaced resource IDs prevent collisions - Lifecycle cleanup prevents memory leaks Performance optimizations: - Decoupled sampling from WebSocket handler (6x reduction in recording) - O(1) ring buffer insertions (no array cloning) - Batched canvas rendering (single rAF for all sparklines) - Debounced localStorage writes - Automatic pruning of removed resources UI improvements: - Consistent radio toggle styling matching other filters - Fixed column widths prevent layout shift during toggle - Fixed row heights prevent vertical size changes - Sparklines fill available column width proportionally --- frontend-modern/src/App.tsx | 6 + .../src/components/Dashboard/Dashboard.tsx | 6 +- .../components/Dashboard/DashboardFilter.tsx | 6 + .../src/components/Dashboard/GuestRow.tsx | 16 +- .../src/components/Dashboard/MetricBar.tsx | 69 +++- .../src/components/Docker/DockerFilter.tsx | 4 + .../Docker/DockerHostSummaryTable.tsx | 19 +- .../components/Docker/DockerUnifiedTable.tsx | 32 +- .../components/shared/MetricsViewToggle.tsx | 42 +++ .../components/shared/NodeSummaryTable.tsx | 8 +- .../src/components/shared/Sparkline.tsx | 286 +++++++++++++++ frontend-modern/src/stores/metricsHistory.ts | 347 ++++++++++++++++++ frontend-modern/src/stores/metricsSampler.ts | 181 +++++++++ frontend-modern/src/stores/metricsViewMode.ts | 75 ++++ frontend-modern/src/stores/websocket.ts | 42 ++- .../src/utils/canvasRenderQueue.ts | 69 ++++ frontend-modern/src/utils/localStorage.ts | 3 + frontend-modern/src/utils/metricsKeys.ts | 40 ++ 18 files changed, 1216 insertions(+), 35 deletions(-) create mode 100644 frontend-modern/src/components/shared/MetricsViewToggle.tsx create mode 100644 frontend-modern/src/components/shared/Sparkline.tsx create mode 100644 frontend-modern/src/stores/metricsHistory.ts create mode 100644 frontend-modern/src/stores/metricsSampler.ts create mode 100644 frontend-modern/src/stores/metricsViewMode.ts create mode 100644 frontend-modern/src/utils/canvasRenderQueue.ts create mode 100644 frontend-modern/src/utils/metricsKeys.ts diff --git a/frontend-modern/src/App.tsx b/frontend-modern/src/App.tsx index ec19174..5c14a77 100644 --- a/frontend-modern/src/App.tsx +++ b/frontend-modern/src/App.tsx @@ -34,6 +34,7 @@ import { DemoBanner } from './components/DemoBanner'; import { createTooltipSystem } from './components/shared/Tooltip'; import type { State } from '@/types/api'; import { ProxmoxIcon } from '@/components/icons/ProxmoxIcon'; +import { startMetricsSampler } from './stores/metricsSampler'; import BoxesIcon from 'lucide-solid/icons/boxes'; import MonitorIcon from 'lucide-solid/icons/monitor'; import BellIcon from 'lucide-solid/icons/bell'; @@ -207,6 +208,11 @@ function App() { }; const alertsActivation = useAlertsActivation(); + // Start metrics sampler for sparklines + onMount(() => { + startMetricsSampler(); + }); + let hasPreloadedRoutes = false; let hasFetchedVersionInfo = false; const preloadLazyRoutes = () => { diff --git a/frontend-modern/src/components/Dashboard/Dashboard.tsx b/frontend-modern/src/components/Dashboard/Dashboard.tsx index 9534ef8..23a2bfa 100644 --- a/frontend-modern/src/components/Dashboard/Dashboard.tsx +++ b/frontend-modern/src/components/Dashboard/Dashboard.tsx @@ -940,19 +940,19 @@ export function Dashboard(props: DashboardProps) { Uptime {sortKey() === 'uptime' && (sortDirection() === 'asc' ? '▲' : '▼')} handleSort('cpu')} > CPU {sortKey() === 'cpu' && (sortDirection() === 'asc' ? '▲' : '▼')} handleSort('memory')} > Memory {sortKey() === 'memory' && (sortDirection() === 'asc' ? '▲' : '▼')} handleSort('disk')} > Disk {sortKey() === 'disk' && (sortDirection() === 'asc' ? '▲' : '▼')} diff --git a/frontend-modern/src/components/Dashboard/DashboardFilter.tsx b/frontend-modern/src/components/Dashboard/DashboardFilter.tsx index 60e7d0c..54296ec 100644 --- a/frontend-modern/src/components/Dashboard/DashboardFilter.tsx +++ b/frontend-modern/src/components/Dashboard/DashboardFilter.tsx @@ -1,6 +1,7 @@ import { Component, Show, For, createSignal, onMount, createEffect, onCleanup } from 'solid-js'; import { Card } from '@/components/shared/Card'; import { SearchTipsPopover } from '@/components/shared/SearchTipsPopover'; +import { MetricsViewToggle } from '@/components/shared/MetricsViewToggle'; import { STORAGE_KEYS } from '@/utils/localStorage'; import { createSearchHistoryManager } from '@/utils/searchHistory'; @@ -394,6 +395,11 @@ export const DashboardFilter: Component = (props) => { + {/* Metrics View Toggle */} + + + + {/* Reset Button */}