mirror of
https://github.com/wassname/optuna-dashboard.git
synced 2026-09-10 12:23:22 +08:00
Merge pull request #877 from keisuke-umezawa/feature/table-viewer
Add TableArtifactViewer for csv file
This commit is contained in:
Generated
+16
-3
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 }) => {
|
||||
<FullscreenIcon />
|
||||
</IconButton>
|
||||
) : null}
|
||||
{isTableArtifact(artifact) ? (
|
||||
<IconButton
|
||||
aria-label="show artifact table"
|
||||
size="small"
|
||||
color="inherit"
|
||||
sx={{ margin: "auto 0" }}
|
||||
onClick={() => {
|
||||
openTableArtifactModal(urlPath, artifact)
|
||||
}}
|
||||
>
|
||||
<FullscreenIcon />
|
||||
</IconButton>
|
||||
) : null}
|
||||
<IconButton
|
||||
aria-label="delete artifact"
|
||||
size="small"
|
||||
@@ -125,6 +141,7 @@ export const StudyArtifactCards: FC<{ study: StudyDetail }> = ({ study }) => {
|
||||
</Box>
|
||||
{renderDeleteArtifactDialog()}
|
||||
{renderThreejsArtifactModal()}
|
||||
{renderTableArtifactModal()}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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<TableArtifactViewerProps> = (
|
||||
props
|
||||
) => {
|
||||
const [data, setData] = useState<Data[]>([])
|
||||
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 <DataGrid data={data} columns={columns} initialRowsPerPage={10} />
|
||||
}
|
||||
|
||||
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 (
|
||||
<Modal
|
||||
open={open}
|
||||
onClose={() => {
|
||||
setOpen(false)
|
||||
setTarget(["", null])
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
component="div"
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
bgcolor: "background.paper",
|
||||
borderRadius: "15px",
|
||||
width: "80%",
|
||||
maxHeight: "80%",
|
||||
overflowY: "auto",
|
||||
p: 2,
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: theme.spacing(2),
|
||||
right: theme.spacing(2),
|
||||
}}
|
||||
onClick={() => {
|
||||
setOpen(false)
|
||||
setTarget(["", null])
|
||||
}}
|
||||
>
|
||||
<ClearIcon />
|
||||
</IconButton>
|
||||
<TableArtifactViewer
|
||||
src={target[0]}
|
||||
filetype={target[1]?.filename.split(".").pop()}
|
||||
/>
|
||||
</Box>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
return [openModal, renderDeleteStudyDialog]
|
||||
}
|
||||
|
||||
const loadCSV = (props: TableArtifactViewerProps): Promise<Data[]> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
Papa.parse(props.src, {
|
||||
header: true,
|
||||
download: true,
|
||||
complete: (results: Papa.ParseResult<Data>) => {
|
||||
resolve(results?.data)
|
||||
},
|
||||
error: () => {
|
||||
reject(new Error("csv parse err"))
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -119,17 +119,24 @@ function FilterMenu<T>({
|
||||
function DataGrid<T>({
|
||||
data,
|
||||
columns,
|
||||
initialRowsPerPage,
|
||||
}: {
|
||||
data: T[]
|
||||
columns: ColumnDef<T>[]
|
||||
initialRowsPerPage?: number
|
||||
}): React.ReactElement {
|
||||
const [sorting, setSorting] = React.useState<SortingState>([])
|
||||
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
|
||||
[]
|
||||
)
|
||||
const rowsPerPageOptions = [10, 50, 100, { label: "All", value: data.length }]
|
||||
|
||||
const [pagination, setPagination] = React.useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: 50,
|
||||
pageSize:
|
||||
initialRowsPerPage && rowsPerPageOptions.includes(initialRowsPerPage)
|
||||
? initialRowsPerPage
|
||||
: 50,
|
||||
})
|
||||
|
||||
const table = useReactTable({
|
||||
@@ -236,12 +243,7 @@ function DataGrid<T>({
|
||||
</TableContainer>
|
||||
<Box component="div" display="flex" alignItems="center">
|
||||
<TablePagination
|
||||
rowsPerPageOptions={[
|
||||
10,
|
||||
50,
|
||||
100,
|
||||
{ label: "All", value: data.length },
|
||||
]}
|
||||
rowsPerPageOptions={rowsPerPageOptions}
|
||||
component="div"
|
||||
count={table.getFilteredRowModel().rows.length}
|
||||
rowsPerPage={table.getState().pagination.pageSize}
|
||||
|
||||
Reference in New Issue
Block a user