Skip to main content

App

About 1 minModuleapplicationmanifestapplication configapplication loadingapplication module instance

GitHub package.json version (subfolder of monorepo)

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

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');
                    el.style.display = 'contents';
                    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;