Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BwxdSt4reTm7Dj1oxRvpP3
177 lines
6.3 KiB
TypeScript
177 lines
6.3 KiB
TypeScript
import { useState } from "react"
|
|
import { Link } from "react-router-dom"
|
|
import { Inbox, Loader2, Upload } from "lucide-react"
|
|
import { Button } from "@/components/ui/button"
|
|
import { StatusBadge } from "@/components/StatusBadge"
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "@/components/ui/select"
|
|
import {
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableHead,
|
|
TableHeader,
|
|
TableRow,
|
|
} from "@/components/ui/table"
|
|
import {
|
|
useAccounts,
|
|
useDomains,
|
|
useImportZones,
|
|
useSetDomainTemplate,
|
|
useTemplates,
|
|
} from "@/hooks/useApi"
|
|
|
|
const NO_TEMPLATE = "__none__"
|
|
|
|
export function DomainsPage() {
|
|
const domains = useDomains()
|
|
const accounts = useAccounts()
|
|
const templates = useTemplates()
|
|
const importZones = useImportZones()
|
|
const setTemplate = useSetDomainTemplate()
|
|
|
|
const accountList = accounts.data ?? []
|
|
const templateList = templates.data ?? []
|
|
const domainList = domains.data ?? []
|
|
|
|
const [importAccountId, setImportAccountId] = useState<string | null>(null)
|
|
const selectedImportAccount = importAccountId ?? accountList[0]?.id ?? null
|
|
|
|
const accountItems = accountList.map((a) => ({
|
|
value: a.id,
|
|
label: `${a.provider} · ${a.comment}`,
|
|
}))
|
|
|
|
function accountLabel(id: string) {
|
|
const acc = accountList.find((a) => a.id === id)
|
|
return acc ? `${acc.provider} · ${acc.comment}` : id
|
|
}
|
|
|
|
function onImport() {
|
|
if (!selectedImportAccount) return
|
|
importZones.mutate(selectedImportAccount)
|
|
}
|
|
|
|
function onTemplateChange(domainId: string, value: unknown) {
|
|
const templateId = value === NO_TEMPLATE ? null : (value as string)
|
|
setTemplate.mutate({ id: domainId, templateId })
|
|
}
|
|
|
|
return (
|
|
<div className="mx-auto flex max-w-5xl flex-col gap-6 px-6 py-8">
|
|
<header className="flex flex-col gap-1">
|
|
<span className="font-dns text-[11px] tracking-wider text-muted-foreground uppercase">
|
|
zones
|
|
</span>
|
|
<h1 className="text-xl font-semibold tracking-tight text-foreground">Domains</h1>
|
|
</header>
|
|
|
|
<div className="flex flex-wrap items-end gap-3 rounded-xl border border-border bg-card/60 p-4">
|
|
<div className="flex flex-col gap-1.5">
|
|
<span className="text-xs font-medium text-muted-foreground">Учётная запись</span>
|
|
<Select
|
|
items={accountItems}
|
|
value={selectedImportAccount}
|
|
onValueChange={(v) => setImportAccountId(v as string)}
|
|
>
|
|
<SelectTrigger aria-label="Учётная запись для импорта" className="min-w-56">
|
|
<SelectValue placeholder="Выберите учётку" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{accountItems.map((item) => (
|
|
<SelectItem key={item.value} value={item.value}>
|
|
{item.label}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<Button onClick={onImport} disabled={!selectedImportAccount || importZones.isPending}>
|
|
{importZones.isPending ? (
|
|
<Loader2 className="size-4 animate-spin" strokeWidth={1.75} />
|
|
) : (
|
|
<Upload className="size-4" strokeWidth={1.75} />
|
|
)}
|
|
Импортировать зоны
|
|
</Button>
|
|
{importZones.isError && (
|
|
<span className="font-dns text-xs text-destructive">{importZones.error.message}</span>
|
|
)}
|
|
</div>
|
|
|
|
{setTemplate.isError && (
|
|
<span role="alert" className="font-dns text-xs text-destructive">
|
|
{setTemplate.error?.message}
|
|
</span>
|
|
)}
|
|
{domainList.length === 0 ? (
|
|
<div className="flex flex-col items-center gap-2 rounded-xl border border-dashed border-border px-4 py-12 text-center text-sm text-muted-foreground">
|
|
<Inbox className="size-6" strokeWidth={1.5} />
|
|
Доменов пока нет — импортируйте зоны из учётной записи.
|
|
</div>
|
|
) : (
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead>Zone</TableHead>
|
|
<TableHead>Учётка</TableHead>
|
|
<TableHead>Шаблон</TableHead>
|
|
<TableHead>Статус</TableHead>
|
|
<TableHead className="text-right">Действия</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{domainList.map((d) => {
|
|
const templateItems = [
|
|
{ value: NO_TEMPLATE, label: "Без шаблона" },
|
|
...templateList.map((t) => ({ value: t.id, label: t.name })),
|
|
]
|
|
return (
|
|
<TableRow key={d.id}>
|
|
<TableCell className="font-dns">{d.zoneName}</TableCell>
|
|
<TableCell className="font-dns text-xs text-muted-foreground">
|
|
{accountLabel(d.providerAccountId)}
|
|
</TableCell>
|
|
<TableCell>
|
|
<Select
|
|
items={templateItems}
|
|
value={d.templateId ?? NO_TEMPLATE}
|
|
onValueChange={(v) => onTemplateChange(d.id, v)}
|
|
>
|
|
<SelectTrigger aria-label={`Шаблон: ${d.zoneName}`} size="sm" className="min-w-40">
|
|
<SelectValue placeholder="Без шаблона" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{templateItems.map((item) => (
|
|
<SelectItem key={item.value} value={item.value}>
|
|
{item.label}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</TableCell>
|
|
<TableCell>
|
|
<StatusBadge status={d.templateId ? d.lastCheckStatus : "no_template"} />
|
|
</TableCell>
|
|
<TableCell className="text-right">
|
|
<div className="flex justify-end gap-1.5">
|
|
<Button variant="outline" size="sm" render={<Link to={`/domains/${d.id}`} />}>
|
|
Diff
|
|
</Button>
|
|
</div>
|
|
</TableCell>
|
|
</TableRow>
|
|
)
|
|
})}
|
|
</TableBody>
|
|
</Table>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|