Default infrastructure settings to install

This commit is contained in:
rcourtman 2026-03-27 09:41:38 +00:00
parent 3df9963df1
commit 2c80bfe9e2
9 changed files with 123 additions and 23 deletions

View file

@ -147,6 +147,12 @@ management, and fleet control surfaces.
governed command copy, and install instructions belong to the canonical
lifecycle path; transport details and adjacent alternatives must remain
secondary to that first-host onboarding narrative.
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.
## Current State

View file

@ -171,6 +171,12 @@ work extends shared components instead of creating new local variants.
completion CTA language aligned with the governed infrastructure install
workspace instead of regressing to a route jump that leaves the next action
implicit.
8. Keep the settings-shell infrastructure landing path aligned with that same
first-session story. `frontend-modern/src/components/Settings/settingsNavigationModel.ts`
must treat `/settings` and the infrastructure settings tab as the canonical
path to `/settings/infrastructure/install`, not to reporting/control, so
the shell does not send first-time operators to the wrong infrastructure
subview by default.
## Current State

View file

@ -20,6 +20,8 @@ export const InfrastructureWorkspace: Component<InfrastructureWorkspaceProps> =
const navigate = useNavigate();
const location = useLocation();
const activeView = createMemo(() => getInfrastructureWorkspaceViewFromPath(location.pathname));
const installPath = createMemo(() => buildInfrastructureWorkspacePath('install'));
const directPath = createMemo(() => buildInfrastructureWorkspacePath('direct'));
const openView = (view: InfrastructureWorkspaceView) =>
navigate(buildInfrastructureWorkspacePath(view));
@ -27,12 +29,66 @@ export const InfrastructureWorkspace: Component<InfrastructureWorkspaceProps> =
return (
<div class="space-y-6">
<Card padding="lg" class="rounded-xl border border-border shadow-sm">
<div class="space-y-2">
<h3 class="text-base font-semibold text-base-content">Infrastructure operations</h3>
<p class="text-sm text-muted">
New to Pulse? Start with Install on a host to add your first monitored system.
If you prefer a direct integration instead, use Direct Proxmox.
</p>
<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">
Start with Install on a host to connect the first machine you want Pulse to monitor.
If you already know you want a direct integration instead, go straight to Direct
Proxmox.
</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">
Install Pulse on a host first, or use Direct Proxmox if that is the system you are
connecting.
</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, then copy the
generated command.
</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 use Reporting &amp; control once the first
system starts reporting.
</p>
</div>
</div>
<div class="flex flex-wrap gap-3">
<button
type="button"
onClick={() => navigate(installPath())}
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'
}`}
>
{activeView() === 'install' ? 'Install on a host selected' : 'Open Install on a host'}
</button>
<button
type="button"
onClick={() => navigate(directPath())}
class={`inline-flex items-center justify-center rounded-md px-4 py-2 text-sm font-medium transition-colors ${
activeView() === 'direct'
? 'bg-emerald-600 text-white'
: 'border border-border bg-surface text-base-content hover:bg-surface-hover'
}`}
>
{activeView() === 'direct' ? 'Direct Proxmox selected' : 'Open Direct Proxmox'}
</button>
</div>
<p class="text-sm text-muted">
{SELF_HOSTED_PRO_BILLING_PRESENTATION.infrastructureWorkspaceReferral}
</p>

View file

@ -30,7 +30,7 @@ vi.mock('../ProxmoxSettingsPanel', () => ({
describe('InfrastructureWorkspace', () => {
beforeEach(() => {
navigateSpy.mockReset();
mockPathname = '/settings/infrastructure/operations';
mockPathname = '/settings/infrastructure';
});
afterEach(() => {
@ -51,23 +51,26 @@ describe('InfrastructureWorkspace', () => {
) as any,
);
it('renders the canonical subtablist', () => {
it('defaults bare infrastructure routing to install on a host', () => {
renderWorkspace();
const tablist = screen.getByRole('tablist', { name: 'Infrastructure workspace' });
expect(tablist).toBeInTheDocument();
expect(screen.getByText('Infrastructure operations')).toBeInTheDocument();
expect(screen.getByText('Connect your first system')).toBeInTheDocument();
expect(
screen.getByText(
'New to Pulse? Start with Install on a host to add your first monitored system. If you prefer a direct integration instead, use Direct Proxmox.',
'Start with Install on a host to connect the first machine you want Pulse to monitor. If you already know you want a direct integration instead, go straight to Direct Proxmox.',
),
).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',
'false',
'true',
);
expect(screen.getByRole('tab', { name: 'Direct Proxmox' })).toHaveAttribute(
'aria-selected',
@ -75,8 +78,9 @@ describe('InfrastructureWorkspace', () => {
);
expect(screen.getByRole('tab', { name: 'Reporting & control' })).toHaveAttribute(
'aria-selected',
'true',
'false',
);
expect(screen.getByTestId('unified-agents')).toBeInTheDocument();
});
it('uses the shared subtabs to switch to direct proxmox', () => {
@ -98,12 +102,33 @@ describe('InfrastructureWorkspace', () => {
expect(screen.getByTestId('proxmox-settings')).toBeInTheDocument();
});
it('keeps the reporting route available for established operators', () => {
mockPathname = '/settings/infrastructure/operations';
renderWorkspace();
expect(screen.getByRole('tab', { name: 'Reporting & control' })).toHaveAttribute(
'aria-selected',
'true',
);
expect(screen.getByTestId('agent-profiles')).toBeInTheDocument();
});
it('uses the guided workspace actions to open install and direct paths', () => {
renderWorkspace();
fireEvent.click(screen.getByRole('button', { name: 'Install on a host selected' }));
fireEvent.click(screen.getByRole('button', { name: 'Open Direct Proxmox' }));
expect(navigateSpy).toHaveBeenNthCalledWith(1, '/settings/infrastructure/install');
expect(navigateSpy).toHaveBeenNthCalledWith(2, '/settings/infrastructure/proxmox');
});
it('returns to the base settings route when switching away from direct proxmox', () => {
mockPathname = '/settings/infrastructure/proxmox';
renderWorkspace();
fireEvent.click(screen.getByRole('tab', { name: 'Reporting & control' }));
fireEvent.click(screen.getByRole('tab', { name: 'Install on a host' }));
expect(navigateSpy).toHaveBeenCalledWith('/settings/infrastructure/operations');
expect(navigateSpy).toHaveBeenCalledWith('/settings/infrastructure/install');
});
});

View file

@ -480,8 +480,12 @@ describe('Settings architecture guardrails', () => {
expect(settingsInfrastructurePanelPropsSource).toContain('getInfrastructurePanelProps');
expect(settingsNavigationModelSource).toContain('export type SettingsTab =');
expect(settingsNavigationModelSource).toContain('export const DEFAULT_SETTINGS_TAB');
expect(settingsNavigationModelSource).toContain(
"const INFRASTRUCTURE_INSTALL_PREFIX = '/settings/infrastructure/install';",
);
expect(settingsNavigationModelSource).toContain('export function resolveCanonicalSettingsPath');
expect(settingsNavigationModelSource).toContain('export function settingsTabPath');
expect(settingsNavigationModelSource).toContain("return INFRASTRUCTURE_INSTALL_PREFIX;");
expect(settingsNavigationHookSource).toContain('deriveTabFromPath');
expect(settingsNavigationHookSource).toContain('resolveCanonicalSettingsPath');
expect(settingsNavigationHookSource).toContain('settingsTabPath');

View file

@ -7,7 +7,7 @@ import { updateDisableLocalUpgradeMetricsSetting } from '@/stores/systemSettings
const canonicalTabPaths = {
proxmox: '/settings/infrastructure/proxmox',
'infrastructure-operations': '/settings/infrastructure/operations',
'infrastructure-operations': '/settings/infrastructure/install',
'system-general': '/settings/system-general',
'system-network': '/settings/system-network',
'system-updates': '/settings/system-updates',

View file

@ -17,7 +17,7 @@ import { isFeatureLocked, isTabLocked } from '../settingsFeatureGates';
const canonicalTabPaths = {
proxmox: '/settings/infrastructure/proxmox',
'infrastructure-operations': '/settings/infrastructure/operations',
'infrastructure-operations': '/settings/infrastructure/install',
'system-general': '/settings/system-general',
'system-network': '/settings/system-network',
'system-updates': '/settings/system-updates',
@ -61,18 +61,18 @@ describe('settingsNavigationModel', () => {
});
it('resolves only canonical settings paths', () => {
expect(resolveCanonicalSettingsPath('/settings')).toBe('/settings/infrastructure/operations');
expect(resolveCanonicalSettingsPath('/settings')).toBe('/settings/infrastructure/install');
expect(resolveCanonicalSettingsPath('/settings/workloads')).toBe(
'/settings/infrastructure/operations',
'/settings/infrastructure/install',
);
expect(resolveCanonicalSettingsPath('/settings/workloads/docker')).toBe(
'/settings/infrastructure/operations',
'/settings/infrastructure/install',
);
expect(resolveCanonicalSettingsPath('/settings/system-updates')).toBe(
'/settings/system-updates',
);
expect(resolveCanonicalSettingsPath('/settings/infrastructure')).toBe(
'/settings/infrastructure/operations',
'/settings/infrastructure/install',
);
expect(resolveCanonicalSettingsPath('/settings/infrastructure/pve')).toBe(
'/settings/infrastructure/proxmox/pve',

View file

@ -30,10 +30,13 @@ export function getInfrastructureWorkspaceViewFromPath(
if (pathname.startsWith('/settings/infrastructure/proxmox')) {
return 'direct';
}
if (pathname.startsWith('/settings/infrastructure/operations')) {
return 'inventory';
}
if (pathname.startsWith('/settings/infrastructure/install')) {
return 'install';
}
return 'inventory';
return 'install';
}
export function buildInfrastructureWorkspacePath(
@ -41,6 +44,6 @@ export function buildInfrastructureWorkspacePath(
): string {
return (
INFRASTRUCTURE_WORKSPACE_TABS.find((tab) => tab.id === view)?.path ??
'/settings/infrastructure/operations'
'/settings/infrastructure/install'
);
}

View file

@ -309,7 +309,7 @@ export function settingsTabPath(tab: SettingsTab): string {
case 'proxmox':
return PROXMOX_PREFIX;
case 'infrastructure-operations':
return INFRASTRUCTURE_OPERATIONS_PREFIX;
return INFRASTRUCTURE_INSTALL_PREFIX;
case 'system-recovery':
return '/settings/system-recovery';
case 'organization-overview':