チュートリアル

このセクションでは、Web SDKの一般的な使い方レシピをリストアップしています。ここでは以下が必要となります。

  • Angular のコンポーネント、サービス、モジュールについて基本的な理解があること
  • アプリケーションのスキャフォールドの仕方と@c8y/cliを使ってアプリケーションを実行する方法を理解していること
  • @c8y/ngx-components の Extension points の概念について基本的な理解があること

備考
すべてのレシピは、Angular 向け Web SDK の特定のバージョンで書かれています。バージョンはレシピの上部に示されています。レシピで示されている機能の中には利用可能でないものもあるため、古いバージョンでの使用はお勧めしません。レシピよりも新しいバージョンを使用する場合、名前やインポートするものに変更があるかもしれません。概念的な改訂がある場合はレシピを更新しますが、小さな変更での更新はありません。すべてのコンセプトの最新の例を見るには、c8ycli new my-app tutorialでチュートリアルアプリケーションを確認してください。

ダッシュボードにカスタムウィジェットを追加する

バージョン: 1009.0.18 | パッケージ: @c8y/cli, @c8y/apps, @c8y/ngx-components

Things Cloudで提供しているウィジェットが要件を満たさない場合、カスタムウィジェットを作成しダッシュボードに追加できます。

典型的なダッシュボードは以下のようになり、さまざまなウィジェットが表示されます。

ダッシュボード

ここでは HOOK_COMPONENTS を用いてダッシュボードにカスタムウィジェットを追加する方法を示しています。

1. サンプルアプリケーションを初期化する

まずはじめに、ダッシュボードを表示するアプリケーションが必要です。このために、c8ycli を使用して新しいコックピットのアプリケーションを作成します。

c8ycli new my-cockpit cockpit -a @c8y/apps@1009.0.18

次に、依存関係をインストールします。新しく作成されたフォルダに移動し、npm install を実行してください。

備考
c8ycli newコマンドには、スキャフォールディングに使用するパッケージを定義する-aフラグがあります。このようにして、スキャフォールディングとするアプリケーションのバージョンを定義することも可能です。例えば、以下の通りです。

  • c8ycli new my-cockpit cockpit -a @c8y/apps@1009.0.18 は、アプリケーションのバージョン 10.9.0.18 をスキャフォールディングにします。
  • c8ycli new my-cockpit cockpit -a @c8y/apps@latest は、アプリケーションの最新公式リリースをスキャフォールディングにします。-a フラグを付けずに使用する場合と同じです。
  • c8ycli new my-cockpit cockpit -a @c8y/apps@next は、アプリケーションの最新ベータリリースをスキャフォールディングにします。

2. ウィジェットコンポーネントを作成する

ウィジェットは通常2つの部分で構成されます。

そのため2つのコンポーネントを作成する必要があります。

はじめに、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;
}

このコンポーネントは、CSSを介して、垂直に鏡写しされたテキストが表示します。他のAngularコンポーネントでできることは何でも行うことができます。

次のように定義されている demo-widget-config.component.ts から設定を渡すために config の入力が必要です。

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

再び、ウィジェットに渡したいシリアル化可能な設定で埋めることのできる config オブジェクトを追加する必要があります。

ウィジェット設定の検証を有効にするには、@Componentデコレータに次のオプションを追加する必要があります。

import { ControlContainer, NgForm } from "@angular/forms";

@Component({
    ...
    viewProviders: [{ provide: ControlContainer, useExisting: NgForm }]
})

上記の例と組み合わせると、設定検証を有効にしたdemo-widget-config.component.tsコンポーネントは、次のようになります。

import { Component, Input } from '@angular/core';
import { ControlContainer, NgForm } from "@angular/forms";

@Component({
  selector: 'c8y-widget-config-demo',
  template: `<div class="form-group">
    <c8y-form-group>
      <label translate>Text</label>
      <textarea style="width:100%" [(ngModel)]="config.text" name="text"></textarea>
    </c8y-form-group>
  </div>`,
  viewProviders: [{ provide: ControlContainer, useExisting: NgForm }]
})
export class WidgetConfigDemo {
  @Input() config: any = {};
}

3. アプリケーションにウィジェットを追加する

ウィジェットを追加するには、HOOK_COMPONENTSを使用しentryComponent として作成したコンポーネントを定義する必要があります。

そのためには、app.module.ts に以下を追加します。

import { NgModule } from '@angular/core';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { RouterModule as NgRouterModule } from '@angular/router';
import { UpgradeModule as NgUpgradeModule } from '@angular/upgrade/static';
import { DashboardUpgradeModule, UpgradeModule, HybridAppModule, UPGRADE_ROUTES} from '@c8y/ngx-components/upgrade';

// --- 8< 変更箇所 ----
import { CoreModule, RouterModule, HOOK_COMPONENTS } from '@c8y/ngx-components';
// --- >8 ----

import { AssetsNavigatorModule } from '@c8y/ngx-components/assets-navigator';
import { CockpitDashboardModule, ReportDashboardModule } from '@c8y/ngx-components/context-dashboard';
import { ReportsModule } from '@c8y/ngx-components/reports';
import { SensorPhoneModule } from '@c8y/ngx-components/sensor-phone';
import { BinaryFileDownloadModule } from '@c8y/ngx-components/binary-file-download';

// --- 8< 追加箇所 ----
import { WidgetDemo } from './demo-widget.component';
import { WidgetConfigDemo } from './demo-widget-config.component';
// --- >8 ----

@NgModule({
  imports: [
    // Upgrade module must be the first
    UpgradeModule,
    BrowserAnimationsModule,
    RouterModule.forRoot(),
    NgRouterModule.forRoot([...UPGRADE_ROUTES], { enableTracing: false, useHash: true }),
    CoreModule.forRoot(),
    AssetsNavigatorModule,
    ReportsModule,
    NgUpgradeModule,
    DashboardUpgradeModule,
    CockpitDashboardModule,
    SensorPhoneModule,
    ReportDashboardModule,
    BinaryFileDownloadModule
  ],

  // --- 8< 追加箇所 ----
  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();
  }
}

上記の数字の説明は以下の通りです。

  1. エントリーコンポーネントとしてコンポーネントを定義し、このモジュールによってアクセスできるように宣言します。
  2. HOOK_COMPONENTS と共にマルチプロバイダーフックを追加します。このフックはアプリケーションによって収集され、設定した値に基づいてウィジェットに追加します。
  3. インベントリに保存されているデータを特定するため、IDは一意である必要があります。ラベルと詳細記述はタイトルとウィジェットのドロップダウンとして表示されます。
  4. この部分は、すでに定義してあるコンポーネントとウィジェットに関連付けるようフックに指示します。

npm start でアプリケーションを開始すると、ダッシュボードにカスタムウィジェットが表示されているはずです。

ダッシュボードに追加されると、ウィジェットは以下のように表示されます。

カスタムウィジェット

Jestベースのユニットテストを追加する

バージョン: 1013.0.0 | パッケージ: @c8y/cli, @c8y/apps, @c8y/ngx-components

ユニットテストはすべての開発プロセスにおいて不可欠な要素です。 バージョン 10.13.0.0 以降、すべての新しい c8ycli スキャフォールディングアプリケーションには、ユニットテストフレームワーク Jest がデフォルトで含まれています。 このチュートリアルでは、最初のユニットテストを書いて検証する方法を紹介します。

