1
0
Fork 0
mirror of https://codeberg.org/MarkusThielker/next-ory.git synced 2025-04-10 11:58:41 +00:00
next-ory/dashboard/src/app/(inside)/user/data-table.tsx
2025-01-02 11:33:07 +01:00

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>
)
}
</>
);
}