Browse Source

Widget Editor: Save and save as actions.

pull/2150/head
Igor Kulikov 7 years ago
parent
commit
2eb93dac7e
  1. 1
      ui-ngx/src/app/core/api/widget-subscription.ts
  2. 149
      ui-ngx/src/app/core/http/widget.service.ts
  3. 2
      ui-ngx/src/app/core/services/utils.service.ts
  4. 8
      ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.ts
  5. 63
      ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts
  6. 3
      ui-ngx/src/app/modules/home/components/widget/widget.component.ts
  7. 2
      ui-ngx/src/app/modules/home/models/dashboard-component.models.ts
  8. 24
      ui-ngx/src/app/modules/home/models/widget-component.models.ts
  9. 28
      ui-ngx/src/app/modules/home/pages/dashboard/dashboard-page.component.ts
  10. 3
      ui-ngx/src/app/modules/home/pages/dashboard/layout/dashboard-layout.component.ts
  11. 1
      ui-ngx/src/app/modules/home/pages/dashboard/layout/layout.models.ts
  12. 65
      ui-ngx/src/app/modules/home/pages/widget/save-widget-type-as-dialog.component.html
  13. 81
      ui-ngx/src/app/modules/home/pages/widget/save-widget-type-as-dialog.component.ts
  14. 22
      ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.html
  15. 82
      ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.ts
  16. 2
      ui-ngx/src/app/modules/home/pages/widget/widget-library-routing.module.ts
  17. 5
      ui-ngx/src/app/modules/home/pages/widget/widget-library.component.html
  18. 39
      ui-ngx/src/app/modules/home/pages/widget/widget-library.component.ts
  19. 7
      ui-ngx/src/app/modules/home/pages/widget/widget-library.module.ts
  20. 3
      ui-ngx/src/app/shared/components/dashboard-select.component.ts
  21. 34
      ui-ngx/src/app/shared/components/widgets-bundle-select.component.html
  22. 91
      ui-ngx/src/app/shared/components/widgets-bundle-select.component.scss
  23. 156
      ui-ngx/src/app/shared/components/widgets-bundle-select.component.ts
  24. 3
      ui-ngx/src/app/shared/shared.module.ts

1
ui-ngx/src/app/core/api/widget-subscription.ts

