mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-11 22:12:23 +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:
@@ -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>
|
||||||
|
|||||||
+1
-1
@@ -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 () => {
|
||||||
|
|||||||
+105
-141
@@ -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();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+1
-1
@@ -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'
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user