feat: replace linkedResourceId text input with resource picker dropdown

The 'Link to resource' field in the availability target form was a
plain text input requiring the user to know and type a resource ID.
Replace it with a FormSelect dropdown that lists all known resources
grouped by platform (Agent, Docker, PVE, PBS, etc.), with resource
type labels in parentheses for context.

Default option remains 'Auto-detect by IP (recommended)'. A fallback
option appears for linked resources not currently in the discovery
list.

Also fixes a pre-existing platformType typo ('proxmox' → 'proxmox-pve')
in the availability probe presentation test.
This commit is contained in:
rcourtman 2026-06-26 12:38:54 +01:00
parent 62c2b765d0
commit fd4cb9c8d5
3 changed files with 81 additions and 15 deletions

View file

@ -1,4 +1,4 @@
import { Component, Show, createSignal, onMount } from 'solid-js';
import { Component, For, Show, createMemo, createSignal, onMount } from 'solid-js';
import { Button } from '@/components/shared/Button';
import { CalloutCard } from '@/components/shared/CalloutCard';
import {
@ -23,6 +23,11 @@ import {
availabilityPresetById,
type AvailabilityTargetPresetID,
} from '../availabilityTargetPresets';
import { useResources } from '@/hooks/useResources';
import { getSourcePlatformLabel } from '@/utils/sourcePlatforms';
import { getPreferredInfrastructureDisplayName } from '@/utils/resourceIdentity';
import { getResourceTypeLabel } from '@/utils/resourceTypePresentation';
import type { Resource } from '@/types/resource';
interface AvailabilityForm {
id: string;
@ -121,6 +126,35 @@ const initialPresetForTargetKind = (
targetKind === 'machine' ? 'ping-machine' : CUSTOM_AVAILABILITY_PRESET_ID;
export const AvailabilityTargetSlot: Component<AvailabilityTargetSlotProps> = (props) => {
const { resources } = useResources();
const linkableResources = createMemo<Resource[]>(() =>
resources().filter((r) => r.type !== 'network-endpoint'),
);
const groupedLinkableResources = createMemo(() => {
const groups = new Map<string, Resource[]>();
for (const r of linkableResources()) {
const platform = r.platformType || 'generic';
const list = groups.get(platform);
if (list) {
list.push(r);
} else {
groups.set(platform, [r]);
}
}
for (const list of groups.values()) {
list.sort((a, b) =>
getPreferredInfrastructureDisplayName(a).localeCompare(
getPreferredInfrastructureDisplayName(b),
),
);
}
return [...groups.entries()].sort((a, b) =>
getSourcePlatformLabel(a[0]).localeCompare(getSourcePlatformLabel(b[0])),
);
});
const [form, setForm] = createSignal<AvailabilityForm>(
newAvailabilityForm(props.initialTargetKind),
);
@ -133,6 +167,12 @@ export const AvailabilityTargetSlot: Component<AvailabilityTargetSlotProps> = (p
const [error, setError] = createSignal<string | null>(null);
const [testResult, setTestResult] = createSignal<AvailabilityTestResponse | null>(null);
const linkedResourceMissing = createMemo(() => {
const id = form().linkedResourceId.trim();
if (!id) return false;
return !linkableResources().some((r) => r.id === id);
});
const updateForm = (patch: Partial<AvailabilityForm>, preservePreset = false) => {
setForm((current) => ({ ...current, ...patch }));
if (
@ -308,19 +348,39 @@ export const AvailabilityTargetSlot: Component<AvailabilityTargetSlotProps> = (p
: 'Use a full URL or a hostname. HTTP statuses below 500 count as reachable.'}
</span>
</label>
<label class={`${formField} sm:col-span-2`}>
<span class={formLabel}>Link to resource (optional)</span>
<input
class={formControl}
value={form().linkedResourceId}
onInput={(event) => updateForm({ linkedResourceId: event.currentTarget.value })}
placeholder="auto-detect by IP"
/>
<span class={formHelpText}>
Link this check to a known resource so its status appears on that
resource's row. Leave empty to auto-detect by IP address.
</span>
</label>
<FormSelect
label="Link to resource (optional)"
value={form().linkedResourceId}
onChange={(event) =>
updateForm({ linkedResourceId: event.currentTarget.value })
}
fieldClass="sm:col-span-2"
help="Link this check to a known resource so its status appears on that resource's row. Leave empty to auto-detect by IP address."
>
<option value="">Auto-detect by IP (recommended)</option>
<Show when={linkedResourceMissing()}>
<option value={form().linkedResourceId}>
{form().linkedResourceId} (not currently discovered)
</option>
</Show>
<For each={groupedLinkableResources()}>
{([platform, items]) => (
<optgroup label={getSourcePlatformLabel(platform)}>
<For each={items}>
{(resource) => {
const typeLabel = getResourceTypeLabel(resource.type);
return (
<option value={resource.id}>
{getPreferredInfrastructureDisplayName(resource)}
{typeLabel ? ` (${typeLabel})` : ''}
</option>
);
}}
</For>
</optgroup>
)}
</For>
</FormSelect>
<Show when={form().protocol !== 'icmp'}>
<label class={formField}>
<span class={formLabel}>Port</span>

View file

@ -14,6 +14,12 @@ vi.mock('@/api/availabilityTargets', () => ({
},
}));
vi.mock('@/hooks/useResources', () => ({
useResources: () => ({
resources: () => [],
}),
}));
const mockedCreate = vi.mocked(AvailabilityTargetsAPI.create);
describe('AvailabilityTargetSlot', () => {

View file

@ -141,7 +141,7 @@ describe('availabilityProbePresentation', () => {
makeAvailabilityResource({
id: 'vm:100',
type: 'vm',
platformType: 'proxmox',
platformType: 'proxmox-pve',
platformId: 'proxmox-ve',
sourceType: 'agent',
status: 'online',