mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-19 10:33:29 +00:00
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:
parent
e4bc063eb0
commit
1807ab294c
11 changed files with 464 additions and 104 deletions
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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={
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue