Web SDK for Angular

Overview

This Web SDK enables you

The Web SDK consists of the following packages deployed to npm in the scope @c8y and available under Apache 2.0 license:

These packages depend on each other from top to bottom. While the @c8y/client is a very low-level API interface with nearly no dependencies, the @c8y/apps provide feature rich applications by including @c8y/ngx-components and @c8y/client.

The goal of these splittings is to provide the right package for every use case, e.g. if you want to build a small application with React you could use the @c8y/client to do the API interaction. If you need a brandable feature rich application which is close to our Cockpit or Device Management application you could use @c8y/ngx-components together with @c8y/stlyes.

Following is a list which explains the use cases of each package.

You can find all our packages on npm. To quickly get you bootstrapped with these packages we have built an CLI tool called @c8y/cli. Next, we will showcase how to get started with the @c8y/cli command line tool.

Getting started

First install the Command Line Interface (CLI) which helps you with bootstrapping an application:

$ npm install -g @c8y/cli

Next, bootstrap a new blank application with the new command:

$ c8ycli new myApp

Info: When you want to see the possibilities and implementation details of the Web SDK you should try the tutorial application. You can install it by running c8ycli new <<your-app-name>> tutorial.

Once the CLI installation is completed change to the newly created folder and run npm install:

$ cd myapp
$ npm install

After all packages are installed you can start the application by running:

$ npm start

If you point your browser to http://localhost:9000/apps/myapp/ you will get a login screen which proxies to the tenant defined in the start script. If you cannot log in, it might be pointing to the wrong instance. To change the proxy to your tenant URL change the start script in the script section of the newly created package.json:

{
  "start": "c8ycli server -u http://your-tenant.my-provider.com"
}

After logging in you should see a barely empty starter application. If you want to start with a more complex example read the documentation about @c8y/apps. If you want to build and deploy your application read more about the necessary commands of the developer command line tool.

Info: If you want to extend an existing application like Cockpit you can spin up a hybrid application. This allows you to combine existing AngularJS plugins into the new Web SDK, see Migration.

Info: You need to provide your tenant name or the tenant ID on login (as the application cannot derive it from the URL on localhost). If you don’t know your tenant name or the tenant ID you can use the REST API to request it.

First route & component

After creating the empty bootstrapping application you might want to start with your first content. To do so, add a new component to your project and save it as hello.component.ts:

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

@Component({
  selector: 'app-hello',
  template: `
    <c8y-title>Hello World</c8y-title>
    <p>My first content.</p>
  `
})
export class HelloComponent {
  constructor() {}
}

To hook the new component into the application you need to declare the new component and add it to a route in the 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';
import { HelloComponent } from './hello.component'  // don't forget to import the new component

@NgModule({
  imports: [
    BrowserAnimationsModule,
    RouterModule.forRoot(),
    ngRouterModule.forRoot(
      [{ path: '', component: HelloComponent }], // hook the route here
      { enableTracing: false, useHash: true }
    ),
    CoreModule.forRoot()
  ],
  declarations: [HelloComponent], // add deceleration here
  bootstrap: [BootstrapComponent]
})
export class AppModule {}

If you start this application and log in you will see an application similar to the following screenshot. To extend this application you can use the Angular Router in combination with our @c8y/ngx-components.

An Angular based application

Applications options

Each UI application can be customized with a set of defined options. The options objects are defined in JSON and read and merged at runtime with the following order:

This process of collecting and merging the different options is executed by a thin bootstrap layer included in @c8y/cli.

Although all of the options can be defined on any of the 3 levels some might not make sense at runtime, as they just influence the build process, or require a complex object making it particularly tricky to write as a URL query parameter.

Static options

Defined in the package.json of the application inside the fragment c8y.application:

{
  "c8y": {
    "application": {
      "name": "cockpit",
      "contextPath": "cockpit",
      "key": "cockpit-application-key",
      "tabsHorizontal": true,
      "upgrade": true,
      "rightDrawer": true,
      "contentSecurityPolicy": "default-src 'self' 'unsafe-inline' http: https: ws: wss:; script-src 'self' *.mapquestapi.com 'unsafe-inline' 'unsafe-eval' data:; style-src * 'unsafe-inline' blob:; img-src * data:; font-src * data:; frame-src *;"
    }
  }
}

Can also be passed to c8ycli:

c8ycli build --app.contextPath=cockpit2 --app.dynamicOptionsUrl="/apps/public/public-options/options.json"

Dynamic fetched options

Using the static options dynamicOptionsUrl the application will try to load a json from the specified URL at boot time. In the platform’s built-in applications this option is set to /apps/public/public-options/options.json as that mechanism to provide instance level and enterprise tenant customization. As this property is defined statically at build time, it is possible for the application developer to decide if and where from their applications should load the dynamic fetched options at runtime.

