initial commit

This commit is contained in:
rustmailer
2025-11-19 02:14:37 +08:00
commit 1a8f95117e
355 changed files with 54089 additions and 0 deletions
@@ -0,0 +1,257 @@
//
// Copyright (c) 2025 rustmailer.com (https://rustmailer.com)
//
// This file is part of the Bichon Email Archiving Project
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import { z } from 'zod'
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { toast } from '@/hooks/use-toast'
import { Button } from '@/components/ui/button'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form'
import { Input } from '@/components/ui/input'
import { Proxy } from '../data/schema'
import { AxiosError } from 'axios'
import { ToastAction } from '@/components/ui/toast'
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { Loader2 } from 'lucide-react'
import { add_proxy, update_proxy } from '@/api/system/api'
const proxyFormSchema = z.object({
url: z.string()
.min(1, "Proxy address cannot be empty")
.refine(
(value) => {
try {
const url = new URL(value);
return url.protocol === 'socks5:' || url.protocol === 'http:';
} catch {
return false;
}
},
{
message: "URL must start with http:// or socks5://",
}
)
.refine(
(value) => {
const url = new URL(value);
return /^[a-zA-Z0-9\-\.]+$/.test(url.hostname);
},
{
message: "Hostname contains invalid characters",
}
)
.refine(
(value) => {
const url = new URL(value);
const port = parseInt(url.port || '1080');
return port > 0 && port <= 65535;
},
{
message: "Port must be between 1-65535",
}
)
.refine(
(value) => {
const url = new URL(value);
if (url.username && !url.password) return false;
return true;
},
{
message: "Password cannot be empty when username is provided",
}
)
.refine(
(value) => {
const url = new URL(value);
if (url.password) return url.password.length >= 8;
return true;
},
{
message: "Password must be at least 8 characters",
}
)
});
export type ProxyForm = z.infer<typeof proxyFormSchema>;
interface Props {
currentRow?: Proxy
open: boolean
onOpenChange: (open: boolean) => void
}
const defaultValues = {
url: ""
};
const mapCurrentRowToFormValues = (currentRow: Proxy) => {
let data = {
url: currentRow.url
};
return data;
};
export function ProxyActionDialog({ currentRow, open, onOpenChange }: Props) {
const isEdit = !!currentRow
const queryClient = useQueryClient();
const form = useForm<ProxyForm>({
resolver: zodResolver(proxyFormSchema),
defaultValues: isEdit
? mapCurrentRowToFormValues(currentRow)
: defaultValues,
});
const createMutation = useMutation({
mutationFn: add_proxy,
onSuccess: handleSuccess,
onError: handleError
});
const updateMutation = useMutation({
mutationFn: (url: string) => update_proxy(currentRow?.id!, url),
onSuccess: handleSuccess,
onError: handleError
})
function handleSuccess() {
toast({
title: `Proxy ${isEdit ? 'Updated' : 'Added'}`,
description: `Your Proxy has been successfully ${isEdit ? 'updated' : 'added'}.`,
action: <ToastAction altText="Close">Close</ToastAction>,
});
queryClient.invalidateQueries({ queryKey: ['proxy-list'] });
form.reset();
onOpenChange(false);
}
function handleError(error: AxiosError) {
const errorMessage = (error.response?.data as { message?: string })?.message ||
error.message ||
`${isEdit ? 'Update' : 'Add'} failed, please try again later`;
toast({
variant: "destructive",
title: `Proxy ${isEdit ? 'Update' : 'Add'} Failed`,
description: errorMessage as string,
action: <ToastAction altText="Try again">Try again</ToastAction>,
});
console.error(error);
}
const onSubmit = (values: ProxyForm) => {
const url = values.url;
if (isEdit) {
updateMutation.mutate(url);
} else {
createMutation.mutate(url);
}
}
return (
<Dialog
open={open}
onOpenChange={(state) => {
form.reset()
onOpenChange(state)
}}
>
<DialogContent className='max-w-xl'>
<DialogHeader className='text-left mb-4'>
<DialogTitle>{isEdit ? 'Edit Proxy' : 'Add New Proxy'}</DialogTitle>
<DialogDescription>
{isEdit ? 'Update the Proxy here. ' : 'Add new Proxy here. '}
Click save when you&apos;re done.
</DialogDescription>
</DialogHeader>
<Form {...form}>
<form
id='proxy-form'
onSubmit={form.handleSubmit(onSubmit)}
className='space-y-4 p-0.5'
>
<FormField
control={form.control}
name="url"
render={({ field }) => (
<FormItem>
<FormLabel>Proxy URL</FormLabel>
<FormControl>
<Input
placeholder="socks5://127.0.0.1:22308"
{...field}
/>
</FormControl>
<FormMessage />
<FormDescription>
Please use an IP address (e.g., 127.0.0.1) rather than a hostname or domain for better reliability.
</FormDescription>
</FormItem>
)}
/>
</form>
</Form>
<DialogFooter>
<Button
type="submit"
form="proxy-form"
disabled={isEdit ? updateMutation.isPending : createMutation.isPending}
className="min-w-[120px] relative"
>
<span className="inline-flex items-center justify-center">
{(isEdit ? updateMutation.isPending : createMutation.isPending) && (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
)}
<span>
{isEdit
? updateMutation.isPending
? "Saving..."
: "Save changes"
: createMutation.isPending
? "Adding..."
: "Add"}
</span>
</span>
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}
@@ -0,0 +1,79 @@
//
// Copyright (c) 2025 rustmailer.com (https://rustmailer.com)
//
// This file is part of the Bichon Email Archiving Project
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import { ColumnDef } from '@tanstack/react-table'
import LongText from '@/components/long-text'
import { Proxy } from '../data/schema'
import { DataTableColumnHeader } from './data-table-column-header'
import { DataTableRowActions } from './data-table-row-actions'
import { format } from 'date-fns'
export const columns: ColumnDef<Proxy>[] = [
{
accessorKey: 'id',
header: ({ column }) => (
<DataTableColumnHeader column={column} title='Id' />
),
cell: ({ row }) => (
<LongText className='max-w-72'>{`${row.original.id}`}</LongText>
),
enableHiding: false,
meta: { className: 'w-60' },
enableSorting: false
},
{
accessorKey: "url",
header: ({ column }) => (
<DataTableColumnHeader column={column} title='Url' />
),
cell: ({ row }) => {
return <LongText>{row.original.url}</LongText>
},
meta: { className: 'w-60' },
},
{
accessorKey: 'created_at',
header: ({ column }) => (
<DataTableColumnHeader column={column} title='Created At' />
),
cell: ({ row }) => {
const created_at = row.original.created_at;
const date = format(new Date(created_at), 'yyyy-MM-dd HH:mm:ss');
return <LongText>{date}</LongText>;
},
enableHiding: false,
},
{
accessorKey: 'updated_at',
header: ({ column }) => (
<DataTableColumnHeader column={column} title='Updated At' />
),
cell: ({ row }) => {
const updated_at = row.original.updated_at;
const date = format(new Date(updated_at), 'yyyy-MM-dd HH:mm:ss');
return <LongText>{date}</LongText>;
},
enableHiding: false,
},
{
id: 'actions',
cell: DataTableRowActions,
},
]
@@ -0,0 +1,89 @@
//
// Copyright (c) 2025 rustmailer.com (https://rustmailer.com)
//
// This file is part of the Bichon Email Archiving Project
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import {
ArrowDownIcon,
ArrowUpIcon,
CaretSortIcon,
EyeNoneIcon,
} from '@radix-ui/react-icons'
import { Column } from '@tanstack/react-table'
import { cn } from '@/lib/utils'
import { Button } from '@/components/ui/button'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
interface DataTableColumnHeaderProps<TData, TValue>
extends React.HTMLAttributes<HTMLDivElement> {
column: Column<TData, TValue>
title: string
}
export function DataTableColumnHeader<TData, TValue>({
column,
title,
className,
}: DataTableColumnHeaderProps<TData, TValue>) {
if (!column.getCanSort()) {
return <div className={cn(className)}>{title}</div>
}
return (
<div className={cn('flex items-center space-x-2', className)}>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant='ghost'
size='sm'
className=' h-8 data-[state=open]:bg-accent'
>
<span>{title}</span>
{column.getIsSorted() === 'desc' ? (
<ArrowDownIcon className='ml-2 h-4 w-4' />
) : column.getIsSorted() === 'asc' ? (
<ArrowUpIcon className='ml-2 h-4 w-4' />
) : (
<CaretSortIcon className='ml-2 h-4 w-4' />
)}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align='start'>
<DropdownMenuItem onClick={() => column.toggleSorting(false)}>
<ArrowUpIcon className='mr-2 h-3.5 w-3.5 text-muted-foreground/70' />
Asc
</DropdownMenuItem>
<DropdownMenuItem onClick={() => column.toggleSorting(true)}>
<ArrowDownIcon className='mr-2 h-3.5 w-3.5 text-muted-foreground/70' />
Desc
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => column.toggleVisibility(false)}>
<EyeNoneIcon className='mr-2 h-3.5 w-3.5 text-muted-foreground/70' />
Hide
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
)
}
@@ -0,0 +1,122 @@
//
// Copyright (c) 2025 rustmailer.com (https://rustmailer.com)
//
// This file is part of the Bichon Email Archiving Project
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import {
ChevronLeftIcon,
ChevronRightIcon,
DoubleArrowLeftIcon,
DoubleArrowRightIcon,
} from '@radix-ui/react-icons'
import { Table } from '@tanstack/react-table'
import { Button } from '@/components/ui/button'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
interface DataTablePaginationProps<TData> {
table: Table<TData>
showSelected?: boolean,
showPageSizeSelector?: boolean
}
export function DataTablePagination<TData>({
table,
showSelected = true,
showPageSizeSelector = true
}: DataTablePaginationProps<TData>) {
return (
<div className='flex items-center justify-between overflow-auto px-2'>
{showSelected && <div className='hidden flex-1 text-sm text-muted-foreground sm:block'>
{table.getFilteredSelectedRowModel().rows.length} of{' '}
{table.getFilteredRowModel().rows.length} row(s) selected.
</div>}
{!showPageSizeSelector && <div className='hidden flex-1 text-sm text-muted-foreground sm:block'>
10 rows per page.
</div>}
<div className='flex items-center sm:space-x-6 lg:space-x-8 ml-auto'>
{showPageSizeSelector && <div className='flex items-center space-x-2'>
<p className='hidden text-sm font-medium sm:block'>Rows per page</p>
<Select
value={`${table.getState().pagination.pageSize}`}
onValueChange={(value) => {
table.setPageSize(Number(value))
}}
>
<SelectTrigger className='h-8 w-[70px]'>
<SelectValue placeholder={table.getState().pagination.pageSize} />
</SelectTrigger>
<SelectContent side='top'>
{[10, 20, 30, 40, 50].map((pageSize) => (
<SelectItem key={pageSize} value={`${pageSize}`}>
{pageSize}
</SelectItem>
))}
</SelectContent>
</Select>
</div>}
<div className='flex w-[100px] items-center justify-center text-sm font-medium'>
Page {table.getState().pagination.pageIndex + 1} of{' '}
{table.getPageCount()}
</div>
<div className='flex items-center space-x-2'>
<Button
variant='outline'
className='hidden h-8 w-8 p-0 lg:flex'
onClick={() => table.setPageIndex(0)}
disabled={!table.getCanPreviousPage()}
>
<span className='sr-only'>Go to first page</span>
<DoubleArrowLeftIcon className='h-4 w-4' />
</Button>
<Button
variant='outline'
className='h-8 w-8 p-0'
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
<span className='sr-only'>Go to previous page</span>
<ChevronLeftIcon className='h-4 w-4' />
</Button>
<Button
variant='outline'
className='h-8 w-8 p-0'
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
<span className='sr-only'>Go to next page</span>
<ChevronRightIcon className='h-4 w-4' />
</Button>
<Button
variant='outline'
className='hidden h-8 w-8 p-0 lg:flex'
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
disabled={!table.getCanNextPage()}
>
<span className='sr-only'>Go to last page</span>
<DoubleArrowRightIcon className='h-4 w-4' />
</Button>
</div>
</div>
</div>
)
}
@@ -0,0 +1,82 @@
//
// Copyright (c) 2025 rustmailer.com (https://rustmailer.com)
//
// This file is part of the Bichon Email Archiving Project
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import { DotsHorizontalIcon } from '@radix-ui/react-icons'
import { Row } from '@tanstack/react-table'
import { IconEdit, IconTrash } from '@tabler/icons-react'
import { Button } from '@/components/ui/button'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { useProxyContext } from '../context'
import { Proxy } from '../data/schema'
interface DataTableRowActionsProps {
row: Row<Proxy>
}
export function DataTableRowActions({ row }: DataTableRowActionsProps) {
const { setOpen, setCurrentRow } = useProxyContext()
return (
<>
<DropdownMenu modal={false}>
<DropdownMenuTrigger asChild>
<Button
variant='ghost'
className='flex h-8 w-8 p-0 data-[state=open]:bg-muted'
>
<DotsHorizontalIcon className='h-4 w-4' />
<span className='sr-only'>Open menu</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align='end' className='w-[160px]'>
<DropdownMenuItem
onClick={() => {
setCurrentRow(row.original)
setOpen('edit')
}}
>
Edit
<DropdownMenuShortcut>
<IconEdit size={16} />
</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={() => {
setCurrentRow(row.original)
setOpen('delete')
}}
className='!text-red-500'
>
Delete
<DropdownMenuShortcut>
<IconTrash size={16} />
</DropdownMenuShortcut>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</>
)
}
@@ -0,0 +1,44 @@
//
// Copyright (c) 2025 rustmailer.com (https://rustmailer.com)
//
// This file is part of the Bichon Email Archiving Project
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import { Table } from '@tanstack/react-table'
import { Input } from '@/components/ui/input'
interface DataTableToolbarProps<TData> {
table: Table<TData>
}
export function DataTableToolbar<TData>({
table,
}: DataTableToolbarProps<TData>) {
return (
<div className='flex items-center justify-between'>
<div className='flex flex-1 flex-col-reverse items-start gap-y-2 sm:flex-row sm:items-center sm:space-x-2'>
<Input
placeholder='Filter Proxy...'
value={(table.getState().globalFilter as string) ?? ''}
onChange={(event) => {
table.setGlobalFilter(event.target.value);
}}
className='h-8 w-80'
/>
</div>
</div>
)
}
@@ -0,0 +1,126 @@
//
// Copyright (c) 2025 rustmailer.com (https://rustmailer.com)
//
// This file is part of the Bichon Email Archiving Project
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import { useState } from 'react'
import { IconAlertTriangle } from '@tabler/icons-react'
import { toast } from '@/hooks/use-toast'
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { ConfirmDialog } from '@/components/confirm-dialog'
import { Proxy } from '../data/schema'
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { ToastAction } from '@/components/ui/toast'
import { AxiosError } from 'axios'
import { delete_proxy } from '@/api/system/api'
interface Props {
open: boolean
onOpenChange: (open: boolean) => void
currentRow: Proxy
}
export function ProxyDeleteDialog({ open, onOpenChange, currentRow }: Props) {
const [value, setValue] = useState(0)
const queryClient = useQueryClient();
function handleSuccess() {
toast({
title: 'Delete Success',
description: `Proxy has been successfully deleted.`,
action: <ToastAction altText="Close">Close</ToastAction>,
});
queryClient.invalidateQueries({ queryKey: ['proxy-list'] });
onOpenChange(false);
}
function handleError(error: AxiosError) {
const errorMessage = error.response?.data ||
error.message ||
`Delete failed, please try again later`;
toast({
variant: "destructive",
title: `Proxy delete Failed`,
description: errorMessage as string,
action: <ToastAction altText="Try again">Try again</ToastAction>,
});
console.error(error);
}
const deleteMutation = useMutation({
mutationFn: (id: number) => delete_proxy(id),
onSuccess: handleSuccess,
onError: handleError
})
const handleDelete = () => {
if (value !== currentRow.id) return
deleteMutation.mutate(currentRow.id)
}
return (
<ConfirmDialog
open={open}
onOpenChange={onOpenChange}
handleConfirm={handleDelete}
disabled={value !== currentRow.id}
className="max-w-2xl"
title={
<span className='text-destructive'>
<IconAlertTriangle
className='mr-1 inline-block stroke-destructive'
size={18}
/>{' '}
Delete Proxy
</span>
}
desc={
<div className='space-y-4'>
<p className='mb-2'>
Are you sure you want to delete{' '}
<span className='font-bold'>{`${currentRow.id}`}</span>?
<br />
This action will permanently remove the Proxy from the system. This cannot be undone.
</p>
<Label className='my-2'>
Proxy Id:
<Input
type="number"
value={`${value}`}
onChange={(e) => setValue(parseInt(e.target.value, 10))}
placeholder='Enter Proxy Id to confirm deletion.'
className="mt-2"
/>
</Label>
<Alert variant='destructive'>
<AlertTitle>Warning!</AlertTitle>
<AlertDescription>
Please be carefull, this operation can not be rolled back.
</AlertDescription>
</Alert>
</div>
}
confirmText='Delete'
destructive
/>
)
}
@@ -0,0 +1,153 @@
//
// Copyright (c) 2025 rustmailer.com (https://rustmailer.com)
//
// This file is part of the Bichon Email Archiving Project
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import { useState } from 'react'
import {
ColumnDef,
ColumnFiltersState,
RowData,
SortingState,
VisibilityState,
flexRender,
getCoreRowModel,
getFacetedRowModel,
getFacetedUniqueValues,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable,
} from '@tanstack/react-table'
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table'
import { Proxy } from '../data/schema'
import { DataTablePagination } from './data-table-pagination'
import { DataTableToolbar } from './data-table-toolbar'
declare module '@tanstack/react-table' {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
interface ColumnMeta<TData extends RowData, TValue> {
className: string
}
}
interface DataTableProps {
columns: ColumnDef<Proxy>[]
data: Proxy[]
}
export function ProxyTable({ columns, data }: DataTableProps) {
const [rowSelection, setRowSelection] = useState({})
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({})
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([])
const [sorting, setSorting] = useState<SortingState>([])
const table = useReactTable({
data,
columns,
state: {
sorting,
columnVisibility,
rowSelection,
columnFilters,
},
enableRowSelection: true,
onRowSelectionChange: setRowSelection,
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
onColumnVisibilityChange: setColumnVisibility,
getCoreRowModel: getCoreRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFacetedRowModel: getFacetedRowModel(),
getFacetedUniqueValues: getFacetedUniqueValues(),
})
return (
<div className='space-y-4'>
<DataTableToolbar table={table} />
<div className='rounded-md border'>
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id} className='group/row'>
{headerGroup.headers.map((header) => {
return (
<TableHead
key={header.id}
colSpan={header.colSpan}
className={header.column.columnDef.meta?.className ?? ''}
>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</TableHead>
)
})}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
data-state={row.getIsSelected() && 'selected'}
className='group/row'
>
{row.getVisibleCells().map((cell) => (
<TableCell
key={cell.id}
className={cell.column.columnDef.meta?.className ?? ''}
>
{flexRender(
cell.column.columnDef.cell,
cell.getContext()
)}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell
colSpan={columns.length}
className='h-24 text-center'
>
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
{data.length > 10 && <DataTablePagination table={table} />}
</div>
)
}