From f30be8b48f71e1c06e9ea269133a2e899ce22602 Mon Sep 17 00:00:00 2001 From: rcourtman <8825017+rcourtman@users.noreply.github.com> Date: Mon, 5 Oct 2026 06:13:52 +0100 Subject: [PATCH] Say which TrueNAS services and VMs should be running The Services tab gave a red dot to SMART with no reason, beside the same Stopped pill as SSH, and spent a column on process IDs. The VMs tab spent two columns on the bootloader and device counts. None of those columns answers whether something is wrong. A service stopped while set to start at boot, and a VM set to autostart that is stopped, now say "should be running" beside the state, with the full sentence on hover. Services also gain a drawer Condition row, since the State column is hidden on phones. A VM stopped without autostart gets no note, because someone stopped it on purpose. Process IDs, bootloader and device counts stay in the drawers. Rows stay single-line, as the shared platform-table rhythm requires. --- .../subsystems/frontend-primitives.md | 6 +- .../internal/subsystems/unified-resources.md | 8 ++ .../646d2eed2a5b8d31.json | 43 ++++++ .../resourceDetailDrawerTrueNASModel.test.ts | 46 ++++++ .../resourceDetailDrawerTrueNASModel.ts | 24 +++- .../platformOverviewLayout.guardrails.test.ts | 2 +- .../features/truenas/TrueNASServicesTable.tsx | 116 ++++++++++----- .../truenas/TrueNASVirtualMachinesTable.tsx | 134 +++++++++--------- .../__tests__/TrueNASServicesTable.test.tsx | 51 +++++++ .../TrueNASVirtualMachinesTable.test.tsx | 67 +++++++++ 10 files changed, 389 insertions(+), 108 deletions(-) create mode 100644 frontend-modern/browser-verification/646d2eed2a5b8d31.json create mode 100644 frontend-modern/src/features/truenas/__tests__/TrueNASVirtualMachinesTable.test.tsx diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md index 30fe40df19..e1a0659ebf 100644 --- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md +++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md @@ -2282,7 +2282,11 @@ not a replacement status card, CTA band, or page-local nested card. inline detail rows, which remain content-sized. Summary cells must render one line only: secondary identity, raw provider labels, and descriptive context belong in an existing operational column, a supplemental tooltip, - or its inline detail drawer rather than a stacked subtitle. Text-only + or its inline detail drawer rather than a stacked subtitle. The same holds + for a note that explains a row's status dot: a TrueNAS service stopped while + set to start at boot says `should be running` beside its state in the State + cell, with the full sentence in the title and the drawer, not on a second + line under the name. Text-only operational rows may use `getPlatformTableRowClass` to declare that same shared rhythm explicitly; providers must not introduce local row-height exceptions. Mobile truncation may rely on a full-value row diff --git a/docs/release-control/v6/internal/subsystems/unified-resources.md b/docs/release-control/v6/internal/subsystems/unified-resources.md index 737fdef6f7..e1d02270e6 100644 --- a/docs/release-control/v6/internal/subsystems/unified-resources.md +++ b/docs/release-control/v6/internal/subsystems/unified-resources.md @@ -1685,6 +1685,14 @@ cannot create a browser mutation. `TrueNASData.Services` on the canonical top-level `agent` resource, preserving service name, boot enablement, runtime state, and process IDs through clone, merge, transport, and frontend decode paths. + The TrueNAS Services and VMs tables lead with condition rather than + configuration. Process IDs, bootloader, and device counts stay in the row + drawer instead of taking table columns. A service stopped while set to + start at boot, and a stopped VM set to autostart, say `should be running` + in the State cell beside the raw state, with the full sentence in the + title and a drawer Condition row (the VM drawer summary says it too) because + State is hidden on phones. A stopped VM without autostart carries no note: it was stopped on + purpose. Rows stay single-line, per the shared platform-table rhythm. Docker / Podman inventory extends that same canonical type contract beyond generic workload rows and Swarm services. Runtime containers must preserve Docker Engine container identity, owning host/runtime context, image, diff --git a/frontend-modern/browser-verification/646d2eed2a5b8d31.json b/frontend-modern/browser-verification/646d2eed2a5b8d31.json new file mode 100644 index 0000000000..a79c2cf895 --- /dev/null +++ b/frontend-modern/browser-verification/646d2eed2a5b8d31.json @@ -0,0 +1,43 @@ +{ + "version": 1, + "base_sha": "94ac386abfe13a8eb0109ac1eea4c087390f9717", + "verified_at": "2026-10-05T05:13:41Z", + "result": "passed", + "changed_paths": [ + "frontend-modern/src/components/Infrastructure/resourceDetailDrawerTrueNASModel.ts", + "frontend-modern/src/features/truenas/TrueNASServicesTable.tsx", + "frontend-modern/src/features/truenas/TrueNASVirtualMachinesTable.tsx" + ], + "content_sha256": { + "frontend-modern/src/components/Infrastructure/resourceDetailDrawerTrueNASModel.ts": "27d50ea6c727d0d91ae34e2427e353abfa39a9306c665fc11d0be41a5fa2eabc", + "frontend-modern/src/features/truenas/TrueNASServicesTable.tsx": "cbe56cad46b968c9238c70beb5e77f98cbaec9625935ad6ccda735b2ba5c6f43", + "frontend-modern/src/features/truenas/TrueNASVirtualMachinesTable.tsx": "b9e91b16bb7aeba6f2a02c544258babfd0b675171eac107a2f88622b2dadd5a7" + }, + "routes": [ + "/truenas/services", + "/truenas/vms" + ], + "viewports": [ + { + "width": 1440, + "height": 900 + }, + { + "width": 390, + "height": 844 + } + ], + "states": [ + "Services at 1440: columns are Service, State, Boot, System. SMART reads Stopped plus red should be running in the State cell with the full sentence on hover, SSH (stopped, boot disabled) has no note, NFS and SMB unchanged. Before, SMART and SSH showed the same Stopped pill and a PIDs column took 20 percent", + "Services at 390: rows stay single-line with Service and Boot, and tapping SMART opens the drawer with Condition Should be running between State and Boot, where before the red dot had no reason anywhere on a phone", + "Service drawer still lists PIDs and PID count for SMB", + "VMs at 1440: columns are VM, State, CPU, Memory, Flags. ubuntu-build (stopped, no autostart) carries no note. Bootloader and device counts remain in the drawer under Platform details, Show TrueNAS", + "VMs at 390: VM, CPU and memory, single-line rows", + "The mock estate has no stopped VM set to autostart, so that note and its drawer Condition row are covered by TrueNASVirtualMachinesTable.test.tsx and resourceDetailDrawerTrueNASModel.test.ts rather than seen live" + ], + "interactions": [ + "Tapped the SMART row on a 390 phone and read the drawer", + "Expanded ubuntu-build and the Platform details TrueNAS section to confirm bootloader and devices remain", + "Expanded the SMB service drawer to confirm PIDs remain" + ] +} diff --git a/frontend-modern/src/components/Infrastructure/__tests__/resourceDetailDrawerTrueNASModel.test.ts b/frontend-modern/src/components/Infrastructure/__tests__/resourceDetailDrawerTrueNASModel.test.ts index eccd60937f..539aee7b89 100644 --- a/frontend-modern/src/components/Infrastructure/__tests__/resourceDetailDrawerTrueNASModel.test.ts +++ b/frontend-modern/src/components/Infrastructure/__tests__/resourceDetailDrawerTrueNASModel.test.ts @@ -86,6 +86,52 @@ describe('resourceDetailDrawerTrueNASModel', () => { ]); }); + it('says a stopped VM set to autostart should be running', () => { + const stoppedOnPurpose = baseResource({ + type: 'vm', + truenas: { vm: { name: 'ubuntu-build', state: 'STOPPED', vcpus: 2, autostart: false } }, + }); + const shouldRun = baseResource({ + type: 'vm', + truenas: { vm: { name: 'router', state: 'STOPPED', vcpus: 2, autostart: true } }, + }); + + expect(buildTrueNASDetailsSummary(stoppedOnPurpose)).toBe('Stopped, 2 vCPU'); + expect(buildTrueNASDetailsSummary(shouldRun)).toBe('Stopped, should be running, 2 vCPU'); + const compute = buildTrueNASDetailSections(shouldRun).find( + (section) => section.label === 'Compute', + ); + expect(compute?.rows).toEqual( + expect.arrayContaining([ + expect.objectContaining({ label: 'Condition', value: 'Should be running' }), + ]), + ); + const onPurposeCompute = buildTrueNASDetailSections(stoppedOnPurpose).find( + (section) => section.label === 'Compute', + ); + expect(onPurposeCompute?.rows.map((row) => row.label)).not.toContain('Condition'); + + // The same rule as the VMs table: libvirt aliases, the resource status + // when TrueNAS sent no state, and no claim while TrueNAS data is stale. + const shutOff = baseResource({ + type: 'vm', + truenas: { vm: { name: 'router', domainState: 'SHUTOFF', autostart: true } }, + }); + const statusOnly = baseResource({ + type: 'vm', + status: 'offline', + truenas: { vm: { name: 'router', autostart: true } }, + }); + const stale = baseResource({ + type: 'vm', + truenas: { vm: { name: 'router', state: 'STOPPED', autostart: true } }, + platformData: { sourceStatus: { truenas: { status: 'stale' } } }, + }); + expect(buildTrueNASDetailsSummary(shutOff)).toContain('should be running'); + expect(buildTrueNASDetailsSummary(statusOnly)).toContain('should be running'); + expect(buildTrueNASDetailsSummary(stale)).not.toContain('should be running'); + }); + it('summarizes native app.query metadata for the detail drawer', () => { const resource = baseResource({ type: 'app-container', diff --git a/frontend-modern/src/components/Infrastructure/resourceDetailDrawerTrueNASModel.ts b/frontend-modern/src/components/Infrastructure/resourceDetailDrawerTrueNASModel.ts index 2505d4dabc..f8a666aa5d 100644 --- a/frontend-modern/src/components/Infrastructure/resourceDetailDrawerTrueNASModel.ts +++ b/frontend-modern/src/components/Infrastructure/resourceDetailDrawerTrueNASModel.ts @@ -20,6 +20,7 @@ import { type DetailSection, type DetailValueTone, } from '@/components/shared/detailSectionModel'; +import { hasImpairedResourceSource } from '@/utils/resourceSourceHealth'; export type ResourceDetailDrawerTrueNASRowTone = DetailValueTone; @@ -580,7 +581,25 @@ const formatVMTopology = (vm: ResourceTrueNASVMMeta): string | null => { return null; }; +// Matches the stopped states in mapTrueNASVMStatus (features/truenas). A VM set +// to autostart that is stopped is the one stopped VM nobody chose, and the +// VMs table hides its State note on phones, so the drawer says it too. +const TRUENAS_VM_STOPPED_STATES = new Set(['stopped', 'shutoff', 'shutdown', 'poweroff']); + +// Stale TrueNAS data may hold an old state, so the note waits for a current one. +const vmConditionLabel = (resource: Resource, vm: ResourceTrueNASVMMeta): string | null => { + if (hasImpairedResourceSource(resource, 'truenas')) return null; + if (!vm.autostart) return null; + const state = (asString(vm.state) ?? asString(vm.domainState))?.toLowerCase(); + // Without a native state, the table falls back to the resource status. + const stopped = state + ? TRUENAS_VM_STOPPED_STATES.has(state) + : resource.status === 'offline' || resource.status === 'stopped'; + return stopped ? 'Should be running' : null; +}; + const buildTrueNASVMSections = ( + resource: Resource, vm: ResourceTrueNASVMMeta, ): ResourceDetailDrawerTrueNASSection[] => { const state = normalizeDelimitedLabel(vm.state); @@ -592,6 +611,7 @@ const buildTrueNASVMSections = ( tone: (vm.state ?? vm.domainState)?.toLowerCase() === 'running' ? 'success' : 'warning', }), row('Domain state', sameState ? null : domainState), + row('Condition', vmConditionLabel(resource, vm), { tone: 'warning' }), row('vCPU', formatVMCpu(vm)), row('Topology', formatVMTopology(vm)), row('Memory', formatDetailBytesValue(vm.memoryBytes)), @@ -705,7 +725,7 @@ export const buildTrueNASDetailSections = ( resource: Resource, ): ResourceDetailDrawerTrueNASSection[] => { if (resource.truenas?.share) return buildTrueNASShareSections(resource.truenas.share); - if (resource.truenas?.vm) return buildTrueNASVMSections(resource.truenas.vm); + if (resource.truenas?.vm) return buildTrueNASVMSections(resource, resource.truenas.vm); if (resource.truenas?.app) return buildTrueNASAppSections(resource.truenas.app); if (isTrueNASScopedResource(resource) && resource.storage) { return buildTrueNASStorageSections(resource, resource.storage); @@ -734,8 +754,10 @@ export const buildTrueNASDetailsSummary = (resource: Resource): string | null => const vm = resource.truenas?.vm; if (vm) { const deviceCount = asPositiveNumber(vm.deviceCount); + const condition = vmConditionLabel(resource, vm); const summary = [ normalizeDelimitedLabel(vm.state ?? vm.domainState), + condition?.toLowerCase(), formatVMCpu(vm), formatDetailBytesValue(vm.memoryBytes), deviceCount ? formatDetailCountValue(deviceCount, 'device') : null, 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 b6b814bb92..7100a0385e 100644 --- a/frontend-modern/src/features/platformPage/__tests__/platformOverviewLayout.guardrails.test.ts +++ b/frontend-modern/src/features/platformPage/__tests__/platformOverviewLayout.guardrails.test.ts @@ -548,7 +548,7 @@ describe('platform overview layout guardrails', () => { expect(truenasStorageTopologyTableSource).toContain('md:min-w-[960px]'); expect(truenasVirtualMachinesTableSource).toContain('md:min-w-[960px]'); expect(truenasVirtualMachinesTableSource).toMatch( - /class="hidden sm:table-cell md:w-\[19%\]"[\s\S]{0,120}?Flags/, + /class="hidden sm:table-cell md:w-\[26%\]"[\s\S]{0,120}?Flags/, ); expect(vmwarePageSurfaceSource).toContain(' { return SERVICE_NAME_LABELS[normalized.toLowerCase()] ?? normalized; }; -const formatPIDs = (pids: number[] | undefined): { label: string; title: string } => { - const values = (pids ?? []).filter((pid) => Number.isFinite(pid) && pid > 0); - if (values.length === 0) return { label: '-', title: '' }; - const visible = values.slice(0, 3).map(String); - const suffix = values.length > visible.length ? ` +${values.length - visible.length}` : ''; - return { label: `${visible.join(', ')}${suffix}`, title: values.join(', ') }; +// A service set to start at boot that is not running is the one state a red +// dot alone cannot explain, so the State cell says so beside the raw state. +// Rows stay single-line (the shared platform-table rhythm), so the full +// sentence rides on the title and in the drawer. PIDs are process detail +// nobody acts on from the list, so they live in the drawer too. +type ServiceStateNote = { short: string | null; full: string; tone: 'danger' | 'warning' }; + +const serviceStateNote = ( + row: TrueNASServiceRow, + status: Exclude, +): ServiceStateNote | null => { + if (status === 'stopped') { + return { + short: 'should be running', + full: 'Set to start at boot but not running', + tone: 'danger', + }; + } + if (status !== 'attention') return null; + if (hasImpairedResourceSource(row.system, 'truenas')) { + return { short: 'not updated', full: 'TrueNAS has not updated this recently', tone: 'warning' }; + } + const state = asTrimmedString(row.service.state); + return { + short: state ? null : 'not reported', + full: state + ? `TrueNAS reports ${formatPlatformTableTitleCaseValue(state)}` + : 'TrueNAS has not reported a state', + tone: 'warning', + }; +}; + +// The drawer value cell is one line on a phone, so it carries the short form +// with a capital, and the Boot row beside it says why it should be running. +const serviceConditionLabel = (row: TrueNASServiceRow): string | null => { + const note = serviceStateNote(row, mapTrueNASServiceStatus(row)); + if (!note) return null; + if (!note.short) return note.full; + return note.short.charAt(0).toUpperCase() + note.short.slice(1); +}; + +const SERVICE_STATE_NOTE_CLASS: Record = { + danger: 'text-red-600 dark:text-red-300', + warning: 'text-amber-700 dark:text-amber-300', }; const serviceStatusVariant = ( @@ -124,6 +163,9 @@ const buildServiceDetailSections = (row: TrueNASServiceRow): ServiceDetailSectio detailRow('State', formatPlatformTableTitleCaseValue(row.service.state), { tone: serviceTone(row), }), + detailRow('Condition', serviceConditionLabel(row), { + tone: serviceTone(row), + }), detailRow('Boot', detailBool(row.service.enabled), { tone: row.service.enabled ? 'success' : 'muted', }), @@ -155,9 +197,8 @@ const ServiceDetailTable: Component<{ row: TrueNASServiceRow; onClose: () => voi /> ); -// Columns a user can sort by. PIDs orders on the process count so the busiest -// services surface first. -const TRUENAS_SERVICE_SORT_KEYS = ['service', 'state', 'boot', 'pids', 'system'] as const; +// Columns a user can sort by. +const TRUENAS_SERVICE_SORT_KEYS = ['service', 'state', 'boot', 'system'] as const; type TrueNASServiceSortKey = (typeof TRUENAS_SERVICE_SORT_KEYS)[number]; @@ -172,12 +213,6 @@ const getTrueNASServiceSortValue = ( return asTrimmedString(row.service.state) || null; case 'boot': return row.service.enabled ? 'Enabled' : 'Disabled'; - case 'pids': { - const count = (row.service.pids ?? []).filter( - (pid) => Number.isFinite(pid) && pid > 0, - ).length; - return count > 0 ? count : null; - } case 'system': return asTrimmedString(row.systemName) || null; default: @@ -202,7 +237,6 @@ export const TrueNASServicesTable: Component<{ const sort = createPlatformTableSortState({ storageKey: 'truenasServices', sortKeys: TRUENAS_SERVICE_SORT_KEYS, - descendingFirst: ['pids'], }); const sortedRows = createMemo(() => sort.sortRows(tableState.filtered(), getTrueNASServiceSortValue), @@ -257,7 +291,7 @@ export const TrueNASServicesTable: Component<{ kind="name" sort={sort} sortKey="service" - class="platform-table-mobile-w-30 md:w-[24%]" + class="platform-table-mobile-w-30 md:w-[34%]" > Service @@ -265,7 +299,7 @@ export const TrueNASServicesTable: Component<{ kind="badge" sort={sort} sortKey="state" - class="platform-table-phone-hidden md:w-[14%]" + class="platform-table-phone-hidden md:w-[16%]" > State @@ -273,23 +307,15 @@ export const TrueNASServicesTable: Component<{ kind="badge" sort={sort} sortKey="boot" - class="platform-table-mobile-w-15 md:w-[14%]" + class="platform-table-mobile-w-15 md:w-[16%]" > Boot - - PIDs - System @@ -300,7 +326,7 @@ export const TrueNASServicesTable: Component<{ {(row) => { const status = () => mapTrueNASServiceStatus(row); - const pids = createMemo(() => formatPIDs(row.service.pids)); + const note = () => serviceStateNote(row, status()); const rawState = () => asTrimmedString(row.service.state) || '-'; const detailRowId = () => detail.detailRowId(row); const isExpanded = () => detail.isExpanded(row); @@ -332,9 +358,27 @@ export const TrueNASServicesTable: Component<{ - - {formatPlatformTableTitleCaseValue(rawState())} - +
+ + {formatPlatformTableTitleCaseValue(rawState())} + + + {(short) => ( + + {short()} + + )} + + + {(current) => {current().full}} + +
- - {pids().label} - @@ -362,7 +400,7 @@ export const TrueNASServicesTable: Component<{ diff --git a/frontend-modern/src/features/truenas/TrueNASVirtualMachinesTable.tsx b/frontend-modern/src/features/truenas/TrueNASVirtualMachinesTable.tsx index 21062df9a3..59c13517e9 100644 --- a/frontend-modern/src/features/truenas/TrueNASVirtualMachinesTable.tsx +++ b/frontend-modern/src/features/truenas/TrueNASVirtualMachinesTable.tsx @@ -3,6 +3,7 @@ import { StatusDot } from '@/components/shared/StatusDot'; import { TableCell, TableRow } from '@/components/shared/Table'; import { getSimpleStatusIndicator } from '@/utils/status'; import { asTrimmedString } from '@/utils/stringUtils'; +import { hasImpairedResourceSource } from '@/utils/resourceSourceHealth'; import { PlatformWindowedRows, PlatformResponsiveTableLabel, @@ -30,6 +31,7 @@ import type { Resource, ResourceTrueNASVMMeta } from '@/types/resource'; import { filterTrueNASVMs, getTrueNASResourceDisplayStatus, + mapTrueNASVMStatus, type TrueNASVMStatusFilter, } from './truenasPageModel'; @@ -60,29 +62,6 @@ const formatCPU = (vm: ResourceTrueNASVMMeta | undefined): string => { return '-'; }; -const formatDevices = (vm: ResourceTrueNASVMMeta | undefined): { label: string; title: string } => { - const counts = [ - ['disk', vm?.diskCount], - ['nic', vm?.nicCount], - ['display', vm?.displayCount], - ['cdrom', vm?.cdromCount], - ['usb', vm?.usbCount], - ['pci', vm?.pciCount], - ] as const; - const parts = counts - .filter(([, count]) => typeof count === 'number' && Number.isFinite(count) && count > 0) - .map(([kind, count]) => `${count} ${kind}`); - const total = vm?.deviceCount; - if (parts.length === 0) { - return typeof total === 'number' && total > 0 - ? { label: `${total} devices`, title: `${total} devices` } - : { label: '-', title: '' }; - } - const visible = parts.slice(0, 2); - const suffix = parts.length > visible.length ? ` +${parts.length - visible.length}` : ''; - return { label: `${visible.join(', ')}${suffix}`, title: parts.join(', ') }; -}; - const flagLabels = (vm: ResourceTrueNASVMMeta | undefined): string[] => { const labels: string[] = []; if (vm?.autostart) labels.push('Autostart'); @@ -92,11 +71,44 @@ const flagLabels = (vm: ResourceTrueNASVMMeta | undefined): string[] => { return labels; }; -// Columns a user can sort by. Devices and Flags summarize several values at -// once, so they carry no single scalar to order on. CPU orders on the -// provisioned vCPU count (cores × threads when vCPUs are not reported) and -// Memory on the provisioned bytes. -const TRUENAS_VM_SORT_KEYS = ['vm', 'state', 'cpu', 'memory', 'boot'] as const; +// A VM set to autostart that is not running is the one stopped VM the user +// did not choose, so the State cell says so beside the raw state, as it does +// for a state TrueNAS flags. Rows stay single-line (the shared platform-table +// rhythm), so the full sentence rides on the title. Bootloader and device +// counts are configuration, not condition, so they live in the drawer. +type VMStateNote = { short: string | null; full: string; tone: 'danger' | 'warning' }; + +const vmStateNote = (resource: Resource): VMStateNote | null => { + const status = mapTrueNASVMStatus(resource); + if (status === 'stopped') { + return vmMeta(resource)?.autostart + ? { short: 'should be running', full: 'Set to start at boot but stopped', tone: 'danger' } + : null; + } + if (status !== 'attention') return null; + if (hasImpairedResourceSource(resource, 'truenas')) { + return { short: 'not updated', full: 'TrueNAS has not updated this recently', tone: 'warning' }; + } + const state = asTrimmedString(vmMeta(resource)?.state || vmMeta(resource)?.domainState); + return { + short: state ? null : 'not reported', + full: state + ? `TrueNAS reports ${formatPlatformTableTitleCaseValue(state)}` + : 'TrueNAS has not reported a state', + tone: 'warning', + }; +}; + +const VM_STATE_NOTE_CLASS: Record = { + danger: 'text-red-600 dark:text-red-300', + warning: 'text-amber-700 dark:text-amber-300', +}; + +// Columns a user can sort by. Flags summarize several values at once, so they +// carry no single scalar to order on. CPU orders on the provisioned vCPU count +// (cores × threads when vCPUs are not reported) and Memory on the provisioned +// bytes. +const TRUENAS_VM_SORT_KEYS = ['vm', 'state', 'cpu', 'memory'] as const; type TrueNASVMSortKey = (typeof TRUENAS_VM_SORT_KEYS)[number]; @@ -136,8 +148,6 @@ const getTrueNASVMSortValue = ( return typeof vm?.memoryBytes === 'number' && Number.isFinite(vm.memoryBytes) ? vm.memoryBytes : null; - case 'boot': - return asTrimmedString(vm?.bootloader) || null; default: key satisfies never; return null; @@ -215,7 +225,7 @@ export const TrueNASVirtualMachinesTable: Component<{ kind="name" sort={sort} sortKey="vm" - class="platform-table-mobile-w-30 md:w-[22%]" + class="platform-table-mobile-w-30 md:w-[34%]" > VM @@ -223,7 +233,7 @@ export const TrueNASVirtualMachinesTable: Component<{ kind="badge" sort={sort} sortKey="state" - class="platform-table-phone-hidden md:w-[10%]" + class="platform-table-phone-hidden md:w-[14%]" > State @@ -231,7 +241,7 @@ export const TrueNASVirtualMachinesTable: Component<{ kind="numeric-value" sort={sort} sortKey="cpu" - class="platform-table-mobile-w-15 md:w-[10%]" + class="platform-table-mobile-w-15 md:w-[12%]" > CPU @@ -239,29 +249,14 @@ export const TrueNASVirtualMachinesTable: Component<{ kind="numeric-value" sort={sort} sortKey="memory" - class="platform-table-mobile-w-15 md:w-[10%]" + class="platform-table-mobile-w-15 md:w-[14%]" > - Boot - - - @@ -281,7 +276,7 @@ export const TrueNASVirtualMachinesTable: Component<{ const indicator = () => getSimpleStatusIndicator(displayStatus()); const stateLabel = () => formatPlatformTableTitleCaseValue(vm()?.state || vm()?.domainState); - const devices = createMemo(() => formatDevices(vm())); + const note = () => vmStateNote(resource); const flags = createMemo(() => flagLabels(vm())); const detailRowId = () => drawer.detailRowId(resource); const isExpanded = () => drawer.isExpanded(resource); @@ -326,9 +321,27 @@ export const TrueNASVirtualMachinesTable: Component<{ - - {stateLabel()} - +
+ + {stateLabel()} + + + {(short) => ( + + {short()} + + )} + + + {(current) => {current().full}} + +
{formatPlatformTableBytesValue(vm()?.memoryBytes, '-')} - - {vm()?.bootloader || '-'} - -