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", "name": "active_connectors",
"type": "attribute", "type": "attribute",
"label": "Active Connectors", "label": "Enabled Connectors",
"color": "#3f51b5", "color": "#3f51b5",
"settings": { "settings": {
"columnWidth": "20%", "columnWidth": "20%",
@ -248,7 +248,7 @@
"type": "customPretty", "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", "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", "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": [], "customResources": [],
"openInSeparateDialog": false, "openInSeparateDialog": false,
"openInPopover": false, "openInPopover": false,
@ -567,7 +567,8 @@
"padding": "8px", "padding": "8px",
"settings": { "settings": {
"useMarkdownTextFunction": true, "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, "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}" "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 { import {
AfterViewInit,
ChangeDetectionStrategy, ChangeDetectionStrategy,
ChangeDetectorRef, ChangeDetectorRef,
Component, Component,
ElementRef, forwardRef, ElementRef,
forwardRef,
NgZone, NgZone,
OnDestroy, OnDestroy,
OnInit,
ViewContainerRef ViewContainerRef
} from '@angular/core'; } from '@angular/core';
import { PageComponent } from '@shared/components/page.component'; import { PageComponent } from '@shared/components/page.component';
@ -47,7 +46,8 @@ import {
} from '@angular/forms'; } from '@angular/forms';
import { import {
BrokerSecurityType, BrokerSecurityType,
BrokerSecurityTypeTranslationsMap, noLeadTrailSpacesRegex, BrokerSecurityTypeTranslationsMap,
noLeadTrailSpacesRegex
} from '@home/components/widget/lib/gateway/gateway-widget.models'; } from '@home/components/widget/lib/gateway/gateway-widget.models';
import { takeUntil } from 'rxjs/operators'; 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; BrokerSecurityType = BrokerSecurityType;
@ -115,18 +115,12 @@ export class BrokerSecurityComponent extends PageComponent implements ControlVal
}); });
} }
ngOnInit() {
}
ngOnDestroy() { ngOnDestroy() {
this.destroy$.next(); this.destroy$.next();
this.destroy$.complete(); this.destroy$.complete();
super.ngOnDestroy(); super.ngOnDestroy();
} }
ngAfterViewInit() {
}
registerOnChange(fn: any): void { registerOnChange(fn: any): void {
this.propagateChange = fn; this.propagateChange = fn;
} }

View File

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

View File

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

View File

@ -35,7 +35,10 @@
<div class="tb-form-panel stroked"> <div class="tb-form-panel stroked">
<div class="tb-form-panel-title" translate>gateway.platform-side</div> <div class="tb-form-panel-title" translate>gateway.platform-side</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> <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"> <div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> <mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="key" placeholder="{{ 'gateway.set' | translate }}"/> <input matInput name="value" formControlName="key" placeholder="{{ 'gateway.set' | translate }}"/>
@ -89,7 +92,10 @@
</mat-form-field> </mat-form-field>
</div> </div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> <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"> <mat-form-field fxFlex appearance="outline" subscriptSizing="dynamic" class="tb-flex no-gap">
<input matInput required formControlName="value" <input matInput required formControlName="value"
placeholder="{{ 'gateway.set' | translate }}"/> placeholder="{{ 'gateway.set' | translate }}"/>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -82,6 +82,7 @@
<span class="dot" <span class="dot"
matTooltip="{{ 'Errors: '+ getErrorsCount(attribute)}}" matTooltip="{{ 'Errors: '+ getErrorsCount(attribute)}}"
matTooltipPosition="above" matTooltipPosition="above"
(click)="connectorLogs(attribute, $event)"
[class]="{'hasErrors': +getErrorsCount(attribute) > 0, [class]="{'hasErrors': +getErrorsCount(attribute) > 0,
'noErrors': +getErrorsCount(attribute) === 0 || getErrorsCount(attribute) === ''}"></span> 'noErrors': +getErrorsCount(attribute) === 0 || getErrorsCount(attribute) === ''}"></span>
</mat-cell> </mat-cell>
@ -127,7 +128,7 @@
<mat-icon>private_connectivity</mat-icon> <mat-icon>private_connectivity</mat-icon>
</button> </button>
<button mat-icon-button <button mat-icon-button
matTooltip="Delete connector" matTooltip="Logs"
matTooltipPosition="above" matTooltipPosition="above"
(click)="connectorLogs(attribute, $event)"> (click)="connectorLogs(attribute, $event)">
<mat-icon>list</mat-icon> <mat-icon>list</mat-icon>
@ -192,7 +193,7 @@
<div class="tb-form-panel stroked"> <div class="tb-form-panel stroked">
<div class="tb-form-panel-title" translate>gateway.logs-configuration</div> <div class="tb-form-panel-title" translate>gateway.logs-configuration</div>
<div class="tb-form-row" fxLayoutAlign="space-between center"> <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> <mat-label>
{{ 'gateway.enable-remote-logging' | translate }} {{ 'gateway.enable-remote-logging' | translate }}
</mat-label> </mat-label>
@ -245,13 +246,15 @@
<div class="fixed-title-width tb-required" translate>gateway.port</div> <div class="fixed-title-width tb-required" translate>gateway.port</div>
<div class="tb-flex no-gap"> <div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> <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 <mat-icon matSuffix
matTooltipPosition="above" matTooltipPosition="above"
matTooltipClass="tb-error-tooltip" matTooltipClass="tb-error-tooltip"
[matTooltip]="portErrorTooltip | translate" [matTooltip]="portErrorTooltip"
*ngIf="(connectorForm.get('basicConfig.broker.port').hasError('required') || *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" connectorForm.get('basicConfig.broker.port').touched"
class="tb-error"> class="tb-error">
warning warning