URL options

These can just be appended to the URL of the application as query parameters.

https://<instance domain>/apps/cockpit?dynamicOptionsUrl=/apps/my-options/options.json&rightDrawer:false

Options

Here is a list of the built-in options. As in the end this is just a plain old javascript object this list can be easily extended with any property a developer might want to include in his applications or extensions.

export class ApplicationOptions {
  name: string; // To be saved to the server
  contextPath: string; // To be saved to the server
  key: string; // To be saved to the server
  upgrade?: boolean; // true if the application is hybrid using Angular and angularJS simultaneously
  brandingEntry?: string; // the entry path to the branding
  tsConfigPath?: string; // path to tsCconfig if typescript is used, defaults to ./tsconfig.json
  entryModule?: string;
  indexTemplate?: string; // path to the index.html to be used, otherwise the default will be used
  dynamicOptionsUrl?: string; // URL to load the dynamic fetched options
  faviconUrl?: string; // URL for the favicon
  brandingUrl?: string; // URL for a CSS that will replace the default branding
  brandingCssVars?: { // Object with properties that will be converted to CSS custom properties
    [key: string]: string
  };
  languages?: { // Object with properties to add and change the languages available in the applications
    [langCode: string]: {
      name: string;
      nativeName: string;
      url: string;
    }
  };
  localePath?: string; // The folder where the translations po files are loaded from
  extraCssUrls?: string[]; // URLs for extra CSS files to be loaded at runtime
  docs?: {
    noDefault: boolean, // Hide default links to documentation
    excludeDefault: string[], // The list of regex strings to be matched with the default docs url
    links: Array<{ // Additional documentation links to be displayed
        icon: string;
        label: string;
        url: string;
    }>
  };
  icon?: { // Application icon to be displayed in app switcher and header bar
    class?: string;
    url?: string;
  };
  noAppSwitcher?: boolean; // Hides the application from the application switched (saved to the server)
  globalTitle?: string; // HTML page title
  hidePowered?: boolean; // Hide powered by at the bottom of the navigator
  supportUrl?: boolean | string; // URL for support link
  supportUserString?: string;
  rightDrawer?: boolean; // Show or hide the right drawer
  hideNavigator?: boolean; // Show or hide the navigator
  tabsHorizontal?: boolean;  // Show tabs horizontally or vertically
  loginExtraLink?: { // Extra link to add to login screen
    url: string;
    label: string;
  };
  storageLimitationFeatureEnabled?: boolean;
  companyName?: string; // Company name used to prompt the user about support staff
  guideHrefTemplate?: string; // The full URL for documentation, by default it's ${docsBaseUrl}${partialUrl}
  docsBaseUrl?: string; // The base URL for documentation
  contentSecurityPolicy?: string; // CSP string added to the index.html
  imports?: string[]; // legacy plugin imports
  cookieBanner?: { // Cookie Banner configuration
    cookieBannerTitle?: string;
    cookieBannerText?: string;
    policyUrl?: string;
  };
}

Developer command line tool

To support you with bootstrapping, running and deploying applications we have build a Command Line Interface. The tool is the successor of the cumulocity-node-tools. To avoid conflicts, it listens to the new command c8ycli instead of c8y. You can install it via npm:

npm install -g @c8y/cli

Usage

c8ycli [options] [command]

Info: The commands must be executed from the root path of the project.

Options

    -u, --url <url>                 The URL of the remote instance
    --version                       Provides version number
    -h, --help                      Provides usage information

Commands

All the commands except of new take an array of glob patterns. These will be solved to folders or entry point manifests.

    new [name] [template]                   Creates a folder to start a new application or extend an existing one
    serve [options] [appPaths...]           Runs local development server
    build [options] [appPaths...]           Builds the specified applications
    deploy [options] [appPaths...]          Deploys applications from the specified paths
    locale-extract [options] [srcPaths...]  Extracts all strings for translation and outputs the .po files to defined folder

The new command

The c8ycli new [name] [template] helps to start an empty application or to extend one of our existing applications (Cockpit, Devicemanagement or Administration). To extend an existing application use as [name] and [template] the name of the existing application like this:

$ c8ycli new cockpit cockpit

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.

Application options

Application options can be defined with --app.<option>=<value>. These will be applied to all applications found with [appPaths...].

    --app.name="My Application"
    --app.key=myapp-key
    --app.contextPath=myapplication
    --app.brandingEntry="./branding/mybranding.less"

Webpack options

Webpack options can be defined with --env.<option>=<value>. These will be directly passed to the webpack configuration.

    --env.mode="production"
    --env.hmr

Angular CLI

