diff --git a/optuna_dashboard/ts/action.ts b/optuna_dashboard/ts/action.ts index e0819b37..091e7339 100644 --- a/optuna_dashboard/ts/action.ts +++ b/optuna_dashboard/ts/action.ts @@ -1,27 +1,7 @@ import * as Optuna from "@optuna/types" import { useSnackbar } from "notistack" import { useRecoilState, useSetRecoilState } from "recoil" -import { - createNewStudyAPI, - deleteStudyAPI, - deleteStudyArtifactAPI, - deleteTrialArtifactAPI, - getMetaInfoAPI, - getStudyDetailAPI, - getStudySummariesAPI, - removePreferentialHistoryAPI, - renameStudyAPI, - reportFeedbackComponentAPI, - reportPreferenceAPI, - restorePreferentialHistoryAPI, - saveStudyNoteAPI, - saveTrialNoteAPI, - saveTrialUserAttrsAPI, - skipPreferentialTrialAPI, - tellTrialAPI, - uploadStudyArtifactAPI, - uploadTrialArtifactAPI, -} from "./apiClient" +import { useAPIClient } from "./apiClientProvider" import { getDominatedTrials } from "./dominatedTrials" import { artifactIsAvailable, @@ -46,6 +26,7 @@ import { // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types export const actionCreator = () => { + const { apiClient } = useAPIClient() const { enqueueSnackbar } = useSnackbar() const [studySummaries, setStudySummaries] = useRecoilState(studySummariesState) @@ -218,7 +199,7 @@ export const actionCreator = () => { } const updateAPIMeta = () => { - getMetaInfoAPI().then((r) => { + apiClient.getMetaInfo().then((r) => { setArtifactIsAvailable(r.artifact_is_available) setPlotlypyIsAvailable(r.plotlypy_is_available) }) @@ -226,7 +207,8 @@ export const actionCreator = () => { const updateStudySummaries = (successMsg?: string) => { setStudySummariesLoading(true) - getStudySummariesAPI() + apiClient + .getStudySummaries() .then((studySummaries: StudySummary[]) => { setStudySummariesLoading(false) setStudySummaries(studySummaries) @@ -258,7 +240,8 @@ export const actionCreator = () => { nLocalFixedTrials = firstUpdatable === -1 ? currentTrials.length : firstUpdatable } - getStudyDetailAPI(studyId, nLocalFixedTrials) + apiClient + .getStudyDetail(studyId, nLocalFixedTrials) .then((study) => { setStudyDetailLoading({ ...studyDetailLoading, [studyId]: false }) const currentFixedTrials = @@ -284,7 +267,8 @@ export const actionCreator = () => { studyName: string, directions: Optuna.StudyDirection[] ) => { - createNewStudyAPI(studyName, directions) + apiClient + .createNewStudy(studyName, directions) .then((study_summary) => { const newVal = [...studySummaries, study_summary] setStudySummaries(newVal) @@ -301,7 +285,8 @@ export const actionCreator = () => { } const deleteStudy = (studyId: number) => { - deleteStudyAPI(studyId) + apiClient + .deleteStudy(studyId) .then(() => { setStudySummaries(studySummaries.filter((s) => s.study_id !== studyId)) enqueueSnackbar(`Success to delete a study (id=${studyId})`, { @@ -317,7 +302,8 @@ export const actionCreator = () => { } const renameStudy = (studyId: number, studyName: string) => { - renameStudyAPI(studyId, studyName) + apiClient + .renameStudy(studyId, studyName) .then((study) => { const newStudySummaries = [ ...studySummaries.filter((s) => s.study_id !== studyId), @@ -341,7 +327,8 @@ export const actionCreator = () => { } const saveStudyNote = (studyId: number, note: Note): Promise => { - return saveStudyNoteAPI(studyId, note) + return apiClient + .saveStudyNote(studyId, note) .then(() => { const newStudy = Object.assign({}, studyDetails[studyId]) newStudy.note = note @@ -371,7 +358,8 @@ export const actionCreator = () => { trialId: number, note: Note ): Promise => { - return saveTrialNoteAPI(studyId, trialId, note) + return apiClient + .saveTrialNote(studyId, trialId, note) .then(() => { const index = studyDetails[studyId].trials.findIndex( (t) => t.trial_id === trialId @@ -422,12 +410,13 @@ export const actionCreator = () => { setUploading(true) reader.readAsDataURL(file) reader.onload = (upload: ProgressEvent) => { - uploadTrialArtifactAPI( - studyId, - trialId, - file.name, - upload.target?.result as string - ) + apiClient + .uploadTrialArtifact( + studyId, + trialId, + file.name, + upload.target?.result as string + ) .then((res) => { setUploading(false) const index = studyDetails[studyId].trials.findIndex( @@ -455,11 +444,12 @@ export const actionCreator = () => { setUploading(true) reader.readAsDataURL(file) reader.onload = (upload: ProgressEvent) => { - uploadStudyArtifactAPI( - studyId, - file.name, - upload.target?.result as string - ) + apiClient + .uploadStudyArtifact( + studyId, + file.name, + upload.target?.result as string + ) .then((res) => { setUploading(false) setStudyArtifacts(studyId, res.artifacts) @@ -481,7 +471,8 @@ export const actionCreator = () => { trialId: number, artifactId: string ): void => { - deleteTrialArtifactAPI(studyId, trialId, artifactId) + apiClient + .deleteTrialArtifact(studyId, trialId, artifactId) .then(() => { deleteTrialArtifactState(studyId, trialId, artifactId) enqueueSnackbar(`Success to delete an artifact.`, { @@ -497,7 +488,8 @@ export const actionCreator = () => { } const deleteStudyArtifact = (studyId: number, artifactId: string): void => { - deleteStudyArtifactAPI(studyId, artifactId) + apiClient + .deleteStudyArtifact(studyId, artifactId) .then(() => { deleteStudyArtifactState(studyId, artifactId) enqueueSnackbar(`Success to delete an artifact.`, { @@ -515,7 +507,8 @@ export const actionCreator = () => { const makeTrialFail = (studyId: number, trialId: number): void => { const message = `id=${trialId}, state=Fail` setTrialUpdating(trialId, true) - tellTrialAPI(trialId, "Fail") + apiClient + .tellTrial(trialId, "Fail") .then(() => { const index = studyDetails[studyId].trials.findIndex( (t) => t.trial_id === trialId @@ -551,7 +544,8 @@ export const actionCreator = () => { ): void => { const message = `id=${trialId}, state=Complete, values=${values}` setTrialUpdating(trialId, true) - tellTrialAPI(trialId, "Complete", values) + apiClient + .tellTrial(trialId, "Complete", values) .then(() => { const index = studyDetails[studyId].trials.findIndex( (t) => t.trial_id === trialId @@ -584,7 +578,8 @@ export const actionCreator = () => { ): void => { const message = `id=${trialId}, user_attrs=${JSON.stringify(user_attrs)}` setTrialUpdating(trialId, true) - saveTrialUserAttrsAPI(trialId, user_attrs) + apiClient + .saveTrialUserAttrs(trialId, user_attrs) .then(() => { const index = studyDetails[studyId].trials.findIndex( (t) => t.trial_id === trialId @@ -618,7 +613,7 @@ export const actionCreator = () => { candidates: number[], clicked: number ) => { - reportPreferenceAPI(studyId, candidates, clicked).catch((err) => { + apiClient.reportPreference(studyId, candidates, clicked).catch((err) => { const reason = err.response?.data.reason enqueueSnackbar(`Failed to report preference. Reason: ${reason}`, { variant: "error", @@ -628,7 +623,7 @@ export const actionCreator = () => { } const skipPreferentialTrial = (studyId: number, trialId: number) => { - skipPreferentialTrialAPI(studyId, trialId).catch((err) => { + apiClient.skipPreferentialTrial(studyId, trialId).catch((err) => { const reason = err.response?.data.reason enqueueSnackbar(`Failed to skip trial. Reason: ${reason}`, { variant: "error", @@ -640,7 +635,8 @@ export const actionCreator = () => { studyId: number, compoennt_type: FeedbackComponentType ) => { - reportFeedbackComponentAPI(studyId, compoennt_type) + apiClient + .reportFeedbackComponent(studyId, compoennt_type) .then(() => { const newStudy = Object.assign({}, studyDetails[studyId]) newStudy.feedback_component_type = compoennt_type @@ -659,7 +655,8 @@ export const actionCreator = () => { } const removePreferentialHistory = (studyId: number, historyId: string) => { - removePreferentialHistoryAPI(studyId, historyId) + apiClient + .removePreferentialHistory(studyId, historyId) .then(() => { const newStudy = Object.assign({}, studyDetails[studyId]) newStudy.preference_history = newStudy.preference_history?.map((h) => @@ -683,7 +680,8 @@ export const actionCreator = () => { }) } const restorePreferentialHistory = (studyId: number, historyId: string) => { - restorePreferentialHistoryAPI(studyId, historyId) + apiClient + .restorePreferentialHistory(studyId, historyId) .then(() => { const newStudy = Object.assign({}, studyDetails[studyId]) newStudy.preference_history = newStudy.preference_history?.map((h) => diff --git a/optuna_dashboard/ts/apiClient.ts b/optuna_dashboard/ts/apiClient.ts index 982d1553..038ea9f7 100644 --- a/optuna_dashboard/ts/apiClient.ts +++ b/optuna_dashboard/ts/apiClient.ts @@ -1,5 +1,4 @@ import * as Optuna from "@optuna/types" -import axios from "axios" import * as plotly from "plotly.js-dist-min" import { Artifact, @@ -17,19 +16,11 @@ import { TrialParam, } from "./types/optuna" -const axiosInstance = axios.create({ baseURL: API_ENDPOINT }) - -type APIMeta = { +export type APIMeta = { artifact_is_available: boolean plotlypy_is_available: boolean } -export const getMetaInfoAPI = (): Promise => { - return axiosInstance - .get(`/api/meta`) - .then((res) => res.data) -} - interface TrialResponse { trial_id: number study_id: number @@ -50,29 +41,6 @@ interface TrialResponse { constraints: number[] } -const convertTrialResponse = (res: TrialResponse): Trial => { - return { - trial_id: res.trial_id, - study_id: res.study_id, - number: res.number, - state: res.state, - values: res.values, - intermediate_values: res.intermediate_values, - datetime_start: res.datetime_start - ? new Date(res.datetime_start) - : undefined, - datetime_complete: res.datetime_complete - ? new Date(res.datetime_complete) - : undefined, - params: res.params, - fixed_params: res.fixed_params, - user_attrs: res.user_attrs, - note: res.note, - artifacts: res.artifacts, - constraints: res.constraints, - } -} - interface PreferenceHistoryResponse { history: { id: string @@ -85,21 +53,7 @@ interface PreferenceHistoryResponse { is_removed: boolean } -const convertPreferenceHistory = ( - res: PreferenceHistoryResponse -): PreferenceHistory => { - return { - id: res.history.id, - candidates: res.history.candidates, - clicked: res.history.clicked, - feedback_mode: res.history.mode, - timestamp: new Date(res.history.timestamp), - preferences: res.history.preferences, - is_removed: res.is_removed, - } -} - -interface StudyDetailResponse { +export interface StudyDetailResponse { name: string datetime_start: string directions: Optuna.StudyDirection[] @@ -122,52 +76,7 @@ interface StudyDetailResponse { skipped_trial_numbers?: number[] } -export const getStudyDetailAPI = ( - studyId: number, - nLocalTrials: number -): Promise => { - return axiosInstance - .get(`/api/studies/${studyId}`, { - params: { - after: nLocalTrials, - }, - }) - .then((res) => { - const trials = res.data.trials.map((trial): Trial => { - return convertTrialResponse(trial) - }) - const best_trials = res.data.best_trials.map((trial): Trial => { - return convertTrialResponse(trial) - }) - return { - id: studyId, - name: res.data.name, - datetime_start: new Date(res.data.datetime_start), - directions: res.data.directions, - user_attrs: res.data.user_attrs, - trials: trials, - best_trials: best_trials, - union_search_space: res.data.union_search_space, - intersection_search_space: res.data.intersection_search_space, - union_user_attrs: res.data.union_user_attrs, - has_intermediate_values: res.data.has_intermediate_values, - note: res.data.note, - objective_names: res.data.objective_names, - form_widgets: res.data.form_widgets, - is_preferential: res.data.is_preferential, - feedback_component_type: res.data.feedback_component_type, - preferences: res.data.preferences, - preference_history: res.data.preference_history?.map( - convertPreferenceHistory - ), - plotly_graph_objects: res.data.plotly_graph_objects, - artifacts: res.data.artifacts, - skipped_trial_numbers: res.data.skipped_trial_numbers ?? [], - } - }) -} - -interface StudySummariesResponse { +export interface StudySummariesResponse { study_summaries: { study_id: number study_name: string @@ -178,26 +87,7 @@ interface StudySummariesResponse { }[] } -export const getStudySummariesAPI = (): Promise => { - return axiosInstance - .get(`/api/studies`, {}) - .then((res) => { - return res.data.study_summaries.map((study): StudySummary => { - return { - study_id: study.study_id, - study_name: study.study_name, - directions: study.directions, - user_attrs: study.user_attrs, - is_preferential: study.is_preferential, - datetime_start: study.datetime_start - ? new Date(study.datetime_start) - : undefined, - } - }) - }) -} - -interface CreateNewStudyResponse { +export interface CreateNewStudyResponse { study_summary: { study_id: number study_name: string @@ -208,38 +98,7 @@ interface CreateNewStudyResponse { } } -export const createNewStudyAPI = ( - studyName: string, - directions: Optuna.StudyDirection[] -): Promise => { - return axiosInstance - .post(`/api/studies`, { - study_name: studyName, - directions, - }) - .then((res) => { - const study_summary = res.data.study_summary - return { - study_id: study_summary.study_id, - study_name: study_summary.study_name, - directions: study_summary.directions, - // best_trial: undefined, - user_attrs: study_summary.user_attrs, - is_preferential: study_summary.is_preferential, - datetime_start: study_summary.datetime_start - ? new Date(study_summary.datetime_start) - : undefined, - } - }) -} - -export const deleteStudyAPI = (studyId: number): Promise => { - return axiosInstance.delete(`/api/studies/${studyId}`).then(() => { - return - }) -} - -type RenameStudyResponse = { +export type RenameStudyResponse = { study_id: number study_name: string directions: Optuna.StudyDirection[] @@ -248,220 +107,20 @@ type RenameStudyResponse = { datetime_start?: string } -export const renameStudyAPI = ( - studyId: number, - studyName: string -): Promise => { - return axiosInstance - .post(`/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, - is_preferential: res.data.is_prefential, - datetime_start: res.data.datetime_start - ? new Date(res.data.datetime_start) - : undefined, - } - }) -} - -export const saveStudyNoteAPI = ( - studyId: number, - note: { version: number; body: string } -): Promise => { - return axiosInstance - .put(`/api/studies/${studyId}/note`, note) - .then(() => { - return - }) -} - -export const saveTrialNoteAPI = ( - studyId: number, - trialId: number, - note: { version: number; body: string } -): Promise => { - return axiosInstance - .put(`/api/studies/${studyId}/${trialId}/note`, note) - .then(() => { - return - }) -} - -type UploadArtifactAPIResponse = { +export type UploadArtifactAPIResponse = { artifact_id: string artifacts: Artifact[] } -export const uploadTrialArtifactAPI = ( - studyId: number, - trialId: number, - fileName: string, - dataUrl: string -): Promise => { - return axiosInstance - .post(`/api/artifacts/${studyId}/${trialId}`, { - file: dataUrl, - filename: fileName, - }) - .then((res) => { - return res.data - }) -} - -export const uploadStudyArtifactAPI = ( - studyId: number, - fileName: string, - dataUrl: string -): Promise => { - return axiosInstance - .post(`/api/artifacts/${studyId}`, { - file: dataUrl, - filename: fileName, - }) - .then((res) => { - return res.data - }) -} - -export const deleteTrialArtifactAPI = ( - studyId: number, - trialId: number, - artifactId: string -): Promise => { - return axiosInstance - .delete(`/api/artifacts/${studyId}/${trialId}/${artifactId}`) - .then(() => { - return - }) -} - -export const deleteStudyArtifactAPI = ( - studyId: number, - artifactId: string -): Promise => { - return axiosInstance - .delete(`/api/artifacts/${studyId}/${artifactId}`) - .then(() => { - return - }) -} - -export const tellTrialAPI = ( - trialId: number, - state: Optuna.TrialStateFinished, - values?: number[] -): Promise => { - const req: { state: Optuna.TrialState; values?: number[] } = { - state: state, - values: values, - } - - return axiosInstance - .post(`/api/trials/${trialId}/tell`, req) - .then(() => { - return - }) -} - -export const saveTrialUserAttrsAPI = ( - trialId: number, - user_attrs: { [key: string]: number | string } -): Promise => { - const req = { user_attrs: user_attrs } - - return axiosInstance - .post(`/api/trials/${trialId}/user-attrs`, req) - .then(() => { - return - }) -} - -interface ParamImportancesResponse { +export interface ParamImportancesResponse { param_importances: ParamImportance[][] } -export const getParamImportances = ( - studyId: number -): Promise => { - return axiosInstance - .get(`/api/studies/${studyId}/param_importances`) - .then((res) => { - return res.data.param_importances - }) -} - -export const reportPreferenceAPI = ( - studyId: number, - candidates: number[], - clicked: number -): Promise => { - return axiosInstance - .post(`/api/studies/${studyId}/preference`, { - candidates: candidates, - clicked: clicked, - mode: "ChooseWorst", - }) - .then(() => { - return - }) -} - -export const skipPreferentialTrialAPI = ( - studyId: number, - trialId: number -): Promise => { - return axiosInstance - .post(`/api/studies/${studyId}/${trialId}/skip`) - .then(() => { - return - }) -} - -export const removePreferentialHistoryAPI = ( - studyId: number, - historyUuid: string -): Promise => { - return axiosInstance - .delete(`/api/studies/${studyId}/preference/${historyUuid}`) - .then(() => { - return - }) -} -export const restorePreferentialHistoryAPI = ( - studyId: number, - historyUuid: string -): Promise => { - return axiosInstance - .post(`/api/studies/${studyId}/preference/${historyUuid}`) - .then(() => { - return - }) -} - -export const reportFeedbackComponentAPI = ( - studyId: number, - component_type: FeedbackComponentType -): Promise => { - return axiosInstance - .put( - `/api/studies/${studyId}/preference_feedback_component`, - component_type - ) - .then(() => { - return - }) -} - -type PlotResponse = { +export type PlotResponse = { data: plotly.Data[] layout: plotly.Layout } + export enum PlotType { Contour = "contour", Slice = "slice", @@ -472,25 +131,125 @@ export enum PlotType { ParamImportances = "param_importances", ParetoFront = "pareto_front", } -export const getPlotAPI = ( - studyId: number, - plotType: PlotType -): Promise => { - return axiosInstance - .get(`/api/studies/${studyId}/plot/${plotType}`) - .then((res) => res.data) -} export enum CompareStudiesPlotType { EDF = "edf", } -export const getCompareStudiesPlotAPI = ( - studyIds: number[], - plotType: CompareStudiesPlotType -): Promise => { - return axiosInstance - .get(`/api/compare-studies/plot/${plotType}`, { - params: { study_ids: studyIds }, - }) - .then((res) => res.data) + +export abstract class APIClient { + constructor() {} + + convertTrialResponse(response: TrialResponse): Trial { + return { + trial_id: response.trial_id, + study_id: response.study_id, + number: response.number, + state: response.state, + values: response.values, + intermediate_values: response.intermediate_values, + datetime_start: response.datetime_start + ? new Date(response.datetime_start) + : undefined, + datetime_complete: response.datetime_complete + ? new Date(response.datetime_complete) + : undefined, + params: response.params, + fixed_params: response.fixed_params, + user_attrs: response.user_attrs, + note: response.note, + artifacts: response.artifacts, + constraints: response.constraints, + } + } + convertPreferenceHistory( + response: PreferenceHistoryResponse + ): PreferenceHistory { + return { + id: response.history.id, + candidates: response.history.candidates, + clicked: response.history.clicked, + feedback_mode: response.history.mode, + timestamp: new Date(response.history.timestamp), + preferences: response.history.preferences, + is_removed: response.is_removed, + } + } + + abstract getMetaInfo(): Promise + abstract getStudyDetail( + studyId: number, + nLocalTrials: number + ): Promise + abstract getStudySummaries(): Promise + abstract createNewStudy( + studyName: string, + directions: Optuna.StudyDirection[] + ): Promise + abstract deleteStudy(studyId: number): Promise + abstract renameStudy( + studyId: number, + studyName: string + ): Promise + abstract saveStudyNote(studyId: number, note: Note): Promise + abstract saveTrialNote( + studyId: number, + trialId: number, + note: Note + ): Promise + abstract uploadTrialArtifact( + studyId: number, + trialId: number, + fileName: string, + dataUrl: string + ): Promise + abstract uploadStudyArtifact( + studyId: number, + fileName: string, + dataUrl: string + ): Promise + abstract deleteTrialArtifact( + studyId: number, + trialId: number, + artifactId: string + ): Promise + abstract deleteStudyArtifact( + studyId: number, + artifactId: string + ): Promise + abstract tellTrial( + trialId: number, + state: Optuna.TrialStateFinished, + values?: number[] + ): Promise + abstract saveTrialUserAttrs( + trialId: number, + user_attrs: { [key: string]: number | string } + ): Promise + abstract getParamImportances(studyId: number): Promise + abstract reportPreference( + studyId: number, + candidates: number[], + clicked: number + ): Promise + abstract skipPreferentialTrial( + studyId: number, + trialId: number + ): Promise + abstract removePreferentialHistory( + studyId: number, + historyUuid: string + ): Promise + abstract restorePreferentialHistory( + studyId: number, + historyUuid: string + ): Promise + abstract reportFeedbackComponent( + studyId: number, + component_type: FeedbackComponentType + ): Promise + abstract getPlot(studyId: number, plotType: PlotType): Promise + abstract getCompareStudiesPlot( + studyIds: number[], + plotType: CompareStudiesPlotType + ): Promise } diff --git a/optuna_dashboard/ts/apiClientProvider.tsx b/optuna_dashboard/ts/apiClientProvider.tsx new file mode 100644 index 00000000..2383b1c6 --- /dev/null +++ b/optuna_dashboard/ts/apiClientProvider.tsx @@ -0,0 +1,32 @@ +import React, { createContext, useContext } from "react" +import { APIClient } from "./apiClient" + +type APIClientContextType = { + apiClient: APIClient +} + +export const APIClientContext = createContext( + undefined +) + +export const useAPIClient = (): APIClientContextType => { + const context = useContext(APIClientContext) + if (context === undefined) { + throw new Error("useAPIClient must be used within a APIClientProvider.") + } + return context +} + +export function APIClientProvider({ + apiClient, + children, +}: { + apiClient: APIClient + children: React.ReactNode +}) { + return ( + + {children} + + ) +} diff --git a/optuna_dashboard/ts/axiosClient.ts b/optuna_dashboard/ts/axiosClient.ts new file mode 100644 index 00000000..1c52c42f --- /dev/null +++ b/optuna_dashboard/ts/axiosClient.ts @@ -0,0 +1,295 @@ +import * as Optuna from "@optuna/types" +import axios, { AxiosInstance } from "axios" +import { + APIClient, + APIMeta, + CompareStudiesPlotType, + CreateNewStudyResponse, + ParamImportancesResponse, + PlotResponse, + PlotType, + RenameStudyResponse, + StudyDetailResponse, + StudySummariesResponse, + UploadArtifactAPIResponse, +} from "./apiClient" +import { + FeedbackComponentType, + ParamImportance, + StudyDetail, + StudySummary, + Trial, +} from "./types/optuna" + +export class AxiosClient extends APIClient { + private axiosInstance: AxiosInstance + + constructor() { + super() + this.axiosInstance = axios.create({ baseURL: API_ENDPOINT }) + } + + getMetaInfo = () => + this.axiosInstance + .get(`/api/meta`) + .then((res) => res.data) + getStudyDetail = ( + studyId: number, + nLocalTrials: number + ): Promise => + this.axiosInstance + .get(`/api/studies/${studyId}`, { + params: { + after: nLocalTrials, + }, + }) + .then((res) => { + const trials = res.data.trials.map((trial): Trial => { + return this.convertTrialResponse(trial) + }) + const best_trials = res.data.best_trials.map((trial): Trial => { + return this.convertTrialResponse(trial) + }) + return { + id: studyId, + name: res.data.name, + datetime_start: new Date(res.data.datetime_start), + directions: res.data.directions, + user_attrs: res.data.user_attrs, + trials: trials, + best_trials: best_trials, + union_search_space: res.data.union_search_space, + intersection_search_space: res.data.intersection_search_space, + union_user_attrs: res.data.union_user_attrs, + has_intermediate_values: res.data.has_intermediate_values, + note: res.data.note, + objective_names: res.data.objective_names, + form_widgets: res.data.form_widgets, + is_preferential: res.data.is_preferential, + feedback_component_type: res.data.feedback_component_type, + preferences: res.data.preferences, + preference_history: res.data.preference_history?.map( + this.convertPreferenceHistory + ), + plotly_graph_objects: res.data.plotly_graph_objects, + artifacts: res.data.artifacts, + skipped_trial_numbers: res.data.skipped_trial_numbers ?? [], + } + }) + getStudySummaries = (): Promise => + this.axiosInstance + .get(`/api/studies`, {}) + .then((res) => { + return res.data.study_summaries.map((study): StudySummary => { + return { + study_id: study.study_id, + study_name: study.study_name, + directions: study.directions, + user_attrs: study.user_attrs, + is_preferential: study.is_preferential, + datetime_start: study.datetime_start + ? new Date(study.datetime_start) + : undefined, + } + }) + }) + createNewStudy = ( + studyName: string, + directions: Optuna.StudyDirection[] + ): Promise => + this.axiosInstance + .post(`/api/studies`, { + study_name: studyName, + directions, + }) + .then((res) => { + const study_summary = res.data.study_summary + return { + study_id: study_summary.study_id, + study_name: study_summary.study_name, + directions: study_summary.directions, + // best_trial: undefined, + user_attrs: study_summary.user_attrs, + is_preferential: study_summary.is_preferential, + datetime_start: study_summary.datetime_start + ? new Date(study_summary.datetime_start) + : undefined, + } + }) + deleteStudy = (studyId: number): Promise => + this.axiosInstance.delete(`/api/studies/${studyId}`).then(() => { + return + }) + renameStudy = (studyId: number, studyName: string): Promise => + this.axiosInstance + .post(`/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, + is_preferential: res.data.is_prefential, + datetime_start: res.data.datetime_start + ? new Date(res.data.datetime_start) + : undefined, + } + }) + saveStudyNote = ( + studyId: number, + note: { version: number; body: string } + ): Promise => + this.axiosInstance + .put(`/api/studies/${studyId}/note`, note) + .then(() => { + return + }) + saveTrialNote = ( + studyId: number, + trialId: number, + note: { version: number; body: string } + ): Promise => + this.axiosInstance + .put(`/api/studies/${studyId}/${trialId}/note`, note) + .then(() => { + return + }) + uploadTrialArtifact = ( + studyId: number, + trialId: number, + fileName: string, + dataUrl: string + ): Promise => + this.axiosInstance + .post(`/api/artifacts/${studyId}/${trialId}`, { + file: dataUrl, + filename: fileName, + }) + .then((res) => { + return res.data + }) + uploadStudyArtifact = ( + studyId: number, + fileName: string, + dataUrl: string + ): Promise => + this.axiosInstance + .post(`/api/artifacts/${studyId}`, { + file: dataUrl, + filename: fileName, + }) + .then((res) => { + return res.data + }) + deleteTrialArtifact = ( + studyId: number, + trialId: number, + artifactId: string + ): Promise => + this.axiosInstance + .delete(`/api/artifacts/${studyId}/${trialId}/${artifactId}`) + .then(() => { + return + }) + deleteStudyArtifact = (studyId: number, artifactId: string): Promise => + this.axiosInstance + .delete(`/api/artifacts/${studyId}/${artifactId}`) + .then(() => { + return + }) + tellTrial = ( + trialId: number, + state: Optuna.TrialStateFinished, + values?: number[] + ): Promise => + this.axiosInstance + .post(`/api/trials/${trialId}/tell`, { + state, + values, + }) + .then(() => { + return + }) + saveTrialUserAttrs = ( + trialId: number, + user_attrs: { [key: string]: number | string } + ): Promise => + this.axiosInstance + .post(`/api/trials/${trialId}/user-attrs`, { user_attrs }) + .then(() => { + return + }) + getParamImportances = (studyId: number): Promise => + this.axiosInstance + .get( + `/api/studies/${studyId}/param_importances` + ) + .then((res) => { + return res.data.param_importances + }) + reportPreference = ( + studyId: number, + candidates: number[], + clicked: number + ): Promise => + this.axiosInstance + .post(`/api/studies/${studyId}/preference`, { + candidates: candidates, + clicked: clicked, + mode: "ChooseWorst", + }) + .then(() => { + return + }) + skipPreferentialTrial = (studyId: number, trialId: number): Promise => + this.axiosInstance + .post(`/api/studies/${studyId}/${trialId}/skip`) + .then(() => { + return + }) + removePreferentialHistory = ( + studyId: number, + historyUuid: string + ): Promise => + this.axiosInstance + .delete(`/api/studies/${studyId}/preference/${historyUuid}`) + .then(() => { + return + }) + restorePreferentialHistory = ( + studyId: number, + historyUuid: string + ): Promise => + this.axiosInstance + .post(`/api/studies/${studyId}/preference/${historyUuid}`) + .then(() => { + return + }) + reportFeedbackComponent = ( + studyId: number, + component_type: FeedbackComponentType + ): Promise => + this.axiosInstance + .put( + `/api/studies/${studyId}/preference_feedback_component`, + component_type + ) + .then(() => { + return + }) + getPlot = (studyId: number, plotType: PlotType): Promise => + this.axiosInstance + .get(`/api/studies/${studyId}/plot/${plotType}`) + .then((res) => res.data) + getCompareStudiesPlot = ( + studyIds: number[], + plotType: CompareStudiesPlotType + ): Promise => + this.axiosInstance + .get(`/api/compare-studies/plot/${plotType}`, { + params: { study_ids: studyIds }, + }) + .then((res) => res.data) +} diff --git a/optuna_dashboard/ts/components/App.tsx b/optuna_dashboard/ts/components/App.tsx index c915f7c7..1204ae8f 100644 --- a/optuna_dashboard/ts/components/App.tsx +++ b/optuna_dashboard/ts/components/App.tsx @@ -13,10 +13,14 @@ import { BrowserRouter as Router, Route, Routes } from "react-router-dom" import { RecoilRoot } from "recoil" import { QueryClient, QueryClientProvider } from "@tanstack/react-query" +import { APIClientProvider } from "../apiClientProvider" +import { AxiosClient } from "../axiosClient" import { CompareStudies } from "./CompareStudies" import { StudyDetail } from "./StudyDetail" import { StudyList } from "./StudyList" +const axiosAPIClient = new AxiosClient() + const queryClient = new QueryClient({ defaultOptions: { queries: { @@ -50,100 +54,102 @@ export const App: FC = () => { } return ( - - - - - - - - - - } - /> - - } - /> - - } - /> - - } - /> - - } - /> - - } - /> - - } - /> - - } - /> - } - /> - - - - - - - + + + + + + + + + + + } + /> + + } + /> + + } + /> + + } + /> + + } + /> + + } + /> + + } + /> + + } + /> + } + /> + + + + + + + + ) } diff --git a/optuna_dashboard/ts/components/GraphEdf.tsx b/optuna_dashboard/ts/components/GraphEdf.tsx index e3c17690..72b36b02 100644 --- a/optuna_dashboard/ts/components/GraphEdf.tsx +++ b/optuna_dashboard/ts/components/GraphEdf.tsx @@ -2,7 +2,8 @@ import { Box, Typography, useTheme } from "@mui/material" import * as plotly from "plotly.js-dist-min" import React, { FC, useEffect, useMemo } from "react" import { StudyDetail, Trial } from "ts/types/optuna" -import { CompareStudiesPlotType, getCompareStudiesPlotAPI } from "../apiClient" +import { CompareStudiesPlotType } from "../apiClient" +import { useAPIClient } from "../apiClientProvider" import { useGraphComponentState } from "../hooks/useGraphComponentState" import { useBackendRender, usePlotlyColorTheme } from "../state" import { Target, useFilteredTrialsFromStudies } from "../trialFilter" @@ -29,6 +30,7 @@ export const GraphEdf: FC<{ const GraphEdfBackend: FC<{ studies: StudyDetail[] }> = ({ studies }) => { + const { apiClient } = useAPIClient() const { graphComponentState, notifyGraphDidRender } = useGraphComponentState() const studyIds = studies.map((s) => s.id) @@ -43,7 +45,8 @@ const GraphEdfBackend: FC<{ return } if (graphComponentState !== "componentWillMount") { - getCompareStudiesPlotAPI(studyIds, CompareStudiesPlotType.EDF) + apiClient + .getCompareStudiesPlot(studyIds, CompareStudiesPlotType.EDF) .then(({ data, layout }) => { plotly.react(domId, data, layout).then(notifyGraphDidRender) }) diff --git a/optuna_dashboard/ts/hooks/useParamImportance.ts b/optuna_dashboard/ts/hooks/useParamImportance.ts index 50d37ac2..9cddaafa 100644 --- a/optuna_dashboard/ts/hooks/useParamImportance.ts +++ b/optuna_dashboard/ts/hooks/useParamImportance.ts @@ -3,12 +3,13 @@ import { AxiosError } from "axios" import { useSnackbar } from "notistack" import { useEffect } from "react" import { ParamImportance } from "ts/types/optuna" -import { getParamImportances } from "../apiClient" +import { useAPIClient } from "../apiClientProvider" export const useParamImportance = ({ numCompletedTrials, studyId, }: { numCompletedTrials: number; studyId: number }) => { + const { apiClient } = useAPIClient() const { enqueueSnackbar } = useSnackbar() const { data, isLoading, error } = useQuery< @@ -16,7 +17,7 @@ export const useParamImportance = ({ AxiosError<{ reason: string }> >({ queryKey: ["paramImportance", studyId, numCompletedTrials], - queryFn: () => getParamImportances(studyId), + queryFn: () => apiClient.getParamImportances(studyId), staleTime: Infinity, gcTime: 30 * 60 * 1000, // 30 minutes }) diff --git a/optuna_dashboard/ts/hooks/usePlot.ts b/optuna_dashboard/ts/hooks/usePlot.ts index 4b3588fa..e6ca5166 100644 --- a/optuna_dashboard/ts/hooks/usePlot.ts +++ b/optuna_dashboard/ts/hooks/usePlot.ts @@ -1,7 +1,8 @@ import { useQuery } from "@tanstack/react-query" import { AxiosError } from "axios" import * as plotly from "plotly.js-dist-min" -import { PlotType, getPlotAPI } from "../apiClient" +import { PlotType } from "../apiClient" +import { useAPIClient } from "../apiClientProvider" export const usePlot = ({ numCompletedTrials, @@ -12,6 +13,7 @@ export const usePlot = ({ studyId: number | undefined plotType: PlotType }) => { + const { apiClient } = useAPIClient() const { data, isLoading, error } = useQuery< { data: plotly.Data[]; layout: plotly.Layout }, AxiosError @@ -22,7 +24,7 @@ export const usePlot = ({ if (studyId === undefined) { return Promise.reject(new Error("Invalid studyId")) } - return getPlotAPI(studyId, plotType) + return apiClient.getPlot(studyId, plotType) }, staleTime: Infinity, gcTime: 30 * 60 * 1000, // 30 minutes