Browse Source

mark2 gateway dashboard

pull/8337/head
Maksym Dudnik 3 years ago
parent
commit
42e196906d
  1. 19
      application/src/main/data/json/system/widget_bundles/cards.json
  2. 136
      application/src/main/data/json/system/widget_bundles/gateway_widgets.json
  3. 6
      ui-ngx/src/app/modules/common/modules-map.ts
  4. 30
      ui-ngx/src/app/modules/home/components/widget/lib/action-buttons.component.html
  5. 33
      ui-ngx/src/app/modules/home/components/widget/lib/action-buttons.component.scss
  6. 61
      ui-ngx/src/app/modules/home/components/widget/lib/action-buttons.component.ts
  7. 43
      ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/action-buttons-widget-settings.component.html
  8. 62
      ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/action-buttons-widget-settings.component.ts
  9. 26
      ui-ngx/src/app/modules/home/components/widget/lib/settings/gateway/gateway-logs-settings.component.html
  10. 54
      ui-ngx/src/app/modules/home/components/widget/lib/settings/gateway/gateway-logs-settings.component.ts
  11. 22
      ui-ngx/src/app/modules/home/components/widget/lib/settings/gateway/gateway-service-rpc-settings.component.html
  12. 52
      ui-ngx/src/app/modules/home/components/widget/lib/settings/gateway/gateway-service-rpc-settings.component.ts
  13. 25
      ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts
  14. 7
      ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts
  15. 191
      ui-ngx/src/app/shared/components/device/gateway-configuration.component.html
  16. 41
      ui-ngx/src/app/shared/components/device/gateway-configuration.component.scss
  17. 257
      ui-ngx/src/app/shared/components/device/gateway-configuration.component.ts
  18. 246
      ui-ngx/src/app/shared/components/device/gateway-connectors.component.html
  19. 33
      ui-ngx/src/app/shared/components/device/gateway-connectors.component.scss
  20. 384
      ui-ngx/src/app/shared/components/device/gateway-connectors.component.ts
  21. 57
      ui-ngx/src/app/shared/components/device/gateway-logs.component.html
  22. 55
      ui-ngx/src/app/shared/components/device/gateway-logs.component.scss
  23. 250
      ui-ngx/src/app/shared/components/device/gateway-logs.component.ts
  24. 49
      ui-ngx/src/app/shared/components/device/gateway-service-rpc.component.html
  25. 48
      ui-ngx/src/app/shared/components/device/gateway-service-rpc.component.scss
  26. 125
      ui-ngx/src/app/shared/components/device/gateway-service-rpc.component.ts
  27. 43
      ui-ngx/src/app/shared/components/device/gateway-statistics.component.html
  28. 41
      ui-ngx/src/app/shared/components/device/gateway-statistics.component.scss
  29. 150
      ui-ngx/src/app/shared/components/device/gateway-statistics.component.ts
  30. 2
      ui-ngx/src/app/shared/components/dialog/gateway-remote-configuration-dialog.html
  31. 13
      ui-ngx/src/app/shared/components/file-input.component.html
  32. 4
      ui-ngx/src/app/shared/components/file-input.component.scss
  33. 3
      ui-ngx/src/app/shared/components/file-input.component.ts
  34. 9
      ui-ngx/src/app/shared/shared.module.ts
  35. 51
      ui-ngx/src/assets/locale/locale.constant-en_US.json

19
application/src/main/data/json/system/widget_bundles/cards.json

File diff suppressed because one or more lines are too long

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

File diff suppressed because one or more lines are too long

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

@ -300,6 +300,9 @@ import * as DeviceGatewayCommandComponent from '@shared/components/device/device
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';
@ -406,6 +409,9 @@ class ModulesMap implements IModulesMap {
'@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,

30
ui-ngx/src/app/modules/home/components/widget/lib/action-buttons.component.html

@ -0,0 +1,30 @@
<!--
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="action-buttons-container" [style]="{'align-content': settings.alignment}">
<button mat-button [color]="settings.buttonsClass"
*ngFor="let action of ctx.actionsApi.getActionDescriptors('actionButtonClick')"
[ngClass]="{
'mat-mdc-button mat-mdc-raised-button': settings.buttonsType === 'raised',
'mdc-button--outlined mat-mdc-outlined-button': settings.buttonsType === 'stroked',
'mat-mdc-unelevated-button': settings.buttonsType === 'flat',
'mat-mdc-menu-item mdc-list-item': settings.buttonsType === 'menu'
}"
(click)="actionButtonClick($event, action)"
>{{action.displayName}}
</button>
</div>

33
ui-ngx/src/app/modules/home/components/widget/lib/action-buttons.component.scss

