mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-24 12:13:28 +00:00
Carry the Platform API / Unified Agent vocabulary into the add flow
The explainer at the top of the ledger teaches two paths (Platform API and Pulse Unified Agent), but clicking Add dropped that vocabulary: the user landed on a generic address box with no label and could only reach the agent by clicking "Enter credentials manually" and picking it from a flat list of six raw-token types. Reshape the pre-credential step into two labeled sections that mirror the explainer: - "Platform API" section wraps the probe input and the manual-type fallback (now filtered to the five API types; heading reads "Choose Platform API type manually"). - "Pulse Unified Agent" section with a direct "Install the Unified Agent on a host" button, tinted to match the explainer's agent card. Also aligns the CONNECTION_TYPE_LABELS entry for agent to "Pulse Unified Agent" so the header on the install surface matches the brand used in the ledger and the explainer.
This commit is contained in:
@@ -29,7 +29,7 @@ export interface ConnectionEditorProps {
|
||||
onSaved?: () => void;
|
||||
}
|
||||
|
||||
const DEFAULT_MANUAL_TYPES: ConnectionType[] = ['pve', 'pbs', 'pmg', 'truenas', 'vmware', 'agent'];
|
||||
const DEFAULT_MANUAL_TYPES: ConnectionType[] = ['pve', 'pbs', 'pmg', 'truenas', 'vmware'];
|
||||
|
||||
export const ConnectionEditor: Component<ConnectionEditorProps> = (props) => {
|
||||
const state: ConnectionEditorState = createConnectionEditorState();
|
||||
@@ -80,38 +80,68 @@ export const ConnectionEditor: Component<ConnectionEditorProps> = (props) => {
|
||||
<div>
|
||||
<div class="text-sm font-semibold text-base-content">Add a connection</div>
|
||||
<div class="mt-0.5 text-xs text-muted">
|
||||
Paste an address and Pulse detects the product. One flow for every supported
|
||||
platform.
|
||||
Paste a platform address to connect its API, or install the Unified Agent on a
|
||||
host.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<AddressProbeStep
|
||||
state={state}
|
||||
onSelectCandidate={chooseCandidate}
|
||||
onChooseManually={() => setManualPickerOpen((v) => !v)}
|
||||
/>
|
||||
|
||||
<Show when={manualPickerOpen()}>
|
||||
<div class="space-y-2 rounded-md border border-border bg-surface p-3">
|
||||
<section class="space-y-3 rounded-md border border-border bg-surface-alt/30 p-3">
|
||||
<div>
|
||||
<div class="text-xs font-semibold uppercase tracking-wide text-muted">
|
||||
Choose type manually
|
||||
Platform API
|
||||
</div>
|
||||
<div class="text-[11px] text-muted">
|
||||
Proxmox VE / PBS / PMG, VMware, TrueNAS
|
||||
</div>
|
||||
<ul class="divide-y divide-border rounded-md border border-border">
|
||||
{manualOptions().map((type) => (
|
||||
<li>
|
||||
<button
|
||||
type="button"
|
||||
class="flex w-full items-center justify-between px-3 py-2 text-left text-sm text-base-content transition-colors hover:bg-surface-hover"
|
||||
onClick={() => chooseManualType(type)}
|
||||
>
|
||||
<span>{CONNECTION_TYPE_LABELS[type] ?? type}</span>
|
||||
<span class="text-xs text-muted">{type}</span>
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<AddressProbeStep
|
||||
state={state}
|
||||
onSelectCandidate={chooseCandidate}
|
||||
onChooseManually={() => setManualPickerOpen((v) => !v)}
|
||||
/>
|
||||
|
||||
<Show when={manualPickerOpen()}>
|
||||
<div class="space-y-2 rounded-md border border-border bg-surface p-3">
|
||||
<div class="text-xs font-semibold uppercase tracking-wide text-muted">
|
||||
Choose Platform API type manually
|
||||
</div>
|
||||
<ul class="divide-y divide-border rounded-md border border-border">
|
||||
{manualOptions().map((type) => (
|
||||
<li>
|
||||
<button
|
||||
type="button"
|
||||
class="flex w-full items-center justify-between px-3 py-2 text-left text-sm text-base-content transition-colors hover:bg-surface-hover"
|
||||
onClick={() => chooseManualType(type)}
|
||||
>
|
||||
<span>{CONNECTION_TYPE_LABELS[type] ?? type}</span>
|
||||
<span class="text-xs text-muted">{type}</span>
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</Show>
|
||||
</section>
|
||||
|
||||
<section class="space-y-3 rounded-md border border-blue-200 bg-blue-50/40 p-3 dark:border-blue-900 dark:bg-blue-950/20">
|
||||
<div>
|
||||
<div class="text-xs font-semibold uppercase tracking-wide text-muted">
|
||||
Pulse Unified Agent
|
||||
</div>
|
||||
<div class="text-[11px] text-muted">
|
||||
Host-level telemetry on Proxmox / VMware / TrueNAS, or the only path on
|
||||
bare-metal Linux, Unraid, FreeBSD.
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => chooseManualType('agent')}
|
||||
class="inline-flex items-center rounded-md border border-blue-600 bg-blue-600 px-3 py-2 text-sm font-medium text-white transition-colors hover:bg-blue-500"
|
||||
>
|
||||
Install the Unified Agent on a host
|
||||
</button>
|
||||
</section>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
|
||||
+54
@@ -94,6 +94,60 @@ describe('ConnectionEditor', () => {
|
||||
expect(lastCall.candidate).toBeNull();
|
||||
});
|
||||
|
||||
it('surfaces Platform API and Pulse Unified Agent as the two add-paths, mirroring the explainer', () => {
|
||||
render(() => (
|
||||
<ConnectionEditor
|
||||
renderCredentialSlot={() => <div />}
|
||||
onClose={() => {}}
|
||||
/>
|
||||
));
|
||||
|
||||
expect(screen.getByText('Platform API')).toBeInTheDocument();
|
||||
expect(screen.getByText('Pulse Unified Agent')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole('button', { name: /install the unified agent on a host/i }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('routes the Install Unified Agent button straight to the agent credential slot', () => {
|
||||
const renderSlot = vi.fn(({ type }) => <div data-testid="slot">slot:{type}</div>);
|
||||
|
||||
render(() => (
|
||||
<ConnectionEditor
|
||||
renderCredentialSlot={renderSlot}
|
||||
onClose={() => {}}
|
||||
/>
|
||||
));
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /install the unified agent on a host/i }));
|
||||
|
||||
expect(screen.getByTestId('slot').textContent).toBe('slot:agent');
|
||||
const call = renderSlot.mock.calls.at(-1)![0];
|
||||
expect(call.type).toBe('agent');
|
||||
expect(call.candidate).toBeNull();
|
||||
});
|
||||
|
||||
it('keeps agent out of the Platform API manual picker (it has its own path)', async () => {
|
||||
mockedProbe.mockResolvedValueOnce({ candidates: [], probedMs: 150 });
|
||||
|
||||
render(() => (
|
||||
<ConnectionEditor
|
||||
renderCredentialSlot={() => <div />}
|
||||
onClose={() => {}}
|
||||
/>
|
||||
));
|
||||
|
||||
const input = screen.getByPlaceholderText(/pve01\.lan/) as HTMLInputElement;
|
||||
fireEvent.input(input, { target: { value: 'example.lan' } });
|
||||
fireEvent.click(screen.getByRole('button', { name: /probe address/i }));
|
||||
await waitFor(() => expect(mockedProbe).toHaveBeenCalled());
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /enter credentials manually/i }));
|
||||
|
||||
expect(screen.getByText(/choose platform api type manually/i)).toBeInTheDocument();
|
||||
expect(screen.queryByText(/Agent \(install on host\)/i)).toBeNull();
|
||||
});
|
||||
|
||||
it('skips the probe step when an initialType is supplied (edit mode)', () => {
|
||||
const renderSlot = vi.fn(({ type }) => <div data-testid="slot">slot:{type}</div>);
|
||||
|
||||
|
||||
@@ -102,7 +102,7 @@ export const CONNECTION_TYPE_LABELS: Record<ConnectionType, string> = {
|
||||
pmg: 'Proxmox Mail Gateway',
|
||||
vmware: 'VMware vCenter / ESXi',
|
||||
truenas: 'TrueNAS SCALE',
|
||||
agent: 'Agent (install on host)',
|
||||
agent: 'Pulse Unified Agent',
|
||||
docker: 'Docker',
|
||||
kubernetes: 'Kubernetes',
|
||||
};
|
||||
|
||||
@@ -233,12 +233,13 @@ describe('InfrastructureWorkspace', () => {
|
||||
expect(setSearchParamsSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('routes to the agent install slot when Pulse agent is picked manually', () => {
|
||||
it('routes to the agent install slot via the dedicated Unified Agent path', () => {
|
||||
renderWorkspace();
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /Add connection/i }));
|
||||
fireEvent.click(screen.getByRole('button', { name: /Enter credentials manually/i }));
|
||||
fireEvent.click(screen.getByRole('button', { name: /Agent \(install on host\)/i }));
|
||||
fireEvent.click(
|
||||
screen.getByRole('button', { name: /Install the Unified Agent on a host/i }),
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('install-section')).toBeInTheDocument();
|
||||
});
|
||||
@@ -277,8 +278,9 @@ describe('InfrastructureWorkspace', () => {
|
||||
renderWorkspace();
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /Add connection/i }));
|
||||
fireEvent.click(screen.getByRole('button', { name: /Enter credentials manually/i }));
|
||||
fireEvent.click(screen.getByRole('button', { name: /Agent \(install on host\)/i }));
|
||||
fireEvent.click(
|
||||
screen.getByRole('button', { name: /Install the Unified Agent on a host/i }),
|
||||
);
|
||||
fireEvent.click(screen.getByRole('button', { name: /Back to probe/i }));
|
||||
|
||||
expect(screen.getByRole('button', { name: /Probe address/i })).toBeInTheDocument();
|
||||
@@ -289,8 +291,9 @@ describe('InfrastructureWorkspace', () => {
|
||||
renderWorkspace();
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /Add connection/i }));
|
||||
fireEvent.click(screen.getByRole('button', { name: /Enter credentials manually/i }));
|
||||
fireEvent.click(screen.getByRole('button', { name: /Agent \(install on host\)/i }));
|
||||
fireEvent.click(
|
||||
screen.getByRole('button', { name: /Install the Unified Agent on a host/i }),
|
||||
);
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Manage agent profiles' }));
|
||||
|
||||
expect(screen.getByTestId('agent-profiles')).toBeInTheDocument();
|
||||
|
||||
Reference in New Issue
Block a user