From 934052eec83f7915d40ab7b845dcbcbfd5f25faf Mon Sep 17 00:00:00 2001 From: rcourtman Date: Sun, 22 Mar 2026 00:47:56 +0000 Subject: [PATCH] Split dashboard workload URL sync owners --- .../subsystems/performance-and-scalability.md | 56 ++++---- .../v6/internal/subsystems/registry.json | 4 + .../Dashboard.performance.contract.test.tsx | 25 +++- .../dashboardWorkloadUrlSyncModel.test.ts | 106 +++++++++++++++ .../dashboardWorkloadUrlSyncModel.ts | 122 ++++++++++++++++++ .../Dashboard/useDashboardWorkloadUrlSync.ts | 120 ++++++----------- .../frontendResourceTypeBoundaries.test.ts | 23 +++- .../release_control/subsystem_lookup_test.py | 2 + 8 files changed, 343 insertions(+), 115 deletions(-) create mode 100644 frontend-modern/src/components/Dashboard/__tests__/dashboardWorkloadUrlSyncModel.test.ts create mode 100644 frontend-modern/src/components/Dashboard/dashboardWorkloadUrlSyncModel.ts 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 c1f8ba447..88c119cfb 100644 --- a/docs/release-control/v6/internal/subsystems/performance-and-scalability.md +++ b/docs/release-control/v6/internal/subsystems/performance-and-scalability.md @@ -71,32 +71,34 @@ regression protection. 49. `frontend-modern/src/components/Dashboard/workloadSelectors.ts` 50. `frontend-modern/src/components/Dashboard/workloadTopology.ts` 51. `frontend-modern/src/components/Dashboard/dashboardWorkloadRouteModel.ts` -52. `frontend-modern/src/components/Dashboard/useDashboardWorkloadFilterOptions.ts` -53. `frontend-modern/src/components/Dashboard/__tests__/dashboardWorkloadRouteModel.test.ts` -54. `frontend-modern/src/components/Dashboard/__tests__/workloadTopology.test.ts` -55. `frontend-modern/src/components/Infrastructure/UnifiedResourceTable.tsx` -56. `frontend-modern/src/components/Infrastructure/useUnifiedResourceTableState.ts` -57. `frontend-modern/src/components/Infrastructure/infrastructureSelectors.ts` -58. `frontend-modern/src/components/Infrastructure/resourceDetailMappers.ts` -59. `frontend-modern/src/components/Dashboard/__tests__/Dashboard.performance.contract.test.tsx` -60. `frontend-modern/src/components/Dashboard/__tests__/DashboardFilter.test.tsx` -61. `frontend-modern/src/components/Dashboard/__tests__/useDashboardFilterState.test.ts` -62. `frontend-modern/src/components/Dashboard/__tests__/useDashboardSelectionState.test.ts` -63. `frontend-modern/src/components/Dashboard/MetricBar.test.tsx` -64. `frontend-modern/src/components/Dashboard/__tests__/useMetricBarState.test.tsx` -65. `frontend-modern/src/components/Dashboard/__tests__/EnhancedCPUBar.test.tsx` -66. `frontend-modern/src/components/Dashboard/__tests__/useEnhancedCPUBarState.test.tsx` -67. `frontend-modern/src/components/Dashboard/ThresholdSlider.test.tsx` -68. `frontend-modern/src/components/Dashboard/__tests__/useThresholdSliderState.test.ts` -69. `frontend-modern/src/components/Dashboard/__tests__/StackedDiskBar.test.tsx` -70. `frontend-modern/src/components/Dashboard/__tests__/useStackedDiskBarState.test.tsx` -71. `frontend-modern/src/components/Dashboard/StackedMemoryBar.test.tsx` -72. `frontend-modern/src/components/Dashboard/__tests__/useStackedMemoryBarState.test.tsx` -73. `frontend-modern/src/components/Dashboard/__tests__/DiskList.test.tsx` -74. `frontend-modern/src/components/Dashboard/__tests__/GuestRow.test.tsx` -75. `frontend-modern/src/components/Dashboard/GuestDrawer.test.tsx` -76. `frontend-modern/src/components/Dashboard/__tests__/useGroupedTableWindowing.test.ts` -77. `frontend-modern/src/components/Infrastructure/__tests__/UnifiedResourceTable.performance.contract.test.tsx` +52. `frontend-modern/src/components/Dashboard/dashboardWorkloadUrlSyncModel.ts` +53. `frontend-modern/src/components/Dashboard/useDashboardWorkloadFilterOptions.ts` +54. `frontend-modern/src/components/Dashboard/__tests__/dashboardWorkloadRouteModel.test.ts` +55. `frontend-modern/src/components/Dashboard/__tests__/dashboardWorkloadUrlSyncModel.test.ts` +56. `frontend-modern/src/components/Dashboard/__tests__/workloadTopology.test.ts` +57. `frontend-modern/src/components/Infrastructure/UnifiedResourceTable.tsx` +58. `frontend-modern/src/components/Infrastructure/useUnifiedResourceTableState.ts` +59. `frontend-modern/src/components/Infrastructure/infrastructureSelectors.ts` +60. `frontend-modern/src/components/Infrastructure/resourceDetailMappers.ts` +61. `frontend-modern/src/components/Dashboard/__tests__/Dashboard.performance.contract.test.tsx` +62. `frontend-modern/src/components/Dashboard/__tests__/DashboardFilter.test.tsx` +63. `frontend-modern/src/components/Dashboard/__tests__/useDashboardFilterState.test.ts` +64. `frontend-modern/src/components/Dashboard/__tests__/useDashboardSelectionState.test.ts` +65. `frontend-modern/src/components/Dashboard/MetricBar.test.tsx` +66. `frontend-modern/src/components/Dashboard/__tests__/useMetricBarState.test.tsx` +67. `frontend-modern/src/components/Dashboard/__tests__/EnhancedCPUBar.test.tsx` +68. `frontend-modern/src/components/Dashboard/__tests__/useEnhancedCPUBarState.test.tsx` +69. `frontend-modern/src/components/Dashboard/ThresholdSlider.test.tsx` +70. `frontend-modern/src/components/Dashboard/__tests__/useThresholdSliderState.test.ts` +71. `frontend-modern/src/components/Dashboard/__tests__/StackedDiskBar.test.tsx` +72. `frontend-modern/src/components/Dashboard/__tests__/useStackedDiskBarState.test.tsx` +73. `frontend-modern/src/components/Dashboard/StackedMemoryBar.test.tsx` +74. `frontend-modern/src/components/Dashboard/__tests__/useStackedMemoryBarState.test.tsx` +75. `frontend-modern/src/components/Dashboard/__tests__/DiskList.test.tsx` +76. `frontend-modern/src/components/Dashboard/__tests__/GuestRow.test.tsx` +77. `frontend-modern/src/components/Dashboard/GuestDrawer.test.tsx` +78. `frontend-modern/src/components/Dashboard/__tests__/useGroupedTableWindowing.test.ts` +79. `frontend-modern/src/components/Infrastructure/__tests__/UnifiedResourceTable.performance.contract.test.tsx` ## Shared Boundaries @@ -121,7 +123,7 @@ regression protection. 11. Extend dashboard disk-list derivations and fallback runtime wiring through `frontend-modern/src/components/Dashboard/diskListModel.ts` and `frontend-modern/src/components/Dashboard/useDiskListState.ts` rather than rebuilding usage math, progress-state mapping, or tooltip fallback logic inside `frontend-modern/src/components/Dashboard/DiskList.tsx` 12. Extend dashboard guest metadata cache persistence, metadata refresh, org-scope switching, and optimistic custom-URL updates through `frontend-modern/src/components/Dashboard/useDashboardGuestMetadataState.ts` rather than rebuilding dashboard-local storage caches, event listeners, or guest metadata API wiring inside `frontend-modern/src/components/Dashboard/useDashboardState.ts` 13. Extend dashboard deep-link selection, hovered-row continuity, and table scroll preservation through `frontend-modern/src/components/Dashboard/useDashboardSelectionState.ts` rather than rebuilding resource-query parsing, selected-row scroll pinning, or hovered-row invalidation inside `frontend-modern/src/components/Dashboard/useDashboardState.ts` -14. Extend dashboard workload route ownership, route-driven option catalogs, and toolbar filter config through `frontend-modern/src/components/Dashboard/useDashboardWorkloadRouteState.ts`, `frontend-modern/src/components/Dashboard/useDashboardWorkloadFilterOptions.ts`, and `frontend-modern/src/components/Dashboard/dashboardWorkloadRouteModel.ts`, and extend query-param synchronization through `frontend-modern/src/components/Dashboard/useDashboardWorkloadUrlSync.ts`, rather than rebuilding route sync, alias parsing, option derivation, or managed workload URLs inside `frontend-modern/src/components/Dashboard/useDashboardState.ts` +14. Extend dashboard workload route ownership, route-driven option catalogs, and toolbar filter config through `frontend-modern/src/components/Dashboard/useDashboardWorkloadRouteState.ts`, `frontend-modern/src/components/Dashboard/useDashboardWorkloadFilterOptions.ts`, and `frontend-modern/src/components/Dashboard/dashboardWorkloadRouteModel.ts`, and extend query-param synchronization plus managed workload URL semantics through `frontend-modern/src/components/Dashboard/useDashboardWorkloadUrlSync.ts` and `frontend-modern/src/components/Dashboard/dashboardWorkloadUrlSyncModel.ts`, rather than rebuilding route sync, alias parsing, option derivation, param precedence, or managed workload URLs inside `frontend-modern/src/components/Dashboard/useDashboardState.ts` 15. Extend grouped dashboard workload derivation, summary fallbacks, and grouped/windowed table presentation through `frontend-modern/src/components/Dashboard/useDashboardWorkloadDerivedState.ts`, and extend node parent mapping through `frontend-modern/src/components/Dashboard/workloadTopology.ts`, rather than rebuilding grouped selectors, summary snapshot math, or topology lookups inside `frontend-modern/src/components/Dashboard/useDashboardState.ts` 16. Extend dashboard control defaults, persistent view preferences, keyboard reset behavior, column-visibility ownership, and tag-search flow through `frontend-modern/src/components/Dashboard/useDashboardControlsState.ts` and `frontend-modern/src/components/Dashboard/dashboardFilterModel.ts` rather than rebuilding sort/search/grouping state, reset drift, or column-toggle plumbing inside `frontend-modern/src/components/Dashboard/useDashboardState.ts` 17. Extend dashboard filter active-count, reset semantics, and mobile toolbar state through `frontend-modern/src/components/Dashboard/dashboardFilterModel.ts` and `frontend-modern/src/components/Dashboard/useDashboardFilterState.ts`, rather than rebuilding filter-local state inside `frontend-modern/src/components/Dashboard/DashboardFilter.tsx` diff --git a/docs/release-control/v6/internal/subsystems/registry.json b/docs/release-control/v6/internal/subsystems/registry.json index 79becd6b0..fa6aca2b3 100644 --- a/docs/release-control/v6/internal/subsystems/registry.json +++ b/docs/release-control/v6/internal/subsystems/registry.json @@ -2560,6 +2560,7 @@ "frontend-modern/src/components/Dashboard/DashboardStatsStrip.tsx", "frontend-modern/src/components/Dashboard/dashboardWorkloadRouteModel.ts", "frontend-modern/src/components/Dashboard/DashboardWorkloadTable.tsx", + "frontend-modern/src/components/Dashboard/dashboardWorkloadUrlSyncModel.ts", "frontend-modern/src/components/Dashboard/DiskList.tsx", "frontend-modern/src/components/Dashboard/diskListModel.ts", "frontend-modern/src/components/Dashboard/EnhancedCPUBar.tsx", @@ -2615,6 +2616,7 @@ "frontend-modern/src/components/Dashboard/__tests__/Dashboard.performance.contract.test.tsx", "frontend-modern/src/components/Dashboard/__tests__/DashboardFilter.test.tsx", "frontend-modern/src/components/Dashboard/__tests__/dashboardWorkloadRouteModel.test.ts", + "frontend-modern/src/components/Dashboard/__tests__/dashboardWorkloadUrlSyncModel.test.ts", "frontend-modern/src/components/Dashboard/__tests__/DiskList.test.tsx", "frontend-modern/src/components/Dashboard/__tests__/EnhancedCPUBar.test.tsx", "frontend-modern/src/components/Dashboard/__tests__/StackedDiskBar.test.tsx", @@ -2665,6 +2667,7 @@ "frontend-modern/src/components/Dashboard/DashboardStatsStrip.tsx", "frontend-modern/src/components/Dashboard/dashboardWorkloadRouteModel.ts", "frontend-modern/src/components/Dashboard/DashboardWorkloadTable.tsx", + "frontend-modern/src/components/Dashboard/dashboardWorkloadUrlSyncModel.ts", "frontend-modern/src/components/Dashboard/DiskList.tsx", "frontend-modern/src/components/Dashboard/diskListModel.ts", "frontend-modern/src/components/Dashboard/EnhancedCPUBar.tsx", @@ -2716,6 +2719,7 @@ "frontend-modern/src/components/Dashboard/__tests__/Dashboard.performance.contract.test.tsx", "frontend-modern/src/components/Dashboard/__tests__/DashboardFilter.test.tsx", "frontend-modern/src/components/Dashboard/__tests__/dashboardWorkloadRouteModel.test.ts", + "frontend-modern/src/components/Dashboard/__tests__/dashboardWorkloadUrlSyncModel.test.ts", "frontend-modern/src/components/Dashboard/__tests__/DiskList.test.tsx", "frontend-modern/src/components/Dashboard/__tests__/EnhancedCPUBar.test.tsx", "frontend-modern/src/components/Dashboard/__tests__/GuestRow.test.tsx", diff --git a/frontend-modern/src/components/Dashboard/__tests__/Dashboard.performance.contract.test.tsx b/frontend-modern/src/components/Dashboard/__tests__/Dashboard.performance.contract.test.tsx index f84f9575d..610364d55 100644 --- a/frontend-modern/src/components/Dashboard/__tests__/Dashboard.performance.contract.test.tsx +++ b/frontend-modern/src/components/Dashboard/__tests__/Dashboard.performance.contract.test.tsx @@ -17,6 +17,7 @@ import dashboardSelectionStateSource from '../useDashboardSelectionState.ts?raw' import dashboardWorkloadDerivedStateSource from '../useDashboardWorkloadDerivedState.ts?raw'; import dashboardWorkloadFilterOptionsSource from '../useDashboardWorkloadFilterOptions.ts?raw'; import dashboardWorkloadRouteModelSource from '../dashboardWorkloadRouteModel.ts?raw'; +import dashboardWorkloadUrlSyncModelSource from '../dashboardWorkloadUrlSyncModel.ts?raw'; import dashboardWorkloadRouteStateSource from '../useDashboardWorkloadRouteState.ts?raw'; import dashboardWorkloadUrlSyncSource from '../useDashboardWorkloadUrlSync.ts?raw'; import dashboardStateSource from '../useDashboardState.ts?raw'; @@ -558,15 +559,33 @@ describe('Dashboard performance contract', () => { expect(dashboardWorkloadRouteModelSource).toContain( "normalizeWorkloadViewModeParam(raw) ?? 'all'", ); - expect(dashboardWorkloadUrlSyncSource).toContain('buildWorkloadsPath({'); - expect(dashboardWorkloadUrlSyncSource).toContain('normalizeWorkloadViewModeParam'); + expect(dashboardWorkloadUrlSyncSource).not.toContain('buildWorkloadsPath({'); + expect(dashboardWorkloadUrlSyncSource).not.toContain('normalizeWorkloadViewModeParam'); + expect(dashboardWorkloadUrlSyncSource).not.toContain('parseWorkloadsLinkSearch'); expect(dashboardWorkloadUrlSyncSource).toContain( "from './dashboardWorkloadRouteModel'", ); + expect(dashboardWorkloadUrlSyncSource).toContain( + "from './dashboardWorkloadUrlSyncModel'", + ); expect(dashboardWorkloadUrlSyncSource).toContain( 'const [handledTypeParam, setHandledTypeParam]', ); - expect(dashboardWorkloadUrlSyncSource).toContain('parseWorkloadsLinkSearch'); + expect(dashboardWorkloadUrlSyncSource).toContain('parseDashboardWorkloadUrlParams'); + expect(dashboardWorkloadUrlSyncSource).toContain( + 'resolveDashboardManagedWorkloadsNavigateTarget({', + ); + expect(dashboardWorkloadUrlSyncModelSource).toContain('parseWorkloadsLinkSearch(search)'); + expect(dashboardWorkloadUrlSyncModelSource).toContain('buildWorkloadsPath({'); + expect(dashboardWorkloadUrlSyncModelSource).toContain( + 'resolveDashboardManagedWorkloadsNavigateTarget', + ); + expect(dashboardWorkloadUrlSyncModelSource).toContain( + 'resolveDashboardWorkloadRuntimeParam', + ); + expect(dashboardWorkloadUrlSyncModelSource).toContain( + 'normalizeWorkloadViewModeParam(params.type)', + ); expect(dashboardControlsStateSource).toContain('useBreakpoint'); expect(dashboardControlsStateSource).toContain('useColumnVisibility'); expect(dashboardControlsStateSource).toContain('usePersistentSignal'); diff --git a/frontend-modern/src/components/Dashboard/__tests__/dashboardWorkloadUrlSyncModel.test.ts b/frontend-modern/src/components/Dashboard/__tests__/dashboardWorkloadUrlSyncModel.test.ts new file mode 100644 index 000000000..3348a5f3a --- /dev/null +++ b/frontend-modern/src/components/Dashboard/__tests__/dashboardWorkloadUrlSyncModel.test.ts @@ -0,0 +1,106 @@ +import { describe, expect, it } from 'vitest'; + +import { + parseDashboardWorkloadUrlParams, + resolveDashboardManagedWorkloadsNavigateTarget, + resolveDashboardWorkloadRuntimeParam, + resolveDashboardWorkloadTypeParam, +} from '../dashboardWorkloadUrlSyncModel'; + +describe('dashboardWorkloadUrlSyncModel', () => { + it('parses canonical workload route params and keeps resource deep links intact', () => { + expect( + parseDashboardWorkloadUrlParams( + '?type=docker&runtime=containerd&context=prod&namespace=default&agent=node-a&resource=guest-1', + ), + ).toEqual({ + type: 'docker', + runtime: 'containerd', + context: 'prod', + namespace: 'default', + agent: 'node-a', + resource: 'guest-1', + }); + }); + + it('resolves workload type params through the canonical alias rules and k8s precedence', () => { + expect( + resolveDashboardWorkloadTypeParam({ + type: 'docker', + runtime: '', + context: '', + namespace: '', + agent: '', + resource: '', + }), + ).toBe('app-container'); + expect( + resolveDashboardWorkloadTypeParam({ + type: 'vm', + runtime: '', + context: 'prod', + namespace: '', + agent: '', + resource: '', + }), + ).toBeNull(); + }); + + it('only applies runtime params when the url semantics still resolve to app-container scope', () => { + expect( + resolveDashboardWorkloadRuntimeParam({ + type: 'docker', + runtime: 'containerd', + context: '', + namespace: '', + agent: '', + resource: '', + }), + ).toEqual({ + forceViewMode: 'app-container', + runtime: 'containerd', + shouldApply: true, + }); + + expect( + resolveDashboardWorkloadRuntimeParam({ + type: 'vm', + runtime: 'containerd', + context: 'prod', + namespace: '', + agent: '', + resource: '', + }), + ).toEqual({ + forceViewMode: null, + runtime: 'containerd', + shouldApply: false, + }); + }); + + it('builds managed workload navigate targets without dropping unrelated resource params', () => { + expect( + resolveDashboardManagedWorkloadsNavigateTarget({ + currentSearch: '?resource=guest-1&type=vm&agent=node-a', + viewMode: 'pod', + containerRuntime: 'docker', + selectedKubernetesContext: 'prod', + selectedKubernetesNamespace: 'default', + selectedNode: 'cluster-a-node-a', + selectedHostHint: null, + }), + ).toBe('/workloads?resource=guest-1&type=pod&context=prod&namespace=default'); + + expect( + resolveDashboardManagedWorkloadsNavigateTarget({ + currentSearch: '?type=pod&context=prod&namespace=default', + viewMode: 'pod', + containerRuntime: '', + selectedKubernetesContext: 'prod', + selectedKubernetesNamespace: 'default', + selectedNode: null, + selectedHostHint: null, + }), + ).toBeNull(); + }); +}); diff --git a/frontend-modern/src/components/Dashboard/dashboardWorkloadUrlSyncModel.ts b/frontend-modern/src/components/Dashboard/dashboardWorkloadUrlSyncModel.ts new file mode 100644 index 000000000..3d66a829b --- /dev/null +++ b/frontend-modern/src/components/Dashboard/dashboardWorkloadUrlSyncModel.ts @@ -0,0 +1,122 @@ +import type { ViewMode } from '@/types/workloads'; +import { + buildWorkloadsPath, + parseWorkloadsLinkSearch, + WORKLOADS_PATH, + WORKLOADS_QUERY_PARAMS, +} from '@/routing/resourceLinks'; +import { areSearchParamsEquivalent } from '@/utils/searchParams'; +import { normalizeWorkloadViewModeParam } from '@/utils/workloads'; + +export interface DashboardWorkloadUrlParams { + type: string; + runtime: string; + context: string; + namespace: string; + agent: string; + resource: string; +} + +export interface DashboardWorkloadRuntimeParamResolution { + forceViewMode: ViewMode | null; + runtime: string; + shouldApply: boolean; +} + +interface DashboardManagedWorkloadsNavigateTargetOptions { + containerRuntime: string; + currentSearch: string; + selectedHostHint: string | null; + selectedKubernetesContext: string | null; + selectedKubernetesNamespace: string | null; + selectedNode: string | null; + viewMode: ViewMode; +} + +export const parseDashboardWorkloadUrlParams = (search: string): DashboardWorkloadUrlParams => + parseWorkloadsLinkSearch(search); + +const hasDashboardWorkloadKubernetesScope = (params: DashboardWorkloadUrlParams): boolean => + Boolean(params.context.trim()) || Boolean(params.namespace.trim()); + +export const resolveDashboardWorkloadTypeParam = ( + params: DashboardWorkloadUrlParams, +): ViewMode | null => { + const nextMode = normalizeWorkloadViewModeParam(params.type); + if (!nextMode) return null; + if (hasDashboardWorkloadKubernetesScope(params) && nextMode !== 'pod') return null; + return nextMode; +}; + +export const resolveDashboardWorkloadRuntimeParam = ( + params: DashboardWorkloadUrlParams, +): DashboardWorkloadRuntimeParamResolution => { + const nextMode = resolveDashboardWorkloadTypeParam(params); + const runtimeRelevant = + !hasDashboardWorkloadKubernetesScope(params) && + (nextMode === 'app-container' || !params.type.trim()); + + if (!runtimeRelevant) { + return { + forceViewMode: null, + runtime: params.runtime, + shouldApply: false, + }; + } + + if (!params.runtime.trim()) { + return { + forceViewMode: null, + runtime: '', + shouldApply: true, + }; + } + + return { + forceViewMode: 'app-container', + runtime: params.runtime, + shouldApply: true, + }; +}; + +export const resolveDashboardManagedWorkloadsNavigateTarget = ({ + containerRuntime, + currentSearch, + selectedHostHint, + selectedKubernetesContext, + selectedKubernetesNamespace, + selectedNode, + viewMode, +}: DashboardManagedWorkloadsNavigateTargetOptions): string | null => { + const currentParams = new URLSearchParams(currentSearch); + const nextParams = new URLSearchParams(currentSearch); + const nextType = viewMode === 'all' ? '' : viewMode; + const nextRuntime = viewMode === 'app-container' ? containerRuntime.trim() : ''; + const nextContext = viewMode === 'pod' ? (selectedKubernetesContext ?? '') : ''; + const nextNamespace = viewMode === 'pod' ? (selectedKubernetesNamespace ?? '') : ''; + const nextAgent = viewMode === 'pod' ? '' : (selectedNode ?? selectedHostHint ?? ''); + + const managedPath = buildWorkloadsPath({ + type: nextType || null, + runtime: nextRuntime || null, + context: nextContext || null, + namespace: nextNamespace || null, + agent: nextAgent || null, + }); + const managedUrl = new URL(managedPath, 'http://pulse.local'); + nextParams.delete(WORKLOADS_QUERY_PARAMS.type); + nextParams.delete(WORKLOADS_QUERY_PARAMS.runtime); + nextParams.delete(WORKLOADS_QUERY_PARAMS.context); + nextParams.delete(WORKLOADS_QUERY_PARAMS.namespace); + nextParams.delete(WORKLOADS_QUERY_PARAMS.agent); + managedUrl.searchParams.forEach((value, key) => { + nextParams.set(key, value); + }); + + if (areSearchParamsEquivalent(currentParams, nextParams)) { + return null; + } + + const nextSearch = nextParams.toString(); + return nextSearch ? `${WORKLOADS_PATH}?${nextSearch}` : WORKLOADS_PATH; +}; diff --git a/frontend-modern/src/components/Dashboard/useDashboardWorkloadUrlSync.ts b/frontend-modern/src/components/Dashboard/useDashboardWorkloadUrlSync.ts index ee0a03b5b..f9c30e3e1 100644 --- a/frontend-modern/src/components/Dashboard/useDashboardWorkloadUrlSync.ts +++ b/frontend-modern/src/components/Dashboard/useDashboardWorkloadUrlSync.ts @@ -1,4 +1,5 @@ import { + createMemo, createEffect, createSignal, onCleanup, @@ -9,14 +10,15 @@ import { import { useLocation, useNavigate } from '@solidjs/router'; import type { ViewMode } from '@/types/workloads'; import { - buildWorkloadsPath, - parseWorkloadsLinkSearch, WORKLOADS_PATH, - WORKLOADS_QUERY_PARAMS, } from '@/routing/resourceLinks'; -import { areSearchParamsEquivalent } from '@/utils/searchParams'; -import { normalizeWorkloadViewModeParam } from '@/utils/workloads'; import type { DashboardWorkloadNodeOption } from './dashboardWorkloadRouteModel'; +import { + parseDashboardWorkloadUrlParams, + resolveDashboardManagedWorkloadsNavigateTarget, + resolveDashboardWorkloadRuntimeParam, + resolveDashboardWorkloadTypeParam, +} from './dashboardWorkloadUrlSyncModel'; export interface DashboardWorkloadUrlSyncOptions { containerRuntime: Accessor; @@ -42,6 +44,7 @@ export function useDashboardWorkloadUrlSync(options: DashboardWorkloadUrlSyncOpt const navigate = useNavigate(); const location = useLocation(); const isWorkloadsRoute = () => location.pathname === WORKLOADS_PATH; + const workloadUrlParams = createMemo(() => parseDashboardWorkloadUrlParams(location.search)); const [handledTypeParam, setHandledTypeParam] = createSignal(''); const [handledRuntimeParam, setHandledRuntimeParam] = createSignal(''); @@ -143,9 +146,8 @@ export function useDashboardWorkloadUrlSync(options: DashboardWorkloadUrlSyncOpt }); createEffect(() => { - const parsed = parseWorkloadsLinkSearch(location.search); - const typeParam = parsed.type; - const normalizedType = typeParam ?? ''; + const parsed = workloadUrlParams(); + const normalizedType = parsed.type; if (normalizedType === handledTypeParam()) return; if (!normalizedType) { @@ -153,25 +155,18 @@ export function useDashboardWorkloadUrlSync(options: DashboardWorkloadUrlSyncOpt return; } - const hasK8sScope = - Boolean((parsed.context ?? '').trim()) || Boolean((parsed.namespace ?? '').trim()); - const nextMode = normalizeWorkloadViewModeParam(normalizedType); + const nextMode = resolveDashboardWorkloadTypeParam(parsed); if (!nextMode) { setHandledTypeParam(normalizedType); return; } - if (hasK8sScope && nextMode !== 'pod') { - setHandledTypeParam(normalizedType); - return; - } options.setViewMode(nextMode); setHandledTypeParam(normalizedType); }); createEffect(() => { - const { context: contextParam } = parseWorkloadsLinkSearch(location.search); - const normalized = contextParam ?? ''; + const normalized = workloadUrlParams().context; if (normalized === handledContextParam()) return; if (normalized) { @@ -191,8 +186,7 @@ export function useDashboardWorkloadUrlSync(options: DashboardWorkloadUrlSyncOpt }); createEffect(() => { - const { namespace: namespaceParam } = parseWorkloadsLinkSearch(location.search); - const normalized = namespaceParam ?? ''; + const normalized = workloadUrlParams().namespace; if (normalized === handledNamespaceParam()) return; if (normalized) { @@ -212,8 +206,7 @@ export function useDashboardWorkloadUrlSync(options: DashboardWorkloadUrlSyncOpt }); createEffect(() => { - const { agent: agentParam } = parseWorkloadsLinkSearch(location.search); - const normalized = agentParam ?? ''; + const normalized = workloadUrlParams().agent; if (normalized === handledAgentParam()) return; if (normalized) { @@ -233,34 +226,21 @@ export function useDashboardWorkloadUrlSync(options: DashboardWorkloadUrlSyncOpt }); createEffect(() => { - const parsed = parseWorkloadsLinkSearch(location.search); - const urlRuntime = parsed.runtime ?? ''; + const parsed = workloadUrlParams(); + const urlRuntime = parsed.runtime; if (urlRuntime === handledRuntimeParam()) return; - const urlContext = parsed.context ?? ''; - const hasContext = Boolean(urlContext.trim()); - const hasNamespace = Boolean((parsed.namespace ?? '').trim()); - const urlType = parsed.type ?? ''; - const nextMode = normalizeWorkloadViewModeParam(urlType); - const runtimeRelevant = - !hasContext && !hasNamespace && (nextMode === 'app-container' || !urlType.trim()); - - if (!runtimeRelevant) { + const resolution = resolveDashboardWorkloadRuntimeParam(parsed); + if (!resolution.shouldApply) { setHandledRuntimeParam(urlRuntime); return; } - if (!urlRuntime.trim()) { - options.setContainerRuntime(''); - setHandledRuntimeParam(''); - return; + if (resolution.forceViewMode && options.viewMode() !== resolution.forceViewMode) { + options.setViewMode(resolution.forceViewMode); } - - if (options.viewMode() !== 'app-container') { - options.setViewMode('app-container'); - } - options.setContainerRuntime(urlRuntime); - if (!options.showFilters()) { + options.setContainerRuntime(resolution.runtime); + if (resolution.runtime && !options.showFilters()) { options.setShowFilters(true); } setHandledRuntimeParam(urlRuntime); @@ -269,13 +249,13 @@ export function useDashboardWorkloadUrlSync(options: DashboardWorkloadUrlSyncOpt createEffect(() => { if (!isWorkloadsRoute()) return; - const parsed = parseWorkloadsLinkSearch(location.search); - const urlType = parsed.type ?? ''; - const urlRuntime = parsed.runtime ?? ''; - const urlContext = parsed.context ?? ''; - const urlNamespace = parsed.namespace ?? ''; - const urlAgent = parsed.agent ?? ''; - const urlResource = parsed.resource ?? ''; + const parsed = workloadUrlParams(); + const urlType = parsed.type; + const urlRuntime = parsed.runtime; + const urlContext = parsed.context; + const urlNamespace = parsed.namespace; + const urlAgent = parsed.agent; + const urlResource = parsed.resource; if (handledTypeParam() !== urlType) return; if (handledRuntimeParam() !== urlRuntime) return; @@ -284,40 +264,16 @@ export function useDashboardWorkloadUrlSync(options: DashboardWorkloadUrlSyncOpt if (handledAgentParam() !== urlAgent) return; if (urlResource) return; - const currentParams = new URLSearchParams(location.search); - const nextParams = new URLSearchParams(location.search); - const nextType = options.viewMode() === 'all' ? '' : options.viewMode(); - const nextRuntime = - options.viewMode() === 'app-container' ? options.containerRuntime().trim() : ''; - const nextContext = - options.viewMode() === 'pod' ? (options.selectedKubernetesContext() ?? '') : ''; - const nextNamespace = - options.viewMode() === 'pod' ? (options.selectedKubernetesNamespace() ?? '') : ''; - const nextAgent = - options.viewMode() === 'pod' - ? '' - : (options.selectedNode() ?? options.selectedHostHint() ?? ''); - - const managedPath = buildWorkloadsPath({ - type: nextType || null, - runtime: nextRuntime || null, - context: nextContext || null, - namespace: nextNamespace || null, - agent: nextAgent || null, + const nextPath = resolveDashboardManagedWorkloadsNavigateTarget({ + currentSearch: location.search, + viewMode: options.viewMode(), + containerRuntime: options.containerRuntime(), + selectedKubernetesContext: options.selectedKubernetesContext(), + selectedKubernetesNamespace: options.selectedKubernetesNamespace(), + selectedNode: options.selectedNode(), + selectedHostHint: options.selectedHostHint(), }); - const managedUrl = new URL(managedPath, 'http://pulse.local'); - nextParams.delete(WORKLOADS_QUERY_PARAMS.type); - nextParams.delete(WORKLOADS_QUERY_PARAMS.runtime); - nextParams.delete(WORKLOADS_QUERY_PARAMS.context); - nextParams.delete(WORKLOADS_QUERY_PARAMS.namespace); - nextParams.delete(WORKLOADS_QUERY_PARAMS.agent); - managedUrl.searchParams.forEach((value, key) => { - nextParams.set(key, value); - }); - - if (!areSearchParamsEquivalent(currentParams, nextParams)) { - const nextSearch = nextParams.toString(); - const nextPath = nextSearch ? `${WORKLOADS_PATH}?${nextSearch}` : WORKLOADS_PATH; + if (nextPath) { scheduleUrlSyncNavigate(nextPath); } }); diff --git a/frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts b/frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts index 924f33e30..da4a49439 100644 --- a/frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts +++ b/frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts @@ -93,6 +93,7 @@ import dashboardSelectionStateSource from '@/components/Dashboard/useDashboardSe import dashboardWorkloadDerivedStateSource from '@/components/Dashboard/useDashboardWorkloadDerivedState.ts?raw'; import dashboardWorkloadFilterOptionsSource from '@/components/Dashboard/useDashboardWorkloadFilterOptions.ts?raw'; import dashboardWorkloadRouteModelSource from '@/components/Dashboard/dashboardWorkloadRouteModel.ts?raw'; +import dashboardWorkloadUrlSyncModelSource from '@/components/Dashboard/dashboardWorkloadUrlSyncModel.ts?raw'; import dashboardWorkloadRouteStateSource from '@/components/Dashboard/useDashboardWorkloadRouteState.ts?raw'; import dashboardWorkloadUrlSyncSource from '@/components/Dashboard/useDashboardWorkloadUrlSync.ts?raw'; import dashboardStateSource from '@/components/Dashboard/useDashboardState.ts?raw'; @@ -566,13 +567,29 @@ describe('frontend resource type boundaries', () => { 'const containerRuntimeFilterConfig = createMemo', ); expect(dashboardWorkloadRouteStateSource).not.toContain('const [handledTypeParam, setHandledTypeParam]'); - expect(dashboardWorkloadUrlSyncSource).toContain('buildWorkloadsPath({'); - expect(dashboardWorkloadUrlSyncSource).toContain('normalizeWorkloadViewModeParam'); + expect(dashboardWorkloadUrlSyncSource).not.toContain('buildWorkloadsPath({'); + expect(dashboardWorkloadUrlSyncSource).not.toContain('normalizeWorkloadViewModeParam'); + expect(dashboardWorkloadUrlSyncSource).not.toContain('parseWorkloadsLinkSearch'); expect(dashboardWorkloadUrlSyncSource).toContain("from './dashboardWorkloadRouteModel'"); + expect(dashboardWorkloadUrlSyncSource).toContain("from './dashboardWorkloadUrlSyncModel'"); expect(dashboardWorkloadUrlSyncSource).toContain( 'const [handledTypeParam, setHandledTypeParam]', ); - expect(dashboardWorkloadUrlSyncSource).toContain('parseWorkloadsLinkSearch'); + expect(dashboardWorkloadUrlSyncSource).toContain('parseDashboardWorkloadUrlParams'); + expect(dashboardWorkloadUrlSyncSource).toContain( + 'resolveDashboardManagedWorkloadsNavigateTarget({', + ); + expect(dashboardWorkloadUrlSyncModelSource).toContain('parseWorkloadsLinkSearch(search)'); + expect(dashboardWorkloadUrlSyncModelSource).toContain('buildWorkloadsPath({'); + expect(dashboardWorkloadUrlSyncModelSource).toContain( + 'resolveDashboardManagedWorkloadsNavigateTarget', + ); + expect(dashboardWorkloadUrlSyncModelSource).toContain( + 'resolveDashboardWorkloadRuntimeParam', + ); + expect(dashboardWorkloadUrlSyncModelSource).toContain( + 'normalizeWorkloadViewModeParam(params.type)', + ); expect(dashboardControlsStateSource).toContain('useBreakpoint'); expect(dashboardControlsStateSource).toContain('useColumnVisibility'); expect(dashboardControlsStateSource).toContain('usePersistentSignal'); diff --git a/scripts/release_control/subsystem_lookup_test.py b/scripts/release_control/subsystem_lookup_test.py index f81977aab..371dc5b7d 100644 --- a/scripts/release_control/subsystem_lookup_test.py +++ b/scripts/release_control/subsystem_lookup_test.py @@ -1941,8 +1941,10 @@ class SubsystemLookupTest(unittest.TestCase): result = lookup_paths( [ "frontend-modern/src/components/Dashboard/dashboardWorkloadRouteModel.ts", + "frontend-modern/src/components/Dashboard/dashboardWorkloadUrlSyncModel.ts", "frontend-modern/src/components/Dashboard/useDashboardWorkloadFilterOptions.ts", "frontend-modern/src/components/Dashboard/useDashboardWorkloadRouteState.ts", + "frontend-modern/src/components/Dashboard/useDashboardWorkloadUrlSync.ts", ] ) self.assertEqual(result["unowned_runtime_files"], [])