1. サンプルアプリケーションを初期化する

例として、空のデフォルトのアプリケーションが必要です。

c8ycli new my-app application -a @c8y/apps@1013.0.62

ただし、どのようなアプリケーションでも、同じようにユニットテストをサポートします。次に、すべての依存関係をインストールする必要があります。

備考
c8ycli new コマンドには、スキャフォールディングに使用するパッケージを定義する-aフラグがあります。このようにして、スキャフォールディングとするアプリケーションのバージョンを定義することも可能です。例えば、以下の通りです。

  • c8cycli new my-cockpit cockpit -a @c8y/apps@1013.0.62 はバージョン 1013.0.62 のアプリケーションをスキャフォールディングします。
  • c8ycli new my-cockpit cockpit -a @c8y/apps@latest はアプリケーションの最新公式リリースをスキャフォールディングします。aフラグを付けずに使用する場合と同じです。
  • c8ycli new my-cockpit cockpit -a @c8y/apps@next は、アプリケーションの最新ベータリリースをスキャフォールディングにします。

2. コンポーネントを追加する

何かをテストするためには、まず検証可能なコンポーネントが必要です。そのため、test.component.ts という名前の新しいファイルを追加します。

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

@Component({
  selector: 'app-test',
  template: `<h1>Hello world</h1>`
})
export class TestComponent implements OnInit {
  constructor() { }

  ngOnInit(): void { }
}

新しく作成したコンポーネントを app.module.ts の宣言文に追加します。

import { NgModule } from '@angular/core';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { RouterModule as ngRouterModule } from '@angular/router';
import { CoreModule, BootstrapComponent, RouterModule } from '@c8y/ngx-components';

// --- 8< 変更箇所 ----
import { TestComponent } from ### 2. Adding a component
    ngRouterModule.forRoot([], { enableTracing: false, useHash: true }),
    CoreModule.forRoot()
  ],
  bootstrap: [BootstrapComponent],

  // --- 8< 変更箇所----
  declarations: [
    TestComponent
  ]
  // --- >8 ----
})
export class AppModule {}

サンプルコンポーネントをモジュールに追加した後、コンポーネントをテストする準備ができました。

2. テストコンポーネント用のユニットテストを追加する

テストファイルの拡張子は .spec.ts です。 リポジトリには、app.module.spec.tsというサンプルのspecファイルがあります。 このspecファイル名を test.component.spec.ts に変更して、内容を次のように調整してください。

import { ComponentFixture, TestBed } from '@angular/core/testing';
import { AppModule } from './app.module';
import { TestComponent } from './test.component';

describe('Test component test', () => {
  let fixture: ComponentFixture<TestComponent>;

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [AppModule]
    });
    fixture = TestBed.createComponent(TestComponent);
  });

  test('should be defined', () => {
    expect(fixture).toBeDefined();
  });
});

これは最初のテストファイルです。 Angularのテストモジュールを設定し、TestComponentが定義できるかどうかをチェックします。 Angularのテストサポートについては、Angular webサイトで詳細を確認できます。

テストを開始するには、コマンドラインから npm test を実行します。 これにより、package.jsonにある定義済みのスクリプトが実行され、Jestが起動します。 以下のようなテスト結果が表示されるはずです。

 PASS  ./test.component.spec.ts (32.071 s)
  Test component test
    ✓ should be defined (123 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        32.858 s

テストが「PASS」と表示されれば、すべてがうまくいき、最初のコンポーネントテストは成功したことになります。 これで、より詳細なテストケースを追加してコンポーネントが意図した通りに動作することを確認することができます。

3. スナップショットテストを使用して、コンポーネントテンプレートを検証する

このセクションでは、コンポーネントテンプレートを検証するための他の方法についての追加情報を提供します。

Karmaの代わりにJestを使用するのは、いわゆるスナップショットテストを使用するためのオプションが付属しているためです。 スナップショットテストは、すべての結果を定義することなく、テストの結果を検証することができます。 Jestの関数 toMatchSnapshot() は、初回実行時のテストのスナップショットを含むファイルを作成します。 test.component.spec.tsファイルに以下を追加して、スナップショットテストを使用する別のテストを作成し、 TestComponentのテンプレートを検証します。

test("should show a title tag", () => {
  expect(fixture.debugElement.nativeElement).toMatchSnapshot();
});

npm testを実行します。その結果、スナップショットが書き込まれたことが表示されます。

PASS  ./test.component.spec.ts
  Test component test
    ✓ should be defined (94 ms)
    ✓ should show a title tag (29 ms)

 › 1 snapshot written.
Snapshot Summary
 › 1 snapshot written from 1 test suite.

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   1 written, 1 total
Time:        5.154 s

このスナップショットは新しく作成されたフォルダ ./__snapshot__ にあり、確認することができます。 テンプレートが変更されると、テストは失敗するので、npm test -- -u でテストを上書きする必要があります。 この動作は test.component.ts ファイルでテンプレートを変更することでテストできます。

備考
このスナップショットをコードと一緒にコミットするのが一般的なやり方です。

結論

このチュートリアルでは、c8cycliコマンドを使用して新しくスキャフォールディングされたアプリケーションに、テストを追加する方法を紹介しました。 高度なスナップショットテストには、テンプレートをすばやく検証するためのオプションがあります。

コンテキストルートを使用して詳細ビューにタブを追加する

バージョン: 1009.0.18 | パッケージ: @c8y/cli, @c8y/apps, @c8y/ngx-components

デバイスやグループなどの詳細画面でユーザーに追加の情報を表示したい、というのは一般的なユースケースです。

このレシピでは、デバイス詳細画面に新しいタブを追加する方法を説明します。

カスタムタブでのデバイス情報

Angular向けWeb SDKでは、特定のコンテキストに対してのビューを提供するため、このような画面は ViewContext と呼ばれます。コンテキストビューにはいくつか種類があります(例えば、DeviceGroupUserApplicationTenant など)。ハッシュナビゲーションで特定の Route でナビゲートすることで、コンテキストビューにアクセスできます。例えば、apps/cockpit/#/device/1234 へアクセスした場合、アプリケーションは 1234 のIDを持つデバイスを解決しようとします。

詳細ビューは/info と呼ばれる別のルートから参照されている上記のスクリーンショットの 情報 タブにあるように、通常いくつかの Tabs を表示していますが、情報を表示するためにデバイスのコンテキストを再利用しています。

以下では、apps/cockpit/#/device/:id/hello からアクセスできるこのビューに新しいタブを追加する方法を紹介します。

1. サンプルアプリケーションを初期化する

はじめに、コンテキストルートをサポートするアプリケーションが必要です。 このために、c8cycliを使用して新しいコックピットアプリケーションを作成します。

c8ycli new my-cockpit cockpit  -a @c8y/apps@1009.0.18

次に、すべての依存関係をインストールする必要があります。新しいフォルダに移動して、npm installを実行します。

備考
c8y cli newコマンドには、スキャフォールディングに使用するパッケージを定義する-aフラグがあります。このようにして、スキャフォールディングとするアプリケーションのバージョンを定義することも可能です。例えば、以下の通りです。

  • c8ycli new my-cockpit cockpit -a @c8y/apps@1009.0.18 は、アプリケーションのバージョン10.9.0.18をスキャフォールディングにします。
  • c8ycli new my-cockpit cockpit -a @c8y/apps@latest は、アプリケーションの最新公式リリースをスキャフォールディングにします。-a フラグを付けずに使用する場合と同じです。
  • c8ycli new my-cockpit cockpit -a @c8y/apps@next は、アプリケーションの最新ベータリリースをスキャフォールディングにします。

2. 新しいROUTE_HOOK_ONCEを追加する

フックの概念により既存のコードに追加できます。この例では、device/:id という既存のルートにいわゆるチャイルドルート(Angularによる)を追加します。

これを達成するために、app.module.ts に以下のコードを追加します。

import { NgModule } from '@angular/core';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { RouterModule as NgRouterModule } from '@angular/router';
import { UpgradeModule as NgUpgradeModule } from '@angular/upgrade/static';
// ---- 8< 変更箇所 ----
import { CoreModule, RouterModule, HOOK_ONCE_ROUTE, ViewContext } from '@c8y/ngx-components';
// ---- >8 ----
import { DashboardUpgradeModule, UpgradeModule, HybridAppModule, UPGRADE_ROUTES} from '@c8y/ngx-components/upgrade';
import { AssetsNavigatorModule } from '@c8y/ngx-components/assets-navigator';
import { CockpitDashboardModule, ReportDashboardModule } from '@c8y/ngx-components/context-dashboard';
import { ReportsModule } from '@c8y/ngx-components/reports';
import { SensorPhoneModule } from '@c8y/ngx-components/sensor-phone';
import { BinaryFileDownloadModule } from '@c8y/ngx-components/binary-file-download';

@NgModule({
  imports: [
    // Upgrade module must be the first
    UpgradeModule,
    BrowserAnimationsModule,
    RouterModule.forRoot(),
    NgRouterModule.forRoot([...UPGRADE_ROUTES], { enableTracing: false, useHash: true }),
    CoreModule.forRoot(),
    AssetsNavigatorModule,
    ReportsModule,
    NgUpgradeModule,
    DashboardUpgradeModule,
    CockpitDashboardModule,
    SensorPhoneModule,
    ReportDashboardModule,
    BinaryFileDownloadModule
  ],

  // ---- 8< 追加箇所 ----
  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();
  }
}

上記の数字の説明は以下の通りです。

  1. マルチプロバイダーフックの HOOK_ROUTE_ONCE を提供します。これは現在のルートの設定を拡張するためにアプリケーションに指示します。
  2. ルートフックの定義に値を使用することを指定します。例えば、ルートを非同期に解決したい場合、クラスを使用することもできます。
  3. ルートのコンテキストを定義します。定義するには ViewContext enum を使用する必要があります。この例では、デバイスのコンテキストを拡張します。
  4. 表示されるパスです。コンテキストパスに追加されます。この例では、完全パスは以下になります。device/:id/hello
  5. パスがユーザーによって入力されたらどのコンポーネントを表示するかを定義します。
  6. タブがどのように表示されるかを定義するのは label かつ icon プロパティです。priority はどの位置に表示するべきかを定義します。
備考
HOOK_ONCE_ROUTE はAngular Routeの型を継承します。すべてのプロパティはここで再利用されます。

この一連の設定の後、ルートは登録されますが HelloComponent はまだ存在しないため、アプリケーションはコンパイルに失敗します。次のセクションで HelloComponent を作成します。

3. コンテキストデータを表示するコンポーネントを追加する

HelloComponentは、デバイスの詳細を表示したい場合があるでしょう。 これを行うには、それが開かれたコンテキストに関する情報が必要です。 コンテキストルートは、デバイスを前もって解決してくれます。 また、親ルートで直接アクセスすることができます。

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) {}
}

このコンポーネントは ActivatedRoute を注入し、その親データにアクセスします。 ここがキーとなるポイントです。親コンテキストルートはすでにデバイスのデータを解決しているので、このコンポーネントは常に現在のデバイスの詳細データを表示します。

これを app.module.tsentryComponents に追加することでアプリケーションをコンパイルすることができます。


import { NgModule } from '@angular/core';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { RouterModule as NgRouterModule } from '@angular/router';
import { UpgradeModule as NgUpgradeModule } from '@angular/upgrade/static';
import { CoreModule, RouterModule, HOOK_ONCE_ROUTE, ViewContext } from '@c8y/ngx-components';
import { DashboardUpgradeModule, UpgradeModule, HybridAppModule, UPGRADE_ROUTES} from '@c8y/ngx-components/upgrade';
import { AssetsNavigatorModule } from '@c8y/ngx-components/assets-navigator';
import { CockpitDashboardModule, ReportDashboardModule } from '@c8y/ngx-components/context-dashboard';
import { ReportsModule } from '@c8y/ngx-components/reports';
import { SensorPhoneModule } from '@c8y/ngx-components/sensor-phone';
import { BinaryFileDownloadModule } from '@c8y/ngx-components/binary-file-download';
// ---- 8< 追加箇所 ----
import { HelloComponent } from './hello.component';
// ---- >8 ----

@NgModule({
  imports: [
    // Upgrade module must be the first
    UpgradeModule,
    BrowserAnimationsModule,
    RouterModule.forRoot(),
    NgRouterModule.forRoot([...UPGRADE_ROUTES], { enableTracing: false, useHash: true }),
    CoreModule.forRoot(),
    AssetsNavigatorModule,
    ReportsModule,
    NgUpgradeModule,
    DashboardUpgradeModule,
    CockpitDashboardModule,
    SensorPhoneModule,
    ReportDashboardModule,
    BinaryFileDownloadModule
  ],

  // ---- 8< 追加箇所 ----
  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();
  }
}

npm start でアプリケーションを開始する場合、デバイスの詳細ビューへアクセスすると以下のように表示されます。

カスタムタブ付きのデバイス情報

これで、デバイスにタブが追加されました。 テナント、ユーザー、アプリケーションの詳細ビューでも同じことができます。

次は条件が満たされた場合のみ、このタブを表示する方法を学びましょう。

(付録)4.条件を満たした場合のみ、タブを表示する

場合によっては、特定の条件を満たした場合にのみ追加の情報を利用できることがあります。例えば、デバイスに位置情報のフラグメントが関連付けられている場合にのみ位置情報を表示することは意味があります。そのような条件を追加するには、コンテキストルートは Angularのガード概念 を継承します。

ガードを追加するには、単純にルート定義に canActivate プロパティを追加するだけです。

import { NgModule } from '@angular/core';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { RouterModule as NgRouterModule } from '@angular/router';
import { UpgradeModule as NgUpgradeModule } from '@angular/upgrade/static';
import { CoreModule, RouterModule, HOOK_ONCE_ROUTE, ViewContext } from '@c8y/ngx-components';
import { DashboardUpgradeModule, UpgradeModule, HybridAppModule, UPGRADE_ROUTES} from '@c8y/ngx-components/upgrade';
import { AssetsNavigatorModule } from '@c8y/ngx-components/assets-navigator';
import { CockpitDashboardModule, ReportDashboardModule } from '@c8y/ngx-components/context-dashboard';
import { ReportsModule } from '@c8y/ngx-components/reports';
import { SensorPhoneModule } from '@c8y/ngx-components/sensor-phone';
import { BinaryFileDownloadModule } from '@c8y/ngx-components/binary-file-download';
import { HelloComponent } from './hello.component';

