Expose native TrueNAS details in resource drawer

This commit is contained in:
rcourtman
2026-05-20 23:00:55 +01:00
parent 3fa46aa836
commit 6d55ca531c
7 changed files with 624 additions and 3 deletions
@@ -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<ResourceDetailDrawerOver
when={
drawer.hasServiceDetails() ||
drawer.hasVMwareDetails() ||
drawer.hasTrueNASDetails() ||
drawer.hasHostDetails() ||
drawer.hasAccessContext() ||
drawer.hasInvestigationContext()
@@ -631,6 +645,47 @@ export const ResourceDetailDrawerOverviewTab: Component<ResourceDetailDrawerOver
data-testid="resource-support-sections"
class="flex flex-wrap gap-3 [&>*]:flex-1 [&>*]:basis-[calc(50%-0.375rem)] [&>*]:min-w-[260px] [&>*]:max-w-full [&>*]:overflow-hidden"
>
<Show when={drawer.hasTrueNASDetails()}>
<SupportDisclosure
title="TrueNAS"
summary={drawer.trueNASDetailsSummary()}
expanded={drawer.showTrueNASDetails()}
onToggle={() => drawer.setShowTrueNASDetails((value) => !value)}
showLabel="Show TrueNAS"
hideLabel="Hide TrueNAS"
class="h-full"
contentClass="mt-3 space-y-3"
dataTestId="resource-truenas-details-section"
>
<div class="space-y-3">
<For each={drawer.trueNASDetailSections()}>
{(section) => (
<div class="rounded border border-cyan-200 bg-cyan-50 p-3 dark:border-cyan-700 dark:bg-cyan-900">
<div class="mb-2 text-[10px] font-medium uppercase tracking-wide text-cyan-700 dark:text-cyan-300">
{section.label}
</div>
<div class="space-y-1.5 text-[11px]">
<For each={section.rows}>
{(row) => (
<div class="flex items-center justify-between gap-2">
<span class="text-muted">{row.label}</span>
<span
class={`max-w-[60%] truncate text-right font-medium ${trueNASRowToneClass(row.tone)}`}
title={row.title ?? row.value}
>
{row.value}
</span>
</div>
)}
</For>
</div>
</div>
)}
</For>
</div>
</SupportDisclosure>
</Show>
<Show when={drawer.hasAccessContext()}>
<SupportDisclosure
title="Access"
@@ -15,6 +15,7 @@ import resourceDetailDrawerIdentityModelSource from '@/components/Infrastructure
import resourceDetailDrawerOperationalModelSource from '@/components/Infrastructure/resourceDetailDrawerOperationalModel.ts?raw';
import resourceDetailDrawerServiceModelSource from '@/components/Infrastructure/resourceDetailDrawerServiceModel.ts?raw';
import resourceDetailDrawerVmwareModelSource from '@/components/Infrastructure/resourceDetailDrawerVmwareModel.ts?raw';
import resourceDetailDrawerTrueNASModelSource from '@/components/Infrastructure/resourceDetailDrawerTrueNASModel.ts?raw';
import resourceDetailDrawerDockerActionsStateSource from '@/components/Infrastructure/useResourceDetailDrawerDockerActionsState.ts?raw';
import resourceDetailDrawerStateSource from '@/components/Infrastructure/useResourceDetailDrawerState.ts?raw';
import actionAuditApiSource from '@/api/actionAudit.ts?raw';
@@ -194,9 +195,7 @@ describe('ResourceDetailDrawer change history section', () => {
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 =');
@@ -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: () => <div data-testid="discovery-tab" />,
}));
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>): 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(() => (
<ResourceDetailDrawer resource={resource} />
));
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(() => (
<ResourceDetailDrawer resource={resource} />
));
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();
});
});
@@ -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>): 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',
]);
});
});
@@ -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, 'title' | 'tone'> = {},
): ResourceDetailDrawerTrueNASRow | null => {
const trimmed = value?.trim();
if (!trimmed) return null;
return { label, value: trimmed, ...options };
};
const compactRows = (
rows: Array<ResourceDetailDrawerTrueNASRow | null>,
): ResourceDetailDrawerTrueNASRow[] =>
rows.filter((entry): entry is ResourceDetailDrawerTrueNASRow => Boolean(entry));
const compactSections = (
sections: Array<ResourceDetailDrawerTrueNASSection | null>,
): 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;
};
@@ -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,
@@ -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,