import { useMemo, useState } from "react" import { serversApi } from "@/api/servers.ts" import { sshApi } from "@/api/ssh.ts" import { zodResolver } from "@hookform/resolvers/zod" import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query" import { formatDistanceToNow } from "date-fns" import { Plus, Search } from "lucide-react" import { useForm } from "react-hook-form" import { toast } from "sonner" import { z } from "zod" import { cn } from "@/lib/utils" import { truncateMiddle } from "@/lib/utils.ts" import { Badge } from "@/components/ui/badge.tsx" import { Button } from "@/components/ui/button.tsx" import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from "@/components/ui/dialog.tsx" import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage, } from "@/components/ui/form.tsx" import { Input } from "@/components/ui/input.tsx" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select.tsx" import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table.tsx" import { TooltipProvider } from "@/components/ui/tooltip.tsx" const ROLE_OPTIONS = ["master", "worker", "bastion"] as const type Role = (typeof ROLE_OPTIONS)[number] const STATUS = ["pending", "provisioning", "ready", "failed"] as const type Status = (typeof STATUS)[number] const createServerSchema = z .object({ hostname: z.string().trim().max(60, "Max 60 chars"), public_ip_address: z.string().trim().optional().or(z.literal("")), private_ip_address: z.string().trim().min(1, "Private IP address required"), role: z.enum(ROLE_OPTIONS), ssh_key_id: z.string().uuid("Pick a valid SSH key"), ssh_user: z.string().trim().min(1, "SSH user is required"), status: z.enum(STATUS).default("pending"), }) .refine( (v) => v.role !== "bastion" || (v.public_ip_address && v.public_ip_address.trim() !== ""), { message: "Public IP required for bastion", path: ["public_ip_address"] } ) type CreateServerInput = z.input const updateServerSchema = createServerSchema.partial() type UpdateServerValues = z.infer function StatusBadge({ status }: { status: Status }) { const v = status === "ready" ? "default" : status === "provisioning" ? "secondary" : status === "failed" ? "destructive" : "outline" return ( {status} ) } export const ServerPage = () => { const [filter, setFilter] = useState("") const [createOpen, setCreateOpen] = useState(false) const [updateOpen, setUpdateOpen] = useState(false) const [deleteId, setDeleteId] = useState(null) const [statusFilter, setStatusFilter] = useState("") const [roleFilter, setRoleFilter] = useState("") const [editingId, setEditingId] = useState(null) const qc = useQueryClient() const serverQ = useQuery({ queryKey: ["servers"], queryFn: () => serversApi.listServers(), }) const sshQ = useQuery({ queryKey: ["ssh_keys"], queryFn: () => sshApi.listSshKeys(), }) // Map of ssh_key_id -> label const sshLabelById = useMemo(() => { const m = new Map() for (const k of sshQ.data ?? []) { const name = k.name ? k.name : "Unnamed key" const fp = k.fingerprint ? truncateMiddle(k.fingerprint, 8) : "" m.set(k.id!, fp ? `${name} — ${fp}` : name) } return m }, [sshQ.data]) // --- Create --- const createForm = useForm({ resolver: zodResolver(createServerSchema), defaultValues: { hostname: "", private_ip_address: "", public_ip_address: "", role: "worker", ssh_key_id: "" as unknown as string, ssh_user: "", status: "pending", }, mode: "onChange", }) const roleIsBastion = createForm.watch("role") === "bastion" const pubCreate = createForm.watch("public_ip_address")?.trim() ?? "" const needPubCreate = roleIsBastion && pubCreate === "" const createMut = useMutation({ mutationFn: (values: CreateServerInput) => serversApi.createServer(values as any), onSuccess: async () => { await qc.invalidateQueries({ queryKey: ["servers"] }) createForm.reset() setCreateOpen(false) toast.success("Server created successfully") }, onError: (err: any) => { toast.error(err?.message ?? "Failed to create server") }, }) // --- Update --- const updateForm = useForm({ resolver: zodResolver(updateServerSchema), defaultValues: {}, mode: "onChange", }) const roleIsBastionU = updateForm.watch("role") === "bastion" const pubUpdate = updateForm.watch("public_ip_address")?.trim() ?? "" const needPubUpdate = roleIsBastionU && pubUpdate === "" const updateMut = useMutation({ mutationFn: ({ id, values }: { id: string; values: UpdateServerValues }) => serversApi.updateServer(id, values as any), onSuccess: async () => { await qc.invalidateQueries({ queryKey: ["servers"] }) setUpdateOpen(false) setEditingId(null) toast.success("Server updated successfully") }, onError: (err: any) => { toast.error(err?.message ?? "Failed to update server") }, }) // --- Delete --- const deleteMut = useMutation({ mutationFn: (id: string) => serversApi.deleteServer(id), onSuccess: async () => { await qc.invalidateQueries({ queryKey: ["servers"] }) setDeleteId(null) toast.success("Server deleted successfully") }, onError: (err: any) => { toast.error(err?.message ?? "Failed to delete server") }, }) const filtered = useMemo(() => { const data = serverQ.data ?? [] const q = filter.trim().toLowerCase() const textFiltered = q ? data.filter((k: any) => { return ( k.hostname?.toLowerCase().includes(q) || k.public_ip_address?.toLowerCase().includes(q) || k.private_ip_address?.toLowerCase().includes(q) || k.role?.toLowerCase().includes(q) || k.ssh_user?.toLowerCase().includes(q) ) }) : data const roleFiltered = roleFilter ? textFiltered.filter((k: any) => k.role === roleFilter) : textFiltered const statusFiltered = statusFilter ? roleFiltered.filter((k: any) => k.status === statusFilter) : roleFiltered return statusFiltered }, [filter, roleFilter, statusFilter, serverQ.data]) const onCreateSubmit = (values: CreateServerInput) => { createMut.mutate(values) } const openEdit = (srv: any) => { setEditingId(srv.id) updateForm.reset({ hostname: srv.hostname ?? "", public_ip_address: srv.public_ip_address ?? "", private_ip_address: srv.private_ip_address ?? "", role: (srv.role as Role) ?? "worker", ssh_key_id: srv.ssh_key_id ?? "", ssh_user: srv.ssh_user ?? "", status: (srv.status as Status) ?? "pending", }) setUpdateOpen(true) } if (sshQ.data?.length === 0) return
Please create an SSH key for your organization first.
if (serverQ.isLoading) return
Loading servers…
if (serverQ.error) return
Error loading servers.
return (

Servers

setFilter(e.target.value)} placeholder="Search hostname, Public IP, Private IP, role, user…" className="w-64 pl-8" />
Create server
( Hostname )} />
( Public IP Address {roleIsBastion ? "Required for bastion" : "Optional"} {roleIsBastion && (
Bastion nodes must have a{" "} Public IP.
)}
)} /> ( Private IP Address )} />
( Role )} /> ( SSH user )} />
( SSH key )} /> ( Initial status )} />
Hostname IP address Role SSH user SSH key Status Created Actions {filtered.length === 0 ? ( No servers found. ) : ( filtered.map((k: any) => ( {k.hostname}
{k.public_ip_address || "—"} {k.private_ip_address}
{k.role} {k.ssh_user} {sshLabelById.get(k.ssh_key_id) ?? "—"} {k.created_at ? `${formatDistanceToNow(new Date(k.created_at), { addSuffix: true })}` : "—"}
)) )}
{/* Update dialog */} Edit server
{ if (!editingId) return updateMut.mutate({ id: editingId, values }) })} > ( Hostname )} />
( Public IP Address {roleIsBastionU ? "Required for bastion" : "Optional"} {roleIsBastionU && (
Bastion nodes must have a Public IP.
)}
)} /> ( Private IP Address )} />
( Role )} /> ( SSH user )} />
( SSH key )} /> ( Status )} />
{/* Delete confirm dialog */} !open && setDeleteId(null)}> Delete server

This action cannot be undone. Are you sure you want to delete this server?

{JSON.stringify(serverQ.data, null, 2)}
) }