Gates / test-aarch64 (push) Successful in 6m45s
Gates / frontend (push) Successful in 51s
Gates / test (push) Successful in 1m37s
Gates / container (push) Failing after 7m31s
Gates / package (push) Failing after 15m14s
CI / gates (push) Failing after 24m27s
247 lines
6.8 KiB
TypeScript
247 lines
6.8 KiB
TypeScript
import { useId, useState, type FormEvent } from "react";
|
|
import { useSuspenseQuery } from "@tanstack/react-query";
|
|
import {
|
|
localRecordCreateMutation,
|
|
localRecordDeleteMutation,
|
|
localRecordUpdateMutation,
|
|
localRecordsQuery,
|
|
} from "@/lib/queries";
|
|
import type { LocalRecord, LocalRecordInput, LocalRecordType } from "@/lib/types";
|
|
import InlineError from "@/lib/InlineError";
|
|
import { useCrudForm } from "@/ui/useCrudForm";
|
|
import {
|
|
formCardClass,
|
|
inputClass,
|
|
largeButtonClass,
|
|
largePrimaryButtonClass,
|
|
rowButtonClass,
|
|
tableWrapClass,
|
|
} from "@/ui/classes";
|
|
import { READ_ONLY_HINT, useReadOnlyConfig } from "@/features/settings/authority";
|
|
|
|
const RTYPES: readonly LocalRecordType[] = ["A", "AAAA", "CNAME"];
|
|
|
|
function RecordForm({
|
|
initial,
|
|
busy,
|
|
readOnly,
|
|
error,
|
|
onSubmit,
|
|
onCancel,
|
|
}: {
|
|
initial?: LocalRecord;
|
|
busy: boolean;
|
|
readOnly: boolean;
|
|
error: unknown;
|
|
onSubmit: (input: LocalRecordInput) => void;
|
|
onCancel: () => void;
|
|
}) {
|
|
const id = useId();
|
|
const [name, setName] = useState(initial?.name ?? "");
|
|
const [rtype, setRtype] = useState<LocalRecordType>(initial?.rtype ?? "A");
|
|
const [value, setValue] = useState(initial?.value ?? "");
|
|
const [ttl, setTtl] = useState(initial === undefined ? "" : String(initial.ttl));
|
|
|
|
function submit(event: FormEvent<HTMLFormElement>) {
|
|
event.preventDefault();
|
|
const input: LocalRecordInput = { name: name.trim(), rtype, value: value.trim() };
|
|
if (ttl.trim() !== "") input.ttl = Number(ttl);
|
|
onSubmit(input);
|
|
}
|
|
|
|
return (
|
|
<form onSubmit={submit} className={formCardClass}>
|
|
<h3 className="font-medium">{initial === undefined ? "New record" : `Edit ${initial.name}`}</h3>
|
|
<div>
|
|
<label htmlFor={`${id}-name`} className="block text-sm font-medium">
|
|
Name
|
|
</label>
|
|
<input
|
|
id={`${id}-name`}
|
|
required
|
|
value={name}
|
|
onChange={(event) => setName(event.target.value)}
|
|
placeholder="nas.lan.home"
|
|
className={inputClass}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label htmlFor={`${id}-rtype`} className="block text-sm font-medium">
|
|
Type
|
|
</label>
|
|
<select
|
|
id={`${id}-rtype`}
|
|
value={rtype}
|
|
onChange={(event) => setRtype(event.target.value as LocalRecordType)}
|
|
className={inputClass}
|
|
>
|
|
{RTYPES.map((option) => (
|
|
<option key={option} value={option}>
|
|
{option}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label htmlFor={`${id}-value`} className="block text-sm font-medium">
|
|
Value
|
|
</label>
|
|
<input
|
|
id={`${id}-value`}
|
|
required
|
|
value={value}
|
|
onChange={(event) => setValue(event.target.value)}
|
|
placeholder={
|
|
rtype === "CNAME" ? "target.example.com" : rtype === "AAAA" ? "fd00::10" : "192.168.1.10"
|
|
}
|
|
className={inputClass}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label htmlFor={`${id}-ttl`} className="block text-sm font-medium">
|
|
TTL (seconds)
|
|
</label>
|
|
<input
|
|
id={`${id}-ttl`}
|
|
type="number"
|
|
min={0}
|
|
value={ttl}
|
|
onChange={(event) => setTtl(event.target.value)}
|
|
placeholder="300"
|
|
className={inputClass}
|
|
/>
|
|
</div>
|
|
<div className="flex gap-2">
|
|
<button
|
|
type="submit"
|
|
disabled={busy || readOnly}
|
|
title={readOnly ? READ_ONLY_HINT : undefined}
|
|
className={largePrimaryButtonClass}
|
|
>
|
|
{busy ? "Saving…" : "Save"}
|
|
</button>
|
|
<button type="button" onClick={onCancel} className={largeButtonClass}>
|
|
Cancel
|
|
</button>
|
|
</div>
|
|
<InlineError error={error} />
|
|
</form>
|
|
);
|
|
}
|
|
|
|
export default function RecordsTab() {
|
|
const records = useSuspenseQuery(localRecordsQuery()).data;
|
|
const { create, update, remove, form, openForm, closeForm, onSubmit, onDelete } = useCrudForm<
|
|
LocalRecord,
|
|
LocalRecordInput
|
|
>({
|
|
create: localRecordCreateMutation,
|
|
update: localRecordUpdateMutation,
|
|
remove: localRecordDeleteMutation,
|
|
confirmDelete: (record) => `Delete record "${record.name}"?`,
|
|
});
|
|
const readOnly = useReadOnlyConfig();
|
|
|
|
return (
|
|
<div>
|
|
<div className="mt-4 flex items-center justify-between">
|
|
<p className="text-sm text-zinc-500">Answers served directly for LAN names. Changes apply live.</p>
|
|
<button
|
|
type="button"
|
|
onClick={() => openForm({ mode: "create" })}
|
|
disabled={readOnly}
|
|
title={readOnly ? READ_ONLY_HINT : undefined}
|
|
className={largePrimaryButtonClass}
|
|
>
|
|
Add record
|
|
</button>
|
|
</div>
|
|
<InlineError error={remove.error} />
|
|
{form?.mode === "create" && (
|
|
<RecordForm
|
|
busy={create.isPending}
|
|
readOnly={readOnly}
|
|
error={create.error}
|
|
onSubmit={onSubmit}
|
|
onCancel={closeForm}
|
|
/>
|
|
)}
|
|
<div className={tableWrapClass}>
|
|
<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">
|
|
Name
|
|
</th>
|
|
<th scope="col" className="py-2 pr-4 font-medium">
|
|
Type
|
|
</th>
|
|
<th scope="col" className="py-2 pr-4 font-medium">
|
|
Value
|
|
</th>
|
|
<th scope="col" className="py-2 pr-4 font-medium">
|
|
TTL
|
|
</th>
|
|
<th scope="col" className="py-2">
|
|
<span className="sr-only">Actions</span>
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{records.length === 0 && (
|
|
<tr>
|
|
<td colSpan={5} className="py-4 text-zinc-500">
|
|
No local records yet.
|
|
</td>
|
|
</tr>
|
|
)}
|
|
{records.map((record) => (
|
|
<tr key={record.id} className="border-b border-zinc-100 dark:border-zinc-900">
|
|
{form?.mode === "edit" && form.entity.id === record.id ? (
|
|
<td colSpan={5}>
|
|
<RecordForm
|
|
initial={record}
|
|
busy={update.isPending}
|
|
readOnly={readOnly}
|
|
error={update.error}
|
|
onSubmit={onSubmit}
|
|
onCancel={closeForm}
|
|
/>
|
|
</td>
|
|
) : (
|
|
<>
|
|
<td className="py-2 pr-4 font-mono">{record.name}</td>
|
|
<td className="py-2 pr-4">{record.rtype}</td>
|
|
<td className="py-2 pr-4 font-mono">{record.value}</td>
|
|
<td className="py-2 pr-4">{record.ttl}</td>
|
|
<td className="py-2 text-right whitespace-nowrap">
|
|
<button
|
|
type="button"
|
|
onClick={() => openForm({ mode: "edit", entity: record })}
|
|
disabled={readOnly}
|
|
title={readOnly ? READ_ONLY_HINT : undefined}
|
|
className={`${rowButtonClass} disabled:opacity-50`}
|
|
>
|
|
Edit
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => onDelete(record)}
|
|
disabled={remove.isPending || readOnly}
|
|
title={readOnly ? READ_ONLY_HINT : undefined}
|
|
className={`${rowButtonClass} text-red-600 disabled:opacity-50 dark:text-red-400`}
|
|
>
|
|
Delete
|
|
</button>
|
|
</td>
|
|
</>
|
|
)}
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|