fix(admin): unable to select all users in current page (#11155)
close AF-2380
This commit is contained in:
@@ -3,15 +3,20 @@ import {
|
|||||||
AvatarFallback,
|
AvatarFallback,
|
||||||
AvatarImage,
|
AvatarImage,
|
||||||
} from '@affine/admin/components/ui/avatar';
|
} from '@affine/admin/components/ui/avatar';
|
||||||
import type { UserType } from '@affine/graphql';
|
|
||||||
import { FeatureType } from '@affine/graphql';
|
import { FeatureType } from '@affine/graphql';
|
||||||
import { AccountIcon, LockIcon, UnlockIcon } from '@blocksuite/icons/rc';
|
import { AccountIcon, LockIcon, UnlockIcon } from '@blocksuite/icons/rc';
|
||||||
import type { ColumnDef } from '@tanstack/react-table';
|
import type { ColumnDef } from '@tanstack/react-table';
|
||||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||||
import { MailIcon } from 'lucide-react';
|
import { MailIcon } from 'lucide-react';
|
||||||
import type { ReactNode } from 'react';
|
import {
|
||||||
|
type Dispatch,
|
||||||
|
type ReactNode,
|
||||||
|
type SetStateAction,
|
||||||
|
useMemo,
|
||||||
|
} from 'react';
|
||||||
|
|
||||||
import { Checkbox } from '../../../components/ui/checkbox';
|
import { Checkbox } from '../../../components/ui/checkbox';
|
||||||
|
import type { UserType } from '../schema';
|
||||||
import { DataTableColumnHeader } from './data-table-column-header';
|
import { DataTableColumnHeader } from './data-table-column-header';
|
||||||
import { DataTableRowActions } from './data-table-row-actions';
|
import { DataTableRowActions } from './data-table-row-actions';
|
||||||
|
|
||||||
@@ -47,139 +52,190 @@ const StatusItem = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
export const useColumns = ({
|
||||||
|
setSelectedUserIds,
|
||||||
|
}: {
|
||||||
|
setSelectedUserIds: Dispatch<SetStateAction<Set<string>>>;
|
||||||
|
}) => {
|
||||||
|
const columns: ColumnDef<UserType>[] = useMemo(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: 'select',
|
||||||
|
header: ({ table }) => (
|
||||||
|
<Checkbox
|
||||||
|
checked={
|
||||||
|
table.getIsAllPageRowsSelected() ||
|
||||||
|
(table.getIsSomePageRowsSelected() && 'indeterminate')
|
||||||
|
}
|
||||||
|
onCheckedChange={value => {
|
||||||
|
if (value) {
|
||||||
|
setSelectedUserIds(
|
||||||
|
prev =>
|
||||||
|
new Set([
|
||||||
|
...prev,
|
||||||
|
...table
|
||||||
|
.getFilteredRowModel()
|
||||||
|
.rows.map(row => row.original.id),
|
||||||
|
])
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// remove selected users in the current page
|
||||||
|
setSelectedUserIds(
|
||||||
|
prev =>
|
||||||
|
new Set(
|
||||||
|
[...prev].filter(
|
||||||
|
id =>
|
||||||
|
!table
|
||||||
|
.getFilteredRowModel()
|
||||||
|
.rows.some(row => row.original.id === id)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export const columns: ColumnDef<UserType>[] = [
|
table.toggleAllPageRowsSelected(!!value);
|
||||||
{
|
|
||||||
id: 'select',
|
|
||||||
header: ({ table }) => (
|
|
||||||
<Checkbox
|
|
||||||
checked={
|
|
||||||
table.getIsAllPageRowsSelected() ||
|
|
||||||
(table.getIsSomePageRowsSelected() && 'indeterminate')
|
|
||||||
}
|
|
||||||
onCheckedChange={value => table.toggleAllPageRowsSelected(!!value)}
|
|
||||||
aria-label="Select all"
|
|
||||||
className="translate-y-[2px]"
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
cell: ({ row }) => (
|
|
||||||
<Checkbox
|
|
||||||
checked={row.getIsSelected()}
|
|
||||||
onCheckedChange={value => row.toggleSelected(!!value)}
|
|
||||||
aria-label="Select row"
|
|
||||||
className="translate-y-[2px]"
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
enableSorting: false,
|
|
||||||
enableHiding: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: 'info',
|
|
||||||
header: ({ column }) => (
|
|
||||||
<DataTableColumnHeader className="text-xs" column={column} title="Name" />
|
|
||||||
),
|
|
||||||
cell: ({ row }) => (
|
|
||||||
<div className="flex gap-4 items-center max-w-[50vw] overflow-hidden">
|
|
||||||
<Avatar className="w-10 h-10">
|
|
||||||
<AvatarImage src={row.original.avatarUrl ?? undefined} />
|
|
||||||
<AvatarFallback>
|
|
||||||
<AccountIcon fontSize={20} />
|
|
||||||
</AvatarFallback>
|
|
||||||
</Avatar>
|
|
||||||
<div className="flex flex-col gap-1 max-w-full overflow-hidden">
|
|
||||||
<div className="text-sm font-medium max-w-full overflow-hidden gap-[6px]">
|
|
||||||
<span>{row.original.name}</span>
|
|
||||||
{row.original.features.includes(FeatureType.Admin) && (
|
|
||||||
<span
|
|
||||||
className="ml-2 rounded px-2 py-0.5 text-xs h-5 border text-center inline-flex items-center font-normal"
|
|
||||||
style={{
|
|
||||||
borderRadius: '4px',
|
|
||||||
backgroundColor: cssVarV2('chip/label/blue'),
|
|
||||||
borderColor: cssVarV2('layer/insideBorder/border'),
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Admin
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{row.original.disabled && (
|
|
||||||
<span
|
|
||||||
className="ml-2 rounded px-2 py-0.5 text-xs h-5 border"
|
|
||||||
style={{
|
|
||||||
borderRadius: '4px',
|
|
||||||
backgroundColor: cssVarV2('chip/label/white'),
|
|
||||||
borderColor: cssVarV2('layer/insideBorder/border'),
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Disabled
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="text-xs font-medium max-w-full overflow-hidden"
|
|
||||||
style={{
|
|
||||||
color: cssVarV2('text/secondary'),
|
|
||||||
}}
|
}}
|
||||||
>
|
aria-label="Select all"
|
||||||
{row.original.email}
|
className="translate-y-[2px]"
|
||||||
</div>
|
/>
|
||||||
</div>
|
),
|
||||||
</div>
|
cell: ({ row }) => (
|
||||||
),
|
<Checkbox
|
||||||
enableSorting: false,
|
checked={row.getIsSelected()}
|
||||||
enableHiding: false,
|
onCheckedChange={value => {
|
||||||
},
|
if (value) {
|
||||||
{
|
setSelectedUserIds(prev => new Set([...prev, row.original.id]));
|
||||||
accessorKey: 'property',
|
} else {
|
||||||
header: ({ column }) => (
|
setSelectedUserIds(
|
||||||
<DataTableColumnHeader
|
prev =>
|
||||||
className="text-xs max-md:hidden"
|
new Set([...prev].filter(id => id !== row.original.id))
|
||||||
column={column}
|
);
|
||||||
title="UUID"
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
cell: ({ row: { original: user } }) => (
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<div className="flex flex-col gap-2 text-xs max-md:hidden">
|
|
||||||
<div className="flex justify-start">{user.id}</div>
|
|
||||||
<div className="flex gap-3 items-center justify-start">
|
|
||||||
<StatusItem
|
|
||||||
condition={user.hasPassword}
|
|
||||||
IconTrue={
|
|
||||||
<LockIcon
|
|
||||||
fontSize={16}
|
|
||||||
color={cssVarV2('selfhost/icon/tertiary')}
|
|
||||||
/>
|
|
||||||
}
|
}
|
||||||
IconFalse={<UnlockIcon fontSize={16} />}
|
row.toggleSelected(!!value);
|
||||||
textTrue="Password Set"
|
}}
|
||||||
textFalse="No Password"
|
aria-label="Select row"
|
||||||
/>
|
className="translate-y-[2px]"
|
||||||
<StatusItem
|
/>
|
||||||
condition={user.emailVerified}
|
),
|
||||||
IconTrue={
|
enableSorting: false,
|
||||||
<MailIcon
|
enableHiding: false,
|
||||||
size={16}
|
},
|
||||||
color={cssVarV2('selfhost/icon/tertiary')}
|
{
|
||||||
/>
|
accessorKey: 'info',
|
||||||
}
|
header: ({ column }) => (
|
||||||
IconFalse={<MailIcon size={16} />}
|
<DataTableColumnHeader
|
||||||
textTrue="Email Verified"
|
className="text-xs"
|
||||||
textFalse="Email Not Verified"
|
column={column}
|
||||||
/>
|
title="Name"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<div className="flex gap-4 items-center max-w-[50vw] overflow-hidden">
|
||||||
|
<Avatar className="w-10 h-10">
|
||||||
|
<AvatarImage src={row.original.avatarUrl ?? undefined} />
|
||||||
|
<AvatarFallback>
|
||||||
|
<AccountIcon fontSize={20} />
|
||||||
|
</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
<div className="flex flex-col gap-1 max-w-full overflow-hidden">
|
||||||
|
<div className="text-sm font-medium max-w-full overflow-hidden gap-[6px]">
|
||||||
|
<span>{row.original.name}</span>
|
||||||
|
{row.original.features.includes(FeatureType.Admin) && (
|
||||||
|
<span
|
||||||
|
className="ml-2 rounded px-2 py-0.5 text-xs h-5 border text-center inline-flex items-center font-normal"
|
||||||
|
style={{
|
||||||
|
borderRadius: '4px',
|
||||||
|
backgroundColor: cssVarV2('chip/label/blue'),
|
||||||
|
borderColor: cssVarV2('layer/insideBorder/border'),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Admin
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{row.original.disabled && (
|
||||||
|
<span
|
||||||
|
className="ml-2 rounded px-2 py-0.5 text-xs h-5 border"
|
||||||
|
style={{
|
||||||
|
borderRadius: '4px',
|
||||||
|
backgroundColor: cssVarV2('chip/label/white'),
|
||||||
|
borderColor: cssVarV2('layer/insideBorder/border'),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Disabled
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="text-xs font-medium max-w-full overflow-hidden"
|
||||||
|
style={{
|
||||||
|
color: cssVarV2('text/secondary'),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{row.original.email}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
),
|
||||||
</div>
|
enableSorting: false,
|
||||||
),
|
enableHiding: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'actions',
|
accessorKey: 'property',
|
||||||
header: ({ column }) => (
|
header: ({ column }) => (
|
||||||
<DataTableColumnHeader
|
<DataTableColumnHeader
|
||||||
className="text-xs"
|
className="text-xs max-md:hidden"
|
||||||
column={column}
|
column={column}
|
||||||
title="Actions"
|
title="UUID"
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
cell: ({ row: { original: user } }) => <DataTableRowActions user={user} />,
|
cell: ({ row: { original: user } }) => (
|
||||||
},
|
<div className="flex items-center gap-2">
|
||||||
];
|
<div className="flex flex-col gap-2 text-xs max-md:hidden">
|
||||||
|
<div className="flex justify-start">{user.id}</div>
|
||||||
|
<div className="flex gap-3 items-center justify-start">
|
||||||
|
<StatusItem
|
||||||
|
condition={user.hasPassword}
|
||||||
|
IconTrue={
|
||||||
|
<LockIcon
|
||||||
|
fontSize={16}
|
||||||
|
color={cssVarV2('selfhost/icon/tertiary')}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
IconFalse={<UnlockIcon fontSize={16} />}
|
||||||
|
textTrue="Password Set"
|
||||||
|
textFalse="No Password"
|
||||||
|
/>
|
||||||
|
<StatusItem
|
||||||
|
condition={user.emailVerified}
|
||||||
|
IconTrue={
|
||||||
|
<MailIcon
|
||||||
|
size={16}
|
||||||
|
color={cssVarV2('selfhost/icon/tertiary')}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
IconFalse={<MailIcon size={16} />}
|
||||||
|
textTrue="Email Verified"
|
||||||
|
textFalse="Email Not Verified"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'actions',
|
||||||
|
header: ({ column }) => (
|
||||||
|
<DataTableColumnHeader
|
||||||
|
className="text-xs"
|
||||||
|
column={column}
|
||||||
|
title="Actions"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
cell: ({ row: { original: user } }) => (
|
||||||
|
<DataTableRowActions user={user} />
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}, [setSelectedUserIds]);
|
||||||
|
return columns;
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Button } from '@affine/admin/components/ui/button';
|
import { Button } from '@affine/admin/components/ui/button';
|
||||||
import { Input } from '@affine/admin/components/ui/input';
|
import { Input } from '@affine/admin/components/ui/input';
|
||||||
import { useQuery } from '@affine/admin/use-query';
|
import { useQuery } from '@affine/admin/use-query';
|
||||||
import { getUserByEmailQuery, type UserType } from '@affine/graphql';
|
import { getUserByEmailQuery } from '@affine/graphql';
|
||||||
import { ExportIcon, ImportIcon, PlusIcon } from '@blocksuite/icons/rc';
|
import { ExportIcon, ImportIcon, PlusIcon } from '@blocksuite/icons/rc';
|
||||||
import type { Table } from '@tanstack/react-table';
|
import type { Table } from '@tanstack/react-table';
|
||||||
import {
|
import {
|
||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
} from 'react';
|
} from 'react';
|
||||||
|
|
||||||
import { useRightPanel } from '../../panel/context';
|
import { useRightPanel } from '../../panel/context';
|
||||||
|
import type { UserType } from '../schema';
|
||||||
import { DiscardChanges } from './discard-changes';
|
import { DiscardChanges } from './discard-changes';
|
||||||
import { ExportUsersDialog } from './export-users-dialog';
|
import { ExportUsersDialog } from './export-users-dialog';
|
||||||
import { ImportUsersDialog } from './import-users-dialog';
|
import { ImportUsersDialog } from './import-users-dialog';
|
||||||
@@ -22,6 +23,7 @@ import { CreateUserForm } from './user-form';
|
|||||||
interface DataTableToolbarProps<TData> {
|
interface DataTableToolbarProps<TData> {
|
||||||
data: TData[];
|
data: TData[];
|
||||||
setDataTable: (data: TData[]) => void;
|
setDataTable: (data: TData[]) => void;
|
||||||
|
selectedUsers: UserType[];
|
||||||
table?: Table<TData>;
|
table?: Table<TData>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,6 +60,7 @@ function useDebouncedValue<T>(value: T, delay: number): T {
|
|||||||
|
|
||||||
export function DataTableToolbar<TData>({
|
export function DataTableToolbar<TData>({
|
||||||
data,
|
data,
|
||||||
|
selectedUsers,
|
||||||
setDataTable,
|
setDataTable,
|
||||||
table,
|
table,
|
||||||
}: DataTableToolbarProps<TData>) {
|
}: DataTableToolbarProps<TData>) {
|
||||||
@@ -158,9 +161,7 @@ export function DataTableToolbar<TData>({
|
|||||||
|
|
||||||
{table && (
|
{table && (
|
||||||
<ExportUsersDialog
|
<ExportUsersDialog
|
||||||
users={table
|
users={selectedUsers}
|
||||||
.getFilteredSelectedRowModel()
|
|
||||||
.rows.map(row => row.original as UserType)}
|
|
||||||
open={exportDialogOpen}
|
open={exportDialogOpen}
|
||||||
onOpenChange={setExportDialogOpen}
|
onOpenChange={setExportDialogOpen}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
} from '@tanstack/react-table';
|
} from '@tanstack/react-table';
|
||||||
import { type Dispatch, type SetStateAction, useEffect, useState } from 'react';
|
import { type Dispatch, type SetStateAction, useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import type { UserType } from '../schema';
|
||||||
import { DataTablePagination } from './data-table-pagination';
|
import { DataTablePagination } from './data-table-pagination';
|
||||||
import { DataTableToolbar } from './data-table-toolbar';
|
import { DataTableToolbar } from './data-table-toolbar';
|
||||||
import { useUserCount } from './use-user-management';
|
import { useUserCount } from './use-user-management';
|
||||||
@@ -26,6 +27,7 @@ interface DataTableProps<TData, TValue> {
|
|||||||
columns: ColumnDef<TData, TValue>[];
|
columns: ColumnDef<TData, TValue>[];
|
||||||
data: TData[];
|
data: TData[];
|
||||||
pagination: PaginationState;
|
pagination: PaginationState;
|
||||||
|
selectedUsers: UserType[];
|
||||||
onPaginationChange: Dispatch<
|
onPaginationChange: Dispatch<
|
||||||
SetStateAction<{
|
SetStateAction<{
|
||||||
pageIndex: number;
|
pageIndex: number;
|
||||||
@@ -34,10 +36,11 @@ interface DataTableProps<TData, TValue> {
|
|||||||
>;
|
>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DataTable<TData, TValue>({
|
export function DataTable<TData extends { id: string }, TValue>({
|
||||||
columns,
|
columns,
|
||||||
data,
|
data,
|
||||||
pagination,
|
pagination,
|
||||||
|
selectedUsers,
|
||||||
onPaginationChange,
|
onPaginationChange,
|
||||||
}: DataTableProps<TData, TValue>) {
|
}: DataTableProps<TData, TValue>) {
|
||||||
const usersCount = useUserCount();
|
const usersCount = useUserCount();
|
||||||
@@ -50,6 +53,7 @@ export function DataTable<TData, TValue>({
|
|||||||
data: tableData,
|
data: tableData,
|
||||||
columns,
|
columns,
|
||||||
getCoreRowModel: getCoreRowModel(),
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
getRowId: row => row.id,
|
||||||
manualPagination: true,
|
manualPagination: true,
|
||||||
rowCount: usersCount,
|
rowCount: usersCount,
|
||||||
enableFilters: true,
|
enableFilters: true,
|
||||||
@@ -70,7 +74,12 @@ export function DataTable<TData, TValue>({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-4 py-5 px-6 h-full overflow-auto">
|
<div className="flex flex-col gap-4 py-5 px-6 h-full overflow-auto">
|
||||||
<DataTableToolbar setDataTable={setTableData} data={data} table={table} />
|
<DataTableToolbar
|
||||||
|
setDataTable={setTableData}
|
||||||
|
data={data}
|
||||||
|
table={table}
|
||||||
|
selectedUsers={selectedUsers}
|
||||||
|
/>
|
||||||
<div className="rounded-md border h-full flex flex-col overflow-auto">
|
<div className="rounded-md border h-full flex flex-col overflow-auto">
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
|
|||||||
@@ -9,11 +9,11 @@ import {
|
|||||||
} from '@affine/admin/components/ui/dialog';
|
} from '@affine/admin/components/ui/dialog';
|
||||||
import { Label } from '@affine/admin/components/ui/label';
|
import { Label } from '@affine/admin/components/ui/label';
|
||||||
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
|
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
|
||||||
import type { UserType } from '@affine/graphql';
|
|
||||||
import { CopyIcon } from '@blocksuite/icons/rc';
|
import { CopyIcon } from '@blocksuite/icons/rc';
|
||||||
import { useCallback, useState } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
|
import type { UserType } from '../schema';
|
||||||
import { type ExportField, useExportUsers } from './use-user-management';
|
import { type ExportField, useExportUsers } from './use-user-management';
|
||||||
|
|
||||||
interface ExportUsersDialogProps {
|
interface ExportUsersDialogProps {
|
||||||
|
|||||||
@@ -1,20 +1,39 @@
|
|||||||
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import { Header } from '../header';
|
import { Header } from '../header';
|
||||||
import { columns } from './components/columns';
|
import { useColumns } from './components/columns';
|
||||||
import { DataTable } from './components/data-table';
|
import { DataTable } from './components/data-table';
|
||||||
|
import type { UserType } from './schema';
|
||||||
import { useUserList } from './use-user-list';
|
import { useUserList } from './use-user-list';
|
||||||
|
|
||||||
export function AccountPage() {
|
export function AccountPage() {
|
||||||
const { users, pagination, setPagination } = useUserList();
|
const { users, pagination, setPagination } = useUserList();
|
||||||
|
// Remember the user temporarily, because userList is paginated on the server side,can't get all users at once.
|
||||||
|
const [memoUsers, setMemoUsers] = useState<UserType[]>([]);
|
||||||
|
|
||||||
|
const [selectedUserIds, setSelectedUserIds] = useState<Set<string>>(
|
||||||
|
new Set<string>()
|
||||||
|
);
|
||||||
|
const columns = useColumns({ setSelectedUserIds });
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setMemoUsers(prev => [...new Set([...prev, ...users])]);
|
||||||
|
}, [users]);
|
||||||
|
|
||||||
|
const selectedUsers = useMemo(() => {
|
||||||
|
return memoUsers.filter(user => selectedUserIds.has(user.id));
|
||||||
|
}, [selectedUserIds, memoUsers]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className=" h-screen flex-1 flex-col flex">
|
<div className=" h-screen flex-1 flex-col flex">
|
||||||
<Header title="Accounts" />
|
<Header title="Accounts" />
|
||||||
|
|
||||||
<DataTable
|
<DataTable
|
||||||
data={users}
|
data={users}
|
||||||
// @ts-expect-error do not complains
|
|
||||||
columns={columns}
|
columns={columns}
|
||||||
pagination={pagination}
|
pagination={pagination}
|
||||||
onPaginationChange={setPagination}
|
onPaginationChange={setPagination}
|
||||||
|
selectedUsers={selectedUsers}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user