import {Pencil, RefreshCw, Trash2} from 'lucide-react' import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query' import {useEffect, useState} from 'react' import {deleteFlatItem, getCloudResource, listFlatItems, putFlatItem} from '@/api/cloudProxyClient' import {CopyValueButton} from '@/components/CopyValueButton' import {useAccountId} from '@/lib/accountStore' import type {ChildItem, CloudResource} from '@/types/resource' type Props = { resource?: CloudResource runtimeReachable: boolean } type DnsRecord = { id: string name: string type: string ttl: number | null values: string[] alias: {dnsName: string; hostedZoneId: string} | null setIdentifier: string | null managed: boolean editable: boolean } const RECORD_TYPES = ['A', 'AAAA', 'CNAME', 'TXT', 'MX', 'SRV', 'CAA', 'NS', 'PTR', 'SPF', 'DS', 'HTTPS', 'SVCB', 'NAPTR', 'SSHFP', 'TLSA'] const VALUE_HINTS: Record = { A: 'IPv4 addresses, one per line: 192.0.2.10', AAAA: 'IPv6 addresses, one per line: 2001:db8::10', CNAME: 'One host name: app.example.net', TXT: 'Text, one string per line. Quotes are added for you.', MX: 'Priority and mail server, one per line: 10 mail.example.com', SRV: 'Priority, weight, port and target: 10 5 5060 sip.example.com', CAA: 'Flags, tag and value: 0 issue "amazon.com"', } /** Every page of a zone is fetched; zones on a local runtime are small. */ const MAX_PAGES = 20 const emptyDraft = {name: '', type: 'A', ttl: '300', values: ''} export function Route53RecordsPanel({resource, runtimeReachable}: Props) { const accountId = useAccountId() const queryClient = useQueryClient() const [draft, setDraft] = useState(emptyDraft) const [editing, setEditing] = useState(null) const [notice, setNotice] = useState('') const recordsKey = ['dns-records', accountId, resource?.id] const zoneQuery = useQuery({ queryKey: ['dns-zone', accountId, resource?.id], queryFn: ({signal}) => getCloudResource('aws', 'dns', resource?.id ?? '', signal), enabled: Boolean(resource) && runtimeReachable, }) const recordsQuery = useQuery({ queryKey: recordsKey, queryFn: async ({signal}) => { const items: ChildItem[] = [] let cursor: string | undefined for (let page = 0; page < MAX_PAGES; page++) { const result = await listFlatItems('aws', 'dns', resource?.id ?? '', cursor, signal) items.push(...result.items) if (!result.nextCursor) break cursor = result.nextCursor } return items.map(toRecord) }, enabled: Boolean(resource) && runtimeReachable, }) useEffect(() => { setDraft(emptyDraft) setEditing(null) setNotice('') }, [resource?.id]) // The zone list shows each zone's record count. const refresh = () => Promise.all([ queryClient.invalidateQueries({queryKey: recordsKey}), queryClient.invalidateQueries({queryKey: ['dns-zone', accountId, resource?.id]}), queryClient.invalidateQueries({queryKey: ['cloud-resources', 'aws', 'dns']}), ]) const saveMutation = useMutation({ mutationFn: () => putFlatItem('aws', 'dns', resource?.id ?? '', { name: draft.name, type: draft.type, ttl: Number(draft.ttl), values: draft.values.split('\n'), }), onSuccess: (saved) => { setNotice(`${editing ? 'Saved' : 'Created'} ${String(saved.body.type)} ${String(saved.body.name)}.`) setDraft(emptyDraft) setEditing(null) return refresh() }, }) const deleteMutation = useMutation({ mutationFn: (id: string) => deleteFlatItem('aws', 'dns', resource?.id ?? '', id), onSuccess: (_result, id) => { setNotice(`Deleted ${id.split('|').slice(0, 2).reverse().join(' ')}.`) if (editing === id) { setEditing(null); setDraft(emptyDraft) } return refresh() }, }) if (!resource) { return

Select a hosted zone

Choose a hosted zone to see and change its DNS records.

} const zoneName = resource.name const records = recordsQuery.data ?? [] const nameServers = stringList(zoneQuery.data?.metadata.nameServers) const pending = saveMutation.isPending || deleteMutation.isPending const disabled = !runtimeReachable || pending const error = zoneQuery.error ?? recordsQuery.error ?? saveMutation.error ?? deleteMutation.error const edit = (record: DnsRecord) => { setNotice('') setEditing(record.id) setDraft({name: relativeName(record.name, zoneName), type: record.type, ttl: String(record.ttl ?? 300), values: record.values.map((value) => record.type === 'TXT' || record.type === 'SPF' ? unquote(value) : value).join('\n')}) } return
{nameServers.length > 0 && <>

Name servers

Delegate {zoneName} to these at your registrar. TheCloud stores records but does not answer DNS queries.
    {nameServers.map((server) =>
  • {server}
  • )}
}

Records

{records.length} {records.length === 1 ? 'record' : 'records'} in {zoneName}
{ event.preventDefault(); setNotice(''); saveMutation.mutate() }}>