Collapse infrastructure workspace onto one connections table

Replace the three-subtab install/connect/inventory narrative with a
single unified connections view at /settings/infrastructure. The base
route now renders one alpha-sorted table whose rows cover every
monitored system — Proxmox VE, PBS, PMG, TrueNAS, VMware, and agent
hosts — under the same name/kind/method/status/last-reported shape.
Adding a new system goes through a single "Add a system" picker whose
tiles route operators straight into the right flow:
/settings/infrastructure/install for the agent choice and
/settings/infrastructure/platforms/<kind> for Proxmox, TrueNAS, and
VMware. The install, platforms, and operations routes remain reachable
as detail surfaces, and read-only sessions continue to redirect the
install view back to the inventory base and suppress the add-system
entry point.

Update the agent-lifecycle subsystem contract to reflect that the
workspace shell no longer mandates a "bare routes default to install"
first-host narrative or a three-subtab layout. Refresh the guardrail
and integration tests that pinned the old tab shape and the legacy
operations landing route.
This commit is contained in:
rcourtman
2026-04-18 00:00:34 +01:00
parent 49c8c84387
commit 00c6dc2dd4
8 changed files with 198 additions and 318 deletions
@@ -356,16 +356,25 @@ an add-only capacity posture.
before non-default connection controls. before non-default connection controls.
7. Keep `frontend-modern/src/components/Settings/InfrastructureWorkspace.tsx` 7. Keep `frontend-modern/src/components/Settings/InfrastructureWorkspace.tsx`
and `frontend-modern/src/components/Settings/infrastructureWorkspaceModel.ts` and `frontend-modern/src/components/Settings/infrastructureWorkspaceModel.ts`
aligned with that same lifecycle path. Bare infrastructure settings routes aligned with that same lifecycle path. The bare
must default to the install workspace, and the workspace shell must make `/settings/infrastructure` route must render a unified Connections table
the first-host sequence explicit before operators drift into reporting and that lists every monitored system — Proxmox VE, PBS, PMG, TrueNAS, VMware,
control surfaces. The third workspace subtab owns the reporting-and-control and agent hosts — as sibling rows sharing a single name/kind/method/status/
surface at route `/settings/infrastructure/operations`; its user-facing last-reported shape, so operators read what is currently being monitored
label is `Inventory` so the workspace narrative leads new operators from in one scan instead of tab-hopping between per-kind surfaces. Adding a new
install-or-connect into a reporting surface named after what it shows, not system must be a single entry point on that table: an `Add a system`
after the internal lifecycle stage. The first-host orientation card must picker whose tiles route the operator into the right flow per kind
hide once any platform connection or agent resource is already reporting, (`/settings/infrastructure/install` for the agent choice,
so established operators do not see a first-system prompt. `/settings/infrastructure/platforms/<kind>` for Proxmox/TrueNAS/VMware
tiles). `/settings/infrastructure/install`,
`/settings/infrastructure/platforms`, and
`/settings/infrastructure/operations` remain reachable as detail routes
for install, platform connections, and legacy reporting/control surfaces
respectively, but the workspace shell must not gate inventory visibility
behind tab navigation. Read-only sessions must continue to redirect the
install detail route back to the unified inventory view and suppress the
add-system entry point on the base table so presentation-policy
restrictions still hold.
8. Keep post-install lifecycle completion explicit inside 8. Keep post-install lifecycle completion explicit inside
`frontend-modern/src/components/Settings/InfrastructureInstallerSection.tsx` `frontend-modern/src/components/Settings/InfrastructureInstallerSection.tsx`
and `frontend-modern/src/components/Settings/useInfrastructureInstallState.tsx`. and `frontend-modern/src/components/Settings/useInfrastructureInstallState.tsx`.
@@ -1,17 +1,15 @@
import { Component, Match, Show, Switch, createEffect, createMemo } from 'solid-js'; import { Component, Match, Switch, createEffect, createMemo, createSignal } from 'solid-js';
import { useLocation, useNavigate } from '@solidjs/router'; import { useLocation, useNavigate } from '@solidjs/router';
import { Card } from '@/components/shared/Card';
import { Subtabs } from '@/components/shared/Subtabs';
import { presentationPolicyIsReadOnly } from '@/stores/sessionPresentationPolicy'; import { presentationPolicyIsReadOnly } from '@/stores/sessionPresentationPolicy';
import { SELF_HOSTED_PRO_BILLING_PRESENTATION } from './selfHostedBillingPresentation';
import { InfrastructureInstallPanel } from './InfrastructureInstallPanel'; import { InfrastructureInstallPanel } from './InfrastructureInstallPanel';
import { InfrastructureReportingPanel } from './InfrastructureReportingPanel'; import { InfrastructureReportingPanel } from './InfrastructureReportingPanel';
import { PlatformConnectionsWorkspace } from './PlatformConnectionsWorkspace'; import { PlatformConnectionsWorkspace } from './PlatformConnectionsWorkspace';
import { ConnectionsTable } from './ConnectionsTable';
import { AddSystemPicker, type AddSystemChoice } from './AddSystemPicker';
import { buildConnectionRows, type ConnectionRow } from './connectionsTableModel';
import { import {
INFRASTRUCTURE_WORKSPACE_TABS,
buildInfrastructureWorkspacePath, buildInfrastructureWorkspacePath,
getInfrastructureWorkspaceViewFromPath, getInfrastructureWorkspaceViewFromPath,
type InfrastructureWorkspaceView,
} from './infrastructureWorkspaceModel'; } from './infrastructureWorkspaceModel';
import type { InfrastructurePlatformSettingsProps } from './proxmoxSettingsModel'; import type { InfrastructurePlatformSettingsProps } from './proxmoxSettingsModel';
@@ -22,126 +20,58 @@ export const InfrastructureWorkspace: Component<InfrastructureWorkspaceProps> =
const location = useLocation(); const location = useLocation();
const activeView = createMemo(() => getInfrastructureWorkspaceViewFromPath(location.pathname)); const activeView = createMemo(() => getInfrastructureWorkspaceViewFromPath(location.pathname));
const readOnlyWorkspace = createMemo(() => presentationPolicyIsReadOnly()); const readOnlyWorkspace = createMemo(() => presentationPolicyIsReadOnly());
const installPath = createMemo(() => buildInfrastructureWorkspacePath('install')); const [pickerOpen, setPickerOpen] = createSignal(false);
const platformsPath = createMemo(() => buildInfrastructureWorkspacePath('platforms'));
const inventoryPath = createMemo(() => buildInfrastructureWorkspacePath('inventory'));
const visibleTabs = createMemo(() =>
readOnlyWorkspace()
? INFRASTRUCTURE_WORKSPACE_TABS.filter((tab) => tab.id === 'inventory')
: INFRASTRUCTURE_WORKSPACE_TABS,
);
const hasAnySystem = createMemo(() => {
const summary = props.platformConnectionsSummary?.();
const platformCount = summary
? summary.pveCount +
summary.pbsCount +
summary.pmgCount +
summary.truenasCount +
summary.vmwareCount
: 0;
const agentCount = props.agentStateResources?.()?.length ?? 0;
return platformCount + agentCount > 0;
});
const showOrientation = createMemo(() => !readOnlyWorkspace() && !hasAnySystem());
const openView = (view: InfrastructureWorkspaceView) => const rows = createMemo<ConnectionRow[]>(() =>
navigate(buildInfrastructureWorkspacePath(view)); buildConnectionRows({
pveNodes: props.pveNodes(),
pbsNodes: props.pbsNodes(),
pmgNodes: props.pmgNodes(),
truenasConnections: props.trueNASSettings.connections(),
vmwareConnections: props.vmwareSettings.connections(),
agentResources: props.agentStateResources?.() ?? [],
}),
);
const handleAddSystem = (choice: AddSystemChoice) => {
setPickerOpen(false);
if (choice.kind === 'agent') {
navigate('/settings/infrastructure/install');
return;
}
if (choice.kind === 'truenas') {
navigate('/settings/infrastructure/platforms/truenas');
return;
}
if (choice.kind === 'vmware') {
navigate('/settings/infrastructure/platforms/vmware');
return;
}
props.onSelectAgent(choice.kind);
navigate('/settings/infrastructure/platforms/proxmox');
};
createEffect(() => { createEffect(() => {
if (readOnlyWorkspace() && activeView() !== 'inventory') { if (readOnlyWorkspace() && activeView() === 'install') {
navigate(inventoryPath(), { replace: true }); navigate(buildInfrastructureWorkspacePath('inventory'), { replace: true });
} }
}); });
return ( return (
<div class="space-y-6"> <div class="space-y-6">
<Show when={showOrientation()}>
<Card padding="lg" class="rounded-xl border border-border shadow-sm">
<div class="space-y-4">
<div class="space-y-2">
<h3 class="text-base font-semibold text-base-content">Connect your first system</h3>
<p class="text-sm text-muted">
Use Install on a host for the first machine that should run the unified agent. If
the first system is API-backed, such as Proxmox or TrueNAS, go straight to Platform
connections.
</p>
</div>
<div class="grid gap-3 lg:grid-cols-3">
<div class="rounded-md border border-border bg-surface px-4 py-3">
<p class="text-xs font-semibold uppercase tracking-wide text-muted">
1. Choose path
</p>
<p class="mt-1 text-sm text-base-content">
Choose Install on a host for agent-managed systems, or open Platform connections
for Proxmox, TrueNAS, and other systems Pulse should poll through their own APIs.
</p>
</div>
<div class="rounded-md border border-border bg-surface px-4 py-3">
<p class="text-xs font-semibold uppercase tracking-wide text-muted">
2. Generate access
</p>
<p class="mt-1 text-sm text-base-content">
Create the install token Pulse expects for the first monitored host, or add the
API credentials Pulse should store for API-backed platforms like Proxmox and
TrueNAS.
</p>
</div>
<div class="rounded-md border border-border bg-surface px-4 py-3">
<p class="text-xs font-semibold uppercase tracking-wide text-muted">
3. Confirm reporting
</p>
<p class="mt-1 text-sm text-base-content">
Run the command on that machine, then open Inventory once the first system starts
reporting.
</p>
</div>
</div>
<div class="flex flex-wrap gap-3">
<button
type="button"
onClick={() => navigate(installPath())}
aria-current={activeView() === 'install' ? 'page' : undefined}
class={`inline-flex items-center justify-center rounded-md px-4 py-2 text-sm font-medium transition-colors ${
activeView() === 'install'
? 'bg-blue-600 text-white'
: 'border border-border bg-surface text-base-content hover:bg-surface-hover'
}`}
>
Open Install on a host
</button>
<button
type="button"
onClick={() => navigate(platformsPath())}
aria-current={activeView() === 'platforms' ? 'page' : undefined}
class={`inline-flex items-center justify-center rounded-md px-4 py-2 text-sm font-medium transition-colors ${
activeView() === 'platforms'
? 'bg-emerald-600 text-white'
: 'border border-border bg-surface text-base-content hover:bg-surface-hover'
}`}
>
Open Platform connections
</button>
</div>
<p class="text-sm text-muted">
{SELF_HOSTED_PRO_BILLING_PRESENTATION.infrastructureWorkspaceReferral}
</p>
</div>
</Card>
</Show>
<div class="space-y-3">
<Subtabs
value={activeView()}
onChange={(value) => openView(value as InfrastructureWorkspaceView)}
ariaLabel="Infrastructure workspace"
tabs={visibleTabs().map((tab) => ({
value: tab.id,
label: tab.label,
}))}
/>
</div>
<Switch> <Switch>
<Match when={activeView() === 'inventory'}>
<ConnectionsTable
rows={rows}
onAddSystem={readOnlyWorkspace() ? undefined : () => setPickerOpen(true)}
/>
<AddSystemPicker
isOpen={pickerOpen()}
onClose={() => setPickerOpen(false)}
onSelect={handleAddSystem}
/>
</Match>
<Match when={activeView() === 'install'}> <Match when={activeView() === 'install'}>
<InfrastructureInstallPanel /> <InfrastructureInstallPanel />
</Match> </Match>
@@ -150,10 +80,12 @@ export const InfrastructureWorkspace: Component<InfrastructureWorkspaceProps> =
<PlatformConnectionsWorkspace {...props} /> <PlatformConnectionsWorkspace {...props} />
</Match> </Match>
<Match when={activeView() === 'inventory'}> <Match when={activeView() === 'operations'}>
<InfrastructureReportingPanel <InfrastructureReportingPanel
{...props} {...props}
onManagePlatformConnections={() => openView('platforms')} onManagePlatformConnections={() =>
navigate('/settings/infrastructure/platforms')
}
/> />
</Match> </Match>
</Switch> </Switch>
@@ -1054,7 +1054,7 @@ describe('InfrastructureOperationsController agent lookup', () => {
expect(navigateMock).toHaveBeenCalledWith('/dashboard'); expect(navigateMock).toHaveBeenCalledWith('/dashboard');
fireEvent.click(screen.getByRole('button', { name: 'Open inventory' })); fireEvent.click(screen.getByRole('button', { name: 'Open inventory' }));
expect(navigateMock).toHaveBeenCalledWith('/settings/infrastructure/operations'); expect(navigateMock).toHaveBeenCalledWith('/settings/infrastructure');
}); });
it('shows error message when agent is not found', async () => { it('shows error message when agent is not found', async () => {
@@ -1,9 +1,8 @@
import { cleanup, fireEvent, render, screen } from '@solidjs/testing-library'; import { cleanup, fireEvent, render, screen } from '@solidjs/testing-library';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { SELF_HOSTED_PRO_BILLING_PRESENTATION } from '../selfHostedBillingPresentation';
import { InfrastructureWorkspace } from '../InfrastructureWorkspace'; import { InfrastructureWorkspace } from '../InfrastructureWorkspace';
let mockPathname = '/settings'; let mockPathname = '/settings/infrastructure';
const navigateSpy = vi.hoisted(() => vi.fn()); const navigateSpy = vi.hoisted(() => vi.fn());
const presentationPolicyIsReadOnlyMock = vi.hoisted(() => vi.fn(() => false)); const presentationPolicyIsReadOnlyMock = vi.hoisted(() => vi.fn(() => false));
@@ -21,22 +20,46 @@ vi.mock('@/stores/sessionPresentationPolicy', () => ({
})); }));
vi.mock('../InfrastructureInstallPanel', () => ({ vi.mock('../InfrastructureInstallPanel', () => ({
InfrastructureInstallPanel: () => <div data-testid="unified-agents">install</div>, InfrastructureInstallPanel: () => <div data-testid="install-panel">install</div>,
})); }));
vi.mock('../InfrastructureReportingPanel', () => ({ vi.mock('../InfrastructureReportingPanel', () => ({
InfrastructureReportingPanel: () => <div data-testid="agent-profiles">profiles</div>, InfrastructureReportingPanel: () => <div data-testid="reporting-panel">operations</div>,
})); }));
vi.mock('../PlatformConnectionsWorkspace', () => ({ vi.mock('../PlatformConnectionsWorkspace', () => ({
PlatformConnectionsWorkspace: () => <div data-testid="platform-connections">platforms</div>, PlatformConnectionsWorkspace: () => <div data-testid="platform-connections">platforms</div>,
})); }));
const onSelectAgentSpy = vi.fn();
const baseProps = () =>
({
pveNodes: () => [],
pbsNodes: () => [],
pmgNodes: () => [],
agentStateResources: () => [],
trueNASSettings: { connections: () => [] },
vmwareSettings: { connections: () => [] },
platformConnectionsSummary: () => ({
pveCount: 0,
pbsCount: 0,
pmgCount: 0,
truenasCount: 0,
truenasAvailable: true,
vmwareCount: 0,
vmwareAvailable: true,
}),
selectedAgent: () => 'pve',
onSelectAgent: onSelectAgentSpy,
}) as any;
describe('InfrastructureWorkspace', () => { describe('InfrastructureWorkspace', () => {
beforeEach(() => { beforeEach(() => {
navigateSpy.mockReset(); navigateSpy.mockReset();
presentationPolicyIsReadOnlyMock.mockReset(); presentationPolicyIsReadOnlyMock.mockReset();
presentationPolicyIsReadOnlyMock.mockReturnValue(false); presentationPolicyIsReadOnlyMock.mockReturnValue(false);
onSelectAgentSpy.mockReset();
mockPathname = '/settings/infrastructure'; mockPathname = '/settings/infrastructure';
}); });
@@ -44,170 +67,111 @@ describe('InfrastructureWorkspace', () => {
cleanup(); cleanup();
}); });
const renderWorkspace = () => const renderWorkspace = (propOverrides: Record<string, unknown> = {}) =>
render( render(() => (<InfrastructureWorkspace {...{ ...baseProps(), ...propOverrides }} />) as any);
() =>
(
<InfrastructureWorkspace
{...({
pveNodes: () => [],
pbsNodes: () => [],
pmgNodes: () => [],
} as any)}
/>
) as any,
);
it('defaults bare infrastructure routing to install on a host', () => { it('renders the unified connections table at the base infrastructure route', () => {
renderWorkspace(); renderWorkspace();
const tablist = screen.getByRole('tablist', { name: 'Infrastructure workspace' }); expect(screen.getByText('Connections')).toBeInTheDocument();
expect(tablist).toBeInTheDocument(); expect(screen.getByRole('button', { name: /Add a system/i })).toBeInTheDocument();
expect(screen.getByText('Connect your first system')).toBeInTheDocument(); expect(screen.queryByTestId('install-panel')).toBeNull();
expect( expect(screen.queryByTestId('platform-connections')).toBeNull();
screen.getByText(
'Use Install on a host for the first machine that should run the unified agent. If the first system is API-backed, such as Proxmox or TrueNAS, go straight to Platform connections.',
),
).toBeInTheDocument();
expect(screen.getByText('1. Choose path')).toBeInTheDocument();
expect(screen.getByText('2. Generate access')).toBeInTheDocument();
expect(screen.getByText('3. Confirm reporting')).toBeInTheDocument();
expect(
screen.getByText(SELF_HOSTED_PRO_BILLING_PRESENTATION.infrastructureWorkspaceReferral),
).toBeInTheDocument();
expect(screen.getByRole('tab', { name: 'Install on a host' })).toHaveAttribute(
'aria-selected',
'true',
);
expect(screen.getByRole('tab', { name: 'Platform connections' })).toHaveAttribute(
'aria-selected',
'false',
);
expect(screen.getByRole('tab', { name: 'Inventory' })).toHaveAttribute(
'aria-selected',
'false',
);
expect(screen.getByTestId('unified-agents')).toBeInTheDocument();
}); });
it('uses the shared subtabs to switch to platform connections', () => { it('merges every connection source into a single alpha-sorted table', () => {
renderWorkspace(); renderWorkspace({
pveNodes: () => [
{ id: 'n1', name: 'zeus', host: '10.0.0.1', type: 'pve', status: 'connected' },
],
agentStateResources: () => [
{ id: 'a1', name: 'tower', displayName: 'tower', status: 'online', lastSeen: Date.now() },
],
trueNASSettings: {
connections: () => [
{ id: 't1', name: 'nas.home', host: '10.0.0.2', enabled: true, insecureSkipVerify: false, useHttps: true },
],
},
});
fireEvent.click(screen.getByRole('tab', { name: 'Platform connections' })); const rowNames = screen.getAllByText(/zeus|tower|nas\.home/).map((el) => el.textContent);
expect(rowNames).toContain('nas.home');
expect(navigateSpy).toHaveBeenCalledWith('/settings/infrastructure/platforms'); expect(rowNames).toContain('tower');
expect(rowNames).toContain('zeus');
}); });
it('renders the platform workspace from the router pathname', () => { it('opens the add-system picker when the add button is clicked', () => {
mockPathname = '/settings/infrastructure/platforms';
renderWorkspace(); renderWorkspace();
expect(screen.getByRole('tab', { name: 'Platform connections' })).toHaveAttribute( fireEvent.click(screen.getByRole('button', { name: /Add a system/i }));
'aria-selected',
'true', expect(screen.getByText('Linux or Docker host (agent)')).toBeInTheDocument();
); expect(screen.getByText('Proxmox VE')).toBeInTheDocument();
expect(screen.getByTestId('platform-connections')).toBeInTheDocument(); expect(screen.getByText('TrueNAS SCALE')).toBeInTheDocument();
}); });
it('keeps the reporting route available for established operators', () => { it('routes the agent-host choice to the dedicated install workspace', () => {
mockPathname = '/settings/infrastructure/operations';
renderWorkspace(); renderWorkspace();
fireEvent.click(screen.getByRole('button', { name: /Add a system/i }));
expect(screen.getByRole('tab', { name: 'Inventory' })).toHaveAttribute( fireEvent.click(screen.getByText('Linux or Docker host (agent)'));
'aria-selected',
'true',
);
expect(screen.getByTestId('agent-profiles')).toBeInTheDocument();
});
it('uses the guided workspace actions to open install and platform paths', () => {
renderWorkspace();
fireEvent.click(screen.getByRole('button', { name: 'Open Install on a host' }));
fireEvent.click(screen.getByRole('button', { name: 'Open Platform connections' }));
expect(navigateSpy).toHaveBeenNthCalledWith(1, '/settings/infrastructure/install');
expect(navigateSpy).toHaveBeenNthCalledWith(2, '/settings/infrastructure/platforms');
});
it('returns to the base settings route when switching away from platform connections', () => {
mockPathname = '/settings/infrastructure/platforms';
renderWorkspace();
fireEvent.click(screen.getByRole('tab', { name: 'Install on a host' }));
expect(navigateSpy).toHaveBeenCalledWith('/settings/infrastructure/install'); expect(navigateSpy).toHaveBeenCalledWith('/settings/infrastructure/install');
}); });
it('hides the first-system orientation card once any platform connection exists', () => { it('routes TrueNAS and VMware choices to their platform panels', () => {
render( renderWorkspace();
() => fireEvent.click(screen.getByRole('button', { name: /Add a system/i }));
( fireEvent.click(screen.getByText('TrueNAS SCALE'));
<InfrastructureWorkspace expect(navigateSpy).toHaveBeenCalledWith('/settings/infrastructure/platforms/truenas');
{...({
pveNodes: () => [],
pbsNodes: () => [],
pmgNodes: () => [],
agentStateResources: () => [],
platformConnectionsSummary: () => ({
pveCount: 1,
pbsCount: 0,
pmgCount: 0,
truenasCount: 0,
truenasAvailable: true,
vmwareCount: 0,
vmwareAvailable: true,
}),
} as any)}
/>
) as any,
);
expect(screen.queryByText('Connect your first system')).not.toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: /Add a system/i }));
expect(screen.getByRole('tab', { name: 'Install on a host' })).toBeInTheDocument(); fireEvent.click(screen.getByText('VMware vSphere or ESXi'));
expect(navigateSpy).toHaveBeenCalledWith('/settings/infrastructure/platforms/vmware');
}); });
it('hides the first-system orientation card once any agent resource is reporting', () => { it('preselects the Proxmox kind and lands on the platforms route for PVE, PBS, and PMG', () => {
render( renderWorkspace();
() =>
(
<InfrastructureWorkspace
{...({
pveNodes: () => [],
pbsNodes: () => [],
pmgNodes: () => [],
agentStateResources: () => [{ id: 'agent-1' }],
platformConnectionsSummary: () => ({
pveCount: 0,
pbsCount: 0,
pmgCount: 0,
truenasCount: 0,
truenasAvailable: true,
vmwareCount: 0,
vmwareAvailable: true,
}),
} as any)}
/>
) as any,
);
expect(screen.queryByText('Connect your first system')).not.toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: /Add a system/i }));
fireEvent.click(screen.getByText('Proxmox Backup Server'));
expect(onSelectAgentSpy).toHaveBeenCalledWith('pbs');
expect(navigateSpy).toHaveBeenCalledWith('/settings/infrastructure/platforms/proxmox');
}); });
it('collapses the workspace to reporting and redirects install routes in read-only mode', () => { it('renders the install workspace when the URL is /install', () => {
mockPathname = '/settings/infrastructure/install';
renderWorkspace();
expect(screen.getByTestId('install-panel')).toBeInTheDocument();
});
it('renders the platforms workspace when the URL is under /platforms', () => {
mockPathname = '/settings/infrastructure/platforms/truenas';
renderWorkspace();
expect(screen.getByTestId('platform-connections')).toBeInTheDocument();
});
it('keeps /operations reachable as a legacy detail route', () => {
mockPathname = '/settings/infrastructure/operations';
renderWorkspace();
expect(screen.getByTestId('reporting-panel')).toBeInTheDocument();
});
it('hides the add-system action and redirects install routes in read-only mode', () => {
presentationPolicyIsReadOnlyMock.mockReturnValue(true); presentationPolicyIsReadOnlyMock.mockReturnValue(true);
mockPathname = '/settings/infrastructure/install'; mockPathname = '/settings/infrastructure/install';
renderWorkspace(); renderWorkspace();
expect(screen.queryByText('Connect your first system')).not.toBeInTheDocument(); expect(navigateSpy).toHaveBeenCalledWith('/settings/infrastructure', { replace: true });
expect(screen.queryByRole('tab', { name: 'Install on a host' })).not.toBeInTheDocument(); });
expect(screen.queryByRole('tab', { name: 'Platform connections' })).not.toBeInTheDocument();
expect(screen.getByRole('tab', { name: 'Inventory' })).toHaveAttribute( it('still renders the connections table without an add button in read-only mode', () => {
'aria-selected', presentationPolicyIsReadOnlyMock.mockReturnValue(true);
'false', mockPathname = '/settings/infrastructure';
); renderWorkspace();
expect(navigateSpy).toHaveBeenCalledWith('/settings/infrastructure/operations', {
replace: true, expect(screen.getByText('Connections')).toBeInTheDocument();
}); expect(screen.queryByRole('button', { name: /Add a system/i })).toBeNull();
}); });
}); });
@@ -785,7 +785,7 @@ describe('monitored-system model guardrails', () => {
"pathname.startsWith('/settings/infrastructure/proxmox')", "pathname.startsWith('/settings/infrastructure/proxmox')",
); );
expect(infrastructureWorkspaceModelSource).toContain( expect(infrastructureWorkspaceModelSource).toContain(
"path: '/settings/infrastructure/operations'", "operations: '/settings/infrastructure/operations'",
); );
}); });
@@ -593,12 +593,9 @@ describe('Settings architecture guardrails', () => {
expect(proLicensePanelSource).not.toContain('description="Manage self-hosted billing'); expect(proLicensePanelSource).not.toContain('description="Manage self-hosted billing');
expect(proLicensePanelSource).not.toContain('title="Plan"'); expect(proLicensePanelSource).not.toContain('title="Plan"');
expect(proLicensePanelSource).not.toContain('title="Usage"'); expect(proLicensePanelSource).not.toContain('title="Usage"');
expect(infrastructureWorkspaceSource).toContain('./selfHostedBillingPresentation'); expect(infrastructureWorkspaceSource).not.toContain('./selfHostedBillingPresentation');
expect(infrastructureWorkspaceSource).toContain(
'SELF_HOSTED_PRO_BILLING_PRESENTATION.infrastructureWorkspaceReferral',
);
expect(infrastructureWorkspaceSource).not.toContain( expect(infrastructureWorkspaceSource).not.toContain(
'Billing, monitored-system limits, and Pulse Pro license status live in Pulse Pro, not here.', 'SELF_HOSTED_PRO_BILLING_PRESENTATION.infrastructureWorkspaceReferral',
); );
expect(proLicensePlanSectionSource).toContain('CommercialStatGrid'); expect(proLicensePlanSectionSource).toContain('CommercialStatGrid');
expect(proLicensePlanSectionSource).toContain('getLicenseStatusLoadingState'); expect(proLicensePlanSectionSource).toContain('getLicenseStatusLoadingState');
@@ -892,7 +889,7 @@ describe('Settings architecture guardrails', () => {
); );
expect(infrastructureWorkspaceSource).toContain('createEffect(() =>'); expect(infrastructureWorkspaceSource).toContain('createEffect(() =>');
expect(infrastructureWorkspaceSource).toContain( expect(infrastructureWorkspaceSource).toContain(
"if (readOnlyWorkspace() && activeView() !== 'inventory')", "if (readOnlyWorkspace() && activeView() === 'install')",
); );
expect(infrastructureWorkspaceSource).toContain('InfrastructureInstallPanel'); expect(infrastructureWorkspaceSource).toContain('InfrastructureInstallPanel');
expect(infrastructureWorkspaceSource).toContain('PlatformConnectionsWorkspace'); expect(infrastructureWorkspaceSource).toContain('PlatformConnectionsWorkspace');
@@ -958,8 +955,8 @@ describe('Settings architecture guardrails', () => {
); );
expect(infrastructureActiveRowDetailsSource).toContain('useInfrastructureOperationsContext'); expect(infrastructureActiveRowDetailsSource).toContain('useInfrastructureOperationsContext');
expect(infrastructureIgnoredRowDetailsSource).toContain('useInfrastructureOperationsContext'); expect(infrastructureIgnoredRowDetailsSource).toContain('useInfrastructureOperationsContext');
expect(infrastructureWorkspaceModelSource).toContain( expect(infrastructureWorkspaceModelSource).not.toContain(
'export const INFRASTRUCTURE_WORKSPACE_TABS', 'INFRASTRUCTURE_WORKSPACE_TABS',
); );
expect(infrastructureWorkspaceModelSource).toContain( expect(infrastructureWorkspaceModelSource).toContain(
'export function getInfrastructureWorkspaceViewFromPath', 'export function getInfrastructureWorkspaceViewFromPath',
@@ -1501,7 +1498,7 @@ describe('Settings architecture guardrails', () => {
'Setup changes stay unavailable in this read-only session.', 'Setup changes stay unavailable in this read-only session.',
); );
expect(infrastructureWorkspaceSource).toContain('presentationPolicyIsReadOnly'); expect(infrastructureWorkspaceSource).toContain('presentationPolicyIsReadOnly');
expect(infrastructureWorkspaceSource).toContain("tab.id === 'inventory'"); expect(infrastructureWorkspaceSource).toContain("activeView() === 'inventory'");
}); });
it('keeps relay shell copy on the shared relay presentation owner', () => { it('keeps relay shell copy on the shared relay presentation owner', () => {
@@ -44,7 +44,7 @@ describe('useSettingsNavigation', () => {
renderHarness('/settings'); renderHarness('/settings');
await waitFor(() => { await waitFor(() => {
expect(navigateSpy).toHaveBeenCalledWith('/settings/infrastructure/operations', { expect(navigateSpy).toHaveBeenCalledWith('/settings/infrastructure', {
replace: true, replace: true,
scroll: false, scroll: false,
}); });
@@ -57,7 +57,7 @@ describe('useSettingsNavigation', () => {
fireEvent.click(screen.getByRole('button', { name: 'open infrastructure settings' })); fireEvent.click(screen.getByRole('button', { name: 'open infrastructure settings' }));
expect(navigateSpy).toHaveBeenCalledWith('/settings/infrastructure/operations', { expect(navigateSpy).toHaveBeenCalledWith('/settings/infrastructure', {
scroll: false, scroll: false,
}); });
}); });
@@ -1,28 +1,11 @@
export type InfrastructureWorkspaceView = 'install' | 'platforms' | 'inventory'; export type InfrastructureWorkspaceView = 'inventory' | 'install' | 'platforms' | 'operations';
export interface InfrastructureWorkspaceTabDefinition { const INFRASTRUCTURE_WORKSPACE_PATHS: Record<InfrastructureWorkspaceView, string> = {
id: InfrastructureWorkspaceView; inventory: '/settings/infrastructure',
label: string; install: '/settings/infrastructure/install',
path: string; platforms: '/settings/infrastructure/platforms',
} operations: '/settings/infrastructure/operations',
};
export const INFRASTRUCTURE_WORKSPACE_TABS: readonly InfrastructureWorkspaceTabDefinition[] = [
{
id: 'install',
label: 'Install on a host',
path: '/settings/infrastructure/install',
},
{
id: 'platforms',
label: 'Platform connections',
path: '/settings/infrastructure/platforms',
},
{
id: 'inventory',
label: 'Inventory',
path: '/settings/infrastructure/operations',
},
];
export function getInfrastructureWorkspaceViewFromPath( export function getInfrastructureWorkspaceViewFromPath(
pathname: string, pathname: string,
@@ -35,20 +18,15 @@ export function getInfrastructureWorkspaceViewFromPath(
) { ) {
return 'platforms'; return 'platforms';
} }
if (pathname.startsWith('/settings/infrastructure/operations')) {
return 'inventory';
}
if (pathname.startsWith('/settings/infrastructure/install')) { if (pathname.startsWith('/settings/infrastructure/install')) {
return 'install'; return 'install';
} }
return 'install'; if (pathname.startsWith('/settings/infrastructure/operations')) {
return 'operations';
}
return 'inventory';
} }
export function buildInfrastructureWorkspacePath( export function buildInfrastructureWorkspacePath(view: InfrastructureWorkspaceView): string {
view: InfrastructureWorkspaceView, return INFRASTRUCTURE_WORKSPACE_PATHS[view];
): string {
return (
INFRASTRUCTURE_WORKSPACE_TABS.find((tab) => tab.id === view)?.path ??
'/settings/infrastructure/install'
);
} }