From 2e1e682f70757a2392415e69135425cefd12a627 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Mon, 20 Apr 2026 09:47:32 +0100 Subject: [PATCH] Carry the Platform API / Unified Agent vocabulary into the add flow The explainer at the top of the ledger teaches two paths (Platform API and Pulse Unified Agent), but clicking Add dropped that vocabulary: the user landed on a generic address box with no label and could only reach the agent by clicking "Enter credentials manually" and picking it from a flat list of six raw-token types. Reshape the pre-credential step into two labeled sections that mirror the explainer: - "Platform API" section wraps the probe input and the manual-type fallback (now filtered to the five API types; heading reads "Choose Platform API type manually"). - "Pulse Unified Agent" section with a direct "Install the Unified Agent on a host" button, tinted to match the explainer's agent card. Also aligns the CONNECTION_TYPE_LABELS entry for agent to "Pulse Unified Agent" so the header on the install surface matches the brand used in the ledger and the explainer. --- .../ConnectionEditor/ConnectionEditor.tsx | 84 +++++++++++++------ .../__tests__/ConnectionEditor.test.tsx | 54 ++++++++++++ .../ConnectionEditor/useConnectionEditor.ts | 2 +- .../InfrastructureWorkspace.test.tsx | 17 ++-- 4 files changed, 122 insertions(+), 35 deletions(-) diff --git a/frontend-modern/src/components/Settings/ConnectionEditor/ConnectionEditor.tsx b/frontend-modern/src/components/Settings/ConnectionEditor/ConnectionEditor.tsx index 0b9ad190e..2dfd2d5e4 100644 --- a/frontend-modern/src/components/Settings/ConnectionEditor/ConnectionEditor.tsx +++ b/frontend-modern/src/components/Settings/ConnectionEditor/ConnectionEditor.tsx @@ -29,7 +29,7 @@ export interface ConnectionEditorProps { onSaved?: () => void; } -const DEFAULT_MANUAL_TYPES: ConnectionType[] = ['pve', 'pbs', 'pmg', 'truenas', 'vmware', 'agent']; +const DEFAULT_MANUAL_TYPES: ConnectionType[] = ['pve', 'pbs', 'pmg', 'truenas', 'vmware']; export const ConnectionEditor: Component = (props) => { const state: ConnectionEditorState = createConnectionEditorState(); @@ -80,38 +80,68 @@ export const ConnectionEditor: Component = (props) => {
Add a connection
- Paste an address and Pulse detects the product. One flow for every supported - platform. + Paste a platform address to connect its API, or install the Unified Agent on a + host.
- setManualPickerOpen((v) => !v)} - /> - - -
+
+
- Choose type manually + Platform API +
+
+ Proxmox VE / PBS / PMG, VMware, TrueNAS
-
    - {manualOptions().map((type) => ( -
  • - -
  • - ))} -
- + + setManualPickerOpen((v) => !v)} + /> + + +
+
+ Choose Platform API type manually +
+
    + {manualOptions().map((type) => ( +
  • + +
  • + ))} +
