73 lines
5.3 KiB
TypeScript
73 lines
5.3 KiB
TypeScript
import {useQuery} from '@tanstack/react-query'
|
|
import {FormEvent, useEffect, useState} from 'react'
|
|
import {getLoadBalancingDependencies} from '@/api/cloudProxyClient'
|
|
import type {CloudResource} from '@/types/resource'
|
|
|
|
type Props = {
|
|
resource?: CloudResource
|
|
isSubmitting: boolean
|
|
submitError: string | null
|
|
onCancel: () => void
|
|
onSubmit: (values: Record<string, unknown>) => void
|
|
}
|
|
|
|
export function LoadBalancerPanel({resource, isSubmitting, submitError, onCancel, onSubmit}: Props) {
|
|
const dependencies = useQuery({queryKey: ['loadbalancing-dependencies'], queryFn: ({signal}) => getLoadBalancingDependencies('loadbalancing', signal)})
|
|
const [name, setName] = useState('')
|
|
const [scheme, setScheme] = useState('internet-facing')
|
|
const [vpcId, setVpcId] = useState('')
|
|
const [subnetIds, setSubnetIds] = useState<string[]>([])
|
|
const [securityGroupIds, setSecurityGroupIds] = useState<string[]>([])
|
|
const [error, setError] = useState<string | null>(null)
|
|
|
|
useEffect(() => {
|
|
if (!resource) return
|
|
setVpcId(typeof resource.metadata.vpcId === 'string' ? resource.metadata.vpcId : '')
|
|
setSecurityGroupIds(stringValues(resource.metadata.securityGroups))
|
|
}, [resource])
|
|
|
|
const subnets = (dependencies.data?.subnets ?? []).filter((subnet) => subnet.vpcId === vpcId)
|
|
const groups = (dependencies.data?.securityGroups ?? []).filter((group) => group.vpcId === vpcId)
|
|
const selectedZones = new Set(subnets.filter((subnet) => subnetIds.includes(subnet.id)).map((subnet) => subnet.availabilityZone))
|
|
|
|
function selectVpc(nextVpcId: string) {
|
|
setVpcId(nextVpcId)
|
|
setSubnetIds([])
|
|
setSecurityGroupIds([])
|
|
setError(null)
|
|
}
|
|
|
|
function submit(event: FormEvent<HTMLFormElement>) {
|
|
event.preventDefault()
|
|
if (!resource && !name.trim()) return setError('Load balancer name is required.')
|
|
if (!resource && (subnetIds.length < 2 || selectedZones.size < 2)) return setError('Select two subnets in distinct Availability Zones.')
|
|
if (securityGroupIds.length === 0) return setError('Select at least one security group.')
|
|
setError(null)
|
|
onSubmit(resource ? {securityGroups: securityGroupIds.join(',')} : {
|
|
name: name.trim(), subnets: subnetIds.join(','), securityGroups: securityGroupIds.join(','), scheme,
|
|
})
|
|
}
|
|
|
|
if (dependencies.isLoading) return <p className="muted">Loading VPC configuration...</p>
|
|
if (dependencies.isError) return <p className="form-error" role="alert">Unable to load VPC configuration.</p>
|
|
|
|
return <form className="dynamic-form" onSubmit={submit} noValidate>
|
|
<div className="dynamic-form-group">{resource ? 'Security groups' : 'Application Load Balancer'}</div>
|
|
{!resource && <label className="dynamic-field"><span>Load Balancer Name<em className="field-required">*</em></span><input className="input" value={name} onChange={(event) => setName(event.target.value)} /></label>}
|
|
{!resource && <label className="dynamic-field"><span>Scheme</span><select className="input" value={scheme} onChange={(event) => setScheme(event.target.value)}><option value="internet-facing">internet-facing</option><option value="internal">internal</option></select></label>}
|
|
<label className="dynamic-field"><span>VPC<em className="field-required">*</em></span><select className="input" value={vpcId} required onChange={(event) => selectVpc(event.target.value)} disabled={Boolean(resource)}><option value="">Select a VPC</option>{(dependencies.data?.vpcs ?? []).map((vpc) => <option key={vpc.id} value={vpc.id}>{vpc.id} ({vpc.cidrBlock})</option>)}</select></label>
|
|
{!resource && <fieldset className="dynamic-field dynamic-field--span"><legend>Subnets<em className="field-required">*</em></legend><small>Select at least two subnets in distinct Availability Zones.</small>{subnets.map((subnet) => <label key={subnet.id}><input type="checkbox" checked={subnetIds.includes(subnet.id)} onChange={() => setSubnetIds((current) => toggle(current, subnet.id))} /> {subnet.id} ({subnet.availabilityZone}, {subnet.cidrBlock})</label>)}</fieldset>}
|
|
<fieldset className="dynamic-field dynamic-field--span"><legend>Security Groups<em className="field-required">*</em></legend>{groups.map((group) => <label key={group.id}><input type="checkbox" checked={securityGroupIds.includes(group.id)} onChange={() => setSecurityGroupIds((current) => toggle(current, group.id))} /> {group.name || group.id} ({group.id})</label>)}</fieldset>
|
|
<div style={{gridColumn: '1 / -1', justifySelf: 'end', display: 'flex', gap: 8}}><button className="button" type="button" disabled={isSubmitting} onClick={onCancel}>Cancel</button><button className="button primary" type="submit" disabled={isSubmitting}>{isSubmitting ? (resource ? 'Saving' : 'Creating') : (resource ? 'Set Security Groups' : 'Create Load Balancer')}</button></div>
|
|
{(error || submitError) && <div className="form-error" role="alert">{error ?? submitError}</div>}
|
|
</form>
|
|
}
|
|
|
|
function toggle(values: string[], value: string): string[] {
|
|
return values.includes(value) ? values.filter((entry) => entry !== value) : [...values, value]
|
|
}
|
|
|
|
function stringValues(value: unknown): string[] {
|
|
return Array.isArray(value) ? value.filter((entry): entry is string => typeof entry === 'string') : []
|
|
}
|