diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-config-control/broker-config-control.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-config-control/broker-config-control.component.html new file mode 100644 index 0000000000..4c2db19345 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-config-control/broker-config-control.component.html @@ -0,0 +1,86 @@ + +
+
+
gateway.host
+
+ + + + warning + + +
+
+
+
gateway.port
+
+ + + + warning + + +
+
+
+
gateway.mqtt-version
+
+ + + {{ version.name }} + + +
+
+
+
gateway.client-id
+
+ + + + +
+
+ + +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-config-control/broker-config-control.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-config-control/broker-config-control.component.ts new file mode 100644 index 0000000000..d980bed4a7 --- /dev/null +++ b/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); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/device-info-table.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/device-info-table/device-info-table.component.html similarity index 88% rename from ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/device-info-table.component.html rename to ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/device-info-table/device-info-table.component.html index 2a3108b8b4..0a33f5b885 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/device-info-table.component.html +++ b/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 -
@@ -83,9 +84,10 @@ class="tb-error"> warning -
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/device-info-table.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/device-info-table/device-info-table.component.scss similarity index 100% rename from ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/device-info-table.component.scss rename to ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/device-info-table/device-info-table.component.scss diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/device-info-table.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/device-info-table/device-info-table.component.ts similarity index 83% rename from ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/device-info-table.component.ts rename to ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/device-info-table/device-info-table.component.ts index 8a40f25430..569f40b185 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/device-info-table.component.ts +++ b/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 = Object.values(SourceTypes); + sourceTypes: Array = Object.values(SourceTypes); deviceInfoTypeValue: any; @@ -111,14 +104,6 @@ export class DeviceInfoTableComponent extends PageComponent implements ControlVa constructor(protected store: Store, 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); } - } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/index.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/index.ts new file mode 100644 index 0000000000..bd0d4787d6 --- /dev/null +++ b/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'; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-data-keys-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-data-keys-panel/mapping-data-keys-panel.component.html similarity index 71% rename from ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-data-keys-panel.component.html rename to ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-data-keys-panel/mapping-data-keys-panel.component.html index 4c3a6434fd..8b95c0bd3a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-data-keys-panel.component.html +++ b/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 @@ -
{{ keyControl.get('key').value }}
- {{ '-' }} -
{{ valueTitle(keyControl.get('value').value) }}
+
+ {{ keyControl.get('key').value }}{{ '-' }} +
+
{{ valueTitle(keyControl) }}
+ *ngIf="keysType !== MappingKeysType.CUSTOM && keysType !== MappingKeysType.RPC_METHODS">
gateway.platform-side
@@ -53,12 +54,6 @@ class="tb-error"> warning -
-
@@ -71,18 +66,25 @@
- + - - {{ (rawData ? 'gateway.raw' : valueTypes.get(keyControl.get('type').value)?.name) | translate}} + + {{ (valueTypes.get(keyControl.get('type').value)?.name || valueTypes.get(keyControl.get('type').value)) | translate }} + + {{ 'gateway.raw' | translate }} +
- + - {{ valueTypes.get(valueType).name | translate }} + + {{ valueTypes.get(valueType).name || valueTypes.get(valueType) | translate }} + @@ -112,7 +114,8 @@
@@ -120,7 +123,7 @@
- +
gateway.key
@@ -152,7 +155,41 @@
- +
+
+
+
+ gateway.method-name +
+
+ + + + warning + + +
+
+
+ + + +
+ {{ 'gateway.arguments' | translate }}{{' (' + keyControl.get('arguments').value?.length + ')'}} +
+
+
+ + + +
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-data-keys-panel.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-data-keys-panel/mapping-data-keys-panel.component.scss similarity index 100% rename from ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-data-keys-panel.component.scss rename to ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-data-keys-panel/mapping-data-keys-panel.component.scss diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-data-keys-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-data-keys-panel/mapping-data-keys-panel.component.ts similarity index 64% rename from ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-data-keys-panel.component.ts rename to ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-data-keys-panel/mapping-data-keys-panel.component.ts index 40ee2728bd..7867a7213f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-data-keys-panel.component.ts +++ b/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 | {[key: string]: any}; @Input() - keysType: string; + keysType: MappingKeysType; + + @Input() + valueTypeKeys: Array = Object.values(MappingValueType); + + @Input() + valueTypeEnum = MappingValueType; + + @Input() + valueTypes: Map = mappingValueTypesMap; @Input() @coerceBoolean() @@ -76,16 +89,10 @@ export class MappingDataKeysPanelComponent extends PageComponent implements OnIn popover: TbPopoverComponent; @Output() - keysDataApplied = new EventEmitter | {[key: string]: any}>(); - - valueTypeKeys = Object.values(MappingValueType); + keysDataApplied = new EventEmitter | {[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 | {[key: string]: any}): UntypedFormArray { + private prepareKeysFormArray(keys: Array | {[key: string]: any}): UntypedFormArray { const keysControlGroups: Array = []; 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 ''; } - } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table/mapping-table.component.html similarity index 96% rename from ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table.component.html rename to ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table/mapping-table.component.html index e5c703d8d0..48e280bc67 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table/mapping-table.component.html @@ -60,10 +60,12 @@
- + {{ column.title | translate }} - + {{ mapping[column.def] }} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table/mapping-table.component.scss similarity index 100% rename from ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table.component.scss rename to ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table/mapping-table.component.scss diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table/mapping-table.component.ts similarity index 59% rename from ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table.component.ts rename to ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table/mapping-table.component.ts index 4ef9695c65..75274c9099 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table.component.ts +++ b/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(); - private propagateChange = (v: any) => {}; - constructor(protected store: Store, - 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, { + this.dialog.open(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) ); } - } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-security.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/security-config/security-config.component.html similarity index 65% rename from ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-security.component.html rename to ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/security-config/security-config.component.html index 319b10f0b2..87976abf08 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-security.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/security-config/security-config.component.html @@ -17,7 +17,7 @@ -->
-
gateway.security
+
{{ title | translate }}
{{ SecurityTypeTranslationsMap.get(type) | translate }} @@ -81,6 +81,48 @@
+ +
+
gateway.mode
+
+ + + + {{ type }} + + + +
+
+
+
gateway.username
+
+ + + + warning + + +
+
+
+
gateway.password
+
+ + +
+ +
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-security.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/security-config/security-config.component.scss similarity index 100% rename from ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-security.component.scss rename to ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/security-config/security-config.component.scss diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-security.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/security-config/security-config.component.ts similarity index 57% rename from ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-security.component.ts rename to ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/security-config/security-config.component.ts index 655545e5fb..94f1eb8395 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-security.component.ts +++ b/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(); - private propagateChange = (v: any) => {}; - - constructor(protected store: Store, - 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}); + } } } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/server-config/server-config.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/server-config/server-config.component.html new file mode 100644 index 0000000000..ee097ae642 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/server-config/server-config.component.html @@ -0,0 +1,125 @@ + +
+
+
gateway.server-url
+
+ + + + warning + + +
+
+
+
+ gateway.timeout +
+
+ + + + warning + + +
+
+
+
gateway.security
+
+ + + {{ version.name }} + + +
+
+
+
+ gateway.scan-period +
+
+ + + + warning + + +
+
+
+
+ gateway.sub-check-period +
+
+ + + + warning + + +
+
+
+ + + {{ 'gateway.enable-subscription' | translate }} + + +
+
+ + + {{ 'gateway.show-map' | translate }} + + +
+ + +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/server-config/server-config.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/server-config/server-config.component.scss new file mode 100644 index 0000000000..416f368279 --- /dev/null +++ b/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; +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/server-config/server-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/server-config/server-config.component.ts new file mode 100644 index 0000000000..3466387f3c --- /dev/null +++ b/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); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/type-value-panel/type-value-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/type-value-panel/type-value-panel.component.html new file mode 100644 index 0000000000..7ce7fb94f0 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/type-value-panel/type-value-panel.component.html @@ -0,0 +1,92 @@ + +
+
+
+
+ + + + +
{{ valueTitle(keyControl.get('value').value) }}
+
+
+ +
+
gateway.type
+
+ + + +
+ + + + {{ valueTypes.get(keyControl.get('type').value)?.name | translate}} + +
+
+ + + + {{ valueTypes.get(valueType).name | translate }} + +
+
+
+
+
+
gateway.value
+ + + + warning + + +
+
+
+
+
+ +
+
+
+ +
+
+ +
+ {{ 'gateway.no-value' }} +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/type-value-panel/type-value-panel.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/type-value-panel/type-value-panel.component.scss new file mode 100644 index 0000000000..687025e729 --- /dev/null +++ b/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; + } +} + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/type-value-panel/type-value-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/type-value-panel/type-value-panel.component.ts new file mode 100644 index 0000000000..e163d463ed --- /dev/null +++ b/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(); + 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): 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); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/workers-config-control/workers-config-control.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/workers-config-control/workers-config-control.component.html new file mode 100644 index 0000000000..37f7b1422e --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/workers-config-control/workers-config-control.component.html @@ -0,0 +1,63 @@ + +
+
+
+ gateway.max-number-of-workers +
+
+ + + + warning + + +
+
+
+
+ gateway.max-messages-queue-for-worker +
+
+ + + + warning + + +
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/workers-config-control/workers-config-control.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/workers-config-control/workers-config-control.component.ts new file mode 100644 index 0000000000..45e3aef7e8 --- /dev/null +++ b/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); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.html index 78ceaa566f..a202c85fcb 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.html @@ -615,6 +615,124 @@ + +
+
+
+ gateway.device-node +
+
+
+ + + + {{ SourceTypeTranslationsMap.get(type) | translate }} + + + + + + + warning + +
+
+
+
+
+ + +
+
gateway.attributes
+
+ + + {{ attribute }} + + + + + + +
+
+
+
gateway.timeseries
+
+ + + {{ telemetry }} + + + + + + +
+
+
+
gateway.attribute-updates
+
+ + + {{ attribute }} + + + + + + +
+
+
+
gateway.rpc-methods
+
+ + + {{ attribute }} + + + + + + +
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.scss index db98bc07cb..f70fba0e80 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.scss +++ b/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; + } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.ts index d01050b52e..00e5f2f2ad 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.ts +++ b/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> implements OnDestroy { +export class MappingDialogComponent extends DialogComponent implements OnDestroy { mappingForm: UntypedFormGroup; @@ -72,6 +71,8 @@ export class MappingDialogComponent extends DialogComponent; + OPCUaSourceTypesEnum = OPCUaSourceTypes; sourceTypesEnum = SourceTypes; SourceTypeTranslationsMap = SourceTypeTranslationsMap; @@ -102,7 +103,7 @@ export class MappingDialogComponent extends DialogComponent, protected router: Router, @Inject(MAT_DIALOG_DATA) public data: MappingInfo, - public dialogRef: MatDialogRef, + public dialogRef: MatDialogRef, private fb: FormBuilder, private popoverService: TbPopoverService, private renderer: Renderer2, @@ -125,6 +126,22 @@ export class MappingDialogComponent extends DialogComponent { + return this.mappingForm.get('attributes').value?.map(value => value.key) || []; + } + + get opcTelemetry(): Array { + return this.mappingForm.get('timeseries').value?.map(value => value.key) || []; + } + + get opcRpcMethods(): Array { + return this.mappingForm.get('rpc_methods').value?.map(value => value.method) || []; + } + + get opcAttributesUpdates(): Array { + 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 { - 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 { + 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()); } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.html index 3a7f5403b9..308f9d1a9c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.html +++ b/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 }} - + {{ 'gateway.basic' | translate }} @@ -219,144 +222,41 @@ - - - -
-
-
gateway.host
-
- - - - warning - - -
+ + + + + + + +
+
-
-
gateway.port
-
- - - - warning - - -
+ + +
+
-
-
gateway.mqtt-version
-
- - - {{ version.name }} - - -
+ + +
+
-
-
gateway.client-id
-
- - - - + + + + + + + +
+
-
- - -
- -
- -
- -
-
- -
- -
-
- -
- -
-
-
- gateway.max-number-of-workers -
-
- - - - warning - - -
-
-
-
- gateway.max-messages-queue-for-worker -
-
- - - - warning - - -
-
-
-
-
-
+ + + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts index 4f1b6b72a9..5f329ce66b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts +++ b/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; 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(0); + private inactiveConnectors: Array; 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): 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): void { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts index 0e143cff36..c32baf8d66 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts +++ b/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]*(? | 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 +} + 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.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.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.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( +export const SourceTypeTranslationsMap = new Map( [ [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( 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.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.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 => resourcesService.loadJsonResource(`/assets/metadata/connector-default-configs/${type}.json`); @@ -540,3 +709,15 @@ export const DataConversionTranslationsMap = new Map( [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' } +]; diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts b/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts index a473df40bb..5dffc4b8d9 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts +++ b/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, diff --git a/ui-ngx/src/app/modules/home/pipes/gateway-help-link/gateway-help-link.pipe.ts b/ui-ngx/src/app/modules/home/pipes/gateway-help-link/gateway-help-link.pipe.ts new file mode 100644 index 0000000000..0fec377860 --- /dev/null +++ b/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 ): 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'; + } +} diff --git a/ui-ngx/src/app/modules/home/pipes/index.ts b/ui-ngx/src/app/modules/home/pipes/index.ts new file mode 100644 index 0000000000..1cb3ce312a --- /dev/null +++ b/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'; diff --git a/ui-ngx/src/app/shared/validators/form-array.validators.ts b/ui-ngx/src/app/shared/validators/form-array.validators.ts new file mode 100644 index 0000000000..d7358d58d6 --- /dev/null +++ b/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 }; + }; +} diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/gateway/attributes-identifier_fn.md b/ui-ngx/src/assets/help/en_US/widget/lib/gateway/attributes-identifier_fn.md new file mode 100644 index 0000000000..7989fd7357 --- /dev/null +++ b/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`** diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/gateway/attributes-path_fn.md b/ui-ngx/src/assets/help/en_US/widget/lib/gateway/attributes-path_fn.md new file mode 100644 index 0000000000..48ae478273 --- /dev/null +++ b/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`** diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/gateway/attributes_updates-identifier_fn.md b/ui-ngx/src/assets/help/en_US/widget/lib/gateway/attributes_updates-identifier_fn.md new file mode 100644 index 0000000000..0ffb3d8cd1 --- /dev/null +++ b/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`** diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/gateway/attributes_updates-path_fn.md b/ui-ngx/src/assets/help/en_US/widget/lib/gateway/attributes_updates-path_fn.md new file mode 100644 index 0000000000..f710c0fc72 --- /dev/null +++ b/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. diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/gateway/device-node-identifier_fn.md b/ui-ngx/src/assets/help/en_US/widget/lib/gateway/device-node-identifier_fn.md new file mode 100644 index 0000000000..6ad440978e --- /dev/null +++ b/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. diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/gateway/device-node-path_fn.md b/ui-ngx/src/assets/help/en_US/widget/lib/gateway/device-node-path_fn.md new file mode 100644 index 0000000000..73dde417de --- /dev/null +++ b/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` diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/gateway/name-field-identifier_fn.md b/ui-ngx/src/assets/help/en_US/widget/lib/gateway/name-field-identifier_fn.md new file mode 100644 index 0000000000..f3c5987582 --- /dev/null +++ b/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. diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/gateway/name-field-path_fn.md b/ui-ngx/src/assets/help/en_US/widget/lib/gateway/name-field-path_fn.md new file mode 100644 index 0000000000..731fbd66fe --- /dev/null +++ b/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**". + diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/gateway/profile-name-identifier_fn.md b/ui-ngx/src/assets/help/en_US/widget/lib/gateway/profile-name-identifier_fn.md new file mode 100644 index 0000000000..eb8f4e44bf --- /dev/null +++ b/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. diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/gateway/profile-name-path_fn.md b/ui-ngx/src/assets/help/en_US/widget/lib/gateway/profile-name-path_fn.md new file mode 100644 index 0000000000..fb8c35dee6 --- /dev/null +++ b/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. diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/gateway/timeseries-identifier_fn.md b/ui-ngx/src/assets/help/en_US/widget/lib/gateway/timeseries-identifier_fn.md new file mode 100644 index 0000000000..f144dcac52 --- /dev/null +++ b/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`** diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/gateway/timeseries-path_fn.md b/ui-ngx/src/assets/help/en_US/widget/lib/gateway/timeseries-path_fn.md new file mode 100644 index 0000000000..bb2bf59c4f --- /dev/null +++ b/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`** diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 4eb0679b92..45a8085628 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/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": { diff --git a/ui-ngx/src/assets/metadata/connector-default-configs/opcua.json b/ui-ngx/src/assets/metadata/connector-default-configs/opcua.json index 91ee295648..97621deda8 100644 --- a/ui-ngx/src/assets/metadata/connector-default-configs/opcua.json +++ b/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" + } ] - } -} \ No newline at end of file + }] +}