@ -0,0 +1,33 @@
/**
* 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%;
.action-buttons-container {
display: flex;
flex-wrap: wrap;
flex-direction: row;
height: 100%;
width: 100%;
button {
flex-grow: 1;
margin: 10px;
min-width: 150px;
}
}
}

61
ui-ngx/src/app/modules/home/components/widget/lib/action-buttons.component.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, Input, OnInit } from '@angular/core';
import { WidgetContext } from '@home/models/widget-component.models';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { WidgetActionDescriptor } from '@shared/models/widget.models';
import { PageComponent } from '@shared/components/page.component';
import { ThemePalette } from '@angular/material/core';
interface ActionButtonsWidgetSettings {
buttonsType: string,
buttonsClass: ThemePalette,
alignment: string
}
@Component({
selector: 'tb-action-buttons-widget',
templateUrl: './action-buttons.component.html',
styleUrls: ['./action-buttons.component.scss']
})
export class ActionButtonsComponent implements OnInit {
@Input()
ctx: WidgetContext;
settings: ActionButtonsWidgetSettings;
constructor() {
}
ngOnInit(): void {
this.settings = this.ctx.settings;
}
actionButtonClick($event: MouseEvent, actionDescriptor: WidgetActionDescriptor) {
let entityId, entityName, entityLabel;
if (this.ctx.datasources) {
entityId = this.ctx.datasources[0].entity.id;
entityName = this.ctx.datasources[0].entityName;
entityLabel = this.ctx.datasources[0].entityLabel;
}
this.ctx.actionsApi.handleWidgetAction($event, actionDescriptor, entityId, entityName, null, entityLabel);
}
}

43
ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/action-buttons-widget-settings.component.html

@ -0,0 +1,43 @@
<!--
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]="ActionButtonsSettingsForm" fxLayout="column">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.action-buttons.button-type</mat-label>
<mat-select required formControlName="buttonsType">
<mat-option *ngFor="let type of buttonTypes" [value]="type">
{{type}}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.action-buttons.button-class</mat-label>
<mat-select required formControlName="buttonsClass">
<mat-option *ngFor="let class of buttonClasses" [value]="class">
{{class}}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.action-buttons.alignment</mat-label>
<mat-select required formControlName="alignment">
<mat-option *ngFor="let type of alignment" [value]="type">
{{type}}
</mat-option>
</mat-select>
</mat-form-field>
</section>

62
ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/action-buttons-widget-settings.component.ts

@ -0,0 +1,62 @@
///
/// 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, Validators } from '@angular/forms';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
@Component({
selector: 'tb-action-buttons-widget-settings',
templateUrl: './action-buttons-widget-settings.component.html',
styleUrls: ['./../widget-settings.scss']
})
export class ActionButtonsWidgetSettingsComponent extends WidgetSettingsComponent {
ActionButtonsSettingsForm: UntypedFormGroup;
buttonTypes = ['basic', 'raised', 'stroked', 'flat'];
buttonClasses = ['basic', 'primary', 'accent', 'warn'];
alignment = ['center', 'start', 'end', 'normal', 'baseline', 'space-between', 'space-around', 'stretch'];
constructor(protected store: Store<AppState>,
private fb: UntypedFormBuilder) {
super(store);
}
protected settingsForm(): UntypedFormGroup {
return this.ActionButtonsSettingsForm;
}
protected defaultSettings(): WidgetSettings {
return {
buttonsType: 'basic',
buttonsClass: 'basic',
alignment: 'center'
};
}
protected onSettingsSet(settings: WidgetSettings) {
this.ActionButtonsSettingsForm = this.fb.group({
buttonsType: [settings.buttonsType, []],
buttonsClass: [settings.buttonsClass, []],
alignment: [settings.alignment, []]
});
}
}

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', []]
});
}
}

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

@ -0,0 +1,22 @@
<!--
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="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, []]
});
}
}

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

@ -259,6 +259,16 @@ import {
import {
TripAnimationPointSettingsComponent
} from '@home/components/widget/lib/settings/map/trip-animation-point-settings.component';
import {
ActionButtonsWidgetSettingsComponent
} from '@home/components/widget/lib/settings/cards/action-buttons-widget-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 +367,10 @@ import {
TripAnimationPointSettingsComponent,
MapWidgetSettingsComponent,
RouteMapWidgetSettingsComponent,
TripAnimationWidgetSettingsComponent
TripAnimationWidgetSettingsComponent,
ActionButtonsWidgetSettingsComponent,
GatewayLogsSettingsComponent,
GatewayServiceRPCSettingsComponent
],
imports: [
CommonModule,
@ -460,7 +473,10 @@ import {
TripAnimationPointSettingsComponent,
MapWidgetSettingsComponent,
RouteMapWidgetSettingsComponent,
TripAnimationWidgetSettingsComponent
TripAnimationWidgetSettingsComponent,
ActionButtonsWidgetSettingsComponent,
GatewayLogsSettingsComponent,
GatewayServiceRPCSettingsComponent
]
})
export class WidgetSettingsModule {
@ -527,5 +543,8 @@ 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-action-buttons-widget-settings': ActionButtonsWidgetSettingsComponent,
'tb-gateway-logs-settings': GatewayLogsSettingsComponent,
'tb-gateway-service-rpc-settings':GatewayServiceRPCSettingsComponent
};

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

@ -42,6 +42,7 @@ import { JsonInputWidgetComponent } from '@home/components/widget/lib/json-input
import { QrCodeWidgetComponent } from '@home/components/widget/lib/qrcode-widget.component';
import { MarkdownWidgetComponent } from '@home/components/widget/lib/markdown-widget.component';
import { SelectEntityDialogComponent } from '@home/components/widget/lib/maps/dialogs/select-entity-dialog.component';
import { ActionButtonsComponent } from '@home/components/widget/lib/action-buttons.component';
@NgModule({
declarations:
@ -64,7 +65,8 @@ import { SelectEntityDialogComponent } from '@home/components/widget/lib/maps/di
NavigationCardWidgetComponent,
QrCodeWidgetComponent,
MarkdownWidgetComponent,
SelectEntityDialogComponent
SelectEntityDialogComponent,
ActionButtonsComponent
],
imports: [
CommonModule,
@ -88,7 +90,8 @@ import { SelectEntityDialogComponent } from '@home/components/widget/lib/maps/di
NavigationCardsWidgetComponent,
NavigationCardWidgetComponent,
QrCodeWidgetComponent,
MarkdownWidgetComponent
MarkdownWidgetComponent,
ActionButtonsComponent
],
providers: [
CustomDialogService,

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

@ -19,7 +19,7 @@
<mat-toolbar color="primary">
<h2 translate>gateway.gateway-configuration</h2>
<span fxFlex></span>
<button mat-icon-button
<button mat-icon-button
type="button"
(click)="cancel()"
*ngIf="dialogRef">
@ -34,13 +34,25 @@
<div fxLayout="row wrap">
<mat-slide-toggle color="primary" fxFlex="100" formControlName="remoteConfiguration">
{{ 'gateway.remote-configuration' | translate }}
<mat-icon class="material-icons-outlined" aria-hidden="false" aria-label="help-icon"
matSuffix 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" aria-hidden="false" aria-label="help-icon"
matSuffix 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"
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>
@ -60,6 +72,10 @@
<mat-error *ngIf="gatewayConfigGroup.get('thingsboard.port').hasError('pattern')">
{{'gateway.thingsboard-port-pattern' | translate }}
</mat-error>
<mat-icon class="mat-form-field-infix pe" aria-hidden="false" aria-label="help-icon"
matSuffix style="cursor:pointer;"
matTooltip="{{'gateway.hints.port' | translate }}">info_outlined
</mat-icon>
</mat-form-field>
</div>
@ -82,6 +98,10 @@
<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" 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">
@ -90,6 +110,10 @@
<mat-error *ngIf="gatewayConfigGroup.get('thingsboard.security.clientId').hasError('required')">
{{'security.clientId-required' | translate}}
</mat-error>
<mat-icon class="mat-form-field-infix pe" 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">
@ -98,6 +122,10 @@
<mat-error *ngIf="gatewayConfigGroup.get('thingsboard.security.username').hasError('required')">
{{'security.username-required' | translate}}
</mat-error>
<mat-icon class="mat-form-field-infix pe" 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">
@ -106,18 +134,24 @@
<mat-error *ngIf="gatewayConfigGroup.get('thingsboard.security.password').hasError('required')">
{{'security.password-required' | translate}}
</mat-error>
<mat-icon class="mat-form-field-infix pe" 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-required' | translate}}"
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}}"
@ -127,6 +161,7 @@
</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}}"
@ -150,6 +185,10 @@
<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" 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>
@ -157,9 +196,14 @@
<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" aria-hidden="false" aria-label="help-icon"
matSuffix style="cursor:pointer;"
matTooltip="{{'gateway.hints.log-format' | translate }}">info_outlined
</mat-icon>
</mat-form-field>
<label fxFlex="100" class="logs-label" translate>gateway.logs.log-type</label>
<mat-button-toggle-group class="logs-toggle-group" #logging="matButtonToggleGroup" value="remote" style="margin-bottom: 15px">
<mat-button-toggle-group class="logs-toggle-group" #logging="matButtonToggleGroup" value="remote"
style="margin-bottom: 15px">
<mat-button-toggle value="remote"
aria-label="Remote logging"> {{'gateway.logs.remote' | translate}}</mat-button-toggle>
<mat-button-toggle value="local"
@ -168,6 +212,10 @@
<div fxFlex="100" fxLayout="column" formGroupName="remote" *ngIf="logging.value === 'remote'">
<mat-slide-toggle color="primary" formControlName="enabled">
{{ 'gateway.logs.remote-logs' | translate }}
<mat-icon class="mat-form-field-infix pe" 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>
@ -178,10 +226,10 @@
</div>
<div fxFlex="100" fxLayout="row wrap" formGroupName="local" *ngIf="logging.value === 'local'">
<mat-button-toggle-group class="security-toggle-group" [formControl]="logSelector" fxFlex="100"
name="logSelector">
<mat-button-toggle *ngFor="let logConfig of localLogsConfigs" [value]="logConfig"
class="first-capital">
{{ logConfig }}
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)">
@ -230,6 +278,10 @@
*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" 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>
</div>
@ -243,13 +295,20 @@
<mat-accordion>
<mat-expansion-panel expanded="true" disabled>
<mat-expansion-panel-header>
<mat-panel-title translate class="expansion-panel-header">gateway.storage</mat-panel-title>
<mat-panel-title translate class="expansion-panel-header">gateway.storage
<span class="tb-hint" matTooltip="{{'gateway.hints.storage' | translate}}">Hint about storage</span>
</mat-panel-title>
</mat-expansion-panel-header>
<div fxLayout="row wrap">
<mat-button-toggle-group class="security-toggle-group" formControlName="type" fxFlex="100">
<mat-button-toggle-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"
matTooltip="{{'gateway.hints.'+gatewayConfigGroup.get('storage.type').value | translate}}">
Hint about {{ gatewayConfigGroup.get('storage.type').value | translate }} storage</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">
@ -264,6 +323,10 @@
<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" 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">
@ -278,6 +341,10 @@
<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" 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">
@ -286,6 +353,10 @@
<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" 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">
@ -300,6 +371,10 @@
<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" 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">
@ -314,6 +389,10 @@
<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" 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">
@ -328,6 +407,10 @@
<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" 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">
@ -336,6 +419,10 @@
<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" 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">
@ -350,6 +437,10 @@
<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" 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">
@ -364,6 +455,10 @@
<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" 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>
@ -491,33 +586,20 @@
{{'gateway.statistics.send-period-pattern' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field fxFlex="calc(50%-15px)" class="mat-block tb-value-type">
<mat-label translate>gateway.statistics.check-connectors-configuration</mat-label>
<input matInput formControlName="checkConnectorsConfigurationInSeconds" type="number"/>
<mat-error
*ngIf="gatewayConfigGroup.get('thingsboard.statistics.checkConnectorsConfigurationInSeconds').hasError('required')">
{{'gateway.statistics.check-connectors-configuration-required' | translate }}
</mat-error>
<mat-error
*ngIf="gatewayConfigGroup.get('thingsboard.statistics.checkConnectorsConfigurationInSeconds').hasError('min')">
{{'gateway.statistics.check-connectors-configuration-min' | translate }}
</mat-error>
<mat-error
*ngIf="gatewayConfigGroup.get('thingsboard.statistics.checkConnectorsConfigurationInSeconds').hasError('pattern')">
{{'gateway.statistics.check-connectors-configuration-pattern' | translate }}
</mat-error>
</mat-form-field>
</div>
</mat-expansion-panel>
<mat-expansion-panel expanded="true" disabled>
<mat-expansion-panel-header>
<mat-panel-title translate class="expansion-panel-header">gateway.statistics.statistics</mat-panel-title>
<mat-panel-title class="expansion-panel-header">{{"gateway.statistics.commands" |translate}}
<span class="tb-hint"
matTooltip="{{'gateway.hints.commands' | translate}}"
>Hint about commands</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" style="margin-bottom: 15px" [formGroupName]="$index">
<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="attributeName"/>
@ -525,6 +607,10 @@
*ngIf="commandControl.get('attributeName').hasError('required')">
{{'gateway.statistics.attribute-name-required' | translate }}
</mat-error>
<mat-icon class="mat-form-field-infix pe" 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>
@ -541,6 +627,10 @@
*ngIf="commandControl.get('timeout').hasError('pattern')">
{{'gateway.statistics.timeout-pattern' | translate }}
</mat-error>
<mat-icon class="mat-form-field-infix pe" 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>
@ -549,6 +639,10 @@
*ngIf="commandControl.get('command').hasError('required')">
{{'gateway.statistics.command-required' | translate }}
</mat-error>
<mat-icon class="mat-form-field-infix pe" 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)"
@ -574,12 +668,15 @@
<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 expanded="true" disabled formGroupName="checkingDeviceActivity">
<mat-expansion-panel-header>
<mat-panel-title class="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;"
matTooltip="{{'gateway.hints.check-device-activity' | translate}}"
>Hint about checking activity</span>
</mat-panel-title>
</mat-expansion-panel-header>
<div fxLayout="row wrap">
@ -596,6 +693,10 @@
*ngIf="gatewayConfigGroup.get('thingsboard.checkingDeviceActivity.inactivityTimeoutSeconds').hasError('min')">
{{'gateway.inactivity-timeout-seconds-min' | translate }}
</mat-error>
<mat-icon class="mat-form-field-infix pe" 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"
@ -610,10 +711,14 @@
*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" 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 expanded="true" disabled>
<mat-expansion-panel-header>
<mat-panel-title translate class="expansion-panel-header">gateway.advanced</mat-panel-title>
</mat-expansion-panel-header>
@ -627,6 +732,10 @@
<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" 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>
@ -640,6 +749,26 @@
<mat-error *ngIf="gatewayConfigGroup.get('thingsboard.qos').hasError('max')">
{{ 'gateway.mqtt-qos-range' | translate}}
</mat-error>
<mat-icon class="mat-form-field-infix pe" 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"/>
<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>
@ -661,7 +790,7 @@
type="button"
[disabled]="gatewayConfigGroup.invalid || !gatewayConfigGroup.dirty"
cdkFocusInitial
(click)="saveConfig()">
(click)="saveConfig()">
{{ 'action.save' | translate }}
</button>

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

@ -18,6 +18,10 @@
height: 100%;
display: block;
.mat-icon {
color: rgba(0, 0, 0, .12);
}
.mat-toolbar {
background: transparent;
color: rgba(0, 0, 0, .87) !important;
@ -126,6 +130,12 @@
}
}
.statistics-block {
margin-bottom: 15px;
padding-left: 15px;
padding-top: 15px;
}
.first-capital {
text-transform: capitalize;
}
@ -147,5 +157,36 @@
z-index: -10;
border-radius: 5px;
}
::ng-deep.pe {
pointer-events: all;
}
::ng-deep.mat-mdc-form-field-icon-suffix {
z-index: 100;
}
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%;
}
}

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

@ -14,14 +14,13 @@
/// limitations under the License.
///
import { Component, Input, OnInit } from '@angular/core';
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 { base64toObj, base64toString, objToBase64, stringToBase64 } from '@core/utils';
import { AttributeService } from '@core/http/attribute.service';
import { AttributeScope } from '@shared/models/telemetry/telemetry.models';
import {
@ -29,6 +28,9 @@ import {
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',
@ -46,10 +48,10 @@ export enum GatewayLogLevel {
}
export enum LogSavingPeriod {
days = 'd',
hours = 'h',
minutes = 'm',
seconds = 's'
days = 'D',
hours = 'H',
minutes = 'M',
seconds = 'S'
}
export enum LocalLogsConfigs {
@ -61,6 +63,15 @@ export enum LocalLogsConfigs {
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'],
@ -107,12 +118,12 @@ export class GatewayConfigurationComponent implements OnInit {
logSavingPeriods = logSavingPeriodTranslations;
localLogsConfigLabels = localLogsConfigLabels;
securityTypes = securityTypesTranslationsMap;
gatewayLogLevel = Object.values(GatewayLogLevel);
localLogsConfigs = Object.values(LocalLogsConfigs);
@Input()
device: EntityId;
@ -127,21 +138,21 @@ export class GatewayConfigurationComponent implements OnInit {
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: ['thingsboard.cloud', [Validators.required]],
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(0), Validators.pattern(/^-?[0-9]+$/)]],
checkConnectorsConfigurationInSeconds: [60, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
handleDeviceRenaming: [false, []],
commands: this.fb.array([], [])
}),
maxPayloadSizeBytes: [1024, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
@ -257,6 +268,10 @@ export class GatewayConfigurationComponent implements OnInit {
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();
@ -286,25 +301,44 @@ export class GatewayConfigurationComponent implements OnInit {
}
fetchConfigAttribute(entityId: EntityId) {
this.attributeService.getEntityAttributes(entityId, AttributeScope.SHARED_SCOPE,
['configuration', 'RemoteLoggingLevel']).subscribe(attributes => {
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 configuration = attributes.find(attribute => attribute.key === 'configuration').value;
const remoteLoggingLevel = attributes.find(attribute => attribute.key === 'RemoteLoggingLevel').value;
if (configuration) {
const configObj = base64toObj(configuration).thingsboard;
configObj.logs = this.logsBase64ToObj(configObj.logs);
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 Object.values(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.markAsDirty();
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');
@ -312,41 +346,48 @@ export class GatewayConfigurationComponent implements OnInit {
enabled: remoteLoggingLevel !== GatewayLogLevel.none,
logLevel: remoteLoggingLevel
}, {emitEvent: false});
remoteLogsFormGroup.markAsDirty();
remoteLogsFormGroup.markAsPristine();
}
this.cd.detectChanges();
} else {
this.checkAndFetchCredentials({});
}
});
}
logsBase64ToObj(logs64: string) {
const logsString = base64toString(logs64).split('"').join('');
const logsObject = {};
logsString.split('logger_').forEach((split, index) => {
if (index > 0) {
const key = split.split(']')[0];
if (LocalLogsConfigs[key]) {
const logLevel = split.split('=')[1].replace('}}handlers', '');
logsObject[key] = {
logLevel
};
}
}
});
logsString.split('handler_').forEach((split, index) => {
if (index > 0) {
const key = split.split('Handler]')[0];
if (LocalLogsConfigs[key]) {
const args = split.split('args=(')[1].split(',');
logsObject[key].filePath = args[0].replace(`/${key}.log`, '');
logsObject[key].savingPeriod = args[1].replace(' ', '');
logsObject[key].savingTime = +args[2];
logsObject[key].backupCount = +args[3];
checkAndFetchCredentials(security): void {
if ((security.type == SecurityTypes.ACCESS_TOKEN && !security.accessToken) ||
(security.type === SecurityTypes.USERNAME_PASSWORD && !(security.username && security.password)) || !security.type) {
this.deviceService.getDeviceCredentials(this.device.id).subscribe(credentials => {
if (credentials.credentialsType === DeviceCredentialsType.ACCESS_TOKEN) {
this.gatewayConfigGroup.get('thingsboard.security.accessToken').setValue(credentials.credentialsValue);
} 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);
}
});
}
}
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,
}
});
const formatArgs = logsString.split('format=')[1].split('}}datefmt=');
const logFormat = formatArgs[0];
const dateFormat = formatArgs[1];
}
return {local: logsObject, logFormat, dateFormat};
}
@ -366,7 +407,7 @@ export class GatewayConfigurationComponent implements OnInit {
const localLogsFormGroup = this.gatewayConfigGroup.get('logs.local') as FormGroup;
const configGroup = this.fb.group({
logLevel: [config.logLevel || GatewayLogLevel.info, [Validators.required]],
filePath: [config.filePath || './data', [Validators.required]],
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]]
@ -416,65 +457,105 @@ export class GatewayConfigurationComponent implements OnInit {
);
}
generateLogsFile(logsObj): string {
const line = '[loggers]}}' +
'keys=root, service, connector, converter, tb_connection, storage, extension}}' +
'[handlers]}}' +
'keys=consoleHandler, serviceHandler, connectorHandler, converterHandler, tb_connectionHandler, storageHandler, extensionHandler}}' +
'[formatters]}}' +
'keys=LogFormatter}}' +
'[logger_root]}}' +
'level=ERROR}}' +
'handlers=consoleHandler}}' +
'[logger_database]}}' +
'level=INFO}}' +
'handlers=databaseHandler}}' +
'formatter=LogFormatter}}' +
'qualname=database}}';
let loggerLine = '';
let handlerLine = '';
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)) {
loggerLine += `[logger_${key}]}}` +
`level=${logsObj.local[key].logLevel}}}` +
`handlers=${key}Handler}}` +
`formatter=LogFormatter}}` +
`qualname=${key}}}`;
handlerLine += `[handler_${key}Handler]}}` +
`level=${logsObj.local[key].logLevel}}}` +
`class=thingsboard_gateway.tb_utility.tb_logger.TimedRotatingFileHandler}}` +
`formatter=LogFormatter}}` +
`args=("${logsObj.local[key].filePath}/${key}.log", ${logsObj.local[key].savingPeriod},` +
` ${logsObj.local[key].savingTime}, ${logsObj.local[key].backupCount},)}}`;
logAttrObj.handlers[key+"Handler"] = this.createHandlerObj(logsObj.local[key], key);
logAttrObj.loggers[key] = this.createLoggerObj(logsObj.local[key], key);
}
return logAttrObj;
}
const logerEnding = `[formatter_LogFormatter]}}` +
`format="${logsObj.logFormat}"}}` +
`datefmt="${logsObj.dateFormat}"`;
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"
}
}
return line + loggerLine + handlerLine + logerEnding;
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;
value.logs = stringToBase64(this.generateLogsFile(value.logs));
const configuration = objToBase64({thingsboard: value});
attributes.push({
key: 'configuration',
value: configuration
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();
}
});
}
@ -502,7 +583,5 @@ export class GatewayConfigurationComponent implements OnInit {
}
);
});
}
}

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

@ -15,115 +15,149 @@
limitations under the License.
-->
<form style="height: 100%" fxLayout="column" [formGroup]="gatewayConnectorsGroup">
<mat-toolbar color="primary">
<h2 translate>gateway.connectors</h2>
<span fxFlex></span>
<button mat-icon-button
type="button"
(click)="cancel()"
*ngIf="dialogRef">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<div fxLayout="row" fxLayoutGap="15px" fxFlex style="height: calc(100% - 35px)">
<div fxFlex="30" fxLayout="column" style="overflow: auto">
<mat-accordion multi>
<mat-expansion-panel expanded="true"
formArrayName="connectors"
*ngFor="let connectorControl of connectorsFormArray().controls; let $index = index"
[ngClass]="{'mat-card-selected': $index === selectedConnector}">
<mat-expansion-panel-header>
<mat-panel-title class="expansion-panel-header">
{{generatePanelTitle(connectorControl)}}
</mat-panel-title>
</mat-expansion-panel-header>
<div fxLayout="row">
<mat-card fxLayout="row wrap" style="margin-bottom: 15px" [formGroupName]="$index">
<mat-slide-toggle color="primary" fxFlex="100" formControlName="active">
{{ (connectorControl.get('active').value ? 'gateway.connectors-active' : 'gateway.connectors-inactive') | translate }}
</mat-slide-toggle>
<mat-form-field fxFlex="calc(50%-15px)">
<mat-label translate>gateway.connector-name</mat-label>
<input matInput formControlName="name" required/>
<mat-error
*ngIf="connectorControl.get('name').hasError('required')">
{{'gateway.connector-name-required' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field fxFlex="calc(50%-15px)">
<mat-label translate>gateway.connector-configuration</mat-label>
<input matInput formControlName="configuration" required/>
<mat-error
*ngIf="connectorControl.get('configuration').hasError('required')">
{{'gateway.connector-configuration-required' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field fxFlex="calc(50%-15px)">
<mat-label translate>gateway.connector-type</mat-label>
<mat-select formControlName="type" required>
<mat-option *ngFor="let type of gatewayConnectorDefaultTypes" [value]="type">{{type}}</mat-option>
</mat-select>
<mat-error
*ngIf="connectorControl.get('type').hasError('required')">
{{'gateway.connector-type-required' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field fxFlex="calc(50%-15px)">
<mat-label translate>gateway.connector-key</mat-label>
<input matInput formControlName="key" required/>
<mat-error
*ngIf="connectorControl.get('key').hasError('required')">
{{'gateway.connector-key-required' | translate }}
</mat-error>
</mat-form-field>
</mat-card>
<div fxLayout="column">
<button mat-icon-button (click)="removeConnector($index)"
matTooltip="{{ 'gateway.statistics.remove' | translate }}"
matTooltipPosition="above">
<mat-icon>close</mat-icon>
<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">
{{ attribute.value.type }}
</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 (click)="selectConnector($index)"
matTooltip="{{ 'gateway.select-connector' | translate }}"
matTooltipPosition="above">
<mat-icon>code</mat-icon>
<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>
</div>
</mat-expansion-panel>
</mat-accordion>
<button mat-raised-button color="primary"
style="margin-top: 15px"
type="button"
(click)="addConnector(true)">
{{ 'gateway.connector-add' | translate }}
</button>
</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" [value]="type">{{type}}</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">
<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="end 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>
<tb-json-object-edit
fxFlex
fxLayout="column"
required
*ngIf="selectedConnector !== undefined"
label="{{ 'gateway.connector-json' | translate }}"
[formControl]="getJsonControl(selectedConnector)">
</tb-json-object-edit>
</div>
<div mat-dialog-actions fxLayoutAlign="end center">
<button mat-button color="primary"
class="action-btns"
type="button"
*ngIf="dialogRef"
(click)="cancel()">
{{ 'action.cancel' | translate }}
</button>
<button mat-raised-button color="primary"
class="action-btns"
type="button"
[disabled]="gatewayConnectorsGroup.invalid || !gatewayConnectorsGroup.dirty"
(click)="saveConnectors()">
{{ 'action.save' | translate }}
</button>
</div>
</form>

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

@ -17,6 +17,31 @@
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;
@ -24,7 +49,9 @@
}
.mat-mdc-form-field {
margin-right: 15px;
flex-grow: 1;
//min-width: 200px;
//flex-shrink: 1;
}
mat-expansion-panel {
@ -55,5 +82,9 @@
.action-btns {
margin: 10px 10px 0;
}
mat-row {
cursor: pointer;
}
}

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

@ -14,26 +14,40 @@
/// limitations under the License.
///
import { ChangeDetectorRef, Component, Input, OnInit } from '@angular/core';
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 { FormArray, FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { EntityId } from '@shared/models/id/entity-id';
import { MatDialog, MatDialogRef } from '@angular/material/dialog';
import { AttributeService } from '@core/http/attribute.service';
import { DeviceService } from '@core/http/device.service';
import { TranslateService } from '@ngx-translate/core';
import { forkJoin } from 'rxjs';
import { AttributeScope } from '@shared/models/telemetry/telemetry.models';
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 { updateEntityParams, WidgetContext } from "@home/models/widget-component.models";
import { deepClone } from "@core/utils";
export interface gatewayConnector {
name: string;
type: string;
configuration: string;
jsonConfiguration: string;
key: string;
configuration?: string;
configurationJson: string;
log_level: string;
key?: string;
}
@Component({
@ -41,39 +55,58 @@ export interface gatewayConnector {
templateUrl: './gateway-connectors.component.html',
styleUrls: ['./gateway-connectors.component.scss']
})
export class GatewayConnectorComponent implements OnInit {
export class GatewayConnectorComponent extends PageComponent implements AfterViewInit {
gatewayConnectorsGroup: FormGroup;
pageLink: PageLink;
attributeDataSource: AttributeDatasource;
dataSource: MatTableDataSource<AttributeData>
displayedColumns = ['enabled', 'key', 'type', 'actions'];
gatewayConnectorDefaultTypes: Array<string> =
['mqtt',
'modbus',
'grpc',
'opcua',
'opcua_asyncio',
'ble',
'request',
'can',
'bacnet',
'odbc',
'rest',
'snmp',
'ftp',
'socket',
'xmpp',
'ocpp'
];
selectedConnector: number;
'modbus',
'grpc',
'opcua',
'opcua_asyncio',
'ble',
'request',
'can',
'bacnet',
'odbc',
'rest',
'snmp',
'ftp',
'socket',
'xmpp',
'ocpp',
'custom'
];
@Input()
device: EntityId;
ctx: WidgetContext;
@Input()
dialogRef: MatDialogRef<any>;
device: EntityId;
@ViewChild('searchInput') searchInputField: ElementRef;
@ViewChild(MatSort) sort: MatSort;
logSelector: FormControl;
connectorForm: FormGroup;
viewsInited = false;
textSearchMode: boolean;
activeConnectors: Array<string>;
inactiveConnectors: Array<string>;
InitialActiveConnectors: Array<string>;
gatewayLogLevel = Object.values(GatewayLogLevel);
constructor(protected router: Router,
protected store: Store<AppState>,
@ -81,111 +114,244 @@ export class GatewayConnectorComponent implements OnInit {
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.dataSource = new MatTableDataSource<AttributeData>([]);
this.connectorForm = this.fb.group({
name: ['', [Validators.required]],
type: ['', [Validators.required]],
log_level: ['', [Validators.required]],
key: ['auto'],
configurationJson: [{}, [Validators.required]]
})
this.connectorForm.disable();
}
ngOnInit() {
this.gatewayConnectorsGroup = this.fb.group({
connectors: this.fb.array([], [Validators.required])
});
this.gatewayConnectorsGroup.valueChanges.subscribe(_=>{
this.cd.detectChanges();
});
this.getConnectorsData();
initialConnector: gatewayConnector;
ngAfterViewInit() {
console.log(this.device)
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);
}
})
}
}
cancel(): void {
if (this.dialogRef) {
this.dialogRef.close();
saveConnector(): void {
const value = this.connectorForm.value;
if (value.type !== 'grpc') {
delete value.key;
}
value.configuration = `${value.type}.json`
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(resp => {
this.showToast("Update Successful")
this.updateData(true);
})
}
getConnectorsData(): void {
forkJoin([
this.attributeService.getEntityAttributes(this.device, AttributeScope.SHARED_SCOPE,['implementedConnectors']),
this.attributeService.getEntityAttributes(this.device, AttributeScope.SERVER_SCOPE,['implementedConnectors']),
this.attributeService.getEntityAttributes(this.device, AttributeScope.SHARED_SCOPE,['connectorTypes'])
]).subscribe(attributes=>{
if (attributes[0].length) {
attributes[0][0].value.forEach(connector=>this.addConnector(true, connector));
}
if (attributes[1].length) {
attributes[1][0].value.forEach(connector=>this.addConnector(false, connector));
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);
}
if(attributes[2].length) {
attributes[1][0].value.forEach(type=> {
if (this.gatewayConnectorDefaultTypes.indexOf(type) === -1) {
this.gatewayConnectorDefaultTypes.push(type);
}
});
}
this.cd.detectChanges();
});
}
}
saveConnectors(): void {
const connectors = this.gatewayConnectorsGroup.value.connectors;
const activeConnectors = connectors.filter(connector=>connector.active);
const inactiveConnectors = connectors.filter(connector=>!connector.active);
forkJoin([
this.attributeService.saveEntityAttributes(this.device, AttributeScope.SERVER_SCOPE,
[{key: 'implementedConnectors', value: inactiveConnectors}]),
this.attributeService.saveEntityAttributes(this.device, AttributeScope.SHARED_SCOPE,
[{key: 'implementedConnectors', value: activeConnectors}])
]).subscribe(_=> {
if (this.dialogRef) {
this.dialogRef.close();
} else {
this.gatewayConnectorsGroup.markAsPristine();
this.cd.detectChanges();
}
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.SHARED_SCOPE, this.pageLink, reload).subscribe(data => {
this.dataSource.data = data.data.filter(value => this.activeConnectors.includes(value.key) || this.inactiveConnectors.includes(value.key));
});
}
connectorsFormArray(): FormArray {
return this.gatewayConnectorsGroup.get('connectors') as FormArray;
addAttribute(): void {
if (this.connectorForm.disabled) {
this.connectorForm.enable();
}
this.clearOutConnectorForm();
}
addConnector(active?: boolean,connector?: gatewayConnector) {
const newConnector = this.fb.group({
active: [!!active],
name: [connector?.name || '', [Validators.required]],
type: [connector?.type || '', [Validators.required]],
configuration: [connector?.configuration || '', [Validators.required]],
jsonConfiguration: [connector?.jsonConfiguration || {}, [Validators.required]],
key: [connector?.key || 'auto', [Validators.required]],
});
const connectorsFormArray = this.connectorsFormArray();
connectorsFormArray.push(newConnector);
this.selectConnector(connectorsFormArray.length-1);
clearOutConnectorForm(): void {
this.connectorForm.setValue({
name: '',
type: this.gatewayConnectorDefaultTypes[0],
log_level: GatewayLogLevel.info,
key: 'auto',
configurationJson: {}
})
this.initialConnector = null;
this.connectorForm.markAsPristine();
}
removeConnector(index: number): void {
this.connectorsFormArray().removeAt(index);
if (index !== 0) {
this.selectedConnector = index - 1;
} else {
this.selectedConnector = undefined;
selectConnector(attribute): void {
if (this.connectorForm.disabled) {
this.connectorForm.enable();
}
const connector = attribute.value;
if (!connector.key) {
connector.key = 'auto';
}
if (connector.configuration) {
delete connector.configuration;
}
this.initialConnector = connector;
this.connectorForm.setValue(connector);
}
selectConnector(index: number): void {
this.selectedConnector = index;
showToast(message: string) {
this.store.dispatch(new ActionNotificationShow(
{
message,
type: 'success',
duration: 1000,
verticalPosition: 'top',
horizontalPosition: 'right',
target: 'dashboardRoot',
// panelClass: this.widgetNamespace,
forceDismiss: true
}));
}
getJsonControl(selectedConnector: number): FormControl {
return this.connectorsFormArray().at(selectedConnector).get('jsonConfiguration') as FormControl;
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(resp=>{
if (this.initialConnector ? this.initialConnector.name === attribute.key: true) {
this.clearOutConnectorForm();
this.cd.detectChanges();
this.connectorForm.disable();
}
this.updateData()
})
}
})
}
connectorLogs(attribute: AttributeData, $event: Event): void {
if ($event) {
$event.stopPropagation();
}
console.log(attribute);
const params = deepClone(this.ctx.stateController.getStateParams());
params.connector_logs = attribute;
params.targetEntityParamName = "connector_logs";
this.ctx.stateController.openState("connector_logs", params);
}
generatePanelTitle(connectorControl): string{
const connectorValues = connectorControl.value;
const activeTxt = this.translate.instant(connectorValues.active ? 'gateway.connectors-active' : 'gateway.connectors-inactive');
let title = `${connectorValues.name} | ${connectorValues.type} `;
if (connectorValues.type === 'grpc') {
title += `| ${connectorValues.key} `;
connectorRpc(attribute: AttributeData, $event: Event): void {
if ($event) {
$event.stopPropagation();
}
title += `| ${activeTxt}`;
return title;
console.log(attribute);
const params = deepClone(this.ctx.stateController.getStateParams());
params.connector_logs = 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(resp => {
this.updateData();
})
}
}

57
ui-ngx/src/app/shared/components/device/gateway-logs.component.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.
-->
<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>
<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;
}
}

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

@ -0,0 +1,250 @@
///
/// 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, 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'];
gatewayConnectorDefaultTypes: Array<string> =
['mqtt',
'modbus',
'grpc',
'opcua',
'opcua_asyncio',
'ble',
'request',
'can',
'bacnet',
'odbc',
'rest',
'snmp',
'ftp',
'socket',
'xmpp',
'ocpp',
'custom'
];
@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"
}]
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,
public dialog: MatDialog) {
super(store);
const sortOrder: SortOrder = {property: 'key', direction: Direction.ASC};
this.pageLink = new PageLink(1000, 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) {
console.log(this.ctx.settings.connectorLogState)
const connector = this.ctx.stateController.getStateParams()[this.ctx.settings.connectorLogState];
console.log(connector)
this.logLinks = [{
key: `${connector.key}_LOGS`,
name: "Connector",
filterFn: (attrData)=>{
return !attrData.message.includes(`${connector.key}_converter`)
}
},{
key: `${connector.key}_LOGS`,
name: "Convector",
filterFn: (attrData)=>{
return attrData.message.includes(`${connector.key}_converter`)
}
},
{
key: `${connector.key}_EXTENSION_LOGS`,
name: "Extension"
}]
} 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 => {
return {
ts: data[0],
key: this.activeLink.key,
status: data[1].match(/\|(\w+)\|/)[1],
message: /\[(.*)/.exec(data[1])[0]
};
});
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>Command</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>Time</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>Parameters</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>

48
ui-ngx/src/app/shared/components/device/gateway-service-rpc.component.scss

@ -0,0 +1,48 @@
/**
* 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%;
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;
}
::ng-deep.mat-mdc-form-field-icon-suffix {
z-index: 100;
}
}
.result-block {
padding: 0 15px;
&>span {
font-weight: 600;
}
}
}

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

@ -0,0 +1,125 @@
///
/// 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, 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;
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,
private cd: ChangeDetectorRef,
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]);
}
}
sendCommand() {
const formValues = this.commandForm.value;
this.ctx.controlApi.sendTwoWayCommand(formValues.command.toLowerCase(), {},formValues.time).subscribe(resp=>{
console.log(resp);
this.commandForm.get('result').setValue(resp);
},error => {
console.log(error);
this.commandForm.get('result').setValue(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));
}
}
);
}
}

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

@ -0,0 +1,43 @@
<!--
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">
<mat-form-field class="mat-block" >
<mat-label>Statistic</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.attributeName">
{{command.attributeName}}
</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>Command</mat-label>
<input matInput [value]="commandObj.command" disabled>
</mat-form-field>
</mat-card>
<div #statisticChart></div>
</div>

41
ui-ngx/src/app/shared/components/device/gateway-statistics.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 {
width: 100%;
height: 100%;
padding: 0;
.statistics-container {
height: 100%;
mat-card {
width: 40%;
margin-right: 35px;
padding: 15px;
}
div {
height: 100%;
flex-grow: 1;
}
& > * {
height: 100%;
}
}
}

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

@ -0,0 +1,150 @@
///
/// 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 { AttributeScope, DataKeyType } 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";
@Component({
selector: 'tb-gateway-statistics',
templateUrl: './gateway-statistics.component.html',
styleUrls: ['./gateway-statistics.component.scss']
})
export class GatewayStatisticsComponent extends PageComponent implements AfterViewInit {
@ViewChild('statisticChart') statisticChart: ElementRef;
@Input()
ctx: WidgetContext;
private flot: TbFlot;
public statisticForm: FormGroup;
public statisticsKeys = [];
public commands = [];
public commandObj: any;
private resize$: ResizeObserver;
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,
public dialog: MatDialog) {
super(store);
this.statisticForm = this.fb.group({
statisticKey: [this.statisticsKeys[0], []]
})
this.statisticForm.get('statisticKey').valueChanges.subscribe(value=>{
this.commandObj = null;
if (this.commands.length) {
this.commandObj = this.commands.find(command=>command.attributeName === value);
}
this.changeSubscription(true);
})
}
ngAfterViewInit() {
if (this.ctx.defaultSubscription.datasources.length) {
const gatewayId = this.ctx.defaultSubscription.datasources[0].entity.id;
this.attributeService.getEntityAttributes(gatewayId, AttributeScope.SHARED_SCOPE, ["configuration"]).subscribe(resp=>{
if (resp && resp.length) {
this.commands = resp[0].value.thingsboard.thingsboard.statistics.commands;
if (!this.statisticForm.get('statisticKey').value ) {
this.statisticForm.get('statisticKey').setValue(this.commands[0].attributeName);
this.changeSubscription(true);
}
}
})
}
this.ctx.defaultSubscription.onTimewindowChangeFunction = timeWindow => {
this.ctx.defaultSubscription.options.timeWindowConfig = timeWindow;
this.ctx.defaultSubscription.updateTimewindowConfig(timeWindow);
this.ctx.defaultSubscription.update();
this.updateChart();
return timeWindow;
}
this.changeSubscription(true);
this.resize$ = new ResizeObserver(() => {
this.resize();
});
this.resize$.observe(this.statisticChart.nativeElement)
}
initChart = () => {
if (this.ctx.defaultSubscription.data.length && !this.flot) {
this.ctx.$container = $(this.statisticChart.nativeElement);
this.flot = new TbFlot(this.ctx, 'line');
} else setTimeout(this.initChart, 500);
}
updateChart = () => {
if (this.flot && this.ctx.defaultSubscription.data.length) {
this.flot.update();
}
}
resize = () => {
if (this.flot) {
this.flot.resize();
}
}
changeSubscription(init?: boolean) {
if (this.ctx.datasources[0].entity) {
if (this.flot) {
this.flot.destroy();
delete this.flot;
}
this.ctx.defaultSubscription.options.datasources[0].dataKeys = [{
name: this.statisticForm.get('statisticKey').value,
type: DataKeyType.timeseries,
settings: {},
color: "#2196f3"
}];
this.ctx.defaultSubscription.unsubscribe();
this.ctx.defaultSubscription.updateDataSubscriptions();
if (init) this.initChart();
this.ctx.defaultSubscription.options.callbacks.dataLoading = ()=>{};
this.ctx.defaultSubscription.callbacks.onDataUpdated = () => {
this.updateChart();
}
}
}
}

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

@ -28,7 +28,7 @@
</mat-toolbar>
<div mat-dialog-content tb-toast toastTarget="activationLinkDialogContent">
<div class="mat-content" fxLayout="column">
<span innerHTML="{{'gateway.configuration-delete-dialog-body' | translate}}" > </span>
<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/>

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 = '*/*';

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

