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:
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user