diff --git a/package-lock.json b/package-lock.json index b48949b9..b1ac7125 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,7 @@ "oidc-client-ts": "^3.1.0", "react": "19.0.0", "react-dom": "19.0.0", + "react-error-boundary": "^5.0.0", "react-hook-form": "^7.54.2", "react-i18next": "^15.4.1", "react-oidc-context": "^3.2.0", @@ -6684,6 +6685,18 @@ "react": "^19.0.0" } }, + "node_modules/react-error-boundary": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-5.0.0.tgz", + "integrity": "sha512-tnjAxG+IkpLephNcePNA7v6F/QpWLH8He65+DmedchDwg162JZqx4NmbXj0mlAYVVEd81OW7aFhmbsScYfiAFQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "peerDependencies": { + "react": ">=16.13.1" + } + }, "node_modules/react-hook-form": { "version": "7.54.2", "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.54.2.tgz", diff --git a/package.json b/package.json index 89175999..7242b877 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "oidc-client-ts": "^3.1.0", "react": "19.0.0", "react-dom": "19.0.0", + "react-error-boundary": "^5.0.0", "react-hook-form": "^7.54.2", "react-i18next": "^15.4.1", "react-oidc-context": "^3.2.0", diff --git a/public/locales/en.json b/public/locales/en.json index a0f67cc0..d7ed6eac 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -118,9 +118,6 @@ "EditMembers": { "addButton": "Add" }, - "FrontendConfigContext": { - "errorMessage": "useFrontendConfig must be used within a FrontendConfigProvider" - }, "ControlPlaneListView": { "projectHeader": "Project:" }, @@ -141,9 +138,6 @@ "App": { "loading": "Loading..." }, - "main": { - "failedMessage": "Failed to load frontend configuration" - }, "Providers": { "headerProviders": "Providers", "tableHeaderVersion": "Version", diff --git a/src/context/AuthProviderOnboarding.tsx b/src/context/AuthProviderOnboarding.tsx new file mode 100644 index 00000000..fb160d84 --- /dev/null +++ b/src/context/AuthProviderOnboarding.tsx @@ -0,0 +1,26 @@ +import { ReactNode, use } from 'react'; +import { AuthProvider, AuthProviderProps } from 'react-oidc-context'; +import { useFrontendConfig } from './FrontendConfigContext.tsx'; +import { LoadCrateKubeConfig } from '../lib/oidc/crate.ts'; + +interface AuthProviderOnboardingProps { + children?: ReactNode; +} + +// Promise needs to be cached +// https://react.dev/blog/2024/12/05/react-19#use-does-not-support-promises-created-in-render +const fetchAuthPromiseCache = new Map>(); + +export function AuthProviderOnboarding({ + children, +}: AuthProviderOnboardingProps) { + const { backendUrl } = useFrontendConfig(); + + const fetchAuthConfigPromise = + fetchAuthPromiseCache.get(backendUrl) ?? LoadCrateKubeConfig(backendUrl); + fetchAuthPromiseCache.set(backendUrl, fetchAuthConfigPromise); + + const authConfig = use(fetchAuthConfigPromise); + + return {children}; +} diff --git a/src/context/FrontendConfigContext.tsx b/src/context/FrontendConfigContext.tsx index 699f5635..bdf1346b 100644 --- a/src/context/FrontendConfigContext.tsx +++ b/src/context/FrontendConfigContext.tsx @@ -1,6 +1,5 @@ -import { FC, ReactNode, createContext, useContext } from 'react'; +import { ReactNode, createContext, use } from 'react'; import { DocLinkCreator } from '../lib/shared/links'; -import { useTranslation } from 'react-i18next'; export enum Landscape { Live = 'LIVE', @@ -9,49 +8,47 @@ export enum Landscape { Development = 'DEV', } -export interface FrontendConfig { +interface FrontendConfigContextProps { backendUrl: string; landscape?: Landscape; documentationBaseUrl: string; -} - -export interface FrontendConfigProviderProps extends FrontendConfig { links: DocLinkCreator; } -const FrontendConfigContext = createContext( +const FrontendConfigContext = createContext( null, ); -export const useFrontendConfig = () => { - const c = useContext(FrontendConfigContext); - const { t } = useTranslation(); - - if (!c) { - throw new Error(t('FrontendConfigContext.errorMessage')); - } - return c; -}; +const fetchPromise = fetch('/frontend-config.json').then((res) => res.json()); -export const FrontendConfigProvider: FC<{ +interface FrontendConfigProviderProps { children: ReactNode; - config: FrontendConfig; -}> = ({ children, config }) => { +} + +export function FrontendConfigProvider({ + children, +}: FrontendConfigProviderProps) { + const config = use(fetchPromise); const docLinks = new DocLinkCreator(config.documentationBaseUrl); + const value: FrontendConfigContextProps = { + links: docLinks, + backendUrl: config.backendUrl, + landscape: config.landscape, + documentationBaseUrl: config.documentationBaseUrl, + }; + return ( - - {children} - + {children} ); -}; - -export async function LoadFrontendConfig(): Promise { - return fetch('/frontend-config.json').then((res) => res.json()); } + +export const useFrontendConfig = () => { + const context = use(FrontendConfigContext); + + if (!context) { + throw new Error( + 'useFrontendConfig must be used within a FrontendConfigProvider.', + ); + } + return context; +}; diff --git a/src/lib/oidc/crate.ts b/src/lib/oidc/crate.ts index 2e0489c9..4a1cdcf8 100644 --- a/src/lib/oidc/crate.ts +++ b/src/lib/oidc/crate.ts @@ -3,7 +3,7 @@ import { GetAuthPropsForCurrentContext } from './shared.ts'; export function LoadCrateKubeConfig( backendUrl: string, -): Promise { +): Promise { const uri = backendUrl + '/.well-known/openmcp/kubeconfig'; return fetch(uri) diff --git a/src/main.tsx b/src/main.tsx index 519a54de..85643d21 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,32 +1,33 @@ -import React from 'react'; -import ReactDOM from 'react-dom/client'; +import React, { Suspense } from 'react'; +import { createRoot } from 'react-dom/client'; import './index.css'; import App from './App'; -import { ThemeProvider } from '@ui5/webcomponents-react'; -import { AuthProvider } from 'react-oidc-context'; -import { LoadCrateKubeConfig } from './lib/oidc/crate.ts'; +import { BusyIndicator, ThemeProvider } from '@ui5/webcomponents-react'; import { SWRConfig } from 'swr'; import { ToastProvider } from './context/ToastContext.tsx'; import { CopyButtonProvider } from './context/CopyButtonContext.tsx'; -import { - FrontendConfigProvider, - LoadFrontendConfig, -} from './context/FrontendConfigContext.tsx'; +import { FrontendConfigProvider } from './context/FrontendConfigContext.tsx'; import '@ui5/webcomponents-react/dist/Assets'; //used for loading themes import { DarkModeSystemSwitcher } from './components/Core/DarkModeSystemSwitcher.tsx'; import '.././i18n.ts'; import './utils/i18n/timeAgo'; -import { useTranslation } from 'react-i18next'; +import { ErrorBoundary, FallbackProps } from 'react-error-boundary'; +import IllustratedError from './components/Shared/IllustratedError.tsx'; +import { AuthProviderOnboarding } from './context/AuthProviderOnboarding.tsx'; -(async () => { - try { - const frontendConfig = await LoadFrontendConfig(); - const authconfig = await LoadCrateKubeConfig(frontendConfig.backendUrl); +const ErrorFallback = ({ error }: FallbackProps) => { + return ; +}; - ReactDOM.createRoot(document.getElementById('root')!).render( - - - +const rootElement = document.getElementById('root'); +const root = createRoot(rootElement!); + +root.render( + + {}}> + }> + + @@ -41,17 +42,9 @@ import { useTranslation } from 'react-i18next'; - + - , - ); - } catch (e) { - const { t } = useTranslation(); - console.error('failed to load frontend configuration or kubeconfig', e); - ReactDOM.createRoot(document.getElementById('root')!).render( - -
{t('main.failedMessage')}=
-
, - ); - } -})(); + + + , +);