mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-22 19:56:01 +00:00
Polish four remaining add-infrastructure friction points
After verifying the previous picker promotion in the browser, four specific frictions still showed up: - Picker rendered all 10 platform cards by default, forcing users to scroll past tail platforms (PBS, PMG, Docker, Kubernetes, VMware) on a typical viewport. Add a popular set (PVE, TrueNAS, Unraid, Linux host, Network endpoint) and a 'Show 5 more platforms' disclosure for the rest. Search bypasses the split. - PVE Coverage step had both 'Monitoring coverage' (descriptive paragraph) and 'Collection scope' (toggles + their own paragraph), describing overlapping ideas in two stacked sections. Hide the 'Monitoring coverage' header on PVE and fold its framing into the Collection scope description so the section reads as one. PBS and PMG keep the 'Monitoring coverage' header since they have no toggles. - PVE Authentication tab read 'API Inventory (Recommended)', surfacing internal lane-stage vocabulary. Rename to 'Connect via API'. - Per-platform dialog subtitle concatenated strategy + bestFor + coverage. Coverage repeated the green install-path explanation card inside the dialog body, putting the telemetry list in two places on one screen. Drop coverage from the subtitle; the green card retains the detailed list.
This commit is contained in:
@@ -51,6 +51,18 @@ const CARD_ICON: Record<InfrastructureSourcePickerItemId, Component<{ class?: st
|
||||
availability: Activity,
|
||||
};
|
||||
|
||||
// Popular platforms shown by default. The rest are hidden behind 'Show more
|
||||
// platforms' to keep the picker scannable at a glance and let users scroll
|
||||
// to fewer cards before deciding. Search bypasses this split — searching
|
||||
// for any platform surfaces it regardless of popular status.
|
||||
const POPULAR_PICKER_ITEM_IDS: ReadonlySet<InfrastructureSourcePickerItemId> = new Set([
|
||||
'pve',
|
||||
'truenas',
|
||||
'unraid',
|
||||
'linux-host',
|
||||
'availability',
|
||||
]);
|
||||
|
||||
const itemMatchesQuery = (item: InfrastructureSourcePickerItemPresentation, query: string) => {
|
||||
if (!query) return true;
|
||||
const searchableText = [
|
||||
@@ -72,11 +84,24 @@ const itemMatchesQuery = (item: InfrastructureSourcePickerItemPresentation, quer
|
||||
|
||||
export const InfrastructureSourcePicker: Component<InfrastructureSourcePickerProps> = (props) => {
|
||||
const [query, setQuery] = createSignal('');
|
||||
const [showAllPlatforms, setShowAllPlatforms] = createSignal(false);
|
||||
const items = () => getInfrastructureSourcePickerItems();
|
||||
const normalizedQuery = createMemo(() => query().trim().toLowerCase());
|
||||
const visibleItems = createMemo(() =>
|
||||
const matchedItems = createMemo(() =>
|
||||
items().filter((item) => itemMatchesQuery(item, normalizedQuery())),
|
||||
);
|
||||
// When the user is searching, show every match regardless of popular
|
||||
// status. Otherwise gate the long tail behind a 'Show more platforms'
|
||||
// disclosure so the default scan is short.
|
||||
const visibleItems = createMemo(() => {
|
||||
if (normalizedQuery() || showAllPlatforms()) return matchedItems();
|
||||
return matchedItems().filter((item) => POPULAR_PICKER_ITEM_IDS.has(item.id));
|
||||
});
|
||||
const hiddenCount = createMemo(() =>
|
||||
normalizedQuery() || showAllPlatforms()
|
||||
? 0
|
||||
: matchedItems().filter((item) => !POPULAR_PICKER_ITEM_IDS.has(item.id)).length,
|
||||
);
|
||||
const heading = createMemo(() =>
|
||||
normalizedQuery() ? 'Matching choices' : 'Or pick a specific platform',
|
||||
);
|
||||
@@ -196,6 +221,15 @@ export const InfrastructureSourcePicker: Component<InfrastructureSourcePickerPro
|
||||
}}
|
||||
</For>
|
||||
</div>
|
||||
<Show when={hiddenCount() > 0}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowAllPlatforms(true)}
|
||||
class="mt-1 inline-flex items-center text-xs font-medium text-blue-700 hover:text-blue-900 hover:underline dark:text-blue-300 dark:hover:text-blue-100"
|
||||
>
|
||||
Show {hiddenCount()} more platform{hiddenCount() === 1 ? '' : 's'}
|
||||
</button>
|
||||
</Show>
|
||||
</Show>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@@ -850,13 +850,17 @@ const InfrastructureWorkspaceContent: Component<InfrastructureWorkspaceProps> =
|
||||
const catalogItem = activeCatalogItem();
|
||||
if (catalogItem) {
|
||||
const strategy = getInfrastructureSourceStrategyPresentation(catalogItem.sourceStrategy);
|
||||
return `${strategy.label}. ${trimSentenceTerminal(catalogItem.bestFor)}. ${trimSentenceTerminal(catalogItem.coverage)}.`;
|
||||
// Subtitle is just strategy + bestFor. The detailed coverage (telemetry
|
||||
// breakdown) is repeated by the green install-path explanation card
|
||||
// inside the dialog body, so dropping it here removes a redundant
|
||||
// sentence from a header the user reads first.
|
||||
return `${strategy.label}. ${trimSentenceTerminal(catalogItem.bestFor)}.`;
|
||||
}
|
||||
const presentation = getInfrastructureOnboardingProductPresentation(
|
||||
activeAddType() as InfrastructureOnboardingConnectionType,
|
||||
);
|
||||
const strategy = getInfrastructureSourceStrategyPresentation(presentation.sourceStrategy);
|
||||
return `${strategy.label}. ${trimSentenceTerminal(presentation.bestFor)}. ${trimSentenceTerminal(presentation.coverage)}.`;
|
||||
return `${strategy.label}. ${trimSentenceTerminal(presentation.bestFor)}.`;
|
||||
});
|
||||
|
||||
const editDialogTitle = createMemo(() => {
|
||||
|
||||
@@ -55,15 +55,21 @@ export const NodeModalMonitoringSection: Component<NodeModalMonitoringSectionPro
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<SectionHeader
|
||||
title="Monitoring coverage"
|
||||
size="sm"
|
||||
class="mb-2"
|
||||
titleClass="text-base-content"
|
||||
/>
|
||||
<p class="text-sm text-muted">{getNodeMonitoringCoverageCopy(modalProps.nodeType)}</p>
|
||||
</div>
|
||||
{/* For PVE the Collection scope section below carries both the
|
||||
coverage description and the per-collector toggles, so the
|
||||
standalone Monitoring coverage paragraph would be redundant. PBS
|
||||
and PMG keep the standalone paragraph because they have no toggles. */}
|
||||
<Show when={modalProps.nodeType !== 'pve'}>
|
||||
<div>
|
||||
<SectionHeader
|
||||
title="Monitoring coverage"
|
||||
size="sm"
|
||||
class="mb-2"
|
||||
titleClass="text-base-content"
|
||||
/>
|
||||
<p class="text-sm text-muted">{getNodeMonitoringCoverageCopy(modalProps.nodeType)}</p>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Show when={modalProps.nodeType === 'pve'}>
|
||||
<div class="space-y-3">
|
||||
@@ -73,9 +79,10 @@ export const NodeModalMonitoringSection: Component<NodeModalMonitoringSectionPro
|
||||
class="mb-1"
|
||||
titleClass="text-base-content"
|
||||
/>
|
||||
<p class="text-xs text-muted">
|
||||
Control which Proxmox VE resources Pulse ingests. Disable individual collectors to
|
||||
reduce API load or trim workload noise.
|
||||
<p class="text-sm text-muted">
|
||||
Pulse tracks all supported Proxmox VE resources by default — virtual machines,
|
||||
containers, storage, and backups — so you always get full visibility without extra
|
||||
configuration. Disable individual collectors below to reduce API load or trim workload noise.
|
||||
</p>
|
||||
|
||||
<label class="flex items-start gap-2 text-sm text-base-content">
|
||||
|
||||
@@ -65,7 +65,7 @@ export const NodeModalSetupGuideSection: Component<NodeModalSetupGuideSectionPro
|
||||
onClick={() => state.updateField('setupMode', 'auto')}
|
||||
class={setupModeButtonClass(state.formData().setupMode === 'auto')}
|
||||
>
|
||||
API Inventory
|
||||
Connect via API
|
||||
<span class="ml-1.5 px-1.5 py-0.5 text-[10px] font-semibold bg-green-100 dark:bg-green-900 text-green-700 dark:text-green-300 rounded">
|
||||
Recommended
|
||||
</span>
|
||||
|
||||
@@ -16,6 +16,10 @@ describe('InfrastructureSourcePicker', () => {
|
||||
// alternative.
|
||||
expect(screen.getByText('Choose how Pulse should connect')).toBeInTheDocument();
|
||||
expect(screen.getByText('Or pick a specific platform')).toBeInTheDocument();
|
||||
// VMware lives behind 'Show more platforms' since it is a less common
|
||||
// homelab choice; expand the long tail to verify the Early support
|
||||
// badge still renders for it.
|
||||
fireEvent.click(screen.getByRole('button', { name: /Show \d+ more platforms?/i }));
|
||||
expect(screen.getByText('Early support')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Available now')).toBeNull();
|
||||
expect(screen.getByText('Unraid')).toBeInTheDocument();
|
||||
|
||||
+2
-1
@@ -47,7 +47,8 @@ describe('NodeModalSetupGuideSection', () => {
|
||||
expect(screen.getByText('API inventory')).toBeInTheDocument();
|
||||
expect(screen.getByText(/Recommended least-privilege path/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/Recommended API inventory path/i)).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: /API Inventory/i })).toBeInTheDocument();
|
||||
// Tab renamed from 'API Inventory' (internal term) to 'Connect via API'.
|
||||
expect(screen.getByRole('button', { name: /Connect via API/i })).toBeInTheDocument();
|
||||
expect(screen.queryByText('Manual API token')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user