Pulse/frontend-modern/src/components/Dashboard/MetricBar.tsx
rcourtman 886368ec44 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
2025-11-09 22:31:35 +00:00

105 lines
3.6 KiB
TypeScript

import { Show, createMemo } from 'solid-js';
import { Sparkline } from '@/components/shared/Sparkline';
import { useMetricsViewMode } from '@/stores/metricsViewMode';
import { getMetricHistory } from '@/stores/metricsHistory';
interface MetricBarProps {
value: number;
label: string;
sublabel?: string;
type?: 'cpu' | 'memory' | 'disk' | 'generic';
resourceId?: string; // Required for sparkline mode to fetch history
}
export function MetricBar(props: MetricBarProps) {
const { viewMode } = useMetricsViewMode();
const width = createMemo(() => Math.min(props.value, 100));
// Get color based on percentage and metric type (matching original)
const getColor = createMemo(() => {
const percentage = props.value;
const metric = props.type || 'generic';
if (metric === 'cpu') {
if (percentage >= 90) return 'red';
if (percentage >= 80) return 'yellow';
return 'green';
} else if (metric === 'memory') {
if (percentage >= 85) return 'red';
if (percentage >= 75) return 'yellow';
return 'green';
} else if (metric === 'disk') {
if (percentage >= 90) return 'red';
if (percentage >= 80) return 'yellow';
return 'green';
} else {
if (percentage >= 90) return 'red';
if (percentage >= 75) return 'yellow';
return 'green';
}
});
// Map color to CSS classes
const progressColorClass = createMemo(() => {
const colorMap = {
red: 'bg-red-500/60 dark:bg-red-500/50',
yellow: 'bg-yellow-500/60 dark:bg-yellow-500/50',
green: 'bg-green-500/60 dark:bg-green-500/50',
};
return colorMap[getColor()] || 'bg-gray-500/60 dark:bg-gray-500/50';
});
// Get metric history for sparkline
const metricHistory = createMemo(() => {
if (viewMode() !== 'sparklines' || !props.resourceId) return [];
return getMetricHistory(props.resourceId);
});
// Determine which metric type to use for sparkline
const sparklineMetric = (): 'cpu' | 'memory' | 'disk' => {
const type = props.type || 'cpu';
if (type === 'generic') return 'cpu';
return type;
};
return (
<Show
when={viewMode() === 'sparklines' && props.resourceId}
fallback={
// Original progress bar mode
<div class="metric-text w-full h-6 flex items-center">
<div class="relative w-full h-3.5 rounded overflow-hidden bg-gray-200 dark:bg-gray-600">
<div class={`absolute top-0 left-0 h-full ${progressColorClass()}`} style={{ width: `${width()}%` }} />
<span class="absolute inset-0 flex items-center justify-center text-[10px] font-medium text-gray-800 dark:text-gray-100 leading-none">
<span class="flex items-center gap-1 whitespace-nowrap px-0.5">
<span>{props.label}</span>
<Show when={props.sublabel}>
{(sublabel) => (
<span class="metric-sublabel hidden xl:inline font-normal">
({sublabel()})
</span>
)}
</Show>
</span>
</span>
</div>
</div>
}
>
{/* Sparkline mode */}
<div class="metric-text w-full h-6 flex items-center gap-1.5">
<div class="flex-1 min-w-0">
<Sparkline
data={metricHistory()}
metric={sparklineMetric()}
width={0}
height={24}
/>
</div>
<span class="text-[10px] font-medium text-gray-800 dark:text-gray-100 whitespace-nowrap flex-shrink-0 min-w-[35px]">
{props.label}
</span>
</div>
</Show>
);
}