small feat: svg animation for servers, codebuild, codePipleline etc...
This commit is contained in:
@@ -3,7 +3,7 @@ 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 {LoadBalancingProgress} from '@/components/LoadBalancingProgress'
|
||||
import {TargetHealthIcon} from '@/components/LoadBalancingProgress'
|
||||
import type {CloudResource} from '@/types/resource'
|
||||
|
||||
type Props = {
|
||||
@@ -12,16 +12,19 @@ type Props = {
|
||||
}
|
||||
|
||||
type TargetInstance = {id: string; name: string; privateIp: string | null}
|
||||
type PendingTarget = {id: string; name: string; port: number}
|
||||
|
||||
export function TargetGroupPanel({resource, runtimeReachable}: Props) {
|
||||
const accountId = useAccountId()
|
||||
const queryClient = useQueryClient()
|
||||
const [instanceId, setInstanceId] = useState('')
|
||||
const [port, setPort] = useState('')
|
||||
const [pendingTargets, setPendingTargets] = useState<PendingTarget[]>([])
|
||||
const dependenciesQuery = useQuery({
|
||||
queryKey: ['target-group-instances', accountId],
|
||||
queryFn: ({signal}) => getTargetGroupInstances(signal),
|
||||
enabled: Boolean(resource) && runtimeReachable,
|
||||
refetchInterval: pendingTargets.length ? 500 : false,
|
||||
})
|
||||
const healthQuery = useQuery({
|
||||
queryKey: ['target-group-health', accountId, resource?.id],
|
||||
@@ -34,26 +37,33 @@ export function TargetGroupPanel({resource, runtimeReachable}: Props) {
|
||||
setPort(typeof resource?.metadata.port === 'number' ? String(resource.metadata.port) : '')
|
||||
}, [resource?.id, resource?.metadata.port])
|
||||
|
||||
useEffect(() => {
|
||||
const targets = healthQuery.data?.items ?? []
|
||||
setPendingTargets((current) => current.filter((pending) => {
|
||||
const state = stringValue(targets.find((target) => stringValue(target.body.id) === pending.id && Number(target.body.port) === pending.port)?.body.state).toLowerCase()
|
||||
return state !== 'healthy' && state !== 'unhealthy'
|
||||
}))
|
||||
}, [healthQuery.data])
|
||||
|
||||
useEffect(() => {
|
||||
if (!pendingTargets.length) {
|
||||
return
|
||||
}
|
||||
const interval = window.setInterval(() => {
|
||||
void healthQuery.refetch()
|
||||
}, 1_000)
|
||||
return () => window.clearInterval(interval)
|
||||
}, [pendingTargets.length, healthQuery.refetch])
|
||||
|
||||
const refresh = () => {
|
||||
void Promise.all([dependenciesQuery.refetch(), healthQuery.refetch()])
|
||||
}
|
||||
const registerMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
const startedAt = Date.now()
|
||||
await putFlatItem('aws', 'targetgroups', resource?.id ?? '', {id: instanceId, port: Number(port)})
|
||||
while (true) {
|
||||
const health = await listFlatItems('aws', 'targetgroups', resource?.id ?? '')
|
||||
const target = health.items.find((item) => stringValue(item.body.id) === instanceId && Number(item.body.port) === Number(port))
|
||||
const state = stringValue(target?.body.state).toLowerCase()
|
||||
if (state === 'healthy' || state === 'unhealthy') {
|
||||
const remaining = 5_000 - (Date.now() - startedAt)
|
||||
if (remaining > 0) await new Promise<void>((resolve) => setTimeout(resolve, remaining))
|
||||
return
|
||||
}
|
||||
await new Promise<void>((resolve) => setTimeout(resolve, 500))
|
||||
}
|
||||
mutationFn: (target: PendingTarget) => putFlatItem('aws', 'targetgroups', resource?.id ?? '', {id: target.id, port: target.port}),
|
||||
onSuccess: (_, target) => {
|
||||
setPendingTargets((current) => [...current, target])
|
||||
void queryClient.invalidateQueries({queryKey: ['target-group-health', accountId, resource?.id]})
|
||||
},
|
||||
onSuccess: () => queryClient.invalidateQueries({queryKey: ['target-group-health', accountId, resource?.id]}),
|
||||
})
|
||||
const deregisterMutation = useMutation({
|
||||
mutationFn: (targetKey: string) => deleteFlatItem('aws', 'targetgroups', resource?.id ?? '', targetKey),
|
||||
@@ -68,22 +78,23 @@ export function TargetGroupPanel({resource, runtimeReachable}: Props) {
|
||||
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
|
||||
const pendingRows = pendingTargets.filter((pending) => !targets.some((target) => stringValue(target.body.id) === pending.id && Number(target.body.port) === pending.port))
|
||||
|
||||
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>
|
||||
<form className="dynamic-form" onSubmit={(event) => { event.preventDefault(); const instance = instances.find((candidate) => candidate.id === instanceId); if (instance && validPort) registerMutation.mutate({id: instance.id, name: instance.name, port: Number(port)}) }}>
|
||||
<label className="dynamic-field"><span>Running instance</span><select className="input" value={instanceId} onChange={(event) => setInstanceId(event.target.value)} disabled={dependenciesQuery.isLoading}><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)}/></label>
|
||||
<div style={{alignSelf: 'end'}}><button className="button primary" type="submit" disabled={!runtimeReachable || !instanceId || !validPort}>Register target</button></div>
|
||||
{instances.length === 0 && dependenciesQuery.isSuccess && <p className="muted">No running instances are available in this account.</p>}
|
||||
{registerMutation.isPending && <div className="dynamic-field--span"><LoadBalancingProgress kind="target" description="Waiting for the target health check to become healthy or unhealthy."/></div>}
|
||||
</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>}
|
||||
{healthQuery.isSuccess && targets.length === 0 && pendingRows.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 || pendingRows.length > 0) && <div className="cosmos-items-table target-health-table"><table><thead><tr><th>Instance</th><th>Port</th><th>State</th><th>Reason</th><th>Description</th><th/></tr></thead><tbody>{pendingRows.map((target) => <tr key={`${target.id}:${target.port}`}><td><code>{target.id}</code></td><td>{target.port}</td><td><span className="target-state"><TargetHealthIcon state="checking"/>Checking</span></td><td>-</td><td>Waiting for target health.</td><td/></tr>)}{targets.map((target) => { const state = stringValue(target.body.state); return <tr key={target.id}><td><code>{stringValue(target.body.id)}</code></td><td>{stringValue(target.body.port)}</td><td><span className="target-state"><TargetHealthIcon state={state}/>{state || '-'}</span></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>
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user