As applications complexity increases, frontends are also getting more and more complex. Micro frontend architectures, like microservice architectures, try to reduce the complexity by defining smaller and loosely coupled components that can be deployed individually. Cumulocity is built on top of a micro frontend architecture.
Introduction
There are several options for building a micro frontend architecture:
Server-side - This is the classic approach of a page loaded on a different URL. You can serve unique frontends from different teams (even on separate web servers) on each URL. This already fulfills all the requirements of a micro frontend, as it is loosely coupled, can be developed independently, and deployed separately. However, there are challenges when using this approach, for example, with the communication between the different micro frontends and ensuring a similar look and feel across the other frontends.
Cumulocity already allows using micro frontends via application hosting. You can deploy different frontends via the Application API) and switch between them via the app switcher. If you use the Web SDK, it ensures the same look and feel across all applications.
Compile-time - Another option to allow a micro frontend architecture is to bundle and build your micro frontend as a library and provide it (for example, via npm). The packages can then be used to compose or build a new application. This has many benefits, as the developer has full control. The communication can be clearly defined and the look and feel can be aligned. However, it requires a new build each time one of the components changes. The coupling is much closer.
Cumulocity provides different npm modules which let you use this approach. You can, for example, only use the API client, a default application with the styling, or import different features as Angular modules from the Cumulocity components library. Take a look at the Cumulocity npm options to see all provided libraries.
Runtime - Providing different frontends dynamically while the application is running. You can load different parts of an application in an iframe or only load a script bundle from a different server. However, the communication and coupling with, for example, an iframe is nearly as hard as when using the server side integration. Therefore, the runtime integration is more complex. It allows you to plug deep into the application and shares, for example, the communication or the state layer. New technologies like Module Federation allow for sharing certain dependencies and defining their scope.
We introduced a new plugin concept into Cumulocity which gives you the ability to extend any Web SDK based application at runtime.
Cumulocity is already based on a micro frontend architecture. In fact, the server-side option is a concept of the platform since the beginning. The compile-time option was introduced in 2018. However, there is also a need for the runtime extension of applications, which is why it is introduced to Cumulocity. See the sections below for more information.
Introducing plugins: Dynamically extending platform web applications
Plugins are a new concept to dynamically load features at runtime and allow an extension of any Web SDK based web application. To extend an application:
In the Administration application, clone the application you want to extend, for example clone the Cockpit application.
Open the Cockpit application details and click the Plugins tab.
Click Install plugins and select a plugin.
The application is now extended by the plugin you selected.
This is basically a script injection. The Cockpit application will now request a script called remoteEntry.js from the plugin. In terms of the micro frontend, the application that executes the call is called the “shell” which injects the “remote” into its scope.
Tip
If you have any issue with an application which includes a plugin, you can exclude all plugins via the ?noPlugin=true query parameter.
Those plugins can use any of the concepts that are integrated into the Web SDK. From just adding a certain button on a device to a full feature set with its own navigator node, route, and component. There are many options, but they are all within the borders of the Web SDK. Cumulocity does not support, for example, React or Vue. If you want to use other frameworks, refer to Cumulocity’s compile time integration.
We decided to limit the options to give the same developer experience as we use to build current Angular applications. As a developer, you can start your first plugin by using the c8ycli almost the same way you build a new application.
Custom widget plugin
See Create the widget components on how to create a simple widget, what its structure looks like and how to add it to your application.
The following tutorial focuses on how you can add this widget to an application using the micro frontend architecture and how this process differs from the previous one.
Info
The solution below is fully based on the Module Federation functionality introduced in Webpack 5. For more information on the functionality refer to Webpack: Module Federation.
1. Initialize the widget plugin example
Use the command shown below to start a multi-step process of creating a sample plugin:
c8ycli new
Select the plugin name, for example, “widget-plugin”:
? Enter the name of the project: (my-application) widget-plugin
Select the version for which you want to create a sample application, for example, “1016.0.233”, as micro frontend are supported with version higher then 10.16.0.0:
? Which base version do you want to scaffold from?
1015.0.372 (latest)
1018.106.0 (next)
1018.0.47
1017.0.151
❯ 1016.0.233
1014.0.359
other
Select an application template as the basis for your plugin, in our case, “widget-plugin”:
? Which base project do you want to scaffold from?
administration
application
cockpit
devicemanagement
hybrid
tutorial
❯ widget-plugin
After a few seconds, you should see the following message:
Application created. Go into the folder "widget-plugin" and run npm install
Navigate to your application folder and execute npm install.
The application folder should look like the example shown below.
For this tutorial, the most important files are package.json and README.md.
You have now created your first plugin that uses the micro frontend architecture.
2. Differences in approach to creating custom widgets
There are a couple of differences between a simple widget and one that is built according to the micro frontends architecture.
The biggest difference is the package.json file, where fields such as isPackage, package and exports are located.
The following list shows the fields and what they are responsible for:
isPackage: Indicates if the application is a package. In case of a widget that is added using micro frontends, set the value to true.
package: The type of package (for example, plugin).
exports: Important field. Defines the Angular modules that will be made available by the widget-plugin for the shell application (see also the README.md file):
name: The name of the exported module (that is, “Example widget plugin”).
module: The name of the Angular module class (that is, “WidgetPluginModule”).
path: The path to the TypeScript file with the module. Since the file is nested, use the following path: ./widget/widget-plugin.module.ts.
description: A brief description of what the module does.
contextPath: The context path tells on which URL your plugin can be loaded. As this is also used to generate a global variable, choose a valid JavaScript variable. For example, your contextPath should not start with a number. To avoid conflicts it is a good practice to add a prefix to your context path, for example, the acronym of your company: acme-.
versioningMatrix: Optional field which indicates the supported package versions and the recommended frontend and backend platform versions. If the package is not compatible with any of the recommended platform versions a warning is shown. The matrix must have the following format and all versions must be SemVer. For example: versioningMatrix: [{"1.0.0": {"api": ">1016.0.0", "sdk": ">1016.0.0"}}, {"2.0.0": {"api": ">1017.0.0", "sdk":">1017.0.0"}}]. In case of repository-connect, versions which exist in the platform but are not included in the versioning matrix are removed during sync.
Info
When creating plugins, the custom modules are the backbone of this approach. The exported module is treated as the entry point that links the plugin with the application, which is referred to as the shell. You can create and export several modules, which must contain ready-made functionality.
Furthermore, these modules behave like lazy loading modules. They are not loaded upfront as one big package, but instead like a collection of smaller packages loaded on demand.
You can extend each module with additional features through the HOOK concept, see Extend an existing application and use hooks for more information. For example, a plugin can add another entry to the navigation menu using HOOK_NAVIGATOR_NODES, see Hooking a navigator node for more information.
There is also a difference in how to start the local development server, see the following step for more information on the server’s role.
3. Local server, debugging and deployment
Local server
To facilitate the process of creating a new plugin, the local server command was extended with a new flag to proxy all requests to the shell application “Cockpit”.
This link redirects you to the Cockpit login screen.
Once logged in, go to your dashboard and click Add widget, then select Module Federation widget from the list of available widgets.
For the rest of the widget editing process follow the process for regular widgets. Refresh your browser to see your changes.
Debugging
Another difference in the package.json file between a regular widget and a widget modified for the micro frontend architecture is the field remotes, see the example below:
...
"remotes": {
"widget-plugin": [ // contextPath
"WidgetPluginModule"// module class name
]
}
...
Info
The remotes field is used to import modules. To properly import a module, specify the context path of the plugin (the contextPath field in package.json) followed by the name of the module class.
The plugin imports itself via a field called remotes.
We recommend this as the first step in verifying the correctness of the exported module. It facilitates the application debugging.
After importing your own modules, execute npm start to see if the local server starts.
To check the plugin at a later stage, we recommend you to test it locally with various shell applications, using npm start -- --shell cockpit.
Deployment
Uploading the widget is the same as for regular widgets.
Execute the following commands sequentially:
npm run build
and
npm run deploy
Follow the console prompt to deploy the application to your tenant.
4. Adding a deployed widget to the shell application
To add the uploaded widget-plugin to the dashboard in the Cockpit application, follow these steps:
Access the Packages tab in Administration application > Ecosystem > applications > Packages, where you can see the details of your plugin.
If you already have a custom Cockpit application, navigate to its Details page and then to the Plugins tab. Install the widget-plugin.
If you don’t have your own version of the Cockpit application, navigate to Administration application > Ecosystem > Applications and click Add application. In the resulting dialog, select the option Duplicate existing application. From the list of applications select Cockpit (Subscribed). Edit the available fields such as Name, Application key, and Path. Use the default values and proceed. Install the widget-plugin in the cloned application.
Your custom widget is now available in your version of the Cockpit application.
Navigate to the dashboard where the newly added widget is available in the list of widgets to add.
The widget-plugin was installed from within the Administration application. This is the main difference between the regular and the new approach regarding widgets.
The micro frontends architecture allows you to add new functionality while the application is running (runtime), whereas the old approach only allowed new functionality to be added before the application was built (compile time).
Custom package blueprint
With micro frontends it is possible to add new functionality while the application is running (run-time), whereas the old approach only allowed new functionality to be added before the application was built (compile-time).
Blueprints are combinations of multiple UI functionalities that can be hosted by the platform (static files) and can be used to scaffold a new solution from scratch. On the other hand, a package is the composition of plugins and blueprints. As a blueprint can export plugins as well, they can be packed together into one package and deployed to the platform.
Initialize the blueprint example
Use the command shown below to start a multi-step process of creating a sample blueprint:
c8ycli new
Select the plugin name, for example, “package-blueprint”:
? Enter the name of the project: (my-application) package-blueprint
Select the version for which you want to create a sample application, for example, “1016.0.233”, as micro frontends are supported with version higher then 10.16.0.0:
? Which base version do you want to scaffold from?
1015.0.372 (latest)
1018.106.0 (next)
1018.0.47
1017.0.151
❯ 1016.0.233
1014.0.359
other
Select an application template as the basis for your plugin, in our case, “package-blueprint”:
? Which base project do you want to scaffold from?
cockpit
devicemanagement
hybrid
> package-blueprint
tutorial
widget-plugin
administration
After a few seconds, you should see the following message:
Application created. Go into the folder "package-blueprint" and run npm install
Navigate to your application folder and execute npm install.
The application folder should look like the example shown below.
For this tutorial, the most important files are package.json and README.md.
You have now created your first package blueprint that uses Module Federation.
Stepper setup (optional)
The HOOK_STEPPER can be additionally provided to allow application customization during the first load of an application. In this optional step we show a small single step example in which the user can select whether the navigator will be collapsed or not on startup.
Create a new setup-step1.component.ts file with the following content:
Now on the first application start, users will have to complete the single step wizard above.
Differences in approach to creating custom applications
There are a couple of differences between a simple widget and one that is built according to the micro frontend guidelines.
The biggest difference is the package.json file, where fields such as isPackage and package and exports (not in the current blueprint app) are located.
The following list shows the fields and what they are responsible for:
isPackage: Indicates if the application is a package. In case of a widget that is added using a micro frontend, set the value to true.
package: The type of package (for example, blueprint, but the type of the package can also be a plugin).
exports: Important field. Defines the Angular modules that will be made available by the widget-plugin for the shell application (see also the README.md file).
name: The name of the exported module.
module: The name of the Angular module class.
path: The path to the TypeScript file with the module.
description: A brief description of what the module does.
contextPath: The context path tells on which URL your blueprint can be loaded. As this is also used to generate a global variable, choose a valid JavaScript variable. For example your contextPath should not start with a number. To avoid conflicts it is good practice to add a prefix to your context path, for example, the acronym of your company: acme-.
versioningMatrix: Optional field which indicates the supported package versions and the recommended frontend and backend platform versions. If the package is not compatible with any of the recommended platform versions a warning is shown. The matrix must have the following format and all versions must be SemVer. For example: versioningMatrix: [{"1.0.0": {"api": ">1016.0.0", "sdk": ">1016.0.0"}}, {"2.0.0": {"api": ">1017.0.0", "sdk":">1017.0.0"}}]. In case of repository-connect, versions which exist in the platform but are not included in the versioning matrix are removed during sync.
Info
A blueprint can also include plugins, which can later be used to extend other applications.
Deployment
Uploading the package is the same as for regular widgets.
Execute the following commands sequentially:
npm run build
and
npm run deploy
Follow the console prompt to deploy the application to your tenant.
Best practices for developers
It can be overwhemling to decide which approach to use and how to start your development journey.
If you are a partner or a customer we recommend you join the micro frontend journey.
It allows to extend and use the ecosystem instead of building silo solutions.
This section explains our vision of this journey and describes our best practices regarding development.
The user journey - blueprint or plugin
To develop the right thing it is very important to understand what kind of user you want to target with your application.
For the micro frontend story, the target audience are solution architects with little developer experience.
The idea is simple: Any IoT solution can be set up by a user in a few steps - without coding.
The building blocks of this are developed by the ecosystem.
The steps are the following:
A solution architect selects the blueprint to use. This is the foundation application of the solution.
The blueprint guides through the configuration of the application. One step is, for example, the installation of the required microservices or plugins.
After setup, the solution is ready to use. The solution architect can now test it with other users. There may be additional requirements which can be accounted for with the option to add or remove additional plugins.
The user journey for the micro frontend story is a a self-service approach.
You provide an application that a non-developer can align to their needs.
It assumes that the usage of the application is unknown and up to the solution architect.
Therefore those applications must be designed in a generic way and allow for reuse as much as possible.
Use the following decision tree to decide if you want to develop a classic application or a micro frontend:
Your use case needs a distinct design and components? --YES--> Consider a custom implementation.
|
NO
|
Your use case is very specific? --YES--> Consider a custom implementation based on the Web SDK.
|
NO
|
Your use case is an app that others can use? --YES--> Consider a micro frontend blueprint.
|
NO
|
Consider a micro frontend plugin.
The decision tree can be read from top (most effort, highest customization) to bottom (lowest effort, limited customization).
For example, if you choose to do everything on your own, you must write your own login logic and cannot use our pre-built components like data-grid, dashboard or asset-selector.
The development effort is very high.
If you use the Web SDK instead, you get a lot of functionality out of the box and by branding the application, you are able to make it look and feel like a product of your own development.
Furthermore, you can provide it as an application blueprint to your customers, enabling them to brand it according to their needs.
However, if one of the available blueprints (Administration > Ecosystem > Extensions) or the default applications (Cockpit, Device management or Administration) already mostly fit your case, but misses, for example, an extension, instead implement a plugin.
Plugins are easy to implement and can extend every part of the existing applications. Furthermore, if you intend to provide those extensions to your customers you can do so and share any plugin.
The next section explains the developer story of blueprints and plugins in detail and provides best practices.
Starting the developer journey for plugins and blueprints
All developer stories start with our CLI tool.
You can scaffold a new application and decide which demo you want to use.
For example, for a plugin you can try the widget-plugin demo.
This section does not go into details on how to do this (it was already explained in earlier sections).
Instead it explains what makes an application a plugin or a blueprint and what the difference is.
First of all, there is no big difference between usual applications, blueprints and plugins.
They are all built, tested and deployed via the application API.
However, plugins and blueprints have some detail information in their manifest file.
The manifest file contains all options that are stored in the c8y.application property in the package.json file.
At build time, it is compiled to the cumulocity.json file.
When you upload an archive containing a cumulocity.json file, its information is also accessible to the application API which makes the information available via request for other applications.
There are two important properties that indicate if the application is a plugin and which type it is.
There are additional properties that are either used for plugins or blueprints.
The following list consists of the properties that are relevant in a manifest file for a micro frontend:
The properties are explained in detail in the step-by-step scaffolding of a micro frontend.
However, there are additional properties that might be of interest for creating a meaningful plugin or blueprint:
c8y.application.noAppSwitcher: This should always be set to true to not show a blueprint or a plugin in the app-switcher (UI component in the upper right corner, to switch between your installed applications).
version: This is the version that is used and displayed to the end user. This version is pinned if a plugin is installed and you cannot upload the same version twice. Use semver to let the platform verify the version.
description: This is the first piece of information the user reads about you application: Describe your application in one sentence to encourage the user to open the detail view.
keywords: Can be used to furthermore classify your application.
author: Informs the user who created the plugin/blueprint.
license: Informs the user about the license used. Always provide license information. From version 10.18.0, all community packages ask to confirm the license.
repository: Use this to point to an external repository where the source code is hosted.
homepage: Use this to point to an external application where more information can be found.
requiredPlatformVersion: Use this to point out which backend version you support.
Additionally you can add more information inside a detailed README.md file which is then displayed in the package detail view.
Those packages are the released artifact.
When you are ready, deploy a package to the platform which everyone can use to extend an existing application ( with a plugin) or install a new application (with a blueprint).
The next section explains what a package is and what it contains.
Packages and their content
As pointed out in the user-stories, you can develop two kinds of micro frontends: Blueprints and plugins.
Both are applications that are uploaded to the application API and hosted by it.
As any of those hosted applications can contain more than one plugin or even a combination of blueprints and plugins, a new conceptual unit was introduced called ‘packages’.
Packages allow you to bundle multiple plugins and/or a blueprint into one package and provide a version for them. An optimal package contains:
one or more plugins and/or a blueprint
a README.md file explaining the content
a LICENSE file which contains details about the license used
The idea of a package is to bundle multiple belonging plugins together that can be managed by a blueprint application. So a good use case could be a smart-city management app that exposes multiple widgets to display the information (for example a “free parking spots”-widget) to the cockpit application.
Where can I extend existing applications with an plugin?
The extension ways did not change. You can use any of our HOOK_ interfaces as defined in our ngx-components library. Usual things to hook are a NavigatorNode, a ActionBarItem or a Route.
Tip
From version 10.17.0, there is a typed helper function called hook<<Name>>.
For example, you can use hookNavigator() in a provider to hook a node into the navigator.
Debugging an application
A plugin is a lazily loaded Angular module.
Therefore you can use the default Angular modules and verify them with the default developer story.
Cumulocity provides two more verification methods, so there are three ways of verifying and debugging your application:
Classic: Run your application and import your module into the AppModule. Then start your application by running c8ycli serve.
Lazy loading: Run your application but don’t import the module. Instead, point the remote to your module (this is basically a self imported plugin). Point the c8y.application.remotes option to your module:
This has the benefit that your application acts as a shell and you can see where you might have issues with lazy loading.
Shell: Run the application and point it to any shell by running c8ycli serve --shell cockpit. This has the benefit of testing it in a real application. But as the shell application is already deployed, you might be getting unhelpful error-stacks.
We recommend you to use method 2, lazy loading.
If required, verify your application with method 3.
Avoid method 1 if you can, as you could run into common pitfalls explained in the next section.
Common developer pitfalls when developing a plugin
There are several issues to avoid:
Routing: avoid commonly named routes. Don’t use routes like /home instead, use /<<my-unique-prefix>>/home. Commonly named routes can be overwritten by other plugins. Do the same for any identifier you use in your development process.
Lazy loading: Remember that every plugin is imported lazily. This means that the rules of lazily loaded modules apply to those modules. Don’t use forRoot on the ngx-components CoreModule or the RoutingModule. Use forRoot for any newly introduced dependency.
Injectors: With the lazy loading approach, injectors are sometimes an issue. Usually you have a new injector per plugin. This is done automatically as long as you use the hooks without a factory function. If you use a factory function, you must provide the injector:
(1): This is important if the component you provided wants to use a service that is only available in your plugin. If you don’t define the injector, it will use the root injector and therefore will lead to injector issues.
How to bundle assets in a package
Bundling assets is not as easy as only copying them over. The path must be correctly reflected. For example, if you import an image, the path to the image is something like:
http://<<instance>>/apps/<<context-path>>@<<version>>/my-image.png.
You may not want to change the version on each deployment.
Therefore we recommend you to let the bundler handle images.
This is done by importing an image into a typescript file.
The bundler always returns the correct path to the image.
For example, create a file assets/assets.ts containing the following:
Typescript will throw an error, as the it does not know how to handle PNG files.
Tell typescript to accept such files by declaring them as a module in the * assets/index.d.ts* file:
declare module '*.png';
Now import the asset somewhere in your plugin or blueprint and use the path to display the image:
import { assetPaths } from '../assets/assets';
console.log(assetPaths.previewImage);
The assetPaths.previewImage can now be used in any component or in a hook.
You can see a full example when you scaffold the widget-plugin with c8ycli new.
Note that the feature and example was added with version 10.17.0.
Tip
The recommended size for preview images used for widgets is 340 x 340 pixels.
Translations
Translations should work out of the box as for standard custom application.
Add a .po file to your repository and import it into your index.ts file.
The translation of a plugin might overwrite existing translations, as they are merged at run-time.
An example is created when you scaffold the widget-plugin with c8ycli new.
Note that there is a limitation: A plugin cannot add a new language. It can only extend the translated strings in the existing application.
Styling and branding
Branding is fully supported.
We recommend you to use component-based styling for your applications.
However you can also add custom global CSS styling by importing it.
import './example.css';
Again an example is created if you scaffold the widget-plugin with c8ycli new.
How to ensure application compatibility
There is no way of ensuring general compatibility.
Every major Web SDK version may contain a new Angular version.
Angular is one of our libraries shared between all micro frontends and therefore calling a deprecated method on them might break the compatibility between a micro frontend and the application that imports it.
However we have two version protection methods that avoid such incompatibilities:
You can only add plugins to so called “custom” applications. Custom applications mean that you must own the application. As a side effect, you don’t get automatic updates. This ensures that any platform update does not break your application.
Plugins are always versioned. An update to a plugin results in a new version, and if it is incompatible, the update doesn’t break.
You can still update applications and plugins. For plugin updates we recommend you to clone the actual application and test if the plugin still works with the newer version. The cloned application is a test application and can be deleted afterwards.
For blueprints, you will get a notification that suggests to update the application. Those updates can also be tested and rolled back if some plugins fail.
From version 10.19.0. we also provide an additional version matrix showing exactly which version of a plugin is compatible with which version of the Web SDK.
For plugin developers who want to always provide the most compatible version of their plugin, we recommend our community plugin Github project, which includes some CI/CD workflows to test and verify that the newest version of a plugin still works.
How to use repository connect
Repository connect is a microservice which synchronizes plugins or blueprints with an instance of the Cumulocity platform.
It must be installed on the Management tenant and you can connect multiple repositories.
Currently, only our Cumulocity public GitHub is connected.
You can participate and share blueprints or plugins in multiple ways:
Contribute to our open source plugins. A list can be found in our Cumulocity GitHub packages. There is an official repository which is managed by the internal R&D team of Cumulocity.
Configure repository connect on your on-prem instance and point it to your organization.
Ask our product manager to add your repository as a partner repository.
Info
This is only needed if you want to share an application with every Cumulocity customer. If you want to share a package with your customers (for example on an Enterprise tenant) you can simply upload them in the Packages view and set the availability to ‘shared’.
For synchronization the microservice searches for all repositories with a certain topic and a release (for Cumulocity it’s cumulocity-package).
The release should be a single ZIP file containing the plugin or blueprint.
There is a security mechanism in place which is called scoping which disallows uploading an application without a certain prefix.
This is to avoid that any synced package can overwrite a default application like cockpit or administration.
Prefix the key and contextPath with the configured prefix (for Software AG it’s sag-pkg).
All applications that are uploaded via repository connect are labeled community plugins and the user is informed of the license and maintenance agreements on installation (from version 10.18.0).