mirror of
https://github.com/GlueOps/autoglue.git
synced 2026-02-13 21:00:06 +01:00
feat: sdk migration in progress
This commit is contained in:
216
ui/src/pages/org/api-keys.tsx
Normal file
216
ui/src/pages/org/api-keys.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
262
ui/src/pages/org/members.tsx
Normal file
262
ui/src/pages/org/members.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
147
ui/src/pages/org/settings.tsx
Normal file
147
ui/src/pages/org/settings.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user