diff --git a/frontend-modern/src/components/Settings/ConnectionEditor/CredentialSlots/AvailabilityTargetSlot.tsx b/frontend-modern/src/components/Settings/ConnectionEditor/CredentialSlots/AvailabilityTargetSlot.tsx index 96adcf26d..87898487c 100644 --- a/frontend-modern/src/components/Settings/ConnectionEditor/CredentialSlots/AvailabilityTargetSlot.tsx +++ b/frontend-modern/src/components/Settings/ConnectionEditor/CredentialSlots/AvailabilityTargetSlot.tsx @@ -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 = (props) => { + const { resources } = useResources(); + + const linkableResources = createMemo(() => + resources().filter((r) => r.type !== 'network-endpoint'), + ); + + const groupedLinkableResources = createMemo(() => { + const groups = new Map(); + 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( newAvailabilityForm(props.initialTargetKind), ); @@ -133,6 +167,12 @@ export const AvailabilityTargetSlot: Component = (p const [error, setError] = createSignal(null); const [testResult, setTestResult] = createSignal(null); + const linkedResourceMissing = createMemo(() => { + const id = form().linkedResourceId.trim(); + if (!id) return false; + return !linkableResources().some((r) => r.id === id); + }); + const updateForm = (patch: Partial, preservePreset = false) => { setForm((current) => ({ ...current, ...patch })); if ( @@ -308,19 +348,39 @@ export const AvailabilityTargetSlot: Component = (p : 'Use a full URL or a hostname. HTTP statuses below 500 count as reachable.'} - + + 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." + > + + + + + + {([platform, items]) => ( + + + {(resource) => { + const typeLabel = getResourceTypeLabel(resource.type); + return ( + + ); + }} + + + )} + +