diff --git a/.gitignore b/.gitignore index 3a4af54b75..e9a4e11b85 100644 --- a/.gitignore +++ b/.gitignore @@ -30,9 +30,10 @@ pom.xml.versionsBackup **/build **/target **/Californium.properties +**/Californium3.properties **/.env .instance_id rebuild-docker.sh */.run/** .run/** -.run \ No newline at end of file +.run diff --git a/application/pom.xml b/application/pom.xml index 405f3b4d7d..efbc35dc6c 100644 --- a/application/pom.xml +++ b/application/pom.xml @@ -1,6 +1,6 @@ - + [parentDashboard]="parentDashboard" + [parentAliasController]="parentAliasController"> +
+ {{ 'dashboard.non-existent-dashboard-state-error' | translate:{stateId} }} +
diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-state.component.scss b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-state.component.scss new file mode 100644 index 0000000000..01322e7215 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-state.component.scss @@ -0,0 +1,21 @@ +/** + * Copyright © 2016-2022 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + position: relative; + display: block; + width: 100%; + height: 100%; +} diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-state.component.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-state.component.ts index fb22d9f3c2..50863b0595 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-state.component.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-state.component.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2021 The Thingsboard Authors +/// Copyright © 2016-2022 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -14,14 +14,14 @@ /// limitations under the License. /// -import { ChangeDetectorRef, Component, Input, OnDestroy, OnInit } from '@angular/core'; +import { ChangeDetectorRef, Component, HostBinding, Input, OnDestroy, OnInit } from '@angular/core'; import { PageComponent } from '@shared/components/page.component'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; -import { Dashboard } from '@shared/models/dashboard.models'; -import { StateObject } from '@core/api/widget-api.models'; +import { Dashboard, DashboardLayoutId } from '@shared/models/dashboard.models'; +import { IAliasController, StateObject } from '@core/api/widget-api.models'; import { updateEntityParams, WidgetContext } from '@home/models/widget-component.models'; -import { deepClone, objToBase64 } from '@core/utils'; +import { deepClone, isDefinedAndNotNull, isNotEmptyStr, objToBase64 } from '@core/utils'; import { IDashboardComponent } from '@home/models/dashboard-component.models'; import { EntityId } from '@shared/models/id/entity-id'; import { Subscription } from 'rxjs'; @@ -29,7 +29,7 @@ import { Subscription } from 'rxjs'; @Component({ selector: 'tb-dashboard-state', templateUrl: './dashboard-state.component.html', - styleUrls: [] + styleUrls: ['./dashboard-state.component.scss'] }) export class DashboardStateComponent extends PageComponent implements OnInit, OnDestroy { @@ -42,6 +42,15 @@ export class DashboardStateComponent extends PageComponent implements OnInit, On @Input() syncParentStateParams = false; + @Input() + defaultAutofillLayout = true; + + @Input() + defaultMargin; + + @Input() + defaultBackgroundColor; + @Input() entityParamName: string; @@ -54,6 +63,10 @@ export class DashboardStateComponent extends PageComponent implements OnInit, On parentDashboard: IDashboardComponent; + parentAliasController: IAliasController; + + stateExists = true; + private stateSubscription: Subscription; constructor(protected store: Store, @@ -63,14 +76,32 @@ export class DashboardStateComponent extends PageComponent implements OnInit, On ngOnInit(): void { this.dashboard = deepClone(this.ctx.stateController.dashboardCtrl.dashboardCtx.getDashboard()); - this.updateCurrentState(); - this.parentDashboard = this.ctx.parentDashboard ? - this.ctx.parentDashboard : this.ctx.dashboard; - if (this.syncParentStateParams) { - this.stateSubscription = this.ctx.stateController.dashboardCtrl.dashboardCtx.stateChanged.subscribe(() => { - this.updateCurrentState(); - this.cd.markForCheck(); - }); + const state = this.dashboard.configuration.states[this.stateId]; + if (state) { + for (const layoutId of Object.keys(state.layouts)) { + if (this.defaultAutofillLayout) { + state.layouts[layoutId as DashboardLayoutId].gridSettings.autoFillHeight = true; + state.layouts[layoutId as DashboardLayoutId].gridSettings.mobileAutoFillHeight = true; + } + if (isDefinedAndNotNull(this.defaultMargin)) { + state.layouts[layoutId as DashboardLayoutId].gridSettings.margin = this.defaultMargin; + } + if (isNotEmptyStr(this.defaultBackgroundColor)) { + state.layouts[layoutId as DashboardLayoutId].gridSettings.backgroundColor = this.defaultBackgroundColor; + } + } + this.updateCurrentState(); + this.parentDashboard = this.ctx.parentDashboard ? + this.ctx.parentDashboard : this.ctx.dashboard; + if (this.syncParentStateParams) { + this.parentAliasController = this.parentDashboard.aliasController; + this.stateSubscription = this.ctx.stateController.dashboardCtrl.dashboardCtx.stateChanged.subscribe(() => { + this.updateCurrentState(); + this.cd.markForCheck(); + }); + } + } else { + this.stateExists = false; } } diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-toolbar.component.html b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-toolbar.component.html index de15c409ec..d7fb4c3f88 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-toolbar.component.html +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-toolbar.component.html @@ -1,6 +1,6 @@ + + +
+
+
{{ headerTitle }}
+
{{ headerSubtitle }}
+
+
+ +
+ + +
+
+
+ + + + + + + + +
+ diff --git a/ui-ngx/src/app/modules/home/components/entity/entity-details-page.component.scss b/ui-ngx/src/app/modules/home/components/entity/entity-details-page.component.scss new file mode 100644 index 0000000000..27b3d4f2f8 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/entity/entity-details-page.component.scss @@ -0,0 +1,119 @@ +/** + * Copyright © 2016-2022 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@import "../../../../../scss/constants"; + +:host { + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + overflow: hidden; + + .settings-card { + margin: 8px; + padding: 0; + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + + .details-toolbar { + height: 84px; + min-height: 84px; + border-radius: 4px 4px 0 0; + background: #fff; + border-bottom: 1px solid rgba(0, 0, 0, 0.12); + + .mat-toolbar-tools { + padding: 0 8px; + } + + .tb-details-title-header { + min-width: 0; + width: auto; + } + + .tb-details-title { + font-size: 1rem; + font-weight: 500; + + @media #{$mat-gt-sm} { + font-size: 1.2rem; + } + } + + .tb-details-subtitle { + font-size: 0.9rem; + opacity: .8; + } + + .tb-ellipsis { + width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + } + + @media #{$mat-md} { + width: 80%; + } + + @media #{$mat-gt-md} { + width: 60%; + } + + .tb-header-button { + .tb-btn-header { + position: relative !important; + display: inline-block !important; + animation: tbMoveFromTopFade .3s ease both; + + &.tb-hide { + animation: tbMoveToTopFade .3s ease both; + } + } + } + } +} + +:host ::ng-deep { + .tb-help { + .mat-icon-button.mat-primary { + color: rgba(0, 0, 0, 0.52); + } + } + + .mat-card-content { + position: relative; + overflow: hidden; + + > .mat-tab-group { + > .mat-tab-body-wrapper { + position: absolute; + top: 49px; + left: 0; + right: 0; + bottom: 0; + } + > .mat-tab-header { + .mat-tab-label { + min-width: 40px; + } + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/entity/entity-details-page.component.ts b/ui-ngx/src/app/modules/home/components/entity/entity-details-page.component.ts new file mode 100644 index 0000000000..d0b141256e --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/entity/entity-details-page.component.ts @@ -0,0 +1,173 @@ +/// +/// Copyright © 2016-2022 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ComponentFactoryResolver, + HostBinding, + Injector, + OnDestroy, + OnInit +} from '@angular/core'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { EntityTableConfig } from '@home/models/entity/entities-table-config.models'; +import { BaseData, HasId } from '@shared/models/base-data'; +import { ActivatedRoute, Router } from '@angular/router'; +import { FormGroup } from '@angular/forms'; +import { AssetId } from '@shared/models/id/asset-id'; +import { TranslateService } from '@ngx-translate/core'; +import { deepClone } from '@core/utils'; +import { BroadcastService } from '@core/services/broadcast.service'; +import { EntityDetailsPanelComponent } from '@home/components/entity/entity-details-panel.component'; +import { DialogService } from '@core/services/dialog.service'; +import { IEntityDetailsPageComponent } from '@home/models/entity/entity-details-page-component.models'; + +@Component({ + selector: 'tb-entity-details-page', + templateUrl: './entity-details-page.component.html', + styleUrls: ['./entity-details-page.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class EntityDetailsPageComponent extends EntityDetailsPanelComponent implements IEntityDetailsPageComponent, OnInit, OnDestroy { + + headerTitle: string; + headerSubtitle: string; + + isReadOnly = false; + + set entitiesTableConfig(entitiesTableConfig: EntityTableConfig>) { + if (this.entitiesTableConfigValue !== entitiesTableConfig) { + this.entitiesTableConfigValue = entitiesTableConfig; + if (this.entitiesTableConfigValue) { + this.entitiesTableConfigValue.setEntityDetailsPage(this); + this.isEdit = false; + this.entity = null; + } + } + } + + get entitiesTableConfig(): EntityTableConfig> { + return this.entitiesTableConfigValue; + } + + @HostBinding('class') 'tb-absolute-fill'; + + constructor(private route: ActivatedRoute, + private router: Router, + protected injector: Injector, + protected cd: ChangeDetectorRef, + protected componentFactoryResolver: ComponentFactoryResolver, + private broadcast: BroadcastService, + private translate: TranslateService, + private dialogService: DialogService, + protected store: Store) { + super(store, injector, cd, componentFactoryResolver); + this.entitiesTableConfig = this.route.snapshot.data.entitiesTableConfig; + } + + ngOnInit() { + this.headerSubtitle = ''; + this.headerSubtitle = this.translate.instant(this.entitiesTableConfig.entityTranslations.details); + super.init(); + this.entityComponent.isDetailsPage = true; + this.subscriptions.push(this.entityAction.subscribe((action) => { + if (action.action === 'delete') { + this.deleteEntity(action.event, action.entity); + } + })); + this.subscriptions.push(this.route.paramMap.subscribe( paramMap => { + if (this.entitiesTableConfig) { + const entityType = this.entitiesTableConfig.entityType; + const id = paramMap.get('entityId'); + this.currentEntityId = { id, entityType }; + this.reload(); + this.selectedTab = 0; + } + })); + } + + ngOnDestroy() { + super.ngOnDestroy(); + } + + reload(): void { + this.reloadEntity().subscribe(() => { + this.onUpdateEntity(); + }); + } + + onToggleDetailsEditMode() { + if (this.isEdit) { + this.entityComponent.entity = this.entity; + if (this.entityTabsComponent) { + this.entityTabsComponent.entity = this.entity; + } + this.isEdit = !this.isEdit; + } else { + this.isEdit = !this.isEdit; + this.editingEntity = deepClone(this.entity); + this.entityComponent.entity = this.editingEntity; + if (this.entityTabsComponent) { + this.entityTabsComponent.entity = this.editingEntity; + } + if (this.entitiesTableConfig.hideDetailsTabsOnEdit) { + this.selectedTab = 0; + } + } + } + + onApplyDetails() { + this.saveEntity(false).subscribe((entity) => { + if (entity) { + this.onUpdateEntity(); + } + }); + } + + confirmForm(): FormGroup { + return this.detailsForm; + } + + private onUpdateEntity() { + this.broadcast.broadcast('updateBreadcrumb'); + this.isReadOnly = this.entitiesTableConfig.detailsReadonly(this.entity); + this.headerTitle = this.entitiesTableConfig.entityTitle(this.entity); + } + + private deleteEntity($event: Event, entity: BaseData) { + if ($event) { + $event.stopPropagation(); + } + this.dialogService.confirm( + this.entitiesTableConfig.deleteEntityTitle(entity), + this.entitiesTableConfig.deleteEntityContent(entity), + this.translate.instant('action.no'), + this.translate.instant('action.yes'), + true + ).subscribe((result) => { + if (result) { + this.entitiesTableConfig.deleteEntity(entity.id).subscribe( + () => { + this.router.navigate(['../'], {relativeTo: this.route}); + } + ); + } + }); + } +} diff --git a/ui-ngx/src/app/modules/home/components/entity/entity-details-panel.component.html b/ui-ngx/src/app/modules/home/components/entity/entity-details-panel.component.html index c5f1ec3671..cba0b85ce9 100644 --- a/ui-ngx/src/app/modules/home/components/entity/entity-details-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/entity/entity-details-panel.component.html @@ -1,6 +1,6 @@ -
-
+
+ {{('device-profile.condition' | translate) + ': '}} +
+ + {{ 'device-profile.propagate-alarm-to-owner' | translate }} + + + {{ 'device-profile.propagate-alarm-to-tenant' | translate }} +
diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarm.component.scss b/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarm.component.scss index 3546e48ca4..8596462c3e 100644 --- a/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarm.component.scss +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarm.component.scss @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2021 The Thingsboard Authors + * Copyright © 2016-2022 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarm.component.ts b/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarm.component.ts index 20ea89e53c..81ec55261e 100644 --- a/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarm.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarm.component.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2021 The Thingsboard Authors +/// Copyright © 2016-2022 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -97,7 +97,9 @@ export class DeviceProfileAlarmComponent implements ControlValueAccessor, OnInit createRules: [null], clearRule: [null], propagate: [null], - propagateRelationTypes: [null] + propagateRelationTypes: [null], + propagateToOwner: [null], + propagateToTenant: [null] }, { validators: deviceProfileAlarmValidator }); this.alarmFormGroup.valueChanges.subscribe(() => { this.updateModel(); diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarms.component.html b/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarms.component.html index df0d5af5a3..dde3df31c1 100644 --- a/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarms.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarms.component.html @@ -1,6 +1,6 @@
+
diff --git a/ui-ngx/src/app/modules/home/components/relation/relation-table.component.scss b/ui-ngx/src/app/modules/home/components/relation/relation-table.component.scss index dfb94a7383..453c4ab897 100644 --- a/ui-ngx/src/app/modules/home/components/relation/relation-table.component.scss +++ b/ui-ngx/src/app/modules/home/components/relation/relation-table.component.scss @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2021 The Thingsboard Authors + * Copyright © 2016-2022 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,7 @@ :host { width: 100%; height: 100%; + display: block; .tb-entity-table { .tb-entity-table-content { width: 100%; @@ -41,6 +42,9 @@ .table-container { overflow: auto; + .link { + border-bottom: none; + } } .tb-entity-table-info{ diff --git a/ui-ngx/src/app/modules/home/components/relation/relation-table.component.ts b/ui-ngx/src/app/modules/home/components/relation/relation-table.component.ts index b10ed82249..6b2649ec9a 100644 --- a/ui-ngx/src/app/modules/home/components/relation/relation-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/relation/relation-table.component.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2021 The Thingsboard Authors +/// Copyright © 2016-2022 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -14,7 +14,16 @@ /// limitations under the License. /// -import { AfterViewInit, ChangeDetectionStrategy, Component, ElementRef, Input, OnInit, ViewChild } from '@angular/core'; +import { + AfterViewInit, + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ElementRef, + Input, + OnInit, + ViewChild +} from '@angular/core'; import { PageComponent } from '@shared/components/page.component'; import { PageLink } from '@shared/models/page/page-link'; import { MatPaginator } from '@angular/material/paginator'; @@ -38,6 +47,8 @@ import { import { EntityId } from '@shared/models/id/entity-id'; import { RelationsDatasource } from '../../models/datasource/relation-datasource'; import { RelationDialogComponent, RelationDialogData } from '@home/components/relation/relation-dialog.component'; +import { hidePageSizePixelValue } from '@shared/models/constants'; +import { ResizeObserver } from '@juggle/resize-observer'; @Component({ selector: 'tb-relation-table', @@ -56,6 +67,7 @@ export class RelationTableComponent extends PageComponent implements AfterViewIn displayedColumns: string[]; direction: EntitySearchDirection; pageLink: PageLink; + hidePageSize = false; textSearchMode = false; dataSource: RelationsDatasource; @@ -65,6 +77,8 @@ export class RelationTableComponent extends PageComponent implements AfterViewIn viewsInited = false; + private widgetResize$: ResizeObserver; + @Input() set active(active: boolean) { if (this.activeValue !== active) { @@ -100,7 +114,9 @@ export class RelationTableComponent extends PageComponent implements AfterViewIn private entityRelationService: EntityRelationService, public translate: TranslateService, public dialog: MatDialog, - private dialogService: DialogService) { + private dialogService: DialogService, + private cd: ChangeDetectorRef, + private elementRef: ElementRef) { super(store); this.dirtyValue = !this.activeValue; const sortOrder: SortOrder = { property: 'type', direction: Direction.ASC }; @@ -111,6 +127,20 @@ export class RelationTableComponent extends PageComponent implements AfterViewIn } ngOnInit() { + this.widgetResize$ = new ResizeObserver(() => { + const showHidePageSize = this.elementRef.nativeElement.offsetWidth < hidePageSizePixelValue; + if (showHidePageSize !== this.hidePageSize) { + this.hidePageSize = showHidePageSize; + this.cd.markForCheck(); + } + }); + this.widgetResize$.observe(this.elementRef.nativeElement); + } + + ngOnDestroy() { + if (this.widgetResize$) { + this.widgetResize$.disconnect(); + } } updateColumns() { diff --git a/ui-ngx/src/app/modules/home/components/rule-chain/rule-chain-autocomplete.component.html b/ui-ngx/src/app/modules/home/components/rule-chain/rule-chain-autocomplete.component.html index c3ee17288e..fd99e1bc54 100644 --- a/ui-ngx/src/app/modules/home/components/rule-chain/rule-chain-autocomplete.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-chain/rule-chain-autocomplete.component.html @@ -1,6 +1,6 @@ -
Observable; saveMarkerLocation: (e: FormattedData, lat?: number, lng?: number) => Observable; savePolygonLocation: (e: FormattedData, coordinates?: Array) => Observable; + translateService: TranslateService; protected constructor(public ctx: WidgetContext, public $container: HTMLElement, @@ -94,6 +97,7 @@ export default abstract class LeafletMap { this.options = options; this.editPolygons = options.showPolygon && options.editablePolygon; L.Icon.Default.imagePath = '/'; + this.translateService = this.ctx.$injector.get(TranslateService); } public initSettings(options: MapSettings) { @@ -169,6 +173,38 @@ export default abstract class LeafletMap { if (data !== null) { this.selectedEntity = data; this.toggleDrawMode(type); + let tooltipText; + let customTranslation; + switch (type) { + case 'tbMarker': + tooltipText = this.translateService.instant('widgets.maps.tooltips.placeMarker', {entityName: data.entityName}); + // @ts-ignore + this.map.pm.Draw.tbMarker._hintMarker.setTooltipContent(tooltipText); + break; + case 'tbRectangle': + tooltipText = this.translateService.instant('widgets.maps.tooltips.firstVertex', {entityName: data.entityName}); + // @ts-ignore + this.map.pm.Draw.tbRectangle._hintMarker.setTooltipContent(tooltipText); + customTranslation = { + tooltips: { + finishRect: this.translateService.instant('widgets.maps.tooltips.finishRect', {entityName: data.entityName}) + } + }; + this.map.pm.setLang('en', customTranslation, 'en'); + break; + case 'tbPolygon': + tooltipText = this.translateService.instant('widgets.maps.tooltips.firstVertex', {entityName: data.entityName}); + // @ts-ignore + this.map.pm.Draw.tbPolygon._hintMarker.setTooltipContent(tooltipText); + customTranslation = { + tooltips: { + continueLine: this.translateService.instant('widgets.maps.tooltips.continueLine', {entityName: data.entityName}), + finishPoly: this.translateService.instant('widgets.maps.tooltips.finishPoly', {entityName: data.entityName}) + } + }; + this.map.pm.setLang('en', customTranslation); + break; + } } else { // @ts-ignore this.map.pm.Toolbar.toggleButton(type, false); @@ -182,7 +218,7 @@ export default abstract class LeafletMap { addEditControl() { // Customize edit marker - if (this.options.draggableMarker) { + if (this.options.draggableMarker && !this.options.hideDrawControlButton) { const actions = [{ text: L.PM.Utils.getTranslation('actions.cancel'), onClick: () => this.toggleDrawMode('tbMarker') @@ -197,7 +233,7 @@ export default abstract class LeafletMap { } // Customize edit polygon - if (this.editPolygons) { + if (this.editPolygons && !this.options.hideDrawControlButton) { const rectangleActions = [ { text: L.PM.Utils.getTranslation('actions.cancel'), @@ -229,20 +265,28 @@ export default abstract class LeafletMap { }); } - const translateService = this.ctx.$injector.get(TranslateService); - this.map.pm.setLang('en', translateService.instant('widgets.maps'), 'en'); - this.map.pm.addControls({ - position: 'topleft', - drawMarker: false, - drawCircle: false, - drawCircleMarker: false, - drawRectangle: false, - drawPolyline: false, - drawPolygon: false, - editMode: this.editPolygons, - cutPolygon: this.editPolygons, - rotateMode: this.editPolygons - }); + this.map.pm.setLang('en', this.translateService.instant('widgets.maps'), 'en'); + if (!this.options.hideAllControlButton) { + this.map.pm.addControls({ + position: 'topleft', + drawControls: !this.options.hideDrawControlButton, + drawMarker: false, + drawCircle: false, + drawCircleMarker: false, + drawRectangle: false, + drawPolyline: false, + drawPolygon: false, + dragMode: !this.options.hideEditControlButton, + editMode: this.editPolygons && !this.options.hideEditControlButton, + cutPolygon: this.editPolygons && !this.options.hideEditControlButton, + removalMode: !this.options.hideRemoveControlButton, + rotateMode: this.editPolygons && !this.options.hideEditControlButton + }); + } + + if (this.options.initDragMode) { + this.map.pm.enableGlobalDragMode(); + } this.map.on('pm:create', (e) => { if (e.shape === 'tbMarker') { @@ -344,6 +388,7 @@ export default abstract class LeafletMap { } if (this.options.draggableMarker || this.editPolygons) { map.pm.setGlobalOptions({ snappable: false } as L.PM.GlobalOptions); + map.pm.applyGlobalOptions(); this.addEditControl(); } else { this.map.pm.disableDraw(); @@ -482,16 +527,72 @@ export default abstract class LeafletMap { } this.updateMarkers(formattedData, false); this.updateBoundsInternal(); - if (this.options.draggableMarker) { - const foundEntityWithoutLocation = formattedData.some(mData => !this.convertPosition(mData)); - this.map.pm.Toolbar.setButtonDisabled('tbMarker', !foundEntityWithoutLocation); - this.datasources = formattedData; - } - if (this.editPolygons) { - const foundEntityWithoutPolygon = formattedData.some(pData => !this.isValidPolygonPosition(pData)); - this.map.pm.Toolbar.setButtonDisabled('tbPolygon', !foundEntityWithoutPolygon); - this.map.pm.Toolbar.setButtonDisabled('tbRectangle', !foundEntityWithoutPolygon); - this.datasources = formattedData; + if (this.options.draggableMarker || this.editPolygons) { + let foundEntityWithoutLocation = false; + let foundEntityWithLocation = false; + let foundEntityWithoutPolygon = false; + let foundEntityWithPolygon = false; + + if (this.options.draggableMarker && !this.options.hideDrawControlButton && !this.options.hideAllControlButton) { + for (const mData of formattedData) { + const position = this.convertPosition(mData); + if (!position) { + foundEntityWithoutLocation = true; + } else if (!!position) { + foundEntityWithLocation = true; + } + if (foundEntityWithoutLocation && foundEntityWithLocation) { + break; + } + } + this.map.pm.Toolbar.setButtonDisabled('tbMarker', !foundEntityWithoutLocation); + this.datasources = formattedData; + } + + if (this.editPolygons && !this.options.hideDrawControlButton && !this.options.hideAllControlButton) { + for (const pData of formattedData) { + const isValidPolygon = this.isValidPolygonPosition(pData); + if (!isValidPolygon) { + foundEntityWithoutPolygon = true; + } else if (isValidPolygon) { + foundEntityWithPolygon = true; + } + if (foundEntityWithoutPolygon && foundEntityWithPolygon) { + break; + } + } + this.map.pm.Toolbar.setButtonDisabled('tbPolygon', !foundEntityWithoutPolygon); + this.map.pm.Toolbar.setButtonDisabled('tbRectangle', !foundEntityWithoutPolygon); + this.datasources = formattedData; + } + if (!this.options.hideRemoveControlButton && !this.options.hideAllControlButton) { + const disabledButton = !foundEntityWithLocation && !foundEntityWithPolygon; + if (disabledButton && this.map.pm.globalRemovalModeEnabled()) { + this.map.pm.toggleGlobalRemovalMode(); + } + this.map.pm.Toolbar.setButtonDisabled('removalMode', disabledButton); + } + if (!this.options.hideEditControlButton && !this.options.hideAllControlButton) { + const disabledButton = !foundEntityWithLocation && !foundEntityWithPolygon; + if (disabledButton) { + // @ts-ignore + this.map.pm.Toolbar.toggleButton('dragMode', false); + } + if (this.editPolygons && !foundEntityWithPolygon) { + // @ts-ignore + this.map.pm.Toolbar.toggleButton('editMode', false); + // @ts-ignore + this.map.pm.Toolbar.toggleButton('cutPolygon', false); + // @ts-ignore + this.map.pm.Toolbar.toggleButton('rotateMode', false); + } + this.map.pm.Toolbar.setButtonDisabled('dragMode', disabledButton); + if (this.editPolygons) { + this.map.pm.Toolbar.setButtonDisabled('editMode', !foundEntityWithPolygon); + this.map.pm.Toolbar.setButtonDisabled('cutPolygon', !foundEntityWithPolygon); + this.map.pm.Toolbar.setButtonDisabled('rotateMode', !foundEntityWithPolygon); + } + } } } else { this.updatePending = true; @@ -555,7 +656,7 @@ export default abstract class LeafletMap { updatedMarkers.push(m); } } else { - m = this.createMarker(data.entityName, data, markersData, this.options as MarkerSettings, updateBounds, callback); + m = this.createMarker(data.entityName, data, markersData, this.options, updateBounds, callback); if (m) { createdMarkers.push(m); } @@ -569,7 +670,7 @@ export default abstract class LeafletMap { } }); this.markersData = markersData; - if ((this.options as MarkerSettings).useClusterMarkers) { + if (this.options.useClusterMarkers) { if (createdMarkers.length) { this.markersCluster.addLayers(createdMarkers.map(marker => marker.leafletMarker)); } @@ -589,7 +690,7 @@ export default abstract class LeafletMap { this.saveLocation(data, this.convertToCustomFormat(e.target._latlng)).subscribe(); } - private createMarker(key: string, data: FormattedData, dataSources: FormattedData[], settings: MarkerSettings, + private createMarker(key: string, data: FormattedData, dataSources: FormattedData[], settings: UnitedMapSettings, updateBounds = true, callback?): Marker { const newMarker = new Marker(this, this.convertPosition(data), settings, data, dataSources, this.dragMarker); if (callback) { @@ -597,7 +698,7 @@ export default abstract class LeafletMap { callback(data, true); }); } - if (this.bounds && updateBounds && !(this.options as MarkerSettings).useClusterMarkers) { + if (this.bounds && updateBounds && !this.options.useClusterMarkers) { this.fitBounds(this.bounds.extend(newMarker.leafletMarker.getLatLng())); } this.markers.set(key, newMarker); @@ -786,7 +887,7 @@ export default abstract class LeafletMap { this.saveLocation(data, this.convertPolygonToCustomFormat(coordinates)).subscribe(() => {}); } - createPolygon(polyData: FormattedData, dataSources: FormattedData[], settings: PolygonSettings, updateBounds = true) { + createPolygon(polyData: FormattedData, dataSources: FormattedData[], settings: UnitedMapSettings, updateBounds = true) { const polygon = new Polygon(this.map, polyData, dataSources, settings, this.dragPolygonVertex); if (updateBounds) { const bounds = polygon.leafletPoly.getBounds(); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-models.ts index e65479f588..bbf3194b71 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-models.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2021 The Thingsboard Authors +/// Copyright © 2016-2022 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -33,6 +33,7 @@ export type MapSettings = { posFunction: PosFuncton; defaultZoomLevel?: number; disableScrollZooming?: boolean; + disableZoomControl?: boolean; minZoomLevel?: number; useClusterMarkers: boolean; latKeyName?: string; @@ -192,6 +193,15 @@ export type PolylineSettings = { strokeWeightFunction: GenericFunction; }; +export interface EditorSettings { + snappable: boolean; + initDragMode: boolean; + hideAllControlButton: boolean; + hideDrawControlButton: boolean; + hideEditControlButton: boolean; + hideRemoveControlButton: boolean; +} + export interface HistorySelectSettings { buttonColor: string; } @@ -231,7 +241,7 @@ export interface TripAnimationSettings extends PolygonSettings { export type actionsHandler = ($event: Event, datasource: Datasource) => void; -export type UnitedMapSettings = MapSettings & PolygonSettings & MarkerSettings & PolylineSettings & TripAnimationSettings; +export type UnitedMapSettings = MapSettings & PolygonSettings & MarkerSettings & PolylineSettings & TripAnimationSettings & EditorSettings; export const defaultSettings: any = { xPosKeyName: 'xPos', @@ -271,7 +281,13 @@ export const defaultSettings: any = { draggableMarker: false, editablePolygon: false, fitMapBounds: true, - mapPageSize: DEFAULT_MAP_PAGE_SIZE + mapPageSize: DEFAULT_MAP_PAGE_SIZE, + snappable: false, + initDragMode: false, + hideAllControlButton: false, + hideDrawControlButton: false, + hideEditControlButton: false, + hideRemoveControlButton: false }; export const hereProviders = [ diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.interface.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.interface.ts index 5a74247f2f..2e26bea53e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.interface.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.interface.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2021 The Thingsboard Authors +/// Copyright © 2016-2022 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget2.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget2.ts index 565887b160..55f68bbb00 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget2.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget2.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2021 The Thingsboard Authors +/// Copyright © 2016-2022 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -18,6 +18,7 @@ import { defaultSettings, FormattedData, hereProviders, MapProviders, UnitedMapS import LeafletMap from './leaflet-map'; import { commonMapSettingsSchema, + editorSettingSchema, mapPolygonSchema, markerClusteringSettingsSchema, markerClusteringSettingsSchemaLeaflet, @@ -115,6 +116,8 @@ export class MapWidgetController implements MapWidgetInterface { `model.useClusterMarkers === true && model.provider !== "image-map"`)]); addToSchema(schema, clusteringSchema); addGroupInfo(schema, 'Markers Clustering Settings'); + addToSchema(schema, addCondition(editorSettingSchema, '(model.editablePolygon === true || model.draggableMarker === true)')); + addGroupInfo(schema, 'Editor settings'); } return schema; } @@ -297,8 +300,8 @@ export class MapWidgetController implements MapWidgetInterface { } resize() { - this.map?.invalidateSize(); - this.map.onResize(); + this.map.onResize(); + this.map?.invalidateSize(); } destroy() { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/maps-utils.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/maps-utils.ts index 9c2fbcdcd5..0482f4cec5 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/maps-utils.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/maps-utils.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2021 The Thingsboard Authors +/// Copyright © 2016-2022 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/markers.scss b/ui-ngx/src/app/modules/home/components/widget/lib/maps/markers.scss index f45d08f9e9..2d0740ea0d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/markers.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/markers.scss @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2021 The Thingsboard Authors + * Copyright © 2016-2022 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/markers.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/markers.ts index 85020b7f33..1962828710 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/markers.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/markers.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2021 The Thingsboard Authors +/// Copyright © 2016-2022 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -14,19 +14,17 @@ /// limitations under the License. /// -import L, { Icon, LeafletMouseEvent } from 'leaflet'; -import { FormattedData, MarkerIconInfo, MarkerIconReadyFunction, MarkerImageInfo, MarkerSettings } from './map-models'; +import L, { LeafletMouseEvent } from 'leaflet'; import { - bindPopupActions, - createTooltip, -} from './maps-utils'; -import { - aspectCache, - fillPattern, - parseWithTranslation, - processPattern, - safeExecute -} from './common-maps-utils'; + FormattedData, + MarkerIconInfo, + MarkerIconReadyFunction, + MarkerImageInfo, + MarkerSettings, + UnitedMapSettings +} from './map-models'; +import { bindPopupActions, createTooltip } from './maps-utils'; +import { aspectCache, fillPattern, parseWithTranslation, processPattern, safeExecute } from './common-maps-utils'; import tinycolor from 'tinycolor2'; import { isDefined, isDefinedAndNotNull } from '@core/utils'; import LeafletMap from './leaflet-map'; @@ -40,10 +38,13 @@ export class Marker { data: FormattedData; dataSources: FormattedData[]; - constructor(private map: LeafletMap, private location: L.LatLng, public settings: MarkerSettings, + constructor(private map: LeafletMap, private location: L.LatLng, public settings: UnitedMapSettings, data?: FormattedData, dataSources?, onDragendListener?) { this.setDataSources(data, dataSources); - this.leafletMarker = L.marker(location, {pmIgnore: !settings.draggableMarker}); + this.leafletMarker = L.marker(location, { + pmIgnore: !settings.draggableMarker, + snapIgnore: !settings.snappable + }); this.markerOffset = [ isDefined(settings.markerOffsetX) ? settings.markerOffsetX : 0.5, @@ -64,16 +65,24 @@ export class Marker { if (this.settings.markerClick) { this.leafletMarker.on('click', (event: LeafletMouseEvent) => { - for (const action in this.settings.markerClick) { - if (typeof (this.settings.markerClick[action]) === 'function') { - this.settings.markerClick[action](event.originalEvent, this.data.$datasource); - } + for (const action in this.settings.markerClick) { + if (typeof (this.settings.markerClick[action]) === 'function') { + this.settings.markerClick[action](event.originalEvent, this.data.$datasource); } + } }); } if (settings.draggableMarker && onDragendListener) { - this.leafletMarker.on('pm:dragend', (e) => onDragendListener(e, this.data)); + this.leafletMarker.on('pm:dragstart', (e) => { + (this.leafletMarker.dragging as any)._draggable = { _moved: true }; + (this.leafletMarker.dragging as any)._enabled = true; + }); + this.leafletMarker.on('pm:dragend', (e) => { + onDragendListener(e, this.data); + delete (this.leafletMarker.dragging as any)._draggable; + delete (this.leafletMarker.dragging as any)._enabled; + }); } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/polygon.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/polygon.ts index 908952e7d2..223522bb0a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/polygon.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/polygon.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2021 The Thingsboard Authors +/// Copyright © 2016-2022 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -23,7 +23,7 @@ import { processPattern, safeExecute } from './common-maps-utils'; -import { FormattedData, MarkerSettings, PolygonSettings } from './map-models'; +import { FormattedData, PolygonSettings, UnitedMapSettings } from './map-models'; export class Polygon { @@ -32,7 +32,7 @@ export class Polygon { data: FormattedData; dataSources: FormattedData[]; - constructor(public map, data: FormattedData, dataSources: FormattedData[], private settings: PolygonSettings, + constructor(public map, data: FormattedData, dataSources: FormattedData[], private settings: UnitedMapSettings, private onDragendListener?) { this.dataSources = dataSources; this.data = data; @@ -47,7 +47,8 @@ export class Polygon { weight: settings.polygonStrokeWeight, fillOpacity: settings.polygonOpacity, opacity: settings.polygonStrokeOpacity, - pmIgnore: !settings.editablePolygon + pmIgnore: !settings.editablePolygon, + snapIgnore: !settings.snappable }).addTo(this.map); this.updateLabel(settings); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/polyline.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/polyline.ts index 895dc15763..0c3b48d68e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/polyline.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/polyline.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2021 The Thingsboard Authors +/// Copyright © 2016-2022 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/google-map.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/google-map.ts index 00d9353218..9ac73d2749 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/google-map.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/google-map.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2021 The Thingsboard Authors +/// Copyright © 2016-2022 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -38,6 +38,7 @@ export class GoogleMap extends LeafletMap { this.loadGoogle(() => { const map = L.map($container, { attributionControl: false, + zoomControl: !this.options.disableZoomControl, tap: L.Browser.safari && L.Browser.mobile }).setView(options?.defaultCenterPosition, options?.defaultZoomLevel || DEFAULT_ZOOM_LEVEL); (L.gridLayer as any).googleMutant({ diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/here-map.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/here-map.ts index caac9a78b4..bc261b5c63 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/here-map.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/here-map.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2021 The Thingsboard Authors +/// Copyright © 2016-2022 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -23,7 +23,8 @@ export class HEREMap extends LeafletMap { constructor(ctx: WidgetContext, $container, options: UnitedMapSettings) { super(ctx, $container, options); const map = L.map($container, { - tap: L.Browser.safari && L.Browser.mobile + tap: L.Browser.safari && L.Browser.mobile, + zoomControl: !this.options.disableZoomControl }).setView(options?.defaultCenterPosition, options?.defaultZoomLevel || DEFAULT_ZOOM_LEVEL); const tileLayer = (L.tileLayer as any).provider(options.mapProviderHere || 'HERE.normalDay', options.credentials); tileLayer.addTo(map); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/image-map.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/image-map.ts index be427f55cf..087b9febc8 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/image-map.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/image-map.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2021 The Thingsboard Authors +/// Copyright © 2016-2022 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -29,6 +29,7 @@ import { DataSet, DatasourceType, widgetType } from '@shared/models/widget.model import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; import { WidgetSubscriptionOptions } from '@core/api/widget-api.models'; import { isDefinedAndNotNull, isEmptyStr } from '@core/utils'; +import { EntityDataPageLink } from '@shared/models/query/query.models'; const maxZoom = 4; // ? @@ -88,6 +89,7 @@ export class ImageMap extends LeafletMap { let isUpdate = false; const imageUrlSubscriptionOptions: WidgetSubscriptionOptions = { datasources, + hasDataPageLink: true, useDashboardTimewindow: false, type: widgetType.latest, callbacks: { @@ -97,7 +99,15 @@ export class ImageMap extends LeafletMap { } } }; - this.ctx.subscriptionApi.createSubscription(imageUrlSubscriptionOptions, true).subscribe(() => { }); + this.ctx.subscriptionApi.createSubscription(imageUrlSubscriptionOptions, true).subscribe((subscription) => { + const pageLink: EntityDataPageLink = { + page: 0, + pageSize: 1, + textSearch: null, + dynamic: true + }; + subscription.subscribeAllForPaginatedData(pageLink, null); + }); return this.imageFromAlias(result); } @@ -154,32 +164,32 @@ export class ImageMap extends LeafletMap { const southWest = this.pointToLatLng(-padding, h + padding); const northEast = this.pointToLatLng(w + padding, -padding); const maxBounds = new L.LatLngBounds(southWest, northEast); + (this.map as any)._enforcingBounds = true; this.map.setMaxBounds(maxBounds); if (lastCenterPos) { lastCenterPos.x *= w; lastCenterPos.y *= h; const center = this.pointToLatLng(lastCenterPos.x, lastCenterPos.y); - setTimeout(() => { - this.map.panTo(center, { animate: false }); - }, 0); + this.map.panTo(center, { animate: false }); } + (this.map as any)._enforcingBounds = false; } onResize(updateImage?: boolean) { let width = this.$container.clientWidth; if (width > 0 && this.aspect) { - let height = width / this.aspect; + let height = Math.round(width / this.aspect); const imageMapHeight = this.$container.clientHeight; if (imageMapHeight > 0 && height > imageMapHeight) { height = imageMapHeight; - width = height * this.aspect; + width = Math.round(height * this.aspect); } width *= maxZoom; const prevWidth = this.width; const prevHeight = this.height; if (this.width !== width || updateImage) { this.width = width; - this.height = width / this.aspect; + this.height = Math.round(width / this.aspect); if (!this.map) { this.initMap(updateImage); } else { @@ -187,7 +197,9 @@ export class ImageMap extends LeafletMap { lastCenterPos.x /= prevWidth; lastCenterPos.y /= prevHeight; this.updateBounds(updateImage, lastCenterPos); - this.map.invalidateSize(true); + (this.map as any)._enforcingBounds = true; + this.map.invalidateSize(false); + (this.map as any)._enforcingBounds = false; this.updateMarkers(this.markersData); if (this.options.draggableMarker && this.addMarkers.length) { this.addMarkers.forEach((marker) => { @@ -217,6 +229,7 @@ export class ImageMap extends LeafletMap { maxZoom, scrollWheelZoom: !this.options.disableScrollZooming, center, + zoomControl: !this.options.disableZoomControl, zoom: 1, crs: L.CRS.Simple, attributionControl: false, @@ -257,7 +270,7 @@ export class ImageMap extends LeafletMap { return L.CRS.Simple.pointToLatLng({ x, y } as L.PointExpression, maxZoom - 1); } - latLngToPoint(latLng: LatLngLiteral) { + latLngToPoint(latLng: LatLngLiteral): L.Point { return L.CRS.Simple.latLngToPoint(latLng, maxZoom - 1); } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/index.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/index.ts index 50c0709fb3..e0f7fc37db 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/index.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/index.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2021 The Thingsboard Authors +/// Copyright © 2016-2022 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/openstreet-map.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/openstreet-map.ts index 01eb04ea40..1a27f26931 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/openstreet-map.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/openstreet-map.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2021 The Thingsboard Authors +/// Copyright © 2016-2022 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -23,6 +23,7 @@ export class OpenStreetMap extends LeafletMap { constructor(ctx: WidgetContext, $container, options: UnitedMapSettings) { super(ctx, $container, options); const map = L.map($container, { + zoomControl: !this.options.disableZoomControl, tap: L.Browser.safari && L.Browser.mobile }).setView(options?.defaultCenterPosition, options?.defaultZoomLevel || DEFAULT_ZOOM_LEVEL); let tileLayer; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/tencent-map.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/tencent-map.ts index 5e71d92e5d..fb53f15b3e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/tencent-map.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/tencent-map.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2021 The Thingsboard Authors +/// Copyright © 2016-2022 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -25,6 +25,7 @@ export class TencentMap extends LeafletMap { super(ctx, $container, options); const txUrl = 'http://rt{s}.map.gtimg.com/realtimerender?z={z}&x={x}&y={y}&type=vector&style=0'; const map = L.map($container, { + zoomControl: !this.options.disableZoomControl, tap: L.Browser.safari && L.Browser.mobile }).setView(options?.defaultCenterPosition, options?.defaultZoomLevel || DEFAULT_ZOOM_LEVEL); const txLayer = L.tileLayer(txUrl, { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/schemes.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/schemes.ts index 2924ff688e..6f972e9d3f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/schemes.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/schemes.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2021 The Thingsboard Authors +/// Copyright © 2016-2022 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -269,6 +269,11 @@ export const commonMapSettingsSchema = type: 'boolean', default: false }, + disableZoomControl: { + title: 'Disable zoom control buttons', + type: 'boolean', + default: false + }, latKeyName: { title: 'Latitude key name', type: 'string', @@ -424,6 +429,7 @@ export const commonMapSettingsSchema = 'mapPageSize', 'draggableMarker', 'disableScrollZooming', + 'disableZoomControl', { key: 'latKeyName', condition: 'model.provider !== "image-map"' @@ -1311,3 +1317,61 @@ export const providerSets: { [key: string]: IProvider } = { name: 'image-map' } }; + +export const editorSettingSchema = + { + schema: { + title: 'Editor settings', + type: 'object', + properties: { + snappable: { + title: 'Enable snapping to other vertices for precision drawing', + type: 'boolean', + default: false + }, + initDragMode: { + title: 'Initialize map in draggable mode', + type: 'boolean', + default: false + }, + hideAllControlButton: { + title: 'Hide all edit control buttons', + type: 'boolean', + default: false + }, + hideDrawControlButton: { + title: 'Hide draw buttons', + type: 'boolean', + default: false + }, + hideEditControlButton: { + title: 'Hide edit buttons', + type: 'boolean', + default: false + }, + hideRemoveControlButton: { + title: 'Hide remove button', + type: 'boolean', + default: false + }, + }, + required: [] + }, + form: [ + 'snappable', + 'initDragMode', + 'hideAllControlButton', + { + key: 'hideDrawControlButton', + condition: 'model.hideAllControlButton == false' + }, + { + key: 'hideEditControlButton', + condition: 'model.hideAllControlButton == false' + }, + { + key: 'hideRemoveControlButton', + condition: 'model.hideAllControlButton == false' + } + ] + }; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/markdown-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/markdown-widget.component.html index b808897845..2820a8bade 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/markdown-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/markdown-widget.component.html @@ -1,6 +1,6 @@ + + +

{{ 'widgets.persistent-table.add-title' | translate }}

+ + +
+ + +
+
+
+
+ + {{ rpcMessageTypeText }} + +
+
+ + widgets.persistent-table.method + + + {{'widgets.persistent-table.method-error' | translate}} + + + {{'widgets.persistent-table.white-space-error' | translate}} + + + + widgets.persistent-table.retries + + +
+
+ + +
+ + + + widgets.persistent-table.additional-info + + + + + + + +
+
+
+
+ + +
+ +
+
+ diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-add-dialog.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-add-dialog.component.scss new file mode 100644 index 0000000000..4958d4efa8 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-add-dialog.component.scss @@ -0,0 +1,30 @@ +/** + * Copyright © 2016-2022 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host ::ng-deep { + .add-dialog { + + .params-json-editor, + .additional-json-editor { + .tb-json-object-panel { + margin: 0 0 16px; + } + + .mat-expansion-panel-body { + padding-bottom: 0 !important; + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-add-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-add-dialog.component.ts new file mode 100644 index 0000000000..6e81d98ce7 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-add-dialog.component.ts @@ -0,0 +1,75 @@ +/// +/// Copyright © 2016-2022 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, OnInit } from '@angular/core'; +import { DialogComponent } from '@shared/components/dialog.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { Router } from '@angular/router'; +import { MatDialogRef } from '@angular/material/dialog'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { RequestData } from '@shared/models/rpc.models'; +import { TranslateService } from '@ngx-translate/core'; + +@Component({ + selector: 'tb-persistent-add-dialog', + templateUrl: './persistent-add-dialog.component.html', + styleUrls: ['./persistent-add-dialog.component.scss'] +}) + +export class PersistentAddDialogComponent extends DialogComponent implements OnInit { + + public persistentFormGroup: FormGroup; + public rpcMessageTypeText: string; + + private requestData: RequestData = null; + + constructor(protected store: Store, + protected router: Router, + public dialogRef: MatDialogRef, + private fb: FormBuilder, + private translate: TranslateService) { + super(store, router, dialogRef); + + this.persistentFormGroup = this.fb.group( + { + method: ['', [Validators.required, Validators.pattern(/^\S+$/)]], + oneWayElseTwoWay: [false], + retries: [null, [Validators.pattern(/^-?[0-9]+$/), Validators.min(0)]], + params: [null], + additionalInfo: [null] + } + ); + } + + save() { + this.requestData = this.persistentFormGroup.value; + this.close(); + } + + ngOnInit(): void { + this.rpcMessageTypeText = this.translate.instant('widgets.persistent-table.message-types.false'); + this.persistentFormGroup.get('oneWayElseTwoWay').valueChanges.subscribe( + () => { + this.rpcMessageTypeText = this.translate.instant(`widgets.persistent-table.message-types.${this.persistentFormGroup.get('oneWayElseTwoWay').value}`); + } + ); + } + + close(): void { + this.dialogRef.close(this.requestData); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-details-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-details-dialog.component.html new file mode 100644 index 0000000000..b9e57b4bbd --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-details-dialog.component.html @@ -0,0 +1,118 @@ + +
+ +

{{ persistentFormGroup.get('rpcId').value }}

+ + +
+ + +
+
+
+
+ + widgets.persistent-table.created-time + + + + widgets.persistent-table.expiration-time + + +
+
+ + widgets.persistent-table.message-type + + + + widgets.persistent-table.status + + + + widgets.persistent-table.method + + + + widgets.persistent-table.retries + + +
+ + + + + + {{ 'widgets.persistent-table.response' | translate }} + + + + + + + + {{ 'widgets.persistent-table.params' | translate }} + + + + + + + + + + {{ 'widgets.persistent-table.additional-info' | translate }} + + + + + + + + +
+
+
+ + +
+ +
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-details-dialog.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-details-dialog.component.scss new file mode 100644 index 0000000000..9025d6199e --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-details-dialog.component.scss @@ -0,0 +1,34 @@ +/** + * Copyright © 2016-2022 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host ::ng-deep { + .rpc-dialog { + .mat-expansion-panel-body { + padding-bottom: 0 !important; + } + + .tb-json-object-panel { + margin: 0 0 16px 0; + } + } + + .tb-audit-log-response-data { + width: 100%; + min-width: 400px; + height: 100%; + min-height: 100px; + border: 1px solid #c0c0c0; + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-details-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-details-dialog.component.ts new file mode 100644 index 0000000000..a072ea312b --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-details-dialog.component.ts @@ -0,0 +1,126 @@ +/// +/// Copyright © 2016-2022 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, ElementRef, Inject, OnInit, ViewChild } from '@angular/core'; +import { DialogComponent } from '@shared/components/dialog.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { Router } from '@angular/router'; +import { DatePipe } from '@angular/common'; +import { TranslateService } from '@ngx-translate/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { FormBuilder, FormGroup } from '@angular/forms'; +import { DeviceService } from '@core/http/device.service'; +import { PersistentRpc, RpcStatus, rpcStatusColors, rpcStatusTranslation } from '@shared/models/rpc.models'; +import { NULL_UUID } from '@shared/models/id/has-uuid'; +import { DialogService } from '@core/services/dialog.service'; + +export interface PersistentDetailsDialogData { + persistentRequest: PersistentRpc; + allowDelete: boolean; +} + +@Component({ + selector: 'tb-persistent-details-dialog', + templateUrl: './persistent-details-dialog.component.html', + styleUrls: ['./persistent-details-dialog.component.scss'] +}) + +export class PersistentDetailsDialogComponent extends DialogComponent implements OnInit { + + @ViewChild('responseDataEditor', {static: true}) + responseDataEditorElmRef: ElementRef; + + public persistentFormGroup: FormGroup; + public rpcStatusColorsMap = rpcStatusColors; + public rpcStatus = RpcStatus; + public allowDelete: boolean; + + private persistentUpdated = false; + private responseData: string; + + constructor(protected store: Store, + protected router: Router, + private datePipe: DatePipe, + private translate: TranslateService, + @Inject(MAT_DIALOG_DATA) public data: PersistentDetailsDialogData, + public dialogRef: MatDialogRef, + private dialogService: DialogService, + private deviceService: DeviceService, + private fb: FormBuilder) { + super(store, router, dialogRef); + + this.allowDelete = data.allowDelete; + + this.persistentFormGroup = this.fb.group( + { + rpcId: [''], + createdTime: [''], + expirationTime: [''], + messageType: [''], + status: [''], + method: [''], + params: [''], + retries: [''], + response: [''], + additionalInfo: [''] + } + ); + this.loadPersistentFields(data.persistentRequest); + this.responseData = JSON.stringify(data.persistentRequest.response, null, 2); + } + + loadPersistentFields(request: PersistentRpc) { + this.persistentFormGroup.patchValue({ + rpcId: this.translate.instant('widgets.persistent-table.details-title') + request.id.id, + createdTime: this.datePipe.transform(request.createdTime, 'yyyy-MM-dd HH:mm:ss'), + expirationTime: this.datePipe.transform(request.expirationTime, 'yyyy-MM-dd HH:mm:ss'), + messageType: this.translate.instant('widgets.persistent-table.message-types.' + request.request.oneway), + status: this.translate.instant(rpcStatusTranslation.get(request.status)), + method: request.request.body.method, + retries: request.request.retries || null, + response: request.response || null, + params: JSON.parse(request.request.body.params) || null, + additionalInfo: request.additionalInfo || null + }, {emitEvent: false}); + } + + ngOnInit(): void { + } + + close(): void { + this.dialogRef.close(this.persistentUpdated); + } + + deleteRpcRequest() { + const persistentRpc = this.data.persistentRequest; + if (persistentRpc && persistentRpc.id && persistentRpc.id.id !== NULL_UUID) { + this.dialogService.confirm( + this.translate.instant('widgets.persistent-table.delete-request-title'), + this.translate.instant('widgets.persistent-table.delete-request-text'), + this.translate.instant('action.no'), + this.translate.instant('action.yes') + ).subscribe((res) => { + if (res) { + this.deviceService.deletePersistedRpc(persistentRpc.id.id).subscribe(() => { + this.persistentUpdated = true; + this.close(); + }); + } + }); + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-filter-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-filter-panel.component.html new file mode 100644 index 0000000000..b3bb295ed2 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-filter-panel.component.html @@ -0,0 +1,44 @@ + +
+ + widgets.persistent-table.rpc-status-list + + + {{ 'widgets.persistent-table.rpc-search-status-all' | translate }} + + + {{ rpcSearchStatusTranslationMap.get(searchStatus) | translate }} + + + +
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-filter-panel.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-filter-panel.component.scss new file mode 100644 index 0000000000..45ba061843 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-filter-panel.component.scss @@ -0,0 +1,36 @@ +/** + * Copyright © 2016-2022 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + width: 100%; + height: 100%; + min-width: 300px; + overflow: hidden; + background: #fff; + border-radius: 4px; + box-shadow: + 0 7px 8px -4px rgba(0, 0, 0, .2), + 0 13px 19px 2px rgba(0, 0, 0, .14), + 0 5px 24px 4px rgba(0, 0, 0, .12); + + .mat-content { + overflow: hidden; + background-color: #fff; + } + + .mat-padding { + padding: 16px; + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-filter-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-filter-panel.component.ts new file mode 100644 index 0000000000..e3d46dfca3 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-filter-panel.component.ts @@ -0,0 +1,67 @@ +/// +/// Copyright © 2016-2022 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Inject, InjectionToken } from '@angular/core'; +import { FormBuilder, FormGroup } from '@angular/forms'; +import { OverlayRef } from '@angular/cdk/overlay'; +import { RpcStatus, rpcStatusTranslation } from '@shared/models/rpc.models'; +import { TranslateService } from '@ngx-translate/core'; + +export const PERSISTENT_FILTER_PANEL_DATA = new InjectionToken('AlarmFilterPanelData'); + +export interface PersistentFilterPanelData { + rpcStatus: RpcStatus; +} + +@Component({ + selector: 'tb-persistent-filter-panel', + templateUrl: './persistent-filter-panel.component.html', + styleUrls: ['./persistent-filter-panel.component.scss'] +}) +export class PersistentFilterPanelComponent { + + public persistentFilterFormGroup: FormGroup; + public result: PersistentFilterPanelData; + public rpcSearchStatusTranslationMap = rpcStatusTranslation; + public rpcSearchPlaceholder: string; + + public persistentSearchStatuses = Object.keys(RpcStatus); + + constructor(@Inject(PERSISTENT_FILTER_PANEL_DATA) + public data: PersistentFilterPanelData, + public overlayRef: OverlayRef, + private fb: FormBuilder, + private translate: TranslateService) { + this.persistentFilterFormGroup = this.fb.group( + { + rpcStatus: this.data.rpcStatus + } + ); + this.rpcSearchPlaceholder = this.translate.instant('widgets.persistent-table.any-status'); + } + + update() { + this.result = { + rpcStatus: this.persistentFilterFormGroup.get('rpcStatus').value + }; + this.overlayRef.dispose(); + } + + cancel() { + this.overlayRef.dispose(); + } +} + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-table.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-table.component.html new file mode 100644 index 0000000000..7614d7e0e6 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-table.component.html @@ -0,0 +1,126 @@ + +
+
+
+ + + + {{ 'widgets.persistent-table.rpc-id' | translate }} + + + {{ column.id.id }} + + + + + {{ 'widgets.persistent-table.created-time' | translate }} + + + {{ column.createdTime | date:'yyyy-MM-dd HH:mm:ss' }} + + + + + {{ 'widgets.persistent-table.expiration-time' | translate }} + + + {{ column.expirationTime | date:'yyyy-MM-dd HH:mm:ss' }} + + + + + {{ 'widgets.persistent-table.status' | translate }} + + + {{ rpcStatusTranslation.get(column.status) | translate }} + + + + + {{ 'widgets.persistent-table.message-type' | translate }} + + + {{ 'widgets.persistent-table.message-types.' + column.request.oneway | translate }} + + + + + {{ 'widgets.persistent-table.method' | translate }} + + + {{ column.request.body.method }} + + + + + + +
+ + + +
+
+ + + + + + +
+
+
+ + +
+ {{ noDataDisplayMessageText }} + {{ 'common.loading' | translate }} +
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-table.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-table.component.scss new file mode 100644 index 0000000000..842d53d143 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-table.component.scss @@ -0,0 +1,53 @@ +/** + * Copyright © 2016-2022 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + width: 100%; + height: 100%; + display: block; + .tb-table-widget { + .table-container { + position: relative; + } + + .mat-table { + .mat-row { + &.invisible { + visibility: hidden; + } + } + } + + span.no-data-found { + position: absolute; + top: 60px; + bottom: 0; + left: 0; + right: 0; + } + + .column-id { + min-width: 250px; + max-width: 250px; + width: 250px; + } + + .column-time { + min-width: 120px; + max-width: 120px; + width: 120px; + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-table.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-table.component.ts new file mode 100644 index 0000000000..5beef7ea8c --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-table.component.ts @@ -0,0 +1,582 @@ +/// +/// Copyright © 2016-2022 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + ChangeDetectorRef, + Component, + ElementRef, + Injector, + Input, + OnInit, + StaticProvider, + ViewChild, + ViewContainerRef +} from '@angular/core'; +import { PageComponent } from '@shared/components/page.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { WidgetContext } from '@home/models/widget-component.models'; +import { WidgetConfig } from '@shared/models/widget.models'; +import { IWidgetSubscription } from '@core/api/widget-api.models'; +import { BehaviorSubject, merge, Observable, of, ReplaySubject, Subject, throwError } from 'rxjs'; +import { catchError, map, tap } from 'rxjs/operators'; +import { + constructTableCssString, + noDataMessage, + TableCellButtonActionDescriptor, + TableWidgetSettings +} from '@home/components/widget/lib/table-widget.models'; +import cssjs from '@core/css/css'; +import { UtilsService } from '@core/services/utils.service'; +import { TranslateService } from '@ngx-translate/core'; +import { hashCode, isDefined, isNumber } from '@core/utils'; +import { CollectionViewer, DataSource } from '@angular/cdk/collections'; +import { emptyPageData, PageData } from '@shared/models/page/page-data'; +import { + PersistentRpc, + PersistentRpcData, + RequestData, + RpcStatus, + rpcStatusColors, + rpcStatusTranslation +} from '@shared/models/rpc.models'; +import { PageLink } from '@shared/models/page/page-link'; +import { Direction, SortOrder, sortOrderFromString } from '@shared/models/page/sort-order'; +import { MatPaginator } from '@angular/material/paginator'; +import { MatSort } from '@angular/material/sort'; +import { NULL_UUID } from '@shared/models/id/has-uuid'; +import { DialogService } from '@core/services/dialog.service'; +import { DeviceService } from '@core/http/device.service'; +import { MatDialog } from '@angular/material/dialog'; +import { + PersistentDetailsDialogComponent, + PersistentDetailsDialogData +} from '@home/components/widget/lib/rpc/persistent-details-dialog.component'; +import { ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay'; +import { ComponentPortal } from '@angular/cdk/portal'; +import { + PERSISTENT_FILTER_PANEL_DATA, + PersistentFilterPanelComponent, + PersistentFilterPanelData +} from '@home/components/widget/lib/rpc/persistent-filter-panel.component'; +import { PersistentAddDialogComponent } from '@home/components/widget/lib/rpc/persistent-add-dialog.component'; +import { ResizeObserver } from '@juggle/resize-observer'; +import { hidePageSizePixelValue } from '@shared/models/constants'; +import { HttpErrorResponse } from '@angular/common/http'; + +interface PersistentTableWidgetSettings extends TableWidgetSettings { + defaultSortOrder: string; + defaultPageSize: number; + displayPagination: boolean; + enableStickyAction: boolean; + enableStickyHeader: boolean; + enableFilter: boolean; + displayColumns: string[]; + displayDetails: boolean; + allowDelete: boolean; + allowSendRequest: boolean; +} + +interface PersistentTableWidgetActionDescriptor extends TableCellButtonActionDescriptor { + details?: boolean; + delete?: boolean; +} + +@Component({ + selector: 'tb-persistent-table-widget', + templateUrl: './persistent-table.component.html', + styleUrls: ['./persistent-table.component.scss' , '../table-widget.scss'] +}) + +export class PersistentTableComponent extends PageComponent implements OnInit { + + @Input() + ctx: WidgetContext; + + @ViewChild(MatPaginator) paginator: MatPaginator; + @ViewChild(MatSort) sort: MatSort; + + private settings: PersistentTableWidgetSettings; + private widgetConfig: WidgetConfig; + private subscription: IWidgetSubscription; + private enableFilterAction = true; + private allowSendRequest = true; + private defaultPageSize = 10; + private defaultSortOrder = '-createdTime'; + private rpcStatusFilter: RpcStatus; + private displayDetails = true; + private allowDelete = true; + private displayTableColumns: string[]; + private widgetResize$: ResizeObserver; + + public persistentDatasource: PersistentDatasource; + public noDataDisplayMessageText: string; + public rpcStatusColor = rpcStatusColors; + public rpcStatusTranslation = rpcStatusTranslation; + public displayPagination = true; + public enableStickyHeader = true; + public enableStickyAction = true; + public pageLink: PageLink; + public pageSizeOptions; + public actionCellButtonAction: PersistentTableWidgetActionDescriptor[] = []; + public displayedColumns: string[]; + public hidePageSize = false; + hasData = false; + + constructor(protected store: Store, + private elementRef: ElementRef, + private overlay: Overlay, + private viewContainerRef: ViewContainerRef, + private utils: UtilsService, + private translate: TranslateService, + private dialogService: DialogService, + private deviceService: DeviceService, + private dialog: MatDialog, + private cd: ChangeDetectorRef) { + super(store); + } + + ngOnInit() { + this.ctx.$scope.persistentTableWidget = this; + this.settings = this.ctx.settings; + this.widgetConfig = this.ctx.widgetConfig; + this.subscription = this.ctx.defaultSubscription; + this.initializeConfig(); + this.hasData = this.ctx.defaultSubscription.hasResolvedData; + this.ctx.updateWidgetParams(); + if (this.displayPagination) { + this.widgetResize$ = new ResizeObserver(() => { + const showHidePageSize = this.elementRef.nativeElement.offsetWidth < hidePageSizePixelValue; + if (showHidePageSize !== this.hidePageSize) { + this.hidePageSize = showHidePageSize; + this.cd.markForCheck(); + } + }); + this.widgetResize$.observe(this.elementRef.nativeElement); + } + } + + ngOnDestroy() { + if (this.widgetResize$) { + this.widgetResize$.disconnect(); + } + } + + ngAfterViewInit(): void { + if (this.displayPagination) { + this.sort.sortChange.subscribe(() => this.paginator.pageIndex = 0); + } + ((this.displayPagination ? merge(this.sort.sortChange, this.paginator.page) : this.sort.sortChange) as Observable) + .pipe( + tap(() => this.updateData()) + ) + .subscribe(); + this.updateData(); + } + + private initializeConfig() { + + this.displayPagination = isDefined(this.settings.displayPagination) ? this.settings.displayPagination : true; + this.enableStickyHeader = isDefined(this.settings.enableStickyHeader) ? this.settings.enableStickyHeader : true; + this.displayTableColumns = isDefined(this.settings.displayColumns) ? this.settings.displayColumns : []; + this.enableStickyAction = isDefined(this.settings.enableStickyAction) ? this.settings.enableStickyAction : true; + this.enableFilterAction = isDefined(this.settings.enableFilter) ? this.settings.enableFilter : true; + this.displayDetails = isDefined(this.settings.displayDetails) ? this.settings.displayDetails : true; + this.allowDelete = isDefined(this.settings.allowDelete) ? this.settings.allowDelete : true; + this.allowSendRequest = isDefined(this.settings.allowSendRequest) ? this.settings.allowSendRequest : true; + + this.noDataDisplayMessageText = + noDataMessage(this.widgetConfig.noDataDisplayMessage, 'widgets.persistent-table.no-request-prompt', this.utils, this.translate); + + this.displayedColumns = [...this.displayTableColumns]; + + const pageSize = this.settings.defaultPageSize; + if (isDefined(pageSize) && isNumber(pageSize) && pageSize > 0) { + this.defaultPageSize = pageSize; + } + this.pageSizeOptions = [this.defaultPageSize, this.defaultPageSize * 2, this.defaultPageSize * 3]; + if (this.settings.defaultSortOrder && this.settings.defaultSortOrder.length) { + this.defaultSortOrder = this.settings.defaultSortOrder; + } + const sortOrder: SortOrder = sortOrderFromString(this.defaultSortOrder); + this.pageLink = new PageLink(this.defaultPageSize, 0, null, sortOrder); + this.pageLink.pageSize = this.displayPagination ? this.defaultPageSize : 1024; + + + this.ctx.widgetActions = [ + { + name: 'widgets.persistent-table.add', + show: this.allowSendRequest, + icon: 'add', + onAction: $event => this.addPersistentRpcRequest($event) + }, + { + name: 'widgets.persistent-table.refresh', + show: true, + icon: 'refresh', + onAction: () => this.reloadPersistentRequests() + }, + { + name: 'widgets.persistent-table.filter', + show: this.enableFilterAction, + icon: 'filter_list', + onAction: $event => this.editFilter($event) + } + ]; + + if (this.settings.displayDetails) { + this.actionCellButtonAction.push( + { + displayName: this.translate.instant('widgets.persistent-table.details'), + icon: 'more_horiz', + details: true + } as PersistentTableWidgetActionDescriptor + ); + } + if (this.settings.allowDelete) { + this.actionCellButtonAction.push( + { + displayName: this.translate.instant('widgets.persistent-table.delete'), + icon: 'delete', + delete: true + } as PersistentTableWidgetActionDescriptor + ); + } + if (this.actionCellButtonAction.length) { + this.displayedColumns.push('actions'); + } + + this.persistentDatasource = new PersistentDatasource(this.translate, this.subscription, this.ctx); + + const cssString = constructTableCssString(this.widgetConfig); + const cssParser = new cssjs(); + cssParser.testMode = false; + const namespace = 'persistent-table-' + hashCode(cssString); + cssParser.cssPreviewNamespace = namespace; + cssParser.createStyleElement(namespace, cssString); + $(this.elementRef.nativeElement).addClass(namespace); + } + + private updateData() { + if (this.displayPagination) { + this.pageLink.page = this.paginator.pageIndex; + this.pageLink.pageSize = this.paginator.pageSize; + } else { + this.pageLink.page = 0; + } + if (this.settings.defaultSortOrder && this.settings.defaultSortOrder.length) { + this.defaultSortOrder = this.utils.customTranslation(this.settings.defaultSortOrder, this.settings.defaultSortOrder); + } + this.pageLink.sortOrder.property = this.sort.active; + this.pageLink.sortOrder.direction = Direction[this.sort.direction.toUpperCase()]; + this.persistentDatasource.loadPersistent(this.pageLink, this.rpcStatusFilter); + this.ctx.detectChanges(); + } + + public onDataUpdated() { + this.ctx.detectChanges(); + } + + reloadPersistentRequests() { + if (this.displayPagination) { + this.paginator.pageIndex = 0; + } + this.updateData(); + } + + deleteRpcRequest($event: Event, persistentRpc: PersistentRpc) { + if ($event) { + $event.stopPropagation(); + } + if (persistentRpc && persistentRpc.id && persistentRpc.id.id !== NULL_UUID) { + this.dialogService.confirm( + this.translate.instant('widgets.persistent-table.delete-request-title'), + this.translate.instant('widgets.persistent-table.delete-request-text'), + this.translate.instant('action.no'), + this.translate.instant('action.yes') + ).subscribe((res) => { + if (res) { + this.deviceService.deletePersistedRpc(persistentRpc.id.id).subscribe(() => { + this.reloadPersistentRequests(); + }); + } + }); + } + } + + openRequestDetails($event: Event, persistentRpc: PersistentRpc) { + if ($event) { + $event.stopPropagation(); + } + if (persistentRpc && persistentRpc.id && persistentRpc.id.id !== NULL_UUID) { + this.dialog.open + (PersistentDetailsDialogComponent, + { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + persistentRequest: persistentRpc, + allowDelete: this.allowDelete + } + }).afterClosed().subscribe( + (res) => { + if (res) { + this.reloadPersistentRequests(); + } + } + ); + } + } + + addPersistentRpcRequest($event: Event){ + if ($event) { + $event.stopPropagation(); + } + this.dialog.open + (PersistentAddDialogComponent, + { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'] + }).afterClosed().subscribe( + (requestData) => { + if (requestData) { + this.sendRequests(requestData); + } + } + ); + } + + private sendRequests(requestData: RequestData) { + let commandPromise; + if (requestData.oneWayElseTwoWay) { + commandPromise = this.ctx.controlApi.sendOneWayCommand( + requestData.method, + requestData.params, null, + true, null, + requestData.retries, + requestData.additionalInfo + ); + } else { + commandPromise = this.ctx.controlApi.sendTwoWayCommand( + requestData.method, + requestData.params, + null, + true, null, + requestData.retries, + requestData.additionalInfo + ); + } + commandPromise.subscribe( + () => { + this.reloadPersistentRequests(); + } + ); + } + + public onActionButtonClick($event: Event, persistentRpc: PersistentRpc, actionDescriptor: PersistentTableWidgetActionDescriptor) { + if (actionDescriptor.details) { + this.openRequestDetails($event, persistentRpc); + } + if (actionDescriptor.delete) { + this.deleteRpcRequest($event, persistentRpc); + } + } + + private editFilter($event: Event) { + if ($event) { + $event.stopPropagation(); + } + const target = $event.target || $event.srcElement || $event.currentTarget; + const config = new OverlayConfig(); + config.backdropClass = 'cdk-overlay-transparent-backdrop'; + config.hasBackdrop = true; + const connectedPosition: ConnectedPosition = { + originX: 'end', + originY: 'bottom', + overlayX: 'end', + overlayY: 'top' + }; + config.positionStrategy = this.overlay.position().flexibleConnectedTo(target as HTMLElement) + .withPositions([connectedPosition]); + + const overlayRef = this.overlay.create(config); + overlayRef.backdropClick().subscribe(() => { + overlayRef.dispose(); + }); + const providers: StaticProvider[] = [ + { + provide: PERSISTENT_FILTER_PANEL_DATA, + useValue: { + rpcStatus: this.rpcStatusFilter + } as PersistentFilterPanelData + }, + { + provide: OverlayRef, + useValue: overlayRef + } + ]; + const injector = Injector.create({parent: this.viewContainerRef.injector, providers}); + const componentRef = overlayRef.attach(new ComponentPortal(PersistentFilterPanelComponent, + this.viewContainerRef, injector)); + componentRef.onDestroy(() => { + if (componentRef.instance.result) { + const result = componentRef.instance.result; + this.rpcStatusFilter = result.rpcStatus; + this.reloadPersistentRequests(); + } + }); + this.ctx.detectChanges(); + } +} + +class PersistentDatasource implements DataSource { + + private persistentSubject = new BehaviorSubject([]); + private pageDataSubject = new BehaviorSubject>(emptyPageData()); + + private rpcErrorText: string; + private executingSubjects: Array>; + private executingRpcRequest = false; + + public dataLoading = true; + public pageData$ = this.pageDataSubject.asObservable(); + + constructor(private translate: TranslateService, + private subscription: IWidgetSubscription, + private ctx: WidgetContext) { + } + + connect(collectionViewer: CollectionViewer): Observable> { + return this.persistentSubject.asObservable(); + } + + disconnect(collectionViewer: CollectionViewer): void { + this.persistentSubject.complete(); + this.pageDataSubject.complete(); + } + + reset() { + const pageData = emptyPageData(); + this.persistentSubject.next(pageData.data); + this.pageDataSubject.next(pageData); + } + + loadPersistent(pageLink: PageLink, rpcStatusFilter: RpcStatus) { + this.dataLoading = true; + + const result = new ReplaySubject>(); + this.fetchEntities(pageLink, rpcStatusFilter).pipe( + catchError(() => of(emptyPageData())), + ).subscribe( + (pageData) => { + this.persistentSubject.next(pageData.data); + this.pageDataSubject.next(pageData); + result.next(pageData); + this.dataLoading = false; + } + ); + return result; + } + + fetchEntities(pageLink: PageLink, rpcStatusFilter: RpcStatus): Observable> { + if (!this.subscription.rpcEnabled) { + return throwError(new Error('Rpc disabled!')); + } else if (!this.subscription.targetDeviceId) { + return throwError(new Error('Target device is not set!')); + } + const rpcSubject: Subject = new Subject(); + + this.ctx.deviceService.getPersistedRpcRequests(this.subscription.targetDeviceId, pageLink, rpcStatusFilter).subscribe( + (responseBody) => { + rpcSubject.next(responseBody); + rpcSubject.complete(); + }, + (rejection: HttpErrorResponse) => { + this.rpcErrorText = null; + this.executingSubjects = []; + + const index = this.executingSubjects.indexOf(rpcSubject); + if (index >= 0) { + this.executingSubjects.splice(index, 1); + } + this.executingRpcRequest = this.executingSubjects.length > 0; + this.subscription.options.callbacks.rpcStateChanged(this.subscription); + if (!this.executingRpcRequest || rejection.status === 504) { + this.subscription.rpcRejection = rejection; + if (rejection.status === 504) { + this.subscription.rpcErrorText = 'Request Timeout.'; + } else { + this.subscription.rpcErrorText = 'Error : ' + rejection.status + ' - ' + rejection.statusText; + const error = this.extractRejectionErrorText(rejection); + if (error) { + this.subscription.rpcErrorText += '
'; + this.subscription.rpcErrorText += error.message || ''; + } + } + this.subscription.callbacks.onRpcFailed(this.subscription); + } + rpcSubject.error(rejection); + } + ); + return rpcSubject.asObservable(); + } + + extractRejectionErrorText(rejection: HttpErrorResponse) { + let error = null; + if (rejection.error) { + error = rejection.error; + try { + error = rejection.error ? JSON.parse(rejection.error) : null; + } catch (e) {} + } + if (error && !error.message) { + error = this.prepareMessageFromData(error); + } else if (error && error.message) { + error = error.message; + } + return error; + } + + prepareMessageFromData(data) { + if (typeof data === 'object' && data.constructor === ArrayBuffer) { + const msg = String.fromCharCode.apply(null, new Uint8Array(data)); + try { + const msgObj = JSON.parse(msg); + if (msgObj.message) { + return msgObj.message; + } else { + return msg; + } + } catch (e) { + return msg; + } + } else { + return data; + } + } + + isEmpty(): Observable { + return this.persistentSubject.pipe( + map((requests) => !requests.length) + ); + } + + total(): Observable { + return this.pageDataSubject.pipe( + map((pageData) => pageData.totalElements) + ); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/round-switch.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/round-switch.component.html index a7b1306133..425f7ee37a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/round-switch.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/round-switch.component.html @@ -1,6 +1,6 @@ - + {{section.icon}} {{section.name | translate}} diff --git a/ui-ngx/src/app/modules/home/menu/menu-link.component.scss b/ui-ngx/src/app/modules/home/menu/menu-link.component.scss index 3abcc4c738..da8df4b469 100644 --- a/ui-ngx/src/app/modules/home/menu/menu-link.component.scss +++ b/ui-ngx/src/app/modules/home/menu/menu-link.component.scss @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2021 The Thingsboard Authors + * Copyright © 2016-2022 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/ui-ngx/src/app/modules/home/menu/menu-link.component.ts b/ui-ngx/src/app/modules/home/menu/menu-link.component.ts index 58724b89f9..cae14d32b6 100644 --- a/ui-ngx/src/app/modules/home/menu/menu-link.component.ts +++ b/ui-ngx/src/app/modules/home/menu/menu-link.component.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2021 The Thingsboard Authors +/// Copyright © 2016-2022 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. diff --git a/ui-ngx/src/app/modules/home/menu/menu-toggle.component.html b/ui-ngx/src/app/modules/home/menu/menu-toggle.component.html index 0a41e75657..0635205581 100644 --- a/ui-ngx/src/app/modules/home/menu/menu-toggle.component.html +++ b/ui-ngx/src/app/modules/home/menu/menu-toggle.component.html @@ -1,6 +1,6 @@ + routerLinkActive="tb-active" [routerLinkActiveOptions]="{paths: 'exact', queryParams: 'ignored', matrixParams: 'ignored', fragment: 'ignored'}" routerLink="{{section.path}}"> {{section.icon}} {{section.name | translate}} diff --git a/ui-ngx/src/app/modules/home/menu/menu-toggle.component.scss b/ui-ngx/src/app/modules/home/menu/menu-toggle.component.scss index 87899b02b5..a076fa77d6 100644 --- a/ui-ngx/src/app/modules/home/menu/menu-toggle.component.scss +++ b/ui-ngx/src/app/modules/home/menu/menu-toggle.component.scss @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2021 The Thingsboard Authors + * Copyright © 2016-2022 The Thingsboard Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/ui-ngx/src/app/modules/home/menu/menu-toggle.component.ts b/ui-ngx/src/app/modules/home/menu/menu-toggle.component.ts index 6bbc5414fe..76a818c16b 100644 --- a/ui-ngx/src/app/modules/home/menu/menu-toggle.component.ts +++ b/ui-ngx/src/app/modules/home/menu/menu-toggle.component.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2021 The Thingsboard Authors +/// Copyright © 2016-2022 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. diff --git a/ui-ngx/src/app/modules/home/menu/side-menu.component.html b/ui-ngx/src/app/modules/home/menu/side-menu.component.html index 91275719e9..8a9f07f3c9 100644 --- a/ui-ngx/src/app/modules/home/menu/side-menu.component.html +++ b/ui-ngx/src/app/modules/home/menu/side-menu.component.html @@ -1,6 +1,6 @@
+