diff --git a/frontend/src/App/App.tsx b/frontend/src/App/App.tsx index 0014c1d3f..b71199bb3 100644 --- a/frontend/src/App/App.tsx +++ b/frontend/src/App/App.tsx @@ -1,3 +1,4 @@ +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { ConnectedRouter, ConnectedRouterProps } from 'connected-react-router'; import React from 'react'; import DocumentTitle from 'react-document-title'; @@ -12,17 +13,21 @@ interface AppProps { history: ConnectedRouterProps['history']; } +const queryClient = new QueryClient(); + function App({ store, history }: AppProps) { return ( - - - - - - - - + + + + + + + + + + ); } diff --git a/frontend/src/Helpers/Hooks/useApiQuery.ts b/frontend/src/Helpers/Hooks/useApiQuery.ts new file mode 100644 index 000000000..69c5c8672 --- /dev/null +++ b/frontend/src/Helpers/Hooks/useApiQuery.ts @@ -0,0 +1,56 @@ +import { useQuery } from '@tanstack/react-query'; +import { useMemo } from 'react'; + +interface QueryOptions { + url: string; + headers?: HeadersInit; +} + +const absUrlRegex = /^(https?:)?\/\//i; +const apiRoot = window.Sonarr.apiRoot; + +function isAbsolute(url: string) { + return absUrlRegex.test(url); +} + +function getUrl(url: string) { + return apiRoot + url; +} + +function useApiQuery(options: QueryOptions) { + const { url, headers } = options; + + const final = useMemo(() => { + if (isAbsolute(url)) { + return { + url, + headers, + }; + } + + return { + url: getUrl(url), + headers: { + ...headers, + 'X-Api-Key': window.Sonarr.apiKey, + }, + }; + }, [url, headers]); + + return useQuery({ + queryKey: [final.url], + queryFn: async () => { + const result = await fetch(final.url, { + headers: final.headers, + }); + + if (!result.ok) { + throw new Error('Failed to fetch'); + } + + return result.json() as T; + }, + }); +} + +export default useApiQuery; diff --git a/frontend/src/bootstrap.tsx b/frontend/src/bootstrap.tsx index 6a6d7fc67..9ecf27e0e 100644 --- a/frontend/src/bootstrap.tsx +++ b/frontend/src/bootstrap.tsx @@ -1,6 +1,6 @@ import { createBrowserHistory } from 'history'; import React from 'react'; -import { render } from 'react-dom'; +import { createRoot } from 'react-dom/client'; import createAppStore from 'Store/createAppStore'; import App from './App/App'; @@ -9,9 +9,8 @@ import 'Diag/ConsoleApi'; export async function bootstrap() { const history = createBrowserHistory(); const store = createAppStore(history); + const container = document.getElementById('root'); - render( - , - document.getElementById('root') - ); + const root = createRoot(container!); // createRoot(container!) if you use TypeScript + root.render(); } diff --git a/frontend/src/index.ts b/frontend/src/index.ts index bbb3b5932..325ea4d7f 100644 --- a/frontend/src/index.ts +++ b/frontend/src/index.ts @@ -14,6 +14,31 @@ window.Sonarr = await response.json(); __webpack_public_path__ = `${window.Sonarr.urlBase}/`; /* eslint-enable no-undef, @typescript-eslint/ban-ts-comment */ +const error = console.error; + +// Monkey patch console.error to filter out some warnings from React +// TODO: Remove this after the great TypeScript migration + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function logError(...parameters: any[]) { + const filter = parameters.find((parameter) => { + return ( + parameter.includes( + 'Support for defaultProps will be removed from function components in a future major release' + ) || + parameter.includes( + 'findDOMNode is deprecated and will be removed in the next major release' + ) + ); + }); + + if (!filter) { + error(...parameters); + } +} + +console.error = logError; + const { bootstrap } = await import('./bootstrap'); await bootstrap(); diff --git a/frontend/typings/Globals.d.ts b/frontend/typings/Globals.d.ts index 47c7a49da..d4f61354d 100644 --- a/frontend/typings/Globals.d.ts +++ b/frontend/typings/Globals.d.ts @@ -3,6 +3,7 @@ declare module '*.module.css'; interface Window { Sonarr: { apiKey: string; + apiRoot: string; instanceName: string; theme: string; urlBase: string; diff --git a/package.json b/package.json index 83034c9b9..fe5079530 100644 --- a/package.json +++ b/package.json @@ -29,9 +29,10 @@ "@juggle/resize-observer": "3.4.0", "@microsoft/signalr": "6.0.21", "@sentry/browser": "7.119.1", + "@tanstack/react-query": "5.61.0", "@types/node": "20.16.11", - "@types/react": "18.2.79", - "@types/react-dom": "18.2.25", + "@types/react": "18.3.12", + "@types/react-dom": "18.3.1", "classnames": "2.5.1", "connected-react-router": "6.9.3", "copy-to-clipboard": "3.3.3", @@ -48,7 +49,7 @@ "normalize.css": "8.0.1", "prop-types": "15.8.1", "qs": "6.13.0", - "react": "17.0.2", + "react": "18.3.1", "react-addons-shallow-compare": "15.6.3", "react-async-script": "1.2.0", "react-autosuggest": "10.1.0", @@ -58,7 +59,7 @@ "react-dnd-multi-backend": "6.0.2", "react-dnd-touch-backend": "14.1.1", "react-document-title": "2.0.3", - "react-dom": "17.0.2", + "react-dom": "18.3.1", "react-focus-lock": "2.9.4", "react-google-recaptcha": "2.1.0", "react-lazyload": "3.2.0", diff --git a/yarn.lock b/yarn.lock index 43427e636..d36aba5d1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1223,6 +1223,18 @@ dependencies: "@sentry/types" "7.119.1" +"@tanstack/query-core@5.60.6": + version "5.60.6" + resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.60.6.tgz#0dd33fe231b0d18bf66d0c615b29899738300658" + integrity sha512-tI+k0KyCo1EBJ54vxK1kY24LWj673ujTydCZmzEZKAew4NqZzTaVQJEuaG1qKj2M03kUHN46rchLRd+TxVq/zQ== + +"@tanstack/react-query@5.61.0": + version "5.61.0" + resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.61.0.tgz#73473feb37aa28ceb410e297ee060e18f06f88e0" + integrity sha512-SBzV27XAeCRBOQ8QcC94w2H1Md0+LI0gTWwc3qRJoaGuewKn5FNW4LSqwPFJZVEItfhMfGT7RpZuSFXjTi12pQ== + dependencies: + "@tanstack/query-core" "5.60.6" + "@types/archiver@^5.3.1": version "5.3.4" resolved "https://registry.yarnpkg.com/@types/archiver/-/archiver-5.3.4.tgz#32172d5a56f165b5b4ac902e366248bf03d3ae84" @@ -1335,10 +1347,10 @@ dependencies: "@types/react" "*" -"@types/react-dom@18.2.25": - version "18.2.25" - resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.2.25.tgz#2946a30081f53e7c8d585eb138277245caedc521" - integrity sha512-o/V48vf4MQh7juIKZU2QGDfli6p1+OOi5oXx36Hffpc9adsHeXjVp8rHuPkjd8VT8sOJ2Zp05HR7CdpGTIUFUA== +"@types/react-dom@18.3.1": + version "18.3.1" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.3.1.tgz#1e4654c08a9cdcfb6594c780ac59b55aad42fe07" + integrity sha512-qW1Mfv8taImTthu4KoXgDfLuk4bydU6Q/TkADnDWWHwi4NX4BR+LWfTp2sVmTqRrsHvyDDTelgelxJ+SsejKKQ== dependencies: "@types/react" "*" @@ -1405,10 +1417,10 @@ "@types/prop-types" "*" csstype "^3.0.2" -"@types/react@18.2.79": - version "18.2.79" - resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.79.tgz#c40efb4f255711f554d47b449f796d1c7756d865" - integrity sha512-RwGAGXPl9kSXwdNTafkOEuFrTBD5SA2B3iEB96xi8+xu5ddUa/cpvyVCSNn+asgLCTHkb5ZxN8gbuibYJi4s1w== +"@types/react@18.3.12": + version "18.3.12" + resolved "https://registry.yarnpkg.com/@types/react/-/react-18.3.12.tgz#99419f182ccd69151813b7ee24b792fe08774f60" + integrity sha512-D2wOSq/d6Agt28q7rSI3jhU7G6aiuzljDGZ2hTZHIkrTLUI+AF3WMeKkEZ9nN2fkBAlcktT6vcZjDFiIhMYEQw== dependencies: "@types/prop-types" "*" csstype "^3.0.2" @@ -5427,14 +5439,13 @@ react-document-title@2.0.3: prop-types "^15.5.6" react-side-effect "^1.0.2" -react-dom@17.0.2: - version "17.0.2" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.2.tgz#ecffb6845e3ad8dbfcdc498f0d0a939736502c23" - integrity sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA== +react-dom@18.3.1: + version "18.3.1" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.3.1.tgz#c2265d79511b57d479b3dd3fdfa51536494c5cb4" + integrity sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw== dependencies: loose-envify "^1.1.0" - object-assign "^4.1.1" - scheduler "^0.20.2" + scheduler "^0.23.2" react-focus-lock@2.9.4: version "2.9.4" @@ -5606,13 +5617,12 @@ react-window@1.8.10: "@babel/runtime" "^7.0.0" memoize-one ">=3.1.1 <6" -react@17.0.2: - version "17.0.2" - resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037" - integrity sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA== +react@18.3.1: + version "18.3.1" + resolved "https://registry.yarnpkg.com/react/-/react-18.3.1.tgz#49ab892009c53933625bd16b2533fc754cab2891" + integrity sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ== dependencies: loose-envify "^1.1.0" - object-assign "^4.1.1" read-pkg-up@^7.0.1: version "7.0.1" @@ -5979,13 +5989,12 @@ sax@~1.2.4: resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== -scheduler@^0.20.2: - version "0.20.2" - resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.20.2.tgz#4baee39436e34aa93b4874bddcbf0fe8b8b50e91" - integrity sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ== +scheduler@^0.23.2: + version "0.23.2" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.2.tgz#414ba64a3b282892e944cf2108ecc078d115cdc3" + integrity sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ== dependencies: loose-envify "^1.1.0" - object-assign "^4.1.1" schema-utils@>1.0.0, schema-utils@^4.0.0: version "4.2.0" @@ -6207,6 +6216,7 @@ string-template@~0.2.1: integrity sha512-Yptehjogou2xm4UJbxJ4CxgZx12HBfeystp0y3x7s4Dj32ltVVG1Gg8YhKjHZkHicuKpZX/ffilA8505VbUbpw== "string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.3: + name string-width-cjs version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==