From 749aa6340bdfcf42330268983d766ea885bd2214 Mon Sep 17 00:00:00 2001 From: Dmitriymush Date: Tue, 16 Apr 2024 00:55:00 +0300 Subject: [PATCH] UI: gateway dashboard - mqtt connector design improvements and refactoring --- .../broker-security.component.ts | 12 +- .../device-info-table.component.html | 50 +-- .../device-info-table.component.scss | 34 +- .../device-info-table.component.ts | 22 +- .../ellipsis-chip-list.directive.ts | 91 +++++ .../mapping-data-keys-panel.component.html | 148 +++++--- .../mapping-data-keys-panel.component.scss | 21 ++ .../mapping-data-keys-panel.component.ts | 18 +- .../mapping-table.component.html | 40 ++- .../mapping-table.component.scss | 9 + .../mapping-table.component.ts | 5 +- .../add-connector-dialog.component.html | 18 +- .../add-connector-dialog.component.scss | 6 +- .../dialog/add-connector-dialog.component.ts | 6 +- .../dialog/mapping-dialog.component.html | 322 ++++++++++-------- .../dialog/mapping-dialog.component.scss | 28 +- .../dialog/mapping-dialog.component.ts | 68 ++-- .../gateway/gateway-connectors.component.html | 43 ++- .../gateway/gateway-connectors.component.scss | 5 - .../gateway/gateway-connectors.component.ts | 140 ++++---- ...gateway-service-rpc-connector.component.ts | 3 +- .../lib/gateway/gateway-widget.models.ts | 19 +- .../widget/widget-components.module.ts | 3 + .../assets/locale/locale.constant-en_US.json | 41 ++- .../connector-default-configs/mqtt.json | 4 +- ui-ngx/src/form.scss | 7 + 26 files changed, 768 insertions(+), 395 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/ellipsis-chip-list.directive.ts diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-security.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-security.component.ts index 179329a38d..e67bb61207 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-security.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/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$) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/device-info-table.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/device-info-table.component.html index 7e61dc0aa2..2a3108b8b4 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/device-info-table.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/device-info-table.component.html @@ -15,28 +15,21 @@ limitations under the License. --> -
-
- gateway.device-info-settings -
-
-
-
gateway.device-info.parameter
-
gateway.device-info.source
-
-
gateway.device-info.expression
-
-
+
+
device.device
+
+
+
gateway.device-info.entity-field
+
gateway.device-info.source
+
+ gateway.device-info.expression
-
-
gateway.device-info.device-name
-
+
+
gateway.device-info.name
+
@@ -57,12 +50,19 @@ class="tb-error"> warning +
+
-
-
gateway.device-info.device-profile
-
+
+
gateway.device-info.profile-name
+
@@ -83,6 +83,12 @@ class="tb-error"> warning +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/device-info-table.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/device-info-table.component.scss index 2853e6ada6..9dbaa52831 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/device-info-table.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/device-info-table.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; + } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/device-info-table.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/device-info-table.component.ts index e9cec11420..f292993682 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/device-info-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/device-info-table.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() { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/ellipsis-chip-list.directive.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/ellipsis-chip-list.directive.ts new file mode 100644 index 0000000000..6d525aa4c0 --- /dev/null +++ b/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)); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-data-keys-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-data-keys-panel.component.html index 594cd03874..d7b2222e59 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-data-keys-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-data-keys-panel.component.html @@ -19,7 +19,7 @@
{{ panelTitle | translate }}{{' (' + keysListFormArray.controls.length + ')'}}
-
@@ -30,63 +30,121 @@ -
-
gateway.key
-
- - - - warning - - +
+
+
gateway.platform-side
+
+
gateway.key
+
+ + + + warning + +
+
+
+
+
+
+
+
gateway.connector-side
+
+
gateway.type
+ + + +
+ + + + {{ (rawData ? 'gateway.raw' : valueTypes.get(keyControl.get('type').value)?.name) | translate}} + +
+
+ + + + + {{ valueTypes.get(valueType).name | translate }} + + + + + {{ 'gateway.raw' | translate }} + + +
+
+
+
+
gateway.value
+ + + + warning + +
+
+
+
-
-
gateway.value
-
- - - -
- - - - {{ (rawData ? 'gateway.raw' : valueTypes.get(keyControl.get('type').value)?.name) | translate}} - -
-
- - - - - {{ valueTypes.get(valueType).name | translate }} - - - - {{ 'gateway.raw' | translate }} - -
-
+ +
+
gateway.key
+
+ + + + warning + + +
+
+
+
gateway.value
+ placeholder="{{ 'gateway.set' | translate }}"/> warning
-
+ diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-data-keys-panel.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-data-keys-panel.component.scss index 771925a16d..64233efc6f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-data-keys-panel.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-data-keys-panel.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; } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-data-keys-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-data-keys-panel.component.ts index a9ee443f22..40ee2728bd 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-data-keys-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-data-keys-panel.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; @Output() - keysDataApplied = new EventEmitter>(); + keysDataApplied = new EventEmitter | {[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); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table.component.html index 2cc388a2aa..6dc7e7d03a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table.component.html @@ -57,16 +57,23 @@
-
+
- - {{ column.title | translate }} - {{ mapping[column.def] }} + + + {{ column.title | translate }} + + + {{ mapping[column.def] }} + - - - -
+ + + + +
+
+ + + + + +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table.component.scss index 51ef102eae..affc4e5982 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table.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%; + } + } } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table.component.ts index 9f940f3aec..a5c6addfe4 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table.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 { +export class MappingDatasource implements DataSource<{[key: string]: any}> { private mappingSubject = new BehaviorSubject>([]); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/add-connector-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/add-connector-dialog.component.html index b4a04e1cbd..b4dd8515df 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/add-connector-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/add-connector-dialog.component.html @@ -15,7 +15,7 @@ limitations under the License. --> -
+

{{ "gateway.add-connector" | translate}}

@@ -30,7 +30,7 @@
-
+
gateway.type
@@ -47,12 +47,14 @@
gateway.name
- + warning @@ -83,7 +85,7 @@
-
+
- +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/add-connector-dialog.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/add-connector-dialog.component.scss index 813b4ec9ff..56e4bc229c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/add-connector-dialog.component.scss +++ b/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; -} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/add-connector-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/add-connector-dialog.component.ts index 41994b3bf0..b635618ec9 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/add-connector-dialog.component.ts +++ b/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 -
+

{{ MappingTypeTranslationsMap.get(this.data?.mappingType) | translate}}

@@ -26,26 +26,15 @@ close
- - -
-
+
{{ MappingHintTranslationsMap.get(this.data?.mappingType) | translate }}
-
-
gateway.topic-filter
-
-
-
+
gateway.topic-filter
@@ -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 +
+
-
gateway.mqtt-qos
+
+ gateway.mqtt-qos +
@@ -73,14 +71,18 @@
+
+
gateway.payload-type
+ + + {{ ConvertorTypeTranslationsMap.get(type) | translate }} + + +
-
-
gateway.converter
- - - {{ ConvertorTypeTranslationsMap.get(type) | translate }} - - +
gateway.data-conversion
+
+ {{ DataConversionTranslationsMap.get(converterType) | translate }}
@@ -97,9 +99,12 @@
gateway.attributes
- + - {{ attribute.key }} + {{ attribute }} + + +
-
gateway.telemetry
+
gateway.timeseries
- + - {{ telemetry.key }} + {{ telemetry }} + + +
@@ -228,18 +237,40 @@
gateway.from-device-request-settings
- - -
+
-
gateway.attribute-name-expression
-
-
+
gateway.device-info.device-name-expression
+
+ + + + {{ SourceTypeTranslationsMap.get(type) | translate }} + + + + + + + warning + +
+
+
+
+
+
+
gateway.attribute-name-expression
@@ -259,6 +290,12 @@ class="tb-error"> warning +
+
@@ -266,62 +303,58 @@
gateway.to-device-response-settings
-
-
gateway.response-topic-expression
-
-
-
+
gateway.response-value-expression
- + warning +
+
- - - {{ 'gateway.retain' | translate }} - - -
-
-
-
gateway.response-value-expression
-
-
-
+
gateway.response-topic-expression
- + warning +
+
+
+ + + {{ 'gateway.retain' | translate }} + + +
@@ -362,62 +395,58 @@
-
-
gateway.response-topic-expression
-
-
-
+
gateway.response-value-expression
- + warning +
+
- - - {{ 'gateway.retain' | translate }} - - -
-
-
-
gateway.response-value-expression
-
-
-
+
gateway.response-topic-expression
- + warning +
+
+
+ + + {{ 'gateway.retain' | translate }} + + +
@@ -469,15 +498,7 @@
-
-
gateway.request-topic-expression
-
-
-
+
gateway.request-topic-expression
@@ -490,19 +511,17 @@ class="tb-error"> warning +
+
-
-
gateway.value-expression
-
-
-
+
gateway.value-expression
@@ -515,20 +534,18 @@ class="tb-error"> warning +
+
-
-
gateway.response-topic-expression
-
-
-
+
gateway.response-topic-expression
@@ -541,11 +558,19 @@ class="tb-error"> warning +
+
-
gateway.response-topic-Qos
+
+ gateway.response-topic-Qos +
@@ -577,20 +602,19 @@ -
+
- + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.scss index d976d5df7b..9dbe29d77b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.scss @@ -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; + } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.ts index 9cb558e20c..0457c3693a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.ts @@ -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 { + get converterAttributes(): Array { 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 { 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 + *matRowDef="let attribute; let i = index; columns: displayedColumns;" (click)="selectConnector($event, attribute)">
-
-
- {{initialConnector?.type ? gatewayConnectorDefaultTypes.get(initialConnector.type) : ''}} - {{'gateway.configuration' | translate}} +
+
+ {{ initialConnector?.type ? gatewayConnectorDefaultTypes.get(initialConnector.type) : '' }} + {{ 'gateway.configuration' | translate }}
@@ -164,6 +164,11 @@
+ + gateway.select-connector +
@@ -184,14 +189,24 @@
-
-
gateway.remote-logging-level
-
- - - {{ logLevel }} - - +
+
gateway.logs-configuration
+
+ + + {{ 'gateway.enable-remote-logging' | translate }} + + +
+
+
gateway.remote-logging-level
+
+ + + {{ logLevel }} + + +
@@ -230,7 +245,7 @@
gateway.port
- + ([]); 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 { + 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); + } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc-connector.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc-connector.component.ts index e1919041f5..2ea5964b84 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc-connector.component.ts +++ b/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]*(?( [ [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.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 export const MappingKeysDeleteKeyTranslationsMap = new Map( [ [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.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( ] ); +export const DataConversionTranslationsMap = new Map( + [ + [ConvertorType.JSON, 'gateway.JSON-hint'], + [ConvertorType.BYTES, 'gateway.bytes-hint'], + [ConvertorType.CUSTOM, 'gateway.custom-hint'] + ] +); diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts b/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts index 855c035ff1..6c9a733b47 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts @@ -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, diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 4a3fa1f34f..36e198d49b 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/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.", diff --git a/ui-ngx/src/assets/metadata/connector-default-configs/mqtt.json b/ui-ngx/src/assets/metadata/connector-default-configs/mqtt.json index 4d1a12b3f8..c60e1dd956 100644 --- a/ui-ngx/src/assets/metadata/connector-default-configs/mqtt.json +++ b/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": [ diff --git a/ui-ngx/src/form.scss b/ui-ngx/src/form.scss index 3d0eef9718..9e96571b6f 100644 --- a/ui-ngx/src/form.scss +++ b/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;