overrides
build / build-push (push) Successful in 29s

This commit is contained in:
mats committed 2026-09-28 16:25:55 +02:00
1 parent 98b65945f6
commit 0b6dcbd547
13 files changed
+417 -68

No files matched your search

+2
View File
@@ -22,6 +22,7 @@ import Logs from './pages/Logs.tsx'
import LocalZones from './pages/LocalZones.tsx'
import Lookup from './pages/Lookup.tsx'
import NotFound from './pages/NotFound.tsx'
import Overrides from './pages/Overrides.tsx'
import Settings from './pages/Settings.tsx'
import Upstreams from './pages/Upstreams.tsx'
import Zones from './pages/Zones.tsx'
@@ -54,6 +55,7 @@ function AppRoutes() {
<Route path="/blocklists" element={<Blocklists />} />
<Route path="/domains" element={<Domains />} />
<Route path="/lookup" element={<Lookup />} />
<Route path="/overrides" element={<Overrides />} />
<Route path="/local-zones" element={<LocalZones />} />
<Route path="/zones" element={<Zones />} />
<Route path="/clients" element={<Clients />} />
+2
View File
@@ -13,6 +13,7 @@ import {
LogOut,
Menu,
Network,
MapPin,
PanelLeftClose,
ScrollText,
Search,
@@ -41,6 +42,7 @@ const NAV: NavItem[] = [
{ to: '/blocklists', label: 'Blocklists', icon: ListChecks },
{ to: '/domains', label: 'Domains', icon: Globe },
{ to: '/lookup', label: 'Lookup', icon: Search },
{ to: '/overrides', label: 'Address overrides', icon: MapPin },
{ to: '/local-zones', label: 'Local zones', icon: FileCode2 },
{ to: '/zones', label: 'Zones', icon: Layers },
{ to: '/clients', label: 'Clients', icon: Users },
+8 -2
View File
@@ -280,13 +280,19 @@ export function checkDomain(name: string): Promise<DomainCheckResponse> {
}
/** Runs one query through the live pipeline, the console's connectivity test. */
export function testDns(name: string, qtype: string): Promise<DnsTestResponse> {
export function testDns(name: string, qtype: string, upstream = ''): Promise<DnsTestResponse> {
return apiFetch<DnsTestResponse>('/api/dns/test', {
method: 'POST',
body: JSON.stringify({ name, type: qtype }),
body: JSON.stringify({ name, type: qtype, upstream }),
})
}
export interface AddressOverride { domain: string; address: string }
export interface AddressOverridesResponse { items: AddressOverride[]; writable: boolean; initialized?: boolean }
export function getAddressOverrides(): Promise<AddressOverridesResponse> { return apiFetch<AddressOverridesResponse>('/api/address-overrides') }
export function putAddressOverride(domain: string,address: string): Promise<{ok:boolean}> { return apiFetch('/api/address-overrides',{method:'PUT',body:JSON.stringify({domain,address})}) }
export function deleteAddressOverride(domain: string): Promise<{ok:boolean}> { return apiFetch(`/api/address-overrides/${encodeURIComponent(domain)}`,{method:'DELETE'}) }
export function getZones(): Promise<ZonesResponse> {
return apiFetch<ZonesResponse>('/api/zones')
}
+11 -7
View File
@@ -8,11 +8,11 @@
* with, and every entry can be turned into a local-zone override from there.
*/
import { useState } from 'react'
import { useEffect, useState } from 'react'
import type { ReactNode } from 'react'
import { Plus, Search } from 'lucide-react'
import { addZoneRecord, testDns } from '../lib/api'
import { addZoneRecord, getUpstreams, testDns } from '../lib/api'
import { useWhoami } from '../lib/session.ts'
import type { DnsTestResponse } from '../lib/types'
import { useAsyncAction } from '../hooks/useApi'
@@ -88,13 +88,16 @@ export default function Lookup() {
const [result, setResult] = useState<DnsTestResponse | null>(null)
const [entries, setEntries] = useState<Entry[]>([])
const [listed, setListed] = useState('')
const [upstreams, setUpstreams] = useState<string[]>([])
const [upstream, setUpstream] = useState('')
useEffect(() => { void getUpstreams().then((r) => setUpstreams(r.servers.map((s) => s.addr))).catch(() => setUpstreams([])) }, [])
const [override, setOverride] = useState<Entry | null>(null)
const [zone, setZone] = useState('')
const [relName, setRelName] = useState('')
const [ttl, setTtl] = useState('')
const test = useAsyncAction(async (target: string, type: string) => testDns(target, type))
const test = useAsyncAction(async (target: string, type: string) => testDns(target, type, upstream))
const write = useAsyncAction(async (input: { zone: string; name: string; type: string; ttl: number; value: string }) =>
addZoneRecord({
zone: input.zone,
@@ -119,7 +122,7 @@ export default function Lookup() {
const listEntries = async (): Promise<void> => {
if (trimmed === '') return
const results = await Promise.all(ENTRY_TYPES.map((t) => testDns(trimmed, t)))
const results = await Promise.all(ENTRY_TYPES.map((t) => testDns(trimmed, t, upstream)))
const found: Entry[] = []
for (const [i, res] of results.entries()) {
if (res.error !== undefined) continue
@@ -191,11 +194,11 @@ export default function Lookup() {
<div className="space-y-4">
<PageHeader
title="Lookup"
description="Test the server, list a domain's records, and copy one into a local zone as an override."
description="Test the server or query one configured upstream directly."
/>
<Card title="Test a name" subtitle="Resolved through the live pipeline, exactly as a client would see it.">
<div className="grid grid-cols-1 gap-3 sm:grid-cols-[1fr_auto_auto]">
<Card title="Test a name" subtitle={upstream ? 'Sent directly to the selected upstream, bypassing local policy and cache.' : 'Resolved through the live pipeline, as a client would see it.'}>
<div className="grid grid-cols-1 gap-3 sm:grid-cols-[1fr_auto_auto_auto]">
<Input
label="Name"
placeholder="example.com or www.example.com"
@@ -214,6 +217,7 @@ export default function Lookup() {
onChange={(e) => setQtype(e.target.value)}
options={TEST_TYPES.map((t) => ({ value: t, label: t }))}
/>
<Select label="Resolver" value={upstream} onChange={(e) => setUpstream(e.target.value)} options={[{value:'',label:'Full pipeline'},...upstreams.map((u)=>({value:u,label:u}))]} />
<div className="flex items-end">
<Button onClick={() => void runTest()} loading={test.pending} disabled={trimmed === ''}>
Test
+23
View File
@@ -0,0 +1,23 @@
import { useState } from 'react'
import { Pencil, Plus, Trash2 } from 'lucide-react'
import { deleteAddressOverride, getAddressOverrides, putAddressOverride } from '../lib/api'
import { useApi } from '../hooks/useApi'
import { useWhoami } from '../lib/session.ts'
import { Button, Card, EmptyState, IconButton, Input, Table, useToast } from '../components/ui'
import { PageHeader } from '../components/layout'
export default function Overrides() {
const {canWrite}=useWhoami(); const {toast}=useToast(); const {data,error,loading,refetch}=useApi(()=>getAddressOverrides(),[])
const [domain,setDomain]=useState(''); const [address,setAddress]=useState('')
const save=async()=>{try{await putAddressOverride(domain,address);setDomain('');setAddress('');refetch();toast({title:'Address override saved',tone:'ok'})}catch(e){toast({title:e instanceof Error?e.message:'Could not save override',tone:'danger'})}}
const remove=async(d:string)=>{try{await deleteAddressOverride(d);refetch();toast({title:`Removed ${d}`,tone:'ok'})}catch(e){toast({title:e instanceof Error?e.message:'Could not remove override',tone:'danger'})}}
const items=data?.items??[]
return <div className="space-y-4"><PageHeader title="Address overrides" description="Map a domain and all its subdomains to an IP address. These rules answer through DNS policy without creating an authoritative zone." />
<Card title="Add or edit an override" subtitle={data?.writable?'Changes are shared across replicas.':'The shared Kubernetes ConfigMap is unavailable; overrides are read-only.'}>
<div className="grid gap-3 sm:grid-cols-[1fr_1fr_auto] items-end"><Input label="Domain" placeholder="example.com" value={domain} onChange={e=>setDomain(e.target.value)} disabled={!canWrite||!data?.writable}/><Input label="Address" placeholder="10.10.3.10 or 2001:db8::1" value={address} onChange={e=>setAddress(e.target.value)} disabled={!canWrite||!data?.writable}/><Button icon={Plus} onClick={()=>void save()} disabled={!canWrite||!data?.writable||!domain.trim()||!address.trim()}>{items.some(i=>i.domain===domain.trim().toLowerCase())?'Save':'Add override'}</Button></div>
</Card>
<Card title="Overrides" subtitle={`${items.length} domain${items.length===1?'':'s'}`}>
{error?<p className="text-sm text-red-600">{error}</p>:loading?<p className="text-sm text-slate-500">Loading…</p>:items.length===0?<EmptyState title="No address overrides" description="Add a domain and destination IP above."/>:<Table columns={[{key:'domain',header:'Domain'},{key:'address',header:'Address'},{key:'actions',header:'',align:'right'}]} rows={items.map(i=>({__key:i.domain,domain:<span className="font-mono">{i.domain}</span>,address:<span className="font-mono">{i.address}</span>,actions:<span className="flex justify-end gap-1"><IconButton icon={Pencil} label={`Edit ${i.domain}`} onClick={()=>{setDomain(i.domain);setAddress(i.address)}} disabled={!canWrite||!data?.writable}/><IconButton icon={Trash2} label={`Delete ${i.domain}`} onClick={()=>void remove(i.domain)} disabled={!canWrite||!data?.writable}/></span>}))} rowKey={(row)=>String(row.__key)}/>}
</Card>
</div>
}