Remove infrastructure setup modals

This commit is contained in:
rcourtman 2026-04-18 22:58:17 +01:00
parent 688bdd4246
commit 264c9377a2
5 changed files with 159 additions and 232 deletions

View file

@ -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

View file

@ -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.

View file

@ -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<InfrastructureWorkspaceProps> =
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<InfrastructureSystemRow[]>(() =>
buildInfrastructureSystemRows({
@ -52,7 +51,7 @@ const InfrastructureWorkspaceContent: Component<InfrastructureWorkspaceProps> =
: [
{
label: 'Add infrastructure',
onSelect: () => setPickerOpen(true),
onSelect: () => navigate(buildInfrastructureWorkspacePath('install'), { scroll: false }),
tone: 'primary' as const,
},
],
@ -68,28 +67,12 @@ const InfrastructureWorkspaceContent: Component<InfrastructureWorkspaceProps> =
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<InfrastructureWorkspaceProps> =
}
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<InfrastructureWorkspaceProps> =
if (activeView() === 'inventory') {
return;
}
setPickerOpen(false);
setProfilesOpen(false);
if (activeView() !== 'install') {
setShowInstallProfiles(false);
}
state.setExpandedRowKey(null);
state.setSelectedIgnoredRowKey(null);
});
return (
<div class="space-y-8">
<ConnectionsTable
rows={rows}
headerActions={headerActions()}
onManageRow={(row) => handleManageAction(row.manage)}
/>
<Show when={activeView() === 'inventory'}>
<ConnectionsTable
rows={rows}
headerActions={headerActions()}
onManageRow={(row) => handleManageAction(row.manage)}
/>
</Show>
<AddSystemPicker
isOpen={pickerOpen()}
onClose={() => setPickerOpen(false)}
onSelect={handleAddSystem}
onManageProfiles={() => {
setPickerOpen(false);
setProfilesOpen(true);
}}
/>
<InfrastructureStopMonitoringDialog />
<Dialog
isOpen={profilesOpen()}
onClose={() => setProfilesOpen(false)}
panelClass="h-[calc(100dvh-2rem)] w-full max-w-[1100px]"
ariaLabel="Agent profiles"
>
<div class="flex h-full flex-col bg-surface">
<div class="border-b border-border bg-surface-alt px-4 py-4 sm:px-6">
<div class="flex items-start justify-between gap-4">
<Show when={!readOnlyWorkspace() && activeView() === 'install'}>
<div class="space-y-6">
<div class="space-y-3">
<button
type="button"
onClick={closeInstallWorkspace}
class="inline-flex items-center gap-2 rounded-md border border-border px-3 py-1.5 text-sm font-medium text-base-content transition-colors hover:bg-surface-hover"
>
Back to monitored systems
</button>
<div class="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
<div class="space-y-1">
<div class="text-[11px] font-semibold uppercase tracking-[0.18em] text-muted">
Add infrastructure
</div>
<div class="text-xl font-semibold text-base-content">Choose what to connect</div>
<div class="text-sm text-muted">
Start with a host install or jump straight to an API-backed platform connection.
</div>
</div>
<button
type="button"
onClick={() => setShowInstallProfiles((value) => !value)}
class="inline-flex items-center rounded-md border border-border px-3 py-1.5 text-sm font-medium text-base-content transition-colors hover:bg-surface-hover"
>
{showInstallProfiles() ? 'Hide agent profiles' : 'Manage agent profiles'}
</button>
</div>
</div>
<div class="grid gap-3 md:grid-cols-2 xl:grid-cols-3">
<For each={ADD_SYSTEM_CHOICES}>
{(choice) => (
<button
type="button"
onClick={() => handleAddSystem(choice)}
class={`flex h-full flex-col items-start justify-between gap-4 rounded-xl border px-4 py-4 text-left transition-colors ${
choice.kind === 'agent'
? 'border-blue-300 bg-blue-50/60 text-base-content dark:border-blue-700 dark:bg-blue-950/20'
: 'border-border bg-surface hover:bg-surface-hover'
}`}
>
<div class="space-y-1.5">
<div class="text-sm font-semibold text-base-content">{choice.title}</div>
<div class="text-xs text-muted">{choice.description}</div>
</div>
<span
class={`inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium ${
choice.method === 'api'
? 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-100'
: 'bg-emerald-100 text-emerald-800 dark:bg-emerald-900 dark:text-emerald-100'
}`}
>
{choice.methodLabel}
</span>
</button>
)}
</For>
</div>
<div class="rounded-xl border border-border bg-surface p-4 shadow-sm sm:p-6">
<InfrastructureInstallerSection />
</div>
<Show when={showInstallProfiles()}>
<div class="rounded-xl border border-border bg-surface p-4 shadow-sm sm:p-6">
<div class="mb-4 space-y-1">
<div class="text-lg font-semibold text-base-content">Agent profiles</div>
<div class="text-sm text-muted">
Manage reusable install defaults for agent-based systems.
</div>
</div>
<button
type="button"
onClick={() => setProfilesOpen(false)}
class="rounded-md p-1 hover:bg-surface-hover hover:text-base-content"
aria-label="Close"
>
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
<AgentProfilesPanel />
</div>
</div>
<div class="flex-1 overflow-y-auto p-4 sm:p-6">
<AgentProfilesPanel />
</div>
</Show>
</div>
</Dialog>
</Show>
<Dialog
isOpen={!readOnlyWorkspace() && activeView() === 'platforms'}
onClose={closeConnectionsWorkspace}
panelClass="h-[calc(100dvh-2rem)] w-full max-w-[1280px]"
ariaLabel="Platform connections"
>
<div class="flex h-full flex-col bg-surface">
<div class="border-b border-border bg-surface-alt px-4 py-4 sm:px-6">
<div class="flex items-start justify-between gap-4">
<div class="space-y-1">
<div class="text-[11px] font-semibold uppercase tracking-[0.18em] text-muted">
Connections
</div>
<div class="text-lg font-semibold text-base-content">Platform connections</div>
<div class="text-sm text-muted">
Configure API-backed providers without leaving the infrastructure ledger.
</div>
<Show when={!readOnlyWorkspace() && activeView() === 'platforms'}>
<div class="space-y-6">
<div class="space-y-3">
<button
type="button"
onClick={closeConnectionsWorkspace}
class="inline-flex items-center gap-2 rounded-md border border-border px-3 py-1.5 text-sm font-medium text-base-content transition-colors hover:bg-surface-hover"
>
Back to monitored systems
</button>
<div class="space-y-1">
<div class="text-[11px] font-semibold uppercase tracking-[0.18em] text-muted">
Connections
</div>
<div class="text-xl font-semibold text-base-content">Platform connections</div>
<div class="text-sm text-muted">
Configure API-backed providers in a full workspace instead of a modal overlay.
</div>
<button
type="button"
onClick={closeConnectionsWorkspace}
class="rounded-md p-1 hover:bg-surface-hover hover:text-base-content"
aria-label="Close"
>
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
</div>
</div>
<div class="flex-1 overflow-y-auto p-4 sm:p-6">
<PlatformConnectionsWorkspace {...props} />
</div>
<PlatformConnectionsWorkspace {...props} />
</div>
</Dialog>
</Show>
<Dialog
isOpen={!readOnlyWorkspace() && activeView() === 'install'}
onClose={closeInstallWorkspace}
panelClass="h-[calc(100dvh-2rem)] w-full max-w-[1280px]"
ariaLabel="Install Pulse agent"
>
<div class="flex h-full flex-col bg-surface">
<div class="border-b border-border bg-surface-alt px-4 py-4 sm:px-6">
<div class="flex items-start justify-between gap-4">
<div class="space-y-1">
<div class="text-[11px] font-semibold uppercase tracking-[0.18em] text-muted">
Install
</div>
<div class="text-lg font-semibold text-base-content">Install Pulse agent</div>
<div class="text-sm text-muted">
Generate Linux, macOS, FreeBSD, and Windows install commands from the same
workspace.
</div>
</div>
<button
type="button"
onClick={closeInstallWorkspace}
class="rounded-md p-1 hover:bg-surface-hover hover:text-base-content"
aria-label="Close"
>
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
</div>
</div>
<div class="flex-1 overflow-y-auto p-4 sm:p-6">
<InfrastructureInstallerSection />
</div>
</div>
</Dialog>
<InfrastructureStopMonitoringDialog />
<Dialog
isOpen={Boolean(state.selectedActiveRow())}

View file

@ -99,15 +99,11 @@ const reportingRow = (overrides: Partial<UnifiedAgentRow> = {}): 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';

View file

@ -903,7 +903,7 @@ describe('Settings architecture guardrails', () => {
);
expect(infrastructureWorkspaceSource).not.toContain('<Subtabs');
expect(infrastructureWorkspaceSource).toContain('Platform connections');
expect(infrastructureWorkspaceSource).toContain('Install Pulse agent');
expect(infrastructureWorkspaceSource).toContain('Choose what to connect');
expect(infrastructureWorkspaceSource).toContain('activeView() === \'platforms\'');
expect(infrastructureWorkspaceSource).toContain('activeView() === \'install\'');
expect(infrastructureWorkspaceSource).not.toContain("activeView() === 'operations'");