import {RefreshCw} from 'lucide-react' import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query' import {useEffect, useState} from 'react' import {getCloudResource, setAutoScalingCapacity, terminateAutoScalingInstance} from '@/api/cloudProxyClient' import {cloudQueryKeys} from '@/api/queries/cloudQueries' import {useAccountId} from '@/lib/accountStore' import {formatDateTime} from '@/lib/format' import type {CloudResource} from '@/types/resource' type Props = { resource?: CloudResource runtimeReachable: boolean } type Instance = {id: string; availabilityZone: string | null; lifecycleState: string | null; health: string | null; instanceType: string | null} type Activity = {id: string; description: string; cause: string; status: string | null; statusMessage: string | null; startTime: string | null} type Capacity = {minSize: string; desiredCapacity: string; maxSize: string} /** The runtime reconciles every 10 seconds, so a group that is still moving is re-read this often. */ const SETTLING_POLL_MS = 4000 export function AutoScalingPanel({resource, runtimeReachable}: Props) { const accountId = useAccountId() const queryClient = useQueryClient() const [capacity, setCapacity] = useState({minSize: '', desiredCapacity: '', maxSize: ''}) const [notice, setNotice] = useState('') const groupKey = ['autoscaling-group', accountId, resource?.id] const groupQuery = useQuery({ queryKey: groupKey, queryFn: ({signal}) => getCloudResource('aws', 'autoscaling', resource?.id ?? '', signal), enabled: Boolean(resource) && runtimeReachable, refetchInterval: (query) => query.state.data && !settled(query.state.data) ? SETTLING_POLL_MS : false, }) const group = groupQuery.data ?? resource useEffect(() => { setNotice('') }, [resource?.id]) // The form follows the group's saved capacity whenever it changes, here or elsewhere. const savedMin = String(group?.metadata.minSize ?? '') const savedDesired = String(group?.metadata.desiredCapacity ?? '') const savedMax = String(group?.metadata.maxSize ?? '') useEffect(() => { setCapacity({minSize: savedMin, desiredCapacity: savedDesired, maxSize: savedMax}) }, [group?.id, savedMin, savedDesired, savedMax]) const refresh = () => Promise.all([ queryClient.invalidateQueries({queryKey: groupKey}), queryClient.invalidateQueries({queryKey: cloudQueryKeys.resources('aws', 'autoscaling')}), ]) const capacityMutation = useMutation({ mutationFn: () => setAutoScalingCapacity('aws', resource?.id ?? '', { minSize: Number(capacity.minSize), desiredCapacity: Number(capacity.desiredCapacity), maxSize: Number(capacity.maxSize), }), onSuccess: (updated) => { setNotice(`Capacity set to ${String(updated.metadata.desiredCapacity)}. TheCloud launches or terminates instances within about 10 seconds.`) return refresh() }, }) const terminateMutation = useMutation({ mutationFn: ({instanceId, replace}: {instanceId: string; replace: boolean}) => terminateAutoScalingInstance('aws', resource?.id ?? '', instanceId, replace), onSuccess: (_result, {instanceId, replace}) => { setNotice(replace ? `Terminating ${instanceId}. The group launches a replacement to stay at its desired capacity.` : `Terminating ${instanceId} and lowering the desired capacity by one.`) return refresh() }, }) if (!resource || !group) { return

Select an Auto Scaling group

Choose a group to change its capacity, see its instances and follow its scaling activity.

} const metadata = group.metadata const instances = Array.isArray(metadata.instances) ? metadata.instances as Instance[] : [] const activities = Array.isArray(metadata.activities) ? metadata.activities as Activity[] : [] const desired = Number(metadata.desiredCapacity ?? 0) const min = Number(metadata.minSize ?? 0) const pending = capacityMutation.isPending || terminateMutation.isPending const disabled = !runtimeReachable || pending const error = groupQuery.error ?? capacityMutation.error ?? terminateMutation.error const changed = capacity.minSize !== String(metadata.minSize) || capacity.desiredCapacity !== String(metadata.desiredCapacity) || capacity.maxSize !== String(metadata.maxSize) const field = (name: keyof Capacity, label: string) => return

Auto Scaling group

{String(metadata.capacity ?? '')} · desired {desired}{settled(group) ? '' : ' · scaling'}{group.status ? ` · ${group.status}` : ''}
{ event.preventDefault(); setNotice(''); capacityMutation.mutate() }}> {field('minSize', 'Minimum')} {field('desiredCapacity', 'Desired')} {field('maxSize', 'Maximum')}

Scaling policies are not offered: TheCloud stores EC2 scaling policies but never runs them, so set the capacity here. Alarm-driven scaling works for ECS services through Application Auto Scaling.

Instances

Each is a real EC2 instance in TheCloud, launched from the group's template.
{instances.length === 0 ?

No instances. {desired > 0 ? 'The group is launching them.' : 'Raise the desired capacity to launch some.'}

:
{instances.map((instance) => ( ))}
InstanceStateHealthZoneType
{instance.id} {instance.lifecycleState ?? '—'} {instance.health ?? '—'} {instance.availabilityZone ?? '—'} {instance.instanceType ?? '—'}
} {activities.length > 0 && <>

Activity

The most recent launches and terminations.
{activities.map((activity) => ( ))}
StartedStatusActivity
{formatDateTime(activity.startTime) ?? '—'} {activity.status ?? '—'} {activity.description}
{activity.statusMessage || activity.cause}
} {metadata.activitiesUnavailable === true &&

The scaling activity could not be read just now.

} {notice &&

{notice}

} {error instanceof Error &&

{error.message}

}
} /** Settled once every desired instance is in service and none is still starting or stopping. */ function settled(group: CloudResource): boolean { const instances = Array.isArray(group.metadata.instances) ? group.metadata.instances as Instance[] : [] const inService = instances.filter((instance) => instance.lifecycleState === 'InService').length return inService === Number(group.metadata.desiredCapacity ?? 0) && instances.every((instance) => instance.lifecycleState === 'InService') }