Share TrueNAS detail table rendering

This commit is contained in:
rcourtman
2026-05-22 06:25:22 +01:00
parent d10079f9e3
commit b63953ee0e
8 changed files with 187 additions and 239 deletions
@@ -25,6 +25,7 @@ import { ResourceOperatorStateSection } from './ResourceOperatorStateSection';
import { MaintenanceVerificationSection } from './MaintenanceVerificationSection';
import { InlineResourceSummaryTables, ResourceSummaryCards } from './ResourceDetailSummary';
import { ResourceInvestigationContextTables } from './ResourceInvestigationContextTables';
import { TrueNASDetailSectionTable } from './TrueNASDetailTable';
import {
RESOURCE_CHANGE_KIND_ORDER,
RESOURCE_CHANGE_SOURCE_ADAPTER_ORDER,
@@ -42,7 +43,6 @@ import { buildPbsJobHealthEvidenceModel } from './resourceDetailDrawerServiceMod
import { ResourceDetailDrawerSupportDisclosure as SupportDisclosure } from './ResourceDetailDrawerSupportDisclosure';
import type { UseResourceDetailDrawerStateResult } from './useResourceDetailDrawerState';
import type { ResourceDetailDrawerPresentation } from './resourceDetailDrawerPresentation';
import type { ResourceDetailDrawerTrueNASSection } from './resourceDetailDrawerTrueNASModel';
interface ResourceDetailDrawerOverviewTabProps {
resource: Resource;
@@ -64,59 +64,6 @@ 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 TrueNASDetailSectionTables: Component<{
sections: ResourceDetailDrawerTrueNASSection[];
}> = (props) => (
<div class="overflow-hidden rounded border border-border bg-surface">
<table class="w-full table-fixed text-[11px]">
<tbody class="divide-y divide-border">
<For each={props.sections}>
{(section) => (
<>
<tr class="bg-surface-alt">
<th
colspan="2"
class="px-2 py-1 text-left text-[10px] font-semibold uppercase tracking-wide text-muted"
>
{section.label}
</th>
</tr>
<For each={section.rows}>
{(row) => (
<tr>
<td class="w-[38%] px-2 py-1 align-top text-muted">{row.label}</td>
<td
class={`px-2 py-1 text-right align-top font-medium ${trueNASRowToneClass(
row.tone,
)}`}
title={row.title ?? row.value}
>
<span class="block truncate">{row.value}</span>
</td>
</tr>
)}
</For>
</>
)}
</For>
</tbody>
</table>
</div>
);
const TrueNASDetailsDisclosure: Component<{
drawer: UseResourceDetailDrawerStateResult;
class?: string;
@@ -133,7 +80,7 @@ const TrueNASDetailsDisclosure: Component<{
contentClass={props.contentClass ?? 'mt-3 space-y-3'}
dataTestId="resource-truenas-details-section"
>
<TrueNASDetailSectionTables sections={props.drawer.trueNASDetailSections()} />
<TrueNASDetailSectionTable sections={props.drawer.trueNASDetailSections()} />
</SupportDisclosure>
);
@@ -0,0 +1,104 @@
import { For, type Component } from 'solid-js';
import { Table, TableBody, TableCell, TableHead, TableRow } from '@/components/shared/Table';
import type { TrueNASDetailSection, TrueNASDetailTone } from './trueNASDetailTableModel';
export {
compactTrueNASDetailRows,
compactTrueNASDetailSections,
makeTrueNASDetailRow,
type TrueNASDetailRow,
type TrueNASDetailSection,
type TrueNASDetailTone,
} from './trueNASDetailTableModel';
const detailValueToneClass = (tone: TrueNASDetailTone | undefined): string => {
if (tone === 'accent') return 'text-cyan-700 dark:text-cyan-300';
if (tone === 'success') return 'text-emerald-700 dark:text-emerald-300';
if (tone === 'warning') return 'text-amber-700 dark:text-amber-300';
if (tone === 'danger') return 'text-rose-700 dark:text-rose-300';
if (tone === 'muted') return 'text-muted';
return 'text-base-content';
};
const truenasDetailAttribute = (
detailKind: 'alert' | 'protection' | 'service' | undefined,
detailFor: string,
): Record<string, string> =>
detailKind ? { [`data-truenas-${detailKind}-detail-for`]: detailFor } : {};
export const TrueNASDetailSectionTable: Component<{
sections: TrueNASDetailSection[];
class?: string;
}> = (props) => (
<div class={props.class ?? 'overflow-hidden rounded border border-border bg-surface'}>
<Table class="w-full table-fixed text-[11px]">
<TableBody class="divide-y divide-border">
<For each={props.sections}>
{(section) => (
<>
<TableRow class="bg-surface-alt">
<TableHead
colspan={2}
class="px-2 py-1 text-left text-[10px] font-semibold uppercase tracking-wide text-muted"
>
{section.label}
</TableHead>
</TableRow>
<For each={section.rows}>
{(row) => (
<TableRow>
<TableCell class="w-[38%] px-2 py-1 align-top text-muted">
{row.label}
</TableCell>
<TableCell
class={`px-2 py-1 text-right align-top font-medium ${detailValueToneClass(
row.tone,
)}`}
title={row.title ?? row.value}
>
<span class="block truncate">{row.value}</span>
</TableCell>
</TableRow>
)}
</For>
</>
)}
</For>
</TableBody>
</Table>
</div>
);
export const TrueNASInlineDetailTable: Component<{
testId: string;
detailFor: string;
detailKind?: 'alert' | 'protection' | 'service';
title: string;
summary: string;
sections: TrueNASDetailSection[];
onClose: () => void;
}> = (props) => (
<div
class="space-y-3"
data-testid={props.testId}
data-truenas-inline-detail-for={props.detailFor}
{...truenasDetailAttribute(props.detailKind, props.detailFor)}
>
<div class="flex flex-wrap items-start justify-between gap-3">
<div>
<div class="text-[11px] font-medium uppercase tracking-wide text-base-content">
{props.title}
</div>
<div class="mt-1 text-[10px] text-muted">{props.summary}</div>
</div>
<button
type="button"
class="inline-flex items-center rounded-md border border-border bg-surface px-2.5 py-1 text-[10px] font-medium text-base-content transition-colors hover:bg-base"
onClick={props.onClose}
>
Close
</button>
</div>
<TrueNASDetailSectionTable sections={props.sections} />
</div>
);
@@ -9,20 +9,20 @@ import type {
ResourceTrueNASShareMeta,
ResourceTrueNASVMMeta,
} from '@/types/resource';
import {
compactTrueNASDetailRows,
compactTrueNASDetailSections,
makeTrueNASDetailRow,
type TrueNASDetailRow,
type TrueNASDetailSection,
type TrueNASDetailTone,
} from './trueNASDetailTableModel';
export type ResourceDetailDrawerTrueNASRowTone = 'default' | 'accent' | 'warning' | 'success';
export type ResourceDetailDrawerTrueNASRowTone = TrueNASDetailTone;
export type ResourceDetailDrawerTrueNASRow = {
label: string;
value: string;
title?: string;
tone?: ResourceDetailDrawerTrueNASRowTone;
};
export type ResourceDetailDrawerTrueNASRow = TrueNASDetailRow;
export type ResourceDetailDrawerTrueNASSection = {
label: string;
rows: ResourceDetailDrawerTrueNASRow[];
};
export type ResourceDetailDrawerTrueNASSection = TrueNASDetailSection;
const asString = (value?: string | null): string | null => {
const trimmed = value?.trim();
@@ -105,27 +105,9 @@ const yesNoValue = (value?: boolean): string | 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 row = makeTrueNASDetailRow;
const compactRows = compactTrueNASDetailRows;
const compactSections = compactTrueNASDetailSections;
const isTrueNASScopedResource = (resource: Resource): boolean =>
resource.platformType === 'truenas' ||
@@ -0,0 +1,34 @@
export type TrueNASDetailTone = 'default' | 'accent' | 'success' | 'warning' | 'danger' | 'muted';
export type TrueNASDetailRow = {
label: string;
value: string;
title?: string;
tone?: TrueNASDetailTone;
};
export type TrueNASDetailSection = {
label: string;
rows: TrueNASDetailRow[];
};
export const makeTrueNASDetailRow = (
label: string,
value?: string | null,
options: Pick<TrueNASDetailRow, 'title' | 'tone'> = {},
): TrueNASDetailRow | null => {
const trimmed = value?.trim();
if (!trimmed || trimmed === '-') return null;
return { label, value: trimmed, ...options };
};
export const compactTrueNASDetailRows = (
rows: Array<TrueNASDetailRow | null>,
): TrueNASDetailRow[] => rows.filter((row): row is TrueNASDetailRow => Boolean(row));
export const compactTrueNASDetailSections = (
sections: Array<TrueNASDetailSection | null>,
): TrueNASDetailSection[] =>
sections.filter((section): section is TrueNASDetailSection =>
Boolean(section && section.rows.length > 0),
);
@@ -33,12 +33,12 @@ import {
} from './truenasPageModel';
import {
TrueNASInlineDetailTable,
compactTrueNASInlineDetailRows,
compactTrueNASInlineDetailSections,
makeTrueNASInlineDetailRow,
type TrueNASInlineDetailSection,
type TrueNASInlineDetailTone,
} from './TrueNASInlineDetailTable';
compactTrueNASDetailRows,
compactTrueNASDetailSections,
makeTrueNASDetailRow,
type TrueNASDetailSection,
type TrueNASDetailTone,
} from '@/components/Infrastructure/TrueNASDetailTable';
import type { Resource, ResourceType } from '@/types/resource';
import { getAlertFilteredEmptyState } from '@/utils/alertOverviewPresentation';
@@ -124,8 +124,8 @@ const formatStartedAt = (value: string | undefined): string => {
});
};
type AlertDetailTone = TrueNASInlineDetailTone;
type AlertDetailSection = TrueNASInlineDetailSection;
type AlertDetailTone = TrueNASDetailTone;
type AlertDetailSection = TrueNASDetailSection;
const detailDateTime = (value?: string): string | null => {
if (!value) return null;
@@ -140,9 +140,9 @@ const detailDateTime = (value?: string): string | null => {
});
};
const detailRow = makeTrueNASInlineDetailRow;
const compactDetailRows = compactTrueNASInlineDetailRows;
const compactDetailSections = compactTrueNASInlineDetailSections;
const detailRow = makeTrueNASDetailRow;
const compactDetailRows = compactTrueNASDetailRows;
const compactDetailSections = compactTrueNASDetailSections;
const alertTone = (severity: TrueNASIncidentRow['severityBucket']): AlertDetailTone => {
if (severity === 'critical') return 'danger';
@@ -1,119 +0,0 @@
import { For, type Component } from 'solid-js';
import { Table, TableBody, TableCell, TableHead, TableRow } from '@/components/shared/Table';
export type TrueNASInlineDetailTone = 'default' | 'success' | 'warning' | 'danger' | 'muted';
export type TrueNASInlineDetailRow = {
label: string;
value: string;
title?: string;
tone?: TrueNASInlineDetailTone;
};
export type TrueNASInlineDetailSection = {
label: string;
rows: TrueNASInlineDetailRow[];
};
export const makeTrueNASInlineDetailRow = (
label: string,
value?: string | null,
options: Pick<TrueNASInlineDetailRow, 'title' | 'tone'> = {},
): TrueNASInlineDetailRow | null => {
const trimmed = value?.trim();
if (!trimmed || trimmed === '-') return null;
return { label, value: trimmed, ...options };
};
export const compactTrueNASInlineDetailRows = (
rows: Array<TrueNASInlineDetailRow | null>,
): TrueNASInlineDetailRow[] => rows.filter((row): row is TrueNASInlineDetailRow => Boolean(row));
export const compactTrueNASInlineDetailSections = (
sections: Array<TrueNASInlineDetailSection | null>,
): TrueNASInlineDetailSection[] =>
sections.filter((section): section is TrueNASInlineDetailSection =>
Boolean(section && section.rows.length > 0),
);
const detailValueToneClass = (tone: TrueNASInlineDetailTone | undefined): string => {
if (tone === 'success') return 'text-emerald-700 dark:text-emerald-300';
if (tone === 'warning') return 'text-amber-700 dark:text-amber-300';
if (tone === 'danger') return 'text-rose-700 dark:text-rose-300';
if (tone === 'muted') return 'text-muted';
return 'text-base-content';
};
const truenasDetailAttribute = (
detailKind: 'alert' | 'protection' | 'service' | undefined,
detailFor: string,
): Record<string, string> =>
detailKind ? { [`data-truenas-${detailKind}-detail-for`]: detailFor } : {};
export const TrueNASInlineDetailTable: Component<{
testId: string;
detailFor: string;
detailKind?: 'alert' | 'protection' | 'service';
title: string;
summary: string;
sections: TrueNASInlineDetailSection[];
onClose: () => void;
}> = (props) => (
<div
class="space-y-3"
data-testid={props.testId}
data-truenas-inline-detail-for={props.detailFor}
{...truenasDetailAttribute(props.detailKind, props.detailFor)}
>
<div class="flex flex-wrap items-start justify-between gap-3">
<div>
<div class="text-[11px] font-medium uppercase tracking-wide text-base-content">
{props.title}
</div>
<div class="mt-1 text-[10px] text-muted">{props.summary}</div>
</div>
<button
type="button"
class="inline-flex items-center rounded-md border border-border bg-surface px-2.5 py-1 text-[10px] font-medium text-base-content transition-colors hover:bg-base"
onClick={props.onClose}
>
Close
</button>
</div>
<div class="overflow-hidden rounded border border-border bg-surface">
<Table class="w-full table-fixed text-[11px]">
<TableBody class="divide-y divide-border">
<For each={props.sections}>
{(section) => (
<>
<TableRow class="bg-surface-alt">
<TableHead
colspan={2}
class="px-2 py-1 text-left text-[10px] font-semibold uppercase tracking-wide text-muted"
>
{section.label}
</TableHead>
</TableRow>
<For each={section.rows}>
{(row) => (
<TableRow>
<TableCell class="w-[38%] px-2 py-1 align-top text-muted">
{row.label}
</TableCell>
<TableCell
class={`px-2 py-1 text-right align-top font-medium ${detailValueToneClass(row.tone)}`}
title={row.title ?? row.value}
>
<span class="block truncate">{row.value}</span>
</TableCell>
</TableRow>
)}
</For>
</>
)}
</For>
</TableBody>
</Table>
</div>
</div>
);
@@ -46,12 +46,12 @@ import {
} from './truenasPageModel';
import {
TrueNASInlineDetailTable,
compactTrueNASInlineDetailRows,
compactTrueNASInlineDetailSections,
makeTrueNASInlineDetailRow,
type TrueNASInlineDetailSection,
type TrueNASInlineDetailTone,
} from './TrueNASInlineDetailTable';
compactTrueNASDetailRows,
compactTrueNASDetailSections,
makeTrueNASDetailRow,
type TrueNASDetailSection,
type TrueNASDetailTone,
} from '@/components/Infrastructure/TrueNASDetailTable';
const TRUENAS_PROTECTION_STATUS_OPTIONS: PlatformTableFilterOption<TrueNASProtectionStatusFilter>[] =
[
@@ -185,8 +185,8 @@ const formatPointTime = (point: RecoveryPoint): string => {
const sizeLabel = (point: RecoveryPoint): string =>
typeof point.sizeBytes === 'number' && point.sizeBytes > 0 ? formatBytes(point.sizeBytes) : '-';
type ProtectionDetailTone = TrueNASInlineDetailTone;
type ProtectionDetailSection = TrueNASInlineDetailSection;
type ProtectionDetailTone = TrueNASDetailTone;
type ProtectionDetailSection = TrueNASDetailSection;
const detailBool = (value?: boolean | null): string | null => {
if (value == null) return null;
@@ -207,9 +207,9 @@ const detailDateTime = (value?: string | null): string | null => {
});
};
const detailRow = makeTrueNASInlineDetailRow;
const compactDetailRows = compactTrueNASInlineDetailRows;
const compactDetailSections = compactTrueNASInlineDetailSections;
const detailRow = makeTrueNASDetailRow;
const compactDetailRows = compactTrueNASDetailRows;
const compactDetailSections = compactTrueNASDetailSections;
const outcomeTone = (point: RecoveryPoint): ProtectionDetailTone => {
const outcome = normalizeRecoveryOutcome(point.outcome);
@@ -34,12 +34,12 @@ import {
} from './truenasPageModel';
import {
TrueNASInlineDetailTable,
compactTrueNASInlineDetailRows,
compactTrueNASInlineDetailSections,
makeTrueNASInlineDetailRow,
type TrueNASInlineDetailSection,
type TrueNASInlineDetailTone,
} from './TrueNASInlineDetailTable';
compactTrueNASDetailRows,
compactTrueNASDetailSections,
makeTrueNASDetailRow,
type TrueNASDetailSection,
type TrueNASDetailTone,
} from '@/components/Infrastructure/TrueNASDetailTable';
const TRUENAS_SERVICE_STATUS_OPTIONS: PlatformTableFilterOption<TrueNASServiceStatusFilter>[] = [
{ value: 'all', label: 'All' },
@@ -90,17 +90,17 @@ const serviceStatusVariant = (
return 'muted';
};
type ServiceDetailTone = TrueNASInlineDetailTone;
type ServiceDetailSection = TrueNASInlineDetailSection;
type ServiceDetailTone = TrueNASDetailTone;
type ServiceDetailSection = TrueNASDetailSection;
const detailBool = (value?: boolean): string | null => {
if (value === undefined) return null;
return value ? 'Enabled' : 'Disabled';
};
const detailRow = makeTrueNASInlineDetailRow;
const compactDetailRows = compactTrueNASInlineDetailRows;
const compactDetailSections = compactTrueNASInlineDetailSections;
const detailRow = makeTrueNASDetailRow;
const compactDetailRows = compactTrueNASDetailRows;
const compactDetailSections = compactTrueNASDetailSections;
const serviceTone = (row: TrueNASServiceRow): ServiceDetailTone => {
const status = mapTrueNASServiceStatus(row);