// ---- 8< 追加箇所 ----
import { HelloGuard } from './hello.guard';
// ---- >8 ----

@NgModule({
  imports: [
    // Upgrade module must be the first
    UpgradeModule,
    BrowserAnimationsModule,
    RouterModule.forRoot(),
    NgRouterModule.forRoot([...UPGRADE_ROUTES], { enableTracing: false, useHash: true }),
    CoreModule.forRoot(),
    AssetsNavigatorModule,
    ReportsModule,
    NgUpgradeModule,
    DashboardUpgradeModule,
    CockpitDashboardModule,
    SensorPhoneModule,
    ReportDashboardModule,
    BinaryFileDownloadModule
  ],
  declarations: [HelloComponent],
  entryComponents: [HelloComponent],
  providers: [

    // ---- 8< 追加箇所 ----
    HelloGuard,
    // ---- >8 ----

    {
    provide: HOOK_ONCE_ROUTE,
    useValue: [{
      context: ViewContext.Device,
      path: 'hello',
      component: HelloComponent,
      label: 'hello',
      priority: 100,
      icon: 'rocket',

      // ---- 8< 追加箇所 ----
      canActivate: [HelloGuard]
      // ---- >8 ----

    }],
    multi: true
  }]
})
export class AppModule extends HybridAppModule {
  constructor(protected upgrade: NgUpgradeModule) {
    super();
  }
}

これで、特定の条件をチェックするガードを作成できます。trueに解決されると、タブが表示され、それ以外の場合は表示されません。

デバイスの特定のフラグメントをチェックするガードはこの 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;
  }
}

上記の数字は以下のように説明できます。

  1. これは、Angularルーターと整合していない唯一の部分です。コンテキストルートでは、CanActivate が2回呼び出されます。1回は親ルートがアクティブになり、1回は子ルートがアクティブになります。最初の呼び出しは、タブを表示する必要があるかどうかを確認し、2番目の呼び出しは、ユーザーがそのタブに移動できるかどうかを確認します。したがって、 ActivatedRouteSnapshotは両方の呼び出しで異なり、2番目のケースでは親から contextDataを解決する必要があります。
  2. acme_HelloWorld フラグメントがコンテキストに設定されているかを確認します。

APIに acme_HelloWorld フラグメントを付与してデバイスをポストすると、他のデバイスではなく作成したデバイスのみにHelloタブが表示されるでしょう。

結論

コンテキストルートは、既存のルートをさらに詳細に拡張するのに役立ちます。

同時に、コンテキストは一度のみ解決され、コンテキストが見つからない場合は親によって処理されるため、この概念によりアプリケーションの一貫性がが保たれます。

しかし、コンテキストルートの概念を抽象化し、独自に実装するデフォルトの方法は現時点では存在しません。 このコンセプトはAngularのルーティングに大きく基づいているので、自分で概念を実装することができます。

モジュールフェデレーションによるカスタムウィジェットプラグイン

バージョン: 1011.153.0 | パッケージ: @c8y/cli, @c8y/apps, @c8y/ngx-components

簡単なウィジェットの作成方法、その構造がどのように見えるか、アプリケーションへの追加方法については、ダッシュボードにカスタムウィジェットを追加する > ウィジェットコンポーネントを作成するをご覧ください。 次のチュートリアルでは、モジュールフェデレーションを使用してウィジェットをアプリケーションに追加する方法と、プロセスが前のチュートリアルとどのように異なるかを中心に説明します。

以下のソリューションは、Webpack 5で導入されたモジュールフェデレーション機能に完全に基づいています。 機能の詳細については、Webpack:モジュールフェデレーションをご覧ください。

1. ウィジェットプラグイン(例)を初期化する

以下に示すコマンドを使用して、サンプルプラグインを作成するための複数ステップのプロセスを開始します。

c8ycli new

プラグイン名を選択します(例:widget-plugin)。

? Enter the name of the project:  (my-application) widget-plugin

サンプルアプリケーションを作成するバージョンを選択します(例:10.13.72.0(next))。

? Which base version do you want to scaffold from? (Use arrow keys)
  1011.0.18 (latest)
❯ 1013.72.0 (next)
  1013.0.63
  1010.0.29
  1009.0.33
  1007.0.47
  other

プラグインのベースとなるアプリケーションテンプレートを選択します(例:widget-plugin)。

? Which base project do you want to scaffold from?
  administration
  application
  cockpit
  devicemanagement
  hybrid
  tutorial
❯ widget-plugin

数秒後、次のようなメッセージが表示されます。

Application created. Go into the folder "widget-plugin" and run npm install

アプリケーションフォルダに移動し、npm installを実行します。

アプリケーションフォルダは、以下の例のようになります。 このチュートリアルでは、最も重要なファイルは package.jsonREADME.md です。

app.module.spec.ts;
jest.config.js;
README.md;
tsconfig.spec.json;
app.module.ts;
package.json;
setup-jest.js;
widget/;
index.ts;
polyfills.ts;
tsconfig.json;

これで、モジュールフェデレーションを使用する最初のプラグインが作成されました。

2. カスタムウィジェットを作成時のアプローチの違い

シンプルなウィジェットとモジュールフェデレーションガイドラインに従ってビルドされたウィジェット間には、いくつかの違いがあります。

最大の違いは package.json ファイルで、isPackage, package, exports などのフィールドが配置されています。 以下のリストは、フィールドとその役割を示しています。

備考
プラグインを作成する場合、カスタムモジュールはこのアプローチの中核になります。エクスポートされたモジュールは、シェルと呼ばれるアプリケーションとプラグインをリンクするエントリポイントとして扱われます。既製の機能を含む必要があるいくつかのモジュールを作成して、エクスポートできます。

さらに、これらのモジュールは、遅延ロードモジュールのように動作します。一つの大きなパッケージとして事前に読み込まれるのではなく、オンデマンドで読み込まれる小さなパッケージのコレクションのようなものです。 各モジュールは、HOOKの概念によって追加機能を拡張することができます。詳しくは既存のアプリケーションを拡張しフックを使用するをご覧ください。例えば、プラグインはHOOK_NAVIGATOR_NODESを使用してナビゲーションメニューに別のエントリを追加することができます。詳細はナビゲーターノードを追加をご覧ください。

また、ローカル開発サーバーの起動方法にも違いがあり、サーバーの役割については以下のステップをご覧ください。

3. ローカルサーバー、デバッグとデプロイメント

ローカルサーバー

新しいプラグインの作成プロセスを容易にするため、ローカルサーバーのコマンドに、すべてのリクエストをシェルアプリケーション「コックピット」に代理させる新しいフラグが追加されました。

npm installを実行し、ローカルサーバーを起動します。

npm start -- --shell cockpit

次のような出力が表示されます。

Shell application: cockpit
http://localhost:9000/apps/cockpit/index.html?remotes=%7B%22widget-plugin%22%3A%5B%22WidgetPluginModule%22%5D%7D

リンク先では、コックピットのログイン画面に転送されます。 ログイン後、ウィジェットの追加画面で、widget-pluginをダッシュボードに追加します。

ウィジェットの追加

ウィジェットの編集作業の残りの部分は、通常のウィジェットの編集作業と同じです。変更内容を確認するには、ブラウザを更新してください。

デバッグ

