Commit 2eb93dac7e0c83b98fa01e63f4409be00ed3295c

Authored by Igor Kulikov
1 parent b60b3144

Widget Editor: Save and save as actions.

Showing 24 changed files with 821 additions and 53 deletions
@@ -505,6 +505,7 @@ export class WidgetSubscription implements IWidgetSubscription { @@ -505,6 +505,7 @@ export class WidgetSubscription implements IWidgetSubscription {
505 505
506 private alarmsSubscribe() { 506 private alarmsSubscribe() {
507 // TODO: 507 // TODO:
  508 + this.notifyDataLoaded();
508 } 509 }
509 510
510 511
@@ -16,7 +16,7 @@ @@ -16,7 +16,7 @@
16 16
17 import { Injectable } from '@angular/core'; 17 import { Injectable } from '@angular/core';
18 import { defaultHttpOptions } from './http-utils'; 18 import { defaultHttpOptions } from './http-utils';
19 -import { Observable } from 'rxjs/index'; 19 +import { Observable, Subject, of, ReplaySubject } from 'rxjs/index';
20 import { HttpClient } from '@angular/common/http'; 20 import { HttpClient } from '@angular/common/http';
21 import { PageLink } from '@shared/models/page/page-link'; 21 import { PageLink } from '@shared/models/page/page-link';
22 import { PageData } from '@shared/models/page/page-data'; 22 import { PageData } from '@shared/models/page/page-data';
@@ -25,20 +25,57 @@ import { WidgetType, widgetType, WidgetTypeData, widgetTypesData } from '@shared @@ -25,20 +25,57 @@ import { WidgetType, widgetType, WidgetTypeData, widgetTypesData } from '@shared
25 import { UtilsService } from '@core/services/utils.service'; 25 import { UtilsService } from '@core/services/utils.service';
26 import { TranslateService } from '@ngx-translate/core'; 26 import { TranslateService } from '@ngx-translate/core';
27 import { ResourcesService } from '../services/resources.service'; 27 import { ResourcesService } from '../services/resources.service';
28 -import { toWidgetInfo, WidgetInfo } from '@app/modules/home/models/widget-component.models';  
29 -import { map } from 'rxjs/operators'; 28 +import { toWidgetInfo, WidgetInfo, toWidgetType } from '@app/modules/home/models/widget-component.models';
  29 +import { map, tap, mergeMap, filter } from 'rxjs/operators';
  30 +import { WidgetTypeId } from '@shared/models/id/widget-type-id';
  31 +import { NULL_UUID } from '@shared/models/id/has-uuid';
  32 +import { ActivationEnd, Router } from '@angular/router';
30 33
31 @Injectable({ 34 @Injectable({
32 providedIn: 'root' 35 providedIn: 'root'
33 }) 36 })
34 export class WidgetService { 37 export class WidgetService {
35 38
  39 + private widgetTypeUpdatedSubject = new Subject<WidgetType>();
  40 + private widgetsBundleDeletedSubject = new Subject<WidgetsBundle>();
  41 +
  42 + private allWidgetsBundles: Array<WidgetsBundle>;
  43 + private systemWidgetsBundles: Array<WidgetsBundle>;
  44 + private tenantWidgetsBundles: Array<WidgetsBundle>;
  45 +
36 constructor( 46 constructor(
37 private http: HttpClient, 47 private http: HttpClient,
38 private utils: UtilsService, 48 private utils: UtilsService,
39 private resources: ResourcesService, 49 private resources: ResourcesService,
40 - private translate: TranslateService 50 + private translate: TranslateService,
  51 + private router: Router
41 ) { 52 ) {
  53 + this.router.events.pipe(filter(event => event instanceof ActivationEnd)).subscribe(
  54 + () => {
  55 + this.invalidateWidgetsBundleCache();
  56 + }
  57 + );
  58 + }
  59 +
  60 + public getAllWidgetsBundles(ignoreErrors: boolean = false,
  61 + ignoreLoading: boolean = false): Observable<Array<WidgetsBundle>> {
  62 + return this.loadWidgetsBundleCache(ignoreErrors, ignoreLoading).pipe(
  63 + map(() => this.allWidgetsBundles)
  64 + );
  65 + }
  66 +
  67 + public getSystemWidgetsBundles(ignoreErrors: boolean = false,
  68 + ignoreLoading: boolean = false): Observable<Array<WidgetsBundle>> {
  69 + return this.loadWidgetsBundleCache(ignoreErrors, ignoreLoading).pipe(
  70 + map(() => this.systemWidgetsBundles)
  71 + );
  72 + }
  73 +
  74 + public getTenantWidgetsBundles(ignoreErrors: boolean = false,
  75 + ignoreLoading: boolean = false): Observable<Array<WidgetsBundle>> {
  76 + return this.loadWidgetsBundleCache(ignoreErrors, ignoreLoading).pipe(
  77 + map(() => this.tenantWidgetsBundles)
  78 + );
42 } 79 }
43 80
44 public getWidgetBundles(pageLink: PageLink, ignoreErrors: boolean = false, 81 public getWidgetBundles(pageLink: PageLink, ignoreErrors: boolean = false,
@@ -54,11 +91,26 @@ export class WidgetService { @@ -54,11 +91,26 @@ export class WidgetService {
54 91
55 public saveWidgetsBundle(widgetsBundle: WidgetsBundle, 92 public saveWidgetsBundle(widgetsBundle: WidgetsBundle,
56 ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<WidgetsBundle> { 93 ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<WidgetsBundle> {
57 - return this.http.post<WidgetsBundle>('/api/widgetsBundle', widgetsBundle, defaultHttpOptions(ignoreLoading, ignoreErrors)); 94 + return this.http.post<WidgetsBundle>('/api/widgetsBundle', widgetsBundle,
  95 + defaultHttpOptions(ignoreLoading, ignoreErrors)).pipe(
  96 + tap(() => {
  97 + this.invalidateWidgetsBundleCache();
  98 + })
  99 + );
58 } 100 }
59 101
60 public deleteWidgetsBundle(widgetsBundleId: string, ignoreErrors: boolean = false, ignoreLoading: boolean = false) { 102 public deleteWidgetsBundle(widgetsBundleId: string, ignoreErrors: boolean = false, ignoreLoading: boolean = false) {
61 - return this.http.delete(`/api/widgetsBundle/${widgetsBundleId}`, defaultHttpOptions(ignoreLoading, ignoreErrors)); 103 + return this.getWidgetsBundle(widgetsBundleId, ignoreErrors, ignoreLoading).pipe(
  104 + mergeMap((widgetsBundle) => {
  105 + return this.http.delete(`/api/widgetsBundle/${widgetsBundleId}`,
  106 + defaultHttpOptions(ignoreLoading, ignoreErrors)).pipe(
  107 + tap(() => {
  108 + this.invalidateWidgetsBundleCache();
  109 + this.widgetsBundleDeletedSubject.next(widgetsBundle);
  110 + })
  111 + );
  112 + }
  113 + ));
62 } 114 }
63 115
64 public getBundleWidgetTypes(bundleAlias: string, isSystem: boolean, 116 public getBundleWidgetTypes(bundleAlias: string, isSystem: boolean,
@@ -73,6 +125,41 @@ export class WidgetService { @@ -73,6 +125,41 @@ export class WidgetService {
73 defaultHttpOptions(ignoreLoading, ignoreErrors)); 125 defaultHttpOptions(ignoreLoading, ignoreErrors));
74 } 126 }
75 127
  128 + public saveWidgetType(widgetInfo: WidgetInfo,
  129 + id: WidgetTypeId,
  130 + bundleAlias: string,
  131 + ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<WidgetType> {
  132 + const widgetTypeInstance = toWidgetType(widgetInfo, id, undefined, bundleAlias);
  133 + return this.http.post<WidgetType>('/api/widgetType', widgetTypeInstance,
  134 + defaultHttpOptions(ignoreLoading, ignoreErrors)).pipe(
  135 + tap((savedWidgetType) => {
  136 + this.widgetTypeUpdatedSubject.next(savedWidgetType);
  137 + }));
  138 + }
  139 +
  140 + public saveImportedWidgetType(widgetTypeInstance: WidgetType,
  141 + ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<WidgetType> {
  142 + return this.http.post<WidgetType>('/api/widgetType', widgetTypeInstance,
  143 + defaultHttpOptions(ignoreLoading, ignoreErrors)).pipe(
  144 + tap((savedWidgetType) => {
  145 + this.widgetTypeUpdatedSubject.next(savedWidgetType);
  146 + }));
  147 + }
  148 +
  149 + public deleteWidgetType(bundleAlias: string, widgetTypeAlias: string, isSystem: boolean,
  150 + ignoreErrors: boolean = false, ignoreLoading: boolean = false) {
  151 + return this.getWidgetType(bundleAlias, widgetTypeAlias, isSystem, ignoreErrors, ignoreLoading).pipe(
  152 + mergeMap((widgetTypeInstance) => {
  153 + return this.http.delete(`/api/widgetType/${widgetTypeInstance.id.id}`,
  154 + defaultHttpOptions(ignoreLoading, ignoreErrors)).pipe(
  155 + tap(() => {
  156 + this.widgetTypeUpdatedSubject.next(widgetTypeInstance);
  157 + })
  158 + );
  159 + }
  160 + ));
  161 + }
  162 +
76 public getWidgetTypeById(widgetTypeId: string, 163 public getWidgetTypeById(widgetTypeId: string,
77 ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<WidgetType> { 164 ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<WidgetType> {
78 return this.http.get<WidgetType>(`/api/widgetType/${widgetTypeId}`, 165 return this.http.get<WidgetType>(`/api/widgetType/${widgetTypeId}`,
@@ -90,5 +177,55 @@ export class WidgetService { @@ -90,5 +177,55 @@ export class WidgetService {
90 return widgetInfo; 177 return widgetInfo;
91 }) 178 })
92 ); 179 );
  180 + }
  181 +
  182 + public onWidgetTypeUpdated(): Observable<WidgetType> {
  183 + return this.widgetTypeUpdatedSubject.asObservable();
  184 + }
  185 +
  186 + public onWidgetBundleDeleted(): Observable<WidgetsBundle> {
  187 + return this.widgetsBundleDeletedSubject.asObservable();
  188 + }
  189 +
  190 + private loadWidgetsBundleCache(ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<any> {
  191 + if (!this.allWidgetsBundles) {
  192 + const loadWidgetsBundleCacheSubject = new ReplaySubject();
  193 + this.http.get<Array<WidgetsBundle>>('/api/widgetsBundles',
  194 + defaultHttpOptions(ignoreLoading, ignoreErrors)).subscribe(
  195 + (allWidgetsBundles) => {
  196 + this.allWidgetsBundles = allWidgetsBundles;
  197 + this.systemWidgetsBundles = new Array<WidgetsBundle>();
  198 + this.tenantWidgetsBundles = new Array<WidgetsBundle>();
  199 + this.allWidgetsBundles = this.allWidgetsBundles.sort((wb1, wb2) => {
  200 + let res = wb1.title.localeCompare(wb2.title);
  201 + if (res === 0) {
  202 + res = wb2.createdTime - wb1.createdTime;
  203 + }
  204 + return res;
  205 + });
  206 + this.allWidgetsBundles.forEach((widgetsBundle) => {
  207 + if (widgetsBundle.tenantId.id === NULL_UUID) {
  208 + this.systemWidgetsBundles.push(widgetsBundle);
  209 + } else {
  210 + this.tenantWidgetsBundles.push(widgetsBundle);
  211 + }
  212 + });
  213 + loadWidgetsBundleCacheSubject.next();
  214 + loadWidgetsBundleCacheSubject.complete();
  215 + },
  216 + () => {
  217 + loadWidgetsBundleCacheSubject.error(null);
  218 + });
  219 + return loadWidgetsBundleCacheSubject.asObservable();
  220 + } else {
  221 + return of(null);
93 } 222 }
  223 + }
  224 +
  225 + private invalidateWidgetsBundleCache() {
  226 + this.allWidgetsBundles = undefined;
  227 + this.systemWidgetsBundles = undefined;
  228 + this.tenantWidgetsBundles = undefined;
  229 + }
  230 +
94 } 231 }
@@ -84,7 +84,7 @@ export class UtilsService { @@ -84,7 +84,7 @@ export class UtilsService {
84 } 84 }
85 85
86 public processWidgetException(exception: any): ExceptionData { 86 public processWidgetException(exception: any): ExceptionData {
87 - const data = this.parseException(exception, -5); 87 + const data = this.parseException(exception, -6);
88 if (this.widgetEditMode) { 88 if (this.widgetEditMode) {
89 const message: WindowMessage = { 89 const message: WindowMessage = {
90 type: 'widgetException', 90 type: 'widgetException',
@@ -308,7 +308,13 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo @@ -308,7 +308,13 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
308 $event.stopPropagation(); 308 $event.stopPropagation();
309 } 309 }
310 if (this.isRemoveActionEnabled && this.callbacks && this.callbacks.onRemoveWidget) { 310 if (this.isRemoveActionEnabled && this.callbacks && this.callbacks.onRemoveWidget) {
311 - this.callbacks.onRemoveWidget($event, widget.widget); 311 + this.callbacks.onRemoveWidget($event, widget.widget).subscribe(
  312 + (result) => {
  313 + if (result) {
  314 + this.dashboardWidgets.removeWidget(widget.widget);
  315 + }
  316 + }
  317 + );
312 } 318 }
313 } 319 }
314 320
@@ -18,16 +18,19 @@ import { Inject, Injectable } from '@angular/core'; @@ -18,16 +18,19 @@ import { Inject, Injectable } from '@angular/core';
18 import { DynamicComponentFactoryService } from '@core/services/dynamic-component-factory.service'; 18 import { DynamicComponentFactoryService } from '@core/services/dynamic-component-factory.service';
19 import { WidgetService } from '@core/http/widget.service'; 19 import { WidgetService } from '@core/http/widget.service';
20 import { forkJoin, Observable, of, ReplaySubject, Subject, throwError } from 'rxjs'; 20 import { forkJoin, Observable, of, ReplaySubject, Subject, throwError } from 'rxjs';
21 -import { WidgetInfo, MissingWidgetType, toWidgetInfo, WidgetTypeInstance, ErrorWidgetType } from '@home/models/widget-component.models'; 21 +import {
  22 + ErrorWidgetType,
  23 + MissingWidgetType,
  24 + toWidgetInfo,
  25 + toWidgetType,
  26 + WidgetInfo,
  27 + WidgetTypeInstance
  28 +} from '@home/models/widget-component.models';
22 import cssjs from '@core/css/css'; 29 import cssjs from '@core/css/css';
23 import { UtilsService } from '@core/services/utils.service'; 30 import { UtilsService } from '@core/services/utils.service';
24 import { ResourcesService } from '@core/services/resources.service'; 31 import { ResourcesService } from '@core/services/resources.service';
25 -import {  
26 - widgetActionSources,  
27 - WidgetControllerDescriptor,  
28 - WidgetType  
29 -} from '@shared/models/widget.models';  
30 -import { catchError, switchMap, map, mergeMap } from 'rxjs/operators'; 32 +import { widgetActionSources, WidgetControllerDescriptor, WidgetType } from '@shared/models/widget.models';
  33 +import { catchError, map, mergeMap, switchMap } from 'rxjs/operators';
31 import { isFunction, isUndefined } from '@core/utils'; 34 import { isFunction, isUndefined } from '@core/utils';
32 import { TranslateService } from '@ngx-translate/core'; 35 import { TranslateService } from '@ngx-translate/core';
33 import { DynamicWidgetComponent } from '@home/components/widget/dynamic-widget.component'; 36 import { DynamicWidgetComponent } from '@home/components/widget/dynamic-widget.component';
@@ -37,6 +40,9 @@ import { WINDOW } from '@core/services/window.service'; @@ -37,6 +40,9 @@ import { WINDOW } from '@core/services/window.service';
37 40
38 import * as tinycolor from 'tinycolor2'; 41 import * as tinycolor from 'tinycolor2';
39 import { TbFlot } from './lib/flot-widget'; 42 import { TbFlot } from './lib/flot-widget';
  43 +import { NULL_UUID } from '@shared/models/id/has-uuid';
  44 +import { WidgetTypeId } from '@app/shared/models/id/widget-type-id';
  45 +import { TenantId } from '@app/shared/models/id/tenant-id';
40 46
41 // declare var jQuery: any; 47 // declare var jQuery: any;
42 48
@@ -53,6 +59,7 @@ export class WidgetComponentService { @@ -53,6 +59,7 @@ export class WidgetComponentService {
53 59
54 private missingWidgetType: WidgetInfo; 60 private missingWidgetType: WidgetInfo;
55 private errorWidgetType: WidgetInfo; 61 private errorWidgetType: WidgetInfo;
  62 + private editingWidgetType: WidgetType;
56 63
57 constructor(@Inject(WINDOW) private window: Window, 64 constructor(@Inject(WINDOW) private window: Window,
58 private dynamicComponentFactoryService: DynamicComponentFactoryService, 65 private dynamicComponentFactoryService: DynamicComponentFactoryService,
@@ -68,6 +75,15 @@ export class WidgetComponentService { @@ -68,6 +75,15 @@ export class WidgetComponentService {
68 this.window.TbFlot = TbFlot; 75 this.window.TbFlot = TbFlot;
69 76
70 this.cssParser.testMode = false; 77 this.cssParser.testMode = false;
  78 +
  79 + this.widgetService.onWidgetTypeUpdated().subscribe((widgetType) => {
  80 + this.deleteWidgetInfoFromCache(widgetType.bundleAlias, widgetType.alias, widgetType.tenantId.id === NULL_UUID);
  81 + });
  82 +
  83 + this.widgetService.onWidgetBundleDeleted().subscribe((widgetsBundle) => {
  84 + this.deleteWidgetsBundleFromCache(widgetsBundle.alias, widgetsBundle.tenantId.id === NULL_UUID);
  85 + });
  86 +
71 this.init(); 87 this.init();
72 } 88 }
73 89
@@ -77,6 +93,24 @@ export class WidgetComponentService { @@ -77,6 +93,24 @@ export class WidgetComponentService {
77 } else { 93 } else {
78 this.missingWidgetType = {...MissingWidgetType}; 94 this.missingWidgetType = {...MissingWidgetType};
79 this.errorWidgetType = {...ErrorWidgetType}; 95 this.errorWidgetType = {...ErrorWidgetType};
  96 + if (this.utils.widgetEditMode) {
  97 + this.editingWidgetType = toWidgetType(
  98 + {
  99 + widgetName: this.utils.editWidgetInfo.widgetName,
  100 + alias: 'customWidget',
  101 + type: this.utils.editWidgetInfo.type,
  102 + sizeX: this.utils.editWidgetInfo.sizeX,
  103 + sizeY: this.utils.editWidgetInfo.sizeY,
  104 + resources: this.utils.editWidgetInfo.resources,
  105 + templateHtml: this.utils.editWidgetInfo.templateHtml,
  106 + templateCss: this.utils.editWidgetInfo.templateCss,
  107 + controllerScript: this.utils.editWidgetInfo.controllerScript,
  108 + settingsSchema: this.utils.editWidgetInfo.settingsSchema,
  109 + dataKeySettingsSchema: this.utils.editWidgetInfo.dataKeySettingsSchema,
  110 + defaultConfig: this.utils.editWidgetInfo.defaultConfig
  111 + }, new WidgetTypeId('1'), new TenantId( NULL_UUID ), 'customWidgetBundle'
  112 + );
  113 + }
80 const initSubject = new ReplaySubject(); 114 const initSubject = new ReplaySubject();
81 this.init$ = initSubject.asObservable(); 115 this.init$ = initSubject.asObservable();
82 const loadDefaultWidgetInfoTasks = [ 116 const loadDefaultWidgetInfoTasks = [
@@ -110,7 +144,7 @@ export class WidgetComponentService { @@ -110,7 +144,7 @@ export class WidgetComponentService {
110 widgetInfoSubject.complete(); 144 widgetInfoSubject.complete();
111 } else { 145 } else {
112 if (this.utils.widgetEditMode) { 146 if (this.utils.widgetEditMode) {
113 - // TODO: 147 + this.loadWidget(this.editingWidgetType, bundleAlias, isSystem, widgetInfoSubject);
114 } else { 148 } else {
115 const key = this.createWidgetInfoCacheKey(bundleAlias, widgetTypeAlias, isSystem); 149 const key = this.createWidgetInfoCacheKey(bundleAlias, widgetTypeAlias, isSystem);
116 let fetchQueue = this.widgetsInfoFetchQueue.get(key); 150 let fetchQueue = this.widgetsInfoFetchQueue.get(key);
@@ -377,4 +411,17 @@ export class WidgetComponentService { @@ -377,4 +411,17 @@ export class WidgetComponentService {
377 this.widgetsInfoInMemoryCache.set(key, widgetInfo); 411 this.widgetsInfoInMemoryCache.set(key, widgetInfo);
378 } 412 }
379 413
  414 + private deleteWidgetInfoFromCache(bundleAlias: string, widgetTypeAlias: string, isSystem: boolean) {
  415 + const key = this.createWidgetInfoCacheKey(bundleAlias, widgetTypeAlias, isSystem);
  416 + this.widgetsInfoInMemoryCache.delete(key);
  417 + }
  418 +
  419 + private deleteWidgetsBundleFromCache(bundleAlias: string, isSystem: boolean) {
  420 + const key = (isSystem ? 'sys_' : '') + bundleAlias;
  421 + this.widgetsInfoInMemoryCache.forEach((widgetInfo, cacheKey) => {
  422 + if (cacheKey.startsWith(key)) {
  423 + this.widgetsInfoInMemoryCache.delete(cacheKey);
  424 + }
  425 + });
  426 + }
380 } 427 }
@@ -847,13 +847,16 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI @@ -847,13 +847,16 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
847 createSubscriptionSubject.error(null); 847 createSubscriptionSubject.error(null);
848 } 848 }
849 ); 849 );
  850 + this.cd.detectChanges();
850 } else if (this.widget.type === widgetType.static) { 851 } else if (this.widget.type === widgetType.static) {
851 this.loadingData = false; 852 this.loadingData = false;
852 createSubscriptionSubject.next(); 853 createSubscriptionSubject.next();
853 createSubscriptionSubject.complete(); 854 createSubscriptionSubject.complete();
  855 + this.cd.detectChanges();
854 } else { 856 } else {
855 createSubscriptionSubject.next(); 857 createSubscriptionSubject.next();
856 createSubscriptionSubject.complete(); 858 createSubscriptionSubject.complete();
  859 + this.cd.detectChanges();
857 } 860 }
858 return createSubscriptionSubject.asObservable(); 861 return createSubscriptionSubject.asObservable();
859 } 862 }
@@ -33,7 +33,7 @@ export interface WidgetsData { @@ -33,7 +33,7 @@ export interface WidgetsData {
33 export interface DashboardCallbacks { 33 export interface DashboardCallbacks {
34 onEditWidget?: ($event: Event, widget: Widget) => void; 34 onEditWidget?: ($event: Event, widget: Widget) => void;
35 onExportWidget?: ($event: Event, widget: Widget) => void; 35 onExportWidget?: ($event: Event, widget: Widget) => void;
36 - onRemoveWidget?: ($event: Event, widget: Widget) => void; 36 + onRemoveWidget?: ($event: Event, widget: Widget) => Observable<boolean>;
37 onWidgetMouseDown?: ($event: Event, widget: Widget) => void; 37 onWidgetMouseDown?: ($event: Event, widget: Widget) => void;
38 onWidgetClicked?: ($event: Event, widget: Widget) => void; 38 onWidgetClicked?: ($event: Event, widget: Widget) => void;
39 prepareDashboardContextMenu?: ($event: Event) => void; 39 prepareDashboardContextMenu?: ($event: Event) => void;
@@ -43,6 +43,8 @@ import { @@ -43,6 +43,8 @@ import {
43 import { ComponentFactory } from '@angular/core'; 43 import { ComponentFactory } from '@angular/core';
44 import { HttpErrorResponse } from '@angular/common/http'; 44 import { HttpErrorResponse } from '@angular/common/http';
45 import { RafService } from '@core/services/raf.service'; 45 import { RafService } from '@core/services/raf.service';
  46 +import { WidgetTypeId } from '@shared/models/id/widget-type-id';
  47 +import { TenantId } from '@shared/models/id/tenant-id';
46 48
47 export interface IWidgetAction { 49 export interface IWidgetAction {
48 name: string; 50 name: string;
@@ -192,3 +194,25 @@ export function toWidgetInfo(widgetTypeEntity: WidgetType): WidgetInfo { @@ -192,3 +194,25 @@ export function toWidgetInfo(widgetTypeEntity: WidgetType): WidgetInfo {
192 }; 194 };
193 } 195 }
194 196
  197 +export function toWidgetType(widgetInfo: WidgetInfo, id: WidgetTypeId, tenantId: TenantId, bundleAlias: string): WidgetType {
  198 + const descriptor: WidgetTypeDescriptor = {
  199 + type: widgetInfo.type,
  200 + sizeX: widgetInfo.sizeX,
  201 + sizeY: widgetInfo.sizeY,
  202 + resources: widgetInfo.resources,
  203 + templateHtml: widgetInfo.templateHtml,
  204 + templateCss: widgetInfo.templateCss,
  205 + controllerScript: widgetInfo.controllerScript,
  206 + settingsSchema: widgetInfo.settingsSchema,
  207 + dataKeySettingsSchema: widgetInfo.dataKeySettingsSchema,
  208 + defaultConfig: widgetInfo.defaultConfig
  209 + };
  210 + return {
  211 + id,
  212 + tenantId,
  213 + bundleAlias,
  214 + alias: widgetInfo.alias,
  215 + name: widgetInfo.widgetName,
  216 + descriptor
  217 + };
  218 +}
@@ -51,6 +51,7 @@ import { Subscription } from 'rxjs'; @@ -51,6 +51,7 @@ import { Subscription } from 'rxjs';
51 import { FooterFabButtons } from '@shared/components/footer-fab-buttons.component'; 51 import { FooterFabButtons } from '@shared/components/footer-fab-buttons.component';
52 import { IStateController } from '@core/api/widget-api.models'; 52 import { IStateController } from '@core/api/widget-api.models';
53 import { DashboardUtilsService } from '@core/services/dashboard-utils.service'; 53 import { DashboardUtilsService } from '@core/services/dashboard-utils.service';
  54 +import { DashboardService } from '@core/http/dashboard.service';
54 55
55 @Component({ 56 @Component({
56 selector: 'tb-dashboard-page', 57 selector: 'tb-dashboard-page',
@@ -173,7 +174,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC @@ -173,7 +174,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
173 private dashboardUtils: DashboardUtilsService, 174 private dashboardUtils: DashboardUtilsService,
174 private authService: AuthService, 175 private authService: AuthService,
175 private entityService: EntityService, 176 private entityService: EntityService,
176 - private dialogService: DialogService) { 177 + private dialogService: DialogService,
  178 + private dashboardService: DashboardService) {
177 super(store); 179 super(store);
178 180
179 this.rxSubscriptions.push(this.route.data.subscribe( 181 this.rxSubscriptions.push(this.route.data.subscribe(
@@ -460,6 +462,11 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC @@ -460,6 +462,11 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
460 this.setEditMode(!this.isEdit, true); 462 this.setEditMode(!this.isEdit, true);
461 } 463 }
462 464
  465 + public saveDashboard() {
  466 + this.setEditMode(false, false);
  467 + this.notifyDashboardUpdated();
  468 + }
  469 +
463 public openDashboardState(state: string, openRightLayout: boolean) { 470 public openDashboardState(state: string, openRightLayout: boolean) {
464 const layoutsData = this.dashboardUtils.getStateLayoutsData(this.dashboard, state); 471 const layoutsData = this.dashboardUtils.getStateLayoutsData(this.dashboard, state);
465 if (layoutsData) { 472 if (layoutsData) {
@@ -514,8 +521,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC @@ -514,8 +521,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
514 private setEditMode(isEdit: boolean, revert: boolean) { 521 private setEditMode(isEdit: boolean, revert: boolean) {
515 this.isEdit = isEdit; 522 this.isEdit = isEdit;
516 if (this.isEdit) { 523 if (this.isEdit) {
517 - // TODO:  
518 - // this.dashboardCtx.stateController.preserveState(); 524 + this.dashboardCtx.stateController.preserveState();
519 this.prevDashboard = deepClone(this.dashboard); 525 this.prevDashboard = deepClone(this.dashboard);
520 } else { 526 } else {
521 if (this.widgetEditMode) { 527 if (this.widgetEditMode) {
@@ -549,4 +555,20 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC @@ -549,4 +555,20 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
549 private entityAliasesUpdated() { 555 private entityAliasesUpdated() {
550 this.dashboardCtx.aliasController.updateEntityAliases(this.dashboard.configuration.entityAliases); 556 this.dashboardCtx.aliasController.updateEntityAliases(this.dashboard.configuration.entityAliases);
551 } 557 }
  558 +
  559 + private notifyDashboardUpdated() {
  560 + if (this.widgetEditMode) {
  561 + const widget = this.layouts.main.layoutCtx.widgets[0];
  562 + const layout = this.layouts.main.layoutCtx.widgetLayouts[widget.id];
  563 + widget.sizeX = layout.sizeX;
  564 + widget.sizeY = layout.sizeY;
  565 + const message: WindowMessage = {
  566 + type: 'widgetEditUpdated',
  567 + data: widget
  568 + };
  569 + this.window.parent.postMessage(JSON.stringify(message), '*');
  570 + } else {
  571 + this.dashboardService.saveDashboard(this.dashboard);
  572 + }
  573 + }
552 } 574 }
@@ -77,4 +77,7 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo @@ -77,4 +77,7 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo
77 setResizing(layoutVisibilityChanged: boolean) { 77 setResizing(layoutVisibilityChanged: boolean) {
78 } 78 }
79 79
  80 + resetHighlight() {
  81 + }
  82 +
80 } 83 }
@@ -17,4 +17,5 @@ @@ -17,4 +17,5 @@
17 export interface ILayoutController { 17 export interface ILayoutController {
18 reload(); 18 reload();
19 setResizing(layoutVisibilityChanged: boolean); 19 setResizing(layoutVisibilityChanged: boolean);
  20 + resetHighlight();
20 } 21 }
  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 #saveWidgetTypeAsForm="ngForm"
  19 + [formGroup]="saveWidgetTypeAsFormGroup"(ngSubmit)="saveAs()">
  20 + <mat-toolbar fxLayout="row" color="primary">
  21 + <h2 translate>widget.save-widget-type-as</h2>
  22 + <span fxFlex></span>
  23 + <button mat-button mat-icon-button
  24 + (click)="cancel()"
  25 + type="button">
  26 + <mat-icon class="material-icons">close</mat-icon>
  27 + </button>
  28 + </mat-toolbar>
  29 + <mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
  30 + </mat-progress-bar>
  31 + <div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
  32 + <div mat-dialog-content>
  33 + <fieldset>
  34 + <span translate>widget.save-widget-type-as-text</span>
  35 + <mat-form-field class="mat-block">
  36 + <mat-label translate>widget.title</mat-label>
  37 + <input matInput formControlName="title" required>
  38 + <mat-error *ngIf="saveWidgetTypeAsFormGroup.get('title').hasError('required')">
  39 + {{ 'widget.title-required' | translate }}
  40 + </mat-error>
  41 + </mat-form-field>
  42 + <tb-widgets-bundle-select fxFlex
  43 + formControlName="widgetsBundle"
  44 + required
  45 + bundlesScope="{{bundlesScope}}">
  46 + </tb-widgets-bundle-select>
  47 + </fieldset>
  48 + </div>
  49 + <div mat-dialog-actions fxLayout="row">
  50 + <span fxFlex></span>
  51 + <button mat-button mat-raised-button color="primary"
  52 + type="submit"
  53 + [disabled]="(isLoading$ | async) || saveWidgetTypeAsForm.invalid
  54 + || !saveWidgetTypeAsForm.dirty">
  55 + {{ 'action.saveAs' | translate }}
  56 + </button>
  57 + <button mat-button color="primary"
  58 + style="margin-right: 20px;"
  59 + type="button"
  60 + [disabled]="(isLoading$ | async)"
  61 + (click)="cancel()" cdkFocusInitial>
  62 + {{ 'action.cancel' | translate }}
  63 + </button>
  64 + </div>
  65 +</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, OnInit } from '@angular/core';
  18 +import { MatDialogRef } from '@angular/material';
  19 +import { Store } from '@ngrx/store';
  20 +import { AppState } from '@core/core.state';
  21 +import { FormBuilder, FormGroup, Validators } from '@angular/forms';
  22 +import { DialogComponent } from '@shared/components/dialog.component';
  23 +import { Router } from '@angular/router';
  24 +import { WidgetsBundle } from '@shared/models/widgets-bundle.model';
  25 +import { getCurrentAuthUser } from '@core/auth/auth.selectors';
  26 +import { Authority } from '@shared/models/authority.enum';
  27 +
  28 +export interface SaveWidgetTypeAsDialogResult {
  29 + widgetName: string;
  30 + bundleId: string;
  31 + bundleAlias: string;
  32 +}
  33 +
  34 +@Component({
  35 + selector: 'tb-save-widget-type-as-dialog',
  36 + templateUrl: './save-widget-type-as-dialog.component.html',
  37 + styleUrls: []
  38 +})
  39 +export class SaveWidgetTypeAsDialogComponent extends
  40 + DialogComponent<SaveWidgetTypeAsDialogComponent, SaveWidgetTypeAsDialogResult> implements OnInit {
  41 +
  42 + saveWidgetTypeAsFormGroup: FormGroup;
  43 +
  44 + bundlesScope: string;
  45 +
  46 + constructor(protected store: Store<AppState>,
  47 + protected router: Router,
  48 + public dialogRef: MatDialogRef<SaveWidgetTypeAsDialogComponent, SaveWidgetTypeAsDialogResult>,
  49 + public fb: FormBuilder) {
  50 + super(store, router, dialogRef);
  51 +
  52 + const authUser = getCurrentAuthUser(store);
  53 + if (authUser.authority === Authority.TENANT_ADMIN) {
  54 + this.bundlesScope = 'tenant';
  55 + } else {
  56 + this.bundlesScope = 'system';
  57 + }
  58 + }
  59 +
  60 + ngOnInit(): void {
  61 + this.saveWidgetTypeAsFormGroup = this.fb.group({
  62 + title: [null, [Validators.required]],
  63 + widgetsBundle: [null, [Validators.required]]
  64 + });
  65 + }
  66 +
  67 + cancel(): void {
  68 + this.dialogRef.close(null);
  69 + }
  70 +
  71 + saveAs(): void {
  72 + const widgetName: string = this.saveWidgetTypeAsFormGroup.get('title').value;
  73 + const widgetsBundle: WidgetsBundle = this.saveWidgetTypeAsFormGroup.get('widgetsBundle').value;
  74 + const result: SaveWidgetTypeAsDialogResult = {
  75 + widgetName,
  76 + bundleId: widgetsBundle.id.id,
  77 + bundleAlias: widgetsBundle.alias
  78 + };
  79 + this.dialogRef.close(result);
  80 + }
  81 +}
@@ -19,9 +19,10 @@ @@ -19,9 +19,10 @@
19 <div fxFlex fxLayout="column"> 19 <div fxFlex fxLayout="column">
20 <div fxFlex fxLayout="column" tb-fullscreen [fullscreen]="fullscreen"> 20 <div fxFlex fxLayout="column" tb-fullscreen [fullscreen]="fullscreen">
21 <mat-toolbar class="mat-elevation-z1 tb-edit-toolbar mat-hue-3" fxLayoutGap="16px"> 21 <mat-toolbar class="mat-elevation-z1 tb-edit-toolbar mat-hue-3" fxLayoutGap="16px">
22 - <mat-form-field floatLabel="always" class="tb-widget-title"> 22 + <mat-form-field floatLabel="always" hideRequiredMarker class="tb-widget-title">
23 <mat-label></mat-label> 23 <mat-label></mat-label>
24 - <input [disabled]="isReadOnly" matInput [(ngModel)]="widget.widgetName" (ngModelChange)="isDirty = true" 24 + <input [disabled]="isReadOnly" matInput required
  25 + [(ngModel)]="widget.widgetName" (ngModelChange)="isDirty = true"
25 placeholder="{{ 'widget.title' | translate }}"/> 26 placeholder="{{ 'widget.title' | translate }}"/>
26 </mat-form-field> 27 </mat-form-field>
27 <mat-form-field> 28 <mat-form-field>
@@ -238,14 +239,15 @@ @@ -238,14 +239,15 @@
238 </div> 239 </div>
239 <div tb-fullscreen [fullscreen]="iFrameFullscreen" style="width: 100%; height: 100%;"> 240 <div tb-fullscreen [fullscreen]="iFrameFullscreen" style="width: 100%; height: 100%;">
240 <iframe #widgetIFrame frameborder="0" height="100%" width="100%"></iframe> 241 <iframe #widgetIFrame frameborder="0" height="100%" width="100%"></iframe>
241 - <button mat-button mat-icon-button  
242 - class="tb-fullscreen-button-style"  
243 - style="position: absolute; top: 10px; left: 10px; bottom: initial;"  
244 - (click)="iFrameFullscreen = !iFrameFullscreen"  
245 - matTooltip="{{(iFrameFullscreen ? 'fullscreen.exit' : 'fullscreen.expand') | translate}}"  
246 - matTooltipPosition="above">  
247 - <mat-icon>{{ iFrameFullscreen ? 'fullscreen_exit' : 'fullscreen' }}</mat-icon>  
248 - </button> 242 + <div style="position: absolute; top: 10px; left: 10px; bottom: initial;">
  243 + <button mat-button mat-icon-button
  244 + class="tb-fullscreen-button-style"
  245 + (click)="iFrameFullscreen = !iFrameFullscreen"
  246 + matTooltip="{{(iFrameFullscreen ? 'fullscreen.exit' : 'fullscreen.expand') | translate}}"
  247 + matTooltipPosition="above">
  248 + <mat-icon>{{ iFrameFullscreen ? 'fullscreen_exit' : 'fullscreen' }}</mat-icon>
  249 + </button>
  250 + </div>
249 </div> 251 </div>
250 </div> 252 </div>
251 </div> 253 </div>
@@ -20,9 +20,9 @@ import { WidgetsBundle } from '@shared/models/widgets-bundle.model'; @@ -20,9 +20,9 @@ import { WidgetsBundle } from '@shared/models/widgets-bundle.model';
20 import { Store } from '@ngrx/store'; 20 import { Store } from '@ngrx/store';
21 import { AppState } from '@core/core.state'; 21 import { AppState } from '@core/core.state';
22 import { WidgetService } from '@core/http/widget.service'; 22 import { WidgetService } from '@core/http/widget.service';
23 -import { WidgetInfo } from '@home/models/widget-component.models'; 23 +import { toWidgetInfo, WidgetInfo } from '@home/models/widget-component.models';
24 import { WidgetConfig, widgetType, WidgetType, widgetTypesData, Widget } from '@shared/models/widget.models'; 24 import { WidgetConfig, widgetType, WidgetType, widgetTypesData, Widget } from '@shared/models/widget.models';
25 -import { ActivatedRoute } from '@angular/router'; 25 +import { ActivatedRoute, Router } from '@angular/router';
26 import { deepClone } from '@core/utils'; 26 import { deepClone } from '@core/utils';
27 import { HasDirtyFlag } from '@core/guards/confirm-on-exit.guard'; 27 import { HasDirtyFlag } from '@core/guards/confirm-on-exit.guard';
28 import { AuthUser } from '@shared/models/user.model'; 28 import { AuthUser } from '@shared/models/user.model';
@@ -40,6 +40,13 @@ import { WindowMessage } from '@shared/models/window-message.model'; @@ -40,6 +40,13 @@ import { WindowMessage } from '@shared/models/window-message.model';
40 import { ExceptionData } from '@shared/models/error.models'; 40 import { ExceptionData } from '@shared/models/error.models';
41 import Timeout = NodeJS.Timeout; 41 import Timeout = NodeJS.Timeout;
42 import { ActionNotificationHide, ActionNotificationShow } from '@core/notification/notification.actions'; 42 import { ActionNotificationHide, ActionNotificationShow } from '@core/notification/notification.actions';
  43 +import { MatDialog } from '@angular/material/dialog';
  44 +import { SelectWidgetTypeDialogComponent } from '@home/pages/widget/select-widget-type-dialog.component';
  45 +import {
  46 + SaveWidgetTypeAsDialogComponent,
  47 + SaveWidgetTypeAsDialogResult
  48 +} from '@home/pages/widget/save-widget-type-as-dialog.component';
  49 +import { Subscription } from 'rxjs';
43 50
44 @Component({ 51 @Component({
45 selector: 'tb-widget-editor', 52 selector: 'tb-widget-editor',
@@ -131,25 +138,37 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe @@ -131,25 +138,37 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe
131 138
132 saveWidgetTimeout: Timeout; 139 saveWidgetTimeout: Timeout;
133 140
  141 + private rxSubscriptions = new Array<Subscription>();
  142 +
134 constructor(protected store: Store<AppState>, 143 constructor(protected store: Store<AppState>,
135 @Inject(WINDOW) private window: Window, 144 @Inject(WINDOW) private window: Window,
136 private route: ActivatedRoute, 145 private route: ActivatedRoute,
  146 + private router: Router,
137 private widgetService: WidgetService, 147 private widgetService: WidgetService,
138 private hotkeysService: HotkeysService, 148 private hotkeysService: HotkeysService,
139 private translate: TranslateService, 149 private translate: TranslateService,
140 - private raf: RafService) { 150 + private raf: RafService,
  151 + private dialog: MatDialog) {
141 super(store); 152 super(store);
142 153
143 this.authUser = getCurrentAuthUser(store); 154 this.authUser = getCurrentAuthUser(store);
144 155
145 - this.widgetsBundle = this.route.snapshot.data.widgetsBundle; 156 + this.rxSubscriptions.push(this.route.data.subscribe(
  157 + (data) => {
  158 + this.init(data);
  159 + }
  160 + ));
  161 + }
  162 +
  163 + private init(data: any) {
  164 + this.widgetsBundle = data.widgetsBundle;
146 if (this.authUser.authority === Authority.TENANT_ADMIN) { 165 if (this.authUser.authority === Authority.TENANT_ADMIN) {
147 this.isReadOnly = !this.widgetsBundle || this.widgetsBundle.tenantId.id === NULL_UUID; 166 this.isReadOnly = !this.widgetsBundle || this.widgetsBundle.tenantId.id === NULL_UUID;
148 } else { 167 } else {
149 this.isReadOnly = this.authUser.authority !== Authority.SYS_ADMIN; 168 this.isReadOnly = this.authUser.authority !== Authority.SYS_ADMIN;
150 } 169 }
151 - this.widgetType = this.route.snapshot.data.widgetEditorData.widgetType;  
152 - this.widget = this.route.snapshot.data.widgetEditorData.widget; 170 + this.widgetType = data.widgetEditorData.widgetType;
  171 + this.widget = data.widgetEditorData.widget;
153 if (this.widgetType) { 172 if (this.widgetType) {
154 const config = JSON.parse(this.widget.defaultConfig); 173 const config = JSON.parse(this.widget.defaultConfig);
155 this.widget.defaultConfig = JSON.stringify(config); 174 this.widget.defaultConfig = JSON.stringify(config);
@@ -176,6 +195,10 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe @@ -176,6 +195,10 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe
176 // @ts-ignore 195 // @ts-ignore
177 removeResizeListener(resizeListener.element, resizeListener.resizeListener); 196 removeResizeListener(resizeListener.element, resizeListener.resizeListener);
178 }); 197 });
  198 + this.rxSubscriptions.forEach((subscription) => {
  199 + subscription.unsubscribe();
  200 + });
  201 + this.rxSubscriptions.length = 0;
179 } 202 }
180 203
181 private initHotKeys(): void { 204 private initHotKeys(): void {
@@ -448,13 +471,52 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe @@ -448,13 +471,52 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe
448 } 471 }
449 472
450 private commitSaveWidget() { 473 private commitSaveWidget() {
451 - // TODO:  
452 - this.saveWidgetPending = false; 474 + const id = (this.widgetType && this.widgetType.id) ? this.widgetType.id : undefined;
  475 + this.widgetService.saveWidgetType(this.widget, id, this.widgetsBundle.alias).subscribe(
  476 + (widgetTypeInstance) => {
  477 + this.setWidgetType(widgetTypeInstance);
  478 + this.saveWidgetPending = false;
  479 + this.store.dispatch(new ActionNotificationShow(
  480 + {message: this.translate.instant('widget.widget-saved'), type: 'success', duration: 500}));
  481 + },
  482 + () => {
  483 + this.saveWidgetPending = false;
  484 + }
  485 + );
453 } 486 }
454 487
455 private commitSaveWidgetAs() { 488 private commitSaveWidgetAs() {
456 - // TODO:  
457 - this.saveWidgetAsPending = false; 489 + this.dialog.open<SaveWidgetTypeAsDialogComponent, any,
  490 + SaveWidgetTypeAsDialogResult>(SaveWidgetTypeAsDialogComponent, {
  491 + disableClose: true,
  492 + panelClass: ['tb-dialog', 'tb-fullscreen-dialog']
  493 + }).afterClosed().subscribe(
  494 + (saveWidgetAsData) => {
  495 + if (saveWidgetAsData) {
  496 + this.widget.widgetName = saveWidgetAsData.widgetName;
  497 + this.widget.alias = undefined;
  498 + const config = JSON.parse(this.widget.defaultConfig);
  499 + config.title = this.widget.widgetName;
  500 + this.widget.defaultConfig = JSON.stringify(config);
  501 + this.isDirty = false;
  502 + this.widgetService.saveWidgetType(this.widget, undefined, saveWidgetAsData.bundleAlias).subscribe(
  503 + (widgetTypeInstance) => {
  504 + this.router.navigateByUrl(`/widgets-bundles/${saveWidgetAsData.bundleId}/widgetTypes/${widgetTypeInstance.id.id}`);
  505 + }
  506 + );
  507 + }
  508 + this.saveWidgetAsPending = false;
  509 + }
  510 + );
  511 + }
  512 +
  513 + private setWidgetType(widgetTypeInstance: WidgetType) {
  514 + this.widgetType = widgetTypeInstance;
  515 + this.widget = toWidgetInfo(this.widgetType);
  516 + const config = JSON.parse(this.widget.defaultConfig);
  517 + this.widget.defaultConfig = JSON.stringify(config);
  518 + this.origWidget = deepClone(this.widget);
  519 + this.isDirty = false;
458 } 520 }
459 521
460 applyWidgetScript(): void { 522 applyWidgetScript(): void {
@@ -73,7 +73,7 @@ export class WidgetsTypesDataResolver implements Resolve<WidgetsData> { @@ -73,7 +73,7 @@ export class WidgetsTypesDataResolver implements Resolve<WidgetsData> {
73 } 73 }
74 return result; 74 return result;
75 }); 75 });
76 - const widgetTypes = new Array<Widget>(types.length); 76 + const widgetTypes = new Array<Widget>();
77 let top = 0; 77 let top = 0;
78 const lastTop = [0, 0, 0]; 78 const lastTop = [0, 0, 0];
79 let col = 0; 79 let col = 0;
@@ -15,7 +15,7 @@ @@ -15,7 +15,7 @@
15 limitations under the License. 15 limitations under the License.
16 16
17 --> 17 -->
18 -<section [fxShow]="!(isLoading$ | async) && (widgetTypes$ | async)?.length === 0" fxLayoutAlign="center center" 18 +<section [fxShow]="!(isLoading$ | async) && widgetsData.widgets.length === 0" fxLayoutAlign="center center"
19 style="text-transform: uppercase; display: flex; z-index: 1;" 19 style="text-transform: uppercase; display: flex; z-index: 1;"
20 class="tb-absolute-fill"> 20 class="tb-absolute-fill">
21 <button mat-button *ngIf="!isReadOnly" class="tb-add-new-widget" (click)="addWidgetType($event)"> 21 <button mat-button *ngIf="!isReadOnly" class="tb-add-new-widget" (click)="addWidgetType($event)">
@@ -27,7 +27,8 @@ @@ -27,7 +27,8 @@
27 style="text-transform: uppercase; display: flex;" 27 style="text-transform: uppercase; display: flex;"
28 class="mat-headline tb-absolute-fill">widgets-bundle.empty</span> 28 class="mat-headline tb-absolute-fill">widgets-bundle.empty</span>
29 </section> 29 </section>
30 -<tb-dashboard [aliasController]="aliasController" 30 +<tb-dashboard #dashboard
  31 + [aliasController]="aliasController"
31 [widgets]="widgetsData.widgets" 32 [widgets]="widgetsData.widgets"
32 [widgetLayouts]="widgetsData.widgetLayouts" 33 [widgetLayouts]="widgetsData.widgetLayouts"
33 [isEdit]="false" 34 [isEdit]="false"
@@ -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 } from '@angular/core'; 17 +import { Component, OnInit, ViewChild } from '@angular/core';
18 import { Store } from '@ngrx/store'; 18 import { Store } from '@ngrx/store';
19 import { AppState } from '@core/core.state'; 19 import { AppState } from '@core/core.state';
20 import { PageComponent } from '@shared/components/page.component'; 20 import { PageComponent } from '@shared/components/page.component';
@@ -24,13 +24,13 @@ import { WidgetsBundle } from '@shared/models/widgets-bundle.model'; @@ -24,13 +24,13 @@ import { WidgetsBundle } from '@shared/models/widgets-bundle.model';
24 import { ActivatedRoute, Router } from '@angular/router'; 24 import { ActivatedRoute, Router } from '@angular/router';
25 import { Authority } from '@shared/models/authority.enum'; 25 import { Authority } from '@shared/models/authority.enum';
26 import { NULL_UUID } from '@shared/models/id/has-uuid'; 26 import { NULL_UUID } from '@shared/models/id/has-uuid';
27 -import { Observable } from 'rxjs'; 27 +import { Observable, of } from 'rxjs';
28 import { Widget, widgetType } from '@app/shared/models/widget.models'; 28 import { Widget, widgetType } from '@app/shared/models/widget.models';
29 import { WidgetService } from '@core/http/widget.service'; 29 import { WidgetService } from '@core/http/widget.service';
30 -import { map, share } from 'rxjs/operators'; 30 +import { map, mergeMap, share } from 'rxjs/operators';
31 import { DialogService } from '@core/services/dialog.service'; 31 import { DialogService } from '@core/services/dialog.service';
32 import { FooterFabButtons } from '@app/shared/components/footer-fab-buttons.component'; 32 import { FooterFabButtons } from '@app/shared/components/footer-fab-buttons.component';
33 -import { DashboardCallbacks, WidgetsData } from '@home/models/dashboard-component.models'; 33 +import { DashboardCallbacks, IDashboardComponent, WidgetsData } from '@home/models/dashboard-component.models';
34 import { IAliasController } from '@app/core/api/widget-api.models'; 34 import { IAliasController } from '@app/core/api/widget-api.models';
35 import { toWidgetInfo } from '@home/models/widget-component.models'; 35 import { toWidgetInfo } from '@home/models/widget-component.models';
36 import { DummyAliasController } from '@core/api/alias-controller'; 36 import { DummyAliasController } from '@core/api/alias-controller';
@@ -41,6 +41,7 @@ import { @@ -41,6 +41,7 @@ import {
41 import { DeviceCredentials } from '@shared/models/device.models'; 41 import { DeviceCredentials } from '@shared/models/device.models';
42 import { MatDialog } from '@angular/material/dialog'; 42 import { MatDialog } from '@angular/material/dialog';
43 import { SelectWidgetTypeDialogComponent } from '@home/pages/widget/select-widget-type-dialog.component'; 43 import { SelectWidgetTypeDialogComponent } from '@home/pages/widget/select-widget-type-dialog.component';
  44 +import { TranslateService } from '@ngx-translate/core';
44 45
45 @Component({ 46 @Component({
46 selector: 'tb-widget-library', 47 selector: 'tb-widget-library',
@@ -86,12 +87,15 @@ export class WidgetLibraryComponent extends PageComponent implements OnInit { @@ -86,12 +87,15 @@ export class WidgetLibraryComponent extends PageComponent implements OnInit {
86 87
87 aliasController: IAliasController = new DummyAliasController(); 88 aliasController: IAliasController = new DummyAliasController();
88 89
  90 + @ViewChild('dashboard', {static: true}) dashboard: IDashboardComponent;
  91 +
89 constructor(protected store: Store<AppState>, 92 constructor(protected store: Store<AppState>,
90 private route: ActivatedRoute, 93 private route: ActivatedRoute,
91 private router: Router, 94 private router: Router,
92 private widgetService: WidgetService, 95 private widgetService: WidgetService,
93 private dialogService: DialogService, 96 private dialogService: DialogService,
94 - private dialog: MatDialog) { 97 + private dialog: MatDialog,
  98 + private translate: TranslateService) {
95 super(store); 99 super(store);
96 100
97 this.authUser = getCurrentAuthUser(store); 101 this.authUser = getCurrentAuthUser(store);
@@ -146,11 +150,32 @@ export class WidgetLibraryComponent extends PageComponent implements OnInit { @@ -146,11 +150,32 @@ export class WidgetLibraryComponent extends PageComponent implements OnInit {
146 this.dialogService.todo(); 150 this.dialogService.todo();
147 } 151 }
148 152
149 - removeWidgetType($event: Event, widget: Widget): void { 153 + removeWidgetType($event: Event, widget: Widget): Observable<boolean> {
150 if ($event) { 154 if ($event) {
151 $event.stopPropagation(); 155 $event.stopPropagation();
152 } 156 }
153 - this.dialogService.todo(); 157 + return this.dialogService.confirm(
  158 + this.translate.instant('widget.remove-widget-type-title', {widgetName: widget.config.title}),
  159 + this.translate.instant('widget.remove-widget-type-text'),
  160 + this.translate.instant('action.no'),
  161 + this.translate.instant('action.yes'),
  162 + ).pipe(
  163 + mergeMap((result) => {
  164 + if (result) {
  165 + return this.widgetService.deleteWidgetType(widget.bundleAlias, widget.typeAlias, widget.isSystemType);
  166 + } else {
  167 + return of(false);
  168 + }
  169 + }),
  170 + map((result) => {
  171 + if (result !== false) {
  172 + this.widgetsData.widgets.splice(this.widgetsData.widgets.indexOf(widget), 1);
  173 + return true;
  174 + } else {
  175 + return false;
  176 + }
  177 + }
  178 + ));
154 } 179 }
155 180
156 } 181 }
@@ -23,17 +23,20 @@ import {HomeComponentsModule} from '@modules/home/components/home-components.mod @@ -23,17 +23,20 @@ import {HomeComponentsModule} from '@modules/home/components/home-components.mod
23 import { WidgetLibraryComponent } from './widget-library.component'; 23 import { WidgetLibraryComponent } from './widget-library.component';
24 import { WidgetEditorComponent } from '@home/pages/widget/widget-editor.component'; 24 import { WidgetEditorComponent } from '@home/pages/widget/widget-editor.component';
25 import { SelectWidgetTypeDialogComponent } from '@home/pages/widget/select-widget-type-dialog.component'; 25 import { SelectWidgetTypeDialogComponent } from '@home/pages/widget/select-widget-type-dialog.component';
  26 +import { SaveWidgetTypeAsDialogComponent } from './save-widget-type-as-dialog.component';
26 27
27 @NgModule({ 28 @NgModule({
28 entryComponents: [ 29 entryComponents: [
29 WidgetsBundleComponent, 30 WidgetsBundleComponent,
30 - SelectWidgetTypeDialogComponent 31 + SelectWidgetTypeDialogComponent,
  32 + SaveWidgetTypeAsDialogComponent
31 ], 33 ],
32 declarations: [ 34 declarations: [
33 WidgetsBundleComponent, 35 WidgetsBundleComponent,
34 WidgetLibraryComponent, 36 WidgetLibraryComponent,
35 WidgetEditorComponent, 37 WidgetEditorComponent,
36 - SelectWidgetTypeDialogComponent 38 + SelectWidgetTypeDialogComponent,
  39 + SaveWidgetTypeAsDialogComponent
37 ], 40 ],
38 imports: [ 41 imports: [
39 CommonModule, 42 CommonModule,
@@ -38,6 +38,7 @@ import { @@ -38,6 +38,7 @@ import {
38 DashboardSelectPanelComponent, 38 DashboardSelectPanelComponent,
39 DashboardSelectPanelData 39 DashboardSelectPanelData
40 } from './dashboard-select-panel.component'; 40 } from './dashboard-select-panel.component';
  41 +import { NULL_UUID } from '@shared/models/id/has-uuid';
41 42
42 @Component({ 43 @Component({
43 selector: 'tb-dashboard-select', 44 selector: 'tb-dashboard-select',
@@ -200,7 +201,7 @@ export class DashboardSelectComponent implements ControlValueAccessor, OnInit { @@ -200,7 +201,7 @@ export class DashboardSelectComponent implements ControlValueAccessor, OnInit {
200 let dashboardsObservable: Observable<PageData<DashboardInfo>>; 201 let dashboardsObservable: Observable<PageData<DashboardInfo>>;
201 const authUser = getCurrentAuthUser(this.store); 202 const authUser = getCurrentAuthUser(this.store);
202 if (this.dashboardsScope === 'customer' || authUser.authority === Authority.CUSTOMER_USER) { 203 if (this.dashboardsScope === 'customer' || authUser.authority === Authority.CUSTOMER_USER) {
203 - if (this.customerId) { 204 + if (this.customerId && this.customerId !== NULL_UUID) {
204 dashboardsObservable = this.dashboardService.getCustomerDashboards(this.customerId, pageLink, false, true); 205 dashboardsObservable = this.dashboardService.getCustomerDashboards(this.customerId, pageLink, false, true);
205 } else { 206 } else {
206 dashboardsObservable = of(emptyPageData()); 207 dashboardsObservable = of(emptyPageData());
  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-form-field floatLabel="always" hideRequiredMarker class="mat-block">
  19 + <mat-label></mat-label>
  20 + <mat-select [required]="required"
  21 + [disabled]="disabled"
  22 + [(ngModel)]="widgetsBundle"
  23 + matInput
  24 + panelClass="tb-widgets-bundle-select"
  25 + placeholder="{{ 'widget.select-widgets-bundle' | translate }}"
  26 + (ngModelChange)="widgetsBundleChanged()">
  27 + <mat-option *ngFor="let widgetsBundle of widgetsBundles$ | async" [value]="widgetsBundle">
  28 + <div class="tb-bundle-item">
  29 + <span>{{widgetsBundle.title}}</span>
  30 + <span translate class="tb-bundle-system" *ngIf="isSystem(item)">widgets-bundle.system</span>
  31 + </div>
  32 + </mat-option>
  33 + </mat-select>
  34 +</mat-form-field>
  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 +tb-widgets-bundle-select {
  18 + mat-select {
  19 + margin: 0;
  20 + }
  21 +
  22 + .tb-bundle-item {
  23 + height: 24px;
  24 + line-height: 24px;
  25 + }
  26 +}
  27 +
  28 +.tb-widgets-bundle-select {
  29 + .tb-bundle-item {
  30 + height: 48px;
  31 + line-height: 48px;
  32 + }
  33 +}
  34 +
  35 +tb-widgets-bundle-select,
  36 +.tb-widgets-bundle-select {
  37 + .mat-select-value-text {
  38 + display: block;
  39 + width: 100%;
  40 + }
  41 +
  42 + .tb-bundle-item {
  43 + display: inline-block;
  44 + width: 100%;
  45 +
  46 + span {
  47 + display: inline-block;
  48 + vertical-align: middle;
  49 + }
  50 +
  51 + .tb-bundle-system {
  52 + float: right;
  53 + font-size: .8rem;
  54 + opacity: .8;
  55 + }
  56 + }
  57 +
  58 + mat-option {
  59 + height: auto !important;
  60 + white-space: normal !important;
  61 + }
  62 +}
  63 +
  64 +mat-toolbar {
  65 + tb-widgets-bundle-select {
  66 + mat-select {
  67 + background: rgba(255, 255, 255, .2);
  68 + padding: 5px 20px;
  69 +
  70 + .mat-select-value-text {
  71 + font-size: 1.2rem;
  72 + color: #fff;
  73 +
  74 + span:first-child::after {
  75 + color: #fff;
  76 + }
  77 + }
  78 +
  79 + .mat-select-value.mat-select-placeholder {
  80 + color: #fff;
  81 + opacity: .8;
  82 + }
  83 + }
  84 +
  85 + mat-select.ng-invalid.ng-touched {
  86 + .mat-select-value-text {
  87 + color: #fff !important;
  88 + }
  89 + }
  90 + }
  91 +}
  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, forwardRef, Input, OnChanges, OnInit, ViewEncapsulation, SimpleChanges } from '@angular/core';
  18 +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
  19 +import { Observable } from 'rxjs';
  20 +import { share, tap } from 'rxjs/operators';
  21 +import { Store } from '@ngrx/store';
  22 +import { AppState } from '@app/core/core.state';
  23 +import { coerceBooleanProperty } from '@angular/cdk/coercion';
  24 +import { WidgetsBundle } from '@shared/models/widgets-bundle.model';
  25 +import { WidgetService } from '@core/http/widget.service';
  26 +import { isDefined } from '@core/utils';
  27 +import { NULL_UUID } from '@shared/models/id/has-uuid';
  28 +
  29 +@Component({
  30 + selector: 'tb-widgets-bundle-select',
  31 + templateUrl: './widgets-bundle-select.component.html',
  32 + styleUrls: ['./widgets-bundle-select.component.scss'],
  33 + providers: [{
  34 + provide: NG_VALUE_ACCESSOR,
  35 + useExisting: forwardRef(() => WidgetsBundleSelectComponent),
  36 + multi: true
  37 + }],
  38 + encapsulation: ViewEncapsulation.None
  39 +})
  40 +export class WidgetsBundleSelectComponent implements ControlValueAccessor, OnInit, OnChanges {
  41 +
  42 + @Input()
  43 + bundlesScope: 'system' | 'tenant';
  44 +
  45 + @Input()
  46 + selectFirstBundle: boolean;
  47 +
  48 + @Input()
  49 + selectBundleAlias: string;
  50 +
  51 + private requiredValue: boolean;
  52 + get required(): boolean {
  53 + return this.requiredValue;
  54 + }
  55 + @Input()
  56 + set required(value: boolean) {
  57 + this.requiredValue = coerceBooleanProperty(value);
  58 + }
  59 +
  60 + @Input()
  61 + disabled: boolean;
  62 +
  63 + widgetsBundles$: Observable<Array<WidgetsBundle>>;
  64 +
  65 + widgetsBundles: Array<WidgetsBundle>;
  66 +
  67 + widgetsBundle: WidgetsBundle | null;
  68 +
  69 + private propagateChange = (v: any) => { };
  70 +
  71 + constructor(private store: Store<AppState>,
  72 + private widgetService: WidgetService) {
  73 + }
  74 +
  75 + registerOnChange(fn: any): void {
  76 + this.propagateChange = fn;
  77 + }
  78 +
  79 + registerOnTouched(fn: any): void {
  80 + }
  81 +
  82 + ngOnInit() {
  83 + this.widgetsBundles$ = this.getWidgetsBundles().pipe(
  84 + tap((widgetsBundles) => {
  85 + this.widgetsBundles = widgetsBundles;
  86 + if (this.selectFirstBundle) {
  87 + if (widgetsBundles.length > 0) {
  88 + if (this.widgetsBundle !== widgetsBundles[0]) {
  89 + this.widgetsBundle = widgetsBundles[0];
  90 + this.updateView();
  91 + } else if (isDefined(this.selectBundleAlias)) {
  92 + this.selectWidgetsBundleByAlias(this.selectBundleAlias);
  93 + }
  94 + }
  95 + }
  96 + }),
  97 + share()
  98 + );
  99 + }
  100 +
  101 + ngOnChanges(changes: SimpleChanges): void {
  102 + for (const propName of Object.keys(changes)) {
  103 + const change = changes[propName];
  104 + if (!change.firstChange && change.currentValue !== change.previousValue) {
  105 + if (propName === 'selectBundleAlias') {
  106 + this.selectWidgetsBundleByAlias(this.selectBundleAlias);
  107 + }
  108 + }
  109 + }
  110 + }
  111 +
  112 + setDisabledState(isDisabled: boolean): void {
  113 + this.disabled = isDisabled;
  114 + }
  115 +
  116 + writeValue(value: WidgetsBundle | null): void {
  117 + this.widgetsBundle = value;
  118 + }
  119 +
  120 + widgetsBundleChanged() {
  121 + this.updateView();
  122 + }
  123 +
  124 + isSystem(item: WidgetsBundle) {
  125 + return item && item.tenantId.id === NULL_UUID;
  126 + }
  127 +
  128 + private selectWidgetsBundleByAlias(alias: string) {
  129 + if (this.widgetsBundles && alias) {
  130 + const found = this.widgetsBundles.find((widgetsBundle) => widgetsBundle.alias === alias);
  131 + if (found && this.widgetsBundle !== found) {
  132 + this.widgetsBundle = found;
  133 + this.updateView();
  134 + }
  135 + }
  136 + }
  137 +
  138 + private updateView() {
  139 + this.propagateChange(this.widgetsBundle);
  140 + }
  141 +
  142 + private getWidgetsBundles(): Observable<Array<WidgetsBundle>> {
  143 + let widgetsBundlesObservable: Observable<Array<WidgetsBundle>>;
  144 + if (this.bundlesScope) {
  145 + if (this.bundlesScope === 'system') {
  146 + widgetsBundlesObservable = this.widgetService.getSystemWidgetsBundles();
  147 + } else if (this.bundlesScope === 'tenant') {
  148 + widgetsBundlesObservable = this.widgetService.getTenantWidgetsBundles();
  149 + }
  150 + } else {
  151 + widgetsBundlesObservable = this.widgetService.getAllWidgetsBundles();
  152 + }
  153 + return widgetsBundlesObservable;
  154 + }
  155 +
  156 +}
@@ -94,6 +94,7 @@ import { MatSpinner } from '@angular/material/progress-spinner'; @@ -94,6 +94,7 @@ import { MatSpinner } from '@angular/material/progress-spinner';
94 import { FabToolbarComponent, FabActionsDirective, FabTriggerDirective } from './components/fab-toolbar.component'; 94 import { FabToolbarComponent, FabActionsDirective, FabTriggerDirective } from './components/fab-toolbar.component';
95 import { DashboardSelectPanelComponent } from '@shared/components/dashboard-select-panel.component'; 95 import { DashboardSelectPanelComponent } from '@shared/components/dashboard-select-panel.component';
96 import { DashboardSelectComponent } from '@shared/components/dashboard-select.component'; 96 import { DashboardSelectComponent } from '@shared/components/dashboard-select.component';
  97 +import { WidgetsBundleSelectComponent } from './components/widgets-bundle-select.component';
97 98
98 @NgModule({ 99 @NgModule({
99 providers: [ 100 providers: [
@@ -145,6 +146,7 @@ import { DashboardSelectComponent } from '@shared/components/dashboard-select.co @@ -145,6 +146,7 @@ import { DashboardSelectComponent } from '@shared/components/dashboard-select.co
145 FabTriggerDirective, 146 FabTriggerDirective,
146 FabActionsDirective, 147 FabActionsDirective,
147 FabToolbarComponent, 148 FabToolbarComponent,
  149 + WidgetsBundleSelectComponent,
148 NospacePipe, 150 NospacePipe,
149 MillisecondsToTimeStringPipe, 151 MillisecondsToTimeStringPipe,
150 EnumToArrayPipe, 152 EnumToArrayPipe,
@@ -226,6 +228,7 @@ import { DashboardSelectComponent } from '@shared/components/dashboard-select.co @@ -226,6 +228,7 @@ import { DashboardSelectComponent } from '@shared/components/dashboard-select.co
226 FabTriggerDirective, 228 FabTriggerDirective,
227 FabActionsDirective, 229 FabActionsDirective,
228 FabToolbarComponent, 230 FabToolbarComponent,
  231 + WidgetsBundleSelectComponent,
229 ValueInputComponent, 232 ValueInputComponent,
230 MatButtonModule, 233 MatButtonModule,
231 MatCheckboxModule, 234 MatCheckboxModule,