Tutorials

This section lists common how-to recipes for the Web SDK. It requires:

  • a basic understanding of Angular components, services and modules.
  • an understanding on how to scaffold an application and how to run it with the @c8y/cli.
  • a basic understanding of the extension points concepts of the @c8y/ngx-components.
Info
All recipes are written with a particular version of Web SDK for Angular. The version is mentioned at the top of the recipe. It is not recommended to use an older version for a recipe, as some of the mentioned features might not be available. If you use a newer version, you might face naming or import changes. We will update the recipes if there are conceptual revisions but not for small variations. Check out the tutorial application with c8ycli new my-app tutorial to have an up-to-date example of all concepts.

Add a custom widget to a dashboard

Version: 1009.0.18 | Packages: @c8y/cli, @c8y/apps and @c8y/ngx-components

If the widgets that are provided by the platform do not meet your requirements, you might want to create a custom widget and add it to a dashboard.

A typical dashboard looks like this, showing various widgets:

A dashboard

This recipe will show how to archive a custom widget to a dashboard with the HOOK_COMPONENTS.

1. Initialize the example application

As a starting point, you need an application showing dashboards. For this purpose, create a new Cockpit application using the c8ycli:

c8ycli new my-cockpit cockpit -a @c8y/apps@1009.0.18

Next, you must install all dependencies. Switch to the new folder and run npm install.

Info

The c8ycli new command has a -a flag which defines which package to use for scaffolding. This way you can also define which version of the application you want to scaffold, for example:

  • c8ycli new my-cockpit cockpit -a @c8y/apps@1009.0.18 will scaffold an application with the version 10.9.0.18
  • c8ycli new my-cockpit cockpit -a @c8y/apps@latest will scaffold an application with the latest official release. Same as if used without the -a flag
  • c8ycli new my-cockpit cockpit -a @c8y/apps@next will scaffold an application with the latest beta release.

2. Create the widget components

Widgets usually consist of two parts:

That is why you must create two components.

First, create the demo-widget.component.ts:

import { Component, Input } from '@angular/core';

@Component({
  selector: 'c8y-widget-demo',
  template: `<p class="text">{{config?.text || 'No text'}}</p>`,
  styles: [ `.text { transform: scaleX(-1); font-size: 3em ;}` ]
})
export class WidgetDemo {
  @Input() config;
}

The component will show a configured text which is vertically mirrored via CSS. You can do anything in it that you can also do in other Angular components.

It must have the config input to pass the configuration from the demo-widget-config.component.ts which is defined as the following:

import { Component, Input } from '@angular/core';

@Component({
  selector: 'c8y-widget-config-demo',
  template: `<div class="form-group">
    <c8y-form-group>
      <label translate>Text</label>
      <textarea style="width:100%" [(ngModel)]="config.text"></textarea>
    </c8y-form-group>
  </div>`
})
export class WidgetConfigDemo {
  @Input() config: any = {};
}

Again, you must add a config object which you can fill with any serializable configuration that you want to pass to the widget.

To enable the widget configuration validation, the following option should be added to the @Component decorator.

import { ControlContainer, NgForm } from "@angular/forms";

@Component({
    ...
    viewProviders: [{ provide: ControlContainer, useExisting: NgForm }]
})

Combined with the example above, the demo-widget-config.component.ts component with enabled configuration validation will be:

import { Component, Input } from '@angular/core';
import { ControlContainer, NgForm } from "@angular/forms";

@Component({
  selector: 'c8y-widget-config-demo',
  template: `<div class="form-group">
    <c8y-form-group>
      <label translate>Text</label>
      <textarea style="width:100%" [(ngModel)]="config.text" name="text"></textarea>
    </c8y-form-group>
  </div>`,
  viewProviders: [{ provide: ControlContainer, useExisting: NgForm }]
})
export class WidgetConfigDemo {
  @Input() config: any = {};
}

3. Add the widget to your application

To add the widget you must use the HOOK_COMPONENTS and define the created components as entryComponent.

To do so, add the following to your app.module.ts:

import { NgModule } from '@angular/core';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { RouterModule as NgRouterModule } from '@angular/router';
import { UpgradeModule as NgUpgradeModule } from '@angular/upgrade/static';
import { DashboardUpgradeModule, UpgradeModule, HybridAppModule, UPGRADE_ROUTES} from '@c8y/ngx-components/upgrade';

// --- 8< changed part ----
import { CoreModule, RouterModule, HOOK_COMPONENTS } from '@c8y/ngx-components';
// --- >8 ----

import { AssetsNavigatorModule } from '@c8y/ngx-components/assets-navigator';
import { CockpitDashboardModule, ReportDashboardModule } from '@c8y/ngx-components/context-dashboard';
import { ReportsModule } from '@c8y/ngx-components/reports';
import { SensorPhoneModule } from '@c8y/ngx-components/sensor-phone';
import { BinaryFileDownloadModule } from '@c8y/ngx-components/binary-file-download';

// --- 8< added part ----
import { WidgetDemo } from './demo-widget.component';
import { WidgetConfigDemo } from './demo-widget-config.component';
// --- >8 ----

@NgModule({
  imports: [
    // Upgrade module must be the first
    UpgradeModule,
    BrowserAnimationsModule,
    RouterModule.forRoot(),
    NgRouterModule.forRoot([...UPGRADE_ROUTES], { enableTracing: false, useHash: true }),
    CoreModule.forRoot(),
    AssetsNavigatorModule,
    ReportsModule,
    NgUpgradeModule,
    DashboardUpgradeModule,
    CockpitDashboardModule,
    SensorPhoneModule,
    ReportDashboardModule,
    BinaryFileDownloadModule
  ],

  // --- 8< added part ----
  declarations: [WidgetDemo, WidgetConfigDemo],      // 1.
  entryComponents: [WidgetDemo, WidgetConfigDemo],
  providers: [{
    provide: HOOK_COMPONENTS,                         // 2.
    multi: true,
    useValue: [
      {
        id: 'acme.text.widget',                        // 3.
        label: 'Text widget',
        description: 'Can display a text',
        component: WidgetDemo,                         // 4.
        configComponent: WidgetConfigDemo,
      }
    ]
  }],
  // --- >8 ----

})
export class AppModule extends HybridAppModule {
  constructor(protected upgrade: NgUpgradeModule) {
    super();
  }
}

Explanation of the numbers above:

  1. Define the components as entry components and declare them to make them accessible by this module.
  2. Add a multi-provider hook with the HOOK_COMPONENTS. This hook is collected by the application and adds the widget based on the values you provide.
  3. The ID needs to be unique as it identifies the data stored in the inventory. The label and description is shown as the title and in the widget dropdown.
  4. These parts tell the hook to associate the previously defined components to the widget.

If you now start your application with npm start, you should be able to add your custom widget to a dashboard.

Once added to a dashboard, the widget looks similar to this:

A custom widget

Add a Jest-based unit test

Version: 1013.0.0 | Packages: @c8y/cli, @c8y/apps and @c8y/ngx-components

Unit testing is an essential part of every development process. Since version 10.13.0.0, all new c8ycli scaffolded applications include the unit test framework Jest by default. This tutorial shows you how to write and verify your first unit test.

1. Initialize the example application

You need an application, for example, the empty default application:

c8ycli new my-app application -a @c8y/apps@1013.0.62

However, any application supports unit tests in the same way. Next, you must install all dependencies.

Switch to the new folder and run npm install.

Info

The c8ycli new command has a -a flag which defines which package to use for scaffolding. This way you can also define which version of the application you want to scaffold, for example:

  • c8ycli new my-cockpit cockpit -a @c8y/apps@1013.0.62 will scaffold an application with the version 1013.0.62.
  • c8ycli new my-cockpit cockpit -a @c8y/apps@latest will scaffold an application with the latest official release. Same as if used without the -a flag.
  • c8ycli new my-cockpit cockpit -a @c8y/apps@next will scaffold an application with the latest beta release.

2. Add a component

To test something, you first need a component that you can verify. Therefore, add a new file called test.component.ts:

import { Component, OnInit } from '@angular/core';

@Component({
  selector: 'app-test',
  template: `<h1>Hello world</h1>`
})
export class TestComponent implements OnInit {
  constructor() { }

  ngOnInit(): void { }
}

Add the newly created component to the declarations of your app.module.ts:

import { NgModule } from '@angular/core';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { RouterModule as ngRouterModule } from '@angular/router';
import { CoreModule, BootstrapComponent, RouterModule } from '@c8y/ngx-components';

// --- 8< changed part ----
import { TestComponent } from ### 2. Adding a component
    ngRouterModule.forRoot([], { enableTracing: false, useHash: true }),
    CoreModule.forRoot()
  ],
  bootstrap: [BootstrapComponent],

  // --- 8< changed part ----
  declarations: [
    TestComponent
  ]
  // --- >8 ----
})
export class AppModule {}

After the example component is added to the module, the component is ready for testing.

2. Add a unit test for the test component

Test files have the file extension .spec.ts. There is an example spec file in the repository called app.module.spec.ts. Rename this spec file to test.component.spec.ts and align the content to:

import { ComponentFixture, TestBed } from '@angular/core/testing';
import { AppModule } from './app.module';
import { TestComponent } from './test.component';

describe('Test component test', () => {
  let fixture: ComponentFixture<TestComponent>;

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [AppModule]
    });
    fixture = TestBed.createComponent(TestComponent);
  });

  test('should be defined', () => {
    expect(fixture).toBeDefined();
  });
});

This is your first test file. It configures an Angular testing module and checks if the TestComponent can be defined. You can read more about Angular testing support on the Angular website.

