Browse Source

CE pre PR comit from PE

pull/8337/head
Maksym Dudnik 4 years ago
parent
commit
9497f28535
  1. 45
      application/src/main/data/json/system/widget_bundles/gateway_widgets.json
  2. 7
      ui-ngx/src/app/core/utils.ts
  3. 9
      ui-ngx/src/app/modules/common/modules-map.ts
  4. 41
      ui-ngx/src/app/modules/home/components/device/device-credentials.module.ts
  5. 101
      ui-ngx/src/app/modules/home/components/device/device-example-command.component.html
  6. 56
      ui-ngx/src/app/modules/home/components/device/device-example-command.component.scss
  7. 113
      ui-ngx/src/app/modules/home/components/device/device-example-command.component.ts
  8. 55
      ui-ngx/src/app/modules/home/components/device/device-gateway-command.component.html
  9. 116
      ui-ngx/src/app/modules/home/components/device/device-gateway-command.component.ts
  10. 41
      ui-ngx/src/app/modules/home/components/gateway/gateway-command-dialog.component.html
  11. 58
      ui-ngx/src/app/modules/home/components/gateway/gateway-command-dialog.component.ts
  12. 130
      ui-ngx/src/app/modules/home/components/gateway/gateway-list-table-config.ts
  13. 18
      ui-ngx/src/app/modules/home/components/gateway/gateway-list.component.html
  14. 40
      ui-ngx/src/app/modules/home/components/gateway/gateway-list.component.scss
  15. 63
      ui-ngx/src/app/modules/home/components/gateway/gateway-list.component.ts
  16. 3
      ui-ngx/src/app/modules/home/components/home-components.module.ts
  17. 3
      ui-ngx/src/app/modules/home/components/shared-home-components.module.ts
  18. 15
      ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.html
  19. 4
      ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.scss
  20. 41
      ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.ts
  21. 70
      ui-ngx/src/app/shared/components/device/device-gateway-command.component.html
  22. 125
      ui-ngx/src/app/shared/components/device/device-gateway-command.component.ts
  23. 683
      ui-ngx/src/app/shared/components/device/gateway-configuration.component.html
  24. 166
      ui-ngx/src/app/shared/components/device/gateway-configuration.component.scss
  25. 521
      ui-ngx/src/app/shared/components/device/gateway-configuration.component.ts
  26. 143
      ui-ngx/src/app/shared/components/device/gateway-connectors.component.html
  27. 73
      ui-ngx/src/app/shared/components/device/gateway-connectors.component.scss
  28. 195
      ui-ngx/src/app/shared/components/device/gateway-connectors.component.ts
  29. 72
      ui-ngx/src/app/shared/components/dialog/gateway-remote-configuration-dialog.html
  30. 76
      ui-ngx/src/app/shared/components/dialog/gateway-remote-configuration-dialog.ts
  31. 17
      ui-ngx/src/app/shared/shared.module.ts
  32. 163
      ui-ngx/src/assets/locale/locale.constant-en_US.json

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

File diff suppressed because one or more lines are too long

7
ui-ngx/src/app/core/utils.ts

