mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-05-10 03:51:54 +00:00
Extract dashboard workload route owner
This commit is contained in:
parent
cc771e1bd8
commit
92bd34a73c
7 changed files with 647 additions and 528 deletions
|
|
@ -28,57 +28,58 @@ regression protection.
|
|||
6. `frontend-modern/src/components/Dashboard/Dashboard.tsx`
|
||||
7. `frontend-modern/src/components/Dashboard/useDashboardState.ts`
|
||||
8. `frontend-modern/src/components/Dashboard/useDashboardGuestMetadataState.ts`
|
||||
9. `frontend-modern/src/components/Dashboard/DashboardFilter.tsx`
|
||||
10. `frontend-modern/src/components/Dashboard/dashboardFilterModel.ts`
|
||||
11. `frontend-modern/src/components/Dashboard/useDashboardFilterState.ts`
|
||||
12. `frontend-modern/src/components/Dashboard/ThresholdSlider.tsx`
|
||||
13. `frontend-modern/src/components/Dashboard/thresholdSliderModel.ts`
|
||||
14. `frontend-modern/src/components/Dashboard/useThresholdSliderState.ts`
|
||||
15. `frontend-modern/src/components/Dashboard/StackedDiskBar.tsx`
|
||||
16. `frontend-modern/src/components/Dashboard/stackedDiskBarModel.ts`
|
||||
17. `frontend-modern/src/components/Dashboard/useStackedDiskBarState.ts`
|
||||
18. `frontend-modern/src/components/Dashboard/StackedMemoryBar.tsx`
|
||||
19. `frontend-modern/src/components/Dashboard/stackedMemoryBarModel.ts`
|
||||
20. `frontend-modern/src/components/Dashboard/useStackedMemoryBarState.ts`
|
||||
21. `frontend-modern/src/components/Dashboard/MetricBar.tsx`
|
||||
22. `frontend-modern/src/components/Dashboard/metricBarModel.ts`
|
||||
23. `frontend-modern/src/components/Dashboard/useMetricBarState.ts`
|
||||
24. `frontend-modern/src/components/Dashboard/EnhancedCPUBar.tsx`
|
||||
25. `frontend-modern/src/components/Dashboard/enhancedCpuBarModel.ts`
|
||||
26. `frontend-modern/src/components/Dashboard/useEnhancedCPUBarState.ts`
|
||||
27. `frontend-modern/src/components/Dashboard/DiskList.tsx`
|
||||
28. `frontend-modern/src/components/Dashboard/diskListModel.ts`
|
||||
29. `frontend-modern/src/components/Dashboard/useDiskListState.ts`
|
||||
30. `frontend-modern/src/components/Dashboard/GuestRow.tsx`
|
||||
31. `frontend-modern/src/components/Dashboard/GuestRowCells.tsx`
|
||||
32. `frontend-modern/src/components/Dashboard/guestRowModel.tsx`
|
||||
33. `frontend-modern/src/components/Dashboard/useGuestRowState.ts`
|
||||
34. `frontend-modern/src/components/Dashboard/GuestDrawer.tsx`
|
||||
35. `frontend-modern/src/components/Dashboard/GuestDrawerOverview.tsx`
|
||||
36. `frontend-modern/src/components/Dashboard/guestDrawerModel.ts`
|
||||
37. `frontend-modern/src/components/Dashboard/useGuestDrawerState.ts`
|
||||
38. `frontend-modern/src/components/Dashboard/workloadSelectors.ts`
|
||||
39. `frontend-modern/src/components/Infrastructure/UnifiedResourceTable.tsx`
|
||||
40. `frontend-modern/src/components/Infrastructure/useUnifiedResourceTableState.ts`
|
||||
41. `frontend-modern/src/components/Infrastructure/infrastructureSelectors.ts`
|
||||
42. `frontend-modern/src/components/Infrastructure/resourceDetailMappers.ts`
|
||||
43. `frontend-modern/src/components/Dashboard/__tests__/Dashboard.performance.contract.test.tsx`
|
||||
44. `frontend-modern/src/components/Dashboard/__tests__/DashboardFilter.test.tsx`
|
||||
45. `frontend-modern/src/components/Dashboard/__tests__/useDashboardFilterState.test.ts`
|
||||
46. `frontend-modern/src/components/Dashboard/MetricBar.test.tsx`
|
||||
47. `frontend-modern/src/components/Dashboard/__tests__/useMetricBarState.test.tsx`
|
||||
48. `frontend-modern/src/components/Dashboard/__tests__/EnhancedCPUBar.test.tsx`
|
||||
49. `frontend-modern/src/components/Dashboard/__tests__/useEnhancedCPUBarState.test.tsx`
|
||||
50. `frontend-modern/src/components/Dashboard/ThresholdSlider.test.tsx`
|
||||
51. `frontend-modern/src/components/Dashboard/__tests__/useThresholdSliderState.test.ts`
|
||||
52. `frontend-modern/src/components/Dashboard/__tests__/StackedDiskBar.test.tsx`
|
||||
53. `frontend-modern/src/components/Dashboard/__tests__/useStackedDiskBarState.test.tsx`
|
||||
54. `frontend-modern/src/components/Dashboard/StackedMemoryBar.test.tsx`
|
||||
55. `frontend-modern/src/components/Dashboard/__tests__/useStackedMemoryBarState.test.tsx`
|
||||
56. `frontend-modern/src/components/Dashboard/__tests__/DiskList.test.tsx`
|
||||
57. `frontend-modern/src/components/Dashboard/__tests__/GuestRow.test.tsx`
|
||||
58. `frontend-modern/src/components/Dashboard/GuestDrawer.test.tsx`
|
||||
59. `frontend-modern/src/components/Infrastructure/__tests__/UnifiedResourceTable.performance.contract.test.tsx`
|
||||
9. `frontend-modern/src/components/Dashboard/useDashboardWorkloadRouteState.ts`
|
||||
10. `frontend-modern/src/components/Dashboard/DashboardFilter.tsx`
|
||||
11. `frontend-modern/src/components/Dashboard/dashboardFilterModel.ts`
|
||||
12. `frontend-modern/src/components/Dashboard/useDashboardFilterState.ts`
|
||||
13. `frontend-modern/src/components/Dashboard/ThresholdSlider.tsx`
|
||||
14. `frontend-modern/src/components/Dashboard/thresholdSliderModel.ts`
|
||||
15. `frontend-modern/src/components/Dashboard/useThresholdSliderState.ts`
|
||||
16. `frontend-modern/src/components/Dashboard/StackedDiskBar.tsx`
|
||||
17. `frontend-modern/src/components/Dashboard/stackedDiskBarModel.ts`
|
||||
18. `frontend-modern/src/components/Dashboard/useStackedDiskBarState.ts`
|
||||
19. `frontend-modern/src/components/Dashboard/StackedMemoryBar.tsx`
|
||||
20. `frontend-modern/src/components/Dashboard/stackedMemoryBarModel.ts`
|
||||
21. `frontend-modern/src/components/Dashboard/useStackedMemoryBarState.ts`
|
||||
22. `frontend-modern/src/components/Dashboard/MetricBar.tsx`
|
||||
23. `frontend-modern/src/components/Dashboard/metricBarModel.ts`
|
||||
24. `frontend-modern/src/components/Dashboard/useMetricBarState.ts`
|
||||
25. `frontend-modern/src/components/Dashboard/EnhancedCPUBar.tsx`
|
||||
26. `frontend-modern/src/components/Dashboard/enhancedCpuBarModel.ts`
|
||||
27. `frontend-modern/src/components/Dashboard/useEnhancedCPUBarState.ts`
|
||||
28. `frontend-modern/src/components/Dashboard/DiskList.tsx`
|
||||
29. `frontend-modern/src/components/Dashboard/diskListModel.ts`
|
||||
30. `frontend-modern/src/components/Dashboard/useDiskListState.ts`
|
||||
31. `frontend-modern/src/components/Dashboard/GuestRow.tsx`
|
||||
32. `frontend-modern/src/components/Dashboard/GuestRowCells.tsx`
|
||||
33. `frontend-modern/src/components/Dashboard/guestRowModel.tsx`
|
||||
34. `frontend-modern/src/components/Dashboard/useGuestRowState.ts`
|
||||
35. `frontend-modern/src/components/Dashboard/GuestDrawer.tsx`
|
||||
36. `frontend-modern/src/components/Dashboard/GuestDrawerOverview.tsx`
|
||||
37. `frontend-modern/src/components/Dashboard/guestDrawerModel.ts`
|
||||
38. `frontend-modern/src/components/Dashboard/useGuestDrawerState.ts`
|
||||
39. `frontend-modern/src/components/Dashboard/workloadSelectors.ts`
|
||||
40. `frontend-modern/src/components/Infrastructure/UnifiedResourceTable.tsx`
|
||||
41. `frontend-modern/src/components/Infrastructure/useUnifiedResourceTableState.ts`
|
||||
42. `frontend-modern/src/components/Infrastructure/infrastructureSelectors.ts`
|
||||
43. `frontend-modern/src/components/Infrastructure/resourceDetailMappers.ts`
|
||||
44. `frontend-modern/src/components/Dashboard/__tests__/Dashboard.performance.contract.test.tsx`
|
||||
45. `frontend-modern/src/components/Dashboard/__tests__/DashboardFilter.test.tsx`
|
||||
46. `frontend-modern/src/components/Dashboard/__tests__/useDashboardFilterState.test.ts`
|
||||
47. `frontend-modern/src/components/Dashboard/MetricBar.test.tsx`
|
||||
48. `frontend-modern/src/components/Dashboard/__tests__/useMetricBarState.test.tsx`
|
||||
49. `frontend-modern/src/components/Dashboard/__tests__/EnhancedCPUBar.test.tsx`
|
||||
50. `frontend-modern/src/components/Dashboard/__tests__/useEnhancedCPUBarState.test.tsx`
|
||||
51. `frontend-modern/src/components/Dashboard/ThresholdSlider.test.tsx`
|
||||
52. `frontend-modern/src/components/Dashboard/__tests__/useThresholdSliderState.test.ts`
|
||||
53. `frontend-modern/src/components/Dashboard/__tests__/StackedDiskBar.test.tsx`
|
||||
54. `frontend-modern/src/components/Dashboard/__tests__/useStackedDiskBarState.test.tsx`
|
||||
55. `frontend-modern/src/components/Dashboard/StackedMemoryBar.test.tsx`
|
||||
56. `frontend-modern/src/components/Dashboard/__tests__/useStackedMemoryBarState.test.tsx`
|
||||
57. `frontend-modern/src/components/Dashboard/__tests__/DiskList.test.tsx`
|
||||
58. `frontend-modern/src/components/Dashboard/__tests__/GuestRow.test.tsx`
|
||||
59. `frontend-modern/src/components/Dashboard/GuestDrawer.test.tsx`
|
||||
60. `frontend-modern/src/components/Infrastructure/__tests__/UnifiedResourceTable.performance.contract.test.tsx`
|
||||
|
||||
## Shared Boundaries
|
||||
|
||||
|
|
@ -102,12 +103,13 @@ regression protection.
|
|||
10. Extend dashboard drawer derivations and runtime wiring through `frontend-modern/src/components/Dashboard/guestDrawerModel.ts` and `frontend-modern/src/components/Dashboard/useGuestDrawerState.ts`, and extend drawer overview rendering through `frontend-modern/src/components/Dashboard/GuestDrawerOverview.tsx`, rather than rebuilding canonical guest identity, discovery routing, or drawer-local normalization inside `frontend-modern/src/components/Dashboard/GuestDrawer.tsx`
|
||||
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 filter defaults, active-filter counting, reset semantics, and mobile toolbar state through `frontend-modern/src/components/Dashboard/dashboardFilterModel.ts` and `frontend-modern/src/components/Dashboard/useDashboardFilterState.ts`, and keep dashboard-owned filter-config assembly in `frontend-modern/src/components/Dashboard/useDashboardState.ts`, rather than rebuilding filter-local state inside `frontend-modern/src/components/Dashboard/DashboardFilter.tsx` or inline config IIFEs in `frontend-modern/src/components/Dashboard/Dashboard.tsx`
|
||||
14. Extend threshold-slider value-position math, title/label derivation, and drag scroll-lock runtime through `frontend-modern/src/components/Dashboard/thresholdSliderModel.ts` and `frontend-modern/src/components/Dashboard/useThresholdSliderState.ts` rather than rebuilding slider-local state and pointer lifecycle inside `frontend-modern/src/components/Dashboard/ThresholdSlider.tsx`
|
||||
15. Extend stacked disk-bar capacity math, segment/tooltip derivation, and resize-observer runtime through `frontend-modern/src/components/Dashboard/stackedDiskBarModel.ts` and `frontend-modern/src/components/Dashboard/useStackedDiskBarState.ts` rather than rebuilding disk-bar-local state, mode branching, and tooltip shaping inside `frontend-modern/src/components/Dashboard/StackedDiskBar.tsx`
|
||||
16. Extend stacked memory-bar capacity math, balloon/swap derivation, and resize-observer runtime through `frontend-modern/src/components/Dashboard/stackedMemoryBarModel.ts` and `frontend-modern/src/components/Dashboard/useStackedMemoryBarState.ts` rather than rebuilding memory-bar-local state, tooltip shaping, and label-fit logic inside `frontend-modern/src/components/Dashboard/StackedMemoryBar.tsx`
|
||||
17. Extend metric-bar width, label-fit logic, and resize-observer runtime through `frontend-modern/src/components/Dashboard/metricBarModel.ts` and `frontend-modern/src/components/Dashboard/useMetricBarState.ts` rather than rebuilding metric-local state and threshold mapping inside `frontend-modern/src/components/Dashboard/MetricBar.tsx`
|
||||
18. Extend enhanced CPU bar usage/anomaly presentation and tooltip runtime through `frontend-modern/src/components/Dashboard/enhancedCpuBarModel.ts` and `frontend-modern/src/components/Dashboard/useEnhancedCPUBarState.ts` rather than rebuilding tooltip-local state and CPU-threshold formatting inside `frontend-modern/src/components/Dashboard/EnhancedCPUBar.tsx`
|
||||
13. Extend dashboard workload route ownership, deep-link normalization, route-driven filter state, and workload filter-config assembly through `frontend-modern/src/components/Dashboard/useDashboardWorkloadRouteState.ts` rather than rebuilding query-param parsing, route sync, or host/runtime/namespace filter shaping inside `frontend-modern/src/components/Dashboard/useDashboardState.ts`
|
||||
14. Extend dashboard filter defaults, active-filter counting, 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`
|
||||
15. Extend threshold-slider value-position math, title/label derivation, and drag scroll-lock runtime through `frontend-modern/src/components/Dashboard/thresholdSliderModel.ts` and `frontend-modern/src/components/Dashboard/useThresholdSliderState.ts` rather than rebuilding slider-local state and pointer lifecycle inside `frontend-modern/src/components/Dashboard/ThresholdSlider.tsx`
|
||||
16. Extend stacked disk-bar capacity math, segment/tooltip derivation, and resize-observer runtime through `frontend-modern/src/components/Dashboard/stackedDiskBarModel.ts` and `frontend-modern/src/components/Dashboard/useStackedDiskBarState.ts` rather than rebuilding disk-bar-local state, mode branching, and tooltip shaping inside `frontend-modern/src/components/Dashboard/StackedDiskBar.tsx`
|
||||
17. Extend stacked memory-bar capacity math, balloon/swap derivation, and resize-observer runtime through `frontend-modern/src/components/Dashboard/stackedMemoryBarModel.ts` and `frontend-modern/src/components/Dashboard/useStackedMemoryBarState.ts` rather than rebuilding memory-bar-local state, tooltip shaping, and label-fit logic inside `frontend-modern/src/components/Dashboard/StackedMemoryBar.tsx`
|
||||
18. Extend metric-bar width, label-fit logic, and resize-observer runtime through `frontend-modern/src/components/Dashboard/metricBarModel.ts` and `frontend-modern/src/components/Dashboard/useMetricBarState.ts` rather than rebuilding metric-local state and threshold mapping inside `frontend-modern/src/components/Dashboard/MetricBar.tsx`
|
||||
19. Extend enhanced CPU bar usage/anomaly presentation and tooltip runtime through `frontend-modern/src/components/Dashboard/enhancedCpuBarModel.ts` and `frontend-modern/src/components/Dashboard/useEnhancedCPUBarState.ts` rather than rebuilding tooltip-local state and CPU-threshold formatting inside `frontend-modern/src/components/Dashboard/EnhancedCPUBar.tsx`
|
||||
|
||||
## Forbidden Paths
|
||||
|
||||
|
|
@ -136,13 +138,15 @@ are now part of the protected performance surface rather than proof-only
|
|||
context. Future hot-path filter/group/sort/windowing changes must route through
|
||||
the explicit dashboard performance proof policy in the subsystem registry.
|
||||
That runtime is now intentionally split by concern:
|
||||
`frontend-modern/src/components/Dashboard/useDashboardState.ts` owns workload
|
||||
route synchronization, grouping/windowing, and filter/sort state, while
|
||||
`frontend-modern/src/components/Dashboard/useDashboardState.ts` owns grouped
|
||||
workload derivation, summary fallbacks, sorting, and windowing, while
|
||||
`frontend-modern/src/components/Dashboard/useDashboardGuestMetadataState.ts`
|
||||
owns guest metadata cache persistence, optimistic custom-URL updates,
|
||||
org-scope switching, and metadata refresh. Future dashboard hot-path changes
|
||||
must extend through those owners instead of accreting back into
|
||||
`frontend-modern/src/components/Dashboard/Dashboard.tsx`.
|
||||
org-scope switching, and metadata refresh, and
|
||||
`frontend-modern/src/components/Dashboard/useDashboardWorkloadRouteState.ts`
|
||||
owns workload-route synchronization, deep-link normalization, and route-scoped
|
||||
filter contracts. Future dashboard hot-path changes must extend through those
|
||||
owners instead of accreting back into `frontend-modern/src/components/Dashboard/Dashboard.tsx`.
|
||||
The dashboard guest-row path now follows the same pattern: the render shell
|
||||
stays in `frontend-modern/src/components/Dashboard/GuestRow.tsx`, tooltip-backed
|
||||
cell presentation lives in `frontend-modern/src/components/Dashboard/GuestRowCells.tsx`,
|
||||
|
|
|
|||
|
|
@ -2543,6 +2543,7 @@
|
|||
"frontend-modern/src/components/Dashboard/useDashboardFilterState.ts",
|
||||
"frontend-modern/src/components/Dashboard/useDashboardGuestMetadataState.ts",
|
||||
"frontend-modern/src/components/Dashboard/useDashboardState.ts",
|
||||
"frontend-modern/src/components/Dashboard/useDashboardWorkloadRouteState.ts",
|
||||
"frontend-modern/src/components/Dashboard/useDiskListState.ts",
|
||||
"frontend-modern/src/components/Dashboard/useEnhancedCPUBarState.ts",
|
||||
"frontend-modern/src/components/Dashboard/useGuestDrawerState.ts",
|
||||
|
|
@ -2631,6 +2632,7 @@
|
|||
"frontend-modern/src/components/Dashboard/useDashboardFilterState.ts",
|
||||
"frontend-modern/src/components/Dashboard/useDashboardGuestMetadataState.ts",
|
||||
"frontend-modern/src/components/Dashboard/useDashboardState.ts",
|
||||
"frontend-modern/src/components/Dashboard/useDashboardWorkloadRouteState.ts",
|
||||
"frontend-modern/src/components/Dashboard/useDiskListState.ts",
|
||||
"frontend-modern/src/components/Dashboard/useEnhancedCPUBarState.ts",
|
||||
"frontend-modern/src/components/Dashboard/useGuestDrawerState.ts",
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import dashboardSource from '../Dashboard.tsx?raw';
|
|||
import dashboardFilterSource from '../DashboardFilter.tsx?raw';
|
||||
import dashboardFilterModelSource from '../dashboardFilterModel.ts?raw';
|
||||
import dashboardGuestMetadataStateSource from '../useDashboardGuestMetadataState.ts?raw';
|
||||
import dashboardWorkloadRouteStateSource from '../useDashboardWorkloadRouteState.ts?raw';
|
||||
import dashboardStateSource from '../useDashboardState.ts?raw';
|
||||
import dashboardFilterStateSource from '../useDashboardFilterState.ts?raw';
|
||||
import thresholdSliderSource from '../ThresholdSlider.tsx?raw';
|
||||
|
|
@ -491,14 +492,18 @@ describe('Dashboard performance contract', () => {
|
|||
expect(dashboardSource).toContain('useDashboardState');
|
||||
expect(dashboardSource).not.toContain('const [search, setSearch] = createSignal(');
|
||||
expect(dashboardStateSource).toContain('useDashboardGuestMetadataState');
|
||||
expect(dashboardStateSource).toContain('useDashboardWorkloadRouteState');
|
||||
expect(dashboardStateSource).toContain('useGroupedTableWindowing');
|
||||
expect(dashboardStateSource).toContain('createWorkloadSortComparator');
|
||||
expect(dashboardStateSource).toContain("from './guestRowModel'");
|
||||
expect(dashboardStateSource).not.toContain("from './GuestRow'");
|
||||
expect(dashboardStateSource).not.toContain('GuestMetadataAPI.getAllMetadata()');
|
||||
expect(dashboardStateSource).not.toContain('buildWorkloadsPath({');
|
||||
expect(dashboardGuestMetadataStateSource).toContain('GuestMetadataAPI.getAllMetadata()');
|
||||
expect(dashboardGuestMetadataStateSource).toContain("eventBus.on('org_switched'");
|
||||
expect(dashboardGuestMetadataStateSource).toContain("window.addEventListener('pulse:metadata-changed'");
|
||||
expect(dashboardWorkloadRouteStateSource).toContain('buildWorkloadsPath({');
|
||||
expect(dashboardWorkloadRouteStateSource).toContain('normalizeWorkloadViewModeParam');
|
||||
expect(dashboardSource).toContain('createMemo(() => getCanonicalWorkloadId(guest()))');
|
||||
expect(dashboardStateSource).not.toContain('const guestId = () => {');
|
||||
});
|
||||
|
|
@ -513,9 +518,11 @@ describe('Dashboard performance contract', () => {
|
|||
expect(dashboardFilterStateSource).toContain('useBreakpoint');
|
||||
expect(dashboardFilterModelSource).toContain('export const countActiveDashboardFilters');
|
||||
expect(dashboardFilterModelSource).toContain('export const hasActiveDashboardFilters');
|
||||
expect(dashboardStateSource).toContain('containerRuntimeFilterConfig');
|
||||
expect(dashboardStateSource).toContain('hostFilterConfig');
|
||||
expect(dashboardStateSource).toContain('namespaceFilterConfig');
|
||||
expect(dashboardStateSource).toContain('useDashboardWorkloadRouteState');
|
||||
expect(dashboardStateSource).not.toContain('const containerRuntimeFilterConfig = createMemo');
|
||||
expect(dashboardWorkloadRouteStateSource).toContain('containerRuntimeFilterConfig');
|
||||
expect(dashboardWorkloadRouteStateSource).toContain('hostFilterConfig');
|
||||
expect(dashboardWorkloadRouteStateSource).toContain('namespaceFilterConfig');
|
||||
});
|
||||
|
||||
it('keeps threshold slider runtime and derivations in canonical slider owners', () => {
|
||||
|
|
|
|||
|
|
@ -1,12 +1,11 @@
|
|||
import { createEffect, createMemo, createSignal, onCleanup, untrack } from 'solid-js';
|
||||
import { createEffect, createMemo, createSignal, onCleanup } from 'solid-js';
|
||||
import { useLocation, useNavigate } from '@solidjs/router';
|
||||
import type { VM, Container, Node } from '@/types/api';
|
||||
import type { WorkloadGuest, ViewMode } from '@/types/workloads';
|
||||
import type { WorkloadGuest } from '@/types/workloads';
|
||||
import { GUEST_COLUMNS, VIEW_MODE_COLUMNS } from './guestRowModel';
|
||||
import { useWebSocket } from '@/App';
|
||||
import { useAlertsActivation } from '@/stores/alertsActivation';
|
||||
import { getNodeDisplayName } from '@/utils/nodes';
|
||||
import { logger } from '@/utils/logger';
|
||||
import { usePersistentSignal } from '@/hooks/usePersistentSignal';
|
||||
import { useColumnVisibility } from '@/hooks/useColumnVisibility';
|
||||
import { useBreakpoint } from '@/hooks/useBreakpoint';
|
||||
|
|
@ -23,20 +22,10 @@ import {
|
|||
} from '@/utils/dashboardEmptyStatePresentation';
|
||||
import {
|
||||
getCanonicalWorkloadId,
|
||||
normalizeWorkloadViewModeParam,
|
||||
resolveWorkloadType,
|
||||
} from '@/utils/workloads';
|
||||
import { isSummaryTimeRange } from '@/components/shared/summaryTimeRange';
|
||||
import { parseWorkloadsLinkSearch } from '@/routing/resourceLinks';
|
||||
import {
|
||||
buildWorkloadsPath,
|
||||
parseWorkloadsLinkSearch,
|
||||
WORKLOADS_PATH,
|
||||
WORKLOADS_QUERY_PARAMS,
|
||||
} from '@/routing/resourceLinks';
|
||||
import { areSearchParamsEquivalent } from '@/utils/searchParams';
|
||||
import {
|
||||
workloadNodeScopeId,
|
||||
getKubernetesContextKey,
|
||||
filterWorkloads,
|
||||
getDiskUsagePercent,
|
||||
createWorkloadSortComparator,
|
||||
|
|
@ -51,8 +40,8 @@ import {
|
|||
} from './workloadSelectors';
|
||||
import { useGroupedTableWindowing } from './useGroupedTableWindowing';
|
||||
import { useDashboardGuestMetadataState } from './useDashboardGuestMetadataState';
|
||||
import { useDashboardWorkloadRouteState } from './useDashboardWorkloadRouteState';
|
||||
import type { WorkloadSummarySnapshot } from '@/components/Workloads/WorkloadsSummary';
|
||||
import type { DashboardToolbarFilterConfig } from './dashboardFilterModel';
|
||||
|
||||
export interface DashboardProps {
|
||||
vms: VM[];
|
||||
|
|
@ -74,14 +63,13 @@ const workloadMetricPercent = (value: number | null | undefined): number => {
|
|||
};
|
||||
|
||||
export function useDashboardState(props: DashboardProps) {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const ws = useWebSocket();
|
||||
const { connected, activeAlerts, initialDataReceived, reconnecting, reconnect } = ws;
|
||||
const { isMobile } = useBreakpoint();
|
||||
const alertsActivation = useAlertsActivation();
|
||||
const alertsEnabled = createMemo(() => alertsActivation.activationState() === 'active');
|
||||
const isWorkloadsRoute = () => location.pathname === WORKLOADS_PATH;
|
||||
const [search, setSearch] = createSignal('');
|
||||
|
||||
const kioskMode = useKioskMode();
|
||||
|
|
@ -91,45 +79,9 @@ export function useDashboardState(props: DashboardProps) {
|
|||
const dashboardDisconnectedState = createMemo(() => getDashboardDisconnectedState(reconnecting()));
|
||||
|
||||
const [isSearchLocked, setIsSearchLocked] = createSignal(false);
|
||||
const [selectedNode, setSelectedNode] = createSignal<string | null>(null);
|
||||
const [selectedKubernetesContext, setSelectedKubernetesContext] = createSignal<string | null>(
|
||||
null,
|
||||
);
|
||||
const [selectedKubernetesNamespace, setSelectedKubernetesNamespace] = createSignal<
|
||||
string | null
|
||||
>(null);
|
||||
const [selectedGuestId, setSelectedGuestIdRaw] = createSignal<string | null>(null);
|
||||
const [hoveredWorkloadId, setHoveredWorkloadId] = createSignal<string | null>(null);
|
||||
const [handledResourceId, setHandledResourceId] = createSignal<string | null>(null);
|
||||
const [handledTypeParam, setHandledTypeParam] = createSignal<string>('');
|
||||
const [handledRuntimeParam, setHandledRuntimeParam] = createSignal<string>('');
|
||||
const [handledContextParam, setHandledContextParam] = createSignal('');
|
||||
const [handledNamespaceParam, setHandledNamespaceParam] = createSignal('');
|
||||
const [handledAgentParam, setHandledAgentParam] = createSignal('');
|
||||
const [selectedHostHint, setSelectedHostHint] = createSignal<string | null>(null);
|
||||
|
||||
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);
|
||||
};
|
||||
onCleanup(() => {
|
||||
if (pendingUrlSyncHandle !== null) {
|
||||
window.clearTimeout(pendingUrlSyncHandle);
|
||||
pendingUrlSyncHandle = null;
|
||||
pendingUrlSyncPath = null;
|
||||
}
|
||||
});
|
||||
|
||||
let tableRef: HTMLDivElement | undefined;
|
||||
const [tableBodyRef, setTableBodyRef] = createSignal<HTMLTableSectionElement | null>(null);
|
||||
|
|
@ -189,19 +141,6 @@ export function useDashboardState(props: DashboardProps) {
|
|||
workloadsEnabled() ? dedupeGuests(workloads.workloads()) : [],
|
||||
);
|
||||
|
||||
const [viewMode, setViewMode] = usePersistentSignal<ViewMode>('dashboardViewMode', 'all', {
|
||||
deserialize: (raw) => normalizeWorkloadViewModeParam(raw) ?? 'all',
|
||||
});
|
||||
|
||||
const [containerRuntime, setContainerRuntime] = usePersistentSignal<string>(
|
||||
'dashboardContainerRuntime',
|
||||
'',
|
||||
{
|
||||
deserialize: (raw) => (typeof raw === 'string' ? raw : ''),
|
||||
serialize: (value) => value,
|
||||
},
|
||||
);
|
||||
|
||||
const [statusMode, setStatusMode] = usePersistentSignal<StatusMode>(
|
||||
'dashboardStatusMode',
|
||||
'all',
|
||||
|
|
@ -245,312 +184,31 @@ export function useDashboardState(props: DashboardProps) {
|
|||
const [sortKey, setSortKey] = createSignal<WorkloadSortKey | null>('type');
|
||||
const [sortDirection, setSortDirection] = createSignal<'asc' | 'desc'>('asc');
|
||||
|
||||
const workloadNodeOptions = createMemo(() => {
|
||||
const labelsByScope = new Map<string, string>();
|
||||
const nodeNameCounts = new Map<string, number>();
|
||||
|
||||
for (const guest of allGuests()) {
|
||||
const type = resolveWorkloadType(guest);
|
||||
if (type === 'pod') continue;
|
||||
const scope = workloadNodeScopeId(guest);
|
||||
if (!scope || scope === '-') continue;
|
||||
const nodeName = (guest.node || '').trim();
|
||||
if (!nodeName) continue;
|
||||
nodeNameCounts.set(nodeName, (nodeNameCounts.get(nodeName) || 0) + 1);
|
||||
}
|
||||
|
||||
for (const guest of allGuests()) {
|
||||
const type = resolveWorkloadType(guest);
|
||||
if (type === 'pod') continue;
|
||||
const scope = workloadNodeScopeId(guest);
|
||||
if (!scope || scope === '-' || labelsByScope.has(scope)) continue;
|
||||
const nodeName = (guest.node || '').trim();
|
||||
const instance = (guest.instance || '').trim();
|
||||
if (!nodeName) continue;
|
||||
const hasDuplicateNodeName = (nodeNameCounts.get(nodeName) || 0) > 1;
|
||||
const label = hasDuplicateNodeName && instance ? `${nodeName} (${instance})` : nodeName;
|
||||
labelsByScope.set(scope, label);
|
||||
}
|
||||
|
||||
return Array.from(labelsByScope.entries())
|
||||
.map(([value, label]) => ({ value, label }))
|
||||
.sort((a, b) => a.label.localeCompare(b.label));
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
if (viewMode() === 'pod') return;
|
||||
const hostHint = selectedHostHint();
|
||||
if (!hostHint || selectedNode() !== null) return;
|
||||
const normalizedHint = hostHint.trim().toLowerCase();
|
||||
if (!normalizedHint) return;
|
||||
const option = workloadNodeOptions().find((candidate) => {
|
||||
const label = candidate.label.toLowerCase();
|
||||
const value = candidate.value.toLowerCase();
|
||||
return label === normalizedHint || value === normalizedHint || label.includes(normalizedHint);
|
||||
});
|
||||
if (!option) return;
|
||||
setSelectedNode(option.value);
|
||||
setSelectedHostHint(null);
|
||||
});
|
||||
|
||||
const kubernetesContextOptions = createMemo(() => {
|
||||
const contexts = new Set<string>();
|
||||
for (const guest of allGuests()) {
|
||||
if (resolveWorkloadType(guest) !== 'pod') continue;
|
||||
const context = getKubernetesContextKey(guest);
|
||||
if (context) {
|
||||
contexts.add(context);
|
||||
}
|
||||
}
|
||||
return Array.from(contexts).sort((a, b) => a.localeCompare(b));
|
||||
});
|
||||
|
||||
const kubernetesNamespaceOptions = createMemo(() => {
|
||||
const namespaces = new Set<string>();
|
||||
const contextFilter = (selectedKubernetesContext() || '').trim();
|
||||
for (const guest of allGuests()) {
|
||||
if (resolveWorkloadType(guest) !== 'pod') continue;
|
||||
if (contextFilter && getKubernetesContextKey(guest) !== contextFilter) continue;
|
||||
const ns = (guest.namespace || '').trim();
|
||||
if (ns) namespaces.add(ns);
|
||||
}
|
||||
return Array.from(namespaces).sort((a, b) => a.localeCompare(b));
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
if (!isWorkloadsRoute()) return;
|
||||
if (viewMode() !== 'pod') return;
|
||||
const selected = (selectedKubernetesNamespace() || '').trim();
|
||||
if (!selected) return;
|
||||
const normalized = selected.toLowerCase();
|
||||
const exists = kubernetesNamespaceOptions().some((value) => value.toLowerCase() === normalized);
|
||||
if (!exists) {
|
||||
setSelectedKubernetesNamespace(null);
|
||||
}
|
||||
});
|
||||
|
||||
const containerRuntimeOptions = createMemo(() => {
|
||||
const runtimes = new Set<string>();
|
||||
for (const guest of allGuests()) {
|
||||
if (resolveWorkloadType(guest) !== 'app-container') continue;
|
||||
const runtime = (guest.containerRuntime || '').trim();
|
||||
if (runtime) {
|
||||
runtimes.add(runtime);
|
||||
}
|
||||
}
|
||||
return Array.from(runtimes).sort((a, b) => a.localeCompare(b));
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
if (!isWorkloadsRoute()) return;
|
||||
if (viewMode() !== 'app-container') return;
|
||||
const selected = containerRuntime().trim();
|
||||
if (!selected) return;
|
||||
const normalized = selected.toLowerCase();
|
||||
const exists = containerRuntimeOptions().some((value) => value.toLowerCase() === normalized);
|
||||
if (!exists) {
|
||||
setContainerRuntime('');
|
||||
}
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
if (!isWorkloadsRoute()) return;
|
||||
if (viewMode() === 'pod') {
|
||||
if (selectedNode() !== null) {
|
||||
setSelectedNode(null);
|
||||
}
|
||||
if (selectedHostHint() !== null) {
|
||||
setSelectedHostHint(null);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (selectedKubernetesContext() !== null) {
|
||||
setSelectedKubernetesContext(null);
|
||||
}
|
||||
if (selectedKubernetesNamespace() !== null) {
|
||||
setSelectedKubernetesNamespace(null);
|
||||
}
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
if (!isWorkloadsRoute()) return;
|
||||
if (viewMode() !== 'app-container' && containerRuntime().trim() !== '') {
|
||||
setContainerRuntime('');
|
||||
}
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
const parsed = parseWorkloadsLinkSearch(location.search);
|
||||
const typeParam = parsed.type;
|
||||
const normalizedType = typeParam ?? '';
|
||||
if (normalizedType === handledTypeParam()) return;
|
||||
|
||||
if (!normalizedType) {
|
||||
setHandledTypeParam('');
|
||||
return;
|
||||
}
|
||||
|
||||
const hasK8sScope =
|
||||
Boolean((parsed.context ?? '').trim()) || Boolean((parsed.namespace ?? '').trim());
|
||||
const nextMode = normalizeWorkloadViewModeParam(normalizedType);
|
||||
if (!nextMode) {
|
||||
setHandledTypeParam(normalizedType);
|
||||
return;
|
||||
}
|
||||
if (hasK8sScope && nextMode !== 'pod') {
|
||||
setHandledTypeParam(normalizedType);
|
||||
return;
|
||||
}
|
||||
|
||||
setViewMode(nextMode);
|
||||
setHandledTypeParam(normalizedType);
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
const { context: contextParam } = parseWorkloadsLinkSearch(location.search);
|
||||
const normalized = contextParam ?? '';
|
||||
if (normalized === handledContextParam()) return;
|
||||
|
||||
if (normalized) {
|
||||
if (viewMode() !== 'pod') {
|
||||
setViewMode('pod');
|
||||
}
|
||||
setSelectedKubernetesContext(normalized);
|
||||
if (!showFilters()) {
|
||||
setShowFilters(true);
|
||||
}
|
||||
setHandledContextParam(normalized);
|
||||
return;
|
||||
}
|
||||
|
||||
setSelectedKubernetesContext(null);
|
||||
setHandledContextParam('');
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
const { namespace: namespaceParam } = parseWorkloadsLinkSearch(location.search);
|
||||
const normalized = namespaceParam ?? '';
|
||||
if (normalized === handledNamespaceParam()) return;
|
||||
|
||||
if (normalized) {
|
||||
if (viewMode() !== 'pod') {
|
||||
setViewMode('pod');
|
||||
}
|
||||
setSelectedKubernetesNamespace(normalized);
|
||||
if (!showFilters()) {
|
||||
setShowFilters(true);
|
||||
}
|
||||
setHandledNamespaceParam(normalized);
|
||||
return;
|
||||
}
|
||||
|
||||
setSelectedKubernetesNamespace(null);
|
||||
setHandledNamespaceParam('');
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
const { agent: agentParam } = parseWorkloadsLinkSearch(location.search);
|
||||
const normalized = agentParam ?? '';
|
||||
if (normalized === handledAgentParam()) return;
|
||||
|
||||
if (normalized) {
|
||||
setSelectedHostHint(normalized);
|
||||
if (!showFilters()) {
|
||||
setShowFilters(true);
|
||||
}
|
||||
setHandledAgentParam(normalized);
|
||||
return;
|
||||
}
|
||||
|
||||
setSelectedHostHint(null);
|
||||
if (selectedNode() !== null) {
|
||||
setSelectedNode(null);
|
||||
}
|
||||
setHandledAgentParam('');
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
const parsed = parseWorkloadsLinkSearch(location.search);
|
||||
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) {
|
||||
setHandledRuntimeParam(urlRuntime);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!urlRuntime.trim()) {
|
||||
setContainerRuntime('');
|
||||
setHandledRuntimeParam('');
|
||||
return;
|
||||
}
|
||||
|
||||
if (viewMode() !== 'app-container') {
|
||||
setViewMode('app-container');
|
||||
}
|
||||
setContainerRuntime(urlRuntime);
|
||||
if (!showFilters()) {
|
||||
setShowFilters(true);
|
||||
}
|
||||
setHandledRuntimeParam(urlRuntime);
|
||||
});
|
||||
|
||||
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 ?? '';
|
||||
|
||||
if (handledTypeParam() !== urlType) return;
|
||||
if (handledRuntimeParam() !== urlRuntime) return;
|
||||
if (handledContextParam() !== urlContext) return;
|
||||
if (handledNamespaceParam() !== urlNamespace) return;
|
||||
if (handledAgentParam() !== urlAgent) return;
|
||||
if (urlResource && handledResourceId() !== urlResource) return;
|
||||
|
||||
const currentParams = new URLSearchParams(location.search);
|
||||
const nextParams = new URLSearchParams(location.search);
|
||||
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)) {
|
||||
const nextSearch = nextParams.toString();
|
||||
const nextPath = nextSearch ? `${WORKLOADS_PATH}?${nextSearch}` : WORKLOADS_PATH;
|
||||
scheduleUrlSyncNavigate(nextPath);
|
||||
}
|
||||
const {
|
||||
containerRuntime,
|
||||
containerRuntimeFilterConfig,
|
||||
handleNodeSelect,
|
||||
hostFilterConfig,
|
||||
isWorkloadsRoute,
|
||||
kubernetesContextOptions,
|
||||
kubernetesNamespaceOptions,
|
||||
namespaceFilterConfig,
|
||||
resetWorkloadRouteFilters,
|
||||
selectedHostHint,
|
||||
selectedKubernetesContext,
|
||||
selectedKubernetesNamespace,
|
||||
selectedNode,
|
||||
setContainerRuntime,
|
||||
setSelectedKubernetesContext,
|
||||
setSelectedKubernetesNamespace,
|
||||
setViewMode,
|
||||
viewMode,
|
||||
workloadNodeOptions,
|
||||
containerRuntimeOptions,
|
||||
} = useDashboardWorkloadRouteState({
|
||||
allGuests,
|
||||
showFilters,
|
||||
setShowFilters,
|
||||
});
|
||||
|
||||
const relevantColumns = createMemo(() => {
|
||||
|
|
@ -638,12 +296,7 @@ export function useDashboardState(props: DashboardProps) {
|
|||
setIsSearchLocked(false);
|
||||
setSortKey('type');
|
||||
setSortDirection('asc');
|
||||
setSelectedNode(null);
|
||||
setSelectedHostHint(null);
|
||||
setSelectedKubernetesContext(null);
|
||||
setSelectedKubernetesNamespace(null);
|
||||
setContainerRuntime('');
|
||||
setViewMode('all');
|
||||
resetWorkloadRouteFilters();
|
||||
setStatusMode('all');
|
||||
|
||||
blurFocusedTypeToSearch();
|
||||
|
|
@ -879,19 +532,6 @@ export function useDashboardState(props: DashboardProps) {
|
|||
const totalStats = createMemo<WorkloadStats>(() => computeWorkloadStats(filteredGuests()));
|
||||
const workloadIOEmphasis = createMemo(() => computeWorkloadIOEmphasis(filteredGuests()));
|
||||
|
||||
const handleNodeSelect = (nodeId: string | null, nodeType: 'pve' | 'pbs' | 'pmg' | null) => {
|
||||
logger.debug('handleNodeSelect called', { nodeId, nodeType });
|
||||
|
||||
if (nodeType === 'pve' || nodeType === null) {
|
||||
setSelectedHostHint(null);
|
||||
setSelectedNode(nodeId);
|
||||
logger.debug('Set selected node', { nodeId });
|
||||
if (nodeId && !showFilters()) {
|
||||
setShowFilters(true);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleBeforeAutoFocus = () => {
|
||||
if (aiChatStore.focusInput()) return true;
|
||||
if (!showFilters()) setShowFilters(true);
|
||||
|
|
@ -942,74 +582,6 @@ export function useDashboardState(props: DashboardProps) {
|
|||
onColumnReset: columnVisibility.resetToDefaults,
|
||||
}));
|
||||
|
||||
const containerRuntimeFilterConfig = createMemo<DashboardToolbarFilterConfig | undefined>(() => {
|
||||
if (!isWorkloadsRoute()) return undefined;
|
||||
if (viewMode() !== 'app-container') return undefined;
|
||||
|
||||
const options = containerRuntimeOptions();
|
||||
if (options.length === 0) return undefined;
|
||||
|
||||
return {
|
||||
id: 'workloads-container-runtime-filter',
|
||||
label: 'Runtime',
|
||||
value: containerRuntime(),
|
||||
options: [
|
||||
{ value: '', label: 'All runtimes' },
|
||||
...options.map((value) => ({ value, label: value })),
|
||||
],
|
||||
onChange: (value: string) => setContainerRuntime(value),
|
||||
};
|
||||
});
|
||||
|
||||
const hostFilterConfig = createMemo<DashboardToolbarFilterConfig | undefined>(() => {
|
||||
if (!isWorkloadsRoute()) return undefined;
|
||||
|
||||
if (viewMode() === 'pod') {
|
||||
return {
|
||||
id: 'workloads-k8s-context-filter',
|
||||
label: 'Cluster',
|
||||
value: selectedKubernetesContext() ?? '',
|
||||
options: [
|
||||
{ value: '', label: 'All clusters' },
|
||||
...kubernetesContextOptions().map((context) => ({
|
||||
value: context,
|
||||
label: context,
|
||||
})),
|
||||
],
|
||||
onChange: (value: string) => setSelectedKubernetesContext(value || null),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
id: 'workloads-node-filter',
|
||||
label: 'Node',
|
||||
value: selectedNode() ?? '',
|
||||
options: [{ value: '', label: 'All nodes' }, ...workloadNodeOptions()],
|
||||
onChange: (value: string) => {
|
||||
handleNodeSelect(value || null, value ? 'pve' : null);
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const namespaceFilterConfig = createMemo<DashboardToolbarFilterConfig | undefined>(() => {
|
||||
if (!isWorkloadsRoute()) return undefined;
|
||||
if (viewMode() !== 'pod') return undefined;
|
||||
|
||||
const options = kubernetesNamespaceOptions();
|
||||
if (options.length === 0) return undefined;
|
||||
|
||||
return {
|
||||
id: 'workloads-k8s-namespace-filter',
|
||||
label: 'Namespace',
|
||||
value: selectedKubernetesNamespace() ?? '',
|
||||
options: [
|
||||
{ value: '', label: 'All namespaces' },
|
||||
...options.map((value) => ({ value, label: value })),
|
||||
],
|
||||
onChange: (value: string) => setSelectedKubernetesNamespace(value || null),
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
activeAlerts,
|
||||
alertsEnabled,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,502 @@
|
|||
import {
|
||||
createEffect,
|
||||
createMemo,
|
||||
createSignal,
|
||||
onCleanup,
|
||||
untrack,
|
||||
type Accessor,
|
||||
type Setter,
|
||||
} from 'solid-js';
|
||||
import { useLocation, useNavigate } from '@solidjs/router';
|
||||
import type { WorkloadGuest, ViewMode } from '@/types/workloads';
|
||||
import { usePersistentSignal } from '@/hooks/usePersistentSignal';
|
||||
import {
|
||||
buildWorkloadsPath,
|
||||
parseWorkloadsLinkSearch,
|
||||
WORKLOADS_PATH,
|
||||
WORKLOADS_QUERY_PARAMS,
|
||||
} from '@/routing/resourceLinks';
|
||||
import { areSearchParamsEquivalent } from '@/utils/searchParams';
|
||||
import { normalizeWorkloadViewModeParam, resolveWorkloadType } from '@/utils/workloads';
|
||||
import { getKubernetesContextKey, workloadNodeScopeId } from './workloadSelectors';
|
||||
import type { DashboardToolbarFilterConfig } from './dashboardFilterModel';
|
||||
|
||||
export interface DashboardWorkloadRouteStateOptions {
|
||||
allGuests: Accessor<WorkloadGuest[]>;
|
||||
showFilters: Accessor<boolean>;
|
||||
setShowFilters: Setter<boolean>;
|
||||
}
|
||||
|
||||
export function useDashboardWorkloadRouteState(options: DashboardWorkloadRouteStateOptions) {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const isWorkloadsRoute = () => location.pathname === WORKLOADS_PATH;
|
||||
|
||||
const [selectedNode, setSelectedNode] = createSignal<string | null>(null);
|
||||
const [selectedKubernetesContext, setSelectedKubernetesContext] = createSignal<string | null>(
|
||||
null,
|
||||
);
|
||||
const [selectedKubernetesNamespace, setSelectedKubernetesNamespace] = createSignal<
|
||||
string | null
|
||||
>(null);
|
||||
const [handledTypeParam, setHandledTypeParam] = createSignal('');
|
||||
const [handledRuntimeParam, setHandledRuntimeParam] = createSignal('');
|
||||
const [handledContextParam, setHandledContextParam] = createSignal('');
|
||||
const [handledNamespaceParam, setHandledNamespaceParam] = createSignal('');
|
||||
const [handledAgentParam, setHandledAgentParam] = createSignal('');
|
||||
const [selectedHostHint, setSelectedHostHint] = createSignal<string | null>(null);
|
||||
|
||||
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);
|
||||
};
|
||||
onCleanup(() => {
|
||||
if (pendingUrlSyncHandle !== null) {
|
||||
window.clearTimeout(pendingUrlSyncHandle);
|
||||
pendingUrlSyncHandle = null;
|
||||
pendingUrlSyncPath = null;
|
||||
}
|
||||
});
|
||||
|
||||
const [viewMode, setViewMode] = usePersistentSignal<ViewMode>('dashboardViewMode', 'all', {
|
||||
deserialize: (raw) => normalizeWorkloadViewModeParam(raw) ?? 'all',
|
||||
});
|
||||
|
||||
const [containerRuntime, setContainerRuntime] = usePersistentSignal<string>(
|
||||
'dashboardContainerRuntime',
|
||||
'',
|
||||
{
|
||||
deserialize: (raw) => (typeof raw === 'string' ? raw : ''),
|
||||
serialize: (value) => value,
|
||||
},
|
||||
);
|
||||
|
||||
const workloadNodeOptions = createMemo(() => {
|
||||
const labelsByScope = new Map<string, string>();
|
||||
const nodeNameCounts = new Map<string, number>();
|
||||
|
||||
for (const guest of options.allGuests()) {
|
||||
const type = resolveWorkloadType(guest);
|
||||
if (type === 'pod') continue;
|
||||
const scope = workloadNodeScopeId(guest);
|
||||
if (!scope || scope === '-') continue;
|
||||
const nodeName = (guest.node || '').trim();
|
||||
if (!nodeName) continue;
|
||||
nodeNameCounts.set(nodeName, (nodeNameCounts.get(nodeName) || 0) + 1);
|
||||
}
|
||||
|
||||
for (const guest of options.allGuests()) {
|
||||
const type = resolveWorkloadType(guest);
|
||||
if (type === 'pod') continue;
|
||||
const scope = workloadNodeScopeId(guest);
|
||||
if (!scope || scope === '-' || labelsByScope.has(scope)) continue;
|
||||
const nodeName = (guest.node || '').trim();
|
||||
const instance = (guest.instance || '').trim();
|
||||
if (!nodeName) continue;
|
||||
const hasDuplicateNodeName = (nodeNameCounts.get(nodeName) || 0) > 1;
|
||||
const label = hasDuplicateNodeName && instance ? `${nodeName} (${instance})` : nodeName;
|
||||
labelsByScope.set(scope, label);
|
||||
}
|
||||
|
||||
return Array.from(labelsByScope.entries())
|
||||
.map(([value, label]) => ({ value, label }))
|
||||
.sort((a, b) => a.label.localeCompare(b.label));
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
if (viewMode() === 'pod') return;
|
||||
const hostHint = selectedHostHint();
|
||||
if (!hostHint || selectedNode() !== null) return;
|
||||
const normalizedHint = hostHint.trim().toLowerCase();
|
||||
if (!normalizedHint) return;
|
||||
const option = workloadNodeOptions().find((candidate) => {
|
||||
const label = candidate.label.toLowerCase();
|
||||
const value = candidate.value.toLowerCase();
|
||||
return label === normalizedHint || value === normalizedHint || label.includes(normalizedHint);
|
||||
});
|
||||
if (!option) return;
|
||||
setSelectedNode(option.value);
|
||||
setSelectedHostHint(null);
|
||||
});
|
||||
|
||||
const kubernetesContextOptions = createMemo(() => {
|
||||
const contexts = new Set<string>();
|
||||
for (const guest of options.allGuests()) {
|
||||
if (resolveWorkloadType(guest) !== 'pod') continue;
|
||||
const context = getKubernetesContextKey(guest);
|
||||
if (context) {
|
||||
contexts.add(context);
|
||||
}
|
||||
}
|
||||
return Array.from(contexts).sort((a, b) => a.localeCompare(b));
|
||||
});
|
||||
|
||||
const kubernetesNamespaceOptions = createMemo(() => {
|
||||
const namespaces = new Set<string>();
|
||||
const contextFilter = (selectedKubernetesContext() || '').trim();
|
||||
for (const guest of options.allGuests()) {
|
||||
if (resolveWorkloadType(guest) !== 'pod') continue;
|
||||
if (contextFilter && getKubernetesContextKey(guest) !== contextFilter) continue;
|
||||
const ns = (guest.namespace || '').trim();
|
||||
if (ns) namespaces.add(ns);
|
||||
}
|
||||
return Array.from(namespaces).sort((a, b) => a.localeCompare(b));
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
if (!isWorkloadsRoute()) return;
|
||||
if (viewMode() !== 'pod') return;
|
||||
const selected = (selectedKubernetesNamespace() || '').trim();
|
||||
if (!selected) return;
|
||||
const normalized = selected.toLowerCase();
|
||||
const exists = kubernetesNamespaceOptions().some((value) => value.toLowerCase() === normalized);
|
||||
if (!exists) {
|
||||
setSelectedKubernetesNamespace(null);
|
||||
}
|
||||
});
|
||||
|
||||
const containerRuntimeOptions = createMemo(() => {
|
||||
const runtimes = new Set<string>();
|
||||
for (const guest of options.allGuests()) {
|
||||
if (resolveWorkloadType(guest) !== 'app-container') continue;
|
||||
const runtime = (guest.containerRuntime || '').trim();
|
||||
if (runtime) {
|
||||
runtimes.add(runtime);
|
||||
}
|
||||
}
|
||||
return Array.from(runtimes).sort((a, b) => a.localeCompare(b));
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
if (!isWorkloadsRoute()) return;
|
||||
if (viewMode() !== 'app-container') return;
|
||||
const selected = containerRuntime().trim();
|
||||
if (!selected) return;
|
||||
const normalized = selected.toLowerCase();
|
||||
const exists = containerRuntimeOptions().some((value) => value.toLowerCase() === normalized);
|
||||
if (!exists) {
|
||||
setContainerRuntime('');
|
||||
}
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
if (!isWorkloadsRoute()) return;
|
||||
if (viewMode() === 'pod') {
|
||||
if (selectedNode() !== null) {
|
||||
setSelectedNode(null);
|
||||
}
|
||||
if (selectedHostHint() !== null) {
|
||||
setSelectedHostHint(null);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (selectedKubernetesContext() !== null) {
|
||||
setSelectedKubernetesContext(null);
|
||||
}
|
||||
if (selectedKubernetesNamespace() !== null) {
|
||||
setSelectedKubernetesNamespace(null);
|
||||
}
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
if (!isWorkloadsRoute()) return;
|
||||
if (viewMode() !== 'app-container' && containerRuntime().trim() !== '') {
|
||||
setContainerRuntime('');
|
||||
}
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
const parsed = parseWorkloadsLinkSearch(location.search);
|
||||
const typeParam = parsed.type;
|
||||
const normalizedType = typeParam ?? '';
|
||||
if (normalizedType === handledTypeParam()) return;
|
||||
|
||||
if (!normalizedType) {
|
||||
setHandledTypeParam('');
|
||||
return;
|
||||
}
|
||||
|
||||
const hasK8sScope =
|
||||
Boolean((parsed.context ?? '').trim()) || Boolean((parsed.namespace ?? '').trim());
|
||||
const nextMode = normalizeWorkloadViewModeParam(normalizedType);
|
||||
if (!nextMode) {
|
||||
setHandledTypeParam(normalizedType);
|
||||
return;
|
||||
}
|
||||
if (hasK8sScope && nextMode !== 'pod') {
|
||||
setHandledTypeParam(normalizedType);
|
||||
return;
|
||||
}
|
||||
|
||||
setViewMode(nextMode);
|
||||
setHandledTypeParam(normalizedType);
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
const { context: contextParam } = parseWorkloadsLinkSearch(location.search);
|
||||
const normalized = contextParam ?? '';
|
||||
if (normalized === handledContextParam()) return;
|
||||
|
||||
if (normalized) {
|
||||
if (viewMode() !== 'pod') {
|
||||
setViewMode('pod');
|
||||
}
|
||||
setSelectedKubernetesContext(normalized);
|
||||
if (!options.showFilters()) {
|
||||
options.setShowFilters(true);
|
||||
}
|
||||
setHandledContextParam(normalized);
|
||||
return;
|
||||
}
|
||||
|
||||
setSelectedKubernetesContext(null);
|
||||
setHandledContextParam('');
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
const { namespace: namespaceParam } = parseWorkloadsLinkSearch(location.search);
|
||||
const normalized = namespaceParam ?? '';
|
||||
if (normalized === handledNamespaceParam()) return;
|
||||
|
||||
if (normalized) {
|
||||
if (viewMode() !== 'pod') {
|
||||
setViewMode('pod');
|
||||
}
|
||||
setSelectedKubernetesNamespace(normalized);
|
||||
if (!options.showFilters()) {
|
||||
options.setShowFilters(true);
|
||||
}
|
||||
setHandledNamespaceParam(normalized);
|
||||
return;
|
||||
}
|
||||
|
||||
setSelectedKubernetesNamespace(null);
|
||||
setHandledNamespaceParam('');
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
const { agent: agentParam } = parseWorkloadsLinkSearch(location.search);
|
||||
const normalized = agentParam ?? '';
|
||||
if (normalized === handledAgentParam()) return;
|
||||
|
||||
if (normalized) {
|
||||
setSelectedHostHint(normalized);
|
||||
if (!options.showFilters()) {
|
||||
options.setShowFilters(true);
|
||||
}
|
||||
setHandledAgentParam(normalized);
|
||||
return;
|
||||
}
|
||||
|
||||
setSelectedHostHint(null);
|
||||
if (selectedNode() !== null) {
|
||||
setSelectedNode(null);
|
||||
}
|
||||
setHandledAgentParam('');
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
const parsed = parseWorkloadsLinkSearch(location.search);
|
||||
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) {
|
||||
setHandledRuntimeParam(urlRuntime);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!urlRuntime.trim()) {
|
||||
setContainerRuntime('');
|
||||
setHandledRuntimeParam('');
|
||||
return;
|
||||
}
|
||||
|
||||
if (viewMode() !== 'app-container') {
|
||||
setViewMode('app-container');
|
||||
}
|
||||
setContainerRuntime(urlRuntime);
|
||||
if (!options.showFilters()) {
|
||||
options.setShowFilters(true);
|
||||
}
|
||||
setHandledRuntimeParam(urlRuntime);
|
||||
});
|
||||
|
||||
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 ?? '';
|
||||
|
||||
if (handledTypeParam() !== urlType) return;
|
||||
if (handledRuntimeParam() !== urlRuntime) return;
|
||||
if (handledContextParam() !== urlContext) return;
|
||||
if (handledNamespaceParam() !== urlNamespace) return;
|
||||
if (handledAgentParam() !== urlAgent) return;
|
||||
if (urlResource) return;
|
||||
|
||||
const currentParams = new URLSearchParams(location.search);
|
||||
const nextParams = new URLSearchParams(location.search);
|
||||
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)) {
|
||||
const nextSearch = nextParams.toString();
|
||||
const nextPath = nextSearch ? `${WORKLOADS_PATH}?${nextSearch}` : WORKLOADS_PATH;
|
||||
scheduleUrlSyncNavigate(nextPath);
|
||||
}
|
||||
});
|
||||
|
||||
const handleNodeSelect = (nodeId: string | null, nodeType: 'pve' | 'pbs' | 'pmg' | null) => {
|
||||
if (nodeType === 'pve' || nodeType === null) {
|
||||
setSelectedHostHint(null);
|
||||
setSelectedNode(nodeId);
|
||||
if (nodeId && !options.showFilters()) {
|
||||
options.setShowFilters(true);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const resetWorkloadRouteFilters = () => {
|
||||
setSelectedNode(null);
|
||||
setSelectedHostHint(null);
|
||||
setSelectedKubernetesContext(null);
|
||||
setSelectedKubernetesNamespace(null);
|
||||
setContainerRuntime('');
|
||||
setViewMode('all');
|
||||
};
|
||||
|
||||
const containerRuntimeFilterConfig = createMemo<DashboardToolbarFilterConfig | undefined>(() => {
|
||||
if (!isWorkloadsRoute()) return undefined;
|
||||
if (viewMode() !== 'app-container') return undefined;
|
||||
|
||||
const options = containerRuntimeOptions();
|
||||
if (options.length === 0) return undefined;
|
||||
|
||||
return {
|
||||
id: 'workloads-container-runtime-filter',
|
||||
label: 'Runtime',
|
||||
value: containerRuntime(),
|
||||
options: [
|
||||
{ value: '', label: 'All runtimes' },
|
||||
...options.map((value) => ({ value, label: value })),
|
||||
],
|
||||
onChange: (value: string) => setContainerRuntime(value),
|
||||
};
|
||||
});
|
||||
|
||||
const hostFilterConfig = createMemo<DashboardToolbarFilterConfig | undefined>(() => {
|
||||
if (!isWorkloadsRoute()) return undefined;
|
||||
|
||||
if (viewMode() === 'pod') {
|
||||
return {
|
||||
id: 'workloads-k8s-context-filter',
|
||||
label: 'Cluster',
|
||||
value: selectedKubernetesContext() ?? '',
|
||||
options: [
|
||||
{ value: '', label: 'All clusters' },
|
||||
...kubernetesContextOptions().map((context) => ({
|
||||
value: context,
|
||||
label: context,
|
||||
})),
|
||||
],
|
||||
onChange: (value: string) => setSelectedKubernetesContext(value || null),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
id: 'workloads-node-filter',
|
||||
label: 'Node',
|
||||
value: selectedNode() ?? '',
|
||||
options: [{ value: '', label: 'All nodes' }, ...workloadNodeOptions()],
|
||||
onChange: (value: string) => {
|
||||
handleNodeSelect(value || null, value ? 'pve' : null);
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const namespaceFilterConfig = createMemo<DashboardToolbarFilterConfig | undefined>(() => {
|
||||
if (!isWorkloadsRoute()) return undefined;
|
||||
if (viewMode() !== 'pod') return undefined;
|
||||
|
||||
const options = kubernetesNamespaceOptions();
|
||||
if (options.length === 0) return undefined;
|
||||
|
||||
return {
|
||||
id: 'workloads-k8s-namespace-filter',
|
||||
label: 'Namespace',
|
||||
value: selectedKubernetesNamespace() ?? '',
|
||||
options: [
|
||||
{ value: '', label: 'All namespaces' },
|
||||
...options.map((value) => ({ value, label: value })),
|
||||
],
|
||||
onChange: (value: string) => setSelectedKubernetesNamespace(value || null),
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
containerRuntime,
|
||||
containerRuntimeFilterConfig,
|
||||
containerRuntimeOptions,
|
||||
handleNodeSelect,
|
||||
hostFilterConfig,
|
||||
isWorkloadsRoute,
|
||||
kubernetesContextOptions,
|
||||
kubernetesNamespaceOptions,
|
||||
namespaceFilterConfig,
|
||||
resetWorkloadRouteFilters,
|
||||
selectedHostHint,
|
||||
selectedKubernetesContext,
|
||||
selectedKubernetesNamespace,
|
||||
selectedNode,
|
||||
setContainerRuntime,
|
||||
setSelectedKubernetesContext,
|
||||
setSelectedKubernetesNamespace,
|
||||
setViewMode,
|
||||
viewMode,
|
||||
workloadNodeOptions,
|
||||
} as const;
|
||||
}
|
||||
|
|
@ -82,6 +82,7 @@ import dashboardSource from '@/components/Dashboard/Dashboard.tsx?raw';
|
|||
import dashboardFilterSource from '@/components/Dashboard/DashboardFilter.tsx?raw';
|
||||
import dashboardFilterModelSource from '@/components/Dashboard/dashboardFilterModel.ts?raw';
|
||||
import dashboardGuestMetadataStateSource from '@/components/Dashboard/useDashboardGuestMetadataState.ts?raw';
|
||||
import dashboardWorkloadRouteStateSource from '@/components/Dashboard/useDashboardWorkloadRouteState.ts?raw';
|
||||
import dashboardStateSource from '@/components/Dashboard/useDashboardState.ts?raw';
|
||||
import dashboardFilterStateSource from '@/components/Dashboard/useDashboardFilterState.ts?raw';
|
||||
import thresholdSliderModelSource from '@/components/Dashboard/thresholdSliderModel.ts?raw';
|
||||
|
|
@ -511,7 +512,8 @@ describe('frontend resource type boundaries', () => {
|
|||
expect(dashboardSource).toContain('useDashboardState');
|
||||
expect(dashboardSource).not.toContain('const [search, setSearch] = createSignal(');
|
||||
expect(dashboardStateSource).toContain('useDashboardGuestMetadataState');
|
||||
expect(dashboardStateSource).toContain('normalizeWorkloadViewModeParam');
|
||||
expect(dashboardStateSource).toContain('useDashboardWorkloadRouteState');
|
||||
expect(dashboardWorkloadRouteStateSource).toContain('normalizeWorkloadViewModeParam');
|
||||
expect(dashboardSource).not.toContain('function normalizeViewModeParam');
|
||||
expect(dashboardSource).not.toContain('workloadSummaryGuestId');
|
||||
expect(dashboardSource).toContain('createMemo(() => getCanonicalWorkloadId(guest()))');
|
||||
|
|
@ -521,6 +523,8 @@ describe('frontend resource type boundaries', () => {
|
|||
expect(dashboardStateSource).not.toContain('GuestMetadataAPI.getAllMetadata()');
|
||||
expect(dashboardGuestMetadataStateSource).toContain('GuestMetadataAPI.getAllMetadata()');
|
||||
expect(dashboardGuestMetadataStateSource).toContain("eventBus.on('org_switched'");
|
||||
expect(dashboardStateSource).not.toContain('buildWorkloadsPath({');
|
||||
expect(dashboardWorkloadRouteStateSource).toContain('buildWorkloadsPath({');
|
||||
expect(dashboardStateSource).not.toContain('const guestId = () => {');
|
||||
expect(dashboardFilterSource).toContain('useDashboardFilterState');
|
||||
expect(dashboardFilterSource).not.toContain('const [filtersOpen, setFiltersOpen] =');
|
||||
|
|
@ -531,9 +535,10 @@ describe('frontend resource type boundaries', () => {
|
|||
expect(dashboardFilterStateSource).toContain('useBreakpoint');
|
||||
expect(dashboardFilterModelSource).toContain('export const countActiveDashboardFilters');
|
||||
expect(dashboardFilterModelSource).toContain('export const hasActiveDashboardFilters');
|
||||
expect(dashboardStateSource).toContain('containerRuntimeFilterConfig');
|
||||
expect(dashboardStateSource).toContain('hostFilterConfig');
|
||||
expect(dashboardStateSource).toContain('namespaceFilterConfig');
|
||||
expect(dashboardStateSource).not.toContain('const containerRuntimeFilterConfig = createMemo');
|
||||
expect(dashboardWorkloadRouteStateSource).toContain('containerRuntimeFilterConfig');
|
||||
expect(dashboardWorkloadRouteStateSource).toContain('hostFilterConfig');
|
||||
expect(dashboardWorkloadRouteStateSource).toContain('namespaceFilterConfig');
|
||||
expect(thresholdSliderSource).toContain('useThresholdSliderState');
|
||||
expect(thresholdSliderSource).not.toContain('const [thumbPosition, setThumbPosition] =');
|
||||
expect(thresholdSliderSource).not.toContain('const handleMouseDown = () => {');
|
||||
|
|
|
|||
|
|
@ -1848,6 +1848,33 @@ class SubsystemLookupTest(unittest.TestCase):
|
|||
"dashboard-workload-hot-path",
|
||||
)
|
||||
|
||||
def test_lookup_paths_assigns_dashboard_workload_route_runtime_to_performance_and_scalability(
|
||||
self,
|
||||
) -> None:
|
||||
result = lookup_paths(
|
||||
["frontend-modern/src/components/Dashboard/useDashboardWorkloadRouteState.ts"]
|
||||
)
|
||||
self.assertEqual(result["unowned_runtime_files"], [])
|
||||
self.assertEqual(
|
||||
{item["subsystem"] for item in result["impacted_subsystems"]},
|
||||
{"performance-and-scalability"},
|
||||
)
|
||||
file_entry = result["files"][0]
|
||||
self.assertEqual(file_entry["classification"], "runtime")
|
||||
self.assertEqual(
|
||||
{match["subsystem"] for match in file_entry["matches"]},
|
||||
{"performance-and-scalability"},
|
||||
)
|
||||
match = file_entry["matches"][0]
|
||||
self.assertEqual(
|
||||
match["contract"],
|
||||
"docs/release-control/v6/internal/subsystems/performance-and-scalability.md",
|
||||
)
|
||||
self.assertEqual(
|
||||
match["verification_requirement"]["id"],
|
||||
"dashboard-workload-hot-path",
|
||||
)
|
||||
|
||||
def test_lookup_paths_assigns_dashboard_guest_drawer_runtime_to_performance_and_scalability(self) -> None:
|
||||
result = lookup_paths(
|
||||
[
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue