Browse Source

UI: gateway dashboard - mqtt connector design improvements and refactoring

pull/10482/head
Dmitriymush 2 years ago
parent
commit
749aa6340b
  1. 12
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-security.component.ts
  2. 50
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/device-info-table.component.html
  3. 34
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/device-info-table.component.scss
  4. 22
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/device-info-table.component.ts
  5. 91
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/ellipsis-chip-list.directive.ts
  6. 148
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-data-keys-panel.component.html
  7. 21
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-data-keys-panel.component.scss
  8. 18
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-data-keys-panel.component.ts
  9. 40
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table.component.html
  10. 9
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table.component.scss
  11. 5
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table.component.ts
  12. 18
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/add-connector-dialog.component.html
  13. 6
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/add-connector-dialog.component.scss
  14. 6
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/add-connector-dialog.component.ts
  15. 322
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.html
  16. 28
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.scss
  17. 68
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.ts
  18. 43
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.html
  19. 5
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.scss
  20. 140
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts
  21. 3
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc-connector.component.ts
  22. 19
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts
  23. 3
      ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts
  24. 41
      ui-ngx/src/assets/locale/locale.constant-en_US.json
  25. 4
      ui-ngx/src/assets/metadata/connector-default-configs/mqtt.json
  26. 7
      ui-ngx/src/form.scss

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

@ -47,7 +47,7 @@ import {
} from '@angular/forms';
import {
BrokerSecurityType,
BrokerSecurityTypeTranslationsMap,
BrokerSecurityTypeTranslationsMap, noLeadTrailSpacesRegex,
} from '@home/components/widget/lib/gateway/gateway-widget.models';
import { takeUntil } from 'rxjs/operators';
@ -97,11 +97,11 @@ export class BrokerSecurityComponent extends PageComponent implements ControlVal
super(store);
this.securityFormGroup = this.fb.group({
type: [BrokerSecurityType.ANONYMOUS, []],
username: ['', [Validators.required]],
password: ['', []],
pathToCACert: ['', []],
pathToPrivateKey: ['', []],
pathToClientCert: ['', []]
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$)

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

@ -15,28 +15,21 @@
limitations under the License.
-->
<div class="tb-form-row space-between same-padding tb-flex column" [formGroup]="mappingFormGroup">
<div class="tb-form-panel-title {{ required ? 'tb-required' : '' }}" translate>
gateway.device-info-settings
</div>
<div class="tb-form-table">
<div class="tb-form-table-header tb-flex space-between" style="padding: 0 16px;">
<div class="tb-form-table-header-cell" translate>gateway.device-info.parameter</div>
<div *ngIf="useSource" class="tb-form-table-header-cell" translate>gateway.device-info.source</div>
<div class="tb-form-table-header-cell tb-flex no-flex align-center" translate>
<div translate>gateway.device-info.expression</div>
<div class="see-example"
[tb-help-popup]="'widget/lib/gateway/expressions_fn'"
tb-help-popup-placement="left"
trigger-style="font-size: 12px"
[tb-help-popup-style]="{maxWidth: '970px'}">
</div>
<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-row no-border same-padding">
<div class="fixed-title-width" translate>gateway.device-info.device-name</div>
<div class="tb-flex no-gap" *ngIf="useSource">
<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">
@ -57,12 +50,19 @@
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 no-border same-padding" *ngIf="deviceInfoType === 'full'">
<div class="fixed-title-width" translate>gateway.device-info.device-profile</div>
<div class="tb-flex no-gap" *ngIf="useSource">
<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">
@ -83,6 +83,12 @@
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>

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

@ -19,9 +19,39 @@
display: block;
.tb-form-row {
.tb-form-table-header {
min-height: 48px;
&.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;
}
}

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

@ -49,6 +49,7 @@ import {
} from '@angular/forms';
import {
DeviceInfoType,
noLeadTrailSpacesRegex,
SourceTypes,
SourceTypeTranslationsMap
} from '@home/components/widget/lib/gateway/gateway-widget.models';
@ -76,6 +77,8 @@ export class DeviceInfoTableComponent extends PageComponent implements ControlVa
SourceTypeTranslationsMap = SourceTypeTranslationsMap;
DeviceInfoType = DeviceInfoType;
@coerceBoolean()
@Input()
useSource = true;
@ -122,25 +125,30 @@ export class DeviceInfoTableComponent extends PageComponent implements ControlVa
ngOnInit() {
this.mappingFormGroup = this.fb.group({
deviceNameExpression: ['', this.required ? [Validators.required] : []]
});
this.mappingFormGroup.valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe((value) => {
this.updateView(value);
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] : []));
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() {

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

@ -0,0 +1,91 @@
///
/// Copyright © 2016-2024 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import {
AfterViewInit,
Directive,
ElementRef,
Input,
OnDestroy,
Renderer2
} from '@angular/core';
import { isEqual } from '@core/utils';
import { TranslateService } from '@ngx-translate/core';
@Directive({
// eslint-disable-next-line @angular-eslint/directive-selector
selector: '[tb-ellipsis-chip-list]'
})
export class EllipsisChipListDirective implements AfterViewInit, OnDestroy {
chipsValue: string[];
@Input('tb-ellipsis-chip-list')
set chips(value: any[]) {
if (!isEqual(this.chipsValue, value)) {
this.chipsValue = value;
setTimeout(() => {
this.adjustChips();
}, 0);
}
}
constructor(private el: ElementRef,
private renderer: Renderer2,
private translate: TranslateService) {}
ngAfterViewInit(): void {
this.adjustChips();
window.addEventListener('resize', this.adjustChips.bind(this));
}
private adjustChips(): void {
const chipListElement = this.el.nativeElement;
const ellipsisText = this.el.nativeElement.querySelector('.ellipsis-text');
const chipNodes = chipListElement.querySelectorAll('mat-chip:not(.ellipsis-chip)');
const ellipsisChip = this.el.nativeElement.querySelector('.ellipsis-chip');
this.renderer.setStyle(ellipsisChip,'display', 'inline-flex');
const margin = parseFloat(window.getComputedStyle(ellipsisChip).marginLeft) | 0;
const availableWidth = chipListElement.offsetWidth - (ellipsisChip.offsetWidth + margin);
let usedWidth = 0;
let visibleChipsCount = 0;
chipNodes.forEach((chip) => {
this.renderer.setStyle(chip, 'display', 'inline-flex');
});
chipNodes.forEach((chip) => {
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');
}
}
ngOnDestroy() {
window.removeEventListener('resize', this.adjustChips.bind(this));
}
}

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

@ -19,7 +19,7 @@
<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 align-center fill-width"
<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">
@ -30,63 +30,121 @@
</mat-panel-title>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required" translate>gateway.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.default' | 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 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" 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" 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>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required" translate>gateway.value</div>
<div class="tb-flex">
<mat-form-field *ngIf="keysType !== MappingKeysType.CUSTOM"
appearance="outline" subscriptSizing="dynamic" [style.width.px]="130">
<mat-select name="valueType" formControlName="type">
<mat-select-trigger>
<div class="tb-flex align-center">
<mat-icon class="tb-mat-18" [svgIcon]="valueTypes.get(keyControl.get('type').value)?.icon">
</mat-icon>
<span>
{{ (rawData ? 'gateway.raw' : valueTypes.get(keyControl.get('type').value)?.name) | translate}}
</span>
</div>
</mat-select-trigger>
<ng-container *ngIf="!rawData">
<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>
<mat-option *ngIf="rawData" [value]="'raw'">
<span>{{ 'gateway.raw' | translate }}</span>
</mat-option>
</mat-select>
</mat-form-field>
<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 }}*"/>
placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('value.value-required') | translate"
[matTooltip]="('gateway.value-required') | translate"
*ngIf="keyControl.get('value').hasError('required') && keyControl.get('value').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
</div>
</ng-template>
</ng-template>
</mat-expansion-panel>
</ng-container>

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

@ -27,6 +27,27 @@
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;
}
}

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

@ -38,7 +38,8 @@ import {
MappingDataKey,
MappingKeysType,
MappingValueType,
mappingValueTypesMap
mappingValueTypesMap,
noLeadTrailSpacesRegex
} from '@home/components/widget/lib/gateway/gateway-widget.models';
@Component({
@ -75,7 +76,7 @@ export class MappingDataKeysPanelComponent extends PageComponent implements OnIn
popover: TbPopoverComponent<MappingDataKeysPanelComponent>;
@Output()
keysDataApplied = new EventEmitter<Array<any>>();
keysDataApplied = new EventEmitter<Array<MappingDataKey> | {[key: string]: any}>();
valueTypeKeys = Object.values(MappingValueType);
@ -104,14 +105,10 @@ export class MappingDataKeysPanelComponent extends PageComponent implements OnIn
return keyControl;
}
removeKey(index: number) {
this.keysListFormArray.removeAt(index);
}
addKey(): void {
const dataKeyFormGroup = this.fb.group({
key: ['', Validators.required],
value: ['', Validators.required]
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));
@ -124,6 +121,7 @@ export class MappingDataKeysPanelComponent extends PageComponent implements OnIn
$event.stopPropagation();
}
this.keysListFormArray.removeAt(index);
this.keysListFormArray.markAsDirty();
}
cancel() {
@ -152,8 +150,8 @@ export class MappingDataKeysPanelComponent extends PageComponent implements OnIn
keys.forEach((keyData) => {
const { key, value, type } = keyData;
const dataKeyFormGroup = this.fb.group({
key: [key, Validators.required],
value: [value, Validators.required],
key: [key, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
value: [value, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
type: [type, []]
});
keysControlGroups.push(dataKeyFormGroup);

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

@ -57,16 +57,23 @@
</button>
</div>
</mat-toolbar>
<div fxFlex class="table-container">
<div class="table-container">
<table mat-table [dataSource]="dataSource">
<ng-container [matColumnDef]="column.def" *ngFor="let column of mappingColumns;">
<mat-header-cell *matHeaderCellDef>{{ column.title | translate }}</mat-header-cell>
<mat-cell *matCellDef="let mapping">{{ mapping[column.def] }}</mat-cell>
<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">
<mat-header-cell *matHeaderCellDef style="width: 12%"></mat-header-cell>
<mat-cell *matCellDef="let mapping; let i = index">
<div fxFlex fxLayout="row" style="align-items: center">
<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>
@ -76,6 +83,23 @@
<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>

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

@ -52,6 +52,15 @@
.mat-mdc-table {
table-layout: fixed;
min-width: 450px;
.table-value-column {
padding: 0 12px;
width: 23%;
&.request-column {
width: 38%;
}
}
}
}

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

@ -69,6 +69,7 @@ import { isDefinedAndNotNull, isUndefinedOrNull } from '@core/utils';
export class MappingTableComponent extends PageComponent implements ControlValueAccessor, AfterViewInit, OnInit, OnDestroy {
mappingTypeTranslationsMap = MappingTypeTranslationsMap;
mappingTypeEnum = MappingType;
displayedColumns = [];
mappingColumns = [];
textSearchMode = false;
@ -124,7 +125,7 @@ export class MappingTableComponent extends PageComponent implements ControlValue
this.mappingColumns.push(
{def: 'topicFilter', title: 'gateway.topic-filter'},
{def: 'QoS', title: 'gateway.mqtt-qos'},
{def: 'converter', title: 'gateway.converter'}
{def: 'converter', title: 'gateway.payload-type'}
)
} else {
this.mappingColumns.push(
@ -299,7 +300,7 @@ export class MappingTableComponent extends PageComponent implements ControlValue
}
export class MappingDatasource implements DataSource<any> {
export class MappingDatasource implements DataSource<{[key: string]: any}> {
private mappingSubject = new BehaviorSubject<Array<{[key: string]: any}>>([]);

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

@ -15,7 +15,7 @@
limitations under the License.
-->
<form [formGroup]="connectorForm" (ngSubmit)="add()">
<div [formGroup]="connectorForm" class="add-connector">
<mat-toolbar color="primary">
<h2>{{ "gateway.add-connector" | translate}}</h2>
<span fxFlex></span>
@ -30,7 +30,7 @@
</mat-progress-bar>
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
<div mat-dialog-content>
<fieldset [disabled]="isLoading$ | async" class="tb-form-panel no-border no-padding" fxLayout="column">
<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">
@ -47,12 +47,14 @@
<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.default' | translate }}"/>
<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"
[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>
@ -83,7 +85,7 @@
</mat-label>
</mat-slide-toggle>
</div>
</fieldset>
</div>
</div>
<div mat-dialog-actions fxLayoutAlign="end center">
<button mat-button color="primary"
@ -94,9 +96,9 @@
{{ 'action.cancel' | translate }}
</button>
<button mat-raised-button color="primary"
type="submit"
(click)="add()"
[disabled]="(isLoading$ | async) || connectorForm.invalid || !connectorForm.dirty">
{{ 'action.add' | translate }}
</button>
</div>
</form>
</div>

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

@ -15,12 +15,8 @@
*/
:host {
form {
.add-connector {
min-width: 400px;
width: 500px;
}
}
:host ::ng-deep .mat-padding{
padding: 0;
}

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

@ -25,7 +25,9 @@ import { Router } from '@angular/router';
import {
ConnectorType,
GatewayConnectorDefaultTypesTranslatesMap,
GatewayLogLevel, getDefaultConfig
GatewayLogLevel,
getDefaultConfig,
noLeadTrailSpacesRegex
} from '@home/components/widget/lib/gateway/gateway-widget.models';
import { Subject } from 'rxjs';
import { ResourcesService } from '@core/services/resources.service';
@ -58,7 +60,7 @@ export class AddConnectorDialogComponent extends DialogComponent<AddConnectorDia
super(store, router, dialogRef);
this.connectorForm = this.fb.group({
type: [ConnectorType.MQTT, []],
name: ['', [Validators.required, this.uniqNameRequired()]],
name: ['', [Validators.required, this.uniqNameRequired(), Validators.pattern(noLeadTrailSpacesRegex)]],
logLevel: [GatewayLogLevel.INFO, []],
useDefaults: [true, []],
sendDataOnlyOnChange: [false, []],

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

@ -15,7 +15,7 @@
limitations under the License.
-->
<form [formGroup]="mappingForm" (ngSubmit)="add()">
<div [formGroup]="mappingForm" class="key-mapping">
<mat-toolbar color="primary">
<h2>{{ MappingTypeTranslationsMap.get(this.data?.mappingType) | translate}}</h2>
<span fxFlex></span>
@ -26,26 +26,15 @@
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
<div mat-dialog-content>
<fieldset [disabled]="isLoading$ | async" class="tb-form-panel no-border no-padding" fxLayout="column">
<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-flex no-flex align-center" translate>
<div class="tb-required" translate>gateway.topic-filter</div>
<div class="see-example"
[tb-help-popup]="'widget/lib/gateway/topic-filter_fn'"
tb-help-popup-placement="left"
trigger-style="font-size: 12px"
[tb-help-popup-style]="{maxWidth: '920px'}">
</div>
</div>
<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 }}"/>
@ -53,15 +42,24 @@
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.topic-required') | translate"
*ngIf="mappingForm.get('topicFilter').hasError('required') && mappingForm.get('topicFilter').touched"
*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" translate>gateway.mqtt-qos</div>
<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">
@ -73,14 +71,18 @@
</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-flex row space-between align-center no-gap fill-width">
<div class="tb-form-panel-title" translate>gateway.converter</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 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">
@ -97,9 +99,12 @@
<div class="tb-form-row space-between tb-flex">
<div class="fixed-title-width" translate>gateway.attributes</div>
<div class="tb-flex">
<mat-chip-listbox class="tb-flex">
<mat-chip-listbox [tb-ellipsis-chip-list]="converterAttributes" class="tb-flex">
<mat-chip *ngFor="let attribute of converterAttributes">
{{ attribute.key }}
{{ attribute }}
</mat-chip>
<mat-chip class="mat-mdc-chip ellipsis-chip">
<label class="ellipsis-text"></label>
</mat-chip>
</mat-chip-listbox>
<button type="button"
@ -112,11 +117,14 @@
</div>
</div>
<div class="tb-form-row space-between tb-flex">
<div class="fixed-title-width" translate>gateway.telemetry</div>
<div class="fixed-title-width" translate>gateway.timeseries</div>
<div class="tb-flex">
<mat-chip-listbox class="tb-flex">
<mat-chip-listbox class="tb-flex" [tb-ellipsis-chip-list]="converterTelemetry">
<mat-chip *ngFor="let telemetry of converterTelemetry">
{{ telemetry.key }}
{{ telemetry }}
</mat-chip>
<mat-chip class="mat-mdc-chip ellipsis-chip">
<label class="ellipsis-text"></label>
</mat-chip>
</mat-chip-listbox>
<button type="button"
@ -153,9 +161,12 @@
<div class="tb-form-row space-between tb-flex">
<div class="fixed-title-width" translate>gateway.keys</div>
<div class="tb-flex">
<mat-chip-listbox class="tb-flex">
<mat-chip *ngFor="let telemetry of customKeys | keyvalue">
{{ telemetry.key }}
<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"
@ -192,15 +203,7 @@
*ngIf="requestMappingType === RequestTypeEnum.ATTRIBUTE_REQUEST ||
requestMappingType === RequestTypeEnum.CONNECT_REQUEST ||
requestMappingType === RequestTypeEnum.DISCONNECT_REQUEST">
<div class="fixed-title-width tb-flex no-flex align-center" translate>
<div class="tb-required" translate>gateway.topic-filter</div>
<div class="see-example"
[tb-help-popup]="'widget/lib/gateway/topic-filter_fn'"
tb-help-popup-placement="left"
trigger-style="font-size: 12px"
[tb-help-popup-style]="{maxWidth: '920px'}">
</div>
</div>
<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')"
@ -214,6 +217,12 @@
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>
@ -228,18 +237,40 @@
<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>
<tb-device-info-table formControlName="deviceInfo" [deviceInfoType]="DeviceInfoType.PARTIAL" required="true">
</tb-device-info-table>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<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.attribute-name-expression</div>
<div class="see-example"
[tb-help-popup]="'widget/lib/gateway/expressions_fn'"
tb-help-popup-placement="left"
trigger-style="font-size: 12px"
[tb-help-popup-style]="{maxWidth: '970px'}">
</div>
<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">
@ -259,6 +290,12 @@
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>
@ -266,62 +303,58 @@
<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-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-flex no-flex align-center" translate>
<div class="tb-required" translate>gateway.response-topic-expression</div>
<div class="see-example"
[tb-help-popup]="'widget/lib/gateway/expressions_fn'"
tb-help-popup-placement="left"
trigger-style="font-size: 12px"
[tb-help-popup-style]="{maxWidth: '970px'}">
</div>
</div>
<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="topicExpression" placeholder="{{ 'gateway.set' | translate }}"/>
<input matInput name="value" formControlName="valueExpression" 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"
[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">
<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 class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-flex no-flex align-center" translate>
<div class="tb-required" translate>gateway.response-value-expression</div>
<div class="see-example"
[tb-help-popup]="'widget/lib/gateway/expressions_fn'"
tb-help-popup-placement="left"
trigger-style="font-size: 12px"
[tb-help-popup-style]="{maxWidth: '970px'}">
</div>
</div>
<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="valueExpression" placeholder="{{ 'gateway.set' | translate }}"/>
<input matInput name="value" formControlName="topicExpression" 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"
[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">
@ -362,62 +395,58 @@
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-flex no-flex align-center" translate>
<div class="tb-required" translate>gateway.response-topic-expression</div>
<div class="see-example"
[tb-help-popup]="'widget/lib/gateway/expressions_fn'"
tb-help-popup-placement="left"
trigger-style="font-size: 12px"
[tb-help-popup-style]="{maxWidth: '970px'}">
</div>
</div>
<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="topicExpression" placeholder="{{ 'gateway.set' | translate }}"/>
<input matInput name="value" formControlName="valueExpression" 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"
[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">
<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>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-flex no-flex align-center" translate>
<div class="tb-required" translate>gateway.response-value-expression</div>
<div class="see-example"
[tb-help-popup]="'widget/lib/gateway/expressions_fn'"
tb-help-popup-placement="left"
trigger-style="font-size: 12px"
[tb-help-popup-style]="{maxWidth: '970px'}">
</div>
</div>
<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="valueExpression" placeholder="{{ 'gateway.set' | translate }}"/>
<input matInput name="value" formControlName="topicExpression" 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"
[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">
@ -469,15 +498,7 @@
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-flex no-flex align-center" translate>
<div class="tb-required" translate>gateway.request-topic-expression</div>
<div class="see-example"
[tb-help-popup]="'widget/lib/gateway/expressions_fn'"
tb-help-popup-placement="left"
trigger-style="font-size: 12px"
[tb-help-popup-style]="{maxWidth: '970px'}">
</div>
</div>
<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 }}"/>
@ -490,19 +511,17 @@
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-flex no-flex align-center" translate>
<div class="tb-required" translate>gateway.value-expression</div>
<div class="see-example"
[tb-help-popup]="'widget/lib/gateway/expressions_fn'"
tb-help-popup-placement="left"
trigger-style="font-size: 12px"
[tb-help-popup-style]="{maxWidth: '970px'}">
</div>
</div>
<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 }}"/>
@ -515,20 +534,18 @@
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-flex no-flex align-center" translate>
<div class="tb-required" translate>gateway.response-topic-expression</div>
<div class="see-example"
[tb-help-popup]="'widget/lib/gateway/expressions_fn'"
tb-help-popup-placement="left"
trigger-style="font-size: 12px"
[tb-help-popup-style]="{maxWidth: '970px'}">
</div>
</div>
<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 }}"/>
@ -541,11 +558,19 @@
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" translate>gateway.response-topic-Qos</div>
<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">
@ -577,20 +602,19 @@
</ng-container>
</ng-template>
</ng-container>
</fieldset>
</div>
</div>
<div mat-dialog-actions fxLayoutAlign="end center">
<button mat-button color="primary"
type="button"
cdkFocusInitial
[disabled]="(isLoading$ | async)"
(click)="cancel()">
{{ 'action.cancel' | translate }}
</button>
<button mat-raised-button color="primary"
type="submit"
[disabled]="(isLoading$ | async) || mappingForm.invalid || !mappingForm.dirty || !keysPopupClosed">
(click)="add()"
[disabled]="mappingForm.invalid || !mappingForm.dirty || !keysPopupClosed">
{{ this.data.buttonTitle | translate }}
</button>
</div>
</form>
</div>

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

@ -15,17 +15,25 @@
*/
:host {
form {
.key-mapping {
min-width: 700px;
width: 900px;
height: 770px;
display: flex;
flex-direction: column;
.mat-toolbar {
min-height: 64px;
}
tb-toggle-select {
padding: 4px 0;
}
}
}
:host ::ng-deep {
.mat-padding {
padding: 0;
}
form {
.key-mapping {
.mat-mdc-chip-listbox {
.mdc-evolution-chip-set__chips {
justify-content: flex-end;
@ -45,4 +53,14 @@
width: 0;
}
}
.see-example {
width: 32px;
height: 32px;
margin: 4px;
}
.mat-mdc-form-field-icon-suffix {
display: flex;
}
}

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

@ -24,8 +24,12 @@ import { DialogComponent } from '@shared/components/dialog.component';
import { Router } from '@angular/router';
import {
ConvertorType,
ConvertorTypeTranslationsMap, DeviceInfoType, MappingDataKey,
MappingHintTranslationsMap, MappingInfo,
ConvertorTypeTranslationsMap,
DataConversionTranslationsMap,
DeviceInfoType,
MappingDataKey,
MappingHintTranslationsMap,
MappingInfo,
MappingKeysAddKeyTranslationsMap,
MappingKeysDeleteKeyTranslationsMap,
MappingKeysNoKeysTextTranslationsMap,
@ -33,6 +37,7 @@ import {
MappingKeysType,
MappingType,
MappingTypeTranslationsMap,
noLeadTrailSpacesRegex,
QualityTypes,
QualityTypeTranslationsMap,
RequestType,
@ -84,6 +89,10 @@ export class MappingDialogComponent extends DialogComponent<MappingDialogCompone
MappingTypeTranslationsMap = MappingTypeTranslationsMap;
DataConversionTranslationsMap = DataConversionTranslationsMap;
hiddenAttributesCount = 0;
keysPopupClosed = true;
submitted = false;
@ -103,15 +112,15 @@ export class MappingDialogComponent extends DialogComponent<MappingDialogCompone
this.createMappingForm();
}
get converterAttributes(): Array<MappingDataKey> {
get converterAttributes(): Array<string> {
if (this.converterType) {
return this.mappingForm.get('converter').get(this.converterType).value.attributes;
return this.mappingForm.get('converter').get(this.converterType).value.attributes.map(value => value.key);
}
}
get converterTelemetry(): Array<MappingDataKey> {
if (this.converterType) {
return this.mappingForm.get('converter').get(this.converterType).value.timeseries;
return this.mappingForm.get('converter').get(this.converterType).value.timeseries.map(value => value.key);
}
}
@ -120,7 +129,7 @@ export class MappingDialogComponent extends DialogComponent<MappingDialogCompone
}
get customKeys(): {[key: string]: any} {
return this.mappingForm.get('converter').get('custom').value.extensionConfig;
return Object.keys(this.mappingForm.get('converter').get('custom').value.extensionConfig);
}
get requestMappingType(): RequestType {
@ -139,7 +148,8 @@ export class MappingDialogComponent extends DialogComponent<MappingDialogCompone
private createMappingForm(): void {
this.mappingForm = this.fb.group({});
if (this.data.mappingType === MappingType.DATA) {
this.mappingForm.addControl('topicFilter', this.fb.control('', [Validators.required]));
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, []],
@ -154,7 +164,7 @@ export class MappingDialogComponent extends DialogComponent<MappingDialogCompone
timeseries: [[], []]
}),
custom: this.fb.group({
extension: ['', [Validators.required]],
extension: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
extensionConfig: [{}, []]
}),
}));
@ -175,36 +185,39 @@ export class MappingDialogComponent extends DialogComponent<MappingDialogCompone
this.mappingForm.addControl('requestType', this.fb.control(RequestType.CONNECT_REQUEST, []));
this.mappingForm.addControl('requestValue', this.fb.group({
connectRequests: this.fb.group({
topicFilter: ['', [Validators.required]],
topicFilter: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
deviceInfo: [{}, []]
}),
disconnectRequests: this.fb.group({
topicFilter: ['', [Validators.required]],
topicFilter: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
deviceInfo: [{}, []]
}),
attributeRequests: this.fb.group({
topicFilter: ['', [Validators.required]],
deviceInfo: [{}, [Validators.required]],
attributeNameExpressionSource: [SourceTypes.MSG],
attributeNameExpression: ['', [Validators.required]],
topicExpression: ['', [Validators.required]],
valueExpression: ['', [Validators.required]],
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]],
attributeFilter: ['', [Validators.required]],
topicExpression: ['', [Validators.required]],
valueExpression: ['', [Validators.required]],
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.ONE_WAY, []],
deviceNameFilter: ['', [Validators.required]],
methodFilter: ['', [Validators.required]],
requestTopicExpression: ['', [Validators.required]],
responseTopicExpression: ['', [Validators.required]],
valueExpression: ['', [Validators.required]],
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]],
})
@ -293,6 +306,11 @@ export class MappingDialogComponent extends DialogComponent<MappingDialogCompone
}
}
// updateVisibleChips(count: number): void {
// console.log(count, 'count');
// this.hiddenAttributesCount = count;
// }
private prepareMappingData(): {[key: string]: any} {
const formValue = this.mappingForm.value;
if (this.data.mappingType === MappingType.DATA) {

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

@ -145,15 +145,15 @@
<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; let i = index; 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 section-container flex">
<div class="tb-form-panel-title" fxLayoutAlign="space-between center">
<div class="tb-form-panel-title" style="height: 32px;">
{{initialConnector?.type ? gatewayConnectorDefaultTypes.get(initialConnector.type) : ''}}
{{'gateway.configuration' | translate}}
<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">
@ -164,6 +164,11 @@
</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 }}">
@ -184,14 +189,24 @@
</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 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="sendDataOnlyOnChange">
<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">
@ -230,7 +245,7 @@
<div class="fixed-title-width tb-required" translate>gateway.port</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="port" placeholder="{{ 'gateway.set' | translate }}"/>
<input matInput type="number" name="value" formControlName="port" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"

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

@ -107,11 +107,6 @@
:host ::ng-deep {
.connector-container {
.mat-mdc-icon-button.mat-mdc-button-base {
width: 36px;
height: 40px;
padding: 8px 4px;
}
.mat-mdc-tab-group, .mat-mdc-tab-body-wrapper {
height: 100%;

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

@ -29,7 +29,7 @@ import {
import { EntityId } from '@shared/models/id/entity-id';
import { AttributeService } from '@core/http/attribute.service';
import { TranslateService } from '@ngx-translate/core';
import { forkJoin, Observable, Subject, Subscription } 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';
@ -55,7 +55,8 @@ import {
GatewayConnectorDefaultTypesTranslatesMap,
GatewayLogLevel,
MappingType,
MqttVersions
MqttVersions,
noLeadTrailSpacesRegex
} from './gateway-widget.models';
import { MatDialog } from '@angular/material/dialog';
import { AddConnectorDialogComponent } from '@home/components/widget/lib/gateway/dialog/add-connector-dialog.component';
@ -161,16 +162,17 @@ 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({
mode: [ConnectorConfigurationModes.BASIC, []],
name: ['', [Validators.required, this.uniqNameRequired()]],
type: ['', [Validators.required]],
logLevel: ['', [Validators.required]],
sendDataOnlyOnChange: [false, []],
key: ['auto'],
class: [''],
configuration: [''],
configurationJson: [{}, [Validators.required]],
basicConfig: this.fb.group({
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]],
basicConfig: this.fb.group({
})
});
this.connectorForm.disable();
@ -392,6 +394,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
name: '',
type: ConnectorType.MQTT,
sendDataOnlyOnChange: false,
enableRemoteLogging: false,
logLevel: GatewayLogLevel.INFO,
key: 'auto',
class: '',
@ -402,34 +405,41 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
this.connectorForm.markAsPristine();
}
selectConnector(attribute: AttributeData): void {
const connector = attribute.value;
if (connector?.name !== this.initialConnector?.name) {
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;
selectConnector($event: Event, attribute: AttributeData): void {
if ($event) {
$event.stopPropagation();
}
this.confirmConnectorChange().subscribe((result) => {
if (result) {
const connector = attribute.value;
if (connector?.name !== this.initialConnector?.name) {
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;
this.initialConnector = connector;
if (connector.type === ConnectorType.MQTT) {
this.addMQTTConfigControls();
} else {
this.connectorForm.setControl('basicConfig', this.fb.group({}), {emitEvent: false});
}
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();
}
this.connectorForm.patchValue(connector, {emitEvent: false});
this.connectorForm.markAsPristine();
}
}
});
}
isSameConnector(attribute: AttributeData): boolean {
@ -462,7 +472,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
if ($event) {
$event.stopPropagation();
}
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) {
@ -562,28 +572,31 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
dataSourceData: this.dataSource.data
}
}).afterClosed().subscribe((value) => {
if (value) {
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.confirmConnectorChange().subscribe((changeConfirmed) => {
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();
}
this.connectorForm.patchValue(value, {emitEvent: false});
this.saveConnector()
}
})
});
}
generate(formControlName: string): void {
this.connectorForm.get(formControlName).patchValue(generateSecret(5));
this.connectorForm.get(formControlName).patchValue('tb_gw_' + generateSecret(5));
}
private onDataUpdateError(e: any): void {
@ -625,10 +638,10 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
const configControl = this.fb.group({});
const brokerGroup = this.fb.group({
name: ['', []],
host: ['', [Validators.required]],
host: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
port: [null, [Validators.required]],
version: [5, []],
clientId: ['', []],
clientId: ['', [Validators.pattern(noLeadTrailSpacesRegex)]],
maxNumberOfWorkers: [100, [Validators.required, Validators.min(1)]],
maxMessageNumberPerWorker: [10, [Validators.required, Validators.min(1)]],
security: [{}, [Validators.required]]
@ -663,4 +676,17 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
}
});
}
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);
}
}

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

