Add a custom widget to a dashboard

Version: 1017.0.23 | 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 add 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@1017.0.23

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@1017.0.23 will scaffold an application with the version 10.17.0.23

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 follows:

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 name="text" [(ngModel)]="config.text" style="width:100%"></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 name="text" [(ngModel)]="config.text" style="width:100%"></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 entryComponents.

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';

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

import { DashboardUpgradeModule, UpgradeModule, HybridAppModule, UPGRADE_ROUTES } from '@c8y/ngx-components/upgrade';
import { SubAssetsModule } from '@c8y/ngx-components/sub-assets';
import { ChildDevicesModule } from '@c8y/ngx-components/child-devices';
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 { SearchModule } from '@c8y/ngx-components/search';
import { AssetsNavigatorModule } from '@c8y/ngx-components/assets-navigator';
import { CockpitConfigModule } from '@c8y/ngx-components/cockpit-config';
import { DatapointLibraryModule } from '@c8y/ngx-components/datapoint-library';
import { WidgetsModule } from '@c8y/ngx-components/widgets';
import { PluginSetupStepperModule } from '@c8y/ngx-components/ecosystem/plugin-setup-stepper';

// --- 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(),
    ReportsModule,
    NgUpgradeModule,
    AssetsNavigatorModule,
    DashboardUpgradeModule,
    CockpitDashboardModule,
    SensorPhoneModule,
    ReportDashboardModule,
    BinaryFileDownloadModule,
    SearchModule,
    SubAssetsModule,
    ChildDevicesModule,
    CockpitConfigModule,
    DatapointLibraryModule.forRoot(),
    WidgetsModule,
    PluginSetupStepperModule
  ],

  // --- 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: 1016.274.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@1016.274.0

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-app application -a @c8y/apps@1016.274.0 will scaffold an application with the version 10.16.274.0.

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< added part ----
import { TestComponent } from "./test.component";
// ---- >8 ----

@NgModule({
  imports: [
    BrowserAnimationsModule,
    RouterModule.forRoot(),
    ngRouterModule.forRoot([], { enableTracing: false, useHash: true }),
    CoreModule.forRoot()
  ],
  bootstrap: [BootstrapComponent],

  // --- 8< added 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: 1017.0.23 | 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@1017.0.23

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@1017.0.23 will scaffold an application with the version 10.17.0.23

2. Add a new HOOK_ROUTE

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_ROUTE, ViewContext } from '@c8y/ngx-components';
// ---- >8 ----
import { DashboardUpgradeModule, UpgradeModule, HybridAppModule, UPGRADE_ROUTES } from '@c8y/ngx-components/upgrade';
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 { AssetsNavigatorModule } from '@c8y/ngx-components/assets-navigator';


@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_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. 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_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_ROUTE, ViewContext } from '@c8y/ngx-components';
import { DashboardUpgradeModule, UpgradeModule, HybridAppModule, UPGRADE_ROUTES } from '@c8y/ngx-components/upgrade';
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 { AssetsNavigatorModule } from '@c8y/ngx-components/assets-navigator';
// ---- 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_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_ROUTE, ViewContext } from '@c8y/ngx-components';
import { DashboardUpgradeModule, UpgradeModule, HybridAppModule, UPGRADE_ROUTES } from '@c8y/ngx-components/upgrade';
import { SubAssetsModule } from '@c8y/ngx-components/sub-assets';
import { ChildDevicesModule } from '@c8y/ngx-components/child-devices';
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 { SearchModule } from '@c8y/ngx-components/search';
import { AssetsNavigatorModule } from '@c8y/ngx-components/assets-navigator';
import { CockpitConfigModule } from '@c8y/ngx-components/cockpit-config';
import { DatapointLibraryModule } from '@c8y/ngx-components/datapoint-library';
import { WidgetsModule } from '@c8y/ngx-components/widgets';
import { PluginSetupStepperModule } from '@c8y/ngx-components/ecosystem/plugin-setup-stepper';
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_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.

Extend an existing application and use hooks

Version: 1017.0.23 | 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@1017.0.23

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@1017.0.23 will scaffold an application with the version 10.17.0.23

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 { SubAssetsModule } from '@c8y/ngx-components/sub-assets';
import { ChildDevicesModule } from '@c8y/ngx-components/child-devices';
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 { SearchModule } from '@c8y/ngx-components/search';
import { AssetsNavigatorModule } from '@c8y/ngx-components/assets-navigator';
import { CockpitConfigModule } from '@c8y/ngx-components/cockpit-config';
import { DatapointLibraryModule } from '@c8y/ngx-components/datapoint-library';
import { WidgetsModule } from '@c8y/ngx-components/widgets';
import { PluginSetupStepperModule } from '@c8y/ngx-components/ecosystem/plugin-setup-stepper';

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

