ui: add protected dashboard CRUD screens and assignment docs
This commit is contained in:
parent
bf42037619
commit
cb0d4d7809
24 changed files with 2662 additions and 101 deletions
137
apps/web/src/routes/dashboard.domains.tsx
Normal file
137
apps/web/src/routes/dashboard.domains.tsx
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
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>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue