GET /cloud/members asks the portal who it admits to this instance and
marks those with a local user. DELETE /cloud/members/{portal_sub} takes a
membership back. Admin → Users lists the members who have not arrived
yet, with Revoke, which also surfaces a membership left standing when a
local user was deleted while the portal could not be told.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WgBfkm4rCmtd1T4vHWjEPx
89 lines
2.5 KiB
TypeScript
89 lines
2.5 KiB
TypeScript
import { useSuspenseQuery } from "@tanstack/react-query"
|
|
import { createFileRoute, redirect } from "@tanstack/react-router"
|
|
import { Suspense } from "react"
|
|
|
|
import { type UserPublic, UsersService } from "@/client"
|
|
import AddUser from "@/components/Admin/AddUser"
|
|
import { columns, type UserTableData } from "@/components/Admin/columns"
|
|
import OAuthClients from "@/components/Admin/OAuthClients"
|
|
import PortalMembers from "@/components/Admin/PortalMembers"
|
|
import { DataTable } from "@/components/Common/DataTable"
|
|
import PendingUsers from "@/components/Pending/PendingUsers"
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
|
import useAuth from "@/hooks/useAuth"
|
|
|
|
function getUsersQueryOptions() {
|
|
return {
|
|
queryFn: () => UsersService.readUsers({ skip: 0, limit: 100 }),
|
|
queryKey: ["users"],
|
|
}
|
|
}
|
|
|
|
export const Route = createFileRoute("/_layout/admin")({
|
|
component: Admin,
|
|
beforeLoad: async () => {
|
|
const user = await UsersService.readUserMe()
|
|
if (!user.is_superuser) {
|
|
throw redirect({
|
|
to: "/",
|
|
})
|
|
}
|
|
},
|
|
head: () => ({
|
|
meta: [
|
|
{
|
|
title: "Admin - Fluksio",
|
|
},
|
|
],
|
|
}),
|
|
})
|
|
|
|
function UsersTableContent() {
|
|
const { user: currentUser } = useAuth()
|
|
const { data: users } = useSuspenseQuery(getUsersQueryOptions())
|
|
|
|
const tableData: UserTableData[] = users.data.map((user: UserPublic) => ({
|
|
...user,
|
|
isCurrentUser: currentUser?.id === user.id,
|
|
}))
|
|
|
|
return <DataTable columns={columns} data={tableData} />
|
|
}
|
|
|
|
function UsersTable() {
|
|
return (
|
|
<Suspense fallback={<PendingUsers />}>
|
|
<UsersTableContent />
|
|
</Suspense>
|
|
)
|
|
}
|
|
|
|
function Admin() {
|
|
// Agents get in through the same door as people, so they are administered
|
|
// next to them rather than in a screen of their own.
|
|
return (
|
|
<Tabs defaultValue="users" className="flex flex-col gap-6">
|
|
<TabsList>
|
|
<TabsTrigger value="users">Users</TabsTrigger>
|
|
<TabsTrigger value="agents">Agents</TabsTrigger>
|
|
</TabsList>
|
|
<TabsContent value="users" className="flex flex-col gap-6">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h1 className="text-2xl font-bold tracking-tight">Users</h1>
|
|
<p className="text-muted-foreground">
|
|
Manage user accounts and permissions
|
|
</p>
|
|
</div>
|
|
<AddUser />
|
|
</div>
|
|
<UsersTable />
|
|
<PortalMembers />
|
|
</TabsContent>
|
|
<TabsContent value="agents">
|
|
<OAuthClients />
|
|
</TabsContent>
|
|
</Tabs>
|
|
)
|
|
}
|