mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-21 10:43:36 +00:00
Focus agent install flow by selected source
This commit is contained in:
@@ -186,7 +186,11 @@ Unraid, TrueNAS, Proxmox, Docker, Kubernetes, or standalone hosts, and the
|
||||
helper maps those choices into API-backed, agent-backed, or availability-probe
|
||||
routes only after selection. The first picker surface must support aliases for
|
||||
common operator terms and must not make API/agent/probe taxonomy the primary
|
||||
choice model.
|
||||
choice model. Agent-backed typed routes must keep that context in the
|
||||
installer: Unraid, Docker, Kubernetes, and generic host choices may share the
|
||||
same unified agent installer, but their visible title, recommendation copy,
|
||||
preferred install profile, and first command section must match the selected
|
||||
system instead of falling back to an all-platform generic agent screen.
|
||||
Agent runtime normalization must use that same governed host-profile manifest:
|
||||
profile identity tokens and runtime platform fallback values such as
|
||||
`unraid` -> `linux` are generated into the runtime resolver instead of being
|
||||
|
||||
@@ -548,7 +548,10 @@ prompt explain the same operator-facing priority.
|
||||
The picker must keep that first choice in recognizable system/service
|
||||
vocabulary, while the typed add dialog may use the shared source-strategy
|
||||
vocabulary after selection to explain API inventory, Agent telemetry, or
|
||||
API + Agent coverage.
|
||||
API + Agent coverage. Agent-backed typed add routes keep the same governed
|
||||
dialog shell, but their embedded installer surface should stay focused on
|
||||
the selected system so the first visible command path does not re-expand
|
||||
into irrelevant platform choices.
|
||||
Credential slots are dispatched by the detected or manually-selected type
|
||||
and must still reach the canonical form body rather than diverging into a
|
||||
revived provider-specific workspace.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Component } from 'solid-js';
|
||||
import { For, Show, createSignal } from 'solid-js';
|
||||
import { For, Show, createEffect, createMemo, createSignal } from 'solid-js';
|
||||
import { FormSelect } from '@/components/shared/FormSelect';
|
||||
import SettingsPanel from '@/components/shared/SettingsPanel';
|
||||
import { copyToClipboard } from '@/utils/clipboard';
|
||||
@@ -12,20 +12,145 @@ import {
|
||||
} from '@/utils/unifiedAgentInventoryPresentation';
|
||||
import {
|
||||
INSTALL_PROFILE_OPTIONS,
|
||||
type AgentPlatform,
|
||||
type InfrastructureCommandSection,
|
||||
type InstallProfile,
|
||||
} from './infrastructureOperationsModel';
|
||||
import { useInfrastructureOperationsContext } from './useInfrastructureOperationsState';
|
||||
|
||||
export const InfrastructureInstallerSection: Component = () => {
|
||||
export type InfrastructureInstallerFocus =
|
||||
| 'agent'
|
||||
| 'linux-host'
|
||||
| 'unraid'
|
||||
| 'docker'
|
||||
| 'kubernetes';
|
||||
|
||||
interface InfrastructureInstallerSectionProps {
|
||||
focus?: InfrastructureInstallerFocus;
|
||||
}
|
||||
|
||||
type InfrastructureCommandSectionWithPlatform = InfrastructureCommandSection & {
|
||||
platform: AgentPlatform;
|
||||
};
|
||||
|
||||
type InfrastructureInstallerFocusPresentation = {
|
||||
title: string;
|
||||
description: string;
|
||||
recommendationTitle: string;
|
||||
recommendationDetail: string;
|
||||
preferredProfile: InstallProfile;
|
||||
platforms: readonly AgentPlatform[];
|
||||
};
|
||||
|
||||
const ALL_AGENT_PLATFORMS: readonly AgentPlatform[] = ['linux', 'macos', 'freebsd', 'windows'];
|
||||
|
||||
const INSTALLER_FOCUS_PRESENTATION: Record<
|
||||
InfrastructureInstallerFocus,
|
||||
InfrastructureInstallerFocusPresentation
|
||||
> = {
|
||||
agent: {
|
||||
title: 'Install on a host',
|
||||
description:
|
||||
'Start here to add the first system you want Pulse to monitor, then expand into Docker, Kubernetes, Proxmox, and related infrastructure.',
|
||||
recommendationTitle: 'Recommended install model',
|
||||
recommendationDetail:
|
||||
'Pulse Agent is a low-overhead background service. Install it on each machine where you want full node-local telemetry such as temperatures, SMART disk health, services, Docker, or Kubernetes coverage. For Proxmox clusters, keep the cluster API connection for platform inventory and add the agent to each node for host-level augmentation.',
|
||||
preferredProfile: 'auto',
|
||||
platforms: ALL_AGENT_PLATFORMS,
|
||||
},
|
||||
'linux-host': {
|
||||
title: 'Install on a host',
|
||||
description:
|
||||
'Choose the command for the operating system on the machine you want Pulse to monitor.',
|
||||
recommendationTitle: 'Host install path',
|
||||
recommendationDetail:
|
||||
'Install Pulse Agent on the machine itself. Pulse will collect host telemetry, services, SMART disk health, sensors, and network metrics from that host after it checks in.',
|
||||
preferredProfile: 'auto',
|
||||
platforms: ALL_AGENT_PLATFORMS,
|
||||
},
|
||||
unraid: {
|
||||
title: 'Install on Unraid',
|
||||
description:
|
||||
'Run the Linux installer from the Unraid terminal or SSH session on the server you want Pulse to monitor.',
|
||||
recommendationTitle: 'Unraid install path',
|
||||
recommendationDetail:
|
||||
'Install Pulse Agent directly on the Unraid server. Pulse will classify the reporting host as Unraid from the agent host profile and collect array health, disks, SMART, services, Docker containers, and host telemetry.',
|
||||
preferredProfile: 'auto',
|
||||
platforms: ['linux'],
|
||||
},
|
||||
docker: {
|
||||
title: 'Install on the Docker host',
|
||||
description:
|
||||
'Run the installer on the machine that runs Docker or Podman so Pulse can discover containers from that host.',
|
||||
recommendationTitle: 'Docker install path',
|
||||
recommendationDetail:
|
||||
'Install Pulse Agent on the Docker or Podman host. The Docker profile is selected for this flow so copied commands force container monitoring when automatic detection is restricted.',
|
||||
preferredProfile: 'docker',
|
||||
platforms: ['linux'],
|
||||
},
|
||||
kubernetes: {
|
||||
title: 'Install on a Kubernetes node',
|
||||
description:
|
||||
'Run the installer on a cluster node that should report Kubernetes workload context and host telemetry.',
|
||||
recommendationTitle: 'Kubernetes install path',
|
||||
recommendationDetail:
|
||||
'Install Pulse Agent on a Kubernetes node. The Kubernetes profile is selected for this flow so copied commands enable workload context while preserving node-local telemetry.',
|
||||
preferredProfile: 'kubernetes',
|
||||
platforms: ['linux'],
|
||||
},
|
||||
};
|
||||
|
||||
const getCommandSectionTitle = (
|
||||
section: InfrastructureCommandSectionWithPlatform,
|
||||
focus: InfrastructureInstallerFocus,
|
||||
): string => {
|
||||
if (section.platform !== 'linux') return section.title;
|
||||
if (focus === 'unraid') return 'Run on Unraid';
|
||||
if (focus === 'docker') return 'Run on the Docker host';
|
||||
if (focus === 'kubernetes') return 'Run on a Kubernetes node';
|
||||
return section.title;
|
||||
};
|
||||
|
||||
const getCommandSectionDescription = (
|
||||
section: InfrastructureCommandSectionWithPlatform,
|
||||
focus: InfrastructureInstallerFocus,
|
||||
): string => {
|
||||
if (section.platform !== 'linux') return section.description;
|
||||
if (focus === 'unraid') {
|
||||
return 'Use the Linux installer from Unraid terminal or SSH. The agent stores its local uninstall helper under the Unraid plugin path.';
|
||||
}
|
||||
if (focus === 'docker') {
|
||||
return 'Use the Linux installer on the Docker or Podman host. Commands in this flow include the Docker profile.';
|
||||
}
|
||||
if (focus === 'kubernetes') {
|
||||
return 'Use the Linux installer on a Kubernetes node. Commands in this flow include the Kubernetes profile.';
|
||||
}
|
||||
return section.description;
|
||||
};
|
||||
|
||||
export const InfrastructureInstallerSection: Component<InfrastructureInstallerSectionProps> = (
|
||||
props,
|
||||
) => {
|
||||
const state = useInfrastructureOperationsContext();
|
||||
const [showAdvancedOptions, setShowAdvancedOptions] = createSignal(false);
|
||||
const focus = createMemo<InfrastructureInstallerFocus>(() => props.focus ?? 'agent');
|
||||
const presentation = createMemo(() => INSTALLER_FOCUS_PRESENTATION[focus()]);
|
||||
const commandSections = createMemo(() =>
|
||||
state
|
||||
.commandSections()
|
||||
.filter((section) => presentation().platforms.includes(section.platform)),
|
||||
);
|
||||
|
||||
createEffect(() => {
|
||||
state.handleInstallProfileChange(presentation().preferredProfile);
|
||||
});
|
||||
|
||||
return (
|
||||
<SettingsPanel
|
||||
title={state.isEmbedded() ? 'Install on a host' : 'Infrastructure'}
|
||||
title={state.isEmbedded() ? presentation().title : 'Infrastructure'}
|
||||
description={
|
||||
state.isEmbedded()
|
||||
? 'Start here to add the first system you want Pulse to monitor, then expand into Docker, Kubernetes, Proxmox, and related infrastructure.'
|
||||
? presentation().description
|
||||
: 'Primary setup hub for installing Pulse on the first host you want to monitor, then expanding into Docker, Kubernetes, Proxmox, and related infrastructure.'
|
||||
}
|
||||
bodyClass="space-y-5"
|
||||
@@ -139,12 +264,9 @@ export const InfrastructureInstallerSection: Component = () => {
|
||||
|
||||
<div class="space-y-5">
|
||||
<div class="rounded-md border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm text-emerald-950 dark:border-emerald-700 dark:bg-emerald-900 dark:text-emerald-50">
|
||||
<p class="font-semibold">Recommended install model</p>
|
||||
<p class="font-semibold">{presentation().recommendationTitle}</p>
|
||||
<p class="mt-1 text-xs text-emerald-800 dark:text-emerald-200">
|
||||
Pulse Agent is a low-overhead background service. Install it on each machine where you
|
||||
want full node-local telemetry such as temperatures, SMART disk health, services,
|
||||
Docker, or Kubernetes coverage. For Proxmox clusters, keep the cluster API connection
|
||||
for platform inventory and add the agent to each node for host-level augmentation.
|
||||
{presentation().recommendationDetail}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -260,12 +382,16 @@ export const InfrastructureInstallerSection: Component = () => {
|
||||
</p>
|
||||
</div>
|
||||
<div class="grid gap-3 lg:grid-cols-2">
|
||||
<For each={state.commandSections()}>
|
||||
<For each={commandSections()}>
|
||||
{(section) => (
|
||||
<div class="rounded-md border border-border bg-surface px-3 py-3">
|
||||
<div class="space-y-1">
|
||||
<h5 class="text-sm font-semibold text-base-content">{section.title}</h5>
|
||||
<p class="text-xs text-muted">{section.description}</p>
|
||||
<h5 class="text-sm font-semibold text-base-content">
|
||||
{getCommandSectionTitle(section, focus())}
|
||||
</h5>
|
||||
<p class="text-xs text-muted">
|
||||
{getCommandSectionDescription(section, focus())}
|
||||
</p>
|
||||
</div>
|
||||
<div class="mt-3 flex flex-wrap gap-2">
|
||||
<For each={section.snippets}>
|
||||
@@ -467,12 +593,16 @@ export const InfrastructureInstallerSection: Component = () => {
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<For each={state.commandSections()}>
|
||||
<For each={commandSections()}>
|
||||
{(section) => (
|
||||
<div class="space-y-3 rounded-md border border-border p-4">
|
||||
<div class="space-y-1">
|
||||
<h5 class="text-sm font-semibold text-base-content">{section.title}</h5>
|
||||
<p class="text-xs text-muted">{section.description}</p>
|
||||
<h5 class="text-sm font-semibold text-base-content">
|
||||
{getCommandSectionTitle(section, focus())}
|
||||
</h5>
|
||||
<p class="text-xs text-muted">
|
||||
{getCommandSectionDescription(section, focus())}
|
||||
</p>
|
||||
</div>
|
||||
<div class="space-y-3">
|
||||
<For each={section.snippets}>
|
||||
|
||||
@@ -18,7 +18,10 @@ import type { TrueNASConnection } from '@/api/truenas';
|
||||
import type { VMwareConnection } from '@/api/vmware';
|
||||
import type { NodeConfig, NodeConfigWithStatus } from '@/types/nodes';
|
||||
import { InfrastructureDiscoverySettingsDialog } from './InfrastructureDiscoverySettingsDialog';
|
||||
import { InfrastructureInstallerSection } from './InfrastructureInstallerSection';
|
||||
import {
|
||||
InfrastructureInstallerSection,
|
||||
type InfrastructureInstallerFocus,
|
||||
} from './InfrastructureInstallerSection';
|
||||
import { InfrastructureSourceManager } from './InfrastructureSourceManager';
|
||||
import { InfrastructureSourcePicker } from './InfrastructureSourcePicker';
|
||||
import {
|
||||
@@ -71,6 +74,16 @@ const ADD_STEP_TO_TYPE: Record<ManagedAddTypeStep, ConnectionType> = {
|
||||
vmware: 'vmware',
|
||||
};
|
||||
|
||||
const ADD_STEP_TO_INSTALLER_FOCUS: Partial<
|
||||
Record<ManagedAddTypeStep, InfrastructureInstallerFocus>
|
||||
> = {
|
||||
agent: 'agent',
|
||||
'linux-host': 'linux-host',
|
||||
unraid: 'unraid',
|
||||
docker: 'docker',
|
||||
kubernetes: 'kubernetes',
|
||||
};
|
||||
|
||||
const closeButtonClass =
|
||||
'inline-flex h-9 w-9 items-center justify-center rounded-md border border-border text-base-content transition-colors hover:bg-surface-hover';
|
||||
const buttonClass =
|
||||
@@ -126,6 +139,11 @@ const InfrastructureWorkspaceContent: Component<InfrastructureWorkspaceProps> =
|
||||
const activeCatalogItem = createMemo(() =>
|
||||
getInfrastructureSourcePickerItemForRouteStep(routeStep()),
|
||||
);
|
||||
const activeInstallerFocus = createMemo<InfrastructureInstallerFocus>(() => {
|
||||
const step = routeStep();
|
||||
if (!step || step === 'pick' || step === 'detect') return 'agent';
|
||||
return ADD_STEP_TO_INSTALLER_FOCUS[step as ManagedAddTypeStep] ?? 'agent';
|
||||
});
|
||||
const editingConnection = createMemo<Connection | null>(() => editingRow()?.connection ?? null);
|
||||
const attachedAgentConnections = createMemo(() =>
|
||||
(editingRow()?.attachedConnections ?? []).filter((connection) => connection.type === 'agent'),
|
||||
@@ -337,7 +355,7 @@ const InfrastructureWorkspaceContent: Component<InfrastructureWorkspaceProps> =
|
||||
<AgentProfilesPanel />
|
||||
</div>
|
||||
</Show>
|
||||
<InfrastructureInstallerSection />
|
||||
<InfrastructureInstallerSection focus={activeInstallerFocus()} />
|
||||
</div>
|
||||
);
|
||||
|
||||
|
||||
+17
-8
@@ -222,14 +222,23 @@ describe('infrastructure operations model', () => {
|
||||
|
||||
it('keeps the embedded installer section on the canonical host-install framing', () => {
|
||||
expect(infrastructureInstallerSectionSource).toContain(
|
||||
"title={state.isEmbedded() ? 'Install on a host' : 'Infrastructure'}",
|
||||
"title={state.isEmbedded() ? presentation().title : 'Infrastructure'}",
|
||||
);
|
||||
expect(infrastructureInstallerSectionSource).toContain('Install on Unraid');
|
||||
expect(infrastructureInstallerSectionSource).toContain('Run on Unraid');
|
||||
expect(infrastructureInstallerSectionSource).toContain('Install on the Docker host');
|
||||
expect(infrastructureInstallerSectionSource).toContain('Install on a Kubernetes node');
|
||||
expect(infrastructureInstallerSectionSource).toContain(
|
||||
'state.handleInstallProfileChange(presentation().preferredProfile)',
|
||||
);
|
||||
expect(infrastructureInstallerSectionSource).toContain('Generate install token');
|
||||
expect(infrastructureInstallerSectionSource).toContain('Generate token');
|
||||
expect(infrastructureInstallerSectionSource).toContain(
|
||||
'This is the Pulse Agent handoff from first-run setup inside Add infrastructure.',
|
||||
);
|
||||
expect(infrastructureInstallerSectionSource).toContain('Pulse Agent is a low-overhead background service.');
|
||||
expect(infrastructureInstallerSectionSource).toContain(
|
||||
'Pulse Agent is a low-overhead background service.',
|
||||
);
|
||||
expect(infrastructureInstallerSectionSource).toContain(
|
||||
'For Proxmox clusters, keep the cluster API',
|
||||
);
|
||||
@@ -285,16 +294,16 @@ describe('infrastructure operations model', () => {
|
||||
});
|
||||
|
||||
it('does not reintroduce the retired reporting state hook on the operations state', async () => {
|
||||
const operationsStateSource = await import(
|
||||
'../useInfrastructureOperationsState?raw'
|
||||
).then((mod) => (mod as { default: string }).default);
|
||||
const operationsStateSource = await import('../useInfrastructureOperationsState?raw').then(
|
||||
(mod) => (mod as { default: string }).default,
|
||||
);
|
||||
expect(operationsStateSource).not.toContain('useInfrastructureReportingState');
|
||||
});
|
||||
|
||||
it('keeps discovered-node filtering anchored to canonical represented-host dedupe', async () => {
|
||||
const discoveryStateSource = await import(
|
||||
'../useInfrastructureDiscoveryRuntimeState?raw'
|
||||
).then((mod) => (mod as { default: string }).default);
|
||||
const discoveryStateSource = await import('../useInfrastructureDiscoveryRuntimeState?raw').then(
|
||||
(mod) => (mod as { default: string }).default,
|
||||
);
|
||||
expect(discoveryStateSource).toContain('filterRepresentedDiscoveredServers');
|
||||
expect(discoveryStateSource).toContain('nodes()');
|
||||
});
|
||||
|
||||
@@ -133,7 +133,11 @@ vi.mock('../useConnectionsLedger', () => ({
|
||||
}));
|
||||
|
||||
vi.mock('../InfrastructureInstallerSection', () => ({
|
||||
InfrastructureInstallerSection: () => <div data-testid="install-section">install</div>,
|
||||
InfrastructureInstallerSection: (props: { focus?: string }) => (
|
||||
<div data-testid="install-section" data-focus={props.focus}>
|
||||
install
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock('../ConnectionEditor/CredentialSlots/NodeCredentialSlot', () => ({
|
||||
@@ -617,9 +621,7 @@ describe('InfrastructureWorkspace', () => {
|
||||
const dialog = screen.getByRole('dialog');
|
||||
expect(screen.getAllByText('Add infrastructure').length).toBeGreaterThan(0);
|
||||
expect(
|
||||
screen.getByText(
|
||||
'Choose the system, platform, host, or service you want Pulse to monitor.',
|
||||
),
|
||||
screen.getByText('Choose the system, platform, host, or service you want Pulse to monitor.'),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
within(dialog).getByPlaceholderText('Search Unraid, TrueNAS, Proxmox, Docker...'),
|
||||
@@ -686,6 +688,16 @@ describe('InfrastructureWorkspace', () => {
|
||||
await waitFor(() => expect(screen.getByRole('dialog')).toBeInTheDocument());
|
||||
expect(screen.getAllByText('Add Pulse Agent').length).toBeGreaterThan(0);
|
||||
expect(screen.getByTestId('install-section')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('install-section')).toHaveAttribute('data-focus', 'agent');
|
||||
});
|
||||
|
||||
it('tailors agent-backed catalog routes to the selected system', async () => {
|
||||
routeState.search = '?add=unraid';
|
||||
renderWorkspace();
|
||||
|
||||
await waitFor(() => expect(screen.getByRole('dialog')).toBeInTheDocument());
|
||||
expect(screen.getAllByText('Add Unraid').length).toBeGreaterThan(0);
|
||||
expect(screen.getByTestId('install-section')).toHaveAttribute('data-focus', 'unraid');
|
||||
});
|
||||
|
||||
it('renders a direct type route before the add dialog mounts', async () => {
|
||||
|
||||
Reference in New Issue
Block a user