@ -180,6 +180,9 @@ import { DeviceGatewayCommandComponent } from '@shared/components/device/device-
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;
@ -247,6 +250,9 @@ export const MarkedOptionsFactory = (markedOptionsService: MarkedOptionsService)
DeviceGatewayCommandComponent,
GatewayConfigurationComponent,
GatewayConnectorComponent,
GatewayLogsComponent,
GatewayStatisticsComponent,
GatewayServiceRPCComponent,
GatewayRemoteConfigurationDialogComponent,
TbCheckboxComponent,
TbSnackBarComponent,
@ -417,6 +423,9 @@ export const MarkedOptionsFactory = (markedOptionsService: MarkedOptionsService)
DeviceGatewayCommandComponent,
GatewayConfigurationComponent,
GatewayConnectorComponent,
GatewayLogsComponent,
GatewayStatisticsComponent,
GatewayServiceRPCComponent,
GatewayRemoteConfigurationDialogComponent,
TbCheckboxComponent,
TbErrorComponent,

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

@ -2512,7 +2512,7 @@
"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-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",
@ -2609,6 +2609,7 @@
"stats-send-period-in-sec": "Stats send period in seconds",
"statistics": {
"statistics": "Statistics",
"commands": "Commands",
"send-period": "Statistic send period (in ms)",
"send-period-required": "Statistic send period is required",
"send-period-min": "Statistic send period can not be less then 0",
@ -2689,7 +2690,45 @@
"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?",
@ -5247,6 +5286,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": {
@ -5273,7 +5317,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