diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.ts b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.ts
index 60fb27a752..0a943ab5af 100644
--- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.ts
+++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.ts
@@ -23,6 +23,7 @@ import {
BINDING_MODE,
BINDING_MODE_NAMES,
getDefaultProfileConfig,
+ Instance,
INSTANCES,
KEY_NAME,
Lwm2mProfileConfigModels,
@@ -37,6 +38,7 @@ import { DeviceProfileService } from '@core/http/device-profile.service';
import { deepClone, isDefinedAndNotNull, isEmpty, isUndefined } from '@core/utils';
import { JsonArray, JsonObject } from '@angular/compiler-cli/ngcc/src/packages/entry_point';
import { Direction } from '@shared/models/page/sort-order';
+import _ from 'lodash';
@Component({
selector: 'tb-profile-lwm2m-device-transport-configuration',
@@ -230,24 +232,30 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro
const isNotZeroInstanceId = (instance) => !instance.includes('/0/');
return attribute.some(isNotZeroInstanceId) || telemetry.some(isNotZeroInstanceId);
}
-
private addInstances = (attribute: string[], telemetry: string[], clientObserveAttrTelemetry: ObjectLwM2M[]): void => {
const instancesPath = attribute.concat(telemetry)
.filter(instance => !instance.includes('/0/'))
.map(instance => instance.slice(1, instance.lastIndexOf('/')))
.sort(this.sortPath);
-
new Set(instancesPath).forEach(path => {
const pathParameter = Array.from(path.split('/'), String);
const objectLwM2M = clientObserveAttrTelemetry.find(x => x.keyId === pathParameter[0]);
if (objectLwM2M) {
- const instance = deepClone(objectLwM2M.instances[0]);
- instance.id = +pathParameter[1];
+ const instance = this.updateInInstanceKeyName (objectLwM2M.instances[0], +pathParameter[1]);
objectLwM2M.instances.push(instance);
}
});
}
+ private updateInInstanceKeyName = (instance: Instance, instanceId: number): Instance => {
+ const instanceUpdate = deepClone(instance);
+ instanceUpdate.id = instanceId;
+ instanceUpdate.resources.forEach(resource => {
+ resource.keyName = _.camelCase(resource.name + instanceUpdate.id);
+ })
+ return instanceUpdate;
+ }
+
private updateObserveAttrTelemetryObjects = (parameters: string[], objectLwM2MS: ObjectLwM2M[],
nameParameter: string): void => {
parameters.forEach(parameter => {
diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry.component.ts b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry.component.ts
index 3d6b93b84c..530679113c 100644
--- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry.component.ts
+++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry.component.ts
@@ -45,6 +45,7 @@ import {
Lwm2mObjectAddInstancesData,
Lwm2mObjectAddInstancesDialogComponent
} from '@home/components/profile/device/lwm2m/lwm2m-object-add-instances-dialog.component';
+import _ from 'lodash';
@Component({
selector: 'tb-profile-lwm2m-observe-attr-telemetry',
@@ -261,7 +262,6 @@ export class Lwm2mObserveAttrTelemetryComponent implements ControlValueAccessor
r.attribute = false;
r.telemetry = false;
r.observe = false;
- r.keyName = {};
r.attributeLwm2m = {};
});
const valueOld = this.instancesToSetId(instancesArray);
@@ -280,7 +280,8 @@ export class Lwm2mObserveAttrTelemetryComponent implements ControlValueAccessor
private addInstancesNew = (idsAdd: Set
, objectLwM2MFormGroup: FormGroup, instancesFormArray: FormArray,
instanceNew: Instance): void => {
idsAdd.forEach(x => {
- this.pushInstance(instancesFormArray, x, instanceNew);
+ instanceNew.resources.forEach(resource => {resource.keyName = _.camelCase(resource.name + x);});
+ this.pushInstance(instancesFormArray, x, deepClone(instanceNew as Instance));
});
(instancesFormArray.controls as FormGroup[]).sort((a, b) => a.value.id - b.value.id);
}
diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-profile-config.models.ts b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-profile-config.models.ts
index f6e58df903..acea41394a 100644
--- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-profile-config.models.ts
+++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-profile-config.models.ts
@@ -246,7 +246,7 @@ export interface ResourceLwM2M {
observe: boolean;
attribute: boolean;
telemetry: boolean;
- keyName: {};
+ keyName: string;
attributeLwm2m?: {};
}
diff --git a/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.html b/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.html
index da7598488e..13e3976299 100644
--- a/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.html
+++ b/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.html
@@ -196,6 +196,18 @@
{{ 'tenant-profile.max-sms-range' | translate}}
+
+ tenant-profile.max-created-alarms
+
+
+ {{ 'tenant-profile.max-created-alarms-required' | translate}}
+
+
+ {{ 'tenant-profile.max-created-alarms-range' | translate}}
+
+
tenant-profile.transport-tenant-msg-rate-limit
diff --git a/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.ts b/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.ts
index e78ad83ebd..ad00df7476 100644
--- a/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.ts
+++ b/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.ts
@@ -73,6 +73,7 @@ export class DefaultTenantProfileConfigurationComponent implements ControlValueA
maxRuleNodeExecutionsPerMessage: [null, [Validators.required, Validators.min(0)]],
maxEmails: [null, [Validators.required, Validators.min(0)]],
maxSms: [null, [Validators.required, Validators.min(0)]],
+ maxCreatedAlarms: [null, [Validators.required, Validators.min(0)]],
defaultStorageTtlDays: [null, [Validators.required, Validators.min(0)]]
});
this.defaultTenantProfileConfigurationFormGroup.valueChanges.subscribe(() => {
diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-container.component.html b/ui-ngx/src/app/modules/home/components/widget/widget-container.component.html
new file mode 100644
index 0000000000..09f3199b06
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/widget/widget-container.component.html
@@ -0,0 +1,112 @@
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-container.component.scss b/ui-ngx/src/app/modules/home/components/widget/widget-container.component.scss
new file mode 100644
index 0000000000..ed66a2b235
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/widget/widget-container.component.scss
@@ -0,0 +1,117 @@
+/**
+ * Copyright © 2016-2021 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 {
+
+}
+
+tb-widget.tb-widget {
+ position: relative;
+ height: 100%;
+ margin: 0;
+ overflow: hidden;
+ outline: none;
+
+ transition: all .2s ease-in-out;
+}
+
+div.tb-widget {
+ position: relative;
+ height: 100%;
+ margin: 0;
+ overflow: hidden;
+ outline: none;
+
+ transition: all .2s ease-in-out;
+
+ .tb-widget-title {
+ max-height: 65px;
+ padding-top: 5px;
+ padding-left: 5px;
+ overflow: hidden;
+
+ tb-timewindow {
+ font-size: 14px;
+ opacity: .85;
+ margin: 0;
+ }
+
+ .title {
+ width: 100%;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ line-height: 24px;
+ letter-spacing: .01em;
+ margin: 0;
+ display: -webkit-box;
+ -webkit-box-orient: vertical;
+ -webkit-line-clamp: 2;
+
+ &.single-row{
+ -webkit-line-clamp: 1;
+ }
+ }
+ }
+
+ .tb-widget-actions {
+ z-index: 19;
+ margin: 5px 0 0;
+
+ &-absolute {
+ position: absolute;
+ top: 3px;
+ right: 8px;
+ z-index: 150;
+ }
+
+ button.mat-icon-button {
+ width: 32px;
+ min-width: 32px;
+ height: 32px;
+ min-height: 32px;
+ padding: 0 !important;
+ margin: 0 !important;
+ line-height: 20px;
+
+ mat-icon {
+ width: 20px;
+ min-width: 20px;
+ height: 20px;
+ min-height: 20px;
+ font-size: 20px;
+ line-height: 20px;
+ }
+ }
+ }
+
+ .tb-widget-content {
+ &.tb-no-interaction {
+ pointer-events: none;
+ }
+ tb-widget {
+ position: relative;
+ width: 100%;
+ }
+ }
+
+ &.tb-highlighted {
+ border: 1px solid #039be5;
+ box-shadow: 0 0 20px #039be5;
+ }
+
+ &.tb-not-highlighted {
+ opacity: .5;
+ }
+}
diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-container.component.ts b/ui-ngx/src/app/modules/home/components/widget/widget-container.component.ts
new file mode 100644
index 0000000000..cd2b322420
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/widget/widget-container.component.ts
@@ -0,0 +1,153 @@
+///
+/// Copyright © 2016-2021 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,
+ EventEmitter,
+ Input,
+ OnInit,
+ Output
+} from '@angular/core';
+import { PageComponent } from '@shared/components/page.component';
+import { DashboardWidget, DashboardWidgets } from '@home/models/dashboard-component.models';
+import { Store } from '@ngrx/store';
+import { AppState } from '@core/core.state';
+import { SafeStyle } from '@angular/platform-browser';
+
+export enum WidgetComponentActionType {
+ MOUSE_DOWN,
+ CLICKED,
+ CONTEXT_MENU,
+ EDIT,
+ EXPORT,
+ REMOVE
+}
+
+export class WidgetComponentAction {
+ event: MouseEvent;
+ actionType: WidgetComponentActionType;
+}
+
+@Component({
+ selector: 'tb-widget-container',
+ templateUrl: './widget-container.component.html',
+ styleUrls: ['./widget-container.component.scss'],
+ changeDetection: ChangeDetectionStrategy.OnPush
+})
+export class WidgetContainerComponent extends PageComponent implements OnInit {
+
+ @Input()
+ widget: DashboardWidget;
+
+ @Input()
+ dashboardStyle: {[klass: string]: any};
+
+ @Input()
+ backgroundImage: SafeStyle | string;
+
+ @Input()
+ isEdit: boolean;
+
+ @Input()
+ isMobile: boolean;
+
+ @Input()
+ dashboardWidgets: DashboardWidgets;
+
+ @Input()
+ isEditActionEnabled: boolean;
+
+ @Input()
+ isExportActionEnabled: boolean;
+
+ @Input()
+ isRemoveActionEnabled: boolean;
+
+ @Input()
+ disableWidgetInteraction = false;
+
+ @Output()
+ widgetFullscreenChanged: EventEmitter = new EventEmitter();
+
+ @Output()
+ widgetComponentAction: EventEmitter = new EventEmitter();
+
+ constructor(protected store: Store,
+ private cd: ChangeDetectorRef) {
+ super(store);
+ }
+
+ ngOnInit(): void {
+ this.widget.widgetContext.containerChangeDetector = this.cd;
+ }
+
+ isHighlighted(widget: DashboardWidget) {
+ return this.dashboardWidgets.isHighlighted(widget);
+ }
+
+ isNotHighlighted(widget: DashboardWidget) {
+ return this.dashboardWidgets.isNotHighlighted(widget);
+ }
+
+ onFullscreenChanged(expanded: boolean) {
+ this.widgetFullscreenChanged.emit(expanded);
+ }
+
+ onMouseDown(event: MouseEvent) {
+ this.widgetComponentAction.emit({
+ event,
+ actionType: WidgetComponentActionType.MOUSE_DOWN
+ });
+ }
+
+ onClicked(event: MouseEvent) {
+ this.widgetComponentAction.emit({
+ event,
+ actionType: WidgetComponentActionType.CLICKED
+ });
+ }
+
+ onContextMenu(event: MouseEvent) {
+ this.widgetComponentAction.emit({
+ event,
+ actionType: WidgetComponentActionType.CONTEXT_MENU
+ });
+ }
+
+ onEdit(event: MouseEvent) {
+ this.widgetComponentAction.emit({
+ event,
+ actionType: WidgetComponentActionType.EDIT
+ });
+ }
+
+ onExport(event: MouseEvent) {
+ this.widgetComponentAction.emit({
+ event,
+ actionType: WidgetComponentActionType.EXPORT
+ });
+ }
+
+ onRemove(event: MouseEvent) {
+ this.widgetComponentAction.emit({
+ event,
+ actionType: WidgetComponentActionType.REMOVE
+ });
+ }
+
+}
diff --git a/ui-ngx/src/app/modules/home/components/widget/widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/widget.component.ts
index 3044f29bca..fea6e14291 100644
--- a/ui-ngx/src/app/modules/home/components/widget/widget.component.ts
+++ b/ui-ngx/src/app/modules/home/components/widget/widget.component.ts
@@ -468,10 +468,13 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
);
}
- private detectChanges() {
+ private detectChanges(detectContainerChanges = false) {
if (!this.destroyed) {
try {
this.cd.detectChanges();
+ if (detectContainerChanges) {
+ this.widgetContext.detectContainerChanges();
+ }
} catch (e) {
// console.log(e);
}
@@ -491,6 +494,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
}
if (!this.widgetContext.inited && this.isReady()) {
this.widgetContext.inited = true;
+ this.widgetContext.detectContainerChanges();
if (this.cafs.init) {
this.cafs.init();
this.cafs.init = null;
@@ -878,7 +882,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
timeWindowUpdated: (subscription, timeWindowConfig) => {
this.ngZone.run(() => {
this.widget.config.timewindow = timeWindowConfig;
- this.detectChanges();
+ this.detectChanges(true);
});
}
};
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 a0000e40be..58724b89f9 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
@@ -14,13 +14,14 @@
/// limitations under the License.
///
-import { Component, Input, OnInit } from '@angular/core';
+import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
import { MenuSection } from '@core/services/menu.models';
@Component({
selector: 'tb-menu-link',
templateUrl: './menu-link.component.html',
- styleUrls: ['./menu-link.component.scss']
+ styleUrls: ['./menu-link.component.scss'],
+ changeDetection: ChangeDetectionStrategy.OnPush
})
export class MenuLinkComponent implements OnInit {
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 a3afd99508..6bbc5414fe 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
@@ -14,14 +14,15 @@
/// limitations under the License.
///
-import { Component, Input, OnInit } from '@angular/core';
+import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
import { MenuSection } from '@core/services/menu.models';
import { Router } from '@angular/router';
@Component({
selector: 'tb-menu-toggle',
templateUrl: './menu-toggle.component.html',
- styleUrls: ['./menu-toggle.component.scss']
+ styleUrls: ['./menu-toggle.component.scss'],
+ changeDetection: ChangeDetectionStrategy.OnPush
})
export class MenuToggleComponent implements OnInit {
diff --git a/ui-ngx/src/app/modules/home/menu/side-menu.component.ts b/ui-ngx/src/app/modules/home/menu/side-menu.component.ts
index aa96b517f9..c2b974cd6b 100644
--- a/ui-ngx/src/app/modules/home/menu/side-menu.component.ts
+++ b/ui-ngx/src/app/modules/home/menu/side-menu.component.ts
@@ -14,14 +14,15 @@
/// limitations under the License.
///
-import { Component, OnInit } from '@angular/core';
+import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { MenuService } from '@core/services/menu.service';
import { MenuSection } from '@core/services/menu.models';
@Component({
selector: 'tb-side-menu',
templateUrl: './side-menu.component.html',
- styleUrls: ['./side-menu.component.scss']
+ styleUrls: ['./side-menu.component.scss'],
+ changeDetection: ChangeDetectionStrategy.OnPush
})
export class SideMenuComponent implements OnInit {
diff --git a/ui-ngx/src/app/modules/home/models/dashboard-component.models.ts b/ui-ngx/src/app/modules/home/models/dashboard-component.models.ts
index b1ded98ba7..2edd7f9afa 100644
--- a/ui-ngx/src/app/modules/home/models/dashboard-component.models.ts
+++ b/ui-ngx/src/app/modules/home/models/dashboard-component.models.ts
@@ -286,8 +286,8 @@ export class DashboardWidgets implements Iterable {
export class DashboardWidget implements GridsterItem, IDashboardWidget {
- highlighted = false;
- selected = false;
+ private highlightedValue = false;
+ private selectedValue = false;
isFullscreen = false;
@@ -334,6 +334,28 @@ export class DashboardWidget implements GridsterItem, IDashboardWidget {
this.gridsterItemComponentSubject.complete();
}
+ get highlighted() {
+ return this.highlightedValue;
+ }
+
+ set highlighted(highlighted: boolean) {
+ if (this.highlightedValue !== highlighted) {
+ this.highlightedValue = highlighted;
+ this.widgetContext.detectContainerChanges();
+ }
+ }
+
+ get selected() {
+ return this.selectedValue;
+ }
+
+ set selected(selected: boolean) {
+ if (this.selectedValue !== selected) {
+ this.selectedValue = selected;
+ this.widgetContext.detectContainerChanges();
+ }
+ }
+
constructor(
private dashboard: IDashboardComponent,
public widget: Widget,
@@ -342,7 +364,7 @@ export class DashboardWidget implements GridsterItem, IDashboardWidget {
widget.id = guid();
}
this.widgetId = widget.id;
- this.updateWidgetParams();
+ this.updateWidgetParams(false);
}
gridsterItemComponent$(): Observable {
@@ -353,7 +375,7 @@ export class DashboardWidget implements GridsterItem, IDashboardWidget {
}
}
- updateWidgetParams() {
+ updateWidgetParams(detectChanges = true) {
this.color = this.widget.config.color || 'rgba(0, 0, 0, 0.87)';
this.backgroundColor = this.widget.config.backgroundColor || '#fff';
this.padding = this.widget.config.padding || '8px';
@@ -405,6 +427,9 @@ export class DashboardWidget implements GridsterItem, IDashboardWidget {
this.customHeaderActions = this.widgetContext.customHeaderActions ? this.widgetContext.customHeaderActions : [];
this.widgetActions = this.widgetContext.widgetActions ? this.widgetContext.widgetActions : [];
+ if (detectChanges) {
+ this.widgetContext.detectContainerChanges();
+ }
}
@enumerable(true)
diff --git a/ui-ngx/src/app/modules/home/models/widget-component.models.ts b/ui-ngx/src/app/modules/home/models/widget-component.models.ts
index 64fbe59efa..50466d7600 100644
--- a/ui-ngx/src/app/modules/home/models/widget-component.models.ts
+++ b/ui-ngx/src/app/modules/home/models/widget-component.models.ts
@@ -136,6 +136,10 @@ export class WidgetContext {
this.changeDetectorValue = cd;
}
+ set containerChangeDetector(cd: ChangeDetectorRef) {
+ this.containerChangeDetectorValue = cd;
+ }
+
get currentUser(): AuthUser {
if (this.store) {
return getCurrentAuthUser(this.store);
@@ -162,6 +166,7 @@ export class WidgetContext {
router: Router;
private changeDetectorValue: ChangeDetectorRef;
+ private containerChangeDetectorValue: ChangeDetectorRef;
inited = false;
destroyed = false;
@@ -309,6 +314,16 @@ export class WidgetContext {
}
}
+ detectContainerChanges() {
+ if (!this.destroyed) {
+ try {
+ this.containerChangeDetectorValue.detectChanges();
+ } catch (e) {
+ // console.log(e);
+ }
+ }
+ }
+
updateWidgetParams() {
if (!this.destroyed) {
setTimeout(() => {
diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.html b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.html
index 1fb8978f22..ca7512d7ec 100644
--- a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.html
+++ b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.html
@@ -105,6 +105,11 @@
{{ 'dashboard.title-required' | translate }}
+
+
dashboard.description
diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.ts b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.ts
index 37030c57f1..70914e069d 100644
--- a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.ts
+++ b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.ts
@@ -80,6 +80,7 @@ export class DashboardFormComponent extends EntityComponent {
return this.fb.group(
{
title: [entity ? entity.title : '', [Validators.required]],
+ image: [entity ? entity.image : null],
configuration: this.fb.group(
{
description: [entity && entity.configuration ? entity.configuration.description : ''],
@@ -92,6 +93,7 @@ export class DashboardFormComponent extends EntityComponent {
updateForm(entity: Dashboard) {
this.updateFields(entity);
this.entityForm.patchValue({title: entity.title});
+ this.entityForm.patchValue({image: entity.image});
this.entityForm.patchValue({configuration: {description: entity.configuration ? entity.configuration.description : ''}});
}
diff --git a/ui-ngx/src/app/modules/home/pages/entity-view/entity-views-table-config.resolver.ts b/ui-ngx/src/app/modules/home/pages/entity-view/entity-views-table-config.resolver.ts
index 730ac2603a..3225c73b2c 100644
--- a/ui-ngx/src/app/modules/home/pages/entity-view/entity-views-table-config.resolver.ts
+++ b/ui-ngx/src/app/modules/home/pages/entity-view/entity-views-table-config.resolver.ts
@@ -104,7 +104,8 @@ export class EntityViewsTableConfigResolver implements Resolve this.onEntityViewAction(action);
- this.config.detailsReadonly = () => (this.config.componentsData.entityViewScope === 'customer_user' || this.config.componentsData.entityViewScope === 'edge_customer_user');
+ this.config.detailsReadonly = () => (this.config.componentsData.entityViewScope === 'customer_user' ||
+ this.config.componentsData.entityViewScope === 'edge_customer_user');
this.config.headerComponent = EntityViewTableHeaderComponent;
@@ -151,7 +152,8 @@ export class EntityViewsTableConfigResolver implements Resolve this.config.componentsData.entityViewScope === 'tenant';
return this.config;
@@ -243,7 +245,7 @@ export class EntityViewsTableConfigResolver implements Resolve true,
+ isEnabled: () => true,
onAction: ($event, entity) => this.unassignFromEdge($event, entity)
}
);
diff --git a/ui-ngx/src/app/modules/home/pages/home-links/home-links.component.ts b/ui-ngx/src/app/modules/home/pages/home-links/home-links.component.ts
index 7f485df45c..79accbe99b 100644
--- a/ui-ngx/src/app/modules/home/pages/home-links/home-links.component.ts
+++ b/ui-ngx/src/app/modules/home/pages/home-links/home-links.component.ts
@@ -14,7 +14,7 @@
/// limitations under the License.
///
-import { Component, OnInit } from '@angular/core';
+import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit } from '@angular/core';
import { MenuService } from '@core/services/menu.service';
import { BreakpointObserver, BreakpointState } from '@angular/cdk/layout';
import { MediaBreakpoints } from '@shared/models/constants';
@@ -25,7 +25,8 @@ import { HomeDashboard } from '@shared/models/dashboard.models';
@Component({
selector: 'tb-home-links',
templateUrl: './home-links.component.html',
- styleUrls: ['./home-links.component.scss']
+ styleUrls: ['./home-links.component.scss'],
+ changeDetection: ChangeDetectionStrategy.OnPush
})
export class HomeLinksComponent implements OnInit {
@@ -37,6 +38,7 @@ export class HomeLinksComponent implements OnInit {
constructor(private menuService: MenuService,
public breakpointObserver: BreakpointObserver,
+ private cd: ChangeDetectorRef,
private route: ActivatedRoute) {
}
@@ -57,6 +59,7 @@ export class HomeLinksComponent implements OnInit {
if (this.breakpointObserver.isMatched(MediaBreakpoints['gt-lg'])) {
this.cols = 4;
}
+ this.cd.detectChanges();
}
sectionColspan(section: HomeSection): number {
diff --git a/ui-ngx/src/app/shared/components/breadcrumb.component.ts b/ui-ngx/src/app/shared/components/breadcrumb.component.ts
index c967c3815e..3fd2f05ff3 100644
--- a/ui-ngx/src/app/shared/components/breadcrumb.component.ts
+++ b/ui-ngx/src/app/shared/components/breadcrumb.component.ts
@@ -14,7 +14,7 @@
/// limitations under the License.
///
-import { Component, Input, OnDestroy, OnInit } from '@angular/core';
+import { ChangeDetectionStrategy, Component, Input, OnDestroy, OnInit } from '@angular/core';
import { BehaviorSubject, Subject } from 'rxjs';
import { BreadCrumb, BreadCrumbConfig } from './breadcrumb';
import { ActivatedRoute, ActivatedRouteSnapshot, NavigationEnd, Router } from '@angular/router';
@@ -25,7 +25,8 @@ import { guid } from '@core/utils';
@Component({
selector: 'tb-breadcrumb',
templateUrl: './breadcrumb.component.html',
- styleUrls: ['./breadcrumb.component.scss']
+ styleUrls: ['./breadcrumb.component.scss'],
+ changeDetection: ChangeDetectionStrategy.OnPush
})
export class BreadcrumbComponent implements OnInit, OnDestroy {
diff --git a/ui-ngx/src/app/shared/components/dashboard-autocomplete.component.ts b/ui-ngx/src/app/shared/components/dashboard-autocomplete.component.ts
index 9a754fa81d..782f0787dd 100644
--- a/ui-ngx/src/app/shared/components/dashboard-autocomplete.component.ts
+++ b/ui-ngx/src/app/shared/components/dashboard-autocomplete.component.ts
@@ -139,6 +139,11 @@ export class DashboardAutocompleteComponent implements ControlValueAccessor, OnI
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
+ if (this.disabled) {
+ this.selectDashboardFormGroup.disable({emitEvent: false});
+ } else {
+ this.selectDashboardFormGroup.enable({emitEvent: false});
+ }
}
writeValue(value: DashboardInfo | string | null): void {
diff --git a/ui-ngx/src/app/shared/components/image-input.component.html b/ui-ngx/src/app/shared/components/image-input.component.html
index 4b7e4fe1d4..2fe8cc4102 100644
--- a/ui-ngx/src/app/shared/components/image-input.component.html
+++ b/ui-ngx/src/app/shared/components/image-input.component.html
@@ -21,10 +21,10 @@
[flowConfig]="{singleFile: true, allowDuplicateUploads: true}">
-
dashboard.no-image
+
{{ (disabled ? 'dashboard.empty-image' : 'dashboard.no-image') | translate }}
-
+
-
@@ -42,5 +42,5 @@
-
dashboard.maximum-upload-file-size
+
dashboard.maximum-upload-file-size
diff --git a/ui-ngx/src/app/shared/components/time/timewindow.component.ts b/ui-ngx/src/app/shared/components/time/timewindow.component.ts
index 0169feaae7..77d10eadcc 100644
--- a/ui-ngx/src/app/shared/components/time/timewindow.component.ts
+++ b/ui-ngx/src/app/shared/components/time/timewindow.component.ts
@@ -15,6 +15,7 @@
///
import {
+ ChangeDetectorRef,
Component,
forwardRef,
Inject,
@@ -158,6 +159,7 @@ export class TimewindowComponent implements OnInit, OnDestroy, ControlValueAcces
private millisecondsToTimeStringPipe: MillisecondsToTimeStringPipe,
private datePipe: DatePipe,
private overlay: Overlay,
+ private cd: ChangeDetectorRef,
public viewContainerRef: ViewContainerRef,
public breakpointObserver: BreakpointObserver,
@Inject(DOCUMENT) private document: Document,
@@ -313,6 +315,7 @@ export class TimewindowComponent implements OnInit, OnDestroy, ControlValueAcces
} else {
this.innerValue.displayTimezoneAbbr = '';
}
+ this.cd.detectChanges();
}
hideLabel() {
diff --git a/ui-ngx/src/app/shared/components/user-menu.component.ts b/ui-ngx/src/app/shared/components/user-menu.component.ts
index 8598eed5d8..f27f272195 100644
--- a/ui-ngx/src/app/shared/components/user-menu.component.ts
+++ b/ui-ngx/src/app/shared/components/user-menu.component.ts
@@ -14,7 +14,7 @@
/// limitations under the License.
///
-import { Component, Input, OnDestroy, OnInit } from '@angular/core';
+import { ChangeDetectionStrategy, Component, Input, OnDestroy, OnInit } from '@angular/core';
import { User } from '@shared/models/user.model';
import { Authority } from '@shared/models/authority.enum';
import { select, Store } from '@ngrx/store';
@@ -27,7 +27,8 @@ import { Router } from '@angular/router';
@Component({
selector: 'tb-user-menu',
templateUrl: './user-menu.component.html',
- styleUrls: ['./user-menu.component.scss']
+ styleUrls: ['./user-menu.component.scss'],
+ changeDetection: ChangeDetectionStrategy.OnPush
})
export class UserMenuComponent implements OnInit, OnDestroy {
diff --git a/ui-ngx/src/app/shared/models/alarm.models.ts b/ui-ngx/src/app/shared/models/alarm.models.ts
index a76bf1b425..e629e1c574 100644
--- a/ui-ngx/src/app/shared/models/alarm.models.ts
+++ b/ui-ngx/src/app/shared/models/alarm.models.ts
@@ -21,7 +21,7 @@ import { EntityId } from '@shared/models/id/entity-id';
import { TimePageLink } from '@shared/models/page/page-link';
import { NULL_UUID } from '@shared/models/id/has-uuid';
import { EntityType } from '@shared/models/entity-type.models';
-import { isString } from '@core/utils';
+import { CustomerId } from '@shared/models/id/customer-id';
export enum AlarmSeverity {
CRITICAL = 'CRITICAL',
@@ -87,6 +87,7 @@ export const alarmSeverityColors = new Map(
export interface Alarm extends BaseData {
tenantId: TenantId;
+ customerId: CustomerId;
type: string;
originator: EntityId;
severity: AlarmSeverity;
@@ -110,6 +111,7 @@ export interface AlarmDataInfo extends AlarmInfo {
export const simulatedAlarm: AlarmInfo = {
id: new AlarmId(NULL_UUID),
tenantId: new TenantId(NULL_UUID),
+ customerId: new CustomerId(NULL_UUID),
createdTime: new Date().getTime(),
startTs: new Date().getTime(),
endTs: 0,
@@ -201,17 +203,15 @@ export class AlarmQuery {
searchStatus: AlarmSearchStatus;
status: AlarmStatus;
fetchOriginator: boolean;
- offset: string;
constructor(entityId: EntityId, pageLink: TimePageLink,
searchStatus: AlarmSearchStatus, status: AlarmStatus,
- fetchOriginator: boolean, offset: string) {
+ fetchOriginator: boolean) {
this.affectedEntityId = entityId;
this.pageLink = pageLink;
this.searchStatus = searchStatus;
this.status = status;
this.fetchOriginator = fetchOriginator;
- this.offset = offset;
}
public toQuery(): string {
@@ -225,9 +225,6 @@ export class AlarmQuery {
if (typeof this.fetchOriginator !== 'undefined' && this.fetchOriginator !== null) {
query += `&fetchOriginator=${this.fetchOriginator}`;
}
- if (isString(this.offset) && this.offset.length) {
- query += `&offset=${this.offset}`;
- }
return query;
}
diff --git a/ui-ngx/src/app/shared/models/dashboard.models.ts b/ui-ngx/src/app/shared/models/dashboard.models.ts
index 6a90101923..a782fe8256 100644
--- a/ui-ngx/src/app/shared/models/dashboard.models.ts
+++ b/ui-ngx/src/app/shared/models/dashboard.models.ts
@@ -26,6 +26,7 @@ import { Filters } from '@shared/models/query/query.models';
export interface DashboardInfo extends BaseData {
tenantId?: TenantId;
title?: string;
+ image?: string;
assignedCustomers?: Array;
}
@@ -90,6 +91,7 @@ export interface DashboardSettings {
dashboardLogoUrl?: string;
showDashboardTimewindow?: boolean;
showDashboardExport?: boolean;
+ showUpdateDashboardImage?: boolean;
toolbarAlwaysOpen?: boolean;
titleColor?: string;
}
diff --git a/ui-ngx/src/app/shared/models/device.models.ts b/ui-ngx/src/app/shared/models/device.models.ts
index 683d9ddcc1..fe7868d584 100644
--- a/ui-ngx/src/app/shared/models/device.models.ts
+++ b/ui-ngx/src/app/shared/models/device.models.ts
@@ -28,6 +28,7 @@ import { TimeUnit } from '@shared/models/time/time.models';
import * as _moment from 'moment';
import { AbstractControl, ValidationErrors } from '@angular/forms';
import { FirmwareId } from '@shared/models/id/firmware-id';
+import { DashboardId } from '@shared/models/id/dashboard-id';
export enum DeviceProfileType {
DEFAULT = 'DEFAULT',
@@ -492,10 +493,12 @@ export interface DeviceProfile extends BaseData {
description?: string;
default?: boolean;
type: DeviceProfileType;
+ image?: string;
transportType: DeviceTransportType;
provisionType: DeviceProvisionType;
provisionDeviceKey?: string;
defaultRuleChainId?: RuleChainId;
+ defaultDashboardId?: DashboardId;
defaultQueueName?: string;
firmwareId?: FirmwareId;
softwareId?: FirmwareId;
@@ -505,6 +508,8 @@ export interface DeviceProfile extends BaseData {
export interface DeviceProfileInfo extends EntityInfoData {
type: DeviceProfileType;
transportType: DeviceTransportType;
+ image?: string;
+ defaultDashboardId?: DashboardId;
}
export interface DefaultDeviceConfiguration {
diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json
index d8a21107bc..93c5c89955 100644
--- a/ui-ngx/src/assets/locale/locale.constant-en_US.json
+++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json
@@ -666,6 +666,9 @@
"no-widgets": "No widgets configured",
"add-widget": "Add new widget",
"title": "Title",
+ "image": "Dashboard image",
+ "update-image": "Update dashboard image",
+ "take-screenshot": "Take screenshot",
"select-widget-title": "Select widget",
"select-widget-value": "{{title}}: select widget",
"select-widget-subtitle": "List of available widget types",
@@ -712,6 +715,7 @@
"background-image": "Background image",
"background-size-mode": "Background size mode",
"no-image": "No image selected",
+ "empty-image": "No image",
"drop-image": "Drop an image or click to select a file to upload.",
"maximum-upload-file-size": "Maximum upload file size: {{ size }}",
"cannot-upload-file": "Cannot upload file",
@@ -746,6 +750,7 @@
"display-filters": "Display filters",
"display-dashboard-timewindow": "Display timewindow",
"display-dashboard-export": "Display export",
+ "display-update-dashboard-image": "Display update dashboard image",
"display-dashboard-logo": "Display logo in dashboard fullscreen mode",
"dashboard-logo-image": "Dashboard logo image",
"import": "Import dashboard",
@@ -1022,6 +1027,7 @@
"type": "Profile type",
"type-required": "Profile type is required.",
"type-default": "Default",
+ "image": "Device profile image",
"transport-type": "Transport type",
"transport-type-required": "Transport type is required.",
"transport-type-default": "Default",
@@ -1039,6 +1045,7 @@
"profile-configuration": "Profile configuration",
"transport-configuration": "Transport configuration",
"default-rule-chain": "Default rule chain",
+ "default-dashboard": "Default dashboard",
"select-queue-hint": "Select from a drop-down list or add a custom name.",
"delete-device-profile-title": "Are you sure you want to delete the device profile '{{deviceProfileName}}'?",
"delete-device-profile-text": "Be careful, after the confirmation the device profile and all related data will become unrecoverable.",
@@ -1221,7 +1228,6 @@
"required": " value is required.",
"mode": "Security config mode",
"pattern_hex_dec": "{ count, plural, 0 {must be hex decimal format} other {must be # characters} }",
- "bootstrap-tab": "Bootstrap",
"servers": "Servers",
"short-id": "Short ID",
"short-id-tip": "Short Server ID",
@@ -2518,7 +2524,10 @@
"max-emails-range": "Maximum number of emails sent can't be negative",
"max-sms": "Maximum number of SMS sent (0 - unlimited)",
"max-sms-required": "Maximum number of SMS sent is required.",
- "max-sms-range": "Maximum number of SMS sent can't be negative"
+ "max-sms-range": "Maximum number of SMS sent can't be negative",
+ "max-created-alarms": "Maximum number of alarms created (0 - unlimited)",
+ "max-created-alarms-required": "Maximum number of alarms created is required.",
+ "max-created-alarms-range": "Maximum number of alarms created can't be negative"
},
"timeinterval": {
"seconds-interval": "{ seconds, plural, 1 {1 second} other {# seconds} }",
diff --git a/ui-ngx/yarn.lock b/ui-ngx/yarn.lock
index b1c5167ac9..d7ed0d4e41 100644
--- a/ui-ngx/yarn.lock
+++ b/ui-ngx/yarn.lock
@@ -2222,6 +2222,11 @@ base64-arraybuffer@0.1.5:
resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz#73926771923b5a19747ad666aa5cd4bf9c6e9ce8"
integrity sha1-c5JncZI7Whl0etZmqlzUv5xunOg=
+base64-arraybuffer@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.2.0.tgz#4b944fac0191aa5907afe2d8c999ccc57ce80f45"
+ integrity sha512-7emyCsu1/xiBXgQZrscw/8KPRT44I4Yq9Pe6EGs3aPRTsWuggML1/1DTuZUuIaJPIm1FTDUVXl4x/yW8s0kQDQ==
+
base64-js@^1.0.2:
version "1.3.1"
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1"
@@ -3206,6 +3211,13 @@ css-declaration-sorter@^4.0.1:
postcss "^7.0.1"
timsort "^0.3.0"
+css-line-break@1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/css-line-break/-/css-line-break-1.1.1.tgz#d5e9bdd297840099eb0503c7310fd34927a026ef"
+ integrity sha512-1feNVaM4Fyzdj4mKPIQNL2n70MmuYzAXZ1aytlROFX1JsOo070OsugwGjj7nl6jnDJWHDM8zRZswkmeYVWZJQA==
+ dependencies:
+ base64-arraybuffer "^0.2.0"
+
css-loader@4.2.2:
version "4.2.2"
resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-4.2.2.tgz#b668b3488d566dc22ebcf9425c5f254a05808c89"
@@ -4782,6 +4794,13 @@ html-escaper@^2.0.0:
resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453"
integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==
+html2canvas@^1.0.0-rc.7:
+ version "1.0.0-rc.7"
+ resolved "https://registry.yarnpkg.com/html2canvas/-/html2canvas-1.0.0-rc.7.tgz#70c159ce0e63954a91169531894d08ad5627ac98"
+ integrity sha512-yvPNZGejB2KOyKleZspjK/NruXVQuowu8NnV2HYG7gW7ytzl+umffbtUI62v2dCHQLDdsK6HIDtyJZ0W3neerA==
+ dependencies:
+ css-line-break "1.1.1"
+
http-cache-semantics@^3.8.1:
version "3.8.1"
resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-3.8.1.tgz#39b0e16add9b605bf0a9ef3d9daaf4843b4cacd2"