feat: fully functioning ALB
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
import {RefreshCw, Trash2} from 'lucide-react'
|
||||
import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'
|
||||
import {useEffect, useState} from 'react'
|
||||
import {getLoadBalancingDependencies, listFlatItems, putFlatItem, deleteFlatItem} from '@/api/cloudProxyClient'
|
||||
import {useAccountId} from '@/lib/accountStore'
|
||||
import type {CloudResource} from '@/types/resource'
|
||||
|
||||
type Props = {
|
||||
resource?: CloudResource
|
||||
runtimeReachable: boolean
|
||||
}
|
||||
|
||||
type TargetInstance = {id: string; name: string; privateIp: string | null}
|
||||
|
||||
export function TargetGroupPanel({resource, runtimeReachable}: Props) {
|
||||
const accountId = useAccountId()
|
||||
const queryClient = useQueryClient()
|
||||
const [instanceId, setInstanceId] = useState('')
|
||||
const [port, setPort] = useState('')
|
||||
const dependenciesQuery = useQuery({
|
||||
queryKey: ['target-group-instances', accountId],
|
||||
queryFn: ({signal}) => getTargetGroupInstances(signal),
|
||||
enabled: Boolean(resource) && runtimeReachable,
|
||||
})
|
||||
const healthQuery = useQuery({
|
||||
queryKey: ['target-group-health', accountId, resource?.id],
|
||||
queryFn: ({signal}) => listFlatItems('aws', 'targetgroups', resource?.id ?? '', undefined, signal),
|
||||
enabled: Boolean(resource) && runtimeReachable,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
setInstanceId('')
|
||||
setPort(typeof resource?.metadata.port === 'number' ? String(resource.metadata.port) : '')
|
||||
}, [resource?.id, resource?.metadata.port])
|
||||
|
||||
const refresh = () => {
|
||||
void Promise.all([dependenciesQuery.refetch(), healthQuery.refetch()])
|
||||
}
|
||||
const registerMutation = useMutation({
|
||||
mutationFn: () => putFlatItem('aws', 'targetgroups', resource?.id ?? '', {id: instanceId, port: Number(port)}),
|
||||
onSuccess: () => queryClient.invalidateQueries({queryKey: ['target-group-health', accountId, resource?.id]}),
|
||||
})
|
||||
const deregisterMutation = useMutation({
|
||||
mutationFn: (targetKey: string) => deleteFlatItem('aws', 'targetgroups', resource?.id ?? '', targetKey),
|
||||
onSuccess: () => queryClient.invalidateQueries({queryKey: ['target-group-health', accountId, resource?.id]}),
|
||||
})
|
||||
|
||||
if (!resource) {
|
||||
return <section className="table-panel"><div className="empty compact"><h3>Select a target group</h3><p>Choose a target group to register running instances and inspect target health.</p></div></section>
|
||||
}
|
||||
|
||||
const instances = dependenciesQuery.data ?? []
|
||||
const targets = healthQuery.data?.items ?? []
|
||||
const validPort = /^\d+$/.test(port) && Number(port) >= 1 && Number(port) <= 65535
|
||||
const error = healthQuery.error ?? dependenciesQuery.error ?? registerMutation.error ?? deregisterMutation.error
|
||||
return <section className="table-panel" style={{marginTop: 16}}>
|
||||
<div className="input-row resource-table-bar">
|
||||
<div><p className="eyebrow">Target health</p><span className="muted">{targets.length} registered targets in {resource.name}</span></div>
|
||||
<button className="button" type="button" disabled={!runtimeReachable || healthQuery.isFetching} onClick={refresh}><RefreshCw size={14}/>{healthQuery.isFetching ? 'Loading' : 'Refresh'}</button>
|
||||
</div>
|
||||
<form className="dynamic-form" onSubmit={(event) => { event.preventDefault(); if (instanceId && validPort) registerMutation.mutate() }}>
|
||||
<label className="dynamic-field"><span>Running instance</span><select className="input" value={instanceId} onChange={(event) => setInstanceId(event.target.value)} disabled={dependenciesQuery.isLoading || registerMutation.isPending}><option value="">Select an instance</option>{instances.map((instance) => <option key={instance.id} value={instance.id}>{instance.name} ({instance.id}{instance.privateIp ? `, ${instance.privateIp}` : ''})</option>)}</select></label>
|
||||
<label className="dynamic-field"><span>Target port</span><input className="input" type="number" min="1" max="65535" value={port} onChange={(event) => setPort(event.target.value)} disabled={registerMutation.isPending}/></label>
|
||||
<div style={{alignSelf: 'end'}}><button className="button primary" type="submit" disabled={!runtimeReachable || !instanceId || !validPort || registerMutation.isPending}>{registerMutation.isPending ? 'Registering' : 'Register target'}</button></div>
|
||||
{instances.length === 0 && dependenciesQuery.isSuccess && <p className="muted">No running instances are available in this account.</p>}
|
||||
</form>
|
||||
{error instanceof Error && <p className="error-text compact-text" role="alert">{error.message}</p>}
|
||||
{healthQuery.isLoading && <div className="muted padded">Loading target health</div>}
|
||||
{healthQuery.isSuccess && targets.length === 0 && <div className="empty compact"><h3>No registered targets</h3><p>Register a running instance to see its ELB health state.</p></div>}
|
||||
{targets.length > 0 && <div className="cosmos-items-table"><table><thead><tr><th>Instance</th><th>Port</th><th>State</th><th>Reason</th><th>Description</th><th/></tr></thead><tbody>{targets.map((target) => <tr key={target.id}><td><code>{stringValue(target.body.id)}</code></td><td>{stringValue(target.body.port)}</td><td>{stringValue(target.body.state) || '-'}</td><td>{stringValue(target.body.reason) || '-'}</td><td>{stringValue(target.body.description) || '-'}</td><td><button className="icon-btn danger" type="button" title="Deregister target" aria-label={`Deregister ${stringValue(target.body.id)}`} disabled={!runtimeReachable || deregisterMutation.isPending} onClick={() => deregisterMutation.mutate(target.id)}><Trash2 size={14}/></button></td></tr>)}</tbody></table></div>}
|
||||
</section>
|
||||
}
|
||||
|
||||
async function getTargetGroupInstances(signal?: AbortSignal): Promise<TargetInstance[]> {
|
||||
const dependencies = await getLoadBalancingDependencies('targetgroups', signal)
|
||||
return Array.isArray(dependencies.instances) ? dependencies.instances : []
|
||||
}
|
||||
|
||||
function stringValue(value: unknown): string {
|
||||
return typeof value === 'string' || typeof value === 'number' ? String(value) : ''
|
||||
}
|
||||
Reference in New Issue
Block a user