Browse Source

Merge branch 'feature/3806-opcua-extension' of github.com:maxunbearable/thingsboard into feature/3806-opcua-extension

pull/11101/head
Vladyslav_Prykhodko 2 years ago
parent
commit
026c50c249
  1. 86
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-config-control/broker-config-control.component.html
  2. 136
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-config-control/broker-config-control.component.ts
  3. 10
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/device-info-table/device-info-table.component.html
  4. 0
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/device-info-table/device-info-table.component.scss
  5. 25
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/device-info-table/device-info-table.component.ts
  6. 80
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/general-config/general-config.component.html
  7. 52
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/general-config/general-config.component.ts
  8. 73
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-data-keys-panel/mapping-data-keys-panel.component.html
  9. 0
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-data-keys-panel/mapping-data-keys-panel.component.scss
  10. 80
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-data-keys-panel/mapping-data-keys-panel.component.ts
  11. 6
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table/mapping-table.component.html
  12. 0
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table/mapping-table.component.scss
  13. 273
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table/mapping-table.component.ts
  14. 24
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mqtt-basic-config/mqtt-basic-config.component.html
  15. 170
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mqtt-basic-config/mqtt-basic-config.component.ts
  16. 13
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc-ua-basic-config/opc-ua-basic-config.component.html
  17. 139
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc-ua-basic-config/opc-ua-basic-config.component.ts
  18. 27
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/public-api.ts
  19. 44
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/security-config/security-config.component.html
  20. 0
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/security-config/security-config.component.scss
  21. 140
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/security-config/security-config.component.ts
  22. 125
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/server-config/server-config.component.html
  23. 20
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/server-config/server-config.component.scss
  24. 121
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/server-config/server-config.component.ts
  25. 92
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/type-value-panel/type-value-panel.component.html
  26. 52
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/type-value-panel/type-value-panel.component.scss
  27. 144
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/type-value-panel/type-value-panel.component.ts
  28. 63
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/workers-config-control/workers-config-control.component.html
  29. 101
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/workers-config-control/workers-config-control.component.ts
  30. 118
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.html
  31. 4
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.scss
  32. 347
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.ts
  33. 231
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.html
  34. 160
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts
  35. 222
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts
  36. 2
      ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget2.ts
  37. 0
      ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/public-api.ts
  38. 305
      ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts
  39. 39
      ui-ngx/src/app/modules/home/pipes/gateway-help-link/gateway-help-link.pipe.ts
  40. 17
      ui-ngx/src/app/modules/home/pipes/public-api.ts
  41. 54
      ui-ngx/src/assets/help/en_US/widget/lib/gateway/attributes-identifier_fn.md
  42. 36
      ui-ngx/src/assets/help/en_US/widget/lib/gateway/attributes-path_fn.md
  43. 46
      ui-ngx/src/assets/help/en_US/widget/lib/gateway/attributes_updates-identifier_fn.md
  44. 34
      ui-ngx/src/assets/help/en_US/widget/lib/gateway/attributes_updates-path_fn.md
  45. 34
      ui-ngx/src/assets/help/en_US/widget/lib/gateway/device-node-identifier_fn.md
  46. 20
      ui-ngx/src/assets/help/en_US/widget/lib/gateway/device-node-path_fn.md
  47. 67
      ui-ngx/src/assets/help/en_US/widget/lib/gateway/name-field-identifier_fn.md
  48. 41
      ui-ngx/src/assets/help/en_US/widget/lib/gateway/name-field-path_fn.md
  49. 60
      ui-ngx/src/assets/help/en_US/widget/lib/gateway/profile-name-identifier_fn.md
  50. 38
      ui-ngx/src/assets/help/en_US/widget/lib/gateway/profile-name-path_fn.md
  51. 62
      ui-ngx/src/assets/help/en_US/widget/lib/gateway/timeseries-identifier_fn.md
  52. 45
      ui-ngx/src/assets/help/en_US/widget/lib/gateway/timeseries-path_fn.md
  53. 50
      ui-ngx/src/assets/locale/locale.constant-en_US.json
  54. 78
      ui-ngx/src/assets/metadata/connector-default-configs/opcua.json

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

@ -0,0 +1,86 @@
<!--
Copyright © 2016-2024 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div class="tb-form-panel no-border no-padding padding-top" [formGroup]="brokerConfigFormGroup">
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required" translate>gateway.host</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="host" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.host-required') | translate"
*ngIf="brokerConfigFormGroup.get('host').hasError('required')
&& brokerConfigFormGroup.get('host').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required" translate>gateway.port</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput type="number" min="{{portLimits.MIN}}" max="{{portLimits.MAX}}"
name="value" formControlName="port" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="portErrorTooltip"
*ngIf="(brokerConfigFormGroup.get('port').hasError('required') ||
brokerConfigFormGroup.get('port').hasError('min') ||
brokerConfigFormGroup.get('port').hasError('max')) &&
brokerConfigFormGroup.get('port').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" translate>gateway.mqtt-version</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="version">
<mat-option *ngFor="let version of mqttVersions" [value]="version.value">{{ version.name }}</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" translate>gateway.client-id</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="clientId" placeholder="{{ 'gateway.set' | translate }}"/>
<button type="button"
matSuffix
mat-icon-button
aria-label="Generate"
matTooltip="{{ 'gateway.generate-client-id' | translate }}"
matTooltipPosition="above"
(click)="generate('clientId')"
*ngIf="!brokerConfigFormGroup.get('clientId').value">
<mat-icon>autorenew</mat-icon>
</button>
</mat-form-field>
</div>
</div>
<tb-security-config formControlName="security">
</tb-security-config>
</div>

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

@ -0,0 +1,136 @@
///
/// Copyright © 2016-2024 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import {
ChangeDetectionStrategy,
Component,
forwardRef,
inject,
OnDestroy,
} from '@angular/core';
import {
ControlValueAccessor,
FormBuilder,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
UntypedFormGroup,
ValidationErrors,
Validator,
Validators
} from '@angular/forms';
import {
BrokerConfig,
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/public-api';
import { Subject } from 'rxjs';
@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
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => BrokerConfigControlComponent),
multi: true
}
]
})
export class BrokerConfigControlComponent implements ControlValueAccessor, Validator, OnDestroy {
brokerConfigFormGroup: UntypedFormGroup;
mqttVersions = MqttVersions;
portLimits = PortLimits;
onChange!: (value: string) => void;
onTouched!: () => void;
private translate = inject(TranslateService);
private destroy$ = new Subject<void>();
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)]],
security: []
});
this.brokerConfigFormGroup.valueChanges.subscribe(value => {
this.onChange(value);
this.onTouched();
});
}
get portErrorTooltip(): string {
if (this.brokerConfigFormGroup.get('port').hasError('required')) {
return this.translate.instant('gateway.port-required');
} else if (
this.brokerConfigFormGroup.get('port').hasError('min') ||
this.brokerConfigFormGroup.get('port').hasError('max')
) {
return this.translate.instant('gateway.port-limits-error',
{min: PortLimits.MIN, max: PortLimits.MAX});
}
return '';
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
generate(formControlName: string): void {
this.brokerConfigFormGroup.get(formControlName)?.patchValue('tb_gw_' + generateSecret(5));
}
registerOnChange(fn: (value: string) => void): void {
this.onChange = fn;
}
registerOnTouched(fn: () => void): void {
this.onTouched = fn;
}
writeValue(brokerConfig: BrokerConfig): void {
this.brokerConfigFormGroup.patchValue(brokerConfig, {emitEvent: false});
}
validate(): ValidationErrors | null {
return this.brokerConfigFormGroup.valid ? null : {
brokerConfigFormGroup: {valid: false}
};
}
}

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

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

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

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

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

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

@ -0,0 +1,80 @@
<!--
Copyright © 2016-2024 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div class="tb-form-panel no-border no-padding padding-top" [formGroup]="parentFormGroup">
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required" translate>gateway.name</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput autocomplete="off" name="value" formControlName="name" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="(parentFormGroup.get('name').hasError('duplicateName') ?
'gateway.connector-duplicate-name' : 'gateway.name-required') | translate"
*ngIf="(parentFormGroup.get('name').hasError('required') && parentFormGroup.get('name').touched) ||
parentFormGroup.get('name').hasError('duplicateName')"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
</div>
<div *ngIf="parentFormGroup.get('type').value === connectorType.CUSTOM" class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" translate>gateway.connectors-table-class</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="class" placeholder="{{ 'gateway.set' | translate }}"/>
</mat-form-field>
</div>
</div>
<div *ngIf="parentFormGroup.get('type').value === connectorType.GRPC" class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" translate>gateway.connectors-table-key</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="key" placeholder="{{ 'gateway.set' | translate }}"/>
</mat-form-field>
</div>
</div>
<div class="tb-form-panel stroked">
<div class="tb-form-panel-title" translate>gateway.logs-configuration</div>
<div class="tb-form-row" fxLayoutAlign="space-between center">
<mat-slide-toggle class="mat-slide" formControlName="enableRemoteLogging">
<mat-label>
{{ 'gateway.enable-remote-logging' | translate }}
</mat-label>
</mat-slide-toggle>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" translate>gateway.remote-logging-level</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="logLevel">
<mat-option *ngFor="let logLevel of gatewayLogLevel" [value]="logLevel">{{ logLevel }}</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
</div>
<div *ngIf="parentFormGroup.get('type').value === connectorType.MQTT" class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<mat-slide-toggle class="mat-slide" formControlName="sendDataOnlyOnChange">
<mat-label tb-hint-tooltip-icon="{{ 'gateway.send-change-data-hint' | translate }}">
{{ 'gateway.send-change-data' | translate }}
</mat-label>
</mat-slide-toggle>
</div>
</div>

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

