From 4083df1f47891709d8beb429322c26c164aea06c Mon Sep 17 00:00:00 2001 From: rcourtman Date: Thu, 25 Dec 2025 22:32:56 +0000 Subject: [PATCH] feat(mobile): optimize tables for mobile devices - Implement horizontal scrolling for key tables (Dashboard, Storage, Backups, Docker, Node Summary). - Ensure consistent column widths (140px fixed on desktop, 60px min on mobile) for metric columns (CPU, Memory, Disk). - Switch Backups view from card to scrollable table for consistency. - Reduce shell padding on mobile. - Update ResourceTable (Alerts) with dedicated mobile view. - Refine column visibility logic. --- .../src/components/Alerts/ResourceTable.tsx | 2553 +++++++++-------- .../src/components/Backups/UnifiedBackups.tsx | 127 +- .../src/components/Dashboard/Dashboard.tsx | 14 +- .../src/components/Dashboard/GuestRow.tsx | 140 +- .../Docker/DockerHostSummaryTable.tsx | 56 +- .../components/Docker/DockerUnifiedTable.tsx | 31 +- .../src/components/Storage/Storage.tsx | 22 +- .../components/shared/NodeSummaryTable.tsx | 37 +- .../src/hooks/useColumnVisibility.ts | 2 +- frontend-modern/src/index.css | 2 +- 10 files changed, 1532 insertions(+), 1452 deletions(-) diff --git a/frontend-modern/src/components/Alerts/ResourceTable.tsx b/frontend-modern/src/components/Alerts/ResourceTable.tsx index f937862243..7fc84d045f 100644 --- a/frontend-modern/src/components/Alerts/ResourceTable.tsx +++ b/frontend-modern/src/components/Alerts/ResourceTable.tsx @@ -1,4 +1,5 @@ import { For, Show, createSignal, createEffect } from 'solid-js'; +import { useBreakpoint } from '@/hooks/useBreakpoint'; import { TogglePrimitive } from '@/components/shared/Toggle'; import { StatusBadge } from '@/components/shared/StatusBadge'; import type { Alert } from '@/types/api'; @@ -149,6 +150,8 @@ interface ResourceTableProps { type OfflineState = 'off' | 'warning' | 'critical'; export function ResourceTable(props: ResourceTableProps) { + const { isMobile } = useBreakpoint(); + const flattenResources = (): Resource[] => { if (props.groupedResources) { return Object.values(props.groupedResources).flat(); @@ -478,490 +481,1208 @@ export function ResourceTable(props: ResourceTableProps) { ); }; + const renderMobileView = () => ( +
+ {/* Global Defaults - Mobile Card */} + + +
+
+ Global Defaults + + Custom + +
+ + { props.onToggleGlobalDisable?.(); props.setHasUnsavedChanges?.(true); }} /> + +
+ +
+ + {(column) => { + const metric = normalizeMetricKey(column); + const bounds = metricBounds(metric); + const val = () => props.globalDefaults?.[metric] ?? 0; + const isOff = () => val() === -1; + return ( +
+ {column} +
+ { + const value = parseFloat(e.currentTarget.value); + if (props.setGlobalDefaults) { + props.setGlobalDefaults((prev) => ({ ...prev, [metric]: Number.isNaN(value) ? 0 : value })); + } + if (props.setHasUnsavedChanges) props.setHasUnsavedChanges(true); + }} + /> + + + +
+
+ ) + }} +
+
+
+
+ + a.localeCompare(b)) + : [['default', props.resources || []]] + } + > + {([groupName, resources]) => ( +
+ +
{renderGroupHeader(groupName, props.groupHeaderMeta?.[groupName])}
+
+ + {(resource) => { + const isEditing = () => props.editingId() === resource.id; + const thresholds = () => isEditing() ? props.editingThresholds() : (resource.thresholds ?? {}); + + // Helper logic duplicated/adapted for scope access + const displayValue = (metric: string): number => { + const parseNumeric = (value: unknown): number | undefined => { + if (value === undefined || value === null) return undefined; + if (typeof value === 'number') return value; + const parsed = Number(value); + return Number.isFinite(parsed) ? parsed : undefined; + }; + const extract = (source: Record | undefined) => parseNumeric(source?.[metric]); + const defaults = resource.defaults as Record | undefined; + + if (isEditing()) { + const edited = extract(thresholds() as Record); + if (edited !== undefined) return edited; + const fallback = extract(defaults); + return fallback !== undefined ? fallback : 0; + } + + const liveValue = extract(resource.thresholds as Record | undefined); + if (liveValue !== undefined) return liveValue; + const fallback = extract(defaults); + return fallback !== undefined ? fallback : 0; + }; + + const isOverridden = (metric: string) => resource.thresholds?.[metric] !== undefined && resource.thresholds?.[metric] !== null; + + return ( + + {/* Header */} +
+
+ {/* Toggle */} + +
+ !(props.globalDisableFlag?.() ?? false) && props.onToggleDisabled?.(resource.id)} + /> +
+
+ + {/* Name */} +
+
{resource.displayName || resource.name}
+
{resource.subtitle}
+
+
+ + {/* Actions */} +
+ + + + + + + + + + +
+
+ + {/* Note editor in mobile */} + +