@ -205,6 +205,13 @@ export function base64toObj(b64Encoded: string): any {
return JSON.parse(json);
}
export function stringToBase64(value: string): string {
return btoa(encodeURIComponent(value).replace(/%([0-9A-F]{2})/g,
function toSolidBytes(match, p1) {
return String.fromCharCode(Number('0x' + p1));
}));
}
const scrollRegex = /(auto|scroll)/;
function parentNodes(node: Node, nodes: Node[]): Node[] {

9
ui-ngx/src/app/modules/common/modules-map.ts

@ -296,6 +296,11 @@ import * as QueueFormComponent from '@home/components/queue/queue-form.component
import * as AssetProfileComponent from '@home/components/profile/asset-profile.component';
import * as AssetProfileDialogComponent from '@home/components/profile/asset-profile-dialog.component';
import * as AssetProfileAutocompleteComponent from '@home/components/profile/asset-profile-autocomplete.component';
import * as DeviceGatewayCommandComponent from '@shared/components/device/device-gateway-command.component';
import * as GatewayRemoteConfigurationDialogComponent from '@shared/components/dialog/gateway-remote-configuration-dialog';
import * as GatewayConfigurationComponent from '@shared/components/device/gateway-configuration.component';
import * as GatewayConnectorComponent from '@shared/components/device/gateway-connectors.component';
import { IModulesMap } from '@modules/common/modules-map.models';
@ -398,6 +403,10 @@ class ModulesMap implements IModulesMap {
'@shared/components/help.component': HelpComponent,
'@shared/components/help-markdown.component': HelpMarkdownComponent,
'@shared/components/help-popup.component': HelpPopupComponent,
'@shared/components/device/device-gateway-command.component': DeviceGatewayCommandComponent,
'@shared/components/device/gateway-configuration.component': GatewayConfigurationComponent,
'@shared/components/device/gateway-connectors.component': GatewayConnectorComponent,
'@shared/components/device/gateway-remote-configuration-dialog': GatewayRemoteConfigurationDialogComponent,
'@shared/components/tb-checkbox.component': TbCheckboxComponent,
'@shared/components/toast.directive': TbToast,
'@shared/components/tb-error.component': TbErrorComponent,

41
ui-ngx/src/app/modules/home/components/device/device-credentials.module.ts

@ -1,17 +1,32 @@
///
/// Copyright © 2016-2023 The Thingsboard Authors
/// ThingsBoard, Inc. ("COMPANY") CONFIDENTIAL
///
/// 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
/// Copyright © 2016-2023 ThingsBoard, Inc. All Rights Reserved.
///
/// http://www.apache.org/licenses/LICENSE-2.0
/// NOTICE: All information contained herein is, and remains
/// the property of ThingsBoard, Inc. and its suppliers,
/// if any. The intellectual and technical concepts contained
/// herein are proprietary to ThingsBoard, Inc.
/// and its suppliers and may be covered by U.S. and Foreign Patents,
/// patents in process, and are protected by trade secret or copyright law.
///
/// 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.
/// Dissemination of this information or reproduction of this material is strictly forbidden
/// unless prior written permission is obtained from COMPANY.
///
/// Access to the source code contained herein is hereby forbidden to anyone except current COMPANY employees,
/// managers or contractors who have executed Confidentiality and Non-disclosure agreements
/// explicitly covering such access.
///
/// The copyright notice above does not evidence any actual or intended publication
/// or disclosure of this source code, which includes
/// information that is confidential and/or proprietary, and is a trade secret, of COMPANY.
/// ANY REPRODUCTION, MODIFICATION, DISTRIBUTION, PUBLIC PERFORMANCE,
/// OR PUBLIC DISPLAY OF OR THROUGH USE OF THIS SOURCE CODE WITHOUT
/// THE EXPRESS WRITTEN CONSENT OF COMPANY IS STRICTLY PROHIBITED,
/// AND IN VIOLATION OF APPLICABLE LAWS AND INTERNATIONAL TREATIES.
/// THE RECEIPT OR POSSESSION OF THIS SOURCE CODE AND/OR RELATED INFORMATION
/// DOES NOT CONVEY OR IMPLY ANY RIGHTS TO REPRODUCE, DISCLOSE OR DISTRIBUTE ITS CONTENTS,
/// OR TO MANUFACTURE, USE, OR SELL ANYTHING THAT IT MAY DESCRIBE, IN WHOLE OR IN PART.
///
import { NgModule } from '@angular/core';
@ -22,7 +37,7 @@ import { DeviceCredentialsComponent } from '@home/components/device/device-crede
import { DeviceCredentialsLwm2mComponent } from '@home/components/device/device-credentials-lwm2m.component';
import { DeviceCredentialsLwm2mServerComponent } from '@home/components/device/device-credentials-lwm2m-server.component';
import { DeviceCredentialsMqttBasicComponent } from '@home/components/device/device-credentials-mqtt-basic.component';
import {DeviceGatewayCommandComponent} from "@home/components/device/device-gateway-command.component";
import { DeviceExampleCommandComponent } from '@home/components/device/device-example-command.component';
@NgModule({
declarations: [
@ -31,7 +46,7 @@ import {DeviceGatewayCommandComponent} from "@home/components/device/device-gate
DeviceCredentialsLwm2mComponent,
DeviceCredentialsLwm2mServerComponent,
DeviceCredentialsMqttBasicComponent,
DeviceGatewayCommandComponent
DeviceExampleCommandComponent
],
imports: [
CommonModule,
@ -43,7 +58,7 @@ import {DeviceGatewayCommandComponent} from "@home/components/device/device-gate
DeviceCredentialsLwm2mComponent,
DeviceCredentialsLwm2mServerComponent,
DeviceCredentialsMqttBasicComponent,
DeviceGatewayCommandComponent
DeviceExampleCommandComponent
]
})
export class DeviceCredentialsModule { }

101
ui-ngx/src/app/modules/home/components/device/device-example-command.component.html

@ -0,0 +1,101 @@
<!--
ThingsBoard, Inc. ("COMPANY") CONFIDENTIAL
Copyright © 2016-2023 ThingsBoard, Inc. All Rights Reserved.
NOTICE: All information contained herein is, and remains
the property of ThingsBoard, Inc. and its suppliers,
if any. The intellectual and technical concepts contained
herein are proprietary to ThingsBoard, Inc.
and its suppliers and may be covered by U.S. and Foreign Patents,
patents in process, and are protected by trade secret or copyright law.
Dissemination of this information or reproduction of this material is strictly forbidden
unless prior written permission is obtained from COMPANY.
Access to the source code contained herein is hereby forbidden to anyone except current COMPANY employees,
managers or contractors who have executed Confidentiality and Non-disclosure agreements
explicitly covering such access.
The copyright notice above does not evidence any actual or intended publication
or disclosure of this source code, which includes
information that is confidential and/or proprietary, and is a trade secret, of COMPANY.
ANY REPRODUCTION, MODIFICATION, DISTRIBUTION, PUBLIC PERFORMANCE,
OR PUBLIC DISPLAY OF OR THROUGH USE OF THIS SOURCE CODE WITHOUT
THE EXPRESS WRITTEN CONSENT OF COMPANY IS STRICTLY PROHIBITED,
AND IN VIOLATION OF APPLICABLE LAWS AND INTERNATIONAL TREATIES.
THE RECEIPT OR POSSESSION OF THIS SOURCE CODE AND/OR RELATED INFORMATION
DOES NOT CONVEY OR IMPLY ANY RIGHTS TO REPRODUCE, DISCLOSE OR DISTRIBUTE ITS CONTENTS,
OR TO MANUFACTURE, USE, OR SELL ANYTHING THAT IT MAY DESCRIBE, IN WHOLE OR IN PART.
-->
<div mat-dialog-content tb-toast fxLayout="column" toastTarget="dockerCommandDialogContent">
<mat-button-toggle-group [formControl]="protocolCtrl" class="protocol-toggle">
<mat-button-toggle value="mqtt" aria-label="mqtt">
MQTT
</mat-button-toggle>
<mat-button-toggle value="coap" aria-label="coap">
COAP
</mat-button-toggle>
<mat-button-toggle value="http" aria-label="html">
HTTP
</mat-button-toggle>
</mat-button-toggle-group>
<span class="help-title">In order to publish telemetry data to (<a [href]="helpLink" target="_blank">ThingsBoard device</a>) with credentials of the current device you can use the following commands.</span>
<div class="mat-content" fxLayout="column">
<div class="mat-content" fxLayout="column" *ngIf="protocolCtrl.value === 'mqtt'">
<span>MQTT </span>
<span>1. Setup: <span [innerHTML]="mqttSetup"></span></span>
<span class="protocol-send-command">2. Send command:</span>
<div fxLayout="row wrap" fxLayoutAlign="start center">
<pre class="tb-highlight" fxFlex><code class="pre-wrap">{{ mqttCode }}</code></pre>
<button mat-icon-button
color="primary"
ngxClipboard
cbContent="{{ mqttCode }}"
(cbOnSuccess)="onDockerCodeCopied()"
matTooltip="{{ 'device.transportCommandCopiedMessage' | translate }}"
matTooltipPosition="above">
<mat-icon svgIcon="mdi:clipboard-arrow-left"></mat-icon>
</button>
</div>
</div>
<div class="mat-content" fxLayout="column" *ngIf="protocolCtrl.value === 'coap'">
<span>COAP </span>
<span>1. Setup: <span [innerHTML]="coapSetup"></span></span>
<span class="protocol-send-command">2. Send command: (based on CoAP cli)</span>
<div fxLayout="row wrap" fxLayoutAlign="start center">
<pre class="tb-highlight" fxFlex><code class="pre-wrap">{{ coapCode }}</code></pre>
<button mat-icon-button
color="primary"
ngxClipboard
cbContent="{{ coapCode }}"
(cbOnSuccess)="onDockerCodeCopied()"
matTooltip="{{ 'device.transportCommandCopiedMessage' | translate }}"
matTooltipPosition="above">
<mat-icon svgIcon="mdi:clipboard-arrow-left"></mat-icon>
</button>
<pre class="tb-highlight" fxFlex="100"><code
class="pre-wrap">JSON example: {{'[{"temperature": 40}, {"isRainy": true}]'}}</code></pre>
</div>
</div>
<div class="mat-content" fxLayout="column" *ngIf="protocolCtrl.value === 'http'">
<span>HTTP </span>
<span>1. Setup: <span [innerHTML]="httpSetup"></span></span>
<span class="protocol-send-command">2. Send command:</span>
<div fxLayout="row wrpa"fxLayoutAlign="start center">
<pre class="tb-highlight" fxFlex><code class="pre-wrap">{{ httpCode }}</code></pre>
<button mat-icon-button
color="primary"
ngxClipboard
cbContent="{{ httpCode }}"
(cbOnSuccess)="onDockerCodeCopied()"
matTooltip="{{ 'device.transportCommandCopiedMessage' | translate }}"
matTooltipPosition="above">
<mat-icon svgIcon="mdi:clipboard-arrow-left"></mat-icon>
</button>
</div>
</div>
</div>
</div>

56
ui-ngx/src/app/modules/home/components/device/device-example-command.component.scss

@ -0,0 +1,56 @@
/**
* ThingsBoard, Inc. ("COMPANY") CONFIDENTIAL
*
* Copyright © 2016-2023 ThingsBoard, Inc. All Rights Reserved.
*
* NOTICE: All information contained herein is, and remains
* the property of ThingsBoard, Inc. and its suppliers,
* if any. The intellectual and technical concepts contained
* herein are proprietary to ThingsBoard, Inc.
* and its suppliers and may be covered by U.S. and Foreign Patents,
* patents in process, and are protected by trade secret or copyright law.
*
* Dissemination of this information or reproduction of this material is strictly forbidden
* unless prior written permission is obtained from COMPANY.
*
* Access to the source code contained herein is hereby forbidden to anyone except current COMPANY employees,
* managers or contractors who have executed Confidentiality and Non-disclosure agreements
* explicitly covering such access.
*
* The copyright notice above does not evidence any actual or intended publication
* or disclosure of this source code, which includes
* information that is confidential and/or proprietary, and is a trade secret, of COMPANY.
* ANY REPRODUCTION, MODIFICATION, DISTRIBUTION, PUBLIC PERFORMANCE,
* OR PUBLIC DISPLAY OF OR THROUGH USE OF THIS SOURCE CODE WITHOUT
* THE EXPRESS WRITTEN CONSENT OF COMPANY IS STRICTLY PROHIBITED,
* AND IN VIOLATION OF APPLICABLE LAWS AND INTERNATIONAL TREATIES.
* THE RECEIPT OR POSSESSION OF THIS SOURCE CODE AND/OR RELATED INFORMATION
* DOES NOT CONVEY OR IMPLY ANY RIGHTS TO REPRODUCE, DISCLOSE OR DISTRIBUTE ITS CONTENTS,
* OR TO MANUFACTURE, USE, OR SELL ANYTHING THAT IT MAY DESCRIBE, IN WHOLE OR IN PART.
*/
:host {
mat-button-toggle-group, .protocol-title {
margin-bottom: 20px;
}
span {
margin-bottom: 10px;
}
.protocol-toggle {
width: fit-content;
}
.help-title {
padding-bottom: 5px;
}
.protocol-send-command {
width: 100%;
}
.pre-wrap {
white-space: pre-wrap;
}
}

113
ui-ngx/src/app/modules/home/components/device/device-example-command.component.ts

@ -0,0 +1,113 @@
///
/// ThingsBoard, Inc. ("COMPANY") CONFIDENTIAL
///
/// Copyright © 2016-2023 ThingsBoard, Inc. All Rights Reserved.
///
/// NOTICE: All information contained herein is, and remains
/// the property of ThingsBoard, Inc. and its suppliers,
/// if any. The intellectual and technical concepts contained
/// herein are proprietary to ThingsBoard, Inc.
/// and its suppliers and may be covered by U.S. and Foreign Patents,
/// patents in process, and are protected by trade secret or copyright law.
///
/// Dissemination of this information or reproduction of this material is strictly forbidden
/// unless prior written permission is obtained from COMPANY.
///
/// Access to the source code contained herein is hereby forbidden to anyone except current COMPANY employees,
/// managers or contractors who have executed Confidentiality and Non-disclosure agreements
/// explicitly covering such access.
///
/// The copyright notice above does not evidence any actual or intended publication
/// or disclosure of this source code, which includes
/// information that is confidential and/or proprietary, and is a trade secret, of COMPANY.
/// ANY REPRODUCTION, MODIFICATION, DISTRIBUTION, PUBLIC PERFORMANCE,
/// OR PUBLIC DISPLAY OF OR THROUGH USE OF THIS SOURCE CODE WITHOUT
/// THE EXPRESS WRITTEN CONSENT OF COMPANY IS STRICTLY PROHIBITED,
/// AND IN VIOLATION OF APPLICABLE LAWS AND INTERNATIONAL TREATIES.
/// THE RECEIPT OR POSSESSION OF THIS SOURCE CODE AND/OR RELATED INFORMATION
/// DOES NOT CONVEY OR IMPLY ANY RIGHTS TO REPRODUCE, DISCLOSE OR DISTRIBUTE ITS CONTENTS,
/// OR TO MANUFACTURE, USE, OR SELL ANYTHING THAT IT MAY DESCRIBE, IN WHOLE OR IN PART.
///
import { Component, Input, OnInit } from '@angular/core';
import { FormControl } from '@angular/forms';
import { Router } from '@angular/router';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { TranslateService } from '@ngx-translate/core';
import { ActionNotificationShow } from '@core/notification/notification.actions';
import { helpBaseUrl } from '@shared/models/constants';
@Component({
selector: 'tb-device-example-command',
templateUrl: './device-example-command.component.html',
styleUrls: ['./device-example-command.component.scss']
})
export class DeviceExampleCommandComponent implements OnInit {
@Input()
token: string;
helpLink: string = helpBaseUrl + '/docs/reference/protocols/';
mqttCode: string;
coapCode: string;
httpCode: string;
mqttSetup: string;
coapSetup: string;
httpSetup: string;
protocolCtrl: FormControl;
constructor(protected router: Router,
protected store: Store<AppState>,
private translate: TranslateService) {
}
ngOnInit(): void {
const HOST = window.location.hostname;
this.mqttCode = `mosquitto_pub -d -q 1 -h ${HOST} -t "v1/devices/me/telemetry" -u "${this.token}" -m "{"temperature":42}"`;
this.coapCode = `cat telemetry-data.json | coap post coap://${HOST}/api/v1/${this.token}/telemetry`;
this.httpCode = `curl -v -X POST --data "{"temperature":42,"humidity":73}" http://${HOST}/api/v1/${this.token}/telemetry --header "Content-Type:application/json"`;
this.protocolCtrl = new FormControl('mqtt');
const mqttLink = '<a href="https://thingsboard.io/docs/getting-started-guides/helloworld/?connectdevice=mqtt-windows"> Mqtt Guide</a>';
// @ts-ignore
const platform = window.navigator?.userAgentData?.platform || window.navigator.platform,
macosPlatforms = ['Macintosh', 'MacIntel', 'MacPPC', 'Mac68K'],
windowsPlatforms = ['Win32', 'Win64', 'Windows', 'WinCE'];
if (macosPlatforms.indexOf(platform) !== -1) {
this.mqttSetup = 'brew install mosquitto-clients';
this.coapSetup = 'npm install coap-cli -g';
this.httpSetup = 'brew install curl';
} else if (windowsPlatforms.indexOf(platform) !== -1) {
this.mqttSetup = mqttLink;
this.coapSetup = 'npm install coap-cli -g';
this.httpSetup = 'not required, available by default in windows 10+';
} else if (/Linux/.test(platform)) {
this.mqttSetup = 'sudo apt-get install mosquitto-clients';
this.coapSetup = 'npm install coap-cli -g';
this.httpSetup = 'sudo apt-get install curl';
}
}
onDockerCodeCopied() {
this.store.dispatch(new ActionNotificationShow(
{
message: this.translate.instant('gateway.command-copied-message'),
type: 'success',
target: 'dockerCommandDialogContent',
duration: 1200,
verticalPosition: 'bottom',
horizontalPosition: 'left'
}));
}
}

55
ui-ngx/src/app/modules/home/components/device/device-gateway-command.component.html

@ -1,55 +0,0 @@
<!--
Copyright © 2016-2022 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.
-->
<div mat-dialog-content tb-toast fxLayout="column" toastTarget="dockerCommandDialogContent">
<span style="padding-bottom: 15px;">In order to run ThingsBoard IoT gateway (<a [href]="helpLink" target="_blank">ThingsBoard IoT gateway</a>) in docker with credentials for this device you can use the following commands.</span>
<div fxFlex fxLayout="row" style="justify-content: space-between">
<mat-button-toggle-group [formControl]="selectedOSControll" aria-label="Favorite Color" style="width: max-content">
<mat-button-toggle [value]="osTypes.windows"><span translate>gateway.windows </span></mat-button-toggle>
<mat-button-toggle [value]="osTypes.linux"><span translate>gateway.linux-macos </span></mat-button-toggle>
</mat-button-toggle-group>
<div class="tb-help" [tb-help]="helpLink"></div>
</div>
<div class="mat-content" fxLayout="column" *ngIf="selectedOSControll.value !== osTypes.windows">
<div fxLayout="row" fxLayoutAlign="start center">
<pre class="tb-highlight" fxFlex><code style="white-space: pre-wrap;">{{ linuxCode }}</code></pre>
<button mat-icon-button
color="primary"
ngxClipboard
cbContent="{{ linuxCode }}"
(cbOnSuccess)="onDockerCodeCopied()"
matTooltip="{{ 'gateway.copy-command' | translate }}"
matTooltipPosition="above">
<mat-icon svgIcon="mdi:clipboard-arrow-left"></mat-icon>
</button>
</div>
</div>
<div class="mat-content" fxLayout="column" *ngIf="selectedOSControll.value === osTypes.windows">
<div fxLayout="row" fxLayoutAlign="start center">
<pre class="tb-highlight" fxFlex><code style="white-space: pre-wrap;">{{ windowsCode }}</code></pre>
<button mat-icon-button
color="primary"
ngxClipboard
cbContent="{{ windowsCode }}"
(cbOnSuccess)="onDockerCodeCopied()"
matTooltip="{{ 'gateway.copy-command' | translate }}"
matTooltipPosition="above">
<mat-icon svgIcon="mdi:clipboard-arrow-left"></mat-icon>
</button>
</div>
</div>
</div>

116
ui-ngx/src/app/modules/home/components/device/device-gateway-command.component.ts

@ -1,116 +0,0 @@
///
/// Copyright © 2016-2022 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, Input, OnInit} from '@angular/core';
import {
FormControl,
} from '@angular/forms';
import {Router} from "@angular/router";
import {Store} from "@ngrx/store";
import {AppState} from "@core/core.state";
import {TranslateService} from "@ngx-translate/core";
import {ActionNotificationShow} from "@core/notification/notification.actions";
import {DeviceService} from "@core/http/device.service";
import {helpBaseUrl} from "@shared/models/constants";
enum OsType {
linux = 'linux',
macos = 'macos',
windows = 'win'
}
@Component({
selector: 'tb-gateway-command',
templateUrl: './device-gateway-command.component.html',
styleUrls: []
})
export class DeviceGatewayCommandComponent implements OnInit {
@Input()
token: string;
@Input()
deviceId: string;
linuxCode: string;
windowsCode: string;
selectedOSControll: FormControl;
osTypes = OsType;
helpLink: string = helpBaseUrl + '/docs/iot-gateway/install/docker-installation/';
constructor(protected router: Router,
protected store: Store<AppState>,
private translate: TranslateService,
private deviceService: DeviceService) {
}
ngOnInit(): void {
const HOST = window.location.hostname;
if (this.deviceId) {
this.deviceService.getDeviceCredentials(this.deviceId).subscribe(credentials=>{
this.token = credentials.credentialsId;
this.createRunCode(HOST);
})
}
this.selectedOSControll = new FormControl('');
// @ts-ignore
const platform = window.navigator?.userAgentData?.platform || window.navigator.platform,
macosPlatforms = ['Macintosh', 'MacIntel', 'MacPPC', 'Mac68K'],
windowsPlatforms = ['Win32', 'Win64', 'Windows', 'WinCE'];
if (macosPlatforms.indexOf(platform) !== -1) {
this.selectedOSControll.setValue(OsType.macos);
} else if (windowsPlatforms.indexOf(platform) !== -1) {
this.selectedOSControll.setValue(OsType.windows);
} else if (/Linux/.test(platform)) {
this.selectedOSControll.setValue(OsType.linux);
}
this.createRunCode(HOST);
}
createRunCode(HOST) {
this.linuxCode = "docker run -it -v ~/.tb-gateway/logs:/thingsboard_gateway/logs -v " +
"~/.tb-gateway/extensions:/thingsboard_gateway/extensions -v ~/.tb-gateway/config:/thingsboard_gateway/config --name tb-gateway -e host=" +
HOST +
" -e port=1883 -e accessToken=" +
this.token +
" --restart always thingsboard/tb-gateway";
this.windowsCode = "docker run -it -v %HOMEPATH%/tb-gateway/config:/thingsboard_gateway/config -v " +
"%HOMEPATH%/tb-gateway/extensions:/thingsboard_gateway/extensions -v %HOMEPATH%/tb-gateway/logs:/thingsboard_gateway/logs " +
"--name tb-gateway -e host=" +
HOST +
" -e port=1883 -e accessToken=" +
this.token +
" --restart always thingsboard/tb-gateway";
}
goToDocumentationPage() {
window.open(this.helpLink, '_blank');
}
onDockerCodeCopied() {
this.store.dispatch(new ActionNotificationShow(
{
message: this.translate.instant('gateway.command-copied-message'),
type: 'success',
target: 'dockerCommandDialogContent',
duration: 1200,
verticalPosition: 'bottom',
horizontalPosition: 'left'
}));
}
}

41
ui-ngx/src/app/modules/home/components/gateway/gateway-command-dialog.component.html

@ -1,41 +0,0 @@
<!--
Copyright © 2016-2022 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 style="min-width: 400px; position: relative;">
<mat-toolbar color="primary">
<h2 translate>gateway.command</h2>
<span fxFlex></span>
<button mat-button mat-icon-button
(click)="close()"
type="button">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
<tb-gateway-command [token]="data.credentials.credentialsId"></tb-gateway-command>
<div mat-dialog-actions fxLayoutAlign="end center">
<button mat-button color="primary"
type="button"
cdkFocusInitial
[disabled]="(isLoading$ | async)"
(click)="close()">
{{ 'action.ok' | translate }}
</button>
</div>
</form>

58
ui-ngx/src/app/modules/home/components/gateway/gateway-command-dialog.component.ts

@ -1,58 +0,0 @@
///
/// Copyright © 2016-2022 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, Inject, OnInit} from '@angular/core';
import {MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog';
import {Store} from '@ngrx/store';
import {AppState} from '@core/core.state';
import {Router} from '@angular/router';
import {DialogComponent} from '@app/shared/components/dialog.component';
import {TranslateService} from '@ngx-translate/core';
import {Device, DeviceCredentials} from "@shared/models/device.models";
export interface GatewayCommandDialogData {
device: Device,
credentials: DeviceCredentials
}
enum OsType {
linux = 'linux',
macos = 'macos',
windows = 'win'
}
@Component({
selector: 'tb-gateway-command-dialog',
templateUrl: './gateway-command-dialog.component.html',
styleUrls: []
})
export class GatewayCommandDialogComponent extends DialogComponent<GatewayCommandDialogComponent> implements OnInit {
constructor(protected router: Router,
protected store: Store<AppState>,
@Inject(MAT_DIALOG_DATA) public data: GatewayCommandDialogData,
public dialogRef: MatDialogRef<GatewayCommandDialogComponent, boolean>,) {
super(store, router, dialogRef);
}
ngOnInit() {
}
close(): void {
this.dialogRef.close();
}
}

130
ui-ngx/src/app/modules/home/components/gateway/gateway-list-table-config.ts

@ -1,130 +0,0 @@
///
/// Copyright © 2016-2022 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 {
DateEntityTableColumn,
EntityTableColumn,
EntityTableConfig
} from '@home/models/entity/entities-table-config.models';
import {EntityType, EntityTypeResource} from '@shared/models/entity-type.models';
import {TranslateService} from '@ngx-translate/core';
import {DatePipe} from '@angular/common';
import {Direction} from '@shared/models/page/sort-order';
import {MatDialog} from '@angular/material/dialog';
import {TimePageLink} from '@shared/models/page/page-link';
import {Observable} from 'rxjs';
import {emptyPageData, PageData} from '@shared/models/page/page-data';
import {UtilsService} from '@core/services/utils.service';
import {DeviceService} from "@core/http/device.service";
import {AttributeService} from "@core/http/attribute.service";
import {Device} from "@shared/models/device.models";
import {
GatewayCommandDialogComponent,
GatewayCommandDialogData
} from "@home/components/gateway/gateway-command-dialog.component";
import {ActionNotificationShow} from "@core/notification/notification.actions";
import {Store} from "@ngrx/store";
import {AppState} from "@core/core.state";
import {map} from "rxjs/operators";
import {EntityService} from "@core/http/entity.service";
export class GatewayListTableConfig extends EntityTableConfig<Device, TimePageLink> {
constructor(protected store: Store<AppState>,
private deviceService: DeviceService,
private attributeService: AttributeService,
private entityService: EntityService,
private datePipe: DatePipe,
private translate: TranslateService,
private utils: UtilsService,
private dialog: MatDialog,
updateOnInit = true) {
super();
this.loadDataOnInit = updateOnInit;
this.tableTitle = 'Gateway list';
this.useTimePageLink = false;
this.pageMode = false;
this.displayPagination = false;
this.detailsPanelEnabled = false;
this.selectionEnabled = false;
this.searchEnabled = false;
this.addEnabled = false;
this.entitiesDeleteEnabled = false;
this.actionsColumnTitle = '';
this.entityTranslations = {
noEntities: 'gateway.no-gateway-found',
search: 'gateway.gateway-search'
};
this.entityResources = {} as EntityTypeResource<Device>;
this.entitiesFetchFunction = () => this.fetchGateways();
this.defaultSortOrder = {property: 'createdTime', direction: Direction.DESC};
this.columns.push(
new DateEntityTableColumn<Device>('createdTime', 'gateway.created-time', this.datePipe, '150px'));
this.columns.push(
new EntityTableColumn<Device>('entityName', 'gateway.gateway-name', '20%',
(entity => this.utils.customTranslation(entity.name, entity.name))
)
);
this.cellActionDescriptors.push(
{
name: this.translate.instant('gateway.command'),
icon: 'vpn_key',
isEnabled: () => true,
onAction: ($event, entity) => this.showGatewayDockerCommand(entity)
}
);
}
showGatewayDockerCommand(entity: Device) {
this.deviceService.getDeviceCredentials(entity.id.id).subscribe(credentials => {
this.dialog.open<GatewayCommandDialogComponent, GatewayCommandDialogData, boolean>
(GatewayCommandDialogComponent,
{
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
credentials: credentials,
device: entity
}
}).afterClosed().subscribe(
(res) => {
if (res) {
this.updateData();
}
}
);
}, error => {
const messageToShow = `<div>${error}</div>`;
this.store.dispatch(new ActionNotificationShow({message: messageToShow, type: 'error'}));
})
}
fetchGateways(): Observable<PageData<Device>> {
return this.entityService.getEntitiesByNameFilter(EntityType.DEVICE, "", -1).pipe(
map((array: Array<Device>) => {
const pageData = emptyPageData<Device>();
pageData.data = array.filter(device => device.additionalInfo?.gateway);
pageData.totalPages = 1;
pageData.totalElements = array.length;
return pageData;
}))
}
}

18
ui-ngx/src/app/modules/home/components/gateway/gateway-list.component.html

@ -1,18 +0,0 @@
<!--
Copyright © 2016-2022 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.
-->
<tb-entities-table [entitiesTableConfig]="gatewayListTableConfig"></tb-entities-table>

40
ui-ngx/src/app/modules/home/components/gateway/gateway-list.component.scss

@ -1,40 +0,0 @@
/**
* Copyright © 2016-2022 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 {
width: 100%;
height: 100%;
display: block;
.tb-table-widget {
.table-container {
position: relative;
}
.mat-table {
.mat-row {
&.invisible {
visibility: hidden;
}
}
}
span.no-data-found {
position: absolute;
top: 60px;
bottom: 0;
left: 0;
right: 0;
}
}
}

63
ui-ngx/src/app/modules/home/components/gateway/gateway-list.component.ts

@ -1,63 +0,0 @@
///
/// Copyright © 2016-2022 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, OnInit} from '@angular/core';
import {UtilsService} from '@core/services/utils.service';
import {TranslateService} from '@ngx-translate/core';
import {DeviceService} from '@core/http/device.service';
import {AttributeService} from '@core/http/attribute.service';
import {GatewayListTableConfig} from "@home/components/gateway/gateway-list-table-config";
import {DatePipe} from "@angular/common";
import {MatDialog} from "@angular/material/dialog";
import {Store} from "@ngrx/store";
import {AppState} from "@core/core.state";
import {EntityService} from "@core/http/entity.service";
@Component({
selector: 'tb-gateway-list',
templateUrl: './gateway-list.component.html',
styleUrls: ['./gateway-list.component.scss']
})
export class GatewayListComponent implements OnInit {
gatewayListTableConfig: GatewayListTableConfig;
constructor(
protected store: Store<AppState>,
private utils: UtilsService,
private translate: TranslateService,
private datePipe: DatePipe,
private deviceService: DeviceService,
private entityService: EntityService,
private attributeService: AttributeService,
private dialog: MatDialog,
) {
}
ngOnInit(): void {
this.gatewayListTableConfig = new GatewayListTableConfig(
this.store,
this.deviceService,
this.attributeService,
this.entityService,
this.datePipe,
this.translate,
this.utils,
this.dialog
);
}
}

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

@ -178,7 +178,6 @@ import { modulesMap } from '@modules/common/modules-map';
import { AlarmAssigneePanelComponent } from '@home/components/alarm/alarm-assignee-panel.component';
import { RouterTabsComponent } from '@home/components/router-tabs.component';
import { SendNotificationButtonComponent } from '@home/components/notification/send-notification-button.component';
import { GatewayListComponent } from "@home/components/gateway/gateway-list.component";
@NgModule({
declarations:
@ -203,7 +202,6 @@ import { GatewayListComponent } from "@home/components/gateway/gateway-list.comp
AlarmTableHeaderComponent,
AlarmTableComponent,
AlarmAssigneePanelComponent,
GatewayListComponent,
AttributeTableComponent,
AddAttributeDialogComponent,
EditAttributeValuePanelComponent,
@ -354,7 +352,6 @@ import { GatewayListComponent } from "@home/components/gateway/gateway-list.comp
RelationFiltersComponent,
AlarmTableComponent,
AlarmAssigneePanelComponent,
GatewayListComponent,
AttributeTableComponent,
AliasesEntitySelectComponent,
AliasesEntityAutocompleteComponent,

3
ui-ngx/src/app/modules/home/components/shared-home-components.module.ts

@ -18,7 +18,6 @@ import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { SharedModule } from '@app/shared/shared.module';
import { AlarmDetailsDialogComponent } from '@home/components/alarm/alarm-details-dialog.component';
import { GatewayCommandDialogComponent } from "@home/components/gateway/gateway-command-dialog.component";
import { SHARED_HOME_COMPONENTS_MODULE_TOKEN } from '@home/components/tokens';
import { DeviceCredentialsModule } from "@home/components/device/device-credentials.module";
import { AlarmCommentComponent } from '@home/components/alarm/alarm-comment.component';
@ -35,7 +34,6 @@ import { AlarmAssigneeComponent } from '@home/components/alarm/alarm-assignee.co
AlarmCommentComponent,
AlarmCommentDialogComponent,
AlarmAssigneeComponent
GatewayCommandDialogComponent
],
imports: [
CommonModule,
@ -47,7 +45,6 @@ import { AlarmAssigneeComponent } from '@home/components/alarm/alarm-assignee.co
AlarmCommentComponent,
AlarmCommentDialogComponent,
AlarmAssigneeComponent
GatewayCommandDialogComponent
]
})
export class SharedHomeComponentsModule { }

15
ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.html

@ -29,7 +29,7 @@
</mat-progress-bar>
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
<div mat-dialog-content>
<mat-horizontal-stepper [linear]="true" [labelPosition]="labelPosition" #addDeviceWizardStepper (selectionChange)="changeStep($event)">
<mat-horizontal-stepper [linear]="true" [labelPosition]="labelPosition" #addDeviceWizardStepper (selectionChange)="changeStep($event)" *ngIf="!deviceCredentials">
<ng-template matStepperIcon="edit">
<mat-icon>check</mat-icon>
</ng-template>
@ -178,9 +178,17 @@
</form>
</mat-step>
</mat-horizontal-stepper>
<div *ngIf="deviceCredentials" class="commands-container">
<tb-gateway-command *ngIf="device.additionalInfo?.gateway"
[token]="deviceCredentials.credentialsId">
</tb-gateway-command>
<tb-device-example-command *ngIf="!device.additionalInfo?.gateway"
[token]="deviceCredentials.credentialsId">
</tb-device-example-command>
</div>
</div>
<div mat-dialog-actions style="padding: 0">
<div class="dialog-actions-row" fxFlex fxLayout="row" fxLayoutAlign="end center">
<div class="dialog-actions-row" fxFlex fxLayout="row" fxLayoutAlign="end center" *ngIf="!deviceCredentials">
<button mat-stroked-button *ngIf="selectedIndex > 0"
[disabled]="(isLoading$ | async)"
(click)="previousStep()">{{ 'action.back' | translate }}</button>
@ -195,10 +203,11 @@
<div class="dialog-actions-row" fxFlex fxLayout="row" fxLayoutGap="8px" fxLayoutAlign="end center">
<button mat-button
[disabled]="(isLoading$ | async)"
*ngIf="!deviceCredentials"
(click)="cancel()">{{ 'action.cancel' | translate }}</button>
<button mat-raised-button
[disabled]="(isLoading$ | async)"
color="primary"
(click)="add()">{{ 'action.add' | translate }}</button>
(click)="add()">{{ (deviceCredentials ? 'action.close': 'action.add') | translate }}</button>
</div>
</div>

4
ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.scss

@ -37,6 +37,10 @@
}
:host ::ng-deep {
.commands-container {
padding: 24px;
}
.mat-mdc-dialog-content {
display: flex;
flex-direction: column;

41
ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.ts

@ -24,6 +24,8 @@ import { Router } from '@angular/router';
import {
createDeviceProfileConfiguration,
createDeviceProfileTransportConfiguration,
Device,
DeviceCredentials,
DeviceProfile,
DeviceProfileInfo,
DeviceProfileType,
@ -57,9 +59,9 @@ import { deepTrim } from '@core/utils';
styleUrls: ['./device-wizard-dialog.component.scss']
})
export class DeviceWizardDialogComponent extends
DialogComponent<DeviceWizardDialogComponent, boolean> implements OnDestroy, ErrorStateMatcher {
DialogComponent<DeviceWizardDialogComponent, Device> implements OnDestroy, ErrorStateMatcher {
@ViewChild('addDeviceWizardStepper', {static: true}) addDeviceWizardStepper: MatStepper;
@ViewChild('addDeviceWizardStepper') addDeviceWizardStepper: MatStepper;
selectedIndex = 0;
@ -69,7 +71,7 @@ export class DeviceWizardDialogComponent extends
entityType = EntityType;
deviceTransportTypes = Object.values(DeviceTransportType);
deviceTransportTypes = Object.values(DeviceTransportType) as DeviceTransportType[];
deviceTransportTypeTranslations = deviceTransportTypeTranslationMap;
@ -91,6 +93,10 @@ export class DeviceWizardDialogComponent extends
serviceType = ServiceType.TB_RULE_ENGINE;
device: Device;
deviceCredentials: DeviceCredentials;
private subscriptions: Subscription[] = [];
private currentDeviceProfileTransportType = DeviceTransportType.DEFAULT;
@ -98,7 +104,7 @@ export class DeviceWizardDialogComponent extends
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: AddEntityDialogData<BaseData<EntityId>>,
@SkipSelf() private errorStateMatcher: ErrorStateMatcher,
public dialogRef: MatDialogRef<DeviceWizardDialogComponent, boolean>,
public dialogRef: MatDialogRef<DeviceWizardDialogComponent, Device>,
private deviceProfileService: DeviceProfileService,
private deviceService: DeviceService,
private breakpointObserver: BreakpointObserver,
@ -263,13 +269,15 @@ export class DeviceWizardDialogComponent extends
}
add(): void {
if (this.allValid()) {
if (this.deviceCredentials) {
this.dialogRef.close(this.device);
} else if (this.allValid()) {
this.createDeviceProfile().pipe(
mergeMap(profileId => this.createDevice(profileId)),
mergeMap(device => this.saveCredentials(device))
mergeMap(device => this.saveCredentials(device)),
).subscribe(
(created) => {
this.dialogRef.close(created);
(device) => {
this.device = device;
}
);
}
@ -326,7 +334,7 @@ export class DeviceWizardDialogComponent extends
}
}
private createDevice(profileId): Observable<BaseData<HasId>> {
private createDevice(profileId): Observable<Device> {
const device = {
name: this.deviceWizardFormGroup.get('name').value,
label: this.deviceWizardFormGroup.get('label').value,
@ -349,10 +357,10 @@ export class DeviceWizardDialogComponent extends
this.addDeviceWizardStepper.selectedIndex = 0;
return throwError(e);
})
);
) as Observable<Device>;
}
private saveCredentials(device: BaseData<HasId>): Observable<boolean> {
private saveCredentials(device: Device): Observable<Device> {
if (this.credentialsFormGroup.get('setCredential').value) {
return this.deviceService.getDeviceCredentials(device.id.id).pipe(
mergeMap(
@ -370,9 +378,16 @@ export class DeviceWizardDialogComponent extends
);
}
),
map(() => true));
map(() => device));
} else {
return this.deviceService.getDeviceCredentials(device.id.id).pipe(
map((deviceCredentials) => {
this.deviceCredentials = deviceCredentials;
return device;
})
);
}
return of(true);
return of(device);
}
allValid(): boolean {

70
ui-ngx/src/app/shared/components/device/device-gateway-command.component.html

@ -0,0 +1,70 @@
<!--
ThingsBoard, Inc. ("COMPANY") CONFIDENTIAL
Copyright © 2016-2023 ThingsBoard, Inc. All Rights Reserved.
NOTICE: All information contained herein is, and remains
the property of ThingsBoard, Inc. and its suppliers,
if any. The intellectual and technical concepts contained
herein are proprietary to ThingsBoard, Inc.
and its suppliers and may be covered by U.S. and Foreign Patents,
patents in process, and are protected by trade secret or copyright law.
Dissemination of this information or reproduction of this material is strictly forbidden
unless prior written permission is obtained from COMPANY.
Access to the source code contained herein is hereby forbidden to anyone except current COMPANY employees,
managers or contractors who have executed Confidentiality and Non-disclosure agreements
explicitly covering such access.
The copyright notice above does not evidence any actual or intended publication
or disclosure of this source code, which includes
information that is confidential and/or proprietary, and is a trade secret, of COMPANY.
ANY REPRODUCTION, MODIFICATION, DISTRIBUTION, PUBLIC PERFORMANCE,
OR PUBLIC DISPLAY OF OR THROUGH USE OF THIS SOURCE CODE WITHOUT
THE EXPRESS WRITTEN CONSENT OF COMPANY IS STRICTLY PROHIBITED,
AND IN VIOLATION OF APPLICABLE LAWS AND INTERNATIONAL TREATIES.
THE RECEIPT OR POSSESSION OF THIS SOURCE CODE AND/OR RELATED INFORMATION
DOES NOT CONVEY OR IMPLY ANY RIGHTS TO REPRODUCE, DISCLOSE OR DISTRIBUTE ITS CONTENTS,
OR TO MANUFACTURE, USE, OR SELL ANYTHING THAT IT MAY DESCRIBE, IN WHOLE OR IN PART.
-->
<div mat-dialog-content tb-toast fxLayout="column" toastTarget="dockerCommandDialogContent">
<span style="padding-bottom: 15px;">In order to run ThingsBoard IoT gateway (<a [href]="helpLink" target="_blank">ThingsBoard IoT gateway</a>) in docker with credentials for this device you can use the following commands.</span>
<div fxFlex fxLayout="row" style="justify-content: space-between">
<mat-button-toggle-group [formControl]="selectedOSCControl" aria-label="Favorite Color" style="width: max-content; flex-grow: 0">
<mat-button-toggle [value]="osTypes.windows"><span translate>gateway.windows </span></mat-button-toggle>
<mat-button-toggle [value]="osTypes.linux"><span translate>gateway.linux-macos </span></mat-button-toggle>
</mat-button-toggle-group>
<div class="tb-help" [tb-help]="helpLink"></div>
</div>
<div class="mat-content" fxLayout="column" *ngIf="selectedOSCControl.value !== osTypes.windows">
<div fxLayout="row" fxLayoutAlign="start center">
<pre class="tb-highlight" fxFlex><code style="white-space: pre-wrap;">{{ linuxCode }}</code></pre>
<button mat-icon-button
color="primary"
ngxClipboard
cbContent="{{ linuxCode }}"
(cbOnSuccess)="onDockerCodeCopied()"
matTooltip="{{ 'gateway.copy-command' | translate }}"
matTooltipPosition="above">
<mat-icon svgIcon="mdi:clipboard-arrow-left"></mat-icon>
</button>
</div>
</div>
<div class="mat-content" fxLayout="column" *ngIf="selectedOSCControl.value === osTypes.windows">
<div fxLayout="row" fxLayoutAlign="start center">
<pre class="tb-highlight" fxFlex><code style="white-space: pre-wrap;">{{ windowsCode }}</code></pre>
<button mat-icon-button
color="primary"
ngxClipboard
cbContent="{{ windowsCode }}"
(cbOnSuccess)="onDockerCodeCopied()"
matTooltip="{{ 'gateway.copy-command' | translate }}"
matTooltipPosition="above">
<mat-icon svgIcon="mdi:clipboard-arrow-left"></mat-icon>
</button>
</div>
</div>
</div>

125
ui-ngx/src/app/shared/components/device/device-gateway-command.component.ts

@ -0,0 +1,125 @@
///
/// ThingsBoard, Inc. ("COMPANY") CONFIDENTIAL
///
/// Copyright © 2016-2023 ThingsBoard, Inc. All Rights Reserved.
///
/// NOTICE: All information contained herein is, and remains
/// the property of ThingsBoard, Inc. and its suppliers,
/// if any. The intellectual and technical concepts contained
/// herein are proprietary to ThingsBoard, Inc.
/// and its suppliers and may be covered by U.S. and Foreign Patents,
/// patents in process, and are protected by trade secret or copyright law.
///
/// Dissemination of this information or reproduction of this material is strictly forbidden
/// unless prior written permission is obtained from COMPANY.
///
/// Access to the source code contained herein is hereby forbidden to anyone except current COMPANY employees,
/// managers or contractors who have executed Confidentiality and Non-disclosure agreements
/// explicitly covering such access.
///
/// The copyright notice above does not evidence any actual or intended publication
/// or disclosure of this source code, which includes
/// information that is confidential and/or proprietary, and is a trade secret, of COMPANY.
/// ANY REPRODUCTION, MODIFICATION, DISTRIBUTION, PUBLIC PERFORMANCE,
/// OR PUBLIC DISPLAY OF OR THROUGH USE OF THIS SOURCE CODE WITHOUT
/// THE EXPRESS WRITTEN CONSENT OF COMPANY IS STRICTLY PROHIBITED,
/// AND IN VIOLATION OF APPLICABLE LAWS AND INTERNATIONAL TREATIES.
/// THE RECEIPT OR POSSESSION OF THIS SOURCE CODE AND/OR RELATED INFORMATION
/// DOES NOT CONVEY OR IMPLY ANY RIGHTS TO REPRODUCE, DISCLOSE OR DISTRIBUTE ITS CONTENTS,
/// OR TO MANUFACTURE, USE, OR SELL ANYTHING THAT IT MAY DESCRIBE, IN WHOLE OR IN PART.
///
import { Component, Input, OnInit } from '@angular/core';
import { FormControl } from '@angular/forms';
import { Router } from '@angular/router';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { TranslateService } from '@ngx-translate/core';
import { ActionNotificationShow } from '@core/notification/notification.actions';
import { DeviceService } from '@core/http/device.service';
import { helpBaseUrl } from '@shared/models/constants';
enum OsType {
linux = 'linux',
macos = 'macos',
windows = 'win'
}
@Component({
selector: 'tb-gateway-command',
templateUrl: './device-gateway-command.component.html',
styleUrls: []
})
export class DeviceGatewayCommandComponent implements OnInit {
@Input()
token: string;
@Input()
deviceId: string;
linuxCode: string;
windowsCode: string;
selectedOSCControl: FormControl;
osTypes = OsType;
helpLink: string = helpBaseUrl + '/docs/iot-gateway/install/docker-installation/';
constructor(protected router: Router,
protected store: Store<AppState>,
private translate: TranslateService,
private deviceService: DeviceService) {
}
ngOnInit(): void {
const HOST = window.location.hostname;
if (this.deviceId) {
this.deviceService.getDeviceCredentials(this.deviceId).subscribe(credentials => {
this.token = credentials.credentialsId;
this.createRunCode(HOST);
});
}
this.selectedOSCControl = new FormControl('');
// @ts-ignore
const platform = window.navigator?.userAgentData?.platform || window.navigator.platform,
macosPlatforms = ['Macintosh', 'MacIntel', 'MacPPC', 'Mac68K'],
windowsPlatforms = ['Win32', 'Win64', 'Windows', 'WinCE'];
if (macosPlatforms.indexOf(platform) !== -1) {
this.selectedOSCControl.setValue(OsType.macos);
} else if (windowsPlatforms.indexOf(platform) !== -1) {
this.selectedOSCControl.setValue(OsType.windows);
} else if (/Linux/.test(platform)) {
this.selectedOSCControl.setValue(OsType.linux);
}
this.createRunCode(HOST);
}
createRunCode(HOST) {
this.linuxCode = 'docker run -it -v ~/.tb-gateway/logs:/thingsboard_gateway/logs -v ' +
'~/.tb-gateway/extensions:/thingsboard_gateway/extensions -v ~/.tb-gateway/config:/thingsboard_gateway/config --name tb-gateway -e host=' +
HOST +
' -e port=1883 -e accessToken=' +
this.token +
' --restart always thingsboard/tb-gateway';
this.windowsCode = 'docker run -it -v %HOMEPATH%/tb-gateway/config:/thingsboard_gateway/config -v ' +
'%HOMEPATH%/tb-gateway/extensions:/thingsboard_gateway/extensions -v %HOMEPATH%/tb-gateway/logs:/thingsboard_gateway/logs ' +
'--name tb-gateway -e host=' +
HOST +
' -e port=1883 -e accessToken=' +
this.token +
' --restart always thingsboard/tb-gateway';
}
onDockerCodeCopied() {
this.store.dispatch(new ActionNotificationShow(
{
message: this.translate.instant('gateway.command-copied-message'),
type: 'success',
target: 'dockerCommandDialogContent',
duration: 1200,
verticalPosition: 'bottom',
horizontalPosition: 'left'
}));
}
}

683
ui-ngx/src/app/shared/components/device/gateway-configuration.component.html

@ -0,0 +1,683 @@
<!--
ThingsBoard, Inc. ("COMPANY") CONFIDENTIAL
Copyright © 2016-2023 ThingsBoard, Inc. All Rights Reserved.
NOTICE: All information contained herein is, and remains
the property of ThingsBoard, Inc. and its suppliers,
if any. The intellectual and technical concepts contained
herein are proprietary to ThingsBoard, Inc.
and its suppliers and may be covered by U.S. and Foreign Patents,
patents in process, and are protected by trade secret or copyright law.
Dissemination of this information or reproduction of this material is strictly forbidden
unless prior written permission is obtained from COMPANY.
Access to the source code contained herein is hereby forbidden to anyone except current COMPANY employees,
managers or contractors who have executed Confidentiality and Non-disclosure agreements
explicitly covering such access.
The copyright notice above does not evidence any actual or intended publication
or disclosure of this source code, which includes
information that is confidential and/or proprietary, and is a trade secret, of COMPANY.
ANY REPRODUCTION, MODIFICATION, DISTRIBUTION, PUBLIC PERFORMANCE,
OR PUBLIC DISPLAY OF OR THROUGH USE OF THIS SOURCE CODE WITHOUT
THE EXPRESS WRITTEN CONSENT OF COMPANY IS STRICTLY PROHIBITED,
AND IN VIOLATION OF APPLICABLE LAWS AND INTERNATIONAL TREATIES.
THE RECEIPT OR POSSESSION OF THIS SOURCE CODE AND/OR RELATED INFORMATION
DOES NOT CONVEY OR IMPLY ANY RIGHTS TO REPRODUCE, DISCLOSE OR DISTRIBUTE ITS CONTENTS,
OR TO MANUFACTURE, USE, OR SELL ANYTHING THAT IT MAY DESCRIBE, IN WHOLE OR IN PART.
-->
<form [formGroup]="gatewayConfigGroup" style="height: 100%" fxLayout="column">
<mat-toolbar color="primary">
<h2 translate>gateway.gateway-configuration</h2>
<span fxFlex></span>
<button mat-icon-button
type="button"
(click)="cancel()"
*ngIf="dialogRef">
<mat-icon class="material-icons" style="color:#000;">close</mat-icon>
</button>
</mat-toolbar>
<mat-tab-group style="max-height: calc(100% - 115px)">
<mat-tab label="{{'gateway.thingsboard-general' | translate}}">
<div formGroupName="thingsboard" fxLayout="column" class="mat-content mat-padding">
<mat-accordion multi>
<mat-expansion-panel expanded="true" disabled style="padding-top: 20px">
<div fxLayout="row wrap">
<mat-slide-toggle color="primary" fxFlex="100" formControlName="remoteConfiguration">
{{ 'gateway.remote-configuration' | translate }}
</mat-slide-toggle>
<mat-slide-toggle color="primary" fxFlex="100" formControlName="remoteShell">
{{ 'gateway.remote-shell' | translate }}
</mat-slide-toggle>
<mat-form-field fxFlex="calc(50%-15px)" class="mat-block tb-value-type">
<mat-label translate>gateway.thingsboard-host</mat-label>
<input matInput formControlName="host"/>
<mat-error *ngIf="gatewayConfigGroup.get('thingsboard.host').hasError('required')">
{{ 'gateway.thingsboard-host-required' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field fxFlex="calc(50%-15px)" class="mat-block tb-value-type">
<mat-label translate>gateway.thingsboard-port</mat-label>
<input matInput formControlName="port" type="number"/>
<mat-error *ngIf="gatewayConfigGroup.get('thingsboard.port').hasError('required')">
{{'gateway.thingsboard-port-required' | translate }}
</mat-error>
<mat-error *ngIf="gatewayConfigGroup.get('thingsboard.port').hasError('min')">
{{'gateway.thingsboard-port-min' | translate }}
</mat-error>
<mat-error *ngIf="gatewayConfigGroup.get('thingsboard.port').hasError('max')">
{{'gateway.thingsboard-port-max' | translate }}
</mat-error>
<mat-error *ngIf="gatewayConfigGroup.get('thingsboard.port').hasError('pattern')">
{{'gateway.thingsboard-port-pattern' | translate }}
</mat-error>
</mat-form-field>
</div>
</mat-expansion-panel>
<mat-expansion-panel expanded="true" disabled>
<mat-expansion-panel-header>
<mat-panel-title translate class="expansion-panel-header">security.security</mat-panel-title>
</mat-expansion-panel-header>
<div fxLayout="row wrap" formGroupName="security">
<mat-button-toggle-group class="security-toggle-group" formControlName="type" fxFlex="100">
<mat-button-toggle *ngFor="let securityType of securityTypes | keyvalue" [value]="securityType.key">
{{ securityType.value | translate }}
</mat-button-toggle>
</mat-button-toggle-group>
<mat-form-field
*ngIf="gatewayConfigGroup.get('thingsboard.security.type').value.toLowerCase().includes('accesstoken')"
fxFlex="calc(50%-15px)" class="mat-block tb-value-type">
<mat-label translate>security.access-token</mat-label>
<input matInput formControlName="accessToken"/>
<mat-error *ngIf="gatewayConfigGroup.get('thingsboard.security.accessToken').hasError('required')">
{{'security.access-token-required' | translate}}
</mat-error>
</mat-form-field>
<mat-form-field *ngIf="gatewayConfigGroup.get('thingsboard.security.type').value === 'usernamePassword'"
fxFlex="calc(50%-15px)" class="mat-block tb-value-type">
<mat-label translate>security.clientId</mat-label>
<input matInput formControlName="clientId"/>
<mat-error *ngIf="gatewayConfigGroup.get('thingsboard.security.clientId').hasError('required')">
{{'security.clientId-required' | translate}}
</mat-error>
</mat-form-field>
<mat-form-field *ngIf="gatewayConfigGroup.get('thingsboard.security.type').value === 'usernamePassword'"
fxFlex="calc(50%-15px)" class="mat-block tb-value-type">
<mat-label translate>security.username</mat-label>
<input matInput formControlName="username"/>
<mat-error *ngIf="gatewayConfigGroup.get('thingsboard.security.username').hasError('required')">
{{'security.username-required' | translate}}
</mat-error>
</mat-form-field>
<mat-form-field *ngIf="gatewayConfigGroup.get('thingsboard.security.type').value === 'usernamePassword'"
fxFlex="calc(50%-15px)" class="mat-block tb-value-type">
<mat-label translate>security.password</mat-label>
<input matInput formControlName="password"/>
<mat-error *ngIf="gatewayConfigGroup.get('thingsboard.security.password').hasError('required')">
{{'security.password-required' | translate}}
</mat-error>
</mat-form-field>
<tb-file-input
fxFlex="100"
*ngIf="gatewayConfigGroup.get('thingsboard.security.type').value.toLowerCase().includes('tls')"
formControlName="caCert"
label="{{'security.ca-cert-required' | translate}}"
[allowedExtensions]="'pem, cert, key'"
[accept]="'.pem, application/pem,.cert, application/cert, .key,application/key'"
dropLabel="{{'gateway.drop-file' | translate}}">
</tb-file-input>
<tb-file-input
fxFlex="100"
*ngIf="gatewayConfigGroup.get('thingsboard.security.type').value === 'tlsPrivateKey'"
formControlName="cert"
label="{{'security.cert' | translate}}"
[allowedExtensions]="'pem, cert, key'"
[accept]="'.pem, application/pem,.cert, application/cert, .key,application/key'"
dropLabel="{{'gateway.drop-file' | translate}}">
</tb-file-input>
<tb-file-input
fxFlex="100"
*ngIf="gatewayConfigGroup.get('thingsboard.security.type').value === 'tlsPrivateKey'"
formControlName="privateKey"
label="{{'security.private-key' | translate}}"
[allowedExtensions]="'pem, cert, key'"
[accept]="'.pem, application/pem,.cert, application/cert, .key,application/key'"
dropLabel="{{'gateway.drop-file' | translate}}">
</tb-file-input>
</div>
</mat-expansion-panel>
</mat-accordion>
</div>
</mat-tab>
<mat-tab label="{{'gateway.logs.logs' | translate}}">
<div formGroupName="logs" fxLayout="column" class="mat-content mat-padding">
<mat-accordion multi>
<mat-expansion-panel expanded="true" disabled style="padding-top: 20px">
<div fxLayout="row wrap">
<mat-form-field fxFlex="100" class="mat-block tb-value-type">
<mat-label translate>gateway.logs.date-format</mat-label>
<input matInput formControlName="dateFormat"/>
<mat-error *ngIf="gatewayConfigGroup.get('logs.dateFormat').hasError('required')">
{{'gateway.logs.date-format-required' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field fxFlex="100" class="mat-block tb-value-type">
<mat-label translate>gateway.logs.log-format</mat-label>
<textarea matInput formControlName="logFormat" rows="2"></textarea>
<mat-error *ngIf="gatewayConfigGroup.get('logs.logFormat').hasError('required')">
{{'gateway.logs.log-format-required' | translate }}
</mat-error>
</mat-form-field>
<label fxFlex="100" class="logs-label" translate>gateway.logs.log-type</label>
<mat-button-toggle-group class="logs-toggle-group" #logging="matButtonToggleGroup" value="remote" style="margin-bottom: 15px">
<mat-button-toggle value="remote"
aria-label="Remote logging"> {{'gateway.logs.remote' | translate}}</mat-button-toggle>
<mat-button-toggle value="local"
aria-label="Local logging">{{'gateway.logs.local' | translate}} </mat-button-toggle>
</mat-button-toggle-group>
<div fxFlex="100" fxLayout="column" formGroupName="remote" *ngIf="logging.value === 'remote'">
<mat-slide-toggle color="primary" formControlName="enabled">
{{ 'gateway.logs.remote-logs' | translate }}
</mat-slide-toggle>
<mat-form-field class="mat-block tb-value-type">
<mat-label translate>gateway.logs.level</mat-label>
<mat-select formControlName="logLevel">
<mat-option *ngFor="let logLevel of gatewayLogLevel" [value]="logLevel">{{logLevel}}</mat-option>
</mat-select>
</mat-form-field>
</div>
<div fxFlex="100" fxLayout="row wrap" formGroupName="local" *ngIf="logging.value === 'local'">
<mat-button-toggle-group class="security-toggle-group" [formControl]="logSelector" fxFlex="100"
name="logSelector">
<mat-button-toggle *ngFor="let logConfig of localLogsConfigs" [value]="logConfig"
class="first-capital">
{{ logConfig }}
</mat-button-toggle>
</mat-button-toggle-group>
<div fxFlex="100" fxLayout="row wrap" [formGroup]="getLogFormGroup(logSelector.value)">
<mat-form-field class="mat-block tb-value-type" fxFlex="calc(50%-15px)">
<mat-label translate>gateway.logs.level</mat-label>
<mat-select formControlName="logLevel">
<mat-option *ngFor="let logLevel of gatewayLogLevel" [value]="logLevel">{{logLevel}}</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="mat-block tb-value-type" fxFlex="calc(50%-15px)">
<mat-label translate>gateway.logs.file-path</mat-label>
<input matInput formControlName="filePath"/>
<mat-error
*ngIf="gatewayConfigGroup.get('logs.local.' + logSelector.value + '.filePath').hasError('required')">
{{'gateway.logs.file-path-required' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field class="mat-block tb-value-type" fxFlex="calc(35%-30px)">
<mat-label translate>gateway.logs.saving-period</mat-label>
<input matInput formControlName="savingTime" type="number"/>
<mat-error
*ngIf="gatewayConfigGroup.get('logs.local.' + logSelector.value + '.savingTime').hasError('required')">
{{'gateway.logs.saving-period-required' | translate }}
</mat-error>
<mat-error
*ngIf="gatewayConfigGroup.get('logs.local.' + logSelector.value + '.savingTime').hasError('min')">
{{'gateway.logs.saving-period-min' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field class="mat-block tb-value-type" fxFlex="calc(15%-30px)">
<mat-label translate></mat-label>
<mat-select formControlName="savingPeriod">
<mat-option *ngFor="let period of logSavingPeriods | keyvalue" [value]="period.key">
{{period.value | translate}}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="mat-block tb-value-type" fxFlex="calc(50%-30px)">
<mat-label translate>gateway.logs.backup-count</mat-label>
<input matInput formControlName="backupCount" type="number"/>
<mat-error
*ngIf="gatewayConfigGroup.get('logs.local.' + logSelector.value + '.backupCount').hasError('required')">
{{'gateway.logs.backup-count-required' | translate }}
</mat-error>
<mat-error
*ngIf="gatewayConfigGroup.get('logs.local.' + logSelector.value + '.backupCount').hasError('min')">
{{'gateway.logs.backup-count-min' | translate }}
</mat-error>
</mat-form-field>
</div>
</div>
</div>
</mat-expansion-panel>
</mat-accordion>
</div>
</mat-tab>
<mat-tab label="{{'gateway.storage' | translate}}">
<div formGroupName="storage" fxLayout="column" class="mat-content mat-padding">
<mat-accordion>
<mat-expansion-panel expanded="true" disabled>
<mat-expansion-panel-header>
<mat-panel-title translate class="expansion-panel-header">gateway.storage</mat-panel-title>
</mat-expansion-panel-header>
<div fxLayout="row wrap">
<mat-button-toggle-group class="security-toggle-group" formControlName="type" fxFlex="100">
<mat-button-toggle *ngFor="let storageType of storageTypes | keyvalue" [value]="storageType.key">
{{ storageType.value | translate }}
</mat-button-toggle>
</mat-button-toggle-group>
<mat-form-field *ngIf="gatewayConfigGroup.get('storage.type').value === 'memory'"
fxFlex="calc(50%-15px)" class="mat-block tb-value-type">
<mat-label translate>gateway.storage-read-record-count</mat-label>
<input matInput formControlName="read_records_count"/>
<mat-error *ngIf="gatewayConfigGroup.get('storage.read_records_count').hasError('required')">
{{'gateway.storage-read-record-count-required' | translate}}
</mat-error>
<mat-error *ngIf="gatewayConfigGroup.get('storage.read_records_count').hasError('min')">
{{'gateway.storage-read-record-count-min' | translate}}
</mat-error>
<mat-error *ngIf="gatewayConfigGroup.get('storage.read_records_count').hasError('pattern')">
{{'gateway.storage-read-record-count-pattern' | translate}}
</mat-error>
</mat-form-field>
<mat-form-field *ngIf="gatewayConfigGroup.get('storage.type').value === 'memory'"
fxFlex="calc(50%-15px)" class="mat-block tb-value-type">
<mat-label translate>gateway.storage-max-records</mat-label>
<input matInput formControlName="max_records_count"/>
<mat-error *ngIf="gatewayConfigGroup.get('storage.max_records_count').hasError('required')">
{{'gateway.storage-max-records-required' | translate}}
</mat-error>
<mat-error *ngIf="gatewayConfigGroup.get('storage.max_records_count').hasError('min')">
{{'gateway.storage-max-records-min' | translate}}
</mat-error>
<mat-error *ngIf="gatewayConfigGroup.get('storage.max_records_count').hasError('pattern')">
{{'gateway.storage-max-records-pattern' | translate}}
</mat-error>
</mat-form-field>
<mat-form-field *ngIf="gatewayConfigGroup.get('storage.type').value === 'file'"
fxFlex="calc(50%-15px)" class="mat-block tb-value-type">
<mat-label translate>gateway.storage-data-folder-path</mat-label>
<input matInput formControlName="data_folder_path"/>
<mat-error *ngIf="gatewayConfigGroup.get('storage.data_folder_path').hasError('required')">
{{'gateway.storage-data-folder-path-required' | translate}}
</mat-error>
</mat-form-field>
<mat-form-field *ngIf="gatewayConfigGroup.get('storage.type').value === 'file'"
fxFlex="calc(50%-15px)" class="mat-block tb-value-type">
<mat-label translate>gateway.storage-max-files</mat-label>
<input matInput formControlName="max_file_count"/>
<mat-error *ngIf="gatewayConfigGroup.get('storage.max_file_count').hasError('required')">
{{'gateway.storage-max-files-required' | translate}}
</mat-error>
<mat-error *ngIf="gatewayConfigGroup.get('storage.max_file_count').hasError('min')">
{{'gateway.storage-max-files-min' | translate}}
</mat-error>
<mat-error *ngIf="gatewayConfigGroup.get('storage.max_file_count').hasError('pattern')">
{{'gateway.storage-max-files-pattern' | translate}}
</mat-error>
</mat-form-field>
<mat-form-field *ngIf="gatewayConfigGroup.get('storage.type').value === 'file'"
fxFlex="calc(50%-15px)" class="mat-block tb-value-type">
<mat-label translate>gateway.storage-max-read-record-count</mat-label>
<input matInput formControlName="max_read_records_count"/>
<mat-error *ngIf="gatewayConfigGroup.get('storage.max_read_records_count').hasError('required')">
{{'gateway.storage-max-read-record-count-required' | translate}}
</mat-error>
<mat-error *ngIf="gatewayConfigGroup.get('storage.max_read_records_count').hasError('min')">
{{'gateway.storage-max-read-record-count-min' | translate}}
</mat-error>
<mat-error *ngIf="gatewayConfigGroup.get('storage.max_read_records_count').hasError('pattern')">
{{'gateway.storage-max-read-record-count-pattern' | translate}}
</mat-error>
</mat-form-field>
<mat-form-field *ngIf="gatewayConfigGroup.get('storage.type').value === 'file'"
fxFlex="calc(50%-15px)" class="mat-block tb-value-type">
<mat-label translate>gateway.storage-max-file-records</mat-label>
<input matInput formControlName="max_records_per_file"/>
<mat-error *ngIf="gatewayConfigGroup.get('storage.max_records_per_file').hasError('required')">
{{'gateway.storage-max-records-required' | translate}}
</mat-error>
<mat-error *ngIf="gatewayConfigGroup.get('storage.max_records_per_file').hasError('min')">
{{'gateway.storage-max-records-min' | translate}}
</mat-error>
<mat-error *ngIf="gatewayConfigGroup.get('storage.max_records_per_file').hasError('pattern')">
{{'gateway.storage-max-records-pattern' | translate}}
</mat-error>
</mat-form-field>
<mat-form-field *ngIf="gatewayConfigGroup.get('storage.type').value === 'sqlite'"
fxFlex="calc(50%-15px)" class="mat-block tb-value-type">
<mat-label translate>gateway.storage-path</mat-label>
<input matInput formControlName="data_file_path"/>
<mat-error *ngIf="gatewayConfigGroup.get('storage.data_file_path').hasError('required')">
{{'gateway.storage-path-required' | translate}}
</mat-error>
</mat-form-field>
<mat-form-field *ngIf="gatewayConfigGroup.get('storage.type').value === 'sqlite'"
fxFlex="calc(50%-15px)" class="mat-block tb-value-type">
<mat-label translate>gateway.messages-ttl-check-in-hours</mat-label>
<input matInput formControlName="messages_ttl_check_in_hours"/>
<mat-error *ngIf="gatewayConfigGroup.get('storage.messages_ttl_check_in_hours').hasError('required')">
{{'gateway.messages-ttl-check-in-hours-required' | translate}}
</mat-error>
<mat-error *ngIf="gatewayConfigGroup.get('storage.messages_ttl_check_in_hours').hasError('min')">
{{'gateway.messages-ttl-check-in-hours-min' | translate}}
</mat-error>
<mat-error *ngIf="gatewayConfigGroup.get('storage.messages_ttl_check_in_hours').hasError('pattern')">
{{'gateway.messages-ttl-check-in-hours-pattern' | translate}}
</mat-error>
</mat-form-field>
<mat-form-field *ngIf="gatewayConfigGroup.get('storage.type').value === 'sqlite'"
fxFlex="calc(50%-15px)" class="mat-block tb-value-type" style="flex-grow: 0">
<mat-label translate>gateway.messages-ttl-in-days</mat-label>
<input matInput formControlName="messages_ttl_in_days"/>
<mat-error *ngIf="gatewayConfigGroup.get('storage.messages_ttl_in_days').hasError('required')">
{{'gateway.messages-ttl-in-days-required' | translate}}
</mat-error>
<mat-error *ngIf="gatewayConfigGroup.get('storage.messages_ttl_in_days').hasError('min')">
{{'gateway.messages-ttl-in-days-min' | translate}}
</mat-error>
<mat-error *ngIf="gatewayConfigGroup.get('storage.messages_ttl_in_days').hasError('pattern')">
{{'gateway.messages-ttl-in-days-pattern' | translate}}
</mat-error>
</mat-form-field>
</div>
</mat-expansion-panel>
</mat-accordion>
</div>
</mat-tab>
<mat-tab label="{{'gateway.grpc' | translate}}">
<div formGroupName="grpc" fxLayout="column" class="mat-content mat-padding">
<mat-accordion multi>
<mat-expansion-panel expanded="true" disabled style="padding-top: 20px">
<div fxLayout="row wrap">
<mat-slide-toggle color="primary" fxFlex="100" formControlName="enabled">
{{ 'gateway.grpc' | translate }}
</mat-slide-toggle>
<mat-slide-toggle color="primary" fxFlex="100" formControlName="keepalivePermitWithoutCalls">
{{ 'gateway.permit-without-calls' | translate }}
</mat-slide-toggle>
<mat-form-field fxFlex="calc(50%-15px)" class="mat-block tb-value-type">
<mat-label translate>gateway.server-port</mat-label>
<input matInput formControlName="serverPort" type="number"/>
<mat-error *ngIf="gatewayConfigGroup.get('grpc.serverPort').hasError('required')">
{{'gateway.thingsboard-port-required' | translate }}
</mat-error>
<mat-error *ngIf="gatewayConfigGroup.get('grpc.serverPort').hasError('min')">
{{'gateway.thingsboard-port-min' | translate }}
</mat-error>
<mat-error *ngIf="gatewayConfigGroup.get('grpc.serverPort').hasError('max')">
{{'gateway.thingsboard-port-max' | translate }}
</mat-error>
<mat-error *ngIf="gatewayConfigGroup.get('grpc.serverPort').hasError('pattern')">
{{'gateway.thingsboard-port-pattern' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field fxFlex="calc(50%-15px)" class="mat-block tb-value-type">
<mat-label translate>gateway.grpc-keep-alive-timeout</mat-label>
<input matInput formControlName="keepAliveTimeoutMs" type="number"/>
<mat-error *ngIf="gatewayConfigGroup.get('grpc.keepAliveTimeoutMs').hasError('required')">
{{'gateway.grpc-keep-alive-timeout-required' | translate }}
</mat-error>
<mat-error *ngIf="gatewayConfigGroup.get('grpc.keepAliveTimeoutMs').hasError('min')">
{{'gateway.grpc-keep-alive-timeout-min' | translate }}
</mat-error>
<mat-error *ngIf="gatewayConfigGroup.get('grpc.keepAliveTimeoutMs').hasError('pattern')">
{{'gateway.grpc-keep-alive-timeout-pattern' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field fxFlex="calc(50%-15px)" class="mat-block tb-value-type">
<mat-label translate>gateway.grpc-keep-alive</mat-label>
<input matInput formControlName="keepAliveTimeMs" type="number"/>
<mat-error *ngIf="gatewayConfigGroup.get('grpc.keepAliveTimeMs').hasError('required')">
{{'gateway.grpc-keep-alive-required' | translate }}
</mat-error>
<mat-error *ngIf="gatewayConfigGroup.get('grpc.keepAliveTimeMs').hasError('min')">
{{'gateway.grpc-keep-alive-min' | translate }}
</mat-error>
<mat-error *ngIf="gatewayConfigGroup.get('grpc.keepAliveTimeMs').hasError('pattern')">
{{'gateway.grpc-keep-alive-pattern' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field fxFlex="calc(50%-15px)" class="mat-block tb-value-type">
<mat-label translate>gateway.grpc-min-time-between-pings</mat-label>
<input matInput formControlName="minTimeBetweenPingsMs" type="number"/>
<mat-error *ngIf="gatewayConfigGroup.get('grpc.minTimeBetweenPingsMs').hasError('required')">
{{'gateway.grpc-min-time-between-pings-required' | translate }}
</mat-error>
<mat-error *ngIf="gatewayConfigGroup.get('grpc.minTimeBetweenPingsMs').hasError('min')">
{{'gateway.grpc-min-time-between-pings-min' | translate }}
</mat-error>
<mat-error *ngIf="gatewayConfigGroup.get('grpc.minTimeBetweenPingsMs').hasError('pattern')">
{{'gateway.grpc-min-time-between-pings-pattern' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field fxFlex="calc(50%-15px)" class="mat-block tb-value-type">
<mat-label translate>gateway.grpc-max-pings-without-data</mat-label>
<input matInput formControlName="maxPingsWithoutData" type="number"/>
<mat-error *ngIf="gatewayConfigGroup.get('grpc.maxPingsWithoutData').hasError('required')">
{{'gateway.grpc-max-pings-without-data-required' | translate }}
</mat-error>
<mat-error *ngIf="gatewayConfigGroup.get('grpc.maxPingsWithoutData').hasError('min')">
{{'gateway.grpc-max-pings-without-data-min' | translate }}
</mat-error>
<mat-error *ngIf="gatewayConfigGroup.get('grpc.maxPingsWithoutData').hasError('pattern')">
{{'gateway.grpc-max-pings-without-data-pattern' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field fxFlex="calc(50%-15px)" class="mat-block tb-value-type">
<mat-label translate>gateway.grpc-min-ping-interval-without-data</mat-label>
<input matInput formControlName="minPingIntervalWithoutDataMs" type="number"/>
<mat-error *ngIf="gatewayConfigGroup.get('grpc.minPingIntervalWithoutDataMs').hasError('required')">
{{'gateway.grpc-min-ping-interval-without-data-required' | translate }}
</mat-error>
<mat-error *ngIf="gatewayConfigGroup.get('grpc.minPingIntervalWithoutDataMs').hasError('min')">
{{'gateway.grpc-min-ping-interval-without-data-min' | translate }}
</mat-error>
<mat-error *ngIf="gatewayConfigGroup.get('grpc.minPingIntervalWithoutDataMs').hasError('pattern')">
{{'gateway.grpc-min-ping-interval-without-data-pattern' | translate }}
</mat-error>
</mat-form-field>
</div>
</mat-expansion-panel>
</mat-accordion>
</div>
</mat-tab>
<mat-tab label="{{'gateway.statistics.statistics' | translate}}">
<div formGroupName="thingsboard" fxLayout="column" class="mat-content mat-padding">
<mat-accordion multi>
<mat-expansion-panel expanded="true" disabled style="padding-top: 20px">
<div fxLayout="row wrap" formGroupName="statistics">
<mat-slide-toggle color="primary" fxFlex="100" formControlName="enable">
{{ 'gateway.statistics.statistics' | translate }}
</mat-slide-toggle>
<mat-form-field fxFlex="calc(50%-15px)" class="mat-block tb-value-type">
<mat-label translate>gateway.statistics.send-period</mat-label>
<input matInput formControlName="statsSendPeriodInSeconds" type="number"/>
<mat-error
*ngIf="gatewayConfigGroup.get('thingsboard.statistics.statsSendPeriodInSeconds').hasError('required')">
{{'gateway.statistics.send-period-required' | translate }}
</mat-error>
<mat-error
*ngIf="gatewayConfigGroup.get('thingsboard.statistics.statsSendPeriodInSeconds').hasError('min')">
{{'gateway.statistics.send-period-min' | translate }}
</mat-error>
<mat-error
*ngIf="gatewayConfigGroup.get('thingsboard.statistics.statsSendPeriodInSeconds').hasError('pattern')">
{{'gateway.statistics.send-period-pattern' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field fxFlex="calc(50%-15px)" class="mat-block tb-value-type">
<mat-label translate>gateway.statistics.check-connectors-configuration</mat-label>
<input matInput formControlName="checkConnectorsConfigurationInSeconds" type="number"/>
<mat-error
*ngIf="gatewayConfigGroup.get('thingsboard.statistics.checkConnectorsConfigurationInSeconds').hasError('required')">
{{'gateway.statistics.check-connectors-configuration-required' | translate }}
</mat-error>
<mat-error
*ngIf="gatewayConfigGroup.get('thingsboard.statistics.checkConnectorsConfigurationInSeconds').hasError('min')">
{{'gateway.statistics.check-connectors-configuration-min' | translate }}
</mat-error>
<mat-error
*ngIf="gatewayConfigGroup.get('thingsboard.statistics.checkConnectorsConfigurationInSeconds').hasError('pattern')">
{{'gateway.statistics.check-connectors-configuration-pattern' | translate }}
</mat-error>
</mat-form-field>
</div>
</mat-expansion-panel>
<mat-expansion-panel expanded="true" disabled>
<mat-expansion-panel-header>
<mat-panel-title translate class="expansion-panel-header">gateway.statistics.statistics</mat-panel-title>
</mat-expansion-panel-header>
<div fxLayout="column" formGroupName="statistics">
<div fxLayout="row" formArrayName="commands"
*ngFor="let commandControl of commandFormArray().controls; let $index = index"
style="margin-bottom: 15px">
<mat-card fxLayout="row wrap" style="margin-bottom: 15px" [formGroupName]="$index">
<mat-form-field fxFlex="calc(50%-15px)" class="mat-block tb-value-type">
<mat-label translate>gateway.statistics.attribute-name</mat-label>
<input matInput formControlName="attributeName"/>
<mat-error
*ngIf="commandControl.get('attributeName').hasError('required')">
{{'gateway.statistics.attribute-name-required' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field fxFlex="calc(50%-15px)" class="mat-block tb-value-type">
<mat-label translate>gateway.statistics.timeout</mat-label>
<input matInput formControlName="timeout" type="number"/>
<mat-error
*ngIf="commandControl.get('timeout').hasError('required')">
{{'gateway.statistics.timeout-required' | translate }}
</mat-error>
<mat-error
*ngIf="commandControl.get('timeout').hasError('min')">
{{'gateway.statistics.timeout-min' | translate }}
</mat-error>
<mat-error
*ngIf="commandControl.get('timeout').hasError('pattern')">
{{'gateway.statistics.timeout-pattern' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field fxFlex="calc(50%-15px)" class="mat-block tb-value-type" style="flex-grow: 0">
<mat-label translate>gateway.statistics.command</mat-label>
<input matInput formControlName="command"/>
<mat-error
*ngIf="commandControl.get('command').hasError('required')">
{{'gateway.statistics.command-required' | translate }}
</mat-error>
</mat-form-field>
</mat-card>
<button mat-icon-button (click)="removeCommandControl($index)"
[disabled]="!gatewayConfigGroup.get('thingsboard.remoteConfiguration').value"
matTooltip="{{ 'gateway.statistics.remove' | translate }}"
matTooltipPosition="above">
<mat-icon>close</mat-icon>
</button>
</div>
<button mat-stroked-button color="primary"
style="width: fit-content;"
type="button"
cdkFocusInitial
[disabled]="!gatewayConfigGroup.get('thingsboard.remoteConfiguration').value"
(click)="addCommand()">
{{ 'gateway.statistics.add' | translate }}
</button>
</div>
</mat-expansion-panel>
</mat-accordion>
</div>
</mat-tab>
<mat-tab label="{{'gateway.other' | translate}}">
<div formGroupName="thingsboard" fxLayout="column" class="mat-content mat-padding">
<mat-accordion multi>
<mat-expansion-panel expanded="true" disabled formGroupName="checkingDeviceActivity">
<mat-expansion-panel-header>
<mat-panel-title class="expansion-panel-header">
<mat-slide-toggle color="primary" fxFlex="100" formControlName="checkDeviceInactivity">
{{ 'gateway.checking-device-activity' | translate }}
</mat-slide-toggle>
</mat-panel-title>
</mat-expansion-panel-header>
<div fxLayout="row wrap">
<mat-form-field
*ngIf="gatewayConfigGroup.get('thingsboard.checkingDeviceActivity.checkDeviceInactivity').value"
fxFlex="calc(50%-15px)" class="mat-block tb-value-type">
<mat-label translate>gateway.inactivity-timeout-seconds</mat-label>
<input matInput formControlName="inactivityTimeoutSeconds" type="number"/>
<mat-error
*ngIf="gatewayConfigGroup.get('thingsboard.checkingDeviceActivity.inactivityTimeoutSeconds').hasError('required')">
{{'gateway.inactivity-timeout-seconds-required' | translate }}
</mat-error>
<mat-error
*ngIf="gatewayConfigGroup.get('thingsboard.checkingDeviceActivity.inactivityTimeoutSeconds').hasError('min')">
{{'gateway.inactivity-timeout-seconds-min' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field
*ngIf="gatewayConfigGroup.get('thingsboard.checkingDeviceActivity.checkDeviceInactivity').value"
fxFlex="calc(50%-15px)" class="mat-block tb-value-type">
<mat-label translate>gateway.inactivity-check-period-seconds</mat-label>
<input matInput formControlName="inactivityCheckPeriodSeconds"/>
<mat-error
*ngIf="gatewayConfigGroup.get('thingsboard.checkingDeviceActivity.inactivityCheckPeriodSeconds').hasError('required')">
{{'gateway.inactivity-check-period-seconds-required' | translate }}
</mat-error>
<mat-error
*ngIf="gatewayConfigGroup.get('thingsboard.checkingDeviceActivity.inactivityCheckPeriodSeconds').hasError('min')">
{{'gateway.inactivity-check-period-seconds-min' | translate }}
</mat-error>
</mat-form-field>
</div>
</mat-expansion-panel>
<mat-expansion-panel expanded="true" disabled >
<mat-expansion-panel-header>
<mat-panel-title translate class="expansion-panel-header">gateway.advanced</mat-panel-title>
</mat-expansion-panel-header>
<div fxLayout="row wrap">
<mat-form-field fxFlex="calc(50%-15px)" class="mat-block tb-value-type">
<mat-label translate>gateway.min-pack-send-delay</mat-label>
<input matInput formControlName="minPackSendDelayMS" type="number" min="0"/>
<mat-error *ngIf="gatewayConfigGroup.get('thingsboard.minPackSendDelayMS').hasError('required')">
{{ 'gateway.min-pack-send-delay-required' | translate }}
</mat-error>
<mat-error *ngIf="gatewayConfigGroup.get('thingsboard.minPackSendDelayMS').hasError('min')">
{{ 'gateway.min-pack-send-delay-min' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field fxFlex="calc(50%-15px)" class="mat-block tb-value-type">
<mat-label translate>gateway.mqtt-qos</mat-label>
<input matInput formControlName="qos" type="number" min="0" max="2"/>
<mat-error *ngIf="gatewayConfigGroup.get('thingsboard.qos').hasError('required')">
{{ 'gateway.mqtt-qos-required' | translate}}
</mat-error>
<mat-error *ngIf="gatewayConfigGroup.get('thingsboard.qos').hasError('min')">
{{ 'gateway.mqtt-qos-range' | translate}}
</mat-error>
<mat-error *ngIf="gatewayConfigGroup.get('thingsboard.qos').hasError('max')">
{{ 'gateway.mqtt-qos-range' | translate}}
</mat-error>
</mat-form-field>
</div>
</mat-expansion-panel>
</mat-accordion>
</div>
</mat-tab>
</mat-tab-group>
<div fxFlex></div>
<div mat-dialog-actions fxLayoutAlign="end center">
<button mat-button color="primary"
type="button"
cdkFocusInitial
(click)="cancel()">
{{ 'action.cancel' | translate }}
</button>
<button mat-raised-button color="primary"
type="button"
[disabled]="gatewayConfigGroup.invalid || !gatewayConfigGroup.dirty"
cdkFocusInitial
(click)="saveConfig()">
{{ 'action.save' | translate }}
</button>
</div>
</form>

166
ui-ngx/src/app/shared/components/device/gateway-configuration.component.scss

@ -0,0 +1,166 @@
/**
* ThingsBoard, Inc. ("COMPANY") CONFIDENTIAL
*
* Copyright © 2016-2023 ThingsBoard, Inc. All Rights Reserved.
*
* NOTICE: All information contained herein is, and remains
* the property of ThingsBoard, Inc. and its suppliers,
* if any. The intellectual and technical concepts contained
* herein are proprietary to ThingsBoard, Inc.
* and its suppliers and may be covered by U.S. and Foreign Patents,
* patents in process, and are protected by trade secret or copyright law.
*
* Dissemination of this information or reproduction of this material is strictly forbidden
* unless prior written permission is obtained from COMPANY.
*
* Access to the source code contained herein is hereby forbidden to anyone except current COMPANY employees,
* managers or contractors who have executed Confidentiality and Non-disclosure agreements
* explicitly covering such access.
*
* The copyright notice above does not evidence any actual or intended publication
* or disclosure of this source code, which includes
* information that is confidential and/or proprietary, and is a trade secret, of COMPANY.
* ANY REPRODUCTION, MODIFICATION, DISTRIBUTION, PUBLIC PERFORMANCE,
* OR PUBLIC DISPLAY OF OR THROUGH USE OF THIS SOURCE CODE WITHOUT
* THE EXPRESS WRITTEN CONSENT OF COMPANY IS STRICTLY PROHIBITED,
* AND IN VIOLATION OF APPLICABLE LAWS AND INTERNATIONAL TREATIES.
* THE RECEIPT OR POSSESSION OF THIS SOURCE CODE AND/OR RELATED INFORMATION
* DOES NOT CONVEY OR IMPLY ANY RIGHTS TO REPRODUCE, DISCLOSE OR DISTRIBUTE ITS CONTENTS,
* OR TO MANUFACTURE, USE, OR SELL ANYTHING THAT IT MAY DESCRIBE, IN WHOLE OR IN PART.
*/
:host {
width: 100%;
height: 100%;
display: block;
.mat-toolbar {
background: transparent;
color: rgba(0, 0, 0, .87) !important;
}
.mat-content {
.expansion-panel-header {
font-weight: 600;
color: rgba(0, 0, 0, .87) !important;
}
mat-slide-toggle {
margin-bottom: 16px;
}
mat-form-field {
margin-right: 15px;
}
.block-title {
font-size: 20px;
font-weight: 400;
padding-top: 16px;
}
.hover-cursor {
cursor: pointer;
}
}
.security-toggle-group {
padding-bottom: 0;
background: transparent !important;
margin-bottom: 15px;
border: none;
.mat-button-toggle {
border-radius: 15px;
border: 1px solid rgba(0, 0, 0, 0);
background: rgba(0, 0, 0, .05);
margin: 5px;
color: rgba(0, 0, 0, .2);
font-weight: 500;
}
.mat-button-toggle-checked {
border-radius: 15px;
border: 1px solid #305680;
background: white;
color: #305680;
}
::ng-deep span {
text-transform: capitalize;
line-height: 20px;
font-size: 13px;
padding: 5px 25px;
}
::ng-deep .mat-button-toggle-ripple {
border-radius: 15px;
}
::ng-deep .mat-button-toggle-appearance-standard:not(.mat-button-toggle-disabled):hover .mat-button-toggle-focus-overlay {
opacity: 0 !important;
}
}
.logs-label {
font-weight: 500;
margin-bottom: 10px;
}
.logs-toggle-group {
border: none;
border-radius: 15px;
background: rgba(0, 0, 0, .05);
color: rgba(0, 0, 0, .2);
font-weight: 500;
.mat-button-toggle {
border: none;
background: rgba(0, 0, 0, .0);
color: rgba(0, 0, 0, .2);
font-weight: 500;
}
.mat-button-toggle-checked {
border-radius: 15px;
color: white;
background: #305680;
}
::ng-deep span {
line-height: 20px;
font-size: 13px;
padding: 5px 25px;
}
::ng-deep .mat-button-toggle-ripple {
border-radius: 15px;
}
::ng-deep .mat-button-toggle-appearance-standard:not(.mat-button-toggle-disabled):hover .mat-button-toggle-focus-overlay {
opacity: 0 !important;
}
}
.first-capital {
text-transform: capitalize;
}
::ng-deep.mat-tab-label-active {
color: white;
opacity: 1;
}
::ng-deep.mat-tab-label, ::ng-deep.mat-tab-label-active {
min-width: 50px !important;
padding: 3px !important;
margin: 3px !important;
flex-grow: 1;
}
::ng-deep.mat-ink-bar {
height: 100%;
z-index: -10;
border-radius: 5px;
}
}

521
ui-ngx/src/app/shared/components/device/gateway-configuration.component.ts

@ -0,0 +1,521 @@
///
/// ThingsBoard, Inc. ("COMPANY") CONFIDENTIAL
///
/// Copyright © 2016-2023 ThingsBoard, Inc. All Rights Reserved.
///
/// NOTICE: All information contained herein is, and remains
/// the property of ThingsBoard, Inc. and its suppliers,
/// if any. The intellectual and technical concepts contained
/// herein are proprietary to ThingsBoard, Inc.
/// and its suppliers and may be covered by U.S. and Foreign Patents,
/// patents in process, and are protected by trade secret or copyright law.
///
/// Dissemination of this information or reproduction of this material is strictly forbidden
/// unless prior written permission is obtained from COMPANY.
///
/// Access to the source code contained herein is hereby forbidden to anyone except current COMPANY employees,
/// managers or contractors who have executed Confidentiality and Non-disclosure agreements
/// explicitly covering such access.
///
/// The copyright notice above does not evidence any actual or intended publication
/// or disclosure of this source code, which includes
/// information that is confidential and/or proprietary, and is a trade secret, of COMPANY.
/// ANY REPRODUCTION, MODIFICATION, DISTRIBUTION, PUBLIC PERFORMANCE,
/// OR PUBLIC DISPLAY OF OR THROUGH USE OF THIS SOURCE CODE WITHOUT
/// THE EXPRESS WRITTEN CONSENT OF COMPANY IS STRICTLY PROHIBITED,
/// AND IN VIOLATION OF APPLICABLE LAWS AND INTERNATIONAL TREATIES.
/// THE RECEIPT OR POSSESSION OF THIS SOURCE CODE AND/OR RELATED INFORMATION
/// DOES NOT CONVEY OR IMPLY ANY RIGHTS TO REPRODUCE, DISCLOSE OR DISTRIBUTE ITS CONTENTS,
/// OR TO MANUFACTURE, USE, OR SELL ANYTHING THAT IT MAY DESCRIBE, IN WHOLE OR IN PART.
///
import { Component, Input, OnInit } from '@angular/core';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { Router } from '@angular/router';
import { FormArray, FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms';
import { EntityId } from '@shared/models/id/entity-id';
import { MatDialog, MatDialogRef } from '@angular/material/dialog';
import { base64toObj, base64toString, objToBase64, stringToBase64 } from '@core/utils';
import { AttributeService } from '@core/http/attribute.service';
import { AttributeScope } from '@shared/models/telemetry/telemetry.models';
import {
GatewayRemoteConfigurationDialogComponent,
GatewayRemoteConfigurationDialogData
} from '@shared/components/dialog/gateway-remote-configuration-dialog';
import { DeviceService } from '@core/http/device.service';
export enum StorageTypes {
MEMORY = 'memory',
FILE = 'file',
SQLITE = 'sqlite'
}
export enum GatewayLogLevel {
none = 'NONE',
critical = 'CRITICAL',
error = 'ERROR',
warning = 'WARNING',
info = 'INFO',
debug = 'DEBUG'
}
export enum LogSavingPeriod {
days = 'd',
hours = 'h',
minutes = 'm',
seconds = 's'
}
export enum LocalLogsConfigs {
service = 'service',
connector = 'connector',
converter = 'converter',
tb_connection = 'tb_connection',
storage = 'storage',
extension = 'extension'
}
export const logSavingPeriodTranslations = new Map<LogSavingPeriod, string>(
[
[LogSavingPeriod.days, 'gateway.logs.days'],
[LogSavingPeriod.hours, 'gateway.logs.hours'],
[LogSavingPeriod.minutes, 'gateway.logs.minutes'],
[LogSavingPeriod.seconds, 'gateway.logs.seconds']
]
);
export const storageTypesTranslations = new Map<StorageTypes, string>(
[
[StorageTypes.MEMORY, 'gateway.storage-types.memory-storage'],
[StorageTypes.FILE, 'gateway.storage-types.file-storage'],
[StorageTypes.SQLITE, 'gateway.storage-types.sqlite']
]
);
export enum SecurityTypes {
ACCESS_TOKEN = 'accessToken',
USERNAME_PASSWORD = 'usernamePassword',
TLS_ACCESS_TOKEN = 'tlsAccessToken',
TLS_PRIVATE_KEY = 'tlsPrivateKey'
}
export const securityTypesTranslationsMap = new Map<SecurityTypes, string>(
[
[SecurityTypes.ACCESS_TOKEN, 'gateway.security-types.access-token'],
[SecurityTypes.USERNAME_PASSWORD, 'gateway.security-types.username-password'],
[SecurityTypes.TLS_ACCESS_TOKEN, 'gateway.security-types.tls-access-token'],
[SecurityTypes.TLS_PRIVATE_KEY, 'gateway.security-types.tls-private-key'],
]
);
@Component({
selector: 'tb-gateway-configuration',
templateUrl: './gateway-configuration.component.html',
styleUrls: ['./gateway-configuration.component.scss']
})
export class GatewayConfigurationComponent implements OnInit {
gatewayConfigGroup: FormGroup;
storageTypes = storageTypesTranslations;
logSavingPeriods = logSavingPeriodTranslations;
securityTypes = securityTypesTranslationsMap;
gatewayLogLevel = Object.values(GatewayLogLevel);
localLogsConfigs = Object.values(LocalLogsConfigs);
@Input()
device: EntityId;
@Input()
dialogRef: MatDialogRef<any>;
logSelector: FormControl;
constructor(protected router: Router,
protected store: Store<AppState>,
protected fb: FormBuilder,
protected attributeService: AttributeService,
protected deviceService: DeviceService,
public dialog: MatDialog) {
}
ngOnInit() {
this.gatewayConfigGroup = this.fb.group({
thingsboard: this.fb.group({
host: ['thingsboard.cloud', [Validators.required]],
port: [1883, [Validators.required, Validators.min(1), Validators.max(65535), Validators.pattern(/^-?[0-9]+$/)]],
remoteShell: [false, []],
remoteConfiguration: [true, []],
statistics: this.fb.group({
enable: [true, []],
statsSendPeriodInSeconds: [3600, [Validators.required, Validators.min(0), Validators.pattern(/^-?[0-9]+$/)]],
checkConnectorsConfigurationInSeconds: [60, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
handleDeviceRenaming: [false, []],
commands: this.fb.array([], [])
}),
maxPayloadSizeBytes: [1024, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
minPackSendDelayMS: [200, [Validators.required, Validators.min(0), Validators.pattern(/^-?[0-9]+$/)]],
minPackSizeToSend: [500, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
handleDeviceRenaming: [true, []],
checkingDeviceActivity: this.fb.group({
checkDeviceInactivity: [false, []],
inactivityTimeoutSeconds: [200, [Validators.min(1)]],
inactivityCheckPeriodSeconds: [500, [Validators.min(1)]]
}),
security: this.fb.group({
type: [SecurityTypes.ACCESS_TOKEN, [Validators.required]],
accessToken: [null, [Validators.required]],
clientId: [null, []],
username: [null, []],
password: [null, []],
caCert: [null, []],
cert: [null, []],
privateKey: [null, []],
}),
qos: [1, [Validators.min(0), Validators.max(2), Validators.required]]
}),
storage: this.fb.group({
type: [StorageTypes.MEMORY, [Validators.required]],
read_records_count: [100, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required]],
max_records_count: [100000, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required]],
data_folder_path: ['./data/', []],
max_file_count: [10, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
max_read_records_count: [10, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
max_records_per_file: [10000, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
data_file_path: ['./data/data.db', []],
messages_ttl_check_in_hours: [1, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
messages_ttl_in_days: [7, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
}),
grpc: this.fb.group({
enabled: [false, []],
serverPort: [9595, [Validators.required, Validators.min(1), Validators.max(65535), Validators.pattern(/^-?[0-9]+$/)]],
keepAliveTimeMs: [10000, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
keepAliveTimeoutMs: [5000, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
keepalivePermitWithoutCalls: [true, []],
maxPingsWithoutData: [0, [Validators.required, Validators.min(0), Validators.pattern(/^-?[0-9]+$/)]],
minTimeBetweenPingsMs: [10000, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
minPingIntervalWithoutDataMs: [5000, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
}),
connectors: this.fb.array([]),
logs: this.fb.group({
dateFormat: ['%Y-%m-%d %H:%M:%S', [Validators.required]],
logFormat: ['%(asctime)s - |%(levelname)s| - [%(filename)s] - %(module)s - %(funcName)s - %(lineno)d - %(message)s',
[Validators.required]],
type: ['remote', [Validators.required]],
remote: this.fb.group({
enabled: [false],
logLevel: [GatewayLogLevel.info, [Validators.required]],
}),
local: this.fb.group({})
})
});
this.gatewayConfigGroup.get('thingsboard.remoteConfiguration').valueChanges.subscribe(enabled => {
if (!enabled) {
this.openConfigurationConfirmDialog();
}
});
this.logSelector = this.fb.control(LocalLogsConfigs.service);
for (const localLogsConfigsKey of Object.keys(LocalLogsConfigs)) {
this.addLocalLogConfig(localLogsConfigsKey, {});
}
const checkingDeviceActivityGroup = this.gatewayConfigGroup.get('thingsboard.checkingDeviceActivity') as FormGroup;
checkingDeviceActivityGroup.get('checkDeviceInactivity').valueChanges.subscribe(enabled => {
checkingDeviceActivityGroup.updateValueAndValidity();
if (enabled) {
checkingDeviceActivityGroup.get('inactivityTimeoutSeconds').setValidators([Validators.min(1), Validators.required]);
checkingDeviceActivityGroup.get('inactivityCheckPeriodSeconds').setValidators([Validators.min(1), Validators.required]);
} else {
checkingDeviceActivityGroup.get('inactivityTimeoutSeconds').clearValidators();
checkingDeviceActivityGroup.get('inactivityTimeoutSeconds').setErrors(null);
checkingDeviceActivityGroup.get('inactivityCheckPeriodSeconds').clearValidators();
checkingDeviceActivityGroup.get('inactivityCheckPeriodSeconds').setErrors(null);
}
});
const securityGroup = this.gatewayConfigGroup.get('thingsboard.security') as FormGroup;
securityGroup.get('type').valueChanges.subscribe(type => {
this.removeAllSecurityValidators();
if (type === SecurityTypes.ACCESS_TOKEN) {
securityGroup.get('accessToken').addValidators([Validators.required]);
securityGroup.get('accessToken').updateValueAndValidity();
} else if (type === SecurityTypes.TLS_PRIVATE_KEY) {
securityGroup.get('caCert').addValidators([Validators.required]);
securityGroup.get('caCert').updateValueAndValidity();
securityGroup.get('privateKey').addValidators([Validators.required]);
securityGroup.get('privateKey').updateValueAndValidity();
securityGroup.get('cert').addValidators([Validators.required]);
securityGroup.get('cert').updateValueAndValidity();
} else if (type === SecurityTypes.TLS_ACCESS_TOKEN) {
securityGroup.get('accessToken').addValidators([Validators.required]);
securityGroup.get('accessToken').updateValueAndValidity();
securityGroup.get('caCert').addValidators([Validators.required]);
securityGroup.get('caCert').updateValueAndValidity();
} else if (type === SecurityTypes.USERNAME_PASSWORD) {
securityGroup.get('clientId').addValidators([Validators.required]);
securityGroup.get('clientId').updateValueAndValidity();
securityGroup.get('username').addValidators([Validators.required]);
securityGroup.get('username').updateValueAndValidity();
securityGroup.get('password').addValidators([Validators.required]);
securityGroup.get('password').updateValueAndValidity();
}
securityGroup.updateValueAndValidity();
});
const storageGroup = this.gatewayConfigGroup.get('storage') as FormGroup;
storageGroup.get('type').valueChanges.subscribe(type => {
this.removeAllStorageValidators();
if (type === StorageTypes.MEMORY) {
storageGroup.get('read_records_count').addValidators(
[Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]);
storageGroup.get('max_records_count').addValidators(
[Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required]);
} else if (type === StorageTypes.FILE) {
storageGroup.get('data_folder_path').addValidators([Validators.required]);
storageGroup.get('max_file_count').addValidators(
[Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required]);
storageGroup.get('max_read_records_count').addValidators(
[Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required]);
storageGroup.get('max_records_per_file').addValidators(
[Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required]);
} else if (type === StorageTypes.SQLITE) {
storageGroup.get('data_file_path').addValidators([Validators.required]);
storageGroup.get('messages_ttl_check_in_hours').addValidators(
[Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required]);
storageGroup.get('messages_ttl_in_days').addValidators(
[Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required]);
}
});
this.fetchConfigAttribute(this.device);
}
fetchConfigAttribute(entityId: EntityId) {
this.attributeService.getEntityAttributes(entityId, AttributeScope.SHARED_SCOPE,
['configuration', 'RemoteLoggingLevel']).subscribe(attributes => {
if (attributes.length) {
const configuration = attributes.find(attribute => attribute.key === 'configuration').value;
const remoteLoggingLevel = attributes.find(attribute => attribute.key === 'RemoteLoggingLevel').value;
if (configuration) {
const configObj = base64toObj(configuration).thingsboard;
configObj.logs = this.logsBase64ToObj(configObj.logs);
if (configObj.thingsboard.statistics && configObj.thingsboard.statistics.commands) {
for (const command of Object.values(configObj.thingsboard.statistics.commands)) {
this.addCommand(command);
}
delete configObj.thingsboard.statistics.commands;
}
this.gatewayConfigGroup.patchValue(configObj, {emitEvent: false});
this.gatewayConfigGroup.markAsDirty();
if (!configObj.thingsboard.remoteConfiguration) {
this.gatewayConfigGroup.disable({emitEvent: false});
}
}
if (remoteLoggingLevel) {
const remoteLogsFormGroup = this.gatewayConfigGroup.get('logs.remote');
remoteLogsFormGroup.patchValue({
enabled: remoteLoggingLevel !== GatewayLogLevel.none,
logLevel: remoteLoggingLevel
}, {emitEvent: false});
remoteLogsFormGroup.markAsDirty();
}
}
});
}
logsBase64ToObj(logs64: string) {
const logsString = base64toString(logs64).split('"').join('');
const logsObject = {};
logsString.split('logger_').forEach((split, index) => {
if (index > 0) {
const key = split.split(']')[0];
if (LocalLogsConfigs[key]) {
const logLevel = split.split('=')[1].replace('}}handlers', '');
logsObject[key] = {
logLevel
};
}
}
});
logsString.split('handler_').forEach((split, index) => {
if (index > 0) {
const key = split.split('Handler]')[0];
if (LocalLogsConfigs[key]) {
const args = split.split('args=(')[1].split(',');
logsObject[key].filePath = args[0].replace(`/${key}.log`, '');
logsObject[key].savingPeriod = args[1].replace(' ', '');
logsObject[key].savingTime = +args[2];
logsObject[key].backupCount = +args[3];
}
}
});
const formatArgs = logsString.split('format=')[1].split('}}datefmt=');
const logFormat = formatArgs[0];
const dateFormat = formatArgs[1];
return {local: logsObject, logFormat, dateFormat};
}
addCommand(command?): void {
const data = command || {};
const commandsFormArray = this.commandFormArray();
const commandFormGroup = this.fb.group({
attributeName: [data.attributeName || null, [Validators.required]],
command: [data.command || null, [Validators.required]],
timeout: [data.timeout || null, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
});
commandsFormArray.push(commandFormGroup);
}
addLocalLogConfig(name, config): void {
const localLogsFormGroup = this.gatewayConfigGroup.get('logs.local') as FormGroup;
const configGroup = this.fb.group({
logLevel: [config.logLevel || GatewayLogLevel.info, [Validators.required]],
filePath: [config.filePath || './data', [Validators.required]],
backupCount: [config.backupCount || 7, [Validators.required, Validators.min(0)]],
savingTime: [config.savingTime || 3, [Validators.required, Validators.min(0)]],
savingPeriod: [config.savingPeriod || LogSavingPeriod.days, [Validators.required]]
});
localLogsFormGroup.addControl(name, configGroup);
}
getLogFormGroup(value: string): FormGroup {
return this.gatewayConfigGroup.get(`logs.local.${value}`) as FormGroup;
}
commandFormArray(): FormArray {
return this.gatewayConfigGroup.get('thingsboard.statistics.commands') as FormArray;
}
removeCommandControl(index: number): void {
this.commandFormArray().removeAt(index);
}
removeAllSecurityValidators(): void {
const securityGroup = this.gatewayConfigGroup.get('thingsboard.security') as FormGroup;
for (const controlsKey in securityGroup.controls) {
if (controlsKey !== 'type') {
securityGroup.controls[controlsKey].clearValidators();
securityGroup.controls[controlsKey].setErrors(null);
securityGroup.controls[controlsKey].updateValueAndValidity();
}
}
}
removeAllStorageValidators(): void {
const storageGroup = this.gatewayConfigGroup.get('storage') as FormGroup;
for (const storageKey in storageGroup.controls) {
if (storageKey !== 'type') {
storageGroup.controls[storageKey].clearValidators();
storageGroup.controls[storageKey].setErrors(null);
storageGroup.controls[storageKey].updateValueAndValidity();
}
}
}
removeEmpty(obj) {
return Object.fromEntries(
Object.entries(obj)
.filter(([_, v]) => v != null)
.map(([k, v]) => [k, v === Object(v) ? this.removeEmpty(v) : v])
);
}
generateLogsFile(logsObj): string {
const line = '[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_database]}}' +
'level=INFO}}' +
'handlers=databaseHandler}}' +
'formatter=LogFormatter}}' +
'qualname=database}}';
let loggerLine = '';
let handlerLine = '';
for (const key of Object.keys(logsObj.local)) {
loggerLine += `[logger_${key}]}}` +
`level=${logsObj.local[key].logLevel}}}` +
`handlers=${key}Handler}}` +
`formatter=LogFormatter}}` +
`qualname=${key}}}`;
handlerLine += `[handler_${key}Handler]}}` +
`level=${logsObj.local[key].logLevel}}}` +
`class=thingsboard_gateway.tb_utility.tb_logger.TimedRotatingFileHandler}}` +
`formatter=LogFormatter}}` +
`args=("${logsObj.local[key].filePath}/${key}.log", ${logsObj.local[key].savingPeriod},` +
` ${logsObj.local[key].savingTime}, ${logsObj.local[key].backupCount},)}}`;
}
const logerEnding = `[formatter_LogFormatter]}}` +
`format="${logsObj.logFormat}"}}` +
`datefmt="${logsObj.dateFormat}"`;
return line + loggerLine + handlerLine + logerEnding;
}
saveConfig(): void {
const value = this.removeEmpty(this.gatewayConfigGroup.value);
const attributes = [];
attributes.push({
key: 'RemoteLoggingLevel',
value: value.logs.remote.logLevel
});
delete value.connectors;
value.logs = stringToBase64(this.generateLogsFile(value.logs));
const configuration = objToBase64({thingsboard: value});
attributes.push({
key: 'configuration',
value: configuration
});
this.attributeService.saveEntityAttributes(this.device, AttributeScope.SHARED_SCOPE, attributes).subscribe(_ => {
if (this.dialogRef) {
this.dialogRef.close();
}
});
}
cancel(): void {
if (this.dialogRef) {
this.dialogRef.close();
}
}
private openConfigurationConfirmDialog(): void {
this.deviceService.getDevice(this.device.id).subscribe(gateway => {
this.dialog.open<GatewayRemoteConfigurationDialogComponent, GatewayRemoteConfigurationDialogData>
(GatewayRemoteConfigurationDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
gatewayName: gateway.name
}
}).afterClosed().subscribe(
(res) => {
if (!res) {
this.gatewayConfigGroup.get('thingsboard.remoteConfiguration').setValue(true, {emitEvent: false});
}
}
);
});
}
}

143
ui-ngx/src/app/shared/components/device/gateway-connectors.component.html

@ -0,0 +1,143 @@
<!--
ThingsBoard, Inc. ("COMPANY") CONFIDENTIAL
Copyright © 2016-2023 ThingsBoard, Inc. All Rights Reserved.
NOTICE: All information contained herein is, and remains
the property of ThingsBoard, Inc. and its suppliers,
if any. The intellectual and technical concepts contained
herein are proprietary to ThingsBoard, Inc.
and its suppliers and may be covered by U.S. and Foreign Patents,
patents in process, and are protected by trade secret or copyright law.
Dissemination of this information or reproduction of this material is strictly forbidden
unless prior written permission is obtained from COMPANY.
Access to the source code contained herein is hereby forbidden to anyone except current COMPANY employees,
managers or contractors who have executed Confidentiality and Non-disclosure agreements
explicitly covering such access.
The copyright notice above does not evidence any actual or intended publication
or disclosure of this source code, which includes
information that is confidential and/or proprietary, and is a trade secret, of COMPANY.
ANY REPRODUCTION, MODIFICATION, DISTRIBUTION, PUBLIC PERFORMANCE,
OR PUBLIC DISPLAY OF OR THROUGH USE OF THIS SOURCE CODE WITHOUT
THE EXPRESS WRITTEN CONSENT OF COMPANY IS STRICTLY PROHIBITED,
AND IN VIOLATION OF APPLICABLE LAWS AND INTERNATIONAL TREATIES.
THE RECEIPT OR POSSESSION OF THIS SOURCE CODE AND/OR RELATED INFORMATION
DOES NOT CONVEY OR IMPLY ANY RIGHTS TO REPRODUCE, DISCLOSE OR DISTRIBUTE ITS CONTENTS,
OR TO MANUFACTURE, USE, OR SELL ANYTHING THAT IT MAY DESCRIBE, IN WHOLE OR IN PART.
-->
<form style="height: 100%" fxLayout="column" [formGroup]="gatewayConnectorsGroup">
<mat-toolbar color="primary">
<h2 translate>gateway.connectors</h2>
<span fxFlex></span>
<button mat-icon-button
type="button"
(click)="cancel()"
*ngIf="dialogRef">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<div fxLayout="row" fxLayoutGap="15px" fxFlex style="height: calc(100% - 35px)">
<div fxFlex="30" fxLayout="column" style="overflow: auto">
<mat-accordion multi>
<mat-expansion-panel expanded="true"
formArrayName="connectors"
*ngFor="let connectorControl of connectorsFormArray().controls; let $index = index"
[ngClass]="{'mat-card-selected': $index === selectedConnector}">
<mat-expansion-panel-header>
<mat-panel-title class="expansion-panel-header">
{{generatePanelTitle(connectorControl)}}
</mat-panel-title>
</mat-expansion-panel-header>
<div fxLayout="row">
<mat-card fxLayout="row wrap" style="margin-bottom: 15px" [formGroupName]="$index">
<mat-slide-toggle color="primary" fxFlex="100" formControlName="active">
{{ (connectorControl.get('active').value ? 'gateway.connectors-active' : 'gateway.connectors-inactive') | translate }}
</mat-slide-toggle>
<mat-form-field fxFlex="calc(50%-15px)">
<mat-label translate>gateway.connector-name</mat-label>
<input matInput formControlName="name" required/>
<mat-error
*ngIf="connectorControl.get('name').hasError('required')">
{{'gateway.connector-name-required' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field fxFlex="calc(50%-15px)">
<mat-label translate>gateway.connector-configuration</mat-label>
<input matInput formControlName="configuration" required/>
<mat-error
*ngIf="connectorControl.get('configuration').hasError('required')">
{{'gateway.connector-configuration-required' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field fxFlex="calc(50%-15px)">
<mat-label translate>gateway.connector-type</mat-label>
<mat-select formControlName="type" required>
<mat-option *ngFor="let type of gatewayConnectorDefaultTypes" [value]="type">{{type}}</mat-option>
</mat-select>
<mat-error
*ngIf="connectorControl.get('type').hasError('required')">
{{'gateway.connector-type-required' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field fxFlex="calc(50%-15px)">
<mat-label translate>gateway.connector-key</mat-label>
<input matInput formControlName="key" required/>
<mat-error
*ngIf="connectorControl.get('key').hasError('required')">
{{'gateway.connector-key-required' | translate }}
</mat-error>
</mat-form-field>
</mat-card>
<div fxLayout="column">
<button mat-icon-button (click)="removeConnector($index)"
matTooltip="{{ 'gateway.statistics.remove' | translate }}"
matTooltipPosition="above">
<mat-icon>close</mat-icon>
</button>
<button mat-icon-button (click)="selectConnector($index)"
matTooltip="{{ 'gateway.select-connector' | translate }}"
matTooltipPosition="above">
<mat-icon>code</mat-icon>
</button>
</div>
</div>
</mat-expansion-panel>
</mat-accordion>
<button mat-raised-button color="primary"
style="margin-top: 15px"
type="button"
(click)="addConnector(true)">
{{ 'gateway.connector-add' | translate }}
</button>
</div>
<tb-json-object-edit
fxFlex
fxLayout="column"
required
*ngIf="selectedConnector !== undefined"
label="{{ 'gateway.connector-json' | translate }}"
[formControl]="getJsonControl(selectedConnector)">
</tb-json-object-edit>
</div>
<div mat-dialog-actions fxLayoutAlign="end center">
<button mat-button color="primary"
class="action-btns"
type="button"
(click)="cancel()">
{{ 'action.cancel' | translate }}
</button>
<button mat-raised-button color="primary"
class="action-btns"
type="button"
[disabled]="gatewayConnectorsGroup.invalid || !gatewayConnectorsGroup.dirty"
(click)="saveConnectors()">
{{ 'action.save' | translate }}
</button>
</div>
</form>

73
ui-ngx/src/app/shared/components/device/gateway-connectors.component.scss

@ -0,0 +1,73 @@
/**
* ThingsBoard, Inc. ("COMPANY") CONFIDENTIAL
*
* Copyright © 2016-2023 ThingsBoard, Inc. All Rights Reserved.
*
* NOTICE: All information contained herein is, and remains
* the property of ThingsBoard, Inc. and its suppliers,
* if any. The intellectual and technical concepts contained
* herein are proprietary to ThingsBoard, Inc.
* and its suppliers and may be covered by U.S. and Foreign Patents,
* patents in process, and are protected by trade secret or copyright law.
*
* Dissemination of this information or reproduction of this material is strictly forbidden
* unless prior written permission is obtained from COMPANY.
*
* Access to the source code contained herein is hereby forbidden to anyone except current COMPANY employees,
* managers or contractors who have executed Confidentiality and Non-disclosure agreements
* explicitly covering such access.
*
* The copyright notice above does not evidence any actual or intended publication
* or disclosure of this source code, which includes
* information that is confidential and/or proprietary, and is a trade secret, of COMPANY.
* ANY REPRODUCTION, MODIFICATION, DISTRIBUTION, PUBLIC PERFORMANCE,
* OR PUBLIC DISPLAY OF OR THROUGH USE OF THIS SOURCE CODE WITHOUT
* THE EXPRESS WRITTEN CONSENT OF COMPANY IS STRICTLY PROHIBITED,
* AND IN VIOLATION OF APPLICABLE LAWS AND INTERNATIONAL TREATIES.
* THE RECEIPT OR POSSESSION OF THIS SOURCE CODE AND/OR RELATED INFORMATION
* DOES NOT CONVEY OR IMPLY ANY RIGHTS TO REPRODUCE, DISCLOSE OR DISTRIBUTE ITS CONTENTS,
* OR TO MANUFACTURE, USE, OR SELL ANYTHING THAT IT MAY DESCRIBE, IN WHOLE OR IN PART.
*/
:host {
width: 100%;
height: 100%;
display: block;
.mat-toolbar {
background: transparent;
color: rgba(0, 0, 0, .87) !important;
}
.mat-mdc-form-field {
margin-right: 15px;
}
mat-expansion-panel {
border: 1px solid #305680;
}
mat-card {
padding-left: 10px;
}
.mat-card-selected {
background-color: white;
}
.mat-mdc-slide-toggle {
margin: 15px;
}
::ng-deep tb-json-object-edit > div {
flex-grow: 1;
}
.mat-accordion .mat-expansion-panel {
border-radius: 5px;
}
.action-btns {
margin: 10px 10px 0;
}
}

195
ui-ngx/src/app/shared/components/device/gateway-connectors.component.ts

@ -0,0 +1,195 @@
///
/// ThingsBoard, Inc. ("COMPANY") CONFIDENTIAL
///
/// Copyright © 2016-2023 ThingsBoard, Inc. All Rights Reserved.
///
/// NOTICE: All information contained herein is, and remains
/// the property of ThingsBoard, Inc. and its suppliers,
/// if any. The intellectual and technical concepts contained
/// herein are proprietary to ThingsBoard, Inc.
/// and its suppliers and may be covered by U.S. and Foreign Patents,
/// patents in process, and are protected by trade secret or copyright law.
///
/// Dissemination of this information or reproduction of this material is strictly forbidden
/// unless prior written permission is obtained from COMPANY.
///
/// Access to the source code contained herein is hereby forbidden to anyone except current COMPANY employees,
/// managers or contractors who have executed Confidentiality and Non-disclosure agreements
/// explicitly covering such access.
///
/// The copyright notice above does not evidence any actual or intended publication
/// or disclosure of this source code, which includes
/// information that is confidential and/or proprietary, and is a trade secret, of COMPANY.
/// ANY REPRODUCTION, MODIFICATION, DISTRIBUTION, PUBLIC PERFORMANCE,
/// OR PUBLIC DISPLAY OF OR THROUGH USE OF THIS SOURCE CODE WITHOUT
/// THE EXPRESS WRITTEN CONSENT OF COMPANY IS STRICTLY PROHIBITED,
/// AND IN VIOLATION OF APPLICABLE LAWS AND INTERNATIONAL TREATIES.
/// THE RECEIPT OR POSSESSION OF THIS SOURCE CODE AND/OR RELATED INFORMATION
/// DOES NOT CONVEY OR IMPLY ANY RIGHTS TO REPRODUCE, DISCLOSE OR DISTRIBUTE ITS CONTENTS,
/// OR TO MANUFACTURE, USE, OR SELL ANYTHING THAT IT MAY DESCRIBE, IN WHOLE OR IN PART.
///
import { ChangeDetectorRef, Component, Input, OnInit } from '@angular/core';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { Router } from '@angular/router';
import { FormArray, FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms';
import { EntityId } from '@shared/models/id/entity-id';
import { MatDialog, MatDialogRef } from '@angular/material/dialog';
import { AttributeService } from '@core/http/attribute.service';
import { DeviceService } from '@core/http/device.service';
import { TranslateService } from '@ngx-translate/core';
import { forkJoin } from 'rxjs';
import { AttributeScope } from '@shared/models/telemetry/telemetry.models';
export interface gatewayConnector {
name: string;
type: string;
configuration: string;
jsonConfiguration: string;
key: string;
}
@Component({
selector: 'tb-gateway-connector',
templateUrl: './gateway-connectors.component.html',
styleUrls: ['./gateway-connectors.component.scss']
})
export class GatewayConnectorComponent implements OnInit {
gatewayConnectorsGroup: FormGroup;
gatewayConnectorDefaultTypes: Array<string> =
['mqtt',
'modbus',
'grpc',
'opcua',
'opcua_asyncio',
'ble',
'request',
'can',
'bacnet',
'odbc',
'rest',
'snmp',
'ftp',
'socket',
'xmpp',
'ocpp'
];
selectedConnector: number;
@Input()
device: EntityId;
@Input()
dialogRef: MatDialogRef<any>;
logSelector: FormControl;
constructor(protected router: Router,
protected store: Store<AppState>,
protected fb: FormBuilder,
protected translate: TranslateService,
protected attributeService: AttributeService,
protected deviceService: DeviceService,
private cd: ChangeDetectorRef,
public dialog: MatDialog) {
}
ngOnInit() {
this.gatewayConnectorsGroup = this.fb.group({
connectors: this.fb.array([], [Validators.required])
});
this.getConnectorsData();
}
cancel(): void {
if (this.dialogRef) {
this.dialogRef.close();
}
}
getConnectorsData(): void {
forkJoin([
this.attributeService.getEntityAttributes(this.device, AttributeScope.SHARED_SCOPE,['implementedConnectors']),
this.attributeService.getEntityAttributes(this.device, AttributeScope.SERVER_SCOPE,['implementedConnectors']),
this.attributeService.getEntityAttributes(this.device, AttributeScope.SHARED_SCOPE,['connectorTypes'])
]).subscribe(attributes=>{
if (attributes[0].length) {
attributes[0][0].value.forEach(connector=>this.addConnector(true, connector));
}
if (attributes[1].length) {
attributes[1][0].value.forEach(connector=>this.addConnector(false, connector));
}
if(attributes[2].length) {
attributes[1][0].value.forEach(type=> {
if (this.gatewayConnectorDefaultTypes.indexOf(type) === -1) {
this.gatewayConnectorDefaultTypes.push(type);
}
});
}
this.cd.detectChanges();
});
}
saveConnectors(): void {
const connectors = this.gatewayConnectorsGroup.value.connectors;
const activeConnectors = connectors.filter(connector=>connector.active);
const inactiveConnectors = connectors.filter(connector=>!connector.active);
forkJoin([
this.attributeService.saveEntityAttributes(this.device, AttributeScope.SERVER_SCOPE,
[{key: 'implementedConnectors', value: inactiveConnectors}]),
this.attributeService.saveEntityAttributes(this.device, AttributeScope.SHARED_SCOPE,
[{key: 'implementedConnectors', value: activeConnectors}])
]).subscribe(_=> {
if (this.dialogRef) {
this.dialogRef.close();
}
});
}
connectorsFormArray(): FormArray {
return this.gatewayConnectorsGroup.get('connectors') as FormArray;
}
addConnector(active?: boolean,connector?: gatewayConnector) {
const newConnector = this.fb.group({
active: [!!active],
name: [connector?.name || '', [Validators.required]],
type: [connector?.type || '', [Validators.required]],
configuration: [connector?.configuration || '', [Validators.required]],
jsonConfiguration: [connector?.jsonConfiguration || {}, [Validators.required]],
key: [connector?.key || 'auto', [Validators.required]],
});
const connectorsFormArray = this.connectorsFormArray();
connectorsFormArray.push(newConnector);
this.selectConnector(connectorsFormArray.length-1);
}
removeConnector(index: number): void {
this.connectorsFormArray().removeAt(index);
}
selectConnector(index: number): void {
this.selectedConnector = index;
}
getJsonControl(selectedConnector: number): FormControl {
return this.connectorsFormArray().at(selectedConnector).get('jsonConfiguration') as FormControl;
}
generatePanelTitle(connectorControl): string{
const connectorValues = connectorControl.value;
const activeTxt = this.translate.instant(connectorValues.active ? 'gateway.connectors-active' : 'gateway.connectors-inactive');
let title = `${connectorValues.name} | ${connectorValues.type} `;
if (connectorValues.type === 'grpc') {
title += `| ${connectorValues.key} `;
}
title += `| ${activeTxt}`;
return title;
}
}

72
ui-ngx/src/app/shared/components/dialog/gateway-remote-configuration-dialog.html

@ -0,0 +1,72 @@
<!--
ThingsBoard, Inc. ("COMPANY") CONFIDENTIAL
Copyright © 2016-2023 ThingsBoard, Inc. All Rights Reserved.
NOTICE: All information contained herein is, and remains
the property of ThingsBoard, Inc. and its suppliers,
if any. The intellectual and technical concepts contained
herein are proprietary to ThingsBoard, Inc.
and its suppliers and may be covered by U.S. and Foreign Patents,
patents in process, and are protected by trade secret or copyright law.
Dissemination of this information or reproduction of this material is strictly forbidden
unless prior written permission is obtained from COMPANY.
Access to the source code contained herein is hereby forbidden to anyone except current COMPANY employees,
managers or contractors who have executed Confidentiality and Non-disclosure agreements
explicitly covering such access.
The copyright notice above does not evidence any actual or intended publication
or disclosure of this source code, which includes
information that is confidential and/or proprietary, and is a trade secret, of COMPANY.
ANY REPRODUCTION, MODIFICATION, DISTRIBUTION, PUBLIC PERFORMANCE,
OR PUBLIC DISPLAY OF OR THROUGH USE OF THIS SOURCE CODE WITHOUT
THE EXPRESS WRITTEN CONSENT OF COMPANY IS STRICTLY PROHIBITED,
AND IN VIOLATION OF APPLICABLE LAWS AND INTERNATIONAL TREATIES.
THE RECEIPT OR POSSESSION OF THIS SOURCE CODE AND/OR RELATED INFORMATION
DOES NOT CONVEY OR IMPLY ANY RIGHTS TO REPRODUCE, DISCLOSE OR DISTRIBUTE ITS CONTENTS,
OR TO MANUFACTURE, USE, OR SELL ANYTHING THAT IT MAY DESCRIBE, IN WHOLE OR IN PART.
-->
<form style="width: 600px; position: relative;">
<mat-toolbar color="warn">
<mat-icon>warning</mat-icon>
<h2 translate>gateway.configuration-delete-dialog-header</h2>
<span fxFlex></span>
<button mat-icon-button
(click)="close()"
type="button">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<div mat-dialog-content tb-toast toastTarget="activationLinkDialogContent">
<div class="mat-content" fxLayout="column">
<span innerHTML="{{'gateway.configuration-delete-dialog-body' | translate}}" > </span>
<mat-form-field class="mat-block tb-value-type" style="flex-grow: 0">
<mat-label translate>gateway.configuration-delete-dialog-input</mat-label>
<input matInput [formControl]="gatewayForm" required/>
<mat-error
*ngIf="gatewayForm.hasError('required')">
{{'gateway.configuration-delete-dialog-input-required' | translate }}
</mat-error>
</mat-form-field>
</div>
</div>
<div mat-dialog-actions fxLayoutAlign="end center">
<button mat-button color="warn"
type="button"
cdkFocusInitial
(click)="close()">
{{ 'action.cancel' | translate }}
</button>
<button mat-button color="warn"
type="button"
cdkFocusInitial
[disabled]="gatewayForm.value !== gatewayName"
(click)="turnOff()">
{{ 'gateway.configuration-delete-dialog-confirm' | translate }}
</button>
</div>
</form>

76
ui-ngx/src/app/shared/components/dialog/gateway-remote-configuration-dialog.ts

@ -0,0 +1,76 @@
///
/// ThingsBoard, Inc. ("COMPANY") CONFIDENTIAL
///
/// Copyright © 2016-2023 ThingsBoard, Inc. All Rights Reserved.
///
/// NOTICE: All information contained herein is, and remains
/// the property of ThingsBoard, Inc. and its suppliers,
/// if any. The intellectual and technical concepts contained
/// herein are proprietary to ThingsBoard, Inc.
/// and its suppliers and may be covered by U.S. and Foreign Patents,
/// patents in process, and are protected by trade secret or copyright law.
///
/// Dissemination of this information or reproduction of this material is strictly forbidden
/// unless prior written permission is obtained from COMPANY.
///
/// Access to the source code contained herein is hereby forbidden to anyone except current COMPANY employees,
/// managers or contractors who have executed Confidentiality and Non-disclosure agreements
/// explicitly covering such access.
///
/// The copyright notice above does not evidence any actual or intended publication
/// or disclosure of this source code, which includes
/// information that is confidential and/or proprietary, and is a trade secret, of COMPANY.
/// ANY REPRODUCTION, MODIFICATION, DISTRIBUTION, PUBLIC PERFORMANCE,
/// OR PUBLIC DISPLAY OF OR THROUGH USE OF THIS SOURCE CODE WITHOUT
/// THE EXPRESS WRITTEN CONSENT OF COMPANY IS STRICTLY PROHIBITED,
/// AND IN VIOLATION OF APPLICABLE LAWS AND INTERNATIONAL TREATIES.
/// THE RECEIPT OR POSSESSION OF THIS SOURCE CODE AND/OR RELATED INFORMATION
/// DOES NOT CONVEY OR IMPLY ANY RIGHTS TO REPRODUCE, DISCLOSE OR DISTRIBUTE ITS CONTENTS,
/// OR TO MANUFACTURE, USE, OR SELL ANYTHING THAT IT MAY DESCRIBE, IN WHOLE OR IN PART.
///
import { Component, Inject, OnInit } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { DialogComponent } from '@shared/components/dialog.component';
import { Router } from '@angular/router';
import { FormBuilder, FormControl } from '@angular/forms';
export interface GatewayRemoteConfigurationDialogData {
gatewayName: string;
}
@Component({
selector: 'tb-activation-link-dialog',
templateUrl: './gateway-remote-configuration-dialog.html'
})
export class GatewayRemoteConfigurationDialogComponent extends DialogComponent<GatewayRemoteConfigurationDialogComponent,
boolean> implements OnInit {
gatewayName: string;
gatewayForm: FormControl;
constructor(protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: GatewayRemoteConfigurationDialogData,
public dialogRef: MatDialogRef<GatewayRemoteConfigurationDialogComponent, boolean>,
private fb: FormBuilder) {
super(store, router, dialogRef);
this.gatewayName = this.data.gatewayName;
this.gatewayForm = this.fb.control(null);
}
ngOnInit(): void {
}
close(): void {
this.dialogRef.close();
}
turnOff(): void {
this.dialogRef.close(true);
}
}

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

@ -176,10 +176,13 @@ import { NotificationComponent } from '@shared/components/notification/notificat
import { TemplateAutocompleteComponent } from '@shared/components/notification/template-autocomplete.component';
import { SlackConversationAutocompleteComponent } from '@shared/components/slack-conversation-autocomplete.component';
import { DateAgoPipe } from '@shared/pipe/date-ago.pipe';
import { DeviceGatewayCommandComponent } from '@shared/components/device/device-gateway-command.component';
import { GatewayConfigurationComponent } from '@shared/components/device/gateway-configuration.component';
import { GatewayRemoteConfigurationDialogComponent } from '@shared/components/dialog/gateway-remote-configuration-dialog';
import { GatewayConnectorComponent } from '@shared/components/device/gateway-connectors.component';
export const MarkedOptionsFactory = (markedOptionsService: MarkedOptionsService) => markedOptionsService;
export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) {
return markedOptionsService;
}
@NgModule({
providers: [
@ -241,6 +244,10 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
HelpComponent,
HelpMarkdownComponent,
HelpPopupComponent,
DeviceGatewayCommandComponent,
GatewayConfigurationComponent,
GatewayConnectorComponent,
GatewayRemoteConfigurationDialogComponent,
TbCheckboxComponent,
TbSnackBarComponent,
TbErrorComponent,
@ -407,6 +414,10 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
HelpComponent,
HelpMarkdownComponent,
HelpPopupComponent,
DeviceGatewayCommandComponent,
GatewayConfigurationComponent,
GatewayConnectorComponent,
GatewayRemoteConfigurationDialogComponent,
TbCheckboxComponent,
TbErrorComponent,
TbCheatSheetComponent,

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

@ -1186,6 +1186,7 @@
"device-details": "Device details",
"add-device-text": "Add new device",
"credentials": "Credentials",
"commands": "Commands",
"manage-credentials": "Manage credentials",
"delete": "Delete device",
"assign-devices": "Assign devices",
@ -1278,6 +1279,7 @@
"copyId": "Copy device Id",
"copyAccessToken": "Copy access token",
"copy-mqtt-authentication": "Copy MQTT credentials",
"transportCommandCopiedMessage": "Transport Command has been copied to clipboard",
"idCopiedMessage": "Device Id has been copied to clipboard",
"accessTokenCopiedMessage": "Device access token has been copied to clipboard",
"mqtt-authentication-copied-message": "Device MQTT authentication has been copied to clipboard",
@ -2466,44 +2468,164 @@
},
"gateway": {
"add-entry": "Add configuration",
"advanced": "Advanced",
"checking-device-activity": "Checking Device Activity:",
"command": "Docker commands",
"command-copied-message": "Docker command has been copied to clipboard",
"configuration": "Configuration",
"connector-json": "Connector JSON",
"connector-add": "Add new connector",
"connector-enabled": "Enable connector",
"connector-name": "Connector name",
"connector-name-required": "Connector name is required.",
"connector-key": "Connector key",
"connector-key-required": "Connector key is required.",
"connector-configuration": "Configuration file name",
"connector-configuration-required": "Configuration file name is required.",
"connector-type": "Connector type",
"connector-type-required": "Connector type is required.",
"connector-types": {
"mqtt": "MQTT Broker Connector",
"modbus": "Modbus Connector",
"modbus_serial": "Modbus Connector (serial)",
"opcua": "OPC-UA Connector",
"opcua_asyncio": "OPC-UA Connector (asyncio)",
"ble": "BLE Connector",
"request": "REQUEST Connector",
"can": "CAN Connector",
"bacnet": "BACnet Connector",
"odbc": "ODBC Connector",
"rest": "REST Connector",
"snmp": "SNMP Connector",
"ftp": "FTP Connector",
"socket": "Socket TCP/UDP Connector",
"xmpp": "XMPP Connector",
"ocpp": "OCPP Connector"
},
"connectors": "Connectors configuration",
"connectors-active": "Connector active",
"connectors-inactive": "Connector inactive",
"select-connector": "Select connector",
"gateway-configuration": "Gateway Configuration",
"copy-command": "Copy docker command",
"create-new-gateway": "Create a new gateway",
"create-new-gateway-text": "Are you sure you want create a new gateway with name: '{{gatewayName}}'?",
"created-time": "Created time",
"configuration-delete-dialog-header": "Configurations will be deleted",
"configuration-delete-dialog-body": "Turning off Remote Configuration is possible only if there is physical access to the Gateway. All previous configurations will be deleted.<br><br> \nTo turn off configuration, enter gateway name below.",
"configuration-delete-dialog-input": "Gateway name",
"configuration-delete-dialog-input-required": "Gateway name is mandatory",
"configuration-delete-dialog-confirm": "Turn Off",
"delete": "Delete configuration",
"download-tip": "Download configuration file",
"drop-file": "Drop file here or",
"gateway": "Gateway",
"gateway-exists": "Device with same name is already exists.",
"gateway-name": "Gateway name",
"gateway-name-required": "Gateway name is required.",
"gateway-saved": "Gateway configuration successfully saved.",
"gateway-search": "Gateway search",
"grpc": "GRPC",
"grpc-keep-alive-timeout": "Keep alive timeout (in ms)",
"grpc-keep-alive-timeout-required": "Keep alive timeout is required",
"grpc-keep-alive-timeout-min": "Keep alive timeout can not be less then 1",
"grpc-keep-alive-timeout-pattern": "Keep alive timeout is not valid",
"grpc-keep-alive": "Keep alive (in ms)",
"grpc-keep-alive-required": "Keep alive is required",
"grpc-keep-alive-min": "Keep alive can not be less then 1",
"grpc-keep-alive-pattern": "Keep alive is not valid",
"grpc-min-time-between-pings": "Min time between pings (in ms)",
"grpc-min-time-between-pings-required": "Min time between pings is required",
"grpc-min-time-between-pings-min": "Min time between pings can not be less then 1",
"grpc-min-time-between-pings-pattern": "Min time between pings is not valid",
"grpc-min-ping-interval-without-data": "Min ping interval without data (in ms)",
"grpc-min-ping-interval-without-data-required": "Min ping interval without data is required",
"grpc-min-ping-interval-without-data-min": "Min ping interval without data can not be less then 1",
"grpc-min-ping-interval-without-data-pattern": "Min ping interval without data is not valid",
"grpc-max-pings-without-data": "Max pings without data",
"grpc-max-pings-without-data-required": "Max pings without data is required",
"grpc-max-pings-without-data-min": "Max pings without data can not be less then 1",
"grpc-max-pings-without-data-pattern": "Max pings without data is not valid",
"handle-device-renaming": "Handle device renaming",
"inactivity-check-period-seconds": "Inactivity check period (in sec)",
"inactivity-check-period-seconds-required": "Inactivity check period is required",
"inactivity-check-period-seconds-min": "Inactivity check period can not be less then 1",
"inactivity-timeout-seconds": "Inactivity timeout (in sec)",
"inactivity-timeout-seconds-required": "Inactivity timeout is required",
"inactivity-timeout-seconds-min": "Inactivity timeout can not be less then 1",
"json-parse": "Not valid JSON.",
"json-required": "Field cannot be empty.",
"linux-macos": "Linux/MacOS",
"logs": {
"logs": "Logs",
"days": "days",
"hours": "hours",
"minutes": "minutes",
"seconds": "seconds",
"date-format": "Date format",
"date-format-required": "Date format required",
"log-format": "Log format",
"log-type": "Log type",
"log-format-required": "Log format required",
"remote": "Remote logging",
"remote-logs": "Remote logs",
"local": "Local logging",
"level": "Log level",
"file-path": "File path",
"file-path-required": "File path required",
"saving-period": "Log saving period",
"saving-period-min": "Log saving period can not be less then 0",
"saving-period-required": "Log saving period required",
"backup-count": "Backup count",
"backup-count-min": "Backup count can not be less then 0",
"backup-count-required": "Backup count required"
},
"min-pack-send-delay": "Min pack send delay (in ms)",
"min-pack-send-delay-required": "Min pack send delay is required",
"min-pack-send-delay-min": "Min pack send delay can not be less then 1",
"no-connectors": "No connectors",
"no-data": "No configurations",
"no-gateway-found": "No gateway found.",
"no-gateway-matching": " '{{item}}' not found.",
"path-logs": "Path to log files",
"path-logs-required": "Path is required.",
"permit-without-calls": "Keep alive permit without calls",
"remote": "Remote configuration",
"remote-logging-level": "Logging level",
"remove-entry": "Remove configuration",
"remote-shell": "Remote shell",
"remote-configuration": "Remote Configuration",
"other": "Other",
"save-tip": "Save configuration file",
"security-type": "Security type",
"security-types": {
"access-token": "Access Token",
"tls": "TLS"
"tls": "TLS",
"tls-access-token": "TLS + Access Token",
"tls-private-key": "TLS + Private Key"
},
"server-port": "Server port",
"stats-send-period-in-sec": "Stats send period in seconds",
"statistics": {
"statistics": "Statistics",
"send-period": "Statistic send period (in ms)",
"send-period-required": "Statistic send period is required",
"send-period-min": "Statistic send period can not be less then 0",
"send-period-pattern": "Statistic send period is not valid",
"check-connectors-configuration": "Check connectors configuration (in ms)",
"check-connectors-configuration-required": "Check connectors configuration is required",
"check-connectors-configuration-min": "Check connectors configuration can not be less then 0",
"check-connectors-configuration-pattern": "Check connectors configuration is not valid",
"add": "Add command",
"timeout": "Timeout",
"timeout-required": "Timeout is required",
"timeout-min": "Timeout can not be less then 0",
"timeout-pattern": "Timeout is not valid",
"attribute-name": "Attribute name",
"attribute-name-required": "Attribute name is required",
"command": "Command",
"command-required": "Command is required",
"remove": "Remove command"
},
"storage": "Storage",
"storage-max-file-records": "Maximum records in file",
@ -2515,6 +2637,16 @@
"storage-max-records-min": "Minimum number of records is 1.",
"storage-max-records-pattern": "Number is not valid.",
"storage-max-records-required": "Maximum records is required.",
"storage-read-record-count": "Read record count in storage",
"storage-read-record-count-min": "Minimum number of records is 1.",
"storage-read-record-count-pattern": "Number is not valid.",
"storage-read-record-count-required": "Read record count is required.",
"storage-max-read-record-count": "Max read record count in storage",
"storage-max-read-record-count-min": "Minimum number of records is 1.",
"storage-max-read-record-count-pattern": "Number is not valid.",
"storage-max-read-record-count-required": "Max Read record count is required.",
"storage-data-folder-path": "Data folder path",
"storage-data-folder-path-required": "Data folder path is required.",
"storage-pack-size": "Maximum event pack size",
"storage-pack-size-min": "Minimum number is 1.",
"storage-pack-size-pattern": "Number is not valid.",
@ -2524,9 +2656,11 @@
"storage-type": "Storage type",
"storage-types": {
"file-storage": "File storage",
"memory-storage": "Memory storage"
"memory-storage": "Memory storage",
"sqlite": "SQLITE"
},
"thingsboard": "ThingsBoard",
"thingsboard-general": "General",
"thingsboard-host": "ThingsBoard host",
"thingsboard-host-required": "Host is required.",
"thingsboard-port": "ThingsBoard port",
@ -2539,6 +2673,17 @@
"title-connectors-json": "Connector {{typeName}} configuration",
"tls-path-ca-certificate": "Path to CA certificate on gateway",
"tls-path-client-certificate": "Path to client certificate on gateway",
"messages-ttl-check-in-hours": "Messages TTL check in hours",
"messages-ttl-check-in-hours-required": "Messages TTL check in hours is required.",
"messages-ttl-check-in-hours-min": "Min number is 1.",
"messages-ttl-check-in-hours-pattern": "Number is not valid.",
"messages-ttl-in-days": "Messages TTL in days",
"messages-ttl-in-days-required": "Messages TTL in days is required.",
"messages-ttl-in-days-min": "Min number is 1.",
"messages-ttl-in-days-pattern": "Number is not valid.",
"mqtt-qos": "QoS",
"mqtt-qos-required": "QoS is required",
"mqtt-qos-range": "QoS values range is from 0 to 2",
"tls-path-private-key": "Path to private key on gateway",
"toggle-fullscreen": "Toggle fullscreen",
"transformer-json-config": "Configuration JSON*",
@ -3065,6 +3210,20 @@
},
"security": {
"security": "Security",
"access-token": "Access token",
"access-token-required": "Access token is required",
"clientId": "Client ID",
"clientId-required": "Client ID is required",
"username": "Username",
"username-required": "Username is required",
"password": "Password",
"password-required": "Password is required",
"ca-cert": "CA certificate",
"ca-cert-required": "CA certificate is required",
"cert": "Certificate",
"cert-required": "Certificate is required",
"private-key": "Private Key",
"private-key-required": "Private Key is required",
"2fa": {
"2fa": "Two-factor authentication",
"2fa-description": "Two-factor authentication protects your account from unauthorized access. All you have to do is enter a security code when you log in.",

Loading…
Cancel
Save