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 GatewayRemoteConfigurationDialogComponent from '@shared/components/dialog/gateway-remote-configuration-dialog';
import * as GatewayConfigurationComponent from '@shared/components/device/gateway-configuration.component'; import * as GatewayConfigurationComponent from '@shared/components/device/gateway-configuration.component';
import * as GatewayConnectorComponent from '@shared/components/device/gateway-connectors.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'; 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/device-gateway-command.component': DeviceGatewayCommandComponent,
'@shared/components/device/gateway-configuration.component': GatewayConfigurationComponent, '@shared/components/device/gateway-configuration.component': GatewayConfigurationComponent,
'@shared/components/device/gateway-connectors.component': GatewayConnectorComponent, '@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/device/gateway-remote-configuration-dialog': GatewayRemoteConfigurationDialogComponent,
'@shared/components/tb-checkbox.component': TbCheckboxComponent, '@shared/components/tb-checkbox.component': TbCheckboxComponent,
'@shared/components/toast.directive': TbToast, '@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 { import {
TripAnimationPointSettingsComponent TripAnimationPointSettingsComponent
} from '@home/components/widget/lib/settings/map/trip-animation-point-settings.component'; } 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({ @NgModule({
declarations: [ declarations: [
@ -357,7 +367,10 @@ import {
TripAnimationPointSettingsComponent, TripAnimationPointSettingsComponent,
MapWidgetSettingsComponent, MapWidgetSettingsComponent,
RouteMapWidgetSettingsComponent, RouteMapWidgetSettingsComponent,
TripAnimationWidgetSettingsComponent TripAnimationWidgetSettingsComponent,
ActionButtonsWidgetSettingsComponent,
GatewayLogsSettingsComponent,
GatewayServiceRPCSettingsComponent
], ],
imports: [ imports: [
CommonModule, CommonModule,
@ -460,7 +473,10 @@ import {
TripAnimationPointSettingsComponent, TripAnimationPointSettingsComponent,
MapWidgetSettingsComponent, MapWidgetSettingsComponent,
RouteMapWidgetSettingsComponent, RouteMapWidgetSettingsComponent,
TripAnimationWidgetSettingsComponent TripAnimationWidgetSettingsComponent,
ActionButtonsWidgetSettingsComponent,
GatewayLogsSettingsComponent,
GatewayServiceRPCSettingsComponent
] ]
}) })
export class WidgetSettingsModule { export class WidgetSettingsModule {
@ -527,5 +543,8 @@ export const widgetSettingsComponentsMap: {[key: string]: Type<IWidgetSettingsCo
'tb-update-multiple-attributes-key-settings': UpdateMultipleAttributesKeySettingsComponent, 'tb-update-multiple-attributes-key-settings': UpdateMultipleAttributesKeySettingsComponent,
'tb-map-widget-settings': MapWidgetSettingsComponent, 'tb-map-widget-settings': MapWidgetSettingsComponent,
'tb-route-map-widget-settings': RouteMapWidgetSettingsComponent, '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 { QrCodeWidgetComponent } from '@home/components/widget/lib/qrcode-widget.component';
import { MarkdownWidgetComponent } from '@home/components/widget/lib/markdown-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 { SelectEntityDialogComponent } from '@home/components/widget/lib/maps/dialogs/select-entity-dialog.component';
import { ActionButtonsComponent } from '@home/components/widget/lib/action-buttons.component';
@NgModule({ @NgModule({
declarations: declarations:
@ -64,7 +65,8 @@ import { SelectEntityDialogComponent } from '@home/components/widget/lib/maps/di
NavigationCardWidgetComponent, NavigationCardWidgetComponent,
QrCodeWidgetComponent, QrCodeWidgetComponent,
MarkdownWidgetComponent, MarkdownWidgetComponent,
SelectEntityDialogComponent SelectEntityDialogComponent,
ActionButtonsComponent
], ],
imports: [ imports: [
CommonModule, CommonModule,
@ -88,7 +90,8 @@ import { SelectEntityDialogComponent } from '@home/components/widget/lib/maps/di
NavigationCardsWidgetComponent, NavigationCardsWidgetComponent,
NavigationCardWidgetComponent, NavigationCardWidgetComponent,
QrCodeWidgetComponent, QrCodeWidgetComponent,
MarkdownWidgetComponent MarkdownWidgetComponent,
ActionButtonsComponent
], ],
providers: [ providers: [
CustomDialogService, CustomDialogService,

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

@ -19,7 +19,7 @@
<mat-toolbar color="primary"> <mat-toolbar color="primary">
<h2 translate>gateway.gateway-configuration</h2> <h2 translate>gateway.gateway-configuration</h2>
<span fxFlex></span> <span fxFlex></span>
<button mat-icon-button <button mat-icon-button
type="button" type="button"
(click)="cancel()" (click)="cancel()"
*ngIf="dialogRef"> *ngIf="dialogRef">
@ -34,13 +34,25 @@
<div fxLayout="row wrap"> <div fxLayout="row wrap">
<mat-slide-toggle color="primary" fxFlex="100" formControlName="remoteConfiguration"> <mat-slide-toggle color="primary" fxFlex="100" formControlName="remoteConfiguration">
{{ 'gateway.remote-configuration' | translate }} {{ '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>
<mat-slide-toggle color="primary" fxFlex="100" formControlName="remoteShell"> <mat-slide-toggle color="primary" fxFlex="100" formControlName="remoteShell">
{{ 'gateway.remote-shell' | translate }} {{ '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-slide-toggle>
<mat-form-field fxFlex="calc(50%-15px)" class="mat-block tb-value-type"> <mat-form-field fxFlex="calc(50%-15px)" class="mat-block tb-value-type">
<mat-label translate>gateway.thingsboard-host</mat-label> <mat-label translate>gateway.thingsboard-host</mat-label>
<input matInput formControlName="host"/> <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')"> <mat-error *ngIf="gatewayConfigGroup.get('thingsboard.host').hasError('required')">
{{ 'gateway.thingsboard-host-required' | translate }} {{ 'gateway.thingsboard-host-required' | translate }}
</mat-error> </mat-error>
@ -60,6 +72,10 @@
<mat-error *ngIf="gatewayConfigGroup.get('thingsboard.port').hasError('pattern')"> <mat-error *ngIf="gatewayConfigGroup.get('thingsboard.port').hasError('pattern')">
{{'gateway.thingsboard-port-pattern' | translate }} {{'gateway.thingsboard-port-pattern' | translate }}
</mat-error> </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> </mat-form-field>
</div> </div>
@ -82,6 +98,10 @@
<mat-error *ngIf="gatewayConfigGroup.get('thingsboard.security.accessToken').hasError('required')"> <mat-error *ngIf="gatewayConfigGroup.get('thingsboard.security.accessToken').hasError('required')">
{{'security.access-token-required' | translate}} {{'security.access-token-required' | translate}}
</mat-error> </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>
<mat-form-field *ngIf="gatewayConfigGroup.get('thingsboard.security.type').value === 'usernamePassword'" <mat-form-field *ngIf="gatewayConfigGroup.get('thingsboard.security.type').value === 'usernamePassword'"
fxFlex="calc(50%-15px)" class="mat-block tb-value-type"> fxFlex="calc(50%-15px)" class="mat-block tb-value-type">
@ -90,6 +110,10 @@
<mat-error *ngIf="gatewayConfigGroup.get('thingsboard.security.clientId').hasError('required')"> <mat-error *ngIf="gatewayConfigGroup.get('thingsboard.security.clientId').hasError('required')">
{{'security.clientId-required' | translate}} {{'security.clientId-required' | translate}}
</mat-error> </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>
<mat-form-field *ngIf="gatewayConfigGroup.get('thingsboard.security.type').value === 'usernamePassword'" <mat-form-field *ngIf="gatewayConfigGroup.get('thingsboard.security.type').value === 'usernamePassword'"
fxFlex="calc(50%-15px)" class="mat-block tb-value-type"> fxFlex="calc(50%-15px)" class="mat-block tb-value-type">
@ -98,6 +122,10 @@
<mat-error *ngIf="gatewayConfigGroup.get('thingsboard.security.username').hasError('required')"> <mat-error *ngIf="gatewayConfigGroup.get('thingsboard.security.username').hasError('required')">
{{'security.username-required' | translate}} {{'security.username-required' | translate}}
</mat-error> </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>
<mat-form-field *ngIf="gatewayConfigGroup.get('thingsboard.security.type').value === 'usernamePassword'" <mat-form-field *ngIf="gatewayConfigGroup.get('thingsboard.security.type').value === 'usernamePassword'"
fxFlex="calc(50%-15px)" class="mat-block tb-value-type"> fxFlex="calc(50%-15px)" class="mat-block tb-value-type">
@ -106,18 +134,24 @@
<mat-error *ngIf="gatewayConfigGroup.get('thingsboard.security.password').hasError('required')"> <mat-error *ngIf="gatewayConfigGroup.get('thingsboard.security.password').hasError('required')">
{{'security.password-required' | translate}} {{'security.password-required' | translate}}
</mat-error> </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> </mat-form-field>
<tb-file-input <tb-file-input
fxFlex="100" fxFlex="100"
hint="{{'gateway.hints.ca-cert' | translate}}"
*ngIf="gatewayConfigGroup.get('thingsboard.security.type').value.toLowerCase().includes('tls')" *ngIf="gatewayConfigGroup.get('thingsboard.security.type').value.toLowerCase().includes('tls')"
formControlName="caCert" formControlName="caCert"
label="{{'security.ca-cert-required' | translate}}" label="{{'security.ca-cert' | translate}}"
[allowedExtensions]="'pem, cert, key'" [allowedExtensions]="'pem, cert, key'"
[accept]="'.pem, application/pem,.cert, application/cert, .key,application/key'" [accept]="'.pem, application/pem,.cert, application/cert, .key,application/key'"
dropLabel="{{'gateway.drop-file' | translate}}"> dropLabel="{{'gateway.drop-file' | translate}}">
</tb-file-input> </tb-file-input>
<tb-file-input <tb-file-input
fxFlex="100" fxFlex="100"
hint="{{'gateway.hints.cert' | translate}}"
*ngIf="gatewayConfigGroup.get('thingsboard.security.type').value === 'tlsPrivateKey'" *ngIf="gatewayConfigGroup.get('thingsboard.security.type').value === 'tlsPrivateKey'"
formControlName="cert" formControlName="cert"
label="{{'security.cert' | translate}}" label="{{'security.cert' | translate}}"
@ -127,6 +161,7 @@
</tb-file-input> </tb-file-input>
<tb-file-input <tb-file-input
fxFlex="100" fxFlex="100"
hint="{{'gateway.hints.private-key' | translate}}"
*ngIf="gatewayConfigGroup.get('thingsboard.security.type').value === 'tlsPrivateKey'" *ngIf="gatewayConfigGroup.get('thingsboard.security.type').value === 'tlsPrivateKey'"
formControlName="privateKey" formControlName="privateKey"
label="{{'security.private-key' | translate}}" label="{{'security.private-key' | translate}}"
@ -150,6 +185,10 @@
<mat-error *ngIf="gatewayConfigGroup.get('logs.dateFormat').hasError('required')"> <mat-error *ngIf="gatewayConfigGroup.get('logs.dateFormat').hasError('required')">
{{'gateway.logs.date-format-required' | translate }} {{'gateway.logs.date-format-required' | translate }}
</mat-error> </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>
<mat-form-field fxFlex="100" class="mat-block tb-value-type"> <mat-form-field fxFlex="100" class="mat-block tb-value-type">
<mat-label translate>gateway.logs.log-format</mat-label> <mat-label translate>gateway.logs.log-format</mat-label>
@ -157,9 +196,14 @@
<mat-error *ngIf="gatewayConfigGroup.get('logs.logFormat').hasError('required')"> <mat-error *ngIf="gatewayConfigGroup.get('logs.logFormat').hasError('required')">
{{'gateway.logs.log-format-required' | translate }} {{'gateway.logs.log-format-required' | translate }}
</mat-error> </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> </mat-form-field>
<label fxFlex="100" class="logs-label" translate>gateway.logs.log-type</label> <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" <mat-button-toggle value="remote"
aria-label="Remote logging"> {{'gateway.logs.remote' | translate}}</mat-button-toggle> aria-label="Remote logging"> {{'gateway.logs.remote' | translate}}</mat-button-toggle>
<mat-button-toggle value="local" <mat-button-toggle value="local"
@ -168,6 +212,10 @@
<div fxFlex="100" fxLayout="column" formGroupName="remote" *ngIf="logging.value === 'remote'"> <div fxFlex="100" fxLayout="column" formGroupName="remote" *ngIf="logging.value === 'remote'">
<mat-slide-toggle color="primary" formControlName="enabled"> <mat-slide-toggle color="primary" formControlName="enabled">
{{ 'gateway.logs.remote-logs' | translate }} {{ '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-slide-toggle>
<mat-form-field class="mat-block tb-value-type"> <mat-form-field class="mat-block tb-value-type">
<mat-label translate>gateway.logs.level</mat-label> <mat-label translate>gateway.logs.level</mat-label>
@ -178,10 +226,10 @@
</div> </div>
<div fxFlex="100" fxLayout="row wrap" formGroupName="local" *ngIf="logging.value === 'local'"> <div fxFlex="100" fxLayout="row wrap" formGroupName="local" *ngIf="logging.value === 'local'">
<mat-button-toggle-group class="security-toggle-group" [formControl]="logSelector" fxFlex="100" <mat-button-toggle-group class="security-toggle-group" [formControl]="logSelector" fxFlex="100"
name="logSelector"> name="logSelector">
<mat-button-toggle *ngFor="let logConfig of localLogsConfigs" [value]="logConfig" <mat-button-toggle *ngFor="let logConfig of localLogsConfigLabels| keyvalue" [value]="logConfig.key"
class="first-capital"> class="first-capital">
{{ logConfig }} {{ logConfig.value }}
</mat-button-toggle> </mat-button-toggle>
</mat-button-toggle-group> </mat-button-toggle-group>
<div fxFlex="100" fxLayout="row wrap" [formGroup]="getLogFormGroup(logSelector.value)"> <div fxFlex="100" fxLayout="row wrap" [formGroup]="getLogFormGroup(logSelector.value)">
@ -230,6 +278,10 @@
*ngIf="gatewayConfigGroup.get('logs.local.' + logSelector.value + '.backupCount').hasError('min')"> *ngIf="gatewayConfigGroup.get('logs.local.' + logSelector.value + '.backupCount').hasError('min')">
{{'gateway.logs.backup-count-min' | translate }} {{'gateway.logs.backup-count-min' | translate }}
</mat-error> </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> </mat-form-field>
</div> </div>
</div> </div>
@ -243,13 +295,20 @@
<mat-accordion> <mat-accordion>
<mat-expansion-panel expanded="true" disabled> <mat-expansion-panel expanded="true" disabled>
<mat-expansion-panel-header> <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> </mat-expansion-panel-header>
<div fxLayout="row wrap"> <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"> <mat-button-toggle *ngFor="let storageType of storageTypes | keyvalue" [value]="storageType.key">
{{ storageType.value | translate }} {{ storageType.value | translate }}
</mat-button-toggle> </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-button-toggle-group>
<mat-form-field *ngIf="gatewayConfigGroup.get('storage.type').value === 'memory'" <mat-form-field *ngIf="gatewayConfigGroup.get('storage.type').value === 'memory'"
fxFlex="calc(50%-15px)" class="mat-block tb-value-type"> 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')"> <mat-error *ngIf="gatewayConfigGroup.get('storage.read_records_count').hasError('pattern')">
{{'gateway.storage-read-record-count-pattern' | translate}} {{'gateway.storage-read-record-count-pattern' | translate}}
</mat-error> </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>
<mat-form-field *ngIf="gatewayConfigGroup.get('storage.type').value === 'memory'" <mat-form-field *ngIf="gatewayConfigGroup.get('storage.type').value === 'memory'"
fxFlex="calc(50%-15px)" class="mat-block tb-value-type"> 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')"> <mat-error *ngIf="gatewayConfigGroup.get('storage.max_records_count').hasError('pattern')">
{{'gateway.storage-max-records-pattern' | translate}} {{'gateway.storage-max-records-pattern' | translate}}
</mat-error> </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>
<mat-form-field *ngIf="gatewayConfigGroup.get('storage.type').value === 'file'" <mat-form-field *ngIf="gatewayConfigGroup.get('storage.type').value === 'file'"
fxFlex="calc(50%-15px)" class="mat-block tb-value-type"> 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')"> <mat-error *ngIf="gatewayConfigGroup.get('storage.data_folder_path').hasError('required')">
{{'gateway.storage-data-folder-path-required' | translate}} {{'gateway.storage-data-folder-path-required' | translate}}
</mat-error> </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>
<mat-form-field *ngIf="gatewayConfigGroup.get('storage.type').value === 'file'" <mat-form-field *ngIf="gatewayConfigGroup.get('storage.type').value === 'file'"
fxFlex="calc(50%-15px)" class="mat-block tb-value-type"> 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')"> <mat-error *ngIf="gatewayConfigGroup.get('storage.max_file_count').hasError('pattern')">
{{'gateway.storage-max-files-pattern' | translate}} {{'gateway.storage-max-files-pattern' | translate}}
</mat-error> </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>
<mat-form-field *ngIf="gatewayConfigGroup.get('storage.type').value === 'file'" <mat-form-field *ngIf="gatewayConfigGroup.get('storage.type').value === 'file'"
fxFlex="calc(50%-15px)" class="mat-block tb-value-type"> 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')"> <mat-error *ngIf="gatewayConfigGroup.get('storage.max_read_records_count').hasError('pattern')">
{{'gateway.storage-max-read-record-count-pattern' | translate}} {{'gateway.storage-max-read-record-count-pattern' | translate}}
</mat-error> </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>
<mat-form-field *ngIf="gatewayConfigGroup.get('storage.type').value === 'file'" <mat-form-field *ngIf="gatewayConfigGroup.get('storage.type').value === 'file'"
fxFlex="calc(50%-15px)" class="mat-block tb-value-type"> 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')"> <mat-error *ngIf="gatewayConfigGroup.get('storage.max_records_per_file').hasError('pattern')">
{{'gateway.storage-max-records-pattern' | translate}} {{'gateway.storage-max-records-pattern' | translate}}
</mat-error> </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>
<mat-form-field *ngIf="gatewayConfigGroup.get('storage.type').value === 'sqlite'" <mat-form-field *ngIf="gatewayConfigGroup.get('storage.type').value === 'sqlite'"
fxFlex="calc(50%-15px)" class="mat-block tb-value-type"> 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')"> <mat-error *ngIf="gatewayConfigGroup.get('storage.data_file_path').hasError('required')">
{{'gateway.storage-path-required' | translate}} {{'gateway.storage-path-required' | translate}}
</mat-error> </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>
<mat-form-field *ngIf="gatewayConfigGroup.get('storage.type').value === 'sqlite'" <mat-form-field *ngIf="gatewayConfigGroup.get('storage.type').value === 'sqlite'"
fxFlex="calc(50%-15px)" class="mat-block tb-value-type"> 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')"> <mat-error *ngIf="gatewayConfigGroup.get('storage.messages_ttl_check_in_hours').hasError('pattern')">
{{'gateway.messages-ttl-check-in-hours-pattern' | translate}} {{'gateway.messages-ttl-check-in-hours-pattern' | translate}}
</mat-error> </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>
<mat-form-field *ngIf="gatewayConfigGroup.get('storage.type').value === 'sqlite'" <mat-form-field *ngIf="gatewayConfigGroup.get('storage.type').value === 'sqlite'"
fxFlex="calc(50%-15px)" class="mat-block tb-value-type" style="flex-grow: 0"> 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')"> <mat-error *ngIf="gatewayConfigGroup.get('storage.messages_ttl_in_days').hasError('pattern')">
{{'gateway.messages-ttl-in-days-pattern' | translate}} {{'gateway.messages-ttl-in-days-pattern' | translate}}
</mat-error> </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> </mat-form-field>
</div> </div>
</mat-expansion-panel> </mat-expansion-panel>
@ -491,33 +586,20 @@
{{'gateway.statistics.send-period-pattern' | translate }} {{'gateway.statistics.send-period-pattern' | translate }}
</mat-error> </mat-error>
</mat-form-field> </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> </div>
</mat-expansion-panel> </mat-expansion-panel>
<mat-expansion-panel expanded="true" disabled> <mat-expansion-panel expanded="true" disabled>
<mat-expansion-panel-header> <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> </mat-expansion-panel-header>
<div fxLayout="column" formGroupName="statistics"> <div fxLayout="column" formGroupName="statistics">
<div fxLayout="row" formArrayName="commands" <div fxLayout="row" formArrayName="commands"
*ngFor="let commandControl of commandFormArray().controls; let $index = index" *ngFor="let commandControl of commandFormArray().controls; let $index = index"
style="margin-bottom: 15px"> 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-form-field fxFlex="calc(50%-15px)" class="mat-block tb-value-type">
<mat-label translate>gateway.statistics.attribute-name</mat-label> <mat-label translate>gateway.statistics.attribute-name</mat-label>
<input matInput formControlName="attributeName"/> <input matInput formControlName="attributeName"/>
@ -525,6 +607,10 @@
*ngIf="commandControl.get('attributeName').hasError('required')"> *ngIf="commandControl.get('attributeName').hasError('required')">
{{'gateway.statistics.attribute-name-required' | translate }} {{'gateway.statistics.attribute-name-required' | translate }}
</mat-error> </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>
<mat-form-field fxFlex="calc(50%-15px)" class="mat-block tb-value-type"> <mat-form-field fxFlex="calc(50%-15px)" class="mat-block tb-value-type">
<mat-label translate>gateway.statistics.timeout</mat-label> <mat-label translate>gateway.statistics.timeout</mat-label>
@ -541,6 +627,10 @@
*ngIf="commandControl.get('timeout').hasError('pattern')"> *ngIf="commandControl.get('timeout').hasError('pattern')">
{{'gateway.statistics.timeout-pattern' | translate }} {{'gateway.statistics.timeout-pattern' | translate }}
</mat-error> </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>
<mat-form-field fxFlex="calc(50%-15px)" class="mat-block tb-value-type" style="flex-grow: 0"> <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> <mat-label translate>gateway.statistics.command</mat-label>
@ -549,6 +639,10 @@
*ngIf="commandControl.get('command').hasError('required')"> *ngIf="commandControl.get('command').hasError('required')">
{{'gateway.statistics.command-required' | translate }} {{'gateway.statistics.command-required' | translate }}
</mat-error> </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-form-field>
</mat-card> </mat-card>
<button mat-icon-button (click)="removeCommandControl($index)" <button mat-icon-button (click)="removeCommandControl($index)"
@ -574,12 +668,15 @@
<mat-tab label="{{'gateway.other' | translate}}"> <mat-tab label="{{'gateway.other' | translate}}">
<div formGroupName="thingsboard" fxLayout="column" class="mat-content mat-padding"> <div formGroupName="thingsboard" fxLayout="column" class="mat-content mat-padding">
<mat-accordion multi> <mat-accordion multi>
<mat-expansion-panel expanded="true" disabled formGroupName="checkingDeviceActivity"> <mat-expansion-panel expanded="true" disabled formGroupName="checkingDeviceActivity">
<mat-expansion-panel-header> <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"> <mat-slide-toggle color="primary" fxFlex="100" formControlName="checkDeviceInactivity">
{{ 'gateway.checking-device-activity' | translate }} {{ 'gateway.checking-device-activity' | translate }}
</mat-slide-toggle> </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-panel-title>
</mat-expansion-panel-header> </mat-expansion-panel-header>
<div fxLayout="row wrap"> <div fxLayout="row wrap">
@ -596,6 +693,10 @@
*ngIf="gatewayConfigGroup.get('thingsboard.checkingDeviceActivity.inactivityTimeoutSeconds').hasError('min')"> *ngIf="gatewayConfigGroup.get('thingsboard.checkingDeviceActivity.inactivityTimeoutSeconds').hasError('min')">
{{'gateway.inactivity-timeout-seconds-min' | translate }} {{'gateway.inactivity-timeout-seconds-min' | translate }}
</mat-error> </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>
<mat-form-field <mat-form-field
*ngIf="gatewayConfigGroup.get('thingsboard.checkingDeviceActivity.checkDeviceInactivity').value" *ngIf="gatewayConfigGroup.get('thingsboard.checkingDeviceActivity.checkDeviceInactivity').value"
@ -610,10 +711,14 @@
*ngIf="gatewayConfigGroup.get('thingsboard.checkingDeviceActivity.inactivityCheckPeriodSeconds').hasError('min')"> *ngIf="gatewayConfigGroup.get('thingsboard.checkingDeviceActivity.inactivityCheckPeriodSeconds').hasError('min')">
{{'gateway.inactivity-check-period-seconds-min' | translate }} {{'gateway.inactivity-check-period-seconds-min' | translate }}
</mat-error> </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> </mat-form-field>
</div> </div>
</mat-expansion-panel> </mat-expansion-panel>
<mat-expansion-panel expanded="true" disabled > <mat-expansion-panel expanded="true" disabled>
<mat-expansion-panel-header> <mat-expansion-panel-header>
<mat-panel-title translate class="expansion-panel-header">gateway.advanced</mat-panel-title> <mat-panel-title translate class="expansion-panel-header">gateway.advanced</mat-panel-title>
</mat-expansion-panel-header> </mat-expansion-panel-header>
@ -627,6 +732,10 @@
<mat-error *ngIf="gatewayConfigGroup.get('thingsboard.minPackSendDelayMS').hasError('min')"> <mat-error *ngIf="gatewayConfigGroup.get('thingsboard.minPackSendDelayMS').hasError('min')">
{{ 'gateway.min-pack-send-delay-min' | translate }} {{ 'gateway.min-pack-send-delay-min' | translate }}
</mat-error> </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>
<mat-form-field fxFlex="calc(50%-15px)" class="mat-block tb-value-type"> <mat-form-field fxFlex="calc(50%-15px)" class="mat-block tb-value-type">
<mat-label translate>gateway.mqtt-qos</mat-label> <mat-label translate>gateway.mqtt-qos</mat-label>
@ -640,6 +749,26 @@
<mat-error *ngIf="gatewayConfigGroup.get('thingsboard.qos').hasError('max')"> <mat-error *ngIf="gatewayConfigGroup.get('thingsboard.qos').hasError('max')">
{{ 'gateway.mqtt-qos-range' | translate}} {{ 'gateway.mqtt-qos-range' | translate}}
</mat-error> </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> </mat-form-field>
</div> </div>
</mat-expansion-panel> </mat-expansion-panel>
@ -661,7 +790,7 @@
type="button" type="button"
[disabled]="gatewayConfigGroup.invalid || !gatewayConfigGroup.dirty" [disabled]="gatewayConfigGroup.invalid || !gatewayConfigGroup.dirty"
cdkFocusInitial cdkFocusInitial
(click)="saveConfig()"> (click)="saveConfig()">
{{ 'action.save' | translate }} {{ 'action.save' | translate }}
</button> </button>

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

@ -18,6 +18,10 @@
height: 100%; height: 100%;
display: block; display: block;
.mat-icon {
color: rgba(0, 0, 0, .12);
}
.mat-toolbar { .mat-toolbar {
background: transparent; background: transparent;
color: rgba(0, 0, 0, .87) !important; color: rgba(0, 0, 0, .87) !important;
@ -126,6 +130,12 @@
} }
} }
.statistics-block {
margin-bottom: 15px;
padding-left: 15px;
padding-top: 15px;
}
.first-capital { .first-capital {
text-transform: capitalize; text-transform: capitalize;
} }
@ -147,5 +157,36 @@
z-index: -10; z-index: -10;
border-radius: 5px; 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. /// 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 { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { FormArray, FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms'; import { FormArray, FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms';
import { EntityId } from '@shared/models/id/entity-id'; import { EntityId } from '@shared/models/id/entity-id';
import { MatDialog, MatDialogRef } from '@angular/material/dialog'; import { MatDialog, MatDialogRef } from '@angular/material/dialog';
import { base64toObj, base64toString, objToBase64, stringToBase64 } from '@core/utils';
import { AttributeService } from '@core/http/attribute.service'; import { AttributeService } from '@core/http/attribute.service';
import { AttributeScope } from '@shared/models/telemetry/telemetry.models'; import { AttributeScope } from '@shared/models/telemetry/telemetry.models';
import { import {
@ -29,6 +28,9 @@ import {
GatewayRemoteConfigurationDialogData GatewayRemoteConfigurationDialogData
} from '@shared/components/dialog/gateway-remote-configuration-dialog'; } from '@shared/components/dialog/gateway-remote-configuration-dialog';
import { DeviceService } from '@core/http/device.service'; 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 { export enum StorageTypes {
MEMORY = 'memory', MEMORY = 'memory',
@ -46,10 +48,10 @@ export enum GatewayLogLevel {
} }
export enum LogSavingPeriod { export enum LogSavingPeriod {
days = 'd', days = 'D',
hours = 'h', hours = 'H',
minutes = 'm', minutes = 'M',
seconds = 's' seconds = 'S'
} }
export enum LocalLogsConfigs { export enum LocalLogsConfigs {
@ -61,6 +63,15 @@ export enum LocalLogsConfigs {
extension = 'extension' 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>( export const logSavingPeriodTranslations = new Map<LogSavingPeriod, string>(
[ [
[LogSavingPeriod.days, 'gateway.logs.days'], [LogSavingPeriod.days, 'gateway.logs.days'],
@ -107,12 +118,12 @@ export class GatewayConfigurationComponent implements OnInit {
logSavingPeriods = logSavingPeriodTranslations; logSavingPeriods = logSavingPeriodTranslations;
localLogsConfigLabels = localLogsConfigLabels;
securityTypes = securityTypesTranslationsMap; securityTypes = securityTypesTranslationsMap;
gatewayLogLevel = Object.values(GatewayLogLevel); gatewayLogLevel = Object.values(GatewayLogLevel);
localLogsConfigs = Object.values(LocalLogsConfigs);
@Input() @Input()
device: EntityId; device: EntityId;
@ -127,21 +138,21 @@ export class GatewayConfigurationComponent implements OnInit {
protected fb: FormBuilder, protected fb: FormBuilder,
protected attributeService: AttributeService, protected attributeService: AttributeService,
protected deviceService: DeviceService, protected deviceService: DeviceService,
private cd: ChangeDetectorRef,
public dialog: MatDialog) { public dialog: MatDialog) {
} }
ngOnInit() { ngOnInit() {
this.gatewayConfigGroup = this.fb.group({ this.gatewayConfigGroup = this.fb.group({
thingsboard: 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]+$/)]], port: [1883, [Validators.required, Validators.min(1), Validators.max(65535), Validators.pattern(/^-?[0-9]+$/)]],
remoteShell: [false, []], remoteShell: [false, []],
remoteConfiguration: [true, []], remoteConfiguration: [true, []],
checkConnectorsConfigurationInSeconds: [60, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
statistics: this.fb.group({ statistics: this.fb.group({
enable: [true, []], enable: [true, []],
statsSendPeriodInSeconds: [3600, [Validators.required, Validators.min(0), Validators.pattern(/^-?[0-9]+$/)]], 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([], []) commands: this.fb.array([], [])
}), }),
maxPayloadSizeBytes: [1024, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]], maxPayloadSizeBytes: [1024, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
@ -257,6 +268,10 @@ export class GatewayConfigurationComponent implements OnInit {
securityGroup.updateValueAndValidity(); securityGroup.updateValueAndValidity();
}); });
securityGroup.get('caCert').valueChanges.subscribe(_ => this.cd.detectChanges())
securityGroup.get('privateKey').valueChanges.subscribe(_ => this.cd.detectChanges())
securityGroup.get('cert').valueChanges.subscribe(_ => this.cd.detectChanges())
const storageGroup = this.gatewayConfigGroup.get('storage') as FormGroup; const storageGroup = this.gatewayConfigGroup.get('storage') as FormGroup;
storageGroup.get('type').valueChanges.subscribe(type => { storageGroup.get('type').valueChanges.subscribe(type => {
this.removeAllStorageValidators(); this.removeAllStorageValidators();
@ -286,25 +301,44 @@ export class GatewayConfigurationComponent implements OnInit {
} }
fetchConfigAttribute(entityId: EntityId) { fetchConfigAttribute(entityId: EntityId) {
this.attributeService.getEntityAttributes(entityId, AttributeScope.SHARED_SCOPE, this.attributeService.getEntityAttributes(entityId, AttributeScope.CLIENT_SCOPE,
['configuration', 'RemoteLoggingLevel']).subscribe(attributes => { ['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) { if (attributes.length) {
const configuration = attributes.find(attribute => attribute.key === 'configuration').value; const general_configuration = attributes.find(attribute => attribute.key === 'general_configuration')?.value;
const remoteLoggingLevel = attributes.find(attribute => attribute.key === 'RemoteLoggingLevel').value; const grpc_configuration = attributes.find(attribute => attribute.key === 'grpc_configuration')?.value;
if (configuration) { const logs_configuration = attributes.find(attribute => attribute.key === 'logs_configuration')?.value;
const configObj = base64toObj(configuration).thingsboard; const storage_configuration = attributes.find(attribute => attribute.key === 'storage_configuration')?.value;
configObj.logs = this.logsBase64ToObj(configObj.logs); 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) { 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); this.addCommand(command);
} }
delete configObj.thingsboard.statistics.commands; delete configObj.thingsboard.statistics.commands;
} }
this.gatewayConfigGroup.patchValue(configObj, {emitEvent: false}); this.gatewayConfigGroup.patchValue(configObj, {emitEvent: false});
this.gatewayConfigGroup.markAsDirty(); this.gatewayConfigGroup.markAsPristine();
if (!configObj.thingsboard.remoteConfiguration) { if (!configObj.thingsboard.remoteConfiguration) {
this.gatewayConfigGroup.disable({emitEvent: false}); 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) { if (remoteLoggingLevel) {
const remoteLogsFormGroup = this.gatewayConfigGroup.get('logs.remote'); const remoteLogsFormGroup = this.gatewayConfigGroup.get('logs.remote');
@ -312,41 +346,48 @@ export class GatewayConfigurationComponent implements OnInit {
enabled: remoteLoggingLevel !== GatewayLogLevel.none, enabled: remoteLoggingLevel !== GatewayLogLevel.none,
logLevel: remoteLoggingLevel logLevel: remoteLoggingLevel
}, {emitEvent: false}); }, {emitEvent: false});
remoteLogsFormGroup.markAsDirty(); remoteLogsFormGroup.markAsPristine();
} }
this.cd.detectChanges();
} else {
this.checkAndFetchCredentials({});
} }
}); });
} }
logsBase64ToObj(logs64: string) { checkAndFetchCredentials(security): void {
const logsString = base64toString(logs64).split('"').join(''); if ((security.type == SecurityTypes.ACCESS_TOKEN && !security.accessToken) ||
const logsObject = {}; (security.type === SecurityTypes.USERNAME_PASSWORD && !(security.username && security.password)) || !security.type) {
logsString.split('logger_').forEach((split, index) => { this.deviceService.getDeviceCredentials(this.device.id).subscribe(credentials => {
if (index > 0) { if (credentials.credentialsType === DeviceCredentialsType.ACCESS_TOKEN) {
const key = split.split(']')[0]; this.gatewayConfigGroup.get('thingsboard.security.accessToken').setValue(credentials.credentialsValue);
if (LocalLogsConfigs[key]) { } else if (credentials.credentialsType === DeviceCredentialsType.MQTT_BASIC) {
const logLevel = split.split('=')[1].replace('}}handlers', ''); const parsedValue = JSON.parse(credentials.credentialsValue);
logsObject[key] = { this.gatewayConfigGroup.get('thingsboard.security.clientId').setValue(parsedValue.clientId);
logLevel this.gatewayConfigGroup.get('thingsboard.security.username').setValue(parsedValue.userName);
}; this.gatewayConfigGroup.get('thingsboard.security.password').setValue(parsedValue.password);
}
}
});
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];
} }
});
}
}
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}; return {local: logsObject, logFormat, dateFormat};
} }
@ -366,7 +407,7 @@ export class GatewayConfigurationComponent implements OnInit {
const localLogsFormGroup = this.gatewayConfigGroup.get('logs.local') as FormGroup; const localLogsFormGroup = this.gatewayConfigGroup.get('logs.local') as FormGroup;
const configGroup = this.fb.group({ const configGroup = this.fb.group({
logLevel: [config.logLevel || GatewayLogLevel.info, [Validators.required]], 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)]], backupCount: [config.backupCount || 7, [Validators.required, Validators.min(0)]],
savingTime: [config.savingTime || 3, [Validators.required, Validators.min(0)]], savingTime: [config.savingTime || 3, [Validators.required, Validators.min(0)]],
savingPeriod: [config.savingPeriod || LogSavingPeriod.days, [Validators.required]] savingPeriod: [config.savingPeriod || LogSavingPeriod.days, [Validators.required]]
@ -416,65 +457,105 @@ export class GatewayConfigurationComponent implements OnInit {
); );
} }
generateLogsFile(logsObj): string { generateLogsFile(logsObj) {
const line = '[loggers]}}' + const logAttrObj = {
'keys=root, service, connector, converter, tb_connection, storage, extension}}' + version: 1,
'[handlers]}}' + disable_existing_loggers: false,
'keys=consoleHandler, serviceHandler, connectorHandler, converterHandler, tb_connectionHandler, storageHandler, extensionHandler}}' + formatters: {
'[formatters]}}' + LogFormatter: {
'keys=LogFormatter}}' + class: "logging.Formatter",
'[logger_root]}}' + format: logsObj.logFormat,
'level=ERROR}}' + datefmt: logsObj.dateFormat,
'handlers=consoleHandler}}' + }
'[logger_database]}}' + },
'level=INFO}}' + handlers: {
'handlers=databaseHandler}}' + consoleHandler: {
'formatter=LogFormatter}}' + class: "logging.StreamHandler",
'qualname=database}}'; formatter: "LogFormatter",
level: "DEBUG",
let loggerLine = ''; stream: "ext://sys.stdout"
let handlerLine = ''; },
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)) { for (const key of Object.keys(logsObj.local)) {
loggerLine += `[logger_${key}]}}` + logAttrObj.handlers[key+"Handler"] = this.createHandlerObj(logsObj.local[key], key);
`level=${logsObj.local[key].logLevel}}}` + logAttrObj.loggers[key] = this.createLoggerObj(logsObj.local[key], key);
`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},)}}`;
} }
return logAttrObj;
}
const logerEnding = `[formatter_LogFormatter]}}` + createHandlerObj(logObj, key) {
`format="${logsObj.logFormat}"}}` + return {
`datefmt="${logsObj.dateFormat}"`; 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 { saveConfig(): void {
const value = this.removeEmpty(this.gatewayConfigGroup.value); const value = this.removeEmpty(this.gatewayConfigGroup.value);
value.thingsboard.statistics.commands = Object.values(value.thingsboard.statistics.commands);
const attributes = []; const attributes = [];
attributes.push({ attributes.push({
key: 'RemoteLoggingLevel', key: 'RemoteLoggingLevel',
value: value.logs.remote.enabled ? value.logs.remote.logLevel : GatewayLogLevel.none value: value.logs.remote.enabled ? value.logs.remote.logLevel : GatewayLogLevel.none
}); });
delete value.connectors; delete value.connectors;
value.logs = stringToBase64(this.generateLogsFile(value.logs));
const configuration = objToBase64({thingsboard: value});
attributes.push({ attributes.push({
key: 'configuration', key: 'logs_configuration',
value: 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(_ => { this.attributeService.saveEntityAttributes(this.device, AttributeScope.SHARED_SCOPE, attributes).subscribe(_ => {
if (this.dialogRef) { if (this.dialogRef) {
this.dialogRef.close(); this.dialogRef.close();
} else { } else {
this.gatewayConfigGroup.markAsPristine(); 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. limitations under the License.
--> -->
<form style="height: 100%" fxLayout="column" [formGroup]="gatewayConnectorsGroup"> <form style="height: calc(100% - 7px)" fxLayout="column">
<mat-toolbar color="primary"> <div fxLayout="row" fxLayoutGap="15px" fxFlex class="connector-container">
<h2 translate>gateway.connectors</h2> <mat-card fxLayout="column" style="overflow: auto;">
<span fxFlex></span> <mat-toolbar class="mat-mdc-table-toolbar">
<button mat-icon-button <h2>Connectors</h2>
type="button" <span fxFlex></span>
(click)="cancel()" <button mat-icon-button
*ngIf="dialogRef"> [disabled]="isLoading$ | async"
<mat-icon class="material-icons">close</mat-icon> (click)="addAttribute()"
</button> matTooltip="{{ 'action.add' | translate }}"
</mat-toolbar> matTooltipPosition="above">
<div fxLayout="row" fxLayoutGap="15px" fxFlex style="height: calc(100% - 35px)"> <mat-icon>add</mat-icon>
<div fxFlex="30" fxLayout="column" style="overflow: auto"> </button>
<mat-accordion multi> </mat-toolbar>
<mat-expansion-panel expanded="true" <table mat-table [dataSource]="dataSource"
formArrayName="connectors" matSort [matSortActive]="pageLink.sortOrder.property" [matSortDirection]="pageLink.sortDirection()"
*ngFor="let connectorControl of connectorsFormArray().controls; let $index = index" matSortDisableClear>
[ngClass]="{'mat-card-selected': $index === selectedConnector}"> <ng-container matColumnDef="enabled" sticky>
<mat-expansion-panel-header> <mat-header-cell *matHeaderCellDef style="width: 30px;">
<mat-panel-title class="expansion-panel-header"> Enabled
{{generatePanelTitle(connectorControl)}} </mat-header-cell>
</mat-panel-title> <mat-cell *matCellDef="let attribute">
</mat-expansion-panel-header> <mat-slide-toggle [checked]="activeConnectors.includes(attribute.key)"
<div fxLayout="row"> (click)="$event.stopPropagation(); enableConnector(attribute)"></mat-slide-toggle>
<mat-card fxLayout="row wrap" style="margin-bottom: 15px" [formGroupName]="$index"> </mat-cell>
<mat-slide-toggle color="primary" fxFlex="100" formControlName="active"> </ng-container>
{{ (connectorControl.get('active').value ? 'gateway.connectors-active' : 'gateway.connectors-inactive') | translate }} <ng-container matColumnDef="key">
</mat-slide-toggle> <mat-header-cell *matHeaderCellDef mat-sort-header style="width: 40%"> Name</mat-header-cell>
<mat-form-field fxFlex="calc(50%-15px)"> <mat-cell *matCellDef="let attribute">
<mat-label translate>gateway.connector-name</mat-label> {{ attribute.key }}
<input matInput formControlName="name" required/> </mat-cell>
<mat-error </ng-container>
*ngIf="connectorControl.get('name').hasError('required')"> <ng-container matColumnDef="type">
{{'gateway.connector-name-required' | translate }} <mat-header-cell *matHeaderCellDef mat-sort-header style="width: 30%"> Type</mat-header-cell>
</mat-error> <mat-cell *matCellDef="let attribute" style="text-transform: uppercase">
</mat-form-field> {{ attribute.value.type }}
<mat-form-field fxFlex="calc(50%-15px)"> </mat-cell>
<mat-label translate>gateway.connector-configuration</mat-label> </ng-container>
<input matInput formControlName="configuration" required/> <ng-container matColumnDef="actions" stickyEnd>
<mat-error <mat-header-cell *matHeaderCellDef
*ngIf="connectorControl.get('configuration').hasError('required')"> [ngStyle.gt-md]="{ minWidth: '144px', maxWidth: '144px', width: '144px'}">
{{'gateway.connector-configuration-required' | translate }} Actions
</mat-error> </mat-header-cell>
</mat-form-field> <mat-cell *matCellDef="let attribute"
<mat-form-field fxFlex="calc(50%-15px)"> [ngStyle.gt-md]="{ minWidth: '144px', maxWidth: '144px', width: '144px'}">
<mat-label translate>gateway.connector-type</mat-label> <div fxHide fxShow.gt-md fxFlex fxLayout="row" fxLayoutAlign="end">
<mat-select formControlName="type" required> <button mat-icon-button
<mat-option *ngFor="let type of gatewayConnectorDefaultTypes" [value]="type">{{type}}</mat-option> matTooltip="RPC"
</mat-select> matTooltipPosition="above"
<mat-error (click)="connectorRpc(attribute, $event)">
*ngIf="connectorControl.get('type').hasError('required')"> <mat-icon>private_connectivity</mat-icon>
{{'gateway.connector-type-required' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field fxFlex="calc(50%-15px)">
<mat-label translate>gateway.connector-key</mat-label>
<input matInput formControlName="key" required/>
<mat-error
*ngIf="connectorControl.get('key').hasError('required')">
{{'gateway.connector-key-required' | translate }}
</mat-error>
</mat-form-field>
</mat-card>
<div fxLayout="column">
<button mat-icon-button (click)="removeConnector($index)"
matTooltip="{{ 'gateway.statistics.remove' | translate }}"
matTooltipPosition="above">
<mat-icon>close</mat-icon>
</button> </button>
<button mat-icon-button (click)="selectConnector($index)" <button mat-icon-button
matTooltip="{{ 'gateway.select-connector' | translate }}" matTooltip="Logs"
matTooltipPosition="above"> matTooltipPosition="above"
<mat-icon>code</mat-icon> (click)="connectorLogs(attribute, $event)">
<mat-icon>list</mat-icon>
</button> </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>
</div> </mat-cell>
</mat-expansion-panel> </ng-container>
</mat-accordion> <mat-header-row [ngClass]="{'mat-row-select': true}"
<button mat-raised-button color="primary" *matHeaderRowDef="displayedColumns; sticky: true"></mat-header-row>
style="margin-top: 15px" <mat-row [ngClass]="{'mat-row-select': true}"
type="button" *matRowDef="let attribute; columns: displayedColumns;" (click)="selectConnector(attribute)"></mat-row>
(click)="addConnector(true)"> </table>
{{ 'gateway.connector-add' | translate }} <mat-divider></mat-divider>
</button> </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> </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> </div>
</form> </form>

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

@ -17,6 +17,31 @@
width: 100%; width: 100%;
height: 100%; height: 100%;
display: block; 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 { .mat-toolbar {
background: transparent; background: transparent;
@ -24,7 +49,9 @@
} }
.mat-mdc-form-field { .mat-mdc-form-field {
margin-right: 15px; flex-grow: 1;
//min-width: 200px;
//flex-shrink: 1;
} }
mat-expansion-panel { mat-expansion-panel {
@ -55,5 +82,9 @@
.action-btns { .action-btns {
margin: 10px 10px 0; 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. /// 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 { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { Router } from '@angular/router'; 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 { EntityId } from '@shared/models/id/entity-id';
import { MatDialog, MatDialogRef } from '@angular/material/dialog'; import { MatDialog, MatDialogRef } from '@angular/material/dialog';
import { AttributeService } from '@core/http/attribute.service'; import { AttributeService } from '@core/http/attribute.service';
import { DeviceService } from '@core/http/device.service'; import { DeviceService } from '@core/http/device.service';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { forkJoin } from 'rxjs'; import { forkJoin, merge } from 'rxjs';
import { AttributeScope } from '@shared/models/telemetry/telemetry.models'; 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 { export interface gatewayConnector {
name: string; name: string;
type: string; type: string;
configuration: string; configuration?: string;
jsonConfiguration: string; configurationJson: string;
key: string; log_level: string;
key?: string;
} }
@Component({ @Component({
@ -41,39 +55,58 @@ export interface gatewayConnector {
templateUrl: './gateway-connectors.component.html', templateUrl: './gateway-connectors.component.html',
styleUrls: ['./gateway-connectors.component.scss'] 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> = gatewayConnectorDefaultTypes: Array<string> =
['mqtt', ['mqtt',
'modbus', 'modbus',
'grpc', 'grpc',
'opcua', 'opcua',
'opcua_asyncio', 'opcua_asyncio',
'ble', 'ble',
'request', 'request',
'can', 'can',
'bacnet', 'bacnet',
'odbc', 'odbc',
'rest', 'rest',
'snmp', 'snmp',
'ftp', 'ftp',
'socket', 'socket',
'xmpp', 'xmpp',
'ocpp' 'ocpp',
]; 'custom'
];
selectedConnector: number;
@Input() @Input()
device: EntityId; ctx: WidgetContext;
@Input() @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, constructor(protected router: Router,
protected store: Store<AppState>, protected store: Store<AppState>,
@ -81,111 +114,244 @@ export class GatewayConnectorComponent implements OnInit {
protected translate: TranslateService, protected translate: TranslateService,
protected attributeService: AttributeService, protected attributeService: AttributeService,
protected deviceService: DeviceService, protected deviceService: DeviceService,
protected dialogService: DialogService,
private telemetryWsService: TelemetryWebsocketService,
private zone: NgZone,
private cd: ChangeDetectorRef, private cd: ChangeDetectorRef,
public dialog: MatDialog) { 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({ initialConnector: gatewayConnector;
connectors: this.fb.array([], [Validators.required])
}); ngAfterViewInit() {
this.gatewayConnectorsGroup.valueChanges.subscribe(_=>{ console.log(this.device)
this.cd.detectChanges(); merge(this.sort.sortChange)
}); .pipe(
this.getConnectorsData(); 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 { saveConnector(): void {
if (this.dialogRef) { const value = this.connectorForm.value;
this.dialogRef.close(); 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 { resetSortAndFilter(update: boolean = true) {
forkJoin([ this.textSearchMode = false;
this.attributeService.getEntityAttributes(this.device, AttributeScope.SHARED_SCOPE,['implementedConnectors']), this.pageLink.textSearch = null;
this.attributeService.getEntityAttributes(this.device, AttributeScope.SERVER_SCOPE,['implementedConnectors']), if (this.viewsInited) {
this.attributeService.getEntityAttributes(this.device, AttributeScope.SHARED_SCOPE,['connectorTypes']) const sortable = this.sort.sortables.get('key');
]).subscribe(attributes=>{ this.sort.active = sortable.id;
if (attributes[0].length) { this.sort.direction = 'asc';
attributes[0][0].value.forEach(connector=>this.addConnector(true, connector)); if (update) {
} this.updateData(true);
if (attributes[1].length) {
attributes[1][0].value.forEach(connector=>this.addConnector(false, connector));
} }
if(attributes[2].length) { }
attributes[1][0].value.forEach(type=> {
if (this.gatewayConnectorDefaultTypes.indexOf(type) === -1) {
this.gatewayConnectorDefaultTypes.push(type);
}
});
}
this.cd.detectChanges();
});
} }
saveConnectors(): void { updateData(reload: boolean = false) {
const connectors = this.gatewayConnectorsGroup.value.connectors; this.pageLink.sortOrder.property = this.sort.active;
const activeConnectors = connectors.filter(connector=>connector.active); this.pageLink.sortOrder.direction = Direction[this.sort.direction.toUpperCase()];
const inactiveConnectors = connectors.filter(connector=>!connector.active); this.attributeDataSource.loadAttributes(this.device, AttributeScope.SHARED_SCOPE, this.pageLink, reload).subscribe(data => {
forkJoin([ this.dataSource.data = data.data.filter(value => this.activeConnectors.includes(value.key) || this.inactiveConnectors.includes(value.key));
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();
}
}); });
} }
connectorsFormArray(): FormArray { addAttribute(): void {
return this.gatewayConnectorsGroup.get('connectors') as FormArray; if (this.connectorForm.disabled) {
this.connectorForm.enable();
}
this.clearOutConnectorForm();
} }
addConnector(active?: boolean,connector?: gatewayConnector) { clearOutConnectorForm(): void {
const newConnector = this.fb.group({ this.connectorForm.setValue({
active: [!!active], name: '',
name: [connector?.name || '', [Validators.required]], type: this.gatewayConnectorDefaultTypes[0],
type: [connector?.type || '', [Validators.required]], log_level: GatewayLogLevel.info,
configuration: [connector?.configuration || '', [Validators.required]], key: 'auto',
jsonConfiguration: [connector?.jsonConfiguration || {}, [Validators.required]], configurationJson: {}
key: [connector?.key || 'auto', [Validators.required]], })
}); this.initialConnector = null;
const connectorsFormArray = this.connectorsFormArray(); this.connectorForm.markAsPristine();
connectorsFormArray.push(newConnector);
this.selectConnector(connectorsFormArray.length-1);
} }
removeConnector(index: number): void { selectConnector(attribute): void {
this.connectorsFormArray().removeAt(index); if (this.connectorForm.disabled) {
if (index !== 0) { this.connectorForm.enable();
this.selectedConnector = index - 1; }
} else { const connector = attribute.value;
this.selectedConnector = undefined; if (!connector.key) {
connector.key = 'auto';
} }
if (connector.configuration) {
delete connector.configuration;
}
this.initialConnector = connector;
this.connectorForm.setValue(connector);
} }
selectConnector(index: number): void { showToast(message: string) {
this.selectedConnector = index; 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 { deleteConnector(attribute: AttributeData, $event: Event): void {
return this.connectorsFormArray().at(selectedConnector).get('jsonConfiguration') as FormControl; 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{ connectorRpc(attribute: AttributeData, $event: Event): void {
const connectorValues = connectorControl.value; if ($event) {
const activeTxt = this.translate.instant(connectorValues.active ? 'gateway.connectors-active' : 'gateway.connectors-inactive'); $event.stopPropagation();
let title = `${connectorValues.name} | ${connectorValues.type} `;
if (connectorValues.type === 'grpc') {
title += `| ${connectorValues.key} `;
} }
title += `| ${activeTxt}`; console.log(attribute);
return title; 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> </mat-toolbar>
<div mat-dialog-content tb-toast toastTarget="activationLinkDialogContent"> <div mat-dialog-content tb-toast toastTarget="activationLinkDialogContent">
<div class="mat-content" fxLayout="column"> <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-form-field class="mat-block tb-value-type" style="flex-grow: 0">
<mat-label translate>gateway.configuration-delete-dialog-input</mat-label> <mat-label translate>gateway.configuration-delete-dialog-input</mat-label>
<input matInput [formControl]="gatewayForm" required/> <input matInput [formControl]="gatewayForm" required/>

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

@ -16,7 +16,12 @@
--> -->
<div class="tb-container"> <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" <ng-container #flow="flow"
[flowConfig]="{allowDuplicateUploads: true}"> [flowConfig]="{allowDuplicateUploads: true}">
<div class="tb-file-select-container"> <div class="tb-file-select-container">
@ -37,9 +42,11 @@
<mat-icon>cloud_upload</mat-icon> <mat-icon>cloud_upload</mat-icon>
<span>{{ dropLabel }}</span> <span>{{ dropLabel }}</span>
<button type="button" mat-button color="primary" class="browse-file"> <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> </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> </div>
</div> </div>

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

@ -93,4 +93,8 @@ $previewSize: 100px !default;
padding: 0 16px; 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() @Input()
label: string; label: string;
@Input()
hint: string;
@Input() @Input()
accept = '*/*'; 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 { GatewayConfigurationComponent } from '@shared/components/device/gateway-configuration.component';
import { GatewayRemoteConfigurationDialogComponent } from '@shared/components/dialog/gateway-remote-configuration-dialog'; import { GatewayRemoteConfigurationDialogComponent } from '@shared/components/dialog/gateway-remote-configuration-dialog';
import { GatewayConnectorComponent } from '@shared/components/device/gateway-connectors.component'; import { GatewayConnectorComponent } from '@shared/components/device/gateway-connectors.component';
import { GatewayLogsComponent } from '@shared/components/device/gateway-logs.component';
import { GatewayStatisticsComponent } from '@shared/components/device/gateway-statistics.component';
import { GatewayServiceRPCComponent } from '@shared/components/device/gateway-service-rpc.component';
export const MarkedOptionsFactory = (markedOptionsService: MarkedOptionsService) => markedOptionsService; export const MarkedOptionsFactory = (markedOptionsService: MarkedOptionsService) => markedOptionsService;
@ -247,6 +250,9 @@ export const MarkedOptionsFactory = (markedOptionsService: MarkedOptionsService)
DeviceGatewayCommandComponent, DeviceGatewayCommandComponent,
GatewayConfigurationComponent, GatewayConfigurationComponent,
GatewayConnectorComponent, GatewayConnectorComponent,
GatewayLogsComponent,
GatewayStatisticsComponent,
GatewayServiceRPCComponent,
GatewayRemoteConfigurationDialogComponent, GatewayRemoteConfigurationDialogComponent,
TbCheckboxComponent, TbCheckboxComponent,
TbSnackBarComponent, TbSnackBarComponent,
@ -417,6 +423,9 @@ export const MarkedOptionsFactory = (markedOptionsService: MarkedOptionsService)
DeviceGatewayCommandComponent, DeviceGatewayCommandComponent,
GatewayConfigurationComponent, GatewayConfigurationComponent,
GatewayConnectorComponent, GatewayConnectorComponent,
GatewayLogsComponent,
GatewayStatisticsComponent,
GatewayServiceRPCComponent,
GatewayRemoteConfigurationDialogComponent, GatewayRemoteConfigurationDialogComponent,
TbCheckboxComponent, TbCheckboxComponent,
TbErrorComponent, 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}}'?", "create-new-gateway-text": "Are you sure you want create a new gateway with name: '{{gatewayName}}'?",
"created-time": "Created time", "created-time": "Created time",
"configuration-delete-dialog-header": "Configurations will be deleted", "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": "Gateway name",
"configuration-delete-dialog-input-required": "Gateway name is mandatory", "configuration-delete-dialog-input-required": "Gateway name is mandatory",
"configuration-delete-dialog-confirm": "Turn Off", "configuration-delete-dialog-confirm": "Turn Off",
@ -2609,6 +2609,7 @@
"stats-send-period-in-sec": "Stats send period in seconds", "stats-send-period-in-sec": "Stats send period in seconds",
"statistics": { "statistics": {
"statistics": "Statistics", "statistics": "Statistics",
"commands": "Commands",
"send-period": "Statistic send period (in ms)", "send-period": "Statistic send period (in ms)",
"send-period-required": "Statistic send period is required", "send-period-required": "Statistic send period is required",
"send-period-min": "Statistic send period can not be less then 0", "send-period-min": "Statistic send period can not be less then 0",
@ -2689,7 +2690,45 @@
"toggle-fullscreen": "Toggle fullscreen", "toggle-fullscreen": "Toggle fullscreen",
"transformer-json-config": "Configuration JSON*", "transformer-json-config": "Configuration JSON*",
"update-config": "Add/update 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": { "grid": {
"delete-item-title": "Are you sure you want to delete this item?", "delete-item-title": "Are you sure you want to delete this item?",
@ -5247,6 +5286,11 @@
"font-weight-lighter": "Lighter", "font-weight-lighter": "Lighter",
"color": "Color", "color": "Color",
"shadow-color": "Shadow color" "shadow-color": "Shadow color"
},
"action-buttons": {
"button-type": "Button type",
"button-class": "Button class",
"alignment": "Buttons alignment"
} }
}, },
"icon": { "icon": {
@ -5273,7 +5317,8 @@
"node-selected": "On node selected", "node-selected": "On node selected",
"element-click": "On HTML element click", "element-click": "On HTML element click",
"pie-slice-click": "On slice 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" : { "paginator" : {

Loading…
Cancel
Save