AngularJSのためのWeb SDK

概要

ここでは以下のことが可能になるAngularJSのためのWeb SDKの概要を説明します:

  • こちらのドキュメントに記載されちるモジュール"c8y.core"にあるAngularJSサービスを使用してThings Cloud のエコシステムに接続されるAngularJSコンポーネントを作成。
  • 独自ユースケースに応じたカスタム機能を導入。

Things Cloud アプリケーションの基本的概念については「アプリケーション開発」をご覧ください。

アプリケーション開発

このチュートリアルでは、Web SDKを使って簡単なAngularJSアプリケーションを構築するプロセスを紹介します。この結果は Cumulocity 社のリポジトリhttps://bitbucket.org/m2m/cumulocity-ui-plugin-examplesにて、hello-core-apiフォルダから閲覧可能です。実行してみたい場合、リポジトリを複製するか、またはbitbucketからダウンロードし、以下の手順で実行してください。

  • hello-core-api/js/app.jsにアクセスし、c8yCumulocityProviderを含む行を探します。c8yCumulocityProvider.setAppKeyc8yCumulocityProvider.setBaseUrlを使用してあなたのアプリケーション・キーとCumulocity domainを設定します。次の手順は以下の通りです:
$ cd hello-core-api
$ npm install
$ bower install
$ grunt server
  • 最後に、あなたのブラウザからlocalhost:8080/にアクセスします。

前提条件

以下の技術を使用し、読者はこれらを理解している必要があります。

プラグインを開発し、実行できるようになるための前提条件は以下の通りです:

  • Node.js(0.10以上、安定しているもの)とBower およびGruntのインストールが必要となります。
  • あなたの Things Cloud アカウントへのアクセス、すなわちあなたのテナント名、ユーザー名およびパスワードが必要となります。

あなたのシステムにnodegruntおよびbowerが確実にいインストールされているようであれば、スキップしてstep 1へ進んでください。

Steps

  1. 従属物のバージョンをチェックする
  2. プロジェクト構造をセットアップする
  3. 従属物をセットアップする
  4. index.htmlを作成する
  5. AngularJSアプリケーションを作成する
  6. ログイン画面を作成する
  7. メイン画面を作成する
  8. デバイス/アラーム/イベントリストを作成する
  9. フィルタリングを実装する
  10. リフレッシュボタンを作成する

0. 従属物のバージョンをチェックする

ノード

まずノードバージョンをチェックし、0.10以上であることを確認します。

~ $ node --version
v0.10.39

Bower

bowerがグローバルにインストールされている必要があります。以下のようにチェックします。

~ $ bower --version
1.4.1

bowerコマンドが見つからない場合:

~ $ npm install bower -g

bowerを最新版に更新する場合:

~ $ npm update bower -g

Grunt CLI

grunt-cliのコマンド行インターフェースがグローバルにインストールされている必要があります。以下のようにチェックします。

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

gruntコマンドが見つからない場合:

~ $ npm install grunt-cli -g

grunt-cliを最新版に更新する場合:

~ $ npm update grunt-cli -g

1. プロジェクト構造をセットアップする

プロジェクト用に以下のフォルダ構成を作ります。

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

Cumulocity 社のリポジトリにあるプロジェクト例hello-core-api/cssフォルダからcssファイルをコピーします。

2. 従属物をセットアップする

以下をbower.jsonにコピーします。

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

このjsonファイルは、ここでのプロジェクト例で依存するモジュール/ライブラリを定義します。これで、以下を実行できるようになります。

hello-core-api $ bower install

以下をpackage.jsonにコピーします。

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

http-serverを、静的ファイルを提供するミニHTTPサーバーとして使用することにします(file://ドメインへのAJAXリクエストをブラウザが禁じているため)。次に、依存関係のあるモジュール/ライブラリをインストールします。

hello-core-api $ npm install

以下を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']);
};

これはHTTPサーバーを起動するgruntタスクを登録します。

3. index.htmlを作成する

では、index.htmlファイルを作成しましょう。

<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_dir.js"></script>
  </head>
  <body ng-app="helloCoreApi">
    <ng-view />
  </body>
</html>

これで、以下を実行できます。

hello-core-api $ grunt server

あなたのブラウザからhttp://localhost:8080にアクセスする場合、ブラウザは空のページをロードし、ブラウザのコンソールに大量のエラーが表示されるはずです。これは一部のJavaScriptファイルが見つからないからです。

  • ng-app: 所定の名称で定義されたモジュールからAngularJSアプリケーションをブートストラップします。
  • ng-view: 経路構成設定時に定義された通りに部分的なHTMLファイルをロードするangular-routeディレクティブ。

4. AngularJSアプリケーションを作成する

AngularJSアプリケーションを作成しましょう。js/app.jsにおいて以下を実行します。

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

helloCoreAping-appディレクティブと一緒に使用されるモジュール名です。括弧の間はすべて、他のモジュールに対する依存関係です。Things Cloud で利用できるコアサービスはc8y.sdk内で定義されます。

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はAMDと似た構文を使用して依存関係を宣言します。詳しくは「AngularJS DIガイド」をご覧ください。$routeProviderは、ロードするHTMLファイルおよび実行するコントローラの経路を変更できるようにします。例えば、ブラウザからlocalhost:8080/index.html/#/loginにアクセスする場合、login.htmlがロードされ、LoginCtrlが実行されます。.when('/:section')はURLの:section部分が何でもよいことを示し、その値をコントローラから変更することができます。controllerAsの値は、コントローラ値にHTMLファイルからアクセスする際の変数名(例:login.username)となるため、重要です。

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

以上が、Things Cloud アプリケーションに役立つあなたのアプリケーション・キー、テナントおよびドメインを設定するためのc8y.coreの構成設定方法です。

5. ログイン画面を作成する

Login screen

以下のコードを使ってログイン画面を作成します。c8y.coreはテナント、ユーザー名およびパスワードが設定されていないと機能しないため、"src/login_ctrl.js"内での設定が必要です。

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

function LoginCtrl(
  $location,
  $rootScope,
  c8yUser
) {

  c8yUser.current().then(function () {
    $location.path('/');
    $rootScope.c8y.user = c8yUser;
  });

  this.credentials = {};
  this.onSuccess = function () {
    $location.path('/');
  };
}

c8yUser.currentは現在ログイン中のユーザーのPromiseを返します。ユーザーがすでにログイン中であれば、/へリダイレクトされます。ここではアプリケーションを/へリダイレクトするonSuccess関数を定義します。

<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はAngularJS こちらで、よく文書化されています。c8y-loginディレクティブはc8y.coreモジュール内で定義されます。これはユーザーがログインした際の、認証情報を保持しており、以下のようなシグネチャです。

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

localhost:8080/index.html/#/loginにアクセスすると、ログイン画面が表示されるはずです。あなたの認証情報を入力すればログインできますが、やはりlocalhost:8080/#/には何も表示されません。

ログイン画面でテナントのフィールドを省略したい場合、設定段階でc8yCumulocityProvider.setTenant関数を使用してテナントを設定しておくとよいです。

6. メイン画面を作成する

Main screen

メイン画面は上部のナビゲータ、左側のナビゲータ、そしてコンテンツエリアで構成されます。デバイス/アラーム/イベント画面を実装すると、コンテンツが表示されますが、とりあえず、メイン画面に専念しましょう。以下の機能を"js/main_ctrl.js"ファイルに追加してください:

angular.module('helloCoreApi').controller('MainCtrl', [
  '$location',
  '$routeParams',
  '$rootScope',
  'c8yAuth',
   MainCtrl
]);
var loggedIn = false;

