mirror of
https://github.com/wassname/optuna-dashboard.git
synced 2026-09-23 13:30:25 +08:00
Add button to rename study
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user