Import source: src/components/features/dns-editor

Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality.
This commit is contained in:
arsam
2026-09-18 23:10:46 +03:30
parent 93d46851be
commit 444efdf92e
+267
View File
@@ -0,0 +1,267 @@
"use client";
import type { ColumnDef } from "@tanstack/react-table";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { Copy, FileDown, FileUp, Plus, Trash2 } from "lucide-react";
import { useState } from "react";
import type { DnsRecord } from "@/api/contracts/models";
import { panelService } from "@/api/services/panel-service";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { DataTable } from "@/components/common/data-table";
import { Card, CardContent } from "@/components/ui/card";
import { CodeEditor } from "@/components/common/code-editor";
import { Input } from "@/components/ui/input";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
const columns: ColumnDef<DnsRecord>[] = [
{ accessorKey: "name", header: "Name" },
{ accessorKey: "ttl", header: "TTL" },
{ accessorKey: "class", header: "Class" },
{
accessorKey: "type",
header: "Type",
cell: ({ row }) => <Badge variant="outline">{row.original.type}</Badge>,
},
{ accessorKey: "record", header: "Record" },
{
accessorKey: "warning",
header: "Warnings",
cell: ({ row }) => row.original.warning ?? "None",
},
];
const dnsTypes: DnsRecord["type"][] = [
"A",
"AAAA",
"CNAME",
"MX",
"TXT",
"SRV",
"CAA",
"NS",
"PTR",
];
export function DnsEditor() {
const queryClient = useQueryClient();
const [dialogOpen, setDialogOpen] = useState(false);
const [record, setRecord] = useState<Partial<DnsRecord>>({
name: "api.example.com.",
ttl: 14400,
type: "A",
record: "198.51.100.40",
});
const [error, setError] = useState("");
const { data, isLoading } = useQuery({
queryKey: ["dns-records"],
queryFn: () => panelService.domains.records(),
});
const createRecord = useMutation({
mutationFn: () => panelService.domains.saveRecord(record),
onSuccess: async (response) => {
if (response.error) {
setError(response.error.message);
return;
}
await queryClient.invalidateQueries({ queryKey: ["dns-records"] });
setDialogOpen(false);
setError("");
setRecord({
name: "api.example.com.",
ttl: 14400,
type: "A",
record: "198.51.100.40",
});
},
onError: (mutationError) => {
setError(
mutationError instanceof Error ? mutationError.message : "DNS record creation failed.",
);
},
});
return (
<div className="space-y-4">
<Card>
<CardContent className="space-y-2 p-3">
<div className="-mx-1 flex items-center gap-1.5 overflow-x-auto px-1 pb-1">
{["All", "A", "AAAA", "CNAME", "MX", "TXT", "SRV", "CAA", "NS", "PTR"].map(
(type) => (
<Button
key={type}
variant={type === "All" ? "default" : "outline"}
size="sm"
className="shrink-0"
>
{type}
</Button>
),
)}
</div>
<div className="flex flex-wrap items-center gap-2">
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
<DialogTrigger asChild>
<Button size="sm">
<Plus className="h-4 w-4" />
Add record
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Add DNS record</DialogTitle>
<DialogDescription>
Create a DNS record in the active zone and refresh the zone editor.
</DialogDescription>
</DialogHeader>
<form
className="grid gap-3 sm:grid-cols-2"
onSubmit={(event) => {
event.preventDefault();
setError("");
createRecord.mutate();
}}
>
<label className="space-y-1 text-sm sm:col-span-2">
<span className="font-medium">Name</span>
<Input
value={record.name ?? ""}
onChange={(event) =>
setRecord((value) => ({ ...value, name: event.target.value }))
}
/>
</label>
<label className="space-y-1 text-sm">
<span className="font-medium">Type</span>
<Select
value={record.type ?? "A"}
onValueChange={(value) =>
setRecord((current) => ({
...current,
type: value as DnsRecord["type"],
}))
}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{dnsTypes.map((type) => (
<SelectItem key={type} value={type}>
{type}
</SelectItem>
))}
</SelectContent>
</Select>
</label>
<label className="space-y-1 text-sm">
<span className="font-medium">TTL</span>
<Input
type="number"
min={60}
value={record.ttl ?? 14400}
onChange={(event) =>
setRecord((value) => ({
...value,
ttl: Number(event.target.value),
}))
}
/>
</label>
<label className="space-y-1 text-sm sm:col-span-2">
<span className="font-medium">Record value</span>
<Input
value={record.record ?? ""}
onChange={(event) =>
setRecord((value) => ({ ...value, record: event.target.value }))
}
/>
</label>
{record.type === "MX" || record.type === "SRV" ? (
<label className="space-y-1 text-sm">
<span className="font-medium">Priority</span>
<Input
type="number"
min={0}
value={record.priority ?? 10}
onChange={(event) =>
setRecord((value) => ({
...value,
priority: Number(event.target.value),
}))
}
/>
</label>
) : null}
{error ? (
<div className="rounded-md border border-destructive/30 bg-destructive/10 p-2 text-sm text-destructive sm:col-span-2">
{error}
</div>
) : null}
<div className="flex justify-end gap-2 sm:col-span-2">
<Button
type="button"
variant="outline"
onClick={() => setDialogOpen(false)}
disabled={createRecord.isPending}
>
Cancel
</Button>
<Button type="submit" disabled={createRecord.isPending}>
{createRecord.isPending ? "Creating..." : "Create record"}
</Button>
</div>
</form>
</DialogContent>
</Dialog>
<Button size="sm" variant="outline">
<Copy className="h-4 w-4" />
Duplicate
</Button>
<Button size="sm" variant="outline">
<FileUp className="h-4 w-4" />
Import
</Button>
<Button size="sm" variant="outline">
<FileDown className="h-4 w-4" />
Export
</Button>
<Button size="sm" variant="destructive" className="sm:ml-auto">
<Trash2 className="h-4 w-4" />
Delete
</Button>
</div>
</CardContent>
</Card>
{isLoading ? (
<div className="skeleton h-80 rounded-lg border border-border/50" />
) : (
<DataTable
data={data?.data ?? []}
columns={columns}
searchPlaceholder="Filter by name or record"
/>
)}
<CodeEditor
title="Raw zone editor"
language="dns"
value={
"$TTL 14400\nexample.com. IN A 198.51.100.24\nwww IN CNAME example.com.\n@ IN MX 10 mail.example.com.\n"
}
/>
</div>
);
}