mirror of
https://github.com/wassname/optuna-dashboard.git
synced 2026-09-10 12:23:22 +08:00
Implement uplead study artifact ui
This commit is contained in:
@@ -105,7 +105,7 @@ def register_artifact_route(
|
||||
|
||||
@app.post("/api/artifacts/<study_id:int>/<trial_id:int>")
|
||||
@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 register_artifact_route(
|
||||
"artifacts": list_trial_artifacts(storage.get_study_system_attrs(study_id), trial),
|
||||
}
|
||||
|
||||
@app.post("/api/artifacts/<study_id:int>")
|
||||
@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/<study_id:int>/<trial_id:int>/<artifact_id:re:[0-9a-fA-F-]+>")
|
||||
@json_api_view
|
||||
def delete_artifact(study_id: int, trial_id: int, artifact_id: str) -> dict[str, Any]:
|
||||
|
||||
@@ -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<FileReader>) => {
|
||||
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,
|
||||
|
||||
@@ -282,12 +282,15 @@ type UploadArtifactAPIResponse = {
|
||||
|
||||
export const uploadArtifactAPI = (
|
||||
studyId: number,
|
||||
trialId: number,
|
||||
trialId: number | null,
|
||||
fileName: string,
|
||||
dataUrl: string
|
||||
): Promise<UploadArtifactAPIResponse> => {
|
||||
const APIurl = `/api/artifacts/${studyId}${
|
||||
trialId != null ? `/${trialId}` : ""
|
||||
}`
|
||||
return axiosInstance
|
||||
.post<UploadArtifactAPIResponse>(`/api/artifacts/${studyId}/${trialId}`, {
|
||||
.post<UploadArtifactAPIResponse>(APIurl, {
|
||||
file: dataUrl,
|
||||
filename: fileName,
|
||||
})
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
<Typography
|
||||
variant="h5"
|
||||
sx={{ fontWeight: theme.typography.fontWeightBold }}
|
||||
>
|
||||
Study Artifacts Test
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ display: "flex", flexWrap: "wrap", p: theme.spacing(1, 0) }}>
|
||||
{study.artifacts.map((artifact) => {
|
||||
const urlPath = `/artifacts/${study.id}/${artifact.artifact_id}`
|
||||
return (
|
||||
<Card
|
||||
key={artifact.artifact_id}
|
||||
sx={{
|
||||
marginBottom: theme.spacing(2),
|
||||
width: width,
|
||||
margin: theme.spacing(0, 1, 1, 0),
|
||||
}}
|
||||
>
|
||||
<ArtifactCardMedia
|
||||
artifact={artifact}
|
||||
urlPath={urlPath}
|
||||
height={height}
|
||||
/>
|
||||
<CardContent
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
padding: `${theme.spacing(1)} !important`,
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
sx={{
|
||||
p: theme.spacing(0.5, 0),
|
||||
flexGrow: 1,
|
||||
wordWrap: "break-word",
|
||||
maxWidth: `calc(100% - ${
|
||||
isThreejsArtifact(artifact)
|
||||
? theme.spacing(12)
|
||||
: theme.spacing(8)
|
||||
})`,
|
||||
}}
|
||||
>
|
||||
{artifact.filename}
|
||||
</Typography>
|
||||
{isThreejsArtifact(artifact) ? (
|
||||
<IconButton
|
||||
aria-label="show artifact 3d model"
|
||||
size="small"
|
||||
color="inherit"
|
||||
sx={{ margin: "auto 0" }}
|
||||
onClick={() => {
|
||||
openThreejsArtifactModal(urlPath, artifact)
|
||||
}}
|
||||
>
|
||||
<FullscreenIcon />
|
||||
</IconButton>
|
||||
) : null}
|
||||
{/* TODO(gen740): add delete functionality
|
||||
<IconButton
|
||||
aria-label="delete artifact"
|
||||
size="small"
|
||||
color="inherit"
|
||||
sx={{ margin: "auto 0" }}
|
||||
onClick={() => {
|
||||
openDeleteArtifactDialog(
|
||||
trial.study_id,
|
||||
trial.trial_id,
|
||||
artifact
|
||||
)
|
||||
}}
|
||||
>
|
||||
<DeleteIcon />
|
||||
</IconButton>*/}
|
||||
<IconButton
|
||||
aria-label="download artifact"
|
||||
size="small"
|
||||
color="inherit"
|
||||
download={artifact.filename}
|
||||
sx={{ margin: "auto 0" }}
|
||||
href={urlPath}
|
||||
>
|
||||
<DownloadIcon />
|
||||
</IconButton>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
})}
|
||||
<StudyArtifactUploader study={study} width={width} height={height} />
|
||||
</Box>
|
||||
{renderThreejsArtifactModal()}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const StudyArtifactUploader: FC<{
|
||||
study: StudyDetail
|
||||
width: string
|
||||
height: string
|
||||
}> = ({ study, width, height }) => {
|
||||
const theme = useTheme()
|
||||
const [dragOver, setDragOver] = useState<boolean>(false)
|
||||
const action = actionCreator()
|
||||
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
const handleClick: MouseEventHandler = () => {
|
||||
if (!inputRef || !inputRef.current) {
|
||||
return
|
||||
}
|
||||
inputRef.current.click()
|
||||
}
|
||||
|
||||
const handleOnChange: ChangeEventHandler<HTMLInputElement> = (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 (
|
||||
<Card
|
||||
sx={{
|
||||
marginBottom: theme.spacing(2),
|
||||
width: width,
|
||||
minHeight: height,
|
||||
margin: theme.spacing(0, 1, 1, 0),
|
||||
border: dragOver
|
||||
? `3px dashed ${theme.palette.mode === "dark" ? "white" : "black"}`
|
||||
: `1px solid ${theme.palette.divider}`,
|
||||
}}
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
>
|
||||
<CardActionArea
|
||||
onClick={handleClick}
|
||||
sx={{
|
||||
height: "100%",
|
||||
}}
|
||||
>
|
||||
<CardContent
|
||||
sx={{
|
||||
display: "flex",
|
||||
height: "100%",
|
||||
flexDirection: "column",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<UploadFileIcon
|
||||
sx={{ fontSize: 80, marginBottom: theme.spacing(2) }}
|
||||
/>
|
||||
<input
|
||||
type="file"
|
||||
ref={inputRef}
|
||||
onChange={handleOnChange}
|
||||
style={{ display: "none" }}
|
||||
/>
|
||||
<Typography>Upload a New File</Typography>
|
||||
<Typography
|
||||
sx={{ textAlign: "center", color: theme.palette.grey.A400 }}
|
||||
>
|
||||
Drag your file here or click to browse.
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</CardActionArea>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -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<boolean>(false)
|
||||
const [includePruned, setIncludePruned] = useState<boolean>(true)
|
||||
const artifactEnabled = useRecoilValue<boolean>(artifactIsAvailable)
|
||||
|
||||
const handleLogScaleChange = () => {
|
||||
setLogScale(!logScale)
|
||||
@@ -167,6 +171,16 @@ export const StudyHistory: FC<{ studyId: number }> = ({ studyId }) => {
|
||||
</Card>
|
||||
</Grid2>
|
||||
</Grid2>
|
||||
|
||||
<Grid2 container spacing={2} sx={{ padding: theme.spacing(0, 2) }}>
|
||||
<Grid2 xs={6}>
|
||||
<Card>
|
||||
{artifactEnabled && studyDetail !== null && (
|
||||
<StudyArtifactCards study={studyDetail} />
|
||||
)}
|
||||
</Card>
|
||||
</Grid2>
|
||||
</Grid2>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user