mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-07-10 00:14:38 +00:00
storage(disks): restore v5 information density to the physical disks table
The v6 physical disks table regressed against its v5 ancestor: device path was demoted to a tooltip (identical models became indistinguishable), SSD wearout vanished from the surface entirely despite flowing all the way into the presentation layer, the Belongs column rendered empty for every disk without a ZFS pool mapping, and a Source badge spent a column saying PVE on every row. Flagged from the side-by-side screenshots in #1471. - Add a Device column (mono devPath, md+). - Add a Life column rendering wearout with v5-style thresholds (md+). - Fall back Belongs to Proxmox's usage string (ZFS / ext4 / BIOS boot) when no pool mapping exists; searchable via the existing haystack. - Demote the Source badge to the row expansion header next to the serial; drop the table column. - Render missing temperatures as muted dashes instead of green ones (getTemperatureTextClass(0) returned the healthy tone). - Tighten rows from 38px to 32px to match the platform table standard.
This commit is contained in:
parent
55db13fc06
commit
809e2c9008
6 changed files with 150 additions and 47 deletions
|
|
@ -38,6 +38,7 @@ import {
|
|||
STORAGE_DETAIL_MONO_CHIP_CLASS,
|
||||
STORAGE_DETAIL_SECTION_TITLE_CLASS,
|
||||
} from '@/features/storageBackups/detailPresentation';
|
||||
import { getPhysicalDiskSourceBadgePresentation } from '@/features/storageBackups/diskPresentation';
|
||||
import { StorageDetailMetricCard } from './StorageDetailMetricCard';
|
||||
import { useDiskDetailModel } from './useDiskDetailModel';
|
||||
|
||||
|
|
@ -82,6 +83,12 @@ export const DiskDetail: Component<DiskDetailProps> = (props) => {
|
|||
<span class={STORAGE_DISK_DETAIL_MODEL_CLASS}>{diskData().model || 'Unknown Disk'}</span>
|
||||
<span class={STORAGE_DETAIL_MONO_CHIP_CLASS}>{diskData().devPath}</span>
|
||||
<span class={STORAGE_DISK_DETAIL_NODE_CLASS}>{diskData().node}</span>
|
||||
<span
|
||||
class={getPhysicalDiskSourceBadgePresentation(props.disk).className}
|
||||
title="Data source"
|
||||
>
|
||||
{getPhysicalDiskSourceBadgePresentation(props.disk).label}
|
||||
</span>
|
||||
<Show when={diskData().serial}>
|
||||
<span class={STORAGE_DISK_DETAIL_SERIAL_CLASS}>S/N: {diskData().serial}</span>
|
||||
</Show>
|
||||
|
|
|
|||
|
|
@ -16,21 +16,23 @@ import {
|
|||
import { formatBytes } from '@/utils/format';
|
||||
import { formatTemperature, getTemperatureTextClass } from '@/utils/temperature';
|
||||
import {
|
||||
PHYSICAL_DISK_CELL_DEVICE_CLASS,
|
||||
PHYSICAL_DISK_CELL_DISK_CLASS,
|
||||
PHYSICAL_DISK_CELL_HEALTH_CLASS,
|
||||
PHYSICAL_DISK_CELL_HOST_CLASS,
|
||||
PHYSICAL_DISK_CELL_LIFE_CLASS,
|
||||
PHYSICAL_DISK_CELL_PARENT_CLASS,
|
||||
PHYSICAL_DISK_CELL_ROLE_CLASS,
|
||||
PHYSICAL_DISK_CELL_SIZE_CLASS,
|
||||
PHYSICAL_DISK_CELL_SOURCE_CLASS,
|
||||
PHYSICAL_DISK_CELL_TEMP_CLASS,
|
||||
PHYSICAL_DISK_COL_DEVICE_CLASS,
|
||||
PHYSICAL_DISK_COL_DISK_CLASS,
|
||||
PHYSICAL_DISK_COL_HEALTH_CLASS,
|
||||
PHYSICAL_DISK_COL_HOST_CLASS,
|
||||
PHYSICAL_DISK_COL_LIFE_CLASS,
|
||||
PHYSICAL_DISK_COL_PARENT_CLASS,
|
||||
PHYSICAL_DISK_COL_ROLE_CLASS,
|
||||
PHYSICAL_DISK_COL_SIZE_CLASS,
|
||||
PHYSICAL_DISK_COL_SOURCE_CLASS,
|
||||
PHYSICAL_DISK_COL_TEMP_CLASS,
|
||||
PHYSICAL_DISK_DETAIL_ROW_CELL_CLASS,
|
||||
PHYSICAL_DISK_EMPTY_CARD_CLASS,
|
||||
|
|
@ -42,17 +44,20 @@ import {
|
|||
PHYSICAL_DISK_EMPTY_REQUIREMENTS_NOTE_CLASS,
|
||||
PHYSICAL_DISK_EMPTY_REQUIREMENTS_TITLE_CLASS,
|
||||
PHYSICAL_DISK_EMPTY_TITLE_CLASS,
|
||||
PHYSICAL_DISK_HEADER_DEVICE_CLASS,
|
||||
PHYSICAL_DISK_HEADER_DISK_CLASS,
|
||||
PHYSICAL_DISK_HEADER_HEALTH_CLASS,
|
||||
PHYSICAL_DISK_HEADER_HOST_CLASS,
|
||||
PHYSICAL_DISK_HEADER_LIFE_CLASS,
|
||||
PHYSICAL_DISK_HEADER_PARENT_CLASS,
|
||||
PHYSICAL_DISK_HEADER_ROLE_CLASS,
|
||||
PHYSICAL_DISK_HEADER_SIZE_CLASS,
|
||||
PHYSICAL_DISK_HEADER_SOURCE_CLASS,
|
||||
PHYSICAL_DISK_HEADER_TEMP_CLASS,
|
||||
PHYSICAL_DISK_HEALTH_LABEL_CLASS,
|
||||
PHYSICAL_DISK_HEALTH_SUMMARY_CLASS,
|
||||
PHYSICAL_DISK_HEALTH_WRAP_CLASS,
|
||||
PHYSICAL_DISK_DEVICE_TEXT_CLASS,
|
||||
PHYSICAL_DISK_LIFE_CLASS,
|
||||
PHYSICAL_DISK_MUTED_PLACEHOLDER_CLASS,
|
||||
PHYSICAL_DISK_NAME_TEXT_CLASS,
|
||||
PHYSICAL_DISK_NAME_WRAP_CLASS,
|
||||
|
|
@ -70,9 +75,10 @@ import {
|
|||
getPhysicalDiskHealthStatus,
|
||||
getPhysicalDiskHealthSummary,
|
||||
getPhysicalDiskHostLabel,
|
||||
getPhysicalDiskLifeLabel,
|
||||
getPhysicalDiskLifeTextClass,
|
||||
getPhysicalDiskParentLabel,
|
||||
getPhysicalDiskRoleLabel,
|
||||
getPhysicalDiskSourceBadgePresentation,
|
||||
} from '@/features/storageBackups/diskPresentation';
|
||||
import type { Resource } from '@/types/resource';
|
||||
import type { StorageHealthFilter } from '@/features/storageBackups/models';
|
||||
|
|
@ -178,29 +184,19 @@ export const DiskList: Component<DiskListProps> = (props) => {
|
|||
<Table class={PHYSICAL_DISK_TABLE_CLASS}>
|
||||
<colgroup>
|
||||
<col class={PHYSICAL_DISK_COL_DISK_CLASS} />
|
||||
<col class={PHYSICAL_DISK_COL_SOURCE_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} />
|
||||
</colgroup>
|
||||
<TableHeader>
|
||||
<TableRow class={PHYSICAL_DISK_TABLE_HEADER_ROW_CLASS}>
|
||||
<TableHead class={PHYSICAL_DISK_HEADER_DISK_CLASS}>Disk</TableHead>
|
||||
<TableHead
|
||||
class={PHYSICAL_DISK_HEADER_SOURCE_CLASS}
|
||||
aria-label="Source"
|
||||
title="Source"
|
||||
>
|
||||
<span aria-hidden="true" class="hidden sm:inline">
|
||||
Source
|
||||
</span>
|
||||
<span aria-hidden="true" class="sm:hidden">
|
||||
Src
|
||||
</span>
|
||||
</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
|
||||
|
|
@ -211,6 +207,13 @@ export const DiskList: Component<DiskListProps> = (props) => {
|
|||
Belongs
|
||||
</TableHead>
|
||||
<TableHead class={PHYSICAL_DISK_HEADER_HEALTH_CLASS}>Health</TableHead>
|
||||
<TableHead
|
||||
class={PHYSICAL_DISK_HEADER_LIFE_CLASS}
|
||||
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>
|
||||
</TableRow>
|
||||
|
|
@ -222,7 +225,6 @@ export const DiskList: Component<DiskListProps> = (props) => {
|
|||
const status = getPhysicalDiskHealthStatus(data);
|
||||
const hostLabel = getPhysicalDiskHostLabel(data, disk);
|
||||
const healthSummary = getPhysicalDiskHealthSummary(status);
|
||||
const sourceBadge = getPhysicalDiskSourceBadgePresentation(disk);
|
||||
const isSelected = () => model.selectedDisk()?.id === disk.id;
|
||||
const summarySeriesId = resolvePhysicalDiskMetricResourceId(disk);
|
||||
const isSummaryHighlighted = () =>
|
||||
|
|
@ -269,8 +271,15 @@ export const DiskList: Component<DiskListProps> = (props) => {
|
|||
</div>
|
||||
</TableCell>
|
||||
|
||||
<TableCell class={PHYSICAL_DISK_CELL_SOURCE_CLASS}>
|
||||
<span class={sourceBadge.className}>{sourceBadge.label}</span>
|
||||
<TableCell class={PHYSICAL_DISK_CELL_DEVICE_CLASS}>
|
||||
<Show
|
||||
when={data.devPath}
|
||||
fallback={<span class={PHYSICAL_DISK_MUTED_PLACEHOLDER_CLASS}>—</span>}
|
||||
>
|
||||
<span class={PHYSICAL_DISK_DEVICE_TEXT_CLASS} title={data.devPath}>
|
||||
{data.devPath}
|
||||
</span>
|
||||
</Show>
|
||||
</TableCell>
|
||||
|
||||
<TableCell class={PHYSICAL_DISK_CELL_HOST_CLASS}>
|
||||
|
|
@ -325,12 +334,30 @@ export const DiskList: Component<DiskListProps> = (props) => {
|
|||
</div>
|
||||
</TableCell>
|
||||
|
||||
<TableCell class={PHYSICAL_DISK_CELL_TEMP_CLASS}>
|
||||
<span
|
||||
class={`${PHYSICAL_DISK_TEMPERATURE_CLASS} ${getTemperatureTextClass(data.temperature)}`}
|
||||
<TableCell class={PHYSICAL_DISK_CELL_LIFE_CLASS}>
|
||||
<Show
|
||||
when={getPhysicalDiskLifeLabel(data)}
|
||||
fallback={<span class={PHYSICAL_DISK_MUTED_PLACEHOLDER_CLASS}>—</span>}
|
||||
>
|
||||
{data.temperature > 0 ? formatTemperature(data.temperature) : '—'}
|
||||
</span>
|
||||
<span
|
||||
class={`${PHYSICAL_DISK_LIFE_CLASS} ${getPhysicalDiskLifeTextClass(data)}`}
|
||||
>
|
||||
{getPhysicalDiskLifeLabel(data)}
|
||||
</span>
|
||||
</Show>
|
||||
</TableCell>
|
||||
|
||||
<TableCell class={PHYSICAL_DISK_CELL_TEMP_CLASS}>
|
||||
<Show
|
||||
when={data.temperature > 0}
|
||||
fallback={<span class={PHYSICAL_DISK_MUTED_PLACEHOLDER_CLASS}>—</span>}
|
||||
>
|
||||
<span
|
||||
class={`${PHYSICAL_DISK_TEMPERATURE_CLASS} ${getTemperatureTextClass(data.temperature)}`}
|
||||
>
|
||||
{formatTemperature(data.temperature)}
|
||||
</span>
|
||||
</Show>
|
||||
</TableCell>
|
||||
|
||||
<TableCell class={PHYSICAL_DISK_CELL_SIZE_CLASS}>
|
||||
|
|
@ -355,7 +382,7 @@ export const DiskList: Component<DiskListProps> = (props) => {
|
|||
<TableRow data-inline-detail-for={summarySeriesId}>
|
||||
<TableCell
|
||||
id={detailControlsId}
|
||||
colSpan={8}
|
||||
colSpan={9}
|
||||
class={PHYSICAL_DISK_DETAIL_ROW_CELL_CLASS}
|
||||
>
|
||||
<DiskDetail disk={disk} nodes={props.nodes} />
|
||||
|
|
|
|||
|
|
@ -105,17 +105,39 @@ describe('DiskList', () => {
|
|||
});
|
||||
|
||||
expect(screen.getByRole('columnheader', { name: 'Disk' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('columnheader', { name: 'Source' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('columnheader', { name: 'Device' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('columnheader', { name: 'Host' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('columnheader', { name: 'SSD life remaining' })).toBeInTheDocument();
|
||||
expect(screen.queryByRole('columnheader', { name: 'Source' })).not.toBeInTheDocument();
|
||||
expect(screen.getByText('Disk sda')).toBeInTheDocument();
|
||||
expect(screen.getByText('/dev/sda')).toBeInTheDocument();
|
||||
expect(screen.getByText('tower')).toBeInTheDocument();
|
||||
expect(screen.getByText('PVE')).toBeInTheDocument();
|
||||
expect(screen.getByText('Parity')).toBeInTheDocument();
|
||||
expect(screen.getByText('Tower Array')).toBeInTheDocument();
|
||||
expect(screen.getByText('Needs Attention')).toBeInTheDocument();
|
||||
expect(screen.getByText('Pending sectors detected.')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders SSD life and falls back to the Proxmox usage string for Belongs', () => {
|
||||
renderDiskList({
|
||||
disks: [
|
||||
buildDisk('sda', 'tower', {
|
||||
diskType: 'ssd',
|
||||
wearout: 96,
|
||||
storageGroup: '',
|
||||
used: 'ZFS',
|
||||
storageRole: '',
|
||||
}),
|
||||
],
|
||||
nodes: [buildNode('node-tower', 'tower')],
|
||||
selectedNode: null,
|
||||
searchTerm: '',
|
||||
});
|
||||
|
||||
expect(screen.getByText('96%')).toBeInTheDocument();
|
||||
expect(screen.getByText('ZFS')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('filters disks by search term and supports row expansion', () => {
|
||||
renderDiskList({
|
||||
disks: [buildDisk('sda', 'tower'), buildDisk('sdb', 'tower', { model: 'Cache SSD' })],
|
||||
|
|
@ -196,7 +218,7 @@ describe('DiskList', () => {
|
|||
searchTerm: '',
|
||||
});
|
||||
|
||||
expect(screen.getByText('TrueNAS')).toBeInTheDocument();
|
||||
expect(screen.getByText('truenas-main')).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByText('Seagate IronWolf'));
|
||||
expect(screen.getByTestId('disk-detail')).toHaveTextContent('sda');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -10,8 +10,9 @@ import {
|
|||
PHYSICAL_DISK_EMPTY_CARD_CLASS,
|
||||
PHYSICAL_DISK_ALL_GROUPS_FILTER_LABEL,
|
||||
PHYSICAL_DISK_ALL_ROLES_FILTER_LABEL,
|
||||
PHYSICAL_DISK_HEADER_DEVICE_CLASS,
|
||||
PHYSICAL_DISK_HEADER_DISK_CLASS,
|
||||
PHYSICAL_DISK_HEADER_SOURCE_CLASS,
|
||||
PHYSICAL_DISK_HEADER_LIFE_CLASS,
|
||||
PHYSICAL_DISK_NAME_TEXT_CLASS,
|
||||
PHYSICAL_DISK_SOURCE_BADGE_CLASS,
|
||||
PHYSICAL_DISK_TABLE_CLASS,
|
||||
|
|
@ -20,6 +21,8 @@ import {
|
|||
getPhysicalDiskHealthStatus,
|
||||
getPhysicalDiskHealthSummary,
|
||||
getPhysicalDiskHostLabel,
|
||||
getPhysicalDiskLifeLabel,
|
||||
getPhysicalDiskLifeTextClass,
|
||||
getPhysicalDiskNormalizedHealth,
|
||||
getPhysicalDiskParentLabel,
|
||||
getPhysicalDiskPlatformLabel,
|
||||
|
|
@ -63,7 +66,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_SOURCE_CLASS).toContain('sm:w-[11%]');
|
||||
expect(PHYSICAL_DISK_HEADER_DEVICE_CLASS).toContain('md:table-cell');
|
||||
expect(PHYSICAL_DISK_HEADER_LIFE_CLASS).toContain('md:table-cell');
|
||||
expect(PHYSICAL_DISK_NAME_TEXT_CLASS).toContain('font-semibold');
|
||||
expect(PHYSICAL_DISK_SOURCE_BADGE_CLASS).toContain('justify-center');
|
||||
|
||||
|
|
@ -119,6 +123,19 @@ describe('diskPresentation', () => {
|
|||
storageGroup: 'tank',
|
||||
}),
|
||||
).toBe('tank');
|
||||
expect(getPhysicalDiskParentLabel(makeDiskData({ storageGroup: '', used: 'ZFS' }))).toBe('ZFS');
|
||||
expect(getPhysicalDiskParentLabel(makeDiskData({ storageGroup: 'tank', used: 'ZFS' }))).toBe(
|
||||
'tank',
|
||||
);
|
||||
expect(getPhysicalDiskParentLabel(makeDiskData({ storageGroup: '', used: 'unknown' }))).toBe(
|
||||
'',
|
||||
);
|
||||
expect(getPhysicalDiskLifeLabel(makeDiskData({ wearout: 96 }))).toBe('96%');
|
||||
expect(getPhysicalDiskLifeLabel(makeDiskData({ wearout: 0 }))).toBe('');
|
||||
expect(getPhysicalDiskLifeLabel(makeDiskData({ wearout: -1 }))).toBe('');
|
||||
expect(getPhysicalDiskLifeTextClass(makeDiskData({ wearout: 96 }))).toContain('text-green');
|
||||
expect(getPhysicalDiskLifeTextClass(makeDiskData({ wearout: 35 }))).toContain('text-amber');
|
||||
expect(getPhysicalDiskLifeTextClass(makeDiskData({ wearout: 9 }))).toContain('text-red');
|
||||
expect(getPhysicalDiskPlatformLabel({} as Resource, 'PVE')).toBe('PVE');
|
||||
expect(
|
||||
getPhysicalDiskSourceBadgePresentation({
|
||||
|
|
|
|||
|
|
@ -96,25 +96,28 @@ export const PHYSICAL_DISK_TABLE_BODY_CLASS = 'divide-y divide-border';
|
|||
export const PHYSICAL_DISK_TABLE_ROW_CLASS = 'cursor-pointer transition-colors';
|
||||
export const PHYSICAL_DISK_TABLE_ROW_SELECTED_CLASS = 'bg-blue-50 dark:bg-blue-900';
|
||||
export const PHYSICAL_DISK_TABLE_ROW_HOVER_CLASS = 'hover:bg-surface-hover';
|
||||
export const PHYSICAL_DISK_TABLE_ROW_STYLE = { height: '38px' } as const;
|
||||
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-[42%] sm:w-[28%] md:w-[28%] xl:w-[22%]';
|
||||
export const PHYSICAL_DISK_COL_SOURCE_CLASS = 'w-[15%] sm:w-[11%] md:w-[9%] xl:w-[8%]';
|
||||
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-[18%] md:w-[14%] xl:w-[12%]';
|
||||
export const PHYSICAL_DISK_COL_ROLE_CLASS = 'hidden xl:table-column xl:w-[10%]';
|
||||
export const PHYSICAL_DISK_COL_PARENT_CLASS = 'hidden xl:table-column xl:w-[13%]';
|
||||
export const PHYSICAL_DISK_COL_HEALTH_CLASS = 'w-[25%] sm:w-[24%] md:w-[20%] xl:w-[16%]';
|
||||
export const PHYSICAL_DISK_COL_TEMP_CLASS = 'hidden md:table-column md:w-[9%] xl:w-[7%]';
|
||||
export const PHYSICAL_DISK_COL_SIZE_CLASS = 'w-[18%] sm:w-[19%] md:w-[20%] xl:w-[12%]';
|
||||
'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-[18%] md:w-[14%] xl:w-[12%]';
|
||||
const PHYSICAL_DISK_CELL_ROLE_RESPONSIVE_CLASS = 'hidden xl:table-cell xl:w-[10%]';
|
||||
const PHYSICAL_DISK_CELL_PARENT_RESPONSIVE_CLASS = 'hidden xl:table-cell xl:w-[13%]';
|
||||
const PHYSICAL_DISK_CELL_TEMP_RESPONSIVE_CLASS = 'hidden md:table-cell md:w-[9%] xl:w-[7%]';
|
||||
'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 const PHYSICAL_DISK_HEADER_DISK_CLASS = `${PHYSICAL_DISK_TABLE_HEADER_CLASS} ${PHYSICAL_DISK_COL_DISK_CLASS}`;
|
||||
export const PHYSICAL_DISK_HEADER_SOURCE_CLASS = `${PHYSICAL_DISK_TABLE_HEADER_CLASS} ${PHYSICAL_DISK_COL_SOURCE_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
|
||||
// `table-column`, which is for `<col>` and does not render `<th>`
|
||||
// content), otherwise the cells render but the column header is blank.
|
||||
|
|
@ -122,14 +125,16 @@ export const PHYSICAL_DISK_HEADER_HOST_CLASS = `${PHYSICAL_DISK_TABLE_HEADER_CLA
|
|||
export const PHYSICAL_DISK_HEADER_ROLE_CLASS = `${PHYSICAL_DISK_TABLE_HEADER_CLASS} ${PHYSICAL_DISK_CELL_ROLE_RESPONSIVE_CLASS}`;
|
||||
export const PHYSICAL_DISK_HEADER_PARENT_CLASS = `${PHYSICAL_DISK_TABLE_HEADER_CLASS} ${PHYSICAL_DISK_CELL_PARENT_RESPONSIVE_CLASS}`;
|
||||
export const PHYSICAL_DISK_HEADER_HEALTH_CLASS = `${PHYSICAL_DISK_TABLE_HEADER_CLASS} ${PHYSICAL_DISK_COL_HEALTH_CLASS}`;
|
||||
export const PHYSICAL_DISK_HEADER_LIFE_CLASS = `${PHYSICAL_DISK_TABLE_HEADER_CLASS} ${PHYSICAL_DISK_CELL_LIFE_RESPONSIVE_CLASS}`;
|
||||
export const PHYSICAL_DISK_HEADER_TEMP_CLASS = `${PHYSICAL_DISK_TABLE_HEADER_CLASS} ${PHYSICAL_DISK_CELL_TEMP_RESPONSIVE_CLASS}`;
|
||||
export const PHYSICAL_DISK_HEADER_SIZE_CLASS = `${PHYSICAL_DISK_TABLE_HEADER_CLASS} ${PHYSICAL_DISK_COL_SIZE_CLASS}`;
|
||||
export const PHYSICAL_DISK_CELL_DISK_CLASS = `${PHYSICAL_DISK_COL_DISK_CLASS} overflow-hidden px-1 sm:px-1.5 lg:px-2 py-1 align-middle text-xs`;
|
||||
export const PHYSICAL_DISK_CELL_SOURCE_CLASS = `${PHYSICAL_DISK_COL_SOURCE_CLASS} overflow-hidden px-1 sm:px-1.5 lg:px-2 py-1 align-middle text-xs`;
|
||||
export const PHYSICAL_DISK_CELL_DEVICE_CLASS = `${PHYSICAL_DISK_CELL_DEVICE_RESPONSIVE_CLASS} overflow-hidden px-1 sm:px-1.5 lg:px-2 py-1 align-middle text-xs`;
|
||||
export const PHYSICAL_DISK_CELL_HOST_CLASS = `${PHYSICAL_DISK_CELL_HOST_RESPONSIVE_CLASS} overflow-hidden px-1 sm:px-1.5 lg:px-2 py-1 align-middle text-xs`;
|
||||
export const PHYSICAL_DISK_CELL_ROLE_CLASS = `${PHYSICAL_DISK_CELL_ROLE_RESPONSIVE_CLASS} overflow-hidden px-1 sm:px-1.5 lg:px-2 py-1 align-middle text-xs`;
|
||||
export const PHYSICAL_DISK_CELL_PARENT_CLASS = `${PHYSICAL_DISK_CELL_PARENT_RESPONSIVE_CLASS} overflow-hidden px-1 sm:px-1.5 lg:px-2 py-1 align-middle text-xs`;
|
||||
export const PHYSICAL_DISK_CELL_HEALTH_CLASS = `${PHYSICAL_DISK_COL_HEALTH_CLASS} overflow-hidden px-1 sm:px-1.5 lg:px-2 py-1 align-middle text-xs`;
|
||||
export const PHYSICAL_DISK_CELL_LIFE_CLASS = `${PHYSICAL_DISK_CELL_LIFE_RESPONSIVE_CLASS} overflow-hidden px-1 sm:px-1.5 lg:px-2 py-1 align-middle text-xs whitespace-nowrap`;
|
||||
export const PHYSICAL_DISK_CELL_TEMP_CLASS = `${PHYSICAL_DISK_CELL_TEMP_RESPONSIVE_CLASS} overflow-hidden px-1 sm:px-1.5 lg:px-2 py-1 align-middle text-xs whitespace-nowrap`;
|
||||
export const PHYSICAL_DISK_CELL_SIZE_CLASS = `${PHYSICAL_DISK_COL_SIZE_CLASS} overflow-hidden px-1 sm:px-1.5 lg:px-2 py-1 align-middle text-xs whitespace-nowrap`;
|
||||
export const PHYSICAL_DISK_NAME_WRAP_CLASS = 'flex min-w-0 items-center gap-1.5 whitespace-nowrap';
|
||||
|
|
@ -138,7 +143,10 @@ export const PHYSICAL_DISK_NAME_TEXT_CLASS =
|
|||
export const PHYSICAL_DISK_SOURCE_BADGE_CLASS =
|
||||
'inline-flex max-w-full min-w-0 justify-center overflow-hidden text-ellipsis px-1 sm:px-1.5 py-px text-[9px] font-medium';
|
||||
export const PHYSICAL_DISK_VALUE_TEXT_CLASS = 'block truncate text-[11px] text-base-content';
|
||||
export const PHYSICAL_DISK_DEVICE_TEXT_CLASS =
|
||||
'block truncate font-mono text-[11px] text-base-content';
|
||||
export const PHYSICAL_DISK_MUTED_PLACEHOLDER_CLASS = 'text-[11px] text-muted';
|
||||
export const PHYSICAL_DISK_LIFE_CLASS = 'text-[11px] font-medium';
|
||||
export const PHYSICAL_DISK_HEALTH_WRAP_CLASS =
|
||||
'flex min-w-0 items-center gap-1.5 whitespace-nowrap';
|
||||
export const PHYSICAL_DISK_HEALTH_LABEL_CLASS = 'min-w-0 truncate text-[11px] font-semibold';
|
||||
|
|
@ -568,9 +576,28 @@ export function getPhysicalDiskRoleLabel(disk: PhysicalDiskPresentationData): st
|
|||
|
||||
export function getPhysicalDiskParentLabel(disk: PhysicalDiskPresentationData): string {
|
||||
if (disk.storageGroup?.trim()) return disk.storageGroup.trim();
|
||||
// Proxmox reports a usage string ("ZFS", "ext4", "BIOS boot", "LVM", ...)
|
||||
// for disks it cannot map to a named pool; better than an empty cell.
|
||||
const used = disk.used?.trim() || '';
|
||||
if (used && used.toLowerCase() !== 'unknown') return used;
|
||||
return '';
|
||||
}
|
||||
|
||||
export function getPhysicalDiskLifeLabel(disk: PhysicalDiskPresentationData): string {
|
||||
// Wearout is "% life remaining" (100 = new); 0 and -1 both mean unreported.
|
||||
if (typeof disk.wearout !== 'number' || disk.wearout <= 0) return '';
|
||||
return `${Math.min(disk.wearout, 100)}%`;
|
||||
}
|
||||
|
||||
export function getPhysicalDiskLifeTextClass(disk: PhysicalDiskPresentationData): string {
|
||||
if (typeof disk.wearout !== 'number' || disk.wearout <= 0) {
|
||||
return PHYSICAL_DISK_MUTED_PLACEHOLDER_CLASS;
|
||||
}
|
||||
if (disk.wearout < 20) return 'text-red-600 dark:text-red-400';
|
||||
if (disk.wearout < 50) return 'text-amber-600 dark:text-amber-400';
|
||||
return 'text-green-600 dark:text-green-400';
|
||||
}
|
||||
|
||||
export function getPhysicalDiskRoleFilterValue(disk: PhysicalDiskPresentationData): string {
|
||||
return normalizePhysicalDiskFacetFilter(getPhysicalDiskRoleLabel(disk));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1238,7 +1238,10 @@ describe('frontend resource type boundaries', () => {
|
|||
expect(diskListSource).toContain('getPhysicalDiskEmptyStatePresentation');
|
||||
expect(diskListSource).toContain('getPhysicalDiskRoleLabel');
|
||||
expect(diskListSource).toContain('getPhysicalDiskParentLabel');
|
||||
expect(diskListSource).toContain('getPhysicalDiskSourceBadgePresentation');
|
||||
expect(diskListSource).toContain('getPhysicalDiskLifeLabel');
|
||||
// The source badge renders in the expansion (DiskDetail), not the table.
|
||||
expect(diskListSource).not.toContain('getPhysicalDiskSourceBadgePresentation');
|
||||
expect(diskDetailSource).toContain('getPhysicalDiskSourceBadgePresentation');
|
||||
expect(diskListSource).not.toContain('const titleize =');
|
||||
expect(diskListSource).not.toContain('const platformLabel =');
|
||||
expect(diskListSource).not.toContain('function hasSmartWarning(');
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue