feat(web): TemplatesPage + RecordEditor — CRUD шаблонов с редактором записей

This commit is contained in:
2026-07-03 18:02:08 +07:00
parent 4e91211a89
commit 99e09d35fb
5 changed files with 608 additions and 5 deletions
+111
View File
@@ -0,0 +1,111 @@
import { Plus, Trash2 } from "lucide-react"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Textarea } from "@/components/ui/textarea"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import type { RecordDTO } from "@/api/types"
const RECORD_TYPES = ["A", "AAAA", "CNAME", "MX", "TXT", "SRV", "NS", "SOA"] as const
const typeItems = RECORD_TYPES.map((t) => ({ value: t, label: t }))
const DEFAULT_TTL = 3600
function emptyRecord(): RecordDTO {
return { type: "A", name: "", ttl: DEFAULT_TTL, values: [""] }
}
export function RecordEditor({
value,
onChange,
}: {
value: RecordDTO[]
onChange: (records: RecordDTO[]) => void
}) {
function updateRecord(index: number, patch: Partial<RecordDTO>) {
onChange(value.map((r, i) => (i === index ? { ...r, ...patch } : r)))
}
function addRecord() {
onChange([...value, emptyRecord()])
}
function removeRecord(index: number) {
onChange(value.filter((_, i) => i !== index))
}
return (
<div className="flex flex-col gap-2">
{value.map((record, index) => (
<div
key={index}
className="flex flex-col gap-2 rounded-lg border border-border bg-background/40 p-2.5 sm:flex-row sm:items-start"
>
<Select
items={typeItems}
value={record.type}
onValueChange={(v) => updateRecord(index, { type: v as string })}
>
<SelectTrigger aria-label={`Тип записи ${index + 1}`} size="sm" className="font-dns sm:w-24">
<SelectValue />
</SelectTrigger>
<SelectContent>
{typeItems.map((item) => (
<SelectItem key={item.value} value={item.value} className="font-dns">
{item.label}
</SelectItem>
))}
</SelectContent>
</Select>
<Input
aria-label={`Имя записи ${index + 1}`}
className="font-dns sm:flex-1"
placeholder="www"
value={record.name}
onChange={(e) => updateRecord(index, { name: e.target.value })}
/>
<Input
aria-label={`TTL записи ${index + 1}`}
type="number"
min={0}
className="font-dns sm:w-24"
value={record.ttl}
onChange={(e) => updateRecord(index, { ttl: Number(e.target.value) })}
/>
<Textarea
aria-label={`Значения записи ${index + 1}`}
className="font-dns sm:flex-1"
placeholder="192.0.2.1"
rows={1}
value={record.values.join("\n")}
onChange={(e) => updateRecord(index, { values: e.target.value.split("\n") })}
/>
<Button
type="button"
variant="destructive"
size="icon-sm"
aria-label={`Удалить запись ${index + 1}`}
onClick={() => removeRecord(index)}
>
<Trash2 className="size-3.5" strokeWidth={1.75} />
</Button>
</div>
))}
<Button type="button" variant="outline" size="sm" onClick={addRecord} className="self-start">
<Plus className="size-3.5" strokeWidth={1.75} />
Добавить запись
</Button>
</div>
)
}