Browse Source

Merge pull request #11251 from vvlladd28/improvement/gateway-dashboard

Improvement gateway dashboard
pull/11294/head
Igor Kulikov 2 years ago
committed by GitHub
parent
commit
6a27de82bd
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 2
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-config-control/broker-config-control.component.html
  2. 30
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-config-control/broker-config-control.component.ts
  3. 12
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-data-keys-panel/mapping-data-keys-panel.component.html
  4. 6
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table/mapping-table.component.html
  5. 46
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table/mapping-table.component.ts
  6. 28
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.html
  7. 24
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.scss
  8. 116
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.ts
  9. 180
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.html
  10. 44
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.scss
  11. 208
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.ts
  12. 131
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.html
  13. 90
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.scss
  14. 228
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.ts
  15. 62
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-security-config/modbus-security-config.component.html
  16. 163
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-security-config/modbus-security-config.component.ts
  17. 263
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.html
  18. 27
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.scss
  19. 287
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.ts
  20. 362
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.html
  21. 21
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.scss
  22. 243
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.ts
  23. 121
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-values/modbus-values.component.html
  24. 24
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-values/modbus-values.component.scss
  25. 236
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-values/modbus-values.component.ts
  26. 41
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mqtt-basic-config/mqtt-basic-config.component.ts
  27. 125
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc-server-config/opc-server-config.component.html
  28. 8
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc-server-config/opc-server-config.component.scss
  29. 30
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc-server-config/opc-server-config.component.ts
  30. 2
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc-ua-basic-config/opc-ua-basic-config.component.html
  31. 28
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc-ua-basic-config/opc-ua-basic-config.component.ts
  32. 26
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/public-api.ts
  33. 4
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/security-config/security-config.component.ts
  34. 127
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/server-config/server-config.component.html
  35. 8
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/workers-config-control/workers-config-control.component.html
  36. 12
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/workers-config-control/workers-config-control.component.ts
  37. 2
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/add-connector-dialog.component.ts
  38. 34
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.html
  39. 1
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.scss
  40. 37
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.ts
  41. 3
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-configuration.component.ts
  42. 7
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.html
  43. 9
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts
  44. 6
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc-connector.component.html
  45. 32
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc-connector.component.ts
  46. 3
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc.component.ts
  47. 18
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-statistics.component.html
  48. 1
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-statistics.component.scss
  49. 6
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-statistics.component.ts
  50. 315
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts
  51. 0
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/pipes/gateway-help-link.pipe.ts
  52. 42
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/pipes/gateway-port-tooltip.pipe.ts
  53. 55
      ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts
  54. 17
      ui-ngx/src/app/modules/home/pipes/public-api.ts
  55. 2
      ui-ngx/src/app/shared/components/hint-tooltip-icon.component.html
  56. 8
      ui-ngx/src/app/shared/components/hint-tooltip-icon.component.scss
  57. 48
      ui-ngx/src/app/shared/components/table/table-datasource.abstract.ts
  58. 20
      ui-ngx/src/app/shared/directives/ellipsis-chip-list.directive.ts
  59. 116
      ui-ngx/src/app/shared/directives/truncate-with-tooltip.directive.ts
  60. 2
      ui-ngx/src/app/shared/pipe/key-value-not-empty.pipe.ts
  61. 70
      ui-ngx/src/assets/locale/locale.constant-en_US.json
  62. 112
      ui-ngx/src/assets/metadata/connector-default-configs/modbus.json

2
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-config-control/broker-config-control.component.html

@ -42,7 +42,7 @@
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="portErrorTooltip"
[matTooltip]="brokerConfigFormGroup.get('port') | getGatewayPortTooltip"
*ngIf="(brokerConfigFormGroup.get('port').hasError('required') ||
brokerConfigFormGroup.get('port').hasError('min') ||
brokerConfigFormGroup.get('port').hasError('max')) &&

30
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-config-control/broker-config-control.component.ts

