-
Notifications
You must be signed in to change notification settings - Fork 3.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Allow to filter user
by display_name
#821
Changes from 2 commits
801ad55
8eff693
622a476
99d9ee1
7099ff4
1f945fb
7eb5023
77210ee
27e1e54
0cc6b3b
c1dd188
aebfaac
15acd1c
fa5702a
6945cc5
101f2c5
d9205be
7025b17
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,158 @@ | ||
import { | ||
Box, | ||
Button, | ||
Card, | ||
CardBody, | ||
Flex, | ||
FormControl, | ||
FormLabel, | ||
Input, | ||
Popover, | ||
PopoverArrow, | ||
PopoverBody, | ||
PopoverCloseButton, | ||
PopoverContent, | ||
PopoverTrigger, | ||
Spacer, | ||
Table, | ||
TableCaption, | ||
TableContainer, | ||
Tbody, | ||
Td, | ||
Th, | ||
Thead, | ||
Tr, | ||
useDisclosure, | ||
} from "@chakra-ui/react"; | ||
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table"; | ||
import { ChangeEvent, ReactNode } from "react"; | ||
import { FaFilter } from "react-icons/fa"; | ||
import { useDebouncedCallback } from "use-debounce"; | ||
|
||
export type DataTableColumnDef<T> = ColumnDef<T> & { | ||
filterable?: boolean; | ||
}; | ||
|
||
// TODO: stricter type | ||
export type FilterItem = { | ||
id: string; | ||
value: string; | ||
}; | ||
|
||
export type DataTableProps<T> = { | ||
data: T[]; | ||
columns: DataTableColumnDef<T>[]; | ||
caption?: string; | ||
filterValues?: FilterItem[]; | ||
onNextClick?: () => void; | ||
onPreviousClick?: () => void; | ||
onFilterChange?: (items: FilterItem[]) => void; | ||
}; | ||
|
||
export const DataTable = <T,>({ | ||
data, | ||
columns, | ||
caption, | ||
filterValues = [], | ||
onNextClick, | ||
onPreviousClick, | ||
onFilterChange, | ||
}: DataTableProps<T>) => { | ||
const { getHeaderGroups, getRowModel } = useReactTable<T>({ | ||
data, | ||
columns, | ||
getCoreRowModel: getCoreRowModel(), | ||
}); | ||
|
||
const handleFilterChange = (value: FilterItem) => { | ||
const idx = filterValues.findIndex((oldValue) => oldValue.id === value.id); | ||
let newValues: FilterItem[] = []; | ||
if (idx === -1) { | ||
newValues = [...filterValues, value]; | ||
} else { | ||
newValues = filterValues.map((oldValue) => (oldValue.id === value.id ? value : oldValue)); | ||
} | ||
onFilterChange(newValues); | ||
}; | ||
return ( | ||
<Card> | ||
<CardBody> | ||
<Flex mb="2"> | ||
<Button onClick={onPreviousClick}>Previous</Button> | ||
<Spacer /> | ||
<Button onClick={onNextClick}>Next</Button> | ||
</Flex> | ||
<TableContainer> | ||
<Table variant="simple"> | ||
<TableCaption>{caption}</TableCaption> | ||
<Thead> | ||
{getHeaderGroups().map((headerGroup) => ( | ||
<Tr key={headerGroup.id}> | ||
{headerGroup.headers.map((header) => ( | ||
<Th key={header.id}> | ||
<Box display="flex" alignItems="center"> | ||
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())} | ||
{(header.column.columnDef as DataTableColumnDef<T>).filterable && ( | ||
<FilterModal | ||
value={filterValues.find((value) => value.id === header.id)?.value ?? ""} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The filter API is temporary for now. It will change significantly in the next PR. I want to have something like MUI Grid. It will be an array to support multiple filters in 1 column. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thank you, I was mainly referring to a suggestion to use object for items and not an array which would enable memorable look up of filters.
|
||
onChange={(value) => handleFilterChange({ id: header.id, value })} | ||
label={flexRender(header.column.columnDef.header, header.getContext())} | ||
></FilterModal> | ||
)} | ||
</Box> | ||
</Th> | ||
))} | ||
</Tr> | ||
))} | ||
</Thead> | ||
<Tbody> | ||
{getRowModel().rows.map((row) => ( | ||
<Tr key={row.id}> | ||
{row.getVisibleCells().map((cell) => ( | ||
<Td key={cell.id}>{flexRender(cell.column.columnDef.cell, cell.getContext())}</Td> | ||
))} | ||
</Tr> | ||
))} | ||
</Tbody> | ||
</Table> | ||
</TableContainer> | ||
</CardBody> | ||
</Card> | ||
); | ||
}; | ||
|
||
const FilterModal = ({ | ||
label, | ||
onChange, | ||
value, | ||
}: { | ||
label: ReactNode; | ||
onChange: (val: string) => void; | ||
value: string; | ||
}) => { | ||
const { isOpen, onOpen, onClose } = useDisclosure(); | ||
|
||
const handleInputChange = useDebouncedCallback((e: ChangeEvent<HTMLInputElement>) => { | ||
onChange(e.target.value); | ||
}, 500); | ||
|
||
return ( | ||
<Popover isOpen={isOpen} onOpen={onOpen} onClose={onClose}> | ||
<PopoverTrigger> | ||
<Button variant={"unstyled"} ml="2"> | ||
<FaFilter></FaFilter> | ||
</Button> | ||
</PopoverTrigger> | ||
<PopoverContent w="fit-content"> | ||
<PopoverArrow /> | ||
<PopoverCloseButton /> | ||
<PopoverBody mt="4"> | ||
<FormControl> | ||
<FormLabel>{label}</FormLabel> | ||
<Input onChange={handleInputChange} defaultValue={value}></Input> | ||
</FormControl> | ||
</PopoverBody> | ||
</PopoverContent> | ||
</Popover> | ||
); | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,133 @@ | ||
import { IconButton, useToast } from "@chakra-ui/react"; | ||
import { createColumnHelper } from "@tanstack/react-table"; | ||
import Link from "next/link"; | ||
import { memo, useState } from "react"; | ||
import { FaPen } from "react-icons/fa"; | ||
import { get } from "src/lib/api"; | ||
import type { User } from "src/types/Users"; | ||
import useSWR from "swr"; | ||
|
||
import { DataTable, DataTableColumnDef, FilterItem } from "./DataTable"; | ||
|
||
interface Pagination { | ||
/** | ||
* The user's `display_name` used for pagination. | ||
*/ | ||
cursor: string; | ||
|
||
/** | ||
* The pagination direction. | ||
*/ | ||
direction: "forward" | "back"; | ||
} | ||
|
||
const columnHelper = createColumnHelper<User>(); | ||
|
||
const columns: DataTableColumnDef<User>[] = [ | ||
columnHelper.accessor("user_id", { | ||
header: "ID", | ||
}), | ||
columnHelper.accessor("id", { | ||
header: "Auth ID", | ||
}), | ||
columnHelper.accessor("auth_method", { | ||
header: "Auth Method", | ||
}), | ||
{ | ||
...columnHelper.accessor("display_name", { | ||
header: "Name", | ||
}), | ||
filterable: true, | ||
}, | ||
columnHelper.accessor("role", { | ||
header: "Role", | ||
}), | ||
columnHelper.accessor((user) => user.user_id, { | ||
cell: ({ getValue }) => ( | ||
<IconButton | ||
as={Link} | ||
href={`/admin/manage_user/${getValue()}`} | ||
aria-label="Manage" | ||
icon={<FaPen></FaPen>} | ||
></IconButton> | ||
), | ||
header: "Update", | ||
}), | ||
]; | ||
|
||
export const UserTable = memo(function UserTable() { | ||
const toast = useToast(); | ||
const [pagination, setPagination] = useState<Pagination>({ cursor: "", direction: "forward" }); | ||
const [users, setUsers] = useState<User[]>([]); | ||
const [filterValues, setFilterValues] = useState<FilterItem[]>([]); | ||
// Fetch and save the users. | ||
// This follows useSWR's recommendation for simple pagination: | ||
// https://swr.vercel.app/docs/pagination#when-to-use-useswr | ||
const display_name = filterValues.find((value) => value.id === "display_name")?.value ?? ""; | ||
useSWR( | ||
`/api/admin/users?direction=${pagination.direction}&cursor=${pagination.cursor}&display_name=${display_name}`, | ||
get, | ||
{ | ||
onSuccess: (data) => { | ||
// When no more users can be found, trigger a toast to indicate why no | ||
// changes have taken place. We have to maintain a non-empty set of | ||
// users otherwise we can't paginate using a cursor (since we've lost the | ||
// cursor). | ||
if (data.length === 0) { | ||
toast({ | ||
title: "No more users", | ||
status: "warning", | ||
duration: 1000, | ||
isClosable: true, | ||
}); | ||
return; | ||
} | ||
setUsers(data); | ||
}, | ||
} | ||
); | ||
|
||
const toPreviousPage = () => { | ||
if (users.length >= 0) { | ||
setPagination({ | ||
cursor: users[0].user_id, | ||
direction: "back", | ||
}); | ||
} else { | ||
toast({ | ||
title: "Can not paginate when no users are found", | ||
status: "warning", | ||
duration: 1000, | ||
isClosable: true, | ||
}); | ||
} | ||
}; | ||
|
||
const toNextPage = () => { | ||
if (users.length >= 0) { | ||
setPagination({ | ||
cursor: users[users.length - 1].user_id, | ||
direction: "forward", | ||
}); | ||
} else { | ||
toast({ | ||
title: "Can not paginate when no users are found", | ||
status: "warning", | ||
duration: 1000, | ||
isClosable: true, | ||
}); | ||
} | ||
}; | ||
|
||
return ( | ||
<DataTable | ||
data={users} | ||
columns={columns} | ||
caption="Users" | ||
onNextClick={toNextPage} | ||
onPreviousClick={toPreviousPage} | ||
filterValues={filterValues} | ||
onFilterChange={setFilterValues} | ||
></DataTable> | ||
); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure what the intention here is, but it doesn't look right.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought we want to cursor based on the user id?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is the user id column monotonically increasing?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
UUID V4 isn't guaranteed to be monotonically increasing. I think only UUID V6 or V7 have that property. V4 is random in it's ordering.