mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-19 10:33:29 +00:00
Clarify API platform onboarding flow
This commit is contained in:
parent
d0940db33b
commit
73d549f248
14 changed files with 97 additions and 205 deletions
|
|
@ -1452,7 +1452,7 @@ action, and the page does not fork back into a second monitored-systems ledger
|
|||
below.
|
||||
Adding infrastructure therefore happens in two governed steps. The
|
||||
`?add=pick` modal owns grouped source-type selection and may offer
|
||||
`Detect from address` as a secondary utility. The `?add=detect` modal owns
|
||||
`Detect API platform` as a secondary utility. The `?add=detect` modal owns
|
||||
probe-driven handoff into `ConnectionEditor.tsx`, while typed add routes jump
|
||||
straight into the matching credential or install body. The shared Settings
|
||||
sidebar still owns only the top-level `Infrastructure` destination; movement
|
||||
|
|
|
|||
|
|
@ -325,6 +325,10 @@ prompt explain the same operator-facing priority.
|
|||
session presentation policy says the operator cannot manage setup, `/settings`
|
||||
and sidebar navigation must land on the canonical reporting/control surface
|
||||
instead of setup-oriented install routes.
|
||||
Route normalization must wait until that presentation policy has resolved
|
||||
before stripping infrastructure onboarding queries such as `?add=pick`,
|
||||
`?add=agent`, or `?add=detect`, so first-session and explicit add-flow
|
||||
handoffs do not lose their modal target during session bootstrap.
|
||||
That same settings-shell boundary also owns explicit organization-route
|
||||
stability. Deep links such as `/settings/organization`,
|
||||
`/settings/organization/access`, and adjacent organization shells must keep
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ export const AddressProbeStep: Component<AddressProbeStepProps> = (props) => {
|
|||
<form class="space-y-4" onSubmit={handleSubmit}>
|
||||
<div class={formField}>
|
||||
<label class={formLabel} for="connection-address">
|
||||
Address
|
||||
API endpoint
|
||||
</label>
|
||||
<input
|
||||
id="connection-address"
|
||||
|
|
@ -44,8 +44,8 @@ export const AddressProbeStep: Component<AddressProbeStepProps> = (props) => {
|
|||
disabled={props.state.phase() === 'probing'}
|
||||
/>
|
||||
<p class={formHelpText}>
|
||||
Paste a hostname, IP, or URL to identify a supported platform. Pulse validates the match
|
||||
and asks for credentials next.
|
||||
Paste a hostname, IP, or URL for a supported API-backed platform. Pulse validates the
|
||||
match and asks for credentials next.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
@ -55,7 +55,7 @@ export const AddressProbeStep: Component<AddressProbeStepProps> = (props) => {
|
|||
class="inline-flex items-center rounded-md bg-blue-600 px-3 py-2 text-sm font-medium text-white transition-colors hover:bg-blue-500 disabled:opacity-60"
|
||||
disabled={props.state.phase() === 'probing' || props.state.address().trim().length === 0}
|
||||
>
|
||||
{props.state.phase() === 'probing' ? 'Probing…' : 'Probe address'}
|
||||
{props.state.phase() === 'probing' ? 'Probing…' : 'Probe API endpoint'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -101,7 +101,7 @@ export const ConnectionEditor: Component<ConnectionEditorProps> = (props) => {
|
|||
<section class="space-y-4 rounded-xl border border-border bg-surface p-4">
|
||||
<div class="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div class="space-y-1">
|
||||
<div class="text-sm font-semibold text-base-content">Address probe</div>
|
||||
<div class="text-sm font-semibold text-base-content">API platform probe</div>
|
||||
<p class="text-xs text-muted">
|
||||
Pulse can auto-detect these platforms from an address when their management API
|
||||
is reachable.
|
||||
|
|
@ -162,7 +162,7 @@ export const ConnectionEditor: Component<ConnectionEditorProps> = (props) => {
|
|||
onClick={reopenProbe}
|
||||
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"
|
||||
>
|
||||
← Back to detect
|
||||
← Back to API probe
|
||||
</button>
|
||||
</Show>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ describe('ConnectionEditor', () => {
|
|||
const input = screen.getByPlaceholderText(/vcenter\.lab/) as HTMLInputElement;
|
||||
fireEvent.input(input, { target: { value: 'pve.lab' } });
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /probe address/i }));
|
||||
fireEvent.click(screen.getByRole('button', { name: /probe api endpoint/i }));
|
||||
|
||||
await waitFor(() => expect(mockedProbe).toHaveBeenCalledWith('pve.lab'));
|
||||
|
||||
|
|
@ -61,7 +61,7 @@ describe('ConnectionEditor', () => {
|
|||
/>
|
||||
));
|
||||
|
||||
expect(screen.getByText('Address probe')).toBeInTheDocument();
|
||||
expect(screen.getByText('API platform probe')).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: /Back to source types/i })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: /Install Pulse Agent/i })).toBeInTheDocument();
|
||||
|
||||
|
|
@ -83,7 +83,7 @@ describe('ConnectionEditor', () => {
|
|||
|
||||
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 }));
|
||||
fireEvent.click(screen.getByRole('button', { name: /probe api endpoint/i }));
|
||||
|
||||
await waitFor(() =>
|
||||
expect(screen.getByText(/No supported API-backed platform detected/i)).toBeInTheDocument(),
|
||||
|
|
@ -107,7 +107,7 @@ describe('ConnectionEditor', () => {
|
|||
|
||||
const input = screen.getByPlaceholderText(/vcenter\.lab/) as HTMLInputElement;
|
||||
fireEvent.input(input, { target: { value: 'baremetal.lan' } });
|
||||
fireEvent.click(screen.getByRole('button', { name: /probe address/i }));
|
||||
fireEvent.click(screen.getByRole('button', { name: /probe api endpoint/i }));
|
||||
await waitFor(() => expect(mockedProbe).toHaveBeenCalled());
|
||||
|
||||
const agentButton = await screen.findByRole('button', {
|
||||
|
|
@ -146,17 +146,17 @@ describe('ConnectionEditor', () => {
|
|||
|
||||
const input = screen.getByPlaceholderText(/vcenter\.lab/) as HTMLInputElement;
|
||||
fireEvent.input(input, { target: { value: 'truenas.lab' } });
|
||||
fireEvent.click(screen.getByRole('button', { name: /probe address/i }));
|
||||
fireEvent.click(screen.getByRole('button', { name: /probe api endpoint/i }));
|
||||
|
||||
fireEvent.click((await screen.findByText('https://truenas.lab')).closest('button')!);
|
||||
await waitFor(() => expect(screen.getByTestId('slot').textContent).toBe('slot:truenas'));
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /Back to detect/i }));
|
||||
fireEvent.click(screen.getByRole('button', { name: /Back to API probe/i }));
|
||||
|
||||
const resetInput = screen.getByPlaceholderText(/vcenter\.lab/) as HTMLInputElement;
|
||||
expect(resetInput.value).toBe('');
|
||||
expect(screen.queryByTestId('slot')).toBeNull();
|
||||
expect(screen.getByText('Address probe')).toBeInTheDocument();
|
||||
expect(screen.getByText('API platform probe')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('opens direct type routes without the detect setup', async () => {
|
||||
|
|
@ -171,7 +171,7 @@ describe('ConnectionEditor', () => {
|
|||
));
|
||||
|
||||
await waitFor(() => expect(screen.getByTestId('slot').textContent).toBe('slot:truenas'));
|
||||
expect(screen.queryByRole('button', { name: /probe address/i })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('button', { name: /probe api endpoint/i })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('skips probe setup when an initialType is supplied in edit mode', () => {
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ export function createConnectionEditorState(): ConnectionEditorState {
|
|||
const runProbe = async () => {
|
||||
const value = address().trim();
|
||||
if (!isSubmittableAddress(value)) {
|
||||
setErrorMessage('Enter an address to probe.');
|
||||
setErrorMessage('Enter an API endpoint to probe.');
|
||||
setPhase('error');
|
||||
return 'error';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import {
|
|||
|
||||
interface InfrastructureSourcePickerProps {
|
||||
onSelectStep: (step: InfrastructureSourcePickerRouteStep) => void;
|
||||
onDetectFromAddress?: () => void;
|
||||
onDetectApiPlatform?: () => void;
|
||||
}
|
||||
|
||||
const detectButtonClass =
|
||||
|
|
@ -82,10 +82,10 @@ export const InfrastructureSourcePicker: Component<InfrastructureSourcePickerPro
|
|||
placeholder="Search platforms, hosts, services..."
|
||||
/>
|
||||
</label>
|
||||
<Show when={props.onDetectFromAddress}>
|
||||
<Show when={props.onDetectApiPlatform}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={props.onDetectFromAddress}
|
||||
onClick={props.onDetectApiPlatform}
|
||||
class={`${detectButtonClass} sm:w-auto`}
|
||||
>
|
||||
<Search class="mr-2 h-4 w-4" />
|
||||
|
|
|
|||
|
|
@ -960,7 +960,7 @@ const InfrastructureWorkspaceContent: Component<InfrastructureWorkspaceProps> =
|
|||
<Match when={routeStep() === 'pick'}>
|
||||
<InfrastructureSourcePicker
|
||||
onSelectStep={(step) => openAddFlow(step as ManagedAddTypeStep)}
|
||||
onDetectFromAddress={() => openAddFlow('detect')}
|
||||
onDetectApiPlatform={() => openAddFlow('detect')}
|
||||
/>
|
||||
</Match>
|
||||
|
||||
|
|
|
|||
|
|
@ -677,7 +677,7 @@ describe('InfrastructureWorkspace', () => {
|
|||
within(screen.getByRole('dialog')).getByRole('button', { name: /Back to source types/i }),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
within(screen.getByRole('dialog')).getByRole('button', { name: /Probe address/i }),
|
||||
within(screen.getByRole('dialog')).getByRole('button', { name: /Probe API endpoint/i }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -456,7 +456,7 @@ describe('settings architecture guardrails', () => {
|
|||
expect(infrastructureWorkspaceSource).toContain('flex h-full min-h-0 flex-col');
|
||||
expect(infrastructureWorkspaceSource).toContain('showSlotHeader={false}');
|
||||
expect(infrastructureWorkspaceSource).toContain(
|
||||
"onDetectFromAddress={() => openAddFlow('detect')}",
|
||||
"onDetectApiPlatform={() => openAddFlow('detect')}",
|
||||
);
|
||||
expect(infrastructureWorkspaceSource).toContain("onBackToCatalog={() => openAddFlow('pick')}");
|
||||
expect(infrastructureWorkspaceSource).not.toContain('trackInitialCatalogSelection');
|
||||
|
|
@ -532,10 +532,10 @@ describe('settings architecture guardrails', () => {
|
|||
"from '@/utils/infrastructureOnboardingPresentation';",
|
||||
);
|
||||
expect(connectionEditorSource).toContain('<AddressProbeStep');
|
||||
expect(connectionEditorSource).toContain('Address probe');
|
||||
expect(connectionEditorSource).toContain('API platform probe');
|
||||
expect(connectionEditorSource).toContain('flex h-full min-h-0 flex-col');
|
||||
expect(connectionEditorSource).toContain('Back to source types');
|
||||
expect(connectionEditorSource).toContain('Back to detect');
|
||||
expect(connectionEditorSource).toContain('Back to API probe');
|
||||
expect(connectionEditorSource).toContain('Install Pulse Agent');
|
||||
expect(connectionEditorSource).not.toContain('buildConnectionEditorCatalogEntries');
|
||||
expect(connectionEditorSource).not.toContain('selectedFamilyId');
|
||||
|
|
@ -545,7 +545,7 @@ describe('settings architecture guardrails', () => {
|
|||
expect(connectionEditorSource).not.toContain('Back to platforms');
|
||||
expect(connectionEditorSource).not.toContain('NodeModal');
|
||||
|
||||
expect(addressProbeStepSource).toContain('Probe address');
|
||||
expect(addressProbeStepSource).toContain('Probe API endpoint');
|
||||
expect(addressProbeStepSource).toContain('install Pulse Agent instead');
|
||||
expect(addressProbeStepSource).toContain('Choose a source type instead');
|
||||
expect(addressProbeStepSource).toContain('getInfrastructureAgentHostProfileSupportText');
|
||||
|
|
|
|||
|
|
@ -3,10 +3,12 @@ import { cleanup, fireEvent, render, screen, waitFor } from '@solidjs/testing-li
|
|||
import { useSettingsNavigation } from '../useSettingsNavigation';
|
||||
|
||||
const presentationPolicyIsReadOnlyMock = vi.hoisted(() => vi.fn(() => false));
|
||||
const sessionPresentationPolicyResolvedMock = vi.hoisted(() => vi.fn(() => true));
|
||||
const navigateSpy = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock('@/stores/sessionPresentationPolicy', () => ({
|
||||
presentationPolicyIsReadOnly: () => presentationPolicyIsReadOnlyMock(),
|
||||
sessionPresentationPolicyResolved: () => sessionPresentationPolicyResolvedMock(),
|
||||
}));
|
||||
|
||||
function renderHarness(pathname = '/settings', search = '', hash = '') {
|
||||
|
|
@ -36,7 +38,9 @@ describe('useSettingsNavigation', () => {
|
|||
cleanup();
|
||||
navigateSpy.mockReset();
|
||||
presentationPolicyIsReadOnlyMock.mockReset();
|
||||
sessionPresentationPolicyResolvedMock.mockReset();
|
||||
presentationPolicyIsReadOnlyMock.mockReturnValue(false);
|
||||
sessionPresentationPolicyResolvedMock.mockReturnValue(true);
|
||||
});
|
||||
|
||||
it('lands /settings on reporting inventory when the session is read-only', async () => {
|
||||
|
|
@ -62,6 +66,20 @@ describe('useSettingsNavigation', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('does not strip infrastructure onboarding queries before presentation policy resolves', async () => {
|
||||
presentationPolicyIsReadOnlyMock.mockReturnValue(true);
|
||||
sessionPresentationPolicyResolvedMock.mockReturnValue(false);
|
||||
|
||||
renderHarness('/settings/infrastructure', '?add=pick');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(navigateSpy).not.toHaveBeenCalledWith('/settings/infrastructure', {
|
||||
replace: true,
|
||||
scroll: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('syncs the selected proxmox agent from canonical deep links on initial load', async () => {
|
||||
renderHarness('/settings/infrastructure/platforms/proxmox/pbs');
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
import { createEffect, createSignal, on } from 'solid-js';
|
||||
import { presentationPolicyIsReadOnly } from '@/stores/sessionPresentationPolicy';
|
||||
import {
|
||||
presentationPolicyIsReadOnly,
|
||||
sessionPresentationPolicyResolved,
|
||||
} from '@/stores/sessionPresentationPolicy';
|
||||
import { resolveCanonicalSelfHostedBillingHref } from '@/utils/pricingHandoff';
|
||||
import {
|
||||
buildInfrastructureOnboardingPath,
|
||||
|
|
@ -91,6 +94,7 @@ export function useSettingsNavigation({ navigate, location }: UseSettingsNavigat
|
|||
}
|
||||
|
||||
if (
|
||||
sessionPresentationPolicyResolved() &&
|
||||
presentationPolicyIsReadOnly() &&
|
||||
(path.startsWith('/settings/infrastructure') ||
|
||||
path === '/settings/workloads' ||
|
||||
|
|
|
|||
|
|
@ -39,14 +39,16 @@ const test = base.extend<{}, WorkerFixtures>({
|
|||
test.describe("runtime-home onboarding contract", () => {
|
||||
test.setTimeout(180_000);
|
||||
|
||||
test("normalizes the agent install handoff onto the shared infrastructure workspace", async ({
|
||||
test("opens the agent install handoff on the shared infrastructure workspace", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/settings/infrastructure?add=agent", {
|
||||
waitUntil: "domcontentloaded",
|
||||
});
|
||||
|
||||
await page.waitForURL(/\/settings\/infrastructure$/, { timeout: 15_000 });
|
||||
await page.waitForURL(/\/settings\/infrastructure\?add=agent$/, {
|
||||
timeout: 15_000,
|
||||
});
|
||||
await expect(
|
||||
page.getByRole("heading", {
|
||||
level: 1,
|
||||
|
|
@ -54,24 +56,35 @@ test.describe("runtime-home onboarding contract", () => {
|
|||
}),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole("heading", { level: 2, name: "Install on a host" }),
|
||||
page.getByRole("dialog").getByRole("heading", {
|
||||
name: "Add Pulse Agent",
|
||||
exact: true,
|
||||
}),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole("dialog").getByRole("heading", {
|
||||
level: 2,
|
||||
name: "Install on a host",
|
||||
}),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole("button", { name: /Generate token/i }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole("button", { name: /Probe address/i }),
|
||||
page.getByRole("button", { name: /Probe API endpoint/i }),
|
||||
).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("normalizes the platform-pick handoff onto the shared infrastructure workspace", async ({
|
||||
test("opens the platform-pick handoff on the shared infrastructure workspace", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/settings/infrastructure?add=pick", {
|
||||
waitUntil: "domcontentloaded",
|
||||
});
|
||||
|
||||
await page.waitForURL(/\/settings\/infrastructure$/, { timeout: 15_000 });
|
||||
await page.waitForURL(/\/settings\/infrastructure\?add=pick$/, {
|
||||
timeout: 15_000,
|
||||
});
|
||||
await expect(
|
||||
page.getByRole("heading", {
|
||||
level: 1,
|
||||
|
|
@ -79,7 +92,9 @@ test.describe("runtime-home onboarding contract", () => {
|
|||
}),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole("button", { name: /Probe address/i }),
|
||||
page.getByRole("dialog").getByRole("button", {
|
||||
name: /Detect API platform/i,
|
||||
}),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole("button", { name: /Generate token/i }),
|
||||
|
|
@ -101,10 +116,10 @@ test.describe("runtime-home onboarding contract", () => {
|
|||
}),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole("heading", { level: 3, name: "Monitored systems" }),
|
||||
page.getByText("Infrastructure systems", { exact: true }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole("button", { name: /Probe address/i }),
|
||||
page.getByRole("button", { name: /Probe API endpoint/i }),
|
||||
).toHaveCount(0);
|
||||
await expect(
|
||||
page.getByRole("heading", { level: 2, name: "Install on a host" }),
|
||||
|
|
|
|||
|
|
@ -10,13 +10,6 @@ type WorkerFixtures = {
|
|||
authStorageStatePath: string;
|
||||
};
|
||||
|
||||
type UpgradeMetricEventPayload = {
|
||||
type: string;
|
||||
surface: string;
|
||||
capability?: string;
|
||||
idempotency_key?: string;
|
||||
};
|
||||
|
||||
type OverflowAudit = {
|
||||
viewportWidth: number;
|
||||
pageWidth: number;
|
||||
|
|
@ -69,49 +62,10 @@ async function stubConnectionsList(page: Page): Promise<void> {
|
|||
});
|
||||
}
|
||||
|
||||
async function recordUpgradeMetricEvents(
|
||||
page: Page,
|
||||
): Promise<UpgradeMetricEventPayload[]> {
|
||||
const events: UpgradeMetricEventPayload[] = [];
|
||||
|
||||
await page.route("**/api/upgrade-metrics/events", async (route) => {
|
||||
const payload = route.request().postDataJSON() as UpgradeMetricEventPayload;
|
||||
events.push(payload);
|
||||
await route.fulfill({
|
||||
status: 202,
|
||||
contentType: "application/json",
|
||||
body: "{}",
|
||||
});
|
||||
});
|
||||
|
||||
return events;
|
||||
}
|
||||
|
||||
async function prepareOnboardingPage(page: Page): Promise<void> {
|
||||
await stubConnectionsList(page);
|
||||
}
|
||||
|
||||
function countUpgradeEvents(
|
||||
events: readonly UpgradeMetricEventPayload[],
|
||||
type: string,
|
||||
capability?: string,
|
||||
): number {
|
||||
const matchingKeys = new Set<string>();
|
||||
|
||||
for (const event of events) {
|
||||
if (event.surface !== "settings_infrastructure_add") continue;
|
||||
if (event.type !== type) continue;
|
||||
if (capability !== undefined && event.capability !== capability) continue;
|
||||
|
||||
matchingKeys.add(
|
||||
event.idempotency_key ??
|
||||
`${event.type}:${event.surface}:${event.capability ?? ""}:${matchingKeys.size}`,
|
||||
);
|
||||
}
|
||||
|
||||
return matchingKeys.size;
|
||||
}
|
||||
|
||||
async function scrollToBottom(page: Page): Promise<void> {
|
||||
const viewportHeight = await page.evaluate(() => window.innerHeight || 800);
|
||||
const step = Math.max(240, Math.floor(viewportHeight * 0.75));
|
||||
|
|
@ -188,7 +142,6 @@ test.describe("Infrastructure onboarding", () => {
|
|||
"Desktop-only infrastructure manager coverage",
|
||||
);
|
||||
|
||||
const metricEvents = await recordUpgradeMetricEvents(page);
|
||||
await prepareOnboardingPage(page);
|
||||
await page.route("**/api/discover", async (route) => {
|
||||
const requestUrl = new URL(route.request().url());
|
||||
|
|
@ -253,7 +206,7 @@ test.describe("Infrastructure onboarding", () => {
|
|||
page.getByRole("button", { name: /Add TrueNAS SCALE/i }),
|
||||
).toHaveCount(0);
|
||||
await expect(
|
||||
page.getByRole("button", { name: /Detect from address/i }),
|
||||
page.getByRole("button", { name: /Detect API platform/i }),
|
||||
).toHaveCount(0);
|
||||
await expect(
|
||||
page.getByText("Monitored systems", { exact: true }),
|
||||
|
|
@ -261,12 +214,6 @@ test.describe("Infrastructure onboarding", () => {
|
|||
await expect(
|
||||
page.getByText("Connection types", { exact: true }),
|
||||
).toHaveCount(0);
|
||||
|
||||
await expect
|
||||
.poll(() =>
|
||||
countUpgradeEvents(metricEvents, "infrastructure_onboarding_opened"),
|
||||
)
|
||||
.toBe(0);
|
||||
});
|
||||
|
||||
test("desktop discovery settings open as an infrastructure-owned dialog", async ({
|
||||
|
|
@ -302,7 +249,7 @@ test.describe("Infrastructure onboarding", () => {
|
|||
await expect(page).toHaveURL(/\/settings\/infrastructure(?:\?.*)?$/);
|
||||
});
|
||||
|
||||
test("desktop picker add opens the matching modal and records type onboarding", async ({
|
||||
test("desktop picker add opens the matching modal", async ({
|
||||
page,
|
||||
}, testInfo) => {
|
||||
test.skip(
|
||||
|
|
@ -310,7 +257,6 @@ test.describe("Infrastructure onboarding", () => {
|
|||
"Desktop-only manager add-path instrumentation coverage",
|
||||
);
|
||||
|
||||
const metricEvents = await recordUpgradeMetricEvents(page);
|
||||
await prepareOnboardingPage(page);
|
||||
|
||||
await page.goto("/settings/infrastructure", {
|
||||
|
|
@ -320,7 +266,10 @@ test.describe("Infrastructure onboarding", () => {
|
|||
timeout: 15_000,
|
||||
});
|
||||
|
||||
await page.getByRole("button", { name: /Add infrastructure/i }).click();
|
||||
await page
|
||||
.getByRole("button", { name: "Add infrastructure", exact: true })
|
||||
.first()
|
||||
.click();
|
||||
await page.waitForURL(/\/settings\/infrastructure\?add=pick$/, {
|
||||
timeout: 15_000,
|
||||
});
|
||||
|
|
@ -347,39 +296,6 @@ test.describe("Infrastructure onboarding", () => {
|
|||
exact: true,
|
||||
}),
|
||||
).toBeVisible();
|
||||
|
||||
await expect
|
||||
.poll(() =>
|
||||
countUpgradeEvents(metricEvents, "infrastructure_onboarding_opened"),
|
||||
)
|
||||
.toBe(1);
|
||||
await expect
|
||||
.poll(() =>
|
||||
countUpgradeEvents(
|
||||
metricEvents,
|
||||
"infrastructure_onboarding_path_selected",
|
||||
"api",
|
||||
),
|
||||
)
|
||||
.toBe(1);
|
||||
await expect
|
||||
.poll(() =>
|
||||
countUpgradeEvents(
|
||||
metricEvents,
|
||||
"infrastructure_onboarding_catalog_selected",
|
||||
"truenas",
|
||||
),
|
||||
)
|
||||
.toBe(1);
|
||||
await expect
|
||||
.poll(() =>
|
||||
countUpgradeEvents(
|
||||
metricEvents,
|
||||
"infrastructure_onboarding_credentials_opened",
|
||||
"truenas",
|
||||
),
|
||||
)
|
||||
.toBe(1);
|
||||
});
|
||||
|
||||
test("desktop explicit discovery scan surfaces a candidate row and opens a prefilled review dialog", async ({
|
||||
|
|
@ -390,7 +306,6 @@ test.describe("Infrastructure onboarding", () => {
|
|||
"Desktop-only discovery-candidate review coverage",
|
||||
);
|
||||
|
||||
const metricEvents = await recordUpgradeMetricEvents(page);
|
||||
await prepareOnboardingPage(page);
|
||||
|
||||
await page.route("**/api/discover", async (route) => {
|
||||
|
|
@ -469,42 +384,9 @@ test.describe("Infrastructure onboarding", () => {
|
|||
await expect(
|
||||
page.getByPlaceholder("https://proxmox.example.com:8006"),
|
||||
).toHaveValue("https://discovered-pve.lab:8006");
|
||||
|
||||
await expect
|
||||
.poll(() =>
|
||||
countUpgradeEvents(metricEvents, "infrastructure_onboarding_opened"),
|
||||
)
|
||||
.toBe(1);
|
||||
await expect
|
||||
.poll(() =>
|
||||
countUpgradeEvents(
|
||||
metricEvents,
|
||||
"infrastructure_onboarding_path_selected",
|
||||
"api",
|
||||
),
|
||||
)
|
||||
.toBe(1);
|
||||
await expect
|
||||
.poll(() =>
|
||||
countUpgradeEvents(
|
||||
metricEvents,
|
||||
"infrastructure_onboarding_credentials_opened",
|
||||
"pve",
|
||||
),
|
||||
)
|
||||
.toBe(1);
|
||||
await expect
|
||||
.poll(() =>
|
||||
countUpgradeEvents(
|
||||
metricEvents,
|
||||
"infrastructure_onboarding_catalog_selected",
|
||||
"pve",
|
||||
),
|
||||
)
|
||||
.toBe(0);
|
||||
});
|
||||
|
||||
test("desktop detect utility records no-match agent fallback from the add-infrastructure picker", async ({
|
||||
test("desktop detect utility offers no-match agent fallback from the add-infrastructure picker", async ({
|
||||
page,
|
||||
}, testInfo) => {
|
||||
test.skip(
|
||||
|
|
@ -512,7 +394,6 @@ test.describe("Infrastructure onboarding", () => {
|
|||
"Desktop-only onboarding probe instrumentation coverage",
|
||||
);
|
||||
|
||||
const metricEvents = await recordUpgradeMetricEvents(page);
|
||||
await prepareOnboardingPage(page);
|
||||
|
||||
await page.route("**/api/connections/probe", async (route) => {
|
||||
|
|
@ -541,18 +422,21 @@ test.describe("Infrastructure onboarding", () => {
|
|||
await page.waitForURL(/\/settings\/infrastructure(?:\?.*)?$/, {
|
||||
timeout: 15_000,
|
||||
});
|
||||
await page.getByRole("button", { name: /Add infrastructure/i }).click();
|
||||
await page
|
||||
.getByRole("button", { name: "Add infrastructure", exact: true })
|
||||
.first()
|
||||
.click();
|
||||
await page.waitForURL(/\/settings\/infrastructure\?add=pick$/, {
|
||||
timeout: 15_000,
|
||||
});
|
||||
await page.getByRole("button", { name: /Detect from address/i }).click();
|
||||
await page.getByRole("button", { name: /Detect API platform/i }).click();
|
||||
await page.waitForURL(/\/settings\/infrastructure\?add=detect$/, {
|
||||
timeout: 15_000,
|
||||
});
|
||||
|
||||
await page.getByLabel("Address").fill("baremetal.lab");
|
||||
await page.getByLabel("API endpoint").fill("baremetal.lab");
|
||||
await page
|
||||
.getByRole("button", { name: "Probe address", exact: true })
|
||||
.getByRole("button", { name: "Probe API endpoint", exact: true })
|
||||
.click();
|
||||
|
||||
await expect(
|
||||
|
|
@ -561,51 +445,18 @@ test.describe("Infrastructure onboarding", () => {
|
|||
{ exact: true },
|
||||
),
|
||||
).toBeVisible();
|
||||
|
||||
await expect
|
||||
.poll(() =>
|
||||
countUpgradeEvents(
|
||||
metricEvents,
|
||||
"infrastructure_onboarding_path_selected",
|
||||
"api",
|
||||
),
|
||||
)
|
||||
.toBe(1);
|
||||
await expect
|
||||
.poll(() =>
|
||||
countUpgradeEvents(
|
||||
metricEvents,
|
||||
"infrastructure_onboarding_probe_result",
|
||||
"no-match",
|
||||
),
|
||||
)
|
||||
.toBe(1);
|
||||
|
||||
await page
|
||||
.getByRole("button", { name: "install Pulse Agent instead", exact: true })
|
||||
.click();
|
||||
await page.waitForURL(/\/settings\/infrastructure\?add=agent$/, {
|
||||
timeout: 15_000,
|
||||
});
|
||||
await expect(
|
||||
page.getByRole("button", { name: /Back to detect/i }),
|
||||
page.getByRole("heading", { level: 2, name: "Install on a host" }),
|
||||
).toBeVisible();
|
||||
|
||||
await expect
|
||||
.poll(() =>
|
||||
countUpgradeEvents(
|
||||
metricEvents,
|
||||
"infrastructure_onboarding_path_selected",
|
||||
"agent",
|
||||
),
|
||||
)
|
||||
.toBe(1);
|
||||
await expect
|
||||
.poll(() =>
|
||||
countUpgradeEvents(
|
||||
metricEvents,
|
||||
"infrastructure_onboarding_credentials_opened",
|
||||
"agent",
|
||||
),
|
||||
)
|
||||
.toBe(1);
|
||||
await expect(
|
||||
page.getByRole("button", { name: /Probe API endpoint/i }),
|
||||
).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("mobile landing and grouped platform tables stay inside the viewport", async ({
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue