All files / client/src/app App.tsx

95.65% Statements 22/23
61.53% Branches 8/13
100% Functions 8/8
95.45% Lines 21/22

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135                                                608x   608x 1306x 1300x 6781x   1300x 608x 680x 680x 656x                         1300x 1290x 671x     619x 454x       1300x                                                                                       1x                   1x                 625x         1x 625x              
import { ReactElement, Suspense, useEffect, useState } from "react";
import { BrowserRouter } from "react-router";
import styled from "styled-components";
import { config } from "shared/config";
import { AppRoutes } from "client/app/AppRoutes";
import { HistoryProvider } from "client/app/HistoryProvider";
import { prefetchLazyViews } from "client/app/lazyViews";
import { AdminMessageBanner } from "client/components/AdminMessageBanner";
import { Announcement } from "client/components/Announcement";
import { AppUpdateBanner } from "client/components/AppUpdateBanner";
import { ErrorBar } from "client/components/ErrorBar";
import { FirstLogin } from "client/components/FirstLogin";
import { HEADER_HEIGHT, Header } from "client/components/Header";
import { Loading } from "client/components/Loading";
import { ViewErrorBoundary } from "client/components/ViewErrorBoundary";
import { MOBILE_MARGIN } from "client/globalStyle";
import { TestGenerateSerial } from "client/test/test-components/TestGenerateSerial";
import { TestTime } from "client/test/test-components/TestTime";
import { startDataPolling } from "client/utils/dataPolling";
import { useAppSelector } from "client/utils/hooks";
import { getIconLibrary } from "client/utils/icons";
import { whenIdle } from "client/utils/whenIdle";
import { NotificationBar } from "client/views/event-log/NotificationBar";
 
const { loadedSettings, showTestValues, showAnnouncement } = config.client();
 
const App = (): ReactElement => {
  const [loading, setLoading] = useState<boolean>(true);
  const [dataLoaded, setDataLoaded] = useState<boolean>(false);
  const userGroup = useAppSelector((state) => state.login.userGroup);
 
  useEffect(() => {
    return startDataPolling((succeeded) => {
      setLoading(false);
      if (succeeded) {
        setDataLoaded(true);
      }
    });
  }, []);
 
  // Warm the split view chunks once a data load has actually succeeded and the
  // browser reports spare time. Gating on success rather than on `loading`
  // matters twice over: a failed load flips `loading` too, so these requests
  // would pile onto a connection that is already struggling, and a failed
  // dynamic import is cached by the browser for the session - the later real
  // navigation would then reject instantly and force a full page reload even
  // though connectivity had recovered. Re-runs when the role changes, because
  // signing in as admin/helper makes chunks reachable that weren't before.
  useEffect(() => {
    if (!dataLoaded) {
      return;
    }
 
    return whenIdle(() => {
      prefetchLazyViews(userGroup);
    });
  }, [dataLoaded, userGroup]);
 
  getIconLibrary();
 
  return (
    <>
      {loading && <Loading />}
 
      {!loading && (
        <BrowserRouter>
          <HistoryProvider>
            {loadedSettings !== "production" && showTestValues && (
              <TestValueContainer>
                <TestTime />
                <TestGenerateSerial />
              </TestValueContainer>
            )}
            <Header />
            {/* One sticky wrapper rather than sticky bars: siblings pinned to
                the same offset would overlap instead of stacking */}
            <StickyBars>
              <FirstLogin />
              <ErrorBar />
              <AppUpdateBanner />
              <AdminMessageBanner />
              <NotificationBar />
            </StickyBars>
            {showAnnouncement && <Announcement />}
            <AppContainer>
              {/* Boundaries for the lazily loaded views, placed here rather
                  than around the whole app so a slow chunk or a render error in
                  one keeps the header and bars on screen instead of blanking
                  the page */}
              <ViewErrorBoundary>
                <Suspense fallback={<Loading />}>
                  <AppRoutes />
                </Suspense>
              </ViewErrorBoundary>
            </AppContainer>
          </HistoryProvider>
        </BrowserRouter>
      )}
    </>
  );
};
 
const StickyBars = styled.div`
  position: sticky;
  top: ${HEADER_HEIGHT}px;
  z-index: 10;
 
  /* Owns the horizontal inset for every bar inside, so their dismiss icons
     line up instead of each bar picking its own margin */
  margin: 0 2px;
`;
 
const TestValueContainer = styled.div`
  position: fixed;
  top: 0;
  left: 0;
  z-index: 1000;
 
  /* Don't block clicks on the header underneath; children opt back in */
  pointer-events: none;
 
  @media (max-width: ${(props) => props.theme.breakpointDesktop}) {
    display: none;
  }
`;
 
const AppContainer = styled.div`
  @media (max-width: ${(props) => props.theme.breakpointDesktop}) {
    margin-left: ${MOBILE_MARGIN}px;
    margin-right: ${MOBILE_MARGIN}px;
  }
`;
 
export default App;