Repository: prometheus/prometheus (language: go)

Issue:
React UI: Starting Screen flashes before going to requested page
<!--

    Please do *NOT* ask support questions in Github issues.

    If your issue is not a feature request or bug report use our
    community support.

    https://prometheus.io/community/

    There is also commercial support available.

    https://prometheus.io/support-training/

-->

**What did you do?**

I opened the Prometheus web UI on a version that has the starting screen (for example https://prometheus-react.netlify.app).

**What did you expect to see?**

I expected to be taken right to the graph page.

**What did you see instead? Under which circumstances?**

The starting screen flashed for a second before forwarding me to the graph page.

* Prometheus version:

	main (1270b87)

This happens because the UI starts out on the starting screen and only moves off it once a request to `/ready` responds with a `200` status. I wonder if we could have the request happen in the background instead and just delay all requests on other pages until it responds (and if the server isn't ready _then_ go to the starting screen).


Discussion / hints:
Can you assign this issue to me?
I have some experience in ReactJS, I think I can help.
@LeviHarrison 
Feel free to work on it. Thanks!
Thanks.
@LeviHarrison Could I direct me to the right path were this issue might be the source?
Sure! The code for the starting screen is in https://github.com/prometheus/prometheus/blob/main/web/ui/react-app/src/components/withStartingIndicator.tsx

Fix patch:
diff --git a/web/ui/react-app/public/index.html b/web/ui/react-app/public/index.html
index a3f7b7a8503..57131a26b69 100755
--- a/web/ui/react-app/public/index.html
+++ b/web/ui/react-app/public/index.html
@@ -16,10 +16,14 @@
       - PROMETHEUS_AGENT_MODE is replaced by a boolean indicating if Prometheus is running in agent mode.
         It true, it will disable querying capacities in the UI and generally adapt the UI to the agent mode.
         It has to be represented as a string, because booleans can be mangled to !1 in production builds.
+      - PROMETHEUS_READY is replaced by a boolean indicating whether Prometheus was ready at the time the
+        web app was served. It has to be represented as a string, because booleans can be mangled to !1 in
+        production builds.
     -->
     <script>
       const GLOBAL_CONSOLES_LINK='CONSOLES_LINK_PLACEHOLDER';
       const GLOBAL_AGENT_MODE='AGENT_MODE_PLACEHOLDER';
+      const GLOBAL_READY='READY_PLACEHOLDER';
     </script>
 
     <!--
diff --git a/web/ui/react-app/src/App.tsx b/web/ui/react-app/src/App.tsx
index dcf5f98f17a..30b07f26132 100755
--- a/web/ui/react-app/src/App.tsx
+++ b/web/ui/react-app/src/App.tsx
@@ -5,6 +5,7 @@ import Navigation from './Navbar';
 import { BrowserRouter as Router, Redirect, Route, Switch } from 'react-router-dom';
 import { PathPrefixContext } from './contexts/PathPrefixContext';
 import { ThemeContext, themeName, themeSetting } from './contexts/ThemeContext';
+import { ReadyContext } from './contexts/ReadyContext';
 import { useLocalStorage } from './hooks/useLocalStorage';
 import useMedia from './hooks/useMedia';
 import {
@@ -24,9 +25,10 @@ import { Theme, themeLocalStorageKey } from './Theme';
 interface AppProps {
   consolesLink: string | null;
   agentMode: boolean;
+  ready: boolean;
 }
 
-const App: FC<AppProps> = ({ consolesLink, agentMode }) => {
+const App: FC<AppProps> = ({ consolesLink, agentMode, ready }) => {
   // This dynamically/generically determines the pathPrefix by stripping the first known
   // endpoint suffix from the window location path. It works out of the box for both direct
   // hosting and reverse proxy deployments with no additional configurations required.
@@ -72,48 +74,50 @@ const App: FC<AppProps> = ({ consolesLink, agentMode }) => {
     >
       <Theme />
       <PathPrefixContext.Provider value={basePath}>
-        <Router basename={basePath}>
-          <Navigation consolesLink={consolesLink} agentMode={agentMode} />
-          <Container fluid style={{ paddingTop: 70 }}>
-            <Switch>
-              <Redirect exact from="/" to={agentMode ? '/agent' : '/graph'} />
-              {/*
+        <ReadyContext.Provider value={ready}>
+          <Router basename={basePath}>
+            <Navigation consolesLink={consolesLink} agentMode={agentMode} />
+            <Container fluid style={{ paddingTop: 70 }}>
+              <Switch>
+                <Redirect exact from="/" to={agentMode ? '/agent' : '/graph'} />
+                {/*
               NOTE: Any route added here needs to also be added to the list of
               React-handled router paths ("reactRouterPaths") in /web/web.go.
             */}
-              <Route path="/agent">
-                <AgentPage />
-              </Route>
-              <Route path="/graph">
-                <PanelListPage />
-              </Route>
-              <Route path="/alerts">
-                <AlertsPage />
-              </Route>
-              <Route path="/config">
-                <ConfigPage />
-              </Route>
-              <Route path="/flags">
-                <FlagsPage />
-              </Route>
-              <Route path="/rules">
-                <RulesPage />
-              </Route>
-              <Route path="/service-discovery">
-                <ServiceDiscoveryPage />
-              </Route>
-              <Route path="/status">
-                <StatusPage agentMode={agentMode} />
-              </Route>
-              <Route path="/tsdb-status">
-                <TSDBStatusPage />
-              </Route>
-              <Route path="/targets">
-                <TargetsPage />
-              </Route>
-            </Switch>
-          </Container>
-        </Router>
+                <Route path="/agent">
+                  <AgentPage />
+                </Route>
+                <Route path="/graph">
+                  <PanelListPage />
+                </Route>
+                <Route path="/alerts">
+                  <AlertsPage />
+                </Route>
+                <Route path="/config">
+                  <ConfigPage />
+                </Route>
+                <Route path="/flags">
+                  <FlagsPage />
+                </Route>
+                <Route path="/rules">
+                  <RulesPage />
+                </Route>
+                <Route path="/service-discovery">
+                  <ServiceDiscoveryPage />
+                </Route>
+                <Route path="/status">
+                  <StatusPage agentMode={agentMode} />
+                </Route>
+                <Route path="/tsdb-status">
+                  <TSDBStatusPage />
+                </Route>
+                <Route path="/targets">
+                  <TargetsPage />
+                </Route>
+              </Switch>
+            </Container>
+          </Router>
+        </ReadyContext.Provider>
       </PathPrefixContext.Provider>
     </ThemeContext.Provider>
   );
diff --git a/web/ui/react-app/src/components/withStartingIndicator.tsx b/web/ui/react-app/src/components/withStartingIndicator.tsx
index 0f29d8b1ee6..eb2724ed137 100644
--- a/web/ui/react-app/src/components/withStartingIndicator.tsx
+++ b/web/ui/react-app/src/components/withStartingIndicator.tsx
@@ -4,6 +4,7 @@ import { Progress, Alert } from 'reactstrap';
 import { useFetchReadyInterval } from '../hooks/useFetch';
 import { WALReplayData } from '../types/types';
 import { usePathPrefix } from '../contexts/PathPrefixContext';
+import { useReady } from '../contexts/ReadyContext';
 
 interface StartingContentProps {
   isUnexpected: boolean;
@@ -48,8 +49,9 @@ export const withStartingIndicator =
   ({ ...rest }) => {
     const pathPrefix = usePathPrefix();
     const { ready, walReplayStatus, isUnexpected } = useFetchReadyInterval(pathPrefix);
+    const staticReady = useReady();
 
-    if (ready || isUnexpected) {
+    if (staticReady || ready || isUnexpected) {
       return <Page {...(rest as T)} />;
     }
 
diff --git a/web/ui/react-app/src/contexts/ReadyContext.tsx b/web/ui/react-app/src/contexts/ReadyContext.tsx
new file mode 100644
index 00000000000..f3a95cfb8af
--- /dev/null
+++ b/web/ui/react-app/src/contexts/ReadyContext.tsx
@@ -0,0 +1,9 @@
+import React from 'react';
+
+const ReadyContext = React.createContext(false);
+
+function useReady(): boolean {
+  return React.useContext(ReadyContext);
+}
+
+export { useReady, ReadyContext };
diff --git a/web/ui/react-app/src/index.tsx b/web/ui/react-app/src/index.tsx
index a798d1b623a..61a9ae083c6 100755
--- a/web/ui/react-app/src/index.tsx
+++ b/web/ui/react-app/src/index.tsx
@@ -11,9 +11,11 @@ import { isPresent } from './utils';
 // Declared/defined in public/index.html, value replaced by Prometheus when serving bundle.
 declare const GLOBAL_CONSOLES_LINK: string;
 declare const GLOBAL_AGENT_MODE: string;
+declare const GLOBAL_READY: string;
 
 let consolesLink: string | null = GLOBAL_CONSOLES_LINK;
 const agentMode: string | null = GLOBAL_AGENT_MODE;
+const ready: string | null = GLOBAL_READY;
 
 if (
   GLOBAL_CONSOLES_LINK === 'CONSOLES_LINK_PLACEHOLDER' ||
@@ -23,4 +25,7 @@ if (
   consolesLink = null;
 }
 
-ReactDOM.render(<App consolesLink={consolesLink} agentMode={agentMode === 'true'} />, document.getElementById('root'));
+ReactDOM.render(
+  <App consolesLink={consolesLink} agentMode={agentMode === 'true'} ready={ready === 'true'} />,
+  document.getElementById('root')
+);
diff --git a/web/web.go b/web/web.go
index ba7c2d2895f..9d63094f69a 100644
--- a/web/web.go
+++ b/web/web.go
@@ -401,6 +401,7 @@ func New(logger log.Logger, o *Options) *Handler {
 		replacedIdx := bytes.ReplaceAll(idx, []byte("CONSOLES_LINK_PLACEHOLDER"), []byte(h.consolesPath()))
 		replacedIdx = bytes.ReplaceAll(replacedIdx, []byte("TITLE_PLACEHOLDER"), []byte(h.options.PageTitle))
 		replacedIdx = bytes.ReplaceAll(replacedIdx, []byte("AGENT_MODE_PLACEHOLDER"), []byte(strconv.FormatBool(h.options.IsAgent)))
+		replacedIdx = bytes.ReplaceAll(replacedIdx, []byte("READY_PLACEHOLDER"), []byte(strconv.FormatBool(h.isReady())))
 		w.Write(replacedIdx)
 	}
 


Test patch:
diff --git a/web/ui/react-app/src/App.test.tsx b/web/ui/react-app/src/App.test.tsx
index 6bc11b4dcd6..62f685c921a 100755
--- a/web/ui/react-app/src/App.test.tsx
+++ b/web/ui/react-app/src/App.test.tsx
@@ -18,7 +18,7 @@ import {
 } from './pages';
 
 describe('App', () => {
-  const app = shallow(<App consolesLink={null} agentMode={false} />);
+  const app = shallow(<App consolesLink={null} agentMode={false} ready={false} />);
 
   it('navigates', () => {
     expect(app.find(Navigation)).toHaveLength(1);