mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-19 10:33:29 +00:00
phase 5: wire ConnectionEditor into the add drawer
The Add infrastructure drawer now opens ConnectionEditor instead of the legacy type picker + per-type step branches. Paste an address, Pulse probes, detected type dispatches into a credential slot. The slot still mounts the existing per-type panels (ProxmoxSettingsPanel, TrueNASSettingsPanel, VMwareSettingsPanel, InfrastructureInstallerSection) so mature credential flows keep working end-to-end; slot components are a later phase. - InfrastructureWorkspace: panelStep state machine replaced with addDrawerOpen + initialAddType. Legacy deep links still redirect and pre-select the matching type. Agent setup handoff still auto-opens into the agent slot. Read-only mode still suppresses the drawer. - AddSystemPicker + its test deleted (dead code, the picker UI is gone). - InfrastructureWorkspace.test.tsx rewritten for the probe-first flow (probe button, manual-type escape hatch, back-to-probe, per-type credential slot routing, legacy redirects, read-only). - settingsArchitecture guardrail swapped from panelStep string assertions to ConnectionEditor + renderCredentialSlot assertions. - agent-lifecycle + frontend-primitives contracts extended to require the unified add surface run through ConnectionEditor.
This commit is contained in:
parent
3608bf34ff
commit
e4663561dd
7 changed files with 172 additions and 423 deletions
|
|
@ -313,6 +313,16 @@ an add-only capacity posture.
|
|||
endpoints that own admission checks, and the `Disabled` flag on
|
||||
PVE/PBS/PMG surfaced by that ledger must remain a pause-only signal
|
||||
rather than an installer pre-flight gate.
|
||||
The unified add surface is
|
||||
`frontend-modern/src/components/Settings/ConnectionEditor/ConnectionEditor.tsx`,
|
||||
mounted by `InfrastructureWorkspace.tsx` in place of the legacy
|
||||
type-picker step tree. The editor's probe step calls the aggregator
|
||||
probe endpoint and dispatches the detected or manually-selected type
|
||||
into a credential slot that renders the existing per-type panel; it
|
||||
must not bypass the probe endpoint or fabricate probe candidates, and
|
||||
the agent credential slot must continue to reach
|
||||
`InfrastructureInstallerSection.tsx` so install handoffs remain on the
|
||||
canonical unified-agent install path.
|
||||
|
||||
## Forbidden Paths
|
||||
|
||||
|
|
|
|||
|
|
@ -230,6 +230,12 @@ work extends shared components instead of creating new local variants.
|
|||
default ledger plus route-backed `Connections` and `Install` workspaces
|
||||
inside that destination, while each secondary flow still stays
|
||||
single-purpose instead of stacking multiple workspace surfaces at once.
|
||||
`InfrastructureWorkspace.tsx` must open a new connection through
|
||||
`frontend-modern/src/components/Settings/ConnectionEditor/ConnectionEditor.tsx`,
|
||||
not through a per-type picker or per-type step tree. The editor's probe
|
||||
step is the canonical entry point; credential slots are dispatched by the
|
||||
detected or manually-selected type and must reach the existing per-type
|
||||
settings panel rather than diverging into a parallel add surface.
|
||||
6. Keep Proxmox deep-link route selection on the shared settings-navigation boundary. `frontend-modern/src/components/Settings/settingsNavigationModel.ts` and `frontend-modern/src/components/Settings/useSettingsNavigation.ts` must treat the canonical PBS and PMG Proxmox deep links as agent-selection authority even though those URLs resolve to the shared `infrastructure-operations` tab. Reloading or remounting on a PBS or PMG deep link must not silently fall back to the PVE selector state.
|
||||
7. Keep shared storage feature presenters on canonical platform truth. When reusable storage presenters under `frontend-modern/src/features/storageBackups/` classify canonical resources for the shared storage route, API-backed virtualization datastores such as VMware must stay inventory-only datastores instead of inheriting PBS-specific backup-repository or protected-target copy from older fallback branches.
|
||||
8. Keep shared source/platform vocabulary on the governed manifest boundary. `frontend-modern/src/utils/platformSupportManifest.generated.ts` must be the tracked frontend projection of `docs/release-control/v6/internal/PLATFORM_SUPPORT_MANIFEST.json`, `frontend-modern/src/utils/platformSupportManifest.ts`, `frontend-modern/src/utils/sourcePlatforms.ts`, and `frontend-modern/src/utils/sourcePlatformOptions.ts` must consume that generated projection instead of embedding divergent future-label lists, setup/onboarding path allowlists, or presentation-only guesses, and `frontend-modern/scripts/canonical-platform-audit.mjs` must fail when the generated projection drifts from the governed manifest.
|
||||
|
|
|
|||
|
|
@ -1,142 +0,0 @@
|
|||
import { Component, For } from 'solid-js';
|
||||
import { Dialog } from '@/components/shared/Dialog';
|
||||
|
||||
export type AddSystemChoiceKind = 'pve' | 'pbs' | 'pmg' | 'truenas' | 'vmware' | 'agent';
|
||||
export type AddSystemChoiceMethod = 'api' | 'agent';
|
||||
|
||||
export interface AddSystemChoice {
|
||||
kind: AddSystemChoiceKind;
|
||||
title: string;
|
||||
description: string;
|
||||
method: AddSystemChoiceMethod;
|
||||
methodLabel: string;
|
||||
}
|
||||
|
||||
export const ADD_SYSTEM_CHOICES: readonly AddSystemChoice[] = [
|
||||
{
|
||||
kind: 'agent',
|
||||
title: 'Install on a host',
|
||||
description:
|
||||
'Generate an install token and run the Pulse agent on the host itself. Best for the first monitored system or anything without a supported API.',
|
||||
method: 'agent',
|
||||
methodLabel: 'Agent install',
|
||||
},
|
||||
{
|
||||
kind: 'pve',
|
||||
title: 'Proxmox VE',
|
||||
description: 'Connect a Proxmox cluster or standalone node. Pulse polls the Proxmox API.',
|
||||
method: 'api',
|
||||
methodLabel: 'API connection',
|
||||
},
|
||||
{
|
||||
kind: 'pbs',
|
||||
title: 'Proxmox Backup Server',
|
||||
description: 'Connect a PBS instance so Pulse can track datastores, sync, and verify jobs.',
|
||||
method: 'api',
|
||||
methodLabel: 'API connection',
|
||||
},
|
||||
{
|
||||
kind: 'pmg',
|
||||
title: 'Proxmox Mail Gateway',
|
||||
description: 'Connect a PMG instance for queue, quarantine, and mail-stats reporting.',
|
||||
method: 'api',
|
||||
methodLabel: 'API connection',
|
||||
},
|
||||
{
|
||||
kind: 'truenas',
|
||||
title: 'TrueNAS SCALE',
|
||||
description: 'Connect a TrueNAS system so Pulse can report pools, datasets, and apps.',
|
||||
method: 'api',
|
||||
methodLabel: 'API connection',
|
||||
},
|
||||
{
|
||||
kind: 'vmware',
|
||||
title: 'VMware vSphere or ESXi',
|
||||
description: 'Connect a vCenter or ESXi host. Pulse polls the vSphere API.',
|
||||
method: 'api',
|
||||
methodLabel: 'API connection',
|
||||
},
|
||||
];
|
||||
|
||||
interface AddSystemPickerProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onSelect: (choice: AddSystemChoice) => void;
|
||||
onManageProfiles?: () => void;
|
||||
}
|
||||
|
||||
export const AddSystemPicker: Component<AddSystemPickerProps> = (props) => {
|
||||
return (
|
||||
<Dialog
|
||||
isOpen={props.isOpen}
|
||||
onClose={props.onClose}
|
||||
ariaLabelledBy="add-system-picker-title"
|
||||
closeOnBackdrop
|
||||
panelClass="w-full max-w-2xl rounded-lg bg-surface shadow-xl"
|
||||
>
|
||||
<div class="space-y-4 p-6">
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<h2 id="add-system-picker-title" class="text-lg font-semibold text-base-content">
|
||||
Add infrastructure
|
||||
</h2>
|
||||
<p class="mt-1 text-sm text-muted">
|
||||
Choose the system or platform you want Pulse to start monitoring.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={props.onClose}
|
||||
class="rounded-md border border-border bg-surface px-2 py-1 text-xs text-muted hover:bg-surface-hover"
|
||||
aria-label="Close"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
<ul class="divide-y divide-border rounded-md border border-border">
|
||||
<For each={ADD_SYSTEM_CHOICES}>
|
||||
{(choice) => (
|
||||
<li>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => props.onSelect(choice)}
|
||||
class="flex w-full items-start justify-between gap-4 px-4 py-3 text-left hover:bg-surface-hover"
|
||||
>
|
||||
<div>
|
||||
<div class="text-sm font-semibold text-base-content">{choice.title}</div>
|
||||
<div class="mt-0.5 text-xs text-muted">{choice.description}</div>
|
||||
</div>
|
||||
<span
|
||||
class={`inline-flex shrink-0 items-center rounded-full px-2 py-0.5 text-xs font-medium ${
|
||||
choice.method === 'api'
|
||||
? 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-100'
|
||||
: 'bg-emerald-100 text-emerald-800 dark:bg-emerald-900 dark:text-emerald-100'
|
||||
}`}
|
||||
>
|
||||
{choice.methodLabel}
|
||||
</span>
|
||||
</button>
|
||||
</li>
|
||||
)}
|
||||
</For>
|
||||
</ul>
|
||||
<For each={props.onManageProfiles ? [props.onManageProfiles] : []}>
|
||||
{(onManageProfiles) => (
|
||||
<div class="flex items-center justify-between gap-3 rounded-md border border-border bg-surface-alt px-4 py-3">
|
||||
<div class="text-xs text-muted">
|
||||
Agent profiles change install defaults for agent-based systems.
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onManageProfiles}
|
||||
class="inline-flex items-center rounded-md border border-border px-3 py-1.5 text-sm font-medium text-base-content transition-colors hover:bg-surface-hover"
|
||||
>
|
||||
Manage agent profiles
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,15 +1,16 @@
|
|||
import { Component, For, Show, createEffect, createMemo, createSignal } from 'solid-js';
|
||||
import { Component, Show, createEffect, createMemo, createSignal } from 'solid-js';
|
||||
import { useLocation, useNavigate } from '@solidjs/router';
|
||||
import { Dialog } from '@/components/shared/Dialog';
|
||||
import { presentationPolicyIsReadOnly } from '@/stores/sessionPresentationPolicy';
|
||||
import { AgentProfilesPanel } from './AgentProfilesPanel';
|
||||
import { ADD_SYSTEM_CHOICES, type AddSystemChoice } from './AddSystemPicker';
|
||||
import { ConnectionsTable, type ConnectionsTableHeaderAction } from './ConnectionsTable';
|
||||
import {
|
||||
buildInfrastructureSystemRows,
|
||||
type InfrastructureSystemRow,
|
||||
type SystemManageAction,
|
||||
} from './connectionsTableModel';
|
||||
import { ConnectionEditor } from './ConnectionEditor/ConnectionEditor';
|
||||
import type { ConnectionType } from '@/api/connections';
|
||||
import { InfrastructureActiveRowDetails } from './InfrastructureActiveRowDetails';
|
||||
import { InfrastructureInstallerSection } from './InfrastructureInstallerSection';
|
||||
import { InfrastructureIgnoredRowDetails } from './InfrastructureIgnoredRowDetails';
|
||||
|
|
@ -21,7 +22,6 @@ import {
|
|||
buildInfrastructureWorkspacePath,
|
||||
deriveAddStepFromLegacyPath,
|
||||
type InfrastructureAddStep,
|
||||
type InfrastructurePanelStep,
|
||||
} from './infrastructureWorkspaceModel';
|
||||
import type { InfrastructurePlatformSettingsProps } from './proxmoxSettingsModel';
|
||||
import type { NodeType } from './infrastructureSettingsModel';
|
||||
|
|
@ -32,13 +32,13 @@ import {
|
|||
|
||||
export type InfrastructureWorkspaceProps = InfrastructurePlatformSettingsProps;
|
||||
|
||||
const STEP_TITLES: Record<InfrastructureAddStep, string> = {
|
||||
agent: 'Install on a host',
|
||||
pve: 'Proxmox VE',
|
||||
pbs: 'Proxmox Backup Server',
|
||||
pmg: 'Proxmox Mail Gateway',
|
||||
truenas: 'TrueNAS SCALE',
|
||||
vmware: 'VMware vSphere / ESXi',
|
||||
const ADD_STEP_TO_TYPE: Record<InfrastructureAddStep, ConnectionType> = {
|
||||
agent: 'agent',
|
||||
pve: 'pve',
|
||||
pbs: 'pbs',
|
||||
pmg: 'pmg',
|
||||
truenas: 'truenas',
|
||||
vmware: 'vmware',
|
||||
};
|
||||
|
||||
const InfrastructureWorkspaceContent: Component<InfrastructureWorkspaceProps> = (props) => {
|
||||
|
|
@ -46,33 +46,40 @@ const InfrastructureWorkspaceContent: Component<InfrastructureWorkspaceProps> =
|
|||
const location = useLocation();
|
||||
const state = useInfrastructureOperationsContext();
|
||||
|
||||
const [panelStep, setPanelStep] = createSignal<InfrastructurePanelStep | null>(null);
|
||||
const [addDrawerOpen, setAddDrawerOpen] = createSignal(false);
|
||||
const [initialAddType, setInitialAddType] = createSignal<ConnectionType | null>(null);
|
||||
const [showAgentProfiles, setShowAgentProfiles] = createSignal(false);
|
||||
const readOnly = createMemo(() => presentationPolicyIsReadOnly());
|
||||
|
||||
// Redirect legacy deep links and open the appropriate panel.
|
||||
// Redirect legacy deep links and pre-select the matching type in the editor.
|
||||
createEffect(() => {
|
||||
const path = location.pathname;
|
||||
if (path === '/settings/infrastructure') return;
|
||||
|
||||
const step = deriveAddStepFromLegacyPath(path);
|
||||
navigate(buildInfrastructureWorkspacePath(), { replace: true });
|
||||
if (step && !readOnly()) {
|
||||
setPanelStep(step);
|
||||
if (!readOnly()) {
|
||||
setAddDrawerOpen(true);
|
||||
if (step && step !== 'pick') {
|
||||
setInitialAddType(ADD_STEP_TO_TYPE[step]);
|
||||
} else {
|
||||
setInitialAddType(null);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Auto-open the agent installer when a setup handoff is waiting.
|
||||
createEffect(() => {
|
||||
if (state.setupHandoff?.() && panelStep() === null && !readOnly()) {
|
||||
setPanelStep('agent');
|
||||
if (state.setupHandoff?.() && !addDrawerOpen() && !readOnly()) {
|
||||
setAddDrawerOpen(true);
|
||||
setInitialAddType('agent');
|
||||
}
|
||||
});
|
||||
|
||||
// Close add panel in read-only mode.
|
||||
createEffect(() => {
|
||||
if (readOnly() && panelStep() !== null) {
|
||||
setPanelStep(null);
|
||||
if (readOnly() && addDrawerOpen()) {
|
||||
setAddDrawerOpen(false);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -90,7 +97,8 @@ const InfrastructureWorkspaceContent: Component<InfrastructureWorkspaceProps> =
|
|||
{
|
||||
label: 'Add infrastructure',
|
||||
onSelect: () => {
|
||||
setPanelStep('pick');
|
||||
setInitialAddType(null);
|
||||
setAddDrawerOpen(true);
|
||||
setShowAgentProfiles(false);
|
||||
},
|
||||
tone: 'primary' as const,
|
||||
|
|
@ -113,42 +121,22 @@ const InfrastructureWorkspaceContent: Component<InfrastructureWorkspaceProps> =
|
|||
}
|
||||
};
|
||||
|
||||
const handleAddSystem = (choice: AddSystemChoice) => {
|
||||
setShowAgentProfiles(false);
|
||||
setPanelStep(choice.kind as InfrastructureAddStep);
|
||||
};
|
||||
|
||||
const closePanel = () => {
|
||||
setPanelStep(null);
|
||||
const closeAddDrawer = () => {
|
||||
setAddDrawerOpen(false);
|
||||
setInitialAddType(null);
|
||||
setShowAgentProfiles(false);
|
||||
};
|
||||
|
||||
const goBackToPick = () => {
|
||||
setShowAgentProfiles(false);
|
||||
setPanelStep('pick');
|
||||
};
|
||||
|
||||
// For Proxmox: selectedAgent is derived from the active panel step.
|
||||
const proxmoxAgent = createMemo<NodeType>(() => {
|
||||
const step = panelStep();
|
||||
if (step === 'pbs') return 'pbs';
|
||||
if (step === 'pmg') return 'pmg';
|
||||
return 'pve';
|
||||
});
|
||||
|
||||
const onSelectProxmoxAgent = (agent: NodeType) => {
|
||||
setPanelStep(agent as InfrastructureAddStep);
|
||||
};
|
||||
|
||||
const isProxmoxStep = () => {
|
||||
const step = panelStep();
|
||||
return step === 'pve' || step === 'pbs' || step === 'pmg';
|
||||
};
|
||||
|
||||
const stepTitle = () => {
|
||||
const step = panelStep();
|
||||
if (!step || step === 'pick') return 'Add infrastructure';
|
||||
return STEP_TITLES[step];
|
||||
const renderProxmoxSlot = (type: NodeType) => {
|
||||
const selectedAgent = () => type;
|
||||
return (
|
||||
<ProxmoxSettingsPanel
|
||||
{...props}
|
||||
selectedAgent={selectedAgent}
|
||||
onSelectAgent={() => {}}
|
||||
embedded
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
@ -159,139 +147,87 @@ const InfrastructureWorkspaceContent: Component<InfrastructureWorkspaceProps> =
|
|||
onManageRow={(row) => handleManageAction(row.manage)}
|
||||
/>
|
||||
|
||||
{/* Add-infrastructure panel — right drawer */}
|
||||
{/* Unified add-connection drawer */}
|
||||
<Dialog
|
||||
isOpen={panelStep() !== null}
|
||||
onClose={closePanel}
|
||||
isOpen={addDrawerOpen()}
|
||||
onClose={closeAddDrawer}
|
||||
layout="drawer-right"
|
||||
panelClass="max-w-[820px]"
|
||||
ariaLabel={stepTitle()}
|
||||
ariaLabel="Add connection"
|
||||
>
|
||||
<div class="flex h-full flex-col overflow-hidden">
|
||||
{/* Drawer header */}
|
||||
<div class="flex shrink-0 items-center justify-between gap-3 border-b border-border px-4 py-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<Show when={panelStep() !== 'pick'}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={goBackToPick}
|
||||
class="inline-flex items-center gap-1 rounded-md border border-border px-2.5 py-1 text-xs font-medium text-base-content transition-colors hover:bg-surface-hover"
|
||||
>
|
||||
← Choose different type
|
||||
</button>
|
||||
</Show>
|
||||
<span class="text-sm font-semibold text-base-content">{stepTitle()}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<Show when={panelStep() === 'agent'}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowAgentProfiles((v) => !v)}
|
||||
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"
|
||||
>
|
||||
{showAgentProfiles() ? 'Hide agent profiles' : 'Manage agent profiles'}
|
||||
</button>
|
||||
</Show>
|
||||
<button
|
||||
type="button"
|
||||
onClick={closePanel}
|
||||
class="rounded-md p-1 text-muted hover:bg-surface-hover hover:text-base-content"
|
||||
aria-label="Close"
|
||||
>
|
||||
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<span class="text-sm font-semibold text-base-content">Add connection</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={closeAddDrawer}
|
||||
class="rounded-md p-1 text-muted hover:bg-surface-hover hover:text-base-content"
|
||||
aria-label="Close"
|
||||
>
|
||||
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Drawer content */}
|
||||
<div class="flex-1 overflow-y-auto">
|
||||
{/* Step 1: picker */}
|
||||
<Show when={panelStep() === 'pick'}>
|
||||
<div class="space-y-4 p-4">
|
||||
<p class="text-sm text-muted">
|
||||
Choose the system or platform you want Pulse to start monitoring.
|
||||
</p>
|
||||
<ul class="divide-y divide-border rounded-md border border-border">
|
||||
<For each={ADD_SYSTEM_CHOICES}>
|
||||
{(choice) => (
|
||||
<li>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleAddSystem(choice)}
|
||||
class="flex w-full items-start justify-between gap-4 px-4 py-3 text-left hover:bg-surface-hover"
|
||||
>
|
||||
<div>
|
||||
<div class="text-sm font-semibold text-base-content">
|
||||
{choice.title}
|
||||
</div>
|
||||
<div class="mt-0.5 text-xs text-muted">{choice.description}</div>
|
||||
<Show when={addDrawerOpen()}>
|
||||
<ConnectionEditor
|
||||
mode="add"
|
||||
initialType={initialAddType() ?? undefined}
|
||||
onClose={closeAddDrawer}
|
||||
renderCredentialSlot={({ type }) => {
|
||||
switch (type) {
|
||||
case 'pve':
|
||||
case 'pbs':
|
||||
case 'pmg':
|
||||
return renderProxmoxSlot(type);
|
||||
case 'truenas':
|
||||
return <TrueNASSettingsPanel state={props.trueNASSettings} />;
|
||||
case 'vmware':
|
||||
return <VMwareSettingsPanel state={props.vmwareSettings} />;
|
||||
case 'agent':
|
||||
return (
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center justify-end">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowAgentProfiles((v) => !v)}
|
||||
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"
|
||||
>
|
||||
{showAgentProfiles() ? 'Hide agent profiles' : 'Manage agent profiles'}
|
||||
</button>
|
||||
</div>
|
||||
<span
|
||||
class={`inline-flex shrink-0 items-center rounded-full px-2 py-0.5 text-xs font-medium ${
|
||||
choice.method === 'api'
|
||||
? 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-100'
|
||||
: 'bg-emerald-100 text-emerald-800 dark:bg-emerald-900 dark:text-emerald-100'
|
||||
}`}
|
||||
>
|
||||
{choice.methodLabel}
|
||||
</span>
|
||||
</button>
|
||||
</li>
|
||||
)}
|
||||
</For>
|
||||
</ul>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
{/* Step 2a: agent installer */}
|
||||
<Show when={panelStep() === 'agent'}>
|
||||
<div class="space-y-4 p-4">
|
||||
<Show when={showAgentProfiles()}>
|
||||
<div class="rounded-xl border border-border bg-surface p-4 shadow-sm">
|
||||
<div class="mb-4 space-y-1">
|
||||
<div class="text-base font-semibold text-base-content">Agent profiles</div>
|
||||
<div class="text-sm text-muted">
|
||||
Manage reusable install defaults for agent-based systems.
|
||||
</div>
|
||||
</div>
|
||||
<AgentProfilesPanel />
|
||||
</div>
|
||||
</Show>
|
||||
<InfrastructureInstallerSection />
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
{/* Step 2b: Proxmox (PVE / PBS / PMG) */}
|
||||
<Show when={isProxmoxStep()}>
|
||||
<div class="p-4">
|
||||
<ProxmoxSettingsPanel
|
||||
{...props}
|
||||
selectedAgent={proxmoxAgent}
|
||||
onSelectAgent={onSelectProxmoxAgent}
|
||||
embedded
|
||||
/>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
{/* Step 2c: TrueNAS */}
|
||||
<Show when={panelStep() === 'truenas'}>
|
||||
<div class="p-4">
|
||||
<TrueNASSettingsPanel state={props.trueNASSettings} />
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
{/* Step 2d: VMware */}
|
||||
<Show when={panelStep() === 'vmware'}>
|
||||
<div class="p-4">
|
||||
<VMwareSettingsPanel state={props.vmwareSettings} />
|
||||
</div>
|
||||
<Show when={showAgentProfiles()}>
|
||||
<div class="rounded-xl border border-border bg-surface p-4 shadow-sm">
|
||||
<div class="mb-4 space-y-1">
|
||||
<div class="text-base font-semibold text-base-content">
|
||||
Agent profiles
|
||||
</div>
|
||||
<div class="text-sm text-muted">
|
||||
Manage reusable install defaults for agent-based systems.
|
||||
</div>
|
||||
</div>
|
||||
<AgentProfilesPanel />
|
||||
</div>
|
||||
</Show>
|
||||
<InfrastructureInstallerSection />
|
||||
</div>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<div class="text-sm text-muted">
|
||||
No credential form is wired up for the {type} type yet.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,76 +0,0 @@
|
|||
import { cleanup, fireEvent, render, screen } from '@solidjs/testing-library';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import { ADD_SYSTEM_CHOICES, AddSystemPicker } from '../AddSystemPicker';
|
||||
|
||||
describe('AddSystemPicker', () => {
|
||||
afterEach(() => cleanup());
|
||||
|
||||
it('does not render any dialog content when closed', () => {
|
||||
render(() => (
|
||||
<AddSystemPicker isOpen={false} onClose={() => {}} onSelect={() => {}} />
|
||||
) as any);
|
||||
expect(screen.queryByRole('dialog')).toBeNull();
|
||||
});
|
||||
|
||||
it('lists one button per add-system choice including Proxmox VE, PBS, PMG, TrueNAS, VMware, and agent host', () => {
|
||||
render(() => (
|
||||
<AddSystemPicker isOpen={true} onClose={() => {}} onSelect={() => {}} />
|
||||
) as any);
|
||||
|
||||
for (const choice of ADD_SYSTEM_CHOICES) {
|
||||
expect(screen.getByText(choice.title)).toBeInTheDocument();
|
||||
}
|
||||
expect(ADD_SYSTEM_CHOICES.map((c) => c.kind)).toEqual([
|
||||
'agent',
|
||||
'pve',
|
||||
'pbs',
|
||||
'pmg',
|
||||
'truenas',
|
||||
'vmware',
|
||||
]);
|
||||
});
|
||||
|
||||
it('invokes onSelect with the matching choice when a tile is clicked', () => {
|
||||
const onSelect = vi.fn();
|
||||
render(() => (
|
||||
<AddSystemPicker isOpen={true} onClose={() => {}} onSelect={onSelect} />
|
||||
) as any);
|
||||
|
||||
fireEvent.click(screen.getByText('TrueNAS SCALE'));
|
||||
expect(onSelect).toHaveBeenCalledTimes(1);
|
||||
expect(onSelect.mock.calls[0][0].kind).toBe('truenas');
|
||||
expect(onSelect.mock.calls[0][0].method).toBe('api');
|
||||
});
|
||||
|
||||
it('invokes onClose when the close button is clicked', () => {
|
||||
const onClose = vi.fn();
|
||||
render(() => (
|
||||
<AddSystemPicker isOpen={true} onClose={onClose} onSelect={() => {}} />
|
||||
) as any);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Close' }));
|
||||
expect(onClose).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('offers agent profile management as a secondary action when provided', () => {
|
||||
const onManageProfiles = vi.fn();
|
||||
render(() => (
|
||||
<AddSystemPicker
|
||||
isOpen={true}
|
||||
onClose={() => {}}
|
||||
onSelect={() => {}}
|
||||
onManageProfiles={onManageProfiles}
|
||||
/>
|
||||
) as any);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Manage agent profiles' }));
|
||||
expect(onManageProfiles).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('marks the agent-host choice as an agent install and everything else as an API connection', () => {
|
||||
const agent = ADD_SYSTEM_CHOICES.find((c) => c.kind === 'agent');
|
||||
const apis = ADD_SYSTEM_CHOICES.filter((c) => c.kind !== 'agent');
|
||||
expect(agent?.method).toBe('agent');
|
||||
expect(apis.every((c) => c.method === 'api')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { cleanup, fireEvent, render, screen } from '@solidjs/testing-library';
|
||||
import { cleanup, fireEvent, render, screen, waitFor } from '@solidjs/testing-library';
|
||||
import { createSignal } from 'solid-js';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import type { UnifiedAgentRow } from '../infrastructureOperationsModel';
|
||||
|
|
@ -82,6 +82,17 @@ vi.mock('../AgentProfilesPanel', () => ({
|
|||
AgentProfilesPanel: () => <div data-testid="agent-profiles">profiles</div>,
|
||||
}));
|
||||
|
||||
vi.mock('@/api/connections', async () => {
|
||||
const actual = await vi.importActual<typeof import('@/api/connections')>('@/api/connections');
|
||||
return {
|
||||
...actual,
|
||||
ConnectionsAPI: {
|
||||
list: vi.fn(),
|
||||
probe: vi.fn().mockResolvedValue({ candidates: [], probedMs: 0 }),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const reportingRow = (overrides: Partial<UnifiedAgentRow> = {}): UnifiedAgentRow =>
|
||||
({
|
||||
rowKey: 'agent:tower',
|
||||
|
|
@ -187,82 +198,85 @@ describe('InfrastructureWorkspace', () => {
|
|||
expect(screen.queryByText('lab-vcenter')).toBeNull();
|
||||
});
|
||||
|
||||
it('opens the type picker in the add panel when Add infrastructure is clicked', () => {
|
||||
it('opens the unified add drawer with the probe step when Add infrastructure is clicked', () => {
|
||||
renderWorkspace();
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /Add infrastructure/i }));
|
||||
|
||||
expect(screen.getByText('Choose the system or platform you want Pulse to start monitoring.')).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: /Install on a host/i })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: /Probe address/i })).toBeInTheDocument();
|
||||
expect(screen.getByPlaceholderText(/pve01\.lan/)).toBeInTheDocument();
|
||||
expect(navigateSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('shows the agent installer when Install on a host is chosen', () => {
|
||||
it('routes to the agent credential slot when the user picks agent manually', () => {
|
||||
renderWorkspace();
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /Add infrastructure/i }));
|
||||
fireEvent.click(screen.getByRole('button', { name: /Install on a host/i }));
|
||||
fireEvent.click(screen.getByRole('button', { name: /Enter credentials manually/i }));
|
||||
fireEvent.click(screen.getByRole('button', { name: /Agent \(install on host\)/i }));
|
||||
|
||||
expect(screen.getByTestId('install-section')).toBeInTheDocument();
|
||||
expect(screen.queryByText('What do you want to connect?')).toBeNull();
|
||||
});
|
||||
|
||||
it('shows the TrueNAS panel when TrueNAS SCALE is chosen', () => {
|
||||
it('routes to the TrueNAS credential slot when TrueNAS is picked manually', () => {
|
||||
renderWorkspace();
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /Add infrastructure/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();
|
||||
expect(navigateSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('shows the VMware panel when VMware vSphere is chosen', () => {
|
||||
it('routes to the VMware credential slot when VMware is picked manually', () => {
|
||||
renderWorkspace();
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /Add infrastructure/i }));
|
||||
fireEvent.click(screen.getByRole('button', { name: /VMware vSphere or ESXi/i }));
|
||||
fireEvent.click(screen.getByRole('button', { name: /Enter credentials manually/i }));
|
||||
fireEvent.click(screen.getByRole('button', { name: /VMware vCenter/i }));
|
||||
|
||||
expect(screen.getByTestId('vmware-section')).toBeInTheDocument();
|
||||
expect(navigateSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('shows the Proxmox panel when Proxmox VE is chosen', () => {
|
||||
it('routes to the Proxmox credential slot when Proxmox VE is picked manually', () => {
|
||||
renderWorkspace();
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /Add infrastructure/i }));
|
||||
fireEvent.click(screen.getByRole('button', { name: /Proxmox VE/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();
|
||||
expect(navigateSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('returns to the picker via back button from a step-2 form', () => {
|
||||
it('can return to the probe step from a credential slot via Back to probe', () => {
|
||||
renderWorkspace();
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /Add infrastructure/i }));
|
||||
fireEvent.click(screen.getByRole('button', { name: /Install on a host/i }));
|
||||
fireEvent.click(screen.getByRole('button', { name: /Choose different type/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: /Back to probe/i }));
|
||||
|
||||
expect(screen.getByText('Choose the system or platform you want Pulse to start monitoring.')).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: /Probe address/i })).toBeInTheDocument();
|
||||
expect(screen.queryByTestId('install-section')).toBeNull();
|
||||
});
|
||||
|
||||
it('toggles agent profiles in the agent install drawer', () => {
|
||||
it('toggles agent profiles inside the agent credential slot', () => {
|
||||
renderWorkspace();
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /Add infrastructure/i }));
|
||||
fireEvent.click(screen.getByRole('button', { name: /Install on a host/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: 'Manage agent profiles' }));
|
||||
|
||||
expect(screen.getByTestId('agent-profiles')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not use modals or child-node setup, only sets panel state on no-modal choices', () => {
|
||||
it('does not mount node-modal legacy spies when a credential slot is rendered', () => {
|
||||
renderWorkspace();
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /Add infrastructure/i }));
|
||||
fireEvent.click(screen.getByRole('button', { name: /Proxmox VE/i }));
|
||||
fireEvent.click(screen.getByRole('button', { name: /Enter credentials manually/i }));
|
||||
fireEvent.click(screen.getByRole('button', { name: /^Proxmox VE/i }));
|
||||
|
||||
expect(setShowNodeModalSpy).not.toHaveBeenCalled();
|
||||
expect(setEditingNodeSpy).not.toHaveBeenCalled();
|
||||
|
|
@ -279,28 +293,29 @@ describe('InfrastructureWorkspace', () => {
|
|||
expect(screen.getByTestId('active-details')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('redirects legacy install deep link to the base route and opens the agent panel', () => {
|
||||
it('redirects legacy install deep link and pre-selects the agent credential slot', async () => {
|
||||
mockPathname = '/settings/infrastructure/install';
|
||||
renderWorkspace();
|
||||
|
||||
expect(navigateSpy).toHaveBeenCalledWith('/settings/infrastructure', { replace: true });
|
||||
expect(screen.getByTestId('install-section')).toBeInTheDocument();
|
||||
await waitFor(() => expect(screen.getByTestId('install-section')).toBeInTheDocument());
|
||||
});
|
||||
|
||||
it('redirects legacy platforms deep link to the base route and opens the picker', () => {
|
||||
it('redirects legacy truenas deep link and pre-selects the TrueNAS credential slot', async () => {
|
||||
mockPathname = '/settings/infrastructure/platforms/truenas';
|
||||
renderWorkspace();
|
||||
|
||||
expect(navigateSpy).toHaveBeenCalledWith('/settings/infrastructure', { replace: true });
|
||||
expect(screen.getByTestId('truenas-section')).toBeInTheDocument();
|
||||
await waitFor(() => expect(screen.getByTestId('truenas-section')).toBeInTheDocument());
|
||||
});
|
||||
|
||||
it('hides Add infrastructure and the add panel in read-only mode', () => {
|
||||
it('hides Add infrastructure and the add drawer in read-only mode', () => {
|
||||
presentationPolicyIsReadOnlyMock.mockReturnValue(true);
|
||||
renderWorkspace();
|
||||
|
||||
expect(screen.queryByRole('button', { name: /Add infrastructure/i })).toBeNull();
|
||||
expect(screen.queryByTestId('install-section')).toBeNull();
|
||||
expect(screen.queryByRole('button', { name: /Probe address/i })).toBeNull();
|
||||
});
|
||||
|
||||
it('collapses read-only sessions back to the inventory and redirects', () => {
|
||||
|
|
|
|||
|
|
@ -896,8 +896,8 @@ describe('Settings architecture guardrails', () => {
|
|||
"import { Subtabs } from '@/components/shared/Subtabs'",
|
||||
);
|
||||
expect(infrastructureWorkspaceSource).not.toContain('<Subtabs');
|
||||
expect(infrastructureWorkspaceSource).toContain("panelStep() === 'pick'");
|
||||
expect(infrastructureWorkspaceSource).toContain("panelStep() === 'agent'");
|
||||
expect(infrastructureWorkspaceSource).toContain('./ConnectionEditor/ConnectionEditor');
|
||||
expect(infrastructureWorkspaceSource).toContain('renderCredentialSlot');
|
||||
expect(infrastructureWorkspaceSource).not.toContain("activeView() === 'operations'");
|
||||
expect(connectionsTableSource).toContain('headerActions');
|
||||
expect(connectionsTableModelSource).toContain('export function buildInfrastructureSystemRows');
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue