From 2eb93dac7e0c83b98fa01e63f4409be00ed3295c Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Mon, 23 Sep 2019 20:35:31 +0300 Subject: [PATCH] Widget Editor: Save and save as actions. --- .../src/app/core/api/widget-subscription.ts | 1 + ui-ngx/src/app/core/http/widget.service.ts | 149 ++++++++++++++++- ui-ngx/src/app/core/services/utils.service.ts | 2 +- .../dashboard/dashboard.component.ts | 8 +- .../widget/widget-component.service.ts | 63 ++++++- .../components/widget/widget.component.ts | 3 + .../home/models/dashboard-component.models.ts | 2 +- .../home/models/widget-component.models.ts | 24 +++ .../dashboard/dashboard-page.component.ts | 28 +++- .../layout/dashboard-layout.component.ts | 3 + .../pages/dashboard/layout/layout.models.ts | 1 + .../save-widget-type-as-dialog.component.html | 65 ++++++++ .../save-widget-type-as-dialog.component.ts | 81 +++++++++ .../pages/widget/widget-editor.component.html | 22 +-- .../pages/widget/widget-editor.component.ts | 82 +++++++-- .../widget/widget-library-routing.module.ts | 2 +- .../widget/widget-library.component.html | 5 +- .../pages/widget/widget-library.component.ts | 39 ++++- .../pages/widget/widget-library.module.ts | 7 +- .../components/dashboard-select.component.ts | 3 +- .../widgets-bundle-select.component.html | 34 ++++ .../widgets-bundle-select.component.scss | 91 ++++++++++ .../widgets-bundle-select.component.ts | 156 ++++++++++++++++++ ui-ngx/src/app/shared/shared.module.ts | 3 + 24 files changed, 821 insertions(+), 53 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/pages/widget/save-widget-type-as-dialog.component.html create mode 100644 ui-ngx/src/app/modules/home/pages/widget/save-widget-type-as-dialog.component.ts create mode 100644 ui-ngx/src/app/shared/components/widgets-bundle-select.component.html create mode 100644 ui-ngx/src/app/shared/components/widgets-bundle-select.component.scss create mode 100644 ui-ngx/src/app/shared/components/widgets-bundle-select.component.ts diff --git a/ui-ngx/src/app/core/api/widget-subscription.ts b/ui-ngx/src/app/core/api/widget-subscription.ts index da15591613..f6ff935349 100644 --- a/ui-ngx/src/app/core/api/widget-subscription.ts +++ b/ui-ngx/src/app/core/api/widget-subscription.ts @@ -505,6 +505,7 @@ export class WidgetSubscription implements IWidgetSubscription { private alarmsSubscribe() { // TODO: + this.notifyDataLoaded(); } diff --git a/ui-ngx/src/app/core/http/widget.service.ts b/ui-ngx/src/app/core/http/widget.service.ts index 50739dc6df..306f3e6daa 100644 --- a/ui-ngx/src/app/core/http/widget.service.ts +++ b/ui-ngx/src/app/core/http/widget.service.ts @@ -16,7 +16,7 @@ import { Injectable } from '@angular/core'; import { defaultHttpOptions } from './http-utils'; -import { Observable } from 'rxjs/index'; +import { Observable, Subject, of, ReplaySubject } from 'rxjs/index'; import { HttpClient } from '@angular/common/http'; import { PageLink } from '@shared/models/page/page-link'; import { PageData } from '@shared/models/page/page-data'; @@ -25,20 +25,57 @@ import { WidgetType, widgetType, WidgetTypeData, widgetTypesData } from '@shared import { UtilsService } from '@core/services/utils.service'; import { TranslateService } from '@ngx-translate/core'; import { ResourcesService } from '../services/resources.service'; -import { toWidgetInfo, WidgetInfo } from '@app/modules/home/models/widget-component.models'; -import { map } from 'rxjs/operators'; +import { toWidgetInfo, WidgetInfo, toWidgetType } from '@app/modules/home/models/widget-component.models'; +import { map, tap, mergeMap, filter } from 'rxjs/operators'; +import { WidgetTypeId } from '@shared/models/id/widget-type-id'; +import { NULL_UUID } from '@shared/models/id/has-uuid'; +import { ActivationEnd, Router } from '@angular/router'; @Injectable({ providedIn: 'root' }) export class WidgetService { + private widgetTypeUpdatedSubject = new Subject(); + private widgetsBundleDeletedSubject = new Subject(); + + private allWidgetsBundles: Array; + private systemWidgetsBundles: Array; + private tenantWidgetsBundles: Array; + constructor( private http: HttpClient, private utils: UtilsService, private resources: ResourcesService, - private translate: TranslateService + private translate: TranslateService, + private router: Router ) { + this.router.events.pipe(filter(event => event instanceof ActivationEnd)).subscribe( + () => { + this.invalidateWidgetsBundleCache(); + } + ); + } + + public getAllWidgetsBundles(ignoreErrors: boolean = false, + ignoreLoading: boolean = false): Observable> { + return this.loadWidgetsBundleCache(ignoreErrors, ignoreLoading).pipe( + map(() => this.allWidgetsBundles) + ); + } + + public getSystemWidgetsBundles(ignoreErrors: boolean = false, + ignoreLoading: boolean = false): Observable> { + return this.loadWidgetsBundleCache(ignoreErrors, ignoreLoading).pipe( + map(() => this.systemWidgetsBundles) + ); + } + + public getTenantWidgetsBundles(ignoreErrors: boolean = false, + ignoreLoading: boolean = false): Observable> { + return this.loadWidgetsBundleCache(ignoreErrors, ignoreLoading).pipe( + map(() => this.tenantWidgetsBundles) + ); } public getWidgetBundles(pageLink: PageLink, ignoreErrors: boolean = false, @@ -54,11 +91,26 @@ export class WidgetService { public saveWidgetsBundle(widgetsBundle: WidgetsBundle, ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable { - return this.http.post('/api/widgetsBundle', widgetsBundle, defaultHttpOptions(ignoreLoading, ignoreErrors)); + return this.http.post('/api/widgetsBundle', widgetsBundle, + defaultHttpOptions(ignoreLoading, ignoreErrors)).pipe( + tap(() => { + this.invalidateWidgetsBundleCache(); + }) + ); } public deleteWidgetsBundle(widgetsBundleId: string, ignoreErrors: boolean = false, ignoreLoading: boolean = false) { - return this.http.delete(`/api/widgetsBundle/${widgetsBundleId}`, defaultHttpOptions(ignoreLoading, ignoreErrors)); + return this.getWidgetsBundle(widgetsBundleId, ignoreErrors, ignoreLoading).pipe( + mergeMap((widgetsBundle) => { + return this.http.delete(`/api/widgetsBundle/${widgetsBundleId}`, + defaultHttpOptions(ignoreLoading, ignoreErrors)).pipe( + tap(() => { + this.invalidateWidgetsBundleCache(); + this.widgetsBundleDeletedSubject.next(widgetsBundle); + }) + ); + } + )); } public getBundleWidgetTypes(bundleAlias: string, isSystem: boolean, @@ -73,6 +125,41 @@ export class WidgetService { defaultHttpOptions(ignoreLoading, ignoreErrors)); } + public saveWidgetType(widgetInfo: WidgetInfo, + id: WidgetTypeId, + bundleAlias: string, + ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable { + const widgetTypeInstance = toWidgetType(widgetInfo, id, undefined, bundleAlias); + return this.http.post('/api/widgetType', widgetTypeInstance, + defaultHttpOptions(ignoreLoading, ignoreErrors)).pipe( + tap((savedWidgetType) => { + this.widgetTypeUpdatedSubject.next(savedWidgetType); + })); + } + + public saveImportedWidgetType(widgetTypeInstance: WidgetType, + ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable { + return this.http.post('/api/widgetType', widgetTypeInstance, + defaultHttpOptions(ignoreLoading, ignoreErrors)).pipe( + tap((savedWidgetType) => { + this.widgetTypeUpdatedSubject.next(savedWidgetType); + })); + } + + public deleteWidgetType(bundleAlias: string, widgetTypeAlias: string, isSystem: boolean, + ignoreErrors: boolean = false, ignoreLoading: boolean = false) { + return this.getWidgetType(bundleAlias, widgetTypeAlias, isSystem, ignoreErrors, ignoreLoading).pipe( + mergeMap((widgetTypeInstance) => { + return this.http.delete(`/api/widgetType/${widgetTypeInstance.id.id}`, + defaultHttpOptions(ignoreLoading, ignoreErrors)).pipe( + tap(() => { + this.widgetTypeUpdatedSubject.next(widgetTypeInstance); + }) + ); + } + )); + } + public getWidgetTypeById(widgetTypeId: string, ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable { return this.http.get(`/api/widgetType/${widgetTypeId}`, @@ -90,5 +177,55 @@ export class WidgetService { return widgetInfo; }) ); + } + + public onWidgetTypeUpdated(): Observable { + return this.widgetTypeUpdatedSubject.asObservable(); + } + + public onWidgetBundleDeleted(): Observable { + return this.widgetsBundleDeletedSubject.asObservable(); + } + + private loadWidgetsBundleCache(ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable { + if (!this.allWidgetsBundles) { + const loadWidgetsBundleCacheSubject = new ReplaySubject(); + this.http.get>('/api/widgetsBundles', + defaultHttpOptions(ignoreLoading, ignoreErrors)).subscribe( + (allWidgetsBundles) => { + this.allWidgetsBundles = allWidgetsBundles; + this.systemWidgetsBundles = new Array(); + this.tenantWidgetsBundles = new Array(); + this.allWidgetsBundles = this.allWidgetsBundles.sort((wb1, wb2) => { + let res = wb1.title.localeCompare(wb2.title); + if (res === 0) { + res = wb2.createdTime - wb1.createdTime; + } + return res; + }); + this.allWidgetsBundles.forEach((widgetsBundle) => { + if (widgetsBundle.tenantId.id === NULL_UUID) { + this.systemWidgetsBundles.push(widgetsBundle); + } else { + this.tenantWidgetsBundles.push(widgetsBundle); + } + }); + loadWidgetsBundleCacheSubject.next(); + loadWidgetsBundleCacheSubject.complete(); + }, + () => { + loadWidgetsBundleCacheSubject.error(null); + }); + return loadWidgetsBundleCacheSubject.asObservable(); + } else { + return of(null); } + } + + private invalidateWidgetsBundleCache() { + this.allWidgetsBundles = undefined; + this.systemWidgetsBundles = undefined; + this.tenantWidgetsBundles = undefined; + } + } diff --git a/ui-ngx/src/app/core/services/utils.service.ts b/ui-ngx/src/app/core/services/utils.service.ts index 1e33f84625..c3b1f2896c 100644 --- a/ui-ngx/src/app/core/services/utils.service.ts +++ b/ui-ngx/src/app/core/services/utils.service.ts @@ -84,7 +84,7 @@ export class UtilsService { } public processWidgetException(exception: any): ExceptionData { - const data = this.parseException(exception, -5); + const data = this.parseException(exception, -6); if (this.widgetEditMode) { const message: WindowMessage = { type: 'widgetException', diff --git a/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.ts b/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.ts index 92c611458e..d1c5fe976e 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.ts @@ -308,7 +308,13 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo $event.stopPropagation(); } if (this.isRemoveActionEnabled && this.callbacks && this.callbacks.onRemoveWidget) { - this.callbacks.onRemoveWidget($event, widget.widget); + this.callbacks.onRemoveWidget($event, widget.widget).subscribe( + (result) => { + if (result) { + this.dashboardWidgets.removeWidget(widget.widget); + } + } + ); } } diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts b/ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts index 7271dff81a..7a2007274a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts @@ -18,16 +18,19 @@ import { Inject, Injectable } from '@angular/core'; import { DynamicComponentFactoryService } from '@core/services/dynamic-component-factory.service'; import { WidgetService } from '@core/http/widget.service'; import { forkJoin, Observable, of, ReplaySubject, Subject, throwError } from 'rxjs'; -import { WidgetInfo, MissingWidgetType, toWidgetInfo, WidgetTypeInstance, ErrorWidgetType } from '@home/models/widget-component.models'; +import { + ErrorWidgetType, + MissingWidgetType, + toWidgetInfo, + toWidgetType, + WidgetInfo, + WidgetTypeInstance +} from '@home/models/widget-component.models'; import cssjs from '@core/css/css'; import { UtilsService } from '@core/services/utils.service'; import { ResourcesService } from '@core/services/resources.service'; -import { - widgetActionSources, - WidgetControllerDescriptor, - WidgetType -} from '@shared/models/widget.models'; -import { catchError, switchMap, map, mergeMap } from 'rxjs/operators'; +import { widgetActionSources, WidgetControllerDescriptor, WidgetType } from '@shared/models/widget.models'; +import { catchError, map, mergeMap, switchMap } from 'rxjs/operators'; import { isFunction, isUndefined } from '@core/utils'; import { TranslateService } from '@ngx-translate/core'; import { DynamicWidgetComponent } from '@home/components/widget/dynamic-widget.component'; @@ -37,6 +40,9 @@ import { WINDOW } from '@core/services/window.service'; import * as tinycolor from 'tinycolor2'; import { TbFlot } from './lib/flot-widget'; +import { NULL_UUID } from '@shared/models/id/has-uuid'; +import { WidgetTypeId } from '@app/shared/models/id/widget-type-id'; +import { TenantId } from '@app/shared/models/id/tenant-id'; // declare var jQuery: any; @@ -53,6 +59,7 @@ export class WidgetComponentService { private missingWidgetType: WidgetInfo; private errorWidgetType: WidgetInfo; + private editingWidgetType: WidgetType; constructor(@Inject(WINDOW) private window: Window, private dynamicComponentFactoryService: DynamicComponentFactoryService, @@ -68,6 +75,15 @@ export class WidgetComponentService { this.window.TbFlot = TbFlot; this.cssParser.testMode = false; + + this.widgetService.onWidgetTypeUpdated().subscribe((widgetType) => { + this.deleteWidgetInfoFromCache(widgetType.bundleAlias, widgetType.alias, widgetType.tenantId.id === NULL_UUID); + }); + + this.widgetService.onWidgetBundleDeleted().subscribe((widgetsBundle) => { + this.deleteWidgetsBundleFromCache(widgetsBundle.alias, widgetsBundle.tenantId.id === NULL_UUID); + }); + this.init(); } @@ -77,6 +93,24 @@ export class WidgetComponentService { } else { this.missingWidgetType = {...MissingWidgetType}; this.errorWidgetType = {...ErrorWidgetType}; + if (this.utils.widgetEditMode) { + this.editingWidgetType = toWidgetType( + { + widgetName: this.utils.editWidgetInfo.widgetName, + alias: 'customWidget', + type: this.utils.editWidgetInfo.type, + sizeX: this.utils.editWidgetInfo.sizeX, + sizeY: this.utils.editWidgetInfo.sizeY, + resources: this.utils.editWidgetInfo.resources, + templateHtml: this.utils.editWidgetInfo.templateHtml, + templateCss: this.utils.editWidgetInfo.templateCss, + controllerScript: this.utils.editWidgetInfo.controllerScript, + settingsSchema: this.utils.editWidgetInfo.settingsSchema, + dataKeySettingsSchema: this.utils.editWidgetInfo.dataKeySettingsSchema, + defaultConfig: this.utils.editWidgetInfo.defaultConfig + }, new WidgetTypeId('1'), new TenantId( NULL_UUID ), 'customWidgetBundle' + ); + } const initSubject = new ReplaySubject(); this.init$ = initSubject.asObservable(); const loadDefaultWidgetInfoTasks = [ @@ -110,7 +144,7 @@ export class WidgetComponentService { widgetInfoSubject.complete(); } else { if (this.utils.widgetEditMode) { - // TODO: + this.loadWidget(this.editingWidgetType, bundleAlias, isSystem, widgetInfoSubject); } else { const key = this.createWidgetInfoCacheKey(bundleAlias, widgetTypeAlias, isSystem); let fetchQueue = this.widgetsInfoFetchQueue.get(key); @@ -377,4 +411,17 @@ export class WidgetComponentService { this.widgetsInfoInMemoryCache.set(key, widgetInfo); } + private deleteWidgetInfoFromCache(bundleAlias: string, widgetTypeAlias: string, isSystem: boolean) { + const key = this.createWidgetInfoCacheKey(bundleAlias, widgetTypeAlias, isSystem); + this.widgetsInfoInMemoryCache.delete(key); + } + + private deleteWidgetsBundleFromCache(bundleAlias: string, isSystem: boolean) { + const key = (isSystem ? 'sys_' : '') + bundleAlias; + this.widgetsInfoInMemoryCache.forEach((widgetInfo, cacheKey) => { + if (cacheKey.startsWith(key)) { + this.widgetsInfoInMemoryCache.delete(cacheKey); + } + }); + } } diff --git a/ui-ngx/src/app/modules/home/components/widget/widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/widget.component.ts index 0097c56238..68850525b0 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget.component.ts @@ -847,13 +847,16 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI createSubscriptionSubject.error(null); } ); + this.cd.detectChanges(); } else if (this.widget.type === widgetType.static) { this.loadingData = false; createSubscriptionSubject.next(); createSubscriptionSubject.complete(); + this.cd.detectChanges(); } else { createSubscriptionSubject.next(); createSubscriptionSubject.complete(); + this.cd.detectChanges(); } return createSubscriptionSubject.asObservable(); } diff --git a/ui-ngx/src/app/modules/home/models/dashboard-component.models.ts b/ui-ngx/src/app/modules/home/models/dashboard-component.models.ts index 66d5a340fd..6d5cde2904 100644 --- a/ui-ngx/src/app/modules/home/models/dashboard-component.models.ts +++ b/ui-ngx/src/app/modules/home/models/dashboard-component.models.ts @@ -33,7 +33,7 @@ export interface WidgetsData { export interface DashboardCallbacks { onEditWidget?: ($event: Event, widget: Widget) => void; onExportWidget?: ($event: Event, widget: Widget) => void; - onRemoveWidget?: ($event: Event, widget: Widget) => void; + onRemoveWidget?: ($event: Event, widget: Widget) => Observable; onWidgetMouseDown?: ($event: Event, widget: Widget) => void; onWidgetClicked?: ($event: Event, widget: Widget) => void; prepareDashboardContextMenu?: ($event: Event) => void; diff --git a/ui-ngx/src/app/modules/home/models/widget-component.models.ts b/ui-ngx/src/app/modules/home/models/widget-component.models.ts index 5071cd8509..76b70285e4 100644 --- a/ui-ngx/src/app/modules/home/models/widget-component.models.ts +++ b/ui-ngx/src/app/modules/home/models/widget-component.models.ts @@ -43,6 +43,8 @@ import { import { ComponentFactory } from '@angular/core'; import { HttpErrorResponse } from '@angular/common/http'; import { RafService } from '@core/services/raf.service'; +import { WidgetTypeId } from '@shared/models/id/widget-type-id'; +import { TenantId } from '@shared/models/id/tenant-id'; export interface IWidgetAction { name: string; @@ -192,3 +194,25 @@ export function toWidgetInfo(widgetTypeEntity: WidgetType): WidgetInfo { }; } +export function toWidgetType(widgetInfo: WidgetInfo, id: WidgetTypeId, tenantId: TenantId, bundleAlias: string): WidgetType { + const descriptor: WidgetTypeDescriptor = { + type: widgetInfo.type, + sizeX: widgetInfo.sizeX, + sizeY: widgetInfo.sizeY, + resources: widgetInfo.resources, + templateHtml: widgetInfo.templateHtml, + templateCss: widgetInfo.templateCss, + controllerScript: widgetInfo.controllerScript, + settingsSchema: widgetInfo.settingsSchema, + dataKeySettingsSchema: widgetInfo.dataKeySettingsSchema, + defaultConfig: widgetInfo.defaultConfig + }; + return { + id, + tenantId, + bundleAlias, + alias: widgetInfo.alias, + name: widgetInfo.widgetName, + descriptor + }; +} diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-page.component.ts b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-page.component.ts index 9b68478506..c9d98a4d59 100644 --- a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-page.component.ts +++ b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-page.component.ts @@ -51,6 +51,7 @@ import { Subscription } from 'rxjs'; import { FooterFabButtons } from '@shared/components/footer-fab-buttons.component'; import { IStateController } from '@core/api/widget-api.models'; import { DashboardUtilsService } from '@core/services/dashboard-utils.service'; +import { DashboardService } from '@core/http/dashboard.service'; @Component({ selector: 'tb-dashboard-page', @@ -173,7 +174,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC private dashboardUtils: DashboardUtilsService, private authService: AuthService, private entityService: EntityService, - private dialogService: DialogService) { + private dialogService: DialogService, + private dashboardService: DashboardService) { super(store); this.rxSubscriptions.push(this.route.data.subscribe( @@ -460,6 +462,11 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC this.setEditMode(!this.isEdit, true); } + public saveDashboard() { + this.setEditMode(false, false); + this.notifyDashboardUpdated(); + } + public openDashboardState(state: string, openRightLayout: boolean) { const layoutsData = this.dashboardUtils.getStateLayoutsData(this.dashboard, state); if (layoutsData) { @@ -514,8 +521,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC private setEditMode(isEdit: boolean, revert: boolean) { this.isEdit = isEdit; if (this.isEdit) { - // TODO: - // this.dashboardCtx.stateController.preserveState(); + this.dashboardCtx.stateController.preserveState(); this.prevDashboard = deepClone(this.dashboard); } else { if (this.widgetEditMode) { @@ -549,4 +555,20 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC private entityAliasesUpdated() { this.dashboardCtx.aliasController.updateEntityAliases(this.dashboard.configuration.entityAliases); } + + private notifyDashboardUpdated() { + if (this.widgetEditMode) { + const widget = this.layouts.main.layoutCtx.widgets[0]; + const layout = this.layouts.main.layoutCtx.widgetLayouts[widget.id]; + widget.sizeX = layout.sizeX; + widget.sizeY = layout.sizeY; + const message: WindowMessage = { + type: 'widgetEditUpdated', + data: widget + }; + this.window.parent.postMessage(JSON.stringify(message), '*'); + } else { + this.dashboardService.saveDashboard(this.dashboard); + } + } } diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/layout/dashboard-layout.component.ts b/ui-ngx/src/app/modules/home/pages/dashboard/layout/dashboard-layout.component.ts index 8c0ba8671b..d571f05fa8 100644 --- a/ui-ngx/src/app/modules/home/pages/dashboard/layout/dashboard-layout.component.ts +++ b/ui-ngx/src/app/modules/home/pages/dashboard/layout/dashboard-layout.component.ts @@ -77,4 +77,7 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo setResizing(layoutVisibilityChanged: boolean) { } + resetHighlight() { + } + } diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/layout/layout.models.ts b/ui-ngx/src/app/modules/home/pages/dashboard/layout/layout.models.ts index 86b224fe0a..131a7c2a18 100644 --- a/ui-ngx/src/app/modules/home/pages/dashboard/layout/layout.models.ts +++ b/ui-ngx/src/app/modules/home/pages/dashboard/layout/layout.models.ts @@ -17,4 +17,5 @@ export interface ILayoutController { reload(); setResizing(layoutVisibilityChanged: boolean); + resetHighlight(); } diff --git a/ui-ngx/src/app/modules/home/pages/widget/save-widget-type-as-dialog.component.html b/ui-ngx/src/app/modules/home/pages/widget/save-widget-type-as-dialog.component.html new file mode 100644 index 0000000000..768f0b4077 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/widget/save-widget-type-as-dialog.component.html @@ -0,0 +1,65 @@ + +
+ +

widget.save-widget-type-as

+ + +
+ + +
+
+
+ widget.save-widget-type-as-text + + widget.title + + + {{ 'widget.title-required' | translate }} + + + + +
+
+
+ + + +
+
diff --git a/ui-ngx/src/app/modules/home/pages/widget/save-widget-type-as-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/widget/save-widget-type-as-dialog.component.ts new file mode 100644 index 0000000000..e7a874e7cc --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/widget/save-widget-type-as-dialog.component.ts @@ -0,0 +1,81 @@ +/// +/// Copyright © 2016-2019 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, OnInit } from '@angular/core'; +import { MatDialogRef } from '@angular/material'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { DialogComponent } from '@shared/components/dialog.component'; +import { Router } from '@angular/router'; +import { WidgetsBundle } from '@shared/models/widgets-bundle.model'; +import { getCurrentAuthUser } from '@core/auth/auth.selectors'; +import { Authority } from '@shared/models/authority.enum'; + +export interface SaveWidgetTypeAsDialogResult { + widgetName: string; + bundleId: string; + bundleAlias: string; +} + +@Component({ + selector: 'tb-save-widget-type-as-dialog', + templateUrl: './save-widget-type-as-dialog.component.html', + styleUrls: [] +}) +export class SaveWidgetTypeAsDialogComponent extends + DialogComponent implements OnInit { + + saveWidgetTypeAsFormGroup: FormGroup; + + bundlesScope: string; + + constructor(protected store: Store, + protected router: Router, + public dialogRef: MatDialogRef, + public fb: FormBuilder) { + super(store, router, dialogRef); + + const authUser = getCurrentAuthUser(store); + if (authUser.authority === Authority.TENANT_ADMIN) { + this.bundlesScope = 'tenant'; + } else { + this.bundlesScope = 'system'; + } + } + + ngOnInit(): void { + this.saveWidgetTypeAsFormGroup = this.fb.group({ + title: [null, [Validators.required]], + widgetsBundle: [null, [Validators.required]] + }); + } + + cancel(): void { + this.dialogRef.close(null); + } + + saveAs(): void { + const widgetName: string = this.saveWidgetTypeAsFormGroup.get('title').value; + const widgetsBundle: WidgetsBundle = this.saveWidgetTypeAsFormGroup.get('widgetsBundle').value; + const result: SaveWidgetTypeAsDialogResult = { + widgetName, + bundleId: widgetsBundle.id.id, + bundleAlias: widgetsBundle.alias + }; + this.dialogRef.close(result); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.html b/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.html index 224490a5a4..3031652eae 100644 --- a/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.html +++ b/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.html @@ -19,9 +19,10 @@
- + - @@ -238,14 +239,15 @@
- +
+ +
diff --git a/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.ts b/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.ts index ef07407bd3..bdee67f4e9 100644 --- a/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.ts +++ b/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.ts @@ -20,9 +20,9 @@ import { WidgetsBundle } from '@shared/models/widgets-bundle.model'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { WidgetService } from '@core/http/widget.service'; -import { WidgetInfo } from '@home/models/widget-component.models'; +import { toWidgetInfo, WidgetInfo } from '@home/models/widget-component.models'; import { WidgetConfig, widgetType, WidgetType, widgetTypesData, Widget } from '@shared/models/widget.models'; -import { ActivatedRoute } from '@angular/router'; +import { ActivatedRoute, Router } from '@angular/router'; import { deepClone } from '@core/utils'; import { HasDirtyFlag } from '@core/guards/confirm-on-exit.guard'; import { AuthUser } from '@shared/models/user.model'; @@ -40,6 +40,13 @@ import { WindowMessage } from '@shared/models/window-message.model'; import { ExceptionData } from '@shared/models/error.models'; import Timeout = NodeJS.Timeout; import { ActionNotificationHide, ActionNotificationShow } from '@core/notification/notification.actions'; +import { MatDialog } from '@angular/material/dialog'; +import { SelectWidgetTypeDialogComponent } from '@home/pages/widget/select-widget-type-dialog.component'; +import { + SaveWidgetTypeAsDialogComponent, + SaveWidgetTypeAsDialogResult +} from '@home/pages/widget/save-widget-type-as-dialog.component'; +import { Subscription } from 'rxjs'; @Component({ selector: 'tb-widget-editor', @@ -131,25 +138,37 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe saveWidgetTimeout: Timeout; + private rxSubscriptions = new Array(); + constructor(protected store: Store, @Inject(WINDOW) private window: Window, private route: ActivatedRoute, + private router: Router, private widgetService: WidgetService, private hotkeysService: HotkeysService, private translate: TranslateService, - private raf: RafService) { + private raf: RafService, + private dialog: MatDialog) { super(store); this.authUser = getCurrentAuthUser(store); - this.widgetsBundle = this.route.snapshot.data.widgetsBundle; + this.rxSubscriptions.push(this.route.data.subscribe( + (data) => { + this.init(data); + } + )); + } + + private init(data: any) { + this.widgetsBundle = data.widgetsBundle; if (this.authUser.authority === Authority.TENANT_ADMIN) { this.isReadOnly = !this.widgetsBundle || this.widgetsBundle.tenantId.id === NULL_UUID; } else { this.isReadOnly = this.authUser.authority !== Authority.SYS_ADMIN; } - this.widgetType = this.route.snapshot.data.widgetEditorData.widgetType; - this.widget = this.route.snapshot.data.widgetEditorData.widget; + this.widgetType = data.widgetEditorData.widgetType; + this.widget = data.widgetEditorData.widget; if (this.widgetType) { const config = JSON.parse(this.widget.defaultConfig); this.widget.defaultConfig = JSON.stringify(config); @@ -176,6 +195,10 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe // @ts-ignore removeResizeListener(resizeListener.element, resizeListener.resizeListener); }); + this.rxSubscriptions.forEach((subscription) => { + subscription.unsubscribe(); + }); + this.rxSubscriptions.length = 0; } private initHotKeys(): void { @@ -448,13 +471,52 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe } private commitSaveWidget() { - // TODO: - this.saveWidgetPending = false; + const id = (this.widgetType && this.widgetType.id) ? this.widgetType.id : undefined; + this.widgetService.saveWidgetType(this.widget, id, this.widgetsBundle.alias).subscribe( + (widgetTypeInstance) => { + this.setWidgetType(widgetTypeInstance); + this.saveWidgetPending = false; + this.store.dispatch(new ActionNotificationShow( + {message: this.translate.instant('widget.widget-saved'), type: 'success', duration: 500})); + }, + () => { + this.saveWidgetPending = false; + } + ); } private commitSaveWidgetAs() { - // TODO: - this.saveWidgetAsPending = false; + this.dialog.open(SaveWidgetTypeAsDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'] + }).afterClosed().subscribe( + (saveWidgetAsData) => { + if (saveWidgetAsData) { + this.widget.widgetName = saveWidgetAsData.widgetName; + this.widget.alias = undefined; + const config = JSON.parse(this.widget.defaultConfig); + config.title = this.widget.widgetName; + this.widget.defaultConfig = JSON.stringify(config); + this.isDirty = false; + this.widgetService.saveWidgetType(this.widget, undefined, saveWidgetAsData.bundleAlias).subscribe( + (widgetTypeInstance) => { + this.router.navigateByUrl(`/widgets-bundles/${saveWidgetAsData.bundleId}/widgetTypes/${widgetTypeInstance.id.id}`); + } + ); + } + this.saveWidgetAsPending = false; + } + ); + } + + private setWidgetType(widgetTypeInstance: WidgetType) { + this.widgetType = widgetTypeInstance; + this.widget = toWidgetInfo(this.widgetType); + const config = JSON.parse(this.widget.defaultConfig); + this.widget.defaultConfig = JSON.stringify(config); + this.origWidget = deepClone(this.widget); + this.isDirty = false; } applyWidgetScript(): void { diff --git a/ui-ngx/src/app/modules/home/pages/widget/widget-library-routing.module.ts b/ui-ngx/src/app/modules/home/pages/widget/widget-library-routing.module.ts index 966c3feee7..96c9de20a4 100644 --- a/ui-ngx/src/app/modules/home/pages/widget/widget-library-routing.module.ts +++ b/ui-ngx/src/app/modules/home/pages/widget/widget-library-routing.module.ts @@ -73,7 +73,7 @@ export class WidgetsTypesDataResolver implements Resolve { } return result; }); - const widgetTypes = new Array(types.length); + const widgetTypes = new Array(); let top = 0; const lastTop = [0, 0, 0]; let col = 0; diff --git a/ui-ngx/src/app/modules/home/pages/widget/widget-library.component.html b/ui-ngx/src/app/modules/home/pages/widget/widget-library.component.html index 7d23708ed0..addf70d069 100644 --- a/ui-ngx/src/app/modules/home/pages/widget/widget-library.component.html +++ b/ui-ngx/src/app/modules/home/pages/widget/widget-library.component.html @@ -15,7 +15,7 @@ limitations under the License. --> -
-, private route: ActivatedRoute, private router: Router, private widgetService: WidgetService, private dialogService: DialogService, - private dialog: MatDialog) { + private dialog: MatDialog, + private translate: TranslateService) { super(store); this.authUser = getCurrentAuthUser(store); @@ -146,11 +150,32 @@ export class WidgetLibraryComponent extends PageComponent implements OnInit { this.dialogService.todo(); } - removeWidgetType($event: Event, widget: Widget): void { + removeWidgetType($event: Event, widget: Widget): Observable { if ($event) { $event.stopPropagation(); } - this.dialogService.todo(); + return this.dialogService.confirm( + this.translate.instant('widget.remove-widget-type-title', {widgetName: widget.config.title}), + this.translate.instant('widget.remove-widget-type-text'), + this.translate.instant('action.no'), + this.translate.instant('action.yes'), + ).pipe( + mergeMap((result) => { + if (result) { + return this.widgetService.deleteWidgetType(widget.bundleAlias, widget.typeAlias, widget.isSystemType); + } else { + return of(false); + } + }), + map((result) => { + if (result !== false) { + this.widgetsData.widgets.splice(this.widgetsData.widgets.indexOf(widget), 1); + return true; + } else { + return false; + } + } + )); } } diff --git a/ui-ngx/src/app/modules/home/pages/widget/widget-library.module.ts b/ui-ngx/src/app/modules/home/pages/widget/widget-library.module.ts index 25ff3a9b78..46f082e455 100644 --- a/ui-ngx/src/app/modules/home/pages/widget/widget-library.module.ts +++ b/ui-ngx/src/app/modules/home/pages/widget/widget-library.module.ts @@ -23,17 +23,20 @@ import {HomeComponentsModule} from '@modules/home/components/home-components.mod import { WidgetLibraryComponent } from './widget-library.component'; import { WidgetEditorComponent } from '@home/pages/widget/widget-editor.component'; import { SelectWidgetTypeDialogComponent } from '@home/pages/widget/select-widget-type-dialog.component'; +import { SaveWidgetTypeAsDialogComponent } from './save-widget-type-as-dialog.component'; @NgModule({ entryComponents: [ WidgetsBundleComponent, - SelectWidgetTypeDialogComponent + SelectWidgetTypeDialogComponent, + SaveWidgetTypeAsDialogComponent ], declarations: [ WidgetsBundleComponent, WidgetLibraryComponent, WidgetEditorComponent, - SelectWidgetTypeDialogComponent + SelectWidgetTypeDialogComponent, + SaveWidgetTypeAsDialogComponent ], imports: [ CommonModule, diff --git a/ui-ngx/src/app/shared/components/dashboard-select.component.ts b/ui-ngx/src/app/shared/components/dashboard-select.component.ts index b9d56ac080..341dd9e34e 100644 --- a/ui-ngx/src/app/shared/components/dashboard-select.component.ts +++ b/ui-ngx/src/app/shared/components/dashboard-select.component.ts @@ -38,6 +38,7 @@ import { DashboardSelectPanelComponent, DashboardSelectPanelData } from './dashboard-select-panel.component'; +import { NULL_UUID } from '@shared/models/id/has-uuid'; @Component({ selector: 'tb-dashboard-select', @@ -200,7 +201,7 @@ export class DashboardSelectComponent implements ControlValueAccessor, OnInit { let dashboardsObservable: Observable>; const authUser = getCurrentAuthUser(this.store); if (this.dashboardsScope === 'customer' || authUser.authority === Authority.CUSTOMER_USER) { - if (this.customerId) { + if (this.customerId && this.customerId !== NULL_UUID) { dashboardsObservable = this.dashboardService.getCustomerDashboards(this.customerId, pageLink, false, true); } else { dashboardsObservable = of(emptyPageData()); diff --git a/ui-ngx/src/app/shared/components/widgets-bundle-select.component.html b/ui-ngx/src/app/shared/components/widgets-bundle-select.component.html new file mode 100644 index 0000000000..d905564bf4 --- /dev/null +++ b/ui-ngx/src/app/shared/components/widgets-bundle-select.component.html @@ -0,0 +1,34 @@ + + + + + +
+ {{widgetsBundle.title}} + widgets-bundle.system +
+
+
+
diff --git a/ui-ngx/src/app/shared/components/widgets-bundle-select.component.scss b/ui-ngx/src/app/shared/components/widgets-bundle-select.component.scss new file mode 100644 index 0000000000..313d243f8c --- /dev/null +++ b/ui-ngx/src/app/shared/components/widgets-bundle-select.component.scss @@ -0,0 +1,91 @@ +/** + * Copyright © 2016-2019 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +tb-widgets-bundle-select { + mat-select { + margin: 0; + } + + .tb-bundle-item { + height: 24px; + line-height: 24px; + } +} + +.tb-widgets-bundle-select { + .tb-bundle-item { + height: 48px; + line-height: 48px; + } +} + +tb-widgets-bundle-select, +.tb-widgets-bundle-select { + .mat-select-value-text { + display: block; + width: 100%; + } + + .tb-bundle-item { + display: inline-block; + width: 100%; + + span { + display: inline-block; + vertical-align: middle; + } + + .tb-bundle-system { + float: right; + font-size: .8rem; + opacity: .8; + } + } + + mat-option { + height: auto !important; + white-space: normal !important; + } +} + +mat-toolbar { + tb-widgets-bundle-select { + mat-select { + background: rgba(255, 255, 255, .2); + padding: 5px 20px; + + .mat-select-value-text { + font-size: 1.2rem; + color: #fff; + + span:first-child::after { + color: #fff; + } + } + + .mat-select-value.mat-select-placeholder { + color: #fff; + opacity: .8; + } + } + + mat-select.ng-invalid.ng-touched { + .mat-select-value-text { + color: #fff !important; + } + } + } +} diff --git a/ui-ngx/src/app/shared/components/widgets-bundle-select.component.ts b/ui-ngx/src/app/shared/components/widgets-bundle-select.component.ts new file mode 100644 index 0000000000..80b4934633 --- /dev/null +++ b/ui-ngx/src/app/shared/components/widgets-bundle-select.component.ts @@ -0,0 +1,156 @@ +/// +/// Copyright © 2016-2019 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, Input, OnChanges, OnInit, ViewEncapsulation, SimpleChanges } from '@angular/core'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { Observable } from 'rxjs'; +import { share, tap } from 'rxjs/operators'; +import { Store } from '@ngrx/store'; +import { AppState } from '@app/core/core.state'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { WidgetsBundle } from '@shared/models/widgets-bundle.model'; +import { WidgetService } from '@core/http/widget.service'; +import { isDefined } from '@core/utils'; +import { NULL_UUID } from '@shared/models/id/has-uuid'; + +@Component({ + selector: 'tb-widgets-bundle-select', + templateUrl: './widgets-bundle-select.component.html', + styleUrls: ['./widgets-bundle-select.component.scss'], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => WidgetsBundleSelectComponent), + multi: true + }], + encapsulation: ViewEncapsulation.None +}) +export class WidgetsBundleSelectComponent implements ControlValueAccessor, OnInit, OnChanges { + + @Input() + bundlesScope: 'system' | 'tenant'; + + @Input() + selectFirstBundle: boolean; + + @Input() + selectBundleAlias: string; + + private requiredValue: boolean; + get required(): boolean { + return this.requiredValue; + } + @Input() + set required(value: boolean) { + this.requiredValue = coerceBooleanProperty(value); + } + + @Input() + disabled: boolean; + + widgetsBundles$: Observable>; + + widgetsBundles: Array; + + widgetsBundle: WidgetsBundle | null; + + private propagateChange = (v: any) => { }; + + constructor(private store: Store, + private widgetService: WidgetService) { + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + ngOnInit() { + this.widgetsBundles$ = this.getWidgetsBundles().pipe( + tap((widgetsBundles) => { + this.widgetsBundles = widgetsBundles; + if (this.selectFirstBundle) { + if (widgetsBundles.length > 0) { + if (this.widgetsBundle !== widgetsBundles[0]) { + this.widgetsBundle = widgetsBundles[0]; + this.updateView(); + } else if (isDefined(this.selectBundleAlias)) { + this.selectWidgetsBundleByAlias(this.selectBundleAlias); + } + } + } + }), + share() + ); + } + + ngOnChanges(changes: SimpleChanges): void { + for (const propName of Object.keys(changes)) { + const change = changes[propName]; + if (!change.firstChange && change.currentValue !== change.previousValue) { + if (propName === 'selectBundleAlias') { + this.selectWidgetsBundleByAlias(this.selectBundleAlias); + } + } + } + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + } + + writeValue(value: WidgetsBundle | null): void { + this.widgetsBundle = value; + } + + widgetsBundleChanged() { + this.updateView(); + } + + isSystem(item: WidgetsBundle) { + return item && item.tenantId.id === NULL_UUID; + } + + private selectWidgetsBundleByAlias(alias: string) { + if (this.widgetsBundles && alias) { + const found = this.widgetsBundles.find((widgetsBundle) => widgetsBundle.alias === alias); + if (found && this.widgetsBundle !== found) { + this.widgetsBundle = found; + this.updateView(); + } + } + } + + private updateView() { + this.propagateChange(this.widgetsBundle); + } + + private getWidgetsBundles(): Observable> { + let widgetsBundlesObservable: Observable>; + if (this.bundlesScope) { + if (this.bundlesScope === 'system') { + widgetsBundlesObservable = this.widgetService.getSystemWidgetsBundles(); + } else if (this.bundlesScope === 'tenant') { + widgetsBundlesObservable = this.widgetService.getTenantWidgetsBundles(); + } + } else { + widgetsBundlesObservable = this.widgetService.getAllWidgetsBundles(); + } + return widgetsBundlesObservable; + } + +} diff --git a/ui-ngx/src/app/shared/shared.module.ts b/ui-ngx/src/app/shared/shared.module.ts index 4a074919b8..17d2ee00cb 100644 --- a/ui-ngx/src/app/shared/shared.module.ts +++ b/ui-ngx/src/app/shared/shared.module.ts @@ -94,6 +94,7 @@ import { MatSpinner } from '@angular/material/progress-spinner'; import { FabToolbarComponent, FabActionsDirective, FabTriggerDirective } from './components/fab-toolbar.component'; import { DashboardSelectPanelComponent } from '@shared/components/dashboard-select-panel.component'; import { DashboardSelectComponent } from '@shared/components/dashboard-select.component'; +import { WidgetsBundleSelectComponent } from './components/widgets-bundle-select.component'; @NgModule({ providers: [ @@ -145,6 +146,7 @@ import { DashboardSelectComponent } from '@shared/components/dashboard-select.co FabTriggerDirective, FabActionsDirective, FabToolbarComponent, + WidgetsBundleSelectComponent, NospacePipe, MillisecondsToTimeStringPipe, EnumToArrayPipe, @@ -226,6 +228,7 @@ import { DashboardSelectComponent } from '@shared/components/dashboard-select.co FabTriggerDirective, FabActionsDirective, FabToolbarComponent, + WidgetsBundleSelectComponent, ValueInputComponent, MatButtonModule, MatCheckboxModule,