diff --git a/frontend-modern/src/components/Storage/EnhancedStorageBar.tsx b/frontend-modern/src/components/Storage/EnhancedStorageBar.tsx new file mode 100644 index 0000000..4e7e4c5 --- /dev/null +++ b/frontend-modern/src/components/Storage/EnhancedStorageBar.tsx @@ -0,0 +1,146 @@ +import { Show, createMemo, createSignal } from 'solid-js'; +import { Portal } from 'solid-js/web'; +import { formatBytes, formatPercent } from '@/utils/format'; +import type { ZFSPool } from '@/types/api'; + +interface EnhancedStorageBarProps { + used: number; + total: number; + free: number; + zfsPool?: ZFSPool; +} + +export function EnhancedStorageBar(props: EnhancedStorageBarProps) { + const [showTooltip, setShowTooltip] = createSignal(false); + const [tooltipPos, setTooltipPos] = createSignal({ x: 0, y: 0 }); + let containerRef: HTMLDivElement | undefined; + + const usagePercent = createMemo(() => { + if (props.total <= 0) return 0; + return (props.used / props.total) * 100; + }); + + const barColor = createMemo(() => { + const p = usagePercent(); + if (p >= 90) return 'bg-red-500/60 dark:bg-red-500/50'; + if (p >= 80) return 'bg-yellow-500/60 dark:bg-yellow-500/50'; + return 'bg-green-500/60 dark:bg-green-500/50'; + }); + + const isScrubbing = createMemo(() => { + return props.zfsPool?.scan?.toLowerCase().includes('scrub') ?? false; + }); + + const isResilvering = createMemo(() => { + return props.zfsPool?.scan?.toLowerCase().includes('resilver') ?? false; + }); + + const hasErrors = createMemo(() => { + if (!props.zfsPool) return false; + return ( + props.zfsPool.readErrors > 0 || + props.zfsPool.writeErrors > 0 || + props.zfsPool.checksumErrors > 0 + ); + }); + + const handleMouseEnter = (e: MouseEvent) => { + const rect = (e.currentTarget as HTMLElement).getBoundingClientRect(); + setTooltipPos({ x: rect.left + rect.width / 2, y: rect.top }); + setShowTooltip(true); + }; + + const handleMouseLeave = () => { + setShowTooltip(false); + }; + + return ( +
+
+ {/* Usage Bar */} +
+ + {/* Scrubbing/Resilvering Animation Overlay */} + +
+ + + {/* Error Indicator (Red border/glow) */} + +
+ + + {/* Label */} + + + {formatPercent(usagePercent())} ( + {formatBytes(props.used, 0)}/ + {formatBytes(props.total, 0)}) + + +
+ + {/* Tooltip */} + + +
+
+
+ Storage Details +
+ +
+ Used + {formatBytes(props.used)} +
+
+ Free + {formatBytes(props.free)} +
+
+ Total + {formatBytes(props.total)} +
+ + +
+
ZFS Status
+
+ State + + {props.zfsPool?.state} + +
+ +
+ {props.zfsPool?.scan} +
+
+ +
+ Errors: R:{props.zfsPool?.readErrors} W:{props.zfsPool?.writeErrors} C:{props.zfsPool?.checksumErrors} +
+
+
+
+
+
+
+
+
+ ); +} diff --git a/frontend-modern/src/components/Storage/Storage.tsx b/frontend-modern/src/components/Storage/Storage.tsx index d4f46c3..0f83a93 100644 --- a/frontend-modern/src/components/Storage/Storage.tsx +++ b/frontend-modern/src/components/Storage/Storage.tsx @@ -9,6 +9,7 @@ import { UnifiedNodeSelector } from '@/components/shared/UnifiedNodeSelector'; import { StorageFilter } from './StorageFilter'; import { DiskList } from './DiskList'; import { ZFSHealthMap } from './ZFSHealthMap'; +import { EnhancedStorageBar } from './EnhancedStorageBar'; import { Card } from '@/components/shared/Card'; import { EmptyState } from '@/components/shared/EmptyState'; import { NodeGroupHeader } from '@/components/shared/NodeGroupHeader'; @@ -485,12 +486,7 @@ const Storage: Component = () => { } }); - const getProgressBarColor = (usage: number) => { - // Match MetricBar component styling exactly - disk type thresholds - 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'; - }; + const resetFilters = () => { setSearchTerm(''); @@ -1148,19 +1144,12 @@ const Storage: Component = () => {
-
-
- - - {formatPercent(usagePercent)} ( - {formatBytes(storage.used || 0, 0)}/ - {formatBytes(storage.total || 0, 0)}) - - -
+ {formatBytes(storage.free || 0, 0)}