From c68be0041285192fbb726de06d770834a914df5c Mon Sep 17 00:00:00 2001 From: rcourtman Date: Tue, 18 Aug 2026 21:01:07 +0100 Subject: [PATCH] fix(frontend): keep narrow mobile data readable --- .../subsystems/frontend-primitives.md | 27 +- .../subsystems/performance-and-scalability.md | 4 + .../internal/subsystems/unified-resources.md | 13 + frontend-modern/browser-verification.json | 74 ++-- .../src/components/Workloads/GuestRow.tsx | 10 +- .../Workloads/WorkloadTableHeader.tsx | 1 + .../Workloads/__tests__/GuestRow.test.tsx | 44 +++ .../components/Workloads/guestRowModel.tsx | 43 ++- .../Workloads/useWorkloadsControlsState.ts | 4 +- .../SharedPrimitives.guardrails.test.ts | 12 + .../src/features/docker/DockerHostsTable.tsx | 44 +-- .../docker/DockerNativeTableShared.tsx | 2 +- .../__tests__/DockerHostsTable.test.tsx | 8 + .../dockerContainerTableModel.test.ts | 11 + .../docker/dockerContainerTableModel.ts | 37 +- .../sharedPlatformPage.navigation.test.tsx | 6 + .../platformPage/sharedPlatformPage.tsx | 9 +- .../features/proxmox/ProxmoxNodesTable.tsx | 27 +- .../proxmox/ProxmoxReplicationTable.tsx | 316 ++++++++++++------ .../ProxmoxReplicationTable.test.tsx | 28 +- .../__tests__/proxmoxHostTableModel.test.ts | 13 + .../features/proxmox/proxmoxHostTableModel.ts | 42 ++- 22 files changed, 550 insertions(+), 225 deletions(-) diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md index b10abdc3f6..058c902e31 100644 --- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md +++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md @@ -394,7 +394,10 @@ alignment classes, or ad hoc report-category coercion. Platform tabs are responsively scrollable within that shared shell. When the rail overflows, the primitive must expose focusable, labeled previous/next controls and keep their availability synchronized with the rail position so hidden workflows remain -discoverable without widening the document or requiring precision swipes. +discoverable without widening the document or requiring precision swipes. At +the leading edge, the first tab must begin flush with the rail instead of +reserving an empty previous-control slot; trailing space may be reserved only +while the next-control affordance is actually present. Platform tabs are feature-owned consumers of canonical resource payloads: Docker page model helpers may prefer backend-authored `DockerData` stack and Podman metadata for @@ -1094,12 +1097,22 @@ not a replacement status card, CTA band, or page-local nested card. high-value phone columns when that many meaningful fields exist, keep the identity column at the canonical 30 percent of the available width, use the shared compact label and column-width classes, and preserve the fuller tablet - and desktop presentation. `PlatformTableShell` marks every owned table with - the shared `platform-table` class; direct Workloads and Storage consumers and - nested provider detail tables must apply that same marker so the phone - contract cannot vary by rendering path. They must not rely on a global rule - that reveals every hidden - column: phone priority remains source-specific because state, capacity, + and desktop presentation. Below a 360-pixel content width, the shared narrow + layout must retain five high-value columns, promote identity to 40 percent, + and remove the lowest-priority context columns rather than shrinking names + and metric values into illegible fragments. `PlatformTableShell` marks every + owned table with the shared `platform-table` class; direct Workloads and + Storage consumers and nested provider detail tables must apply that same + marker so the phone contract cannot vary by rendering path. Text-only + operational rows must use `getPlatformTableRowClass` so the absence of + buttons, badges, or progress bars does not collapse them below the canonical + compact scanning rhythm. Mobile truncation may rely on a full-value row + detail only when that row actually has a keyboard- and touch-operable + disclosure; a non-expandable identity cell must otherwise expose its + complete value without requiring hover. Default data rows stay single-line + so scan density and row rhythm are not traded for automatic text wrapping. + They must not rely on a global rule that reveals + every hidden column: phone priority remains source-specific because state, capacity, freshness, and recovery posture do not carry equal value for every resource type. Narrow viewports keep document-level overflow contained by `Table` and diff --git a/docs/release-control/v6/internal/subsystems/performance-and-scalability.md b/docs/release-control/v6/internal/subsystems/performance-and-scalability.md index 1e04fc7ea3..d100747f0a 100644 --- a/docs/release-control/v6/internal/subsystems/performance-and-scalability.md +++ b/docs/release-control/v6/internal/subsystems/performance-and-scalability.md @@ -571,6 +571,10 @@ change may globally weaken the Task 03 lifecycle-state idempotency invariant. 8. Render workload row identity directly from the shared canonical workload helper so row selection, hover, and fallback metadata lookup stay aligned with the same workload contract 9. Format infrastructure sensor labels through the shared `frontend-modern/src/utils/textPresentation.ts` presentation helper instead of maintaining a local title-casing implementation in `frontend-modern/src/components/Infrastructure/resourceDetailMappers.ts` 10. Extend workload row contract and per-row hot-path derivations through `frontend-modern/src/components/Workloads/guestRowModel.tsx` and `frontend-modern/src/components/Workloads/useGuestRowState.ts`, and extend tooltip-backed row cell presentation through `frontend-modern/src/components/Workloads/GuestRowCells.tsx`, rather than rebuilding column metadata, row identity, cell tooltips, or anomaly correlation inside `frontend-modern/src/components/Workloads/GuestRow.tsx` + Ultra-narrow workload presentation belongs in the existing layout and + column model: below 360 pixels it keeps five identity/health columns with a + 40-percent identity track. It must not add per-row measurement, viewport + listeners, or render-time column discovery to the Workloads hot path. Workload runtime badges must consume the shared resource badge presentation helper rather than carrying local neutral chip classes, so Docker, Podman, and future container runtimes keep the same identity tones diff --git a/docs/release-control/v6/internal/subsystems/unified-resources.md b/docs/release-control/v6/internal/subsystems/unified-resources.md index 97e7a8b6ef..e78e769367 100644 --- a/docs/release-control/v6/internal/subsystems/unified-resources.md +++ b/docs/release-control/v6/internal/subsystems/unified-resources.md @@ -453,6 +453,19 @@ and the relative weights for those columns, while dense platform table rendering must use `getPlatformTableWeightedColumnWidthStyle` for visible-column percentage normalization, zero-width fallback, and stable percentage formatting instead of declaring local width-percent helpers. +Workload, Proxmox host, Docker host, and Docker container consumers must enter the shared +`narrow` layout below a 360-pixel content width: identity receives 40 percent, +five high-value identity/health columns remain, and lower-priority type, ID, +temperature, restart, or availability context returns at the ordinary phone +stage instead of making the retained values unreadable. +`ProxmoxReplicationTable` must also compose the frontend-primitives-owned +`getPlatformTableRowClass` for its text-only job rows so its five-column phone +projection retains the shared platform scanning rhythm instead of collapsing +to the intrinsic text height. That phone projection keeps the +guest identity on one line and makes every row keyboard- and touch-operable so +the complete guest identity and job context are available through an explicit +detail disclosure rather than hover-only title text. Compact status, route, +last-sync, and next-sync values must remain fully visible in the scanning row. Optional numeric table cells follow the same split: unified-resource consumers own which count or replica field is meaningful, whether the domain should zero-default an absent scheduler/service/inventory count, whether a diff --git a/frontend-modern/browser-verification.json b/frontend-modern/browser-verification.json index da83ce00a6..5ee3d7aaea 100644 --- a/frontend-modern/browser-verification.json +++ b/frontend-modern/browser-verification.json @@ -1,41 +1,65 @@ { "version": 1, "result": "passed", - "verified_at": "2026-08-18T19:20:22Z", - "base_sha": "7879862c24a9657707f8a963f2bf70d1c50ed32c", + "verified_at": "2026-08-18T19:58:12Z", + "base_sha": "9ba62be3033d5ed326c55bac7304e47683ea3e11", "changed_paths": [ - "frontend-modern/src/features/alerts/OverviewTab.tsx", - "frontend-modern/src/features/alerts/tabs/DestinationsTab.tsx", - "frontend-modern/src/features/alerts/useAlertDestinationsTabState.ts", - "frontend-modern/src/features/alerts/useNotificationDeliveryHealth.ts" + "frontend-modern/src/components/Workloads/GuestRow.tsx", + "frontend-modern/src/components/Workloads/WorkloadTableHeader.tsx", + "frontend-modern/src/components/Workloads/guestRowModel.tsx", + "frontend-modern/src/components/Workloads/useWorkloadsControlsState.ts", + "frontend-modern/src/features/docker/DockerHostsTable.tsx", + "frontend-modern/src/features/docker/DockerNativeTableShared.tsx", + "frontend-modern/src/features/docker/dockerContainerTableModel.ts", + "frontend-modern/src/features/platformPage/sharedPlatformPage.tsx", + "frontend-modern/src/features/proxmox/ProxmoxNodesTable.tsx", + "frontend-modern/src/features/proxmox/ProxmoxReplicationTable.tsx", + "frontend-modern/src/features/proxmox/proxmoxHostTableModel.ts" ], "content_sha256": { - "frontend-modern/src/features/alerts/OverviewTab.tsx": "a96d21aed9960769a7fbdf8fcdd3ebbd64890749c4afab2259cd11df97a92653", - "frontend-modern/src/features/alerts/tabs/DestinationsTab.tsx": "eb85b628ea48e3e399d8613fa0511638d62f66b4b1f0e2a12d7127901dede711", - "frontend-modern/src/features/alerts/useAlertDestinationsTabState.ts": "76046151c145a681b0bf098d50be3c7a8d5fa89d80304fc014bd8f6fb79025b3", - "frontend-modern/src/features/alerts/useNotificationDeliveryHealth.ts": "092df36398945a9436017c070adf5f5a9e0a439017bed4069ee48abcfa7117fb" + "frontend-modern/src/components/Workloads/GuestRow.tsx": "012be1639b4cd68b0a1e08f912bac125024795433e9ddc17d1c0f6e0adbdf732", + "frontend-modern/src/components/Workloads/WorkloadTableHeader.tsx": "5a6489738cf5e5422ba015a4f97450b734270ed0d8bf68a8b713760fd0d98153", + "frontend-modern/src/components/Workloads/guestRowModel.tsx": "718a01cc0567e90f08ea4b4c3d98d70fd350fe45c3e587ef233f425db2011279", + "frontend-modern/src/components/Workloads/useWorkloadsControlsState.ts": "f94bfc204f366f7fb644d4b162c0ff18192493aebbf28f843e978d7fc73ffbe9", + "frontend-modern/src/features/docker/DockerHostsTable.tsx": "b9ba1e7cce85d15c63de1191fd979fd2fb8097c506ad884fd9ca2c0fbca51348", + "frontend-modern/src/features/docker/DockerNativeTableShared.tsx": "f9f6f064c4089fc86277561431cda5ca96daff0dc0e3aef08a40d7fae7bf8903", + "frontend-modern/src/features/docker/dockerContainerTableModel.ts": "e05dda8e13765526322bde8d9c09f02e369b132e14fece0162d063c1968df80f", + "frontend-modern/src/features/platformPage/sharedPlatformPage.tsx": "0d6d76222bf096713285926373a362bda62222c550f8c5d4060d80c54e9964a8", + "frontend-modern/src/features/proxmox/ProxmoxNodesTable.tsx": "e62a369d083a581dacb1906477c4351c0bf3f92221138f8e23baa2780e31a54e", + "frontend-modern/src/features/proxmox/ProxmoxReplicationTable.tsx": "04249616c146d3f768661b09adc323b703a310af10a96e8223103838c54d1680", + "frontend-modern/src/features/proxmox/proxmoxHostTableModel.ts": "7e6e79f3bab5faba79b18fd9a8dcee8c7bb20054f8d27ed3c51754ce9c028f9c" }, "routes": [ - "/alerts/overview", - "/alerts/notifications" - ], - "states": [ - "healthy queue: no delivery warning on the alerts overview", - "degraded queue: warning renders above the overview stat cards, naming the dominant failure class", - "before the first health load resolves: nothing rendered, so no warning flash" - ], - "interactions": [ - "loaded /alerts/overview against a healthy queue and confirmed the overview stays clean", - "served a degraded notification health payload and re-entered /alerts/overview; the delivery warning appeared with its refresh control" + "/proxmox/overview", + "/proxmox/replication", + "/docker/overview" ], "viewports": [ { - "width": 1280, - "height": 800 + "width": 1440, + "height": 1000 }, { - "width": 375, - "height": 812 + "width": 390, + "height": 844 + }, + { + "width": 320, + "height": 844 } + ], + "states": [ + "Platform tab rails start flush at the leading edge and reserve trailing space only while the next control is present", + "Proxmox overview shows five readable host and workload fields at 320 pixels and restores Type, ID, and Temperature at 390 pixels", + "Replication shows five single-line priority fields throughout phone widths with complete route and time values", + "Docker hosts show Host, CPU, Memory, Disk, and Uptime at 320 pixels without wrapped OS metadata", + "Desktop Proxmox and Docker tables retain their full column sets and primary navigation" + ], + "interactions": [ + "used the Proxmox tab rail next and previous controls at 320 pixels", + "expanded and collapsed a replication row at 320 and 390 pixels to verify touch access to the complete guest, job, and route values", + "opened Proxmox host and workload disclosures to confirm truncated identities remain touch-accessible", + "switched from Proxmox to Docker through the dedicated mobile platform switcher", + "verified the Docker host and container rows retain keyboard- and touch-operable detail disclosures" ] } diff --git a/frontend-modern/src/components/Workloads/GuestRow.tsx b/frontend-modern/src/components/Workloads/GuestRow.tsx index 4db1a044fe..c4c75fd062 100644 --- a/frontend-modern/src/components/Workloads/GuestRow.tsx +++ b/frontend-modern/src/components/Workloads/GuestRow.tsx @@ -104,7 +104,13 @@ export function GuestRow(props: GuestRowProps) { const metricDisplayMode = createMemo(() => props.metricDisplayMode ?? 'bars'); const isSparklineMode = createMemo(() => metricDisplayMode() === 'sparklines'); const usesCompactTableLayout = createMemo( - () => props.workloadTableLayoutMode === 'phone' || props.workloadTableLayoutMode === 'mobile', + () => + props.workloadTableLayoutMode === 'narrow' || + props.workloadTableLayoutMode === 'phone' || + props.workloadTableLayoutMode === 'mobile', + ); + const usesCondensedIdentity = createMemo( + () => props.workloadTableLayoutMode === 'narrow' || props.workloadTableLayoutMode === 'phone', ); const telemetryAvailable = ( metric: keyof NonNullable, @@ -228,7 +234,7 @@ export function GuestRow(props: GuestRowProps) { a]:hidden' : ''}`} nameClass="truncate text-[11px] font-medium text-base-content select-none" /> {/* Show backup indicator in name cell only if backup column is hidden */} diff --git a/frontend-modern/src/components/Workloads/WorkloadTableHeader.tsx b/frontend-modern/src/components/Workloads/WorkloadTableHeader.tsx index 0cd5286d26..d9807a5339 100644 --- a/frontend-modern/src/components/Workloads/WorkloadTableHeader.tsx +++ b/frontend-modern/src/components/Workloads/WorkloadTableHeader.tsx @@ -71,6 +71,7 @@ export function WorkloadTableHeader(props: WorkloadTableHeaderProps) { const isSorted = () => sortKeyForCol && props.sortKey() === sortKeyForCol; const usesCompactHeader = () => props.isMobile() || + props.workloadTableLayoutMode() === 'narrow' || props.workloadTableLayoutMode() === 'phone' || props.workloadTableLayoutMode() === 'mobile'; const label = () => diff --git a/frontend-modern/src/components/Workloads/__tests__/GuestRow.test.tsx b/frontend-modern/src/components/Workloads/__tests__/GuestRow.test.tsx index 61a2c09507..c839e8c8ae 100644 --- a/frontend-modern/src/components/Workloads/__tests__/GuestRow.test.tsx +++ b/frontend-modern/src/components/Workloads/__tests__/GuestRow.test.tsx @@ -926,6 +926,22 @@ describe('GuestRow', () => { expect(container.querySelector('td[data-workload-col="link"]')).toBeNull(); }); + it.each(['narrow', 'phone'] as const)( + 'prioritizes the workload name over the adjacent web link in %s rows', + (workloadTableLayoutMode) => { + const { container } = renderGuestRow({ + guest: makeGuest(), + customUrl: 'https://example.com', + visibleColumnIds: ['name'], + workloadTableLayoutMode, + }); + + expect(container.querySelector('a[href="https://example.com"]')?.parentElement).toHaveClass( + '[&>a]:hidden', + ); + }, + ); + it('does not render a trailing infrastructure fallback link when no customUrl is set', () => { const { container } = renderGuestRow({ guest: makeGuest(), @@ -1104,11 +1120,35 @@ describe('GUEST_COLUMNS', () => { it('derives normalized tablet and compact widths from the visible workload columns', () => { const allModeColumns = GUEST_COLUMNS.filter((column) => VIEW_MODE_COLUMNS.all!.has(column.id)); + const narrowColumns = getWorkloadVisibleColumnsForLayout(allModeColumns, 'narrow'); const phoneColumns = getWorkloadVisibleColumnsForLayout(allModeColumns, 'phone'); const mobileColumns = getWorkloadVisibleColumnsForLayout(allModeColumns, 'mobile'); const tabletColumns = getWorkloadVisibleColumnsForLayout(allModeColumns, 'tablet'); const compactColumns = getWorkloadVisibleColumnsForLayout(allModeColumns, 'compact'); + expect(narrowColumns.map((column) => column.id)).toEqual([ + 'name', + 'cpu', + 'memory', + 'disk', + 'uptime', + ]); + expect( + getGuestColumnWidthStyle( + 'name', + true, + 'narrow', + narrowColumns.map((column) => column.id), + ), + ).toEqual({ width: '40%' }); + expect( + getGuestColumnWidthStyle( + 'cpu', + true, + 'narrow', + narrowColumns.map((column) => column.id), + ), + ).toEqual({ width: '16%' }); expect(phoneColumns.map((column) => column.id)).toEqual([ 'name', 'availability', @@ -1209,6 +1249,8 @@ describe('GUEST_COLUMNS', () => { }); it('maps workload table layout modes to viewport width stages', () => { + expect(getWorkloadTableLayoutMode(359)).toBe('narrow'); + expect(getWorkloadTableLayoutMode(360)).toBe('phone'); expect(getWorkloadTableLayoutMode(479)).toBe('phone'); expect(getWorkloadTableLayoutMode(480)).toBe('mobile'); expect(getWorkloadTableLayoutMode(767)).toBe('mobile'); @@ -1223,6 +1265,8 @@ describe('GUEST_COLUMNS', () => { }); it('maps workload table layout modes to the actual table container', () => { + expect(getWorkloadTableLayoutModeForContainer(359)).toBe('narrow'); + expect(getWorkloadTableLayoutModeForContainer(360)).toBe('phone'); expect(getWorkloadTableLayoutModeForContainer(439)).toBe('phone'); expect(getWorkloadTableLayoutModeForContainer(440)).toBe('mobile'); expect(getWorkloadTableLayoutModeForContainer(719)).toBe('mobile'); diff --git a/frontend-modern/src/components/Workloads/guestRowModel.tsx b/frontend-modern/src/components/Workloads/guestRowModel.tsx index 0843636ae4..1a8171e54b 100644 --- a/frontend-modern/src/components/Workloads/guestRowModel.tsx +++ b/frontend-modern/src/components/Workloads/guestRowModel.tsx @@ -3,6 +3,7 @@ import type { JSX } from 'solid-js'; import type { ColumnDef } from '@/hooks/useColumnVisibility'; import type { SummaryGroupMemberInteractionState } from '@/components/shared/summaryCardInteraction'; import { + PLATFORM_TABLE_NARROW_IDENTITY_WIDTH_PERCENT, getPlatformTableWeightedColumnWidthStyle, PLATFORM_TABLE_PHONE_IDENTITY_WIDTH_PERCENT, } from '@/features/platformPage/sharedPlatformPage'; @@ -15,8 +16,9 @@ import type { import type { NestedWorkloadContext } from './nestedWorkloadContext'; import type { WorkloadsMemoryDisplayBasis } from './workloadsFilterModel'; -export type WorkloadTableLayoutMode = 'phone' | 'mobile' | 'tablet' | 'compact' | 'wide'; +export type WorkloadTableLayoutMode = 'narrow' | 'phone' | 'mobile' | 'tablet' | 'compact' | 'wide'; +export const WORKLOAD_TABLE_NARROW_LAYOUT_WIDTH = 360; export const WORKLOAD_TABLE_PHONE_LAYOUT_WIDTH = 480; export const WORKLOAD_TABLE_MOBILE_LAYOUT_WIDTH = 768; export const WORKLOAD_TABLE_TABLET_LAYOUT_WIDTH = 900; @@ -27,33 +29,35 @@ export const WORKLOAD_TABLE_TABLET_LAYOUT_WIDTH = 900; // relying on horizontal scroll, so wide waits until the shell can actually // hold the full set. export const WORKLOAD_TABLE_WIDE_LAYOUT_WIDTH = 1536; +export const WORKLOAD_TABLE_CONTAINER_NARROW_WIDTH = 360; export const WORKLOAD_TABLE_CONTAINER_PHONE_WIDTH = 440; export const WORKLOAD_TABLE_CONTAINER_MOBILE_WIDTH = 720; export const WORKLOAD_TABLE_CONTAINER_TABLET_WIDTH = 900; export const WORKLOAD_TABLE_CONTAINER_WIDE_WIDTH = 1440; const WORKLOAD_TABLE_LAYOUT_ORDER: Record = { - phone: 0, - mobile: 1, - tablet: 2, - compact: 3, - wide: 4, + narrow: 0, + phone: 1, + mobile: 2, + tablet: 3, + compact: 4, + wide: 5, }; const WORKLOAD_COLUMN_MIN_LAYOUT: Record = { - name: 'phone', + name: 'narrow', availability: 'phone', runtime: 'tablet', - cpu: 'phone', - memory: 'phone', + cpu: 'narrow', + memory: 'narrow', // Phone rows retain the compact operational scan: workload kind, all three // capacity metrics, and age. The percentage model below preserves a useful // identity track without hiding those signals behind row expansion. - disk: 'phone', + disk: 'narrow', type: 'phone', info: 'phone', vmid: 'tablet', - uptime: 'phone', + uptime: 'narrow', backup: 'compact', image: 'compact', namespace: 'compact', @@ -445,6 +449,13 @@ const GUEST_COLUMN_RESPONSIVE_WEIGHTS: Record< Exclude, Record > = { + narrow: { + name: 40, + cpu: 16, + memory: 16, + disk: 16, + uptime: 12, + }, phone: { name: 33, availability: 5, @@ -509,9 +520,11 @@ const getResponsiveColumnOverride = ( columnId, weights, activeIds, - layoutMode === 'phone' || layoutMode === 'mobile' - ? { columnId: 'name', widthPercent: PLATFORM_TABLE_PHONE_IDENTITY_WIDTH_PERCENT } - : undefined, + layoutMode === 'narrow' + ? { columnId: 'name', widthPercent: PLATFORM_TABLE_NARROW_IDENTITY_WIDTH_PERCENT } + : layoutMode === 'phone' || layoutMode === 'mobile' + ? { columnId: 'name', widthPercent: PLATFORM_TABLE_PHONE_IDENTITY_WIDTH_PERCENT } + : undefined, ); return typeof style.width === 'string' ? percentageColumn(style.width) : undefined; }; @@ -571,6 +584,7 @@ export const getGuestColumnWidthStyle = ( }; export const getWorkloadTableLayoutMode = (width: number): WorkloadTableLayoutMode => { + if (!Number.isFinite(width) || width < WORKLOAD_TABLE_NARROW_LAYOUT_WIDTH) return 'narrow'; if (!Number.isFinite(width) || width < WORKLOAD_TABLE_PHONE_LAYOUT_WIDTH) return 'phone'; if (width < WORKLOAD_TABLE_MOBILE_LAYOUT_WIDTH) return 'mobile'; if (width < WORKLOAD_TABLE_TABLET_LAYOUT_WIDTH) return 'tablet'; @@ -579,6 +593,7 @@ export const getWorkloadTableLayoutMode = (width: number): WorkloadTableLayoutMo }; export const getWorkloadTableLayoutModeForContainer = (width: number): WorkloadTableLayoutMode => { + if (!Number.isFinite(width) || width < WORKLOAD_TABLE_CONTAINER_NARROW_WIDTH) return 'narrow'; if (!Number.isFinite(width) || width < WORKLOAD_TABLE_CONTAINER_PHONE_WIDTH) return 'phone'; if (width < WORKLOAD_TABLE_CONTAINER_MOBILE_WIDTH) return 'mobile'; if (width < WORKLOAD_TABLE_CONTAINER_TABLET_WIDTH) return 'tablet'; diff --git a/frontend-modern/src/components/Workloads/useWorkloadsControlsState.ts b/frontend-modern/src/components/Workloads/useWorkloadsControlsState.ts index 18f3854d44..5c4247a398 100644 --- a/frontend-modern/src/components/Workloads/useWorkloadsControlsState.ts +++ b/frontend-modern/src/components/Workloads/useWorkloadsControlsState.ts @@ -96,7 +96,9 @@ export function useWorkloadsControlsState(options: WorkloadsControlsStateOptions ? getWorkloadTableLayoutModeForContainer(measuredWidth) : getWorkloadTableLayoutMode(breakpoint.width()); }); - const isMobile = createMemo(() => workloadTableLayoutMode() === 'mobile'); + const isMobile = createMemo(() => + ['narrow', 'phone', 'mobile'].includes(workloadTableLayoutMode()), + ); const [isSearchLocked, setIsSearchLocked] = createSignal(false); const updateSearchParam = (mutate: (params: URLSearchParams) => void): void => { diff --git a/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts b/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts index e86ce8cc93..d03d814a81 100644 --- a/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts +++ b/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts @@ -7276,6 +7276,18 @@ describe('shared primitive guardrails', () => { expect(sharedPlatformPageSource).toContain('props.tabs.length > 1'); expect(sharedPlatformPageSource).toContain('href={tab.path}'); expect(sharedPlatformPageSource).toContain('border-b-2'); + expect(sharedPlatformPageSource).toContain('overflow-x-auto pl-0'); + expect(sharedPlatformPageSource).toContain("canScrollRight() ? 'pr-10' : 'pr-0'"); + expect(sharedPlatformPageSource).not.toContain('overflow-x-auto px-10'); + expect(sharedPlatformPageSource).toContain('export const getPlatformTableRowClass'); + expect(sharedPlatformPageSource).toContain( + 'export const PLATFORM_TABLE_NARROW_IDENTITY_WIDTH_PERCENT = 40', + ); + expect(proxmoxNodesTableSource).toContain("usesCondensedIdentity() ? '[&>a]:hidden' : ''"); + expect(proxmoxNodesTableSource).toContain( + "layoutMode() === 'narrow' && nativeNodeName().length", + ); + expect(proxmoxReplicationTableSource).toContain('getPlatformTableRowClass()'); expect(horizontalRailVisibilityModelSource).toContain( 'export function getHorizontalRailScrollLeft', ); diff --git a/frontend-modern/src/features/docker/DockerHostsTable.tsx b/frontend-modern/src/features/docker/DockerHostsTable.tsx index e7943e3ea5..2c78504da4 100644 --- a/frontend-modern/src/features/docker/DockerHostsTable.tsx +++ b/frontend-modern/src/features/docker/DockerHostsTable.tsx @@ -246,7 +246,7 @@ export const DockerHostsTable: Component<{ kind="name" sort={sort} sortKey="host" - class="platform-table-mobile-w-30 w-[34%] md:w-[13%]" + class="w-[40%] min-[360px]:w-[34%] md:w-[13%]" > Host @@ -278,7 +278,7 @@ export const DockerHostsTable: Component<{ kind="metric-bar" sort={sort} sortKey="cpu" - class="w-[14%] md:w-[14%]" + class="w-[15%] min-[360px]:w-[13%] md:w-[14%]" > CPU @@ -286,7 +286,7 @@ export const DockerHostsTable: Component<{ kind="metric-bar" sort={sort} sortKey="memory" - class="w-[14%] md:w-[14%]" + class="w-[15%] min-[360px]:w-[13%] md:w-[14%]" > Mem @@ -295,7 +295,7 @@ export const DockerHostsTable: Component<{ kind="metric-bar" sort={sort} sortKey="disk" - class="w-[14%] md:w-[14%]" + class="w-[15%] min-[360px]:w-[13%] md:w-[14%]" > Disk @@ -303,7 +303,7 @@ export const DockerHostsTable: Component<{ kind="numeric-value" sort={sort} sortKey="uptime" - class="w-[12%] sm:hidden md:table-cell md:w-[6%]" + class="w-[15%] min-[360px]:w-[11%] sm:hidden md:table-cell md:w-[6%]" > @@ -311,7 +311,7 @@ export const DockerHostsTable: Component<{ kind="numeric-value" sort={sort} sortKey="temp" - class="w-[16%] sm:hidden md:table-cell md:w-[6%]" + class="hidden min-[360px]:table-cell min-[360px]:w-[16%] md:table-cell md:w-[6%]" > @@ -397,14 +397,13 @@ export const DockerHostsTable: Component<{ onKeyDown={handleActivationKey} tabIndex={0} > - +
- - {(badge) => ( - - {badge().label} - - )} -
- + - + } @@ -483,9 +469,7 @@ export const DockerHostsTable: Component<{ /> - + } @@ -499,12 +483,12 @@ export const DockerHostsTable: Component<{ {formatPlatformTableUptimeValue(host.uptime ?? docker()?.uptimeSeconds)}