From b63953ee0ebd8567e5c26226b69ff2e554790905 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Fri, 22 May 2026 06:25:22 +0100 Subject: [PATCH] Share TrueNAS detail table rendering --- .../ResourceDetailDrawerOverviewTab.tsx | 57 +-------- .../Infrastructure/TrueNASDetailTable.tsx | 104 +++++++++++++++ .../resourceDetailDrawerTrueNASModel.ts | 46 +++---- .../Infrastructure/trueNASDetailTableModel.ts | 34 +++++ .../features/truenas/TrueNASAlertsTable.tsx | 22 ++-- .../truenas/TrueNASInlineDetailTable.tsx | 119 ------------------ .../truenas/TrueNASProtectionTable.tsx | 22 ++-- .../features/truenas/TrueNASServicesTable.tsx | 22 ++-- 8 files changed, 187 insertions(+), 239 deletions(-) create mode 100644 frontend-modern/src/components/Infrastructure/TrueNASDetailTable.tsx create mode 100644 frontend-modern/src/components/Infrastructure/trueNASDetailTableModel.ts delete mode 100644 frontend-modern/src/features/truenas/TrueNASInlineDetailTable.tsx diff --git a/frontend-modern/src/components/Infrastructure/ResourceDetailDrawerOverviewTab.tsx b/frontend-modern/src/components/Infrastructure/ResourceDetailDrawerOverviewTab.tsx index 1196accabe..2443d495fd 100644 --- a/frontend-modern/src/components/Infrastructure/ResourceDetailDrawerOverviewTab.tsx +++ b/frontend-modern/src/components/Infrastructure/ResourceDetailDrawerOverviewTab.tsx @@ -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) => ( -
- - - - {(section) => ( - <> - - - - - {(row) => ( - - - - - )} - - - )} - - -
- {section.label} -
{row.label} - {row.value} -
-
-); - 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" > - + ); diff --git a/frontend-modern/src/components/Infrastructure/TrueNASDetailTable.tsx b/frontend-modern/src/components/Infrastructure/TrueNASDetailTable.tsx new file mode 100644 index 0000000000..e2c8ec3844 --- /dev/null +++ b/frontend-modern/src/components/Infrastructure/TrueNASDetailTable.tsx @@ -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 => + detailKind ? { [`data-truenas-${detailKind}-detail-for`]: detailFor } : {}; + +export const TrueNASDetailSectionTable: Component<{ + sections: TrueNASDetailSection[]; + class?: string; +}> = (props) => ( +
+ + + + {(section) => ( + <> + + + {section.label} + + + + {(row) => ( + + + {row.label} + + + {row.value} + + + )} + + + )} + + +
+
+); + +export const TrueNASInlineDetailTable: Component<{ + testId: string; + detailFor: string; + detailKind?: 'alert' | 'protection' | 'service'; + title: string; + summary: string; + sections: TrueNASDetailSection[]; + onClose: () => void; +}> = (props) => ( +
+
+
+
+ {props.title} +
+
{props.summary}
+
+ +
+ +
+); diff --git a/frontend-modern/src/components/Infrastructure/resourceDetailDrawerTrueNASModel.ts b/frontend-modern/src/components/Infrastructure/resourceDetailDrawerTrueNASModel.ts index 8ead26d318..7c134dcada 100644 --- a/frontend-modern/src/components/Infrastructure/resourceDetailDrawerTrueNASModel.ts +++ b/frontend-modern/src/components/Infrastructure/resourceDetailDrawerTrueNASModel.ts @@ -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 | 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 row = makeTrueNASDetailRow; +const compactRows = compactTrueNASDetailRows; +const compactSections = compactTrueNASDetailSections; const isTrueNASScopedResource = (resource: Resource): boolean => resource.platformType === 'truenas' || diff --git a/frontend-modern/src/components/Infrastructure/trueNASDetailTableModel.ts b/frontend-modern/src/components/Infrastructure/trueNASDetailTableModel.ts new file mode 100644 index 0000000000..b05576d9ae --- /dev/null +++ b/frontend-modern/src/components/Infrastructure/trueNASDetailTableModel.ts @@ -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 | null => { + const trimmed = value?.trim(); + if (!trimmed || trimmed === '-') return null; + return { label, value: trimmed, ...options }; +}; + +export const compactTrueNASDetailRows = ( + rows: Array, +): TrueNASDetailRow[] => rows.filter((row): row is TrueNASDetailRow => Boolean(row)); + +export const compactTrueNASDetailSections = ( + sections: Array, +): TrueNASDetailSection[] => + sections.filter((section): section is TrueNASDetailSection => + Boolean(section && section.rows.length > 0), + ); diff --git a/frontend-modern/src/features/truenas/TrueNASAlertsTable.tsx b/frontend-modern/src/features/truenas/TrueNASAlertsTable.tsx index 71aec1c555..8030029281 100644 --- a/frontend-modern/src/features/truenas/TrueNASAlertsTable.tsx +++ b/frontend-modern/src/features/truenas/TrueNASAlertsTable.tsx @@ -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'; diff --git a/frontend-modern/src/features/truenas/TrueNASInlineDetailTable.tsx b/frontend-modern/src/features/truenas/TrueNASInlineDetailTable.tsx deleted file mode 100644 index 8d201133f1..0000000000 --- a/frontend-modern/src/features/truenas/TrueNASInlineDetailTable.tsx +++ /dev/null @@ -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 | null => { - const trimmed = value?.trim(); - if (!trimmed || trimmed === '-') return null; - return { label, value: trimmed, ...options }; -}; - -export const compactTrueNASInlineDetailRows = ( - rows: Array, -): TrueNASInlineDetailRow[] => rows.filter((row): row is TrueNASInlineDetailRow => Boolean(row)); - -export const compactTrueNASInlineDetailSections = ( - sections: Array, -): 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 => - 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) => ( -
-
-
-
- {props.title} -
-
{props.summary}
-
- -
-
- - - - {(section) => ( - <> - - - {section.label} - - - - {(row) => ( - - - {row.label} - - - {row.value} - - - )} - - - )} - - -
-
-
-); diff --git a/frontend-modern/src/features/truenas/TrueNASProtectionTable.tsx b/frontend-modern/src/features/truenas/TrueNASProtectionTable.tsx index e7b765c44b..91c3b82bad 100644 --- a/frontend-modern/src/features/truenas/TrueNASProtectionTable.tsx +++ b/frontend-modern/src/features/truenas/TrueNASProtectionTable.tsx @@ -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[] = [ @@ -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); diff --git a/frontend-modern/src/features/truenas/TrueNASServicesTable.tsx b/frontend-modern/src/features/truenas/TrueNASServicesTable.tsx index aec4f8bfa9..9dd0affee2 100644 --- a/frontend-modern/src/features/truenas/TrueNASServicesTable.tsx +++ b/frontend-modern/src/features/truenas/TrueNASServicesTable.tsx @@ -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[] = [ { 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);