function MainCtrl(
  $location,
  $routeParams,
  $rootScope,
  c8yAuth
) {

  $rootScope.$on('authStateChange', function (evt, state) {
    loggedIn = state.hasAuth;
  });

  c8yAuth.initializing.then(function() {
    if(!loggedIn) {
      $location.path('/login');
    }
  });

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

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

現在のユーザーのstate.hasAuthfalseとなった場合、ログイン画面にリダイレクトします。currentSectionthisに割り当てることにより、main.htmlからアクセスできるようにします。this.sectionsはメニューラベル、セクション名ペアのキーバリュー型辞書です。this.filterはセクション全体について1つの同期されたフィルターオブジェクトを持つ目的で使用されます。this.logoutはログアウト時のコールバックとして使用されます。次に、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="#/">Cumulocity</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は、クリックされた場合にすべてのユーザーとセッション情報をc8y.coreから削除するディレクティブです。これはログアウト完了時に実行されるng-clickと併用できます。シグネチャは以下の通りです。

<ANY c8y-logout>
</ANY>

eg-sectionステップ7で作成することになるディレクティブです。これはデバイス、アラームまたはイベントリストを、main.currentSectionの値に応じて表示する表を描画します。タグ間のコンテンツは、ng-transcludeを使用して、描画される表の上に配置されます。今回その機能を使用して、フィルターがリストするコンポーネントを描画します。

c8y.userは、ログイン中の Things Cloud ユーザーが存在する場合に利用可能なオブジェクトです。これは$rootScope上で定義されます。ユーザーオブジェクト構造について詳しくは Cumulocity社の c8yUser関連資料(英語)をご覧ください。

残りは単純な ng-switchng-repeatなどのAngularJSディレクティブで、これらについて詳しくはAngularJS関連資料をご覧ください。

localhost:8080/index.html/#/を一度ご覧ください。

7. デバイス/アラーム/イベントリストを作成する

Devices screen

全てのセクション画面と同様、デバイス、アラームおよびイベントの画面は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>

ここではすべてのセクション画面向けに使用されるeg-sectionディレクティブを定義します。これはngTransclude, $watchおよびcontroller as構文を活用します。これはfilter.pageSizeに10を割り当てます。REST APIに詳しい方なら、GETリクエストから返される結果オブジェクトの数を制限していることにお気付きのはずです。

ここの決定的な要素はc8y-repeatディレクティブです。シグネチャは以下の通りです。

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

repeat_expressionsomeVar in *によく似た表現が使えます。ただし*はサポートサービスのうち1つが該当します。本書の末尾をご覧ください。

ここではcontroller as構文を、ルートを定義した際と同様に使用します。refreshc8y-repeatにより設定され、これを使用してデータをリフレッシュすることができます。注記:双方向データ結合を使用しますので、ドットルールに従わなければなりません。

サポート対象フィルターについてはそれぞれのサービス関連資料をご覧ください(service documentation)。

以上で、デバイス、アラームおよびイベントをリストアップ可能な、完全に機能を果たすWebアプリケーションを用意できました。

8. フィルタリングを実装する

ここでは、テキストによるデバイスフィルタリグと、重大度によるアラームフィルタリングを実装します。

デバイス検索

Device search screen

sections/devices.html冒頭の <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>

main.filter.textおよびmain.textFilterでは似て比なる変数が存在します。c8y-repeatは、フィルターが変わるとデータをリフレッシュします。検索フィールドにユーザーが文字を入力する都度、データをリフレッシュさせるわけにもいきませんので、2つの別々の変数を使用し、それらをng-submitにおいて同期させます。

ここで、localhost:8080/index.html/#/devicesを再びチェックします。

重大度によるアラームフィルタリング

重大度によるアラームフィルタリングはもっと冗長になりますので、まず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>

このフィルタリング向けに、アラーム重大度を表現可能な複数のオブジェクトから成るアレイを定義します。ng-repeatを使用しての反復は大したことではありません。それらのうち1つがクリックされると、重大度を解除してfilter.severityundefinedに設定するか、あるいは実際の重大度を設定します。c8y-repeatはフィルターが変わると自動的にリフレッシュしますので、すべきことは以上です。

9. リフレッシュボタンを作成する

この最後の例では、フィルターを作成しません。フィルターがない状態になりますので、別の方法でデータをリフレッシュする必要があります。それをsections/events.html内で行う手順は以下の通りです。

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

まだ説明できていない部分として、この例では2段階の2方向結合の連鎖が存在します。eg-sectionディレクティブはmain.refreshsection.refreshを相互に結合します。c8y-repeatsection.refreshを固有の専用リフレッシュ関数に結合します。events.html内ではsectionに全くアクセスできません。それは"section.html"ではなく"main.html"内のngIncludedであるからです。

結果

c8y.core API、もっとわかりやすく言えばAngularJSのためのWeb SDKを使用して、AngularJSアプリケーションを1から作成できました。おめでとうございます!


c8y-repeatでサポートされるサービス

  • デバイス
  • アラーム
  • イベント
  • インベントリ