How-to recipes

This section lists common how-to recipes for Web SDK for Angular. They require:

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

Extend an existing application and use hooks

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

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

This recipe explains step by step, how you can extend the Cockpit app with a custom route and hook this route into the navigator. Before starting with the step-by-step description 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 application 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 to find a solution to serve both frameworks. The @c8y/cli allows scaffolding a default app which allows exactly this. It uses the Angular upgrade functionality, to serve an Angular and angularjs app at the same time. This enables us to develop new features in Angular while every angularjs plugin should be easily integrable. 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. That pure version of the app comes in two flavors:

So in total, there are three possibilities to start with the Web SDK: Extending an existing hybrid app, building a pure Angular app with Angular CLI or building it with @c8y/cli. Which one to choose heavily depends on the application you want to build. E.g. if you want an application that just follows the look&feel of the platform but want to use special dependencies for certain scenarios (e.g. Material-Framework), you are best with the pure Angular CLI solution.

Most likely you just want to extend a hybrid app, which we will cover in this recipe. But first, we must show the limitations of that approach to understanding better, why 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 app, there are some limitations. The following list tries to explain them:

Now that you know the limitations we can start to extend the first application and develop our first extension hook. To do so, we need to scaffold a hybrid app. Here the @c8y/apps package comes into play. It is a package containing the default apps and their minimum setup. The c8ycli uses that packages every time you initialize an app with the new command. The next section will explain that process and will then extend a hybrid app step by step.

1. Initialize the example app

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@1004.11.0

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

Tip: 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 app you want to scaffold, e.g.:

  • c8ycli new my-cockpit cockpit -a @c8y/apps@1004.11.0 will scaffold an app with the version 10.4.11.0
  • c8ycli new my-cockpit cockpit -a @c8y/apps@latest will scaffold an app 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 app 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 (remember the hybrid limitations). Therefore we can create a simple 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 very basic component. Only the template uses a special feature called “content projection” to show a title. Content projection is an Angular concept to display content in other places then they are defined. Which components support content projection is described in the @c8y/ngx-components documentation.

We can now bind this custom component to a route by changing the app.module.ts 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 { UpgradeModule, HybridAppModule, UPGRADE_ROUTES } from '@c8y/ngx-components/upgrade';
import { AssetsNavigatorModule } from '@c8y/ngx-components/assets-navigator';

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

@NgModule({

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

  imports: [
    BrowserAnimationsModule,
    RouterModule.forRoot(),
    NgRouterModule.forRoot([
      // --- 8< changed part ----
      { path: 'hello', component: HelloComponent},     // 3
      // --- >8 ----

      ...UPGRADE_ROUTES
    ], { enableTracing: false, useHash: true }),
    CoreModule.forRoot(),
    AssetsNavigatorModule,
    NgUpgradeModule,
    // Upgrade module must be the last
    UpgradeModule
  ]
})
export class AppModule extends HybridAppModule {
  constructor(protected upgrade: NgUpgradeModule) {
    super();
  }
}

