diff --git a/ui-ngx/angular.json b/ui-ngx/angular.json index 113233b012..7165edf085 100644 --- a/ui-ngx/angular.json +++ b/ui-ngx/angular.json @@ -24,7 +24,11 @@ "tsConfig": "src/tsconfig.app.json", "assets": [ "src/thingsboard.ico", - "src/assets" + "src/assets", + { "glob": "worker-html.js", "input": "./node_modules/ace-builds/src-min/", "output": "/" }, + { "glob": "worker-css.js", "input": "./node_modules/ace-builds/src-min/", "output": "/" }, + { "glob": "worker-json.js", "input": "./node_modules/ace-builds/src-min/", "output": "/" }, + { "glob": "worker-javascript.js", "input": "./node_modules/ace-builds/src-min/", "output": "/" } ], "styles": [ "src/styles.scss" @@ -49,11 +53,7 @@ "node_modules/ace-builds/src-min/snippets/css.js", "node_modules/ace-builds/src-min/snippets/json.js", "node_modules/ace-builds/src-min/snippets/java.js", - "node_modules/ace-builds/src-min/snippets/javascript.js", - { "bundleName": "worker-html", "input": "node_modules/ace-builds/src-min/worker-html.js" }, - { "bundleName": "worker-css", "input": "node_modules/ace-builds/src-min/worker-css.js" }, - { "bundleName": "worker-json", "input": "node_modules/ace-builds/src-min/worker-json.js" }, - { "bundleName": "worker-javascript", "input": "node_modules/ace-builds/src-min/worker-javascript.js" } + "node_modules/ace-builds/src-min/snippets/javascript.js" ], "es5BrowserSupport": true, "customWebpackConfig": { diff --git a/ui-ngx/extra-webpack.config.js b/ui-ngx/extra-webpack.config.js index c86417512a..527470d162 100644 --- a/ui-ngx/extra-webpack.config.js +++ b/ui-ngx/extra-webpack.config.js @@ -34,7 +34,7 @@ module.exports = { new CompressionPlugin({ filename: "[path].gz[query]", algorithm: "gzip", - test: /\.js$|\.css$|\.svg$|\.ttf$|\.woff$|\.woff2$|\.eot$|\.json$/, + test: /\.js$|\.css$|\.html$|\.svg?.+$|\.jpg$|\.ttf?.+$|\.woff?.+$|\.eot?.+$|\.json$/, threshold: 10240, minRatio: 0.8, deleteOriginalAssets: false diff --git a/ui-ngx/src/app/core/http/widget.service.ts b/ui-ngx/src/app/core/http/widget.service.ts index 151a2865eb..4c0606b599 100644 --- a/ui-ngx/src/app/core/http/widget.service.ts +++ b/ui-ngx/src/app/core/http/widget.service.ts @@ -14,47 +14,29 @@ /// limitations under the License. /// -import {Injectable} from '@angular/core'; -import {defaultHttpOptions} from './http-utils'; -import { Observable, ReplaySubject, Subject, of, forkJoin, throwError } 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'; -import {WidgetsBundle} from '@shared/models/widgets-bundle.model'; -import { - WidgetControllerDescriptor, - WidgetInfo, - WidgetType, - WidgetTypeInstance, - widgetActionSources, - MissingWidgetType, toWidgetInfo, ErrorWidgetType -} from '@shared/models/widget.models'; +import { Injectable } from '@angular/core'; +import { defaultHttpOptions } from './http-utils'; +import { Observable } 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'; +import { WidgetsBundle } from '@shared/models/widgets-bundle.model'; +import { WidgetType } from '@shared/models/widget.models'; import { UtilsService } from '@core/services/utils.service'; -import { isFunction, isUndefined } from '@core/utils'; import { TranslateService } from '@ngx-translate/core'; -import { AuthPayload } from '@core/auth/auth.models'; -import cssjs from '@core/css/css'; import { ResourcesService } from '../services/resources.service'; -import { catchError, map, switchMap } from 'rxjs/operators'; @Injectable({ providedIn: 'root' }) export class WidgetService { - private cssParser = new cssjs(); - - private widgetsInfoInMemoryCache = new Map(); - - private widgetsInfoFetchQueue = new Map>>(); - constructor( private http: HttpClient, private utils: UtilsService, private resources: ResourcesService, private translate: TranslateService ) { - this.cssParser.testMode = false; } public getWidgetBundles(pageLink: PageLink, ignoreErrors: boolean = false, @@ -88,261 +70,4 @@ export class WidgetService { return this.http.get(`/api/widgetType?isSystem=${isSystem}&bundleAlias=${bundleAlias}&alias=${widgetTypeAlias}`, defaultHttpOptions(ignoreLoading, ignoreErrors)); } - - public getWidgetInfo(bundleAlias: string, widgetTypeAlias: string, isSystem: boolean): Observable { - const widgetInfoSubject = new ReplaySubject(); - const widgetInfo = this.getWidgetInfoFromCache(bundleAlias, widgetTypeAlias, isSystem); - if (widgetInfo) { - widgetInfoSubject.next(widgetInfo); - widgetInfoSubject.complete(); - } else { - if (this.utils.widgetEditMode) { - // TODO: - } else { - const key = this.createWidgetInfoCacheKey(bundleAlias, widgetTypeAlias, isSystem); - let fetchQueue = this.widgetsInfoFetchQueue.get(key); - if (fetchQueue) { - fetchQueue.push(widgetInfoSubject); - } else { - fetchQueue = new Array>(); - this.widgetsInfoFetchQueue.set(key, fetchQueue); - this.getWidgetType(bundleAlias, widgetTypeAlias, isSystem).subscribe( - (widgetType) => { - this.loadWidget(widgetType, bundleAlias, isSystem, widgetInfoSubject); - }, - () => { - widgetInfoSubject.next(MissingWidgetType); - widgetInfoSubject.complete(); - this.resolveWidgetsInfoFetchQueue(key, MissingWidgetType); - } - ); - } - } - } - return widgetInfoSubject.asObservable(); - } - - private loadWidget(widgetType: WidgetType, bundleAlias: string, isSystem: boolean, widgetInfoSubject: Subject) { - const widgetInfo = toWidgetInfo(widgetType); - const key = this.createWidgetInfoCacheKey(bundleAlias, widgetInfo.alias, isSystem); - this.loadWidgetResources(widgetInfo, bundleAlias, isSystem).subscribe( - () => { - let widgetControllerDescriptor: WidgetControllerDescriptor = null; - try { - widgetControllerDescriptor = this.createWidgetControllerDescriptor(widgetInfo, key); - } catch (e) { - const details = this.utils.parseException(e); - const errorMessage = `Failed to compile widget script. \n Error: ${details.message}`; - this.processWidgetLoadError([errorMessage], key, widgetInfoSubject); - } - if (widgetControllerDescriptor) { - if (widgetControllerDescriptor.settingsSchema) { - widgetInfo.typeSettingsSchema = widgetControllerDescriptor.settingsSchema; - } - if (widgetControllerDescriptor.dataKeySettingsSchema) { - widgetInfo.typeDataKeySettingsSchema = widgetControllerDescriptor.dataKeySettingsSchema; - } - widgetInfo.typeParameters = widgetControllerDescriptor.typeParameters; - widgetInfo.actionSources = widgetControllerDescriptor.actionSources; - widgetInfo.widgetTypeFunction = widgetControllerDescriptor.widgetTypeFunction; - this.putWidgetInfoToCache(widgetInfo, bundleAlias, widgetInfo.alias, isSystem); - if (widgetInfoSubject) { - widgetInfoSubject.next(widgetInfo); - widgetInfoSubject.complete(); - } - this.resolveWidgetsInfoFetchQueue(key, widgetInfo); - } - }, - (errorMessages: string[]) => { - this.processWidgetLoadError(errorMessages, key, widgetInfoSubject); - } - ); - } - - private loadWidgetResources(widgetInfo: WidgetInfo, bundleAlias: string, isSystem: boolean): Observable { - const widgetNamespace = `widget-type-${(isSystem ? 'sys-' : '')}${bundleAlias}-${widgetInfo.alias}`; - this.cssParser.cssPreviewNamespace = widgetNamespace; - this.cssParser.createStyleElement(widgetNamespace, widgetInfo.templateCss); - const resourceTasks: Observable[] = []; - if (widgetInfo.resources.length > 0) { - widgetInfo.resources.forEach((resource) => { - resourceTasks.push( - this.resources.loadResource(resource.url).pipe( - catchError(e => of(`Failed to load widget resource: '${resource.url}'`)) - ) - ); - }); - return forkJoin(resourceTasks).pipe( - switchMap(msgs => { - let errors: string[]; - if (msgs && msgs.length) { - errors = msgs.filter(msg => msg && msg.length > 0); - } - if (errors && errors.length) { - return throwError(errors); - } else { - return of(null); - } - } - )); - } else { - return of(null); - } - } - - private createWidgetControllerDescriptor(widgetInfo: WidgetInfo, name: string): WidgetControllerDescriptor { - let widgetTypeFunctionBody = `return function ${name} (ctx) {\n` + - ' var self = this;\n' + - ' self.ctx = ctx;\n\n'; /*+ - - ' self.onInit = function() {\n\n' + - - ' }\n\n' + - - ' self.onDataUpdated = function() {\n\n' + - - ' }\n\n' + - - ' self.useCustomDatasources = function() {\n\n' + - - ' }\n\n' + - - ' self.typeParameters = function() {\n\n' + - return { - useCustomDatasources: false, - maxDatasources: -1, //unlimited - maxDataKeys: -1, //unlimited - dataKeysOptional: false, - stateData: false - }; - ' }\n\n' + - - ' self.actionSources = function() {\n\n' + - return { - 'headerButton': { - name: 'Header button', - multiple: true - } - }; - }\n\n' + - ' self.onResize = function() {\n\n' + - - ' }\n\n' + - - ' self.onEditModeChanged = function() {\n\n' + - - ' }\n\n' + - - ' self.onMobileModeChanged = function() {\n\n' + - - ' }\n\n' + - - ' self.getSettingsSchema = function() {\n\n' + - - ' }\n\n' + - - ' self.getDataKeySettingsSchema = function() {\n\n' + - - ' }\n\n' + - - ' self.onDestroy = function() {\n\n' + - - ' }\n\n' + - '}';*/ - - widgetTypeFunctionBody += widgetInfo.controllerScript; - widgetTypeFunctionBody += '\n};\n'; - - try { - - const widgetTypeFunction = new Function(widgetTypeFunctionBody); - const widgetType = widgetTypeFunction.apply(this); - const widgetTypeInstance: WidgetTypeInstance = new widgetType(); - const result: WidgetControllerDescriptor = { - widgetTypeFunction: widgetType - }; - if (isFunction(widgetTypeInstance.getSettingsSchema)) { - result.settingsSchema = widgetTypeInstance.getSettingsSchema(); - } - if (isFunction(widgetTypeInstance.getDataKeySettingsSchema)) { - result.dataKeySettingsSchema = widgetTypeInstance.getDataKeySettingsSchema(); - } - if (isFunction(widgetTypeInstance.typeParameters)) { - result.typeParameters = widgetTypeInstance.typeParameters(); - } else { - result.typeParameters = {}; - } - if (isFunction(widgetTypeInstance.useCustomDatasources)) { - result.typeParameters.useCustomDatasources = widgetTypeInstance.useCustomDatasources(); - } else { - result.typeParameters.useCustomDatasources = false; - } - if (isUndefined(result.typeParameters.maxDatasources)) { - result.typeParameters.maxDatasources = -1; - } - if (isUndefined(result.typeParameters.maxDataKeys)) { - result.typeParameters.maxDataKeys = -1; - } - if (isUndefined(result.typeParameters.dataKeysOptional)) { - result.typeParameters.dataKeysOptional = false; - } - if (isUndefined(result.typeParameters.stateData)) { - result.typeParameters.stateData = false; - } - if (isFunction(widgetTypeInstance.actionSources)) { - result.actionSources = widgetTypeInstance.actionSources(); - } else { - result.actionSources = {}; - } - for (const actionSourceId of Object.keys(widgetActionSources)) { - result.actionSources[actionSourceId] = {...widgetActionSources[actionSourceId]}; - result.actionSources[actionSourceId].name = this.translate.instant(result.actionSources[actionSourceId].name); - } - return result; - } catch (e) { - this.utils.processWidgetException(e); - throw e; - } - } - - private processWidgetLoadError(errorMessages: string[], cacheKey: string, widgetInfoSubject: Subject) { - const widgetInfo = {...ErrorWidgetType}; - errorMessages.forEach(error => { - widgetInfo.templateHtml += `
${error}
`; - }); - widgetInfo.templateHtml += ''; - if (widgetInfoSubject) { - widgetInfoSubject.next(widgetInfo); - widgetInfoSubject.complete(); - } - this.resolveWidgetsInfoFetchQueue(cacheKey, widgetInfo); - } - - private resolveWidgetsInfoFetchQueue(key: string, widgetInfo: WidgetInfo) { - const fetchQueue = this.widgetsInfoFetchQueue.get(key); - if (fetchQueue) { - fetchQueue.forEach(subject => { - subject.next(widgetInfo); - subject.complete(); - }); - this.widgetsInfoFetchQueue.delete(key); - } - } - - // Cache functions - - private createWidgetInfoCacheKey(bundleAlias: string, widgetTypeAlias: string, isSystem: boolean): string { - return `${isSystem ? 'sys_' : ''}${bundleAlias}_${widgetTypeAlias}`; - } - - private getWidgetInfoFromCache(bundleAlias: string, widgetTypeAlias: string, isSystem: boolean): WidgetInfo | undefined { - const key = this.createWidgetInfoCacheKey(bundleAlias, widgetTypeAlias, isSystem); - return this.widgetsInfoInMemoryCache.get(key); - } - - private putWidgetInfoToCache(widgetInfo: WidgetInfo, bundleAlias: string, widgetTypeAlias: string, isSystem: boolean) { - const key = this.createWidgetInfoCacheKey(bundleAlias, widgetTypeAlias, isSystem); - this.widgetsInfoInMemoryCache.set(key, widgetInfo); - } - } 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 new file mode 100644 index 0000000000..1bfd9c6507 --- /dev/null +++ b/ui-ngx/src/app/core/services/dynamic-component-factory.service.ts @@ -0,0 +1,113 @@ +/// +/// 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 { + Compiler, + Component, + ComponentFactory, + Injectable, + Injector, + NgModule, + NgModuleRef, + OnDestroy, + Type +} from '@angular/core'; +import { Observable, ReplaySubject } from 'rxjs'; +import { CommonModule } from '@angular/common'; + +export abstract class DynamicComponentModule implements OnDestroy { + + ngOnDestroy(): void { + console.log('Module destroyed!'); + } + +} + +interface DynamicComponentModuleData { + moduleRef: NgModuleRef; + moduleType: Type; +} + +@Injectable( + { + providedIn: 'root' + } +) +export class DynamicComponentFactoryService { + + private dynamicComponentModulesMap = new Map, DynamicComponentModuleData>(); + + constructor(private compiler: Compiler, + private injector: Injector) { + } + + public createDynamicComponentFactory( + componentType: Type, + template: string, + modules?: Type[]): Observable> { + const dymamicComponentFactorySubject = new ReplaySubject>(); + const comp = this.createDynamicComponent(componentType, template); + let moduleImports = [CommonModule]; + if (modules) { + moduleImports = [...moduleImports, ...modules]; + } + // noinspection AngularInvalidImportedOrDeclaredSymbol,AngularInvalidEntryComponent + @NgModule({ + declarations: [comp], + entryComponents: [comp], + imports: moduleImports + }) + class DynamicComponentInstanceModule extends DynamicComponentModule {} + try { + this.compiler.compileModuleAsync(DynamicComponentInstanceModule).then( + (module) => { + const moduleRef = module.create(this.injector); + const factory = moduleRef.componentFactoryResolver.resolveComponentFactory(comp); + this.dynamicComponentModulesMap.set(factory, { + moduleRef, + moduleType: module.moduleType + }); + dymamicComponentFactorySubject.next(factory); + dymamicComponentFactorySubject.complete(); + } + ).catch( + (e) => { + dymamicComponentFactorySubject.error(e); + } + ); + } catch (e) { + dymamicComponentFactorySubject.error(e); + } + return dymamicComponentFactorySubject.asObservable(); + } + + public destroyDynamicComponentFactory(factory: ComponentFactory) { + const moduleData = this.dynamicComponentModulesMap.get(factory); + if (moduleData) { + moduleData.moduleRef.destroy(); + this.compiler.clearCacheFor(moduleData.moduleType); + this.dynamicComponentModulesMap.delete(factory); + } + } + + private createDynamicComponent(componentType: Type, template: string): Type { + // noinspection AngularMissingOrInvalidDeclarationInModule + return Component({ + template + })(componentType); + } + +} diff --git a/ui-ngx/src/app/core/services/utils.service.ts b/ui-ngx/src/app/core/services/utils.service.ts index 44f2a25fd0..687f53061a 100644 --- a/ui-ngx/src/app/core/services/utils.service.ts +++ b/ui-ngx/src/app/core/services/utils.service.ts @@ -16,7 +16,6 @@ import { Inject, Injectable } from '@angular/core'; import { WINDOW } from '@core/services/window.service'; -import { WidgetInfo } from '@shared/models/widget.models'; import { ExceptionData } from '@app/shared/models/error.models'; import { isUndefined } from '@core/utils'; import { WindowMessage } from '@shared/models/window-message.model'; @@ -30,7 +29,7 @@ export class UtilsService { iframeMode = false; widgetEditMode = false; - editWidgetInfo: WidgetInfo = null; + editWidgetInfo: any = null; constructor(@Inject(WINDOW) private window: Window, private translate: TranslateService) { 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 9418afc161..d306bc6a26 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 @@ -36,7 +36,8 @@ import { AddAttributeDialogComponent } from './attribute/add-attribute-dialog.co import { EditAttributeValuePanelComponent } from './attribute/edit-attribute-value-panel.component'; import { DashboardComponent } from '@home/components/dashboard/dashboard.component'; import { WidgetComponent } from '@home/components/widget/widget.component'; -import { DynamicWidgetComponentFactoryService } from './widget/dynamic-widget-component-factory.service'; +import { WidgetComponentService } from './widget/widget-component.service'; +import { LegendComponent } from '@home/components/widget/legend.component'; @NgModule({ entryComponents: [ @@ -69,7 +70,8 @@ import { DynamicWidgetComponentFactoryService } from './widget/dynamic-widget-co AddAttributeDialogComponent, EditAttributeValuePanelComponent, DashboardComponent, - WidgetComponent + WidgetComponent, + LegendComponent ], imports: [ CommonModule, @@ -88,10 +90,11 @@ import { DynamicWidgetComponentFactoryService } from './widget/dynamic-widget-co AlarmDetailsDialogComponent, AttributeTableComponent, DashboardComponent, - WidgetComponent + WidgetComponent, + LegendComponent ], providers: [ - DynamicWidgetComponentFactoryService + WidgetComponentService ] }) export class HomeComponentsModule { } diff --git a/ui-ngx/src/app/modules/home/components/widget/dynamic-widget-component-factory.service.ts b/ui-ngx/src/app/modules/home/components/widget/dynamic-widget-component-factory.service.ts deleted file mode 100644 index a6d39a1d45..0000000000 --- a/ui-ngx/src/app/modules/home/components/widget/dynamic-widget-component-factory.service.ts +++ /dev/null @@ -1,100 +0,0 @@ -/// -/// 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 { - Compiler, - Component, - ComponentFactory, - Injectable, - Injector, - NgModule, - NgModuleRef, - Type, - ViewEncapsulation -} from '@angular/core'; -import { - DynamicWidgetComponent, - DynamicWidgetComponentModule -} from '@home/components/widget/dynamic-widget.component'; -import { CommonModule } from '@angular/common'; -import { SharedModule } from '@shared/shared.module'; -import { Observable, ReplaySubject } from 'rxjs'; -import { HomeComponentsModule } from '../home-components.module'; -import { WidgetComponentsModule } from './widget-components.module'; - -interface DynamicWidgetComponentModuleData { - moduleRef: NgModuleRef; - moduleType: Type; -} - -@Injectable() -export class DynamicWidgetComponentFactoryService { - - private dynamicComponentModulesMap = new Map, DynamicWidgetComponentModuleData>(); - - constructor(private compiler: Compiler, - private injector: Injector) { - } - - public createDynamicWidgetComponentFactory(template: string): Observable> { - const dymamicWidgetComponentFactorySubject = new ReplaySubject>(); - const comp = this.createDynamicWidgetComponent(template); - // noinspection AngularInvalidImportedOrDeclaredSymbol,AngularInvalidEntryComponent - @NgModule({ - declarations: [comp], - entryComponents: [comp], - imports: [CommonModule, SharedModule, WidgetComponentsModule], - }) - class DynamicWidgetComponentInstanceModule extends DynamicWidgetComponentModule {} - this.compiler.compileModuleAsync(DynamicWidgetComponentInstanceModule).then( - (module) => { - const moduleRef = module.create(this.injector); - const factory = moduleRef.componentFactoryResolver.resolveComponentFactory(comp); - this.dynamicComponentModulesMap.set(factory, { - moduleRef, - moduleType: module.moduleType - }); - dymamicWidgetComponentFactorySubject.next(factory); - dymamicWidgetComponentFactorySubject.complete(); - } - ).catch( - (e) => { - dymamicWidgetComponentFactorySubject.error(`Failed to create dynamic widget component factory: ${e}`); - } - ); - return dymamicWidgetComponentFactorySubject.asObservable(); - } - - public destroyDynamicWidgetComponentFactory(factory: ComponentFactory) { - const moduleData = this.dynamicComponentModulesMap.get(factory); - if (moduleData) { - moduleData.moduleRef.destroy(); - this.compiler.clearCacheFor(moduleData.moduleType); - this.dynamicComponentModulesMap.delete(factory); - } - } - - private createDynamicWidgetComponent(template: string): Type { - // noinspection AngularMissingOrInvalidDeclarationInModule - @Component({ - template - }) - class DynamicWidgetInstanceComponent extends DynamicWidgetComponent { } - - return DynamicWidgetInstanceComponent; - } - -} diff --git a/ui-ngx/src/app/modules/home/components/widget/dynamic-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/dynamic-widget.component.ts index baa2972456..20fbc803fe 100644 --- a/ui-ngx/src/app/modules/home/components/widget/dynamic-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/dynamic-widget.component.ts @@ -21,24 +21,13 @@ import { AppState } from '@core/core.state'; import { WidgetContext, IDynamicWidgetComponent } from '@home/models/widget-component.models'; import { ExceptionData } from '@shared/models/error.models'; -export abstract class DynamicWidgetComponentModule implements OnDestroy { - - ngOnDestroy(): void { - console.log('Module destroyed!'); - } - -} - -export abstract class DynamicWidgetComponent extends PageComponent implements IDynamicWidgetComponent, OnInit, OnDestroy { +export class DynamicWidgetComponent extends PageComponent implements IDynamicWidgetComponent, OnInit, OnDestroy { @Input() widgetContext: WidgetContext; @Input() - widgetErrorData: ExceptionData; - - @Input() - loadingData: boolean; + errorMessages: string[]; [key: string]: any; @@ -51,7 +40,7 @@ export abstract class DynamicWidgetComponent extends PageComponent implements ID } ngOnDestroy(): void { - console.log('Component destroyed!'); + console.log('Widget component destroyed!'); } clearRpcError() { 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 new file mode 100644 index 0000000000..3d8adfa186 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts @@ -0,0 +1,366 @@ +/// +/// 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 } 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 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 { isFunction, isUndefined } from '@core/utils'; +import { TranslateService } from '@ngx-translate/core'; +import { DynamicWidgetComponent } from '@home/components/widget/dynamic-widget.component'; +import { SharedModule } from '@shared/shared.module'; +import { WidgetComponentsModule } from '@home/components/widget/widget-components.module'; + +@Injectable() +export class WidgetComponentService { + + private cssParser = new cssjs(); + + private widgetsInfoInMemoryCache = new Map(); + + private widgetsInfoFetchQueue = new Map>>(); + + private init$: Observable; + + private missingWidgetType: WidgetInfo; + private errorWidgetType: WidgetInfo; + + constructor(private dynamicComponentFactoryService: DynamicComponentFactoryService, + private widgetService: WidgetService, + private utils: UtilsService, + private resources: ResourcesService, + private translate: TranslateService) { + this.cssParser.testMode = false; + this.init(); + } + + private init(): Observable { + if (this.init$) { + return this.init$; + } else { + this.missingWidgetType = {...MissingWidgetType}; + this.errorWidgetType = {...ErrorWidgetType}; + const initSubject = new ReplaySubject(); + this.init$ = initSubject.asObservable(); + const loadDefaultWidgetInfoTasks = [ + this.loadWidgetResources(this.missingWidgetType, 'global-widget-missing-type'), + this.loadWidgetResources(this.errorWidgetType, 'global-widget-error-type'), + ]; + forkJoin(loadDefaultWidgetInfoTasks).subscribe( + () => { + initSubject.next(); + }, + () => { + console.error('Failed to load default widget types!'); + initSubject.error('Failed to load default widget types!'); + } + ); + return this.init$; + } + } + + public getWidgetInfo(bundleAlias: string, widgetTypeAlias: string, isSystem: boolean): Observable { + return this.init().pipe( + mergeMap(() => this.getWidgetInfoInternal(bundleAlias, widgetTypeAlias, isSystem)) + ); + } + + private getWidgetInfoInternal(bundleAlias: string, widgetTypeAlias: string, isSystem: boolean): Observable { + const widgetInfoSubject = new ReplaySubject(); + const widgetInfo = this.getWidgetInfoFromCache(bundleAlias, widgetTypeAlias, isSystem); + if (widgetInfo) { + widgetInfoSubject.next(widgetInfo); + widgetInfoSubject.complete(); + } else { + if (this.utils.widgetEditMode) { + // TODO: + } else { + const key = this.createWidgetInfoCacheKey(bundleAlias, widgetTypeAlias, isSystem); + let fetchQueue = this.widgetsInfoFetchQueue.get(key); + if (fetchQueue) { + fetchQueue.push(widgetInfoSubject); + } else { + fetchQueue = new Array>(); + this.widgetsInfoFetchQueue.set(key, fetchQueue); + this.widgetService.getWidgetType(bundleAlias, widgetTypeAlias, isSystem).subscribe( + (widgetType) => { + this.loadWidget(widgetType, bundleAlias, isSystem, widgetInfoSubject); + }, + () => { + widgetInfoSubject.next(this.missingWidgetType); + widgetInfoSubject.complete(); + this.resolveWidgetsInfoFetchQueue(key, this.missingWidgetType); + } + ); + } + } + } + return widgetInfoSubject.asObservable(); + } + + private loadWidget(widgetType: WidgetType, bundleAlias: string, isSystem: boolean, widgetInfoSubject: Subject) { + const widgetInfo = toWidgetInfo(widgetType); + const key = this.createWidgetInfoCacheKey(bundleAlias, widgetInfo.alias, isSystem); + let widgetControllerDescriptor: WidgetControllerDescriptor = null; + try { + widgetControllerDescriptor = this.createWidgetControllerDescriptor(widgetInfo, key); + } catch (e) { + const details = this.utils.parseException(e); + const errorMessage = `Failed to compile widget script. \n Error: ${details.message}`; + this.processWidgetLoadError([errorMessage], key, widgetInfoSubject); + } + if (widgetControllerDescriptor) { + const widgetNamespace = `widget-type-${(isSystem ? 'sys-' : '')}${bundleAlias}-${widgetInfo.alias}`; + this.loadWidgetResources(widgetInfo, widgetNamespace).subscribe( + () => { + if (widgetControllerDescriptor.settingsSchema) { + widgetInfo.typeSettingsSchema = widgetControllerDescriptor.settingsSchema; + } + if (widgetControllerDescriptor.dataKeySettingsSchema) { + widgetInfo.typeDataKeySettingsSchema = widgetControllerDescriptor.dataKeySettingsSchema; + } + widgetInfo.typeParameters = widgetControllerDescriptor.typeParameters; + widgetInfo.actionSources = widgetControllerDescriptor.actionSources; + widgetInfo.widgetTypeFunction = widgetControllerDescriptor.widgetTypeFunction; + this.putWidgetInfoToCache(widgetInfo, bundleAlias, widgetInfo.alias, isSystem); + if (widgetInfoSubject) { + widgetInfoSubject.next(widgetInfo); + widgetInfoSubject.complete(); + } + this.resolveWidgetsInfoFetchQueue(key, widgetInfo); + }, + (errorMessages: string[]) => { + this.processWidgetLoadError(errorMessages, key, widgetInfoSubject); + } + ); + } + } + + private loadWidgetResources(widgetInfo: WidgetInfo, widgetNamespace: string): Observable { + this.cssParser.cssPreviewNamespace = widgetNamespace; + this.cssParser.createStyleElement(widgetNamespace, widgetInfo.templateCss); + const resourceTasks: Observable[] = []; + if (widgetInfo.resources.length > 0) { + widgetInfo.resources.forEach((resource) => { + resourceTasks.push( + this.resources.loadResource(resource.url).pipe( + catchError(e => of(`Failed to load widget resource: '${resource.url}'`)) + ) + ); + }); + } + resourceTasks.push( + this.dynamicComponentFactoryService.createDynamicComponentFactory( + class DynamicWidgetComponentInstance extends DynamicWidgetComponent {}, + widgetInfo.templateHtml, + [SharedModule, WidgetComponentsModule] + ).pipe( + map((factory) => { + widgetInfo.componentFactory = factory; + return null; + }), + catchError(e => { + const details = this.utils.parseException(e); + const errorMessage = `Failed to compile widget html. \n Error: ${details.message}`; + return of(errorMessage); + }) + ) + ); + return forkJoin(resourceTasks).pipe( + switchMap(msgs => { + let errors: string[]; + if (msgs && msgs.length) { + errors = msgs.filter(msg => msg && msg.length > 0); + } + if (errors && errors.length) { + return throwError(errors); + } else { + return of(null); + } + } + )); + } + + private createWidgetControllerDescriptor(widgetInfo: WidgetInfo, name: string): WidgetControllerDescriptor { + let widgetTypeFunctionBody = `return function ${name} (ctx) {\n` + + ' var self = this;\n' + + ' self.ctx = ctx;\n\n'; /*+ + + ' self.onInit = function() {\n\n' + + + ' }\n\n' + + + ' self.onDataUpdated = function() {\n\n' + + + ' }\n\n' + + + ' self.useCustomDatasources = function() {\n\n' + + + ' }\n\n' + + + ' self.typeParameters = function() {\n\n' + + return { + useCustomDatasources: false, + maxDatasources: -1, //unlimited + maxDataKeys: -1, //unlimited + dataKeysOptional: false, + stateData: false + }; + ' }\n\n' + + + ' self.actionSources = function() {\n\n' + + return { + 'headerButton': { + name: 'Header button', + multiple: true + } + }; + }\n\n' + + ' self.onResize = function() {\n\n' + + + ' }\n\n' + + + ' self.onEditModeChanged = function() {\n\n' + + + ' }\n\n' + + + ' self.onMobileModeChanged = function() {\n\n' + + + ' }\n\n' + + + ' self.getSettingsSchema = function() {\n\n' + + + ' }\n\n' + + + ' self.getDataKeySettingsSchema = function() {\n\n' + + + ' }\n\n' + + + ' self.onDestroy = function() {\n\n' + + + ' }\n\n' + + '}';*/ + + widgetTypeFunctionBody += widgetInfo.controllerScript; + widgetTypeFunctionBody += '\n};\n'; + + try { + + const widgetTypeFunction = new Function(widgetTypeFunctionBody); + const widgetType = widgetTypeFunction.apply(this); + const widgetTypeInstance: WidgetTypeInstance = new widgetType(); + const result: WidgetControllerDescriptor = { + widgetTypeFunction: widgetType + }; + if (isFunction(widgetTypeInstance.getSettingsSchema)) { + result.settingsSchema = widgetTypeInstance.getSettingsSchema(); + } + if (isFunction(widgetTypeInstance.getDataKeySettingsSchema)) { + result.dataKeySettingsSchema = widgetTypeInstance.getDataKeySettingsSchema(); + } + if (isFunction(widgetTypeInstance.typeParameters)) { + result.typeParameters = widgetTypeInstance.typeParameters(); + } else { + result.typeParameters = {}; + } + if (isFunction(widgetTypeInstance.useCustomDatasources)) { + result.typeParameters.useCustomDatasources = widgetTypeInstance.useCustomDatasources(); + } else { + result.typeParameters.useCustomDatasources = false; + } + if (isUndefined(result.typeParameters.maxDatasources)) { + result.typeParameters.maxDatasources = -1; + } + if (isUndefined(result.typeParameters.maxDataKeys)) { + result.typeParameters.maxDataKeys = -1; + } + if (isUndefined(result.typeParameters.dataKeysOptional)) { + result.typeParameters.dataKeysOptional = false; + } + if (isUndefined(result.typeParameters.stateData)) { + result.typeParameters.stateData = false; + } + if (isFunction(widgetTypeInstance.actionSources)) { + result.actionSources = widgetTypeInstance.actionSources(); + } else { + result.actionSources = {}; + } + for (const actionSourceId of Object.keys(widgetActionSources)) { + result.actionSources[actionSourceId] = {...widgetActionSources[actionSourceId]}; + result.actionSources[actionSourceId].name = this.translate.instant(result.actionSources[actionSourceId].name); + } + return result; + } catch (e) { + this.utils.processWidgetException(e); + throw e; + } + } + + private processWidgetLoadError(errorMessages: string[], cacheKey: string, widgetInfoSubject: Subject) { + if (widgetInfoSubject) { + widgetInfoSubject.error({ + widgetInfo: this.errorWidgetType, + errorMessages + }); + } + this.resolveWidgetsInfoFetchQueue(cacheKey, this.errorWidgetType, errorMessages); + } + + private resolveWidgetsInfoFetchQueue(key: string, widgetInfo: WidgetInfo, errorMessages?: string[]) { + const fetchQueue = this.widgetsInfoFetchQueue.get(key); + if (fetchQueue) { + fetchQueue.forEach(subject => { + if (!errorMessages) { + subject.next(widgetInfo); + subject.complete(); + } else { + subject.error({ + widgetInfo, + errorMessages + }); + } + }); + this.widgetsInfoFetchQueue.delete(key); + } + } + + // Cache functions + + private createWidgetInfoCacheKey(bundleAlias: string, widgetTypeAlias: string, isSystem: boolean): string { + return `${isSystem ? 'sys_' : ''}${bundleAlias}_${widgetTypeAlias}`; + } + + private getWidgetInfoFromCache(bundleAlias: string, widgetTypeAlias: string, isSystem: boolean): WidgetInfo | undefined { + const key = this.createWidgetInfoCacheKey(bundleAlias, widgetTypeAlias, isSystem); + return this.widgetsInfoInMemoryCache.get(key); + } + + private putWidgetInfoToCache(widgetInfo: WidgetInfo, bundleAlias: string, widgetTypeAlias: string, isSystem: boolean) { + const key = this.createWidgetInfoCacheKey(bundleAlias, widgetTypeAlias, isSystem); + this.widgetsInfoInMemoryCache.set(key, widgetInfo); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts b/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts index 06e30fab0a..d81dcca350 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts @@ -25,14 +25,12 @@ import { LegendComponent } from '@home/components/widget/legend.component'; ], declarations: [ - LegendComponent ], imports: [ CommonModule, SharedModule ], exports: [ - LegendComponent ] }) export class WidgetComponentsModule { } diff --git a/ui-ngx/src/app/modules/home/components/widget/widget.component.html b/ui-ngx/src/app/modules/home/components/widget/widget.component.html index 2ec95ac4c8..2f943d93d3 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/widget.component.html @@ -15,4 +15,16 @@ limitations under the License. --> - +
+ Widget Error: {{ widgetErrorData.name + ": " + widgetErrorData.message}} +
+
+ +
+
+ +
+ +
+ +
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 99005a25f4..db0bb2b953 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 @@ -17,7 +17,6 @@ import { AfterViewInit, Component, - ComponentFactory, ComponentFactoryResolver, ComponentRef, ElementRef, @@ -38,34 +37,37 @@ import { LegendPosition, Widget, WidgetActionDescriptor, + widgetActionSources, WidgetActionType, - WidgetInfo, WidgetResource, - widgetType, - WidgetTypeInstance, - widgetActionSources + WidgetResource, + widgetType } from '@shared/models/widget.models'; import { PageComponent } from '@shared/components/page.component'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { WidgetService } from '@core/http/widget.service'; import { UtilsService } from '@core/services/utils.service'; -import { DynamicWidgetComponent } from '@home/components/widget/dynamic-widget.component'; -import { forkJoin, Observable, of, ReplaySubject, throwError } from 'rxjs'; -import { DynamicWidgetComponentFactoryService } from '@home/components/widget/dynamic-widget-component-factory.service'; +import { forkJoin, Observable, of, throwError } from 'rxjs'; import { isDefined, objToBase64 } from '@core/utils'; import * as $ from 'jquery'; -import { WidgetContext, WidgetHeaderAction } from '@home/models/widget-component.models'; +import { + IDynamicWidgetComponent, + WidgetContext, + WidgetHeaderAction, + WidgetInfo, + WidgetTypeInstance +} from '@home/models/widget-component.models'; import { EntityInfo, IWidgetSubscription, - SubscriptionInfo, - WidgetSubscriptionOptions, StateObject, StateParams, - WidgetSubscriptionContext + SubscriptionInfo, + WidgetSubscriptionContext, + WidgetSubscriptionOptions } from '@core/api/widget-api.models'; import { EntityId } from '@shared/models/id/entity-id'; -import { ActivatedRoute, Router, UrlSegment } from '@angular/router'; +import { ActivatedRoute, Router } from '@angular/router'; import cssjs from '@core/css/css'; import { ResourcesService } from '@core/services/resources.service'; import { catchError, switchMap } from 'rxjs/operators'; @@ -74,6 +76,7 @@ import { TimeService } from '@core/services/time.service'; import { DeviceService } from '@app/core/http/device.service'; import { AlarmService } from '@app/core/http/alarm.service'; import { ExceptionData } from '@shared/models/error.models'; +import { WidgetComponentService } from './widget-component.service'; @Component({ selector: 'tb-widget', @@ -99,14 +102,22 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI widget: Widget; widgetInfo: WidgetInfo; + errorMessages: string[]; widgetContext: WidgetContext; widgetType: any; widgetTypeInstance: WidgetTypeInstance; widgetErrorData: ExceptionData; + loadingData: boolean; - dynamicWidgetComponentFactory: ComponentFactory; - dynamicWidgetComponentRef: ComponentRef; - dynamicWidgetComponent: DynamicWidgetComponent; + displayLegend: boolean; + legendConfig: LegendConfig; + legendData: LegendData; + isLegendFirst: boolean; + legendContainerLayoutType: string; + legendStyle: {[klass: string]: any}; + + dynamicWidgetComponentRef: ComponentRef; + dynamicWidgetComponent: IDynamicWidgetComponent; subscriptionContext: WidgetSubscriptionContext; @@ -120,7 +131,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI constructor(protected store: Store, private route: ActivatedRoute, private router: Router, - private dynamicWidgetComponentFactoryService: DynamicWidgetComponentFactoryService, + private widgetComponentService: WidgetComponentService, private componentFactoryResolver: ComponentFactoryResolver, private elementRef: ElementRef, private injector: Injector, @@ -134,8 +145,67 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI } ngOnInit(): void { + + this.loadingData = true; + this.widget = this.dashboardWidget.widget; + this.displayLegend = isDefined(this.widget.config.showLegend) ? this.widget.config.showLegend + : this.widget.type === widgetType.timeseries; + + this.legendContainerLayoutType = 'column'; + + if (this.displayLegend) { + this.legendConfig = this.widget.config.legendConfig || + { + position: LegendPosition.bottom, + showMin: false, + showMax: false, + showAvg: this.widget.type === widgetType.timeseries, + showTotal: false + }; + this.legendData = { + keys: [], + data: [] + }; + if (this.legendConfig.position === LegendPosition.top || + this.legendConfig.position === LegendPosition.bottom) { + this.legendContainerLayoutType = 'column'; + } else { + this.legendContainerLayoutType = 'row'; + } + switch (this.legendConfig.position) { + case LegendPosition.top: + this.legendStyle = { + paddingBottom: '8px', + maxHeight: '50%', + overflowY: 'auto' + }; + break; + case LegendPosition.bottom: + this.legendStyle = { + paddingTop: '8px', + maxHeight: '50%', + overflowY: 'auto' + }; + break; + case LegendPosition.left: + this.legendStyle = { + paddingRight: '0px', + maxWidth: '50%', + overflowY: 'auto' + }; + break; + case LegendPosition.right: + this.legendStyle = { + paddingLeft: '0px', + maxWidth: '50%', + overflowY: 'auto' + }; + break; + } + } + const actionDescriptorsBySourceId: {[actionSourceId: string]: Array} = {}; if (this.widget.config.actions) { for (const actionSourceId of Object.keys(this.widget.config.actions)) { @@ -244,10 +314,15 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI aliasController: this.dashboard.aliasController }; - this.widgetService.getWidgetInfo(this.widget.bundleAlias, this.widget.typeAlias, this.widget.isSystemType).subscribe( + this.widgetComponentService.getWidgetInfo(this.widget.bundleAlias, this.widget.typeAlias, this.widget.isSystemType).subscribe( (widgetInfo) => { this.widgetInfo = widgetInfo; this.loadFromWidgetInfo(); + }, + (errorData) => { + this.widgetInfo = errorData.widgetInfo; + this.errorMessages = errorData.errorMessages; + this.loadFromWidgetInfo(); } ); @@ -363,14 +438,9 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI } private initialize() { - this.configureDynamicWidgetComponent().subscribe( - () => { - this.dynamicWidgetComponent.loadingData = false; - }, - (error) => { - // TODO: - } - ); + this.configureDynamicWidgetComponent(); + // TODO: + this.loadingData = false; } private destroyDynamicWidgetComponent() { @@ -381,127 +451,37 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI if (this.dynamicWidgetComponentRef) { this.dynamicWidgetComponentRef.destroy(); } - if (this.dynamicWidgetComponentFactory) { - this.dynamicWidgetComponentFactoryService.destroyDynamicWidgetComponentFactory(this.dynamicWidgetComponentFactory); - } } private handleWidgetException(e) { console.error(e); this.widgetErrorData = this.utils.processWidgetException(e); - if (this.dynamicWidgetComponent) { - this.dynamicWidgetComponent.widgetErrorData = this.widgetErrorData; - } } - private configureDynamicWidgetComponent(): Observable { + private configureDynamicWidgetComponent() { + this.widgetContentContainer.clear(); + this.dynamicWidgetComponentRef = this.widgetContentContainer.createComponent(this.widgetInfo.componentFactory); + this.dynamicWidgetComponent = this.dynamicWidgetComponentRef.instance; - const dynamicWidgetComponentSubject = new ReplaySubject(); + this.dynamicWidgetComponent.widgetContext = this.widgetContext; + this.dynamicWidgetComponent.errorMessages = this.errorMessages; - let html = '
' + - 'Widget Error: {{ widgetErrorData.name + ": " + widgetErrorData.message}}' + - '
' + - '
' + - '' + - '
'; + this.widgetContext.$scope = this.dynamicWidgetComponent; - let containerHtml = `
${this.widgetInfo.templateHtml}
`; + const containerElement = $(this.elementRef.nativeElement.querySelector('#widget-container')); - const displayLegend = isDefined(this.widget.config.showLegend) ? this.widget.config.showLegend - : this.widget.type === widgetType.timeseries; + this.widgetContext.$container = $('> ng-component', containerElement); + this.widgetContext.$container.css('display', 'block'); + this.widgetContext.$container.attr('id', 'container'); + this.widgetContext.$containerParent = $(containerElement); - let legendConfig: LegendConfig; - let legendData: LegendData; - if (displayLegend) { - legendConfig = this.widget.config.legendConfig || - { - position: LegendPosition.bottom, - showMin: false, - showMax: false, - showAvg: this.widget.type === widgetType.timeseries, - showTotal: false - }; - legendData = { - keys: [], - data: [] - }; - let layoutType; - if (legendConfig.position === LegendPosition.top || - legendConfig.position === LegendPosition.bottom) { - layoutType = 'column'; - } else { - layoutType = 'row'; + if (this.widgetSizeDetected) { + this.widgetContext.$container.css('height', this.widgetContext.height + 'px'); + this.widgetContext.$container.css('width', this.widgetContext.width + 'px'); } - let legendStyle; - switch (legendConfig.position) { - case LegendPosition.top: - legendStyle = 'padding-bottom: 8px; max-height: 50%; overflow-y: auto;'; - break; - case LegendPosition.bottom: - legendStyle = 'padding-top: 8px; max-height: 50%; overflow-y: auto;'; - break; - case LegendPosition.left: - legendStyle = 'padding-right: 0px; max-width: 50%; overflow-y: auto;'; - break; - case LegendPosition.right: - legendStyle = 'padding-left: 0px; max-width: 50%; overflow-y: auto;'; - break; - } - - const legendHtml = ``; - containerHtml = `
${containerHtml}
`; - html += `
`; - if (legendConfig.position === LegendPosition.top || - legendConfig.position === LegendPosition.left) { - html += legendHtml; - html += containerHtml; - } else { - html += containerHtml; - html += legendHtml; - } - html += '
'; - } else { - html += containerHtml; - } - - this.dynamicWidgetComponentFactoryService.createDynamicWidgetComponentFactory(html).subscribe( - (componentFactory) => { - this.dynamicWidgetComponentFactory = componentFactory; - this.widgetContentContainer.clear(); - this.dynamicWidgetComponentRef = this.widgetContentContainer.createComponent(this.dynamicWidgetComponentFactory); - this.dynamicWidgetComponent = this.dynamicWidgetComponentRef.instance; - - this.dynamicWidgetComponent.loadingData = true; - this.dynamicWidgetComponent.widgetContext = this.widgetContext; - this.dynamicWidgetComponent.widgetErrorData = this.widgetErrorData; - this.dynamicWidgetComponent.displayLegend = displayLegend; - this.dynamicWidgetComponent.legendConfig = legendConfig; - this.dynamicWidgetComponent.legendData = legendData; - - this.widgetContext.$scope = this.dynamicWidgetComponent; - const containerElement = displayLegend ? $(this.elementRef.nativeElement.querySelector('#widget-container')) - : $(this.elementRef.nativeElement); - - this.widgetContext.$container = $('#container', containerElement); - this.widgetContext.$containerParent = $(containerElement); - - if (this.widgetSizeDetected) { - this.widgetContext.$container.css('height', this.widgetContext.height + 'px'); - this.widgetContext.$container.css('width', this.widgetContext.width + 'px'); - } - - // @ts-ignore - addResizeListener(this.widgetContext.$containerParent[0], this.onResizeListener); - - dynamicWidgetComponentSubject.next(); - dynamicWidgetComponentSubject.complete(); - }, - (e) => { - dynamicWidgetComponentSubject.error(e); - } - ); - return dynamicWidgetComponentSubject.asObservable(); + // @ts-ignore + addResizeListener(this.widgetContext.$containerParent[0], this.onResizeListener); } private createSubscription(options: WidgetSubscriptionOptions, subscribe: boolean): Observable { 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 63a959c3d0..8bc55612e2 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 @@ -16,23 +16,30 @@ import { ExceptionData } from '@shared/models/error.models'; import { IDashboardComponent } from '@home/models/dashboard-component.models'; -import { WidgetActionDescriptor, WidgetConfig, WidgetConfigSettings, widgetType } from '@shared/models/widget.models'; +import { + WidgetActionDescriptor, + WidgetActionSource, + WidgetConfig, + WidgetConfigSettings, + WidgetControllerDescriptor, + WidgetType, + widgetType, + WidgetTypeDescriptor, + WidgetTypeParameters +} from '@shared/models/widget.models'; import { Timewindow } from '@shared/models/time/time.models'; import { EntityInfo, - IWidgetSubscription, - SubscriptionInfo, - WidgetSubscriptionOptions, - IStateController, IAliasController, - TimewindowFunctions, - WidgetSubscriptionApi, + IStateController, + IWidgetSubscription, + IWidgetUtils, RpcApi, + TimewindowFunctions, WidgetActionsApi, - IWidgetUtils + WidgetSubscriptionApi } from '@core/api/widget-api.models'; -import { Observable } from 'rxjs'; -import { EntityId } from '@shared/models/id/entity-id'; +import { ComponentFactory } from '@angular/core'; export interface IWidgetAction { name: string; @@ -49,13 +56,6 @@ export interface WidgetAction extends IWidgetAction { show: boolean; } -export interface IDynamicWidgetComponent { - widgetContext: WidgetContext; - widgetErrorData: ExceptionData; - loadingData: boolean; - [key: string]: any; -} - export interface WidgetContext { inited?: boolean; $container?: any; @@ -87,3 +87,93 @@ export interface WidgetContext { customHeaderActions?: Array; widgetActions?: Array; } + +export interface IDynamicWidgetComponent { + widgetContext: WidgetContext; + errorMessages: string[]; + [key: string]: any; +} + +export interface WidgetInfo extends WidgetTypeDescriptor, WidgetControllerDescriptor { + widgetName: string; + alias: string; + typeSettingsSchema?: string; + typeDataKeySettingsSchema?: string; + componentFactory?: ComponentFactory; +} + +export const MissingWidgetType: WidgetInfo = { + type: widgetType.latest, + widgetName: 'Widget type not found', + alias: 'undefined', + sizeX: 8, + sizeY: 6, + resources: [], + templateHtml: '
' + + '
widget.widget-type-not-found
' + + '
', + templateCss: '', + controllerScript: 'self.onInit = function() {}', + settingsSchema: '{}\n', + dataKeySettingsSchema: '{}\n', + defaultConfig: '{\n' + + '"title": "Widget type not found",\n' + + '"datasources": [],\n' + + '"settings": {}\n' + + '}\n' +}; + +export const ErrorWidgetType: WidgetInfo = { + type: widgetType.latest, + widgetName: 'Error loading widget', + alias: 'error', + sizeX: 8, + sizeY: 6, + resources: [], + templateHtml: '
' + + '
widget.widget-type-load-error
' + + '
{{ error }}
' + + '
', + templateCss: '', + controllerScript: 'self.onInit = function() {}', + settingsSchema: '{}\n', + dataKeySettingsSchema: '{}\n', + defaultConfig: '{\n' + + '"title": "Widget failed to load",\n' + + '"datasources": [],\n' + + '"settings": {}\n' + + '}\n' +}; + +export interface WidgetTypeInstance { + getSettingsSchema?: () => string; + getDataKeySettingsSchema?: () => string; + typeParameters?: () => WidgetTypeParameters; + useCustomDatasources?: () => boolean; + actionSources?: () => {[key: string]: WidgetActionSource}; + + onInit?: () => void; + onDataUpdated?: () => void; + onResize?: () => void; + onEditModeChanged?: () => void; + onMobileModeChanged?: () => void; + onDestroy?: () => void; +} + +export function toWidgetInfo(widgetTypeEntity: WidgetType): WidgetInfo { + return { + widgetName: widgetTypeEntity.name, + alias: widgetTypeEntity.alias, + type: widgetTypeEntity.descriptor.type, + sizeX: widgetTypeEntity.descriptor.sizeX, + sizeY: widgetTypeEntity.descriptor.sizeY, + resources: widgetTypeEntity.descriptor.resources, + templateHtml: widgetTypeEntity.descriptor.templateHtml, + templateCss: widgetTypeEntity.descriptor.templateCss, + controllerScript: widgetTypeEntity.descriptor.controllerScript, + settingsSchema: widgetTypeEntity.descriptor.settingsSchema, + dataKeySettingsSchema: widgetTypeEntity.descriptor.dataKeySettingsSchema, + defaultConfig: widgetTypeEntity.descriptor.defaultConfig + }; +} + diff --git a/ui-ngx/src/app/modules/home/pages/widget/widget-library.component.ts b/ui-ngx/src/app/modules/home/pages/widget/widget-library.component.ts index 3707e0c277..a1ba47ee1c 100644 --- a/ui-ngx/src/app/modules/home/pages/widget/widget-library.component.ts +++ b/ui-ngx/src/app/modules/home/pages/widget/widget-library.component.ts @@ -25,13 +25,14 @@ import { ActivatedRoute } from '@angular/router'; import { Authority } from '@shared/models/authority.enum'; import { NULL_UUID } from '@shared/models/id/has-uuid'; import { Observable } from 'rxjs'; -import { toWidgetInfo, 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 { map, share } from 'rxjs/operators'; import { DialogService } from '@core/services/dialog.service'; import { FooterFabButtons } from '@app/shared/components/footer-fab-buttons.component'; import { DashboardCallbacks, WidgetsData } from '@home/models/dashboard-component.models'; import { IAliasController } from '@app/core/api/widget-api.models'; +import { toWidgetInfo } from '@home/models/widget-component.models'; @Component({ selector: 'tb-widget-library', diff --git a/ui-ngx/src/app/shared/models/widget.models.ts b/ui-ngx/src/app/shared/models/widget.models.ts index f40e380364..daba2d232a 100644 --- a/ui-ngx/src/app/shared/models/widget.models.ts +++ b/ui-ngx/src/app/shared/models/widget.models.ts @@ -14,11 +14,9 @@ /// limitations under the License. /// -import {BaseData} from '@shared/models/base-data'; -import {TenantId} from '@shared/models/id/tenant-id'; -import {WidgetsBundleId} from '@shared/models/id/widgets-bundle-id'; -import {WidgetTypeId} from '@shared/models/id/widget-type-id'; -import { AliasEntityType, EntityType, EntityTypeTranslation } from '@shared/models/entity-type.models'; +import { BaseData } from '@shared/models/base-data'; +import { TenantId } from '@shared/models/id/tenant-id'; +import { WidgetTypeId } from '@shared/models/id/widget-type-id'; import { Timewindow } from '@shared/models/time/time.models'; export enum widgetType { @@ -139,86 +137,6 @@ export interface WidgetType extends BaseData { descriptor: WidgetTypeDescriptor; } -export interface WidgetInfo extends WidgetTypeDescriptor, WidgetControllerDescriptor { - widgetName: string; - alias: string; - typeSettingsSchema?: string; - typeDataKeySettingsSchema?: string; -} - -export const MissingWidgetType: WidgetInfo = { - type: widgetType.latest, - widgetName: 'Widget type not found', - alias: 'undefined', - sizeX: 8, - sizeY: 6, - resources: [], - templateHtml: '
' + - '
widget.widget-type-not-found
' + - '
', - templateCss: '', - controllerScript: 'self.onInit = function() {}', - settingsSchema: '{}\n', - dataKeySettingsSchema: '{}\n', - defaultConfig: '{\n' + - '"title": "Widget type not found",\n' + - '"datasources": [],\n' + - '"settings": {}\n' + - '}\n' -}; - -export const ErrorWidgetType: WidgetInfo = { - type: widgetType.latest, - widgetName: 'Error loading widget', - alias: 'error', - sizeX: 8, - sizeY: 6, - resources: [], - templateHtml: '
' + - '
widget.widget-type-load-error
', - templateCss: '', - controllerScript: 'self.onInit = function() {}', - settingsSchema: '{}\n', - dataKeySettingsSchema: '{}\n', - defaultConfig: '{\n' + - '"title": "Widget failed to load",\n' + - '"datasources": [],\n' + - '"settings": {}\n' + - '}\n' -}; - -export interface WidgetTypeInstance { - getSettingsSchema?: () => string; - getDataKeySettingsSchema?: () => string; - typeParameters?: () => WidgetTypeParameters; - useCustomDatasources?: () => boolean; - actionSources?: () => {[key: string]: WidgetActionSource}; - - onInit?: () => void; - onDataUpdated?: () => void; - onResize?: () => void; - onEditModeChanged?: () => void; - onMobileModeChanged?: () => void; - onDestroy?: () => void; -} - -export function toWidgetInfo(widgetTypeEntity: WidgetType): WidgetInfo { - return { - widgetName: widgetTypeEntity.name, - alias: widgetTypeEntity.alias, - type: widgetTypeEntity.descriptor.type, - sizeX: widgetTypeEntity.descriptor.sizeX, - sizeY: widgetTypeEntity.descriptor.sizeY, - resources: widgetTypeEntity.descriptor.resources, - templateHtml: widgetTypeEntity.descriptor.templateHtml, - templateCss: widgetTypeEntity.descriptor.templateCss, - controllerScript: widgetTypeEntity.descriptor.controllerScript, - settingsSchema: widgetTypeEntity.descriptor.settingsSchema, - dataKeySettingsSchema: widgetTypeEntity.descriptor.dataKeySettingsSchema, - defaultConfig: widgetTypeEntity.descriptor.defaultConfig - }; -} - export enum LegendDirection { column = 'column', row = 'row'