To start the test, run npm test on your command line. This executes the predefined script in the package.json which then starts Jest. You should see the following test result:

 PASS  ./test.component.spec.ts (32.071 s)
  Test component test
    ✓ should be defined (123 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        32.858 s

If the test says PASS, everything went well and your first component test was successful. Now, you can add more detailed test cases to verify your component works as intended.

3. Use a snapshot test to verify the component template

This section provides you with additional information on other ways to verify the component template.

We use Jest instead of Karma as it comes with the option to use so called snapshot tests. Snapshot tests allow the verification of the outcome of a test without defining all results. The Jest function toMatchSnapshot() creates a file which contains the snapshot of the test on the first run. Create another test, which will use snapshot testing, to verify the template of our TestComponent by adding the following to your test.component.spec.ts file:

test("should show a title tag", () => {
  expect(fixture.debugElement.nativeElement).toMatchSnapshot();
});

Run npm test. The result should say that a snapshot is written:

PASS  ./test.component.spec.ts
  Test component test
    ✓ should be defined (94 ms)
    ✓ should show a title tag (29 ms)

 › 1 snapshot written.
Snapshot Summary
 › 1 snapshot written from 1 test suite.

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   1 written, 1 total
Time:        5.154 s

You can find and verify this snapshot in the newly created folder ./__snapshot__. When the template changes, the test will fail and you must overwrite your test with npm test -- -u. You can test this behavior by changing your template in the test.component.ts file.

Info
It is common practice to commit these snapshots with your code.

Conclusion

This tutorial showed you how to add tests to newly scaffolded applications via the c8ycli command. The advanced snapshot testing has the option to verify templates quickly.

Add a tab to a details views with context routes

Version: 1009.0.18 | Packages: @c8y/cli, @c8y/apps and @c8y/ngx-components

It is a common use case that you want to show additional information to a user in a details view, for example, for a device or a group.

This how-to recipe explains how to create a new tab in the device details view:

Device info with custom tab

In Web SDK for Angular, this kind of view is called ViewContext as it provides a view for a certain context. There are a couple of context views, for example, Device, Group, User, Application and Tenant. You can access them by navigating to a certain Route with the hash navigation. For example, if you go to the route apps/cockpit/#/device/1234, the application tries to resolve the device with the ID 1234.

The details view usually shows a couple of Tabs, like the Info tab in the screenshot above. It is referenced by another route called /info but reuses the context of the device to show information about it.

In the following, we will guide you through the process of creating a new tab for this view that is accessible through the route apps/cockpit/#/device/:id/hello.

1. Initialize the example application

As a starting point, you need an application supporting context routes. For this purpose, create a new Cockpit application using the c8ycli:

c8ycli new my-cockpit cockpit  -a @c8y/apps@1009.0.18

Next, you must install all dependencies. Switch to the new folder and run npm install.

Info

The c8ycli new command has a -a flag which defines which package to use for scaffolding. This way you can also define which version of the application you want to scaffold, for example:

  • c8ycli new my-cockpit cockpit -a @c8y/apps@1009.0.18 will scaffold an application with the version 1009.0.18
  • c8ycli new my-cockpit cockpit -a @c8y/apps@latest will scaffold an application with the latest official release. Same as if used without the -a flag
  • c8ycli new my-cockpit cockpit -a @c8y/apps@next will scaffold an application with the latest beta release.

2. Add a new ROUTE_HOOK_ONCE

The hook concept allows you to hook into the existing code. In this example we want to add a so-called ChildRoute (by Angular) on the existing route device/:id.

To achieve this, add the following code to the app.module.ts:

import { NgModule } from '@angular/core';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { RouterModule as NgRouterModule } from '@angular/router';
import { UpgradeModule as NgUpgradeModule } from '@angular/upgrade/static';
// ---- 8< changed part ----
import { CoreModule, RouterModule, HOOK_ONCE_ROUTE, ViewContext } from '@c8y/ngx-components';
// ---- >8 ----
import { DashboardUpgradeModule, UpgradeModule, HybridAppModule, UPGRADE_ROUTES} from '@c8y/ngx-components/upgrade';
import { AssetsNavigatorModule } from '@c8y/ngx-components/assets-navigator';
import { CockpitDashboardModule, ReportDashboardModule } from '@c8y/ngx-components/context-dashboard';
import { ReportsModule } from '@c8y/ngx-components/reports';
import { SensorPhoneModule } from '@c8y/ngx-components/sensor-phone';
import { BinaryFileDownloadModule } from '@c8y/ngx-components/binary-file-download';

@NgModule({
  imports: [
    // Upgrade module must be the first
    UpgradeModule,
    BrowserAnimationsModule,
    RouterModule.forRoot(),
    NgRouterModule.forRoot([...UPGRADE_ROUTES], { enableTracing: false, useHash: true }),
    CoreModule.forRoot(),
    AssetsNavigatorModule,
    ReportsModule,
    NgUpgradeModule,
    DashboardUpgradeModule,
    CockpitDashboardModule,
    SensorPhoneModule,
    ReportDashboardModule,
    BinaryFileDownloadModule
  ],

  // ---- 8< added part ----
  providers: [{
    provide: HOOK_ONCE_ROUTE,          // 1.
    useValue: [{                       // 2.
      context: ViewContext.Device,     // 3.
      path: 'hello',                   // 4.
      component: HelloComponent,       // 5.
      label: 'hello',                  // 6.
      priority: 100,
      icon: 'rocket'
    }],
    multi: true
  }]
  // ---- >8 ----

})
export class AppModule extends HybridAppModule {
  constructor(protected upgrade: NgUpgradeModule) {
    super();
  }
}

Explanation of the numbers above:

  1. Provides the multi-provider hook HOOK_ROUTE_ONCE. This tells the application to extend the current route configuration.
  2. Specifies that we want to use a value to define the route hook. You can also use a class here, for example, if you want to resolve the routes asynchronously.
  3. Defines the context of the route. Use the ViewContext enum to define it. For this example you want to extend the context of a device.
  4. The path where it is going to be shown. It is added to the context path. For this example the complete path is: device/:id/hello.
  5. Defines which component should be shown if the path is hit by a user.
  6. The properties label and icon define what the tab should look like. The priority defines in which position it should be shown.
Info
The HOOK_ONCE_ROUTE inherits the Angular Route type. All of its properties can be reused here.

After this alignment the route is registered but the application will fail to compile as the HelloComponent does not exist yet. You will create it in the next section.

3. Add a component to display context data

The HelloComponent might want to display details about the device. To do this, it needs information about the context it has been opened in. The context route resolves the device upfront. You can directly access it via the parent route.

Create a new file called hello.component.ts:

import { Component } from '@angular/core';
import { ActivatedRoute } from '@angular/router';

@Component({
  selector: 'app-hello',
  template: `
  <c8y-title>world</c8y-title>
  <pre>
    <code>
      {{route.snapshot.parent.data.contextData | json}}
    </code>
  </pre>
  `
})
export class HelloComponent {
  constructor(public route: ActivatedRoute) {}
}

This component injects the ActivatedRoute and accesses its parent data. This is the key point: as the parent context route already has resolved the data of the device, this component will always show the detailed data of the current device.

Adding it to the entryComponents in app.module.ts will allow you to compile the application:


import { NgModule } from '@angular/core';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { RouterModule as NgRouterModule } from '@angular/router';
import { UpgradeModule as NgUpgradeModule } from '@angular/upgrade/static';
import { CoreModule, RouterModule, HOOK_ONCE_ROUTE, ViewContext } from '@c8y/ngx-components';
import { DashboardUpgradeModule, UpgradeModule, HybridAppModule, UPGRADE_ROUTES} from '@c8y/ngx-components/upgrade';
import { AssetsNavigatorModule } from '@c8y/ngx-components/assets-navigator';
import { CockpitDashboardModule, ReportDashboardModule } from '@c8y/ngx-components/context-dashboard';
import { ReportsModule } from '@c8y/ngx-components/reports';
import { SensorPhoneModule } from '@c8y/ngx-components/sensor-phone';
import { BinaryFileDownloadModule } from '@c8y/ngx-components/binary-file-download';
// ---- 8< added part ----
import { HelloComponent } from './hello.component';
// ---- >8 ----

@NgModule({
  imports: [
    // Upgrade module must be the first
    UpgradeModule,
    BrowserAnimationsModule,
    RouterModule.forRoot(),
    NgRouterModule.forRoot([...UPGRADE_ROUTES], { enableTracing: false, useHash: true }),
    CoreModule.forRoot(),
    AssetsNavigatorModule,
    ReportsModule,
    NgUpgradeModule,
    DashboardUpgradeModule,
    CockpitDashboardModule,
    SensorPhoneModule,
    ReportDashboardModule,
    BinaryFileDownloadModule
  ],

  // ---- 8< added part ----
  declarations: [HelloComponent],
  entryComponents: [HelloComponent],
  // ---- >8 ----

  providers: [{
    provide: HOOK_ONCE_ROUTE,
    useValue: [{
      context: ViewContext.Device,
      path: 'hello',
      component: HelloComponent,
      label: 'hello',
      priority: 100,
      icon: 'rocket'
    }],
    multi: true
  }]
})
export class AppModule extends HybridAppModule {
  constructor(protected upgrade: NgUpgradeModule) {
    super();
  }
}

When you now start your application with npm start and navigate to a details view of a device it should look like this:

Device info with custom tab

You have now added a tab to a device. You can do the same for tenants, users or applications details views.

Next you will learn how to show this tab only if a condition is met.

(Bonus) 4. Show the tab only if a condition is met

In some cases, additional information is available only if a condition is met. For example, it only makes sense to show a location if the device has a location fragment associated. To add such a condition, the context routes inherit the guard concept of Angular.

To add a guard, you simply need to add the canActivate property to the route definition:

import { NgModule } from '@angular/core';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { RouterModule as NgRouterModule } from '@angular/router';
import { UpgradeModule as NgUpgradeModule } from '@angular/upgrade/static';
import { CoreModule, RouterModule, HOOK_ONCE_ROUTE, ViewContext } from '@c8y/ngx-components';
import { DashboardUpgradeModule, UpgradeModule, HybridAppModule, UPGRADE_ROUTES} from '@c8y/ngx-components/upgrade';
import { AssetsNavigatorModule } from '@c8y/ngx-components/assets-navigator';
import { CockpitDashboardModule, ReportDashboardModule } from '@c8y/ngx-components/context-dashboard';
import { ReportsModule } from '@c8y/ngx-components/reports';
import { SensorPhoneModule } from '@c8y/ngx-components/sensor-phone';
import { BinaryFileDownloadModule } from '@c8y/ngx-components/binary-file-download';
import { HelloComponent } from './hello.component';

// ---- 8< added part ----
import { HelloGuard } from './hello.guard';
// ---- >8 ----

@NgModule({
  imports: [
    // Upgrade module must be the first
    UpgradeModule,
    BrowserAnimationsModule,
    RouterModule.forRoot(),
    NgRouterModule.forRoot([...UPGRADE_ROUTES], { enableTracing: false, useHash: true }),
    CoreModule.forRoot(),
    AssetsNavigatorModule,
    ReportsModule,
    NgUpgradeModule,
    DashboardUpgradeModule,
    CockpitDashboardModule,
    SensorPhoneModule,
    ReportDashboardModule,
    BinaryFileDownloadModule
  ],
  declarations: [HelloComponent],
  entryComponents: [HelloComponent],
  providers: [

    // ---- 8< added part ----
    HelloGuard,
    // ---- >8 ----

    {
    provide: HOOK_ONCE_ROUTE,
    useValue: [{
      context: ViewContext.Device,
      path: 'hello',
      component: HelloComponent,
      label: 'hello',
      priority: 100,
      icon: 'rocket',

      // ---- 8< added part ----
      canActivate: [HelloGuard]
      // ---- >8 ----

    }],
    multi: true
  }]
})
export class AppModule extends HybridAppModule {
  constructor(protected upgrade: NgUpgradeModule) {
    super();
  }
}

Now you can write a guard which checks a certain condition. If it resolves to true, the tab will be shown, otherwise not.

A guard to check for a certain fragment on a device can look like hello.guard.ts:

import { Injectable } from '@angular/core';
import { CanActivate, ActivatedRouteSnapshot } from '@angular/router';
import { Observable } from 'rxjs';

@Injectable()
export class HelloGuard implements CanActivate {

  canActivate(route: ActivatedRouteSnapshot): Observable<boolean> | Promise<boolean> | boolean {
    const contextData = route.data.contextData || route.parent.data.contextData;          // 1.
    const { 'acme_HelloWorld': helloWorldFragment } = contextData;                        // 2.
    return !!helloWorldFragment;
  }
}

Explanation of the above numbers:

  1. This is the only part which is not aligned with the Angular router. In a context route, CanActivate will be called twice, once when the parent route is activated and once when the child route is activated. The first call checks if the tab should be shown at all, while the second call checks if the user is allowed to navigate to it. Hence, the ActivatedRouteSnapshot is different in both calls and you must resolve the contextData in the second case from the parent.
  2. Checks if the acme_HelloWorld fragment is set on the context.

If you now post a device with the fragment "acme_HelloWorld": {} to the API, the Hello tab will only be shown for that device and not for others.

Conclusion

Context routes help you to extend existing routes with further information.

At the same time, the concept allows the application to be consistent since the context is just resolved once and if the context is not found, it can be handled by the parent.

However, there is currently no default way of abstracting the context route concept and implementing your own. Since the concept is heavily based on Angular routing you can implement the concept yourself.

Custom widget plugin with Module Federation

Version: 1011.153.0 | Packages: @c8y/cli, @c8y/apps and @c8y/ngx-components

See Add a custom widget to a dashboard > 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 Module Federation and how this process differs from the previous one.

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, “1013.72.0 (next)":

? Which base version do you want to scaffold from? (Use arrow keys)
  1011.0.18 (latest)
❯ 1013.72.0 (next)
  1013.0.63
  1010.0.29
  1009.0.33
  1007.0.47
  other

Select an application template as the basis for your plugin, for example, “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.

app.module.spec.ts;
jest.config.js;
README.md;
tsconfig.spec.json;
app.module.ts;
package.json;
setup-jest.js;
widget/;
index.ts;
polyfills.ts;
tsconfig.json;

You have now created your first plugin that uses Module Federation.

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 Module Federation guidelines.

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:

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 have to 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”.

Run npm install, then start your local server:

npm start -- --shell cockpit

You should see the following output:

Shell application: cockpit
http://localhost:9000/apps/cockpit/index.html?remotes=%7B%22widget-plugin%22%3A%5B%22WidgetPluginModule%22%5D%7D

The link redirects you to the Cockpit login screen. Once logged in, add the widget-plugin to your dashboard in the Add widget dialogue window shown below:

Add widget

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 Module Federation is the field remote, see 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 control 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

Currently, the views and logic related to Module Federation are hidden behind a beta flag. To add the uploaded widget-plugin to the dashboard in the Cockpit application, follow these steps:

You should now be able to access the Packages tab in Administration application > Ecosystem > applications > Packages, where you can see the details of your plugin.

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. Module Federation 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).