@ -505,6 +505,7 @@ export class WidgetSubscription implements IWidgetSubscription {
private alarmsSubscribe() { private alarmsSubscribe() {
// TODO: // TODO:
this.notifyDataLoaded();
} }

149
ui-ngx/src/app/core/http/widget.service.ts

@ -16,7 +16,7 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { defaultHttpOptions } from './http-utils'; 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 { HttpClient } from '@angular/common/http';
import { PageLink } from '@shared/models/page/page-link'; import { PageLink } from '@shared/models/page/page-link';
import { PageData } from '@shared/models/page/page-data'; 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 { UtilsService } from '@core/services/utils.service';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { ResourcesService } from '../services/resources.service'; import { ResourcesService } from '../services/resources.service';
import { toWidgetInfo, WidgetInfo } from '@app/modules/home/models/widget-component.models'; import { toWidgetInfo, WidgetInfo, toWidgetType } from '@app/modules/home/models/widget-component.models';
import { map } from 'rxjs/operators'; 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({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class WidgetService { export class WidgetService {
private widgetTypeUpdatedSubject = new Subject<WidgetType>();
private widgetsBundleDeletedSubject = new Subject<WidgetsBundle>();
private allWidgetsBundles: Array<WidgetsBundle>;
private systemWidgetsBundles: Array<WidgetsBundle>;
private tenantWidgetsBundles: Array<WidgetsBundle>;
constructor( constructor(
private http: HttpClient, private http: HttpClient,
private utils: UtilsService, private utils: UtilsService,
private resources: ResourcesService, 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<Array<WidgetsBundle>> {
return this.loadWidgetsBundleCache(ignoreErrors, ignoreLoading).pipe(
map(() => this.allWidgetsBundles)
);
}
public getSystemWidgetsBundles(ignoreErrors: boolean = false,
ignoreLoading: boolean = false): Observable<Array<WidgetsBundle>> {
return this.loadWidgetsBundleCache(ignoreErrors, ignoreLoading).pipe(
map(() => this.systemWidgetsBundles)
);
}
public getTenantWidgetsBundles(ignoreErrors: boolean = false,
ignoreLoading: boolean = false): Observable<Array<WidgetsBundle>> {
return this.loadWidgetsBundleCache(ignoreErrors, ignoreLoading).pipe(
map(() => this.tenantWidgetsBundles)
);
} }
public getWidgetBundles(pageLink: PageLink, ignoreErrors: boolean = false, public getWidgetBundles(pageLink: PageLink, ignoreErrors: boolean = false,
@ -54,11 +91,26 @@ export class WidgetService {
public saveWidgetsBundle(widgetsBundle: WidgetsBundle, public saveWidgetsBundle(widgetsBundle: WidgetsBundle,
ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<WidgetsBundle> { ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<WidgetsBundle> {
return this.http.post<WidgetsBundle>('/api/widgetsBundle', widgetsBundle, defaultHttpOptions(ignoreLoading, ignoreErrors)); return this.http.post<WidgetsBundle>('/api/widgetsBundle', widgetsBundle,
defaultHttpOptions(ignoreLoading, ignoreErrors)).pipe(
tap(() => {
this.invalidateWidgetsBundleCache();
})
);
} }
public deleteWidgetsBundle(widgetsBundleId: string, ignoreErrors: boolean = false, ignoreLoading: boolean = false) { 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, public getBundleWidgetTypes(bundleAlias: string, isSystem: boolean,
@ -73,6 +125,41 @@ export class WidgetService {
defaultHttpOptions(ignoreLoading, ignoreErrors)); defaultHttpOptions(ignoreLoading, ignoreErrors));
} }
public saveWidgetType(widgetInfo: WidgetInfo,
id: WidgetTypeId,
bundleAlias: string,
ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<WidgetType> {
const widgetTypeInstance = toWidgetType(widgetInfo, id, undefined, bundleAlias);
return this.http.post<WidgetType>('/api/widgetType', widgetTypeInstance,
defaultHttpOptions(ignoreLoading, ignoreErrors)).pipe(
tap((savedWidgetType) => {
this.widgetTypeUpdatedSubject.next(savedWidgetType);
}));
}
public saveImportedWidgetType(widgetTypeInstance: WidgetType,
ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<WidgetType> {
return this.http.post<WidgetType>('/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, public getWidgetTypeById(widgetTypeId: string,
ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<WidgetType> { ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<WidgetType> {
return this.http.get<WidgetType>(`/api/widgetType/${widgetTypeId}`, return this.http.get<WidgetType>(`/api/widgetType/${widgetTypeId}`,
@ -90,5 +177,55 @@ export class WidgetService {
return widgetInfo; return widgetInfo;
}) })
); );
}
public onWidgetTypeUpdated(): Observable<WidgetType> {
return this.widgetTypeUpdatedSubject.asObservable();
}
public onWidgetBundleDeleted(): Observable<WidgetsBundle> {
return this.widgetsBundleDeletedSubject.asObservable();
}
private loadWidgetsBundleCache(ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<any> {
if (!this.allWidgetsBundles) {
const loadWidgetsBundleCacheSubject = new ReplaySubject();
this.http.get<Array<WidgetsBundle>>('/api/widgetsBundles',
defaultHttpOptions(ignoreLoading, ignoreErrors)).subscribe(
(allWidgetsBundles) => {
this.allWidgetsBundles = allWidgetsBundles;
this.systemWidgetsBundles = new Array<WidgetsBundle>();
this.tenantWidgetsBundles = new Array<WidgetsBundle>();
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;
}
} }

2
ui-ngx/src/app/core/services/utils.service.ts

@ -84,7 +84,7 @@ export class UtilsService {
} }
public processWidgetException(exception: any): ExceptionData { public processWidgetException(exception: any): ExceptionData {
const data = this.parseException(exception, -5); const data = this.parseException(exception, -6);
if (this.widgetEditMode) { if (this.widgetEditMode) {
const message: WindowMessage = { const message: WindowMessage = {
type: 'widgetException', type: 'widgetException',

8
ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.ts

@ -308,7 +308,13 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
$event.stopPropagation(); $event.stopPropagation();
} }
if (this.isRemoveActionEnabled && this.callbacks && this.callbacks.onRemoveWidget) { 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);
}
}
);
} }
} }

63
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 { DynamicComponentFactoryService } from '@core/services/dynamic-component-factory.service';
import { WidgetService } from '@core/http/widget.service'; import { WidgetService } from '@core/http/widget.service';
import { forkJoin, Observable, of, ReplaySubject, Subject, throwError } from 'rxjs'; 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 cssjs from '@core/css/css';
import { UtilsService } from '@core/services/utils.service'; import { UtilsService } from '@core/services/utils.service';
import { ResourcesService } from '@core/services/resources.service'; import { ResourcesService } from '@core/services/resources.service';
import { import { widgetActionSources, WidgetControllerDescriptor, WidgetType } from '@shared/models/widget.models';
widgetActionSources, import { catchError, map, mergeMap, switchMap } from 'rxjs/operators';
WidgetControllerDescriptor,
WidgetType
} from '@shared/models/widget.models';
import { catchError, switchMap, map, mergeMap } from 'rxjs/operators';
import { isFunction, isUndefined } from '@core/utils'; import { isFunction, isUndefined } from '@core/utils';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { DynamicWidgetComponent } from '@home/components/widget/dynamic-widget.component'; 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 * as tinycolor from 'tinycolor2';
import { TbFlot } from './lib/flot-widget'; 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; // declare var jQuery: any;
@ -53,6 +59,7 @@ export class WidgetComponentService {
private missingWidgetType: WidgetInfo; private missingWidgetType: WidgetInfo;
private errorWidgetType: WidgetInfo; private errorWidgetType: WidgetInfo;
private editingWidgetType: WidgetType;
constructor(@Inject(WINDOW) private window: Window, constructor(@Inject(WINDOW) private window: Window,
private dynamicComponentFactoryService: DynamicComponentFactoryService, private dynamicComponentFactoryService: DynamicComponentFactoryService,
@ -68,6 +75,15 @@ export class WidgetComponentService {
this.window.TbFlot = TbFlot; this.window.TbFlot = TbFlot;
this.cssParser.testMode = false; 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(); this.init();
} }
@ -77,6 +93,24 @@ export class WidgetComponentService {
} else { } else {
this.missingWidgetType = {...MissingWidgetType}; this.missingWidgetType = {...MissingWidgetType};
this.errorWidgetType = {...ErrorWidgetType}; 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(); const initSubject = new ReplaySubject();
this.init$ = initSubject.asObservable(); this.init$ = initSubject.asObservable();
const loadDefaultWidgetInfoTasks = [ const loadDefaultWidgetInfoTasks = [
@ -110,7 +144,7 @@ export class WidgetComponentService {
widgetInfoSubject.complete(); widgetInfoSubject.complete();
} else { } else {
if (this.utils.widgetEditMode) { if (this.utils.widgetEditMode) {
// TODO: this.loadWidget(this.editingWidgetType, bundleAlias, isSystem, widgetInfoSubject);
} else { } else {
const key = this.createWidgetInfoCacheKey(bundleAlias, widgetTypeAlias, isSystem); const key = this.createWidgetInfoCacheKey(bundleAlias, widgetTypeAlias, isSystem);
let fetchQueue = this.widgetsInfoFetchQueue.get(key); let fetchQueue = this.widgetsInfoFetchQueue.get(key);
@ -377,4 +411,17 @@ export class WidgetComponentService {
this.widgetsInfoInMemoryCache.set(key, widgetInfo); 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);
}
});
}
} }

3
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); createSubscriptionSubject.error(null);
} }
); );
this.cd.detectChanges();
} else if (this.widget.type === widgetType.static) { } else if (this.widget.type === widgetType.static) {
this.loadingData = false; this.loadingData = false;
createSubscriptionSubject.next(); createSubscriptionSubject.next();
createSubscriptionSubject.complete(); createSubscriptionSubject.complete();
this.cd.detectChanges();
} else { } else {
createSubscriptionSubject.next(); createSubscriptionSubject.next();
createSubscriptionSubject.complete(); createSubscriptionSubject.complete();
this.cd.detectChanges();
} }
return createSubscriptionSubject.asObservable(); return createSubscriptionSubject.asObservable();
} }

2
ui-ngx/src/app/modules/home/models/dashboard-component.models.ts

@ -33,7 +33,7 @@ export interface WidgetsData {
export interface DashboardCallbacks { export interface DashboardCallbacks {
onEditWidget?: ($event: Event, widget: Widget) => void; onEditWidget?: ($event: Event, widget: Widget) => void;
onExportWidget?: ($event: Event, widget: Widget) => void; onExportWidget?: ($event: Event, widget: Widget) => void;
onRemoveWidget?: ($event: Event, widget: Widget) => void; onRemoveWidget?: ($event: Event, widget: Widget) => Observable<boolean>;
onWidgetMouseDown?: ($event: Event, widget: Widget) => void; onWidgetMouseDown?: ($event: Event, widget: Widget) => void;
onWidgetClicked?: ($event: Event, widget: Widget) => void; onWidgetClicked?: ($event: Event, widget: Widget) => void;
prepareDashboardContextMenu?: ($event: Event) => void; prepareDashboardContextMenu?: ($event: Event) => void;

24
ui-ngx/src/app/modules/home/models/widget-component.models.ts

@ -43,6 +43,8 @@ import {
import { ComponentFactory } from '@angular/core'; import { ComponentFactory } from '@angular/core';
import { HttpErrorResponse } from '@angular/common/http'; import { HttpErrorResponse } from '@angular/common/http';
import { RafService } from '@core/services/raf.service'; 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 { export interface IWidgetAction {
name: string; 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
};
}

28
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 { FooterFabButtons } from '@shared/components/footer-fab-buttons.component';
import { IStateController } from '@core/api/widget-api.models'; import { IStateController } from '@core/api/widget-api.models';
import { DashboardUtilsService } from '@core/services/dashboard-utils.service'; import { DashboardUtilsService } from '@core/services/dashboard-utils.service';
import { DashboardService } from '@core/http/dashboard.service';
@Component({ @Component({
selector: 'tb-dashboard-page', selector: 'tb-dashboard-page',
@ -173,7 +174,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
private dashboardUtils: DashboardUtilsService, private dashboardUtils: DashboardUtilsService,
private authService: AuthService, private authService: AuthService,
private entityService: EntityService, private entityService: EntityService,
private dialogService: DialogService) { private dialogService: DialogService,
private dashboardService: DashboardService) {
super(store); super(store);
this.rxSubscriptions.push(this.route.data.subscribe( this.rxSubscriptions.push(this.route.data.subscribe(
@ -460,6 +462,11 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
this.setEditMode(!this.isEdit, true); this.setEditMode(!this.isEdit, true);
} }
public saveDashboard() {
this.setEditMode(false, false);
this.notifyDashboardUpdated();
}
public openDashboardState(state: string, openRightLayout: boolean) { public openDashboardState(state: string, openRightLayout: boolean) {
const layoutsData = this.dashboardUtils.getStateLayoutsData(this.dashboard, state); const layoutsData = this.dashboardUtils.getStateLayoutsData(this.dashboard, state);
if (layoutsData) { if (layoutsData) {
@ -514,8 +521,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
private setEditMode(isEdit: boolean, revert: boolean) { private setEditMode(isEdit: boolean, revert: boolean) {
this.isEdit = isEdit; this.isEdit = isEdit;
if (this.isEdit) { if (this.isEdit) {
// TODO: this.dashboardCtx.stateController.preserveState();
// this.dashboardCtx.stateController.preserveState();
this.prevDashboard = deepClone(this.dashboard); this.prevDashboard = deepClone(this.dashboard);
} else { } else {
if (this.widgetEditMode) { if (this.widgetEditMode) {
@ -549,4 +555,20 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
private entityAliasesUpdated() { private entityAliasesUpdated() {
this.dashboardCtx.aliasController.updateEntityAliases(this.dashboard.configuration.entityAliases); 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);
}
}
} }

3
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) { setResizing(layoutVisibilityChanged: boolean) {
} }
resetHighlight() {
}
} }

1
ui-ngx/src/app/modules/home/pages/dashboard/layout/layout.models.ts

@ -17,4 +17,5 @@
export interface ILayoutController { export interface ILayoutController {
reload(); reload();
setResizing(layoutVisibilityChanged: boolean); setResizing(layoutVisibilityChanged: boolean);
resetHighlight();
} }

65
ui-ngx/src/app/modules/home/pages/widget/save-widget-type-as-dialog.component.html

@ -0,0 +1,65 @@
<!--
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.
-->
<form #saveWidgetTypeAsForm="ngForm"
[formGroup]="saveWidgetTypeAsFormGroup"(ngSubmit)="saveAs()">
<mat-toolbar fxLayout="row" color="primary">
<h2 translate>widget.save-widget-type-as</h2>
<span fxFlex></span>
<button mat-button mat-icon-button
(click)="cancel()"
type="button">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
<div mat-dialog-content>
<fieldset>
<span translate>widget.save-widget-type-as-text</span>
<mat-form-field class="mat-block">
<mat-label translate>widget.title</mat-label>
<input matInput formControlName="title" required>
<mat-error *ngIf="saveWidgetTypeAsFormGroup.get('title').hasError('required')">
{{ 'widget.title-required' | translate }}
</mat-error>
</mat-form-field>
<tb-widgets-bundle-select fxFlex
formControlName="widgetsBundle"
required
bundlesScope="{{bundlesScope}}">
</tb-widgets-bundle-select>
</fieldset>
</div>
<div mat-dialog-actions fxLayout="row">
<span fxFlex></span>
<button mat-button mat-raised-button color="primary"
type="submit"
[disabled]="(isLoading$ | async) || saveWidgetTypeAsForm.invalid
|| !saveWidgetTypeAsForm.dirty">
{{ 'action.saveAs' | translate }}
</button>
<button mat-button color="primary"
style="margin-right: 20px;"
type="button"
[disabled]="(isLoading$ | async)"
(click)="cancel()" cdkFocusInitial>
{{ 'action.cancel' | translate }}
</button>
</div>
</form>

81
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<SaveWidgetTypeAsDialogComponent, SaveWidgetTypeAsDialogResult> implements OnInit {
saveWidgetTypeAsFormGroup: FormGroup;
bundlesScope: string;
constructor(protected store: Store<AppState>,
protected router: Router,
public dialogRef: MatDialogRef<SaveWidgetTypeAsDialogComponent, SaveWidgetTypeAsDialogResult>,
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);
}
}

22
ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.html

@ -19,9 +19,10 @@
<div fxFlex fxLayout="column"> <div fxFlex fxLayout="column">
<div fxFlex fxLayout="column" tb-fullscreen [fullscreen]="fullscreen"> <div fxFlex fxLayout="column" tb-fullscreen [fullscreen]="fullscreen">
<mat-toolbar class="mat-elevation-z1 tb-edit-toolbar mat-hue-3" fxLayoutGap="16px"> <mat-toolbar class="mat-elevation-z1 tb-edit-toolbar mat-hue-3" fxLayoutGap="16px">
<mat-form-field floatLabel="always" class="tb-widget-title"> <mat-form-field floatLabel="always" hideRequiredMarker class="tb-widget-title">
<mat-label></mat-label> <mat-label></mat-label>
<input [disabled]="isReadOnly" matInput [(ngModel)]="widget.widgetName" (ngModelChange)="isDirty = true" <input [disabled]="isReadOnly" matInput required
[(ngModel)]="widget.widgetName" (ngModelChange)="isDirty = true"
placeholder="{{ 'widget.title' | translate }}"/> placeholder="{{ 'widget.title' | translate }}"/>
</mat-form-field> </mat-form-field>
<mat-form-field> <mat-form-field>
@ -238,14 +239,15 @@
</div> </div>
<div tb-fullscreen [fullscreen]="iFrameFullscreen" style="width: 100%; height: 100%;"> <div tb-fullscreen [fullscreen]="iFrameFullscreen" style="width: 100%; height: 100%;">
<iframe #widgetIFrame frameborder="0" height="100%" width="100%"></iframe> <iframe #widgetIFrame frameborder="0" height="100%" width="100%"></iframe>
<button mat-button mat-icon-button <div style="position: absolute; top: 10px; left: 10px; bottom: initial;">
class="tb-fullscreen-button-style" <button mat-button mat-icon-button
style="position: absolute; top: 10px; left: 10px; bottom: initial;" class="tb-fullscreen-button-style"
(click)="iFrameFullscreen = !iFrameFullscreen" (click)="iFrameFullscreen = !iFrameFullscreen"
matTooltip="{{(iFrameFullscreen ? 'fullscreen.exit' : 'fullscreen.expand') | translate}}" matTooltip="{{(iFrameFullscreen ? 'fullscreen.exit' : 'fullscreen.expand') | translate}}"
matTooltipPosition="above"> matTooltipPosition="above">
<mat-icon>{{ iFrameFullscreen ? 'fullscreen_exit' : 'fullscreen' }}</mat-icon> <mat-icon>{{ iFrameFullscreen ? 'fullscreen_exit' : 'fullscreen' }}</mat-icon>
</button> </button>
</div>
</div> </div>
</div> </div>
</div> </div>

82
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 { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { WidgetService } from '@core/http/widget.service'; 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 { 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 { deepClone } from '@core/utils';
import { HasDirtyFlag } from '@core/guards/confirm-on-exit.guard'; import { HasDirtyFlag } from '@core/guards/confirm-on-exit.guard';
import { AuthUser } from '@shared/models/user.model'; 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 { ExceptionData } from '@shared/models/error.models';
import Timeout = NodeJS.Timeout; import Timeout = NodeJS.Timeout;
import { ActionNotificationHide, ActionNotificationShow } from '@core/notification/notification.actions'; 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({ @Component({
selector: 'tb-widget-editor', selector: 'tb-widget-editor',
@ -131,25 +138,37 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe
saveWidgetTimeout: Timeout; saveWidgetTimeout: Timeout;
private rxSubscriptions = new Array<Subscription>();
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
@Inject(WINDOW) private window: Window, @Inject(WINDOW) private window: Window,
private route: ActivatedRoute, private route: ActivatedRoute,
private router: Router,
private widgetService: WidgetService, private widgetService: WidgetService,
private hotkeysService: HotkeysService, private hotkeysService: HotkeysService,
private translate: TranslateService, private translate: TranslateService,
private raf: RafService) { private raf: RafService,
private dialog: MatDialog) {
super(store); super(store);
this.authUser = getCurrentAuthUser(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) { if (this.authUser.authority === Authority.TENANT_ADMIN) {
this.isReadOnly = !this.widgetsBundle || this.widgetsBundle.tenantId.id === NULL_UUID; this.isReadOnly = !this.widgetsBundle || this.widgetsBundle.tenantId.id === NULL_UUID;
} else { } else {
this.isReadOnly = this.authUser.authority !== Authority.SYS_ADMIN; this.isReadOnly = this.authUser.authority !== Authority.SYS_ADMIN;
} }
this.widgetType = this.route.snapshot.data.widgetEditorData.widgetType; this.widgetType = data.widgetEditorData.widgetType;
this.widget = this.route.snapshot.data.widgetEditorData.widget; this.widget = data.widgetEditorData.widget;
if (this.widgetType) { if (this.widgetType) {
const config = JSON.parse(this.widget.defaultConfig); const config = JSON.parse(this.widget.defaultConfig);
this.widget.defaultConfig = JSON.stringify(config); this.widget.defaultConfig = JSON.stringify(config);
@ -176,6 +195,10 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe
// @ts-ignore // @ts-ignore
removeResizeListener(resizeListener.element, resizeListener.resizeListener); removeResizeListener(resizeListener.element, resizeListener.resizeListener);
}); });
this.rxSubscriptions.forEach((subscription) => {
subscription.unsubscribe();
});
this.rxSubscriptions.length = 0;
} }
private initHotKeys(): void { private initHotKeys(): void {
@ -448,13 +471,52 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe
} }
private commitSaveWidget() { private commitSaveWidget() {
// TODO: const id = (this.widgetType && this.widgetType.id) ? this.widgetType.id : undefined;
this.saveWidgetPending = false; 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() { private commitSaveWidgetAs() {
// TODO: this.dialog.open<SaveWidgetTypeAsDialogComponent, any,
this.saveWidgetAsPending = false; SaveWidgetTypeAsDialogResult>(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 { applyWidgetScript(): void {

2
ui-ngx/src/app/modules/home/pages/widget/widget-library-routing.module.ts

@ -73,7 +73,7 @@ export class WidgetsTypesDataResolver implements Resolve<WidgetsData> {
} }
return result; return result;
}); });
const widgetTypes = new Array<Widget>(types.length); const widgetTypes = new Array<Widget>();
let top = 0; let top = 0;
const lastTop = [0, 0, 0]; const lastTop = [0, 0, 0];
let col = 0; let col = 0;

5
ui-ngx/src/app/modules/home/pages/widget/widget-library.component.html

@ -15,7 +15,7 @@
limitations under the License. limitations under the License.
--> -->
<section [fxShow]="!(isLoading$ | async) && (widgetTypes$ | async)?.length === 0" fxLayoutAlign="center center" <section [fxShow]="!(isLoading$ | async) && widgetsData.widgets.length === 0" fxLayoutAlign="center center"
style="text-transform: uppercase; display: flex; z-index: 1;" style="text-transform: uppercase; display: flex; z-index: 1;"
class="tb-absolute-fill"> class="tb-absolute-fill">
<button mat-button *ngIf="!isReadOnly" class="tb-add-new-widget" (click)="addWidgetType($event)"> <button mat-button *ngIf="!isReadOnly" class="tb-add-new-widget" (click)="addWidgetType($event)">
@ -27,7 +27,8 @@
style="text-transform: uppercase; display: flex;" style="text-transform: uppercase; display: flex;"
class="mat-headline tb-absolute-fill">widgets-bundle.empty</span> class="mat-headline tb-absolute-fill">widgets-bundle.empty</span>
</section> </section>
<tb-dashboard [aliasController]="aliasController" <tb-dashboard #dashboard
[aliasController]="aliasController"
[widgets]="widgetsData.widgets" [widgets]="widgetsData.widgets"
[widgetLayouts]="widgetsData.widgetLayouts" [widgetLayouts]="widgetsData.widgetLayouts"
[isEdit]="false" [isEdit]="false"

39
ui-ngx/src/app/modules/home/pages/widget/widget-library.component.ts

@ -14,7 +14,7 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { Component, OnInit } from '@angular/core'; import { Component, OnInit, ViewChild } from '@angular/core';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { PageComponent } from '@shared/components/page.component'; import { PageComponent } from '@shared/components/page.component';
@ -24,13 +24,13 @@ import { WidgetsBundle } from '@shared/models/widgets-bundle.model';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { Authority } from '@shared/models/authority.enum'; import { Authority } from '@shared/models/authority.enum';
import { NULL_UUID } from '@shared/models/id/has-uuid'; import { NULL_UUID } from '@shared/models/id/has-uuid';
import { Observable } from 'rxjs'; import { Observable, of } from 'rxjs';
import { Widget, widgetType } from '@app/shared/models/widget.models'; import { Widget, widgetType } from '@app/shared/models/widget.models';
import { WidgetService } from '@core/http/widget.service'; import { WidgetService } from '@core/http/widget.service';
import { map, share } from 'rxjs/operators'; import { map, mergeMap, share } from 'rxjs/operators';
import { DialogService } from '@core/services/dialog.service'; import { DialogService } from '@core/services/dialog.service';
import { FooterFabButtons } from '@app/shared/components/footer-fab-buttons.component'; import { FooterFabButtons } from '@app/shared/components/footer-fab-buttons.component';
import { DashboardCallbacks, WidgetsData } from '@home/models/dashboard-component.models'; import { DashboardCallbacks, IDashboardComponent, WidgetsData } from '@home/models/dashboard-component.models';
import { IAliasController } from '@app/core/api/widget-api.models'; import { IAliasController } from '@app/core/api/widget-api.models';
import { toWidgetInfo } from '@home/models/widget-component.models'; import { toWidgetInfo } from '@home/models/widget-component.models';
import { DummyAliasController } from '@core/api/alias-controller'; import { DummyAliasController } from '@core/api/alias-controller';
@ -41,6 +41,7 @@ import {
import { DeviceCredentials } from '@shared/models/device.models'; import { DeviceCredentials } from '@shared/models/device.models';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { SelectWidgetTypeDialogComponent } from '@home/pages/widget/select-widget-type-dialog.component'; import { SelectWidgetTypeDialogComponent } from '@home/pages/widget/select-widget-type-dialog.component';
import { TranslateService } from '@ngx-translate/core';
@Component({ @Component({
selector: 'tb-widget-library', selector: 'tb-widget-library',
@ -86,12 +87,15 @@ export class WidgetLibraryComponent extends PageComponent implements OnInit {
aliasController: IAliasController = new DummyAliasController(); aliasController: IAliasController = new DummyAliasController();
@ViewChild('dashboard', {static: true}) dashboard: IDashboardComponent;
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
private route: ActivatedRoute, private route: ActivatedRoute,
private router: Router, private router: Router,
private widgetService: WidgetService, private widgetService: WidgetService,
private dialogService: DialogService, private dialogService: DialogService,
private dialog: MatDialog) { private dialog: MatDialog,
private translate: TranslateService) {
super(store); super(store);
this.authUser = getCurrentAuthUser(store); this.authUser = getCurrentAuthUser(store);
@ -146,11 +150,32 @@ export class WidgetLibraryComponent extends PageComponent implements OnInit {
this.dialogService.todo(); this.dialogService.todo();
} }
removeWidgetType($event: Event, widget: Widget): void { removeWidgetType($event: Event, widget: Widget): Observable<boolean> {
if ($event) { if ($event) {
$event.stopPropagation(); $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;
}
}
));
} }
} }

7
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 { WidgetLibraryComponent } from './widget-library.component';
import { WidgetEditorComponent } from '@home/pages/widget/widget-editor.component'; import { WidgetEditorComponent } from '@home/pages/widget/widget-editor.component';
import { SelectWidgetTypeDialogComponent } from '@home/pages/widget/select-widget-type-dialog.component'; import { SelectWidgetTypeDialogComponent } from '@home/pages/widget/select-widget-type-dialog.component';
import { SaveWidgetTypeAsDialogComponent } from './save-widget-type-as-dialog.component';
@NgModule({ @NgModule({
entryComponents: [ entryComponents: [
WidgetsBundleComponent, WidgetsBundleComponent,
SelectWidgetTypeDialogComponent SelectWidgetTypeDialogComponent,
SaveWidgetTypeAsDialogComponent
], ],
declarations: [ declarations: [
WidgetsBundleComponent, WidgetsBundleComponent,
WidgetLibraryComponent, WidgetLibraryComponent,
WidgetEditorComponent, WidgetEditorComponent,
SelectWidgetTypeDialogComponent SelectWidgetTypeDialogComponent,
SaveWidgetTypeAsDialogComponent
], ],
imports: [ imports: [
CommonModule, CommonModule,

3
ui-ngx/src/app/shared/components/dashboard-select.component.ts

@ -38,6 +38,7 @@ import {
DashboardSelectPanelComponent, DashboardSelectPanelComponent,
DashboardSelectPanelData DashboardSelectPanelData
} from './dashboard-select-panel.component'; } from './dashboard-select-panel.component';
import { NULL_UUID } from '@shared/models/id/has-uuid';
@Component({ @Component({
selector: 'tb-dashboard-select', selector: 'tb-dashboard-select',
@ -200,7 +201,7 @@ export class DashboardSelectComponent implements ControlValueAccessor, OnInit {
let dashboardsObservable: Observable<PageData<DashboardInfo>>; let dashboardsObservable: Observable<PageData<DashboardInfo>>;
const authUser = getCurrentAuthUser(this.store); const authUser = getCurrentAuthUser(this.store);
if (this.dashboardsScope === 'customer' || authUser.authority === Authority.CUSTOMER_USER) { 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); dashboardsObservable = this.dashboardService.getCustomerDashboards(this.customerId, pageLink, false, true);
} else { } else {
dashboardsObservable = of(emptyPageData()); dashboardsObservable = of(emptyPageData());

34
ui-ngx/src/app/shared/components/widgets-bundle-select.component.html

@ -0,0 +1,34 @@
<!--
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.
-->
<mat-form-field floatLabel="always" hideRequiredMarker class="mat-block">
<mat-label></mat-label>
<mat-select [required]="required"
[disabled]="disabled"
[(ngModel)]="widgetsBundle"
matInput
panelClass="tb-widgets-bundle-select"
placeholder="{{ 'widget.select-widgets-bundle' | translate }}"
(ngModelChange)="widgetsBundleChanged()">
<mat-option *ngFor="let widgetsBundle of widgetsBundles$ | async" [value]="widgetsBundle">
<div class="tb-bundle-item">
<span>{{widgetsBundle.title}}</span>
<span translate class="tb-bundle-system" *ngIf="isSystem(item)">widgets-bundle.system</span>
</div>
</mat-option>
</mat-select>
</mat-form-field>

91
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;
}
}
}
}

156
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<Array<WidgetsBundle>>;
widgetsBundles: Array<WidgetsBundle>;
widgetsBundle: WidgetsBundle | null;
private propagateChange = (v: any) => { };
constructor(private store: Store<AppState>,
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<Array<WidgetsBundle>> {
let widgetsBundlesObservable: Observable<Array<WidgetsBundle>>;
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;
}
}

3
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 { FabToolbarComponent, FabActionsDirective, FabTriggerDirective } from './components/fab-toolbar.component';
import { DashboardSelectPanelComponent } from '@shared/components/dashboard-select-panel.component'; import { DashboardSelectPanelComponent } from '@shared/components/dashboard-select-panel.component';
import { DashboardSelectComponent } from '@shared/components/dashboard-select.component'; import { DashboardSelectComponent } from '@shared/components/dashboard-select.component';
import { WidgetsBundleSelectComponent } from './components/widgets-bundle-select.component';
@NgModule({ @NgModule({
providers: [ providers: [
@ -145,6 +146,7 @@ import { DashboardSelectComponent } from '@shared/components/dashboard-select.co
FabTriggerDirective, FabTriggerDirective,
FabActionsDirective, FabActionsDirective,
FabToolbarComponent, FabToolbarComponent,
WidgetsBundleSelectComponent,
NospacePipe, NospacePipe,
MillisecondsToTimeStringPipe, MillisecondsToTimeStringPipe,
EnumToArrayPipe, EnumToArrayPipe,
@ -226,6 +228,7 @@ import { DashboardSelectComponent } from '@shared/components/dashboard-select.co
FabTriggerDirective, FabTriggerDirective,
FabActionsDirective, FabActionsDirective,
FabToolbarComponent, FabToolbarComponent,
WidgetsBundleSelectComponent,
ValueInputComponent, ValueInputComponent,
MatButtonModule, MatButtonModule,
MatCheckboxModule, MatCheckboxModule,

Loading…
Cancel
Save