Delete unnecessary code

This commit is contained in:
keisuke-umezawa
2024-05-02 15:11:57 +09:00
parent f8d245cb61
commit dbc7264c2c
2 changed files with 8 additions and 554 deletions
+5 -418
View File
@@ -18,13 +18,16 @@ import {
TableRow,
TableSortLabel,
TextField,
Collapse,
IconButton,
Menu,
MenuItem,
Box,
useTheme,
} from "@mui/material"
import { styled } from "@mui/system"
import CheckBoxOutlineBlankIcon from "@mui/icons-material/CheckBoxOutlineBlank"
import CheckBoxIcon from "@mui/icons-material/CheckBox"
import FilterListIcon from "@mui/icons-material/FilterList"
import ListItemIcon from "@mui/material/ListItemIcon"
import { styled } from "@mui/system"
import React from "react"
@@ -50,224 +53,6 @@ import {
useReactTable,
} from "@tanstack/react-table"
type Order = "asc" | "desc"
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type Value = any
const defaultRowsPerPageOption = [10, 50, 100, { label: "All", value: -1 }]
interface DataGridColumn<T> {
field: keyof T
label: string
sortable?: boolean
less?: (a: T, b: T, ascending: boolean) => number
filterChoices?: (string | null)[]
toCellValue?: (rowIndex: number) => string | React.ReactNode
padding?: "normal" | "checkbox" | "none"
}
interface RowFilter {
columnIdx: number
values: 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
}): 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)
}
const PaginationForm: 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 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>
)
}
// Filtering
const filteredRows = rows.filter((row, rowIdx) => {
if (defaultFilter !== undefined && defaultFilter(row)) {
return false
}
return filters.length === 0
? true
: filters.every((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
const cellValue =
toCellValue !== undefined
? toCellValue(rowIdx)
: row[columns[f.columnIdx].field]
return f.values.some((v) => v === cellValue)
})
})
// Sorting
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 maxPageNumber = Math.ceil(filteredRows.length / rowsPerPage)
return (
<RootDiv>
<TableContainer>
<Table
aria-labelledby="tableTitle"
size={dense ? "small" : "medium"}
aria-label="data grid"
>
<TableHead>
<TableRow>
{collapseBody ? <TableCell /> : null}
{columns.map((column, columnIdx) => {
return (
<DataGridHeaderColumn<T>
key={columnIdx}
column={column}
order={orderBy === columnIdx ? order : null}
filter={
filters.find((f) => f.columnIdx === columnIdx) || null
}
onOrderByChange={(direction: Order) => {
setOrder(direction)
setOrderBy(columnIdx)
}}
onFilterChange={(values: Value[]) => {
const newFilters = filters.filter(
(f) => f.columnIdx !== columnIdx
)
newFilters.push({
columnIdx: columnIdx,
values: values,
})
setFilters(newFilters)
}}
/>
)
})}
</TableRow>
</TableHead>
<TableBody>
{currentPageRows.map((row) => (
<DataGridRow<T>
columns={columns}
rowIndex={getRowIndex(row)}
row={row}
keyField={keyField}
collapseBody={collapseBody}
key={`${row[keyField]}`}
/>
))}
{emptyRows > 0 && (
<TableRow style={{ height: (dense ? 33 : 53) * emptyRows }}>
<TableCell colSpan={6} />
</TableRow>
)}
</TableBody>
</Table>
</TableContainer>
{filteredRows.length > 0 ? (
<>
{/* @ts-ignore */}
<Box display="flex" alignItems="center">
<TablePagination
rowsPerPageOptions={rowsPerPageOption}
component="div"
count={filteredRows.length}
rowsPerPage={rowsPerPage}
page={page}
onPageChange={handleChangePage}
onRowsPerPageChange={handleChangeRowsPerPage}
/>
{maxPageNumber > 2 ? (
<PaginationForm
onPageNumberSubmit={(page) => setPage(page)}
maxPageNumber={maxPageNumber}
/>
) : null}
</Box>
</>
) : null}
</RootDiv>
)
}
const TableHeaderCellSpan = styled("span")({
display: "inline-flex",
})
@@ -284,204 +69,6 @@ const HiddenSpan = styled("span")({
width: 1,
})
function DataGridHeaderColumn<T>(props: {
column: DataGridColumn<T>
order: Order | null
onOrderByChange: (order: Order) => void
filter: RowFilter | null
onFilterChange: (values: Value[]) => void
dense?: boolean
}) {
const { column, order, onOrderByChange, filter, onFilterChange, dense } =
props
const [filterMenuAnchorEl, setFilterMenuAnchorEl] =
React.useState<null | HTMLElement>(null)
const filterChoices = column.filterChoices
return (
<TableCell
padding={column.padding || "normal"}
sortDirection={order !== null ? order : false}
>
<TableHeaderCellSpan>
{column.sortable ? (
<TableSortLabel
active={order !== null}
direction={order || "asc"}
onClick={() => {
onOrderByChange(order === "asc" ? "desc" : "asc")
}}
>
{column.label}
{order !== null ? (
<HiddenSpan>
{order === "desc" ? "sorted descending" : "sorted ascending"}
</HiddenSpan>
) : null}
</TableSortLabel>
) : (
column.label
)}
{filterChoices !== undefined ? (
<>
<IconButton
size={dense ? "small" : "medium"}
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 newTickedValues =
filter === null
? filterChoices.filter((v) => v !== choice) // By default, every choice is ticked, so the chosen option will be unticked.
: filter.values.some((v) => v === choice)
? filter.values.filter((v) => v !== choice)
: [...filter.values, choice]
onFilterChange(newTickedValues)
}}
>
<ListItemIcon>
{!filter || filter.values.some((v) => v === choice) ? (
<CheckBoxIcon color="primary" />
) : (
<CheckBoxOutlineBlankIcon color="primary" />
)}
</ListItemIcon>
{choice ?? "(missing value)"}
</MenuItem>
))}
</Menu>
</>
) : null}
</TableHeaderCellSpan>
</TableCell>
)
}
function DataGridRow<T>(props: {
columns: DataGridColumn<T>[]
rowIndex: number
row: T
keyField: keyof T
collapseBody?: (rowIndex: number) => React.ReactNode
}) {
const { columns, rowIndex, row, keyField, collapseBody } = props
const [open, setOpen] = React.useState(false)
return (
<React.Fragment>
<TableRow hover tabIndex={-1}>
{collapseBody ? (
<TableCell>
<IconButton
aria-label="expand row"
size="small"
onClick={() => setOpen(!open)}
>
{open ? <KeyboardArrowUpIcon /> : <KeyboardArrowDownIcon />}
</IconButton>
</TableCell>
) : 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 (
<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>
)
}
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
}
if (b[field] > a[field]) {
return 1
}
return 0
}
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 isNumber = (
rowsPerPage: number | { value: number; label: string }
): rowsPerPage is number => {
return typeof rowsPerPage === "number"
}
function DataGrid2<T>(props: {
data: T[]
columns: ColumnDef<T>[]
@@ -790,4 +377,4 @@ const PaginationForm1: React.FC<{
)
}
export { DataGrid, DataGrid2, DataGridColumn }
export { DataGrid2 }
+3 -136
View File
@@ -6,7 +6,7 @@ import LinkIcon from "@mui/icons-material/Link"
import { Button, IconButton, useTheme } from "@mui/material"
import React, { FC } from "react"
import { DataGridColumn, DataGrid, DataGrid2 } from "./DataGrid"
import { DataGrid2 } from "./DataGrid"
import { Link } from "react-router-dom"
import { StudyDetail, Trial } from "ts/types/optuna"
import { DataGrid, DataGridColumn } from "./DataGrid"
@@ -34,19 +34,7 @@ export const TrialTable: FC<{
}> = ({ studyDetail, initialRowsPerPage }) => {
const theme = useTheme()
const trials: Trial[] = studyDetail !== null ? studyDetail.trials : []
const objectiveNames: string[] = studyDetail?.objective_names || []
const columns: DataGridColumn<Trial>[] = [
{ field: "number", label: "Number", sortable: true, padding: "none" },
{
field: "state",
label: "State",
sortable: true,
filterChoices: ["Complete", "Pruned", "Fail", "Running", "Waiting"],
padding: "none",
toCellValue: (i) => trials[i].state.toString(),
},
]
// TODO: const objectiveNames: string[] = studyDetail?.objective_names || []
const columnHelper = createColumnHelper<Trial>()
const tcolumns: ColumnDef<Trial>[] = [
@@ -63,40 +51,7 @@ export const TrialTable: FC<{
filterFn: multiValueFilter,
}),
]
const valueComparator = (
firstVal?: number,
secondVal?: number,
ascending = true
): number => {
if (firstVal === secondVal) {
return 0
}
if (firstVal === undefined) {
return ascending ? -1 : 1
} else if (secondVal === undefined) {
return ascending ? 1 : -1
}
return firstVal < secondVal ? 1 : -1
}
if (studyDetail === null || studyDetail.directions.length === 1) {
columns.push({
field: "values",
label: "Value",
sortable: true,
less: (firstEl, secondEl, ascending): number => {
return valueComparator(
firstEl.values?.[0],
secondEl.values?.[0],
ascending
)
},
toCellValue: (i) => {
if (trials[i].values === undefined) {
return null
}
return trials[i].values?.[0]
},
})
tcolumns.push(
columnHelper.accessor("values", {
header: "Value",
@@ -107,29 +62,6 @@ export const TrialTable: FC<{
})
)
} else {
const objectiveColumns: DataGridColumn<Trial>[] =
studyDetail.directions.map((s, objectiveId) => ({
field: "values",
label:
objectiveNames.length === studyDetail?.directions.length
? objectiveNames[objectiveId]
: `Objective ${objectiveId}`,
sortable: true,
less: (firstEl, secondEl, ascending): number => {
return valueComparator(
firstEl.values?.[objectiveId],
secondEl.values?.[objectiveId],
ascending
)
},
toCellValue: (i) => {
if (trials[i].values === undefined) {
return null
}
return trials[i].values?.[objectiveId]
},
}))
columns.push(...objectiveColumns)
tcolumns.push(
...studyDetail.directions.map((s, objectiveId) =>
columnHelper.accessor((row) => row["values"]?.[objectiveId], {
@@ -158,25 +90,6 @@ export const TrialTable: FC<{
if (filterChoices !== undefined && isDynamicSpace && hasMissingValue) {
filterChoices.push(null)
}
columns.push({
field: "params",
label: `Param ${s.name}`,
toCellValue: (i) =>
trials[i].params.find((p) => p.name === s.name)?.param_external_value ||
null,
sortable: sortable,
filterChoices: filterChoices,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
less: (firstEl, secondEl, _): number => {
const firstVal = firstEl.params.find(
(p) => p.name === s.name
)?.param_internal_value
const secondVal = secondEl.params.find(
(p) => p.name === s.name
)?.param_internal_value
return valueComparator(firstVal, secondVal)
},
})
tcolumns.push(
columnHelper.accessor(
(row) =>
@@ -196,27 +109,6 @@ export const TrialTable: FC<{
})
studyDetail?.union_user_attrs.forEach((attr_spec) => {
columns.push({
field: "user_attrs",
label: `UserAttribute ${attr_spec.key}`,
toCellValue: (i) =>
trials[i].user_attrs.find((attr) => attr.key === attr_spec.key)
?.value || null,
sortable: attr_spec.sortable,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
less: (firstEl, secondEl, _): number => {
const firstValString = firstEl.user_attrs.find(
(attr) => attr.key === attr_spec.key
)?.value
const secondValString = secondEl.user_attrs.find(
(attr) => attr.key === attr_spec.key
)?.value
return valueComparator(
Number(firstValString) ?? firstValString,
Number(secondValString) ?? secondValString
)
},
})
tcolumns.push(
columnHelper.accessor(
(row) =>
@@ -232,24 +124,6 @@ export const TrialTable: FC<{
)
)
})
columns.push({
field: "trial_id",
label: "Detail",
toCellValue: (i) => (
<IconButton
component={Link}
to={
URL_PREFIX +
`/studies/${trials[i].study_id}/trials?numbers=${trials[i].number}`
}
color="inherit"
title="Go to the trial's detail page"
size="small"
>
<LinkIcon />
</IconButton>
),
})
tcolumns.push(
columnHelper.accessor((row) => row, {
header: "Detail",
@@ -277,13 +151,7 @@ export const TrialTable: FC<{
return (
<>
<DataGrid<Trial>
columns={columns}
rows={trials}
keyField={"trial_id"}
dense={true}
initialRowsPerPage={initialRowsPerPage}
/>
<DataGrid2 data={trials} columns={tcolumns} />
<Button
variant="outlined"
startIcon={<DownloadIcon />}
@@ -293,7 +161,6 @@ export const TrialTable: FC<{
>
Download CSV File
</Button>
<DataGrid2 data={trials} columns={tcolumns} />
</>
)
}