diff --git a/optuna_dashboard/package-lock.json b/optuna_dashboard/package-lock.json index e4d44204..e7ee060d 100644 --- a/optuna_dashboard/package-lock.json +++ b/optuna_dashboard/package-lock.json @@ -19,10 +19,12 @@ "@tanstack/react-query": "^5.18.1", "@tanstack/react-table": "^8.16.0", "@tanstack/react-virtual": "^3.1.2", + "@types/papaparse": "^5.3.14", "@types/three": "^0.160.0", "axios": "^1.6.7", "elkjs": "^0.9.1", "notistack": "^3.0.1", + "papaparse": "^5.4.1", "plotly.js-dist-min": "^2.28.0", "react": "^18.2.0", "react-dom": "^18.2.0", @@ -3496,7 +3498,6 @@ "version": "20.12.7", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.7.tgz", "integrity": "sha512-wq0cICSkRLVaf3UGLMGItu/PtdY7oaXaI/RVU+xliKVOtRna3PRY57ZDfztpDL0n11vfymMUnXv8QwYCO7L1wg==", - "dev": true, "dependencies": { "undici-types": "~5.26.4" } @@ -3506,6 +3507,14 @@ "resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.7.3.tgz", "integrity": "sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A==" }, + "node_modules/@types/papaparse": { + "version": "5.3.14", + "resolved": "https://registry.npmjs.org/@types/papaparse/-/papaparse-5.3.14.tgz", + "integrity": "sha512-LxJ4iEFcpqc6METwp9f6BV6VVc43m6MfH0VqFosHvrUgfXiFe6ww7R3itkOQ+TCK6Y+Iv/+RnnvtRZnkc5Kc9g==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/parse-json": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", @@ -6939,6 +6948,11 @@ "node": ">=6" } }, + "node_modules/papaparse": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.4.1.tgz", + "integrity": "sha512-HipMsgJkZu8br23pW15uvo6sib6wne/4woLZPlFf3rpDyMe9ywEXUsuD7+6K9PRkJlVT51j/sCOYDKGGS3ZJrw==" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -8523,8 +8537,7 @@ "node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "dev": true + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" }, "node_modules/unicode-canonical-property-names-ecmascript": { "version": "2.0.0", diff --git a/optuna_dashboard/package.json b/optuna_dashboard/package.json index b81887c1..c0a3d271 100644 --- a/optuna_dashboard/package.json +++ b/optuna_dashboard/package.json @@ -24,10 +24,12 @@ "@tanstack/react-query": "^5.18.1", "@tanstack/react-table": "^8.16.0", "@tanstack/react-virtual": "^3.1.2", + "@types/papaparse": "^5.3.14", "@types/three": "^0.160.0", "axios": "^1.6.7", "elkjs": "^0.9.1", "notistack": "^3.0.1", + "papaparse": "^5.4.1", "plotly.js-dist-min": "^2.28.0", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/optuna_dashboard/ts/components/Artifact/StudyArtifactCards.tsx b/optuna_dashboard/ts/components/Artifact/StudyArtifactCards.tsx index fc749b5e..9fd12cf4 100644 --- a/optuna_dashboard/ts/components/Artifact/StudyArtifactCards.tsx +++ b/optuna_dashboard/ts/components/Artifact/StudyArtifactCards.tsx @@ -24,6 +24,7 @@ import { StudyDetail } from "ts/types/optuna" import { actionCreator } from "../../action" import { ArtifactCardMedia } from "./ArtifactCardMedia" import { useDeleteStudyArtifactDialog } from "./DeleteArtifactDialog" +import { isTableArtifact, useTableArtifactModal } from "./TableArtifactViewer" import { isThreejsArtifact, useThreejsArtifactModal, @@ -35,6 +36,8 @@ export const StudyArtifactCards: FC<{ study: StudyDetail }> = ({ study }) => { useDeleteStudyArtifactDialog() const [openThreejsArtifactModal, renderThreejsArtifactModal] = useThreejsArtifactModal() + const [openTableArtifactModal, renderTableArtifactModal] = + useTableArtifactModal() const width = "200px" const height = "150px" @@ -96,6 +99,19 @@ export const StudyArtifactCards: FC<{ study: StudyDetail }> = ({ study }) => { ) : null} + {isTableArtifact(artifact) ? ( + { + openTableArtifactModal(urlPath, artifact) + }} + > + + + ) : null} = ({ study }) => { {renderDeleteArtifactDialog()} {renderThreejsArtifactModal()} + {renderTableArtifactModal()} ) } diff --git a/optuna_dashboard/ts/components/Artifact/TableArtifactViewer.tsx b/optuna_dashboard/ts/components/Artifact/TableArtifactViewer.tsx new file mode 100644 index 00000000..c0338259 --- /dev/null +++ b/optuna_dashboard/ts/components/Artifact/TableArtifactViewer.tsx @@ -0,0 +1,131 @@ +import ClearIcon from "@mui/icons-material/Clear" +import { Box, Modal, useTheme } from "@mui/material" +import IconButton from "@mui/material/IconButton" +import { useSnackbar } from "notistack" +import Papa from "papaparse" +import React, { useState, useEffect, ReactNode } from "react" +import { DataGrid } from "../DataGrid" + +import { Artifact } from "ts/types/optuna" + +export const isTableArtifact = (artifact: Artifact): boolean => { + return artifact.filename.endsWith(".csv") +} + +interface TableArtifactViewerProps { + src: string + filetype: string | undefined +} + +type Data = { + [key: string]: string | number +} + +export const TableArtifactViewer: React.FC = ( + props +) => { + const [data, setData] = useState([]) + const { enqueueSnackbar } = useSnackbar() + + useEffect(() => { + const handleFileChange = async () => { + try { + const loadedData = await loadCSV(props) + setData(loadedData) + } catch (error: unknown) { + enqueueSnackbar("Failed to load the csv file.", { + variant: "error", + }) + } + } + handleFileChange() + }, [props]) + + const columns = React.useMemo(() => { + const keys = data[0] ? Object.keys(data[0]) : [] + return keys.map((key) => ({ + header: key, + accessorKey: key, + enableSorting: true, + enableColumnFilter: false, + })) + }, [data]) + + return +} + +export const useTableArtifactModal = (): [ + (path: string, artifact: Artifact) => void, + () => ReactNode, +] => { + const [open, setOpen] = useState(false) + const [target, setTarget] = useState<[string, Artifact | null]>(["", null]) + const theme = useTheme() + + const openModal = (artifactUrlPath: string, artifact: Artifact) => { + setTarget([artifactUrlPath, artifact]) + setOpen(true) + } + + const renderDeleteStudyDialog = () => { + return ( + { + setOpen(false) + setTarget(["", null]) + }} + > + + { + setOpen(false) + setTarget(["", null]) + }} + > + + + + + + ) + } + return [openModal, renderDeleteStudyDialog] +} + +const loadCSV = (props: TableArtifactViewerProps): Promise => { + return new Promise((resolve, reject) => { + Papa.parse(props.src, { + header: true, + download: true, + complete: (results: Papa.ParseResult) => { + resolve(results?.data) + }, + error: () => { + reject(new Error("csv parse err")) + }, + }) + }) +} diff --git a/optuna_dashboard/ts/components/DataGrid.tsx b/optuna_dashboard/ts/components/DataGrid.tsx index b268ee81..607f580c 100644 --- a/optuna_dashboard/ts/components/DataGrid.tsx +++ b/optuna_dashboard/ts/components/DataGrid.tsx @@ -119,17 +119,24 @@ function FilterMenu({ function DataGrid({ data, columns, + initialRowsPerPage, }: { data: T[] columns: ColumnDef[] + initialRowsPerPage?: number }): React.ReactElement { const [sorting, setSorting] = React.useState([]) const [columnFilters, setColumnFilters] = React.useState( [] ) + const rowsPerPageOptions = [10, 50, 100, { label: "All", value: data.length }] + const [pagination, setPagination] = React.useState({ pageIndex: 0, - pageSize: 50, + pageSize: + initialRowsPerPage && rowsPerPageOptions.includes(initialRowsPerPage) + ? initialRowsPerPage + : 50, }) const table = useReactTable({ @@ -236,12 +243,7 @@ function DataGrid({