@ -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.
///
import {
ChangeDetectionStrategy,
Component,
inject,
} from '@angular/core';
import {
ControlContainer,
FormGroup,
} from '@angular/forms';
import {
ConnectorType,
GatewayLogLevel,
} from '@home/components/widget/lib/gateway/gateway-widget.models';
import { SharedModule } from '@shared/shared.module';
import { CommonModule } from '@angular/common';
@Component({
selector: 'tb-general-config',
templateUrl: './general-config.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [
CommonModule,
SharedModule,
],
})
export class GeneralConfigComponent {
gatewayLogLevel = Object.values(GatewayLogLevel);
connectorType = ConnectorType;
get parentFormGroup(): FormGroup {
return this.parentContainer.control as FormGroup;
}
private parentContainer = inject(ControlContainer);
}

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

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

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

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

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

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

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

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

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

@ -17,41 +17,49 @@
import {
AfterViewInit,
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
ElementRef,
forwardRef,
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 {
ControlValueAccessor,
FormArray,
FormBuilder,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
UntypedFormArray,
ValidationErrors,
Validator,
} from '@angular/forms';
import {
ConnectorMapping,
ConverterConnectorMapping,
ConvertorTypeTranslationsMap,
DeviceConnectorMapping,
MappingInfo,
MappingType,
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 { SharedModule } from '@shared/shared.module';
import { CommonModule } from '@angular/common';
@Component({
selector: 'tb-mapping-table',
@ -63,10 +71,28 @@ import { isDefinedAndNotNull, isUndefinedOrNull } from '@core/utils';
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => MappingTableComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => MappingTableComponent),
multi: true
}
]
],
standalone: true,
imports: [CommonModule, SharedModule]
})
export class MappingTableComponent extends PageComponent implements ControlValueAccessor, AfterViewInit, OnInit, OnDestroy {
export class MappingTableComponent implements ControlValueAccessor, Validator, AfterViewInit, OnInit, OnDestroy {
@coerceBoolean()
@Input() required = false;
@Input()
set mappingType(value: MappingType) {
if (this.mappingTypeValue !== value) {
this.mappingTypeValue = value;
}
}
@ViewChild('searchInput') searchInputField: ElementRef;
mappingTypeTranslationsMap = MappingTypeTranslationsMap;
mappingTypeEnum = MappingType;
@ -75,79 +101,48 @@ export class MappingTableComponent extends PageComponent implements ControlValue
textSearchMode = false;
dataSource: MappingDatasource;
hidePageSize = false;
activeValue = false;
dirtyValue = false;
viewsInited = false;
mappingTypeValue: MappingType;
mappingFormGroup: UntypedFormArray;
textSearch = this.fb.control('', {nonNullable: true});
get mappingType(): MappingType {
return this.mappingTypeValue;
}
@Input()
set mappingType(value: MappingType) {
if (this.mappingTypeValue !== value) {
this.mappingTypeValue = value;
}
}
@ViewChild('searchInput') searchInputField: ElementRef;
mappingFormGroup: UntypedFormArray;
textSearch = this.fb.control('', {nonNullable: true});
onChange: (value: string) => void = () => {};
onTouched: () => void = () => {};
private destroy$ = new Subject<void>();
private propagateChange = (v: any) => {};
constructor(protected store: Store<AppState>,
public translate: TranslateService,
constructor(public translate: TranslateService,
public dialog: MatDialog,
private overlay: Overlay,
private viewContainerRef: ViewContainerRef,
private dialogService: DialogService,
private entityService: EntityService,
private utils: UtilsService,
private zone: NgZone,
private cd: ChangeDetectorRef,
private elementRef: ElementRef,
private fb: FormBuilder) {
super(store);
this.mappingFormGroup = this.fb.array([]);
this.dirtyValue = !this.activeValue;
this.dataSource = new MappingDatasource();
}
ngOnInit() {
if (this.mappingType === MappingType.DATA) {
this.mappingColumns.push(
{def: 'topicFilter', title: 'gateway.topic-filter'},
{def: 'QoS', title: 'gateway.mqtt-qos'},
{def: 'converter', title: 'gateway.payload-type'}
)
} else {
this.mappingColumns.push(
{def: 'type', title: 'gateway.type'},
{def: 'details', title: 'gateway.details'}
);
}
ngOnInit(): void {
this.setMappingColumns();
this.displayedColumns.push(...this.mappingColumns.map(column => column.def), 'actions');
this.mappingFormGroup.valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe((value) => {
this.updateTableData(value);
this.updateView(value);
this.onChange(value);
this.onTouched();
});
}
ngOnDestroy() {
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
ngAfterViewInit() {
ngAfterViewInit(): void {
this.textSearch.valueChanges.pipe(
debounceTime(150),
distinctUntilChanged((prev, current) => (prev ?? '') === current.trim()),
@ -158,55 +153,26 @@ export class MappingTableComponent extends PageComponent implements ControlValue
});
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
registerOnChange(fn: (value: string) => void): void {
this.onChange = fn;
}
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);
registerOnTouched(fn: () => void): void {
this.onTouched = fn;
}
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;
}
writeValue(connectorMappings: ConnectorMapping[]): void {
(this.mappingFormGroup as FormArray).clear();
this.pushDataAsFormArrays(connectorMappings)
}
this.propagateChange(config);
validate(): ValidationErrors | null {
return !this.required || this.mappingFormGroup.controls.length ? null : {
mappingFormGroup: {valid: false}
};
}
enterFilterMode() {
enterFilterMode(): void {
this.textSearchMode = true;
setTimeout(() => {
this.searchInputField.nativeElement.focus();
@ -214,18 +180,18 @@ export class MappingTableComponent extends PageComponent implements ControlValue
}, 10);
}
exitFilterMode() {
exitFilterMode(): void {
this.updateTableData(this.mappingFormGroup.value);
this.textSearchMode = false;
this.textSearch.reset();
}
manageMapping($event: Event, index?: number) {
manageMapping($event: Event, index?: number): void {
if ($event) {
$event.stopPropagation();
}
const value = isDefinedAndNotNull(index) ? this.mappingFormGroup.at(index).value : {};
this.dialog.open<MappingDialogComponent, MappingInfo, boolean>(MappingDialogComponent, {
this.dialog.open<MappingDialogComponent, MappingInfo, MappingValue>(MappingDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
@ -233,45 +199,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 +226,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 +239,76 @@ export class MappingTableComponent extends PageComponent implements ControlValue
).subscribe((result) => {
if (result) {
this.mappingFormGroup.removeAt(index);
this.mappingFormGroup.markAsDirty();
}
});
}
private pushDataAsFormArrays(data: ConnectorMapping[]): void {
if (data?.length) {
data.forEach((mapping: ConnectorMapping) => this.mappingFormGroup.push(this.fb.control(mapping)));
}
}
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' }
);
}
}
}
export class MappingDatasource implements DataSource<{[key: string]: any}> {
@ -335,5 +345,4 @@ export class MappingDatasource implements DataSource<{[key: string]: any}> {
map((mappings) => mappings.length)
);
}
}

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

@ -0,0 +1,24 @@
<mat-tab-group [formGroup]="basicFormGroup">
<mat-tab label="{{ 'gateway.general' | translate }}">
<ng-container [ngTemplateOutlet]="generalTabContent"></ng-container>
</mat-tab>
<mat-tab label="{{ 'gateway.broker.connection' | translate }}*">
<tb-broker-config-control formControlName="broker"></tb-broker-config-control>
</mat-tab>
<mat-tab label="{{ 'gateway.data-mapping' | translate }}*">
<div class="tb-form-panel no-border no-padding padding-top tb-flex fill-height">
<tb-mapping-table formControlName="dataMapping" [required]="true" [mappingType]="mappingTypes.DATA"></tb-mapping-table>
</div>
</mat-tab>
<mat-tab label="{{ 'gateway.requests-mapping' | translate }}">
<div class="tb-form-panel no-border no-padding padding-top tb-flex fill-height">
<tb-mapping-table formControlName="requestsMapping" [mappingType]="mappingTypes.REQUESTS"></tb-mapping-table>
</div>
</mat-tab>
<mat-tab label="{{ 'gateway.workers-settings' | translate }}">
<div class="tb-form-panel no-border no-padding">
<tb-workers-config-control formControlName="workers"></tb-workers-config-control>
</div>
</mat-tab>
</mat-tab-group>

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

@ -0,0 +1,170 @@
///
/// Copyright © 2016-2024 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import {
ChangeDetectionStrategy,
Component,
forwardRef,
Input,
OnDestroy,
TemplateRef,
} from '@angular/core';
import {
ControlValueAccessor,
FormBuilder,
FormGroup,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
ValidationErrors,
Validator,
} from '@angular/forms';
import {
ConnectorBaseConfig,
ConnectorType,
MappingType,
RequestMappingData,
RequestType,
} from '@home/components/widget/lib/gateway/gateway-widget.models';
import { SharedModule } from '@shared/shared.module';
import { CommonModule } from '@angular/common';
import {
BrokerConfigControlComponent,
MappingTableComponent,
SecurityConfigComponent,
WorkersConfigControlComponent,
GeneralConfigComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/public-api';
import { takeUntil } from 'rxjs/operators';
import { Subject } from 'rxjs';
import { isObject } from 'lodash';
@Component({
selector: 'tb-mqtt-basic-config',
templateUrl: './mqtt-basic-config.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => MqttBasicConfigComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => MqttBasicConfigComponent),
multi: true
}
],
standalone: true,
imports: [
CommonModule,
SharedModule,
SecurityConfigComponent,
WorkersConfigControlComponent,
BrokerConfigControlComponent,
MappingTableComponent,
GeneralConfigComponent,
],
styles: [`
:host {
height: 100%;
}
:host ::ng-deep {
.mat-mdc-tab-group, .mat-mdc-tab-body-wrapper {
height: 100%;
}
}
`],
})
export class MqttBasicConfigComponent implements ControlValueAccessor, Validator, OnDestroy {
@Input() generalTabContent: TemplateRef<GeneralConfigComponent>;
mappingTypes = MappingType;
basicFormGroup: FormGroup;
onChange!: (value: string) => void;
onTouched!: () => void;
protected readonly connectorType = ConnectorType;
private destroy$ = new Subject<void>();
constructor(private fb: FormBuilder) {
this.basicFormGroup = this.fb.group({
dataMapping: [],
requestsMapping: [],
broker: [],
workers: [],
});
this.basicFormGroup.valueChanges
.pipe(takeUntil(this.destroy$))
.subscribe(value => {
this.onChange(value);
this.onTouched();
});
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
registerOnChange(fn: (value: string) => void): void {
this.onChange = fn;
}
registerOnTouched(fn: () => void): void {
this.onTouched = fn;
}
writeValue(basicConfig: ConnectorBaseConfig): void {
const editedBase = {
...basicConfig,
workers: {
maxNumberOfWorkers: basicConfig.broker?.maxNumberOfWorkers,
maxMessageNumberPerWorker: basicConfig.broker?.maxMessageNumberPerWorker,
},
requestsMapping: Array.isArray(basicConfig.requestsMapping)
? basicConfig.requestsMapping
: this.getRequestDataArray(basicConfig.requestsMapping),
};
this.basicFormGroup.patchValue(editedBase, {emitEvent: false});
}
validate(): ValidationErrors | null {
return this.basicFormGroup.valid ? null : {
basicFormGroup: {valid: false}
};
}
private getRequestDataArray(value: Record<RequestType, RequestMappingData>): RequestMappingData[] {
const mappingConfigs = [];
if (isObject(value)) {
Object.keys(value).forEach((configKey: string) => {
for (let mapping of value[configKey]) {
mappingConfigs.push({
requestType: configKey,
requestValue: mapping
});
}
});
}
return mappingConfigs;
}
}

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

@ -0,0 +1,13 @@
<mat-tab-group [formGroup]="basicFormGroup">
<mat-tab label="{{ 'gateway.general' | translate }}">
<ng-container [ngTemplateOutlet]="generalTabContent"></ng-container>
</mat-tab>
<mat-tab label="{{ 'gateway.server' | translate }}*">
<tb-server-config formControlName="server"></tb-server-config>
</mat-tab>
<mat-tab label="{{ 'gateway.data-mapping' | translate }}*">
<div class="tb-form-panel no-border no-padding padding-top tb-flex fill-height">
<tb-mapping-table formControlName="mapping" [required]="true" [mappingType]="mappingTypes.OPCUA"></tb-mapping-table>
</div>
</mat-tab>
</mat-tab-group>

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

@ -0,0 +1,139 @@
///
/// Copyright © 2016-2024 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import {
ChangeDetectionStrategy,
Component,
forwardRef,
Input,
OnDestroy,
TemplateRef,
} from '@angular/core';
import {
ControlValueAccessor,
FormBuilder,
FormGroup,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
ValidationErrors,
Validator,
} from '@angular/forms';
import {
ConnectorBaseConfig,
ConnectorType,
MappingType,
} from '@home/components/widget/lib/gateway/gateway-widget.models';
import { SharedModule } from '@shared/shared.module';
import { CommonModule } from '@angular/common';
import {
BrokerConfigControlComponent,
MappingTableComponent,
SecurityConfigComponent,
ServerConfigComponent,
WorkersConfigControlComponent,
GeneralConfigComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/public-api';
import { takeUntil } from 'rxjs/operators';
import { Subject } from 'rxjs';
@Component({
selector: 'tb-opc-ua-basic-config',
templateUrl: './opc-ua-basic-config.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => OpcUaBasicConfigComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => OpcUaBasicConfigComponent),
multi: true
}
],
standalone: true,
imports: [
CommonModule,
SharedModule,
SecurityConfigComponent,
WorkersConfigControlComponent,
BrokerConfigControlComponent,
MappingTableComponent,
GeneralConfigComponent,
ServerConfigComponent,
],
styles: [`
:host {
height: 100%;
}
:host ::ng-deep {
.mat-mdc-tab-group, .mat-mdc-tab-body-wrapper {
height: 100%;
}
}
`],
})
export class OpcUaBasicConfigComponent implements ControlValueAccessor, Validator, OnDestroy {
@Input() generalTabContent: TemplateRef<GeneralConfigComponent>;
mappingTypes = MappingType;
basicFormGroup: FormGroup;
onChange!: (value: string) => void;
onTouched!: () => void;
protected readonly connectorType = ConnectorType;
private destroy$ = new Subject<void>();
constructor(private fb: FormBuilder) {
this.basicFormGroup = this.fb.group({
mapping: [],
server: [],
});
this.basicFormGroup.valueChanges
.pipe(takeUntil(this.destroy$))
.subscribe(value => {
this.onChange(value);
this.onTouched();
});
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
registerOnChange(fn: (value: string) => void): void {
this.onChange = fn;
}
registerOnTouched(fn: () => void): void {
this.onTouched = fn;
}
writeValue(basicConfig: ConnectorBaseConfig): void {
this.basicFormGroup.patchValue(basicConfig, {emitEvent: false});
}
validate(): ValidationErrors | null {
return this.basicFormGroup.valid ? null : {
basicFormGroup: {valid: false}
};
}
}

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

@ -0,0 +1,27 @@
///
/// Copyright © 2016-2024 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
export * from './mapping-table/mapping-table.component';
export * from './device-info-table/device-info-table.component';
export * from './security-config/security-config.component';
export * from './server-config/server-config.component';
export * from './mapping-data-keys-panel/mapping-data-keys-panel.component';
export * from './type-value-panel/type-value-panel.component';
export * from './broker-config-control/broker-config-control.component';
export * from './workers-config-control/workers-config-control.component';
export * from './opc-ua-basic-config/opc-ua-basic-config.component';
export * from './mqtt-basic-config/mqtt-basic-config.component';
export * from './general-config/general-config.component';

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

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

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

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

@ -16,24 +16,13 @@
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,
@ -41,124 +30,143 @@ import {
NG_VALUE_ACCESSOR,
UntypedFormGroup,
ValidationErrors,
Validator,
Validators
} from '@angular/forms';
import {
BrokerSecurityType,
BrokerSecurityTypeTranslationsMap,
noLeadTrailSpacesRegex
SecurityType,
SecurityTypeTranslationsMap,
ModeType,
noLeadTrailSpacesRegex,
ConnectorSecurity
} 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),
useExisting: forwardRef(() => SecurityConfigComponent),
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';
BrokerSecurityType = BrokerSecurityType;
securityTypes = Object.values(BrokerSecurityType);
SecurityTypeTranslationsMap = BrokerSecurityTypeTranslationsMap;
@Input()
@coerceBoolean()
extendCertificatesModel = false;
BrokerSecurityType = SecurityType;
securityTypes = Object.values(SecurityType);
modeTypes = Object.values(ModeType);
SecurityTypeTranslationsMap = SecurityTypeTranslationsMap;
securityFormGroup: UntypedFormGroup;
onChange!: (value: string) => void;
onTouched!: () => void;
private destroy$ = new Subject<void>();
private propagateChange = (v: any) => {};
constructor(protected store: Store<AppState>,
public translate: TranslateService,
public dialog: MatDialog,
private overlay: Overlay,
private viewContainerRef: ViewContainerRef,
private dialogService: DialogService,
private entityService: EntityService,
private utils: UtilsService,
private zone: NgZone,
private cd: ChangeDetectorRef,
private elementRef: ElementRef,
private fb: FormBuilder) {
super(store);
constructor(private fb: FormBuilder) {}
ngOnInit(): void {
this.securityFormGroup = this.fb.group({
type: [BrokerSecurityType.ANONYMOUS, []],
type: [SecurityType.ANONYMOUS, []],
username: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
password: ['', [Validators.pattern(noLeadTrailSpacesRegex)]],
pathToCACert: ['', [Validators.pattern(noLeadTrailSpacesRegex)]],
pathToPrivateKey: ['', [Validators.pattern(noLeadTrailSpacesRegex)]],
pathToClientCert: ['', [Validators.pattern(noLeadTrailSpacesRegex)]]
});
if (this.extendCertificatesModel) {
this.securityFormGroup.addControl('mode', this.fb.control(ModeType.NONE, []));
}
this.securityFormGroup.valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe((value) => {
this.updateView(value);
this.onChange(value);
this.onTouched();
});
this.securityFormGroup.get('type').valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe((type) => {
this.updateValidators(type);
});
).subscribe((type) => this.updateValidators(type));
}
ngOnDestroy() {
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
super.ngOnDestroy();
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {}
writeValue(deviceInfo: any) {
if (!deviceInfo.type) {
deviceInfo.type = BrokerSecurityType.ANONYMOUS;
writeValue(securityInfo: ConnectorSecurity): void {
if (!securityInfo) {
const defaultSecurity = {type: SecurityType.ANONYMOUS};
this.securityFormGroup.reset(defaultSecurity, {emitEvent: false});
} else {
if (!securityInfo.type) {
securityInfo.type = SecurityType.ANONYMOUS;
}
this.securityFormGroup.reset(securityInfo, {emitEvent: false});
}
this.securityFormGroup.reset(deviceInfo);
this.updateView(deviceInfo);
}
validate(): ValidationErrors | null {
return this.securityFormGroup.valid ? null : {
return this.securityFormGroup.get('type').value !== SecurityType.BASIC || this.securityFormGroup.valid ? null : {
securityForm: { valid: false }
};
}
updateView(value: any) {
this.propagateChange(value);
registerOnChange(fn: (value: string) => void): void {
this.onChange = fn;
}
private updateValidators(type) {
registerOnTouched(fn: () => void): void {
this.onTouched = fn;
}
private updateValidators(type: SecurityType): 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});
if (type === BrokerSecurityType.BASIC) {
this.securityFormGroup.get('mode')?.disable({emitEvent: false});
if (type === SecurityType.BASIC) {
this.securityFormGroup.get('username').enable({emitEvent: false});
this.securityFormGroup.get('password').enable({emitEvent: false});
} else if (type === BrokerSecurityType.CERTIFICATES) {
} else if (type === SecurityType.CERTIFICATES) {
this.securityFormGroup.get('pathToCACert').enable({emitEvent: false});
this.securityFormGroup.get('pathToPrivateKey').enable({emitEvent: false});
this.securityFormGroup.get('pathToClientCert').enable({emitEvent: false});
if (this.extendCertificatesModel) {
const modeControl = this.securityFormGroup.get('mode');
if (modeControl && !modeControl.value) {
modeControl.setValue(ModeType.NONE, {emitEvent: false});
}
modeControl?.enable({emitEvent: false});
this.securityFormGroup.get('username').enable({emitEvent: false});
this.securityFormGroup.get('password').enable({emitEvent: false});
}
}
}
}

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

@ -0,0 +1,125 @@
<!--
Copyright © 2016-2024 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div class="tb-form-panel no-border no-padding padding-top" [formGroup]="serverConfigFormGroup">
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" translate>gateway.server-url</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="url" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.server-url-required') | translate"
*ngIf="serverConfigFormGroup.get('url').hasError('required') &&
serverConfigFormGroup.get('url').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" tb-hint-tooltip-icon="{{ 'gateway.hints.opcua-timeout' | translate }}" translate>
gateway.timeout
</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput type="number" min="0" name="value" formControlName="timeoutInMillis" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="'gateway.timeout-error' | translate: {min: 1000}"
*ngIf="(serverConfigFormGroup.get('timeoutInMillis').hasError('required') ||
serverConfigFormGroup.get('timeoutInMillis').hasError('min')) &&
serverConfigFormGroup.get('timeoutInMillis').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" translate>gateway.security</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="security">
<mat-option *ngFor="let version of serverSecurityTypes" [value]="version.value">{{ version.name }}</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" tb-hint-tooltip-icon="{{ 'gateway.hints.scan-period' | translate }}" translate>
gateway.scan-period
</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput type="number" min="0" name="value"
formControlName="scanPeriodInMillis" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="'gateway.scan-period-error' | translate: {min: 1000}"
*ngIf="(serverConfigFormGroup.get('scanPeriodInMillis').hasError('required') ||
serverConfigFormGroup.get('scanPeriodInMillis').hasError('min')) &&
serverConfigFormGroup.get('scanPeriodInMillis').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" tb-hint-tooltip-icon="{{ 'gateway.hints.sub-check-period' | translate }}" translate>
gateway.sub-check-period
</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput type="number" min="0" name="value"
formControlName="subCheckPeriodInMillis" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="'gateway.sub-check-period-error' | translate: {min: 10}"
*ngIf="(serverConfigFormGroup.get('subCheckPeriodInMillis').hasError('required') ||
serverConfigFormGroup.get('subCheckPeriodInMillis').hasError('min')) &&
serverConfigFormGroup.get('subCheckPeriodInMillis').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
</div>
<div class="tb-form-row" fxLayoutAlign="space-between center">
<mat-slide-toggle class="mat-slide" formControlName="enableSubscriptions">
<mat-label tb-hint-tooltip-icon="{{ 'gateway.hints.enable-subscription' | translate }}">
{{ 'gateway.enable-subscription' | translate }}
</mat-label>
</mat-slide-toggle>
</div>
<div class="tb-form-row" fxLayoutAlign="space-between center">
<mat-slide-toggle class="mat-slide" formControlName="showMap">
<mat-label tb-hint-tooltip-icon="{{ 'gateway.hints.show-map' | translate }}">
{{ 'gateway.show-map' | translate }}
</mat-label>
</mat-slide-toggle>
</div>
<tb-security-config [formControl]="serverConfigFormGroup.get('identity')"
[extendCertificatesModel]="true">
</tb-security-config>
</div>

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

@ -0,0 +1,20 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
:host {
width: 100%;
height: 100%;
display: block;
}

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

@ -0,0 +1,121 @@
///
/// Copyright © 2016-2024 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import {
ChangeDetectionStrategy,
Component,
forwardRef,
OnDestroy
} from '@angular/core';
import {
ControlValueAccessor,
FormBuilder,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
UntypedFormGroup,
ValidationErrors,
Validator,
Validators
} from '@angular/forms';
import {
noLeadTrailSpacesRegex,
SecurityType,
ServerConfig,
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/public-api';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
@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
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => ServerConfigComponent),
multi: true
}
],
standalone: true,
imports: [
CommonModule,
SharedModule,
SecurityConfigComponent,
]
})
export class ServerConfigComponent implements ControlValueAccessor, Validator, OnDestroy {
serverSecurityTypes = ServerSecurityTypes;
serverConfigFormGroup: UntypedFormGroup;
onChange!: (value: string) => void;
onTouched!: () => void;
private destroy$ = new Subject<void>();
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]]
});
this.serverConfigFormGroup.valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe((value) => {
this.onChange(value);
this.onTouched();
});
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
registerOnChange(fn: (value: string) => void): void {
this.onChange = fn;
}
registerOnTouched(fn: () => void): void {
this.onTouched = fn;
}
validate(): ValidationErrors | null {
return this.serverConfigFormGroup.valid ? null : {
serverConfigFormGroup: { valid: false }
};
}
writeValue(serverConfig: ServerConfig): void {
this.serverConfigFormGroup.patchValue(serverConfig, {emitEvent: false});
}
}

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

@ -0,0 +1,92 @@
<!--
Copyright © 2016-2024 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div class="tb-form-panel no-border no-padding">
<div class="tb-form-panel no-border no-padding key-panel" *ngIf="valueListFormArray.controls.length; else noKeys">
<div class="tb-form-panel no-border no-padding tb-flex no-flex row center fill-width"
*ngFor="let keyControl of valueListFormArray.controls; trackBy: trackByKey; let $index = index; let last = last;">
<div class="tb-form-panel stroked tb-flex">
<ng-container [formGroup]="keyControl">
<mat-expansion-panel class="tb-settings" [expanded]="last">
<mat-expansion-panel-header fxLayout="row wrap">
<mat-panel-title>
<div class="title-container">{{ valueTitle(keyControl.get('value').value) }}</div>
</mat-panel-title>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required" translate>gateway.type</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap fill-width" appearance="outline" subscriptSizing="dynamic">
<mat-select name="valueType" formControlName="type">
<mat-select-trigger>
<div class="tb-flex align-center">
<mat-icon class="tb-mat-18" [svgIcon]="valueTypes.get(keyControl.get('type').value)?.icon">
</mat-icon>
<span>
{{ valueTypes.get(keyControl.get('type').value)?.name | translate}}
</span>
</div>
</mat-select-trigger>
<mat-option *ngFor="let valueType of valueTypeKeys" [value]="valueType">
<mat-icon class="tb-mat-20" svgIcon="{{ valueTypes.get(valueType).icon }}">
</mat-icon>
<span>{{ valueTypes.get(valueType).name | translate }}</span>
</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required" translate>gateway.value</div>
<mat-form-field fxFlex appearance="outline" subscriptSizing="dynamic" class="tb-inline-field flex tb-suffix-absolute">
<input matInput required formControlName="value"
placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.value-required') | translate"
*ngIf="keyControl.get('value').hasError('required') && keyControl.get('value').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
</ng-template>
</mat-expansion-panel>
</ng-container>
</div>
<button type="button"
mat-icon-button
(click)="deleteKey($event, $index)"
[matTooltip]="'gateway.delete-value' | translate"
matTooltipPosition="above">
<mat-icon>delete</mat-icon>
</button>
</div>
</div>
<div>
<button type="button" mat-stroked-button color="primary" (click)="addKey()">
{{ 'gateway.add-value' | translate }}
</button>
</div>
</div>
<ng-template #noKeys>
<div class="tb-flex no-flex center align-center key-panel">
<span class="tb-prompt" translate>{{ 'gateway.no-value' }}</span>
</div>
</ng-template>

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

