Browse Source

Gateway - Connectors - OPCUA extended functionality

pull/11101/head
mpetrov 2 years ago
parent
commit
e4e028e0b4
  1. 86
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-config-control/broker-config-control.component.html
  2. 129
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-config-control/broker-config-control.component.ts
  3. 10
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/device-info-table/device-info-table.component.html
  4. 0
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/device-info-table/device-info-table.component.scss
  5. 25
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/device-info-table/device-info-table.component.ts
  6. 24
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/index.ts
  7. 73
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-data-keys-panel/mapping-data-keys-panel.component.html
  8. 0
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-data-keys-panel/mapping-data-keys-panel.component.scss
  9. 80
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-data-keys-panel/mapping-data-keys-panel.component.ts
  10. 6
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table/mapping-table.component.html
  11. 0
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table/mapping-table.component.scss
  12. 241
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table/mapping-table.component.ts
  13. 44
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/security-config/security-config.component.html
  14. 0
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/security-config/security-config.component.scss
  15. 115
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/security-config/security-config.component.ts
  16. 125
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/server-config/server-config.component.html
  17. 20
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/server-config/server-config.component.scss
  18. 106
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/server-config/server-config.component.ts
  19. 92
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/type-value-panel/type-value-panel.component.html
  20. 52
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/type-value-panel/type-value-panel.component.scss
  21. 144
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/type-value-panel/type-value-panel.component.ts
  22. 63
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/workers-config-control/workers-config-control.component.html
  23. 94
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/workers-config-control/workers-config-control.component.ts
  24. 118
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.html
  25. 8
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.scss
  26. 347
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.ts
  27. 170
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.html
  28. 275
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts
  29. 205
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts
  30. 25
      ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts
  31. 39
      ui-ngx/src/app/modules/home/pipes/gateway-help-link/gateway-help-link.pipe.ts
  32. 17
      ui-ngx/src/app/modules/home/pipes/index.ts
  33. 24
      ui-ngx/src/app/shared/validators/form-array.validators.ts
  34. 54
      ui-ngx/src/assets/help/en_US/widget/lib/gateway/attributes-identifier_fn.md
  35. 36
      ui-ngx/src/assets/help/en_US/widget/lib/gateway/attributes-path_fn.md
  36. 46
      ui-ngx/src/assets/help/en_US/widget/lib/gateway/attributes_updates-identifier_fn.md
  37. 34
      ui-ngx/src/assets/help/en_US/widget/lib/gateway/attributes_updates-path_fn.md
  38. 34
      ui-ngx/src/assets/help/en_US/widget/lib/gateway/device-node-identifier_fn.md
  39. 20
      ui-ngx/src/assets/help/en_US/widget/lib/gateway/device-node-path_fn.md
  40. 67
      ui-ngx/src/assets/help/en_US/widget/lib/gateway/name-field-identifier_fn.md
  41. 41
      ui-ngx/src/assets/help/en_US/widget/lib/gateway/name-field-path_fn.md
  42. 60
      ui-ngx/src/assets/help/en_US/widget/lib/gateway/profile-name-identifier_fn.md
  43. 38
      ui-ngx/src/assets/help/en_US/widget/lib/gateway/profile-name-path_fn.md
  44. 62
      ui-ngx/src/assets/help/en_US/widget/lib/gateway/timeseries-identifier_fn.md
  45. 45
      ui-ngx/src/assets/help/en_US/widget/lib/gateway/timeseries-path_fn.md
  46. 50
      ui-ngx/src/assets/locale/locale.constant-en_US.json
  47. 78
      ui-ngx/src/assets/metadata/connector-default-configs/opcua.json

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

@ -0,0 +1,86 @@
<!--
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]="brokerConfigFormGroup">
<div 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="brokerConfigFormGroup.get('host').hasError('required')
&& brokerConfigFormGroup.get('host').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.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]="portErrorTooltip"
*ngIf="(brokerConfigFormGroup.get('port').hasError('required') ||
brokerConfigFormGroup.get('port').hasError('min') ||
brokerConfigFormGroup.get('port').hasError('max')) &&
brokerConfigFormGroup.get('port').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.mqtt-version</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="version">
<mat-option *ngFor="let version of mqttVersions" [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" translate>gateway.client-id</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="clientId" placeholder="{{ 'gateway.set' | translate }}"/>
<button type="button"
matSuffix
mat-icon-button
aria-label="Generate"
matTooltip="{{ 'gateway.generate-client-id' | translate }}"
matTooltipPosition="above"
(click)="generate('clientId')"
*ngIf="!brokerConfigFormGroup.get('clientId').value">
<mat-icon>autorenew</mat-icon>
</button>
</mat-form-field>
</div>
</div>
<tb-security-config [formControl]="brokerConfigFormGroup.get('security')">
</tb-security-config>
</div>

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

@ -0,0 +1,129 @@
///
/// 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,
Input,
OnDestroy,
OnInit
} from '@angular/core';
import {
ControlContainer,
ControlValueAccessor,
FormBuilder,
FormGroup,
NG_VALUE_ACCESSOR,
UntypedFormGroup,
Validators
} from '@angular/forms';
import {
MqttVersions,
noLeadTrailSpacesRegex,
PortLimits,
} from '@home/components/widget/lib/gateway/gateway-widget.models';
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';
@Component({
selector: 'tb-broker-config-control',
templateUrl: './broker-config-control.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [
CommonModule,
SharedModule,
SecurityConfigComponent,
],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => BrokerConfigControlComponent),
multi: true
}
]
})
export class BrokerConfigControlComponent implements ControlValueAccessor, OnInit, OnDestroy {
@Input() controlKey = 'broker';
brokerConfigFormGroup: UntypedFormGroup;
mqttVersions = MqttVersions;
portLimits = PortLimits;
get parentFormGroup(): FormGroup {
return this.parentContainer.control as FormGroup;
}
private parentContainer = inject(ControlContainer);
private translate = inject(TranslateService);
constructor(private fb: FormBuilder) {
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)]],
maxNumberOfWorkers: [100, [Validators.required, Validators.min(1)]],
maxMessageNumberPerWorker: [10, [Validators.required, Validators.min(1)]],
security: [{}, [Validators.required]]
});
}
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 '';
}
ngOnInit(): void {
this.addSelfControl();
}
ngOnDestroy(): void {
this.removeSelfControl();
}
generate(formControlName: string): void {
this.brokerConfigFormGroup.get(formControlName)?.patchValue('tb_gw_' + generateSecret(5));
}
registerOnChange(fn: any): void {}
registerOnTouched(fn: any): void {}
writeValue(obj: any): void {}
private addSelfControl(): void {
this.parentFormGroup.addControl(this.controlKey, this.brokerConfigFormGroup);
}
private removeSelfControl(): void {
this.parentFormGroup.removeControl(this.controlKey);
}
}

10
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/device-info-table.component.html → ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/device-info-table/device-info-table.component.html

@ -50,9 +50,10 @@
class="tb-error">
warning
</mat-icon>
<div matSuffix
<div *ngIf="'name-field' | getGatewayHelpLink : mappingFormGroup.get('deviceNameExpressionSource').value : sourceTypes"
matSuffix
class="see-example"
[tb-help-popup]="'widget/lib/gateway/expressions_fn'"
[tb-help-popup]="'name-field' | getGatewayHelpLink : mappingFormGroup.get('deviceNameExpressionSource').value : sourceTypes"
tb-help-popup-placement="left"
[tb-help-popup-style]="{maxWidth: '970px'}">
</div>
@ -83,9 +84,10 @@
class="tb-error">
warning
</mat-icon>
<div matSuffix
<div *ngIf="'profile-name' | getGatewayHelpLink: mappingFormGroup.get('deviceProfileExpressionSource').value : sourceTypes"
matSuffix
class="see-example"
[tb-help-popup]="'widget/lib/gateway/expressions_fn'"
[tb-help-popup]="'profile-name' | getGatewayHelpLink: mappingFormGroup.get('deviceProfileExpressionSource').value : sourceTypes"
tb-help-popup-placement="left"
[tb-help-popup-style]="{maxWidth: '970px'}">
</div>

0
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/device-info-table.component.scss → ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/device-info-table/device-info-table.component.scss

25
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/device-info-table.component.ts → ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/device-info-table/device-info-table.component.ts

