mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-21 03:05:44 +00:00
Extract shared TrueNAS detail table
This commit is contained in:
@@ -31,6 +31,14 @@ import {
|
||||
type TrueNASIncidentRow,
|
||||
type TrueNASIncidentSeverityFilter,
|
||||
} from './truenasPageModel';
|
||||
import {
|
||||
TrueNASInlineDetailTable,
|
||||
compactTrueNASInlineDetailRows,
|
||||
compactTrueNASInlineDetailSections,
|
||||
makeTrueNASInlineDetailRow,
|
||||
type TrueNASInlineDetailSection,
|
||||
type TrueNASInlineDetailTone,
|
||||
} from './TrueNASInlineDetailTable';
|
||||
import type { Resource, ResourceType } from '@/types/resource';
|
||||
import { getAlertFilteredEmptyState } from '@/utils/alertOverviewPresentation';
|
||||
|
||||
@@ -116,19 +124,8 @@ const formatStartedAt = (value: string | undefined): string => {
|
||||
});
|
||||
};
|
||||
|
||||
type AlertDetailTone = 'default' | 'danger' | 'warning' | 'muted';
|
||||
|
||||
type AlertDetailRow = {
|
||||
label: string;
|
||||
value: string;
|
||||
title?: string;
|
||||
tone?: AlertDetailTone;
|
||||
};
|
||||
|
||||
type AlertDetailSection = {
|
||||
label: string;
|
||||
rows: AlertDetailRow[];
|
||||
};
|
||||
type AlertDetailTone = TrueNASInlineDetailTone;
|
||||
type AlertDetailSection = TrueNASInlineDetailSection;
|
||||
|
||||
const detailDateTime = (value?: string): string | null => {
|
||||
if (!value) return null;
|
||||
@@ -143,23 +140,9 @@ const detailDateTime = (value?: string): string | null => {
|
||||
});
|
||||
};
|
||||
|
||||
const detailRow = (
|
||||
label: string,
|
||||
value?: string | null,
|
||||
options: Pick<AlertDetailRow, 'title' | 'tone'> = {},
|
||||
): AlertDetailRow | null => {
|
||||
const trimmed = value?.trim();
|
||||
if (!trimmed || trimmed === '-') return null;
|
||||
return { label, value: trimmed, ...options };
|
||||
};
|
||||
|
||||
const compactDetailRows = (rows: Array<AlertDetailRow | null>): AlertDetailRow[] =>
|
||||
rows.filter((row): row is AlertDetailRow => Boolean(row));
|
||||
|
||||
const compactDetailSections = (sections: Array<AlertDetailSection | null>): AlertDetailSection[] =>
|
||||
sections.filter((section): section is AlertDetailSection =>
|
||||
Boolean(section && section.rows.length > 0),
|
||||
);
|
||||
const detailRow = makeTrueNASInlineDetailRow;
|
||||
const compactDetailRows = compactTrueNASInlineDetailRows;
|
||||
const compactDetailSections = compactTrueNASInlineDetailSections;
|
||||
|
||||
const alertTone = (severity: TrueNASIncidentRow['severityBucket']): AlertDetailTone => {
|
||||
if (severity === 'critical') return 'danger';
|
||||
@@ -167,13 +150,6 @@ const alertTone = (severity: TrueNASIncidentRow['severityBucket']): AlertDetailT
|
||||
return 'muted';
|
||||
};
|
||||
|
||||
const detailValueToneClass = (tone: AlertDetailTone | undefined): string => {
|
||||
if (tone === 'danger') return 'text-rose-700 dark:text-rose-300';
|
||||
if (tone === 'warning') return 'text-amber-700 dark:text-amber-300';
|
||||
if (tone === 'muted') return 'text-muted';
|
||||
return 'text-base-content';
|
||||
};
|
||||
|
||||
const buildAlertDetailSections = (incident: TrueNASIncidentRow): AlertDetailSection[] => {
|
||||
const parentName = incident.resource.parentName?.trim();
|
||||
return compactDetailSections([
|
||||
@@ -214,70 +190,17 @@ const buildAlertDetailSections = (incident: TrueNASIncidentRow): AlertDetailSect
|
||||
|
||||
const AlertDetailTable: Component<{ incident: TrueNASIncidentRow; onClose: () => void }> = (
|
||||
props,
|
||||
) => {
|
||||
const sections = () => buildAlertDetailSections(props.incident);
|
||||
|
||||
return (
|
||||
<div
|
||||
class="space-y-3"
|
||||
data-testid="truenas-alert-detail"
|
||||
data-truenas-alert-detail-for={props.incident.id}
|
||||
>
|
||||
<div class="flex flex-wrap items-start justify-between gap-3">
|
||||
<div>
|
||||
<div class="text-[11px] font-medium uppercase tracking-wide text-base-content">
|
||||
Alert detail
|
||||
</div>
|
||||
<div class="mt-1 text-[10px] text-muted">
|
||||
{severityLabel(props.incident.severity)} · {formatCode(props.incident.code)}
|
||||
</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={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>
|
||||
);
|
||||
};
|
||||
) => (
|
||||
<TrueNASInlineDetailTable
|
||||
testId="truenas-alert-detail"
|
||||
detailFor={props.incident.id}
|
||||
detailKind="alert"
|
||||
title="Alert detail"
|
||||
summary={`${severityLabel(props.incident.severity)} · ${formatCode(props.incident.code)}`}
|
||||
sections={buildAlertDetailSections(props.incident)}
|
||||
onClose={props.onClose}
|
||||
/>
|
||||
);
|
||||
|
||||
export const TrueNASAlertsTable: Component<{
|
||||
incidents: TrueNASIncidentRow[];
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
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>
|
||||
);
|
||||
@@ -44,6 +44,14 @@ import {
|
||||
type TrueNASProtectionKind,
|
||||
type TrueNASProtectionStatusFilter,
|
||||
} from './truenasPageModel';
|
||||
import {
|
||||
TrueNASInlineDetailTable,
|
||||
compactTrueNASInlineDetailRows,
|
||||
compactTrueNASInlineDetailSections,
|
||||
makeTrueNASInlineDetailRow,
|
||||
type TrueNASInlineDetailSection,
|
||||
type TrueNASInlineDetailTone,
|
||||
} from './TrueNASInlineDetailTable';
|
||||
|
||||
const TRUENAS_PROTECTION_STATUS_OPTIONS: PlatformTableFilterOption<TrueNASProtectionStatusFilter>[] =
|
||||
[
|
||||
@@ -177,19 +185,8 @@ const formatPointTime = (point: RecoveryPoint): string => {
|
||||
const sizeLabel = (point: RecoveryPoint): string =>
|
||||
typeof point.sizeBytes === 'number' && point.sizeBytes > 0 ? formatBytes(point.sizeBytes) : '-';
|
||||
|
||||
type ProtectionDetailTone = 'default' | 'success' | 'warning' | 'danger' | 'muted';
|
||||
|
||||
type ProtectionDetailRow = {
|
||||
label: string;
|
||||
value: string;
|
||||
title?: string;
|
||||
tone?: ProtectionDetailTone;
|
||||
};
|
||||
|
||||
type ProtectionDetailSection = {
|
||||
label: string;
|
||||
rows: ProtectionDetailRow[];
|
||||
};
|
||||
type ProtectionDetailTone = TrueNASInlineDetailTone;
|
||||
type ProtectionDetailSection = TrueNASInlineDetailSection;
|
||||
|
||||
const detailBool = (value?: boolean | null): string | null => {
|
||||
if (value == null) return null;
|
||||
@@ -210,33 +207,9 @@ const detailDateTime = (value?: string | null): string | null => {
|
||||
});
|
||||
};
|
||||
|
||||
const detailRow = (
|
||||
label: string,
|
||||
value?: string | null,
|
||||
options: Pick<ProtectionDetailRow, 'title' | 'tone'> = {},
|
||||
): ProtectionDetailRow | null => {
|
||||
const trimmed = value?.trim();
|
||||
if (!trimmed || trimmed === '-') return null;
|
||||
return { label, value: trimmed, ...options };
|
||||
};
|
||||
|
||||
const compactDetailRows = (rows: Array<ProtectionDetailRow | null>): ProtectionDetailRow[] =>
|
||||
rows.filter((row): row is ProtectionDetailRow => Boolean(row));
|
||||
|
||||
const compactDetailSections = (
|
||||
sections: Array<ProtectionDetailSection | null>,
|
||||
): ProtectionDetailSection[] =>
|
||||
sections.filter((section): section is ProtectionDetailSection =>
|
||||
Boolean(section && section.rows.length > 0),
|
||||
);
|
||||
|
||||
const detailValueToneClass = (tone: ProtectionDetailTone | 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 detailRow = makeTrueNASInlineDetailRow;
|
||||
const compactDetailRows = compactTrueNASInlineDetailRows;
|
||||
const compactDetailSections = compactTrueNASInlineDetailSections;
|
||||
|
||||
const outcomeTone = (point: RecoveryPoint): ProtectionDetailTone => {
|
||||
const outcome = normalizeRecoveryOutcome(point.outcome);
|
||||
@@ -326,71 +299,19 @@ const DatasetCell: Component<{ point: RecoveryPoint }> = (props) => {
|
||||
);
|
||||
};
|
||||
|
||||
const ProtectionDetailTable: Component<{ point: RecoveryPoint; onClose: () => void }> = (props) => {
|
||||
const sections = createMemo(() => buildProtectionDetailSections(props.point));
|
||||
|
||||
return (
|
||||
<div
|
||||
class="space-y-3"
|
||||
data-testid="truenas-protection-detail"
|
||||
data-truenas-protection-detail-for={props.point.id}
|
||||
>
|
||||
<div class="flex flex-wrap items-start justify-between gap-3">
|
||||
<div>
|
||||
<div class="text-[11px] font-medium uppercase tracking-wide text-base-content">
|
||||
Protection detail
|
||||
</div>
|
||||
<div class="mt-1 text-[10px] text-muted">
|
||||
{kindLabel(mapTrueNASProtectionKind(props.point))} ·{' '}
|
||||
{getRecoveryOutcomeLabel(normalizeRecoveryOutcome(props.point.outcome))}
|
||||
</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={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>
|
||||
);
|
||||
};
|
||||
const ProtectionDetailTable: Component<{ point: RecoveryPoint; onClose: () => void }> = (props) => (
|
||||
<TrueNASInlineDetailTable
|
||||
testId="truenas-protection-detail"
|
||||
detailFor={props.point.id}
|
||||
detailKind="protection"
|
||||
title="Protection detail"
|
||||
summary={`${kindLabel(mapTrueNASProtectionKind(props.point))} · ${getRecoveryOutcomeLabel(
|
||||
normalizeRecoveryOutcome(props.point.outcome),
|
||||
)}`}
|
||||
sections={buildProtectionDetailSections(props.point)}
|
||||
onClose={props.onClose}
|
||||
/>
|
||||
);
|
||||
|
||||
export const TrueNASProtectionTable: Component<{
|
||||
points: RecoveryPoint[];
|
||||
|
||||
@@ -32,6 +32,14 @@ import {
|
||||
type TrueNASServiceRow,
|
||||
type TrueNASServiceStatusFilter,
|
||||
} from './truenasPageModel';
|
||||
import {
|
||||
TrueNASInlineDetailTable,
|
||||
compactTrueNASInlineDetailRows,
|
||||
compactTrueNASInlineDetailSections,
|
||||
makeTrueNASInlineDetailRow,
|
||||
type TrueNASInlineDetailSection,
|
||||
type TrueNASInlineDetailTone,
|
||||
} from './TrueNASInlineDetailTable';
|
||||
|
||||
const TRUENAS_SERVICE_STATUS_OPTIONS: PlatformTableFilterOption<TrueNASServiceStatusFilter>[] = [
|
||||
{ value: 'all', label: 'All' },
|
||||
@@ -82,44 +90,17 @@ const serviceStatusVariant = (
|
||||
return 'muted';
|
||||
};
|
||||
|
||||
type ServiceDetailTone = 'default' | 'success' | 'warning' | 'danger' | 'muted';
|
||||
|
||||
type ServiceDetailRow = {
|
||||
label: string;
|
||||
value: string;
|
||||
title?: string;
|
||||
tone?: ServiceDetailTone;
|
||||
};
|
||||
|
||||
type ServiceDetailSection = {
|
||||
label: string;
|
||||
rows: ServiceDetailRow[];
|
||||
};
|
||||
type ServiceDetailTone = TrueNASInlineDetailTone;
|
||||
type ServiceDetailSection = TrueNASInlineDetailSection;
|
||||
|
||||
const detailBool = (value?: boolean): string | null => {
|
||||
if (value === undefined) return null;
|
||||
return value ? 'Enabled' : 'Disabled';
|
||||
};
|
||||
|
||||
const detailRow = (
|
||||
label: string,
|
||||
value?: string | null,
|
||||
options: Pick<ServiceDetailRow, 'title' | 'tone'> = {},
|
||||
): ServiceDetailRow | null => {
|
||||
const trimmed = value?.trim();
|
||||
if (!trimmed || trimmed === '-') return null;
|
||||
return { label, value: trimmed, ...options };
|
||||
};
|
||||
|
||||
const compactDetailRows = (rows: Array<ServiceDetailRow | null>): ServiceDetailRow[] =>
|
||||
rows.filter((row): row is ServiceDetailRow => Boolean(row));
|
||||
|
||||
const compactDetailSections = (
|
||||
sections: Array<ServiceDetailSection | null>,
|
||||
): ServiceDetailSection[] =>
|
||||
sections.filter((section): section is ServiceDetailSection =>
|
||||
Boolean(section && section.rows.length > 0),
|
||||
);
|
||||
const detailRow = makeTrueNASInlineDetailRow;
|
||||
const compactDetailRows = compactTrueNASInlineDetailRows;
|
||||
const compactDetailSections = compactTrueNASInlineDetailSections;
|
||||
|
||||
const serviceTone = (row: TrueNASServiceRow): ServiceDetailTone => {
|
||||
const status = mapTrueNASServiceStatus(row);
|
||||
@@ -129,14 +110,6 @@ const serviceTone = (row: TrueNASServiceRow): ServiceDetailTone => {
|
||||
return 'default';
|
||||
};
|
||||
|
||||
const detailValueToneClass = (tone: ServiceDetailTone | 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 formatAllPIDs = (pids: number[] | undefined): { label: string | null; title?: string } => {
|
||||
const values = (pids ?? []).filter((pid) => Number.isFinite(pid) && pid > 0);
|
||||
if (values.length === 0) return { label: null };
|
||||
@@ -180,71 +153,19 @@ const buildServiceDetailSections = (row: TrueNASServiceRow): ServiceDetailSectio
|
||||
]);
|
||||
};
|
||||
|
||||
const ServiceDetailTable: Component<{ row: TrueNASServiceRow; onClose: () => void }> = (props) => {
|
||||
const sections = createMemo(() => buildServiceDetailSections(props.row));
|
||||
|
||||
return (
|
||||
<div
|
||||
class="space-y-3"
|
||||
data-testid="truenas-service-detail"
|
||||
data-truenas-service-detail-for={props.row.id}
|
||||
>
|
||||
<div class="flex flex-wrap items-start justify-between gap-3">
|
||||
<div>
|
||||
<div class="text-[11px] font-medium uppercase tracking-wide text-base-content">
|
||||
Service detail
|
||||
</div>
|
||||
<div class="mt-1 text-[10px] text-muted">
|
||||
{formatServiceName(props.row.service.service)} ·{' '}
|
||||
{titleCase(mapTrueNASServiceStatus(props.row))}
|
||||
</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={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>
|
||||
);
|
||||
};
|
||||
const ServiceDetailTable: Component<{ row: TrueNASServiceRow; onClose: () => void }> = (props) => (
|
||||
<TrueNASInlineDetailTable
|
||||
testId="truenas-service-detail"
|
||||
detailFor={props.row.id}
|
||||
detailKind="service"
|
||||
title="Service detail"
|
||||
summary={`${formatServiceName(props.row.service.service)} · ${titleCase(
|
||||
mapTrueNASServiceStatus(props.row),
|
||||
)}`}
|
||||
sections={buildServiceDetailSections(props.row)}
|
||||
onClose={props.onClose}
|
||||
/>
|
||||
);
|
||||
|
||||
export const TrueNASServicesTable: Component<{
|
||||
services: TrueNASServiceRow[];
|
||||
@@ -391,10 +312,7 @@ export const TrueNASServicesTable: Component<{
|
||||
class="px-2 py-3 sm:px-4 sm:py-4"
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
>
|
||||
<ServiceDetailTable
|
||||
row={row}
|
||||
onClose={() => detail.close(row)}
|
||||
/>
|
||||
<ServiceDetailTable row={row} onClose={() => detail.close(row)} />
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
||||
Reference in New Issue
Block a user