@ -0,0 +1,52 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
:host {
.title-container {
max-width: 11vw;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap
}
.key-panel {
height: 250px;
overflow: auto;
}
.tb-form-panel {
.mat-mdc-icon-button {
width: 56px;
height: 56px;
padding: 16px;
color: rgba(0, 0, 0, 0.54);
}
}
.see-example {
width: 32px;
height: 32px;
margin: 4px;
}
}
:host ::ng-deep {
.mat-mdc-form-field-icon-suffix {
display: flex;
}
}

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

@ -0,0 +1,144 @@
///
/// Copyright © 2016-2024 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import {
Component,
forwardRef,
OnDestroy,
OnInit,
} from '@angular/core';
import {
AbstractControl,
ControlValueAccessor,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
UntypedFormArray,
UntypedFormBuilder,
ValidationErrors,
Validator,
Validators
} from '@angular/forms';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { isDefinedAndNotNull } from '@core/utils';
import {
MappingDataKey,
MappingValueType,
mappingValueTypesMap,
noLeadTrailSpacesRegex
} from '@home/components/widget/lib/gateway/gateway-widget.models';
import { takeUntil } from 'rxjs/operators';
import { Subject } from 'rxjs';
@Component({
selector: 'tb-type-value-panel',
templateUrl: './type-value-panel.component.html',
styleUrls: ['./type-value-panel.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => TypeValuePanelComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => TypeValuePanelComponent),
multi: true
}
]
})
export class TypeValuePanelComponent implements ControlValueAccessor, Validator, OnInit, OnDestroy {
valueTypeKeys = Object.values(MappingValueType);
valueTypeEnum = MappingValueType;
valueTypes = mappingValueTypesMap;
dataKeyType: DataKeyType;
valueListFormArray: UntypedFormArray;
errorText = '';
private destroy$ = new Subject<void>();
private propagateChange = (v: any) => {};
constructor(private fb: UntypedFormBuilder) {}
ngOnInit(): void {
this.valueListFormArray = this.fb.array([]);
this.valueListFormArray.valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe((value) => {
this.updateView(value);
});
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
trackByKey(_: number, keyControl: AbstractControl): any {
return keyControl;
}
addKey(): void {
const dataKeyFormGroup = this.fb.group({
type: [MappingValueType.STRING, []],
value: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]]
});
this.valueListFormArray.push(dataKeyFormGroup);
}
deleteKey($event: Event, index: number): void {
if ($event) {
$event.stopPropagation();
}
this.valueListFormArray.removeAt(index);
this.valueListFormArray.markAsDirty();
}
valueTitle(value: any): string {
if (isDefinedAndNotNull(value)) {
if (typeof value === 'object') {
return JSON.stringify(value);
}
return value;
}
return '';
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {}
writeValue(deviceInfoArray: Array<MappingDataKey>): void {
for (const deviceInfo of deviceInfoArray) {
const dataKeyFormGroup = this.fb.group({
type: [deviceInfo.type, []],
value: [deviceInfo.value, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]]
});
this.valueListFormArray.push(dataKeyFormGroup);
}
}
validate(): ValidationErrors | null {
return this.valueListFormArray.valid ? null : {
valueListForm: { valid: false }
};
}
updateView(value: any): void {
this.propagateChange(value);
}
}

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

