feat: Complete AG Loadbalancer & Cluster API

Refactor routing logic (Chi can be a pain when you're managing large sets of routes, but its one of the better options when considering a potential gRPC future)
       Upgrade API Generation to fully support OAS3.1
      Update swagger interface to RapiDoc - the old swagger interface doesnt support OAS3.1 yet
      Docs are now embedded as part of the UI - once logged in they pick up the cookies and org id from what gets set by the UI, but you can override it
      Other updates include better portability of the db-studio

Signed-off-by: allanice001 <allanice001@gmail.com>
This commit is contained in:
allanice001
2025-11-17 04:59:39 +00:00
parent 165d2a2af1
commit 7985b310c5
67 changed files with 10745 additions and 3283 deletions

View File

@@ -1,6 +1,5 @@
import { useMemo, useState } from "react"
import { annotationsApi } from "@/api/annotations.ts"
import { labelsApi } from "@/api/labels.ts"
import type { DtoLabelResponse } from "@/sdk"
import { zodResolver } from "@hookform/resolvers/zod"
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"

View File

@@ -1,66 +1,28 @@
import { useMemo, useState } from "react"
import { credentialsApi } from "@/api/credentials"
import { zodResolver } from "@hookform/resolvers/zod"
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
import {
AlertTriangle,
Eye,
Loader2,
MoreHorizontal,
Pencil,
Plus,
Search,
Trash2,
} from "lucide-react"
import { Controller, useForm } from "react-hook-form"
import { toast } from "sonner"
import { z } from "zod"
import { useMemo, useState } from "react";
import { credentialsApi } from "@/api/credentials";
import { zodResolver } from "@hookform/resolvers/zod";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { AlertTriangle, Eye, Loader2, MoreHorizontal, Pencil, Plus, Search, Trash2 } from "lucide-react";
import { Controller, useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from "@/components/ui/alert-dialog";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import { Textarea } from "@/components/ui/textarea";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form"
import { Input } from "@/components/ui/input"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import { Switch } from "@/components/ui/switch"
import { Textarea } from "@/components/ui/textarea"
// -------------------- Constants --------------------
@@ -192,7 +154,9 @@ function extractErr(e: any): string {
try {
const msg = (e as any)?.response?.data?.message || (e as any)?.message
if (msg) return String(msg)
} catch {}
} catch {
return "Unknown error"
}
return "Unknown error"
}

View File

@@ -224,9 +224,12 @@ export const DnsPage = () => {
const r53Credentials = useMemo(() => (credentialQ.data ?? []).filter(isR53), [credentialQ.data])
useEffect(() => {
if (!selected && domainsQ.data && domainsQ.data.length) {
setSelected(domainsQ.data[0]!)
const setSelectedDns = () => {
if (!selected && domainsQ.data && domainsQ.data.length) {
setSelected(domainsQ.data[0]!)
}
}
setSelectedDns()
}, [domainsQ.data, selected])
const filteredDomains = useMemo(() => {

View File

@@ -0,0 +1,171 @@
import { useEffect, useRef, useState, type FC } from "react"
import { useTheme } from "next-themes"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import "rapidoc"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card.tsx"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select.tsx"
type RdThemeMode = "auto" | "light" | "dark"
export const DocsPage: FC = () => {
const rdRef = useRef<any>(null)
const { theme, systemTheme, setTheme } = useTheme()
const [orgId, setOrgId] = useState("")
const [rdThemeMode, setRdThemeMode] = useState<RdThemeMode>("auto")
useEffect(() => {
const stateSetter = () => {
const stored = localStorage.getItem("autoglue.org")
if (stored) setOrgId(stored)
}
stateSetter()
}, [])
useEffect(() => {
const rd = rdRef.current
if (!rd) return
let effectiveTheme: "light" | "dark" = "light"
if (rdThemeMode === "light") {
effectiveTheme = "light"
} else if (rdThemeMode === "dark") {
effectiveTheme = "dark"
} else {
const appTheme = theme === "system" ? systemTheme : theme
effectiveTheme = appTheme === "dark" ? "dark" : "light"
}
rd.setAttribute("theme", effectiveTheme)
if (typeof window !== "undefined") {
const defaultServer = `${window.location.origin}/api/v1`
rd.setAttribute("default-api-server", defaultServer)
}
if (orgId) {
rd.setAttribute("api-key-name", "X-ORG-ID")
rd.setAttribute("api-key-location", "header")
rd.setAttribute("api-key-value", orgId)
} else {
rd.removeAttribute("api-key-value")
}
}, [theme, systemTheme, rdThemeMode, orgId])
const handleSaveOrg = () => {
const trimmed = orgId.trim()
localStorage.setItem("autoglue.org", trimmed)
const rd = rdRef.current
if (!rd) return
if (trimmed) {
rd.setAttribute("api-key-value", trimmed)
} else {
rd.removeAttribute("api-key-value")
}
}
const handleResetOrg = () => {
localStorage.removeItem("autoglue.org")
setOrgId("")
const rd = rdRef.current
if (!rd) return
rd.removeAttribute("api-key-value")
}
return (
<div className="flex h-[100svh] flex-col">
{/* Control bar */}
<Card className="rounded-none border-b">
<CardHeader className="py-3">
<CardTitle className="flex flex-wrap items-center justify-between gap-4 text-base">
<span>AutoGlue API Docs</span>
<div className="flex items-center gap-2 text-xs">
<div className="flex flex-wrap items-center gap-3 text-xs">
{/* Theme selector */}
<div className="flex items-center gap-2">
<span className="text-muted-foreground">Docs theme</span>
<Select
value={rdThemeMode}
onValueChange={(v) => {
const mode = v as RdThemeMode
setRdThemeMode(mode)
if (mode === "auto") {
setTheme("system")
} else {
setTheme(v)
}
}}
>
<SelectTrigger className="h-8 w-[120px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="auto">Auto (match app)</SelectItem>
<SelectItem value="light">Light</SelectItem>
<SelectItem value="dark">Dark</SelectItem>
</SelectContent>
</Select>
</div>
{/* Org ID controls */}
<div className="flex items-center gap-2">
<span className="text-muted-foreground">Org ID (X-ORG-ID)</span>
<Input
className="h-8 w-80"
value={orgId}
onChange={(e) => setOrgId(e.target.value)}
placeholder="org_..."
/>
<Button size="sm" onClick={handleSaveOrg}>
Save
</Button>
<Button size="sm" variant="outline" onClick={handleResetOrg}>
Reset
</Button>
</div>
</div>
</div>
</CardTitle>
</CardHeader>
<CardContent className="text-muted-foreground py-0 pb-2 text-xs">
Requests from <code>&lt;rapi-doc&gt;</code> will include:
<code className="ml-1">Cookie: ag_jwt=</code> and{" "}
<code className="ml-1">X-ORG-ID={orgId}</code>
{!orgId && <> (set an Org ID above to send an X-ORG-ID header)</>}
</CardContent>
</Card>
{/* @ts-expect-error ts-2339 */}
<rapi-doc
ref={rdRef}
id="autoglue-docs"
spec-url="/swagger/swagger.json"
render-style="read"
show-header="false"
persist-auth="true"
allow-advanced-search="true"
schema-description-expanded="true"
allow-schema-description-expand-toggle="false"
allow-spec-file-download="true"
allow-spec-file-load="false"
allow-spec-url-load="false"
allow-try="true"
schema-style="tree"
fetch-credentials="include"
/>
</div>
)
}

View File

@@ -1,8 +1,6 @@
import { useEffect, useMemo } from "react"
import { credentialsApi } from "@/api/credentials.ts"
import { useEffect } from "react"
import { withRefresh } from "@/api/with-refresh.ts"
import { orgStore } from "@/auth/org.ts"
import type { DtoCredentialOut } from "@/sdk"
import { makeOrgsApi } from "@/sdkClient.ts"
import { zodResolver } from "@hookform/resolvers/zod"
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
@@ -22,6 +20,7 @@ import {
} from "@/components/ui/form.tsx"
import { Input } from "@/components/ui/input.tsx"
/*
const isS3 = (c: DtoCredentialOut) =>
c.provider === "aws" &&
c.scope_kind === "service" &&
@@ -35,6 +34,7 @@ const isS3 = (c: DtoCredentialOut) =>
return false
}
})()
*/
const schema = z.object({
name: z.string().min(1, "Required"),
@@ -54,13 +54,14 @@ export const OrgSettings = () => {
queryFn: () => withRefresh(() => api.getOrg({ id: orgId! })),
})
/*
const credentialQ = useQuery({
queryKey: ["credentials", "s3"],
queryFn: () => credentialsApi.listCredentials(), // client-side filter
})
const s3Credentials = useMemo(() => (credentialQ.data ?? []).filter(isS3), [credentialQ.data])
*/
const form = useForm<Values>({
resolver: zodResolver(schema),
defaultValues: {
@@ -76,7 +77,7 @@ export const OrgSettings = () => {
domain: q.data.domain ?? "",
})
}
}, [q.data])
}, [q.data, form])
const updateMut = useMutation({
mutationFn: (v: Partial<Values>) => api.updateOrg({ id: orgId!, body: v }),

View File

@@ -5,7 +5,7 @@ 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 { useForm, useWatch } from "react-hook-form"
import { toast } from "sonner"
import { z } from "zod"
@@ -136,8 +136,17 @@ export const ServerPage = () => {
mode: "onChange",
})
const roleIsBastion = createForm.watch("role") === "bastion"
const pubCreate = createForm.watch("public_ip_address")?.trim() ?? ""
const watchedRoleCreate = useWatch({
control: createForm.control,
name: "role",
})
const roleIsBastion = watchedRoleCreate === "bastion"
const watchedPublicIpCreate = useWatch({
control: createForm.control,
name: "public_ip_address",
})
const pubCreate = watchedPublicIpCreate?.trim() ?? ""
const needPubCreate = roleIsBastion && pubCreate === ""
const createMut = useMutation({
@@ -160,8 +169,19 @@ export const ServerPage = () => {
mode: "onChange",
})
const roleIsBastionU = updateForm.watch("role") === "bastion"
const pubUpdate = updateForm.watch("public_ip_address")?.trim() ?? ""
const watchedRoleUpdate = useWatch({
control: updateForm.control,
name: "role",
})
const watchedPublicIpAddressUpdate = useWatch({
control: updateForm.control,
name: "public_ip_address",
})
const roleIsBastionU = watchedRoleUpdate === "bastion"
const pubUpdate = watchedPublicIpAddressUpdate?.trim() ?? ""
const needPubUpdate = roleIsBastionU && pubUpdate === ""
const updateMut = useMutation({

View File

@@ -4,11 +4,10 @@ import type { DtoCreateSSHRequest, DtoSshRevealResponse } from "@/sdk"
import { zodResolver } from "@hookform/resolvers/zod"
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
import { Download, Eye, Loader2, Plus, Trash2 } from "lucide-react"
import { useForm } from "react-hook-form"
import { useForm, useWatch } from "react-hook-form"
import { toast } from "sonner"
import { z } from "zod"
import { truncateMiddle } from "@/lib/utils.ts"
import { Badge } from "@/components/ui/badge.tsx"
import { Button } from "@/components/ui/button.tsx"
import {
@@ -105,6 +104,11 @@ export const SshPage = () => {
},
})
const watchedType = useWatch({
control: form.control,
name: "type",
})
const createMutation = useMutation({
mutationFn: async (values: CreateKeyInput) => {
const payload: DtoCreateSSHRequest = {
@@ -257,7 +261,7 @@ export const SshPage = () => {
<FormControl>
<Select
value={field.value}
disabled={form.watch("type") === "ed25519"}
disabled={watchedType === "ed25519"}
onValueChange={field.onChange}
>
<SelectTrigger className="w-[180px]">
@@ -316,7 +320,6 @@ export const SshPage = () => {
<TableBody>
{filtered.map((k) => {
const keyType = getKeyType(k.public_key!)
const truncated = truncateMiddle(k.public_key!, 18)
return (
<TableRow key={k.id}>
<TableCell className="font-medium">{k.name || "—"}</TableCell>