feat: cluster page ui

Signed-off-by: allanice001 <allanice001@gmail.com>
This commit is contained in:
allanice001
2025-11-17 18:21:48 +00:00
parent d163a050d8
commit 56f86a11b4
24 changed files with 1864 additions and 68 deletions

119
ui/src/pages/login.tsx Normal file
View File

@@ -0,0 +1,119 @@
import { useEffect, useMemo } from "react"
import { authStore, type TokenPair } from "@/auth/store.ts"
import { API_BASE } from "@/sdkClient.ts"
import { useLocation, useNavigate } from "react-router-dom"
import { cn } from "@/lib/utils.ts"
import { Button } from "@/components/ui/button.tsx"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card.tsx"
function openPopup(url: string, name = "gsot-auth", w = 520, h = 640) {
const y = window.top!.outerHeight / 2 + window.top!.screenY - h / 2
const x = window.top!.outerWidth / 2 + window.top!.screenX - w / 2
return window.open(
url,
name,
`toolbar=no,location=no,status=no,menubar=no,scrollbars=yes,resizable=yes,width=${w},height=${h},top=${y},left=${x}`
)
}
async function startAuth(provider: "google" | "github") {
const params = new URLSearchParams({
mode: "spa",
origin: window.location.origin,
})
const res = await fetch(`${API_BASE}/auth/${provider}/start?` + params, {
method: "POST",
})
if (!res.ok) throw new Error("Failed to start auth")
const data = await res.json()
return data.auth_url as string
}
export const Login = () => {
const navigate = useNavigate()
const loc = useLocation()
const to = useMemo(() => {
const p = new URLSearchParams(loc.search).get("to") || "/me"
try {
// prevent absolute URLs & open redirects
const url = new URL(p, window.location.origin)
return url.origin === window.location.origin ? url.pathname + url.search : "/me"
} catch {
return "/me"
}
}, [loc.search])
useEffect(() => {
if (authStore.get()?.access_token) {
navigate(to, { replace: true })
}
}, [navigate, to])
useEffect(() => {
const onMsg = (ev: MessageEvent) => {
const okType = typeof ev.data === "object" && ev.data?.type === "autoglue:auth"
if (!okType) return
const tokens: TokenPair = ev.data.payload
authStore.set(tokens)
navigate(to, { replace: true })
}
window.addEventListener("message", onMsg)
return () => window.removeEventListener("message", onMsg)
}, [navigate, to])
const login = async (provider: "google" | "github") => {
const url = await startAuth(provider)
const win = openPopup(url)
if (!win) alert("Please allow popups to sign in.")
}
return (
<div className="mx-auto flex items-center justify-center p-4">
<Card className="w-full max-w-md">
<CardHeader>
<CardTitle className="text-lg md:text-xl">Sign In</CardTitle>
<CardDescription className="text-xs md:text-sm">
Continue with a provider below to access your account.
</CardDescription>
</CardHeader>
<CardContent>
<div className="grid gap-4">
<div className={cn("flex w-full items-center gap-2", "flex-col justify-between")}>
<Button variant="outline" className="w-full gap-2" onClick={() => login("google")}>
<svg
xmlns="http://www.w3.org/2000/svg"
width="0.98em"
height="1em"
viewBox="0 0 256 262"
aria-hidden="true"
focusable="false"
>
<path
fill="#4285F4"
d="M255.878 133.451c0-10.734-.871-18.567-2.756-26.69H130.55v48.448h71.947c-1.45 12.04-9.283 30.172-26.69 42.356l-.244 1.622l38.755 30.023l2.685.268c24.659-22.774 38.875-56.282 38.875-96.027"
></path>
<path
fill="#34A853"
d="M130.55 261.1c35.248 0 64.839-11.605 86.453-31.622l-41.196-31.913c-11.024 7.688-25.82 13.055-45.257 13.055c-34.523 0-63.824-22.773-74.269-54.25l-1.531.13l-40.298 31.187l-.527 1.465C35.393 231.798 79.49 261.1 130.55 261.1"
></path>
<path
fill="#FBBC05"
d="M56.281 156.37c-2.756-8.123-4.351-16.827-4.351-25.82c0-8.994 1.595-17.697 4.206-25.82l-.073-1.73L15.26 71.312l-1.335.635C5.077 89.644 0 109.517 0 130.55s5.077 40.905 13.925 58.602z"
></path>
<path
fill="#EB4335"
d="M130.55 50.479c24.514 0 41.05 10.589 50.479 19.438l36.844-35.974C195.245 12.91 165.798 0 130.55 0C79.49 0 35.393 29.301 13.925 71.947l42.211 32.783c10.59-31.477 39.891-54.251 74.414-54.251"
></path>
</svg>
Sign in with Google
</Button>
</div>
</div>
</CardContent>
</Card>
</div>
)
}