mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-21 03:05:44 +00:00
Canonicalize platform row detail toggles
This commit is contained in:
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user