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 };