When developing a pure Angular you can create an Angular CLI project and add Cumulocity CLI to it. This functionality is only available after 1004.2.0.

Install Angular CLI

Follow the instructions to install @c8y/cli globally.

npm install -g @angular/cli

Create a new project

ng new my-first-iot-project
cd my-first-iot-project

Add Cumulocity IoT CLI

cd my-first-iot-project
ng add @c8y/cli

Run application

ng serve

In your browser, open http://localhost:4200/ to see the new application run.

You can configure the application options inside the package.json file and customize branding with LESS or CSS custom variables

Branding

For styling the application global CSS created with LESS is used. These styles are based on Bootstrap 3, and the original LESS source is distributed via the npm package @c8y/style. By extending these styles it is possible to change any detail of the application but the vast majority of developer want to change: colors, logos and fonts and these can be very easily achieved by replacing a few variables.

To override the variables it is possible to use:

CSS custom properties

Exposed via CSS custom properties there is only a subset of the LESS variables available. Here is a list of the available variables.

:root {
--brand-primary: gold ;
--brand-complementary: darkgreen;
--brand-dark:  red;
--brand-light: purple;
--gray-text: #333;
--link-color: var(--brand-primary);
--link-hover-color: var(--brand-complementary);
--body-background-color:#f2f3f4;
--brand-logo-img: url('/apps/ui-assets-management/logo-nav.svg');
--brand-logo-img-height: 20%;
--navigator-platform-logo: url('/apps/ui-assets-management/logo-nav.svg');
--navigator-platform-logo-height: 36px; /* height of the logo set to 0 to hide the element */

--navigator-font-family: inherit;
--navigator-app-name-size: 16px; /* font size of the application name set to 0 to hide app's name */
--navigator-app-icon-size: 46px; /* size of the application icon. set to 0 to hide the application icon.*/
--navigator-bg-color: var(--brand-primary);
--navigator-header-bg: var(--navigator-bg-color);
--navigator-text-color: #ffffff;
--navigator-separator-color: rgba(0,0,0,.05);
--navigator-active-color: var(--navigator-text-color);
--navigator-active-bg: var(--brand-complementary);

--header-color: #ffffff;
--header-text-color: var(--brand-dark);
--header-hover-color:var(--brand-primary);
--header-border-color: rgba(57,72,82,.05);

--font-family-base: "Roboto", Helvetica, Arial, sans-serif;
--headings-font-family: var(--font-family-base);
}

Note that these can be customized at runtime using application options using the property brandingCssVars. The option is only available after version 9.22.0.

Using LESS

Prerequisites

If you do not use the @c8y/cli make sure that you install the base styles from npm with:

npm install @c8y/style
  1. Create a LESS file called for instance branding.less.
  2. Save it inside a new folder, which can have any name you like.
  3. Inside this folder, create a sub folder for images.
my-application
│   app.modules.ts
│   index.ts
│   packages.json
|   ...
└───branding
│   │   branding.less
│   └───img
│       │   favicon.ico
│       │   main-logo.svg
│       │   tenant-brand.svg
│

The first line of code within the branding.less has to be:

@import '~@c8y/style/extend.less';

Example customizations

At this point we are able to change the desired variables according to our needs.

Let us change for example the most important color of your branding, the main color, called brand-color.

This is done by setting the respective LESS variable to a new color.

@brand-color: red;

User interface elements like buttons, active navigation nodes or even active tabs as well as also hover-states of buttons are red now.

What about changing the main logo that is located at the top of the login dialog? Look at this:

@{logo-login} { background-image: url('./img/logo-main.svg')}
@brand-logo-height: 48%;

You can check the branding changes with the help of the @c8y/cli.

c8ycli server --app.brandingEntry="<path-to-your-branding.less>"

You can also take a look at our tutorial application which has an example branding applied:

c8ycli new <appName> tutorial

More branding details

There are three main areas of a branding that you can easily control.

Colors

The colors that may be edited are separated in multiple categories, like:

Brand colors
@brand-color:                 #53cd61;
@brand-primary:               @brand-color;
@brand-complementary:         #a8b3b5;
@brand-primary-light:         lighten(@brand-primary, 20%);
Status colors
@brand-success:               #5cb85c;
@brand-info:                  @brand-color;
@brand-warning:               #f0ad4e;
@brand-danger:                #d9534f;
@danger:                      #D90000;
@warning:                     #FDC000;
@dark-warning:                #FF8000;
@success:                     #5cb85c;
Gray shades
@gray-text:                   #444;
@gray-darker:                 #2c3637;
@gray-dark:                   #3b4748;
@gray-medium-dark:            #49595B;
@gray-medium:                 #6D7A7C;
@gray:                        #8A9596;
@gray-light:                  #cacece;
@gray-lighter:                #f8f8f8;
@gray-white:                  #fcfcfc;
@text-muted:                  @gray;
Component colors

