mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-05-15 18:20:35 +00:00
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:
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.
|
||||
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`.
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 () => {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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'",
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue