Move Install agent attention from global callout to row-level chips

The 'Install agents' recommendation rendered above the table as a
secondary button + paragraph: 'Install Pulse Agent on pbs-docker
when you want node-local telemetry...'. The user had to read the
paragraph and then map the named system back to the table row.

Surface the same attention directly on the affected row instead.
For systems with API coverage but no Pulse Agent, render a small
'+ Install agent' chip in the actions column above the Manage
button. The chip routes to the same agent-install picker the global
button used.

Hide the global recommendation card when its kind is 'agent' so the
two affordances don't duplicate each other. Other recommendation
states (discovery scan, coverage coherent, add infrastructure on
empty) still render as before.
This commit is contained in:
rcourtman
2026-05-09 14:49:37 +01:00
parent cf2e61ea22
commit deb2950d28
2 changed files with 60 additions and 18 deletions
@@ -610,7 +610,17 @@ export const InfrastructureSourceManager: Component<InfrastructureSourceManagerP
)}
</For>
</dl>
<Show when={!props.readOnly && Boolean(setupConfidenceAction().onClick)}>
{/* Recommendation button hides for the apiOnly install-agents case
because row-level install-agent chips now surface the same
attention per-system. Other recommendations (discovery scan,
coverage coherent, add infrastructure) still render here. */}
<Show
when={
!props.readOnly &&
Boolean(setupConfidenceAction().onClick) &&
setupConfidenceAction().kind !== 'agent'
}
>
<button
type="button"
onClick={() => setupConfidenceAction().onClick?.()}
@@ -623,7 +633,9 @@ export const InfrastructureSourceManager: Component<InfrastructureSourceManagerP
</Show>
</div>
<p class="mt-3 text-xs leading-5 text-muted">{setupConfidenceAction().detail}</p>
<Show when={setupConfidenceAction().kind !== 'agent'}>
<p class="mt-3 text-xs leading-5 text-muted">{setupConfidenceAction().detail}</p>
</Show>
</section>
);
@@ -849,13 +861,37 @@ export const InfrastructureSourceManager: Component<InfrastructureSourceManagerP
<span class="text-xs text-muted">Read only</span>
}
>
<button
type="button"
onClick={() => props.onOpenConnection?.(row)}
class={inlineButtonClass}
>
Manage
</button>
<div class="flex flex-col items-end gap-1">
{/* Row-level install-agent shortcut: the
apiOnly attention state lands directly on
the affected system instead of the user
reading a global callout and figuring out
which row it referred to. */}
<Show
when={
rowHasApiCoverage(row) &&
!rowHasAgentCoverage(row) &&
Boolean(props.onAddSource)
}
>
<button
type="button"
onClick={() => props.onAddSource?.('agent')}
class="inline-flex items-center gap-1 rounded px-2 py-0.5 text-xs font-medium text-blue-700 transition-colors hover:bg-blue-50 dark:text-blue-300 dark:hover:bg-blue-950/30"
title="Install Pulse Agent on this system to add node-local telemetry (temperatures, SMART, host identity)."
>
<Plus class="h-3 w-3" />
Install agent
</button>
</Show>
<button
type="button"
onClick={() => props.onOpenConnection?.(row)}
class={inlineButtonClass}
>
Manage
</button>
</div>
</Show>
</TableCell>
</Show>
@@ -289,7 +289,12 @@ describe('InfrastructureWorkspace', () => {
expect(screen.getByRole('button', { name: /Discovery settings/i })).toBeInTheDocument();
expect(screen.getByRole('button', { name: /^Add infrastructure$/i })).toBeInTheDocument();
expect(screen.queryByRole('button', { name: /^Detect address$/i })).toBeNull();
expect(screen.queryByRole('button', { name: /^Install agent$/i })).toBeNull();
// Row-level 'Install agent' surfaces per system that has API coverage
// but no Pulse Agent yet. The fixture has one such system, so at least
// one of these buttons should exist.
expect(
screen.getAllByRole('button', { name: /^Install agent$/i }).length,
).toBeGreaterThan(0);
const readiness = screen.getByRole('region', {
name: /Infrastructure setup summary/i,
});
@@ -304,7 +309,12 @@ describe('InfrastructureWorkspace', () => {
expect(within(readiness).getAllByText('1 system').length).toBeGreaterThan(0);
expect(within(readiness).getAllByText('0 systems').length).toBeGreaterThan(0);
expect(within(readiness).getByText('Discovery off')).toBeInTheDocument();
expect(within(readiness).getByRole('button', { name: /Install agents/i })).toBeInTheDocument();
// Global 'Install agents' recommendation button is hidden when
// row-level 'Install agent' chips already surface the apiOnly state
// per-system.
expect(
within(readiness).queryByRole('button', { name: /Install agents/i }),
).not.toBeInTheDocument();
expect(screen.getByText('Proxmox VE')).toBeInTheDocument();
expect(screen.getByText('Proxmox VE').closest('tr')?.className).toContain('grouped-table-row');
expect(screen.queryByText('VMware vCenter')).toBeNull();
@@ -322,13 +332,9 @@ describe('InfrastructureWorkspace', () => {
await waitFor(() => expect(screen.getByText('Infrastructure systems')).toBeInTheDocument());
fireEvent.click(
within(
screen.getByRole('region', {
name: /Infrastructure setup summary/i,
}),
).getByRole('button', { name: /Install agents/i }),
);
// Row-level 'Install agent' replaced the global 'Install agents'
// recommendation button; same routing target.
fireEvent.click(screen.getAllByRole('button', { name: /^Install agent$/i })[0]);
expect(navigateSpy).toHaveBeenLastCalledWith('/settings/infrastructure?add=agent', {
scroll: false,
});