Canonicalize platform row detail toggles

This commit is contained in:
rcourtman
2026-06-12 12:11:50 +01:00
parent b179b1782c
commit 2169d94b41
47 changed files with 539 additions and 39 deletions
@@ -156,6 +156,7 @@ work extends shared components instead of creating new local variants.
129. `frontend-modern/scripts/shared-template-audit.mjs`
130. `frontend-modern/scripts/shared-template-registry.json`
131. `frontend-modern/src/features/platformPage/sharedPlatformPage.tsx`
131a. `frontend-modern/src/features/platformPage/PlatformResourceDetailTableRow.tsx`
132. `frontend-modern/src/utils/platformSupportManifest.generated.ts`
133. `frontend-modern/src/utils/platformSupportManifest.ts`
134. `frontend-modern/src/utils/sourcePlatformOptions.ts`
@@ -2382,6 +2383,14 @@ divide frame locally. Platform table consumers must preserve the owner split:
frontend-primitives owns the repeated `PlatformTableShell` frame and guardrail
registry, while platform and unified-resource consumers own the source-specific
row fields, drawers, and resource semantics.
Platform row-detail disclosure controls are also registry-backed templates.
`frontend-modern/src/features/platformPage/PlatformResourceDetailTableRow.tsx`
owns `PlatformResourceDetailToggleButton`, which composes
`SummaryRowActionButton` for the canonical row-detail affordance, accessible
label, `aria-expanded`, `aria-controls`, and propagation containment. Platform
tables that use `createPlatformResourceDetailState` or render local inline
detail rows must compose that toggle; they may still own the detail row content,
drawer payload, and platform-specific fields.
The shared help icon now follows that same owner split.
`frontend-modern/src/components/shared/HelpIcon.tsx` stays the render shell,
`frontend-modern/src/components/shared/useHelpIconState.ts` owns open state,
@@ -3142,6 +3142,7 @@
"frontend-modern/src/components/SetupWizard/SetupCompletionPreview.tsx",
"frontend-modern/src/components/SetupWizard/SetupWizard.tsx",
"frontend-modern/src/components/Toast/Toast.tsx",
"frontend-modern/src/features/platformPage/PlatformResourceDetailTableRow.tsx",
"frontend-modern/src/features/platformPage/sharedPlatformPage.tsx",
"frontend-modern/src/hooks/createNonSuspendingQuery.ts",
"frontend-modern/src/index.css",
@@ -3576,6 +3577,7 @@
"match_files": [
"frontend-modern/scripts/shared-template-audit.mjs",
"frontend-modern/scripts/shared-template-registry.json",
"frontend-modern/src/features/platformPage/PlatformResourceDetailTableRow.tsx",
"frontend-modern/src/features/platformPage/sharedPlatformPage.tsx"
],
"allow_same_subsystem_tests": false,
@@ -214,6 +214,14 @@ filter semantics, drawer handoffs, and resource projections, while
Future platform tables must keep that split: row data and platform semantics
stay in the unified-resource consumer, and the repeated table shell stays in the
shared frontend primitive.
That same split applies to platform row-detail disclosure controls. Platform
tables own which row opens, which resource label is exposed, and which
source-specific detail payload or drawer is rendered, but the disclosure
affordance itself must compose
`PlatformResourceDetailToggleButton` from the frontend-primitives-owned
`PlatformResourceDetailTableRow.tsx` contract. Future platform tables must not
add page-local chevron buttons, bespoke `aria-expanded` handling, or local
event-propagation variants for row detail expansion.
Product-originated resource references may arrive as registered unified
resource IDs, source-specific IDs, or canonical identity aliases. The
unified-resource registry owns resolving those references through
@@ -12,6 +12,9 @@ const toRegistryPath = (path) => relative(root, path).split('\\').join('/');
const registry = readJson(registryPath);
const rules = Array.isArray(registry.rules) ? registry.rules : [];
const patternGuards = Array.isArray(registry.patternGuards) ? registry.patternGuards : [];
const requiredPatternGuards = Array.isArray(registry.requiredPatternGuards)
? registry.requiredPatternGuards
: [];
const failures = [];
@@ -156,6 +159,79 @@ for (const guard of patternGuards) {
}
}
for (const guard of requiredPatternGuards) {
if (!guard.id) failures.push('required pattern guard is missing id');
if (!guard.summary) failures.push(`${guard.id}: missing summary`);
if (!guard.category) failures.push(`${guard.id}: missing category`);
if (!guard.canonical?.path) failures.push(`${guard.id}: missing canonical.path`);
if (!guard.id || !guard.canonical?.path) continue;
verifyCanonicalExport(guard.id, guard.canonical);
const triggerPatterns = Array.isArray(guard.triggerPatterns) ? guard.triggerPatterns : [];
if (triggerPatterns.length === 0) {
failures.push(`${guard.id}: missing triggerPatterns`);
continue;
}
const requiredPatterns = Array.isArray(guard.requiredPatterns) ? guard.requiredPatterns : [];
if (requiredPatterns.length === 0) {
failures.push(`${guard.id}: missing requiredPatterns`);
continue;
}
const scopes = Array.isArray(guard.scopes) ? guard.scopes : [];
if (scopes.length === 0) {
failures.push(`${guard.id}: missing scopes`);
continue;
}
const extensions = guard.extensions ?? ['.ts', '.tsx'];
const allowedEntries = guard.allowedPaths ?? [];
const allowedPaths = new Set(allowedEntries.map(getAllowedPath).filter(Boolean));
const files = [...new Set(scopes.flatMap((scope) => collectFiles(scope, extensions)))].sort();
for (const file of files) {
if (file === guard.canonical.path) continue;
const source = read(file);
const triggered = triggerPatterns.every((pattern) => source.includes(pattern));
if (!triggered) continue;
if (allowedPaths.has(file)) continue;
for (const pattern of requiredPatterns) {
if (!source.includes(pattern)) {
failures.push(
`${guard.id}: ${file} matches ${JSON.stringify(
triggerPatterns,
)} but does not compose ${guard.canonical.path}`,
);
break;
}
}
}
for (const allowedPath of allowedPaths) {
if (!fileExists(allowedPath)) {
failures.push(`${guard.id}: allowed path ${allowedPath} does not exist`);
continue;
}
if (allowedPath === guard.canonical.path) {
failures.push(`${guard.id}: canonical path ${allowedPath} must not be allowlisted`);
continue;
}
const source = read(allowedPath);
const stillTriggered = triggerPatterns.every((pattern) => source.includes(pattern));
if (!stillTriggered) {
failures.push(`${guard.id}: allowed path ${allowedPath} is stale; remove it from the guard`);
}
}
}
if (failures.length > 0) {
console.error('Shared template audit failed:');
for (const failure of failures) {
@@ -167,7 +243,9 @@ if (failures.length > 0) {
console.log(
`Shared template audit passed (${rules.length} rule${rules.length === 1 ? '' : 's'}, ${
patternGuards.length
} pattern guard${patternGuards.length === 1 ? '' : 's'} from ${relative(
} pattern guard${patternGuards.length === 1 ? '' : 's'}, ${
requiredPatternGuards.length
} required pattern guard${requiredPatternGuards.length === 1 ? '' : 's'} from ${relative(
process.cwd(),
resolve(root, registryPath),
)})`,
@@ -66,5 +66,57 @@
"scripts/shared-template-audit.mjs"
]
}
],
"requiredPatternGuards": [
{
"id": "platform-row-detail-toggle-shared-state",
"category": "row-disclosure",
"summary": "Platform rows using shared resource-detail expansion state must render the canonical disclosure toggle instead of relying on row-only click behavior or local chevrons.",
"canonical": {
"path": "src/features/platformPage/PlatformResourceDetailTableRow.tsx",
"export": "PlatformResourceDetailToggleButton"
},
"scopes": [
"src/features/docker",
"src/features/kubernetes",
"src/features/proxmox",
"src/features/standalone",
"src/features/truenas",
"src/features/vmware"
],
"extensions": [".tsx"],
"triggerPatterns": ["createPlatformResourceDetailState"],
"requiredPatterns": ["PlatformResourceDetailToggleButton"],
"allowedPaths": [],
"proof": [
"src/components/shared/SharedPrimitives.guardrails.test.ts",
"scripts/shared-template-audit.mjs"
]
},
{
"id": "platform-row-detail-toggle-inline-detail",
"category": "row-disclosure",
"summary": "Platform tables that render page-local inline detail rows must compose the canonical disclosure toggle in the owning row.",
"canonical": {
"path": "src/features/platformPage/PlatformResourceDetailTableRow.tsx",
"export": "PlatformResourceDetailToggleButton"
},
"scopes": [
"src/features/docker",
"src/features/kubernetes",
"src/features/proxmox",
"src/features/standalone",
"src/features/truenas",
"src/features/vmware"
],
"extensions": [".tsx"],
"triggerPatterns": ["data-inline", "aria-expanded"],
"requiredPatterns": ["PlatformResourceDetailToggleButton"],
"allowedPaths": [],
"proof": [
"src/components/shared/SharedPrimitives.guardrails.test.ts",
"scripts/shared-template-audit.mjs"
]
}
]
}
@@ -92,6 +92,7 @@ import webInterfaceUrlFieldStateSource from '@/components/shared/useWebInterface
import webInterfaceNameLinkSource from '@/components/shared/WebInterfaceNameLink.tsx?raw';
import sharedTemplateRegistrySource from '../../../scripts/shared-template-registry.json?raw';
import sharedPlatformPageSource from '@/features/platformPage/sharedPlatformPage.tsx?raw';
import platformResourceDetailTableRowSource from '@/features/platformPage/PlatformResourceDetailTableRow.tsx?raw';
import upgradeNavigationSource from '@/utils/upgradeNavigation.ts?raw';
import guestRowSource from '@/components/Workloads/GuestRow.tsx?raw';
import workloadsTableSource from '@/components/Workloads/WorkloadsTable.tsx?raw';
@@ -100,6 +101,7 @@ import guestRowStateSource from '@/components/Workloads/useGuestRowState.ts?raw'
import workloadSelectionStateSource from '@/components/Workloads/useWorkloadSelectionState.ts?raw';
import dockerHostsTableSource from '@/features/docker/DockerHostsTable.tsx?raw';
import kubernetesNodesTableSource from '@/features/kubernetes/KubernetesNodesTable.tsx?raw';
import proxmoxCoverageTableSource from '@/features/proxmox/ProxmoxCoverageTable.tsx?raw';
import proxmoxNodesTableSource from '@/features/proxmox/ProxmoxNodesTable.tsx?raw';
import vsphereHostsTableSource from '@/features/vmware/VsphereHostsTable.tsx?raw';
import agentsMachinesTableSource from '@/features/standalone/AgentsMachinesTable.tsx?raw';
@@ -626,6 +628,8 @@ describe('shared primitive guardrails', () => {
expect(summaryRowActionButtonSource).toContain('aria-expanded');
expect(summaryRowActionButtonSource).toContain('aria-pressed');
expect(summaryRowActionButtonSource).toContain('data-row-action="true"');
expect(platformResourceDetailTableRowSource).toContain('PlatformResourceDetailToggleButton');
expect(platformResourceDetailTableRowSource).toContain('SummaryRowActionButton');
for (const source of [
guestRowSource,
@@ -981,6 +985,54 @@ describe('shared primitive guardrails', () => {
}
});
it('keeps platform row-detail disclosure controls on the shared toggle primitive', () => {
const registry = JSON.parse(sharedTemplateRegistrySource) as {
requiredPatternGuards?: Array<{
id: string;
canonical?: { path?: string; export?: string };
triggerPatterns?: string[];
requiredPatterns?: string[];
allowedPaths?: string[];
}>;
};
const requiredGuards = registry.requiredPatternGuards ?? [];
const sharedStateGuard = requiredGuards.find(
(guard) => guard.id === 'platform-row-detail-toggle-shared-state',
);
const inlineDetailGuard = requiredGuards.find(
(guard) => guard.id === 'platform-row-detail-toggle-inline-detail',
);
for (const guard of [sharedStateGuard, inlineDetailGuard]) {
expect(guard?.canonical?.path).toBe(
'src/features/platformPage/PlatformResourceDetailTableRow.tsx',
);
expect(guard?.canonical?.export).toBe('PlatformResourceDetailToggleButton');
expect(guard?.requiredPatterns).toContain('PlatformResourceDetailToggleButton');
expect(guard?.allowedPaths ?? []).toHaveLength(0);
}
expect(sharedStateGuard?.triggerPatterns).toContain('createPlatformResourceDetailState');
expect(inlineDetailGuard?.triggerPatterns).toEqual(['data-inline', 'aria-expanded']);
expect(platformResourceDetailTableRowSource).toContain(
'export const PlatformResourceDetailToggleButton',
);
expect(platformResourceDetailTableRowSource).toContain('subjectLabel={`details for');
expect(platformResourceDetailTableRowSource).toContain('SummaryRowActionButton');
for (const source of [
dockerHostsTableSource,
kubernetesNodesTableSource,
proxmoxCoverageTableSource,
proxmoxNodesTableSource,
vsphereHostsTableSource,
agentsMachinesTableSource,
]) {
expect(source).toContain('PlatformResourceDetailToggleButton');
}
expect(agentsMachinesTableSource).not.toContain('data-agent-machine-expand-icon');
});
it('keeps help icon on shell, runtime, and model owners', () => {
expect(helpIconSource).toContain('useHelpIconState');
expect(helpIconSource).not.toContain('getHelpContent(');
@@ -13,6 +13,7 @@ import {
PlatformTableShell,
} from '@/features/platformPage/sharedPlatformPage';
import {
PlatformResourceDetailToggleButton,
createPlatformResourceDetailState,
getPlatformResourceDetailRowClass,
} from '@/features/platformPage/PlatformResourceDetailTableRow';
@@ -288,6 +289,12 @@ export const DockerAlertsTable: Component<{
>
<TableCell class={getPlatformTableCellClassForKind('name')}>
<div class="flex min-w-0 items-center gap-2">
<PlatformResourceDetailToggleButton
expanded={isExpanded()}
resourceLabel={incident.resourceName}
controlsId={detailRowId()}
onToggle={() => drawer.toggle(incident)}
/>
<StatusDot
size="sm"
variant={severityVariant(incident.severityBucket)}
@@ -19,6 +19,7 @@ import {
PlatformTableShell,
} from '@/features/platformPage/sharedPlatformPage';
import {
PlatformResourceDetailToggleButton,
PlatformResourceDetailTableRow,
createPlatformResourceDetailState,
createPlatformResourceLabelResolver,
@@ -28,6 +29,7 @@ import {
DockerResourceNameCell,
dockerHostName,
dockerJoinValues,
dockerResourceName,
dockerTextValue,
type DockerNativeTableProps,
} from './DockerNativeTableShared';
@@ -368,7 +370,18 @@ export const DockerContainersTable: Component<DockerNativeTableProps> = (props)
switch (column.id) {
case 'container':
return (
<DockerResourceNameCell resource={resource} indicator={indicator} />
<DockerResourceNameCell
resource={resource}
indicator={indicator}
detailToggle={
<PlatformResourceDetailToggleButton
expanded={isExpanded()}
resourceLabel={dockerResourceName(resource)}
controlsId={detailRowId()}
onToggle={() => drawer.toggle(resource)}
/>
}
/>
);
case 'host':
return (
@@ -18,6 +18,7 @@ import {
getPlatformTableCellClassForKind,
getPlatformTableHeadClassForKind,
} from '@/features/platformPage/sharedPlatformPage';
import { PlatformResourceDetailToggleButton } from '@/features/platformPage/PlatformResourceDetailTableRow';
import type { Disk } from '@/types/api';
import type { Resource } from '@/types/resource';
import {
@@ -277,6 +278,12 @@ export const DockerHostsTable: Component<{
class={`${getPlatformTableCellClassForKind('name')} w-[40%] md:w-auto`}
>
<div class="flex min-w-0 items-center gap-2">
<PlatformResourceDetailToggleButton
expanded={isSelected()}
resourceLabel={name()}
controlsId={detailRowId()}
onToggle={toggleDrawer}
/>
<StatusDot
size="sm"
variant={indicator().variant}
@@ -10,6 +10,7 @@ import {
PlatformTableShell,
} from '@/features/platformPage/sharedPlatformPage';
import {
PlatformResourceDetailToggleButton,
PlatformResourceDetailTableRow,
createPlatformResourceDetailState,
createPlatformResourceLabelResolver,
@@ -20,6 +21,7 @@ import {
dockerByteValue,
dockerHostName,
dockerJoinValues,
dockerResourceName,
dockerNumberValue,
type DockerNativeTableProps,
} from './DockerNativeTableShared';
@@ -118,7 +120,17 @@ export const DockerImagesTable: Component<DockerNativeTableProps> = (props) => {
onKeyDown={drawer.handleActivationKey(resource)}
tabIndex={0}
>
<DockerResourceNameCell resource={resource} />
<DockerResourceNameCell
resource={resource}
detailToggle={
<PlatformResourceDetailToggleButton
expanded={isExpanded()}
resourceLabel={dockerResourceName(resource)}
controlsId={detailRowId()}
onToggle={() => drawer.toggle(resource)}
/>
}
/>
<TableCell
class={`${getPlatformTableCellClassForKind('text')} text-base-content`}
>
@@ -55,7 +55,7 @@ export const dockerLabelsSummary = (labels: Record<string, string> | undefined):
export const dockerHostName = (resource: Resource): string =>
dockerTextValue(resource.docker?.hostname);
const dockerResourceName = (resource: Resource): string =>
export const dockerResourceName = (resource: Resource): string =>
asTrimmedString(resource.name) || asTrimmedString(resource.displayName) || resource.id;
export const DockerResourceNameCell: Component<{
@@ -64,6 +64,7 @@ export const DockerResourceNameCell: Component<{
// domain-specific mapper (mapDockerContainerStatus, mapDockerTaskStatus,
// etc.) rather than from the generic resource.status triad.
indicator?: StatusIndicator;
detailToggle?: JSX.Element;
}> = (props) => {
const resolvedIndicator = (): StatusIndicator =>
props.indicator ?? getSimpleStatusIndicator(props.resource.status);
@@ -72,6 +73,7 @@ export const DockerResourceNameCell: Component<{
return (
<TableCell class={getPlatformTableCellClassForKind('name')}>
<div class="flex min-w-0 items-center gap-2">
{props.detailToggle}
<StatusDot
size="sm"
variant={resolvedIndicator().variant}
@@ -12,6 +12,7 @@ import {
PlatformTableShell,
} from '@/features/platformPage/sharedPlatformPage';
import {
PlatformResourceDetailToggleButton,
createPlatformResourceDetailState,
getPlatformResourceDetailRowClass,
} from '@/features/platformPage/PlatformResourceDetailTableRow';
@@ -20,6 +21,7 @@ import {
DockerResourceNameCell,
dockerHostName,
dockerJoinValues,
dockerResourceName,
dockerTextValue,
type DockerNativeTableProps,
} from './DockerNativeTableShared';
@@ -445,7 +447,17 @@ export const DockerNetworksTable: Component<DockerNetworksTableProps> = (props)
onKeyDown={drawer.handleActivationKey(resource)}
tabIndex={0}
>
<DockerResourceNameCell resource={resource} />
<DockerResourceNameCell
resource={resource}
detailToggle={
<PlatformResourceDetailToggleButton
expanded={isExpanded()}
resourceLabel={dockerResourceName(resource)}
controlsId={detailRowId()}
onToggle={() => drawer.toggle(resource)}
/>
}
/>
<TableCell
class={`${getPlatformTableCellClassForKind('text')} text-base-content`}
>
@@ -10,6 +10,7 @@ import {
PlatformTableShell,
} from '@/features/platformPage/sharedPlatformPage';
import {
PlatformResourceDetailToggleButton,
PlatformResourceDetailTableRow,
createPlatformResourceDetailState,
createPlatformResourceLabelResolver,
@@ -19,6 +20,7 @@ import {
DockerResourceNameCell,
dockerByteValue,
dockerNumberValue,
dockerResourceName,
dockerTextValue,
type DockerNativeTableProps,
} from './DockerNativeTableShared';
@@ -145,7 +147,17 @@ export const DockerVolumesTable: Component<DockerNativeTableProps> = (props) =>
onKeyDown={drawer.handleActivationKey(resource)}
tabIndex={0}
>
<DockerResourceNameCell resource={resource} />
<DockerResourceNameCell
resource={resource}
detailToggle={
<PlatformResourceDetailToggleButton
expanded={isExpanded()}
resourceLabel={dockerResourceName(resource)}
controlsId={detailRowId()}
onToggle={() => drawer.toggle(resource)}
/>
}
/>
<TableCell
class={`${getPlatformTableCellClassForKind('text')} text-base-content`}
>
@@ -13,6 +13,7 @@ import {
PlatformTableShell,
} from '@/features/platformPage/sharedPlatformPage';
import {
PlatformResourceDetailToggleButton,
createPlatformResourceDetailState,
getPlatformResourceDetailRowClass,
} from '@/features/platformPage/PlatformResourceDetailTableRow';
@@ -317,6 +318,12 @@ export const KubernetesAlertsTable: Component<{
>
<TableCell class={getPlatformTableCellClassForKind('name')}>
<div class="flex min-w-0 items-center gap-2">
<PlatformResourceDetailToggleButton
expanded={isExpanded()}
resourceLabel={incident.resourceName}
controlsId={detailRowId()}
onToggle={() => drawer.toggle(incident)}
/>
<StatusDot
size="sm"
variant={severityVariant(incident.severityBucket)}
@@ -13,6 +13,7 @@ import {
PlatformTableShell,
} from '@/features/platformPage/sharedPlatformPage';
import {
PlatformResourceDetailToggleButton,
PlatformResourceDetailTableRow,
createPlatformResourceDetailState,
createPlatformResourceLabelResolver,
@@ -182,6 +183,12 @@ export const KubernetesAutoscalingTable: Component<{
>
<TableCell class={getPlatformTableCellClassForKind('name')}>
<div class="flex min-w-0 items-center gap-2">
<PlatformResourceDetailToggleButton
expanded={isExpanded()}
resourceLabel={name()}
controlsId={detailRowId()}
onToggle={() => drawer.toggle(resource)}
/>
<StatusDot
size="sm"
variant={indicator().variant}
@@ -16,6 +16,7 @@ import {
PlatformTableShell,
} from '@/features/platformPage/sharedPlatformPage';
import {
PlatformResourceDetailToggleButton,
PlatformResourceDetailTableRow,
createPlatformResourceDetailState,
createPlatformResourceLabelResolver,
@@ -199,6 +200,12 @@ export const KubernetesClustersTable: Component<{
>
<TableCell class={getPlatformTableCellClassForKind('name')}>
<div class="flex min-w-0 items-center gap-2">
<PlatformResourceDetailToggleButton
expanded={isExpanded()}
resourceLabel={name()}
controlsId={detailRowId()}
onToggle={() => drawer.toggle(cluster)}
/>
<StatusDot
size="sm"
variant={indicator().variant}
@@ -13,6 +13,7 @@ import {
PlatformTableShell,
} from '@/features/platformPage/sharedPlatformPage';
import {
PlatformResourceDetailToggleButton,
PlatformResourceDetailTableRow,
createPlatformResourceDetailState,
createPlatformResourceLabelResolver,
@@ -285,6 +286,12 @@ export const KubernetesConfigTable: Component<{
>
<TableCell class={getPlatformTableCellClassForKind('name')}>
<div class="flex min-w-0 items-center gap-2">
<PlatformResourceDetailToggleButton
expanded={isExpanded()}
resourceLabel={name()}
controlsId={detailRowId()}
onToggle={() => drawer.toggle(resource)}
/>
<StatusDot
size="sm"
variant={indicator().variant}
@@ -13,6 +13,7 @@ import {
PlatformTableShell,
} from '@/features/platformPage/sharedPlatformPage';
import {
PlatformResourceDetailToggleButton,
PlatformResourceDetailTableRow,
createPlatformResourceDetailState,
createPlatformResourceLabelResolver,
@@ -278,6 +279,12 @@ export const KubernetesControllersTable: Component<{
>
<TableCell class={getPlatformTableCellClassForKind('name')}>
<div class="flex min-w-0 items-center gap-2">
<PlatformResourceDetailToggleButton
expanded={isExpanded()}
resourceLabel={name()}
controlsId={detailRowId()}
onToggle={() => drawer.toggle(resource)}
/>
<StatusDot
size="sm"
variant={indicator().variant}
@@ -13,6 +13,7 @@ import {
PlatformTableShell,
} from '@/features/platformPage/sharedPlatformPage';
import {
PlatformResourceDetailToggleButton,
PlatformResourceDetailTableRow,
createPlatformResourceDetailState,
createPlatformResourceLabelResolver,
@@ -173,6 +174,12 @@ export const KubernetesDeploymentsTable: Component<{
>
<TableCell class={getPlatformTableCellClassForKind('name')}>
<div class="flex min-w-0 items-center gap-2">
<PlatformResourceDetailToggleButton
expanded={isExpanded()}
resourceLabel={name()}
controlsId={detailRowId()}
onToggle={() => drawer.toggle(deployment)}
/>
<StatusDot
size="sm"
variant={indicator().variant}
@@ -13,6 +13,7 @@ import {
PlatformTableShell,
} from '@/features/platformPage/sharedPlatformPage';
import {
PlatformResourceDetailToggleButton,
PlatformResourceDetailTableRow,
createPlatformResourceDetailState,
createPlatformResourceLabelResolver,
@@ -167,6 +168,12 @@ export const KubernetesEventsTable: Component<{
>
<TableCell class={getPlatformTableCellClassForKind('name')}>
<div class="flex min-w-0 items-center gap-2">
<PlatformResourceDetailToggleButton
expanded={isExpanded()}
resourceLabel={name()}
controlsId={detailRowId()}
onToggle={() => drawer.toggle(resource)}
/>
<StatusDot
size="sm"
variant={indicator().variant}
@@ -13,6 +13,7 @@ import {
PlatformTableShell,
} from '@/features/platformPage/sharedPlatformPage';
import {
PlatformResourceDetailToggleButton,
PlatformResourceDetailTableRow,
createPlatformResourceDetailState,
createPlatformResourceLabelResolver,
@@ -228,6 +229,12 @@ export const KubernetesNetworkingTable: Component<{
>
<TableCell class={getPlatformTableCellClassForKind('name')}>
<div class="flex min-w-0 items-center gap-2">
<PlatformResourceDetailToggleButton
expanded={isExpanded()}
resourceLabel={name()}
controlsId={detailRowId()}
onToggle={() => drawer.toggle(resource)}
/>
<StatusDot
size="sm"
variant={indicator().variant}
@@ -15,6 +15,7 @@ import {
getPlatformTableHeadClassForKind,
} from '@/features/platformPage/sharedPlatformPage';
import {
PlatformResourceDetailToggleButton,
PlatformResourceDetailTableRow,
createPlatformResourceDetailState,
createPlatformResourceLabelResolver,
@@ -237,6 +238,12 @@ export const KubernetesNodesTable: Component<{
>
<TableCell class={getPlatformTableCellClassForKind('name')}>
<div class="flex min-w-0 items-center gap-2">
<PlatformResourceDetailToggleButton
expanded={isExpanded()}
resourceLabel={name()}
controlsId={detailRowId()}
onToggle={() => drawer.toggle(node)}
/>
<StatusDot
size="sm"
variant={indicator().variant}
@@ -12,6 +12,7 @@ import {
PlatformTableShell,
} from '@/features/platformPage/sharedPlatformPage';
import {
PlatformResourceDetailToggleButton,
PlatformResourceDetailTableRow,
createPlatformResourceDetailState,
createPlatformResourceLabelResolver,
@@ -201,6 +202,12 @@ export const KubernetesPodsTable: Component<{
>
<TableCell class={getPlatformTableCellClassForKind('name')}>
<div class="flex min-w-0 items-center gap-2">
<PlatformResourceDetailToggleButton
expanded={isExpanded()}
resourceLabel={name()}
controlsId={detailRowId()}
onToggle={() => drawer.toggle(resource)}
/>
<StatusDot
size="sm"
variant={indicator().variant}
@@ -13,6 +13,7 @@ import {
PlatformTableShell,
} from '@/features/platformPage/sharedPlatformPage';
import {
PlatformResourceDetailToggleButton,
PlatformResourceDetailTableRow,
createPlatformResourceDetailState,
createPlatformResourceLabelResolver,
@@ -308,6 +309,12 @@ export const KubernetesPolicyTable: Component<{
>
<TableCell class={getPlatformTableCellClassForKind('name')}>
<div class="flex min-w-0 items-center gap-2">
<PlatformResourceDetailToggleButton
expanded={isExpanded()}
resourceLabel={name()}
controlsId={detailRowId()}
onToggle={() => drawer.toggle(resource)}
/>
<StatusDot
size="sm"
variant={indicator().variant}
@@ -13,6 +13,7 @@ import {
PlatformTableShell,
} from '@/features/platformPage/sharedPlatformPage';
import {
PlatformResourceDetailToggleButton,
PlatformResourceDetailTableRow,
createPlatformResourceDetailState,
createPlatformResourceLabelResolver,
@@ -189,6 +190,12 @@ export const KubernetesServicesTable: Component<{
>
<TableCell class={getPlatformTableCellClassForKind('name')}>
<div class="flex min-w-0 items-center gap-2">
<PlatformResourceDetailToggleButton
expanded={isExpanded()}
resourceLabel={name()}
controlsId={detailRowId()}
onToggle={() => drawer.toggle(resource)}
/>
<StatusDot
size="sm"
variant={indicator().variant}
@@ -14,6 +14,7 @@ import {
PlatformTableShell,
} from '@/features/platformPage/sharedPlatformPage';
import {
PlatformResourceDetailToggleButton,
PlatformResourceDetailTableRow,
createPlatformResourceDetailState,
createPlatformResourceLabelResolver,
@@ -256,6 +257,12 @@ export const KubernetesStorageTable: Component<{
>
<TableCell class={getPlatformTableCellClassForKind('name')}>
<div class="flex min-w-0 items-center gap-2">
<PlatformResourceDetailToggleButton
expanded={isExpanded()}
resourceLabel={name()}
controlsId={detailRowId()}
onToggle={() => drawer.toggle(resource)}
/>
<StatusDot
size="sm"
variant={indicator().variant}
@@ -1,6 +1,7 @@
import { Show, createSignal, type Component, type JSX } from 'solid-js';
import { ResourceDetailDrawer } from '@/components/Infrastructure/ResourceDetailDrawer';
import { hasTrueNASDetailSections } from '@/components/Infrastructure/resourceDetailDrawerTrueNASModel';
import { SummaryRowActionButton } from '@/components/shared/SummaryRowActionButton';
import { TableCell, TableRow } from '@/components/shared/Table';
import type { Resource } from '@/types/resource';
import { getPreferredInfrastructureDisplayName } from '@/utils/resourceIdentity';
@@ -25,6 +26,23 @@ export const PLATFORM_RESOURCE_DETAIL_ROW_CLASS =
export const getPlatformResourceDetailRowClass = (expanded: boolean): string =>
`${PLATFORM_RESOURCE_DETAIL_ROW_CLASS}${expanded ? ' bg-surface-hover' : ''}`;
export const PlatformResourceDetailToggleButton: Component<{
expanded: boolean;
resourceLabel: string;
controlsId?: string;
class?: string;
onToggle: () => void;
}> = (props) => (
<SummaryRowActionButton
kind="disclosure"
expanded={props.expanded}
subjectLabel={`details for ${props.resourceLabel}`}
controlsId={props.controlsId}
class={props.class}
onAction={props.onToggle}
/>
);
export function createPlatformResourceDetailState(options: {
idPrefix: string;
}): PlatformResourceDetailState {
@@ -21,6 +21,7 @@ import {
type PlatformTableFilterOption,
PlatformTableShell,
} from '@/features/platformPage/sharedPlatformPage';
import { PlatformResourceDetailToggleButton } from '@/features/platformPage/PlatformResourceDetailTableRow';
import type { Resource, ResourceCephServiceMeta } from '@/types/resource';
import { ProxmoxCephClusterDrawer } from './ProxmoxCephClusterDrawer';
@@ -271,6 +272,7 @@ export const ProxmoxCephTable: Component<{
const name = asTrimmedString(cluster.name) || cluster.id;
const fsid = asTrimmedString(cluster.ceph?.fsid) || '—';
const isOpen = () => selectedId() === cluster.id;
const detailRowId = () => `proxmox-ceph-detail-${cluster.id}`;
return (
<>
<TableRow
@@ -278,10 +280,17 @@ export const ProxmoxCephTable: Component<{
isOpen() ? 'bg-surface-hover' : ''
}`}
onClick={() => toggleSelected(cluster.id)}
aria-controls={isOpen() ? detailRowId() : undefined}
aria-expanded={isOpen()}
>
<TableCell class={getPlatformTableCellClassForKind('name')}>
<div class="flex items-center gap-2 min-w-0">
<PlatformResourceDetailToggleButton
expanded={isOpen()}
resourceLabel={name}
controlsId={detailRowId()}
onToggle={() => toggleSelected(cluster.id)}
/>
<span class="font-semibold text-base-content truncate" title={name}>
{name}
</span>
@@ -362,6 +371,7 @@ export const ProxmoxCephTable: Component<{
<Show when={isOpen()}>
<TableRow data-inline-detail-for={cluster.id}>
<TableCell
id={detailRowId()}
colspan={10}
class="p-0 border-b border-border bg-surface-alt"
>
@@ -1,5 +1,4 @@
import { For, Show, type Accessor, type JSX } from 'solid-js';
import ChevronRightIcon from 'lucide-solid/icons/chevron-right';
import { Card } from '@/components/shared/Card';
import { EmptyState } from '@/components/shared/EmptyState';
@@ -11,6 +10,7 @@ import {
getPlatformTableHeadClassForKind,
PlatformTableShell,
} from '@/features/platformPage/sharedPlatformPage';
import { PlatformResourceDetailToggleButton } from '@/features/platformPage/PlatformResourceDetailTableRow';
import type { StatusIndicatorVariant } from '@/utils/status';
import {
@@ -212,6 +212,7 @@ export function ProxmoxCoverageTable(props: {
[...row.artifacts]
.sort((left, right) => (right.createdMs ?? 0) - (left.createdMs ?? 0))
.slice(0, 8);
const detailRowId = () => `proxmox-coverage-evidence-${row.key}`;
return (
<>
<TableRow class="hover:bg-surface-hover">
@@ -219,20 +220,12 @@ export function ProxmoxCoverageTable(props: {
class={`${getPlatformTableCellClassForKind('name')} text-base-content`}
>
<div class="flex min-w-0 items-start gap-2">
<button
type="button"
class="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded-sm text-muted transition-colors hover:bg-surface-hover hover:text-base-content focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-1 focus-visible:ring-offset-surface"
onClick={() => props.onToggleExpand(row.key)}
aria-label={`${isExpanded() ? 'Hide' : 'Show'} restore evidence for ${row.workload.label}`}
aria-expanded={isExpanded()}
>
<ChevronRightIcon
class={`h-3.5 w-3.5 transition-transform ${
isExpanded() ? 'rotate-90' : ''
}`}
aria-hidden="true"
/>
</button>
<PlatformResourceDetailToggleButton
expanded={isExpanded()}
resourceLabel={row.workload.label}
controlsId={detailRowId()}
onToggle={() => props.onToggleExpand(row.key)}
/>
<div class="min-w-0">
<span class="block truncate font-semibold">
{row.workload.name || row.workload.label}
@@ -361,8 +354,8 @@ export function ProxmoxCoverageTable(props: {
</Show>
</TableRow>
<Show when={isExpanded()}>
<TableRow class="bg-surface-alt/40">
<TableCell class="px-3 py-2" colspan={columnCount()}>
<TableRow class="bg-surface-alt/40" data-inline-detail-for={row.key}>
<TableCell id={detailRowId()} class="px-3 py-2" colspan={columnCount()}>
<Show
when={evidence().length > 0}
fallback={
@@ -15,6 +15,7 @@ import {
type PlatformResourceStatusFilter,
PlatformTableShell,
} from '@/features/platformPage/sharedPlatformPage';
import { PlatformResourceDetailToggleButton } from '@/features/platformPage/PlatformResourceDetailTableRow';
import type { Resource } from '@/types/resource';
import { ProxmoxMailGatewayDrawer } from './ProxmoxMailGatewayDrawer';
@@ -126,6 +127,7 @@ export const ProxmoxMailGatewayTable: Component<{
const version = () => asTrimmedString(pmg()?.version) || '—';
const indicator = () => getSimpleStatusIndicator(instance.status);
const isOpen = () => selectedId() === instance.id;
const detailRowId = () => `proxmox-mail-gateway-detail-${instance.id}`;
return (
<>
<TableRow
@@ -133,10 +135,17 @@ export const ProxmoxMailGatewayTable: Component<{
isOpen() ? 'bg-surface-hover' : ''
}`}
onClick={() => toggleSelected(instance.id)}
aria-controls={isOpen() ? detailRowId() : undefined}
aria-expanded={isOpen()}
>
<TableCell class={getPlatformTableCellClassForKind('name')}>
<div class="flex items-center gap-2 min-w-0">
<PlatformResourceDetailToggleButton
expanded={isOpen()}
resourceLabel={name()}
controlsId={detailRowId()}
onToggle={() => toggleSelected(instance.id)}
/>
<StatusDot
size="sm"
variant={indicator().variant}
@@ -197,6 +206,7 @@ export const ProxmoxMailGatewayTable: Component<{
<Show when={isOpen()}>
<TableRow data-inline-detail-for={instance.id}>
<TableCell
id={detailRowId()}
colspan={10}
class="p-0 border-b border-border bg-surface-alt"
>
@@ -34,6 +34,7 @@ import {
getPlatformTableCellClassForKind,
getPlatformTableHeadClassForKind,
} from '@/features/platformPage/sharedPlatformPage';
import { PlatformResourceDetailToggleButton } from '@/features/platformPage/PlatformResourceDetailTableRow';
import { type WorkloadsMetricDisplayMode } from '@/components/Workloads/workloadsFilterModel';
import { type WorkloadTableMetricHistoryRange } from '@/components/Workloads/workloadMetricHistoryModel';
import type { Disk, Node as LegacyNode } from '@/types/api';
@@ -368,6 +369,12 @@ export const ProxmoxNodesTable: Component<{
return (
<TableCell class={getPlatformTableCellClassForKind(column.kind)}>
<div class="flex min-w-0 items-center gap-2">
<PlatformResourceDetailToggleButton
expanded={isSelected()}
resourceLabel={name()}
controlsId={detailRowId()}
onToggle={toggleNodeDrawer}
/>
<StatusDot
size="sm"
variant={indicator().variant}
@@ -244,7 +244,7 @@ describe('ProxmoxBackupsTable', () => {
// Per-source detail is one click down inside the workload's row.
await fireEvent.click(
screen.getByRole('button', { name: /show restore evidence for pbs-docker/i }),
screen.getByRole('button', { name: /expand details for pbs-docker/i }),
);
expect(screen.getByText('Restore evidence')).toBeInTheDocument();
expect(screen.getAllByText('PVE file').length).toBeGreaterThan(0);
@@ -1,4 +1,3 @@
import ChevronRightIcon from 'lucide-solid/icons/chevron-right';
import Loader2Icon from 'lucide-solid/icons/loader-2';
import MoreHorizontalIcon from 'lucide-solid/icons/more-horizontal';
import Trash2Icon from 'lucide-solid/icons/trash-2';
@@ -31,6 +30,7 @@ import {
hostAgentVersion,
} from '@/features/platformPage/agentVersion';
import {
PlatformResourceDetailToggleButton,
PlatformResourceDetailTableRow,
createPlatformResourceDetailState,
getPlatformResourceDetailRowClass,
@@ -1514,11 +1514,11 @@ export const AgentsMachinesTable: Component<{
class={`${getPlatformTableCellClassForKind('name')} ${machineColumnWidthClass('machine')}`}
>
<div class="flex min-w-0 items-center gap-2">
<ChevronRightIcon
data-agent-machine-expand-icon
class="h-3.5 w-3.5 shrink-0 text-muted transition-transform duration-150"
classList={{ 'rotate-90': isExpanded() }}
aria-hidden="true"
<PlatformResourceDetailToggleButton
expanded={isExpanded()}
resourceLabel={name()}
controlsId={detailRowId()}
onToggle={toggleDetails}
/>
<StatusDot
size="sm"
@@ -443,18 +443,20 @@ describe('AgentsMachinesTable', () => {
/>
));
const icon = container.querySelector('[data-agent-machine-expand-icon]');
expect(icon).not.toBeNull();
expect(icon).not.toHaveClass('rotate-90');
expect(
screen.getByRole('button', { name: 'Expand details for Expandable Machine' }),
).toHaveAttribute('aria-expanded', 'false');
expect(screen.queryByTestId('resource-detail-drawer')).not.toBeInTheDocument();
const row = container.querySelector('[data-agents-machine-row="expandable-machine"]');
expect(row).not.toBeNull();
if (!row || !icon) return;
if (!row) return;
await fireEvent.click(row);
expect(icon).toHaveClass('rotate-90');
expect(
screen.getByRole('button', { name: 'Collapse details for Expandable Machine' }),
).toHaveAttribute('aria-expanded', 'true');
expect(screen.getByTestId('resource-detail-drawer')).toBeInTheDocument();
});
@@ -11,6 +11,7 @@ import {
PlatformTableShell,
} from '@/features/platformPage/sharedPlatformPage';
import {
PlatformResourceDetailToggleButton,
createPlatformResourceDetailState,
getPlatformResourceDetailRowClass,
} from '@/features/platformPage/PlatformResourceDetailTableRow';
@@ -293,6 +294,12 @@ export const TrueNASAlertsTable: Component<{
>
<TableCell class={getPlatformTableCellClassForKind('name')}>
<div class="flex min-w-0 items-center gap-2">
<PlatformResourceDetailToggleButton
expanded={isExpanded()}
resourceLabel={incident.resourceName}
controlsId={detailRowId()}
onToggle={() => drawer.toggle(incident)}
/>
<StatusDot
size="sm"
variant={severityVariant(incident.severityBucket)}
@@ -17,6 +17,7 @@ import {
PlatformTableShell,
} from '@/features/platformPage/sharedPlatformPage';
import {
PlatformResourceDetailToggleButton,
PlatformResourceDetailTableRow,
createPlatformResourceDetailState,
createPlatformResourceLabelResolver,
@@ -263,6 +264,12 @@ export const TrueNASAppsTable: Component<{
>
<TableCell class={getPlatformTableCellClassForKind('name')}>
<div class="flex min-w-0 items-center gap-2">
<PlatformResourceDetailToggleButton
expanded={isExpanded()}
resourceLabel={name()}
controlsId={detailRowId()}
onToggle={() => drawer.toggle(resource)}
/>
<StatusDot
size="sm"
variant={indicator().variant}
@@ -13,6 +13,7 @@ import {
PlatformTableShell,
} from '@/features/platformPage/sharedPlatformPage';
import {
PlatformResourceDetailToggleButton,
PlatformResourceDetailTableRow,
createPlatformResourceDetailState,
createPlatformResourceLabelResolver,
@@ -213,6 +214,12 @@ export const TrueNASNetworkSharesTable: Component<{
>
<TableCell class={getPlatformTableCellClassForKind('name')}>
<div class="flex min-w-0 items-center gap-2">
<PlatformResourceDetailToggleButton
expanded={isExpanded()}
resourceLabel={name()}
controlsId={detailRowId()}
onToggle={() => drawer.toggle(resource)}
/>
<StatusDot
size="sm"
variant={indicator().variant}
@@ -21,6 +21,7 @@ import {
PlatformTableShell,
} from '@/features/platformPage/sharedPlatformPage';
import {
PlatformResourceDetailToggleButton,
createPlatformResourceDetailState,
getPlatformResourceDetailRowClass,
} from '@/features/platformPage/PlatformResourceDetailTableRow';
@@ -262,7 +263,7 @@ const buildProtectionDetailSections = (point: RecoveryPoint): ProtectionDetailSe
]);
};
const DatasetCell: Component<{ point: RecoveryPoint }> = (props) => {
const DatasetCell: Component<{ point: RecoveryPoint; detailToggle?: JSX.Element }> = (props) => {
const status = () => mapTrueNASProtectionStatus(props.point);
const kind = () => mapTrueNASProtectionKind(props.point);
const name = () => datasetLabel(props.point);
@@ -270,6 +271,7 @@ const DatasetCell: Component<{ point: RecoveryPoint }> = (props) => {
return (
<div class="flex min-w-0 items-center gap-2">
{props.detailToggle}
<StatusDot
size="sm"
variant={protectionVariant(status())}
@@ -436,7 +438,17 @@ export const TrueNASProtectionTable: Component<{
tabIndex={0}
>
<TableCell class={getPlatformTableCellClassForKind('name')}>
<DatasetCell point={point} />
<DatasetCell
point={point}
detailToggle={
<PlatformResourceDetailToggleButton
expanded={isExpanded()}
resourceLabel={datasetLabel(point)}
controlsId={detailRowId()}
onToggle={() => detail.toggle(point)}
/>
}
/>
</TableCell>
<TableCell class={getPlatformTableCellClassForKind('text')}>
<span class="block truncate text-base-content" title={artifact()}>
@@ -12,6 +12,7 @@ import {
PlatformTableShell,
} from '@/features/platformPage/sharedPlatformPage';
import {
PlatformResourceDetailToggleButton,
createPlatformResourceDetailState,
getPlatformResourceDetailRowClass,
} from '@/features/platformPage/PlatformResourceDetailTableRow';
@@ -252,6 +253,12 @@ export const TrueNASServicesTable: Component<{
>
<TableCell class={getPlatformTableCellClassForKind('name')}>
<div class="flex min-w-0 items-center gap-2">
<PlatformResourceDetailToggleButton
expanded={isExpanded()}
resourceLabel={formatServiceName(row.service.service)}
controlsId={detailRowId()}
onToggle={() => detail.toggle(row)}
/>
<StatusDot
size="sm"
variant={serviceStatusVariant(status())}
@@ -17,6 +17,7 @@ import {
PlatformTableShell,
} from '@/features/platformPage/sharedPlatformPage';
import {
PlatformResourceDetailToggleButton,
PlatformResourceDetailTableRow,
createPlatformResourceDetailState,
createPlatformResourceLabelResolver,
@@ -182,7 +183,9 @@ export const getTrueNASStorageTopologyIndentClass = (depth: number): string => {
return 'pl-12 sm:pl-16';
};
const ResourceCell: Component<{ row: TrueNASStorageTopologyRow }> = (props) => {
const ResourceCell: Component<{ row: TrueNASStorageTopologyRow; detailToggle?: JSX.Element }> = (
props,
) => {
const displayStatus = () => getTrueNASResourceDisplayStatus(props.row.resource);
const indicator = () => getSimpleStatusIndicator(displayStatus());
const name = () => resourceName(props.row.resource);
@@ -193,6 +196,7 @@ const ResourceCell: Component<{ row: TrueNASStorageTopologyRow }> = (props) => {
)}`}
data-truenas-storage-indent-depth={props.row.depth}
>
{props.detailToggle}
<StatusDot size="sm" variant={indicator().variant} title={indicator().label} />
<div class="min-w-0">
<div class="truncate font-medium text-base-content" title={name()}>
@@ -310,7 +314,17 @@ export const TrueNASStorageTopologyTable: Component<{
tabIndex={0}
>
<TableCell class={getPlatformTableCellClassForKind('name')}>
<ResourceCell row={row} />
<ResourceCell
row={row}
detailToggle={
<PlatformResourceDetailToggleButton
expanded={isExpanded()}
resourceLabel={resourceName(resource())}
controlsId={detailRowId()}
onToggle={() => drawer.toggle(resource())}
/>
}
/>
</TableCell>
<TableCell class={getPlatformTableCellClassForKind('text')}>
<span class="text-base-content">{kindLabel(row.kind)}</span>
@@ -18,6 +18,7 @@ import {
PlatformTableShell,
} from '@/features/platformPage/sharedPlatformPage';
import {
PlatformResourceDetailToggleButton,
PlatformResourceDetailTableRow,
createPlatformResourceDetailState,
createPlatformResourceLabelResolver,
@@ -271,6 +272,12 @@ export const TrueNASSystemsTable: Component<{
>
<TableCell class={getPlatformTableCellClassForKind('name')}>
<div class="flex min-w-0 items-center gap-2">
<PlatformResourceDetailToggleButton
expanded={isExpanded()}
resourceLabel={name()}
controlsId={detailRowId()}
onToggle={() => drawer.toggle(system)}
/>
<StatusDot
size="sm"
variant={indicator().variant}
@@ -13,6 +13,7 @@ import {
PlatformTableShell,
} from '@/features/platformPage/sharedPlatformPage';
import {
PlatformResourceDetailToggleButton,
PlatformResourceDetailTableRow,
createPlatformResourceDetailState,
createPlatformResourceLabelResolver,
@@ -220,6 +221,12 @@ export const TrueNASVirtualMachinesTable: Component<{
>
<TableCell class={getPlatformTableCellClassForKind('name')}>
<div class="flex min-w-0 items-center gap-2">
<PlatformResourceDetailToggleButton
expanded={isExpanded()}
resourceLabel={name()}
controlsId={detailRowId()}
onToggle={() => drawer.toggle(resource)}
/>
<StatusDot
size="sm"
variant={indicator().variant}
@@ -13,6 +13,7 @@ import {
PlatformTableShell,
} from '@/features/platformPage/sharedPlatformPage';
import {
PlatformResourceDetailToggleButton,
createPlatformResourceDetailState,
getPlatformResourceDetailRowClass,
} from '@/features/platformPage/PlatformResourceDetailTableRow';
@@ -287,6 +288,12 @@ export const VsphereActivityTable: Component<{
>
<TableCell class={getPlatformTableCellClassForKind('name')}>
<div class="flex min-w-0 items-center gap-2">
<PlatformResourceDetailToggleButton
expanded={isExpanded()}
resourceLabel={activity.resourceName}
controlsId={detailRowId()}
onToggle={() => drawer.toggle(activity)}
/>
<StatusDot
size="sm"
variant={activityStateVariant(activity.stateBucket)}
@@ -13,6 +13,7 @@ import {
PlatformTableShell,
} from '@/features/platformPage/sharedPlatformPage';
import {
PlatformResourceDetailToggleButton,
createPlatformResourceDetailState,
getPlatformResourceDetailRowClass,
} from '@/features/platformPage/PlatformResourceDetailTableRow';
@@ -283,6 +284,12 @@ export const VsphereAlertsTable: Component<{
>
<TableCell class={getPlatformTableCellClassForKind('name')}>
<div class="flex min-w-0 items-center gap-2">
<PlatformResourceDetailToggleButton
expanded={isExpanded()}
resourceLabel={incident.resourceName}
controlsId={detailRowId()}
onToggle={() => drawer.toggle(incident)}
/>
<StatusDot
size="sm"
variant={severityVariant(incident.severityBucket)}
@@ -15,6 +15,7 @@ import {
PlatformTableShell,
} from '@/features/platformPage/sharedPlatformPage';
import {
PlatformResourceDetailToggleButton,
PlatformResourceDetailTableRow,
createPlatformResourceDetailState,
createPlatformResourceLabelResolver,
@@ -236,6 +237,12 @@ export const VsphereDatastoresTable: Component<{
>
<TableCell class={getPlatformTableCellClassForKind('name')}>
<div class="flex min-w-0 items-center gap-2">
<PlatformResourceDetailToggleButton
expanded={isExpanded()}
resourceLabel={name()}
controlsId={detailRowId()}
onToggle={() => drawer.toggle(datastore)}
/>
<StatusDot
size="sm"
variant={indicator().variant}
@@ -25,6 +25,7 @@ import {
type PlatformResourceStatusFilter,
} from '@/features/platformPage/sharedPlatformPage';
import {
PlatformResourceDetailToggleButton,
PlatformResourceDetailTableRow,
createPlatformResourceDetailState,
createPlatformResourceLabelResolver,
@@ -236,6 +237,12 @@ export const VsphereHostsTable: Component<{
>
<TableCell class={getPlatformTableCellClassForKind('name')}>
<div class="flex min-w-0 items-center gap-2">
<PlatformResourceDetailToggleButton
expanded={isExpanded()}
resourceLabel={name()}
controlsId={detailRowId()}
onToggle={() => drawer.toggle(host)}
/>
<StatusDot
size="sm"
variant={indicator().variant}
@@ -14,6 +14,7 @@ import {
PlatformTableShell,
} from '@/features/platformPage/sharedPlatformPage';
import {
PlatformResourceDetailToggleButton,
PlatformResourceDetailTableRow,
createPlatformResourceDetailState,
createPlatformResourceLabelResolver,
@@ -204,6 +205,12 @@ export const VsphereNetworksTable: Component<{
>
<TableCell class={getPlatformTableCellClassForKind('name')}>
<div class="flex min-w-0 items-center gap-2">
<PlatformResourceDetailToggleButton
expanded={isExpanded()}
resourceLabel={name()}
controlsId={detailRowId()}
onToggle={() => drawer.toggle(network)}
/>
<StatusDot
size="sm"
variant={indicator().variant}