fix(ui): refine responsive platform tables

Contract-Neutral: responsive presentation and touch-target adjustments only; no API or payload changes
This commit is contained in:
rcourtman
2026-08-06 15:55:11 +01:00
parent 2d9ba27065
commit 737bde7362
27 changed files with 294 additions and 115 deletions
+94 -25
View File
@@ -1,46 +1,115 @@
{
"version": 1,
"base_sha": "c25d393fff818067d9e75f2fed01ad5f47f41ec2",
"verified_at": "2026-08-06T14:51:21Z",
"base_sha": "2d9ba270651ed5e1028ae4aa15843ab34303c7c9",
"verified_at": "2026-08-06T14:52:48Z",
"result": "passed",
"changed_paths": [
"frontend-modern/src/AppLayout.tsx",
"frontend-modern/src/components/Brand/PulseBrandMark.tsx",
"frontend-modern/src/components/Login.tsx",
"frontend-modern/src/components/SetupWizard/steps/WelcomeStep.tsx"
"frontend-modern/src/components/Infrastructure/ResourceDetailDrawerSupportDisclosure.tsx",
"frontend-modern/src/components/Infrastructure/ResourceOperatorStateSection.tsx",
"frontend-modern/src/components/Workloads/GuestDrawerHistory.tsx",
"frontend-modern/src/components/shared/buttonModel.ts",
"frontend-modern/src/features/docker/DockerImagesTable.tsx",
"frontend-modern/src/features/docker/DockerServicesTable.tsx",
"frontend-modern/src/features/docker/DockerSwarmNodesTable.tsx",
"frontend-modern/src/features/docker/DockerTasksTable.tsx",
"frontend-modern/src/features/kubernetes/KubernetesClustersTable.tsx",
"frontend-modern/src/features/kubernetes/KubernetesConfigTable.tsx",
"frontend-modern/src/features/kubernetes/KubernetesDeploymentsTable.tsx",
"frontend-modern/src/features/kubernetes/KubernetesNetworkingTable.tsx",
"frontend-modern/src/features/kubernetes/KubernetesNodesTable.tsx",
"frontend-modern/src/features/kubernetes/KubernetesPodsTable.tsx",
"frontend-modern/src/features/kubernetes/KubernetesPolicyTable.tsx",
"frontend-modern/src/features/kubernetes/KubernetesServicesTable.tsx",
"frontend-modern/src/features/kubernetes/KubernetesStorageTable.tsx",
"frontend-modern/src/features/standalone/AgentsMachinesTable.tsx",
"frontend-modern/src/features/truenas/TrueNASStorageTopologyTable.tsx",
"frontend-modern/src/features/vmware/VsphereNetworksTable.tsx"
],
"content_sha256": {
"frontend-modern/src/AppLayout.tsx": "5f0ee2909e3c08840af0c5b8eac78cd6c833aeb60f12250d770aea9fc319ce68",
"frontend-modern/src/components/Brand/PulseBrandMark.tsx": "af8766f84e628b00f22cfc2c5b786a0e0914b8555f2796748e8634265d8b7f0b",
"frontend-modern/src/components/Login.tsx": "c81e3c29a7e59f1217945bb373dced6aa28b047c6d6a7929e46681994eb9305f",
"frontend-modern/src/components/SetupWizard/steps/WelcomeStep.tsx": "f3f77aebdc6c26291a731506da5d1a40184dc485079fc60f7b5c57795557bb03"
"frontend-modern/src/components/Infrastructure/ResourceDetailDrawerSupportDisclosure.tsx": "c51b9e1530cd64dfe119adf16ec37e0ac1014be8ce52593c34735776ccaf7fbc",
"frontend-modern/src/components/Infrastructure/ResourceOperatorStateSection.tsx": "35e44d398281f05e48a770801ece59ef120150bc613e2f17d3bed0f30e349b34",
"frontend-modern/src/components/Workloads/GuestDrawerHistory.tsx": "3bfcbffa02f3e1b2512e925e3f33a9ff490b269fffb6b946e0211f31d31854e2",
"frontend-modern/src/components/shared/buttonModel.ts": "bd672d8673cab1b467afc72b932a1b1ef62bd25eac28d7c2e40e74477da71e04",
"frontend-modern/src/features/docker/DockerImagesTable.tsx": "dc37ef6b662b2fbbecce312894fa5e8c19d2224cf1895683e63fdaca930d6301",
"frontend-modern/src/features/docker/DockerServicesTable.tsx": "5e793d307797c96b2496b7c5cef4807890f480fc5fb22cc1c4bc9a506f85383e",
"frontend-modern/src/features/docker/DockerSwarmNodesTable.tsx": "bb0b23679387945dd5bab4a86231754549419d0e2350fa1a1aca7f26ee317403",
"frontend-modern/src/features/docker/DockerTasksTable.tsx": "201200a74d8da6bcf246e5ec5de1e8df018c05833f5fae66f4f6435768a27cb4",
"frontend-modern/src/features/kubernetes/KubernetesClustersTable.tsx": "c50271a335b9008196ce832f5ab18f90b93996ee0516570f731ec036377e84f0",
"frontend-modern/src/features/kubernetes/KubernetesConfigTable.tsx": "ce3bd1d75937b4d8a914e09385f71b32ef3e0ac7b898680f2d31c1aab6dd043e",
"frontend-modern/src/features/kubernetes/KubernetesDeploymentsTable.tsx": "237338df289a7fddbf3b6c8b889e55e8357fc2c43b8e9195ffd7ddf1c8e78f13",
"frontend-modern/src/features/kubernetes/KubernetesNetworkingTable.tsx": "348fd5f8c7b76de1dd971328b7622d46314f2c8c995110883ffb278aab7d26bb",
"frontend-modern/src/features/kubernetes/KubernetesNodesTable.tsx": "9dab059b7f6375ece604b760cbf05db93b7bfedd20a1e1e0f345b80e4188c5f3",
"frontend-modern/src/features/kubernetes/KubernetesPodsTable.tsx": "fba4eb11395cac6aa8555807bd92f07b15b1013a1d19606b1b70eddffa143813",
"frontend-modern/src/features/kubernetes/KubernetesPolicyTable.tsx": "b486880d9147a3af212ba9c1b570959242900d4fd8c7b4c2c7a72d30046e5edb",
"frontend-modern/src/features/kubernetes/KubernetesServicesTable.tsx": "2ba5c960a276a25b34992c2c8f59b783220e84852f60b0c6069b277cd9e89efb",
"frontend-modern/src/features/kubernetes/KubernetesStorageTable.tsx": "f09c277ca2ee298d585207acf4e1d50fae4a7472b818233df69842a772f53a58",
"frontend-modern/src/features/standalone/AgentsMachinesTable.tsx": "3ede12e55a4975563f9f4c7c4db7e9ffced3d4aae9ff79c9806eb443e78da58c",
"frontend-modern/src/features/truenas/TrueNASStorageTopologyTable.tsx": "d85fc6739a235a295b483044b9250b2e59a542a875ae3c8b6b46bb507091385c",
"frontend-modern/src/features/vmware/VsphereNetworksTable.tsx": "219fe6f2a4726b9bff7f5b8fdd51ef05b622a931743a3fecf9541de9894f2020"
},
"routes": [
"/",
"/proxmox/overview"
"/docker/overview",
"/docker/images",
"/docker/storage",
"/docker/networks",
"/docker/swarm",
"/kubernetes/overview",
"/kubernetes/nodes",
"/kubernetes/workloads",
"/kubernetes/services",
"/kubernetes/storage",
"/kubernetes/configuration",
"/kubernetes/events",
"/truenas/overview",
"/truenas/storage",
"/truenas/services",
"/truenas/apps",
"/truenas/vms",
"/truenas/shares",
"/truenas/protection",
"/vmware/overview",
"/vmware/storage",
"/vmware/networks",
"/vmware/health",
"/vmware/activity",
"/standalone/machines",
"/standalone/availability"
],
"viewports": [
{
"width": 1280,
"height": 720
"height": 900
},
{
"width": 375,
"height": 812
"width": 768,
"height": 900
},
{
"width": 390,
"height": 900
},
{
"width": 320,
"height": 900
}
],
"states": [
"Login screen brand mark in app-dark theme, computed disc #3b82f6 with ring stroke and centre fill both #dbeafe",
"Login screen brand mark in app-light theme while the OS colour scheme was dark, computed disc #2563eb with ring stroke and centre fill both #ffffff, confirming the mark follows the app theme class rather than the OS preference",
"Authenticated header brand lockup in app-light and app-dark themes at 1280x720, ring stroke and centre fill matching in both",
"Authenticated header brand lockup at 375x812 mobile, mark visible at 20x20 with dark palette intact",
"SetupWizard WelcomeStep was NOT exercised live: it renders only when the instance has no authentication configured, and the dev backend is configured. Covered by WelcomeStep.test.tsx and by the shared PulseBrandMark component verified on the other two surfaces"
"Mock-data inventory tables across Docker, Kubernetes, TrueNAS, vSphere, and standalone Machines and Availability routes",
"Phone layouts with operator-priority identity, health, capacity, state, and update columns while secondary context is hidden",
"Tablet layouts with namespace, scope, role, host, mode, address, class, kind, disk, and node context restored where space permits",
"Desktop layouts with the complete field set and stable platform-page width",
"TrueNAS nested storage topology at multiple depths with long resource names and usage and health values",
"Expanded machine and Docker image resource drawers with overview, history, discovery, support disclosure, and operator override sections",
"Operator maintenance scheduler open and cancelled without saving, plus machine action menu open and closed"
],
"interactions": [
"Signed in with local credentials from the login form to move from the unauthenticated to the authenticated header surface",
"Toggled pulseThemePreference between light and dark in localStorage and reloaded, reading getComputedStyle fill and stroke off .pulse-bg, .pulse-ring and .pulse-center each time",
"Held the OS colour scheme at dark while forcing the app theme to light to prove the inline mark tracks the app theme, the divergence an <img src=\"/logo.svg\"> cannot represent",
"Read getAnimations() on the extracted mark and confirmed pulse-brand-logo, pulse-brand-bg, pulse-brand-ring and pulse-brand-center are all running, proving the .animate-pulse-brand descendant selectors still reach the circles after the SVG moved into a component",
"Resized from 1280x720 to 375x812 and re-measured the header mark bounding box and computed ring stroke"
"Navigated every listed platform route and inspected table headers, visible information priority, truncation, and document overflow at phone, tablet, and desktop widths",
"Added responsive context progressively between 320px, 390px, 768px, and 1280px instead of scaling the application or forcing horizontal table scrolling",
"Expanded and collapsed resource rows, switched drawer tabs, opened support and detail disclosures, and measured drawer header, select, schedule, preset, form, and history-range controls",
"Verified changed phone controls meet a 44px touch target while returning to the compact treatment above the phone breakpoint",
"Opened the machine row action menu at 320px and confirmed its bounds stay inside the viewport",
"Pressed Escape on the open machine action menu and confirmed it closes and restores focus to the originating action button",
"Closed an expanded machine detail drawer at 320px and confirmed focus returns to the exact row expand button",
"Confirmed no horizontal document overflow on the audited pages, including expanded drawer states, then reset the viewport and finalized the browser session"
]
}
@@ -5373,7 +5373,7 @@
"scopes": ["src/components", "src/features", "src/pages"],
"extensions": [".tsx"],
"allPatterns": [
"inline-flex h-8 items-center gap-1.5 rounded border border-border bg-surface px-2 text-xs font-medium text-base-content transition-colors hover:bg-surface-hover focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500"
"inline-flex h-8 min-h-11 min-w-11 items-center gap-1.5 rounded border border-border bg-surface px-2 text-xs font-medium text-base-content transition-colors hover:bg-surface-hover focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500"
],
"legacyReason": "Retired migration debt. Drawer header Assistant and context-copy actions belong to the shared Button primitive family.",
"allowedPaths": [],
@@ -5395,7 +5395,7 @@
"scopes": ["src/components", "src/features", "src/pages"],
"extensions": [".tsx"],
"allPatterns": [
"inline-flex h-8 w-8 items-center justify-center rounded-md hover:bg-surface-hover hover:text-base-content focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500"
"inline-flex h-8 w-8 min-h-11 min-w-11 items-center justify-center rounded-md hover:bg-surface-hover hover:text-base-content focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500"
],
"legacyReason": "Retired migration debt. Drawer header close and icon actions belong to the shared Button primitive family.",
"allowedPaths": [],
@@ -46,7 +46,7 @@ export const ResourceDetailDrawerSupportDisclosure: Component<
onClick={props.onToggle}
class={
props.buttonClass ??
'inline-flex shrink-0 items-center rounded-md border border-border bg-surface px-2.5 py-1 text-[10px] font-medium text-base-content transition-colors hover:bg-base'
'inline-flex min-h-11 shrink-0 items-center rounded-md border border-border bg-surface px-2.5 py-1 text-[10px] font-medium text-base-content transition-colors hover:bg-base sm:min-h-0'
}
>
{props.expanded ? props.hideLabel : props.showLabel}
@@ -508,7 +508,7 @@ export const ResourceOperatorStateSection: Component<ResourceOperatorStateSectio
value={criticality()}
onChange={(e) => setCriticality(e.currentTarget.value as ResourceCriticality)}
disabled={saving()}
class="mt-1 block w-full text-xs rounded border border-border bg-surface px-2 py-1.5 text-base-content focus:outline-none focus:ring-1 focus:ring-blue-400 disabled:opacity-50"
class="mt-1 block min-h-11 w-full text-xs rounded border border-border bg-surface px-2 py-1.5 text-base-content focus:outline-none focus:ring-1 focus:ring-blue-400 disabled:opacity-50 sm:min-h-0"
>
<option value="">Default</option>
<option value="high">High</option>
@@ -552,7 +552,7 @@ export const ResourceOperatorStateSection: Component<ResourceOperatorStateSectio
type="button"
onClick={handleOpenScheduler}
disabled={saving()}
class="px-2.5 py-1 text-xs font-medium text-base-content border border-border rounded hover:bg-surface-hover disabled:opacity-50"
class="min-h-11 px-2.5 py-1 text-xs font-medium text-base-content border border-border rounded hover:bg-surface-hover disabled:opacity-50 sm:min-h-0"
>
Schedule window
</button>
@@ -563,7 +563,7 @@ export const ResourceOperatorStateSection: Component<ResourceOperatorStateSectio
type="button"
onClick={handleOpenScheduler}
disabled={saving()}
class="px-2.5 py-1 text-xs font-medium text-base-content border border-border rounded hover:bg-surface-hover disabled:opacity-50"
class="min-h-11 px-2.5 py-1 text-xs font-medium text-base-content border border-border rounded hover:bg-surface-hover disabled:opacity-50 sm:min-h-0"
>
Edit window
</button>
@@ -571,7 +571,7 @@ export const ResourceOperatorStateSection: Component<ResourceOperatorStateSectio
type="button"
onClick={handleClearMaintenanceWindow}
disabled={saving()}
class="px-2.5 py-1 text-xs font-medium text-amber-700 border border-amber-200 rounded hover:bg-amber-50 dark:text-amber-300 dark:border-amber-800 dark:hover:bg-amber-900 disabled:opacity-50"
class="min-h-11 px-2.5 py-1 text-xs font-medium text-amber-700 border border-amber-200 rounded hover:bg-amber-50 dark:text-amber-300 dark:border-amber-800 dark:hover:bg-amber-900 disabled:opacity-50 sm:min-h-0"
>
Cancel window
</button>
@@ -590,7 +590,7 @@ export const ResourceOperatorStateSection: Component<ResourceOperatorStateSectio
type="datetime-local"
value={scheduleStart()}
onInput={(e) => setScheduleStart(e.currentTarget.value)}
class="mt-0.5 w-full text-xs rounded border border-border bg-surface px-2 py-1 text-base-content focus:outline-none focus:ring-1 focus:ring-blue-400"
class="mt-0.5 min-h-11 w-full text-xs rounded border border-border bg-surface px-2 py-1 text-base-content focus:outline-none focus:ring-1 focus:ring-blue-400 sm:min-h-0"
disabled={saving()}
/>
</label>
@@ -600,7 +600,7 @@ export const ResourceOperatorStateSection: Component<ResourceOperatorStateSectio
type="datetime-local"
value={scheduleEnd()}
onInput={(e) => setScheduleEnd(e.currentTarget.value)}
class="mt-0.5 w-full text-xs rounded border border-border bg-surface px-2 py-1 text-base-content focus:outline-none focus:ring-1 focus:ring-blue-400"
class="mt-0.5 min-h-11 w-full text-xs rounded border border-border bg-surface px-2 py-1 text-base-content focus:outline-none focus:ring-1 focus:ring-blue-400 sm:min-h-0"
disabled={saving()}
/>
</label>
@@ -612,7 +612,7 @@ export const ResourceOperatorStateSection: Component<ResourceOperatorStateSectio
type="button"
onClick={() => applyPresetDuration(1)}
disabled={saving()}
class="px-1.5 py-0.5 rounded border border-border hover:bg-surface-hover disabled:opacity-50"
class="min-h-11 min-w-11 px-1.5 py-0.5 rounded border border-border hover:bg-surface-hover disabled:opacity-50 sm:min-h-0 sm:min-w-0"
>
1h
</button>
@@ -620,7 +620,7 @@ export const ResourceOperatorStateSection: Component<ResourceOperatorStateSectio
type="button"
onClick={() => applyPresetDuration(4)}
disabled={saving()}
class="px-1.5 py-0.5 rounded border border-border hover:bg-surface-hover disabled:opacity-50"
class="min-h-11 min-w-11 px-1.5 py-0.5 rounded border border-border hover:bg-surface-hover disabled:opacity-50 sm:min-h-0 sm:min-w-0"
>
4h
</button>
@@ -628,7 +628,7 @@ export const ResourceOperatorStateSection: Component<ResourceOperatorStateSectio
type="button"
onClick={() => applyPresetDuration(24)}
disabled={saving()}
class="px-1.5 py-0.5 rounded border border-border hover:bg-surface-hover disabled:opacity-50"
class="min-h-11 min-w-11 px-1.5 py-0.5 rounded border border-border hover:bg-surface-hover disabled:opacity-50 sm:min-h-0 sm:min-w-0"
>
24h
</button>
@@ -641,7 +641,7 @@ export const ResourceOperatorStateSection: Component<ResourceOperatorStateSectio
value={scheduleReason()}
onInput={(e) => setScheduleReason(e.currentTarget.value)}
placeholder="e.g. Q3 storage upgrade, kernel patch reboot"
class="mt-0.5 w-full text-xs rounded border border-border bg-surface px-2 py-1 text-base-content focus:outline-none focus:ring-1 focus:ring-blue-400"
class="mt-0.5 min-h-11 w-full text-xs rounded border border-border bg-surface px-2 py-1 text-base-content focus:outline-none focus:ring-1 focus:ring-blue-400 sm:min-h-0"
disabled={saving()}
maxLength={200}
/>
@@ -656,7 +656,7 @@ export const ResourceOperatorStateSection: Component<ResourceOperatorStateSectio
type="button"
onClick={() => setSchedulerOpen(false)}
disabled={saving()}
class="px-2.5 py-1 text-xs font-medium text-muted hover:bg-surface-hover rounded transition-colors disabled:opacity-50"
class="min-h-11 px-2.5 py-1 text-xs font-medium text-muted hover:bg-surface-hover rounded transition-colors disabled:opacity-50 sm:min-h-0"
>
Cancel
</button>
@@ -664,7 +664,7 @@ export const ResourceOperatorStateSection: Component<ResourceOperatorStateSectio
type="button"
onClick={handleScheduleSave}
disabled={saving() || Boolean(scheduleValidationError())}
class="px-2.5 py-1 text-xs font-medium text-white bg-blue-600 hover:bg-blue-700 disabled:bg-blue-400 rounded transition-colors"
class="min-h-11 px-2.5 py-1 text-xs font-medium text-white bg-blue-600 hover:bg-blue-700 disabled:bg-blue-400 rounded transition-colors sm:min-h-0"
>
{saving() ? 'Saving…' : 'Save window'}
</button>
@@ -12,6 +12,11 @@ const overviewTabSource = readFileSync(
'utf-8',
);
const supportDisclosureSource = readFileSync(
resolve(__dirname, '..', 'ResourceDetailDrawerSupportDisclosure.tsx'),
'utf-8',
);
describe('ResourceOperatorStateSection', () => {
it('exposes operator-set controls bound to the canonical API client', () => {
// The section is the operator's window into the per-resource state
@@ -160,6 +165,16 @@ describe('ResourceOperatorStateSection', () => {
expect(sectionSource).toContain('maintenanceEndAt: undefined,');
});
it('keeps drawer disclosure and operator controls touch-safe on phones', () => {
expect(supportDisclosureSource).toContain('inline-flex min-h-11 shrink-0 items-center');
expect(sectionSource).toContain('min-h-11 w-full text-xs rounded');
expect(sectionSource).toContain('min-h-11 min-w-11 px-1.5 py-0.5');
expect(sectionSource).toContain('min-h-11 px-2.5 py-1 text-xs font-medium text-base-content');
expect(sectionSource).toContain(
'min-h-11 px-2.5 py-1 text-xs font-medium text-white bg-blue-600',
);
});
it('renders a maintenance-window-active badge when the persisted window covers now', () => {
// The section must gate the active badge on the
// now-falls-within-window check, not just on the presence of a
@@ -104,7 +104,7 @@ export const GuestDrawerHistoryRangeSelect: Component<GuestDrawerHistoryRangeSel
label="History range"
labelClass="sr-only"
fieldBaseClass="contents"
selectBaseClass={`${filterSelectClass} h-7 py-0 text-[11px]`}
selectBaseClass={`${filterSelectClass} h-7 min-h-11 py-0 text-[11px] sm:min-h-0`}
data-testid="guest-history-range-control"
value={props.range}
onChange={(event) => props.onRangeChange(event.currentTarget.value as HistoryTimeRange)}
@@ -1425,6 +1425,7 @@ describe('Workloads performance contract', () => {
expect(guestDrawerSource).not.toContain('Open related infrastructure');
expect(guestDrawerHistorySource).toContain('data-testid="guest-history-hover-time"');
expect(guestDrawerHistorySource).toContain('data-testid="guest-history-range-control"');
expect(guestDrawerHistorySource).toContain('h-7 min-h-11 py-0 text-[11px] sm:min-h-0');
expect(guestDrawerHistorySource).toContain('onPointerMove={handleHoverMove}');
expect(guestDrawerHistorySource).toContain('fallbackMetrics');
expect(guestDrawerHistorySource).toContain('props.groups ?? GUEST_DRAWER_HISTORY_GROUPS');
@@ -3815,7 +3815,7 @@ describe('shared primitive guardrails', () => {
expect(drawerHeaderActionGuard?.canonical?.path).toBe('src/components/shared/Button.tsx');
expect(drawerHeaderActionGuard?.canonical?.export).toBe('DrawerHeaderActionButton');
expect(drawerHeaderActionGuard?.allPatterns).toEqual([
'inline-flex h-8 items-center gap-1.5 rounded border border-border bg-surface px-2 text-xs font-medium text-base-content transition-colors hover:bg-surface-hover focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500',
'inline-flex h-8 min-h-11 min-w-11 items-center gap-1.5 rounded border border-border bg-surface px-2 text-xs font-medium text-base-content transition-colors hover:bg-surface-hover focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500',
]);
expect(drawerHeaderActionGuard?.scopes).toEqual([
'src/components',
@@ -3829,7 +3829,7 @@ describe('shared primitive guardrails', () => {
expect(drawerHeaderIconGuard?.canonical?.path).toBe('src/components/shared/Button.tsx');
expect(drawerHeaderIconGuard?.canonical?.export).toBe('DrawerHeaderIconButton');
expect(drawerHeaderIconGuard?.allPatterns).toEqual([
'inline-flex h-8 w-8 items-center justify-center rounded-md hover:bg-surface-hover hover:text-base-content focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500',
'inline-flex h-8 w-8 min-h-11 min-w-11 items-center justify-center rounded-md hover:bg-surface-hover hover:text-base-content focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500',
]);
expect(drawerHeaderIconGuard?.scopes).toEqual(['src/components', 'src/features', 'src/pages']);
expect(drawerHeaderIconGuard?.allowedPaths ?? []).toHaveLength(0);
@@ -4147,10 +4147,10 @@ describe('shared primitive guardrails', () => {
expect(drawerSource).toContain('DrawerHeaderActionButton');
expect(drawerSource).toContain('DrawerHeaderIconButton');
expect(drawerSource).not.toContain(
'inline-flex h-8 items-center gap-1.5 rounded border border-border bg-surface px-2 text-xs font-medium text-base-content transition-colors hover:bg-surface-hover focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500',
'inline-flex h-8 min-h-11 min-w-11 items-center gap-1.5 rounded border border-border bg-surface px-2 text-xs font-medium text-base-content transition-colors hover:bg-surface-hover focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500',
);
expect(drawerSource).not.toContain(
'inline-flex h-8 w-8 items-center justify-center rounded-md hover:bg-surface-hover hover:text-base-content focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500',
'inline-flex h-8 w-8 min-h-11 min-w-11 items-center justify-center rounded-md hover:bg-surface-hover hover:text-base-content focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500',
);
}
expect(chatMessagesSource).toContain('@/components/shared/Button');
@@ -183,10 +183,10 @@ export const getActionIconButtonClass = (options: ActionIconButtonClassOptions =
export const DRAWER_HEADER_ACTION_GROUP_CLASS = 'flex shrink-0 items-center gap-1.5';
export const DRAWER_HEADER_ACTION_BUTTON_CLASS =
'inline-flex h-8 items-center gap-1.5 rounded border border-border bg-surface px-2 text-xs font-medium text-base-content transition-colors hover:bg-surface-hover focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 disabled:cursor-wait disabled:opacity-60';
'inline-flex h-8 min-h-11 min-w-11 items-center gap-1.5 rounded border border-border bg-surface px-2 text-xs font-medium text-base-content transition-colors hover:bg-surface-hover focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 disabled:cursor-wait disabled:opacity-60 sm:min-h-0 sm:min-w-0';
export const DRAWER_HEADER_ICON_BUTTON_CLASS =
'inline-flex h-8 w-8 items-center justify-center rounded-md hover:bg-surface-hover hover:text-base-content focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500';
'inline-flex h-8 w-8 min-h-11 min-w-11 items-center justify-center rounded-md hover:bg-surface-hover hover:text-base-content focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 sm:min-h-0 sm:min-w-0';
export const getDrawerHeaderActionGroupClass = (className?: string): string =>
[DRAWER_HEADER_ACTION_GROUP_CLASS, className].filter(Boolean).join(' ');
@@ -162,7 +162,7 @@ export const DockerImagesTable: Component<
kind="numeric-value"
sort={sort}
sortKey="size"
class="md:w-[12%]"
class="hidden sm:table-cell md:w-[12%]"
>
Size
</PlatformSortableTableHead>
@@ -225,7 +225,7 @@ export const DockerImagesTable: Component<
</span>
</TableCell>
<TableCell
class={`${getPlatformTableCellClassForKind('numeric-value')} text-base-content`}
class={`${getPlatformTableCellClassForKind('numeric-value')} hidden text-base-content sm:table-cell`}
>
{dockerByteValue(resource.docker?.sizeBytes)}
</TableCell>
@@ -187,12 +187,13 @@ export const DockerServicesTable: Component<{
header={
<>
{/*
Desktop widths: Service and Image take the lion's share
Wide desktop widths: Service and Image take the lion's share
because their content is long (registry refs, fully
qualified service names). Mode / Desired / Running trim
to short text and 1-2 digit counts. Update, Ports, and
Host get middle slices for rollout state, port lists, and
hostnames. Mobile widths are unchanged.
hostnames. Phones keep the decision-making replica pair;
the basic container tier restores mode, update, and host.
*/}
<PlatformSortableTableHead
kind="name"
@@ -218,14 +219,19 @@ export const DockerServicesTable: Component<{
>
Image
</PlatformSortableTableHead>
<PlatformSortableTableHead kind="text" sort={sort} sortKey="mode" class="md:w-[8%]">
<PlatformSortableTableHead
kind="text"
sort={sort}
sortKey="mode"
class="hidden sm:table-cell md:w-[8%]"
>
Mode
</PlatformSortableTableHead>
<PlatformSortableTableHead
kind="numeric-value"
sort={sort}
sortKey="desired"
class="hidden md:table-cell md:w-[8%]"
class="md:w-[8%]"
>
Desired
</PlatformSortableTableHead>
@@ -241,7 +247,7 @@ export const DockerServicesTable: Component<{
kind="text"
sort={sort}
sortKey="update"
class="hidden md:table-cell md:w-[12%]"
class="hidden sm:table-cell md:w-[12%]"
>
Update
</PlatformSortableTableHead>
@@ -256,7 +262,7 @@ export const DockerServicesTable: Component<{
kind="text"
sort={sort}
sortKey="host"
class="hidden md:table-cell md:w-[10%]"
class="hidden sm:table-cell md:w-[10%]"
>
Host
</PlatformSortableTableHead>
@@ -303,12 +309,12 @@ export const DockerServicesTable: Component<{
</span>
</TableCell>
<TableCell
class={`${getPlatformTableCellClassForKind('text')} text-base-content`}
class={`${getPlatformTableCellClassForKind('text')} hidden text-base-content sm:table-cell`}
>
{mode()}
</TableCell>
<TableCell
class={`${getPlatformTableCellClassForKind('numeric-value')} hidden text-base-content md:table-cell`}
class={`${getPlatformTableCellClassForKind('numeric-value')} text-base-content`}
>
<PlatformTableNumberValue value={service.docker?.desiredTasks ?? 0} />
</TableCell>
@@ -318,7 +324,7 @@ export const DockerServicesTable: Component<{
<PlatformTableNumberValue value={service.docker?.runningTasks ?? 0} />
</TableCell>
<TableCell
class={`${getPlatformTableCellClassForKind('text')} hidden text-base-content md:table-cell`}
class={`${getPlatformTableCellClassForKind('text')} hidden text-base-content sm:table-cell`}
>
<span class="truncate inline-block max-w-[10rem]" title={update().title}>
{update().label}
@@ -332,7 +338,7 @@ export const DockerServicesTable: Component<{
</span>
</TableCell>
<TableCell
class={`${getPlatformTableCellClassForKind('text')} hidden text-base-content md:table-cell`}
class={`${getPlatformTableCellClassForKind('text')} hidden text-base-content sm:table-cell`}
>
{host()}
</TableCell>
@@ -166,7 +166,7 @@ export const DockerSwarmNodesTable: Component<DockerNativeTableProps> = (props)
kind="text"
sort={sort}
sortKey="reachability"
class="hidden md:table-cell md:w-[14%]"
class="md:w-[14%]"
>
Reachability
</PlatformSortableTableHead>
@@ -190,7 +190,7 @@ export const DockerSwarmNodesTable: Component<DockerNativeTableProps> = (props)
kind="numeric-value"
sort={sort}
sortKey="memory"
class="md:w-[10%]"
class="hidden sm:table-cell md:w-[10%]"
>
<PlatformResponsiveTableLabel compact="Mem" full="Memory" />
</PlatformSortableTableHead>
@@ -198,7 +198,7 @@ export const DockerSwarmNodesTable: Component<DockerNativeTableProps> = (props)
kind="text"
sort={sort}
sortKey="address"
class="hidden md:table-cell md:w-[10%]"
class="hidden sm:table-cell md:w-[10%]"
>
Address
</PlatformSortableTableHead>
@@ -233,7 +233,7 @@ export const DockerSwarmNodesTable: Component<DockerNativeTableProps> = (props)
{dockerTextValue(resource.docker?.availability)}
</TableCell>
<TableCell
class={`${getPlatformTableCellClassForKind('text')} hidden text-base-content md:table-cell`}
class={`${getPlatformTableCellClassForKind('text')} text-base-content`}
>
{managerReachability()}
</TableCell>
@@ -250,12 +250,12 @@ export const DockerSwarmNodesTable: Component<DockerNativeTableProps> = (props)
{dockerCpuValue(resource.docker?.nanoCpus)}
</TableCell>
<TableCell
class={`${getPlatformTableCellClassForKind('numeric-value')} text-base-content`}
class={`${getPlatformTableCellClassForKind('numeric-value')} hidden text-base-content sm:table-cell`}
>
{dockerByteValue(resource.docker?.memoryBytes)}
</TableCell>
<TableCell
class={`${getPlatformTableCellClassForKind('text')} hidden text-base-content md:table-cell`}
class={`${getPlatformTableCellClassForKind('text')} hidden text-base-content sm:table-cell`}
>
{dockerTextValue(
resource.docker?.address || resource.docker?.managerAddress,
@@ -133,7 +133,7 @@ export const DockerTasksTable: Component<DockerNativeTableProps> = (props) => {
kind="text"
sort={sort}
sortKey="service"
class="hidden md:table-cell md:w-[18%]"
class="hidden sm:table-cell md:w-[18%]"
>
Service
</PlatformSortableTableHead>
@@ -165,7 +165,7 @@ export const DockerTasksTable: Component<DockerNativeTableProps> = (props) => {
kind="text"
sort={sort}
sortKey="node"
class="hidden md:table-cell md:w-[16%]"
class="hidden sm:table-cell md:w-[16%]"
>
Node
</PlatformSortableTableHead>
@@ -189,7 +189,7 @@ export const DockerTasksTable: Component<DockerNativeTableProps> = (props) => {
indicator={mapDockerTaskStatus(resource)}
/>
<TableCell
class={`${getPlatformTableCellClassForKind('text')} hidden text-base-content md:table-cell`}
class={`${getPlatformTableCellClassForKind('text')} hidden text-base-content sm:table-cell`}
>
{dockerTextValue(resource.docker?.serviceName)}
</TableCell>
@@ -218,7 +218,7 @@ export const DockerTasksTable: Component<DockerNativeTableProps> = (props) => {
</span>
</TableCell>
<TableCell
class={`${getPlatformTableCellClassForKind('text')} hidden text-base-content md:table-cell`}
class={`${getPlatformTableCellClassForKind('text')} hidden text-base-content sm:table-cell`}
>
{dockerTextValue(resource.docker?.nodeName || resource.docker?.nodeId)}
</TableCell>
@@ -315,7 +315,7 @@ export const KubernetesClustersTable: Component<{
<Show when={props.onSelectCluster} fallback={name()}>
<button
type="button"
class={`truncate rounded-sm text-left font-semibold underline-offset-2 hover:text-blue-600 hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 dark:hover:text-blue-400 ${
class={`-my-3 inline-flex min-h-11 items-center truncate rounded-sm py-3 text-left font-semibold underline-offset-2 hover:text-blue-600 hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 sm:my-0 sm:min-h-0 sm:py-0 dark:hover:text-blue-400 ${
props.selectedClusterId === clusterScopeId()
? 'text-blue-600 dark:text-blue-400'
: 'text-base-content'
@@ -274,7 +274,7 @@ export const KubernetesConfigTable: Component<{
kind="text"
sort={sort}
sortKey="scope"
class="hidden md:table-cell md:w-[16%]"
class="hidden sm:table-cell md:w-[16%]"
>
Scope
</PlatformSortableTableHead>
@@ -356,7 +356,7 @@ export const KubernetesConfigTable: Component<{
{kind()}
</TableCell>
<TableCell
class={`${getPlatformTableCellClassForKind('text')} hidden text-base-content md:table-cell`}
class={`${getPlatformTableCellClassForKind('text')} hidden text-base-content sm:table-cell`}
>
<span class="inline-block max-w-[13rem] truncate" title={scope()}>
{scope()}
@@ -167,7 +167,8 @@ export const KubernetesDeploymentsTable: Component<{
the biggest shares because their content can be long.
The integer-count columns (Desired / Updated / Ready /
Available) trim to what their headers plus 1-2 digit
values need. Mobile widths are unchanged.
values need. The basic tier restores namespace context
without crowding the phone tier.
*/}
<PlatformSortableTableHead
kind="name"
@@ -181,7 +182,7 @@ export const KubernetesDeploymentsTable: Component<{
kind="text"
sort={sort}
sortKey="namespace"
class="hidden md:table-cell md:w-[20%]"
class="hidden sm:table-cell md:w-[20%]"
>
Namespace
</PlatformSortableTableHead>
@@ -279,7 +280,7 @@ export const KubernetesDeploymentsTable: Component<{
</div>
</TableCell>
<TableCell
class={`${getPlatformTableCellClassForKind('text')} hidden text-base-content md:table-cell`}
class={`${getPlatformTableCellClassForKind('text')} hidden text-base-content sm:table-cell`}
>
{ns()}
</TableCell>
@@ -218,7 +218,7 @@ export const KubernetesNetworkingTable: Component<{
kind="text"
sort={sort}
sortKey="scope"
class="hidden md:table-cell md:w-[14%]"
class="hidden sm:table-cell md:w-[14%]"
>
Scope
</PlatformSortableTableHead>
@@ -305,7 +305,7 @@ export const KubernetesNetworkingTable: Component<{
{networkKind(resource)}
</TableCell>
<TableCell
class={`${getPlatformTableCellClassForKind('text')} hidden text-base-content md:table-cell`}
class={`${getPlatformTableCellClassForKind('text')} hidden text-base-content sm:table-cell`}
>
<span class="inline-block max-w-[12rem] truncate" title={scope()}>
{scope()}
@@ -210,7 +210,7 @@ export const KubernetesNodesTable: Component<{
kind="text"
sort={sort}
sortKey="cluster"
class="hidden md:table-cell md:w-[10%]"
class="hidden sm:table-cell md:w-[10%]"
>
Cluster
</PlatformSortableTableHead>
@@ -218,7 +218,7 @@ export const KubernetesNodesTable: Component<{
kind="text"
sort={sort}
sortKey="roles"
class="hidden md:table-cell md:w-[10%]"
class="hidden sm:table-cell md:w-[10%]"
>
Roles
</PlatformSortableTableHead>
@@ -266,7 +266,7 @@ export const KubernetesNodesTable: Component<{
kind="numeric-value"
sort={sort}
sortKey="capacity"
class="md:w-[14%]"
class="hidden sm:table-cell md:w-[14%]"
>
Capacity
</PlatformSortableTableHead>
@@ -366,12 +366,12 @@ export const KubernetesNodesTable: Component<{
</div>
</TableCell>
<TableCell
class={`${getPlatformTableCellClassForKind('text')} hidden text-base-content md:table-cell`}
class={`${getPlatformTableCellClassForKind('text')} hidden text-base-content sm:table-cell`}
>
{cluster()}
</TableCell>
<TableCell
class={`${getPlatformTableCellClassForKind('text')} hidden text-base-content md:table-cell`}
class={`${getPlatformTableCellClassForKind('text')} hidden text-base-content sm:table-cell`}
>
{formatRoles(meta()?.roles)}
</TableCell>
@@ -421,7 +421,7 @@ export const KubernetesNodesTable: Component<{
{formatPlatformTableUptimeValue(node.uptime)}
</TableCell>
<TableCell
class={`${getPlatformTableCellClassForKind('numeric-value')} text-base-content tabular-nums`}
class={`${getPlatformTableCellClassForKind('numeric-value')} hidden text-base-content tabular-nums sm:table-cell`}
>
<span class="md:hidden">{compactCapacityLabel()}</span>
<span class="hidden md:inline">{capacityLabel()}</span>
@@ -212,7 +212,7 @@ export const KubernetesPodsTable: Component<{
kind="text"
sort={sort}
sortKey="scope"
class="hidden md:table-cell md:w-[13%]"
class="hidden sm:table-cell md:w-[13%]"
>
Scope
</PlatformSortableTableHead>
@@ -220,7 +220,7 @@ export const KubernetesPodsTable: Component<{
kind="text"
sort={sort}
sortKey="node"
class="hidden md:table-cell md:w-[13%]"
class="hidden sm:table-cell md:w-[13%]"
>
Node
</PlatformSortableTableHead>
@@ -322,14 +322,14 @@ export const KubernetesPodsTable: Component<{
</div>
</TableCell>
<TableCell
class={`${getPlatformTableCellClassForKind('text')} hidden text-base-content md:table-cell`}
class={`${getPlatformTableCellClassForKind('text')} hidden text-base-content sm:table-cell`}
>
<span class="block max-w-full truncate" title={scope()}>
{scope()}
</span>
</TableCell>
<TableCell
class={`${getPlatformTableCellClassForKind('text')} hidden text-base-content md:table-cell`}
class={`${getPlatformTableCellClassForKind('text')} hidden text-base-content sm:table-cell`}
>
<span class="block max-w-full truncate" title={node()}>
{node()}
@@ -246,7 +246,7 @@ export const KubernetesPolicyTable: Component<{
Kind
</TableHead>
<TableHead
class={`${getPlatformTableHeadClassForKind('text')} hidden md:table-cell md:w-[15%]`}
class={`${getPlatformTableHeadClassForKind('text')} hidden sm:table-cell md:w-[15%]`}
>
Scope
</TableHead>
@@ -318,7 +318,7 @@ export const KubernetesPolicyTable: Component<{
{policyKind(resource)}
</TableCell>
<TableCell
class={`${getPlatformTableCellClassForKind('text')} hidden text-base-content md:table-cell`}
class={`${getPlatformTableCellClassForKind('text')} hidden text-base-content sm:table-cell`}
>
<span class="inline-block max-w-[13rem] truncate" title={scope()}>
{scope()}
@@ -168,7 +168,7 @@ export const KubernetesServicesTable: Component<{
kind="text"
sort={sort}
sortKey="scope"
class="hidden md:table-cell md:w-[15%]"
class="hidden sm:table-cell md:w-[15%]"
>
Scope
</PlatformSortableTableHead>
@@ -262,7 +262,7 @@ export const KubernetesServicesTable: Component<{
</div>
</TableCell>
<TableCell
class={`${getPlatformTableCellClassForKind('text')} hidden text-base-content md:table-cell`}
class={`${getPlatformTableCellClassForKind('text')} hidden text-base-content sm:table-cell`}
>
<span class="inline-block max-w-[12rem] truncate" title={scope()}>
{scope()}
@@ -288,7 +288,7 @@ export const KubernetesStorageTable: Component<{
kind="text"
sort={sort}
sortKey="scope"
class="hidden md:table-cell md:w-[13%]"
class="hidden sm:table-cell md:w-[13%]"
>
Scope
</PlatformSortableTableHead>
@@ -304,7 +304,7 @@ export const KubernetesStorageTable: Component<{
kind="text"
sort={sort}
sortKey="class"
class="hidden md:table-cell md:w-[10%]"
class="hidden sm:table-cell md:w-[10%]"
>
Class
</PlatformSortableTableHead>
@@ -382,7 +382,7 @@ export const KubernetesStorageTable: Component<{
{kind()}
</TableCell>
<TableCell
class={`${getPlatformTableCellClassForKind('text')} hidden text-base-content md:table-cell`}
class={`${getPlatformTableCellClassForKind('text')} hidden text-base-content sm:table-cell`}
>
<span class="inline-block max-w-[12rem] truncate" title={scope()}>
{scope()}
@@ -394,7 +394,7 @@ export const KubernetesStorageTable: Component<{
{bindingOrPhase(resource)}
</TableCell>
<TableCell
class={`${getPlatformTableCellClassForKind('text')} hidden text-base-content md:table-cell`}
class={`${getPlatformTableCellClassForKind('text')} hidden text-base-content sm:table-cell`}
>
{storageClass(resource)}
</TableCell>
@@ -282,6 +282,88 @@ describe('platform overview layout guardrails', () => {
expect(dockerHostsTableSource).toContain('md:w-[10%]');
});
it('prioritizes Docker phone decisions and restores context at the basic table tier', () => {
expect(dockerImagesTableSource).toMatch(
/sortKey="size"[\s\S]{0,120}?class="hidden sm:table-cell md:w-\[12%\]"/,
);
expect(dockerServicesTableSource).toMatch(
/sortKey="mode"[\s\S]{0,120}?class="hidden sm:table-cell md:w-\[8%\]"/,
);
expect(dockerServicesTableSource).toMatch(/sortKey="desired"[\s\S]{0,120}?class="md:w-\[8%\]"/);
expect(dockerServicesTableSource).toMatch(
/sortKey="update"[\s\S]{0,120}?class="hidden sm:table-cell md:w-\[12%\]"/,
);
expect(dockerTasksTableSource).toMatch(
/sortKey="service"[\s\S]{0,120}?class="hidden sm:table-cell md:w-\[18%\]"/,
);
expect(dockerTasksTableSource).toMatch(
/sortKey="node"[\s\S]{0,120}?class="hidden sm:table-cell md:w-\[16%\]"/,
);
expect(dockerSwarmNodesTableSource).toMatch(
/sortKey="reachability"[\s\S]{0,120}?class="md:w-\[14%\]"/,
);
expect(dockerSwarmNodesTableSource).toMatch(
/sortKey="memory"[\s\S]{0,120}?class="hidden sm:table-cell md:w-\[10%\]"/,
);
});
it('keeps Kubernetes phone identity readable and restores scope at the basic table tier', () => {
expect(kubernetesClustersTableSource).toContain(
'-my-3 inline-flex min-h-11 items-center truncate',
);
expect(kubernetesNodesTableSource).toMatch(
/sortKey="roles"[\s\S]{0,120}?class="hidden sm:table-cell md:w-\[10%\]"/,
);
expect(kubernetesNodesTableSource).toMatch(
/sortKey="capacity"[\s\S]{0,120}?class="hidden sm:table-cell md:w-\[14%\]"/,
);
expect(kubernetesDeploymentsTableSource).toMatch(
/sortKey="namespace"[\s\S]{0,120}?class="hidden sm:table-cell md:w-\[20%\]"/,
);
expect(kubernetesPodsTableSource).toMatch(
/sortKey="scope"[\s\S]{0,120}?class="hidden sm:table-cell md:w-\[13%\]"/,
);
expect(kubernetesPodsTableSource).toMatch(
/sortKey="node"[\s\S]{0,120}?class="hidden sm:table-cell md:w-\[13%\]"/,
);
expect(kubernetesServicesTableSource).toMatch(
/sortKey="scope"[\s\S]{0,120}?class="hidden sm:table-cell md:w-\[15%\]"/,
);
expect(kubernetesNetworkingTableSource).toMatch(
/sortKey="scope"[\s\S]{0,120}?class="hidden sm:table-cell md:w-\[14%\]"/,
);
expect(kubernetesStorageTableSource).toMatch(
/sortKey="class"[\s\S]{0,120}?class="hidden sm:table-cell md:w-\[10%\]"/,
);
expect(kubernetesConfigTableSource).toMatch(
/sortKey="scope"[\s\S]{0,120}?class="hidden sm:table-cell md:w-\[16%\]"/,
);
expect(kubernetesPolicyTableSource).toContain(
"getPlatformTableHeadClassForKind('text')} hidden sm:table-cell md:w-[15%]",
);
});
it('keeps remaining platform phone identity and actions usable', () => {
expect(truenasStorageTopologyTableSource).toMatch(
/sortKey="kind"[\s\S]{0,120}?class="hidden sm:table-cell md:w-\[10%\]"/,
);
expect(truenasStorageTopologyTableSource).toContain("return 'pl-6 sm:pl-11'");
expect(truenasStorageTopologyTableSource).toMatch(
/sortKey="resource"[\s\S]{0,120}?class="w-\[47%\] md:w-\[32%\]"/,
);
expect(vsphereNetworksTableSource).toMatch(
/sortKey="network"[\s\S]{0,120}?class="w-\[80%\] sm:w-\[45%\] md:w-\[24%\]"/,
);
expect(vsphereNetworksTableSource).toMatch(
/sortKey="type"[\s\S]{0,120}?class="hidden sm:table-cell sm:w-\[35%\] md:w-\[13%\]"/,
);
expect(agentsMachinesTableSource).toContain('class="min-h-11 min-w-11 sm:min-h-0 sm:min-w-0"');
expect(agentsMachinesTableSource).toContain(
'-my-3 inline-flex min-h-11 max-w-full items-center',
);
expect(agentsMachinesTableSource).toContain('flex min-h-11 w-full items-center gap-2 rounded');
});
it('keeps provider overview pages in the parent-table plus child-inventory stack', () => {
for (const source of overviewSurfaceSources) {
expect(source).toMatch(/<div[^>]*class="[^"]*\bspace-y-4\b[^"]*"/);
@@ -758,6 +758,7 @@ const AgentMachineActionsCell: Component<{
label={`Machine actions for ${props.name}`}
tone="muted"
size="sm"
class="min-h-11 min-w-11 sm:min-h-0 sm:min-w-0"
aria-haspopup="menu"
aria-expanded={props.menuOpen ? 'true' : 'false'}
onClick={(event) => {
@@ -790,7 +791,7 @@ const AgentMachineActionsCell: Component<{
<button
type="button"
role="menuitem"
class="flex w-full items-center gap-2 rounded px-2 py-1.5 text-left text-xs font-medium text-red-600 transition-colors hover:bg-red-50 focus:outline-none focus-visible:ring-2 focus-visible:ring-red-500/60 disabled:cursor-not-allowed disabled:opacity-50 dark:text-red-400 dark:hover:bg-red-950"
class="flex min-h-11 w-full items-center gap-2 rounded px-2 py-1.5 text-left text-xs font-medium text-red-600 transition-colors hover:bg-red-50 focus:outline-none focus-visible:ring-2 focus-visible:ring-red-500/60 disabled:cursor-not-allowed disabled:opacity-50 sm:min-h-0 dark:text-red-400 dark:hover:bg-red-950"
aria-label={props.confirmingRemoval ? confirmLabel() : removeLabel()}
disabled={props.removing || !props.agentId}
onClick={(event) => {
@@ -1137,7 +1138,7 @@ const AgentMachineSortableHead: Component<{
{(sortKey) => (
<button
type="button"
class="inline-flex max-w-full items-center gap-1 truncate hover:text-base-content focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500/60"
class="-my-3 inline-flex min-h-11 max-w-full items-center gap-1 truncate py-3 hover:text-base-content focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500/60 sm:my-0 sm:min-h-0 sm:py-0"
onClick={() => props.onSort(sortKey())}
aria-label={`Sort by ${props.column.label}`}
>
@@ -242,9 +242,9 @@ const sortTrueNASStorageTopologyRows = (
export const getTrueNASStorageTopologyIndentClass = (depth: number): string => {
if (depth <= 0) return '';
if (depth === 1) return 'pl-5 sm:pl-7';
if (depth === 2) return 'pl-9 sm:pl-11';
return 'pl-12 sm:pl-16';
if (depth === 1) return 'pl-3 sm:pl-7';
if (depth === 2) return 'pl-6 sm:pl-11';
return 'pl-8 sm:pl-16';
};
const ResourceCell: Component<{ row: TrueNASStorageTopologyRow; detailToggle?: JSX.Element }> = (
@@ -342,7 +342,7 @@ export const TrueNASStorageTopologyTable: Component<{
kind="name"
sort={sort}
sortKey="resource"
class="md:w-[32%]"
class="w-[47%] md:w-[32%]"
>
Resource
</PlatformSortableTableHead>
@@ -350,7 +350,7 @@ export const TrueNASStorageTopologyTable: Component<{
kind="text"
sort={sort}
sortKey="kind"
class="md:w-[10%]"
class="hidden sm:table-cell md:w-[10%]"
>
Kind
</PlatformSortableTableHead>
@@ -358,7 +358,7 @@ export const TrueNASStorageTopologyTable: Component<{
kind="metric-bar"
sort={sort}
sortKey="usage"
class="md:w-[28%]"
class="w-[28%] md:w-[28%]"
>
<PlatformResponsiveTableLabel compact="Usage" full="Usage / Size" />
</PlatformSortableTableHead>
@@ -382,7 +382,7 @@ export const TrueNASStorageTopologyTable: Component<{
kind="badge"
sort={sort}
sortKey="health"
class="md:w-[14%]"
class="w-[25%] md:w-[14%]"
>
Health
</PlatformSortableTableHead>
@@ -422,7 +422,9 @@ export const TrueNASStorageTopologyTable: Component<{
}
/>
</TableCell>
<TableCell class={getPlatformTableCellClassForKind('text')}>
<TableCell
class={`${getPlatformTableCellClassForKind('text')} hidden sm:table-cell`}
>
<span class="text-base-content">{kindLabel(row.kind)}</span>
</TableCell>
<TableCell class={getPlatformTableCellClassForKind('metric-bar')}>
@@ -71,23 +71,23 @@ describe('TrueNASStorageTopologyTable', () => {
expect(
mediaRow
?.querySelector('[data-truenas-storage-indent-depth="1"]')
?.classList.contains('pl-5'),
?.classList.contains('pl-3'),
).toBe(true);
expect(
photosRow
?.querySelector('[data-truenas-storage-indent-depth="2"]')
?.classList.contains('pl-9'),
?.classList.contains('pl-6'),
).toBe(true);
expect(
rawRow?.querySelector('[data-truenas-storage-indent-depth="3"]')?.classList.contains('pl-12'),
rawRow?.querySelector('[data-truenas-storage-indent-depth="3"]')?.classList.contains('pl-8'),
).toBe(true);
});
it('caps deep indentation at the table-safe depth class', () => {
expect(getTrueNASStorageTopologyIndentClass(0)).toBe('');
expect(getTrueNASStorageTopologyIndentClass(1)).toBe('pl-5 sm:pl-7');
expect(getTrueNASStorageTopologyIndentClass(2)).toBe('pl-9 sm:pl-11');
expect(getTrueNASStorageTopologyIndentClass(3)).toBe('pl-12 sm:pl-16');
expect(getTrueNASStorageTopologyIndentClass(8)).toBe('pl-12 sm:pl-16');
expect(getTrueNASStorageTopologyIndentClass(1)).toBe('pl-3 sm:pl-7');
expect(getTrueNASStorageTopologyIndentClass(2)).toBe('pl-6 sm:pl-11');
expect(getTrueNASStorageTopologyIndentClass(3)).toBe('pl-8 sm:pl-16');
expect(getTrueNASStorageTopologyIndentClass(8)).toBe('pl-8 sm:pl-16');
});
});
@@ -173,7 +173,7 @@ export const VsphereNetworksTable: Component<{
kind="name"
sort={sort}
sortKey="network"
class="w-[45%] md:w-[24%]"
class="w-[80%] sm:w-[45%] md:w-[24%]"
>
Network
</PlatformSortableTableHead>
@@ -181,7 +181,7 @@ export const VsphereNetworksTable: Component<{
kind="text"
sort={sort}
sortKey="type"
class="w-[35%] md:w-[13%]"
class="hidden sm:table-cell sm:w-[35%] md:w-[13%]"
>
Type
</PlatformSortableTableHead>
@@ -268,7 +268,9 @@ export const VsphereNetworksTable: Component<{
</span>
</div>
</TableCell>
<TableCell class={getPlatformTableCellClassForKind('text')}>
<TableCell
class={`${getPlatformTableCellClassForKind('text')} hidden sm:table-cell`}
>
<span class="font-mono text-[11px] text-base-content">
{networkType(network)}
</span>