From 933070b8a1e120f88d7c48165b0ef6f37214eabf Mon Sep 17 00:00:00 2001 From: gen740 Date: Fri, 20 Oct 2023 17:19:52 +0900 Subject: [PATCH 1/7] Implement uplead study artifact ui --- optuna_dashboard/artifact/_backend.py | 35 ++- optuna_dashboard/ts/action.ts | 41 +++- optuna_dashboard/ts/apiClient.ts | 7 +- optuna_dashboard/ts/components/Note.tsx | 4 +- .../ts/components/StudyArtifactCards.tsx | 232 ++++++++++++++++++ .../ts/components/StudyHistory.tsx | 14 ++ .../ts/components/TrialArtifactCards.tsx | 4 +- 7 files changed, 328 insertions(+), 9 deletions(-) create mode 100644 optuna_dashboard/ts/components/StudyArtifactCards.tsx diff --git a/optuna_dashboard/artifact/_backend.py b/optuna_dashboard/artifact/_backend.py index 10a5b1ee6..ee1b85a8c 100644 --- a/optuna_dashboard/artifact/_backend.py +++ b/optuna_dashboard/artifact/_backend.py @@ -105,7 +105,7 @@ def proxy_trial_artifact( @app.post("/api/artifacts//") @json_api_view - def upload_artifact_api(study_id: int, trial_id: int) -> dict[str, Any]: + def upload_trial_artifact_api(study_id: int, trial_id: int) -> dict[str, Any]: trial = storage.get_trial(trial_id) if trial is None: response.status = 400 @@ -144,6 +144,39 @@ def upload_artifact_api(study_id: int, trial_id: int) -> dict[str, Any]: "artifacts": list_trial_artifacts(storage.get_study_system_attrs(study_id), trial), } + @app.post("/api/artifacts/") + @json_api_view + def upload_study_artifact_api(study_id: int) -> dict[str, Any]: + if artifact_store is None: + response.status = 400 # Bad Request + return {"reason": "Cannot access to the artifacts."} + file = request.json.get("file") + if file is None: + response.status = 400 + return {"reason": "Please specify the 'file' key."} + + _, data = parse_data_uri(file) + filename = request.json.get("filename", "") + artifact_id = str(uuid.uuid4()) + artifact_store.write(artifact_id, io.BytesIO(data)) + + mimetype, encoding = mimetypes.guess_type(filename) + artifact = { + "artifact_id": artifact_id, + "filename": filename, + "mimetype": mimetype or DEFAULT_MIME_TYPE, + "encoding": encoding, + } + attr_key = ARTIFACTS_ATTR_PREFIX + artifact_id + storage.set_study_system_attr(study_id, attr_key, json.dumps(artifact)) + + response.status = 201 + + return { + "artifact_id": artifact_id, + "artifacts": list_study_artifacts(storage.get_study_system_attrs(study_id)), + } + @app.delete("/api/artifacts///") @json_api_view def delete_artifact(study_id: int, trial_id: int, artifact_id: str) -> dict[str, Any]: diff --git a/optuna_dashboard/ts/action.ts b/optuna_dashboard/ts/action.ts index 41afdadba..cfcf4c24f 100644 --- a/optuna_dashboard/ts/action.ts +++ b/optuna_dashboard/ts/action.ts @@ -100,6 +100,12 @@ export const actionCreator = () => { setTrial(studyId, trialIndex, newTrial) } + const setStudyArtifacts = (studyId: number, artifacts: Artifact[]) => { + const newStudy: StudyDetail = Object.assign({}, studyDetails[studyId]) + newStudy.artifacts = artifacts + setStudyDetailState(studyId, newStudy) + } + const deleteTrialArtifact = ( studyId: number, trialId: number, @@ -430,7 +436,7 @@ export const actionCreator = () => { }) } - const uploadArtifact = ( + const uploadTrialArtifact = ( studyId: number, trialId: number, file: File @@ -467,6 +473,36 @@ export const actionCreator = () => { } } + const uploadStudyArtifact = ( + studyId: number, + file: File + ): void => { + const reader = new FileReader() + setUploading(true) + reader.readAsDataURL(file) + reader.onload = (upload: ProgressEvent) => { + uploadArtifactAPI( + studyId, + null, + file.name, + upload.target?.result as string + ) + .then((res) => { + setUploading(false) + setStudyArtifacts(studyId, res.artifacts) + }) + .catch((err) => { + setUploading(false) + const reason = err.response?.data.reason + enqueueSnackbar(`Failed to upload ${reason}`, { variant: "error" }) + }) + } + reader.onerror = (error) => { + enqueueSnackbar(`Failed to read the file ${error}`, { variant: "error" }) + console.log(error) + } + } + const deleteArtifact = ( studyId: number, trialId: number, @@ -693,7 +729,8 @@ export const actionCreator = () => { saveReloadInterval, saveStudyNote, saveTrialNote, - uploadArtifact, + uploadTrialArtifact, + uploadStudyArtifact, deleteArtifact, makeTrialComplete, makeTrialFail, diff --git a/optuna_dashboard/ts/apiClient.ts b/optuna_dashboard/ts/apiClient.ts index e5510d679..c1dedd144 100644 --- a/optuna_dashboard/ts/apiClient.ts +++ b/optuna_dashboard/ts/apiClient.ts @@ -282,12 +282,15 @@ type UploadArtifactAPIResponse = { export const uploadArtifactAPI = ( studyId: number, - trialId: number, + trialId: number | null, fileName: string, dataUrl: string ): Promise => { + const APIurl = `/api/artifacts/${studyId}${ + trialId != null ? `/${trialId}` : "" + }` return axiosInstance - .post(`/api/artifacts/${studyId}/${trialId}`, { + .post(APIurl, { file: dataUrl, filename: fileName, }) diff --git a/optuna_dashboard/ts/components/Note.tsx b/optuna_dashboard/ts/components/Note.tsx index 09270213f..586f34e0d 100644 --- a/optuna_dashboard/ts/components/Note.tsx +++ b/optuna_dashboard/ts/components/Note.tsx @@ -425,7 +425,7 @@ const ArtifactUploader: FC<{ if (files === null) { return } - action.uploadArtifact(studyId, trialId, files[0]) + action.uploadTrialArtifact(studyId, trialId, files[0]) } const handleDrop: DragEventHandler = (e) => { @@ -433,7 +433,7 @@ const ArtifactUploader: FC<{ e.preventDefault() const file = e.dataTransfer.files[0] setDragOver(false) - action.uploadArtifact(studyId, trialId, file) + action.uploadTrialArtifact(studyId, trialId, file) } const handleDragOver: DragEventHandler = (e) => { diff --git a/optuna_dashboard/ts/components/StudyArtifactCards.tsx b/optuna_dashboard/ts/components/StudyArtifactCards.tsx new file mode 100644 index 000000000..32f36830f --- /dev/null +++ b/optuna_dashboard/ts/components/StudyArtifactCards.tsx @@ -0,0 +1,232 @@ +import React, { + FC, + useState, + DragEventHandler, + useRef, + MouseEventHandler, + ChangeEventHandler, +} from "react" +import { + Typography, + Box, + Card, + useTheme, + CardContent, + CardActionArea, + IconButton, +} from "@mui/material" +import { ArtifactCardMedia } from "./ArtifactCardMedia" +import FullscreenIcon from "@mui/icons-material/Fullscreen" +import UploadFileIcon from "@mui/icons-material/UploadFile" +import DownloadIcon from "@mui/icons-material/Download" +import { actionCreator } from "../action" + +import { + isThreejsArtifact, + useThreejsArtifactModal, +} from "./ThreejsArtifactViewer" + +export const StudyArtifactCards: FC<{ study: StudyDetail }> = ({ study }) => { + const theme = useTheme() + const height = "150px" + const width = "200px" + + const [openThreejsArtifactModal, renderThreejsArtifactModal] = + useThreejsArtifactModal() + + return ( + <> + + Study Artifacts Test + + + + {study.artifacts.map((artifact) => { + const urlPath = `/artifacts/${study.id}/${artifact.artifact_id}` + return ( + + + + + {artifact.filename} + + {isThreejsArtifact(artifact) ? ( + { + openThreejsArtifactModal(urlPath, artifact) + }} + > + + + ) : null} + {/* TODO(gen740): add delete functionality + { + openDeleteArtifactDialog( + trial.study_id, + trial.trial_id, + artifact + ) + }} + > + + */} + + + + + + ) + })} + + + {renderThreejsArtifactModal()} + + ) +} + +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 907acd1ac..e89a0ee47 100644 --- a/optuna_dashboard/ts/components/StudyHistory.tsx +++ b/optuna_dashboard/ts/components/StudyHistory.tsx @@ -17,12 +17,15 @@ import { DataGrid, DataGridColumn } from "./DataGrid" import { GraphHyperparameterImportance } from "./GraphHyperparameterImportances" import { UserDefinedPlot } from "./UserDefinedPlot" import { BestTrialsCard } from "./BestTrialsCard" +import { StudyArtifactCards } from "./StudyArtifactCards" +import { useRecoilValue } from "recoil" import { useStudyDetailValue, useStudyDirections, useStudySummaryValue, } from "../state" import FormControlLabel from "@mui/material/FormControlLabel" +import { artifactIsAvailable } from "../state" export const StudyHistory: FC<{ studyId: number }> = ({ studyId }) => { const theme = useTheme() @@ -31,6 +34,7 @@ export const StudyHistory: FC<{ studyId: number }> = ({ studyId }) => { const studyDetail = useStudyDetailValue(studyId) const [logScale, setLogScale] = useState(false) const [includePruned, setIncludePruned] = useState(true) + const artifactEnabled = useRecoilValue(artifactIsAvailable) const handleLogScaleChange = () => { setLogScale(!logScale) @@ -167,6 +171,16 @@ export const StudyHistory: FC<{ studyId: number }> = ({ studyId }) => { + + + + + {artifactEnabled && studyDetail !== null && ( + + )} + + + ) } diff --git a/optuna_dashboard/ts/components/TrialArtifactCards.tsx b/optuna_dashboard/ts/components/TrialArtifactCards.tsx index 3e15f7ede..edbbce650 100644 --- a/optuna_dashboard/ts/components/TrialArtifactCards.tsx +++ b/optuna_dashboard/ts/components/TrialArtifactCards.tsx @@ -158,7 +158,7 @@ const TrialArtifactUploader: FC<{ if (files === null) { return } - action.uploadArtifact(trial.study_id, trial.trial_id, files[0]) + action.uploadTrialArtifact(trial.study_id, trial.trial_id, files[0]) } const handleDrop: DragEventHandler = (e) => { e.stopPropagation() @@ -166,7 +166,7 @@ const TrialArtifactUploader: FC<{ const files = e.dataTransfer.files setDragOver(false) for (let i = 0; i < files.length; i++) { - action.uploadArtifact(trial.study_id, trial.trial_id, files[i]) + action.uploadTrialArtifact(trial.study_id, trial.trial_id, files[i]) } } const handleDragOver: DragEventHandler = (e) => { From ee36eefcdcd8718ef0deb17c2b85f6b9b0860d7f Mon Sep 17 00:00:00 2001 From: gen740 Date: Fri, 20 Oct 2023 17:29:54 +0900 Subject: [PATCH 2/7] reorder import statements --- .../ts/components/StudyArtifactCards.tsx | 33 ++++++++++--------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/optuna_dashboard/ts/components/StudyArtifactCards.tsx b/optuna_dashboard/ts/components/StudyArtifactCards.tsx index 32f36830f..9d9f35f9b 100644 --- a/optuna_dashboard/ts/components/StudyArtifactCards.tsx +++ b/optuna_dashboard/ts/components/StudyArtifactCards.tsx @@ -1,39 +1,43 @@ import React, { - FC, - useState, + ChangeEventHandler, DragEventHandler, - useRef, + FC, MouseEventHandler, - ChangeEventHandler, + useRef, + useState, } from "react" import { Typography, Box, - Card, useTheme, + IconButton, + Card, CardContent, CardActionArea, - IconButton, } from "@mui/material" -import { ArtifactCardMedia } from "./ArtifactCardMedia" -import FullscreenIcon from "@mui/icons-material/Fullscreen" import UploadFileIcon from "@mui/icons-material/UploadFile" import DownloadIcon from "@mui/icons-material/Download" -import { actionCreator } from "../action" +import DeleteIcon from "@mui/icons-material/Delete" +import FullscreenIcon from "@mui/icons-material/Fullscreen" +import { actionCreator } from "../action" +import { useDeleteArtifactDialog } from "./DeleteArtifactDialog" import { - isThreejsArtifact, useThreejsArtifactModal, + isThreejsArtifact, } from "./ThreejsArtifactViewer" +import { ArtifactCardMedia } from "./ArtifactCardMedia" export const StudyArtifactCards: FC<{ study: StudyDetail }> = ({ study }) => { const theme = useTheme() - const height = "150px" - const width = "200px" - + const [openDeleteArtifactDialog, renderDeleteArtifactDialog] = + useDeleteArtifactDialog() const [openThreejsArtifactModal, renderThreejsArtifactModal] = useThreejsArtifactModal() + const width = "200px" + const height = "150px" + return ( <> = ({ study }) => { ) : null} - {/* TODO(gen740): add delete functionality = ({ study }) => { }} > - */} + Date: Fri, 20 Oct 2023 18:04:29 +0900 Subject: [PATCH 3/7] Add delete study artifact api --- optuna_dashboard/artifact/_backend.py | 29 ++++- optuna_dashboard/ts/action.ts | 55 ++++++-- optuna_dashboard/ts/apiClient.ts | 37 +++++- .../ts/components/DeleteArtifactDialog.tsx | 122 +++++++++++++----- .../ts/components/StudyArtifactCards.tsx | 11 +- .../ts/components/TrialArtifactCards.tsx | 4 +- python_tests/artifact/test_backend.py | 2 +- 7 files changed, 195 insertions(+), 65 deletions(-) diff --git a/optuna_dashboard/artifact/_backend.py b/optuna_dashboard/artifact/_backend.py index ee1b85a8c..55dc844c0 100644 --- a/optuna_dashboard/artifact/_backend.py +++ b/optuna_dashboard/artifact/_backend.py @@ -179,7 +179,7 @@ def upload_study_artifact_api(study_id: int) -> dict[str, Any]: @app.delete("/api/artifacts///") @json_api_view - def delete_artifact(study_id: int, trial_id: int, artifact_id: str) -> dict[str, Any]: + def delete_trial_artifact(study_id: int, trial_id: int, artifact_id: str) -> dict[str, Any]: if artifact_store is None: response.status = 400 # Bad Request return {"reason": "Cannot access to the artifacts."} @@ -187,7 +187,7 @@ def delete_artifact(study_id: int, trial_id: int, artifact_id: str) -> dict[str, # The artifact's metadata is stored in one of the following two locations: storage.set_study_system_attr( - study_id, _dashboard_trial_artifact_prefix(trial_id) + artifact_id, json.dumps(None) + study_id, _dashboard_artifact_prefix(trial_id) + artifact_id, json.dumps(None) ) storage.set_trial_system_attr( trial_id, ARTIFACTS_ATTR_PREFIX + artifact_id, json.dumps(None) @@ -196,6 +196,25 @@ def delete_artifact(study_id: int, trial_id: int, artifact_id: str) -> dict[str, response.status = 204 return {} + @app.delete("/api/artifacts//") + @json_api_view + def delete_study_artifact(study_id: int, artifact_id: str) -> dict[str, Any]: + if artifact_store is None: + response.status = 400 # Bad Request + return {"reason": "Cannot access to the artifacts."} + artifact_store.remove(artifact_id) + + # The artifact's metadata is stored in one of the following two locations: + storage.set_study_system_attr( + study_id, _dashboard_artifact_prefix(study_id) + artifact_id, json.dumps(None) + ) + storage.set_study_system_attr( + study_id, ARTIFACTS_ATTR_PREFIX + artifact_id, json.dumps(None) + ) + + response.status = 204 + return {} + def upload_artifact( backend: ArtifactBackend, @@ -253,7 +272,7 @@ def objective(trial: optuna.Trial) -> float: return artifact_id -def _dashboard_trial_artifact_prefix(trial_id: int) -> str: +def _dashboard_artifact_prefix(trial_id: int) -> str: return DASHBOARD_ARTIFACTS_ATTR_PREFIX + f"{trial_id}:" @@ -273,7 +292,7 @@ def get_trial_artifact_meta( ) -> Optional[ArtifactMeta]: # Search study_system_attrs due to backward compatibility. study_system_attrs = storage.get_study_system_attrs(study_id) - attr_key = _dashboard_trial_artifact_prefix(trial_id=trial_id) + artifact_id + attr_key = _dashboard_artifact_prefix(trial_id=trial_id) + artifact_id artifact_meta = study_system_attrs.get(attr_key) if artifact_meta is not None: return json.loads(artifact_meta) @@ -317,7 +336,7 @@ def list_trial_artifacts( dashboard_artifact_metas = [ json.loads(value) for key, value in study_system_attrs.items() - if key.startswith(_dashboard_trial_artifact_prefix(trial._trial_id)) + if key.startswith(_dashboard_artifact_prefix(trial._trial_id)) ] # Collect ArtifactMeta from trial_system_attrs. Note that artifacts uploaded via diff --git a/optuna_dashboard/ts/action.ts b/optuna_dashboard/ts/action.ts index cfcf4c24f..e3be3d850 100644 --- a/optuna_dashboard/ts/action.ts +++ b/optuna_dashboard/ts/action.ts @@ -11,9 +11,11 @@ import { tellTrialAPI, saveTrialUserAttrsAPI, renameStudyAPI, - uploadArtifactAPI, + uploadTrialArtifactAPI, + uploadStudyArtifactAPI, getMetaInfoAPI, - deleteArtifactAPI, + deleteTrialArtifactAPI, + deleteStudyArtifactAPI, reportPreferenceAPI, skipPreferentialTrialAPI, removePreferentialHistoryAPI, @@ -106,7 +108,7 @@ export const actionCreator = () => { setStudyDetailState(studyId, newStudy) } - const deleteTrialArtifact = ( + const deleteTrialArtifactState = ( studyId: number, trialId: number, artifact_id: string @@ -128,6 +130,18 @@ export const actionCreator = () => { setTrialArtifacts(studyId, index, newArtifacts) } + const deleteStudyArtifactState = (studyId: number, artifact_id: string) => { + const artifacts = studyDetails[studyId].artifacts + const artifactIndex = artifacts.findIndex( + (a) => a.artifact_id === artifact_id + ) + const newArtifacts = [ + ...artifacts.slice(0, artifactIndex), + ...artifacts.slice(artifactIndex + 1, artifacts.length), + ] + setStudyArtifacts(studyId, newArtifacts) + } + const setTrialStateValues = ( studyId: number, index: number, @@ -445,7 +459,7 @@ export const actionCreator = () => { setUploading(true) reader.readAsDataURL(file) reader.onload = (upload: ProgressEvent) => { - uploadArtifactAPI( + uploadTrialArtifactAPI( studyId, trialId, file.name, @@ -473,17 +487,13 @@ export const actionCreator = () => { } } - const uploadStudyArtifact = ( - studyId: number, - file: File - ): void => { + const uploadStudyArtifact = (studyId: number, file: File): void => { const reader = new FileReader() setUploading(true) reader.readAsDataURL(file) reader.onload = (upload: ProgressEvent) => { - uploadArtifactAPI( + uploadStudyArtifactAPI( studyId, - null, file.name, upload.target?.result as string ) @@ -503,14 +513,30 @@ export const actionCreator = () => { } } - const deleteArtifact = ( + const deleteTrialArtifact = ( studyId: number, trialId: number, artifactId: string ): void => { - deleteArtifactAPI(studyId, trialId, artifactId) + deleteTrialArtifactAPI(studyId, trialId, artifactId) + .then(() => { + deleteTrialArtifactState(studyId, trialId, artifactId) + enqueueSnackbar(`Success to delete an artifact.`, { + variant: "success", + }) + }) + .catch((err) => { + const reason = err.response?.data.reason + enqueueSnackbar(`Failed to delete ${reason}.`, { + variant: "error", + }) + }) + } + + const deleteStudyArtifact = (studyId: number, artifactId: string): void => { + deleteStudyArtifactAPI(studyId, artifactId) .then(() => { - deleteTrialArtifact(studyId, trialId, artifactId) + deleteStudyArtifactState(studyId, artifactId) enqueueSnackbar(`Success to delete an artifact.`, { variant: "success", }) @@ -731,7 +757,8 @@ export const actionCreator = () => { saveTrialNote, uploadTrialArtifact, uploadStudyArtifact, - deleteArtifact, + deleteTrialArtifact, + deleteStudyArtifact, makeTrialComplete, makeTrialFail, saveTrialUserAttrs, diff --git a/optuna_dashboard/ts/apiClient.ts b/optuna_dashboard/ts/apiClient.ts index c1dedd144..f42d20de3 100644 --- a/optuna_dashboard/ts/apiClient.ts +++ b/optuna_dashboard/ts/apiClient.ts @@ -280,17 +280,14 @@ type UploadArtifactAPIResponse = { artifacts: Artifact[] } -export const uploadArtifactAPI = ( +export const uploadTrialArtifactAPI = ( studyId: number, - trialId: number | null, + trialId: number, fileName: string, dataUrl: string ): Promise => { - const APIurl = `/api/artifacts/${studyId}${ - trialId != null ? `/${trialId}` : "" - }` return axiosInstance - .post(APIurl, { + .post(`/api/artifacts/${studyId}/${trialId}`, { file: dataUrl, filename: fileName, }) @@ -299,7 +296,22 @@ export const uploadArtifactAPI = ( }) } -export const deleteArtifactAPI = ( +export const uploadStudyArtifactAPI = ( + studyId: number, + fileName: string, + dataUrl: string +): Promise => { + return axiosInstance + .post(`/api/artifacts/${studyId}`, { + file: dataUrl, + filename: fileName, + }) + .then((res) => { + return res.data + }) +} + +export const deleteTrialArtifactAPI = ( studyId: number, trialId: number, artifactId: string @@ -311,6 +323,17 @@ export const deleteArtifactAPI = ( }) } +export const deleteStudyArtifactAPI = ( + studyId: number, + artifactId: string +): Promise => { + return axiosInstance + .delete(`/api/artifacts/${studyId}/${artifactId}`) + .then(() => { + return + }) +} + export const tellTrialAPI = ( trialId: number, state: TrialStateFinished, diff --git a/optuna_dashboard/ts/components/DeleteArtifactDialog.tsx b/optuna_dashboard/ts/components/DeleteArtifactDialog.tsx index 2c9c229e4..4bf5e95df 100644 --- a/optuna_dashboard/ts/components/DeleteArtifactDialog.tsx +++ b/optuna_dashboard/ts/components/DeleteArtifactDialog.tsx @@ -1,4 +1,4 @@ -import React, { ReactNode, useState } from "react" +import React, { ReactNode, useState, FC } from "react" import { Dialog, DialogTitle, @@ -9,9 +9,9 @@ import { } from "@mui/material" import { actionCreator } from "../action" -export const useDeleteArtifactDialog = (): [ +export const useDeleteTrialArtifactDialog = (): [ (studyId: number, trialId: number, artifact: Artifact) => void, - () => ReactNode + () => ReactNode, ] => { const action = actionCreator() @@ -33,7 +33,7 @@ export const useDeleteArtifactDialog = (): [ if (artifact === null) { return } - action.deleteArtifact(studyId, trialId, artifact.artifact_id) + action.deleteTrialArtifact(studyId, trialId, artifact.artifact_id) setOpenDeleteArtifactDialog(false) setTarget([-1, -1, null]) } @@ -45,32 +45,96 @@ export const useDeleteArtifactDialog = (): [ const renderDeleteArtifactDialog = () => { return ( - { - handleCloseDeleteArtifactDialog() - }} - aria-labelledby="delete-artifact-dialog-title" - > - - Delete artifact - - - - Are you sure you want to delete an artifact (" - {target[2]?.filename}")? - - - - - - - + ) } return [openDialog, renderDeleteArtifactDialog] } + +export const useDeleteStudyArtifactDialog = (): [ + (studyId: number, artifact: Artifact) => void, + () => ReactNode, +] => { + const action = actionCreator() + + const [openDeleteArtifactDialog, setOpenDeleteArtifactDialog] = + useState(false) + const [target, setTarget] = useState<[number, Artifact | null]>([-1, null]) + + const handleCloseDeleteArtifactDialog = () => { + setOpenDeleteArtifactDialog(false) + setTarget([-1, null]) + } + + const handleDeleteArtifact = () => { + const [studyId, artifact] = target + if (artifact === null) { + return + } + action.deleteStudyArtifact(studyId, artifact.artifact_id) + setOpenDeleteArtifactDialog(false) + setTarget([-1, null]) + } + + const openDialog = (studyId: number, artifact: Artifact) => { + setTarget([studyId, artifact]) + setOpenDeleteArtifactDialog(true) + } + + const renderDeleteArtifactDialog = () => { + return ( + + ) + } + return [openDialog, renderDeleteArtifactDialog] +} + +const DeleteDialog: FC<{ + openDeleteArtifactDialog: boolean + handleCloseDeleteArtifactDialog: () => void + filename: string | undefined + handleDeleteArtifact: () => void +}> = ({ + openDeleteArtifactDialog, + handleCloseDeleteArtifactDialog, + filename, + handleDeleteArtifact, +}) => { + return ( + { + handleCloseDeleteArtifactDialog() + }} + aria-labelledby="delete-artifact-dialog-title" + > + + Delete artifact + + + + Are you sure you want to delete an artifact (" + {filename}")? + + + + + + + + ) +} diff --git a/optuna_dashboard/ts/components/StudyArtifactCards.tsx b/optuna_dashboard/ts/components/StudyArtifactCards.tsx index 9d9f35f9b..71143ed4d 100644 --- a/optuna_dashboard/ts/components/StudyArtifactCards.tsx +++ b/optuna_dashboard/ts/components/StudyArtifactCards.tsx @@ -21,7 +21,7 @@ import DeleteIcon from "@mui/icons-material/Delete" import FullscreenIcon from "@mui/icons-material/Fullscreen" import { actionCreator } from "../action" -import { useDeleteArtifactDialog } from "./DeleteArtifactDialog" +import { useDeleteStudyArtifactDialog } from "./DeleteArtifactDialog" import { useThreejsArtifactModal, isThreejsArtifact, @@ -31,7 +31,7 @@ import { ArtifactCardMedia } from "./ArtifactCardMedia" export const StudyArtifactCards: FC<{ study: StudyDetail }> = ({ study }) => { const theme = useTheme() const [openDeleteArtifactDialog, renderDeleteArtifactDialog] = - useDeleteArtifactDialog() + useDeleteStudyArtifactDialog() const [openThreejsArtifactModal, renderThreejsArtifactModal] = useThreejsArtifactModal() @@ -104,11 +104,7 @@ export const StudyArtifactCards: FC<{ study: StudyDetail }> = ({ study }) => { color="inherit" sx={{ margin: "auto 0" }} onClick={() => { - openDeleteArtifactDialog( - trial.study_id, - trial.trial_id, - artifact - ) + openDeleteArtifactDialog(study.id, artifact) }} > @@ -129,6 +125,7 @@ export const StudyArtifactCards: FC<{ study: StudyDetail }> = ({ study }) => { })} + {renderDeleteArtifactDialog()} {renderThreejsArtifactModal()} ) diff --git a/optuna_dashboard/ts/components/TrialArtifactCards.tsx b/optuna_dashboard/ts/components/TrialArtifactCards.tsx index edbbce650..1a078f299 100644 --- a/optuna_dashboard/ts/components/TrialArtifactCards.tsx +++ b/optuna_dashboard/ts/components/TrialArtifactCards.tsx @@ -21,7 +21,7 @@ import DeleteIcon from "@mui/icons-material/Delete" import FullscreenIcon from "@mui/icons-material/Fullscreen" import { actionCreator } from "../action" -import { useDeleteArtifactDialog } from "./DeleteArtifactDialog" +import { useDeleteTrialArtifactDialog } from "./DeleteArtifactDialog" import { useThreejsArtifactModal, isThreejsArtifact, @@ -31,7 +31,7 @@ import { ArtifactCardMedia } from "./ArtifactCardMedia" export const TrialArtifactCards: FC<{ trial: Trial }> = ({ trial }) => { const theme = useTheme() const [openDeleteArtifactDialog, renderDeleteArtifactDialog] = - useDeleteArtifactDialog() + useDeleteTrialArtifactDialog() const [openThreejsArtifactModal, renderThreejsArtifactModal] = useThreejsArtifactModal() diff --git a/python_tests/artifact/test_backend.py b/python_tests/artifact/test_backend.py index 74698a095..22544e6f1 100644 --- a/python_tests/artifact/test_backend.py +++ b/python_tests/artifact/test_backend.py @@ -12,7 +12,7 @@ def test_get_artifact_path() -> None: def test_artifact_prefix() -> None: - actual = _backend._dashboard_trial_artifact_prefix(trial_id=0) + actual = _backend._dashboard_artifact_prefix(trial_id=0) assert actual == "dashboard:artifacts:0:" From f4405f033311c987e75febb24bbeb861128854a7 Mon Sep 17 00:00:00 2001 From: gen740 Date: Fri, 27 Oct 2023 17:30:33 +0900 Subject: [PATCH 4/7] Add bordor to the Study Artifact Card Content --- .../ts/components/StudyArtifactCards.tsx | 8 +------ .../ts/components/StudyHistory.tsx | 22 ++++++++++++++++--- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/optuna_dashboard/ts/components/StudyArtifactCards.tsx b/optuna_dashboard/ts/components/StudyArtifactCards.tsx index 71143ed4d..c63948960 100644 --- a/optuna_dashboard/ts/components/StudyArtifactCards.tsx +++ b/optuna_dashboard/ts/components/StudyArtifactCards.tsx @@ -40,13 +40,6 @@ export const StudyArtifactCards: FC<{ study: StudyDetail }> = ({ study }) => { return ( <> - - Study Artifacts Test - - {study.artifacts.map((artifact) => { const urlPath = `/artifacts/${study.id}/${artifact.artifact_id}` @@ -57,6 +50,7 @@ export const StudyArtifactCards: FC<{ study: StudyDetail }> = ({ study }) => { marginBottom: theme.spacing(2), width: width, margin: theme.spacing(0, 1, 1, 0), + border: `1px solid ${theme.palette.divider}`, }} > = ({ studyId }) => { - {artifactEnabled && studyDetail !== null && ( - - )} + + + Study Artifacts + + {artifactEnabled && studyDetail !== null && ( + + )} + From 7dbef5050115948aeb5508e84fcca10d9730a15a Mon Sep 17 00:00:00 2001 From: gen740 Date: Fri, 3 Nov 2023 04:02:35 +0900 Subject: [PATCH 5/7] Apply Format --- optuna_dashboard/ts/components/DeleteArtifactDialog.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/optuna_dashboard/ts/components/DeleteArtifactDialog.tsx b/optuna_dashboard/ts/components/DeleteArtifactDialog.tsx index 4bf5e95df..7ac461624 100644 --- a/optuna_dashboard/ts/components/DeleteArtifactDialog.tsx +++ b/optuna_dashboard/ts/components/DeleteArtifactDialog.tsx @@ -11,7 +11,7 @@ import { actionCreator } from "../action" export const useDeleteTrialArtifactDialog = (): [ (studyId: number, trialId: number, artifact: Artifact) => void, - () => ReactNode, + () => ReactNode ] => { const action = actionCreator() @@ -58,7 +58,7 @@ export const useDeleteTrialArtifactDialog = (): [ export const useDeleteStudyArtifactDialog = (): [ (studyId: number, artifact: Artifact) => void, - () => ReactNode, + () => ReactNode ] => { const action = actionCreator() From 9d7e10759e7e9dcffc45939e0cfdae6d38fdf4fd Mon Sep 17 00:00:00 2001 From: gen740 Date: Fri, 17 Nov 2023 14:53:56 +0900 Subject: [PATCH 6/7] Hide study artifact card when artifact is not enabled --- .../ts/components/StudyHistory.tsx | 40 +++++++++---------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/optuna_dashboard/ts/components/StudyHistory.tsx b/optuna_dashboard/ts/components/StudyHistory.tsx index dbe154275..bdfb225be 100644 --- a/optuna_dashboard/ts/components/StudyHistory.tsx +++ b/optuna_dashboard/ts/components/StudyHistory.tsx @@ -172,31 +172,31 @@ export const StudyHistory: FC<{ studyId: number }> = ({ studyId }) => { - - - - - + + + - Study Artifacts - - {artifactEnabled && studyDetail !== null && ( + + Study Artifacts + - )} - - + + + - + )} ) } From 68e181e838e769fcfc7964add044d8cf9e636869 Mon Sep 17 00:00:00 2001 From: gen740 Date: Fri, 17 Nov 2023 16:53:00 +0900 Subject: [PATCH 7/7] Delete unnecessary dashboard_artifact system_attr deletion --- optuna_dashboard/artifact/_backend.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/optuna_dashboard/artifact/_backend.py b/optuna_dashboard/artifact/_backend.py index 55dc844c0..9720124b0 100644 --- a/optuna_dashboard/artifact/_backend.py +++ b/optuna_dashboard/artifact/_backend.py @@ -204,10 +204,6 @@ def delete_study_artifact(study_id: int, artifact_id: str) -> dict[str, Any]: return {"reason": "Cannot access to the artifacts."} artifact_store.remove(artifact_id) - # The artifact's metadata is stored in one of the following two locations: - storage.set_study_system_attr( - study_id, _dashboard_artifact_prefix(study_id) + artifact_id, json.dumps(None) - ) storage.set_study_system_attr( study_id, ARTIFACTS_ATTR_PREFIX + artifact_id, json.dumps(None) )