通常のウィジェットとモジュールフェデレーション用に変更されたウィジェットのpackage.jsonファイルのもう一つの違いは、remoteフィールドです。(以下の例をご覧ください。)

...
"remotes": {
  "widget-plugin": [     // contextPath
    "WidgetPluginModule" // module class name
  ]
}
...
備考
remotesフィールドは、モジュールをインポートするために使用されます。モジュールを正しくインポートするには、プラグインのコンテキストパス (package.jsoncontextPath フィールド) に続いて、モジュールのクラス名を指定します。

プラグインは、remotesというフィールドを介してそれ自体をインポートします。 エクスポートされたモジュールの正確性を検証する最初のステップとしてお勧めします。これにより、アプリケーションのデバッグが容易になります。 独自のモジュールをインポートした後、npm startを実行して、ローカルサーバが起動するかどうかを確認します。

後でプラグインを確認するために、npm start -- --shell cockpitを使用して、さまざまなシェルアプリケーションでローカルに制御することをお勧めします。

デプロイ

ウィジェットのアップロードは、通常のウィジェットと同じです。 以下のコマンドを順番に実行してください。

npm run build

そして、

npm run deploy

コンソールのプロンプトに従って、アプリケーションをテナントにデプロイします。

4. デプロイされたウィジェットをシェルアプリケーションに追加する

現在、モジュールフェデレーションに関連するビューとロジックは、ベータフラグの後ろに隠されています。 アップロードされたウィジェット プラグインをコックピットアプリケーションのダッシュボードに追加するには、以下のステップを実行します。

これで、管理アプリケーション > エコシステム > アプリケーション > パッケージパッケージ タブにアクセスできるようになり、プラグインの詳細を確認できます。

これで、カスタムウィジェットは、コックピットアプリケーションのバージョンで利用できるようになりました。 ウィジェットの追加のリストに、新しく追加されたウィジェットが利用可能なダッシュボードに移動します。

widget-pluginは、管理アプリケーションの中からインストールされました。これが、ウィジェットに関する通常のアプローチと新しいアプローチとの主な違いです。 モジュール フェデレーションでは、アプリケーションが実行中(ランタイム)に新しい機能を追加することができますが、古いアプローチでは、アプリケーションがビルド(コンパイル時)にしか新しい機能を追加することができませんでした。

既存のアプリケーションを拡張しフックを使用する

バージョン: 1009.0.18 | パッケージ: @c8y/cli, @c8y/apps, @c8y/ngx-components

コックピットやデバイス管理のような既存のアプリケーションを拡張することは、一般的なユースケースです。

カスタムルートでコックピットを拡張する方法、ナビゲータにこのルートを追加する方法を順番に説明していきます。 まず、何をハイブリッド アプリケーションと呼んでいるのか、@c8y/apps npmパッケージが何を含んでいるのか、という背景を紹介します。

背景

デフォルトのアプリケーションはThings Cloudに同梱されている3つのアプリケーションで構成されています。これらのアプリケーションは何年も開発が続けられた結果、コードは主にAngularJSベースとなっています。現在、すべての組み込みアプリケーションではAngularを使用しているため、双方のフレームワークを提供する方法が必要でした。 @c8y/cli により、2つのフレームワークを共存させるデフォルトアプリケーションをスキャフォールドすることが可能になりました。これは、AngularとAngularJSのアプリケーションを同時に提供するためにAngularのアップグレード機能を用いています。これにより、新機能はすべてのAngular JS プラグインを統合しながら、Angular で新しい機能を開発できます。これを私たちは ハイブリッド モードと呼んでいます。

しかし、ハイブリッドモードにはいくつか制限があります。このレシピの後半で説明します。これらの制限により、純粋で 空のスターターAngularアプリケーションを提供することにしました。これはAngularJSのプラグインを統合する可能性がありません。このAngularのみのアプリケーションは以下の2つを含みます。

Web SDKを使い始めるには、全部で3つの可能性があります。

どれを選択するかは、ビルドするアプリケーションに大きく依存します。 例えば、プラットフォームのルックアンドフィールに従ったアプリケーションが必要であるが、マテリアル フレームワークなど特定のシナリオに特別な依存関係を使用したい場合は、純粋な Angular CLI ソリューションを使用するのが最適です。

最も一般的なユースケースは、ハイブリッドアプリケーションの拡張で、このレシピではこれを取り上げます。 まず、このアプローチの限界を見て、なぜこの概念がそのように設計されているのかを理解しましょう。

ハイブリッドモードの制限

ハイブリッドアプリケーションを実行する時、AngularとAngularJSは並行して実行されるようにする必要があるため、いくつかの制限があります。

制限について理解したところで最初のアプリケーションを拡張し、拡張用フックを開発することができます。これを行うには、ハイブリッドアプリケーションをスキャフォールディングにしておく必要があります。 c8y/appsは、デフォルトのアプリケーションとその最小限のセットアップを含むパッケージです。 c8yclinew コマンドでアプリケーションを初期化するたびにこのパッケージを使用します。 次のセクションでは、スキャフォールディングプロセスとハイブリッドアプリケーションを拡張する方法について順を追って説明します。

1. サンプルアプリケーションを初期化する

まずはじめに、アプリケーションが必要です。c8ycli を利用して新しくコックピットアプリケーションを作成します。

c8ycli new my-cockpit cockpit -a @c8y/apps@1009.0.18

次に、依存関係をインストールします。新しく作成されたフォルダに移動し npm install を実行してください。

備考
c8y cli newコマンドには、スキャフォールディングに使用するパッケージを定義する-aフラグがあります。このようにして、スキャフォールディングとするアプリケーションのバージョンを定義することも可能です。例えば、以下の通りです。

  • c8ycli new my-cockpit cockpit -a @c8y/apps@1004.11.0 は、アプリケーションのバージョン10.9.0.18をスキャフォールディングにします。
  • c8ycli new my-cockpit cockpit -a @c8y/apps@latest は、アプリケーションの最新公式リリースをスキャフォールディングにします。-a フラグを付けずに使用する場合と同じです。
  • c8ycli new my-cockpit cockpit -a @c8y/apps@next は、アプリケーションの最新ベータリリースをスキャフォールディングにします。

2. ルートにカスタムコンポーネントをバインドする

ルートはAngularと同じ方法で追加することができます。 唯一の違いは、ハイブリッドの制限のため、UPGRADE_ROUTESの前に定義する必要があることです。 プロジェクト内に以下の内容で hello.component.ts ファイルを作成します。

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

@Component({
  selector: "app-hello",
  template: `
    <c8y-title>Hello</c8y-title>
    World
  `,
})
export class HelloComponent {
  constructor() {}
}

これは、基本的なコンポーネントです。 テンプレートだけは、タイトルを表示するために「コンテンツ投影」という特別な機能を使用しています。 コンテンツ投影とは、コンテンツを定義されている場所以外の場所に表示するために使用されるAngularの概念です。 どのコンポーネントがコンテンツ投影をサポートしているかについては、コンポーネントライブラリ(ngx) ドキュメントをご覧ください。

以下の方法で app.module.ts を変更することによりルートにカスタムコンポーネントをバインドできるようになります。

