Browse Source

mk2 gateway dashboard

pull/8337/head
Maksym Dudnik 4 years ago
parent
commit
c9b9959f47
  1. 143
      application/src/main/data/json/system/widget_bundles/gateway_widgets.json
  2. 7
      ui-ngx/src/app/core/utils.ts
  3. 15
      ui-ngx/src/app/modules/common/modules-map.ts
  4. 6
      ui-ngx/src/app/modules/home/components/device/device-credentials.module.ts
  5. 86
      ui-ngx/src/app/modules/home/components/device/device-example-command.component.html
  6. 41
      ui-ngx/src/app/modules/home/components/device/device-example-command.component.scss
  7. 98
      ui-ngx/src/app/modules/home/components/device/device-example-command.component.ts
  8. 41
      ui-ngx/src/app/modules/home/components/gateway/gateway-command-dialog.component.html
  9. 58
      ui-ngx/src/app/modules/home/components/gateway/gateway-command-dialog.component.ts
  10. 130
      ui-ngx/src/app/modules/home/components/gateway/gateway-list-table-config.ts
  11. 63
      ui-ngx/src/app/modules/home/components/gateway/gateway-list.component.ts
  12. 3
      ui-ngx/src/app/modules/home/components/home-components.module.ts
  13. 3
      ui-ngx/src/app/modules/home/components/shared-home-components.module.ts
  14. 26
      ui-ngx/src/app/modules/home/components/widget/lib/settings/gateway/gateway-logs-settings.component.html
  15. 54
      ui-ngx/src/app/modules/home/components/widget/lib/settings/gateway/gateway-logs-settings.component.ts
  16. 8
      ui-ngx/src/app/modules/home/components/widget/lib/settings/gateway/gateway-service-rpc-settings.component.html
  17. 52
      ui-ngx/src/app/modules/home/components/widget/lib/settings/gateway/gateway-service-rpc-settings.component.ts
  18. 19
      ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts
  19. 15
      ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.html
  20. 4
      ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.scss
  21. 41
      ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.ts
  22. 12
      ui-ngx/src/app/shared/components/device/device-gateway-command.component.html
  23. 58
      ui-ngx/src/app/shared/components/device/device-gateway-command.component.ts
  24. 789
      ui-ngx/src/app/shared/components/device/gateway-configuration.component.html
  25. 201
      ui-ngx/src/app/shared/components/device/gateway-configuration.component.scss
  26. 589
      ui-ngx/src/app/shared/components/device/gateway-configuration.component.ts
  27. 167
      ui-ngx/src/app/shared/components/device/gateway-connectors.component.html
  28. 90
      ui-ngx/src/app/shared/components/device/gateway-connectors.component.scss
  29. 390
      ui-ngx/src/app/shared/components/device/gateway-connectors.component.ts
  30. 60
      ui-ngx/src/app/shared/components/device/gateway-logs.component.html
  31. 55
      ui-ngx/src/app/shared/components/device/gateway-logs.component.scss
  32. 234
      ui-ngx/src/app/shared/components/device/gateway-logs.component.ts
  33. 49
      ui-ngx/src/app/shared/components/device/gateway-service-rpc.component.html
  34. 42
      ui-ngx/src/app/shared/components/device/gateway-service-rpc.component.scss
  35. 128
      ui-ngx/src/app/shared/components/device/gateway-service-rpc.component.ts
  36. 81
      ui-ngx/src/app/shared/components/device/gateway-statistics.component.html
  37. 78
      ui-ngx/src/app/shared/components/device/gateway-statistics.component.scss
  38. 299
      ui-ngx/src/app/shared/components/device/gateway-statistics.component.ts
  39. 57
      ui-ngx/src/app/shared/components/dialog/gateway-remote-configuration-dialog.html
  40. 61
      ui-ngx/src/app/shared/components/dialog/gateway-remote-configuration-dialog.ts
  41. 13
      ui-ngx/src/app/shared/components/file-input.component.html
  42. 4
      ui-ngx/src/app/shared/components/file-input.component.scss
  43. 3
      ui-ngx/src/app/shared/components/file-input.component.ts
  44. 26
      ui-ngx/src/app/shared/shared.module.ts
  45. 215
      ui-ngx/src/assets/locale/locale.constant-en_US.json

143
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[] {

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

@ -296,6 +296,14 @@ 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 * as GatewayLogsComponent from '@shared/components/device/gateway-logs.component';
import * as GatewayStatisticsComponent from '@shared/components/device/gateway-statistics.component';
import * as GatewayServiceRPCComponent from '@shared/components/device/gateway-service-rpc.component';
import { IModulesMap } from '@modules/common/modules-map.models';
@ -398,6 +406,13 @@ 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-logs.component': GatewayLogsComponent,
'@shared/components/device/gateway-statistics.component': GatewayStatisticsComponent,
'@shared/components/device/gateway-service-rpc.component': GatewayServiceRPCComponent,
'@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,

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

@ -22,7 +22,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 +31,7 @@ import {DeviceGatewayCommandComponent} from "@home/components/device/device-gate
DeviceCredentialsLwm2mComponent,
DeviceCredentialsLwm2mServerComponent,
DeviceCredentialsMqttBasicComponent,
DeviceGatewayCommandComponent
DeviceExampleCommandComponent
],
imports: [
CommonModule,
@ -43,7 +43,7 @@ import {DeviceGatewayCommandComponent} from "@home/components/device/device-gate
DeviceCredentialsLwm2mComponent,
DeviceCredentialsLwm2mServerComponent,
DeviceCredentialsMqttBasicComponent,
DeviceGatewayCommandComponent
DeviceExampleCommandComponent
]
})
export class DeviceCredentialsModule { }

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

@ -0,0 +1,86 @@
<!--
Copyright © 2016-2023 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">
<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>

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

@ -0,0 +1,41 @@
/**
* Copyright © 2016-2023 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 {
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;
}
}

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

@ -0,0 +1,98 @@
///
/// Copyright © 2016-2023 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 { 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'
}));
}
}

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

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

26
ui-ngx/src/app/modules/home/components/widget/lib/settings/gateway/gateway-logs-settings.component.html

@ -0,0 +1,26 @@
<!--
Copyright © 2016-2023 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.
-->
<section class="tb-widget-settings" [formGroup]="GatewayLogSettingForm" fxLayout="column">
<mat-slide-toggle class="mat-slide" formControlName="isConnectorLog">
Is Connector
</mat-slide-toggle>
<mat-form-field fxFlex class="mat-block" *ngIf="GatewayLogSettingForm.get('isConnectorLog').value">
<mat-label translate>State parameter connector name</mat-label>
<input matInput formControlName="connectorLogState">
</mat-form-field>
</section>

54
ui-ngx/src/app/modules/home/components/widget/lib/settings/gateway/gateway-logs-settings.component.ts

@ -0,0 +1,54 @@
///
/// Copyright © 2016-2023 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 } from '@angular/core';
import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models';
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
@Component({
selector: 'tb-gateway-logs-settings',
templateUrl: './gateway-logs-settings.component.html',
styleUrls: ['../widget-settings.scss']
})
export class GatewayLogsSettingsComponent extends WidgetSettingsComponent {
GatewayLogSettingForm: UntypedFormGroup;
constructor(protected store: Store<AppState>,
private fb: UntypedFormBuilder) {
super(store);
}
protected settingsForm(): UntypedFormGroup {
return this.GatewayLogSettingForm;
}
protected defaultSettings(): WidgetSettings {
return {
isConnectorLog: false,
connectorLogState: 'default'
};
}
protected onSettingsSet(settings: WidgetSettings) {
this.GatewayLogSettingForm = this.fb.group({
isConnectorLog: [false, []],
connectorLogState: ['default', []]
});
}
}

8
ui-ngx/src/app/modules/home/components/gateway/gateway-list.component.html → ui-ngx/src/app/modules/home/components/widget/lib/settings/gateway/gateway-service-rpc-settings.component.html

@ -1,6 +1,6 @@
<!--
Copyright © 2016-2022 The Thingsboard Authors
Copyright © 2016-2023 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.
@ -15,4 +15,8 @@
limitations under the License.
-->
<tb-entities-table [entitiesTableConfig]="gatewayListTableConfig"></tb-entities-table>
<section class="tb-widget-settings" [formGroup]="GatewayLogSettingForm" fxLayout="column">
<mat-slide-toggle class="mat-slide" formControlName="isConnector">
Is Connector
</mat-slide-toggle>
</section>

52
ui-ngx/src/app/modules/home/components/widget/lib/settings/gateway/gateway-service-rpc-settings.component.ts

@ -0,0 +1,52 @@
///
/// Copyright © 2016-2023 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 } from '@angular/core';
import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models';
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
@Component({
selector: 'tb-gateway-service-rpc-settings',
templateUrl: './gateway-service-rpc-settings.component.html',
styleUrls: ['../widget-settings.scss']
})
export class GatewayServiceRPCSettingsComponent extends WidgetSettingsComponent {
GatewayLogSettingForm: UntypedFormGroup;
constructor(protected store: Store<AppState>,
private fb: UntypedFormBuilder) {
super(store);
}
protected settingsForm(): UntypedFormGroup {
return this.GatewayLogSettingForm;
}
protected defaultSettings(): WidgetSettings {
return {
isConnector: false,
};
}
protected onSettingsSet(settings: WidgetSettings) {
this.GatewayLogSettingForm = this.fb.group({
isConnector: [false, []]
});
}
}

19
ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts

@ -259,6 +259,13 @@ import {
import {
TripAnimationPointSettingsComponent
} from '@home/components/widget/lib/settings/map/trip-animation-point-settings.component';
import {
GatewayLogsSettingsComponent
} from '@home/components/widget/lib/settings/gateway/gateway-logs-settings.component';
import {
GatewayServiceRPCSettingsComponent
} from '@home/components/widget/lib/settings/gateway/gateway-service-rpc-settings.component';
@NgModule({
declarations: [
@ -357,7 +364,9 @@ import {
TripAnimationPointSettingsComponent,
MapWidgetSettingsComponent,
RouteMapWidgetSettingsComponent,
TripAnimationWidgetSettingsComponent
TripAnimationWidgetSettingsComponent,
GatewayLogsSettingsComponent,
GatewayServiceRPCSettingsComponent
],
imports: [
CommonModule,
@ -460,7 +469,9 @@ import {
TripAnimationPointSettingsComponent,
MapWidgetSettingsComponent,
RouteMapWidgetSettingsComponent,
TripAnimationWidgetSettingsComponent
TripAnimationWidgetSettingsComponent,
GatewayLogsSettingsComponent,
GatewayServiceRPCSettingsComponent
]
})
export class WidgetSettingsModule {
@ -527,5 +538,7 @@ export const widgetSettingsComponentsMap: {[key: string]: Type<IWidgetSettingsCo
'tb-update-multiple-attributes-key-settings': UpdateMultipleAttributesKeySettingsComponent,
'tb-map-widget-settings': MapWidgetSettingsComponent,
'tb-route-map-widget-settings': RouteMapWidgetSettingsComponent,
'tb-trip-animation-widget-settings': TripAnimationWidgetSettingsComponent
'tb-trip-animation-widget-settings': TripAnimationWidgetSettingsComponent,
'tb-gateway-logs-settings': GatewayLogsSettingsComponent,
'tb-gateway-service-rpc-settings':GatewayServiceRPCSettingsComponent
};

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 {

12
ui-ngx/src/app/modules/home/components/device/device-gateway-command.component.html → ui-ngx/src/app/shared/components/device/device-gateway-command.component.html

@ -1,6 +1,6 @@
<!--
Copyright © 2016-2022 The Thingsboard Authors
Copyright © 2016-2023 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.
@ -16,15 +16,15 @@
-->
<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">
<span style="padding-bottom: 15px;">In order to run ThingsBoard IoT gateway in docker with credentials for this device you can use the following commands.</span>
<div fxFlex fxLayout="row" style="justify-content: space-between; flex-grow: 0">
<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="selectedOSControll.value !== osTypes.windows">
<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
@ -38,7 +38,7 @@
</button>
</div>
</div>
<div class="mat-content" fxLayout="column" *ngIf="selectedOSControll.value === osTypes.windows">
<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

58
ui-ngx/src/app/modules/home/components/device/device-gateway-command.component.ts → ui-ngx/src/app/shared/components/device/device-gateway-command.component.ts

@ -1,5 +1,5 @@
///
/// Copyright © 2016-2022 The Thingsboard Authors
/// Copyright © 2016-2023 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.
@ -14,17 +14,15 @@
/// 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";
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',
@ -48,7 +46,7 @@ export class DeviceGatewayCommandComponent implements OnInit {
linuxCode: string;
windowsCode: string;
selectedOSControll: FormControl;
selectedOSCControl: FormControl;
osTypes = OsType;
helpLink: string = helpBaseUrl + '/docs/iot-gateway/install/docker-installation/';
@ -62,44 +60,40 @@ export class DeviceGatewayCommandComponent implements OnInit {
ngOnInit(): void {
const HOST = window.location.hostname;
if (this.deviceId) {
this.deviceService.getDeviceCredentials(this.deviceId).subscribe(credentials=>{
this.deviceService.getDeviceCredentials(this.deviceId).subscribe(credentials => {
this.token = credentials.credentialsId;
this.createRunCode(HOST);
})
});
}
this.selectedOSControll = new FormControl('');
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.selectedOSControll.setValue(OsType.macos);
this.selectedOSCControl.setValue(OsType.linux);
} else if (windowsPlatforms.indexOf(platform) !== -1) {
this.selectedOSControll.setValue(OsType.windows);
this.selectedOSCControl.setValue(OsType.windows);
} else if (/Linux/.test(platform)) {
this.selectedOSControll.setValue(OsType.linux);
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=" +
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=" +
' -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=" +
' --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=" +
' -e port=1883 -e accessToken=' +
this.token +
" --restart always thingsboard/tb-gateway";
}
goToDocumentationPage() {
window.open(this.helpLink, '_blank');
' --restart always thingsboard/tb-gateway';
}
onDockerCodeCopied() {

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

@ -0,0 +1,789 @@
<!--
Copyright © 2016-2023 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 [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-icon class="material-icons-outlined suffix-icon" aria-hidden="false" aria-label="help-icon"
style="cursor:pointer;"
matTooltip="{{'gateway.hints.remote-configuration' | translate }}">info
</mat-icon>
</mat-slide-toggle>
<mat-slide-toggle color="primary" fxFlex="100" formControlName="remoteShell">
{{ 'gateway.remote-shell' | translate }}
<mat-icon class="material-icons-outlined suffix-icon" aria-hidden="false" aria-label="help-icon"
style="cursor:pointer;"
matTooltip="{{'gateway.hints.remote-shell' | translate }}">info
</mat-icon>
</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-icon class="mat-form-field-infix pe suffix-icon"
matSuffix style="cursor:pointer;"
matTooltip="{{'gateway.hints.host' | translate }}">info_outlined
</mat-icon>
<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" min="0" />
<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-icon class="mat-form-field-infix pe suffix-icon" aria-hidden="false" aria-label="help-icon"
matSuffix style="cursor:pointer;"
matTooltip="{{'gateway.hints.port' | translate }}">info_outlined
</mat-icon>
</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-icon class="mat-form-field-infix pe suffix-icon" aria-hidden="false" aria-label="help-icon"
matSuffix style="cursor:pointer;"
matTooltip="{{'gateway.hints.token' | translate }}">info_outlined
</mat-icon>
</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-icon class="mat-form-field-infix pe suffix-icon" aria-hidden="false" aria-label="help-icon"
matSuffix style="cursor:pointer;"
matTooltip="{{'gateway.hints.client-id' | translate }}">info_outlined
</mat-icon>
</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-icon class="mat-form-field-infix pe suffix-icon" aria-hidden="false" aria-label="help-icon"
matSuffix style="cursor:pointer;"
matTooltip="{{'gateway.hints.username' | translate }}">info_outlined
</mat-icon>
</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-icon class="mat-form-field-infix pe suffix-icon" aria-hidden="false" aria-label="help-icon"
matSuffix style="cursor:pointer;"
matTooltip="{{'gateway.hints.password' | translate }}">info_outlined
</mat-icon>
</mat-form-field>
<tb-file-input
fxFlex="100"
hint="{{'gateway.hints.ca-cert' | translate}}"
*ngIf="gatewayConfigGroup.get('thingsboard.security.type').value.toLowerCase().includes('tls')"
formControlName="caCert"
label="{{'security.ca-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"
hint="{{'gateway.hints.cert' | translate}}"
*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"
hint="{{'gateway.hints.private-key' | translate}}"
*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-icon class="mat-form-field-infix pe suffix-icon" aria-hidden="false" aria-label="help-icon"
matSuffix style="cursor:pointer;"
matTooltip="{{'gateway.hints.date-form' | translate }}">info_outlined
</mat-icon>
</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-icon class="mat-form-field-infix pe suffix-icon" aria-hidden="false" aria-label="help-icon"
matSuffix style="cursor:pointer;"
matTooltip="{{'gateway.hints.log-format' | translate }}">info_outlined
</mat-icon>
</mat-form-field>
</div>
</mat-expansion-panel>
<mat-expansion-panel expanded="true" disabled formGroupName="remote" >
<mat-expansion-panel-header>
<mat-panel-title translate class="expansion-panel-header">gateway.logs.remote</mat-panel-title>
</mat-expansion-panel-header>
<mat-slide-toggle color="primary" formControlName="enabled">
{{ 'gateway.logs.remote-logs' | translate }}
<mat-icon class="mat-form-field-infix pe suffix-icon" aria-hidden="false" aria-label="help-icon"
matSuffix style="cursor:pointer;"
matTooltip="{{'gateway.hints.remote-log' | translate }}">info
</mat-icon>
</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>
</mat-expansion-panel>
<mat-expansion-panel expanded="true" disabled formGroupName="local" >
<mat-expansion-panel-header>
<mat-panel-title translate class="expansion-panel-header">gateway.logs.local</mat-panel-title>
</mat-expansion-panel-header>
<mat-button-toggle-group class="security-toggle-group" [formControl]="logSelector" fxFlex="100"
name="logSelector">
<mat-button-toggle *ngFor="let logConfig of localLogsConfigLabels| keyvalue" [value]="logConfig.key"
class="first-capital">
{{ logConfig.value }}
</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" min="0" />
<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" min="0" />
<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-icon class="mat-form-field-infix pe suffix-icon" aria-hidden="false" aria-label="help-icon"
matSuffix style="cursor:pointer;"
matTooltip="{{'gateway.hints.backup-count' | translate }}">info_outlined
</mat-icon>
</mat-form-field>
</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
<span class="tb-hint">{{'gateway.hints.storage' | translate}}</span>
</mat-panel-title>
</mat-expansion-panel-header>
<div fxLayout="row wrap">
<mat-button-toggle-group fxLayout="row wrap" 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>
<div class="line-break "></div>
<span class="tb-hint" style="padding-left: 0">
{{'gateway.hints.'+gatewayConfigGroup.get('storage.type').value | translate}}</span>
</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-icon class="mat-form-field-infix pe suffix-icon" aria-hidden="false" aria-label="help-icon"
matSuffix style="cursor:pointer;"
matTooltip="{{'gateway.hints.read-record-count' | translate }}">info_outlined
</mat-icon>
</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-icon class="mat-form-field-infix pe suffix-icon" aria-hidden="false" aria-label="help-icon"
matSuffix style="cursor:pointer;"
matTooltip="{{'gateway.hints.max-records-count' | translate }}">info_outlined
</mat-icon>
</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-icon class="mat-form-field-infix pe suffix-icon" aria-hidden="false" aria-label="help-icon"
matSuffix style="cursor:pointer;"
matTooltip="{{'gateway.hints.data-folder' | translate }}">info_outlined
</mat-icon>
</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-icon class="mat-form-field-infix pe suffix-icon" aria-hidden="false" aria-label="help-icon"
matSuffix style="cursor:pointer;"
matTooltip="{{'gateway.hints.max-file-count' | translate }}">info_outlined
</mat-icon>
</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-icon class="mat-form-field-infix pe suffix-icon" aria-hidden="false" aria-label="help-icon"
matSuffix style="cursor:pointer;"
matTooltip="{{'gateway.hints.max-read-count' | translate }}">info_outlined
</mat-icon>
</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-icon class="mat-form-field-infix pe suffix-icon" aria-hidden="false" aria-label="help-icon"
matSuffix style="cursor:pointer;"
matTooltip="{{'gateway.hints.max-records' | translate }}">info_outlined
</mat-icon>
</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-icon class="mat-form-field-infix pe suffix-icon" aria-hidden="false" aria-label="help-icon"
matSuffix style="cursor:pointer;"
matTooltip="{{'gateway.hints.data-folder' | translate }}">info_outlined
</mat-icon>
</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-icon class="mat-form-field-infix pe suffix-icon" aria-hidden="false" aria-label="help-icon"
matSuffix style="cursor:pointer;"
matTooltip="{{'gateway.hints.ttl-check-hour' | translate }}">info_outlined
</mat-icon>
</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-icon class="mat-form-field-infix pe suffix-icon" aria-hidden="false" aria-label="help-icon"
matSuffix style="cursor:pointer;"
matTooltip="{{'gateway.hints.ttl-messages-day' | translate }}">info_outlined
</mat-icon>
</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" min="0" />
<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" min="0" />
<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" min="0" />
<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" min="0" />
<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" min="0" />
<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" min="0" />
<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" min="1" />
<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>
</div>
</mat-expansion-panel>
<mat-expansion-panel expanded="true" disabled>
<mat-expansion-panel-header>
<mat-panel-title class="expansion-panel-header">{{"gateway.statistics.commands" |translate}}
<span class="tb-hint">
{{'gateway.hints.commands' | translate}}</span></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" [formGroupName]="$index" class="statistics-block">
<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="attributeOnGateway"/>
<mat-error
*ngIf="commandControl.get('attributeOnGateway').hasError('required')">
{{'gateway.statistics.attribute-name-required' | translate }}
</mat-error>
<mat-icon class="mat-form-field-infix pe suffix-icon" aria-hidden="false" aria-label="help-icon"
matSuffix style="cursor:pointer;"
matTooltip="{{'gateway.hints.attribute' | translate }}">info_outlined
</mat-icon>
</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" min="0" />
<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-icon class="mat-form-field-infix pe suffix-icon" aria-hidden="false" aria-label="help-icon"
matSuffix style="cursor:pointer;"
matTooltip="{{'gateway.hints.timeout' | translate }}">info_outlined
</mat-icon>
</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-icon class="mat-form-field-infix pe suffix-icon" aria-hidden="false" aria-label="help-icon"
matSuffix style="cursor:pointer;"
matTooltip="{{'gateway.hints.command' | translate }}">info_outlined
</mat-icon>
</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"
[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>
<span class="tb-hint" style="padding-left: 40px; margin-top: -15px;">{{'gateway.hints.check-device-activity' | translate}}</span>
</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" min="0" />
<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-icon class="mat-form-field-infix pe suffix-icon" aria-hidden="false" aria-label="help-icon"
matSuffix style="cursor:pointer;"
matTooltip="{{'gateway.hints.inactivity-timeout' | translate }}">info_outlined
</mat-icon>
</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-icon class="mat-form-field-infix pe suffix-icon" aria-hidden="false" aria-label="help-icon"
matSuffix style="cursor:pointer;"
matTooltip="{{'gateway.hints.inactivity-period' | translate }}">info_outlined
</mat-icon>
</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-icon class="mat-form-field-infix pe suffix-icon" aria-hidden="false" aria-label="help-icon"
matSuffix style="cursor:pointer;"
matTooltip="{{'gateway.hints.minimal-pack-delay' | translate }}">info_outlined
</mat-icon>
</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="1"/>
<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-icon class="mat-form-field-infix pe suffix-icon" aria-hidden="false" aria-label="help-icon"
matSuffix style="cursor:pointer;"
matTooltip="{{'gateway.hints.qos' | translate }}">info_outlined
</mat-icon>
</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" min="0" />
<mat-error
*ngIf="gatewayConfigGroup.get('thingsboard.checkConnectorsConfigurationInSeconds').hasError('required')">
{{'gateway.statistics.check-connectors-configuration-required' | translate }}
</mat-error>
<mat-error
*ngIf="gatewayConfigGroup.get('thingsboard.checkConnectorsConfigurationInSeconds').hasError('min')">
{{'gateway.statistics.check-connectors-configuration-min' | translate }}
</mat-error>
<mat-error
*ngIf="gatewayConfigGroup.get('thingsboard.checkConnectorsConfigurationInSeconds').hasError('pattern')">
{{'gateway.statistics.check-connectors-configuration-pattern' | 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="start center">
<button mat-button color="primary"
type="button"
*ngIf="dialogRef"
(click)="cancel()">
{{ 'action.cancel' | translate }}
</button>
<button mat-raised-button color="primary"
type="button"
[disabled]="gatewayConfigGroup.invalid || !gatewayConfigGroup.dirty"
(click)="saveConfig()">
{{ 'action.save' | translate }}
</button>
</div>
</form>

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

@ -0,0 +1,201 @@
/**
* Copyright © 2016-2023 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;
.mat-icon {
color: rgba(0, 0, 0, .12);
}
.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;
}
}
.statistics-block {
margin-bottom: 15px;
padding-left: 15px;
padding-top: 15px;
}
.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;
}
::ng-deep.pe {
pointer-events: all;
}
::ng-deep.mat-mdc-form-field-icon-suffix {
z-index: 100;
}
.suffix-icon {
position: relative;
transform: translateY(5px);
}
mat-panel-title {
display: block;
padding-top: 20px;
}
mat-panel-title span {
display: block;
padding-left: 0;
padding-top: 5px;
}
.tb-hint {
font-size: 13px;
color: rgba(0, 0, 0, .54);
width: fit-content;
cursor: pointer;
text-transform: none!important;
}
.line-break {
width: 100%;
}
textarea {
resize: none;
}
}

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

@ -0,0 +1,589 @@
///
/// Copyright © 2016-2023 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 { 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 { 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';
import { of } from 'rxjs';
import { mergeMap } from 'rxjs/operators';
import { DeviceCredentialsType } from '@shared/models/device.models';
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 localLogsConfigLabels = new Map<LocalLogsConfigs, string>([
[LocalLogsConfigs.service, 'Service'],
[LocalLogsConfigs.connector, 'Connector'],
[LocalLogsConfigs.converter, 'Converter'],
[LocalLogsConfigs.tb_connection, 'TB Connection'],
[LocalLogsConfigs.storage, 'Storage'],
[LocalLogsConfigs.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;
localLogsConfigLabels = localLogsConfigLabels;
securityTypes = securityTypesTranslationsMap;
gatewayLogLevel = Object.values(GatewayLogLevel);
@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,
private cd: ChangeDetectorRef,
public dialog: MatDialog) {
}
ngOnInit() {
this.gatewayConfigGroup = this.fb.group({
thingsboard: this.fb.group({
host: [window.location.hostname, [Validators.required]],
port: [1883, [Validators.required, Validators.min(1), Validators.max(65535), Validators.pattern(/^-?[0-9]+$/)]],
remoteShell: [false, []],
remoteConfiguration: [true, []],
checkConnectorsConfigurationInSeconds: [60, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
statistics: this.fb.group({
enable: [true, []],
statsSendPeriodInSeconds: [3600, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
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(1), 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();
});
securityGroup.get('caCert').valueChanges.subscribe(_ => this.cd.detectChanges())
securityGroup.get('privateKey').valueChanges.subscribe(_ => this.cd.detectChanges())
securityGroup.get('cert').valueChanges.subscribe(_ => this.cd.detectChanges())
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.CLIENT_SCOPE,
['general_configuration', 'grpc_configuration', 'logs_configuration', 'storage_configuration', 'RemoteLoggingLevel']).pipe(
mergeMap(attributes => attributes.length ? of(attributes) : this.attributeService.getEntityAttributes(
entityId, AttributeScope.SHARED_SCOPE, ['general_configuration', 'grpc_configuration', 'logs_configuration', 'storage_configuration', 'RemoteLoggingLevel']))
).subscribe(attributes => {
if (attributes.length) {
const general_configuration = attributes.find(attribute => attribute.key === 'general_configuration')?.value;
const grpc_configuration = attributes.find(attribute => attribute.key === 'grpc_configuration')?.value;
const logs_configuration = attributes.find(attribute => attribute.key === 'logs_configuration')?.value;
const storage_configuration = attributes.find(attribute => attribute.key === 'storage_configuration')?.value;
const remoteLoggingLevel = attributes.find(attribute => attribute.key === 'RemoteLoggingLevel')?.value;
if (general_configuration) {
const configObj = {thingsboard: general_configuration};
if (configObj.thingsboard.statistics && configObj.thingsboard.statistics.commands) {
for (const command of configObj.thingsboard.statistics.commands) {
this.addCommand(command);
}
delete configObj.thingsboard.statistics.commands;
}
this.gatewayConfigGroup.patchValue(configObj, {emitEvent: false});
this.gatewayConfigGroup.markAsPristine();
if (!configObj.thingsboard.remoteConfiguration) {
this.gatewayConfigGroup.disable({emitEvent: false});
}
this.checkAndFetchCredentials(configObj.thingsboard.security);
}
if (grpc_configuration) {
const configObj = {grpc: grpc_configuration};
this.gatewayConfigGroup.patchValue(configObj, {emitEvent: false});
}
if (logs_configuration) {
const configObj = {logs: this.logsToObj(logs_configuration)};
this.gatewayConfigGroup.patchValue(configObj, {emitEvent: false});
this.cd.detectChanges();
}
if (storage_configuration) {
const configObj = {storage: storage_configuration};
this.gatewayConfigGroup.patchValue(configObj, {emitEvent: false});
}
if (remoteLoggingLevel) {
const remoteLogsFormGroup = this.gatewayConfigGroup.get('logs.remote');
remoteLogsFormGroup.patchValue({
enabled: remoteLoggingLevel !== GatewayLogLevel.none,
logLevel: remoteLoggingLevel
}, {emitEvent: false});
remoteLogsFormGroup.markAsPristine();
}
this.cd.detectChanges();
} else {
this.checkAndFetchCredentials({});
}
});
}
checkAndFetchCredentials(security): void {
if (security.type !== SecurityTypes.TLS_PRIVATE_KEY) {
this.deviceService.getDeviceCredentials(this.device.id).subscribe(credentials => {
if (credentials.credentialsType === DeviceCredentialsType.ACCESS_TOKEN || security.type !== SecurityTypes.TLS_ACCESS_TOKEN) {
this.gatewayConfigGroup.get('thingsboard.security.accessToken').setValue(credentials.credentialsId);
} else if (credentials.credentialsType === DeviceCredentialsType.MQTT_BASIC) {
const parsedValue = JSON.parse(credentials.credentialsValue);
this.gatewayConfigGroup.get('thingsboard.security.clientId').setValue(parsedValue.clientId);
this.gatewayConfigGroup.get('thingsboard.security.username').setValue(parsedValue.userName);
this.gatewayConfigGroup.get('thingsboard.security.password').setValue(parsedValue.password);
} else if (credentials.credentialsType === DeviceCredentialsType.X509_CERTIFICATE ) {
//if sertificate is present set sertificate as present
}
});
}
}
logsToObj(logsConfig) {
const logsObject = {
local: {}
}
const logFormat = logsConfig.formatters.LogFormatter.format;
const dateFormat = logsConfig.formatters.LogFormatter.datefmt;
for (const localLogsConfigsKey of Object.keys(LocalLogsConfigs)) {
const handlerKey = localLogsConfigsKey + "Handler";
logsObject[localLogsConfigsKey] = {
logLevel: logsConfig.loggers[localLogsConfigsKey].level,
filePath: logsConfig.handlers[handlerKey].filename.split('/'+localLogsConfigsKey)[0],
backupCount: logsConfig.handlers[handlerKey].backupCount,
savingTime: logsConfig.handlers[handlerKey].interval,
savingPeriod: logsConfig.handlers[handlerKey].when,
}
}
return {local: logsObject, logFormat, dateFormat};
}
addCommand(command?): void {
const data = command || {};
const commandsFormArray = this.commandFormArray();
const commandFormGroup = this.fb.group({
attributeOnGateway: [data.attributeOnGateway || 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 || './logs', [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);
this.gatewayConfigGroup.markAsDirty();
}
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) {
const logAttrObj = {
version: 1,
disable_existing_loggers: false,
formatters: {
LogFormatter: {
class: "logging.Formatter",
format: logsObj.logFormat,
datefmt: logsObj.dateFormat,
}
},
handlers: {
consoleHandler: {
class: "logging.StreamHandler",
formatter: "LogFormatter",
level: "DEBUG",
stream: "ext://sys.stdout"
},
databaseHandler: {
class: "thingsboard_gateway.tb_utility.tb_logger.TimedRotatingFileHandler",
formatter: "LogFormatter",
filename: "./logs/database.log",
backupCount: 1,
encoding: "utf-8"
}
},
loggers: {
database: {
handlers: ["databaseHandler", "consoleHandler"],
level: "DEBUG",
propagate: false
}
},
root: {
level: "ERROR",
handlers: [
"consoleHandler"
]
}
}
for (const key of Object.keys(logsObj.local)) {
logAttrObj.handlers[key+"Handler"] = this.createHandlerObj(logsObj.local[key], key);
logAttrObj.loggers[key] = this.createLoggerObj(logsObj.local[key], key);
}
return logAttrObj;
}
createHandlerObj(logObj, key) {
return {
class: "thingsboard_gateway.tb_utility.tb_logger.TimedRotatingFileHandler",
formatter: "LogFormatter",
filename: `${logObj.filePath}/${key}.log`,
backupCount: logObj.backupCount,
interval: logObj.savingTime,
when: logObj.savingPeriod,
encoding: "utf-8"
}
}
createLoggerObj(logObj, key) {
return {
handlers: [`${key}Handler`, "consoleHandler"],
level: logObj.logLevel,
propagate: false
}
}
saveConfig(): void {
const value = this.removeEmpty(this.gatewayConfigGroup.value);
value.thingsboard.statistics.commands = Object.values(value.thingsboard.statistics.commands);
const attributes = [];
attributes.push({
key: 'RemoteLoggingLevel',
value: value.logs.remote.enabled ? value.logs.remote.logLevel : GatewayLogLevel.none
});
delete value.connectors;
attributes.push({
key: 'logs_configuration',
value: this.generateLogsFile(value.logs)
});
attributes.push({
key: 'grpc_configuration',
value: value.grpc
});
attributes.push({
key: 'storage_configuration',
value: value.storage
});
attributes.push({
key: 'general_configuration',
value: value.thingsboard
});
this.attributeService.saveEntityAttributes(this.device, AttributeScope.SHARED_SCOPE, attributes).subscribe(_ => {
if (this.dialogRef) {
this.dialogRef.close();
} else {
this.gatewayConfigGroup.markAsPristine();
this.cd.detectChanges();
}
});
}
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});
}
}
);
});
}
}

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

@ -0,0 +1,167 @@
<!--
Copyright © 2016-2023 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="height: calc(100% - 7px)" fxLayout="column">
<div fxLayout="row" fxLayoutGap="15px" fxFlex class="connector-container">
<mat-card fxLayout="column" style="overflow: auto;">
<mat-toolbar class="mat-mdc-table-toolbar">
<h2>Connectors</h2>
<span fxFlex></span>
<button mat-icon-button
[disabled]="isLoading$ | async"
(click)="addAttribute()"
matTooltip="{{ 'action.add' | translate }}"
matTooltipPosition="above">
<mat-icon>add</mat-icon>
</button>
</mat-toolbar>
<table mat-table [dataSource]="dataSource"
matSort [matSortActive]="pageLink.sortOrder.property" [matSortDirection]="pageLink.sortDirection()"
matSortDisableClear>
<ng-container matColumnDef="enabled" sticky>
<mat-header-cell *matHeaderCellDef style="width: 30px;">
Enabled
</mat-header-cell>
<mat-cell *matCellDef="let attribute">
<mat-slide-toggle [checked]="activeConnectors.includes(attribute.key)"
(click)="$event.stopPropagation(); enableConnector(attribute)"></mat-slide-toggle>
</mat-cell>
</ng-container>
<ng-container matColumnDef="key">
<mat-header-cell *matHeaderCellDef mat-sort-header style="width: 40%"> Name</mat-header-cell>
<mat-cell *matCellDef="let attribute">
{{ attribute.key }}
</mat-cell>
</ng-container>
<ng-container matColumnDef="type">
<mat-header-cell *matHeaderCellDef mat-sort-header style="width: 30%"> Type</mat-header-cell>
<mat-cell *matCellDef="let attribute" style="text-transform: uppercase">
{{ returnType(attribute) }}
</mat-cell>
</ng-container>
<ng-container matColumnDef="actions" stickyEnd>
<mat-header-cell *matHeaderCellDef
[ngStyle.gt-md]="{ minWidth: '144px', maxWidth: '144px', width: '144px'}">
Actions
</mat-header-cell>
<mat-cell *matCellDef="let attribute"
[ngStyle.gt-md]="{ minWidth: '144px', maxWidth: '144px', width: '144px'}">
<div fxHide fxShow.gt-md fxFlex fxLayout="row" fxLayoutAlign="end">
<button mat-icon-button
matTooltip="RPC"
matTooltipPosition="above"
(click)="connectorRpc(attribute, $event)">
<mat-icon>private_connectivity</mat-icon>
</button>
<button mat-icon-button
matTooltip="Logs"
matTooltipPosition="above"
(click)="connectorLogs(attribute, $event)">
<mat-icon>list</mat-icon>
</button>
<button mat-icon-button
matTooltip="Delete connector"
matTooltipPosition="above"
(click)="deleteConnector(attribute, $event)">
<mat-icon>delete</mat-icon>
</button>
</div>
<div fxHide fxShow.lt-lg>
<button mat-icon-button
(click)="$event.stopPropagation()"
[matMenuTriggerFor]="cellActionsMenu">
<mat-icon class="material-icons">more_vert</mat-icon>
</button>
<mat-menu #cellActionsMenu="matMenu" xPosition="before">
<button mat-icon-button
matTooltip="RPC"
matTooltipPosition="above"
(click)="connectorRpc(attribute, $event)">
<mat-icon>private_connectivity</mat-icon>
</button>
<button mat-icon-button
matTooltip="Delete connector"
matTooltipPosition="above"
(click)="connectorLogs(attribute, $event)">
<mat-icon>list</mat-icon>
</button>
<button mat-icon-button
matTooltip="Delete connector"
matTooltipPosition="above"
(click)="deleteConnector(attribute, $event)">
<mat-icon>delete</mat-icon>
</button>
</mat-menu>
</div>
</mat-cell>
</ng-container>
<mat-header-row [ngClass]="{'mat-row-select': true}"
*matHeaderRowDef="displayedColumns; sticky: true"></mat-header-row>
<mat-row [ngClass]="{'mat-row-select': true}"
*matRowDef="let attribute; columns: displayedColumns;" (click)="selectConnector(attribute)"></mat-row>
</table>
<mat-divider></mat-divider>
</mat-card>
<div [formGroup]="connectorForm" fxLayout="column">
<mat-card fxLayout="row" fxLayoutGap="15px">
<mat-form-field class="mat-block tb-value-type">
<mat-label>Name</mat-label>
<input matInput formControlName="name"/>
</mat-form-field>
<mat-form-field class="mat-block tb-value-type">
<mat-label>Type</mat-label>
<mat-select formControlName="type">
<mat-option style="text-transform: uppercase"
*ngFor="let type of gatewayConnectorDefaultTypes | keyvalue" [value]="type.key">{{type.value}}</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="mat-block tb-value-type" *ngIf="connectorForm.get('type').value === 'grpc'">
<mat-label>Key</mat-label>
<input matInput formControlName="key"/>
</mat-form-field>
<mat-form-field class="mat-block tb-value-type" *ngIf="connectorForm.get('type').value === 'custom'">
<mat-label>Class</mat-label>
<input matInput formControlName="class"/>
</mat-form-field>
<mat-form-field class="mat-block tb-value-type">
<mat-label translate>gateway.remote-logging-level</mat-label>
<mat-select formControlName="log_level">
<mat-option *ngFor="let logLevel of gatewayLogLevel" [value]="logLevel">{{logLevel}}</mat-option>
</mat-select>
</mat-form-field>
</mat-card>
<mat-card fxLayout="column" fxFlex>
<tb-json-object-edit
fxFlex
fxLayout="column"
required
label="{{ 'gateway.connector-json' | translate }}"
formControlName="configurationJson">
</tb-json-object-edit>
<div fxLayoutAlign="start center">
<button mat-raised-button color="primary"
class="action-btns"
type="button"
[disabled]="!connectorForm.dirty || connectorForm.invalid"
(click)="saveConnector()">
{{ 'action.save' | translate }}
</button>
</div>
</mat-card>
</div>
</div>
</form>

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

@ -0,0 +1,90 @@
/**
* Copyright © 2016-2023 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;
overflow-x: auto;
padding: 0;
.connector-container {
width: 100%;
&>mat-card, &>div {
min-width: 50%;
}
mat-card {
margin: 10px;
padding: 10px;
max-width: 100%;
}
}
.tb-entity-table {
.tb-entity-table-content {
width: 100%;
height: 100%;
background: #fff;
overflow: hidden;
}
}
.mat-toolbar {
background: transparent;
color: rgba(0, 0, 0, .87) !important;
}
.mat-mdc-form-field {
flex-grow: 1;
//min-width: 200px;
//flex-shrink: 1;
}
mat-expansion-panel {
border: 1px solid #305680;
}
mat-card {
padding-left: 10px;
background: transparent;
}
.mat-card-selected {
background-color: rgba(48, 86, 128, 0.1);
}
.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;
}
mat-row {
cursor: pointer;
}
}

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

@ -0,0 +1,390 @@
///
/// Copyright © 2016-2023 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 { AfterViewInit, ChangeDetectorRef, Component, ElementRef, Input, NgZone, ViewChild } from '@angular/core';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { Router } from '@angular/router';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { EntityId } from '@shared/models/id/entity-id';
import { MatDialog } 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, merge } from 'rxjs';
import { AttributeData, AttributeScope } from '@shared/models/telemetry/telemetry.models';
import { PageComponent } from '@shared/components/page.component';
import { PageLink } from '@shared/models/page/page-link';
import { AttributeDatasource } from '@home/models/datasource/attribute-datasource';
import { Direction, SortOrder } from '@shared/models/page/sort-order';
import { MatSort } from '@angular/material/sort';
import { tap } from 'rxjs/operators';
import { TelemetryWebsocketService } from '@core/ws/telemetry-websocket.service';
import { MatTableDataSource } from '@angular/material/table';
import { GatewayLogLevel } from '@shared/components/device/gateway-configuration.component';
import { ActionNotificationShow } from '@core/notification/notification.actions';
import { DialogService } from '@app/core/services/dialog.service';
import { WidgetContext } from '@home/models/widget-component.models';
import { deepClone } from '@core/utils';
export interface gatewayConnector {
name: string;
type: string;
configuration?: string;
configurationJson: string;
log_level: string;
key?: string;
}
export const GatewayConnectorDefaultTypesTranslates = new Map<string, string>([
['mqtt', 'MQTT'],
['modbus', 'MODBUS'],
['grpc', 'GRPC'],
['opcua', 'OPCUA'],
['opcua_asyncio', 'OPCUA ASYNCIO'],
['ble', 'BLE'],
['request', 'REQUEST'],
['can', 'CAN'],
['bacnet', 'BACNET'],
['odbc', 'ODBC'],
['rest', 'REST'],
['snmp', 'SNMP'],
['ftp', 'FTP'],
['socket', 'SOCKET'],
['xmpp', 'XMPP'],
['ocpp', 'OCCP'],
['custom', 'CUSTOM']
]);
@Component({
selector: 'tb-gateway-connector',
templateUrl: './gateway-connectors.component.html',
styleUrls: ['./gateway-connectors.component.scss']
})
export class GatewayConnectorComponent extends PageComponent implements AfterViewInit {
pageLink: PageLink;
attributeDataSource: AttributeDatasource;
inactiveConnectorsDataSource: AttributeDatasource;
dataSource: MatTableDataSource<AttributeData>
displayedColumns = ['enabled', 'key', 'type', 'actions'];
gatewayConnectorDefaultTypes = GatewayConnectorDefaultTypesTranslates;
@Input()
ctx: WidgetContext;
@Input()
device: EntityId;
@ViewChild('searchInput') searchInputField: ElementRef;
@ViewChild(MatSort) sort: MatSort;
connectorForm: FormGroup;
viewsInited = false;
textSearchMode: boolean;
activeConnectors: Array<string>;
inactiveConnectors: Array<string>;
InitialActiveConnectors: Array<string>;
gatewayLogLevel = Object.values(GatewayLogLevel);
activeData: Array<any> = [];
inactiveData: Array<any> = [];
constructor(protected router: Router,
protected store: Store<AppState>,
protected fb: FormBuilder,
protected translate: TranslateService,
protected attributeService: AttributeService,
protected deviceService: DeviceService,
protected dialogService: DialogService,
private telemetryWsService: TelemetryWebsocketService,
private zone: NgZone,
private cd: ChangeDetectorRef,
public dialog: MatDialog) {
super(store);
const sortOrder: SortOrder = {property: 'key', direction: Direction.ASC};
this.pageLink = new PageLink(1000, 0, null, sortOrder);
this.attributeDataSource = new AttributeDatasource(this.attributeService, this.telemetryWsService, this.zone, this.translate);
this.inactiveConnectorsDataSource = new AttributeDatasource(this.attributeService, this.telemetryWsService, this.zone, this.translate);
this.dataSource = new MatTableDataSource<AttributeData>([]);
this.connectorForm = this.fb.group({
name: ['', [Validators.required]],
type: ['', [Validators.required]],
log_level: ['', [Validators.required]],
key: ['auto'],
class: [''],
configuration: [''],
configurationJson: [{}, [Validators.required]]
})
this.connectorForm.disable();
}
initialConnector: gatewayConnector;
ngAfterViewInit() {
this.connectorForm.valueChanges.subscribe(() => {
this.cd.detectChanges();
});
merge(this.sort.sortChange)
.pipe(
tap(() => this.updateData())
)
.subscribe();
this.viewsInited = true;
if (this.device) {
forkJoin(this.attributeService.getEntityAttributes(this.device, AttributeScope.SHARED_SCOPE, ['active_connectors']),
this.attributeService.getEntityAttributes(this.device, AttributeScope.SERVER_SCOPE, ['inactive_connectors'])).subscribe(attributes => {
if (attributes.length) {
this.activeConnectors = attributes[0].length ? attributes[0][0].value : [];
this.inactiveConnectors = attributes[1].length ? attributes[1][0].value : [];
this.updateData(true);
} else {
this.activeConnectors = [];
this.inactiveConnectors = [];
this.updateData(true);
}
})
}
}
saveConnector(): void {
const value = this.connectorForm.value;
value.configuration = this.camelize(value.name) + ".json";
if (value.type !== 'grpc') {
delete value.key;
}
const attributesToSave = [{
key: value.name,
value: value
}];
const attributesToDelete = [];
const scope = (this.activeConnectors.includes(value.name) || !this.initialConnector) ? AttributeScope.SHARED_SCOPE : AttributeScope.SERVER_SCOPE;
let updateActiveConnectors = false;
if (this.initialConnector && this.initialConnector.name !== value.name) {
attributesToDelete.push({key: this.initialConnector.name});
updateActiveConnectors = true;
const activeIndex = this.activeConnectors.indexOf(this.initialConnector.name);
const inactiveIndex = this.inactiveConnectors.indexOf(this.initialConnector.name);
if (activeIndex !== -1) this.activeConnectors.splice(activeIndex, 1);
if (inactiveIndex !== -1) this.inactiveConnectors.splice(activeIndex, 1);
}
if (!this.activeConnectors.includes(value.name) && scope == AttributeScope.SHARED_SCOPE) {
this.activeConnectors.push(value.name);
updateActiveConnectors = true;
}
if (!this.inactiveConnectors.includes(value.name) && scope == AttributeScope.SERVER_SCOPE) {
this.inactiveConnectors.push(value.name);
updateActiveConnectors = true;
}
const tasks = [this.attributeService.saveEntityAttributes(this.device, AttributeScope.SHARED_SCOPE, attributesToSave)];
if (updateActiveConnectors) {
tasks.push(this.attributeService.saveEntityAttributes(this.device, scope, [{
key: scope == AttributeScope.SHARED_SCOPE ? 'active_connectors' : 'inactive_connectors',
value: scope == AttributeScope.SHARED_SCOPE ? this.activeConnectors : this.inactiveConnectors
}]));
}
if (attributesToDelete.length) {
tasks.push(this.attributeService.deleteEntityAttributes(this.device, AttributeScope.SHARED_SCOPE, attributesToDelete));
}
forkJoin(tasks).subscribe(_ => {
this.showToast("Update Successful")
this.updateData(true);
})
}
resetSortAndFilter(update: boolean = true) {
this.textSearchMode = false;
this.pageLink.textSearch = null;
if (this.viewsInited) {
const sortable = this.sort.sortables.get('key');
this.sort.active = sortable.id;
this.sort.direction = 'asc';
if (update) {
this.updateData(true);
}
}
}
updateData(reload: boolean = false) {
this.pageLink.sortOrder.property = this.sort.active;
this.pageLink.sortOrder.direction = Direction[this.sort.direction.toUpperCase()];
this.attributeDataSource.loadAttributes(this.device, AttributeScope.CLIENT_SCOPE, this.pageLink, reload).subscribe(data => {
this.activeData = data.data.filter(value => this.activeConnectors.includes(value.key));
this.combineData()
});
this.inactiveConnectorsDataSource.loadAttributes(this.device, AttributeScope.SHARED_SCOPE, this.pageLink, reload).subscribe(data => {
this.inactiveData = data.data.filter(value =>this.inactiveConnectors.includes(value.key));
this.combineData()
});
}
combineData () {
this.dataSource.data = [...this.activeData, ...this.inactiveData];
}
addAttribute(): void {
if (this.connectorForm.disabled) {
this.connectorForm.enable();
}
this.clearOutConnectorForm();
}
clearOutConnectorForm(): void {
this.connectorForm.setValue({
name: '',
type: 'mqtt',
log_level: GatewayLogLevel.info,
key: 'auto',
class: '',
configuration: '',
configurationJson: {}
})
this.initialConnector = null;
this.connectorForm.markAsPristine();
}
selectConnector(attribute): void {
if (this.connectorForm.disabled) {
this.connectorForm.enable();
}
const connector = typeof attribute.value === 'string' ? JSON.parse(attribute.value): attribute.value;
if (!connector.configuration) {
connector.configuration = "";
}
if (!connector.key) {
connector.key = 'auto';
}
this.initialConnector = connector;
this.connectorForm.setValue(connector);
this.connectorForm.markAsPristine();
}
showToast(message: string) {
this.store.dispatch(new ActionNotificationShow(
{
message,
type: 'success',
duration: 1000,
verticalPosition: 'top',
horizontalPosition: 'right',
target: 'dashboardRoot',
// panelClass: this.widgetNamespace,
forceDismiss: true
}));
}
returnType (attribute) {
const value = typeof attribute.value === 'string' ? JSON.parse(attribute.value): attribute.value;
return this.gatewayConnectorDefaultTypes.get(value.type)
}
deleteConnector(attribute: AttributeData, $event: Event): void {
if ($event) {
$event.stopPropagation();
}
const title = `Delete connector ${attribute.key}?`;
const content = `All connector data will be deleted.`;
this.dialogService.confirm(title, content, 'Cancel', 'Delete').subscribe(result => {
if (result) {
const tasks = [];
const scope = (this.activeConnectors.includes(attribute.key) || !this.initialConnector) ? AttributeScope.SHARED_SCOPE : AttributeScope.SERVER_SCOPE;
tasks.push(this.attributeService.deleteEntityAttributes(this.device, AttributeScope.SHARED_SCOPE, [attribute]));
const activeIndex = this.activeConnectors.indexOf(attribute.key);
const inactiveIndex = this.inactiveConnectors.indexOf(attribute.key);
if (activeIndex !== -1) this.activeConnectors.splice(activeIndex, 1);
if (inactiveIndex !== -1) this.inactiveConnectors.splice(activeIndex, 1);
tasks.push(this.attributeService.saveEntityAttributes(this.device, scope, [{
key: scope == AttributeScope.SHARED_SCOPE ? 'active_connectors' : 'inactive_connectors',
value: scope == AttributeScope.SHARED_SCOPE ? this.activeConnectors : this.inactiveConnectors
}]));
forkJoin(tasks).subscribe(_ => {
if (this.initialConnector ? this.initialConnector.name === attribute.key : true) {
this.clearOutConnectorForm();
this.cd.detectChanges();
this.connectorForm.disable();
}
this.updateData()
})
}
})
}
camelize(str): string {
return str.toLowerCase().replace(/\s+/g, '_');
}
connectorLogs(attribute: AttributeData, $event: Event): void {
if ($event) {
$event.stopPropagation();
}
attribute.value = typeof attribute.value === 'string' ? JSON.parse(attribute.value): attribute.value;
const params = deepClone(this.ctx.stateController.getStateParams());
params.targetEntityParamName = "connector_logs";
this.ctx.stateController.openState("connector_logs", params);
}
connectorRpc(attribute: AttributeData, $event: Event): void {
if ($event) {
$event.stopPropagation();
}
attribute.value = typeof attribute.value === 'string' ? JSON.parse(attribute.value): attribute.value;
const params = deepClone(this.ctx.stateController.getStateParams());
params.connector_rpc = attribute;
params.targetEntityParamName = "connector_rpc";
this.ctx.stateController.openState("connector_rpc", params);
}
enableConnector(attribute): void {
const wasEnabled = this.activeConnectors.includes(attribute.key);
if (wasEnabled) {
let index = this.activeConnectors.indexOf(attribute.key);
this.activeConnectors.splice(index, 1);
this.inactiveConnectors.push(attribute.key);
} else {
let index = this.inactiveConnectors.indexOf(attribute.key);
this.inactiveConnectors.splice(index, 1);
this.activeConnectors.push(attribute.key);
}
forkJoin([this.attributeService.saveEntityAttributes(this.device, AttributeScope.SHARED_SCOPE, [{
key: 'active_connectors',
value: this.activeConnectors
}]), this.attributeService.saveEntityAttributes(this.device, AttributeScope.SERVER_SCOPE, [{
key: 'inactive_connectors',
value: this.inactiveConnectors
}]),]).subscribe(_ => {
this.updateData();
})
}
}

60
ui-ngx/src/app/shared/components/device/gateway-logs.component.html

@ -0,0 +1,60 @@
<!--
Copyright © 2016-2023 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.
-->
<nav mat-tab-nav-bar [tabPanel]="tabPanel">
<a mat-tab-link *ngFor="let link of logLinks"
(click)="onTabChanged(link)"
[active]="activeLink.name == link.name"> {{link.name}} </a>
</nav>
<mat-tab-nav-panel #tabPanel></mat-tab-nav-panel>
<table mat-table [dataSource]="dataSource"
matSort [matSortActive]="pageLink.sortOrder.property" [matSortDirection]="pageLink.sortDirection()"
matSortDisableClear>
<ng-container matColumnDef="ts">
<mat-header-cell *matHeaderCellDef mat-sort-header> Created time</mat-header-cell>
<mat-cell *matCellDef="let attribute">
{{ attribute.ts | date:'yyyy-MM-dd HH:mm:ss'}}
</mat-cell>
</ng-container>
<ng-container matColumnDef="status">
<mat-header-cell *matHeaderCellDef mat-sort-header> Status</mat-header-cell>
<mat-cell *matCellDef="let attribute">
<span [ngClass]="statusClass(attribute.status )">{{ attribute.status }}</span>
</mat-cell>
</ng-container>
<ng-container matColumnDef="message">
<mat-header-cell *matHeaderCellDef mat-sort-header style="width: 70%">Message</mat-header-cell>
<mat-cell *matCellDef="let attribute" [ngClass]="statusClassMsg(attribute.status )">
{{ attribute.message }}
</mat-cell>
</ng-container>
<mat-header-row [ngClass]="{'mat-row-select': true}"
*matHeaderRowDef="displayedColumns; sticky: true"></mat-header-row>
<mat-row [ngClass]="{'mat-row-select': true}"
*matRowDef="let attribute; columns: displayedColumns;"></mat-row>
<!-- <ng-container matColumnDef="lastUpdateTs">-->
<!-- <mat-header-cell *matHeaderCellDef mat-sort-header style="min-width: 150px; max-width: 150px; width: 150px;"> {{ 'attribute.last-update-time' | translate }} </mat-header-cell>-->
<!-- <mat-cell *matCellDef="let attribute">-->
<!-- {{ attribute.lastUpdateTs | date:'yyyy-MM-dd HH:mm:ss' }}-->
<!-- </mat-cell>-->
<!-- </ng-container>-->
</table>
<span [fxShow]="dataSource.data.length === 0"
fxLayoutAlign="center center"
class="no-data-found">{{'attribute.no-telemetry-text' | translate}}</span>
<mat-divider></mat-divider>

55
ui-ngx/src/app/shared/components/device/gateway-logs.component.scss

@ -0,0 +1,55 @@
/**
* Copyright © 2016-2023 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;
overflow-x: auto;
padding: 0;
.status {
border-radius: 20px;
font-weight: 500;
padding: 5px 15px;
}
.status-debug {
color: green;
background: rgba(0, 128, 0, 0.1);
}
.status-warning {
color: orange;
background: rgba(255, 165, 0, 0.1);
}
.status-error {
color: red;
background: rgba(255, 0, 0, 0.1);
}
.status-info {
color: black;
background: rgba(0, 0, 0, 0.1);
}
.msg-status-exception {
color: red;
}
}

234
ui-ngx/src/app/shared/components/device/gateway-logs.component.ts

@ -0,0 +1,234 @@
///
/// Copyright © 2016-2023 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 { AfterViewInit, Component, ElementRef, Input, ViewChild } from '@angular/core';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { Router } from '@angular/router';
import { FormBuilder, FormGroup } from '@angular/forms';
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 { AttributeData, DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { PageComponent } from '@shared/components/page.component';
import { PageLink } from '@shared/models/page/page-link';
import { AttributeDatasource } from "@home/models/datasource/attribute-datasource";
import { Direction, SortOrder } from "@shared/models/page/sort-order";
import { MatSort } from '@angular/material/sort';
import { MatTableDataSource } from '@angular/material/table';
import { GatewayLogLevel } from '@shared/components/device/gateway-configuration.component';
import { DialogService } from '@app/core/services/dialog.service';
import { WidgetContext } from '@home/models/widget-component.models';
export interface GatewayConnector {
name: string;
type: string;
configuration?: string;
configurationJson: string;
log_level: string;
key?: string;
}
export interface LogLink {
name: string;
key: string;
filterFn?: Function;
}
@Component({
selector: 'tb-gateway-logs',
templateUrl: './gateway-logs.component.html',
styleUrls: ['./gateway-logs.component.scss']
})
export class GatewayLogsComponent extends PageComponent implements AfterViewInit {
pageLink: PageLink;
attributeDataSource: AttributeDatasource;
dataSource: MatTableDataSource<any>
displayedColumns = ['ts', 'status', 'message'];
@Input()
ctx: WidgetContext;
@Input()
dialogRef: MatDialogRef<any>;
@ViewChild('searchInput') searchInputField: ElementRef;
@ViewChild(MatSort) sort: MatSort;
connectorForm: FormGroup;
viewsInited = false;
textSearchMode: boolean;
activeConnectors: Array<string>;
inactiveConnectors: Array<string>;
InitialActiveConnectors: Array<string>;
gatewayLogLevel = Object.values(GatewayLogLevel);
logLinks: Array<LogLink>;
initialConnector: GatewayConnector;
activeLink: LogLink;
gatewayLogLinks: Array<LogLink> = [
{
name: "General",
key: "LOGS"
}, {
name: "Service",
key: "SERVICE_LOGS"
},
{
name: "Connection",
key: "CONNECTION_LOGS"
}, {
name: "Storage",
key: "STORAGE_LOGS"
},
{
key: 'EXTENSIONS_LOGS',
name: "Extension"
}]
constructor(protected router: Router,
protected store: Store<AppState>,
protected fb: FormBuilder,
protected translate: TranslateService,
protected attributeService: AttributeService,
protected deviceService: DeviceService,
protected dialogService: DialogService,
public dialog: MatDialog) {
super(store);
const sortOrder: SortOrder = {property: 'ts', direction: Direction.DESC};
this.pageLink = new PageLink(Number.POSITIVE_INFINITY, 0, null, sortOrder);
this.dataSource = new MatTableDataSource<AttributeData>([]);
}
ngAfterViewInit() {
this.dataSource.sort = this.sort;
this.ctx.defaultSubscription.onTimewindowChangeFunction = timewindow => {
this.ctx.defaultSubscription.options.timeWindowConfig = timewindow;
this.ctx.defaultSubscription.updateDataSubscriptions();
return timewindow;
}
if (this.ctx.settings.isConnectorLog && this.ctx.settings.connectorLogState) {
const connector = this.ctx.stateController.getStateParams()[this.ctx.settings.connectorLogState];
this.logLinks = [{
key: `${connector.key}_LOGS`,
name: "Connector",
filterFn: (attrData)=>{
return !attrData.message.includes(`_converter.py`)
}
},{
key: `${connector.key}_LOGS`,
name: "Converter",
filterFn: (attrData)=>{
return attrData.message.includes(`_converter.py`)
}
}]
} else {
this.logLinks = this.gatewayLogLinks;
}
this.activeLink = this.logLinks[0];
this.changeSubscription();
}
updateData(sort?) {
if (this.ctx.defaultSubscription.data.length) {
let attrData = this.ctx.defaultSubscription.data[0].data.map(data => {
let result = {
ts: data[0],
key: this.activeLink.key,
message: /\[(.*)/.exec(data[1])[0],
status: 'INVALID LOG FORMAT'
};
try {
result.status= data[1].match(/\|(\w+)\|/)[1];
} catch (e) {
result.status = 'INVALID LOG FORMAT'
}
return result;
});
if (this.activeLink.filterFn) {
attrData = attrData.filter(data => this.activeLink.filterFn(data));
}
this.dataSource.data = attrData;
if (sort) {
this.dataSource.sortData(this.dataSource.data, this.sort);
}
}
}
onTabChanged(link) {
this.activeLink = link;
this.changeSubscription();
}
statusClass(status) {
switch (status) {
case GatewayLogLevel.debug:
return "status status-debug";
case GatewayLogLevel.warning:
return "status status-warning";
case GatewayLogLevel.error:
case "EXCEPTION":
return "status status-error";
case GatewayLogLevel.info:
default:
return "status status-info";
}
}
statusClassMsg(status) {
if (status === "EXCEPTION") {
return 'msg-status-exception';
}
}
changeSubscription() {
if (this.ctx.datasources[0].entity) {
this.ctx.defaultSubscription.options.datasources[0].dataKeys = [{
name: this.activeLink.key,
type: DataKeyType.timeseries,
settings: {}
}];
this.ctx.defaultSubscription.unsubscribe();
this.ctx.defaultSubscription.updateDataSubscriptions();
this.ctx.defaultSubscription.callbacks.onDataUpdated = () => {
this.updateData();
}
}
}
}

49
ui-ngx/src/app/shared/components/device/gateway-service-rpc.component.html

@ -0,0 +1,49 @@
<!--
Copyright © 2016-2023 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<mat-card fxLayout="row" class="command-form" fxLayoutGap="10px" [formGroup]="commandForm">
<mat-form-field class="mat-block tb-value-type">
<mat-label>{{'gateway.statistics.command' | translate}}</mat-label>
<mat-select formControlName="command" *ngIf="!isConnector">
<mat-option *ngFor="let command of RPCCommands" [value]="command">
{{command}}
</mat-option>
</mat-select>
<input matInput formControlName="command" *ngIf="isConnector"/>
</mat-form-field>
<mat-form-field class="mat-block tb-value-type" fxFlex *ngIf="!isConnector">
<mat-label>{{'gateway.statistics.timeout-ms' | translate}}</mat-label>
<input matInput formControlName="time" type="number"/>
</mat-form-field>
<mat-form-field class="mat-block tb-value-type" fxFlex *ngIf="isConnector">
<mat-label>{{'widget-config.datasource-parameters' | translate}}</mat-label>
<input matInput formControlName="params" type="JSON"/>
<mat-icon class="material-icons-outlined" aria-hidden="false" aria-label="help-icon"
matSuffix style="cursor:pointer;"
(click)="openEditJSONDialog($event)"
matTooltip="Edit params">edit
</mat-icon>
</mat-form-field>
<button mat-raised-button color="primary" (click)="sendCommand()"
[disabled]="commandForm.invalid">Send</button>
</mat-card>
<mat-card class="result-block" [formGroup]="commandForm" >
<span>Result</span>
<mat-divider></mat-divider>
<tb-json-content [contentType]="contentTypes.JSON" readonly="true" formControlName="result"></tb-json-content>
</mat-card>

42
ui-ngx/src/app/modules/home/components/gateway/gateway-list.component.scss → ui-ngx/src/app/shared/components/device/gateway-service-rpc.component.scss

@ -1,5 +1,5 @@
/**
* Copyright © 2016-2022 The Thingsboard Authors
* Copyright © 2016-2023 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.
@ -16,25 +16,33 @@
:host {
width: 100%;
height: 100%;
display: block;
.tb-table-widget {
.table-container {
position: relative;
overflow-x: auto;
display: flex;
flex-direction: column;
padding: 0;
.command-form {
width: 100%;
flex-wrap: nowrap;
padding: 0 15px;
margin-bottom: 5px;
&>button {
margin-top: 10px;
}
.mat-table {
.mat-row {
&.invisible {
visibility: hidden;
}
}
::ng-deep.mat-mdc-form-field-icon-suffix {
z-index: 100;
}
span.no-data-found {
position: absolute;
top: 60px;
bottom: 0;
left: 0;
right: 0;
}
.result-block {
padding: 0 15px;
&>span {
font-weight: 600;
}
}
}

128
ui-ngx/src/app/shared/components/device/gateway-service-rpc.component.ts

@ -0,0 +1,128 @@
///
/// Copyright © 2016-2023 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 { AfterViewInit, Component, Input } from '@angular/core';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { Router } from '@angular/router';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
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 { PageComponent } from "@shared/components/page.component";
import { DialogService } from '@app/core/services/dialog.service';
import { WidgetContext } from '@home/models/widget-component.models';
import { ContentType } from '@shared/models/constants';
import {
JsonObjectEditDialogComponent,
JsonObjectEditDialogData
} from '@shared/components/dialog/json-object-edit-dialog.component';
@Component({
selector: 'tb-gateway-service-rpc',
templateUrl: './gateway-service-rpc.component.html',
styleUrls: ['./gateway-service-rpc.component.scss']
})
export class GatewayServiceRPCComponent extends PageComponent implements AfterViewInit {
@Input()
ctx: WidgetContext;
contentTypes = ContentType;
@Input()
dialogRef: MatDialogRef<any>;
commandForm: FormGroup;
isConnector: boolean;
connectorType: string;
RPCCommands: Array<string> = [
"Ping",
"Stats",
"Devices",
"Update",
"Version",
"Restart",
"Reboot"
]
constructor(protected router: Router,
protected store: Store<AppState>,
protected fb: FormBuilder,
protected translate: TranslateService,
protected attributeService: AttributeService,
protected deviceService: DeviceService,
protected dialogService: DialogService,
public dialog: MatDialog) {
super(store);
this.commandForm = this.fb.group({
command: [null,[Validators.required]],
time: [60, [Validators.required]],
params: ["{}", [Validators.required]],
result: [null]
})
}
ngAfterViewInit() {
this.isConnector = this.ctx.settings.isConnector;
if (!this.isConnector) {
this.commandForm.get('command').setValue(this.RPCCommands[0]);
} else {
this.connectorType = this.ctx.stateController.getStateParams().connector_rpc.value.type;
}
}
sendCommand() {
const formValues = this.commandForm.value;
const commandPrefix = this.isConnector ? `${this.connectorType}_` : 'gateway_';
this.ctx.controlApi.sendTwoWayCommand(commandPrefix+formValues.command.toLowerCase(), {},formValues.time).subscribe(resp=>{
this.commandForm.get('result').setValue(JSON.stringify(resp));
},error => {
console.log(error);
this.commandForm.get('result').setValue(JSON.stringify(error.error));
})
}
openEditJSONDialog($event: Event) {
if ($event) {
$event.stopPropagation();
}
this.dialog.open<JsonObjectEditDialogComponent, JsonObjectEditDialogData, object>(JsonObjectEditDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
jsonValue: JSON.parse(this.commandForm.get('params').value)
}
}).afterClosed().subscribe(
(res) => {
if (res) {
this.commandForm.get('params').setValue(JSON.stringify(res));
}
}
);
}
}

81
ui-ngx/src/app/shared/components/device/gateway-statistics.component.html

@ -0,0 +1,81 @@
<!--
Copyright © 2016-2023 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 class="statistics-container" fxLayout="row">
<mat-card [formGroup]="statisticForm" *ngIf="!general">
<mat-form-field class="mat-block">
<mat-label>{{'gateway.statistics.statistic' | translate}}</mat-label>
<mat-select formControlName="statisticKey">
<mat-option *ngFor="let key of statisticsKeys" [value]="key">
{{key}}
</mat-option>
<mat-option *ngFor="let command of commands" [value]="command.attributeOnGateway">
{{command.attributeOnGateway}}
</mat-option>
</mat-select>
</mat-form-field>
<!-- <mat-form-field class="mat-block" *ngIf="commandObj">-->
<!-- <mat-label>Timeout</mat-label>-->
<!-- <input matInput [value]="commandObj.timeout" disabled>-->
<!-- </mat-form-field>-->
<mat-form-field class="mat-block" *ngIf="commandObj">
<mat-label>{{'gateway.statistics.command' | translate}}</mat-label>
<input matInput [value]="commandObj.command" disabled>
</mat-form-field>
</mat-card>
<div class="chart-box" fxLayout="column">
<div class="chart-container" #statisticChart [fxShow]="isNumericData">
</div>
<table [fxShow]="!isNumericData" mat-table [dataSource]="dataSource"
matSort [matSortActive]="pageLink.sortOrder.property" [matSortDirection]="pageLink.sortDirection()"
matSortDisableClear>
<ng-container matColumnDef="0">
<mat-header-cell *matHeaderCellDef mat-sort-header>Timestamp</mat-header-cell>
<mat-cell *matCellDef="let row; let rowIndex = index">
{{row[0]| date:'yyyy-MM-dd HH:mm:ss' }}
</mat-cell>
</ng-container>
<ng-container matColumnDef="1">
<mat-header-cell *matHeaderCellDef mat-sort-header style="width: 70%">Message</mat-header-cell>
<mat-cell *matCellDef="let row">
{{ row[1] }}
</mat-cell>
</ng-container>
<mat-header-row [ngClass]="{'mat-row-select': true}"
*matHeaderRowDef="displayedColumns; sticky: true"></mat-header-row>
<mat-row [ngClass]="{'mat-row-select': true}"
*matRowDef="let row; columns: displayedColumns;"></mat-row>
</table>
<span [fxShow]="dataSource.data.length === 0 && !isNumericData"
fxLayoutAlign="center center"
class="no-data-found">{{'attribute.no-telemetry-text' | translate}}</span>
<div fxFlex class="legend" fxLayout="row" fxLayoutAlign="center center" [fxShow]="isNumericData">
<div class="legend-keys" *ngFor="let legendKey of legendData?.keys" fxLayout="row"
fxLayoutAlign="center center">
<span class="legend-line" [ngStyle]="{backgroundColor: legendKey.dataKey.color}"></span>
<div class="legend-label"
(click)="onLegendKeyHiddenChange(legendKey.dataIndex)"
[ngClass]="{ 'hidden-label': legendData.keys[legendKey.dataIndex].dataKey.hidden }"
[innerHTML]="legendKey.dataKey.label">
</div>
</div>
</div>
</div>
</div>

78
ui-ngx/src/app/shared/components/device/gateway-statistics.component.scss

@ -0,0 +1,78 @@
/**
* Copyright © 2016-2023 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%;
padding: 0;
.statistics-container {
height: 100%;
mat-card {
width: 40%;
margin-right: 35px;
padding: 15px;
}
.chart-box, .chart-container {
height: 100%;
flex-grow: 1;
}
.chart-box {
overflow: auto;
}
& > * {
height: 100%;
}
}
.legend {
flex-wrap: wrap;
width: 100%;
padding-top: 8px;
padding-bottom: 4px;
margin-top: 15px;
.legend-keys {
.legend-label {
padding: 2px 20px 2px 10px;
white-space: nowrap;
&.hidden-label {
text-decoration: line-through;
opacity: .6;
}
&:focus {
outline: none;
}
}
.legend-line {
display: inline-block;
width: 15px;
height: 3px;
text-align: left;
vertical-align: middle;
outline: none;
}
}
}
}

299
ui-ngx/src/app/shared/components/device/gateway-statistics.component.ts

@ -0,0 +1,299 @@
///
/// Copyright © 2016-2023 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 { AfterViewInit, ChangeDetectorRef, Component, ElementRef, Input, ViewChild } from '@angular/core';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { Router } from '@angular/router';
import { FormBuilder, FormGroup } from '@angular/forms';
import { MatDialog } 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 { AttributeData, AttributeScope } from '@shared/models/telemetry/telemetry.models';
import { PageComponent } from '@shared/components/page.component';
import { DialogService } from '@app/core/services/dialog.service';
import { WidgetContext } from '@home/models/widget-component.models';
import { TbFlot } from '@home/components/widget/lib/flot-widget';
import { ResizeObserver } from '@juggle/resize-observer';
import { IWidgetSubscription, SubscriptionInfo, WidgetSubscriptionOptions } from '@core/api/widget-api.models';
import { UtilsService } from '@core/services/utils.service';
import { DatasourceType, LegendConfig, LegendData, LegendPosition, widgetType } from '@shared/models/widget.models';
import { EntityType } from '@shared/models/entity-type.models';
import { EntityId } from '@shared/models/id/entity-id';
import { BaseData } from '@shared/models/base-data';
import { PageLink } from '@shared/models/page/page-link';
import { Direction, SortOrder } from '@shared/models/page/sort-order';
import { MatTableDataSource } from '@angular/material/table';
import { MatSort } from '@angular/material/sort';
import { isNumber } from '@core/utils';
@Component({
selector: 'tb-gateway-statistics',
templateUrl: './gateway-statistics.component.html',
styleUrls: ['./gateway-statistics.component.scss']
})
export class GatewayStatisticsComponent extends PageComponent implements AfterViewInit {
@ViewChild(MatSort) sort: MatSort;
@ViewChild('statisticChart') statisticChart: ElementRef;
@Input()
ctx: WidgetContext;
@Input()
public general: boolean;
public isNumericData: boolean = true;
public chartInited: boolean;
private flot: TbFlot;
private flotCtx;
public statisticForm: FormGroup;
public statisticsKeys = [];
public commands = [];
public commandObj: any;
public dataSource: MatTableDataSource<any>;
public pageLink: PageLink;
private resize$: ResizeObserver;
private subscription: IWidgetSubscription;
private subscriptionInfo: SubscriptionInfo [];
public legendData: LegendData;
public displayedColumns: Array<string>;
private subscriptionOptions: WidgetSubscriptionOptions = {
callbacks: {
onDataUpdated: () => this.ctx.ngZone.run(() => {
this.onDataUpdated();
}),
onDataUpdateError: (subscription, e) => this.ctx.ngZone.run(() => {
this.onDataUpdateError(e);
})
},
useDashboardTimewindow: false,
legendConfig: {
position: LegendPosition.bottom
} as LegendConfig
};
constructor(protected router: Router,
protected store: Store<AppState>,
protected fb: FormBuilder,
protected translate: TranslateService,
protected attributeService: AttributeService,
protected deviceService: DeviceService,
protected dialogService: DialogService,
private cd: ChangeDetectorRef,
private utils: UtilsService,
public dialog: MatDialog) {
super(store);
const sortOrder: SortOrder = {property: '0', direction: Direction.DESC};
this.pageLink = new PageLink(Number.POSITIVE_INFINITY, 0, null, sortOrder);
this.displayedColumns = ['0', '1'];
this.dataSource = new MatTableDataSource<any>([]);
this.statisticForm = this.fb.group({
statisticKey: [null, []]
})
this.statisticForm.get('statisticKey').valueChanges.subscribe(value => {
this.commandObj = null;
if (this.commands.length) {
this.commandObj = this.commands.find(command => command.attributeOnGateway === value);
}
if (this.subscriptionInfo) this.createChartsSubscription(this.ctx.defaultSubscription.datasources[0].entity, value);
})
}
ngAfterViewInit() {
this.dataSource.sort = this.sort;
this.sort.sortChange.subscribe(_=>{
this.sortData();
})
this.init();
if (this.ctx.defaultSubscription.datasources.length) {
const gateway = this.ctx.defaultSubscription.datasources[0].entity;
if (!this.general) {
this.attributeService.getEntityAttributes(gateway.id, AttributeScope.SHARED_SCOPE, ["general_configuration"]).subscribe((resp: AttributeData[]) => {
if (resp && resp.length) {
this.commands = resp[0].value.statistics.commands;
if (!this.statisticForm.get('statisticKey').value) {
this.statisticForm.get('statisticKey').setValue(this.commands[0].attributeOnGateway);
this.createChartsSubscription(gateway, this.commands[0].attributeOnGateway);
}
}
})
} else {
let connectorsTs;
this.attributeService.getEntityTimeseriesLatest(gateway.id).subscribe(
data => {
connectorsTs = Object.keys(data)
.filter(el => el.includes(
'ConnectorEventsProduced'
) || el.includes(
'ConnectorEventsSent'))
this.createGeneralChartsSubscription(gateway, connectorsTs);
})
}
}
}
public sortData () {
this.dataSource.sortData(this.dataSource.data, this.sort);
}
public onLegendKeyHiddenChange(index: number) {
this.legendData.keys[index].dataKey.hidden = !this.legendData.keys[index].dataKey.hidden;
this.subscription.updateDataVisibility(index);
}
private createChartsSubscription(gateway: BaseData<EntityId>, attr: string) {
let subscriptionInfo = [{
type: DatasourceType.entity,
entityType: EntityType.DEVICE,
entityId: gateway.id.id,
entityName: gateway.name,
timeseries: []
}];
subscriptionInfo[0].timeseries = [{name: attr, label: attr}];
this.subscriptionInfo = subscriptionInfo;
this.changeSubscription(subscriptionInfo);
}
private createGeneralChartsSubscription(gateway: BaseData<EntityId>, attrData: [string]) {
let subscriptionInfo = [{
type: DatasourceType.entity,
entityType: EntityType.DEVICE,
entityId: gateway.id.id,
entityName: gateway.name,
timeseries: []
}];
subscriptionInfo[0].timeseries = [];
if (attrData && attrData.length) {
attrData.forEach(attr => {
subscriptionInfo[0].timeseries.push({name: attr, label: attr})
})
}
this.ctx.defaultSubscription.datasources[0].dataKeys.forEach(dataKey => {
subscriptionInfo[0].timeseries.push({name: dataKey.name, label: dataKey.label})
})
this.subscriptionInfo = subscriptionInfo;
this.changeSubscription(subscriptionInfo);
}
init = () => {
this.flotCtx = {
$scope: this.ctx.$scope,
$injector: this.ctx.$injector,
utils: this.ctx.utils,
isMobile: this.ctx.isMobile,
isEdit: this.ctx.isEdit,
subscriptionApi: this.ctx.subscriptionApi,
detectChanges: this.ctx.detectChanges,
settings: this.ctx.settings
};
}
updateChart = () => {
if (this.flot && this.ctx.defaultSubscription.data.length) {
this.flot.update();
}
}
resize = () => {
if (this.flot) {
this.flot.resize();
}
}
private reset() {
if (this.resize$) {
this.resize$.disconnect();
}
if (this.subscription) {
this.subscription.unsubscribe();
}
if (this.flot) {
this.flot.destroy();
}
}
private onDataUpdateError(e: any) {
const exceptionData = this.utils.parseException(e);
let errorText = exceptionData.name;
if (exceptionData.message) {
errorText += ': ' + exceptionData.message;
}
console.error(errorText);
}
private onDataUpdated() {
this.isDataOnlyNumbers();
if (this.isNumericData) {
if (this.chartInited) {
if (this.flot) {
this.flot.update();
}
} else {
this.initChart();
}
}
}
private initChart() {
this.chartInited = true;
this.flotCtx.$container = $(this.statisticChart.nativeElement);
this.resize$.observe(this.statisticChart.nativeElement);
this.flot = new TbFlot(this.flotCtx as WidgetContext, "line");
this.flot.update();
}
private isDataOnlyNumbers() {
if (this.general) {
this.isNumericData = true;
return;
}
this.dataSource.data = this.subscription.data.length ? this.subscription.data[0].data : [];
this.isNumericData = this.dataSource.data.every(data => isNumber(data[1]));
}
changeSubscription(subscriptionInfo: SubscriptionInfo[]) {
if (this.subscription) {
this.reset();
}
if (this.ctx.datasources[0].entity) {
this.ctx.subscriptionApi.createSubscriptionFromInfo(widgetType.timeseries, subscriptionInfo, this.subscriptionOptions, false, true).subscribe(subscription => {
this.subscription = subscription;
this.isDataOnlyNumbers();
this.legendData = this.subscription.legendData;
this.flotCtx.defaultSubscription = subscription;
this.resize$ = new ResizeObserver(() => {
this.resize();
});
this.ctx.detectChanges();
if (this.isNumericData) {
this.initChart();
}
})
}
}
}

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

@ -0,0 +1,57 @@
<!--
Copyright © 2016-2023 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="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}} <b>{{gatewayName}}</b>" > </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>

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

@ -0,0 +1,61 @@
///
/// Copyright © 2016-2023 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 { 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);
}
}

13
ui-ngx/src/app/shared/components/file-input.component.html

@ -16,7 +16,12 @@
-->
<div class="tb-container">
<label class="tb-title" [ngClass]="{'tb-required': !disabled && required}">{{ label }}</label>
<label class="tb-title" [ngClass]="{'tb-required': !disabled && required}">{{ label }}
<mat-icon *ngIf="hint" class="material-icons-outlined pe"
matSuffix style="cursor:pointer;"
matTooltip="{{hint}}">info_outlined
</mat-icon>
</label>
<ng-container #flow="flow"
[flowConfig]="{allowDuplicateUploads: true}">
<div class="tb-file-select-container">
@ -37,9 +42,11 @@
<mat-icon>cloud_upload</mat-icon>
<span>{{ dropLabel }}</span>
<button type="button" mat-button color="primary" class="browse-file">
<label for="{{inputId}}">{{ (multipleFile ? 'file-input.browse-files' : 'file-input.browse-file') | translate}}</label>
<label
for="{{inputId}}">{{ (multipleFile ? 'file-input.browse-files' : 'file-input.browse-file') | translate}}</label>
</button>
<input class="file-input" flowButton #flowInput type="file" [flow]="flow.flowJs" [flowAttributes]="{accept: accept}" id="{{inputId}}">
<input class="file-input" flowButton #flowInput type="file" [flow]="flow.flowJs"
[flowAttributes]="{accept: accept}" id="{{inputId}}">
</div>
</div>
</div>

4
ui-ngx/src/app/shared/components/file-input.component.scss

@ -93,4 +93,8 @@ $previewSize: 100px !default;
padding: 0 16px;
}
}
.pe {
pointer-events: auto;
}
}

3
ui-ngx/src/app/shared/components/file-input.component.ts

@ -54,6 +54,9 @@ export class FileInputComponent extends PageComponent implements AfterViewInit,
@Input()
label: string;
@Input()
hint: string;
@Input()
accept = '*/*';

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

