1 parent
98b65945f6
commit
0b6dcbd547
13 files changed
+417
-68
No files matched your search
@@ -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 />} />
|
||||
|
||||
@@ -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
@@ -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')
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
Reference in new issue
Block a user