From 6e8bcc73b6fec6b3f53e7c101529607c333d9b2b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Jan 2025 15:21:01 +0000 Subject: [PATCH 01/13] Bump nanoid from 3.3.7 to 3.3.8 Bumps [nanoid](https://github.com/ai/nanoid) from 3.3.7 to 3.3.8. - [Release notes](https://github.com/ai/nanoid/releases) - [Changelog](https://github.com/ai/nanoid/blob/main/CHANGELOG.md) - [Commits](https://github.com/ai/nanoid/compare/3.3.7...3.3.8) --- updated-dependencies: - dependency-name: nanoid dependency-type: indirect ... Signed-off-by: dependabot[bot] --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 5c8c27dfa..ecea1fc27 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6582,11 +6582,11 @@ __metadata: linkType: hard "nanoid@npm:^3.3.7": - version: 3.3.7 - resolution: "nanoid@npm:3.3.7" + version: 3.3.8 + resolution: "nanoid@npm:3.3.8" bin: nanoid: bin/nanoid.cjs - checksum: 10c0/e3fb661aa083454f40500473bb69eedb85dc160e763150b9a2c567c7e9ff560ce028a9f833123b618a6ea742e311138b591910e795614a629029e86e180660f3 + checksum: 10c0/4b1bb29f6cfebf3be3bc4ad1f1296fb0a10a3043a79f34fbffe75d1621b4318319211cd420549459018ea3592f0d2f159247a6f874911d6d26eaaadda2478120 languageName: node linkType: hard From 1f1a1fd3f3e0a725608a0d0ed422f6ba570340c4 Mon Sep 17 00:00:00 2001 From: Joshua Kitenge Date: Mon, 20 Jan 2025 16:03:54 +0000 Subject: [PATCH 02/13] Error handling for OG - Create a ogApi axiosInstance - To manage 404 errors - set the authorisation headers - create handleOG_APIError to handle unknown errors - create the retryOG_APIErrors to hanlde retries - refactor all of the files in api directory to use ogApi --- public/mockServiceWorker.js | 2 +- src/App.tsx | 23 ++++- src/api/api.tsx | 91 +++++++++++++++++ src/api/channels.test.tsx | 12 --- src/api/channels.tsx | 57 ++++------- src/api/experiment.test.tsx | 4 - src/api/experiment.tsx | 23 +---- src/api/export.test.tsx | 28 ++---- src/api/export.tsx | 53 ++++------ src/api/favouriteFilters.tsx | 71 +++---------- src/api/functions.test.tsx | 4 - src/api/functions.tsx | 44 +++----- src/api/images.test.tsx | 12 --- src/api/images.tsx | 60 +++-------- src/api/records.test.tsx | 14 --- src/api/records.tsx | 58 ++++------- src/api/sessions.test.tsx | 20 ---- src/api/sessions.tsx | 91 ++++------------- src/api/userPreferences.test.tsx | 14 +-- src/api/userPreferences.tsx | 63 +++--------- src/api/waveforms.test.tsx | 4 - src/api/waveforms.tsx | 16 +-- src/handleOG_APIError.test.ts | 167 +++++++++++++++++++++++++++++++ src/handleOG_APIError.ts | 43 ++++++++ src/retryOG_APIErrors.test.ts | 47 +++++++++ src/retryOG_APIErrors.ts | 11 ++ src/state/scigateway.actions.tsx | 7 ++ 27 files changed, 537 insertions(+), 502 deletions(-) create mode 100644 src/api/api.tsx create mode 100644 src/handleOG_APIError.test.ts create mode 100644 src/handleOG_APIError.ts create mode 100644 src/retryOG_APIErrors.test.ts create mode 100644 src/retryOG_APIErrors.ts diff --git a/public/mockServiceWorker.js b/public/mockServiceWorker.js index b4281b637..ec47a9a50 100644 --- a/public/mockServiceWorker.js +++ b/public/mockServiceWorker.js @@ -8,7 +8,7 @@ * - Please do NOT serve this file on production. */ -const PACKAGE_VERSION = '2.6.7' +const PACKAGE_VERSION = '2.7.0' const INTEGRITY_CHECKSUM = '00729d72e3b82faf54ca8b9621dbb96f' const IS_MOCKED_RESPONSE = Symbol('isMockedResponse') const activeClientIds = new Set() diff --git a/src/App.tsx b/src/App.tsx index 2425b88b2..fcf4ec3eb 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -4,14 +4,25 @@ import { QueryClientProvider, } from '@tanstack/react-query'; import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; +import type { AxiosError } from 'axios'; import React from 'react'; import { connect, Provider } from 'react-redux'; +import { + clearFailedAuthRequestsQueue, + retryFailedAuthRequests, +} from './api/api'; import './App.css'; import { MicroFrontendId } from './app.types'; +import handleOG_APIError from './handleOG_APIError'; import OGThemeProvider from './ogThemeProvider.component'; import Preloader from './preloader/preloader.component'; +import retryOG_APIErrors from './retryOG_APIErrors'; import SettingsMenuItems from './settingsMenuItems.component'; -import { requestPluginRerender } from './state/scigateway.actions'; +import { + broadcastSignOut, + requestPluginRerender, + tokenRefreshed, +} from './state/scigateway.actions'; import { configureApp } from './state/slices/configSlice'; import { RootState, store } from './state/store'; import ViewTabs from './views/viewTabs.component'; @@ -23,12 +34,15 @@ const queryClient = new QueryClient({ queries: { refetchOnWindowFocus: true, staleTime: 300000, + retry: (failureCount, error) => { + return retryOG_APIErrors(failureCount, error as AxiosError); + }, }, }, - // TODO: implement proper error handling + queryCache: new QueryCache({ onError: (error) => { - console.log('Got error ' + error.message); + handleOG_APIError(error as AxiosError); }, }), }); @@ -58,7 +72,8 @@ const App: React.FunctionComponent = () => { const action = (e as CustomEvent).detail; if (requestPluginRerender.match(action)) { forceUpdate(); - } + } else if (tokenRefreshed.match(action)) retryFailedAuthRequests(); + else if (broadcastSignOut.match(action)) clearFailedAuthRequestsQueue(); } React.useEffect(() => { diff --git a/src/api/api.tsx b/src/api/api.tsx new file mode 100644 index 000000000..3f1be7433 --- /dev/null +++ b/src/api/api.tsx @@ -0,0 +1,91 @@ +import axios from 'axios'; +import { MicroFrontendId, type APIError } from '../app.types'; +import { readSciGatewayToken } from '../parseTokens'; +import { settings } from '../settings'; +import { InvalidateTokenType } from '../state/scigateway.actions'; + +// These are for ensuring refresh request is only sent once when multiple requests +// are failing due to 403's at the same time +let isFetchingAccessToken = false; +let failedAuthRequestQueue: ((shouldReject?: boolean) => void)[] = []; + +/* This should be called when SciGateway successfully refreshes the access token - it retries + all requests that failed due to an invalid token */ +export const retryFailedAuthRequests = () => { + isFetchingAccessToken = false; + failedAuthRequestQueue.forEach((callback) => callback()); + failedAuthRequestQueue = []; +}; + +/* This should be called when SciGateway logs out as would occur if a token refresh fails + due to the refresh token being out of date - it rejects all active request promises that + were awaiting a token refresh using the original error that occurred on the first attempt */ +export const clearFailedAuthRequestsQueue = () => { + isFetchingAccessToken = false; + failedAuthRequestQueue.forEach((callback) => callback(true)); + failedAuthRequestQueue = []; +}; + +export const ogApi = axios.create(); + +ogApi.interceptors.request.use(async (config) => { + const settingsData = await settings; + config.baseURL = settingsData ? settingsData.apiUrl : ''; + // const settingsData = await settings; + // config.baseURL = settingsData ? settingsData.apiUrl : ''; + config.headers['Authorization'] = `Bearer ${readSciGatewayToken()}`; + return config; +}); + +ogApi.interceptors.response.use( + (response) => response, + (error) => { + const originalRequest = error.config; + + const errorMessage: string = error.response?.data + ? Array.isArray((error.response.data as APIError).detail) + ? '' + : (( + (error.response.data as APIError).detail as string + )?.toLocaleLowerCase() ?? error.message) + : error.message; + + // Check if the token is invalid and needs refreshing + // only allow a request to be retried once. Don't retry if not logged + // in, it should not have been accessible + if ( + error.response?.status === 403 && + errorMessage.includes('expired token') && + !originalRequest._retried && + localStorage.getItem('scigateway:token') + ) { + originalRequest._retried = true; + + // Prevent other requests from also attempting to refresh while waiting for + // SciGateway to refresh the token + if (!isFetchingAccessToken) { + isFetchingAccessToken = true; + + // Request SciGateway to refresh the token + document.dispatchEvent( + new CustomEvent(MicroFrontendId, { + detail: { + type: InvalidateTokenType, + }, + }) + ); + } + + // Add request to queue to be resolved only once SciGateway has successfully + // refreshed the token + return new Promise((resolve, reject) => { + failedAuthRequestQueue.push((shouldReject?: boolean) => { + if (shouldReject) reject(error); + else resolve(ogApi(originalRequest)); + }); + }); + } + // Any other error + else return Promise.reject(error); + } +); diff --git a/src/api/channels.test.tsx b/src/api/channels.test.tsx index ff328faaf..92ddae25b 100644 --- a/src/api/channels.test.tsx +++ b/src/api/channels.test.tsx @@ -219,10 +219,6 @@ describe('channels api functions', () => { expect(result.current.data).toEqual([]); }); - - it.todo( - 'sends axios request to fetch channels and throws an appropriate error on failure' - ); }); describe('getScalarChannels', () => { @@ -310,10 +306,6 @@ describe('channels api functions', () => { expect(result.current.data).toEqual([]); }); - - it.todo( - 'sends axios request to fetch records and throws an appropriate error on failure' - ); }); describe('useChannelSummary', () => { @@ -363,10 +355,6 @@ describe('channels api functions', () => { expect(result.current.isPending).toBeTruthy(); expect(requestSent).toBe(false); }); - - it.todo( - 'sends axios request to fetch records and throws an appropriate error on failure' - ); }); describe('useScalarChannels', () => { diff --git a/src/api/channels.tsx b/src/api/channels.tsx index 5deaa8910..104717639 100644 --- a/src/api/channels.tsx +++ b/src/api/channels.tsx @@ -4,7 +4,7 @@ import { UseQueryResult, } from '@tanstack/react-query'; import { ColumnDef, createColumnHelper } from '@tanstack/react-table'; -import axios, { AxiosError } from 'axios'; +import { AxiosError } from 'axios'; import React from 'react'; import { FullChannelMetadata, @@ -16,9 +16,7 @@ import { timeChannelName, ValidateFunctionState, } from '../app.types'; -import { readSciGatewayToken } from '../parseTokens'; import { useAppDispatch, useAppSelector } from '../state/hooks'; -import { selectUrls } from '../state/slices/configSlice'; import { selectAppliedFunctions } from '../state/slices/functionsSlice'; import { openImageWindow, openTraceWindow } from '../state/slices/windowSlice'; import { AppDispatch } from '../state/store'; @@ -26,6 +24,7 @@ import { roundNumber, TraceOrImageThumbnail, } from '../table/cellRenderers/cellContentRenderers'; +import { ogApi } from './api'; import { convertExpressionsToStrings } from './functions'; interface ChannelsEndpoint { @@ -62,27 +61,17 @@ export const staticChannels: { [systemName: string]: FullChannelMetadata } = { }, }; -const fetchChannels = (apiUrl: string): Promise => { - return axios - .get(`${apiUrl}/channels`, { - headers: { - Authorization: `Bearer ${readSciGatewayToken()}`, - }, +const fetchChannels = async (): Promise => { + const response = await ogApi.get(`/channels`); + const { channels } = response.data; + if (!channels || Object.keys(channels).length === 0) return []; + const convertedChannels: FullChannelMetadata[] = Object.entries(channels).map( + ([systemName, channel]) => ({ + systemName, + ...channel, }) - .then((response) => { - const { channels } = response.data; - - if (!channels || Object.keys(channels).length === 0) return []; - - const convertedChannels: FullChannelMetadata[] = Object.entries( - channels - ).map(([systemName, channel]) => ({ - systemName, - ...channel, - })); - - return [...Object.values(staticChannels), ...convertedChannels]; - }); + ); + return [...Object.values(staticChannels), ...convertedChannels]; }; export interface ChannelSummary { @@ -91,19 +80,12 @@ export interface ChannelSummary { recent_sample: { [timestamp: string]: string | number }[]; } -const fetchChannelSummary = ( - apiUrl: string, +const fetchChannelSummary = async ( channel: string ): Promise => { - return axios - .get(`${apiUrl}/channels/summary/${channel}`, { - headers: { - Authorization: `Bearer ${readSciGatewayToken()}`, - }, - }) - .then((response) => { - return response.data; - }); + return ogApi + .get(`/channels/summary/${channel}`) + .then((response) => response.data); }; // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-constraint @@ -113,12 +95,10 @@ export const useChannels = ( 'queryKey' > ): UseQueryResult => { - const { apiUrl } = useAppSelector(selectUrls); - return useQuery({ queryKey: ['channels'], queryFn: () => { - return fetchChannels(apiUrl); + return fetchChannels(); }, ...(options ?? {}), @@ -128,7 +108,6 @@ export const useChannels = ( export const useChannelSummary = ( channel: string | undefined ): UseQueryResult => { - const { apiUrl } = useAppSelector(selectUrls); const dataChannel = typeof channel !== 'undefined' && !(channel in staticChannels) ? channel @@ -138,7 +117,7 @@ export const useChannelSummary = ( queryKey: ['channelSummary', dataChannel], queryFn: () => { - return fetchChannelSummary(apiUrl, dataChannel); + return fetchChannelSummary(dataChannel); }, enabled: dataChannel.length !== 0, diff --git a/src/api/experiment.test.tsx b/src/api/experiment.test.tsx index 6f89bcb30..9c9177a1a 100644 --- a/src/api/experiment.test.tsx +++ b/src/api/experiment.test.tsx @@ -23,9 +23,5 @@ describe('channels api functions', () => { expect(result.current.data).toEqual(expected); }); - - it.todo( - 'sends axios request to fetch records and throws an appropriate error on failure' - ); }); }); diff --git a/src/api/experiment.tsx b/src/api/experiment.tsx index 76e617e4a..204bdd4e6 100644 --- a/src/api/experiment.tsx +++ b/src/api/experiment.tsx @@ -1,31 +1,18 @@ import { useQuery, UseQueryResult } from '@tanstack/react-query'; -import axios, { AxiosError } from 'axios'; +import { AxiosError } from 'axios'; import { ExperimentParams } from '../app.types'; -import { readSciGatewayToken } from '../parseTokens'; -import { useAppSelector } from '../state/hooks'; -import { selectUrls } from '../state/slices/configSlice'; +import { ogApi } from './api'; -const fetchExperiment = (apiUrl: string): Promise => { - return axios - .get(`${apiUrl}/experiments`, { - headers: { - Authorization: `Bearer ${readSciGatewayToken()}`, - }, - }) - .then((response) => { - return response.data; - }); +const fetchExperiment = async (): Promise => { + return ogApi.get(`/experiments`).then((response) => response.data); }; export const useExperiment = (): UseQueryResult< ExperimentParams[], AxiosError > => { - const { apiUrl } = useAppSelector(selectUrls); - return useQuery({ queryKey: ['experiments'], - - queryFn: () => fetchExperiment(apiUrl), + queryFn: () => fetchExperiment(), }); }; diff --git a/src/api/export.test.tsx b/src/api/export.test.tsx index 1a003a821..da0030270 100644 --- a/src/api/export.test.tsx +++ b/src/api/export.test.tsx @@ -1,8 +1,8 @@ import { act, renderHook, waitFor } from '@testing-library/react'; -import axios from 'axios'; import { MockInstance } from 'vitest'; import { RootState } from '../state/store'; import { getInitialState, hooksWrapperWithProviders } from '../testUtils'; +import { ogApi } from './api'; import { useExportData } from './export'; describe('useExportData', () => { @@ -59,7 +59,7 @@ describe('useExportData', () => { else return document.body.originalAppendChild(node); }); - axiosGetSpy = vi.spyOn(axios, 'get'); + axiosGetSpy = vi.spyOn(ogApi, 'get'); }); afterEach(() => { @@ -73,7 +73,7 @@ describe('useExportData', () => { wrapper: hooksWrapperWithProviders(state), }); - expect(axios.get).not.toHaveBeenCalled(); + expect(ogApi.get).not.toHaveBeenCalled(); expect(result.current.isIdle).toBe(true); await act(async () => { @@ -120,9 +120,6 @@ describe('useExportData', () => { expect(axiosGetSpy).toHaveBeenCalledWith('/export', { params, - headers: { - Authorization: 'Bearer null', - }, responseType: 'blob', }); @@ -139,7 +136,7 @@ describe('useExportData', () => { wrapper: hooksWrapperWithProviders(state), }); - expect(axios.get).not.toHaveBeenCalled(); + expect(ogApi.get).not.toHaveBeenCalled(); expect(result.current.isIdle).toBe(true); await act(async () => { @@ -186,9 +183,6 @@ describe('useExportData', () => { expect(axiosGetSpy).toHaveBeenCalledWith('/export', { params, - headers: { - Authorization: 'Bearer null', - }, responseType: 'blob', }); @@ -205,7 +199,7 @@ describe('useExportData', () => { wrapper: hooksWrapperWithProviders(state), }); - expect(axios.get).not.toHaveBeenCalled(); + expect(ogApi.get).not.toHaveBeenCalled(); expect(result.current.isIdle).toBe(true); await act(async () => { @@ -253,9 +247,6 @@ describe('useExportData', () => { expect(axiosGetSpy).toHaveBeenCalledWith('/export', { params, - headers: { - Authorization: 'Bearer null', - }, responseType: 'blob', }); @@ -274,7 +265,7 @@ describe('useExportData', () => { wrapper: hooksWrapperWithProviders(state), }); - expect(axios.get).not.toHaveBeenCalled(); + expect(ogApi.get).not.toHaveBeenCalled(); expect(result.current.isIdle).toBe(true); await act(async () => { @@ -320,9 +311,6 @@ describe('useExportData', () => { expect(axiosGetSpy).toHaveBeenCalledWith('/export', { params, - headers: { - Authorization: 'Bearer null', - }, responseType: 'blob', }); @@ -333,8 +321,4 @@ describe('useExportData', () => { expect(mockLinkClick).toHaveBeenCalled(); expect(mockLinkRemove).toHaveBeenCalled(); }); - - it.todo( - 'sends request to export data and throws an appropriate error on failure' - ); }); diff --git a/src/api/export.tsx b/src/api/export.tsx index dcd82b439..8a9047003 100644 --- a/src/api/export.tsx +++ b/src/api/export.tsx @@ -1,12 +1,11 @@ -import axios, { AxiosError } from 'axios'; import { useMutation, UseMutationResult } from '@tanstack/react-query'; +import { AxiosError } from 'axios'; +import { SearchParams, SortType } from '../app.types'; import { useAppSelector } from '../state/hooks'; -import { selectUrls } from '../state/slices/configSlice'; -import { selectSelectedRows } from '../state/slices/selectionSlice'; import { selectQueryParams } from '../state/slices/searchSlice'; +import { selectSelectedRows } from '../state/slices/selectionSlice'; import { selectSelectedIdsIgnoreOrder } from '../state/slices/tableSlice'; -import { readSciGatewayToken } from '../parseTokens'; -import { SearchParams, SortType } from '../app.types'; +import { ogApi } from './api'; import { staticChannels } from './channels'; interface DataToExport { @@ -16,8 +15,7 @@ interface DataToExport { 'Waveform Images': boolean; } -export const exportData = ( - apiUrl: string, +export const exportData = async ( sort: SortType, searchParams: SearchParams, filters: string[], @@ -118,34 +116,24 @@ export const exportData = ( ); } - return axios - .get(`${apiUrl}/export`, { - params: queryParams, - headers: { - Authorization: `Bearer ${readSciGatewayToken()}`, - }, - responseType: 'blob', - }) - .then((response) => { - const href = URL.createObjectURL(response.data); - const link = document.createElement('a'); - link.href = href; - link.download = response.headers['content-disposition'] - .split('filename=')[1] - .slice(1, -1); - - link.style.display = 'none'; - - document.body.appendChild(link); - link.click(); - - link.remove(); - URL.revokeObjectURL(href); - }); + const response = await ogApi.get(`/export`, { + params: queryParams, + responseType: 'blob', + }); + const href = URL.createObjectURL(response.data); + const link = document.createElement('a'); + link.href = href; + link.download = response.headers['content-disposition'] + .split('filename=')[1] + .slice(1, -1); + link.style.display = 'none'; + document.body.appendChild(link); + link.click(); + link.remove(); + URL.revokeObjectURL(href); }; export const useExportData = (): UseMutationResult => { - const { apiUrl } = useAppSelector(selectUrls); const selectedRows = useAppSelector(selectSelectedRows); const { searchParams, page, resultsPerPage, sort, filters } = useAppSelector(selectQueryParams); @@ -171,7 +159,6 @@ export const useExportData = (): UseMutationResult => { : 0; return exportData( - apiUrl, sort, searchParams, filters, diff --git a/src/api/favouriteFilters.tsx b/src/api/favouriteFilters.tsx index 75d612480..de7b96b42 100644 --- a/src/api/favouriteFilters.tsx +++ b/src/api/favouriteFilters.tsx @@ -5,18 +5,15 @@ import { useQueryClient, UseQueryResult, } from '@tanstack/react-query'; -import axios, { AxiosError } from 'axios'; +import { AxiosError } from 'axios'; import { FavouriteFilter, FavouriteFilterPatch, FavouriteFilterPost, } from '../app.types'; -import { readSciGatewayToken } from '../parseTokens'; -import { useAppSelector } from '../state/hooks'; -import { selectUrls } from '../state/slices/configSlice'; +import { ogApi } from './api'; -const addFavouriteFilter = ( - apiUrl: string, +const addFavouriteFilter = async ( favouriteFilter: FavouriteFilterPost ): Promise => { const queryParams = new URLSearchParams(); @@ -24,15 +21,12 @@ const addFavouriteFilter = ( queryParams.append('name', favouriteFilter.name); queryParams.append('filter', favouriteFilter.filter); - return axios + return ogApi .post( - `${apiUrl}/users/filters`, + `/users/filters`, {}, { params: queryParams, - headers: { - Authorization: `Bearer ${readSciGatewayToken()}`, - }, } ) .then((response) => response.data); @@ -43,22 +37,17 @@ export const useAddFavouriteFilter = (): UseMutationResult< AxiosError, FavouriteFilterPost > => { - const { apiUrl } = useAppSelector(selectUrls); const queryClient = useQueryClient(); return useMutation({ mutationFn: (favouriteFilter: FavouriteFilterPost) => - addFavouriteFilter(apiUrl, favouriteFilter), - onError: (error) => { - console.log('Got error ' + error.message); - }, + addFavouriteFilter(favouriteFilter), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['favouriteFilters'] }); }, }); }; -const editFavouriteFilter = ( - apiUrl: string, +const editFavouriteFilter = async ( id: string, favouriteFilter: FavouriteFilterPatch ): Promise => { @@ -68,15 +57,12 @@ const editFavouriteFilter = ( if (favouriteFilter.filter) queryParams.append('filter', favouriteFilter.filter); - return axios + return ogApi .patch( - `${apiUrl}/users/filters/${id}`, + `/users/filters/${id}`, {}, { params: queryParams, - headers: { - Authorization: `Bearer ${readSciGatewayToken()}`, - }, } ) .then((response) => response.data); @@ -87,55 +73,34 @@ export const useEditFavouriteFilter = (): UseMutationResult< AxiosError, { id: string; favouriteFilter: FavouriteFilterPatch } > => { - const { apiUrl } = useAppSelector(selectUrls); const queryClient = useQueryClient(); return useMutation({ mutationFn: ({ id, favouriteFilter }) => - editFavouriteFilter(apiUrl, id, favouriteFilter), - onError: (error) => { - console.log('Got error ' + error.message); - }, + editFavouriteFilter(id, favouriteFilter), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['favouriteFilters'] }); }, }); }; -const fetchFavouriteFilters = (apiUrl: string): Promise => { - return axios - .get(`${apiUrl}/users/filters`, { - headers: { - Authorization: `Bearer ${readSciGatewayToken()}`, - }, - }) - .then((response) => { - return response.data; - }); +const fetchFavouriteFilters = async (): Promise => { + return ogApi.get(`/users/filters`).then((response) => response.data); }; export const useFavouriteFilters = (): UseQueryResult< FavouriteFilter[], AxiosError > => { - const { apiUrl } = useAppSelector(selectUrls); - return useQuery({ queryKey: ['favouriteFilters'], - queryFn: () => { - return fetchFavouriteFilters(apiUrl); + return fetchFavouriteFilters(); }, }); }; -const deleteFavouriteFilter = (apiUrl: string, id: string): Promise => { - return axios - .delete(`${apiUrl}/users/filters/${id}`, { - headers: { - Authorization: `Bearer ${readSciGatewayToken()}`, - }, - }) - .then((response) => response.data); +const deleteFavouriteFilter = async (id: string): Promise => { + return ogApi.delete(`/users/filters/${id}`).then((response) => response.data); }; export const useDeleteFavouriteFilter = (): UseMutationResult< @@ -143,13 +108,9 @@ export const useDeleteFavouriteFilter = (): UseMutationResult< AxiosError, string > => { - const { apiUrl } = useAppSelector(selectUrls); const queryClient = useQueryClient(); return useMutation({ - mutationFn: (id: string) => deleteFavouriteFilter(apiUrl, id), - onError: (error) => { - console.log('Got error ' + error.message); - }, + mutationFn: (id: string) => deleteFavouriteFilter(id), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['favouriteFilters'] }); }, diff --git a/src/api/functions.test.tsx b/src/api/functions.test.tsx index 82328dbfa..92b57c675 100644 --- a/src/api/functions.test.tsx +++ b/src/api/functions.test.tsx @@ -16,10 +16,6 @@ describe('useFunctionsTokens', () => { const expected: FunctionOperator[] = functionTokenJson; expect(result.current.data).toEqual(expected); }); - - it.todo( - 'sends axios request to fetch functions tokens and throws an appropriate error on failure' - ); }); describe('useValidateFunctions', () => { diff --git a/src/api/functions.tsx b/src/api/functions.tsx index 8d15e709a..8cb6ce693 100644 --- a/src/api/functions.tsx +++ b/src/api/functions.tsx @@ -4,16 +4,14 @@ import { useMutation, useQuery, } from '@tanstack/react-query'; -import axios, { AxiosError } from 'axios'; +import { AxiosError } from 'axios'; import { APIFunctionState, DataType, FunctionOperator, ValidateFunctionState, } from '../app.types'; -import { readSciGatewayToken } from '../parseTokens'; -import { useAppSelector } from '../state/hooks'; -import { selectUrls } from '../state/slices/configSlice'; +import { ogApi } from './api'; export function convertExpressionsToStrings( functionStates: ValidateFunctionState[] @@ -42,47 +40,31 @@ export function convertExpressionsToStrings( }; } -const getFunctionsTokens = (apiUrl: string): Promise => { - return axios - .get(`${apiUrl}/functions/tokens`, { - headers: { - Authorization: `Bearer ${readSciGatewayToken()}`, - }, - }) - .then((response) => { - return response.data; - }); +const getFunctionsTokens = async (): Promise => { + return ogApi.get(`/functions/tokens`).then((response) => { + return response.data; + }); }; export const useFunctionsTokens = (): UseQueryResult< FunctionOperator[], AxiosError > => { - const { apiUrl } = useAppSelector(selectUrls); - return useQuery({ queryKey: ['FunctionTokens'], queryFn: () => { - return getFunctionsTokens(apiUrl); + return getFunctionsTokens(); }, }); }; -const postValidateFunctions = ( - apiUrl: string, +const postValidateFunctions = async ( functions: ValidateFunctionState[] ): Promise => { const formattedFunctions = convertExpressionsToStrings(functions).functions; - - return axios - .post(`${apiUrl}/functions/validate`, formattedFunctions, { - headers: { - Authorization: `Bearer ${readSciGatewayToken()}`, - }, - }) - .then((response) => { - return response.data; - }); + return ogApi + .post(`/functions/validate`, formattedFunctions) + .then((response) => response.data); }; export const useValidateFunctions = (): UseMutationResult< @@ -90,11 +72,9 @@ export const useValidateFunctions = (): UseMutationResult< AxiosError, ValidateFunctionState[] > => { - const { apiUrl } = useAppSelector(selectUrls); - return useMutation({ mutationFn: (functions: ValidateFunctionState[]) => { - return postValidateFunctions(apiUrl, functions); + return postValidateFunctions(functions); }, }); }; diff --git a/src/api/images.test.tsx b/src/api/images.test.tsx index fc406771d..fef0b7d6f 100644 --- a/src/api/images.test.tsx +++ b/src/api/images.test.tsx @@ -147,10 +147,6 @@ describe('images api functions', () => { expect(result.current.data).toEqual('blob:testObjectUrl'); expect(new URL(request.url).searchParams).toEqual(params); }); - - it.todo( - 'sends axios request to fetch image and throws an appropriate error on failure' - ); }); describe('useColourBar', () => { @@ -188,10 +184,6 @@ describe('images api functions', () => { expect(result.current.data).toEqual('blob:testObjectUrl'); expect(new URL(request.url).searchParams).toEqual(params); }); - - it.todo( - 'sends axios request to fetch colourbar and throws an appropriate error on failure' - ); }); describe('useColourMaps', () => { @@ -206,9 +198,5 @@ describe('images api functions', () => { expect(result.current.data).toEqual(colourMapsJson); }); - - it.todo( - 'sends axios request to fetch colourmaps and throws an appropriate error on failure' - ); }); }); diff --git a/src/api/images.tsx b/src/api/images.tsx index 90937635b..49d0d4849 100644 --- a/src/api/images.tsx +++ b/src/api/images.tsx @@ -3,12 +3,11 @@ import { keepPreviousData, useQuery, } from '@tanstack/react-query'; -import axios, { AxiosError } from 'axios'; +import { AxiosError } from 'axios'; import { APIFunctionState } from '../app.types'; -import { readSciGatewayToken } from '../parseTokens'; import { useAppSelector } from '../state/hooks'; -import { selectUrls } from '../state/slices/configSlice'; import { selectQueryParams } from '../state/slices/searchSlice'; +import { ogApi } from './api'; export interface FalseColourParams { colourMap?: string; @@ -21,7 +20,6 @@ export interface ColourMapsParams { } export const fetchImage = async ( - apiUrl: string, recordId: string, channelName: string, functionsState: APIFunctionState, @@ -47,12 +45,9 @@ export const fetchImage = async ( params.append('functions', JSON.stringify(func)); }); - return axios - .get(`${apiUrl}/images/${recordId}/${channelName}`, { + return ogApi + .get(`/images/${recordId}/${channelName}`, { params, - headers: { - Authorization: `Bearer ${readSciGatewayToken()}`, - }, responseType: 'blob', }) .then((response) => { @@ -61,7 +56,6 @@ export const fetchImage = async ( }; export const fetchColourBar = async ( - apiUrl: string, falseColourParams: FalseColourParams ): Promise => { const params = new URLSearchParams(); @@ -71,12 +65,9 @@ export const fetchColourBar = async ( if (lowerLevel) params.set('lower_level', lowerLevel.toString()); if (upperLevel) params.set('upper_level', upperLevel.toString()); - return axios - .get(`${apiUrl}/images/colour_bar`, { + return ogApi + .get(`/images/colour_bar`, { params, - headers: { - Authorization: `Bearer ${readSciGatewayToken()}`, - }, responseType: 'blob', }) .then((response) => { @@ -84,18 +75,10 @@ export const fetchColourBar = async ( }); }; -export const fetchColourMaps = async ( - apiUrl: string -): Promise => { - return axios - .get(`${apiUrl}/images/colourmap_names`, { - headers: { - Authorization: `Bearer ${readSciGatewayToken()}`, - }, - }) - .then((response) => { - return response.data; - }); +export const fetchColourMaps = async (): Promise => { + return ogApi.get(`/images/colourmap_names`).then((response) => { + return response.data; + }); }; export const useImage = ( @@ -104,21 +87,11 @@ export const useImage = ( falseColourParams?: FalseColourParams ): UseQueryResult => { const { functions } = useAppSelector(selectQueryParams); - const { apiUrl } = useAppSelector(selectUrls); - return useQuery({ queryKey: ['images', recordId, channelName, functions, falseColourParams], - queryFn: () => { - return fetchImage( - apiUrl, - recordId, - channelName, - functions, - falseColourParams - ); + return fetchImage(recordId, channelName, functions, falseColourParams); }, - // set to display old image whilst new one is loading placeholderData: keepPreviousData, }); @@ -127,15 +100,11 @@ export const useImage = ( export const useColourBar = ( falseColourParams: FalseColourParams ): UseQueryResult => { - const { apiUrl } = useAppSelector(selectUrls); - return useQuery({ queryKey: ['colourbar', falseColourParams], - queryFn: () => { - return fetchColourBar(apiUrl, falseColourParams); + return fetchColourBar(falseColourParams); }, - // set to display old colour bar whilst new one is loading placeholderData: keepPreviousData, }); @@ -145,13 +114,10 @@ export const useColourMaps = (): UseQueryResult< ColourMapsParams, AxiosError > => { - const { apiUrl } = useAppSelector(selectUrls); - return useQuery({ queryKey: ['colourmaps'], - queryFn: () => { - return fetchColourMaps(apiUrl); + return fetchColourMaps(); }, }); }; diff --git a/src/api/records.test.tsx b/src/api/records.test.tsx index 6c3190503..aed3c7992 100644 --- a/src/api/records.test.tsx +++ b/src/api/records.test.tsx @@ -179,10 +179,6 @@ describe('records api functions', () => { incomingRecordCountResult.current.data ); }); - - it.todo( - 'sends axios request to fetch record count and throws an appropriate error on failure' - ); }); describe('useShotnumToDateConverter', () => { @@ -219,9 +215,6 @@ describe('records api functions', () => { expect(result.current.isPending).toBe(true); expect(result.current.fetchStatus).toBe('idle'); }); - it.todo( - 'sends axios request to fetch records and throws an appropriate error on failure' - ); }); describe('useDateToShotnumConverter', () => { @@ -258,9 +251,6 @@ describe('records api functions', () => { expect(result.current.isPending).toBe(true); expect(result.current.fetchStatus).toBe('idle'); }); - it.todo( - 'sends axios request to fetch records and throws an appropriate error on failure' - ); }); describe('useIncomingRecordCount', () => { @@ -425,10 +415,6 @@ describe('records api functions', () => { ); expect(result.current.data).toEqual(recordsJson.length); }); - - it.todo( - 'sends axios request to fetch incoming record count and throws an appropriate error on failure' - ); }); describe('useRecordsPaginated', () => { diff --git a/src/api/records.tsx b/src/api/records.tsx index 4ccfac004..06b7e0522 100644 --- a/src/api/records.tsx +++ b/src/api/records.tsx @@ -3,7 +3,7 @@ import { useQueryClient, UseQueryResult, } from '@tanstack/react-query'; -import axios, { AxiosError } from 'axios'; +import { AxiosError } from 'axios'; import { parseISO } from 'date-fns'; import { APIFunctionState, @@ -21,14 +21,13 @@ import { } from '../app.types'; import { readSciGatewayToken } from '../parseTokens'; import { useAppSelector } from '../state/hooks'; -import { selectUrls } from '../state/slices/configSlice'; import { selectQueryParams } from '../state/slices/searchSlice'; import { selectSelectedIdsIgnoreOrder } from '../state/slices/tableSlice'; import { renderTimestamp } from '../table/cellRenderers/cellContentRenderers'; +import { ogApi } from './api'; import { staticChannels } from './channels'; const fetchRecords = async ( - apiUrl: string, sort: SortType, searchParams: SearchParams, filters: string[], @@ -127,12 +126,9 @@ const fetchRecords = async ( ); } - return axios - .get(`${apiUrl}/records`, { + return ogApi + .get(`/records`, { params: queryParams, - headers: { - Authorization: `Bearer ${readSciGatewayToken()}`, - }, }) .then((response) => { const records: Record[] = response.data; @@ -140,8 +136,7 @@ const fetchRecords = async ( }); }; -const fetchRecordCountQuery = ( - apiUrl: string, +const fetchRecordCountQuery = async ( searchParams: SearchParams, filters: string[], functionsState: APIFunctionState, @@ -209,8 +204,8 @@ const fetchRecordCountQuery = ( queryParams.append('conditions', JSON.stringify(query)); } - return axios - .get(`${apiUrl}/records/count`, { + return ogApi + .get(`/records/count`, { params: queryParams, headers: { Authorization: `Bearer ${readSciGatewayToken()}`, @@ -219,8 +214,7 @@ const fetchRecordCountQuery = ( .then((response) => response.data); }; -export const fetchRangeRecordConverterQuery = ( - apiUrl: string, +export const fetchRangeRecordConverterQuery = async ( fromDate: string | undefined, toDate: string | undefined, shotnumMin: number | undefined, @@ -251,24 +245,22 @@ export const fetchRangeRecordConverterQuery = ( queryParams.append('shotnum_range', JSON.stringify(shotnumObj)); } - return axios - .get(`${apiUrl}/records/range_converter`, { + return ogApi + .get(`/records/range_converter`, { params: queryParams, headers: { Authorization: `Bearer ${readSciGatewayToken()}`, }, }) .then((response) => { - if (response.data) { - let inputRange; - if (fromDate || toDate) { - inputRange = { from: fromDate, to: toDate }; - } - if (shotnumMin || shotnumMax) { - inputRange = { min: shotnumMin, max: shotnumMax }; - } - return { ...inputRange, ...response.data }; + let inputRange; + if (fromDate || toDate) { + inputRange = { from: fromDate, to: toDate }; } + if (shotnumMin || shotnumMax) { + inputRange = { min: shotnumMin, max: shotnumMax }; + } + return { ...inputRange, ...response.data }; }); }; @@ -277,14 +269,11 @@ export const useDateToShotnumConverter = ( toDate: string | undefined, enabled?: boolean ): UseQueryResult => { - const { apiUrl } = useAppSelector(selectUrls); - return useQuery({ queryKey: ['dateToShotnumConverter', { fromDate, toDate }], queryFn: () => { return fetchRangeRecordConverterQuery( - apiUrl, fromDate, toDate, undefined, @@ -301,13 +290,10 @@ export const useShotnumToDateConverter = ( shotnumMax: number | undefined, enabled?: boolean ): UseQueryResult => { - const { apiUrl } = useAppSelector(selectUrls); - return useQuery({ queryKey: ['shotnumToDateConverter', { shotnumMin, shotnumMax }], queryFn: () => fetchRangeRecordConverterQuery( - apiUrl, undefined, undefined, shotnumMin, @@ -322,7 +308,6 @@ export const useRecordsPaginated = (): UseQueryResult< > => { const { searchParams, page, resultsPerPage, sort, filters, functions } = useAppSelector(selectQueryParams); - const { apiUrl } = useAppSelector(selectUrls); const projection = useAppSelector(selectSelectedIdsIgnoreOrder); return useQuery({ @@ -355,7 +340,6 @@ export const useRecordsPaginated = (): UseQueryResult< const startIndex = page * resultsPerPage; const stopIndex = startIndex + resultsPerPage; return fetchRecords( - apiUrl, sort, searchParams, filters, @@ -443,7 +427,6 @@ export const usePlotRecords = ( selectedPlotChannels: SelectedPlotChannel[], XAxis?: string ): UseQueryResult => { - const { apiUrl } = useAppSelector(selectUrls); const { searchParams, filters, functions } = useAppSelector(selectQueryParams); const parsedXAxis = XAxis ?? timeChannelName; @@ -483,7 +466,6 @@ export const usePlotRecords = ( }; } return fetchRecords( - apiUrl, sort as SortType, searchParams, filters, @@ -533,7 +515,6 @@ export const useThumbnails = ( ): UseQueryResult => { const { searchParams, sort, filters, functions } = useAppSelector(selectQueryParams); - const { apiUrl } = useAppSelector(selectUrls); return useQuery({ queryKey: [ @@ -564,7 +545,6 @@ export const useThumbnails = ( const startIndex = page * resultsPerPage; const stopIndex = startIndex + resultsPerPage; return fetchRecords( - apiUrl, sort, searchParams, filters, @@ -580,7 +560,6 @@ export const useThumbnails = ( }; export const useRecordCount = (): UseQueryResult => { - const { apiUrl } = useAppSelector(selectUrls); const { searchParams, filters, functions } = useAppSelector(selectQueryParams); const queryClient = useQueryClient(); @@ -598,7 +577,6 @@ export const useRecordCount = (): UseQueryResult => { projection: string[]; }; return fetchRecordCountQuery( - apiUrl, searchParams, filters, functions, @@ -623,7 +601,6 @@ export const useIncomingRecordCount = ( filters?: string[], searchParams?: SearchParams ): UseQueryResult => { - const { apiUrl } = useAppSelector(selectUrls); const { filters: storeFilters, searchParams: storeSearchParams, @@ -665,7 +642,6 @@ export const useIncomingRecordCount = ( }; return fetchRecordCountQuery( - apiUrl, searchParams, filters, functions, diff --git a/src/api/sessions.test.tsx b/src/api/sessions.test.tsx index fe038db8f..3567874c1 100644 --- a/src/api/sessions.test.tsx +++ b/src/api/sessions.test.tsx @@ -50,10 +50,6 @@ describe('session api functions', () => { expect(result.current.data).toEqual('1'); }); - - it.todo( - 'sends axios request to post user session and throws an appropriate error on failure' - ); }); describe('useEditSession', () => { @@ -71,10 +67,6 @@ describe('session api functions', () => { expect(result.current.data).toEqual('1'); }); - - it.todo( - 'sends axios request to patch user session and throws an appropriate error on failure' - ); }); describe('useDeleteSession', () => { @@ -92,10 +84,6 @@ describe('session api functions', () => { expect(result.current.data).toEqual(''); }); - - it.todo( - 'sends axios request to delete user session and throws an appropriate error on failure' - ); }); describe('useSessionList', () => { @@ -110,10 +98,6 @@ describe('session api functions', () => { const expected: SessionListItem[] = sessionsListJSON; expect(result.current.data).toEqual(expected); }); - - it.todo( - 'sends axios request to fetch sessions and throws an appropriate error on failure' - ); }); describe('useSession', () => { @@ -138,9 +122,5 @@ describe('session api functions', () => { expect(result.current.isPending).toBe(true); expect(result.current.fetchStatus).toBe('idle'); }); - - it.todo( - 'sends axios request to fetch sessions and throws an appropriate error on failure' - ); }); }); diff --git a/src/api/sessions.tsx b/src/api/sessions.tsx index 48827d927..4f53518ba 100644 --- a/src/api/sessions.tsx +++ b/src/api/sessions.tsx @@ -5,24 +5,20 @@ import { useQueryClient, UseQueryResult, } from '@tanstack/react-query'; -import axios, { AxiosError } from 'axios'; +import { AxiosError } from 'axios'; import { Session, SessionListItem, SessionResponse } from '../app.types'; import { readSciGatewayToken } from '../parseTokens'; -import { useAppSelector } from '../state/hooks'; -import { selectUrls } from '../state/slices/configSlice'; +import { ogApi } from './api'; -const saveSession = (apiUrl: string, session: Session): Promise => { +const saveSession = async (session: Session): Promise => { const queryParams = new URLSearchParams(); queryParams.append('name', session.name); queryParams.append('summary', session.summary); queryParams.append('auto_saved', session.auto_saved.toString()); - return axios - .post(`${apiUrl}/sessions`, session.session, { + return ogApi + .post(`/sessions`, session.session, { params: queryParams, - headers: { - Authorization: `Bearer ${readSciGatewayToken()}`, - }, }) .then((response) => response.data); }; @@ -32,35 +28,25 @@ export const useSaveSession = (): UseMutationResult< AxiosError, Session > => { - const { apiUrl } = useAppSelector(selectUrls); const queryClient = useQueryClient(); return useMutation({ - mutationFn: (session: Session) => saveSession(apiUrl, session), - onError: (error) => { - console.log('Got error ' + error.message); - }, + mutationFn: (session: Session) => saveSession(session), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['sessionList'] }); }, }); }; -const editSession = ( - apiUrl: string, - session: SessionResponse -): Promise => { +const editSession = async (session: SessionResponse): Promise => { const queryParams = new URLSearchParams(); queryParams.append('name', session.name); queryParams.append('summary', session.summary); queryParams.append('auto_saved', session.auto_saved.toString()); - return axios - .patch(`${apiUrl}/sessions/${session._id}`, session.session, { + return ogApi + .patch(`/sessions/${session._id}`, session.session, { params: queryParams, - headers: { - Authorization: `Bearer ${readSciGatewayToken()}`, - }, }) .then((response) => response.data); }; @@ -70,13 +56,9 @@ export const useEditSession = (): UseMutationResult< AxiosError, SessionResponse > => { - const { apiUrl } = useAppSelector(selectUrls); const queryClient = useQueryClient(); return useMutation({ - mutationFn: (session: SessionResponse) => editSession(apiUrl, session), - onError: (error) => { - console.log('Got error ' + error.message); - }, + mutationFn: (session: SessionResponse) => editSession(session), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['sessionList'] }); queryClient.invalidateQueries({ queryKey: ['session'] }); @@ -84,16 +66,9 @@ export const useEditSession = (): UseMutationResult< }); }; -const deleteSession = ( - apiUrl: string, - session: SessionResponse -): Promise => { - return axios - .delete(`${apiUrl}/sessions/${session._id}`, { - headers: { - Authorization: `Bearer ${readSciGatewayToken()}`, - }, - }) +const deleteSession = async (session: SessionResponse): Promise => { + return ogApi + .delete(`/sessions/${session._id}`) .then((response) => response.data); }; @@ -102,73 +77,51 @@ export const useDeleteSession = (): UseMutationResult< AxiosError, SessionResponse > => { - const { apiUrl } = useAppSelector(selectUrls); const queryClient = useQueryClient(); return useMutation({ - mutationFn: (session: SessionResponse) => deleteSession(apiUrl, session), - onError: (error) => { - console.log('Got error ' + error.message); - }, + mutationFn: (session: SessionResponse) => deleteSession(session), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['sessionList'] }); }, }); }; -const fetchSessionList = (apiUrl: string): Promise => { - return axios - .get(`${apiUrl}/sessions/list`, { - headers: { - Authorization: `Bearer ${readSciGatewayToken()}`, - }, - }) - .then((response) => { - return response.data; - }); +const fetchSessionList = async (): Promise => { + return ogApi.get(`/sessions/list`).then((response) => response.data); }; export const useSessionList = (): UseQueryResult< SessionListItem[], AxiosError > => { - const { apiUrl } = useAppSelector(selectUrls); - return useQuery({ queryKey: ['sessionList'], - queryFn: () => { - return fetchSessionList(apiUrl); + return fetchSessionList(); }, }); }; -const fetchSession = ( - apiUrl: string, +const fetchSession = async ( sessionId: string | undefined ): Promise => { - return axios - .get(`${apiUrl}/sessions/${sessionId}`, { + return ogApi + .get(`/sessions/${sessionId}`, { headers: { Authorization: `Bearer ${readSciGatewayToken()}`, }, }) - .then((response) => { - return response.data; - }); + .then((response) => response.data); }; export const useSession = ( session_id: string | undefined ): UseQueryResult => { - const { apiUrl } = useAppSelector(selectUrls); - return useQuery({ queryKey: ['session', session_id], - queryFn: () => { - return fetchSession(apiUrl, session_id); + return fetchSession(session_id); }, - enabled: typeof session_id !== 'undefined', }); }; diff --git a/src/api/userPreferences.test.tsx b/src/api/userPreferences.test.tsx index d8aaaaa41..1ed861f4b 100644 --- a/src/api/userPreferences.test.tsx +++ b/src/api/userPreferences.test.tsx @@ -1,13 +1,13 @@ import { renderHook, waitFor } from '@testing-library/react'; -import axios from 'axios'; import { setMockedPreferredColourMap } from '../mocks/handlers'; import { PREFERRED_COLOUR_MAP_PREFERENCE_NAME } from '../settingsMenuItems.component'; import { hooksWrapperWithProviders } from '../testUtils'; +import { ogApi } from './api'; import { useUpdateUserPreference, useUserPreference } from './userPreferences'; describe('user preferences api functions', () => { - const axiosPost = vi.spyOn(axios, 'post'); - const axiosDelete = vi.spyOn(axios, 'delete'); + const axiosPost = vi.spyOn(ogApi, 'post'); + const axiosDelete = vi.spyOn(ogApi, 'delete'); afterEach(() => { vi.clearAllMocks(); @@ -46,10 +46,6 @@ describe('user preferences api functions', () => { }); expect(result.current.data).toEqual(null); }); - - it.todo( - 'sends axios request to fetch user preference and throws an appropriate error on failure' - ); }); describe('useUpdateUserPreference', () => { @@ -90,9 +86,5 @@ describe('user preferences api functions', () => { expect(result.current.data).toEqual(''); expect(axiosDelete).toHaveBeenCalled(); }); - - it.todo( - 'sends axios request to update user preference and throws an appropriate error on failure' - ); }); }); diff --git a/src/api/userPreferences.tsx b/src/api/userPreferences.tsx index 66d80ac99..6f5e50d96 100644 --- a/src/api/userPreferences.tsx +++ b/src/api/userPreferences.tsx @@ -5,23 +5,16 @@ import { useQuery, useQueryClient, } from '@tanstack/react-query'; -import axios, { AxiosError, isAxiosError } from 'axios'; -import { readSciGatewayToken } from '../parseTokens'; -import { useAppSelector } from '../state/hooks'; -import { selectUrls } from '../state/slices/configSlice'; +import { AxiosError, isAxiosError } from 'axios'; +import { ogApi } from './api'; // make all these functions generic, as we can store multiple types as user preferences export const fetchUserPreference = async ( - apiUrl: string, name: string ): Promise => { - return axios - .get(`${apiUrl}/users/preferences/${name}`, { - headers: { - Authorization: `Bearer ${readSciGatewayToken()}`, - }, - }) + return ogApi + .get(`/users/preferences/${name}`) .then((response) => { return response.data; }) @@ -38,69 +31,41 @@ export const fetchUserPreference = async ( export const useUserPreference = ( name: string ): UseQueryResult => { - const { apiUrl } = useAppSelector(selectUrls); - return useQuery({ queryKey: ['userPreference', name], - queryFn: () => { - return fetchUserPreference(apiUrl, name); + return fetchUserPreference(name); }, }); }; export const updateUserPreference = async ( - apiUrl: string, name: string, value: T ): Promise => { - return axios - .post( - `${apiUrl}/users/preferences`, - { name, value }, - { - headers: { - Authorization: `Bearer ${readSciGatewayToken()}`, - }, - } - ) - .then((response) => { - return response.data; - }); + return ogApi.post(`/users/preferences`, { name, value }).then((response) => { + return response.data; + }); }; -export const deleteUserPreference = async ( - apiUrl: string, - name: string -): Promise => { - return axios - .delete(`${apiUrl}/users/preferences/${name}`, { - headers: { - Authorization: `Bearer ${readSciGatewayToken()}`, - }, - }) - .then((response) => { - return response.data; - }); +export const deleteUserPreference = async (name: string): Promise => { + return ogApi.delete(`/users/preferences/${name}`).then((response) => { + return response.data; + }); }; export const useUpdateUserPreference = ( name: string ): UseMutationResult => { const queryClient = useQueryClient(); - const { apiUrl } = useAppSelector(selectUrls); - return useMutation({ mutationFn: ({ value }: { value: T }) => { if (value !== null) { - return updateUserPreference(apiUrl, name, value); + return updateUserPreference(name, value); } else { - return deleteUserPreference(apiUrl, name); + return deleteUserPreference(name); } }, - onError: (error) => { - console.log('Got error ' + error.message); - }, onSuccess: (_data, vars) => { queryClient.setQueryData(['userPreference', name], vars.value); }, diff --git a/src/api/waveforms.test.tsx b/src/api/waveforms.test.tsx index db74e7d85..e17d4e82f 100644 --- a/src/api/waveforms.test.tsx +++ b/src/api/waveforms.test.tsx @@ -83,9 +83,5 @@ describe('waveform api functions', () => { y: [2, 10, 8, 7, 1, 4, 5, 3, 6, 9], }); }); - - it.todo( - 'sends axios request to fetch waveform and throws an appropriate error on failure' - ); }); }); diff --git a/src/api/waveforms.tsx b/src/api/waveforms.tsx index e6b52feca..a0bb583dd 100644 --- a/src/api/waveforms.tsx +++ b/src/api/waveforms.tsx @@ -1,13 +1,11 @@ import { UseQueryResult, useQuery } from '@tanstack/react-query'; -import axios, { AxiosError } from 'axios'; +import { AxiosError } from 'axios'; import { APIFunctionState, Waveform } from '../app.types'; -import { readSciGatewayToken } from '../parseTokens'; import { useAppSelector } from '../state/hooks'; -import { selectUrls } from '../state/slices/configSlice'; import { selectQueryParams } from '../state/slices/searchSlice'; +import { ogApi } from './api'; export const fetchWaveform = async ( - apiUrl: string, recordId: string, channelName: string, functionsState: APIFunctionState @@ -16,12 +14,9 @@ export const fetchWaveform = async ( functionsState.functions.forEach((func) => { queryParams.append('functions', JSON.stringify(func)); }); - return axios - .get(`${apiUrl}/waveforms/${recordId}/${channelName}`, { + return ogApi + .get(`/waveforms/${recordId}/${channelName}`, { params: queryParams, - headers: { - Authorization: `Bearer ${readSciGatewayToken()}`, - }, }) .then((response) => { return response.data; @@ -33,13 +28,12 @@ export const useWaveform = ( channelName: string ): UseQueryResult => { const { functions } = useAppSelector(selectQueryParams); - const { apiUrl } = useAppSelector(selectUrls); return useQuery({ queryKey: ['waveforms', recordId, channelName, functions], queryFn: () => { - return fetchWaveform(apiUrl, recordId, channelName, functions); + return fetchWaveform(recordId, channelName, functions); }, }); }; diff --git a/src/handleOG_APIError.test.ts b/src/handleOG_APIError.test.ts new file mode 100644 index 000000000..185751e38 --- /dev/null +++ b/src/handleOG_APIError.test.ts @@ -0,0 +1,167 @@ +import { AxiosError } from 'axios'; +import log from 'loglevel'; +import { UnknownAction } from 'redux'; +import handleOG_APIError from './handleOG_APIError'; +import { NotificationType } from './state/scigateway.actions'; + +vi.mock('loglevel'); + +describe('handleOG_APIError', () => { + let error: AxiosError; + let events: CustomEvent[] = []; + + beforeEach(() => { + events = []; + + document.dispatchEvent = (e: Event) => { + events.push(e as CustomEvent); + return true; + }; + + error = { + isAxiosError: true, + response: { + data: { detail: 'Test error message (response data)' }, + status: 404, + statusText: 'Not found', + headers: {}, + // @ts-expect-error: not needed for test + config: {}, + }, + name: 'Test error name', + message: 'Test error message', + toJSON: vi.fn(), + }; + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it('logs an error and sends a notification to SciGateway', () => { + handleOG_APIError(error); + + expect(log.error).toHaveBeenCalledWith( + 'Test error message (response data)' + ); + expect(events.length).toBe(1); + expect(events[0].detail).toEqual({ + type: NotificationType, + payload: { + severity: 'error', + message: 'Test error message (response data)', + }, + }); + }); + + it('does not broadcast 403 errors', () => { + error = { + isAxiosError: true, + response: { + data: {}, + status: 403, + statusText: 'Invalid token or expired token', + headers: {}, + // @ts-expect-error: not needed for test + config: {}, + }, + name: 'Test error name', + message: 'Test error message', + toJSON: vi.fn(), + }; + + handleOG_APIError(error); + + expect(log.error).toHaveBeenCalledWith('Test error message'); + expect(events.length).toBe(0); + }); + + it('logs fallback error.message if there is no response message', () => { + error = { + isAxiosError: true, + response: { + data: {}, + status: 418, + statusText: 'Internal Server Error', + headers: {}, + // @ts-expect-error: not needed for test + config: {}, + }, + name: 'Test error name', + message: 'Test error message', + toJSON: vi.fn(), + }; + + handleOG_APIError(error); + + expect(log.error).toHaveBeenCalledWith('Test error message'); + expect(events.length).toBe(1); + expect(events[0].detail).toEqual({ + type: NotificationType, + payload: { + severity: 'error', + message: 'Test error message', + }, + }); + }); + + it('logs generic message if the error is a 500', () => { + error = { + isAxiosError: true, + response: { + data: {}, + status: 500, + statusText: 'Internal Server Error', + headers: {}, + // @ts-expect-error: not needed for test + config: {}, + }, + name: 'Test error name', + message: 'Test error message', + toJSON: vi.fn(), + }; + + handleOG_APIError(error); + + expect(log.error).toHaveBeenCalledWith('Test error message'); + expect(events.length).toBe(1); + expect(events[0].detail).toEqual({ + type: NotificationType, + payload: { + severity: 'error', + message: + 'Something went wrong, please contact the system administrator', + }, + }); + }); + + it('logs network error message if there is no response', () => { + error = { + isAxiosError: true, + name: 'Test error name', + message: 'Network Error', + toJSON: vi.fn(), + }; + + handleOG_APIError(error); + + expect(log.error).toHaveBeenCalledWith('Network Error'); + expect(events.length).toBe(1); + expect(events[0].detail).toEqual({ + type: NotificationType, + payload: { + severity: 'error', + message: 'Network Error, please reload the page or try again later', + }, + }); + }); + + it('just logs an error if broadcast is false', () => { + handleOG_APIError(error, false); + + expect(log.error).toHaveBeenCalledWith( + 'Test error message (response data)' + ); + expect(events.length).toBe(0); + }); +}); diff --git a/src/handleOG_APIError.ts b/src/handleOG_APIError.ts new file mode 100644 index 000000000..6d8acefb4 --- /dev/null +++ b/src/handleOG_APIError.ts @@ -0,0 +1,43 @@ +import { AxiosError } from 'axios'; +import log from 'loglevel'; +import { APIError, MicroFrontendId } from './app.types'; +import { NotificationType } from './state/scigateway.actions'; + +const handleOG_APIError = (error: AxiosError, broadcast = true): void => { + const status = error.response?.status; + const message = error.response?.data + ? ((error.response.data as APIError).detail ?? error.message) + : error.message; + + log.error(message); + // Don't broadcast any error for an authentication issue - navigating via homepage links causes + // a split second render of the page when not logged in. This would otherwise display an error + // that is not displayed if navigating via SciGateway's navigation drawer instead (presumably + // due to the plugin its routing from being different). It is assumed that errors of this nature + // should not be possible due to SciGateway verifying the user itself, so we follow DataGateway's + // approach and don't display any of these errors. + if (broadcast && status !== 403) { + let broadcastMessage; + if (!error.response) + // No response so it's a network error + broadcastMessage = + 'Network Error, please reload the page or try again later'; + else if (status === 500) + broadcastMessage = + 'Something went wrong, please contact the system administrator'; + else broadcastMessage = message; + document.dispatchEvent( + new CustomEvent(MicroFrontendId, { + detail: { + type: NotificationType, + payload: { + severity: 'error', + message: broadcastMessage, + }, + }, + }) + ); + } +}; + +export default handleOG_APIError; diff --git a/src/retryOG_APIErrors.test.ts b/src/retryOG_APIErrors.test.ts new file mode 100644 index 000000000..1d6139839 --- /dev/null +++ b/src/retryOG_APIErrors.test.ts @@ -0,0 +1,47 @@ +import { AxiosError } from 'axios'; +import retryOG_APIErrors from './retryOG_APIErrors'; + +describe('retryOG_APIErrors', () => { + let error: AxiosError; + + beforeEach(() => { + error = { + isAxiosError: true, + response: { + data: { detail: 'Test error message (response data)' }, + status: 500, + statusText: 'Internal Server Error', + headers: {}, + // @ts-expect-error: not needed for test + config: {}, + }, + name: 'Test error name', + message: 'Test error message', + toJSON: vi.fn(), + }; + }); + + it('returns false if error code is 403', () => { + if (error.response) { + error.response.status = 403; + } + const result = retryOG_APIErrors(0, error); + expect(result).toBe(false); + }); + + it('returns false if failureCount is 3 or greater', () => { + let result = retryOG_APIErrors(3, error); + expect(result).toBe(false); + + result = retryOG_APIErrors(4, error); + expect(result).toBe(false); + }); + + it('returns true if non-auth error and failureCount is less than 3', () => { + let result = retryOG_APIErrors(0, error); + expect(result).toBe(true); + + result = retryOG_APIErrors(2, error); + expect(result).toBe(true); + }); +}); diff --git a/src/retryOG_APIErrors.ts b/src/retryOG_APIErrors.ts new file mode 100644 index 000000000..76cebabce --- /dev/null +++ b/src/retryOG_APIErrors.ts @@ -0,0 +1,11 @@ +import { AxiosError } from 'axios'; + +const retryOG_APIErrors = ( + failureCount: number, + error: AxiosError +): boolean => { + if (error.response?.status === 403 || failureCount >= 3) return false; + return true; +}; + +export default retryOG_APIErrors; diff --git a/src/state/scigateway.actions.tsx b/src/state/scigateway.actions.tsx index 8ae3fa4de..5e809514a 100644 --- a/src/state/scigateway.actions.tsx +++ b/src/state/scigateway.actions.tsx @@ -3,6 +3,10 @@ import { createAction } from '@reduxjs/toolkit'; import { MicroFrontendId } from '../app.types'; export const CustomFrontendMessageType = `${MicroFrontendId}:api`; + +export const NotificationType = `${CustomFrontendMessageType}:notification`; +export const InvalidateTokenType = `${CustomFrontendMessageType}:invalidate_token`; + // parent app actions export const registerRoute = createAction( `${CustomFrontendMessageType}:register_route` @@ -13,6 +17,9 @@ export const requestPluginRerender = createAction( export const sendThemeOptions = createAction<{ theme: Theme }>( `${CustomFrontendMessageType}:send_themeoptions` ); +export const tokenRefreshed = createAction( + `${CustomFrontendMessageType}:token_refreshed` +); export const broadcastSignOut = createAction( `${CustomFrontendMessageType}:signout` ); From 6db1d2276accc8de4c9a3b7c6d0031406dd5d10f Mon Sep 17 00:00:00 2001 From: Joshua Kitenge Date: Tue, 21 Jan 2025 10:50:51 +0000 Subject: [PATCH 03/13] Add error handling to mutations - minor styling changes to make dialogs mutation dialogs similar --- src/api/api.tsx | 2 - src/api/export.tsx | 4 ++ src/api/sessions.tsx | 4 ++ ...eleteFavouriteFilterDialogue.component.tsx | 13 +++-- .../favouriteFilterDialogue.component.tsx | 22 +++++--- src/functions/functionsDialog.component.tsx | 6 ++- src/mocks/handlers.ts | 12 +++-- .../deleteSessionDialogue.component.test.tsx | 13 +++++ .../deleteSessionDialogue.component.tsx | 32 +++++++---- src/session/sessionDialogue.component.tsx | 32 +++++------ src/session/sessionSaveButtons.component.tsx | 12 +++-- src/settingsMenuItems.component.tsx | 54 ++++++++++--------- 12 files changed, 130 insertions(+), 76 deletions(-) diff --git a/src/api/api.tsx b/src/api/api.tsx index 3f1be7433..75a20dbd2 100644 --- a/src/api/api.tsx +++ b/src/api/api.tsx @@ -31,8 +31,6 @@ export const ogApi = axios.create(); ogApi.interceptors.request.use(async (config) => { const settingsData = await settings; config.baseURL = settingsData ? settingsData.apiUrl : ''; - // const settingsData = await settings; - // config.baseURL = settingsData ? settingsData.apiUrl : ''; config.headers['Authorization'] = `Bearer ${readSciGatewayToken()}`; return config; }); diff --git a/src/api/export.tsx b/src/api/export.tsx index 8a9047003..7f0e1d9a1 100644 --- a/src/api/export.tsx +++ b/src/api/export.tsx @@ -1,6 +1,7 @@ import { useMutation, UseMutationResult } from '@tanstack/react-query'; import { AxiosError } from 'axios'; import { SearchParams, SortType } from '../app.types'; +import handleOG_APIError from '../handleOG_APIError'; import { useAppSelector } from '../state/hooks'; import { selectQueryParams } from '../state/slices/searchSlice'; import { selectSelectedRows } from '../state/slices/selectionSlice'; @@ -168,5 +169,8 @@ export const useExportData = (): UseMutationResult => { exportType === 'Selected Rows' ? selectedRows : undefined ); }, + onError: (error) => { + handleOG_APIError(error); + }, }); }; diff --git a/src/api/sessions.tsx b/src/api/sessions.tsx index 4f53518ba..16bc8608b 100644 --- a/src/api/sessions.tsx +++ b/src/api/sessions.tsx @@ -7,6 +7,7 @@ import { } from '@tanstack/react-query'; import { AxiosError } from 'axios'; import { Session, SessionListItem, SessionResponse } from '../app.types'; +import handleOG_APIError from '../handleOG_APIError'; import { readSciGatewayToken } from '../parseTokens'; import { ogApi } from './api'; @@ -59,6 +60,9 @@ export const useEditSession = (): UseMutationResult< const queryClient = useQueryClient(); return useMutation({ mutationFn: (session: SessionResponse) => editSession(session), + onError: (error) => { + handleOG_APIError(error); + }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['sessionList'] }); queryClient.invalidateQueries({ queryKey: ['session'] }); diff --git a/src/filtering/deleteFavouriteFilterDialogue.component.tsx b/src/filtering/deleteFavouriteFilterDialogue.component.tsx index 39a878ec8..81f627abf 100644 --- a/src/filtering/deleteFavouriteFilterDialogue.component.tsx +++ b/src/filtering/deleteFavouriteFilterDialogue.component.tsx @@ -7,7 +7,7 @@ import { DialogTitle, FormHelperText, } from '@mui/material'; -import React, { useState } from 'react'; +import React from 'react'; import { AxiosError } from 'axios'; import { useDeleteFavouriteFilter } from '../api/favouriteFilters'; @@ -24,14 +24,12 @@ const DeleteFavouriteFilterDialogue = ( ) => { const { open, onClose, favouriteFilter } = props; - const [error, setError] = useState(false); const [errorMessage, setErrorMessage] = React.useState( undefined ); const handleClose = React.useCallback(() => { onClose(); - setError(false); setErrorMessage(undefined); }, [onClose]); @@ -44,11 +42,9 @@ const DeleteFavouriteFilterDialogue = ( handleClose(); }) .catch((error: AxiosError) => { - setError(true); setErrorMessage((error.response?.data as { detail: string }).detail); }); } else { - setError(true); setErrorMessage('No data provided, Please refresh and try again'); } }, [deleteFavouriteFilter, handleClose, favouriteFilter]); @@ -65,11 +61,14 @@ const DeleteFavouriteFilterDialogue = ( - - {error && ( + {errorMessage !== undefined && ( { const hasError = handleDuplicateNameError(data.name); if (hasError) return; - addFavouriteFilter(data).then(() => { - handleClose(); - }); + addFavouriteFilter(data) + .then(() => { + handleClose(); + }) + .catch((error: AxiosError) => { + handleOG_APIError(error); + }); }, [ addFavouriteFilter, favouriteFilter.filter, @@ -172,9 +178,13 @@ const FavouriteFilterDialogue = (props: FavouriteFilterDialogueProps) => { editFavouriteFilter({ id: selectedFavouriteFilter._id, favouriteFilter: editData, - }).then(() => { - handleClose(); - }); + }) + .then(() => { + handleClose(); + }) + .catch((error: AxiosError) => { + handleOG_APIError(error); + }); } else { setErrorMessage( "There have been no changes made. Please change a field's value or press Close to exit." diff --git a/src/functions/functionsDialog.component.tsx b/src/functions/functionsDialog.component.tsx index 2f5f7c110..827e40ca8 100644 --- a/src/functions/functionsDialog.component.tsx +++ b/src/functions/functionsDialog.component.tsx @@ -24,6 +24,7 @@ import { ValidateFunctionState, } from '../app.types'; import { Heading } from '../filtering/filterDialogue.component'; +import handleOG_APIError from '../handleOG_APIError'; import { useAppDispatch, useAppSelector } from '../state/hooks'; import { changeAppliedFunctions, @@ -218,7 +219,10 @@ const FunctionsDialog = (props: FunctionsDialogProps) => { (error: AxiosError) => { const errorCode = (error.response?.data as APIError).detail; - if (typeof errorCode === 'string' && !errorCode.includes(':')) return; + if (typeof errorCode === 'string' && !errorCode.includes(':')) { + handleOG_APIError(error); + return; + } const parsedErrors = parseErrorCode(errorCode); parsedErrors.forEach((error) => { const { index, errorMessage, isNameError } = error; diff --git a/src/mocks/handlers.ts b/src/mocks/handlers.ts index 2fe276592..7e097d374 100644 --- a/src/mocks/handlers.ts +++ b/src/mocks/handlers.ts @@ -15,9 +15,9 @@ import experimentsJson from './experiments.json'; import favouriteFiltersJson from './favouriteFilters.json'; import functionsTokensJson from './functionTokens.json'; import functionsJson from './functions.json'; +import imageCrosshairJson from './imageCrosshair.json'; import recordsJson from './records.json'; import sessionsJson from './sessionsList.json'; -import imageCrosshairJson from './imageCrosshair.json'; // have to add undefined here due to how TS JSON parsing works type RecordsJSONType = (Omit & { @@ -67,10 +67,12 @@ export const handlers = [ http.delete('/sessions/:id', async ({ params }) => { const { id } = params; - const validId = [1, 2, 3, 4]; - if (validId.includes(Number(id))) { - return new HttpResponse(null, { status: 204 }); - } else HttpResponse.json(null, { status: 422 }); + if (id === sessionsJson[2]._id) + return HttpResponse.json( + { detail: 'User session cannot be found' }, + { status: 404 } + ); + return new HttpResponse(null, { status: 204 }); }), http.get('/sessions/list', async () => { return HttpResponse.json(sessionsJson, { status: 200 }); diff --git a/src/session/deleteSessionDialogue.component.test.tsx b/src/session/deleteSessionDialogue.component.test.tsx index 7e6c1eefe..57f15afaa 100644 --- a/src/session/deleteSessionDialogue.component.test.tsx +++ b/src/session/deleteSessionDialogue.component.test.tsx @@ -67,6 +67,19 @@ describe('delete session dialogue', () => { expect(onClose).not.toHaveBeenCalled(); }); + it('displays error message if the User session is no found', async () => { + props = { + ...props, + sessionData: { ...sessionData, _id: '3' }, + }; + createView(); + const continueButton = screen.getByRole('button', { name: 'Continue' }); + await user.click(continueButton); + const helperTexts = screen.getByText('User session cannot be found'); + expect(helperTexts).toBeInTheDocument(); + expect(onClose).not.toHaveBeenCalled(); + }); + it('calls handleDeleteSession when continue button is clicked with a valid session name', async () => { createView(); const continueButton = screen.getByRole('button', { name: 'Continue' }); diff --git a/src/session/deleteSessionDialogue.component.tsx b/src/session/deleteSessionDialogue.component.tsx index 11ea0942c..3abd81e42 100644 --- a/src/session/deleteSessionDialogue.component.tsx +++ b/src/session/deleteSessionDialogue.component.tsx @@ -6,7 +6,8 @@ import { DialogTitle, FormHelperText, } from '@mui/material'; -import React, { useState } from 'react'; +import type { AxiosError } from 'axios'; +import React from 'react'; import { useDeleteSession } from '../api/sessions'; import { SessionResponse } from '../app.types'; @@ -22,7 +23,6 @@ const DeleteSessionDialogue = (props: DeleteSessionDialogueProps) => { const { open, onClose, sessionData, loadedSessionId, onDeleteLoadedSession } = props; - const [error, setError] = useState(false); const [errorMessage, setErrorMessage] = React.useState( undefined ); @@ -38,12 +38,10 @@ const DeleteSessionDialogue = (props: DeleteSessionDialogueProps) => { } onClose(); }) - .catch((error) => { - setError(true); - setErrorMessage(error.message); + .catch((error: AxiosError) => { + setErrorMessage((error.response?.data as { detail: string }).detail); }); } else { - setError(true); setErrorMessage('No data provided, Please refresh and try again'); } }, [ @@ -54,18 +52,32 @@ const DeleteSessionDialogue = (props: DeleteSessionDialogueProps) => { sessionData, ]); + const handleClose = React.useCallback(() => { + onClose(); + setErrorMessage(undefined); + }, [onClose]); + return ( - + Delete Session Are you sure you want to delete{' '} {sessionData?.name}? - - - {error && {errorMessage}} + + + {errorMessage !== undefined && ( + + {errorMessage} + + )} ); }; diff --git a/src/session/sessionDialogue.component.tsx b/src/session/sessionDialogue.component.tsx index 07826717b..50ba3ece3 100644 --- a/src/session/sessionDialogue.component.tsx +++ b/src/session/sessionDialogue.component.tsx @@ -6,10 +6,12 @@ import { DialogTitle, TextField, } from '@mui/material'; -import React, { useState } from 'react'; +import type { AxiosError } from 'axios'; +import React from 'react'; import { shallowEqual } from 'react-redux'; import { useEditSession, useSaveSession } from '../api/sessions'; import { SessionResponse } from '../app.types'; +import handleOG_APIError from '../handleOG_APIError'; import { useUpdateWindowPositions } from '../hooks'; import { sessionSelector, useAppSelector } from '../state/hooks'; @@ -45,7 +47,6 @@ const SessionDialogue = (props: SessionDialogueProps) => { const { mutateAsync: saveSession } = useSaveSession(); const { mutateAsync: editSession } = useEditSession(); - const [error, setError] = useState(false); const [errorMessage, setErrorMessage] = React.useState( undefined ); @@ -73,13 +74,10 @@ const SessionDialogue = (props: SessionDialogueProps) => { onChangeLoadedSessionId(response); handleClose(); }) - .catch((error) => { - setError(true); - console.log(error.message); - setErrorMessage(error.message); + .catch((error: AxiosError) => { + handleOG_APIError(error); }); } else { - setError(true); setErrorMessage('Please enter a name'); } }, [ @@ -106,19 +104,16 @@ const SessionDialogue = (props: SessionDialogueProps) => { editSession(session) .then(() => handleClose()) - .catch((error) => { - setError(true); - console.log(error.message); - setErrorMessage(error.message); + .catch((error: AxiosError) => { + handleOG_APIError(error); }); } else { - setError(true); setErrorMessage('Please enter a name'); } }, [sessionName, sessionData, sessionSummary, editSession, handleClose]); return ( - + {requestType === 'create' ? 'Save Session' : 'Edit Session'} @@ -126,20 +121,20 @@ const SessionDialogue = (props: SessionDialogueProps) => { { onChangeSessionName( event.target.value ? event.target.value : undefined ); - setError(false); // Reset the error when the user makes changes + setErrorMessage(undefined); // Reset the error when the user makes changes }} /> { @@ -152,6 +147,7 @@ const SessionDialogue = (props: SessionDialogueProps) => {