@ -176,10 +176,16 @@ 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';
import { GatewayLogsComponent } from '@shared/components/device/gateway-logs.component';
import { GatewayStatisticsComponent } from '@shared/components/device/gateway-statistics.component';
import { GatewayServiceRPCComponent } from '@shared/components/device/gateway-service-rpc.component';
export const MarkedOptionsFactory = (markedOptionsService: MarkedOptionsService) => markedOptionsService;
export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) {
return markedOptionsService;
}
@NgModule({
providers: [
@ -241,6 +247,13 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
HelpComponent,
HelpMarkdownComponent,
HelpPopupComponent,
DeviceGatewayCommandComponent,
GatewayConfigurationComponent,
GatewayConnectorComponent,
GatewayLogsComponent,
GatewayStatisticsComponent,
GatewayServiceRPCComponent,
GatewayRemoteConfigurationDialogComponent,
TbCheckboxComponent,
TbSnackBarComponent,
TbErrorComponent,
@ -407,6 +420,13 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
HelpComponent,
HelpMarkdownComponent,
HelpPopupComponent,
DeviceGatewayCommandComponent,
GatewayConfigurationComponent,
GatewayConnectorComponent,
GatewayLogsComponent,
GatewayStatisticsComponent,
GatewayServiceRPCComponent,
GatewayRemoteConfigurationDialogComponent,
TbCheckboxComponent,
TbErrorComponent,
TbCheatSheetComponent,

215
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,168 @@
},
"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 1",
"saving-period-required": "Log saving period required",
"backup-count": "Backup count",
"backup-count-min": "Backup count can not be less then 1",
"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"
"username-password": "Username and Password",
"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": {
"statistic": "Statistic",
"statistics": "Statistics",
"commands": "Commands",
"send-period": "Statistic send period (in sec)",
"send-period-required": "Statistic send period is required",
"send-period-min": "Statistic send period can not be less then 1",
"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 1",
"check-connectors-configuration-pattern": "Check connectors configuration is not valid",
"add": "Add command",
"timeout": "Timeout",
"timeout-ms": "Timeout (in ms)",
"timeout-required": "Timeout is required",
"timeout-min": "Timeout can not be less then 1",
"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 +2641,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 +2660,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,11 +2677,60 @@
"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 1",
"tls-path-private-key": "Path to private key on gateway",
"toggle-fullscreen": "Toggle fullscreen",
"transformer-json-config": "Configuration JSON*",
"update-config": "Add/update configuration JSON",
"windows": "Windows"
"windows": "Windows",
"hints": {
"remote-configuration": "Enables remote configuration and management of the gateway",
"remote-shell": "Enables remote control of the operating system with the gateway from the Remote Shell widget",
"host": "Hostname or ip address of ThingsBoard server",
"port": "Port of MQTT service on ThingsBoard server",
"token": "Access token for the gateway from ThingsBoard server",
"client-id": "MQTT client id for the gateway form ThingsBoard server",
"username": "MQTT username for the gateway form ThingsBoard server",
"password": "MQTT password for the gateway form ThingsBoard server",
"ca-cert": "Path to CA certificate file",
"cert": "Path to certificate file",
"private-key": "Path to private key file",
"date-form": "Date format of log message",
"log-format": "Log message format",
"remote-log": "Enables remote logging and logs reading from the gateway",
"backup-count": "If Backup count is > 0, when a rollover is done, no more than Backup count files are kept - the oldest ones are deleted",
"storage": "Provides configuration for saving incoming data before it is sent to the ThingsBoard platform",
"file": "Received data saving to the hard drive",
"memory": "Received data saving to the RAM memory",
"sqlite": "Received data saving to the .db file",
"data-folder": "Path to folder, that will contains data (Relative or Absolute)",
"max-file-count": "Maximum count of file that will be saved",
"max-read-count": "Count of messages to get from storage and send to ThingsBoard",
"max-records": "Maximum count of records that will be stored in one file",
"read-record-count": "Count of messages to get from storage and send to ThingsBoard",
"max-records-count": "Maximum count of data in storage before send to ThingsBoard",
"ttl-check-hour": "How often will Gateway check data for obsolescence",
"ttl-messages-day": "Maximum days that storage will save data",
"commands": "Commands for collecting additional statistic",
"attribute": "Gateway client attribute name",
"timeout": "Timeout for command executing",
"command": "The result of the command will be used as the value of the client attribute (\"/bin/sh -c ipconfig getifaddr en0\")",
"check-device-activity": "Enables monitor the activity of each connected device",
"inactivity-timeout": "Inactivity device time after whose the gateway will disconnect device",
"inactivity-period": "Periodicity of device activity check",
"minimal-pack-delay": "Delay between sending packets (Decreasing this setting results in increased CPU usage)",
"qos": "Quality of Service in MQTT messaging (0 - at most once, 1 - at least once)"
}
},
"grid": {
"delete-item-title": "Are you sure you want to delete this item?",
@ -3065,6 +3252,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.",
@ -5087,6 +5288,11 @@
"font-weight-lighter": "Lighter",
"color": "Color",
"shadow-color": "Shadow color"
},
"action-buttons": {
"button-type": "Button type",
"button-class": "Button class",
"alignment": "Buttons alignment"
}
},
"icon": {
@ -5113,7 +5319,8 @@
"node-selected": "On node selected",
"element-click": "On HTML element click",
"pie-slice-click": "On slice click",
"row-double-click": "On row double click"
"row-double-click": "On row double click",
"action-button-click": "Action button click"
}
},
"paginator" : {

Loading…
Cancel
Save