@NgModule({

  // --- 8< added part ----
  declarations: [HelloComponent],                      // 2
  // --- >8 ----
  imports: [
    // Upgrade module must be the first
    UpgradeModule,
    BrowserAnimationsModule,
    RouterModule.forRoot(),
    // --- 8< changed part ----
    NgRouterModule.forRoot(
      [{ path: "hello", component: HelloComponent }, ...UPGRADE_ROUTES],
      { enableTracing: false, useHash: true }
    ),
    // --- >8 ----
    CoreModule.forRoot(),
    ReportsModule,
    NgUpgradeModule,
    AssetsNavigatorModule,
    DashboardUpgradeModule,
    CockpitDashboardModule,
    SensorPhoneModule,
    ReportDashboardModule,
    BinaryFileDownloadModule,
    SearchModule,
    SubAssetsModule,
    ChildDevicesModule,
    CockpitConfigModule,
    DatapointLibraryModule.forRoot(),
    WidgetsModule,
    PluginSetupStepperModule
  ],
})
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 in app.module.ts in the following way:

{
      provide: HOOK_NAVIGATOR_NODES,
      useValue: [{
        label: 'Hello',  
        path: 'hello',
        icon: 'rocket',
        priority: 1000
      }] as NavigatorNode[],         // 1
      multi: true
}

As you see in (1) you need to take care of the typing on your own. To avoid it, you can also use the hookX function, which allow the same but without taking care of the boilerplate code. The following example uses these functions, to add a navigator node, using hookRoute and hookNavigatorNode:

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, hookNavigator, hookRoute } from "@c8y/ngx-components";
// --- >8 ----
import { DashboardUpgradeModule, UpgradeModule, HybridAppModule, UPGRADE_ROUTES } from "@c8y/ngx-components/upgrade";
import { SubAssetsModule } from "@c8y/ngx-components/sub-assets";
import { ChildDevicesModule } from "@c8y/ngx-components/child-devices";
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 { SearchModule } from "@c8y/ngx-components/search";
import { AssetsNavigatorModule } from "@c8y/ngx-components/assets-navigator";
import { CockpitConfigModule } from "@c8y/ngx-components/cockpit-config";
import { DatapointLibraryModule } from "@c8y/ngx-components/datapoint-library";
import { WidgetsModule } from "@c8y/ngx-components/widgets";
import { PluginSetupStepperModule } from "@c8y/ngx-components/ecosystem/plugin-setup-stepper";
import { HelloComponent } from "./hello.component";

@NgModule({
  declarations: [HelloComponent],

  imports: [
    // Upgrade module must be the first
    UpgradeModule,
    BrowserAnimationsModule,
    RouterModule.forRoot(),
    NgRouterModule.forRoot(
      [{ path: "hello", component: HelloComponent }, ...UPGRADE_ROUTES],
      { enableTracing: false, useHash: true }
    ),
    CoreModule.forRoot(),
    ReportsModule,
    NgUpgradeModule,
    AssetsNavigatorModule,
    DashboardUpgradeModule,
    CockpitDashboardModule,
    SensorPhoneModule,
    ReportDashboardModule,
    BinaryFileDownloadModule,
    SearchModule,
    SubAssetsModule,
    ChildDevicesModule,
    CockpitConfigModule,
    DatapointLibraryModule.forRoot(),
    WidgetsModule,
    PluginSetupStepperModule,
  ],
  // --- 8< changed part ----
  providers: [
    hookRoute({                     // 1
      path: "hello",
      component: HelloComponent,
    }),
    hookNavigator({                 // 1, 2
      priority: 1000,
      path: "/hello",               // 3
      icon: "rocket",
      label: "Hello",               // 4
    }),
  ],
  // --- >8 ----
})
export class AppModule extends HybridAppModule {
  constructor(protected upgrade: NgUpgradeModule) {
    super();
  }
}

Explanation of the above comment numbers:

  1. You provide the hookRoute and hookNavigator.
  2. You use a certain value. For complex cases you can also define a useClass and a get() function.
  3. You provide a path to your application, should always start with /.
  4. You define what the navigator node should look like.

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: 1016.274.0 | 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@1016.274.0

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@1016.274.0 will scaffold an application with the version 10.16.274.0

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 { SubAssetsModule } from '@c8y/ngx-components/sub-assets';
import { ChildDevicesModule } from '@c8y/ngx-components/child-devices';
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 { SearchModule } from '@c8y/ngx-components/search';
import { AssetsNavigatorModule } from '@c8y/ngx-components/assets-navigator';
import { CockpitConfigModule } from '@c8y/ngx-components/cockpit-config';
import { DatapointLibraryModule } from '@c8y/ngx-components/datapoint-library';
import { WidgetsModule } from '@c8y/ngx-components/widgets';
import { PluginSetupStepperModule } from '@c8y/ngx-components/ecosystem/plugin-setup-stepper';

