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({