Extend an existing application and use hooks

Version: 1009.0.18 | Packages: @c8y/cli, @c8y/apps and @c8y/ngx-components

It is a common use case to extend one of our existing applications like Cockpit or Device Management.

This recipe explains step by step, how you can extend the Cockpit application with a custom route and hook this route into the navigator. First, we will provide some background on what we call a hybrid application and what the @c8y/apps npm package contains.

Brief background

The default applications consist of three applications which are shipped with our platform by default. As these applications are a result of several years of development, the code is mainly based on angularjs. As we now use Angular for all of our build-in applications, we needed a solution to serve both frameworks. The @c8y/cli allows scaffolding a default application which does exactly that. It uses the Angular upgrade functionality to serve an Angular and angularjs application at the same time. This enables us to develop new features in Angular while every angularjs plugin can be integrated. This is what we call the hybrid mode.

The hybrid mode, however, comes with some limitations we will cover later in this recipe. Due to these limitations, we decided to provide a pure Angular empty starter application which comes without the possibility to integrate angularjs plugins. This pure version of the application comes in two flavors:

There are three possibilities in total to start with the Web SDK:

Which one to choose heavily depends on the application you want to build. For example, if you want an application that just follows the look and feel of the platform but want to use special dependencies for certain scenarios, for example, the Material framework, you are best set with the pure Angular CLI solution.

The most common use case is the extension of a hybrid application, which we will cover in this recipe. First, take a look at the limitations of that approach to understand why the concepts are designed the way they are.

Hybrid mode limitations

As we need to make sure that Angular and angularjs run side by side when running a hybrid application, there are some limitations:

Now that you know the limitations you can start to extend the first application and develop your first extension hook. To do so, you must scaffold a hybrid application. @c8y/apps is a package which contains the default applications and their minimum setup. The c8ycli uses this package every time you initialize an application with the new command. The next section will explain the scaffolding process and how to extend a hybrid application step by step.

1. Initialize the example application

As a starting point, you need an application. For this purpose, create a new Cockpit application using the c8ycli:

c8ycli new my-cockpit cockpit -a @c8y/apps@1009.0.18

Next, you must install all dependencies. Switch to the new folder and run npm install.

Info

The c8ycli new command has a -a flag which defines which package to use for scaffolding. This way you can also define which version of the application you want to scaffold, for example:

  • c8ycli new my-cockpit cockpit -a @c8y/apps@1009.0.18 will scaffold an application with the version 10.9.0.18
  • c8ycli new my-cockpit cockpit -a @c8y/apps@latest will scaffold an application with the latest official release. Same as if used without the -a flag
  • c8ycli new my-cockpit cockpit -a @c8y/apps@next will scaffold an application with the latest beta release.

2. Bind a custom component to a route

Routes can be added the same way as in Angular. The only difference is that it needs to be defined before the UPGRADE_ROUTES because of the hybrid limitations. Create the hello.component.ts file in our project with the following content:

import {Component} from "@angular/core";

@Component({
  selector: "app-hello",
  template: `
    <c8y-title>Hello</c8y-title>
    World
  `,
})
export class HelloComponent {
  constructor() {}
}

This is a basic component. Only the template uses a special feature called “content projection” to show a title. Content projection is an Angular concept used to display content in other places than they are defined. For more information on which components support content projection refer to the @c8y/ngx-components documentation.

We can now bind this custom component to a route by changing the app.module.ts in the following way:

import {NgModule} from "@angular/core";
import {BrowserAnimationsModule} from "@angular/platform-browser/animations";
import {RouterModule as NgRouterModule} from "@angular/router";
import {UpgradeModule as NgUpgradeModule} from "@angular/upgrade/static";
import {CoreModule, RouterModule} from "@c8y/ngx-components";
import {DashboardUpgradeModule, UpgradeModule, HybridAppModule, UPGRADE_ROUTES} from "@c8y/ngx-components/upgrade";
import {AssetsNavigatorModule} from "@c8y/ngx-components/assets-navigator";
import {CockpitDashboardModule, ReportDashboardModule} from "@c8y/ngx-components/context-dashboard";
import {ReportsModule} from "@c8y/ngx-components/reports";
import {SensorPhoneModule} from "@c8y/ngx-components/sensor-phone";
import {BinaryFileDownloadModule} from "@c8y/ngx-components/binary-file-download";

// --- 8< changed part ----
import { HelloComponent } from './hello.component';    // 1
// --- >8 ----

@NgModule({

  // --- 8< changed part ----
  declarations: [HelloComponent],                      // 2
  // --- >8 ----

  imports: [
    // Upgrade module must be the first
    UpgradeModule,
    BrowserAnimationsModule,
    RouterModule.forRoot(),
    NgRouterModule.forRoot(
      // --- 8< changed part ----
      { path: 'hello', component: HelloComponent},     // 3
      // --- >8 ----

      ...UPGRADE_ROUTES
    ], { enableTracing: false, useHash: true }),
    CoreModule.forRoot(),
    AssetsNavigatorModule,
    ReportsModule,
    NgUpgradeModule,
    DashboardUpgradeModule,
    CockpitDashboardModule,
    SensorPhoneModule,
    ReportDashboardModule,
    BinaryFileDownloadModule,
  ]
})
export class AppModule extends HybridAppModule {
  constructor(protected upgrade: NgUpgradeModule) {
    super();
  }
}

The changes here are straightforward. First, you import the component (1.). Then you add it to the declarations (2.). Last you need to bind it to a path, for this example, hello (3.). When you now spin up the application with the c8ycli server command and navigate to the URL by adding the right hash to it (http://localhost:9000/apps/cockpit/#/hello) you should see the custom component. In the next step, you will hook the component in the navigator at the left.

3. Hooking a navigator node

To allow the user to navigate to your newly created hello.component.ts, add some navigation to the navigator on the left. To do so, you will use a so-called hook.

Hooks are providers that are bound to a certain injection token. To allow the addition of multiple providers, use the multi-provider concept of Angular. Explaining it in detail goes beyond the scope of this tutorial. Refer to the angular.io documentation.

The injection tokens can be received from the @c8y/ngx-components package by importing it. They all start with HOOK_ followed by what they are used for. For example, to add a navigator node, use the HOOK_NAVIGATOR_NODE:

import {NgModule} from "@angular/core";
import {BrowserAnimationsModule} from "@angular/platform-browser/animations";
import {RouterModule as NgRouterModule} from "@angular/router";
import {UpgradeModule as NgUpgradeModule} from "@angular/upgrade/static";
// --- 8< changed part ----
import {CoreModule, RouterModule, HOOK_NAVIGATOR_NODES, NavigatorNode} from "@c8y/ngx-components";
// --- >8 ----
import {DashboardUpgradeModule, UpgradeModule, HybridAppModule, UPGRADE_ROUTES} from "@c8y/ngx-components/upgrade";
import {AssetsNavigatorModule} from "@c8y/ngx-components/assets-navigator";
import {CockpitDashboardModule, ReportDashboardModule} from "@c8y/ngx-components/context-dashboard";
import {ReportsModule} from "@c8y/ngx-components/reports";
import {SensorPhoneModule} from "@c8y/ngx-components/sensor-phone";
import {BinaryFileDownloadModule} from "@c8y/ngx-components/binary-file-download";

@NgModule({
  declarations: [HelloComponent],

  imports: [
    // Upgrade module must be the first
    UpgradeModule,
    BrowserAnimationsModule,
    RouterModule.forRoot(),
    NgRouterModule.forRoot(
      [
        {path: "hello", component: HelloComponent}, // 3
        ...UPGRADE_ROUTES,
      ],
      {
        enableTracing: false,
        useHash: true,
      }
    ),
    CoreModule.forRoot(),
    AssetsNavigatorModule,
    ReportsModule,
    NgUpgradeModule,
    DashboardUpgradeModule,
    CockpitDashboardModule,
    SensorPhoneModule,
    ReportDashboardModule,
    BinaryFileDownloadModule,
  ],

  // --- 8< changed part ----
  providers: [
    {
      provide: HOOK_NAVIGATOR_NODES, // 1
      useValue: [{                   // 2
        label: 'Hello',              // 3
        path: 'hello',
        icon: 'rocket',
        priority: 1000
      }] as NavigatorNode[],         // 4
      multi: true                    // 5
    }
  ]
  // --- >8 ----

})
export class AppModule extends HybridAppModule {
  constructor(protected upgrade: NgUpgradeModule) {
    super();
  }
}

Explanation of the above comment numbers:

  1. You provide the HOOK_NAVIGATOR_NODES.
  2. You use a certain value. For complex cases you can also define a useClass and a get() function.
  3. You define what the navigator node should look like.
  4. Most hooks have interfaces which allow type-ahead information in typescript.
  5. The multi-provider flag tells Angular that there could be more than just one hook.

After you implement this extension hook you get a new entry in the navigator which looks like this:

The extended Cockpit application

Note that the property priority of the NavigatorNode interface defines in which order the nodes are shown.

The hello.component.ts is now like a blank canvas inside the Cockpit application. You can implement any kind of feature you need, while the given functionality of the Cockpit is not affected.

Conclusion

A hybrid application is limited because of its angularjs and Angular integration. However, the hook concept and a custom route allow for additions to existing hybrid applications. They are a powerful tool to extend the build-in applications. Sometimes additional features are needed and a pure Angular application is a better fit. This depends on the use case.

Remove login page and authentication

Version: 1009.0.18 | Packages: @c8y/cli, @c8y/apps and @c8y/ngx-components

Info
This technique exposes the username and password. Ensure that this user doesn’t have access to sensible data.

The default application always takes you to the login page for authentication before it allows you to access a page. This recipe will explain how to remove the login authentication and use the application directly.

Brief background

The removal of all authentication is not possible. In order to get around it you must pass default credentials that the application will read upon request. Your goal is to trigger the login with the default credentials before the application requests the login page because it is not authenticated.

The login functionality is part of the CoreModule in the @c8y/ngx-components package which is loaded when Angular bootstraps the application. The default credentials must be passed to the API before that happens. The result will be that, when Angular loads the initial page, the user will be already authenticated and the login page will be skipped.

1. Initialize a new application

As a starting point, you need an application. For this purpose, create a new application using the c8ycli:

c8ycli new my-cockpit cockpit -a @c8y/apps@1009.0.18

This will create a new application that is an exact copy of the Cockpit application. Next, you must install all dependencies. Switch to the new folder and run npm install.

Info

The c8ycli new command has a -a flag which defines which package to use for scaffolding. This way you can also define which version of the application you want to scaffold, for example:

  • c8ycli new my-cockpit cockpit -a @c8y/apps@1009.0.18 will scaffold an application with the version 1009.0.18
  • c8ycli new my-cockpit cockpit -a @c8y/apps@latest will scaffold an application with the latest official release. Same as if used without the -a flag
  • c8ycli new my-cockpit cockpit -a @c8y/apps@next will scaffold an application with the latest beta release.

2. Add logic for default authentication

First you must make sure to add the default authentication before Angular bootstraps your custom application. For that reason you must add a new provider in the app.module.ts in the newly created custom Cockpit application, which will be triggered before the login. For that, use Angular’s injection token APP_INITIALIZER. This token will ensure that the application will not be initialized until the new functionality is being executed.

providers: [
  {
    provide: APP_INITIALIZER,
    useFactory: initApp,
    multi: true,
    deps: [LoginService],
  },
];

Use a factory function initApp, where you will define and send your default authentication. To send your credentials to the API, add a dependency to the LoginService (http://resources.cumulocity.com/documentation/websdk/ngx-components/injectables/LoginService.html), which is a part of @c8y/ngx-components:

export function initApp(loginService: LoginService) {
  return () => {
    const credentials = {
      tenant: "tenantName",
      user: "admin",
      password: "C8Yadmin",
    };
    const basicAuth = loginService.useBasicAuth(credentials);

    return loginService.login(basicAuth, credentials);
  };
}

To login with your default credentials, you must call the login function from the service and pass the authentication method and the default credentials.

With that, the recipe is completed and authentication will be done behind the scenes:

// --- 8< changed part ----
import { APP_INITIALIZER, NgModule } from "@angular/core";
// --- >8 ----
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
import { RouterModule as NgRouterModule } from "@angular/router";
import { UpgradeModule as NgUpgradeModule } from "@angular/upgrade/static";
// --- 8< changed part ----
import { CoreModule, LoginService, RouterModule } from "@c8y/ngx-components";
// --- >8 ----
import { DashboardUpgradeModule, UpgradeModule, HybridAppModule, UPGRADE_ROUTES} from '@c8y/ngx-components/upgrade';
import { AssetsNavigatorModule } from '@c8y/ngx-components/assets-navigator';
import { CockpitDashboardModule, ReportDashboardModule } from '@c8y/ngx-components/context-dashboard';
import { ReportsModule } from '@c8y/ngx-components/reports';
import { SensorPhoneModule } from '@c8y/ngx-components/sensor-phone';
import { BinaryFileDownloadModule } from '@c8y/ngx-components/binary-file-download';

@NgModule({
  imports: [
    // Upgrade module must be the first
    UpgradeModule,
    BrowserAnimationsModule,
    RouterModule.forRoot(),
    NgRouterModule.forRoot([...UPGRADE_ROUTES], { enableTracing: false, useHash: true }),
    CoreModule.forRoot(),
    AssetsNavigatorModule,
    ReportsModule,
    NgUpgradeModule,
    DashboardUpgradeModule,
    CockpitDashboardModule,
    SensorPhoneModule,
    ReportDashboardModule,
    BinaryFileDownloadModule
  ],
  providers: [
    {
      provide: APP_INITIALIZER,
      useFactory: initApp,
      multi: true,
      deps: [LoginService],
    },
  ],
})
export class AppModule extends HybridAppModule {
  constructor(protected upgrade: NgUpgradeModule) {
    super();
  }
}

export function initApp(loginService: LoginService) {
  return () => {
    const credentials = {
      tenant: "tenantName",
      user: "admin",
      password: "C8Yadmin",
    };
    const basicAuth = loginService.useBasicAuth(credentials);

    return loginService.login(basicAuth, credentials);
  };
}

Conclusion

This tutorial shows how to remove authentication when developing a custom application. This kind of technique can be used if an application does not have confidential information. If you need data protection you should avoid this technique.

Request data from a custom microservice

Version: 1009.0.18 | Packages: @c8y/cli, @c8y/apps and @c8y/ngx-components

In some situations, the UI needs data from a custom microservice. While you can always read that data with any HTTP client, for example, Angular’s HttpModule, you might want authentication out of the box.

This recipe shows how to access custom endpoints with the @c8y/client and get authenticated automatically. First, it will take a deeper look at the basics to explain how the client works in Angular applications.

Basic: How the client works

Let’s first look at how the @c8y/client works and what its benefits are.

The client handles HTTP requests from the browser or, if desired, from node.js to the platform. As most platform APIs are secured it allows to set the authentication to use.

Currently, there are two options for the authentication method:

When you set the authentication method on a new client instance you can define which authentication to use. The client returns an object with all common endpoints of the platform. For example, the following example requests data from the inventory via BasicAuth:

const client = new Client(new BasicAuth({
  user: 'admin',
  password: 'password',
  tenant: 'acme'
}), 'https://acme.cumulocity.com');
try {
 const { data, paging, res } = await client.inventory.list();
 console.log('Login with admin:password successful');
 console.log(data);
} catch(ex) {
 console.log('Login failed: ', ex)
}

Each of the pre-configured endpoints returns an object containing the data, an optional paging object and the res object. The response is given by fetch, a next-generation XHR API which is implemented in all modern browsers (and can be polyfilled for IE11).

In conclusion, the @c8y/client is a helper library for JavaScript that abstracts fetch to allow easy authentication and direct access on the common platform APIs.

The next section shows how you can use that concept in an Angular application with the help of the Dependency Injection (DI) model of Angular.

Basic: Interaction between @c8y/client and an Angular application

@c8y/ngx-components is an Angular component that allows to spin up an application. It is, for example, used in our basic applications like Cockpit, Administration and Device Management to display the login screen. When you spin up a new Angular-based application the @c8y/client and the @c8y/ngx-components are always included. Moreover the ngx-components have a subpackage which is called @c8y/ngx-components/api and which exports a DataModule. That module already imports all common endpoint services, so that you can use the standard dependency injection of Angular to access data.

The example above in an Angular application would look like this:

import { InventoryService } from '@c8y/client';                       // 1

@Component({
  selector: '[app-hello]',
  template: `<h1>hello</h1>`
})
export class HelloComponent {
  constructor(public inventory: InventoryService) {}                  // 2

  async ngOnInit() {
    const { data, paging, res } = await client.inventory.list();      // 3
    console.log(data);
  }
}
  1. Import the desired service from the client.
  2. Use dependency injection to use the desired service. The DI concept of Angular will take care of all dependencies needed when the DataModule has been imported correctly in your main module.
  3. You can now request data. Authentication is already handled. When used directly in a constructor or as an EntryComponent, the request might fail unauthorized as the component is loaded previous to the login module. To avoid this, inject the AppStateService. It provides a currentUser observable that updates as soon as a user is logged in.

This covers the overview on how to use the common endpoints. The following recipe shows how to add a custom endpoint.

1. Initialize the example application

As a starting point, you need an application showing dashboards. For this purpose, create a new Cockpit application using the c8ycli:

c8ycli new my-cockpit cockpit -a @c8y/apps@1009.0.18

Next, you must install all dependencies. Switch to the new folder and run npm install.

Info

The c8ycli new command has a -a flag which defines which package to use for scaffolding. This way you can also define which version of the application you want to scaffold, for example:

  • c8ycli new my-cockpit cockpit -a @c8y/apps@1009.0.18 will scaffold an application with the version 1009.0.18
  • c8ycli new my-cockpit cockpit -a @c8y/apps@latest will scaffold an application with the latest official release. Same as if used without the -a flag
  • c8ycli new my-cockpit cockpit -a @c8y/apps@next will scaffold an application with the latest beta release.

2. Request data directly with fetch

If you want to access data from the endpoint service/acme via HTTP GET, the easiest way to achieve this with authentication is to reuse the fetch implementation of the client. Add a file to the application and call it acme.component.ts:

import { Component, OnInit } from '@angular/core';
import { FetchClient } from '@c8y/client';

@Component({
  selector: 'app-acme',
  template: '<h1>Hello world</h1>{{data | json}}'
})
export class AcmeComponent implements OnInit {
  data: any;

  constructor(private fetchClient: FetchClient) {}                    // 1

  async ngOnInit() {
    const response = await this.fetchClient.fetch('service/acme');    // 2
    this.data = await response.json();                                // 3
  }
}
  1. Inject the FetchClient which is the fetch abstraction used by the client.
  2. Request the data via fetchClient.fetch. The function is identical to the Fetch API, as a second parameter it accepts, for example, the method or data, except that it adds the authentication to the platform.
  3. Parse the data and set it onto your controller to display it in the template.

Next, add a route to your application where you can show the component. The following code does this in the app.module.ts, also refer to our other tutorials for more details:

import { NgModule } from '@angular/core';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { RouterModule as NgRouterModule } from '@angular/router';
import { UpgradeModule as NgUpgradeModule } from '@angular/upgrade/static';
import { CoreModule, RouterModule } from '@c8y/ngx-components';
import { DashboardUpgradeModule, UpgradeModule, HybridAppModule, UPGRADE_ROUTES} from '@c8y/ngx-components/upgrade';
import { AssetsNavigatorModule } from '@c8y/ngx-components/assets-navigator';
import { CockpitDashboardModule, ReportDashboardModule } from '@c8y/ngx-components/context-dashboard';
import { ReportsModule } from '@c8y/ngx-components/reports';
import { SensorPhoneModule } from '@c8y/ngx-components/sensor-phone';
import { BinaryFileDownloadModule } from '@c8y/ngx-components/binary-file-download';

// ---- 8< added part ----
import { AcmeComponent } from './acme.component';
// ---- >8 ----

@NgModule({
  imports: [
    // Upgrade module must be the first
    UpgradeModule,
    BrowserAnimationsModule,
    RouterModule.forRoot(),
    // ---- 8< added part ----
    NgRouterModule.forRoot([
      { path: 'acme', component: AcmeComponent },
      ...UPGRADE_ROUTES,
    ], { enableTracing: false, useHash: true }),
    // ---- >8 ----
    CoreModule.forRoot(),
    AssetsNavigatorModule,
    ReportsModule,
    NgUpgradeModule,
    DashboardUpgradeModule,
    CockpitDashboardModule,
    SensorPhoneModule,
    ReportDashboardModule,
    BinaryFileDownloadModule
  ],

  // ---- 8< added part ----
  declarations: [
    AcmeComponent
  ]
  // ---- >8 ----

})
export class AppModule extends HybridAppModule {
  constructor(protected upgrade: NgUpgradeModule) {
    super();
  }
}

3. Run and verify the application

When you run the application with c8ycli server and point your browser to the path defined in the module http://localhost:9000/apps/cockpit/#/acme, you should see the following:

Custom client service

The request fails as we don’t have a microservice with this context path running. However, as you can see in the developer tools the request has an authorization cookie attached. If the microservice existed, the request would pass and the data would be displayed.

4. Bonus: Write a Service.ts abstraction

In the example above, you have used the underlying fetch abstraction to directly access a custom microservice. You might want to achieve the same simplicity for the common service of the client. It handles the URL and the JSON parsing for you internally. To do so, extend the Service class returned by the @c8y/client and override the necessary methods or properties.

Do this for the acme microservice example by creating a new file called acme.service.ts:

import { Injectable } from '@angular/core';
import { Service, FetchClient } from '@c8y/client';

@Injectable({
  providedIn: 'root'
})
export class AcmeService extends Service<any> {  // 1
  baseUrl = 'service';                           // 2
  listUrl = 'acme';

  constructor(client: FetchClient) {             // 3
    super(client);
  }

  detail(entityOrId) {                           // 4
    return super.detail(entityOrId);
  }

  list(filter?) {                                // 4
    return super.list(filter);
  }
}

Explanation of the numbers above:

  1. By extending the service you get the same capabilities as of all common services in @c8y/client. The generic type, in this case, is set to any, to keep the example as easy as possible. It is a common pattern to create an interface that reflects the data you are sending via this service and replace any by this interface.
  2. The URLs are the main entry points for this service. The pattern is always <<url>>/<<baseUrl>>/<<listUrl>>/<id>. If your microservice follows a different structure, you can override the getUrl method of the service class.
  3. The constructor needs the current FetchClient imported via dependency injection. It also needs to get it passed to the extended Service class via super(). If you want your endpoint to support real time, you also must inject the RealTime abstraction here and pass it.
  4. You can now override the detail() or list() implementation. You can call the super method only, modify the result of the super call or write your own implementation. The choice depends on the implementation details of your microservice.

Now you can reuse the AcmeService in the acme.component.ts:

import { Component, OnInit } from '@angular/core';
import { AcmeService } from './acme.service';
import { AlertService } from '@c8y/ngx-components';

@Component({
  selector: 'app-acme',
  template: '<h1>Hello world</h1>{{data | json}}'
})
export class AcmeComponent implements OnInit {
  data: any;

  constructor(private acmeService: AcmeService, private alert: AlertService) {} // 1

  async ngOnInit() {
    try {
      const { data } = await this.acmeService.list();                           // 2
      this.data = data;
    } catch (ex) {
      this.alert.addServerFailure(ex);                                          // 3
    }
  }
}

Inject the services (1.) and directly do a list request on the service (2.). The service will throw an error which is why you wrap the call in a try/catch block and on error show an alert by adding the exception to the addServerFailure method (3.).

Conclusion

The examples above show how to access custom microservices via the client. While it might be simpler to use a well-known client abstraction like Angular’s HttpModule, reusing the @c8y/client gives you authentication out of the box. This solution is more robust against changes as you can update the @c8y/client without worrying about underlying changes.