mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-23 20:25:56 +00:00
Add first-host install success handoff
This commit is contained in:
@@ -153,6 +153,13 @@ management, and fleet control surfaces.
|
||||
must default to the install workspace, and the workspace shell must make
|
||||
the first-host sequence explicit before operators drift into reporting and
|
||||
control surfaces.
|
||||
8. Keep post-install lifecycle completion explicit inside
|
||||
`frontend-modern/src/components/Settings/InfrastructureInstallerSection.tsx`
|
||||
and `frontend-modern/src/components/Settings/useInfrastructureInstallState.tsx`.
|
||||
When the first host reports successfully, the install workflow must treat
|
||||
that as a completion handoff with direct navigation into `/dashboard` and
|
||||
`/settings/infrastructure/operations` instead of leaving operators on a
|
||||
generic lookup result.
|
||||
|
||||
## Current State
|
||||
|
||||
|
||||
@@ -202,6 +202,13 @@ Own canonical runtime payload shapes between backend and frontend.
|
||||
15. Keep recovery payload platform vocabulary canonical across that same `/api/recovery/*` surface: point payloads must expose `platform`, rollup payloads must expose `platforms`, and any compatibility `provider` / `providers` aliases must remain secondary fallback fields rather than replacing the shared response model
|
||||
16. Keep recovery linked-resource vocabulary canonical across that same `/api/recovery/*` surface: points and rollups must expose `itemResourceId` as the canonical linked-resource field, accepted legacy `subjectResourceId` aliases must remain compatibility-only input or secondary payload fields, and the shared proof surface must pin that normalization in the same slice as any handler change
|
||||
17. Keep recovery external item-reference vocabulary canonical across that same `/api/recovery/*` surface: point and rollup payloads must expose `itemRef` as the canonical external item-reference field, accepted legacy `subjectRef` aliases must remain compatibility-only secondary payload fields, and the shared proof surface must pin that normalization in the same slice as any handler change
|
||||
18. Keep first-host lookup completion explicit on the shared install-state API
|
||||
boundary: when
|
||||
`frontend-modern/src/components/Settings/useInfrastructureInstallState.tsx`
|
||||
receives a successful connected-agent lookup result, the canonical install
|
||||
flow must expose direct navigation into `/dashboard` and
|
||||
`/settings/infrastructure/operations` rather than leaving the operator on a
|
||||
transport-only status readout.
|
||||
|
||||
## Current State
|
||||
|
||||
|
||||
@@ -513,11 +513,33 @@ export const InfrastructureInstallerSection: Component = () => {
|
||||
const agent = () => result().agent!;
|
||||
const lookupStatusPresentation = () =>
|
||||
getUnifiedAgentLookupStatusPresentation(agent().connected);
|
||||
const isConnected = () => agent().connected;
|
||||
|
||||
return (
|
||||
<div class="space-y-1 rounded-md border border-blue-200 bg-surface px-3 py-2 text-xs text-blue-900 dark:border-blue-700 dark:bg-blue-900 dark:text-blue-100">
|
||||
<div class="flex flex-col gap-1 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div class="text-sm font-semibold">{agent().displayName || agent().hostname}</div>
|
||||
<div
|
||||
class={`space-y-3 rounded-md border px-3 py-3 text-xs ${
|
||||
isConnected()
|
||||
? 'border-emerald-200 bg-emerald-50 text-emerald-950 dark:border-emerald-700 dark:bg-emerald-900 dark:text-emerald-50'
|
||||
: 'border-blue-200 bg-surface text-blue-900 dark:border-blue-700 dark:bg-blue-900 dark:text-blue-100'
|
||||
}`}
|
||||
>
|
||||
<div class="flex flex-col gap-2 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div class="space-y-1">
|
||||
<div class="text-sm font-semibold">
|
||||
{isConnected() ? 'First host connected' : agent().displayName || agent().hostname}
|
||||
</div>
|
||||
<p
|
||||
class={`text-xs ${
|
||||
isConnected()
|
||||
? 'text-emerald-800 dark:text-emerald-200'
|
||||
: 'text-blue-800 dark:text-blue-200'
|
||||
}`}
|
||||
>
|
||||
{isConnected()
|
||||
? `${agent().displayName || agent().hostname} is reporting live telemetry to Pulse. Open the dashboard to verify your first overview, or continue in Reporting & control to inspect this host and add more infrastructure.`
|
||||
: `${agent().displayName || agent().hostname} has been found, but Pulse is not receiving a live check-in yet. Keep the installer running on that machine and check again.`}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span
|
||||
class={`inline-flex items-center rounded-full px-2 py-0.5 text-[11px] font-semibold ${lookupStatusPresentation().badgeClass}`}
|
||||
@@ -529,13 +551,39 @@ export const InfrastructureInstallerSection: Component = () => {
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
Last seen {formatRelativeTime(agent().lastSeen)} (
|
||||
{formatAbsoluteTime(agent().lastSeen)})
|
||||
<div class="space-y-1">
|
||||
<div>
|
||||
Last seen {formatRelativeTime(agent().lastSeen)} (
|
||||
{formatAbsoluteTime(agent().lastSeen)})
|
||||
</div>
|
||||
<Show when={agent().agentVersion}>
|
||||
<div
|
||||
class={`text-xs ${
|
||||
isConnected()
|
||||
? 'text-emerald-800 dark:text-emerald-200'
|
||||
: 'text-blue-700 dark:text-blue-200'
|
||||
}`}
|
||||
>
|
||||
Agent version {agent().agentVersion}
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
<Show when={agent().agentVersion}>
|
||||
<div class="text-xs text-blue-700 dark:text-blue-200">
|
||||
Agent version {agent().agentVersion}
|
||||
<Show when={isConnected()}>
|
||||
<div class="flex flex-col gap-2 sm:flex-row">
|
||||
<button
|
||||
type="button"
|
||||
onClick={state.openDashboard}
|
||||
class="inline-flex items-center justify-center rounded-md bg-emerald-600 px-3 py-2 text-sm font-medium text-white transition-colors hover:bg-emerald-700"
|
||||
>
|
||||
Open dashboard
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={state.openInfrastructureInventory}
|
||||
class="inline-flex items-center justify-center rounded-md border border-emerald-300 bg-white px-3 py-2 text-sm font-medium text-emerald-900 transition-colors hover:bg-emerald-100 dark:border-emerald-700 dark:bg-emerald-950 dark:text-emerald-100 dark:hover:bg-emerald-800"
|
||||
>
|
||||
Open reporting & control
|
||||
</button>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
+25
@@ -25,6 +25,18 @@ import type {
|
||||
State,
|
||||
} from '@/types/api';
|
||||
|
||||
const { navigateMock } = vi.hoisted(() => ({
|
||||
navigateMock: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('@solidjs/router', async () => {
|
||||
const actual = await vi.importActual<typeof import('@solidjs/router')>('@solidjs/router');
|
||||
return {
|
||||
...actual,
|
||||
useNavigate: () => navigateMock,
|
||||
};
|
||||
});
|
||||
|
||||
let mockWsStore: {
|
||||
state: Pick<State, 'connectedInfrastructure'>;
|
||||
connected: () => boolean;
|
||||
@@ -561,6 +573,7 @@ const getConnectionUrlInput = (): HTMLInputElement => {
|
||||
|
||||
beforeEach(() => {
|
||||
securityStatusResponse = { requiresAuth: true, apiTokenConfigured: false };
|
||||
navigateMock.mockReset();
|
||||
lookupMock.mockReset();
|
||||
createTokenMock.mockReset();
|
||||
deleteAgentMock.mockReset();
|
||||
@@ -698,6 +711,18 @@ describe('InfrastructureOperationsController agent lookup', () => {
|
||||
|
||||
await waitFor(() => expect(lookupMock).toHaveBeenCalled(), { interval: 0 });
|
||||
await waitFor(() => expect(screen.getByText('Connected')).toBeInTheDocument(), { interval: 0 });
|
||||
expect(screen.getByText('First host connected')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(
|
||||
'Host One is reporting live telemetry to Pulse. Open the dashboard to verify your first overview, or continue in Reporting & control to inspect this host and add more infrastructure.',
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Open dashboard' }));
|
||||
expect(navigateMock).toHaveBeenCalledWith('/dashboard');
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Open reporting & control' }));
|
||||
expect(navigateMock).toHaveBeenCalledWith('/settings/infrastructure/operations');
|
||||
});
|
||||
|
||||
it('shows error message when agent is not found', async () => {
|
||||
|
||||
@@ -16,6 +16,7 @@ import { copyToClipboard } from '@/utils/clipboard';
|
||||
import { getPulseBaseUrl } from '@/utils/url';
|
||||
import { logger } from '@/utils/logger';
|
||||
import { STORAGE_KEYS } from '@/utils/localStorage';
|
||||
import { buildInfrastructureWorkspacePath } from './infrastructureWorkspaceModel';
|
||||
import {
|
||||
buildUnixAgentInstallCommand,
|
||||
buildWindowsAgentInstallCommand,
|
||||
@@ -330,6 +331,14 @@ Generate a scoped install token below before copying Unified Agent install comma
|
||||
navigate('/settings/infrastructure/proxmox');
|
||||
};
|
||||
|
||||
const openDashboard = () => {
|
||||
navigate('/dashboard');
|
||||
};
|
||||
|
||||
const openInfrastructureInventory = () => {
|
||||
navigate(buildInfrastructureWorkspacePath('inventory'));
|
||||
};
|
||||
|
||||
const isEmbedded = () => options.embedded ?? false;
|
||||
|
||||
return {
|
||||
@@ -361,7 +370,9 @@ Generate a scoped install token below before copying Unified Agent install comma
|
||||
lookupLoading,
|
||||
lookupResult,
|
||||
lookupValue,
|
||||
openDashboard,
|
||||
openDirectProxmoxSetup,
|
||||
openInfrastructureInventory,
|
||||
requiresToken,
|
||||
selectedAgentUrl,
|
||||
setCustomAgentUrl,
|
||||
|
||||
Reference in New Issue
Block a user