mirror of
https://codeberg.org/MarkusThielker/next-ory.git
synced 2025-04-10 11:58:41 +00:00
353 lines
16 KiB
TypeScript
353 lines
16 KiB
TypeScript
'use client';
|
|
|
|
import { ColumnDef } from '@tanstack/react-table';
|
|
import { Identity } from '@ory/client';
|
|
import { DataTable } from '@/components/ui/data-table';
|
|
import { CircleCheck, CircleX, Copy, MoreHorizontal, Trash, UserCheck, UserMinus, UserPen, UserX } from 'lucide-react';
|
|
import React, { useEffect, useRef, useState } from 'react';
|
|
import { FetchIdentityPageProps } from '@/app/(inside)/user/page';
|
|
import { Spinner } from '@/components/ui/spinner';
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuLabel,
|
|
DropdownMenuSeparator,
|
|
DropdownMenuTrigger,
|
|
} from '@/components/ui/dropdown-menu';
|
|
import { Button, buttonVariants } from '@/components/ui/button';
|
|
import Link from 'next/link';
|
|
import { toast } from 'sonner';
|
|
import {
|
|
AlertDialog,
|
|
AlertDialogAction,
|
|
AlertDialogCancel,
|
|
AlertDialogContent,
|
|
AlertDialogDescription,
|
|
AlertDialogFooter,
|
|
AlertDialogHeader,
|
|
AlertDialogTitle,
|
|
} from '@/components/ui/alert-dialog';
|
|
import { blockIdentity, deleteIdentity, deleteIdentitySessions, unblockIdentity } from '@/app/(inside)/user/action';
|
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
|
|
|
interface IdentityDataTableProps {
|
|
data: Identity[];
|
|
pageSize: number;
|
|
pageToken: string | undefined;
|
|
query: string;
|
|
fetchIdentityPage: (props: FetchIdentityPageProps) => Promise<{ data: Identity[], tokens: Map<string, string> }>;
|
|
}
|
|
|
|
export function IdentityDataTable({ data, pageSize, pageToken, query, fetchIdentityPage }: IdentityDataTableProps) {
|
|
|
|
const columns: ColumnDef<Identity>[] = [
|
|
{
|
|
id: 'id',
|
|
accessorKey: 'id',
|
|
header: 'ID',
|
|
},
|
|
{
|
|
id: 'active',
|
|
header: 'Active',
|
|
cell: ({ row }) => {
|
|
|
|
const identity = row.original;
|
|
|
|
if (identity.state === 'active') {
|
|
return <CircleCheck/>;
|
|
} else {
|
|
return <CircleX/>;
|
|
}
|
|
},
|
|
},
|
|
{
|
|
id: 'name',
|
|
accessorKey: 'traits.name',
|
|
header: 'Name',
|
|
},
|
|
{
|
|
id: 'email',
|
|
header: 'Email',
|
|
cell: ({ row }) => {
|
|
|
|
const identity = row.original;
|
|
const email = identity.verifiable_addresses ?
|
|
identity.verifiable_addresses[0] : undefined;
|
|
|
|
if (!email) {
|
|
return <p>Something went wrong</p>;
|
|
} else {
|
|
return (
|
|
<div className="flex flex-row space-x-2 items-center">
|
|
<span>{email.value}</span>
|
|
{
|
|
email.verified ?
|
|
<Tooltip>
|
|
<TooltipTrigger>
|
|
<CircleCheck/>
|
|
</TooltipTrigger>
|
|
<TooltipContent>
|
|
<span>This email was confirmed at </span>
|
|
{new Date(email.verified_at!!).toLocaleString()}
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
:
|
|
<Tooltip>
|
|
<TooltipTrigger>
|
|
<CircleX/>
|
|
</TooltipTrigger>
|
|
<TooltipContent>
|
|
This email is not confirmed yet
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
}
|
|
</div>
|
|
);
|
|
}
|
|
},
|
|
},
|
|
{
|
|
id: 'actions',
|
|
cell: ({ row }) => {
|
|
const identity = row.original;
|
|
return (
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button variant="ghost" className="h-8 w-8 p-0">
|
|
<span className="sr-only">Open menu</span>
|
|
<MoreHorizontal className="h-4 w-4"/>
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end">
|
|
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
|
<DropdownMenuItem
|
|
className="flex items-center space-x-2"
|
|
onClick={() => {
|
|
navigator.clipboard.writeText(identity.id);
|
|
toast.success('Copied to clipboard');
|
|
}}
|
|
>
|
|
<Copy className="h-4 w-4"/>
|
|
<span>Copy identity ID</span>
|
|
</DropdownMenuItem>
|
|
<DropdownMenuSeparator/>
|
|
<Link href={`/user/${identity.id}`}>
|
|
<DropdownMenuItem className="flex items-center space-x-2">
|
|
<UserPen className="h-4 w-4"/>
|
|
<span>View identity</span>
|
|
</DropdownMenuItem>
|
|
</Link>
|
|
<DropdownMenuSeparator/>
|
|
<DropdownMenuItem
|
|
onClick={() => {
|
|
setCurrentIdentity(identity);
|
|
setIdentitySessionVisible(true);
|
|
}}
|
|
className="flex items-center space-x-2 text-red-500">
|
|
<UserMinus className="h-4 w-4"/>
|
|
<span>Delete sessions</span>
|
|
</DropdownMenuItem>
|
|
{
|
|
identity.state === 'active' &&
|
|
<DropdownMenuItem
|
|
onClick={() => {
|
|
setCurrentIdentity(identity);
|
|
setBlockIdentityVisible(true);
|
|
}}
|
|
className="flex items-center space-x-2 text-red-500">
|
|
<UserX className="h-4 w-4"/>
|
|
<span>Block identity</span>
|
|
</DropdownMenuItem>
|
|
}
|
|
{
|
|
identity.state === 'inactive' &&
|
|
<DropdownMenuItem
|
|
onClick={() => {
|
|
setCurrentIdentity(identity);
|
|
setUnblockIdentityVisible(true);
|
|
}}
|
|
className="flex items-center space-x-2 text-red-500">
|
|
<UserCheck className="h-4 w-4"/>
|
|
<span>Unblock identity</span>
|
|
</DropdownMenuItem>
|
|
}
|
|
<DropdownMenuItem
|
|
onClick={() => {
|
|
setCurrentIdentity(identity);
|
|
setDeleteIdentityVisible(true);
|
|
}}
|
|
className="flex items-center space-x-2 text-red-500">
|
|
<Trash className="h-4 w-4"/>
|
|
<span>Delete identity</span>
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
);
|
|
},
|
|
},
|
|
];
|
|
|
|
const [items, setItems] = useState<Identity[]>(data);
|
|
const [nextToken, setNextToken] = useState<string | undefined>(pageToken);
|
|
|
|
// react on changes from ssr (query params)
|
|
useEffect(() => {
|
|
setItems(data);
|
|
setNextToken(pageToken);
|
|
}, [data, pageSize, pageToken, query]);
|
|
|
|
// infinite scroll handling
|
|
const infiniteScrollSensor = useRef(null);
|
|
useEffect(() => {
|
|
const observer = new IntersectionObserver(
|
|
(entries) => {
|
|
if (entries[0].isIntersecting) {
|
|
fetchMore();
|
|
}
|
|
},
|
|
{ threshold: 0.5 }, // Adjust threshold as needed
|
|
);
|
|
|
|
if (infiniteScrollSensor.current) {
|
|
observer.observe(infiniteScrollSensor.current);
|
|
}
|
|
|
|
return () => {
|
|
if (infiniteScrollSensor.current) {
|
|
observer.unobserve(infiniteScrollSensor.current);
|
|
}
|
|
};
|
|
}, [items]);
|
|
|
|
const fetchMore = async () => {
|
|
if (!nextToken) return;
|
|
|
|
const response = await fetchIdentityPage({
|
|
pageSize: pageSize,
|
|
pageToken: nextToken,
|
|
query: query,
|
|
});
|
|
|
|
setItems([...items, ...response.data]);
|
|
setNextToken(response.tokens.get('next') ?? undefined);
|
|
};
|
|
|
|
// quick actions
|
|
const [currentIdentity, setCurrentIdentity] = useState<Identity | undefined>(undefined);
|
|
const [identitySessionVisible, setIdentitySessionVisible] = useState(false);
|
|
const [blockIdentityVisible, setBlockIdentityVisible] = useState(false);
|
|
const [unblockIdentityVisible, setUnblockIdentityVisible] = useState(false);
|
|
const [deleteIdentityVisible, setDeleteIdentityVisible] = useState(false);
|
|
|
|
return (
|
|
<>
|
|
<DataTable columns={columns} data={items}/>
|
|
{
|
|
currentIdentity && (
|
|
<>
|
|
{/* delete sessions dialog */}
|
|
<AlertDialog
|
|
open={identitySessionVisible}
|
|
onOpenChange={(open) => setIdentitySessionVisible(open)}>
|
|
<AlertDialogContent>
|
|
<AlertDialogHeader>
|
|
<AlertDialogTitle>Delete sessions</AlertDialogTitle>
|
|
<AlertDialogDescription className="grid grid-cols-1 gap-3">
|
|
<span>Are you sure you want to delete this identity's sessions?</span>
|
|
<span>{JSON.stringify(currentIdentity.traits, null, 4)}</span>
|
|
</AlertDialogDescription>
|
|
</AlertDialogHeader>
|
|
<AlertDialogFooter>
|
|
<AlertDialogCancel>
|
|
Cancel
|
|
</AlertDialogCancel>
|
|
<AlertDialogAction
|
|
className={buttonVariants({ variant: 'destructive' })}
|
|
onClick={() => deleteIdentitySessions(currentIdentity.id)}>
|
|
Invalidate sessions
|
|
</AlertDialogAction>
|
|
</AlertDialogFooter>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
|
|
{/* block identity dialog */}
|
|
<AlertDialog open={blockIdentityVisible} onOpenChange={(open) => setBlockIdentityVisible(open)}>
|
|
<AlertDialogContent>
|
|
<AlertDialogHeader>
|
|
<AlertDialogTitle>Block identity</AlertDialogTitle>
|
|
<AlertDialogDescription className="grid grid-cols-1 gap-3">
|
|
<span>Are you sure you want to block this identity?</span>
|
|
<span>{JSON.stringify(currentIdentity.traits, null, 4)}</span>
|
|
</AlertDialogDescription>
|
|
</AlertDialogHeader>
|
|
<AlertDialogFooter>
|
|
<AlertDialogCancel>
|
|
Cancel
|
|
</AlertDialogCancel>
|
|
<AlertDialogAction
|
|
onClick={() => blockIdentity(currentIdentity.id)}>
|
|
Block identity
|
|
</AlertDialogAction>
|
|
</AlertDialogFooter>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
|
|
{/* unblock identity dialog */}
|
|
<AlertDialog open={unblockIdentityVisible} onOpenChange={(open) => setUnblockIdentityVisible(open)}>
|
|
<AlertDialogContent>
|
|
<AlertDialogHeader>
|
|
<AlertDialogTitle>Unblock identity</AlertDialogTitle>
|
|
<AlertDialogDescription className="grid grid-cols-1 gap-3">
|
|
<span>Are you sure you want to unblock this identity?</span>
|
|
<span>{JSON.stringify(currentIdentity.traits, null, 4)}</span>
|
|
</AlertDialogDescription>
|
|
</AlertDialogHeader>
|
|
<AlertDialogFooter>
|
|
<AlertDialogCancel>
|
|
Cancel
|
|
</AlertDialogCancel>
|
|
<AlertDialogAction
|
|
onClick={() => unblockIdentity(currentIdentity.id)}>
|
|
Unblock identity
|
|
</AlertDialogAction>
|
|
</AlertDialogFooter>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
|
|
{/* delete identity dialog */}
|
|
<AlertDialog open={deleteIdentityVisible} onOpenChange={(open) => setDeleteIdentityVisible(open)}>
|
|
<AlertDialogContent>
|
|
<AlertDialogHeader>
|
|
<AlertDialogTitle>Delete identity</AlertDialogTitle>
|
|
<AlertDialogDescription className="grid grid-cols-1 gap-3">
|
|
<span>Are you sure you want to delete this identity?</span>
|
|
<strong className="text-yellow-500">This action can not be undone!</strong>
|
|
<span>{JSON.stringify(currentIdentity.traits, null, 4)}</span>
|
|
</AlertDialogDescription>
|
|
</AlertDialogHeader>
|
|
<AlertDialogFooter>
|
|
<AlertDialogCancel>
|
|
Cancel
|
|
</AlertDialogCancel>
|
|
<AlertDialogAction
|
|
className={buttonVariants({ variant: 'destructive' })}
|
|
onClick={() => deleteIdentity(currentIdentity.id)}>
|
|
Delete identity
|
|
</AlertDialogAction>
|
|
</AlertDialogFooter>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
</>
|
|
)
|
|
}
|
|
{
|
|
nextToken && (
|
|
<div className="flex w-full justify-center">
|
|
<Spinner ref={infiniteScrollSensor} className="h-10"/>
|
|
</div>
|
|
)
|
|
}
|
|
</>
|
|
);
|
|
}
|