Files
floci-ui/packages/frontend/src/components/LoadBalancerPanel.tsx
T
2026-09-26 16:14:24 -04:00

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') : []
}