Smart Apps Toolkit v1.0

Overview

Smart Apps Toolkit allows you to create AngularJS components that are connected to Device Integration Platform ecosystem. With c8y.sdk, you can use AngularJS services as documented here. In this document, we describe step-by-step how to create a simple AngularJS application using c8y.sdk. For the basic concepts of Device Integration Platform applications, please see Developing applications. For reference information, please see the Plugins reference.

The result of this tutorial is available at https://bitbucket.org/m2m/cumulocity-examples under folder hello-core-api. If you wish to run it, either clone the repo or download it from bitbucket and do as follows:

Go to hello-core-api/js/app.js and find the lines with c8yCumulocityProvider. Use c8yCumulocityProvider.setAppKey and c8yCumulocityProvider.setBaseUrl to set your application key and your Device Integration Platform domain. Then:

$ cd hello-core-api
$ npm install
$ bower install
$ grunt server

Now go to localhost:8080/ in your browser.

Prerequisites

You should be familiar with the following technologies:

You will need the following prerequisites for being able to develop plugins and to execute the examples:

  • You will need Node.js (0.10 or newer, stable) and Grunt installed.
  • You will need access to your Device Integration Platform account, i.e. you need your tenant name, username and password.

If you are sure that you have node, grunt and bower installed on your system, you can skip to Step 1.

Steps

  1. Check dependency versions
  2. Setup project structure
  3. Setup dependencies
  4. Create index.html
  5. Create an AngularJS app
  6. Create login screen
  7. Create main screen
  8. Create device/alarm/event lists
  9. Implement filtering
  10. Create a refresh button

0. Check dependency versions

node

Start with checking your node version and make sure that it is 0.10 or newer:

~ $ node --version
v0.10.39

bower

You need bower installed globally. First check if you have it:

~ $ bower --version
1.4.1

If bower command cannot be found:

~ $ npm install bower -g

To update bower to the latest version:

~ $ npm update bower -g

grunt-cli

You need grunt-cli installed globally. First check if you have it:

~ $ grunt --version
grunt-cli v0.1.13
grunt v0.4.5

If grunt command cannot be found:

~ $ npm install grunt-cli -g

To update grunt-cli to the latest version:

~ $ npm update grunt-cli -g

1. Setup project structure

Create the following folder structure for the project:

hello-core-api
.
├── Gruntfile.js
├── bower.json
├── css
├── index.html
├── js
│   ├── alarms_ctrl.js
│   ├── app.js
│   ├── login_ctrl.js
│   ├── main_ctrl.js
│   └── section_dir.js
├── login.html
├── main.html
├── package.json
├── section.html
└── sections
    ├── alarms.html
    ├── devices.html
    └── events.html

Copy css files from the example project under folder hello-core-api/css.

2. Setup dependencies

Copy the following into bower.json:

{
  "name": "hello-core-api",
  "dependencies": {
    "bootstrap": "~3.3.5",
    "angular-route": "1.2.20",
    "cumulocity-clients-javascript": "latest"
  }
}

This json file defines modules/libraries that our example project depends on. Now you can run the following:

hello-core-api $ bower install

Copy the following into package.json:

{
  "name": "hello-core-api",
  "devDependencies": {
    "grunt": "^0.4.5",
    "grunt-http-server": "^1.4.0",
    "http-server": "^0.8.0"
  }
}

