feat: sdk migration in progress

This commit is contained in:
allanice001
2025-11-02 13:19:30 +00:00
commit 0d10d42442
492 changed files with 71067 additions and 0 deletions

View File

@@ -0,0 +1,216 @@
import { useState } from "react"
import { withRefresh } from "@/api/with-refresh.ts"
import { orgStore } from "@/auth/org.ts"
import { makeOrgsApi } from "@/sdkClient.ts"
import { zodResolver } from "@hookform/resolvers/zod"
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
import { useForm } from "react-hook-form"
import { toast } from "sonner"
import { z } from "zod"
import { Button } from "@/components/ui/button.tsx"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card.tsx"
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} 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 {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table.tsx"
const createSchema = z.object({
name: z.string(),
expires_in_hours: z.number().min(1).max(43800),
})
type CreateValues = z.infer<typeof createSchema>
export const OrgApiKeys = () => {
const api = makeOrgsApi()
const qc = useQueryClient()
const orgId = orgStore.get()
const q = useQuery({
enabled: !!orgId,
queryKey: ["org:keys", orgId],
queryFn: () => withRefresh(() => api.listOrgKeys({ id: orgId! })),
})
const form = useForm<CreateValues>({
resolver: zodResolver(createSchema),
defaultValues: {
name: "",
expires_in_hours: 720,
},
})
const [showSecret, setShowSecret] = useState<{
key?: string
secret?: string
} | null>(null)
const createMut = useMutation({
mutationFn: (v: CreateValues) => api.createOrgKey({ id: orgId!, body: v }),
onSuccess: (resp) => {
void qc.invalidateQueries({ queryKey: ["org:keys", orgId] })
setShowSecret({ key: resp.org_key, secret: resp.org_secret })
toast.success("Key created")
form.reset({ name: "", expires_in_hours: undefined })
},
onError: (e: any) => toast.error(e?.message ?? "Failed to create key"),
})
const deleteMut = useMutation({
mutationFn: (keyId: string) => api.deleteOrgKey({ id: orgId!, keyId }),
onSuccess: () => {
void qc.invalidateQueries({ queryKey: ["org:keys", orgId] })
toast.success("Key deleted")
},
onError: (e: any) => toast.error(e?.message ?? "Failed to delete key"),
})
if (!orgId) return <p className="text-muted-foreground">Pick an organization.</p>
if (q.isLoading) return <p>Loading...</p>
if (q.error) return <p className="text-destructive">Failed to load keys.</p>
return (
<Card>
<CardHeader>
<CardTitle>Org API Keys</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
<Form {...form}>
<form
onSubmit={form.handleSubmit((v) => createMut.mutate(v))}
className="grid grid-cols-1 items-end gap-3 md:grid-cols-12"
>
<div className="md:col-span-6">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input placeholder="automation-bot" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="md:col-span-4">
<FormField
control={form.control}
name="expires_in_hours"
render={({ field }) => (
<FormItem>
<FormLabel>Expires In (hours)</FormLabel>
<FormControl>
<Input placeholder="e.g. 720" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="md:col-span-2">
<Button type="submit" className="w-full" disabled={createMut.isPending}>
Create
</Button>
</div>
</form>
</Form>
<div className="overflow-x-auto rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Scope</TableHead>
<TableHead>Created</TableHead>
<TableHead>Expires</TableHead>
<TableHead className="w-28" />
</TableRow>
</TableHeader>
<TableBody>
{q.data?.map((k) => (
<TableRow key={k.id}>
<TableCell>{k.name ?? "-"}</TableCell>
<TableCell>{k.scope}</TableCell>
<TableCell>{new Date(k.created_at!).toLocaleString()}</TableCell>
<TableCell>
{k.expires_at ? new Date(k.expires_at).toLocaleString() : "-"}
</TableCell>
<TableCell className="text-right">
<Button variant="destructive" size="sm" onClick={() => deleteMut.mutate(k.id!)}>
Delete
</Button>
</TableCell>
</TableRow>
))}
{q.data?.length === 0 && (
<TableRow>
<TableCell colSpan={5} className="text-muted-foreground p-4">
No keys.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
{/* Show once dialog with key/secret */}
<Dialog open={!!showSecret} onOpenChange={(o) => !o && setShowSecret(null)}>
<DialogContent>
<DialogHeader>
<DialogTitle>Copy your credentials</DialogTitle>
</DialogHeader>
<div className="space-y-2">
<div>
<div className="text-muted-foreground mb-1 text-xs">Org Key</div>
<Input
readOnly
value={showSecret?.key ?? ""}
onFocus={(e) => e.currentTarget.select()}
/>
</div>
<div>
<div className="text-muted-foreground mb-1 text-xs">Org Secret</div>
<Input
readOnly
value={showSecret?.secret ?? ""}
onFocus={(e) => e.currentTarget.select()}
/>
</div>
<p className="text-muted-foreground text-xs">
This secret is shown once. Store it securely.
</p>
</div>
<DialogFooter>
<Button onClick={() => setShowSecret(null)}>Done</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</CardContent>
</Card>
)
}

View File

@@ -0,0 +1,262 @@
import { useMemo, useState } from "react"
import { withRefresh } from "@/api/with-refresh.ts"
import { orgStore } from "@/auth/org.ts"
import { makeOrgsApi } from "@/sdkClient.ts"
import { zodResolver } from "@hookform/resolvers/zod"
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
import { Loader2 } from "lucide-react"
import { useForm } from "react-hook-form"
import { toast } from "sonner"
import { z } from "zod"
import { Button } from "@/components/ui/button.tsx"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card.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"
const addSchema = z.object({
user_id: z.uuid("Invalid UUID"),
role: z.enum(["owner", "admin", "member"]),
})
type AddValues = z.infer<typeof addSchema>
export const OrgMembers = () => {
const api = makeOrgsApi()
const qc = useQueryClient()
const orgId = orgStore.get()
const [updatingId, setUpdatingId] = useState<string | null>(null)
const q = useQuery({
enabled: !!orgId,
queryKey: ["org:members", orgId],
queryFn: () => withRefresh(() => api.listMembers({ id: orgId! })),
})
const ownersCount = useMemo(
() => (q.data ?? []).filter((m) => m.role === "owner").length,
[q.data]
)
const form = useForm<AddValues>({
resolver: zodResolver(addSchema),
defaultValues: {
user_id: "",
role: "member",
},
})
const addMut = useMutation({
mutationFn: (v: AddValues) => api.addOrUpdateMember({ id: orgId!, body: v }),
onSuccess: () => {
toast.success("Member added/updated")
void qc.invalidateQueries({ queryKey: ["org:members", orgId] })
form.reset({ user_id: "", role: "member" })
},
onError: (e: any) => toast.error(e?.message ?? "Failed"),
})
const removeMut = useMutation({
mutationFn: (userId: string) => api.removeMember({ id: orgId!, userId }),
onSuccess: () => {
toast.success("Member removed")
void qc.invalidateQueries({ queryKey: ["org:members", orgId] })
},
onError: (e: any) => toast.error(e?.message ?? "Failed"),
})
const roleMut = useMutation({
mutationFn: ({ userId, role }: { userId: string; role: "owner" | "admin" | "member" }) =>
api.addOrUpdateMember({ id: orgId!, body: { user_id: userId, role } }),
onMutate: async ({ userId, role }) => {
setUpdatingId(userId)
// cancel queries and snapshot previous
await qc.cancelQueries({ queryKey: ["org:members", orgId] })
const prev = qc.getQueryData<any>(["org:members", orgId])
// optimistic update
qc.setQueryData(["org:members", orgId], (old: any[] = []) =>
old.map((m) => (m.user_id === userId ? { ...m, role } : m))
)
return { prev }
},
onError: (e, _vars, ctx) => {
if (ctx?.prev) qc.setQueryData(["org:members", orgId], ctx.prev)
toast.error((e as any)?.message ?? "Failed to update role")
},
onSuccess: () => {
toast.success("Role updated")
},
onSettled: () => {
setUpdatingId(null)
void qc.invalidateQueries({ queryKey: ["org:members", orgId] })
},
})
const canDowngrade = (m: any) => !(m.role === "owner" && ownersCount <= 1)
if (!orgId) return <p className="text-muted-foreground">Pick an organization.</p>
if (q.isLoading) return <p>Loading...</p>
if (q.error) return <p className="text-destructive">Failed to load members.</p>
return (
<Card>
<CardHeader>
<CardTitle>Members</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
{/* Add/Update */}
<Form {...form}>
<form
className="grid grid-cols-1 items-end gap-3 md:grid-cols-12"
onSubmit={form.handleSubmit((v) => addMut.mutate(v))}
>
<div className="md:col-span-6">
<FormField
control={form.control}
name="user_id"
render={({ field }) => (
<FormItem>
<FormLabel>User ID</FormLabel>
<FormControl>
<Input placeholder="UUID" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="md:col-span-4">
<FormField
control={form.control}
name="role"
render={({ field }) => (
<FormItem>
<FormLabel>Role</FormLabel>
<Select onValueChange={field.onChange} value={field.value}>
<SelectTrigger>
<SelectValue placeholder="Select role" />
</SelectTrigger>
<SelectContent>
<SelectItem value="member">member</SelectItem>
<SelectItem value="admin">admin</SelectItem>
<SelectItem value="owner">owner</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="md:col-span-2">
<Button type="submit" className="w-full" disabled={addMut.isPending}>
Save
</Button>
</div>
</form>
</Form>
<div className="overflow-x-auto rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead>Id</TableHead>
<TableHead>User</TableHead>
<TableHead>Role</TableHead>
<TableHead className="w-28"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{q.data?.map((m) => {
const isRowPending = updatingId === m.user_id
return (
<TableRow key={m.user_id} className="align-middle">
<TableCell className="font-mono text-xs">{m.user_id}</TableCell>
<TableCell>{m.email}</TableCell>
{/* Inline role select */}
<TableCell className="capitalize">
<div className="flex items-center gap-2">
<Select
value={m.role}
onValueChange={(next) => {
if (m.role === next) return
if (m.role === "owner" && next !== "owner" && !canDowngrade(m)) {
toast.error("You cannot demote the last owner.")
return
}
roleMut.mutate({
userId: m.user_id!,
role: next as any,
})
}}
disabled={isRowPending}
>
<SelectTrigger className="h-8 w-[140px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="member">member</SelectItem>
<SelectItem value="admin">admin</SelectItem>
<SelectItem value="owner">owner</SelectItem>
</SelectContent>
</Select>
{isRowPending && <Loader2 className="h-4 w-4 animate-spin" />}
</div>
</TableCell>
<TableCell className="text-right">
<Button
variant="destructive"
size="sm"
onClick={() => removeMut.mutate(m.user_id!)}
disabled={m.role === "owner" && ownersCount <= 1}
title={
m.role === "owner" && ownersCount <= 1
? "Cannot remove the last owner"
: ""
}
>
Remove
</Button>
</TableCell>
</TableRow>
)
})}
{q.data?.length === 0 && (
<TableRow>
<TableCell colSpan={3} className="text-muted-foreground p-4">
No members.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
</CardContent>
</Card>
)
}

View File

@@ -0,0 +1,147 @@
import { useEffect } from "react"
import { withRefresh } from "@/api/with-refresh.ts"
import { orgStore } from "@/auth/org.ts"
import { makeOrgsApi } from "@/sdkClient.ts"
import { zodResolver } from "@hookform/resolvers/zod"
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
import { useForm } from "react-hook-form"
import { toast } from "sonner"
import { z } from "zod"
import { Button } from "@/components/ui/button.tsx"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card.tsx"
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form.tsx"
import { Input } from "@/components/ui/input.tsx"
const schema = z.object({
name: z.string().min(1, "Required"),
domain: z.string().optional(),
})
type Values = z.infer<typeof schema>
export const OrgSettings = () => {
const api = makeOrgsApi()
const qc = useQueryClient()
const orgId = orgStore.get()
const q = useQuery({
enabled: !!orgId,
queryKey: ["org", orgId],
queryFn: () => withRefresh(() => api.getOrg({ id: orgId! })),
})
const form = useForm<Values>({
resolver: zodResolver(schema),
defaultValues: {
name: "",
domain: "",
},
})
useEffect(() => {
if (q.data) {
form.reset({
name: q.data.name ?? "",
domain: q.data.domain ?? "",
})
}
}, [q.data])
const updateMut = useMutation({
mutationFn: (v: Partial<Values>) => api.updateOrg({ id: orgId!, body: v }),
onSuccess: () => {
void qc.invalidateQueries({ queryKey: ["org", orgId] })
toast.success("Organization updated")
},
onError: (e: any) => toast.error(e?.message ?? "Update failed"),
})
const deleteMut = useMutation({
mutationFn: () => api.deleteOrg({ id: orgId! }),
onSuccess: () => {
toast.success("Organization deleted")
orgStore.set("")
void qc.invalidateQueries({ queryKey: ["orgs:mine"] })
},
onError: (e: any) => toast.error(e?.message ?? "Delete failed"),
})
if (!orgId) {
return <p className="text-muted-foreground">Pick an organization.</p>
}
if (q.isLoading) return <p>Loading...</p>
if (q.error) return <p className="text-destructive">Failed to load.</p>
const onSubmit = (v: Values) => {
const delta: Partial<Values> = {}
if (v.name !== q.data?.name) delta.name = v.name
const normDomain = v.domain?.trim() || undefined
if ((normDomain ?? null) !== (q.data?.domain ?? null)) delta.domain = normDomain
if (Object.keys(delta).length === 0) return
updateMut.mutate(delta)
}
return (
<Card>
<CardHeader>
<CardTitle>Organization Settings</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
<Form {...form}>
<form className="space-y-4" onSubmit={form.handleSubmit(onSubmit)}>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="domain"
render={({ field }) => (
<FormItem>
<FormLabel>Domain (optional)</FormLabel>
<FormControl>
<Input placeholder="acme.com" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="flex gap-2">
<Button type="submit" disabled={updateMut.isPending}>
Save
</Button>
<Button
type="button"
variant="destructive"
onClick={() => deleteMut.mutate()}
disabled={deleteMut.isPending}
>
Delete Org
</Button>
</div>
</form>
</Form>
</CardContent>
</Card>
)
}