import {ChevronDown, ExternalLink, HardDrive, Play, RotateCw, Settings, Square, Terminal, Trash2} from 'lucide-react' import {useEffect, useRef, useState} from 'react' import {useMutation, useQueryClient} from '@tanstack/react-query' import {deleteCloudResource, deployLightsailNodeApplication, deployLightsailStaticFiles, rebootCloudResource, startCloudResource, stopCloudResource, updateCloudResource} from '@/api/cloudProxyClient' import type {CloudResource} from '@/types/resource' type Tab = 'overview' | 'deploy' | 'networking' | 'storage' | 'connect' type PublicPort = {fromPort: number; toPort: number; protocol: string} type HostPortMapping = {instancePort: number; host: string; hostPort: number; protocol?: string} type Disk = {name: string; sizeInGb: number; isSystemDisk: boolean; path?: string; state?: string} export function LightsailInstancePanel({resource, runtimeReachable, onDeleted}: {resource?: CloudResource; runtimeReachable: boolean; onDeleted?: () => void}) { const queryClient = useQueryClient() const [tab, setTab] = useState('overview') const [fromPort, setFromPort] = useState('80') const [toPort, setToPort] = useState('80') const [protocol, setProtocol] = useState('tcp') const [nodePort, setNodePort] = useState('3000') const [startCommand, setStartCommand] = useState('') const [confirmDelete, setConfirmDelete] = useState(false) const fileInput = useRef(null) const ports = publicPorts(resource?.metadata.networking) const hostMappings = hostPortMappings(resource?.metadata.hostPortMappings) const disks = instanceDisks(resource?.metadata.hardware) const status = resource?.status?.toLowerCase() const publicIp = stringMetadata(resource?.metadata.publicIpAddress) const privateIp = stringMetadata(resource?.metadata.privateIpAddress) const staticIp = staticIpAddress(resource?.metadata.networking) useEffect(() => { setTab('overview') setFromPort('80') setToPort('80') setProtocol('tcp') setConfirmDelete(false) }, [resource?.id]) const refresh = () => { if (!resource) return void queryClient.invalidateQueries({queryKey: ['cloud-resources', resource.cloud, resource.service]}) void queryClient.invalidateQueries({queryKey: ['cloud-resource', resource.cloud, resource.service, resource.id]}) } const lifecycle = useMutation({ mutationFn: (action: 'start' | 'stop' | 'reboot') => { if (!resource) throw new Error('Select a Lightsail instance.') if (action === 'start') return startCloudResource(resource.cloud, resource.service, resource.id) if (action === 'stop') return stopCloudResource(resource.cloud, resource.service, resource.id) return rebootCloudResource(resource.cloud, resource.service, resource.id) }, onSuccess: refresh, }) const portMutation = useMutation({ mutationFn: (values: {action: 'openPort' | 'closePort'; fromPort: number; toPort: number; protocol: string}) => { if (!resource) throw new Error('Select a Lightsail instance.') return updateCloudResource(resource.cloud, resource.service, resource.id, values) }, onSuccess: refresh, }) const deleteMutation = useMutation({ mutationFn: () => { if (!resource) throw new Error('Select a Lightsail instance.') return deleteCloudResource(resource.cloud, resource.service, resource.id) }, onSuccess: () => { refresh() onDeleted?.() }, }) const deployment = useMutation({ mutationFn: (files: File[]) => { if (!resource) throw new Error('Select a Lightsail instance.') return deployLightsailStaticFiles(resource.id, files) }, }) const nodeDeployment = useMutation({ mutationFn: async (files: File[]) => { if (!resource) throw new Error('Select a Lightsail instance.') const port = Number(nodePort) if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error('Port must be between 1 and 65535.') const result = await deployLightsailNodeApplication(resource.id, files, startCommand, port) try { await updateCloudResource(resource.cloud, resource.service, resource.id, {action: 'openPort', fromPort: port, toPort: port, protocol: 'tcp'}) } catch (error) { throw Object.assign(new Error(`Deployment succeeded, but port ${port} could not be opened: ${error instanceof Error ? error.message : 'unknown error'}`), {cause: error}) } return result }, onSuccess: refresh, }) if (!resource) { return

Select a Lightsail instance

Manage its lifecycle, networking, storage metadata, and connection details.

} const validPortRange = validPort(fromPort) && validPort(toPort) && Number(fromPort) <= Number(toPort) const busy = lifecycle.isPending || portMutation.isPending || deleteMutation.isPending || deployment.isPending || nodeDeployment.isPending const error = lifecycle.error ?? portMutation.error ?? deleteMutation.error ?? deployment.error ?? nodeDeployment.error const nginx = stringMetadata(resource.metadata.blueprintId) === 'nginx' const nodejs = stringMetadata(resource.metadata.blueprintId) === 'nodejs' const wordpress = stringMetadata(resource.metadata.blueprintId) === 'wordpress' const wordpressPort = hostMappings.find((mapping) => mapping.instancePort === 80) const stateLabel = resource.status ?? 'Unknown' return

Amazon Lightsail instance

{resource.name}

{resource.id}{resource.region && <> ยท {resource.region}}

{stateLabel}
Actions
{(nginx || nodejs) && }
{tab === 'overview' &&
{wordpress && wordpressPort &&

Open site Open WordPress admin

}

Use the Networking tab to manage public firewall rules and open mapped browser endpoints.

} {tab === 'deploy' && nginx &&

Deploy static files

Selected files are copied to /usr/share/nginx/html. Folder paths are preserved when your browser provides them.

{ const files = Array.from(event.target.files ?? []); if (files.length) deployment.mutate(files); event.currentTarget.value = '' }}/>{deployment.data &&

Deployment complete: {deployment.data.files} {deployment.data.files === 1 ? 'file' : 'files'} copied.

}
} {tab === 'deploy' && nodejs &&

Deploy Node.js application

Upload a zip archive or source files. package.json is required; package-lock.json uses npm ci, otherwise npm install. Existing releases remain active if deployment fails.

{ const files = Array.from(event.target.files ?? []); if (files.length) nodeDeployment.mutate(files); event.currentTarget.value = '' }}/>{nodeDeployment.data &&

Release {nodeDeployment.data.releaseId} is healthy on port {nodeDeployment.data.port}. The browser port was opened automatically.

}
} {tab === 'networking' &&

Static IP is metadata-only. Attach or detach operations are not available from this console.

Firewall rules

Public ports returned by the Lightsail instance networking API.

{ event.preventDefault(); if (validPortRange) portMutation.mutate({action: 'openPort', fromPort: Number(fromPort), toPort: Number(toPort), protocol}) }}>
{ports.length > 0 ?
{ports.map((port) => )}
Port rangeProtocol
{formatPortRange(port)}{port.protocol.toUpperCase()}
:

No public firewall rules are configured.

}

