Merge pull request #877 from keisuke-umezawa/feature/table-viewer

Add TableArtifactViewer for csv file
This commit is contained in:
c-bata
2024-06-07 14:04:09 +09:00
committed by GitHub
5 changed files with 175 additions and 10 deletions
+16 -3
View File
@@ -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",
+2
View File
@@ -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"))
},
})
})
}
+9 -7
View File
@@ -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}