diff --git a/frontend-modern/browser-verification.json b/frontend-modern/browser-verification.json index 9d6341fda6..e2632dcb53 100644 --- a/frontend-modern/browser-verification.json +++ b/frontend-modern/browser-verification.json @@ -1,29 +1,54 @@ { "version": 1, - "base_sha": "2bfab3610e499abafdb416ff4eb1b57a26f01bca", - "verified_at": "2026-08-04T09:22:02Z", + "base_sha": "e4bc063eb0a070484a0e22c9d6da20fb318df3d5", + "verified_at": "2026-08-04T09:43:18Z", "result": "passed", - "changed_paths": ["frontend-modern/src/components/Workloads/workloadSelectors.ts"], + "changed_paths": [ + "frontend-modern/src/components/Storage/DiskList.tsx", + "frontend-modern/src/components/Storage/StoragePoolRow.tsx", + "frontend-modern/src/components/Storage/StoragePoolsTable.tsx", + "frontend-modern/src/features/storageBackups/diskPresentation.ts", + "frontend-modern/src/features/storageBackups/storagePagePresentation.ts", + "frontend-modern/src/features/storageBackups/storagePoolRowPresentation.ts" + ], "content_sha256": { - "frontend-modern/src/components/Workloads/workloadSelectors.ts": "f7d8e836a655443cbbc64e8f831e98047b1fe713db2a908fb84f1a3585ceb0e6" + "frontend-modern/src/components/Storage/DiskList.tsx": "496706e7748774b05f04a8299d3f375d2acd51c421d2c6837d383ca1984c55db", + "frontend-modern/src/components/Storage/StoragePoolRow.tsx": "624de18ccae7fa7b082dac65169608145cad96a3bfe3c488ca722eb94e479e5a", + "frontend-modern/src/components/Storage/StoragePoolsTable.tsx": "30a7f26a69e36191a55034e220a848707cdc305b9b51e4b30edbd45552b0d500", + "frontend-modern/src/features/storageBackups/diskPresentation.ts": "3d0403057a70aeea76ac0aa153a4ed41557978547bf8267386f3e51e49f44253", + "frontend-modern/src/features/storageBackups/storagePagePresentation.ts": "8a0d6d574aa51147f673c299455310e69b709b648a68f127897b51715dd9c92e", + "frontend-modern/src/features/storageBackups/storagePoolRowPresentation.ts": "3f84b93a3ef161ccde6195d9970cbfcdcdb4083819d72ae01d5d9e3164528bb3" }, - "routes": ["/proxmox/overview"], + "routes": ["/proxmox/storage", "/proxmox/storage?tab=disks&source=proxmox-all"], "viewports": [ + { "width": 375, "height": 900 }, + { "width": 480, "height": 900 }, + { "width": 640, "height": 900 }, + { "width": 768, "height": 900 }, + { "width": 800, "height": 900 }, + { "width": 900, "height": 900 }, + { "width": 1024, "height": 900 }, + { "width": 1280, "height": 900 }, { "width": 1440, "height": 900 }, - { "width": 390, "height": 844 } + { "width": 1536, "height": 900 }, + { "width": 1680, "height": 900 }, + { "width": 1920, "height": 900 }, + { "width": 2560, "height": 900 } ], "states": [ - "Proxmox Overview against the live backend with 42 grouped workload rows and 36 attached availability checks, none of them currently failing", - "Attention status filter applied, returning the canonical no-guests-match empty state because no guest is degraded", - "Filters cleared back to All with the full workload inventory restored", - "Narrow 390px layout with the unfiltered workload table" + "Storage pool table in compact, operational, and full container-width layouts", + "Physical Disks table in compact, basic, operational, expanded, and full container-width layouts", + "Pulse Assistant open at 1280, 1536, and 1920 pixel viewports, reducing the real table container independently of the viewport", + "Expanded storage-pool capacity and configuration detail at desktop, Assistant-constrained, and 375 pixel widths", + "Expanded physical-disk SMART and live-I/O detail at desktop, operational, and 375 pixel widths", + "Filtered pool and disk inventories, sorted pool inventory, and restored default filters and ordering" ], "interactions": [ - "Read the live resource payload and confirmed 36 availability checks are correlated as attached with zero currently unavailable, so the new degraded bucket introduces no false positives on a healthy fleet", - "Applied the Attention status filter and confirmed it still resolves to the empty state rather than pulling in healthy guests", - "Cleared filters and confirmed all 42 workload rows returned", - "Resized to 390x844 and confirmed the unfiltered table renders with no horizontal document overflow", - "Read console messages filtered to errors across both viewports and found none", - "The failing-probe path itself is proven by unit test rather than in the browser because no attached check is currently failing on this instance; the new cases in workloadSelectors.test.ts were confirmed red with the degradation branch disabled and green with it restored" + "Switched repeatedly between Storage and Physical Disks using the real tab controls", + "Opened and closed Pulse Assistant and confirmed ResizeObserver-driven column stages update without reload or horizontal overflow", + "Expanded and collapsed pool and physical-disk rows and inspected their inline detail layouts", + "Sorted by Usage through the table header, inspected View preferences, and restored Priority ordering", + "Applied the pbs search at compact width, verified filtered results, and cleared the filter through the UI", + "Measured table client width, scroll width, visible headers, and document overflow across all listed viewports; every tested default and constrained state had zero table and document overflow" ] } diff --git a/frontend-modern/src/components/Storage/DiskList.tsx b/frontend-modern/src/components/Storage/DiskList.tsx index a27227f7c2..93a48922ce 100644 --- a/frontend-modern/src/components/Storage/DiskList.tsx +++ b/frontend-modern/src/components/Storage/DiskList.tsx @@ -1,4 +1,4 @@ -import { Component, For, Show } from 'solid-js'; +import { Component, For, Show, createMemo } from 'solid-js'; import { buildSummaryDisclosureControlsId, createSummaryInteractiveRowPreviewHandlers, @@ -72,6 +72,7 @@ import { PHYSICAL_DISK_TABLE_ROW_HOVER_CLASS, PHYSICAL_DISK_TABLE_ROW_SELECTED_CLASS, PHYSICAL_DISK_TABLE_ROW_STYLE, + getPhysicalDiskColumnWidthPercent, getPhysicalDiskEmptyStatePresentation, getPhysicalDiskHealthStatus, getPhysicalDiskHealthSummary, @@ -80,7 +81,11 @@ import { getPhysicalDiskLifeTextClass, getPhysicalDiskParentLabel, getPhysicalDiskRoleLabel, + getPhysicalDiskTableLayoutModeForContainer, + isPhysicalDiskColumnVisible, + type PhysicalDiskTableColumnId, } from '@/features/storageBackups/diskPresentation'; +import { useObservedElementWidth } from '@/hooks/useObservedElementWidth'; import type { Resource } from '@/types/resource'; import type { StorageHealthFilter } from '@/features/storageBackups/models'; import { getStorageSourceOption } from '@/utils/storageSources'; @@ -103,6 +108,19 @@ interface DiskListProps { } export const DiskList: Component = (props) => { + const tableWidth = useObservedElementWidth(); + const layoutMode = createMemo(() => + getPhysicalDiskTableLayoutModeForContainer(tableWidth.width() ?? 0), + ); + const columnClass = ( + baseClass: string, + columnId: PhysicalDiskTableColumnId, + visibleClass: 'table-column' | 'table-cell', + ) => + `${baseClass} ${isPhysicalDiskColumnVisible(layoutMode(), columnId) ? visibleClass : 'hidden'}`.trim(); + const columnStyle = (columnId: PhysicalDiskTableColumnId) => ({ + width: `${getPhysicalDiskColumnWidthPercent(layoutMode(), columnId)}%`, + }); const { getDiskTemperatureThresholds } = useAlertsActivation(); const model = useDiskListModel({ disks: () => props.disks, @@ -183,41 +201,119 @@ export const DiskList: Component = (props) => { 0}> - +
- - - - - - - - - + + + + + + + + + - Disk - Device - Host - Role + Disk + + + Device + + + Host + + + Role + + Belongs - Health + Health + + Life - Temp - Size + + Temp + + + Size + @@ -254,7 +350,10 @@ export const DiskList: Component = (props) => { onClick={() => model.toggleSelectedDisk(disk)} {...interactiveRowHandlers} > - +
= (props) => {
- + —} @@ -284,7 +386,10 @@ export const DiskList: Component = (props) => { - + —} @@ -295,7 +400,10 @@ export const DiskList: Component = (props) => { - + —} @@ -309,7 +417,10 @@ export const DiskList: Component = (props) => { - + —} @@ -323,7 +434,10 @@ export const DiskList: Component = (props) => { - +
{status.label} @@ -336,7 +450,10 @@ export const DiskList: Component = (props) => {
- + —} @@ -349,7 +466,10 @@ export const DiskList: Component = (props) => { - + 0} fallback={—} @@ -366,7 +486,10 @@ export const DiskList: Component = (props) => { - + 0} fallback={ diff --git a/frontend-modern/src/components/Storage/StoragePoolRow.tsx b/frontend-modern/src/components/Storage/StoragePoolRow.tsx index d7c6dd5002..34e665325f 100644 --- a/frontend-modern/src/components/Storage/StoragePoolRow.tsx +++ b/frontend-modern/src/components/Storage/StoragePoolRow.tsx @@ -25,6 +25,11 @@ import { } from '@/features/storageBackups/storagePoolRowPresentation'; import type { StorageCapacityDeltaPresentation } from '@/features/storageBackups/storageCapacityDeltaPresentation'; import type { Resource } from '@/types/resource'; +import { + isStoragePoolColumnVisible, + type StoragePoolTableColumnId, + type StoragePoolTableLayoutMode, +} from '@/features/storageBackups/storagePagePresentation'; import type { SummaryGroupMemberInteractionState } from '@/components/shared/summaryCardInteraction'; import { EnhancedStorageBar } from './EnhancedStorageBar'; import { StoragePoolDetail } from './StoragePoolDetail'; @@ -35,6 +40,7 @@ import { import { SummaryRowActionButton } from '@/components/shared/SummaryRowActionButton'; interface StoragePoolRowProps { + layoutMode: StoragePoolTableLayoutMode; record: StorageRecord; growthDelta?: StorageCapacityDeltaPresentation | null; summarySeriesId: string; @@ -54,6 +60,8 @@ interface StoragePoolRowProps { } export const StoragePoolRow: Component = (props) => { + const columnClass = (baseClass: string, columnId: StoragePoolTableColumnId) => + `${baseClass} ${isStoragePoolColumnVisible(props.layoutMode, columnId) ? 'table-cell' : 'hidden'}`.trim(); const row = createMemo(() => buildStoragePoolRowModel(props.record, props.growthDelta ?? null)); const detailControlsId = createMemo(() => buildSummaryDisclosureControlsId(props.summarySeriesId), @@ -78,7 +86,10 @@ export const StoragePoolRow: Component = (props) => { data-summary-row-active={props.summaryHighlighted && !props.expanded ? 'true' : 'false'} {...props.alertDataAttrs} > -
- - - - - -
+
= (props) => {
+ —} @@ -108,19 +122,28 @@ export const StoragePoolRow: Component = (props) => { + {row().topologyLabel} + {row().hostLabel} + —} @@ -134,7 +157,10 @@ export const StoragePoolRow: Component = (props) => { + 0} fallback={n/a} @@ -152,7 +178,10 @@ export const StoragePoolRow: Component = (props) => { + = (props) => { + const tableWidth = useObservedElementWidth(); + const layoutMode = createMemo(() => + getStoragePoolTableLayoutModeForContainer(tableWidth.width() ?? 0), + ); + const columnClass = ( + baseClass: string, + columnId: Parameters[1], + visibleClass: 'table-column' | 'table-cell', + ) => + `${baseClass} ${isStoragePoolColumnVisible(layoutMode(), columnId) ? visibleClass : 'hidden'}`.trim(); const model = useStoragePoolsTableModel({ groupedRecords: () => props.groupedRecords, groupBy: () => props.groupBy, @@ -107,10 +121,23 @@ export const StoragePoolsTable: Component = (props) => {
{getStorageEmptyStateMessage()}
} > - +
- {(column) => } + {(column) => ( + + )} @@ -118,7 +145,8 @@ export const StoragePoolsTable: Component = (props) => { {(column) => ( = (props) => { props.sortDirection, )} > - - {column.compactLabel} + {column.compactLabel}} + > + {column.label} + = (props) => { return ( { }); expect(screen.getByRole('columnheader', { name: 'Disk' })).toBeInTheDocument(); + expect(screen.getByRole('table')).toHaveAttribute('data-storage-table', 'physical-disks'); + expect(screen.getByRole('table')).toHaveAttribute('data-storage-layout', 'compact'); expect(screen.getByRole('columnheader', { name: 'Device' })).toBeInTheDocument(); + expect(screen.getByRole('columnheader', { name: 'Device' })).toHaveAttribute( + 'data-storage-column', + 'device', + ); expect(screen.getByRole('columnheader', { name: 'Host' })).toBeInTheDocument(); expect(screen.getByRole('columnheader', { name: 'SSD life remaining' })).toBeInTheDocument(); expect(screen.queryByRole('columnheader', { name: 'Source' })).not.toBeInTheDocument(); diff --git a/frontend-modern/src/components/Storage/__tests__/Storage.test.tsx b/frontend-modern/src/components/Storage/__tests__/Storage.test.tsx index e02d5c685b..f269aa87a5 100644 --- a/frontend-modern/src/components/Storage/__tests__/Storage.test.tsx +++ b/frontend-modern/src/components/Storage/__tests__/Storage.test.tsx @@ -529,7 +529,7 @@ describe('Storage', () => { render(() => ); - expect(await screen.findByText('Growth (24h)')).toBeInTheDocument(); + expect(await screen.findByRole('columnheader', { name: 'Growth (24h)' })).toBeInTheDocument(); const storageContentSurface = screen.getByTestId('storage-content-surface'); expect(await within(storageContentSurface).findByText(/\+40(?:\.0)? GB/)).toBeInTheDocument(); expect(within(storageContentSurface).getByText(/-20(?:\.0)? GB/)).toBeInTheDocument(); @@ -575,6 +575,8 @@ describe('Storage', () => { render(() => ); + expect(screen.getByRole('table')).toHaveAttribute('data-storage-table', 'pools'); + expect(screen.getByRole('table')).toHaveAttribute('data-storage-layout', 'compact'); expect(screen.getByRole('columnheader', { name: 'Storage' })).toBeInTheDocument(); expect(screen.queryByRole('columnheader', { name: 'Source' })).not.toBeInTheDocument(); expect(screen.getByRole('columnheader', { name: 'Protection' })).toBeInTheDocument(); diff --git a/frontend-modern/src/features/storageBackups/__tests__/diskPresentation.test.ts b/frontend-modern/src/features/storageBackups/__tests__/diskPresentation.test.ts index 0ac5664be9..6772946734 100644 --- a/frontend-modern/src/features/storageBackups/__tests__/diskPresentation.test.ts +++ b/frontend-modern/src/features/storageBackups/__tests__/diskPresentation.test.ts @@ -19,6 +19,7 @@ import { PHYSICAL_DISK_TABLE_CLASS, PHYSICAL_DISK_TABLE_ROW_HOVER_CLASS, getPhysicalDiskEmptyStatePresentation, + getPhysicalDiskColumnWidthPercent, getPhysicalDiskCollectionMessages, getPhysicalDiskFieldStatusMessage, getPhysicalDiskHealthStatus, @@ -33,9 +34,11 @@ import { getPhysicalDiskRoleFilterValue, getPhysicalDiskSourceKey, getPhysicalDiskSourceBadgePresentation, + getPhysicalDiskTableLayoutModeForContainer, hasUnraidPhysicalDiskFaultSignal, hasPhysicalDiskSmartWarning, isPhysicalDiskWearoutReported, + isPhysicalDiskColumnVisible, isUnraidPhysicalDisk, matchesPhysicalDiskSearch, normalizePhysicalDiskFacetFilter, @@ -111,8 +114,8 @@ describe('diskPresentation', () => { expect(PHYSICAL_DISK_TABLE_CLASS).toBe('w-full table-fixed text-xs'); expect(PHYSICAL_DISK_TABLE_ROW_HOVER_CLASS).toContain('hover:bg-surface-hover'); expect(PHYSICAL_DISK_HEADER_DISK_CLASS).toContain('uppercase'); - expect(PHYSICAL_DISK_HEADER_DEVICE_CLASS).toContain('md:table-cell'); - expect(PHYSICAL_DISK_HEADER_LIFE_CLASS).toContain('md:table-cell'); + expect(PHYSICAL_DISK_HEADER_DEVICE_CLASS).toContain('uppercase'); + expect(PHYSICAL_DISK_HEADER_LIFE_CLASS).toContain('uppercase'); expect(PHYSICAL_DISK_NAME_TEXT_CLASS).toContain('font-semibold'); expect(PHYSICAL_DISK_SOURCE_BADGE_CLASS).toContain('justify-center'); @@ -131,6 +134,23 @@ describe('diskPresentation', () => { }); }); + it('selects operator-priority disk layouts from the rendered table width', () => { + expect(getPhysicalDiskTableLayoutModeForContainer(519)).toBe('compact'); + expect(getPhysicalDiskTableLayoutModeForContainer(520)).toBe('basic'); + expect(getPhysicalDiskTableLayoutModeForContainer(649)).toBe('basic'); + expect(getPhysicalDiskTableLayoutModeForContainer(650)).toBe('operational'); + expect(getPhysicalDiskTableLayoutModeForContainer(899)).toBe('operational'); + expect(getPhysicalDiskTableLayoutModeForContainer(900)).toBe('expanded'); + expect(getPhysicalDiskTableLayoutModeForContainer(1_119)).toBe('expanded'); + expect(getPhysicalDiskTableLayoutModeForContainer(1_120)).toBe('full'); + expect(isPhysicalDiskColumnVisible('operational', 'parent')).toBe(true); + expect(isPhysicalDiskColumnVisible('operational', 'device')).toBe(false); + expect(isPhysicalDiskColumnVisible('expanded', 'role')).toBe(true); + expect(isPhysicalDiskColumnVisible('expanded', 'device')).toBe(false); + expect(getPhysicalDiskColumnWidthPercent('operational', 'life')).toBe(9); + expect(getPhysicalDiskColumnWidthPercent('compact', 'device')).toBe(0); + }); + it('detects SMART warnings from counters', () => { expect( hasPhysicalDiskSmartWarning({ diff --git a/frontend-modern/src/features/storageBackups/__tests__/storagePagePresentation.test.ts b/frontend-modern/src/features/storageBackups/__tests__/storagePagePresentation.test.ts index 325f413044..a84a27a509 100644 --- a/frontend-modern/src/features/storageBackups/__tests__/storagePagePresentation.test.ts +++ b/frontend-modern/src/features/storageBackups/__tests__/storagePagePresentation.test.ts @@ -1,7 +1,10 @@ import { describe, expect, it } from 'vitest'; import { + getStoragePoolColumnWidthPercent, getStoragePoolTableColumns, + getStoragePoolTableLayoutModeForContainer, getStorageTableHeading, + isStoragePoolColumnVisible, STORAGE_VIEW_OPTIONS, } from '@/features/storageBackups/storagePagePresentation'; @@ -28,9 +31,25 @@ describe('storagePagePresentation', () => { expect(getStoragePoolTableColumns('Growth (24h)').map((column) => column.compactLabel)).toEqual( ['Storage', 'State', 'Type', 'Host', 'Prot', 'Used', '24h'], ); - expect(getStoragePoolTableColumns('Growth (24h)')[0].colClassName).toContain('xl:w-[20%]'); - expect(getStoragePoolTableColumns('Growth (24h)')[2].className).toContain( - 'hidden xl:table-cell', - ); + expect(getStoragePoolTableColumns('Growth (24h)').map((column) => column.id)).toEqual([ + 'name', + 'state', + 'type', + 'host', + 'protection', + 'usage', + 'growth', + ]); + }); + + it('selects pool layouts from the rendered table width', () => { + expect(getStoragePoolTableLayoutModeForContainer(559)).toBe('compact'); + expect(getStoragePoolTableLayoutModeForContainer(560)).toBe('operational'); + expect(getStoragePoolTableLayoutModeForContainer(1_039)).toBe('operational'); + expect(getStoragePoolTableLayoutModeForContainer(1_040)).toBe('full'); + expect(isStoragePoolColumnVisible('operational', 'host')).toBe(true); + expect(isStoragePoolColumnVisible('operational', 'growth')).toBe(false); + expect(getStoragePoolColumnWidthPercent('operational', 'usage')).toBe(21); + expect(getStoragePoolColumnWidthPercent('compact', 'host')).toBe(0); }); }); diff --git a/frontend-modern/src/features/storageBackups/diskPresentation.ts b/frontend-modern/src/features/storageBackups/diskPresentation.ts index 973dc7ee27..a71825fa12 100644 --- a/frontend-modern/src/features/storageBackups/diskPresentation.ts +++ b/frontend-modern/src/features/storageBackups/diskPresentation.ts @@ -107,23 +107,78 @@ export const PHYSICAL_DISK_TABLE_ROW_HOVER_CLASS = 'hover:bg-surface-hover'; export const PHYSICAL_DISK_TABLE_ROW_STYLE = { height: '32px' } as const; export const PHYSICAL_DISK_DETAIL_ROW_CELL_CLASS = 'border-b border-border-subtle bg-surface-alt px-4 py-4 shadow-inner'; -export const PHYSICAL_DISK_COL_DISK_CLASS = 'w-[47%] sm:w-[31%] md:w-[24%] xl:w-[19%]'; -export const PHYSICAL_DISK_COL_DEVICE_CLASS = 'hidden md:table-column md:w-[11%] xl:w-[9%]'; -export const PHYSICAL_DISK_COL_HOST_CLASS = - 'hidden sm:table-column sm:w-[20%] md:w-[12%] xl:w-[10%]'; -export const PHYSICAL_DISK_COL_ROLE_CLASS = 'hidden xl:table-column xl:w-[8%]'; -export const PHYSICAL_DISK_COL_PARENT_CLASS = 'hidden xl:table-column xl:w-[12%]'; -export const PHYSICAL_DISK_COL_HEALTH_CLASS = 'w-[29%] sm:w-[27%] md:w-[19%] xl:w-[17%]'; -export const PHYSICAL_DISK_COL_LIFE_CLASS = 'hidden md:table-column md:w-[7%] xl:w-[6%]'; -export const PHYSICAL_DISK_COL_TEMP_CLASS = 'hidden md:table-column md:w-[8%] xl:w-[7%]'; -export const PHYSICAL_DISK_COL_SIZE_CLASS = 'w-[24%] sm:w-[22%] md:w-[19%] xl:w-[12%]'; -const PHYSICAL_DISK_CELL_DEVICE_RESPONSIVE_CLASS = 'hidden md:table-cell md:w-[11%] xl:w-[9%]'; -const PHYSICAL_DISK_CELL_HOST_RESPONSIVE_CLASS = - 'hidden sm:table-cell sm:w-[20%] md:w-[12%] xl:w-[10%]'; -const PHYSICAL_DISK_CELL_ROLE_RESPONSIVE_CLASS = 'hidden xl:table-cell xl:w-[8%]'; -const PHYSICAL_DISK_CELL_PARENT_RESPONSIVE_CLASS = 'hidden xl:table-cell xl:w-[12%]'; -const PHYSICAL_DISK_CELL_LIFE_RESPONSIVE_CLASS = 'hidden md:table-cell md:w-[7%] xl:w-[6%]'; -const PHYSICAL_DISK_CELL_TEMP_RESPONSIVE_CLASS = 'hidden md:table-cell md:w-[8%] xl:w-[7%]'; +export type PhysicalDiskTableLayoutMode = 'compact' | 'basic' | 'operational' | 'expanded' | 'full'; + +export const getPhysicalDiskTableLayoutModeForContainer = ( + containerWidth: number, +): PhysicalDiskTableLayoutMode => { + if (containerWidth >= 1_120) return 'full'; + if (containerWidth >= 900) return 'expanded'; + if (containerWidth >= 650) return 'operational'; + if (containerWidth >= 520) return 'basic'; + return 'compact'; +}; + +export type PhysicalDiskTableColumnId = + 'disk' | 'device' | 'host' | 'role' | 'parent' | 'health' | 'life' | 'temp' | 'size'; + +const PHYSICAL_DISK_VISIBLE_COLUMNS: Record< + PhysicalDiskTableLayoutMode, + readonly PhysicalDiskTableColumnId[] +> = { + compact: ['disk', 'health', 'size'], + basic: ['disk', 'host', 'health', 'temp', 'size'], + operational: ['disk', 'host', 'parent', 'health', 'life', 'temp', 'size'], + expanded: ['disk', 'host', 'role', 'parent', 'health', 'life', 'temp', 'size'], + full: ['disk', 'device', 'host', 'role', 'parent', 'health', 'life', 'temp', 'size'], +}; + +const PHYSICAL_DISK_COLUMN_WIDTHS: Record< + PhysicalDiskTableLayoutMode, + Partial> +> = { + compact: { disk: 47, health: 29, size: 24 }, + basic: { disk: 33, host: 17, health: 22, temp: 12, size: 16 }, + operational: { disk: 25, host: 12, parent: 15, health: 17, life: 9, temp: 9, size: 13 }, + expanded: { disk: 22, host: 10, role: 9, parent: 14, health: 15, life: 8, temp: 8, size: 14 }, + full: { + disk: 19, + device: 9, + host: 10, + role: 8, + parent: 12, + health: 17, + life: 6, + temp: 7, + size: 12, + }, +}; + +export const isPhysicalDiskColumnVisible = ( + layout: PhysicalDiskTableLayoutMode, + columnId: PhysicalDiskTableColumnId, +): boolean => PHYSICAL_DISK_VISIBLE_COLUMNS[layout].includes(columnId); + +export const getPhysicalDiskColumnWidthPercent = ( + layout: PhysicalDiskTableLayoutMode, + columnId: PhysicalDiskTableColumnId, +): number => PHYSICAL_DISK_COLUMN_WIDTHS[layout][columnId] ?? 0; + +export const PHYSICAL_DISK_COL_DISK_CLASS = ''; +export const PHYSICAL_DISK_COL_DEVICE_CLASS = ''; +export const PHYSICAL_DISK_COL_HOST_CLASS = ''; +export const PHYSICAL_DISK_COL_ROLE_CLASS = ''; +export const PHYSICAL_DISK_COL_PARENT_CLASS = ''; +export const PHYSICAL_DISK_COL_HEALTH_CLASS = ''; +export const PHYSICAL_DISK_COL_LIFE_CLASS = ''; +export const PHYSICAL_DISK_COL_TEMP_CLASS = ''; +export const PHYSICAL_DISK_COL_SIZE_CLASS = ''; +const PHYSICAL_DISK_CELL_DEVICE_RESPONSIVE_CLASS = ''; +const PHYSICAL_DISK_CELL_HOST_RESPONSIVE_CLASS = ''; +const PHYSICAL_DISK_CELL_ROLE_RESPONSIVE_CLASS = ''; +const PHYSICAL_DISK_CELL_PARENT_RESPONSIVE_CLASS = ''; +const PHYSICAL_DISK_CELL_LIFE_RESPONSIVE_CLASS = ''; +const PHYSICAL_DISK_CELL_TEMP_RESPONSIVE_CLASS = ''; export const PHYSICAL_DISK_HEADER_DISK_CLASS = `${PHYSICAL_DISK_TABLE_HEADER_CLASS} ${PHYSICAL_DISK_COL_DISK_CLASS}`; export const PHYSICAL_DISK_HEADER_DEVICE_CLASS = `${PHYSICAL_DISK_TABLE_HEADER_CLASS} ${PHYSICAL_DISK_CELL_DEVICE_RESPONSIVE_CLASS}`; // Headers for responsively-hidden columns must use `table-cell` (not diff --git a/frontend-modern/src/features/storageBackups/storagePagePresentation.ts b/frontend-modern/src/features/storageBackups/storagePagePresentation.ts index 0b54395763..772925b904 100644 --- a/frontend-modern/src/features/storageBackups/storagePagePresentation.ts +++ b/frontend-modern/src/features/storageBackups/storagePagePresentation.ts @@ -5,7 +5,11 @@ export type StorageViewOption = { label: string; }; +export type StoragePoolTableColumnId = + 'name' | 'state' | 'type' | 'host' | 'protection' | 'usage' | 'growth'; + export type StoragePoolTableColumn = { + id: StoragePoolTableColumnId; label: string; compactLabel: string; sortKey: StorageSortKey; @@ -13,6 +17,44 @@ export type StoragePoolTableColumn = { colClassName: string; }; +export type StoragePoolTableLayoutMode = 'compact' | 'operational' | 'full'; + +export const getStoragePoolTableLayoutModeForContainer = ( + containerWidth: number, +): StoragePoolTableLayoutMode => { + if (containerWidth >= 1_040) return 'full'; + if (containerWidth >= 560) return 'operational'; + return 'compact'; +}; + +const STORAGE_POOL_VISIBLE_COLUMNS: Record< + StoragePoolTableLayoutMode, + readonly StoragePoolTableColumnId[] +> = { + compact: ['name', 'state', 'usage'], + operational: ['name', 'state', 'host', 'protection', 'usage'], + full: ['name', 'state', 'type', 'host', 'protection', 'usage', 'growth'], +}; + +const STORAGE_POOL_COLUMN_WIDTHS: Record< + StoragePoolTableLayoutMode, + Partial> +> = { + compact: { name: 39, state: 28, usage: 33 }, + operational: { name: 29, state: 20, host: 15, protection: 15, usage: 21 }, + full: { name: 20, state: 14, type: 10, host: 12, protection: 13, usage: 20, growth: 11 }, +}; + +export const isStoragePoolColumnVisible = ( + layout: StoragePoolTableLayoutMode, + columnId: StoragePoolTableColumnId, +): boolean => STORAGE_POOL_VISIBLE_COLUMNS[layout].includes(columnId); + +export const getStoragePoolColumnWidthPercent = ( + layout: StoragePoolTableLayoutMode, + columnId: StoragePoolTableColumnId, +): number => STORAGE_POOL_COLUMN_WIDTHS[layout][columnId] ?? 0; + const STORAGE_POOL_TABLE_HEADER_CLASS = 'overflow-hidden text-ellipsis whitespace-nowrap px-1 sm:px-1.5 lg:px-2 py-0.5 text-left text-[10px] sm:text-[11px] lg:text-xs font-medium uppercase tracking-wider'; @@ -25,53 +67,60 @@ export const getStoragePoolTableColumns = ( growthColumnLabel: string, ): readonly StoragePoolTableColumn[] => [ { + id: 'name', label: 'Storage', compactLabel: 'Storage', sortKey: 'name', - className: `${STORAGE_POOL_TABLE_HEADER_CLASS} w-[39%] sm:w-[29%] md:w-[24%] xl:w-[20%]`, - colClassName: 'w-[39%] sm:w-[29%] md:w-[24%] xl:w-[20%]', + className: STORAGE_POOL_TABLE_HEADER_CLASS, + colClassName: '', }, { + id: 'state', label: 'State', compactLabel: 'State', sortKey: 'state', - className: `${STORAGE_POOL_TABLE_HEADER_CLASS} w-[28%] sm:w-[20%] md:w-[18%] xl:w-[14%]`, - colClassName: 'w-[28%] sm:w-[20%] md:w-[18%] xl:w-[14%]', + className: STORAGE_POOL_TABLE_HEADER_CLASS, + colClassName: '', }, { + id: 'type', label: 'Type', compactLabel: 'Type', sortKey: 'type', - className: `${STORAGE_POOL_TABLE_HEADER_CLASS} hidden xl:table-cell xl:w-[10%]`, - colClassName: 'hidden xl:table-column xl:w-[10%]', + className: STORAGE_POOL_TABLE_HEADER_CLASS, + colClassName: '', }, { + id: 'host', label: 'Host', compactLabel: 'Host', sortKey: 'host', - className: `${STORAGE_POOL_TABLE_HEADER_CLASS} hidden sm:table-cell sm:w-[15%] md:w-[14%] xl:w-[12%]`, - colClassName: 'hidden sm:table-column sm:w-[15%] md:w-[14%] xl:w-[12%]', + className: STORAGE_POOL_TABLE_HEADER_CLASS, + colClassName: '', }, { + id: 'protection', label: 'Protection', compactLabel: 'Prot', sortKey: 'protection', - className: `${STORAGE_POOL_TABLE_HEADER_CLASS} hidden sm:table-cell sm:w-[15%] md:w-[15%] xl:w-[13%]`, - colClassName: 'hidden sm:table-column sm:w-[15%] md:w-[15%] xl:w-[13%]', + className: STORAGE_POOL_TABLE_HEADER_CLASS, + colClassName: '', }, { + id: 'usage', label: 'Usage', compactLabel: 'Used', sortKey: 'usage', - className: `${STORAGE_POOL_TABLE_HEADER_CLASS} w-[33%] sm:w-[21%] md:w-[29%] xl:w-[20%]`, - colClassName: 'w-[33%] sm:w-[21%] md:w-[29%] xl:w-[20%]', + className: STORAGE_POOL_TABLE_HEADER_CLASS, + colClassName: '', }, { + id: 'growth', label: growthColumnLabel, compactLabel: growthColumnLabel.replace(/^Growth\s*\((.+)\)$/i, '$1'), sortKey: 'growth', - className: `${STORAGE_POOL_TABLE_HEADER_CLASS} hidden xl:table-cell xl:w-[11%]`, - colClassName: 'hidden xl:table-column xl:w-[11%]', + className: STORAGE_POOL_TABLE_HEADER_CLASS, + colClassName: '', }, ]; export const STORAGE_CONTENT_CARD_BODY_CLASS = 'p-2'; diff --git a/frontend-modern/src/features/storageBackups/storagePoolRowPresentation.ts b/frontend-modern/src/features/storageBackups/storagePoolRowPresentation.ts index c997ee988a..bab03425d1 100644 --- a/frontend-modern/src/features/storageBackups/storagePoolRowPresentation.ts +++ b/frontend-modern/src/features/storageBackups/storagePoolRowPresentation.ts @@ -34,25 +34,24 @@ export const STORAGE_POOL_ROW_CLASS = 'group cursor-pointer'; export const STORAGE_POOL_ROW_HEIGHT_CLASS = 'h-[32px]'; export const STORAGE_POOL_ROW_EXPANDED_CLASS = 'bg-surface-alt'; export const STORAGE_POOL_ROW_NAME_CELL_CLASS = - 'w-[39%] sm:w-[29%] md:w-[24%] xl:w-[20%] overflow-hidden px-1.5 sm:px-2 py-1 align-middle text-base-content'; + 'overflow-hidden px-1.5 sm:px-2 py-1 align-middle text-base-content'; export const STORAGE_POOL_ROW_NAME_TEXT_CLASS = 'block truncate text-[12px] font-semibold'; export const STORAGE_POOL_ROW_TYPE_CELL_CLASS = - 'hidden xl:table-cell xl:w-[10%] overflow-hidden px-2 py-1 align-middle text-[11px] text-base-content'; + 'overflow-hidden px-2 py-1 align-middle text-[11px] text-base-content'; export const STORAGE_POOL_ROW_HOST_CELL_CLASS = - 'hidden sm:table-cell sm:w-[15%] md:w-[14%] xl:w-[12%] overflow-hidden px-1.5 sm:px-2 py-1 align-middle text-[11px] text-base-content'; + 'overflow-hidden px-1.5 sm:px-2 py-1 align-middle text-[11px] text-base-content'; export const STORAGE_POOL_ROW_TEXT_TRUNCATE_CLASS = 'block truncate'; export const STORAGE_POOL_ROW_PROTECTION_CELL_CLASS = - 'hidden sm:table-cell sm:w-[15%] md:w-[15%] xl:w-[13%] overflow-hidden px-1.5 sm:px-2 py-1 align-middle text-[11px]'; + 'overflow-hidden px-1.5 sm:px-2 py-1 align-middle text-[11px]'; export const STORAGE_POOL_ROW_PROTECTION_TEXT_CLASS = 'block truncate font-semibold'; -export const STORAGE_POOL_ROW_USAGE_CELL_CLASS = - 'w-[33%] sm:w-[21%] md:w-[29%] xl:w-[20%] overflow-hidden px-1.5 sm:px-2 py-1 align-middle'; +export const STORAGE_POOL_ROW_USAGE_CELL_CLASS = 'overflow-hidden px-1.5 sm:px-2 py-1 align-middle'; export const STORAGE_POOL_ROW_USAGE_WRAP_CLASS = 'flex items-center whitespace-nowrap text-[11px]'; export const STORAGE_POOL_ROW_USAGE_BAR_WRAP_CLASS = 'min-w-0 flex-1'; export const STORAGE_POOL_ROW_GROWTH_CELL_CLASS = - 'hidden xl:table-cell xl:w-[11%] overflow-hidden px-2 py-1 align-middle text-[11px]'; + 'overflow-hidden px-2 py-1 align-middle text-[11px]'; export const STORAGE_POOL_ROW_GROWTH_TEXT_CLASS = 'block truncate font-mono font-semibold'; export const STORAGE_POOL_ROW_ISSUE_CELL_CLASS = - 'w-[28%] sm:w-[20%] md:w-[18%] xl:w-[14%] overflow-hidden px-1.5 sm:px-2 py-1 align-middle text-[11px]'; + 'overflow-hidden px-1.5 sm:px-2 py-1 align-middle text-[11px]'; export const STORAGE_POOL_ROW_ISSUE_TEXT_CLASS = 'block truncate text-[11px] font-semibold'; export const STORAGE_POOL_ROW_PLACEHOLDER_CLASS = 'text-muted'; export const STORAGE_POOL_ROW_USAGE_FALLBACK_CLASS = 'text-[11px] text-muted';