diff --git a/CHANGELOG.md b/CHANGELOG.md index faff16efc..06d6b08be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,7 @@ NOTE: As semantic versioning states all 0.y.z releases can contain breaking chan ### Changed - [#400](https://github.com/kobsio/kobs/pull/#400): [app] Add recover handler for `httpmetrics` middleware and rename metrics to `kobs_requests_total`, `kobs_request_duration_seconds` and `kobs_request_size_bytes`. +- [#405](https://github.com/kobsio/kobs/pull/405): [app] Imporove federated module core ## [v0.9.1](https://github.com/kobsio/kobs/releases/tag/v0.9.1) (2022-07-08) diff --git a/plugins/app/src/components/module/Module.tsx b/plugins/app/src/components/module/Module.tsx index 764ea6a76..fcadab4e7 100644 --- a/plugins/app/src/components/module/Module.tsx +++ b/plugins/app/src/components/module/Module.tsx @@ -1,4 +1,4 @@ -import React, { memo } from 'react'; +import React, { ComponentProps, ComponentType, ReactElement, memo } from 'react'; import { ErrorBoundary } from 'react-error-boundary'; import { useDynamicScript } from '../../hooks/useDynamicScript'; @@ -13,9 +13,18 @@ import { useDynamicScript } from '../../hooks/useDynamicScript'; // return; // }; -const loadComponent = (scope: string, module: string) => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return async (): Promise => { +/** + * Load and initialize a federated module via webpack container. + * @param scope the module scope. In kobs it's usually the plugin name: e.g. 'kiali' + * @param module the name of the module entry point. e.g. './Page' + * @returns the module component or method + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const loadComponent = >( + scope: string, + module: string, +): (() => Promise<{ default: T }>) => { + return async (): Promise<{ default: T }> => { // wait can be used to simulate long loading times // await wait(); @@ -31,8 +40,8 @@ const loadComponent = (scope: string, module: string) => { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore const factory = await window[scope].get(module); - const Module = factory(); - return Module; + const ModuleInstance = factory(); + return ModuleInstance; }; }; @@ -41,25 +50,26 @@ declare function errorContentRenderer(props: { children: React.ReactElement; }): React.ReactElement | null; -export interface IModuleProps { +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export interface IModuleProps

{ version: string; name: string; module: string; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - props: any; + props: P; loadingContent: React.FunctionComponent; errorContent: typeof errorContentRenderer; } -const Module: React.FunctionComponent = ({ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const Module = , P extends ComponentProps>({ version, name, module, props, errorContent, loadingContent, -}: IModuleProps) => { - const { ready, failed } = useDynamicScript(name, version); +}: IModuleProps

): ReactElement => { + const { isReady, isFailed } = useDynamicScript(name, version); const ErrorContent = errorContent; const LoadingContent = loadingContent; @@ -74,11 +84,7 @@ const Module: React.FunctionComponent = ({ ); } - if (!ready) { - return ; - } - - if (failed) { + if (isFailed) { return (

@@ -88,7 +94,11 @@ const Module: React.FunctionComponent = ({ ); } - const Component = React.lazy(loadComponent(name, module)); + if (!isReady) { + return ; + } + + const Component = React.lazy(loadComponent(name, module)); return ( } = {}; - -export const useDynamicScript = ( - name: string, - version: string, -): { - failed: boolean; - ready: boolean; -} => { +const loadedScripts: Record> = {}; + +export type ScriptStatus = { isFailed: boolean; isReady: boolean }; + +/** + * Load a kobs specific federated module from a remote server. + * @param name the plugin name aka. federated module name + * @param version version of the module. Used as cache buster for CDNs. + * Note: the version can't be changed during runtime. + * @returns the status of the script/module + */ +export const useDynamicScript = (name: string, version: string): ScriptStatus => { const url = process.env.NODE_ENV === 'production' ? `/plugins/${name}/remoteEntry.js?version=${version}` : `http://localhost:3001/remoteEntry.js?version=${version}`; - const [ready, setReady] = useState(false); - const [failed, setFailed] = useState(false); + return useScript(name, url); +}; + +/** + * Load any javascript file from a remote server and execute it in the DOM. + * @param name a unique name of the script so that it's only loaded once + * @param url the URL where the script is located + * @returns the status of the script + */ +export const useScript = (name: string, url: string): ScriptStatus => { + const [isReady, setReady] = useState(false); + const [isFailed, setFailed] = useState(false); + + const setLoading = (): void => { + setReady(false); + setFailed(false); + }; + + const setSuccess = (): void => { + setReady(true); + setFailed(false); + }; + + const setError = (): void => { + setReady(false); + setFailed(true); + }; useEffect(() => { if (!name) { @@ -25,12 +53,10 @@ export const useDynamicScript = ( if (name in loadedScripts) { loadedScripts[name] .then(() => { - setReady(true); - setFailed(false); + setSuccess(); }) .catch(() => { - setReady(false); - setFailed(true); + setError(); }); return; } @@ -42,17 +68,15 @@ export const useDynamicScript = ( element.type = 'text/javascript'; element.async = true; - setReady(false); - setFailed(false); + setLoading(); element.onload = (): void => { - setReady(true); + setSuccess(); resolve(); }; element.onerror = (): void => { - setReady(false); - setFailed(true); + setError(); reject(); }; @@ -65,7 +89,7 @@ export const useDynamicScript = ( }, [name, url]); return { - failed, - ready, + isFailed, + isReady, }; };