@ -37,6 +37,7 @@ import {
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',

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

@ -17,7 +17,8 @@
import { ResourcesService } from '@core/services/resources.service';
import { Observable } from 'rxjs';
import { ValueTypeData } from '@shared/models/constants';
import { isUndefinedOrNull } from '@core/utils';
export const noLeadTrailSpacesRegex: RegExp = /^(?! )[\S\s]*(?<! )$/;
export enum StorageTypes {
MEMORY = 'memory',
@ -109,6 +110,7 @@ export interface GatewayConnector {
configurationJson: string;
logLevel: string;
key?: string;
class?: string;
}
export enum ConnectorType {
@ -437,7 +439,7 @@ export enum MappingKeysType {
export const MappingKeysPanelTitleTranslationsMap = new Map<MappingKeysType, string>(
[
[MappingKeysType.ATTRIBUTES, 'gateway.attributes'],
[MappingKeysType.TIMESERIES, 'gateway.telemetry'],
[MappingKeysType.TIMESERIES, 'gateway.timeseries'],
[MappingKeysType.CUSTOM, 'gateway.keys']
]
);
@ -445,7 +447,7 @@ export const MappingKeysPanelTitleTranslationsMap = new Map<MappingKeysType, str
export const MappingKeysAddKeyTranslationsMap = new Map<MappingKeysType, string>(
[
[MappingKeysType.ATTRIBUTES, 'gateway.add-attribute'],
[MappingKeysType.TIMESERIES, 'gateway.add-telemetry'],
[MappingKeysType.TIMESERIES, 'gateway.add-timeseries'],
[MappingKeysType.CUSTOM, 'gateway.add-key']
]
);
@ -453,7 +455,7 @@ export const MappingKeysAddKeyTranslationsMap = new Map<MappingKeysType, string>
export const MappingKeysDeleteKeyTranslationsMap = new Map<MappingKeysType, string>(
[
[MappingKeysType.ATTRIBUTES, 'gateway.delete-attribute'],
[MappingKeysType.TIMESERIES, 'gateway.delete-telemetry'],
[MappingKeysType.TIMESERIES, 'gateway.delete-timeseries'],
[MappingKeysType.CUSTOM, 'gateway.delete-key']
]
);
@ -461,7 +463,7 @@ export const MappingKeysDeleteKeyTranslationsMap = new Map<MappingKeysType, stri
export const MappingKeysNoKeysTextTranslationsMap = new Map<MappingKeysType, string>(
[
[MappingKeysType.ATTRIBUTES, 'gateway.no-attributes'],
[MappingKeysType.TIMESERIES, 'gateway.no-telemetry'],
[MappingKeysType.TIMESERIES, 'gateway.no-timeseries'],
[MappingKeysType.CUSTOM, 'gateway.no-keys']
]
);
@ -515,3 +517,10 @@ export const mappingValueTypesMap = new Map<MappingValueType, ValueTypeData>(
]
);
export const DataConversionTranslationsMap = new Map<ConvertorType, string>(
[
[ConvertorType.JSON, 'gateway.JSON-hint'],
[ConvertorType.BYTES, 'gateway.bytes-hint'],
[ConvertorType.CUSTOM, 'gateway.custom-hint']
]
);

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

@ -92,6 +92,7 @@ import { MappingDialogComponent } from '@home/components/widget/lib/gateway/dial
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';
@NgModule({
declarations:
@ -132,6 +133,7 @@ import { BrokerSecurityComponent } from '@home/components/widget/lib/gateway/con
GatewayConfigurationComponent,
GatewayRemoteConfigurationDialogComponent,
GatewayServiceRPCConnectorTemplateDialogComponent,
EllipsisChipListDirective,
ValueCardWidgetComponent,
AggregatedValueCardWidgetComponent,
CountWidgetComponent,
@ -189,6 +191,7 @@ import { BrokerSecurityComponent } from '@home/components/widget/lib/gateway/con
GatewayLogsComponent,
GatewayServiceRPCConnectorComponent,
GatewayServiceRPCConnectorTemplatesComponent,
EllipsisChipListDirective,
GatewayStatisticsComponent,
GatewayServiceRPCComponent,
DeviceGatewayCommandComponent,

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

@ -2708,12 +2708,12 @@
"add-entry": "Add configuration",
"add-attribute": "Add attribute",
"add-key": "Add key",
"add-telemetry": "Add telemetry",
"add-timeseries": "Add timeseries",
"add-mapping": "Add mapping",
"advanced": "Advanced",
"attributes": "Attributes",
"attribute-filter": "Attribute filter",
"attribute-filter-hint": "Filter for incoming attribute name, supports regular expression.",
"attribute-filter-hint": "Filter for incoming attribute name from ThingsBoard, supports regular expression.",
"attribute-filter-required": "Attribute filter required.",
"attribute-name-expression": "Attribute name expression",
"attribute-name-expression-required": "Attribute name expression required.",
@ -2731,6 +2731,8 @@
},
"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",
@ -2760,14 +2762,15 @@
"install-docker-compose": "Use the instructions to download, install and setup docker compose",
"device-info-settings": "Device info settings",
"device-info": {
"parameter": "Parameter",
"entity-field": "Entity field",
"source": "Source",
"expression": "Expression",
"expression": "Value / Expression",
"expression-hint": "Show help",
"device-name": "Device name",
"device-profile": "Device profile",
"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 profil expression required."
"device-profile-expression-required": "Device profile expression required."
},
"device-name-filter": "Device name filter",
"device-name-filter-hint": "Regular expression for device name.",
@ -2776,8 +2779,11 @@
"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",
@ -2786,19 +2792,26 @@
"configuration-delete-dialog-input": "Gateway name",
"configuration-delete-dialog-input-required": "Gateway name is mandatory",
"configuration-delete-dialog-confirm": "Turn Off",
"converter": "Converter",
"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 telemetry data keys.",
"delete": "Delete configuration",
"delete-attribute": "Delete attribute",
"delete-key": "Delete key",
"delete-telemetry": "Delete telemetry",
"delete-timeseries": "Delete timeseries",
"default": "Default",
"download-tip": "Download configuration file",
"drop-file": "Drop file here or",
@ -2808,8 +2821,8 @@
"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": "From device request settings",
"to-device-response-settings": "To device request settings",
"from-device-request-settings": "Input request parsing",
"to-device-response-settings": "Output request handling",
"gateway": "Gateway",
"gateway-exists": "Device with same name is already exists.",
"gateway-name": "Gateway name",
@ -2890,7 +2903,7 @@
"no-data": "No configurations",
"no-gateway-found": "No gateway found.",
"no-gateway-matching": " '{{item}}' not found.",
"no-telemetry": "No telemetry",
"no-timeseries": "No timeseries",
"no-keys": "No keys",
"path-hint": "The path is local to the gateway file system",
"path-logs": "Path to log files",
@ -3016,6 +3029,7 @@
"response-timeout": "Response timeout (ms)",
"response-timeout-required": "Response timeout is required.",
"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-hint": "Response topic expression hint",
"response-topic-expression-required": "Response topic expression is required.",
@ -3040,6 +3054,7 @@
"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 telemetry in the converter output.",
"server-port": "Server port",
@ -3110,7 +3125,7 @@
"workers-settings": "Workers settings",
"thingsboard": "ThingsBoard",
"general": "General",
"telemetry": "Telemetry",
"timeseries": "Timeseries",
"key": "Key",
"keys": "Keys",
"key-required": "Key is required.",

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

@ -9,9 +9,7 @@
"maxNumberOfWorkers": 100,
"sendDataOnlyOnChange": false,
"security": {
"type": "basic",
"username": "user",
"password": "password"
"type": "anonymous"
}
},
"dataMapping": [

7
ui-ngx/src/form.scss

@ -505,6 +505,13 @@
gap: 12px;
padding-bottom: 12px;
&.no-padding {
padding: 0;
}
&.no-gap {
gap: 0;
}
.tb-form-table-body {
display: flex;
flex-direction: column;

Loading…
Cancel
Save