@ -0,0 +1,63 @@
<!--
Copyright © 2016-2024 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div class="tb-form-panel no-border no-padding padding-top" [formGroup]="workersConfigFormGroup">
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required" [style.width.%]="50"
tb-hint-tooltip-icon="{{ 'gateway.max-number-of-workers-hint' | translate }}" translate>
gateway.max-number-of-workers
</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" type="number" min="1" formControlName="maxNumberOfWorkers"
placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.max-number-of-workers-required') | translate"
*ngIf="workersConfigFormGroup.get('maxNumberOfWorkers').hasError('min') ||
(workersConfigFormGroup.get('maxNumberOfWorkers').hasError('required') &&
workersConfigFormGroup.get('maxNumberOfWorkers').touched)"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required" [style.width.%]="50"
tb-hint-tooltip-icon="{{ 'gateway.max-messages-queue-for-worker-hint' | translate }}" translate>
gateway.max-messages-queue-for-worker
</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" type="number" min="1" formControlName="maxMessageNumberPerWorker"
placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.max-messages-queue-for-worker-required') | translate"
*ngIf="workersConfigFormGroup.get('maxMessageNumberPerWorker').hasError('min') ||
(workersConfigFormGroup.get('maxMessageNumberPerWorker').hasError('required') &&
workersConfigFormGroup.get('maxMessageNumberPerWorker').touched)"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
</div>
</div>

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

@ -0,0 +1,101 @@
///
/// Copyright © 2016-2024 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import {
ChangeDetectionStrategy,
Component,
forwardRef,
OnDestroy,
} from '@angular/core';
import {
ControlValueAccessor,
FormBuilder,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
UntypedFormGroup, ValidationErrors, Validator,
Validators
} from '@angular/forms';
import { SharedModule } from '@shared/shared.module';
import { CommonModule } from '@angular/common';
import { WorkersConfig } from '@home/components/widget/lib/gateway/gateway-widget.models';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
@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
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => WorkersConfigControlComponent),
multi: true
}
]
})
export class WorkersConfigControlComponent implements OnDestroy, ControlValueAccessor, Validator {
workersConfigFormGroup: UntypedFormGroup;
onChange!: (value: string) => void;
onTouched!: () => void;
private destroy$ = new Subject<void>();
constructor(private fb: FormBuilder) {
this.workersConfigFormGroup = this.fb.group({
maxNumberOfWorkers: [100, [Validators.required, Validators.min(1)]],
maxMessageNumberPerWorker: [10, [Validators.required, Validators.min(1)]],
});
this.workersConfigFormGroup.valueChanges.pipe(takeUntil(this.destroy$)).subscribe(value => {
this.onChange(value);
this.onTouched();
});
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
registerOnChange(fn: (value: string) => void): void {
this.onChange = fn;
}
registerOnTouched(fn: () => void): void {
this.onTouched = fn;
}
writeValue(workersConfig: WorkersConfig): void {
this.workersConfigFormGroup.patchValue(workersConfig, {emitEvent: false});
}
validate(): ValidationErrors | null {
return this.workersConfigFormGroup.valid ? null : {
workersConfigFormGroup: {valid: false}
};
}
}

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

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

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

@ -72,4 +72,8 @@
.mat-mdc-form-field-icon-suffix {
display: flex;
}
.device-config {
padding-left: 10px;
}
}

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

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

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

@ -156,7 +156,10 @@
{{ initialConnector?.type ? gatewayConnectorDefaultTypes.get(initialConnector.type) : '' }}
{{ 'gateway.configuration' | translate }}
</div>
<tb-toggle-select *ngIf="initialConnector && initialConnector.type === connectorType.MQTT" formControlName="mode" appearance="fill">
<tb-toggle-select *ngIf="initialConnector &&
(initialConnector.type === connectorType.MQTT ||
initialConnector.type === connectorType.OPCUA)"
formControlName="mode" appearance="fill">
<tb-toggle-option [value]="connectorConfigurationModes.BASIC">
{{ 'gateway.basic' | translate }}
</tb-toggle-option>
@ -170,213 +173,25 @@
class="no-data-found" translate>
gateway.select-connector
</span>
<ng-template #generalTabContent>
<tb-general-config></tb-general-config>
</ng-template>
<section class="tb-form-panel section-container no-border no-padding tb-flex space-between" *ngIf="initialConnector">
<mat-tab-group>
<mat-tab label="{{ 'gateway.general' | translate }}">
<div class="tb-form-panel no-border no-padding padding-top">
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required" translate>gateway.name</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput autocomplete="off" name="value" formControlName="name" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="(connectorForm.get('name').hasError('duplicateName') ?
'gateway.connector-duplicate-name' : 'gateway.name-required') | translate"
*ngIf="(connectorForm.get('name').hasError('required') && connectorForm.get('name').touched) ||
connectorForm.get('name').hasError('duplicateName')"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
</div>
<div *ngIf="connectorForm.get('type').value === connectorType.CUSTOM" class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" translate>gateway.connectors-table-class</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="class" placeholder="{{ 'gateway.set' | translate }}"/>
</mat-form-field>
</div>
</div>
<div *ngIf="connectorForm.get('type').value === connectorType.GRPC" class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" translate>gateway.connectors-table-key</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="key" placeholder="{{ 'gateway.set' | translate }}"/>
</mat-form-field>
</div>
</div>
<div class="tb-form-panel stroked">
<div class="tb-form-panel-title" translate>gateway.logs-configuration</div>
<div class="tb-form-row" fxLayoutAlign="space-between center">
<mat-slide-toggle class="mat-slide" formControlName="enableRemoteLogging">
<mat-label>
{{ 'gateway.enable-remote-logging' | translate }}
</mat-label>
</mat-slide-toggle>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" translate>gateway.remote-logging-level</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="logLevel">
<mat-option *ngFor="let logLevel of gatewayLogLevel" [value]="logLevel">{{ logLevel }}</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
</div>
<div *ngIf="connectorForm.get('type').value === connectorType.MQTT" class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<mat-slide-toggle class="mat-slide" formControlName="sendDataOnlyOnChange">
<mat-label tb-hint-tooltip-icon="{{ 'gateway.send-change-data-hint' | translate }}">
{{ 'gateway.send-change-data' | translate }}
</mat-label>
</mat-slide-toggle>
</div>
</div>
</mat-tab>
<ng-container *ngIf="connectorForm.get('type').value === connectorType.MQTT &&
connectorForm.get('mode').value === connectorConfigurationModes.BASIC; else defaultConfig"
formGroupName="basicConfig">
<mat-tab label="{{ 'gateway.broker.connection' | translate }}*">
<ng-container formGroupName="broker">
<div class="tb-form-panel no-border no-padding padding-top">
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required" translate>gateway.host</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="host" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.host-required') | translate"
*ngIf="connectorForm.get('basicConfig.broker.host').hasError('required')
&& connectorForm.get('basicConfig.broker.host').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required" translate>gateway.port</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput type="number" min="{{portLimits.MIN}}" max="{{portLimits.MAX}}"
name="value" formControlName="port" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="portErrorTooltip"
*ngIf="(connectorForm.get('basicConfig.broker.port').hasError('required') ||
connectorForm.get('basicConfig.broker.port').hasError('min') ||
connectorForm.get('basicConfig.broker.port').hasError('max')) &&
connectorForm.get('basicConfig.broker.port').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" translate>gateway.mqtt-version</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="version">
<mat-option *ngFor="let version of mqttVersions" [value]="version.value">{{ version.name }}</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" translate>gateway.client-id</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="clientId" placeholder="{{ 'gateway.set' | translate }}"/>
<button type="button"
matSuffix
mat-icon-button
aria-label="Generate"
matTooltip="{{ 'gateway.generate-client-id' | translate }}"
matTooltipPosition="above"
(click)="generate('basicConfig.broker.clientId')"
*ngIf="!connectorForm.get('basicConfig.broker.clientId').value">
<mat-icon>autorenew</mat-icon>
</button>
</mat-form-field>
</div>
</div>
<tb-broker-security [formControl]="connectorForm.get('basicConfig.broker.security')">
</tb-broker-security>
</div>
</ng-container>
<ng-container *ngIf="connectorForm.get('mode')?.value === connectorConfigurationModes.BASIC else defaultConfig">
<tb-mqtt-basic-config *ngIf="(initialConnector.type === connectorType.MQTT)"
formControlName="basicConfig"
[generalTabContent]="generalTabContent">
</tb-mqtt-basic-config>
<tb-opc-ua-basic-config *ngIf="(initialConnector.type === connectorType.OPCUA)"
formControlName="basicConfig"
[generalTabContent]="generalTabContent">
</tb-opc-ua-basic-config>
</ng-container>
<ng-template #defaultConfig>
<mat-tab-group>
<mat-tab label="{{ 'gateway.general' | translate }}">
<ng-container [ngTemplateOutlet]="generalTabContent"></ng-container>
</mat-tab>
<mat-tab label="{{ 'gateway.data-mapping' | translate }}*">
<div class="tb-form-panel no-border no-padding padding-top tb-flex fill-height">
<tb-mapping-table formControlName="dataMapping" [mappingType]="mappingTypes.DATA"></tb-mapping-table>
</div>
</mat-tab>
<mat-tab label="{{ 'gateway.requests-mapping' | translate }}">
<div class="tb-form-panel no-border no-padding padding-top tb-flex fill-height">
<tb-mapping-table formControlName="requestsMapping" [mappingType]="mappingTypes.REQUESTS"></tb-mapping-table>
</div>
</mat-tab>
<mat-tab label="{{ 'gateway.workers-settings' | translate }}">
<div class="tb-form-panel no-border no-padding">
<ng-container formGroupName="broker">
<div class="tb-form-panel no-border no-padding padding-top">
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required" [style.width.%]="50"
tb-hint-tooltip-icon="{{ 'gateway.max-number-of-workers-hint' | translate }}" translate>
gateway.max-number-of-workers
</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" type="number" min="1" formControlName="maxNumberOfWorkers"
placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.max-number-of-workers-required') | translate"
*ngIf="connectorForm.get('basicConfig.broker.maxNumberOfWorkers').hasError('min') ||
(connectorForm.get('basicConfig.broker.maxNumberOfWorkers').hasError('required') &&
connectorForm.get('basicConfig.broker.maxNumberOfWorkers').touched)"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required" [style.width.%]="50"
tb-hint-tooltip-icon="{{ 'gateway.max-messages-queue-for-worker-hint' | translate }}" translate>
gateway.max-messages-queue-for-worker
</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" type="number" min="1" formControlName="maxMessageNumberPerWorker"
placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.max-messages-queue-for-worker-required') | translate"
*ngIf="connectorForm.get('basicConfig.broker.maxMessageNumberPerWorker').hasError('min') ||
(connectorForm.get('basicConfig.broker.maxMessageNumberPerWorker').hasError('required') &&
connectorForm.get('basicConfig.broker.maxMessageNumberPerWorker').touched)"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
</div>
</div>
</ng-container>
</div>
</mat-tab>
</ng-container>
<ng-template #defaultConfig>
<mat-tab label="{{ 'gateway.configuration' | translate }}*">
<tb-json-object-edit
fillHeight="true"
@ -387,8 +202,8 @@
formControlName="configurationJson">
</tb-json-object-edit>
</mat-tab>
</ng-template>
</mat-tab-group>
</mat-tab-group>
</ng-template>
<div fxLayoutAlign="end center">
<button mat-raised-button color="primary"
type="button"

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

