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