diff --git a/internal/web/dist/index.html b/internal/web/dist/index.html index 09247ac..ed9d371 100644 --- a/internal/web/dist/index.html +++ b/internal/web/dist/index.html @@ -5,8 +5,8 @@ web - - + +
diff --git a/web/src/pages/LocalZones.tsx b/web/src/pages/LocalZones.tsx index eda8ebe..5d51a20 100644 --- a/web/src/pages/LocalZones.tsx +++ b/web/src/pages/LocalZones.tsx @@ -3,10 +3,20 @@ import { useEffect, useMemo, useState } from 'react' import { FileCode2, Plus, Save, Trash2 } from 'lucide-react' -import { deleteLocalZone, getLocalZones, saveLocalZone } from '../lib/api' +import { deleteLocalZone, getLocalZones, getZones, saveLocalZone } from '../lib/api' import { useWhoami } from '../lib/session.ts' +import type { ZoneRecord } from '../lib/types' import { useApi, useAsyncAction } from '../hooks/useApi' -import { Button, Card, CardBody, Input, Textarea, useToast } from '../components/ui' +import { + Badge, + Button, + Card, + CardBody, + Input, + Table, + Textarea, + useToast, +} from '../components/ui' import { LoadingBlock, PageError, PageHeader } from '../components/layout' const EXAMPLE = '$ORIGIN example.net.\n$TTL 300\n@ IN A 192.0.2.10\nwww IN CNAME @\n' @@ -23,6 +33,10 @@ export default function LocalZones() { const { canWrite } = useWhoami() const { toast } = useToast() const { data, error, loading, refetch } = useApi(() => getLocalZones(), []) + // What the daemon has actually compiled out of the selected file. A raw + // textarea says nothing about what a zone ends up serving, and guessing + // from $TTL and owner columns is a bad way to find a typo. + const { data: compiled } = useApi(getZones, []) const [selected, setSelected] = useState(null) const [draft, setDraft] = useState<{ name: string; content: string } | null>(null) const [creating, setCreating] = useState(false) @@ -35,6 +49,14 @@ export default function LocalZones() { const zones = data?.zones ?? [] const active = useMemo(() => zones.find((zone) => zone.name === selected) ?? null, [zones, selected]) + // The compiled view is keyed by the zone's canonical apex, which carries a + // trailing dot the file listing does not. + const served = useMemo(() => { + if (active === null || compiled === null) return null + const want = active.name.endsWith('.') ? active.name : `${active.name}.` + return compiled.zones.find((zone) => zone.origin === want) ?? null + }, [active, compiled]) + useEffect(() => { if (selected === null && zones.length > 0 && !creating && draft === null) setSelected(zones[0].name) }, [creating, draft, selected, zones]) @@ -141,6 +163,58 @@ export default function LocalZones() {

Choose a zone or create one to add records.

) : (
+ {served === null ? ( +

+ The daemon has not compiled a zone called{' '} + {draft.name}. — the file may be invalid, or + it may be a name the resolver does not serve. The errors are on the status page. +

+ ) : ( +
+

+ + {served.record_count} record{served.record_count === 1 ? '' : 's'} served + {' '} + for this apex + {served.serial > 0 ? ` · SOA serial ${served.serial}` : ''} + {served.delegations.length > 0 + ? ` · ${served.delegations.length} delegation${served.delegations.length === 1 ? '' : 's'}` + : ''} + . This is what the resolver answers with, compiled from the file below. +

+
+ ({ + __key: `${record.name}-${record.type}-${record.value}-${i}`, + name: ( + + {record.name} + {record.wildcard ? ( + wildcard + ) : null} + + ), + type: {record.type}, + value: ( + + {record.value} + + ), + ttl: {record.ttl}, + managed: record.managed ? 'console' : 'file', + }))} + rowKey={(row) => String(row.__key)} + /> + + + )}
setDraft({ ...draft, name: event.currentTarget.value })} /> diff --git a/web/src/pages/Zones.tsx b/web/src/pages/Zones.tsx index 818568c..754da2e 100644 --- a/web/src/pages/Zones.tsx +++ b/web/src/pages/Zones.tsx @@ -11,7 +11,8 @@ import { useEffect, useMemo, useState } from 'react' import type { ReactNode } from 'react' -import { AlertTriangle, List, Pencil, Plus, Search, Trash2, Waypoints } from 'lucide-react' +import { useNavigate } from 'react-router-dom' +import { AlertTriangle, FileCode2, List, Pencil, Plus, Search, Trash2, Waypoints } from 'lucide-react' import { addZoneDelegation, @@ -121,6 +122,7 @@ const NEW_ZONE = '__new__' export default function Zones() { const { toast } = useToast() const { canWrite } = useWhoami() + const navigate = useNavigate() const { data, error, loading, refetch } = useApi(getZones, []) const [origin, setOrigin] = useState('') @@ -486,6 +488,16 @@ export default function Zones() {
+ {zone !== null && !zone.managed ? ( + + ) : null} {zone !== null && zone.managed ? (