@ -14,7 +14,17 @@
/// 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 {
@ -30,7 +40,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';
@ -55,16 +65,14 @@ import {
GatewayConnector,
GatewayConnectorDefaultTypesTranslatesMap,
GatewayLogLevel,
MappingType,
MqttVersions,
noLeadTrailSpacesRegex,
PortLimits
} from './gateway-widget.models';
import { MatDialog } from '@angular/material/dialog';
import { AddConnectorDialogComponent } from '@home/components/widget/lib/gateway/dialog/add-connector-dialog.component';
import { debounceTime, take, takeUntil, tap } from 'rxjs/operators';
import { debounceTime, 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 {
@ -97,8 +105,6 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
displayedColumns = ['enabled', 'key', 'type', 'syncStatus', 'errors', 'actions'];
mqttVersions = MqttVersions;
gatewayConnectorDefaultTypes = GatewayConnectorDefaultTypesTranslatesMap;
connectorConfigurationModes = ConnectorConfigurationModes;
@ -109,12 +115,6 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
activeConnectors: Array<string>;
gatewayLogLevel = Object.values(GatewayLogLevel);
mappingTypes = MappingType;
portLimits = PortLimits;
mode: ConnectorConfigurationModes = this.connectorConfigurationModes.BASIC;
initialConnector: GatewayConnector;
@ -178,27 +178,14 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
class: [''],
configuration: [''],
configurationJson: [{}, [Validators.required]],
basicConfig: this.fb.group({})
basicConfig: [{}]
});
this.connectorForm.disable();
this.observeAttributeChange();
}
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 => {
@ -232,7 +219,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});
@ -285,35 +272,14 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
}
}
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) {
@ -374,6 +340,20 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
});
}
private getMappedMQTTValue(): GatewayConnector {
const value = this.connectorForm.value;
return {
...value,
configurationJson: {
...value.configurationJson,
broker: {
...value.configurationJson.broker,
...value.configurationJson.workers,
}
}
}
}
private updateData(reload: boolean = false): void {
this.pageLink.sortOrder.property = this.sort.active;
this.pageLink.sortOrder.direction = Direction[this.sort.direction.toUpperCase()];
@ -576,12 +556,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();
}
@ -594,6 +569,27 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
this.connectorForm.get(formControlName)?.patchValue('tb_gw_' + generateSecret(5));
}
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 observeAttributeChange(): void {
this.attributeUpdateSubject.pipe(
debounceTime(300),
@ -678,29 +674,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();
@ -713,7 +686,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 };
@ -752,14 +725,21 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
this.initialConnector = connector;
if (connector.type === ConnectorType.MQTT) {
this.addMQTTConfigControls();
} else {
this.connectorForm.setControl('basicConfig', this.fb.group({}), {emitEvent: false});
}
this.updateConnector(connector);
}
this.connectorForm.patchValue(connector, {emitEvent: false});
this.connectorForm.markAsPristine();
private updateConnector(connector: GatewayConnector): void {
switch (connector.type) {
case ConnectorType.MQTT:
case ConnectorType.OPCUA:
this.connectorForm.patchValue({...connector, mode: connector.mode || ConnectorConfigurationModes.BASIC});
this.createBasicConfigWatcher();
this.connectorForm.markAsPristine();
break;
default:
this.connectorForm.patchValue({...connector, mode: null});
this.connectorForm.markAsPristine();
}
}
private setClientData(data: PageData<AttributeData>): void {

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

@ -17,7 +17,6 @@
import { ResourcesService } from '@core/services/resources.service';
import { Observable } from 'rxjs';
import { ValueTypeData } from '@shared/models/constants';
import { Validators } from '@angular/forms';
export const noLeadTrailSpacesRegex: RegExp = /^(?! )[\S\s]*(?<! )$/;
@ -113,11 +112,141 @@ export interface GatewayConnector {
name: string;
type: ConnectorType;
configuration?: string;
configurationJson: string | {[key: string]: any};
basicConfig?: string | {[key: string]: any};
configurationJson: ConnectorBaseConfig;
basicConfig?: ConnectorBaseConfig;
logLevel: string;
key?: string;
class?: string;
mode?: ConnectorConfigurationModes;
}
export interface DataMapping {
topicFilter: string;
QoS: string;
converter: Converter;
}
export interface RequestsMapping {
requestType: RequestType;
type: string;
details: string;
}
export interface OpcUaMapping {
deviceNodePattern?: string;
deviceNamePattern?: string;
deviceProfileExpression?: string;
}
export type MappingValue = DataMapping | RequestsMapping | OpcUaMapping;
export interface ServerConfig {
name: string;
url: string;
timeoutInMillis: number;
scanPeriodInMillis: number;
enableSubscriptions: boolean;
subCheckPeriodInMillis: number;
showMap: boolean;
security: string;
identity: ConnectorSecurity;
}
export interface BrokerConfig {
name: string;
host: string;
port: number;
version: number;
clientId: string;
maxNumberOfWorkers: number;
maxMessageNumberPerWorker: number;
security: ConnectorSecurity;
}
export interface ConnectorSecurity {
type: SecurityType;
username?: string;
password?: string;
pathToCACert?: string;
pathToPrivateKey?: string;
pathToClientCert?: string;
}
export type ConnectorMapping = DeviceConnectorMapping | RequestMappingData | ConverterConnectorMapping;
export interface ConnectorBaseConfig {
mapping?: DeviceConnectorMapping[];
dataMapping?: ConverterConnectorMapping[];
requestsMapping?: Record<RequestType, RequestMappingData> | RequestMappingData[];
server?: ServerConfig;
broker?: BrokerConfig;
workers?: WorkersConfig;
}
export interface WorkersConfig {
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 {
@ -333,6 +462,12 @@ export interface MappingDataKey {
value: any,
type: MappingValueType
}
export interface RpcMethodsMapping {
method: string,
arguments: Array<MappingDataKey>
}
export interface MappingInfo {
mappingType: MappingType,
value: {[key: string]: any},
@ -344,17 +479,23 @@ export enum ConnectorConfigurationModes {
ADVANCED = 'advanced'
}
export enum BrokerSecurityType {
export enum SecurityType {
ANONYMOUS = 'anonymous',
BASIC = 'basic',
CERTIFICATES = 'certificates'
}
export const BrokerSecurityTypeTranslationsMap = new Map<BrokerSecurityType, string>(
export enum ModeType {
NONE = 'None',
SIGN = 'Sign',
SIGNANDENCRYPT = 'SignAndEncrypt'
}
export const SecurityTypeTranslationsMap = new Map<SecurityType, string>(
[
[BrokerSecurityType.ANONYMOUS, 'gateway.broker.security-types.anonymous'],
[BrokerSecurityType.BASIC, 'gateway.broker.security-types.basic'],
[BrokerSecurityType.CERTIFICATES, 'gateway.broker.security-types.certificates']
[SecurityType.ANONYMOUS, 'gateway.broker.security-types.anonymous'],
[SecurityType.BASIC, 'gateway.broker.security-types.basic'],
[SecurityType.CERTIFICATES, 'gateway.broker.security-types.certificates']
]
);
@ -378,19 +519,22 @@ export const MqttVersions = [
export enum MappingType {
DATA = 'data',
REQUESTS = 'requests'
REQUESTS = 'requests',
OPCUA = 'OPCua'
}
export const MappingTypeTranslationsMap = new Map<MappingType, string>(
[
[MappingType.DATA, 'gateway.data-mapping'],
[MappingType.REQUESTS, 'gateway.requests-mapping']
[MappingType.REQUESTS, 'gateway.requests-mapping'],
[MappingType.OPCUA, 'gateway.data-mapping']
]
);
export const MappingHintTranslationsMap = new Map<MappingType, string>(
[
[MappingType.DATA, 'gateway.data-mapping-hint'],
[MappingType.OPCUA, 'gateway.opcua-data-mapping-hint'],
[MappingType.REQUESTS, 'gateway.requests-mapping-hint']
]
);
@ -425,19 +569,42 @@ export enum SourceTypes {
CONST = 'constant'
}
export enum OPCUaSourceTypes {
PATH = 'path',
IDENTIFIER = 'identifier',
CONST = 'constant'
}
export enum DeviceInfoType {
FULL = 'full',
PARTIAL = 'partial'
}
export const SourceTypeTranslationsMap = new Map<SourceTypes, string>(
export const SourceTypeTranslationsMap = new Map<SourceTypes | OPCUaSourceTypes, string>(
[
[SourceTypes.MSG, 'gateway.source-type.msg'],
[SourceTypes.TOPIC, 'gateway.source-type.topic'],
[SourceTypes.CONST, 'gateway.source-type.const'],
[OPCUaSourceTypes.PATH, 'gateway.source-type.path'],
[OPCUaSourceTypes.IDENTIFIER, 'gateway.source-type.identifier'],
[OPCUaSourceTypes.CONST, 'gateway.source-type.const']
]
);
export interface RequestMappingData {
requestType: RequestType;
requestValue: RequestDataItem;
}
export interface RequestDataItem {
type: string;
details: string;
requestType: RequestType;
methodFilter?: string;
attributeFilter?: string;
topicFilter?: string;
}
export enum RequestType {
CONNECT_REQUEST = 'connectRequests',
DISCONNECT_REQUEST = 'disconnectRequests',
@ -459,14 +626,18 @@ export const RequestTypesTranslationsMap = new Map<RequestType, string>(
export enum MappingKeysType {
ATTRIBUTES = 'attributes',
TIMESERIES = 'timeseries',
CUSTOM = 'extensionConfig'
CUSTOM = 'extensionConfig',
RPC_METHODS = 'rpc_methods',
ATTRIBUTES_UPDATES = 'attributes_updates'
}
export const MappingKeysPanelTitleTranslationsMap = new Map<MappingKeysType, string>(
[
[MappingKeysType.ATTRIBUTES, 'gateway.attributes'],
[MappingKeysType.TIMESERIES, 'gateway.timeseries'],
[MappingKeysType.CUSTOM, 'gateway.keys']
[MappingKeysType.CUSTOM, 'gateway.keys'],
[MappingKeysType.ATTRIBUTES_UPDATES, 'gateway.attribute-updates'],
[MappingKeysType.RPC_METHODS, 'gateway.rpc-methods']
]
);
@ -474,7 +645,9 @@ export const MappingKeysAddKeyTranslationsMap = new Map<MappingKeysType, string>
[
[MappingKeysType.ATTRIBUTES, 'gateway.add-attribute'],
[MappingKeysType.TIMESERIES, 'gateway.add-timeseries'],
[MappingKeysType.CUSTOM, 'gateway.add-key']
[MappingKeysType.CUSTOM, 'gateway.add-key'],
[MappingKeysType.ATTRIBUTES_UPDATES, 'gateway.add-attribute-update'],
[MappingKeysType.RPC_METHODS, 'gateway.add-rpc-method']
]
);
@ -482,7 +655,9 @@ export const MappingKeysDeleteKeyTranslationsMap = new Map<MappingKeysType, stri
[
[MappingKeysType.ATTRIBUTES, 'gateway.delete-attribute'],
[MappingKeysType.TIMESERIES, 'gateway.delete-timeseries'],
[MappingKeysType.CUSTOM, 'gateway.delete-key']
[MappingKeysType.CUSTOM, 'gateway.delete-key'],
[MappingKeysType.ATTRIBUTES_UPDATES, 'gateway.delete-attribute-update'],
[MappingKeysType.RPC_METHODS, 'gateway.delete-rpc-method']
]
);
@ -490,7 +665,9 @@ export const MappingKeysNoKeysTextTranslationsMap = new Map<MappingKeysType, str
[
[MappingKeysType.ATTRIBUTES, 'gateway.no-attributes'],
[MappingKeysType.TIMESERIES, 'gateway.no-timeseries'],
[MappingKeysType.CUSTOM, 'gateway.no-keys']
[MappingKeysType.CUSTOM, 'gateway.no-keys'],
[MappingKeysType.ATTRIBUTES_UPDATES, 'gateway.no-attribute-updates'],
[MappingKeysType.RPC_METHODS, 'gateway.no-rpc-methods']
]
);
@ -499,7 +676,6 @@ export enum ServerSideRPCType {
TWO_WAY = 'twoWay'
}
export const getDefaultConfig = (resourcesService: ResourcesService, type: string): Observable<any> =>
resourcesService.loadJsonResource(`/assets/metadata/connector-default-configs/${type}.json`);
@ -550,3 +726,15 @@ export const DataConversionTranslationsMap = new Map<ConvertorType, string>(
[ConvertorType.CUSTOM, 'gateway.custom-hint']
]
);
export enum SecurityType {
BASIC128 = 'Basic128Rsa15',
BASIC256 = 'Basic256',
BASIC256SHA = 'Basic256Sha256'
}
export const ServerSecurityTypes = [
{ value: 'Basic128Rsa15', name: 'Basic128RSA15' },
{ value: 'Basic256', name: 'Basic256' },
{ value: 'Basic256Sha256', name: 'Basic256SHA256' }
];

2
ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget2.ts

@ -29,7 +29,7 @@ import {
import { TranslateService } from '@ngx-translate/core';
import { UtilsService } from '@core/services/utils.service';
import { EntityDataPageLink } from '@shared/models/query/query.models';
import { providerClass } from '@home/components/widget/lib/maps/providers';
import { providerClass } from '@home/components/widget/lib/maps/providers/public-api';
import { isDefined, isDefinedAndNotNull, parseFunction } from '@core/utils';
import L from 'leaflet';
import { forkJoin, Observable, of } from 'rxjs';

0
ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/index.ts → ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/public-api.ts

305
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';
@ -106,148 +102,167 @@ import {
RestConnectorSecurityComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/rest-connector-secuirity/rest-connector-security.component';
import { GatewayHelpLinkPipe } from '@home/pipes/public-api';
import {
DeviceInfoTableComponent,
MappingDataKeysPanelComponent,
MappingTableComponent,
ServerConfigComponent,
TypeValuePanelComponent,
BrokerConfigControlComponent,
WorkersConfigControlComponent,
GeneralConfigComponent,
MqttBasicConfigComponent,
OpcUaBasicConfigComponent,
} from '@home/components/widget/lib/gateway/connectors-configuration/public-api';
@NgModule({
declarations:
[
DisplayColumnsPanelComponent,
EntitiesTableWidgetComponent,
AlarmsTableWidgetComponent,
TimeseriesTableWidgetComponent,
EntitiesHierarchyWidgetComponent,
EdgesOverviewWidgetComponent,
DateRangeNavigatorWidgetComponent,
DateRangeNavigatorPanelComponent,
JsonInputWidgetComponent,
MultipleInputWidgetComponent,
TripAnimationComponent,
PhotoCameraInputWidgetComponent,
GatewayFormComponent,
NavigationCardsWidgetComponent,
NavigationCardWidgetComponent,
QrCodeWidgetComponent,
MobileAppQrcodeWidgetComponent,
MarkdownWidgetComponent,
SelectEntityDialogComponent,
LegendComponent,
FlotWidgetComponent,
GatewayConnectorComponent,
AddConnectorDialogComponent,
MappingTableComponent,
MappingDialogComponent,
DeviceInfoTableComponent,
MappingDataKeysPanelComponent,
BrokerSecurityComponent,
GatewayLogsComponent,
GatewayStatisticsComponent,
GatewayServiceRPCComponent,
GatewayServiceRPCConnectorComponent,
GatewayServiceRPCConnectorTemplatesComponent,
DeviceGatewayCommandComponent,
GatewayConfigurationComponent,
GatewayRemoteConfigurationDialogComponent,
GatewayServiceRPCConnectorTemplateDialogComponent,
EllipsisChipListDirective,
ValueCardWidgetComponent,
AggregatedValueCardWidgetComponent,
CountWidgetComponent,
BatteryLevelWidgetComponent,
WindSpeedDirectionWidgetComponent,
SignalStrengthWidgetComponent,
ValueChartCardWidgetComponent,
ProgressBarWidgetComponent,
LiquidLevelWidgetComponent,
DoughnutWidgetComponent,
RangeChartWidgetComponent,
BarChartWithLabelsWidgetComponent,
SingleSwitchWidgetComponent,
ActionButtonWidgetComponent,
CommandButtonWidgetComponent,
PowerButtonWidgetComponent,
SliderWidgetComponent,
ToggleButtonWidgetComponent,
TimeSeriesChartWidgetComponent,
StatusWidgetComponent,
LatestChartComponent,
PieChartWidgetComponent,
BarChartWidgetComponent,
PolarAreaWidgetComponent,
RadarChartWidgetComponent,
LabelCardWidgetComponent,
LabelValueCardWidgetComponent
],
imports: [
CommonModule,
SharedModule,
RpcWidgetsModule,
HomePageWidgetsModule,
SharedHomeComponentsModule,
RestConnectorSecurityComponent
],
exports: [
EntitiesTableWidgetComponent,
AlarmsTableWidgetComponent,
TimeseriesTableWidgetComponent,
EntitiesHierarchyWidgetComponent,
EdgesOverviewWidgetComponent,
RpcWidgetsModule,
HomePageWidgetsModule,
DateRangeNavigatorWidgetComponent,
JsonInputWidgetComponent,
MultipleInputWidgetComponent,
TripAnimationComponent,
PhotoCameraInputWidgetComponent,
GatewayFormComponent,
NavigationCardsWidgetComponent,
NavigationCardWidgetComponent,
QrCodeWidgetComponent,
MobileAppQrcodeWidgetComponent,
MarkdownWidgetComponent,
LegendComponent,
FlotWidgetComponent,
GatewayConnectorComponent,
AddConnectorDialogComponent,
MappingTableComponent,
MappingDialogComponent,
DeviceInfoTableComponent,
MappingDataKeysPanelComponent,
BrokerSecurityComponent,
GatewayLogsComponent,
GatewayServiceRPCConnectorComponent,
GatewayServiceRPCConnectorTemplatesComponent,
EllipsisChipListDirective,
GatewayStatisticsComponent,
GatewayServiceRPCComponent,
DeviceGatewayCommandComponent,
GatewayConfigurationComponent,
GatewayRemoteConfigurationDialogComponent,
GatewayServiceRPCConnectorTemplateDialogComponent,
ValueCardWidgetComponent,
AggregatedValueCardWidgetComponent,
CountWidgetComponent,
BatteryLevelWidgetComponent,
WindSpeedDirectionWidgetComponent,
SignalStrengthWidgetComponent,
ValueChartCardWidgetComponent,
ProgressBarWidgetComponent,
LiquidLevelWidgetComponent,
DoughnutWidgetComponent,
RangeChartWidgetComponent,
BarChartWithLabelsWidgetComponent,
SingleSwitchWidgetComponent,
ActionButtonWidgetComponent,
CommandButtonWidgetComponent,
PowerButtonWidgetComponent,
SliderWidgetComponent,
ToggleButtonWidgetComponent,
TimeSeriesChartWidgetComponent,
StatusWidgetComponent,
PieChartWidgetComponent,
BarChartWidgetComponent,
PolarAreaWidgetComponent,
RadarChartWidgetComponent,
LabelCardWidgetComponent,
LabelValueCardWidgetComponent
],
declarations: [
DisplayColumnsPanelComponent,
EntitiesTableWidgetComponent,
AlarmsTableWidgetComponent,
TimeseriesTableWidgetComponent,
EntitiesHierarchyWidgetComponent,
EdgesOverviewWidgetComponent,
DateRangeNavigatorWidgetComponent,
DateRangeNavigatorPanelComponent,
JsonInputWidgetComponent,
MultipleInputWidgetComponent,
TripAnimationComponent,
PhotoCameraInputWidgetComponent,
GatewayFormComponent,
NavigationCardsWidgetComponent,
NavigationCardWidgetComponent,
QrCodeWidgetComponent,
MobileAppQrcodeWidgetComponent,
MarkdownWidgetComponent,
SelectEntityDialogComponent,
LegendComponent,
FlotWidgetComponent,
GatewayConnectorComponent,
AddConnectorDialogComponent,
MappingDialogComponent,
DeviceInfoTableComponent,
MappingDataKeysPanelComponent,
TypeValuePanelComponent,
GatewayLogsComponent,
GatewayStatisticsComponent,
GatewayServiceRPCComponent,
GatewayServiceRPCConnectorComponent,
GatewayServiceRPCConnectorTemplatesComponent,
DeviceGatewayCommandComponent,
GatewayConfigurationComponent,
GatewayRemoteConfigurationDialogComponent,
GatewayServiceRPCConnectorTemplateDialogComponent,
EllipsisChipListDirective,
ValueCardWidgetComponent,
AggregatedValueCardWidgetComponent,
CountWidgetComponent,
BatteryLevelWidgetComponent,
WindSpeedDirectionWidgetComponent,
SignalStrengthWidgetComponent,
ValueChartCardWidgetComponent,
ProgressBarWidgetComponent,
LiquidLevelWidgetComponent,
DoughnutWidgetComponent,
RangeChartWidgetComponent,
BarChartWithLabelsWidgetComponent,
SingleSwitchWidgetComponent,
ActionButtonWidgetComponent,
CommandButtonWidgetComponent,
PowerButtonWidgetComponent,
SliderWidgetComponent,
ToggleButtonWidgetComponent,
TimeSeriesChartWidgetComponent,
StatusWidgetComponent,
LatestChartComponent,
PieChartWidgetComponent,
BarChartWidgetComponent,
PolarAreaWidgetComponent,
RadarChartWidgetComponent,
LabelCardWidgetComponent,
LabelValueCardWidgetComponent
],
imports: [
CommonModule,
SharedModule,
RpcWidgetsModule,
HomePageWidgetsModule,
SharedHomeComponentsModule,
RestConnectorSecurityComponent,
GatewayHelpLinkPipe,
BrokerConfigControlComponent,
WorkersConfigControlComponent,
ServerConfigComponent,
GeneralConfigComponent,
MqttBasicConfigComponent,
MappingTableComponent,
OpcUaBasicConfigComponent,
],
exports: [
EntitiesTableWidgetComponent,
AlarmsTableWidgetComponent,
TimeseriesTableWidgetComponent,
EntitiesHierarchyWidgetComponent,
EdgesOverviewWidgetComponent,
RpcWidgetsModule,
HomePageWidgetsModule,
DateRangeNavigatorWidgetComponent,
JsonInputWidgetComponent,
MultipleInputWidgetComponent,
TripAnimationComponent,
PhotoCameraInputWidgetComponent,
GatewayFormComponent,
NavigationCardsWidgetComponent,
NavigationCardWidgetComponent,
QrCodeWidgetComponent,
MobileAppQrcodeWidgetComponent,
MarkdownWidgetComponent,
LegendComponent,
FlotWidgetComponent,
GatewayConnectorComponent,
AddConnectorDialogComponent,
MappingDialogComponent,
DeviceInfoTableComponent,
MappingDataKeysPanelComponent,
TypeValuePanelComponent,
GatewayLogsComponent,
GatewayServiceRPCConnectorComponent,
GatewayServiceRPCConnectorTemplatesComponent,
EllipsisChipListDirective,
GatewayStatisticsComponent,
GatewayServiceRPCComponent,
DeviceGatewayCommandComponent,
GatewayConfigurationComponent,
GatewayRemoteConfigurationDialogComponent,
GatewayServiceRPCConnectorTemplateDialogComponent,
ValueCardWidgetComponent,
AggregatedValueCardWidgetComponent,
CountWidgetComponent,
BatteryLevelWidgetComponent,
WindSpeedDirectionWidgetComponent,
SignalStrengthWidgetComponent,
ValueChartCardWidgetComponent,
ProgressBarWidgetComponent,
LiquidLevelWidgetComponent,
DoughnutWidgetComponent,
RangeChartWidgetComponent,
BarChartWithLabelsWidgetComponent,
SingleSwitchWidgetComponent,
ActionButtonWidgetComponent,
CommandButtonWidgetComponent,
PowerButtonWidgetComponent,
SliderWidgetComponent,
ToggleButtonWidgetComponent,
TimeSeriesChartWidgetComponent,
StatusWidgetComponent,
PieChartWidgetComponent,
BarChartWidgetComponent,
PolarAreaWidgetComponent,
RadarChartWidgetComponent,
LabelCardWidgetComponent,
LabelValueCardWidgetComponent
],
providers: [
{provide: WIDGET_COMPONENTS_MODULE_TOKEN, useValue: WidgetComponentsModule }
]

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

@ -0,0 +1,39 @@
///
/// Copyright © 2016-2024 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Pipe, PipeTransform } from '@angular/core';
import {
MappingValueType,
OPCUaSourceTypes,
SourceTypes
} from '@home/components/widget/lib/gateway/gateway-widget.models';
@Pipe({
name: 'getGatewayHelpLink',
standalone: true,
})
export class GatewayHelpLinkPipe implements PipeTransform {
transform(field: string, sourceType: SourceTypes | OPCUaSourceTypes, sourceTypes?: Array<SourceTypes | OPCUaSourceTypes | MappingValueType> ): string {
if (!sourceTypes || sourceTypes?.includes(OPCUaSourceTypes.PATH)) {
if (sourceType !== OPCUaSourceTypes.CONST) {
return `widget/lib/gateway/${field}-${sourceType}_fn`;
} else {
return;
}
}
return 'widget/lib/gateway/expressions_fn';
}
}

17
ui-ngx/src/app/modules/home/pipes/public-api.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';

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -2757,11 +2757,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.",
@ -2824,8 +2832,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.",
@ -2864,13 +2872,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.",
@ -2909,6 +2924,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",
@ -2952,19 +2968,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.",
@ -3074,6 +3096,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",
@ -3103,6 +3126,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": {
@ -3115,8 +3142,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",
@ -3178,7 +3209,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",
@ -3196,6 +3229,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",
@ -3268,7 +3303,14 @@
"permit-without-calls": "Allow server to keep the GRPC connection alive even when there are no active RPC calls.",
"memory": "Your data will be stored in the in-memory queue, it is a fastest but no persistence guarantee.",
"file": "Your data will be stored in separated files and will be saved even after the gateway restart.",
"sqlite": "Your data will be stored in file based database. And will be saved even after the gateway restart."
"sqlite": "Your data will be stored in file based database. And will be saved even after the gateway restart.",
"opcua-timeout": "Timeout in seconds for connecting to OPC-UA server.",
"scan-period": "Period in milliseconds to rescan the server.",
"sub-check-period": "Period to check the subscriptions in the OPC-UA server.",
"enable-subscription": "If true - the gateway will subscribe to interesting nodes and wait for data update and if false - the gateway will rescan OPC-UA server every scanPeriodInMillis.",
"show-map": "Show nodes on scanning.",
"method-name": "Name of method on OPC-UA server.",
"arguments": "Arguments for the method (will be overwritten by arguments from the RPC request)."
}
},
"grid": {

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

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

Loading…
Cancel
Save