diff --git a/frontend-modern/src/App.tsx b/frontend-modern/src/App.tsx index 9e825c1ea..7c3ccf22f 100644 --- a/frontend-modern/src/App.tsx +++ b/frontend-modern/src/App.tsx @@ -274,6 +274,7 @@ function App() { }); let appShellRoutePreloadCleanup: (() => void) | undefined; let appShellRoutesPreloadScheduled = false; + let workspaceRedirectPending = false; createEffect(() => { location.pathname; @@ -286,8 +287,13 @@ function App() { createEffect(() => { if (runtime.isLoading() || runtime.needsAuth() || isPublicRoute()) return; - if (!isWorkspaceEntryRoutePath(location.pathname)) return; + if (!isWorkspaceEntryRoutePath(location.pathname)) { + workspaceRedirectPending = false; + return; + } if (!platformNavigationResolved()) return; + if (workspaceRedirectPending) return; + workspaceRedirectPending = true; navigate(getDefaultWorkspaceRoute(platformNavigationVisibility(), hasSettingsAccess()), { replace: true, }); diff --git a/frontend-modern/src/__tests__/App.architecture.test.ts b/frontend-modern/src/__tests__/App.architecture.test.ts index 5c7e4768e..6430193fd 100644 --- a/frontend-modern/src/__tests__/App.architecture.test.ts +++ b/frontend-modern/src/__tests__/App.architecture.test.ts @@ -147,6 +147,9 @@ describe('App architecture', () => { expect(appSource).toContain("normalizedPath === '/'"); expect(appSource).toContain("normalizedPath === '/login'"); expect(appSource).toContain("normalizedPath === '/infrastructure'"); + expect(appSource).toContain('let workspaceRedirectPending = false'); + expect(appSource).toContain('if (workspaceRedirectPending) return'); + expect(appSource).toContain('workspaceRedirectPending = true'); expect(appSource).toContain( '', ); diff --git a/frontend-modern/src/components/Workloads/__tests__/WorkloadsSurface.performance.contract.test.tsx b/frontend-modern/src/components/Workloads/__tests__/WorkloadsSurface.performance.contract.test.tsx index 648c22d19..390fdc9cd 100644 --- a/frontend-modern/src/components/Workloads/__tests__/WorkloadsSurface.performance.contract.test.tsx +++ b/frontend-modern/src/components/Workloads/__tests__/WorkloadsSurface.performance.contract.test.tsx @@ -1045,6 +1045,10 @@ describe('Workloads performance contract', () => { ); expect(workloadsWorkloadRouteStateSource).toContain('useWorkloadUrlSync'); expect(workloadsWorkloadRouteStateSource).toContain('useWorkloadFilterOptions'); + expect(workloadsWorkloadRouteStateSource).not.toContain('window.location.pathname'); + expect(workloadsWorkloadRouteStateSource).not.toContain('window.location.search'); + expect(workloadsControlsStateSource).not.toContain('window.location.pathname'); + expect(workloadsControlsStateSource).not.toContain('window.location.search'); expect(workloadsWorkloadRouteStateSource).not.toContain('buildWorkloadsPath({'); expect(workloadsWorkloadRouteStateSource).not.toContain('normalizeWorkloadViewModeParam'); expect(workloadsWorkloadRouteStateSource).not.toContain( diff --git a/frontend-modern/src/components/Workloads/__tests__/useWorkloadsControlsState.test.ts b/frontend-modern/src/components/Workloads/__tests__/useWorkloadsControlsState.test.ts index 0f133cd52..5dbbd2e19 100644 --- a/frontend-modern/src/components/Workloads/__tests__/useWorkloadsControlsState.test.ts +++ b/frontend-modern/src/components/Workloads/__tests__/useWorkloadsControlsState.test.ts @@ -141,7 +141,9 @@ describe('useWorkloadsControlsState', () => { }); setMockRouterSearch(''); - window.history.replaceState(null, '', '/workloads'); + // During a router transition the browser URL can still reflect the route + // being left. Restores must target the router location that owns the hook. + window.history.replaceState(null, '', '/stale-workspace-entry'); navigateSpy.mockClear(); const disposeRestore = createRoot((dispose) => { diff --git a/frontend-modern/src/components/Workloads/useWorkloadRouteState.ts b/frontend-modern/src/components/Workloads/useWorkloadRouteState.ts index 24ab1f610..06cc1d526 100644 --- a/frontend-modern/src/components/Workloads/useWorkloadRouteState.ts +++ b/frontend-modern/src/components/Workloads/useWorkloadRouteState.ts @@ -1,5 +1,5 @@ import { createSignal, onMount, type Accessor, type Setter } from 'solid-js'; -import { useNavigate } from '@solidjs/router'; +import { useLocation, useNavigate } from '@solidjs/router'; import type { WorkloadGuest, ViewMode } from '@/types/workloads'; import { deserializeWorkloadViewMode } from './workloadRouteModel'; import { @@ -20,6 +20,7 @@ export interface WorkloadRouteStateOptions { } export function useWorkloadRouteState(options: WorkloadRouteStateOptions) { + const location = useLocation(); const navigate = useNavigate(); const [selectedNode, setSelectedNode] = createSignal(null); const [selectedPlatform, setSelectedPlatform] = createSignal(null); @@ -36,7 +37,7 @@ export function useWorkloadRouteState(options: WorkloadRouteStateOptions) { onMount(() => { if (typeof window === 'undefined') return; - const params = new URLSearchParams(window.location.search); + const params = new URLSearchParams(location.search); let mutated = false; if (!params.has(WORKLOADS_QUERY_PARAMS.type)) { @@ -60,7 +61,7 @@ export function useWorkloadRouteState(options: WorkloadRouteStateOptions) { } if (mutated) { - navigate(`${window.location.pathname}?${params.toString()}`, { replace: true }); + navigate(`${location.pathname}?${params.toString()}`, { replace: true }); } }); const filterViewMode = () => options.forcedViewMode ?? viewMode(); diff --git a/frontend-modern/src/components/Workloads/useWorkloadsControlsState.ts b/frontend-modern/src/components/Workloads/useWorkloadsControlsState.ts index 8cbaea829..4eb5cfea4 100644 --- a/frontend-modern/src/components/Workloads/useWorkloadsControlsState.ts +++ b/frontend-modern/src/components/Workloads/useWorkloadsControlsState.ts @@ -125,12 +125,12 @@ export function useWorkloadsControlsState(options: WorkloadsControlsStateOptions onMount(() => { if (typeof window === 'undefined') return; - const params = new URLSearchParams(window.location.search); + const params = new URLSearchParams(location.search); if (params.has('status')) return; const saved = readSavedWorkloadsStatusMode(options.statusModeStorageScope); if (saved !== DEFAULT_WORKLOADS_STATUS_MODE) { params.set('status', saved); - navigate(`${window.location.pathname}?${params.toString()}`, { replace: true }); + navigate(`${location.pathname}?${params.toString()}`, { replace: true }); } }); diff --git a/frontend-modern/src/features/docker/DockerImagesTable.tsx b/frontend-modern/src/features/docker/DockerImagesTable.tsx index 08c36961b..4a623c1b4 100644 --- a/frontend-modern/src/features/docker/DockerImagesTable.tsx +++ b/frontend-modern/src/features/docker/DockerImagesTable.tsx @@ -20,14 +20,26 @@ import { DockerResourceNameCell, dockerByteValue, dockerHostName, - dockerJoinValues, dockerResourceName, - dockerNumberValue, type DockerNativeTableProps, } from './DockerNativeTableShared'; import { filterDockerResources, type DockerResourceStatusFilter } from './dockerPageModel'; +import { + getDockerImageOperationalPresentation, + type DockerImageUpdateTone, +} from './dockerImagePresentation'; +import type { Resource } from '@/types/resource'; -export const DockerImagesTable: Component = (props) => { +const updateToneClass: Record = { + danger: 'bg-red-100 text-red-700 dark:bg-red-950/40 dark:text-red-300', + warning: 'bg-amber-100 text-amber-700 dark:bg-amber-950/40 dark:text-amber-300', + success: 'bg-emerald-100 text-emerald-700 dark:bg-emerald-950/40 dark:text-emerald-300', + muted: 'bg-surface-hover text-muted', +}; + +export const DockerImagesTable: Component< + DockerNativeTableProps & { relatedContainers?: Resource[] } +> = (props) => { const tableState = createPlatformTableFilterState({ resources: () => props.resources, initialStatus: 'all' as DockerResourceStatusFilter, @@ -52,7 +64,7 @@ export const DockerImagesTable: Component = (props) => { = (props) => { > - + Image - - Tags + + Host + + + Used by - Size - - In Use - -