137 lines
4.9 KiB
TypeScript
137 lines
4.9 KiB
TypeScript
import { Button } from "@minmon/ui/components/button";
|
|
import { StatusBadge } from "@minmon/ui/components/status-badge";
|
|
import { useMutation, useQuery } from "@tanstack/react-query";
|
|
import { createFileRoute, Link } from "@tanstack/react-router";
|
|
import { toast } from "sonner";
|
|
|
|
import { PageSection, QueryStateCard, ResourceListCard } from "@/components/dashboard-ui";
|
|
import { queryClient, trpc, trpcClient } from "@/utils/trpc";
|
|
|
|
export const Route = createFileRoute("/dashboard/domains")({
|
|
component: RouteComponent,
|
|
});
|
|
|
|
function dnsStatusVariant(status: "MATCH" | "MISMATCH" | "UNRESOLVED") {
|
|
if (status === "MATCH") return "success" as const;
|
|
if (status === "MISMATCH") return "warning" as const;
|
|
return "destructive" as const;
|
|
}
|
|
|
|
function RouteComponent() {
|
|
const domains = useQuery(trpc.domains.list.queryOptions());
|
|
const services = useQuery(trpc.services.list.queryOptions());
|
|
const removeDomain = useMutation({
|
|
mutationFn: async (id: string) => trpcClient.domains.delete.mutate({ id }),
|
|
onSuccess: async () => {
|
|
toast.success("Domain deleted");
|
|
await queryClient.invalidateQueries();
|
|
},
|
|
onError: (error) => toast.error(error.message),
|
|
});
|
|
const checkDns = useMutation({
|
|
mutationFn: async (id: string) => trpcClient.domains.check.mutate({ id }),
|
|
onSuccess: async () => {
|
|
toast.success("DNS check completed");
|
|
await queryClient.invalidateQueries();
|
|
},
|
|
onError: (error) => toast.error(error.message),
|
|
});
|
|
|
|
if (domains.isLoading || services.isLoading) {
|
|
return <QueryStateCard title="Loading domains" description="Fetching domain records and service references." />;
|
|
}
|
|
|
|
if (domains.isError || services.isError || !domains.data || !services.data) {
|
|
return <QueryStateCard title="Domains unavailable" description="The domain list could not be loaded." />;
|
|
}
|
|
|
|
const serviceMap = new Map(services.data.map((item) => [item.id, item.name]));
|
|
|
|
return (
|
|
<PageSection
|
|
title="Domains"
|
|
description="Manage service domains, expected IPs, and DNS resolution checks."
|
|
action={
|
|
<Link to="/dashboard/domains/new">
|
|
<Button>Add domain</Button>
|
|
</Link>
|
|
}
|
|
>
|
|
<ResourceListCard
|
|
title="Domain list"
|
|
description="Use the Check DNS action to resolve A records and compare them with the expected server IP."
|
|
columns={[
|
|
{
|
|
key: "name",
|
|
header: "Domain",
|
|
render: (item: (typeof domains.data)[number]) => (
|
|
<div className="grid gap-1">
|
|
<Link to="/dashboard/domains/$domainId" params={{ domainId: item.id }} className="font-medium hover:underline">
|
|
{item.name}
|
|
</Link>
|
|
<span className="text-xs text-muted-foreground">{item.remarks || "No remarks"}</span>
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
key: "service",
|
|
header: "Service",
|
|
render: (item: (typeof domains.data)[number]) => serviceMap.get(item.serviceId) || item.serviceId,
|
|
},
|
|
{
|
|
key: "expectedServerIp",
|
|
header: "Expected IP",
|
|
render: (item: (typeof domains.data)[number]) => item.expectedServerIp,
|
|
},
|
|
{
|
|
key: "lastResolvedIp",
|
|
header: "Last resolved IP",
|
|
render: (item: (typeof domains.data)[number]) => item.lastResolvedIp || "-",
|
|
},
|
|
{
|
|
key: "status",
|
|
header: "DNS status",
|
|
className: "w-36",
|
|
render: (item: (typeof domains.data)[number]) => (
|
|
<StatusBadge variant={dnsStatusVariant(item.resolutionStatus)}>{item.resolutionStatus}</StatusBadge>
|
|
),
|
|
},
|
|
{
|
|
key: "actions",
|
|
header: "Actions",
|
|
className: "w-44",
|
|
render: (item: (typeof domains.data)[number]) => (
|
|
<div className="flex flex-col items-start gap-2">
|
|
<Button
|
|
type="button"
|
|
variant="ghost"
|
|
className="h-auto px-0"
|
|
disabled={checkDns.isPending}
|
|
onClick={() => checkDns.mutate(item.id)}
|
|
>
|
|
Check DNS
|
|
</Button>
|
|
<Button
|
|
type="button"
|
|
variant="ghost"
|
|
className="h-auto px-0 text-red-600 hover:text-red-700"
|
|
disabled={removeDomain.isPending}
|
|
onClick={() => {
|
|
if (confirm(`Delete domain \"${item.name}\"?`)) {
|
|
removeDomain.mutate(item.id);
|
|
}
|
|
}}
|
|
>
|
|
Delete
|
|
</Button>
|
|
</div>
|
|
),
|
|
},
|
|
]}
|
|
items={domains.data}
|
|
emptyTitle="No domains added"
|
|
emptyDescription="Create a domain after adding a service."
|
|
/>
|
|
</PageSection>
|
|
);
|
|
}
|