Commit c3ebc2f20d4dc1e03418c80d278a2cbd34460918

Authored by Igor Kulikov
1 parent c317be2a

Widget Configuration: Widget Actions.

Showing 55 changed files with 3959 additions and 177 deletions

Too many changes to show.

To preserve performance only 55 of 67 files are displayed.

@@ -5432,9 +5432,9 @@ @@ -5432,9 +5432,9 @@
5432 "dev": true 5432 "dev": true
5433 }, 5433 },
5434 "https-proxy-agent": { 5434 "https-proxy-agent": {
5435 - "version": "2.2.2",  
5436 - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.2.tgz",  
5437 - "integrity": "sha512-c8Ndjc9Bkpfx/vCJueCPy0jlP4ccCCSNDp8xwCZzPjKJUm+B+u9WX2x98Qx4n1PiMNTWo3D7KK5ifNV/yJyRzg==", 5435 + "version": "2.2.3",
  5436 + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.3.tgz",
  5437 + "integrity": "sha512-Ytgnz23gm2DVftnzqRRz2dOXZbGd2uiajSw/95bPp6v53zPRspQjLm/AfBgqbJ2qfeRXWIOMVLpp86+/5yX39Q==",
5438 "dev": true, 5438 "dev": true,
5439 "requires": { 5439 "requires": {
5440 "agent-base": "^4.3.0", 5440 "agent-base": "^4.3.0",
@@ -15,7 +15,8 @@ @@ -15,7 +15,8 @@
15 /// 15 ///
16 16
17 import { 17 import {
18 - IWidgetSubscription, SubscriptionEntityInfo, 18 + IWidgetSubscription,
  19 + SubscriptionEntityInfo,
19 WidgetSubscriptionCallbacks, 20 WidgetSubscriptionCallbacks,
20 WidgetSubscriptionContext, 21 WidgetSubscriptionContext,
21 WidgetSubscriptionOptions 22 WidgetSubscriptionOptions
@@ -48,6 +49,7 @@ import { deepClone, isDefined } from '@core/utils'; @@ -48,6 +49,7 @@ import { deepClone, isDefined } from '@core/utils';
48 import { AlarmSourceListener } from '@core/http/alarm.service'; 49 import { AlarmSourceListener } from '@core/http/alarm.service';
49 import { DatasourceListener } from '@core/api/datasource.service'; 50 import { DatasourceListener } from '@core/api/datasource.service';
50 import * as deepEqual from 'deep-equal'; 51 import * as deepEqual from 'deep-equal';
  52 +import { EntityId } from '@app/shared/models/id/entity-id';
51 53
52 export class WidgetSubscription implements IWidgetSubscription { 54 export class WidgetSubscription implements IWidgetSubscription {
53 55
@@ -339,7 +341,44 @@ export class WidgetSubscription implements IWidgetSubscription { @@ -339,7 +341,44 @@ export class WidgetSubscription implements IWidgetSubscription {
339 } 341 }
340 342
341 getFirstEntityInfo(): SubscriptionEntityInfo { 343 getFirstEntityInfo(): SubscriptionEntityInfo {
342 - return undefined; 344 + let entityId: EntityId;
  345 + let entityName: string;
  346 + if (this.type === widgetType.rpc) {
  347 + if (this.targetDeviceId) {
  348 + entityId = {
  349 + entityType: EntityType.DEVICE,
  350 + id: this.targetDeviceId
  351 + };
  352 + entityName = this.targetDeviceName;
  353 + }
  354 + } else if (this.type === widgetType.alarm) {
  355 + if (this.alarmSource && this.alarmSource.entityType && this.alarmSource.entityId) {
  356 + entityId = {
  357 + entityType: this.alarmSource.entityType,
  358 + id: this.alarmSource.entityId
  359 + };
  360 + entityName = this.alarmSource.entityName;
  361 + }
  362 + } else {
  363 + for (const datasource of this.datasources) {
  364 + if (datasource && datasource.entityType && datasource.entityId) {
  365 + entityId = {
  366 + entityType: datasource.entityType,
  367 + id: datasource.entityId
  368 + };
  369 + entityName = datasource.entityName;
  370 + break;
  371 + }
  372 + }
  373 + }
  374 + if (entityId) {
  375 + return {
  376 + entityId,
  377 + entityName
  378 + };
  379 + } else {
  380 + return null;
  381 + }
343 } 382 }
344 383
345 onAliasesChanged(aliasIds: Array<string>): boolean { 384 onAliasesChanged(aliasIds: Array<string>): boolean {
@@ -26,6 +26,10 @@ import { @@ -26,6 +26,10 @@ import {
26 ColorPickerDialogComponent, 26 ColorPickerDialogComponent,
27 ColorPickerDialogData 27 ColorPickerDialogData
28 } from '@shared/components/dialog/color-picker-dialog.component'; 28 } from '@shared/components/dialog/color-picker-dialog.component';
  29 +import {
  30 + MaterialIconsDialogComponent,
  31 + MaterialIconsDialogData
  32 +} from '@shared/components/dialog/material-icons-dialog.component';
29 33
30 @Injectable( 34 @Injectable(
31 { 35 {
@@ -85,6 +89,17 @@ export class DialogService { @@ -85,6 +89,17 @@ export class DialogService {
85 }).afterClosed(); 89 }).afterClosed();
86 } 90 }
87 91
  92 + materialIconPicker(icon: string): Observable<string> {
  93 + return this.dialog.open<MaterialIconsDialogComponent, MaterialIconsDialogData, string>(MaterialIconsDialogComponent,
  94 + {
  95 + disableClose: true,
  96 + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
  97 + data: {
  98 + icon
  99 + }
  100 + }).afterClosed();
  101 + }
  102 +
88 private permissionDenied() { 103 private permissionDenied() {
89 this.alert( 104 this.alert(
90 this.translate.instant('access.permission-denied'), 105 this.translate.instant('access.permission-denied'),
@@ -14,7 +14,7 @@ @@ -14,7 +14,7 @@
14 /// limitations under the License. 14 /// limitations under the License.
15 /// 15 ///
16 16
17 -import { Inject, Injectable } from '@angular/core'; 17 +import { Inject, Injectable, NgZone } from '@angular/core';
18 import { WINDOW } from '@core/services/window.service'; 18 import { WINDOW } from '@core/services/window.service';
19 import { ExceptionData } from '@app/shared/models/error.models'; 19 import { ExceptionData } from '@app/shared/models/error.models';
20 import { deepClone, deleteNullProperties, isDefined, isUndefined } from '@core/utils'; 20 import { deepClone, deleteNullProperties, isDefined, isUndefined } from '@core/utils';
@@ -28,6 +28,8 @@ import { alarmFields } from '@shared/models/alarm.models'; @@ -28,6 +28,8 @@ import { alarmFields } from '@shared/models/alarm.models';
28 import { materialColors } from '@app/shared/models/material.models'; 28 import { materialColors } from '@app/shared/models/material.models';
29 import { WidgetInfo } from '@home/models/widget-component.models'; 29 import { WidgetInfo } from '@home/models/widget-component.models';
30 import jsonSchemaDefaults from 'json-schema-defaults'; 30 import jsonSchemaDefaults from 'json-schema-defaults';
  31 +import * as materialIconsCodepoints from '!raw-loader!material-design-icons/iconfont/codepoints';
  32 +import { Observable, of, ReplaySubject } from 'rxjs';
31 33
32 const varsRegex = /\$\{([^}]*)\}/g; 34 const varsRegex = /\$\{([^}]*)\}/g;
33 35
@@ -58,6 +60,13 @@ const defaultAlarmFields: Array<string> = [ @@ -58,6 +60,13 @@ const defaultAlarmFields: Array<string> = [
58 alarmFields.status.keyName 60 alarmFields.status.keyName
59 ]; 61 ];
60 62
  63 +const commonMaterialIcons: Array<string> = [ 'more_horiz', 'more_vert', 'open_in_new',
  64 + 'visibility', 'play_arrow', 'arrow_back', 'arrow_downward',
  65 + 'arrow_forward', 'arrow_upwards', 'close', 'refresh', 'menu', 'show_chart', 'multiline_chart', 'pie_chart', 'insert_chart', 'people',
  66 + 'person', 'domain', 'devices_other', 'now_widgets', 'dashboards', 'map', 'pin_drop', 'my_location', 'extension', 'search',
  67 + 'settings', 'notifications', 'notifications_active', 'info', 'info_outline', 'warning', 'list', 'file_download', 'import_export',
  68 + 'share', 'add', 'edit', 'done' ];
  69 +
61 @Injectable({ 70 @Injectable({
62 providedIn: 'root' 71 providedIn: 'root'
63 }) 72 })
@@ -85,7 +94,10 @@ export class UtilsService { @@ -85,7 +94,10 @@ export class UtilsService {
85 94
86 defaultAlarmDataKeys: Array<DataKey> = []; 95 defaultAlarmDataKeys: Array<DataKey> = [];
87 96
  97 + materialIcons: Array<string> = [];
  98 +
88 constructor(@Inject(WINDOW) private window: Window, 99 constructor(@Inject(WINDOW) private window: Window,
  100 + private zone: NgZone,
89 private translate: TranslateService) { 101 private translate: TranslateService) {
90 let frame: Element = null; 102 let frame: Element = null;
91 try { 103 try {
@@ -282,6 +294,31 @@ export class UtilsService { @@ -282,6 +294,31 @@ export class UtilsService {
282 return datasources; 294 return datasources;
283 } 295 }
284 296
  297 + public getMaterialIcons(): Observable<Array<string>> {
  298 + if (this.materialIcons.length) {
  299 + return of(this.materialIcons);
  300 + } else {
  301 + const materialIconsSubject = new ReplaySubject<Array<string>>();
  302 + this.zone.runOutsideAngular(() => {
  303 + const codepointsArray = materialIconsCodepoints
  304 + .split('\n')
  305 + .filter((codepoint) => codepoint && codepoint.length);
  306 + codepointsArray.forEach((codepoint) => {
  307 + const values = codepoint.split(' ');
  308 + if (values && values.length === 2) {
  309 + this.materialIcons.push(values[0]);
  310 + }
  311 + });
  312 + materialIconsSubject.next(this.materialIcons);
  313 + });
  314 + return materialIconsSubject.asObservable();
  315 + }
  316 + }
  317 +
  318 + public getCommonMaterialIcons(): Array<string> {
  319 + return commonMaterialIcons;
  320 + }
  321 +
285 public getMaterialColor(index: number) { 322 public getMaterialColor(index: number) {
286 const colorIndex = index % materialColors.length; 323 const colorIndex = index % materialColors.length;
287 return materialColors[colorIndex].value; 324 return materialColors[colorIndex].value;
@@ -88,7 +88,7 @@ @@ -88,7 +88,7 @@
88 <tb-timewindow *ngIf="widget.hasTimewindow" 88 <tb-timewindow *ngIf="widget.hasTimewindow"
89 #timewindowComponent 89 #timewindowComponent
90 aggregation="{{widget.hasAggregation}}" 90 aggregation="{{widget.hasAggregation}}"
91 - [ngModel]="widget.widget.config.timewindow" 91 + [ngModel]="widgetComponent.widget.config.timewindow"
92 (ngModelChange)="widgetComponent.onTimewindowChanged($event)"> 92 (ngModelChange)="widgetComponent.onTimewindowChanged($event)">
93 </tb-timewindow> 93 </tb-timewindow>
94 </div> 94 </div>
@@ -39,6 +39,7 @@ @@ -39,6 +39,7 @@
39 gridster-item { 39 gridster-item {
40 transition: none; 40 transition: none;
41 overflow: visible; 41 overflow: visible;
  42 + background: none;
42 } 43 }
43 } 44 }
44 45
@@ -15,12 +15,12 @@ @@ -15,12 +15,12 @@
15 /// 15 ///
16 16
17 import { 17 import {
18 - AfterViewInit, 18 + AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef,
19 Component, 19 Component,
20 DoCheck, 20 DoCheck,
21 Input, 21 Input,
22 IterableDiffers, 22 IterableDiffers,
23 - KeyValueDiffers, 23 + KeyValueDiffers, NgZone,
24 OnChanges, 24 OnChanges,
25 OnInit, 25 OnInit,
26 SimpleChanges, 26 SimpleChanges,
@@ -162,7 +162,8 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo @@ -162,7 +162,8 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
162 private dialogService: DialogService, 162 private dialogService: DialogService,
163 private breakpointObserver: BreakpointObserver, 163 private breakpointObserver: BreakpointObserver,
164 private differs: IterableDiffers, 164 private differs: IterableDiffers,
165 - private kvDiffers: KeyValueDiffers) { 165 + private kvDiffers: KeyValueDiffers,
  166 + private ngZone: NgZone) {
166 super(store); 167 super(store);
167 this.authUser = getCurrentAuthUser(store); 168 this.authUser = getCurrentAuthUser(store);
168 } 169 }
@@ -259,20 +260,24 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo @@ -259,20 +260,24 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
259 } 260 }
260 261
261 onUpdateTimewindow(startTimeMs: number, endTimeMs: number, interval?: number): void { 262 onUpdateTimewindow(startTimeMs: number, endTimeMs: number, interval?: number): void {
262 - if (!this.originalDashboardTimewindow) {  
263 - this.originalDashboardTimewindow = deepClone(this.dashboardTimewindow);  
264 - }  
265 - this.dashboardTimewindow = toHistoryTimewindow(this.dashboardTimewindow,  
266 - startTimeMs, endTimeMs, interval, this.timeService);  
267 - this.dashboardTimewindowChangedSubject.next(this.dashboardTimewindow); 263 + this.ngZone.run(() => {
  264 + if (!this.originalDashboardTimewindow) {
  265 + this.originalDashboardTimewindow = deepClone(this.dashboardTimewindow);
  266 + }
  267 + this.dashboardTimewindow = toHistoryTimewindow(this.dashboardTimewindow,
  268 + startTimeMs, endTimeMs, interval, this.timeService);
  269 + this.dashboardTimewindowChangedSubject.next(this.dashboardTimewindow);
  270 + });
268 } 271 }
269 272
270 onResetTimewindow(): void { 273 onResetTimewindow(): void {
271 - if (this.originalDashboardTimewindow) {  
272 - this.dashboardTimewindow = deepClone(this.originalDashboardTimewindow);  
273 - this.originalDashboardTimewindow = null;  
274 - this.dashboardTimewindowChangedSubject.next(this.dashboardTimewindow);  
275 - } 274 + this.ngZone.run(() => {
  275 + if (this.originalDashboardTimewindow) {
  276 + this.dashboardTimewindow = deepClone(this.originalDashboardTimewindow);
  277 + this.originalDashboardTimewindow = null;
  278 + this.dashboardTimewindowChangedSubject.next(this.dashboardTimewindow);
  279 + }
  280 + });
276 } 281 }
277 282
278 isAutofillHeight(): boolean { 283 isAutofillHeight(): boolean {
@@ -456,7 +461,7 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo @@ -456,7 +461,7 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
456 this.gridsterOpts.draggable.enabled = this.isEdit; 461 this.gridsterOpts.draggable.enabled = this.isEdit;
457 } 462 }
458 463
459 - private notifyGridsterOptionsChanged() { 464 + public notifyGridsterOptionsChanged() {
460 if (this.gridster && this.gridster.options) { 465 if (this.gridster && this.gridster.options) {
461 this.gridster.optionsChanged(); 466 this.gridster.optionsChanged();
462 } 467 }
@@ -14,7 +14,7 @@ @@ -14,7 +14,7 @@
14 /// limitations under the License. 14 /// limitations under the License.
15 /// 15 ///
16 16
17 -import { Component, EventEmitter, Input, Output } from '@angular/core'; 17 +import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
18 import { PageComponent } from '@shared/components/page.component'; 18 import { PageComponent } from '@shared/components/page.component';
19 import { Store } from '@ngrx/store'; 19 import { Store } from '@ngrx/store';
20 import { AppState } from '@core/core.state'; 20 import { AppState } from '@core/core.state';
@@ -50,6 +50,12 @@ import { EntityAliasSelectComponent } from './alias/entity-alias-select.componen @@ -50,6 +50,12 @@ import { EntityAliasSelectComponent } from './alias/entity-alias-select.componen
50 import { DataKeysComponent } from '@home/components/widget/data-keys.component'; 50 import { DataKeysComponent } from '@home/components/widget/data-keys.component';
51 import { DataKeyConfigDialogComponent } from './widget/data-key-config-dialog.component'; 51 import { DataKeyConfigDialogComponent } from './widget/data-key-config-dialog.component';
52 import { DataKeyConfigComponent } from './widget/data-key-config.component'; 52 import { DataKeyConfigComponent } from './widget/data-key-config.component';
  53 +import { LegendConfigPanelComponent } from './widget/legend-config-panel.component';
  54 +import { LegendConfigComponent } from './widget/legend-config.component';
  55 +import { ManageWidgetActionsComponent } from './widget/action/manage-widget-actions.component';
  56 +import { WidgetActionDialogComponent } from './widget/action/widget-action-dialog.component';
  57 +import { CustomActionPrettyResourcesTabsComponent } from './widget/action/custom-action-pretty-resources-tabs.component';
  58 +import { CustomActionPrettyEditorComponent } from './widget/action/custom-action-pretty-editor.component';
53 59
54 @NgModule({ 60 @NgModule({
55 entryComponents: [ 61 entryComponents: [
@@ -64,7 +70,9 @@ import { DataKeyConfigComponent } from './widget/data-key-config.component'; @@ -64,7 +70,9 @@ import { DataKeyConfigComponent } from './widget/data-key-config.component';
64 AliasesEntitySelectPanelComponent, 70 AliasesEntitySelectPanelComponent,
65 EntityAliasesDialogComponent, 71 EntityAliasesDialogComponent,
66 EntityAliasDialogComponent, 72 EntityAliasDialogComponent,
67 - DataKeyConfigDialogComponent 73 + DataKeyConfigDialogComponent,
  74 + LegendConfigPanelComponent,
  75 + WidgetActionDialogComponent
68 ], 76 ],
69 declarations: 77 declarations:
70 [ 78 [
@@ -99,7 +107,13 @@ import { DataKeyConfigComponent } from './widget/data-key-config.component'; @@ -99,7 +107,13 @@ import { DataKeyConfigComponent } from './widget/data-key-config.component';
99 EntityAliasSelectComponent, 107 EntityAliasSelectComponent,
100 DataKeysComponent, 108 DataKeysComponent,
101 DataKeyConfigComponent, 109 DataKeyConfigComponent,
102 - DataKeyConfigDialogComponent 110 + DataKeyConfigDialogComponent,
  111 + LegendConfigPanelComponent,
  112 + LegendConfigComponent,
  113 + ManageWidgetActionsComponent,
  114 + WidgetActionDialogComponent,
  115 + CustomActionPrettyResourcesTabsComponent,
  116 + CustomActionPrettyEditorComponent
103 ], 117 ],
104 imports: [ 118 imports: [
105 CommonModule, 119 CommonModule,
@@ -130,7 +144,12 @@ import { DataKeyConfigComponent } from './widget/data-key-config.component'; @@ -130,7 +144,12 @@ import { DataKeyConfigComponent } from './widget/data-key-config.component';
130 EntityAliasSelectComponent, 144 EntityAliasSelectComponent,
131 DataKeysComponent, 145 DataKeysComponent,
132 DataKeyConfigComponent, 146 DataKeyConfigComponent,
133 - DataKeyConfigDialogComponent 147 + DataKeyConfigDialogComponent,
  148 + LegendConfigComponent,
  149 + ManageWidgetActionsComponent,
  150 + WidgetActionDialogComponent,
  151 + CustomActionPrettyResourcesTabsComponent,
  152 + CustomActionPrettyEditorComponent
134 ], 153 ],
135 providers: [ 154 providers: [
136 WidgetComponentService 155 WidgetComponentService
  1 +<!--
  2 +
  3 + Copyright © 2016-2019 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<div class="tb-custom-action-pretty mat-elevation-z1" tb-fullscreen [fullscreen]="fullscreen">
  19 + <div fxLayout="row" fxLayoutAlign="end center" class="tb-action-expand-button" [ngClass]="{'tb-fullscreen-editor': fullscreen}">
  20 + <button mat-button fxHide.xs fxHide.sm
  21 + matTooltip="{{ 'widget.toggle-fullscreen' | translate }}"
  22 + matTooltipPosition="above"
  23 + (click)="fullscreen = !fullscreen">
  24 + <mat-icon>{{ fullscreen ? 'fullscreen_exit' : 'fullscreen' }}</mat-icon>
  25 + <span *ngIf="fullscreen" translate>widget.toggle-fullscreen</span>
  26 + </button>
  27 + </div>
  28 + <div class="tb-custom-action-editor" [ngClass]="{'tb-fullscreen-editor': fullscreen}">
  29 + <div *ngIf="!fullscreen; else fullscreenEditor">
  30 + <tb-custom-action-pretty-resources-tabs [hasCustomFunction]="true"
  31 + [action]="action" (actionUpdated)="onActionUpdated($event ? true : false)">
  32 + </tb-custom-action-pretty-resources-tabs>
  33 + </div>
  34 + <ng-template #fullscreenEditor>
  35 + <div class="tb-fullscreen-panel tb-layout-fill" fxLayout="row">
  36 + <div #leftPanel class="tb-split tb-content">
  37 + <tb-custom-action-pretty-resources-tabs [hasCustomFunction]="false"
  38 + [action]="action" (actionUpdated)="onActionUpdated($event ? true : false)">
  39 + </tb-custom-action-pretty-resources-tabs>
  40 + </div>
  41 + <div #rightPanel class="tb-split tb-content right-panel">
  42 + <tb-js-func
  43 + [(ngModel)]="action.customFunction"
  44 + (ngModelChange)="onActionUpdated()"
  45 + [fillHeight]="true"
  46 + [functionArgs]="['$event', 'widgetContext', 'entityId', 'entityName', 'htmlTemplate', 'additionalParams']"
  47 + [validationArgs]="[]">
  48 + </tb-js-func>
  49 + </div>
  50 + </div>
  51 + </ng-template>
  52 + </div>
  53 +</div>
  1 +/**
  2 + * Copyright © 2016-2019 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +.tb-custom-action-pretty {
  17 + box-sizing: border-box;
  18 + position: relative;
  19 + padding: 8px;
  20 + background-color: #fff;
  21 +
  22 + .tb-fullscreen-panel {
  23 + .tb-custom-action-editor-container {
  24 + height: calc(100% - 40px);
  25 + }
  26 +
  27 + .right-panel {
  28 + padding-top: 8px;
  29 + padding-left: 3px;
  30 + }
  31 +
  32 + tb-js-func .tb-js-func-panel {
  33 + box-sizing: border-box;
  34 + }
  35 +
  36 + mat-tab-group {
  37 + .mat-tab-body-wrapper {
  38 + height: 100%;
  39 + mat-tab-body {
  40 + height: 100%;
  41 + & > div {
  42 + height: 100%;
  43 + }
  44 + }
  45 + }
  46 + }
  47 + }
  48 +
  49 + .tb-split {
  50 + box-sizing: border-box;
  51 + overflow-x: hidden;
  52 + overflow-y: auto;
  53 + }
  54 +
  55 + .tb-content {
  56 + border: 1px solid #c0c0c0;
  57 + }
  58 +
  59 + .gutter {
  60 + background-color: #eee;
  61 + background-repeat: no-repeat;
  62 + background-position: 50%;
  63 + }
  64 +
  65 + .gutter.gutter-horizontal {
  66 + cursor: col-resize;
  67 + background-image: url("../../../../../../assets/split.js/grips/vertical.png");
  68 + }
  69 +
  70 + .tb-split.tb-split-horizontal,
  71 + .gutter.gutter-horizontal {
  72 + float: left;
  73 + height: 100%;
  74 + }
  75 +
  76 + .tb-action-expand-button {
  77 + position: absolute;
  78 + right: 14px;
  79 + z-index: 2;
  80 +
  81 + &.tb-fullscreen-editor {
  82 + position: relative;
  83 + right: 0;
  84 + .mat-button {
  85 + .mat-icon {
  86 + margin-right: 5px;
  87 + }
  88 + }
  89 + }
  90 +
  91 + .mat-button {
  92 + min-width: 36px;
  93 + padding: 0;
  94 + .mat-icon {
  95 + margin-right: 0;
  96 + }
  97 + }
  98 + }
  99 +
  100 + .tb-custom-action-editor {
  101 + &.tb-fullscreen-editor {
  102 + height: 100%;
  103 + }
  104 + }
  105 +}
  106 +
  107 +
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import {
  18 + AfterViewInit,
  19 + ChangeDetectionStrategy,
  20 + Component,
  21 + ElementRef,
  22 + forwardRef,
  23 + Input,
  24 + OnDestroy,
  25 + OnInit,
  26 + ViewChild, ViewEncapsulation, ViewChildren, QueryList, ComponentFactoryResolver
  27 +} from '@angular/core';
  28 +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
  29 +import { TranslateService } from '@ngx-translate/core';
  30 +import { PageComponent } from '@shared/components/page.component';
  31 +import { Store } from '@ngrx/store';
  32 +import { AppState } from '@core/core.state';
  33 +import { MatDialog } from '@angular/material/dialog';
  34 +import { DialogService } from '@core/services/dialog.service';
  35 +import { PageLink } from '@shared/models/page/page-link';
  36 +import { Direction, SortOrder } from '@shared/models/page/sort-order';
  37 +import { MatPaginator } from '@angular/material/paginator';
  38 +import { MatSort } from '@angular/material/sort';
  39 +import { combineLatest, fromEvent, merge } from 'rxjs';
  40 +import { debounceTime, distinctUntilChanged, tap } from 'rxjs/operators';
  41 +import {
  42 + WidgetActionDescriptorInfo,
  43 + WidgetActionsData,
  44 + WidgetActionsDatasource,
  45 + WidgetActionCallbacks, toWidgetActionDescriptor
  46 +} from '@home/components/widget/action/manage-widget-actions.component.models';
  47 +import { UtilsService } from '@core/services/utils.service';
  48 +import { EntityRelation, EntitySearchDirection, RelationTypeGroup } from '@shared/models/relation.models';
  49 +import { RelationDialogComponent, RelationDialogData } from '@home/components/relation/relation-dialog.component';
  50 +import { CustomActionDescriptor, WidgetActionDescriptor, WidgetActionSource } from '@shared/models/widget.models';
  51 +import {
  52 + WidgetActionDialogComponent,
  53 + WidgetActionDialogData
  54 +} from '@home/components/widget/action/widget-action-dialog.component';
  55 +import { deepClone } from '@core/utils';
  56 +import { TbAnchorComponent } from '@shared/components/tb-anchor.component';
  57 +import { CustomActionPrettyResourcesTabsComponent } from '@home/components/widget/action/custom-action-pretty-resources-tabs.component';
  58 +import { MatTab, MatTabGroup } from '@angular/material/tabs';
  59 +
  60 +@Component({
  61 + selector: 'tb-custom-action-pretty-editor',
  62 + templateUrl: './custom-action-pretty-editor.component.html',
  63 + styleUrls: ['./custom-action-pretty-editor.component.scss'],
  64 + providers: [
  65 + {
  66 + provide: NG_VALUE_ACCESSOR,
  67 + useExisting: forwardRef(() => CustomActionPrettyEditorComponent),
  68 + multi: true
  69 + }
  70 + ],
  71 + encapsulation: ViewEncapsulation.None
  72 +})
  73 +export class CustomActionPrettyEditorComponent extends PageComponent implements OnInit, AfterViewInit, OnDestroy, ControlValueAccessor {
  74 +
  75 + @Input() disabled: boolean;
  76 +
  77 + action: CustomActionDescriptor;
  78 +
  79 + fullscreen = false;
  80 +
  81 + @ViewChildren('leftPanel')
  82 + leftPanelElmRef: QueryList<ElementRef<HTMLElement>>;
  83 +
  84 + @ViewChildren('rightPanel')
  85 + rightPanelElmRef: QueryList<ElementRef<HTMLElement>>;
  86 +
  87 + private propagateChange = (_: any) => {};
  88 +
  89 + constructor(protected store: Store<AppState>) {
  90 + super(store);
  91 + }
  92 +
  93 + ngOnInit(): void {
  94 + }
  95 +
  96 + ngAfterViewInit(): void {
  97 + combineLatest(this.leftPanelElmRef.changes, this.rightPanelElmRef.changes).subscribe(() => {
  98 + if (this.leftPanelElmRef.length && this.rightPanelElmRef.length) {
  99 + this.initSplitLayout(this.leftPanelElmRef.first.nativeElement,
  100 + this.rightPanelElmRef.first.nativeElement);
  101 + }
  102 + });
  103 + }
  104 +
  105 + private initSplitLayout(leftPanel: any, rightPanel: any) {
  106 + Split([leftPanel, rightPanel], {
  107 + sizes: [50, 50],
  108 + gutterSize: 8,
  109 + cursor: 'col-resize'
  110 + });
  111 + }
  112 +
  113 + ngOnDestroy(): void {
  114 + }
  115 +
  116 + registerOnChange(fn: any): void {
  117 + this.propagateChange = fn;
  118 + }
  119 +
  120 + registerOnTouched(fn: any): void {
  121 + }
  122 +
  123 + setDisabledState(isDisabled: boolean): void {
  124 + this.disabled = isDisabled;
  125 + }
  126 +
  127 + writeValue(obj: CustomActionDescriptor): void {
  128 + this.action = obj;
  129 + }
  130 +
  131 + public onActionUpdated(valid: boolean = true) {
  132 + if (!valid) {
  133 + this.propagateChange(null);
  134 + } else {
  135 + this.propagateChange(this.action);
  136 + }
  137 + }
  138 +}
  1 +<!--
  2 +
  3 + Copyright © 2016-2019 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<mat-tab-group selectedIndex="{{hasCustomFunction ? 3 : 2}}" dynamicHeight="true" style="width: 100%; height: 100%;">
  19 + <mat-tab label="{{ 'widget.resources' | translate }}" style="width: 100%; height: 100%;">
  20 + <div class="tb-custom-action-editor-container" style="background-color: #fff;">
  21 + <div class="mat-padding">
  22 + <div fxFlex fxLayout="row" style="max-height: 40px;"
  23 + fxLayoutAlign="start center"
  24 + *ngFor="let resource of action.customResources; let i = index" >
  25 + <mat-form-field fxFlex class="mat-block resource-field" floatLabel="never"
  26 + style="margin: 10px 0px 0px 0px; max-height: 40px;">
  27 + <input required matInput [(ngModel)]="resource.url"
  28 + (ngModelChange)="notifyActionUpdated()"
  29 + placeholder="{{ 'widget.resource-url' | translate }}"/>
  30 + </mat-form-field>
  31 + <button mat-button mat-icon-button color="primary"
  32 + [disabled]="isLoading$ | async"
  33 + type="button"
  34 + (click)="removeResource(i)"
  35 + matTooltip="{{'widget.remove-resource' | translate}}"
  36 + matTooltipPosition="above">
  37 + <mat-icon>close</mat-icon>
  38 + </button>
  39 + </div>
  40 + <div style="margin-top: 6px;">
  41 + <button mat-button mat-raised-button color="primary"
  42 + [disabled]="isLoading$ | async"
  43 + type="button"
  44 + (click)="addResource()"
  45 + matTooltip="{{'widget.add-resource' | translate}}"
  46 + matTooltipPosition="above">
  47 + <span translate>action.add</span>
  48 + </button>
  49 + </div>
  50 + </div>
  51 + </div>
  52 + </mat-tab>
  53 + <mat-tab label="{{ 'widget.css' | translate }}" style="width: 100%; height: 100%;">
  54 + <div class="tb-custom-action-editor-container" tb-fullscreen [fullscreen]="cssFullscreen">
  55 + <div class="tb-editor-area-title-panel">
  56 + <button mat-button
  57 + type="button"
  58 + (click)="beautifyCss()">
  59 + {{ 'widget.tidy' | translate }}
  60 + </button>
  61 + <button mat-button
  62 + type="button"
  63 + mat-icon-button class="tb-mat-32"
  64 + (click)="cssFullscreen = !cssFullscreen"
  65 + matTooltip="{{(cssFullscreen ? 'fullscreen.exit' : 'fullscreen.expand') | translate}}"
  66 + matTooltipPosition="above">
  67 + <mat-icon>{{ cssFullscreen ? 'fullscreen_exit' : 'fullscreen' }}</mat-icon>
  68 + </button>
  69 + </div>
  70 + <div #cssInput class="css-panel"></div>
  71 + </div>
  72 + </mat-tab>
  73 + <mat-tab label="{{ 'widget.html' | translate }}" style="width: 100%; height: 100%;">
  74 + <div class="tb-custom-action-editor-container" tb-fullscreen [fullscreen]="htmlFullscreen">
  75 + <div class="tb-editor-area-title-panel">
  76 + <button mat-button
  77 + type="button" (click)="beautifyHtml()">
  78 + {{ 'widget.tidy' | translate }}
  79 + </button>
  80 + <button mat-button
  81 + type="button"
  82 + mat-icon-button class="tb-mat-32"
  83 + (click)="htmlFullscreen = !htmlFullscreen"
  84 + matTooltip="{{(htmlFullscreen ? 'fullscreen.exit' : 'fullscreen.expand') | translate}}"
  85 + matTooltipPosition="above">
  86 + <mat-icon>{{ htmlFullscreen ? 'fullscreen_exit' : 'fullscreen' }}</mat-icon>
  87 + </button>
  88 + </div>
  89 + <div #htmlInput class="html-panel"></div>
  90 + </div>
  91 + </mat-tab>
  92 + <mat-tab *ngIf="hasCustomFunction" label="{{ 'widget.js' | translate }}" style="width: 100%; height: 100%;">
  93 + <tb-js-func
  94 + [(ngModel)]="action.customFunction"
  95 + (ngModelChange)="notifyActionUpdated()"
  96 + [functionArgs]="['$event', 'widgetContext', 'entityId', 'entityName', 'htmlTemplate', 'additionalParams']"
  97 + [validationArgs]="[]">
  98 + </tb-js-func>
  99 + </mat-tab>
  100 +</mat-tab-group>
  1 +/**
  2 + * Copyright © 2016-2019 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +.tb-custom-action-editor-container {
  17 +
  18 + mat-form-field.resource-field {
  19 + max-height: 40px;
  20 + margin: 10px 0px 0px 0px;
  21 + .mat-form-field-wrapper {
  22 + padding-bottom: 0;
  23 + .mat-form-field-flex {
  24 + max-height: 40px;
  25 + .mat-form-field-infix {
  26 + border: 0;
  27 + }
  28 + }
  29 + .mat-form-field-underline {
  30 + bottom: 0;
  31 + }
  32 + }
  33 + }
  34 +
  35 + .html-panel,
  36 + .css-panel {
  37 + width: 100%;
  38 + min-width: 200px;
  39 + height: 100%;
  40 + min-height: 200px;
  41 + }
  42 +
  43 + div.tb-editor-area-title-panel {
  44 + position: absolute;
  45 + top: 5px;
  46 + right: 20px;
  47 + z-index: 5;
  48 + font-size: .8rem;
  49 + font-weight: 500;
  50 +
  51 + label {
  52 + padding: 4px;
  53 + color: #00acc1;
  54 + text-transform: uppercase;
  55 + background: rgba(220, 220, 220, .35);
  56 + border-radius: 5px;
  57 + &:not(:last-child) {
  58 + margin-right: 4px;
  59 + }
  60 + }
  61 +
  62 + button.mat-button, button.mat-icon-button, button.mat-icon-button.tb-mat-32 {
  63 + align-items: center;
  64 + vertical-align: middle;
  65 + min-width: 32px;
  66 + min-height: 15px;
  67 + padding: 4px;
  68 + margin: 0;
  69 + font-size: .8rem;
  70 + line-height: 15px;
  71 + color: #7b7b7b;
  72 + background: rgba(220, 220, 220, .35);
  73 + &:not(:last-child) {
  74 + margin-right: 4px;
  75 + }
  76 + }
  77 + }
  78 +
  79 +}
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import {
  18 + Component,
  19 + ElementRef,
  20 + EventEmitter,
  21 + Input,
  22 + OnChanges,
  23 + OnDestroy,
  24 + OnInit,
  25 + Output, QueryList,
  26 + SimpleChanges,
  27 + ViewChild, ViewChildren, ViewEncapsulation
  28 +} from '@angular/core';
  29 +import { TranslateService } from '@ngx-translate/core';
  30 +import { PageComponent } from '@shared/components/page.component';
  31 +import { Store } from '@ngrx/store';
  32 +import { AppState } from '@core/core.state';
  33 +import { CustomActionDescriptor } from '@shared/models/widget.models';
  34 +import * as ace from 'ace-builds';
  35 +import { CancelAnimationFrame, RafService } from '@core/services/raf.service';
  36 +import { css_beautify, html_beautify } from 'js-beautify';
  37 +import { MatTab } from '@angular/material/tabs';
  38 +import { BehaviorSubject } from 'rxjs';
  39 +
  40 +@Component({
  41 + selector: 'tb-custom-action-pretty-resources-tabs',
  42 + templateUrl: './custom-action-pretty-resources-tabs.component.html',
  43 + styleUrls: ['./custom-action-pretty-resources-tabs.component.scss'],
  44 + encapsulation: ViewEncapsulation.None
  45 +})
  46 +export class CustomActionPrettyResourcesTabsComponent extends PageComponent implements OnInit, OnChanges, OnDestroy {
  47 +
  48 + @Input()
  49 + action: CustomActionDescriptor;
  50 +
  51 + @Input()
  52 + hasCustomFunction: boolean;
  53 +
  54 + @Output()
  55 + actionUpdated: EventEmitter<CustomActionDescriptor> = new EventEmitter<CustomActionDescriptor>();
  56 +
  57 + @ViewChild('htmlInput', {static: true})
  58 + htmlInputElmRef: ElementRef;
  59 +
  60 + @ViewChild('cssInput', {static: true})
  61 + cssInputElmRef: ElementRef;
  62 +
  63 + htmlFullscreen = false;
  64 + cssFullscreen = false;
  65 +
  66 + aceEditors: ace.Ace.Editor[] = [];
  67 + editorsResizeCafs: {[editorId: string]: CancelAnimationFrame} = {};
  68 + aceResizeListeners: { element: any, resizeListener: any }[] = [];
  69 + htmlEditor: ace.Ace.Editor;
  70 + cssEditor: ace.Ace.Editor;
  71 + setValuesPending = false;
  72 +
  73 + constructor(protected store: Store<AppState>,
  74 + private translate: TranslateService,
  75 + private raf: RafService) {
  76 + super(store);
  77 + }
  78 +
  79 + ngOnInit(): void {
  80 + this.initAceEditors();
  81 + if (this.setValuesPending) {
  82 + this.setAceEditorValues();
  83 + this.setValuesPending = false;
  84 + }
  85 + }
  86 +
  87 + ngOnDestroy(): void {
  88 + this.aceResizeListeners.forEach((resizeListener) => {
  89 + // @ts-ignore
  90 + removeResizeListener(resizeListener.element, resizeListener.resizeListener);
  91 + });
  92 + }
  93 +
  94 + ngOnChanges(changes: SimpleChanges): void {
  95 + for (const propName of Object.keys(changes)) {
  96 + const change = changes[propName];
  97 + if (propName === 'action') {
  98 + if (this.aceEditors.length) {
  99 + this.setAceEditorValues();
  100 + } else {
  101 + this.setValuesPending = true;
  102 + }
  103 + }
  104 + }
  105 + }
  106 +
  107 + public notifyActionUpdated() {
  108 + this.actionUpdated.emit(this.validate() ? this.action : null);
  109 + }
  110 +
  111 + private validate(): boolean {
  112 + for (const resource of this.action.customResources) {
  113 + if (!resource.url) {
  114 + return false;
  115 + }
  116 + }
  117 + return true;
  118 + }
  119 +
  120 + public addResource() {
  121 + this.action.customResources.push({url: ''});
  122 + this.notifyActionUpdated();
  123 + }
  124 +
  125 + public removeResource(index: number) {
  126 + if (index > -1) {
  127 + if (this.action.customResources.splice(index, 1).length > 0) {
  128 + this.notifyActionUpdated();
  129 + }
  130 + }
  131 + }
  132 +
  133 + public beautifyCss(): void {
  134 + const res = css_beautify(this.action.customCss, {indent_size: 4});
  135 + if (this.action.customCss !== res) {
  136 + this.action.customCss = res;
  137 + this.cssEditor.setValue(this.action.customCss ? this.action.customCss : '', -1);
  138 + this.notifyActionUpdated();
  139 + }
  140 + }
  141 +
  142 + public beautifyHtml(): void {
  143 + const res = html_beautify(this.action.customHtml, {indent_size: 4, wrap_line_length: 60});
  144 + if (this.action.customHtml !== res) {
  145 + this.action.customHtml = res;
  146 + this.htmlEditor.setValue(this.action.customHtml ? this.action.customHtml : '', -1);
  147 + this.notifyActionUpdated();
  148 + }
  149 + }
  150 +
  151 + private initAceEditors() {
  152 + this.htmlEditor = this.createAceEditor(this.htmlInputElmRef, 'html');
  153 + this.htmlEditor.on('input', () => {
  154 + const editorValue = this.htmlEditor.getValue();
  155 + if (this.action.customHtml !== editorValue) {
  156 + this.action.customHtml = editorValue;
  157 + this.notifyActionUpdated();
  158 + }
  159 + });
  160 + this.cssEditor = this.createAceEditor(this.cssInputElmRef, 'css');
  161 + this.cssEditor.on('input', () => {
  162 + const editorValue = this.cssEditor.getValue();
  163 + if (this.action.customCss !== editorValue) {
  164 + this.action.customCss = editorValue;
  165 + this.notifyActionUpdated();
  166 + }
  167 + });
  168 + }
  169 +
  170 + private createAceEditor(editorElementRef: ElementRef, mode: string): ace.Ace.Editor {
  171 + const editorElement = editorElementRef.nativeElement;
  172 + let editorOptions: Partial<ace.Ace.EditorOptions> = {
  173 + mode: `ace/mode/${mode}`,
  174 + showGutter: true,
  175 + showPrintMargin: true
  176 + };
  177 + const advancedOptions = {
  178 + enableSnippets: true,
  179 + enableBasicAutocompletion: true,
  180 + enableLiveAutocompletion: true
  181 + };
  182 + editorOptions = {...editorOptions, ...advancedOptions};
  183 + const aceEditor = ace.edit(editorElement, editorOptions);
  184 + aceEditor.session.setUseWrapMode(true);
  185 + this.aceEditors.push(aceEditor);
  186 +
  187 + const resizeListener = this.onAceEditorResize.bind(this, aceEditor);
  188 +
  189 + // @ts-ignore
  190 + addResizeListener(editorElement, resizeListener);
  191 + this.aceResizeListeners.push({element: editorElement, resizeListener});
  192 + return aceEditor;
  193 + }
  194 +
  195 + private setAceEditorValues() {
  196 + this.htmlEditor.setValue(this.action.customHtml ? this.action.customHtml : '', -1);
  197 + this.cssEditor.setValue(this.action.customCss ? this.action.customCss : '', -1);
  198 + }
  199 +
  200 + private onAceEditorResize(aceEditor: ace.Ace.Editor) {
  201 + if (this.editorsResizeCafs[aceEditor.id]) {
  202 + this.editorsResizeCafs[aceEditor.id]();
  203 + delete this.editorsResizeCafs[aceEditor.id];
  204 + }
  205 + this.editorsResizeCafs[aceEditor.id] = this.raf.raf(() => {
  206 + aceEditor.resize();
  207 + aceEditor.renderer.updateFull();
  208 + });
  209 + }
  210 +
  211 +}
  1 +/*=======================================================================*/
  2 +/*========== There are two examples: for edit and add entity ==========*/
  3 +/*=======================================================================*/
  4 +/*======================== Edit entity example ========================*/
  5 +/*=======================================================================*/
  6 +/*
  7 +.edit-entity-form md-input-container {
  8 + padding-right: 10px;
  9 +}
  10 +
  11 +.edit-entity-form .boolean-value-input {
  12 + padding-left: 5px;
  13 +}
  14 +
  15 +.edit-entity-form .boolean-value-input .checkbox-label {
  16 + margin-bottom: 8px;
  17 + color: rgba(0,0,0,0.54);
  18 + font-size: 12px;
  19 +}
  20 +
  21 +.relations-list .header {
  22 + padding-right: 5px;
  23 + padding-bottom: 5px;
  24 + padding-left: 5px;
  25 +}
  26 +
  27 +.relations-list .header .cell {
  28 + padding-right: 5px;
  29 + padding-left: 5px;
  30 + font-size: 12px;
  31 + font-weight: 700;
  32 + color: rgba(0, 0, 0, .54);
  33 + white-space: nowrap;
  34 +}
  35 +
  36 +.relations-list .body {
  37 + padding-right: 5px;
  38 + padding-bottom: 15px;
  39 + padding-left: 5px;
  40 +}
  41 +
  42 +.relations-list .body .row {
  43 + padding-top: 5px;
  44 +}
  45 +
  46 +.relations-list .body .cell {
  47 + padding-right: 5px;
  48 + padding-left: 5px;
  49 +}
  50 +
  51 +.relations-list .body md-autocomplete-wrap md-input-container {
  52 + height: 30px;
  53 +}
  54 +
  55 +.relations-list .body .md-button {
  56 + margin: 0;
  57 +}
  58 +
  59 +.relations-list.old-relations tb-entity-select tb-entity-autocomplete button {
  60 + display: none;
  61 +}
  62 +*/
  63 +/*========================================================================*/
  64 +/*========================= Add entity example =========================*/
  65 +/*========================================================================*/
  66 +/*
  67 +.add-entity-form md-input-container {
  68 + padding-right: 10px;
  69 +}
  70 +
  71 +.add-entity-form .boolean-value-input {
  72 + padding-left: 5px;
  73 +}
  74 +
  75 +.add-entity-form .boolean-value-input .checkbox-label {
  76 + margin-bottom: 8px;
  77 + color: rgba(0,0,0,0.54);
  78 + font-size: 12px;
  79 +}
  80 +
  81 +.relations-list .header {
  82 + padding-right: 5px;
  83 + padding-bottom: 5px;
  84 + padding-left: 5px;
  85 +}
  86 +
  87 +.relations-list .header .cell {
  88 + padding-right: 5px;
  89 + padding-left: 5px;
  90 + font-size: 12px;
  91 + font-weight: 700;
  92 + color: rgba(0, 0, 0, .54);
  93 + white-space: nowrap;
  94 +}
  95 +
  96 +.relations-list .body {
  97 + padding-right: 5px;
  98 + padding-bottom: 15px;
  99 + padding-left: 5px;
  100 +}
  101 +
  102 +.relations-list .body .row {
  103 + padding-top: 5px;
  104 +}
  105 +
  106 +.relations-list .body .cell {
  107 + padding-right: 5px;
  108 + padding-left: 5px;
  109 +}
  110 +
  111 +.relations-list .body md-autocomplete-wrap md-input-container {
  112 + height: 30px;
  113 +}
  114 +
  115 +.relations-list .body .md-button {
  116 + margin: 0;
  117 +}
  118 +*/
  1 +<!--=======================================================================-->
  2 +<!--===== There are two example templates: for edit and add entity =====-->
  3 +<!--=======================================================================-->
  4 +<!--======================== Edit entity example ========================-->
  5 +<!--=======================================================================-->
  6 +<!-- -->
  7 +<!--<md-dialog aria-label="Edit entity">-->
  8 +<!-- <form name="editEntityForm" class="edit-entity-form" ng-submit="vm.save()">-->
  9 +<!-- <md-toolbar>-->
  10 +<!-- <div class="md-toolbar-tools">-->
  11 +<!-- <h2>Edit {{vm.entityType.toLowerCase()}} {{vm.entityName}}</h2>-->
  12 +<!-- <span flex></span>-->
  13 +<!-- <md-button class="md-icon-button" ng-click="vm.cancel()">-->
  14 +<!-- <ng-md-icon icon="close" aria-label="Close"></ng-md-icon>-->
  15 +<!-- </md-button>-->
  16 +<!-- </div>-->
  17 +<!-- </md-toolbar>-->
  18 +<!-- <md-dialog-content>-->
  19 +<!-- <div class="md-dialog-content">-->
  20 +<!-- <div layout="row">-->
  21 +<!-- <md-input-container flex class="md-block">-->
  22 +<!-- <label>Entity Name</label>-->
  23 +<!-- <input ng-model="vm.entityName" readonly>-->
  24 +<!-- </md-input-container>-->
  25 +<!-- <md-input-container flex class="md-block">-->
  26 +<!-- <label>Entity Type</label>-->
  27 +<!-- <input ng-model="vm.entityType" readonly>-->
  28 +<!-- </md-input-container>-->
  29 +<!-- <md-input-container flex class="md-block">-->
  30 +<!-- <label>Type</label>-->
  31 +<!-- <input ng-model="vm.type" readonly>-->
  32 +<!-- </md-input-container>-->
  33 +<!-- </div>-->
  34 +<!-- <div layout="row">-->
  35 +<!-- <md-input-container flex class="md-block">-->
  36 +<!-- <label>Latitude</label>-->
  37 +<!-- <input name="latitude" type="number" step="any" ng-model="vm.attributes.latitude">-->
  38 +<!-- </md-input-container>-->
  39 +<!-- <md-input-container flex class="md-block">-->
  40 +<!-- <label>Longitude</label>-->
  41 +<!-- <input name="longitude" type="number" step="any" ng-model="vm.attributes.longitude">-->
  42 +<!-- </md-input-container>-->
  43 +<!-- </div>-->
  44 +<!-- <div layout="row">-->
  45 +<!-- <md-input-container flex class="md-block">-->
  46 +<!-- <label>Address</label>-->
  47 +<!-- <input ng-model="vm.attributes.address">-->
  48 +<!-- </md-input-container>-->
  49 +<!-- <md-input-container flex class="md-block">-->
  50 +<!-- <label>Owner</label>-->
  51 +<!-- <input ng-model="vm.attributes.owner">-->
  52 +<!-- </md-input-container>-->
  53 +<!-- </div>-->
  54 +<!-- <div layout="row">-->
  55 +<!-- <md-input-container flex class="md-block">-->
  56 +<!-- <label>Integer Value</label>-->
  57 +<!-- <input name="integerNumber" type="number" step="1" ng-pattern="/^-?[0-9]+$/" ng-model="vm.attributes.number">-->
  58 +<!-- <div ng-messages="editEntityForm.integerNumber.$error">-->
  59 +<!-- <div ng-message="pattern">Invalid integer value.</div>-->
  60 +<!-- </div>-->
  61 +<!-- </md-input-container>-->
  62 +<!-- <div class="boolean-value-input" layout="column" layout-align="center start" flex>-->
  63 +<!-- <label class="checkbox-label">Boolean Value</label>-->
  64 +<!-- <md-checkbox ng-model="vm.attributes.booleanValue" style="margin-bottom: 40px;">-->
  65 +<!-- {{ (vm.attributes.booleanValue ? "value.true" : "value.false") | translate }}-->
  66 +<!-- </md-checkbox>-->
  67 +<!-- </div>-->
  68 +<!-- </div>-->
  69 +<!-- <div class="relations-list old-relations">-->
  70 +<!-- <div class="md-body-1" style="padding-bottom: 10px; color: rgba(0,0,0,0.57);">Relations</div>-->
  71 +<!-- <div class="body" ng-show="vm.relations.length">-->
  72 +<!-- <div class="row" layout="row" layout-align="start center" ng-repeat="relation in vm.relations track by $index">-->
  73 +<!-- <div class="md-whiteframe-1dp" flex layout="row" style="padding-left: 5px; margin-bottom: 3px;">-->
  74 +<!-- <div flex layout="column">-->
  75 +<!-- <div layout="row">-->
  76 +<!-- <md-input-container class="md-block" style="min-width: 100px;">-->
  77 +<!-- <label>Direction</label>-->
  78 +<!-- <md-select ng-disabled="true" required ng-model="relation.direction">-->
  79 +<!-- <md-option ng-repeat="direction in vm.entitySearchDirection" ng-value="direction">-->
  80 +<!-- {{ ("relation.search-direction." + direction) | translate}}-->
  81 +<!-- </md-option>-->
  82 +<!-- </md-select>-->
  83 +<!-- </md-input-container>-->
  84 +<!-- <tb-relation-type-autocomplete ng-disabled="true" flex class="md-block"-->
  85 +<!-- the-form="editEntityForm"-->
  86 +<!-- ng-model="relation.relationType"-->
  87 +<!-- tb-required="true">-->
  88 +<!-- </tb-relation-type-autocomplete>-->
  89 +<!-- </div>-->
  90 +<!-- <div layout="row">-->
  91 +<!-- <tb-entity-select flex class="md-block"-->
  92 +<!-- the-form="editEntityForm"-->
  93 +<!-- ng-disabled="true"-->
  94 +<!-- tb-required="true"-->
  95 +<!-- ng-model="relation.relatedEntity">-->
  96 +<!-- </tb-entity-select>-->
  97 +<!-- </div>-->
  98 +<!-- </div>-->
  99 +<!-- <div layout="column" layout-align="center center">-->
  100 +<!-- <md-button class="md-icon-button md-primary" style="width: 40px; min-width: 40px;"-->
  101 +<!-- ng-click="vm.removeOldRelation($index,relation)" aria-label="Remove">-->
  102 +<!-- <md-tooltip md-direction="top">Remove relation</md-tooltip>-->
  103 +<!-- <md-icon aria-label="Remove" class="material-icons">-->
  104 +<!-- close-->
  105 +<!-- </md-icon>-->
  106 +<!-- </md-button>-->
  107 +<!-- </div>-->
  108 +<!-- </div>-->
  109 +<!-- </div>-->
  110 +<!-- </div>-->
  111 +<!-- </div>-->
  112 +<!-- <div class="relations-list">-->
  113 +<!-- <div class="md-body-1" style="padding-bottom: 10px; color: rgba(0,0,0,0.57);">New Relations</div>-->
  114 +<!-- <div class="body" ng-show="vm.newRelations.length">-->
  115 +<!-- <div class="row" layout="row" layout-align="start center" ng-repeat="relation in vm.newRelations track by $index">-->
  116 +<!-- <div class="md-whiteframe-1dp" flex layout="row" style="padding-left: 5px; margin-bottom: 3px;">-->
  117 +<!-- <div flex layout="column">-->
  118 +<!-- <div layout="row">-->
  119 +<!-- <md-input-container class="md-block" style="min-width: 100px;">-->
  120 +<!-- <label>Direction</label>-->
  121 +<!-- <md-select name="direction" required ng-model="relation.direction">-->
  122 +<!-- <md-option ng-repeat="direction in vm.entitySearchDirection" ng-value="direction">-->
  123 +<!-- {{ ("relation.search-direction." + direction) | translate}}-->
  124 +<!-- </md-option>-->
  125 +<!-- </md-select>-->
  126 +<!-- <div ng-messages="editEntityForm.direction.$error">-->
  127 +<!-- <div ng-message="required">Relation direction is required.</div>-->
  128 +<!-- </div>-->
  129 +<!-- </md-input-container>-->
  130 +<!-- <tb-relation-type-autocomplete flex class="md-block"-->
  131 +<!-- the-form="editEntityForm"-->
  132 +<!-- ng-model="relation.relationType"-->
  133 +<!-- tb-required="true">-->
  134 +<!-- </tb-relation-type-autocomplete>-->
  135 +<!-- </div>-->
  136 +<!-- <div layout="row">-->
  137 +<!-- <tb-entity-select flex class="md-block"-->
  138 +<!-- the-form="editEntityForm"-->
  139 +<!-- tb-required="true"-->
  140 +<!-- ng-model="relation.relatedEntity">-->
  141 +<!-- </tb-entity-select>-->
  142 +<!-- </div>-->
  143 +<!-- </div>-->
  144 +<!-- <div layout="column" layout-align="center center">-->
  145 +<!-- <md-button class="md-icon-button md-primary" style="width: 40px; min-width: 40px;"-->
  146 +<!-- ng-click="vm.removeRelation($index)" aria-label="Remove">-->
  147 +<!-- <md-tooltip md-direction="top">Remove relation</md-tooltip>-->
  148 +<!-- <md-icon aria-label="Remove" class="material-icons">-->
  149 +<!-- close-->
  150 +<!-- </md-icon>-->
  151 +<!-- </md-button>-->
  152 +<!-- </div>-->
  153 +<!-- </div>-->
  154 +<!-- </div>-->
  155 +<!-- </div>-->
  156 +<!-- <div>-->
  157 +<!-- <md-button class="md-primary md-raised" ng-click="vm.addRelation()" aria-label="Add">-->
  158 +<!-- <md-tooltip md-direction="top">Add Relation</md-tooltip>-->
  159 +<!-- Add-->
  160 +<!-- </md-button>-->
  161 +<!-- </div> -->
  162 +<!-- </div>-->
  163 +<!-- </div>-->
  164 +<!-- </md-dialog-content>-->
  165 +<!-- <md-dialog-actions>-->
  166 +<!-- <md-button type="submit" ng-disabled="editEntityForm.$invalid || !editEntityForm.$dirty" class="md-raised md-primary">Save</md-button>-->
  167 +<!-- <md-button ng-click="vm.cancel()" class="md-primary">Cancel</md-button>-->
  168 +<!-- </md-dialog-actions>-->
  169 +<!-- </form>-->
  170 +<!--</md-dialog>-->
  171 +<!---->
  172 +<!--========================================================================-->
  173 +<!--========================= Add entity example =========================-->
  174 +<!--========================================================================-->
  175 +<!---->
  176 +<!--<md-dialog aria-label="Add entity">-->
  177 +<!-- <form name="addEntityForm" class="add-entity-form" ng-submit="vm.save()">-->
  178 +<!-- <md-toolbar>-->
  179 +<!-- <div class="md-toolbar-tools">-->
  180 +<!-- <h2>Add entity</h2>-->
  181 +<!-- <span flex></span>-->
  182 +<!-- <md-button class="md-icon-button" ng-click="vm.cancel()">-->
  183 +<!-- <ng-md-icon icon="close" aria-label="Close"></ng-md-icon>-->
  184 +<!-- </md-button>-->
  185 +<!-- </div>-->
  186 +<!-- </md-toolbar>-->
  187 +<!-- <md-dialog-content>-->
  188 +<!-- <div class="md-dialog-content">-->
  189 +<!-- <div layout="row">-->
  190 +<!-- <md-input-container flex class="md-block">-->
  191 +<!-- <label>Entity Name</label>-->
  192 +<!-- <input ng-model="vm.entityName" name=entityName required>-->
  193 +<!-- <div ng-messages="addEntityForm.entityName.$error">-->
  194 +<!-- <div ng-message="required">Entity name is required.</div>-->
  195 +<!-- </div>-->
  196 +<!-- </md-input-container>-->
  197 +<!-- <tb-entity-type-select class="md-block" style="min-width: 100px; width: 100px;"-->
  198 +<!-- the-form="addEntityForm"-->
  199 +<!-- tb-required="true"-->
  200 +<!-- allowed-entity-types="vm.allowedEntityTypes"-->
  201 +<!-- ng-model="vm.entityType">-->
  202 +<!-- </tb-entity-type-select>-->
  203 +<!-- <md-input-container flex class="md-block">-->
  204 +<!-- <label>Entity Subtype</label>-->
  205 +<!-- <input ng-model="vm.type" name=type required>-->
  206 +<!-- <div ng-messages="addEntityForm.type.$error">-->
  207 +<!-- <div ng-message="required">Entity subtype is required.</div>-->
  208 +<!-- </div>-->
  209 +<!-- </md-input-container>-->
  210 +<!-- </div>-->
  211 +<!-- <div layout="row">-->
  212 +<!-- <md-input-container flex class="md-block">-->
  213 +<!-- <label>Latitude</label>-->
  214 +<!-- <input name="latitude" type="number" step="any" ng-model="vm.attributes.latitude">-->
  215 +<!-- </md-input-container>-->
  216 +<!-- <md-input-container flex class="md-block">-->
  217 +<!-- <label>Longitude</label>-->
  218 +<!-- <input name="longitude" type="number" step="any" ng-model="vm.attributes.longitude">-->
  219 +<!-- </md-input-container>-->
  220 +<!-- </div>-->
  221 +<!-- <div layout="row">-->
  222 +<!-- <md-input-container flex class="md-block">-->
  223 +<!-- <label>Address</label>-->
  224 +<!-- <input ng-model="vm.attributes.address">-->
  225 +<!-- </md-input-container>-->
  226 +<!-- <md-input-container flex class="md-block">-->
  227 +<!-- <label>Owner</label>-->
  228 +<!-- <input ng-model="vm.attributes.owner">-->
  229 +<!-- </md-input-container>-->
  230 +<!-- </div>-->
  231 +<!-- <div layout="row">-->
  232 +<!-- <md-input-container flex class="md-block">-->
  233 +<!-- <label>Integer Value</label>-->
  234 +<!-- <input name="integerNumber" type="number" step="1" ng-pattern="/^-?[0-9]+$/" ng-model="vm.attributes.number">-->
  235 +<!-- <div ng-messages="addEntityForm.integerNumber.$error">-->
  236 +<!-- <div ng-message="pattern">Invalid integer value.</div>-->
  237 +<!-- </div>-->
  238 +<!-- </md-input-container>-->
  239 +<!-- <div class="boolean-value-input" layout="column" layout-align="center start" flex>-->
  240 +<!-- <label class="checkbox-label">Boolean Value</label>-->
  241 +<!-- <md-checkbox ng-model="vm.attributes.booleanValue" style="margin-bottom: 40px;">-->
  242 +<!-- {{ (vm.attributes.booleanValue ? "value.true" : "value.false") | translate }}-->
  243 +<!-- </md-checkbox>-->
  244 +<!-- </div>-->
  245 +<!-- </div>-->
  246 +<!-- <div class="relations-list">-->
  247 +<!-- <div class="md-body-1" style="padding-bottom: 10px; color: rgba(0,0,0,0.57);">Relations</div>-->
  248 +<!-- <div class="body" ng-show="vm.relations.length">-->
  249 +<!-- <div class="row" layout="row" layout-align="start center" ng-repeat="relation in vm.relations track by $index">-->
  250 +<!-- <div class="md-whiteframe-1dp" flex layout="row" style="padding-left: 5px;">-->
  251 +<!-- <div flex layout="column">-->
  252 +<!-- <div layout="row">-->
  253 +<!-- <md-input-container class="md-block" style="min-width: 100px;">-->
  254 +<!-- <label>Direction</label>-->
  255 +<!-- <md-select name="direction" required ng-model="relation.direction">-->
  256 +<!-- <md-option ng-repeat="direction in vm.entitySearchDirection" ng-value="direction">-->
  257 +<!-- {{ ("relation.search-direction." + direction) | translate}}-->
  258 +<!-- </md-option>-->
  259 +<!-- </md-select>-->
  260 +<!-- <div ng-messages="addEntityForm.direction.$error">-->
  261 +<!-- <div ng-message="required">Relation direction is required.</div>-->
  262 +<!-- </div>-->
  263 +<!-- </md-input-container>-->
  264 +<!-- <tb-relation-type-autocomplete flex class="md-block"-->
  265 +<!-- the-form="addEntityForm"-->
  266 +<!-- ng-model="relation.relationType"-->
  267 +<!-- tb-required="true">-->
  268 +<!-- </tb-relation-type-autocomplete>-->
  269 +<!-- </div>-->
  270 +<!-- <div layout="row">-->
  271 +<!-- <tb-entity-select flex class="md-block"-->
  272 +<!-- the-form="addEntityForm"-->
  273 +<!-- tb-required="true"-->
  274 +<!-- ng-model="relation.relatedEntity">-->
  275 +<!-- </tb-entity-select>-->
  276 +<!-- </div>-->
  277 +<!-- </div>-->
  278 +<!-- <div layout="column" layout-align="center center">-->
  279 +<!-- <md-button class="md-icon-button md-primary" style="width: 40px; min-width: 40px;"-->
  280 +<!-- ng-click="vm.removeRelation($index)" aria-label="Remove">-->
  281 +<!-- <md-tooltip md-direction="top">Remove relation</md-tooltip>-->
  282 +<!-- <md-icon aria-label="Remove" class="material-icons">-->
  283 +<!-- close-->
  284 +<!-- </md-icon>-->
  285 +<!-- </md-button>-->
  286 +<!-- </div>-->
  287 +<!-- </div>-->
  288 +<!-- </div>-->
  289 +<!-- </div>-->
  290 +<!-- <div>-->
  291 +<!-- <md-button class="md-primary md-raised" ng-click="vm.addRelation()" aria-label="Add">-->
  292 +<!-- <md-tooltip md-direction="top">Add Relation</md-tooltip>-->
  293 +<!-- Add-->
  294 +<!-- </md-button>-->
  295 +<!-- </div> -->
  296 +<!-- </div>-->
  297 +<!-- </div>-->
  298 +<!-- </md-dialog-content>-->
  299 +<!-- <md-dialog-actions>-->
  300 +<!-- <md-button type="submit" ng-disabled="addEntityForm.$invalid || !addEntityForm.$dirty" class="md-raised md-primary">Create</md-button>-->
  301 +<!-- <md-button ng-click="vm.cancel()" class="md-primary">Cancel</md-button>-->
  302 +<!-- </md-dialog-actions>-->
  303 +<!-- </form>-->
  304 +<!--</md-dialog>-->
  1 +/*=======================================================================*/
  2 +/*===== There are three examples: for delete, edit and add entity =====*/
  3 +/*=======================================================================*/
  4 +/*======================= Delete entity example =======================*/
  5 +/*=======================================================================*/
  6 +//
  7 +//var $injector = widgetContext.$scope.$injector;
  8 +//var $mdDialog = $injector.get('$mdDialog'),
  9 +// $document = $injector.get('$document'),
  10 +// types = $injector.get('types'),
  11 +// assetService = $injector.get('assetService'),
  12 +// deviceService = $injector.get('deviceService')
  13 +// $rootScope = $injector.get('$rootScope'),
  14 +// $q = $injector.get('$q');
  15 +//
  16 +//openDeleteEntityDialog();
  17 +//
  18 +//function openDeleteEntityDialog() {
  19 +// var title = 'Delete ' + entityId.entityType
  20 +// .toLowerCase() + ' ' +
  21 +// entityName;
  22 +// var content = 'Are you sure you want to delete the ' +
  23 +// entityId.entityType.toLowerCase() + ' ' +
  24 +// entityName + '?';
  25 +// var confirm = $mdDialog.confirm()
  26 +// .targetEvent($event)
  27 +// .title(title)
  28 +// .htmlContent(content)
  29 +// .ariaLabel(title)
  30 +// .cancel('Cancel')
  31 +// .ok('Delete');
  32 +// $mdDialog.show(confirm).then(function() {
  33 +// deleteEntity();
  34 +// })
  35 +//}
  36 +//
  37 +//function deleteEntity() {
  38 +// deleteEntityPromise(entityId).then(
  39 +// function success() {
  40 +// updateAliasData();
  41 +// },
  42 +// function fail() {
  43 +// showErrorDialog();
  44 +// }
  45 +// );
  46 +//}
  47 +//
  48 +//function deleteEntityPromise(entityId) {
  49 +// if (entityId.entityType == types.entityType.asset) {
  50 +// return assetService.deleteAsset(entityId.id);
  51 +// } else if (entityId.entityType == types.entityType.device) {
  52 +// return deviceService.deleteDevice(entityId.id);
  53 +// }
  54 +//}
  55 +//
  56 +//function updateAliasData() {
  57 +// var aliasIds = [];
  58 +// for (var id in widgetContext.aliasController.resolvedAliases) {
  59 +// aliasIds.push(id);
  60 +// }
  61 +// var tasks = [];
  62 +// aliasIds.forEach(function(aliasId) {
  63 +// widgetContext.aliasController.setAliasUnresolved(aliasId);
  64 +// tasks.push(widgetContext.aliasController.getAliasInfo(aliasId));
  65 +// });
  66 +// $q.all(tasks).then(function() {
  67 +// $rootScope.$broadcast('entityAliasesChanged', aliasIds);
  68 +// });
  69 +//}
  70 +//
  71 +//function showErrorDialog() {
  72 +// var title = 'Error';
  73 +// var content = 'An error occurred while deleting the entity. Please try again.';
  74 +// var alert = $mdDialog.alert()
  75 +// .title(title)
  76 +// .htmlContent(content)
  77 +// .ariaLabel(title)
  78 +// .parent(angular.element($document[0].body))
  79 +// .targetEvent($event)
  80 +// .multiple(true)
  81 +// .clickOutsideToClose(true)
  82 +// .ok('CLOSE');
  83 +// $mdDialog.show(alert);
  84 +//}
  85 +//
  86 +/*=======================================================================*/
  87 +/*======================== Edit entity example ========================*/
  88 +/*=======================================================================*/
  89 +//
  90 +//var $injector = widgetContext.$scope.$injector;
  91 +//var $mdDialog = $injector.get('$mdDialog'),
  92 +// $document = $injector.get('$document'),
  93 +// $q = $injector.get('$q'),
  94 +// types = $injector.get('types'),
  95 +// $rootScope = $injector.get('$rootScope'),
  96 +// entityService = $injector.get('entityService'),
  97 +// attributeService = $injector.get('attributeService'),
  98 +// entityRelationService = $injector.get('entityRelationService');
  99 +//
  100 +//openEditEntityDialog();
  101 +//
  102 +//function openEditEntityDialog() {
  103 +// $mdDialog.show({
  104 +// controller: ['$scope','$mdDialog', EditEntityDialogController],
  105 +// controllerAs: 'vm',
  106 +// template: htmlTemplate,
  107 +// locals: {
  108 +// entityId: entityId
  109 +// },
  110 +// parent: angular.element($document[0].body),
  111 +// targetEvent: $event,
  112 +// multiple: true,
  113 +// clickOutsideToClose: false
  114 +// });
  115 +//}
  116 +//
  117 +//function EditEntityDialogController($scope,$mdDialog) {
  118 +// var vm = this;
  119 +// vm.entityId = entityId;
  120 +// vm.entityName = entityName;
  121 +// vm.entityType = entityId.entityType;
  122 +// vm.allowedEntityTypes = [types.entityType.asset, types.entityType.device];
  123 +// vm.allowedRelatedEntityTypes = [];
  124 +// vm.entitySearchDirection = types.entitySearchDirection;
  125 +// vm.attributes = {};
  126 +// vm.serverAttributes = {};
  127 +// vm.relations = [];
  128 +// vm.newRelations = [];
  129 +// vm.relationsToDelete = [];
  130 +// getEntityInfo();
  131 +//
  132 +// vm.addRelation = function() {
  133 +// var relation = {
  134 +// direction: null,
  135 +// relationType: null,
  136 +// relatedEntity: null
  137 +// };
  138 +// vm.newRelations.push(relation);
  139 +// $scope.editEntityForm.$setDirty();
  140 +// };
  141 +// vm.removeRelation = function(index) {
  142 +// if (index > -1) {
  143 +// vm.newRelations.splice(index, 1);
  144 +// $scope.editEntityForm.$setDirty();
  145 +// }
  146 +// };
  147 +// vm.removeOldRelation = function(index, relation) {
  148 +// if (index > -1) {
  149 +// vm.relations.splice(index, 1);
  150 +// vm.relationsToDelete.push(relation);
  151 +// $scope.editEntityForm.$setDirty();
  152 +// }
  153 +// };
  154 +// vm.save = function() {
  155 +// saveAttributes();
  156 +// saveRelations();
  157 +// $scope.editEntityForm.$setPristine();
  158 +// };
  159 +// vm.cancel = function() {
  160 +// $mdDialog.hide();
  161 +// };
  162 +//
  163 +// function getEntityAttributes(attributes) {
  164 +// for (var i = 0; i < attributes.length; i++) {
  165 +// vm.attributes[attributes[i].key] = attributes[i].value;
  166 +// }
  167 +// vm.serverAttributes = angular.copy(vm.attributes);
  168 +// }
  169 +//
  170 +// function getEntityRelations(relations) {
  171 +// var relationsFrom = relations[0];
  172 +// var relationsTo = relations[1];
  173 +// for (var i=0; i < relationsFrom.length; i++) {
  174 +// var relation = {
  175 +// direction: types.entitySearchDirection.from,
  176 +// relationType: relationsFrom[i].type,
  177 +// relatedEntity: relationsFrom[i].to
  178 +// };
  179 +// vm.relations.push(relation);
  180 +// }
  181 +// for (var i=0; i < relationsTo.length; i++) {
  182 +// var relation = {
  183 +// direction: types.entitySearchDirection.to,
  184 +// relationType: relationsTo[i].type,
  185 +// relatedEntity: relationsTo[i].from
  186 +// };
  187 +// vm.relations.push(relation);
  188 +// }
  189 +// }
  190 +//
  191 +// function getEntityInfo() {
  192 +// entityService.getEntity(entityId.entityType, entityId.id).then(
  193 +// function(entity) {
  194 +// vm.entity = entity;
  195 +// vm.type = vm.entity.type;
  196 +// });
  197 +// attributeService.getEntityAttributesValues(entityId.entityType, entityId.id, 'SERVER_SCOPE').then(
  198 +// function(data){
  199 +// if (data.length) {
  200 +// getEntityAttributes(data);
  201 +// }
  202 +// });
  203 +// $q.all([entityRelationService.findInfoByFrom(entityId.id, entityId.entityType), entityRelationService.findInfoByTo(entityId.id, entityId.entityType)]).then(
  204 +// function(relations){
  205 +// getEntityRelations(relations);
  206 +// });
  207 +// }
  208 +//
  209 +// function saveAttributes() {
  210 +// var attributesArray = [];
  211 +// for (var key in vm.attributes) {
  212 +// if (vm.attributes[key] !== vm.serverAttributes[key]) {
  213 +// attributesArray.push({key: key, value: vm.attributes[key]});
  214 +// }
  215 +// }
  216 +// if (attributesArray.length > 0) {
  217 +// attributeService.saveEntityAttributes(entityId.entityType, entityId.id, \"SERVER_SCOPE\", attributesArray);
  218 +// }
  219 +// }
  220 +//
  221 +// function saveRelations() {
  222 +// var tasks = [];
  223 +// for (var i=0; i < vm.newRelations.length; i++) {
  224 +// var relation = {
  225 +// type: vm.newRelations[i].relationType
  226 +// };
  227 +// if (vm.newRelations[i].direction == types.entitySearchDirection.from) {
  228 +// relation.to = vm.newRelations[i].relatedEntity;
  229 +// relation.from = entityId;
  230 +// } else {
  231 +// relation.to = entityId;
  232 +// relation.from = vm.newRelations[i].relatedEntity;
  233 +// }
  234 +// tasks.push(entityRelationService.saveRelation(relation));
  235 +// }
  236 +// for (var i=0; i < vm.relationsToDelete.length; i++) {
  237 +// var relation = {
  238 +// type: vm.relationsToDelete[i].relationType
  239 +// };
  240 +// if (vm.relationsToDelete[i].direction == types.entitySearchDirection.from) {
  241 +// relation.to = vm.relationsToDelete[i].relatedEntity;
  242 +// relation.from = entityId;
  243 +// } else {
  244 +// relation.to = entityId;
  245 +// relation.from = vm.relationsToDelete[i].relatedEntity;
  246 +// }
  247 +// tasks.push(entityRelationService.deleteRelation(relation.from.id, relation.from.entityType, relation.type, relation.to.id, relation.to.entityType));
  248 +// }
  249 +// $q.all(tasks).then(function(){
  250 +// vm.relations = vm.relations.concat(vm.newRelations);
  251 +// vm.newRelations = [];
  252 +// vm.relationsToDelete = [];
  253 +// updateAliasData();
  254 +// });
  255 +// }
  256 +//
  257 +// function updateAliasData() {
  258 +// var aliasIds = [];
  259 +// for (var id in widgetContext.aliasController.resolvedAliases) {
  260 +// aliasIds.push(id);
  261 +// }
  262 +// var tasks = [];
  263 +// aliasIds.forEach(function(aliasId) {
  264 +// widgetContext.aliasController.setAliasUnresolved(aliasId);
  265 +// tasks.push(widgetContext.aliasController.getAliasInfo(aliasId));
  266 +// });
  267 +// $q.all(tasks).then(function() {
  268 +// $rootScope.$broadcast('entityAliasesChanged', aliasIds);
  269 +// });
  270 +// }
  271 +//}
  272 +//
  273 +/*========================================================================*/
  274 +/*========================= Add entity example =========================*/
  275 +/*========================================================================*/
  276 +//
  277 +//var $injector = widgetContext.$scope.$injector;
  278 +//var $mdDialog = $injector.get('$mdDialog'),
  279 +// $document = $injector.get('$document'),
  280 +// $q = $injector.get('$q'),
  281 +// $rootScope = $injector.get('$rootScope'),
  282 +// types = $injector.get('types'),
  283 +// assetService = $injector.get('assetService'),
  284 +// deviceService = $injector.get('deviceService'),
  285 +// attributeService = $injector.get('attributeService'),
  286 +// entityRelationService = $injector.get('entityRelationService');
  287 +//
  288 +//openAddEntityDialog();
  289 +//
  290 +//function openAddEntityDialog() {
  291 +// $mdDialog.show({
  292 +// controller: ['$scope','$mdDialog', AddEntityDialogController],
  293 +// controllerAs: 'vm',
  294 +// template: htmlTemplate,
  295 +// locals: {
  296 +// entityId: entityId
  297 +// },
  298 +// parent: angular.element($document[0].body),
  299 +// targetEvent: $event,
  300 +// multiple: true,
  301 +// clickOutsideToClose: false
  302 +// });
  303 +//}
  304 +//
  305 +//function AddEntityDialogController($scope, $mdDialog) {
  306 +// var vm = this;
  307 +// vm.allowedEntityTypes = [types.entityType.asset, types.entityType.device];
  308 +// vm.allowedRelatedEntityTypes = [];
  309 +// vm.entitySearchDirection = types.entitySearchDirection;
  310 +// vm.attributes = {};
  311 +// vm.relations = [];
  312 +//
  313 +// vm.addRelation = function() {
  314 +// var relation = {
  315 +// direction: null,
  316 +// relationType: null,
  317 +// relatedEntity: null
  318 +// };
  319 +// vm.relations.push(relation);
  320 +// };
  321 +// vm.removeRelation = function(index) {
  322 +// if (index > -1) {
  323 +// vm.relations.splice(index, 1);
  324 +// }
  325 +// };
  326 +// vm.save = function() {
  327 +// $scope.addEntityForm.$setPristine();
  328 +// saveEntityPromise().then(
  329 +// function (entity) {
  330 +// saveAttributes(entity.id);
  331 +// saveRelations(entity.id);
  332 +// $mdDialog.hide();
  333 +// }
  334 +// );
  335 +// };
  336 +// vm.cancel = function() {
  337 +// $mdDialog.hide();
  338 +// };
  339 +//
  340 +//
  341 +// function saveEntityPromise() {
  342 +// var entity = {
  343 +// name: vm.entityName,
  344 +// type: vm.type
  345 +// };
  346 +// if (vm.entityType == types.entityType.asset) {
  347 +// return assetService.saveAsset(entity);
  348 +// } else if (vm.entityType == types.entityType.device) {
  349 +// return deviceService.saveDevice(entity);
  350 +// }
  351 +// }
  352 +//
  353 +// function saveAttributes(entityId) {
  354 +// var attributesArray = [];
  355 +// for (var key in vm.attributes) {
  356 +// attributesArray.push({key: key, value: vm.attributes[key]});
  357 +// }
  358 +// if (attributesArray.length > 0) {
  359 +// attributeService.saveEntityAttributes(entityId.entityType, entityId.id, \"SERVER_SCOPE\", attributesArray);
  360 +// }
  361 +// }
  362 +//
  363 +// function saveRelations(entityId) {
  364 +// var tasks = [];
  365 +// for (var i=0; i < vm.relations.length; i++) {
  366 +// var relation = {
  367 +// type: vm.relations[i].relationType
  368 +// };
  369 +// if (vm.relations[i].direction == types.entitySearchDirection.from) {
  370 +// relation.to = vm.relations[i].relatedEntity;
  371 +// relation.from = entityId;
  372 +// } else {
  373 +// relation.to = entityId;
  374 +// relation.from = vm.relations[i].relatedEntity;
  375 +// }
  376 +// tasks.push(entityRelationService.saveRelation(relation));
  377 +// }
  378 +// $q.all(tasks).then(function(){
  379 +// updateAliasData();
  380 +// });
  381 +// }
  382 +//
  383 +// function updateAliasData() {
  384 +// var aliasIds = [];
  385 +// for (var id in widgetContext.aliasController.resolvedAliases) {
  386 +// aliasIds.push(id);
  387 +// }
  388 +// var tasks = [];
  389 +// aliasIds.forEach(function(aliasId) {
  390 +// widgetContext.aliasController.setAliasUnresolved(aliasId);
  391 +// tasks.push(widgetContext.aliasController.getAliasInfo(aliasId));
  392 +// });
  393 +// $q.all(tasks).then(function() {
  394 +// $rootScope.$broadcast('entityAliasesChanged', aliasIds);
  395 +// });
  396 +// }
  397 +//}
  1 +<!--
  2 +
  3 + Copyright © 2016-2019 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<div class="mat-padding tb-entity-table tb-absolute-fill">
  19 + <div fxFlex fxLayout="column" class="mat-elevation-z1 tb-entity-table-content">
  20 + <mat-toolbar class="mat-table-toolbar" [fxShow]="!textSearchMode">
  21 + <div class="mat-toolbar-tools">
  22 + <span class="tb-entity-table-title" translate>widget-config.actions</span>
  23 + <span fxFlex></span>
  24 + <button mat-button mat-icon-button [disabled]="isLoading$ | async"
  25 + (click)="addAction($event)"
  26 + matTooltip="{{ 'widget-config.add-action' | translate }}"
  27 + matTooltipPosition="above">
  28 + <mat-icon>add</mat-icon>
  29 + </button>
  30 + <button mat-button mat-icon-button [disabled]="isLoading$ | async" (click)="enterFilterMode()"
  31 + matTooltip="{{ 'action.search' | translate }}"
  32 + matTooltipPosition="above">
  33 + <mat-icon>search</mat-icon>
  34 + </button>
  35 + </div>
  36 + </mat-toolbar>
  37 + <mat-toolbar class="mat-table-toolbar" [fxShow]="textSearchMode">
  38 + <div class="mat-toolbar-tools">
  39 + <button mat-button mat-icon-button
  40 + matTooltip="{{ 'widget-config.search-actions' | translate }}"
  41 + matTooltipPosition="above">
  42 + <mat-icon>search</mat-icon>
  43 + </button>
  44 + <mat-form-field fxFlex>
  45 + <mat-label>&nbsp;</mat-label>
  46 + <input #searchInput matInput
  47 + [(ngModel)]="pageLink.textSearch"
  48 + placeholder="{{ 'widget-config.search-actions' | translate }}"/>
  49 + </mat-form-field>
  50 + <button mat-button mat-icon-button (click)="exitFilterMode()"
  51 + matTooltip="{{ 'action.close' | translate }}"
  52 + matTooltipPosition="above">
  53 + <mat-icon>close</mat-icon>
  54 + </button>
  55 + </div>
  56 + </mat-toolbar>
  57 + <div fxFlex class="table-container">
  58 + <mat-table [dataSource]="dataSource"
  59 + matSort [matSortActive]="pageLink.sortOrder.property" [matSortDirection]="(pageLink.sortOrder.direction + '').toLowerCase()" matSortDisableClear>
  60 + <ng-container matColumnDef="actionSourceName">
  61 + <mat-header-cell *matHeaderCellDef mat-sort-header> {{ 'widget-config.action-source' | translate }} </mat-header-cell>
  62 + <mat-cell *matCellDef="let action">
  63 + {{ action.actionSourceName }}
  64 + </mat-cell>
  65 + </ng-container>
  66 + <ng-container matColumnDef="name">
  67 + <mat-header-cell *matHeaderCellDef mat-sort-header> {{ 'widget-config.action-name' | translate }} </mat-header-cell>
  68 + <mat-cell *matCellDef="let action">
  69 + {{ action.name }}
  70 + </mat-cell>
  71 + </ng-container>
  72 + <ng-container matColumnDef="icon">
  73 + <mat-header-cell *matHeaderCellDef mat-sort-header> {{ 'widget-config.action-icon' | translate }} </mat-header-cell>
  74 + <mat-cell *matCellDef="let action">
  75 + <mat-icon>{{ action.icon }}</mat-icon>
  76 + </mat-cell>
  77 + </ng-container>
  78 + <ng-container matColumnDef="typeName">
  79 + <mat-header-cell *matHeaderCellDef mat-sort-header> {{ 'widget-config.action-type' | translate }} </mat-header-cell>
  80 + <mat-cell *matCellDef="let action">
  81 + {{ action.typeName }}
  82 + </mat-cell>
  83 + </ng-container>
  84 + <ng-container matColumnDef="actions" stickyEnd>
  85 + <mat-header-cell *matHeaderCellDef [ngStyle]="{ minWidth: '80px', maxWidth: '80px' }">
  86 + </mat-header-cell>
  87 + <mat-cell *matCellDef="let action" [ngStyle]="{ minWidth: '80px', maxWidth: '80px' }">
  88 + <div fxFlex fxLayout="row" fxLayoutAlign="end">
  89 + <button mat-button mat-icon-button [disabled]="isLoading$ | async"
  90 + matTooltip="{{ 'widget-config.edit-action' | translate }}"
  91 + matTooltipPosition="above"
  92 + (click)="editAction($event, action)">
  93 + <mat-icon>edit</mat-icon>
  94 + </button>
  95 + <button mat-button mat-icon-button [disabled]="isLoading$ | async"
  96 + matTooltip="{{ 'widget-config.delete-action' | translate }}"
  97 + matTooltipPosition="above"
  98 + (click)="deleteAction($event, action)">
  99 + <mat-icon>delete</mat-icon>
  100 + </button>
  101 + </div>
  102 + </mat-cell>
  103 + </ng-container>
  104 + <mat-header-row [ngClass]="{'mat-row-select': true}" *matHeaderRowDef="displayedColumns; sticky: true"></mat-header-row>
  105 + <mat-row [ngClass]="{'mat-row-select': true}"
  106 + *matRowDef="let action; columns: displayedColumns;"></mat-row>
  107 + </mat-table>
  108 + <span [fxShow]="dataSource.isEmpty() | async"
  109 + fxLayoutAlign="center center"
  110 + class="no-data-found" translate>{{ 'widget-config.no-actions-text' }}</span>
  111 + </div>
  112 + <mat-divider></mat-divider>
  113 + <mat-paginator [length]="dataSource.total() | async"
  114 + [pageIndex]="pageLink.page"
  115 + [pageSize]="pageLink.pageSize"
  116 + [pageSizeOptions]="[10, 20, 30]"></mat-paginator>
  117 + </div>
  118 +</div>
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { WidgetActionDescriptor, WidgetActionSource,
  18 + widgetActionTypeTranslationMap, CustomActionDescriptor } from '@app/shared/models/widget.models';
  19 +import { CollectionViewer, DataSource } from '@angular/cdk/typings/collections';
  20 +import { EntityRelationInfo, EntitySearchDirection } from '@shared/models/relation.models';
  21 +import { BehaviorSubject, Observable, of, ReplaySubject } from 'rxjs';
  22 +import { emptyPageData, PageData } from '@shared/models/page/page-data';
  23 +import { SelectionModel } from '@angular/cdk/collections';
  24 +import { EntityRelationService } from '@core/http/entity-relation.service';
  25 +import { TranslateService } from '@ngx-translate/core';
  26 +import { EntityId } from '@shared/models/id/entity-id';
  27 +import { PageLink } from '@shared/models/page/page-link';
  28 +import { catchError, map, publishReplay, refCount, share, take, tap } from 'rxjs/operators';
  29 +import { entityTypeTranslations } from '@shared/models/entity-type.models';
  30 +import { UtilsService } from '@core/services/utils.service';
  31 +import { deepClone, isUndefined } from '@core/utils';
  32 +
  33 +import * as customSampleJs from '!raw-loader!./custom-sample-js.raw';
  34 +import * as customSampleCss from '!raw-loader!./custom-sample-css.raw';
  35 +import * as customSampleHtml from '!raw-loader!./custom-sample-html.raw';
  36 +
  37 +export interface WidgetActionCallbacks {
  38 + fetchDashboardStates: (query: string) => Array<string>;
  39 +}
  40 +
  41 +export interface WidgetActionsData {
  42 + actionsMap: {[actionSourceId: string]: Array<WidgetActionDescriptor>};
  43 + actionSources: {[actionSourceId: string]: WidgetActionSource};
  44 +}
  45 +
  46 +export interface WidgetActionDescriptorInfo extends WidgetActionDescriptor {
  47 + actionSourceId?: string;
  48 + actionSourceName?: string;
  49 + typeName?: string;
  50 +}
  51 +
  52 +export function toWidgetActionDescriptor(action: WidgetActionDescriptorInfo): WidgetActionDescriptor {
  53 + const copy = deepClone(action);
  54 + delete copy.actionSourceId;
  55 + delete copy.actionSourceName;
  56 + delete copy.typeName;
  57 + return copy;
  58 +}
  59 +
  60 +export function toCustomAction(action: WidgetActionDescriptorInfo): CustomActionDescriptor {
  61 + let result: CustomActionDescriptor;
  62 + if (!action || (isUndefined(action.customFunction) && isUndefined(action.customHtml) && isUndefined(action.customCss))) {
  63 + result = {
  64 + customHtml: customSampleHtml,
  65 + customCss: customSampleCss,
  66 + customFunction: customSampleJs
  67 + };
  68 + } else {
  69 + result = {
  70 + customHtml: action.customHtml,
  71 + customCss: action.customCss,
  72 + customFunction: action.customFunction
  73 + };
  74 + }
  75 + result.customResources = action ? deepClone(action.customResources) : [];
  76 + return result;
  77 +}
  78 +
  79 +export class WidgetActionsDatasource implements DataSource<WidgetActionDescriptorInfo> {
  80 +
  81 + private actionsSubject = new BehaviorSubject<WidgetActionDescriptorInfo[]>([]);
  82 + private pageDataSubject = new BehaviorSubject<PageData<WidgetActionDescriptorInfo>>(emptyPageData<WidgetActionDescriptorInfo>());
  83 +
  84 + public pageData$ = this.pageDataSubject.asObservable();
  85 +
  86 + private allActions: Observable<Array<WidgetActionDescriptorInfo>>;
  87 +
  88 + private actionsMap: {[actionSourceId: string]: Array<WidgetActionDescriptor>};
  89 + private actionSources: {[actionSourceId: string]: WidgetActionSource};
  90 +
  91 + constructor(private translate: TranslateService,
  92 + private utils: UtilsService) {}
  93 +
  94 + connect(collectionViewer: CollectionViewer): Observable<WidgetActionDescriptorInfo[] | ReadonlyArray<WidgetActionDescriptorInfo>> {
  95 + return this.actionsSubject.asObservable();
  96 + }
  97 +
  98 + disconnect(collectionViewer: CollectionViewer): void {
  99 + this.actionsSubject.complete();
  100 + this.pageDataSubject.complete();
  101 + }
  102 +
  103 + setActions(actionsData: WidgetActionsData) {
  104 + this.actionsMap = actionsData.actionsMap;
  105 + this.actionSources = actionsData.actionSources;
  106 + }
  107 +
  108 + loadActions(pageLink: PageLink, reload: boolean = false): Observable<PageData<WidgetActionDescriptorInfo>> {
  109 + if (reload) {
  110 + this.allActions = null;
  111 + }
  112 + const result = new ReplaySubject<PageData<WidgetActionDescriptorInfo>>();
  113 + this.fetchActions(pageLink).pipe(
  114 + catchError(() => of(emptyPageData<WidgetActionDescriptorInfo>())),
  115 + ).subscribe(
  116 + (pageData) => {
  117 + this.actionsSubject.next(pageData.data);
  118 + this.pageDataSubject.next(pageData);
  119 + result.next(pageData);
  120 + }
  121 + );
  122 + return result;
  123 + }
  124 +
  125 + fetchActions(pageLink: PageLink): Observable<PageData<WidgetActionDescriptorInfo>> {
  126 + return this.getAllActions().pipe(
  127 + map((data) => pageLink.filterData(data))
  128 + );
  129 + }
  130 +
  131 + getAllActions(): Observable<Array<WidgetActionDescriptorInfo>> {
  132 + if (!this.allActions) {
  133 + const actions: WidgetActionDescriptorInfo[] = [];
  134 + for (const actionSourceId of Object.keys(this.actionsMap)) {
  135 + const descriptors = this.actionsMap[actionSourceId];
  136 + descriptors.forEach((descriptor) => {
  137 + actions.push(this.toWidgetActionDescriptorInfo(actionSourceId, descriptor));
  138 + });
  139 + }
  140 + this.allActions = of(actions).pipe(
  141 + publishReplay(1),
  142 + refCount()
  143 + );
  144 + }
  145 + return this.allActions;
  146 + }
  147 +
  148 + private toWidgetActionDescriptorInfo(actionSourceId: string, action: WidgetActionDescriptor): WidgetActionDescriptorInfo {
  149 + const actionSource = this.actionSources[actionSourceId];
  150 + const actionSourceName = actionSource ? this.utils.customTranslation(actionSource.name, actionSource.name) : actionSourceId;
  151 + const typeName = this.translate.instant(widgetActionTypeTranslationMap.get(action.type));
  152 + return { actionSourceId, actionSourceName, typeName, ...action};
  153 + }
  154 +
  155 + isEmpty(): Observable<boolean> {
  156 + return this.actionsSubject.pipe(
  157 + map((actions) => !actions.length)
  158 + );
  159 + }
  160 +
  161 + total(): Observable<number> {
  162 + return this.pageDataSubject.pipe(
  163 + map((pageData) => pageData.totalElements)
  164 + );
  165 + }
  166 +
  167 +}
  1 +/**
  2 + * Copyright © 2016-2019 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +:host {
  17 + width: 100%;
  18 + height: 100%;
  19 + .tb-entity-table {
  20 + .tb-entity-table-content {
  21 + width: 100%;
  22 + height: 100%;
  23 + background: #fff;
  24 +
  25 + .tb-entity-table-title {
  26 + padding-right: 20px;
  27 + white-space: nowrap;
  28 + overflow: hidden;
  29 + text-overflow: ellipsis;
  30 + }
  31 +
  32 + .table-container {
  33 + overflow: auto;
  34 + }
  35 + }
  36 + }
  37 +}
  38 +
  39 +:host ::ng-deep {
  40 + .mat-sort-header-sorted .mat-sort-header-arrow {
  41 + opacity: 1 !important;
  42 + }
  43 +}
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import {
  18 + AfterViewInit,
  19 + ChangeDetectionStrategy,
  20 + Component,
  21 + ElementRef,
  22 + forwardRef,
  23 + Input,
  24 + OnDestroy,
  25 + OnInit,
  26 + ViewChild
  27 +} from '@angular/core';
  28 +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
  29 +import { TranslateService } from '@ngx-translate/core';
  30 +import { PageComponent } from '@shared/components/page.component';
  31 +import { Store } from '@ngrx/store';
  32 +import { AppState } from '@core/core.state';
  33 +import { MatDialog } from '@angular/material/dialog';
  34 +import { DialogService } from '@core/services/dialog.service';
  35 +import { PageLink } from '@shared/models/page/page-link';
  36 +import { Direction, SortOrder } from '@shared/models/page/sort-order';
  37 +import { MatPaginator } from '@angular/material/paginator';
  38 +import { MatSort } from '@angular/material/sort';
  39 +import { fromEvent, merge } from 'rxjs';
  40 +import { debounceTime, distinctUntilChanged, tap } from 'rxjs/operators';
  41 +import {
  42 + WidgetActionDescriptorInfo,
  43 + WidgetActionsData,
  44 + WidgetActionsDatasource,
  45 + WidgetActionCallbacks, toWidgetActionDescriptor
  46 +} from '@home/components/widget/action/manage-widget-actions.component.models';
  47 +import { UtilsService } from '@core/services/utils.service';
  48 +import { EntityRelation, EntitySearchDirection, RelationTypeGroup } from '@shared/models/relation.models';
  49 +import { RelationDialogComponent, RelationDialogData } from '@home/components/relation/relation-dialog.component';
  50 +import { WidgetActionDescriptor, WidgetActionSource } from '@shared/models/widget.models';
  51 +import {
  52 + WidgetActionDialogComponent,
  53 + WidgetActionDialogData
  54 +} from '@home/components/widget/action/widget-action-dialog.component';
  55 +import { deepClone } from '@core/utils';
  56 +
  57 +@Component({
  58 + selector: 'tb-manage-widget-actions',
  59 + templateUrl: './manage-widget-actions.component.html',
  60 + styleUrls: ['./manage-widget-actions.component.scss'],
  61 + providers: [
  62 + {
  63 + provide: NG_VALUE_ACCESSOR,
  64 + useExisting: forwardRef(() => ManageWidgetActionsComponent),
  65 + multi: true
  66 + }
  67 + ]
  68 +})
  69 +export class ManageWidgetActionsComponent extends PageComponent implements OnInit, AfterViewInit, OnDestroy, ControlValueAccessor {
  70 +
  71 + @Input() disabled: boolean;
  72 +
  73 + @Input() callbacks: WidgetActionCallbacks;
  74 +
  75 + innerValue: WidgetActionsData;
  76 +
  77 + displayedColumns: string[];
  78 + pageLink: PageLink;
  79 + textSearchMode = false;
  80 + dataSource: WidgetActionsDatasource;
  81 +
  82 + viewsInited = false;
  83 + dirtyValue = false;
  84 +
  85 + @ViewChild('searchInput', {static: false}) searchInputField: ElementRef;
  86 +
  87 + @ViewChild(MatPaginator, {static: false}) paginator: MatPaginator;
  88 + @ViewChild(MatSort, {static: false}) sort: MatSort;
  89 +
  90 + private propagateChange = (_: any) => {};
  91 +
  92 + constructor(protected store: Store<AppState>,
  93 + private translate: TranslateService,
  94 + private utils: UtilsService,
  95 + private dialog: MatDialog,
  96 + private dialogs: DialogService) {
  97 + super(store);
  98 + const sortOrder: SortOrder = { property: 'actionSourceName', direction: Direction.ASC };
  99 + this.pageLink = new PageLink(10, 0, null, sortOrder);
  100 + this.dataSource = new WidgetActionsDatasource(this.translate, this.utils);
  101 + this.displayedColumns = ['actionSourceName', 'name', 'icon', 'typeName', 'actions'];
  102 + }
  103 +
  104 + ngOnInit(): void {
  105 + }
  106 +
  107 + ngOnDestroy(): void {
  108 + }
  109 +
  110 + ngAfterViewInit() {
  111 +
  112 + fromEvent(this.searchInputField.nativeElement, 'keyup')
  113 + .pipe(
  114 + debounceTime(150),
  115 + distinctUntilChanged(),
  116 + tap(() => {
  117 + this.paginator.pageIndex = 0;
  118 + this.updateData();
  119 + })
  120 + )
  121 + .subscribe();
  122 +
  123 + this.sort.sortChange.subscribe(() => this.paginator.pageIndex = 0);
  124 +
  125 + merge(this.sort.sortChange, this.paginator.page)
  126 + .pipe(
  127 + tap(() => this.updateData())
  128 + )
  129 + .subscribe();
  130 +
  131 + this.viewsInited = true;
  132 + if (this.dirtyValue) {
  133 + this.dirtyValue = false;
  134 + this.updateData(true);
  135 + }
  136 +
  137 + }
  138 +
  139 + updateData(reload: boolean = false) {
  140 + this.pageLink.page = this.paginator.pageIndex;
  141 + this.pageLink.pageSize = this.paginator.pageSize;
  142 + this.pageLink.sortOrder.property = this.sort.active;
  143 + this.pageLink.sortOrder.direction = Direction[this.sort.direction.toUpperCase()];
  144 + this.dataSource.loadActions(this.pageLink, reload);
  145 + }
  146 +
  147 + addAction($event: Event) {
  148 + this.openWidgetActionDialog($event);
  149 + }
  150 +
  151 + editAction($event: Event, action: WidgetActionDescriptorInfo) {
  152 + this.openWidgetActionDialog($event, action);
  153 + }
  154 +
  155 + openWidgetActionDialog($event: Event, action: WidgetActionDescriptorInfo = null) {
  156 + if ($event) {
  157 + $event.stopPropagation();
  158 + }
  159 + const isAdd = action === null;
  160 + let prevActionSourceId = null;
  161 + if (!isAdd) {
  162 + prevActionSourceId = action.actionSourceId;
  163 + }
  164 + const availableActionSources: {[actionSourceId: string]: WidgetActionSource} = {};
  165 + for (const id of Object.keys(this.innerValue.actionSources)) {
  166 + const actionSource = this.innerValue.actionSources[id];
  167 + if (actionSource.multiple) {
  168 + availableActionSources[id] = actionSource;
  169 + } else {
  170 + if (!isAdd && action.actionSourceId === id) {
  171 + availableActionSources[id] = actionSource;
  172 + } else {
  173 + const existing = this.innerValue.actionsMap[id];
  174 + if (!existing || !existing.length) {
  175 + availableActionSources[id] = actionSource;
  176 + }
  177 + }
  178 + }
  179 + }
  180 +
  181 + const actionsData: WidgetActionsData = {
  182 + actionsMap: this.innerValue.actionsMap,
  183 + actionSources: availableActionSources
  184 + };
  185 +
  186 + this.dialog.open<WidgetActionDialogComponent, WidgetActionDialogData,
  187 + WidgetActionDescriptorInfo>(WidgetActionDialogComponent, {
  188 + disableClose: true,
  189 + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
  190 + data: {
  191 + isAdd,
  192 + callbacks: this.callbacks,
  193 + actionsData,
  194 + action: deepClone(action)
  195 + }
  196 + }).afterClosed().subscribe(
  197 + (res) => {
  198 + if (res) {
  199 + this.saveAction(res, isAdd, prevActionSourceId);
  200 + }
  201 + }
  202 + );
  203 + }
  204 +
  205 + private saveAction(actionInfo: WidgetActionDescriptorInfo, isAdd: boolean, prevActionSourceId: string) {
  206 + const actionSourceId = actionInfo.actionSourceId;
  207 + const action = toWidgetActionDescriptor(actionInfo);
  208 + if (isAdd) {
  209 + const targetActions = this.getOrCreateTargetActions(actionSourceId);
  210 + targetActions.push(action);
  211 + } else {
  212 + if (actionSourceId !== prevActionSourceId) {
  213 + let targetActions = this.getOrCreateTargetActions(prevActionSourceId);
  214 + const targetIndex = targetActions.findIndex((targetAction) => targetAction.id === action.id);
  215 + if (targetIndex > -1) {
  216 + targetActions.splice(targetIndex, 1);
  217 + }
  218 + targetActions = this.getOrCreateTargetActions(actionSourceId);
  219 + targetActions.push(action);
  220 + } else {
  221 + const targetActions = this.getOrCreateTargetActions(actionSourceId);
  222 + const targetIndex = targetActions.findIndex((targetAction) => targetAction.id === action.id);
  223 + if (targetIndex > -1) {
  224 + targetActions[targetIndex] = action;
  225 + }
  226 + }
  227 + }
  228 + this.onActionsUpdated();
  229 + }
  230 +
  231 + private getOrCreateTargetActions(actionSourceId: string): Array<WidgetActionDescriptor> {
  232 + const actionsMap = this.innerValue.actionsMap;
  233 + let targetActions = actionsMap[actionSourceId];
  234 + if (!targetActions) {
  235 + targetActions = [];
  236 + actionsMap[actionSourceId] = targetActions;
  237 + }
  238 + return targetActions;
  239 + }
  240 +
  241 + deleteAction($event: Event, action: WidgetActionDescriptorInfo) {
  242 + if ($event) {
  243 + $event.stopPropagation();
  244 + }
  245 + const title = this.translate.instant('widget-config.delete-action-title');
  246 + const content = this.translate.instant('widget-config.delete-action-text', {actionName: action.name});
  247 + this.dialogs.confirm(title, content,
  248 + this.translate.instant('action.no'),
  249 + this.translate.instant('action.yes'), true).subscribe(
  250 + (res) => {
  251 + if (res) {
  252 + const targetActions = this.getOrCreateTargetActions(action.actionSourceId);
  253 + const targetIndex = targetActions.findIndex((targetAction) => targetAction.id === action.id);
  254 + if (targetIndex > -1) {
  255 + targetActions.splice(targetIndex, 1);
  256 + this.onActionsUpdated();
  257 + }
  258 + }
  259 + });
  260 + }
  261 +
  262 + enterFilterMode() {
  263 + this.textSearchMode = true;
  264 + this.pageLink.textSearch = '';
  265 + setTimeout(() => {
  266 + this.searchInputField.nativeElement.focus();
  267 + this.searchInputField.nativeElement.setSelectionRange(0, 0);
  268 + }, 10);
  269 + }
  270 +
  271 + exitFilterMode() {
  272 + this.textSearchMode = false;
  273 + this.pageLink.textSearch = null;
  274 + this.paginator.pageIndex = 0;
  275 + this.updateData();
  276 + }
  277 +
  278 + resetSortAndFilter(update: boolean = true) {
  279 + this.pageLink.textSearch = null;
  280 + this.paginator.pageIndex = 0;
  281 + const sortable = this.sort.sortables.get('actionSourceName');
  282 + this.sort.active = sortable.id;
  283 + this.sort.direction = 'asc';
  284 + if (update) {
  285 + this.updateData(true);
  286 + }
  287 + }
  288 +
  289 + registerOnChange(fn: any): void {
  290 + this.propagateChange = fn;
  291 + }
  292 +
  293 + registerOnTouched(fn: any): void {
  294 + }
  295 +
  296 + setDisabledState(isDisabled: boolean): void {
  297 + this.disabled = isDisabled;
  298 + }
  299 +
  300 + writeValue(obj: WidgetActionsData): void {
  301 + this.innerValue = obj;
  302 + setTimeout(() => {
  303 + this.dataSource.setActions(this.innerValue);
  304 + if (this.viewsInited) {
  305 + this.resetSortAndFilter(true);
  306 + } else {
  307 + this.dirtyValue = true;
  308 + }
  309 + }, 0);
  310 + }
  311 +
  312 + private onActionsUpdated() {
  313 + this.updateData(true);
  314 + this.propagateChange(this.innerValue);
  315 + }
  316 +}
  1 +<!--
  2 +
  3 + Copyright © 2016-2019 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<form #widgetActionForm="ngForm" [formGroup]="widgetActionFormGroup" (ngSubmit)="save()" style="min-width: 600px;">
  19 + <mat-toolbar fxLayout="row" color="primary">
  20 + <h2>{{ (isAdd ? 'widget-config.add-action' : 'widget-config.edit-action' ) | translate }}</h2>
  21 + <span fxFlex></span>
  22 + <button mat-button mat-icon-button
  23 + (click)="cancel()"
  24 + type="button">
  25 + <mat-icon class="material-icons">close</mat-icon>
  26 + </button>
  27 + </mat-toolbar>
  28 + <mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
  29 + </mat-progress-bar>
  30 + <div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
  31 + <div mat-dialog-content>
  32 + <fieldset [disabled]="isLoading$ | async">
  33 + <mat-form-field class="mat-block">
  34 + <mat-label translate>widget-config.action-source</mat-label>
  35 + <mat-select required matInput formControlName="actionSourceId">
  36 + <mat-option *ngFor="let actionSourceItem of data.actionsData.actionSources | keyvalue" [value]="actionSourceItem.key">
  37 + {{ actionSourceName(actionSourceItem.value) }}
  38 + </mat-option>
  39 + </mat-select>
  40 + <mat-error *ngIf="widgetActionFormGroup.get('actionSourceId').hasError('required')">
  41 + {{ 'widget-config.action-source-required' | translate }}
  42 + </mat-error>
  43 + </mat-form-field>
  44 + <mat-form-field class="mat-block" style="padding-bottom: 20px;">
  45 + <mat-label translate>widget-config.action-name</mat-label>
  46 + <input required matInput formControlName="name">
  47 + <mat-error *ngIf="widgetActionFormGroup.get('name').hasError('required')">
  48 + {{ 'widget-config.action-name-required' | translate }}
  49 + </mat-error>
  50 + <mat-error *ngIf="widgetActionFormGroup.get('name').hasError('actionNameNotUnique')"
  51 + [innerHTML]="'widget-config.action-name-not-unique' | translate">
  52 + </mat-error>
  53 + </mat-form-field>
  54 + <tb-material-icon-select
  55 + formControlName="icon">
  56 + </tb-material-icon-select>
  57 + <mat-form-field class="mat-block">
  58 + <mat-label translate>widget-config.action-type</mat-label>
  59 + <mat-select required matInput formControlName="type">
  60 + <mat-option *ngFor="let actionType of widgetActionTypes" [value]="actionType">
  61 + {{ widgetActionTypeTranslations.get(actionType) | translate }}
  62 + </mat-option>
  63 + </mat-select>
  64 + <mat-error *ngIf="widgetActionFormGroup.get('type').hasError('required')">
  65 + {{ 'widget-config.action-type-required' | translate }}
  66 + </mat-error>
  67 + </mat-form-field>
  68 + <section fxLayout="column" [formGroup]="actionTypeFormGroup" [ngSwitch]="widgetActionFormGroup.get('type').value">
  69 + <ng-template [ngSwitchCase]="widgetActionType.openDashboard">
  70 + <div fxLayout="column" style="padding-bottom: 20px;">
  71 + <div class="mat-caption tb-required"
  72 + style="padding-left: 3px; padding-bottom: 10px; color: rgba(0,0,0,0.57);" translate>widget-action.target-dashboard</div>
  73 + <tb-dashboard-autocomplete
  74 + formControlName="targetDashboardId"
  75 + required
  76 + [selectFirstDashboard]="true"
  77 + ></tb-dashboard-autocomplete>
  78 + </div>
  79 + </ng-template>
  80 + <ng-template [ngSwitchCase]="widgetActionFormGroup.get('type').value === widgetActionType.openDashboardState ||
  81 + widgetActionFormGroup.get('type').value === widgetActionType.updateDashboardState ||
  82 + widgetActionFormGroup.get('type').value === widgetActionType.openDashboard ?
  83 + widgetActionFormGroup.get('type').value : ''">
  84 + <mat-form-field class="mat-block">
  85 + <input matInput type="text" placeholder="{{ 'widget-action.target-dashboard-state' | translate }}"
  86 + #dashboardStateInput
  87 + formControlName="targetDashboardStateId"
  88 + [required]="widgetActionFormGroup.get('type').value === widgetActionType.openDashboardState"
  89 + [matAutocomplete]="targetDashboardStateAutocomplete">
  90 + <button *ngIf="actionTypeFormGroup.get('targetDashboardStateId').value"
  91 + type="button"
  92 + matSuffix mat-button mat-icon-button aria-label="Clear"
  93 + (click)="clearTargetDashboardState()">
  94 + <mat-icon class="material-icons">close</mat-icon>
  95 + </button>
  96 + <mat-autocomplete
  97 + class="tb-autocomplete"
  98 + #targetDashboardStateAutocomplete="matAutocomplete">
  99 + <mat-option *ngFor="let state of filteredDashboardStates | async" [value]="state">
  100 + <span [innerHTML]="state | highlight:targetDashboardStateSearchText"></span>
  101 + </mat-option>
  102 + </mat-autocomplete>
  103 + <mat-error *ngIf="actionTypeFormGroup.get('targetDashboardStateId').hasError('required')">
  104 + {{ 'widget-action.target-dashboard-state-required' | translate }}
  105 + </mat-error>
  106 + </mat-form-field>
  107 + </ng-template>
  108 + <ng-template [ngSwitchCase]="widgetActionFormGroup.get('type').value === widgetActionType.openDashboardState ||
  109 + widgetActionFormGroup.get('type').value === widgetActionType.updateDashboardState ?
  110 + widgetActionFormGroup.get('type').value : ''">
  111 + <mat-checkbox formControlName="openRightLayout">
  112 + {{ 'widget-action.open-right-layout' | translate }}
  113 + </mat-checkbox>
  114 + </ng-template>
  115 + <ng-template [ngSwitchCase]="widgetActionFormGroup.get('type').value === widgetActionType.openDashboardState ||
  116 + widgetActionFormGroup.get('type').value === widgetActionType.updateDashboardState ||
  117 + widgetActionFormGroup.get('type').value === widgetActionType.openDashboard ?
  118 + widgetActionFormGroup.get('type').value : ''">
  119 + <div fxFlex fxLayout="column">
  120 + <mat-checkbox formControlName="setEntityId">
  121 + {{ 'widget-action.set-entity-from-widget' | translate }}
  122 + </mat-checkbox>
  123 + <mat-form-field *ngIf="actionTypeFormGroup.get('setEntityId').value"
  124 + floatLabel="always"
  125 + class="mat-block">
  126 + <mat-label translate>alias.state-entity-parameter-name</mat-label>
  127 + <input matInput
  128 + placeholder="{{ 'alias.default-entity-parameter-name' | translate }}"
  129 + formControlName="stateEntityParamName">
  130 + </mat-form-field>
  131 + </div>
  132 + </ng-template>
  133 + <ng-template [ngSwitchCase]="widgetActionType.custom">
  134 + <tb-js-func
  135 + formControlName="customFunction"
  136 + [functionArgs]="['$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams']"
  137 + [validationArgs]="[]"
  138 + ></tb-js-func>
  139 + </ng-template>
  140 + <ng-template [ngSwitchCase]="widgetActionType.customPretty">
  141 + <tb-custom-action-pretty-editor
  142 + formControlName="customAction">
  143 + </tb-custom-action-pretty-editor>
  144 + </ng-template>
  145 + </section>
  146 + </fieldset>
  147 + </div>
  148 + <div mat-dialog-actions fxLayout="row">
  149 + <span fxFlex></span>
  150 + <button mat-button mat-raised-button color="primary"
  151 + type="submit"
  152 + [disabled]="(isLoading$ | async) || widgetActionFormGroup.invalid || actionTypeFormGroup.invalid || (!widgetActionFormGroup.dirty && !actionTypeFormGroup.dirty)">
  153 + {{ (isAdd ? 'action.add' : 'action.save') | translate }}
  154 + </button>
  155 + <button mat-button color="primary"
  156 + style="margin-right: 20px;"
  157 + type="button"
  158 + [disabled]="(isLoading$ | async)"
  159 + (click)="cancel()" cdkFocusInitial>
  160 + {{ 'action.cancel' | translate }}
  161 + </button>
  162 + </div>
  163 +</form>
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { Component, ElementRef, Inject, OnInit, SkipSelf, ViewChild } from '@angular/core';
  18 +import { ErrorStateMatcher, MAT_DIALOG_DATA, MatDialogRef } from '@angular/material';
  19 +import { Store } from '@ngrx/store';
  20 +import { AppState } from '@core/core.state';
  21 +import {
  22 + FormBuilder,
  23 + FormControl,
  24 + FormGroup,
  25 + FormGroupDirective,
  26 + NgForm,
  27 + ValidatorFn,
  28 + Validators
  29 +} from '@angular/forms';
  30 +import { Observable, of } from 'rxjs';
  31 +import { Router } from '@angular/router';
  32 +import { DialogComponent } from '@app/shared/components/dialog.component';
  33 +import {
  34 + toCustomAction,
  35 + WidgetActionCallbacks,
  36 + WidgetActionDescriptorInfo,
  37 + WidgetActionsData
  38 +} from '@home/components/widget/action/manage-widget-actions.component.models';
  39 +import { UtilsService } from '@core/services/utils.service';
  40 +import { WidgetActionSource, WidgetActionType, widgetActionTypeTranslationMap } from '@shared/models/widget.models';
  41 +import { map, mergeMap, startWith, tap } from 'rxjs/operators';
  42 +import { DashboardService } from '@core/http/dashboard.service';
  43 +import { Dashboard } from '@shared/models/dashboard.models';
  44 +import { DashboardUtilsService } from '@core/services/dashboard-utils.service';
  45 +
  46 +export interface WidgetActionDialogData {
  47 + isAdd: boolean;
  48 + callbacks: WidgetActionCallbacks;
  49 + actionsData: WidgetActionsData;
  50 + action?: WidgetActionDescriptorInfo;
  51 +}
  52 +
  53 +@Component({
  54 + selector: 'tb-widget-action-dialog',
  55 + templateUrl: './widget-action-dialog.component.html',
  56 + providers: [{provide: ErrorStateMatcher, useExisting: WidgetActionDialogComponent}],
  57 + styleUrls: []
  58 +})
  59 +export class WidgetActionDialogComponent extends DialogComponent<WidgetActionDialogComponent,
  60 + WidgetActionDescriptorInfo> implements OnInit, ErrorStateMatcher {
  61 +
  62 + @ViewChild('dashboardStateInput', {static: false}) dashboardStateInput: ElementRef;
  63 +
  64 + widgetActionFormGroup: FormGroup;
  65 + actionTypeFormGroup: FormGroup;
  66 +
  67 + isAdd: boolean;
  68 + action: WidgetActionDescriptorInfo;
  69 +
  70 + widgetActionTypes = Object.keys(WidgetActionType);
  71 + widgetActionTypeTranslations = widgetActionTypeTranslationMap;
  72 + widgetActionType = WidgetActionType;
  73 +
  74 + filteredDashboardStates: Observable<Array<string>>;
  75 + targetDashboardStateSearchText = '';
  76 + selectedDashboardStateIds: Observable<Array<string>>;
  77 +
  78 + submitted = false;
  79 +
  80 + constructor(protected store: Store<AppState>,
  81 + protected router: Router,
  82 + private utils: UtilsService,
  83 + private dashboardService: DashboardService,
  84 + private dashboardUtils: DashboardUtilsService,
  85 + @Inject(MAT_DIALOG_DATA) public data: WidgetActionDialogData,
  86 + @SkipSelf() private errorStateMatcher: ErrorStateMatcher,
  87 + public dialogRef: MatDialogRef<WidgetActionDialogComponent, WidgetActionDescriptorInfo>,
  88 + public fb: FormBuilder) {
  89 + super(store, router, dialogRef);
  90 + this.isAdd = data.isAdd;
  91 + if (this.isAdd) {
  92 + this.action = {
  93 + id: this.utils.guid(),
  94 + name: '',
  95 + icon: 'more_horiz',
  96 + type: null
  97 + };
  98 + } else {
  99 + this.action = this.data.action;
  100 + }
  101 + }
  102 +
  103 + ngOnInit(): void {
  104 + this.widgetActionFormGroup = this.fb.group({});
  105 + this.widgetActionFormGroup.addControl('actionSourceId',
  106 + this.fb.control(this.action.actionSourceId, [Validators.required]));
  107 + this.widgetActionFormGroup.addControl('name',
  108 + this.fb.control(this.action.name, [this.validateActionName(), Validators.required]));
  109 + this.widgetActionFormGroup.addControl('icon',
  110 + this.fb.control(this.action.icon, [Validators.required]));
  111 + this.widgetActionFormGroup.addControl('type',
  112 + this.fb.control(this.action.type, [Validators.required]));
  113 + this.updateActionTypeFormGroup(this.action.type, this.action);
  114 + this.widgetActionFormGroup.get('type').valueChanges.subscribe((type: WidgetActionType) => {
  115 + this.updateActionTypeFormGroup(type);
  116 + });
  117 + this.widgetActionFormGroup.get('actionSourceId').valueChanges.subscribe(() => {
  118 + this.widgetActionFormGroup.get('name').updateValueAndValidity();
  119 + });
  120 + }
  121 +
  122 + private updateActionTypeFormGroup(type?: WidgetActionType, action?: WidgetActionDescriptorInfo) {
  123 + this.actionTypeFormGroup = this.fb.group({});
  124 + if (type) {
  125 + switch (type) {
  126 + case WidgetActionType.openDashboard:
  127 + case WidgetActionType.openDashboardState:
  128 + case WidgetActionType.updateDashboardState:
  129 + this.actionTypeFormGroup.addControl(
  130 + 'targetDashboardStateId',
  131 + this.fb.control(action ? action.targetDashboardStateId : null,
  132 + type === WidgetActionType.openDashboardState ? [Validators.required] : [])
  133 + );
  134 + this.actionTypeFormGroup.addControl(
  135 + 'setEntityId',
  136 + this.fb.control(action ? action.setEntityId : true, [])
  137 + );
  138 + this.actionTypeFormGroup.addControl(
  139 + 'stateEntityParamName',
  140 + this.fb.control(action ? action.stateEntityParamName : null, [])
  141 + );
  142 + if (type === WidgetActionType.openDashboard) {
  143 + this.actionTypeFormGroup.addControl(
  144 + 'targetDashboardId',
  145 + this.fb.control(action ? action.targetDashboardId : null,
  146 + [Validators.required])
  147 + );
  148 + this.setupSelectedDashboardStateIds(action ? action.targetDashboardId : null);
  149 + } else {
  150 + this.actionTypeFormGroup.addControl(
  151 + 'openRightLayout',
  152 + this.fb.control(action ? action.openRightLayout : false, [])
  153 + );
  154 + }
  155 + this.setupFilteredDashboardStates();
  156 + break;
  157 + case WidgetActionType.custom:
  158 + this.actionTypeFormGroup.addControl(
  159 + 'customFunction',
  160 + this.fb.control(action ? action.customFunction : null, [])
  161 + );
  162 + break;
  163 + case WidgetActionType.customPretty:
  164 + this.actionTypeFormGroup.addControl(
  165 + 'customAction',
  166 + this.fb.control(toCustomAction(action), [Validators.required])
  167 + );
  168 + break;
  169 + }
  170 + }
  171 + }
  172 +
  173 + private setupSelectedDashboardStateIds(targetDashboardId?: string) {
  174 + this.selectedDashboardStateIds =
  175 + this.actionTypeFormGroup.get('targetDashboardId').valueChanges.pipe(
  176 + // startWith<string>(targetDashboardId),
  177 + tap(() => {
  178 + this.targetDashboardStateSearchText = '';
  179 + }),
  180 + mergeMap((dashboardId) => {
  181 + if (dashboardId) {
  182 + return this.dashboardService.getDashboard(dashboardId);
  183 + } else {
  184 + return of(null);
  185 + }
  186 + }),
  187 + map((dashboard: Dashboard) => {
  188 + if (dashboard) {
  189 + dashboard = this.dashboardUtils.validateAndUpdateDashboard(dashboard);
  190 + const states = dashboard.configuration.states;
  191 + return Object.keys(states);
  192 + } else {
  193 + return [];
  194 + }
  195 + })
  196 + );
  197 + }
  198 +
  199 + private setupFilteredDashboardStates() {
  200 + this.targetDashboardStateSearchText = '';
  201 + this.filteredDashboardStates = this.actionTypeFormGroup.get('targetDashboardStateId').valueChanges
  202 + .pipe(
  203 + startWith(''),
  204 + map(value => value ? value : ''),
  205 + mergeMap(name => this.fetchDashboardStates(name) )
  206 + );
  207 + }
  208 +
  209 + private fetchDashboardStates(searchText?: string): Observable<Array<string>> {
  210 + this.targetDashboardStateSearchText = searchText;
  211 + if (this.widgetActionFormGroup.get('type').value === WidgetActionType.openDashboard) {
  212 + return this.selectedDashboardStateIds.pipe(
  213 + map(stateIds => {
  214 + const result = searchText ? stateIds.filter(this.createFilterForDashboardState(searchText)) : stateIds;
  215 + if (result && result.length) {
  216 + return result;
  217 + } else {
  218 + return [searchText];
  219 + }
  220 + })
  221 + );
  222 + } else {
  223 + return of(this.data.callbacks.fetchDashboardStates(searchText));
  224 + }
  225 + }
  226 +
  227 + private createFilterForDashboardState(query: string): (stateId: string) => boolean {
  228 + const lowercaseQuery = query.toLowerCase();
  229 + return stateId => stateId.toLowerCase().indexOf(lowercaseQuery) === 0;
  230 + }
  231 +
  232 + public clearTargetDashboardState(value: string = '') {
  233 + this.dashboardStateInput.nativeElement.value = value;
  234 + this.actionTypeFormGroup.get('targetDashboardStateId').patchValue(value, {emitEvent: true});
  235 + setTimeout(() => {
  236 + this.dashboardStateInput.nativeElement.blur();
  237 + this.dashboardStateInput.nativeElement.focus();
  238 + }, 0);
  239 + }
  240 +
  241 + private validateActionName(): ValidatorFn {
  242 + return (c: FormControl) => {
  243 + const newName = c.value;
  244 + const valid = this.checkActionName(newName, this.widgetActionFormGroup.get('actionSourceId').value);
  245 + return !valid ? {
  246 + actionNameNotUnique: true
  247 + } : null;
  248 + };
  249 + }
  250 +
  251 + private checkActionName(name: string, actionSourceId: string): boolean {
  252 + let actionNameIsUnique = true;
  253 + if (name && actionSourceId) {
  254 + const sourceActions = this.data.actionsData.actionsMap[actionSourceId];
  255 + if (sourceActions) {
  256 + const result = sourceActions.filter((sourceAction) => sourceAction.name === name);
  257 + if (result && result.length && result[0].id !== this.action.id) {
  258 + actionNameIsUnique = false;
  259 + }
  260 + }
  261 + }
  262 + return actionNameIsUnique;
  263 + }
  264 +
  265 + isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
  266 + const originalErrorState = this.errorStateMatcher.isErrorState(control, form);
  267 + const customErrorState = !!(control && control.invalid && this.submitted);
  268 + return originalErrorState || customErrorState;
  269 + }
  270 +
  271 + public actionSourceName(actionSource: WidgetActionSource): string {
  272 + if (actionSource) {
  273 + return this.utils.customTranslation(actionSource.name, actionSource.name);
  274 + } else {
  275 + return '';
  276 + }
  277 + }
  278 +
  279 + cancel(): void {
  280 + this.dialogRef.close(null);
  281 + }
  282 +
  283 + save(): void {
  284 + this.submitted = true;
  285 + const type: WidgetActionType = this.widgetActionFormGroup.get('type').value;
  286 + let result: WidgetActionDescriptorInfo;
  287 + if (type === WidgetActionType.customPretty) {
  288 + result = {...this.widgetActionFormGroup.value, ...this.actionTypeFormGroup.get('customAction').value};
  289 + } else {
  290 + result = {...this.widgetActionFormGroup.value, ...this.actionTypeFormGroup.value};
  291 + }
  292 + result.id = this.action.id;
  293 + this.dialogRef.close(result);
  294 + }
  295 +}
@@ -16,7 +16,7 @@ @@ -16,7 +16,7 @@
16 16
17 import { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes'; 17 import { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes';
18 import { 18 import {
19 - AfterViewInit, 19 + AfterViewInit, ChangeDetectionStrategy,
20 Component, 20 Component,
21 ElementRef, 21 ElementRef,
22 forwardRef, 22 forwardRef,
@@ -413,7 +413,7 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, AfterVie @@ -413,7 +413,7 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, AfterVie
413 let fetchObservable: Observable<Array<DataKey>> = null; 413 let fetchObservable: Observable<Array<DataKey>> = null;
414 if (this.datasourceType === DatasourceType.function || this.widgetType === widgetType.alarm) { 414 if (this.datasourceType === DatasourceType.function || this.widgetType === widgetType.alarm) {
415 const dataKeyFilter = this.createDataKeyFilter(this.searchText); 415 const dataKeyFilter = this.createDataKeyFilter(this.searchText);
416 - const targetKeysList = this.datasourceType === DatasourceType.function ? this.functionTypeKeys : this.alarmKeys; 416 + const targetKeysList = this.widgetType === widgetType.alarm ? this.alarmKeys : this.functionTypeKeys;
417 fetchObservable = of(targetKeysList.filter(dataKeyFilter)); 417 fetchObservable = of(targetKeysList.filter(dataKeyFilter));
418 } else { 418 } else {
419 if (this.entityAliasId) { 419 if (this.entityAliasId) {
  1 +<!--
  2 +
  3 + Copyright © 2016-2019 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<form [formGroup]="legendConfigForm">
  19 + <fieldset [disabled]="(isLoading$ | async)">
  20 + <div class="mat-content" style="height: 100%;">
  21 + <div class="mat-padding">
  22 + <section fxLayout="column">
  23 + <mat-form-field>
  24 + <mat-label translate>legend.direction</mat-label>
  25 + <mat-select matInput formControlName="direction" style="min-width: 150px;">
  26 + <mat-option *ngFor="let direction of legendDirections" [value]="direction">
  27 + {{ legendDirectionTranslations.get(direction) | translate }}
  28 + </mat-option>
  29 + </mat-select>
  30 + </mat-form-field>
  31 + <mat-form-field>
  32 + <mat-label translate>legend.position</mat-label>
  33 + <mat-select matInput formControlName="position" style="min-width: 150px;">
  34 + <mat-option *ngFor="let pos of legendPositions" [value]="pos"
  35 + [disabled]="legendConfigForm.get('direction').value === legendDirection.row &&
  36 + (pos === legendPosition.left || pos === legendPosition.right)">
  37 + {{ legendPositionTranslations.get(pos) | translate }}
  38 + </mat-option>
  39 + </mat-select>
  40 + </mat-form-field>
  41 + <mat-checkbox fxFlex formControlName="showMin">
  42 + {{ 'legend.show-min' | translate }}
  43 + </mat-checkbox>
  44 + <mat-checkbox fxFlex formControlName="showMax">
  45 + {{ 'legend.show-max' | translate }}
  46 + </mat-checkbox>
  47 + <mat-checkbox fxFlex formControlName="showAvg">
  48 + {{ 'legend.show-avg' | translate }}
  49 + </mat-checkbox>
  50 + <mat-checkbox fxFlex formControlName="showTotal">
  51 + {{ 'legend.show-total' | translate }}
  52 + </mat-checkbox>
  53 + </section>
  54 + </div>
  55 + </div>
  56 + </fieldset>
  57 +</form>
  1 +/**
  2 + * Copyright © 2016-2019 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +:host {
  17 + width: 100%;
  18 + height: 100%;
  19 + form,
  20 + fieldset {
  21 + height: 100%;
  22 + }
  23 +
  24 + .mat-content {
  25 + overflow: hidden;
  26 + background-color: #fff;
  27 + }
  28 +
  29 + .mat-padding {
  30 + padding: 16px;
  31 + }
  32 +}
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { Component, Inject, InjectionToken, OnInit, ViewContainerRef } from '@angular/core';
  18 +import { Overlay, OverlayRef } from '@angular/cdk/overlay';
  19 +import { PageComponent } from '@shared/components/page.component';
  20 +import { Store } from '@ngrx/store';
  21 +import { AppState } from '@core/core.state';
  22 +import { FormBuilder, FormGroup } from '@angular/forms';
  23 +import {
  24 + LegendConfig,
  25 + LegendDirection,
  26 + legendDirectionTranslationMap,
  27 + LegendPosition,
  28 + legendPositionTranslationMap
  29 +} from '@shared/models/widget.models';
  30 +
  31 +export const LEGEND_CONFIG_PANEL_DATA = new InjectionToken<any>('LegendConfigPanelData');
  32 +
  33 +export interface LegendConfigPanelData {
  34 + legendConfig: LegendConfig;
  35 + legendConfigUpdated: (legendConfig: LegendConfig) => void;
  36 +}
  37 +
  38 +@Component({
  39 + selector: 'tb-legend-config-panel',
  40 + templateUrl: './legend-config-panel.component.html',
  41 + styleUrls: ['./legend-config-panel.component.scss']
  42 +})
  43 +export class LegendConfigPanelComponent extends PageComponent implements OnInit {
  44 +
  45 + legendConfigForm: FormGroup;
  46 +
  47 + legendDirection = LegendDirection;
  48 +
  49 + legendDirections = Object.keys(LegendDirection);
  50 +
  51 + legendDirectionTranslations = legendDirectionTranslationMap;
  52 +
  53 + legendPosition = LegendPosition;
  54 +
  55 + legendPositions = Object.keys(LegendPosition);
  56 +
  57 + legendPositionTranslations = legendPositionTranslationMap;
  58 +
  59 + constructor(@Inject(LEGEND_CONFIG_PANEL_DATA) public data: LegendConfigPanelData,
  60 + public overlayRef: OverlayRef,
  61 + protected store: Store<AppState>,
  62 + public fb: FormBuilder,
  63 + private overlay: Overlay,
  64 + public viewContainerRef: ViewContainerRef) {
  65 + super(store);
  66 + }
  67 +
  68 + ngOnInit(): void {
  69 + this.legendConfigForm = this.fb.group({
  70 + direction: [this.data.legendConfig.direction, []],
  71 + position: [this.data.legendConfig.position, []],
  72 + showMin: [this.data.legendConfig.showMin, []],
  73 + showMax: [this.data.legendConfig.showMax, []],
  74 + showAvg: [this.data.legendConfig.showAvg, []],
  75 + showTotal: [this.data.legendConfig.showTotal, []]
  76 + });
  77 + this.legendConfigForm.get('direction').valueChanges.subscribe((direction: LegendDirection) => {
  78 + this.onDirectionChanged(direction);
  79 + });
  80 + this.onDirectionChanged(this.data.legendConfig.direction);
  81 + this.legendConfigForm.valueChanges.subscribe(() => {
  82 + this.update();
  83 + });
  84 + }
  85 +
  86 + private onDirectionChanged(direction: LegendDirection) {
  87 + if (direction === LegendDirection.row) {
  88 + let position: LegendPosition = this.legendConfigForm.get('position').value;
  89 + if (position !== LegendPosition.bottom && position !== LegendPosition.top) {
  90 + position = LegendPosition.bottom;
  91 + }
  92 + this.legendConfigForm.patchValue(
  93 + {
  94 + position,
  95 + showMin: false,
  96 + showMax: false,
  97 + showAvg: false,
  98 + showTotal: false
  99 + }, {emitEvent: false}
  100 + );
  101 + this.legendConfigForm.get('showMin').disable({emitEvent: false});
  102 + this.legendConfigForm.get('showMax').disable({emitEvent: false});
  103 + this.legendConfigForm.get('showAvg').disable({emitEvent: false});
  104 + this.legendConfigForm.get('showTotal').disable({emitEvent: false});
  105 + } else {
  106 + this.legendConfigForm.get('showMin').enable({emitEvent: false});
  107 + this.legendConfigForm.get('showMax').enable({emitEvent: false});
  108 + this.legendConfigForm.get('showAvg').enable({emitEvent: false});
  109 + this.legendConfigForm.get('showTotal').enable({emitEvent: false});
  110 + }
  111 + }
  112 +
  113 + update() {
  114 + const newLegendConfig: LegendConfig = this.legendConfigForm.value;
  115 + this.data.legendConfigUpdated(newLegendConfig);
  116 + }
  117 +
  118 +}
  1 +<!--
  2 +
  3 + Copyright © 2016-2019 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<button cdkOverlayOrigin #legendConfigPanelOrigin="cdkOverlayOrigin" [disabled]="disabled"
  19 + mat-button mat-raised-button color="primary" (click)="openEditMode($event)">
  20 + <mat-icon class="material-icons">toc</mat-icon>
  21 + <span translate>legend.settings</span>
  22 +</button>
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import {
  18 + Component,
  19 + forwardRef, Inject,
  20 + Input,
  21 + OnDestroy,
  22 + OnInit,
  23 + ViewChild,
  24 + ViewContainerRef
  25 +} from '@angular/core';
  26 +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
  27 +import { TranslateService } from '@ngx-translate/core';
  28 +import { MillisecondsToTimeStringPipe } from '@shared/pipe/milliseconds-to-time-string.pipe';
  29 +import {
  30 + HistoryWindowType,
  31 + Timewindow,
  32 + TimewindowType,
  33 + initModelFromDefaultTimewindow, cloneSelectedTimewindow
  34 +} from '@shared/models/time/time.models';
  35 +import { DatePipe } from '@angular/common';
  36 +import {
  37 + Overlay,
  38 + CdkOverlayOrigin,
  39 + OverlayConfig,
  40 + OverlayPositionBuilder, ConnectedPosition, PositionStrategy, OverlayRef
  41 +} from '@angular/cdk/overlay';
  42 +import {
  43 + TIMEWINDOW_PANEL_DATA,
  44 + TimewindowPanelComponent,
  45 + TimewindowPanelData
  46 +} from '@shared/components/time/timewindow-panel.component';
  47 +import { ComponentPortal, PortalInjector } from '@angular/cdk/portal';
  48 +import { MediaBreakpoints } from '@shared/models/constants';
  49 +import { BreakpointObserver } from '@angular/cdk/layout';
  50 +import { DOCUMENT } from '@angular/common';
  51 +import { WINDOW } from '@core/services/window.service';
  52 +import { TimeService } from '@core/services/time.service';
  53 +import { TooltipPosition } from '@angular/material/typings/tooltip';
  54 +import { deepClone } from '@core/utils';
  55 +import { LegendConfig } from '@shared/models/widget.models';
  56 +import {
  57 + LEGEND_CONFIG_PANEL_DATA,
  58 + LegendConfigPanelComponent,
  59 + LegendConfigPanelData
  60 +} from '@home/components/widget/legend-config-panel.component';
  61 +
  62 +@Component({
  63 + selector: 'tb-legend-config',
  64 + templateUrl: './legend-config.component.html',
  65 + styleUrls: [],
  66 + providers: [
  67 + {
  68 + provide: NG_VALUE_ACCESSOR,
  69 + useExisting: forwardRef(() => LegendConfigComponent),
  70 + multi: true
  71 + }
  72 + ]
  73 +})
  74 +export class LegendConfigComponent implements OnInit, OnDestroy, ControlValueAccessor {
  75 +
  76 + @Input() disabled: boolean;
  77 +
  78 + @ViewChild('legendConfigPanelOrigin', {static: false}) legendConfigPanelOrigin: CdkOverlayOrigin;
  79 +
  80 + innerValue: LegendConfig;
  81 +
  82 + private propagateChange = (_: any) => {};
  83 +
  84 + constructor(private overlay: Overlay,
  85 + public viewContainerRef: ViewContainerRef,
  86 + public breakpointObserver: BreakpointObserver,
  87 + @Inject(DOCUMENT) private document: Document,
  88 + @Inject(WINDOW) private window: Window) {
  89 + }
  90 +
  91 + ngOnInit(): void {
  92 + }
  93 +
  94 + ngOnDestroy(): void {
  95 + }
  96 +
  97 + openEditMode() {
  98 + if (this.disabled) {
  99 + return;
  100 + }
  101 + const isGtSm = this.breakpointObserver.isMatched(MediaBreakpoints['gt-sm']);
  102 + const position = this.overlay.position();
  103 + const config = new OverlayConfig({
  104 + panelClass: 'tb-legend-config-panel',
  105 + backdropClass: 'cdk-overlay-transparent-backdrop',
  106 + hasBackdrop: isGtSm,
  107 + });
  108 + if (isGtSm) {
  109 + config.minWidth = '220px';
  110 + config.maxHeight = '300px';
  111 + const panelHeight = 220;
  112 + const panelWidth = 220;
  113 + const el = this.legendConfigPanelOrigin.elementRef.nativeElement;
  114 + const offset = el.getBoundingClientRect();
  115 + const scrollTop = this.window.pageYOffset || this.document.documentElement.scrollTop || this.document.body.scrollTop || 0;
  116 + const scrollLeft = this.window.pageXOffset || this.document.documentElement.scrollLeft || this.document.body.scrollLeft || 0;
  117 + const bottomY = offset.bottom - scrollTop;
  118 + const leftX = offset.left - scrollLeft;
  119 + let originX;
  120 + let originY;
  121 + let overlayX;
  122 + let overlayY;
  123 + const wHeight = this.document.documentElement.clientHeight;
  124 + const wWidth = this.document.documentElement.clientWidth;
  125 + if (bottomY + panelHeight > wHeight) {
  126 + originY = 'top';
  127 + overlayY = 'bottom';
  128 + } else {
  129 + originY = 'bottom';
  130 + overlayY = 'top';
  131 + }
  132 + if (leftX + panelWidth > wWidth) {
  133 + originX = 'end';
  134 + overlayX = 'end';
  135 + } else {
  136 + originX = 'start';
  137 + overlayX = 'start';
  138 + }
  139 + const connectedPosition: ConnectedPosition = {
  140 + originX,
  141 + originY,
  142 + overlayX,
  143 + overlayY
  144 + };
  145 + config.positionStrategy = position.flexibleConnectedTo(this.legendConfigPanelOrigin.elementRef)
  146 + .withPositions([connectedPosition]);
  147 + } else {
  148 + config.minWidth = '100%';
  149 + config.minHeight = '100%';
  150 + config.positionStrategy = position.global().top('0%').left('0%')
  151 + .right('0%').bottom('0%');
  152 + }
  153 +
  154 + const overlayRef = this.overlay.create(config);
  155 +
  156 + overlayRef.backdropClick().subscribe(() => {
  157 + overlayRef.dispose();
  158 + });
  159 +
  160 + const injector = this._createLegendConfigPanelInjector(
  161 + overlayRef,
  162 + {
  163 + legendConfig: deepClone(this.innerValue),
  164 + legendConfigUpdated: this.legendConfigUpdated.bind(this)
  165 + }
  166 + );
  167 +
  168 + overlayRef.attach(new ComponentPortal(LegendConfigPanelComponent, this.viewContainerRef, injector));
  169 + }
  170 +
  171 + private _createLegendConfigPanelInjector(overlayRef: OverlayRef, data: LegendConfigPanelData): PortalInjector {
  172 + const injectionTokens = new WeakMap<any, any>([
  173 + [LEGEND_CONFIG_PANEL_DATA, data],
  174 + [OverlayRef, overlayRef]
  175 + ]);
  176 + return new PortalInjector(this.viewContainerRef.injector, injectionTokens);
  177 + }
  178 +
  179 + registerOnChange(fn: any): void {
  180 + this.propagateChange = fn;
  181 + }
  182 +
  183 + registerOnTouched(fn: any): void {
  184 + }
  185 +
  186 + setDisabledState(isDisabled: boolean): void {
  187 + this.disabled = isDisabled;
  188 + }
  189 +
  190 + writeValue(obj: LegendConfig): void {
  191 + this.innerValue = obj;
  192 + }
  193 +
  194 + private legendConfigUpdated(legendConfig: LegendConfig) {
  195 + this.innerValue = legendConfig;
  196 + this.propagateChange(this.innerValue);
  197 + }
  198 +}
@@ -181,6 +181,188 @@ @@ -181,6 +181,188 @@
181 </tb-entity-alias-select> 181 </tb-entity-alias-select>
182 </div> 182 </div>
183 </mat-expansion-panel> 183 </mat-expansion-panel>
  184 + <mat-expansion-panel class="tb-datasources" *ngIf="widgetType === widgetTypes.alarm &&
  185 + modelValue?.isDataEnabled" [expanded]="true">
  186 + <mat-expansion-panel-header>
  187 + <mat-panel-title>
  188 + {{ 'widget-config.alarm-source' | translate }}
  189 + </mat-panel-title>
  190 + </mat-expansion-panel-header>
  191 + <div [formGroup]="alarmSourceSettings" style="padding: 0 5px;">
  192 + <section fxFlex
  193 + fxLayout="column"
  194 + fxLayoutAlign="center"
  195 + fxLayout.gt-sm="row"
  196 + fxLayoutAlign.gt-sm="start center">
  197 + <mat-form-field class="tb-datasource-type">
  198 + <mat-select matInput formControlName="type">
  199 + <mat-option *ngFor="let datasourceType of datasourceTypes" [value]="datasourceType">
  200 + {{ datasourceTypesTranslations.get(datasourceType) | translate }}
  201 + </mat-option>
  202 + </mat-select>
  203 + </mat-form-field>
  204 + <section class="tb-datasource" [ngSwitch]="alarmSourceSettings.get('type').value">
  205 + <ng-template [ngSwitchCase]="datasourceType.function">
  206 + <mat-form-field floatLabel="always" [fxShow]="widgetType !== widgetTypes.alarm"
  207 + class="tb-datasource-name" style="min-width: 200px;">
  208 + <mat-label></mat-label>
  209 + <input matInput
  210 + placeholder="{{ 'datasource.name' | translate }}"
  211 + formControlName="name">
  212 + </mat-form-field>
  213 + </ng-template>
  214 + <ng-template [ngSwitchCase]="datasourceType.entity">
  215 + <tb-entity-alias-select
  216 + [tbRequired]="alarmSourceSettings.get('type').value === datasourceType.entity"
  217 + [aliasController]="aliasController"
  218 + formControlName="entityAliasId"
  219 + [callbacks]="widgetConfigCallbacks">
  220 + </tb-entity-alias-select>
  221 + </ng-template>
  222 + </section>
  223 + <tb-data-keys class="tb-data-keys" fxFlex
  224 + [widgetType]="widgetType"
  225 + [datasourceType]="alarmSourceSettings.get('type').value"
  226 + [aliasController]="aliasController"
  227 + [datakeySettingsSchema]="modelValue?.dataKeySettingsSchema"
  228 + [callbacks]="widgetConfigCallbacks"
  229 + [entityAliasId]="alarmSourceSettings.get('entityAliasId').value"
  230 + formControlName="dataKeys">
  231 + </tb-data-keys>
  232 + </section>
  233 + </div>
  234 + </mat-expansion-panel>
  235 + </div>
  236 + </mat-tab>
  237 + <mat-tab label="{{ 'widget-config.settings' | translate }}">
  238 + <div class="mat-content mat-padding"
  239 + fxLayout="column" fxLayoutGap="8px">
  240 + <div [formGroup]="widgetSettings" fxLayout="column" fxLayoutGap="8px">
  241 + <span translate>widget-config.general-settings</span>
  242 + <div fxLayout="column" fxLayoutAlign="center" fxLayout.gt-sm="row" fxLayoutAlign.gt-sm="start center">
  243 + <mat-form-field fxFlex class="mat-block">
  244 + <mat-label translate>widget-config.title</mat-label>
  245 + <input matInput formControlName="title">
  246 + </mat-form-field>
  247 + <div fxFlex [fxShow]="widgetSettings.get('showTitle').value">
  248 + <tb-json-object-edit
  249 + [editorStyle]="{minHeight: '100px'}"
  250 + required
  251 + label="{{ 'widget-config.title-style' | translate }}"
  252 + formControlName="titleStyle"
  253 + ></tb-json-object-edit>
  254 + </div>
  255 + </div>
  256 + <div fxLayout="column" fxLayoutAlign="center" fxLayout.gt-sm="row" fxLayoutAlign.gt-sm="start center"
  257 + fxLayoutGap="8px">
  258 + <div fxLayout="row" fxFlex>
  259 + <mat-checkbox formControlName="showTitleIcon">
  260 + {{ 'widget-config.display-icon' | translate }}
  261 + </mat-checkbox>
  262 + </div>
  263 + <div fxFlex>
  264 + <tb-material-icon-select
  265 + formControlName="titleIcon">
  266 + </tb-material-icon-select>
  267 + </div>
  268 + <tb-color-input fxFlex
  269 + label="{{'widget-config.icon-color' | translate}}"
  270 + icon="format_color_fill"
  271 + openOnInput
  272 + formControlName="iconColor">
  273 + </tb-color-input>
  274 + <mat-form-field fxFlex>
  275 + <mat-label translate>widget-config.icon-size</mat-label>
  276 + <input matInput formControlName="iconSize">
  277 + </mat-form-field>
  278 + </div>
  279 + <div fxLayout="column" fxLayoutAlign="center" fxLayout.gt-sm="row" fxLayoutAlign.gt-sm="start center"
  280 + fxLayoutGap="8px">
  281 + <div fxLayout="row">
  282 + <mat-checkbox formControlName="showTitle">
  283 + {{ 'widget-config.display-title' | translate }}
  284 + </mat-checkbox>
  285 + </div>
  286 + <div fxLayout="row">
  287 + <mat-checkbox formControlName="dropShadow">
  288 + {{ 'widget-config.drop-shadow' | translate }}
  289 + </mat-checkbox>
  290 + </div>
  291 + <div fxLayout="row">
  292 + <mat-checkbox formControlName="enableFullscreen">
  293 + {{ 'widget-config.enable-fullscreen' | translate }}
  294 + </mat-checkbox>
  295 + </div>
  296 + <div fxFlex>
  297 + <tb-json-object-edit
  298 + [editorStyle]="{minHeight: '100px'}"
  299 + required
  300 + label="{{ 'widget-config.widget-style' | translate }}"
  301 + formControlName="widgetStyle"
  302 + ></tb-json-object-edit>
  303 + </div>
  304 + </div>
  305 + <div fxLayout="column" fxLayoutAlign="center" fxLayout.gt-sm="row" fxLayoutAlign.gt-sm="start center"
  306 + fxLayoutGap="8px">
  307 + <tb-color-input fxFlex
  308 + label="{{'widget-config.background-color' | translate}}"
  309 + icon="format_color_fill"
  310 + openOnInput
  311 + formControlName="backgroundColor">
  312 + </tb-color-input>
  313 + <tb-color-input fxFlex
  314 + label="{{'widget-config.text-color' | translate}}"
  315 + icon="format_color_fill"
  316 + openOnInput
  317 + formControlName="color">
  318 + </tb-color-input>
  319 + <mat-form-field fxFlex>
  320 + <mat-label translate>widget-config.padding</mat-label>
  321 + <input matInput formControlName="padding">
  322 + </mat-form-field>
  323 + <mat-form-field fxFlex>
  324 + <mat-label translate>widget-config.margin</mat-label>
  325 + <input matInput formControlName="margin">
  326 + </mat-form-field>
  327 + </div>
  328 + <div fxLayout="column" fxLayoutAlign="center" fxLayout.gt-sm="row" fxLayoutAlign.gt-sm="start center"
  329 + fxLayoutGap="8px">
  330 + <mat-form-field fxFlex>
  331 + <mat-label translate>widget-config.units</mat-label>
  332 + <input matInput formControlName="units">
  333 + </mat-form-field>
  334 + <mat-form-field fxFlex>
  335 + <mat-label translate>widget-config.decimals</mat-label>
  336 + <input matInput formControlName="decimals" type="number" min="0" max="15" step="1">
  337 + </mat-form-field>
  338 + </div>
  339 + <div [fxShow]="widgetType === widgetTypes.timeseries ||
  340 + widgetType === widgetTypes.latest"
  341 + fxLayout="column" fxLayoutAlign="center" fxLayout.gt-sm="row" fxLayoutAlign.gt-sm="start center"
  342 + fxLayoutGap="8px">
  343 + <mat-checkbox fxFlex formControlName="showLegend">
  344 + {{ 'widget-config.display-legend' | translate }}
  345 + </mat-checkbox>
  346 + <section fxFlex fxLayout="row" fxLayoutAlign="start center" style="margin-bottom: 16px;">
  347 + <tb-legend-config formControlName="legendConfig">
  348 + </tb-legend-config>
  349 + </section>
  350 + </div>
  351 + </div>
  352 + <div [formGroup]="layoutSettings" fxLayout="column" fxLayoutGap="8px">
  353 + <span translate>widget-config.mobile-mode-settings</span>
  354 + <div fxLayout="column" fxLayoutAlign="center" fxLayout.gt-sm="row" fxLayoutAlign.gt-sm="start center"
  355 + fxLayoutGap="8px">
  356 + <mat-form-field fxFlex>
  357 + <mat-label translate>widget-config.order</mat-label>
  358 + <input matInput formControlName="mobileOrder" type="number" step="1">
  359 + </mat-form-field>
  360 + <mat-form-field fxFlex>
  361 + <mat-label translate>widget-config.height</mat-label>
  362 + <input matInput formControlName="mobileHeight" type="number" min="1" max="10" step="1">
  363 + </mat-form-field>
  364 + </div>
  365 + </div>
184 </div> 366 </div>
185 </mat-tab> 367 </mat-tab>
186 <mat-tab *ngIf="displayAdvanced()" label="{{ 'widget-config.advanced' | translate }}"> 368 <mat-tab *ngIf="displayAdvanced()" label="{{ 'widget-config.advanced' | translate }}">
@@ -191,6 +373,10 @@ @@ -191,6 +373,10 @@
191 </tb-json-form> 373 </tb-json-form>
192 </div> 374 </div>
193 </mat-tab> 375 </mat-tab>
194 - <mat-tab label="{{ 'widget-config.actions' | translate }}"> 376 + <mat-tab label="{{ 'widget-config.actions' | translate }}" [formGroup]="actionsSettings">
  377 + <tb-manage-widget-actions
  378 + [callbacks]="widgetConfigCallbacks"
  379 + formControlName="actionsData">
  380 + </tb-manage-widget-actions>
195 </mat-tab> 381 </mat-tab>
196 </mat-tab-group> 382 </mat-tab-group>
@@ -16,5 +16,6 @@ @@ -16,5 +16,6 @@
16 16
17 import { EntityAliasSelectCallbacks } from '../alias/entity-alias-select.component.models'; 17 import { EntityAliasSelectCallbacks } from '../alias/entity-alias-select.component.models';
18 import { DataKeysCallbacks } from './data-keys.component.models'; 18 import { DataKeysCallbacks } from './data-keys.component.models';
  19 +import { WidgetActionCallbacks } from './action/manage-widget-actions.component.models';
19 20
20 -export type WidgetConfigCallbacks = EntityAliasSelectCallbacks & DataKeysCallbacks; 21 +export type WidgetConfigCallbacks = EntityAliasSelectCallbacks & DataKeysCallbacks & WidgetActionCallbacks;
@@ -21,6 +21,9 @@ @@ -21,6 +21,9 @@
21 .tb-advanced-widget-config { 21 .tb-advanced-widget-config {
22 height: 100%; 22 height: 100%;
23 } 23 }
  24 + .tb-advanced-widget-config {
  25 + height: 100%;
  26 + }
24 .tb-datasource-type { 27 .tb-datasource-type {
25 min-width: 110px; 28 min-width: 110px;
26 } 29 }
@@ -45,6 +48,13 @@ @@ -45,6 +48,13 @@
45 48
46 :host ::ng-deep { 49 :host ::ng-deep {
47 .tb-widget-config { 50 .tb-widget-config {
  51 + .mat-tab-body-wrapper {
  52 + position: absolute;
  53 + top: 49px;
  54 + left: 0;
  55 + right: 0;
  56 + bottom: 0;
  57 + }
48 .mat-tab-body.mat-tab-body-active { 58 .mat-tab-body.mat-tab-body-active {
49 .mat-tab-body-content > div { 59 .mat-tab-body-content > div {
50 height: 100%; 60 height: 100%;
@@ -14,19 +14,17 @@ @@ -14,19 +14,17 @@
14 /// limitations under the License. 14 /// limitations under the License.
15 /// 15 ///
16 16
17 -import { Component, forwardRef, Input, OnInit } from '@angular/core'; 17 +import { ChangeDetectionStrategy, Component, forwardRef, Input, OnInit } from '@angular/core';
18 import { PageComponent } from '@shared/components/page.component'; 18 import { PageComponent } from '@shared/components/page.component';
19 import { Store } from '@ngrx/store'; 19 import { Store } from '@ngrx/store';
20 import { AppState } from '@core/core.state'; 20 import { AppState } from '@core/core.state';
21 import { 21 import {
22 DataKey, 22 DataKey,
23 Datasource, 23 Datasource,
24 - DatasourceType, datasourceTypeTranslationMap,  
25 - LegendConfig, 24 + DatasourceType,
  25 + datasourceTypeTranslationMap, defaultLegendConfig,
26 WidgetActionDescriptor, 26 WidgetActionDescriptor,
27 - WidgetActionSource,  
28 - widgetType,  
29 - WidgetTypeParameters 27 + widgetType
30 } from '@shared/models/widget.models'; 28 } from '@shared/models/widget.models';
31 import { 29 import {
32 AbstractControl, 30 AbstractControl,
@@ -37,7 +35,7 @@ import { @@ -37,7 +35,7 @@ import {
37 FormGroup, 35 FormGroup,
38 NG_VALIDATORS, 36 NG_VALIDATORS,
39 NG_VALUE_ACCESSOR, 37 NG_VALUE_ACCESSOR,
40 - Validator, ValidatorFn, 38 + Validator,
41 Validators 39 Validators
42 } from '@angular/forms'; 40 } from '@angular/forms';
43 import { WidgetConfigComponentData } from '@home/models/widget-component.models'; 41 import { WidgetConfigComponentData } from '@home/models/widget-component.models';
@@ -55,10 +53,12 @@ import { @@ -55,10 +53,12 @@ import {
55 EntityAliasDialogComponent, 53 EntityAliasDialogComponent,
56 EntityAliasDialogData 54 EntityAliasDialogData
57 } from '@home/components/alias/entity-alias-dialog.component'; 55 } from '@home/components/alias/entity-alias-dialog.component';
58 -import { tap, mergeMap, map, catchError } from 'rxjs/operators'; 56 +import { catchError, map, mergeMap, tap } from 'rxjs/operators';
59 import { MatDialog } from '@angular/material/dialog'; 57 import { MatDialog } from '@angular/material/dialog';
60 import { EntityService } from '@core/http/entity.service'; 58 import { EntityService } from '@core/http/entity.service';
61 import { JsonFormComponentData } from '@shared/components/json-form/json-form-component.models'; 59 import { JsonFormComponentData } from '@shared/components/json-form/json-form-component.models';
  60 +import { WidgetActionsData } from './action/manage-widget-actions.component.models';
  61 +import { Dashboard } from '@shared/models/dashboard.models';
62 62
63 const emptySettingsSchema = { 63 const emptySettingsSchema = {
64 type: 'object', 64 type: 'object',
@@ -106,6 +106,9 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont @@ -106,6 +106,9 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
106 @Input() 106 @Input()
107 functionsOnly: boolean; 107 functionsOnly: boolean;
108 108
  109 + @Input()
  110 + dashboardStates: Array<string>;
  111 +
109 @Input() disabled: boolean; 112 @Input() disabled: boolean;
110 113
111 widgetType: widgetType; 114 widgetType: widgetType;
@@ -117,34 +120,13 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont @@ -117,34 +120,13 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
117 widgetConfigCallbacks: WidgetConfigCallbacks = { 120 widgetConfigCallbacks: WidgetConfigCallbacks = {
118 createEntityAlias: this.createEntityAlias.bind(this), 121 createEntityAlias: this.createEntityAlias.bind(this),
119 generateDataKey: this.generateDataKey.bind(this), 122 generateDataKey: this.generateDataKey.bind(this),
120 - fetchEntityKeys: this.fetchEntityKeys.bind(this) 123 + fetchEntityKeys: this.fetchEntityKeys.bind(this),
  124 + fetchDashboardStates: this.fetchDashboardStates.bind(this)
121 }; 125 };
122 126
123 widgetEditMode = this.utils.widgetEditMode; 127 widgetEditMode = this.utils.widgetEditMode;
124 128
125 selectedTab: number; 129 selectedTab: number;
126 - title: string;  
127 - showTitleIcon: boolean;  
128 - titleIcon: string;  
129 - iconColor: string;  
130 - iconSize: string;  
131 - showTitle: boolean;  
132 - dropShadow: boolean;  
133 - enableFullscreen: boolean;  
134 - backgroundColor: string;  
135 - color: string;  
136 - padding: string;  
137 - margin: string;  
138 - widgetStyle: string;  
139 - titleStyle: string;  
140 - units: string;  
141 - decimals: number;  
142 - showLegend: boolean;  
143 - legendConfig: LegendConfig;  
144 - actions: {[actionSourceId: string]: Array<WidgetActionDescriptor>};  
145 - alarmSource: Datasource;  
146 - mobileOrder: number;  
147 - mobileHeight: number;  
148 130
149 private modelValue: WidgetConfigComponentData; 131 private modelValue: WidgetConfigComponentData;
150 132
@@ -152,11 +134,19 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont @@ -152,11 +134,19 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
152 134
153 public dataSettings: FormGroup; 135 public dataSettings: FormGroup;
154 public targetDeviceSettings: FormGroup; 136 public targetDeviceSettings: FormGroup;
  137 + public alarmSourceSettings: FormGroup;
  138 + public widgetSettings: FormGroup;
  139 + public layoutSettings: FormGroup;
155 public advancedSettings: FormGroup; 140 public advancedSettings: FormGroup;
  141 + public actionsSettings: FormGroup;
156 142
157 private dataSettingsChangesSubscription: Subscription; 143 private dataSettingsChangesSubscription: Subscription;
158 private targetDeviceSettingsSubscription: Subscription; 144 private targetDeviceSettingsSubscription: Subscription;
  145 + private alarmSourceSettingsSubscription: Subscription;
  146 + private widgetSettingsSubscription: Subscription;
  147 + private layoutSettingsSubscription: Subscription;
159 private advancedSettingsSubscription: Subscription; 148 private advancedSettingsSubscription: Subscription;
  149 + private actionsSettingsSubscription: Subscription;
160 150
161 constructor(protected store: Store<AppState>, 151 constructor(protected store: Store<AppState>,
162 private utils: UtilsService, 152 private utils: UtilsService,
@@ -173,6 +163,47 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont @@ -173,6 +163,47 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
173 } else { 163 } else {
174 this.datasourceTypes = [DatasourceType.function, DatasourceType.entity]; 164 this.datasourceTypes = [DatasourceType.function, DatasourceType.entity];
175 } 165 }
  166 + this.widgetSettings = this.fb.group({
  167 + title: [null, []],
  168 + showTitleIcon: [null, []],
  169 + titleIcon: [null, []],
  170 + iconColor: [null, []],
  171 + iconSize: [null, []],
  172 + showTitle: [null, []],
  173 + dropShadow: [null, []],
  174 + enableFullscreen: [null, []],
  175 + backgroundColor: [null, []],
  176 + color: [null, []],
  177 + padding: [null, []],
  178 + margin: [null, []],
  179 + widgetStyle: [null, []],
  180 + titleStyle: [null, []],
  181 + units: [null, []],
  182 + decimals: [null, [Validators.min(0), Validators.max(15), Validators.pattern(/^\d*$/)]],
  183 + showLegend: [null, []],
  184 + legendConfig: [null, []]
  185 + });
  186 + this.widgetSettings.get('showTitleIcon').valueChanges.subscribe((value: boolean) => {
  187 + if (value) {
  188 + this.widgetSettings.get('titleIcon').enable({emitEvent: false});
  189 + } else {
  190 + this.widgetSettings.get('titleIcon').disable({emitEvent: false});
  191 + }
  192 + });
  193 + this.widgetSettings.get('showLegend').valueChanges.subscribe((value: boolean) => {
  194 + if (value) {
  195 + this.widgetSettings.get('legendConfig').enable({emitEvent: false});
  196 + } else {
  197 + this.widgetSettings.get('legendConfig').disable({emitEvent: false});
  198 + }
  199 + });
  200 + this.layoutSettings = this.fb.group({
  201 + mobileOrder: [null, [Validators.pattern(/^-?[0-9]+$/)]],
  202 + mobileHeight: [null, [Validators.min(1), Validators.max(10), Validators.pattern(/^\d*$/)]]
  203 + });
  204 + this.actionsSettings = this.fb.group({
  205 + actionsData: [null, []]
  206 + });
176 } 207 }
177 208
178 private removeChangeSubscriptions() { 209 private removeChangeSubscriptions() {
@@ -184,10 +215,26 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont @@ -184,10 +215,26 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
184 this.targetDeviceSettingsSubscription.unsubscribe(); 215 this.targetDeviceSettingsSubscription.unsubscribe();
185 this.targetDeviceSettingsSubscription = null; 216 this.targetDeviceSettingsSubscription = null;
186 } 217 }
  218 + if (this.alarmSourceSettingsSubscription) {
  219 + this.alarmSourceSettingsSubscription.unsubscribe();
  220 + this.alarmSourceSettingsSubscription = null;
  221 + }
  222 + if (this.widgetSettingsSubscription) {
  223 + this.widgetSettingsSubscription.unsubscribe();
  224 + this.widgetSettingsSubscription = null;
  225 + }
  226 + if (this.layoutSettingsSubscription) {
  227 + this.layoutSettingsSubscription.unsubscribe();
  228 + this.layoutSettingsSubscription = null;
  229 + }
187 if (this.advancedSettingsSubscription) { 230 if (this.advancedSettingsSubscription) {
188 this.advancedSettingsSubscription.unsubscribe(); 231 this.advancedSettingsSubscription.unsubscribe();
189 this.advancedSettingsSubscription = null; 232 this.advancedSettingsSubscription = null;
190 } 233 }
  234 + if (this.actionsSettingsSubscription) {
  235 + this.actionsSettingsSubscription.unsubscribe();
  236 + this.actionsSettingsSubscription = null;
  237 + }
191 } 238 }
192 239
193 private createChangeSubscriptions() { 240 private createChangeSubscriptions() {
@@ -197,14 +244,27 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont @@ -197,14 +244,27 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
197 this.targetDeviceSettingsSubscription = this.targetDeviceSettings.valueChanges.subscribe( 244 this.targetDeviceSettingsSubscription = this.targetDeviceSettings.valueChanges.subscribe(
198 () => this.updateTargetDeviceSettings() 245 () => this.updateTargetDeviceSettings()
199 ); 246 );
  247 + this.alarmSourceSettingsSubscription = this.alarmSourceSettings.valueChanges.subscribe(
  248 + () => this.updateAlarmSourceSettings()
  249 + );
  250 + this.widgetSettingsSubscription = this.widgetSettings.valueChanges.subscribe(
  251 + () => this.updateWidgetSettings()
  252 + );
  253 + this.layoutSettingsSubscription = this.layoutSettings.valueChanges.subscribe(
  254 + () => this.updateLayoutSettings()
  255 + );
200 this.advancedSettingsSubscription = this.advancedSettings.valueChanges.subscribe( 256 this.advancedSettingsSubscription = this.advancedSettings.valueChanges.subscribe(
201 () => this.updateAdvancedSettings() 257 () => this.updateAdvancedSettings()
202 ); 258 );
  259 + this.actionsSettingsSubscription = this.actionsSettings.valueChanges.subscribe(
  260 + () => this.updateActionSettings()
  261 + );
203 } 262 }
204 263
205 private buildForms() { 264 private buildForms() {
206 this.dataSettings = this.fb.group({}); 265 this.dataSettings = this.fb.group({});
207 this.targetDeviceSettings = this.fb.group({}); 266 this.targetDeviceSettings = this.fb.group({});
  267 + this.alarmSourceSettings = this.fb.group({});
208 this.advancedSettings = this.fb.group({}); 268 this.advancedSettings = this.fb.group({});
209 if (this.widgetType === widgetType.timeseries || this.widgetType === widgetType.alarm) { 269 if (this.widgetType === widgetType.timeseries || this.widgetType === widgetType.alarm) {
210 this.dataSettings.addControl('useDashboardTimewindow', this.fb.control(null)); 270 this.dataSettings.addControl('useDashboardTimewindow', this.fb.control(null));
@@ -235,6 +295,8 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont @@ -235,6 +295,8 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
235 this.targetDeviceSettings.addControl('targetDeviceAliasId', 295 this.targetDeviceSettings.addControl('targetDeviceAliasId',
236 this.fb.control(null, 296 this.fb.control(null,
237 this.widgetEditMode ? [] : [Validators.required])); 297 this.widgetEditMode ? [] : [Validators.required]));
  298 + } else if (this.widgetType === widgetType.alarm) {
  299 + this.alarmSourceSettings = this.buildDatasourceForm();
238 } 300 }
239 } 301 }
240 this.advancedSettings.addControl('settings', 302 this.advancedSettings.addControl('settings',
@@ -264,31 +326,54 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont @@ -264,31 +326,54 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
264 const layout = this.modelValue.layout; 326 const layout = this.modelValue.layout;
265 if (config) { 327 if (config) {
266 this.selectedTab = 0; 328 this.selectedTab = 0;
267 - this.title = config.title;  
268 - this.showTitleIcon = isDefined(config.showTitleIcon) ? config.showTitleIcon : false;  
269 - this.titleIcon = isDefined(config.titleIcon) ? config.titleIcon : '';  
270 - this.iconColor = isDefined(config.iconColor) ? config.iconColor : 'rgba(0, 0, 0, 0.87)';  
271 - this.iconSize = isDefined(config.iconSize) ? config.iconSize : '24px';  
272 - this.showTitle = config.showTitle;  
273 - this.dropShadow = isDefined(config.dropShadow) ? config.dropShadow : true;  
274 - this.enableFullscreen = isDefined(config.enableFullscreen) ? config.enableFullscreen : true;  
275 - this.backgroundColor = config.backgroundColor;  
276 - this.color = config.color;  
277 - this.padding = config.padding;  
278 - this.margin = config.margin;  
279 - this.widgetStyle =  
280 - JSON.stringify(isDefined(config.widgetStyle) ? config.widgetStyle : {}, null, 2);  
281 - this.titleStyle =  
282 - JSON.stringify(isDefined(config.titleStyle) ? config.titleStyle : {  
283 - fontSize: '16px',  
284 - fontWeight: 400  
285 - }, null, 2);  
286 - this.units = config.units;  
287 - this.decimals = config.decimals;  
288 - this.actions = config.actions;  
289 - if (!this.actions) {  
290 - this.actions = {}; 329 + this.widgetSettings.patchValue({
  330 + title: config.title,
  331 + showTitleIcon: isDefined(config.showTitleIcon) ? config.showTitleIcon : false,
  332 + titleIcon: isDefined(config.titleIcon) ? config.titleIcon : '',
  333 + iconColor: isDefined(config.iconColor) ? config.iconColor : 'rgba(0, 0, 0, 0.87)',
  334 + iconSize: isDefined(config.iconSize) ? config.iconSize : '24px',
  335 + showTitle: config.showTitle,
  336 + dropShadow: isDefined(config.dropShadow) ? config.dropShadow : true,
  337 + enableFullscreen: isDefined(config.enableFullscreen) ? config.enableFullscreen : true,
  338 + backgroundColor: config.backgroundColor,
  339 + color: config.color,
  340 + padding: config.padding,
  341 + margin: config.margin,
  342 + widgetStyle: isDefined(config.widgetStyle) ? config.widgetStyle : {},
  343 + titleStyle: isDefined(config.titleStyle) ? config.titleStyle : {
  344 + fontSize: '16px',
  345 + fontWeight: 400
  346 + },
  347 + units: config.units,
  348 + decimals: config.decimals,
  349 + showLegend: isDefined(config.showLegend) ? config.showLegend :
  350 + this.widgetType === widgetType.timeseries,
  351 + legendConfig: config.legendConfig || defaultLegendConfig(this.widgetType)
  352 + },
  353 + {emitEvent: false}
  354 + );
  355 + const showTitleIcon: boolean = this.widgetSettings.get('showTitleIcon').value;
  356 + if (showTitleIcon) {
  357 + this.widgetSettings.get('titleIcon').enable({emitEvent: false});
  358 + } else {
  359 + this.widgetSettings.get('titleIcon').disable({emitEvent: false});
  360 + }
  361 + const showLegend: boolean = this.widgetSettings.get('showLegend').value;
  362 + if (showLegend) {
  363 + this.widgetSettings.get('legendConfig').enable({emitEvent: false});
  364 + } else {
  365 + this.widgetSettings.get('legendConfig').disable({emitEvent: false});
291 } 366 }
  367 + const actionsData: WidgetActionsData = {
  368 + actionsMap: config.actions || {},
  369 + actionSources: this.modelValue.actionSources || {}
  370 + };
  371 + this.actionsSettings.patchValue(
  372 + {
  373 + actionsData
  374 + },
  375 + {emitEvent: false}
  376 + );
292 if (this.widgetType === widgetType.timeseries || this.widgetType === widgetType.alarm) { 377 if (this.widgetType === widgetType.timeseries || this.widgetType === widgetType.alarm) {
293 const useDashboardTimewindow = isDefined(config.useDashboardTimewindow) ? 378 const useDashboardTimewindow = isDefined(config.useDashboardTimewindow) ?
294 config.useDashboardTimewindow : true; 379 config.useDashboardTimewindow : true;
@@ -346,29 +431,42 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont @@ -346,29 +431,42 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
346 { alarmsPollingInterval: isDefined(config.alarmsPollingInterval) ? 431 { alarmsPollingInterval: isDefined(config.alarmsPollingInterval) ?
347 config.alarmsPollingInterval : 5}, {emitEvent: false} 432 config.alarmsPollingInterval : 5}, {emitEvent: false}
348 ); 433 );
349 - if (config.alarmSource) {  
350 - this.alarmSource = config.alarmSource;  
351 - } else {  
352 - this.alarmSource = null;  
353 - } 434 + this.alarmSourceSettings.patchValue(
  435 + config.alarmSource, {emitEvent: false}
  436 + );
  437 + const alarmSourceType: DatasourceType = this.alarmSourceSettings.get('type').value;
  438 + this.alarmSourceSettings.get('entityAliasId').setValidators(
  439 + alarmSourceType === DatasourceType.entity ? [Validators.required] : []
  440 + );
  441 + this.alarmSourceSettings.get('entityAliasId').updateValueAndValidity({emitEvent: false});
354 } 442 }
355 } 443 }
356 444
357 this.updateSchemaForm(config.settings); 445 this.updateSchemaForm(config.settings);
358 446
359 if (layout) { 447 if (layout) {
360 - this.mobileOrder = layout.mobileOrder;  
361 - this.mobileHeight = layout.mobileHeight; 448 + this.layoutSettings.patchValue(
  449 + {
  450 + mobileOrder: layout.mobileOrder,
  451 + mobileHeight: layout.mobileHeight
  452 + },
  453 + {emitEvent: false}
  454 + );
362 } else { 455 } else {
363 - this.mobileOrder = undefined;  
364 - this.mobileHeight = undefined; 456 + this.layoutSettings.patchValue(
  457 + {
  458 + mobileOrder: null,
  459 + mobileHeight: null
  460 + },
  461 + {emitEvent: false}
  462 + );
365 } 463 }
366 } 464 }
367 this.createChangeSubscriptions(); 465 this.createChangeSubscriptions();
368 } 466 }
369 } 467 }
370 468
371 - private buildDatasourceForm(datasource?: Datasource): AbstractControl { 469 + private buildDatasourceForm(datasource?: Datasource): FormGroup {
372 const dataKeysRequired = !this.modelValue.typeParameters || !this.modelValue.typeParameters.dataKeysOptional; 470 const dataKeysRequired = !this.modelValue.typeParameters || !this.modelValue.typeParameters.dataKeysOptional;
373 const datasourceFormGroup = this.fb.group( 471 const datasourceFormGroup = this.fb.group(
374 { 472 {
@@ -427,6 +525,34 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont @@ -427,6 +525,34 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
427 } 525 }
428 } 526 }
429 527
  528 + private updateAlarmSourceSettings() {
  529 + if (this.modelValue) {
  530 + if (this.modelValue.config) {
  531 + const alarmSource: Datasource = this.alarmSourceSettings.value;
  532 + this.modelValue.config.alarmSource = alarmSource;
  533 + }
  534 + this.propagateChange(this.modelValue);
  535 + }
  536 + }
  537 +
  538 + private updateWidgetSettings() {
  539 + if (this.modelValue) {
  540 + if (this.modelValue.config) {
  541 + Object.assign(this.modelValue.config, this.widgetSettings.value);
  542 + }
  543 + this.propagateChange(this.modelValue);
  544 + }
  545 + }
  546 +
  547 + private updateLayoutSettings() {
  548 + if (this.modelValue) {
  549 + if (this.modelValue.layout) {
  550 + Object.assign(this.modelValue.layout, this.layoutSettings.value);
  551 + }
  552 + this.propagateChange(this.modelValue);
  553 + }
  554 + }
  555 +
430 private updateAdvancedSettings() { 556 private updateAdvancedSettings() {
431 if (this.modelValue) { 557 if (this.modelValue) {
432 if (this.modelValue.config) { 558 if (this.modelValue.config) {
@@ -437,6 +563,16 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont @@ -437,6 +563,16 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
437 } 563 }
438 } 564 }
439 565
  566 + private updateActionSettings() {
  567 + if (this.modelValue) {
  568 + if (this.modelValue.config) {
  569 + const actions = (this.actionsSettings.get('actionsData').value as WidgetActionsData).actionsMap;
  570 + this.modelValue.config.actions = actions;
  571 + }
  572 + this.propagateChange(this.modelValue);
  573 + }
  574 + }
  575 +
440 public displayAdvanced(): boolean { 576 public displayAdvanced(): boolean {
441 return this.modelValue.settingsSchema && this.modelValue.settingsSchema.schema; 577 return this.modelValue.settingsSchema && this.modelValue.settingsSchema.schema;
442 } 578 }
@@ -596,6 +732,21 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont @@ -596,6 +732,21 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
596 ); 732 );
597 } 733 }
598 734
  735 + private fetchDashboardStates(query: string): Array<string> {
  736 + const stateIds = Object.keys(this.dashboardStates);
  737 + const result = query ? stateIds.filter(this.createFilterForDashboardState(query)) : stateIds;
  738 + if (result && result.length) {
  739 + return result;
  740 + } else {
  741 + return [query];
  742 + }
  743 + }
  744 +
  745 + private createFilterForDashboardState(query: string): (stateId: string) => boolean {
  746 + const lowercaseQuery = query.toLowerCase();
  747 + return stateId => stateId.toLowerCase().indexOf(lowercaseQuery) === 0;
  748 + }
  749 +
599 public validate(c: FormControl) { 750 public validate(c: FormControl) {
600 if (!this.dataSettings.valid) { 751 if (!this.dataSettings.valid) {
601 return { 752 return {
@@ -603,6 +754,18 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont @@ -603,6 +754,18 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
603 valid: false 754 valid: false
604 } 755 }
605 }; 756 };
  757 + } else if (!this.widgetSettings.valid) {
  758 + return {
  759 + widgetSettings: {
  760 + valid: false
  761 + }
  762 + };
  763 + } else if (!this.layoutSettings.valid) {
  764 + return {
  765 + widgetSettings: {
  766 + valid: false
  767 + }
  768 + };
606 } else if (!this.advancedSettings.valid) { 769 } else if (!this.advancedSettings.valid) {
607 return { 770 return {
608 advancedSettings: { 771 advancedSettings: {
@@ -636,24 +799,6 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont @@ -636,24 +799,6 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
636 }; 799 };
637 } 800 }
638 } 801 }
639 - try {  
640 - JSON.parse(this.widgetStyle);  
641 - } catch (e) {  
642 - return {  
643 - widgetStyle: {  
644 - valid: false  
645 - }  
646 - };  
647 - }  
648 - try {  
649 - JSON.parse(this.titleStyle);  
650 - } catch (e) {  
651 - return {  
652 - titleStyle: {  
653 - valid: false  
654 - }  
655 - };  
656 - }  
657 } 802 }
658 return null; 803 return null;
659 } 804 }
@@ -38,6 +38,7 @@ import { @@ -38,6 +38,7 @@ import {
38 Datasource, 38 Datasource,
39 LegendConfig, 39 LegendConfig,
40 LegendData, 40 LegendData,
  41 + LegendDirection,
41 LegendPosition, 42 LegendPosition,
42 Widget, 43 Widget,
43 WidgetActionDescriptor, 44 WidgetActionDescriptor,
@@ -45,7 +46,8 @@ import { @@ -45,7 +46,8 @@ import {
45 WidgetActionType, 46 WidgetActionType,
46 WidgetResource, 47 WidgetResource,
47 widgetType, 48 widgetType,
48 - WidgetTypeParameters 49 + WidgetTypeParameters,
  50 + defaultLegendConfig
49 } from '@shared/models/widget.models'; 51 } from '@shared/models/widget.models';
50 import { PageComponent } from '@shared/components/page.component'; 52 import { PageComponent } from '@shared/components/page.component';
51 import { Store } from '@ngrx/store'; 53 import { Store } from '@ngrx/store';
@@ -165,6 +167,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI @@ -165,6 +167,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
165 private ngZone: NgZone, 167 private ngZone: NgZone,
166 private cd: ChangeDetectorRef) { 168 private cd: ChangeDetectorRef) {
167 super(store); 169 super(store);
  170 + this.cssParser.testMode = false;
168 } 171 }
169 172
170 ngOnInit(): void { 173 ngOnInit(): void {
@@ -179,14 +182,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI @@ -179,14 +182,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
179 this.legendContainerLayoutType = 'column'; 182 this.legendContainerLayoutType = 'column';
180 183
181 if (this.displayLegend) { 184 if (this.displayLegend) {
182 - this.legendConfig = this.widget.config.legendConfig ||  
183 - {  
184 - position: LegendPosition.bottom,  
185 - showMin: false,  
186 - showMax: false,  
187 - showAvg: this.widget.type === widgetType.timeseries,  
188 - showTotal: false  
189 - }; 185 + this.legendConfig = this.widget.config.legendConfig || defaultLegendConfig(this.widget.type);
190 this.legendData = { 186 this.legendData = {
191 keys: [], 187 keys: [],
192 data: [] 188 data: []
@@ -194,8 +190,10 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI @@ -194,8 +190,10 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
194 if (this.legendConfig.position === LegendPosition.top || 190 if (this.legendConfig.position === LegendPosition.top ||
195 this.legendConfig.position === LegendPosition.bottom) { 191 this.legendConfig.position === LegendPosition.bottom) {
196 this.legendContainerLayoutType = 'column'; 192 this.legendContainerLayoutType = 'column';
  193 + this.isLegendFirst = this.legendConfig.position === LegendPosition.top;
197 } else { 194 } else {
198 this.legendContainerLayoutType = 'row'; 195 this.legendContainerLayoutType = 'row';
  196 + this.isLegendFirst = this.legendConfig.position === LegendPosition.left;
199 } 197 }
200 switch (this.legendConfig.position) { 198 switch (this.legendConfig.position) {
201 case LegendPosition.top: 199 case LegendPosition.top:
@@ -352,7 +350,9 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI @@ -352,7 +350,9 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
352 this.loadFromWidgetInfo(); 350 this.loadFromWidgetInfo();
353 } 351 }
354 ); 352 );
355 - 353 + setTimeout(() => {
  354 + this.dashboardWidget.updateWidgetParams();
  355 + }, 0);
356 } 356 }
357 357
358 ngAfterViewInit(): void { 358 ngAfterViewInit(): void {
@@ -764,6 +764,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI @@ -764,6 +764,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
764 timeWindowUpdated: (subscription, timeWindowConfig) => { 764 timeWindowUpdated: (subscription, timeWindowConfig) => {
765 this.ngZone.run(() => { 765 this.ngZone.run(() => {
766 this.widget.config.timewindow = timeWindowConfig; 766 this.widget.config.timewindow = timeWindowConfig;
  767 + this.cd.detectChanges();
767 }); 768 });
768 } 769 }
769 }; 770 };
@@ -924,24 +925,16 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI @@ -924,24 +925,16 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
924 if (targetDashboardStateId) { 925 if (targetDashboardStateId) {
925 stateObject.id = targetDashboardStateId; 926 stateObject.id = targetDashboardStateId;
926 } 927 }
927 - const stateParams = {  
928 - dashboardId: targetDashboardId,  
929 - state: objToBase64([ stateObject ])  
930 - };  
931 const state = objToBase64([ stateObject ]); 928 const state = objToBase64([ stateObject ]);
932 - const currentUrl = this.route.snapshot.url; 929 + const isSinglePage = this.route.snapshot.data.singlePageMode;
933 let url; 930 let url;
934 - if (currentUrl.length > 1) {  
935 - if (currentUrl[currentUrl.length - 2].path === 'dashboard') {  
936 - url = `/dashboard/${targetDashboardId}?state=${state}`;  
937 - } else {  
938 - url = `/dashboards/${targetDashboardId}?state=${state}`;  
939 - }  
940 - }  
941 - if (url) {  
942 - const urlTree = this.router.parseUrl(url);  
943 - this.router.navigateByUrl(url); 931 + if (isSinglePage) {
  932 + url = `/dashboard/${targetDashboardId}?state=${state}`;
  933 + } else {
  934 + url = `/dashboards/${targetDashboardId}?state=${state}`;
944 } 935 }
  936 + const urlTree = this.router.parseUrl(url);
  937 + this.router.navigateByUrl(url);
945 break; 938 break;
946 case WidgetActionType.custom: 939 case WidgetActionType.custom:
947 const customFunction = descriptor.customFunction; 940 const customFunction = descriptor.customFunction;
@@ -78,6 +78,7 @@ export interface IDashboardComponent { @@ -78,6 +78,7 @@ export interface IDashboardComponent {
78 selectWidget(index: number, delay?: number); 78 selectWidget(index: number, delay?: number);
79 getSelectedWidget(): Widget; 79 getSelectedWidget(): Widget;
80 getEventGridPosition(event: Event): WidgetPosition; 80 getEventGridPosition(event: Event): WidgetPosition;
  81 + notifyGridsterOptionsChanged();
81 } 82 }
82 83
83 declare type DashboardWidgetUpdateOperation = 'add' | 'remove' | 'update'; 84 declare type DashboardWidgetUpdateOperation = 'add' | 'remove' | 'update';
@@ -185,7 +186,7 @@ export class DashboardWidgets implements Iterable<DashboardWidget> { @@ -185,7 +186,7 @@ export class DashboardWidgets implements Iterable<DashboardWidget> {
185 186
186 highlightWidget(index: number): DashboardWidget { 187 highlightWidget(index: number): DashboardWidget {
187 const widget = this.findWidgetAtIndex(index); 188 const widget = this.findWidgetAtIndex(index);
188 - if (widget && (!this.highlightedMode || !widget.highlighted)) { 189 + if (widget && (!this.highlightedMode || !widget.highlighted || this.highlightedMode && widget.highlighted)) {
189 this.highlightedMode = true; 190 this.highlightedMode = true;
190 widget.highlighted = true; 191 widget.highlighted = true;
191 this.dashboardWidgets.forEach((dashboardWidget) => { 192 this.dashboardWidgets.forEach((dashboardWidget) => {
@@ -248,6 +249,7 @@ export class DashboardWidgets implements Iterable<DashboardWidget> { @@ -248,6 +249,7 @@ export class DashboardWidgets implements Iterable<DashboardWidget> {
248 }); 249 });
249 this.sortWidgets(); 250 this.sortWidgets();
250 this.dashboard.gridsterOpts.maxRows = maxRows; 251 this.dashboard.gridsterOpts.maxRows = maxRows;
  252 + this.dashboard.notifyGridsterOptionsChanged();
251 } 253 }
252 254
253 sortWidgets() { 255 sortWidgets() {
@@ -68,7 +68,6 @@ export interface WidgetContext { @@ -68,7 +68,6 @@ export interface WidgetContext {
68 width?: number; 68 width?: number;
69 height?: number; 69 height?: number;
70 $scope?: IDynamicWidgetComponent; 70 $scope?: IDynamicWidgetComponent;
71 - hideTitlePanel?: boolean;  
72 isEdit?: boolean; 71 isEdit?: boolean;
73 isMobile?: boolean; 72 isMobile?: boolean;
74 dashboard?: IDashboardComponent; 73 dashboard?: IDashboardComponent;
@@ -87,15 +86,18 @@ export interface WidgetContext { @@ -87,15 +86,18 @@ export interface WidgetContext {
87 stateController?: IStateController; 86 stateController?: IStateController;
88 aliasController?: IAliasController; 87 aliasController?: IAliasController;
89 activeEntityInfo?: SubscriptionEntityInfo; 88 activeEntityInfo?: SubscriptionEntityInfo;
90 - widgetTitleTemplate?: string;  
91 - widgetTitle?: string;  
92 - customHeaderActions?: Array<WidgetHeaderAction>;  
93 - widgetActions?: Array<WidgetAction>;  
94 89
95 datasources?: Array<Datasource>; 90 datasources?: Array<Datasource>;
96 data?: Array<DatasourceData>; 91 data?: Array<DatasourceData>;
97 hiddenData?: Array<{data: DataSet}>; 92 hiddenData?: Array<{data: DataSet}>;
98 timeWindow?: WidgetTimewindow; 93 timeWindow?: WidgetTimewindow;
  94 +
  95 + hideTitlePanel?: boolean;
  96 + widgetTitleTemplate?: string;
  97 + widgetTitle?: string;
  98 + customHeaderActions?: Array<WidgetHeaderAction>;
  99 + widgetActions?: Array<WidgetAction>;
  100 +
99 } 101 }
100 102
101 export interface IDynamicWidgetComponent { 103 export interface IDynamicWidgetComponent {
@@ -122,7 +124,7 @@ export interface WidgetConfigComponentData { @@ -122,7 +124,7 @@ export interface WidgetConfigComponentData {
122 layout: WidgetLayout; 124 layout: WidgetLayout;
123 widgetType: widgetType; 125 widgetType: widgetType;
124 typeParameters: WidgetTypeParameters; 126 typeParameters: WidgetTypeParameters;
125 - actionSources: {[key: string]: WidgetActionSource}; 127 + actionSources: {[actionSourceId: string]: WidgetActionSource};
126 isDataEnabled: boolean; 128 isDataEnabled: boolean;
127 settingsSchema: any; 129 settingsSchema: any;
128 dataKeySettingsSchema: any; 130 dataKeySettingsSchema: any;
@@ -178,7 +180,7 @@ export interface WidgetTypeInstance { @@ -178,7 +180,7 @@ export interface WidgetTypeInstance {
178 getDataKeySettingsSchema?: () => string; 180 getDataKeySettingsSchema?: () => string;
179 typeParameters?: () => WidgetTypeParameters; 181 typeParameters?: () => WidgetTypeParameters;
180 useCustomDatasources?: () => boolean; 182 useCustomDatasources?: () => boolean;
181 - actionSources?: () => {[key: string]: WidgetActionSource}; 183 + actionSources?: () => {[actionSourceId: string]: WidgetActionSource};
182 184
183 onInit?: () => void; 185 onInit?: () => void;
184 onDataUpdated?: () => void; 186 onDataUpdated?: () => void;
@@ -14,7 +14,16 @@ @@ -14,7 +14,16 @@
14 /// limitations under the License. 14 /// limitations under the License.
15 /// 15 ///
16 16
17 -import { Component, Inject, OnDestroy, OnInit, ViewEncapsulation, ViewChild, NgZone } from '@angular/core'; 17 +import {
  18 + Component,
  19 + Inject,
  20 + OnDestroy,
  21 + OnInit,
  22 + ViewEncapsulation,
  23 + ViewChild,
  24 + NgZone,
  25 + ChangeDetectorRef, ChangeDetectionStrategy, ApplicationRef
  26 +} from '@angular/core';
18 import { PageComponent } from '@shared/components/page.component'; 27 import { PageComponent } from '@shared/components/page.component';
19 import { Store } from '@ngrx/store'; 28 import { Store } from '@ngrx/store';
20 import { AppState } from '@core/core.state'; 29 import { AppState } from '@core/core.state';
@@ -77,7 +86,8 @@ import { EditWidgetComponent } from '@home/pages/dashboard/edit-widget.component @@ -77,7 +86,8 @@ import { EditWidgetComponent } from '@home/pages/dashboard/edit-widget.component
77 selector: 'tb-dashboard-page', 86 selector: 'tb-dashboard-page',
78 templateUrl: './dashboard-page.component.html', 87 templateUrl: './dashboard-page.component.html',
79 styleUrls: ['./dashboard-page.component.scss'], 88 styleUrls: ['./dashboard-page.component.scss'],
80 - encapsulation: ViewEncapsulation.None 89 + encapsulation: ViewEncapsulation.None,
  90 + // changeDetection: ChangeDetectionStrategy.OnPush
81 }) 91 })
82 export class DashboardPageComponent extends PageComponent implements IDashboardController, OnDestroy { 92 export class DashboardPageComponent extends PageComponent implements IDashboardController, OnDestroy {
83 93
@@ -149,7 +159,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC @@ -149,7 +159,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
149 dashboardTimewindow: null, 159 dashboardTimewindow: null,
150 state: null, 160 state: null,
151 stateController: null, 161 stateController: null,
152 - aliasController: null 162 + aliasController: null,
  163 + runChangeDetection: this.runChangeDetection.bind(this)
153 }; 164 };
154 165
155 addWidgetFabButtons: FooterFabButtons = { 166 addWidgetFabButtons: FooterFabButtons = {
@@ -204,12 +215,15 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC @@ -204,12 +215,15 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
204 private dashboardService: DashboardService, 215 private dashboardService: DashboardService,
205 private itembuffer: ItemBufferService, 216 private itembuffer: ItemBufferService,
206 private fb: FormBuilder, 217 private fb: FormBuilder,
207 - private dialog: MatDialog) { 218 + private dialog: MatDialog,
  219 + private ngZone: NgZone,
  220 + private cd: ChangeDetectorRef) {
208 super(store); 221 super(store);
209 222
210 this.rxSubscriptions.push(this.route.data.subscribe( 223 this.rxSubscriptions.push(this.route.data.subscribe(
211 (data) => { 224 (data) => {
212 this.init(data); 225 this.init(data);
  226 + this.runChangeDetection();
213 } 227 }
214 )); 228 ));
215 229
@@ -294,6 +308,12 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC @@ -294,6 +308,12 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
294 this.rxSubscriptions.length = 0; 308 this.rxSubscriptions.length = 0;
295 } 309 }
296 310
  311 + public runChangeDetection() {
  312 + /*setTimeout(() => {
  313 + this.cd.detectChanges();
  314 + });*/
  315 + }
  316 +
297 public openToolbar() { 317 public openToolbar() {
298 this.isToolbarOpenedAnimate = true; 318 this.isToolbarOpenedAnimate = true;
299 this.isToolbarOpened = true; 319 this.isToolbarOpened = true;
@@ -646,7 +666,9 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC @@ -646,7 +666,9 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
646 this.editingWidgetLayoutOriginal = widgetLayout; 666 this.editingWidgetLayoutOriginal = widgetLayout;
647 this.editingLayoutCtx.widgets[index] = widget; 667 this.editingLayoutCtx.widgets[index] = widget;
648 this.editingLayoutCtx.widgetLayouts[widget.id] = widgetLayout; 668 this.editingLayoutCtx.widgetLayouts[widget.id] = widgetLayout;
649 - this.editingLayoutCtx.ctrl.highlightWidget(index, 0); 669 + setTimeout(() => {
  670 + this.editingLayoutCtx.ctrl.highlightWidget(index, 0);
  671 + }, 0);
650 } 672 }
651 673
652 onEditWidgetClosed() { 674 onEditWidgetClosed() {
@@ -25,6 +25,7 @@ import { @@ -25,6 +25,7 @@ import {
25 WidgetPosition 25 WidgetPosition
26 } from '@home/models/dashboard-component.models'; 26 } from '@home/models/dashboard-component.models';
27 import { Observable } from 'rxjs'; 27 import { Observable } from 'rxjs';
  28 +import { ChangeDetectorRef } from '@angular/core';
28 29
29 export declare type DashboardPageScope = 'tenant' | 'customer'; 30 export declare type DashboardPageScope = 'tenant' | 'customer';
30 31
@@ -34,6 +35,7 @@ export interface DashboardContext { @@ -34,6 +35,7 @@ export interface DashboardContext {
34 dashboardTimewindow: Timewindow; 35 dashboardTimewindow: Timewindow;
35 aliasController: IAliasController; 36 aliasController: IAliasController;
36 stateController: IStateController; 37 stateController: IStateController;
  38 + runChangeDetection: () => void;
37 } 39 }
38 40
39 export interface IDashboardController { 41 export interface IDashboardController {
@@ -21,6 +21,7 @@ @@ -21,6 +21,7 @@
21 [aliasController]="aliasController" 21 [aliasController]="aliasController"
22 [functionsOnly]="widgetEditMode" 22 [functionsOnly]="widgetEditMode"
23 [entityAliases]="dashboard.configuration.entityAliases" 23 [entityAliases]="dashboard.configuration.entityAliases"
  24 + [dashboardStates]="dashboard.configuration.states"
24 formControlName="widgetConfig"> 25 formControlName="widgetConfig">
25 </tb-widget-config> 26 </tb-widget-config>
26 </fieldset> 27 </fieldset>
@@ -14,7 +14,7 @@ @@ -14,7 +14,7 @@
14 /// limitations under the License. 14 /// limitations under the License.
15 /// 15 ///
16 16
17 -import { Component, OnInit, Input, OnChanges, SimpleChanges, ViewChild } from '@angular/core'; 17 +import { Component, OnInit, Input, OnChanges, SimpleChanges, ViewChild, ChangeDetectionStrategy } from '@angular/core';
18 import { PageComponent } from '@shared/components/page.component'; 18 import { PageComponent } from '@shared/components/page.component';
19 import { Store } from '@ngrx/store'; 19 import { Store } from '@ngrx/store';
20 import { AppState } from '@core/core.state'; 20 import { AppState } from '@core/core.state';
@@ -85,8 +85,8 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo @@ -85,8 +85,8 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo
85 this.rxSubscriptions.push(this.dashboard.dashboardTimewindowChanged.subscribe( 85 this.rxSubscriptions.push(this.dashboard.dashboardTimewindowChanged.subscribe(
86 (dashboardTimewindow) => { 86 (dashboardTimewindow) => {
87 this.dashboardCtx.dashboardTimewindow = dashboardTimewindow; 87 this.dashboardCtx.dashboardTimewindow = dashboardTimewindow;
88 - }  
89 - ) 88 + this.dashboardCtx.runChangeDetection();
  89 + })
90 ); 90 );
91 this.initHotKeys(); 91 this.initHotKeys();
92 } 92 }
@@ -90,7 +90,7 @@ export class DefaultStateControllerComponent extends StateControllerComponent im @@ -90,7 +90,7 @@ export class DefaultStateControllerComponent extends StateControllerComponent im
90 90
91 protected onStateChanged() { 91 protected onStateChanged() {
92 this.stateObject = this.parseState(this.currentState); 92 this.stateObject = this.parseState(this.currentState);
93 - this.gotoState(this.stateObject[0].id, true); 93 + this.gotoState(this.stateObject[0].id, false);
94 } 94 }
95 95
96 protected stateControllerId(): string { 96 protected stateControllerId(): string {
@@ -95,7 +95,7 @@ export class EntityStateControllerComponent extends StateControllerComponent imp @@ -95,7 +95,7 @@ export class EntityStateControllerComponent extends StateControllerComponent imp
95 protected onStateChanged() { 95 protected onStateChanged() {
96 this.stateObject = this.parseState(this.currentState); 96 this.stateObject = this.parseState(this.currentState);
97 this.selectedStateIndex = this.stateObject.length - 1; 97 this.selectedStateIndex = this.stateObject.length - 1;
98 - this.gotoState(this.stateObject[this.stateObject.length - 1].id, true); 98 + this.gotoState(this.stateObject[this.stateObject.length - 1].id, false);
99 } 99 }
100 100
101 protected stateControllerId(): string { 101 protected stateControllerId(): string {
@@ -120,7 +120,7 @@ export class DashboardAutocompleteComponent implements ControlValueAccessor, OnI @@ -120,7 +120,7 @@ export class DashboardAutocompleteComponent implements ControlValueAccessor, OnI
120 } 120 }
121 121
122 ngAfterViewInit(): void { 122 ngAfterViewInit(): void {
123 - this.selectFirstDashboardIfNeeded(); 123 + // this.selectFirstDashboardIfNeeded();
124 } 124 }
125 125
126 selectFirstDashboardIfNeeded(): void { 126 selectFirstDashboardIfNeeded(): void {
@@ -159,6 +159,7 @@ export class DashboardAutocompleteComponent implements ControlValueAccessor, OnI @@ -159,6 +159,7 @@ export class DashboardAutocompleteComponent implements ControlValueAccessor, OnI
159 } else { 159 } else {
160 this.modelValue = null; 160 this.modelValue = null;
161 this.selectDashboardFormGroup.get('dashboard').patchValue(null, {emitEvent: true}); 161 this.selectDashboardFormGroup.get('dashboard').patchValue(null, {emitEvent: true});
  162 + this.selectFirstDashboardIfNeeded();
162 } 163 }
163 } 164 }
164 165
  1 +<!--
  2 +
  3 + Copyright © 2016-2019 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<form class="tb-material-icons-dialog" style="min-width: 600px;">
  19 + <mat-toolbar fxLayout="row" color="primary">
  20 + <h2>{{ 'icon.select-icon' | translate }}</h2>
  21 + <span fxFlex></span>
  22 + <section fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
  23 + <mat-slide-toggle [formControl]="showAllControl">
  24 + </mat-slide-toggle>
  25 + <label translate>icon.show-all</label>
  26 + </section>
  27 + <button mat-button mat-icon-button
  28 + (click)="cancel()"
  29 + type="button">
  30 + <mat-icon class="material-icons">close</mat-icon>
  31 + </button>
  32 + </mat-toolbar>
  33 + <mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
  34 + </mat-progress-bar>
  35 + <div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
  36 + <div class="tb-absolute-fill tb-icons-load" *ngIf="loadingIcons$ | async" fxLayout="column" fxLayoutAlign="center center">
  37 + <mat-spinner color="accent" mode="indeterminate" diameter="40"></mat-spinner>
  38 + </div>
  39 + <div mat-dialog-content>
  40 + <div class="mat-content mat-padding" fxLayout="column">
  41 + <fieldset [disabled]="(isLoading$ | async)">
  42 + <ng-template ngFor let-icon [ngForOf]="icons$ | async" let-last="last">
  43 + <ng-container #iconButtons>
  44 + <button *ngIf="icon === selectedIcon"
  45 + class="tb-select-icon-button"
  46 + mat-button mat-icon-button
  47 + mat-raised-button
  48 + color="primary"
  49 + (click)="selectIcon(icon)"
  50 + matTooltip="{{ icon }}"
  51 + matTooltipPosition="above"
  52 + type="button">
  53 + <mat-icon>{{icon}}</mat-icon>
  54 + </button>
  55 + <button *ngIf="icon !== selectedIcon"
  56 + class="tb-select-icon-button"
  57 + mat-button mat-icon-button
  58 + (click)="selectIcon(icon)"
  59 + matTooltip="{{ icon }}"
  60 + matTooltipPosition="above"
  61 + type="button">
  62 + <mat-icon>{{icon}}</mat-icon>
  63 + </button>
  64 + </ng-container>
  65 + </ng-template>
  66 + </fieldset>
  67 + </div>
  68 + </div>
  69 + <div mat-dialog-actions fxLayout="row">
  70 + <span fxFlex></span>
  71 + <button mat-button
  72 + type="button"
  73 + [disabled]="(isLoading$ | async)"
  74 + (click)="cancel()">
  75 + {{ 'action.cancel' | translate }}
  76 + </button>
  77 + </div>
  78 +</form>
  1 +/**
  2 + * Copyright © 2016-2019 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +:host {
  17 + .tb-material-icons-dialog {
  18 + position: relative;
  19 + }
  20 + .tb-icons-load {
  21 + top: 64px;
  22 + z-index: 3;
  23 + background: rgba(255, 255, 255, .75);
  24 + }
  25 +}
  26 +
  27 +:host ::ng-deep {
  28 + .tb-material-icons-dialog {
  29 + button.mat-icon-button.tb-select-icon-button {
  30 + width: 56px;
  31 + height: 56px;
  32 + padding: 16px;
  33 + margin: 10px;
  34 + border: solid 1px #ffa500;
  35 + border-radius: 0%;
  36 + line-height: 0;
  37 + }
  38 + }
  39 +}
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { Component, Inject, OnInit, QueryList, ViewChildren, TemplateRef, AfterViewInit } from '@angular/core';
  18 +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material';
  19 +import { Store } from '@ngrx/store';
  20 +import { AppState } from '@core/core.state';
  21 +import { Router } from '@angular/router';
  22 +import { DialogComponent } from '@app/shared/components/dialog.component';
  23 +import { UtilsService } from '@core/services/utils.service';
  24 +import { FormControl } from '@angular/forms';
  25 +import { Observable, of, merge, noop } from 'rxjs';
  26 +import { delay, map, mergeMap, share, startWith, tap, mapTo } from 'rxjs/operators';
  27 +import { DashboardInfo } from '@shared/models/dashboard.models';
  28 +import { MatTab } from '@angular/material/tabs';
  29 +
  30 +export interface MaterialIconsDialogData {
  31 + icon: string;
  32 +}
  33 +
  34 +@Component({
  35 + selector: 'tb-material-icons-dialog',
  36 + templateUrl: './material-icons-dialog.component.html',
  37 + providers: [],
  38 + styleUrls: ['./material-icons-dialog.component.scss']
  39 +})
  40 +export class MaterialIconsDialogComponent extends DialogComponent<MaterialIconsDialogComponent, string>
  41 + implements OnInit, AfterViewInit {
  42 +
  43 + @ViewChildren('iconButtons') iconButtons: QueryList<HTMLElement>;
  44 +
  45 + selectedIcon: string;
  46 + icons$: Observable<Array<string>>;
  47 + loadingIcons$: Observable<boolean>;
  48 +
  49 + showAllControl: FormControl;
  50 +
  51 + constructor(protected store: Store<AppState>,
  52 + protected router: Router,
  53 + @Inject(MAT_DIALOG_DATA) public data: MaterialIconsDialogData,
  54 + private utils: UtilsService,
  55 + public dialogRef: MatDialogRef<MaterialIconsDialogComponent, string>) {
  56 + super(store, router, dialogRef);
  57 + this.selectedIcon = data.icon;
  58 + this.showAllControl = new FormControl(false);
  59 + }
  60 +
  61 + ngOnInit(): void {
  62 + this.icons$ = this.showAllControl.valueChanges.pipe(
  63 + map((showAll) => {
  64 + return {firstTime: false, showAll};
  65 + }),
  66 + startWith<{firstTime: boolean, showAll: boolean}>({firstTime: true, showAll: false}),
  67 + mergeMap((data) => {
  68 + if (data.showAll) {
  69 + return this.utils.getMaterialIcons().pipe(delay(100));
  70 + } else {
  71 + const res = of(this.utils.getCommonMaterialIcons());
  72 + return data.firstTime ? res : res.pipe(delay(50));
  73 + }
  74 + }),
  75 + share()
  76 + );
  77 + }
  78 +
  79 + ngAfterViewInit(): void {
  80 + this.loadingIcons$ = merge(
  81 + this.showAllControl.valueChanges.pipe(
  82 + mapTo(true),
  83 + ),
  84 + this.iconButtons.changes.pipe(
  85 + delay(100),
  86 + mapTo( false),
  87 + )
  88 + ).pipe(
  89 + tap((loadingIcons) => {
  90 + if (loadingIcons) {
  91 + this.showAllControl.disable({emitEvent: false});
  92 + } else {
  93 + this.showAllControl.enable({emitEvent: false});
  94 + }
  95 + }),
  96 + share()
  97 + );
  98 + }
  99 +
  100 + selectIcon(icon: string) {
  101 + this.dialogRef.close(icon);
  102 + }
  103 +
  104 + cancel(): void {
  105 + this.dialogRef.close(null);
  106 + }
  107 +
  108 +}
@@ -14,7 +14,7 @@ @@ -14,7 +14,7 @@
14 /// limitations under the License. 14 /// limitations under the License.
15 /// 15 ///
16 16
17 -import { Component, Input, HostListener } from '@angular/core'; 17 +import { Component, HostListener, Input } from '@angular/core';
18 import { PageComponent } from '@shared/components/page.component'; 18 import { PageComponent } from '@shared/components/page.component';
19 import { Store } from '@ngrx/store'; 19 import { Store } from '@ngrx/store';
20 import { AppState } from '@core/core.state'; 20 import { AppState } from '@core/core.state';
@@ -18,7 +18,7 @@ import { @@ -18,7 +18,7 @@ import {
18 Directive, 18 Directive,
19 ElementRef, 19 ElementRef,
20 EventEmitter, 20 EventEmitter,
21 - Input, OnChanges, 21 + Input, OnChanges, OnDestroy,
22 Output, SimpleChanges, 22 Output, SimpleChanges,
23 ViewContainerRef 23 ViewContainerRef
24 } from '@angular/core'; 24 } from '@angular/core';
@@ -29,7 +29,7 @@ import { TbAnchorComponent } from '@shared/components/tb-anchor.component'; @@ -29,7 +29,7 @@ import { TbAnchorComponent } from '@shared/components/tb-anchor.component';
29 @Directive({ 29 @Directive({
30 selector: '[tb-fullscreen]' 30 selector: '[tb-fullscreen]'
31 }) 31 })
32 -export class FullscreenDirective implements OnChanges { 32 +export class FullscreenDirective implements OnChanges, OnDestroy {
33 33
34 fullscreenValue = false; 34 fullscreenValue = false;
35 35
@@ -69,6 +69,12 @@ export class FullscreenDirective implements OnChanges { @@ -69,6 +69,12 @@ export class FullscreenDirective implements OnChanges {
69 } 69 }
70 } 70 }
71 71
  72 + ngOnDestroy(): void {
  73 + if (this.fullscreen) {
  74 + this.exitFullscreen();
  75 + }
  76 + }
  77 +
72 enterFullscreen() { 78 enterFullscreen() {
73 const targetElement: HTMLElement = this.fullscreenElement || this.elementRef.nativeElement; 79 const targetElement: HTMLElement = this.fullscreenElement || this.elementRef.nativeElement;
74 this.parentElement = targetElement.parentElement; 80 this.parentElement = targetElement.parentElement;
@@ -17,7 +17,7 @@ @@ -17,7 +17,7 @@
17 --> 17 -->
18 <div class="tb-js-func" style="background: #fff;" [ngClass]="{'tb-disabled': disabled, 'fill-height': fillHeight}" 18 <div class="tb-js-func" style="background: #fff;" [ngClass]="{'tb-disabled': disabled, 'fill-height': fillHeight}"
19 tb-fullscreen 19 tb-fullscreen
20 - [fullscreen]="fullscreen" (fullscreenChanged)="onFullscreen()" fxLayout="column"> 20 + [fullscreen]="fullscreen" fxLayout="column">
21 <div fxLayout="row" fxLayoutAlign="start center" style="height: 40px;" class="tb-js-func-toolbar"> 21 <div fxLayout="row" fxLayoutAlign="start center" style="height: 40px;" class="tb-js-func-toolbar">
22 <label class="tb-title no-padding">{{'function ' + (functionName ? functionName : '') + '(' + functionArgsString + ') {'}}</label> 22 <label class="tb-title no-padding">{{'function ' + (functionName ? functionName : '') + '(' + functionArgsString + ') {'}}</label>
23 <span fxFlex></span> 23 <span fxFlex></span>
@@ -25,6 +25,7 @@ @@ -25,6 +25,7 @@
25 {{'js-func.tidy' | translate }} 25 {{'js-func.tidy' | translate }}
26 </button> 26 </button>
27 <button type='button' mat-button mat-icon-button (click)="fullscreen = !fullscreen" 27 <button type='button' mat-button mat-icon-button (click)="fullscreen = !fullscreen"
  28 + class="tb-mat-32"
28 matTooltip="{{(fullscreen ? 'fullscreen.exit' : 'fullscreen.expand') | translate}}" 29 matTooltip="{{(fullscreen ? 'fullscreen.exit' : 'fullscreen.expand') | translate}}"
29 matTooltipPosition="above"> 30 matTooltipPosition="above">
30 <mat-icon class="material-icons">{{ fullscreen ? 'fullscreen_exit' : 'fullscreen' }}</mat-icon> 31 <mat-icon class="material-icons">{{ fullscreen ? 'fullscreen_exit' : 'fullscreen' }}</mat-icon>
@@ -16,11 +16,11 @@ @@ -16,11 +16,11 @@
16 .tb-js-func { 16 .tb-js-func {
17 position: relative; 17 position: relative;
18 18
19 - .tb-disabled { 19 + &.tb-disabled {
20 color: rgba(0, 0, 0, .38); 20 color: rgba(0, 0, 0, .38);
21 } 21 }
22 22
23 - .fill-height { 23 + &.fill-height {
24 height: 100%; 24 height: 100%;
25 } 25 }
26 26
@@ -41,15 +41,20 @@ @@ -41,15 +41,20 @@
41 } 41 }
42 42
43 .tb-js-func-toolbar { 43 .tb-js-func-toolbar {
44 - .mat-button.tidy { 44 + button.mat-button, button.mat-icon-button, button.mat-icon-button.tb-mat-32 {
  45 + align-items: center;
  46 + vertical-align: middle;
45 min-width: 32px; 47 min-width: 32px;
46 min-height: 15px; 48 min-height: 15px;
47 padding: 4px; 49 padding: 4px;
48 - margin: 0 5px 0 0; 50 + margin: 0;
49 font-size: .8rem; 51 font-size: .8rem;
50 line-height: 15px; 52 line-height: 15px;
51 color: #7b7b7b; 53 color: #7b7b7b;
52 background: rgba(220, 220, 220, .35); 54 background: rgba(220, 220, 220, .35);
  55 + &:not(:last-child) {
  56 + margin-right: 4px;
  57 + }
53 } 58 }
54 } 59 }
55 } 60 }
@@ -14,7 +14,16 @@ @@ -14,7 +14,16 @@
14 /// limitations under the License. 14 /// limitations under the License.
15 /// 15 ///
16 16
17 -import { Component, ElementRef, forwardRef, Input, OnInit, ViewChild, ViewEncapsulation } from '@angular/core'; 17 +import {
  18 + ChangeDetectionStrategy,
  19 + Component,
  20 + ElementRef,
  21 + forwardRef,
  22 + Input, OnDestroy,
  23 + OnInit,
  24 + ViewChild,
  25 + ViewEncapsulation
  26 +} from '@angular/core';
18 import { ControlValueAccessor, FormControl, NG_VALIDATORS, NG_VALUE_ACCESSOR, Validator } from '@angular/forms'; 27 import { ControlValueAccessor, FormControl, NG_VALIDATORS, NG_VALUE_ACCESSOR, Validator } from '@angular/forms';
19 import * as ace from 'ace-builds'; 28 import * as ace from 'ace-builds';
20 import { coerceBooleanProperty } from '@angular/cdk/coercion'; 29 import { coerceBooleanProperty } from '@angular/cdk/coercion';
@@ -24,6 +33,7 @@ import { AppState } from '@core/core.state'; @@ -24,6 +33,7 @@ import { AppState } from '@core/core.state';
24 import { UtilsService } from '@core/services/utils.service'; 33 import { UtilsService } from '@core/services/utils.service';
25 import { isUndefined } from '@app/core/utils'; 34 import { isUndefined } from '@app/core/utils';
26 import { TranslateService } from '@ngx-translate/core'; 35 import { TranslateService } from '@ngx-translate/core';
  36 +import { CancelAnimationFrame, RafService } from '@core/services/raf.service';
27 37
28 @Component({ 38 @Component({
29 selector: 'tb-js-func', 39 selector: 'tb-js-func',
@@ -43,12 +53,14 @@ import { TranslateService } from '@ngx-translate/core'; @@ -43,12 +53,14 @@ import { TranslateService } from '@ngx-translate/core';
43 ], 53 ],
44 encapsulation: ViewEncapsulation.None 54 encapsulation: ViewEncapsulation.None
45 }) 55 })
46 -export class JsFuncComponent implements OnInit, ControlValueAccessor, Validator { 56 +export class JsFuncComponent implements OnInit, OnDestroy, ControlValueAccessor, Validator {
47 57
48 @ViewChild('javascriptEditor', {static: true}) 58 @ViewChild('javascriptEditor', {static: true})
49 javascriptEditorElmRef: ElementRef; 59 javascriptEditorElmRef: ElementRef;
50 60
51 private jsEditor: ace.Ace.Editor; 61 private jsEditor: ace.Ace.Editor;
  62 + private editorsResizeCaf: CancelAnimationFrame;
  63 + private editorResizeListener: any;
52 64
53 @Input() functionName: string; 65 @Input() functionName: string;
54 66
@@ -100,7 +112,8 @@ export class JsFuncComponent implements OnInit, ControlValueAccessor, Validator @@ -100,7 +112,8 @@ export class JsFuncComponent implements OnInit, ControlValueAccessor, Validator
100 constructor(public elementRef: ElementRef, 112 constructor(public elementRef: ElementRef,
101 private utils: UtilsService, 113 private utils: UtilsService,
102 private translate: TranslateService, 114 private translate: TranslateService,
103 - protected store: Store<AppState>) { 115 + protected store: Store<AppState>,
  116 + private raf: RafService) {
104 } 117 }
105 118
106 ngOnInit(): void { 119 ngOnInit(): void {
@@ -137,6 +150,28 @@ export class JsFuncComponent implements OnInit, ControlValueAccessor, Validator @@ -137,6 +150,28 @@ export class JsFuncComponent implements OnInit, ControlValueAccessor, Validator
137 this.cleanupJsErrors(); 150 this.cleanupJsErrors();
138 this.updateView(); 151 this.updateView();
139 }); 152 });
  153 + this.editorResizeListener = this.onAceEditorResize.bind(this);
  154 + // @ts-ignore
  155 + addResizeListener(editorElement, this.editorResizeListener);
  156 + }
  157 +
  158 + ngOnDestroy(): void {
  159 + if (this.editorResizeListener) {
  160 + const editorElement = this.javascriptEditorElmRef.nativeElement;
  161 + // @ts-ignore
  162 + removeResizeListener(editorElement, this.editorResizeListener);
  163 + }
  164 + }
  165 +
  166 + private onAceEditorResize() {
  167 + if (this.editorsResizeCaf) {
  168 + this.editorsResizeCaf();
  169 + this.editorsResizeCaf = null;
  170 + }
  171 + this.editorsResizeCaf = this.raf.raf(() => {
  172 + this.jsEditor.resize();
  173 + this.jsEditor.renderer.updateFull();
  174 + });
140 } 175 }
141 176
142 registerOnChange(fn: any): void { 177 registerOnChange(fn: any): void {
@@ -298,12 +333,4 @@ export class JsFuncComponent implements OnInit, ControlValueAccessor, Validator @@ -298,12 +333,4 @@ export class JsFuncComponent implements OnInit, ControlValueAccessor, Validator
298 } 333 }
299 } 334 }
300 335
301 - onFullscreen() {  
302 - if (this.jsEditor) {  
303 - setTimeout(() => {  
304 - this.jsEditor.resize();  
305 - }, 0);  
306 - }  
307 - }  
308 -  
309 } 336 }
@@ -15,6 +15,7 @@ @@ -15,6 +15,7 @@
15 /// 15 ///
16 16
17 import { 17 import {
  18 + ChangeDetectionStrategy,
18 Component, 19 Component,
19 ElementRef, 20 ElementRef,
20 forwardRef, 21 forwardRef,
@@ -32,6 +32,6 @@ @@ -32,6 +32,6 @@
32 </div> 32 </div>
33 <div fxFlex="0%" id="tb-json-panel" tb-toast toastTarget="jsonObjectEditor" 33 <div fxFlex="0%" id="tb-json-panel" tb-toast toastTarget="jsonObjectEditor"
34 class="tb-json-object-panel" fxLayout="column"> 34 class="tb-json-object-panel" fxLayout="column">
35 - <div fxFlex #jsonEditor id="tb-json-input" [ngClass]="{'fill-height': fillHeight}"></div> 35 + <div #jsonEditor id="tb-json-input" [ngStyle]="editorStyle" [ngClass]="{'fill-height': fillHeight}"></div>
36 </div> 36 </div>
37 </div> 37 </div>