The changes here are straight forward: First, we import the component (1.). Then we add it to the declarations (2.). Last we need to bind it to a path, in this case, 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 the URL (http://localhost:9000/apps/cockpit/#/hello) you should already see that custom component. In the next step, we will hook that component in the navigator on the left.

3. Hooking a navigator node

To allow the user to navigate to our created hello.component.ts we need to add some navigation to the left-side navigator. To do so, we will use a so-called hook.

The hooks are just providers that are bound to a certain injection token. To allow adding multiple providers we use the multi-provider concept of Angular. Explaining it in detail goes beyond the scope of this tutorial but there is a good official documentation describing it.

The injection tokens can be received from the @c8y/ngx-components package by simply importing it. They all start with HOOK_ following what they are used for. To add a navigator node we will therefore 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 { UpgradeModule, HybridAppModule, UPGRADE_ROUTES } from '@c8y/ngx-components/upgrade';
import { AssetsNavigatorModule } from '@c8y/ngx-components/assets-navigator';
import { HelloComponent } from './hello.component';

@NgModule({
  declarations: [HelloComponent],                      

  imports: [
    BrowserAnimationsModule,
    RouterModule.forRoot(),
    NgRouterModule.forRoot([
      { path: 'hello', component: HelloComponent},     
      ...UPGRADE_ROUTES
    ], { enableTracing: false, useHash: true }),
    CoreModule.forRoot(),
    AssetsNavigatorModule,
    NgUpgradeModule,
    // Upgrade module must be the last
    UpgradeModule
  ],

  // --- 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. We provide the HOOK_NAVIGATOR_NODES.
  2. We use a certain value. For complex cases we can also define a useClass and a get() function.
  3. We define how 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 we implement this extension hook we get a new entry in the navigator which looks like this (note that the property priority of the NavigatorNode interface defines in which order the nodes are shown):

The extended Cockpit application

As you can see the hello.component.ts is now like a blank canvas inside the Cockpit app. In that, you can implement any kind of feature you need, while the given functionality of the Cockpit isn’t touched.

Conclusion

As seen in this recipe, a hybrid app is limited due to its angularjs and Angular integration. However, the hook concept and a custom route allow adding nearly anything to the existing hybrid apps. They give you a powerful tool to extend the build-in apps. But sometimes more features are needed and a pure Angular app might fit better. It depends on the use case to decide if a simple extension is enough or a new application needs to be implemented.

Update to a newer Web SDK version

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

A UI build with an earlier version of the Web SDK is locked to the current version. A platform update doesn’t update the UI version, however a UI running against a newer backend always keeps working as all APIs are backwards compatible. That’s why an update makes mostly sense if a newer feature of the UI or the Web SDK needs to be used. Therefore, a new application needs to be built and deployed to the platform. This recipe describes best practices to do so.

Preparation

We recommend you to use a Source Control System to backup the data and to get better diffing of the code. If you are not using an SCM yet, see below for an introduction on how to use git to store your changes. If you are already using an SCM or you don’t want to use any, you can jump to the next section. If you decide not to use an SCM, backup your application before running the update.

Ensure that you have git installed on your system. Then open a terminal and run the following commands:

cd <<path/to-you-app>>
git init
git add .
git commit -m "init commit"

Now your code is committed to a local git repository stored in the .git folder. Next this recipe explains you how to update the Web SDK. If you don’t want to use git anymore after the update, you can simply erase the .git folder.

Updating

To update the Web SDK you can simply use the new command that is used for scaffolding:

c8ycli new <<app-name>> <<cockpit|devicemanagement|administration>> -a @c8y/apps@<<version>>

So for example if your current working directory is an application called “my-cockpit” based on Cumulocity’s Cockpit application and you want to update to version 10.6.2.0, you need to run the following command:

cd ..
c8ycli new my-cockpit cockpit -a @c8y/apps@1006.2.0

Info: The first two numbers of the version are combined (eg. 10.6 becomes 1006) as npm only supports semver version numbers. You can also remove the -a flag to always update to the latest version (the version our cloud platform is running on).

The command simply copies over the files that are used for building a new application in the particular version. The following files are currently overwritten:

These are the files that are overwritten by an update based on the version of that article. The list might change in later versions. Next, we need to reapply the changes that were made earlier to these files. A git diffing tool can be very helpful for that.

Diffing to reapply changes

A git diffing tool is useful to identify which changes have been made with the upgrade and which have been made earlier and now need to be reapplied. In the following screenshot we are using Visual Studio Code to identify the changes, as it has a well integrated diffing tool for git (mostly all other IDEs have support for git diffing as well):

Comparing the difference with vscode

With that tool it is easy to compare which file was changed with the upgrade and where custom changes may need to be reapplied. In this case MyCustomModule must only be placed in the upgrade app.module.ts. When this change is done, the update can be verified.

Verifying the update

To check if the version update worked, it is usually a good practice to run it locally first. Therefore, you need first to install the dependencies again. Remove the current node_modules directory and run npm install (or yarn) again to refresh the dependencies. After this, start the application with npm start. After login you can check the current UI version by clicking on you username.

If everything worked as expected, you can now deploy your application by running the commandnpm run deploy.

Conclusion

The update process is sometimes a bit tricky, especially if you have many changes in the app.module.ts. However, with git and Visual Studio Code the visual diffing may help you to accomplish this task. Also, it is a good practice to put your own Angular customizations into a module and only to make changes to the app.module.ts when it is absolutely necessary.

Add a custom widget to a dashboard

Version: 10.4.11.0 | Packages: @c8y/cli 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 app

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@1004.11.0

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

Tip: 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 app you want to scaffold, e.g.:

  • c8ycli new my-cockpit cockpit -a @c8y/apps@1004.11.0 will scaffold an app with the version 10.4.11.0
  • c8ycli new my-cockpit cockpit -a @c8y/apps@latest will scaffold an app 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 app with the latest beta release.

2. Create the widget components

Widgets usually consist of two parts:

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

There is nothing special to mention about this component. It will just show a configured text which is vertically mirrored via CSS. You can basically do everything in it what you can do in any other Angular component.

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

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 = {};
}

Here again, you just need to add a config object which you can fill with any serializable configuration that you want to pass to the widget.

3. Add the widget to your application

To add the widget you have to 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 { BrowserModule } from '@angular/platform-browser';
import { RouterModule } from '@angular/router';
import { UpgradeModule as NgUpgradeModule } from '@angular/upgrade/static';

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

import { UpgradeModule, HybridAppModule, UPGRADE_ROUTES } from '@c8y/ngx-components/upgrade';
import { AssetsNavigatorModule } from '@c8y/ngx-components/assets-navigator';

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

@NgModule({
  imports: [
    BrowserModule,
    RouterModule.forRoot([
      ...UPGRADE_ROUTES
    ], { enableTracing: false, useHash: true }),
    CoreModule.forRoot(),
    AssetsNavigatorModule,
    NgUpgradeModule,
    UpgradeModule
  ],

  // --- 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 above numbers:

  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 part tells 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 tab to a details views with context routes

Version: 10.4.11.0 | Packages: @c8y/cli and @c8y/ngx-components

It is a common use case that you want to show additional information to a user in a details view (e.g. for a device or a group).

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

Device info with custom tab

In Web SDK for Angular, this kind of views are called ViewContext as they provide a view for a certain context. There are a couple of context views e.g. Device, Group, User, Application and Tenant. The user 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 which 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 to this view that is accessible through the route apps/cockpit/#/device/:id/hello.

1. Initialize the example app

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@1004.11.0

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

Tip: 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 app you want to scaffold, e.g.:

  • c8ycli new my-cockpit cockpit -a @c8y/apps@1004.11.0 will scaffold an app with the version 10.4.11.0
  • c8ycli new my-cockpit cockpit -a @c8y/apps@latest will scaffold an app 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 app 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 { BrowserModule } from '@angular/platform-browser';
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 { UpgradeModule, HybridAppModule, UPGRADE_ROUTES } from '@c8y/ngx-components/upgrade';
import { AssetsNavigatorModule } from '@c8y/ngx-components/assets-navigator';

@NgModule({
  imports: [
    BrowserModule,
    RouterModule.forRoot(),
    NgRouterModule.forRoot([
      ...UPGRADE_ROUTES,
    ], { enableTracing: false, useHash: true }),
    CoreModule.forRoot(),
    AssetsNavigatorModule,
    NgUpgradeModule,
    UpgradeModule
    ]

  // ---- 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 above numbers:

  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, e.g. if you want to resolve the routes async.
  3. Defines the context of the route. You should use the ViewContext enum to define it. In this case we want to extend the context of a device.
  4. The path where it is going to be shown. Is added to the context path. In this case 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 how the tab should look like. The priority defines on which position it should be shown.

Info: The HOOK_ONCE_ROUTE inherits the Angular Route type, so all properties of it can be reused here.

After this alignments the route is registered but the application will fail to compile, as the HelloComponent does not exist yet. We 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. Hence it needs the information in which context it has been opened. The context route resolves the device upfront, so there is no need to handle this. You can directly access it via the parent route.

So let’s 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) {}
}

There is nothing special to mention about this component other than that it injects the ActivatedRoute and accesses the parent data of it. 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 this to the entryComponents in app.module.ts will allow to compile the application:


import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
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 { UpgradeModule, HybridAppModule, UPGRADE_ROUTES } from '@c8y/ngx-components/upgrade';
import { AssetsNavigatorModule } from '@c8y/ngx-components/assets-navigator';
// ---- 8< added part ----
import { HelloComponent } from './hello.component';
// ---- >8 ----

@NgModule({
  imports: [
    BrowserModule,
    RouterModule.forRoot(),
    NgRouterModule.forRoot([
      ...UPGRADE_ROUTES,
    ], { enableTracing: false, useHash: true }),
    CoreModule.forRoot(),
    AssetsNavigatorModule,
    NgUpgradeModule,
    UpgradeModule
  ],

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

Congratulations, you 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 certain criteria is met.

(Bonus) 4. Show the tab only on certain criteria

In some cases, additional information is available only if a certain criteria is met. For example, it only makes sense to show a location if the device has a location fragment associated. To add such a criteria, 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 { BrowserModule } from '@angular/platform-browser';
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 { UpgradeModule, HybridAppModule, UPGRADE_ROUTES } from '@c8y/ngx-components/upgrade';
import { AssetsNavigatorModule } from '@c8y/ngx-components/assets-navigator';
import { HelloComponent } from './hello.component';

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

@NgModule({
  imports: [
    BrowserModule,
    RouterModule.forRoot(),
    NgRouterModule.forRoot([
      ...UPGRADE_ROUTES,
    ], { enableTracing: false, useHash: true }),
    CoreModule.forRoot(),
    AssetsNavigatorModule,
    NgUpgradeModule,
    UpgradeModule
  ],
  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 certain criteria. 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 this 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 tricky part which is not aligned with the Angular router as in a context route the 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 we need to 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 just 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 context not found can be handled by the parent.

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

Request data from a custom microservice

Version: 10.4.11.0 | Packages: @c8y/client 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 (e.g. the HttpModule from Angular) you might want to have authentication out of the box.

This recipe shows how to access custom endpoints with the @c8y/client and get authenticated automatically. But 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 clarify how the @c8y/client (short just client) works and what its benefits are.

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

Currently, there are two possible authentication methods:

When you set the authentication method on a new client instance you can define which authentication to use. The client then 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. Last is the response 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.

In the next section, we will show how you can easily use that concept in an Angular app 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 used in our basic apps like Cockpit, Administration and Device Management to display for example 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 sub-package which is called @c8y/ngx-components/api which exports a DataModule. That module already imports all common endpoint services, so that you can just use the standard dependency injection of Angular to access data.

The example above in a 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 change this, you can inject the AppStateService. It provides a currentUser observable that updates as soon as a user is logged in.

The basic gives an overview on how to use the common endpoints. The following recipe shows how to add a custom endpoint.

1. Initialize the example app

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@1004.11.0

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

Tip: 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 app you want to scaffold, e.g.:

  • c8ycli new my-cockpit cockpit -a @c8y/apps@1004.11.0 will scaffold an app with the version 10.4.11.0
  • c8ycli new my-cockpit cockpit -a @c8y/apps@latest will scaffold an app 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 app with the latest beta release.

2. Request data directly with fetch

Let’s say you want to access data from the endpoint service/acme via HTTP GET. The easiest way to archive this with authentification is to reuse the fetch implementation of the client. So we first 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. We inject the FetchClient which is the fetch abstraction used by the client.
  2. We 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. We parse the data and set it onto our controller to display it in the template.

Next, we need to add a route to our application where we can show this component. The following code does this in the app.module.ts, without going into many details, as this is already explained in other recipes:

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

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

@NgModule({
  imports: [
    BrowserAnimationsModule,
    RouterModule.forRoot(),

    // ---- 8< added part ----
    NgRouterModule.forRoot([
      { path: 'acme', component: AcmeComponent },
      ...UPGRADE_ROUTES,
    ], { enableTracing: false, useHash: true }),
    // ---- >8 ----

    CoreModule.forRoot(),
    AssetsNavigatorModule,
    NgUpgradeModule,
    UpgradeModule
  ],

  // ---- 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. But as you can see (in the dev tools) the request has an authorization cookie attached. So if the microservice would exist, the request would pass and the data would be displayed.

4. Bonus: Write a Service.ts abstraction

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

Let’s 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 above comment numbers:

  1. By extending the service we 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 always is <<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 need to 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. Which one to use here 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
    }
  }
}

We now simply inject the services (1.) and directly do a list request on the service (2.). As we know the service will throw an error we wrap the call in a try/catch and on error we show an alert by simply adding the exception to the addServerFailure method (3.).

Conclusion

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