Implement FileUpload on MarkdownEditor

This commit is contained in:
c-bata
2023-01-14 01:13:13 +09:00
parent 4dc7178c1e
commit 920770e5fa
7 changed files with 125 additions and 9 deletions
+4
View File
@@ -13,6 +13,7 @@ from typing import TypeVar
from typing import Union
from bottle import BaseResponse
from bottle import HTTPError
from bottle import response
@@ -29,6 +30,9 @@ def json_api_view(view: BottleAPIView) -> BottleAPIView:
response.content_type = "application/json"
response_body = view(*args, **kwargs)
return response_body
except HTTPError as e:
response.status = e.status_code
return json.dumps({"reason": str(e.body)})
except Exception as e:
response.status = 500
response.content_type = "application/json"
+13 -7
View File
@@ -1,11 +1,13 @@
from __future__ import annotations
import io
import json
import mimetypes
import os.path
from typing import TYPE_CHECKING
import uuid
from bottle import BaseRequest
from bottle import Bottle
from bottle import request
from bottle import response
@@ -37,6 +39,9 @@ if TYPE_CHECKING:
ARTIFACTS_ATTR_PREFIX = "dashboard:artifacts:"
DEFAULT_MIME_TYPE = "application/octet-stream"
BaseRequest.MEMFILE_MAX = int(
os.environ.get("OPTUNA_DASHBOARD_MEMFILE_MAX", 1024 * 1024 * 128)
) # 128MB
def register_artifact_route(
@@ -56,22 +61,22 @@ def register_artifact_route(
body = f.read()
return body
@app.post("/api/artifacts/<trial_id:int>/")
@app.post("/api/studies/<study_id:int>/artifacts")
@json_api_view
def upload_artifact(trial_id: int) -> dict[str, Any]:
def upload_artifact_api(study_id: int) -> dict[str, Any]:
if artifact_backend is None:
response.status = 400 # Bad Request
return {"reason": "Cannot access to the artifacts."}
file = request.json.get("file")
if file is None:
trial_id = request.json.get("trial_id")
if file is None or trial_id 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())
with artifact_backend.open(artifact_id=artifact_id) as f:
f.write(data)
artifact_backend.write(artifact_id, io.BytesIO(data))
mimetype, encoding = mimetypes.guess_type(filename)
artifact = {
@@ -81,7 +86,7 @@ def register_artifact_route(
"encoding": encoding,
}
attr_key = _artifact_prefix(trial_id=trial_id) + artifact_id
storage.set_study_system_attr(trial_id, attr_key, json.dumps(artifact))
storage.set_study_system_attr(study_id, attr_key, json.dumps(artifact))
response.status = 201
return artifact
@@ -124,6 +129,7 @@ def upload_artifact(
filename = os.path.basename(file_path)
storage = trial.storage
trial_id = trial._trial_id
study_id = trial._study_id
artifact_id = str(uuid.uuid4())
guess_mimetype, guess_encoding = mimetypes.guess_type(filename)
artifact: ArtifactMeta = {
@@ -133,7 +139,7 @@ def upload_artifact(
"filename": filename,
}
attr_key = _artifact_prefix(trial_id=trial_id) + artifact_id
storage.set_study_system_attr(trial_id, attr_key, json.dumps(artifact))
storage.set_study_system_attr(study_id, attr_key, json.dumps(artifact))
with open(file_path, "rb") as f:
backend.write(artifact_id, f)
+32 -1
View File
@@ -1,4 +1,4 @@
import { useRecoilState } from "recoil"
import { useRecoilState, useSetRecoilState } from "recoil"
import { useSnackbar } from "notistack"
import {
getStudyDetailAPI,
@@ -9,12 +9,14 @@ import {
saveStudyNoteAPI,
saveTrialNoteAPI,
renameStudyAPI,
uploadArtifactAPI,
} from "./apiClient"
import {
graphVisibilityState,
studyDetailsState,
studySummariesState,
paramImportanceState,
isFileUploading,
} from "./state"
const localStorageGraphVisibility = "graphVisibility"
@@ -29,6 +31,7 @@ export const actionCreator = () => {
useRecoilState<GraphVisibility>(graphVisibilityState)
const [paramImportance, setParamImportance] =
useRecoilState<StudyParamImportance>(paramImportanceState)
const setUploading = useSetRecoilState<boolean>(isFileUploading)
const setStudyDetailState = (studyId: number, study: StudyDetail) => {
const newVal = Object.assign({}, studyDetails)
@@ -265,6 +268,33 @@ export const actionCreator = () => {
})
}
const uploadArtifact = (
studyId: number,
trialId: number,
file: File
): void => {
const reader = new FileReader()
setUploading(true)
reader.readAsDataURL(file)
reader.onload = (upload: any) => {
uploadArtifactAPI(studyId, trialId, file.name, upload.target.result)
.then((artifact) => {
setUploading(false)
// TODO: update global state
console.dir(artifact)
})
.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)
}
}
return {
updateStudyDetail,
updateStudySummaries,
@@ -276,6 +306,7 @@ export const actionCreator = () => {
saveGraphVisibility,
saveStudyNote,
saveTrialNote,
uploadArtifact,
}
}
+17
View File
@@ -217,6 +217,23 @@ export const saveTrialNoteAPI = (
})
}
export const uploadArtifactAPI = (
studyId: number,
trialId: number,
fileName: string,
dataUrl: string
): Promise<Artifact> => {
return axiosInstance
.post<Artifact>(`/api/studies/${studyId}/artifacts`, {
trial_id: trialId,
file: dataUrl,
filename: fileName,
})
.then((res) => {
return res.data
})
}
interface ParamImportancesResponse {
param_importances: ParamImportance[][]
}
+47 -1
View File
@@ -15,7 +15,13 @@ import {
Typography,
useTheme,
} from "@mui/material"
import React, { FC, createRef, useState, useEffect } from "react"
import React, {
FC,
createRef,
useState,
useEffect,
DragEventHandler,
} from "react"
import ReactMarkdown from "react-markdown"
import remarkGfm from "remark-gfm"
import remarkMath from "remark-math"
@@ -36,6 +42,8 @@ import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"
import { darcula } from "react-syntax-highlighter/dist/esm/styles/prism"
import { actionCreator } from "../action"
import { useRecoilValue } from "recoil"
import { isFileUploading } from "../state"
const placeholder = `## What is this feature for?
@@ -171,9 +179,11 @@ const MarkdownEditorModal: FC<{
window.onbeforeunload = null
})
const [dragOver, setDragOver] = useState<boolean>(false)
const [saving, setSaving] = useState(false)
const [edited, setEdited] = useState(false)
const [curNote, setCurNote] = useState({ version: 0, body: "" })
const uploading = useRecoilValue<boolean>(isFileUploading)
const textAreaRef = createRef<HTMLTextAreaElement>()
const notLatest = latestNote.version > curNote.version
@@ -225,6 +235,37 @@ const MarkdownEditorModal: FC<{
window.onbeforeunload = null
}
const handleDrop: DragEventHandler = (e) => {
if (trialId === undefined) {
return
}
e.stopPropagation()
e.preventDefault()
const file = e.dataTransfer.files[0]
setDragOver(false)
action.uploadArtifact(studyId, trialId, file)
}
const handleDragOver: DragEventHandler = (e) => {
if (trialId === undefined) {
return
}
e.stopPropagation()
e.preventDefault()
e.dataTransfer.dropEffect = "copy"
setDragOver(true)
}
const handleDragLeave: DragEventHandler = (e) => {
if (trialId === undefined) {
return
}
e.stopPropagation()
e.preventDefault()
e.dataTransfer.dropEffect = "copy"
setDragOver(false)
}
// See https://github.com/iamhosseindhv/notistack/issues/231#issuecomment-825924840
const zIndex = theme.zIndex.snackbar - 2
@@ -273,10 +314,15 @@ const MarkdownEditorModal: FC<{
>
<MarkdownRenderer body={previewMarkdown} />
</Box>
<Typography>{dragOver ? "DragOver=true" : "DragOver=false"}</Typography>
<Typography>{uploading ? "uploading=true" : "not uploading"}</Typography>
<TextField
disabled={saving}
multiline={true}
placeholder={placeholder}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
sx={{
position: "relative",
resize: "none",
+5
View File
@@ -39,6 +39,11 @@ export const drawerOpenState = atom<boolean>({
default: false,
})
export const isFileUploading = atom<boolean>({
key: "isFileUploading",
default: false,
})
export const useStudyDetailValue = (studyId: number): StudyDetail | null => {
const studyDetails = useRecoilValue<StudyDetails>(studyDetailsState)
return studyDetails[studyId] || null
+7
View File
@@ -88,6 +88,13 @@ type Note = {
body: string
}
type Artifact = {
artifact_id: string
filename: string
mimetype: string
encoding: string
}
type Trial = {
trial_id: number
study_id: number