@NgModule({
  imports: [
    // Upgrade module must be the first
    UpgradeModule,
    BrowserAnimationsModule,
    RouterModule.forRoot(),
    NgRouterModule.forRoot([...UPGRADE_ROUTES], { enableTracing: false, useHash: true }),
    CoreModule.forRoot(),
    ReportsModule,
    NgUpgradeModule,
    AssetsNavigatorModule,
    DashboardUpgradeModule,
    CockpitDashboardModule,
    SensorPhoneModule,
    ReportDashboardModule,
    BinaryFileDownloadModule,
    SearchModule,
    SubAssetsModule,
    ChildDevicesModule,
    CockpitConfigModule,
    DatapointLibraryModule.forRoot(),
    WidgetsModule,
    PluginSetupStepperModule
  ],
  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.

Implementing internationalization

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

Introduction

Cumulocity IoT provides an integrated tool which allows you to translate your content. This tool is based on the ngx-translate library. The CoreModule exports a preconfigured instance of this tool, with Cumulocity IoT already being integrated. Refer to the ngx-translate Github page for more information.

For this tutorial, create a new application with minimal configuration.

Setting up a new application

Start with creating a new application based on version 1016.0.321 or higher, and based on the basic application project.

Execute the following commands:

c8ycli new my-app-i18n
cd my-app-i18n
npm install

After the application is set up, create a new basic module that you can use to add and translate your content.

Create the following files:

Import TranslationsModule to the application’s module:

Now you can run the application. Initially, the application displays a single Translations menu item, which renders a blank page with the text: Index.

Extending default translations

Cumulocity IoT comes with a wide range of content that is already translated into multiple languages. These translations can be extended by adding a custom *.po file for a language. This allows for both adding new translations and modifying the existing ones.

Example

You can override one of the existing strings, for example, “User settings”, to display “User settings (de)” instead of the default “Benutzereinstellungen”, with the following steps:

  1. Create a new file translations/locales/de.po:

    msgid ""
    msgstr ""
    "Project-Id-Version: c8yui.core\n"
    "Report-Msgid-Bugs-To: \n"
    "POT-Creation-Date: \n"
    "PO-Revision-Date: \n"
    "Last-Translator: \n"
    "Language: de\n"
    "Language-Team: \n"
    "Content-Type: text/plain; charset=UTF-8\n"
    "Content-Transfer-Encoding: 8bit\n"
    "Plural-Forms: nplurals=2; plural=(n != 1);\n"
    
    
    msgid "User settings"
    msgstr "User settings (de)"
    
  2. Open the index.ts file and import the newly created file as shown below:

    (...)
    import { AppModule } from './app.module';
    
    import './translations/locales/de.po';
    
    declare const __MODE__: string;
    (...)
    
  3. Restart the server and the application. Now you can select German and the User settings label is changed to User settings (de), as defined in the de.po file.

Info
You can find *.po files with default translations under node_modules/@c8y/ngx-components/locales. To override these files, copy them to your locales directory and add an import statement to index.ts like the one for de.po above.

Adding new languages

To define new languages which are not supported by default, follow the example below. It adds an Italian translation.

  1. Create a new translation file translations/locales/it.po:

    msgid ""
    msgstr ""
    "Project-Id-Version: c8yui.core\n"
    "Report-Msgid-Bugs-To: \n"
    "POT-Creation-Date: \n"
    "PO-Revision-Date: \n"
    "Last-Translator: \n"
    "Language: it\n"
    "Language-Team: \n"
    "Content-Type: text/plain; charset=UTF-8\n"
    "Content-Transfer-Encoding: 8bit\n"
    "Plural-Forms: nplurals=2; plural=(n != 1);\n"
    
    
    msgid "User settings"
    msgstr "User settings (it)"
    
  2. Open the package.json file, and modify the c8y.application object as shown below:

    {
      (...)
      "c8y": {
        "application": {
          "name": "my-app-i18n",
          "contextPath": "my-app-i18n",
          "key": "my-app-i18n-application-key",
          "dynamicOptionsUrl": "/apps/public/public-options/options.json",
          "languages": {
            "it": {
              "name": "Italian",
              "nativeName": "Italiano"
            }
          }
        },
        "cli": {}
      }
    }
    
  3. Import the new it.po file within index.ts:

    (...)
    import { AppModule } from './app.module';
    
    import './translations/locales/de.po';
    import './translations/locales/it.po'; // <--
    
    declare const __MODE__: string;
    (...)
    
  4. Restart the server and the application.

Now you can select Italian and the User settings label is changed to User settings (it), as defined in the it.po file.

Basic text translation

There are multiple ways of translating content. The most common is the translate pipe and directive, which is explained in the following section.

Translate pipe

The translate pipe is the most common way to translate content that is present in your HTML views. The following example works assuming that you have added a custom it.po file as described in the previous section.

In your translations/text-translation.component.html file, add:

<div>{{ 'User settings' | translate }}</div>

If your language is set to Italian, reloading the application renders the content as User settings (it).

The translate pipe allows you to include parameters in the translated strings.

The result is: "Sig. Smith ha 40 anni".

Info

If you put the text wrapped with {{ ... }} directly in the template, you must escape curly braces which are part of the text.

For example: <div>{{ 'Mr. Smith is \{\{ age \}\} years old' | translate:{ age: 40 } }}</div>

This avoids compilation issues. The string extraction tool does not support such cases currently and you must put such a string in *.po files yourself.

Translate directive

Another way of translating content is to use the attribute translate, as shown in the example for translations/text-translation.component.html:

<div class="card">
  <div class="card-header separator">
    <h4 class="card-title">Translate directive example</h4>
  </div>
  <div class="card-block">
    This phrase will be translated:
    <span class="m-r-4" translate>User settings</span>
  </div>
</div>

Similar to the example with the translate pipe, the content of the span is translated to User settings (it).

You can use parameters with the translate directive in the following way:

In the example above, you must use Angular’s ngNonBindable directive in addition to the translate directive, so that Angular ignores curly braces and lets the translation service handle them.

Furthermore, you can translate entire HTML code blocks, as shown in the example below:

Important
In general we recommend you to have ngNonBindable present while translating HTML blocks, because the Angular compiler might otherwise interfere with the translation service.

Translating content of variables

Your content can be located in TypeScript as string variables. It is possible to translate such variables, as in the example below:

Info
Wrap such strings with the gettext function. This enables automatic extraction of the strings to the locales/locales.pot file. This also indicates that such strings are meant to be translated. See Extracting strings for translation using the locale-extract tool for information about extracting strings for translation.

Manual translation in TypeScript code

It is also possible to translate strings manually in TypeScript code. To do so, inject the TranslateService into the component and use its instant method to translate the content:

Translating content using the instant method is a one-time operation, so the translation won’t be updated if the user changes the language and decides not to reload the application. If you want the translation to be updated in such a case, we recommend you to use the stream method instead.

this.textStream = this.translateService.stream(this.variableWithText);
{{ textStream | async }}

Alternatively, you can subscribe to the onLangChange event emitter and re-translate the text explicitly.

this.translateService.onLangChange.subscribe(() => {
  this.translatedVariableWithText = this.translateService.instant(this.variableWithText);
});
Important
All subscriptions must be unsubscribed in order to prevent memory leaks. This can be avoided by using Angular’s async pipe on observables instead.

Extracting strings for translation using the locale-extract tool

You can use the c8ycli locale-extract command to extract strings from:

After using the command, a new directory ./locales will be created if it doesn’t exist yet. It contains:

Translating dates

In order to display dates according to the current locale settings, use the Angular date pipe, as shown in the example below:

Alternatively, use the c8yDate pipe to return dates in medium format. This also works with values outside of the range supported by ECMAScript:

Request data from a custom microservice

Version: 1017.0.23 | 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 module 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@1017.0.23

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@1017.0.23 will scaffold an application with the version 10.17.0.23

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 { SubAssetsModule } from '@c8y/ngx-components/sub-assets';
import { ChildDevicesModule } from '@c8y/ngx-components/child-devices';
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 { SearchModule } from '@c8y/ngx-components/search';
import { AssetsNavigatorModule } from '@c8y/ngx-components/assets-navigator';
import { CockpitConfigModule } from '@c8y/ngx-components/cockpit-config';
import { DatapointLibraryModule } from '@c8y/ngx-components/datapoint-library';
import { WidgetsModule } from '@c8y/ngx-components/widgets';
import { PluginSetupStepperModule } from '@c8y/ngx-components/ecosystem/plugin-setup-stepper';

// ---- 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(),
    ReportsModule,
    NgUpgradeModule,
    AssetsNavigatorModule,
    DashboardUpgradeModule,
    CockpitDashboardModule,
    SensorPhoneModule,
    ReportDashboardModule,
    BinaryFileDownloadModule,
    SearchModule,
    SubAssetsModule,
    ChildDevicesModule,
    CockpitConfigModule,
    DatapointLibraryModule.forRoot(),
    WidgetsModule,
    PluginSetupStepperModule
  ],

  // ---- 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.