Two components are always visible to the user, the header and the navigator. Therefore you should determine the look & feel of these components with care.

/* HEADER */
@headerColor:                 white;
@header-text-color:           @gray-medium-dark;
@header-text-color-hover:     @brand-primary;
@header-active-color:         darken(@gray-medium-dark, 15%);

/* NAVIGATOR */
@navColor:                    @gray-darker;
@navColorHeader:              transparent;
@navigator-title-color:       white;
@navigator-text-color:        @gray-lighter;
@navigator-separator-color:   fade(white, 5%);
@navigator-font-family:       @headings-font-family;
@navigator-font-size:         13px;
@navigator-active-color:      white;
@navigator-active-bg:         @brand-primary;

As you can see, some variables re-use others. Be careful that these variables are all defined to avoid build errors.

Logos

There is no branding without logos.

You can change the logo at the top of the login dialog, the tenant brand logo and of course the favicon.

To change the favicon, enter:

// to be loaded by webpack
.favicon-webpack-loader { background: url('./img/favicon.ico') }

To change the main logo, enter:

@{logo-login} { background-image: url('./img/main-logo.svg') }
@brand-logo-height: 48%;

To change the tenant brand logo inside the navigator, enter:

@{logo-navigator} { background-image: url('./img/tenant-brand.svg') }
@navigator-platform-logo-height: 100px;

Typography

The look and feel of an application is also driven by its typography. Of course you can change the font as well.

@font-family-sans-serif:      "Lato",Arial, Verdana, sans-serif;
@font-family-base:            @font-family-sans-serif; @headings-font-family:        "Roboto",Arial, Verdana, sans-serif;

Example Branding

Above we described the possible options for creating your custom branding in detail. If you do not want to start from scratch in every application use the following example branding as snippet. It defines the most important variables.

@import '~@c8y/style/extend.less';

// Replace and uncomment each variable as you need them
/* LOGOS */
.favicon-webpack-loader { background: url('./img/favicon.ico') } // to be loaded by webpack
@{logo-login} { background-image: url('./img/logo-main.svg') }
@brand-logo-height: 48%; // percentage - height / width * 100
@{logo-navigator} { background-image: url('./img/logo.svg') }
@navigator-platform-logo-height: 100px;

/* COLORS */
@brand-color:                 #53cd61; // main color
@brand-primary:               @brand-color;
@brand-complementary:         #a8b3b5;
@brand-primary-light:         lighten(@brand-primary, 20%);
// status colors
@brand-success:               #5cb85c;
@brand-info:                  @brand-color;
@brand-warning:               #f0ad4e;
@brand-danger:                #d9534f;
@danger:                      #D90000;
@warning:                     #FDC000;
@dark-warning:                #FF8000;
@success:                     #5cb85c;
// grays
@gray-text:                   #444;
@gray-darker:                 #2c3637;
@gray-dark:                   #3b4748;
@gray-medium-dark:            #49595B;
@gray-medium:                 #6D7A7C;
@gray:                        #8A9596;
@gray-light:                  #cacece;
@gray-lighter:                #f8f8f8;
@gray-white:                  #fcfcfc;
@text-muted:                  @gray;

@body-background-color:       #f8f8f8; // page background color - always use a light background

/* HEADER */
@headerColor:                 white;
@header-text-color:           @gray-medium-dark;
@header-text-color-hover:     @brand-primary;
@header-active-color:         darken(@gray-medium-dark, 15%);

/* NAVIGATOR */
@navColor:                    @gray-darker;
@navColorHeader:              transparent;
@navigator-title-color:       white;
@navigator-text-color:        @gray-lighter;
@navigator-separator-color:   fade(white, 5%);
@navigator-font-family:       @headings-font-family;
@navigator-font-size:         13px;
@navigator-active-color:      white;
@navigator-active-bg:         @brand-primary;
// when set adds a vertical gradient in the navigator background
// @grad-top:                    "";
// @grad-bottom:                 "";

/* TYPOGRAPHY */
// @font-family-sans-serif:      "Lato",Arial, Verdana, sans-serif;
// @font-family-base:            @font-family-sans-serif;
// @headings-font-family:        "Roboto",Arial, Verdana, sans-serif;

/* BUTTONS */
// @btn-border-radius-base:      2px;
// @btn-border-radius-large:     @btn-border-radius-base;
// @btn-border-radius-small:     @btn-border-radius-base;
// @btn-shadow:                  none;

/* COMPONENTS */
// @spinner-color:               lighten(@brand-primary, 30%);
// @link-color:                  #337ab7;
// @link-hover-color:            darken(@link-color, 15%);
// @input-focus-color:           #66afe9;

