mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-05-07 17:19:57 +00:00
Split infrastructure page route sync owner
This commit is contained in:
parent
44249ed048
commit
96b67fbcdd
10 changed files with 298 additions and 171 deletions
|
|
@ -153,8 +153,10 @@ Top-level route files are now also expected to stay thin when a feature owns
|
|||
the real product surface. `frontend-modern/src/pages/Infrastructure.tsx` now
|
||||
acts only as the route boundary, while
|
||||
`frontend-modern/src/features/infrastructure/InfrastructurePageSurface.tsx`
|
||||
and `frontend-modern/src/features/infrastructure/useInfrastructurePageState.ts`
|
||||
own the actual infrastructure page shell and state contract. Future feature
|
||||
owns the shell, `frontend-modern/src/features/infrastructure/useInfrastructurePageState.ts`
|
||||
owns page-control composition, and
|
||||
`frontend-modern/src/features/infrastructure/useInfrastructurePageRouteState.ts`
|
||||
owns infrastructure route/deep-link synchronization. Future feature
|
||||
surfaces under `frontend-modern/src/features/` should follow that same pattern
|
||||
instead of letting page files accumulate route sync, filter, and modal
|
||||
orchestration inline.
|
||||
|
|
|
|||
|
|
@ -3171,6 +3171,7 @@
|
|||
"frontend-modern/src/components/Infrastructure/useResourceDetailDrawerState.ts",
|
||||
"frontend-modern/src/components/Infrastructure/useUnifiedResourceTableState.ts",
|
||||
"frontend-modern/src/features/infrastructure/InfrastructurePageSurface.tsx",
|
||||
"frontend-modern/src/features/infrastructure/useInfrastructurePageRouteState.ts",
|
||||
"frontend-modern/src/features/infrastructure/useInfrastructurePageState.ts",
|
||||
"frontend-modern/src/hooks/useDashboardTrends.ts",
|
||||
"frontend-modern/src/hooks/useUnifiedResources.ts",
|
||||
|
|
@ -3255,6 +3256,7 @@
|
|||
"frontend-modern/src/components/Infrastructure/useResourceDetailDrawerState.ts",
|
||||
"frontend-modern/src/components/Infrastructure/useUnifiedResourceTableState.ts",
|
||||
"frontend-modern/src/features/infrastructure/InfrastructurePageSurface.tsx",
|
||||
"frontend-modern/src/features/infrastructure/useInfrastructurePageRouteState.ts",
|
||||
"frontend-modern/src/features/infrastructure/useInfrastructurePageState.ts",
|
||||
"frontend-modern/src/hooks/useDashboardTrends.ts",
|
||||
"frontend-modern/src/hooks/useUnifiedResources.ts",
|
||||
|
|
@ -3271,6 +3273,7 @@
|
|||
"frontend-modern/src/components/Infrastructure/__tests__/resourceDetailMappers.test.ts",
|
||||
"frontend-modern/src/components/Infrastructure/__tests__/UnifiedResourceTable.performance.contract.test.tsx",
|
||||
"frontend-modern/src/components/Infrastructure/__tests__/UnifiedResourceTable.workloads-link.test.tsx",
|
||||
"frontend-modern/src/features/infrastructure/__tests__/InfrastructurePageSurface.guardrails.test.ts",
|
||||
"frontend-modern/src/hooks/__tests__/useDashboardTrends.test.ts",
|
||||
"frontend-modern/src/hooks/__tests__/useUnifiedResources.test.ts",
|
||||
"frontend-modern/src/pages/__tests__/Infrastructure.empty-state.test.tsx",
|
||||
|
|
|
|||
|
|
@ -60,7 +60,8 @@ cross-source deduplication.
|
|||
38. `frontend-modern/src/components/Discovery/DiscoveryTab.tsx`
|
||||
39. `frontend-modern/src/components/Discovery/useDiscoveryTabState.ts`
|
||||
40. `frontend-modern/src/features/infrastructure/InfrastructurePageSurface.tsx`
|
||||
41. `frontend-modern/src/features/infrastructure/useInfrastructurePageState.ts`
|
||||
41. `frontend-modern/src/features/infrastructure/useInfrastructurePageRouteState.ts`
|
||||
42. `frontend-modern/src/features/infrastructure/useInfrastructurePageState.ts`
|
||||
|
||||
## Shared Boundaries
|
||||
|
||||
|
|
@ -772,6 +773,13 @@ The route file `frontend-modern/src/pages/Infrastructure.tsx` is now only the
|
|||
navigation boundary for that surface; canonical infrastructure filter, search,
|
||||
deep-link, and expansion state now live behind the dedicated infrastructure
|
||||
feature owner instead of accumulating in the page shell itself.
|
||||
That infrastructure feature now also follows an explicit shell/composition/route
|
||||
split: `frontend-modern/src/features/infrastructure/InfrastructurePageSurface.tsx`
|
||||
owns the render shell, `frontend-modern/src/features/infrastructure/useInfrastructurePageState.ts`
|
||||
owns page controls and filtered-resource composition, and
|
||||
`frontend-modern/src/features/infrastructure/useInfrastructurePageRouteState.ts`
|
||||
owns URL-sync, deep-link expansion, highlight continuity, and managed
|
||||
infrastructure-route navigation.
|
||||
Shared unified-resource consumers now also normalize org scope through
|
||||
`frontend-modern/src/utils/orgScope.ts` before building cache keys or
|
||||
multi-tenant resource fetch state, so the canonical resource hooks do not
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { For, Show } from 'solid-js';
|
||||
import { useNavigate } from '@solidjs/router';
|
||||
import { EmptyState } from '@/components/shared/EmptyState';
|
||||
import { Card } from '@/components/shared/Card';
|
||||
import { FilterSegmentedControl, LabeledFilterSelect } from '@/components/shared/FilterToolbar';
|
||||
|
|
@ -22,6 +23,7 @@ import {
|
|||
import { useInfrastructurePageState, type GroupingMode } from './useInfrastructurePageState';
|
||||
|
||||
export function InfrastructurePageSurface() {
|
||||
const navigate = useNavigate();
|
||||
const sourceOptions = DEFAULT_INFRASTRUCTURE_SOURCE_OPTIONS;
|
||||
const {
|
||||
loading,
|
||||
|
|
@ -59,7 +61,6 @@ export function InfrastructurePageSurface() {
|
|||
clearFilters,
|
||||
filteredResources,
|
||||
hasFilteredResources,
|
||||
handleNavigateToSettings,
|
||||
} = useInfrastructurePageState();
|
||||
|
||||
const infrastructureEmptyState = () => getInfrastructureEmptyState();
|
||||
|
|
@ -118,7 +119,7 @@ export function InfrastructurePageSurface() {
|
|||
actions={
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleNavigateToSettings}
|
||||
onClick={() => navigate('/settings')}
|
||||
class="inline-flex items-center gap-2 rounded-md border border-border bg-surface px-3 py-1.5 text-xs font-medium text-base-content shadow-sm hover:bg-slate-50"
|
||||
>
|
||||
<SettingsIcon class="h-3.5 w-3.5" />
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
import infrastructurePageSurfaceSource from '@/features/infrastructure/InfrastructurePageSurface.tsx?raw';
|
||||
import infrastructurePageStateSource from '@/features/infrastructure/useInfrastructurePageState.ts?raw';
|
||||
import infrastructurePageRouteStateSource from '@/features/infrastructure/useInfrastructurePageRouteState.ts?raw';
|
||||
|
||||
describe('InfrastructurePageSurface guardrails', () => {
|
||||
it('keeps the feature shell separate from route-sync ownership', () => {
|
||||
expect(infrastructurePageSurfaceSource).toContain('useInfrastructurePageState');
|
||||
expect(infrastructurePageSurfaceSource).toContain('useNavigate');
|
||||
expect(infrastructurePageSurfaceSource).not.toContain('useLocation(');
|
||||
expect(infrastructurePageSurfaceSource).not.toContain('buildInfrastructurePath(');
|
||||
|
||||
expect(infrastructurePageStateSource).toContain('useInfrastructurePageRouteState');
|
||||
expect(infrastructurePageStateSource).not.toContain('useLocation(');
|
||||
expect(infrastructurePageStateSource).not.toContain('useNavigate(');
|
||||
expect(infrastructurePageStateSource).not.toContain('parseInfrastructureLinkSearch(');
|
||||
expect(infrastructurePageStateSource).not.toContain('buildInfrastructurePath(');
|
||||
expect(infrastructurePageStateSource).not.toContain('areSearchParamsEquivalent(');
|
||||
|
||||
expect(infrastructurePageRouteStateSource).toContain('useLocation');
|
||||
expect(infrastructurePageRouteStateSource).toContain('useNavigate');
|
||||
expect(infrastructurePageRouteStateSource).toContain('parseInfrastructureLinkSearch');
|
||||
expect(infrastructurePageRouteStateSource).toContain('buildInfrastructurePath');
|
||||
expect(infrastructurePageRouteStateSource).toContain('areSearchParamsEquivalent');
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,192 @@
|
|||
import { createEffect, createSignal, onCleanup, type Accessor, type Setter, untrack } from 'solid-js';
|
||||
import { useLocation, useNavigate } from '@solidjs/router';
|
||||
import type { Resource } from '@/types/resource';
|
||||
import { normalizeSourcePlatformKey } from '@/utils/sourcePlatforms';
|
||||
import {
|
||||
buildInfrastructurePath,
|
||||
INFRASTRUCTURE_PATH,
|
||||
INFRASTRUCTURE_QUERY_PARAMS,
|
||||
parseInfrastructureLinkSearch,
|
||||
} from '@/routing/resourceLinks';
|
||||
import { areSearchParamsEquivalent } from '@/utils/searchParams';
|
||||
|
||||
interface InfrastructurePageRouteStateOptions {
|
||||
resources: Accessor<Resource[]>;
|
||||
filteredResources: Accessor<Resource[]>;
|
||||
initialLoadComplete: Accessor<boolean>;
|
||||
selectedSource: Accessor<string>;
|
||||
setSelectedSource: Setter<string>;
|
||||
searchQuery: Accessor<string>;
|
||||
setSearchQuery: Setter<string>;
|
||||
}
|
||||
|
||||
export function useInfrastructurePageRouteState(options: InfrastructurePageRouteStateOptions) {
|
||||
const {
|
||||
resources,
|
||||
filteredResources,
|
||||
initialLoadComplete,
|
||||
selectedSource,
|
||||
setSelectedSource,
|
||||
searchQuery,
|
||||
setSearchQuery,
|
||||
} = options;
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [expandedResourceId, setExpandedResourceId] = createSignal<string | null>(null);
|
||||
const [hoveredResourceId, setHoveredResourceId] = createSignal<string | null>(null);
|
||||
const [highlightedResourceId, setHighlightedResourceId] = createSignal<string | null>(null);
|
||||
const [handledResourceId, setHandledResourceId] = createSignal<string | null>(null);
|
||||
const [handledSourceParam, setHandledSourceParam] = createSignal<string | null>(null);
|
||||
const [handledQueryParam, setHandledQueryParam] = createSignal('');
|
||||
|
||||
let highlightTimer: number | undefined;
|
||||
let pendingUrlSyncHandle: number | null = null;
|
||||
let pendingUrlSyncPath: string | null = null;
|
||||
|
||||
const scheduleUrlSyncNavigate = (nextPath: string) => {
|
||||
pendingUrlSyncPath = nextPath;
|
||||
if (pendingUrlSyncHandle !== null) return;
|
||||
pendingUrlSyncHandle = window.setTimeout(() => {
|
||||
pendingUrlSyncHandle = null;
|
||||
const target = pendingUrlSyncPath;
|
||||
pendingUrlSyncPath = null;
|
||||
if (!target) return;
|
||||
const current = `${untrack(() => location.pathname)}${untrack(() => location.search)}`;
|
||||
if (current === target) return;
|
||||
navigate(target, { replace: true });
|
||||
}, 0);
|
||||
};
|
||||
|
||||
createEffect(() => {
|
||||
const { resource: resourceId } = parseInfrastructureLinkSearch(location.search);
|
||||
if (!resourceId || resourceId === handledResourceId()) return;
|
||||
const matching = resources().some((resource) => resource.id === resourceId);
|
||||
if (!matching) return;
|
||||
setExpandedResourceId(resourceId);
|
||||
setHighlightedResourceId(resourceId);
|
||||
setHandledResourceId(resourceId);
|
||||
if (highlightTimer) {
|
||||
window.clearTimeout(highlightTimer);
|
||||
}
|
||||
highlightTimer = window.setTimeout(() => {
|
||||
setHighlightedResourceId(null);
|
||||
}, 2000);
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
const { resource: resourceId } = parseInfrastructureLinkSearch(location.search);
|
||||
if (resourceId) return;
|
||||
if (handledResourceId() === null) return;
|
||||
|
||||
if (expandedResourceId() !== null) {
|
||||
setExpandedResourceId(null);
|
||||
}
|
||||
if (highlightedResourceId() !== null) {
|
||||
setHighlightedResourceId(null);
|
||||
}
|
||||
setHandledResourceId(null);
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
const { source: sourceParam } = parseInfrastructureLinkSearch(location.search);
|
||||
if (!sourceParam) {
|
||||
const previous = (handledSourceParam() ?? '').trim();
|
||||
if (previous) {
|
||||
if (selectedSource() !== '') setSelectedSource('');
|
||||
setHandledSourceParam('');
|
||||
} else if (handledSourceParam() === null) {
|
||||
setHandledSourceParam('');
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (sourceParam === handledSourceParam()) return;
|
||||
const normalized = normalizeSourcePlatformKey(sourceParam) ?? '';
|
||||
setSelectedSource(normalized);
|
||||
setHandledSourceParam(sourceParam);
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
const { query: nextSearch } = parseInfrastructureLinkSearch(location.search);
|
||||
const normalized = nextSearch ?? '';
|
||||
if (normalized !== handledQueryParam()) {
|
||||
if (normalized !== untrack(searchQuery)) {
|
||||
setSearchQuery(normalized);
|
||||
}
|
||||
setHandledQueryParam(normalized);
|
||||
}
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
if (location.pathname !== INFRASTRUCTURE_PATH) return;
|
||||
|
||||
const parsed = parseInfrastructureLinkSearch(location.search);
|
||||
const urlSource = parsed.source ?? '';
|
||||
const urlQuery = parsed.query ?? '';
|
||||
const urlResource = parsed.resource ?? '';
|
||||
if ((handledSourceParam() ?? '') !== urlSource) return;
|
||||
if (handledQueryParam() !== urlQuery) return;
|
||||
if (urlResource && handledResourceId() !== urlResource && !initialLoadComplete()) return;
|
||||
|
||||
const nextSource = selectedSource();
|
||||
const nextQuery = searchQuery().trim();
|
||||
const currentLinkedResource = parsed.resource;
|
||||
const selectedResourceId = expandedResourceId();
|
||||
const shouldPreserveIncomingResource =
|
||||
!selectedResourceId && Boolean(currentLinkedResource) && !initialLoadComplete();
|
||||
const nextResource = shouldPreserveIncomingResource
|
||||
? currentLinkedResource
|
||||
: (selectedResourceId ?? '');
|
||||
|
||||
const managedPath = buildInfrastructurePath({
|
||||
source: nextSource || null,
|
||||
query: nextQuery || null,
|
||||
resource: nextResource || null,
|
||||
});
|
||||
const managedUrl = new URL(managedPath, 'http://pulse.local');
|
||||
const currentParams = new URLSearchParams(location.search);
|
||||
const nextParams = new URLSearchParams(location.search);
|
||||
nextParams.delete(INFRASTRUCTURE_QUERY_PARAMS.source);
|
||||
nextParams.delete(INFRASTRUCTURE_QUERY_PARAMS.query);
|
||||
nextParams.delete(INFRASTRUCTURE_QUERY_PARAMS.resource);
|
||||
managedUrl.searchParams.forEach((value, key) => {
|
||||
nextParams.set(key, value);
|
||||
});
|
||||
|
||||
if (!areSearchParamsEquivalent(currentParams, nextParams)) {
|
||||
const nextSearch = nextParams.toString();
|
||||
const nextPath = nextSearch ? `${INFRASTRUCTURE_PATH}?${nextSearch}` : INFRASTRUCTURE_PATH;
|
||||
scheduleUrlSyncNavigate(nextPath);
|
||||
}
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
const hoveredId = hoveredResourceId();
|
||||
if (!hoveredId) return;
|
||||
const exists = filteredResources().some((resource) => resource.id === hoveredId);
|
||||
if (!exists) {
|
||||
setHoveredResourceId(null);
|
||||
}
|
||||
});
|
||||
|
||||
onCleanup(() => {
|
||||
if (pendingUrlSyncHandle !== null) {
|
||||
window.clearTimeout(pendingUrlSyncHandle);
|
||||
pendingUrlSyncHandle = null;
|
||||
pendingUrlSyncPath = null;
|
||||
}
|
||||
if (highlightTimer) {
|
||||
window.clearTimeout(highlightTimer);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
expandedResourceId,
|
||||
setExpandedResourceId,
|
||||
hoveredResourceId,
|
||||
setHoveredResourceId,
|
||||
highlightedResourceId,
|
||||
};
|
||||
}
|
||||
|
||||
export type InfrastructurePageRouteState = ReturnType<typeof useInfrastructurePageRouteState>;
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
import { createEffect, createMemo, createSignal, onCleanup, untrack } from 'solid-js';
|
||||
import { useLocation, useNavigate } from '@solidjs/router';
|
||||
import { createEffect, createMemo, createSignal } from 'solid-js';
|
||||
import type { TimeRange } from '@/api/charts';
|
||||
import { useUnifiedResources } from '@/hooks/useUnifiedResources';
|
||||
import { usePersistentSignal } from '@/hooks/usePersistentSignal';
|
||||
|
|
@ -14,14 +13,7 @@ import {
|
|||
collectAvailableStatuses,
|
||||
buildStatusOptions,
|
||||
} from '@/components/Infrastructure/infrastructureSelectors';
|
||||
import { normalizeSourcePlatformKey } from '@/utils/sourcePlatforms';
|
||||
import {
|
||||
buildInfrastructurePath,
|
||||
INFRASTRUCTURE_PATH,
|
||||
INFRASTRUCTURE_QUERY_PARAMS,
|
||||
parseInfrastructureLinkSearch,
|
||||
} from '@/routing/resourceLinks';
|
||||
import { areSearchParamsEquivalent } from '@/utils/searchParams';
|
||||
import { useInfrastructurePageRouteState } from './useInfrastructurePageRouteState';
|
||||
|
||||
export type GroupingMode = 'grouped' | 'flat';
|
||||
|
||||
|
|
@ -32,8 +24,6 @@ type DeployCluster = {
|
|||
|
||||
export function useInfrastructurePageState() {
|
||||
const { resources, loading, error, refetch } = useUnifiedResources();
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const kioskMode = useKioskMode();
|
||||
const { isMobile } = useBreakpoint();
|
||||
|
||||
|
|
@ -55,12 +45,6 @@ export function useInfrastructurePageState() {
|
|||
'grouped',
|
||||
{ deserialize: (raw) => (raw === 'grouped' || raw === 'flat' ? raw : 'grouped') },
|
||||
);
|
||||
const [expandedResourceId, setExpandedResourceId] = createSignal<string | null>(null);
|
||||
const [hoveredResourceId, setHoveredResourceId] = createSignal<string | null>(null);
|
||||
const [highlightedResourceId, setHighlightedResourceId] = createSignal<string | null>(null);
|
||||
const [handledResourceId, setHandledResourceId] = createSignal<string | null>(null);
|
||||
const [handledSourceParam, setHandledSourceParam] = createSignal<string | null>(null);
|
||||
const [handledQueryParam, setHandledQueryParam] = createSignal('');
|
||||
const [deployCluster, setDeployCluster] = createSignal<DeployCluster | null>(null);
|
||||
const [filtersOpen, setFiltersOpen] = createSignal(false);
|
||||
|
||||
|
|
@ -85,24 +69,15 @@ export function useInfrastructurePageState() {
|
|||
),
|
||||
);
|
||||
const hasFilteredResources = createMemo(() => filteredResources().length > 0);
|
||||
|
||||
let highlightTimer: number | undefined;
|
||||
let pendingUrlSyncHandle: number | null = null;
|
||||
let pendingUrlSyncPath: string | null = null;
|
||||
|
||||
const scheduleUrlSyncNavigate = (nextPath: string) => {
|
||||
pendingUrlSyncPath = nextPath;
|
||||
if (pendingUrlSyncHandle !== null) return;
|
||||
pendingUrlSyncHandle = window.setTimeout(() => {
|
||||
pendingUrlSyncHandle = null;
|
||||
const target = pendingUrlSyncPath;
|
||||
pendingUrlSyncPath = null;
|
||||
if (!target) return;
|
||||
const current = `${untrack(() => location.pathname)}${untrack(() => location.search)}`;
|
||||
if (current === target) return;
|
||||
navigate(target, { replace: true });
|
||||
}, 0);
|
||||
};
|
||||
const routeState = useInfrastructurePageRouteState({
|
||||
resources,
|
||||
filteredResources,
|
||||
initialLoadComplete,
|
||||
selectedSource,
|
||||
setSelectedSource,
|
||||
searchQuery,
|
||||
setSearchQuery,
|
||||
});
|
||||
|
||||
const clearFilters = () => {
|
||||
setSelectedSource('');
|
||||
|
|
@ -110,136 +85,12 @@ export function useInfrastructurePageState() {
|
|||
setSearchQuery('');
|
||||
};
|
||||
|
||||
const handleNavigateToSettings = () => navigate('/settings');
|
||||
|
||||
createEffect(() => {
|
||||
if (!loading() && !initialLoadComplete()) {
|
||||
setInitialLoadComplete(true);
|
||||
}
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
const { resource: resourceId } = parseInfrastructureLinkSearch(location.search);
|
||||
if (!resourceId || resourceId === handledResourceId()) return;
|
||||
const matching = resources().some((resource) => resource.id === resourceId);
|
||||
if (!matching) return;
|
||||
setExpandedResourceId(resourceId);
|
||||
setHighlightedResourceId(resourceId);
|
||||
setHandledResourceId(resourceId);
|
||||
if (highlightTimer) {
|
||||
window.clearTimeout(highlightTimer);
|
||||
}
|
||||
highlightTimer = window.setTimeout(() => {
|
||||
setHighlightedResourceId(null);
|
||||
}, 2000);
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
const { resource: resourceId } = parseInfrastructureLinkSearch(location.search);
|
||||
if (resourceId) return;
|
||||
if (handledResourceId() === null) return;
|
||||
|
||||
if (expandedResourceId() !== null) {
|
||||
setExpandedResourceId(null);
|
||||
}
|
||||
if (highlightedResourceId() !== null) {
|
||||
setHighlightedResourceId(null);
|
||||
}
|
||||
setHandledResourceId(null);
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
const { source: sourceParam } = parseInfrastructureLinkSearch(location.search);
|
||||
if (!sourceParam) {
|
||||
const previous = (handledSourceParam() ?? '').trim();
|
||||
if (previous) {
|
||||
if (selectedSource() !== '') setSelectedSource('');
|
||||
setHandledSourceParam('');
|
||||
} else if (handledSourceParam() === null) {
|
||||
setHandledSourceParam('');
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (sourceParam === handledSourceParam()) return;
|
||||
const normalized = normalizeSourcePlatformKey(sourceParam) ?? '';
|
||||
setSelectedSource(normalized);
|
||||
setHandledSourceParam(sourceParam);
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
const { query: nextSearch } = parseInfrastructureLinkSearch(location.search);
|
||||
const normalized = nextSearch ?? '';
|
||||
if (normalized !== handledQueryParam()) {
|
||||
if (normalized !== untrack(searchQuery)) {
|
||||
setSearchQuery(normalized);
|
||||
}
|
||||
setHandledQueryParam(normalized);
|
||||
}
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
if (location.pathname !== INFRASTRUCTURE_PATH) return;
|
||||
|
||||
const parsed = parseInfrastructureLinkSearch(location.search);
|
||||
const urlSource = parsed.source ?? '';
|
||||
const urlQuery = parsed.query ?? '';
|
||||
const urlResource = parsed.resource ?? '';
|
||||
if ((handledSourceParam() ?? '') !== urlSource) return;
|
||||
if (handledQueryParam() !== urlQuery) return;
|
||||
if (urlResource && handledResourceId() !== urlResource && !initialLoadComplete()) return;
|
||||
|
||||
const nextSource = selectedSource();
|
||||
const nextQuery = searchQuery().trim();
|
||||
const currentLinkedResource = parsed.resource;
|
||||
const selectedResourceId = expandedResourceId();
|
||||
const shouldPreserveIncomingResource =
|
||||
!selectedResourceId && Boolean(currentLinkedResource) && !initialLoadComplete();
|
||||
const nextResource = shouldPreserveIncomingResource
|
||||
? currentLinkedResource
|
||||
: (selectedResourceId ?? '');
|
||||
|
||||
const managedPath = buildInfrastructurePath({
|
||||
source: nextSource || null,
|
||||
query: nextQuery || null,
|
||||
resource: nextResource || null,
|
||||
});
|
||||
const managedUrl = new URL(managedPath, 'http://pulse.local');
|
||||
const currentParams = new URLSearchParams(location.search);
|
||||
const nextParams = new URLSearchParams(location.search);
|
||||
nextParams.delete(INFRASTRUCTURE_QUERY_PARAMS.source);
|
||||
nextParams.delete(INFRASTRUCTURE_QUERY_PARAMS.query);
|
||||
nextParams.delete(INFRASTRUCTURE_QUERY_PARAMS.resource);
|
||||
managedUrl.searchParams.forEach((value, key) => {
|
||||
nextParams.set(key, value);
|
||||
});
|
||||
|
||||
if (!areSearchParamsEquivalent(currentParams, nextParams)) {
|
||||
const nextSearch = nextParams.toString();
|
||||
const nextPath = nextSearch ? `${INFRASTRUCTURE_PATH}?${nextSearch}` : INFRASTRUCTURE_PATH;
|
||||
scheduleUrlSyncNavigate(nextPath);
|
||||
}
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
const hoveredId = hoveredResourceId();
|
||||
if (!hoveredId) return;
|
||||
const exists = filteredResources().some((resource) => resource.id === hoveredId);
|
||||
if (!exists) {
|
||||
setHoveredResourceId(null);
|
||||
}
|
||||
});
|
||||
|
||||
onCleanup(() => {
|
||||
if (pendingUrlSyncHandle !== null) {
|
||||
window.clearTimeout(pendingUrlSyncHandle);
|
||||
pendingUrlSyncHandle = null;
|
||||
pendingUrlSyncPath = null;
|
||||
}
|
||||
if (highlightTimer) {
|
||||
window.clearTimeout(highlightTimer);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
loading,
|
||||
error,
|
||||
|
|
@ -258,11 +109,7 @@ export function useInfrastructurePageState() {
|
|||
setSummaryCollapsed,
|
||||
groupingMode,
|
||||
setGroupingMode,
|
||||
expandedResourceId,
|
||||
setExpandedResourceId,
|
||||
hoveredResourceId,
|
||||
setHoveredResourceId,
|
||||
highlightedResourceId,
|
||||
...routeState,
|
||||
isMobile,
|
||||
deployCluster,
|
||||
setDeployCluster,
|
||||
|
|
@ -276,6 +123,5 @@ export function useInfrastructurePageState() {
|
|||
clearFilters,
|
||||
filteredResources,
|
||||
hasFilteredResources,
|
||||
handleNavigateToSettings,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -508,6 +508,7 @@ import aiChatPresentationSource from '@/utils/aiChatPresentation.ts?raw';
|
|||
import infrastructureDetailsDrawerSource from '@/components/shared/InfrastructureDetailsDrawer.tsx?raw';
|
||||
import infrastructurePageSurfaceSource from '@/features/infrastructure/InfrastructurePageSurface.tsx?raw';
|
||||
import infrastructurePageStateSource from '@/features/infrastructure/useInfrastructurePageState.ts?raw';
|
||||
import infrastructurePageRouteStateSource from '@/features/infrastructure/useInfrastructurePageRouteState.ts?raw';
|
||||
|
||||
const aiSettingsSource = [
|
||||
aiSettingsShellSource,
|
||||
|
|
@ -533,6 +534,7 @@ const resourceDetailDrawerSource = [
|
|||
const infrastructurePageSource = [
|
||||
infrastructurePageShellSource,
|
||||
infrastructurePageSurfaceSource,
|
||||
infrastructurePageRouteStateSource,
|
||||
infrastructurePageStateSource,
|
||||
].join('\n');
|
||||
|
||||
|
|
@ -3897,6 +3899,21 @@ describe('frontend resource type boundaries', () => {
|
|||
});
|
||||
|
||||
it('keeps infrastructure page empty-state copy in a shared presentation utility', () => {
|
||||
expect(infrastructurePageSurfaceSource).toContain('useInfrastructurePageState');
|
||||
expect(infrastructurePageSurfaceSource).toContain('useNavigate');
|
||||
expect(infrastructurePageSurfaceSource).not.toContain('useLocation(');
|
||||
expect(infrastructurePageSurfaceSource).not.toContain('buildInfrastructurePath(');
|
||||
expect(infrastructurePageStateSource).toContain('useInfrastructurePageRouteState');
|
||||
expect(infrastructurePageStateSource).not.toContain('useLocation(');
|
||||
expect(infrastructurePageStateSource).not.toContain('useNavigate(');
|
||||
expect(infrastructurePageStateSource).not.toContain('parseInfrastructureLinkSearch(');
|
||||
expect(infrastructurePageStateSource).not.toContain('buildInfrastructurePath(');
|
||||
expect(infrastructurePageStateSource).not.toContain('areSearchParamsEquivalent(');
|
||||
expect(infrastructurePageRouteStateSource).toContain('useLocation');
|
||||
expect(infrastructurePageRouteStateSource).toContain('useNavigate');
|
||||
expect(infrastructurePageRouteStateSource).toContain('parseInfrastructureLinkSearch');
|
||||
expect(infrastructurePageRouteStateSource).toContain('buildInfrastructurePath');
|
||||
expect(infrastructurePageRouteStateSource).toContain('areSearchParamsEquivalent');
|
||||
expect(infrastructurePageSource).toContain('getInfrastructureEmptyState');
|
||||
expect(infrastructurePageSource).toContain('getInfrastructureFilterEmptyState');
|
||||
expect(infrastructurePageSource).toContain('getInfrastructureLoadFailureState');
|
||||
|
|
|
|||
|
|
@ -504,6 +504,7 @@ class CanonicalCompletionGuardTest(unittest.TestCase):
|
|||
"frontend-modern/src/components/Infrastructure/__tests__/UnifiedResourceTable.workloads-link.test.tsx",
|
||||
"frontend-modern/src/components/Infrastructure/__tests__/infrastructureSelectors.test.ts",
|
||||
"frontend-modern/src/components/Infrastructure/__tests__/resourceDetailMappers.test.ts",
|
||||
"frontend-modern/src/features/infrastructure/__tests__/InfrastructurePageSurface.guardrails.test.ts",
|
||||
"frontend-modern/src/hooks/__tests__/useDashboardTrends.test.ts",
|
||||
"frontend-modern/src/hooks/__tests__/useUnifiedResources.test.ts",
|
||||
"frontend-modern/src/pages/__tests__/Infrastructure.empty-state.test.tsx",
|
||||
|
|
|
|||
|
|
@ -66,6 +66,36 @@ class SubsystemLookupTest(unittest.TestCase):
|
|||
"shared-component-guardrails",
|
||||
)
|
||||
|
||||
def test_lookup_paths_assigns_infrastructure_page_route_state_to_unified_resources(self) -> None:
|
||||
result = lookup_paths(
|
||||
["frontend-modern/src/features/infrastructure/useInfrastructurePageRouteState.ts"]
|
||||
)
|
||||
self.assertEqual(result["unowned_runtime_files"], [])
|
||||
self.assertEqual(
|
||||
{item["subsystem"] for item in result["impacted_subsystems"]},
|
||||
{"unified-resources"},
|
||||
)
|
||||
file_entry = result["files"][0]
|
||||
self.assertEqual(file_entry["classification"], "runtime")
|
||||
self.assertEqual(
|
||||
{match["subsystem"] for match in file_entry["matches"]},
|
||||
{"unified-resources"},
|
||||
)
|
||||
match = file_entry["matches"][0]
|
||||
self.assertEqual(
|
||||
match["contract"],
|
||||
"docs/release-control/v6/internal/subsystems/unified-resources.md",
|
||||
)
|
||||
self.assertEqual(match["lane_context"]["lane_id"], "L13")
|
||||
self.assertEqual(
|
||||
match["verification_requirement"]["id"],
|
||||
"resource-consumers",
|
||||
)
|
||||
self.assertIn(
|
||||
"frontend-modern/src/features/infrastructure/__tests__/InfrastructurePageSurface.guardrails.test.ts",
|
||||
match["verification_requirement"]["exact_files"],
|
||||
)
|
||||
|
||||
def test_lookup_paths_assigns_recent_alerts_panel_to_alerts(self) -> None:
|
||||
result = lookup_paths(["frontend-modern/src/components/Alerts/RecentAlertsPanel.tsx"])
|
||||
self.assertEqual(result["unowned_runtime_files"], [])
|
||||
|
|
@ -3909,6 +3939,7 @@ class SubsystemLookupTest(unittest.TestCase):
|
|||
"frontend-modern/src/components/Infrastructure/__tests__/UnifiedResourceTable.workloads-link.test.tsx",
|
||||
"frontend-modern/src/components/Infrastructure/__tests__/infrastructureSelectors.test.ts",
|
||||
"frontend-modern/src/components/Infrastructure/__tests__/resourceDetailMappers.test.ts",
|
||||
"frontend-modern/src/features/infrastructure/__tests__/InfrastructurePageSurface.guardrails.test.ts",
|
||||
"frontend-modern/src/hooks/__tests__/useDashboardTrends.test.ts",
|
||||
"frontend-modern/src/hooks/__tests__/useUnifiedResources.test.ts",
|
||||
"frontend-modern/src/pages/__tests__/Infrastructure.empty-state.test.tsx",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue