From b3abb746bca99df1ac5cf4807e1051f1ab2007a1 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Mon, 20 Apr 2026 15:01:51 +0100 Subject: [PATCH] infra: replace Add picker with Datadog-style catalog landing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The intermediate two-card picker screen ("Add connection" vs "Install agent") restated the same decision twice and framed the agent path as a walled-off alternative. Replace it with a single catalog landing that matches how Datadog, Grafana, and GCP Marketplace present "add an integration" — an address probe at the top, and a tile grid below listing Proxmox VE / PBS / PMG / VMware / TrueNAS / Install Pulse Agent as peer entries. Picking a tile or a probed candidate drops straight into that product's credential slot; "Back to catalog" returns to the landing. Retires the ConnectionsExplainer component, the picker-mode state in InfrastructureWorkspace, and the "Enter credentials manually" toggle in AddressProbeStep. Tests updated for the shortened path (one click from ledger header to any credential slot). --- .../ConnectionEditor/AddressProbeStep.tsx | 12 +- .../ConnectionEditor/ConnectionEditor.tsx | 98 ++++++++---- .../__tests__/ConnectionEditor.test.tsx | 45 ++---- .../Settings/ConnectionsExplainer.tsx | 143 ------------------ .../Settings/InfrastructureWorkspace.tsx | 82 ++-------- .../__tests__/ConnectionsExplainer.test.tsx | 87 ----------- .../InfrastructureWorkspace.test.tsx | 81 +++------- .../__tests__/settingsArchitecture.test.ts | 1 - 8 files changed, 116 insertions(+), 433 deletions(-) delete mode 100644 frontend-modern/src/components/Settings/ConnectionsExplainer.tsx delete mode 100644 frontend-modern/src/components/Settings/__tests__/ConnectionsExplainer.test.tsx diff --git a/frontend-modern/src/components/Settings/ConnectionEditor/AddressProbeStep.tsx b/frontend-modern/src/components/Settings/ConnectionEditor/AddressProbeStep.tsx index 286a601dd..1269f1f95 100644 --- a/frontend-modern/src/components/Settings/ConnectionEditor/AddressProbeStep.tsx +++ b/frontend-modern/src/components/Settings/ConnectionEditor/AddressProbeStep.tsx @@ -7,7 +7,6 @@ import { CONNECTION_TYPE_LABELS } from './useConnectionEditor'; export interface AddressProbeStepProps { state: ConnectionEditorState; onSelectCandidate: (candidate: ProbeCandidate) => void; - onChooseManually: () => void; onInstallAgent?: () => void; } @@ -47,13 +46,6 @@ export const AddressProbeStep: Component = (props) => { > {props.state.phase() === 'probing' ? 'Probing…' : 'Probe address'} - 0}> @@ -66,8 +58,8 @@ export const AddressProbeStep: Component = (props) => {
No supported product detected at that address.
- Pick a Platform API type manually and enter credentials, or if this is - bare-metal Linux / Unraid / FreeBSD,{' '} + Pick your system from the catalog below, or if this is bare-metal Linux / Unraid / + FreeBSD,{' '} install the Unified Agent instead} diff --git a/frontend-modern/src/components/Settings/ConnectionEditor/ConnectionEditor.tsx b/frontend-modern/src/components/Settings/ConnectionEditor/ConnectionEditor.tsx index 3d69b203f..0fdc1c324 100644 --- a/frontend-modern/src/components/Settings/ConnectionEditor/ConnectionEditor.tsx +++ b/frontend-modern/src/components/Settings/ConnectionEditor/ConnectionEditor.tsx @@ -1,4 +1,5 @@ -import { Component, type JSX, Show, createMemo, createSignal } from 'solid-js'; +import { Component, For, type JSX, Show, createMemo, createSignal } from 'solid-js'; +import { Archive, Cpu, Database, Mail, Server, ServerCog } from 'lucide-solid'; import type { ConnectionType, ProbeCandidate } from '@/api/connections'; import { AddressProbeStep } from './AddressProbeStep'; import { @@ -29,7 +30,24 @@ export interface ConnectionEditorProps { onSaved?: () => void; } -const DEFAULT_MANUAL_TYPES: ConnectionType[] = ['pve', 'pbs', 'pmg', 'truenas', 'vmware']; +const DEFAULT_MANUAL_TYPES: ConnectionType[] = ['pve', 'pbs', 'pmg', 'vmware', 'truenas', 'agent']; + +interface TileMeta { + icon: Component<{ class?: string }>; + description: string; +} + +const TILE_META: Partial> = { + pve: { icon: Server, description: 'VMs, containers, storage, backups' }, + pbs: { icon: Archive, description: 'Backups, sync and verify jobs' }, + pmg: { icon: Mail, description: 'Mail stats, queues, quarantine' }, + vmware: { icon: ServerCog, description: 'vCenter or ESXi clusters' }, + truenas: { icon: Database, description: 'Pools, datasets, replications' }, + agent: { + icon: Cpu, + description: 'Host metrics, or bare-metal Linux / Unraid / FreeBSD', + }, +}; export const ConnectionEditor: Component = (props) => { const state: ConnectionEditorState = createConnectionEditorState(); @@ -41,7 +59,6 @@ export const ConnectionEditor: Component = (props) => { props.initialType ?? null, ); const [selectedCandidate, setSelectedCandidate] = createSignal(null); - const [manualPickerOpen, setManualPickerOpen] = createSignal(false); const manualOptions = createMemo(() => props.manualTypeOptions ?? DEFAULT_MANUAL_TYPES); @@ -51,19 +68,16 @@ export const ConnectionEditor: Component = (props) => { const chooseCandidate = (candidate: ProbeCandidate) => { setSelectedCandidate(candidate); setSelectedType(candidate.type); - setManualPickerOpen(false); }; const chooseManualType = (type: ConnectionType) => { setSelectedCandidate(null); setSelectedType(type); - setManualPickerOpen(false); }; const reopenProbe = () => { setSelectedCandidate(null); setSelectedType(null); - setManualPickerOpen(false); }; const handleSaved = () => { @@ -76,42 +90,64 @@ export const ConnectionEditor: Component = (props) => { +
setManualPickerOpen((v) => !v)} onInstallAgent={() => chooseManualType('agent')} /> - -
-
- Choose Platform API type manually -
-
    - {manualOptions().map((type) => ( -
  • - -
  • - ))} -
-
-
+
+
+ +
+ + {(type) => { + const meta = TILE_META[type]; + const Icon = meta?.icon ?? Server; + const label = CONNECTION_TYPE_LABELS[type] ?? type; + const isAgent = type === 'agent'; + return ( + + ); + }} + +
} >
- {CONNECTION_TYPE_LABELS[activeType()!] ?? activeType()} + {activeType() === 'agent' + ? 'Install Pulse Agent' + : CONNECTION_TYPE_LABELS[activeType()!] ?? activeType()} {selectedCandidate()!.host} @@ -123,7 +159,7 @@ export const ConnectionEditor: Component = (props) => { onClick={reopenProbe} class="inline-flex items-center rounded-md border border-border px-2.5 py-1 text-xs font-medium text-base-content transition-colors hover:bg-surface-hover" > - ← Back to probe + ← Back to catalog
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 d245865e1..bceda7cf8 100644 --- a/frontend-modern/src/components/Settings/ConnectionEditor/__tests__/ConnectionEditor.test.tsx +++ b/frontend-modern/src/components/Settings/ConnectionEditor/__tests__/ConnectionEditor.test.tsx @@ -62,7 +62,7 @@ describe('ConnectionEditor', () => { expect(lastCall.mode).toBe('add'); }); - it('falls back to manual type selection when probe returns no match', async () => { + it('lets the user pick a product tile when probe returns no match', async () => { mockedProbe.mockResolvedValueOnce({ candidates: [], probedMs: 203 }); const renderSlot = vi.fn(({ type }) =>
slot:{type}
); @@ -84,9 +84,9 @@ describe('ConnectionEditor', () => { await screen.findByText(/no supported product detected/i); - fireEvent.click(screen.getByRole('button', { name: /enter credentials manually/i })); - - fireEvent.click(screen.getByText('TrueNAS SCALE')); + // The catalog grid is always visible below the probe, so the user picks a + // tile directly — no intermediate "enter credentials manually" toggle. + fireEvent.click(screen.getByRole('button', { name: /TrueNAS SCALE/i })); await waitFor(() => expect(screen.getByTestId('slot').textContent).toBe('slot:truenas')); const lastCall = renderSlot.mock.calls.at(-1)![0]; @@ -94,7 +94,7 @@ describe('ConnectionEditor', () => { expect(lastCall.candidate).toBeNull(); }); - it('leads with the address probe and does not re-teach the two-mode split', () => { + it('surfaces the probe input and the full product catalog as peers', () => { render(() => (
} @@ -102,15 +102,15 @@ describe('ConnectionEditor', () => { /> )); - // Primary path is probe-the-address. The explainer on the ledger already - // taught the user the two-mode split, so the editor must not restate it - // as a second decision screen — no "Platform API" card header, no - // always-visible "install agent" subtext competing with the primary flow. - // The agent path is surfaced by the ledger header, not duplicated here. + // Catalog landing — the probe input sits above a tile grid that includes + // every supported product as a peer, Install Pulse Agent included. There + // is no intermediate mode picker or "Platform API" framing. expect(screen.getByRole('button', { name: /probe address/i })).toBeInTheDocument(); - expect(screen.getByRole('button', { name: /enter credentials manually/i })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /^Proxmox VE/i })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /TrueNAS SCALE/i })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /VMware vCenter \/ ESXi/i })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /Install Pulse Agent/i })).toBeInTheDocument(); expect(screen.queryByText('Platform API')).toBeNull(); - expect(screen.queryByText(/host-level metrics/i)).toBeNull(); expect( screen.queryByRole('button', { name: /install the unified agent on a host/i }), ).toBeNull(); @@ -147,27 +147,6 @@ describe('ConnectionEditor', () => { 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/ConnectionsExplainer.tsx b/frontend-modern/src/components/Settings/ConnectionsExplainer.tsx deleted file mode 100644 index 5d2704fd4..000000000 --- a/frontend-modern/src/components/Settings/ConnectionsExplainer.tsx +++ /dev/null @@ -1,143 +0,0 @@ -import { Component, For, Show } from 'solid-js'; -import { ArrowRight, Cloud, Cpu } from 'lucide-solid'; - -const OPT_IN_CAPABILITIES = ['Assistant commands', 'Patrol remediation']; - -const AGENT_FACTS = [ - 'Single Go binary', - '~13 MB download', - 'No runtime dependencies', - 'Open source', -]; - -interface ConnectionsExplainerProps { - onAddConnection?: () => void; - onInstallAgent?: () => void; - readOnly?: boolean; -} - -export const ConnectionsExplainer: Component = (props) => { - const interactive = () => !props.readOnly; - - const ctaClass = - 'mt-4 inline-flex w-full items-center justify-center gap-1.5 rounded-md border border-border bg-surface px-3 py-2 text-sm font-medium text-base-content transition-colors hover:bg-surface-hover disabled:cursor-not-allowed disabled:opacity-60'; - - return ( -
-
-

How Pulse collects data

-

- The platform API covers workloads. The Unified Agent adds host-level metrics, and stands - alone where there's no API. -

-
- -
-
-
- -
-
Platform API
-
Primary source for workloads
-
-
-

- Pulse polls the platform's own API (Proxmox VE / PBS / PMG, VMware, TrueNAS) for VMs, - containers, storage, backups, and other workload data. Required for every API-backed - target; fastest to set up. -

-
- -
-
- -
- -
-
- ); -}; diff --git a/frontend-modern/src/components/Settings/InfrastructureWorkspace.tsx b/frontend-modern/src/components/Settings/InfrastructureWorkspace.tsx index 33a12224b..ece42ec9e 100644 --- a/frontend-modern/src/components/Settings/InfrastructureWorkspace.tsx +++ b/frontend-modern/src/components/Settings/InfrastructureWorkspace.tsx @@ -4,7 +4,6 @@ import { presentationPolicyIsReadOnly } from '@/stores/sessionPresentationPolicy import { copyToClipboard } from '@/utils/clipboard'; import { notificationStore } from '@/stores/notifications'; import { AgentProfilesPanel } from './AgentProfilesPanel'; -import { ConnectionsExplainer } from './ConnectionsExplainer'; import { ConnectionsTable, type ConnectionsTableHeaderAction } from './ConnectionsTable'; import { ConnectionEditor } from './ConnectionEditor/ConnectionEditor'; import { NodeCredentialSlot } from './ConnectionEditor/CredentialSlots/NodeCredentialSlot'; @@ -50,7 +49,6 @@ const InfrastructureWorkspaceContent: Component = const rowActions = useConnectionRowActions({ onMutated: () => ledger.reload() }); const [addMode, setAddMode] = createSignal(false); - const [addPickerMode, setAddPickerMode] = createSignal(false); const [initialAddType, setInitialAddType] = createSignal(null); const [showAgentProfiles, setShowAgentProfiles] = createSignal(false); const [editingConnection, setEditingConnection] = createSignal(null); @@ -99,17 +97,14 @@ const InfrastructureWorkspaceContent: Component = if (!readOnly() && step) { setAddMode(true); if (step === 'pick') { - setAddPickerMode(true); setInitialAddType(null); } else { - setAddPickerMode(false); setInitialAddType(ADD_STEP_TO_TYPE[step]); } return; } setAddMode(false); - setAddPickerMode(false); setInitialAddType(null); setShowAgentProfiles(false); }); @@ -128,27 +123,10 @@ const InfrastructureWorkspaceContent: Component = const startAddInfrastructure = () => { setInitialAddType(null); - setAddPickerMode(true); setAddMode(true); setShowAgentProfiles(false); }; - const pickPlatformApi = () => { - setInitialAddType(null); - setAddPickerMode(false); - }; - - const pickAgent = () => { - setInitialAddType('agent'); - setAddPickerMode(false); - }; - - const backToPicker = () => { - setInitialAddType(null); - setAddPickerMode(true); - setShowAgentProfiles(false); - }; - const headerActions = createMemo(() => readOnly() ? [] @@ -177,7 +155,6 @@ const InfrastructureWorkspaceContent: Component = const exitAddMode = () => { setAddMode(false); - setAddPickerMode(false); setInitialAddType(null); setShowAgentProfiles(false); }; @@ -333,61 +310,22 @@ const InfrastructureWorkspaceContent: Component = }} - -
-
-
-
Add infrastructure
-
- Pick how Pulse should connect to this system. -
-
- -
- - -
-
- - +
-
- {initialAddType() === 'agent' ? 'Install agent' : 'Add connection'} -
+
Add infrastructure
- {initialAddType() === 'agent' - ? 'Install the Pulse Unified Agent on a host for CPU / disk temps, SMART, and power, or on bare-metal Linux / Unraid / FreeBSD.' - : 'Paste an address. Pulse detects the product, you enter credentials, and save.'} + Paste an address to auto-detect, or pick your system from the catalog.
-
- - -
+
diff --git a/frontend-modern/src/components/Settings/__tests__/ConnectionsExplainer.test.tsx b/frontend-modern/src/components/Settings/__tests__/ConnectionsExplainer.test.tsx deleted file mode 100644 index 9ff8723a2..000000000 --- a/frontend-modern/src/components/Settings/__tests__/ConnectionsExplainer.test.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import { cleanup, fireEvent, render, screen } from '@solidjs/testing-library'; -import { afterEach, describe, expect, it, vi } from 'vitest'; -import { ConnectionsExplainer } from '../ConnectionsExplainer'; - -describe('ConnectionsExplainer', () => { - afterEach(() => { - cleanup(); - }); - - it('names both ingestion modes with their branded labels', () => { - render(() => ); - - expect(screen.getByText('Platform API')).toBeInTheDocument(); - expect(screen.getByText('Pulse Unified Agent')).toBeInTheDocument(); - }); - - it('frames the agent as supplementary to the API, not a replacement', () => { - render(() => ); - - expect(screen.queryByText(/Recommended/i)).toBeNull(); - expect(screen.getByText(/supplements the API/i)).toBeInTheDocument(); - expect(screen.getByText(/Primary source for workloads/i)).toBeInTheDocument(); - }); - - it('calls out that Assistant / Patrol command execution is opt-in, not default', () => { - render(() => ); - - expect(screen.getByText(/off by default, opt in per host/i)).toBeInTheDocument(); - for (const capability of ['Assistant commands', 'Patrol remediation']) { - expect(screen.getByText(capability)).toBeInTheDocument(); - } - // The old "Always on: Hardware metrics" framing read as unavoidable - // surveillance. The agent paragraph already names what it collects; - // we don't restate it as an always-on chip. - expect(screen.queryByText(/^Always on$/i)).toBeNull(); - expect(screen.queryByText('Hardware metrics')).toBeNull(); - }); - - it('surfaces trust facts users care about', () => { - render(() => ); - - for (const fact of ['Single Go binary', '~13 MB download', 'No runtime dependencies', 'Open source']) { - expect(screen.getByText(fact)).toBeInTheDocument(); - } - }); - - it('exposes Add connection and Install agent as the entry CTAs, not as duplicate buttons elsewhere', () => { - const onAddConnection = vi.fn(); - const onInstallAgent = vi.fn(); - - render(() => ( - - )); - - const addBtn = screen.getByRole('button', { name: /^Add connection$/i }); - const installBtn = screen.getByRole('button', { name: /^Install agent$/i }); - - fireEvent.click(addBtn); - expect(onAddConnection).toHaveBeenCalledTimes(1); - expect(onInstallAgent).not.toHaveBeenCalled(); - - fireEvent.click(installBtn); - expect(onInstallAgent).toHaveBeenCalledTimes(1); - }); - - it('does not render the action CTAs in read-only mode', () => { - render(() => ( - {}} - onInstallAgent={() => {}} - /> - )); - - expect(screen.queryByRole('button', { name: /^Add connection$/i })).toBeNull(); - expect(screen.queryByRole('button', { name: /^Install agent$/i })).toBeNull(); - }); - - it('no longer offers a dismiss affordance — the cards are the entry path, not a tutorial banner', () => { - render(() => ); - - expect(screen.queryByRole('button', { name: /Dismiss/i })).toBeNull(); - }); -}); diff --git a/frontend-modern/src/components/Settings/__tests__/InfrastructureWorkspace.test.tsx b/frontend-modern/src/components/Settings/__tests__/InfrastructureWorkspace.test.tsx index dacdbae49..fafe45291 100644 --- a/frontend-modern/src/components/Settings/__tests__/InfrastructureWorkspace.test.tsx +++ b/frontend-modern/src/components/Settings/__tests__/InfrastructureWorkspace.test.tsx @@ -221,83 +221,68 @@ describe('InfrastructureWorkspace', () => { expect(screen.getByText('Paused')).toBeInTheDocument(); }); - it('opens the picker screen when Add infrastructure is clicked', () => { + it('opens the catalog landing when Add infrastructure is clicked', () => { renderWorkspace(); fireEvent.click(screen.getByRole('button', { name: /Add infrastructure/i })); - // The picker screen surfaces both mode choices side-by-side; the probe flow - // has not started yet. - expect(screen.getByRole('button', { name: /^Add connection$/i })).toBeInTheDocument(); - expect(screen.getByRole('button', { name: /^Install agent$/i })).toBeInTheDocument(); - expect(screen.queryByRole('button', { name: /Probe address/i })).toBeNull(); + // The catalog landing surfaces the probe input and a tile grid of every + // supported product — including Install Pulse Agent as a peer tile — with + // no intermediate picker screen. + expect(screen.getByRole('button', { name: /Probe address/i })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /Install Pulse Agent/i })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /^Proxmox VE/i })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /TrueNAS SCALE/i })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /VMware vCenter \/ ESXi/i })).toBeInTheDocument(); expect(navigateSpy).not.toHaveBeenCalled(); expect(setSearchParamsSpy).not.toHaveBeenCalled(); }); - it('routes from the picker into the probe flow when Add connection is chosen', () => { + it('routes to the agent install slot when Install Pulse Agent tile is clicked', () => { renderWorkspace(); fireEvent.click(screen.getByRole('button', { name: /Add infrastructure/i })); - fireEvent.click(screen.getByRole('button', { name: /^Add connection$/i })); - - expect(screen.getByRole('button', { name: /Probe address/i })).toBeInTheDocument(); - expect(screen.getByRole('button', { name: /Enter credentials manually/i })).toBeInTheDocument(); - }); - - it('routes from the picker into the agent install slot when Install agent is chosen', () => { - renderWorkspace(); - - // The agent path is a first-class peer entry inside the picker, not a - // subtext offramp hidden elsewhere — a user who wants CPU/disk temps or - // is on bare-metal Linux should reach it in two clicks from the ledger. - fireEvent.click(screen.getByRole('button', { name: /Add infrastructure/i })); - fireEvent.click(screen.getByRole('button', { name: /^Install agent$/i })); + fireEvent.click(screen.getByRole('button', { name: /Install Pulse Agent/i })); expect(screen.getByTestId('install-section')).toBeInTheDocument(); }); - it('routes to the TrueNAS credential slot when TrueNAS is picked manually', () => { + it('routes to the TrueNAS credential slot when TrueNAS tile is clicked', () => { renderWorkspace(); fireEvent.click(screen.getByRole('button', { name: /Add infrastructure/i })); - 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: /TrueNAS SCALE/i })); expect(screen.getByTestId('truenas-section')).toBeInTheDocument(); }); - it('routes to the VMware credential slot when VMware is picked manually', () => { + it('routes to the VMware credential slot when VMware tile is clicked', () => { renderWorkspace(); fireEvent.click(screen.getByRole('button', { name: /Add infrastructure/i })); - 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: /VMware vCenter \/ ESXi/i })); expect(screen.getByTestId('vmware-section')).toBeInTheDocument(); }); - it('routes to the Proxmox credential slot when Proxmox VE is picked manually', () => { + it('routes to the Proxmox credential slot when Proxmox VE tile is clicked', () => { renderWorkspace(); fireEvent.click(screen.getByRole('button', { name: /Add infrastructure/i })); - 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: /^Proxmox VE/i })); expect(screen.getByTestId('proxmox-section')).toBeInTheDocument(); }); - it('can return to the probe step from a credential slot', () => { + it('can return to the catalog from a credential slot', () => { renderWorkspace(); fireEvent.click(screen.getByRole('button', { name: /Add infrastructure/i })); - fireEvent.click(screen.getByRole('button', { name: /^Install agent$/i })); - fireEvent.click(screen.getByRole('button', { name: /Back to probe/i })); + fireEvent.click(screen.getByRole('button', { name: /Install Pulse Agent/i })); + fireEvent.click(screen.getByRole('button', { name: /Back to catalog/i })); expect(screen.getByRole('button', { name: /Probe address/i })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /Install Pulse Agent/i })).toBeInTheDocument(); expect(screen.queryByTestId('install-section')).toBeNull(); }); @@ -305,28 +290,12 @@ describe('InfrastructureWorkspace', () => { renderWorkspace(); fireEvent.click(screen.getByRole('button', { name: /Add infrastructure/i })); - fireEvent.click(screen.getByRole('button', { name: /^Install agent$/i })); + fireEvent.click(screen.getByRole('button', { name: /Install Pulse Agent/i })); fireEvent.click(screen.getByRole('button', { name: 'Manage agent profiles' })); expect(screen.getByTestId('agent-profiles')).toBeInTheDocument(); }); - it('offers Change method from a sub-flow to return to the picker', () => { - renderWorkspace(); - - fireEvent.click(screen.getByRole('button', { name: /Add infrastructure/i })); - fireEvent.click(screen.getByRole('button', { name: /^Install agent$/i })); - expect(screen.getByTestId('install-section')).toBeInTheDocument(); - - fireEvent.click(screen.getByRole('button', { name: /Change method/i })); - - // Back on the picker screen — both card CTAs are visible again, install - // section is gone. - expect(screen.getByRole('button', { name: /^Add connection$/i })).toBeInTheDocument(); - expect(screen.getByRole('button', { name: /^Install agent$/i })).toBeInTheDocument(); - expect(screen.queryByTestId('install-section')).toBeNull(); - }); - it('exposes Edit / Pause / Remove on each ledger row directly', async () => { renderWorkspace(); @@ -376,17 +345,18 @@ describe('InfrastructureWorkspace', () => { await waitFor(() => expect(screen.getByTestId('install-section')).toBeInTheDocument()); }); - it('clears the canonical query onboarding route for platform picking and lands on the picker', async () => { + it('clears the canonical query onboarding route for platform picking and lands on the catalog', async () => { routeState.search = '?add=pick'; renderWorkspace(); expect(setSearchParamsSpy).toHaveBeenCalledWith({ add: null }, { replace: true }); expect(navigateSpy).not.toHaveBeenCalled(); await waitFor(() => - expect(screen.getByRole('button', { name: /^Add connection$/i })).toBeInTheDocument(), + expect(screen.getByRole('button', { name: /Probe address/i })).toBeInTheDocument(), ); - // Picker screen — probe hasn't started yet, install section isn't open. - expect(screen.queryByRole('button', { name: /Probe address/i })).toBeNull(); + // Catalog landing — probe input + tile grid are visible, no credential + // slot has been entered yet. + expect(screen.getByRole('button', { name: /Install Pulse Agent/i })).toBeInTheDocument(); expect(screen.queryByTestId('install-section')).toBeNull(); }); @@ -408,8 +378,7 @@ describe('InfrastructureWorkspace', () => { renderWorkspace(); expect(screen.queryByRole('button', { name: /Add infrastructure/i })).toBeNull(); - expect(screen.queryByRole('button', { name: /^Add connection$/i })).toBeNull(); - expect(screen.queryByRole('button', { name: /^Install agent$/i })).toBeNull(); + expect(screen.queryByRole('button', { name: /Install Pulse Agent/i })).toBeNull(); expect(screen.queryByRole('button', { name: /Probe address/i })).toBeNull(); expect(screen.queryByTestId('install-section')).toBeNull(); }); diff --git a/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts b/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts index 2a2752ff8..c79472ec9 100644 --- a/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts +++ b/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts @@ -83,7 +83,6 @@ describe('settings architecture guardrails', () => { expect(connectionEditorSource).not.toContain('NodeModal'); expect(addressProbeStepSource).toContain('Probe address'); - expect(addressProbeStepSource).toContain('Enter credentials manually'); // The no-match branch must name the agent alternative so a user who // probed bare-metal Linux / Unraid / FreeBSD is not left in a // Platform-API-only dead end.