diff --git a/frontend-modern/src/components/Settings/ConnectionEditor/AddressProbeStep.tsx b/frontend-modern/src/components/Settings/ConnectionEditor/AddressProbeStep.tsx index ee9f91c23..2384c2ad8 100644 --- a/frontend-modern/src/components/Settings/ConnectionEditor/AddressProbeStep.tsx +++ b/frontend-modern/src/components/Settings/ConnectionEditor/AddressProbeStep.tsx @@ -3,7 +3,6 @@ import type { ProbeCandidate } from '@/api/connections'; import { formControl, formField, formHelpText, formLabel } from '@/components/shared/Form'; import type { CompletedProbePhase, ConnectionEditorState } from './useConnectionEditor'; import { CONNECTION_TYPE_LABELS } from './useConnectionEditor'; -import { getInfrastructureAutoDetectLabels } from '@/utils/infrastructureOnboardingPresentation'; export interface AddressProbeStepProps { state: ConnectionEditorState; @@ -26,8 +25,6 @@ export const AddressProbeStep: Component = (props) => { props.onProbeResolved?.(outcome); }; - const autoDetectLabels = getInfrastructureAutoDetectLabels(); - return (
@@ -49,15 +46,6 @@ export const AddressProbeStep: Component = (props) => { Paste a hostname, IP, or URL to identify a supported platform. Pulse validates the match and asks for credentials next.

-
- - {(label) => ( - - {label} - - )} - -
@@ -93,7 +81,7 @@ export const AddressProbeStep: Component = (props) => { , or if this is - bare-metal Linux / Unraid / FreeBSD,{' '} + a Linux, macOS, Windows, FreeBSD, or Unraid host,{' '} install Pulse Agent instead} diff --git a/frontend-modern/src/components/Settings/ConnectionEditor/ConnectionEditor.tsx b/frontend-modern/src/components/Settings/ConnectionEditor/ConnectionEditor.tsx index 761a8eaa2..a5295d196 100644 --- a/frontend-modern/src/components/Settings/ConnectionEditor/ConnectionEditor.tsx +++ b/frontend-modern/src/components/Settings/ConnectionEditor/ConnectionEditor.tsx @@ -6,10 +6,7 @@ import { createConnectionEditorState, type ConnectionEditorState, } from './useConnectionEditor'; -import { - INFRASTRUCTURE_ONBOARDING_STEPS, - getInfrastructureAutoDetectLabels, -} from '@/utils/infrastructureOnboardingPresentation'; +import { getInfrastructureAutoDetectLabels } from '@/utils/infrastructureOnboardingPresentation'; import { createInfrastructureOnboardingMetricsTracker, type InfrastructureOnboardingMetricsTracker, @@ -31,10 +28,13 @@ export interface ConnectionEditorProps { mode?: ConnectionEditorMode; initialType?: ConnectionType; initialAddress?: string; + initialCandidate?: ProbeCandidate | null; showSlotHeader?: boolean; trackInitialCatalogSelection?: boolean; onboardingMetricsTracker?: InfrastructureOnboardingMetricsTracker | null; onBackToCatalog?: () => void; + onSelectAgentRoute?: () => void; + onSelectCandidate?: (candidate: ProbeCandidate) => void; renderCredentialSlot: CredentialSlotRenderer; onClose: () => void; onSaved?: () => void; @@ -49,7 +49,9 @@ export const ConnectionEditor: Component = (props) => { const [selectedType, setSelectedType] = createSignal( props.initialType ?? null, ); - const [selectedCandidate, setSelectedCandidate] = createSignal(null); + const [selectedCandidate, setSelectedCandidate] = createSignal( + props.initialCandidate ?? null, + ); const ownsOnboardingMetricsTracker = (props.mode ?? 'add') === 'add' && !props.onboardingMetricsTracker; const onboardingMetrics = @@ -86,6 +88,10 @@ export const ConnectionEditor: Component = (props) => { const chooseCandidate = (candidate: ProbeCandidate) => { onboardingMetrics?.recordPathSelected('api'); + if (props.onSelectCandidate) { + props.onSelectCandidate(candidate); + return; + } setSelectedCandidate(candidate); setSelectedType(candidate.type); }; @@ -96,6 +102,15 @@ export const ConnectionEditor: Component = (props) => { setSelectedType(type); }; + const installAgent = () => { + if (props.onSelectAgentRoute) { + onboardingMetrics?.recordPathSelected('agent'); + props.onSelectAgentRoute(); + return; + } + chooseManualType('agent'); + }; + const reopenProbe = () => { state.reset(); setSelectedCandidate(null); @@ -119,74 +134,51 @@ export const ConnectionEditor: Component = (props) => { when={showCredentialSlot()} fallback={
-
-
+
+
-
Detect from address
-

- Enter a hostname, IP, or URL and Pulse will try to identify a supported - platform automatically before opening the matching credential form. +

Address probe
+

+ Pulse can auto-detect these platforms from an address when their management API + is reachable.

-
- -
-
-
Address probe
-

- Pulse can auto-detect these platforms from an address when their management API - is reachable. -

-
{(label) => renderBadge(label)}
+

+ Not in this list?{' '} + {' '} + for Linux, macOS, Windows, FreeBSD, or Unraid hosts. +

+ chooseManualType('agent')} + onInstallAgent={installAgent} onChooseSourceTypeInstead={props.onBackToCatalog} onProbeSubmitted={() => onboardingMetrics?.recordPathSelected('api')} onProbeResolved={(outcome) => onboardingMetrics?.recordProbeResult(outcome)} />
- -
-
-
-
What happens next
-

- Pulse validates the connection before the system lands in the shared - infrastructure ledger. -

-
- -
- - {(step, index) => ( -
-
- Step {index() + 1} -
-
{step}
-
- )} -
-
-
-
} > @@ -214,6 +206,24 @@ export const ConnectionEditor: Component = (props) => {
+ +
+ +
+
+
{props.renderCredentialSlot({ mode: props.mode ?? 'add', 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 6a232eac3..7efcbd017 100644 --- a/frontend-modern/src/components/Settings/ConnectionEditor/__tests__/ConnectionEditor.test.tsx +++ b/frontend-modern/src/components/Settings/ConnectionEditor/__tests__/ConnectionEditor.test.tsx @@ -82,10 +82,9 @@ describe('ConnectionEditor', () => { /> )); - expect(screen.getByText('Detect from address')).toBeInTheDocument(); expect(screen.getByText('Address probe')).toBeInTheDocument(); expect(screen.getByRole('button', { name: /Back to source types/i })).toBeInTheDocument(); - expect(screen.getByText('What happens next')).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /Install Pulse Agent/i })).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: /Back to source types/i })); expect(onBackToCatalog).toHaveBeenCalledTimes(1); @@ -164,7 +163,7 @@ describe('ConnectionEditor', () => { const resetInput = screen.getByPlaceholderText(/vcenter\.lab/) as HTMLInputElement; expect(resetInput.value).toBe(''); expect(screen.queryByTestId('slot')).toBeNull(); - expect(screen.getByText('Detect from address')).toBeInTheDocument(); + expect(screen.getByText('Address probe')).toBeInTheDocument(); }); it('uses an injected tracker for direct type routes without creating another one', async () => { diff --git a/frontend-modern/src/components/Settings/InfrastructureSourcePicker.tsx b/frontend-modern/src/components/Settings/InfrastructureSourcePicker.tsx index 28a70f22c..c629c6961 100644 --- a/frontend-modern/src/components/Settings/InfrastructureSourcePicker.tsx +++ b/frontend-modern/src/components/Settings/InfrastructureSourcePicker.tsx @@ -28,28 +28,18 @@ export const InfrastructureSourcePicker: Component -
-
-
-
Choose a source type
-

- Add the kind of infrastructure you want Pulse to connect. Existing sources stay - visible on the page behind this dialog so you can open one, close it, and keep - managing the same list. -

-
- - - + +
+
-
+ {(group) => ( diff --git a/frontend-modern/src/utils/infrastructureOnboardingPresentation.ts b/frontend-modern/src/utils/infrastructureOnboardingPresentation.ts index 2a7cdedb9..b33f01503 100644 --- a/frontend-modern/src/utils/infrastructureOnboardingPresentation.ts +++ b/frontend-modern/src/utils/infrastructureOnboardingPresentation.ts @@ -17,6 +17,7 @@ export interface InfrastructureOnboardingProductPresentation { catalogDescription: string; autoDetect: boolean; governanceState: PlatformGovernanceState; + defaultSurfaceKeys: readonly string[]; } export interface InfrastructureSourceManagerProductPresentation extends InfrastructureOnboardingProductPresentation { @@ -37,6 +38,7 @@ interface BaseProductPresentation { catalogDescription: string; autoDetect: boolean; sourcePlatformId?: string; + defaultSurfaceKeys: readonly string[]; } export interface InfrastructureOnboardingPathPresentation { @@ -57,6 +59,7 @@ const PRODUCT_PRESENTATION: Record< coverage: 'Low-overhead host telemetry, SMART, services, Docker, and Kubernetes', catalogDescription: 'Low-overhead host telemetry, services, Docker, Kubernetes', autoDetect: false, + defaultSurfaceKeys: ['host'], }, vmware: { label: 'VMware vCenter', @@ -65,6 +68,7 @@ const PRODUCT_PRESENTATION: Record< catalogDescription: 'VM inventory, ESXi hosts, datastores', autoDetect: true, sourcePlatformId: 'vmware-vsphere', + defaultSurfaceKeys: ['vms', 'hosts', 'datastores'], }, truenas: { label: 'TrueNAS SCALE', @@ -73,6 +77,7 @@ const PRODUCT_PRESENTATION: Record< catalogDescription: 'Pools, datasets, apps, replications', autoDetect: true, sourcePlatformId: 'truenas', + defaultSurfaceKeys: ['datasets', 'pools', 'replication'], }, pve: { label: 'Proxmox VE', @@ -82,6 +87,7 @@ const PRODUCT_PRESENTATION: Record< catalogDescription: 'VMs, containers, storage, cluster health', autoDetect: true, sourcePlatformId: 'proxmox-pve', + defaultSurfaceKeys: ['vms', 'containers', 'storage', 'backups'], }, pbs: { label: 'Proxmox Backup Server', @@ -90,6 +96,7 @@ const PRODUCT_PRESENTATION: Record< catalogDescription: 'Backup jobs, sync, verify, prune, GC', autoDetect: true, sourcePlatformId: 'proxmox-pbs', + defaultSurfaceKeys: ['backups', 'datastores', 'syncJobs', 'verifyJobs', 'pruneJobs', 'garbageJobs'], }, pmg: { label: 'Proxmox Mail Gateway', @@ -98,6 +105,7 @@ const PRODUCT_PRESENTATION: Record< catalogDescription: 'Mail stats, queues, quarantine, relay health', autoDetect: true, sourcePlatformId: 'proxmox-pmg', + defaultSurfaceKeys: ['mailStats', 'queues', 'quarantine', 'domainStats'], }, }; @@ -159,21 +167,19 @@ export const INFRASTRUCTURE_ONBOARDING_PATHS: Record< }, }; -export const INFRASTRUCTURE_ONBOARDING_STEPS = [ - 'Probe address', - 'Identify platform', - 'Request credentials', - 'Validate access', - 'Start monitoring', -] as const; - export const INFRASTRUCTURE_AGENT_DISCOVERY_LABELS = [ 'Pulse Agent hosts', 'Docker', 'Kubernetes', ] as const; -export const INFRASTRUCTURE_AGENT_HOST_LABELS = ['Linux', 'FreeBSD', 'Unraid'] as const; +export const INFRASTRUCTURE_AGENT_HOST_LABELS = [ + 'Linux', + 'macOS', + 'Windows', + 'FreeBSD', + 'Unraid', +] as const; const SOURCE_PICKER_GROUPS: InfrastructureSourcePickerGroupPresentation[] = [ { diff --git a/tests/integration/README.md b/tests/integration/README.md index 968154e01..9dd0528d5 100644 --- a/tests/integration/README.md +++ b/tests/integration/README.md @@ -55,10 +55,11 @@ End-to-end Playwright tests that validate critical user flows against a running - Mocks a blocked Patrol runtime with stale healthy summary payloads - Proves the real `/ai` route shows Patrol as paused and suppresses stale healthy summary copy - `tests/68-infrastructure-onboarding.spec.ts` — infrastructure onboarding browser proof: - - Proves `/settings/infrastructure` behaves as the persistent source-manager landing in the shared settings shell - - Verifies direct type-add and detect-from-address both open managed dialogs while the source-manager surface remains visible underneath - - Verifies the onboarding funnel emits catalog-driven API handoff and no-match-to-agent fallback metrics on the real browser runtime - - Verifies the mobile infrastructure manager fits the viewport without horizontal overflow + - Proves `/settings/infrastructure` stays instance-first until the user opens the add flow + - Verifies the add tile opens a grouped source-type picker, with detect-from-address as a secondary utility inside that modal flow + - Verifies an explicit discovery run surfaces Proxmox-family candidates in the source-manager table and opens the matching prefilled review dialog + - Verifies the onboarding funnel emits picker-driven API handoff and no-match-to-agent fallback metrics on the real browser runtime + - Verifies the mobile landing and picker modal fit the viewport without horizontal overflow - `tests/69-diagnostics-onboarding.spec.ts` — diagnostics onboarding analytics browser proof: - Proves the Diagnostics & Health page renders the infrastructure onboarding analytics card after a real diagnostics run - Verifies the shared diagnostics surface shows onboarding path/platform attribution alongside the existing commercial funnel