// @body-background-pattern:     "";
// @darker-header:               @gray-dark;
// @appswitcher-background:      none;
// @table-bg-hover:              fade(black, 1.5%);
// @header-app-name:             @header-text-color;
// @image-path:                  'img/';

ngx-components

ngx-components is a components collection and data access layer for Angular applications. It allows to access our platform from within an Angular application as well as to provide the core components. To achieve this the ngx-components consists of two basic imports:

The full documentation of all modules and components can be found here.

Prerequisites

If you do not use the @c8y/cli to bootstrap a new application you first need to install the package:

$ npm install @c8y/ngx-components

Next, you can add the ngx-components modules to your app module (e.g. 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';

@NgModule({
  imports: [
    BrowserAnimationsModule,
    RouterModule.forRoot(),
    ngRouterModule.forRoot([], { enableTracing: false, useHash: true }), // 1
    CoreModule.forRoot() // 2
  ],
  bootstrap: [BootstrapComponent] // 3
})
export class AppModule {}

  1. Make sure to set useHash navigation to true as the platform does not support pushState
  2. Import the CoreModule to allow the use of the c8y- prefixed components.
  3. Bootstrap your application with the BootstrapComponent which will use the <c8y-bootstrap> component to initialize the root application. Alternatively, you can bootstrap a component of your choice and include that tag into its template or only reuse the given components.

Extension points

To extend and compose an application, ngx-components provide four core architecture concepts called Extensions points:

Content Projection (CP)

This concept allows to project content from one component to another. For example, you can configure the title of a page by setting a <c8y-title> in any other component. The content of the <c8y-title> tag is then projected to an outlet component, which is placed in the header bar. The benefit of this concept is that you can place anything into the projected content, for example you can project another custom component into the title.

A good example to use this concept is the c8y-action-bar-item which uses a routerLink directive from Angular to route to a different context:

   <c8y-action-bar-item [placement]="'right'">
     <a class="btn btn-link" routerLink="add">
       <i class="fa fa-plus-square"></i> {{'Add' | translate}}
     </a>
   </c8y-action-bar-item>

The above example gives you an action bar item in the header bar, regardless in which component you define it. If the component is initialized the item is shown and it is removed on destroy.

Multi Provider (MP)

The Multi Provider extension allows a declarative approach to extend the application. Instead of defining it in the template, you extend an already defined factory via a HOOK. This hook gets executed if the application state changes. The return values are then injected into the page. You can use the normal dependency injection system of Angular and as a result you can usually return an Observable, Promise or Array of a certain type. As an example we can define the tabs of certain routes by hooking into the HOOK_TABS provider:

   import { Injectable } from '@angular/core';
   import { Router } from '@angular/router';
   import { Tab, TabFactory, _ } from '@c8y/ngx-components';

   @Injectable()
   export class ExampleTabFactory implements TabFactory { // 1

     constructor(public router: Router) { }

     get() {
       const tabs: Tab[] = [];
       if (this.router.url.match(/world/g)) {            // 2
         tabs.push({
           path: 'world/awesome',
           label: 'Awesome',
           icon: 'angellist'
         } as Tab);
       }
       return tabs;                                      // 3
     }
   }

By defining a Injectable() services which implements the TabFactory (1) you can define which tabs you want to show on which page. By using the Router service of Angular we check in this example if the URL of the route contains the name world (2) and only if this matches the tab labeled Awesome is returned (3). By hooking this into your provider definition of your module you make sure, that the get() function is checked on each route change:

    @NgModule({
      declarations: [
        /* ... */
      ],
      imports: [
        BrowserAnimationsModule,
        RouterModule.forRoot(),
        ngRouterModule.forRoot([/* ... */], { enableTracing: false, useHash: true }),
        CoreModule.forRoot()
      ],
      providers: [
        { provide: HOOK_TABS, useClass: ExampleTabFactory, multi: true} // hook the ExampleTabFactory defined earlier
      ],
      bootstrap: [BootstrapComponent]
    })
    export class AppModule { }

Usually you use Content Projection within a route and Multi Provider if the context is shared across multiple routes or needs more complex logic to resolve the content. Examples: a title is just valid for one route -> use Content Projection. A tab should only be shown on specific routes under certain conditions -> use Multi Provider. The following hooks are currently supported:

Services

A service is defined for most components of ngx-components. They can be used via the dependency injection concept of Angular, that means that these services can be injected in the constructor of a component and then add or remove certain UI elements. The following example shows how to use that concept with an alert:

   constructor(private alert: AlertService) {
     try {
       // do something that might throw an exception
     } catch(ex) {
       this.alert.add({
         text: 'Something bad happened!'
         type: 'danger';
         detailedData: ex;
       } as Alert);
     }
   }

Legacy plugins

If you are extending a default application (Cockpit, Device Management or Administration) you get a file called ng1.ts. These are so called plugins which haven’t been migrated to Angular yet and are still using angular.js. You can add or remove these plugins to customize the application appearance like it has been done previously in a target file by the addImports: [] or removeImports: [] property. The following shows an example which removes the default import in the angular.js target file:

    {
      "name": "example",
      "applications": [
        {
          "contextPath": "cockpit",
          "addImports": [
            "my-plugin/cockpit-home",
          ],
          "removeImports": [
            "core/cockpit-home"
          ]
        }
      ]
    }

You get the same result in the new Angular framework by modifying the ng1.ts file of the cockpit app:

    import '@c8y/ng1-modules/core';
    // [...] more imports removed for readability
    import '@c8y/ng1-modules/alarmAssets/cumulocity.json';
    // import '@c8y/ng1-modules/cockpit-home/cumulocity.json';              // 1
    import '@c8y/ng1-modules/deviceControlMessage/cumulocity.json';
    import '@c8y/ng1-modules/deviceControlRelay/cumulocity.json';
    // [...] more imports removed for readability
    import 'my-plugin/cumulocity.json';                                    // 2

As you can see we simply removed the import of the original welcome screen plugin (1.) and replaced it by the custom implementation (2.). Note that all angular.js plugins need to have the /cumulocity.json addition to tell webpack that a legacy plugin is imported.

To use legacy plugins in your custom non-default application you need to set the upgrade flag in the package.json file and use the same import approach like described before:

    "c8y": {
      "application": {
        "name": "myapp",
        "contextPath": "myapp",
        "key": "myapp-application-key",
        "upgrade": true
      }
    }

Also the module definition of your application must be changed to support these plugins:

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

    @NgModule({
      imports: [
        BrowserAnimationsModule,
        RouterModule.forRoot(),
        NgRouterModule.forRoot([
          ...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();
      }
    }

That will let your app start in a hybrid mode, which allows to use angular.js and Angular plugins/modules.

To determine which extension points are supported and which concept should be used for certain scenarios the following section gives an overview on all supported components and explains in which case they should be used.

Data access to the platform

The CommonModule exports the DataModule, an abstraction of the @c8y/client which allows to use the services of the client with the dependency injection system of Angular. So in any module in which the CommonModule or DataModule is imported you can use simple injection to access data of the platform:

import { Component } from '@angular/core';
import { AlarmService } from '@c8y/client';              // 1

@Component({selector: 'app-alerts', template: ''})
export class AlarmComponent {
  constructor(private alarmService: AlarmService) {}    // 2

  async getAllAlarms() {
    const alarms = await this.alarmService.list();      // 3
    return alarms.data;
  }
}
  1. Import the service from the @c8y/client package.
  2. Dependency inject that service.
  3. Use that service to request data from the platform.

For detailed information on all available services and on how to filter and select data refer to @c8y/client.

Client library

The @c8y/client is an isomorphic (node and browser) Javascript client library for the Cumulocity IoT platform API.

Installation

npm install @c8y/client

Usage

Use client.<endpoint>.list() to request listed data from the Cumulocity IoT REST API and client.<endpoint>.detail(<id>) to request detail information. These methods always return a promise. To get an observable use list$ or detail$.

In the following sections, the default signature of these functions is described. For detailed information, refer to the complete documentation).

Get detail and list data with promises (pull)

Method Description Parameters Return
detail(entityOrId) Request detail data of a specific entity. entityOrId: string | number | IIdentified: An object which contains an id or an id as number or string. Promise<IResult<TData>>: The list as Promise wrapped in an IResult. IResultList contains data and response.
list(filter) Request a list of data with an optional filter. filter:object: (optional) A filter for paging or filtering of the list. Promise<IResultList<TData>>: The list as Promise wrapped in an IResultList. IResultList contains data, response and paging.

Accessing a microservice with the Fetch API

The client internally uses the Fetch API. By accessing this core function, you can do any authenticated request to any resource. Standalone you can use core.client.fetch(url, options) and in @c8y/ngx-components/data for Angular you simply need to inject the FetchClient:

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

async getData() {
  const options: IFetchOptions = {
      method: 'GET',
      headers: { 'Content-Type': 'application/json' }
    };
  const response = await fetchClient.fetch('/service/my-service', options); // Fetch API Response
}

All fetch responses can be parsed to JSON if the content type is set correctly. Find more information on handling fetch responses in the MDN documentation.

Authentication strategy

In the Cumulocity IoT platform we currently allow two ways to authenticate:

To quickly get you started, the @c8y/client provides a shorthand static function which always uses Basic Auth and verifies the login directly:

await Client.authenticate({ tenant, user, password }), url);

It internally creates a client instance and tries to contact the API to verify if the given credentials are correct. In some cases you need to use a more fine-grained authentication, e.g. when you don’t know which authentication strategy the user is going to use. In this case you need to construct an own instance of the client and pass the authentication strategy to it:

 const baseUrl = 'https://acme.cumulocity.com';
 const client = new Client(new CookieAuth(), baseUrl); // use here `new BasicAuth()` to switch to Basic Auth
 try {
  const { data, paging, res } = await client.user.currentUser();
  console.log('Login with cookie successful');
 } catch(ex) {
  console.log('Login failed: ', ex)
 }

Subscribe to detail and list data with observables (push)

The detail$ and list$ functions allow to subscribe to realtime channels that omit data on each change:

Method Description Parameters Return
detail$(entityOrId, options) Returns an observable for detail data of one entity entityOrId: string | number | IIdentified: An object which contains an id or an id as number or string.
options: IObservableOptions: (optional) An configuration object to define the observable.
Observable<TData>>: The list as subscribable observable.
list$(filter, options) Returns an observable for a list of entities. filter: object: (optional) A filter for paging or filtering of the list (optional).
options: IObservableOptions: (optional) An configuration object to define the observable.
ObservableList<TData>>: The list as subscribable observable.

Examples

Below some examples are provided which may help you to get started. To see a complex and full implementation of the client into Angular, have a look at @c8y/cli and the new command to spin up a example application for Angular.

Requesting list data from the inventory:

import { Client } from '@c8y/client';

const baseUrl = 'https://demos.cumulocity.com/';
const tenant = 'demos';
const user = 'user';
const password = 'pw';

(async () => {
  const client = await Client.authenticate({
    tenant,
    user,
    password
  }, baseUrl);
  const { data, paging } = await client.inventory.list();
  // data = first page of inventory
  const nextPage = await paging.next();
  // nextPage.data = second page of inventory
})();

Getting an observable of the inventory endpoint:

import { Client } from '@c8y/client';

const baseUrl = 'https://demos.cumulocity.com/';
const tenant = 'demos';
const user = 'user';
const password = 'pw';

(async () => {
  const client = await Client.authenticate({
    tenant,
    user,
    password
  }, baseUrl);
  client.inventory.list$().subscribe((data) => {
    // request inventory data via fetch and adds realtime if data changes
    console.log(data);
  });
})();

Using realtime:

// realtime event
const subscription = client.realtime.subscribe('/alarms/*', (data) => {
  console.log(data); // logs all alarm CRUD changes
});
client.realtime.unsubscribe(subscription);

// realtime observable
const observable$ = client.realtime.observable('/alarms/*');
const observableSubscription = observable$.subscribe((data) => {
  console.log(data)); // logs all alarm CRUD changes
});
observableSubscription.unsubscribe();

Authenticate in node.js

The constructor new Client([...]) initializes a new client which allows to request data from the API. Unlike to Client.authenticate([...]) it needs a tenant given and does not verify if the login is correct. This is useful if you are developing a node.js microservice.

const auth = new BasicAuth({ 
   user: 'youruser',
   password: 'yourpassword',
   tenant: 'acme'
 });

 const baseUrl = 'https://acme.cumulocity.com';
 const client = new Client(auth, baseUrl);
 (async () => {
   const { data, paging, res }); =  await client.inventory.list({ pageSize: 100 });
 })();

