diff --git a/README.md b/README.md
index bb5d664..263c437 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,5 @@
-# cumulocity-thinedge-web-app
\ No newline at end of file
+# cumulocity-thinedge-web-app
+
+ToDo before run:
+- change URL in package.json
+- run 'npm init'
diff --git a/app.module.ts b/app.module.ts
new file mode 100755
index 0000000..9f68262
--- /dev/null
+++ b/app.module.ts
@@ -0,0 +1,95 @@
+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 { AssetsNavigatorModule } from '@c8y/ngx-components/assets-navigator';
+import { DeviceConfigurationModule } from '@c8y/ngx-components/device-configuration';
+import { DeviceListsModule } from '@c8y/ngx-components/device-lists';
+import { ImpactProtocolModule } from '@c8y/ngx-components/protocol-impact';
+import { OpcuaProtocolModule } from '@c8y/ngx-components/protocol-opcua';
+import { RepositoryModule } from '@c8y/ngx-components/repository';
+import { TrustedCertificatesModule } from '@c8y/ngx-components/trusted-certificates';
+import { BsDropdownModule } from 'ngx-bootstrap/dropdown';
+
+import { DockerComponent } from './components/docker-comp/docker.component';
+import { AnalyticsComponent } from './components/analytics/analytics.component';
+import { AnalyticsEPLComponent } from './components/analytics/apamaepl/apamaepl.component';
+import { AnalyticsBuilderComponent } from './components/analytics/analyticsbuilder/apamaAB.component';
+import { DockerGuard } from './components/helper/docker.guard';
+import { AnalyticsGuard } from './components/helper/analytics.guard';
+
+import {
+ DashboardUpgradeModule,
+ HybridAppModule,
+ UpgradeModule,
+ UPGRADE_ROUTES
+} from '@c8y/ngx-components/upgrade';
+
+@NgModule({
+ imports: [
+ // Upgrade module must be the first
+ UpgradeModule,
+ BrowserAnimationsModule,
+ RouterModule.forRoot(),
+ NgRouterModule.forRoot([
+ ...UPGRADE_ROUTES
+ ], { enableTracing: false, useHash: true }),
+ CoreModule.forRoot(),
+ AssetsNavigatorModule.config({
+ smartGroups: true
+ }),
+ OpcuaProtocolModule,
+ ImpactProtocolModule,
+ TrustedCertificatesModule,
+ DeviceConfigurationModule,
+ DeviceListsModule,
+ NgUpgradeModule,
+ DashboardUpgradeModule,
+ RepositoryModule,
+ BsDropdownModule
+ ],
+ declarations: [
+ AnalyticsComponent,
+ DockerComponent,
+ AnalyticsEPLComponent,
+ AnalyticsBuilderComponent
+ ],
+ entryComponents: [
+ AnalyticsComponent,
+ DockerComponent
+ ],
+
+ providers: [
+ DockerGuard,
+ AnalyticsGuard,
+ {
+ provide: HOOK_ONCE_ROUTE, // 1.
+ useValue: [{ // 2.
+ context: ViewContext.Device, // 3.
+ path: 'analytics', // 4.
+ component: AnalyticsComponent, // 5.
+ label: 'Analytics', // 6.
+ priority: 100,
+ icon: 'diamond',
+ canActivate: [AnalyticsGuard]
+ },{
+ context: ViewContext.Device, // 3.
+ path: 'docker', // 4.
+ component: DockerComponent, // 5.
+ label: 'Docker', // 6.
+ priority: 100,
+ icon: 'cubes',
+ canActivate: [DockerGuard]
+ }],
+ multi: true
+ }]
+
+
+})
+export class AppModule extends HybridAppModule {
+ constructor(protected upgrade: NgUpgradeModule) {
+ super();
+ }
+}
diff --git a/components/analytics/analytics.component.css b/components/analytics/analytics.component.css
new file mode 100755
index 0000000..66dbd24
--- /dev/null
+++ b/components/analytics/analytics.component.css
@@ -0,0 +1,12 @@
+
+.card{
+ width: 28em;
+}
+
+.card:hover {
+ box-shadow: 0 5px 15px rgba(0,0,0,0.3);
+ }
+
+.card-title{
+ text-overflow: ellipsis;
+}
diff --git a/components/analytics/analytics.component.html b/components/analytics/analytics.component.html
new file mode 100755
index 0000000..68ea3f0
--- /dev/null
+++ b/components/analytics/analytics.component.html
@@ -0,0 +1,23 @@
+
+
+
+ Apama EPL Apps
+
+
+
+
+
+
+
+
+ Apama Analytics Builder
+
+
+
+
+
+
diff --git a/components/analytics/analytics.component.ts b/components/analytics/analytics.component.ts
new file mode 100755
index 0000000..795a5ee
--- /dev/null
+++ b/components/analytics/analytics.component.ts
@@ -0,0 +1,38 @@
+import { Component, OnInit } from '@angular/core';
+import { ActivatedRoute } from '@angular/router';
+import { FetchClient } from "@c8y/client";
+
+@Component({
+ selector: 'app-analytics',
+ templateUrl: "./analytics.component.html",
+ styleUrls: ['./analytics.component.css']
+ })
+
+ export class AnalyticsComponent implements OnInit {
+ epl_data: any;
+ aBuilder_data: any;
+ constructor(public route: ActivatedRoute,
+ private fetchClient: FetchClient) {} // 1
+
+ ngOnInit() {
+ this.fetchClient.fetch('service/cep/eplfiles').then((response) => { //try...
+ response.json().then( (data) => {
+ this.epl_data = data["eplfiles"];
+ // console.log(this.epl_data);
+ } );
+
+ } ,(error) => { //...catch
+ console.log(error);
+ } );
+
+ this.fetchClient.fetch('service/cep/analyticsbuilder').then((response) => { //try...
+ response.json().then( (data) => {
+ this.aBuilder_data = data["analyticsbuilder"];
+ // console.log(this.aBuilder_data);
+ } );
+
+ } ,(error) => { //...catch
+ console.log(error);
+ } );
+ }
+ }
\ No newline at end of file
diff --git a/components/analytics/analyticsbuilder/apamaAB.component.css b/components/analytics/analyticsbuilder/apamaAB.component.css
new file mode 100755
index 0000000..6506f0b
--- /dev/null
+++ b/components/analytics/analyticsbuilder/apamaAB.component.css
@@ -0,0 +1,12 @@
+.card{
+ width: 28em;
+ }
+
+ .card:hover {
+ box-shadow: 0 5px 15px rgba(0,0,0,0.3);
+ }
+
+ .card-title{
+ text-overflow: ellipsis;
+ }
+
\ No newline at end of file
diff --git a/components/analytics/analyticsbuilder/apamaAB.component.html b/components/analytics/analyticsbuilder/apamaAB.component.html
new file mode 100755
index 0000000..1288438
--- /dev/null
+++ b/components/analytics/analyticsbuilder/apamaAB.component.html
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+
+
+
+
Description:
+
{{ model.description }}
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/components/analytics/analyticsbuilder/apamaAB.component.ts b/components/analytics/analyticsbuilder/apamaAB.component.ts
new file mode 100755
index 0000000..a3efab5
--- /dev/null
+++ b/components/analytics/analyticsbuilder/apamaAB.component.ts
@@ -0,0 +1,141 @@
+import { Component, OnInit } from '@angular/core';
+import { ActivatedRoute } from '@angular/router';
+
+import { OperationService, IOperation, FetchClient } from '@c8y/client';
+import { Alert, AlertService } from '@c8y/ngx-components';
+
+@Component({
+ selector: 'app-analytics-builder',
+ templateUrl: "./apamaAB.component.html",
+ styleUrls: ['./apamaAB.component.css']
+ })
+
+ export class AnalyticsBuilderComponent implements OnInit {
+ aBuilder_models: any;
+ aBuilder_data: any;
+
+ modelUrl: string;
+ deviceId: string;
+
+ constructor(public route: ActivatedRoute,
+ private fetchClient: FetchClient,
+ private ops: OperationService,
+ private alert: AlertService) {} // 1
+
+ ngOnInit() {
+ var data = this.route.snapshot.parent.data.contextData;
+ this.deviceId = data["id"];
+
+ const indexObj = data["self"].indexOf("/inventory/");
+ this.modelUrl = data["self"].slice(0,indexObj) + "/apps/apamaanalyticsbuilder";
+
+ this.fetchABModels();
+ this.fetchThinEdgeModel();
+
+ }
+
+ fetchABModels(){
+ this.fetchClient.fetch('service/cep/analyticsbuilder').then((response) => { //try...
+ response.json().then( (data) => {
+ this.aBuilder_data = data["analyticsBuilderModelRepresentations"];
+ } );
+
+ } ,(error) => { //...catch
+ console.log(error);
+ } );
+ }
+
+ fetchThinEdgeModel(){
+ this.fetchClient.fetch("inventory/managedObjects/" + this.deviceId).then(
+ (response) => {
+ //try...
+ response.json().then((data) => {
+ this.aBuilder_models = data["c8y_ThinEdge_Model"];
+ // console.log(this.epl_models); //thin edge models
+ });
+ },
+ (error) => {
+ //...catch
+ console.log(error);
+ }
+ );
+ }
+
+ isModelStatusActive(model):boolean{
+ for(var i = 0; i < this.aBuilder_models.length; i++) {
+ if (model.name == this.aBuilder_models[i].name){
+ return true;
+ }
+ }
+ return false;
+ }
+
+ switchModelOnOff(model: any, event: any, trigger: any){
+ //console.log(model); // {}
+ //console.log(trigger); // {}
+ var isActive = this.isModelStatusActive(model);
+
+ const objIndex = this.aBuilder_data.findIndex((obj => obj.id == model.id));
+
+ if (trigger && !isActive){
+ console.log("switch ON");
+ this.switchModelOn(model);
+ this.aBuilder_data[objIndex].state = "active";
+ }else if(!trigger && isActive){
+ console.log("switch OFF");
+ this.switchModelOff(model);
+ this.aBuilder_data[objIndex].state = "inactive";
+ }else if ((trigger && isActive) || (!trigger && !isActive) ){
+ console.log("Try to prevent check.");
+ event.preventDefault();
+ }
+
+ }
+
+ switchModelOn(model: any){
+ const modelOnOperation: IOperation = {
+ deviceId: this.deviceId,
+ c8y_ThinEdge_Model: {
+ name: model.name,
+ type: "AnalyticsBuilder",
+ id: model.id,
+ order: "load"
+ },
+ description: `Load model '${model.name}'`
+ };
+ this.sendOperation(modelOnOperation);
+ }
+ switchModelOff(model: any){
+ const modelOffOperation: IOperation = {
+ deviceId: this.deviceId,
+ c8y_ThinEdge_Model: {
+ name: model.name,
+ type: "AnalyticsBuilder",
+ id: model.id,
+ order: "delete"
+ },
+ description: `Delete model '${model.name}'`
+ };
+ this.sendOperation(modelOffOperation);
+ }
+
+ sendOperation(operation){
+ this.ops.create(operation).then(result => {
+ const myAlert:Alert = {
+ text :`${operation.description}`,
+ type: "info",
+ timeout : 8000
+ };
+ this.alert.add(myAlert);
+ },error => {
+ const myAlert:Alert = {
+ text : "Error creating operation. " + JSON.stringify(error),
+ type: "danger",
+ timeout : 8000
+ };
+ this.alert.add(myAlert);
+ } );
+
+ }
+
+ }
\ No newline at end of file
diff --git a/components/analytics/apamaepl/apamaepl.component.css b/components/analytics/apamaepl/apamaepl.component.css
new file mode 100755
index 0000000..6506f0b
--- /dev/null
+++ b/components/analytics/apamaepl/apamaepl.component.css
@@ -0,0 +1,12 @@
+.card{
+ width: 28em;
+ }
+
+ .card:hover {
+ box-shadow: 0 5px 15px rgba(0,0,0,0.3);
+ }
+
+ .card-title{
+ text-overflow: ellipsis;
+ }
+
\ No newline at end of file
diff --git a/components/analytics/apamaepl/apamaepl.component.html b/components/analytics/apamaepl/apamaepl.component.html
new file mode 100755
index 0000000..33e485e
--- /dev/null
+++ b/components/analytics/apamaepl/apamaepl.component.html
@@ -0,0 +1,57 @@
+
+
+
+
+
+
+
+
+
+
+
Description:
+
{{ model.description }}
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/components/analytics/apamaepl/apamaepl.component.ts b/components/analytics/apamaepl/apamaepl.component.ts
new file mode 100755
index 0000000..a72710e
--- /dev/null
+++ b/components/analytics/apamaepl/apamaepl.component.ts
@@ -0,0 +1,150 @@
+import { Component, OnInit } from "@angular/core";
+import { ActivatedRoute } from "@angular/router";
+
+import { OperationService, IOperation, FetchClient } from "@c8y/client";
+import { Alert, AlertService } from "@c8y/ngx-components";
+
+@Component({
+ selector: "app-analytics-epl",
+ templateUrl: "./apamaepl.component.html",
+ styleUrls: ["./apamaepl.component.css"],
+})
+export class AnalyticsEPLComponent implements OnInit {
+ epl_data: any;
+ epl_models: any;
+
+ deviceId: string;
+ eplUrl: string;
+
+ isdeviceAvailable: boolean;
+
+ constructor(
+ public route: ActivatedRoute,
+ private fetchClient: FetchClient,
+ private ops: OperationService,
+ private alert: AlertService
+ ) {} // 1
+
+ ngOnInit() {
+ console.log("ngOnInit execute...");
+ var data = this.route.snapshot.parent.data.contextData;
+ this.deviceId = data["id"];
+
+ const indexObj = data["self"].indexOf("/inventory/");
+ this.eplUrl = data["self"].slice(0, indexObj) + "/apps/apamaepl";
+ //console.log("URL: " + this.url);
+
+ this.fetchEPLModels();
+ this.fetchThinEdgeModels();
+ }
+
+ fetchThinEdgeModels(){
+ this.fetchClient.fetch("inventory/managedObjects/" + this.deviceId).then(
+ (response) => {
+ //try...
+ response.json().then((data) => {
+ this.epl_models = data["c8y_ThinEdge_Model"];
+ // console.log(this.epl_models); //thin edge models
+ });
+ },
+ (error) => {
+ //...catch
+ console.log(error);
+ }
+ );
+ }
+
+ fetchEPLModels() {
+ console.log("Fetch new epl models...");
+ this.fetchClient.fetch("service/cep/eplfiles").then(
+ (response) => {
+ //try...
+ response.json().then((data) => {
+ this.epl_data = data["eplfiles"];
+ // console.log(this.epl_data); //plattform models
+ // var available = data["c8y_Availability"];
+ });
+ },
+ (error) => {
+ //...catch
+ console.log(error);
+ }
+ );
+ }
+
+ isModelStatusActive(model): boolean {
+
+ for (var i = 0; i < this.epl_models.length; i++) {
+ if (model.name == this.epl_models[i].name) {
+ return true;
+ }
+ }
+ return false;
+ }
+ sendOperation(operation) {
+ this.ops.create(operation).then(
+ (result) => {
+ const myAlert: Alert = {
+ text: `${operation.description}`,
+ type: "info",
+ timeout: 8000,
+ };
+ this.alert.add(myAlert);
+ },
+ (error) => {
+ const myAlert: Alert = {
+ text: "Error creating operations. " + JSON.stringify(error),
+ type: "danger",
+ timeout: 8000,
+ };
+ this.alert.add(myAlert);
+ }
+ );
+ }
+ switchModelOn(model: any) {
+ const modelOnOperation: IOperation = {
+ deviceId: this.deviceId,
+ c8y_ThinEdge_Model: {
+ name: model.name,
+ type: "EPL",
+ id: model.id,
+ order: "load",
+ },
+ description: `Load model '${model.name}'`,
+ };
+ this.sendOperation(modelOnOperation);
+ }
+ switchModelOff(model: any) {
+ const modelOffOperation: IOperation = {
+ deviceId: this.deviceId,
+ c8y_ThinEdge_Model: {
+ name: model.name,
+ type: "EPL",
+ id: model.id,
+ order: "delete",
+ },
+ description: `Delete model '${model.name}'`,
+ };
+ this.sendOperation(modelOffOperation);
+ }
+ switchModelOnOff(model: any, event: any, trigger: any) {
+ console.log(model); // {}
+ console.log(trigger); // {}
+ var isActive = this.isModelStatusActive(model);
+
+ const objIndex = this.epl_data.findIndex((obj) => obj.id == model.id);
+
+ if (trigger && !isActive) {
+ console.log("switch ON");
+ this.switchModelOn(model);
+ this.epl_data[objIndex].state = "active";
+ } else if (!trigger && isActive) {
+ console.log("switch OFF");
+ this.switchModelOff(model);
+ this.epl_data[objIndex].state = "inactive";
+ } else if ((trigger && isActive) || (!trigger && !isActive)) {
+ console.log("Try to prevent check.");
+ event.preventDefault();
+ }
+ }
+}
diff --git a/components/docker-comp/docker.component.css b/components/docker-comp/docker.component.css
new file mode 100755
index 0000000..1f34487
--- /dev/null
+++ b/components/docker-comp/docker.component.css
@@ -0,0 +1,78 @@
+.card{
+ width: 25em;
+}
+
+.card.d{
+ background-color: rgba(70, 67, 67, 0.603);
+}
+
+.card:hover {
+ box-shadow: 0 5px 15px rgba(0,0,0,0.3);
+ }
+
+.card-title{
+ text-overflow: ellipsis;
+}
+
+/* The Modal (background) */
+.modal{
+ display: none; /* Hidden by default */
+ position: fixed; /* Stay in place */
+ z-index: 1; /* Sit on top */
+ left: 0;
+ top: 0;
+ width: 100%; /* Full width */
+ height: 100%; /* Full height */
+ overflow: auto; /* Enable scroll if needed */
+ background-color: rgb(0,0,0); /* Fallback color */
+ background-color: rgba(0,0,0,0.4); /* Black w/ opacity */
+}
+/* Modal Content/Box */
+.modal-content {
+ background-color: #fefefe;
+ margin: 15% auto; /* 15% from the top and centered */
+ padding: 20px;
+ border: 1px solid #888;
+ width: 30em; /* Could be more or less, depending on screen size */
+ height: 40em;
+}
+/* Modal Header */
+.modal-header {
+ padding: 1em;
+ text-align: center;
+}
+/* Modal Body */
+.modal-body {
+ padding: 1em;
+ margin-top: 3em;
+}
+
+/* Modal Footer */
+.modal-footer {
+ display: flex;
+ width: 100%;
+ position: absolute;
+ justify-content: center;
+ align-items: center;
+ bottom: 0;
+}
+
+/* The Close Button */
+.close {
+ color: #aaa;
+ float: right;
+ font-size: 28px;
+ font-weight: bold;
+}
+
+.close:hover,
+.close:focus {
+ color: black;
+ text-decoration: none;
+ cursor: pointer;
+}
+
+input {
+ width: 100%;
+ margin-bottom: 0.5em;
+}
diff --git a/components/docker-comp/docker.component.html b/components/docker-comp/docker.component.html
new file mode 100755
index 0000000..6645c50
--- /dev/null
+++ b/components/docker-comp/docker.component.html
@@ -0,0 +1,148 @@
+
+
List containers
+
+
+
+
+
+
+
+
+
+
+
CPU: {{ container.cpu }}%
+
RAM: {{ container.memory }}
+
+ Container ID: {{ container.containerID }}
+
+
Status: {{ container.status }}
+
+
+
+
+
+
+
+
+
+
diff --git a/components/docker-comp/docker.component.ts b/components/docker-comp/docker.component.ts
new file mode 100755
index 0000000..0bd03b9
--- /dev/null
+++ b/components/docker-comp/docker.component.ts
@@ -0,0 +1,223 @@
+import { elementEventFullName } from "@angular/compiler/src/view_compiler/view_compiler";
+import { Component, OnInit } from "@angular/core";
+import { ActivatedRoute } from "@angular/router";
+
+import { OperationService, IOperation, FetchClient } from "@c8y/client";
+import { Alert, AlertService } from "@c8y/ngx-components";
+import { interval } from "rxjs";
+
+@Component({
+ selector: "app-docker",
+ templateUrl: "./docker.component.html",
+ styleUrls: ["./docker.component.css"],
+})
+export class DockerComponent implements OnInit {
+ containers: any;
+ deviceId: string;
+ available: any;
+
+ isAvailable: boolean;
+
+ modal: any;
+ spanClose: any;
+
+ constructor(
+ public route: ActivatedRoute,
+ private ops: OperationService,
+ private alert: AlertService,
+ private fetchClient: FetchClient
+ ) {
+ this.available = new Object();
+ }
+
+ ngOnInit(): void {
+ this.deviceId = this.route.snapshot.parent.data.contextData["id"];
+ // this.containers = this.route.snapshot.parent.data.contextData["c8y_Docker"] ;
+ this.isAvailable = true;
+ this.deviceFetchClient();
+ this.modal = document.getElementById("docker-modal");
+ this.spanClose = document.getElementById("closeModal");
+ }
+
+ ngAfterViewChecked() {
+ /* if (Object.keys(