diff --git a/optuna_dashboard/_app.py b/optuna_dashboard/_app.py index d91710f6..c521775a 100644 --- a/optuna_dashboard/_app.py +++ b/optuna_dashboard/_app.py @@ -447,6 +447,23 @@ def create_app( response.status = 204 return {} + @app.post("/api/trials//user-attrs") + @json_api_view + def save_trial_user_attrs(trial_id: int) -> dict[str, Any]: + if "user_attrs" not in request.json: + response.status = 400 # Bad request + return {"reason": "user_attrs must be specified."} + + try: # TODO(knshnb): Proper error handling. + for key, val in request.json.get("user_attrs").items(): + storage.set_trial_user_attr(trial_id, key, val) + except Exception as e: + response.status = 500 + return {"reason": f"Internal server error: {e}"} + + response.status = 204 + return {} + @app.put("/api/studies///note") @json_api_view def save_trial_note(study_id: int, trial_id: int) -> dict[str, Any]: diff --git a/optuna_dashboard/ts/action.ts b/optuna_dashboard/ts/action.ts index e8f2aa35..1a4ba870 100644 --- a/optuna_dashboard/ts/action.ts +++ b/optuna_dashboard/ts/action.ts @@ -13,6 +13,7 @@ import { uploadArtifactAPI, getMetaInfoAPI, deleteArtifactAPI, + saveTrialUserAttrsAPI, } from "./apiClient" import { graphVisibilityState, @@ -482,6 +483,32 @@ export const actionCreator = () => { }) } + const saveTrialUserAttrs = ( + studyId: number, + trialId: number, + user_attrs: {[key: string]: number}, + ): void => { + console.log("user_attrs", user_attrs) + // TODO(knshnb): Update rendering of `user_attrs`. + const message = `id=${trialId}, user_attrs=${user_attrs}` + saveTrialUserAttrsAPI(trialId, user_attrs) + .then(() => { + // TODO(knshnb): Update states. + enqueueSnackbar(`Successfully updated trial (${message})`, { + variant: "success", + }) + }) + .catch((err) => { + const reason = err.response?.data.reason + enqueueSnackbar( + `Failed to update trial (${message}). Reason: ${reason}`, + { + variant: "error", + } + ) + console.log(err) + }) + } return { updateAPIMeta, updateStudyDetail, @@ -499,6 +526,7 @@ export const actionCreator = () => { uploadArtifact, deleteArtifact, tellTrial, + saveTrialUserAttrs, } } diff --git a/optuna_dashboard/ts/apiClient.ts b/optuna_dashboard/ts/apiClient.ts index e263117c..2cd32c53 100644 --- a/optuna_dashboard/ts/apiClient.ts +++ b/optuna_dashboard/ts/apiClient.ts @@ -281,6 +281,19 @@ export const tellTrialAPI = ( }) } +export const saveTrialUserAttrsAPI = ( + trialId: number, + user_attrs: { [key: string]: number } +): Promise => { + const req = { user_attrs: user_attrs } + + return axiosInstance + .post(`/api/trials/${trialId}/user-attrs`, req) + .then((res) => { + return + }) +} + interface ParamImportancesResponse { param_importances: ParamImportance[][] } diff --git a/optuna_dashboard/ts/components/ObjectiveForm.tsx b/optuna_dashboard/ts/components/ObjectiveForm.tsx index ae8b2ae8..e2bca3de 100644 --- a/optuna_dashboard/ts/components/ObjectiveForm.tsx +++ b/optuna_dashboard/ts/components/ObjectiveForm.tsx @@ -65,11 +65,8 @@ export const ObjectiveForm: FC<{ const handleSubmit = (e: React.MouseEvent): void => { e.preventDefault() - const filtered = values.filter((v): v is number => v !== null) - if (filtered.length !== directions.length) { - return - } - action.tellTrial(trial.study_id, trial.trial_id, "Complete", filtered) + const user_attrs = Object.fromEntries(widgets.map((widget, i) => [widget.description, values[i]])) + action.saveTrialUserAttrs(trial.study_id, trial.trial_id, user_attrs) } const getObjectiveName = (i: number): string => {