Migrating

This section targets to developers who have either already developed a plugin with the AngularJS SDK for plugins or want to extend the existing default applications.

You should have read and understand the following concepts:

Setting up a hybrid application

With a hybrid application AngularJS and Angular can be used at the same time. It allows to use not-migrated plugins written in AngularJS in an Angular context. The CLI automatically sets up a hybrid application, if you use one of our default applications as a template (cockpit, devicemangement or administration). The command to be used is c8ycli new <your-app-name> <template-name>.

For example, to override the default cockpit you use:

c8ycli new cockpit cockpit

When you run this command it provides you with a very simple file structure that contains the following files:

Importing AngularJS plugins

If you want to integrate your custom plugin into an application, you first need to set up the hybrid application for the application into which you want to import the plugin. Then you simply copy the plugin to the hybrid applicatiob and reference the cumulocity.json of the plugin in the ng1.ts file with an import:

import 'my-custom-plugin/cumulocity.json';

Webpack now reads the manifest file and converts the content to CommonJS require imports so that the plugin will be loaded and applied to the application.

Custom bootstrapping and upgrading

In the app.module.ts file we bootstrap the hybrid application with the following code on the HybridAppModule.

import { UpgradeModule as NgUpgradeModule } from '@angular/upgrade/static';
import { ng1Modules } from './ng1';

