Merge pull request #871 from porink0424/feat/APIClient

Made `apiClient` abstract
This commit is contained in:
c-bata
2024-04-24 17:30:59 +09:00
committed by GitHub
8 changed files with 614 additions and 518 deletions
+48 -50
View File
@@ -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<StudySummary[]>(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<void> => {
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<void> => {
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<FileReader>) => {
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<FileReader>) => {
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) =>
+126 -367
View File
@@ -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<APIMeta> => {
return axiosInstance
.get<APIMeta>(`/api/meta`)
.then<APIMeta>((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<StudyDetail> => {
return axiosInstance
.get<StudyDetailResponse>(`/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<StudySummary[]> => {
return axiosInstance
.get<StudySummariesResponse>(`/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<StudySummary> => {
return axiosInstance
.post<CreateNewStudyResponse>(`/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<void> => {
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<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,
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<void> => {
return axiosInstance
.put<void>(`/api/studies/${studyId}/note`, note)
.then(() => {
return
})
}
export const saveTrialNoteAPI = (
studyId: number,
trialId: number,
note: { version: number; body: string }
): Promise<void> => {
return axiosInstance
.put<void>(`/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<UploadArtifactAPIResponse> => {
return axiosInstance
.post<UploadArtifactAPIResponse>(`/api/artifacts/${studyId}/${trialId}`, {
file: dataUrl,
filename: fileName,
})
.then((res) => {
return res.data
})
}
export const uploadStudyArtifactAPI = (
studyId: number,
fileName: string,
dataUrl: string
): Promise<UploadArtifactAPIResponse> => {
return axiosInstance
.post<UploadArtifactAPIResponse>(`/api/artifacts/${studyId}`, {
file: dataUrl,
filename: fileName,
})
.then((res) => {
return res.data
})
}
export const deleteTrialArtifactAPI = (
studyId: number,
trialId: number,
artifactId: string
): Promise<void> => {
return axiosInstance
.delete<void>(`/api/artifacts/${studyId}/${trialId}/${artifactId}`)
.then(() => {
return
})
}
export const deleteStudyArtifactAPI = (
studyId: number,
artifactId: string
): Promise<void> => {
return axiosInstance
.delete<void>(`/api/artifacts/${studyId}/${artifactId}`)
.then(() => {
return
})
}
export const tellTrialAPI = (
trialId: number,
state: Optuna.TrialStateFinished,
values?: number[]
): Promise<void> => {
const req: { state: Optuna.TrialState; values?: number[] } = {
state: state,
values: values,
}
return axiosInstance
.post<void>(`/api/trials/${trialId}/tell`, req)
.then(() => {
return
})
}
export const saveTrialUserAttrsAPI = (
trialId: number,
user_attrs: { [key: string]: number | string }
): Promise<void> => {
const req = { user_attrs: user_attrs }
return axiosInstance
.post<void>(`/api/trials/${trialId}/user-attrs`, req)
.then(() => {
return
})
}
interface ParamImportancesResponse {
export interface ParamImportancesResponse {
param_importances: ParamImportance[][]
}
export const getParamImportances = (
studyId: number
): Promise<ParamImportance[][]> => {
return axiosInstance
.get<ParamImportancesResponse>(`/api/studies/${studyId}/param_importances`)
.then((res) => {
return res.data.param_importances
})
}
export const reportPreferenceAPI = (
studyId: number,
candidates: number[],
clicked: number
): Promise<void> => {
return axiosInstance
.post<void>(`/api/studies/${studyId}/preference`, {
candidates: candidates,
clicked: clicked,
mode: "ChooseWorst",
})
.then(() => {
return
})
}
export const skipPreferentialTrialAPI = (
studyId: number,
trialId: number
): Promise<void> => {
return axiosInstance
.post<void>(`/api/studies/${studyId}/${trialId}/skip`)
.then(() => {
return
})
}
export const removePreferentialHistoryAPI = (
studyId: number,
historyUuid: string
): Promise<void> => {
return axiosInstance
.delete<void>(`/api/studies/${studyId}/preference/${historyUuid}`)
.then(() => {
return
})
}
export const restorePreferentialHistoryAPI = (
studyId: number,
historyUuid: string
): Promise<void> => {
return axiosInstance
.post<void>(`/api/studies/${studyId}/preference/${historyUuid}`)
.then(() => {
return
})
}
export const reportFeedbackComponentAPI = (
studyId: number,
component_type: FeedbackComponentType
): Promise<void> => {
return axiosInstance
.put<void>(
`/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<PlotResponse> => {
return axiosInstance
.get<PlotResponse>(`/api/studies/${studyId}/plot/${plotType}`)
.then<PlotResponse>((res) => res.data)
}
export enum CompareStudiesPlotType {
EDF = "edf",
}
export const getCompareStudiesPlotAPI = (
studyIds: number[],
plotType: CompareStudiesPlotType
): Promise<PlotResponse> => {
return axiosInstance
.get<PlotResponse>(`/api/compare-studies/plot/${plotType}`, {
params: { study_ids: studyIds },
})
.then<PlotResponse>((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<APIMeta>
abstract getStudyDetail(
studyId: number,
nLocalTrials: number
): Promise<StudyDetail>
abstract getStudySummaries(): Promise<StudySummary[]>
abstract createNewStudy(
studyName: string,
directions: Optuna.StudyDirection[]
): Promise<StudySummary>
abstract deleteStudy(studyId: number): Promise<void>
abstract renameStudy(
studyId: number,
studyName: string
): Promise<StudySummary>
abstract saveStudyNote(studyId: number, note: Note): Promise<void>
abstract saveTrialNote(
studyId: number,
trialId: number,
note: Note
): Promise<void>
abstract uploadTrialArtifact(
studyId: number,
trialId: number,
fileName: string,
dataUrl: string
): Promise<UploadArtifactAPIResponse>
abstract uploadStudyArtifact(
studyId: number,
fileName: string,
dataUrl: string
): Promise<UploadArtifactAPIResponse>
abstract deleteTrialArtifact(
studyId: number,
trialId: number,
artifactId: string
): Promise<void>
abstract deleteStudyArtifact(
studyId: number,
artifactId: string
): Promise<void>
abstract tellTrial(
trialId: number,
state: Optuna.TrialStateFinished,
values?: number[]
): Promise<void>
abstract saveTrialUserAttrs(
trialId: number,
user_attrs: { [key: string]: number | string }
): Promise<void>
abstract getParamImportances(studyId: number): Promise<ParamImportance[][]>
abstract reportPreference(
studyId: number,
candidates: number[],
clicked: number
): Promise<void>
abstract skipPreferentialTrial(
studyId: number,
trialId: number
): Promise<void>
abstract removePreferentialHistory(
studyId: number,
historyUuid: string
): Promise<void>
abstract restorePreferentialHistory(
studyId: number,
historyUuid: string
): Promise<void>
abstract reportFeedbackComponent(
studyId: number,
component_type: FeedbackComponentType
): Promise<void>
abstract getPlot(studyId: number, plotType: PlotType): Promise<PlotResponse>
abstract getCompareStudiesPlot(
studyIds: number[],
plotType: CompareStudiesPlotType
): Promise<PlotResponse>
}
+32
View File
@@ -0,0 +1,32 @@
import React, { createContext, useContext } from "react"
import { APIClient } from "./apiClient"
type APIClientContextType = {
apiClient: APIClient
}
export const APIClientContext = createContext<APIClientContextType | undefined>(
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 (
<APIClientContext.Provider value={{ apiClient }}>
{children}
</APIClientContext.Provider>
)
}
+295
View File
@@ -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<APIMeta>(`/api/meta`)
.then<APIMeta>((res) => res.data)
getStudyDetail = (
studyId: number,
nLocalTrials: number
): Promise<StudyDetail> =>
this.axiosInstance
.get<StudyDetailResponse>(`/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<StudySummary[]> =>
this.axiosInstance
.get<StudySummariesResponse>(`/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<StudySummary> =>
this.axiosInstance
.post<CreateNewStudyResponse>(`/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<void> =>
this.axiosInstance.delete(`/api/studies/${studyId}`).then(() => {
return
})
renameStudy = (studyId: number, studyName: string): Promise<StudySummary> =>
this.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,
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<void> =>
this.axiosInstance
.put<void>(`/api/studies/${studyId}/note`, note)
.then(() => {
return
})
saveTrialNote = (
studyId: number,
trialId: number,
note: { version: number; body: string }
): Promise<void> =>
this.axiosInstance
.put<void>(`/api/studies/${studyId}/${trialId}/note`, note)
.then(() => {
return
})
uploadTrialArtifact = (
studyId: number,
trialId: number,
fileName: string,
dataUrl: string
): Promise<UploadArtifactAPIResponse> =>
this.axiosInstance
.post<UploadArtifactAPIResponse>(`/api/artifacts/${studyId}/${trialId}`, {
file: dataUrl,
filename: fileName,
})
.then((res) => {
return res.data
})
uploadStudyArtifact = (
studyId: number,
fileName: string,
dataUrl: string
): Promise<UploadArtifactAPIResponse> =>
this.axiosInstance
.post<UploadArtifactAPIResponse>(`/api/artifacts/${studyId}`, {
file: dataUrl,
filename: fileName,
})
.then((res) => {
return res.data
})
deleteTrialArtifact = (
studyId: number,
trialId: number,
artifactId: string
): Promise<void> =>
this.axiosInstance
.delete<void>(`/api/artifacts/${studyId}/${trialId}/${artifactId}`)
.then(() => {
return
})
deleteStudyArtifact = (studyId: number, artifactId: string): Promise<void> =>
this.axiosInstance
.delete<void>(`/api/artifacts/${studyId}/${artifactId}`)
.then(() => {
return
})
tellTrial = (
trialId: number,
state: Optuna.TrialStateFinished,
values?: number[]
): Promise<void> =>
this.axiosInstance
.post<void>(`/api/trials/${trialId}/tell`, {
state,
values,
})
.then(() => {
return
})
saveTrialUserAttrs = (
trialId: number,
user_attrs: { [key: string]: number | string }
): Promise<void> =>
this.axiosInstance
.post<void>(`/api/trials/${trialId}/user-attrs`, { user_attrs })
.then(() => {
return
})
getParamImportances = (studyId: number): Promise<ParamImportance[][]> =>
this.axiosInstance
.get<ParamImportancesResponse>(
`/api/studies/${studyId}/param_importances`
)
.then((res) => {
return res.data.param_importances
})
reportPreference = (
studyId: number,
candidates: number[],
clicked: number
): Promise<void> =>
this.axiosInstance
.post<void>(`/api/studies/${studyId}/preference`, {
candidates: candidates,
clicked: clicked,
mode: "ChooseWorst",
})
.then(() => {
return
})
skipPreferentialTrial = (studyId: number, trialId: number): Promise<void> =>
this.axiosInstance
.post<void>(`/api/studies/${studyId}/${trialId}/skip`)
.then(() => {
return
})
removePreferentialHistory = (
studyId: number,
historyUuid: string
): Promise<void> =>
this.axiosInstance
.delete<void>(`/api/studies/${studyId}/preference/${historyUuid}`)
.then(() => {
return
})
restorePreferentialHistory = (
studyId: number,
historyUuid: string
): Promise<void> =>
this.axiosInstance
.post<void>(`/api/studies/${studyId}/preference/${historyUuid}`)
.then(() => {
return
})
reportFeedbackComponent = (
studyId: number,
component_type: FeedbackComponentType
): Promise<void> =>
this.axiosInstance
.put<void>(
`/api/studies/${studyId}/preference_feedback_component`,
component_type
)
.then(() => {
return
})
getPlot = (studyId: number, plotType: PlotType): Promise<PlotResponse> =>
this.axiosInstance
.get<PlotResponse>(`/api/studies/${studyId}/plot/${plotType}`)
.then<PlotResponse>((res) => res.data)
getCompareStudiesPlot = (
studyIds: number[],
plotType: CompareStudiesPlotType
): Promise<PlotResponse> =>
this.axiosInstance
.get<PlotResponse>(`/api/compare-studies/plot/${plotType}`, {
params: { study_ids: studyIds },
})
.then<PlotResponse>((res) => res.data)
}
+101 -95
View File
@@ -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 (
<QueryClientProvider client={queryClient}>
<RecoilRoot>
<ThemeProvider theme={theme}>
<CssBaseline />
<Box
component="div"
sx={{
backgroundColor: colorMode === "dark" ? "#121212" : "#ffffff",
width: "100%",
minHeight: "100vh",
}}
>
<SnackbarProvider maxSnack={3}>
<Router>
<Routes>
<Route
path={URL_PREFIX + "/studies/:studyId/analytics"}
element={
<StudyDetail
toggleColorMode={toggleColorMode}
page={"analytics"}
/>
}
/>
<Route
path={URL_PREFIX + "/studies/:studyId/trials"}
element={
<StudyDetail
toggleColorMode={toggleColorMode}
page={"trialList"}
/>
}
/>
<Route
path={URL_PREFIX + "/studies/:studyId/trialTable"}
element={
<StudyDetail
toggleColorMode={toggleColorMode}
page={"trialTable"}
/>
}
/>
<Route
path={URL_PREFIX + "/studies/:studyId/note"}
element={
<StudyDetail
toggleColorMode={toggleColorMode}
page={"note"}
/>
}
/>
<Route
path={URL_PREFIX + "/studies/:studyId/graph"}
element={
<StudyDetail
toggleColorMode={toggleColorMode}
page={"graph"}
/>
}
/>
<Route
path={URL_PREFIX + "/studies/:studyId"}
element={
<StudyDetail
toggleColorMode={toggleColorMode}
page={"top"}
/>
}
/>
<Route
path={URL_PREFIX + "/studies/:studyId/preference-history"}
element={
<StudyDetail
toggleColorMode={toggleColorMode}
page={"preferenceHistory"}
/>
}
/>
<Route
path={URL_PREFIX + "/compare-studies"}
element={
<CompareStudies toggleColorMode={toggleColorMode} />
}
/>
<Route
path={URL_PREFIX + "/"}
element={<StudyList toggleColorMode={toggleColorMode} />}
/>
</Routes>
</Router>
</SnackbarProvider>
</Box>
</ThemeProvider>
</RecoilRoot>
</QueryClientProvider>
<APIClientProvider apiClient={axiosAPIClient}>
<QueryClientProvider client={queryClient}>
<RecoilRoot>
<ThemeProvider theme={theme}>
<CssBaseline />
<Box
component="div"
sx={{
backgroundColor: colorMode === "dark" ? "#121212" : "#ffffff",
width: "100%",
minHeight: "100vh",
}}
>
<SnackbarProvider maxSnack={3}>
<Router>
<Routes>
<Route
path={URL_PREFIX + "/studies/:studyId/analytics"}
element={
<StudyDetail
toggleColorMode={toggleColorMode}
page={"analytics"}
/>
}
/>
<Route
path={URL_PREFIX + "/studies/:studyId/trials"}
element={
<StudyDetail
toggleColorMode={toggleColorMode}
page={"trialList"}
/>
}
/>
<Route
path={URL_PREFIX + "/studies/:studyId/trialTable"}
element={
<StudyDetail
toggleColorMode={toggleColorMode}
page={"trialTable"}
/>
}
/>
<Route
path={URL_PREFIX + "/studies/:studyId/note"}
element={
<StudyDetail
toggleColorMode={toggleColorMode}
page={"note"}
/>
}
/>
<Route
path={URL_PREFIX + "/studies/:studyId/graph"}
element={
<StudyDetail
toggleColorMode={toggleColorMode}
page={"graph"}
/>
}
/>
<Route
path={URL_PREFIX + "/studies/:studyId"}
element={
<StudyDetail
toggleColorMode={toggleColorMode}
page={"top"}
/>
}
/>
<Route
path={URL_PREFIX + "/studies/:studyId/preference-history"}
element={
<StudyDetail
toggleColorMode={toggleColorMode}
page={"preferenceHistory"}
/>
}
/>
<Route
path={URL_PREFIX + "/compare-studies"}
element={
<CompareStudies toggleColorMode={toggleColorMode} />
}
/>
<Route
path={URL_PREFIX + "/"}
element={<StudyList toggleColorMode={toggleColorMode} />}
/>
</Routes>
</Router>
</SnackbarProvider>
</Box>
</ThemeProvider>
</RecoilRoot>
</QueryClientProvider>
</APIClientProvider>
)
}
+5 -2
View File
@@ -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)
})
@@ -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
})
+4 -2
View File
@@ -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