mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-11 14:00:29 +00:00
fix(ui): refine compact platform tables
Contract-Neutral: Responsive table presentation and compact action-menu behavior only; no canonical API, data model, or product contract changes.
This commit is contained in:
@@ -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"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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<void>;
|
||||
};
|
||||
|
||||
@@ -67,6 +79,38 @@ export const DockerContainerLifecycleControls: Component<DockerContainerLifecycl
|
||||
const [reviewAction, setReviewAction] = createSignal<DockerContainerLifecycleAction | null>(null);
|
||||
const [reviewDetail, setReviewDetail] = createSignal<ActionDetailResponse | null>(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<DockerContainerLifecycl
|
||||
class={`inline-flex items-center justify-end gap-1 ${props.class ?? ''}`.trim()}
|
||||
data-prevent-toggle
|
||||
data-docker-container-actions-surface={props.surface ?? 'docker-page'}
|
||||
data-docker-lifecycle-menu-root
|
||||
>
|
||||
<For each={DOCKER_CONTAINER_LIFECYCLE_ACTIONS}>
|
||||
{(spec) => {
|
||||
const disabled = () =>
|
||||
Boolean(getDockerContainerLifecycleDisabledReason(props.resource, spec.action)) ||
|
||||
(planningAction() !== null && planningAction() !== spec.action);
|
||||
const Icon = iconForAction(spec.action);
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
class={`${buttonBaseClass} ${planningAction() === spec.action ? runningButtonClass : disabled() ? disabledButtonClass : enabledButtonClass}`}
|
||||
disabled={disabled()}
|
||||
title={titleForAction(spec.action, spec.label)}
|
||||
aria-label={titleForAction(spec.action, spec.label)}
|
||||
data-docker-container-action={spec.action}
|
||||
<Show
|
||||
when={props.collapsed}
|
||||
fallback={
|
||||
<For each={DOCKER_CONTAINER_LIFECYCLE_ACTIONS}>
|
||||
{(spec) => {
|
||||
const disabled = () =>
|
||||
Boolean(getDockerContainerLifecycleDisabledReason(props.resource, spec.action)) ||
|
||||
(planningAction() !== null && planningAction() !== spec.action);
|
||||
const Icon = iconForAction(spec.action);
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
class={`${buttonBaseClass} ${planningAction() === spec.action ? runningButtonClass : disabled() ? disabledButtonClass : enabledButtonClass}`}
|
||||
disabled={disabled()}
|
||||
title={titleForAction(spec.action, spec.label)}
|
||||
aria-label={titleForAction(spec.action, spec.label)}
|
||||
data-docker-container-action={spec.action}
|
||||
onMouseDown={(event) => event.stopPropagation()}
|
||||
onKeyDown={(event) => event.stopPropagation()}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
void prepareLifecycleReview(spec.action);
|
||||
}}
|
||||
>
|
||||
<Switch>
|
||||
<Match when={planningAction() === spec.action}>
|
||||
<Loader2Icon class="h-3.5 w-3.5 animate-spin" aria-hidden="true" />
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<Icon class="h-3.5 w-3.5" aria-hidden="true" />
|
||||
</Match>
|
||||
</Switch>
|
||||
</button>
|
||||
);
|
||||
}}
|
||||
</For>
|
||||
}
|
||||
>
|
||||
<button
|
||||
ref={menuTriggerRef}
|
||||
type="button"
|
||||
class={`inline-flex h-7 w-7 shrink-0 items-center justify-center rounded border ${enabledButtonClass}`}
|
||||
title={`Container actions for ${dockerContainerLifecycleName(props.resource)}`}
|
||||
aria-label={`Container actions for ${dockerContainerLifecycleName(props.resource)}`}
|
||||
aria-haspopup="menu"
|
||||
aria-expanded={menuOpen() ? 'true' : 'false'}
|
||||
onMouseDown={(event) => event.stopPropagation()}
|
||||
onKeyDown={(event) => {
|
||||
event.stopPropagation();
|
||||
if (event.key === 'Escape') setMenuOpen(false);
|
||||
}}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
updateMenuPosition();
|
||||
setMenuOpen((open) => !open);
|
||||
}}
|
||||
>
|
||||
<MoreHorizontalIcon class="h-4 w-4" aria-hidden="true" />
|
||||
</button>
|
||||
|
||||
<Show when={menuOpen()}>
|
||||
<Portal mount={document.body}>
|
||||
<div
|
||||
data-prevent-toggle
|
||||
data-docker-lifecycle-menu-root
|
||||
role="menu"
|
||||
class="fixed z-[9999] w-52 rounded-md border border-border bg-surface p-1 text-left shadow-lg"
|
||||
style={{ left: `${menuPosition().left}px`, top: `${menuPosition().top}px` }}
|
||||
onMouseDown={(event) => event.stopPropagation()}
|
||||
onKeyDown={(event) => event.stopPropagation()}
|
||||
onClick={(event) => {
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
onKeyDown={(event) => {
|
||||
event.stopPropagation();
|
||||
void prepareLifecycleReview(spec.action);
|
||||
if (event.key === 'Escape') setMenuOpen(false);
|
||||
}}
|
||||
>
|
||||
<Switch>
|
||||
<Match when={planningAction() === spec.action}>
|
||||
<Loader2Icon class="h-3.5 w-3.5 animate-spin" aria-hidden="true" />
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<Icon class="h-3.5 w-3.5" aria-hidden="true" />
|
||||
</Match>
|
||||
</Switch>
|
||||
</button>
|
||||
);
|
||||
}}
|
||||
</For>
|
||||
<For each={DOCKER_CONTAINER_LIFECYCLE_ACTIONS}>
|
||||
{(spec) => {
|
||||
const disabled = () =>
|
||||
Boolean(
|
||||
getDockerContainerLifecycleDisabledReason(props.resource, spec.action),
|
||||
) ||
|
||||
(planningAction() !== null && planningAction() !== spec.action);
|
||||
const Icon = iconForAction(spec.action);
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
role="menuitem"
|
||||
class="flex w-full items-center gap-2 rounded px-2 py-2 text-left 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/60 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
disabled={disabled()}
|
||||
title={titleForAction(spec.action, spec.label)}
|
||||
aria-label={titleForAction(spec.action, spec.label)}
|
||||
data-docker-container-action={spec.action}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
setMenuOpen(false);
|
||||
void prepareLifecycleReview(spec.action);
|
||||
}}
|
||||
>
|
||||
<Show
|
||||
when={planningAction() === spec.action}
|
||||
fallback={<Icon class="h-3.5 w-3.5" aria-hidden="true" />}
|
||||
>
|
||||
<Loader2Icon class="h-3.5 w-3.5 animate-spin" aria-hidden="true" />
|
||||
</Show>
|
||||
<span>{spec.label}</span>
|
||||
</button>
|
||||
);
|
||||
}}
|
||||
</For>
|
||||
</div>
|
||||
</Portal>
|
||||
</Show>
|
||||
</Show>
|
||||
<ActionReviewDialog
|
||||
detail={reviewDetail()}
|
||||
onClose={() => setReviewDetail(null)}
|
||||
|
||||
@@ -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<DockerContainersTableProps> = (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<DockerContainersTableProps> = (pro
|
||||
<TableCell class={getPlatformTableCellClassForKind(column.kind)}>
|
||||
<DockerContainerLifecycleControls
|
||||
resource={resource}
|
||||
collapsed={layoutMode() !== 'wide'}
|
||||
onActionSettled={props.onLifecycleActionSettled}
|
||||
/>
|
||||
</TableCell>
|
||||
@@ -708,7 +720,7 @@ export const DockerContainersTable: Component<DockerContainersTableProps> = (pro
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div class="space-y-3">
|
||||
<div ref={observedWidth.setElement} class="space-y-3">
|
||||
<Show when={props.showToolbar !== false}>
|
||||
<PlatformTableToolbar
|
||||
search={tableState.search}
|
||||
@@ -778,9 +790,25 @@ export const DockerContainersTable: Component<DockerContainersTableProps> = (pro
|
||||
sort={sort}
|
||||
sortKey={getDockerContainerSortKey(column.id)}
|
||||
>
|
||||
<Show when={column.id === 'memory'} fallback={<>{column.label}</>}>
|
||||
<span class="md:hidden">Mem</span>
|
||||
<span class="hidden md:inline">Memory</span>
|
||||
<Show
|
||||
when={column.id !== 'actions'}
|
||||
fallback={<span class="sr-only">{column.label}</span>}
|
||||
>
|
||||
<Show
|
||||
when={column.compactLabel}
|
||||
fallback={
|
||||
<Show when={column.id === 'memory'} fallback={<>{column.label}</>}>
|
||||
<PlatformResponsiveTableLabel compact="Mem" full="Memory" />
|
||||
</Show>
|
||||
}
|
||||
>
|
||||
{(compactLabel) => (
|
||||
<PlatformResponsiveTableLabel
|
||||
compact={compactLabel()}
|
||||
full={column.label}
|
||||
/>
|
||||
)}
|
||||
</Show>
|
||||
</Show>
|
||||
</PlatformSortableTableHead>
|
||||
)}
|
||||
|
||||
@@ -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
|
||||
<PlatformResponsiveTableLabel compact="Update" full="Update check" />
|
||||
</PlatformSortableTableHead>
|
||||
</>
|
||||
}
|
||||
|
||||
@@ -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<DockerNetworksTableProps> = (props)
|
||||
<>
|
||||
<TableHead class={getPlatformTableHeadClassForKind('name')}>Network</TableHead>
|
||||
<TableHead class={getPlatformTableHeadClassForKind('text')}>
|
||||
Attached workloads
|
||||
<PlatformResponsiveTableLabel compact="Attached" full="Attached workloads" />
|
||||
</TableHead>
|
||||
<TableHead class={getPlatformTableHeadClassForKind('text')}>Attention</TableHead>
|
||||
<Show when={showSubnet()}>
|
||||
|
||||
@@ -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(() => (
|
||||
<DockerContainersTable
|
||||
resources={[
|
||||
@@ -319,7 +319,7 @@ describe('Docker native tables', () => {
|
||||
/>
|
||||
));
|
||||
|
||||
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();
|
||||
|
||||
@@ -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%',
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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<Record<DockerContainerTableColumnId, number>>
|
||||
> = {
|
||||
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,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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
|
||||
</PlatformSortableTableHead>
|
||||
<PlatformSortableTableHead
|
||||
kind="text"
|
||||
@@ -196,17 +197,17 @@ export const KubernetesAutoscalingTable: Component<{
|
||||
kind="numeric-value"
|
||||
sort={sort}
|
||||
sortKey="current"
|
||||
class="md:w-[9%]"
|
||||
class="platform-table-mobile-w-15 md:w-[9%]"
|
||||
>
|
||||
Current
|
||||
<PlatformResponsiveTableLabel compact="Now" full="Current" />
|
||||
</PlatformSortableTableHead>
|
||||
<PlatformSortableTableHead
|
||||
kind="numeric-value"
|
||||
sort={sort}
|
||||
sortKey="desired"
|
||||
class="md:w-[9%]"
|
||||
class="platform-table-mobile-w-15 md:w-[9%]"
|
||||
>
|
||||
Desired
|
||||
<PlatformResponsiveTableLabel compact="Goal" full="Desired" />
|
||||
</PlatformSortableTableHead>
|
||||
<PlatformSortableTableHead
|
||||
kind="text"
|
||||
|
||||
@@ -5,6 +5,7 @@ import { getSimpleStatusIndicator } from '@/utils/status';
|
||||
import { asTrimmedString } from '@/utils/stringUtils';
|
||||
import {
|
||||
PLATFORM_HEALTH_FILTER_OPTIONS,
|
||||
PlatformResponsiveTableLabel,
|
||||
PlatformSortableTableHead,
|
||||
PlatformTableEmptyState,
|
||||
PlatformTableToolbar,
|
||||
@@ -283,10 +284,10 @@ export const KubernetesConfigTable: Component<{
|
||||
sortKey="lifecycle"
|
||||
class="md:w-[16%]"
|
||||
>
|
||||
Lifecycle / trust
|
||||
<PlatformResponsiveTableLabel compact="Status" full="Lifecycle / trust" />
|
||||
</PlatformSortableTableHead>
|
||||
<PlatformSortableTableHead kind="text" sort={sort} class="md:w-[14%]">
|
||||
Data shape
|
||||
<PlatformResponsiveTableLabel compact="Data" full="Data shape" />
|
||||
</PlatformSortableTableHead>
|
||||
<PlatformSortableTableHead
|
||||
kind="text"
|
||||
|
||||
@@ -5,6 +5,7 @@ import { getResourceTypeLabel } from '@/utils/resourceTypePresentation';
|
||||
import { asTrimmedString } from '@/utils/stringUtils';
|
||||
import {
|
||||
PLATFORM_HEALTH_FILTER_OPTIONS,
|
||||
PlatformResponsiveTableLabel,
|
||||
PlatformSortableTableHead,
|
||||
PlatformTableEmptyState,
|
||||
PlatformTableNumberValue,
|
||||
@@ -280,7 +281,12 @@ export const KubernetesControllersTable: Component<{
|
||||
>
|
||||
Controller
|
||||
</PlatformSortableTableHead>
|
||||
<PlatformSortableTableHead kind="text" sort={sort} sortKey="kind" class="md:w-[9%]">
|
||||
<PlatformSortableTableHead
|
||||
kind="text"
|
||||
sort={sort}
|
||||
sortKey="kind"
|
||||
class="platform-table-mobile-w-25 md:w-[9%]"
|
||||
>
|
||||
Kind
|
||||
</PlatformSortableTableHead>
|
||||
<PlatformSortableTableHead
|
||||
@@ -311,9 +317,9 @@ export const KubernetesControllersTable: Component<{
|
||||
kind="numeric-value"
|
||||
sort={sort}
|
||||
sortKey="ready"
|
||||
class="md:w-[10%]"
|
||||
class="platform-table-mobile-w-15 md:w-[10%]"
|
||||
>
|
||||
Ready/Done
|
||||
<PlatformResponsiveTableLabel compact="Ready" full="Ready/Done" />
|
||||
</PlatformSortableTableHead>
|
||||
<PlatformSortableTableHead
|
||||
kind="numeric-value"
|
||||
@@ -327,9 +333,9 @@ export const KubernetesControllersTable: Component<{
|
||||
kind="text"
|
||||
sort={sort}
|
||||
sortKey="exceptions"
|
||||
class="md:w-[11%]"
|
||||
class="platform-table-mobile-w-20 md:w-[11%]"
|
||||
>
|
||||
Exceptions
|
||||
<PlatformResponsiveTableLabel compact="Issues" full="Exceptions" />
|
||||
</PlatformSortableTableHead>
|
||||
<PlatformSortableTableHead
|
||||
kind="text"
|
||||
|
||||
@@ -5,6 +5,7 @@ import { getSimpleStatusIndicator } from '@/utils/status';
|
||||
import { asTrimmedString } from '@/utils/stringUtils';
|
||||
import {
|
||||
PLATFORM_HEALTH_FILTER_OPTIONS,
|
||||
PlatformResponsiveTableLabel,
|
||||
PlatformSortableTableHead,
|
||||
PlatformTableEmptyState,
|
||||
PlatformTableToolbar,
|
||||
@@ -227,7 +228,7 @@ export const KubernetesNetworkingTable: Component<{
|
||||
sortKey="typeClass"
|
||||
class="md:w-[12%]"
|
||||
>
|
||||
Type / class
|
||||
<PlatformResponsiveTableLabel compact="Class" full="Type / class" />
|
||||
</PlatformSortableTableHead>
|
||||
<PlatformSortableTableHead
|
||||
kind="text"
|
||||
|
||||
@@ -5,6 +5,7 @@ import { getSimpleStatusIndicator } from '@/utils/status';
|
||||
import { asTrimmedString } from '@/utils/stringUtils';
|
||||
import {
|
||||
PLATFORM_HEALTH_FILTER_OPTIONS,
|
||||
PlatformResponsiveTableLabel,
|
||||
PlatformTableEmptyState,
|
||||
PlatformTableToolbar,
|
||||
createPlatformTableFilterState,
|
||||
@@ -250,10 +251,10 @@ export const KubernetesPolicyTable: Component<{
|
||||
Scope
|
||||
</TableHead>
|
||||
<TableHead class={`${getPlatformTableHeadClassForKind('text')} md:w-[15%]`}>
|
||||
Policy shape
|
||||
<PlatformResponsiveTableLabel compact="Policy" full="Policy shape" />
|
||||
</TableHead>
|
||||
<TableHead class={`${getPlatformTableHeadClassForKind('text')} md:w-[16%]`}>
|
||||
Spec / limits
|
||||
<PlatformResponsiveTableLabel compact="Limits" full="Spec / limits" />
|
||||
</TableHead>
|
||||
<TableHead
|
||||
class={`${getPlatformTableHeadClassForKind('text')} hidden md:table-cell md:w-[14%]`}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { getSimpleStatusIndicator } from '@/utils/status';
|
||||
import { asTrimmedString } from '@/utils/stringUtils';
|
||||
import {
|
||||
PLATFORM_HEALTH_FILTER_OPTIONS,
|
||||
PlatformResponsiveTableLabel,
|
||||
PlatformSortableTableHead,
|
||||
PlatformTableEmptyState,
|
||||
PlatformTableToolbar,
|
||||
@@ -185,7 +186,7 @@ export const KubernetesServicesTable: Component<{
|
||||
sortKey="clusterIp"
|
||||
class="md:w-[13%]"
|
||||
>
|
||||
Cluster IP
|
||||
<PlatformResponsiveTableLabel compact="IP" full="Cluster IP" />
|
||||
</PlatformSortableTableHead>
|
||||
<PlatformSortableTableHead
|
||||
kind="text"
|
||||
|
||||
@@ -5,6 +5,7 @@ import { getSimpleStatusIndicator } from '@/utils/status';
|
||||
import { asTrimmedString } from '@/utils/stringUtils';
|
||||
import {
|
||||
PLATFORM_HEALTH_FILTER_OPTIONS,
|
||||
PlatformResponsiveTableLabel,
|
||||
PlatformSortableTableHead,
|
||||
PlatformTableEmptyState,
|
||||
PlatformTableToolbar,
|
||||
@@ -297,7 +298,7 @@ export const KubernetesStorageTable: Component<{
|
||||
sortKey="phase"
|
||||
class="md:w-[14%]"
|
||||
>
|
||||
Binding / phase
|
||||
<PlatformResponsiveTableLabel compact="State" full="Binding / phase" />
|
||||
</PlatformSortableTableHead>
|
||||
<PlatformSortableTableHead
|
||||
kind="text"
|
||||
|
||||
+1
-1
@@ -57,7 +57,7 @@ describe('KubernetesAutoscalingTable', () => {
|
||||
));
|
||||
|
||||
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();
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
<span class="platform-table-label-compact">{props.compact}</span>
|
||||
<span class="platform-table-label-full">{props.full}</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// --- User-controlled column sorting ----------------------------------------
|
||||
//
|
||||
// Platform tables keep their built-in attention-first ordering until the user
|
||||
|
||||
@@ -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<{
|
||||
<TableHead class={getPlatformTableHeadClassForKind('name')}>Service</TableHead>
|
||||
<TableHead class={getPlatformTableHeadClassForKind('text')}>Status</TableHead>
|
||||
<TableHead class={getPlatformTableHeadClassForKind('numeric-value')}>
|
||||
Running
|
||||
<PlatformResponsiveTableLabel compact="Up" full="Running" />
|
||||
</TableHead>
|
||||
<TableHead class={getPlatformTableHeadClassForKind('numeric-value')}>
|
||||
Total
|
||||
|
||||
@@ -260,8 +260,11 @@ export const ProxmoxReplicationTable: Component<{
|
||||
<TableHead class={getPlatformTableHeadClassForKind('text')}>Job</TableHead>
|
||||
</Show>
|
||||
<TableHead class={getPlatformTableHeadClassForKind('name')}>Guest</TableHead>
|
||||
<TableHead class={getPlatformTableHeadClassForKind('text')}>
|
||||
Source → Target
|
||||
<TableHead
|
||||
class={getPlatformTableHeadClassForKind('text')}
|
||||
title="Source → target"
|
||||
>
|
||||
Route
|
||||
</TableHead>
|
||||
<Show when={showOperational()}>
|
||||
<TableHead class={getPlatformTableHeadClassForKind('text')}>
|
||||
|
||||
@@ -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
|
||||
<PlatformResponsiveTableLabel compact="Finished" full="Completed" />
|
||||
</PlatformSortableTableHead>
|
||||
<PlatformSortableTableHead
|
||||
kind="numeric-value"
|
||||
|
||||
@@ -8,6 +8,7 @@ import { asTrimmedString } from '@/utils/stringUtils';
|
||||
import { buildMetricKeyForUnifiedResource } from '@/utils/metricsKeys';
|
||||
import {
|
||||
PlatformSortableTableHead,
|
||||
PlatformResponsiveTableLabel,
|
||||
PlatformTableEmptyState,
|
||||
PlatformTableNumberValue,
|
||||
PlatformTableTemperatureValue,
|
||||
@@ -359,7 +360,7 @@ export const TrueNASStorageTopologyTable: Component<{
|
||||
sortKey="usage"
|
||||
class="md:w-[28%]"
|
||||
>
|
||||
Usage / Size
|
||||
<PlatformResponsiveTableLabel compact="Usage" full="Usage / Size" />
|
||||
</PlatformSortableTableHead>
|
||||
<PlatformSortableTableHead
|
||||
kind="numeric-value"
|
||||
|
||||
+2
-2
@@ -157,8 +157,8 @@ describe('TrueNASStorageTopologyTable user sorting', () => {
|
||||
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.
|
||||
|
||||
@@ -209,7 +209,7 @@ export const VsphereHostsTable: Component<{
|
||||
kind="name"
|
||||
sort={sort}
|
||||
sortKey="host"
|
||||
class="md:w-[16%]"
|
||||
class="md:w-[15%]"
|
||||
>
|
||||
Host
|
||||
</PlatformSortableTableHead>
|
||||
@@ -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
|
||||
</PlatformSortableTableHead>
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user