go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/web/rpcexplorer/src/context/globals.tsx (about)

     1  // Copyright 2022 The LUCI Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  import {
    16    createContext,
    17    useContext, useEffect, useState,
    18  } from 'react';
    19  
    20  import Box from '@mui/material/Box';
    21  import LinearProgress from '@mui/material/LinearProgress';
    22  
    23  import { ErrorAlert } from '../components/error_alert';
    24  
    25  import { loadTokenClient, TokenClient } from '../data/oauth';
    26  import { Descriptors, loadDescriptors } from '../data/prpc';
    27  
    28  
    29  // Globals are fetched once and then indefinitely used by all routes.
    30  export interface Globals {
    31    // Type information with server's RPC APIs.
    32    descriptors: Descriptors;
    33    // The client to use for getting an access token.
    34    tokenClient: TokenClient;
    35  }
    36  
    37  // loadGlobals loads the globals by querying the server.
    38  const loadGlobals = async (): Promise<Globals> => {
    39    const [descriptors, tokenClient] = await Promise.all([
    40      loadDescriptors(),
    41      loadTokenClient(),
    42    ]);
    43    return {
    44      descriptors: descriptors,
    45      tokenClient: tokenClient,
    46    };
    47  };
    48  
    49  
    50  // GlobalsContextData wraps Globals with loading status.
    51  interface GlobalsContextData {
    52    isLoading: boolean;
    53    error?: Error;
    54    globals?: Globals;
    55  }
    56  
    57  const GlobalsContext = createContext<GlobalsContextData>({
    58    isLoading: true,
    59  });
    60  
    61  
    62  export interface GlobalsProviderProps {
    63    children: React.ReactNode;
    64  }
    65  
    66  // GlobalsProvider loads globals and makes them accessible to children elements.
    67  export const GlobalsProvider = ({ children }: GlobalsProviderProps) => {
    68    const [globalsData, setGlobalsData] = useState<GlobalsContextData>({
    69      isLoading: true,
    70    });
    71  
    72    useEffect(() => {
    73      loadGlobals()
    74          .then((globals) => setGlobalsData({
    75            isLoading: false,
    76            globals: globals,
    77          }))
    78          .catch((error) => setGlobalsData({
    79            isLoading: false,
    80            error: error,
    81          }));
    82    }, []);
    83  
    84    return (
    85      <GlobalsContext.Provider value={globalsData}>
    86        {children}
    87      </GlobalsContext.Provider>
    88    );
    89  };
    90  
    91  
    92  export interface GlobalsWaiterProps {
    93    silent?: boolean;
    94    children: React.ReactNode;
    95  }
    96  
    97  // GlobalsWaiter waits for globals to be loaded.
    98  //
    99  // On success, it renders the children. On error it renders the error message.
   100  //
   101  // If silent is true, it renders nothing when loading and on errors. Useful
   102  // when there are multiple GlobalsWaiter on a page: only on of them can be
   103  // showing the progress and the error.
   104  export const GlobalsWaiter = ({ silent, children }: GlobalsWaiterProps) => {
   105    const globalsData = useContext(GlobalsContext);
   106  
   107    if (globalsData.isLoading) {
   108      if (silent) {
   109        return <></>;
   110      }
   111      return (
   112        <Box sx={{ width: '100%' }}>
   113          <LinearProgress />
   114        </Box>
   115      );
   116    }
   117  
   118    if (globalsData.error) {
   119      if (silent) {
   120        return <></>;
   121      }
   122      return (
   123        <ErrorAlert
   124          title="Failed to initialize RPC Explorer"
   125          error={globalsData.error}
   126        />
   127      );
   128    }
   129  
   130    return <>{children}</>;
   131  };
   132  
   133  
   134  // useGlobals returns loaded globals or throws an error.
   135  //
   136  // Should be used somewhere under GlobalsWaiter component, which renders
   137  // children only if globals are actually loaded.
   138  export const useGlobals = (): Globals => {
   139    const globalsData = useContext(GlobalsContext);
   140    if (globalsData.isLoading) {
   141      throw new Error('Globals are not loaded yet');
   142    }
   143    if (globalsData.error) {
   144      throw new Error('Globals failed to be loaded');
   145    }
   146    if (globalsData.globals === undefined) {
   147      throw new Error('Globals unexpectedly empty');
   148    }
   149    return globalsData.globals;
   150  };