App
About 1 min
This module purpose is:
This module is mainly developed for helping portal for keeping track of selected application and loading of application.
Application instance
Initialize
when calling initialize
, it might emit multiple time, since manifest and config has cache.
How to only use settle values
// Observable
app.initialize().pipe(last());
// Async
await lastValueFrom(app.initialize());
Manifest
Meta data description of application, loaded from the Fusion Application Store
Config
Configuration for the application
Script Modules
imported javascript script modules
Instance
Collection of initialized modules of the application
Configuration
simple
export const configure = (configurator) => {
enableAppModule(configurator);
}
custom
const manifestMapper = (value: any): AppManifest => {
const { appKey, name, entry, version } = value;
return { appKey, name, entry, version };
}
export const configure = (configurator) => {
enableAppModule(configurator, async(builder) => {
const httpProvider = await builder.requireInstance('http');
const appClient = httpProvider.createClient('app-api-client');
builder.setAppClient(() => {
/** callback for fetching an applications */
getAppManifest: ({ appKey: string }) => appClient.json$(
`/api/app/${appKey}`,
{ selector: async(x) => manifestMapper(await res.json()) }
),
/** callback for fetching all applications */
getAppManifests: () => appClient.json$(
`/api/apps`,
{ selector: async(x) => (await res.json()).map(manifestMapper) }
),
/** callback for fetching application config */
getAppConfig: ({ appKey: string }) => appClient.json$(
`/api/app/${appKey}/config`,
),
});
});
}
Events
import { FrameworkEvent, FrameworkEventInit } from '@equinor/fusion-framework-module-event';
import { App } from './app/App';
import './app/events';
declare module '@equinor/fusion-framework-module-event' {
interface FrameworkEventMap {
/** fired when the current selected application changes */
onCurrentAppChanged: FrameworkEvent<
FrameworkEventInit<{
/** current application */
next?: App;
/** previous application */
previous?: App;
}>
>;
}
}
App
import type { FrameworkEvent, FrameworkEventInit } from '@equinor/fusion-framework-module-event';
import type { App } from './App';
import type { AppConfig, AppManifest, AppModulesInstance, AppScriptModule } from '../types';
/** base event type for applications */
export type AppEventEventInit<TDetail extends Record<string, unknown> | unknown = unknown> =
FrameworkEventInit<
/** additional event details and key of target event */
TDetail & { appKey: string },
/** source of the event */
App
>;
export type AppEvent<TDetail extends Record<string, unknown> | unknown = unknown> = FrameworkEvent<
AppEventEventInit<TDetail>
>;
export type AppEventFailure = FrameworkEvent<
AppEventEventInit<{
error: AppConfig;
}>
>;
declare module '@equinor/fusion-framework-module-event' {
interface FrameworkEventMap {
/** fired when the application has initiated its modules */
onAppModulesLoaded: AppEvent<{
/** initiated modules for application */
modules: AppModulesInstance;
}>;
onAppManifestLoad: AppEvent;
/** fired when the application has loaded corresponding manifest */
onAppManifestLoaded: AppEvent<{
manifest: AppManifest;
}>;
onAppManifestFailure: AppEventFailure;
onAppConfigLoad: AppEvent;
/** fired when the application has loaded corresponding config */
onAppConfigLoaded: AppEvent<{
config: AppConfig;
}>;
onAppConfigFailure: AppEventFailure;
/** fired when the application has loaded corresponding javascript module */
onAppScriptLoad: AppEvent;
onAppScriptLoaded: AppEvent<{
script: AppScriptModule;
}>;
onAppScriptFailure: AppEventFailure;
/** fired before application loads manifest, config and script */
onAppInitialize: AppEvent;
/**
* fired after application has loaded manifest, config and script
*
* __note:__ not fired until all loaders has settled (last emit)
*/
onAppInitialized: AppEvent;
/** fired when application fails to load either manifest, config and script */
onAppInitializeFailure: AppEventFailure;
/** fired when the application is disposed (unmounts) */
onAppDispose: FrameworkEvent<AppEventEventInit>;
}
}
Examples
Apploader
import { useEffect, useMemo, useRef, useState } from 'react';
import { Subscription } from 'rxjs';
import { useFramework } from '@equinor/fusion-framework-react';
import { useObservableState } from '@equinor/fusion-observable/react';
import { AppManifestError } from '@equinor/fusion-framework-module-app/errors.js';
import { ErrorViewer } from './ErrorViewer';
import { AppModule } from '@equinor/fusion-framework-module-app';
import EquinorLoader from './EquinorLoader';
/**
* React Functional Component for handling current application
*
* this component will set the current app by provided appKey.
* when the appKey changes, this component will try to initialize the referred application
* and render it.
*/
export const AppLoader = (props: { readonly appKey: string }) => {
const { appKey } = props;
const fusion = useFramework<[AppModule]>();
/** reference of application section/container */
const ref = useRef<HTMLElement>(null);
const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<Error | undefined>();
// TODO change to `useCurrentApp`
/** observe and use the current selected application from framework */
const { value: currentApp } = useObservableState(
useMemo(() => fusion.modules.app.current$, [fusion.modules.app]),
);
useEffect(() => {
/** when appKey property change, assign it to current */
fusion.modules.app.setCurrentApp(appKey);
}, [appKey, fusion]);
useEffect(() => {
/** flag that application is loading */
setLoading(true);
/** clear previous errors */
setError(undefined);
/** create a teardown of load */
const subscription = new Subscription();
/** make sure that initialize is canceled and disposed if current app changes */
subscription.add(
currentApp?.initialize().subscribe({
next: ({ manifest, script, config }) => {
/** generate basename for application */
const [basename] = window.location.pathname.match(
/\/?apps\/[a-z|-]+(\/)?/g,
) ?? [''];
/** create a 'private' element for the application */
const el = document.createElement('div');
if (!ref.current) {
throw Error('Missing application mounting point');
}
ref.current.appendChild(el);
/** extract render callback function from javascript module */
const render = script.renderApp ?? script.default;
/** add application teardown to current render effect teardown */
subscription.add(render(el, { fusion, env: { basename, config, manifest } }));
/** remove app element when application unmounts */
subscription.add(() => el.remove());
},
complete: () => {
/** flag that application is no longer loading */
setLoading(false);
},
error: (err) => {
/** set error if initialization of application fails */
setError(err);
},
}),
);
/** teardown application when hook unmounts */
return () => subscription.unsubscribe();
}, [fusion, currentApp, ref]);
if (error) {
if (error.cause instanceof AppManifestError) {
return (
<div>
<h2>🔥 Failed to load application manifest 🤬</h2>
<h3>{error.cause.type}</h3>
<ErrorViewer error={error} />
</div>
);
}
return (
<div>
<h2>🔥 Failed to load application 🤬</h2>
<ErrorViewer error={error} />
</div>
);
}
return (
<section id="application-content" ref={ref} style={{ display: 'contents' }}>
{loading && <EquinorLoader text="Loading Application" />}
</section>
);
};
export default AppLoader;