mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-04-28 03:20:11 +00:00
Refine infrastructure onboarding flows
This commit is contained in:
parent
643db3f378
commit
485bac636b
10 changed files with 880 additions and 688 deletions
|
|
@ -116,6 +116,12 @@ management, and fleet control surfaces.
|
|||
22. `scripts/install.ps1` shared with `deployment-installability`: the Windows installer is both a deployment installability entry point and a canonical agent lifecycle runtime continuity boundary.
|
||||
23. `scripts/install.sh` shared with `deployment-installability`: the shell installer is both a deployment installability entry point and a canonical agent lifecycle runtime continuity boundary.
|
||||
|
||||
The node setup modal boundary must keep guided setup and manual credential
|
||||
submission separate. For new PVE/PBS setup, Agent Install and Direct Connection
|
||||
setup-script modes are command-driven auto-registration paths; Token ID/Value
|
||||
fields, Test Connection, and Add Node submission belong only to Manual Token
|
||||
Setup or existing-node edit flows.
|
||||
|
||||
That shared monitored-system admission preview boundary also owns the disabled
|
||||
platform-connection lifecycle state. Once a TrueNAS or VMware setup form marks
|
||||
the connection disabled, lifecycle surfaces must treat a canonical zero-delta
|
||||
|
|
@ -227,7 +233,7 @@ an add-only capacity posture.
|
|||
Persistence-sensitive NAS targets must keep one canonical continuity model here: installer-owned bootstraps may use flash-backed or immutable-root launch hooks only as thin trampolines, while the durable wrapper, state, and reboot-surviving binary copy stay in the governed persistent state directory that updater continuity also refreshes.
|
||||
9. Add or change profile management, the extracted agent profiles runtime owner, the infrastructure source-manager landing, the pure unified-agent inventory/install model, the connections-ledger workspace shell, the unified ConnectionEditor and its per-type credential slots, route model, shared install section owner, the shared direct-node/discovery infrastructure settings owners plus their model, shared frontend install-command assembly, Proxmox setup/install API transport, TrueNAS platform-connection management, VMware platform-connection management, the shared monitored-system admission preview shell for those platform connections, setup-completion install handoff transport, deploy-fallback manual install transport, and fleet-control presentation through `frontend-modern/src/api/agentProfiles.ts`, `frontend-modern/src/api/nodes.ts`, `frontend-modern/src/components/Settings/AgentProfilesPanel.tsx`, `frontend-modern/src/components/Settings/useAgentProfilesPanelState.ts`, `frontend-modern/src/components/Settings/ConnectionsTable.tsx`, `frontend-modern/src/components/Settings/connectionsTableModel.ts`, `frontend-modern/src/components/Settings/useConnectionsLedger.ts`, `frontend-modern/src/components/Settings/useConnectionRowActions.ts`, `frontend-modern/src/components/Settings/ConnectionEditor/ConnectionEditor.tsx`, `frontend-modern/src/components/Settings/ConnectionEditor/AddressProbeStep.tsx`, `frontend-modern/src/components/Settings/ConnectionEditor/useConnectionEditor.ts`, `frontend-modern/src/components/Settings/ConnectionEditor/CredentialSlots/NodeCredentialSlot.tsx`, `frontend-modern/src/components/Settings/ConnectionEditor/CredentialSlots/TrueNASCredentialSlot.tsx`, `frontend-modern/src/components/Settings/ConnectionEditor/CredentialSlots/VMwareCredentialSlot.tsx`, `frontend-modern/src/components/Settings/infrastructureOperationsModel.tsx`, `frontend-modern/src/components/Settings/InfrastructureInstallerSection.tsx`, `frontend-modern/src/components/Settings/InfrastructureWorkspace.tsx`, `frontend-modern/src/components/Settings/InfrastructureSourceManager.tsx`, `frontend-modern/src/components/Settings/infrastructureWorkspaceModel.ts`, `frontend-modern/src/components/Settings/MonitoredSystemAdmissionPreview.tsx`, `frontend-modern/src/components/Settings/platformConnectionsModel.ts`, `frontend-modern/src/components/Settings/useTrueNASSettingsPanelState.ts`, `frontend-modern/src/components/Settings/useVMwareSettingsPanelState.ts`, `frontend-modern/src/components/Settings/proxmoxSettingsModel.ts`, `frontend-modern/src/components/Settings/ConfiguredNodeTables.tsx`, `frontend-modern/src/components/Settings/SettingsSectionNav.tsx`, `frontend-modern/src/components/Settings/infrastructureSettingsModel.ts`, `frontend-modern/src/components/Settings/useInfrastructureConfiguredNodesState.ts`, `frontend-modern/src/components/Settings/useInfrastructureDiscoveryRuntimeState.ts`, `frontend-modern/src/components/Settings/useInfrastructureInstallState.tsx`, `frontend-modern/src/components/Settings/useInfrastructureOperationsState.tsx`, `frontend-modern/src/components/Settings/useInfrastructureSettingsState.ts`, `frontend-modern/src/components/Settings/nodeModalModel.ts`, `frontend-modern/src/components/Settings/useNodeModalState.ts`, `frontend-modern/src/components/SetupWizard/SetupCompletionPanel.tsx`, and `frontend-modern/src/utils/agentInstallCommand.ts`. Phase 9 retired the legacy reporting/inventory surface (InfrastructureOperationsController, InfrastructureInventorySection, InfrastructureActiveRowDetails, InfrastructureIgnoredRowDetails, InfrastructureStopMonitoringDialog, useInfrastructureReportingState) and the per-type shells (PlatformConnectionsWorkspace, ProxmoxSettingsPanel, ProxmoxDirectWorkspace, ProxmoxConfiguredNodesTable, ProxmoxDirectConnectionsCard, ProxmoxDiscoveryResultsCard, ProxmoxDeleteNodeDialog, ProxmoxNodeModalStack, NodeModal shell, TrueNASSettingsPanel, VMwareSettingsPanel, useProxmoxDirectWorkspaceState); lifecycle extensions must route through the unified aggregator ledger, source-manager cards, and ConnectionEditor credential slots rather than reintroducing those retired surfaces.
|
||||
Those lifecycle-owned settings hooks may consume websocket state only through `frontend-modern/src/contexts/appRuntime.ts`; they must not import `frontend-modern/src/App.tsx` or recreate root-shell providers.
|
||||
Discovery configuration is part of that same lifecycle-owned workspace boundary. `InfrastructureSourceManager.tsx` must open one canonical discovery editor through `InfrastructureDiscoverySettingsDialog.tsx`, `DiscoverySettingsForm.tsx`, and `discoverySettingsModel.ts`, while the System/Network shell stays limited to network-boundary controls instead of reintroducing a second editable discovery surface. That same workspace boundary now owns the add-flow entry split too: the landing strip opens the grouped `Add infrastructure` picker, while `Detect from address` remains a picker-owned utility route instead of a competing top-level header action.
|
||||
Discovery configuration is part of that same lifecycle-owned workspace boundary. `InfrastructureSourceManager.tsx` must open one canonical discovery editor through `InfrastructureDiscoverySettingsDialog.tsx`, `DiscoverySettingsForm.tsx`, and `discoverySettingsModel.ts`, while the System/Network shell stays limited to network-boundary controls instead of reintroducing a second editable discovery surface. That same workspace boundary now owns the add-flow entry split too: the landing guidance exposes `Detect from address`, `Install Pulse Agent`, and `Choose source type` as first-run actions above the source ledger, while header actions stay limited to ongoing discovery controls.
|
||||
The same lifecycle-owned workspace boundary now also owns attached-agent composition. When a unified Pulse Agent augments a first-class platform source such as Proxmox VE, the source manager and edit dialog must present one primary platform row with explicit `API` plus `Pulse Agent` composition rather than duplicating that same machine as a second peer row under a generic Pulse Agent platform bucket. Standalone hosts with no owning platform source remain grouped under a standalone-host owner bucket, with `Pulse Agent` shown as the collection method rather than as the pseudo-platform label.
|
||||
When that primary Proxmox source is cluster-backed, the same workspace boundary must render the row under the canonical cluster moniker carried by the backend grouping contract rather than under one sibling node's hostname. That same backend grouping contract must also carry the explicit member-node list for the cluster so the source manager can render child node composition such as `delly` and `minipc` beneath the owning cluster row with per-node coverage/status. Cluster-member node agents belong as augmentations on that owning Proxmox row and its child nodes, not as separate standalone-host peers.
|
||||
That same lifecycle-owned workspace boundary also owns agent version
|
||||
|
|
|
|||
|
|
@ -123,6 +123,11 @@ Own canonical runtime payload shapes between backend and frontend.
|
|||
29. `frontend-modern/src/utils/apiTokenPresentation.ts` shared with `security-privacy`: the API token presentation helper is both a security/privacy control surface and a canonical API token management boundary.
|
||||
30. `frontend-modern/src/utils/infrastructureSettingsPresentation.ts` shared with `agent-lifecycle`: the infrastructure settings presentation helper is both an agent lifecycle control surface and an API-backed direct-node/discovery settings boundary.
|
||||
31. `internal/api/access_control_handlers.go` shared with `organization-settings`: RBAC role and user-assignment handlers are both an organization settings control surface and a canonical API payload contract boundary.
|
||||
The shared node setup boundary above owns the guided/manual setup split
|
||||
for PVE/PBS consumers: Agent Install and Direct Connection setup-script
|
||||
modes are auto-registration paths, while Token ID/Value fields, Test
|
||||
Connection, and Add Node are manual-token or existing-node edit controls
|
||||
only.
|
||||
32. `internal/api/agent_install_command_shared.go` shared with `agent-lifecycle`: agent install command assembly is both an agent lifecycle control surface and a canonical API payload contract boundary.
|
||||
33. `internal/api/ai_handler.go` shared with `ai-runtime`: Pulse Assistant handlers are both an AI runtime control surface and a canonical API payload contract boundary.
|
||||
34. `internal/api/ai_handlers.go` shared with `ai-runtime`: AI settings and remediation handlers are both an AI runtime control surface and a canonical API payload contract boundary.
|
||||
|
|
@ -2281,6 +2286,11 @@ masked: copy-success messaging may not tell the operator to paste a token
|
|||
"shown below" once only `tokenHint` remains visible, and stale raw-token
|
||||
cleanup paths may not survive in one Proxmox branch after the shared UI state
|
||||
has moved to hint-only handling.
|
||||
That same shared settings consumer must keep command-driven setup and manual
|
||||
credential submission distinct. When a new PVE/PBS setup is in Agent Install or
|
||||
Direct Connection setup-script mode, the settings UI must not render Token
|
||||
ID/Value fields, Test Connection, or Add Node controls; those controls are only
|
||||
valid for Manual Token Setup or existing-node edit flows.
|
||||
That same shared frontend setup surface must also trim and validate the
|
||||
canonical `host` input before invoking `/api/setup-script` downloads, and the
|
||||
shared `frontend-modern/src/api/nodes.ts` helper must reject empty `host` or
|
||||
|
|
|
|||
|
|
@ -245,15 +245,16 @@ work extends shared components instead of creating new local variants.
|
|||
from a top-level product route such as `/recovery`, the first highlighted
|
||||
step should match that route instead of always restarting at Dashboard.
|
||||
5. Keep shared infrastructure shell state on the reusable settings boundary: `frontend-modern/src/components/Settings/useSettingsInfrastructurePanelProps.ts` and `frontend-modern/src/components/Settings/InfrastructureWorkspace.tsx` must continue to derive provider counts, availability, and shared subtab copy from one infrastructure-settings source — via the unified aggregator through `frontend-modern/src/components/Settings/useConnectionsLedger.ts` — instead of creating provider-local summary fetches or VMware-only shell vocabulary. Phase 9 retired the old `PlatformConnectionsWorkspace` per-type shell, but setup guidance may still use `Platform connections` as the operator-facing label for the shared API-backed onboarding path.
|
||||
That same shared shell boundary now owns the default posture for
|
||||
That same shared shell boundary now owns the first-run posture for
|
||||
`/settings/infrastructure`: the landing route should read as one
|
||||
source-manager workspace with configured infrastructure instances first
|
||||
and no redundant monitored-systems ledger beneath it. The landing route
|
||||
must not lead with
|
||||
connection-type explanations, onboarding-path copy, or separate detect
|
||||
utilities. Existing sources stay visible on the page, and add, detect,
|
||||
install, and edit flows open as secondary interactions from that same
|
||||
destination instead of taking over the whole page.
|
||||
and no redundant monitored-systems ledger beneath it. The landing route may
|
||||
include a compact guidance strip that explains platform APIs and host agents
|
||||
as Pulse 6 infrastructure sources and exposes `Detect from address`, `Install
|
||||
Pulse Agent`, and `Choose source type` as first-run actions. Existing sources
|
||||
stay visible on the page, and add, detect, install, and edit flows open as
|
||||
secondary interactions from that same destination instead of taking over the
|
||||
whole page.
|
||||
Those secondary views must stay under the same single `Infrastructure`
|
||||
sidebar destination, but they may open in governed modal/dialog chrome when
|
||||
that preserves the persistent source-manager page behind them.
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -875,6 +875,7 @@ const InfrastructureWorkspaceContent: Component<InfrastructureWorkspaceProps> =
|
|||
: (type) => openAddFlow(type === 'agent' ? 'agent' : (type as ManagedAddTypeStep))
|
||||
}
|
||||
onAddInfrastructure={readOnly() ? undefined : () => openAddFlow('pick')}
|
||||
onDetectFromAddress={readOnly() ? undefined : () => openAddFlow('detect')}
|
||||
onRunDiscovery={
|
||||
readOnly()
|
||||
? undefined
|
||||
|
|
|
|||
|
|
@ -22,12 +22,7 @@ export const NodeModalAuthenticationSection: Component<NodeModalAuthenticationSe
|
|||
|
||||
return (
|
||||
<div>
|
||||
<SectionHeader
|
||||
title="Authentication"
|
||||
size="sm"
|
||||
class="mb-4"
|
||||
titleClass="text-base-content"
|
||||
/>
|
||||
<SectionHeader title="Authentication" size="sm" class="mb-4" titleClass="text-base-content" />
|
||||
|
||||
<div class="mb-4">
|
||||
<div class="flex gap-4">
|
||||
|
|
@ -62,8 +57,8 @@ export const NodeModalAuthenticationSection: Component<NodeModalAuthenticationSe
|
|||
<Show when={modalProps.nodeType === 'pmg'}>
|
||||
<p class="text-xs text-muted mt-2">
|
||||
Proxmox Mail Gateway does not support API tokens. Use a service account with password
|
||||
authentication (for example <code>root@pam</code> or a dedicated{' '}
|
||||
<code>api@pmg</code> user).
|
||||
authentication (for example <code>root@pam</code> or a dedicated <code>api@pmg</code>{' '}
|
||||
user).
|
||||
</p>
|
||||
</Show>
|
||||
</div>
|
||||
|
|
@ -98,7 +93,9 @@ export const NodeModalAuthenticationSection: Component<NodeModalAuthenticationSe
|
|||
type="password"
|
||||
value={state.formData().password}
|
||||
onInput={(event) => state.updateField('password', event.currentTarget.value)}
|
||||
placeholder={state.isEditingExistingNode() ? 'Leave blank to keep existing' : 'Password'}
|
||||
placeholder={
|
||||
state.isEditingExistingNode() ? 'Leave blank to keep existing' : 'Password'
|
||||
}
|
||||
required={state.formData().authType === 'password' && !state.isEditingExistingNode()}
|
||||
class={controlClass()}
|
||||
/>
|
||||
|
|
@ -110,44 +107,58 @@ export const NodeModalAuthenticationSection: Component<NodeModalAuthenticationSe
|
|||
<div class="space-y-4">
|
||||
<NodeModalSetupGuideSection modalProps={modalProps} state={state} />
|
||||
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div class={formField}>
|
||||
<label class={labelClass()}>
|
||||
Token ID <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={state.formData().tokenName}
|
||||
onInput={(event) => state.updateField('tokenName', event.currentTarget.value)}
|
||||
placeholder={getNodeTokenIdPlaceholder(modalProps.nodeType)}
|
||||
required={state.formData().authType === 'token'}
|
||||
class={controlClass('font-mono')}
|
||||
/>
|
||||
<p class={formHelpText}>Full token ID from Proxmox (user@realm!tokenname).</p>
|
||||
<Show when={state.formData().setupMode === 'manual'}>
|
||||
<div class="rounded-md border border-border bg-surface-alt px-3 py-2 text-xs leading-5 text-muted">
|
||||
Use the manual token path only when you already created the API token yourself. The
|
||||
automatic setup paths register the node without copying token credentials into this
|
||||
form.
|
||||
</div>
|
||||
|
||||
<div class={formField}>
|
||||
<label class={labelClass('flex items-center gap-2')}>
|
||||
Token Value
|
||||
<Show when={!state.isEditingExistingNode()}>
|
||||
<span class="text-red-500">*</span>
|
||||
</Show>
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
value={state.formData().tokenValue}
|
||||
onInput={(event) => state.updateField('tokenValue', event.currentTarget.value)}
|
||||
placeholder={
|
||||
state.isEditingExistingNode()
|
||||
? 'Leave blank to keep existing'
|
||||
: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'
|
||||
}
|
||||
required={state.formData().authType === 'token' && !state.isEditingExistingNode()}
|
||||
class={controlClass('font-mono')}
|
||||
/>
|
||||
<p class={formHelpText}>The secret value shown when creating the token.</p>
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div class={formField}>
|
||||
<label class={labelClass()}>
|
||||
Token ID <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={state.formData().tokenName}
|
||||
onInput={(event) => state.updateField('tokenName', event.currentTarget.value)}
|
||||
placeholder={getNodeTokenIdPlaceholder(modalProps.nodeType)}
|
||||
required={
|
||||
state.formData().authType === 'token' && state.formData().setupMode === 'manual'
|
||||
}
|
||||
class={controlClass('font-mono')}
|
||||
/>
|
||||
<p class={formHelpText}>Full token ID from Proxmox (user@realm!tokenname).</p>
|
||||
</div>
|
||||
|
||||
<div class={formField}>
|
||||
<label class={labelClass('flex items-center gap-2')}>
|
||||
Token Value
|
||||
<Show when={!state.isEditingExistingNode()}>
|
||||
<span class="text-red-500">*</span>
|
||||
</Show>
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
value={state.formData().tokenValue}
|
||||
onInput={(event) => state.updateField('tokenValue', event.currentTarget.value)}
|
||||
placeholder={
|
||||
state.isEditingExistingNode()
|
||||
? 'Leave blank to keep existing'
|
||||
: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'
|
||||
}
|
||||
required={
|
||||
state.formData().authType === 'token' &&
|
||||
state.formData().setupMode === 'manual' &&
|
||||
!state.isEditingExistingNode()
|
||||
}
|
||||
class={controlClass('font-mono')}
|
||||
/>
|
||||
<p class={formHelpText}>The secret value shown when creating the token.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -17,6 +17,15 @@ interface NodeModalStatusFooterProps {
|
|||
|
||||
export const NodeModalStatusFooter: Component<NodeModalStatusFooterProps> = (props) => {
|
||||
const { modalProps, state } = props;
|
||||
const guidedSetupOnlyMode = () =>
|
||||
!state.isEditingExistingNode() &&
|
||||
modalProps.nodeType !== 'pmg' &&
|
||||
state.formData().authType === 'token' &&
|
||||
state.formData().setupMode !== 'manual';
|
||||
const guidedSetupFooterHint = () =>
|
||||
state.formData().setupMode === 'agent'
|
||||
? 'Run the generated command on the host. Pulse adds the node automatically after the agent starts.'
|
||||
: 'Run the generated command on the host. Pulse adds the API connection automatically after the setup script finishes.';
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
@ -84,7 +93,9 @@ export const NodeModalStatusFooter: Component<NodeModalStatusFooterProps> = (pro
|
|||
<div class="mt-2 space-y-1">
|
||||
<p class="text-xs font-semibold opacity-90">Warnings:</p>
|
||||
<ul class="text-xs space-y-0.5 opacity-80">
|
||||
<For each={state.testResult()?.warnings}>{(warning) => <li>• {warning}</li>}</For>
|
||||
<For each={state.testResult()?.warnings}>
|
||||
{(warning) => <li>• {warning}</li>}
|
||||
</For>
|
||||
</ul>
|
||||
</div>
|
||||
</Show>
|
||||
|
|
@ -152,14 +163,21 @@ export const NodeModalStatusFooter: Component<NodeModalStatusFooterProps> = (pro
|
|||
: 'Delete connection'}
|
||||
</button>
|
||||
</Show>
|
||||
<button
|
||||
type="button"
|
||||
onClick={state.handleTestConnection}
|
||||
disabled={state.isTesting() || props.togglePending || props.deletePending}
|
||||
class="px-4 py-2 text-sm border border-border text-base-content rounded-md hover:bg-surface-hover transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
<Show
|
||||
when={!guidedSetupOnlyMode()}
|
||||
fallback={
|
||||
<p class="max-w-md text-xs leading-5 text-muted">{guidedSetupFooterHint()}</p>
|
||||
}
|
||||
>
|
||||
{state.isTesting() ? 'Testing...' : 'Test Connection'}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={state.handleTestConnection}
|
||||
disabled={state.isTesting() || props.togglePending || props.deletePending}
|
||||
class="px-4 py-2 text-sm border border-border text-base-content rounded-md hover:bg-surface-hover transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{state.isTesting() ? 'Testing...' : 'Test Connection'}
|
||||
</button>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-3">
|
||||
|
|
@ -208,15 +226,17 @@ export const NodeModalStatusFooter: Component<NodeModalStatusFooterProps> = (pro
|
|||
disabled={props.togglePending || props.deletePending}
|
||||
class="px-4 py-2 text-sm border border-border text-base-content rounded-md hover:bg-surface-hover transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={props.togglePending || props.deletePending}
|
||||
class="px-4 py-2 text-sm bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{state.isEditingExistingNode() ? 'Update' : 'Add'} Node
|
||||
{guidedSetupOnlyMode() ? 'Close' : 'Cancel'}
|
||||
</button>
|
||||
<Show when={!guidedSetupOnlyMode()}>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={props.togglePending || props.deletePending}
|
||||
class="px-4 py-2 text-sm bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{state.isEditingExistingNode() ? 'Update' : 'Add'} Node
|
||||
</button>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -287,9 +287,15 @@ describe('InfrastructureWorkspace', () => {
|
|||
renderWorkspace();
|
||||
|
||||
await waitFor(() => expect(screen.getByText('Infrastructure systems')).toBeInTheDocument());
|
||||
expect(screen.getByText('Start by connecting what Pulse should monitor')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(/Pulse 6 treats platform APIs and host agents as infrastructure sources/i),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: /Run discovery/i })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: /Discovery settings/i })).toBeInTheDocument();
|
||||
expect(screen.queryByRole('button', { name: /Detect from address/i })).toBeNull();
|
||||
expect(screen.getByRole('button', { name: /Detect from address/i })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: /Install Pulse Agent/i })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: /Choose source type/i })).toBeInTheDocument();
|
||||
expect(screen.getByText('Proxmox VE')).toBeInTheDocument();
|
||||
expect(screen.getByText('Proxmox VE').closest('tr')?.className).toContain('bg-base');
|
||||
expect(screen.queryByText('VMware vCenter')).toBeNull();
|
||||
|
|
@ -303,6 +309,27 @@ describe('InfrastructureWorkspace', () => {
|
|||
expect(screen.queryByRole('heading', { name: 'Monitored systems' })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('routes first-run actions from the source manager guidance', async () => {
|
||||
renderWorkspace();
|
||||
|
||||
await waitFor(() => expect(screen.getByText('Infrastructure systems')).toBeInTheDocument());
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /Detect from address/i }));
|
||||
expect(navigateSpy).toHaveBeenLastCalledWith('/settings/infrastructure?add=detect', {
|
||||
scroll: false,
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /Install Pulse Agent/i }));
|
||||
expect(navigateSpy).toHaveBeenLastCalledWith('/settings/infrastructure?add=agent', {
|
||||
scroll: false,
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /Choose source type/i }));
|
||||
expect(navigateSpy).toHaveBeenLastCalledWith('/settings/infrastructure?add=pick', {
|
||||
scroll: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('switches the source manager layout from measured container width during live resize', async () => {
|
||||
installResizeObserverMock();
|
||||
Object.defineProperty(window, 'innerWidth', {
|
||||
|
|
@ -327,6 +354,22 @@ describe('InfrastructureWorkspace', () => {
|
|||
expect(screen.getByText('Active')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('keeps onboarding copy visible in the empty infrastructure state', async () => {
|
||||
connectionState.connections = [];
|
||||
connectionState.rows = [];
|
||||
|
||||
renderWorkspace();
|
||||
|
||||
await waitFor(() =>
|
||||
expect(screen.getByText('Start monitoring infrastructure')).toBeInTheDocument(),
|
||||
);
|
||||
expect(
|
||||
screen.getByText('Add infrastructure systems to start monitoring your environment.'),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText(/Available system types: VMware vCenter/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/standalone hosts through Pulse Agent/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('routes discovery actions from the manager and shows discovered candidates in the matching platform group', async () => {
|
||||
const triggerDiscoveryScan = vi.fn();
|
||||
renderWorkspace({
|
||||
|
|
|
|||
|
|
@ -15,6 +15,9 @@ import connectionEditorSource from '../ConnectionEditor/ConnectionEditor.tsx?raw
|
|||
import addressProbeStepSource from '../ConnectionEditor/AddressProbeStep.tsx?raw';
|
||||
import connectionEditorStateSource from '../ConnectionEditor/useConnectionEditor.ts?raw';
|
||||
import nodeCredentialSlotSource from '../ConnectionEditor/CredentialSlots/NodeCredentialSlot.tsx?raw';
|
||||
import nodeModalAuthenticationSectionSource from '../NodeModalAuthenticationSection.tsx?raw';
|
||||
import nodeModalStatusFooterSource from '../NodeModalStatusFooter.tsx?raw';
|
||||
import nodeModalStateSource from '../useNodeModalState.ts?raw';
|
||||
import trueNASCredentialSlotSource from '../ConnectionEditor/CredentialSlots/TrueNASCredentialSlot.tsx?raw';
|
||||
import vmwareCredentialSlotSource from '../ConnectionEditor/CredentialSlots/VMwareCredentialSlot.tsx?raw';
|
||||
import diagnosticsResultsPanelSource from '../DiagnosticsResultsPanel.tsx?raw';
|
||||
|
|
@ -168,7 +171,13 @@ describe('settings architecture guardrails', () => {
|
|||
expect(infrastructureSourceManagerSource).toContain('aria-label={product.actionLabel}');
|
||||
expect(infrastructureSourceManagerSource).toContain('Review');
|
||||
expect(infrastructureSourceManagerSource).toContain('Edit');
|
||||
expect(infrastructureSourceManagerSource).not.toContain('Detect from address');
|
||||
expect(infrastructureSourceManagerSource).toContain(
|
||||
'Start by connecting what Pulse should monitor',
|
||||
);
|
||||
expect(infrastructureSourceManagerSource).toContain('Detect from address');
|
||||
expect(infrastructureSourceManagerSource).toContain('Install Pulse Agent');
|
||||
expect(infrastructureSourceManagerSource).toContain('Choose source type');
|
||||
expect(infrastructureSourceManagerSource).toContain('getInfrastructureEmptyStateSummary');
|
||||
expect(infrastructureSourceManagerSource).not.toContain('Connection types');
|
||||
expect(infrastructureSourcePickerSource).toContain('Detect from address');
|
||||
expect(infrastructureSourcePickerSource).toContain('getInfrastructureSourcePickerGroups');
|
||||
|
|
@ -230,6 +239,11 @@ describe('settings architecture guardrails', () => {
|
|||
expect(nodeCredentialSlotSource).toContain('<NodeModalMonitoringSection');
|
||||
expect(nodeCredentialSlotSource).toContain('<NodeModalStatusFooter');
|
||||
expect(nodeCredentialSlotSource).not.toContain('<Dialog');
|
||||
expect(nodeModalAuthenticationSectionSource).toContain(
|
||||
"state.formData().setupMode === 'manual'",
|
||||
);
|
||||
expect(nodeModalStatusFooterSource).toContain('guidedSetupOnlyMode');
|
||||
expect(nodeModalStateSource).toContain('data.setupMode !==');
|
||||
|
||||
expect(vmwareCredentialSlotSource).toContain('TlsVerificationWarningBanner');
|
||||
expect(vmwareCredentialSlotSource).toContain('subject="this vCenter connection"');
|
||||
|
|
|
|||
|
|
@ -96,10 +96,7 @@ export const useNodeModalState = (props: NodeModalProps) => {
|
|||
}
|
||||
};
|
||||
|
||||
const copyProxmoxAgentInstallCommand = async (
|
||||
type: 'pve' | 'pbs',
|
||||
successMessage: string,
|
||||
) => {
|
||||
const copyProxmoxAgentInstallCommand = async (type: 'pve' | 'pbs', successMessage: string) => {
|
||||
try {
|
||||
setLoadingAgentCommand(true);
|
||||
setAgentCommandError(null);
|
||||
|
|
@ -356,6 +353,20 @@ export const useNodeModalState = (props: NodeModalProps) => {
|
|||
event.preventDefault();
|
||||
const data = formData();
|
||||
|
||||
if (
|
||||
!isEditingExistingNode() &&
|
||||
props.nodeType !== 'pmg' &&
|
||||
data.authType === 'token' &&
|
||||
data.setupMode !== 'manual'
|
||||
) {
|
||||
const setupPath =
|
||||
data.setupMode === 'agent' ? 'Pulse Agent install command' : 'setup command';
|
||||
notificationStore.info(
|
||||
`Run the ${setupPath} on the host. Pulse will add the node automatically.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const normalizedName = data.name.trim() || deriveNameFromHost(data.host);
|
||||
if (!normalizedName) {
|
||||
notificationStore.error('Node name is required');
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue