Browse Source

Merge pull request #10482 from Dmitriymush/improvements/connectors-config

[Gateway dashboard] Added MQTT connectors config improvements and redesign.
pull/10743/head
Igor Kulikov 2 years ago
committed by GitHub
parent
commit
2e1617ef71
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 17
      application/src/main/data/json/tenant/dashboards/gateways.json
  2. 86
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-security.component.html
  3. 20
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-security.component.scss
  4. 165
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-security.component.ts
  5. 97
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/device-info-table.component.html
  6. 57
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/device-info-table.component.scss
  7. 179
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/device-info-table.component.ts
  8. 128
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/ellipsis-chip-list.directive.ts
  9. 195
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-data-keys-panel.component.html
  10. 60
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-data-keys-panel.component.scss
  11. 173
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-data-keys-panel.component.ts
  12. 123
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table.component.html
  13. 101
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table.component.scss
  14. 339
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table.component.ts
  15. 100
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/add-connector-dialog.component.html
  16. 22
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/add-connector-dialog.component.scss
  17. 122
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/add-connector-dialog.component.ts
  18. 634
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.html
  19. 76
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.scss
  20. 360
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.ts
  21. 38
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-configuration.component.html
  22. 4
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-configuration.component.scss
  23. 275
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.html
  24. 45
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.scss
  25. 341
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts
  26. 7
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc-connector.component.ts
  27. 247
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts
  28. 21
      ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts
  29. 4
      ui-ngx/src/app/shared/components/hint-tooltip-icon.component.scss
  30. 96
      ui-ngx/src/assets/help/en_US/widget/lib/gateway/expressions_fn.md
  31. 15
      ui-ngx/src/assets/help/en_US/widget/lib/gateway/topic-filter_fn.md
  32. 192
      ui-ngx/src/assets/locale/locale.constant-en_US.json
  33. 58
      ui-ngx/src/assets/metadata/connector-default-configs/bacnet.json
  34. 73
      ui-ngx/src/assets/metadata/connector-default-configs/ble.json
  35. 89
      ui-ngx/src/assets/metadata/connector-default-configs/can.json
  36. 65
      ui-ngx/src/assets/metadata/connector-default-configs/ftp.json
  37. 248
      ui-ngx/src/assets/metadata/connector-default-configs/modbus.json
  38. 218
      ui-ngx/src/assets/metadata/connector-default-configs/mqtt.json
  39. 66
      ui-ngx/src/assets/metadata/connector-default-configs/ocpp.json
  40. 58
      ui-ngx/src/assets/metadata/connector-default-configs/odbc.json
  41. 52
      ui-ngx/src/assets/metadata/connector-default-configs/opcua.json
  42. 52
      ui-ngx/src/assets/metadata/connector-default-configs/opcua_asyncio.json
  43. 169
      ui-ngx/src/assets/metadata/connector-default-configs/request.json
  44. 190
      ui-ngx/src/assets/metadata/connector-default-configs/rest.json
  45. 138
      ui-ngx/src/assets/metadata/connector-default-configs/snmp.json
  46. 59
      ui-ngx/src/assets/metadata/connector-default-configs/socket.json
  47. 53
      ui-ngx/src/assets/metadata/connector-default-configs/xmpp.json
  48. 22
      ui-ngx/src/form.scss

17
application/src/main/data/json/tenant/dashboards/gateways.json

File diff suppressed because one or more lines are too long

86
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-security.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-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>
<tb-toggle-select formControlName="type" appearance="fill">
<tb-toggle-option *ngFor="let type of securityTypes" [value]="type">
{{ SecurityTypeTranslationsMap.get(type) | translate }}
</tb-toggle-option>
</tb-toggle-select>
</div>
<ng-container [ngSwitch]="securityFormGroup.get('type').value">
<ng-template [ngSwitchCase]="BrokerSecurityType.BASIC">
<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-template>
<ng-template [ngSwitchCase]="BrokerSecurityType.CERTIFICATES">
<div class="tb-form-hint tb-primary-fill">{{ 'gateway.path-hint' | translate }}</div>
<div class="tb-form-row space-between tb-flex fill-width">
<div class="fixed-title-width" translate>gateway.CA-certificate-path</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="pathToCACert" placeholder="{{ 'gateway.set' | translate }}"/>
</mat-form-field>
</div>
</div>
<div class="tb-form-row space-between tb-flex fill-width">
<div class="fixed-title-width" translate>gateway.private-key-path</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="pathToPrivateKey" placeholder="{{ 'gateway.set' | translate }}"/>
</mat-form-field>
</div>
</div>
<div class="tb-form-row space-between tb-flex fill-width">
<div class="fixed-title-width" translate>gateway.client-cert-path</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="pathToClientCert" placeholder="{{ 'gateway.set' | translate }}"/>
</mat-form-field>
</div>
</div>
</ng-template>
</ng-container>
</div>

20
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-security.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;
}

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

@ -0,0 +1,165 @@
///
/// Copyright © 2016-2024 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
ElementRef,
forwardRef,
NgZone,
OnDestroy,
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 { Overlay } from '@angular/cdk/overlay';
import { UtilsService } from '@core/services/utils.service';
import { EntityService } from '@core/http/entity.service';
import {
ControlValueAccessor,
FormBuilder,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
UntypedFormGroup,
ValidationErrors,
Validator,
Validators
} from '@angular/forms';
import {
BrokerSecurityType,
BrokerSecurityTypeTranslationsMap,
noLeadTrailSpacesRegex
} from '@home/components/widget/lib/gateway/gateway-widget.models';
import { takeUntil } from 'rxjs/operators';
@Component({
selector: 'tb-broker-security',
templateUrl: './broker-security.component.html',
styleUrls: ['./broker-security.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => BrokerSecurityComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => BrokerSecurityComponent),
multi: true
}
]
})
export class BrokerSecurityComponent extends PageComponent implements ControlValueAccessor, Validator, OnDestroy {
BrokerSecurityType = BrokerSecurityType;
securityTypes = Object.values(BrokerSecurityType);
SecurityTypeTranslationsMap = BrokerSecurityTypeTranslationsMap;
securityFormGroup: UntypedFormGroup;
private destroy$ = new Subject<void>();
private propagateChange = (v: any) => {};
constructor(protected store: Store<AppState>,
public translate: TranslateService,
public dialog: MatDialog,
private overlay: Overlay,
private viewContainerRef: ViewContainerRef,
private dialogService: DialogService,
private entityService: EntityService,
private utils: UtilsService,
private zone: NgZone,
private cd: ChangeDetectorRef,
private elementRef: ElementRef,
private fb: FormBuilder) {
super(store);
this.securityFormGroup = this.fb.group({
type: [BrokerSecurityType.ANONYMOUS, []],
username: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
password: ['', [Validators.pattern(noLeadTrailSpacesRegex)]],
pathToCACert: ['', [Validators.pattern(noLeadTrailSpacesRegex)]],
pathToPrivateKey: ['', [Validators.pattern(noLeadTrailSpacesRegex)]],
pathToClientCert: ['', [Validators.pattern(noLeadTrailSpacesRegex)]]
});
this.securityFormGroup.valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe((value) => {
this.updateView(value);
});
this.securityFormGroup.get('type').valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe((type) => {
this.updateValidators(type);
});
}
ngOnDestroy() {
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;
}
this.securityFormGroup.reset(deviceInfo);
this.updateView(deviceInfo);
}
validate(): ValidationErrors | null {
return this.securityFormGroup.valid ? null : {
securityForm: { valid: false }
};
}
updateView(value: any) {
this.propagateChange(value);
}
private updateValidators(type) {
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('username').enable({emitEvent: false});
this.securityFormGroup.get('password').enable({emitEvent: false});
} else if (type === BrokerSecurityType.CERTIFICATES) {
this.securityFormGroup.get('pathToCACert').enable({emitEvent: false});
this.securityFormGroup.get('pathToPrivateKey').enable({emitEvent: false});
this.securityFormGroup.get('pathToClientCert').enable({emitEvent: false});
}
}
}
}

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

@ -0,0 +1,97 @@
<!--
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 stroked" [formGroup]="mappingFormGroup">
<div class="tb-form-panel-title" [class.tb-required]="required" translate>device.device</div>
<div class="tb-form-table no-padding no-gap">
<div class="tb-form-table-header">
<div class="tb-form-table-header-cell table-name-column" translate>gateway.device-info.entity-field</div>
<div *ngIf="useSource" class="tb-form-table-header-cell table-column" translate>gateway.device-info.source</div>
<div class="tb-form-table-header-cell table-column" translate>
gateway.device-info.expression
</div>
</div>
<div class="tb-form-table-body no-gap">
<div class="tb-form-table-row tb-form-row no-border same-padding top-same-padding"
[class.bottom-same-padding]="deviceInfoType !== DeviceInfoType.FULL">
<div class="fixed-title-width" translate>gateway.device-info.name</div>
<div class="tb-flex no-gap raw-value-option" *ngIf="useSource">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="deviceNameExpressionSource">
<mat-option *ngFor="let type of sourceTypes" [value]="type">
{{ SourceTypeTranslationsMap.get(type) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="tb-form-table-row-cell tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="deviceNameExpression" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.device-info.device-name-expression-required') | translate"
*ngIf="mappingFormGroup.get('deviceNameExpression').hasError('required') &&
mappingFormGroup.get('deviceNameExpression').touched"
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>
<div class="tb-form-table-row tb-form-row no-border same-padding bottom-same-padding"
*ngIf="deviceInfoType === DeviceInfoType.FULL">
<div class="fixed-title-width" translate>gateway.device-info.profile-name</div>
<div class="tb-flex no-gap raw-value-option" *ngIf="useSource">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="deviceProfileExpressionSource">
<mat-option *ngFor="let type of sourceTypes" [value]="type">
{{ SourceTypeTranslationsMap.get(type) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="tb-form-table-row-cell tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="deviceProfileExpression" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.device-info.device-profile-expression-required') | translate"
*ngIf="mappingFormGroup.get('deviceProfileExpression').hasError('required') &&
mappingFormGroup.get('deviceProfileExpression').touched"
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>
</div>
</div>
</div>

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

@ -0,0 +1,57 @@
/**
* 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;
.tb-form-row {
&.bottom-same-padding {
padding-bottom: 16px;
}
&.top-same-padding {
padding-top: 16px;
}
.fixed-title-width {
width: 19%;
}
}
.table-column {
width: 40%;
}
.table-name-column {
width: 20%;
}
.raw-name {
width: 19%;
}
.raw-value-option {
max-width: 40%;
}
}
:host ::ng-deep {
.mat-mdc-form-field-icon-suffix {
display: flex;
}
}

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

@ -0,0 +1,179 @@
///
/// Copyright © 2016-2024 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
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,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
UntypedFormGroup,
ValidationErrors,
Validator,
Validators
} from '@angular/forms';
import {
DeviceInfoType,
noLeadTrailSpacesRegex,
SourceTypes,
SourceTypeTranslationsMap
} from '@home/components/widget/lib/gateway/gateway-widget.models';
import { coerceBoolean } from '@shared/decorators/coercion';
@Component({
selector: 'tb-device-info-table',
templateUrl: './device-info-table.component.html',
styleUrls: ['./device-info-table.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => DeviceInfoTableComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => DeviceInfoTableComponent),
multi: true
}
]
})
export class DeviceInfoTableComponent extends PageComponent implements ControlValueAccessor, Validator, OnInit, OnDestroy {
SourceTypeTranslationsMap = SourceTypeTranslationsMap;
DeviceInfoType = DeviceInfoType;
@coerceBoolean()
@Input()
useSource = true;
@coerceBoolean()
@Input()
required = false;
@Input()
sourceTypes: Array<SourceTypes> = Object.values(SourceTypes);
deviceInfoTypeValue: any;
get deviceInfoType(): any {
return this.deviceInfoTypeValue;
}
@Input()
set deviceInfoType(value: any) {
if (this.deviceInfoTypeValue !== value) {
this.deviceInfoTypeValue = value;
}
}
mappingFormGroup: UntypedFormGroup;
private destroy$ = new Subject<void>();
private propagateChange = (v: any) => {};
constructor(protected store: Store<AppState>,
public translate: TranslateService,
public dialog: MatDialog,
private overlay: Overlay,
private viewContainerRef: ViewContainerRef,
private dialogService: DialogService,
private entityService: EntityService,
private utils: UtilsService,
private zone: NgZone,
private cd: ChangeDetectorRef,
private elementRef: ElementRef,
private fb: FormBuilder) {
super(store);
}
ngOnInit() {
this.mappingFormGroup = this.fb.group({
deviceNameExpression: ['', this.required ?
[Validators.required, Validators.pattern(noLeadTrailSpacesRegex)] : [Validators.pattern(noLeadTrailSpacesRegex)]]
});
if (this.useSource) {
this.mappingFormGroup.addControl('deviceNameExpressionSource',
this.fb.control(SourceTypes.MSG, []));
}
if (this.deviceInfoType === DeviceInfoType.FULL) {
if (this.useSource) {
this.mappingFormGroup.addControl('deviceProfileExpressionSource',
this.fb.control(SourceTypes.MSG, []));
}
this.mappingFormGroup.addControl('deviceProfileExpression',
this.fb.control('', this.required ?
[Validators.required, Validators.pattern(noLeadTrailSpacesRegex)] : [Validators.pattern(noLeadTrailSpacesRegex)]));
}
this.mappingFormGroup.valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe((value) => {
this.updateView(value);
});
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {}
writeValue(deviceInfo: any) {
this.mappingFormGroup.patchValue(deviceInfo, {emitEvent: false});
}
validate(): ValidationErrors | null {
return this.mappingFormGroup.valid ? null : {
mappingForm: { valid: false }
};
}
updateView(value: any) {
this.propagateChange(value);
}
}

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

@ -0,0 +1,128 @@
///
/// 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 {
Directive,
ElementRef,
Inject,
Input,
OnDestroy,
Renderer2
} from '@angular/core';
import { isEqual } from '@core/utils';
import { TranslateService } from '@ngx-translate/core';
import { WINDOW } from '@core/services/window.service';
import { fromEvent, Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
@Directive({
// eslint-disable-next-line @angular-eslint/directive-selector
selector: '[tb-ellipsis-chip-list]'
})
export class EllipsisChipListDirective implements OnDestroy {
chipsValue: string[];
private destroy$ = new Subject<void>();
@Input('tb-ellipsis-chip-list')
set chips(value: string[]) {
if (!isEqual(this.chipsValue, value)) {
this.chipsValue = value;
setTimeout(() => {
this.adjustChips();
}, 0);
}
}
constructor(private el: ElementRef,
private renderer: Renderer2,
private translate: TranslateService,
@Inject(WINDOW) private window: Window) {
this.renderer.setStyle(this.el.nativeElement, 'max-height', '48px');
this.renderer.setStyle(this.el.nativeElement, 'overflow', 'auto');
fromEvent(window, 'resize').pipe(
takeUntil(this.destroy$)
).subscribe(() => {
this.adjustChips();
});
}
private adjustChips(): void {
const chipListElement = this.el.nativeElement;
const ellipsisChip = this.el.nativeElement.querySelector('.ellipsis-chip');
const margin = parseFloat(this.window.getComputedStyle(ellipsisChip).marginLeft) || 0;
const chipNodes = chipListElement.querySelectorAll('mat-chip:not(.ellipsis-chip)');
if (this.chipsValue.length > 1) {
const ellipsisText = this.el.nativeElement.querySelector('.ellipsis-text');
this.renderer.setStyle(ellipsisChip, 'display', 'inline-flex');
ellipsisText.innerHTML = this.translate.instant('gateway.ellipsis-chips-text',
{count: (this.chipsValue.length)});
const availableWidth = chipListElement.offsetWidth - (ellipsisChip.offsetWidth + margin);
let usedWidth = 0;
let visibleChipsCount = 0;
chipNodes.forEach((chip) => {
this.renderer.setStyle(chip, 'display', 'inline-flex');
const textLabelContainer = chip.querySelector('.mdc-evolution-chip__text-label');
this.applyMaxChipTextWidth(textLabelContainer, (availableWidth / 3));
if ((usedWidth + (chip.offsetWidth + margin) <= availableWidth) && (visibleChipsCount < this.chipsValue.length)) {
visibleChipsCount++;
usedWidth += chip.offsetWidth + margin;
} else {
this.renderer.setStyle(chip, 'display', 'none');
}
});
ellipsisText.innerHTML = this.translate.instant('gateway.ellipsis-chips-text',
{count: (this.chipsValue.length - visibleChipsCount)});
if (visibleChipsCount === this.chipsValue?.length) {
this.renderer.setStyle(ellipsisChip, 'display', 'none');
}
} else if (this.chipsValue.length === 1) {
const chipLabelContainer = chipNodes[0].querySelector('.mdc-evolution-chip__action');
const textLabelContainer = chipLabelContainer.querySelector('.mdc-evolution-chip__text-label');
const leftPadding = parseFloat(this.window.getComputedStyle(chipLabelContainer).paddingLeft) || 0;
const rightPadding = parseFloat(this.window.getComputedStyle(chipLabelContainer).paddingRight) || 0;
const computedTextWidth = chipListElement.offsetWidth - margin -
(leftPadding + rightPadding);
this.renderer.setStyle(ellipsisChip, 'display', 'none');
this.renderer.setStyle(chipNodes[0], 'display', 'inline-flex');
this.applyMaxChipTextWidth(textLabelContainer, computedTextWidth);
} else {
this.renderer.setStyle(ellipsisChip, 'display', 'none');
}
}
private applyMaxChipTextWidth(element: HTMLElement, widthLimit: number): void {
this.renderer.setStyle(element, 'max-width', widthLimit + 'px');
this.renderer.setStyle(element, 'overflow', 'hidden');
this.renderer.setStyle(element, 'text-overflow', 'ellipsis');
this.renderer.setStyle(element, 'white-space', 'nowrap');
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
}

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

@ -0,0 +1,195 @@
<!--
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-mapping-keys-panel">
<div class="tb-form-panel no-border no-padding">
<div class="tb-form-panel-title">{{ panelTitle | translate }}{{' (' + keysListFormArray.controls.length + ')'}}</div>
<div class="tb-form-panel no-border no-padding key-panel" *ngIf="keysListFormArray.controls.length; else noKeys">
<div class="tb-form-panel no-border no-padding tb-flex no-flex row center fill-width"
*ngFor="let keyControl of keysListFormArray.controls; trackBy: 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">{{ keyControl.get('key').value }}</div>
{{ '-' }}
<div class="title-container">{{ valueTitle(keyControl.get('value').value) }}</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">
<div class="tb-form-panel stroked">
<div class="tb-form-panel-title" translate>gateway.platform-side</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required"
tb-hint-tooltip-icon="{{ 'gateway.JSONPath-hint' | translate }}" translate>
gateway.key
</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="key" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.key-required') | translate"
*ngIf="keyControl.get('key').hasError('required') &&
keyControl.get('key').touched"
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>
</div>
<div class="tb-form-panel stroked">
<div class="tb-form-panel-title" translate>gateway.connector-side</div>
<div class="tb-form-row">
<div class="fixed-title-width tb-required" translate>gateway.type</div>
<mat-form-field class="tb-flex no-gap fill-width" appearance="outline" subscriptSizing="dynamic">
<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>
<span>
{{ (rawData ? 'gateway.raw' : valueTypes.get(keyControl.get('type').value)?.name) | translate}}
</span>
</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>
<span>{{ valueTypes.get(valueType).name | translate }}</span>
</mat-option>
</ng-container>
<ng-template #rawOption>
<mat-option [value]="'raw'">
<span>{{ 'gateway.raw' | translate }}</span>
</mat-option>
</ng-template>
</mat-select>
</mat-form-field>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required"
tb-hint-tooltip-icon="{{ 'gateway.JSONPath-hint' | translate }}" translate>
gateway.value
</div>
<mat-form-field fxFlex appearance="outline" subscriptSizing="dynamic" class="tb-flex no-gap">
<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>
<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>
</div>
<ng-template #customPanel>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required" translate>gateway.key</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="key" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.key-required') | translate"
*ngIf="keyControl.get('key').hasError('required') && keyControl.get('key').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.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>
</ng-template>
</mat-expansion-panel>
</ng-container>
</div>
<button type="button"
mat-icon-button
(click)="deleteKey($event, $index)"
[matTooltip]="deleteKeyTitle | translate"
matTooltipPosition="above">
<mat-icon>delete</mat-icon>
</button>
</div>
</div>
<div>
<button type="button" mat-stroked-button color="primary" (click)="addKey()">
{{ addKeyTitle | translate }}
</button>
</div>
</div>
<ng-template #noKeys>
<div class="tb-flex no-flex center align-center key-panel">
<span class="tb-prompt" translate>{{ noKeysText }}</span>
</div>
</ng-template>
<div class="tb-flex flex-end">
<button mat-button
color="primary"
type="button"
(click)="cancel()">
{{ 'action.cancel' | translate }}
</button>
<button mat-raised-button
color="primary"
type="button"
(click)="applyKeysData()"
[disabled]="keysListFormArray.invalid || !keysListFormArray.dirty">
{{ 'action.apply' | translate }}
</button>
</div>
</div>

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

@ -0,0 +1,60 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
:host {
.tb-mapping-keys-panel {
width: 77vw;
max-width: 700px;
.title-container {
max-width: 11vw;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap
}
.key-panel {
height: 500px;
overflow: auto;
}
tb-value-input {
width: 100%;
}
.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;
}
}

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

@ -0,0 +1,173 @@
///
/// Copyright © 2016-2024 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import {
Component,
EventEmitter,
Input,
OnInit,
Output
} from '@angular/core';
import {
AbstractControl,
UntypedFormArray,
UntypedFormBuilder,
Validators
} from '@angular/forms';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { coerceBoolean } from '@shared/decorators/coercion';
import { TbPopoverComponent } from '@shared/components/popover.component';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { PageComponent } from '@shared/components/page.component';
import { isDefinedAndNotNull } from '@core/utils';
import {
MappingDataKey,
MappingKeysType,
MappingValueType,
mappingValueTypesMap,
noLeadTrailSpacesRegex
} from '@home/components/widget/lib/gateway/gateway-widget.models';
@Component({
selector: 'tb-mapping-data-keys-panel',
templateUrl: './mapping-data-keys-panel.component.html',
styleUrls: ['./mapping-data-keys-panel.component.scss'],
providers: []
})
export class MappingDataKeysPanelComponent extends PageComponent implements OnInit {
@Input()
panelTitle: string;
@Input()
addKeyTitle: string;
@Input()
deleteKeyTitle: string;
@Input()
noKeysText: string;
@Input()
keys: Array<MappingDataKey> | {[key: string]: any};
@Input()
keysType: string;
@Input()
@coerceBoolean()
rawData = false;
@Input()
popover: TbPopoverComponent<MappingDataKeysPanelComponent>;
@Output()
keysDataApplied = new EventEmitter<Array<MappingDataKey> | {[key: string]: any}>();
valueTypeKeys = Object.values(MappingValueType);
MappingKeysType = MappingKeysType;
valueTypeEnum = MappingValueType;
valueTypes = mappingValueTypesMap;
dataKeyType: DataKeyType;
keysListFormArray: UntypedFormArray;
errorText = '';
constructor(private fb: UntypedFormBuilder,
protected store: Store<AppState>) {
super(store);
}
ngOnInit() {
this.keysListFormArray = this.prepareKeysFormArray(this.keys)
}
trackByKey(index: number, keyControl: AbstractControl): any {
return keyControl;
}
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));
}
this.keysListFormArray.push(dataKeyFormGroup);
}
deleteKey($event: Event, index: number): void {
if ($event) {
$event.stopPropagation();
}
this.keysListFormArray.removeAt(index);
this.keysListFormArray.markAsDirty();
}
cancel() {
this.popover?.hide();
}
applyKeysData() {
let keys = this.keysListFormArray.value;
if (this.keysType === MappingKeysType.CUSTOM) {
keys = {};
for (let key of this.keysListFormArray.value) {
keys[key.key] = key.value;
}
}
this.keysDataApplied.emit(keys);
}
private prepareKeysFormArray(keys: Array<MappingDataKey> | {[key: string]: any}): UntypedFormArray {
const keysControlGroups: Array<AbstractControl> = [];
if (keys) {
if (this.keysType === MappingKeysType.CUSTOM) {
keys = Object.keys(keys).map(key => {
return {key, value: keys[key], type: ''};
});
}
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, []]
});
keysControlGroups.push(dataKeyFormGroup);
});
}
return this.fb.array(keysControlGroups);
}
valueTitle(value: any): string {
if (isDefinedAndNotNull(value)) {
if (typeof value === 'object') {
return JSON.stringify(value);
}
return value;
}
return '';
}
}

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

@ -0,0 +1,123 @@
<!--
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-mapping-table tb-absolute-fill">
<div fxFlex fxLayout="column" class="tb-mapping-table-content">
<mat-toolbar class="mat-mdc-table-toolbar" [fxShow]="!textSearchMode">
<div class="mat-toolbar-tools">
<div fxLayout="row" fxLayoutAlign="start center" fxLayout.xs="column" fxLayoutAlign.xs="center start" class="title-container">
<span class="tb-mapping-table-title">{{mappingTypeTranslationsMap.get(mappingType) | translate}}</span>
</div>
<span fxFlex></span>
<button mat-icon-button
(click)="manageMapping($event)"
matTooltip="{{ 'action.add' | translate }}"
matTooltipPosition="above">
<mat-icon>add</mat-icon>
</button>
<button mat-icon-button
(click)="enterFilterMode()"
matTooltip="{{ 'action.search' | translate }}"
matTooltipPosition="above">
<mat-icon>search</mat-icon>
</button>
</div>
</mat-toolbar>
<mat-toolbar class="mat-mdc-table-toolbar" [fxShow]="textSearchMode">
<div class="mat-toolbar-tools">
<button mat-icon-button
matTooltip="{{ 'action.search' | translate }}"
matTooltipPosition="above">
<mat-icon>search</mat-icon>
</button>
<mat-form-field fxFlex>
<mat-label>&nbsp;</mat-label>
<input #searchInput matInput
[formControl]="textSearch"
placeholder="{{ 'common.enter-search' | translate }}"/>
</mat-form-field>
<button mat-icon-button (click)="exitFilterMode()"
matTooltip="{{ 'action.close' | translate }}"
matTooltipPosition="above">
<mat-icon>close</mat-icon>
</button>
</div>
</mat-toolbar>
<div class="table-container">
<table mat-table [dataSource]="dataSource">
<ng-container [matColumnDef]="column.def" *ngFor="let column of mappingColumns; let i = index">
<mat-header-cell *matHeaderCellDef class="table-value-column" [class.request-column]="mappingType === mappingTypeEnum.REQUESTS">
{{ column.title | translate }}
</mat-header-cell>
<mat-cell *matCellDef="let mapping" class="table-value-column" [class.request-column]="mappingType === mappingTypeEnum.REQUESTS">
{{ mapping[column.def] }}
</mat-cell>
</ng-container>
<ng-container matColumnDef="actions" stickyEnd>
<mat-header-cell *matHeaderCellDef
[ngStyle.gt-md]="{ minWidth: '96px', maxWidth: '96px', width: '96px', textAlign: 'center'}">
</mat-header-cell>
<mat-cell *matCellDef="let mapping; let i = index"
[ngStyle.gt-md]="{ minWidth: '96px', maxWidth: '96px', width: '96px'}">
<div fxHide fxShow.gt-md fxFlex fxLayout="row" fxLayoutAlign="end">
<button mat-icon-button
(click)="manageMapping($event, i)">
<tb-icon>edit</tb-icon>
</button>
<button mat-icon-button
(click)="deleteMapping($event, i)">
<tb-icon>delete</tb-icon>
</button>
</div>
<div fxHide fxShow.lt-lg fxFlex fxLayout="row" fxLayoutAlign="end">
<button mat-icon-button
(click)="$event.stopPropagation()"
[matMenuTriggerFor]="cellActionsMenu">
<mat-icon class="material-icons">more_vert</mat-icon>
</button>
<mat-menu #cellActionsMenu="matMenu" xPosition="before">
<button mat-icon-button
(click)="manageMapping($event, i)">
<tb-icon>edit</tb-icon>
</button>
<button mat-icon-button
(click)="deleteMapping($event, i)">
<tb-icon>delete</tb-icon>
</button>
</mat-menu>
</div>
</mat-cell>
</ng-container>
<mat-header-row [ngClass]="{'mat-row-select': true}" *matHeaderRowDef="displayedColumns; sticky: true"></mat-header-row>
<mat-row *matRowDef="let mapping; columns: displayedColumns;"></mat-row>
</table>
<section [fxShow]="!textSearchMode && (dataSource.isEmpty() | async)" fxLayoutAlign="center center"
class="mat-headline-5 tb-absolute-fill tb-add-new">
<button mat-button class="connector"
(click)="manageMapping($event)">
<mat-icon class="tb-mat-96">add</mat-icon>
<span>{{ 'gateway.add-mapping' | translate }}</span>
</button>
</section>
</div>
<span [fxShow]="textSearchMode && (dataSource.isEmpty() | async)"
fxLayoutAlign="center center"
class="no-data-found" translate>
widget.no-data-found
</span>
</div>
</div>

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

@ -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 '../scss/constants';
:host {
width: 100%;
height: 100%;
display: block;
.tb-mapping-table {
.tb-mapping-table-content {
width: 100%;
height: 100%;
background: #fff;
overflow: hidden;
&.tb-outlined-border {
box-shadow: 0 0 0 0 rgb(0 0 0 / 20%), 0 0 0 0 rgb(0 0 0 / 14%), 0 0 0 0 rgb(0 0 0 / 12%);
border: solid 1px #e0e0e0;
border-radius: 4px;
}
.mat-toolbar-tools{
min-height: auto;
}
.title-container{
overflow: hidden;
}
.tb-mapping-table-title {
padding-right: 20px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.table-container {
overflow: auto;
.mat-mdc-table {
table-layout: fixed;
min-width: 450px;
.table-value-column {
padding: 0 12px;
width: 23%;
&.request-column {
width: 38%;
}
}
}
}
.ellipsis {
overflow: hidden;
text-overflow: ellipsis;
}
}
}
.no-data-found {
height: calc(100% - 120px);
}
@media #{$mat-xs} {
.mat-toolbar {
height: auto;
min-height: 100px;
.tb-mapping-table-title{
padding-bottom: 5px;
width: 100%;
}
}
}
}
:host ::ng-deep {
mat-cell.tb-value-cell {
cursor: pointer;
.mat-icon {
height: 24px;
width: 24px;
font-size: 24px;
color: #757575
}
}
}

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

@ -0,0 +1,339 @@
///
/// Copyright © 2016-2024 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import {
AfterViewInit,
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
ElementRef,
forwardRef,
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 {
ConvertorTypeTranslationsMap,
MappingInfo,
MappingType,
MappingTypeTranslationsMap,
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';
@Component({
selector: 'tb-mapping-table',
templateUrl: './mapping-table.component.html',
styleUrls: ['./mapping-table.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => MappingTableComponent),
multi: true
}
]
})
export class MappingTableComponent extends PageComponent implements ControlValueAccessor, AfterViewInit, OnInit, OnDestroy {
mappingTypeTranslationsMap = MappingTypeTranslationsMap;
mappingTypeEnum = MappingType;
displayedColumns = [];
mappingColumns = [];
textSearchMode = false;
dataSource: MappingDatasource;
hidePageSize = false;
activeValue = false;
dirtyValue = false;
viewsInited = false;
mappingTypeValue: MappingType;
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});
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);
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'}
);
}
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);
});
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
ngAfterViewInit() {
this.textSearch.valueChanges.pipe(
debounceTime(150),
distinctUntilChanged((prev, current) => (prev ?? '') === current.trim()),
takeUntil(this.destroy$)
).subscribe((text) => {
const searchText = text.trim();
this.updateTableData(this.mappingFormGroup.value, searchText.trim())
});
}
registerOnChange(fn: any): void {
this.propagateChange = 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);
}
updateView(mappingConfigs: Array<{[key: string]: any}>) {
let config;
if (this.mappingType === MappingType.REQUESTS) {
config = {};
for (let mappingConfig of mappingConfigs) {
if (config[mappingConfig.requestType]) {
config[mappingConfig.requestType].push(mappingConfig.requestValue);
} else {
config[mappingConfig.requestType] = [mappingConfig.requestValue];
}
}
} else {
config = mappingConfigs;
}
this.propagateChange(config);
}
enterFilterMode() {
this.textSearchMode = true;
setTimeout(() => {
this.searchInputField.nativeElement.focus();
this.searchInputField.nativeElement.setSelectionRange(0, 0);
}, 10);
}
exitFilterMode() {
this.updateTableData(this.mappingFormGroup.value);
this.textSearchMode = false;
this.textSearch.reset();
}
manageMapping($event: Event, index?: number) {
if ($event) {
$event.stopPropagation();
}
const value = isDefinedAndNotNull(index) ? this.mappingFormGroup.at(index).value : {};
this.dialog.open<MappingDialogComponent, MappingInfo, boolean>(MappingDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
mappingType: this.mappingType,
value,
buttonTitle: isUndefinedOrNull(index) ? 'action.add' : 'action.apply'
}
}).afterClosed().subscribe(
(res) => {
if (res) {
if (isDefinedAndNotNull(index)) {
this.mappingFormGroup.at(index).patchValue(res);
} else {
this.mappingFormGroup.push(this.fb.group(res));
}
}
}
);
}
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
};
});
}
if (textSearch) {
tableValue = tableValue.filter(value =>
Object.values(value).some(val =>
val.toString().toLowerCase().includes(textSearch.toLowerCase())
)
);
}
this.dataSource.loadMappings(tableValue);
}
deleteMapping($event: Event, index: number) {
if ($event) {
$event.stopPropagation();
}
this.dialogService.confirm(
this.translate.instant('gateway.delete-mapping-title'),
'',
this.translate.instant('action.no'),
this.translate.instant('action.yes'),
true
).subscribe((result) => {
if (result) {
this.mappingFormGroup.removeAt(index);
}
});
}
}
export class MappingDatasource implements DataSource<{[key: string]: any}> {
private mappingSubject = new BehaviorSubject<Array<{[key: string]: any}>>([]);
private allMappings: Observable<Array<{[key: string]: any}>>;
constructor() {}
connect(collectionViewer: CollectionViewer): Observable<Array<{[key: string]: any}>> {
return this.mappingSubject.asObservable();
}
disconnect(collectionViewer: CollectionViewer): void {
this.mappingSubject.complete();
}
loadMappings(mappings: Array<{[key: string]: any}>, pageLink?: PageLink, reload: boolean = false): void {
if (reload) {
this.allMappings = null;
}
this.mappingSubject.next(mappings);
}
isEmpty(): Observable<boolean> {
return this.mappingSubject.pipe(
map((mappings) => !mappings.length)
);
}
total(): Observable<number> {
return this.mappingSubject.pipe(
map((mappings) => mappings.length)
);
}
}

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

@ -0,0 +1,100 @@
<!--
Copyright © 2016-2024 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div [formGroup]="connectorForm" class="add-connector">
<mat-toolbar color="primary">
<h2>{{ "gateway.add-connector" | translate}}</h2>
<span fxFlex></span>
<div [tb-help]="helpLinkId()"></div>
<button mat-icon-button
(click)="cancel()"
type="button">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<div mat-dialog-content>
<div class="tb-form-panel no-border no-padding" fxLayout="column">
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" translate>gateway.type</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="type">
<mat-option *ngFor="let type of gatewayConnectorDefaultTypesTranslatesMap | keyvalue" [value]="type.key">
{{ type.value }}
</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.name</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="name" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="(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 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 class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="useDefaults">
<mat-label tb-hint-tooltip-icon="{{ 'gateway.fill-connector-defaults-hint' | translate }}">
{{ 'gateway.fill-connector-defaults' | translate }}
</mat-label>
</mat-slide-toggle>
</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 fixed-title-width" 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>
</div>
<div mat-dialog-actions fxLayoutAlign="end center">
<button mat-button color="primary"
type="button"
cdkFocusInitial
(click)="cancel()">
{{ 'action.cancel' | translate }}
</button>
<button mat-raised-button color="primary"
(click)="add()"
[disabled]="connectorForm.invalid || !connectorForm.dirty">
{{ 'action.add' | translate }}
</button>
</div>
</div>

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

@ -0,0 +1,22 @@
/**
* 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 {
.add-connector {
min-width: 400px;
width: 500px;
}
}

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

@ -0,0 +1,122 @@
///
/// 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, Inject, OnDestroy } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { FormBuilder, UntypedFormControl, UntypedFormGroup, ValidatorFn, 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 {
AddConnectorConfigData,
ConnectorType,
CreatedConnectorConfigData,
GatewayConnectorDefaultTypesTranslatesMap,
GatewayLogLevel,
getDefaultConfig,
noLeadTrailSpacesRegex
} from '@home/components/widget/lib/gateway/gateway-widget.models';
import { Subject } from 'rxjs';
import { ResourcesService } from '@core/services/resources.service';
@Component({
selector: 'tb-add-connector-dialog',
templateUrl: './add-connector-dialog.component.html',
styleUrls: ['./add-connector-dialog.component.scss'],
providers: [],
})
export class AddConnectorDialogComponent extends DialogComponent<AddConnectorDialogComponent, BaseData<HasId>> implements OnDestroy {
connectorForm: UntypedFormGroup;
connectorType = ConnectorType;
gatewayConnectorDefaultTypesTranslatesMap = GatewayConnectorDefaultTypesTranslatesMap;
gatewayLogLevel = Object.values(GatewayLogLevel);
submitted = false;
private destroy$ = new Subject<void>();
constructor(protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: AddConnectorConfigData,
public dialogRef: MatDialogRef<AddConnectorDialogComponent, CreatedConnectorConfigData>,
private fb: FormBuilder,
private resourcesService: ResourcesService) {
super(store, router, dialogRef);
this.connectorForm = this.fb.group({
type: [ConnectorType.MQTT, []],
name: ['', [Validators.required, this.uniqNameRequired(), Validators.pattern(noLeadTrailSpacesRegex)]],
logLevel: [GatewayLogLevel.INFO, []],
useDefaults: [true, []],
sendDataOnlyOnChange: [false, []],
});
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
super.ngOnDestroy();
}
helpLinkId(): string {
return 'https://thingsboard.io/docs/iot-gateway/configuration/';
}
cancel(): void {
this.dialogRef.close(null);
}
add(): void {
this.submitted = true;
const value = this.connectorForm.getRawValue();
if (value.useDefaults) {
getDefaultConfig(this.resourcesService, value.type).subscribe((defaultConfig) => {
value.configurationJson = defaultConfig;
if (this.connectorForm.valid) {
this.dialogRef.close(value);
}
});
}
if (this.connectorForm.valid) {
this.dialogRef.close(value);
}
}
private uniqNameRequired(): ValidatorFn {
return (c: UntypedFormControl) => {
const newName = c.value.trim().toLowerCase();
const found = this.data.dataSourceData.find((connectorAttr) => {
const connectorData = connectorAttr.value;
return connectorData.name.toLowerCase() === newName;
});
if (found) {
if (c.hasError('required')) {
return c.getError('required');
}
return {
duplicateName: {
valid: false
}
};
}
return null;
};
}
}

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

@ -0,0 +1,634 @@
<!--
Copyright © 2016-2024 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div [formGroup]="mappingForm" class="key-mapping">
<mat-toolbar color="primary">
<h2>{{ MappingTypeTranslationsMap.get(this.data?.mappingType) | translate}}</h2>
<span fxFlex></span>
<div [tb-help]="helpLinkId()"></div>
<button mat-icon-button
(click)="cancel()"
type="button">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<div mat-dialog-content>
<div class="tb-form-panel no-border no-padding" fxLayout="column">
<div class="tb-form-hint tb-primary-fill">
{{ MappingHintTranslationsMap.get(this.data?.mappingType) | translate }}
</div>
<ng-container [ngSwitch]="data.mappingType">
<ng-template [ngSwitchCase]="MappingType.DATA">
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required" translate>gateway.topic-filter</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="topicFilter" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.topic-required') | translate"
*ngIf="mappingForm.get('topicFilter').hasError('required') &&
mappingForm.get('topicFilter').touched;"
class="tb-error">
warning
</mat-icon>
<div matSuffix
class="see-example"
[tb-help-popup]="'widget/lib/gateway/topic-filter_fn'"
tb-help-popup-placement="left"
[tb-help-popup-style]="{maxWidth: '970px'}">
</div>
</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.response-topic-Qos-hint' | translate }}" translate>
gateway.mqtt-qos
</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="subscriptionQos">
<mat-option *ngFor="let type of qualityTypes" [value]="type">
{{ QualityTranslationsMap.get(type) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
<ng-container formGroupName="converter">
<div class="tb-form-row space-between tb-flex">
<div class="fixed-title-width" translate>gateway.payload-type</div>
<tb-toggle-select formControlName="type" appearance="fill">
<tb-toggle-option *ngFor="let type of convertorTypes" [value]="type">
{{ ConvertorTypeTranslationsMap.get(type) | translate }}
</tb-toggle-option>
</tb-toggle-select>
</div>
<div class="tb-form-panel stroked">
<div class="tb-form-panel-title" translate>gateway.data-conversion</div>
<div class="tb-form-hint tb-primary-fill">
{{ DataConversionTranslationsMap.get(converterType) | translate }}
</div>
<ng-container [formGroupName]="converterType" [ngSwitch]="converterType">
<ng-template [ngSwitchCase]="ConvertorTypeEnum.JSON">
<tb-device-info-table formControlName="deviceInfo" [deviceInfoType]="DeviceInfoType.FULL" required="true">
</tb-device-info-table>
</ng-template>
<ng-template [ngSwitchCase]="ConvertorTypeEnum.BYTES">
<tb-device-info-table formControlName="deviceInfo" [deviceInfoType]="DeviceInfoType.FULL"
[sourceTypes]="[sourceTypesEnum.MSG, sourceTypesEnum.CONST]" required="true">
</tb-device-info-table>
</ng-template>
<div class="tb-form-panel no-border no-padding"
*ngIf="converterType === ConvertorTypeEnum.BYTES || converterType === ConvertorTypeEnum.JSON">
<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]="converterAttributes" class="tb-flex">
<mat-chip *ngFor="let attribute of converterAttributes">
{{ 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"
#attributesButton
(click)="manageKeys($event, attributesButton, 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]="converterTelemetry">
<mat-chip *ngFor="let telemetry of converterTelemetry">
{{ 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"
#telemetryButton
(click)="manageKeys($event, telemetryButton, MappingKeysType.TIMESERIES)">
<tb-icon matButtonIcon>edit</tb-icon>
</button>
</div>
</div>
</div>
<div class="tb-form-panel no-border no-padding" *ngIf="converterType === ConvertorTypeEnum.CUSTOM">
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required"
tb-hint-tooltip-icon="{{ 'gateway.extension-hint' | translate }}" translate>
gateway.extension
</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="extension" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.extension-required') | translate"
*ngIf="mappingForm.get('converter.custom.extension').hasError('required') &&
mappingForm.get('converter.custom.extension').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
</div>
<div class="tb-form-row space-between same-padding tb-flex column">
<div class="tb-form-panel-title" translate>gateway.extension-configuration</div>
<div class="tb-form-hint tb-primary-fill">{{ 'gateway.extension-configuration-hint' | translate }}</div>
<div class="tb-form-row space-between tb-flex">
<div class="fixed-title-width" translate>gateway.keys</div>
<div class="tb-flex ellipsis-chips-container">
<mat-chip-listbox [tb-ellipsis-chip-list]="customKeys" class="tb-flex">
<mat-chip *ngFor="let telemetry of customKeys">
{{ 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"
#keysButton
(click)="manageKeys($event, keysButton, MappingKeysType.CUSTOM)">
<tb-icon matButtonIcon>edit</tb-icon>
</button>
</div>
</div>
</div>
</div>
</ng-container>
</div>
</ng-container>
</ng-template>
<ng-template [ngSwitchCase]="MappingType.REQUESTS">
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" translate>gateway.request-type</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="requestType">
<mat-option *ngFor="let type of requestTypes" [value]="type">
{{ RequestTypesTranslationsMap.get(type) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
<ng-container formGroupName="requestValue">
<ng-container [formGroup]="mappingForm.get('requestValue').get(requestMappingType)" [ngSwitch]="requestMappingType">
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"
*ngIf="requestMappingType === RequestTypeEnum.ATTRIBUTE_REQUEST ||
requestMappingType === RequestTypeEnum.CONNECT_REQUEST ||
requestMappingType === RequestTypeEnum.DISCONNECT_REQUEST">
<div class="fixed-title-width tb-required" translate>gateway.topic-filter</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" [formControl]="mappingForm.get('requestValue').get(requestMappingType).get('topicFilter')"
placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.topic-required') | translate"
*ngIf="mappingForm.get('requestValue').get(requestMappingType).get('topicFilter').hasError('required') &&
mappingForm.get('requestValue').get(requestMappingType).get('topicFilter').touched"
class="tb-error">
warning
</mat-icon>
<div matSuffix
class="see-example"
[tb-help-popup]="'widget/lib/gateway/topic-filter_fn'"
tb-help-popup-placement="left"
[tb-help-popup-style]="{maxWidth: '970px'}">
</div>
</mat-form-field>
</div>
</div>
<ng-template [ngSwitchCase]="RequestTypeEnum.CONNECT_REQUEST">
<tb-device-info-table formControlName="deviceInfo" [deviceInfoType]="DeviceInfoType.FULL" required="true">
</tb-device-info-table>
</ng-template>
<ng-template [ngSwitchCase]="RequestTypeEnum.DISCONNECT_REQUEST">
<tb-device-info-table formControlName="deviceInfo" [deviceInfoType]="DeviceInfoType.PARTIAL" required="true">
</tb-device-info-table>
</ng-template>
<ng-template [ngSwitchCase]="RequestTypeEnum.ATTRIBUTE_REQUEST">
<div class="tb-form-panel stroked">
<div class="tb-form-panel-title tb-required" translate>gateway.from-device-request-settings</div>
<div class="tb-form-hint tb-primary-fill" translate>
gateway.from-device-request-settings-hint
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center" formGroupName="deviceInfo">
<div class="fixed-title-width tb-flex no-flex align-center" translate>
<div class="tb-required" translate>gateway.device-info.device-name-expression</div>
</div>
<div class="tb-flex">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="deviceNameExpressionSource">
<mat-option *ngFor="let type of sourceTypes" [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="deviceNameExpression" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.device-info.device-name-expression-required') | translate"
*ngIf="(mappingForm.get('requestValue.attributeRequests.deviceInfo.deviceNameExpression').hasError('required') &&
mappingForm.get('requestValue.attributeRequests.deviceInfo.deviceNameExpression').touched)"
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>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required" translate>gateway.attribute-name-expression</div>
<div class="tb-flex">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="attributeNameExpressionSource">
<mat-option *ngFor="let type of sourceTypes" [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="attributeNameExpression" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.attribute-name-expression-required') | translate"
*ngIf="mappingForm.get('requestValue.attributeRequests.attributeNameExpression').hasError('required') &&
mappingForm.get('requestValue.attributeRequests.attributeNameExpression').touched"
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>
</div>
<div class="tb-form-panel stroked">
<div class="tb-form-panel-title tb-required" translate>gateway.to-device-response-settings</div>
<div class="tb-form-hint tb-primary-fill" translate>
gateway.to-device-response-settings-hint
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required" translate>gateway.response-value-expression</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="valueExpression" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.response-value-expression-required') | translate"
*ngIf="mappingForm.get('requestValue.attributeRequests.valueExpression').hasError('required') &&
mappingForm.get('requestValue.attributeRequests.valueExpression').touched"
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>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required" translate>gateway.response-topic-expression</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="topicExpression" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.response-topic-expression-required') | translate"
*ngIf="mappingForm.get('requestValue.attributeRequests.topicExpression').hasError('required') &&
mappingForm.get('requestValue.attributeRequests.topicExpression').touched"
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>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<mat-slide-toggle class="mat-slide" formControlName="retain">
<mat-label tb-hint-tooltip-icon="{{ 'gateway.retain-hint' | translate }}">
{{ 'gateway.retain' | translate }}
</mat-label>
</mat-slide-toggle>
</div>
</div>
</ng-template>
<ng-template [ngSwitchCase]="RequestTypeEnum.ATTRIBUTE_UPDATE">
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required"
tb-hint-tooltip-icon="{{ 'gateway.device-name-filter-hint' | translate }}" translate>
gateway.device-name-filter
</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="deviceNameFilter" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.device-name-filter-required') | translate"
*ngIf="mappingForm.get('requestValue.attributeUpdates.deviceNameFilter').hasError('required') &&
mappingForm.get('requestValue.attributeUpdates.deviceNameFilter').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" tb-hint-tooltip-icon="{{ 'gateway.attribute-filter-hint' | translate }}" translate>
gateway.attribute-filter
</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="attributeFilter" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.attribute-filter-required') | translate"
*ngIf="mappingForm.get('requestValue.attributeUpdates.attributeFilter').hasError('required') &&
mappingForm.get('requestValue.attributeUpdates.attributeFilter').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.response-value-expression</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="valueExpression" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.response-value-expression-required') | translate"
*ngIf="mappingForm.get('requestValue.attributeUpdates.valueExpression').hasError('required') &&
mappingForm.get('requestValue.attributeUpdates.valueExpression').touched"
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>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required" translate>gateway.response-topic-expression</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="topicExpression" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.response-topic-expression-required') | translate"
*ngIf="mappingForm.get('requestValue.attributeUpdates.topicExpression').hasError('required') &&
mappingForm.get('requestValue.attributeUpdates.topicExpression').touched"
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>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="retain">
<mat-label tb-hint-tooltip-icon="{{ 'gateway.retain-hint' | translate }}">
{{ 'gateway.retain' | translate }}
</mat-label>
</mat-slide-toggle>
</div>
</ng-template>
<ng-template [ngSwitchCase]="RequestTypeEnum.SERVER_SIDE_RPC">
<div class="tb-flex row center align-center no-gap fill-width">
<tb-toggle-select formControlName="type" appearance="fill">
<tb-toggle-option [value]="ServerSideRPCType.TWO_WAY">
{{ 'gateway.with-response' | translate }}
</tb-toggle-option>
<tb-toggle-option [value]="ServerSideRPCType.ONE_WAY">
{{ 'gateway.without-response' | translate }}
</tb-toggle-option>
</tb-toggle-select>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required" tb-hint-tooltip-icon="{{ 'gateway.device-name-filter-hint' | translate }}" translate>
gateway.device-name-filter
</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="deviceNameFilter" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.device-name-filter-required') | translate"
*ngIf="mappingForm.get('requestValue.serverSideRpc.deviceNameFilter').hasError('required') &&
mappingForm.get('requestValue.serverSideRpc.deviceNameFilter').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" tb-hint-tooltip-icon="{{ 'gateway.method-filter-hint' | translate }}" translate>
gateway.method-filter
</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="methodFilter" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.method-filter-required') | translate"
*ngIf="mappingForm.get('requestValue.serverSideRpc.methodFilter').hasError('required') &&
mappingForm.get('requestValue.serverSideRpc.methodFilter').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.request-topic-expression</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="requestTopicExpression" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.request-topic-expression-required') | translate"
*ngIf="mappingForm.get('requestValue.serverSideRpc.requestTopicExpression').hasError('required') &&
mappingForm.get('requestValue.serverSideRpc.requestTopicExpression').touched"
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>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required" translate>gateway.value-expression</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="valueExpression" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.value-expression-required') | translate"
*ngIf="mappingForm.get('requestValue.serverSideRpc.valueExpression').hasError('required') &&
mappingForm.get('requestValue.serverSideRpc.valueExpression').touched"
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>
<ng-container *ngIf="mappingForm.get('requestValue.serverSideRpc.type').value === ServerSideRPCType.TWO_WAY">
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required" translate>gateway.response-topic-expression</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="responseTopicExpression" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.response-topic-expression-required') | translate"
*ngIf="mappingForm.get('requestValue.serverSideRpc.responseTopicExpression').hasError('required') &&
mappingForm.get('requestValue.serverSideRpc.responseTopicExpression').touched"
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>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" tb-hint-tooltip-icon="{{ 'gateway.response-topic-Qos-hint' | translate }}" translate>
gateway.response-topic-Qos
</div>
<mat-form-field class="tb-flex" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="responseTopicQoS">
<mat-option *ngFor="let type of qualityTypes" [value]="type">
{{ QualityTranslationsMap.get(type) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required" translate>gateway.response-timeout</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="responseTimeout"
placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="responseTimeoutErrorTooltip"
*ngIf="(mappingForm.get('requestValue.serverSideRpc.responseTimeout').hasError('required') ||
mappingForm.get('requestValue.serverSideRpc.responseTimeout').hasError('min')) &&
mappingForm.get('requestValue.serverSideRpc.responseTimeout').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
</div>
</ng-container>
</ng-template>
</ng-container>
</ng-container>
</ng-template>
</ng-container>
</div>
</div>
<div mat-dialog-actions fxLayoutAlign="end center">
<button mat-button color="primary"
type="button"
cdkFocusInitial
(click)="cancel()">
{{ 'action.cancel' | translate }}
</button>
<button mat-raised-button color="primary"
(click)="add()"
[disabled]="mappingForm.invalid || !mappingForm.dirty || !keysPopupClosed">
{{ this.data.buttonTitle | translate }}
</button>
</div>
</div>

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

@ -0,0 +1,76 @@
/**
* 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 {
display: grid;
height: 100%;
.key-mapping {
max-width: 900px;
display: flex;
flex-direction: column;
.mat-toolbar {
min-height: 64px;
}
tb-toggle-select {
padding: 4px 0;
}
}
.mat-mdc-dialog-content {
max-height: 670px;
height: 670px;
}
.ellipsis-chips-container {
max-width: 70%;
}
}
:host ::ng-deep {
.key-mapping {
.mat-mdc-chip-listbox {
.mdc-evolution-chip-set__chips {
justify-content: flex-end;
align-items: center;
}
}
}
.tb-form-row {
.fixed-title-width {
min-width: 40px;
width: 35%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.mat-mdc-form-field {
width: 0;
}
}
.see-example {
width: 32px;
height: 32px;
margin: 4px;
}
.mat-mdc-form-field-icon-suffix {
display: flex;
}
}

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

@ -0,0 +1,360 @@
///
/// 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, Inject, OnDestroy, Renderer2, ViewContainerRef } from '@angular/core';
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 {
ConvertorType,
ConvertorTypeTranslationsMap,
DataConversionTranslationsMap,
DeviceInfoType,
MappingHintTranslationsMap,
MappingInfo,
MappingKeysAddKeyTranslationsMap,
MappingKeysDeleteKeyTranslationsMap,
MappingKeysNoKeysTextTranslationsMap,
MappingKeysPanelTitleTranslationsMap,
MappingKeysType,
MappingType,
MappingTypeTranslationsMap,
noLeadTrailSpacesRegex,
QualityTypes,
QualityTypeTranslationsMap,
RequestType,
RequestTypesTranslationsMap,
ServerSideRPCType,
SourceTypes,
SourceTypeTranslationsMap
} from '@home/components/widget/lib/gateway/gateway-widget.models';
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';
@Component({
selector: 'tb-mapping-dialog',
templateUrl: './mapping-dialog.component.html',
styleUrls: ['./mapping-dialog.component.scss'],
providers: [],
})
export class MappingDialogComponent extends DialogComponent<MappingDialogComponent, BaseData<HasId>> implements OnDestroy {
mappingForm: UntypedFormGroup;
MappingType = MappingType;
qualityTypes = QualityTypes;
QualityTranslationsMap = QualityTypeTranslationsMap;
convertorTypes = Object.values(ConvertorType);
ConvertorTypeEnum = ConvertorType;
ConvertorTypeTranslationsMap = ConvertorTypeTranslationsMap;
sourceTypes = Object.values(SourceTypes);
sourceTypesEnum = SourceTypes;
SourceTypeTranslationsMap = SourceTypeTranslationsMap;
requestTypes = Object.values(RequestType);
RequestTypeEnum = RequestType;
RequestTypesTranslationsMap = RequestTypesTranslationsMap;
DeviceInfoType = DeviceInfoType;
ServerSideRPCType = ServerSideRPCType;
MappingKeysType = MappingKeysType;
MappingHintTranslationsMap = MappingHintTranslationsMap;
MappingTypeTranslationsMap = MappingTypeTranslationsMap;
DataConversionTranslationsMap = DataConversionTranslationsMap;
hiddenAttributesCount = 0;
keysPopupClosed = true;
submitted = false;
private destroy$ = new Subject<void>();
constructor(protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: MappingInfo,
public dialogRef: MatDialogRef<MappingDialogComponent, {[key: string]: any}>,
private fb: FormBuilder,
private popoverService: TbPopoverService,
private renderer: Renderer2,
private viewContainerRef: ViewContainerRef,
private translate: TranslateService) {
super(store, router, dialogRef);
this.createMappingForm();
}
get converterAttributes(): Array<string> {
if (this.converterType) {
return this.mappingForm.get('converter').get(this.converterType).value.attributes.map(value => value.key);
}
}
get converterTelemetry(): Array<string> {
if (this.converterType) {
return this.mappingForm.get('converter').get(this.converterType).value.timeseries.map(value => value.key);
}
}
get converterType(): ConvertorType {
return this.mappingForm.get('converter').get('type').value;
}
get customKeys(): Array<string> {
return Object.keys(this.mappingForm.get('converter').get('custom').value.extensionConfig);
}
get requestMappingType(): RequestType {
return this.mappingForm.get('requestType').value;
}
get responseTimeoutErrorTooltip(): string {
const control = this.mappingForm.get('requestValue.serverSideRpc.responseTimeout');
if (control.hasError('required')) {
return this.translate.instant('gateway.response-timeout-required');
} else if (control.hasError('min')) {
return this.translate.instant('gateway.response-timeout-limits-error', {min: 1});
}
return '';
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
super.ngOnDestroy();
}
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());
}
}
helpLinkId(): string {
return 'https://thingsboard.io/docs/iot-gateway/config/mqtt/#section-mapping';
}
cancel(): void {
if (this.keysPopupClosed) {
this.dialogRef.close(null);
}
}
add(): void {
this.submitted = true;
if (this.mappingForm.valid) {
this.dialogRef.close(this.prepareMappingData());
}
}
manageKeys($event: Event, matButton: MatButton, keysType: MappingKeysType) {
if ($event) {
$event.stopPropagation();
}
const trigger = matButton._elementRef.nativeElement;
if (this.popoverService.hasPopover(trigger)) {
this.popoverService.hidePopover(trigger);
} else {
const keysControl = this.mappingForm.get('converter').get(this.converterType).get(keysType);
const ctx: any = {
keys: keysControl.value,
keysType: keysType,
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)
};
this.keysPopupClosed = false;
const dataKeysPanelPopover = this.popoverService.displayPopover(trigger, this.renderer,
this.viewContainerRef, MappingDataKeysPanelComponent, 'leftBottom', false, null,
ctx,
{},
{}, {}, true);
dataKeysPanelPopover.tbComponentRef.instance.popover = dataKeysPanelPopover;
dataKeysPanelPopover.tbComponentRef.instance.keysDataApplied.subscribe((keysData) => {
dataKeysPanelPopover.hide();
keysControl.patchValue(keysData);
keysControl.markAsDirty();
});
dataKeysPanelPopover.tbHideStart.subscribe(() => {
this.keysPopupClosed = true;
});
}
}
private prepareMappingData(): {[key: string]: any} {
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;
return {
topicFilter,
subscriptionQos,
converter: {
type: converter.type,
[converter.type]: { ...converter }
}
};
} else {
return {
requestType: this.data.value.requestType,
requestValue: {
[this.data.value.requestType]: this.data.value.requestValue
}
};
}
}
return this.data.value;
}
}

38
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-configuration.component.html

@ -80,6 +80,15 @@
<mat-error *ngIf="gatewayConfigGroup.get('thingsboard.security.accessToken').hasError('required')">
{{ 'security.access-token-required' | translate }}
</mat-error>
<tb-copy-button
matSuffix
miniButton="false"
*ngIf="gatewayConfigGroup.get('thingsboard.security.accessToken').value"
[copyText]="gatewayConfigGroup.get('thingsboard.security.accessToken').value"
tooltipText="{{ 'device.copy-access-token' | translate }}"
tooltipPosition="above"
icon="content_copy">
</tb-copy-button>
<mat-icon matIconSuffix style="cursor:pointer;"
matTooltip="{{ 'gateway.hints.token' | translate }}">info_outlined
</mat-icon>
@ -92,6 +101,15 @@
<mat-error *ngIf="gatewayConfigGroup.get('thingsboard.security.clientId').hasError('required')">
{{ 'security.clientId-required' | translate }}
</mat-error>
<tb-copy-button
matSuffix
miniButton="false"
*ngIf="gatewayConfigGroup.get('thingsboard.security.clientId').value"
[copyText]="gatewayConfigGroup.get('thingsboard.security.clientId').value"
tooltipText="{{ 'gateway.copy-client-id' | translate }}"
tooltipPosition="above"
icon="content_copy">
</tb-copy-button>
<mat-icon matIconSuffix style="cursor:pointer;"
matTooltip="{{ 'gateway.hints.client-id' | translate }}">info_outlined
</mat-icon>
@ -102,6 +120,15 @@
<mat-error *ngIf="gatewayConfigGroup.get('thingsboard.security.username').hasError('required')">
{{ 'security.username-required' | translate }}
</mat-error>
<tb-copy-button
matSuffix
miniButton="false"
*ngIf="gatewayConfigGroup.get('thingsboard.security.username').value"
[copyText]="gatewayConfigGroup.get('thingsboard.security.username').value"
tooltipText="{{ 'gateway.copy-username' | translate }}"
tooltipPosition="above"
icon="content_copy">
</tb-copy-button>
<mat-icon matIconSuffix style="cursor:pointer;"
matTooltip="{{ 'gateway.hints.username' | translate }}">info_outlined
</mat-icon>
@ -109,8 +136,17 @@
</div>
<mat-form-field appearance="outline" subscriptSizing="dynamic"
*ngIf="gatewayConfigGroup.get('thingsboard.security.type').value === 'usernamePassword'">
<mat-label translate>security.password</mat-label>
<mat-label translate>gateway.password</mat-label>
<input matInput formControlName="password"/>
<tb-copy-button
matSuffix
miniButton="false"
*ngIf="gatewayConfigGroup.get('thingsboard.security.password').value"
[copyText]="gatewayConfigGroup.get('thingsboard.security.password').value"
tooltipText="{{ 'gateway.copy-password' | translate }}"
tooltipPosition="above"
icon="content_copy">
</tb-copy-button>
<mat-icon matIconSuffix style="cursor:pointer;"
matTooltip="{{ 'gateway.hints.password' | translate }}">info_outlined
</mat-icon>

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

@ -97,5 +97,9 @@
color: #9E9E9E;
}
}
.mat-mdc-form-field-icon-suffix {
display: flex;
}
}

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

@ -22,13 +22,21 @@
<span fxFlex></span>
<button mat-icon-button
[disabled]="isLoading$ | async"
(click)="addAttribute()"
(click)="addConnector($event)"
matTooltip="{{ 'action.add' | translate }}"
matTooltipPosition="above">
<mat-icon>add</mat-icon>
</button>
</mat-toolbar>
<div class="table-container">
<section *ngIf="!dataSource?.data?.length" fxLayoutAlign="center center"
class="mat-headline-5 tb-absolute-fill tb-add-new">
<button mat-button class="connector"
(click)="addConnector($event)">
<mat-icon class="tb-mat-96">add</mat-icon>
<span>{{ 'gateway.add-connector' | translate }}</span>
</button>
</section>
<table mat-table [dataSource]="dataSource"
matSort [matSortActive]="pageLink.sortOrder.property" [matSortDirection]="pageLink.sortDirection()"
matSortDisableClear>
@ -74,6 +82,7 @@
<span class="dot"
matTooltip="{{ 'Errors: '+ getErrorsCount(attribute)}}"
matTooltipPosition="above"
(click)="connectorLogs(attribute, $event)"
[class]="{'hasErrors': +getErrorsCount(attribute) > 0,
'noErrors': +getErrorsCount(attribute) === 0 || getErrorsCount(attribute) === ''}"></span>
</mat-cell>
@ -119,7 +128,7 @@
<mat-icon>private_connectivity</mat-icon>
</button>
<button mat-icon-button
matTooltip="Delete connector"
matTooltip="Logs"
matTooltipPosition="above"
(click)="connectorLogs(attribute, $event)">
<mat-icon>list</mat-icon>
@ -137,49 +146,231 @@
<mat-header-row class="mat-row-select"
*matHeaderRowDef="displayedColumns; sticky: true"></mat-header-row>
<mat-row class="mat-row-select" [class]="{'tb-current-entity': isSameConnector(attribute)}"
*matRowDef="let attribute; columns: displayedColumns;" (click)="selectConnector(attribute)"></mat-row>
*matRowDef="let attribute; let i = index; columns: displayedColumns;" (click)="selectConnector($event, attribute)"></mat-row>
</table>
</div>
</section>
<section [formGroup]="connectorForm" class="tb-form-panel no-border no-padding flex">
<section class="tb-form-panel input-container section-container">
<section class="tb-form-row tb-standard-fields no-padding no-border column-lt-md input-container">
<mat-form-field class="flex" subscriptSizing="dynamic">
<mat-label>{{ 'gateway.connectors-table-name' | translate }}</mat-label>
<input matInput formControlName="name" #nameInput/>
</mat-form-field>
<mat-form-field class="flex" subscriptSizing="dynamic" hideRequiredMarker>
<mat-label>{{ 'gateway.connectors-table-type' | translate }}</mat-label>
<mat-select formControlName="type">
<mat-option style="text-transform: uppercase"
*ngFor="let type of gatewayConnectorDefaultTypes | keyvalue" [value]="type.key">{{ type.value }}</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="flex" *ngIf="connectorForm.get('type').value === 'grpc'" subscriptSizing="dynamic">
<mat-label>{{ 'gateway.connectors-table-key' | translate }}</mat-label>
<input matInput formControlName="key"/>
</mat-form-field>
<mat-form-field class="flex" *ngIf="connectorForm.get('type').value === 'custom'" subscriptSizing="dynamic">
<mat-label>{{ 'gateway.connectors-table-class' | translate }}</mat-label>
<input matInput formControlName="class"/>
</mat-form-field>
<mat-form-field class="flex" subscriptSizing="dynamic" hideRequiredMarker>
<mat-label translate>gateway.remote-logging-level</mat-label>
<mat-select formControlName="logLevel">
<mat-option *ngFor="let logLevel of gatewayLogLevel" [value]="logLevel">{{ logLevel }}</mat-option>
</mat-select>
</mat-form-field>
</section>
</section>
<section class="tb-form-panel flex section-container">
<tb-json-object-edit
fillHeight="true"
class="flex"
fxLayout="column"
jsonRequired
label="{{ 'gateway.configuration' | translate }}"
formControlName="configurationJson">
</tb-json-object-edit>
<section [formGroup]="connectorForm" class="tb-form-panel section-container flex">
<div class="tb-form-panel-title tb-flex no-flex space-between align-center">
<div class="tb-form-panel-title">
{{ 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-option [value]="connectorConfigurationModes.BASIC">
{{ 'gateway.basic' | translate }}
</tb-toggle-option>
<tb-toggle-option [value]="connectorConfigurationModes.ADVANCED">
{{ 'gateway.advanced' | translate }}
</tb-toggle-option>
</tb-toggle-select>
</div>
<span [fxShow]="!initialConnector"
fxLayoutAlign="center center"
class="no-data-found" translate>
gateway.select-connector
</span>
<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 name="value" formControlName="name" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.name-required') | translate"
*ngIf="connectorForm.get('name').hasError('required') && connectorForm.get('name').touched"
class="tb-error">
warning
</mat-icon>
</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>
</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"
class="tb-flex fill-height"
fxLayout="column"
jsonRequired
label="{{ 'gateway.configuration' | translate }}"
formControlName="configurationJson">
</tb-json-object-edit>
</mat-tab>
</ng-template>
</mat-tab-group>
<div fxLayoutAlign="end center">
<button mat-raised-button color="primary"
type="button"

45
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.scss

@ -104,3 +104,48 @@
background-color: rgb(25, 128, 56);
}
}
:host ::ng-deep {
.connector-container {
.mat-mdc-tab-group, .mat-mdc-tab-body-wrapper {
height: 100%;
}
.mat-mdc-tab-body.mat-mdc-tab-body-active {
position: absolute;
}
.tb-form-row {
.fixed-title-width {
min-width: 120px;
width: 30%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
.tb-add-new {
display: flex;
z-index: 999;
pointer-events: none;
background-color: #fff;
button.connector {
height: auto;
padding-right: 12px;
font-size: 20px;
border-style: dashed;
border-width: 2px;
border-radius: 8px;
display: flex;
flex-wrap: wrap;
justify-content: center;
align-items: center;
color: rgba(0, 0, 0, 0.38);
}
}
}
}

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

@ -17,11 +17,20 @@
import { AfterViewInit, ChangeDetectorRef, Component, ElementRef, Input, NgZone, ViewChild } from '@angular/core';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { FormBuilder, FormGroup, UntypedFormControl, ValidatorFn, Validators } from '@angular/forms';
import {
FormBuilder,
FormControl,
FormGroup,
FormGroupDirective,
NgForm,
UntypedFormControl,
ValidatorFn,
Validators
} from '@angular/forms';
import { EntityId } from '@shared/models/id/entity-id';
import { AttributeService } from '@core/http/attribute.service';
import { TranslateService } from '@ngx-translate/core';
import { forkJoin, Observable } from 'rxjs';
import { 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';
@ -33,34 +42,44 @@ import { MatTableDataSource } from '@angular/material/table';
import { ActionNotificationShow } from '@core/notification/notification.actions';
import { DialogService } from '@core/services/dialog.service';
import { WidgetContext } from '@home/models/widget-component.models';
import { camelCase, deepClone, isString } from '@core/utils';
import { camelCase, deepClone, generateSecret, isEqual, isString } from '@core/utils';
import { NULL_UUID } from '@shared/models/id/has-uuid';
import { IWidgetSubscription, WidgetSubscriptionOptions } from '@core/api/widget-api.models';
import { DatasourceType, widgetType } from '@shared/models/widget.models';
import { UtilsService } from '@core/services/utils.service';
import { EntityType } from '@shared/models/entity-type.models';
import {
AddConnectorConfigData,
ConnectorConfigurationModes,
ConnectorType,
GatewayConnector,
GatewayConnectorDefaultTypesTranslates,
GatewayLogLevel
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 { takeUntil } from 'rxjs/operators';
import { ErrorStateMatcher } from '@angular/material/core';
import { PageData } from '@shared/models/page/page-data';
export class ForceErrorStateMatcher implements ErrorStateMatcher {
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
return (control && control.invalid);
}
}
@Component({
selector: 'tb-gateway-connector',
templateUrl: './gateway-connectors.component.html',
providers: [{ provide: ErrorStateMatcher, useClass: ForceErrorStateMatcher }],
styleUrls: ['./gateway-connectors.component.scss']
})
export class GatewayConnectorComponent extends PageComponent implements AfterViewInit {
pageLink: PageLink;
dataSource: MatTableDataSource<AttributeData>;
displayedColumns = ['enabled', 'key', 'type', 'syncStatus', 'errors', 'actions'];
gatewayConnectorDefaultTypes = GatewayConnectorDefaultTypesTranslates;
@Input()
ctx: WidgetContext;
@ -70,6 +89,20 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
@ViewChild('nameInput') nameInput: ElementRef;
@ViewChild(MatSort, {static: false}) sort: MatSort;
pageLink: PageLink;
connectorType = ConnectorType;
dataSource: MatTableDataSource<AttributeData>;
displayedColumns = ['enabled', 'key', 'type', 'syncStatus', 'errors', 'actions'];
mqttVersions = MqttVersions;
gatewayConnectorDefaultTypes = GatewayConnectorDefaultTypesTranslatesMap;
connectorConfigurationModes = ConnectorConfigurationModes;
connectorForm: FormGroup;
textSearchMode: boolean;
@ -78,6 +111,14 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
gatewayLogLevel = Object.values(GatewayLogLevel);
mappingTypes = MappingType;
portLimits = PortLimits;
mode: ConnectorConfigurationModes = this.connectorConfigurationModes.BASIC;
initialConnector: GatewayConnector;
private inactiveConnectors: Array<string>;
private attributeDataSource: AttributeDatasource;
@ -92,7 +133,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
private sharedAttributeData: Array<AttributeData> = [];
private initialConnector: GatewayConnector;
private basicConfigSub: Subscription;
private subscriptionOptions: WidgetSubscriptionOptions = {
callbacks: {
@ -105,6 +146,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
}
};
private destroy$ = new Subject<void>();
private subscription: IWidgetSubscription;
constructor(protected store: Store<AppState>,
@ -112,6 +154,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
private translate: TranslateService,
private attributeService: AttributeService,
private dialogService: DialogService,
private dialog: MatDialog,
private telemetryWsService: TelemetryWebsocketService,
private zone: NgZone,
private utils: UtilsService,
@ -124,24 +167,39 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
this.serverDataSource = new AttributeDatasource(this.attributeService, this.telemetryWsService, this.zone, this.translate);
this.dataSource = new MatTableDataSource<AttributeData>([]);
this.connectorForm = this.fb.group({
name: ['', [Validators.required, this.uniqNameRequired()]],
mode: [ConnectorConfigurationModes.BASIC, []],
name: ['', [Validators.required, this.uniqNameRequired(), Validators.pattern(noLeadTrailSpacesRegex)]],
type: ['', [Validators.required]],
enableRemoteLogging: [false, []],
logLevel: ['', [Validators.required]],
sendDataOnlyOnChange: [false, []],
key: ['auto'],
class: [''],
configuration: [''],
configurationJson: [{}, [Validators.required]]
configurationJson: [{}, [Validators.required]],
basicConfig: this.fb.group({})
});
this.connectorForm.disable();
}
ngAfterViewInit() {
this.connectorForm.valueChanges.subscribe(() => {
this.cd.detectChanges();
});
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 '';
}
this.connectorForm.get('type').valueChanges.subscribe(type=> {
if(type && !this.initialConnector) {
ngAfterViewInit() {
this.connectorForm.get('type').valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe(type => {
if (type && !this.initialConnector) {
this.attributeService.getEntityAttributes(this.device, AttributeScope.CLIENT_SCOPE,
[`${type.toUpperCase()}_DEFAULT_CONFIG`], {ignoreErrors: true}).subscribe(defaultConfig=>{
if (defaultConfig && defaultConfig.length) {
@ -155,6 +213,29 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
}
});
this.connectorForm.get('name').valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe((name) => {
if (this.connectorForm.get('type').value === ConnectorType.MQTT) {
this.connectorForm.get('basicConfig').get('broker.name')?.setValue(name);
}
});
this.connectorForm.get('configurationJson').valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe((config) => {
const basicConfig = this.connectorForm.get('basicConfig');
const type = this.connectorForm.get('type').value;
const mode = this.connectorForm.get('mode').value;
if (
!isEqual(config, basicConfig?.value) &&
type === ConnectorType.MQTT &&
mode === ConnectorConfigurationModes.ADVANCED
) {
this.connectorForm.get('basicConfig').patchValue(config, {emitEvent: false});
}
});
this.dataSource.sort = this.sort;
this.dataSource.sortingDataAccessor = (data: AttributeData, sortHeaderId: string) => {
if (sortHeaderId === 'syncStatus') {
@ -211,9 +292,16 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
};
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
super.ngOnDestroy();
}
saveConnector(): void {
const value = this.connectorForm.value;
value.configuration = camelCase(value.name) + '.json';
delete value.basicConfig;
if (value.type !== ConnectorType.GRPC) {
delete value.key;
}
@ -268,16 +356,18 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
);
this.initialConnector = value;
this.updateData(true);
this.connectorForm.markAsPristine();
});
}
private updateData(reload: boolean = false) {
private updateData(reload: boolean = false): void {
this.pageLink.sortOrder.property = this.sort.active;
this.pageLink.sortOrder.direction = Direction[this.sort.direction.toUpperCase()];
this.attributeDataSource.loadAttributes(this.device, AttributeScope.CLIENT_SCOPE, this.pageLink, reload).subscribe(data => {
this.activeData = data.data.filter(value => this.activeConnectors.includes(value.key));
this.combineData();
this.generateSubscription();
this.setClientData(data);
});
this.inactiveConnectorsDataSource.loadAttributes(this.device, AttributeScope.SHARED_SCOPE, this.pageLink, reload).subscribe(data => {
this.sharedAttributeData = data.data.filter(value => this.activeConnectors.includes(value.key));
@ -308,7 +398,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
return sharedIndex !== -1;
}
private combineData() {
private combineData(): void {
this.dataSource.data = [...this.activeData, ...this.inactiveData, ...this.sharedAttributeData].filter((item, index, self) =>
index === self.findIndex((t) => t.key === item.key)
).map(attribute => {
@ -317,42 +407,37 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
});
}
addAttribute(): void {
if (this.connectorForm.disabled) {
this.connectorForm.enable();
}
this.nameInput.nativeElement.focus();
this.clearOutConnectorForm();
}
private clearOutConnectorForm(): void {
this.initialConnector = null;
this.connectorForm.setControl('basicConfig', this.fb.group({}), {emitEvent: false});
this.connectorForm.setValue({
mode: ConnectorConfigurationModes.BASIC,
name: '',
type: ConnectorType.MQTT,
sendDataOnlyOnChange: false,
enableRemoteLogging: false,
logLevel: GatewayLogLevel.INFO,
key: 'auto',
class: '',
configuration: '',
configurationJson: {}
});
configurationJson: {},
basicConfig: {}
}, {emitEvent: false});
this.connectorForm.markAsPristine();
}
selectConnector(attribute: AttributeData): void {
if (this.connectorForm.disabled) {
this.connectorForm.enable();
selectConnector($event: Event, attribute: AttributeData): void {
if ($event) {
$event.stopPropagation();
}
const connector = attribute.value;
if (!connector.configuration) {
connector.configuration = '';
}
if (!connector.key) {
connector.key = 'auto';
if (connector?.name !== this.initialConnector?.name) {
this.confirmConnectorChange().subscribe((result) => {
if (result) {
this.setFormValue(connector);
}
});
}
this.initialConnector = connector;
this.connectorForm.patchValue(connector);
this.connectorForm.markAsPristine();
}
isSameConnector(attribute: AttributeData): boolean {
@ -363,7 +448,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
return this.initialConnector.name === connector.name;
}
showToast(message: string) {
showToast(message: string): void {
this.store.dispatch(new ActionNotificationShow(
{
message,
@ -385,15 +470,14 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
if ($event) {
$event.stopPropagation();
}
this.initialConnector = attribute.value;
const title = `Delete connector ${attribute.key}?`;
const title = `Delete connector \"${attribute.key}\"?`;
const content = `All connector data will be deleted.`;
this.dialogService.confirm(title, content, 'Cancel', 'Delete').subscribe(result => {
if (result) {
const tasks: Array<Observable<any>> = [];
const scope = (this.initialConnector && this.activeConnectors.includes(this.initialConnector.name))
? AttributeScope.SHARED_SCOPE
: AttributeScope.SERVER_SCOPE;
const scope = this.activeConnectors.includes(attribute.value?.name) ?
AttributeScope.SHARED_SCOPE :
AttributeScope.SERVER_SCOPE;
tasks.push(this.attributeService.deleteEntityAttributes(this.device, scope, [attribute]));
const activeIndex = this.activeConnectors.indexOf(attribute.key);
const inactiveIndex = this.inactiveConnectors.indexOf(attribute.key);
@ -467,7 +551,55 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
});
}
private onDataUpdateError(e: any) {
getErrorsCount(attribute: AttributeData): string {
const connectorName = attribute.key;
const connector = this.subscription && this.subscription.data
.find(data => data && data.dataKey.name === `${connectorName}_ERRORS_COUNT`);
return (connector && this.activeConnectors.includes(connectorName)) ? (connector.data[0][1] || 0) : 'Inactive';
}
addConnector($event: Event) {
if ($event) {
$event.stopPropagation();
}
this.confirmConnectorChange().subscribe((changeConfirmed) => {
if (changeConfirmed) {
return this.dialog.open<AddConnectorDialogComponent,
AddConnectorConfigData>(AddConnectorDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
dataSourceData: this.dataSource.data
}
}).afterClosed().subscribe((value) => {
if (value && changeConfirmed) {
this.initialConnector = null;
if (this.connectorForm.disabled) {
this.connectorForm.enable();
}
if (!value.configurationJson) {
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.generate('basicConfig.broker.clientId');
this.saveConnector();
}
});
}
});
}
generate(formControlName: string): void {
this.connectorForm.get(formControlName)?.patchValue('tb_gw_' + generateSecret(5));
}
private onDataUpdateError(e: any): void {
const exceptionData = this.utils.parseException(e);
let errorText = exceptionData.name;
if (exceptionData.message) {
@ -476,11 +608,11 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
console.error(errorText);
}
private onDataUpdated() {
private onDataUpdated(): void {
this.cd.detectChanges();
}
private generateSubscription() {
private generateSubscription(): void {
if (this.subscription) {
this.subscription.unsubscribe();
}
@ -502,10 +634,101 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
}
}
getErrorsCount(attribute: AttributeData): string {
const connectorName = attribute.key;
const connector = this.subscription && this.subscription.data
.find(data => data && data.dataKey.name === `${connectorName}_ERRORS_COUNT`);
return (connector && this.activeConnectors.includes(connectorName)) ? (connector.data[0][1] || 0) : 'Inactive';
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();
}
this.basicConfigSub = this.connectorForm.get('basicConfig').valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe((config) => {
const configJson = this.connectorForm.get('configurationJson');
const type = this.connectorForm.get('type').value;
const mode = this.connectorForm.get('mode').value;
if (
!isEqual(config, configJson?.value) &&
type === ConnectorType.MQTT &&
mode === ConnectorConfigurationModes.BASIC
) {
const newConfig = { ...configJson.value, ...config };
this.connectorForm.get('configurationJson').patchValue(newConfig, {emitEvent: false});
}
});
}
private confirmConnectorChange(): Observable<boolean> {
if (this.initialConnector && this.connectorForm.dirty) {
return this.dialogService.confirm(
this.translate.instant('gateway.change-connector-title'),
this.translate.instant('gateway.change-connector-text'),
this.translate.instant('action.no'),
this.translate.instant('action.yes'),
true
);
}
return of(true);
}
private setFormValue(connector: GatewayConnector): void {
if (this.connectorForm.disabled) {
this.connectorForm.enable();
}
if (!connector.configuration) {
connector.configuration = '';
}
if (!connector.key) {
connector.key = 'auto';
}
if (!connector.configurationJson) {
connector.configurationJson = {};
}
connector.basicConfig = connector.configurationJson;
this.initialConnector = connector;
if (connector.type === ConnectorType.MQTT) {
this.addMQTTConfigControls();
} else {
this.connectorForm.setControl('basicConfig', this.fb.group({}), {emitEvent: false});
}
this.connectorForm.patchValue(connector, {emitEvent: false});
this.connectorForm.markAsPristine();
}
private setClientData(data: PageData<AttributeData>): void {
if (this.initialConnector) {
const clientConnectorData = data.data.find(attr => attr.key === this.initialConnector.name);
if (clientConnectorData) {
clientConnectorData.value = typeof clientConnectorData.value === 'string' ?
JSON.parse(clientConnectorData.value) : clientConnectorData.value;
if (this.isConnectorSynced(clientConnectorData) && clientConnectorData.value.configurationJson) {
this.setFormValue(clientConnectorData.value);
}
}
}
}
}

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

@ -33,10 +33,11 @@ import {
BLEMethodsTranslates,
CANByteOrders,
ConnectorType,
GatewayConnectorDefaultTypesTranslates,
GatewayConnectorDefaultTypesTranslatesMap,
HTTPMethods,
ModbusCodesTranslate,
ModbusCommandTypes,
noLeadTrailSpacesRegex,
RPCCommand,
RPCTemplateConfig,
SNMPMethods,
@ -53,8 +54,6 @@ import {
import { jsonRequired } from '@shared/components/json-object-edit.component';
import { deepClone } from '@core/utils';
export const noLeadTrailSpacesRegex: RegExp = /^(?! )[\S\s]*(?<! )$/;
@Component({
selector: 'tb-gateway-service-rpc-connector',
templateUrl: './gateway-service-rpc-connector.component.html',
@ -97,7 +96,7 @@ export class GatewayServiceRPCConnectorComponent implements OnInit, ControlValue
bLEMethodsTranslates = BLEMethodsTranslates;
SocketMethodProcessingsTranslates = SocketMethodProcessingsTranslates;
SNMPMethodsTranslations = SNMPMethodsTranslations;
gatewayConnectorDefaultTypesTranslates = GatewayConnectorDefaultTypesTranslates;
gatewayConnectorDefaultTypesTranslates = GatewayConnectorDefaultTypesTranslatesMap;
modbusCodesTranslate = ModbusCodesTranslate;
urlPattern = /^[-a-zA-Zd_$:{}?~+=\/.0-9-]*$/;

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

@ -14,6 +14,13 @@
/// limitations under the License.
///
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]*(?<! )$/;
export enum StorageTypes {
MEMORY = 'memory',
FILE = 'file',
@ -33,6 +40,11 @@ export enum GatewayLogLevel {
DEBUG = 'DEBUG'
}
export enum PortLimits {
MIN = 1,
MAX = 65535
}
export const GatewayStatus = {
...GatewayLogLevel,
...DeviceGatewayStatus
@ -93,18 +105,19 @@ export const GecurityTypesTranslationsMap = new Map<SecurityTypes, string>(
[
[SecurityTypes.ACCESS_TOKEN, 'gateway.security-types.access-token'],
[SecurityTypes.USERNAME_PASSWORD, 'gateway.security-types.username-password'],
[SecurityTypes.TLS_ACCESS_TOKEN, 'gateway.security-types.tls-access-token'],
// [SecurityTypes.TLS_PRIVATE_KEY, 'gateway.security-types.tls-private-key'],
[SecurityTypes.TLS_ACCESS_TOKEN, 'gateway.security-types.tls-access-token']
]
);
export interface GatewayConnector {
name: string;
type: string;
type: ConnectorType;
configuration?: string;
configurationJson: string;
configurationJson: string | {[key: string]: any};
basicConfig?: string | {[key: string]: any};
logLevel: string;
key?: string;
class?: string;
}
export enum ConnectorType {
@ -127,7 +140,7 @@ export enum ConnectorType {
CUSTOM = 'custom'
}
export const GatewayConnectorDefaultTypesTranslates = new Map<ConnectorType, string>([
export const GatewayConnectorDefaultTypesTranslatesMap = new Map<ConnectorType, string>([
[ConnectorType.MQTT, 'MQTT'],
[ConnectorType.MODBUS, 'MODBUS'],
[ConnectorType.GRPC, 'GRPC'],
@ -303,3 +316,227 @@ export interface GatewayLogData {
message: string;
status: GatewayStatus;
}
export interface AddConnectorConfigData {
dataSourceData: Array<any>
}
export interface CreatedConnectorConfigData {
type: ConnectorType,
name: string,
logLevel: GatewayLogLevel,
useDefaults: boolean,
sendDataOnlyOnChange: boolean,
configurationJson?: {[key: string]: any}
}
export interface MappingDataKey {
key: string,
value: any,
type: MappingValueType
}
export interface MappingInfo {
mappingType: MappingType,
value: {[key: string]: any},
buttonTitle: string
}
export enum ConnectorConfigurationModes {
BASIC = 'basic',
ADVANCED = 'advanced'
}
export enum BrokerSecurityType {
ANONYMOUS = 'anonymous',
BASIC = 'basic',
CERTIFICATES = 'certificates'
}
export const BrokerSecurityTypeTranslationsMap = new Map<BrokerSecurityType, string>(
[
[BrokerSecurityType.ANONYMOUS, 'gateway.broker.security-types.anonymous'],
[BrokerSecurityType.BASIC, 'gateway.broker.security-types.basic'],
[BrokerSecurityType.CERTIFICATES, 'gateway.broker.security-types.certificates']
]
);
export const MqttVersions = [
{ name: 3.1, value: 3 },
{ name: 3.11, value: 4 },
{ name: 5, value: 5 }
];
export enum MappingType {
DATA = 'data',
REQUESTS = 'requests'
}
export const MappingTypeTranslationsMap = new Map<MappingType, string>(
[
[MappingType.DATA, 'gateway.data-mapping'],
[MappingType.REQUESTS, 'gateway.requests-mapping']
]
);
export const MappingHintTranslationsMap = new Map<MappingType, string>(
[
[MappingType.DATA, 'gateway.data-mapping-hint'],
[MappingType.REQUESTS, 'gateway.requests-mapping-hint']
]
);
export const QualityTypes = [0, 1 ,2];
export const QualityTypeTranslationsMap = new Map<number, string>(
[
[0, 'gateway.qos.at-most-once'],
[1, 'gateway.qos.at-least-once'],
[2, 'gateway.qos.exactly-once']
]
);
export enum ConvertorType {
JSON = 'json',
BYTES = 'bytes',
CUSTOM = 'custom'
}
export const ConvertorTypeTranslationsMap = new Map<ConvertorType, string>(
[
[ConvertorType.JSON, 'gateway.JSON'],
[ConvertorType.BYTES, 'gateway.bytes'],
[ConvertorType.CUSTOM, 'gateway.custom']
]
);
export enum SourceTypes {
MSG = 'message',
TOPIC = 'topic',
CONST = 'constant'
}
export enum DeviceInfoType {
FULL = 'full',
PARTIAL = 'partial'
}
export const SourceTypeTranslationsMap = new Map<SourceTypes, string>(
[
[SourceTypes.MSG, 'gateway.source-type.msg'],
[SourceTypes.TOPIC, 'gateway.source-type.topic'],
[SourceTypes.CONST, 'gateway.source-type.const'],
]
);
export enum RequestType {
CONNECT_REQUEST = 'connectRequests',
DISCONNECT_REQUEST = 'disconnectRequests',
ATTRIBUTE_REQUEST = 'attributeRequests',
ATTRIBUTE_UPDATE = 'attributeUpdates',
SERVER_SIDE_RPC = 'serverSideRpc'
}
export const RequestTypesTranslationsMap = new Map<RequestType, string>(
[
[RequestType.CONNECT_REQUEST, 'gateway.request.connect-request'],
[RequestType.DISCONNECT_REQUEST, 'gateway.request.disconnect-request'],
[RequestType.ATTRIBUTE_REQUEST, 'gateway.request.attribute-request'],
[RequestType.ATTRIBUTE_UPDATE, 'gateway.request.attribute-update'],
[RequestType.SERVER_SIDE_RPC, 'gateway.request.rpc-connection'],
]
);
export enum MappingKeysType {
ATTRIBUTES = 'attributes',
TIMESERIES = 'timeseries',
CUSTOM = 'extensionConfig'
}
export const MappingKeysPanelTitleTranslationsMap = new Map<MappingKeysType, string>(
[
[MappingKeysType.ATTRIBUTES, 'gateway.attributes'],
[MappingKeysType.TIMESERIES, 'gateway.timeseries'],
[MappingKeysType.CUSTOM, 'gateway.keys']
]
);
export const MappingKeysAddKeyTranslationsMap = new Map<MappingKeysType, string>(
[
[MappingKeysType.ATTRIBUTES, 'gateway.add-attribute'],
[MappingKeysType.TIMESERIES, 'gateway.add-timeseries'],
[MappingKeysType.CUSTOM, 'gateway.add-key']
]
);
export const MappingKeysDeleteKeyTranslationsMap = new Map<MappingKeysType, string>(
[
[MappingKeysType.ATTRIBUTES, 'gateway.delete-attribute'],
[MappingKeysType.TIMESERIES, 'gateway.delete-timeseries'],
[MappingKeysType.CUSTOM, 'gateway.delete-key']
]
);
export const MappingKeysNoKeysTextTranslationsMap = new Map<MappingKeysType, string>(
[
[MappingKeysType.ATTRIBUTES, 'gateway.no-attributes'],
[MappingKeysType.TIMESERIES, 'gateway.no-timeseries'],
[MappingKeysType.CUSTOM, 'gateway.no-keys']
]
);
export enum ServerSideRPCType {
ONE_WAY = 'oneWay',
TWO_WAY = 'twoWay'
}
export const getDefaultConfig = (resourcesService: ResourcesService, type: string): Observable<any> =>
resourcesService.loadJsonResource(`/assets/metadata/connector-default-configs/${type}.json`);
export enum MappingValueType {
STRING = 'string',
INTEGER = 'integer',
DOUBLE = 'double',
BOOLEAN = 'boolean'
}
export const mappingValueTypesMap = new Map<MappingValueType, ValueTypeData>(
[
[
MappingValueType.STRING,
{
name: 'value.string',
icon: 'mdi:format-text'
}
],
[
MappingValueType.INTEGER,
{
name: 'value.integer',
icon: 'mdi:numeric'
}
],
[
MappingValueType.DOUBLE,
{
name: 'value.double',
icon: 'mdi:numeric'
}
],
[
MappingValueType.BOOLEAN,
{
name: 'value.boolean',
icon: 'mdi:checkbox-marked-outline'
}
]
]
);
export const DataConversionTranslationsMap = new Map<ConvertorType, string>(
[
[ConvertorType.JSON, 'gateway.JSON-hint'],
[ConvertorType.BYTES, 'gateway.bytes-hint'],
[ConvertorType.CUSTOM, 'gateway.custom-hint']
]
);

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

@ -86,6 +86,13 @@ import { PowerButtonWidgetComponent } from '@home/components/widget/lib/rpc/powe
import { SliderWidgetComponent } from '@home/components/widget/lib/rpc/slider-widget.component';
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';
import { PieChartWidgetComponent } from '@home/components/widget/lib/chart/pie-chart-widget.component';
@ -117,6 +124,12 @@ import { RadarChartWidgetComponent } from '@home/components/widget/lib/chart/rad
LegendComponent,
FlotWidgetComponent,
GatewayConnectorComponent,
AddConnectorDialogComponent,
MappingTableComponent,
MappingDialogComponent,
DeviceInfoTableComponent,
MappingDataKeysPanelComponent,
BrokerSecurityComponent,
GatewayLogsComponent,
GatewayStatisticsComponent,
GatewayServiceRPCComponent,
@ -126,6 +139,7 @@ import { RadarChartWidgetComponent } from '@home/components/widget/lib/chart/rad
GatewayConfigurationComponent,
GatewayRemoteConfigurationDialogComponent,
GatewayServiceRPCConnectorTemplateDialogComponent,
EllipsisChipListDirective,
ValueCardWidgetComponent,
AggregatedValueCardWidgetComponent,
CountWidgetComponent,
@ -180,9 +194,16 @@ import { RadarChartWidgetComponent } from '@home/components/widget/lib/chart/rad
LegendComponent,
FlotWidgetComponent,
GatewayConnectorComponent,
AddConnectorDialogComponent,
MappingTableComponent,
MappingDialogComponent,
DeviceInfoTableComponent,
MappingDataKeysPanelComponent,
BrokerSecurityComponent,
GatewayLogsComponent,
GatewayServiceRPCConnectorComponent,
GatewayServiceRPCConnectorTemplatesComponent,
EllipsisChipListDirective,
GatewayStatisticsComponent,
GatewayServiceRPCComponent,
DeviceGatewayCommandComponent,

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

@ -17,13 +17,15 @@
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
gap: 4px;
}
:host {
.tb-hint-tooltip-icon {
color: #E0E0E0;
overflow: visible;
order: 3;
margin-left: 4px;
&:hover {
color: #9E9E9E;
}

96
ui-ngx/src/assets/help/en_US/widget/lib/gateway/expressions_fn.md

@ -0,0 +1,96 @@
### Expressions
#### JSON Path:
The expression field is used to extract data from the MQTT message. There are various available options for different parts of the messages:
- The JSONPath format can be used to extract data from the message body.
- The regular expression format can be used to extract data from the topic where the message will arrive.
- Slices can only be used in the expression fields of bytes converters.
JSONPath expressions specify the items within a JSON structure (which could be an object, array, or nested combination of both) that you want to access. These expressions can select elements from JSON data on specific criteria. Here's a basic overview of how JSONPath expressions are structured:
- `$`: The root element of the JSON document;
- `.`: Child operator used to select child elements. For example, $.store.book ;
- `[]`: Child operator used to select child elements. $['store']['book'] accesses the book array within a store object;
##### Examples:
For example, if we want to extract the device name from the following message, we can use the expression below:
MQTT message:
```
{
"sensorModelInfo": {
"sensorName": "AM-123",
"sensorType": "myDeviceType"
},
"data": {
"temp": 12.2,
"hum": 56,
"status": "ok"
}
}
{:copy-code}
```
Expression:
`${sensorModelInfo.sensorName}`
Converted data:
`AM-123`
If we want to extract all data from the message above, we can use the following expression:
`${data}`
Converted data:
`{"temp": 12.2, "hum": 56, "status": "ok"}`
Or if we want to extract specific data (for example “temperature”), you can use the following expression:
`${data.temp}`
And as a converted data we will get:
`12.2`
<br/>
#### Regular expressions for topic:
Device name or device profile can be parsed from the MQTT topic using regular expression. A regular expression, often abbreviated as regex or regexp, is a sequence of characters that forms a search pattern, primarily used for string matching and manipulation.
##### Regular expression for topic examples:
| Topic | Regular expression | Output data | Description |
|:---------------------------|----------------------------------|--------------------------|--------------------------------------|
| /devices/AM123/mytype/data | /devices/([^/]+)/mytype/data | AM123 | Getting device name from topic |
| /devices/AM123/mytype/data | /devices/[A-Z0-9]+/([^/]+)/data | mytype | Getting device profile from topic |
<br/>
#### Bytes converter:
For bytes converter, expression fields can use slices format only. A slice specifies how to slice a sequence, determining the start point, and the endpoint. Here's a basic overview of slice components:
- `start`: The starting index of the slice. It is included in the slice. If omitted, slicing starts at the beginning of the sequence. Indexing starts at 0, so the first element of the sequence is at index 0.
- `stop`: The ending index of the slice. It is excluded from the slice, meaning the slice will end just before this index. If omitted, slicing goes through the end of the sequence.
##### Bytes parsing examples:
| Message body | Slice | Output data | Description |
|:-----------------------|-----------------|--------------------------|------------------------------|
| AM123,mytype,12.2,45 | [:5] | AM123 | Extracting device name |
| AM123,mytype,12.2,45 | [:] | AM123,mytype,12.2,45 | Extracting all data |
| AM123,mytype,12.2,45 | [18:] | 45 | Extracting humidity value |
| AM123,mytype,12.2,45 | [13:17] | 12.2 | Extracting temperature value |

15
ui-ngx/src/assets/help/en_US/widget/lib/gateway/topic-filter_fn.md

@ -0,0 +1,15 @@
### Topic filter
The `topicFilter` supports special symbols: `#` and `+`, allowing to subscribe to multiple topics.
Also, the MQTT connector supports shared subscriptions. To create a shared subscription you need to add `$share/`
as a prefix for the topic filter and shared subscription group name. For example to subscribe to the my-shared-topic
in group **my-group-name** you can set the topic filter to “$share/**my-group-name**/my-shared-topic”.
#### Examples:
| Topic | Topic filter | Payload | Description |
|:-----------------------|----------------------------------|---------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------|
| sensor/data | sensor/data | `{"serialNumber": "SN-001", "sensorType": "Thermometer", "sensorModel": "T1000", "temp": 42, "hum": 58}` | Device Name is part of the payload, to extract it you need to use JSON Path expression |
| sensor/SN-001/data | sensor/+/data | `{"sensorType": "Thermometer", "sensorModel": "T1000", "temp": 42, "hum": 58}` | Device Name is part of the topic, to extract it you need to use regular expression. |

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

@ -2709,11 +2709,38 @@
},
"gateway": {
"add-entry": "Add configuration",
"add-attribute": "Add attribute",
"add-key": "Add key",
"add-timeseries": "Add time series",
"add-mapping": "Add mapping",
"advanced": "Advanced",
"attributes": "Attributes",
"attribute-filter": "Attribute filter",
"attribute-filter-hint": "Filter for incoming attribute name from platform, supports regular expression.",
"attribute-filter-required": "Attribute filter required.",
"attribute-name-expression": "Attribute name expression",
"attribute-name-expression-required": "Attribute name expression required.",
"attribute-name-expression-hint": "Hint for Attribute name expression",
"basic": "Basic",
"broker": {
"connection": "Connection to broker",
"name": "Broker name",
"name-required": "Broker name required.",
"security-types": {
"anonymous": "Anonymous",
"basic": "Basic",
"certificates": "Certificates"
}
},
"CA-certificate-path": "Path to CA certificate file",
"path-to-CA-cert-required": "Path to CA certificate file is required.",
"change-connector-title": "Confirm connector change",
"change-connector-text": "Switching connectors will discard any unsaved changes. Continue?",
"checking-device-activity": "Checking device activity",
"command": "Docker commands",
"command-copied-message": "Docker command has been copied to clipboard",
"configuration": "Configuration",
"add-connector": "Add connector",
"connector-add": "Add new connector",
"connector-enabled": "Enable connector",
"connector-name": "Connector name",
@ -2729,6 +2756,9 @@
"connectors-table-actions": "Actions",
"connectors-table-key": "Key",
"connectors-table-class": "Class",
"copy-username": "Copy username",
"copy-password": "Copy password",
"copy-client-id": "Copy client ID",
"connector-created": "Connector created.",
"connector-updated": "Connector updated.",
"rpc-command-save-template": "Save Template",
@ -2738,10 +2768,30 @@
"gateway-configuration": "General Configuration",
"docker-label": "Use the following instruction to run IoT Gateway in Docker compose with credentials for selected device",
"install-docker-compose": "Use the instructions to download, install and setup docker compose",
"device-info-settings": "Device info settings",
"device-info": {
"entity-field": "Entity field",
"source": "Source",
"expression": "Value / Expression",
"expression-hint": "Show help",
"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-filter": "Device name filter",
"device-name-filter-hint": "This field supports Regular expressions to filter incoming data by device name.",
"device-name-filter-required": "Device name filter is required.",
"details": "Details",
"delete-mapping-title": "Delete mapping ?",
"download-configuration-file": "Download configuration file",
"download-docker-compose": "Download docker-compose.yml for your gateway",
"enable-remote-logging": "Enable remote logging",
"ellipsis-chips-text": "+ {{count}} more",
"launch-gateway": "Launch gateway",
"launch-docker-compose": "Start the gateway using the following command in the terminal from folder with docker-compose.yml file",
"logs-configuration": "Logs configuration",
"create-new-gateway": "Create a new gateway",
"create-new-gateway-text": "Are you sure you want create a new gateway with name: '{{gatewayName}}'?",
"created-time": "Created time",
@ -2750,14 +2800,46 @@
"configuration-delete-dialog-input": "Gateway name",
"configuration-delete-dialog-input-required": "Gateway name is mandatory",
"configuration-delete-dialog-confirm": "Turn Off",
"connector-duplicate-name": "Connector with such name already exists.",
"connector-side": "Connector side",
"payload-type": "Payload type",
"platform-side": "Platform side",
"JSON": "JSON",
"JSON-hint": "Converter for this payload type processes MQTT messages in JSON format. It uses JSON Path expressions to extract vital details such as device names, device profile names, attributes, and time series from the message. And regular expressions to get device details from topics.",
"bytes": "Bytes",
"bytes-hint": "Converter for this payload type designed for binary MQTT payloads, this converter directly interprets binary data to retrieve device names and device profile names, along with attributes and time series, using specific byte positions for data extraction.",
"custom": "Custom",
"custom-hint": "This option allows you to use a custom converter for specific data tasks. You need to add your custom converter to the extension folder and enter its class name in the UI settings. Any keys you provide will be sent as configuration to your custom converter.",
"client-cert-path": "Path to client certificate file",
"path-to-client-cert-required": "Path to client certificate file is required.",
"client-id": "Client ID",
"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.",
"delete": "Delete configuration",
"delete-attribute": "Delete attribute",
"delete-key": "Delete key",
"delete-timeseries": "Delete time series",
"default": "Default",
"download-tip": "Download configuration file",
"drop-file": "Drop file here or",
"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.",
"extension-configuration": "Extension configuration",
"extension-configuration-hint": "Configuration for convertor",
"fill-connector-defaults": "Fill configuration with default values",
"fill-connector-defaults-hint": "This property allows to fill connector configuration with default values on it's creation.",
"from-device-request-settings": "Input request parsing",
"from-device-request-settings-hint": "These fields support JSONPath expressions to extract a name from incoming message.",
"to-device-response-settings": "Output request processing",
"to-device-response-settings-hint": "For these fields you can use the following variables and they will be replaced with actual values: ${deviceName}, ${attributeKey}, ${attributeValue}",
"gateway": "Gateway",
"gateway-exists": "Device with same name is already exists.",
"gateway-name": "Gateway name",
"gateway-name-required": "Gateway name is required.",
"gateway-saved": "Gateway configuration successfully saved.",
"generate-client-id": "Generate Client ID",
"grpc": "GRPC",
"grpc-keep-alive-timeout": "Keep alive timeout (in ms)",
"grpc-keep-alive-timeout-required": "Keep alive timeout is required",
@ -2787,8 +2869,11 @@
"inactivity-timeout-seconds-required": "Inactivity timeout is required",
"inactivity-timeout-seconds-min": "Inactivity timeout can not be less then 1",
"inactivity-timeout-seconds-pattern": "Inactivity timeout is not valid",
"host": "Host",
"host-required": "Host is required.",
"json-parse": "Not valid JSON.",
"json-required": "Field cannot be empty.",
"JSONPath-hint": "This field supports constants and JSONPath expressions.",
"logs": {
"logs": "Logs",
"days": "days",
@ -2813,16 +2898,39 @@
"backup-count-min": "Backup count can not be less then 1",
"backup-count-required": "Backup count required"
},
"max-number-of-workers": "Max number of workers",
"max-number-of-workers-hint": "Maximal number of workers threads for converters \n(The amount of workers changes dynamically, depending on load) \nRecommended amount 50-150.",
"max-number-of-workers-required": "Max number of workers is required.",
"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.",
"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",
"mqtt-version": "MQTT version",
"name": "Name",
"name-required": "Name is required.",
"no-attributes": "No attributes",
"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",
"path-hint": "The path is local to the gateway file system",
"path-logs": "Path to log files",
"path-logs-required": "Path is required.",
"password": "Password",
"password-required": "Password is required.",
"permit-without-calls": "Keep alive permit without calls",
"port": "Port",
"port-required": "Port is required.",
"port-limits-error": "Port should be number from {{min}} to {{max}}.",
"private-key-path": "Path to private key file",
"path-to-private-key-required": "Path to private key file is required.",
"raw": "Raw",
"retain": "Retain",
"retain-hint": "This flag tells the broker to store the message for a topic\nand ensures any new client subscribing to that topic\nwill receive the stored message.",
"remote": "Remote configuration",
"remote-logging-level": "Logging level",
"remove-entry": "Remove configuration",
@ -2918,8 +3026,36 @@
"write-multiple-holding-registers": "16: Write Multiple Holding Registers",
"json-value-invalid": "JSON value has an invalid format"
},
"request" : {
"connect-request": "Connect request",
"disconnect-request": "Disconnect request",
"attribute-request": "Attribute request",
"attribute-update": "Attribute update",
"rpc-connection": "RPC command"
},
"request-type": "Request type",
"requests-mapping": "Requests mapping",
"requests-mapping-hint": "MQTT Connector requests allows you to connect, disconnect, process attribute requests from the device, handle attribute updates on the server and RPC processing configuration.",
"request-topic-expression": "Request topic expression",
"request-topic-expression-required": "Request topic expression is required.",
"response-timeout": "Response timeout (ms)",
"response-timeout-required": "Response timeout is required.",
"response-timeout-limits-error": "Timeout must be more then {{min}} ms.",
"response-topic-Qos": "Response topic QoS",
"response-topic-Qos-hint": "MQTT Quality of Service (QoS) is an agreement between the message sender and receiver that defines the level of delivery guarantee for a specific message.",
"response-topic-expression": "Response topic expression",
"response-topic-expression-required": "Response topic expression is required.",
"response-value-expression": "Response value expression",
"response-value-expression-required": "Response value expression is required.",
"value": "Value",
"value-required": "Value is required.",
"value-expression": "Value expression",
"value-expression-required": "Value expression is required.",
"with-response": "With response",
"without-response": "Without response",
"other": "Other",
"save-tip": "Save configuration file",
"security": "Security",
"security-type": "Security type",
"security-types": {
"access-token": "Access Token",
@ -2928,11 +3064,15 @@
"tls-access-token": "TLS + Access Token",
"tls-private-key": "TLS + Private Key"
},
"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-port": "Server port",
"set": "Set",
"statistics": {
"statistic": "Statistic",
"statistics": "Statistics",
"statistic-commands-empty": "No statistics available",
"statistic-commands-empty": "No configured statistic keys found. You can configure them in \"Statistics\" tab in general configuration.",
"commands": "Commands",
"send-period": "Statistic send period (in sec)",
"send-period-required": "Statistic send period is required",
@ -2987,11 +3127,21 @@
"memory-storage": "Memory storage",
"sqlite": "SQLITE"
},
"source-type": {
"msg": "Extract from message",
"topic": "Extract from topic",
"const": "Constant"
},
"workers-settings": "Workers settings",
"thingsboard": "ThingsBoard",
"general": "General",
"thingsboard-host": "ThingsBoard host",
"timeseries": "Time series",
"key": "Key",
"keys": "Keys",
"key-required": "Key is required.",
"thingsboard-host": "Platform host",
"thingsboard-host-required": "Host is required.",
"thingsboard-port": "ThingsBoard port",
"thingsboard-port": "Platform port",
"thingsboard-port-max": "Maximum port number is 65535.",
"thingsboard-port-min": "Minimum port number is 1.",
"thingsboard-port-pattern": "Port is not valid.",
@ -2999,8 +3149,14 @@
"tidy": "Tidy",
"tidy-tip": "Tidy config JSON",
"title-connectors-json": "Connector {{typeName}} configuration",
"type": "Type",
"topic-filter": "Topic filter",
"topic-required": "Topic filter is required.",
"tls-path-ca-certificate": "Path to CA certificate on gateway",
"tls-path-client-certificate": "Path to client certificate on gateway",
"method-filter": "Method filter",
"method-filter-hint": "Regular expression to filter incoming RPC method from platform.",
"method-filter-required": "Method filter is required.",
"messages-ttl-check-in-hours": "Messages TTL check in hours",
"messages-ttl-check-in-hours-required": "Messages TTL check in hours is required.",
"messages-ttl-check-in-hours-min": "Min number is 1.",
@ -3012,19 +3168,26 @@
"mqtt-qos": "QoS",
"mqtt-qos-required": "QoS is required",
"mqtt-qos-range": "QoS values range is from 0 to 1",
"qos": {
"at-most-once": "0 - At most once",
"at-least-once": "1 - At least once",
"exactly-once": "2 - Exactly once"
},
"tls-path-private-key": "Path to private key on gateway",
"toggle-fullscreen": "Toggle fullscreen",
"transformer-json-config": "Configuration JSON*",
"update-config": "Add/update configuration JSON",
"username": "Username",
"username-required": "Username is required.",
"hints": {
"remote-configuration": "Enables remote configuration and management of the gateway",
"remote-shell": "Enables remote control of the operating system with the gateway from the Remote Shell widget",
"host": "Hostname or IP address of ThingsBoard server",
"port": "Port of MQTT service on ThingsBoard server",
"token": "Access token for the gateway from ThingsBoard server",
"client-id": "MQTT client id for the gateway form ThingsBoard server",
"username": "MQTT username for the gateway form ThingsBoard server",
"password": "MQTT password for the gateway form ThingsBoard server",
"host": "Hostname or IP address of platform server",
"port": "Port of MQTT service on platform server",
"token": "Access token for the gateway from platform server",
"client-id": "MQTT client id for the gateway form platform server",
"username": "MQTT username for the gateway form platform server",
"password": "MQTT password for the gateway form platform server",
"ca-cert": "Path to CA certificate file",
"date-form": "Date format in log message",
"data-folder": "Path to folder, that will contains data (Relative or Absolute)",
@ -3033,10 +3196,10 @@
"backup-count": "If backup count is > 0, when a rollover is done, no more than backup count files are kept - the oldest ones are deleted",
"storage": "Provides configuration for saving incoming data before it is sent to the platform",
"max-file-count": "Maximum count of file that will be created",
"max-read-count": "Count of messages to get from storage and send to ThingsBoard",
"max-read-count": "Count of messages to get from storage and send to platform",
"max-records": "Maximum count of records that will be stored in one file",
"read-record-count": "Count of messages to get from storage and send to ThingsBoard",
"max-records-count": "Maximum count of data in storage before send to ThingsBoard",
"read-record-count": "Count of messages to get from storage and send to platform",
"max-records-count": "Maximum count of data in storage before send to platform",
"ttl-check-hour": "How often will Gateway check data for obsolescence",
"ttl-messages-day": "Maximum days that storage will save data",
"commands": "Commands for collecting additional statistic",
@ -3054,7 +3217,10 @@
"grpc-min-time-between-pings": "Minimum amount of time the server should wait between sending keepalive ping messages",
"grpc-max-pings-without-data": "Maximum number of keepalive ping messages that the server can send without receiving any data before it considers the connection dead.",
"grpc-min-ping-interval-without-data": "Minimum amount of time the server should wait between sending keepalive ping messages when there is no data being sent or received.",
"permit-without-calls": "Allow server to keep the GRPC connection alive even when there are no active RPC calls."
"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."
}
},
"grid": {

58
ui-ngx/src/assets/metadata/connector-default-configs/bacnet.json

@ -0,0 +1,58 @@
{
"general": {
"objectName": "TB_gateway",
"address": "0.0.0.0:47808",
"objectIdentifier": 599,
"maxApduLengthAccepted": 1476,
"segmentationSupported": "segmentedBoth",
"vendorIdentifier": 15
},
"devices": [
{
"deviceName": "BACnet Device ${objectName}",
"deviceType": "default",
"address": "192.168.2.110:47808",
"pollPeriod": 10000,
"attributes": [
{
"key": "temperature",
"type": "string",
"objectId": "analogOutput:1",
"propertyId": "presentValue"
}
],
"timeseries": [
{
"key": "state",
"type": "bool",
"objectId": "binaryValue:1",
"propertyId": "presentValue"
}
],
"attributeUpdates": [
{
"key": "brightness",
"requestType": "writeProperty",
"objectId": "analogOutput:1",
"propertyId": "presentValue"
}
],
"serverSideRpc": [
{
"method": "set_state",
"requestType": "writeProperty",
"requestTimeout": 10000,
"objectId": "binaryOutput:1",
"propertyId": "presentValue"
},
{
"method": "get_state",
"requestType": "readProperty",
"requestTimeout": 10000,
"objectId": "binaryOutput:1",
"propertyId": "presentValue"
}
]
}
]
}

73
ui-ngx/src/assets/metadata/connector-default-configs/ble.json

@ -0,0 +1,73 @@
{
"passiveScanMode": true,
"showMap": false,
"scanner": {
"timeout": 10000,
"deviceName": "Device name"
},
"devices": [
{
"name": "Temperature and humidity sensor",
"MACAddress": "4C:65:A8:DF:85:C0",
"pollPeriod": 500000,
"showMap": false,
"timeout": 10000,
"connectRetry": 5,
"connectRetryInSeconds": 0,
"waitAfterConnectRetries": 10,
"telemetry": [
{
"key": "temperature",
"method": "notify",
"characteristicUUID": "226CAA55-6476-4566-7562-66734470666D",
"valueExpression": "[2]"
},
{
"key": "humidity",
"method": "notify",
"characteristicUUID": "226CAA55-6476-4566-7562-66734470666D",
"valueExpression": "[0]"
}
],
"attributes": [
{
"key": "name",
"method": "read",
"characteristicUUID": "00002A00-0000-1000-8000-00805F9B34FB",
"valueExpression": "[0:2]cm [2:]A"
},
{
"key": "values",
"method": "read",
"characteristicUUID": "00002A00-0000-1000-8000-00805F9B34FB",
"valueExpression": "All values: [:]"
}
],
"attributeUpdates": [
{
"attributeOnThingsBoard": "sharedName",
"characteristicUUID": "00002A00-0000-1000-8000-00805F9B34FB"
}
],
"serverSideRpc": [
{
"methodRPC": "rpcMethod1",
"withResponse": true,
"characteristicUUID": "00002A00-0000-1000-8000-00805F9B34FB",
"methodProcessing": "read"
},
{
"methodRPC": "rpcMethod2",
"withResponse": true,
"characteristicUUID": "00002A00-0000-1000-8000-00805F9B34FB",
"methodProcessing": "write"
},
{
"methodRPC": "rpcMethod3",
"withResponse": true,
"methodProcessing": "scan"
}
]
}
]
}

89
ui-ngx/src/assets/metadata/connector-default-configs/can.json

@ -0,0 +1,89 @@
{
"interface": "socketcan",
"channel": "vcan0",
"backend": {
"fd": true
},
"reconnectPeriod": 5,
"devices": [
{
"name": "Car",
"sendDataOnlyOnChange": false,
"enableUnknownRpc": true,
"strictEval": false,
"attributes": [
{
"key": "isDriverDoorOpened",
"nodeId": 41,
"command": "2:2:big:8717",
"value": "4:1:int",
"expression": "bool(value & 0b00000100)",
"polling": {
"type": "once",
"dataInHex": "AB CD AB CD"
}
}
],
"timeseries": [
{
"key": "rpm",
"nodeId": 1918,
"isExtendedId": true,
"command": "2:2:big:48059",
"value": "4:2:big:int",
"expression": "value / 4",
"polling": {
"type": "always",
"period": 5,
"dataInHex": "aaaa bbbb aaaa bbbb"
}
},
{
"key": "milliage",
"nodeId": 1918,
"isExtendedId": true,
"value": "4:2:little:int",
"expression": "value * 10",
"polling": {
"type": "always",
"period": 30,
"dataInHex": "aa bb cc dd ee ff aa bb"
}
}
],
"attributeUpdates": [
{
"attributeOnThingsBoard": "softwareVersion",
"nodeId": 64,
"isExtendedId": true,
"dataLength": 4,
"dataExpression": "value + 5",
"dataByteorder": "little"
}
],
"serverSideRpc": [
{
"method": "sendSameData",
"nodeId": 4,
"isExtendedId": true,
"isFd": true,
"bitrateSwitch": true,
"dataInHex": "aa bb cc dd ee ff aa bb aa bb cc d ee ff"
},
{
"method": "setLightLevel",
"nodeId": 5,
"dataLength": 2,
"dataByteorder": "little",
"dataBefore": "00AA"
},
{
"method": "setSpeed",
"nodeId": 16,
"dataAfter": "0102",
"dataExpression": "userSpeed if maxAllowedSpeed > userSpeed else maxAllowedSpeed"
}
]
}
]
}

65
ui-ngx/src/assets/metadata/connector-default-configs/ftp.json

@ -0,0 +1,65 @@
{
"host": "0.0.0.0",
"port": 21,
"TLSSupport": false,
"security": {
"type": "basic",
"username": "admin",
"password": "admin"
},
"paths": [
{
"devicePatternName": "asd",
"devicePatternType": "Device",
"delimiter": ",",
"path": "fol/*_hello*.txt",
"readMode": "FULL",
"maxFileSize": 5,
"pollPeriod": 500,
"txtFileDataView": "SLICED",
"withSortingFiles": true,
"attributes": [
{
"key": "temp",
"value": "[1:]"
},
{
"key": "tmp",
"value": "[0:1]"
}
],
"timeseries": [
{
"type": "int",
"key": "[0:1]",
"value": "[0:1]"
},
{
"type": "int",
"key": "temp",
"value": "[1:]"
}
]
}
],
"attributeUpdates": [
{
"path": "fol/hello.json",
"deviceNameFilter": ".*",
"writingMode": "WRITE",
"valueExpression": "{'${attributeKey}':'${attributeValue}'}"
}
],
"serverSideRpc": [
{
"deviceNameFilter": ".*",
"methodFilter": "read",
"valueExpression": "${params}"
},
{
"deviceNameFilter": ".*",
"methodFilter": "write",
"valueExpression": "${params}"
}
]
}

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

@ -0,0 +1,248 @@
{
"master": {
"slaves": [
{
"host": "127.0.0.1",
"port": 5021,
"type": "tcp",
"method": "socket",
"timeout": 35,
"byteOrder": "LITTLE",
"wordOrder": "LITTLE",
"retries": true,
"retryOnEmpty": true,
"retryOnInvalid": true,
"pollPeriod": 5000,
"unitId": 1,
"deviceName": "Temp Sensor",
"sendDataOnlyOnChange": true,
"connectAttemptTimeMs": 5000,
"connectAttemptCount": 5,
"waitAfterFailedAttemptsMs": 300000,
"attributes": [
{
"tag": "string_read",
"type": "string",
"functionCode": 4,
"objectsCount": 4,
"address": 1
},
{
"tag": "bits_read",
"type": "bits",
"functionCode": 4,
"objectsCount": 1,
"address": 5
},
{
"tag": "8int_read",
"type": "8int",
"functionCode": 4,
"objectsCount": 1,
"address": 6
},
{
"tag": "16int_read",
"type": "16int",
"functionCode": 4,
"objectsCount": 1,
"address": 7
},
{
"tag": "32int_read_divider",
"type": "32int",
"functionCode": 4,
"objectsCount": 2,
"address": 8,
"divider": 10
},
{
"tag": "8int_read_multiplier",
"type": "8int",
"functionCode": 4,
"objectsCount": 1,
"address": 10,
"multiplier": 10
},
{
"tag": "32int_read",
"type": "32int",
"functionCode": 4,
"objectsCount": 2,
"address": 11
},
{
"tag": "64int_read",
"type": "64int",
"functionCode": 4,
"objectsCount": 4,
"address": 13
}
],
"timeseries": [
{
"tag": "8uint_read",
"type": "8uint",
"functionCode": 4,
"objectsCount": 1,
"address": 17
},
{
"tag": "16uint_read",
"type": "16uint",
"functionCode": 4,
"objectsCount": 2,
"address": 18
},
{
"tag": "32uint_read",
"type": "32uint",
"functionCode": 4,
"objectsCount": 4,
"address": 20
},
{
"tag": "64uint_read",
"type": "64uint",
"functionCode": 4,
"objectsCount": 1,
"address": 24
},
{
"tag": "16float_read",
"type": "16float",
"functionCode": 4,
"objectsCount": 1,
"address": 25
},
{
"tag": "32float_read",
"type": "32float",
"functionCode": 4,
"objectsCount": 2,
"address": 26
},
{
"tag": "64float_read",
"type": "64float",
"functionCode": 4,
"objectsCount": 4,
"address": 28
}
],
"attributeUpdates": [
{
"tag": "shared_attribute_write",
"type": "32int",
"functionCode": 6,
"objectsCount": 2,
"address": 29
}
],
"rpc": [
{
"tag": "setValue",
"type": "bits",
"functionCode": 5,
"objectsCount": 1,
"address": 31
},
{
"tag": "getValue",
"type": "bits",
"functionCode": 1,
"objectsCount": 1,
"address": 31
},
{
"tag": "setCPUFanSpeed",
"type": "32int",
"functionCode": 16,
"objectsCount": 2,
"address": 33
},
{
"tag": "getCPULoad",
"type": "32int",
"functionCode": 4,
"objectsCount": 2,
"address": 35
}
]
}
]
},
"slave": {
"type": "tcp",
"host": "127.0.0.1",
"port": 5026,
"method": "socket",
"deviceName": "Modbus Slave Example",
"deviceType": "default",
"pollPeriod": 5000,
"sendDataToThingsBoard": false,
"byteOrder": "LITTLE",
"wordOrder": "LITTLE",
"unitId": 0,
"values": {
"holding_registers": [
{
"attributes": [
{
"address": 1,
"type": "string",
"tag": "sm",
"objectsCount": 1,
"value": "ON"
}
],
"timeseries": [
{
"address": 2,
"type": "int",
"tag": "smm",
"objectsCount": 1,
"value": "12334"
}
],
"attributeUpdates": [
{
"tag": "shared_attribute_write",
"type": "32int",
"functionCode": 6,
"objectsCount": 2,
"address": 29,
"value": 1243
}
],
"rpc": [
{
"tag": "setValue",
"type": "bits",
"functionCode": 5,
"objectsCount": 1,
"address": 31,
"value": 22
}
]
}
],
"coils_initializer": [
{
"attributes": [
{
"address": 5,
"type": "string",
"tag": "sm",
"objectsCount": 1,
"value": "12"
}
],
"timeseries": [],
"attributeUpdates": [],
"rpc": []
}
]
}
}
}

218
ui-ngx/src/assets/metadata/connector-default-configs/mqtt.json

@ -0,0 +1,218 @@
{
"broker": {
"name": "Default Local Broker",
"host": "127.0.0.1",
"port": 1883,
"clientId": "ThingsBoard_gateway",
"version": 5,
"maxMessageNumberPerWorker": 10,
"maxNumberOfWorkers": 100,
"sendDataOnlyOnChange": false,
"security": {
"type": "anonymous"
}
},
"dataMapping": [
{
"topicFilter": "sensor/data",
"subscriptionQos": 1,
"converter": {
"type": "json",
"deviceInfo": {
"deviceNameExpressionSource": "message",
"deviceNameExpression": "${serialNumber}",
"deviceProfileExpressionSource": "message",
"deviceProfileExpression": "${sensorType}"
},
"sendDataOnlyOnChange": false,
"timeout": 60000,
"attributes": [
{
"type": "string",
"key": "model",
"value": "${sensorModel}"
},
{
"type": "string",
"key": "${sensorModel}",
"value": "on"
}
],
"timeseries": [
{
"type": "string",
"key": "temperature",
"value": "${temp}"
},
{
"type": "double",
"key": "humidity",
"value": "${hum}"
},
{
"type": "string",
"key": "combine",
"value": "${hum}:${temp}"
}
]
}
},
{
"topicFilter": "sensor/+/data",
"subscriptionQos": 1,
"converter": {
"type": "json",
"deviceInfo": {
"deviceNameExpressionSource": "topic",
"deviceNameExpression": "(?<=sensor\/)(.*?)(?=\/data)",
"deviceProfileExpressionSource": "constant",
"deviceProfileExpression": "Thermometer"
},
"sendDataOnlyOnChange": false,
"timeout": 60000,
"attributes": [
{
"type": "string",
"key": "model",
"value": "${sensorModel}"
}
],
"timeseries": [
{
"type": "double",
"key": "temperature",
"value": "${temp}"
},
{
"type": "string",
"key": "humidity",
"value": "${hum}"
}
]
}
},
{
"topicFilter": "sensor/raw_data",
"subscriptionQos": 1,
"converter": {
"type": "bytes",
"deviceInfo": {
"deviceNameExpressionSource": "message",
"deviceNameExpression": "[0:4]",
"deviceProfileExpressionSource": "constant",
"deviceProfileExpression": "default"
},
"sendDataOnlyOnChange": false,
"timeout": 60000,
"attributes": [
{
"type": "raw",
"key": "rawData",
"value": "[:]"
}
],
"timeseries": [
{
"type": "raw",
"key": "temp",
"value": "[4:]"
}
]
}
},
{
"topicFilter": "custom/sensors/+",
"subscriptionQos": 1,
"converter": {
"type": "custom",
"extension": "CustomMqttUplinkConverter",
"cached": true,
"extensionConfig": {
"temperature": 2,
"humidity": 2,
"batteryLevel": 1
}
}
}
],
"requestsMapping": {
"connectRequests": [
{
"topicFilter": "sensor/connect",
"deviceInfo": {
"deviceNameExpressionSource": "message",
"deviceNameExpression": "${serialNumber}",
"deviceProfileExpressionSource": "constant",
"deviceProfileExpression": "Thermometer"
}
},
{
"topicFilter": "sensor/+/connect",
"deviceInfo": {
"deviceNameExpressionSource": "topic",
"deviceNameExpression": "(?<=sensor\/)(.*?)(?=\/connect)",
"deviceProfileExpressionSource": "constant",
"deviceProfileExpression": "Thermometer"
}
}
],
"disconnectRequests": [
{
"topicFilter": "sensor/disconnect",
"deviceInfo": {
"deviceNameExpressionSource": "message",
"deviceNameExpression": "${serialNumber}"
}
},
{
"topicFilter": "sensor/+/disconnect",
"deviceInfo": {
"deviceNameExpressionSource": "topic",
"deviceNameExpression": "(?<=sensor\/)(.*?)(?=\/connect)"
}
}
],
"attributeRequests": [
{
"retain": false,
"topicFilter": "v1/devices/me/attributes/request",
"deviceInfo": {
"deviceNameExpressionSource": "message",
"deviceNameExpression": "${serialNumber}"
},
"attributeNameExpressionSource": "message",
"attributeNameExpression": "${versionAttribute}, ${pduAttribute}",
"topicExpression": "devices/${deviceName}/attrs",
"valueExpression": "${attributeKey}: ${attributeValue}"
}
],
"attributeUpdates": [
{
"retain": true,
"deviceNameFilter": ".*",
"attributeFilter": "firmwareVersion",
"topicExpression": "sensor/${deviceName}/${attributeKey}",
"valueExpression": "{\"${attributeKey}\":\"${attributeValue}\"}"
}
],
"serverSideRpc": [
{
"type": "twoWay",
"deviceNameFilter": ".*",
"methodFilter": "echo",
"requestTopicExpression": "sensor/${deviceName}/request/${methodName}/${requestId}",
"responseTopicExpression": "sensor/${deviceName}/response/${methodName}/${requestId}",
"responseTopicQoS": 1,
"responseTimeout": 10000,
"valueExpression": "${params}"
},
{
"type": "oneWay",
"deviceNameFilter": ".*",
"methodFilter": "no-reply",
"requestTopicExpression": "sensor/${deviceName}/request/${methodName}/${requestId}",
"valueExpression": "${params}"
}
]
}
}

66
ui-ngx/src/assets/metadata/connector-default-configs/ocpp.json

@ -0,0 +1,66 @@
{
"centralSystem": {
"name": "Central System",
"host": "127.0.0.1",
"port": 9000,
"connection": {
"type": "insecure"
},
"security": [
{
"type": "token",
"tokens": [
"Bearer ACCESS_TOKEN"
]
},
{
"type": "basic",
"credentials": [
{
"username": "admin",
"password": "admin"
}
]
}
]
},
"chargePoints": [
{
"idRegexpPattern": "bidon/hello/CP_1",
"deviceNameExpression": "${Vendor} ${Model}",
"deviceTypeExpression": "default",
"attributes": [
{
"messageTypeFilter": "MeterValues,",
"key": "temp1",
"value": "${meter_value[:].sampled_value[:].value}"
},
{
"messageTypeFilter": "MeterValues,",
"key": "vendorId",
"value": "${connector_id}"
}
],
"timeseries": [
{
"messageTypeFilter": "DataTransfer,",
"key": "temp",
"value": "${data.temp}"
}
],
"attributeUpdates": [
{
"attributeOnThingsBoard": "shared",
"valueExpression": "{\"${attributeKey}\":\"${attributeValue}\"}"
}
],
"serverSideRpc": [
{
"methodRPC": "rpc1",
"withResponse": true,
"valueExpression": "${params}"
}
]
}
]
}

58
ui-ngx/src/assets/metadata/connector-default-configs/odbc.json

@ -0,0 +1,58 @@
{
"connection": {
"str": "Driver={PostgreSQL};Server=localhost;Port=5432;Database=thingsboard;Uid=postgres;Pwd=postgres;",
"attributes": {
"autocommit": true,
"timeout": 0
},
"encoding": "utf-8",
"decoding": {
"char": "utf-8",
"wchar": "utf-8",
"metadata": "utf-16le"
},
"reconnect": true,
"reconnectPeriod": 60
},
"pyodbc": {
"pooling": false
},
"polling": {
"query": "SELECT bool_v, str_v, dbl_v, long_v, entity_id, ts FROM ts_kv WHERE ts > ? ORDER BY ts ASC LIMIT 10",
"period": 10,
"iterator": {
"column": "ts",
"query": "SELECT MIN(ts) - 1 FROM ts_kv",
"persistent": false
}
},
"mapping": {
"device": {
"type": "postgres",
"name": "'ODBC ' + entity_id"
},
"sendDataOnlyOnChange": false,
"attributes": "*",
"timeseries": [
{
"name": "value",
"value": "[i for i in [str_v, long_v, dbl_v,bool_v] if i is not None][0]"
}
]
},
"serverSideRpc": {
"enableUnknownRpc": false,
"overrideRpcConfig": true,
"methods": [
"procedureOne",
{
"name": "procedureTwo",
"args": [
"One",
2,
3.0
]
}
]
}
}

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

@ -0,0 +1,52 @@
{
"server": {
"name": "OPC-UA Default Server",
"url": "localhost:4840/freeopcua/server/",
"timeoutInMillis": 5000,
"scanPeriodInMillis": 5000,
"disableSubscriptions": false,
"subCheckPeriodInMillis": 100,
"showMap": false,
"security": "Basic128Rsa15",
"identity": {
"type": "anonymous"
},
"mapping": [
{
"deviceNodePattern": "Root\\.Objects\\.Device1",
"deviceNamePattern": "Device ${Root\\.Objects\\.Device1\\.serialNumber}",
"attributes": [
{
"key": "temperature °C",
"path": "${ns=2;i=5}"
}
],
"timeseries": [
{
"key": "humidity",
"path": "${Root\\.Objects\\.Device1\\.TemperatureAndHumiditySensor\\.Humidity}"
},
{
"key": "batteryLevel",
"path": "${Battery\\.batteryLevel}"
}
],
"rpc_methods": [
{
"method": "multiply",
"arguments": [
2,
4
]
}
],
"attributes_updates": [
{
"attributeOnThingsBoard": "deviceName",
"attributeOnDevice": "Root\\.Objects\\.Device1\\.serialNumber"
}
]
}
]
}
}

52
ui-ngx/src/assets/metadata/connector-default-configs/opcua_asyncio.json

@ -0,0 +1,52 @@
{
"server": {
"name": "OPC-UA Default Server",
"url": "localhost:4840/freeopcua/server/",
"timeoutInMillis": 5000,
"scanPeriodInMillis": 5000,
"disableSubscriptions": false,
"subCheckPeriodInMillis": 100,
"showMap": false,
"security": "Basic128Rsa15",
"identity": {
"type": "anonymous"
},
"mapping": [
{
"deviceNodePattern": "Root\\.Objects\\.Device1",
"deviceNamePattern": "Device ${Root\\.Objects\\.Device1\\.serialNumber}",
"attributes": [
{
"key": "temperature °C",
"path": "${ns=2;i=5}"
}
],
"timeseries": [
{
"key": "humidity",
"path": "${Root\\.Objects\\.Device1\\.TemperatureAndHumiditySensor\\.Humidity}"
},
{
"key": "batteryLevel",
"path": "${Battery\\.batteryLevel}"
}
],
"rpc_methods": [
{
"method": "multiply",
"arguments": [
2,
4
]
}
],
"attributes_updates": [
{
"attributeOnThingsBoard": "deviceName",
"attributeOnDevice": "Root\\.Objects\\.Device1\\.serialNumber"
}
]
}
]
}
}

169
ui-ngx/src/assets/metadata/connector-default-configs/request.json

@ -0,0 +1,169 @@
{
"host": "http://127.0.0.1:5000",
"SSLVerify": true,
"security": {
"type": "basic",
"username": "user",
"password": "password"
},
"mapping": [
{
"url": "getdata",
"httpMethod": "GET",
"httpHeaders": {
"ACCEPT": "application/json"
},
"content": {
"name": "morpheus",
"job": "leader"
},
"allowRedirects": true,
"timeout": 0.5,
"scanPeriod": 5,
"converter": {
"type": "json",
"deviceNameJsonExpression": "SD8500",
"deviceTypeJsonExpression": "SD",
"attributes": [
{
"key": "serialNumber",
"type": "string",
"value": "${serial}"
}
],
"telemetry": [
{
"key": "Maintainer",
"type": "string",
"value": "${Developer}"
},
{
"key": "combine",
"type": "string",
"value": "${Developer}:${hum}"
}
]
}
},
{
"url": "get_info",
"httpMethod": "GET",
"httpHeaders": {
"ACCEPT": "application/json"
},
"allowRedirects": true,
"timeout": 0.5,
"scanPeriod": 100,
"converter": {
"type": "custom",
"deviceNameJsonExpression": "SD8500",
"deviceTypeJsonExpression": "SD",
"extension": "CustomRequestUplinkConverter",
"extension-config": [
{
"key": "Totaliser",
"type": "float",
"fromByte": 0,
"toByte": 4,
"byteorder": "big",
"signed": true,
"multiplier": 1
},
{
"key": "Flow",
"type": "int",
"fromByte": 4,
"toByte": 6,
"byteorder": "big",
"signed": true,
"multiplier": 0.01
},
{
"key": "Temperature",
"type": "int",
"fromByte": 8,
"toByte": 10,
"byteorder": "big",
"signed": true,
"multiplier": 0.01
},
{
"key": "Pressure",
"type": "int",
"fromByte": 12,
"toByte": 14,
"byteorder": "big",
"signed": true,
"multiplier": 0.01
},
{
"key": "deviceStatus",
"type": "int",
"byteAddress": 15,
"fromBit": 4,
"toBit": 8,
"byteorder": "big",
"signed": false
},
{
"key": "OUT2",
"type": "int",
"byteAddress": 15,
"fromBit": 1,
"toBit": 2,
"byteorder": "big"
},
{
"key": "OUT1",
"type": "int",
"byteAddress": 15,
"fromBit": 0,
"toBit": 1,
"byteorder": "big"
}
]
}
}
],
"attributeUpdates": [
{
"httpMethod": "POST",
"httpHeaders": {
"CONTENT-TYPE": "application/json"
},
"timeout": 0.5,
"tries": 3,
"allowRedirects": true,
"deviceNameFilter": "SD.*",
"attributeFilter": "send_data",
"requestUrlExpression": "sensor/${deviceName}/${attributeKey}",
"requestValueExpression": "{\"${attributeKey}\":\"${attributeValue}\"}"
}
],
"serverSideRpc": [
{
"deviceNameFilter": ".*",
"methodFilter": "echo",
"requestUrlExpression": "sensor/${deviceName}/request/${methodName}/${requestId}",
"responseTimeout": 1,
"httpMethod": "GET",
"requestValueExpression": "${params}",
"responseValueExpression": "${temp}",
"timeout": 0.5,
"tries": 3,
"httpHeaders": {
"Content-Type": "application/json"
}
},
{
"deviceNameFilter": ".*",
"methodFilter": "no-reply",
"requestUrlExpression": "sensor/${deviceName}/request/${methodName}/${requestId}",
"httpMethod": "POST",
"requestValueExpression": "${params}",
"httpHeaders": {
"Content-Type": "application/json"
}
}
]
}

190
ui-ngx/src/assets/metadata/connector-default-configs/rest.json

@ -0,0 +1,190 @@
{
"host": "127.0.0.1",
"port": "5000",
"SSL": false,
"security": {
"cert": "~/ssl/cert.pem",
"key": "~/ssl/key.pem"
},
"mapping": [
{
"endpoint": "/my_devices",
"HTTPMethods": [
"POST"
],
"security": {
"type": "anonymous"
},
"converter": {
"type": "json",
"deviceNameExpression": "${sensorName}",
"deviceTypeExpression": "${sensorType}",
"attributes": [
{
"type": "string",
"key": "model",
"value": "${sensorModel}"
},
{
"type": "string",
"key": "certificateNumber",
"value": "${certificateNumber}"
}
],
"timeseries": [
{
"type": "double",
"key": "temperature",
"value": "${temp}"
},
{
"type": "double",
"key": "humidity",
"value": "${hum}"
}
]
}
},
{
"endpoint": "/anon1",
"HTTPMethods": [
"GET",
"POST"
],
"security": {
"type": "anonymous"
},
"converter": {
"type": "json",
"deviceNameExpression": "Device 2",
"deviceTypeExpression": "default",
"attributes": [
{
"type": "string",
"key": "model",
"value": "Model2"
}
],
"timeseries": [
{
"type": "double",
"key": "temperature",
"value": "${temp}"
},
{
"type": "double",
"key": "humidity",
"value": "${hum}"
}
]
}
},
{
"endpoint": "/anon2",
"HTTPMethods": [
"POST"
],
"security": {
"type": "anonymous"
},
"converter": {
"type": "custom",
"deviceNameExpression": "SuperAnonDevice",
"deviceTypeExpression": "default",
"extension": "CustomRestUplinkConverter",
"extension-config": [
{
"key": "Totaliser",
"datatype": "float",
"fromByte": 0,
"toByte": 4,
"byteorder": "big",
"signed": true,
"multiplier": 1
}
]
}
}
],
"attributeRequests": [
{
"endpoint": "/sharedAttributes",
"type": "shared",
"HTTPMethods": [
"POST"
],
"security": {
"type": "anonymous"
},
"timeout": 10.0,
"deviceNameExpression": "${deviceName}",
"attributeNameExpression": "${attribute}${attribute1}"
}
],
"attributeUpdates": [
{
"HTTPMethod": "POST",
"SSLVerify": false,
"httpHeaders": {
"CONTENT-TYPE": "application/json"
},
"security": {
"type": "anonymous"
},
"timeout": 0.5,
"tries": 3,
"allowRedirects": true,
"deviceNameFilter": "SN.*",
"attributeFilter": ".*",
"requestUrlExpression": "http://127.0.0.1:5001/",
"valueExpression": "{\"deviceName\":\"${deviceName}\",\"${attributeKey}\":\"${attributeValue}\"}"
}
],
"serverSideRpc": [
{
"deviceNameFilter": ".*",
"methodFilter": "echo",
"requestUrlExpression": "http://127.0.0.1:5001/${deviceName}",
"responseTimeout": 1,
"HTTPMethod": "GET",
"valueExpression": "${params}",
"timeout": 10.0,
"tries": 3,
"httpHeaders": {
"Content-Type": "application/json"
},
"security": {
"type": "anonymous"
}
},
{
"deviceNameFilter": "SN.*",
"methodFilter": "post_attributes",
"requestUrlExpression": "http://127.0.0.1:5000/my_devices",
"responseTimeout": 1,
"HTTPMethod": "POST",
"valueExpression": "{\"sensorName\":\"${deviceName}\", \"sensorModel\":\"${params.sensorModel}\", \"certificateNumber\":\"${params.certificateNumber}\", \"temp\":\"${params.temp}\", \"hum\":\"${params.hum}\"}",
"timeout": 10.0,
"tries": 3,
"httpHeaders": {
"Content-Type": "application/json"
},
"security": {
"type": "anonymous"
}
},
{
"deviceNameFilter": ".*",
"methodFilter": "no-reply",
"requestUrlExpression": "sensor/${deviceName}/request/${methodName}/${requestId}",
"HTTPMethod": "POST",
"valueExpression": "${params}",
"httpHeaders": {
"Content-Type": "application/json"
},
"security": {
"type": "anonymous"
}
}
]
}

138
ui-ngx/src/assets/metadata/connector-default-configs/snmp.json

@ -0,0 +1,138 @@
{
"devices": [
{
"deviceName": "SNMP router",
"deviceType": "snmp",
"ip": "snmp.live.gambitcommunications.com",
"port": 161,
"pollPeriod": 5000,
"community": "public",
"attributes": [
{
"key": "ReceivedFromGet",
"method": "get",
"oid": "1.3.6.1.2.1.1.1.0",
"timeout": 6
},
{
"key": "ReceivedFromMultiGet",
"method": "multiget",
"oid": [
"1.3.6.1.2.1.1.1.0",
"1.3.6.1.2.1.1.2.0"
],
"timeout": 6
},
{
"key": "ReceivedFromGetNext",
"method": "getnext",
"oid": "1.3.6.1.2.1.1.1.0",
"timeout": 6
},
{
"key": "ReceivedFromMultiWalk",
"method": "multiwalk",
"oid": [
"1.3.6.1.2.1.1.1.0",
"1.3.6.0.1.2.1"
]
},
{
"key": "ReceivedFromBulkWalk",
"method": "bulkwalk",
"oid": [
"1.3.6.1.2.1.1.1.0",
"1.3.6.1.2.1.1.2.0"
]
},
{
"key": "ReceivedFromBulkGet",
"method": "bulkget",
"scalarOid": [
"1.3.6.1.2.1.1.1.0",
"1.3.6.1.2.1.1.2.0"
],
"repeatingOid": [
"1.3.6.1.2.1.1.1.0",
"1.3.6.1.2.1.1.2.0"
],
"maxListSize": 10
}
],
"telemetry": [
{
"key": "ReceivedFromWalk",
"community": "private",
"method": "walk",
"oid": "1.3.6.1.2.1.1.1.0"
},
{
"key": "ReceivedFromTable",
"method": "table",
"oid": "1.3.6.1.2.1.1"
}
],
"attributeUpdateRequests": [
{
"attributeFilter": "dataToSet",
"method": "set",
"oid": "1.3.6.1.2.1.1.1.0"
},
{
"attributeFilter": "dataToMultiSet",
"method": "multiset",
"mappings": {
"1.2.3": "10",
"2.3.4": "${attribute}"
}
}
],
"serverSideRpcRequests": [
{
"requestFilter": "setData",
"method": "set",
"oid": "1.3.6.1.2.1.1.1.0"
},
{
"requestFilter": "multiSetData",
"method": "multiset"
},
{
"requestFilter": "getData",
"method": "get",
"oid": "1.3.6.1.2.1.1.1.0"
},
{
"requestFilter": "runBulkWalk",
"method": "bulkwalk",
"oid": [
"1.3.6.1.2.1.1.1.0",
"1.3.6.1.2.1.1.2.0"
]
}
]
},
{
"deviceName": "SNMP router",
"deviceType": "snmp",
"ip": "127.0.0.1",
"pollPeriod": 5000,
"community": "public",
"converter": "CustomSNMPConverter",
"attributes": [
{
"key": "ReceivedFromGetWithCustomConverter",
"method": "get",
"oid": "1.3.6.1.2.1.1.1.0"
}
],
"telemetry": [
{
"key": "ReceivedFromTableWithCustomConverter",
"method": "table",
"oid": "1.3.6.1.2.1.1.1.0"
}
]
}
]
}

59
ui-ngx/src/assets/metadata/connector-default-configs/socket.json

@ -0,0 +1,59 @@
{
"type": "TCP",
"address": "127.0.0.1",
"port": 50000,
"bufferSize": 1024,
"devices": [
{
"address": "127.0.0.1:50001",
"deviceName": "Device Example",
"deviceType": "default",
"encoding": "utf-8",
"telemetry": [
{
"key": "temp",
"byteFrom": 0,
"byteTo": -1
},
{
"key": "hum",
"byteFrom": 0,
"byteTo": 2
}
],
"attributes": [
{
"key": "name",
"byteFrom": 0,
"byteTo": -1
},
{
"key": "num",
"byteFrom": 2,
"byteTo": 4
}
],
"attributeRequests": [
{
"type": "shared",
"requestExpression": "${[0:3]==atr}",
"attributeNameExpression": "[3:]"
}
],
"attributeUpdates": [
{
"encoding": "utf-16",
"attributeOnThingsBoard": "sharedName"
}
],
"serverSideRpc": [
{
"methodRPC": "rpcMethod1",
"withResponse": true,
"methodProcessing": "write",
"encoding": "utf-8"
}
]
}
]
}

53
ui-ngx/src/assets/metadata/connector-default-configs/xmpp.json

@ -0,0 +1,53 @@
{
"server": {
"jid": "gateway@localhost",
"password": "password",
"host": "localhost",
"port": 5222,
"use_ssl": false,
"disable_starttls": false,
"force_starttls": true,
"timeout": 10000,
"plugins": [
"xep_0030",
"xep_0323",
"xep_0325"
]
},
"devices": [
{
"jid": "device@localhost/TMP_1101",
"deviceNameExpression": "${serialNumber}",
"deviceTypeExpression": "default",
"attributes": [
{
"key": "temperature",
"value": "${temp}"
}
],
"timeseries": [
{
"key": "humidity",
"value": "${hum}"
},
{
"key": "combination",
"value": "${temp}:${hum}"
}
],
"attributeUpdates": [
{
"attributeOnThingsBoard": "shared",
"valueExpression": "{\"${attributeKey}\":\"${attributeValue}\"}"
}
],
"serverSideRpc": [
{
"methodRPC": "rpc1",
"withResponse": true,
"valueExpression": "${params}"
}
]
}
]
}

22
ui-ngx/src/form.scss

@ -62,6 +62,9 @@
}
&.no-padding {
padding: 0;
&.padding-top {
padding-top: 16px;
}
}
&.stroked {
box-shadow: none;
@ -254,6 +257,9 @@
display: flex;
flex: 1;
gap: 8px;
&.no-flex {
flex: none;
}
&.row {
flex-direction: row;
}
@ -269,6 +275,9 @@
&.space-between {
justify-content: space-between;
}
&.center {
justify-content: center;
}
&.align-center {
align-items: center;
}
@ -278,6 +287,9 @@
&.fill-width {
width: 100%;
}
&.fill-height {
height: 100%;
}
}
.tb-form-panel, .tb-form-row {
@ -505,10 +517,20 @@
gap: 12px;
padding-bottom: 12px;
&.no-padding {
padding: 0;
}
&.no-gap {
gap: 0;
}
.tb-form-table-body {
display: flex;
flex-direction: column;
gap: 12px;
&.no-gap {
gap: 0;
}
}
.tb-prompt {

Loading…
Cancel
Save