diff --git a/optuna_dashboard/ts/components/DataGrid.tsx b/optuna_dashboard/ts/components/DataGrid.tsx index 8346a0ad..d64cc1cb 100644 --- a/optuna_dashboard/ts/components/DataGrid.tsx +++ b/optuna_dashboard/ts/components/DataGrid.tsx @@ -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 { - 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(props: { - columns: DataGridColumn[] - rows: T[] - keyField: keyof T - dense?: boolean - collapseBody?: (rowIndex: number) => React.ReactNode - initialRowsPerPage?: number - rowsPerPageOption?: Array - defaultFilter?: (row: T) => boolean -}): React.ReactElement { - const { columns, rows, keyField, dense, collapseBody, defaultFilter } = props - let { initialRowsPerPage, rowsPerPageOption } = props - const [order, setOrder] = React.useState("asc") - const [orderBy, setOrderBy] = React.useState(0) // index of columns - const [page, setPage] = React.useState(0) - const [filters, setFilters] = React.useState([]) - - 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 - ) => { - 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 - ) => { - event.preventDefault() - const newPageNumber = parseInt(specifiedPageText, 10) - // Page is 0-indexed in `TablePagination`. - onPageNumberSubmit(newPageNumber - 1) - setSpecifiedPageText("") // reset the input field - } - - return ( -
- { - setSpecifiedPageText(e.target.value) - }} - /> - - ) - } - - // 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(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 ( - - - - - - {collapseBody ? : null} - {columns.map((column, columnIdx) => { - return ( - - 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) - }} - /> - ) - })} - - - - {currentPageRows.map((row) => ( - - columns={columns} - rowIndex={getRowIndex(row)} - row={row} - keyField={keyField} - collapseBody={collapseBody} - key={`${row[keyField]}`} - /> - ))} - {emptyRows > 0 && ( - - - - )} - -
-
- {filteredRows.length > 0 ? ( - <> - {/* @ts-ignore */} - - - {maxPageNumber > 2 ? ( - setPage(page)} - maxPageNumber={maxPageNumber} - /> - ) : null} - - - ) : null} -
- ) -} - const TableHeaderCellSpan = styled("span")({ display: "inline-flex", }) @@ -284,204 +69,6 @@ const HiddenSpan = styled("span")({ width: 1, }) -function DataGridHeaderColumn(props: { - column: DataGridColumn - 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) - - const filterChoices = column.filterChoices - - return ( - - - {column.sortable ? ( - { - onOrderByChange(order === "asc" ? "desc" : "asc") - }} - > - {column.label} - {order !== null ? ( - - {order === "desc" ? "sorted descending" : "sorted ascending"} - - ) : null} - - ) : ( - column.label - )} - {filterChoices !== undefined ? ( - <> - { - setFilterMenuAnchorEl(e.currentTarget) - }} - > - - - { - setFilterMenuAnchorEl(null) - }} - > - {filterChoices.map((choice) => ( - { - 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) - }} - > - - {!filter || filter.values.some((v) => v === choice) ? ( - - ) : ( - - )} - - {choice ?? "(missing value)"} - - ))} - - - ) : null} - - - ) -} - -function DataGridRow(props: { - columns: DataGridColumn[] - 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 ( - - - {collapseBody ? ( - - setOpen(!open)} - > - {open ? : } - - - ) : 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 ( - - {cellItem} - - ) - })} - - {collapseBody ? ( - - - - {collapseBody(rowIndex)} - - - - ) : null} - - ) -} - -function getComparator( - order: Order, - columns: DataGridColumn[], - orderBy: number -): (a: T, b: T) => number { - return order === "desc" - ? (a, b) => descendingComparator(a, b, columns, orderBy) - : (a, b) => -descendingComparator(a, b, columns, orderBy) -} - -function descendingComparator( - a: T, - b: T, - columns: DataGridColumn[], - 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( - array: T[], - order: Order, - orderBy: number, - columns: DataGridColumn[] -) { - // 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(props: { data: T[] columns: ColumnDef[] @@ -790,4 +377,4 @@ const PaginationForm1: React.FC<{ ) } -export { DataGrid, DataGrid2, DataGridColumn } +export { DataGrid2 } diff --git a/optuna_dashboard/ts/components/TrialTable.tsx b/optuna_dashboard/ts/components/TrialTable.tsx index 47698ddc..93177997 100644 --- a/optuna_dashboard/ts/components/TrialTable.tsx +++ b/optuna_dashboard/ts/components/TrialTable.tsx @@ -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[] = [ - { 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() const tcolumns: ColumnDef[] = [ @@ -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[] = - 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) => ( - - - - ), - }) tcolumns.push( columnHelper.accessor((row) => row, { header: "Detail", @@ -277,13 +151,7 @@ export const TrialTable: FC<{ return ( <> - - columns={columns} - rows={trials} - keyField={"trial_id"} - dense={true} - initialRowsPerPage={initialRowsPerPage} - /> + - ) }