@ -14,7 +14,7 @@
/// limitations under the License.
///
import { ChangeDetectionStrategy, Component, forwardRef, OnDestroy } from '@angular/core';
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, forwardRef, OnDestroy } from '@angular/core';
import {
ControlValueAccessor,
FormBuilder,
@ -35,8 +35,9 @@ import { SharedModule } from '@shared/shared.module';
import { CommonModule } from '@angular/common';
import { TranslateService } from '@ngx-translate/core';
import { generateSecret } from '@core/utils';
import { SecurityConfigComponent } from '@home/components/widget/lib/gateway/connectors-configuration/public-api';
import { Subject } from 'rxjs';
import { GatewayPortTooltipPipe } from '@home/components/widget/lib/gateway/pipes/gateway-port-tooltip.pipe';
import { SecurityConfigComponent } from '../security-config/security-config.component';
@Component({
selector: 'tb-broker-config-control',
@ -47,6 +48,7 @@ import { Subject } from 'rxjs';
CommonModule,
SharedModule,
SecurityConfigComponent,
GatewayPortTooltipPipe,
],
providers: [
{
@ -72,13 +74,14 @@ export class BrokerConfigControlComponent implements ControlValueAccessor, Valid
private destroy$ = new Subject<void>();
constructor(private fb: FormBuilder,
private cdr: ChangeDetectorRef,
private translate: TranslateService) {
this.brokerConfigFormGroup = this.fb.group({
name: ['', []],
host: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
port: [null, [Validators.required, Validators.min(PortLimits.MIN), Validators.max(PortLimits.MAX)]],
version: [5, []],
clientId: ['', [Validators.pattern(noLeadTrailSpacesRegex)]],
clientId: ['tb_gw_' + generateSecret(5), [Validators.pattern(noLeadTrailSpacesRegex)]],
security: []
});
@ -88,19 +91,6 @@ export class BrokerConfigControlComponent implements ControlValueAccessor, Valid
});
}
get portErrorTooltip(): string {
if (this.brokerConfigFormGroup.get('port').hasError('required')) {
return this.translate.instant('gateway.port-required');
} else if (
this.brokerConfigFormGroup.get('port').hasError('min') ||
this.brokerConfigFormGroup.get('port').hasError('max')
) {
return this.translate.instant('gateway.port-limits-error',
{min: PortLimits.MIN, max: PortLimits.MAX});
}
return '';
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
@ -119,7 +109,13 @@ export class BrokerConfigControlComponent implements ControlValueAccessor, Valid
}
writeValue(brokerConfig: BrokerConfig): void {
this.brokerConfigFormGroup.patchValue(brokerConfig, {emitEvent: false});
const brokerConfigState = {
...brokerConfig,
version: brokerConfig.version || 5,
clientId: brokerConfig.clientId || 'tb_gw_' + generateSecret(5),
};
this.brokerConfigFormGroup.reset(brokerConfigState, {emitEvent: false});
this.cdr.markForCheck();
}
validate(): ValidationErrors | null {

12
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-data-keys-panel/mapping-data-keys-panel.component.html

@ -39,8 +39,8 @@
<div class="tb-form-panel-title" translate>gateway.platform-side</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required"
tb-hint-tooltip-icon="{{ 'gateway.JSONPath-hint' | translate }}" translate>
gateway.key
tb-hint-tooltip-icon="{{ 'gateway.JSONPath-hint' | translate }}">
{{ 'gateway.key' | translate }}
</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
@ -97,8 +97,8 @@
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required"
tb-hint-tooltip-icon="{{ 'gateway.JSONPath-hint' | translate }}" translate>
gateway.value
tb-hint-tooltip-icon="{{ 'gateway.JSONPath-hint' | translate }}">
{{ 'gateway.value' | translate }}
</div>
<mat-form-field fxFlex appearance="outline" subscriptSizing="dynamic" class="tb-flex no-gap">
<input matInput required formControlName="value"
@ -158,8 +158,8 @@
</div>
<div class="tb-form-panel no-border no-padding" *ngIf="keysType === MappingKeysType.RPC_METHODS">
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required" tb-hint-tooltip-icon="{{ 'gateway.hints.method-name' | translate }}" translate>
gateway.method-name
<div class="fixed-title-width tb-required" tb-hint-tooltip-icon="{{ 'gateway.hints.method-name' | translate }}">
{{ 'gateway.method-name' | translate }}
</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">

6
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table/mapping-table.component.html

@ -61,11 +61,11 @@
<table mat-table [dataSource]="dataSource">
<ng-container [matColumnDef]="column.def" *ngFor="let column of mappingColumns; let i = index">
<mat-header-cell *matHeaderCellDef class="table-value-column"
[class.request-column]="mappingType !== mappingTypeEnum.DATA">
[class.request-column]="mappingType === mappingTypeEnum.REQUESTS">
{{ column.title | translate }}
</mat-header-cell>
<mat-cell *matCellDef="let mapping" class="table-value-column"
[class.request-column]="mappingType !== mappingTypeEnum.DATA">
<mat-cell tbTruncateWithTooltip *matCellDef="let mapping" class="table-value-column"
[class.request-column]="mappingType === mappingTypeEnum.REQUESTS">
{{ mapping[column.def] }}
</mat-cell>
</ng-container>

46
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table/mapping-table.component.ts

@ -28,8 +28,8 @@ import {
import { TranslateService } from '@ngx-translate/core';
import { MatDialog } from '@angular/material/dialog';
import { DialogService } from '@core/services/dialog.service';
import { BehaviorSubject, Observable, Subject } from 'rxjs';
import { debounceTime, distinctUntilChanged, map, take, takeUntil } from 'rxjs/operators';
import { Subject } from 'rxjs';
import { debounceTime, distinctUntilChanged, take, takeUntil } from 'rxjs/operators';
import {
ControlValueAccessor,
FormBuilder,
@ -52,12 +52,13 @@ import {
RequestType,
RequestTypesTranslationsMap
} from '@home/components/widget/lib/gateway/gateway-widget.models';
import { DataSource } from '@angular/cdk/collections';
import { MappingDialogComponent } from '@home/components/widget/lib/gateway/dialog/mapping-dialog.component';
import { isDefinedAndNotNull, isUndefinedOrNull } from '@core/utils';
import { coerceBoolean } from '@shared/decorators/coercion';
import { SharedModule } from '@shared/shared.module';
import { CommonModule } from '@angular/common';
import { TruncateWithTooltipDirective } from '@shared/directives/truncate-with-tooltip.directive';
import { TbTableDatasource } from '@shared/components/table/table-datasource.abstract';
@Component({
selector: 'tb-mapping-table',
@ -77,7 +78,7 @@ import { CommonModule } from '@angular/common';
}
],
standalone: true,
imports: [CommonModule, SharedModule]
imports: [CommonModule, SharedModule, TruncateWithTooltipDirective]
})
export class MappingTableComponent implements ControlValueAccessor, Validator, AfterViewInit, OnInit, OnDestroy {
@ -191,7 +192,7 @@ export class MappingTableComponent implements ControlValueAccessor, Validator, A
$event.stopPropagation();
}
const value = isDefinedAndNotNull(index) ? this.mappingFormGroup.at(index).value : {};
this.dialog.open<MappingDialogComponent, MappingInfo, MappingValue>(MappingDialogComponent, {
this.dialog.open<MappingDialogComponent, MappingInfo, ConnectorMapping>(MappingDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
@ -206,7 +207,7 @@ export class MappingTableComponent implements ControlValueAccessor, Validator, A
if (isDefinedAndNotNull(index)) {
this.mappingFormGroup.at(index).patchValue(res);
} else {
this.mappingFormGroup.push(this.fb.group(res));
this.pushDataAsFormArrays([res]);
}
this.mappingFormGroup.markAsDirty();
}
@ -222,7 +223,7 @@ export class MappingTableComponent implements ControlValueAccessor, Validator, A
)
);
}
this.dataSource.loadMappings(tableValue);
this.dataSource.loadData(tableValue);
}
deleteMapping($event: Event, index: number): void {
@ -310,33 +311,8 @@ export class MappingTableComponent implements ControlValueAccessor, Validator, A
}
}
export class MappingDatasource implements DataSource<{[key: string]: any}> {
private mappingSubject = new BehaviorSubject<Array<{[key: string]: any}>>([]);
constructor() {}
connect(): Observable<Array<{[key: string]: any}>> {
return this.mappingSubject.asObservable();
}
disconnect(): void {
this.mappingSubject.complete();
}
loadMappings(mappings: Array<{[key: string]: any}>): void {
this.mappingSubject.next(mappings);
}
isEmpty(): Observable<boolean> {
return this.mappingSubject.pipe(
map((mappings) => !mappings.length)
);
}
total(): Observable<number> {
return this.mappingSubject.pipe(
map((mappings) => mappings.length)
);
export class MappingDatasource extends TbTableDatasource<MappingValue> {
constructor() {
super();
}
}

28
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.html

@ -0,0 +1,28 @@
<!--
Copyright © 2016-2024 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-tab-group [formGroup]="basicFormGroup">
<mat-tab label="{{ 'gateway.general' | translate }}">
<ng-container [ngTemplateOutlet]="generalTabContent"></ng-container>
</mat-tab>
<mat-tab label="{{ 'gateway.master-connections' | translate }}*">
<tb-modbus-master-table formControlName="master"></tb-modbus-master-table>
</mat-tab>
<mat-tab label="{{ 'gateway.server-config' | translate }}">
<tb-modbus-slave-config formControlName="slave"></tb-modbus-slave-config>
</mat-tab>
</mat-tab-group>

24
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.scss

@ -0,0 +1,24 @@
/**
* Copyright © 2016-2024 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 {
height: 100%;
}
:host ::ng-deep {
.mat-mdc-tab-body-content {
overflow: hidden !important;
}
}

116
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.ts

@ -0,0 +1,116 @@
///
/// Copyright © 2016-2024 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 { ChangeDetectionStrategy, Component, forwardRef, Input, OnDestroy, TemplateRef } from '@angular/core';
import {
ControlValueAccessor,
FormBuilder,
FormGroup,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
ValidationErrors,
Validator,
} from '@angular/forms';
import { ConnectorType, ModbusBasicConfig } from '@home/components/widget/lib/gateway/gateway-widget.models';
import { SharedModule } from '@shared/shared.module';
import { CommonModule } from '@angular/common';
import { takeUntil } from 'rxjs/operators';
import { Subject } from 'rxjs';
import { EllipsisChipListDirective } from '@shared/directives/ellipsis-chip-list.directive';
import { ModbusSlaveConfigComponent } from '../modbus-slave-config/modbus-slave-config.component';
import { ModbusMasterTableComponent } from '../modbus-master-table/modbus-master-table.component';
@Component({
selector: 'tb-modbus-basic-config',
templateUrl: './modbus-basic-config.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => ModbusBasicConfigComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => ModbusBasicConfigComponent),
multi: true
}
],
standalone: true,
imports: [
CommonModule,
SharedModule,
ModbusSlaveConfigComponent,
ModbusMasterTableComponent,
EllipsisChipListDirective,
],
styleUrls: ['./modbus-basic-config.component.scss'],
})
export class ModbusBasicConfigComponent implements ControlValueAccessor, Validator, OnDestroy {
@Input() generalTabContent: TemplateRef<any>;
basicFormGroup: FormGroup;
onChange: (value: ModbusBasicConfig) => void;
onTouched: () => void;
private destroy$ = new Subject<void>();
constructor(private fb: FormBuilder) {
this.basicFormGroup = this.fb.group({
master: [],
slave: [],
});
this.basicFormGroup.valueChanges
.pipe(takeUntil(this.destroy$))
.subscribe(value => {
this.onChange(value);
this.onTouched();
});
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
registerOnChange(fn: (value: ModbusBasicConfig) => void): void {
this.onChange = fn;
}
registerOnTouched(fn: () => void): void {
this.onTouched = fn;
}
writeValue(basicConfig: ModbusBasicConfig): void {
const editedBase = {
slave: basicConfig.slave ?? {},
master: basicConfig.master ?? {},
};
this.basicFormGroup.setValue(editedBase, {emitEvent: false});
}
validate(): ValidationErrors | null {
return this.basicFormGroup.valid ? null : {
basicFormGroup: {valid: false}
};
}
}

180
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.html

@ -0,0 +1,180 @@
<!--
Copyright © 2016-2024 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="tb-modbus-keys-panel">
<div class="tb-form-panel no-border no-padding">
<div class="tb-form-panel-title">{{ panelTitle | translate }}{{' (' + keysListFormArray.controls.length + ')'}}</div>
<div class="tb-form-panel no-border no-padding key-panel" *ngIf="keysListFormArray.controls.length; else noKeys">
<div class="tb-form-panel no-border no-padding tb-flex no-flex row center fill-width"
*ngFor="let keyControl of keysListFormArray.controls; trackBy: trackByControlId; let $index = index; let last = last;">
<div class="tb-form-panel stroked tb-flex">
<ng-container [formGroup]="keyControl">
<mat-expansion-panel class="tb-settings" [expanded]="last">
<mat-expansion-panel-header fxLayout="row wrap">
<mat-panel-title>
<div class="title-container">
<span *ngIf="isMaster else tagName">
{{ keyControl.get('tag').value }}{{ '-' }}{{ keyControl.get('value').value }}
</span>
<ng-template #tagName>{{ keyControl.get('tag').value }}</ng-template>
</div>
</mat-panel-title>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<div class="tb-form-panel stroked">
<div class="tb-form-panel-title" translate>gateway.platform-side</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required" translate>
gateway.key
</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="tag" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.key-required') | translate"
*ngIf="keyControl.get('tag').hasError('required') &&
keyControl.get('tag').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
</div>
</div>
<div class="tb-form-panel stroked">
<div class="tb-form-panel-title" translate>gateway.connector-side</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" translate>
gateway.type
</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="type">
<mat-option *ngFor="let type of modbusDataTypes" [value]="type">{{ type }}</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
<div *ngIf="withFunctionCode" class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" translate>gateway.function-code</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="functionCode">
<mat-option
*ngFor="let code of functionCodesMap.get(keyControl.get('id').value) || defaultFunctionCodes"
[value]="code"
>
{{ ModbusFunctionCodeTranslationsMap.get(code) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" translate>gateway.objects-count</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input
matInput
type="number"
min="1"
max="50000"
name="value"
formControlName="objectsCount"
placeholder="{{ 'gateway.set' | translate }}"
[readonly]="!editableDataTypes.includes(keyControl.get('type').value)"
/>
</mat-form-field>
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required" translate>gateway.address</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput type="number" min="0" max="50000" name="value" formControlName="address" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.address-required') | translate"
*ngIf="keyControl.get('address').hasError('required') &&
keyControl.get('address').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
</div>
<div *ngIf="isMaster" class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required" translate>gateway.value</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="value" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.value-required') | translate"
*ngIf="keyControl.get('value').hasError('required') &&
keyControl.get('value').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
</div>
</div>
</ng-template>
</mat-expansion-panel>
</ng-container>
</div>
<button type="button"
mat-icon-button
(click)="deleteKey($event, $index)"
[matTooltip]="deleteKeyTitle | translate"
matTooltipPosition="above">
<mat-icon>delete</mat-icon>
</button>
</div>
</div>
<div>
<button type="button" mat-stroked-button color="primary" (click)="addKey()">
{{ addKeyTitle | translate }}
</button>
</div>
</div>
<ng-template #noKeys>
<div class="tb-flex no-flex center align-center key-panel">
<span class="tb-prompt" translate>{{ noKeysText }}</span>
</div>
</ng-template>
<div class="tb-flex flex-end">
<button mat-button
color="primary"
type="button"
(click)="cancel()">
{{ 'action.cancel' | translate }}
</button>
<button mat-raised-button
color="primary"
type="button"
(click)="applyKeysData()"
[disabled]="keysListFormArray.invalid || !keysListFormArray.dirty">
{{ 'action.apply' | translate }}
</button>
</div>
</div>

44
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.scss

@ -0,0 +1,44 @@
/**
* Copyright © 2016-2024 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 {
.tb-modbus-keys-panel {
width: 77vw;
max-width: 700px;
.title-container {
max-width: 11vw;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap
}
.key-panel {
height: 500px;
overflow: auto;
}
.tb-form-panel {
.mat-mdc-icon-button {
width: 56px;
height: 56px;
padding: 16px;
color: rgba(0, 0, 0, 0.54);
}
}
}
}

208
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.ts

@ -0,0 +1,208 @@
///
/// Copyright © 2016-2024 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, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core';
import {
AbstractControl,
FormArray,
FormGroup,
UntypedFormArray,
UntypedFormBuilder,
UntypedFormGroup,
Validators
} from '@angular/forms';
import { TbPopoverComponent } from '@shared/components/popover.component';
import {
ModbusDataType,
ModbusFunctionCodeTranslationsMap,
ModbusObjectCountByDataType,
ModbusValue,
ModbusValueKey,
noLeadTrailSpacesRegex,
} from '@home/components/widget/lib/gateway/gateway-widget.models';
import { CommonModule } from '@angular/common';
import { SharedModule } from '@shared/shared.module';
import { GatewayHelpLinkPipe } from '@home/components/widget/lib/gateway/pipes/gateway-help-link.pipe';
import { generateSecret } from '@core/utils';
import { coerceBoolean } from '@shared/decorators/coercion';
import { takeUntil } from 'rxjs/operators';
import { Subject } from 'rxjs';
@Component({
selector: 'tb-modbus-data-keys-panel',
templateUrl: './modbus-data-keys-panel.component.html',
styleUrls: ['./modbus-data-keys-panel.component.scss'],
standalone: true,
imports: [
CommonModule,
SharedModule,
GatewayHelpLinkPipe,
]
})
export class ModbusDataKeysPanelComponent implements OnInit, OnDestroy {
@coerceBoolean()
@Input() isMaster = false;
@Input() panelTitle: string;
@Input() addKeyTitle: string;
@Input() deleteKeyTitle: string;
@Input() noKeysText: string;
@Input() keysType: ModbusValueKey;
@Input() values: ModbusValue[];
@Input() popover: TbPopoverComponent<ModbusDataKeysPanelComponent>;
@Output() keysDataApplied = new EventEmitter<Array<ModbusValue>>();
keysListFormArray: FormArray<UntypedFormGroup>;
modbusDataTypes = Object.values(ModbusDataType);
withFunctionCode = true;
functionCodesMap = new Map();
defaultFunctionCodes = [];
readonly editableDataTypes = [ModbusDataType.BYTES, ModbusDataType.BITS, ModbusDataType.STRING];
readonly ModbusFunctionCodeTranslationsMap = ModbusFunctionCodeTranslationsMap;
private destroy$ = new Subject<void>();
private readonly defaultReadFunctionCodes = [3, 4];
private readonly defaultWriteFunctionCodes = [5, 6, 15, 16];
private readonly stringAttrUpdatesWriteFunctionCodes = [6, 16];
constructor(private fb: UntypedFormBuilder) {}
ngOnInit(): void {
this.withFunctionCode = !this.isMaster || (this.keysType !== ModbusValueKey.ATTRIBUTES && this.keysType !== ModbusValueKey.TIMESERIES);
this.keysListFormArray = this.prepareKeysFormArray(this.values);
this.defaultFunctionCodes = this.getDefaultFunctionCodes();
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
trackByControlId(_: number, keyControl: AbstractControl): string {
return keyControl.value.id;
}
addKey(): void {
const dataKeyFormGroup = this.fb.group({
tag: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
value: [{value: '', disabled: !this.isMaster}, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
type: [ModbusDataType.BYTES, [Validators.required]],
address: [null, [Validators.required]],
objectsCount: [1, [Validators.required]],
functionCode: [{ value: this.getDefaultFunctionCodes()[0], disabled: !this.withFunctionCode }, [Validators.required]],
id: [{value: generateSecret(5), disabled: true}],
});
this.observeKeyDataType(dataKeyFormGroup);
this.keysListFormArray.push(dataKeyFormGroup);
}
deleteKey($event: Event, index: number): void {
if ($event) {
$event.stopPropagation();
}
this.keysListFormArray.removeAt(index);
this.keysListFormArray.markAsDirty();
}
cancel(): void {
this.popover.hide();
}
applyKeysData(): void {
this.keysDataApplied.emit(this.keysListFormArray.value);
}
private prepareKeysFormArray(values: ModbusValue[]): UntypedFormArray {
const keysControlGroups: Array<AbstractControl> = [];
if (values) {
values.forEach(value => {
const dataKeyFormGroup = this.createDataKeyFormGroup(value);
this.observeKeyDataType(dataKeyFormGroup);
this.functionCodesMap.set(dataKeyFormGroup.get('id').value, this.getFunctionCodes(value.type));
keysControlGroups.push(dataKeyFormGroup);
});
}
return this.fb.array(keysControlGroups);
}
private createDataKeyFormGroup(modbusValue: ModbusValue): FormGroup {
const { tag, value, type, address, objectsCount, functionCode } = modbusValue;
return this.fb.group({
tag: [tag, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
value: [{ value, disabled: !this.isMaster }, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
type: [type, [Validators.required]],
address: [address, [Validators.required]],
objectsCount: [objectsCount, [Validators.required]],
functionCode: [{ value: functionCode, disabled: !this.withFunctionCode }, [Validators.required]],
id: [{ value: generateSecret(5), disabled: true }],
});
}
private observeKeyDataType(keyFormGroup: FormGroup): void {
keyFormGroup.get('type').valueChanges.pipe(takeUntil(this.destroy$)).subscribe(dataType => {
if (!this.editableDataTypes.includes(dataType)) {
keyFormGroup.get('objectsCount').patchValue(ModbusObjectCountByDataType[dataType], {emitEvent: false});
}
this.updateFunctionCodes(keyFormGroup, dataType);
});
}
private updateFunctionCodes(keyFormGroup: FormGroup, dataType: ModbusDataType): void {
const functionCodes = this.getFunctionCodes(dataType);
this.functionCodesMap.set(keyFormGroup.get('id').value, functionCodes);
if (!functionCodes.includes(keyFormGroup.get('functionCode').value)) {
keyFormGroup.get('functionCode').patchValue(functionCodes[0], {emitEvent: false});
}
}
private getFunctionCodes(dataType: ModbusDataType): number[] {
if (this.keysType === ModbusValueKey.ATTRIBUTES_UPDATES) {
return dataType === ModbusDataType.STRING
? this.stringAttrUpdatesWriteFunctionCodes
: this.defaultWriteFunctionCodes;
}
const functionCodes = [...this.defaultReadFunctionCodes];
if (dataType === ModbusDataType.BITS) {
const bitsFunctionCodes = [1, 2];
functionCodes.push(...bitsFunctionCodes);
functionCodes.sort();
}
if (this.keysType === ModbusValueKey.RPC_REQUESTS) {
functionCodes.push(...this.defaultWriteFunctionCodes);
}
return functionCodes;
}
private getDefaultFunctionCodes(): number[] {
if (this.keysType === ModbusValueKey.ATTRIBUTES_UPDATES) {
return this.defaultWriteFunctionCodes;
}
if (this.keysType === ModbusValueKey.RPC_REQUESTS) {
return [...this.defaultReadFunctionCodes, ...this.defaultWriteFunctionCodes];
}
return this.defaultReadFunctionCodes;
}
}

131
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.html

@ -0,0 +1,131 @@
<!--
Copyright © 2016-2024 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="tb-master-table tb-absolute-fill">
<div fxFlex fxLayout="column" class="tb-master-table-content">
<mat-toolbar class="mat-mdc-table-toolbar" [fxShow]="!textSearchMode">
<div class="mat-toolbar-tools">
<div fxLayout="row" fxLayoutAlign="start center" fxLayout.xs="column" fxLayoutAlign.xs="center start" class="title-container">
<span class="tb-master-table-title">{{ 'gateway.servers-slaves' | translate}}</span>
</div>
<span fxFlex></span>
<button mat-icon-button
(click)="manageSlave($event)"
matTooltip="{{ 'action.add' | translate }}"
matTooltipPosition="above">
<mat-icon>add</mat-icon>
</button>
<button mat-icon-button
(click)="enterFilterMode()"
matTooltip="{{ 'action.search' | translate }}"
matTooltipPosition="above">
<mat-icon>search</mat-icon>
</button>
</div>
</mat-toolbar>
<mat-toolbar class="mat-mdc-table-toolbar" [fxShow]="textSearchMode">
<div class="mat-toolbar-tools">
<button mat-icon-button
matTooltip="{{ 'action.search' | translate }}"
matTooltipPosition="above">
<mat-icon>search</mat-icon>
</button>
<mat-form-field fxFlex>
<mat-label>&nbsp;</mat-label>
<input #searchInput matInput
[formControl]="textSearch"
placeholder="{{ 'common.enter-search' | translate }}"/>
</mat-form-field>
<button mat-icon-button (click)="exitFilterMode()"
matTooltip="{{ 'action.close' | translate }}"
matTooltipPosition="above">
<mat-icon>close</mat-icon>
</button>
</div>
</mat-toolbar>
<div class="table-container">
<table mat-table [dataSource]="dataSource">
<ng-container [matColumnDef]="'name'">
<mat-header-cell *matHeaderCellDef class="table-value-column">
{{ 'gateway.name' | translate }}
</mat-header-cell>
<mat-cell *matCellDef="let mapping" class="table-value-column">
{{ mapping['name'] }}
</mat-cell>
</ng-container>
<ng-container [matColumnDef]="'type'">
<mat-header-cell *matHeaderCellDef class="table-value-column">
{{ 'gateway.client-communication-type' | translate }}
</mat-header-cell>
<mat-cell *matCellDef="let mapping" class="table-value-column">
{{ ModbusProtocolLabelsMap.get(mapping['type']) }}
</mat-cell>
</ng-container>
<ng-container matColumnDef="actions" stickyEnd>
<mat-header-cell *matHeaderCellDef
[ngStyle.gt-md]="{ minWidth: '96px', maxWidth: '96px', width: '96px', textAlign: 'center'}">
</mat-header-cell>
<mat-cell *matCellDef="let mapping; let i = index"
[ngStyle.gt-md]="{ minWidth: '96px', maxWidth: '96px', width: '96px'}">
<div fxHide fxShow.gt-md fxFlex fxLayout="row" fxLayoutAlign="end">
<button mat-icon-button
(click)="manageSlave($event, i)">
<tb-icon>edit</tb-icon>
</button>
<button mat-icon-button
(click)="deleteMapping($event, i)">
<tb-icon>delete</tb-icon>
</button>
</div>
<div fxHide fxShow.lt-lg fxFlex fxLayout="row" fxLayoutAlign="end">
<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
(click)="manageSlave($event, i)">
<tb-icon>edit</tb-icon>
</button>
<button mat-icon-button
(click)="deleteMapping($event, i)">
<tb-icon>delete</tb-icon>
</button>
</mat-menu>
</div>
</mat-cell>
</ng-container>
<mat-header-row [ngClass]="{'mat-row-select': true}" *matHeaderRowDef="['name', 'type', 'actions']; sticky: true"></mat-header-row>
<mat-row *matRowDef="let mapping; columns: ['name', 'type', 'actions']"></mat-row>
</table>
<section [fxShow]="!textSearchMode && (dataSource.isEmpty() | async)" fxLayoutAlign="center center"
class="mat-headline-5 tb-absolute-fill tb-add-new">
<button mat-button class="connector"
(click)="manageSlave($event)">
<mat-icon class="tb-mat-96">add</mat-icon>
<span>{{ 'gateway.add-slave' | translate }}</span>
</button>
</section>
</div>
<span [fxShow]="textSearchMode && (dataSource.isEmpty() | async)"
fxLayoutAlign="center center"
class="no-data-found" translate>
widget.no-data-found
</span>
</div>
</div>

90
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.scss

@ -0,0 +1,90 @@
/**
* Copyright © 2016-2024 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 '../scss/constants';
:host {
width: 100%;
height: 100%;
display: block;
.tb-master-table {
.tb-master-table-content {
width: 100%;
height: 100%;
background: #fff;
overflow: hidden;
.mat-toolbar-tools{
min-height: auto;
}
.title-container{
overflow: hidden;
}
.tb-master-table-title {
padding-right: 20px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.table-container {
overflow: auto;
.mat-mdc-table {
table-layout: fixed;
min-width: 450px;
.table-value-column {
padding: 0 12px;
width: 38%;
}
}
}
}
}
.no-data-found {
height: calc(100% - 120px);
}
@media #{$mat-xs} {
.mat-toolbar {
height: auto;
min-height: 100px;
.tb-master-table-title{
padding-bottom: 5px;
width: 100%;
}
}
}
}
:host ::ng-deep {
mat-cell.tb-value-cell {
cursor: pointer;
.mat-icon {
height: 24px;
width: 24px;
font-size: 24px;
color: #757575
}
}
}

228
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.ts

@ -0,0 +1,228 @@
///
/// Copyright © 2016-2024 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,
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
ElementRef,
forwardRef,
OnDestroy,
OnInit,
ViewChild,
} from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { MatDialog } from '@angular/material/dialog';
import { DialogService } from '@core/services/dialog.service';
import { Subject } from 'rxjs';
import { debounceTime, distinctUntilChanged, take, takeUntil } from 'rxjs/operators';
import {
ControlValueAccessor,
FormArray,
FormBuilder,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
UntypedFormGroup,
ValidationErrors,
Validator,
} from '@angular/forms';
import {
ModbusMasterConfig,
ModbusProtocolLabelsMap,
SlaveConfig
} from '@home/components/widget/lib/gateway/gateway-widget.models';
import { isDefinedAndNotNull, isUndefinedOrNull } from '@core/utils';
import { SharedModule } from '@shared/shared.module';
import { CommonModule } from '@angular/common';
import { ModbusSlaveDialogComponent } from '../modbus-slave-dialog/modbus-slave-dialog.component';
import { TbTableDatasource } from '@shared/components/table/table-datasource.abstract';
@Component({
selector: 'tb-modbus-master-table',
templateUrl: './modbus-master-table.component.html',
styleUrls: ['./modbus-master-table.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => ModbusMasterTableComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => ModbusMasterTableComponent),
multi: true
}
],
standalone: true,
imports: [CommonModule, SharedModule]
})
export class ModbusMasterTableComponent implements ControlValueAccessor, Validator, AfterViewInit, OnInit, OnDestroy {
@ViewChild('searchInput') searchInputField: ElementRef;
textSearchMode = false;
dataSource: SlavesDatasource;
masterFormGroup: UntypedFormGroup;
textSearch = this.fb.control('', {nonNullable: true});
readonly ModbusProtocolLabelsMap = ModbusProtocolLabelsMap;
private onChange: (value: ModbusMasterConfig) => void = () => {};
private onTouched: () => void = () => {};
private destroy$ = new Subject<void>();
constructor(
public translate: TranslateService,
public dialog: MatDialog,
private dialogService: DialogService,
private fb: FormBuilder,
private cdr: ChangeDetectorRef,
) {
this.masterFormGroup = this.fb.group({ slaves: this.fb.array([]) });
this.dataSource = new SlavesDatasource();
}
get slaves(): FormArray {
return this.masterFormGroup.get('slaves') as FormArray;
}
ngOnInit(): void {
this.masterFormGroup.valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe((value) => {
this.updateTableData(value.slaves);
this.onChange(value);
this.onTouched();
});
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
ngAfterViewInit(): void {
this.textSearch.valueChanges.pipe(
debounceTime(150),
distinctUntilChanged((prev, current) => (prev ?? '') === current.trim()),
takeUntil(this.destroy$)
).subscribe(text => this.updateTableData(this.slaves.value, text.trim()));
}
registerOnChange(fn: (value: ModbusMasterConfig) => void): void {
this.onChange = fn;
}
registerOnTouched(fn: () => void): void {
this.onTouched = fn;
}
writeValue(master: ModbusMasterConfig): void {
this.slaves.clear();
this.pushDataAsFormArrays(master.slaves);
}
validate(): ValidationErrors | null {
return this.slaves.controls.length ? null : {
slavesFormGroup: {valid: false}
};
}
enterFilterMode(): void {
this.textSearchMode = true;
this.cdr.detectChanges();
const searchInput = this.searchInputField.nativeElement;
searchInput.focus();
searchInput.setSelectionRange(0, 0);
}
exitFilterMode(): void {
this.updateTableData(this.slaves.value);
this.textSearchMode = false;
this.textSearch.reset();
}
manageSlave($event: Event, index?: number): void {
if ($event) {
$event.stopPropagation();
}
const withIndex = isDefinedAndNotNull(index);
const value = withIndex ? this.slaves.at(index).value : {};
this.dialog.open<ModbusSlaveDialogComponent, any, any>(ModbusSlaveDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
value,
buttonTitle: withIndex ? 'action.add' : 'action.apply'
}
}).afterClosed()
.pipe(take(1), takeUntil(this.destroy$))
.subscribe(res => {
if (res) {
if (withIndex) {
this.slaves.at(index).patchValue(res);
} else {
this.slaves.push(this.fb.control(res));
}
this.masterFormGroup.markAsDirty();
}
});
}
deleteMapping($event: Event, index: number): void {
if ($event) {
$event.stopPropagation();
}
this.dialogService.confirm(
this.translate.instant('gateway.delete-slave-title'),
'',
this.translate.instant('action.no'),
this.translate.instant('action.yes'),
true
).pipe(take(1), takeUntil(this.destroy$)).subscribe((result) => {
if (result) {
this.slaves.removeAt(index);
this.masterFormGroup.markAsDirty();
}
});
}
private updateTableData(data: SlaveConfig[], textSearch?: string): void {
if (textSearch) {
data = data.filter(item =>
Object.values(item).some(value =>
value.toString().toLowerCase().includes(textSearch.toLowerCase())
)
);
}
this.dataSource.loadData(data);
}
private pushDataAsFormArrays(slaves: SlaveConfig[]): void {
if (slaves?.length) {
slaves.forEach((slave: SlaveConfig) => this.slaves.push(this.fb.control(slave)));
}
}
}
export class SlavesDatasource extends TbTableDatasource<SlaveConfig> {
constructor() {
super();
}
}

62
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-security-config/modbus-security-config.component.html

@ -0,0 +1,62 @@
<!--
Copyright © 2016-2024 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="tb-form-panel no-border no-padding" [formGroup]="securityConfigFormGroup">
<div class="tb-form-hint tb-primary-fill">{{ 'gateway.hints.path-in-os' | translate }}</div>
<div class="tb-form-row space-between tb-flex fill-width">
<div class="fixed-title-width" translate>gateway.client-cert-path</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="certfile" placeholder="{{ 'gateway.set' | translate }}"/>
</mat-form-field>
</div>
</div>
<div class="tb-form-row space-between tb-flex fill-width">
<div class="fixed-title-width" translate>gateway.private-key-path</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="keyfile" placeholder="{{ 'gateway.set' | translate }}"/>
</mat-form-field>
</div>
</div>
<div class="tb-form-row space-between tb-flex fill-width">
<div class="fixed-title-width" translate>gateway.password</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput type="password" name="value" formControlName="password" placeholder="{{ 'gateway.set' | translate }}"/>
<div class="tb-flex no-gap align-center fill-height" matSuffix>
<tb-toggle-password class="tb-flex align-center fill-height"></tb-toggle-password>
</div>
</mat-form-field>
</div>
</div>
<div *ngIf="!isMaster" class="tb-form-row space-between tb-flex fill-width">
<div class="fixed-title-width" translate>gateway.server-hostname</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="server_hostname" placeholder="{{ 'gateway.set' | translate }}"/>
</mat-form-field>
</div>
</div>
<div *ngIf="isMaster" class="tb-form-row" fxLayoutAlign="space-between center">
<mat-slide-toggle class="mat-slide" formControlName="reqclicert">
<mat-label>
{{ 'gateway.request-client-certificate' | translate }}
</mat-label>
</mat-slide-toggle>
</div>
</div>

163
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-security-config/modbus-security-config.component.ts

@ -0,0 +1,163 @@
///
/// Copyright © 2016-2024 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 {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
forwardRef,
Input,
OnChanges,
OnDestroy
} from '@angular/core';
import {
ControlValueAccessor,
FormBuilder,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
UntypedFormGroup,
ValidationErrors,
Validator,
Validators
} from '@angular/forms';
import {
ModbusSecurity,
noLeadTrailSpacesRegex,
} from '@home/components/widget/lib/gateway/gateway-widget.models';
import { SharedModule } from '@shared/shared.module';
import { CommonModule } from '@angular/common';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { coerceBoolean } from '@shared/decorators/coercion';
@Component({
selector: 'tb-modbus-security-config',
templateUrl: './modbus-security-config.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => ModbusSecurityConfigComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => ModbusSecurityConfigComponent),
multi: true
}
],
standalone: true,
imports: [
CommonModule,
SharedModule,
]
})
export class ModbusSecurityConfigComponent implements ControlValueAccessor, Validator, OnChanges, OnDestroy {
@coerceBoolean()
@Input() isMaster = false;
securityConfigFormGroup: UntypedFormGroup;
private disabled = false;
private onChange: (value: ModbusSecurity) => void;
private onTouched: () => void;
private destroy$ = new Subject<void>();
constructor(private fb: FormBuilder, private cdr: ChangeDetectorRef) {
this.securityConfigFormGroup = this.fb.group({
certfile: ['', [Validators.pattern(noLeadTrailSpacesRegex)]],
keyfile: ['', [Validators.pattern(noLeadTrailSpacesRegex)]],
password: ['', [Validators.pattern(noLeadTrailSpacesRegex)]],
server_hostname: ['', [Validators.pattern(noLeadTrailSpacesRegex)]],
reqclicert: [{value: false, disabled: true}],
});
this.observeValueChanges();
}
ngOnChanges(): void {
this.updateMasterEnabling();
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
registerOnChange(fn: (value: ModbusSecurity) => void): void {
this.onChange = fn;
}
registerOnTouched(fn: () => void): void {
this.onTouched = fn;
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (this.disabled) {
this.securityConfigFormGroup.disable({emitEvent: false});
} else {
this.securityConfigFormGroup.enable({emitEvent: false});
}
this.updateMasterEnabling();
this.cdr.markForCheck();
}
validate(): ValidationErrors | null {
return this.securityConfigFormGroup.valid ? null : {
securityConfigFormGroup: { valid: false }
};
}
writeValue(securityConfig: ModbusSecurity): void {
const { certfile, password, keyfile, server_hostname } = securityConfig;
const securityState = {
certfile: certfile ?? '',
password: password ?? '',
keyfile: keyfile ?? '',
server_hostname: server_hostname ?? '',
reqclicert: !!securityConfig.reqclicert,
};
this.securityConfigFormGroup.reset(securityState, {emitEvent: false});
}
private updateMasterEnabling(): void {
if (this.isMaster) {
if (!this.disabled) {
this.securityConfigFormGroup.get('reqclicert').enable({emitEvent: false});
}
this.securityConfigFormGroup.get('server_hostname').disable({emitEvent: false});
} else {
if (!this.disabled) {
this.securityConfigFormGroup.get('server_hostname').enable({emitEvent: false});
}
this.securityConfigFormGroup.get('reqclicert').disable({emitEvent: false});
}
}
private observeValueChanges(): void {
this.securityConfigFormGroup.valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe((value: ModbusSecurity) => {
this.onChange(value);
this.onTouched();
});
}
}

263
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.html

@ -0,0 +1,263 @@
<!--
Copyright © 2016-2024 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 [formGroup]="slaveConfigFormGroup" class="slave-container">
<div class="tb-form-panel no-border no-padding padding-top">
<div class="tb-form-hint tb-primary-fill tb-flex center">{{ 'gateway.hints.modbus-server' | translate }}</div>
<div class="tb-form-row" fxLayoutAlign="space-between center">
<mat-slide-toggle class="mat-slide" formControlName="sendDataToThingsBoard">
<mat-label>
{{ 'gateway.enable' | translate }}
</mat-label>
</mat-slide-toggle>
</div>
</div>
<div class="slave-content tb-form-panel no-border no-padding padding-top" >
<div class="tb-flex row space-between align-center no-gap fill-width">
<div class="fixed-title-width" translate>gateway.server-slave-config</div>
<tb-toggle-select formControlName="type" appearance="fill">
<tb-toggle-option *ngFor="let type of modbusProtocolTypes" [value]="type">{{ ModbusProtocolLabelsMap.get(type) }}</tb-toggle-option>
</tb-toggle-select>
</div>
<div class="tb-form-panel no-border no-padding padding-top">
<div *ngIf="protocolType !== ModbusProtocolType.Serial"
class="tb-form-row column-xs"
fxLayoutAlign="space-between center"
>
<div class="fixed-title-width tb-required" translate>gateway.host</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="host" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.host-required') | translate"
*ngIf="slaveConfigFormGroup.get('host').hasError('required')
&& slaveConfigFormGroup.get('host').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
</div>
<div *ngIf="protocolType !== ModbusProtocolType.Serial else serialPort"
class="tb-form-row column-xs"
fxLayoutAlign="space-between center"
>
<div class="fixed-title-width tb-required" translate>gateway.port</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput type="number" min="{{portLimits.MIN}}" max="{{portLimits.MAX}}"
name="value" formControlName="port" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="slaveConfigFormGroup.get('port') | getGatewayPortTooltip"
*ngIf="(slaveConfigFormGroup.get('port').hasError('required') ||
slaveConfigFormGroup.get('port').hasError('min') ||
slaveConfigFormGroup.get('port').hasError('max')) &&
slaveConfigFormGroup.get('port').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
</div>
<ng-template #serialPort>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required" translate>gateway.port</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="serialPort" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="'gateway.port-required' | translate"
*ngIf="slaveConfigFormGroup.get('port').hasError('required') && slaveConfigFormGroup.get('port').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
</div>
</ng-template>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" tb-hint-tooltip-icon="{{ 'gateway.hints.framer-type' | translate }}" translate>
gateway.method
</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="method">
<mat-option *ngFor="let method of protocolType === ModbusProtocolType.Serial ? modbusSerialMethodTypes : modbusMethodTypes"
[value]="method">{{ ModbusMethodLabelsMap.get(method) }}</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required" translate>gateway.unit-id</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput type="number" min="0" name="value" formControlName="unitId" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.unit-id-required') | translate"
*ngIf="slaveConfigFormGroup.get('unitId').hasError('required') &&
slaveConfigFormGroup.get('unitId').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required" translate>gateway.device-name</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="deviceName" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.device-name-required') | translate"
*ngIf="slaveConfigFormGroup.get('deviceName').hasError('required') &&
slaveConfigFormGroup.get('deviceName').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required" translate>gateway.device-profile</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="deviceType" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.device-profile-required') | translate"
*ngIf="slaveConfigFormGroup.get('deviceType').hasError('required') &&
slaveConfigFormGroup.get('deviceType').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" translate>gateway.poll-period</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput type="number" min="0" name="value" formControlName="pollPeriod" placeholder="{{ 'gateway.set' | translate }}"/>
</mat-form-field>
</div>
</div>
<div *ngIf="protocolType === ModbusProtocolType.Serial" class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" translate>gateway.baudrate</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="baudrate">
<mat-option *ngFor="let rate of modbusBaudrates" [value]="rate">{{ rate }}</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
<div class="tb-form-panel stroked">
<mat-expansion-panel class="tb-settings">
<mat-expansion-panel-header>
<mat-panel-title>
<div class="tb-form-panel-title" translate>gateway.advanced-connection-settings</div>
</mat-panel-title>
</mat-expansion-panel-header>
<div class="tb-form-panel no-border no-padding padding-top">
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" translate>gateway.byte-order</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="byteOrder">
<mat-option *ngFor="let order of modbusOrderType" [value]="order">{{ order }}</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
<div *ngIf="protocolType !== ModbusProtocolType.Serial" class="tb-form-panel stroked tb-slide-toggle">
<mat-expansion-panel class="tb-settings" [expanded]="showSecurityControl.value">
<mat-expansion-panel-header fxLayout="row wrap">
<mat-panel-title>
<mat-slide-toggle fxLayoutAlign="center" [formControl]="showSecurityControl" class="mat-slide" (click)="$event.stopPropagation()">
<mat-label>
{{ 'gateway.tls-connection' | translate }}
</mat-label>
</mat-slide-toggle>
</mat-panel-title>
</mat-expansion-panel-header>
<tb-modbus-security-config formControlName="security"></tb-modbus-security-config>
</mat-expansion-panel>
</div>
<ng-container [formGroup]="slaveConfigFormGroup.get('identity')">
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" translate>gateway.vendor-name</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="vendorName" placeholder="{{ 'gateway.set' | translate }}"/>
</mat-form-field>
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" translate>gateway.product-code</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="productCode" placeholder="{{ 'gateway.set' | translate }}"/>
</mat-form-field>
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" translate>gateway.vendor-url</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="vendorUrl" placeholder="{{ 'gateway.set' | translate }}"/>
</mat-form-field>
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" translate>gateway.product-name</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="productName" placeholder="{{ 'gateway.set' | translate }}"/>
</mat-form-field>
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" translate>gateway.model-name</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="modelName" placeholder="{{ 'gateway.set' | translate }}"/>
</mat-form-field>
</div>
</div>
</ng-container>
</div>
</mat-expansion-panel>
</div>
<div class="tb-form-panel stroked">
<div class="tb-form-panel-title" translate>gateway.values</div>
<tb-modbus-values formControlName="values"></tb-modbus-values>
</div>
</div>
</div>

27
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.scss

@ -0,0 +1,27 @@
/**
* Copyright © 2016-2024 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.
*/
$server-config-header-height: 132px;
:host {
.slave-content {
height: calc(100% - #{$server-config-header-height});
overflow: auto;
}
.slave-container {
display: inherit;
}
}

287
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.ts

@ -0,0 +1,287 @@
///
/// Copyright © 2016-2024 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 { ChangeDetectionStrategy, Component, forwardRef, OnDestroy } from '@angular/core';
import {
ControlValueAccessor,
FormBuilder,
FormControl,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
UntypedFormGroup,
ValidationErrors,
Validator,
Validators,
} from '@angular/forms';
import {
ModbusBaudrates,
ModbusMethodLabelsMap,
ModbusMethodType,
ModbusOrderType,
ModbusProtocolLabelsMap,
ModbusProtocolType,
ModbusRegisterValues,
ModbusSerialMethodType,
ModbusSlave,
noLeadTrailSpacesRegex,
PortLimits,
SlaveConfig,
} from '@home/components/widget/lib/gateway/gateway-widget.models';
import { SharedModule } from '@shared/shared.module';
import { CommonModule } from '@angular/common';
import { Subject } from 'rxjs';
import { startWith, takeUntil } from 'rxjs/operators';
import { GatewayPortTooltipPipe } from '@home/components/widget/lib/gateway/pipes/gateway-port-tooltip.pipe';
import { ModbusSecurityConfigComponent } from '../modbus-security-config/modbus-security-config.component';
import { ModbusValuesComponent, } from '../modbus-values/modbus-values.component';
import { isEqual } from '@core/utils';
@Component({
selector: 'tb-modbus-slave-config',
templateUrl: './modbus-slave-config.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => ModbusSlaveConfigComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => ModbusSlaveConfigComponent),
multi: true
}
],
standalone: true,
imports: [
CommonModule,
SharedModule,
ModbusValuesComponent,
ModbusSecurityConfigComponent,
GatewayPortTooltipPipe,
],
styleUrls: ['./modbus-slave-config.component.scss'],
})
export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validator, OnDestroy {
slaveConfigFormGroup: UntypedFormGroup;
showSecurityControl: FormControl<boolean>;
ModbusProtocolLabelsMap = ModbusProtocolLabelsMap;
ModbusMethodLabelsMap = ModbusMethodLabelsMap;
portLimits = PortLimits;
readonly modbusProtocolTypes = Object.values(ModbusProtocolType);
readonly modbusMethodTypes = Object.values(ModbusMethodType);
readonly modbusSerialMethodTypes = Object.values(ModbusSerialMethodType);
readonly modbusOrderType = Object.values(ModbusOrderType);
readonly ModbusProtocolType = ModbusProtocolType;
readonly modbusBaudrates = ModbusBaudrates;
private readonly serialSpecificControlKeys = ['serialPort', 'baudrate'];
private readonly tcpUdpSpecificControlKeys = ['port', 'security', 'host'];
private onChange: (value: SlaveConfig) => void;
private onTouched: () => void;
private destroy$ = new Subject<void>();
constructor(private fb: FormBuilder) {
this.showSecurityControl = this.fb.control(false);
this.slaveConfigFormGroup = this.fb.group({
type: [ModbusProtocolType.TCP],
host: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
port: [null, [Validators.required, Validators.min(PortLimits.MIN), Validators.max(PortLimits.MAX)]],
serialPort: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
method: [ModbusMethodType.SOCKET],
unitId: [0, [Validators.required]],
baudrate: [this.modbusBaudrates[0]],
deviceName: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
deviceType: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
pollPeriod: [5000],
sendDataToThingsBoard: [false],
byteOrder:[ModbusOrderType.BIG],
security: [],
identity: this.fb.group({
vendorName: ['', [Validators.pattern(noLeadTrailSpacesRegex)]],
productCode: ['', [Validators.pattern(noLeadTrailSpacesRegex)]],
vendorUrl: ['', [Validators.pattern(noLeadTrailSpacesRegex)]],
productName: ['', [Validators.pattern(noLeadTrailSpacesRegex)]],
modelName: ['', [Validators.pattern(noLeadTrailSpacesRegex)]],
}),
values: [],
});
this.observeValueChanges();
this.observeTypeChange();
this.observeFormEnable();
this.observeShowSecurity();
}
get isSlaveEnabled(): boolean {
return this.slaveConfigFormGroup.get('sendDataToThingsBoard').value;
}
get protocolType(): ModbusProtocolType {
return this.slaveConfigFormGroup.get('type').value;
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
registerOnChange(fn: (value: SlaveConfig) => void): void {
this.onChange = fn;
}
registerOnTouched(fn: () => void): void {
this.onTouched = fn;
}
validate(): ValidationErrors | null {
return this.slaveConfigFormGroup.valid ? null : {
slaveConfigFormGroup: { valid: false }
};
}
writeValue(slaveConfig: ModbusSlave): void {
this.showSecurityControl.patchValue(!!slaveConfig.security && !isEqual(slaveConfig.security, {}));
this.updateSlaveConfig(slaveConfig);
this.updateFormEnableState(slaveConfig.sendDataToThingsBoard);
}
private observeValueChanges(): void {
this.slaveConfigFormGroup.valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe((value: SlaveConfig) => {
if (value.type === ModbusProtocolType.Serial) {
value.port = value.serialPort;
delete value.serialPort;
}
this.onChange(value);
this.onTouched();
});
}
private observeTypeChange(): void {
this.slaveConfigFormGroup.get('type').valueChanges
.pipe(takeUntil(this.destroy$))
.subscribe(type => {
this.updateFormEnableState(this.isSlaveEnabled);
this.updateMethodType(type);
});
}
private updateMethodType(type: ModbusProtocolType): void {
if (this.slaveConfigFormGroup.get('method').value !== ModbusMethodType.RTU) {
this.slaveConfigFormGroup.get('method').patchValue(
type === ModbusProtocolType.Serial
? ModbusSerialMethodType.ASCII
: ModbusMethodType.SOCKET,
{emitEvent: false}
);
}
}
private observeFormEnable(): void {
this.slaveConfigFormGroup.get('sendDataToThingsBoard').valueChanges
.pipe(startWith(this.isSlaveEnabled), takeUntil(this.destroy$))
.subscribe(value => this.updateFormEnableState(value));
}
private updateFormEnableState(enabled: boolean): void {
if (enabled) {
this.slaveConfigFormGroup.enable({emitEvent: false});
this.showSecurityControl.enable({emitEvent: false});
} else {
this.slaveConfigFormGroup.disable({emitEvent: false});
this.showSecurityControl.disable({emitEvent: false});
this.slaveConfigFormGroup.get('sendDataToThingsBoard').enable({emitEvent: false});
}
this.updateEnablingByProtocol(this.protocolType);
this.updateSecurityEnable(this.showSecurityControl.value);
}
private observeShowSecurity(): void {
this.showSecurityControl.valueChanges
.pipe(takeUntil(this.destroy$))
.subscribe(value => this.updateSecurityEnable(value));
}
private updateSecurityEnable(securityEnabled: boolean): void {
if (securityEnabled && this.isSlaveEnabled && this.protocolType !== ModbusProtocolType.Serial) {
this.slaveConfigFormGroup.get('security').enable({emitEvent: false});
} else {
this.slaveConfigFormGroup.get('security').disable({emitEvent: false});
}
}
private updateEnablingByProtocol(type: ModbusProtocolType): void {
const enableKeys = type === ModbusProtocolType.Serial ? this.serialSpecificControlKeys : this.tcpUdpSpecificControlKeys;
const disableKeys = type === ModbusProtocolType.Serial ? this.tcpUdpSpecificControlKeys : this.serialSpecificControlKeys;
if (this.isSlaveEnabled) {
enableKeys.forEach(key => this.slaveConfigFormGroup.get(key)?.enable({ emitEvent: false }));
}
disableKeys.forEach(key => this.slaveConfigFormGroup.get(key)?.disable({ emitEvent: false }));
}
private updateSlaveConfig(slaveConfig: ModbusSlave): void {
const {
type = ModbusProtocolType.TCP,
method = ModbusMethodType.RTU,
unitId = 0,
deviceName = '',
deviceType = '',
pollPeriod = 5000,
sendDataToThingsBoard = false,
byteOrder = ModbusOrderType.BIG,
security = {},
identity = {
vendorName: '',
productCode: '',
vendorUrl: '',
productName: '',
modelName: '',
},
values = {} as ModbusRegisterValues,
baudrate = this.modbusBaudrates[0],
host = '',
port = null,
} = slaveConfig;
const slaveState: ModbusSlave = {
type,
method,
unitId,
deviceName,
deviceType,
pollPeriod,
sendDataToThingsBoard: !!sendDataToThingsBoard,
byteOrder,
security,
identity,
values,
baudrate,
host: type === ModbusProtocolType.Serial ? '' : host,
port: type === ModbusProtocolType.Serial ? null : port,
serialPort: (type === ModbusProtocolType.Serial ? port : '') as string,
};
this.slaveConfigFormGroup.setValue(slaveState, { emitEvent: false });
}
}

362
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.html

@ -0,0 +1,362 @@
<!--
Copyright © 2016-2024 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="slaves-config-container">
<mat-toolbar color="primary">
<h2>{{ 'gateway.server-slave' | translate }}</h2>
<span fxFlex></span>
<div [tb-help]="modbusHelpLink"></div>
<button mat-icon-button
(click)="cancel()"
type="button">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<div mat-dialog-content [formGroup]="slaveConfigFormGroup" class="tb-form-panel">
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required" translate>gateway.name</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="name" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.name-required') | translate"
*ngIf="slaveConfigFormGroup.get('name').hasError('required') &&
slaveConfigFormGroup.get('name').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
</div>
<div class="stroked tb-form-panel">
<div class="tb-form-panel no-border no-padding padding-top">
<div class="tb-flex row space-between align-center no-gap fill-width">
<div class="fixed-title-width" translate>gateway.server-connection</div>
<tb-toggle-select formControlName="type" appearance="fill">
<tb-toggle-option *ngFor="let type of modbusProtocolTypes" [value]="type">{{ ModbusProtocolLabelsMap.get(type) }}</tb-toggle-option>
</tb-toggle-select>
</div>
<div class="tb-form-panel no-border no-padding padding-top">
<div *ngIf="protocolType !== ModbusProtocolType.Serial"
class="tb-form-row column-xs"
fxLayoutAlign="space-between center"
>
<div class="fixed-title-width tb-required" translate>gateway.host</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="host" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.host-required') | translate"
*ngIf="slaveConfigFormGroup.get('host').hasError('required')
&& slaveConfigFormGroup.get('host').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
</div>
<div *ngIf="protocolType !== ModbusProtocolType.Serial else serialPort"
class="tb-form-row column-xs"
fxLayoutAlign="space-between center"
>
<div class="fixed-title-width tb-required" translate>gateway.port</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput type="number" min="{{portLimits.MIN}}" max="{{portLimits.MAX}}"
name="value" formControlName="port" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="slaveConfigFormGroup.get('port') | getGatewayPortTooltip"
*ngIf="(slaveConfigFormGroup.get('port').hasError('required') ||
slaveConfigFormGroup.get('port').hasError('min') ||
slaveConfigFormGroup.get('port').hasError('max')) &&
slaveConfigFormGroup.get('port').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
</div>
<ng-template #serialPort>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required" translate>gateway.port</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="serialPort" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="'gateway.port-required' | translate"
*ngIf="slaveConfigFormGroup.get('serialPort').hasError('required') &&
slaveConfigFormGroup.get('serialPort').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
</div>
</ng-template>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" tb-hint-tooltip-icon="{{ 'gateway.hints.framer-type' | translate }}" translate>
gateway.method
</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="method">
<mat-option *ngFor="let method of protocolType === ModbusProtocolType.Serial ? modbusSerialMethodTypes : modbusMethodTypes"
[value]="method">{{ ModbusMethodLabelsMap.get(method) }}</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
</div>
<ng-container *ngIf="protocolType === ModbusProtocolType.Serial">
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" translate>gateway.baudrate</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="baudrate">
<mat-option *ngFor="let rate of modbusBaudrates" [value]="rate">{{ rate }}</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" translate>gateway.bytesize</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="bytesize">
<mat-option *ngFor="let size of modbusByteSizes" [value]="size">{{ size }}</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" translate>gateway.stopbits</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput type="number" min="0" name="value" formControlName="stopbits" placeholder="{{ 'gateway.set' | translate }}"/>
</mat-form-field>
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" translate>gateway.parity</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="parity">
<mat-option *ngFor="let parity of modbusParities" [value]="parity">{{ ModbusParityLabelsMap.get(parity) }}</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
<div class="tb-form-row" fxLayoutAlign="space-between center">
<mat-slide-toggle class="mat-slide" formControlName="strict">
<mat-label>
{{ 'gateway.strict' | translate }}
</mat-label>
</mat-slide-toggle>
</div>
</ng-container>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required" translate>gateway.unit-id</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput type="number" min="0" name="value" formControlName="unitId" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.unit-id-required') | translate"
*ngIf="slaveConfigFormGroup.get('unitId').hasError('required') &&
slaveConfigFormGroup.get('unitId').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required" translate>gateway.device-name</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="deviceName" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.device-name-required') | translate"
*ngIf="slaveConfigFormGroup.get('deviceName').hasError('required') &&
slaveConfigFormGroup.get('deviceName').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required" translate>gateway.device-profile</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="deviceType" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.device-profile-required') | translate"
*ngIf="slaveConfigFormGroup.get('deviceType').hasError('required') &&
slaveConfigFormGroup.get('deviceType').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
</div>
<div class="tb-form-row" fxLayoutAlign="space-between center">
<mat-slide-toggle class="mat-slide" formControlName="sendDataOnlyOnChange">
<mat-label>
{{ 'gateway.send-data-on-change' | translate }}
</mat-label>
</mat-slide-toggle>
</div>
<div class="tb-form-panel stroked">
<mat-expansion-panel class="tb-settings">
<mat-expansion-panel-header>
<mat-panel-title>
<div class="tb-form-panel-title" translate>gateway.advanced-connection-settings</div>
</mat-panel-title>
</mat-expansion-panel-header>
<div class="tb-form-panel no-border no-padding padding-top">
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" translate>gateway.connection-timeout</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput type="number" min="0" name="value" formControlName="timeout" placeholder="{{ 'gateway.set' | translate }}"/>
</mat-form-field>
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" translate>gateway.byte-order</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="byteOrder">
<mat-option *ngFor="let order of modbusOrderType" [value]="order">{{ order }}</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
<div *ngIf="protocolType !== ModbusProtocolType.Serial" class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" translate>gateway.word-order</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="wordOrder">
<mat-option *ngFor="let order of modbusOrderType" [value]="order">{{ order }}</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
<div *ngIf="protocolType !== ModbusProtocolType.Serial" class="tb-form-panel stroked tb-slide-toggle">
<mat-expansion-panel class="tb-settings" [expanded]="showSecurityControl.value">
<mat-expansion-panel-header fxLayout="row wrap">
<mat-panel-title>
<mat-slide-toggle fxLayoutAlign="center" [formControl]="showSecurityControl" class="mat-slide" (click)="$event.stopPropagation()">
<mat-label>
{{ 'gateway.tls-connection' | translate }}
</mat-label>
</mat-slide-toggle>
</mat-panel-title>
</mat-expansion-panel-header>
<tb-modbus-security-config formControlName="security"></tb-modbus-security-config>
</mat-expansion-panel>
</div>
<div class="tb-form-row" fxLayoutAlign="space-between center">
<mat-slide-toggle class="mat-slide" formControlName="retries">
<mat-label>
{{ 'gateway.retries' | translate }}
</mat-label>
</mat-slide-toggle>
</div>
<div class="tb-form-row" fxLayoutAlign="space-between center">
<mat-slide-toggle class="mat-slide" formControlName="retryOnEmpty">
<mat-label>
{{ 'gateway.retries-on-empty' | translate }}
</mat-label>
</mat-slide-toggle>
</div>
<div class="tb-form-row" fxLayoutAlign="space-between center">
<mat-slide-toggle class="mat-slide" formControlName="retryOnInvalid">
<mat-label>
{{ 'gateway.retries-on-invalid' | translate }}
</mat-label>
</mat-slide-toggle>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width-230" translate>gateway.poll-period</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput type="number" min="0" name="value" formControlName="pollPeriod" placeholder="{{ 'gateway.set' | translate }}"/>
</mat-form-field>
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width-230" translate>gateway.connect-attempt-time</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput type="number" min="0" name="value" formControlName="connectAttemptTimeMs" placeholder="{{ 'gateway.set' | translate }}"/>
</mat-form-field>
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width-230" translate>gateway.connect-attempt-count</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput type="number" min="0" name="value" formControlName="connectAttemptCount" placeholder="{{ 'gateway.set' | translate }}"/>
</mat-form-field>
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width-230" translate>gateway.wait-after-failed-attempts</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput type="number" min="0" name="value" formControlName="waitAfterFailedAttemptsMs" placeholder="{{ 'gateway.set' | translate }}"/>
</mat-form-field>
</div>
</div>
</div>
</mat-expansion-panel>
</div>
<div class="tb-form-panel stroked">
<tb-modbus-values [singleMode]="true" formControlName="values"></tb-modbus-values>
</div>
</div>
</div>
</div>
<div mat-dialog-actions fxLayoutAlign="end center">
<button mat-button color="primary"
type="button"
cdkFocusInitial
(click)="cancel()">
{{ 'action.cancel' | translate }}
</button>
<button mat-raised-button color="primary"
(click)="add()"
[disabled]="slaveConfigFormGroup.invalid || !slaveConfigFormGroup.dirty">
{{ data.buttonTitle | translate }}
</button>
</div>
</div>

21
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.scss

@ -0,0 +1,21 @@
/**
* Copyright © 2016-2024 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 {
.slaves-config-container {
width: 80vw;
max-width: 900px;
}
}

243
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.ts

@ -0,0 +1,243 @@
///
/// Copyright © 2016-2024 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 { ChangeDetectionStrategy, Component, forwardRef, Inject, OnDestroy } from '@angular/core';
import {
FormBuilder,
FormControl,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
UntypedFormGroup,
Validators,
} from '@angular/forms';
import {
ModbusBaudrates,
ModbusByteSizes,
ModbusMethodLabelsMap,
ModbusMethodType,
ModbusOrderType,
ModbusParity,
ModbusParityLabelsMap,
ModbusProtocolLabelsMap,
ModbusProtocolType,
ModbusSerialMethodType,
ModbusSlaveInfo,
noLeadTrailSpacesRegex,
PortLimits,
SlaveConfig,
} from '@home/components/widget/lib/gateway/gateway-widget.models';
import { SharedModule } from '@shared/shared.module';
import { CommonModule } from '@angular/common';
import { Subject } from 'rxjs';
import { ModbusValuesComponent } from '../modbus-values/modbus-values.component';
import { ModbusSecurityConfigComponent } from '../modbus-security-config/modbus-security-config.component';
import { DialogComponent } from '@shared/components/dialog.component';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { Router } from '@angular/router';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { GatewayPortTooltipPipe } from '@home/components/widget/lib/gateway/pipes/gateway-port-tooltip.pipe';
import { takeUntil } from 'rxjs/operators';
import { isEqual } from '@core/utils';
@Component({
selector: 'tb-modbus-slave-dialog',
templateUrl: './modbus-slave-dialog.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => ModbusSlaveDialogComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => ModbusSlaveDialogComponent),
multi: true
}
],
standalone: true,
imports: [
CommonModule,
SharedModule,
ModbusValuesComponent,
ModbusSecurityConfigComponent,
GatewayPortTooltipPipe,
],
styleUrls: ['./modbus-slave-dialog.component.scss'],
})
export class ModbusSlaveDialogComponent extends DialogComponent<ModbusSlaveDialogComponent, SlaveConfig> implements OnDestroy {
slaveConfigFormGroup: UntypedFormGroup;
showSecurityControl: FormControl<boolean>;
portLimits = PortLimits;
readonly modbusProtocolTypes = Object.values(ModbusProtocolType);
readonly modbusMethodTypes = Object.values(ModbusMethodType);
readonly modbusSerialMethodTypes = Object.values(ModbusSerialMethodType);
readonly modbusParities = Object.values(ModbusParity);
readonly modbusByteSizes = ModbusByteSizes;
readonly modbusBaudrates = ModbusBaudrates;
readonly modbusOrderType = Object.values(ModbusOrderType);
readonly ModbusProtocolType = ModbusProtocolType;
readonly ModbusParityLabelsMap = ModbusParityLabelsMap;
readonly ModbusProtocolLabelsMap = ModbusProtocolLabelsMap;
readonly ModbusMethodLabelsMap = ModbusMethodLabelsMap;
readonly modbusHelpLink =
'https://thingsboard.io/docs/iot-gateway/config/modbus/#section-master-description-and-configuration-parameters';
private readonly serialSpecificControlKeys = ['serialPort', 'baudrate', 'stopbits', 'bytesize', 'parity', 'strict'];
private readonly tcpUdpSpecificControlKeys = ['port', 'security', 'host', 'wordOrder'];
private destroy$ = new Subject<void>();
constructor(
private fb: FormBuilder,
protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: ModbusSlaveInfo,
public dialogRef: MatDialogRef<ModbusSlaveDialogComponent, SlaveConfig>,
) {
super(store, router, dialogRef);
this.showSecurityControl = this.fb.control(false);
this.initializeSlaveFormGroup();
this.updateSlaveFormGroup();
this.updateControlsEnabling(this.data.value.type);
this.observeTypeChange();
this.observeShowSecurity();
this.showSecurityControl.patchValue(!!this.data.value.security && !isEqual(this.data.value.security, {}));
}
get protocolType(): ModbusProtocolType {
return this.slaveConfigFormGroup.get('type').value;
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
cancel(): void {
this.dialogRef.close(null);
}
add(): void {
if (!this.slaveConfigFormGroup.valid) {
return;
}
const { values, type, serialPort, ...rest } = this.slaveConfigFormGroup.value;
const slaveResult = { ...rest, type, ...values };
if (type === ModbusProtocolType.Serial) {
slaveResult.port = serialPort;
}
this.dialogRef.close(slaveResult);
}
private initializeSlaveFormGroup(): void {
this.slaveConfigFormGroup = this.fb.group({
name: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
type: [ModbusProtocolType.TCP],
host: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
port: [null, [Validators.required, Validators.min(PortLimits.MIN), Validators.max(PortLimits.MAX)]],
serialPort: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
method: [ModbusMethodType.SOCKET, [Validators.required]],
baudrate: [this.modbusBaudrates[0]],
stopbits: [1],
bytesize: [ModbusByteSizes[0]],
parity: [ModbusParity.None],
strict: [true],
unitId: [0, [Validators.required]],
deviceName: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
deviceType: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
sendDataOnlyOnChange: [false],
timeout: [35],
byteOrder: [ModbusOrderType.BIG],
wordOrder: [ModbusOrderType.BIG],
retries: [true],
retryOnEmpty: [true],
retryOnInvalid: [true],
pollPeriod: [5000],
connectAttemptTimeMs: [5000],
connectAttemptCount: [5],
waitAfterFailedAttemptsMs: [300000],
values: [{}],
security: [{}],
});
}
private updateSlaveFormGroup(): void {
this.slaveConfigFormGroup.patchValue({
...this.data.value,
port: this.data.value.type === ModbusProtocolType.Serial ? null : this.data.value.port,
serialPort: this.data.value.type === ModbusProtocolType.Serial ? this.data.value.port : '',
values: {
attributes: this.data.value.attributes ?? [],
timeseries: this.data.value.timeseries ?? [],
attributeUpdates: this.data.value.attributeUpdates ?? [],
rpc: this.data.value.rpc ?? [],
}
});
}
private observeTypeChange(): void {
this.slaveConfigFormGroup.get('type').valueChanges
.pipe(takeUntil(this.destroy$))
.subscribe(type => {
this.updateControlsEnabling(type);
this.updateMethodType(type);
});
}
private updateMethodType(type: ModbusProtocolType): void {
if (this.slaveConfigFormGroup.get('method').value !== ModbusMethodType.RTU) {
this.slaveConfigFormGroup.get('method').patchValue(
type === ModbusProtocolType.Serial
? ModbusSerialMethodType.ASCII
: ModbusMethodType.SOCKET,
{emitEvent: false}
);
}
}
private updateControlsEnabling(type: ModbusProtocolType): void {
const [enableKeys, disableKeys] = type === ModbusProtocolType.Serial
? [this.serialSpecificControlKeys, this.tcpUdpSpecificControlKeys]
: [this.tcpUdpSpecificControlKeys, this.serialSpecificControlKeys];
enableKeys.forEach(key => this.slaveConfigFormGroup.get(key)?.enable({ emitEvent: false }));
disableKeys.forEach(key => this.slaveConfigFormGroup.get(key)?.disable({ emitEvent: false }));
this.updateSecurityEnabling(this.showSecurityControl.value);
}
private observeShowSecurity(): void {
this.showSecurityControl.valueChanges
.pipe(takeUntil(this.destroy$))
.subscribe(value => this.updateSecurityEnabling(value));
}
private updateSecurityEnabling(isEnabled: boolean): void {
if (isEnabled && this.protocolType !== ModbusProtocolType.Serial) {
this.slaveConfigFormGroup.get('security').enable({emitEvent: false});
} else {
this.slaveConfigFormGroup.get('security').disable({emitEvent: false});
}
}
}

121
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-values/modbus-values.component.html

@ -0,0 +1,121 @@
<!--
Copyright © 2016-2024 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.
-->
<ng-container *ngIf="singleMode else multipleView">
<div [formGroup]="valuesFormGroup" class="tb-form-panel no-border no-padding padding-top" fxLayout="column">
<ng-container [ngTemplateOutlet]="singleView" [ngTemplateOutletContext]="{$implicit: null}"></ng-container>
</div>
</ng-container>
<ng-template #multipleView>
<mat-tab-group [formGroup]="valuesFormGroup">
<mat-tab *ngFor="let register of modbusRegisterTypes" label="{{ ModbusValuesTranslationsMap.get(register) | translate }}">
<div [formGroup]="valuesFormGroup.get(register)" class="tb-form-panel no-border no-padding padding-top" fxLayout="column">
<ng-container [ngTemplateOutlet]="singleView" [ngTemplateOutletContext]="{$implicit: register}"></ng-container>
</div>
</mat-tab>
</mat-tab-group>
</ng-template>
<ng-template #singleView let-register>
<div class="tb-form-row space-between tb-flex">
<div class="fixed-title-width" translate>gateway.attributes</div>
<div class="tb-flex ellipsis-chips-container">
<mat-chip-listbox [tb-ellipsis-chip-list]="getValueGroup(ModbusValueKey.ATTRIBUTES, register).value" class="tb-flex">
<mat-chip *ngFor="let attribute of getValueGroup(ModbusValueKey.ATTRIBUTES, register).value">
{{ attribute.tag }}
</mat-chip>
<mat-chip class="mat-mdc-chip ellipsis-chip">
<label class="ellipsis-text"></label>
</mat-chip>
</mat-chip-listbox>
<button type="button"
mat-icon-button
color="primary"
[disabled]="disabled"
#attributesButton
(click)="manageKeys($event, attributesButton, ModbusValueKey.ATTRIBUTES, register)">
<tb-icon matButtonIcon>edit</tb-icon>
</button>
</div>
</div>
<div class="tb-form-row space-between tb-flex">
<div class="fixed-title-width" translate>gateway.timeseries</div>
<div class="tb-flex ellipsis-chips-container">
<mat-chip-listbox class="tb-flex" [tb-ellipsis-chip-list]="getValueGroup(ModbusValueKey.TIMESERIES, register).value">
<mat-chip *ngFor="let telemetry of getValueGroup(ModbusValueKey.TIMESERIES, register).value">
{{ telemetry.tag }}
</mat-chip>
<mat-chip class="mat-mdc-chip ellipsis-chip">
<label class="ellipsis-text"></label>
</mat-chip>
</mat-chip-listbox>
<button type="button"
mat-icon-button
color="primary"
[disabled]="disabled"
#telemetryButton
(click)="manageKeys($event, telemetryButton, ModbusValueKey.TIMESERIES, register)">
<tb-icon matButtonIcon>edit</tb-icon>
</button>
</div>
</div>
<div class="tb-form-row space-between tb-flex">
<div class="fixed-title-width" translate>gateway.attribute-updates</div>
<div class="tb-flex ellipsis-chips-container">
<mat-chip-listbox [tb-ellipsis-chip-list]="getValueGroup(ModbusValueKey.ATTRIBUTES_UPDATES, register).value" class="tb-flex">
<mat-chip *ngFor="let attributeUpdate of getValueGroup(ModbusValueKey.ATTRIBUTES_UPDATES, register).value">
{{ attributeUpdate.tag }}
</mat-chip>
<mat-chip class="mat-mdc-chip ellipsis-chip">
<label class="ellipsis-text"></label>
</mat-chip>
</mat-chip-listbox>
<button type="button"
mat-icon-button
[disabled]="disabled"
color="primary"
#attributesUpdatesButton
(click)="manageKeys($event, attributesUpdatesButton, ModbusValueKey.ATTRIBUTES_UPDATES, register)">
<tb-icon matButtonIcon>edit</tb-icon>
</button>
</div>
</div>
<div class="tb-form-row space-between tb-flex">
<div class="fixed-title-width" translate>gateway.rpc-requests</div>
<div class="tb-flex ellipsis-chips-container">
<mat-chip-listbox [tb-ellipsis-chip-list]="getValueGroup(ModbusValueKey.RPC_REQUESTS, register).value" class="tb-flex">
<mat-chip *ngFor="let rpcRequest of getValueGroup(ModbusValueKey.RPC_REQUESTS, register).value">
{{ rpcRequest.tag }}
</mat-chip>
<mat-chip class="mat-mdc-chip ellipsis-chip">
<label class="ellipsis-text"></label>
</mat-chip>
</mat-chip-listbox>
<button type="button"
mat-icon-button
color="primary"
[disabled]="disabled"
#rpcRequestsButton
(click)="manageKeys($event, rpcRequestsButton, ModbusValueKey.RPC_REQUESTS, register)">
<tb-icon matButtonIcon>edit</tb-icon>
</button>
</div>
</div>
</ng-template>

24
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-values/modbus-values.component.scss

@ -0,0 +1,24 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
:host {
.mat-mdc-tab-body-wrapper {
min-height: 320px;
}
}
::ng-deep .mdc-evolution-chip-set__chips {
align-items: center;
}

236
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-values/modbus-values.component.ts

@ -0,0 +1,236 @@
///
/// Copyright © 2016-2024 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 {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
forwardRef,
Input,
OnDestroy,
OnInit,
Renderer2,
ViewContainerRef
} from '@angular/core';
import {
ControlValueAccessor,
FormBuilder,
FormGroup,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
ValidationErrors,
Validator,
} from '@angular/forms';
import {
ModbusKeysAddKeyTranslationsMap,
ModbusKeysDeleteKeyTranslationsMap,
ModbusKeysNoKeysTextTranslationsMap,
ModbusKeysPanelTitleTranslationsMap,
ModbusRegisterTranslationsMap,
ModbusRegisterType,
ModbusRegisterValues,
ModbusValue,
ModbusValueKey,
ModbusValues,
ModbusValuesState,
} from '@home/components/widget/lib/gateway/gateway-widget.models';
import { SharedModule } from '@shared/shared.module';
import { CommonModule } from '@angular/common';
import { takeUntil } from 'rxjs/operators';
import { Subject } from 'rxjs';
import { EllipsisChipListDirective } from '@shared/directives/ellipsis-chip-list.directive';
import { MatButton } from '@angular/material/button';
import { TbPopoverService } from '@shared/components/popover.service';
import { ModbusDataKeysPanelComponent } from '../modbus-data-keys-panel/modbus-data-keys-panel.component';
import { coerceBoolean } from '@shared/decorators/coercion';
@Component({
selector: 'tb-modbus-values',
templateUrl: './modbus-values.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => ModbusValuesComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => ModbusValuesComponent),
multi: true
}
],
standalone: true,
imports: [
CommonModule,
SharedModule,
EllipsisChipListDirective,
],
styleUrls: ['./modbus-values.component.scss']
})
export class ModbusValuesComponent implements ControlValueAccessor, Validator, OnInit, OnDestroy {
@coerceBoolean()
@Input() singleMode = false;
disabled = false;
modbusRegisterTypes: ModbusRegisterType[] = Object.values(ModbusRegisterType);
modbusValueKeys = Object.values(ModbusValueKey);
ModbusValuesTranslationsMap = ModbusRegisterTranslationsMap;
ModbusValueKey = ModbusValueKey;
valuesFormGroup: FormGroup;
private onChange: (value: ModbusValuesState) => void;
private onTouched: () => void;
private destroy$ = new Subject<void>();
constructor(private fb: FormBuilder,
private popoverService: TbPopoverService,
private renderer: Renderer2,
private viewContainerRef: ViewContainerRef,
private cdr: ChangeDetectorRef,
) {}
ngOnInit() {
this.initializeValuesFormGroup();
this.observeValuesChanges();
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
registerOnChange(fn: (value: ModbusValuesState) => void): void {
this.onChange = fn;
}
registerOnTouched(fn: () => void): void {
this.onTouched = fn;
}
writeValue(values: ModbusValuesState): void {
if (this.singleMode) {
this.valuesFormGroup.setValue(this.getSingleRegisterState(values as ModbusValues), { emitEvent: false });
} else {
const { holding_registers, coils_initializer, input_registers, discrete_inputs } = values as ModbusRegisterValues;
this.valuesFormGroup.setValue({
holding_registers: this.getSingleRegisterState(holding_registers),
coils_initializer: this.getSingleRegisterState(coils_initializer),
input_registers: this.getSingleRegisterState(input_registers),
discrete_inputs: this.getSingleRegisterState(discrete_inputs),
}, { emitEvent: false });
}
this.cdr.markForCheck();
}
validate(): ValidationErrors | null {
return this.valuesFormGroup.valid ? null : {
valuesFormGroup: {valid: false}
};
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
this.cdr.markForCheck();
}
getValueGroup(valueKey: ModbusValueKey, register?: ModbusRegisterType): FormGroup {
return register
? this.valuesFormGroup.get(register).get(valueKey) as FormGroup
: this.valuesFormGroup.get(valueKey) as FormGroup;
}
manageKeys($event: Event, matButton: MatButton, keysType: ModbusValueKey, register?: ModbusRegisterType): void {
$event.stopPropagation();
const trigger = matButton._elementRef.nativeElement;
if (this.popoverService.hasPopover(trigger)) {
this.popoverService.hidePopover(trigger);
return;
}
const keysControl = this.getValueGroup(keysType, register);
const ctx = {
values: keysControl.value,
isMaster: !this.singleMode,
keysType,
panelTitle: ModbusKeysPanelTitleTranslationsMap.get(keysType),
addKeyTitle: ModbusKeysAddKeyTranslationsMap.get(keysType),
deleteKeyTitle: ModbusKeysDeleteKeyTranslationsMap.get(keysType),
noKeysText: ModbusKeysNoKeysTextTranslationsMap.get(keysType)
};
const dataKeysPanelPopover = this.popoverService.displayPopover(
trigger,
this.renderer,
this.viewContainerRef,
ModbusDataKeysPanelComponent,
'leftBottom',
false,
null,
ctx,
{},
{},
{},
true
);
dataKeysPanelPopover.tbComponentRef.instance.popover = dataKeysPanelPopover;
dataKeysPanelPopover.tbComponentRef.instance.keysDataApplied.pipe(takeUntil(this.destroy$)).subscribe((keysData: ModbusValue[]) => {
dataKeysPanelPopover.hide();
keysControl.patchValue(keysData);
keysControl.markAsDirty();
this.cdr.markForCheck();
});
}
private initializeValuesFormGroup(): void {
const getValuesFormGroup = () => this.fb.group(this.modbusValueKeys.reduce((acc, key) => {
acc[key] = this.fb.control([[], []]);
return acc;
}, {}));
if (this.singleMode) {
this.valuesFormGroup = getValuesFormGroup();
} else {
this.valuesFormGroup = this.fb.group(
this.modbusRegisterTypes.reduce((registersAcc, register) => {
registersAcc[register] = getValuesFormGroup();
return registersAcc;
}, {})
);
}
}
private observeValuesChanges(): void {
this.valuesFormGroup.valueChanges
.pipe(takeUntil(this.destroy$))
.subscribe(value => {
this.onChange(value);
this.onTouched();
});
}
private getSingleRegisterState(values: ModbusValues): ModbusValues {
return {
attributes: values?.attributes ?? [],
timeseries: values?.timeseries ?? [],
attributeUpdates: values?.attributeUpdates ?? [],
rpc: values?.rpc ?? [],
};
}
}

41
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mqtt-basic-config/mqtt-basic-config.component.ts

@ -25,22 +25,28 @@ import {
Validator,
} from '@angular/forms';
import {
ConnectorBaseConfig,
MappingType,
MQTTBasicConfig,
RequestMappingData,
RequestType,
} from '@home/components/widget/lib/gateway/gateway-widget.models';
import { SharedModule } from '@shared/shared.module';
import { CommonModule } from '@angular/common';
import {
BrokerConfigControlComponent,
MappingTableComponent,
SecurityConfigComponent,
WorkersConfigControlComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/public-api';
import { takeUntil } from 'rxjs/operators';
import { Subject } from 'rxjs';
import { isObject } from 'lodash';
import {
SecurityConfigComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/security-config/security-config.component';
import {
WorkersConfigControlComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/workers-config-control/workers-config-control.component';
import {
BrokerConfigControlComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/broker-config-control/broker-config-control.component';
import {
MappingTableComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/mapping-table/mapping-table.component';
@Component({
selector: 'tb-mqtt-basic-config',
@ -112,17 +118,18 @@ export class MqttBasicConfigComponent implements ControlValueAccessor, Validator
this.onTouched = fn;
}
writeValue(basicConfig: ConnectorBaseConfig): void {
writeValue(basicConfig: MQTTBasicConfig): void {
const { broker, dataMapping = [], requestsMapping } = basicConfig;
const editedBase = {
workers: {
maxNumberOfWorkers: basicConfig.broker?.maxNumberOfWorkers,
maxMessageNumberPerWorker: basicConfig.broker?.maxMessageNumberPerWorker,
},
dataMapping: basicConfig.dataMapping || [],
broker: basicConfig.broker || {},
requestsMapping: Array.isArray(basicConfig.requestsMapping)
? basicConfig.requestsMapping
: this.getRequestDataArray(basicConfig.requestsMapping),
workers: broker && (broker.maxNumberOfWorkers || broker.maxMessageNumberPerWorker) ? {
maxNumberOfWorkers: broker.maxNumberOfWorkers,
maxMessageNumberPerWorker: broker.maxMessageNumberPerWorker,
} : {},
dataMapping: dataMapping || [],
broker: broker || {},
requestsMapping: Array.isArray(requestsMapping)
? requestsMapping
: this.getRequestDataArray(requestsMapping),
};
this.basicFormGroup.setValue(editedBase, {emitEvent: false});

125
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc-server-config/opc-server-config.component.html

@ -0,0 +1,125 @@
<!--
Copyright © 2016-2024 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="tb-form-panel no-border no-padding padding-top" [formGroup]="serverConfigFormGroup">
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" tbTruncateWithTooltip translate>gateway.server-url</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="url" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.server-url-required') | translate"
*ngIf="serverConfigFormGroup.get('url').hasError('required') &&
serverConfigFormGroup.get('url').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" tb-hint-tooltip-icon="{{ 'gateway.hints.opcua-timeout' | translate }}">
<div tbTruncateWithTooltip>{{ 'gateway.timeout' | translate }}</div>
</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput type="number" min="0" name="value" formControlName="timeoutInMillis" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="'gateway.timeout-error' | translate: {min: 1000}"
*ngIf="(serverConfigFormGroup.get('timeoutInMillis').hasError('required') ||
serverConfigFormGroup.get('timeoutInMillis').hasError('min')) &&
serverConfigFormGroup.get('timeoutInMillis').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" tbTruncateWithTooltip translate>gateway.security</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="security">
<mat-option *ngFor="let version of securityPolicyTypes" [value]="version.value">{{ version.name }}</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" tb-hint-tooltip-icon="{{ 'gateway.hints.scan-period' | translate }}">
<div tbTruncateWithTooltip>{{ 'gateway.scan-period' | translate }}</div>
</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput type="number" min="0" name="value"
formControlName="scanPeriodInMillis" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="'gateway.scan-period-error' | translate: {min: 1000}"
*ngIf="(serverConfigFormGroup.get('scanPeriodInMillis').hasError('required') ||
serverConfigFormGroup.get('scanPeriodInMillis').hasError('min')) &&
serverConfigFormGroup.get('scanPeriodInMillis').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" tb-hint-tooltip-icon="{{ 'gateway.hints.sub-check-period' | translate }}">
<div tbTruncateWithTooltip>{{ 'gateway.sub-check-period' | translate }}</div>
</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput type="number" min="0" name="value"
formControlName="subCheckPeriodInMillis" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="'gateway.sub-check-period-error' | translate: {min: 10}"
*ngIf="(serverConfigFormGroup.get('subCheckPeriodInMillis').hasError('required') ||
serverConfigFormGroup.get('subCheckPeriodInMillis').hasError('min')) &&
serverConfigFormGroup.get('subCheckPeriodInMillis').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
</div>
<div class="tb-form-row" fxLayoutAlign="space-between center">
<mat-slide-toggle class="mat-slide" formControlName="enableSubscriptions">
<mat-label tb-hint-tooltip-icon="{{ 'gateway.hints.enable-subscription' | translate }}">
<div tbTruncateWithTooltip>{{ 'gateway.enable-subscription' | translate }}</div>
</mat-label>
</mat-slide-toggle>
</div>
<div class="tb-form-row" fxLayoutAlign="space-between center">
<mat-slide-toggle class="mat-slide" formControlName="showMap">
<mat-label tb-hint-tooltip-icon="{{ 'gateway.hints.show-map' | translate }}">
{{ 'gateway.show-map' | translate }}
</mat-label>
</mat-slide-toggle>
</div>
<tb-security-config formControlName="identity"
[extendCertificatesModel]="true">
</tb-security-config>
</div>

8
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/server-config/server-config.component.scss → ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc-server-config/opc-server-config.component.scss

@ -17,12 +17,4 @@
width: 100%;
height: 100%;
display: block;
.server-conf-field-title {
min-width: 250px;
width: 30%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}

30
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/server-config/server-config.component.ts → ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc-server-config/opc-server-config.component.ts

@ -33,24 +33,27 @@ import {
} from '@home/components/widget/lib/gateway/gateway-widget.models';
import { SharedModule } from '@shared/shared.module';
import { CommonModule } from '@angular/common';
import { SecurityConfigComponent } from '@home/components/widget/lib/gateway/connectors-configuration/public-api';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { TruncateWithTooltipDirective } from '@shared/directives/truncate-with-tooltip.directive';
import {
SecurityConfigComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/security-config/security-config.component';
@Component({
selector: 'tb-server-config',
templateUrl: './server-config.component.html',
styleUrls: ['./server-config.component.scss'],
selector: 'tb-opc-server-config',
templateUrl: './opc-server-config.component.html',
styleUrls: ['./opc-server-config.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => ServerConfigComponent),
useExisting: forwardRef(() => OpcServerConfigComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => ServerConfigComponent),
useExisting: forwardRef(() => OpcServerConfigComponent),
multi: true
}
],
@ -59,9 +62,10 @@ import { takeUntil } from 'rxjs/operators';
CommonModule,
SharedModule,
SecurityConfigComponent,
TruncateWithTooltipDirective,
]
})
export class ServerConfigComponent implements ControlValueAccessor, Validator, OnDestroy {
export class OpcServerConfigComponent implements ControlValueAccessor, Validator, OnDestroy {
securityPolicyTypes = SecurityPolicyTypes;
serverConfigFormGroup: UntypedFormGroup;
@ -112,6 +116,16 @@ export class ServerConfigComponent implements ControlValueAccessor, Validator, O
}
writeValue(serverConfig: ServerConfig): void {
this.serverConfigFormGroup.patchValue(serverConfig, {emitEvent: false});
const { timeoutInMillis, scanPeriodInMillis, enableSubscriptions, subCheckPeriodInMillis, showMap, security } = serverConfig;
const serverConfigState = {
...serverConfig,
timeoutInMillis: timeoutInMillis || 1000,
scanPeriodInMillis: scanPeriodInMillis || 1000,
enableSubscriptions: enableSubscriptions || true,
subCheckPeriodInMillis: subCheckPeriodInMillis || 10,
showMap: showMap || false,
security: security || SecurityPolicy.BASIC128,
};
this.serverConfigFormGroup.reset(serverConfigState, {emitEvent: false});
}
}

2
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc-ua-basic-config/opc-ua-basic-config.component.html

@ -20,7 +20,7 @@
<ng-container [ngTemplateOutlet]="generalTabContent"></ng-container>
</mat-tab>
<mat-tab label="{{ 'gateway.server' | translate }}*">
<tb-server-config formControlName="server"></tb-server-config>
<tb-opc-server-config formControlName="server"></tb-opc-server-config>
</mat-tab>
<mat-tab label="{{ 'gateway.data-mapping' | translate }}*">
<div class="tb-form-panel no-border no-padding padding-top tb-flex fill-height">

28
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc-ua-basic-config/opc-ua-basic-config.component.ts

@ -25,21 +25,29 @@ import {
Validator,
} from '@angular/forms';
import {
ConnectorBaseConfig,
ConnectorType,
MappingType,
OPCBasicConfig,
} from '@home/components/widget/lib/gateway/gateway-widget.models';
import { SharedModule } from '@shared/shared.module';
import { CommonModule } from '@angular/common';
import {
BrokerConfigControlComponent,
MappingTableComponent,
SecurityConfigComponent,
ServerConfigComponent,
WorkersConfigControlComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/public-api';
import { takeUntil } from 'rxjs/operators';
import { Subject } from 'rxjs';
import {
SecurityConfigComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/security-config/security-config.component';
import {
WorkersConfigControlComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/workers-config-control/workers-config-control.component';
import {
BrokerConfigControlComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/broker-config-control/broker-config-control.component';
import {
MappingTableComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/mapping-table/mapping-table.component';
import {
OpcServerConfigComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/opc-server-config/opc-server-config.component';
@Component({
selector: 'tb-opc-ua-basic-config',
@ -65,7 +73,7 @@ import { Subject } from 'rxjs';
WorkersConfigControlComponent,
BrokerConfigControlComponent,
MappingTableComponent,
ServerConfigComponent,
OpcServerConfigComponent,
],
styleUrls: ['./opc-ua-basic-config.component.scss']
})
@ -109,7 +117,7 @@ export class OpcUaBasicConfigComponent implements ControlValueAccessor, Validato
this.onTouched = fn;
}
writeValue(basicConfig: ConnectorBaseConfig): void {
writeValue(basicConfig: OPCBasicConfig): void {
const editedBase = {
server: basicConfig.server || {},
mapping: basicConfig.mapping || [],

26
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/public-api.ts

@ -1,26 +0,0 @@
///
/// Copyright © 2016-2024 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.
///
export * from './mapping-table/mapping-table.component';
export * from './device-info-table/device-info-table.component';
export * from './security-config/security-config.component';
export * from './server-config/server-config.component';
export * from './mapping-data-keys-panel/mapping-data-keys-panel.component';
export * from './type-value-panel/type-value-panel.component';
export * from './broker-config-control/broker-config-control.component';
export * from './workers-config-control/workers-config-control.component';
export * from './opc-ua-basic-config/opc-ua-basic-config.component';
export * from './mqtt-basic-config/mqtt-basic-config.component';

4
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/security-config/security-config.component.ts

@ -16,6 +16,7 @@
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
forwardRef,
Input,
@ -87,7 +88,7 @@ export class SecurityConfigComponent implements ControlValueAccessor, OnInit, On
private destroy$ = new Subject<void>();
constructor(private fb: FormBuilder) {}
constructor(private fb: FormBuilder, private cdr: ChangeDetectorRef) {}
ngOnInit(): void {
this.securityFormGroup = this.fb.group({
@ -127,6 +128,7 @@ export class SecurityConfigComponent implements ControlValueAccessor, OnInit, On
}
this.securityFormGroup.reset(securityInfo, {emitEvent: false});
}
this.cdr.markForCheck();
}
validate(): ValidationErrors | null {

127
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/server-config/server-config.component.html

@ -1,127 +0,0 @@
<!--
Copyright © 2016-2024 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="tb-form-panel no-border no-padding padding-top" [formGroup]="serverConfigFormGroup">
<div class="server-settings">
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="server-conf-field-title" translate>gateway.server-url</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="url" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.server-url-required') | translate"
*ngIf="serverConfigFormGroup.get('url').hasError('required') &&
serverConfigFormGroup.get('url').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="server-conf-field-title" tb-hint-tooltip-icon="{{ 'gateway.hints.opcua-timeout' | translate }}" translate>
gateway.timeout
</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput type="number" min="0" name="value" formControlName="timeoutInMillis" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="'gateway.timeout-error' | translate: {min: 1000}"
*ngIf="(serverConfigFormGroup.get('timeoutInMillis').hasError('required') ||
serverConfigFormGroup.get('timeoutInMillis').hasError('min')) &&
serverConfigFormGroup.get('timeoutInMillis').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="server-conf-field-title" translate>gateway.security</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="security">
<mat-option *ngFor="let version of securityPolicyTypes" [value]="version.value">{{ version.name }}</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="server-conf-field-title" tb-hint-tooltip-icon="{{ 'gateway.hints.scan-period' | translate }}" translate>
gateway.scan-period
</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput type="number" min="0" name="value"
formControlName="scanPeriodInMillis" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="'gateway.scan-period-error' | translate: {min: 1000}"
*ngIf="(serverConfigFormGroup.get('scanPeriodInMillis').hasError('required') ||
serverConfigFormGroup.get('scanPeriodInMillis').hasError('min')) &&
serverConfigFormGroup.get('scanPeriodInMillis').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="server-conf-field-title" tb-hint-tooltip-icon="{{ 'gateway.hints.sub-check-period' | translate }}" translate>
gateway.sub-check-period
</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput type="number" min="0" name="value"
formControlName="subCheckPeriodInMillis" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="'gateway.sub-check-period-error' | translate: {min: 10}"
*ngIf="(serverConfigFormGroup.get('subCheckPeriodInMillis').hasError('required') ||
serverConfigFormGroup.get('subCheckPeriodInMillis').hasError('min')) &&
serverConfigFormGroup.get('subCheckPeriodInMillis').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
</div>
</div>
<div class="tb-form-row" fxLayoutAlign="space-between center">
<mat-slide-toggle class="mat-slide" formControlName="enableSubscriptions">
<mat-label tb-hint-tooltip-icon="{{ 'gateway.hints.enable-subscription' | translate }}">
{{ 'gateway.enable-subscription' | translate }}
</mat-label>
</mat-slide-toggle>
</div>
<div class="tb-form-row" fxLayoutAlign="space-between center">
<mat-slide-toggle class="mat-slide" formControlName="showMap">
<mat-label tb-hint-tooltip-icon="{{ 'gateway.hints.show-map' | translate }}">
{{ 'gateway.show-map' | translate }}
</mat-label>
</mat-slide-toggle>
</div>
<tb-security-config formControlName="identity"
[extendCertificatesModel]="true">
</tb-security-config>
</div>

8
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/workers-config-control/workers-config-control.component.html

@ -18,8 +18,8 @@
<div class="tb-form-panel no-border no-padding padding-top" [formGroup]="workersConfigFormGroup">
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required" [style.width.%]="50"
tb-hint-tooltip-icon="{{ 'gateway.max-number-of-workers-hint' | translate }}" translate>
gateway.max-number-of-workers
tb-hint-tooltip-icon="{{ 'gateway.max-number-of-workers-hint' | translate }}">
<div tbTruncateWithTooltip>{{ 'gateway.max-number-of-workers' | translate }}</div>
</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
@ -40,8 +40,8 @@
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required" [style.width.%]="50"
tb-hint-tooltip-icon="{{ 'gateway.max-messages-queue-for-worker-hint' | translate }}" translate>
gateway.max-messages-queue-for-worker
tb-hint-tooltip-icon="{{ 'gateway.max-messages-queue-for-worker-hint' | translate }}">
<div tbTruncateWithTooltip>{{ 'gateway.max-messages-queue-for-worker' | translate }}</div>
</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">

12
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/workers-config-control/workers-config-control.component.ts

@ -25,7 +25,9 @@ import {
FormBuilder,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
UntypedFormGroup, ValidationErrors, Validator,
UntypedFormGroup,
ValidationErrors,
Validator,
Validators
} from '@angular/forms';
import { SharedModule } from '@shared/shared.module';
@ -33,6 +35,7 @@ import { CommonModule } from '@angular/common';
import { WorkersConfig } from '@home/components/widget/lib/gateway/gateway-widget.models';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { TruncateWithTooltipDirective } from '@shared/directives/truncate-with-tooltip.directive';
@Component({
selector: 'tb-workers-config-control',
@ -42,6 +45,7 @@ import { takeUntil } from 'rxjs/operators';
imports: [
CommonModule,
SharedModule,
TruncateWithTooltipDirective,
],
providers: [
{
@ -91,7 +95,11 @@ export class WorkersConfigControlComponent implements OnDestroy, ControlValueAcc
}
writeValue(workersConfig: WorkersConfig): void {
this.workersConfigFormGroup.patchValue(workersConfig, {emitEvent: false});
const { maxNumberOfWorkers, maxMessageNumberPerWorker } = workersConfig;
this.workersConfigFormGroup.reset({
maxNumberOfWorkers: maxNumberOfWorkers || 100,
maxMessageNumberPerWorker: maxMessageNumberPerWorker || 10,
}, {emitEvent: false});
}
validate(): ValidationErrors | null {

2
ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/add-connector-dialog.component.ts

@ -33,7 +33,7 @@ import {
} from '@home/components/widget/lib/gateway/gateway-widget.models';
import { Subject } from 'rxjs';
import { ResourcesService } from '@core/services/resources.service';
import { takeUntil, tap } from "rxjs/operators";
import { takeUntil, tap } from 'rxjs/operators';
@Component({
selector: 'tb-add-connector-dialog',

34
ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.html

@ -19,7 +19,7 @@
<mat-toolbar color="primary">
<h2>{{ MappingTypeTranslationsMap.get(this.data?.mappingType) | translate}}</h2>
<span fxFlex></span>
<div [tb-help]="helpLinkId()"></div>
<div [tb-help]="HelpLinkByMappingTypeMap.get(this.data.mappingType)"></div>
<button mat-icon-button
(click)="cancel()"
type="button">
@ -57,8 +57,8 @@
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" tb-hint-tooltip-icon="{{ 'gateway.response-topic-Qos-hint' | translate }}" translate>
gateway.mqtt-qos
<div class="fixed-title-width" tb-hint-tooltip-icon="{{ 'gateway.response-topic-Qos-hint' | translate }}">
{{ 'gateway.mqtt-qos' | translate }}
</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
@ -140,8 +140,8 @@
<div class="tb-form-panel no-border no-padding" *ngIf="converterType === ConvertorTypeEnum.CUSTOM">
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required"
tb-hint-tooltip-icon="{{ 'gateway.extension-hint' | translate }}" translate>
gateway.extension
tb-hint-tooltip-icon="{{ 'gateway.extension-hint' | translate }}">
{{ 'gateway.extension' | translate }}
</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
@ -369,8 +369,8 @@
<ng-template [ngSwitchCase]="RequestTypeEnum.ATTRIBUTE_UPDATE">
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required"
tb-hint-tooltip-icon="{{ 'gateway.device-name-filter-hint' | translate }}" translate>
gateway.device-name-filter
tb-hint-tooltip-icon="{{ 'gateway.device-name-filter-hint' | translate }}">
{{ 'gateway.device-name-filter' | translate }}
</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
@ -388,8 +388,8 @@
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required" tb-hint-tooltip-icon="{{ 'gateway.attribute-filter-hint' | translate }}" translate>
gateway.attribute-filter
<div class="fixed-title-width tb-required" tb-hint-tooltip-icon="{{ 'gateway.attribute-filter-hint' | translate }}">
{{ 'gateway.attribute-filter' | translate }}
</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
@ -472,8 +472,8 @@
</tb-toggle-select>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required" tb-hint-tooltip-icon="{{ 'gateway.device-name-filter-hint' | translate }}" translate>
gateway.device-name-filter
<div class="fixed-title-width tb-required" tb-hint-tooltip-icon="{{ 'gateway.device-name-filter-hint' | translate }}">
{{ 'gateway.device-name-filter' | translate }}
</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
@ -491,8 +491,8 @@
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required" tb-hint-tooltip-icon="{{ 'gateway.method-filter-hint' | translate }}" translate>
gateway.method-filter
<div class="fixed-title-width tb-required" tb-hint-tooltip-icon="{{ 'gateway.method-filter-hint' | translate }}">
{{ 'gateway.method-filter' | translate }}
</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
@ -580,8 +580,8 @@
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" tb-hint-tooltip-icon="{{ 'gateway.response-topic-Qos-hint' | translate }}" translate>
gateway.response-topic-Qos
<div class="fixed-title-width" tb-hint-tooltip-icon="{{ 'gateway.response-topic-Qos-hint' | translate }}">
{{ 'gateway.response-topic-Qos' | translate }}
</div>
<mat-form-field class="tb-flex" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="responseTopicQoS">
@ -618,8 +618,8 @@
<ng-template [ngSwitchCase]="MappingType.OPCUA">
<div class="tb-form-row column-xs" fxLayoutAlign="center">
<div class="tb-flex no-flex align-center" translate>
<div class="tb-required" tb-hint-tooltip-icon="{{ 'gateway.device-node-hint' | translate }}" translate>
gateway.device-node
<div class="tb-required" tb-hint-tooltip-icon="{{ 'gateway.device-node-hint' | translate }}">
{{ 'gateway.device-node' | translate }}
</div>
</div>
<div class="tb-flex device-config">

1
ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.scss

@ -47,6 +47,7 @@
.mdc-evolution-chip-set__chips {
justify-content: flex-end;
align-items: center;
flex-wrap: nowrap;
}
}
}

37
ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.ts

@ -24,10 +24,15 @@ import { Router } from '@angular/router';
import {
Attribute,
AttributesUpdate,
ConnectorMapping,
ConnectorMappingFormValue,
ConverterMappingFormValue,
ConvertorType,
ConvertorTypeTranslationsMap,
DataConversionTranslationsMap,
DeviceConnectorMapping,
DeviceInfoType,
HelpLinkByMappingTypeMap,
MappingHintTranslationsMap,
MappingInfo,
MappingKeysAddKeyTranslationsMap,
@ -37,11 +42,11 @@ import {
MappingKeysType,
MappingType,
MappingTypeTranslationsMap,
MappingValue,
noLeadTrailSpacesRegex,
OPCUaSourceTypes,
QualityTypes,
QualityTypeTranslationsMap,
RequestMappingFormValue,
RequestType,
RequestTypesTranslationsMap,
RpcMethod,
@ -55,14 +60,16 @@ import { startWith, takeUntil } from 'rxjs/operators';
import { MatButton } from '@angular/material/button';
import { TbPopoverService } from '@shared/components/popover.service';
import { TranslateService } from '@ngx-translate/core';
import { MappingDataKeysPanelComponent } from '@home/components/widget/lib/gateway/connectors-configuration/public-api';
import {
MappingDataKeysPanelComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/mapping-data-keys-panel/mapping-data-keys-panel.component';
@Component({
selector: 'tb-mapping-dialog',
templateUrl: './mapping-dialog.component.html',
styleUrls: ['./mapping-dialog.component.scss']
})
export class MappingDialogComponent extends DialogComponent<MappingDialogComponent, MappingValue> implements OnDestroy {
export class MappingDialogComponent extends DialogComponent<MappingDialogComponent, ConnectorMapping> implements OnDestroy {
mappingForm: UntypedFormGroup;
@ -97,6 +104,8 @@ export class MappingDialogComponent extends DialogComponent<MappingDialogCompone
DataConversionTranslationsMap = DataConversionTranslationsMap;
HelpLinkByMappingTypeMap = HelpLinkByMappingTypeMap;
keysPopupClosed = true;
private destroy$ = new Subject<void>();
@ -104,7 +113,7 @@ export class MappingDialogComponent extends DialogComponent<MappingDialogCompone
constructor(protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: MappingInfo,
public dialogRef: MatDialogRef<MappingDialogComponent, MappingValue>,
public dialogRef: MatDialogRef<MappingDialogComponent, ConnectorMapping>,
private fb: FormBuilder,
private popoverService: TbPopoverService,
private renderer: Renderer2,
@ -187,10 +196,6 @@ export class MappingDialogComponent extends DialogComponent<MappingDialogCompone
}
}
helpLinkId(): string {
return 'https://thingsboard.io/docs/iot-gateway/config/mqtt/#section-mapping';
}
cancel(): void {
if (this.keysPopupClosed) {
this.dialogRef.close(null);
@ -247,7 +252,7 @@ export class MappingDialogComponent extends DialogComponent<MappingDialogCompone
}
}
private prepareMappingData(): { [key: string]: unknown } {
private prepareMappingData(): ConnectorMapping {
const formValue = this.mappingForm.value;
switch (this.data.mappingType) {
case MappingType.DATA:
@ -270,7 +275,7 @@ export class MappingDialogComponent extends DialogComponent<MappingDialogCompone
}
}
private prepareFormValueData(): { [key: string]: unknown } {
private getFormValueData(): ConnectorMappingFormValue {
if (this.data.value && Object.keys(this.data.value).length) {
switch (this.data.mappingType) {
case MappingType.DATA:
@ -282,16 +287,16 @@ export class MappingDialogComponent extends DialogComponent<MappingDialogCompone
type: converter.type,
[converter.type]: {...converter}
}
};
} as ConverterMappingFormValue;
case MappingType.REQUESTS:
return {
requestType: this.data.value.requestType,
requestValue: {
[this.data.value.requestType]: this.data.value.requestValue
}
};
} as RequestMappingFormValue;
default:
return this.data.value;
return this.data.value as DeviceConnectorMapping;
}
}
}
@ -317,7 +322,7 @@ export class MappingDialogComponent extends DialogComponent<MappingDialogCompone
extensionConfig: [{}, []]
}),
}));
this.mappingForm.patchValue(this.prepareFormValueData());
this.mappingForm.patchValue(this.getFormValueData());
this.mappingForm.get('converter.type').valueChanges.pipe(
startWith(this.mappingForm.get('converter.type').value),
takeUntil(this.destroy$)
@ -397,7 +402,7 @@ export class MappingDialogComponent extends DialogComponent<MappingDialogCompone
requestValueGroup.get('responseTimeout').enable({emitEvent: false});
}
});
this.mappingForm.patchValue(this.prepareFormValueData());
this.mappingForm.patchValue(this.getFormValueData());
}
private createOPCUAMappingForm(): void {
@ -410,6 +415,6 @@ export class MappingDialogComponent extends DialogComponent<MappingDialogCompone
rpc_methods: [[], []],
attributes_updates: [[], []]
});
this.mappingForm.patchValue(this.prepareFormValueData());
this.mappingForm.patchValue(this.getFormValueData());
}
}

3
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-configuration.component.ts

@ -619,6 +619,9 @@ export class GatewayConfigurationComponent implements OnInit {
updateCredentials = true;
} else {
updateCredentials = this.initialCredentials.credentialsId !== securityConfig.accessToken;
if (updateCredentials) {
this.initialCredentials.credentialsId = securityConfig.accessToken;
}
}
if (updateCredentials) {
newCredentials = {

7
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.html

@ -20,7 +20,8 @@
<mat-toolbar class="mat-mdc-table-toolbar">
<h2>{{ 'gateway.connectors' | translate }}</h2>
<span fxFlex></span>
<button mat-icon-button
<button *ngIf="dataSource?.data?.length"
mat-icon-button
[disabled]="isLoading$ | async"
(click)="addConnector($event)"
matTooltip="{{ 'action.add' | translate }}"
@ -182,6 +183,10 @@
formControlName="basicConfig"
[generalTabContent]="generalTabContent">
</tb-opc-ua-basic-config>
<tb-modbus-basic-config *ngSwitchCase="connectorType.MODBUS"
formControlName="basicConfig"
[generalTabContent]="generalTabContent">
</tb-modbus-basic-config>
</ng-container>
</ng-container>
<ng-template #defaultConfig>

9
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts

@ -50,6 +50,7 @@ import { UtilsService } from '@core/services/utils.service';
import { EntityType } from '@shared/models/entity-type.models';
import {
AddConnectorConfigData,
ConnectorBaseConfig,
ConnectorConfigurationModes,
ConnectorType,
GatewayConnector,
@ -92,7 +93,8 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
allowBasicConfig = new Set<ConnectorType>([
ConnectorType.MQTT,
ConnectorType.OPCUA
ConnectorType.OPCUA,
ConnectorType.MODBUS,
]);
gatewayLogLevel = Object.values(GatewayLogLevel);
@ -397,7 +399,6 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
private clearOutConnectorForm(): void {
this.initialConnector = null;
this.connectorForm.setControl('basicConfig', this.fb.group({}), {emitEvent: false});
this.connectorForm.setValue({
mode: ConnectorConfigurationModes.BASIC,
name: '',
@ -719,7 +720,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
connector.key = 'auto';
}
if (!connector.configurationJson) {
connector.configurationJson = {};
connector.configurationJson = {} as ConnectorBaseConfig;
}
connector.basicConfig = connector.configurationJson;
@ -732,7 +733,9 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
switch (connector.type) {
case ConnectorType.MQTT:
case ConnectorType.OPCUA:
case ConnectorType.MODBUS:
this.connectorForm.get('type').patchValue(connector.type, {emitValue: false, onlySelf: true});
this.connectorForm.get('basicConfig').setValue({}, {emitEvent: false});
setTimeout(() => {
this.connectorForm.patchValue({...connector, mode: connector.mode || ConnectorConfigurationModes.BASIC});

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

@ -31,15 +31,15 @@
<input matInput formControlName="requestTopicExpression"
placeholder="sensor/${deviceName}/request/${methodName}/${requestId}"/>
</mat-form-field>
<mat-slide-toggle class="margin" (click)="$event.stopPropagation()" [formControl]="isMQTTWithResponse">
<mat-slide-toggle class="margin" (click)="$event.stopPropagation()" formControlName="withResponse">
{{ 'gateway.rpc.withResponse' | translate }}
</mat-slide-toggle>
<mat-form-field *ngIf="isMQTTWithResponse.value">
<mat-form-field *ngIf="commandForm.get('withResponse')?.value">
<mat-label>{{ 'gateway.rpc.responseTopicExpression' | translate }}</mat-label>
<input matInput formControlName="responseTopicExpression"
placeholder="sensor/${deviceName}/response/${methodName}/${requestId}"/>
</mat-form-field>
<mat-form-field *ngIf="isMQTTWithResponse.value">
<mat-form-field *ngIf="commandForm.get('withResponse')?.value">
<mat-label>{{ 'gateway.rpc.responseTimeout' | translate }}</mat-label>
<input matInput formControlName="responseTimeout" type="number"
placeholder="10000" min="10" step="1"/>

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

@ -53,7 +53,7 @@ import {
} from '@shared/components/dialog/json-object-edit-dialog.component';
import { jsonRequired } from '@shared/components/json-object-edit.component';
import { deepClone } from '@core/utils';
import { takeUntil, tap } from "rxjs/operators";
import { filter, takeUntil, tap } from "rxjs/operators";
import { Subject } from "rxjs";
@Component({
@ -80,7 +80,6 @@ export class GatewayServiceRPCConnectorComponent implements OnInit, OnDestroy, C
saveTemplate: EventEmitter<RPCTemplateConfig> = new EventEmitter();
commandForm: FormGroup;
isMQTTWithResponse: FormControl;
codesArray: Array<number> = [1, 2, 3, 4, 5, 6, 15, 16];
ConnectorType = ConnectorType;
modbusCommandTypes = Object.values(ModbusCommandTypes) as ModbusCommandTypes[];
@ -135,7 +134,6 @@ export class GatewayServiceRPCConnectorComponent implements OnInit, OnDestroy, C
this.propagateChange({...this.commandForm.value, ...value});
}
});
this.isMQTTWithResponse = this.fb.control(false);
this.observeMQTTWithResponse();
}
@ -153,9 +151,10 @@ export class GatewayServiceRPCConnectorComponent implements OnInit, OnDestroy, C
methodFilter: [null, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
requestTopicExpression: [null, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
responseTopicExpression: [{ value: null, disabled: true }, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
responseTimeout: [null, [Validators.min(10), Validators.pattern(this.numbersOnlyPattern)]],
responseTimeout: [{ value: null, disabled: true }, [Validators.min(10), Validators.pattern(this.numbersOnlyPattern)]],
valueExpression: [null, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
})
withResponse: [false, []],
});
break;
case ConnectorType.MODBUS:
formGroup = this.fb.group({
@ -407,12 +406,21 @@ export class GatewayServiceRPCConnectorComponent implements OnInit, OnDestroy, C
}
private observeMQTTWithResponse(): void {
this.isMQTTWithResponse.valueChanges.pipe(
tap((isActive: boolean) => {
const responseControl = this.commandForm.get('responseTopicExpression');
isActive ? responseControl.enable() : responseControl.disable();
}),
takeUntil(this.destroy$),
).subscribe();
if (this.connectorType === ConnectorType.MQTT) {
this.commandForm.get('withResponse').valueChanges.pipe(
tap((isActive: boolean) => {
const responseTopicControl = this.commandForm.get('responseTopicExpression');
const responseTimeoutControl = this.commandForm.get('responseTimeout');
if (isActive) {
responseTopicControl.enable();
responseTimeoutControl.enable();
} else {
responseTopicControl.disable();
responseTimeoutControl.disable();
}
}),
takeUntil(this.destroy$),
).subscribe();
}
}
}

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

@ -206,9 +206,6 @@ export class GatewayServiceRPCComponent implements OnInit {
private updateTemplates() {
this.templates = this.subscription.data[0].data[0][1].length ?
JSON.parse(this.subscription.data[0].data[0][1]) : [];
if (this.templates.length && this.commandForm.get('params').value == "{}") {
this.commandForm.get('params').patchValue(this.templates[0].config);
}
this.cd.detectChanges();
}

18
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-statistics.component.html

@ -17,7 +17,7 @@
-->
<div class="statistics-container" fxLayout="row" fxLayout.lt-md="column">
<mat-card [formGroup]="statisticForm" *ngIf="!general">
<mat-form-field class="mat-block">
<mat-form-field class="mat-block" subscriptSizing="dynamic">
<mat-label>{{ 'gateway.statistics.statistic' | translate }}</mat-label>
<mat-select formControlName="statisticKey">
<mat-option *ngFor="let key of statisticsKeys" [value]="key">
@ -30,8 +30,13 @@
</mat-form-field>
<mat-error
*ngIf="!statisticsKeys.length && !commands.length">
{{'gateway.statistics.statistic-commands-empty' | translate }}
{{ 'gateway.statistics.statistic-commands-empty' | translate }}
</mat-error>
<div>
<button mat-flat-button color="primary" (click)="navigateToStatistics()">
{{ 'gateway.statistics.statistics-button' | translate }}
</button>
</div>
<mat-form-field class="mat-block" *ngIf="commandObj">
<mat-label>{{ 'gateway.statistics.command' | translate }}</mat-label>
<input matInput [value]="commandObj.command" disabled>
@ -43,13 +48,16 @@
matSort [matSortActive]="pageLink.sortOrder.property" [matSortDirection]="pageLink.sortDirection()"
matSortDisableClear>
<ng-container matColumnDef="0">
<mat-header-cell *matHeaderCellDef mat-sort-header>{{ 'widgets.gateway.created-time' | translate }}</mat-header-cell>
<mat-header-cell *matHeaderCellDef mat-sort-header>{{ 'widgets.gateway.created-time' | translate }}
</mat-header-cell>
<mat-cell *matCellDef="let row; let rowIndex = index">
{{row[0]| date:'yyyy-MM-dd HH:mm:ss' }}
{{ row[0]| date:'yyyy-MM-dd HH:mm:ss' }}
</mat-cell>
</ng-container>
<ng-container matColumnDef="1">
<mat-header-cell *matHeaderCellDef mat-sort-header style="width: 70%">{{ 'widgets.gateway.message' | translate }}</mat-header-cell>
<mat-header-cell *matHeaderCellDef mat-sort-header
style="width: 70%">{{ 'widgets.gateway.message' | translate }}
</mat-header-cell>
<mat-cell *matCellDef="let row">
{{ row[1] }}
</mat-cell>

1
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-statistics.component.scss

@ -28,6 +28,7 @@
height: 100%;
margin-right: 35px;
padding: 15px;
gap: 22px;
}
@media only screen and (max-width: 750px) {

6
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-statistics.component.ts

@ -32,6 +32,7 @@ import { Direction, SortOrder } from '@shared/models/page/sort-order';
import { MatTableDataSource } from '@angular/material/table';
import { MatSort } from '@angular/material/sort';
import { NULL_UUID } from '@shared/models/id/has-uuid';
import { deepClone } from '@core/utils';
@Component({
selector: 'tb-gateway-statistics',
@ -135,6 +136,11 @@ export class GatewayStatisticsComponent implements AfterViewInit {
}
}
public navigateToStatistics() {
const params = deepClone(this.ctx.stateController.getStateParams());
this.ctx.stateController.openState('configuration', params);
}
public sortData() {
this.dataSource.sortData(this.dataSource.data, this.sort);
}

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

@ -174,13 +174,25 @@ export interface ConnectorSecurity {
export type ConnectorMapping = DeviceConnectorMapping | RequestMappingData | ConverterConnectorMapping;
export interface ConnectorBaseConfig {
mapping?: DeviceConnectorMapping[];
dataMapping?: ConverterConnectorMapping[];
requestsMapping?: Record<RequestType, RequestMappingData> | RequestMappingData[];
server?: ServerConfig;
broker?: BrokerConfig;
workers?: WorkersConfig;
export type ConnectorMappingFormValue = DeviceConnectorMapping | RequestMappingFormValue | ConverterMappingFormValue;
export type ConnectorBaseConfig = MQTTBasicConfig | OPCBasicConfig | ModbusBasicConfig;
export interface MQTTBasicConfig {
dataMapping: ConverterConnectorMapping[];
requestsMapping: Record<RequestType, RequestMappingData> | RequestMappingData[];
broker: BrokerConfig;
workers: WorkersConfig;
}
export interface OPCBasicConfig {
mapping: DeviceConnectorMapping[];
server: ServerConfig;
}
export interface ModbusBasicConfig {
master: ModbusMasterConfig;
slave: ModbusSlave;
}
export interface WorkersConfig {
@ -223,7 +235,7 @@ export interface AttributesUpdate {
value: string;
}
interface Converter {
export interface Converter {
type: ConvertorType;
deviceNameJsonExpression: string;
deviceTypeJsonExpression: string;
@ -239,14 +251,20 @@ export interface ConverterConnectorMapping {
converter: Converter;
}
export type ConverterMappingFormValue = Omit<ConverterConnectorMapping, 'converter'> & {
converter: {
type: ConvertorType;
} & Record<ConvertorType, Converter>;
};
export interface DeviceConnectorMapping {
deviceNodePattern: string;
deviceNodeSource: string;
deviceInfo: DeviceInfo;
attributes: Attribute[];
timeseries: Timeseries[];
rpc_methods: RpcMethod[];
attributes_updates: AttributesUpdate[];
attributes?: Attribute[];
timeseries?: Timeseries[];
rpc_methods?: RpcMethod[];
attributes_updates?: AttributesUpdate[];
}
export enum ConnectorType {
@ -475,6 +493,11 @@ export interface MappingInfo {
buttonTitle: string;
}
export interface ModbusSlaveInfo {
value: SlaveConfig;
buttonTitle: string;
}
export enum ConnectorConfigurationModes {
BASIC = 'basic',
ADVANCED = 'advanced'
@ -540,6 +563,14 @@ export const MappingHintTranslationsMap = new Map<MappingType, string>(
]
);
export const HelpLinkByMappingTypeMap = new Map<MappingType, string>(
[
[MappingType.DATA, 'https://thingsboard.io/docs/iot-gateway/config/mqtt/#section-mapping'],
[MappingType.OPCUA, 'https://thingsboard.io/docs/iot-gateway/config/opc-ua/#section-mapping'],
[MappingType.REQUESTS, 'https://thingsboard.io/docs/iot-gateway/config/mqtt/#section-mapping']
]
);
export const QualityTypes = [0, 1 ,2];
export const QualityTypeTranslationsMap = new Map<number, string>(
@ -597,6 +628,10 @@ export interface RequestMappingData {
requestValue: RequestDataItem;
}
export type RequestMappingFormValue = Omit<RequestMappingData, 'requestValue'> & {
requestValue: Record<RequestType, RequestDataItem>;
};
export interface RequestDataItem {
type: string;
details: string;
@ -739,3 +774,259 @@ export const SecurityPolicyTypes = [
{ value: SecurityPolicy.BASIC256, name: 'Basic256' },
{ value: SecurityPolicy.BASIC256SHA, name: 'Basic256SHA256' }
];
export enum ModbusProtocolType {
TCP = 'tcp',
UDP = 'udp',
Serial = 'serial',
}
export const ModbusProtocolLabelsMap = new Map<ModbusProtocolType, string>(
[
[ModbusProtocolType.TCP, 'TCP'],
[ModbusProtocolType.UDP, 'UDP'],
[ModbusProtocolType.Serial, 'Serial'],
]
);
export enum ModbusMethodType {
SOCKET = 'socket',
RTU = 'rtu',
}
export enum ModbusSerialMethodType {
RTU = 'rtu',
ASCII = 'ascii',
}
export const ModbusMethodLabelsMap = new Map<ModbusMethodType | ModbusSerialMethodType, string>(
[
[ModbusMethodType.SOCKET, 'Socket'],
[ModbusMethodType.RTU, 'RTU'],
[ModbusSerialMethodType.ASCII, 'ASCII'],
]
);
export const ModbusByteSizes = [5, 6, 7 ,8];
export enum ModbusParity {
Even = 'E',
Odd = 'O',
None = 'N'
}
export const ModbusParityLabelsMap = new Map<ModbusParity, string>(
[
[ModbusParity.Even, 'Even'],
[ModbusParity.Odd, 'Odd'],
[ModbusParity.None, 'None'],
]
);
export enum ModbusOrderType {
BIG = 'BIG',
LITTLE = 'LITTLE',
}
export enum ModbusRegisterType {
HoldingRegisters = 'holding_registers',
CoilsInitializer = 'coils_initializer',
InputRegisters = 'input_registers',
DiscreteInputs = 'discrete_inputs'
}
export const ModbusRegisterTranslationsMap = new Map<ModbusRegisterType, string>(
[
[ModbusRegisterType.HoldingRegisters, 'gateway.holding_registers'],
[ModbusRegisterType.CoilsInitializer, 'gateway.coils_initializer'],
[ModbusRegisterType.InputRegisters, 'gateway.input_registers'],
[ModbusRegisterType.DiscreteInputs, 'gateway.discrete_inputs']
]
);
export enum ModbusDataType {
STRING = 'string',
BYTES = 'bytes',
BITS = 'bits',
INT8 = '8int',
UINT8 = '8uint',
FLOAT8 = '8float',
INT16 = '16int',
UINT16 = '16uint',
FLOAT16 = '16float',
INT32 = '32int',
UINT32 = '32uint',
FLOAT32 = '32float',
INT64 = '64int',
UINT64 = '64uint',
FLOAT64 = '64float'
}
export enum ModbusObjectCountByDataType {
'8int' = 1,
'8uint' = 1,
'8float' = 1,
'16int' = 1,
'16uint' = 1,
'16float' = 1,
'32int' = 2,
'32uint' = 2,
'32float' = 2,
'64int' = 4,
'64uint' = 4,
'64float' = 4,
}
export enum ModbusValueKey {
ATTRIBUTES = 'attributes',
TIMESERIES = 'timeseries',
ATTRIBUTES_UPDATES = 'attributeUpdates',
RPC_REQUESTS = 'rpc',
}
export const ModbusKeysPanelTitleTranslationsMap = new Map<ModbusValueKey, string>(
[
[ModbusValueKey.ATTRIBUTES, 'gateway.attributes'],
[ModbusValueKey.TIMESERIES, 'gateway.timeseries'],
[ModbusValueKey.ATTRIBUTES_UPDATES, 'gateway.attribute-updates'],
[ModbusValueKey.RPC_REQUESTS, 'gateway.rpc-requests']
]
);
export const ModbusKeysAddKeyTranslationsMap = new Map<ModbusValueKey, string>(
[
[ModbusValueKey.ATTRIBUTES, 'gateway.add-attribute'],
[ModbusValueKey.TIMESERIES, 'gateway.add-timeseries'],
[ModbusValueKey.ATTRIBUTES_UPDATES, 'gateway.add-attribute-update'],
[ModbusValueKey.RPC_REQUESTS, 'gateway.add-rpc-request']
]
);
export const ModbusKeysDeleteKeyTranslationsMap = new Map<ModbusValueKey, string>(
[
[ModbusValueKey.ATTRIBUTES, 'gateway.delete-attribute'],
[ModbusValueKey.TIMESERIES, 'gateway.delete-timeseries'],
[ModbusValueKey.ATTRIBUTES_UPDATES, 'gateway.delete-attribute-update'],
[ModbusValueKey.RPC_REQUESTS, 'gateway.delete-rpc-request']
]
);
export const ModbusKeysNoKeysTextTranslationsMap = new Map<ModbusValueKey, string>(
[
[ModbusValueKey.ATTRIBUTES, 'gateway.no-attributes'],
[ModbusValueKey.TIMESERIES, 'gateway.no-timeseries'],
[ModbusValueKey.ATTRIBUTES_UPDATES, 'gateway.no-attribute-updates'],
[ModbusValueKey.RPC_REQUESTS, 'gateway.no-rpc-requests']
]
);
export const ModbusFunctionCodeTranslationsMap = new Map<number, string>(
[
[1, 'gateway.read-coils'],
[2, 'gateway.read-discrete-inputs'],
[3, 'gateway.read-multiple-holding-registers'],
[4, 'gateway.read-input-registers'],
[5, 'gateway.write-coil'],
[6, 'gateway.write-register'],
[15, 'gateway.write-coils'],
[16, 'gateway.write-registers'],
]
);
export interface ModbusMasterConfig {
slaves: SlaveConfig[];
}
export interface SlaveConfig {
name: string;
host?: string;
port: string | number;
serialPort?: string;
type: ModbusProtocolType;
method: ModbusMethodType;
timeout: number;
byteOrder: ModbusOrderType;
wordOrder: ModbusOrderType;
retries: boolean;
retryOnEmpty: boolean;
retryOnInvalid: boolean;
pollPeriod: number;
unitId: number;
deviceName: string;
deviceType: string;
sendDataOnlyOnChange: boolean;
connectAttemptTimeMs: number;
connectAttemptCount: number;
waitAfterFailedAttemptsMs: number;
attributes: ModbusValue[];
timeseries: ModbusValue[];
attributeUpdates: ModbusValue[];
rpc: ModbusValue[];
security?: ModbusSecurity;
baudrate?: number;
stopbits?: number;
bytesize?: number;
parity?: ModbusParity;
strict?: boolean;
}
export interface ModbusValue {
tag: string;
type: ModbusDataType;
functionCode?: number;
objectsCount: number;
address: number;
value?: string;
}
export interface ModbusSecurity {
certfile?: string;
keyfile?: string;
password?: string;
server_hostname?: string;
reqclicert?: boolean;
}
export interface ModbusSlave {
host?: string;
type: ModbusProtocolType;
method: ModbusMethodType;
unitId: number;
serialPort?: string;
baudrate?: number;
deviceName: string;
deviceType: string;
pollPeriod: number;
sendDataToThingsBoard: boolean;
byteOrder: ModbusOrderType;
identity: ModbusIdentity;
values: ModbusValuesState;
port: string | number;
security: ModbusSecurity;
}
export type ModbusValuesState = ModbusRegisterValues | ModbusValues;
export interface ModbusRegisterValues {
holding_registers: ModbusValues;
coils_initializer: ModbusValues;
input_registers: ModbusValues;
discrete_inputs: ModbusValues;
}
export interface ModbusValues {
attributes: ModbusValue[];
timeseries: ModbusValue[];
attributeUpdates: ModbusValue[];
rpc: ModbusValue[];
}
export interface ModbusIdentity {
vendorName?: string;
productCode?: string;
vendorUrl?: string;
productName?: string;
modelName?: string;
}
export const ModbusBaudrates = [4800, 9600, 19200, 38400, 57600, 115200, 230400, 460800, 921600];

0
ui-ngx/src/app/modules/home/pipes/gateway-help-link/gateway-help-link.pipe.ts → ui-ngx/src/app/modules/home/components/widget/lib/gateway/pipes/gateway-help-link.pipe.ts

42
ui-ngx/src/app/modules/home/components/widget/lib/gateway/pipes/gateway-port-tooltip.pipe.ts

@ -0,0 +1,42 @@
///
/// Copyright © 2016-2024 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 { Pipe, PipeTransform } from '@angular/core';
import { PortLimits } from '@home/components/widget/lib/gateway/gateway-widget.models';
import { AbstractControl } from '@angular/forms';
import { TranslateService } from '@ngx-translate/core';
@Pipe({
name: 'getGatewayPortTooltip',
standalone: true,
})
export class GatewayPortTooltipPipe implements PipeTransform {
constructor(private translate: TranslateService) {}
transform(portControl: AbstractControl): string {
if (portControl.hasError('required')) {
return this.translate.instant('gateway.port-required');
}
if (portControl.hasError('min') || portControl.hasError('max')) {
return this.translate.instant('gateway.port-limits-error', {
min: PortLimits.MIN,
max: PortLimits.MAX,
});
}
return '';
}
}

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

@ -90,9 +90,6 @@ import { ToggleButtonWidgetComponent } from '@home/components/widget/lib/button/
import { TimeSeriesChartWidgetComponent } from '@home/components/widget/lib/chart/time-series-chart-widget.component';
import { AddConnectorDialogComponent } from '@home/components/widget/lib/gateway/dialog/add-connector-dialog.component';
import { MappingDialogComponent } from '@home/components/widget/lib/gateway/dialog/mapping-dialog.component';
import {
EllipsisChipListDirective
} from '@home/components/widget/lib/gateway/connectors-configuration/ellipsis-chip-list.directive';
import { StatusWidgetComponent } from '@home/components/widget/lib/indicator/status-widget.component';
import { LatestChartComponent } from '@home/components/widget/lib/chart/latest-chart.component';
import { PieChartWidgetComponent } from '@home/components/widget/lib/chart/pie-chart-widget.component';
@ -112,18 +109,38 @@ import {
import {
NotificationTypeFilterPanelComponent
} from '@home/components/widget/lib/cards/notification-type-filter-panel.component';
import { GatewayHelpLinkPipe } from '@home/pipes/public-api';
import { GatewayHelpLinkPipe } from '@home/components/widget/lib/gateway/pipes/gateway-help-link.pipe';
import { EllipsisChipListDirective } from '@shared/directives/ellipsis-chip-list.directive';
import {
BrokerConfigControlComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/broker-config-control/broker-config-control.component';
import {
WorkersConfigControlComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/workers-config-control/workers-config-control.component';
import {
OpcServerConfigComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/opc-server-config/opc-server-config.component';
import {
MqttBasicConfigComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/mqtt-basic-config/mqtt-basic-config.component';
import {
MappingTableComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/mapping-table/mapping-table.component';
import {
OpcUaBasicConfigComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/opc-ua-basic-config/opc-ua-basic-config.component';
import {
BrokerConfigControlComponent,
DeviceInfoTableComponent,
MappingDataKeysPanelComponent,
MappingTableComponent,
MqttBasicConfigComponent,
OpcUaBasicConfigComponent,
ServerConfigComponent,
TypeValuePanelComponent,
WorkersConfigControlComponent,
} from '@home/components/widget/lib/gateway/connectors-configuration/public-api';
ModbusBasicConfigComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component';
import {
DeviceInfoTableComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/device-info-table/device-info-table.component';
import {
MappingDataKeysPanelComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/mapping-data-keys-panel/mapping-data-keys-panel.component';
import {
TypeValuePanelComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/type-value-panel/type-value-panel.component';
@NgModule({
declarations: [
@ -163,7 +180,6 @@ import {
GatewayConfigurationComponent,
GatewayRemoteConfigurationDialogComponent,
GatewayServiceRPCConnectorTemplateDialogComponent,
EllipsisChipListDirective,
ValueCardWidgetComponent,
AggregatedValueCardWidgetComponent,
CountWidgetComponent,
@ -192,7 +208,8 @@ import {
LabelCardWidgetComponent,
LabelValueCardWidgetComponent,
UnreadNotificationWidgetComponent,
NotificationTypeFilterPanelComponent],
NotificationTypeFilterPanelComponent
],
imports: [
CommonModule,
SharedModule,
@ -203,11 +220,13 @@ import {
GatewayHelpLinkPipe,
BrokerConfigControlComponent,
WorkersConfigControlComponent,
ServerConfigComponent,
OpcServerConfigComponent,
MqttBasicConfigComponent,
MappingTableComponent,
OpcUaBasicConfigComponent,
KeyValueIsNotEmptyPipe
KeyValueIsNotEmptyPipe,
ModbusBasicConfigComponent,
EllipsisChipListDirective,
],
exports: [
EntitiesTableWidgetComponent,

17
ui-ngx/src/app/modules/home/pipes/public-api.ts

@ -1,17 +0,0 @@
///
/// Copyright © 2016-2024 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.
///
export * from './gateway-help-link/gateway-help-link.pipe';

2
ui-ngx/src/app/shared/components/hint-tooltip-icon.component.html

@ -15,7 +15,7 @@
limitations under the License.
-->
<ng-content></ng-content>
<ng-content class="tb-hint-tooltip-content"></ng-content>
<tb-icon class="tb-hint-tooltip-icon tb-mat-18"
*ngIf="tooltipText"
matTooltip="{{ tooltipText }}"

8
ui-ngx/src/app/shared/components/hint-tooltip-icon.component.scss

@ -21,11 +21,19 @@
}
:host {
.tb-hint-tooltip-content {
flex: 1 1 auto;
min-width: 0;
max-width: fit-content;
}
.tb-hint-tooltip-icon {
color: #E0E0E0;
overflow: visible;
order: 3;
margin-left: 4px;
flex-shrink: 0;
&:hover {
color: #9E9E9E;
}

48
ui-ngx/src/app/shared/components/table/table-datasource.abstract.ts

@ -0,0 +1,48 @@
///
/// Copyright © 2016-2024 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 { DataSource } from '@angular/cdk/collections';
import { BehaviorSubject, Observable } from 'rxjs';
import { map } from 'rxjs/operators';
export abstract class TbTableDatasource<T> implements DataSource<T> {
protected dataSubject = new BehaviorSubject<Array<T>>([]);
connect(): Observable<Array<T>> {
return this.dataSubject.asObservable();
}
disconnect(): void {
this.dataSubject.complete();
}
loadData(data: Array<T>): void {
this.dataSubject.next(data);
}
isEmpty(): Observable<boolean> {
return this.dataSubject.pipe(
map((data: T[]) => !data.length)
);
}
total(): Observable<number> {
return this.dataSubject.pipe(
map((data: T[]) => data.length)
);
}
}

20
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/ellipsis-chip-list.directive.ts → ui-ngx/src/app/shared/directives/ellipsis-chip-list.directive.ts

@ -20,7 +20,7 @@ import {
Inject,
Input,
OnDestroy,
Renderer2
Renderer2,
} from '@angular/core';
import { isEqual } from '@core/utils';
import { TranslateService } from '@ngx-translate/core';
@ -30,13 +30,15 @@ import { takeUntil } from 'rxjs/operators';
@Directive({
// eslint-disable-next-line @angular-eslint/directive-selector
selector: '[tb-ellipsis-chip-list]'
selector: '[tb-ellipsis-chip-list]',
standalone: true,
})
export class EllipsisChipListDirective implements OnDestroy {
chipsValue: string[];
private destroy$ = new Subject<void>();
private intersectionObserver: IntersectionObserver;
@Input('tb-ellipsis-chip-list')
set chips(value: string[]) {
@ -59,6 +61,19 @@ export class EllipsisChipListDirective implements OnDestroy {
).subscribe(() => {
this.adjustChips();
});
this.observeIntersection();
}
private observeIntersection(): void {
this.intersectionObserver = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (entry.isIntersecting) {
this.adjustChips();
}
});
});
this.intersectionObserver.observe(this.el.nativeElement);
}
private adjustChips(): void {
@ -124,5 +139,6 @@ export class EllipsisChipListDirective implements OnDestroy {
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
this.intersectionObserver.disconnect();
}
}

116
ui-ngx/src/app/shared/directives/truncate-with-tooltip.directive.ts

@ -0,0 +1,116 @@
///
/// Copyright © 2016-2024 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,
Directive,
ElementRef,
Input,
OnDestroy,
OnInit,
Renderer2,
} from '@angular/core';
import { fromEvent, Subject } from 'rxjs';
import { filter, takeUntil, tap } from 'rxjs/operators';
import { MatTooltip, TooltipPosition } from '@angular/material/tooltip';
import { coerceBoolean } from '@shared/decorators/coercion';
@Directive({
standalone: true,
selector: '[tbTruncateWithTooltip]',
providers: [MatTooltip],
})
export class TruncateWithTooltipDirective implements OnInit, AfterViewInit, OnDestroy {
@Input('tbTruncateWithTooltip')
text: string;
@Input()
@coerceBoolean()
tooltipEnabled = true;
@Input()
position: TooltipPosition = 'above';
private destroy$ = new Subject<void>();
constructor(
private elementRef: ElementRef,
private renderer: Renderer2,
private tooltip: MatTooltip
) {}
ngOnInit(): void {
this.observeMouseEvents();
this.applyTruncationStyles();
}
ngAfterViewInit(): void {
if (!this.text) {
this.text = this.elementRef.nativeElement.innerText;
}
this.tooltip.position = this.position;
}
ngOnDestroy(): void {
if (this.tooltip._isTooltipVisible()) {
this.hideTooltip();
}
this.destroy$.next();
this.destroy$.complete();
}
private observeMouseEvents(): void {
fromEvent(this.elementRef.nativeElement, 'mouseenter')
.pipe(
filter(() => this.tooltipEnabled),
filter(() => this.isOverflown(this.elementRef.nativeElement)),
tap(() => this.showTooltip()),
takeUntil(this.destroy$),
)
.subscribe();
fromEvent(this.elementRef.nativeElement, 'mouseleave')
.pipe(
filter(() => this.tooltipEnabled),
filter(() => this.tooltip._isTooltipVisible()),
tap(() => this.hideTooltip()),
takeUntil(this.destroy$),
)
.subscribe();
}
private applyTruncationStyles(): void {
this.renderer.setStyle(this.elementRef.nativeElement, 'white-space', 'nowrap');
this.renderer.setStyle(this.elementRef.nativeElement, 'overflow', 'hidden');
this.renderer.setStyle(this.elementRef.nativeElement, 'text-overflow', 'ellipsis');
}
private isOverflown(element: HTMLElement): boolean {
return element.clientWidth < element.scrollWidth;
}
private showTooltip(): void {
this.tooltip.message = this.text;
this.renderer.setAttribute(this.elementRef.nativeElement, 'matTooltip', this.text);
this.tooltip.show();
}
private hideTooltip(): void {
this.tooltip.hide();
}
}

2
ui-ngx/src/app/shared/pipe/key-value-not-empty.pipe.ts

@ -62,6 +62,6 @@ export class KeyValueIsNotEmptyPipe implements PipeTransform {
}
private makeKeyValuePair(key: string, value: unknown): KeyValue<string, unknown> {
return {key: key, value: value};
return {key, value};
}
}

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

@ -2755,19 +2755,27 @@
"function": "Function"
},
"gateway": {
"address": "Address",
"address-required": "Address required",
"add-entry": "Add configuration",
"add-attribute": "Add attribute",
"add-attribute-update": "Add attribute update",
"add-key": "Add key",
"add-timeseries": "Add time series",
"add-mapping": "Add mapping",
"add-slave": "Add Slave",
"arguments": "Arguments",
"add-rpc-method": "Add method",
"add-rpc-request": "Add request",
"add-value": "Add argument",
"baudrate": "Baudrate",
"bytesize": "Bytesize",
"delete-value": "Delete value",
"delete-rpc-method": "Delete method",
"delete-attribute-update": "Add attribute update",
"delete-rpc-request": "Delete request",
"delete-attribute-update": "Delete attribute update",
"advanced": "Advanced",
"advanced-connection-settings": "Advanced connection settings",
"attributes": "Attributes",
"attribute-updates": "Attribute updates",
"attribute-filter": "Attribute filter",
@ -2777,6 +2785,8 @@
"attribute-name-expression-required": "Attribute name expression required.",
"attribute-name-expression-hint": "Hint for Attribute name expression",
"basic": "Basic",
"byte-order": "Byte order",
"word-order": "Word order",
"broker": {
"connection": "Connection to broker",
"name": "Broker name",
@ -2811,6 +2821,9 @@
"connectors-table-actions": "Actions",
"connectors-table-key": "Key",
"connectors-table-class": "Class",
"connection-timeout": "Connection timeout (s)",
"connect-attempt-time": "Connect attempt time (ms)",
"connect-attempt-count": "Connect attempt count",
"copy-username": "Copy username",
"copy-password": "Copy password",
"copy-client-id": "Copy client ID",
@ -2839,7 +2852,8 @@
"device-name-filter-hint": "This field supports Regular expressions to filter incoming data by device name.",
"device-name-filter-required": "Device name filter is required.",
"details": "Details",
"delete-mapping-title": "Delete mapping ?",
"delete-mapping-title": "Delete mapping?",
"delete-slave-title": "Delete slave?",
"download-configuration-file": "Download configuration file",
"download-docker-compose": "Download docker-compose.yml for your gateway",
"enable-remote-logging": "Enable remote logging",
@ -2858,6 +2872,7 @@
"configuration-delete-dialog-confirm": "Turn Off",
"connector-duplicate-name": "Connector with such name already exists.",
"connector-side": "Connector side",
"client-communication-type": "Client communication type",
"payload-type": "Payload type",
"platform-side": "Platform side",
"JSON": "JSON",
@ -2883,8 +2898,11 @@
"device-node-hint": "Path or identifier for device node on OPC UA server. Relative paths from it for attributes and time series can be used.",
"device-name": "Device name",
"device-profile": "Device profile",
"device-name-required": "Device name required",
"device-profile-required": "Device profile required",
"download-tip": "Download configuration file",
"drop-file": "Drop file here or",
"enable": "Enable",
"enable-subscription": "Enable subscription",
"extension": "Extension",
"extension-hint": "Put your converter classname in the field. Custom converter with such class should be in extension/mqtt folder.",
@ -2895,6 +2913,7 @@
"fill-connector-defaults-hint": "This property allows to fill connector configuration with default values on it's creation.",
"from-device-request-settings": "Input request parsing",
"from-device-request-settings-hint": "These fields support JSONPath expressions to extract a name from incoming message.",
"function-code": "Function code",
"to-device-response-settings": "Output request processing",
"to-device-response-settings-hint": "For these fields you can use the following variables and they will be replaced with actual values: ${deviceName}, ${attributeKey}, ${attributeValue}",
"gateway": "Gateway",
@ -2933,8 +2952,13 @@
"inactivity-timeout-seconds-required": "Inactivity timeout is required",
"inactivity-timeout-seconds-min": "Inactivity timeout can not be less then 1",
"inactivity-timeout-seconds-pattern": "Inactivity timeout is not valid",
"unit-id": "Unit ID",
"host": "Host",
"host-required": "Host is required.",
"holding_registers": "Holding registers",
"coils_initializer": "Coils initializer",
"input_registers": "Input registers",
"discrete_inputs": "Discrete inputs",
"json-parse": "Not valid JSON.",
"json-required": "Field cannot be empty.",
"JSONPath-hint": "This field supports constants and JSONPath expressions.",
@ -2968,12 +2992,14 @@
"max-messages-queue-for-worker": "Max messages queue per worker",
"max-messages-queue-for-worker-hint": "Maximal messages count that will be in the queue \nfor each converter worker.",
"max-messages-queue-for-worker-required": "Max messages queue per worker is required.",
"method": "Method",
"method-name": "Method name",
"method-required": "Method name is required.",
"min-pack-send-delay": "Min pack send delay (in ms)",
"min-pack-send-delay-required": "Min pack send delay is required",
"min-pack-send-delay-min": "Min pack send delay can not be less then 0",
"mode": "Mode",
"model-name": "Model name",
"mqtt-version": "MQTT version",
"name": "Name",
"name-required": "Name is required.",
@ -2987,17 +3013,22 @@
"no-keys": "No keys",
"no-value": "No arguments",
"no-rpc-methods": "No RPC methods",
"no-rpc-requests": "No RPC requests",
"path-hint": "The path is local to the gateway file system",
"path-logs": "Path to log files",
"path-logs-required": "Path is required.",
"password": "Password",
"password-required": "Password is required.",
"permit-without-calls": "Keep alive permit without calls",
"poll-period": "Poll period (ms)",
"port": "Port",
"port-required": "Port is required.",
"port-limits-error": "Port should be number from {{min}} to {{max}}.",
"private-key-path": "Path to private key file",
"path-to-private-key-required": "Path to private key file is required.",
"parity": "Parity",
"product-code": "Product code",
"product-name": "Product name",
"raw": "Raw",
"retain": "Retain",
"retain-hint": "This flag tells the broker to store the message for a topic\nand ensures any new client subscribing to that topic\nwill receive the stored message.",
@ -3006,6 +3037,9 @@
"remove-entry": "Remove configuration",
"remote-shell": "Remote shell",
"remote-configuration": "Remote Configuration",
"retries": "Retries",
"retries-on-empty": "Retries on empty",
"retries-on-invalid": "Retries on invalid",
"rpc": {
"title": "{{type}} Connector RPC parameters",
"templates-title": "Connector RPC Templates",
@ -3097,6 +3131,7 @@
"json-value-invalid": "JSON value has an invalid format"
},
"rpc-methods": "RPC methods",
"rpc-requests": "RPC requests",
"request" : {
"connect-request": "Connect request",
"disconnect-request": "Disconnect request",
@ -3108,6 +3143,7 @@
"requests-mapping": "Requests mapping",
"requests-mapping-hint": "MQTT Connector requests allows you to connect, disconnect, process attribute requests from the device, handle attribute updates on the server and RPC processing configuration.",
"request-topic-expression": "Request topic expression",
"request-client-certificate": "Request client certificate",
"request-topic-expression-required": "Request topic expression is required.",
"response-timeout": "Response timeout (ms)",
"response-timeout-required": "Response timeout is required.",
@ -3118,7 +3154,10 @@
"response-topic-expression-required": "Response topic expression is required.",
"response-value-expression": "Response value expression",
"response-value-expression-required": "Response value expression is required.",
"vendor-name": "Vendor name",
"vendor-url": "Vendor URL",
"value": "Value",
"values": "Values",
"value-required": "Value is required.",
"value-expression": "Value expression",
"value-expression-required": "Value expression is required.",
@ -3141,17 +3180,28 @@
},
"select-connector": "Select connector to display config",
"send-change-data": "Send data only on change",
"send-data-TB": "Send data to ThingsBoard",
"send-data-on-change": "Send data only on change",
"send-change-data-hint": "The values will be saved to the database only if they are different from the corresponding values in the previous converted message. This functionality applies to both attributes and time series in the converter output.",
"server": "Server",
"server-hostname": "Server hostname",
"server-slave": "Server (Slave)",
"servers-slaves": "Servers (Slaves)",
"server-port": "Server port",
"server-url": "Server endpoint url",
"server-connection": "Server Connection",
"server-config": "Server configuration",
"server-slave-config": "Server (Slave) configuration",
"server-url-required": "Server endpoint url is required.",
"stopbits": "Stopbits",
"strict": "Strict",
"set": "Set",
"show-map": "Show map",
"statistics": {
"statistic": "Statistic",
"statistics": "Statistics",
"statistic-commands-empty": "No configured statistic keys found. You can configure them in \"Statistics\" tab in general configuration.",
"statistics-button": "Go to configuration",
"commands": "Commands",
"send-period": "Statistic send period (in sec)",
"send-period-required": "Statistic send period is required",
@ -3237,6 +3287,8 @@
"topic-required": "Topic filter is required.",
"tls-path-ca-certificate": "Path to CA certificate on gateway",
"tls-path-client-certificate": "Path to client certificate on gateway",
"tls-connection": "TLS Connection",
"master-connections": "Master Connections",
"method-filter": "Method filter",
"method-filter-hint": "Regular expression to filter incoming RPC method from platform.",
"method-filter-required": "Method filter is required.",
@ -3256,13 +3308,25 @@
"at-least-once": "1 - At least once",
"exactly-once": "2 - Exactly once"
},
"objects-count": "Objects count",
"wait-after-failed-attempts": "Wait after failed attempts (ms)",
"tls-path-private-key": "Path to private key on gateway",
"toggle-fullscreen": "Toggle fullscreen",
"transformer-json-config": "Configuration JSON*",
"update-config": "Add/update configuration JSON",
"username": "Username",
"username-required": "Username is required.",
"unit-id-required": "Unit ID is required.",
"read-coils": "Read Coils",
"read-discrete-inputs": "Read Discrete Inputs",
"read-multiple-holding-registers": "Read Multiple Holding Register",
"read-input-registers": "Read Input Registers",
"write-coil": "Write Coil",
"write-coils": "Write Coils",
"write-register": "Write Register",
"write-registers": "Write Registers",
"hints": {
"modbus-server": "Starting with version 3.0, Gateway can run as a Modbus slave.",
"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 platform server",
@ -3301,7 +3365,9 @@
"grpc-max-pings-without-data": "Maximum number of keepalive ping messages that the server can send without receiving any data before it considers the connection dead.",
"grpc-min-ping-interval-without-data": "Minimum amount of time the server should wait between sending keepalive ping messages when there is no data being sent or received.",
"permit-without-calls": "Allow server to keep the GRPC connection alive even when there are no active RPC calls.",
"path-in-os": "Path in gateway os.",
"memory": "Your data will be stored in the in-memory queue, it is a fastest but no persistence guarantee.",
"framer-type": "Type of framer.",
"file": "Your data will be stored in separated files and will be saved even after the gateway restart.",
"sqlite": "Your data will be stored in file based database. And will be saved even after the gateway restart.",
"opcua-timeout": "Timeout in seconds for connecting to OPC-UA server.",

112
ui-ngx/src/assets/metadata/connector-default-configs/modbus.json

@ -2,6 +2,7 @@
"master": {
"slaves": [
{
"name": "Slave 1",
"host": "127.0.0.1",
"port": 5021,
"type": "tcp",
@ -15,6 +16,7 @@
"pollPeriod": 5000,
"unitId": 1,
"deviceName": "Temp Sensor",
"deviceType": "default",
"sendDataOnlyOnChange": true,
"connectAttemptTimeMs": 5000,
"connectAttemptCount": 5,
@ -185,64 +187,60 @@
"wordOrder": "LITTLE",
"unitId": 0,
"values": {
"holding_registers": [
{
"attributes": [
{
"address": 1,
"type": "string",
"tag": "sm",
"objectsCount": 1,
"value": "ON"
}
],
"timeseries": [
{
"address": 2,
"type": "int",
"tag": "smm",
"objectsCount": 1,
"value": "12334"
}
],
"attributeUpdates": [
{
"tag": "shared_attribute_write",
"type": "32int",
"functionCode": 6,
"objectsCount": 2,
"address": 29,
"value": 1243
}
],
"rpc": [
{
"tag": "setValue",
"type": "bits",
"functionCode": 5,
"objectsCount": 1,
"address": 31,
"value": 22
}
]
"holding_registers": {
"attributes": [
{
"address": 1,
"type": "string",
"tag": "sm",
"objectsCount": 1,
"value": "ON"
}
],
"timeseries": [
{
"address": 2,
"type": "8int",
"tag": "smm",
"objectsCount": 1,
"value": "12334"
}
],
"attributeUpdates": [
{
"tag": "shared_attribute_write",
"type": "32int",
"functionCode": 6,
"objectsCount": 2,
"address": 29,
"value": 1243
}
],
"rpc": [
{
"tag": "setValue",
"type": "bits",
"functionCode": 5,
"objectsCount": 1,
"address": 31,
"value": 22
}
]
},
"coils_initializer": {
"attributes": [
{
"address": 5,
"type": "string",
"tag": "sm",
"objectsCount": 1,
"value": "12"
}
],
"timeseries": [],
"attributeUpdates": [],
"rpc": []
}
],
"coils_initializer": [
{
"attributes": [
{
"address": 5,
"type": "string",
"tag": "sm",
"objectsCount": 1,
"value": "12"
}
],
"timeseries": [],
"attributeUpdates": [],
"rpc": []
}
]
}
}
}

Loading…
Cancel
Save