203 lines
6.1 KiB
TypeScript
203 lines
6.1 KiB
TypeScript
import { useId, useState, type FormEvent } from "react";
|
|
import { useMutation, useQueryClient, useSuspenseQuery } from "@tanstack/react-query";
|
|
import {
|
|
forwardZoneCreateMutation,
|
|
forwardZoneDeleteMutation,
|
|
forwardZoneUpdateMutation,
|
|
forwardZonesQuery,
|
|
} from "@/lib/queries";
|
|
import type { ForwardZone, ForwardZoneInput } from "@/lib/types";
|
|
import InlineError from "@/lib/InlineError";
|
|
|
|
const inputClass =
|
|
"mt-1 w-full rounded border border-zinc-300 bg-white px-3 py-2 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600 dark:border-zinc-700 dark:bg-zinc-900";
|
|
const primaryButtonClass =
|
|
"rounded bg-blue-600 px-3 py-2 font-medium text-white disabled:opacity-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600";
|
|
const secondaryButtonClass =
|
|
"rounded border border-zinc-300 px-3 py-2 font-medium focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600 dark:border-zinc-700";
|
|
const rowButtonClass =
|
|
"rounded px-2 py-1 text-sm text-blue-600 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600 dark:text-blue-400";
|
|
|
|
type FormState = { mode: "create" } | { mode: "edit"; zone: ForwardZone };
|
|
|
|
function ZoneForm({
|
|
initial,
|
|
busy,
|
|
error,
|
|
onSubmit,
|
|
onCancel,
|
|
}: {
|
|
initial?: ForwardZone;
|
|
busy: boolean;
|
|
error: unknown;
|
|
onSubmit: (input: ForwardZoneInput) => void;
|
|
onCancel: () => void;
|
|
}) {
|
|
const id = useId();
|
|
const [zone, setZone] = useState(initial?.zone ?? "");
|
|
const [resolver, setResolver] = useState(initial?.resolver ?? "");
|
|
|
|
function submit(event: FormEvent<HTMLFormElement>) {
|
|
event.preventDefault();
|
|
onSubmit({ zone: zone.trim(), resolver: resolver.trim() });
|
|
}
|
|
|
|
return (
|
|
<form
|
|
onSubmit={submit}
|
|
className="mt-4 max-w-lg space-y-3 rounded border border-zinc-200 p-4 dark:border-zinc-800"
|
|
>
|
|
<h3 className="font-medium">{initial === undefined ? "New forward zone" : `Edit ${initial.zone}`}</h3>
|
|
<div>
|
|
<label htmlFor={`${id}-zone`} className="block text-sm font-medium">
|
|
Zone
|
|
</label>
|
|
<input
|
|
id={`${id}-zone`}
|
|
required
|
|
value={zone}
|
|
onChange={(event) => setZone(event.target.value)}
|
|
placeholder="lan.home"
|
|
className={inputClass}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label htmlFor={`${id}-resolver`} className="block text-sm font-medium">
|
|
Resolver
|
|
</label>
|
|
<input
|
|
id={`${id}-resolver`}
|
|
required
|
|
value={resolver}
|
|
onChange={(event) => setResolver(event.target.value)}
|
|
placeholder="udp://192.168.1.1:53"
|
|
className={inputClass}
|
|
/>
|
|
</div>
|
|
<div className="flex gap-2">
|
|
<button type="submit" disabled={busy} className={primaryButtonClass}>
|
|
{busy ? "Saving…" : "Save"}
|
|
</button>
|
|
<button type="button" onClick={onCancel} className={secondaryButtonClass}>
|
|
Cancel
|
|
</button>
|
|
</div>
|
|
<InlineError error={error} />
|
|
</form>
|
|
);
|
|
}
|
|
|
|
export default function ZonesTab() {
|
|
const zones = useSuspenseQuery(forwardZonesQuery()).data;
|
|
const queryClient = useQueryClient();
|
|
const create = useMutation(forwardZoneCreateMutation(queryClient));
|
|
const update = useMutation(forwardZoneUpdateMutation(queryClient));
|
|
const remove = useMutation(forwardZoneDeleteMutation(queryClient));
|
|
const [form, setForm] = useState<FormState | null>(null);
|
|
|
|
function openForm(next: FormState) {
|
|
create.reset();
|
|
update.reset();
|
|
setForm(next);
|
|
}
|
|
|
|
function onSubmit(input: ForwardZoneInput) {
|
|
if (form === null) return;
|
|
if (form.mode === "create") {
|
|
create.mutate(input, { onSuccess: () => setForm(null) });
|
|
} else {
|
|
update.mutate({ id: form.zone.id, input }, { onSuccess: () => setForm(null) });
|
|
}
|
|
}
|
|
|
|
function onDelete(zone: ForwardZone) {
|
|
if (!window.confirm(`Delete forward zone "${zone.zone}"?`)) return;
|
|
remove.mutate(zone.id);
|
|
}
|
|
|
|
return (
|
|
<div>
|
|
<div className="mt-4 flex items-center justify-between">
|
|
<p className="text-sm text-zinc-500">
|
|
Names under these zones go to their own resolver. Changes apply live.
|
|
</p>
|
|
<button type="button" onClick={() => openForm({ mode: "create" })} className={primaryButtonClass}>
|
|
Add zone
|
|
</button>
|
|
</div>
|
|
<InlineError error={remove.error} />
|
|
{form?.mode === "create" && (
|
|
<ZoneForm
|
|
busy={create.isPending}
|
|
error={create.error}
|
|
onSubmit={onSubmit}
|
|
onCancel={() => setForm(null)}
|
|
/>
|
|
)}
|
|
<div className="mt-4 overflow-x-auto">
|
|
<table className="w-full text-left text-sm">
|
|
<thead>
|
|
<tr className="border-b border-zinc-200 text-zinc-500 dark:border-zinc-800">
|
|
<th scope="col" className="py-2 pr-4 font-medium">
|
|
Zone
|
|
</th>
|
|
<th scope="col" className="py-2 pr-4 font-medium">
|
|
Resolver
|
|
</th>
|
|
<th scope="col" className="py-2">
|
|
<span className="sr-only">Actions</span>
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{zones.length === 0 && (
|
|
<tr>
|
|
<td colSpan={3} className="py-4 text-zinc-500">
|
|
No forward zones yet.
|
|
</td>
|
|
</tr>
|
|
)}
|
|
{zones.map((zone) => (
|
|
<tr key={zone.id} className="border-b border-zinc-100 dark:border-zinc-900">
|
|
{form?.mode === "edit" && form.zone.id === zone.id ? (
|
|
<td colSpan={3}>
|
|
<ZoneForm
|
|
initial={zone}
|
|
busy={update.isPending}
|
|
error={update.error}
|
|
onSubmit={onSubmit}
|
|
onCancel={() => setForm(null)}
|
|
/>
|
|
</td>
|
|
) : (
|
|
<>
|
|
<td className="py-2 pr-4 font-mono">{zone.zone}</td>
|
|
<td className="py-2 pr-4 font-mono">{zone.resolver}</td>
|
|
<td className="py-2 text-right whitespace-nowrap">
|
|
<button
|
|
type="button"
|
|
onClick={() => openForm({ mode: "edit", zone })}
|
|
className={rowButtonClass}
|
|
>
|
|
Edit
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => onDelete(zone)}
|
|
disabled={remove.isPending}
|
|
className={`${rowButtonClass} text-red-600 dark:text-red-400`}
|
|
>
|
|
Delete
|
|
</button>
|
|
</td>
|
|
</>
|
|
)}
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|