UI: gateway dashboard bug-fixes and improvements

This commit is contained in:
Dmitriymush 2024-04-19 12:56:11 +03:00
parent 214f9d178e
commit 355acfc30a
15 changed files with 169 additions and 130 deletions

View File

@ -136,7 +136,7 @@
{
"name": "active_connectors",
"type": "attribute",
"label": "Active Connectors",
"label": "Enabled Connectors",
"color": "#3f51b5",
"settings": {
"columnWidth": "20%",
@ -248,7 +248,7 @@
"type": "customPretty",
"customHtml": "<form #addEntityForm=\"ngForm\" [formGroup]=\"addEntityFormGroup\"\r\n (ngSubmit)=\"save($event)\" class=\"add-entity-form\">\r\n <mat-toolbar fxLayout=\"row\" color=\"primary\">\r\n <h2>Add gateway</h2>\r\n <span fxFlex></span>\r\n <button mat-icon-button (click)=\"cancel()\" type=\"button\">\r\n <mat-icon class=\"material-icons\">close</mat-icon>\r\n </button>\r\n </mat-toolbar>\r\n <mat-progress-bar color=\"warn\" mode=\"indeterminate\" *ngIf=\"isLoading$ | async\">\r\n </mat-progress-bar>\r\n <div style=\"height: 4px;\" *ngIf=\"!(isLoading$ | async)\"></div>\r\n <div mat-dialog-content fxLayout=\"column\">\r\n <div fxLayout=\"row\" fxLayoutGap=\"8px\" fxLayout.xs=\"column\" fxLayoutGap.xs=\"0\">\r\n <mat-form-field fxFlex class=\"mat-block\">\r\n <mat-label>Name</mat-label>\r\n <input matInput formControlName=\"entityName\" required>\r\n <mat-error *ngIf=\"addEntityFormGroup.get('entityName').hasError('required')\">\r\n Gateway name is required.\r\n </mat-error>\r\n </mat-form-field>\r\n </div>\r\n <div fxLayout=\"row\" fxLayoutGap=\"8px\" fxLayout.xs=\"column\" fxLayoutGap.xs=\"0\">\r\n <tb-entity-subtype-autocomplete\r\n fxFlex\r\n class=\"mat-block\"\r\n formControlName=\"type\"\r\n [required]=\"true\"\r\n [entityType]=\"'DEVICE'\"\r\n ></tb-entity-subtype-autocomplete>\r\n </div>\r\n </div>\r\n <div mat-dialog-actions fxLayout=\"row\" fxLayoutAlign=\"end center\">\r\n <button mat-button color=\"primary\"\r\n type=\"button\"\r\n [disabled]=\"(isLoading$ | async)\"\r\n (click)=\"cancel()\" cdkFocusInitial>\r\n Cancel\r\n </button>\r\n <button mat-button mat-raised-button color=\"primary\"\r\n type=\"submit\"\r\n [disabled]=\"(isLoading$ | async) || addEntityForm.invalid || !addEntityForm.dirty\">\r\n Create\r\n </button>\r\n </div>\r\n</form>\r\n",
"customCss": ".add-entity-form {\r\n min-width: 400px !important;\r\n}\r\n\r\n.add-entity-form .boolean-value-input {\r\n padding-left: 5px;\r\n}\r\n\r\n.add-entity-form .boolean-value-input .checkbox-label {\r\n margin-bottom: 8px;\r\n color: rgba(0,0,0,0.54);\r\n font-size: 12px;\r\n}\r\n\r\n.relations-list .header {\r\n padding-right: 5px;\r\n padding-bottom: 5px;\r\n padding-left: 5px;\r\n}\r\n\r\n.relations-list .header .cell {\r\n padding-right: 5px;\r\n padding-left: 5px;\r\n font-size: 12px;\r\n font-weight: 700;\r\n color: rgba(0, 0, 0, .54);\r\n white-space: nowrap;\r\n}\r\n\r\n.relations-list .mat-form-field-infix {\r\n width: auto !important;\r\n}\r\n\r\n.relations-list .body {\r\n padding-right: 5px;\r\n padding-bottom: 15px;\r\n padding-left: 5px;\r\n}\r\n\r\n.relations-list .body .row {\r\n padding-top: 5px;\r\n}\r\n\r\n.relations-list .body .cell {\r\n padding-right: 5px;\r\n padding-left: 5px;\r\n}\r\n\r\n.relations-list .body .md-button {\r\n margin: 0;\r\n}\r\n\r\n",
"customFunction": "let $injector = widgetContext.$scope.$injector;\r\nlet customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));\r\nlet assetService = $injector.get(widgetContext.servicesMap.get('assetService'));\r\nlet deviceService = $injector.get(widgetContext.servicesMap.get('deviceService'));\r\nlet attributeService = $injector.get(widgetContext.servicesMap.get('attributeService'));\r\nlet entityRelationService = $injector.get(widgetContext.servicesMap.get('entityRelationService'));\r\nlet userSettingsService = $injector.get(widgetContext.servicesMap.get('userSettingsService'));\r\n\r\nopenAddEntityDialog();\r\n\r\nfunction openAddEntityDialog() {\r\n customDialog.customDialog(htmlTemplate, AddEntityDialogController).subscribe();\r\n}\r\n\r\nfunction AddEntityDialogController(instance) {\r\n let vm = instance;\r\n let userSettings;\r\n userSettingsService.loadUserSettings().subscribe(settings=> {\r\n userSettings = settings;\r\n if (!userSettings.createdGatewaysCount) userSettings.createdGatewaysCount = 0;\r\n });\r\n \r\n\r\n vm.addEntityFormGroup = vm.fb.group({\r\n entityName: ['', [vm.validators.required]],\r\n entityType: ['DEVICE'],\r\n entityLabel: [''],\r\n type: ['', [vm.validators.required]],\r\n });\r\n\r\n vm.cancel = function() {\r\n vm.dialogRef.close(null);\r\n };\r\n\r\n\r\n vm.save = function($event) {\r\n vm.addEntityFormGroup.markAsPristine();\r\n saveEntityObservable().subscribe(\r\n function (device) {\r\n widgetContext.updateAliases();\r\n userSettingsService.putUserSettings({ createdGatewaysCount: ++userSettings.createdGatewaysCount }).subscribe(_=>{\r\n });\r\n vm.dialogRef.close(null);\r\n openCommandDialog(device, $event);\r\n }\r\n );\r\n };\r\n \r\n function openCommandDialog(device, $event) {\r\n vm.device = device;\r\n let openCommandAction = widgetContext.actionsApi.getActionDescriptors(\"actionCellButton\").find(action => action.name == \"Docker commands\");\r\n widgetContext.actionsApi.handleWidgetAction($event, openCommandAction, device.id, device.name, {newDevice: true});\r\n setTimeout(function() {\r\n document.querySelector(\".dashboard-state-dialog .mat-mdc-button-touch-target\").addEventListener('click', goToConfigState);\r\n document.querySelector(\".dashboard-state-dialog .mat-mdc-button.mat-primary\").addEventListener('click', goToConfigState);\r\n }, 500);\r\n }\r\n\r\n \r\n function goToConfigState() {\r\n document.querySelector(\".dashboard-state-dialog .mat-mdc-button-touch-target\").removeEventListener('click', goToConfigState);\r\n document.querySelector(\".dashboard-state-dialog .mat-mdc-button.mat-primary\").removeEventListener('click', goToConfigState);\r\n const stateParams = {};\r\n stateParams.entityId = vm.device.id;\r\n stateParams.entityName = vm.device.name;\r\n const newStateParams = {\r\n targetEntityParamName: 'default',\r\n new_gateway: {\r\n entityId: vm.device.id,\r\n entityName: vm.device.name\r\n }\r\n }\r\n const params = {...stateParams, ...newStateParams};\r\n widgetContext.stateController.openState('gateway_details', params, false);\r\n }\r\n\r\n function saveEntityObservable() {\r\n const formValues = vm.addEntityFormGroup.value;\r\n let entity = {\r\n name: formValues.entityName,\r\n type: formValues.type,\r\n label: formValues.entityLabel,\r\n additionalInfo: {\r\n gateway: true\r\n }\r\n };\r\n return deviceService.saveDevice(entity);\r\n }\r\n}\r\n",
"customFunction": "let $injector = widgetContext.$scope.$injector;\r\nlet customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));\r\nlet assetService = $injector.get(widgetContext.servicesMap.get('assetService'));\r\nlet deviceService = $injector.get(widgetContext.servicesMap.get('deviceService'));\r\nlet attributeService = $injector.get(widgetContext.servicesMap.get('attributeService'));\r\nlet entityRelationService = $injector.get(widgetContext.servicesMap.get('entityRelationService'));\r\nlet userSettingsService = $injector.get(widgetContext.servicesMap.get('userSettingsService'));\r\n\r\nopenAddEntityDialog();\r\n\r\nfunction openAddEntityDialog() {\r\n customDialog.customDialog(htmlTemplate, AddEntityDialogController).subscribe();\r\n}\r\n\r\nfunction AddEntityDialogController(instance) {\r\n let vm = instance;\r\n let userSettings;\r\n userSettingsService.loadUserSettings().subscribe(settings=> {\r\n userSettings = settings;\r\n if (!userSettings.createdGatewaysCount) userSettings.createdGatewaysCount = 0;\r\n });\r\n \r\n\r\n vm.addEntityFormGroup = vm.fb.group({\r\n entityName: ['', [vm.validators.required]],\r\n entityType: ['DEVICE'],\r\n entityLabel: [''],\r\n type: ['', [vm.validators.required]],\r\n });\r\n\r\n vm.cancel = function() {\r\n vm.dialogRef.close(null);\r\n };\r\n\r\n\r\n vm.save = function($event) {\r\n vm.addEntityFormGroup.markAsPristine();\r\n saveEntityObservable().subscribe(\r\n function (device) {\r\n widgetContext.updateAliases();\r\n userSettingsService.putUserSettings({ createdGatewaysCount: ++userSettings.createdGatewaysCount }).subscribe(_=>{\r\n });\r\n vm.dialogRef.close(null);\r\n openCommandDialog(device, $event);\r\n }\r\n );\r\n };\r\n \r\n function openCommandDialog(device, $event) {\r\n vm.device = device;\r\n let openCommandAction = widgetContext.actionsApi.getActionDescriptors(\"actionCellButton\").find(action => action.name == \"Docker commands\");\r\n widgetContext.actionsApi.handleWidgetAction($event, openCommandAction, device.id, device.name, {newDevice: true});\r\n goToConfigState();\r\n }\r\n\r\n \r\n function goToConfigState() {\r\n const stateParams = {};\r\n stateParams.entityId = vm.device.id;\r\n stateParams.entityName = vm.device.name;\r\n const newStateParams = {\r\n targetEntityParamName: 'default',\r\n new_gateway: {\r\n entityId: vm.device.id,\r\n entityName: vm.device.name\r\n }\r\n }\r\n const params = {...stateParams, ...newStateParams};\r\n widgetContext.stateController.openState('gateway_details', params, false);\r\n }\r\n\r\n function saveEntityObservable() {\r\n const formValues = vm.addEntityFormGroup.value;\r\n let entity = {\r\n name: formValues.entityName,\r\n type: formValues.type,\r\n label: formValues.entityLabel,\r\n additionalInfo: {\r\n gateway: true\r\n }\r\n };\r\n return deviceService.saveDevice(entity);\r\n }\r\n}\r\n",
"customResources": [],
"openInSeparateDialog": false,
"openInPopover": false,
@ -567,7 +567,8 @@
"padding": "8px",
"settings": {
"useMarkdownTextFunction": true,
"markdownTextFunction": "var blockData = '';\nvar connectorsIndex = ctx.actionsApi.getActionDescriptors('elementClick').findIndex(action=>action.name==\"Connectors\");\nvar logsIndex = ctx.actionsApi.getActionDescriptors('elementClick').findIndex(action=>action.name==\"Logs\");\nfunction generateMatHeader(index) {\n if( index !== undefined && index > -1) {\n return `<mat-card-header class='tb-home-widget-link' (click)=\"ctx.actionsApi.handleWidgetAction($event, ctx.actionsApi.getActionDescriptors('elementClick')[${index}], ctx.datasources[0].entity.id)\">`\n } else {\n return \"<mat-card-header >\" \n }\n}\nfunction createDataBlock(value, label, dividerStyle, mobile, index) {\n blockData += `\n <mat-card style=\"flex-grow: 1; width: ${mobile? '100%': 'auto'}; min-height: ${mobile? 'auto': '57px'}\" class=\" ${dividerStyle}\">\n <div class=\"divider\"></div>\n <mat-divider vertical style=\"height:100%\"></mat-divider>\n ${generateMatHeader(index)}\n <mat-card-subtitle>${label}</mat-card-subtitle>\n </mat-card-header>\n <mat-card-content> ${value}</mat-card-content>\n </mat-card>`;\n}\ncreateDataBlock(data[0].Status, \"Status\", data[0].Status === \"Active\"? 'divider-green' : 'divider-red');\ncreateDataBlock(data[0].Name, \"Gateway Name\", '', ctx.isMobile);\ncreateDataBlock(data[0].Type, \"Gateway Type\", '');\ncreateDataBlock(\n `<span style=\"color:rgb(25,128,56)\">${(data[1]?data[1].count:0)} </span>`\n + \" | \" + \n `<span style=\"color:rgb(203,37,48)\">${(data[2]?data[2][\"count 2\"]:0)} </span>`\n , \"Devices <span class='tb-hint' style='padding-left: 0'>(Active | Inactive)</span>\", '');\ncreateDataBlock(\n `<span style=\"color:rgb(25,128,56)\">${(data[0].active_connectors?JSON.parse(data[0].active_connectors).length:0)} </span>`\n + \" | \" + \n `<span style=\"color:rgb(203,37,48)\">${(data[0].inactive_connectors?JSON.parse(data[0].inactive_connectors).length:0)} </span>`\n , \"Connectors <span class='tb-hint' style='padding-left: 0'>(Active | Inactive)</span>\", '', '', connectorsIndex);\ncreateDataBlock(data[0].ALL_ERRORS_COUNT || 0, \"Errors\", (data[0].ALL_ERRORS_COUNT || 0) === 0 ? 'divider-green' : 'divider-red', '', logsIndex);\nreturn `<div fxLayout=\"row wrap\" fxLayoutGap=\"8px\" class=\"cards-container\">${blockData}</div>`;",
"markdownTextPattern": "# Markdown/HTML card \\n - **Current entity**: **${entityName}**. \\n - **Current value**: **${Random}**.",
"markdownTextFunction": "var blockData = '';\nvar connectorsIndex = ctx.actionsApi.getActionDescriptors('elementClick').findIndex(action=>action.name==\"Connectors\");\nvar logsIndex = ctx.actionsApi.getActionDescriptors('elementClick').findIndex(action=>action.name==\"Logs\");\nfunction generateMatHeader(index) {\n if( index !== undefined && index > -1) {\n return `<mat-card-header class='tb-home-widget-link' (click)=\"ctx.actionsApi.handleWidgetAction($event, ctx.actionsApi.getActionDescriptors('elementClick')[${index}], ctx.datasources[0].entity.id)\">`\n } else {\n return \"<mat-card-header >\" \n }\n}\nfunction createDataBlock(value, label, dividerStyle, mobile, index) {\n blockData += `\n <mat-card style=\"flex-grow: 1; width: ${mobile? '100%': 'auto'}; min-height: ${mobile? 'auto': '57px'}\" class=\" ${dividerStyle}\">\n <div class=\"divider\"></div>\n <mat-divider vertical style=\"height:100%\"></mat-divider>\n ${generateMatHeader(index)}\n <mat-card-subtitle>${label}</mat-card-subtitle>\n </mat-card-header>\n <mat-card-content> ${value}</mat-card-content>\n </mat-card>`;\n}\ncreateDataBlock(data[0].Status, \"Status\", data[0].Status === \"Active\"? 'divider-green' : 'divider-red');\ncreateDataBlock(data[0].Name, \"Gateway Name\", '', ctx.isMobile);\ncreateDataBlock(data[0].Type, \"Gateway Type\", '');\ncreateDataBlock(\n `<span style=\"color:rgb(25,128,56)\">${(data[1]?data[1].count:0)} </span>`\n + \" | \" + \n `<span style=\"color:rgb(203,37,48)\">${(data[2]?data[2][\"count 2\"]:0)} </span>`\n , \"Devices <span class='tb-hint' style='padding-left: 0'>(Active | Inactive)</span>\", '');\ncreateDataBlock(\n `<span style=\"color:rgb(25,128,56)\">${(data[0].active_connectors?JSON.parse(data[0].active_connectors).length:0)} </span>`\n + \" | \" + \n `<span style=\"color:rgb(203,37,48)\">${(data[0].inactive_connectors?JSON.parse(data[0].inactive_connectors).length:0)} </span>`\n , \"Connectors <span class='tb-hint' style='padding-left: 0'>(Enabled | Disabled)</span>\", '', '', connectorsIndex);\ncreateDataBlock(data[0].ALL_ERRORS_COUNT || 0, \"Errors\", (data[0].ALL_ERRORS_COUNT || 0) === 0 ? 'divider-green' : 'divider-red', '', logsIndex);\nreturn `<div fxLayout=\"row wrap\" fxLayoutGap=\"8px\" class=\"cards-container\">${blockData}</div>`;",
"applyDefaultMarkdownStyle": false,
"markdownCss": ".divider {\n position: absolute;\n width: 3px;\n top: 8px;\n border-radius: 2px;\n bottom: 8px;\n border: 1px solid rgba(31, 70, 144, 1);\n background-color: rgba(31, 70, 144, 1);\n left: 10px;\n}\n.divider-green .divider {\n border: 1px solid rgb(25,128,56);\n background-color: rgb(25,128,56);\n}\n\n.divider-green .mat-mdc-card-content {\n color: rgb(25,128,56);\n}\n\n.divider-red .divider {\n border: 1px solid rgb(203,37,48);\n background-color: rgb(203,37,48);\n}\n\n.divider-red .mat-mdc-card-content {\n color: rgb(203,37,48);\n}\n\n.mdc-card {\n position: relative;\n padding-left: 10px;\n margin-bottom: 1px;\n}\n\n.mat-mdc-card-subtitle {\n font-weight: 400;\n font-size: 12px;\n}\n\n.mat-mdc-card-header {\n padding: 8px 16px 0;\n}\n\n.mat-mdc-card-content:last-child {\n padding-bottom: 8px;\n font-size: 16px;\n}\n\n.cards-container {\n height: calc(100% - 1px);\n justify-content: stretch;\n align-items: center;\n margin-bottom: 1px;\n}\n\n::ng-deep.tb-home-widget-link > div {\n flex-grow: 1;\n cursor: pointer;\n}\n\n .tb-home-widget-link {\n width: 100%;\n }\n\n .tb-home-widget-link:hover::after{\n color: inherit;\n }\n \n .tb-home-widget-link::after{\n content: 'arrow_forward';\n display: inline-block;\n transform: rotate(315deg);\n font-family: 'Material Icons';\n font-weight: normal;\n font-style: normal;\n font-size: 18px;\n color: rgba(0, 0, 0, 0.12);\n vertical-align: bottom;\n margin-left: 6px;\n}"
},

View File

@ -15,14 +15,13 @@
///
import {
AfterViewInit,
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
ElementRef, forwardRef,
ElementRef,
forwardRef,
NgZone,
OnDestroy,
OnInit,
ViewContainerRef
} from '@angular/core';
import { PageComponent } from '@shared/components/page.component';
@ -47,7 +46,8 @@ import {
} from '@angular/forms';
import {
BrokerSecurityType,
BrokerSecurityTypeTranslationsMap, noLeadTrailSpacesRegex,
BrokerSecurityTypeTranslationsMap,
noLeadTrailSpacesRegex
} from '@home/components/widget/lib/gateway/gateway-widget.models';
import { takeUntil } from 'rxjs/operators';
@ -69,7 +69,7 @@ import { takeUntil } from 'rxjs/operators';
}
]
})
export class BrokerSecurityComponent extends PageComponent implements ControlValueAccessor, Validator, AfterViewInit, OnInit, OnDestroy {
export class BrokerSecurityComponent extends PageComponent implements ControlValueAccessor, Validator, OnDestroy {
BrokerSecurityType = BrokerSecurityType;
@ -115,18 +115,12 @@ export class BrokerSecurityComponent extends PageComponent implements ControlVal
});
}
ngOnInit() {
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
super.ngOnDestroy();
}
ngAfterViewInit() {
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}

