From 1c10ede1c594532329f09ede08f211ff77f967ef Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Fri, 25 Oct 2019 17:50:12 +0300 Subject: [PATCH] Custom actions implementation --- ui-ngx/package.json | 2 +- ui-ngx/src/app/core/api/widget-api.models.ts | 3 + .../src/app/core/services/dialog.service.ts | 2 + .../dynamic-component-factory.service.ts | 1 - .../home/components/home-components.module.ts | 14 ++- .../widget/action/custom-sample-css.raw | 8 ++ .../widget/action/custom-sample-html.raw | 45 ++++++++++ .../widget/action/custom-sample-js.raw | 40 +++++++++ .../custom-dialog-container.component.ts | 85 +++++++++++++++++++ .../widget/dialog/custom-dialog.component.ts | 48 +++++++++++ .../widget/dialog/custom-dialog.service.ts | 85 +++++++++++++++++++ .../components/widget/widget.component.ts | 13 ++- .../home/models/widget-component.models.ts | 7 +- .../shared/components/fullscreen.directive.ts | 4 +- ui-ngx/src/styles.scss | 1 + 15 files changed, 348 insertions(+), 10 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/components/widget/dialog/custom-dialog-container.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/dialog/custom-dialog.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/dialog/custom-dialog.service.ts diff --git a/ui-ngx/package.json b/ui-ngx/package.json index 4a4b2e5ee1..ce64c8037e 100644 --- a/ui-ngx/package.json +++ b/ui-ngx/package.json @@ -3,7 +3,7 @@ "version": "3.0.0", "scripts": { "ng": "ng", - "start": "ng serve --open", + "start": "ng serve --host 0.0.0.0 --open", "build": "ng build", "build:prod": "ng build --prod --vendor-chunk", "test": "ng test", diff --git a/ui-ngx/src/app/core/api/widget-api.models.ts b/ui-ngx/src/app/core/api/widget-api.models.ts index 2d501c74e6..c364479290 100644 --- a/ui-ngx/src/app/core/api/widget-api.models.ts +++ b/ui-ngx/src/app/core/api/widget-api.models.ts @@ -36,6 +36,9 @@ import { DatasourceService } from '@core/api/datasource.service'; import { RafService } from '@core/services/raf.service'; import { EntityAliases } from '@shared/models/alias.models'; import { EntityInfo } from '@app/shared/models/entity.models'; +import { Type } from '@angular/core'; +import { AssetService } from '@core/http/asset.service'; +import { DialogService } from '@core/services/dialog.service'; export interface TimewindowFunctions { onUpdateTimewindow: (startTimeMs: number, endTimeMs: number, interval?: number) => void; diff --git a/ui-ngx/src/app/core/services/dialog.service.ts b/ui-ngx/src/app/core/services/dialog.service.ts index 4f627e3a6e..0e0c87d689 100644 --- a/ui-ngx/src/app/core/services/dialog.service.ts +++ b/ui-ngx/src/app/core/services/dialog.service.ts @@ -30,6 +30,7 @@ import { MaterialIconsDialogComponent, MaterialIconsDialogData } from '@shared/components/dialog/material-icons-dialog.component'; +import { DynamicComponentFactoryService } from '@core/services/dynamic-component-factory.service'; @Injectable( { @@ -41,6 +42,7 @@ export class DialogService { constructor( private translate: TranslateService, private authService: AuthService, + private dynamicComponentFactoryService: DynamicComponentFactoryService, public dialog: MatDialog ) { } diff --git a/ui-ngx/src/app/core/services/dynamic-component-factory.service.ts b/ui-ngx/src/app/core/services/dynamic-component-factory.service.ts index 2830da8721..f169193122 100644 --- a/ui-ngx/src/app/core/services/dynamic-component-factory.service.ts +++ b/ui-ngx/src/app/core/services/dynamic-component-factory.service.ts @@ -31,7 +31,6 @@ import { CommonModule } from '@angular/common'; export abstract class DynamicComponentModule implements OnDestroy { ngOnDestroy(): void { - console.log('Module destroyed!'); } } diff --git a/ui-ngx/src/app/modules/home/components/home-components.module.ts b/ui-ngx/src/app/modules/home/components/home-components.module.ts index 6bb19470b7..3409a20a3c 100644 --- a/ui-ngx/src/app/modules/home/components/home-components.module.ts +++ b/ui-ngx/src/app/modules/home/components/home-components.module.ts @@ -56,6 +56,8 @@ import { ManageWidgetActionsComponent } from './widget/action/manage-widget-acti import { WidgetActionDialogComponent } from './widget/action/widget-action-dialog.component'; import { CustomActionPrettyResourcesTabsComponent } from './widget/action/custom-action-pretty-resources-tabs.component'; import { CustomActionPrettyEditorComponent } from './widget/action/custom-action-pretty-editor.component'; +import { CustomDialogService } from './widget/dialog/custom-dialog.service'; +import { CustomDialogContainerComponent } from './widget/dialog/custom-dialog-container.component'; @NgModule({ entryComponents: [ @@ -72,7 +74,8 @@ import { CustomActionPrettyEditorComponent } from './widget/action/custom-action EntityAliasDialogComponent, DataKeyConfigDialogComponent, LegendConfigPanelComponent, - WidgetActionDialogComponent + WidgetActionDialogComponent, + CustomDialogContainerComponent ], declarations: [ @@ -113,7 +116,8 @@ import { CustomActionPrettyEditorComponent } from './widget/action/custom-action ManageWidgetActionsComponent, WidgetActionDialogComponent, CustomActionPrettyResourcesTabsComponent, - CustomActionPrettyEditorComponent + CustomActionPrettyEditorComponent, + CustomDialogContainerComponent ], imports: [ CommonModule, @@ -149,10 +153,12 @@ import { CustomActionPrettyEditorComponent } from './widget/action/custom-action ManageWidgetActionsComponent, WidgetActionDialogComponent, CustomActionPrettyResourcesTabsComponent, - CustomActionPrettyEditorComponent + CustomActionPrettyEditorComponent, + CustomDialogContainerComponent ], providers: [ - WidgetComponentService + WidgetComponentService, + CustomDialogService ] }) export class HomeComponentsModule { } diff --git a/ui-ngx/src/app/modules/home/components/widget/action/custom-sample-css.raw b/ui-ngx/src/app/modules/home/components/widget/action/custom-sample-css.raw index 9167f2c10e..56620aee51 100644 --- a/ui-ngx/src/app/modules/home/components/widget/action/custom-sample-css.raw +++ b/ui-ngx/src/app/modules/home/components/widget/action/custom-sample-css.raw @@ -1,3 +1,11 @@ +/*================================================================================*/ +/*======================= New TB 3.0 / Angular 8 Example =======================*/ +/*================================================================================*/ +/* +.edit-entity-form mat-form-field { + padding-right: 10px; +} +*/ /*=======================================================================*/ /*========== There are two examples: for edit and add entity ==========*/ /*=======================================================================*/ diff --git a/ui-ngx/src/app/modules/home/components/widget/action/custom-sample-html.raw b/ui-ngx/src/app/modules/home/components/widget/action/custom-sample-html.raw index 5de572fd4d..73d771b9c6 100644 --- a/ui-ngx/src/app/modules/home/components/widget/action/custom-sample-html.raw +++ b/ui-ngx/src/app/modules/home/components/widget/action/custom-sample-html.raw @@ -1,4 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ui-ngx/src/app/modules/home/components/widget/action/custom-sample-js.raw b/ui-ngx/src/app/modules/home/components/widget/action/custom-sample-js.raw index 98ec6d044b..960594c76b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/action/custom-sample-js.raw +++ b/ui-ngx/src/app/modules/home/components/widget/action/custom-sample-js.raw @@ -1,3 +1,43 @@ +/*================================================================================*/ +/*======================= New TB 3.0 / Angular 8 Example =======================*/ +/*================================================================================*/ +// +//let $injector = widgetContext.$scope.$injector; +//let deviceService = $injector.get(widgetContext.servicesMap.get('deviceService')); +// +//deviceService.getDevice(entityId.id).subscribe(function(device) { +// console.log(device); +//}); +// +// +//let customDialog = $injector.get(widgetContext.servicesMap.get('customDialog')); +// +//customDialog.customDialog(htmlTemplate, EditEntityDialogController).subscribe( +// function(res) { +// console.log(res); +// } +//); +// +//function EditEntityDialogController(instance) { +// let vm = instance; +// vm.entityId = entityId; +// vm.entityName = entityName; +// vm.entityType = entityId.entityType; +// +// vm.editEntityFormGroup = vm.fb.group({ +// entityName: [vm.entityName, [vm.Validators.required]] +// }); +// +// vm.cancel = function() { +// vm.dialogRef.close(null); +// }; +// +// vm.save = function() { +// const newVal = vm.editEntityFormGroup.value; +// vm.dialogRef.close(newVal); +// }; +//} +// /*=======================================================================*/ /*===== There are three examples: for delete, edit and add entity =====*/ /*=======================================================================*/ diff --git a/ui-ngx/src/app/modules/home/components/widget/dialog/custom-dialog-container.component.ts b/ui-ngx/src/app/modules/home/components/widget/dialog/custom-dialog-container.component.ts new file mode 100644 index 0000000000..9d17557ac9 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/dialog/custom-dialog-container.component.ts @@ -0,0 +1,85 @@ +/// +/// 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 { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { + Inject, + ComponentRef, + ElementRef, + ViewContainerRef, + ComponentFactory, + Injector, + ReflectiveInjector, + Component, + OnDestroy +} from '@angular/core'; +import { DialogComponent } from '@shared/components/dialog.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { Router } from '@angular/router'; +import { + CustomDialogComponent, + CUSTOM_DIALOG_DATA, + CustomDialogData +} from '@home/components/widget/dialog/custom-dialog.component'; + +export interface CustomDialogContainerData { + controller: (instance: CustomDialogComponent) => void; + data?: any; + customComponentFactory: ComponentFactory; +} + +@Component({ + selector: 'tb-custom-dialog-container-component', + template: '' +}) +export class CustomDialogContainerComponent extends DialogComponent implements OnDestroy { + + private customComponentRef: ComponentRef; + + constructor(protected store: Store, + protected router: Router, + public viewContainerRef: ViewContainerRef, + public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: CustomDialogContainerData) { + super(store, router, dialogRef); + let customDialogData: CustomDialogData = { + controller: this.data.controller + }; + if (this.data.data) { + customDialogData = {...customDialogData, ...this.data.data}; + } + const injector: Injector = ReflectiveInjector.resolveAndCreate([ + { + provide: CUSTOM_DIALOG_DATA, + useValue: customDialogData + }, + { + provide: MatDialogRef, + useValue: dialogRef + } + ]); + this.customComponentRef = this.viewContainerRef.createComponent(this.data.customComponentFactory, 0, injector); + } + + ngOnDestroy(): void { + super.ngOnDestroy(); + if (this.customComponentRef) { + this.customComponentRef.destroy(); + } + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/dialog/custom-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/dialog/custom-dialog.component.ts new file mode 100644 index 0000000000..1f592e2449 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/dialog/custom-dialog.component.ts @@ -0,0 +1,48 @@ +/// +/// 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 { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { Inject, InjectionToken } from '@angular/core'; +import { DialogComponent } from '@shared/components/dialog.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { Router } from '@angular/router'; +import { PageComponent } from '@shared/components/page.component'; +import { CustomDialogContainerComponent } from './custom-dialog-container.component'; +import { FormBuilder, Validators } from '@angular/forms'; + +export const CUSTOM_DIALOG_DATA = new InjectionToken('ConfigDialogData'); + +export interface CustomDialogData { + controller: (instance: CustomDialogComponent) => void; + [key: string]: any; +} + +export class CustomDialogComponent extends PageComponent { + + [key: string]: any; + + constructor(protected store: Store, + protected router: Router, + public dialogRef: MatDialogRef, + public fb: FormBuilder, + @Inject(CUSTOM_DIALOG_DATA) public data: CustomDialogData) { + super(store); + // @ts-ignore + this.Validators = Validators; + this.data.controller(this); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/dialog/custom-dialog.service.ts b/ui-ngx/src/app/modules/home/components/widget/dialog/custom-dialog.service.ts new file mode 100644 index 0000000000..b20213c764 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/dialog/custom-dialog.service.ts @@ -0,0 +1,85 @@ +/// +/// 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 { Injectable, Injector, NgModule } from '@angular/core'; +import { Observable } from 'rxjs'; +import { MatDialog } from '@angular/material'; +import { TranslateService } from '@ngx-translate/core'; +import { AuthService } from '@core/auth/auth.service'; +import { DynamicComponentFactoryService } from '@core/services/dynamic-component-factory.service'; +import { CommonModule } from '@angular/common'; +import { SharedModule } from '@shared/shared.module'; +import { mergeMap, tap } from 'rxjs/operators'; +import { CustomDialogComponent } from './custom-dialog.component'; +import { + CustomDialogContainerComponent, + CustomDialogContainerData +} from '@home/components/widget/dialog/custom-dialog-container.component'; + +@Injectable() +export class CustomDialogService { + + constructor( + private translate: TranslateService, + private authService: AuthService, + private dynamicComponentFactoryService: DynamicComponentFactoryService, + private injector: Injector, + public dialog: MatDialog + ) { + } + + customDialog(template: string, controller: (instance: CustomDialogComponent) => void, data?: any): Observable { + return this.dynamicComponentFactoryService.createDynamicComponentFactory( + class CustomDialogComponentInstance extends CustomDialogComponent {}, + template, + [SharedModule, CustomDialogModule]).pipe( + mergeMap((factory) => { + const dialogData: CustomDialogContainerData = { + controller, + customComponentFactory: factory, + data + }; + return this.dialog.open( + CustomDialogContainerComponent, + { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: dialogData + }).afterClosed().pipe( + tap(() => { + this.dynamicComponentFactoryService.destroyDynamicComponentFactory(factory); + }) + ); + } + )); + } + +} + +@NgModule({ + entryComponents: [ + ], + declarations: + [ + ], + imports: [ + CommonModule, + SharedModule + ], + exports: [ + ] +}) +class CustomDialogModule { } 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 90e7771bab..7eb77c0fa1 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 @@ -28,7 +28,7 @@ import { OnChanges, OnDestroy, OnInit, - SimpleChanges, + SimpleChanges, Type, ViewChild, ViewContainerRef, ViewEncapsulation @@ -90,6 +90,15 @@ import { DashboardService } from '@core/http/dashboard.service'; import { DatasourceService } from '@core/api/datasource.service'; import { WidgetSubscription } from '@core/api/widget-subscription'; import { EntityService } from '@core/http/entity.service'; +import { AssetService } from '@core/http/asset.service'; +import { DialogService } from '@core/services/dialog.service'; +import { CustomDialogService } from '@home/components/widget/dialog/custom-dialog.service'; + +const ServicesMap = new Map>(); +ServicesMap.set('deviceService', DeviceService); +ServicesMap.set('assetService', AssetService); +ServicesMap.set('dialogs', DialogService); +ServicesMap.set('customDialog', CustomDialogService); @Component({ selector: 'tb-widget', @@ -242,6 +251,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI } this.widgetContext = this.dashboardWidget.widgetContext; + this.widgetContext.servicesMap = ServicesMap; this.widgetContext.inited = false; this.widgetContext.hideTitlePanel = false; this.widgetContext.isEdit = this.isEdit; @@ -647,6 +657,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI this.dynamicWidgetComponent.errorMessages = this.errorMessages; this.widgetContext.$scope = this.dynamicWidgetComponent; + this.widgetContext.$scope.$injector = this.injector; const containerElement = $(this.elementRef.nativeElement.querySelector('#widget-container')); 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 438b0110ae..764d26d4a0 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 @@ -39,12 +39,16 @@ import { WidgetActionsApi, WidgetSubscriptionApi } from '@core/api/widget-api.models'; -import { ComponentFactory } from '@angular/core'; +import { ComponentFactory, Type } 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'; import { WidgetLayout } from '@shared/models/dashboard.models'; +import { DeviceService } from '@core/http/device.service'; +import { AssetService } from '@app/core/http/asset.service'; +import { DialogService } from '@core/services/dialog.service'; +import { CustomDialogService } from '@home/components/widget/dialog/custom-dialog.service'; export interface IWidgetAction { name: string; @@ -98,6 +102,7 @@ export interface WidgetContext { customHeaderActions?: Array; widgetActions?: Array; + servicesMap?: Map>; } export interface IDynamicWidgetComponent { diff --git a/ui-ngx/src/app/shared/components/fullscreen.directive.ts b/ui-ngx/src/app/shared/components/fullscreen.directive.ts index b94b105d99..d8b3827036 100644 --- a/ui-ngx/src/app/shared/components/fullscreen.directive.ts +++ b/ui-ngx/src/app/shared/components/fullscreen.directive.ts @@ -92,7 +92,7 @@ export class FullscreenDirective implements OnChanges, OnDestroy { this.overlayRef = this.overlay.create(config); this.overlayRef.attach(new EmptyPortal()); - this.overlayRef.overlayElement.append( targetElement ); + this.overlayRef.overlayElement.appendChild( targetElement ); this.fullscreenChanged.emit(true); } @@ -100,7 +100,7 @@ export class FullscreenDirective implements OnChanges, OnDestroy { const targetElement: HTMLElement = this.fullscreenElement || this.elementRef.nativeElement; if (this.parentElement) { this.overlayRef.overlayElement.removeChild( targetElement ); - this.parentElement.append(targetElement); + this.parentElement.appendChild(targetElement); this.parentElement = null; } targetElement.classList.remove('tb-fullscreen'); diff --git a/ui-ngx/src/styles.scss b/ui-ngx/src/styles.scss index 3411f7b4c6..ede38b56a2 100644 --- a/ui-ngx/src/styles.scss +++ b/ui-ngx/src/styles.scss @@ -332,6 +332,7 @@ pre.tb-highlight { .tb-fullscreen-parent { background: #eee; + z-index: 0; } mat-label {