Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[app] Imporove federated module core #405

Merged
merged 2 commits into from
Jul 31, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
46 changes: 28 additions & 18 deletions plugins/app/src/components/module/Module.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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<any> => {
/**
* 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
*/
Copy link
Contributor Author

@marwonline marwonline Jul 31, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added some documentation. In a later PR, I'll try to type this with generics so that we can get rid of any.

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const loadComponent = <T extends ComponentType<any>>(
scope: string,
module: string,
): (() => Promise<{ default: T }>) => {
return async (): Promise<{ default: T }> => {
// wait can be used to simulate long loading times
// await wait();

Expand All @@ -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;
};
};

Expand All @@ -41,25 +50,26 @@ declare function errorContentRenderer(props: {
children: React.ReactElement;
}): React.ReactElement<unknown, string | React.FunctionComponent | typeof React.Component> | null;

export interface IModuleProps {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export interface IModuleProps<P = any> {
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<IModuleProps> = ({
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const Module = <M extends ComponentType<any>, P extends ComponentProps<M>>({
version,
name,
module,
props,
errorContent,
loadingContent,
}: IModuleProps) => {
const { ready, failed } = useDynamicScript(name, version);
}: IModuleProps<P>): ReactElement => {
const { isReady, isFailed } = useDynamicScript(name, version);

const ErrorContent = errorContent;
const LoadingContent = loadingContent;
Expand All @@ -74,11 +84,7 @@ const Module: React.FunctionComponent<IModuleProps> = ({
);
}

if (!ready) {
return <LoadingContent />;
}

if (failed) {
if (isFailed) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've re-ordered the cases so that the error case always apprears first.

return (
<ErrorContent title="Failed to load module">
<p>
Expand All @@ -88,7 +94,11 @@ const Module: React.FunctionComponent<IModuleProps> = ({
);
}

const Component = React.lazy(loadComponent(name, module));
if (!isReady) {
return <LoadingContent />;
}

const Component = React.lazy(loadComponent<M>(name, module));

return (
<ErrorBoundary
Expand Down
68 changes: 46 additions & 22 deletions plugins/app/src/hooks/useDynamicScript.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,49 @@
import { useEffect, useState } from 'react';

const loadedScripts: { [name: string]: Promise<void> } = {};

export const useDynamicScript = (
name: string,
version: string,
): {
failed: boolean;
ready: boolean;
} => {
const loadedScripts: Record<string, Promise<void>> = {};

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 => {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've separated the Kobs business logic and the script loading into two functions. Should make it easier to re-use it later

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) {
Expand All @@ -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;
}
Expand All @@ -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();
};

Expand All @@ -65,7 +89,7 @@ export const useDynamicScript = (
}, [name, url]);

return {
failed,
ready,
isFailed,
isReady,
};
};