export abstract class HybridAppModule {
  ng1Modules = ng1Modules;
  protected upgrade: NgUpgradeModule;

  ngDoBootstrap() {
    (window as any).bootstrap();
    this.upgrade.bootstrap(document.getElementById('app'), this.ng1Modules, { strictDi: false });
  }
}

This module not only bootstraps the upgrade module but also initializes the Angular application.

In the app.module.ts you can control the bootstrapping. For example, you can add another module to the bootstrapped ng1 modules:

constructor() {
  this.ng1Modules.push('my-custom-module');
}

This will also load your my-custom-module. You can use this to override the ngDoBootstrap() function to whatever fits your application needs, e.g. upgrade or downgrade Angular services as shown in the Angular docs.

Applications

The application package (@c8y/apps) provides example applications for the Web SDK.

Prerequisites

To use the @c8y/apps you need to install the @c8y/cli. Refer to its documentation for installation instructions.

Once installed you can run:

$ c8ycli new [your-app-name] [example-name]

For example, to generate the tutorial application with the name my-app you need to run:

$ c8ycli new my-app tutorial

Included applications

The following table provides an overview on the currently supported applications:

Name Description
application An empty application to quickly bootstrap new applications. It is the default application and used if you don’t specify an [example-name].
tutorial An application that already assembles most of the concepts of the @c8y/ngx-components. Use this to get real code examples.
cockpit The Cockpit default application. Use this to extend the existing Cockpit application.
devicemanagement The Devicemanagement default application. Use this to extend the existing Device Management application.
administration The Administration default application. Use this to extend the existing Administration application.