import {NgModule} from "@angular/core";
import {BrowserAnimationsModule} from "@angular/platform-browser/animations";
import {RouterModule as NgRouterModule} from "@angular/router";
import {UpgradeModule as NgUpgradeModule} from "@angular/upgrade/static";
import {CoreModule, RouterModule} from "@c8y/ngx-components";
import {DashboardUpgradeModule, UpgradeModule, HybridAppModule, UPGRADE_ROUTES} from "@c8y/ngx-components/upgrade";
import {AssetsNavigatorModule} from "@c8y/ngx-components/assets-navigator";
import {CockpitDashboardModule, ReportDashboardModule} from "@c8y/ngx-components/context-dashboard";
import {ReportsModule} from "@c8y/ngx-components/reports";
import {SensorPhoneModule} from "@c8y/ngx-components/sensor-phone";
import {BinaryFileDownloadModule} from "@c8y/ngx-components/binary-file-download";

// --- 8< 変更箇所----
import { HelloComponent } from './hello.component';    // 1
// --- >8 ----

@NgModule({

  // --- 8< 変更箇所----
  declarations: [HelloComponent],                      // 2
  // --- >8 ----

  imports: [
    // Upgrade module must be the first
    UpgradeModule,
    BrowserAnimationsModule,
    RouterModule.forRoot(),
    NgRouterModule.forRoot(
      // --- 8< 変更箇所----
      { path: 'hello', component: HelloComponent},     // 3
      // --- >8 ----

      ...UPGRADE_ROUTES
    ], { enableTracing: false, useHash: true }),
    CoreModule.forRoot(),
    AssetsNavigatorModule,
    ReportsModule,
    NgUpgradeModule,
    DashboardUpgradeModule,
    CockpitDashboardModule,
    SensorPhoneModule,
    ReportDashboardModule,
    BinaryFileDownloadModule,
  ]
})
export class AppModule extends HybridAppModule {
  constructor(protected upgrade: NgUpgradeModule) {
    super();
  }
}

ここでの変更は単純です。まずはじめに、(1)コンポーネントをインポートします。(2)次に、そのコンポーネントを宣言部へ追加します。(3)最後にこの場合では hello というパスへバインドする必要があります。「c8ycli server」コマンドでアプリケーションを起動し、正しいハッシュを追加して URLhttp://localhost:9000/apps/cockpit/#/hello)に移動すると(、カスタム コンポーネントが表示されます。次のステップでは、左ナビゲータのコンポーネントを追加します。

3. ナビゲーターノードをフックする

新しく作成した hello.component.ts にユーザーが移動できるようにするために、左側のナビゲーターにナビゲーションを追加します。 そのためには、いわゆるフックを使用します。

フックは特定のインジェクション トークンにバインドされている単なるプロバイダーです。 複数のプロバイダーを追加できるようにするには、Angularのマルチプロバイダーの概念を使います。 詳しく説明することは、このチュートリアルの範囲を超えています。 angular.ioドキュメントをご覧ください。

インジェクショントークンは @c8y/ngx-components パッケージをインポートすることで受け取ることができます。 これらはすべて HOOK_ で始まり、その後に何に使われるかが続きます。 例えば、ナビゲータノードを追加するには 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< 変更箇所----
import {CoreModule, RouterModule, HOOK_NAVIGATOR_NODES, NavigatorNode} from "@c8y/ngx-components";
// --- >8 ----
import {DashboardUpgradeModule, UpgradeModule, HybridAppModule, UPGRADE_ROUTES} from "@c8y/ngx-components/upgrade";
import {AssetsNavigatorModule} from "@c8y/ngx-components/assets-navigator";
import {CockpitDashboardModule, ReportDashboardModule} from "@c8y/ngx-components/context-dashboard";
import {ReportsModule} from "@c8y/ngx-components/reports";
import {SensorPhoneModule} from "@c8y/ngx-components/sensor-phone";
import {BinaryFileDownloadModule} from "@c8y/ngx-components/binary-file-download";

@NgModule({
  declarations: [HelloComponent],

  imports: [
    // Upgrade module must be the first
    UpgradeModule,
    BrowserAnimationsModule,
    RouterModule.forRoot(),
    NgRouterModule.forRoot(
      [
        {path: "hello", component: HelloComponent}, // 3
        ...UPGRADE_ROUTES,
      ],
      {
        enableTracing: false,
        useHash: true,
      }
    ),
    CoreModule.forRoot(),
    AssetsNavigatorModule,
    ReportsModule,
    NgUpgradeModule,
    DashboardUpgradeModule,
    CockpitDashboardModule,
    SensorPhoneModule,
    ReportDashboardModule,
    BinaryFileDownloadModule,
  ],

  // --- 8< 変更箇所----
  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();
  }
}

上記の数字の説明は以下の通りです。

  1. HOOK_NAVIGATOR_NODES を提供します。
  2. 特定のvalueを使います。複雑なケースでは useClassget() を定義することもできます。
  3. ナビゲータ ノードがどのように見えるかを定義します。
  4. フックのほとんどはTypeScriptで先行入力できるインターフェースを持っています。
  5. マルチプロバイダーフラグは複数のフックが存在する可能性があることをAngularに伝えます。

この拡張フックを実装すると、ナビゲータに次のような新しいエントリーが表示されます。

拡張したコックピットアプリケーション

NavigatorNodeインターフェースのプロパティ priority は、ノードの表示順を定義することに注意してください。。

これで hello.component.ts は、コックピットアプリケーション内の空白のキャンバスのようになりました。 コックピットの所定の機能に影響を与えることなく、必要な機能をすべて実装することができます。

まとめ

ハイブリッドアプリケーションは、Angular JSとAngularの統合による制限があります。 しかし、フックの概念とカスタムルートによって、既存のハイブリッドアプリケーションに追加できるようになります。 これらは、組み込みアプリケーションを拡張するための強力なツールです。 追加機能が必要な場合は、純粋なAngularアプリケーションの方が適していることもあります。 これはユースケースによって異なります。

ログインページと認証の削除

バージョン: 1009.0.18 | パッケージ: @c8y/cli, @c8y/apps, @c8y/ngx-components

備考
この手法では、ユーザー名とパスワードが公開されます。このユーザーが重要なデータにアクセスできないことを確認してください。

デフォルトのアプリケーションでは、ページにアクセスする前に必ずログインページに移動して認証を行います。 このレシピでは、ログイン認証を解除してアプリケーションを直接使用する方法を説明します。

背景

すべての認証を削除することはできません。 これを回避するためには、アプリケーションがリクエスト時に読み取るデフォルトの認証情報を渡す必要があります。目標は、アプリケーションが認証されていないためにログインページをリクエストする前に、デフォルトの認証情報を使ってログインをトリガーすることです。

ログイン機能は @c8y/ngx-components パッケージの CoreModule に含まれており、Angular がアプリケーションをブートストラップする際に読み込まれます。その前に、デフォルトの認証情報をAPIに渡す必要があります。その結果、Angularが最初のページを読み込むとき、ユーザーはすでに認証されており、ログインページはスキップされることになります。

1. 新しいアプリケーションを初期化する

出発点として、アプリケーションが必要です。そのために、c8cycliを使用して新しいアプリケーションを作成します。

c8ycli new my-cockpit cockpit -a @c8y/apps@1009.0.18

これにより、コックピットアプリケーションの完全なコピーである新しいアプリケーションが作成されます。 次に、すべての依存関係をインストールする必要があります。 新しいフォルダに移動して、npm installを実行します。

