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

View file

@ -356,16 +356,25 @@ an add-only capacity posture.
before non-default connection controls.
7. Keep `frontend-modern/src/components/Settings/InfrastructureWorkspace.tsx`
and `frontend-modern/src/components/Settings/infrastructureWorkspaceModel.ts`
aligned with that same lifecycle path. Bare infrastructure settings routes
must default to the install workspace, and the workspace shell must make
the first-host sequence explicit before operators drift into reporting and
control surfaces. The third workspace subtab owns the reporting-and-control
surface at route `/settings/infrastructure/operations`; its user-facing
label is `Inventory` so the workspace narrative leads new operators from
install-or-connect into a reporting surface named after what it shows, not
after the internal lifecycle stage. The first-host orientation card must
hide once any platform connection or agent resource is already reporting,
so established operators do not see a first-system prompt.
aligned with that same lifecycle path. The bare
`/settings/infrastructure` route must render a unified Connections table
that lists every monitored system — Proxmox VE, PBS, PMG, TrueNAS, VMware,
and agent hosts — as sibling rows sharing a single name/kind/method/status/
last-reported shape, so operators read what is currently being monitored
in one scan instead of tab-hopping between per-kind surfaces. Adding a new
system must be a single entry point on that table: an `Add a system`
picker whose tiles route the operator into the right flow per kind
(`/settings/infrastructure/install` for the agent choice,
`/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
`frontend-modern/src/components/Settings/InfrastructureInstallerSection.tsx`
and `frontend-modern/src/components/Settings/useInfrastructureInstallState.tsx`.

View file

@ -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 { Card } from '@/components/shared/Card';
import { Subtabs } from '@/components/shared/Subtabs';
import { presentationPolicyIsReadOnly } from '@/stores/sessionPresentationPolicy';
import { SELF_HOSTED_PRO_BILLING_PRESENTATION } from './selfHostedBillingPresentation';
import { InfrastructureInstallPanel } from './InfrastructureInstallPanel';
import { InfrastructureReportingPanel } from './InfrastructureReportingPanel';
import { PlatformConnectionsWorkspace } from './PlatformConnectionsWorkspace';
import { ConnectionsTable } from './ConnectionsTable';
import { AddSystemPicker, type AddSystemChoice } from './AddSystemPicker';
import { buildConnectionRows, type ConnectionRow } from './connectionsTableModel';
import {
INFRASTRUCTURE_WORKSPACE_TABS,
buildInfrastructureWorkspacePath,
getInfrastructureWorkspaceViewFromPath,
type InfrastructureWorkspaceView,
} from './infrastructureWorkspaceModel';
import type { InfrastructurePlatformSettingsProps } from './proxmoxSettingsModel';
@ -22,126 +20,58 @@ export const InfrastructureWorkspace: Component<InfrastructureWorkspaceProps> =
const location = useLocation();
const activeView = createMemo(() => getInfrastructureWorkspaceViewFromPath(location.pathname));
const readOnlyWorkspace = createMemo(() => presentationPolicyIsReadOnly());
const installPath = createMemo(() => buildInfrastructureWorkspacePath('install'));
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 [pickerOpen, setPickerOpen] = createSignal(false);
const openView = (view: InfrastructureWorkspaceView) =>
navigate(buildInfrastructureWorkspacePath(view));
const rows = createMemo<ConnectionRow[]>(() =>
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(() => {
if (readOnlyWorkspace() && activeView() !== 'inventory') {
navigate(inventoryPath(), { replace: true });
if (readOnlyWorkspace() && activeView() === 'install') {
navigate(buildInfrastructureWorkspacePath('inventory'), { replace: true });
}
});
return (
<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>
<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'}>
<InfrastructureInstallPanel />
</Match>
@ -150,10 +80,12 @@ export const InfrastructureWorkspace: Component<InfrastructureWorkspaceProps> =
<PlatformConnectionsWorkspace {...props} />
</Match>
<Match when={activeView() === 'inventory'}>
<Match when={activeView() === 'operations'}>
<InfrastructureReportingPanel
{...props}
onManagePlatformConnections={() => openView('platforms')}
onManagePlatformConnections={() =>
navigate('/settings/infrastructure/platforms')
}
/>
</Match>
</Switch>

View file

@ -1054,7 +1054,7 @@ describe('InfrastructureOperationsController agent lookup', () => {
expect(navigateMock).toHaveBeenCalledWith('/dashboard');
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 () => {

View file

@ -1,9 +1,8 @@
import { cleanup, fireEvent, render, screen } from '@solidjs/testing-library';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { SELF_HOSTED_PRO_BILLING_PRESENTATION } from '../selfHostedBillingPresentation';
import { InfrastructureWorkspace } from '../InfrastructureWorkspace';
let mockPathname = '/settings';
let mockPathname = '/settings/infrastructure';
const navigateSpy = vi.hoisted(() => vi.fn());
const presentationPolicyIsReadOnlyMock = vi.hoisted(() => vi.fn(() => false));
@ -21,22 +20,46 @@ vi.mock('@/stores/sessionPresentationPolicy', () => ({
}));
vi.mock('../InfrastructureInstallPanel', () => ({
InfrastructureInstallPanel: () => <div data-testid="unified-agents">install</div>,
InfrastructureInstallPanel: () => <div data-testid="install-panel">install</div>,
}));
vi.mock('../InfrastructureReportingPanel', () => ({
InfrastructureReportingPanel: () => <div data-testid="agent-profiles">profiles</div>,
InfrastructureReportingPanel: () => <div data-testid="reporting-panel">operations</div>,
}));
vi.mock('../PlatformConnectionsWorkspace', () => ({
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', () => {
beforeEach(() => {
navigateSpy.mockReset();
presentationPolicyIsReadOnlyMock.mockReset();
presentationPolicyIsReadOnlyMock.mockReturnValue(false);
onSelectAgentSpy.mockReset();
mockPathname = '/settings/infrastructure';
});
@ -44,170 +67,111 @@ describe('InfrastructureWorkspace', () => {
cleanup();
});
const renderWorkspace = () =>
render(
() =>
(
<InfrastructureWorkspace
{...({
pveNodes: () => [],
pbsNodes: () => [],
pmgNodes: () => [],
} as any)}
/>
) as any,
);
const renderWorkspace = (propOverrides: Record<string, unknown> = {}) =>
render(() => (<InfrastructureWorkspace {...{ ...baseProps(), ...propOverrides }} />) as any);
it('defaults bare infrastructure routing to install on a host', () => {
it('renders the unified connections table at the base infrastructure route', () => {
renderWorkspace();
const tablist = screen.getByRole('tablist', { name: 'Infrastructure workspace' });
expect(tablist).toBeInTheDocument();
expect(screen.getByText('Connect your first system')).toBeInTheDocument();
expect(
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();
expect(screen.getByText('Connections')).toBeInTheDocument();
expect(screen.getByRole('button', { name: /Add a system/i })).toBeInTheDocument();
expect(screen.queryByTestId('install-panel')).toBeNull();
expect(screen.queryByTestId('platform-connections')).toBeNull();
});
it('uses the shared subtabs to switch to platform connections', () => {
renderWorkspace();
it('merges every connection source into a single alpha-sorted table', () => {
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' }));
expect(navigateSpy).toHaveBeenCalledWith('/settings/infrastructure/platforms');
const rowNames = screen.getAllByText(/zeus|tower|nas\.home/).map((el) => el.textContent);
expect(rowNames).toContain('nas.home');
expect(rowNames).toContain('tower');
expect(rowNames).toContain('zeus');
});
it('renders the platform workspace from the router pathname', () => {
mockPathname = '/settings/infrastructure/platforms';
it('opens the add-system picker when the add button is clicked', () => {
renderWorkspace();
expect(screen.getByRole('tab', { name: 'Platform connections' })).toHaveAttribute(
'aria-selected',
'true',
);
expect(screen.getByTestId('platform-connections')).toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: /Add a system/i }));
expect(screen.getByText('Linux or Docker host (agent)')).toBeInTheDocument();
expect(screen.getByText('Proxmox VE')).toBeInTheDocument();
expect(screen.getByText('TrueNAS SCALE')).toBeInTheDocument();
});
it('keeps the reporting route available for established operators', () => {
mockPathname = '/settings/infrastructure/operations';
it('routes the agent-host choice to the dedicated install workspace', () => {
renderWorkspace();
fireEvent.click(screen.getByRole('button', { name: /Add a system/i }));
expect(screen.getByRole('tab', { name: 'Inventory' })).toHaveAttribute(
'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' }));
fireEvent.click(screen.getByText('Linux or Docker host (agent)'));
expect(navigateSpy).toHaveBeenCalledWith('/settings/infrastructure/install');
});
it('hides the first-system orientation card once any platform connection exists', () => {
render(
() =>
(
<InfrastructureWorkspace
{...({
pveNodes: () => [],
pbsNodes: () => [],
pmgNodes: () => [],
agentStateResources: () => [],
platformConnectionsSummary: () => ({
pveCount: 1,
pbsCount: 0,
pmgCount: 0,
truenasCount: 0,
truenasAvailable: true,
vmwareCount: 0,
vmwareAvailable: true,
}),
} as any)}
/>
) as any,
);
it('routes TrueNAS and VMware choices to their platform panels', () => {
renderWorkspace();
fireEvent.click(screen.getByRole('button', { name: /Add a system/i }));
fireEvent.click(screen.getByText('TrueNAS SCALE'));
expect(navigateSpy).toHaveBeenCalledWith('/settings/infrastructure/platforms/truenas');
expect(screen.queryByText('Connect your first system')).not.toBeInTheDocument();
expect(screen.getByRole('tab', { name: 'Install on a host' })).toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: /Add a system/i }));
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', () => {
render(
() =>
(
<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,
);
it('preselects the Proxmox kind and lands on the platforms route for PVE, PBS, and PMG', () => {
renderWorkspace();
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);
mockPathname = '/settings/infrastructure/install';
renderWorkspace();
expect(screen.queryByText('Connect your first system')).not.toBeInTheDocument();
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(
'aria-selected',
'false',
);
expect(navigateSpy).toHaveBeenCalledWith('/settings/infrastructure/operations', {
replace: true,
});
expect(navigateSpy).toHaveBeenCalledWith('/settings/infrastructure', { replace: true });
});
it('still renders the connections table without an add button in read-only mode', () => {
presentationPolicyIsReadOnlyMock.mockReturnValue(true);
mockPathname = '/settings/infrastructure';
renderWorkspace();
expect(screen.getByText('Connections')).toBeInTheDocument();
expect(screen.queryByRole('button', { name: /Add a system/i })).toBeNull();
});
});

View file

@ -785,7 +785,7 @@ describe('monitored-system model guardrails', () => {
"pathname.startsWith('/settings/infrastructure/proxmox')",
);
expect(infrastructureWorkspaceModelSource).toContain(
"path: '/settings/infrastructure/operations'",
"operations: '/settings/infrastructure/operations'",
);
});

View file

@ -593,12 +593,9 @@ describe('Settings architecture guardrails', () => {
expect(proLicensePanelSource).not.toContain('description="Manage self-hosted billing');
expect(proLicensePanelSource).not.toContain('title="Plan"');
expect(proLicensePanelSource).not.toContain('title="Usage"');
expect(infrastructureWorkspaceSource).toContain('./selfHostedBillingPresentation');
expect(infrastructureWorkspaceSource).toContain(
'SELF_HOSTED_PRO_BILLING_PRESENTATION.infrastructureWorkspaceReferral',
);
expect(infrastructureWorkspaceSource).not.toContain('./selfHostedBillingPresentation');
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('getLicenseStatusLoadingState');
@ -892,7 +889,7 @@ describe('Settings architecture guardrails', () => {
);
expect(infrastructureWorkspaceSource).toContain('createEffect(() =>');
expect(infrastructureWorkspaceSource).toContain(
"if (readOnlyWorkspace() && activeView() !== 'inventory')",
"if (readOnlyWorkspace() && activeView() === 'install')",
);
expect(infrastructureWorkspaceSource).toContain('InfrastructureInstallPanel');
expect(infrastructureWorkspaceSource).toContain('PlatformConnectionsWorkspace');
@ -958,8 +955,8 @@ describe('Settings architecture guardrails', () => {
);
expect(infrastructureActiveRowDetailsSource).toContain('useInfrastructureOperationsContext');
expect(infrastructureIgnoredRowDetailsSource).toContain('useInfrastructureOperationsContext');
expect(infrastructureWorkspaceModelSource).toContain(
'export const INFRASTRUCTURE_WORKSPACE_TABS',
expect(infrastructureWorkspaceModelSource).not.toContain(
'INFRASTRUCTURE_WORKSPACE_TABS',
);
expect(infrastructureWorkspaceModelSource).toContain(
'export function getInfrastructureWorkspaceViewFromPath',
@ -1501,7 +1498,7 @@ describe('Settings architecture guardrails', () => {
'Setup changes stay unavailable in this read-only session.',
);
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', () => {

View file

@ -44,7 +44,7 @@ describe('useSettingsNavigation', () => {
renderHarness('/settings');
await waitFor(() => {
expect(navigateSpy).toHaveBeenCalledWith('/settings/infrastructure/operations', {
expect(navigateSpy).toHaveBeenCalledWith('/settings/infrastructure', {
replace: true,
scroll: false,
});
@ -57,7 +57,7 @@ describe('useSettingsNavigation', () => {
fireEvent.click(screen.getByRole('button', { name: 'open infrastructure settings' }));
expect(navigateSpy).toHaveBeenCalledWith('/settings/infrastructure/operations', {
expect(navigateSpy).toHaveBeenCalledWith('/settings/infrastructure', {
scroll: false,
});
});

View file

@ -1,28 +1,11 @@
export type InfrastructureWorkspaceView = 'install' | 'platforms' | 'inventory';
export type InfrastructureWorkspaceView = 'inventory' | 'install' | 'platforms' | 'operations';
export interface InfrastructureWorkspaceTabDefinition {
id: InfrastructureWorkspaceView;
label: string;
path: string;
}
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',
},
];
const INFRASTRUCTURE_WORKSPACE_PATHS: Record<InfrastructureWorkspaceView, string> = {
inventory: '/settings/infrastructure',
install: '/settings/infrastructure/install',
platforms: '/settings/infrastructure/platforms',
operations: '/settings/infrastructure/operations',
};
export function getInfrastructureWorkspaceViewFromPath(
pathname: string,
@ -35,20 +18,15 @@ export function getInfrastructureWorkspaceViewFromPath(
) {
return 'platforms';
}
if (pathname.startsWith('/settings/infrastructure/operations')) {
return 'inventory';
}
if (pathname.startsWith('/settings/infrastructure/install')) {
return 'install';
}
return 'install';
if (pathname.startsWith('/settings/infrastructure/operations')) {
return 'operations';
}
return 'inventory';
}
export function buildInfrastructureWorkspacePath(
view: InfrastructureWorkspaceView,
): string {
return (
INFRASTRUCTURE_WORKSPACE_TABS.find((tab) => tab.id === view)?.path ??
'/settings/infrastructure/install'
);
export function buildInfrastructureWorkspacePath(view: InfrastructureWorkspaceView): string {
return INFRASTRUCTURE_WORKSPACE_PATHS[view];
}