From 264c9377a2ca8b41b2cfbda244a7ac24c2b0a779 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Sat, 18 Apr 2026 22:58:17 +0100 Subject: [PATCH] Remove infrastructure setup modals --- .../v6/internal/subsystems/agent-lifecycle.md | 18 +- .../subsystems/frontend-primitives.md | 13 +- .../Settings/InfrastructureWorkspace.tsx | 257 ++++++++---------- .../InfrastructureWorkspace.test.tsx | 101 +++---- .../__tests__/settingsArchitecture.test.ts | 2 +- 5 files changed, 159 insertions(+), 232 deletions(-) diff --git a/docs/release-control/v6/internal/subsystems/agent-lifecycle.md b/docs/release-control/v6/internal/subsystems/agent-lifecycle.md index c4a26249a..e385f273a 100644 --- a/docs/release-control/v6/internal/subsystems/agent-lifecycle.md +++ b/docs/release-control/v6/internal/subsystems/agent-lifecycle.md @@ -858,18 +858,18 @@ ledger. Inline detail drawers may surface reporting-item and ignored-item controls, but installer setup, platform-specific configuration, and profile management must remain secondary flows rather than being dumped underneath the default ledger. -Those secondary infrastructure views must open through route-backed modal work -surfaces rather than replacing the ledger body or stacking beneath it. When the +Those secondary infrastructure views must open through route-backed workspaces +rather than modal overlays or stacked inline bodies. When the operator opens platform connection management, install tooling, or the legacy operations workspace route, `InfrastructureWorkspace.tsx` must keep the same -top-level systems ledger anchored in place and layer the secondary surface in a -centered modal that can close back to `/settings/infrastructure` without -trapping the user or relegating primary setup work to an off-canvas detail -pattern. +single `Infrastructure` destination and provide explicit back navigation to +`/settings/infrastructure` without trapping the user behind overlays or +relegating primary setup work to a detail pattern. That infrastructure surface must now stay single-purpose per route-backed -modal: the default systems view owns the top-level monitored-system ledger, -the connections modal owns API-backed platform management, and the install -modal owns Linux/Windows/macOS/FreeBSD command generation. The shared +workspace: the default systems view owns the top-level monitored-system ledger, +the connections workspace owns API-backed platform management, and the install +workspace owns Linux/Windows/macOS/FreeBSD command generation plus the add-flow +handoff. The shared Settings sidebar owns only the top-level `Infrastructure` destination; movement between those three jobs belongs to explicit ledger actions inside `InfrastructureWorkspace.tsx`, not extra sidebar entries or body-replacing diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md index 1152be0ec..da4c8bd20 100644 --- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md +++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md @@ -220,17 +220,14 @@ work extends shared components instead of creating new local variants. connections must stay in their own management workspace instead of showing up as peer rows in the default table, while installer tooling, provider setup workspaces, and profile management remain secondary flows opened by explicit - deep links or modal work surfaces instead of being dumped inline underneath the default + deep links or dedicated workspace routes instead of being dumped inline underneath the default table. - Those deep-linked secondary views must keep the systems ledger mounted and - open inside route-backed modal surfaces instead of replacing the page body or - stacking another inline workspace underneath it, so the operator always - stays anchored to the same canonical ledger while managing setup flows - without relegating primary setup work to off-canvas side drawers. + Those deep-linked secondary views must stay under the same single + `Infrastructure` sidebar destination, but they should render as normal page + content with explicit back navigation instead of modal or drawer overlays. That same shared shell boundary now owns one canonical infrastructure destination in the Settings sidebar. `InfrastructureWorkspace.tsx` owns the - one default ledger plus route-backed `Connections` and `Install` modal work - surfaces + default ledger plus route-backed `Connections` and `Install` workspaces inside that destination, while each secondary flow still stays single-purpose instead of stacking multiple workspace surfaces at once. 6. Keep Proxmox deep-link route selection on the shared settings-navigation boundary. `frontend-modern/src/components/Settings/settingsNavigationModel.ts` and `frontend-modern/src/components/Settings/useSettingsNavigation.ts` must treat the canonical PBS and PMG Proxmox deep links as agent-selection authority even though those URLs resolve to the shared `infrastructure-operations` tab. Reloading or remounting on a PBS or PMG deep link must not silently fall back to the PVE selector state. diff --git a/frontend-modern/src/components/Settings/InfrastructureWorkspace.tsx b/frontend-modern/src/components/Settings/InfrastructureWorkspace.tsx index 3cb66179b..43488bccf 100644 --- a/frontend-modern/src/components/Settings/InfrastructureWorkspace.tsx +++ b/frontend-modern/src/components/Settings/InfrastructureWorkspace.tsx @@ -1,9 +1,9 @@ -import { Component, Show, createEffect, createMemo, createSignal } from 'solid-js'; +import { Component, For, Show, createEffect, createMemo, createSignal } from 'solid-js'; import { useLocation, useNavigate } from '@solidjs/router'; import { Dialog } from '@/components/shared/Dialog'; import { presentationPolicyIsReadOnly } from '@/stores/sessionPresentationPolicy'; import { AgentProfilesPanel } from './AgentProfilesPanel'; -import { AddSystemPicker, type AddSystemChoice } from './AddSystemPicker'; +import { ADD_SYSTEM_CHOICES, type AddSystemChoice } from './AddSystemPicker'; import { ConnectionsTable, type ConnectionsTableHeaderAction } from './ConnectionsTable'; import { buildInfrastructureSystemRows, @@ -37,8 +37,7 @@ const InfrastructureWorkspaceContent: Component = const activeView = createMemo(() => getInfrastructureWorkspaceViewFromPath(location.pathname)); const readOnlyWorkspace = createMemo(() => presentationPolicyIsReadOnly()); - const [pickerOpen, setPickerOpen] = createSignal(false); - const [profilesOpen, setProfilesOpen] = createSignal(false); + const [showInstallProfiles, setShowInstallProfiles] = createSignal(false); const rows = createMemo(() => buildInfrastructureSystemRows({ @@ -52,7 +51,7 @@ const InfrastructureWorkspaceContent: Component = : [ { label: 'Add infrastructure', - onSelect: () => setPickerOpen(true), + onSelect: () => navigate(buildInfrastructureWorkspacePath('install'), { scroll: false }), tone: 'primary' as const, }, ], @@ -68,28 +67,12 @@ const InfrastructureWorkspaceContent: Component = navigate(buildInfrastructureWorkspacePath('inventory'), { scroll: false }); }; const closeInstallWorkspace = () => { + setShowInstallProfiles(false); navigate(buildInfrastructureWorkspacePath('inventory'), { scroll: false }); }; - const openProxmoxNode = (nodeKind: 'pve' | 'pbs' | 'pmg', nodeId: string) => { - const nodes = - nodeKind === 'pve' - ? props.pveNodes() - : nodeKind === 'pbs' - ? props.pbsNodes() - : props.pmgNodes(); - const node = nodes.find((candidate) => candidate.id === nodeId) ?? null; - - props.onSelectAgent(nodeKind); - props.setCurrentNodeType(nodeKind); - props.setEditingNode(node); - props.setModalResetKey((value) => value + 1); - props.setShowNodeModal(true); - navigate(proxmoxRouteForKind(nodeKind), { scroll: false }); - }; - const handleAddSystem = (choice: AddSystemChoice) => { - setPickerOpen(false); + setShowInstallProfiles(false); if (choice.kind === 'agent') { navigate(buildInfrastructureWorkspacePath('install'), { scroll: false }); @@ -97,18 +80,16 @@ const InfrastructureWorkspaceContent: Component = } if (choice.kind === 'truenas') { - props.trueNASSettings.openCreateDialog(); navigate('/settings/infrastructure/platforms/truenas', { scroll: false }); return; } if (choice.kind === 'vmware') { - props.vmwareSettings.openCreateDialog(); navigate('/settings/infrastructure/platforms/vmware', { scroll: false }); return; } - openProxmoxNode(choice.kind, ''); + navigate(proxmoxRouteForKind(choice.kind), { scroll: false }); }; const handleManageAction = (action: SystemManageAction) => { @@ -136,152 +117,126 @@ const InfrastructureWorkspaceContent: Component = if (activeView() === 'inventory') { return; } - setPickerOpen(false); - setProfilesOpen(false); + if (activeView() !== 'install') { + setShowInstallProfiles(false); + } state.setExpandedRowKey(null); state.setSelectedIgnoredRowKey(null); }); return (
- handleManageAction(row.manage)} - /> + + handleManageAction(row.manage)} + /> + - setPickerOpen(false)} - onSelect={handleAddSystem} - onManageProfiles={() => { - setPickerOpen(false); - setProfilesOpen(true); - }} - /> - - - - setProfilesOpen(false)} - panelClass="h-[calc(100dvh-2rem)] w-full max-w-[1100px]" - ariaLabel="Agent profiles" - > -
-
-
+ +
+
+ +
+
+ Add infrastructure +
+
Choose what to connect
+
+ Start with a host install or jump straight to an API-backed platform connection. +
+
+ +
+
+ +
+ + {(choice) => ( + + )} + +
+ +
+ +
+ + +
+
Agent profiles
Manage reusable install defaults for agent-based systems.
- +
-
-
- -
+
-
+ - -
-
-
-
-
- Connections -
-
Platform connections
-
- Configure API-backed providers without leaving the infrastructure ledger. -
+ +
+
+ +
+
+ Connections +
+
Platform connections
+
+ Configure API-backed providers in a full workspace instead of a modal overlay.
-
-
- -
+
-
+ - -
-
-
-
-
- Install -
-
Install Pulse agent
-
- Generate Linux, macOS, FreeBSD, and Windows install commands from the same - workspace. -
-
- -
-
-
- -
-
-
+ = {}): UnifiedAgentRow ...overrides, }) as UnifiedAgentRow; -const trueNASOpenCreateDialogSpy = vi.fn(); -const vmwareOpenCreateDialogSpy = vi.fn(); const setShowNodeModalSpy = vi.fn(); const setEditingNodeSpy = vi.fn(); const setCurrentNodeTypeSpy = vi.fn(); const setModalResetKeySpy = vi.fn(); -const onSelectAgentSpy = vi.fn(); - const baseProps = () => ({ pveNodes: () => [{ id: 'pve-1', name: 'zeus', host: '10.0.0.1', type: 'pve', status: 'connected' }], @@ -116,20 +112,20 @@ const baseProps = () => agentStateResources: () => [], trueNASSettings: { connections: () => [{ id: 'tn-1', name: 'Tower NAS', host: '10.0.0.20', enabled: true }], - openCreateDialog: trueNASOpenCreateDialogSpy, + openCreateDialog: vi.fn(), closeDialog: vi.fn(), closeDeleteDialog: vi.fn(), openEditDialog: vi.fn(), }, vmwareSettings: { connections: () => [{ id: 'vm-1', name: 'lab-vcenter', host: '10.0.0.30', enabled: true }], - openCreateDialog: vmwareOpenCreateDialogSpy, + openCreateDialog: vi.fn(), closeDialog: vi.fn(), closeDeleteDialog: vi.fn(), openEditDialog: vi.fn(), }, selectedAgent: () => 'pve', - onSelectAgent: onSelectAgentSpy, + onSelectAgent: vi.fn(), setShowNodeModal: setShowNodeModalSpy, setEditingNode: setEditingNodeSpy, setCurrentNodeType: setCurrentNodeTypeSpy, @@ -143,13 +139,10 @@ describe('InfrastructureWorkspace', () => { presentationPolicyIsReadOnlyMock.mockReturnValue(false); setExpandedRowKeySpy.mockReset(); setSelectedIgnoredRowKeySpy.mockReset(); - trueNASOpenCreateDialogSpy.mockReset(); - vmwareOpenCreateDialogSpy.mockReset(); setShowNodeModalSpy.mockReset(); setEditingNodeSpy.mockReset(); setCurrentNodeTypeSpy.mockReset(); setModalResetKeySpy.mockReset(); - onSelectAgentSpy.mockReset(); mockPathname = '/settings/infrastructure'; mockActiveRows = [reportingRow()]; mockIgnoredRows = []; @@ -185,59 +178,16 @@ describe('InfrastructureWorkspace', () => { expect(screen.queryByText('lab-vcenter')).toBeNull(); }); - it('opens the add-system picker when the add button is clicked', () => { + it('routes the add-infrastructure action to the install workspace', () => { renderWorkspace(); fireEvent.click(screen.getByRole('button', { name: /Add infrastructure/i })); - expect(screen.getByText('Install on a host')).toBeInTheDocument(); - expect(screen.getByText('Proxmox VE')).toBeInTheDocument(); - expect(screen.getByText('TrueNAS SCALE')).toBeInTheDocument(); - }); - - it('routes the agent-host choice to the install section deep link', () => { - renderWorkspace(); - fireEvent.click(screen.getByRole('button', { name: /Add infrastructure/i })); - fireEvent.click(screen.getByText('Install on a host')); - expect(navigateSpy).toHaveBeenCalledWith('/settings/infrastructure/install', { scroll: false, }); }); - it('opens provider creation flows directly from the add-system picker', () => { - renderWorkspace(); - fireEvent.click(screen.getByRole('button', { name: /Add infrastructure/i })); - fireEvent.click(screen.getByText('TrueNAS SCALE')); - - expect(trueNASOpenCreateDialogSpy).toHaveBeenCalledTimes(1); - expect(navigateSpy).toHaveBeenCalledWith('/settings/infrastructure/platforms/truenas', { - scroll: false, - }); - - fireEvent.click(screen.getByRole('button', { name: /Add infrastructure/i })); - fireEvent.click(screen.getByText('VMware vSphere or ESXi')); - - expect(vmwareOpenCreateDialogSpy).toHaveBeenCalledTimes(1); - expect(navigateSpy).toHaveBeenCalledWith('/settings/infrastructure/platforms/vmware', { - scroll: false, - }); - }); - - it('opens the proxmox node modal directly from the add-system picker', () => { - renderWorkspace(); - fireEvent.click(screen.getByRole('button', { name: /Add infrastructure/i })); - fireEvent.click(screen.getByText('Proxmox VE')); - - expect(onSelectAgentSpy).toHaveBeenCalledWith('pve'); - expect(setCurrentNodeTypeSpy).toHaveBeenCalledWith('pve'); - expect(setEditingNodeSpy).toHaveBeenCalledWith(null); - expect(setShowNodeModalSpy).toHaveBeenCalledWith(true); - expect(navigateSpy).toHaveBeenCalledWith('/settings/infrastructure/platforms/proxmox/pve', { - scroll: false, - }); - }); - it('opens reporting details from the top ledger in a drawer', () => { renderWorkspace(); @@ -247,46 +197,71 @@ describe('InfrastructureWorkspace', () => { expect(screen.getByTestId('active-details')).toBeInTheDocument(); }); - it('keeps the ledger visible and opens platform setup in a drawer on platform deep links', () => { + it('renders platform connections as a full-page workspace on platform deep links', () => { mockPathname = '/settings/infrastructure/platforms/truenas'; renderWorkspace(); - expect(screen.getByRole('heading', { name: 'Monitored systems' })).toBeInTheDocument(); + expect(screen.queryByRole('heading', { name: 'Monitored systems' })).toBeNull(); expect(screen.getByText('Platform connections')).toBeInTheDocument(); expect(screen.getByTestId('platform-section')).toBeInTheDocument(); expect(screen.queryByTestId('install-section')).toBeNull(); }); - it('keeps the ledger visible and opens install tools in a drawer on install deep links', () => { + it('renders install tools as a full-page workspace on install deep links', () => { mockPathname = '/settings/infrastructure/install'; renderWorkspace(); - expect(screen.getByRole('heading', { name: 'Monitored systems' })).toBeInTheDocument(); - expect(screen.getByText('Install Pulse agent')).toBeInTheDocument(); + expect(screen.getByText('Choose what to connect')).toBeInTheDocument(); + expect(screen.getByText('Install on a host')).toBeInTheDocument(); expect(screen.getByTestId('install-section')).toBeInTheDocument(); expect(screen.queryByTestId('platform-section')).toBeNull(); }); - it('opens agent profiles from the add-infrastructure chooser secondary action', () => { + it('opens agent profiles inline inside the install workspace', () => { + mockPathname = '/settings/infrastructure/install'; renderWorkspace(); - fireEvent.click(screen.getByRole('button', { name: 'Add infrastructure' })); fireEvent.click(screen.getByRole('button', { name: 'Manage agent profiles' })); expect(screen.getByTestId('agent-profiles')).toBeInTheDocument(); }); - it('closes the platform drawer back to the ledger route', () => { + it('returns from platform connections to the monitored systems route', () => { mockPathname = '/settings/infrastructure/platforms/truenas'; renderWorkspace(); - fireEvent.click(screen.getAllByRole('button', { name: 'Close' })[0]); + fireEvent.click(screen.getByRole('button', { name: 'Back to monitored systems' })); expect(navigateSpy).toHaveBeenCalledWith('/settings/infrastructure', { scroll: false, }); }); + it('routes infrastructure choices through full-page setup routes instead of auto-opening dialogs', () => { + mockPathname = '/settings/infrastructure/install'; + renderWorkspace(); + + fireEvent.click(screen.getByRole('button', { name: /TrueNAS SCALE/i })); + expect(navigateSpy).toHaveBeenCalledWith('/settings/infrastructure/platforms/truenas', { + scroll: false, + }); + + fireEvent.click(screen.getByRole('button', { name: /VMware vSphere or ESXi/i })); + expect(navigateSpy).toHaveBeenCalledWith('/settings/infrastructure/platforms/vmware', { + scroll: false, + }); + + fireEvent.click(screen.getByRole('button', { name: /Proxmox VE/i })); + expect(navigateSpy).toHaveBeenCalledWith('/settings/infrastructure/platforms/proxmox/pve', { + scroll: false, + }); + + expect(setShowNodeModalSpy).not.toHaveBeenCalled(); + expect(setEditingNodeSpy).not.toHaveBeenCalled(); + expect(setCurrentNodeTypeSpy).not.toHaveBeenCalled(); + expect(setModalResetKeySpy).not.toHaveBeenCalled(); + }); + it('collapses read-only sessions back to inventory and hides setup sections', () => { presentationPolicyIsReadOnlyMock.mockReturnValue(true); mockPathname = '/settings/infrastructure/install'; diff --git a/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts b/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts index f24486e55..69044806e 100644 --- a/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts +++ b/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts @@ -903,7 +903,7 @@ describe('Settings architecture guardrails', () => { ); expect(infrastructureWorkspaceSource).not.toContain('