View File

@ -15,7 +15,6 @@
///
import {
AfterViewInit,
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
@ -40,7 +39,8 @@ import { UtilsService } from '@core/services/utils.service';
import { EntityService } from '@core/http/entity.service';
import {
ControlValueAccessor,
FormBuilder, NG_VALIDATORS,
FormBuilder,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
UntypedFormGroup,
ValidationErrors,
@ -73,7 +73,7 @@ import { coerceBoolean } from '@shared/decorators/coercion';
}
]
})
export class DeviceInfoTableComponent extends PageComponent implements ControlValueAccessor, Validator, AfterViewInit, OnInit, OnDestroy {
export class DeviceInfoTableComponent extends PageComponent implements ControlValueAccessor, Validator, OnInit, OnDestroy {
SourceTypeTranslationsMap = SourceTypeTranslationsMap;
@ -156,9 +156,6 @@ export class DeviceInfoTableComponent extends PageComponent implements ControlVa
this.destroy$.complete();
}
ngAfterViewInit() {
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}

View File

@ -15,26 +15,31 @@
///
import {
AfterViewInit,
Directive,
ElementRef,
Inject,
Input,
OnDestroy,
Renderer2
} from '@angular/core';
import { isEqual } from '@core/utils';
import { TranslateService } from '@ngx-translate/core';
import { WINDOW } from '@core/services/window.service';
import { fromEvent, Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
@Directive({
// eslint-disable-next-line @angular-eslint/directive-selector
selector: '[tb-ellipsis-chip-list]'
})
export class EllipsisChipListDirective implements AfterViewInit, OnDestroy {
export class EllipsisChipListDirective implements OnDestroy {
chipsValue: string[];
private destroy$ = new Subject<void>();
@Input('tb-ellipsis-chip-list')
set chips(value: any[]) {
set chips(value: string[]) {
if (!isEqual(this.chipsValue, value)) {
this.chipsValue = value;
setTimeout(() => {
@ -45,11 +50,15 @@ export class EllipsisChipListDirective implements AfterViewInit, OnDestroy {
constructor(private el: ElementRef,
private renderer: Renderer2,
private translate: TranslateService) {}
ngAfterViewInit(): void {
private translate: TranslateService,
@Inject(WINDOW) private window: Window) {
this.renderer.setStyle(this.el.nativeElement, 'max-height', '48px');
this.renderer.setStyle(this.el.nativeElement, 'overflow', 'auto');
fromEvent(window, 'resize').pipe(
takeUntil(this.destroy$)
).subscribe(() => {
this.adjustChips();
window.addEventListener('resize', this.adjustChips.bind(this));
});
}
private adjustChips(): void {
@ -58,17 +67,16 @@ export class EllipsisChipListDirective implements AfterViewInit, OnDestroy {
const chipNodes = chipListElement.querySelectorAll('mat-chip:not(.ellipsis-chip)');
const ellipsisChip = this.el.nativeElement.querySelector('.ellipsis-chip');
this.renderer.setStyle(ellipsisChip,'display', 'inline-flex');
ellipsisText.innerHTML = this.translate.instant('gateway.ellipsis-chips-text',
{count: (this.chipsValue.length)});
const margin = parseFloat(window.getComputedStyle(ellipsisChip).marginLeft) | 0;
const margin = parseFloat(this.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;
@ -85,7 +93,8 @@ export class EllipsisChipListDirective implements AfterViewInit, OnDestroy {
}
}
ngOnDestroy() {
window.removeEventListener('resize', this.adjustChips.bind(this));
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
}

View File

@ -35,7 +35,10 @@
<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="fixed-title-width tb-required"
tb-hint-tooltip-icon="{{ 'gateway.JSONPath-hint' | translate }}" translate>
gateway.key
</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="key" placeholder="{{ 'gateway.set' | translate }}"/>
@ -89,7 +92,10 @@
</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>
<div class="fixed-title-width tb-required"
tb-hint-tooltip-icon="{{ 'gateway.JSONPath-hint' | translate }}" translate>
gateway.value
</div>
<mat-form-field fxFlex appearance="outline" subscriptSizing="dynamic" class="tb-flex no-gap">
<input matInput required formControlName="value"
placeholder="{{ 'gateway.set' | translate }}"/>

View File

@ -26,9 +26,6 @@
<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>
<div class="tb-form-panel no-border no-padding" fxLayout="column">
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
@ -91,13 +88,12 @@
<button mat-button color="primary"
type="button"
cdkFocusInitial
[disabled]="(isLoading$ | async)"
(click)="cancel()">
{{ 'action.cancel' | translate }}
</button>
<button mat-raised-button color="primary"
(click)="add()"
[disabled]="(isLoading$ | async) || connectorForm.invalid || !connectorForm.dirty">
[disabled]="connectorForm.invalid || !connectorForm.dirty">
{{ 'action.add' | translate }}
</button>
</div>

View File

@ -14,7 +14,7 @@
/// limitations under the License.
///
import { Component, Inject, OnDestroy, OnInit } from '@angular/core';
import { Component, Inject, OnDestroy } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
@ -23,7 +23,9 @@ import { BaseData, HasId } from '@shared/models/base-data';
import { DialogComponent } from '@shared/components/dialog.component';
import { Router } from '@angular/router';
import {
AddConnectorConfigData,
ConnectorType,
CreatedConnectorConfigData,
GatewayConnectorDefaultTypesTranslatesMap,
GatewayLogLevel,
getDefaultConfig,
@ -38,7 +40,7 @@ import { ResourcesService } from '@core/services/resources.service';
styleUrls: ['./add-connector-dialog.component.scss'],
providers: [],
})
export class AddConnectorDialogComponent extends DialogComponent<AddConnectorDialogComponent, BaseData<HasId>> implements OnInit, OnDestroy {
export class AddConnectorDialogComponent extends DialogComponent<AddConnectorDialogComponent, BaseData<HasId>> implements OnDestroy {
connectorForm: UntypedFormGroup;
@ -53,8 +55,8 @@ export class AddConnectorDialogComponent extends DialogComponent<AddConnectorDia
constructor(protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: any,
public dialogRef: MatDialogRef<AddConnectorDialogComponent, any>,
@Inject(MAT_DIALOG_DATA) public data: AddConnectorConfigData,
public dialogRef: MatDialogRef<AddConnectorDialogComponent, CreatedConnectorConfigData>,
private fb: FormBuilder,
private resourcesService: ResourcesService) {
super(store, router, dialogRef);
@ -67,9 +69,6 @@ export class AddConnectorDialogComponent extends DialogComponent<AddConnectorDia
});
}
ngOnInit(): void {
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();

View File

@ -139,7 +139,10 @@
</div>
<div class="tb-form-panel no-border no-padding" *ngIf="converterType === ConvertorTypeEnum.CUSTOM">
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required" translate>gateway.extension</div>
<div class="fixed-title-width tb-required"
tb-hint-tooltip-icon="{{ 'gateway.extension-hint' | translate }}" translate>
gateway.extension
</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="extension" placeholder="{{ 'gateway.set' | translate }}"/>
@ -237,6 +240,9 @@
<ng-template [ngSwitchCase]="RequestTypeEnum.ATTRIBUTE_REQUEST">
<div class="tb-form-panel stroked">
<div class="tb-form-panel-title tb-required" translate>gateway.from-device-request-settings</div>
<div class="tb-form-hint tb-primary-fill" translate>
gateway.from-device-request-settings-hint
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center" formGroupName="deviceInfo">
<div class="fixed-title-width tb-flex no-flex align-center" translate>
<div class="tb-required" translate>gateway.device-info.device-name-expression</div>
@ -302,6 +308,9 @@
</div>
<div class="tb-form-panel stroked">
<div class="tb-form-panel-title tb-required" translate>gateway.to-device-response-settings</div>
<div class="tb-form-hint tb-primary-fill" translate>
gateway.to-device-response-settings-hint
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required" translate>gateway.response-value-expression</div>
<div class="tb-flex no-gap">
@ -359,7 +368,10 @@
</ng-template>
<ng-template [ngSwitchCase]="RequestTypeEnum.ATTRIBUTE_UPDATE">
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required" translate>gateway.device-name-filter</div>
<div class="fixed-title-width tb-required"
tb-hint-tooltip-icon="{{ 'gateway.device-name-filter-hint' | translate }}" translate>
gateway.device-name-filter
</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="deviceNameFilter" placeholder="{{ 'gateway.set' | translate }}"/>

View File

@ -34,6 +34,7 @@
.mat-mdc-dialog-content {
max-height: 670px;
height: 670px;
}
}

View File

@ -14,7 +14,7 @@
/// limitations under the License.
///
import { Component, Inject, OnDestroy, OnInit, Renderer2, ViewContainerRef } from '@angular/core';
import { Component, Inject, OnDestroy, Renderer2, ViewContainerRef } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
@ -27,7 +27,6 @@ import {
ConvertorTypeTranslationsMap,
DataConversionTranslationsMap,
DeviceInfoType,
MappingDataKey,
MappingHintTranslationsMap,
MappingInfo,
MappingKeysAddKeyTranslationsMap,
@ -58,7 +57,7 @@ import { MappingDataKeysPanelComponent } from '@home/components/widget/lib/gatew
styleUrls: ['./mapping-dialog.component.scss'],
providers: [],
})
export class MappingDialogComponent extends DialogComponent<MappingDialogComponent, BaseData<HasId>> implements OnInit, OnDestroy {
export class MappingDialogComponent extends DialogComponent<MappingDialogComponent, BaseData<HasId>> implements OnDestroy {
mappingForm: UntypedFormGroup;
@ -118,7 +117,7 @@ export class MappingDialogComponent extends DialogComponent<MappingDialogCompone
}
}
get converterTelemetry(): Array<MappingDataKey> {
get converterTelemetry(): Array<string> {
if (this.converterType) {
return this.mappingForm.get('converter').get(this.converterType).value.timeseries.map(value => value.key);
}
@ -128,7 +127,7 @@ export class MappingDialogComponent extends DialogComponent<MappingDialogCompone
return this.mappingForm.get('converter').get('type').value;
}
get customKeys(): {[key: string]: any} {
get customKeys(): Array<string> {
return Object.keys(this.mappingForm.get('converter').get('custom').value.extensionConfig);
}
@ -136,9 +135,6 @@ export class MappingDialogComponent extends DialogComponent<MappingDialogCompone
return this.mappingForm.get('requestType').value;
}
ngOnInit(): void {
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();

View File

@ -82,6 +82,7 @@
<span class="dot"
matTooltip="{{ 'Errors: '+ getErrorsCount(attribute)}}"
matTooltipPosition="above"
(click)="connectorLogs(attribute, $event)"
[class]="{'hasErrors': +getErrorsCount(attribute) > 0,
'noErrors': +getErrorsCount(attribute) === 0 || getErrorsCount(attribute) === ''}"></span>
</mat-cell>
@ -127,7 +128,7 @@
<mat-icon>private_connectivity</mat-icon>
</button>
<button mat-icon-button
matTooltip="Delete connector"
matTooltip="Logs"
matTooltipPosition="above"
(click)="connectorLogs(attribute, $event)">
<mat-icon>list</mat-icon>
@ -192,7 +193,7 @@
<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-slide-toggle class="mat-slide" formControlName="enableRemoteLogging">
<mat-label>
{{ 'gateway.enable-remote-logging' | translate }}
</mat-label>
@ -245,13 +246,15 @@
<div class="fixed-title-width tb-required" translate>gateway.port</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput type="number" min="0" name="value" formControlName="port" placeholder="{{ 'gateway.set' | translate }}"/>
<input matInput type="number" min="{{portLimits.MIN}}" max="{{portLimits.MAX}}"
name="value" formControlName="port" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="portErrorTooltip | translate"
[matTooltip]="portErrorTooltip"
*ngIf="(connectorForm.get('basicConfig.broker.port').hasError('required') ||
connectorForm.get('basicConfig.broker.port').hasError('min')) &&
connectorForm.get('basicConfig.broker.port').hasError('min') ||
connectorForm.get('basicConfig.broker.port').hasError('max')) &&
connectorForm.get('basicConfig.broker.port').touched"
class="tb-error">
warning

View File

@ -56,7 +56,8 @@ import {
GatewayLogLevel,
MappingType,
MqttVersions,
noLeadTrailSpacesRegex
noLeadTrailSpacesRegex,
PortLimits
} from './gateway-widget.models';
import { MatDialog } from '@angular/material/dialog';
import { AddConnectorDialogComponent } from '@home/components/widget/lib/gateway/dialog/add-connector-dialog.component';
@ -110,6 +111,8 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
mappingTypes = MappingType;
portLimits = PortLimits;
mode: ConnectorConfigurationModes = this.connectorConfigurationModes.BASIC;
initialConnector: GatewayConnector;
@ -180,9 +183,13 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
get portErrorTooltip(): string {
if (this.connectorForm.get('basicConfig.broker.port').hasError('required')) {
return 'gateway.port-required';
} else if (this.connectorForm.get('basicConfig.broker.port').hasError('min')) {
return 'gateway.only-natural-numbers';
return this.translate.instant('gateway.port-required');
} else if (
this.connectorForm.get('basicConfig.broker.port').hasError('min') ||
this.connectorForm.get('basicConfig.broker.port').hasError('max')
) {
return this.translate.instant('gateway.port-limits-error',
{min: PortLimits.MIN, max: PortLimits.MAX});
}
return '';
}
@ -418,10 +425,10 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
if ($event) {
$event.stopPropagation();
}
this.confirmConnectorChange().subscribe((result) => {
if (result) {
const connector = attribute.value;
if (connector?.name !== this.initialConnector?.name) {
this.confirmConnectorChange().subscribe((result) => {
if (result) {
if (this.connectorForm.disabled) {
this.connectorForm.enable();
}
@ -447,9 +454,9 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
this.connectorForm.patchValue(connector, {emitEvent: false});
this.connectorForm.markAsPristine();
}
}
});
}
}
isSameConnector(attribute: AttributeData): boolean {
if (!this.initialConnector) {
@ -573,6 +580,8 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
if ($event) {
$event.stopPropagation();
}
this.confirmConnectorChange().subscribe((changeConfirmed) => {
if (changeConfirmed) {
return this.dialog.open<AddConnectorDialogComponent,
AddConnectorConfigData>(AddConnectorDialogComponent, {
disableClose: true,
@ -581,7 +590,6 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
dataSourceData: this.dataSource.data
}
}).afterClosed().subscribe((value) => {
this.confirmConnectorChange().subscribe((changeConfirmed) => {
if (value && changeConfirmed) {
this.initialConnector = null;
if (this.connectorForm.disabled) {
@ -600,7 +608,8 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
this.generate('basicConfig.broker.clientId');
this.saveConnector();
}
})
});
}
});
}
@ -648,7 +657,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
const brokerGroup = this.fb.group({
name: ['', []],
host: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
port: [null, [Validators.required, Validators.min(0)]],
port: [null, [Validators.required, Validators.min(PortLimits.MIN), Validators.max(PortLimits.MAX)]],
version: [5, []],
clientId: ['', [Validators.pattern(noLeadTrailSpacesRegex)]],
maxNumberOfWorkers: [100, [Validators.required, Validators.min(1)]],

View File

@ -17,6 +17,7 @@
import { ResourcesService } from '@core/services/resources.service';
import { Observable } from 'rxjs';
import { ValueTypeData } from '@shared/models/constants';
import { Validators } from '@angular/forms';
export const noLeadTrailSpacesRegex: RegExp = /^(?! )[\S\s]*(?<! )$/;
@ -39,6 +40,11 @@ export enum GatewayLogLevel {
DEBUG = 'DEBUG'
}
export enum PortLimits {
MIN = 1,
MAX = 65535
}
export const GatewayStatus = {
...GatewayLogLevel,
...DeviceGatewayStatus
@ -314,6 +320,15 @@ export interface AddConnectorConfigData {
dataSourceData: Array<any>
}
export interface CreatedConnectorConfigData {
type: ConnectorType,
name: string,
logLevel: GatewayLogLevel,
useDefaults: boolean,
sendDataOnlyOnChange: boolean,
configurationJson?: {[key: string]: any}
}
export interface MappingDataKey {
key: string,
value: any,

View File

@ -94,6 +94,3 @@ For bytes converter, expression fields can use slices format only. A slice speci
| AM123,mytype,12.2,45 | [:] | AM123,mytype,12.2,45 | Extracting all data |
| AM123,mytype,12.2,45 | [18:] | 45 | Extracting humidity value |
| AM123,mytype,12.2,45 | [13:17] | 12.2 | Extracting temperature value |
For more information about MQTT connector configuration use the [official documentation](https://thingsboard.io/docs/iot-gateway/config/mqtt/?MqttConverterTypeConfig=json#subsection-security).

View File

@ -2713,7 +2713,7 @@
"advanced": "Advanced",
"attributes": "Attributes",
"attribute-filter": "Attribute filter",
"attribute-filter-hint": "Filter for incoming attribute name from ThingsBoard, supports regular expression.",
"attribute-filter-hint": "Filter for incoming attribute name from platform, supports regular expression.",
"attribute-filter-required": "Attribute filter required.",
"attribute-name-expression": "Attribute name expression",
"attribute-name-expression-required": "Attribute name expression required.",
@ -2773,7 +2773,7 @@
"device-profile-expression-required": "Device profile expression required."
},
"device-name-filter": "Device name filter",
"device-name-filter-hint": "Regular expression for device name.",
"device-name-filter-hint": "This field supports Regular expressions to filter incoming data by device name.",
"device-name-filter-required": "Device name filter is required.",
"details": "Details",
"delete-mapping-title": "Delete mapping ?",
@ -2816,13 +2816,16 @@
"download-tip": "Download configuration file",
"drop-file": "Drop file here or",
"extension": "Extension",
"extension-hint": "Put your converter classname in the field. Custom converter with such class should be in extension/mqtt folder.",
"extension-required": "Extension is required.",
"extension-configuration": "Extension configuration",
"extension-configuration-hint": "Configuration for convertor",
"fill-connector-defaults": "Fill configuration with default values",
"fill-connector-defaults-hint": "This property allows to fill connector configuration with default values on it's creation.",
"from-device-request-settings": "Input request parsing",
"to-device-response-settings": "Output request handling",
"from-device-request-settings-hint": "These fields support JSONPath expressions to extract a name from incoming message.",
"to-device-response-settings": "Output request processing",
"to-device-response-settings-hint": "For these fields you can use the following variables and they will be replaced with actual values: ${deviceName}, ${attributeKey}, ${attributeValue}",
"gateway": "Gateway",
"gateway-exists": "Device with same name is already exists.",
"gateway-name": "Gateway name",
@ -2862,6 +2865,7 @@
"host-required": "Host is required.",
"json-parse": "Not valid JSON.",
"json-required": "Field cannot be empty.",
"JSONPath-hint": "This field supports constants and JSONPath expressions.",
"logs": {
"logs": "Logs",
"days": "days",
@ -2913,6 +2917,7 @@
"permit-without-calls": "Keep alive permit without calls",
"port": "Port",
"port-required": "Port is required.",
"port-limits-error": "Port should be number from {{min}} to {{max}}.",
"private-key-path": "Path to private key file",
"path-to-private-key-required": "Path to private key file is required.",
"raw": "Raw",
@ -3044,7 +3049,6 @@
"with-response": "With response",
"without-response": "Without response",
"other": "Other",
"only-natural-numbers": "Only natural numbers allowed.",
"save-tip": "Save configuration file",
"security": "Security",
"security-type": "Security type",
@ -3130,9 +3134,9 @@
"key": "Key",
"keys": "Keys",
"key-required": "Key is required.",
"thingsboard-host": "ThingsBoard host",
"thingsboard-host": "Platform host",
"thingsboard-host-required": "Host is required.",
"thingsboard-port": "ThingsBoard port",
"thingsboard-port": "Platform port",
"thingsboard-port-max": "Maximum port number is 65535.",
"thingsboard-port-min": "Minimum port number is 1.",
"thingsboard-port-pattern": "Port is not valid.",
@ -3146,7 +3150,7 @@
"tls-path-ca-certificate": "Path to CA certificate on gateway",
"tls-path-client-certificate": "Path to client certificate on gateway",
"method-filter": "Method filter",
"method-filter-hint": "Regular expression for RPC method.",
"method-filter-hint": "Regular expression to filter incoming RPC method from platform.",
"method-filter-required": "Method filter is required.",
"messages-ttl-check-in-hours": "Messages TTL check in hours",
"messages-ttl-check-in-hours-required": "Messages TTL check in hours is required.",
@ -3173,12 +3177,12 @@
"hints": {
"remote-configuration": "Enables remote configuration and management of the gateway",
"remote-shell": "Enables remote control of the operating system with the gateway from the Remote Shell widget",
"host": "Hostname or IP address of ThingsBoard server",
"port": "Port of MQTT service on ThingsBoard server",
"token": "Access token for the gateway from ThingsBoard server",
"client-id": "MQTT client id for the gateway form ThingsBoard server",
"username": "MQTT username for the gateway form ThingsBoard server",
"password": "MQTT password for the gateway form ThingsBoard server",
"host": "Hostname or IP address of platform server",
"port": "Port of MQTT service on platform server",
"token": "Access token for the gateway from platform server",
"client-id": "MQTT client id for the gateway form platform server",
"username": "MQTT username for the gateway form platform server",
"password": "MQTT password for the gateway form platform server",
"ca-cert": "Path to CA certificate file",
"date-form": "Date format in log message",
"data-folder": "Path to folder, that will contains data (Relative or Absolute)",
@ -3187,10 +3191,10 @@
"backup-count": "If backup count is > 0, when a rollover is done, no more than backup count files are kept - the oldest ones are deleted",
"storage": "Provides configuration for saving incoming data before it is sent to the platform",
"max-file-count": "Maximum count of file that will be created",
"max-read-count": "Count of messages to get from storage and send to ThingsBoard",
"max-read-count": "Count of messages to get from storage and send to platform",
"max-records": "Maximum count of records that will be stored in one file",
"read-record-count": "Count of messages to get from storage and send to ThingsBoard",
"max-records-count": "Maximum count of data in storage before send to ThingsBoard",
"read-record-count": "Count of messages to get from storage and send to platform",
"max-records-count": "Maximum count of data in storage before send to platform",
"ttl-check-hour": "How often will Gateway check data for obsolescence",
"ttl-messages-day": "Maximum days that storage will save data",
"commands": "Commands for collecting additional statistic",