Branding and language customization

Using application options, each tenant can customize the look and feel of built-in applications and add or replace the languages available in the applications. As described in Application options, the underlying mechanism is static hosted web application.

In this tutorial we are publishing 2 web applications:

For deploying we use the nodejs @c8y/cli that can be installed with the command:

npm install -g @c8y/cli

Downloading or cloning the initial repository

For your convenience you can download or clone the repository available at https://github.com/Cumulocity/ui-customization, in which you can find an example for branding and for adding a new language.

git clone https://github.com/Cumulocity/ui-customization

Inside this folder you can find two other folders:

public-options
ui-assets

Branding options

Edit the file public-options/options.json and change the sub-properties of brandingCssVars. These properties will be converted into CSS custom properties at runtime.

Note that the properties brand-logo-img and navigator-platform-logo are both URLs. Therefore the corresponding files must be placed inside the folder ui-assets.

To change the favicon, edit the property faviconUrl and/or add the corresponding file inside the ui-assets folder.

To change the browser window title, change the property globalTitle.

If these configurations are not enough you can still add a list of URLs to the property extraCssUrls and load extra CSS at runtime:

{
  "extraCssUrls": [
    "/apps/ui-assets/extra.css"
  ]
}

Languages

The platform UI strings used for internationalization are stored in gettext. If you want to add a new language to the platform you need a software to edit these files, for example Poedit.

Each translated catalog is loaded at runtime in a JSON format. To convert .po (gettext) files into .json files we rely on @c8y/cli installed during the first step.

How to add your own translations

  1. Download the string catalog from @c8y/ngx-components@1004.0.6/locales/locales.pot (starting from version 1004.0.6,latest can be replaced by your current used version).
  2. Load the downloaded locales.pot template file in your preferred .pot file editor and create a new translation from it. Choose the target language of the translation, e.g. Afrikaans, and translate each string. Repeat the process for as many languages as you like. The outcome of this step will be a .po catalog file for each language. Make sure to store these files in a safe place, as they will be useful when updating the strings in subsequent versions.
  3. Transform the newly created .po file into a .json file using the c8ycli:
c8ycli locale-compile path/to/language.po
  1. Copy the generated .json file into the ui-assets folder.
  2. Update the languages fragment in public-options/options.json.
languages?: {
  [langCode: string]: {
    name: string;
    nativeName: string;
    url: string;
  }
}

In the example provided in the repository to be downloaded you can find an example of a Russian translation which looks like this:

"languages": {
  "ru": {
    "name": "Russian",
    "nativeName": "русский язык",
    "url": "/apps/public/ui-assets/ru.json"
  }
}

The imported language can be changed in the UI after login. To do so, click the User icon at the top left, select User settings from the menu and in the upcoming window select the language of your choice.

Deploying

Inside the folder ui-customization that contains both public-optionsand ui-assets run the command:

c8ycli deploy public-options ui-assets

Fill in your tenant/instance information and the applications will be deployed and will be visible to that specific tenant and its subtenants.

Info: For performance reasons the options are cached. Therefore the application must be refreshed twice to make the changes visible.