備考
c8ycli new コマンドには -a フラグがあり、スキャフォールディングするパッケージを定義することができます。このようにして、スキャフォールディングするアプリケーションのバージョンを定義することも可能です。例えば、以下の通りです。

  • c8cycli new my-cockpit cockpit -a @c8y/apps@1009.0.18 は、アプリケーションのバージョン1009.0.18 をスキャフォールディングにします。
  • c8ycli new my-cockpit cockpit -a @c8y/apps@latestは、アプリケーションの最新公式リリースをスキャフォールディングにします。-a フラグを付けずに使用する場合と同じです。
  • c8ycli new my-cockpit cockpit -a @c8y/apps@nextは、アプリケーションの最新ベータリリースをスキャフォールディングにします。

2. デフォルト認証のロジックを追加する

まず、Angularがカスタムアプリケーションをブートストラップする前に、デフォルトの認証を追加する必要があります。 そのため、新しく作成したカスタムコックピットアプリケーションの app.module.ts に、ログイン前にトリガーされる新しいプロバイダーを追加する必要があります。 そのためには、Angularのインジェクション トークンAPP_INITIALIZERを使用します。 このトークンによって、新機能が実行されるまでアプリケーションが初期化されないようになります。

providers: [
  {
    provide: APP_INITIALIZER,
    useFactory: initApp,
    multi: true,
    deps: [LoginService],
  },
];

