fix(ui): make Proxmox storage tables container-aware

Contract-Neutral: Responsive Proxmox Storage table presentation and verification only; storage ownership, data contracts, and extension boundaries are unchanged
This commit is contained in:
rcourtman 2026-08-04 10:44:15 +01:00
parent e4bc063eb0
commit 1807ab294c
11 changed files with 464 additions and 104 deletions

View file

@ -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"
]
}

View file

@ -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<DiskListProps> = (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<DiskListProps> = (props) => {
</Show>
<Show when={model.filteredDisks().length > 0}>
<Table class={PHYSICAL_DISK_TABLE_CLASS}>
<Table
class={PHYSICAL_DISK_TABLE_CLASS}
wrapperRef={tableWidth.setElement}
data-storage-table="physical-disks"
data-storage-layout={layoutMode()}
>
<colgroup>
<col class={PHYSICAL_DISK_COL_DISK_CLASS} />
<col class={PHYSICAL_DISK_COL_DEVICE_CLASS} />
<col class={PHYSICAL_DISK_COL_HOST_CLASS} />
<col class={PHYSICAL_DISK_COL_ROLE_CLASS} />
<col class={PHYSICAL_DISK_COL_PARENT_CLASS} />
<col class={PHYSICAL_DISK_COL_HEALTH_CLASS} />
<col class={PHYSICAL_DISK_COL_LIFE_CLASS} />
<col class={PHYSICAL_DISK_COL_TEMP_CLASS} />
<col class={PHYSICAL_DISK_COL_SIZE_CLASS} />
<col
class={columnClass(PHYSICAL_DISK_COL_DISK_CLASS, 'disk', 'table-column')}
style={columnStyle('disk')}
data-storage-column="disk"
/>
<col
class={columnClass(PHYSICAL_DISK_COL_DEVICE_CLASS, 'device', 'table-column')}
style={columnStyle('device')}
data-storage-column="device"
/>
<col
class={columnClass(PHYSICAL_DISK_COL_HOST_CLASS, 'host', 'table-column')}
style={columnStyle('host')}
data-storage-column="host"
/>
<col
class={columnClass(PHYSICAL_DISK_COL_ROLE_CLASS, 'role', 'table-column')}
style={columnStyle('role')}
data-storage-column="role"
/>
<col
class={columnClass(PHYSICAL_DISK_COL_PARENT_CLASS, 'parent', 'table-column')}
style={columnStyle('parent')}
data-storage-column="parent"
/>
<col
class={columnClass(PHYSICAL_DISK_COL_HEALTH_CLASS, 'health', 'table-column')}
style={columnStyle('health')}
data-storage-column="health"
/>
<col
class={columnClass(PHYSICAL_DISK_COL_LIFE_CLASS, 'life', 'table-column')}
style={columnStyle('life')}
data-storage-column="life"
/>
<col
class={columnClass(PHYSICAL_DISK_COL_TEMP_CLASS, 'temp', 'table-column')}
style={columnStyle('temp')}
data-storage-column="temp"
/>
<col
class={columnClass(PHYSICAL_DISK_COL_SIZE_CLASS, 'size', 'table-column')}
style={columnStyle('size')}
data-storage-column="size"
/>
</colgroup>
<TableHeader>
<TableRow class={PHYSICAL_DISK_TABLE_HEADER_ROW_CLASS}>
<TableHead class={PHYSICAL_DISK_HEADER_DISK_CLASS}>Disk</TableHead>
<TableHead class={PHYSICAL_DISK_HEADER_DEVICE_CLASS}>Device</TableHead>
<TableHead class={PHYSICAL_DISK_HEADER_HOST_CLASS}>Host</TableHead>
<TableHead class={PHYSICAL_DISK_HEADER_ROLE_CLASS}>Role</TableHead>
<TableHead
class={PHYSICAL_DISK_HEADER_PARENT_CLASS}
class={columnClass(PHYSICAL_DISK_HEADER_DISK_CLASS, 'disk', 'table-cell')}
data-storage-column="disk"
>
Disk
</TableHead>
<TableHead
class={columnClass(PHYSICAL_DISK_HEADER_DEVICE_CLASS, 'device', 'table-cell')}
data-storage-column="device"
>
Device
</TableHead>
<TableHead
class={columnClass(PHYSICAL_DISK_HEADER_HOST_CLASS, 'host', 'table-cell')}
data-storage-column="host"
>
Host
</TableHead>
<TableHead
class={columnClass(PHYSICAL_DISK_HEADER_ROLE_CLASS, 'role', 'table-cell')}
data-storage-column="role"
>
Role
</TableHead>
<TableHead
class={columnClass(PHYSICAL_DISK_HEADER_PARENT_CLASS, 'parent', 'table-cell')}
data-storage-column="parent"
aria-label="Belongs To"
title="Belongs To"
>
Belongs
</TableHead>
<TableHead class={PHYSICAL_DISK_HEADER_HEALTH_CLASS}>Health</TableHead>
<TableHead
class={PHYSICAL_DISK_HEADER_LIFE_CLASS}
class={columnClass(PHYSICAL_DISK_HEADER_HEALTH_CLASS, 'health', 'table-cell')}
data-storage-column="health"
>
Health
</TableHead>
<TableHead
class={columnClass(PHYSICAL_DISK_HEADER_LIFE_CLASS, 'life', 'table-cell')}
data-storage-column="life"
aria-label="SSD life remaining"
title="SSD life remaining"
>
Life
</TableHead>
<TableHead class={PHYSICAL_DISK_HEADER_TEMP_CLASS}>Temp</TableHead>
<TableHead class={PHYSICAL_DISK_HEADER_SIZE_CLASS}>Size</TableHead>
<TableHead
class={columnClass(PHYSICAL_DISK_HEADER_TEMP_CLASS, 'temp', 'table-cell')}
data-storage-column="temp"
>
Temp
</TableHead>
<TableHead
class={columnClass(PHYSICAL_DISK_HEADER_SIZE_CLASS, 'size', 'table-cell')}
data-storage-column="size"
>
Size
</TableHead>
</TableRow>
</TableHeader>
<TableBody class={PHYSICAL_DISK_TABLE_BODY_CLASS}>
@ -254,7 +350,10 @@ export const DiskList: Component<DiskListProps> = (props) => {
onClick={() => model.toggleSelectedDisk(disk)}
{...interactiveRowHandlers}
>
<TableCell class={PHYSICAL_DISK_CELL_DISK_CLASS}>
<TableCell
class={columnClass(PHYSICAL_DISK_CELL_DISK_CLASS, 'disk', 'table-cell')}
data-storage-column="disk"
>
<div class={PHYSICAL_DISK_NAME_WRAP_CLASS}>
<SummaryRowActionButton
kind="disclosure"
@ -273,7 +372,10 @@ export const DiskList: Component<DiskListProps> = (props) => {
</div>
</TableCell>
<TableCell class={PHYSICAL_DISK_CELL_DEVICE_CLASS}>
<TableCell
class={columnClass(PHYSICAL_DISK_CELL_DEVICE_CLASS, 'device', 'table-cell')}
data-storage-column="device"
>
<Show
when={data.devPath}
fallback={<span class={PHYSICAL_DISK_MUTED_PLACEHOLDER_CLASS}>—</span>}
@ -284,7 +386,10 @@ export const DiskList: Component<DiskListProps> = (props) => {
</Show>
</TableCell>
<TableCell class={PHYSICAL_DISK_CELL_HOST_CLASS}>
<TableCell
class={columnClass(PHYSICAL_DISK_CELL_HOST_CLASS, 'host', 'table-cell')}
data-storage-column="host"
>
<Show
when={hostLabel}
fallback={<span class={PHYSICAL_DISK_MUTED_PLACEHOLDER_CLASS}>—</span>}
@ -295,7 +400,10 @@ export const DiskList: Component<DiskListProps> = (props) => {
</Show>
</TableCell>
<TableCell class={PHYSICAL_DISK_CELL_ROLE_CLASS}>
<TableCell
class={columnClass(PHYSICAL_DISK_CELL_ROLE_CLASS, 'role', 'table-cell')}
data-storage-column="role"
>
<Show
when={getPhysicalDiskRoleLabel(data)}
fallback={<span class={PHYSICAL_DISK_MUTED_PLACEHOLDER_CLASS}>—</span>}
@ -309,7 +417,10 @@ export const DiskList: Component<DiskListProps> = (props) => {
</Show>
</TableCell>
<TableCell class={PHYSICAL_DISK_CELL_PARENT_CLASS}>
<TableCell
class={columnClass(PHYSICAL_DISK_CELL_PARENT_CLASS, 'parent', 'table-cell')}
data-storage-column="parent"
>
<Show
when={getPhysicalDiskParentLabel(data)}
fallback={<span class={PHYSICAL_DISK_MUTED_PLACEHOLDER_CLASS}>—</span>}
@ -323,7 +434,10 @@ export const DiskList: Component<DiskListProps> = (props) => {
</Show>
</TableCell>
<TableCell class={PHYSICAL_DISK_CELL_HEALTH_CLASS}>
<TableCell
class={columnClass(PHYSICAL_DISK_CELL_HEALTH_CLASS, 'health', 'table-cell')}
data-storage-column="health"
>
<div class={PHYSICAL_DISK_HEALTH_WRAP_CLASS}>
<span class={`${PHYSICAL_DISK_HEALTH_LABEL_CLASS} ${status.tone}`}>
{status.label}
@ -336,7 +450,10 @@ export const DiskList: Component<DiskListProps> = (props) => {
</div>
</TableCell>
<TableCell class={PHYSICAL_DISK_CELL_LIFE_CLASS}>
<TableCell
class={columnClass(PHYSICAL_DISK_CELL_LIFE_CLASS, 'life', 'table-cell')}
data-storage-column="life"
>
<Show
when={getPhysicalDiskLifeLabel(data)}
fallback={<span class={PHYSICAL_DISK_MUTED_PLACEHOLDER_CLASS}>—</span>}
@ -349,7 +466,10 @@ export const DiskList: Component<DiskListProps> = (props) => {
</Show>
</TableCell>
<TableCell class={PHYSICAL_DISK_CELL_TEMP_CLASS}>
<TableCell
class={columnClass(PHYSICAL_DISK_CELL_TEMP_CLASS, 'temp', 'table-cell')}
data-storage-column="temp"
>
<Show
when={data.temperature > 0}
fallback={<span class={PHYSICAL_DISK_MUTED_PLACEHOLDER_CLASS}>—</span>}
@ -366,7 +486,10 @@ export const DiskList: Component<DiskListProps> = (props) => {
</Show>
</TableCell>
<TableCell class={PHYSICAL_DISK_CELL_SIZE_CLASS}>
<TableCell
class={columnClass(PHYSICAL_DISK_CELL_SIZE_CLASS, 'size', 'table-cell')}
data-storage-column="size"
>
<Show
when={data.size > 0}
fallback={

View file

@ -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<StoragePoolRowProps> = (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<StoragePoolRowProps> = (props) => {
data-summary-row-active={props.summaryHighlighted && !props.expanded ? 'true' : 'false'}
{...props.alertDataAttrs}
>
<td class={STORAGE_POOL_ROW_NAME_CELL_CLASS}>
<td
class={columnClass(STORAGE_POOL_ROW_NAME_CELL_CLASS, 'name')}
data-storage-column="name"
>
<div class="flex min-w-0 items-center gap-1.5">
<SummaryRowActionButton
kind="disclosure"
@ -94,7 +105,10 @@ export const StoragePoolRow: Component<StoragePoolRowProps> = (props) => {
</div>
</td>
<td class={STORAGE_POOL_ROW_ISSUE_CELL_CLASS}>
<td
class={columnClass(STORAGE_POOL_ROW_ISSUE_CELL_CLASS, 'state')}
data-storage-column="state"
>
<Show
when={row().stateLabel !== '—'}
fallback={<span class={STORAGE_POOL_ROW_PLACEHOLDER_CLASS}>—</span>}
@ -108,19 +122,28 @@ export const StoragePoolRow: Component<StoragePoolRowProps> = (props) => {
</Show>
</td>
<td class={STORAGE_POOL_ROW_TYPE_CELL_CLASS}>
<td
class={columnClass(STORAGE_POOL_ROW_TYPE_CELL_CLASS, 'type')}
data-storage-column="type"
>
<span class={STORAGE_POOL_ROW_TEXT_TRUNCATE_CLASS} title={row().topologyLabel}>
{row().topologyLabel}
</span>
</td>
<td class={STORAGE_POOL_ROW_HOST_CELL_CLASS}>
<td
class={columnClass(STORAGE_POOL_ROW_HOST_CELL_CLASS, 'host')}
data-storage-column="host"
>
<span class={STORAGE_POOL_ROW_TEXT_TRUNCATE_CLASS} title={row().hostLabel}>
{row().hostLabel}
</span>
</td>
<td class={STORAGE_POOL_ROW_PROTECTION_CELL_CLASS}>
<td
class={columnClass(STORAGE_POOL_ROW_PROTECTION_CELL_CLASS, 'protection')}
data-storage-column="protection"
>
<Show
when={row().compactProtection !== '—'}
fallback={<span class={STORAGE_POOL_ROW_PLACEHOLDER_CLASS}>—</span>}
@ -134,7 +157,10 @@ export const StoragePoolRow: Component<StoragePoolRowProps> = (props) => {
</Show>
</td>
<td class={STORAGE_POOL_ROW_USAGE_CELL_CLASS}>
<td
class={columnClass(STORAGE_POOL_ROW_USAGE_CELL_CLASS, 'usage')}
data-storage-column="usage"
>
<Show
when={row().totalBytes > 0}
fallback={<span class={STORAGE_POOL_ROW_USAGE_FALLBACK_CLASS}>n/a</span>}
@ -152,7 +178,10 @@ export const StoragePoolRow: Component<StoragePoolRowProps> = (props) => {
</Show>
</td>
<td class={STORAGE_POOL_ROW_GROWTH_CELL_CLASS}>
<td
class={columnClass(STORAGE_POOL_ROW_GROWTH_CELL_CLASS, 'growth')}
data-storage-column="growth"
>
<span
class={`${STORAGE_POOL_ROW_GROWTH_TEXT_CLASS} ${row().capacityDeltaToneClass}`}
title={row().capacityDeltaTitle}

View file

@ -1,10 +1,12 @@
import { Component, For, Index, Show } from 'solid-js';
import { Component, For, Index, Show, createMemo } from 'solid-js';
import ArrowDownIcon from 'lucide-solid/icons/arrow-down';
import ArrowUpIcon from 'lucide-solid/icons/arrow-up';
import ArrowUpDownIcon from 'lucide-solid/icons/arrow-up-down';
import { Table, TableBody, TableHead, TableHeader, TableRow } from '@/components/shared/Table';
import {
getStoragePoolColumnWidthPercent,
getStoragePoolTableColumns,
getStoragePoolTableLayoutModeForContainer,
getStorageEmptyStateMessage,
getStorageLoadingMessage,
STORAGE_POOLS_BODY_CLASS,
@ -12,7 +14,9 @@ import {
STORAGE_POOLS_HEADER_ROW_CLASS,
STORAGE_POOLS_LOADING_STATE_CLASS,
STORAGE_POOLS_TABLE_CLASS,
isStoragePoolColumnVisible,
} from '@/features/storageBackups/storagePagePresentation';
import { useObservedElementWidth } from '@/hooks/useObservedElementWidth';
import { resolveStorageRecordMetricResourceId } from '@/features/storageBackups/storageMetricsIdentity';
import type { StorageCapacityDeltaPresentation } from '@/features/storageBackups/storageCapacityDeltaPresentation';
import type { StorageAlertRowState } from '@/features/storageBackups/storageAlertState';
@ -79,6 +83,16 @@ const getStorageColumnSortButtonLabel = (
};
export const StoragePoolsTable: Component<StoragePoolsTableProps> = (props) => {
const tableWidth = useObservedElementWidth();
const layoutMode = createMemo(() =>
getStoragePoolTableLayoutModeForContainer(tableWidth.width() ?? 0),
);
const columnClass = (
baseClass: string,
columnId: Parameters<typeof isStoragePoolColumnVisible>[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<StoragePoolsTableProps> = (props) => {
<div class={STORAGE_POOLS_EMPTY_STATE_CLASS}>{getStorageEmptyStateMessage()}</div>
}
>
<Table class={STORAGE_POOLS_TABLE_CLASS}>
<Table
class={STORAGE_POOLS_TABLE_CLASS}
wrapperRef={tableWidth.setElement}
data-storage-table="pools"
data-storage-layout={layoutMode()}
>
<colgroup>
<For each={getStoragePoolTableColumns(props.storageGrowthColumnLabel)}>
{(column) => <col class={column.colClassName} />}
{(column) => (
<col
class={columnClass(column.colClassName, column.id, 'table-column')}
style={{
width: `${getStoragePoolColumnWidthPercent(layoutMode(), column.id)}%`,
}}
data-storage-column={column.id}
/>
)}
</For>
</colgroup>
<TableHeader>
@ -118,7 +145,8 @@ export const StoragePoolsTable: Component<StoragePoolsTableProps> = (props) => {
<For each={getStoragePoolTableColumns(props.storageGrowthColumnLabel)}>
{(column) => (
<TableHead
class={column.className}
class={columnClass(column.className, column.id, 'table-cell')}
data-storage-column={column.id}
aria-label={column.label}
aria-sort={
props.sortKey === column.sortKey
@ -144,8 +172,12 @@ export const StoragePoolsTable: Component<StoragePoolsTableProps> = (props) => {
props.sortDirection,
)}
>
<span class="hidden min-w-0 truncate xl:inline">{column.label}</span>
<span class="min-w-0 truncate xl:hidden">{column.compactLabel}</span>
<Show
when={layoutMode() === 'full'}
fallback={<span class="min-w-0 truncate">{column.compactLabel}</span>}
>
<span class="min-w-0 truncate">{column.label}</span>
</Show>
<Show
when={props.sortKey === column.sortKey}
fallback={
@ -210,6 +242,7 @@ export const StoragePoolsTable: Component<StoragePoolsTableProps> = (props) => {
return (
<StoragePoolRow
layoutMode={layoutMode()}
record={record()}
growthDelta={
props.storageGrowthBySeriesId.get(

View file

@ -105,7 +105,13 @@ describe('DiskList', () => {
});
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();

View file

@ -529,7 +529,7 @@ describe('Storage', () => {
render(() => <Storage />);
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(() => <Storage />);
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();

View file

@ -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({

View file

@ -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);
});
});

View file

@ -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<Record<PhysicalDiskTableColumnId, number>>
> = {
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

View file

@ -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<Record<StoragePoolTableColumnId, number>>
> = {
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';

View file

@ -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';