diff --git a/docs/release-control/v6/internal/PLATFORM_SUPPORT_MANIFEST.json b/docs/release-control/v6/internal/PLATFORM_SUPPORT_MANIFEST.json index 8474eeca7..fbb142cbd 100644 --- a/docs/release-control/v6/internal/PLATFORM_SUPPORT_MANIFEST.json +++ b/docs/release-control/v6/internal/PLATFORM_SUPPORT_MANIFEST.json @@ -1,13 +1,13 @@ { "schema_version": 1, "default_infrastructure_source_order": [ - "proxmox-pve", "agent", - "docker", + "truenas", + "proxmox-pve", "proxmox-pbs", "proxmox-pmg", - "kubernetes", - "truenas" + "docker", + "kubernetes" ], "platforms": [ { diff --git a/docs/release-control/v6/internal/subsystems/agent-lifecycle.md b/docs/release-control/v6/internal/subsystems/agent-lifecycle.md index f1632c264..84b3b65a4 100644 --- a/docs/release-control/v6/internal/subsystems/agent-lifecycle.md +++ b/docs/release-control/v6/internal/subsystems/agent-lifecycle.md @@ -1001,18 +1001,21 @@ surfaces grow a second VMware availability fetch or a VMware-only handoff path. That same infrastructure workspace boundary now also owns the first-run handoff copy for new operators. `InfrastructureWorkspace.tsx` must keep -`Install on a host` visible as the first monitored-system path while still -presenting `Platform connections` as the explicit API-backed alternative -instead of leaving first-session install guidance implicit in generic -settings-shell prose or retreating to one provider's name as the primary -alternative. +`Install on a host` and `Platform connections` explicit in the shared +workspace instead of leaving first-session guidance implicit in generic +settings-shell prose or retreating to one provider's name or one onboarding +mode as the primary story. That same first-run infrastructure handoff now also owns the inline add-infrastructure landing in `ConnectionEditor.tsx`: when the target can run -the Unified Agent, the landing must lead with the recommended host-install -card above the platform-API probe and picker instead of teaching probe-first -as the default story, and returning `Back to catalog` must reset probe input -and result state rather than reopening the landing with stale no-match or -detected-product state already rendered. +the Unified Agent, the landing must lead with the platform-API probe and +picker as the primary catalog story while keeping the host-install path in a +separate secondary section instead of teaching one provider-specific shortcut +as the frame for the whole screen. Product-specific host advantages such as +Proxmox auto-registration may be explained inside that host-install section, +but they must not become the headline copy or top visual weight for the +shared add-infrastructure landing. Returning `Back to catalog` must reset +probe input and result state rather than reopening the landing with stale +no-match or detected-product state already rendered. Render-order proof for that landing belongs to DOM-backed settings tests, not raw source-string position checks, so lifecycle ownership continues to guard the operator-visible order after reasonable component extraction or copy diff --git a/frontend-modern/src/components/Settings/ConnectionEditor/AddressProbeStep.tsx b/frontend-modern/src/components/Settings/ConnectionEditor/AddressProbeStep.tsx index 1269f1f95..4de1b1a89 100644 --- a/frontend-modern/src/components/Settings/ConnectionEditor/AddressProbeStep.tsx +++ b/frontend-modern/src/components/Settings/ConnectionEditor/AddressProbeStep.tsx @@ -26,7 +26,7 @@ export const AddressProbeStep: Component = (props) => { id="connection-address" type="text" class={formControl} - placeholder="pve01.lan, 10.0.0.4:8006, https://pbs.lab:8007" + placeholder="vcenter.lab, truenas.lan, https://pve.lab:8006" value={props.state.address()} onInput={(event) => props.state.setAddress(event.currentTarget.value)} autocomplete="off" diff --git a/frontend-modern/src/components/Settings/ConnectionEditor/ConnectionEditor.tsx b/frontend-modern/src/components/Settings/ConnectionEditor/ConnectionEditor.tsx index 9f8ffcd07..02f3f9411 100644 --- a/frontend-modern/src/components/Settings/ConnectionEditor/ConnectionEditor.tsx +++ b/frontend-modern/src/components/Settings/ConnectionEditor/ConnectionEditor.tsx @@ -4,6 +4,7 @@ import type { ConnectionType, ProbeCandidate } from '@/api/connections'; import { AddressProbeStep } from './AddressProbeStep'; import { CONNECTION_TYPE_LABELS, + DEFAULT_CONNECTION_EDITOR_PLATFORM_TYPES, createConnectionEditorState, type ConnectionEditorState, } from './useConnectionEditor'; @@ -30,12 +31,6 @@ export interface ConnectionEditorProps { onSaved?: () => void; } -// Platform integrations — connect to a product's management API. Peers of -// each other. The agent is NOT in this list: it is a different kind of -// integration (see the dedicated section below the grid) and surfacing it -// as a tile alongside these hides what it actually adds. -const DEFAULT_PLATFORM_TYPES: ConnectionType[] = ['pve', 'pbs', 'pmg', 'vmware', 'truenas']; - interface TileMeta { icon: Component<{ class?: string }>; description: string; @@ -61,7 +56,9 @@ export const ConnectionEditor: Component = (props) => { const [selectedCandidate, setSelectedCandidate] = createSignal(null); const platformOptions = createMemo(() => - (props.manualTypeOptions ?? DEFAULT_PLATFORM_TYPES).filter((type) => type !== 'agent'), + (props.manualTypeOptions ?? DEFAULT_CONNECTION_EDITOR_PLATFORM_TYPES).filter( + (type) => type !== 'agent', + ), ); const activeType = () => selectedType(); @@ -94,62 +91,14 @@ export const ConnectionEditor: Component = (props) => { when={showCredentialSlot()} fallback={
-
- -
-
-
-
+ +
+
+
Install on a host instead
+

+ Use the Pulse Agent when you want machine-level telemetry or the system has no + management API to connect. +

+
+ + +
} > 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 7f7d55172..a7161e7fd 100644 --- a/frontend-modern/src/components/Settings/ConnectionEditor/__tests__/ConnectionEditor.test.tsx +++ b/frontend-modern/src/components/Settings/ConnectionEditor/__tests__/ConnectionEditor.test.tsx @@ -38,7 +38,7 @@ describe('ConnectionEditor', () => { render(() => {}} />); - const input = screen.getByPlaceholderText(/pve01\.lan/) as HTMLInputElement; + const input = screen.getByPlaceholderText(/vcenter\.lab/) as HTMLInputElement; fireEvent.input(input, { target: { value: 'pve.lab' } }); const probeButton = screen.getByRole('button', { name: /probe address/i }); @@ -66,7 +66,7 @@ describe('ConnectionEditor', () => { render(() => {}} />); - const input = screen.getByPlaceholderText(/pve01\.lan/) as HTMLInputElement; + const input = screen.getByPlaceholderText(/vcenter\.lab/) as HTMLInputElement; fireEvent.input(input, { target: { value: '192.168.1.50' } }); fireEvent.click(screen.getByRole('button', { name: /probe address/i })); @@ -84,26 +84,29 @@ describe('ConnectionEditor', () => { expect(lastCall.candidate).toBeNull(); }); - it('renders an agent-led catalog with the API fallback beneath it', () => { + it('renders a platform-first catalog with the host-install path beneath it', () => { render(() =>
} onClose={() => {}} />); + const platformHeading = screen.getByText('Connect a platform'); const agentButton = screen.getByRole('button', { name: /Install Pulse Agent/i }); - const apiHeading = screen.getByText('Or connect a platform API directly'); const probeButton = screen.getByRole('button', { name: /probe address/i }); + const vmwareButton = screen.getByRole('button', { name: /VMware vCenter \/ ESXi/i }); + const trueNASButton = screen.getByRole('button', { name: /TrueNAS SCALE/i }); + const proxmoxButton = screen.getByRole('button', { name: /^Proxmox VE/i }); - // Catalog landing — lead with the agent card, then collapse the probe and - // direct platform options into one API fallback section below it. + // Catalog landing — lead with management-platform onboarding, then keep + // host install as a secondary path below it. + expect(platformHeading).toBeInTheDocument(); expect(agentButton).toBeInTheDocument(); - expect(apiHeading).toBeInTheDocument(); expect(probeButton).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(); - expectNodeBefore(agentButton, apiHeading); - expectNodeBefore(apiHeading, probeButton); - expect( - screen.queryByRole('button', { name: /install the unified agent on a host/i }), - ).toBeNull(); + expect(vmwareButton).toBeInTheDocument(); + expect(trueNASButton).toBeInTheDocument(); + expect(proxmoxButton).toBeInTheDocument(); + expectNodeBefore(platformHeading, probeButton); + expectNodeBefore(vmwareButton, trueNASButton); + expectNodeBefore(trueNASButton, proxmoxButton); + expectNodeBefore(proxmoxButton, agentButton); + expect(screen.queryByText('Recommended')).toBeNull(); }); it('offers the agent path contextually when a probe returns no match', async () => { @@ -113,7 +116,7 @@ describe('ConnectionEditor', () => { render(() => {}} />); - const input = screen.getByPlaceholderText(/pve01\.lan/) as HTMLInputElement; + const input = screen.getByPlaceholderText(/vcenter\.lab/) as HTMLInputElement; fireEvent.input(input, { target: { value: 'baremetal.lan' } }); fireEvent.click(screen.getByRole('button', { name: /probe address/i })); await waitFor(() => expect(mockedProbe).toHaveBeenCalled()); @@ -158,7 +161,7 @@ describe('ConnectionEditor', () => { render(() => {}} />); - const input = screen.getByPlaceholderText(/pve01\.lan/) as HTMLInputElement; + const input = screen.getByPlaceholderText(/vcenter\.lab/) as HTMLInputElement; fireEvent.input(input, { target: { value: '192.168.1.50' } }); fireEvent.click(screen.getByRole('button', { name: /probe address/i })); @@ -170,7 +173,7 @@ describe('ConnectionEditor', () => { fireEvent.click(screen.getByRole('button', { name: /back to catalog/i })); - const resetInput = screen.getByPlaceholderText(/pve01\.lan/) as HTMLInputElement; + const resetInput = screen.getByPlaceholderText(/vcenter\.lab/) as HTMLInputElement; expect(resetInput.value).toBe(''); expect(screen.queryByText(/no supported product detected/i)).toBeNull(); expect(screen.queryByTestId('slot')).toBeNull(); diff --git a/frontend-modern/src/components/Settings/ConnectionEditor/useConnectionEditor.ts b/frontend-modern/src/components/Settings/ConnectionEditor/useConnectionEditor.ts index 49c2dfadc..f11c7a423 100644 --- a/frontend-modern/src/components/Settings/ConnectionEditor/useConnectionEditor.ts +++ b/frontend-modern/src/components/Settings/ConnectionEditor/useConnectionEditor.ts @@ -5,6 +5,7 @@ import { type ProbeCandidate, type ProbeResponse, } from '@/api/connections'; +import { DEFAULT_INFRASTRUCTURE_SOURCE_ORDER } from '@/utils/platformSupportManifest'; const PROBE_ERROR_FALLBACK = 'Probe failed. Try again or enter credentials manually.'; @@ -31,6 +32,38 @@ export interface ConnectionEditorState { runProbe: () => Promise; } +type PlatformConnectionType = Extract; + +const SOURCE_PLATFORM_TO_CONNECTION_TYPE: Partial> = { + 'vmware-vsphere': 'vmware', + truenas: 'truenas', + 'proxmox-pve': 'pve', + 'proxmox-pbs': 'pbs', + 'proxmox-pmg': 'pmg', +}; + +// The supported-source manifest order is reused where it applies, but the +// add-infrastructure catalog still needs to surface the admitted vSphere path. +const CONNECTION_EDITOR_PRIORITY_TYPES: PlatformConnectionType[] = ['vmware']; + +const PLATFORM_CONNECTION_TYPE_FALLBACK_ORDER: PlatformConnectionType[] = [ + 'truenas', + 'pve', + 'pbs', + 'pmg', +]; + +export const DEFAULT_CONNECTION_EDITOR_PLATFORM_TYPES: PlatformConnectionType[] = Array.from( + new Set([ + ...CONNECTION_EDITOR_PRIORITY_TYPES, + ...DEFAULT_INFRASTRUCTURE_SOURCE_ORDER.flatMap((platformKey) => { + const type = SOURCE_PLATFORM_TO_CONNECTION_TYPE[platformKey]; + return type ? [type] : []; + }), + ...PLATFORM_CONNECTION_TYPE_FALLBACK_ORDER, + ]), +); + // Validation on the client side is intentionally lenient: the backend is the // real authority on what constitutes a probeable address. We only reject the // obviously empty case so the API does not see a payload it will always diff --git a/frontend-modern/src/components/Settings/__tests__/InfrastructureWorkspace.test.tsx b/frontend-modern/src/components/Settings/__tests__/InfrastructureWorkspace.test.tsx index 20aa65453..3367b6d21 100644 --- a/frontend-modern/src/components/Settings/__tests__/InfrastructureWorkspace.test.tsx +++ b/frontend-modern/src/components/Settings/__tests__/InfrastructureWorkspace.test.tsx @@ -119,6 +119,10 @@ const connectionFixture = (overrides: Partial = {}): Connection => ( ...overrides, }); +function expectNodeBefore(a: Node, b: Node) { + expect(a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy(); +} + const baseProps = () => ({ selectedAgent: () => 'pve', @@ -227,14 +231,23 @@ describe('InfrastructureWorkspace', () => { fireEvent.click(screen.getByRole('button', { name: /Add infrastructure/i })); - // The catalog landing leads with the agent path and keeps the direct API - // probe/catalog visible below it in the same shared editor. + // The catalog landing leads with peer platform onboarding and keeps the + // host install path available beneath it in the same shared editor. + const platformHeading = screen.getByText('Connect a platform'); + const vmwareButton = screen.getByRole('button', { name: /VMware vCenter \/ ESXi/i }); + const trueNASButton = screen.getByRole('button', { name: /TrueNAS SCALE/i }); + const proxmoxButton = screen.getByRole('button', { name: /^Proxmox VE/i }); + const agentButton = screen.getByRole('button', { name: /Install Pulse Agent/i }); + + expect(platformHeading).toBeInTheDocument(); expect(screen.getByRole('button', { name: /Install Pulse Agent/i })).toBeInTheDocument(); - expect(screen.getByText('Or connect a platform API directly')).toBeInTheDocument(); expect(screen.getByRole('button', { name: /Probe address/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(proxmoxButton).toBeInTheDocument(); + expect(trueNASButton).toBeInTheDocument(); + expect(vmwareButton).toBeInTheDocument(); + expectNodeBefore(vmwareButton, trueNASButton); + expectNodeBefore(trueNASButton, proxmoxButton); + expectNodeBefore(proxmoxButton, agentButton); expect(navigateSpy).not.toHaveBeenCalled(); expect(setSearchParamsSpy).not.toHaveBeenCalled(); }); diff --git a/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts b/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts index 74b608617..37da40cc9 100644 --- a/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts +++ b/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts @@ -81,28 +81,18 @@ describe('settings architecture guardrails', () => { expect(infrastructureWorkspaceSource).not.toContain('layout="drawer-right"'); }); - it('keeps the agent-led add landing and inline node credentials on the shared editor model', () => { + it('keeps the platform-first add landing and inline node credentials on the shared editor model', () => { expect(connectionEditorSource).toContain( "import { AddressProbeStep } from './AddressProbeStep';", ); - // Platform integrations render as peer tiles; the agent lives in its own - // section below so it can explain what host-level telemetry adds instead - // of being mistaken for one more peer of Proxmox / VMware / TrueNAS. - expect(connectionEditorSource).toContain('const DEFAULT_PLATFORM_TYPES: ConnectionType[] ='); - expect(connectionEditorSource).not.toContain("'agent'] ="); + expect(connectionEditorSource).toContain('DEFAULT_CONNECTION_EDITOR_PLATFORM_TYPES'); expect(connectionEditorSource).toContain(' { expect(connectionEditorStateSource).toContain('ConnectionsAPI.probe(value)'); expect(connectionEditorStateSource).toContain('export const CONNECTION_TYPE_LABELS'); + expect(connectionEditorStateSource).toContain('DEFAULT_INFRASTRUCTURE_SOURCE_ORDER'); + expect(connectionEditorStateSource).toContain( + 'export const DEFAULT_CONNECTION_EDITOR_PLATFORM_TYPES', + ); expect(nodeCredentialSlotSource).toContain('useNodeModalState(modalProps)'); expect(nodeCredentialSlotSource).toContain(' { it('orders canonical source platform keys with preferred source ordering', () => { expect(orderSourcePlatformKeys(['truenas', 'pbs', 'agent', 'docker'])).toEqual([ 'agent', - 'docker', - 'proxmox-pbs', 'truenas', + 'proxmox-pbs', + 'docker', ]); }); @@ -25,13 +25,13 @@ describe('sourcePlatformOptions', () => { it('exports the default infrastructure source options in canonical order', () => { expect(DEFAULT_INFRASTRUCTURE_SOURCE_OPTIONS.map((option) => option.key)).toEqual([ - 'proxmox-pve', 'agent', - 'docker', + 'truenas', + 'proxmox-pve', 'proxmox-pbs', 'proxmox-pmg', + 'docker', 'kubernetes', - 'truenas', ]); }); }); diff --git a/frontend-modern/src/utils/platformSupportManifest.generated.ts b/frontend-modern/src/utils/platformSupportManifest.generated.ts index 32f93592c..dc900ee3d 100644 --- a/frontend-modern/src/utils/platformSupportManifest.generated.ts +++ b/frontend-modern/src/utils/platformSupportManifest.generated.ts @@ -1,22 +1,22 @@ // This file is generated by scripts/release_control/generate_platform_support_frontend_module.py. // Do not edit by hand. // Source: docs/release-control/v6/internal/PLATFORM_SUPPORT_MANIFEST.json -// Source SHA256: 79568ec8c69f13d8d49afe2b7a82b28c921b1885a90ebfb08cc98284d48b80e9 +// Source SHA256: afe3841efbb355a9d6245e3aea6af47d31d87a870a44cba5c79ec749448cd78f export const PLATFORM_SUPPORT_MANIFEST_SOURCE = { path: 'docs/release-control/v6/internal/PLATFORM_SUPPORT_MANIFEST.json', - sha256: '79568ec8c69f13d8d49afe2b7a82b28c921b1885a90ebfb08cc98284d48b80e9', + sha256: 'afe3841efbb355a9d6245e3aea6af47d31d87a870a44cba5c79ec749448cd78f', } as const; export const PLATFORM_SUPPORT_MANIFEST = { schemaVersion: 1, defaultInfrastructureSourceOrder: [ - 'proxmox-pve', 'agent', - 'docker', + 'truenas', + 'proxmox-pve', 'proxmox-pbs', 'proxmox-pmg', + 'docker', 'kubernetes', - 'truenas', ], platforms: [ { @@ -208,13 +208,13 @@ export const KNOWN_SOURCE_PLATFORM_KEYS = [ 'generic', ] as const; export const DEFAULT_INFRASTRUCTURE_SOURCE_ORDER = [ - 'proxmox-pve', 'agent', - 'docker', + 'truenas', + 'proxmox-pve', 'proxmox-pbs', 'proxmox-pmg', + 'docker', 'kubernetes', - 'truenas', ] as const; export const SOURCE_PLATFORM_ALIAS_MAP = { k8s: 'kubernetes',