View File

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

View File

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

@ -2708,12 +2708,12 @@
"add-entry": "Add configuration", "add-entry": "Add configuration",
"add-attribute": "Add attribute", "add-attribute": "Add attribute",
"add-key": "Add key", "add-key": "Add key",
"add-timeseries": "Add timeseries", "add-timeseries": "Add time series",
"add-mapping": "Add mapping", "add-mapping": "Add mapping",
"advanced": "Advanced", "advanced": "Advanced",
"attributes": "Attributes", "attributes": "Attributes",
"attribute-filter": "Attribute filter", "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-filter-required": "Attribute filter required.",
"attribute-name-expression": "Attribute name expression", "attribute-name-expression": "Attribute name expression",
"attribute-name-expression-required": "Attribute name expression required.", "attribute-name-expression-required": "Attribute name expression required.",
@ -2773,7 +2773,7 @@
"device-profile-expression-required": "Device profile expression required." "device-profile-expression-required": "Device profile expression required."
}, },
"device-name-filter": "Device name filter", "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.", "device-name-filter-required": "Device name filter is required.",
"details": "Details", "details": "Details",
"delete-mapping-title": "Delete mapping ?", "delete-mapping-title": "Delete mapping ?",
@ -2797,9 +2797,9 @@
"payload-type": "Payload type", "payload-type": "Payload type",
"platform-side": "Platform side", "platform-side": "Platform side",
"JSON": "JSON", "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 timeseries from the message. And regular expressions to get device details from topics.", "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": "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 timeseries, using specific byte positions for data extraction.", "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": "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.", "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", "client-cert-path": "Path to client certificate file",
@ -2807,22 +2807,25 @@
"client-id": "Client ID", "client-id": "Client ID",
"data-conversion": "Data conversion", "data-conversion": "Data conversion",
"data-mapping": "Data mapping", "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 timeseries data keys.", "data-mapping-hint": "Data mapping provides the capability to parse and convert the data received from a MQTT client in incoming messages into specific attributes and time series data keys.",
"delete": "Delete configuration", "delete": "Delete configuration",
"delete-attribute": "Delete attribute", "delete-attribute": "Delete attribute",
"delete-key": "Delete key", "delete-key": "Delete key",
"delete-timeseries": "Delete timeseries", "delete-timeseries": "Delete time series",
"default": "Default", "default": "Default",
"download-tip": "Download configuration file", "download-tip": "Download configuration file",
"drop-file": "Drop file here or", "drop-file": "Drop file here or",
"extension": "Extension", "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-required": "Extension is required.",
"extension-configuration": "Extension configuration", "extension-configuration": "Extension configuration",
"extension-configuration-hint": "Configuration for convertor", "extension-configuration-hint": "Configuration for convertor",
"fill-connector-defaults": "Fill configuration with default values", "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.", "fill-connector-defaults-hint": "This property allows to fill connector configuration with default values on it's creation.",
"from-device-request-settings": "Input request parsing", "from-device-request-settings": "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": "Gateway",
"gateway-exists": "Device with same name is already exists.", "gateway-exists": "Device with same name is already exists.",
"gateway-name": "Gateway name", "gateway-name": "Gateway name",
@ -2862,6 +2865,7 @@
"host-required": "Host is required.", "host-required": "Host is required.",
"json-parse": "Not valid JSON.", "json-parse": "Not valid JSON.",
"json-required": "Field cannot be empty.", "json-required": "Field cannot be empty.",
"JSONPath-hint": "This field supports constants and JSONPath expressions.",
"logs": { "logs": {
"logs": "Logs", "logs": "Logs",
"days": "days", "days": "days",
@ -2903,7 +2907,7 @@
"no-data": "No configurations", "no-data": "No configurations",
"no-gateway-found": "No gateway found.", "no-gateway-found": "No gateway found.",
"no-gateway-matching": " '{{item}}' not found.", "no-gateway-matching": " '{{item}}' not found.",
"no-timeseries": "No timeseries", "no-timeseries": "No time series",
"no-keys": "No keys", "no-keys": "No keys",
"path-hint": "The path is local to the gateway file system", "path-hint": "The path is local to the gateway file system",
"path-logs": "Path to log files", "path-logs": "Path to log files",
@ -2913,6 +2917,7 @@
"permit-without-calls": "Keep alive permit without calls", "permit-without-calls": "Keep alive permit without calls",
"port": "Port", "port": "Port",
"port-required": "Port is required.", "port-required": "Port is required.",
"port-limits-error": "Port should be number from {{min}} to {{max}}.",
"private-key-path": "Path to private key file", "private-key-path": "Path to private key file",
"path-to-private-key-required": "Path to private key file is required.", "path-to-private-key-required": "Path to private key file is required.",
"raw": "Raw", "raw": "Raw",
@ -3044,7 +3049,6 @@
"with-response": "With response", "with-response": "With response",
"without-response": "Without response", "without-response": "Without response",
"other": "Other", "other": "Other",
"only-natural-numbers": "Only natural numbers allowed.",
"save-tip": "Save configuration file", "save-tip": "Save configuration file",
"security": "Security", "security": "Security",
"security-type": "Security type", "security-type": "Security type",
@ -3057,7 +3061,7 @@
}, },
"select-connector": "Select connector to display config", "select-connector": "Select connector to display config",
"send-change-data": "Send data only on change", "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 timeseries in the converter output.", "send-change-data-hint": "The values will be saved to the database only if they are different from the corresponding values in the previous converted message. This functionality applies to both attributes and time series in the converter output.",
"server-port": "Server port", "server-port": "Server port",
"set": "Set", "set": "Set",
"statistics": { "statistics": {
@ -3126,13 +3130,13 @@
"workers-settings": "Workers settings", "workers-settings": "Workers settings",
"thingsboard": "ThingsBoard", "thingsboard": "ThingsBoard",
"general": "General", "general": "General",
"timeseries": "Timeseries", "timeseries": "Time series",
"key": "Key", "key": "Key",
"keys": "Keys", "keys": "Keys",
"key-required": "Key is required.", "key-required": "Key is required.",
"thingsboard-host": "ThingsBoard host", "thingsboard-host": "Platform host",
"thingsboard-host-required": "Host is required.", "thingsboard-host-required": "Host is required.",
"thingsboard-port": "ThingsBoard port", "thingsboard-port": "Platform port",
"thingsboard-port-max": "Maximum port number is 65535.", "thingsboard-port-max": "Maximum port number is 65535.",
"thingsboard-port-min": "Minimum port number is 1.", "thingsboard-port-min": "Minimum port number is 1.",
"thingsboard-port-pattern": "Port is not valid.", "thingsboard-port-pattern": "Port is not valid.",
@ -3146,7 +3150,7 @@
"tls-path-ca-certificate": "Path to CA certificate on gateway", "tls-path-ca-certificate": "Path to CA certificate on gateway",
"tls-path-client-certificate": "Path to client certificate on gateway", "tls-path-client-certificate": "Path to client certificate on gateway",
"method-filter": "Method filter", "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.", "method-filter-required": "Method filter is required.",
"messages-ttl-check-in-hours": "Messages TTL check in hours", "messages-ttl-check-in-hours": "Messages TTL check in hours",
"messages-ttl-check-in-hours-required": "Messages TTL check in hours is required.", "messages-ttl-check-in-hours-required": "Messages TTL check in hours is required.",
@ -3173,12 +3177,12 @@
"hints": { "hints": {
"remote-configuration": "Enables remote configuration and management of the gateway", "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", "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", "host": "Hostname or IP address of platform server",
"port": "Port of MQTT service on ThingsBoard server", "port": "Port of MQTT service on platform server",
"token": "Access token for the gateway from ThingsBoard server", "token": "Access token for the gateway from platform server",
"client-id": "MQTT client id for the gateway form ThingsBoard server", "client-id": "MQTT client id for the gateway form platform server",
"username": "MQTT username for the gateway form ThingsBoard server", "username": "MQTT username for the gateway form platform server",
"password": "MQTT password for the gateway form ThingsBoard server", "password": "MQTT password for the gateway form platform server",
"ca-cert": "Path to CA certificate file", "ca-cert": "Path to CA certificate file",
"date-form": "Date format in log message", "date-form": "Date format in log message",
"data-folder": "Path to folder, that will contains data (Relative or Absolute)", "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", "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", "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-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", "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", "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 ThingsBoard", "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-check-hour": "How often will Gateway check data for obsolescence",
"ttl-messages-day": "Maximum days that storage will save data", "ttl-messages-day": "Maximum days that storage will save data",
"commands": "Commands for collecting additional statistic", "commands": "Commands for collecting additional statistic",