Browse Source

[3.0] Add gateway widget (#2547)

* Create gateway widget

* Refacoring and add styled

* Update bundle

* Add model and refactoring
pull/2562/head
Vladyslav 7 years ago
committed by GitHub
parent
commit
67d9b094fa
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 6
      application/src/main/data/json/system/widget_bundles/gateway_widgets.json
  2. 37
      ui-ngx/package-lock.json
  3. 2
      ui-ngx/package.json
  4. 12
      ui-ngx/src/app/modules/home/components/import-export/import-export.models.ts
  5. 39
      ui-ngx/src/app/modules/home/components/import-export/import-export.service.ts
  6. 262
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-form.component.html
  7. 41
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-form.component.scss
  8. 407
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-form.component.ts
  9. 374
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-form.models.ts
  10. 11
      ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts
  11. 1
      ui-ngx/src/app/modules/home/components/widget/widget.component.ts
  12. 2
      ui-ngx/src/app/shared/components/dialog/json-object-edit-dialog.component.html
  13. 33
      ui-ngx/src/app/shared/components/dialog/json-object-edit-dialog.component.ts
  14. 56
      ui-ngx/src/app/shared/components/entity/entity-gateway-select.component.html
  15. 223
      ui-ngx/src/app/shared/components/entity/entity-gateway-select.component.ts
  16. 4
      ui-ngx/src/app/shared/components/value-input.component.ts
  17. 8
      ui-ngx/src/app/shared/shared.module.ts
  18. 3
      ui-ngx/src/assets/locale/locale.constant-en_US.json
  19. 5
      ui-ngx/tsconfig.json

6
application/src/main/data/json/system/widget_bundles/gateway_widgets.json

@ -13,13 +13,13 @@
"sizeX": 8,
"sizeY": 6.5,
"resources": [],
"templateHtml": "<tb-gateway-form\n form-id=\"formId\"\n ctx=\"ctx\">\n</tb-gateway-form>\n",
"templateHtml": "<tb-gateway-form\n [ctx]=\"ctx\">\n</tb-gateway-form>\n",
"templateCss": "",
"controllerScript": "self.onInit = function() {\n var scope = self.ctx.$scope;\n var id = self.ctx.$scope.$injector.get('utils').guid();\n scope.formId = \"form-\"+id;\n scope.ctx = self.ctx;\n}\n\nself.onResize = function() {\n self.ctx.$scope.$broadcast('gateway-form-resize', self.ctx.$scope.formId);\n}\n",
"controllerScript": "self.onInit = function() {\n}\n",
"settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"Gateway Configuration\"\n },\n \"archiveFileName\": {\n \"title\": \"Default archive file name\",\n \"type\": \"string\",\n \"default\": \"gatewayConfiguration\"\n },\n \"gatewayType\": {\n \"title\": \"Device type for new gateway\",\n \"type\": \"string\",\n \"default\": \"Gateway\"\n },\n \"successfulSave\": {\n \"title\": \"Text message about successfully saved gateway configuration\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"gatewayNameExists\": {\n \"title\": \"Text message when device with entered name is already exists\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n [\n \"widgetTitle\",\n \"archiveFileName\",\n \"gatewayType\"\n ],\n [\n \"successfulSave\",\n \"gatewayNameExists\"\n ]\n ],\n \"groupInfoes\": [{\n \"formIndex\": 0,\n \"GroupTitle\": \"General settings\"\n }, {\n \"formIndex\": 1,\n \"GroupTitle\": \"Messages settings\"\n }]\n}",
"dataKeySettingsSchema": "{}\n",
"defaultConfig": "{\"datasources\":[{\"type\":\"static\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"widgetTitle\":\"Gateway Configuration\",\"archiveFileName\":\"configurationGateway\"},\"title\":\"Gateway Configuration\",\"dropShadow\":true,\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"enableFullscreen\":true,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"showLegend\":false,\"actions\":{}}"
}
}
]
}
}

37
ui-ngx/package-lock.json

