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 505
506 506 private alarmsSubscribe() {
507 507 // TODO:
  508 + this.notifyDataLoaded();
508 509 }
509 510
510 511
... ...
... ... @@ -16,7 +16,7 @@
16 16
17 17 import { Injectable } from '@angular/core';
18 18 import { defaultHttpOptions } from './http-utils';
19   -import { Observable } from 'rxjs/index';
  19 +import { Observable, Subject, of, ReplaySubject } from 'rxjs/index';
20 20 import { HttpClient } from '@angular/common/http';
21 21 import { PageLink } from '@shared/models/page/page-link';
22 22 import { PageData } from '@shared/models/page/page-data';
... ... @@ -25,20 +25,57 @@ import { WidgetType, widgetType, WidgetTypeData, widgetTypesData } from '@shared
25 25 import { UtilsService } from '@core/services/utils.service';
26 26 import { TranslateService } from '@ngx-translate/core';
27 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 34 @Injectable({
32 35 providedIn: 'root'
33 36 })
34 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 46 constructor(
37 47 private http: HttpClient,
38 48 private utils: UtilsService,
39 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 81 public getWidgetBundles(pageLink: PageLink, ignoreErrors: boolean = false,
... ... @@ -54,11 +91,26 @@ export class WidgetService {
54 91
55 92 public saveWidgetsBundle(widgetsBundle: WidgetsBundle,
56 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 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 116 public getBundleWidgetTypes(bundleAlias: string, isSystem: boolean,
... ... @@ -73,6 +125,41 @@ export class WidgetService {
73 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 163 public getWidgetTypeById(widgetTypeId: string,
77 164 ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<WidgetType> {
78 165 return this.http.get<WidgetType>(`/api/widgetType/${widgetTypeId}`,
... ... @@ -90,5 +177,55 @@ export class WidgetService {
90 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 84 }
85 85
86 86 public processWidgetException(exception: any): ExceptionData {
87   - const data = this.parseException(exception, -5);
  87 + const data = this.parseException(exception, -6);
88 88 if (this.widgetEditMode) {
89 89 const message: WindowMessage = {
90 90 type: 'widgetException',
... ...
... ... @@ -308,7 +308,13 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
308 308 $event.stopPropagation();
309 309 }
310 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 18 import { DynamicComponentFactoryService } from '@core/services/dynamic-component-factory.service';
19 19 import { WidgetService } from '@core/http/widget.service';
20 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 29 import cssjs from '@core/css/css';
23 30 import { UtilsService } from '@core/services/utils.service';
24 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 34 import { isFunction, isUndefined } from '@core/utils';
32 35 import { TranslateService } from '@ngx-translate/core';
33 36 import { DynamicWidgetComponent } from '@home/components/widget/dynamic-widget.component';
... ... @@ -37,6 +40,9 @@ import { WINDOW } from '@core/services/window.service';
37 40
38 41 import * as tinycolor from 'tinycolor2';
39 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 47 // declare var jQuery: any;
42 48
... ... @@ -53,6 +59,7 @@ export class WidgetComponentService {
53 59
54 60 private missingWidgetType: WidgetInfo;
55 61 private errorWidgetType: WidgetInfo;
  62 + private editingWidgetType: WidgetType;
56 63
57 64 constructor(@Inject(WINDOW) private window: Window,
58 65 private dynamicComponentFactoryService: DynamicComponentFactoryService,
... ... @@ -68,6 +75,15 @@ export class WidgetComponentService {
68 75 this.window.TbFlot = TbFlot;
69 76
70 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 87 this.init();
72 88 }
73 89
... ... @@ -77,6 +93,24 @@ export class WidgetComponentService {
77 93 } else {
78 94 this.missingWidgetType = {...MissingWidgetType};
79 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 114 const initSubject = new ReplaySubject();
81 115 this.init$ = initSubject.asObservable();
82 116 const loadDefaultWidgetInfoTasks = [
... ... @@ -110,7 +144,7 @@ export class WidgetComponentService {
110 144 widgetInfoSubject.complete();
111 145 } else {
112 146 if (this.utils.widgetEditMode) {
113   - // TODO:
  147 + this.loadWidget(this.editingWidgetType, bundleAlias, isSystem, widgetInfoSubject);
114 148 } else {
115 149 const key = this.createWidgetInfoCacheKey(bundleAlias, widgetTypeAlias, isSystem);
116 150 let fetchQueue = this.widgetsInfoFetchQueue.get(key);
... ... @@ -377,4 +411,17 @@ export class WidgetComponentService {
377 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 847 createSubscriptionSubject.error(null);
848 848 }
849 849 );
  850 + this.cd.detectChanges();
850 851 } else if (this.widget.type === widgetType.static) {
851 852 this.loadingData = false;
852 853 createSubscriptionSubject.next();
853 854 createSubscriptionSubject.complete();
  855 + this.cd.detectChanges();
854 856 } else {
855 857 createSubscriptionSubject.next();
856 858 createSubscriptionSubject.complete();
  859 + this.cd.detectChanges();
857 860 }
858 861 return createSubscriptionSubject.asObservable();
859 862 }
... ...
... ... @@ -33,7 +33,7 @@ export interface WidgetsData {
33 33 export interface DashboardCallbacks {
34 34 onEditWidget?: ($event: Event, widget: Widget) => void;
35 35 onExportWidget?: ($event: Event, widget: Widget) => void;
36   - onRemoveWidget?: ($event: Event, widget: Widget) => void;
  36 + onRemoveWidget?: ($event: Event, widget: Widget) => Observable<boolean>;
37 37 onWidgetMouseDown?: ($event: Event, widget: Widget) => void;
38 38 onWidgetClicked?: ($event: Event, widget: Widget) => void;
39 39 prepareDashboardContextMenu?: ($event: Event) => void;
... ...
... ... @@ -43,6 +43,8 @@ import {
43 43 import { ComponentFactory } from '@angular/core';
44 44 import { HttpErrorResponse } from '@angular/common/http';
45 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 49 export interface IWidgetAction {
48 50 name: string;
... ... @@ -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 51 import { FooterFabButtons } from '@shared/components/footer-fab-buttons.component';
52 52 import { IStateController } from '@core/api/widget-api.models';
53 53 import { DashboardUtilsService } from '@core/services/dashboard-utils.service';
  54 +import { DashboardService } from '@core/http/dashboard.service';
54 55
55 56 @Component({
56 57 selector: 'tb-dashboard-page',
... ... @@ -173,7 +174,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
173 174 private dashboardUtils: DashboardUtilsService,
174 175 private authService: AuthService,
175 176 private entityService: EntityService,
176   - private dialogService: DialogService) {
  177 + private dialogService: DialogService,
  178 + private dashboardService: DashboardService) {
177 179 super(store);
178 180
179 181 this.rxSubscriptions.push(this.route.data.subscribe(
... ... @@ -460,6 +462,11 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
460 462 this.setEditMode(!this.isEdit, true);
461 463 }
462 464
  465 + public saveDashboard() {
  466 + this.setEditMode(false, false);
  467 + this.notifyDashboardUpdated();
  468 + }
  469 +
463 470 public openDashboardState(state: string, openRightLayout: boolean) {
464 471 const layoutsData = this.dashboardUtils.getStateLayoutsData(this.dashboard, state);
465 472 if (layoutsData) {
... ... @@ -514,8 +521,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
514 521 private setEditMode(isEdit: boolean, revert: boolean) {
515 522 this.isEdit = isEdit;
516 523 if (this.isEdit) {
517   - // TODO:
518   - // this.dashboardCtx.stateController.preserveState();
  524 + this.dashboardCtx.stateController.preserveState();
519 525 this.prevDashboard = deepClone(this.dashboard);
520 526 } else {
521 527 if (this.widgetEditMode) {
... ... @@ -549,4 +555,20 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
549 555 private entityAliasesUpdated() {
550 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 77 setResizing(layoutVisibilityChanged: boolean) {
78 78 }
79 79
  80 + resetHighlight() {
  81 + }
  82 +
80 83 }
... ...
... ... @@ -17,4 +17,5 @@
17 17 export interface ILayoutController {
18 18 reload();
19 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 19 <div fxFlex fxLayout="column">
20 20 <div fxFlex fxLayout="column" tb-fullscreen [fullscreen]="fullscreen">
21 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 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 26 placeholder="{{ 'widget.title' | translate }}"/>
26 27 </mat-form-field>
27 28 <mat-form-field>
... ... @@ -238,14 +239,15 @@
238 239 </div>
239 240 <div tb-fullscreen [fullscreen]="iFrameFullscreen" style="width: 100%; height: 100%;">
240 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 251 </div>
250 252 </div>
251 253 </div>
... ...
... ... @@ -20,9 +20,9 @@ import { WidgetsBundle } from '@shared/models/widgets-bundle.model';
20 20 import { Store } from '@ngrx/store';
21 21 import { AppState } from '@core/core.state';
22 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 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 26 import { deepClone } from '@core/utils';
27 27 import { HasDirtyFlag } from '@core/guards/confirm-on-exit.guard';
28 28 import { AuthUser } from '@shared/models/user.model';
... ... @@ -40,6 +40,13 @@ import { WindowMessage } from '@shared/models/window-message.model';
40 40 import { ExceptionData } from '@shared/models/error.models';
41 41 import Timeout = NodeJS.Timeout;
42 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 51 @Component({
45 52 selector: 'tb-widget-editor',
... ... @@ -131,25 +138,37 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe
131 138
132 139 saveWidgetTimeout: Timeout;
133 140
  141 + private rxSubscriptions = new Array<Subscription>();
  142 +
134 143 constructor(protected store: Store<AppState>,
135 144 @Inject(WINDOW) private window: Window,
136 145 private route: ActivatedRoute,
  146 + private router: Router,
137 147 private widgetService: WidgetService,
138 148 private hotkeysService: HotkeysService,
139 149 private translate: TranslateService,
140   - private raf: RafService) {
  150 + private raf: RafService,
  151 + private dialog: MatDialog) {
141 152 super(store);
142 153
143 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 165 if (this.authUser.authority === Authority.TENANT_ADMIN) {
147 166 this.isReadOnly = !this.widgetsBundle || this.widgetsBundle.tenantId.id === NULL_UUID;
148 167 } else {
149 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 172 if (this.widgetType) {
154 173 const config = JSON.parse(this.widget.defaultConfig);
155 174 this.widget.defaultConfig = JSON.stringify(config);
... ... @@ -176,6 +195,10 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe
176 195 // @ts-ignore
177 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 204 private initHotKeys(): void {
... ... @@ -448,13 +471,52 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe
448 471 }
449 472
450 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 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 522 applyWidgetScript(): void {
... ...
... ... @@ -73,7 +73,7 @@ export class WidgetsTypesDataResolver implements Resolve<WidgetsData> {
73 73 }
74 74 return result;
75 75 });
76   - const widgetTypes = new Array<Widget>(types.length);
  76 + const widgetTypes = new Array<Widget>();
77 77 let top = 0;
78 78 const lastTop = [0, 0, 0];
79 79 let col = 0;
... ...
... ... @@ -15,7 +15,7 @@
15 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 19 style="text-transform: uppercase; display: flex; z-index: 1;"
20 20 class="tb-absolute-fill">
21 21 <button mat-button *ngIf="!isReadOnly" class="tb-add-new-widget" (click)="addWidgetType($event)">
... ... @@ -27,7 +27,8 @@
27 27 style="text-transform: uppercase; display: flex;"
28 28 class="mat-headline tb-absolute-fill">widgets-bundle.empty</span>
29 29 </section>
30   -<tb-dashboard [aliasController]="aliasController"
  30 +<tb-dashboard #dashboard
  31 + [aliasController]="aliasController"
31 32 [widgets]="widgetsData.widgets"
32 33 [widgetLayouts]="widgetsData.widgetLayouts"
33 34 [isEdit]="false"
... ...
... ... @@ -14,7 +14,7 @@
14 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 18 import { Store } from '@ngrx/store';
19 19 import { AppState } from '@core/core.state';
20 20 import { PageComponent } from '@shared/components/page.component';
... ... @@ -24,13 +24,13 @@ import { WidgetsBundle } from '@shared/models/widgets-bundle.model';
24 24 import { ActivatedRoute, Router } from '@angular/router';
25 25 import { Authority } from '@shared/models/authority.enum';
26 26 import { NULL_UUID } from '@shared/models/id/has-uuid';
27   -import { Observable } from 'rxjs';
  27 +import { Observable, of } from 'rxjs';
28 28 import { Widget, widgetType } from '@app/shared/models/widget.models';
29 29 import { WidgetService } from '@core/http/widget.service';
30   -import { map, share } from 'rxjs/operators';
  30 +import { map, mergeMap, share } from 'rxjs/operators';
31 31 import { DialogService } from '@core/services/dialog.service';
32 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 34 import { IAliasController } from '@app/core/api/widget-api.models';
35 35 import { toWidgetInfo } from '@home/models/widget-component.models';
36 36 import { DummyAliasController } from '@core/api/alias-controller';
... ... @@ -41,6 +41,7 @@ import {
41 41 import { DeviceCredentials } from '@shared/models/device.models';
42 42 import { MatDialog } from '@angular/material/dialog';
43 43 import { SelectWidgetTypeDialogComponent } from '@home/pages/widget/select-widget-type-dialog.component';
  44 +import { TranslateService } from '@ngx-translate/core';
44 45
45 46 @Component({
46 47 selector: 'tb-widget-library',
... ... @@ -86,12 +87,15 @@ export class WidgetLibraryComponent extends PageComponent implements OnInit {
86 87
87 88 aliasController: IAliasController = new DummyAliasController();
88 89
  90 + @ViewChild('dashboard', {static: true}) dashboard: IDashboardComponent;
  91 +
89 92 constructor(protected store: Store<AppState>,
90 93 private route: ActivatedRoute,
91 94 private router: Router,
92 95 private widgetService: WidgetService,
93 96 private dialogService: DialogService,
94   - private dialog: MatDialog) {
  97 + private dialog: MatDialog,
  98 + private translate: TranslateService) {
95 99 super(store);
96 100
97 101 this.authUser = getCurrentAuthUser(store);
... ... @@ -146,11 +150,32 @@ export class WidgetLibraryComponent extends PageComponent implements OnInit {
146 150 this.dialogService.todo();
147 151 }
148 152
149   - removeWidgetType($event: Event, widget: Widget): void {
  153 + removeWidgetType($event: Event, widget: Widget): Observable<boolean> {
150 154 if ($event) {
151 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 23 import { WidgetLibraryComponent } from './widget-library.component';
24 24 import { WidgetEditorComponent } from '@home/pages/widget/widget-editor.component';
25 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 28 @NgModule({
28 29 entryComponents: [
29 30 WidgetsBundleComponent,
30   - SelectWidgetTypeDialogComponent
  31 + SelectWidgetTypeDialogComponent,
  32 + SaveWidgetTypeAsDialogComponent
31 33 ],
32 34 declarations: [
33 35 WidgetsBundleComponent,
34 36 WidgetLibraryComponent,
35 37 WidgetEditorComponent,
36   - SelectWidgetTypeDialogComponent
  38 + SelectWidgetTypeDialogComponent,
  39 + SaveWidgetTypeAsDialogComponent
37 40 ],
38 41 imports: [
39 42 CommonModule,
... ...
... ... @@ -38,6 +38,7 @@ import {
38 38 DashboardSelectPanelComponent,
39 39 DashboardSelectPanelData
40 40 } from './dashboard-select-panel.component';
  41 +import { NULL_UUID } from '@shared/models/id/has-uuid';
41 42
42 43 @Component({
43 44 selector: 'tb-dashboard-select',
... ... @@ -200,7 +201,7 @@ export class DashboardSelectComponent implements ControlValueAccessor, OnInit {
200 201 let dashboardsObservable: Observable<PageData<DashboardInfo>>;
201 202 const authUser = getCurrentAuthUser(this.store);
202 203 if (this.dashboardsScope === 'customer' || authUser.authority === Authority.CUSTOMER_USER) {
203   - if (this.customerId) {
  204 + if (this.customerId && this.customerId !== NULL_UUID) {
204 205 dashboardsObservable = this.dashboardService.getCustomerDashboards(this.customerId, pageLink, false, true);
205 206 } else {
206 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 94 import { FabToolbarComponent, FabActionsDirective, FabTriggerDirective } from './components/fab-toolbar.component';
95 95 import { DashboardSelectPanelComponent } from '@shared/components/dashboard-select-panel.component';
96 96 import { DashboardSelectComponent } from '@shared/components/dashboard-select.component';
  97 +import { WidgetsBundleSelectComponent } from './components/widgets-bundle-select.component';
97 98
98 99 @NgModule({
99 100 providers: [
... ... @@ -145,6 +146,7 @@ import { DashboardSelectComponent } from '@shared/components/dashboard-select.co
145 146 FabTriggerDirective,
146 147 FabActionsDirective,
147 148 FabToolbarComponent,
  149 + WidgetsBundleSelectComponent,
148 150 NospacePipe,
149 151 MillisecondsToTimeStringPipe,
150 152 EnumToArrayPipe,
... ... @@ -226,6 +228,7 @@ import { DashboardSelectComponent } from '@shared/components/dashboard-select.co
226 228 FabTriggerDirective,
227 229 FabActionsDirective,
228 230 FabToolbarComponent,
  231 + WidgetsBundleSelectComponent,
229 232 ValueInputComponent,
230 233 MatButtonModule,
231 234 MatCheckboxModule,
... ...