Browser links

{hostMappings.length > 0 ?
{hostMappings.map((mapping) => )}
Instance portHost mapping
{mapping.instancePort}{mapping.host}:{mapping.hostPort}Open
: publicIp ? Open HTTP endpoint :

No browser endpoint was returned for this instance.

}
} {tab === 'storage' &&

Disks

Disk details are metadata-only. Creating, attaching, detaching, or resizing disks is not available from this console.

{disks.length > 0 ?
{disks.map((disk) => )}
NameSizeTypeState
{disk.name}{disk.sizeInGb} GB{disk.isSystemDisk ? 'System disk' : 'Attached disk'}{disk.state ?? 'Unknown'}
:

No disk metadata was returned by the runtime.

}
} {tab === 'connect' &&

Connect to instance

Browser SSH and key injection are unavailable in the local Lightsail runtime. This console does not generate credentials or claim a connection is available.

} {confirmDelete &&
Delete {resource.name}? This cannot be undone.
} {error instanceof Error &&

{error.message}

}
} function Detail({label, value}: {label: string; value?: string}) { return
{label}{value ?? 'Not assigned'}
} function validPort(value: string): boolean { return /^\d+$/.test(value) && Number(value) >= 0 && Number(value) <= 65535 } function formatPortRange(port: PublicPort): string { return port.fromPort === port.toPort ? String(port.fromPort) : `${port.fromPort}-${port.toPort}` } function browserUrl(mapping: HostPortMapping): string { return `http://${mapping.host}:${mapping.hostPort}` } function stringMetadata(value: unknown): string | undefined { return typeof value === 'string' && value ? value : undefined } function publicPorts(networking: unknown): PublicPort[] { if (!networking || typeof networking !== 'object' || !Array.isArray((networking as {ports?: unknown}).ports)) return []; return (networking as {ports: unknown[]}).ports.flatMap((port) => { if (!port || typeof port !== 'object') return []; const value = port as {fromPort?: unknown; toPort?: unknown; protocol?: unknown}; return typeof value.fromPort === 'number' && typeof value.toPort === 'number' && typeof value.protocol === 'string' ? [{fromPort: value.fromPort, toPort: value.toPort, protocol: value.protocol}] : [] }) } function hostPortMappings(value: unknown): HostPortMapping[] { if (!Array.isArray(value)) return []; return value.flatMap((mapping) => { if (!mapping || typeof mapping !== 'object') return []; const item = mapping as {instancePort?: unknown; host?: unknown; hostPort?: unknown; protocol?: unknown}; return typeof item.instancePort === 'number' && typeof item.host === 'string' && typeof item.hostPort === 'number' ? [{instancePort: item.instancePort, host: item.host, hostPort: item.hostPort, protocol: typeof item.protocol === 'string' ? item.protocol : undefined}] : [] }) } function staticIpAddress(networking: unknown): string | undefined { return networking && typeof networking === 'object' ? stringMetadata((networking as {staticIp?: unknown}).staticIp) : undefined } function instanceDisks(hardware: unknown): Disk[] { if (!hardware || typeof hardware !== 'object' || !Array.isArray((hardware as {disks?: unknown}).disks)) return []; return (hardware as {disks: unknown[]}).disks.flatMap((disk) => { if (!disk || typeof disk !== 'object') return []; const value = disk as {name?: unknown; sizeInGb?: unknown; isSystemDisk?: unknown; path?: unknown; state?: unknown}; return typeof value.name === 'string' && typeof value.sizeInGb === 'number' && typeof value.isSystemDisk === 'boolean' ? [{name: value.name, sizeInGb: value.sizeInGb, isSystemDisk: value.isSystemDisk, path: stringMetadata(value.path), state: stringMetadata(value.state)}] : [] }) }