ファクトリー関数 initApp を使用し、ここでデフォルトの認証を定義して送信します。 APIに認証情報を送信するには、@c8y/ngx-componentsの一部である LoginService (http://resources.cumulocity.com/documentation/websdk/ngx-components/injectables/LoginService.html) に依存関係を追加してください。

export function initApp(loginService: LoginService) {
  return () => {
    const credentials = {
      tenant: "tenantName",
      user: "admin",
      password: "C8Yadmin",
    };
    const basicAuth = loginService.useBasicAuth(credentials);

    return loginService.login(basicAuth, credentials);
  };
}

デフォルトの認証情報でログインするには、サービスからログイン機能を呼び出して、認証方法とデフォルトの認証情報を渡す必要があります。

これでレシピは完成し、認証はバックグラウンドで行われるようになります。

// --- 8< 変更箇所 ----
import { APP_INITIALIZER, NgModule } from "@angular/core";
// --- >8 ----
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
import { RouterModule as NgRouterModule } from "@angular/router";
import { UpgradeModule as NgUpgradeModule } from "@angular/upgrade/static";
// --- 8< 変更箇所 ----
import { CoreModule, LoginService, RouterModule } from "@c8y/ngx-components";
// --- >8 ----
import { DashboardUpgradeModule, UpgradeModule, HybridAppModule, UPGRADE_ROUTES} from '@c8y/ngx-components/upgrade';
import { AssetsNavigatorModule } from '@c8y/ngx-components/assets-navigator';
import { CockpitDashboardModule, ReportDashboardModule } from '@c8y/ngx-components/context-dashboard';
import { ReportsModule } from '@c8y/ngx-components/reports';
import { SensorPhoneModule } from '@c8y/ngx-components/sensor-phone';
import { BinaryFileDownloadModule } from '@c8y/ngx-components/binary-file-download';

@NgModule({
  imports: [
    // Upgrade module must be the first
    UpgradeModule,
    BrowserAnimationsModule,
    RouterModule.forRoot(),
    NgRouterModule.forRoot([...UPGRADE_ROUTES], { enableTracing: false, useHash: true }),
    CoreModule.forRoot(),
    AssetsNavigatorModule,
    ReportsModule,
    NgUpgradeModule,
    DashboardUpgradeModule,
    CockpitDashboardModule,
    SensorPhoneModule,
    ReportDashboardModule,
    BinaryFileDownloadModule
  ],
  providers: [
    {
      provide: APP_INITIALIZER,
      useFactory: initApp,
      multi: true,
      deps: [LoginService],
    },
  ],
})
export class AppModule extends HybridAppModule {
  constructor(protected upgrade: NgUpgradeModule) {
    super();
  }
}

export function initApp(loginService: LoginService) {
  return () => {
    const credentials = {
      tenant: "tenantName",
      user: "admin",
      password: "C8Yadmin",
    };
    const basicAuth = loginService.useBasicAuth(credentials);

    return loginService.login(basicAuth, credentials);
  };
}

結論

このチュートリアルでは、カスタムアプリケーションを開発する際に、認証を解除する方法を説明しています。 この種の手法は、アプリケーションが機密情報を持っていない場合に使用することができます。 データ保護が必要な場合は、この手法を避ける必要があります。

カスタムマイクロサービスからデータをリクエストする

バージョン: 1009.0.18 | パッケージ: @c8y/cli, @c8y/apps, @c8y/ngx-components

場合によって、UIはカスタムマイクロサービスからのデータを必要とすることがあります。そのデータは、Angular のHttpModule などの任意の HTTP クライアントでいつでも読み取ることができますが、すぐに使用できる認証が必要な場合があります。

このレシピでは @c8y/client を使用してカスタムエンドポイントにアクセスする方法と自動的に認証する方法を紹介します。まず、Angularアプリケーションにおいてクライアントがどのように動作するのか説明するために基本を見ていきましょう。

基本: クライアントがどのように動作するか

どのように @c8y/client が動作するのか、利点は何かを見ていきましょう。

クライアントはブラウザ(もしくはnode.js)からプラットフォームへのHTTPリクエストを処理します。ほとんどのプラットフォームAPIはセキュアなため、認証情報を設定できます。

現在、認証方法には2つのオプションがあります。

新しいクライアントインスタンスで認証方法を設定する際に、どの認証を使用するかを定義することができます。 クライアントは、プラットフォームのすべての共通エンドポイントを持つオブジェクトを返します。 例えば、次の例では BasicAuth を介してインベントリからデータをリクエストします。

const client = new Client(new BasicAuth({
  user: 'admin',
  password: 'password',
  tenant: 'acme'
}), 'https://acme.je1.thingscloud.ntt.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)
}

事前設定された各エンドポイントは、data含むオブジェクト、オプションの paging オブジェクトおよび res オブジェクトを返します。レスポンスは、最新のすべてのブラウザに実装されている次世代の XHR API である フェッチによって提供されます(IE11 ではポリフィル可能)。

つまり、@c8y/client はJavaScript のヘルパー ライブラリであり、フェッチを抽象化して、簡単な認証と共通プラットフォーム API への直接アクセスできるようにします。

次のセクションでは、Angular の依存性注入(DI)モデルを利用して、Angular アプリケーションでその概念を使用する方法を説明します。

基本: @c8y/clientとAngularアプリケーションの相互作用

@c8y/ngx-components はアプリケーションの起動を可能にするAngularのコンポーネントです。例えばコックピット、管理、デバイス管理などの基本アプリケーションでログイン画面を表示するのによく使用されます。新しくAngularベースのアプリケーションを起動する時は常に @c8y/client@c8y/ngx-components が含まれています。さらに、ngx-componentsには @c8y/ngx-components/api と呼ばれ、DataModule をエクスポートするサブパッケージがあります。このモジュールはすでに、すべての一般的なエンドポイント サービスをインポートしているため、Angular の標準的な依存性注入を使用してデータにアクセスできます。

上記のAngularアプリケーションの例は、以下のようになります。

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. クライアントから目的サービスをインポートする。
  2. 目的のサービスを使用するために依存性注入します。AngularのDIの概念は DataModule がメインモジュールに正しくインポートされた場合に。必要なすべての依存関係を処理します。
  3. ここでデータをリクエストできるようになります。認証はすでに処理されています。コンストラクタで直接使用するか、またはEntryComponentとして使用すると、コンポーネントがログイン モジュールの前に読み込まれるため、リクエストは不正に失敗する可能性があります。これを回避するには、AppStateService を注入できます。これにより、ユーザーがログインするとすぐに更新する currentUser ovservableを提供します。

ここでは一般的なエンドポイントの使い方の概要を説明しました。 次のレシピではカスタムエンドポイントの追加方法を紹介します。

1. サンプルアプリケーションを初期化する

はじめに、ダッシュボードを表示するアプリケーションが必要です。 このため、c8ycli を使用して新しいコックピットアプリケーションを作成します。

c8ycli new my-cockpit cockpit -a @c8y/apps@1009.0.18

次に、すべての依存関係をインストールする必要があります。 新しいフォルダに切り替えて、npm installを実行します。

備考
c8ycli newコマンドには、スキャフォールディングに使用するパッケージを定義する -a フラグがあります。このようにして、スキャフォールディングとするアプリケーションのバージョンを定義することも可能です。例えば、以下の通りです。

  • c8cycli new my-cockpit cockpit -a @c8y/apps@1009.0.18 は、アプリケーションのバージョン 1009.0.18 をスキャフォールディングにします。
  • c8ycli new my-cockpit cockpit -a @c8y/apps@latest は、アプリケーションの最新公式リリースをスキャフォールディングにします。-aフラグを付けずに使用する場合と同じです。
  • c8ycli new my-cockpit cockpit -a @c8y/apps@next は、アプリケーションの最新ベータリリースをスキャフォールディングにします。

2. フェッチで直接データをリクエストする

HTTP GET でエンドポイント service/acme からデータにアクセスしたい場合、認証付きでこれを実現する最も簡単な方法はクライアントの fetch の実装を再利用することです。 アプリケーションにファイルを追加し、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. クライアントで使用されている fetch を抽象化した FetchClient を注入します。
  2. fetchClient.fetch でデータをリクエストします。この関数は Fetch API と同じです。ただし、メソッドやデータを受け入れる 2 番目のパラメータとして、認証をプラットフォームに追加する点が異なります。
  3. データをパースし、コントローラに設定してテンプレートに表示します。

次に、コンポーネントを表示できるアプリケーションへのルートを追加します。次のコードは、app.module.ts でルートの追加を行います。詳細については、他のチュートリアルもご覧ください。

import { NgModule } from '@angular/core';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { RouterModule as NgRouterModule } from '@angular/router';
import { UpgradeModule as NgUpgradeModule } from '@angular/upgrade/static';
import { CoreModule, RouterModule } from '@c8y/ngx-components';
import { DashboardUpgradeModule, UpgradeModule, HybridAppModule, UPGRADE_ROUTES} from '@c8y/ngx-components/upgrade';
import { AssetsNavigatorModule } from '@c8y/ngx-components/assets-navigator';
import { CockpitDashboardModule, ReportDashboardModule } from '@c8y/ngx-components/context-dashboard';
import { ReportsModule } from '@c8y/ngx-components/reports';
import { SensorPhoneModule } from '@c8y/ngx-components/sensor-phone';
import { BinaryFileDownloadModule } from '@c8y/ngx-components/binary-file-download';

// ---- 8< 追加箇所  ----
import { AcmeComponent } from './acme.component';
// ---- >8 ----

@NgModule({
  imports: [
    // Upgrade module must be the first
    UpgradeModule,
    BrowserAnimationsModule,
    RouterModule.forRoot(),
    // ---- 8< 追加箇所  ----
    NgRouterModule.forRoot([
      { path: 'acme', component: AcmeComponent },
      ...UPGRADE_ROUTES,
    ], { enableTracing: false, useHash: true }),
    // ---- >8 ----
    CoreModule.forRoot(),
    AssetsNavigatorModule,
    ReportsModule,
    NgUpgradeModule,
    DashboardUpgradeModule,
    CockpitDashboardModule,
    SensorPhoneModule,
    ReportDashboardModule,
    BinaryFileDownloadModule
  ],

  // ---- 8< 追加箇所  ----
  declarations: [
    AcmeComponent
  ]
  // ---- >8 ----

})
export class AppModule extends HybridAppModule {
  constructor(protected upgrade: NgUpgradeModule) {
    super();
  }
}

3. アプリケーションを実行し検証する

c8ycli server でアプリケーションを実行し、ブラウザでモジュールで定義されたパスhttp://localhost:9000/apps/cockpit/#/acme を参照すると、次のように表示されます。

カスタムのクライアントサービス

このコンテキストパスで実行しているマイクロサービスが存在しないため、リクエストは失敗します。しかし、開発者ツールでわかるように、リクエストには authorization の Cookie が付属されています。つまり、マイクロサービスが存在すればリクエストは成功し、データは表示されます。

4.付録:Service.tsの抽象化を記述する

上記の例では、カスタムマイクロサービスへ直接アクセスして、に基本的な fetch 抽象化を使用しました。クライアントの共通サービスについても、同様の単純さを実現したい場合があります。これは内部で URL と JSON 解析を処理します。そのためには、@c8y/client によって返された Service クラスを拡張し、必要なメソッドまたはプロパティを上書きします。

acme.service.ts という新しいファイルを作成し、acme マイクロサービスの例で実践してみましょう。

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

上記の数字の説明は、以下の通りです。

  1. サービスを拡張することにより、@c8y/client のあるすべての共通サービスと同じ機能が得られます。この場合、ジェネリック型は例を簡単にするために any に設定しています。このサービスで送信するデータを反映したインターフェースを作成し、anyをこのインターフェースに置き換えるのが一般的なパターンです。
  2. URLはこのサービスの主なエントリーポイントです。このパターンは常に <<url>>/<<baseUrl>>/<<listUrl>>/<id> になります。マイクロサービスが異なる構造を採用する場合、Serviceクラスの getUrl メソッドを上書きできます。
  3. コンストラクタは依存性注入によってインポートされた FetchClient が必要です。また、super() を介して拡張した Service クラスに渡す必要があります。リアルタイムをサポートするためにエンドポイントが必要な場合、ここに RealTime 抽象化を注入し、渡さなければいけません。
  4. detail() または list() の実装を上書きできます。superメソッドのみを呼び出したり、superコールの結果を変更、または独自の実装を記述したりできます。どちらを選択するかは、マイクロサービスの実装の詳細によって異なります。

これで acme.component.tsAcmeService を再利用できます。

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

(1)単純にサービスを注入し、(2)サービスで直接 list のリクエストを実行します。(3)サービスはエラーを投げるため、try/catchで呼び出しをラッピングし、エラー時には addServerFailure メソッドに例外を追加することで alert を表示します。

まとめ

上記の例では、クライアント経由でカスタムマイクロサービスにアクセスする方法を示しています。AngularのHttpModuleのようなよく知られたクライアント抽象化を使う方が簡単かもしれませんが、@c8y/clientを再利用することで、すぐに認証を得ることができます。 このソリューションは、基になる変更を気にせずに @c8y/client を更新できるので、変更に対して堅牢なソリューションです。