Tighten TrueNAS native inventory tables

- Scope the TrueNAS resource fetch to native TrueNAS and agent-origin system rows
- Group overview inventory counts to keep the table scannable without horizontal overflow
- Remove internal service/resource ids from visible TrueNAS service rows
This commit is contained in:
rcourtman
2026-05-21 16:34:38 +01:00
parent 1118e09fcd
commit 946ebfde1d
5 changed files with 81 additions and 93 deletions
@@ -169,6 +169,7 @@ describe('platform overview layout guardrails', () => {
expect(truenasPageSurfaceSource).toContain('<TrueNASStorageTopologyTable');
expect(truenasPageSurfaceSource).toContain('<TrueNASProtection');
expect(truenasPageSurfaceSource).toContain('platform: TRUENAS_PLATFORM_FILTER');
expect(truenasPageSurfaceSource).toContain('source=truenas,agent');
expect(truenasPageSurfaceSource).not.toContain('forcedView="pools"');
expect(truenasPageSurfaceSource).not.toContain('<RecoverySurface');
expect(truenasPageSurfaceSource).not.toContain('<StorageSurface');
@@ -173,7 +173,7 @@ export const TrueNASAlertsTable: Component<{
>
<TableCard class={PLATFORM_TABLE_CARD_CLASS}>
<TableCardHeader title="Health Alerts" />
<Table class="min-w-full table-fixed text-xs md:min-w-[1120px]">
<Table class="min-w-full table-fixed text-xs md:min-w-[960px]">
<TableHeader>
<TableRow class={PLATFORM_TABLE_HEADER_ROW_CLASS}>
<TableHead class={`${getPlatformTableHeadClassForKind('name')} md:w-[20%]`}>
@@ -28,8 +28,11 @@ import {
// (with `storage.topology` differentiating them) — they are not
// first-class type tokens and including them triggers a 400 from
// `/api/resources`. The page model still buckets by topology
// client-side.
const TRUENAS_RESOURCE_QUERY = 'type=agent,vm,app-container,network-share,storage,physical_disk';
// client-side. Keep `agent` in the source filter so a TrueNAS host that
// reports through the Pulse agent can still appear as the top-level appliance
// while native TrueNAS API inventory remains the primary source.
const TRUENAS_RESOURCE_QUERY =
'source=truenas,agent&type=agent,vm,app-container,network-share,storage,physical_disk';
const TRUENAS_PLATFORM_FILTER = 'truenas';
const VALID_TABS = new Set<TrueNASPageTabId>(TRUENAS_TAB_SPECS.map((tab) => tab.id));
@@ -169,13 +169,8 @@ export const TrueNASServicesTable: Component<{
variant={serviceStatusVariant(status())}
title={titleCase(status())}
/>
<div class="min-w-0">
<div class="truncate font-medium text-base-content">
{formatServiceName(row.service.service)}
</div>
<div class="truncate text-[11px] text-muted">
{asTrimmedString(row.service.id) || row.id}
</div>
<div class="min-w-0 truncate font-medium text-base-content">
{formatServiceName(row.service.service)}
</div>
</div>
</TableCell>
@@ -203,7 +198,6 @@ export const TrueNASServicesTable: Component<{
</TableCell>
<TableCell class={getPlatformTableCellClassForKind('text')}>
<div class="truncate text-base-content">{row.systemName}</div>
<div class="truncate text-[11px] text-muted">{row.systemId}</div>
</TableCell>
</TableRow>
);
@@ -43,8 +43,10 @@ import type { Resource } from '@/types/resource';
// TrueNAS systems are storage appliances, not generic compute hosts.
// The generic infrastructure table's CPU / Memory / Disk columns are
// helpful (the agent payload carries them), but operators also want at-
// a-glance pool count, dataset count, share count, VM count, app count,
// uptime, version, and max-sensor temperature on the same row. This bespoke table reuses
// a-glance native inventory on the same row. Keep that inventory grouped
// instead of giving every count a separate column; the Overview page should
// scan as an appliance summary, while the dedicated tabs own full detail.
// This bespoke table reuses
// canonical shared primitives (Card, Table, SearchInput,
// FilterButtonGroup, StatusDot) and counts the per-system children
// client-side from the same TrueNAS resource scope already fetched by
@@ -104,6 +106,21 @@ const EMPTY_COUNTS: TrueNASSystemChildCounts = {
services: 0,
};
const plural = (count: number, singular: string): string =>
`${count} ${count === 1 ? singular : `${singular}s`}`;
const storageInventoryPrimary = (counts: TrueNASSystemChildCounts): string =>
`${plural(counts.pools, 'pool')} / ${plural(counts.datasets, 'dataset')}`;
const storageInventorySecondary = (counts: TrueNASSystemChildCounts): string =>
plural(counts.disks, 'disk');
const workloadInventoryPrimary = (counts: TrueNASSystemChildCounts): string =>
plural(counts.vms, 'VM');
const workloadInventorySecondary = (counts: TrueNASSystemChildCounts): string =>
plural(counts.apps, 'app');
export const TrueNASSystemsTable: Component<{
systems: Resource[];
// Full TrueNAS resource scope so we can count pools / datasets / apps
@@ -164,75 +181,43 @@ export const TrueNASSystemsTable: Component<{
>
<TableCard class={PLATFORM_TABLE_CARD_CLASS}>
<TableCardHeader title={props.title ?? 'Systems'} />
<Table class="min-w-full table-fixed text-xs md:min-w-[1400px]">
<Table class="min-w-full table-fixed text-xs md:min-w-[960px]">
<TableHeader>
<TableRow class={PLATFORM_TABLE_HEADER_ROW_CLASS}>
{/*
Desktop widths balance the bar-metric columns (CPU /
Memory / Storage) against the short integer-count columns
(Pools / Datasets / Shares / VMs / Disks / Apps /
Services) and give Version the room it needs for full
"TrueNAS-SCALE-24.10.2"-style labels. Mobile widths are unchanged.
*/}
<TableHead class={`${getPlatformTableHeadClassForKind('name')} md:w-[10%]`}>
<TableHead class={`${getPlatformTableHeadClassForKind('name')} md:w-[17%]`}>
System
</TableHead>
<TableHead
class={`${getPlatformTableHeadClassForKind('text')} hidden md:table-cell md:w-[12%]`}
>
Version
</TableHead>
<TableHead class={`${getPlatformTableHeadClassForKind('metric-bar')} md:w-[9%]`}>
<TableHead class={`${getPlatformTableHeadClassForKind('metric-bar')} md:w-[10%]`}>
CPU
</TableHead>
<TableHead class={`${getPlatformTableHeadClassForKind('metric-bar')} md:w-[9%]`}>
<TableHead class={`${getPlatformTableHeadClassForKind('metric-bar')} md:w-[10%]`}>
Memory
</TableHead>
<TableHead class={`${getPlatformTableHeadClassForKind('metric-bar')} md:w-[10%]`}>
Storage
<TableHead class={`${getPlatformTableHeadClassForKind('metric-bar')} md:w-[13%]`}>
Capacity
</TableHead>
<TableHead
class={`${getPlatformTableHeadClassForKind('numeric-value')} hidden md:table-cell md:w-[5%]`}
class={`${getPlatformTableHeadClassForKind('numeric-value')} hidden md:table-cell md:w-[6%]`}
>
Temp
</TableHead>
<TableHead
class={`${getPlatformTableHeadClassForKind('numeric-value')} hidden md:table-cell md:w-[5%]`}
class={`${getPlatformTableHeadClassForKind('text')} hidden lg:table-cell lg:w-[15%]`}
>
Uptime
Inventory
</TableHead>
<TableHead
class={`${getPlatformTableHeadClassForKind('numeric-value')} hidden md:table-cell md:w-[5%]`}
>
Pools
</TableHead>
<TableHead
class={`${getPlatformTableHeadClassForKind('numeric-value')} hidden md:table-cell md:w-[7%]`}
>
Datasets
</TableHead>
<TableHead
class={`${getPlatformTableHeadClassForKind('numeric-value')} hidden md:table-cell md:w-[6%]`}
class={`${getPlatformTableHeadClassForKind('numeric-value')} hidden lg:table-cell lg:w-[8%]`}
>
Shares
</TableHead>
<TableHead
class={`${getPlatformTableHeadClassForKind('numeric-value')} hidden md:table-cell md:w-[5%]`}
class={`${getPlatformTableHeadClassForKind('text')} hidden lg:table-cell lg:w-[10%]`}
>
VMs
VMs / Apps
</TableHead>
<TableHead
class={`${getPlatformTableHeadClassForKind('numeric-value')} hidden md:table-cell md:w-[5%]`}
>
Disks
</TableHead>
<TableHead
class={`${getPlatformTableHeadClassForKind('numeric-value')} hidden md:table-cell md:w-[6%]`}
>
Apps
</TableHead>
<TableHead
class={`${getPlatformTableHeadClassForKind('numeric-value')} hidden md:table-cell md:w-[6%]`}
class={`${getPlatformTableHeadClassForKind('numeric-value')} hidden lg:table-cell lg:w-[10%]`}
>
Services
</TableHead>
@@ -261,6 +246,14 @@ export const TrueNASSystemsTable: Component<{
? `${formatBytes(system.disk.used)} / ${formatBytes(system.disk.total)}`
: formatPercent(storagePercent());
const c = () => countsBySystem().get(system.id) ?? EMPTY_COUNTS;
const uptimeLabel = () => formatUptime(system.uptime);
const systemMeta = () =>
[
version() !== '—' ? version() : '',
uptimeLabel() !== '—' ? `up ${uptimeLabel()}` : '',
]
.filter(Boolean)
.join(' · ');
const metricsKey = () => buildMetricKeyForUnifiedResource(system);
const cpuPercent = () => finiteMetric(system.cpu?.current);
const memoryTotal = () => finiteMetric(system.memory?.total) ?? 0;
@@ -291,16 +284,21 @@ export const TrueNASSystemsTable: Component<{
title={system.status || 'unknown'}
ariaHidden
/>
<span class="truncate font-semibold text-base-content" title={name()}>
{name()}
</span>
<div class="min-w-0">
<div
class="truncate font-semibold text-base-content"
title={name()}
>
{name()}
</div>
<Show when={systemMeta()}>
<div class="truncate text-[11px] text-muted" title={systemMeta()}>
{systemMeta()}
</div>
</Show>
</div>
</div>
</TableCell>
<TableCell
class={`${getPlatformTableCellClassForKind('text')} hidden font-mono text-[11px] text-base-content md:table-cell`}
>
{version()}
</TableCell>
<TableCell
class={`${getPlatformTableCellClassForKind('metric-bar')} w-[20%] md:w-auto`}
>
@@ -339,42 +337,34 @@ export const TrueNASSystemsTable: Component<{
{formatTemperature(system.temperature)}
</TableCell>
<TableCell
class={`${getPlatformTableCellClassForKind('numeric-value')} hidden text-base-content md:table-cell`}
class={`${getPlatformTableCellClassForKind('text')} hidden whitespace-normal text-base-content lg:table-cell`}
title={`${storageInventoryPrimary(c())} · ${storageInventorySecondary(c())}`}
>
{formatUptime(system.uptime)}
<div class="leading-tight">
<div class="truncate">{storageInventoryPrimary(c())}</div>
<div class="truncate text-[11px] text-muted">
{storageInventorySecondary(c())}
</div>
</div>
</TableCell>
<TableCell
class={`${getPlatformTableCellClassForKind('numeric-value')} hidden text-base-content tabular-nums md:table-cell`}
>
{c().pools}
</TableCell>
<TableCell
class={`${getPlatformTableCellClassForKind('numeric-value')} hidden text-base-content tabular-nums md:table-cell`}
>
{c().datasets}
</TableCell>
<TableCell
class={`${getPlatformTableCellClassForKind('numeric-value')} hidden text-base-content tabular-nums md:table-cell`}
class={`${getPlatformTableCellClassForKind('numeric-value')} hidden text-base-content tabular-nums lg:table-cell`}
>
{c().shares}
</TableCell>
<TableCell
class={`${getPlatformTableCellClassForKind('numeric-value')} hidden text-base-content tabular-nums md:table-cell`}
class={`${getPlatformTableCellClassForKind('text')} hidden whitespace-normal text-base-content lg:table-cell`}
title={`${workloadInventoryPrimary(c())} · ${workloadInventorySecondary(c())}`}
>
{c().vms}
<div class="leading-tight">
<div class="truncate">{workloadInventoryPrimary(c())}</div>
<div class="truncate text-[11px] text-muted">
{workloadInventorySecondary(c())}
</div>
</div>
</TableCell>
<TableCell
class={`${getPlatformTableCellClassForKind('numeric-value')} hidden text-base-content tabular-nums md:table-cell`}
>
{c().disks}
</TableCell>
<TableCell
class={`${getPlatformTableCellClassForKind('numeric-value')} hidden text-base-content tabular-nums md:table-cell`}
>
{c().apps}
</TableCell>
<TableCell
class={`${getPlatformTableCellClassForKind('numeric-value')} hidden text-base-content tabular-nums md:table-cell`}
class={`${getPlatformTableCellClassForKind('numeric-value')} hidden text-base-content tabular-nums lg:table-cell`}
>
{c().services}
</TableCell>
@@ -383,7 +373,7 @@ export const TrueNASSystemsTable: Component<{
resource={system}
open={isExpanded()}
detailRowId={detailRowId()}
colSpan={14}
colSpan={9}
resolveResourceLabel={resolveResourceLabel}
onClose={() => drawer.close(system)}
/>