diff --git a/frontend-modern/browser-verification.json b/frontend-modern/browser-verification.json index 406ef4027..404fba00e 100644 --- a/frontend-modern/browser-verification.json +++ b/frontend-modern/browser-verification.json @@ -1,63 +1,49 @@ { "version": 1, - "base_sha": "95745e77ca161831a4c5a90397b5723c2fc80e98", - "verified_at": "2026-08-04T11:37:59Z", + "base_sha": "2c0899e2086b4a2b9f501896dba5e5f894a41f66", + "verified_at": "2026-08-04T13:56:01Z", "result": "passed", "changed_paths": [ - "frontend-modern/src/components/Workloads/WorkloadTableHeader.tsx", - "frontend-modern/src/components/Workloads/WorkloadsTable.tsx", - "frontend-modern/src/components/Workloads/guestRowModel.tsx", + "frontend-modern/src/features/docker/DockerContainerLifecycleControls.tsx", + "frontend-modern/src/features/docker/DockerContainersTable.tsx", "frontend-modern/src/features/docker/DockerImagesTable.tsx", "frontend-modern/src/features/docker/DockerNetworksTable.tsx", - "frontend-modern/src/features/docker/DockerPageSurface.tsx", + "frontend-modern/src/features/docker/dockerContainerTableModel.ts", "frontend-modern/src/features/kubernetes/KubernetesAutoscalingTable.tsx", + "frontend-modern/src/features/kubernetes/KubernetesConfigTable.tsx", "frontend-modern/src/features/kubernetes/KubernetesControllersTable.tsx", - "frontend-modern/src/features/kubernetes/KubernetesEventsTable.tsx", - "frontend-modern/src/features/kubernetes/KubernetesPageSurface.tsx", + "frontend-modern/src/features/kubernetes/KubernetesNetworkingTable.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/platformPage/sharedPlatformPage.tsx", "frontend-modern/src/features/proxmox/ProxmoxCephClusterDrawer.tsx", - "frontend-modern/src/features/proxmox/ProxmoxCephTable.tsx", - "frontend-modern/src/features/proxmox/ProxmoxMailGatewayTable.tsx", - "frontend-modern/src/features/proxmox/ProxmoxNodesTable.tsx", - "frontend-modern/src/features/proxmox/ProxmoxPageSurface.tsx", "frontend-modern/src/features/proxmox/ProxmoxReplicationTable.tsx", - "frontend-modern/src/features/proxmox/proxmoxHostTableModel.ts", - "frontend-modern/src/features/standalone/StandalonePageSurface.tsx", - "frontend-modern/src/features/truenas/TrueNASPageSurface.tsx", + "frontend-modern/src/features/truenas/TrueNASProtectionTable.tsx", "frontend-modern/src/features/truenas/TrueNASStorageTopologyTable.tsx", - "frontend-modern/src/features/truenas/TrueNASSystemsTable.tsx", - "frontend-modern/src/features/vmware/VmwarePageSurface.tsx", - "frontend-modern/src/features/vmware/VsphereDatastoresTable.tsx", - "frontend-modern/src/features/vmware/VsphereNetworksTable.tsx", + "frontend-modern/src/features/vmware/VsphereHostsTable.tsx", "frontend-modern/src/index.css" ], "content_sha256": { - "frontend-modern/src/components/Workloads/WorkloadTableHeader.tsx": "ef811962a39ef0c8c70060241b585f9612cf162c53071524a2d78359c679ada4", - "frontend-modern/src/components/Workloads/WorkloadsTable.tsx": "cfcbda5651a54698af4575f32dda894580108ccfc08d6b87e7b7e8e6b8d4d5ae", - "frontend-modern/src/components/Workloads/guestRowModel.tsx": "336410d2ffe6a72da7f2040156053f67c5503b2d37c0ac7230a7c28afc8a08f0", - "frontend-modern/src/features/docker/DockerImagesTable.tsx": "702d5b2813bca96ecbe632044d3f9dbeab5cb6fea6d634e4a4f9c194e3f10106", - "frontend-modern/src/features/docker/DockerNetworksTable.tsx": "6179a35aa842b4600d16eb581bde4747a1971c9c9d71f2a18de885df2f474b64", - "frontend-modern/src/features/docker/DockerPageSurface.tsx": "e3b4aed8ca3650bdf8bd980c033decc3d6bb9ebfc428620b92bf03808d8dc700", - "frontend-modern/src/features/kubernetes/KubernetesAutoscalingTable.tsx": "54bb033261f4853f31e0b9767be2f9a31886fd1dd2c49c5413fec0f3a840a22d", - "frontend-modern/src/features/kubernetes/KubernetesControllersTable.tsx": "35ec8051643c759d1b72058d04426e246b341401b8b96ca32147e404f444aee6", - "frontend-modern/src/features/kubernetes/KubernetesEventsTable.tsx": "0d5d74c0055ba0365de433f90f2c7cf604b2998cdd59e5cd4fb11e8dde1208b7", - "frontend-modern/src/features/kubernetes/KubernetesPageSurface.tsx": "35723e8090b81d94ec68c29c1baa86e8b1ecf4e903338323136929fb6cbc77b5", - "frontend-modern/src/features/platformPage/sharedPlatformPage.tsx": "53421e71c4046f18dbefa4978dc6c74bf2c852e9d26a4f3ab8c9e003df236dc1", - "frontend-modern/src/features/proxmox/ProxmoxCephClusterDrawer.tsx": "d1e17e02001cd1f88f6e98549115a8c37574a4288e61e5d3d708634d756d0c0d", - "frontend-modern/src/features/proxmox/ProxmoxCephTable.tsx": "6bf47f03195639b2900f16d45539e633a77709860f17150ef400966d161c9f6d", - "frontend-modern/src/features/proxmox/ProxmoxMailGatewayTable.tsx": "54a3c7a1127aa6d5e4822dd2c19ee9242789914a6ee1580bcbd6808fe482d265", - "frontend-modern/src/features/proxmox/ProxmoxNodesTable.tsx": "4539a00569a64c06356c4b0a7decb608842e23c09259e5a12c0b1b40bcde1c5c", - "frontend-modern/src/features/proxmox/ProxmoxPageSurface.tsx": "45a609aeea1ee0800a643b900a57d8d88af3be027e5a7f62c2708d1436b274a3", - "frontend-modern/src/features/proxmox/ProxmoxReplicationTable.tsx": "0c273f41bcd0fbe269d5ec3e876bb5ffaff97542a7fd6327cea2b7785913cde6", - "frontend-modern/src/features/proxmox/proxmoxHostTableModel.ts": "c06ed9b86054a5cef7f4b30d3054619dbb291442d6810fc8761637fc6db59773", - "frontend-modern/src/features/standalone/StandalonePageSurface.tsx": "2ebcc5503c890bfea2f21eaed6c18e0ebb69625701f3884a7c17e62faaea504d", - "frontend-modern/src/features/truenas/TrueNASPageSurface.tsx": "46c506bbf9ca102e3018188e8b0e234d67ff188893d0ea16d95252508579c5ef", - "frontend-modern/src/features/truenas/TrueNASStorageTopologyTable.tsx": "0b846223ff043bc2639f4d2e9fcc70438c02041165b739afebcdb9573c77ccbd", - "frontend-modern/src/features/truenas/TrueNASSystemsTable.tsx": "10a944844841d4188804aaf2a217067b4d0aae21357e7bd3264785f67b3d0308", - "frontend-modern/src/features/vmware/VmwarePageSurface.tsx": "45e71dc9be04a780c175f5c9ff757038f0d5c5df1c538605ec65fc8179cff84f", - "frontend-modern/src/features/vmware/VsphereDatastoresTable.tsx": "76e1acfc2f56dd6e94b142625056bdda2c4ed0c4ce6feec833773e62906dc80e", - "frontend-modern/src/features/vmware/VsphereNetworksTable.tsx": "95e30af160d2e621acf0a1c09f43be788bb8d314c5b4d68790a3594a687e40c0", - "frontend-modern/src/index.css": "4498659bf01b186a9030fe96a58a3c9a0d3f5a901e374f6d9048faf6d2b02b62" + "frontend-modern/src/features/docker/DockerContainerLifecycleControls.tsx": "6626c80c0ff183ef69780362065ab59a627b827c8c8921dd8053a56afcb3fb96", + "frontend-modern/src/features/docker/DockerContainersTable.tsx": "c34d1c973266dbbe4b0de64fc1a1a3af668728cbfd839346b555d1178ec11942", + "frontend-modern/src/features/docker/DockerImagesTable.tsx": "bcccb52e92fe2031e5b4e9b02146d0700c4dd8b8a68b495e23037a51f980c7c5", + "frontend-modern/src/features/docker/DockerNetworksTable.tsx": "3aca178275b70399f32ee1c67168904294cd07551a66999e88f1225bd65271b4", + "frontend-modern/src/features/docker/dockerContainerTableModel.ts": "f997acf6196c492c7aa4e923768545ac3ccb891947d53e37f682359fa7ddb6f3", + "frontend-modern/src/features/kubernetes/KubernetesAutoscalingTable.tsx": "41ae0bcd794d6c4fb21e45cd5fee424fb98c408af82befb4ec57a86213da188d", + "frontend-modern/src/features/kubernetes/KubernetesConfigTable.tsx": "613780c1452feb8392d80d65ca585e474ebacc6919e69dec66c4d41f642fca22", + "frontend-modern/src/features/kubernetes/KubernetesControllersTable.tsx": "007a0a5b37a1055018a6ef0eeae06be9efe5331f23d87f77aaf32f5315f755f3", + "frontend-modern/src/features/kubernetes/KubernetesNetworkingTable.tsx": "d4fdcd6a7061b11b5a890b2e66dd93c44636a401b1afefed806d58b2336e5659", + "frontend-modern/src/features/kubernetes/KubernetesPolicyTable.tsx": "ef509df461f6d5e90911ebd0c0bd7a4d8c465cb43c6b4198a165d24011d5f30f", + "frontend-modern/src/features/kubernetes/KubernetesServicesTable.tsx": "ba9797ebab585bf633d8c9a49762da7239b3dd6aa89de439bafd4471242d7265", + "frontend-modern/src/features/kubernetes/KubernetesStorageTable.tsx": "f3cca1e796b77b92da9af1b8c66001d0b92fecfc199fc1e1f9e921d27978fee4", + "frontend-modern/src/features/platformPage/sharedPlatformPage.tsx": "f3a8799fe8fd60f6cf34b10167ed06d573c825feeb514cf9fd3c3e11e0e128ca", + "frontend-modern/src/features/proxmox/ProxmoxCephClusterDrawer.tsx": "70554e4eaf1434f5bfb88d710b17b638c7e838272244bd8472d79ba7e5e017c3", + "frontend-modern/src/features/proxmox/ProxmoxReplicationTable.tsx": "ad9ecf604b7060d4c96f531099e6e238ac3f47d9a2cd0ad5f1e5d82b64bb8293", + "frontend-modern/src/features/truenas/TrueNASProtectionTable.tsx": "bc68ee342610607a4951c5518c3c652bc54aafa471604b95537e90634a6dbddc", + "frontend-modern/src/features/truenas/TrueNASStorageTopologyTable.tsx": "60b5ee0447c3818c0beb6fdda1e1e73df71e94c51c82855016bcedf4ef267102", + "frontend-modern/src/features/vmware/VsphereHostsTable.tsx": "29ad8f8168af4ed5a63559b25663f04b9d668c58c03ea6d312ebc8f824f85aa8", + "frontend-modern/src/index.css": "e2461a68feb4d21062faa1131c2dd0f9c3c1469b1bcac15e487cc771ac5b7503" }, "routes": [ "/proxmox/overview", @@ -96,44 +82,32 @@ "/standalone/availability" ], "viewports": [ - { - "width": 375, - "height": 900 - }, - { - "width": 768, - "height": 900 - }, - { - "width": 1024, - "height": 900 - }, - { - "width": 1440, - "height": 900 - }, - { - "width": 1920, - "height": 900 - } + { "width": 375, "height": 900 }, + { "width": 540, "height": 900 }, + { "width": 640, "height": 900 }, + { "width": 768, "height": 900 }, + { "width": 900, "height": 900 }, + { "width": 1024, "height": 900 }, + { "width": 1280, "height": 900 }, + { "width": 1440, "height": 900 }, + { "width": 1680, "height": 900 }, + { "width": 1920, "height": 900 } ], "states": [ - "All 34 primary platform routes loaded with the 213-resource mock inventory across Proxmox, Docker, Kubernetes, TrueNAS, VMware, and standalone machines", - "Every platform page root used the same wide data surface geometry at 1024px and 1920px so platform switching did not move content laterally", - "All primary tables at 375px and 1024px had scrollWidth equal to clientWidth with zero document overflow", - "Compact, intermediate, expanded, and full table layouts progressively exposed operator-priority columns from each table's measured container width", - "Proxmox overview compact tables showed Node, CPU, Mem, and Disk for hosts and Name, Up, CPU, Mem, and Disk for workloads", - "Proxmox storage pools and disks, replication, backup coverage and date views, Ceph, and mail tables retained the most useful operational fields at each width", - "Expanded Ceph cluster detail used independent responsive layouts for its narrower nested pool and service tables", - "Docker image and network, Kubernetes controller, autoscaling and event, TrueNAS system and topology, and VMware datastore and network tables restored supporting detail only when space allowed", - "The Docker Swarm mock empty state and all non-table platform views remained aligned and free of page overflow" + "All 34 primary platform routes loaded with the full mock inventory across Proxmox, Docker, Kubernetes, TrueNAS, VMware, and standalone machines", + "Every route was checked at ten viewport widths from 375px through 1920px for document overflow, table overflow, and clipped column headings", + "Compact tables preserved identity and immediate operating state before supporting metadata, while wider containers progressively restored secondary detail", + "Docker container columns responded to their measured table container, including the narrower assistant-open surface", + "Compact labels replaced long headings only where the full label would compete with operator-critical values", + "Nested Proxmox Ceph tables responded independently inside the expanded cluster drawer", + "All platform content roots retained aligned left edges and widths while switching between platforms" ], "interactions": [ - "Switched repeatedly between all six platform roots and confirmed identical content-shell left edges and widths", - "Navigated every primary platform route and switched Proxmox storage pools and disks plus backup coverage and date query views", - "Resized pages through 375px, 768px, 1024px, 1440px, and 1920px while inspecting visible headers, table bounds, and document bounds", - "Expanded Proxmox Ceph cluster detail and verified nested tables responded to their own containers instead of the browser viewport", - "Compared compact operator-priority columns with intermediate and full layouts across each platform's native tables", - "Visually reviewed representative screenshots for Proxmox overview, Docker images, Kubernetes workloads, TrueNAS overview, VMware overview and networks, including narrow and wide layouts" + "Navigated all 34 routes and exercised Proxmox storage and backup query variants", + "Resized every route through 375, 540, 640, 768, 900, 1024, 1280, 1440, 1680, and 1920px", + "Opened the Docker container compact lifecycle menu and confirmed Start, Stop, and Restart remain reachable within the viewport", + "Opened the assistant at 1280px and switched between Docker, Kubernetes, and VMware while checking container-relative layouts", + "Expanded Proxmox Ceph cluster detail and inspected its nested pool and service tables", + "Repeated strict zero-pixel clipping checks after the final Kubernetes autoscaling label adjustment" ] } diff --git a/frontend-modern/src/features/docker/DockerContainerLifecycleControls.tsx b/frontend-modern/src/features/docker/DockerContainerLifecycleControls.tsx index 9511a20d4..3631bde16 100644 --- a/frontend-modern/src/features/docker/DockerContainerLifecycleControls.tsx +++ b/frontend-modern/src/features/docker/DockerContainerLifecycleControls.tsx @@ -1,5 +1,16 @@ -import { For, Match, Switch, createSignal, type Component } from 'solid-js'; +import { + For, + Match, + Show, + Switch, + createEffect, + createSignal, + onCleanup, + type Component, +} from 'solid-js'; +import { Portal } from 'solid-js/web'; import Loader2Icon from 'lucide-solid/icons/loader-2'; +import MoreHorizontalIcon from 'lucide-solid/icons/more-horizontal'; import PlayIcon from 'lucide-solid/icons/play'; import RotateCwIcon from 'lucide-solid/icons/rotate-cw'; import SquareIcon from 'lucide-solid/icons/square'; @@ -26,6 +37,7 @@ export type DockerContainerLifecycleControlsProps = { resource: Resource; class?: string; surface?: DockerContainerLifecycleSurface; + collapsed?: boolean; onActionSettled?: (context: DockerContainerLifecycleSettledContext) => void | Promise; }; @@ -67,6 +79,38 @@ export const DockerContainerLifecycleControls: Component(null); const [reviewDetail, setReviewDetail] = createSignal(null); const [lastError, setLastError] = createSignal(''); + const [menuOpen, setMenuOpen] = createSignal(false); + const [menuPosition, setMenuPosition] = createSignal({ left: 0, top: 0 }); + let menuTriggerRef: HTMLButtonElement | undefined; + + const updateMenuPosition = () => { + if (!menuTriggerRef || typeof window === 'undefined') return; + const rect = menuTriggerRef.getBoundingClientRect(); + const menuWidth = 208; + const estimatedMenuHeight = 132; + setMenuPosition({ + left: Math.max(8, Math.min(window.innerWidth - menuWidth - 8, rect.right - menuWidth)), + top: Math.max(8, Math.min(window.innerHeight - estimatedMenuHeight - 8, rect.bottom + 4)), + }); + }; + + createEffect(() => { + if (!menuOpen() || typeof window === 'undefined') return; + updateMenuPosition(); + const closeOnOutsidePointer = (event: PointerEvent) => { + const target = event.target; + if (target instanceof Element && target.closest('[data-docker-lifecycle-menu-root]')) return; + setMenuOpen(false); + }; + window.addEventListener('resize', updateMenuPosition); + window.addEventListener('scroll', updateMenuPosition, true); + document.addEventListener('pointerdown', closeOnOutsidePointer); + onCleanup(() => { + window.removeEventListener('resize', updateMenuPosition); + window.removeEventListener('scroll', updateMenuPosition, true); + document.removeEventListener('pointerdown', closeOnOutsidePointer); + }); + }); const prepareLifecycleReview = async (action: DockerContainerLifecycleAction) => { const disabledReason = getDockerContainerLifecycleDisabledReason(props.resource, action); @@ -114,40 +158,121 @@ export const DockerContainerLifecycleControls: Component - - {(spec) => { - const disabled = () => - Boolean(getDockerContainerLifecycleDisabledReason(props.resource, spec.action)) || - (planningAction() !== null && planningAction() !== spec.action); - const Icon = iconForAction(spec.action); - return ( - + ); + }} + + } + > + + + + + + + + setReviewDetail(null)} diff --git a/frontend-modern/src/features/docker/DockerContainersTable.tsx b/frontend-modern/src/features/docker/DockerContainersTable.tsx index 33409c702..a0be9912a 100644 --- a/frontend-modern/src/features/docker/DockerContainersTable.tsx +++ b/frontend-modern/src/features/docker/DockerContainersTable.tsx @@ -18,8 +18,12 @@ import { ResourceNameWithWebInterfaceLink } from '@/components/shared/WebInterfa import { usePersistentSignal } from '@/hooks/usePersistentSignal'; import { getSimpleStatusIndicator } from '@/utils/status'; import { StackedMemoryBar } from '@/components/Workloads/StackedMemoryBar'; -import { getWorkloadTableLayoutMode } from '@/components/Workloads/guestRowModel'; +import { + getWorkloadTableLayoutMode, + getWorkloadTableLayoutModeForContainer, +} from '@/components/Workloads/guestRowModel'; import { useBreakpoint } from '@/hooks/useBreakpoint'; +import { useObservedElementWidth } from '@/hooks/useObservedElementWidth'; import { TableCell, TableRow } from '@/components/shared/Table'; import { asTrimmedString } from '@/utils/stringUtils'; import { buildMetricKeyForUnifiedResource } from '@/utils/metricsKeys'; @@ -29,6 +33,7 @@ import { DOCKER_QUERY_PARAMS } from '@/routing/resourceLinks'; import { PLATFORM_HEALTH_FILTER_OPTIONS, PlatformSortableTableHead, + PlatformResponsiveTableLabel, PlatformTableMetricFallback, PlatformTableEmptyState, PlatformTableToolbar, @@ -268,7 +273,13 @@ const DOCKER_CONTAINER_SEARCH_TIPS = { export const DockerContainersTable: Component = (props) => { const alertsActivation = useAlertsActivation(); const breakpoint = useBreakpoint(); - const layoutMode = createMemo(() => getWorkloadTableLayoutMode(breakpoint.width())); + const observedWidth = useObservedElementWidth(); + const layoutMode = createMemo(() => { + const measuredWidth = observedWidth.width(); + return measuredWidth === null + ? getWorkloadTableLayoutMode(breakpoint.width()) + : getWorkloadTableLayoutModeForContainer(measuredWidth); + }); // Search and status live in the URL, like the host scope below, so the // whole filter state is shareable and captured by saved views. That is what // makes exclusions persistent: search `-name`, save it as the default view, @@ -628,6 +639,7 @@ export const DockerContainersTable: Component = (pro @@ -708,7 +720,7 @@ export const DockerContainersTable: Component = (pro /> } > -
+
= (pro sort={sort} sortKey={getDockerContainerSortKey(column.id)} > - {column.label}}> - Mem - + {column.label}} + > + {column.label}}> + + + } + > + {(compactLabel) => ( + + )} + )} diff --git a/frontend-modern/src/features/docker/DockerImagesTable.tsx b/frontend-modern/src/features/docker/DockerImagesTable.tsx index 55ff84188..5d8eaea20 100644 --- a/frontend-modern/src/features/docker/DockerImagesTable.tsx +++ b/frontend-modern/src/features/docker/DockerImagesTable.tsx @@ -2,6 +2,7 @@ import { For, Show, createMemo, type Component } from 'solid-js'; import { TableCell, TableRow } from '@/components/shared/Table'; import { PLATFORM_HEALTH_FILTER_OPTIONS, + PlatformResponsiveTableLabel, PlatformSortableTableHead, PlatformTableEmptyState, PlatformTableToolbar, @@ -171,7 +172,7 @@ export const DockerImagesTable: Component< sortKey="update" class="md:w-[16%]" > - Update check + } diff --git a/frontend-modern/src/features/docker/DockerNetworksTable.tsx b/frontend-modern/src/features/docker/DockerNetworksTable.tsx index 828765587..7b5fd93a0 100644 --- a/frontend-modern/src/features/docker/DockerNetworksTable.tsx +++ b/frontend-modern/src/features/docker/DockerNetworksTable.tsx @@ -4,6 +4,7 @@ import { StatusDot } from '@/components/shared/StatusDot'; import { TableCell, TableHead, TableRow } from '@/components/shared/Table'; import { PLATFORM_HEALTH_FILTER_OPTIONS, + PlatformResponsiveTableLabel, type PlatformTableFilterOption, PlatformTableEmptyState, PlatformTableToolbar, @@ -415,7 +416,7 @@ export const DockerNetworksTable: Component = (props) <> Network - Attached workloads + Attention diff --git a/frontend-modern/src/features/docker/__tests__/DockerNativeTables.test.tsx b/frontend-modern/src/features/docker/__tests__/DockerNativeTables.test.tsx index 28db48ab2..05615f5ee 100644 --- a/frontend-modern/src/features/docker/__tests__/DockerNativeTables.test.tsx +++ b/frontend-modern/src/features/docker/__tests__/DockerNativeTables.test.tsx @@ -248,7 +248,7 @@ describe('Docker native tables', () => { )); expect(screen.getByText('Container')).toBeInTheDocument(); - expect(screen.getByText('Runtime')).toBeInTheDocument(); + expect(screen.getByText('Engine')).toBeInTheDocument(); expect(screen.getByText('CPU')).toBeInTheDocument(); expect(screen.getByText('Memory')).toBeInTheDocument(); expect(screen.getByText('Restarts')).toBeInTheDocument(); @@ -281,7 +281,7 @@ describe('Docker native tables', () => { ); }); - it('hides the Runtime column when every container reports the same runtime', () => { + it('hides the Engine column when every container reports the same runtime', () => { renderInRouter(() => ( { /> )); - expect(screen.queryByText('Runtime')).not.toBeInTheDocument(); + expect(screen.queryByText('Engine')).not.toBeInTheDocument(); expect(screen.queryByText('docker 27.5.1')).not.toBeInTheDocument(); }); @@ -868,7 +868,12 @@ describe('Docker native tables', () => { /> )); - const restartButton = screen.getByRole('button', { + fireEvent.click( + screen.getByRole('button', { + name: 'Container actions for edge-web', + }), + ); + const restartButton = screen.getByRole('menuitem', { name: 'Review restart for edge-web', }); fireEvent.click(restartButton); @@ -916,8 +921,13 @@ describe('Docker native tables', () => { /> )); - expect( + fireEvent.click( screen.getByRole('button', { + name: 'Container actions for edge-web', + }), + ); + expect( + screen.getByRole('menuitem', { name: 'Restart unavailable: Docker inventory is stale; refresh inventory before running lifecycle actions.', }), ).toBeDisabled(); diff --git a/frontend-modern/src/features/docker/__tests__/dockerContainerTableModel.test.ts b/frontend-modern/src/features/docker/__tests__/dockerContainerTableModel.test.ts index 4f5982cbb..08314aaa0 100644 --- a/frontend-modern/src/features/docker/__tests__/dockerContainerTableModel.test.ts +++ b/frontend-modern/src/features/docker/__tests__/dockerContainerTableModel.test.ts @@ -14,10 +14,10 @@ describe('dockerContainerTableModel', () => { expect(ids).toEqual(['container', 'state', 'cpu', 'memory', 'updates', 'actions']); expect(getDockerContainerTableMinWidthClass()).toBe('min-w-full'); expect(getDockerContainerColumnWidthStyle('container', 'mobile', ids)).toEqual({ - width: '28.0702%', + width: '26.3158%', }); expect(getDockerContainerColumnWidthStyle('memory', 'mobile', ids)).toEqual({ - width: '19.2982%', + width: '18.4211%', }); }); diff --git a/frontend-modern/src/features/docker/dockerContainerTableModel.ts b/frontend-modern/src/features/docker/dockerContainerTableModel.ts index 42ec45851..24a609587 100644 --- a/frontend-modern/src/features/docker/dockerContainerTableModel.ts +++ b/frontend-modern/src/features/docker/dockerContainerTableModel.ts @@ -22,6 +22,7 @@ export type DockerContainerTableColumnId = export type DockerContainerTableColumn = { id: DockerContainerTableColumnId; label: string; + compactLabel?: string; kind: PlatformTableColumnKind; }; @@ -77,7 +78,7 @@ const DOCKER_CONTAINER_COLUMN_MIN_LAYOUT: Record< const DOCKER_CONTAINER_COLUMNS: DockerContainerTableColumn[] = [ { id: 'container', label: 'Container', kind: 'name' }, { id: 'host', label: 'Host', kind: 'text' }, - { id: 'runtime', label: 'Runtime', kind: 'text' }, + { id: 'runtime', label: 'Engine', kind: 'text' }, { id: 'image', label: 'Image', kind: 'text' }, { id: 'state', label: 'State', kind: 'text' }, { id: 'cpu', label: 'CPU', kind: 'metric-bar' }, @@ -86,7 +87,7 @@ const DOCKER_CONTAINER_COLUMNS: DockerContainerTableColumn[] = [ { id: 'ports', label: 'Ports', kind: 'text' }, { id: 'networks', label: 'Networks', kind: 'text' }, { id: 'mounts', label: 'Mounts', kind: 'text' }, - { id: 'updates', label: 'Updates', kind: 'badge' }, + { id: 'updates', label: 'Updates', compactLabel: 'Update', kind: 'badge' }, { id: 'actions', label: 'Actions', kind: 'badge' }, ]; @@ -111,34 +112,34 @@ const DOCKER_CONTAINER_RESPONSIVE_WIDTHS: Record< Partial> > = { mobile: { - container: 32, + container: 30, state: 14, cpu: 18, - memory: 22, - updates: 14, + memory: 21, + updates: 17, actions: 14, }, tablet: { - container: 28, - host: 16, + container: 27, + host: 15, state: 10, cpu: 15, - memory: 18, - restarts: 6, - updates: 7, - actions: 8, + memory: 19, + restarts: 9, + updates: 9, + actions: 6, }, compact: { container: 18, host: 10, runtime: 8, - image: 20, + image: 18, state: 7, cpu: 11, memory: 12, - restarts: 7, + restarts: 9, ports: 12, - updates: 8, + updates: 9, actions: 8, }, }; diff --git a/frontend-modern/src/features/kubernetes/KubernetesAutoscalingTable.tsx b/frontend-modern/src/features/kubernetes/KubernetesAutoscalingTable.tsx index 82b3a940a..211e47953 100644 --- a/frontend-modern/src/features/kubernetes/KubernetesAutoscalingTable.tsx +++ b/frontend-modern/src/features/kubernetes/KubernetesAutoscalingTable.tsx @@ -5,6 +5,7 @@ import { getSimpleStatusIndicator } from '@/utils/status'; import { asTrimmedString } from '@/utils/stringUtils'; import { PLATFORM_HEALTH_FILTER_OPTIONS, + PlatformResponsiveTableLabel, PlatformSortableTableHead, PlatformTableEmptyState, PlatformTableNumberValue, @@ -181,9 +182,9 @@ export const KubernetesAutoscalingTable: Component<{ kind="text" sort={sort} sortKey="target" - class="md:w-[18%]" + class="platform-table-mobile-w-30 md:w-[18%]" > - Scale target + Target - Current + - Desired + - Lifecycle / trust + - Data shape + Controller - + Kind - Ready/Done + - Exceptions + - Type / class + - Policy shape + - Spec / limits + - Binding / phase + { )); expect(screen.getByText('Autoscaler')).toBeInTheDocument(); - expect(screen.getByText('Scale target')).toBeInTheDocument(); + expect(screen.getByText('Target')).toBeInTheDocument(); expect(screen.getByText('Bounds')).toBeInTheDocument(); expect(screen.getByText('Metrics')).toBeInTheDocument(); expect(screen.getByText('checkout-api-hpa')).toBeInTheDocument(); diff --git a/frontend-modern/src/features/platformPage/__tests__/sharedPlatformPage.test.ts b/frontend-modern/src/features/platformPage/__tests__/sharedPlatformPage.test.ts index 6c8203051..cfa652b64 100644 --- a/frontend-modern/src/features/platformPage/__tests__/sharedPlatformPage.test.ts +++ b/frontend-modern/src/features/platformPage/__tests__/sharedPlatformPage.test.ts @@ -28,6 +28,7 @@ import { getPlatformTableFiniteMetric, getPlatformTableClass, getPlatformTableContainerLayout, + getPlatformTableHeadClassForKind, getPlatformResourceCountNoun, getPlatformTableResponsiveMinWidthClass, getPlatformTableWeightedColumnWidthStyle, @@ -87,6 +88,13 @@ describe('getPlatformTableContainerLayout', () => { }); }); +describe('getPlatformTableHeadClassForKind', () => { + it('marks semantic identity columns for compact container prioritization', () => { + expect(getPlatformTableHeadClassForKind('name')).toContain('platform-table-name-column'); + expect(getPlatformTableHeadClassForKind('text')).not.toContain('platform-table-name-column'); + }); +}); + describe('normalizePlatformResourceStatusFilter', () => { it('normalizes route-specific legacy status vocabulary', () => { expect(normalizePlatformResourceStatusFilter('running')).toBe('online'); diff --git a/frontend-modern/src/features/platformPage/sharedPlatformPage.tsx b/frontend-modern/src/features/platformPage/sharedPlatformPage.tsx index 2500af735..2a4806ca4 100644 --- a/frontend-modern/src/features/platformPage/sharedPlatformPage.tsx +++ b/frontend-modern/src/features/platformPage/sharedPlatformPage.tsx @@ -196,11 +196,22 @@ export const getPlatformTableCellClass = (align?: PlatformTableCellAlign): strin // added once in columnAlignment.ts and propagated automatically). See // PlatformTableColumnKind for the kind list and rationale. export const getPlatformTableHeadClassForKind = (kind: PlatformTableColumnKind): string => - getPlatformTableHeadClass(getPlatformColumnAlign(kind)); + `${getPlatformTableHeadClass(getPlatformColumnAlign(kind))} ${ + kind === 'name' ? 'platform-table-name-column' : '' + }`.trim(); export const getPlatformTableCellClassForKind = (kind: PlatformTableColumnKind): string => getPlatformTableCellClass(getPlatformColumnAlign(kind)); +export function PlatformResponsiveTableLabel(props: { compact: string; full: string }) { + return ( + <> + {props.compact} + {props.full} + + ); +} + // --- User-controlled column sorting ---------------------------------------- // // Platform tables keep their built-in attention-first ordering until the user diff --git a/frontend-modern/src/features/proxmox/ProxmoxCephClusterDrawer.tsx b/frontend-modern/src/features/proxmox/ProxmoxCephClusterDrawer.tsx index 3104e830c..7615bf9e4 100644 --- a/frontend-modern/src/features/proxmox/ProxmoxCephClusterDrawer.tsx +++ b/frontend-modern/src/features/proxmox/ProxmoxCephClusterDrawer.tsx @@ -19,6 +19,7 @@ import { formatPlatformTablePercentValue, getPlatformTableCellClassForKind, getPlatformTableHeadClassForKind, + PlatformResponsiveTableLabel, PlatformTableNumberValue, } from '@/features/platformPage/sharedPlatformPage'; import type { StatusIndicatorVariant } from '@/utils/status'; @@ -238,7 +239,7 @@ export const ProxmoxCephClusterDrawer: Component<{ Service Status - Running + Total diff --git a/frontend-modern/src/features/proxmox/ProxmoxReplicationTable.tsx b/frontend-modern/src/features/proxmox/ProxmoxReplicationTable.tsx index 4eb6ac974..4390b9af5 100644 --- a/frontend-modern/src/features/proxmox/ProxmoxReplicationTable.tsx +++ b/frontend-modern/src/features/proxmox/ProxmoxReplicationTable.tsx @@ -260,8 +260,11 @@ export const ProxmoxReplicationTable: Component<{ Job Guest - - Source → Target + + Route diff --git a/frontend-modern/src/features/truenas/TrueNASProtectionTable.tsx b/frontend-modern/src/features/truenas/TrueNASProtectionTable.tsx index f8f5d0a90..b11490fad 100644 --- a/frontend-modern/src/features/truenas/TrueNASProtectionTable.tsx +++ b/frontend-modern/src/features/truenas/TrueNASProtectionTable.tsx @@ -13,6 +13,7 @@ import { asTrimmedString } from '@/utils/stringUtils'; import type { StatusIndicatorVariant } from '@/utils/status'; import { PlatformErrorState, + PlatformResponsiveTableLabel, PlatformSortableTableHead, PlatformTableDateTimeValue, PlatformTableEmptyState, @@ -500,7 +501,7 @@ export const TrueNASProtectionTable: Component<{ sortKey="completed" class="md:w-[12%]" > - Completed + - Usage / Size + { it('sorts Usage / Size descending first with missing values last in each subtree', () => { const { container } = renderTable(); - fireEvent.click(headerFor('Usage / Size')); - expect(headerFor('Usage / Size')).toHaveAttribute('aria-sort', 'descending'); + fireEvent.click(headerFor('Usage')); + expect(headerFor('Usage')).toHaveAttribute('aria-sort', 'descending'); // Pools order on their own usage (zulu 70 > alpha 50); alpha's datasets // order 80 > 10 with the metric-less bravo sinking to the bottom of its // sibling group. diff --git a/frontend-modern/src/features/vmware/VsphereHostsTable.tsx b/frontend-modern/src/features/vmware/VsphereHostsTable.tsx index ee525d2e0..2e68ce5a6 100644 --- a/frontend-modern/src/features/vmware/VsphereHostsTable.tsx +++ b/frontend-modern/src/features/vmware/VsphereHostsTable.tsx @@ -209,7 +209,7 @@ export const VsphereHostsTable: Component<{ kind="name" sort={sort} sortKey="host" - class="md:w-[16%]" + class="md:w-[15%]" > Host @@ -265,7 +265,7 @@ export const VsphereHostsTable: Component<{ kind="numeric-value" sort={sort} sortKey="datastores" - class="hidden md:table-cell md:w-[7%]" + class="hidden md:table-cell md:w-[8%]" > Datastores diff --git a/frontend-modern/src/index.css b/frontend-modern/src/index.css index b63353f1e..be601b044 100644 --- a/frontend-modern/src/index.css +++ b/frontend-modern/src/index.css @@ -728,6 +728,71 @@ body, white-space: nowrap; } +/* + * Dense platform tables should spend their width on operational values, not + * oversized heading chrome. Keep headings readable without making every row + * taller, then let unusually long labels provide a container-aware compact + * form below the first table-column reveal breakpoint. + */ +.table-scroll-shell > .table-fixed th { + padding-right: 0.375rem; + padding-left: 0.375rem; + font-size: 11px; + letter-spacing: 0.025em; +} + +.platform-table-label-compact { + display: inline; +} + +.platform-table-label-full { + display: none; +} + +@container (min-width: 34rem) { + .platform-table-label-compact { + display: none; + } + + .platform-table-label-full { + display: inline; + } +} + +@container (max-width: 33.999rem) { + .table-scroll-shell > .table-fixed th { + padding-right: 0.25rem; + padding-left: 0.25rem; + font-size: 10px; + letter-spacing: 0; + } + + /* + * On a phone the expand control and health dot otherwise consume nearly the + * whole resource cell. Identity is the user's anchor, so semantic `name` + * columns receive the largest share before secondary values are divided. + */ + .table-scroll-shell > .table-fixed th.platform-table-name-column { + width: 40%; + } + + .table-scroll-shell > .table-fixed th.platform-table-mobile-w-30 { + width: 30%; + } + + .table-scroll-shell > .table-fixed th.platform-table-mobile-w-25 { + width: 25%; + } + + .table-scroll-shell > .table-fixed th.platform-table-mobile-w-20 { + width: 20%; + } + + .table-scroll-shell > .table-fixed th.platform-table-mobile-w-15 { + width: 15%; + } +} + /* Keep name column single line to avoid wrapping while giving it more space */ .table-fixed td:first-child { white-space: nowrap;