@ -1657,6 +1657,15 @@
"@types/jquery": "*"
}
},
"@types/jszip": {
"version": "3.1.7",
"resolved": "https://registry.npmjs.org/@types/jszip/-/jszip-3.1.7.tgz",
"integrity": "sha512-+XQKNI5zpxutK05hO67huUTw/2imXCuJWjnFdU63tRES/xXSX1yVR9cv/QAdO6Rii2y2tTHbzjQ4i2apLfuK0Q==",
"dev": true,
"requires": {
"@types/node": "*"
}
},
"@types/leaflet": {
"version": "1.5.12",
"resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.5.12.tgz",
@ -3701,8 +3710,7 @@
"core-util-is": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=",
"dev": true
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
},
"cosmiconfig": {
"version": "5.2.1",
@ -6017,8 +6025,7 @@
"immediate": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
"integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps=",
"dev": true
"integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps="
},
"import-cwd": {
"version": "2.1.0",
@ -6503,8 +6510,7 @@
"isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=",
"dev": true
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
},
"isbinaryfile": {
"version": "3.0.3",
@ -7001,7 +7007,6 @@
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.2.2.tgz",
"integrity": "sha512-NmKajvAFQpbg3taXQXr/ccS2wcucR1AZ+NtyWp2Nq7HHVsXhcJFR8p0Baf32C2yVvBylFWVeKf+WI2AnvlPhpA==",
"dev": true,
"requires": {
"lie": "~3.3.0",
"pako": "~1.0.2",
@ -7236,7 +7241,6 @@
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
"integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
"dev": true,
"requires": {
"immediate": "~3.0.5"
}
@ -8748,8 +8752,7 @@
"pako": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
"dev": true
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="
},
"parallel-transform": {
"version": "1.2.0",
@ -9598,8 +9601,7 @@
"process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
"dev": true
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
},
"promise": {
"version": "7.3.1",
@ -10238,7 +10240,6 @@
"version": "2.3.7",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
"integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
"dev": true,
"requires": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
@ -10569,8 +10570,7 @@
"safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"dev": true
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
},
"safe-regex": {
"version": "1.1.0",
@ -10898,8 +10898,7 @@
"set-immediate-shim": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz",
"integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=",
"dev": true
"integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E="
},
"set-value": {
"version": "2.0.1",
@ -11725,7 +11724,6 @@
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"dev": true,
"requires": {
"safe-buffer": "~5.1.0"
}
@ -12623,8 +12621,7 @@
"util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=",
"dev": true
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
},
"util-promisify": {
"version": "2.1.0",

2
ui-ngx/package.json

@ -56,6 +56,7 @@
"json-schema-defaults": "^0.4.0",
"jstree": "^3.3.9",
"jstree-bootstrap-theme": "^1.0.1",
"jszip": "^3.2.2",
"leaflet": "^1.6.0",
"leaflet-geometryutil": "^0.9.3",
"leaflet-polylinedecorator": "^1.6.0",
@ -105,6 +106,7 @@
"@types/jquery": "^3.3.32",
"@types/js-beautify": "^1.8.1",
"@types/jstree": "^3.3.39",
"@types/jszip": "^3.1.7",
"@types/leaflet": "^1.5.9",
"@types/leaflet-polylinedecorator": "^1.6.0",
"@types/lodash": "^4.14.149",

12
ui-ngx/src/app/modules/home/components/import-export/import-export.models.ts

@ -80,6 +80,18 @@ export interface CsvColumnParam {
sampleData: any;
}
export enum FileType {
zip = 'application/zip',
json = 'text/json'
}
export const FileTypeExtension = new Map<FileType, string>(
[
[FileType.zip, 'zip'],
[FileType.json, 'json'],
]
);
export function convertCSVToJson(csvdata: string, config: CsvToJsonConfig,
onError: (messageId: string, params?: any) => void): CsvToJsonResult | number {
config = config || {};

39
ui-ngx/src/app/modules/home/components/import-export/import-export.service.ts

@ -44,7 +44,7 @@ import {
EntityAliasesDialogData
} from '@home/components/alias/entity-aliases-dialog.component';
import { ItemBufferService, WidgetItem } from '@core/services/item-buffer.service';
import { ImportWidgetResult, WidgetsBundleItem } from './import-export.models';
import { FileType, FileTypeExtension, ImportWidgetResult, WidgetsBundleItem } from './import-export.models';
import { EntityType } from '@shared/models/entity-type.models';
import { UtilsService } from '@core/services/utils.service';
import { WidgetService } from '@core/http/widget.service';
@ -54,6 +54,7 @@ import { ImportEntitiesResultInfo, ImportEntityData } from '@shared/models/entit
import { RequestConfig } from '@core/http/http-utils';
import { RuleChain, RuleChainImport, RuleChainMetaData } from '@shared/models/rule-chain.models';
import { RuleChainService } from '@core/http/rule-chain.service';
import * as JSZip from 'jszip';
// @dynamic
@Injectable()
@ -79,7 +80,7 @@ export class ImportExportService {
(dashboard) => {
let name = dashboard.title;
name = name.toLowerCase().replace(/\W/g, '_');
this.exportToPc(this.prepareDashboardExport(dashboard), name + '.json');
this.exportToFile(this.prepareDashboardExport(dashboard), name, FileType.json);
},
(e) => {
this.handleExportError(e, 'dashboard.export-failed-error');
@ -136,7 +137,7 @@ export class ImportExportService {
const widgetItem = this.itembuffer.prepareWidgetItem(dashboard, sourceState, sourceLayout, widget);
let name = widgetItem.widget.config.title;
name = name.toLowerCase().replace(/\W/g, '_');
this.exportToPc(this.prepareExport(widgetItem), name + '.json');
this.exportToFile(this.prepareExport(widgetItem), name, FileType.json);
}
public importWidget(dashboard: Dashboard, targetState: string,
@ -235,7 +236,7 @@ export class ImportExportService {
}
let name = widgetType.name;
name = name.toLowerCase().replace(/\W/g, '_');
this.exportToPc(this.prepareExport(widgetType), name + '.json');
this.exportToFile(this.prepareExport(widgetType), name, FileType.json);
},
(e) => {
this.handleExportError(e, 'widget-type.export-failed-error');
@ -281,7 +282,7 @@ export class ImportExportService {
}
let name = widgetsBundle.title;
name = name.toLowerCase().replace(/\W/g, '_');
this.exportToPc(widgetsBundleItem, name + '.json');
this.exportToFile(widgetsBundleItem, name, FileType.json);
},
(e) => {
this.handleExportError(e, 'widgets-bundle.export-failed-error');
@ -383,7 +384,7 @@ export class ImportExportService {
).subscribe((ruleChainExport) => {
let name = ruleChainExport.ruleChain.name;
name = name.toLowerCase().replace(/\W/g, '_');
this.exportToPc(ruleChainExport, name + '.json');
this.exportToFile(ruleChainExport, name, FileType.json);
},
(e) => {
this.handleExportError(e, 'rulechain.export-failed-error');
@ -414,6 +415,19 @@ export class ImportExportService {
);
}
public exportJSZip(data: object, filename: string) {
const jsZip: JSZip = new JSZip();
for (const keyName in data) {
if (data.hasOwnProperty(keyName)) {
const valueData = data[keyName];
jsZip.file(keyName, valueData);
}
}
jsZip.generateAsync({type: 'blob'}).then(content => {
this.exportToFile(content, filename, FileType.zip);
});
}
private prepareRuleChain(ruleChain: RuleChain): RuleChain {
ruleChain = this.prepareExport(ruleChain);
if (ruleChain.firstRuleNodeId) {
@ -673,18 +687,19 @@ export class ImportExportService {
));
}
private exportToPc(data: any, filename: string) {
private exportToFile(data: any, filename: string, fileType: FileType) {
if (!data) {
console.error('No data');
return;
}
if (!filename) {
filename = 'download.json';
filename = 'download';
}
if (isObject(data)) {
data = JSON.stringify(data, null, 2);
filename += '.' + FileTypeExtension.get(fileType);
if (fileType === FileType.json && isObject(data)) {
data = JSON.stringify(data, null, 2);
}
const blob = new Blob([data], {type: 'text/json'});
const blob = new Blob([data], {type: fileType});
if (this.window.navigator && this.window.navigator.msSaveOrOpenBlob) {
this.window.navigator.msSaveOrOpenBlob(blob, filename);
} else {
@ -692,7 +707,7 @@ export class ImportExportService {
const a = this.document.createElement('a');
a.download = filename;
a.href = URL.createObjectURL(blob);
a.dataset.downloadurl = ['text/json', a.download, a.href].join(':');
a.dataset.downloadurl = [fileType, a.download, a.href].join(':');
// @ts-ignore
e.initEvent('click', true, false, this.window,
0, 0, 0, 0, 0, false, false, false, false, 0, null);

262
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-form.component.html

@ -0,0 +1,262 @@
<!--
Copyright © 2016-2020 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<form #formContainer class="gateway-form"
[formGroup]="gatewayConfigurationGroup"
tb-toast toastTarget="{{ toastTargetId }}"
(ngSubmit)="save()">
<mat-accordion multi="true" class="mat-body-1">
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title>
<div class="tb-panel-title">{{ 'gateway.thingsboard' | translate | uppercase }}</div>
</mat-panel-title>
</mat-expansion-panel-header>
<tb-entity-gateway-select
formControlName="gateway"
[newGatewayType]="gatewayType"
(gatewayNameExist)="gatewayExist()"
required
>
</tb-entity-gateway-select>
<div fxLayout="column">
<mat-form-field fxFlex>
<mat-label>{{'gateway.security-type' | translate }}</mat-label>
<mat-select formControlName="securityType" >
<mat-option *ngFor="let securityType of securityTypes | keyvalue" [value]="securityType.key">
{{ securityType.value.toString() | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div [fxLayout]="alignment" [fxLayoutGap]="layoutGap">
<mat-form-field fxFlex>
<mat-label>{{ 'gateway.thingsboard-host' | translate }}</mat-label>
<input matInput type="text" formControlName="host">
<mat-error *ngIf="gatewayConfigurationGroup.get('host').hasError('required')" translate>
gateway.thingsboard-host-required
</mat-error>
</mat-form-field>
<mat-form-field fxFlex>
<mat-label>{{ 'gateway.thingsboard-port' | translate }}</mat-label>
<input matInput type="number" formControlName="port">
<mat-error *ngIf="gatewayConfigurationGroup.get('port').hasError('required')" translate>
gateway.thingsboard-port-required
</mat-error>
<mat-error *ngIf="gatewayConfigurationGroup.get('port').hasError('min')" translate>
gateway.thingsboard-port-min
</mat-error>
<mat-error *ngIf="gatewayConfigurationGroup.get('port').hasError('max')" translate>
gateway.thingsboard-port-max
</mat-error>
<mat-error *ngIf="gatewayConfigurationGroup.get('port').hasError('pattern')" translate>
gateway.thingsboard-port-pattern
</mat-error>
</mat-form-field>
</div>
<div *ngIf="gatewayConfigurationGroup.get('securityType').value == 'tls'" fxLayout="column">
<mat-form-field fxFlex>
<mat-label>{{ 'gateway.tls-path-ca-certificate' | translate }}</mat-label>
<input matInput type="text" formControlName="caCertPath">
</mat-form-field>
<mat-form-field fxFlex>
<mat-label>{{ 'gateway.tls-path-private-key' | translate }}</mat-label>
<input matInput type="text" formControlName="privateKeyPath">
</mat-form-field>
<mat-form-field fxFlex>
<mat-label>{{ 'gateway.tls-path-client-certificate' | translate }}</mat-label>
<input matInput type="text" formControlName="certPath">
</mat-form-field>
</div>
<mat-checkbox formControlName="remoteConfiguration">{{ 'gateway.remote' | translate }}</mat-checkbox>
<div [fxLayout]="alignment" [fxLayoutGap]="layoutGap">
<mat-form-field fxFlex>
<mat-label>{{'gateway.remote-logging-level' | translate }}</mat-label>
<mat-select formControlName="remoteLoggingLevel">
<mat-option *ngFor="let logLevel of gatewayLogLevels" [value]="logLevel">
{{ logLevel }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field fxFlex>
<mat-label>{{ 'gateway.path-logs' | translate }}</mat-label>
<input matInput type="text" formControlName="remoteLoggingPathToLogs">
<mat-error *ngIf="gatewayConfigurationGroup.get('remoteLoggingPathToLogs').hasError('required')" translate>
gateway.path-logs-required
</mat-error>
</mat-form-field>
</div>
</mat-expansion-panel>
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title>
<div class="tb-panel-title">{{ 'gateway.storage' | translate | uppercase }}</div>
</mat-panel-title>
</mat-expansion-panel-header>
<div fxLayout="column">
<mat-form-field fxFlex>
<mat-label>{{'gateway.storage-type' | translate }}</mat-label>
<mat-select formControlName="storageType">
<mat-option *ngFor="let storageType of storageTypes | keyvalue" [value]="storageType.key">
{{ storageType.value.toString() | translate}}
</mat-option>
</mat-select>
</mat-form-field>
<div [fxLayout]="alignment" [fxLayoutGap]="layoutGap">
<mat-form-field fxFlex>
<mat-label>{{ 'gateway.storage-pack-size' | translate }}</mat-label>
<input matInput type="number" formControlName="readRecordsCount">
<mat-error *ngIf="gatewayConfigurationGroup.get('readRecordsCount').hasError('required')" translate>
gateway.storage-pack-size-required
</mat-error>
<mat-error *ngIf="gatewayConfigurationGroup.get('readRecordsCount').hasError('min')" translate>
gateway.storage-pack-size-min
</mat-error>
<mat-error *ngIf="gatewayConfigurationGroup.get('readRecordsCount').hasError('pattern')" translate>
gateway.storage-pack-size-pattern
</mat-error>
</mat-form-field>
<mat-form-field fxFlex>
<mat-label >
{{ (gatewayConfigurationGroup.get('storageType').value !== 'file' ? 'gateway.storage-max-records' : 'gateway.storage-max-file-records') | translate}}
</mat-label>
<input matInput type="number" formControlName="maxRecordsCount">
<mat-error *ngIf="gatewayConfigurationGroup.get('maxRecordsCount').hasError('required')" translate>
gateway.storage-max-records-required
</mat-error>
<mat-error *ngIf="gatewayConfigurationGroup.get('maxRecordsCount').hasError('min')" translate>
gateway.storage-max-records-min
</mat-error>
<mat-error *ngIf="gatewayConfigurationGroup.get('maxRecordsCount').hasError('pattern')" translate>
gateway.storage-max-records-pattern
</mat-error>
</mat-form-field>
</div>
<div [fxLayout]="alignment" [fxLayoutGap]="layoutGap" *ngIf="gatewayConfigurationGroup.get('storageType').value == 'file'">
<mat-form-field fxFlex>
<mat-label>{{ 'gateway.storage-max-files' | translate }}</mat-label>
<input matInput type="number" formControlName="maxFilesCount">
<mat-error *ngIf="gatewayConfigurationGroup.get('maxFilesCount').hasError('required')" translate>
gateway.storage-max-files-required
</mat-error>
<mat-error *ngIf="gatewayConfigurationGroup.get('maxFilesCount').hasError('min')" translate>
gateway.storage-max-files-min
</mat-error>
<mat-error *ngIf="gatewayConfigurationGroup.get('maxFilesCount').hasError('pattern')" translate>
gateway.storage-max-files-pattern
</mat-error>
</mat-form-field>
<mat-form-field fxFlex>
<mat-label>{{ 'gateway.storage-path' | translate }}</mat-label>
<input matInput type="text" formControlName="dataFolderPath">
<mat-error *ngIf="gatewayConfigurationGroup.get('dataFolderPath').hasError('required')" translate>
gateway.storage-path-required
</mat-error>
</mat-form-field>
</div>
</div>
</mat-expansion-panel>
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title>
<div class="tb-panel-title">{{ 'gateway.connectors' | translate | uppercase }}</div>
</mat-panel-title>
</mat-expansion-panel-header>
<div fxLayout="column" class="gateway-config">
<section formArrayName="connectors" *ngFor="let connector of connectors.controls; let i = index;">
<div [formGroupName]="i" fxLayout="row" fxLayoutAlign="space-between stretch" fxLayoutGap="8px">
<div fxLayout="column" fxLayoutAlign="center start">
<mat-slide-toggle formControlName="enabled"></mat-slide-toggle>
</div>
<div [fxLayout]="alignment" [fxLayoutGap]="layoutGap" fxFlex>
<mat-form-field fxFlex>
<mat-label>{{'gateway.connector-type' | translate }}</mat-label>
<mat-select formControlName="configType" (selectionChange)="changeConnectorType(connector)">
<mat-option *ngFor="let connectorType of connectorTypes" [value]="connectorType">
{{ connectorType }}
</mat-option>
</mat-select>
<mat-error *ngIf="connector.get('configType').hasError('required')" translate>
gateway.connector-type-required
</mat-error>
</mat-form-field>
<mat-form-field fxFlex>
<mat-label>{{ 'gateway.connector-name' | translate }}</mat-label>
<input matInput type="text" formControlName="name" (blur)="changeConnectorName(connector, i)">
<mat-error *ngIf="connector.get('name').hasError('required')" translate>
gateway.connector-name-required
</mat-error>
</mat-form-field>
</div>
<div [fxLayout]="alignment" [fxLayoutGap]="layoutGap"
fxLayoutAlign="{{alignment == 'row' ? 'end center' : 'space-evenly center'}}" class="action-buttons">
<button mat-icon-button (click)="openConfigDialog($event, i, connector.get('config').value, connector.get('name').value)"
matTooltip="{{ 'gateway.update-config' | translate }}"
matTooltipPosition="above"
[ngClass]="{'mat-warn': connector.get('config').invalid}">
<mat-icon>more_horiz</mat-icon>
</button>
<button mat-icon-button (click)="removeConnector(i)"
matTooltip="{{ 'gateway.delete' | translate }}"
matTooltipPosition="above">
<mat-icon>close</mat-icon>
</button>
</div>
</div>
</section>
<span [fxShow]="!connectors.length" fxLayoutAlign="center center" class="no-data-found">{{'gateway.no-connectors' | translate}}</span>
<div>
<button mat-raised-button type="button" (click)="addNewConnector()"
matTooltip="{{ 'gateway.connector-add' | translate }}"
matTooltipPosition="above">
{{ 'action.add' | translate }}
</button>
</div>
</div >
</mat-expansion-panel>
</mat-accordion>
<section fxLayout="row" fxLayoutAlign="end center" class="form-action-buttons">
<button mat-raised-button color="primary" type="button"
(click)="exportConfig()"
*ngIf="!gatewayConfigurationGroup.get('remoteConfiguration').value"
[disabled]="!gatewayConfigurationGroup.dirty || gatewayConfigurationGroup.invalid"
matTooltip="{{'gateway.download-tip' | translate }}">
{{'action.download' | translate }}
</button>
<button mat-raised-button color="primary" type="submit"
*ngIf="gatewayConfigurationGroup.get('remoteConfiguration').value"
[disabled]="!gatewayConfigurationGroup.dirty || gatewayConfigurationGroup.invalid"
matTooltip="{{'gateway.save-tip' | translate }}">
{{'action.save' | translate }}
</button>
</section>
</form>

41
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-form.component.scss

@ -0,0 +1,41 @@
/**
* Copyright © 2016-2020 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
:host{
.gateway-form {
height: 100%;
padding: 5px;
background-color: transparent;
overflow-y: auto;
overflow-x: hidden;
.form-action-buttons{
padding-top: 8px;
}
.gateway-config {
.no-data-found {
position: relative;
display: flex;
height: 40px;
text-transform: uppercase;
}
}
}
::ng-deep .mat-form-field-infix {
width: auto !important;
}
}

407
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-form.component.ts

@ -0,0 +1,407 @@
///
/// Copyright © 2016-2020 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, ElementRef, Inject, Input, NgZone, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { AbstractControl, FormArray, FormBuilder, FormGroup, NgForm, Validators } from '@angular/forms';
import { WidgetContext } from '@home/models/widget-component.models';
import { UtilsService } from '@core/services/utils.service';
import {
CONFIGURATION_ATTRIBUTE,
CONFIGURATION_DRAFT_ATTRIBUTE,
ConnectorType,
createFormConfig,
CURRENT_CONFIGURATION_ATTRIBUTE,
DEFAULT_CONNECTOR,
gatewayConfigJSON,
GatewayFormConnectorModel,
GatewayFormModels,
GatewayLogLevel,
generateConnectorConfigFiles,
generateLogConfigFile,
generateYAMLConfigFile,
getDraftConnectorsJSON,
getEntityId,
REMOTE_LOGGING_LEVEL_ATTRIBUTE,
SecurityType,
SecurityTypeTranslationMap,
StorageType,
StorageTypeTranslationMap,
ValidateJSON,
WidgetSetting
} from './gateway-form.models';
import { WINDOW } from '@core/services/window.service';
import { MatDialog } from '@angular/material/dialog';
import {
JsonObjectEditDialogComponent,
JsonObjectEditDialogData
} from '@shared/components/dialog/json-object-edit-dialog.component';
import { TranslateService } from '@ngx-translate/core';
import { DeviceService } from '@core/http/device.service';
import { AttributeService } from '@core/http/attribute.service';
import { AttributeData, AttributeScope } from '@shared/models/telemetry/telemetry.models';
import { forkJoin, Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
import { ImportExportService } from '@home/components/import-export/import-export.service';
@Component({
selector: 'tb-gateway-form',
templateUrl: './gateway-form.component.html',
styleUrls: ['./gateway-form.component.scss']
})
export class GatewayFormComponent extends PageComponent implements OnInit, OnDestroy {
constructor(
protected store: Store<AppState>,
private elementRef: ElementRef,
private utils: UtilsService,
private ngZone: NgZone,
private fb: FormBuilder,
@Inject(WINDOW) private window: Window,
private dialog: MatDialog,
private translate: TranslateService,
private deviceService: DeviceService,
private attributeService: AttributeService,
private importExport: ImportExportService
) {
super(store);
}
get connectors(): FormArray {
return this.gatewayConfigurationGroup.get('connectors') as FormArray;
}
@ViewChild('formContainer', {static: true}) formContainerRef: ElementRef<HTMLElement>;
@ViewChild('gatewayConfigurationForm', {static: true}) multipleInputForm: NgForm;
private successfulSaved: string;
private gatewayNameExists: string;
private archiveFileName: string;
private formResizeListener: any;
private subscribeStorageType$: any;
private subscribeGateway$: any;
alignment = 'row';
layoutGap = '5px';
gatewayType: string;
gatewayConfigurationGroup: FormGroup;
securityTypes = SecurityTypeTranslationMap;
gatewayLogLevels = Object.values(GatewayLogLevel);
connectorTypes = Object.keys(ConnectorType);
storageTypes = StorageTypeTranslationMap;
toastTargetId = 'gateway-configuration-widget' + this.utils.guid();
@Input()
ctx: WidgetContext;
ngOnInit(): void {
this.initWidgetSettings(this.ctx.settings);
this.buildForm();
this.ctx.updateWidgetParams();
this.formResizeListener = this.resize.bind(this);
// @ts-ignore
addResizeListener(this.formContainerRef.nativeElement, this.formResizeListener);
}
ngOnDestroy(): void {
if (this.formResizeListener) {
// @ts-ignore
removeResizeListener(this.formContainerRef.nativeElement, this.formResizeListener);
}
this.subscribeGateway$.unsubscribe();
this.subscribeStorageType$.unsubscribe();
}
private initWidgetSettings(settings: WidgetSetting): void {
let widgetTitle;
if (settings.widgetTitle && settings.widgetTitle.length) {
widgetTitle = this.utils.customTranslation(settings.widgetTitle, settings.widgetTitle);
} else {
widgetTitle = this.translate.instant('gateway.gateway');
}
this.ctx.widgetTitle = widgetTitle;
this.archiveFileName = settings.archiveFileName?.length ? settings.archiveFileName : 'gatewayConfiguration';
this.gatewayType = settings.gatewayType?.length ? settings.gatewayType : 'Gateway';
this.gatewayNameExists = this.utils.customTranslation(settings.gatewayNameExists, settings.gatewayNameExists) || this.translate.instant('gateway.gateway-exists');
this.successfulSaved = this.utils.customTranslation(settings.successfulSave, settings.successfulSave) || this.translate.instant('gateway.gateway-saved');
this.updateWidgetDisplaying();
}
private resize(): void {
this.ngZone.run(() => {
this.updateWidgetDisplaying();
this.ctx.detectChanges();
});
}
private updateWidgetDisplaying(): void {
if(this.ctx.$container && this.ctx.$container[0].offsetWidth <= 425){
this.layoutGap = '0';
this.alignment = 'column';
} else {
this.layoutGap = '5px';
this.alignment = 'row';
}
}
private saveAttribute(attributeName: string, attributeValue: string, attributeScope: AttributeScope): Observable<any> {
const gatewayId = this.gatewayConfigurationGroup.get('gateway').value;
const attributes: AttributeData = {
key: attributeName,
value: attributeValue
};
return this.attributeService.saveEntityAttributes(getEntityId(gatewayId), attributeScope, [attributes]);
}
private createConnector(setting: GatewayFormConnectorModel = DEFAULT_CONNECTOR): void {
this.connectors.push(this.fb.group({
enabled: [setting.enabled],
configType: [setting.configType, [Validators.required]],
name: [setting.name, [Validators.required]],
config: [setting.config, [Validators.nullValidator, ValidateJSON]]
}));
}
private getFormField(name: string): AbstractControl {
return this.gatewayConfigurationGroup.get(name);
}
private buildForm(): void {
this.gatewayConfigurationGroup = this.fb.group({
gateway: [null],
accessToken: [null, [Validators.required]],
securityType: [SecurityType.accessToken],
host: [this.window.location.hostname, [Validators.required]],
port: [1883, [Validators.required, Validators.min(1), Validators.max(65535), Validators.pattern(/^-?[0-9]+$/)]],
remoteConfiguration: [true],
caCertPath: ['/etc/thingsboard-gateway/ca.pem'],
privateKeyPath: ['/etc/thingsboard-gateway/privateKey.pem'],
certPath: ['/etc/thingsboard-gateway/certificate.pem'],
remoteLoggingLevel: [GatewayLogLevel.debug],
remoteLoggingPathToLogs: ['./logs/', [Validators.required]],
storageType: [StorageType.memory],
readRecordsCount: [100, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
maxRecordsCount: [10000, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
maxFilesCount: [5, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
dataFolderPath: ['./data/', [Validators.required]],
connectors: this.fb.array([])
});
this.subscribeStorageType$ = this.getFormField('storageType').valueChanges.subscribe((value: StorageType) => {
if (value === StorageType.memory) {
this.getFormField('maxFilesCount').disable();
this.getFormField('dataFolderPath').disable();
} else {
this.getFormField('maxFilesCount').enable();
this.getFormField('dataFolderPath').enable();
}
});
this.subscribeGateway$ = this.getFormField('gateway').valueChanges.subscribe((gatewayId: string) => {
if (gatewayId !== null) {
forkJoin([
this.deviceService.getDeviceCredentials(gatewayId).pipe(tap(deviceCredential => {
this.getFormField('accessToken').patchValue(deviceCredential.credentialsId);
})),
...this.getAttributes(gatewayId)]).subscribe(() => {
this.gatewayConfigurationGroup.markAsPristine();
this.ctx.detectChanges();
});
} else {
this.getFormField('accessToken').patchValue('');
}
})
}
gatewayExist(): void {
this.ctx.showErrorToast(this.gatewayNameExists, 'top', 'left', this.toastTargetId);
}
exportConfig(): void {
const gatewayConfiguration: GatewayFormModels = this.gatewayConfigurationGroup.value;
const filesZip: any = {};
filesZip['tb_gateway.yaml'] = generateYAMLConfigFile(gatewayConfiguration);
generateConnectorConfigFiles(filesZip, gatewayConfiguration.connectors);
generateLogConfigFile(filesZip, gatewayConfiguration.remoteLoggingLevel, gatewayConfiguration.remoteLoggingPathToLogs);
this.importExport.exportJSZip(filesZip, this.archiveFileName);
this.saveAttribute(
REMOTE_LOGGING_LEVEL_ATTRIBUTE,
this.gatewayConfigurationGroup.value.remoteLoggingLevel.toUpperCase(),
AttributeScope.SHARED_SCOPE);
}
addNewConnector(): void {
this.createConnector();
}
removeConnector(index: number): void {
if (index > -1) {
this.connectors.removeAt(index);
this.connectors.markAsDirty();
}
}
openConfigDialog($event: Event, index: number, config: object, type: string): void {
if ($event) {
$event.stopPropagation();
$event.preventDefault();
}
this.dialog.open<JsonObjectEditDialogComponent, JsonObjectEditDialogData, object>(JsonObjectEditDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
jsonValue: config,
title: this.translate.instant('gateway.title-connectors-json', {typeName: type})
}
}).afterClosed().subscribe(
(res) => {
if (res) {
this.connectors.at(index).get('config').patchValue(res);
this.ctx.detectChanges();
}
}
);
}
private createConnectorName(connectors: GatewayFormConnectorModel[], name: string, index: number = 0): string {
const newKeyName = index ? name + index : name;
const indexRes = connectors.findIndex((element) => element.name === newKeyName);
return indexRes === -1 ? newKeyName : this.createConnectorName(connectors, name, ++index);
}
private validateConnectorName(connectors: GatewayFormConnectorModel[], name: string, connectorIndex: number, index = 0): string {
for (let i = 0; i < connectors.length; i++) {
const nameEq = (index === 0) ? name : name + index;
if (i !== connectorIndex && connectors[i].name === nameEq) {
this.validateConnectorName(connectors, name, connectorIndex, ++index);
}
}
return (index === 0) ? name : name + index;
}
changeConnectorType(connector: AbstractControl): void {
if (!connector.get('name').value) {
const typeConnector = connector.get('configType').value;
const connectors = this.gatewayConfigurationGroup.value.connectors;
connector.get('name').patchValue(this.createConnectorName(connectors, ConnectorType[typeConnector]));
}
}
changeConnectorName(connector: AbstractControl, index: number): void {
const connectors = this.gatewayConfigurationGroup.value.connectors;
connector.get('name').patchValue(this.validateConnectorName(connectors, connector.get('name').value, index));
}
save(): void {
const gatewayConfiguration: GatewayFormModels = this.gatewayConfigurationGroup.value;
forkJoin([
this.saveAttribute(
CONFIGURATION_ATTRIBUTE,
window.btoa(JSON.stringify(gatewayConfigJSON(gatewayConfiguration))),
AttributeScope.SHARED_SCOPE),
this.saveAttribute(
CONFIGURATION_DRAFT_ATTRIBUTE,
window.btoa(JSON.stringify(getDraftConnectorsJSON(gatewayConfiguration.connectors))),
AttributeScope.SERVER_SCOPE),
this.saveAttribute(
REMOTE_LOGGING_LEVEL_ATTRIBUTE,
this.gatewayConfigurationGroup.value.remoteLoggingLevel.toUpperCase(),
AttributeScope.SHARED_SCOPE)
]).subscribe(() =>{
this.ctx.showSuccessToast(this.successfulSaved,
2000, 'top', 'left', this.toastTargetId);
this.gatewayConfigurationGroup.markAsPristine();
})
}
private getAttributes(gatewayId: string): Array<Observable<Array<AttributeData>>> {
const tasks = [];
tasks.push(forkJoin([this.getAttribute(CURRENT_CONFIGURATION_ATTRIBUTE, AttributeScope.CLIENT_SCOPE, gatewayId),
this.getAttribute(CONFIGURATION_DRAFT_ATTRIBUTE, AttributeScope.SERVER_SCOPE, gatewayId)]).pipe(
tap(([currentConfig, draftConfig]) => {
this.setFormGatewaySettings(currentConfig);
this.setFormConnectorsDraft(draftConfig);
})
)
);
tasks.push(this.getAttribute(REMOTE_LOGGING_LEVEL_ATTRIBUTE, AttributeScope.SHARED_SCOPE, gatewayId).pipe(
tap(logsLevel => this.processLoggingLevel(logsLevel))
));
return tasks;
}
private getAttribute(attributeName: string, attributeScope: AttributeScope, gatewayId: string): Observable<Array<AttributeData>> {
return this.attributeService.getEntityAttributes(getEntityId(gatewayId), attributeScope, [attributeName]);
}
private setFormGatewaySettings(response: Array<AttributeData>): void {
this.connectors.clear();
if (response.length > 0) {
const attribute = JSON.parse(window.atob(response[0].value));
for (const attributeKey of Object.keys(attribute)) {
const keyValue = attribute[attributeKey];
if (attributeKey === 'thingsboard') {
if (keyValue !== null && Object.keys(keyValue).length > 0) {
this.gatewayConfigurationGroup.patchValue(createFormConfig(keyValue));
}
} else {
for (const connector of Object.keys(keyValue)) {
let name = 'No name';
if (Object.prototype.hasOwnProperty.call(keyValue[connector], 'name')) {
name = keyValue[connector].name;
}
const newConnector: GatewayFormConnectorModel = {
enabled: true,
configType: (attributeKey as ConnectorType),
config: keyValue[connector].config,
name
};
this.createConnector(newConnector);
}
}
}
}
}
private setFormConnectorsDraft(response: Array<AttributeData>): void {
if (response.length > 0) {
const attribute = JSON.parse(window.atob(response[0].value));
for (const connectorName of Object.keys(attribute)) {
const newConnector: GatewayFormConnectorModel = {
enabled: false,
configType: (attribute[connectorName].connector as ConnectorType),
config: attribute[connectorName].config,
name: connectorName
};
this.createConnector(newConnector);
}
}
}
private processLoggingLevel(value: Array<AttributeData>): void {
let logsLevel = GatewayLogLevel.debug;
if (value.length > 0 && GatewayLogLevel[value[0].value.toLowerCase()]) {
logsLevel = GatewayLogLevel[value[0].value.toLowerCase()];
}
this.getFormField('remoteLoggingLevel').patchValue(logsLevel);
}
}

374
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-form.models.ts

@ -0,0 +1,374 @@
///
/// Copyright © 2016-2020 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { AbstractControl, ValidationErrors } from '@angular/forms';
import { EntityId } from '@shared/models/id/entity-id';
import { EntityType } from '@shared/models/entity-type.models';
export enum SecurityType {
tls = 'tls',
accessToken = 'accessToken'
}
export interface WidgetSetting {
widgetTitle: string;
archiveFileName: string;
gatewayType: string;
successfulSave: string;
gatewayNameExists: string;
}
export const CURRENT_CONFIGURATION_ATTRIBUTE = 'current_configuration';
export const CONFIGURATION_DRAFT_ATTRIBUTE = 'configuration_drafts';
export const CONFIGURATION_ATTRIBUTE = 'configuration';
export const REMOTE_LOGGING_LEVEL_ATTRIBUTE = 'RemoteLoggingLevel';
export const SecurityTypeTranslationMap = new Map<SecurityType, string>(
[
[SecurityType.tls, 'gateway.security-types.tls'],
[SecurityType.accessToken, 'gateway.security-types.access-token']
]
);
export enum GatewayLogLevel {
none = 'NONE',
critical = 'CRITICAL',
error = 'ERROR',
warning = 'WARNING',
info = 'INFO',
debug = 'DEBUG'
}
export enum StorageType {
memory = 'memory',
file = 'file'
}
export const StorageTypeTranslationMap = new Map<StorageType, string>(
[
[StorageType.memory, 'gateway.storage-types.memory-storage'],
[StorageType.file, 'gateway.storage-types.file-storage']
]
);
export enum ConnectorType {
mqtt= 'MQTT',
modbus = 'Modbus',
opc_ua = 'OPC-UA',
ble = 'BLE'
}
export interface GatewayFormModels {
gateway?: string;
accessToken?: string;
securityType?: SecurityType;
host?: string;
port?: number;
remoteConfiguration?: boolean;
caCertPath?: string;
privateKeyPath?: string;
certPath?: string;
remoteLoggingLevel?: GatewayLogLevel;
remoteLoggingPathToLogs?:string;
storageType?: StorageType;
readRecordsCount?: number;
maxRecordsCount?: number;
maxFilesCount?: number;
dataFolderPath?: number;
connectors?: Array<GatewayFormConnectorModel>;
}
export interface GatewayFormConnectorModel {
config: object;
name: string;
configType: ConnectorType;
enabled: boolean;
}
export const DEFAULT_CONNECTOR: GatewayFormConnectorModel = {
config: {},
name: '',
configType: null,
enabled: false
};
type Connector = {
[key in ConnectorType]?: Array<ConnectorConfig>;
}
interface GatewaySetting extends Connector{
thingsboard: GatewayMainSetting;
}
interface ConnectorConfig {
name: string;
config: object;
}
interface GatewayMainSetting {
thingsboard: GatewayMainThingsboardSetting;
connectors: Array<GatewayMainConnector>,
logs: string,
storage: GatewayStorage
}
interface GatewayMainThingsboardSetting {
host: string,
remoteConfiguration: boolean,
port: number,
security: GatewaySecurity
}
type GatewaySecurity = SecurityToken | SecurityCertificate;
interface SecurityToken {
accessToken: string;
}
interface SecurityCertificate {
caCert: string;
privateKey: string;
cert: string;
}
type GatewayStorage = GatewayStorageMemory | GatewayStorageFile;
interface GatewayStorageMemory {
type: string;
max_records_count: number;
read_records_count: number;
}
interface GatewayStorageFile {
type: string;
data_folder_path: number;
max_file_count: number;
max_read_records_count: number;
max_records_per_file: number;
}
interface GatewayMainConnector {
configuration: string;
name: string;
type: ConnectorType;
}
export function ValidateJSON(control: AbstractControl): ValidationErrors | null {
if (JSON.stringify(control.value) === JSON.stringify({})) {
return { validJSON: true };
}
return null;
}
const TEMPLATE_LOGS_CONFIG = '[loggers]}}keys=root, service, connector, converter, tb_connection, storage, extension}}[handlers]}}keys=consoleHandler, serviceHandler, connectorHandler, converterHandler, tb_connectionHandler, storageHandler, extensionHandler}}[formatters]}}keys=LogFormatter}}[logger_root]}}level=ERROR}}handlers=consoleHandler}}[logger_connector]}}level={ERROR}}}handlers=connectorHandler}}formatter=LogFormatter}}qualname=connector}}[logger_storage]}}level={ERROR}}}handlers=storageHandler}}formatter=LogFormatter}}qualname=storage}}[logger_tb_connection]}}level={ERROR}}}handlers=tb_connectionHandler}}formatter=LogFormatter}}qualname=tb_connection}}[logger_service]}}level={ERROR}}}handlers=serviceHandler}}formatter=LogFormatter}}qualname=service}}[logger_converter]}}level={ERROR}}}handlers=connectorHandler}}formatter=LogFormatter}}qualname=converter}}[logger_extension]}}level={ERROR}}}handlers=connectorHandler}}formatter=LogFormatter}}qualname=extension}}[handler_consoleHandler]}}class=StreamHandler}}level={ERROR}}}formatter=LogFormatter}}args=(sys.stdout,)}}[handler_connectorHandler]}}level={ERROR}}}class=logging.handlers.TimedRotatingFileHandler}}formatter=LogFormatter}}args=("{./logs/}connector.log", "d", 1, 7,)}}[handler_storageHandler]}}level={ERROR}}}class=logging.handlers.TimedRotatingFileHandler}}formatter=LogFormatter}}args=("{./logs/}storage.log", "d", 1, 7,)}}[handler_serviceHandler]}}level={ERROR}}}class=logging.handlers.TimedRotatingFileHandler}}formatter=LogFormatter}}args=("{./logs/}service.log", "d", 1, 7,)}}[handler_converterHandler]}}level={ERROR}}}class=logging.handlers.TimedRotatingFileHandler}}formatter=LogFormatter}}args=("{./logs/}converter.log", "d", 1, 3,)}}[handler_extensionHandler]}}level={ERROR}}}class=logging.handlers.TimedRotatingFileHandler}}formatter=LogFormatter}}args=("{./logs/}extension.log", "d", 1, 3,)}}[handler_tb_connectionHandler]}}level={ERROR}}}class=logging.handlers.TimedRotatingFileHandler}}formatter=LogFormatter}}args=("{./logs/}tb_connection.log", "d", 1, 3,)}}[formatter_LogFormatter]}}format="%(asctime)s - %(levelname)s - [%(filename)s] - %(module)s - %(lineno)d - %(message)s" }}datefmt="%Y-%m-%d %H:%M:%S"';
export function generateYAMLConfigFile(gatewaySetting: GatewayFormModels): string {
let config;
config = 'thingsboard:\n';
config += ' host: ' + gatewaySetting.host + '\n';
config += ' remoteConfiguration: ' + gatewaySetting.remoteConfiguration + '\n';
config += ' port: ' + gatewaySetting.port + '\n';
config += ' security:\n';
if (gatewaySetting.securityType === SecurityType.accessToken) {
config += ' access-token: ' + gatewaySetting.accessToken + '\n';
} else {
config += ' ca_cert: ' + gatewaySetting.caCertPath + '\n';
config += ' privateKey: ' + gatewaySetting.privateKeyPath + '\n';
config += ' cert: ' + gatewaySetting.certPath + '\n';
}
config += 'storage:\n';
if (gatewaySetting.storageType === StorageType.memory) {
config += ' type: memory\n';
config += ' read_records_count: ' + gatewaySetting.readRecordsCount + '\n';
config += ' max_records_count: ' + gatewaySetting.maxRecordsCount + '\n';
} else {
config += ' type: file\n';
config += ' data_folder_path: ' + gatewaySetting.dataFolderPath + '\n';
config += ' max_file_count: ' + gatewaySetting.maxFilesCount + '\n';
config += ' max_read_records_count: ' + gatewaySetting.readRecordsCount + '\n';
config += ' max_records_per_file: ' + gatewaySetting.maxRecordsCount + '\n';
}
config += 'connectors:\n';
for(const connector of gatewaySetting.connectors){
if (connector.enabled) {
config += ' -\n';
config += ' name: ' + connector.name + '\n';
config += ' type: ' + connector.configType + '\n';
config += ' configuration: ' + generateFileName(connector.name) + '\n';
}
}
return config;
}
export function generateConnectorConfigFiles(fileZipAdd: object, connectors: Array<GatewayFormConnectorModel>): void {
for(const connector of connectors) {
if (connector.enabled) {
fileZipAdd[generateFileName(connector.name)] = JSON.stringify(connector.config);
}
}
}
function generateFileName(fileName): string {
return fileName.replace('_', '')
.replace('-', '')
.replace(/^\s+|\s+/g, '')
.toLowerCase() + '.json';
}
export function generateLogConfigFile(fileZipAdd: object, logsLevel: string, logsPath: string): void {
fileZipAdd['logs.conf'] = getLogsConfig(logsLevel, logsPath);
}
function getLogsConfig(logsLevel: string, logsPath: string): string {
return TEMPLATE_LOGS_CONFIG
.replace(/{ERROR}/g, logsLevel)
.replace(/{.\/logs\/}/g, logsPath);
}
export function getEntityId(gatewayId: string): EntityId {
return {
id: gatewayId,
entityType: EntityType.DEVICE
}
}
export function createFormConfig(keyValue: GatewayMainSetting): GatewayFormModels {
const formSetting: GatewayFormModels = {};
if (Object.prototype.hasOwnProperty.call(keyValue, 'thingsboard')) {
formSetting.host = keyValue.thingsboard.host;
formSetting.port = keyValue.thingsboard.port;
formSetting.remoteConfiguration = keyValue.thingsboard.remoteConfiguration;
if (Object.prototype.hasOwnProperty.call(keyValue.thingsboard.security, SecurityType.accessToken)) {
formSetting.securityType = SecurityType.accessToken;
formSetting.accessToken = (keyValue.thingsboard.security as SecurityToken).accessToken;
} else {
formSetting.securityType = SecurityType.tls;
formSetting.caCertPath = (keyValue.thingsboard.security as SecurityCertificate).caCert;
formSetting.privateKeyPath = (keyValue.thingsboard.security as SecurityCertificate).privateKey;
formSetting.certPath = (keyValue.thingsboard.security as SecurityCertificate).cert;
}
}
if (Object.prototype.hasOwnProperty.call(keyValue, 'storage') && Object.prototype.hasOwnProperty.call(keyValue.storage, 'type')) {
if (keyValue.storage.type === StorageType.memory) {
formSetting.storageType = StorageType.memory;
formSetting.readRecordsCount = (keyValue.storage as GatewayStorageMemory).read_records_count;
formSetting.maxRecordsCount = (keyValue.storage as GatewayStorageMemory).max_records_count;
} else if (keyValue.storage.type === StorageType.file) {
formSetting.storageType = StorageType.file;
formSetting.dataFolderPath = (keyValue.storage as GatewayStorageFile).data_folder_path;
formSetting.maxFilesCount = (keyValue.storage as GatewayStorageFile).max_file_count;
formSetting.readRecordsCount = (keyValue.storage as GatewayStorageMemory).read_records_count;
formSetting.maxRecordsCount = (keyValue.storage as GatewayStorageMemory).max_records_count;
}
}
return formSetting;
}
export function getDraftConnectorsJSON(currentConnectors: Array<GatewayFormConnectorModel>) {
const draftConnectors = {};
for(const connector of currentConnectors){
if (!connector.enabled) {
draftConnectors[connector.name] = {
connector: connector.configType,
config: connector.config
};
}
}
return draftConnectors;
}
export function gatewayConfigJSON(gatewayConfiguration: GatewayFormModels): GatewaySetting {
const gatewayConfig = {
thingsboard: gatewayMainConfigJSON(gatewayConfiguration)
};
gatewayConnectorJSON(gatewayConfig, gatewayConfiguration.connectors);
return gatewayConfig;
}
function gatewayMainConfigJSON(gatewayConfiguration: GatewayFormModels): GatewayMainSetting {
let security: GatewaySecurity;
if (gatewayConfiguration.securityType === SecurityType.accessToken) {
security = {
accessToken: gatewayConfiguration.accessToken
}
} else {
security = {
caCert: gatewayConfiguration.caCertPath,
privateKey: gatewayConfiguration.privateKeyPath,
cert: gatewayConfiguration.certPath
}
}
const thingsboard: GatewayMainThingsboardSetting = {
host: gatewayConfiguration.host,
remoteConfiguration: gatewayConfiguration.remoteConfiguration,
port: gatewayConfiguration.port,
security
};
let storage: GatewayStorage;
if (gatewayConfiguration.storageType === StorageType.memory) {
storage = {
type: StorageType.memory,
read_records_count: gatewayConfiguration.readRecordsCount,
max_records_count: gatewayConfiguration.maxRecordsCount
};
} else {
storage = {
type: StorageType.file,
data_folder_path: gatewayConfiguration.dataFolderPath,
max_file_count: gatewayConfiguration.maxFilesCount,
max_read_records_count: gatewayConfiguration.readRecordsCount,
max_records_per_file: gatewayConfiguration.maxRecordsCount
};
}
const connectors: Array<GatewayMainConnector> = [];
for (const connector of gatewayConfiguration.connectors) {
if (connector.enabled) {
const connectorConfig: GatewayMainConnector = {
configuration: generateFileName(connector.name),
name: connector.name,
type: connector.configType
};
connectors.push(connectorConfig);
}
}
return {
thingsboard,
connectors,
storage,
logs: window.btoa(getLogsConfig(gatewayConfiguration.remoteLoggingLevel, gatewayConfiguration.remoteLoggingPathToLogs))
}
}
function gatewayConnectorJSON(gatewayConfiguration, currentConnectors: Array<GatewayFormConnectorModel>): void {
for (const connector of currentConnectors) {
if (connector.enabled) {
const typeConnector = connector.configType;
if (!Array.isArray(gatewayConfiguration[typeConnector])) {
gatewayConfiguration[typeConnector] = [];
}
const connectorConfig: ConnectorConfig = {
name: connector.name,
config: connector.config
};
gatewayConfiguration[typeConnector].push(connectorConfig);
}
}
}

11
ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts

@ -33,6 +33,8 @@ import {
import { MultipleInputWidgetComponent } from './lib/multiple-input-widget.component';
import { TripAnimationComponent } from './trip-animation/trip-animation.component';
import { WebCameraInputWidgetComponent } from './lib/web-camera-input.component';
import { GatewayFormComponent } from './lib/gateway/gateway-form.component';
import { ImportExportService } from '@home/components/import-export/import-export.service';
@NgModule({
declarations:
@ -47,7 +49,8 @@ import { WebCameraInputWidgetComponent } from './lib/web-camera-input.component'
DateRangeNavigatorPanelComponent,
MultipleInputWidgetComponent,
TripAnimationComponent,
WebCameraInputWidgetComponent
WebCameraInputWidgetComponent,
GatewayFormComponent
],
imports: [
CommonModule,
@ -64,10 +67,12 @@ import { WebCameraInputWidgetComponent } from './lib/web-camera-input.component'
DateRangeNavigatorWidgetComponent,
MultipleInputWidgetComponent,
TripAnimationComponent,
WebCameraInputWidgetComponent
WebCameraInputWidgetComponent,
GatewayFormComponent
],
providers: [
CustomDialogService
CustomDialogService,
ImportExportService
]
})
export class WidgetComponentsModule { }

1
ui-ngx/src/app/modules/home/components/widget/widget.component.ts

@ -706,6 +706,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
this.dynamicWidgetComponentRef = this.widgetContentContainer.createComponent(this.widgetInfo.componentFactory, 0, injector);
this.cd.detectChanges();
} catch (e) {
console.error(e);
if (this.dynamicWidgetComponentRef) {
this.dynamicWidgetComponentRef.destroy();
this.dynamicWidgetComponentRef = null;

2
ui-ngx/src/app/shared/components/dialog/json-object-edit-dialog.component.html

@ -17,7 +17,7 @@
-->
<form [formGroup]="jsonFormGroup" (ngSubmit)="add()" style="min-width: 400px;">
<mat-toolbar fxLayout="row" color="primary">
<h2>{{ (this.data.title ? this.data.title : 'details.edit-json') | translate }}</h2>
<h2>{{ title }}</h2>
<span fxFlex></span>
<button mat-button mat-icon-button
(click)="cancel()"

33
ui-ngx/src/app/shared/components/dialog/json-object-edit-dialog.component.ts

@ -14,16 +14,17 @@
/// limitations under the License.
///
import {Component, Inject, OnInit} from '@angular/core';
import {DialogComponent} from '@shared/components/dialog.component';
import {Store} from '@ngrx/store';
import {AppState} from '@core/core.state';
import {Router} from '@angular/router';
import {MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog';
import {FormBuilder, FormGroup} from '@angular/forms';
export interface JsonObjectEdittDialogData {
jsonValue: Object;
import { Component, Inject, OnInit } from '@angular/core';
import { DialogComponent } from '@shared/components/dialog.component';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { Router } from '@angular/router';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { FormBuilder, FormGroup } from '@angular/forms';
import { TranslateService } from '@ngx-translate/core';
export interface JsonObjectEditDialogData {
jsonValue: object;
title?: string;
}
@ -32,22 +33,24 @@ export interface JsonObjectEdittDialogData {
templateUrl: './json-object-edit-dialog.component.html',
styleUrls: []
})
export class JsonObjectEditDialogComponent extends DialogComponent<JsonObjectEditDialogComponent, Object>
implements OnInit {
export class JsonObjectEditDialogComponent extends DialogComponent<JsonObjectEditDialogComponent, object> implements OnInit {
jsonFormGroup: FormGroup;
title: string;
submitted = false;
constructor(protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: JsonObjectEdittDialogData,
public dialogRef: MatDialogRef<JsonObjectEditDialogComponent, Object>,
public fb: FormBuilder) {
@Inject(MAT_DIALOG_DATA) public data: JsonObjectEditDialogData,
public dialogRef: MatDialogRef<JsonObjectEditDialogComponent, object>,
public fb: FormBuilder,
private translate: TranslateService) {
super(store, router, dialogRef);
}
ngOnInit(): void {
this.title = this.data.title ? this.data.title : this.translate.instant('details.edit-json');
this.jsonFormGroup = this.fb.group({
json: [this.data.jsonValue, []]
});

56
ui-ngx/src/app/shared/components/entity/entity-gateway-select.component.html

@ -0,0 +1,56 @@
<!--
Copyright © 2016-2020 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<mat-form-field [formGroup]="selectDeviceGatewayFormGroup" class="mat-block">
<input matInput type="text" #deviceGatewayInput placeholder="{{ 'gateway.gateway-name' | translate }}"
formControlName="gateway"
(focusin)="onFocus()"
[required]="required"
(keydown)="gatewayNameEnter($event)"
(keypress)="gatewayNameEnter($event)"
[matAutocomplete]="gatewayAutocomplete"
>
<button *ngIf="selectDeviceGatewayFormGroup.get('gateway').value" (click)="clear()"
type="button" matSuffix mat-icon-button aria-label="Clear">
<mat-icon class="material-icons">close</mat-icon>
</button>
<mat-autocomplete class="tb-autocomplete"
#gatewayAutocomplete="matAutocomplete"
[displayWith]="displayGatewayFn">
<mat-option *ngFor="let gateway of filteredGateways | async" [value]="gateway">
<span [innerHTML]="gateway.name | highlight:searchText"></span>
</mat-option>
<mat-option *ngIf="!(filteredGateways | async)?.length" [value]="null" class="tb-not-found">
<div class="tb-not-found-content" (click)="$event.stopPropagation()">
<div *ngIf="!textIsNotEmpty(searchText); else searchNotEmpty">
<span translate>gateway.no-gateway-found</span>
</div>
<ng-template #searchNotEmpty>
<span translate [translateParams]="{item: searchText | truncate:true:6:&apos;...&apos;}">
gateway.no-gateway-matching
</span>
</ng-template>
<span>
<a translate (click)="createGateway($event, searchText)">gateway.create-new-gateway</a>
</span>
</div>
</mat-option>
</mat-autocomplete>
<mat-error *ngIf="selectDeviceGatewayFormGroup.get('gateway').hasError('required')">
{{ 'gateway.gateway-name-required' | translate }}
</mat-error>
</mat-form-field>

223
ui-ngx/src/app/shared/components/entity/entity-gateway-select.component.ts

@ -0,0 +1,223 @@
///
/// Copyright © 2016-2020 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, ElementRef, EventEmitter, forwardRef, Input, OnInit, Output, ViewChild } from '@angular/core';
import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { ENTER } from '@angular/cdk/keycodes';
import { Observable, of } from 'rxjs';
import { filter, map, mergeMap, reduce, share, switchMap, tap } from 'rxjs/operators';
import { EntityService } from '@core/http/entity.service';
import { EntityType } from '@shared/models/entity-type.models';
import { Device } from '@shared/models/device.models';
import { DialogService } from '@core/services/dialog.service';
import { TranslateService } from '@ngx-translate/core';
import { DeviceService } from '@core/http/device.service';
@Component({
selector: 'tb-entity-gateway-select',
templateUrl: './entity-gateway-select.component.html',
styleUrls: [],
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => EntityGatewaySelectComponent),
multi: true
}]
})
export class EntityGatewaySelectComponent implements ControlValueAccessor, OnInit {
get required(): boolean {
return this.requiredValue;
}
@Input()
set required(value: boolean) {
this.requiredValue = coerceBooleanProperty(value);
}
@Input()
set newGatewayType(value: string){
this.gatewayType = value;
}
@Output()
private gatewayNameExist = new EventEmitter();
constructor(private store: Store<AppState>,
private entityService: EntityService,
private dialogService: DialogService,
private deviceService: DeviceService,
private translate: TranslateService,
private fb: FormBuilder) {
this.loadGatewayList();
this.selectDeviceGatewayFormGroup = this.fb.group({
gateway: [null]
});
}
private gatewayType = 'Gateway';
private dirty: boolean;
private requiredValue: boolean;
private gatewayList: Array<Device>;
searchText = '';
filteredGateways: Observable<Array<Device>>;
selectDeviceGatewayFormGroup: FormGroup;
modelValue: string | null;
@ViewChild('deviceGatewayInput', {static: true}) deviceGatewayInput: ElementRef<HTMLInputElement>;
private propagateChange = (v: any) => { };
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
ngOnInit() {
this.filteredGateways = this.selectDeviceGatewayFormGroup.get('gateway').valueChanges
.pipe(
tap(value => {
let modelValue;
if (typeof value === 'string' || !value) {
modelValue = null;
} else {
modelValue = value.id.id;
}
this.updateView(modelValue);
if (value === null) {
this.clear();
}
}),
map(value => value ? (typeof value === 'string' ? value : value.name) : ''),
mergeMap(name => this.fetchGateway(name) ),
share()
);
}
fetchGateway(searchText?: string): Observable<Array<Device>> {
this.searchText = searchText;
let result = [];
if (searchText && searchText.length) {
result = this.gatewayList.filter((gateway) => gateway.name.toLowerCase().includes(searchText.toLowerCase()));
} else {
result = this.gatewayList;
}
return of(result);
}
onFocus() {
if (this.dirty) {
this.selectDeviceGatewayFormGroup.get('gateway').updateValueAndValidity({onlySelf: true, emitEvent: true});
this.dirty = false;
}
}
displayGatewayFn(gateway?: Device): string | undefined {
return gateway ? gateway.name : undefined;
}
setDisabledState(isDisabled: boolean): void {
}
writeValue(value: string | null): void {
if(value === null){
this.searchText = '';
this.selectDeviceGatewayFormGroup.get('gateway').patchValue('', {emitEvent: false});
this.dirty = true;
}
}
clear(value: string = '', hideList?: boolean) {
this.searchText = value;
this.selectDeviceGatewayFormGroup.get('gateway').patchValue(value, {emitEvent: true});
if(!hideList) {
setTimeout(() => {
this.deviceGatewayInput.nativeElement.blur();
this.deviceGatewayInput.nativeElement.focus();
}, 0);
}
}
textIsNotEmpty(text: string): boolean {
return !!text && text.length > 0;
}
gatewayNameEnter($event: KeyboardEvent) {
if ($event.keyCode === ENTER) {
if (!this.modelValue) {
this.createGateway($event, this.searchText);
}
}
}
createGateway($event: Event, gatewayName: string) {
$event.preventDefault();
$event.stopPropagation();
const title = this.translate.instant('gateway.create-new-gateway');
const content = this.translate.instant('gateway.create-new-gateway-text', {gatewayName});
this.dialogService.confirm(title, content, null, null, true).subscribe(value => {
if(value){
this.createDeviceGateway(gatewayName);
} else {
this.clear('', true);
}
});
}
private createDeviceGateway(gatewayName: string){
this.deviceService.findByName(gatewayName, {ignoreErrors: true}).subscribe(value => {
this.gatewayNameExist.emit(gatewayName)
}, () => {
const newGateway: Device = {
name: gatewayName,
label: null,
type: this.gatewayType,
additionalInfo: {
gateway: true
}
};
this.deviceService.saveDevice(newGateway).subscribe(
(device) => {
this.searchText = '';
this.gatewayList.push(device);
this.selectDeviceGatewayFormGroup.get('gateway').patchValue(device, {emitEvent: true});
}
);
})
}
private loadGatewayList(): void{
this.entityService.getEntitiesByNameFilter(EntityType.DEVICE, '', -1).pipe(
switchMap(results => results),
filter((device) => (device as Device)?.additionalInfo?.gateway),
reduce((acc, val) => acc.concat(val), [])
).subscribe((devices) => {
this.gatewayList = devices;
if(!this.searchText && this.gatewayList.length){
this.selectDeviceGatewayFormGroup.get('gateway').patchValue(this.gatewayList[0], {emitEvent: true});
}
})
}
private updateView(modelValue: any) {
this.propagateChange(modelValue);
}
}

4
ui-ngx/src/app/shared/components/value-input.component.ts

@ -21,7 +21,7 @@ import { isObject } from '@core/utils';
import { MatDialog } from '@angular/material/dialog';
import {
JsonObjectEditDialogComponent,
JsonObjectEdittDialogData
JsonObjectEditDialogData
} from '@shared/components/dialog/json-object-edit-dialog.component';
@Component({
@ -69,7 +69,7 @@ export class ValueInputComponent implements OnInit, ControlValueAccessor {
if ($event) {
$event.stopPropagation();
}
this.dialog.open<JsonObjectEditDialogComponent, JsonObjectEdittDialogData, Object>(JsonObjectEditDialogComponent, {
this.dialog.open<JsonObjectEditDialogComponent, JsonObjectEditDialogData, object>(JsonObjectEditDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {

8
ui-ngx/src/app/shared/shared.module.ts

@ -94,7 +94,6 @@ import { EntityListSelectComponent } from '@shared/components/entity/entity-list
import { JsonObjectEditComponent } from '@shared/components/json-object-edit.component';
import { FooterFabButtonsComponent } from '@shared/components/footer-fab-buttons.component';
import { CircularProgressDirective } from '@shared/components/circular-progress.directive';
import { MatSpinner } from '@angular/material/progress-spinner';
import { FabToolbarComponent, FabActionsDirective, FabTriggerDirective } from '@shared/components/fab-toolbar.component';
import { DashboardSelectPanelComponent } from '@shared/components/dashboard-select-panel.component';
import { DashboardSelectComponent } from '@shared/components/dashboard-select.component';
@ -129,6 +128,7 @@ import { TbJsonToStringDirective } from '@shared/components/directives/tb-json-t
import { JsonObjectEditDialogComponent } from '@shared/components/dialog/json-object-edit-dialog.component';
import { HistorySelectorComponent } from './components/time/history-selector/history-selector.component';
import { TbTemplatePipe } from '@shared/pipe/template.pipe';
import { EntityGatewaySelectComponent } from '@shared/components/entity/entity-gateway-select.component';
@NgModule({
providers: [
@ -213,7 +213,8 @@ import { TbTemplatePipe } from '@shared/pipe/template.pipe';
KeyboardShortcutPipe,
TbJsonToStringDirective,
JsonObjectEditDialogComponent,
HistorySelectorComponent
HistorySelectorComponent,
EntityGatewaySelectComponent
],
imports: [
CommonModule,
@ -373,7 +374,8 @@ import { TbTemplatePipe } from '@shared/pipe/template.pipe';
KeyboardShortcutPipe,
TranslateModule,
JsonObjectEditDialogComponent,
HistorySelectorComponent
HistorySelectorComponent,
EntityGatewaySelectComponent
]
})
export class SharedModule { }

3
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -53,7 +53,8 @@
"export": "Export",
"share-via": "Share via {{provider}}",
"continue": "Continue",
"discard-changes": "Discard Changes"
"discard-changes": "Discard Changes",
"download": "Download"
},
"aggregation": {
"aggregation": "Aggregation",

5
ui-ngx/tsconfig.json

@ -30,7 +30,10 @@
"@core/*": ["src/app/core/*"],
"@modules/*": ["src/app/modules/*"],
"@shared/*": ["src/app/shared/*"],
"@home/*": ["src/app/modules/home/*"]
"@home/*": ["src/app/modules/home/*"],
"jszip": [
"node_modules/jszip/dist/jszip.min.js"
]
},
"lib": [
"es2018",

Loading…
Cancel
Save