mirror of
https://github.com/wassname/optuna-dashboard.git
synced 2026-09-09 11:28:14 +08:00
Copy DataGrid implementation
This commit is contained in:
@@ -1,9 +1,15 @@
|
||||
import { Clear } from "@mui/icons-material"
|
||||
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown"
|
||||
import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp"
|
||||
import CheckBoxIcon from "@mui/icons-material/CheckBox"
|
||||
import CheckBoxOutlineBlankIcon from "@mui/icons-material/CheckBoxOutlineBlank"
|
||||
import FilterListIcon from "@mui/icons-material/FilterList"
|
||||
import FirstPageIcon from "@mui/icons-material/FirstPage"
|
||||
import KeyboardArrowLeft from "@mui/icons-material/KeyboardArrowLeft"
|
||||
import KeyboardArrowRight from "@mui/icons-material/KeyboardArrowRight"
|
||||
import LastPageIcon from "@mui/icons-material/LastPage"
|
||||
import {
|
||||
Collapse,
|
||||
Box,
|
||||
IconButton,
|
||||
Menu,
|
||||
MenuItem,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
@@ -12,372 +18,364 @@ import {
|
||||
TablePagination,
|
||||
TableRow,
|
||||
TableSortLabel,
|
||||
TextField,
|
||||
useTheme,
|
||||
} from "@mui/material"
|
||||
import ListItemIcon from "@mui/material/ListItemIcon"
|
||||
import Paper from "@mui/material/Paper"
|
||||
import { TablePaginationActionsProps } from "@mui/material/TablePagination/TablePaginationActions"
|
||||
import { styled } from "@mui/system"
|
||||
import React from "react"
|
||||
|
||||
type Order = "asc" | "desc"
|
||||
import {
|
||||
ColumnDef,
|
||||
ColumnFiltersState,
|
||||
Header,
|
||||
PaginationState,
|
||||
SortingState,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
getFacetedRowModel,
|
||||
getFacetedUniqueValues,
|
||||
getFilteredRowModel,
|
||||
getPaginationRowModel,
|
||||
getSortedRowModel,
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table"
|
||||
|
||||
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
|
||||
type Value = any
|
||||
const TableHeaderCellSpan = styled("span")({
|
||||
display: "inline-flex",
|
||||
})
|
||||
|
||||
const defaultRowsPerPageOption = [10, 50, 100, { label: "All", value: -1 }]
|
||||
const HiddenSpan = styled("span")({
|
||||
border: 0,
|
||||
clip: "rect(0 0 0 0)",
|
||||
height: 1,
|
||||
margin: -1,
|
||||
overflow: "hidden",
|
||||
padding: 0,
|
||||
position: "absolute",
|
||||
top: 20,
|
||||
width: 1,
|
||||
})
|
||||
|
||||
interface DataGridColumn<T> {
|
||||
field: keyof T
|
||||
label: string
|
||||
sortable?: boolean
|
||||
less?: (a: T, b: T, ascending: boolean) => number
|
||||
filterable?: boolean
|
||||
toCellValue?: (rowIndex: number) => string | React.ReactNode
|
||||
padding?: "normal" | "checkbox" | "none"
|
||||
}
|
||||
|
||||
interface RowFilter {
|
||||
columnIdx: number
|
||||
value: Value
|
||||
}
|
||||
|
||||
function DataGrid<T>(props: {
|
||||
columns: DataGridColumn<T>[]
|
||||
rows: T[]
|
||||
keyField: keyof T
|
||||
dense?: boolean
|
||||
collapseBody?: (rowIndex: number) => React.ReactNode
|
||||
initialRowsPerPage?: number
|
||||
rowsPerPageOption?: Array<number | { value: number; label: string }>
|
||||
defaultFilter?: (row: T) => boolean
|
||||
function FilterMenu<T>({
|
||||
header,
|
||||
filterChoices,
|
||||
}: {
|
||||
header: Header<T, unknown>
|
||||
filterChoices: string[]
|
||||
}): React.ReactElement {
|
||||
const { columns, rows, keyField, dense, collapseBody, defaultFilter } = props
|
||||
let { initialRowsPerPage, rowsPerPageOption } = props
|
||||
const [order, setOrder] = React.useState<Order>("asc")
|
||||
const [orderBy, setOrderBy] = React.useState<number>(0) // index of columns
|
||||
const [page, setPage] = React.useState(0)
|
||||
const [filters, setFilters] = React.useState<RowFilter[]>([])
|
||||
|
||||
const getRowIndex = (row: T): number => {
|
||||
return rows.findIndex((row2) => row[keyField] === row2[keyField])
|
||||
}
|
||||
|
||||
// Pagination
|
||||
rowsPerPageOption = rowsPerPageOption || defaultRowsPerPageOption
|
||||
initialRowsPerPage = initialRowsPerPage // use first element as default
|
||||
? initialRowsPerPage
|
||||
: isNumber(rowsPerPageOption[0])
|
||||
? rowsPerPageOption[0]
|
||||
: rowsPerPageOption[0].value
|
||||
const [rowsPerPage, setRowsPerPage] = React.useState(initialRowsPerPage)
|
||||
|
||||
const handleChangePage = (_event: unknown, newPage: number) => {
|
||||
setPage(newPage)
|
||||
}
|
||||
|
||||
const handleChangeRowsPerPage = (
|
||||
event: React.ChangeEvent<HTMLInputElement>
|
||||
) => {
|
||||
setRowsPerPage(parseInt(event.target.value, 10))
|
||||
setPage(0)
|
||||
}
|
||||
|
||||
// Filtering
|
||||
const fieldAlreadyFiltered = (columnIdx: number): boolean =>
|
||||
filters.some((f) => f.columnIdx === columnIdx)
|
||||
|
||||
const handleClickFilterCell = (columnIdx: number, value: Value) => {
|
||||
if (fieldAlreadyFiltered(columnIdx)) {
|
||||
return
|
||||
}
|
||||
const newFilters = [...filters, { columnIdx: columnIdx, value: value }]
|
||||
setFilters(newFilters)
|
||||
}
|
||||
|
||||
const clearFilter = (columnIdx: number): void => {
|
||||
setFilters(filters.filter((f) => f.columnIdx !== columnIdx))
|
||||
}
|
||||
|
||||
const filteredRows = rows.filter((row, rowIdx) => {
|
||||
if (defaultFilter?.(row)) {
|
||||
return false
|
||||
}
|
||||
return filters.length === 0
|
||||
? true
|
||||
: filters.some((f) => {
|
||||
if (columns.length <= f.columnIdx) {
|
||||
console.log(
|
||||
`columnIdx=${f.columnIdx} must be smaller than columns.length=${columns.length}`
|
||||
)
|
||||
return true
|
||||
}
|
||||
const toCellValue = columns[f.columnIdx].toCellValue
|
||||
if (toCellValue !== undefined) {
|
||||
return toCellValue(rowIdx) === f.value
|
||||
}
|
||||
const field = columns[f.columnIdx].field
|
||||
return row[field] === f.value
|
||||
})
|
||||
})
|
||||
|
||||
// Sorting
|
||||
const createSortHandler = (columnId: number) => () => {
|
||||
const isAsc = orderBy === columnId && order === "asc"
|
||||
setOrder(isAsc ? "desc" : "asc")
|
||||
setOrderBy(columnId)
|
||||
}
|
||||
const sortedRows = stableSort<T>(filteredRows, order, orderBy, columns)
|
||||
const currentPageRows =
|
||||
rowsPerPage > 0
|
||||
? sortedRows.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage)
|
||||
: sortedRows
|
||||
const emptyRows =
|
||||
rowsPerPage - Math.min(rowsPerPage, sortedRows.length - page * rowsPerPage)
|
||||
|
||||
const RootDiv = styled("div")({
|
||||
width: "100%",
|
||||
})
|
||||
const HiddenSpan = styled("span")({
|
||||
border: 0,
|
||||
clip: "rect(0 0 0 0)",
|
||||
height: 1,
|
||||
margin: -1,
|
||||
overflow: "hidden",
|
||||
padding: 0,
|
||||
position: "absolute",
|
||||
top: 20,
|
||||
width: 1,
|
||||
})
|
||||
const TableHeaderCellSpan = styled("span")({
|
||||
display: "inline-flex",
|
||||
})
|
||||
const [filterMenuAnchorEl, setFilterMenuAnchorEl] =
|
||||
React.useState<null | HTMLElement>(null)
|
||||
return (
|
||||
<RootDiv>
|
||||
<TableContainer>
|
||||
<Table
|
||||
aria-labelledby="tableTitle"
|
||||
size={dense ? "small" : "medium"}
|
||||
aria-label="data grid"
|
||||
>
|
||||
<>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={(e) => {
|
||||
setFilterMenuAnchorEl(e.currentTarget)
|
||||
}}
|
||||
>
|
||||
<FilterListIcon fontSize="small" />
|
||||
</IconButton>
|
||||
<Menu
|
||||
anchorEl={filterMenuAnchorEl}
|
||||
open={filterMenuAnchorEl !== null}
|
||||
onClose={() => {
|
||||
setFilterMenuAnchorEl(null)
|
||||
}}
|
||||
>
|
||||
{filterChoices.map((choice) => (
|
||||
<MenuItem
|
||||
key={choice}
|
||||
onClick={() => {
|
||||
const skippedValues = header.column.getFilterValue() as string[]
|
||||
const isSkipped = skippedValues.includes(choice)
|
||||
const newSkippedValues = isSkipped
|
||||
? skippedValues.filter((v) => v !== choice)
|
||||
: skippedValues.concat(choice)
|
||||
header.column.setFilterValue(newSkippedValues)
|
||||
}}
|
||||
>
|
||||
<ListItemIcon>
|
||||
{header.column.getFilterValue() !== undefined ? (
|
||||
(header.column.getFilterValue() as string[]).includes(
|
||||
choice
|
||||
) ? (
|
||||
<CheckBoxOutlineBlankIcon color="primary" />
|
||||
) : (
|
||||
<CheckBoxIcon color="primary" />
|
||||
)
|
||||
) : null}
|
||||
</ListItemIcon>
|
||||
{choice ?? "(missing value)"}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Menu>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function DataGrid<T>({
|
||||
data,
|
||||
columns,
|
||||
}: {
|
||||
data: T[]
|
||||
columns: ColumnDef<T>[]
|
||||
}): React.ReactElement {
|
||||
const [sorting, setSorting] = React.useState<SortingState>([])
|
||||
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
|
||||
[]
|
||||
)
|
||||
const [pagination, setPagination] = React.useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: 50,
|
||||
})
|
||||
|
||||
const table = useReactTable({
|
||||
data,
|
||||
columns,
|
||||
state: {
|
||||
columnFilters,
|
||||
sorting,
|
||||
pagination,
|
||||
},
|
||||
onColumnFiltersChange: setColumnFilters,
|
||||
onSortingChange: setSorting,
|
||||
onPaginationChange: setPagination,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getFacetedRowModel: getFacetedRowModel(),
|
||||
getFacetedUniqueValues: getFacetedUniqueValues(),
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
getFilteredRowModel: getFilteredRowModel(),
|
||||
autoResetPageIndex: false,
|
||||
})
|
||||
|
||||
return (
|
||||
<Box component="div" sx={{ width: "100%" }}>
|
||||
<TableContainer component={Paper}>
|
||||
<Table sx={{ minWidth: 650 }} aria-label="simple table">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
{collapseBody ? <TableCell /> : null}
|
||||
{columns.map((column, columnIdx) => (
|
||||
<TableCell
|
||||
// biome-ignore lint/suspicious/noArrayIndexKey: <explanation>
|
||||
key={columnIdx}
|
||||
padding={column.padding || "normal"}
|
||||
sortDirection={orderBy === column.field ? order : false}
|
||||
>
|
||||
<TableHeaderCellSpan>
|
||||
{column.sortable ? (
|
||||
<TableSortLabel
|
||||
active={orderBy === columnIdx}
|
||||
direction={orderBy === columnIdx ? order : "asc"}
|
||||
onClick={createSortHandler(columnIdx)}
|
||||
>
|
||||
{column.label}
|
||||
{orderBy === column.field ? (
|
||||
<HiddenSpan>
|
||||
{order === "desc"
|
||||
? "sorted descending"
|
||||
: "sorted ascending"}
|
||||
</HiddenSpan>
|
||||
) : null}
|
||||
</TableSortLabel>
|
||||
) : (
|
||||
column.label
|
||||
)}
|
||||
{column.filterable ? (
|
||||
<IconButton
|
||||
size={dense ? "small" : "medium"}
|
||||
style={
|
||||
fieldAlreadyFiltered(columnIdx)
|
||||
? {}
|
||||
: { visibility: "hidden" }
|
||||
}
|
||||
color="inherit"
|
||||
onClick={() => {
|
||||
clearFilter(columnIdx)
|
||||
}}
|
||||
>
|
||||
<Clear />
|
||||
</IconButton>
|
||||
) : null}
|
||||
</TableHeaderCellSpan>
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => {
|
||||
if (
|
||||
header.column.getCanFilter() &&
|
||||
!header.column.getIsFiltered()
|
||||
) {
|
||||
header.column.setFilterValue([])
|
||||
}
|
||||
const order = header.column.getIsSorted()
|
||||
const filterChoices = header.column.getCanFilter()
|
||||
? Array.from(
|
||||
header.column.getFacetedUniqueValues().keys()
|
||||
).sort()
|
||||
: null
|
||||
return (
|
||||
<TableCell key={header.id} colSpan={header.colSpan}>
|
||||
{header.isPlaceholder ? null : (
|
||||
<TableHeaderCellSpan>
|
||||
{header.column.getCanSort() ? (
|
||||
<TableSortLabel
|
||||
active={order !== false}
|
||||
direction={order || "asc"}
|
||||
onClick={header.column.getToggleSortingHandler()}
|
||||
>
|
||||
{flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext()
|
||||
)}
|
||||
{order !== null ? (
|
||||
<HiddenSpan>
|
||||
{order === "desc"
|
||||
? "sorted descending"
|
||||
: "sorted ascending"}
|
||||
</HiddenSpan>
|
||||
) : null}
|
||||
</TableSortLabel>
|
||||
) : (
|
||||
flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext()
|
||||
)
|
||||
)}
|
||||
{filterChoices !== null ? (
|
||||
<FilterMenu
|
||||
header={header}
|
||||
filterChoices={filterChoices}
|
||||
/>
|
||||
) : null}
|
||||
</TableHeaderCellSpan>
|
||||
)}
|
||||
</TableCell>
|
||||
)
|
||||
})}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{currentPageRows.map((row) => (
|
||||
<DataGridRow<T>
|
||||
columns={columns}
|
||||
rowIndex={getRowIndex(row)}
|
||||
row={row}
|
||||
keyField={keyField}
|
||||
collapseBody={collapseBody}
|
||||
key={`${row[keyField]}`}
|
||||
handleClickFilterCell={handleClickFilterCell}
|
||||
/>
|
||||
))}
|
||||
{emptyRows > 0 && (
|
||||
<TableRow style={{ height: (dense ? 33 : 53) * emptyRows }}>
|
||||
<TableCell colSpan={6} />
|
||||
</TableRow>
|
||||
)}
|
||||
{table.getRowModel().rows.map((row) => {
|
||||
return (
|
||||
<TableRow key={row.id}>
|
||||
{row.getVisibleCells().map((cell) => {
|
||||
return (
|
||||
<TableCell key={cell.id}>
|
||||
{flexRender(
|
||||
cell.column.columnDef.cell,
|
||||
cell.getContext()
|
||||
)}
|
||||
</TableCell>
|
||||
)
|
||||
})}
|
||||
</TableRow>
|
||||
)
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
<TablePagination
|
||||
rowsPerPageOptions={rowsPerPageOption}
|
||||
component="div"
|
||||
count={filteredRows.length}
|
||||
rowsPerPage={rowsPerPage}
|
||||
page={page}
|
||||
onPageChange={handleChangePage}
|
||||
onRowsPerPageChange={handleChangeRowsPerPage}
|
||||
/>
|
||||
</RootDiv>
|
||||
)
|
||||
}
|
||||
|
||||
function DataGridRow<T>(props: {
|
||||
columns: DataGridColumn<T>[]
|
||||
rowIndex: number
|
||||
row: T
|
||||
keyField: keyof T
|
||||
collapseBody?: (rowIndex: number) => React.ReactNode
|
||||
handleClickFilterCell: (columnIdx: number, value: Value) => void
|
||||
}) {
|
||||
const {
|
||||
columns,
|
||||
rowIndex,
|
||||
row,
|
||||
keyField,
|
||||
collapseBody,
|
||||
handleClickFilterCell,
|
||||
} = props
|
||||
const [open, setOpen] = React.useState(false)
|
||||
const theme = useTheme()
|
||||
|
||||
const FilterableDiv = styled("div")({
|
||||
color: theme.palette.primary.main,
|
||||
textDecoration: "underline",
|
||||
cursor: "pointer",
|
||||
})
|
||||
return (
|
||||
<React.Fragment>
|
||||
<TableRow hover tabIndex={-1}>
|
||||
{collapseBody ? (
|
||||
<TableCell>
|
||||
<IconButton
|
||||
aria-label="expand row"
|
||||
size="small"
|
||||
onClick={() => setOpen(!open)}
|
||||
>
|
||||
{open ? <KeyboardArrowUpIcon /> : <KeyboardArrowDownIcon />}
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
<Box component="div" display="flex" alignItems="center">
|
||||
<TablePagination
|
||||
rowsPerPageOptions={[
|
||||
10,
|
||||
50,
|
||||
100,
|
||||
{ label: "All", value: data.length },
|
||||
]}
|
||||
component="div"
|
||||
count={table.getFilteredRowModel().rows.length}
|
||||
rowsPerPage={table.getState().pagination.pageSize}
|
||||
page={table.getState().pagination.pageIndex}
|
||||
slotProps={{
|
||||
select: {
|
||||
inputProps: { "aria-label": "rows per page" },
|
||||
native: true,
|
||||
},
|
||||
}}
|
||||
onPageChange={(_, page) => {
|
||||
table.setPageIndex(page)
|
||||
}}
|
||||
onRowsPerPageChange={(e) => {
|
||||
const size = e.target.value ? Number(e.target.value) : 10
|
||||
table.setPageSize(size)
|
||||
}}
|
||||
ActionsComponent={TablePaginationActions}
|
||||
/>
|
||||
{table.getPageCount() > 2 ? (
|
||||
<PaginationForm1
|
||||
onPageNumberSubmit={(page) => table.setPageIndex(page)}
|
||||
maxPageNumber={table.getPageCount()}
|
||||
/>
|
||||
) : null}
|
||||
{columns.map((column, columnIndex) => {
|
||||
const cellItem = column.toCellValue
|
||||
? column.toCellValue(rowIndex)
|
||||
: // TODO(c-bata): Avoid this implicit type conversion.
|
||||
(row[column.field] as number | string | null | undefined)
|
||||
|
||||
return column.filterable ? (
|
||||
<TableCell
|
||||
key={`${row[keyField]}:${column.field.toString()}:${columnIndex}`}
|
||||
padding={column.padding || "normal"}
|
||||
onClick={() => {
|
||||
const value =
|
||||
column.toCellValue !== undefined
|
||||
? column.toCellValue(rowIndex)
|
||||
: row[column.field]
|
||||
handleClickFilterCell(columnIndex, value)
|
||||
}}
|
||||
>
|
||||
<FilterableDiv>{cellItem}</FilterableDiv>
|
||||
</TableCell>
|
||||
) : (
|
||||
<TableCell
|
||||
key={`${row[keyField]}:${column.field.toString()}:${columnIndex}`}
|
||||
padding={column.padding || "normal"}
|
||||
>
|
||||
{cellItem}
|
||||
</TableCell>
|
||||
)
|
||||
})}
|
||||
</TableRow>
|
||||
{collapseBody ? (
|
||||
<TableRow>
|
||||
<TableCell style={{ paddingBottom: 0, paddingTop: 0 }} colSpan={6}>
|
||||
<Collapse in={open} timeout="auto" unmountOnExit>
|
||||
{collapseBody(rowIndex)}
|
||||
</Collapse>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : null}
|
||||
</React.Fragment>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
function getComparator<T>(
|
||||
order: Order,
|
||||
columns: DataGridColumn<T>[],
|
||||
orderBy: number
|
||||
): (a: T, b: T) => number {
|
||||
return order === "desc"
|
||||
? (a, b) => descendingComparator<T>(a, b, columns, orderBy)
|
||||
: (a, b) => -descendingComparator<T>(a, b, columns, orderBy)
|
||||
}
|
||||
|
||||
function descendingComparator<T>(
|
||||
a: T,
|
||||
b: T,
|
||||
columns: DataGridColumn<T>[],
|
||||
orderBy: number
|
||||
): number {
|
||||
const field = columns[orderBy].field
|
||||
if (b[field] < a[field]) {
|
||||
return -1
|
||||
const TablePaginationActions = ({
|
||||
count,
|
||||
page,
|
||||
rowsPerPage,
|
||||
onPageChange,
|
||||
}: TablePaginationActionsProps) => {
|
||||
const theme = useTheme()
|
||||
const handleFirstPageButtonClick = (
|
||||
event: React.MouseEvent<HTMLButtonElement, MouseEvent>
|
||||
) => {
|
||||
onPageChange(event, 0)
|
||||
}
|
||||
if (b[field] > a[field]) {
|
||||
return 1
|
||||
|
||||
const handleBackButtonClick = (
|
||||
event: React.MouseEvent<HTMLButtonElement, MouseEvent>
|
||||
) => {
|
||||
onPageChange(event, page - 1)
|
||||
}
|
||||
return 0
|
||||
|
||||
const handleNextButtonClick = (
|
||||
event: React.MouseEvent<HTMLButtonElement, MouseEvent>
|
||||
) => {
|
||||
onPageChange(event, page + 1)
|
||||
}
|
||||
|
||||
const handleLastPageButtonClick = (
|
||||
event: React.MouseEvent<HTMLButtonElement, MouseEvent>
|
||||
) => {
|
||||
onPageChange(event, Math.max(0, Math.ceil(count / rowsPerPage) - 1))
|
||||
}
|
||||
|
||||
return (
|
||||
<Box component="div" sx={{ flexShrink: 0, ml: 2.5 }}>
|
||||
<IconButton
|
||||
onClick={handleFirstPageButtonClick}
|
||||
disabled={page === 0}
|
||||
aria-label="first page"
|
||||
>
|
||||
{theme.direction === "rtl" ? <LastPageIcon /> : <FirstPageIcon />}
|
||||
</IconButton>
|
||||
<IconButton
|
||||
onClick={handleBackButtonClick}
|
||||
disabled={page === 0}
|
||||
aria-label="previous page"
|
||||
>
|
||||
{theme.direction === "rtl" ? (
|
||||
<KeyboardArrowRight />
|
||||
) : (
|
||||
<KeyboardArrowLeft />
|
||||
)}
|
||||
</IconButton>
|
||||
<IconButton
|
||||
onClick={handleNextButtonClick}
|
||||
disabled={page >= Math.ceil(count / rowsPerPage) - 1}
|
||||
aria-label="next page"
|
||||
>
|
||||
{theme.direction === "rtl" ? (
|
||||
<KeyboardArrowLeft />
|
||||
) : (
|
||||
<KeyboardArrowRight />
|
||||
)}
|
||||
</IconButton>
|
||||
<IconButton
|
||||
onClick={handleLastPageButtonClick}
|
||||
disabled={page >= Math.ceil(count / rowsPerPage) - 1}
|
||||
aria-label="last page"
|
||||
>
|
||||
{theme.direction === "rtl" ? <FirstPageIcon /> : <LastPageIcon />}
|
||||
</IconButton>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
function stableSort<T>(
|
||||
array: T[],
|
||||
order: Order,
|
||||
orderBy: number,
|
||||
columns: DataGridColumn<T>[]
|
||||
) {
|
||||
// TODO(c-bata): Refactor here by implementing as the same comparator interface.
|
||||
const less = columns[orderBy].less
|
||||
const comparator = getComparator(order, columns, orderBy)
|
||||
const stabilizedThis = array.map((el, index) => [el, index] as [T, number])
|
||||
stabilizedThis.sort((a, b) => {
|
||||
if (less) {
|
||||
const ascending = order === "asc"
|
||||
const result = ascending
|
||||
? -less(a[0], b[0], ascending)
|
||||
: less(a[0], b[0], ascending)
|
||||
if (result !== 0) return result
|
||||
} else {
|
||||
const result = comparator(a[0], b[0])
|
||||
if (result !== 0) return result
|
||||
}
|
||||
return a[1] - b[1]
|
||||
})
|
||||
return stabilizedThis.map((el) => el[0])
|
||||
}
|
||||
const PaginationForm1: React.FC<{
|
||||
onPageNumberSubmit: (value: number) => void
|
||||
maxPageNumber: number
|
||||
}> = ({ onPageNumberSubmit, maxPageNumber }) => {
|
||||
// This component is separated from DataGrid to prevent `DataGrid` from re-rendering the page,
|
||||
// every time any letters are input.
|
||||
const [specifiedPageText, setSpecifiedPageText] = React.useState("")
|
||||
|
||||
const isNumber = (
|
||||
rowsPerPage: number | { value: number; label: string }
|
||||
): rowsPerPage is number => {
|
||||
return typeof rowsPerPage === "number"
|
||||
const handleSubmitPageNumber = (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault()
|
||||
const newPageNumber = parseInt(specifiedPageText, 10)
|
||||
// Page is 0-indexed in `TablePagination`.
|
||||
onPageNumberSubmit(newPageNumber - 1)
|
||||
setSpecifiedPageText("") // reset the input field
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmitPageNumber}>
|
||||
<TextField
|
||||
size="small"
|
||||
label={`Go to Page: n / ${maxPageNumber}`}
|
||||
value={specifiedPageText}
|
||||
type="number"
|
||||
style={{ width: 200 }}
|
||||
inputProps={{ min: 1, max: maxPageNumber }}
|
||||
onChange={(e) => {
|
||||
setSpecifiedPageText(e.target.value)
|
||||
}}
|
||||
/>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
export { DataGrid }
|
||||
export type { DataGridColumn }
|
||||
|
||||
Reference in New Issue
Block a user