+
+
+
+ +
+
+
+ Pulse Unified Agent +
+
+ Host-level telemetry on Proxmox / VMware / TrueNAS, or the only path on + bare-metal Linux, Unraid, FreeBSD. +
+
+ +
} > diff --git a/frontend-modern/src/components/Settings/ConnectionEditor/__tests__/ConnectionEditor.test.tsx b/frontend-modern/src/components/Settings/ConnectionEditor/__tests__/ConnectionEditor.test.tsx index c15b31f86..aac06538b 100644 --- a/frontend-modern/src/components/Settings/ConnectionEditor/__tests__/ConnectionEditor.test.tsx +++ b/frontend-modern/src/components/Settings/ConnectionEditor/__tests__/ConnectionEditor.test.tsx @@ -94,6 +94,60 @@ describe('ConnectionEditor', () => { expect(lastCall.candidate).toBeNull(); }); + it('surfaces Platform API and Pulse Unified Agent as the two add-paths, mirroring the explainer', () => { + render(() => ( +
} + onClose={() => {}} + /> + )); + + expect(screen.getByText('Platform API')).toBeInTheDocument(); + expect(screen.getByText('Pulse Unified Agent')).toBeInTheDocument(); + expect( + screen.getByRole('button', { name: /install the unified agent on a host/i }), + ).toBeInTheDocument(); + }); + + it('routes the Install Unified Agent button straight to the agent credential slot', () => { + const renderSlot = vi.fn(({ type }) =>
slot:{type}
); + + render(() => ( + {}} + /> + )); + + fireEvent.click(screen.getByRole('button', { name: /install the unified agent on a host/i })); + + expect(screen.getByTestId('slot').textContent).toBe('slot:agent'); + const call = renderSlot.mock.calls.at(-1)![0]; + expect(call.type).toBe('agent'); + expect(call.candidate).toBeNull(); + }); + + it('keeps agent out of the Platform API manual picker (it has its own path)', async () => { + mockedProbe.mockResolvedValueOnce({ candidates: [], probedMs: 150 }); + + render(() => ( +
} + onClose={() => {}} + /> + )); + + const input = screen.getByPlaceholderText(/pve01\.lan/) as HTMLInputElement; + fireEvent.input(input, { target: { value: 'example.lan' } }); + fireEvent.click(screen.getByRole('button', { name: /probe address/i })); + await waitFor(() => expect(mockedProbe).toHaveBeenCalled()); + + fireEvent.click(screen.getByRole('button', { name: /enter credentials manually/i })); + + expect(screen.getByText(/choose platform api type manually/i)).toBeInTheDocument(); + expect(screen.queryByText(/Agent \(install on host\)/i)).toBeNull(); + }); + it('skips the probe step when an initialType is supplied (edit mode)', () => { const renderSlot = vi.fn(({ type }) =>
slot:{type}
); diff --git a/frontend-modern/src/components/Settings/ConnectionEditor/useConnectionEditor.ts b/frontend-modern/src/components/Settings/ConnectionEditor/useConnectionEditor.ts index e46ec4230..49c2dfadc 100644 --- a/frontend-modern/src/components/Settings/ConnectionEditor/useConnectionEditor.ts +++ b/frontend-modern/src/components/Settings/ConnectionEditor/useConnectionEditor.ts @@ -102,7 +102,7 @@ export const CONNECTION_TYPE_LABELS: Record = { pmg: 'Proxmox Mail Gateway', vmware: 'VMware vCenter / ESXi', truenas: 'TrueNAS SCALE', - agent: 'Agent (install on host)', + agent: 'Pulse Unified Agent', docker: 'Docker', kubernetes: 'Kubernetes', }; diff --git a/frontend-modern/src/components/Settings/__tests__/InfrastructureWorkspace.test.tsx b/frontend-modern/src/components/Settings/__tests__/InfrastructureWorkspace.test.tsx index 8fcb8e1f7..d9a1c78f5 100644 --- a/frontend-modern/src/components/Settings/__tests__/InfrastructureWorkspace.test.tsx +++ b/frontend-modern/src/components/Settings/__tests__/InfrastructureWorkspace.test.tsx @@ -233,12 +233,13 @@ describe('InfrastructureWorkspace', () => { expect(setSearchParamsSpy).not.toHaveBeenCalled(); }); - it('routes to the agent install slot when Pulse agent is picked manually', () => { + it('routes to the agent install slot via the dedicated Unified Agent path', () => { renderWorkspace(); fireEvent.click(screen.getByRole('button', { name: /Add connection/i })); - fireEvent.click(screen.getByRole('button', { name: /Enter credentials manually/i })); - fireEvent.click(screen.getByRole('button', { name: /Agent \(install on host\)/i })); + fireEvent.click( + screen.getByRole('button', { name: /Install the Unified Agent on a host/i }), + ); expect(screen.getByTestId('install-section')).toBeInTheDocument(); }); @@ -277,8 +278,9 @@ describe('InfrastructureWorkspace', () => { renderWorkspace(); fireEvent.click(screen.getByRole('button', { name: /Add connection/i })); - fireEvent.click(screen.getByRole('button', { name: /Enter credentials manually/i })); - fireEvent.click(screen.getByRole('button', { name: /Agent \(install on host\)/i })); + fireEvent.click( + screen.getByRole('button', { name: /Install the Unified Agent on a host/i }), + ); fireEvent.click(screen.getByRole('button', { name: /Back to probe/i })); expect(screen.getByRole('button', { name: /Probe address/i })).toBeInTheDocument(); @@ -289,8 +291,9 @@ describe('InfrastructureWorkspace', () => { renderWorkspace(); fireEvent.click(screen.getByRole('button', { name: /Add connection/i })); - fireEvent.click(screen.getByRole('button', { name: /Enter credentials manually/i })); - fireEvent.click(screen.getByRole('button', { name: /Agent \(install on host\)/i })); + fireEvent.click( + screen.getByRole('button', { name: /Install the Unified Agent on a host/i }), + ); fireEvent.click(screen.getByRole('button', { name: 'Manage agent profiles' })); expect(screen.getByTestId('agent-profiles')).toBeInTheDocument();