diff --git a/frontend-modern/package.json b/frontend-modern/package.json index cd218d9e7..42902209f 100644 --- a/frontend-modern/package.json +++ b/frontend-modern/package.json @@ -12,7 +12,6 @@ }, "dependencies": { "@solidjs/router": "^0.10.0", - "chart.js": "^4.4.0", "solid-js": "^1.8.0" }, "devDependencies": { diff --git a/frontend-modern/src/api/charts.ts b/frontend-modern/src/api/charts.ts deleted file mode 100644 index 26a7de4ce..000000000 --- a/frontend-modern/src/api/charts.ts +++ /dev/null @@ -1,34 +0,0 @@ -import type { ChartData, NodeChartData, StorageChartData, ChartStats } from '@/types/charts'; - -export interface ChartsResponse { - data: ChartData; - nodeData: NodeChartData; - storageData: StorageChartData; - timestamp: number; - stats: ChartStats; -} - -export class ChartsAPI { - private static baseUrl = '/api/charts'; - - static async getCharts(timeRange: string = '1h'): Promise { - const response = await fetch(`${this.baseUrl}?range=${timeRange}`); - if (!response.ok) { - throw new Error('Failed to fetch chart data'); - } - return response.json(); - } - - static async getStorageCharts(rangeMinutes: number = 60): Promise; - used: Array<{timestamp: number; value: number}>; - total: Array<{timestamp: number; value: number}>; - avail: Array<{timestamp: number; value: number}>; - }>> { - const response = await fetch(`/api/storage-charts?range=${rangeMinutes}`); - if (!response.ok) { - throw new Error('Failed to fetch storage chart data'); - } - return response.json(); - } -} \ No newline at end of file diff --git a/frontend-modern/src/api/monitoring.ts b/frontend-modern/src/api/monitoring.ts index d3a65bc12..f46c6d8cb 100644 --- a/frontend-modern/src/api/monitoring.ts +++ b/frontend-modern/src/api/monitoring.ts @@ -27,26 +27,6 @@ export class MonitoringAPI { return response.json(); } - static async getChartData(params: { - id: string; - type: 'guest' | 'node' | 'storage'; - metric: 'cpu' | 'memory' | 'disk' | 'network'; - timeRange: '1h' | '6h' | '24h' | '7d' | '30d'; - }): Promise<{ - timestamps: string[]; - values: number[]; - unit: string; - }> { - const queryParams = new URLSearchParams(params); - const response = await fetch(`${this.baseUrl}/charts?${queryParams}`); - - if (!response.ok) { - throw new Error('Failed to fetch chart data'); - } - - return response.json(); - } - static async exportDiagnostics(): Promise { const response = await fetch(`${this.baseUrl}/diagnostics/export`); if (!response.ok) { diff --git a/frontend-modern/src/components/Dashboard/Dashboard.tsx b/frontend-modern/src/components/Dashboard/Dashboard.tsx index 69972b68c..436521b33 100644 --- a/frontend-modern/src/components/Dashboard/Dashboard.tsx +++ b/frontend-modern/src/components/Dashboard/Dashboard.tsx @@ -1,12 +1,10 @@ -import { createSignal, createMemo, createEffect, For, Show, onCleanup } from 'solid-js'; +import { createSignal, createMemo, createEffect, For, Show } from 'solid-js'; import type { VM, Container, Node } from '@/types/api'; import { GuestRow } from './GuestRow'; import NodeCard from './NodeCard'; import { useWebSocket } from '@/App'; import { getAlertStyles } from '@/utils/alerts'; -import { fetchChartData, shouldFetchChartData } from '@/stores/charts'; import { createTooltipSystem, showTooltip, hideTooltip } from '@/components/shared/Tooltip'; -import { POLLING_INTERVALS } from '@/constants'; import { ComponentErrorBoundary } from '@/components/ErrorBoundary'; import { ScrollableTable } from '@/components/shared/ScrollableTable'; import { parseFilterStack, evaluateFilterStack } from '@/utils/searchQuery'; @@ -19,8 +17,6 @@ interface DashboardProps { type ViewMode = 'all' | 'vm' | 'lxc'; type StatusMode = 'all' | 'running' | 'stopped'; -type DisplayMode = 'standard' | 'charts'; -type TimeRange = '5m' | '15m' | '30m' | '1h' | '4h' | '12h' | '24h' | '7d'; export function Dashboard(props: DashboardProps) { @@ -38,19 +34,6 @@ export function Dashboard(props: DashboardProps) { (storedStatusMode === 'all' || storedStatusMode === 'running' || storedStatusMode === 'stopped') ? storedStatusMode : 'all' ); - const storedDisplayMode = localStorage.getItem('dashboardDisplayMode'); - const [displayMode, setDisplayMode] = createSignal( - (storedDisplayMode === 'standard' || storedDisplayMode === 'charts') ? storedDisplayMode : 'standard' - ); - - const storedTimeRange = localStorage.getItem('dashboardTimeRange'); - const [timeRange, setTimeRange] = createSignal( - (storedTimeRange === '5m' || storedTimeRange === '15m' || storedTimeRange === '30m' || - storedTimeRange === '1h' || storedTimeRange === '4h' || storedTimeRange === '12h' || - storedTimeRange === '24h' || storedTimeRange === '7d') ? storedTimeRange : '1h' - ); - - const [showCharts, setShowCharts] = createSignal(localStorage.getItem('dashboardShowCharts') === 'true'); const [showFilters, setShowFilters] = createSignal( localStorage.getItem('dashboardShowFilters') !== null ? localStorage.getItem('dashboardShowFilters') === 'true' @@ -73,37 +56,12 @@ export function Dashboard(props: DashboardProps) { localStorage.setItem('dashboardStatusMode', statusMode()); }); - createEffect(() => { - localStorage.setItem('dashboardDisplayMode', displayMode()); - }); - - createEffect(() => { - localStorage.setItem('dashboardTimeRange', timeRange()); - }); - - createEffect(() => { - localStorage.setItem('dashboardShowCharts', showCharts().toString()); - }); createEffect(() => { localStorage.setItem('dashboardShowFilters', showFilters().toString()); }); - // Chart update interval - let chartUpdateInterval: number | undefined; - - // Track if chart data is loading (no longer used for blocking) - - // Preload chart data when component mounts - createEffect(() => { - // Preload chart data immediately on mount for instant charts - if (shouldFetchChartData()) { - fetchChartData(timeRange()).catch(() => { - // Silently handle errors during preload - }); - } - }); // Sort handler const handleSort = (key: keyof (VM | Container)) => { @@ -139,10 +97,9 @@ export function Dashboard(props: DashboardProps) { // Escape key behavior if (e.key === 'Escape') { // First check if we have search/filters to clear - if (search().trim() || showCharts() || sortKey() !== 'vmid' || sortDirection() !== 'asc') { + if (search().trim() || sortKey() !== 'vmid' || sortDirection() !== 'asc') { // Clear search and reset filters setSearch(''); - setShowCharts(false); setSortKey('vmid'); setSortDirection('asc'); @@ -173,40 +130,6 @@ export function Dashboard(props: DashboardProps) { return () => document.removeEventListener('keydown', handleKeyDown); }); - // Fetch chart data when in charts mode or time range changes - createEffect(() => { - if (displayMode() === 'charts') { - // Fetch data without blocking the UI - fetchChartData(timeRange()); - - // Setup periodic updates - chartUpdateInterval = window.setInterval(() => { - if (shouldFetchChartData()) { - fetchChartData(timeRange()); - } - }, POLLING_INTERVALS.CHART_UPDATE); - } else { - // Clear interval when not in charts mode - if (chartUpdateInterval) { - window.clearInterval(chartUpdateInterval); - chartUpdateInterval = undefined; - } - } - }); - - // Update charts when time range changes - createEffect(() => { - if (displayMode() === 'charts') { - fetchChartData(timeRange()); - } - }); - - // Cleanup on unmount - onCleanup(() => { - if (chartUpdateInterval) { - window.clearInterval(chartUpdateInterval); - } - }); // Combine VMs and containers into a single list const allGuests = createMemo(() => { @@ -403,7 +326,7 @@ export function Dashboard(props: DashboardProps) { Filters & Search - + Active @@ -481,7 +404,6 @@ export function Dashboard(props: DashboardProps) { - - - - - {/* Type Filter */}
@@ -608,32 +497,6 @@ export function Dashboard(props: DashboardProps) { Stopped
- - {/* Chart Time Range Controls - Show when charts enabled */} - - <> - -
- Time Range: -
- - {(range) => ( - - )} - -
-
- -
@@ -793,8 +656,6 @@ export function Dashboard(props: DashboardProps) { diff --git a/frontend-modern/src/components/Dashboard/GuestRow.tsx b/frontend-modern/src/components/Dashboard/GuestRow.tsx index e121ccb7e..205148286 100644 --- a/frontend-modern/src/components/Dashboard/GuestRow.tsx +++ b/frontend-modern/src/components/Dashboard/GuestRow.tsx @@ -4,8 +4,6 @@ import { AlertIndicator, AlertCountBadge } from '@/components/shared/AlertIndica import { formatBytes, formatUptime } from '@/utils/format'; import { MetricBar } from './MetricBar'; import { IOMetric } from './IOMetric'; -import { DynamicChart } from '@/components/shared/DynamicChart'; -import { getGuestChartData } from '@/stores/charts'; import { getResourceAlerts } from '@/utils/alerts'; import { useWebSocket } from '@/App'; @@ -16,15 +14,10 @@ const isVM = (guest: Guest): guest is VM => { return guest.type === 'qemu'; }; -type DisplayMode = 'standard' | 'charts' | 'alerts'; -type TimeRange = '5m' | '15m' | '30m' | '1h' | '4h' | '12h' | '24h' | '7d'; interface GuestRowProps { guest: Guest; showNode?: boolean; - displayMode?: DisplayMode; - timeRange?: TimeRange; - chartDataLoading?: boolean; alertStyles?: { rowClass: string; indicatorClass: string; @@ -57,20 +50,6 @@ export function GuestRow(props: GuestRowProps) { return getResourceAlerts(guestId, activeAlerts); }); - - // Get guest ID for chart data lookup - must match the format from the API - const guestId = createMemo(() => props.guest.id || `${props.guest.instance}-${props.guest.name}-${props.guest.vmid}`); - - // Get chart data from store - const cpuHistory = createMemo(() => getGuestChartData(guestId(), 'cpu')); - const memHistory = createMemo(() => getGuestChartData(guestId(), 'memory')); - const diskHistory = createMemo(() => getGuestChartData(guestId(), 'disk')); - const diskReadHistory = createMemo(() => getGuestChartData(guestId(), 'diskread')); - const diskWriteHistory = createMemo(() => getGuestChartData(guestId(), 'diskwrite')); - const netInHistory = createMemo(() => getGuestChartData(guestId(), 'netin')); - const netOutHistory = createMemo(() => getGuestChartData(guestId(), 'netout')); - - // Legacy fetch code removed - chart data is now fetched globally by the Dashboard // Get row styling - include alert styles if present const rowClass = createMemo(() => { @@ -141,234 +120,53 @@ export function GuestRow(props: GuestRowProps) { {/* CPU */} - -
- - -
-
- - - - - - + {/* Memory */} - -
- - -
-
- - - - - - + {/* Disk */} - 0}> -
- - -
-
- 0}> - 0} + fallback={-} + > + - - 0} - fallback={-} - > - - - {/* Disk I/O */} - - - - - - - - - + - - - - - - - - - + {/* Network I/O */} - - - - - - - - - + - - - - - - - - - + diff --git a/frontend-modern/src/components/Dashboard/Sparkline.tsx b/frontend-modern/src/components/Dashboard/Sparkline.tsx deleted file mode 100644 index f2c42c6b7..000000000 --- a/frontend-modern/src/components/Dashboard/Sparkline.tsx +++ /dev/null @@ -1,289 +0,0 @@ -import { Component, createMemo, createSignal, onCleanup } from 'solid-js'; -import { CHART_CONFIG, processChartData, formatTimeAgo, formatChartValue } from '@/stores/charts'; -import type { ChartDataPoint } from '@/stores/charts'; -import { showTooltip, hideTooltip } from '@/components/shared/Tooltip'; - -interface SparklineProps { - data: number[] | ChartDataPoint[]; - width?: number; - height?: number; - color?: string; - strokeWidth?: number; - filled?: boolean; - metric?: string; - guestId?: string; - chartType?: 'mini' | 'sparkline' | 'storage'; - showTooltip?: boolean; - responsive?: boolean; - forceGray?: boolean; -} - -const Sparkline: Component = (props) => { - const chartType = () => props.chartType || 'mini'; - const config = () => CHART_CONFIG[chartType()]; - const width = () => props.width || config().width; - const height = () => props.height || config().height; - const strokeWidth = () => props.strokeWidth || CHART_CONFIG.strokeWidth; - const metric = () => props.metric || 'generic'; - - let svgRef: SVGSVGElement | undefined; - let tooltipTimeout: number | undefined; - const [isHovering, setIsHovering] = createSignal(false); - const [hoverPoint, setHoverPoint] = createSignal<{x: number, y: number, value: number, timestamp?: number} | null>(null); - const [, setTooltipContent] = createSignal(''); - const [, setTooltipPosition] = createSignal<{x: number, y: number}>({x: 0, y: 0}); - - // Convert data to ChartDataPoint format if needed - const chartData = createMemo(() => { - const rawData = props.data || []; - - // If already ChartDataPoint format, use as-is - if (rawData.length > 0 && typeof rawData[0] === 'object' && 'timestamp' in rawData[0]) { - return rawData as ChartDataPoint[]; - } - - // Convert number array to ChartDataPoint array - return (rawData as number[]).map((value, index) => ({ - timestamp: Date.now() - (rawData.length - index - 1) * 5000, // Assume 5s intervals - value - })); - }); - - // Process data with adaptive sampling if needed - const processedData = createMemo(() => { - const data = chartData(); - // Skip processing if we don't have much data - if (data.length <= 10) return data; - - if (!props.guestId || !props.metric) return data; - - // For storage charts, just return the data as-is - const type = chartType(); - if (type === 'storage') return data; - - return processChartData(data, type as 'mini' | 'sparkline', props.guestId, props.metric); - }); - - // Get smart color based on data - const smartColor = createMemo(() => { - if (props.color) return props.color; - - // Force gray for dashboard charts - if (props.forceGray) { - const isDarkMode = document.documentElement.classList.contains('dark'); - return isDarkMode ? '#6b7280' : '#d1d5db'; // gray-500/gray-300 - } - - const data = processedData(); - const values = data.map(d => d.value); - return CHART_CONFIG.getSmartColor(values, metric()); - }); - - // Generate SVG path from data points - const pathData = createMemo(() => { - const data = processedData(); - - if (data.length < 2) return { line: '', area: '', points: [], minValue: 0, maxValue: 0 }; - - const w = width(); - const h = height(); - const padding = config().padding; - - // Extract values for scaling - const values = data.map(d => d.value); - const minValue = Math.min(...values); - const maxValue = Math.max(...values); - // const valueRange = maxValue - minValue; // unused - - // Smart scaling: include 0 for percentage metrics if low values - let scalingMin = minValue; - let scalingMax = maxValue; - - const isPercentageMetric = metric() === 'cpu' || metric() === 'memory' || metric() === 'disk'; - if (isPercentageMetric && minValue < 20) { - scalingMin = 0; - } else if (!isPercentageMetric && minValue < maxValue * 0.01) { - scalingMin = 0; - } - - const scalingRange = scalingMax - scalingMin || 1; - - // Calculate dimensions - const chartAreaWidth = w - 2 * padding; - const chartAreaHeight = h - 2 * padding; - const yScale = chartAreaHeight / scalingRange; - const xStep = chartAreaWidth / Math.max(1, data.length - 1); - - // Generate points with coordinates - const points = data.map((point, i) => { - const x = padding + i * xStep; - const y = h - padding - (scalingRange > 0 ? (point.value - scalingMin) * yScale : chartAreaHeight / 2); - return { x, y, value: point.value, timestamp: point.timestamp }; - }); - - // Build line path - simplified without toFixed for performance - const lineData = points.map((point, index) => - `${index === 0 ? 'M' : 'L'} ${point.x} ${point.y}` - ).join(' '); - - const baseY = h - padding; - const areaData = props.filled ? - `M ${points[0].x} ${baseY} ` + - points.map(p => `L ${p.x} ${p.y}`).join(' ') + - ` L ${points[points.length - 1].x} ${baseY} Z` - : ''; - - return { line: lineData, area: areaData, points, minValue, maxValue }; - }); - - // Generate stable gradient ID based on guest/metric - const gradientId = createMemo(() => { - if (props.guestId && props.metric) { - return `gradient-${props.guestId}-${props.metric}`.replace(/[^a-zA-Z0-9-]/g, '-'); - } - return `gradient-${Math.random().toString(36).substr(2, 9)}`; - }); - - // Handle mouse events - const handleMouseMove = (e: MouseEvent) => { - if (!svgRef || !props.showTooltip) return; - - const rect = svgRef.getBoundingClientRect(); - const x = (e.clientX - rect.left) * (width() / rect.width); - - const data = pathData(); - if (!data.points || data.points.length === 0) return; - - // Find closest point - const chartAreaWidth = width() - 2 * config().padding; - const relativeX = Math.max(0, Math.min(chartAreaWidth, x - config().padding)); - - let closestIndex = 0; - let closestDistance = Infinity; - - data.points.forEach((point, i) => { - const distance = Math.abs(point.x - config().padding - relativeX); - if (distance < closestDistance) { - closestDistance = distance; - closestIndex = i; - } - }); - - const point = data.points[closestIndex]; - if (point) { - setHoverPoint(point); - - // Update tooltip content - const value = formatChartValue(point.value, metric()); - const timeAgo = point.timestamp ? formatTimeAgo(point.timestamp) : ''; - let content = `${value}`; - if (timeAgo) content += `
${timeAgo}`; - - // Add range info - if (data.minValue !== data.maxValue) { - const minFormatted = formatChartValue(data.minValue, metric()); - const maxFormatted = formatChartValue(data.maxValue, metric()); - content += `
Range: ${minFormatted} - ${maxFormatted}`; - } - - setTooltipContent(content); - setTooltipPosition({ x: e.clientX, y: e.clientY }); - - // Show tooltip using global system - showTooltip(content, e.clientX, e.clientY); - } - }; - - const handleMouseEnter = () => { - setIsHovering(true); - }; - - const handleMouseLeave = () => { - setIsHovering(false); - setHoverPoint(null); - if (tooltipTimeout) { - clearTimeout(tooltipTimeout); - } - - // Hide tooltip using global system - hideTooltip(); - }; - - // Cleanup on unmount - onCleanup(() => { - if (tooltipTimeout) { - clearTimeout(tooltipTimeout); - } - }); - - return ( - - {props.filled && ( - - - - - - - )} - - {/* Area fill (if enabled) */} - {props.filled && pathData().area && ( - - )} - - {/* Main line */} - - - {/* Hover indicator */} - {isHovering() && hoverPoint() && ( - - - - )} - - {/* Invisible overlay for better mouse detection */} - {props.showTooltip && ( - - )} - - ); -}; - -export default Sparkline; \ No newline at end of file diff --git a/frontend-modern/src/components/Storage/Storage.tsx b/frontend-modern/src/components/Storage/Storage.tsx index 72d08afd0..83cba21f7 100644 --- a/frontend-modern/src/components/Storage/Storage.tsx +++ b/frontend-modern/src/components/Storage/Storage.tsx @@ -1,12 +1,9 @@ -import { Component, For, Show, createSignal, createMemo, createEffect, onCleanup } from 'solid-js'; +import { Component, For, Show, createSignal, createMemo, createEffect } from 'solid-js'; import { useWebSocket } from '@/App'; import { getAlertStyles } from '@/utils/alerts'; import { AlertIndicator, AlertCountBadge } from '@/components/shared/AlertIndicators'; import { formatBytes } from '@/utils/format'; -import { DynamicChart } from '@/components/shared/DynamicChart'; import { createTooltipSystem } from '@/components/shared/Tooltip'; -import { fetchChartData, getStorageChartData, shouldFetchChartData } from '@/stores/charts'; -import { POLLING_INTERVALS } from '@/constants'; import type { Storage as StorageType } from '@/types/api'; import { ComponentErrorBoundary } from '@/components/ErrorBoundary'; @@ -14,8 +11,6 @@ import { ComponentErrorBoundary } from '@/components/ErrorBoundary'; const Storage: Component = () => { const { state, connected, activeAlerts } = useWebSocket(); const [viewMode, setViewMode] = createSignal<'node' | 'storage'>('node'); - const [chartsEnabled, setChartsEnabled] = createSignal(false); - const [timeRange, setTimeRange] = createSignal('1h'); const [searchTerm, setSearchTerm] = createSignal(''); const [showFilters, setShowFilters] = createSignal( localStorage.getItem('storageShowFilters') !== null @@ -26,25 +21,10 @@ const Storage: Component = () => { // Create tooltip system const TooltipComponent = createTooltipSystem(); - // Time range options - match dashboard format - const timeRanges = [ - { value: '5m', label: '5m' }, - { value: '15m', label: '15m' }, - { value: '30m', label: '30m' }, - { value: '1h', label: '1h' }, - { value: '4h', label: '4h' }, - { value: '12h', label: '12h' }, - { value: '24h', label: '24h' }, - { value: '7d', label: '7d' } - ]; - // Load preferences from localStorage createEffect(() => { const savedViewMode = localStorage.getItem('storageViewMode'); if (savedViewMode === 'storage') setViewMode('storage'); - - const savedChartsEnabled = localStorage.getItem('storageChartsEnabled'); - if (savedChartsEnabled === 'true') setChartsEnabled(true); }); // Save preferences to localStorage @@ -52,52 +32,10 @@ const Storage: Component = () => { localStorage.setItem('storageViewMode', viewMode()); }); - createEffect(() => { - localStorage.setItem('storageChartsEnabled', chartsEnabled().toString()); - }); - createEffect(() => { localStorage.setItem('storageShowFilters', showFilters().toString()); }); - // Chart update interval - let chartUpdateInterval: number | undefined; - - // Fetch chart data when charts are enabled or time range changes - createEffect(() => { - if (chartsEnabled() && connected()) { - // Initial fetch - fetchChartData(timeRange()); - - // Setup periodic updates - chartUpdateInterval = window.setInterval(() => { - if (shouldFetchChartData()) { - fetchChartData(timeRange()); - } - }, POLLING_INTERVALS.CHART_UPDATE); - } else { - // Clear interval when charts not enabled - if (chartUpdateInterval) { - window.clearInterval(chartUpdateInterval); - chartUpdateInterval = undefined; - } - } - }); - - // Update charts when time range changes - createEffect(() => { - if (chartsEnabled()) { - fetchChartData(timeRange()); - } - }); - - // Cleanup on unmount - onCleanup(() => { - if (chartUpdateInterval) { - window.clearInterval(chartUpdateInterval); - } - }); - // Filter storage - in storage view, filter out 0 capacity const filteredStorage = createMemo(() => { const storage = state.storage || []; @@ -156,14 +94,29 @@ const Storage: Component = () => { // Match MetricBar component styling - use the same disk/generic logic if (usage >= 90) return 'bg-red-500/60 dark:bg-red-500/50'; if (usage >= 80) return 'bg-yellow-500/60 dark:bg-yellow-500/50'; - return 'bg-green-500/60 dark:bg-green-500/50'; + if (usage >= 70) return 'bg-amber-500/60 dark:bg-amber-500/50'; + if (usage >= 60) return 'bg-yellow-500/60 dark:bg-yellow-500/50'; + return 'bg-emerald-500/60 dark:bg-emerald-500/50'; }; - // Reset filters const resetFilters = () => { setSearchTerm(''); setViewMode('node'); - setChartsEnabled(false); + }; + + const getTotalByNode = (storages: StorageType[]) => { + const totals = { used: 0, total: 0, free: 0 }; + storages.forEach(s => { + totals.used += s.used || 0; + totals.total += s.total || 0; + totals.free += s.free || 0; + }); + return totals; + }; + + const calculateOverallUsage = (storages: StorageType[]) => { + const totals = getTotalByNode(storages); + return totals.total > 0 ? (totals.used / totals.total * 100) : 0; }; // Handle keyboard shortcuts @@ -171,7 +124,7 @@ const Storage: Component = () => { createEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { - // Ignore if user is typing in an input, textarea, or contenteditable + // Ignore if user is typing in an input const target = e.target as HTMLElement; const isInputField = target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || @@ -181,7 +134,7 @@ const Storage: Component = () => { // Escape key behavior if (e.key === 'Escape') { // First check if we have search/filters to clear - if (searchTerm().trim() || viewMode() !== 'node' || chartsEnabled()) { + if (searchTerm().trim() || viewMode() !== 'node') { // Clear search and reset filters resetFilters(); @@ -212,109 +165,90 @@ const Storage: Component = () => { return () => document.removeEventListener('keydown', handleKeyDown); }); - return ( -
-
- {/* Filter Section */} -
- {/* Filter toggle - visible on all screen sizes */} - - -
-
- {/* Search Row */} -
-
- setSearchTerm(e.currentTarget.value)} - class="w-full pl-9 pr-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg - bg-white dark:bg-gray-900 text-gray-800 dark:text-gray-200 placeholder-gray-400 dark:placeholder-gray-500 - focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 dark:focus:border-blue-400 outline-none transition-all" - /> - - - -
- - + Filters & Search + + + Active + + + + + + + + +
+
+ {/* Search Bar Row */} +
+
+ setSearchTerm(e.currentTarget.value)} + class="w-full pl-9 pr-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg + bg-white dark:bg-gray-900 text-gray-800 dark:text-gray-200 placeholder-gray-400 dark:placeholder-gray-500 + focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 dark:focus:border-blue-400 outline-none transition-all" + /> + + +
- {/* View Controls Row */} -
- {/* View Controls */} -
- Display: - - {/* Charts Toggle */} -
- -
- -
- - {/* View Mode Toggle */} + {/* Reset Button */} + +
+ + {/* Filters Row */} +
+ {/* View Mode Toggle */} +
+ Group by:
- - {/* Chart Time Range Controls - Show when charts enabled */} - -
-
- Time Range: -
- - {(range) => ( - - )} - -
-
-
-
- {/* Table Section */} -
+ {/* Empty State - No Storage Configured */} + +
+
+ + + +

No storage found

+

No storage repositories are configured or visible.

+
+
+
+ + {/* Storage Table */} + 0}> -
- - - - - - - - - - - - - - - - - - - - 0}> - a.localeCompare(b))} fallback={<>}> +
+
- Storage - - Nodes - - Type - - Content - - Type - - Shared - - Usage - - Avail - - Total -
+ + + + + + + + + + + + + + + + + + + a.localeCompare(b))}> {([groupName, storages]) => ( <> - {/* Group Header for Node View */} + {/* Group Header */} - - + + @@ -436,8 +346,6 @@ const Storage: Component = () => { }> {(storage) => { const usagePercent = storage.total > 0 ? (storage.used / storage.total * 100) : 0; - // Get chart data from unified store - const storageChartData = createMemo(() => getStorageChartData(storage.id, 'disk')); const isDisabled = storage.status !== 'available'; const alertStyles = getAlertStyles(storage.id || `${storage.instance}-${storage.name}`, activeAlerts); @@ -445,75 +353,60 @@ const Storage: Component = () => { return ( - - - - - - - - - + + - + + + + @@ -524,81 +417,11 @@ const Storage: Component = () => { )} - - - {/* Empty State */} - - - - - - - {/* Disconnected State */} - - - - - - -
StorageNodeTypeContentStatusSharedUsageFreeTotal
+
{groupName} - - ({storages.length} storage{storages.length !== 1 ? 's' : ''}) + + + {getTotalByNode(storages).total > 0 && ( + + {formatBytes(getTotalByNode(storages).used)} / {formatBytes(getTotalByNode(storages).total)} ({calculateOverallUsage(storages).toFixed(1)}%) + + )}
-
- {storage.name} - - ({storage.status}) - +
+
+ + {storage.name} + -
- +
+ 1}> - +
- {storage.node} (Local) - }> - - All Nodes - - ({storage.instance}) - - - - - {storage.type} - - {storage.content} - + {storage.node} + {storage.type} - + + + {storage.content || '-'} + + + {storage.status || 'unknown'} + + {storage.shared ? '✓' : '-'} - - +
- - -
-
- - {formatBytes(storage.used || 0)} / {formatBytes(storage.total || 0)} ({usagePercent.toFixed(1)}%) - -
- + + {formatBytes(storage.used || 0)} / {formatBytes(storage.total || 0)} ({usagePercent.toFixed(1)}%) + +
{formatBytes(storage.free || 0)} {formatBytes(storage.total || 0)}
-
- - - -

No Active Storage

-

- {viewMode() === 'storage' - ? 'No storage with capacity found in the cluster.' - : 'No storage configured.'} -

-
-
-
- - - -

Connection Lost

-

Unable to connect to the backend server.

-
-
-
+ + +
-
- - - + {/* Tooltip System */} diff --git a/frontend-modern/src/components/shared/DynamicChart.tsx b/frontend-modern/src/components/shared/DynamicChart.tsx deleted file mode 100644 index a5a79e654..000000000 --- a/frontend-modern/src/components/shared/DynamicChart.tsx +++ /dev/null @@ -1,119 +0,0 @@ -import { Component, onMount, onCleanup, createMemo, createEffect, Show } from 'solid-js'; -import Sparkline from '@/components/Dashboard/Sparkline'; -import { observeChart, subscribeToChartDimension } from '@/stores/chartDimensions'; -import { useIntersectionObserver } from '@/hooks/useIntersectionObserver'; -import type { ChartPoint } from '@/types/charts'; - -interface DynamicChartProps { - data: ChartPoint[] | undefined; - metric: string; - guestId: string; - chartType?: 'mini' | 'sparkline' | 'storage'; - containerClass?: string; - paddingAdjustment?: number; - lazy?: boolean; // Enable lazy loading - filled?: boolean; // Pass through to Sparkline - forceGray?: boolean; // Force gray color -} - -/** - * Optimized dynamic chart component that uses a shared ResizeObserver for better performance. - * This ensures charts fill their available space without stretching or distortion. - */ -export const DynamicChart: Component = (props) => { - const chartType = () => props.chartType || 'mini'; - const chartId = () => `${props.guestId}-${props.metric}`; - - const defaultWidth = () => { - switch (chartType()) { - case 'storage': return 184; - case 'mini': return 118; - case 'sparkline': return 66; - default: return 118; - } - }; - - let containerRef: HTMLDivElement | undefined; - let cleanupFn: (() => void) | undefined; - - // Use intersection observer for lazy loading if enabled - const isVisible = props.lazy ? useIntersectionObserver(() => containerRef) : () => true; - - // Create a memoized signal for the chart dimension - const containerWidth = createMemo(() => { - const dimension = subscribeToChartDimension(chartId())(); - if (dimension > 0) { - return dimension - (props.paddingAdjustment || 0); - } - return defaultWidth(); - }); - - onMount(() => { - if (containerRef && isVisible()) { - // Register with shared ResizeObserver only when visible - cleanupFn = observeChart(containerRef, chartId()); - } - }); - - // Re-register when visibility changes - createEffect(() => { - if (containerRef && isVisible() && !cleanupFn) { - cleanupFn = observeChart(containerRef, chartId()); - } - }); - - onCleanup(() => { - // Don't clean up if we're just re-rendering the same chart - // This prevents the dimension from resetting and causing animations - if (cleanupFn) { - cleanupFn(); - } - }); - - const getHeight = createMemo(() => { - switch (chartType()) { - case 'storage': return 14; - case 'mini': return 20; - case 'sparkline': return 16; - default: return 20; - } - }); - - // Only render if we have valid data, width, and visibility - const shouldRender = createMemo(() => { - return isVisible() && containerWidth() > 0 && props.data && (Array.isArray(props.data) ? props.data.length > 0 : true); - }); - - // Show loading skeleton immediately when visible but no data - const showLoadingSkeleton = createMemo(() => { - return isVisible() && containerWidth() > 0 && (!props.data || (Array.isArray(props.data) && props.data.length === 0)); - }); - - return ( -
- -
- - } - > - - -
- ); -}; \ No newline at end of file diff --git a/frontend-modern/src/constants.ts b/frontend-modern/src/constants.ts index e79426302..575b8bea3 100644 --- a/frontend-modern/src/constants.ts +++ b/frontend-modern/src/constants.ts @@ -3,25 +3,12 @@ // Polling and update intervals (in milliseconds) export const POLLING_INTERVALS = { DEFAULT: 5000, // 5 seconds - default polling interval - CHART_UPDATE: 5000, // 5 seconds - chart data update interval RECONNECT_BASE: 1000, // 1 second - base reconnect delay RECONNECT_MAX: 30000, // 30 seconds - max reconnect delay DATA_FLASH: 1000, // 1 second - data update indicator flash duration TOAST_DURATION: 5000, // 5 seconds - default toast notification duration } as const; -// Chart configuration -export const CHART_INTERVALS = { - '5m': 5 * 60 * 1000, - '15m': 15 * 60 * 1000, - '30m': 30 * 60 * 1000, - '1h': 60 * 60 * 1000, - '4h': 4 * 60 * 60 * 1000, - '12h': 12 * 60 * 60 * 1000, - '24h': 24 * 60 * 60 * 1000, - '7d': 7 * 24 * 60 * 60 * 1000, -} as const; - // Display thresholds (percentages) export const THRESHOLDS = { WARNING: 60, // Yellow warning threshold @@ -40,7 +27,6 @@ export const IO_THRESHOLDS = { // Animation durations (in milliseconds) export const ANIMATIONS = { TOAST_SLIDE: 300, // Toast slide in/out animation - CHART_FADE: 150, // Chart fade in/out animation } as const; // UI configuration @@ -65,7 +51,6 @@ export const STORAGE_KEYS = { DISPLAY_MODE: 'displayMode', SORT_KEY: 'sortKey', SORT_DIRECTION: 'sortDirection', - CHART_TIME_RANGE: 'chartTimeRange', ALERT_THRESHOLDS: 'alertThresholds', } as const; @@ -82,10 +67,3 @@ export const LOG_LEVELS = { export type LogLevel = keyof typeof LOG_LEVELS; -// Chart dimensions -export const CHART_DIMENSIONS = { - SPARKLINE: { width: 66, height: 16, padding: 1 }, - MINI: { width: 118, height: 20, padding: 2 }, - STORAGE: { width: 200, height: 14, padding: 1 }, - STROKE_WIDTH: 1.5, -} as const; \ No newline at end of file diff --git a/frontend-modern/src/stores/chartDimensions.ts b/frontend-modern/src/stores/chartDimensions.ts deleted file mode 100644 index b3d798182..000000000 --- a/frontend-modern/src/stores/chartDimensions.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { createSignal } from 'solid-js'; - -interface ChartDimensions { - [key: string]: number; -} - -// Global store for chart dimensions -const [dimensions, setDimensions] = createSignal({}); - -// Cache to persist dimensions between re-renders -const dimensionCache: ChartDimensions = {}; - -// Single ResizeObserver instance shared by all charts -let resizeObserver: ResizeObserver | null = null; -const observedElements = new Map(); - -// Debounce timer -let resizeTimeout: number | undefined; - -// Initialize the shared ResizeObserver -function initResizeObserver() { - if (!resizeObserver && typeof window !== 'undefined') { - resizeObserver = new ResizeObserver((entries) => { - // Clear existing timeout - if (resizeTimeout) { - clearTimeout(resizeTimeout); - } - - // Debounce resize events by 50ms - resizeTimeout = window.setTimeout(() => { - requestAnimationFrame(() => { - const updates: ChartDimensions = {}; - - for (const entry of entries) { - const chartId = observedElements.get(entry.target); - if (chartId) { - const width = Math.floor(entry.contentRect.width); - // Only update if width actually changed - if (dimensions()[chartId] !== width) { - updates[chartId] = width; - } - } - } - - // Batch update all dimensions at once - if (Object.keys(updates).length > 0) { - // Update cache - Object.assign(dimensionCache, updates); - setDimensions(prev => ({ ...prev, ...updates })); - } - }); - }, 50); - }); - } -} - -// Register a chart element for observation -export function observeChart(element: HTMLElement, chartId: string) { - initResizeObserver(); - - if (resizeObserver && element) { - // Store the association - observedElements.set(element, chartId); - resizeObserver.observe(element); - - // Get initial dimension - use cached value if available to prevent animation - const cachedWidth = dimensionCache[chartId]; - const width = cachedWidth || Math.floor(element.offsetWidth); - dimensionCache[chartId] = width; - setDimensions(prev => ({ ...prev, [chartId]: width })); - - // Return cleanup function - return () => { - if (resizeObserver) { - resizeObserver.unobserve(element); - observedElements.delete(element); - - // Don't clean up dimension entry - keep it cached - // This prevents re-animation on re-render - // setDimensions(prev => { - // const next = { ...prev }; - // delete next[chartId]; - // return next; - // }); - } - }; - } - - return () => {}; -} - -// Get dimension for a specific chart -export function getChartDimension(chartId: string): number { - return dimensions()[chartId] || 0; -} - -// Subscribe to dimension changes for a specific chart -export function subscribeToChartDimension(chartId: string) { - return () => dimensions()[chartId]; -} - -// Clean up on app unmount -if (typeof window !== 'undefined') { - window.addEventListener('unload', () => { - if (resizeObserver) { - resizeObserver.disconnect(); - resizeObserver = null; - } - if (resizeTimeout) { - clearTimeout(resizeTimeout); - } - }); -} \ No newline at end of file diff --git a/frontend-modern/src/stores/charts.ts b/frontend-modern/src/stores/charts.ts deleted file mode 100644 index 5c7797536..000000000 --- a/frontend-modern/src/stores/charts.ts +++ /dev/null @@ -1,447 +0,0 @@ -import { createSignal } from 'solid-js'; -import { createStore } from 'solid-js/store'; -import { logger } from '@/utils/logger'; -import { ChartsAPI } from '@/api/charts'; -import type { ChartPoint, MetricData, ChartData, NodeChartData, StorageChartData } from '@/types/charts'; - -// Chart configuration based on main branch -export const CHART_CONFIG = { - // Different sizes for different use cases - sparkline: { width: 66, height: 16, padding: 1 }, // For I/O metrics - mini: { width: 118, height: 20, padding: 2 }, // For usage metrics - storage: { width: 200, height: 14, padding: 1 }, // For storage tab - default width, will be overridden dynamically - strokeWidth: 1.5, - - // Get optimal render points based on screen resolution - getRenderPoints: () => { - const screenWidth = window.screen.width; - const pixelRatio = window.devicePixelRatio || 1; - const effectiveWidth = screenWidth * pixelRatio; - - if (effectiveWidth >= 3840) return 80; // 4K - else if (effectiveWidth >= 2560) return 60; // 2K - else if (effectiveWidth >= 1920) return 40; // 1080p - else if (effectiveWidth >= 1366) return 30; // 720p - else return 25; // Small screens - }, - - // Smart color coding based on data values - getSmartColor: (values: number[], metric: string): string => { - if (!values || values.length === 0) { - const isDarkMode = document.documentElement.classList.contains('dark'); - return isDarkMode ? '#6b7280' : '#d1d5db'; // gray-500/gray-300 - } - - const currentValue = values[values.length - 1]; - const maxValue = Math.max(...values); - - if (metric === 'cpu' || metric === 'memory' || metric === 'disk') { - // Percentage-based metrics - if (metric === 'cpu') { - if (currentValue >= 90 || maxValue >= 95) return '#ef4444'; // red-500 - if (currentValue >= 80 || maxValue >= 85) return '#f59e0b'; // amber-500 - } else if (metric === 'memory') { - if (currentValue >= 85 || maxValue >= 90) return '#ef4444'; // red-500 - if (currentValue >= 75 || maxValue >= 80) return '#f59e0b'; // amber-500 - } else if (metric === 'disk') { - if (currentValue >= 90 || maxValue >= 95) return '#ef4444'; // red-500 - if (currentValue >= 80 || maxValue >= 85) return '#f59e0b'; // amber-500 - } - - const isDarkMode = document.documentElement.classList.contains('dark'); - return isDarkMode ? '#6b7280' : '#d1d5db'; // gray: normal operation - } else if (metric === 'diskread' || metric === 'diskwrite' || metric === 'netin' || metric === 'netout') { - // I/O metrics - use absolute thresholds - // const maxMBps = maxValue / (1024 * 1024); // unused - const avgValue = values.reduce((sum, v) => sum + v, 0) / values.length; - const avgMBps = avgValue / (1024 * 1024); - - if (avgMBps > 50) return '#ef4444'; // red: >50 MB/s - if (avgMBps > 10) return '#f59e0b'; // amber: >10 MB/s - if (avgMBps > 1) return '#10b981'; // green: >1 MB/s - - const isDarkMode = document.documentElement.classList.contains('dark'); - return isDarkMode ? '#6b7280' : '#d1d5db'; // gray: minimal activity - } else { - // Unknown metric - use default color - const isDarkMode = document.documentElement.classList.contains('dark'); - return isDarkMode ? '#6b7280' : '#d1d5db'; - } - }, - - // Default colors for metrics - colors: { - cpu: '#ef4444', // red-500 - memory: '#3b82f6', // blue-500 - disk: '#8b5cf6', // violet-500 - diskread: '#3b82f6', // blue-500 - diskwrite: '#f97316', // orange-500 - netin: '#10b981', // emerald-500 - netout: '#f59e0b' // amber-500 - } -}; - -// Chart data point interface -export interface ChartDataPoint { - timestamp: number; - value: number; -} - -// Store for chart data -interface ChartStore { - guestData: ChartData; - nodeData: NodeChartData; - storageData: StorageChartData; - lastFetch: number; - timeRange: string; - stats: { - oldestDataTimestamp?: number; - }; -} - -const [chartStore, setChartStore] = createStore({ - guestData: {}, - nodeData: {}, - storageData: {}, - lastFetch: 0, - timeRange: '60', // Default 1 hour - stats: {} -}); - -// Signal for current render points -const [renderPoints, setRenderPoints] = createSignal(CHART_CONFIG.getRenderPoints()); - -// Update render points on window resize -if (typeof window !== 'undefined') { - window.addEventListener('resize', () => { - const newPoints = CHART_CONFIG.getRenderPoints(); - if (newPoints !== renderPoints()) { - setRenderPoints(newPoints); - // Clear processed data cache when resolution changes - processedDataCache.clear(); - } - }); -} - -// Cache for processed chart data -const processedDataCache = new Map(); - -// Generate quick hash of data for cache validation -function generateDataHash(data: ChartDataPoint[]): string { - if (!data || data.length === 0) return '0'; - const first = data[0]; - const last = data[data.length - 1]; - // const middle = data[Math.floor(data.length / 2)]; // unused - return `${data.length}-${first?.timestamp || 0}-${last?.timestamp || 0}-${first?.value?.toFixed(2) || 0}-${last?.value?.toFixed(2) || 0}`; -} - -// Calculate importance scores for adaptive sampling -function calculateImportanceScores(data: ChartDataPoint[]): number[] { - const scores = new Array(data.length); - const values = data.map(d => d.value); - - for (let i = 0; i < data.length; i++) { - let score = 0; - - // Rate of change - if (i > 0 && i < data.length - 1) { - const change = Math.abs(values[i + 1] - values[i - 1]); - score += change; - } - - // Peaks and valleys - if (i > 0 && i < data.length - 1) { - const isPeak = values[i] > values[i - 1] && values[i] > values[i + 1]; - const isValley = values[i] < values[i - 1] && values[i] < values[i + 1]; - if (isPeak || isValley) { - score += Math.abs(values[i] - values[i - 1]) + Math.abs(values[i] - values[i + 1]); - } - } - - // Edge points get bonus - if (i === 0 || i === data.length - 1) { - score += 1000; // Ensure edges are always included - } - - scores[i] = score; - } - - return scores; -} - -// Adaptive sampling algorithm -export function adaptiveSample(data: ChartDataPoint[], targetPoints: number): ChartDataPoint[] { - if (data.length <= targetPoints) return data; - - const maxPoints = Math.max(2, targetPoints); - - // Calculate importance scores - const importance = calculateImportanceScores(data); - - // Always include first and last points - const selectedIndices = new Set([0, data.length - 1]); - const remainingPoints = maxPoints - 2; - - if (remainingPoints <= 0) { - return [data[0], data[data.length - 1]]; - } - - // Create candidates array - const candidates = []; - for (let i = 1; i < data.length - 1; i++) { - candidates.push({ index: i, importance: importance[i] }); - } - - // Sort by importance - candidates.sort((a, b) => b.importance - a.importance); - - // Add most important points up to limit - for (let i = 0; i < Math.min(remainingPoints, candidates.length); i++) { - selectedIndices.add(candidates[i].index); - } - - // Convert to sorted array and extract data points - const sortedIndices = Array.from(selectedIndices).sort((a, b) => a - b); - return sortedIndices.map(i => data[i]); -} - -// Process chart data with caching and adaptive sampling -export function processChartData( - serverData: ChartDataPoint[], - chartType: 'mini' | 'sparkline' = 'mini', - guestId: string, - metric: string -): ChartDataPoint[] { - if (!serverData || serverData.length === 0) { - return []; - } - - // Generate cache key - const cacheKey = `${guestId}-${metric}-${chartType}`; - const dataHash = generateDataHash(serverData); - - // Check cache - const cached = processedDataCache.get(cacheKey); - if (cached && cached.hash === dataHash && (Date.now() - cached.timestamp < 30000)) { - return cached.data; - } - - let targetPoints = renderPoints(); - - // Sparklines need fewer points - if (chartType === 'sparkline') { - targetPoints = Math.round(targetPoints * 0.6); - } - - let processedData: ChartDataPoint[]; - if (serverData.length <= targetPoints) { - processedData = serverData; - } else { - processedData = adaptiveSample(serverData, targetPoints); - } - - // Cache the result - processedDataCache.set(cacheKey, { - data: processedData, - timestamp: Date.now(), - hash: dataHash - }); - - // Clean up old cache entries - if (processedDataCache.size > 200) { - const now = Date.now(); - const maxAge = 60000; // 1 minute - - for (const [key, value] of processedDataCache.entries()) { - if (now - value.timestamp > maxAge) { - processedDataCache.delete(key); - } - } - } - - return processedData; -} - -// Fetch chart data from API -export async function fetchChartData(timeRange: string) { - try { - const data = await ChartsAPI.getCharts(timeRange); - - // Calculate time offset between server and browser - const serverTime = data.timestamp || Date.now(); - const browserTime = Date.now(); - const timeOffset = browserTime - serverTime; - - // Adjust all timestamps to compensate for time offset - if (data.data) { - for (const guestId in data.data) { - for (const metric in data.data[guestId]) { - if (Array.isArray(data.data[guestId][metric])) { - data.data[guestId][metric] = data.data[guestId][metric].map((point: ChartPoint) => ({ - ...point, - timestamp: point.timestamp + timeOffset - })); - } - } - } - } - - // Adjust node data timestamps - if (data.nodeData) { - for (const nodeId in data.nodeData) { - for (const metric in data.nodeData[nodeId]) { - if (Array.isArray(data.nodeData[nodeId][metric])) { - data.nodeData[nodeId][metric] = data.nodeData[nodeId][metric].map((point: ChartPoint) => ({ - ...point, - timestamp: point.timestamp + timeOffset - })); - } - } - } - } - - // Adjust storage data timestamps - if (data.storageData) { - for (const storageId in data.storageData) { - for (const metric in data.storageData[storageId]) { - if (Array.isArray(data.storageData[storageId][metric])) { - data.storageData[storageId][metric] = data.storageData[storageId][metric].map((point: ChartPoint) => ({ - ...point, - timestamp: point.timestamp + timeOffset - })); - } - } - } - } - - // Update store - const guestCount = Object.keys(data.data || {}).length; - const nodeCount = Object.keys(data.nodeData || {}).length; - console.debug(`Chart data fetched: ${guestCount} guests, ${nodeCount} nodes`); - - setChartStore({ - guestData: data.data || {}, - nodeData: data.nodeData || {}, - storageData: data.storageData || {}, - lastFetch: Date.now(), - timeRange, - stats: { - oldestDataTimestamp: data.stats?.oldestDataTimestamp - } - }); - - // Don't clear the entire cache - let individual entries expire naturally - // This prevents all charts from recalculating at once - // processedDataCache.clear(); - - return { guestData: data.data, nodeData: data.nodeData, storageData: data.storageData }; - } catch (error) { - logger.error('Failed to fetch chart data', error); - return null; - } -} - -// Empty array constant to avoid recreating arrays -const EMPTY_ARRAY: ChartDataPoint[] = []; - -// Get chart data for a specific guest and metric -export function getGuestChartData(guestId: string, metric: string): ChartDataPoint[] { - const guestData = chartStore.guestData[guestId]; - if (!guestData) { - console.debug(`No chart data for guest ${guestId}`); - return EMPTY_ARRAY; - } - - const metricData = guestData[metric as keyof MetricData]; - if (!metricData) { - console.debug(`No ${metric} data for guest ${guestId}`); - } - return metricData || EMPTY_ARRAY; -} - -// Get chart data for a specific node and metric -export function getNodeChartData(nodeId: string, metric: string): ChartDataPoint[] { - const nodeData = chartStore.nodeData[nodeId]; - if (!nodeData) return EMPTY_ARRAY; - - const metricData = nodeData[metric as keyof MetricData]; - return metricData || EMPTY_ARRAY; -} - -// Get chart data for a specific storage and metric -export function getStorageChartData(storageId: string, metric: string): ChartDataPoint[] { - const storageData = chartStore.storageData[storageId]; - if (!storageData) return EMPTY_ARRAY; - - const metricData = storageData[metric as keyof MetricData]; - return metricData || EMPTY_ARRAY; -} - -// Check if we should fetch new data -export function shouldFetchChartData(): boolean { - const CHART_FETCH_INTERVAL = 5000; // 5 seconds - return !chartStore.lastFetch || (Date.now() - chartStore.lastFetch) > CHART_FETCH_INTERVAL; -} - -// Format time ago for tooltips -export function formatTimeAgo(timestamp: number): string { - const now = Date.now(); - const diffMs = now - timestamp; - - if (diffMs < 0) return '0s ago'; - - const diffMinutes = Math.floor(diffMs / 60000); - const diffSeconds = Math.floor((diffMs % 60000) / 1000); - - if (diffMinutes >= 60) { - const hours = Math.floor(diffMinutes / 60); - const minutes = diffMinutes % 60; - - if (hours >= 24) { - const days = Math.floor(hours / 24); - const remainingHours = hours % 24; - - if (remainingHours > 0) { - return `${days}d ${remainingHours}h ${minutes}m ago`; - } else if (minutes > 0) { - return `${days}d ${minutes}m ago`; - } else { - return `${days}d ago`; - } - } - - return `${hours}h ${minutes}m ago`; - } else if (diffMinutes > 0) { - return `${diffMinutes}m ${diffSeconds}s ago`; - } else { - return `${diffSeconds}s ago`; - } -} - -// Format value for tooltip display -export function formatChartValue(value: number, metric: string): string { - if (metric === 'cpu' || metric === 'memory' || metric === 'disk') { - return Math.round(value) + '%'; - } else if (metric === 'diskread' || metric === 'diskwrite' || metric === 'netin' || metric === 'netout') { - // Format as speed - const mbps = value / (1024 * 1024); - if (mbps >= 100) return `${Math.round(mbps)} MB/s`; - if (mbps >= 10) return `${Math.round(mbps * 10) / 10} MB/s`; - if (mbps >= 1) return `${Math.round(mbps * 100) / 100} MB/s`; - - const kbps = value / 1024; - if (kbps >= 1) return `${Math.round(kbps)} KB/s`; - - return `${Math.round(value)} B/s`; - } else { - // Default formatting - return String(Math.round(value)); - } -} - -// Export store and utilities -export { chartStore, renderPoints }; \ No newline at end of file diff --git a/frontend-modern/src/types/charts.ts b/frontend-modern/src/types/charts.ts deleted file mode 100644 index ddb08a06a..000000000 --- a/frontend-modern/src/types/charts.ts +++ /dev/null @@ -1,43 +0,0 @@ -export interface ChartPoint { - timestamp: number; - value: number; -} - -export interface MetricData { - cpu: ChartPoint[]; - memory: ChartPoint[]; - disk: ChartPoint[]; - diskread: ChartPoint[]; - diskwrite: ChartPoint[]; - netin: ChartPoint[]; - netout: ChartPoint[]; - [key: string]: ChartPoint[]; -} - -export interface ChartData { - [guestId: string]: MetricData; -} - -export interface NodeMetricData { - cpu: ChartPoint[]; - memory: ChartPoint[]; - disk: ChartPoint[]; - [key: string]: ChartPoint[]; -} - -export interface NodeChartData { - [nodeId: string]: NodeMetricData; -} - -export interface StorageMetricData { - disk: ChartPoint[]; - [key: string]: ChartPoint[]; -} - -export interface StorageChartData { - [storageId: string]: StorageMetricData; -} - -export interface ChartStats { - oldestDataTimestamp: number; -} \ No newline at end of file