diff --git a/optuna_dashboard/ts/components/Artifact/TrialArtifactCards.tsx b/optuna_dashboard/ts/components/Artifact/ArtifactCards.tsx similarity index 63% rename from optuna_dashboard/ts/components/Artifact/TrialArtifactCards.tsx rename to optuna_dashboard/ts/components/Artifact/ArtifactCards.tsx index 7bd29c307..4f3127c5c 100644 --- a/optuna_dashboard/ts/components/Artifact/TrialArtifactCards.tsx +++ b/optuna_dashboard/ts/components/Artifact/ArtifactCards.tsx @@ -19,32 +19,44 @@ import React, { useRef, useState, } from "react" - -import { Trial } from "ts/types/optuna" import { actionCreator } from "../../action" +import { StudyDetail, Trial } from "../../types/optuna" import { ArtifactCardMedia } from "./ArtifactCardMedia" -import { useDeleteTrialArtifactDialog } from "./DeleteArtifactDialog" +import { useDeleteArtifactDialog } from "./DeleteArtifactDialog" import { isTableArtifact, useTableArtifactModal } from "./TableArtifactViewer" import { isThreejsArtifact, useThreejsArtifactModal, } from "./ThreejsArtifactViewer" -export const TrialArtifactCards: FC<{ trial: Trial }> = ({ trial }) => { +type StudyOrTrial = + | { + type: "study" + study: StudyDetail + } + | { + type: "trial" + trial: Trial + } +export const ArtifactCards: FC<{ + studyOrTrial: StudyOrTrial + isArtifactModifiable?: boolean +}> = ({ studyOrTrial, isArtifactModifiable = true }) => { const theme = useTheme() const [openDeleteArtifactDialog, renderDeleteArtifactDialog] = - useDeleteTrialArtifactDialog() + useDeleteArtifactDialog() const [openThreejsArtifactModal, renderThreejsArtifactModal] = useThreejsArtifactModal() const [openTableArtifactModal, renderTableArtifactModal] = useTableArtifactModal() - const isArtifactModifiable = (trial: Trial) => { - return trial.state === "Running" || trial.state === "Waiting" - } const width = "200px" const height = "150px" - const artifacts = [...trial.artifacts].sort((a, b) => { + const sortedArtifacts = [ + ...(studyOrTrial.type === "study" + ? studyOrTrial.study.artifacts + : studyOrTrial.trial.artifacts), + ].sort((a, b) => { if (a.filename < b.filename) { return -1 } else if (a.filename > b.filename) { @@ -56,18 +68,15 @@ export const TrialArtifactCards: FC<{ trial: Trial }> = ({ trial }) => { return ( <> - - Artifacts - - {artifacts.map((artifact) => { - const urlPath = `/artifacts/${trial.study_id}/${trial.trial_id}/${artifact.artifact_id}` + {sortedArtifacts.map((artifact) => { + const urlPath = + studyOrTrial.type === "study" + ? `/artifacts/${studyOrTrial.study.id}/${artifact.artifact_id}` + : `/artifacts/${studyOrTrial.trial.study_id}/${studyOrTrial.trial.trial_id}/${artifact.artifact_id}` return ( = ({ trial }) => { marginBottom: theme.spacing(2), width: width, margin: theme.spacing(0, 1, 1, 0), - display: "flex", - flexDirection: "column", - alignItems: "center", + display: studyOrTrial.type === "trial" ? "flex" : undefined, + flexDirection: + studyOrTrial.type === "trial" ? "column" : undefined, + alignItems: + studyOrTrial.type === "trial" ? "center" : undefined, + border: + studyOrTrial.type === "study" + ? `1px solid ${theme.palette.divider}` + : undefined, }} > = ({ trial }) => { sx={{ p: theme.spacing(0.5, 0), flexGrow: 1, - wordBreak: "break-all", - maxWidth: `calc(100% - ${theme.spacing( - 4 + - (isThreejsArtifact(artifact) ? 4 : 0) + - (isArtifactModifiable(trial) ? 4 : 0) - )})`, + wordBreak: + studyOrTrial.type === "trial" ? "break-all" : undefined, + maxWidth: + studyOrTrial.type === "trial" + ? `calc(100% - ${theme.spacing( + 4 + + (isThreejsArtifact(artifact) ? 4 : 0) + + (isArtifactModifiable ? 4 : 0) + )})` + : `calc(100% - ${ + isThreejsArtifact(artifact) + ? theme.spacing(12) + : theme.spacing(8) + })`, + wordWrap: + studyOrTrial.type === "study" ? "break-word" : undefined, }} > {artifact.filename} @@ -132,7 +157,7 @@ export const TrialArtifactCards: FC<{ trial: Trial }> = ({ trial }) => { ) : null} - {isArtifactModifiable(trial) ? ( + {isArtifactModifiable && ( = ({ trial }) => { sx={{ margin: "auto 0" }} onClick={() => { openDeleteArtifactDialog( - trial.study_id, - trial.trial_id, - artifact + studyOrTrial.type === "study" + ? { + type: "study", + studyId: studyOrTrial.study.id, + artifact, + } + : { + type: "trial", + studyId: studyOrTrial.trial.study_id, + trialId: studyOrTrial.trial.trial_id, + artifact, + } ) }} > - ) : null} + )} = ({ trial }) => { ) })} - {isArtifactModifiable(trial) ? ( - - ) : null} + {isArtifactModifiable && ( + + )} {renderDeleteArtifactDialog()} {renderThreejsArtifactModal()} @@ -174,14 +212,14 @@ export const TrialArtifactCards: FC<{ trial: Trial }> = ({ trial }) => { ) } -const TrialArtifactUploader: FC<{ - trial: Trial +const ArtifactUploader: FC<{ + studyOrTrial: StudyOrTrial width: string height: string -}> = ({ trial, width, height }) => { +}> = ({ studyOrTrial, width, height }) => { const theme = useTheme() const action = actionCreator() - const [dragOver, setDragOver] = useState(false) + const [dragOver, setDragOver] = useState(false) const inputRef = useRef(null) const handleClick: MouseEventHandler = () => { @@ -190,34 +228,55 @@ const TrialArtifactUploader: FC<{ } inputRef.current.click() } + const handleOnChange: ChangeEventHandler = (e) => { const files = e.target.files if (files === null) { return } - action.uploadTrialArtifact(trial.study_id, trial.trial_id, files[0]) - } - const handleDrop: DragEventHandler = (e) => { - e.stopPropagation() - e.preventDefault() - const files = e.dataTransfer.files - setDragOver(false) - for (let i = 0; i < files.length; i++) { - action.uploadTrialArtifact(trial.study_id, trial.trial_id, files[i]) + if (studyOrTrial.type === "study") { + action.uploadStudyArtifact(studyOrTrial.study.id, files[0]) + } else if (studyOrTrial.type === "trial") { + action.uploadTrialArtifact( + studyOrTrial.trial.study_id, + studyOrTrial.trial.trial_id, + files[0] + ) } } + const handleDragOver: DragEventHandler = (e) => { e.stopPropagation() e.preventDefault() e.dataTransfer.dropEffect = "copy" setDragOver(true) } + const handleDragLeave: DragEventHandler = (e) => { e.stopPropagation() e.preventDefault() e.dataTransfer.dropEffect = "copy" setDragOver(false) } + + const handleDrop: DragEventHandler = (e) => { + e.stopPropagation() + e.preventDefault() + const files = e.dataTransfer.files + setDragOver(false) + for (let i = 0; i < files.length; i++) { + if (studyOrTrial.type === "study") { + action.uploadStudyArtifact(studyOrTrial.study.id, files[i]) + } else if (studyOrTrial.type === "trial") { + action.uploadTrialArtifact( + studyOrTrial.trial.study_id, + studyOrTrial.trial.trial_id, + files[i] + ) + } + } + } + return ( void, - () => ReactNode, -] => { - const action = actionCreator() - - const [openDeleteArtifactDialog, setOpenDeleteArtifactDialog] = - useState(false) - const [target, setTarget] = useState<[number, number, Artifact | null]>([ - -1, - -1, - null, - ]) - - const handleCloseDeleteArtifactDialog = () => { - setOpenDeleteArtifactDialog(false) - setTarget([-1, -1, null]) - } - - const handleDeleteArtifact = () => { - const [studyId, trialId, artifact] = target - if (artifact === null) { - return +type Target = + | { + type: "study" + studyId: number + artifact: Artifact } - action.deleteTrialArtifact(studyId, trialId, artifact.artifact_id) - setOpenDeleteArtifactDialog(false) - setTarget([-1, -1, null]) - } - - const openDialog = (studyId: number, trialId: number, artifact: Artifact) => { - setTarget([studyId, trialId, artifact]) - setOpenDeleteArtifactDialog(true) - } - - const renderDeleteArtifactDialog = () => { - return ( - - ) - } - return [openDialog, renderDeleteArtifactDialog] -} - -export const useDeleteStudyArtifactDialog = (): [ - (studyId: number, artifact: Artifact) => void, + | { + type: "trial" + studyId: number + trialId: number + artifact: Artifact + } +export const useDeleteArtifactDialog = (): [ + (target: Target) => void, () => ReactNode, ] => { const action = actionCreator() - const [openDeleteArtifactDialog, setOpenDeleteArtifactDialog] = useState(false) - const [target, setTarget] = useState<[number, Artifact | null]>([-1, null]) + const [target, setTarget] = useState(null) const handleCloseDeleteArtifactDialog = () => { setOpenDeleteArtifactDialog(false) - setTarget([-1, null]) + setTarget(null) } const handleDeleteArtifact = () => { - const [studyId, artifact] = target - if (artifact === null) { - return + if (target === null) return + if (target.type === "study") { + action.deleteStudyArtifact(target.studyId, target.artifact.artifact_id) + } else if (target.type === "trial") { + action.deleteTrialArtifact( + target.studyId, + target.trialId, + target.artifact.artifact_id + ) } - action.deleteStudyArtifact(studyId, artifact.artifact_id) setOpenDeleteArtifactDialog(false) - setTarget([-1, null]) + setTarget(null) } - const openDialog = (studyId: number, artifact: Artifact) => { - setTarget([studyId, artifact]) + const openDialog = (target: Target) => { + setTarget(target) setOpenDeleteArtifactDialog(true) } @@ -94,7 +63,7 @@ export const useDeleteStudyArtifactDialog = (): [ ) diff --git a/optuna_dashboard/ts/components/Artifact/SelectedTrialArtifactCards.tsx b/optuna_dashboard/ts/components/Artifact/SelectedTrialArtifactCards.tsx index 7b7bdeb82..e1c9edfa9 100644 --- a/optuna_dashboard/ts/components/Artifact/SelectedTrialArtifactCards.tsx +++ b/optuna_dashboard/ts/components/Artifact/SelectedTrialArtifactCards.tsx @@ -20,7 +20,7 @@ import React, { FC, useMemo, useState } from "react" import { StudyDirection } from "@optuna/types" import { StudyDetail, Trial } from "ts/types/optuna" import { ArtifactCardMedia } from "./ArtifactCardMedia" -import { useDeleteTrialArtifactDialog } from "./DeleteArtifactDialog" +import { useDeleteArtifactDialog } from "./DeleteArtifactDialog" import { isTableArtifact, useTableArtifactModal } from "./TableArtifactViewer" import { isThreejsArtifact, @@ -33,7 +33,7 @@ export const SelectedTrialArtifactCards: FC<{ }> = ({ study, selectedTrials }) => { const theme = useTheme() const [openDeleteArtifactDialog, renderDeleteArtifactDialog] = - useDeleteTrialArtifactDialog() + useDeleteArtifactDialog() const [openThreejsArtifactModal, renderThreejsArtifactModal] = useThreejsArtifactModal() const [openTableArtifactModal, renderTableArtifactModal] = @@ -199,11 +199,12 @@ export const SelectedTrialArtifactCards: FC<{ color="inherit" sx={{ margin: "auto 0" }} onClick={() => { - openDeleteArtifactDialog( - trial.study_id, - trial.trial_id, - artifact - ) + openDeleteArtifactDialog({ + type: "trial", + studyId: trial.study_id, + trialId: trial.trial_id, + artifact, + }) }} > diff --git a/optuna_dashboard/ts/components/Artifact/StudyArtifactCards.tsx b/optuna_dashboard/ts/components/Artifact/StudyArtifactCards.tsx deleted file mode 100644 index b24995f28..000000000 --- a/optuna_dashboard/ts/components/Artifact/StudyArtifactCards.tsx +++ /dev/null @@ -1,256 +0,0 @@ -import DeleteIcon from "@mui/icons-material/Delete" -import DownloadIcon from "@mui/icons-material/Download" -import FullscreenIcon from "@mui/icons-material/Fullscreen" -import UploadFileIcon from "@mui/icons-material/UploadFile" -import { - Box, - Card, - CardActionArea, - CardContent, - IconButton, - Typography, - useTheme, -} from "@mui/material" -import React, { - ChangeEventHandler, - DragEventHandler, - FC, - MouseEventHandler, - useRef, - useState, -} from "react" - -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, -} from "./ThreejsArtifactViewer" - -export const StudyArtifactCards: FC<{ study: StudyDetail }> = ({ study }) => { - const theme = useTheme() - const [openDeleteArtifactDialog, renderDeleteArtifactDialog] = - useDeleteStudyArtifactDialog() - const [openThreejsArtifactModal, renderThreejsArtifactModal] = - useThreejsArtifactModal() - const [openTableArtifactModal, renderTableArtifactModal] = - useTableArtifactModal() - - const width = "200px" - const height = "150px" - const artifacts = [...study.artifacts].sort((a, b) => { - if (a.filename < b.filename) { - return -1 - } else if (a.filename > b.filename) { - return 1 - } else { - return 0 - } - }) - - return ( - <> - - {artifacts.map((artifact) => { - const urlPath = `/artifacts/${study.id}/${artifact.artifact_id}` - return ( - - - - - {artifact.filename} - - {isThreejsArtifact(artifact) ? ( - { - openThreejsArtifactModal(urlPath, artifact) - }} - > - - - ) : null} - {isTableArtifact(artifact) ? ( - { - openTableArtifactModal(urlPath, artifact) - }} - > - - - ) : null} - { - openDeleteArtifactDialog(study.id, artifact) - }} - > - - - - - - - - ) - })} - - - {renderDeleteArtifactDialog()} - {renderThreejsArtifactModal()} - {renderTableArtifactModal()} - - ) -} - -const StudyArtifactUploader: FC<{ - study: StudyDetail - width: string - height: string -}> = ({ study, width, height }) => { - const theme = useTheme() - const [dragOver, setDragOver] = useState(false) - const action = actionCreator() - - const inputRef = useRef(null) - const handleClick: MouseEventHandler = () => { - if (!inputRef || !inputRef.current) { - return - } - inputRef.current.click() - } - - const handleOnChange: ChangeEventHandler = (e) => { - const files = e.target.files - if (files === null) { - return - } - action.uploadStudyArtifact(study.id, files[0]) - } - - const handleDragOver: DragEventHandler = (e) => { - e.stopPropagation() - e.preventDefault() - e.dataTransfer.dropEffect = "copy" - setDragOver(true) - } - - const handleDragLeave: DragEventHandler = (e) => { - e.stopPropagation() - e.preventDefault() - e.dataTransfer.dropEffect = "copy" - setDragOver(false) - } - - const handleDrop: DragEventHandler = (e) => { - e.stopPropagation() - e.preventDefault() - const files = e.dataTransfer.files - setDragOver(false) - for (let i = 0; i < files.length; i++) { - action.uploadStudyArtifact(study.id, files[i]) - } - } - - return ( - - - - - - Upload a New File - - Drag your file here or click to browse. - - - - - ) -} diff --git a/optuna_dashboard/ts/components/StudyHistory.tsx b/optuna_dashboard/ts/components/StudyHistory.tsx index bcf161a4d..532aad615 100644 --- a/optuna_dashboard/ts/components/StudyHistory.tsx +++ b/optuna_dashboard/ts/components/StudyHistory.tsx @@ -20,7 +20,7 @@ import { useStudySummaryValue, } from "../state" import { artifactIsAvailable } from "../state" -import { StudyArtifactCards } from "./Artifact/StudyArtifactCards" +import { ArtifactCards } from "./Artifact/ArtifactCards" import { BestTrialsCard } from "./BestTrialsCard" import { GraphHistory } from "./GraphHistory" import { GraphHyperparameterImportance } from "./GraphHyperparameterImportances" @@ -201,7 +201,9 @@ export const StudyHistory: FC<{ studyId: number }> = ({ studyId }) => { > Study Artifacts - + diff --git a/optuna_dashboard/ts/components/TrialList.tsx b/optuna_dashboard/ts/components/TrialList.tsx index 2c59c4edb..a94c2ac28 100644 --- a/optuna_dashboard/ts/components/TrialList.tsx +++ b/optuna_dashboard/ts/components/TrialList.tsx @@ -30,7 +30,7 @@ import { actionCreator } from "../action" import { useConstants } from "../constantsProvider" import { artifactIsAvailable } from "../state" import { useQuery } from "../urlQuery" -import { TrialArtifactCards } from "./Artifact/TrialArtifactCards" +import { ArtifactCards } from "./Artifact/ArtifactCards" import { TrialNote } from "./Note" import { TrialFormWidgets } from "./TrialFormWidgets" @@ -305,7 +305,25 @@ export const TrialListDetail: FC<{ value !== null ? renderInfo(key, value) : null )} - {artifactEnabled && } + {artifactEnabled && ( + <> + + Artifacts + + + + )} ) }