@ -16,27 +16,19 @@
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
ElementRef,
forwardRef,
Input,
NgZone,
OnDestroy,
OnInit,
ViewContainerRef
} from '@angular/core';
import { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { TranslateService } from '@ngx-translate/core';
import { MatDialog } from '@angular/material/dialog';
import { DialogService } from '@core/services/dialog.service';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { Overlay } from '@angular/cdk/overlay';
import { UtilsService } from '@core/services/utils.service';
import { EntityService } from '@core/http/entity.service';
import {
ControlValueAccessor,
FormBuilder,
@ -50,6 +42,7 @@ import {
import {
DeviceInfoType,
noLeadTrailSpacesRegex,
OPCUaSourceTypes,
SourceTypes,
SourceTypeTranslationsMap
} from '@home/components/widget/lib/gateway/gateway-widget.models';
@ -88,7 +81,7 @@ export class DeviceInfoTableComponent extends PageComponent implements ControlVa
required = false;
@Input()
sourceTypes: Array<SourceTypes> = Object.values(SourceTypes);
sourceTypes: Array<SourceTypes | OPCUaSourceTypes> = Object.values(SourceTypes);
deviceInfoTypeValue: any;
@ -111,14 +104,6 @@ export class DeviceInfoTableComponent extends PageComponent implements ControlVa
constructor(protected store: Store<AppState>,
public translate: TranslateService,
public dialog: MatDialog,
private overlay: Overlay,
private viewContainerRef: ViewContainerRef,
private dialogService: DialogService,
private entityService: EntityService,
private utils: UtilsService,
private zone: NgZone,
private cd: ChangeDetectorRef,
private elementRef: ElementRef,
private fb: FormBuilder) {
super(store);
}
@ -131,13 +116,13 @@ export class DeviceInfoTableComponent extends PageComponent implements ControlVa
if (this.useSource) {
this.mappingFormGroup.addControl('deviceNameExpressionSource',
this.fb.control(SourceTypes.MSG, []));
this.fb.control(this.sourceTypes[0], []));
}
if (this.deviceInfoType === DeviceInfoType.FULL) {
if (this.useSource) {
this.mappingFormGroup.addControl('deviceProfileExpressionSource',
this.fb.control(SourceTypes.MSG, []));
this.fb.control(this.sourceTypes[0], []));
}
this.mappingFormGroup.addControl('deviceProfileExpression',
this.fb.control('', this.required ?
@ -154,6 +139,7 @@ export class DeviceInfoTableComponent extends PageComponent implements ControlVa
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
super.ngOnDestroy();
}
registerOnChange(fn: any): void {
@ -175,5 +161,4 @@ export class DeviceInfoTableComponent extends PageComponent implements ControlVa
updateView(value: any) {
this.propagateChange(value);
}
}

24
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/index.ts

@ -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.
///
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';

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

@ -26,14 +26,15 @@
<mat-expansion-panel class="tb-settings" [expanded]="last">
<mat-expansion-panel-header fxLayout="row wrap">
<mat-panel-title>
<div class="title-container">{{ keyControl.get('key').value }}</div>
{{ '-' }}
<div class="title-container">{{ valueTitle(keyControl.get('value').value) }}</div>
<div class="title-container" *ngIf="keysType !== MappingKeysType.RPC_METHODS">
{{ keyControl.get('key').value }}{{ '-' }}
</div>
<div class="title-container">{{ valueTitle(keyControl) }}</div>
</mat-panel-title>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<div class="tb-form-panel no-border no-padding"
*ngIf="keysType !== MappingKeysType.CUSTOM; else customPanel">
*ngIf="keysType !== MappingKeysType.CUSTOM && keysType !== MappingKeysType.RPC_METHODS">
<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">
@ -53,12 +54,6 @@
class="tb-error">
warning
</mat-icon>
<div matSuffix
class="see-example"
[tb-help-popup]="'widget/lib/gateway/expressions_fn'"
tb-help-popup-placement="left"
[tb-help-popup-style]="{maxWidth: '970px'}">
</div>
</mat-form-field>
</div>
</div>
@ -71,18 +66,25 @@
<mat-select name="valueType" formControlName="type">
<mat-select-trigger *ngIf="!rawData">
<div class="tb-flex align-center">
<mat-icon class="tb-mat-18" [svgIcon]="valueTypes.get(keyControl.get('type').value)?.icon">
<mat-icon *ngIf="valueTypes.get(keyControl.get('type').value)?.icon" class="tb-mat-18"
[svgIcon]="valueTypes.get(keyControl.get('type').value)?.icon">
</mat-icon>
<span>
{{ (rawData ? 'gateway.raw' : valueTypes.get(keyControl.get('type').value)?.name) | translate}}
<span *ngIf="!rawData; else rawText">
{{ (valueTypes.get(keyControl.get('type').value)?.name || valueTypes.get(keyControl.get('type').value)) | translate }}
</span>
<ng-template #rawText>
<span>{{ 'gateway.raw' | translate }}</span>
</ng-template>
</div>
</mat-select-trigger>
<ng-container *ngIf="!rawData; else rawOption">
<mat-option *ngFor="let valueType of valueTypeKeys" [value]="valueType">
<mat-icon class="tb-mat-20" svgIcon="{{ valueTypes.get(valueType).icon }}">
<mat-icon *ngIf="valueTypes.get(valueType).icon" class="tb-mat-20"
svgIcon="{{ valueTypes.get(valueType).icon }}">
</mat-icon>
<span>{{ valueTypes.get(valueType).name | translate }}</span>
<span>
{{ valueTypes.get(valueType).name || valueTypes.get(valueType) | translate }}
</span>
</mat-option>
</ng-container>
<ng-template #rawOption>
@ -112,7 +114,8 @@
</mat-icon>
<div matSuffix
class="see-example"
[tb-help-popup]="'widget/lib/gateway/expressions_fn'"
*ngIf="this.keysType | getGatewayHelpLink : keyControl.get('type').value : valueTypeKeys"
[tb-help-popup]="this.keysType | getGatewayHelpLink : keyControl.get('type').value : valueTypeKeys"
tb-help-popup-placement="left"
[tb-help-popup-style]="{maxWidth: '970px'}">
</div>
@ -120,7 +123,7 @@
</div>
</div>
</div>
<ng-template #customPanel>
<div class="tb-form-panel no-border no-padding" *ngIf="keysType === MappingKeysType.CUSTOM">
<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">
@ -152,7 +155,41 @@
</mat-icon>
</mat-form-field>
</div>
</ng-template>
</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>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="method" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.method-required') | translate"
*ngIf="keyControl.get('method').hasError('required') && keyControl.get('method').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
</div>
<div class="tb-form-panel stroked tb-flex">
<mat-expansion-panel class="tb-settings">
<mat-expansion-panel-header fxLayout="row wrap">
<mat-panel-title>
<div class="title-container" tb-hint-tooltip-icon="{{ 'gateway.hints.arguments' | translate }}">
{{ 'gateway.arguments' | translate }}{{' (' + keyControl.get('arguments').value?.length + ')'}}
</div>
</mat-panel-title>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<tb-type-value-panel formControlName="arguments"></tb-type-value-panel>
</ng-template>
</mat-expansion-panel>
</div>
</div>
</ng-template>
</mat-expansion-panel>
</ng-container>

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

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

@ -23,6 +23,8 @@ import {
} from '@angular/core';
import {
AbstractControl,
FormControl,
FormGroup,
UntypedFormArray,
UntypedFormBuilder,
Validators
@ -39,7 +41,9 @@ import {
MappingKeysType,
MappingValueType,
mappingValueTypesMap,
noLeadTrailSpacesRegex
noLeadTrailSpacesRegex,
OPCUaSourceTypes,
RpcMethodsMapping,
} from '@home/components/widget/lib/gateway/gateway-widget.models';
@Component({
@ -66,7 +70,16 @@ export class MappingDataKeysPanelComponent extends PageComponent implements OnIn
keys: Array<MappingDataKey> | {[key: string]: any};
@Input()
keysType: string;
keysType: MappingKeysType;
@Input()
valueTypeKeys: Array<MappingValueType | OPCUaSourceTypes> = Object.values(MappingValueType);
@Input()
valueTypeEnum = MappingValueType;
@Input()
valueTypes: Map<string, any> = mappingValueTypesMap;
@Input()
@coerceBoolean()
@ -76,16 +89,10 @@ export class MappingDataKeysPanelComponent extends PageComponent implements OnIn
popover: TbPopoverComponent<MappingDataKeysPanelComponent>;
@Output()
keysDataApplied = new EventEmitter<Array<MappingDataKey> | {[key: string]: any}>();
valueTypeKeys = Object.values(MappingValueType);
keysDataApplied = new EventEmitter<Array<MappingDataKey> | {[key: string]: unknown}>();
MappingKeysType = MappingKeysType;
valueTypeEnum = MappingValueType;
valueTypes = mappingValueTypesMap;
dataKeyType: DataKeyType;
keysListFormArray: UntypedFormArray;
@ -97,8 +104,8 @@ export class MappingDataKeysPanelComponent extends PageComponent implements OnIn
super(store);
}
ngOnInit() {
this.keysListFormArray = this.prepareKeysFormArray(this.keys)
ngOnInit(): void {
this.keysListFormArray = this.prepareKeysFormArray(this.keys);
}
trackByKey(index: number, keyControl: AbstractControl): any {
@ -106,12 +113,21 @@ export class MappingDataKeysPanelComponent extends PageComponent implements OnIn
}
addKey(): void {
const dataKeyFormGroup = this.fb.group({
key: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
value: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]]
});
if (this.keysType !== MappingKeysType.CUSTOM) {
dataKeyFormGroup.addControl('type', this.fb.control(this.rawData ? 'raw' : MappingValueType.STRING));
let dataKeyFormGroup: FormGroup;
if (this.keysType === MappingKeysType.RPC_METHODS) {
dataKeyFormGroup = this.fb.group({
method: ['', [Validators.required]],
arguments: [[], []]
});
} else {
dataKeyFormGroup = this.fb.group({
key: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
value: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]]
});
}
if (this.keysType !== MappingKeysType.CUSTOM && this.keysType !== MappingKeysType.RPC_METHODS) {
const controlValue = this.rawData ? 'raw' : this.valueTypeKeys[0];
dataKeyFormGroup.addControl('type', this.fb.control(controlValue));
}
this.keysListFormArray.push(dataKeyFormGroup);
}
@ -124,11 +140,11 @@ export class MappingDataKeysPanelComponent extends PageComponent implements OnIn
this.keysListFormArray.markAsDirty();
}
cancel() {
cancel(): void {
this.popover?.hide();
}
applyKeysData() {
applyKeysData(): void {
let keys = this.keysListFormArray.value;
if (this.keysType === MappingKeysType.CUSTOM) {
keys = {};
@ -139,7 +155,7 @@ export class MappingDataKeysPanelComponent extends PageComponent implements OnIn
this.keysDataApplied.emit(keys);
}
private prepareKeysFormArray(keys: Array<MappingDataKey> | {[key: string]: any}): UntypedFormArray {
private prepareKeysFormArray(keys: Array<MappingDataKey | RpcMethodsMapping> | {[key: string]: any}): UntypedFormArray {
const keysControlGroups: Array<AbstractControl> = [];
if (keys) {
if (this.keysType === MappingKeysType.CUSTOM) {
@ -148,19 +164,28 @@ export class MappingDataKeysPanelComponent extends PageComponent implements OnIn
});
}
keys.forEach((keyData) => {
const { key, value, type } = keyData;
const dataKeyFormGroup = this.fb.group({
key: [key, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
value: [value, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
type: [type, []]
});
let dataKeyFormGroup: FormGroup;
if (this.keysType === MappingKeysType.RPC_METHODS) {
dataKeyFormGroup = this.fb.group({
method: [keyData.method, [Validators.required]],
arguments: [[...keyData.arguments], []]
});
} else {
const { key, value, type } = keyData;
dataKeyFormGroup = this.fb.group({
key: [key, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
value: [value, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
type: [type, []]
});
}
keysControlGroups.push(dataKeyFormGroup);
});
}
return this.fb.array(keysControlGroups);
}
valueTitle(value: any): string {
valueTitle(keyControl: FormControl): string {
const value = keyControl.get(this.keysType === MappingKeysType.RPC_METHODS ? 'method' : 'value').value;
if (isDefinedAndNotNull(value)) {
if (typeof value === 'object') {
return JSON.stringify(value);
@ -169,5 +194,4 @@ export class MappingDataKeysPanelComponent extends PageComponent implements OnIn
}
return '';
}
}

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

@ -60,10 +60,12 @@
<div class="table-container">
<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.REQUESTS">
<mat-header-cell *matHeaderCellDef class="table-value-column"
[class.request-column]="mappingType !== mappingTypeEnum.DATA">
{{ column.title | translate }}
</mat-header-cell>
<mat-cell *matCellDef="let mapping" class="table-value-column" [class.request-column]="mappingType === mappingTypeEnum.REQUESTS">
<mat-cell *matCellDef="let mapping" class="table-value-column"
[class.request-column]="mappingType !== mappingTypeEnum.DATA">
{{ mapping[column.def] }}
</mat-cell>
</ng-container>

0
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table.component.scss → ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table/mapping-table.component.scss

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

@ -17,41 +17,43 @@
import {
AfterViewInit,
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
ElementRef,
forwardRef,
inject,
Input,
NgZone,
OnDestroy,
OnInit,
ViewChild,
ViewContainerRef
} from '@angular/core';
import { PageComponent } from '@shared/components/page.component';
import { PageLink } from '@shared/models/page/page-link';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
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, takeUntil } from 'rxjs/operators';
import { Overlay } from '@angular/cdk/overlay';
import { UtilsService } from '@core/services/utils.service';
import { EntityService } from '@core/http/entity.service';
import { ControlValueAccessor, FormBuilder, NG_VALUE_ACCESSOR, UntypedFormArray } from '@angular/forms';
import { debounceTime, distinctUntilChanged, map, take, takeUntil } from 'rxjs/operators';
import {
ConvertorTypeTranslationsMap,
ControlContainer,
ControlValueAccessor,
FormBuilder,
FormGroup,
NG_VALUE_ACCESSOR,
UntypedFormArray,
} from '@angular/forms';
import {
ConnectorMapping, ConverterConnectorMapping,
ConvertorTypeTranslationsMap, DeviceConnectorMapping,
MappingInfo,
MappingType,
MappingTypeTranslationsMap,
MappingTypeTranslationsMap, MappingValue, RequestMappingData,
RequestType,
RequestTypesTranslationsMap
} from '@home/components/widget/lib/gateway/gateway-widget.models';
import { CollectionViewer, 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 { validateArrayIsNotEmpty } from '@shared/validators/form-array.validators';
@Component({
selector: 'tb-mapping-table',
@ -66,8 +68,13 @@ import { isDefinedAndNotNull, isUndefinedOrNull } from '@core/utils';
}
]
})
export class MappingTableComponent extends PageComponent implements ControlValueAccessor, AfterViewInit, OnInit, OnDestroy {
export class MappingTableComponent implements ControlValueAccessor, AfterViewInit, OnInit, OnDestroy {
@Input() controlKey = 'dataMapping';
@coerceBoolean()
@Input() required = false;
parentContainer = inject(ControlContainer);
mappingTypeTranslationsMap = MappingTypeTranslationsMap;
mappingTypeEnum = MappingType;
displayedColumns = [];
@ -79,14 +86,16 @@ export class MappingTableComponent extends PageComponent implements ControlValue
activeValue = false;
dirtyValue = false;
viewsInited = false;
mappingTypeValue: MappingType;
get mappingType(): MappingType {
return this.mappingTypeValue;
}
get parentFormGroup(): FormGroup {
return this.parentContainer.control as FormGroup;
}
@Input()
set mappingType(value: MappingType) {
if (this.mappingTypeValue !== value) {
@ -100,54 +109,34 @@ export class MappingTableComponent extends PageComponent implements ControlValue
textSearch = this.fb.control('', {nonNullable: true});
private destroy$ = new Subject<void>();
private propagateChange = (v: any) => {};
constructor(protected store: Store<AppState>,
public translate: TranslateService,
constructor(public translate: TranslateService,
public dialog: MatDialog,
private overlay: Overlay,
private viewContainerRef: ViewContainerRef,
private dialogService: DialogService,
private entityService: EntityService,
private utils: UtilsService,
private zone: NgZone,
private cd: ChangeDetectorRef,
private elementRef: ElementRef,
private fb: FormBuilder) {
super(store);
this.mappingFormGroup = this.fb.array([]);
this.dirtyValue = !this.activeValue;
this.dataSource = new MappingDatasource();
}
ngOnInit() {
if (this.mappingType === MappingType.DATA) {
this.mappingColumns.push(
{def: 'topicFilter', title: 'gateway.topic-filter'},
{def: 'QoS', title: 'gateway.mqtt-qos'},
{def: 'converter', title: 'gateway.payload-type'}
)
} else {
this.mappingColumns.push(
{def: 'type', title: 'gateway.type'},
{def: 'details', title: 'gateway.details'}
);
}
ngOnInit(): void {
this.setMappingColumns();
this.displayedColumns.push(...this.mappingColumns.map(column => column.def), 'actions');
this.mappingFormGroup.valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe((value) => {
this.updateTableData(value);
this.updateView(value);
});
this.addSelfControl();
}
ngOnDestroy() {
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
this.removeSelfControl();
}
ngAfterViewInit() {
ngAfterViewInit(): void {
this.textSearch.valueChanges.pipe(
debounceTime(150),
distinctUntilChanged((prev, current) => (prev ?? '') === current.trim()),
@ -158,55 +147,13 @@ export class MappingTableComponent extends PageComponent implements ControlValue
});
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnChange(fn: any): void {}
registerOnTouched(fn: any): void {}
writeValue(config: any) {
if (isUndefinedOrNull(config)) {
config = this.mappingType === MappingType.REQUESTS ? {} : [];
}
let mappingConfigs = config;
if (this.mappingType === MappingType.REQUESTS) {
mappingConfigs = [];
Object.keys(config).forEach((configKey) => {
for (let mapping of config[configKey]) {
mappingConfigs.push({
requestType: configKey,
requestValue: mapping
});
}
});
}
this.mappingFormGroup.clear({emitEvent: false});
for (let mapping of mappingConfigs) {
this.mappingFormGroup.push(this.fb.group(mapping), {emitEvent: false});
}
this.updateTableData(mappingConfigs);
}
writeValue(obj: any): void {}
updateView(mappingConfigs: Array<{[key: string]: any}>) {
let config;
if (this.mappingType === MappingType.REQUESTS) {
config = {};
for (let mappingConfig of mappingConfigs) {
if (config[mappingConfig.requestType]) {
config[mappingConfig.requestType].push(mappingConfig.requestValue);
} else {
config[mappingConfig.requestType] = [mappingConfig.requestValue];
}
}
} else {
config = mappingConfigs;
}
this.propagateChange(config);
}
enterFilterMode() {
enterFilterMode(): void {
this.textSearchMode = true;
setTimeout(() => {
this.searchInputField.nativeElement.focus();
@ -214,18 +161,18 @@ export class MappingTableComponent extends PageComponent implements ControlValue
}, 10);
}
exitFilterMode() {
exitFilterMode(): void {
this.updateTableData(this.mappingFormGroup.value);
this.textSearchMode = false;
this.textSearch.reset();
}
manageMapping($event: Event, index?: number) {
manageMapping($event: Event, index?: number): void {
if ($event) {
$event.stopPropagation();
}
const value = isDefinedAndNotNull(index) ? this.mappingFormGroup.at(index).value : {};
this.dialog.open<MappingDialogComponent, MappingInfo, boolean>(MappingDialogComponent, {
this.dialog.open<MappingDialogComponent, MappingInfo, MappingValue>(MappingDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
@ -233,45 +180,23 @@ export class MappingTableComponent extends PageComponent implements ControlValue
value,
buttonTitle: isUndefinedOrNull(index) ? 'action.add' : 'action.apply'
}
}).afterClosed().subscribe(
(res) => {
}).afterClosed()
.pipe(take(1), takeUntil(this.destroy$))
.subscribe(res => {
if (res) {
if (isDefinedAndNotNull(index)) {
this.mappingFormGroup.at(index).patchValue(res);
} else {
this.mappingFormGroup.push(this.fb.group(res));
}
this.mappingFormGroup.markAsDirty();
}
}
);
});
}
updateTableData(value: Array<{[key: string]: any}>, textSearch?: string): void {
let tableValue = value;
if (this.mappingType === MappingType.DATA) {
tableValue = tableValue.map((value) => {
return {
topicFilter: value.topicFilter,
QoS: value.subscriptionQos,
converter: this.translate.instant(ConvertorTypeTranslationsMap.get(value.converter.type))
};
});
} else {
tableValue = tableValue.map((value) => {
let details;
if (value.requestType === RequestType.ATTRIBUTE_UPDATE) {
details = value.requestValue.attributeFilter;
} else if (value.requestType === RequestType.SERVER_SIDE_RPC) {
details = value.requestValue.methodFilter;
} else {
details = value.requestValue.topicFilter;
}
return {
type: this.translate.instant(RequestTypesTranslationsMap.get(value.requestType)),
details
};
});
}
updateTableData(value: ConnectorMapping[], textSearch?: string): void {
let tableValue =
value.map((value: ConnectorMapping) => this.getMappingValue(value));
if (textSearch) {
tableValue = tableValue.filter(value =>
Object.values(value).some(val =>
@ -282,7 +207,7 @@ export class MappingTableComponent extends PageComponent implements ControlValue
this.dataSource.loadMappings(tableValue);
}
deleteMapping($event: Event, index: number) {
deleteMapping($event: Event, index: number): void {
if ($event) {
$event.stopPropagation();
}
@ -295,10 +220,81 @@ export class MappingTableComponent extends PageComponent implements ControlValue
).subscribe((result) => {
if (result) {
this.mappingFormGroup.removeAt(index);
this.mappingFormGroup.markAsDirty();
}
});
}
private getMappingValue(value: ConnectorMapping): MappingValue {
switch (this.mappingType) {
case MappingType.DATA:
return {
topicFilter: (value as ConverterConnectorMapping).topicFilter,
QoS: (value as ConverterConnectorMapping).subscriptionQos,
converter: this.translate.instant(ConvertorTypeTranslationsMap.get((value as ConverterConnectorMapping).converter.type))
};
case MappingType.REQUESTS:
let details;
if ((value as RequestMappingData).requestType === RequestType.ATTRIBUTE_UPDATE) {
details = (value as RequestMappingData).requestValue.attributeFilter;
} else if ((value as RequestMappingData).requestType === RequestType.SERVER_SIDE_RPC) {
details = (value as RequestMappingData).requestValue.methodFilter;
} else {
details = (value as RequestMappingData).requestValue.topicFilter;
}
return {
requestType: (value as RequestMappingData).requestType,
type: this.translate.instant(RequestTypesTranslationsMap.get((value as RequestMappingData).requestType)),
details
};
case MappingType.OPCUA:
const deviceNamePattern = (value as DeviceConnectorMapping).deviceInfo?.deviceNameExpression;
const deviceProfileExpression = (value as DeviceConnectorMapping).deviceInfo?.deviceProfileExpression;
const { deviceNodePattern } = value as DeviceConnectorMapping;
return {
deviceNodePattern,
deviceNamePattern,
deviceProfileExpression
};
default:
return {} as MappingValue;
}
}
private setMappingColumns(): void {
switch (this.mappingType) {
case MappingType.DATA:
this.mappingColumns.push(
{ def: 'topicFilter', title: 'gateway.topic-filter' },
{ def: 'QoS', title: 'gateway.mqtt-qos' },
{ def: 'converter', title: 'gateway.payload-type' }
);
break;
case MappingType.REQUESTS:
this.mappingColumns.push(
{ def: 'type', title: 'gateway.type' },
{ def: 'details', title: 'gateway.details' }
);
break;
case MappingType.OPCUA:
this.mappingColumns.push(
{ def: 'deviceNodePattern', title: 'gateway.device-node' },
{ def: 'deviceNamePattern', title: 'gateway.device-name' },
{ def: 'deviceProfileExpression', title: 'gateway.device-profile' }
);
}
}
private addSelfControl(): void {
this.parentFormGroup.addControl(this.controlKey, this.mappingFormGroup);
if (this.required) {
this.mappingFormGroup.addValidators(validateArrayIsNotEmpty());
}
}
private removeSelfControl(): void {
this.parentFormGroup.removeControl(this.controlKey);
}
}
export class MappingDatasource implements DataSource<{[key: string]: any}> {
@ -335,5 +331,4 @@ export class MappingDatasource implements DataSource<{[key: string]: any}> {
map((mappings) => mappings.length)
);
}
}

44
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-security.component.html → ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/security-config/security-config.component.html

@ -17,7 +17,7 @@
-->
<div class="tb-form-row space-between same-padding tb-flex column" [formGroup]="securityFormGroup">
<div class="tb-flex row space-between align-center no-gap fill-width">
<div class="fixed-title-width tb-required" translate>gateway.security</div>
<div class="fixed-title-width tb-required">{{ title | translate }}</div>
<tb-toggle-select formControlName="type" appearance="fill">
<tb-toggle-option *ngFor="let type of securityTypes" [value]="type">
{{ SecurityTypeTranslationsMap.get(type) | translate }}
@ -81,6 +81,48 @@
</mat-form-field>
</div>
</div>
<ng-container *ngIf="extendCertificatesModel">
<div class="tb-form-row space-between tb-flex fill-width">
<div class="fixed-title-width" translate>gateway.mode</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="mode">
<mat-option *ngFor="let type of modeTypes" [value]="type">
{{ type }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
<div class="tb-form-row space-between tb-flex fill-width">
<div class="fixed-title-width" translate>gateway.username</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="username" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.username-required') | translate"
*ngIf="securityFormGroup.get('username').hasError('required')
&& securityFormGroup.get('username').touched"
class="tb-error">
warning
</mat-icon>
</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>
</ng-container>
</ng-template>
</ng-container>
</div>

0
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-security.component.scss → ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/security-config/security-config.component.scss

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

@ -16,85 +16,72 @@
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
ElementRef,
forwardRef,
NgZone,
Input,
OnDestroy,
ViewContainerRef
OnInit,
} from '@angular/core';
import { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { TranslateService } from '@ngx-translate/core';
import { MatDialog } from '@angular/material/dialog';
import { DialogService } from '@core/services/dialog.service';
import { Subject } from 'rxjs';
import { Overlay } from '@angular/cdk/overlay';
import { UtilsService } from '@core/services/utils.service';
import { EntityService } from '@core/http/entity.service';
import {
ControlValueAccessor,
FormBuilder,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
UntypedFormGroup,
ValidationErrors,
Validator,
Validators
} from '@angular/forms';
import {
BrokerSecurityType,
BrokerSecurityTypeTranslationsMap,
ModeType,
noLeadTrailSpacesRegex
} from '@home/components/widget/lib/gateway/gateway-widget.models';
import { takeUntil } from 'rxjs/operators';
import { coerceBoolean } from '@shared/decorators/coercion';
import { SharedModule } from '@shared/shared.module';
import { CommonModule } from '@angular/common';
@Component({
selector: 'tb-broker-security',
templateUrl: './broker-security.component.html',
styleUrls: ['./broker-security.component.scss'],
selector: 'tb-security-config',
templateUrl: './security-config.component.html',
styleUrls: ['./security-config.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => BrokerSecurityComponent),
useExisting: forwardRef(() => SecurityConfigComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => BrokerSecurityComponent),
multi: true
}
],
standalone: true,
imports:[
CommonModule,
SharedModule,
]
})
export class BrokerSecurityComponent extends PageComponent implements ControlValueAccessor, Validator, OnDestroy {
export class SecurityConfigComponent implements ControlValueAccessor, OnInit, OnDestroy {
@Input()
title: string = 'gateway.security';
@Input()
@coerceBoolean()
extendCertificatesModel = false;
BrokerSecurityType = BrokerSecurityType;
securityTypes = Object.values(BrokerSecurityType);
modeTypes = Object.values(ModeType);
SecurityTypeTranslationsMap = BrokerSecurityTypeTranslationsMap;
securityFormGroup: UntypedFormGroup;
private destroy$ = new Subject<void>();
private propagateChange = (v: any) => {};
constructor(protected store: Store<AppState>,
public translate: TranslateService,
public dialog: MatDialog,
private overlay: Overlay,
private viewContainerRef: ViewContainerRef,
private dialogService: DialogService,
private entityService: EntityService,
private utils: UtilsService,
private zone: NgZone,
private cd: ChangeDetectorRef,
private elementRef: ElementRef,
private fb: FormBuilder) {
super(store);
constructor(private fb: FormBuilder) {}
ngOnInit(): void {
this.securityFormGroup = this.fb.group({
type: [BrokerSecurityType.ANONYMOUS, []],
username: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
@ -103,11 +90,9 @@ export class BrokerSecurityComponent extends PageComponent implements ControlVal
pathToPrivateKey: ['', [Validators.pattern(noLeadTrailSpacesRegex)]],
pathToClientCert: ['', [Validators.pattern(noLeadTrailSpacesRegex)]]
});
this.securityFormGroup.valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe((value) => {
this.updateView(value);
});
if (this.extendCertificatesModel) {
this.securityFormGroup.addControl('mode', this.fb.control(ModeType.NONE, []));
}
this.securityFormGroup.get('type').valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe((type) => {
@ -115,43 +100,25 @@ export class BrokerSecurityComponent extends PageComponent implements ControlVal
});
}
ngOnDestroy() {
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
super.ngOnDestroy();
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnChange(fn: any): void {}
registerOnTouched(fn: any): void {}
writeValue(deviceInfo: any) {
if (!deviceInfo.type) {
deviceInfo.type = BrokerSecurityType.ANONYMOUS;
}
this.securityFormGroup.reset(deviceInfo);
this.updateView(deviceInfo);
}
writeValue(obj: any): void {}
validate(): ValidationErrors | null {
return this.securityFormGroup.valid ? null : {
securityForm: { valid: false }
};
}
updateView(value: any) {
this.propagateChange(value);
}
private updateValidators(type) {
private updateValidators(type): void {
if (type) {
this.securityFormGroup.get('username').disable({emitEvent: false});
this.securityFormGroup.get('password').disable({emitEvent: false});
this.securityFormGroup.get('pathToCACert').disable({emitEvent: false});
this.securityFormGroup.get('pathToPrivateKey').disable({emitEvent: false});
this.securityFormGroup.get('pathToClientCert').disable({emitEvent: false});
this.securityFormGroup.get('mode')?.disable({emitEvent: false});
if (type === BrokerSecurityType.BASIC) {
this.securityFormGroup.get('username').enable({emitEvent: false});
this.securityFormGroup.get('password').enable({emitEvent: false});
@ -159,6 +126,16 @@ export class BrokerSecurityComponent extends PageComponent implements ControlVal
this.securityFormGroup.get('pathToCACert').enable({emitEvent: false});
this.securityFormGroup.get('pathToPrivateKey').enable({emitEvent: false});
this.securityFormGroup.get('pathToClientCert').enable({emitEvent: false});
if (this.extendCertificatesModel) {
const modeControl = this.securityFormGroup.get('mode');
if (modeControl && !modeControl.value) {
modeControl.setValue(ModeType.NONE, {emitEvent: false});
}
modeControl?.enable({emitEvent: false});
this.securityFormGroup.get('username').enable({emitEvent: false});
this.securityFormGroup.get('password').enable({emitEvent: false});
}
}
}
}

125
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/server-config/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" 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 }}" 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="fixed-title-width" 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 serverSecurityTypes" [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 }}" 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="fixed-title-width" 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 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 [formControl]="serverConfigFormGroup.get('identity')"
[extendCertificatesModel]="true">
</tb-security-config>
</div>

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

@ -0,0 +1,20 @@
/**
* 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 {
width: 100%;
height: 100%;
display: block;
}

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

@ -0,0 +1,106 @@
///
/// 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, Input,
OnDestroy, OnInit
} from '@angular/core';
import {
ControlContainer,
ControlValueAccessor,
FormBuilder, FormGroup,
NG_VALUE_ACCESSOR,
UntypedFormGroup,
Validators
} from '@angular/forms';
import {
noLeadTrailSpacesRegex,
SecurityType,
ServerSecurityTypes
} 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';
@Component({
selector: 'tb-server-config',
templateUrl: './server-config.component.html',
styleUrls: ['./server-config.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => ServerConfigComponent),
multi: true
},
],
standalone: true,
imports: [
CommonModule,
SharedModule,
SecurityConfigComponent,
]
})
export class ServerConfigComponent implements OnInit, ControlValueAccessor, OnDestroy {
@Input() controlKey = 'server';
serverSecurityTypes = ServerSecurityTypes;
serverConfigFormGroup: UntypedFormGroup;
get parentFormGroup(): FormGroup {
return this.parentContainer.control as FormGroup;
}
private parentContainer = inject(ControlContainer);
constructor(private fb: FormBuilder) {
this.serverConfigFormGroup = this.fb.group({
name: ['', []],
url: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
timeoutInMillis: [1000, [Validators.required, Validators.min(1000)]],
scanPeriodInMillis: [1000, [Validators.required, Validators.min(1000)]],
enableSubscriptions: [true, []],
subCheckPeriodInMillis: [10, [Validators.required, Validators.min(10)]],
showMap: [false, []],
security: [SecurityType.BASIC128, []],
identity: [{}, [Validators.required]]
});
}
ngOnInit(): void {
this.addSelfControl();
}
ngOnDestroy(): void {
this.removeSelfControl();
}
registerOnChange(fn: any): void {}
registerOnTouched(fn: any): void {}
writeValue(obj: any): void {}
private addSelfControl(): void {
this.parentFormGroup.addControl(this.controlKey, this.serverConfigFormGroup);
}
private removeSelfControl(): void {
this.parentFormGroup.removeControl(this.controlKey);
}
}

92
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/type-value-panel/type-value-panel.component.html

@ -0,0 +1,92 @@
<!--
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">
<div class="tb-form-panel no-border no-padding key-panel" *ngIf="valueListFormArray.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 valueListFormArray.controls; trackBy: trackByKey; 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">{{ valueTitle(keyControl.get('value').value) }}</div>
</mat-panel-title>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required" translate>gateway.type</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap fill-width" appearance="outline" subscriptSizing="dynamic">
<mat-select name="valueType" formControlName="type">
<mat-select-trigger>
<div class="tb-flex align-center">
<mat-icon class="tb-mat-18" [svgIcon]="valueTypes.get(keyControl.get('type').value)?.icon">
</mat-icon>
<span>
{{ valueTypes.get(keyControl.get('type').value)?.name | translate}}
</span>
</div>
</mat-select-trigger>
<mat-option *ngFor="let valueType of valueTypeKeys" [value]="valueType">
<mat-icon class="tb-mat-20" svgIcon="{{ valueTypes.get(valueType).icon }}">
</mat-icon>
<span>{{ valueTypes.get(valueType).name | translate }}</span>
</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-required" translate>gateway.value</div>
<mat-form-field fxFlex appearance="outline" subscriptSizing="dynamic" class="tb-inline-field flex tb-suffix-absolute">
<input matInput required 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>
</ng-template>
</mat-expansion-panel>
</ng-container>
</div>
<button type="button"
mat-icon-button
(click)="deleteKey($event, $index)"
[matTooltip]="'gateway.delete-value' | translate"
matTooltipPosition="above">
<mat-icon>delete</mat-icon>
</button>
</div>
</div>
<div>
<button type="button" mat-stroked-button color="primary" (click)="addKey()">
{{ 'gateway.add-value' | translate }}
</button>
</div>
</div>
<ng-template #noKeys>
<div class="tb-flex no-flex center align-center key-panel">
<span class="tb-prompt" translate>{{ 'gateway.no-value' }}</span>
</div>
</ng-template>

52
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/type-value-panel/type-value-panel.component.scss

@ -0,0 +1,52 @@
/**
* 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 {
.title-container {
max-width: 11vw;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap
}
.key-panel {
height: 250px;
overflow: auto;
}
.tb-form-panel {
.mat-mdc-icon-button {
width: 56px;
height: 56px;
padding: 16px;
color: rgba(0, 0, 0, 0.54);
}
}
.see-example {
width: 32px;
height: 32px;
margin: 4px;
}
}
:host ::ng-deep {
.mat-mdc-form-field-icon-suffix {
display: flex;
}
}

144
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/type-value-panel/type-value-panel.component.ts

@ -0,0 +1,144 @@
///
/// 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,
forwardRef,
OnDestroy,
OnInit,
} from '@angular/core';
import {
AbstractControl,
ControlValueAccessor,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
UntypedFormArray,
UntypedFormBuilder,
ValidationErrors,
Validator,
Validators
} from '@angular/forms';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { isDefinedAndNotNull } from '@core/utils';
import {
MappingDataKey,
MappingValueType,
mappingValueTypesMap,
noLeadTrailSpacesRegex
} from '@home/components/widget/lib/gateway/gateway-widget.models';
import { takeUntil } from 'rxjs/operators';
import { Subject } from 'rxjs';
@Component({
selector: 'tb-type-value-panel',
templateUrl: './type-value-panel.component.html',
styleUrls: ['./type-value-panel.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => TypeValuePanelComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => TypeValuePanelComponent),
multi: true
}
]
})
export class TypeValuePanelComponent implements ControlValueAccessor, Validator, OnInit, OnDestroy {
valueTypeKeys = Object.values(MappingValueType);
valueTypeEnum = MappingValueType;
valueTypes = mappingValueTypesMap;
dataKeyType: DataKeyType;
valueListFormArray: UntypedFormArray;
errorText = '';
private destroy$ = new Subject<void>();
private propagateChange = (v: any) => {};
constructor(private fb: UntypedFormBuilder) {}
ngOnInit(): void {
this.valueListFormArray = this.fb.array([]);
this.valueListFormArray.valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe((value) => {
this.updateView(value);
});
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
trackByKey(_: number, keyControl: AbstractControl): any {
return keyControl;
}
addKey(): void {
const dataKeyFormGroup = this.fb.group({
type: [MappingValueType.STRING, []],
value: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]]
});
this.valueListFormArray.push(dataKeyFormGroup);
}
deleteKey($event: Event, index: number): void {
if ($event) {
$event.stopPropagation();
}
this.valueListFormArray.removeAt(index);
this.valueListFormArray.markAsDirty();
}
valueTitle(value: any): string {
if (isDefinedAndNotNull(value)) {
if (typeof value === 'object') {
return JSON.stringify(value);
}
return value;
}
return '';
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {}
writeValue(deviceInfoArray: Array<MappingDataKey>): void {
for (const deviceInfo of deviceInfoArray) {
const dataKeyFormGroup = this.fb.group({
type: [deviceInfo.type, []],
value: [deviceInfo.value, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]]
});
this.valueListFormArray.push(dataKeyFormGroup);
}
}
validate(): ValidationErrors | null {
return this.valueListFormArray.valid ? null : {
valueListForm: { valid: false }
};
}
updateView(value: any): void {
this.propagateChange(value);
}
}

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

@ -0,0 +1,63 @@
<!--
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]="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
</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" type="number" min="1" formControlName="maxNumberOfWorkers"
placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.max-number-of-workers-required') | translate"
*ngIf="workersConfigFormGroup.get('maxNumberOfWorkers').hasError('min') ||
(workersConfigFormGroup.get('maxNumberOfWorkers').hasError('required') &&
workersConfigFormGroup.get('maxNumberOfWorkers').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" [style.width.%]="50"
tb-hint-tooltip-icon="{{ 'gateway.max-messages-queue-for-worker-hint' | translate }}" translate>
gateway.max-messages-queue-for-worker
</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" type="number" min="1" formControlName="maxMessageNumberPerWorker"
placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.max-messages-queue-for-worker-required') | translate"
*ngIf="workersConfigFormGroup.get('maxMessageNumberPerWorker').hasError('min') ||
(workersConfigFormGroup.get('maxMessageNumberPerWorker').hasError('required') &&
workersConfigFormGroup.get('maxMessageNumberPerWorker').touched)"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
</div>
</div>

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

@ -0,0 +1,94 @@
///
/// 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,
Input,
OnDestroy,
OnInit
} from '@angular/core';
import {
ControlContainer,
ControlValueAccessor,
FormBuilder,
FormGroup,
NG_VALUE_ACCESSOR,
UntypedFormGroup,
Validators
} from '@angular/forms';
import { SharedModule } from '@shared/shared.module';
import { CommonModule } from '@angular/common';
@Component({
selector: 'tb-workers-config-control',
templateUrl: './workers-config-control.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [
CommonModule,
SharedModule,
],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => WorkersConfigControlComponent),
multi: true
}
]
})
export class WorkersConfigControlComponent implements ControlValueAccessor, OnInit, OnDestroy {
@Input() controlKey = 'workers';
workersConfigFormGroup: UntypedFormGroup;
get parentFormGroup(): FormGroup {
return this.parentContainer.control as FormGroup;
}
private parentContainer = inject(ControlContainer);
constructor(private fb: FormBuilder) {
this.workersConfigFormGroup = this.fb.group({
maxNumberOfWorkers: [100, [Validators.required, Validators.min(1)]],
maxMessageNumberPerWorker: [10, [Validators.required, Validators.min(1)]],
});
}
ngOnInit(): void {
this.addSelfControl();
}
ngOnDestroy(): void {
this.removeSelfControl();
}
registerOnChange(fn: any): void {}
registerOnTouched(fn: any): void {}
writeValue(obj: any): void {}
private addSelfControl(): void {
this.parentFormGroup.addControl(this.controlKey, this.workersConfigFormGroup);
}
private removeSelfControl(): void {
this.parentFormGroup.removeControl(this.controlKey);
}
}

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

@ -615,6 +615,124 @@
</ng-container>
</ng-container>
</ng-template>
<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>
</div>
<div class="tb-flex device-config">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="deviceNodeSource">
<mat-option *ngFor="let type of [OPCUaSourceTypesEnum.PATH, OPCUaSourceTypesEnum.IDENTIFIER]" [value]="type">
{{ SourceTypeTranslationsMap.get(type) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="deviceNodePattern" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.device-node-required') | translate"
*ngIf="(mappingForm.get('deviceNodePattern').hasError('required') &&
mappingForm.get('deviceNodePattern').touched)"
class="tb-error">
warning
</mat-icon>
<div matSuffix
class="see-example"
[tb-help-popup]="'device-node' | getGatewayHelpLink: mappingForm.get('deviceNodeSource').value"
tb-help-popup-placement="left"
[tb-help-popup-style]="{maxWidth: '970px'}">
</div>
</mat-form-field>
</div>
</div>
<tb-device-info-table formControlName="deviceInfo" [sourceTypes]="OPCUaSourceTypes" [deviceInfoType]="DeviceInfoType.FULL" required="true">
</tb-device-info-table>
<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]="opcAttributes" class="tb-flex">
<mat-chip *ngFor="let attribute of opcAttributes">
{{ attribute }}
</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"
#opcAttributesButton
(click)="manageKeys($event, opcAttributesButton, MappingKeysType.ATTRIBUTES)">
<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]="opcTelemetry">
<mat-chip *ngFor="let telemetry of opcTelemetry">
{{ telemetry }}
</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"
#opcTelemetryButton
(click)="manageKeys($event, opcTelemetryButton, MappingKeysType.TIMESERIES)">
<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]="opcAttributesUpdates" class="tb-flex">
<mat-chip *ngFor="let attribute of opcAttributesUpdates">
{{ attribute }}
</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"
#attributesUpdatesButton
(click)="manageKeys($event, attributesUpdatesButton, MappingKeysType.ATTRIBUTES_UPDATES)">
<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-methods</div>
<div class="tb-flex ellipsis-chips-container">
<mat-chip-listbox [tb-ellipsis-chip-list]="opcRpcMethods" class="tb-flex">
<mat-chip *ngFor="let attribute of opcRpcMethods">
{{ attribute }}
</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"
#rpcMethodsButton
(click)="manageKeys($event, rpcMethodsButton, MappingKeysType.RPC_METHODS)">
<tb-icon matButtonIcon>edit</tb-icon>
</button>
</div>
</div>
</ng-template>
</ng-container>
</div>
</div>

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

@ -33,8 +33,8 @@
}
.mat-mdc-dialog-content {
max-height: 670px;
height: 670px;
max-height: 75vh;
height: 75vh;
}
.ellipsis-chips-container {
@ -73,4 +73,8 @@
.mat-mdc-form-field-icon-suffix {
display: flex;
}
.device-config {
padding-left: 10px;
}
}

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

@ -19,7 +19,6 @@ import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { FormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { BaseData, HasId } from '@shared/models/base-data';
import { DialogComponent } from '@shared/components/dialog.component';
import { Router } from '@angular/router';
import {
@ -35,8 +34,9 @@ import {
MappingKeysPanelTitleTranslationsMap,
MappingKeysType,
MappingType,
MappingTypeTranslationsMap,
MappingTypeTranslationsMap, MappingValue,
noLeadTrailSpacesRegex,
OPCUaSourceTypes,
QualityTypes,
QualityTypeTranslationsMap,
RequestType,
@ -49,16 +49,15 @@ import { Subject } from 'rxjs';
import { startWith, takeUntil } from 'rxjs/operators';
import { MatButton } from '@angular/material/button';
import { TbPopoverService } from '@shared/components/popover.service';
import { MappingDataKeysPanelComponent } from '@home/components/widget/lib/gateway/connectors-configuration/mapping-data-keys-panel.component';
import { TranslateService } from '@ngx-translate/core';
import { MappingDataKeysPanelComponent } from '@home/components/widget/lib/gateway/connectors-configuration';
@Component({
selector: 'tb-mapping-dialog',
templateUrl: './mapping-dialog.component.html',
styleUrls: ['./mapping-dialog.component.scss'],
providers: [],
styleUrls: ['./mapping-dialog.component.scss']
})
export class MappingDialogComponent extends DialogComponent<MappingDialogComponent, BaseData<HasId>> implements OnDestroy {
export class MappingDialogComponent extends DialogComponent<MappingDialogComponent, MappingValue> implements OnDestroy {
mappingForm: UntypedFormGroup;
@ -72,6 +71,8 @@ export class MappingDialogComponent extends DialogComponent<MappingDialogCompone
ConvertorTypeTranslationsMap = ConvertorTypeTranslationsMap;
sourceTypes = Object.values(SourceTypes);
OPCUaSourceTypes = Object.values(OPCUaSourceTypes) as Array<OPCUaSourceTypes>;
OPCUaSourceTypesEnum = OPCUaSourceTypes;
sourceTypesEnum = SourceTypes;
SourceTypeTranslationsMap = SourceTypeTranslationsMap;
@ -102,7 +103,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, {[key: string]: any}>,
public dialogRef: MatDialogRef<MappingDialogComponent, MappingValue>,
private fb: FormBuilder,
private popoverService: TbPopoverService,
private renderer: Renderer2,
@ -125,6 +126,22 @@ export class MappingDialogComponent extends DialogComponent<MappingDialogCompone
}
}
get opcAttributes(): Array<string> {
return this.mappingForm.get('attributes').value?.map(value => value.key) || [];
}
get opcTelemetry(): Array<string> {
return this.mappingForm.get('timeseries').value?.map(value => value.key) || [];
}
get opcRpcMethods(): Array<string> {
return this.mappingForm.get('rpc_methods').value?.map(value => value.method) || [];
}
get opcAttributesUpdates(): Array<string> {
return this.mappingForm.get('attributes_updates')?.value?.map(value => value.key) || [];
}
get converterType(): ConvertorType {
return this.mappingForm.get('converter').get('type').value;
}
@ -155,109 +172,17 @@ export class MappingDialogComponent extends DialogComponent<MappingDialogCompone
}
private createMappingForm(): void {
this.mappingForm = this.fb.group({});
if (this.data.mappingType === MappingType.DATA) {
this.mappingForm.addControl('topicFilter',
this.fb.control('', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]));
this.mappingForm.addControl('subscriptionQos', this.fb.control(0));
this.mappingForm.addControl('converter', this.fb.group({
type: [ConvertorType.JSON, []],
json: this.fb.group({
deviceInfo: [{}, []],
attributes: [[], []],
timeseries: [[], []]
}),
bytes: this.fb.group({
deviceInfo: [{}, []],
attributes: [[], []],
timeseries: [[], []]
}),
custom: this.fb.group({
extension: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
extensionConfig: [{}, []]
}),
}));
this.mappingForm.patchValue(this.prepareFormValueData());
this.mappingForm.get('converter.type').valueChanges.pipe(
startWith(this.mappingForm.get('converter.type').value),
takeUntil(this.destroy$)
).subscribe((value) => {
const converterGroup = this.mappingForm.get('converter');
converterGroup.get('json').disable({emitEvent: false});
converterGroup.get('bytes').disable({emitEvent: false});
converterGroup.get('custom').disable({emitEvent: false});
converterGroup.get(value).enable({emitEvent: false});
})
}
if (this.data.mappingType === MappingType.REQUESTS) {
this.mappingForm.addControl('requestType', this.fb.control(RequestType.CONNECT_REQUEST, []));
this.mappingForm.addControl('requestValue', this.fb.group({
connectRequests: this.fb.group({
topicFilter: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
deviceInfo: [{}, []]
}),
disconnectRequests: this.fb.group({
topicFilter: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
deviceInfo: [{}, []]
}),
attributeRequests: this.fb.group({
topicFilter: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
deviceInfo: this.fb.group({
deviceNameExpressionSource: [SourceTypes.MSG, []],
deviceNameExpression: ['', [Validators.required]],
}),
attributeNameExpressionSource: [SourceTypes.MSG, []],
attributeNameExpression: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
topicExpression: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
valueExpression: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
retain: [false, []]
}),
attributeUpdates: this.fb.group({
deviceNameFilter: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
attributeFilter: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
topicExpression: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
valueExpression: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
retain: [true, []]
}),
serverSideRpc: this.fb.group({
type: [ServerSideRPCType.TWO_WAY, []],
deviceNameFilter: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
methodFilter: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
requestTopicExpression: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
responseTopicExpression: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
valueExpression: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
responseTopicQoS: [0, []],
responseTimeout: [10000, [Validators.required, Validators.min(1)]],
})
}));
this.mappingForm.get('requestType').valueChanges.pipe(
startWith(this.mappingForm.get('requestType').value),
takeUntil(this.destroy$)
).subscribe((value) => {
const requestValueGroup = this.mappingForm.get('requestValue');
requestValueGroup.get('connectRequests').disable({emitEvent: false});
requestValueGroup.get('disconnectRequests').disable({emitEvent: false});
requestValueGroup.get('attributeRequests').disable({emitEvent: false});
requestValueGroup.get('attributeUpdates').disable({emitEvent: false});
requestValueGroup.get('serverSideRpc').disable({emitEvent: false});
requestValueGroup.get(value).enable();
});
this.mappingForm.get('requestValue.serverSideRpc.type').valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe((value) => {
const requestValueGroup = this.mappingForm.get('requestValue.serverSideRpc');
if (value === ServerSideRPCType.ONE_WAY) {
requestValueGroup.get('responseTopicExpression').disable({emitEvent: false});
requestValueGroup.get('responseTopicQoS').disable({emitEvent: false});
requestValueGroup.get('responseTimeout').disable({emitEvent: false});
} else {
requestValueGroup.get('responseTopicExpression').enable({emitEvent: false});
requestValueGroup.get('responseTopicQoS').enable({emitEvent: false});
requestValueGroup.get('responseTimeout').enable({emitEvent: false});
}
});
this.mappingForm.patchValue(this.prepareFormValueData());
switch (this.data.mappingType) {
case MappingType.DATA:
this.mappingForm = this.fb.group({});
this.createDataMappingForm();
break;
case MappingType.REQUESTS:
this.mappingForm = this.fb.group({});
this.createRequestMappingForm();
break;
case MappingType.OPCUA:
this.createOPCUAMappingForm();
}
}
@ -278,7 +203,7 @@ export class MappingDialogComponent extends DialogComponent<MappingDialogCompone
}
}
manageKeys($event: Event, matButton: MatButton, keysType: MappingKeysType) {
manageKeys($event: Event, matButton: MatButton, keysType: MappingKeysType): void {
if ($event) {
$event.stopPropagation();
}
@ -286,16 +211,24 @@ export class MappingDialogComponent extends DialogComponent<MappingDialogCompone
if (this.popoverService.hasPopover(trigger)) {
this.popoverService.hidePopover(trigger);
} else {
const keysControl = this.mappingForm.get('converter').get(this.converterType).get(keysType);
const ctx: any = {
const group = this.data.mappingType !== MappingType.OPCUA ? this.mappingForm.get('converter').get(this.converterType)
: this.mappingForm;
const keysControl = group.get(keysType);
const ctx: {[key: string]: any} = {
keys: keysControl.value,
keysType: keysType,
rawData: this.mappingForm.get('converter.type').value === ConvertorType.BYTES,
rawData: this.mappingForm.get('converter.type')?.value === ConvertorType.BYTES,
panelTitle: MappingKeysPanelTitleTranslationsMap.get(keysType),
addKeyTitle: MappingKeysAddKeyTranslationsMap.get(keysType),
deleteKeyTitle: MappingKeysDeleteKeyTranslationsMap.get(keysType),
noKeysText: MappingKeysNoKeysTextTranslationsMap.get(keysType)
};
if (this.data.mappingType === MappingType.OPCUA) {
ctx.valueTypeKeys = Object.values(OPCUaSourceTypes);
ctx.valueTypeEnum = OPCUaSourceTypes;
ctx.valueTypes = SourceTypeTranslationsMap;
}
this.keysPopupClosed = false;
const dataKeysPanelPopover = this.popoverService.displayPopover(trigger, this.renderer,
this.viewContainerRef, MappingDataKeysPanelComponent, 'leftBottom', false, null,
@ -303,58 +236,180 @@ export class MappingDialogComponent extends DialogComponent<MappingDialogCompone
{},
{}, {}, true);
dataKeysPanelPopover.tbComponentRef.instance.popover = dataKeysPanelPopover;
dataKeysPanelPopover.tbComponentRef.instance.keysDataApplied.subscribe((keysData) => {
dataKeysPanelPopover.tbComponentRef.instance.keysDataApplied.pipe(takeUntil(this.destroy$)).subscribe((keysData) => {
dataKeysPanelPopover.hide();
keysControl.patchValue(keysData);
keysControl.markAsDirty();
});
dataKeysPanelPopover.tbHideStart.subscribe(() => {
dataKeysPanelPopover.tbHideStart.pipe(takeUntil(this.destroy$)).subscribe(() => {
this.keysPopupClosed = true;
});
}
}
private prepareMappingData(): {[key: string]: any} {
private prepareMappingData(): {[key: string]: unknown} {
const formValue = this.mappingForm.value;
if (this.data.mappingType === MappingType.DATA) {
const { converter, topicFilter, subscriptionQos } = formValue;
return {
topicFilter,
subscriptionQos,
converter: {
type: converter.type,
...converter[converter.type]
}
};
} else {
return {
requestType: formValue.requestType,
requestValue: formValue.requestValue[formValue.requestType]
};
}
}
private prepareFormValueData(): {[key: string]: any} {
if (this.data.value && Object.keys(this.data.value).length) {
if (this.data.mappingType === MappingType.DATA) {
const { converter, topicFilter, subscriptionQos } = this.data.value;
switch (this.data.mappingType) {
case MappingType.DATA:
const { converter, topicFilter, subscriptionQos } = formValue;
return {
topicFilter,
subscriptionQos,
converter: {
type: converter.type,
[converter.type]: { ...converter }
...converter[converter.type]
}
};
} else {
case MappingType.REQUESTS:
return {
requestType: this.data.value.requestType,
requestValue: {
[this.data.value.requestType]: this.data.value.requestValue
}
requestType: formValue.requestType,
requestValue: formValue.requestValue[formValue.requestType]
};
default:
return formValue;
}
}
private prepareFormValueData(): {[key: string]: unknown} {
if (this.data.value && Object.keys(this.data.value).length) {
switch (this.data.mappingType) {
case MappingType.DATA:
const { converter, topicFilter, subscriptionQos } = this.data.value;
return {
topicFilter,
subscriptionQos,
converter: {
type: converter.type,
[converter.type]: { ...converter }
}
};
case MappingType.REQUESTS:
return {
requestType: this.data.value.requestType,
requestValue: {
[this.data.value.requestType]: this.data.value.requestValue
}
};
default:
return this.data.value;
}
}
return this.data.value;
}
private createDataMappingForm(): void {
this.mappingForm.addControl('topicFilter',
this.fb.control('', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]));
this.mappingForm.addControl('subscriptionQos', this.fb.control(0));
this.mappingForm.addControl('converter', this.fb.group({
type: [ConvertorType.JSON, []],
json: this.fb.group({
deviceInfo: [{}, []],
attributes: [[], []],
timeseries: [[], []]
}),
bytes: this.fb.group({
deviceInfo: [{}, []],
attributes: [[], []],
timeseries: [[], []]
}),
custom: this.fb.group({
extension: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
extensionConfig: [{}, []]
}),
}));
this.mappingForm.patchValue(this.prepareFormValueData());
this.mappingForm.get('converter.type').valueChanges.pipe(
startWith(this.mappingForm.get('converter.type').value),
takeUntil(this.destroy$)
).subscribe((value) => {
const converterGroup = this.mappingForm.get('converter');
converterGroup.get('json').disable({emitEvent: false});
converterGroup.get('bytes').disable({emitEvent: false});
converterGroup.get('custom').disable({emitEvent: false});
converterGroup.get(value).enable({emitEvent: false});
})
}
private createRequestMappingForm(): void {
this.mappingForm.addControl('requestType', this.fb.control(RequestType.CONNECT_REQUEST, []));
this.mappingForm.addControl('requestValue', this.fb.group({
connectRequests: this.fb.group({
topicFilter: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
deviceInfo: [{}, []]
}),
disconnectRequests: this.fb.group({
topicFilter: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
deviceInfo: [{}, []]
}),
attributeRequests: this.fb.group({
topicFilter: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
deviceInfo: this.fb.group({
deviceNameExpressionSource: [SourceTypes.MSG, []],
deviceNameExpression: ['', [Validators.required]],
}),
attributeNameExpressionSource: [SourceTypes.MSG, []],
attributeNameExpression: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
topicExpression: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
valueExpression: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
retain: [false, []]
}),
attributeUpdates: this.fb.group({
deviceNameFilter: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
attributeFilter: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
topicExpression: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
valueExpression: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
retain: [true, []]
}),
serverSideRpc: this.fb.group({
type: [ServerSideRPCType.TWO_WAY, []],
deviceNameFilter: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
methodFilter: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
requestTopicExpression: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
responseTopicExpression: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
valueExpression: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
responseTopicQoS: [0, []],
responseTimeout: [10000, [Validators.required, Validators.min(1)]],
})
}));
this.mappingForm.get('requestType').valueChanges.pipe(
startWith(this.mappingForm.get('requestType').value),
takeUntil(this.destroy$)
).subscribe((value) => {
const requestValueGroup = this.mappingForm.get('requestValue');
requestValueGroup.get('connectRequests').disable({emitEvent: false});
requestValueGroup.get('disconnectRequests').disable({emitEvent: false});
requestValueGroup.get('attributeRequests').disable({emitEvent: false});
requestValueGroup.get('attributeUpdates').disable({emitEvent: false});
requestValueGroup.get('serverSideRpc').disable({emitEvent: false});
requestValueGroup.get(value).enable();
});
this.mappingForm.get('requestValue.serverSideRpc.type').valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe((value) => {
const requestValueGroup = this.mappingForm.get('requestValue.serverSideRpc');
if (value === ServerSideRPCType.ONE_WAY) {
requestValueGroup.get('responseTopicExpression').disable({emitEvent: false});
requestValueGroup.get('responseTopicQoS').disable({emitEvent: false});
requestValueGroup.get('responseTimeout').disable({emitEvent: false});
} else {
requestValueGroup.get('responseTopicExpression').enable({emitEvent: false});
requestValueGroup.get('responseTopicQoS').enable({emitEvent: false});
requestValueGroup.get('responseTimeout').enable({emitEvent: false});
}
});
this.mappingForm.patchValue(this.prepareFormValueData());
}
private createOPCUAMappingForm(): void {
this.mappingForm = this.fb.group({
deviceNodeSource: [OPCUaSourceTypes.PATH, []],
deviceNodePattern: ['', [Validators.required]],
deviceInfo: [{}, []],
attributes: [[], []],
timeseries: [[], []],
rpc_methods: [[], []],
attributes_updates: [[], []]
});
this.mappingForm.patchValue(this.prepareFormValueData());
}
}

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

@ -156,7 +156,10 @@
{{ initialConnector?.type ? gatewayConnectorDefaultTypes.get(initialConnector.type) : '' }}
{{ 'gateway.configuration' | translate }}
</div>
<tb-toggle-select *ngIf="initialConnector && initialConnector.type === connectorType.MQTT" formControlName="mode" appearance="fill">
<tb-toggle-select *ngIf="initialConnector &&
(initialConnector.type === connectorType.MQTT ||
initialConnector.type === connectorType.OPCUA)"
formControlName="mode" appearance="fill">
<tb-toggle-option [value]="connectorConfigurationModes.BASIC">
{{ 'gateway.basic' | translate }}
</tb-toggle-option>
@ -219,144 +222,41 @@
</div>
</div>
</mat-tab>
<ng-container *ngIf="connectorForm.get('type').value === connectorType.MQTT &&
connectorForm.get('mode').value === connectorConfigurationModes.BASIC; else defaultConfig"
formGroupName="basicConfig">
<mat-tab label="{{ 'gateway.broker.connection' | translate }}*">
<ng-container formGroupName="broker">
<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 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="connectorForm.get('basicConfig.broker.host').hasError('required')
&& connectorForm.get('basicConfig.broker.host').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
<ng-container *ngIf="connectorForm.get('mode')?.value === connectorConfigurationModes.BASIC &&
(initialConnector.type === connectorType.MQTT ||
initialConnector.type === connectorType.OPCUA); else defaultConfig">
<ng-container formGroupName="basicConfig" [ngSwitch]="connectorForm.get('type').value">
<ng-template [ngSwitchCase]="connectorType.MQTT">
<mat-tab label="{{ 'gateway.broker.connection' | translate }}*">
<tb-broker-config-control></tb-broker-config-control>
</mat-tab>
<mat-tab label="{{ 'gateway.data-mapping' | translate }}*">
<div class="tb-form-panel no-border no-padding padding-top tb-flex fill-height">
<tb-mapping-table [controlKey]="'dataMapping'" [required]="true" [mappingType]="mappingTypes.DATA"></tb-mapping-table>
</div>
<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 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]="portErrorTooltip"
*ngIf="(connectorForm.get('basicConfig.broker.port').hasError('required') ||
connectorForm.get('basicConfig.broker.port').hasError('min') ||
connectorForm.get('basicConfig.broker.port').hasError('max')) &&
connectorForm.get('basicConfig.broker.port').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
</mat-tab>
<mat-tab label="{{ 'gateway.requests-mapping' | translate }}">
<div class="tb-form-panel no-border no-padding padding-top tb-flex fill-height">
<tb-mapping-table [controlKey]="'requestsMapping'" [mappingType]="mappingTypes.REQUESTS"></tb-mapping-table>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" translate>gateway.mqtt-version</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="version">
<mat-option *ngFor="let version of mqttVersions" [value]="version.value">{{ version.name }}</mat-option>
</mat-select>
</mat-form-field>
</div>
</mat-tab>
<mat-tab label="{{ 'gateway.workers-settings' | translate }}">
<div class="tb-form-panel no-border no-padding">
<tb-workers-config-control></tb-workers-config-control>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" translate>gateway.client-id</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="clientId" placeholder="{{ 'gateway.set' | translate }}"/>
<button type="button"
matSuffix
mat-icon-button
aria-label="Generate"
matTooltip="{{ 'gateway.generate-client-id' | translate }}"
matTooltipPosition="above"
(click)="generate('basicConfig.broker.clientId')"
*ngIf="!connectorForm.get('basicConfig.broker.clientId').value">
<mat-icon>autorenew</mat-icon>
</button>
</mat-form-field>
</mat-tab>
</ng-template>
<ng-template [ngSwitchCase]="connectorType.OPCUA">
<mat-tab label="{{ 'gateway.server' | translate }}*">
<tb-server-config></tb-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">
<tb-mapping-table [controlKey]="'mapping'" [required]="true" [mappingType]="mappingTypes.OPCUA"></tb-mapping-table>
</div>
</div>
<tb-broker-security [formControl]="connectorForm.get('basicConfig.broker.security')">
</tb-broker-security>
</div>
</ng-container>
</mat-tab>
<mat-tab label="{{ 'gateway.data-mapping' | translate }}*">
<div class="tb-form-panel no-border no-padding padding-top tb-flex fill-height">
<tb-mapping-table formControlName="dataMapping" [mappingType]="mappingTypes.DATA"></tb-mapping-table>
</div>
</mat-tab>
<mat-tab label="{{ 'gateway.requests-mapping' | translate }}">
<div class="tb-form-panel no-border no-padding padding-top tb-flex fill-height">
<tb-mapping-table formControlName="requestsMapping" [mappingType]="mappingTypes.REQUESTS"></tb-mapping-table>
</div>
</mat-tab>
<mat-tab label="{{ 'gateway.workers-settings' | translate }}">
<div class="tb-form-panel no-border no-padding">
<ng-container formGroupName="broker">
<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 tb-required" [style.width.%]="50"
tb-hint-tooltip-icon="{{ 'gateway.max-number-of-workers-hint' | translate }}" translate>
gateway.max-number-of-workers
</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" type="number" min="1" formControlName="maxNumberOfWorkers"
placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.max-number-of-workers-required') | translate"
*ngIf="connectorForm.get('basicConfig.broker.maxNumberOfWorkers').hasError('min') ||
(connectorForm.get('basicConfig.broker.maxNumberOfWorkers').hasError('required') &&
connectorForm.get('basicConfig.broker.maxNumberOfWorkers').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" [style.width.%]="50"
tb-hint-tooltip-icon="{{ 'gateway.max-messages-queue-for-worker-hint' | translate }}" translate>
gateway.max-messages-queue-for-worker
</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" type="number" min="1" formControlName="maxMessageNumberPerWorker"
placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.max-messages-queue-for-worker-required') | translate"
*ngIf="connectorForm.get('basicConfig.broker.maxMessageNumberPerWorker').hasError('min') ||
(connectorForm.get('basicConfig.broker.maxMessageNumberPerWorker').hasError('required') &&
connectorForm.get('basicConfig.broker.maxMessageNumberPerWorker').touched)"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
</div>
</div>
</ng-container>
</div>
</mat-tab>
</mat-tab>
</ng-template>
</ng-container>
</ng-container>
<ng-template #defaultConfig>
<mat-tab label="{{ 'gateway.configuration' | translate }}*">

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

@ -14,15 +14,27 @@
/// limitations under the License.
///
import { AfterViewInit, ChangeDetectorRef, Component, ElementRef, Input, NgZone, ViewChild } from '@angular/core';
import {
AfterViewInit,
ChangeDetectorRef,
Component,
ElementRef,
Input,
NgZone,
QueryList,
ViewChild,
ViewChildren
} from '@angular/core';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import {
FormArray,
FormBuilder,
FormControl,
FormGroup,
FormGroupDirective,
NgForm,
UntypedFormArray,
UntypedFormControl,
ValidatorFn,
Validators
@ -30,7 +42,7 @@ import {
import { EntityId } from '@shared/models/id/entity-id';
import { AttributeService } from '@core/http/attribute.service';
import { TranslateService } from '@ngx-translate/core';
import { forkJoin, Observable, of, Subject, Subscription } from 'rxjs';
import { BehaviorSubject, forkJoin, Observable, of, Subject, Subscription } from 'rxjs';
import { AttributeData, AttributeScope } from '@shared/models/telemetry/telemetry.models';
import { PageComponent } from '@shared/components/page.component';
import { PageLink } from '@shared/models/page/page-link';
@ -42,7 +54,7 @@ import { MatTableDataSource } from '@angular/material/table';
import { ActionNotificationShow } from '@core/notification/notification.actions';
import { DialogService } from '@core/services/dialog.service';
import { WidgetContext } from '@home/models/widget-component.models';
import { camelCase, deepClone, generateSecret, isEqual, isString } from '@core/utils';
import { camelCase, deepClone, generateSecret, isEqual, isObject, isString } from '@core/utils';
import { NULL_UUID } from '@shared/models/id/has-uuid';
import { IWidgetSubscription, WidgetSubscriptionOptions } from '@core/api/widget-api.models';
import { DatasourceType, widgetType } from '@shared/models/widget.models';
@ -51,20 +63,22 @@ import { EntityType } from '@shared/models/entity-type.models';
import {
AddConnectorConfigData,
ConnectorConfigurationModes,
ConnectorMapping,
ConnectorType,
GatewayConnector,
GatewayConnectorDefaultTypesTranslatesMap,
GatewayLogLevel,
MappingType,
MqttVersions,
noLeadTrailSpacesRegex,
PortLimits
RequestMappingData,
RequestType,
} from './gateway-widget.models';
import { MatDialog } from '@angular/material/dialog';
import { AddConnectorDialogComponent } from '@home/components/widget/lib/gateway/dialog/add-connector-dialog.component';
import { takeUntil } from 'rxjs/operators';
import { distinctUntilChanged, filter, take, takeUntil, tap } from 'rxjs/operators';
import { ErrorStateMatcher } from '@angular/material/core';
import { PageData } from '@shared/models/page/page-data';
import { MatTab } from '@angular/material/tabs';
export class ForceErrorStateMatcher implements ErrorStateMatcher {
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
@ -88,6 +102,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
@ViewChild('nameInput') nameInput: ElementRef;
@ViewChild(MatSort, {static: false}) sort: MatSort;
@ViewChildren(MatTab) tabs: QueryList<MatTab>;
pageLink: PageLink;
@ -97,8 +112,6 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
displayedColumns = ['enabled', 'key', 'type', 'syncStatus', 'errors', 'actions'];
mqttVersions = MqttVersions;
gatewayConnectorDefaultTypes = GatewayConnectorDefaultTypesTranslatesMap;
connectorConfigurationModes = ConnectorConfigurationModes;
@ -113,12 +126,12 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
mappingTypes = MappingType;
portLimits = PortLimits;
mode: ConnectorConfigurationModes = this.connectorConfigurationModes.BASIC;
initialConnector: GatewayConnector;
private tabsCountSubject = new BehaviorSubject<number>(0);
private inactiveConnectors: Array<string>;
private attributeDataSource: AttributeDatasource;
@ -182,20 +195,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
this.connectorForm.disable();
}
get portErrorTooltip(): string {
if (this.connectorForm.get('basicConfig.broker.port').hasError('required')) {
return this.translate.instant('gateway.port-required');
} else if (
this.connectorForm.get('basicConfig.broker.port').hasError('min') ||
this.connectorForm.get('basicConfig.broker.port').hasError('max')
) {
return this.translate.instant('gateway.port-limits-error',
{min: PortLimits.MIN, max: PortLimits.MAX});
}
return '';
}
ngAfterViewInit() {
ngAfterViewInit(): void {
this.connectorForm.get('type').valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe(type => {
@ -229,7 +229,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
const mode = this.connectorForm.get('mode').value;
if (
!isEqual(config, basicConfig?.value) &&
type === ConnectorType.MQTT &&
(type === ConnectorType.MQTT || type === ConnectorType.OPCUA) &&
mode === ConnectorConfigurationModes.ADVANCED
) {
this.connectorForm.get('basicConfig').patchValue(config, {emitEvent: false});
@ -269,37 +269,18 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
}
});
}
this.observeModeChange();
this.observeTabsChanges();
}
private uniqNameRequired(): ValidatorFn {
return (c: UntypedFormControl) => {
const newName = c.value.trim().toLowerCase();
const found = this.dataSource.data.find((connectorAttr) => {
const connectorData = connectorAttr.value;
return connectorData.name.toLowerCase() === newName;
});
if (found) {
if (this.initialConnector && this.initialConnector.name.toLowerCase() === newName) {
return null;
}
return {
duplicateName: {
valid: false
}
};
}
return null;
};
}
ngOnDestroy() {
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
super.ngOnDestroy();
}
saveConnector(): void {
const value = this.connectorForm.value;
const value = this.connectorForm.get('type').value === ConnectorType.MQTT ? this.getMappedMQTTValue() : this.connectorForm.value;
value.configuration = camelCase(value.name) + '.json';
delete value.basicConfig;
if (value.type !== ConnectorType.GRPC) {
@ -360,6 +341,20 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
});
}
private getMappedMQTTValue(): GatewayConnector {
const value = this.connectorForm.value;
return {
...value,
configurationJson: {
...value.configurationJson,
broker: {
...value.basicConfig.broker,
...value.basicConfig.workers,
}
}
}
}
private updateData(reload: boolean = false): void {
this.pageLink.sortOrder.property = this.sort.active;
this.pageLink.sortOrder.direction = Direction[this.sort.direction.toUpperCase()];
@ -581,12 +576,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
value.configurationJson = {};
}
value.basicConfig = value.configurationJson;
if (value.type === ConnectorType.MQTT) {
this.addMQTTConfigControls();
} else {
this.connectorForm.setControl('basicConfig', this.fb.group({}), {emitEvent: false});
}
this.connectorForm.patchValue(value, {emitEvent: false});
this.updateConnector(value);
this.generate('basicConfig.broker.clientId');
this.saveConnector();
}
@ -634,29 +624,6 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
}
}
private addMQTTConfigControls(): void {
const configControl = this.fb.group({});
const brokerGroup = 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)]],
maxNumberOfWorkers: [100, [Validators.required, Validators.min(1)]],
maxMessageNumberPerWorker: [10, [Validators.required, Validators.min(1)]],
security: [{}, [Validators.required]]
});
configControl.addControl('broker', brokerGroup);
configControl.addControl('dataMapping', this.fb.control([], Validators.required));
configControl.addControl('requestsMapping', this.fb.control({}));
if (this.connectorForm.get('basicConfig')) {
this.connectorForm.setControl('basicConfig', configControl, {emitEvent: false});
} else {
this.connectorForm.addControl('basicConfig', configControl, {emitEvent: false});
}
this.createBasicConfigWatcher();
}
private createBasicConfigWatcher(): void {
if (this.basicConfigSub) {
this.basicConfigSub.unsubscribe();
@ -669,7 +636,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
const mode = this.connectorForm.get('mode').value;
if (
!isEqual(config, configJson?.value) &&
type === ConnectorType.MQTT &&
(type === ConnectorType.MQTT || type === ConnectorType.OPCUA) &&
mode === ConnectorConfigurationModes.BASIC
) {
const newConfig = { ...configJson.value, ...config };
@ -691,6 +658,47 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
return of(true);
}
private observeTabsChanges(): void {
this.tabs.changes
.pipe(
tap(() => this.tabsCountSubject.next(this.tabs.length)),
takeUntil(this.destroy$),
)
.subscribe();
}
private observeModeChange(): void {
this.connectorForm.get('mode').valueChanges
.pipe(
distinctUntilChanged(),
filter(Boolean),
tap(mode => this.updateConnector({...this.initialConnector, basicConfig: this.initialConnector?.configurationJson || {}, mode })),
takeUntil(this.destroy$),
)
.subscribe();
}
private uniqNameRequired(): ValidatorFn {
return (c: UntypedFormControl) => {
const newName = c.value.trim().toLowerCase();
const found = this.dataSource.data.find((connectorAttr) => {
const connectorData = connectorAttr.value;
return connectorData.name.toLowerCase() === newName;
});
if (found) {
if (this.initialConnector && this.initialConnector.name.toLowerCase() === newName) {
return null;
}
return {
duplicateName: {
valid: false
}
};
}
return null;
};
}
private setFormValue(connector: GatewayConnector): void {
if (this.connectorForm.disabled) {
this.connectorForm.enable();
@ -708,14 +716,115 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
this.initialConnector = connector;
if (connector.type === ConnectorType.MQTT) {
this.addMQTTConfigControls();
this.updateConnector(connector);
}
private updateConnector(connector: GatewayConnector): void {
switch (connector.type) {
case ConnectorType.MQTT:
this.patchToMQTT(connector);
this.createBasicConfigWatcher();
break;
case ConnectorType.OPCUA:
this.patchToOPCUA(connector);
this.createBasicConfigWatcher();
break;
default:
this.connectorForm.patchValue({...connector, mode: null});
this.connectorForm.markAsPristine();
}
}
private isTabsInitialized(tabsCount: number, isAdvanced?: boolean): boolean {
return isAdvanced ? tabsCount === 2 : tabsCount > 2;
}
private patchToOPCUA(connector: GatewayConnector): void {
const connectorBase = {...connector, basicConfig: {}};
if (!connector.mode || connector.mode === ConnectorConfigurationModes.BASIC) {
if (!connector.mode) {
this.connectorForm.get('mode').patchValue(ConnectorConfigurationModes.BASIC, {emitEvent: false});
}
this.connectorForm.patchValue(connectorBase, {emitEvent: false});
this.tabsCountSubject.pipe(filter(count => this.isTabsInitialized(count)), take(1)).subscribe(() => {
(this.connectorForm.get('basicConfig.mapping') as FormArray)?.clear();
this.connectorForm.patchValue(connector);
this.pushDataAsFormArrays('basicConfig.mapping', connector.basicConfig.mapping);
this.connectorForm.markAsPristine();
})
} else {
this.connectorForm.setControl('basicConfig', this.fb.group({}), {emitEvent: false});
this.updateInAdvanced(connector);
}
}
this.connectorForm.patchValue(connector, {emitEvent: false});
this.connectorForm.markAsPristine();
private updateInAdvanced(connector: GatewayConnector): void {
this.connectorForm.patchValue({...connector, basicConfig: {}}, {emitEvent: false});
this.tabsCountSubject.pipe(filter(count => this.isTabsInitialized(count, true)), take(1)).subscribe(() => {
this.connectorForm.patchValue({...connector, basicConfig: {}});
this.connectorForm.markAsPristine();
})
}
private pushDataAsFormArrays(controlKey: string, data: ConnectorMapping[]): void {
const control: UntypedFormArray = this.connectorForm.get(controlKey) as FormArray;
if (control && data?.length) {
data.forEach((mapping: ConnectorMapping) => control.push(this.fb.control(mapping)));
}
}
private patchToMQTT(connector: GatewayConnector): void {
if (!connector.mode || connector.mode === ConnectorConfigurationModes.BASIC) {
if (!connector.mode) {
this.connectorForm.get('mode').patchValue(ConnectorConfigurationModes.BASIC, {emitEvent: false});
}
this.connectorForm.patchValue({...connector, basicConfig: {}}, {emitEvent: false});
this.tabsCountSubject.pipe(filter(count => this.isTabsInitialized(count)), take(1)).subscribe(() => {
const editedConnector = {
...connector,
basicConfig: {
...connector.basicConfig,
workers: {
maxNumberOfWorkers: connector.basicConfig?.broker?.maxNumberOfWorkers,
maxMessageNumberPerWorker: connector.basicConfig?.broker?.maxMessageNumberPerWorker,
},
requestsMapping: [],
}
};
(this.connectorForm.get('basicConfig.dataMapping') as FormArray)?.clear();
(this.connectorForm.get('basicConfig.requestsMapping') as FormArray)?.clear();
this.connectorForm.patchValue(editedConnector);
this.pushDataAsFormArrays('basicConfig.dataMapping', editedConnector.basicConfig.dataMapping);
this.pushDataAsFormArrays('basicConfig.requestsMapping',
Array.isArray(connector.basicConfig.requestsMapping)
? connector.basicConfig.requestsMapping
: this.getRequestDataArray(connector.basicConfig.requestsMapping)
);
this.connectorForm.markAsPristine();
})
} else {
this.updateInAdvanced(connector);
}
}
private getRequestDataArray(value: Record<RequestType, RequestMappingData>): RequestMappingData[] {
const mappingConfigs = [];
if (isObject(value)) {
Object.keys(value).forEach((configKey: string) => {
for (let mapping of value[configKey]) {
mappingConfigs.push({
requestType: configKey,
requestValue: mapping
});
}
});
}
return mappingConfigs;
}
private setClientData(data: PageData<AttributeData>): void {

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

@ -17,7 +17,6 @@
import { ResourcesService } from '@core/services/resources.service';
import { Observable } from 'rxjs';
import { ValueTypeData } from '@shared/models/constants';
import { Validators } from '@angular/forms';
export const noLeadTrailSpacesRegex: RegExp = /^(?! )[\S\s]*(?<! )$/;
@ -113,11 +112,134 @@ export interface GatewayConnector {
name: string;
type: ConnectorType;
configuration?: string;
configurationJson: string | {[key: string]: any};
basicConfig?: string | {[key: string]: any};
configurationJson: ConnectorBaseConfig;
basicConfig?: ConnectorBaseConfig;
logLevel: string;
key?: string;
class?: string;
mode?: ConnectorConfigurationModes;
}
export interface DataMapping {
topicFilter: string;
QoS: string;
converter: Converter;
}
export interface RequestsMapping {
requestType: RequestType;
type: string;
details: string;
}
export interface OpcUaMapping {
deviceNodePattern?: string;
deviceNamePattern?: string;
deviceProfileExpression?: string;
}
export type MappingValue = DataMapping | RequestsMapping | OpcUaMapping;
interface ServerConfig {
name: string;
url: string;
timeoutInMillis: number;
scanPeriodInMillis: number;
enableSubscriptions: boolean;
subCheckPeriodInMillis: number;
showMap: boolean;
security: string;
identity: {
type: SecurityType;
};
}
interface BrokerConfig {
name: string;
host: string;
port: number;
version: number;
clientId: string;
maxNumberOfWorkers: number;
maxMessageNumberPerWorker: number;
security: {
type: SecurityType;
};
}
export type ConnectorMapping = DeviceConnectorMapping | RequestMappingData | ConverterConnectorMapping;
export interface ConnectorBaseConfig {
mapping?: DeviceConnectorMapping[];
dataMapping?: ConverterConnectorMapping[];
requestsMapping?: Record<RequestType, RequestMappingData> | RequestMappingData[];
server?: ServerConfig;
broker?: BrokerConfig;
workers?: {
maxNumberOfWorkers: number;
maxMessageNumberPerWorker: number;
};
}
interface DeviceInfo {
deviceNameExpression: string;
deviceNameExpressionSource: string;
deviceProfileExpression: string;
deviceProfileExpressionSource: string;
}
interface Attribute {
key: string;
type: string;
value: string;
}
interface Timeseries {
key: string;
type: string;
value: string;
}
interface RpcArgument {
type: string;
value: number;
}
interface RpcMethod {
method: string;
arguments: RpcArgument[];
}
interface AttributesUpdate {
key: string;
type: string;
value: string;
}
interface Converter {
type: ConvertorType;
deviceNameJsonExpression: string;
deviceTypeJsonExpression: string;
sendDataOnlyOnChange: boolean;
timeout: number;
attributes: Attribute[];
timeseries: Timeseries[];
}
export interface ConverterConnectorMapping {
topicFilter: string;
subscriptionQos?: string;
converter: Converter;
}
export interface DeviceConnectorMapping {
deviceNodePattern: string;
deviceNodeSource: string;
deviceInfo: DeviceInfo;
attributes: Attribute[];
timeseries: Timeseries[];
rpc_methods: RpcMethod[];
attributes_updates: AttributesUpdate[];
}
export enum ConnectorType {
@ -335,6 +457,12 @@ export interface MappingDataKey {
value: any,
type: MappingValueType
}
export interface RpcMethodsMapping {
method: string,
arguments: Array<MappingDataKey>
}
export interface MappingInfo {
mappingType: MappingType,
value: {[key: string]: any},
@ -352,6 +480,12 @@ export enum BrokerSecurityType {
CERTIFICATES = 'certificates'
}
export enum ModeType {
NONE = 'None',
SIGN = 'Sign',
SIGNANDENCRYPT = 'SignAndEncrypt'
}
export const BrokerSecurityTypeTranslationsMap = new Map<BrokerSecurityType, string>(
[
[BrokerSecurityType.ANONYMOUS, 'gateway.broker.security-types.anonymous'],
@ -368,19 +502,22 @@ export const MqttVersions = [
export enum MappingType {
DATA = 'data',
REQUESTS = 'requests'
REQUESTS = 'requests',
OPCUA = 'OPCua'
}
export const MappingTypeTranslationsMap = new Map<MappingType, string>(
[
[MappingType.DATA, 'gateway.data-mapping'],
[MappingType.REQUESTS, 'gateway.requests-mapping']
[MappingType.REQUESTS, 'gateway.requests-mapping'],
[MappingType.OPCUA, 'gateway.data-mapping']
]
);
export const MappingHintTranslationsMap = new Map<MappingType, string>(
[
[MappingType.DATA, 'gateway.data-mapping-hint'],
[MappingType.OPCUA, 'gateway.opcua-data-mapping-hint'],
[MappingType.REQUESTS, 'gateway.requests-mapping-hint']
]
);
@ -415,19 +552,42 @@ export enum SourceTypes {
CONST = 'constant'
}
export enum OPCUaSourceTypes {
PATH = 'path',
IDENTIFIER = 'identifier',
CONST = 'constant'
}
export enum DeviceInfoType {
FULL = 'full',
PARTIAL = 'partial'
}
export const SourceTypeTranslationsMap = new Map<SourceTypes, string>(
export const SourceTypeTranslationsMap = new Map<SourceTypes | OPCUaSourceTypes, string>(
[
[SourceTypes.MSG, 'gateway.source-type.msg'],
[SourceTypes.TOPIC, 'gateway.source-type.topic'],
[SourceTypes.CONST, 'gateway.source-type.const'],
[OPCUaSourceTypes.PATH, 'gateway.source-type.path'],
[OPCUaSourceTypes.IDENTIFIER, 'gateway.source-type.identifier'],
[OPCUaSourceTypes.CONST, 'gateway.source-type.const']
]
);
export interface RequestMappingData {
requestType: RequestType;
requestValue: RequestDataItem;
}
export interface RequestDataItem {
type: string;
details: string;
requestType: RequestType;
methodFilter?: string;
attributeFilter?: string;
topicFilter?: string;
}
export enum RequestType {
CONNECT_REQUEST = 'connectRequests',
DISCONNECT_REQUEST = 'disconnectRequests',
@ -449,14 +609,18 @@ export const RequestTypesTranslationsMap = new Map<RequestType, string>(
export enum MappingKeysType {
ATTRIBUTES = 'attributes',
TIMESERIES = 'timeseries',
CUSTOM = 'extensionConfig'
CUSTOM = 'extensionConfig',
RPC_METHODS = 'rpc_methods',
ATTRIBUTES_UPDATES = 'attributes_updates'
}
export const MappingKeysPanelTitleTranslationsMap = new Map<MappingKeysType, string>(
[
[MappingKeysType.ATTRIBUTES, 'gateway.attributes'],
[MappingKeysType.TIMESERIES, 'gateway.timeseries'],
[MappingKeysType.CUSTOM, 'gateway.keys']
[MappingKeysType.CUSTOM, 'gateway.keys'],
[MappingKeysType.ATTRIBUTES_UPDATES, 'gateway.attribute-updates'],
[MappingKeysType.RPC_METHODS, 'gateway.rpc-methods']
]
);
@ -464,7 +628,9 @@ export const MappingKeysAddKeyTranslationsMap = new Map<MappingKeysType, string>
[
[MappingKeysType.ATTRIBUTES, 'gateway.add-attribute'],
[MappingKeysType.TIMESERIES, 'gateway.add-timeseries'],
[MappingKeysType.CUSTOM, 'gateway.add-key']
[MappingKeysType.CUSTOM, 'gateway.add-key'],
[MappingKeysType.ATTRIBUTES_UPDATES, 'gateway.add-attribute-update'],
[MappingKeysType.RPC_METHODS, 'gateway.add-rpc-method']
]
);
@ -472,7 +638,9 @@ export const MappingKeysDeleteKeyTranslationsMap = new Map<MappingKeysType, stri
[
[MappingKeysType.ATTRIBUTES, 'gateway.delete-attribute'],
[MappingKeysType.TIMESERIES, 'gateway.delete-timeseries'],
[MappingKeysType.CUSTOM, 'gateway.delete-key']
[MappingKeysType.CUSTOM, 'gateway.delete-key'],
[MappingKeysType.ATTRIBUTES_UPDATES, 'gateway.delete-attribute-update'],
[MappingKeysType.RPC_METHODS, 'gateway.delete-rpc-method']
]
);
@ -480,7 +648,9 @@ export const MappingKeysNoKeysTextTranslationsMap = new Map<MappingKeysType, str
[
[MappingKeysType.ATTRIBUTES, 'gateway.no-attributes'],
[MappingKeysType.TIMESERIES, 'gateway.no-timeseries'],
[MappingKeysType.CUSTOM, 'gateway.no-keys']
[MappingKeysType.CUSTOM, 'gateway.no-keys'],
[MappingKeysType.ATTRIBUTES_UPDATES, 'gateway.no-attribute-updates'],
[MappingKeysType.RPC_METHODS, 'gateway.no-rpc-methods']
]
);
@ -489,7 +659,6 @@ export enum ServerSideRPCType {
TWO_WAY = 'twoWay'
}
export const getDefaultConfig = (resourcesService: ResourcesService, type: string): Observable<any> =>
resourcesService.loadJsonResource(`/assets/metadata/connector-default-configs/${type}.json`);
@ -540,3 +709,15 @@ export const DataConversionTranslationsMap = new Map<ConvertorType, string>(
[ConvertorType.CUSTOM, 'gateway.custom-hint']
]
);
export enum SecurityType {
BASIC128 = 'Basic128Rsa15',
BASIC256 = 'Basic256',
BASIC256SHA = 'Basic256Sha256'
}
export const ServerSecurityTypes = [
{ value: 'Basic128Rsa15', name: 'Basic128RSA15' },
{ value: 'Basic256', name: 'Basic256' },
{ value: 'Basic256Sha256', name: 'Basic256SHA256' }
];

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

@ -87,11 +87,7 @@ import { SliderWidgetComponent } from '@home/components/widget/lib/rpc/slider-wi
import { ToggleButtonWidgetComponent } from '@home/components/widget/lib/button/toggle-button-widget.component';
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 { MappingTableComponent } from '@home/components/widget/lib/gateway/connectors-configuration/mapping-table.component';
import { MappingDialogComponent } from '@home/components/widget/lib/gateway/dialog/mapping-dialog.component';
import { DeviceInfoTableComponent } from '@home/components/widget/lib/gateway/connectors-configuration/device-info-table.component';
import { MappingDataKeysPanelComponent } from '@home/components/widget/lib/gateway/connectors-configuration/mapping-data-keys-panel.component';
import { BrokerSecurityComponent } from '@home/components/widget/lib/gateway/connectors-configuration/broker-security.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';
@ -103,6 +99,17 @@ import { MobileAppQrcodeWidgetComponent } from '@home/components/widget/lib/mobi
import { LabelCardWidgetComponent } from '@home/components/widget/lib/cards/label-card-widget.component';
import { LabelValueCardWidgetComponent } from '@home/components/widget/lib/cards/label-value-card-widget.component';
import { GatewayHelpLinkPipe } from '@home/pipes';
import {
DeviceInfoTableComponent,
MappingDataKeysPanelComponent,
MappingTableComponent,
ServerConfigComponent,
TypeValuePanelComponent,
BrokerConfigControlComponent,
WorkersConfigControlComponent,
} from '@home/components/widget/lib/gateway/connectors-configuration';
@NgModule({
declarations:
[
@ -133,7 +140,7 @@ import { LabelValueCardWidgetComponent } from '@home/components/widget/lib/cards
MappingDialogComponent,
DeviceInfoTableComponent,
MappingDataKeysPanelComponent,
BrokerSecurityComponent,
TypeValuePanelComponent,
GatewayLogsComponent,
GatewayStatisticsComponent,
GatewayServiceRPCComponent,
@ -177,7 +184,11 @@ import { LabelValueCardWidgetComponent } from '@home/components/widget/lib/cards
SharedModule,
RpcWidgetsModule,
HomePageWidgetsModule,
SharedHomeComponentsModule
SharedHomeComponentsModule,
GatewayHelpLinkPipe,
BrokerConfigControlComponent,
WorkersConfigControlComponent,
ServerConfigComponent,
],
exports: [
EntitiesTableWidgetComponent,
@ -206,7 +217,7 @@ import { LabelValueCardWidgetComponent } from '@home/components/widget/lib/cards
MappingDialogComponent,
DeviceInfoTableComponent,
MappingDataKeysPanelComponent,
BrokerSecurityComponent,
TypeValuePanelComponent,
GatewayLogsComponent,
GatewayServiceRPCConnectorComponent,
GatewayServiceRPCConnectorTemplatesComponent,

39
ui-ngx/src/app/modules/home/pipes/gateway-help-link/gateway-help-link.pipe.ts

@ -0,0 +1,39 @@
///
/// 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 {
MappingValueType,
OPCUaSourceTypes,
SourceTypes
} from '@home/components/widget/lib/gateway/gateway-widget.models';
@Pipe({
name: 'getGatewayHelpLink',
standalone: true,
})
export class GatewayHelpLinkPipe implements PipeTransform {
transform(field: string, sourceType: SourceTypes | OPCUaSourceTypes, sourceTypes?: Array<SourceTypes | OPCUaSourceTypes | MappingValueType> ): string {
if (!sourceTypes || sourceTypes?.includes(OPCUaSourceTypes.PATH)) {
if (sourceType !== OPCUaSourceTypes.CONST) {
return `widget/lib/gateway/${field}-${sourceType}_fn`;
} else {
return;
}
}
return 'widget/lib/gateway/expressions_fn';
}
}

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

@ -0,0 +1,17 @@
///
/// 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';

24
ui-ngx/src/app/shared/validators/form-array.validators.ts

@ -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.
///
import { UntypedFormArray, ValidatorFn } from '@angular/forms';
export function validateArrayIsNotEmpty(): ValidatorFn {
return (formArray: UntypedFormArray): { formArrayIsEmpty: boolean } => {
const length = formArray.length;
return length ? null : { formArrayIsEmpty: true };
};
}

54
ui-ngx/src/assets/help/en_US/widget/lib/gateway/attributes-identifier_fn.md

@ -0,0 +1,54 @@
An **Identifier** type is a unique ID assigned to a node within the OPC-UA server. It is used to directly reference specific nodes without navigating through the namespace hierarchy.
The **Identifier** type in the OPC-UA connector configuration can be used in various forms to uniquely reference nodes in the OPC-UA server's address space. Identifiers can be of different types, such as numeric (i), string (s), byte string (b), and GUID (g). Below is an explanation of each identifier type with examples.
- ##### Numeric Identifier (`i`)
A **numeric identifier** uses an integer value to uniquely reference a node in the OPC-UA server.
###### Example:
Gateway expects that the node exist and the value of “**ns=2;i=1235**” node is **21.34**.
_Expression:_
**`${ns=2;i=1235}`**
_Converted data:_
**`21.34`**
- ##### String Identifier (`s`)
A **string identifier** uses a string value to uniquely reference a node in the OPC-UA server.
###### Example:
Gateway expects that the node exist and the value of “**ns=3;s=TemperatureSensor**” node is **21.34**.
_Expression:_
**`${ns=3;s=TemperatureSensor}`**
_Converted data:_
**`21.34`**
- ##### Byte String Identifier (`b`)
A **byte string identifier** uses a byte string to uniquely reference a node in the OPC-UA server. This is useful for binary data that can be converted to a byte string.
###### Example:
Gateway expects that the node exist and the value of “**ns=4;b=Q2xpZW50RGF0YQ==**” node is **21.34**.
_Expression:_
**`${ns=4;b=Q2xpZW50RGF0YQ==}`**
_Converted data:_
**`21.34`**
- ##### GUID Identifier (`g`)
A **GUID identifier** uses a globally unique identifier (GUID) to uniquely reference a node in the OPC-UA server.
###### Example:
Gateway expects that the node exist and the value of “**ns=1;g=550e8400-e29b-41d4-a716-446655440000**” node is **21.34**.
_Expression:_
**`${ns=1;g=550e8400-e29b-41d4-a716-446655440000}`**
_Converted data:_
**`21.34`**

36
ui-ngx/src/assets/help/en_US/widget/lib/gateway/attributes-path_fn.md

@ -0,0 +1,36 @@
A **Path** type refers to the hierarchical address within the OPC-UA server's namespace. It is used to navigate to specific
nodes in the server.
The path for the attribute value can be absolute or relative.
### Absolute Path
An **absolute path** specifies the full hierarchical address from the root of the OPC-UA server's namespace to the target node.
###### Example:
Gateway expects that the node exist and the value of Root\\.Objects\\.TempSensor\\.Temperature is 23.54.
_Expression:_
**`${Root\.Objects\.TempSensor\.Temperature}`**
_Converted data:_
**`23.54`**
### Relative Path
A **relative path** specifies the address relative to a predefined starting point in the OPC-UA server's namespace.
###### Example:
Gateway expects that the node exist and the value of “Root\\.Objects\\.TempSensor\\.Temperature” is 23.56.
_Expression:_
**`${Temperature}`**
_Converted data:_
**`23.56`**
Additionally, you can use the node browser name to ensure that your path cannot be altered by the user or server.
###### Example:
**`Root\.0:Objects\.3:Simulation`**

46
ui-ngx/src/assets/help/en_US/widget/lib/gateway/attributes_updates-identifier_fn.md

@ -0,0 +1,46 @@
An **Identifier** type is a unique ID assigned to a node within the OPC-UA server. It is used to directly reference
specific nodes without navigating through the namespace hierarchy.
The **Identifier** type in the OPC-UA connector configuration can be used in various forms to uniquely reference nodes
in the OPC-UA server's address space. Identifiers can be of different types, such as numeric (`i`), string (`s`),
byte string (`b`), and GUID (`g`). Below is an explanation of each identifier type with examples.
- ##### Numeric Identifier (`i`)
A **numeric identifier** uses an integer value to uniquely reference a node in the OPC-UA server.
###### Example:
Gateway expects that the node exist.
_Expression:_
**`ns=2;i=1236`**
- ##### String Identifier (`s`)
A **string identifier** uses a string value to uniquely reference a node in the OPC-UA server.
###### Example:
Gateway expects that the node exist.
_Expression:_
**`ns=3;s=TemperatureSensor`**
- ##### Byte String Identifier (`b`)
A **byte string identifier** uses a byte string to uniquely reference a node in the OPC-UA server. This is useful for binary data that can be converted to a byte string.
###### Example:
Gateway expects that the node exist.
_Expression:_
**`ns=4;b=Q2xpZW50RGF0YQ==`**
- ##### GUID Identifier (`g`)
A **GUID identifier** uses a globally unique identifier (GUID) to uniquely reference a node in the OPC-UA server.
###### Example:
Gateway expects that the node exist.
_Expression:_
**`ns=1;g=550e8400-e29b-41d4-a716-446655440000`**

34
ui-ngx/src/assets/help/en_US/widget/lib/gateway/attributes_updates-path_fn.md

@ -0,0 +1,34 @@
A **Path** type refers to the hierarchical address within the OPC-UA server's namespace. It is used to navigate to
specific nodes in the server.
The path for device name can be absolute or relative.
### Absolute Path
An **absolute path** specifies the full hierarchical address from the root of the OPC-UA server's namespace to
the target node.
###### Example:
Gateway expects that the node exist and the value of **Root\\.Objects\\.TempSensor\\.Version** is “**1.0.3**”.
_Expression:_
**`Root\.Objects\.TempSensor\.Version`**
In this example, the attribute update request will write the received data to the configured node above.
### Relative Path
A **relative path** specifies the address relative to a predefined starting point in the OPC-UA server's namespace.
###### Example:
Gateway expects that the node exist and the value of **Root\\.Objects\\.TempSensor\\.Name** is “**TH-101**”.
_Device Node expression:_
**`Root\.Objects\.TempSensor`**
_Expression:_
**`.Version`**
In this example, **the gateway will search for the child node "Name" in the device node (parent node)
"Root\\.Objects\\.TempSensor"** and will write received data on it.

34
ui-ngx/src/assets/help/en_US/widget/lib/gateway/device-node-identifier_fn.md

@ -0,0 +1,34 @@
## Device Node Field
### Identifier:
Device Node field value is used to identify the node for the current device.
An Identifier type is a unique ID assigned to a node within the OPC-UA server. It is used to directly reference specific nodes without navigating through the namespace hierarchy.
The Identifier type in the OPC-UA connector configuration can be used in various forms to uniquely reference nodes in the OPC-UA server's address space. Identifiers can be of different types, such as numeric (`i`), string (`s`), byte string (`b`), and GUID (`g`). Below is an explanation of each identifier type with examples.
- ##### Numeric Identifier (`i`)
A numeric identifier uses an integer value to uniquely reference a node in the OPC-UA server.
###### Example:
`ns=2;i=1234`
In this example, `ns=2` specifies the namespace index `2`, and `i=1234` specifies the numeric identifier `1234` for the node.
- ##### String Identifier (`s`)
A string identifier uses a string value to uniquely reference a node in the OPC-UA server.
###### Example:
`ns=3;s=TemperatureSensor`
Here, `ns=3` specifies the namespace index `3`, and `s=TemperatureSensor` specifies the string identifier for the node.
- ##### Byte String Identifier (`b`)
An byte string identifier uses a byte string to uniquely reference a node in the OPC-UA server. This is useful for binary data that can be converted to a byte string.
###### Example:
`ns=4;b=Q2xpZW50RGF0YQ==`
In this example, `ns=4` specifies the namespace index `4`, and `b=Q2xpZW50RGF0YQ==` specifies the byte string identifier for the node (base64 encoded).
- ##### GUID Identifier (`g`)
A GUID identifier uses a globally unique identifier (GUID) to uniquely reference a node in the OPC-UA server.
###### Example:
`ns=1;g=550e8400-e29b-41d4-a716-446655440000`
Here, `ns=1` specifies the namespace index `1`, and `g=550e8400-e29b-41d4-a716-446655440000` specifies the GUID for the node.
By using these different identifier types, you can accurately and uniquely reference nodes in the OPC-UA server's address space, regardless of the format of the node identifiers.

20
ui-ngx/src/assets/help/en_US/widget/lib/gateway/device-node-path_fn.md

@ -0,0 +1,20 @@
## Device Node Field
### Path:
Device Node field value is used to identify the node for the current device. The connector will use this node as the parent node of the device.
A Path type refers to the hierarchical address within the OPC-UA server's namespace. It is used to navigate to specific nodes in the server.
The path for Device node can be only absolute.
An absolute path specifies the full hierarchical address from the root of the OPC-UA server's namespace to the target node.
###### Examples:
- `Root\.Objects\.TempSensor`
In this example, the `Value` specifies the full path to the `TempSensor` node located in the `Objects` namespace, starting from the root.
Additionally, you can use the node browser name to ensure that your path cannot be altered by the user or server.
- `Root\.0:Objects\.3:Simulation`

67
ui-ngx/src/assets/help/en_US/widget/lib/gateway/name-field-identifier_fn.md

@ -0,0 +1,67 @@
## Device Name Field
Device name field is used for looking the device name in some variable.
An **Identifier** type is a unique ID assigned to a node within the OPC-UA server. It is used to directly reference
specific nodes without navigating through the namespace hierarchy.
**Identifier** type in the OPC-UA connector configuration can be used in various forms to uniquely reference nodes in
the OPC-UA server's address space. Identifiers can be of different types, such as numeric (`**i**`), string (`**s**`),
byte string (`**b**`), and GUID (`**g**`). Below is an explanation of each identifier type with examples.
- ##### **Numeric Identifier (`i`)**
A **numeric identifier** uses an integer value to uniquely reference a node in the OPC-UA server.
###### **Example:**
Gateway expects that the node exist and the value of **ns=2;i=1234** node is “**TH-101**”.
_Expression:_
**`Device ${ns=2;i=1234}`**
In this example, created device on platform will have “**Device TH-101**” name.
- ##### **String Identifier (`s`)**
A **string identifier** uses a string value to uniquely reference a node in the OPC-UA server.
###### **Example:**
Gateway expects that the node exist and the value of **ns=3;s=TemperatureSensor** node is “**TH-101**”.
_Expression:_
**`Device ${ns=3;s=TemperatureSensor}`**
In this example, created device on platform will have “**Device TH-101**” name.
- ##### **Byte String Identifier (`b`)**
A **byte string identifier** uses a byte string to uniquely reference a node in the OPC-UA server.
This is useful for binary data that can be converted to a byte string.
###### **Example:**
Gateway expects that the node exist and the value of **ns=4;b=Q2xpZW50RGF0YQ==** node is “**TH-101**”.
_Expression:_
**`Device ${ns=4;b=Q2xpZW50RGF0YQ==}`**
In this example, created device on platform will have “**Device TH-101**” name.
- ##### GUID Identifier (**`g`**)
A **GUID identifier** uses a globally unique identifier (GUID) to uniquely reference a node in the OPC-UA server.
###### **Example:**
Gateway expects that the node exist and the value of **ns=1;g=550e8400-e29b-41d4-a716-446655440000** node is “**TH-101**”.
_Expression:_
Device ${ns=1;g=550e8400-e29b-41d4-a716-446655440000}
In this example, created device on platform will have “**Device TH-101**” name.

41
ui-ngx/src/assets/help/en_US/widget/lib/gateway/name-field-path_fn.md

@ -0,0 +1,41 @@
## Device Name Field
Device name field is used for looking the device name in some variable.
A **Path** type refers to the hierarchical address within the OPC-UA server's namespace. It is used to navigate to
specific nodes in the server.
The path for device name can be absolute or relative.
### Absolute Path
An **absolute path** specifies the full hierarchical address from the root of the OPC-UA server's namespace to the
target node.
##### **Example:**
Gateway expects that the node exist and the value of **Root\\.Objects\\.TempSensor\\.Name** is “**TH-101**”.
_Expression:_
`**Device ${Root\.Objects\.TempSensor\.Name}**`
In this example, created device on platform will have “**Device TH-101**” name.
### Relative Path
A relative path specifies the address relative to a predefined starting point in the OPC-UA server's namespace.
##### **Example:**
Gateway expects that the node exist and the value of **Root\\.Objects\\.TempSensor\\.Name** is “**TH-101**”.
_Device Node expression:_
`**Root\.Objects\.TempSensor**`
_Expression:_
`**Device ${Name}**`
In this example, **the gateway will search for the child node "Name" in the device node (parent node)
"Root\\.Objects\\.TempSensor"** and the created device on the platform will be named "**Device TH-101**".

60
ui-ngx/src/assets/help/en_US/widget/lib/gateway/profile-name-identifier_fn.md

@ -0,0 +1,60 @@
## Profile Name Field
Profile name field is used for looking the device profile name in some variable.
An **Identifier** type is a unique ID assigned to a node within the OPC-UA server. It is used to directly reference
specific nodes without navigating through the namespace hierarchy.
The **Identifier** type in the OPC-UA connector configuration can be used in various forms to uniquely reference nodes
in the OPC-UA server's address space. Identifiers can be of different types, such as numeric (**`i`**), string (**`s`**),
byte string (**`b`**), and GUID (**`g`**). Below is an explanation of each identifier type with examples.
- ##### Numeric Identifier (**`i`**)
A **numeric identifier** uses an integer value to uniquely reference a node in the OPC-UA server.
###### Example:
Gateway expects that the node exist and the value of **ns=2;i=1235** node is “**thermostat**”.
_Expression:_
**`Device ${ns=2;i=1235}`**
In this example, created device on platform will have “**thermostat**” profile name.
- ##### String Identifier (**`s`**)
A **string identifier** uses a string value to uniquely reference a node in the OPC-UA server.
###### Example:
Gateway expects that the node exist and the value of **ns=3;s=TemperatureSensorType** node is “**thermostat**”.
_Expression:_
**`Device ${ns=3;s=TemperatureSensorType}`**
In this example, created device on platform will have “**thermostat**” profile name.
- ##### Byte String Identifier (**`b`**)
A **byte string identifier** uses a byte string to uniquely reference a node in the OPC-UA server. This is useful for
binary data that can be converted to a byte string.
###### Example:
Gateway expects that the node exist and the value of **ns=4;b=Q2xpZW50RGF0YQ==** node is “**thermostat**”.
_Expression:_
**`Device ${ns=4;b=Q2xpZW50RGF0YQ==}`**
In this example, created device on platform will have “**thermostat**” profile name.
- ##### GUID Identifier (**`g`**)
A **GUID identifier** uses a globally unique identifier (GUID) to uniquely reference a node in the OPC-UA server.
###### Example:
Gateway expects that the node exist and the value of **ns=1;g=550e8400-e29b-41d4-a716-446655440000** node is
“**thermostat**”.
_Expression:_
**`Device ${ns=1;g=550e8400-e29b-41d4-a716-446655440000}`**
In this example, created device on platform will have “**thermostat**” profile name.

38
ui-ngx/src/assets/help/en_US/widget/lib/gateway/profile-name-path_fn.md

@ -0,0 +1,38 @@
## Profile Name Field
The profile name field is used for looking the device profile name in some variable.
A **Path** type refers to the hierarchical address within the OPC-UA server's namespace. It is used to navigate to
specific nodes in the server.
The path for device name can be absolute or relative.
### Absolute Path
An absolute path specifies the full hierarchical address from the root of the OPC-UA server's namespace to the target
node.
##### Example:
Gateway expects that the node exist and the value of **Root\\.Objects\\.TempSensor\\.Name** is “**thermostat**”.
_Expression:_
**`Device ${Root\.Objects\.TempSensor\.Type}`**
In this example, created device on platform will have “**thermostat**” profile name.
### Relative Path
A relative path specifies the address relative to a predefined starting point in the OPC-UA server's namespace.
##### Example:
Gateway expects that the node exist and the value of Root\\.Objects\\.TempSensor\\.Type is “thermostat”.
_Device Node expression:_
**`Root\.Objects\.TempSensor`**
_Expression:_
**`Device ${Type}`**
In this example, **the gateway will search for the child node "Name" in the device node (parent node)
"Root\\.Objects\\.TempSensor"** and the created device on the platform will have "thermostat" profile name.

62
ui-ngx/src/assets/help/en_US/widget/lib/gateway/timeseries-identifier_fn.md

@ -0,0 +1,62 @@
An **Identifier** type is a unique ID assigned to a node within the OPC-UA server. It is used to directly reference
specific nodes without navigating through the namespace hierarchy.
The **Identifier** type in the OPC-UA connector configuration can be used in various forms to uniquely reference nodes in
the OPC-UA server's address space. Identifiers can be of different types, such as numeric (`i`), string (`s`),
byte string (`b`), and GUID (`g`). Below is an explanation of each identifier type with examples.
- ##### Numeric Identifier (`i`)
A **numeric identifier** uses an integer value to uniquely reference a node in the OPC-UA server.
###### Example:
Gateway expects that the node exist and the value of “**ns=2;i=1235**” node is **21.34**.
_Expression:_
**`${ns=2;i=1235}`**
_Converted data:_
**`21.34`**
- ##### String Identifier (`s`)
A **string identifier** uses a string value to uniquely reference a node in the OPC-UA server.
###### Example:
Gateway expects that the node exist and the value of “**ns=3;s=TemperatureSensor**” node is **21.34**.
_Expression:_
**`${ns=3;s=TemperatureSensor}`**
_Converted data:_
**`21.34`**
- ##### Byte String Identifier (`b`)
A **byte string identifier** uses a byte string to uniquely reference a node in the OPC-UA server. This is useful for binary data that can be converted to a byte string.
###### Example:
Gateway expects that the node exist and the value of “**ns=4;b=Q2xpZW50RGF0YQ==**” node is **21.34**.
_Expression:_
**`${ns=4;b=Q2xpZW50RGF0YQ==}`**
_Converted data:_
**`21.34`**
- ##### GUID Identifier (`g`)
A **GUID identifier** uses a globally unique identifier (GUID) to uniquely reference a node in the OPC-UA server.
###### Example:
Gateway expects that the node exist and the value of “**ns=1;g=550e8400-e29b-41d4-a716-446655440000**” node is **21.34**.
_Expression:_
**`${ns=1;g=550e8400-e29b-41d4-a716-446655440000}`**
_Converted data:_
**`21.34`**

45
ui-ngx/src/assets/help/en_US/widget/lib/gateway/timeseries-path_fn.md

@ -0,0 +1,45 @@
A **Path** type refers to the hierarchical address within the OPC-UA server's namespace. It is used to navigate to
specific nodes in the server.
The path for the attribute value can be absolute or relative.
### Absolute Path
An **absolute path** specifies the full hierarchical address from the root of the OPC-UA server's namespace to
the target node.
###### Example:
Gateway expects that the node exist and the value of **Root\\.Objects\\.TempSensor\\.Temperature** is **23.54**.
_Expression:_
**`${Root\.Objects\.TempSensor\.Temperature}`**
_Converted data:_
**`23.54`**
### Relative Path
A **relative path** specifies the address relative to a predefined starting point in the OPC-UA server's namespace.
###### Example:
Gateway expects that the node exist and the value of “**Root\\.Objects\\.TempSensor\\.Temperature**” is **23.56**.
_Device Node expression:_
**`Root\.Objects\.TempSensor`**
_Expression:_
**`${Temperature}`**
_Converted data:_
**`23.56`**
In this example, **the gateway will search for the child node "Temperature" in the device node (parent node)
"Root\\.Objects\\.TempSensor"** and will send converted data to the device.
Additionally, you can use the node browser name to ensure that your path cannot be altered by the user or server.
###### Example:
**`Root\.0:Objects\.3:Simulation`**

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

@ -2756,11 +2756,19 @@
"gateway": {
"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",
"arguments": "Arguments",
"add-rpc-method": "Add method",
"add-value": "Add argument",
"delete-value": "Delete value",
"delete-rpc-method": "Delete method",
"delete-attribute-update": "Add attribute update",
"advanced": "Advanced",
"attributes": "Attributes",
"attribute-updates": "Attribute updates",
"attribute-filter": "Attribute filter",
"attribute-filter-hint": "Filter for incoming attribute name from platform, supports regular expression.",
"attribute-filter-required": "Attribute filter required.",
@ -2823,8 +2831,8 @@
"name": "Name",
"profile-name": "Profile name",
"device-name-expression": "Device name expression",
"device-name-expression-required": "Device name expression required.",
"device-profile-expression-required": "Device profile expression required."
"device-name-expression-required": "Device name expression is required.",
"device-profile-expression-required": "Device profile expression is required."
},
"device-name-filter": "Device name filter",
"device-name-filter-hint": "This field supports Regular expressions to filter incoming data by device name.",
@ -2863,13 +2871,20 @@
"data-conversion": "Data conversion",
"data-mapping": "Data mapping",
"data-mapping-hint": "Data mapping provides the capability to parse and convert the data received from a MQTT client in incoming messages into specific attributes and time series data keys.",
"opcua-data-mapping-hint": "Data mapping provides the capability to parse and convert the data received from a OPCUA server into specific data keys.",
"delete": "Delete configuration",
"delete-attribute": "Delete attribute",
"delete-key": "Delete key",
"delete-timeseries": "Delete time series",
"default": "Default",
"device-node": "Device node",
"device-node-required": "Device node required.",
"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",
"download-tip": "Download configuration file",
"drop-file": "Drop file here or",
"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.",
"extension-required": "Extension is required.",
@ -2908,6 +2923,7 @@
"grpc-max-pings-without-data-required": "Max pings without data is required",
"grpc-max-pings-without-data-min": "Max pings without data can not be less then 1",
"grpc-max-pings-without-data-pattern": "Max pings without data is not valid",
"identity": "Identity",
"inactivity-check-period-seconds": "Inactivity check period (in sec)",
"inactivity-check-period-seconds-required": "Inactivity check period is required",
"inactivity-check-period-seconds-min": "Inactivity check period can not be less then 1",
@ -2951,19 +2967,25 @@
"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-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",
"mqtt-version": "MQTT version",
"name": "Name",
"name-required": "Name is required.",
"no-attributes": "No attributes",
"no-attribute-updates": "No attribute updates",
"no-connectors": "No connectors",
"no-data": "No configurations",
"no-gateway-found": "No gateway found.",
"no-gateway-matching": " '{{item}}' not found.",
"no-timeseries": "No time series",
"no-keys": "No keys",
"no-value": "No arguments",
"no-rpc-methods": "No RPC methods",
"path-hint": "The path is local to the gateway file system",
"path-logs": "Path to log files",
"path-logs-required": "Path is required.",
@ -3073,6 +3095,7 @@
"write-multiple-holding-registers": "16: Write Multiple Holding Registers",
"json-value-invalid": "JSON value has an invalid format"
},
"rpc-methods": "RPC methods",
"request" : {
"connect-request": "Connect request",
"disconnect-request": "Disconnect request",
@ -3102,6 +3125,10 @@
"without-response": "Without response",
"other": "Other",
"save-tip": "Save configuration file",
"scan-period": "Scan period (ms)",
"scan-period-error": "Scan period should be at least {{min}}(ms).",
"sub-check-period": "Subscription check period (ms)",
"sub-check-period-error": "Subscription check period should be at least {{min}}(ms).",
"security": "Security",
"security-type": "Security type",
"security-types": {
@ -3114,8 +3141,12 @@
"select-connector": "Select connector to display config",
"send-change-data": "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-port": "Server port",
"server-url": "Server endpoint url",
"server-url-required": "Server endpoint url is required.",
"set": "Set",
"show-map": "Show map",
"statistics": {
"statistic": "Statistic",
"statistics": "Statistics",
@ -3177,7 +3208,9 @@
"source-type": {
"msg": "Extract from message",
"topic": "Extract from topic",
"const": "Constant"
"const": "Constant",
"identifier": "Identifier",
"path": "Path"
},
"workers-settings": "Workers settings",
"thingsboard": "ThingsBoard",
@ -3195,6 +3228,8 @@
"thingsboard-port-required": "Port is required.",
"tidy": "Tidy",
"tidy-tip": "Tidy config JSON",
"timeout": "Timeout (ms)",
"timeout-error": "Timeout should be at least {{min}}(ms).",
"title-connectors-json": "Connector {{typeName}} configuration",
"type": "Type",
"topic-filter": "Topic filter",
@ -3267,7 +3302,14 @@
"permit-without-calls": "Allow server to keep the GRPC connection alive even when there are no active RPC calls.",
"memory": "Your data will be stored in the in-memory queue, it is a fastest but no persistence guarantee.",
"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."
"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.",
"scan-period": "Period in milliseconds to rescan the server.",
"sub-check-period": "Period to check the subscriptions in the OPC-UA server.",
"enable-subscription": "If true - the gateway will subscribe to interesting nodes and wait for data update and if false - the gateway will rescan OPC-UA server every scanPeriodInMillis.",
"show-map": "Show nodes on scanning.",
"method-name": "Name of method on OPC-UA server.",
"arguments": "Arguments for the method (will be overwritten by arguments from the RPC request)."
}
},
"grid": {

78
ui-ngx/src/assets/metadata/connector-default-configs/opcua.json

@ -4,49 +4,63 @@
"url": "localhost:4840/freeopcua/server/",
"timeoutInMillis": 5000,
"scanPeriodInMillis": 5000,
"disableSubscriptions": false,
"enableSubscriptions": true,
"subCheckPeriodInMillis": 100,
"showMap": false,
"security": "Basic128Rsa15",
"identity": {
"type": "anonymous"
}
},
"mapping": [{
"deviceNodePattern": "Root\\.Objects\\.Device1",
"deviceNodeSource": "path",
"deviceInfo": {
"deviceNameExpression": "Device ${Root\\.Objects\\.Device1\\.serialNumber}",
"deviceNameExpressionSource": "path",
"deviceProfileExpression": "Device",
"deviceProfileExpressionSource": "constant"
},
"mapping": [
"attributes": [
{
"deviceNodePattern": "Root\\.Objects\\.Device1",
"deviceNamePattern": "Device ${Root\\.Objects\\.Device1\\.serialNumber}",
"attributes": [
{
"key": "temperature °C",
"path": "${ns=2;i=5}"
}
],
"timeseries": [
"key": "temperature °C",
"type": "path",
"value": "${ns=2;i=5}"
}
],
"timeseries": [
{
"key": "humidity",
"type": "path",
"value": "${Root\\.Objects\\.Device1\\.TemperatureAndHumiditySensor\\.Humidity}"
},
{
"key": "batteryLevel",
"type": "path",
"value": "${Battery\\.batteryLevel}"
}
],
"rpc_methods": [
{
"method": "multiply",
"arguments": [
{
"key": "humidity",
"path": "${Root\\.Objects\\.Device1\\.TemperatureAndHumiditySensor\\.Humidity}"
"type": "integer",
"value": 2
},
{
"key": "batteryLevel",
"path": "${Battery\\.batteryLevel}"
}
],
"rpc_methods": [
{
"method": "multiply",
"arguments": [
2,
4
]
}
],
"attributes_updates": [
{
"attributeOnThingsBoard": "deviceName",
"attributeOnDevice": "Root\\.Objects\\.Device1\\.serialNumber"
"type": "integer",
"value": 4
}
]
}
],
"attributes_updates": [
{
"key": "deviceName",
"type": "path",
"value": "Root\\.Objects\\.Device1\\.serialNumber"
}
]
}
}
}]
}

Loading…
Cancel
Save