From d12da0a1c11b6f1b3d7578844a5771f2538d0c70 Mon Sep 17 00:00:00 2001 From: Christian Benincasa Date: Thu, 25 Apr 2024 14:31:00 -0400 Subject: [PATCH] Show 'health check' for backend URL in settings page (#376) Also improves the behavior of when backend URLs change. It's still not perfect though. --- types/src/api/index.ts | 2 ++ web/src/components/TunarrApiContext.tsx | 28 +++++++++++++---- web/src/hooks/useApiQuery.ts | 13 ++++++-- web/src/hooks/useTunarrApi.ts | 15 ---------- web/src/hooks/useVersion.ts | 12 ++++++-- web/src/main.tsx | 2 +- .../pages/settings/GeneralSettingsPage.tsx | 30 ++++++++++++++++++- 7 files changed, 74 insertions(+), 28 deletions(-) diff --git a/types/src/api/index.ts b/types/src/api/index.ts index e1087b78a..b0b33f925 100644 --- a/types/src/api/index.ts +++ b/types/src/api/index.ts @@ -145,6 +145,8 @@ export const VersionApiResponseSchema = z.object({ nodejs: z.string(), }); +export type VersionApiResponse = z.infer; + export const BaseErrorSchema = z.object({ message: z.string(), }); diff --git a/web/src/components/TunarrApiContext.tsx b/web/src/components/TunarrApiContext.tsx index adf9f94fe..3a68788d6 100644 --- a/web/src/components/TunarrApiContext.tsx +++ b/web/src/components/TunarrApiContext.tsx @@ -1,7 +1,9 @@ -import { ReactNode, createContext, useEffect, useState } from 'react'; +import { ReactNode, createContext, useEffect } from 'react'; import { createApiClient } from '../external/api'; import useStore from '../store/index.ts'; import { useSettings } from '../store/settings/selectors'; +import { QueryClient } from '@tanstack/react-query'; +import { isUndefined } from 'lodash-es'; // HACK ALERT // Read zustand state out-of-band here (i.e. not in a hook) because we @@ -21,17 +23,31 @@ export const getApiClient = () => apiClient; export const TunarrApiContext = createContext(apiClient); -export function TunarrApiProvider({ children }: { children: ReactNode }) { +export function TunarrApiProvider({ + children, + queryClient, +}: { + children: ReactNode; + queryClient: QueryClient; +}) { const { backendUri } = useSettings(); - const [api, setApi] = useState(apiClient); useEffect(() => { - apiClient = createApiClient(backendUri); - setApi(apiClient); + // Only do this if something actually changed + if ( + (backendUri.length === 0 && !isUndefined(apiClient.baseURL)) || + (backendUri.length > 0 && isUndefined(apiClient.baseURL)) + ) { + apiClient = createApiClient(backendUri); + } }, [backendUri]); + useEffect(() => { + queryClient.invalidateQueries().catch(console.warn); + }, [backendUri, queryClient]); + return ( - + {children} ); diff --git a/web/src/hooks/useApiQuery.ts b/web/src/hooks/useApiQuery.ts index 4cbd6196d..a910bee59 100644 --- a/web/src/hooks/useApiQuery.ts +++ b/web/src/hooks/useApiQuery.ts @@ -7,8 +7,8 @@ import { UseQueryResult, useQuery, } from '@tanstack/react-query'; +import { getApiClient } from '../components/TunarrApiContext'; import { ApiClient } from '../external/api'; -import { useTunarrApi } from './useTunarrApi'; export function useApiQuery< TQueryFnData = unknown, @@ -27,11 +27,18 @@ export function useApiQuery< }, queryClient?: QueryClient, ): UseQueryResult { - const apiClient = useTunarrApi(); + // NOTE that this query also depends on the backendUrl used to + // create the API client, but we explicitly don't include it in the + // queryKey here because: + // 1. it makes the types super unwieldy + // 2. we do a mass cache invalidation in the tunarr API context when + // the backend URL changes + // 3. it keeps query keys simple for when we have to do more fine-grained + // invalidation (e.g. post-mutates) return useQuery( { ...options, - queryFn: (args) => options.queryFn(apiClient, args), + queryFn: (args) => options.queryFn(getApiClient(), args), }, queryClient, ); diff --git a/web/src/hooks/useTunarrApi.ts b/web/src/hooks/useTunarrApi.ts index 6b16c1a18..41863f80f 100644 --- a/web/src/hooks/useTunarrApi.ts +++ b/web/src/hooks/useTunarrApi.ts @@ -2,20 +2,5 @@ import { useContext } from 'react'; import { TunarrApiContext } from '../components/TunarrApiContext'; export const useTunarrApi = () => { - // const { backendUri } = useSettings(); - // const [api, setApi] = useState(createApiClient(backendUri)); - // const queryClient = useQueryClient(); - - // useEffect(() => { - // setApi(createApiClient(backendUri)); - // // We have to reset everything when the backend URL changes! - // queryClient.resetQueries().catch(console.warn); - // }, [backendUri, queryClient]); - - // return api; return useContext(TunarrApiContext); }; - -// export const useWithTunarrApi = () => { -// const useTunarrApi() -// } diff --git a/web/src/hooks/useVersion.ts b/web/src/hooks/useVersion.ts index 618f95772..f331ffe6e 100644 --- a/web/src/hooks/useVersion.ts +++ b/web/src/hooks/useVersion.ts @@ -1,11 +1,19 @@ +import { UseQueryOptions } from '@tanstack/react-query'; import { useApiQuery } from './useApiQuery.ts'; +import { VersionApiResponse } from '@tunarr/types/api'; -export const useVersion = () => { +export const useVersion = ( + extraOpts: Omit< + UseQueryOptions, + 'queryKey' | 'queryFn' + > = {}, +) => { return useApiQuery({ queryKey: ['version'], queryFn: (apiClient) => { return apiClient.getServerVersions(); }, - staleTime: 30 * 1000, + ...extraOpts, + staleTime: extraOpts.staleTime ?? 30 * 1000, }); }; diff --git a/web/src/main.tsx b/web/src/main.tsx index acce5b992..dbdf8451d 100644 --- a/web/src/main.tsx +++ b/web/src/main.tsx @@ -16,7 +16,7 @@ const queryClient = new QueryClient({ queryCache }); ReactDOM.createRoot(document.getElementById('root')!).render( - + diff --git a/web/src/pages/settings/GeneralSettingsPage.tsx b/web/src/pages/settings/GeneralSettingsPage.tsx index 1eff678bf..780bebbf8 100644 --- a/web/src/pages/settings/GeneralSettingsPage.tsx +++ b/web/src/pages/settings/GeneralSettingsPage.tsx @@ -4,9 +4,19 @@ import Button from '@mui/material/Button'; import { useSettings } from '../../store/settings/selectors.ts'; import { Controller, useForm } from 'react-hook-form'; import { attempt, isEmpty, isError } from 'lodash-es'; -import { Box, Divider, Snackbar, TextField, Typography } from '@mui/material'; +import { + Box, + Divider, + InputAdornment, + Snackbar, + TextField, + Typography, +} from '@mui/material'; import { setBackendUri } from '../../store/settings/actions.ts'; import { useState } from 'react'; +import { useVersion } from '../../hooks/useVersion.ts'; +import { RotatingLoopIcon } from '../../components/base/LoadingIcon.tsx'; +import { CloudDoneOutlined, CloudOff } from '@mui/icons-material'; type GeneralSettingsForm = { backendUri: string; @@ -19,6 +29,11 @@ function isValidUrl(url: string) { export default function GeneralSettingsPage() { const settings = useSettings(); const [snackStatus, setSnackStatus] = useState(false); + const versionInfo = useVersion({ + retry: 0, + }); + + const { isLoading, isError } = versionInfo; const { control, handleSubmit } = useForm({ reValidateMode: 'onBlur', @@ -59,6 +74,19 @@ export default function GeneralSettingsPage() { + {isLoading ? ( + + ) : !isError ? ( + + ) : ( + + )} + + ), + }} {...field} helperText={ error?.type === 'isValidUrl'