-
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 all 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,166 @@ | ||
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; | ||
disableNext?: boolean; | ||
disablePrevious?: boolean; | ||
}; | ||
|
||
export const DataTable = <T,>({ | ||
data, | ||
columns, | ||
caption, | ||
filterValues = [], | ||
onNextClick, | ||
onPreviousClick, | ||
onFilterChange, | ||
disableNext, | ||
disablePrevious, | ||
}: 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} disabled={disablePrevious}> | ||
Previous | ||
</Button> | ||
<Spacer /> | ||
<Button onClick={onNextClick} disabled={disableNext}> | ||
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,108 @@ | ||
import { IconButton } 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 { FetchUsersResponse } from "src/lib/oasst_api_client"; | ||
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 [pagination, setPagination] = useState<Pagination>({ cursor: "", direction: "forward" }); | ||
const [filterValues, setFilterValues] = useState<FilterItem[]>([]); | ||
const handleFilterValuesChange = (values: FilterItem[]) => { | ||
setFilterValues(values); | ||
setPagination((old) => ({ ...old, cursor: "" })); | ||
}; | ||
// 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 ?? ""; | ||
const { data, error } = useSWR<FetchUsersResponse<User>>( | ||
`/api/admin/users?direction=${pagination.direction}&cursor=${pagination.cursor}&searchDisplayName=${display_name}&sortKey=display_name`, | ||
get, | ||
{ | ||
keepPreviousData: true, | ||
} | ||
); | ||
|
||
const toPreviousPage = () => { | ||
setPagination({ | ||
cursor: data.prev, | ||
direction: "back", | ||
}); | ||
}; | ||
|
||
const toNextPage = () => { | ||
setPagination({ | ||
cursor: data.next, | ||
direction: "forward", | ||
}); | ||
}; | ||
|
||
return ( | ||
<> | ||
<DataTable | ||
data={data?.items || []} | ||
columns={columns} | ||
caption="Users" | ||
onNextClick={toNextPage} | ||
onPreviousClick={toPreviousPage} | ||
disableNext={!data?.next} | ||
disablePrevious={!data?.prev} | ||
filterValues={filterValues} | ||
onFilterChange={handleFilterValuesChange} | ||
></DataTable> | ||
{error && "Unable to load users."} | ||
</> | ||
); | ||
}); |
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.
this newline triggers my review as code owner. it's a very beautiful newline. I approve ;)