From eb8ca068859d429fff56503e6dc636a38824a0f3 Mon Sep 17 00:00:00 2001 From: Levi Harrison Date: Tue, 15 Jun 2021 16:37:16 -0400 Subject: [PATCH] React UI: Add Starting Screen to Individual Pages (#8909) * Fix/removed forwarding Signed-off-by: Levi Harrison * Added global 'wasReady' and 'wasUnexpected' Signed-off-by: Levi Harrison * Eslint fixes Signed-off-by: Levi Harrison * Added withStartingIndicator wrapper Signed-off-by: Levi Harrison * Fixed condition Signed-off-by: Levi Harrison * Removed unused import Signed-off-by: Levi Harrison * Moved withStartingIndicator calls to pages index Signed-off-by: Levi Harrison * Fixed withStartingIndicator tests Signed-off-by: Levi Harrison * Fixed eslint (maybe?) Signed-off-by: Levi Harrison * Trailing comma Signed-off-by: Levi Harrison * Added prettier ignore Signed-off-by: Levi Harrison * Fix eslint (pt. 2) Signed-off-by: Levi Harrison --- web/ui/react-app/src/App.test.tsx | 24 +++- web/ui/react-app/src/App.tsx | 44 ++++---- .../withStartingIndicator.test.tsx} | 4 +- .../withStartingIndicator.tsx} | 21 ++-- web/ui/react-app/src/hooks/useFetch.ts | 104 +++++++++--------- web/ui/react-app/src/pages/index.ts | 25 ++++- 6 files changed, 127 insertions(+), 95 deletions(-) rename web/ui/react-app/src/{pages/starting/Starting.test.tsx => components/withStartingIndicator.test.tsx} (92%) rename web/ui/react-app/src/{pages/starting/Starting.tsx => components/withStartingIndicator.tsx} (73%) diff --git a/web/ui/react-app/src/App.test.tsx b/web/ui/react-app/src/App.test.tsx index e96bbf7a40..6ccdf9ffe1 100755 --- a/web/ui/react-app/src/App.test.tsx +++ b/web/ui/react-app/src/App.test.tsx @@ -4,7 +4,17 @@ import App from './App'; import Navigation from './Navbar'; import { Container } from 'reactstrap'; import { Router } from '@reach/router'; -import { Alerts, Config, Flags, Rules, ServiceDiscovery, Status, Targets, TSDBStatus, PanelList } from './pages'; +import { + AlertsPage, + ConfigPage, + FlagsPage, + RulesPage, + ServiceDiscoveryPage, + StatusPage, + TargetsPage, + TSDBStatusPage, + PanelListPage, +} from './pages'; describe('App', () => { const app = shallow(); @@ -13,7 +23,17 @@ describe('App', () => { expect(app.find(Navigation)).toHaveLength(1); }); it('routes', () => { - [Alerts, Config, Flags, Rules, ServiceDiscovery, Status, Targets, TSDBStatus, PanelList].forEach(component => { + [ + AlertsPage, + ConfigPage, + FlagsPage, + RulesPage, + ServiceDiscoveryPage, + StatusPage, + TargetsPage, + TSDBStatusPage, + PanelListPage, + ].forEach(component => { const c = app.find(component); expect(c).toHaveLength(1); }); diff --git a/web/ui/react-app/src/App.tsx b/web/ui/react-app/src/App.tsx index 143b3d3ce2..9370ca48ad 100755 --- a/web/ui/react-app/src/App.tsx +++ b/web/ui/react-app/src/App.tsx @@ -2,15 +2,23 @@ import React, { FC } from 'react'; import Navigation from './Navbar'; import { Container } from 'reactstrap'; -import { Router, Redirect, navigate } from '@reach/router'; +import { Router, Redirect } from '@reach/router'; import useMedia from 'use-media'; -import { Alerts, Config, Flags, Rules, ServiceDiscovery, Status, Targets, TSDBStatus, PanelList, Starting } from './pages'; +import { + AlertsPage, + ConfigPage, + FlagsPage, + RulesPage, + ServiceDiscoveryPage, + StatusPage, + TargetsPage, + TSDBStatusPage, + PanelListPage, +} from './pages'; import { PathPrefixContext } from './contexts/PathPrefixContext'; import { ThemeContext, themeName, themeSetting } from './contexts/ThemeContext'; import { Theme, themeLocalStorageKey } from './Theme'; import { useLocalStorage } from './hooks/useLocalStorage'; -import { useFetchReady } from './hooks/useFetch'; -import { usePathPrefix } from './contexts/PathPrefixContext'; interface AppProps { consolesLink: string | null; @@ -31,7 +39,6 @@ const App: FC = ({ consolesLink }) => { '/rules', '/targets', '/service-discovery', - '/starting', ]; if (basePath.endsWith('/')) { basePath = basePath.slice(0, -1); @@ -45,14 +52,6 @@ const App: FC = ({ consolesLink }) => { } } - const pathPrefix = usePathPrefix(); - const { ready, isLoading, isUnexpected } = useFetchReady(pathPrefix); - if (basePath !== '/starting') { - if (!ready && !isLoading && !isUnexpected) { - navigate('/starting'); - } - } - const [userTheme, setUserTheme] = useLocalStorage(themeLocalStorageKey, 'auto'); const browserHasThemes = useMedia('(prefers-color-scheme)'); const browserWantsDarkTheme = useMedia('(prefers-color-scheme: dark)'); @@ -78,16 +77,15 @@ const App: FC = ({ consolesLink }) => { NOTE: Any route added here needs to also be added to the list of React-handled router paths ("reactRouterPaths") in /web/web.go. */} - - - - - - - - - - + + + + + + + + + diff --git a/web/ui/react-app/src/pages/starting/Starting.test.tsx b/web/ui/react-app/src/components/withStartingIndicator.test.tsx similarity index 92% rename from web/ui/react-app/src/pages/starting/Starting.test.tsx rename to web/ui/react-app/src/components/withStartingIndicator.test.tsx index 80fc814306..ec4fd25f3a 100644 --- a/web/ui/react-app/src/pages/starting/Starting.test.tsx +++ b/web/ui/react-app/src/components/withStartingIndicator.test.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { shallow } from 'enzyme'; -import { WALReplayData } from '../../types/types'; -import { StartingContent } from './Starting'; +import { WALReplayData } from '../types/types'; +import { StartingContent } from './withStartingIndicator'; import { Progress } from 'reactstrap'; describe('Starting', () => { diff --git a/web/ui/react-app/src/pages/starting/Starting.tsx b/web/ui/react-app/src/components/withStartingIndicator.tsx similarity index 73% rename from web/ui/react-app/src/pages/starting/Starting.tsx rename to web/ui/react-app/src/components/withStartingIndicator.tsx index 936fea050c..a935913a01 100644 --- a/web/ui/react-app/src/pages/starting/Starting.tsx +++ b/web/ui/react-app/src/components/withStartingIndicator.tsx @@ -1,10 +1,9 @@ -import React, { FC, useEffect } from 'react'; -import { RouteComponentProps, navigate } from '@reach/router'; +import React, { FC, ComponentType } from 'react'; import { Progress, Alert } from 'reactstrap'; -import { useFetchReadyInterval } from '../../hooks/useFetch'; -import { WALReplayData } from '../../types/types'; -import { usePathPrefix } from '../../contexts/PathPrefixContext'; +import { useFetchReadyInterval } from '../hooks/useFetch'; +import { WALReplayData } from '../types/types'; +import { usePathPrefix } from '../contexts/PathPrefixContext'; interface StartingContentProps { isUnexpected: boolean; @@ -44,17 +43,13 @@ export const StartingContent: FC = ({ status, isUnexpected ); }; -const Starting: FC = () => { +export const withStartingIndicator = (Page: ComponentType): FC => ({ ...rest }) => { const pathPrefix = usePathPrefix(); const { ready, walReplayStatus, isUnexpected } = useFetchReadyInterval(pathPrefix); - useEffect(() => { - if (ready) { - navigate('/'); - } - }, [ready]); + if (ready || isUnexpected) { + return ; + } return ; }; - -export default Starting; diff --git a/web/ui/react-app/src/hooks/useFetch.ts b/web/ui/react-app/src/hooks/useFetch.ts index f88b01c0f0..52e5a5d4b8 100644 --- a/web/ui/react-app/src/hooks/useFetch.ts +++ b/web/ui/react-app/src/hooks/useFetch.ts @@ -47,36 +47,7 @@ export const useFetch = (url: string, options?: RequestInit): Fetc return { response, error, isLoading }; }; -export const useFetchReady = (pathPrefix: string, options?: RequestInit): FetchStateReady => { - const [ready, setReady] = useState(false); - const [isUnexpected, setIsUnexpected] = useState(false); - const [isLoading, setIsLoading] = useState(true); - - useEffect(() => { - const fetchData = async () => { - setIsLoading(true); - try { - const res = await fetch(`${pathPrefix}/-/ready`, { cache: 'no-store', credentials: 'same-origin', ...options }); - if (res.status === 200) { - setReady(true); - } - // The server sends back a 503 if it isn't ready, - // if we get back anything else that means something has gone wrong. - if (res.status !== 503) { - setIsUnexpected(true); - } else { - setIsUnexpected(false); - } - - setIsLoading(false); - } catch (error) { - setIsUnexpected(true); - } - }; - fetchData(); - }, [pathPrefix, options]); - return { ready, isUnexpected, isLoading }; -}; +let wasReady = false; // This is used on the starting page to periodically check if the server is ready yet, // and check the status of the WAL replay. @@ -86,33 +57,60 @@ export const useFetchReadyInterval = (pathPrefix: string, options?: RequestInit) const [walReplayStatus, setWALReplayStatus] = useState({} as any); useEffect(() => { - const interval = setInterval(async () => { - try { - let res = await fetch(`${pathPrefix}/-/ready`, { cache: 'no-store', credentials: 'same-origin', ...options }); - if (res.status === 200) { - setReady(true); + if (wasReady) { + setReady(true); + } else { + // This helps avoid a memory leak. + let mounted = true; + + const fetchStatus = async () => { + try { + let res = await fetch(`${pathPrefix}/-/ready`, { cache: 'no-store', credentials: 'same-origin', ...options }); + if (res.status === 200) { + if (mounted) { + setReady(true); + } + wasReady = true; + clearInterval(interval); + } else if (res.status !== 503) { + if (mounted) { + setIsUnexpected(true); + } + clearInterval(interval); + return; + } else { + if (mounted) { + setIsUnexpected(false); + } + + res = await fetch(`${pathPrefix}/${API_PATH}/status/walreplay`, { + cache: 'no-store', + credentials: 'same-origin', + }); + if (res.ok) { + const data = (await res.json()) as WALReplayStatus; + if (mounted) { + setWALReplayStatus(data); + } + } + } + } catch (error) { + if (mounted) { + setIsUnexpected(true); + } clearInterval(interval); return; } - if (res.status !== 503) { - setIsUnexpected(true); - setWALReplayStatus({ data: { last: 0, first: 0 } } as any); - } else { - setIsUnexpected(false); + }; - res = await fetch(`${pathPrefix}/${API_PATH}/status/walreplay`, { cache: 'no-store', credentials: 'same-origin' }); - if (res.ok) { - const data = (await res.json()) as WALReplayStatus; - setWALReplayStatus(data); - } - } - } catch (error) { - setIsUnexpected(true); - setWALReplayStatus({ data: { last: 0, first: 0 } } as any); - } - }, 1000); - - return () => clearInterval(interval); + fetchStatus(); + const interval = setInterval(fetchStatus, 1000); + return () => { + clearInterval(interval); + mounted = false; + }; + } }, [pathPrefix, options]); + return { ready, isUnexpected, walReplayStatus }; }; diff --git a/web/ui/react-app/src/pages/index.ts b/web/ui/react-app/src/pages/index.ts index d8ad5e72be..c514663375 100644 --- a/web/ui/react-app/src/pages/index.ts +++ b/web/ui/react-app/src/pages/index.ts @@ -7,6 +7,27 @@ import Status from './status/Status'; import Targets from './targets/Targets'; import PanelList from './graph/PanelList'; import TSDBStatus from './tsdbStatus/TSDBStatus'; -import Starting from './starting/Starting'; +import { withStartingIndicator } from '../components/withStartingIndicator'; -export { Alerts, Config, Flags, Rules, ServiceDiscovery, Status, Targets, TSDBStatus, PanelList, Starting }; +const AlertsPage = withStartingIndicator(Alerts); +const ConfigPage = withStartingIndicator(Config); +const FlagsPage = withStartingIndicator(Flags); +const RulesPage = withStartingIndicator(Rules); +const ServiceDiscoveryPage = withStartingIndicator(ServiceDiscovery); +const StatusPage = withStartingIndicator(Status); +const TSDBStatusPage = withStartingIndicator(TSDBStatus); +const TargetsPage = withStartingIndicator(Targets); +const PanelListPage = withStartingIndicator(PanelList); + +// prettier-ignore +export { + AlertsPage, + ConfigPage, + FlagsPage, + RulesPage, + ServiceDiscoveryPage, + StatusPage, + TSDBStatusPage, + TargetsPage, + PanelListPage +};