diff --git a/frontend-modern/src/features/platformPage/__tests__/platformOverviewLayout.guardrails.test.ts b/frontend-modern/src/features/platformPage/__tests__/platformOverviewLayout.guardrails.test.ts index cc93bb1deb..712ffd3372 100644 --- a/frontend-modern/src/features/platformPage/__tests__/platformOverviewLayout.guardrails.test.ts +++ b/frontend-modern/src/features/platformPage/__tests__/platformOverviewLayout.guardrails.test.ts @@ -169,6 +169,7 @@ describe('platform overview layout guardrails', () => { expect(truenasPageSurfaceSource).toContain(' - +
diff --git a/frontend-modern/src/features/truenas/TrueNASPageSurface.tsx b/frontend-modern/src/features/truenas/TrueNASPageSurface.tsx index 906daefc5c..fac5aaa332 100644 --- a/frontend-modern/src/features/truenas/TrueNASPageSurface.tsx +++ b/frontend-modern/src/features/truenas/TrueNASPageSurface.tsx @@ -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(TRUENAS_TAB_SPECS.map((tab) => tab.id)); diff --git a/frontend-modern/src/features/truenas/TrueNASServicesTable.tsx b/frontend-modern/src/features/truenas/TrueNASServicesTable.tsx index c5e7532a8c..d6d2d49e8c 100644 --- a/frontend-modern/src/features/truenas/TrueNASServicesTable.tsx +++ b/frontend-modern/src/features/truenas/TrueNASServicesTable.tsx @@ -169,13 +169,8 @@ export const TrueNASServicesTable: Component<{ variant={serviceStatusVariant(status())} title={titleCase(status())} /> -
-
- {formatServiceName(row.service.service)} -
-
- {asTrimmedString(row.service.id) || row.id} -
+
+ {formatServiceName(row.service.service)}
@@ -203,7 +198,6 @@ export const TrueNASServicesTable: Component<{
{row.systemName}
-
{row.systemId}
); diff --git a/frontend-modern/src/features/truenas/TrueNASSystemsTable.tsx b/frontend-modern/src/features/truenas/TrueNASSystemsTable.tsx index 49a766247e..441c6780b9 100644 --- a/frontend-modern/src/features/truenas/TrueNASSystemsTable.tsx +++ b/frontend-modern/src/features/truenas/TrueNASSystemsTable.tsx @@ -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<{ > -
+
- {/* - 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. - */} - + System - - + CPU - + Memory - - Storage + + Capacity - - - - @@ -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 /> - - {name()} - +
+
+ {name()} +
+ +
+ {systemMeta()} +
+
+
- @@ -339,42 +337,34 @@ export const TrueNASSystemsTable: Component<{ {formatTemperature(system.temperature)} - - - - @@ -383,7 +373,7 @@ export const TrueNASSystemsTable: Component<{ resource={system} open={isExpanded()} detailRowId={detailRowId()} - colSpan={14} + colSpan={9} resolveResourceLabel={resolveResourceLabel} onClose={() => drawer.close(system)} />