Add button to rename study

This commit is contained in:
c-bata
2023-01-06 01:29:04 +09:00
parent 5548d8f206
commit b03b1ef0f7
7 changed files with 217 additions and 44 deletions
+33
View File
@@ -25,6 +25,7 @@ from bottle import response
from bottle import run
from bottle import SimpleTemplate
from bottle import static_file
import optuna
from optuna.exceptions import DuplicatedStudyError
from optuna.storages import BaseStorage
from optuna.storages import RDBStorage
@@ -282,6 +283,38 @@ def create_app(storage: BaseStorage, debug: bool = False) -> Bottle:
response.status = 201 # Created
return {"study_summary": serialize_study_summary(summary)}
@app.post("/api/studies/<study_id:int>/rename")
@json_api_view
def rename_study(study_id: int) -> BottleViewReturn:
dst_study_name = request.json.get("study_name", None)
if dst_study_name is None:
response.status = 400 # Bad request
return {"reason": "You need to set study_name and direction"}
src_study_name = storage.get_study_name_from_id(study_id)
try:
src_study = optuna.load_study(storage=storage, study_name=src_study_name)
except KeyError:
response.status = 404 # Not found
return {"reason": f"study_id={study_id} is not found"}
try:
dst_study = optuna.create_study(storage=storage, study_name=dst_study_name)
dst_study.add_trials(src_study.get_trials(deepcopy=False))
except DuplicatedStudyError:
response.status = 400 # Bad request
return {"reason": f"study_name={dst_study_name} is duplicaated"}
except Exception as e:
logger.exception("Unexpected error:")
response.status = 500
storage.delete_study(dst_study._study_id)
return {"reason": str(e)}
storage.delete_study(src_study._study_id)
response.status = 201
new_study_summary = get_study_summary(storage, dst_study._study_id)
return serialize_study_summary(new_study_summary)
@app.delete("/api/studies/<study_id:int>")
@json_api_view
def delete_study(study_id: int) -> BottleViewReturn:
+23 -1
View File
@@ -8,6 +8,7 @@ import {
deleteStudyAPI,
saveStudyNoteAPI,
saveTrialNoteAPI,
renameStudyAPI,
} from "./apiClient"
import {
graphVisibilityState,
@@ -139,7 +140,7 @@ export const actionCreator = () => {
const deleteStudy = (studyId: number) => {
deleteStudyAPI(studyId)
.then((study) => {
.then(() => {
setStudySummaries(studySummaries.filter((s) => s.study_id !== studyId))
enqueueSnackbar(`Success to delete a study (id=${studyId})`, {
variant: "success",
@@ -153,6 +154,26 @@ export const actionCreator = () => {
})
}
const renameStudy = (studyId: number, studyName: string) => {
renameStudyAPI(studyId, studyName)
.then((study) => {
const newStudySummaries = [
...studySummaries.filter((s) => s.study_id !== studyId),
study,
]
setStudySummaries(newStudySummaries)
enqueueSnackbar(`Success to delete a study (id=${studyId})`, {
variant: "success",
})
})
.catch((err) => {
enqueueSnackbar(`Failed to rename study (id=${studyId})`, {
variant: "error",
})
console.log(err)
})
}
const getGraphVisibility = () => {
const localStoragePreferences = localStorage.getItem(
localStorageGraphVisibility
@@ -250,6 +271,7 @@ export const actionCreator = () => {
updateParamImportance,
createNewStudy,
deleteStudy,
renameStudy,
getGraphVisibility,
saveGraphVisibility,
saveStudyNote,
+33 -28
View File
@@ -91,19 +91,6 @@ interface StudySummariesResponse {
study_id: number
study_name: string
directions: StudyDirection[]
best_trial?: {
trial_id: number
study_id: number
number: number
state: TrialState
value?: number
intermediate_values: TrialIntermediateValue[]
datetime_start: string
datetime_complete?: string
params: TrialParam[]
user_attrs: Attribute[]
system_attrs: Attribute[]
}
user_attrs: Attribute[]
system_attrs: Attribute[]
datetime_start?: string
@@ -134,19 +121,6 @@ interface CreateNewStudyResponse {
study_id: number
study_name: string
directions: StudyDirection[]
best_trial?: {
trial_id: number
study_id: number
number: number
state: TrialState
value?: number
intermediate_values: TrialIntermediateValue[]
datetime_start: string
datetime_complete?: string
params: TrialParam[]
user_attrs: Attribute[]
system_attrs: Attribute[]
}
user_attrs: Attribute[]
system_attrs: Attribute[]
datetime_start?: string
@@ -178,12 +152,43 @@ export const createNewStudyAPI = (
})
}
export const deleteStudyAPI = (studyId: number) => {
export const deleteStudyAPI = (studyId: number): Promise<void> => {
return axiosInstance.delete(`/api/studies/${studyId}`).then((res) => {
return {}
return
})
}
type RenameStudyResponse = {
study_id: number
study_name: string
directions: StudyDirection[]
user_attrs: Attribute[]
system_attrs: Attribute[]
datetime_start?: string
}
export const renameStudyAPI = (
studyId: number,
studyName: string
): Promise<StudySummary> => {
return axiosInstance
.post<RenameStudyResponse>(`/api/studies/${studyId}/rename`, {
study_name: studyName,
})
.then((res) => {
return {
study_id: res.data.study_id,
study_name: res.data.study_name,
directions: res.data.directions,
user_attrs: res.data.user_attrs,
system_attrs: res.data.system_attrs,
datetime_start: res.data.datetime_start
? new Date(res.data.datetime_start)
: undefined,
}
})
}
export const saveStudyNoteAPI = (
studyId: number,
note: { version: number; body: string }
@@ -1,4 +1,4 @@
import React, { useState } from "react"
import React, { ReactNode, useState } from "react"
import {
Dialog,
useTheme,
@@ -19,9 +19,7 @@ import { studySummariesState } from "../state"
import RemoveIcon from "@mui/icons-material/Remove"
import AddIcon from "@mui/icons-material/Add"
type UsePreferenceDialogReturn = [() => void, () => JSX.Element]
export const useCreateStudyDialog = (): UsePreferenceDialogReturn => {
export const useCreateStudyDialog = (): [() => void, () => ReactNode] => {
const theme = useTheme()
const action = actionCreator()
@@ -1,4 +1,4 @@
import React, { useState } from "react"
import React, { ReactNode, useState } from "react"
import {
Dialog,
DialogTitle,
@@ -9,9 +9,10 @@ import {
} from "@mui/material"
import { actionCreator } from "../action"
type UsePreferenceDialogReturn = [(studyId: number) => void, () => JSX.Element]
export const useDeleteStudyDialog = (): UsePreferenceDialogReturn => {
export const useDeleteStudyDialog = (): [
(studyId: number) => void,
() => ReactNode
] => {
const action = actionCreator()
const [openDeleteStudyDialog, setOpenDeleteStudyDialog] = useState(false)
@@ -33,7 +34,7 @@ export const useDeleteStudyDialog = (): UsePreferenceDialogReturn => {
setOpenDeleteStudyDialog(true)
}
const renderCreateNewStudyDialog = () => {
const renderDeleteStudyDialog = () => {
return (
<Dialog
open={openDeleteStudyDialog}
@@ -59,5 +60,5 @@ export const useDeleteStudyDialog = (): UsePreferenceDialogReturn => {
</Dialog>
)
}
return [openDialog, renderCreateNewStudyDialog]
return [openDialog, renderDeleteStudyDialog]
}
@@ -0,0 +1,99 @@
import React, { ReactNode, useState } from "react"
import {
Dialog,
DialogTitle,
DialogContent,
DialogContentText,
Button,
DialogActions,
useTheme,
} from "@mui/material"
import { actionCreator } from "../action"
import { DebouncedInputTextField } from "./Debounce"
export const useRenameStudyDialog = (
studies: StudySummary[]
): [(studyId: number, studyName: string) => void, () => ReactNode] => {
const action = actionCreator()
const theme = useTheme()
const [openRenameStudyDialog, setOpenRenameStudyDialog] = useState(false)
const [renameStudyID, setRenameStudyID] = useState(-1)
const [prevStudyName, setPrevStudyName] = useState("")
const [newStudyName, setNewStudyName] = useState("")
const newStudyNameAlreadyUsed = studies.some(
(v) => v.study_name === newStudyName
)
const handleCloseRenameStudyDialog = () => {
setOpenRenameStudyDialog(false)
setRenameStudyID(-1)
setPrevStudyName("")
}
const handleRenameStudy = () => {
action.renameStudy(renameStudyID, newStudyName)
setOpenRenameStudyDialog(false)
setRenameStudyID(-1)
setPrevStudyName("")
}
const openDialog = (studyId: number, prevStudyName: string) => {
setRenameStudyID(studyId)
setPrevStudyName(prevStudyName)
setOpenRenameStudyDialog(true)
}
const renderRenameStudyDialog = () => {
return (
<Dialog
open={openRenameStudyDialog}
onClose={() => {
handleCloseRenameStudyDialog()
}}
aria-labelledby="rename-study-dialog-title"
>
<DialogTitle id="rename-study-dialog-title">
Rename "{prevStudyName}"
</DialogTitle>
<DialogContent>
<DialogContentText
sx={{ fontWeight: 600, marginBottom: theme.spacing(1) }}
>
Please note that the study_id will be changed because this function
internally creates a new study and copies all trials to it.
</DialogContentText>
<DialogContentText>
Please enter the new study name.
</DialogContentText>
<DebouncedInputTextField
onChange={(s) => {
setNewStudyName(s)
}}
delay={500}
textFieldProps={{
autoFocus: true,
fullWidth: true,
error: newStudyNameAlreadyUsed,
helperText: newStudyNameAlreadyUsed
? `"${newStudyName}" is already used`
: "",
label: "Study name",
type: "text",
}}
/>
</DialogContent>
<DialogActions>
<Button onClick={handleCloseRenameStudyDialog} color="primary">
Cancel
</Button>
<Button onClick={handleRenameStudy} color="primary">
Rename
</Button>
</DialogActions>
</Dialog>
)
}
return [openDialog, renderRenameStudyDialog]
}
@@ -22,6 +22,7 @@ import { Delete, Refresh, Search } from "@mui/icons-material"
import SortIcon from "@mui/icons-material/Sort"
import HomeIcon from "@mui/icons-material/Home"
import AddBoxIcon from "@mui/icons-material/AddBox"
import DriveFileRenameOutlineIcon from "@mui/icons-material/DriveFileRenameOutline"
import { actionCreator } from "../action"
import { DebouncedInputTextField } from "./Debounce"
@@ -30,6 +31,7 @@ import { styled } from "@mui/system"
import { AppDrawer } from "./AppDrawer"
import { useCreateStudyDialog } from "./CreateStudyDialog"
import { useDeleteStudyDialog } from "./DeleteStudyDialog"
import { useRenameStudyDialog } from "./RenameStudyDialog"
export const StudyListBeta: FC<{
toggleColorMode: () => void
@@ -47,16 +49,18 @@ export const StudyListBeta: FC<{
return row.study_name.indexOf(k) >= 0
})
}
const studies = useRecoilValue<StudySummary[]>(studySummariesState)
const [openCreateStudyDialog, renderCreateStudyDialog] =
useCreateStudyDialog()
const [openDeleteStudyDialog, renderDeleteStudyDialog] =
useDeleteStudyDialog()
const [openRenameStudyDialog, renderRenameStudyDialog] =
useRenameStudyDialog(studies)
const [sortBy, setSortBy] = useState<"id-asc" | "id-desc">("id-asc")
let studies = useRecoilValue<StudySummary[]>(studySummariesState)
studies = studies.filter((s) => !studyFilter(s))
let filteredStudies = studies.filter((s) => !studyFilter(s))
if (sortBy === "id-desc") {
studies = studies.reverse()
filteredStudies = filteredStudies.reverse()
}
useEffect(() => {
@@ -118,7 +122,7 @@ export const StudyListBeta: FC<{
<Card sx={{ margin: theme.spacing(2) }}>
<CardContent>
<Typography>
{`Thank you for testing the new UI! we would appreciate it if you could send us the feedback via `}
{`Thank you for testing the new UI! We would appreciate it if you could send us the feedback via `}
<MuiLink
target="_blank"
href="https://github.com/optuna/optuna-dashboard/discussions/332"
@@ -180,7 +184,7 @@ export const StudyListBeta: FC<{
</CardContent>
</Card>
<Box sx={{ display: "flex", flexWrap: "wrap" }}>
{studies.map((study) => (
{filteredStudies.map((study) => (
<Card
key={study.study_id}
sx={{ margin: theme.spacing(2), width: "500px" }}
@@ -207,6 +211,16 @@ export const StudyListBeta: FC<{
</CardActionArea>
<CardActions disableSpacing>
<Box sx={{ flexGrow: 1 }} />
<IconButton
aria-label="rename study"
size="small"
color="inherit"
onClick={() => {
openRenameStudyDialog(study.study_id, study.study_name)
}}
>
<DriveFileRenameOutlineIcon />
</IconButton>
<IconButton
aria-label="delete study"
size="small"
@@ -225,6 +239,7 @@ export const StudyListBeta: FC<{
</AppDrawer>
{renderCreateStudyDialog()}
{renderDeleteStudyDialog()}
{renderRenameStudyDialog()}
</Box>
)
}