We are going to use http-server as a mini http server that serves static files (because browsers prohibit making AJAX requests from a file:// domain). Now install dependencies:

hello-core-api $ npm install

Copy the following into Gruntfile.js:

module.exports = function(grunt) {
  grunt.config('http-server.dev', {
    port: 8080,
    host: "0.0.0.0",
    ext: "html",
    runInBackground: false,
  });
  grunt.loadNpmTasks('grunt-http-server');
  grunt.registerTask('server', ['http-server:dev']);
};

This registers a grunt task to start http server.

3. Create index.html

Now let's create the index.html file:

<html>
  <head>
    <link rel="stylesheet" type="text/css" href="bower_components/bootstrap/dist/css/bootstrap.css">
    <link rel="stylesheet" type="text/css" href="bower_components/bootstrap/dist/css/bootstrap-theme.css">
    <link href="css/login.css" rel="stylesheet">
    <link href="css/dashboard.css" rel="stylesheet">

    <!-- Put cumulocity Javascript dependencies here such as angular, lodash etc. You can copy them from the example project. -->
    <!--<script src="bower_components/angular/angular.js"></script>-->

    <script src="bower_components/cumulocity-clients-javascript/build/main.js"></script>
    <script src="js/app.js"></script>
    <script src="js/login_ctrl.js"></script>
    <script src="js/main_ctrl.js"></script>
    <script src="js/section_ctrl.js"></script>
  </head>
  <body ng-app="helloCoreApi">
    <ng-view />
  </body>
</html>

Now you can run the following:

hello-core-api $ grunt server

If you visit http://localhost:8080 from your browser, it should load an empty page, with a bunch of errors in browser console because some Javascript files cannot be found.

  • ng-app: bootstraps an AngularJS app, from a module defined with given name.
  • ng-view: angular-route directive that loads partial HTML files as defined in routes configuration.

4. Create an AngularJS app

Let's create the AngularJS app. In js/app.js:

var app = angular.module('helloCoreApi', [
  'c8y.sdk',
  'ngRoute',
  'ui.bootstrap'
]);

helloCoreApi is the module name that is used with ng-app directive. Everything between brackets are dependencies to other modules. Device Integration Platform services are defined in c8y.core.

app.config([
  '$routeProvider',
  configRoutes
]);
function configRoutes(
  $routeProvider
) {
  $routeProvider
    .when('/login', {
      templateUrl: 'login.html',
      controller: 'LoginCtrl',
      controllerAs: 'login'
    })
    .when('/', {
      templateUrl: 'main.html',
      controller: 'MainCtrl',
      controllerAs: 'main'
    })
    .when('/:section', {
      templateUrl: 'main.html',
      controller: 'MainCtrl',
      controllerAs: 'main'
    });
}

AngularJS uses syntax similar to AMD to declare dependencies. For more information see AngularJS DI Guide. $routeProvider lets you choose which HTML file to load and which controller to execute depending depending on the route. For example, if browser goes to localhost:8080/index.html/#/login, login.html will load and LoginCtrl will be executed. .when('/:section') allows :section part of the URL to be anything, and you can access that value from controllers. controllerAs value is important as it will be the variable name that is going to be used in HTML files to access controller values (e.g. login.username).

app.config([
  'c8yCumulocityProvider',
  configCumulocity
]);
function configCumulocity(
  c8yCumulocityProvider
) {
  c8yCumulocityProvider.setAppKey('core-application-key');
  c8yCumulocityProvider.setBaseUrl('https://my-tenant.customer.com/');
}

This is how you configure c8y.core to set your application key, tenant and domain that serves Device Integration Platform application.

5. Create login screen

Login screen

Let's create a login screen. c8y.core won't work without tenant,username and password set; so you need it.

src/login_ctrl.js:

angular.module('helloCoreApi').controller('LoginCtrl', [
  '$location',
  'c8yUser',
  LoginCtrl
]);

function LoginCtrl(
  $location,
  c8yUser
) {
  c8yUser.current().then(function () {
    $location.path('/');
  });
  this.onSuccess = function () {
    $location.path('/');
  };
}

That was easy right? c8yUser.current returns a Promise of currently logged in user. If a user is already logged in, a redirection to / is triggered. We define onSuccess function that redirects the application to /.

<div class="container">
  <form class="form-signin">
    <h2 class="form-signin-heading">Please login</h2>
    <label for="inputTenant" class="sr-only">Tenant</label>
    <input type="text" id="inputTenant" class="form-control" placeholder="Tenant" autofocus="" ng-model="login.tenant">
    <label for="inputUsername" class="sr-only">Username</label>
    <input type="text" id="inputUsername" class="form-control" placeholder="Username" required="" autofocus="" ng-model="login.username">
    <label for="inputPassword" class="sr-only">Password</label>
    <input type="password" id="inputPassword" class="form-control" placeholder="Password" required="" ng-model="login.password">
    <div class="checkbox">
      <label>
        <input type="checkbox" ng-model="login.rememberMe"> Remember me
      </label>
    </div>
    <button class="btn btn-lg btn-primary btn-block" type="submit" c8y-login
      data-tenant="login.tenant"
      data-user="login.username"
      data-password="login.password"
      data-remember-me="login.rememberMe"
      on-success="login.onSuccess()"
    >Sign in</button>
  </form>
</div>

ng-model is very well documented by AngularJS here. c8y-login directive is defined in c8y.core module. It logs in the user for which credentials are provided and it has the following signature:

<ANY c8y-login
  tenant="tenantName"
  user="username"
  password="password"
  remember-me="true|false"
  on-success="onSuccessCallback"
  on-failure="onFailureCallback">
</ANY>

If you visit localhost:8080/index.html/#/login, you should see the login screen. You can type in your credentials and login, but we still have nothing at localhost:8080/#/.

If you want to omit tenant field in login screen, you can set tenant once using c8yCumulocityProvider.setTenant function in the config phase.

6. Create main screen

The main screen is consisted of a top navigator, left navigator and a content area. When we implement device/alarm/event screens, content will be visible but for now, let's concentrate on the task on hand:

js/main_ctrl.js:

angular.module('helloCoreApi').controller('MainCtrl', [
  '$location',
  '$routeParams',
  'c8yUser',
   MainCtrl
]);

function MainCtrl(
  $location,
  $routeParams,
  c8yUser
) {
  c8yUser.current().catch(function () {
    $location.path('/login');
  });

  if (!$routeParams.section) {
    $location.path('/devices');
  }

  this.currentSection = $routeParams.section;
  this.sections = {
    Devices: 'devices',
    Alarms: 'alarms',
    Events: 'events'
  };
  this.filter = {};

  this.logout = function () {
    $location.path('/login');
  };
}

Similar to what we have done in login, we redirect to login screen if current user promise fails. In addition, we check if $routeParams.section exists or not (remember when('/:section')?). If it doesn't, then we redirect it to devices screen as an empty content won't make sense. We assign currentSection onto this to be able to access it from main.html. this.sections is a key-value dictionary of menu label, section name pairs. 'this.filter' is used to have one synchronized filter object for a whole section. this.logout will be used as a logout callback. Now for main.html:

<div ng-if="c8y.user">
  <nav class="navbar navbar-inverse navbar-fixed-top">
    <div class="container-fluid">
      <div class="navbar-header" ng-init="isCollapsed = true">
        <button type="button" class="navbar-toggle collapsed" ng-click="isCollapsed = !isCollapsed" aria-expanded="false" aria-controls="navbar">
          <span class="sr-only">Toggle navigation</span>
          <span class="icon-bar"></span>
          <span class="icon-bar"></span>
          <span class="icon-bar"></span>
        </button>
        <a class="navbar-brand" ng-href="#/">Device Integration Platform</a>
      </div>
      <div id="navbar" class="navbar-collapse" collapse="isCollapsed">
        <ul class="nav navbar-nav navbar-right">
          <li><a href="" ng-click="main.logout()" c8y-logout>Logout</a></li>
        </ul>
        <p class="navbar-text navbar-right">Hello {{c8y.user.firstName}}</p>
      </div>
    </div>
  </nav>
  <div class="container-fluid">
    <div class="row">
      <div class="col-sm-3 col-md-2 sidebar">
        <ul class="nav nav-sidebar">
          <li
            ng-repeat="(sectionLabel, section) in main.sections"
            ng-class="{'active': main.currentSection === section}">
            <a href="" ng-href="#/{{section}}">{{sectionLabel}}</a>
          </li>
        </ul>
      </div>
      <div class="col-sm-9 col-sm-offset-3 col-md-10 col-md-offset-2 main" ng-switch="!!main.currentSection">
        <div ng-switch-when="true">
          <eg-section service="{{main.currentSection}}" filter="main.filter" refresh="main.refresh">
            <ng-include src="['sections/', main.currentSection, '.html'].join('')">
            </ng-include>
          </eg-section>
        </div>
      </div>
    </div>
  </div>
</div>

c8y-logout is a directive that removes all user and session information from c8y.core, when clicked. It can be used in conjunction with ng-click, which is executed after logout is complete. Its signature is as follows:

<ANY c8y-logout>
</ANY>

eg-section is a directive that we will create in step 7. It renders a table to display either device, alarm or event list depending on the value of main.currentSection. The content between its tags will be put above the rendered table, using ng-transclude. We will use that capability to render components that filter lists.

c8y.user is an object that is available when there exists a logged in Device Integration Platform user. It is defined on $rootScope. To learn more about the user object structure, see c8yUser documentation.

The rest is simple AngularJS directives such as ng-switch, ng-repeat about which you can get more information from AngularJS docs.

Why don't you give localhost:8080/index.html/#/ a try now?

7. Create device/alarm/event lists

Devices screen

As all section screens; namely devices, alarms and events, will share common functionality, let's start with creating js/section_dir.js:

angular.module('helloCoreApi').controller('SectionCtrl', [
  '$scope',
  SectionCtrl
]).directive('egSection', [
  egSection
]);

function SectionCtrl(
  $scope
) {
  this.filter = $scope.filter || {};
  this.filter.pageSize = 10;
  this.service = $scope.service;
  $scope.$watch('section.refresh', function (val) {
    $scope.refresh = val;
  });
}

function egSection(
) {
  return {
    restrict: 'AE',
    templateUrl: 'section.html',
    controller: 'SectionCtrl',
    controllerAs: 'section',
    transclude: true,
    replace: true,
    scope: {
      service: '@',
      filter: '=?',
      refresh: '=?'
    }
  };
}

section.html:

<div>
  <div ng-transclude></div>
  <p class="text-warning">Page size is {{section.filter.pageSize}} by default. See <code>pageSize</code> filter.</p>
  <table class="table">
    <h2>List</h2>
    <tr c8y-repeat="x in {{section.service}}" filter="section.filter" refresh="section.refresh">
      <td>{{x.id}}</td>
      <td>{{x.type}}</td>
      <td>{{x.text}}</td>
      <td>{{x.name}}</td>
      <td>{{x.severity}}</td>
    </tr>
  </table>
</div>

We define a directive eg-section that will be used for all section screens. It makes use of ngTransclude, $watch and controller as syntax. It assigns filter.pageSize to 10. If you are familiar with Device Integration Platform REST API, you should have noticed we are limiting the number of result objects that are returned from GET requests.

The game breaker here is c8y-repeat directive. Its signature is as follows:

<ANY
  c8y-repeat="repeat_expression"
  filter="optionalFilter"
  refresh="optionalFunction">
...
</ANY>

repeat_expression: someVar in * where * can be one of supported services. See bottom of this document.

We use controller as syntax here as we did when we defined routes. refresh is set by c8y-repeat, then you can use it to refresh the data. Note that you must obey to the dot rule as it uses 2-way data-binding.

For supported filters, see respective service documentation at resources.cumulocity.com/documentation/jssdk/latest/#/core.

Now we have a fully functional web application that can list devices, alarms and events.

8. Implement filtering

In this part, we will implement device filtering by text and alarm filtering by severity.

Device search screen

Add the following to sections/devices.html at the beginning, inside <div ng-controller=...:

<form ng-submit="main.filter.text = main.textFilter">
  <div class="input-group">
    <input type="text" ng-model="main.textFilter" class="form-control" placeholder="Filter with device name...">
    <span class="input-group-btn">
      <button type="submit" class="btn btn-default" type="button">Submit</button>
    </span>
  </div>
</form>

There are main.filter.text and main.textFilter variables which are almost the same thing but differs a little. c8y-repeat will refresh its data when filter changes. Because we don't want it to be refreshed each time user types in a character in the search field, we use two separate variables and synchronize them in ng-submit.

Now check localhost:8080/index.html/#/devices again.

Alarm filtering by severity

Alarm filtering by severity will be more verbose so let's create a controller first at js/alarms_ctrl.js:

angular.module('helloCoreApi').controller('AlarmsCtrl', [
    AlarmsCtrl
  ]);

  function AlarmsCtrl(
  ) {
    this.severities = [
      {name: 'Critical', value: 'CRITICAL', cls: 'btn-danger'},
      {name: 'Major', value: 'MAJOR', cls: 'btn-warning'},
      {name: 'Minor', value: 'MINOR', cls: 'btn-primary'},
      {name: 'Warning', value: 'WARNING', cls: 'btn-info'}
    ];

    this.onClick = function (filter, severity) {
      if (filter.severity === severity.value) {
        filter.severity = undefined;
      } else {
        filter.severity = severity.value;
      }
    };

    this.isActive = function (filter, severity) {
      return filter.severity === severity.value;
    };
  }

HTML:

<div ng-controller="AlarmsCtrl as alarms" class="btn-group alarm-severity" role="group" aria-label="...">
  <style>
  .alarm-severity .btn:focus {
    outline: none;
  }
  </style>
  <button
    ng-repeat="severity in alarms.severities"
    class="btn {{severity.cls}}"
    ng-class="{'active': alarms.isActive(main.filter, severity)}"
    ng-click="alarms.onClick(main.filter, severity)">
    {{severity.name}}
  </button>
</div>

For this filtering, we define an array of objects that can represent alarm severities. Iterating over them using ng-repeat is trivial. When one of them is clicked, it either toggles off and sets filter.severity to undefined, or actually sets the severity. As c8y-repeat refreshes automatically when filter changes, that's all we have to do.

9. Create a refresh button

In this final example, we won't create a filter. As we won't have a filter, we need another way of refreshing data. Here is how we do it in sections/events.html:

<div>
  <button class="btn btn-default pull-right" ng-click="main.refresh()" class="margin-bottom:2em">Refresh</button>
</div>

If you haven't figured already, there's a 2-level chain of 2-way bindings in this example. eg-section directive binds main.refresh and section.refresh to each other. c8y-repeat binds section.refresh to its own private refresh function. Inside events.html, we have no access to section because it is ngIncluded inside main.html and not section.html.

Conclusion

You have created an AngularJS app from scratch using c8y.core API, better known as Smart Apps Toolkit. Congratulations!


Supported Services for c8y-repeat

  • Devices
  • Alarms
  • Events
  • Inventory