From 6d55ca531c028f80b30453fac81d605882154226 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Wed, 20 May 2026 23:00:55 +0100 Subject: [PATCH] Expose native TrueNAS details in resource drawer --- .../ResourceDetailDrawerOverviewTab.tsx | 55 ++++ .../ResourceDetailDrawer.history.test.tsx | 14 +- ...ourceDetailDrawer.truenas-details.test.tsx | 154 +++++++++ .../resourceDetailDrawerTrueNASModel.test.ts | 87 +++++ .../resourceDetailDrawerTrueNASModel.ts | 304 ++++++++++++++++++ .../useResourceDetailDrawerDerivedState.ts | 10 + .../useResourceDetailDrawerState.ts | 3 + 7 files changed, 624 insertions(+), 3 deletions(-) create mode 100644 frontend-modern/src/components/Infrastructure/__tests__/ResourceDetailDrawer.truenas-details.test.tsx create mode 100644 frontend-modern/src/components/Infrastructure/__tests__/resourceDetailDrawerTrueNASModel.test.ts create mode 100644 frontend-modern/src/components/Infrastructure/resourceDetailDrawerTrueNASModel.ts diff --git a/frontend-modern/src/components/Infrastructure/ResourceDetailDrawerOverviewTab.tsx b/frontend-modern/src/components/Infrastructure/ResourceDetailDrawerOverviewTab.tsx index 2781529c35..9bb047d020 100644 --- a/frontend-modern/src/components/Infrastructure/ResourceDetailDrawerOverviewTab.tsx +++ b/frontend-modern/src/components/Infrastructure/ResourceDetailDrawerOverviewTab.tsx @@ -72,6 +72,19 @@ const vmwareRowToneClass = (tone?: 'default' | 'accent' | 'warning'): string => } }; +const trueNASRowToneClass = (tone?: 'default' | 'accent' | 'warning' | 'success'): string => { + switch (tone) { + case 'accent': + return 'text-cyan-700 dark:text-cyan-300'; + case 'warning': + return 'text-amber-700 dark:text-amber-300'; + case 'success': + return 'text-emerald-700 dark:text-emerald-300'; + default: + return 'text-base-content'; + } +}; + const pbsEvidenceBadgeClass = (tone: string): string => { switch (tone) { case 'danger': @@ -622,6 +635,7 @@ export const ResourceDetailDrawerOverviewTab: Component + + drawer.setShowTrueNASDetails((value) => !value)} + showLabel="Show TrueNAS" + hideLabel="Hide TrueNAS" + class="h-full" + contentClass="mt-3 space-y-3" + dataTestId="resource-truenas-details-section" + > +
+ + {(section) => ( +
+
+ {section.label} +
+
+ + {(row) => ( +
+ {row.label} + + {row.value} + +
+ )} +
+
+
+ )} +
+
+
+
+ { expect(resourceDetailDrawerDerivedStateSource).toContain('resource.relationships ?? []'); expect(resourceActionHistorySource).toContain('getActionAuditRecordStatePresentation'); expect(resourceActionHistorySource).toContain('getActionAuditResultPresentation'); - expect(resourceActionHistorySource).toContain( - 'getActionAuditVerificationOutcomePresentation', - ); + expect(resourceActionHistorySource).toContain('getActionAuditVerificationOutcomePresentation'); expect(resourceActionHistorySource).toContain('formatActionApprovalPolicyLabel'); // The audit history must surface the broker's read-after-write // verification outcome alongside the dispatch result, not silently @@ -228,6 +227,9 @@ describe('ResourceDetailDrawer change history section', () => { expect(resourceDetailDrawerDerivedStateSource).toContain( "from './resourceDetailDrawerVmwareModel'", ); + expect(resourceDetailDrawerDerivedStateSource).toContain( + "from './resourceDetailDrawerTrueNASModel'", + ); expect(resourceDetailDrawerDiscoveryModelSource).toContain('export const toDiscoveryConfig'); expect(resourceDetailDrawerIdentityModelSource).toContain( 'export const buildResourceIdentityView', @@ -244,6 +246,12 @@ describe('ResourceDetailDrawer change history section', () => { expect(resourceDetailDrawerVmwareModelSource).toContain( 'export const buildVMwareDetailsSummary', ); + expect(resourceDetailDrawerTrueNASModelSource).toContain( + 'export const buildTrueNASDetailSections', + ); + expect(resourceDetailDrawerTrueNASModelSource).toContain( + 'export const buildTrueNASDetailsSummary', + ); expect(resourceDetailDrawerDerivedStateSource).not.toContain('buildWorkloadsHref'); expect(resourceDetailDrawerDerivedStateSource).not.toContain('buildServiceDetailLinks'); expect(resourceDetailDrawerDerivedStateSource).not.toContain('const supportedBadge ='); diff --git a/frontend-modern/src/components/Infrastructure/__tests__/ResourceDetailDrawer.truenas-details.test.tsx b/frontend-modern/src/components/Infrastructure/__tests__/ResourceDetailDrawer.truenas-details.test.tsx new file mode 100644 index 0000000000..488c535e62 --- /dev/null +++ b/frontend-modern/src/components/Infrastructure/__tests__/ResourceDetailDrawer.truenas-details.test.tsx @@ -0,0 +1,154 @@ +import { describe, expect, it, vi } from 'vitest'; +import { fireEvent, render, within } from '@solidjs/testing-library'; + +import type { Resource } from '@/types/resource'; +import { ResourceDetailDrawer } from '@/components/Infrastructure/ResourceDetailDrawer'; + +vi.mock('@/contexts/appRuntime', () => ({ + useWebSocket: () => ({ + state: { pmg: [] as any[] }, + connected: () => true, + initialDataReceived: () => true, + reconnecting: () => false, + reconnect: vi.fn(), + }), + useDarkMode: () => () => false, +})); + +vi.mock('@/components/Discovery/DiscoveryTab', () => ({ + DiscoveryTab: () =>
, +})); + +vi.mock('@/api/resources', () => ({ + ResourceAPI: { + getFacetBundle: vi.fn().mockResolvedValue({ + capabilities: [], + relationships: [], + recentChanges: [], + }), + }, +})); + +vi.mock('@/api/ai', () => ({ + AIAPI: { + getResourceIntelligence: vi.fn().mockResolvedValue({ + resource_id: 'resource-1', + health: { + score: 100, + grade: 'A', + trend: 'stable', + factors: [], + prediction: 'Stable', + }, + recent_changes: [], + dependencies: [], + dependents: [], + correlations: [], + note_count: 0, + }), + }, +})); + +const baseResource = (overrides: Partial): Resource => + ({ + id: 'resource-1', + type: 'vm', + name: 'resource-1', + displayName: 'resource-1', + platformId: 'truenas-main', + platformType: 'truenas', + sourceType: 'api', + status: 'online', + lastSeen: Date.now(), + platformData: { sources: ['truenas'] }, + ...overrides, + }) as Resource; + +describe('ResourceDetailDrawer TrueNAS details', () => { + it('renders native TrueNAS VM detail only after the TrueNAS section is expanded', () => { + const resource = baseResource({ + id: 'truenas-vm-1', + type: 'vm', + displayName: 'ubuntu-build', + truenas: { + vm: { + name: 'ubuntu-build', + state: 'RUNNING', + vcpus: 4, + memoryBytes: 8 * 1024 ** 3, + deviceCount: 3, + diskCount: 1, + nicCount: 1, + bootloader: 'UEFI', + secureBoot: true, + trustedPlatformModule: true, + }, + }, + }); + + const { getByText, getByRole, getByTestId, getAllByText, queryByText } = render(() => ( + + )); + + expect(getAllByText('TrueNAS').length).toBeGreaterThan(0); + expect(getByText('Running, 4 vCPU, 8.00 GB, 3 devices')).toBeInTheDocument(); + expect(queryByText('Bootloader')).toBeNull(); + + fireEvent.click(getByRole('button', { name: 'Show TrueNAS' })); + + const section = within(getByTestId('resource-truenas-details-section')); + expect(section.getByText('Compute')).toBeInTheDocument(); + expect(section.getByText('Runtime')).toBeInTheDocument(); + expect(section.getByText('Devices')).toBeInTheDocument(); + expect(section.getByText('Flags')).toBeInTheDocument(); + expect(section.getByText('Bootloader')).toBeInTheDocument(); + expect(section.getByText('UEFI')).toBeInTheDocument(); + expect(section.getByText('Secure boot')).toBeInTheDocument(); + expect(section.getAllByText('Enabled').length).toBeGreaterThan(1); + }); + + it('renders native TrueNAS app detail without Docker runtime controls', () => { + const resource = baseResource({ + id: 'truenas-app-1', + type: 'app-container', + displayName: 'nextcloud', + truenas: { + app: { + name: 'nextcloud', + state: 'RUNNING', + humanVersion: '29.0.0', + containerCount: 2, + upgradeAvailable: true, + imageUpdatesAvailable: false, + usedHostIps: ['0.0.0.0'], + usedPorts: [ + { + containerPort: 443, + protocol: 'tcp', + hostPorts: [{ hostIp: '0.0.0.0', hostPort: 30443 }], + }, + ], + networks: [{ name: 'ix-nextcloud_default' }], + volumes: [{ source: '/mnt/tank/apps/nextcloud', destination: '/data' }], + images: ['nextcloud:29'], + }, + }, + }); + + const { getByText, getByRole, getByTestId, queryByText } = render(() => ( + + )); + + expect(getByText('Running, 2 containers, 1 port, 1 update')).toBeInTheDocument(); + expect(queryByText('Docker runtime')).toBeNull(); + + fireEvent.click(getByRole('button', { name: 'Show TrueNAS' })); + + const section = within(getByTestId('resource-truenas-details-section')); + expect(section.getByText('App')).toBeInTheDocument(); + expect(section.getByText('Networking')).toBeInTheDocument(); + expect(section.getByText('Storage')).toBeInTheDocument(); + expect(section.getByText('0.0.0.0:30443 -> 443/tcp')).toBeInTheDocument(); + expect(section.getByText('/mnt/tank/apps/nextcloud -> /data')).toBeInTheDocument(); + }); +}); diff --git a/frontend-modern/src/components/Infrastructure/__tests__/resourceDetailDrawerTrueNASModel.test.ts b/frontend-modern/src/components/Infrastructure/__tests__/resourceDetailDrawerTrueNASModel.test.ts new file mode 100644 index 0000000000..61d32d7b05 --- /dev/null +++ b/frontend-modern/src/components/Infrastructure/__tests__/resourceDetailDrawerTrueNASModel.test.ts @@ -0,0 +1,87 @@ +import { describe, expect, it } from 'vitest'; +import { + buildTrueNASDetailSections, + buildTrueNASDetailsSummary, +} from '@/components/Infrastructure/resourceDetailDrawerTrueNASModel'; +import type { Resource } from '@/types/resource'; + +const baseResource = (overrides: Partial): Resource => + ({ + id: 'truenas-resource', + type: 'vm', + name: 'truenas-resource', + displayName: 'TrueNAS resource', + platformId: 'truenas-main', + platformType: 'truenas', + sourceType: 'api', + status: 'online', + ...overrides, + }) as Resource; + +describe('resourceDetailDrawerTrueNASModel', () => { + it('summarizes native vm.query metadata for the detail drawer', () => { + const resource = baseResource({ + type: 'vm', + truenas: { + vm: { + name: 'ubuntu-build', + state: 'RUNNING', + vcpus: 4, + memoryBytes: 8 * 1024 ** 3, + deviceCount: 3, + diskCount: 1, + nicCount: 1, + bootloader: 'UEFI', + secureBoot: true, + trustedPlatformModule: true, + }, + }, + }); + + expect(buildTrueNASDetailsSummary(resource)).toBe('Running, 4 vCPU, 8.00 GB, 3 devices'); + expect(buildTrueNASDetailSections(resource).map((section) => section.label)).toEqual([ + 'Compute', + 'Runtime', + 'Devices', + 'Flags', + ]); + }); + + it('summarizes native app.query metadata for the detail drawer', () => { + const resource = baseResource({ + type: 'app-container', + truenas: { + app: { + name: 'nextcloud', + state: 'RUNNING', + humanVersion: '29.0.0', + containerCount: 2, + upgradeAvailable: true, + imageUpdatesAvailable: false, + usedHostIps: ['0.0.0.0'], + usedPorts: [ + { + containerPort: 443, + protocol: 'tcp', + hostPorts: [{ hostIp: '0.0.0.0', hostPort: 30443 }], + }, + { + containerPort: 80, + protocol: 'tcp', + hostPorts: [{ hostIp: '0.0.0.0', hostPort: 30080 }], + }, + ], + volumes: [{ source: '/mnt/tank/apps/nextcloud', destination: '/data' }], + images: ['nextcloud:29'], + }, + }, + }); + + expect(buildTrueNASDetailsSummary(resource)).toBe('Running, 2 containers, 2 ports, 1 update'); + expect(buildTrueNASDetailSections(resource).map((section) => section.label)).toEqual([ + 'App', + 'Networking', + 'Storage', + ]); + }); +}); diff --git a/frontend-modern/src/components/Infrastructure/resourceDetailDrawerTrueNASModel.ts b/frontend-modern/src/components/Infrastructure/resourceDetailDrawerTrueNASModel.ts new file mode 100644 index 0000000000..5d3e1068cf --- /dev/null +++ b/frontend-modern/src/components/Infrastructure/resourceDetailDrawerTrueNASModel.ts @@ -0,0 +1,304 @@ +import type { + Resource, + ResourceTrueNASAppMeta, + ResourceTrueNASAppPort, + ResourceTrueNASVMMeta, +} from '@/types/resource'; + +export type ResourceDetailDrawerTrueNASRowTone = 'default' | 'accent' | 'warning' | 'success'; + +export type ResourceDetailDrawerTrueNASRow = { + label: string; + value: string; + title?: string; + tone?: ResourceDetailDrawerTrueNASRowTone; +}; + +export type ResourceDetailDrawerTrueNASSection = { + label: string; + rows: ResourceDetailDrawerTrueNASRow[]; +}; + +const asString = (value?: string | null): string | null => { + const trimmed = value?.trim(); + return trimmed ? trimmed : null; +}; + +const asPositiveNumber = (value?: number): number | null => + typeof value === 'number' && Number.isFinite(value) && value > 0 ? value : null; + +const formatInteger = (value?: number): string | null => { + if (typeof value !== 'number' || !Number.isFinite(value)) return null; + return new Intl.NumberFormat().format(Math.round(value)); +}; + +const normalizeDelimitedLabel = (value?: string): string | null => { + const trimmed = asString(value); + if (!trimmed) return null; + return trimmed + .split(/[\s_-]+/) + .filter(Boolean) + .map((part) => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase()) + .join(' '); +}; + +const formatBytes = (bytes?: number): string | null => { + const value = asPositiveNumber(bytes); + if (!value) return null; + const units = ['B', 'KB', 'MB', 'GB', 'TB']; + let scaled = value; + let unitIndex = 0; + while (scaled >= 1024 && unitIndex < units.length - 1) { + scaled /= 1024; + unitIndex += 1; + } + return `${scaled.toFixed(scaled >= 100 ? 0 : scaled >= 10 ? 1 : 2)} ${units[unitIndex]}`; +}; + +const formatCount = (value: number, singular: string, plural = `${singular}s`): string => + `${new Intl.NumberFormat().format(value)} ${value === 1 ? singular : plural}`; + +const summarizeList = ( + values: string[], + visibleCount = 3, +): ResourceDetailDrawerTrueNASRow['value'] => { + const visible = values.map((value) => value.trim()).filter(Boolean); + if (visible.length === 0) return ''; + const head = visible.slice(0, visibleCount); + const suffix = visible.length > head.length ? ` +${visible.length - head.length}` : ''; + return `${head.join(', ')}${suffix}`; +}; + +const booleanValue = (value?: boolean): string | null => { + if (value === undefined) return null; + return value ? 'Enabled' : 'Disabled'; +}; + +const yesNoValue = (value?: boolean): string | null => { + if (value === undefined) return null; + return value ? 'Yes' : 'No'; +}; + +const row = ( + label: string, + value: string | null | undefined, + options: Pick = {}, +): ResourceDetailDrawerTrueNASRow | null => { + const trimmed = value?.trim(); + if (!trimmed) return null; + return { label, value: trimmed, ...options }; +}; + +const compactRows = ( + rows: Array, +): ResourceDetailDrawerTrueNASRow[] => + rows.filter((entry): entry is ResourceDetailDrawerTrueNASRow => Boolean(entry)); + +const compactSections = ( + sections: Array, +): ResourceDetailDrawerTrueNASSection[] => + sections.filter((section): section is ResourceDetailDrawerTrueNASSection => + Boolean(section && section.rows.length > 0), + ); + +const portLabel = (port: ResourceTrueNASAppPort): string | null => { + const protocol = asString(port.protocol)?.toLowerCase(); + const container = + asPositiveNumber(port.containerPort) !== null + ? `${port.containerPort}${protocol ? `/${protocol}` : ''}` + : protocol; + const hostPorts = (port.hostPorts ?? []) + .map((hostPort) => { + const portNumber = asPositiveNumber(hostPort.hostPort); + if (!portNumber) return null; + const hostIp = asString(hostPort.hostIp); + return hostIp ? `${hostIp}:${portNumber}` : `${portNumber}`; + }) + .filter((value): value is string => Boolean(value)); + + if (hostPorts.length > 0 && container) return `${hostPorts.join(', ')} -> ${container}`; + if (hostPorts.length > 0) return hostPorts.join(', '); + return container ?? null; +}; + +const appPortLabels = (app: ResourceTrueNASAppMeta): string[] => + (app.usedPorts ?? []).map(portLabel).filter((value): value is string => Boolean(value)); + +const appVolumeLabels = (app: ResourceTrueNASAppMeta): string[] => + (app.volumes ?? []) + .map((volume) => { + const source = asString(volume.source); + const destination = asString(volume.destination); + if (source && destination) return `${source} -> ${destination}`; + return destination ?? source; + }) + .filter((value): value is string => Boolean(value)); + +const appNetworkLabels = (app: ResourceTrueNASAppMeta): string[] => + (app.networks ?? []) + .map((network) => asString(network.name) ?? asString(network.id)) + .filter((value): value is string => Boolean(value)); + +const buildTrueNASAppSections = ( + app: ResourceTrueNASAppMeta, +): ResourceDetailDrawerTrueNASSection[] => { + const containerCount = app.containerCount ?? app.containers?.length; + const portLabels = appPortLabels(app); + const volumeLabels = appVolumeLabels(app); + const imageLabels = (app.images ?? []).map((image) => image.trim()).filter(Boolean); + const networkLabels = appNetworkLabels(app); + const appRows = compactRows([ + row('State', normalizeDelimitedLabel(app.state), { + tone: app.state?.toLowerCase() === 'running' ? 'success' : 'warning', + }), + row('Version', asString(app.humanVersion) ?? asString(app.version)), + row('Containers', formatInteger(containerCount)), + row('Custom app', yesNoValue(app.customApp)), + row( + 'App updates', + app.upgradeAvailable === undefined ? null : app.upgradeAvailable ? 'Available' : 'Current', + { + tone: app.upgradeAvailable ? 'warning' : 'success', + }, + ), + row( + 'Image updates', + app.imageUpdatesAvailable === undefined + ? null + : app.imageUpdatesAvailable + ? 'Available' + : 'Current', + { tone: app.imageUpdatesAvailable ? 'warning' : 'success' }, + ), + row('Notes', asString(app.notes)), + ]); + + const networkRows = compactRows([ + row('Host IPs', summarizeList(app.usedHostIps ?? []), { + title: (app.usedHostIps ?? []).join(', '), + }), + row('Ports', summarizeList(portLabels), { title: portLabels.join(', ') }), + row('Networks', summarizeList(networkLabels), { title: networkLabels.join(', ') }), + ]); + + const storageRows = compactRows([ + row('Volumes', summarizeList(volumeLabels), { title: volumeLabels.join(', ') }), + row('Images', summarizeList(imageLabels, 2), { title: imageLabels.join(', ') }), + ]); + + return compactSections([ + { label: 'App', rows: appRows }, + { label: 'Networking', rows: networkRows }, + { label: 'Storage', rows: storageRows }, + ]); +}; + +const formatVMCpu = (vm: ResourceTrueNASVMMeta): string | null => { + const vcpus = asPositiveNumber(vm.vcpus); + if (vcpus) return formatCount(vcpus, 'vCPU', 'vCPU'); + const cores = asPositiveNumber(vm.cores); + const threads = asPositiveNumber(vm.threads); + if (cores && threads) return `${cores} cores x ${threads} threads`; + if (cores) return formatCount(cores, 'core'); + if (threads) return formatCount(threads, 'thread'); + return null; +}; + +const formatVMTopology = (vm: ResourceTrueNASVMMeta): string | null => { + const cores = asPositiveNumber(vm.cores); + const threads = asPositiveNumber(vm.threads); + if (cores && threads) return `${cores} cores x ${threads} threads`; + if (cores) return formatCount(cores, 'core'); + if (threads) return formatCount(threads, 'thread'); + return null; +}; + +const buildTrueNASVMSections = ( + vm: ResourceTrueNASVMMeta, +): ResourceDetailDrawerTrueNASSection[] => { + const state = normalizeDelimitedLabel(vm.state); + const domainState = normalizeDelimitedLabel(vm.domainState); + const sameState = state?.toLowerCase() === domainState?.toLowerCase(); + const machine = [asString(vm.machineType), asString(vm.archType)].filter(Boolean).join(' / '); + const computeRows = compactRows([ + row('State', state ?? domainState, { + tone: (vm.state ?? vm.domainState)?.toLowerCase() === 'running' ? 'success' : 'warning', + }), + row('Domain state', sameState ? null : domainState), + row('vCPU', formatVMCpu(vm)), + row('Topology', formatVMTopology(vm)), + row('Memory', formatBytes(vm.memoryBytes)), + row('Minimum memory', formatBytes(vm.minMemoryBytes)), + row('CPU mode', normalizeDelimitedLabel(vm.cpuMode)), + row('CPU model', asString(vm.cpuModel)), + ]); + + const runtimeRows = compactRows([ + row('Bootloader', asString(vm.bootloader)), + row('Machine', machine), + row('Process ID', formatInteger(vm.pid)), + row('UUID', asString(vm.uuid)), + ]); + + const deviceRows = compactRows([ + row('Total', formatInteger(vm.deviceCount)), + row('Disks', formatInteger(vm.diskCount)), + row('NICs', formatInteger(vm.nicCount)), + row('Displays', formatInteger(vm.displayCount)), + row('CD-ROMs', formatInteger(vm.cdromCount)), + row('USB', formatInteger(vm.usbCount)), + row('PCI', formatInteger(vm.pciCount)), + ]); + + const flagRows = compactRows([ + row('Autostart', booleanValue(vm.autostart)), + row('Secure boot', booleanValue(vm.secureBoot)), + row('TPM', booleanValue(vm.trustedPlatformModule)), + row('Suspend on snapshot', booleanValue(vm.suspendOnSnapshot)), + row('Display available', booleanValue(vm.displayAvailable)), + ]); + + return compactSections([ + { label: 'Compute', rows: computeRows }, + { label: 'Runtime', rows: runtimeRows }, + { label: 'Devices', rows: deviceRows }, + { label: 'Flags', rows: flagRows }, + ]); +}; + +export const buildTrueNASDetailSections = ( + resource: Resource, +): ResourceDetailDrawerTrueNASSection[] => { + if (resource.truenas?.vm) return buildTrueNASVMSections(resource.truenas.vm); + if (resource.truenas?.app) return buildTrueNASAppSections(resource.truenas.app); + return []; +}; + +export const buildTrueNASDetailsSummary = (resource: Resource): string | null => { + const vm = resource.truenas?.vm; + if (vm) { + const deviceCount = asPositiveNumber(vm.deviceCount); + const summary = [ + normalizeDelimitedLabel(vm.state ?? vm.domainState), + formatVMCpu(vm), + formatBytes(vm.memoryBytes), + deviceCount ? formatCount(deviceCount, 'device') : null, + ].filter((value): value is string => Boolean(value)); + return summary.length > 0 ? summary.join(', ') : null; + } + + const app = resource.truenas?.app; + if (app) { + const portCount = appPortLabels(app).length; + const updateCount = [app.upgradeAvailable, app.imageUpdatesAvailable].filter(Boolean).length; + const summary = [ + normalizeDelimitedLabel(app.state), + formatCount(app.containerCount ?? app.containers?.length ?? 0, 'container'), + formatCount(portCount, 'port'), + updateCount > 0 ? formatCount(updateCount, 'update') : null, + ].filter((value): value is string => Boolean(value)); + return summary.length > 0 ? summary.join(', ') : null; + } + + return null; +}; diff --git a/frontend-modern/src/components/Infrastructure/useResourceDetailDrawerDerivedState.ts b/frontend-modern/src/components/Infrastructure/useResourceDetailDrawerDerivedState.ts index bfdcc5c723..7abde54b4d 100644 --- a/frontend-modern/src/components/Infrastructure/useResourceDetailDrawerDerivedState.ts +++ b/frontend-modern/src/components/Infrastructure/useResourceDetailDrawerDerivedState.ts @@ -69,6 +69,10 @@ import { buildVMwareDetailsSummary, buildVMwareDetailSections, } from './resourceDetailDrawerVmwareModel'; +import { + buildTrueNASDetailsSummary, + buildTrueNASDetailSections, +} from './resourceDetailDrawerTrueNASModel'; import { getResourceHealthIssuePresentation } from './resourceHealthPresentation'; type DrawerTab = 'overview' | 'mail' | 'namespaces' | 'deployments' | 'swarm' | 'debug'; @@ -319,6 +323,9 @@ export const useResourceDetailDrawerDerivedState = ( const vmwareDetailsSummary = createMemo(() => buildVMwareDetailsSummary(resource.type, vmwareData()), ); + const trueNASDetailSections = createMemo(() => buildTrueNASDetailSections(resource)); + const hasTrueNASDetails = createMemo(() => trueNASDetailSections().length > 0); + const trueNASDetailsSummary = createMemo(() => buildTrueNASDetailsSummary(resource)); const relatedLinks = createMemo(() => buildRelatedLinks(resource, displayName())); const accessSummary = createMemo(() => @@ -444,6 +451,9 @@ export const useResourceDetailDrawerDerivedState = ( hasVMwareDetails, vmwareDetailsSummary, vmwareDetailSections, + hasTrueNASDetails, + trueNASDetailsSummary, + trueNASDetailSections, relatedLinks, hasRuntimeOperationalContext, sourceSections, diff --git a/frontend-modern/src/components/Infrastructure/useResourceDetailDrawerState.ts b/frontend-modern/src/components/Infrastructure/useResourceDetailDrawerState.ts index 581a297f14..2b379eaa8c 100644 --- a/frontend-modern/src/components/Infrastructure/useResourceDetailDrawerState.ts +++ b/frontend-modern/src/components/Infrastructure/useResourceDetailDrawerState.ts @@ -27,6 +27,7 @@ export const useResourceDetailDrawerState = (options: UseResourceDetailDrawerSta const [showHostDetails, setShowHostDetails] = createSignal(false); const [showServiceDetails, setShowServiceDetails] = createSignal(false); const [showVMwareDetails, setShowVMwareDetails] = createSignal(false); + const [showTrueNASDetails, setShowTrueNASDetails] = createSignal(false); const [showPbsJobDetail, setShowPbsJobDetail] = createSignal(false); const [showPmgMailFlowDetail, setShowPmgMailFlowDetail] = createSignal(false); const [k8sDeploymentsPrefillNamespace, setK8sDeploymentsPrefillNamespace] = createSignal(''); @@ -105,6 +106,8 @@ export const useResourceDetailDrawerState = (options: UseResourceDetailDrawerSta setShowServiceDetails, showVMwareDetails, setShowVMwareDetails, + showTrueNASDetails, + setShowTrueNASDetails, showPbsJobDetail, setShowPbsJobDetail, showPmgMailFlowDetail,