Commit ab0a1d1ea7b491b144bf37b4324686c6200be015

Authored by Igor Kulikov
1 parent 9ec843cb

Improve dynamic widgets loading.

... ... @@ -24,7 +24,11 @@
24 24 "tsConfig": "src/tsconfig.app.json",
25 25 "assets": [
26 26 "src/thingsboard.ico",
27   - "src/assets"
  27 + "src/assets",
  28 + { "glob": "worker-html.js", "input": "./node_modules/ace-builds/src-min/", "output": "/" },
  29 + { "glob": "worker-css.js", "input": "./node_modules/ace-builds/src-min/", "output": "/" },
  30 + { "glob": "worker-json.js", "input": "./node_modules/ace-builds/src-min/", "output": "/" },
  31 + { "glob": "worker-javascript.js", "input": "./node_modules/ace-builds/src-min/", "output": "/" }
28 32 ],
29 33 "styles": [
30 34 "src/styles.scss"
... ... @@ -49,11 +53,7 @@
49 53 "node_modules/ace-builds/src-min/snippets/css.js",
50 54 "node_modules/ace-builds/src-min/snippets/json.js",
51 55 "node_modules/ace-builds/src-min/snippets/java.js",
52   - "node_modules/ace-builds/src-min/snippets/javascript.js",
53   - { "bundleName": "worker-html", "input": "node_modules/ace-builds/src-min/worker-html.js" },
54   - { "bundleName": "worker-css", "input": "node_modules/ace-builds/src-min/worker-css.js" },
55   - { "bundleName": "worker-json", "input": "node_modules/ace-builds/src-min/worker-json.js" },
56   - { "bundleName": "worker-javascript", "input": "node_modules/ace-builds/src-min/worker-javascript.js" }
  56 + "node_modules/ace-builds/src-min/snippets/javascript.js"
57 57 ],
58 58 "es5BrowserSupport": true,
59 59 "customWebpackConfig": {
... ...
... ... @@ -34,7 +34,7 @@ module.exports = {
34 34 new CompressionPlugin({
35 35 filename: "[path].gz[query]",
36 36 algorithm: "gzip",
37   - test: /\.js$|\.css$|\.svg$|\.ttf$|\.woff$|\.woff2$|\.eot$|\.json$/,
  37 + test: /\.js$|\.css$|\.html$|\.svg?.+$|\.jpg$|\.ttf?.+$|\.woff?.+$|\.eot?.+$|\.json$/,
38 38 threshold: 10240,
39 39 minRatio: 0.8,
40 40 deleteOriginalAssets: false
... ...
... ... @@ -14,47 +14,29 @@
14 14 /// limitations under the License.
15 15 ///
16 16
17   -import {Injectable} from '@angular/core';
18   -import {defaultHttpOptions} from './http-utils';
19   -import { Observable, ReplaySubject, Subject, of, forkJoin, throwError } from 'rxjs/index';
20   -import {HttpClient} from '@angular/common/http';
21   -import {PageLink} from '@shared/models/page/page-link';
22   -import {PageData} from '@shared/models/page/page-data';
23   -import {WidgetsBundle} from '@shared/models/widgets-bundle.model';
24   -import {
25   - WidgetControllerDescriptor,
26   - WidgetInfo,
27   - WidgetType,
28   - WidgetTypeInstance,
29   - widgetActionSources,
30   - MissingWidgetType, toWidgetInfo, ErrorWidgetType
31   -} from '@shared/models/widget.models';
  17 +import { Injectable } from '@angular/core';
  18 +import { defaultHttpOptions } from './http-utils';
  19 +import { Observable } from 'rxjs/index';
  20 +import { HttpClient } from '@angular/common/http';
  21 +import { PageLink } from '@shared/models/page/page-link';
  22 +import { PageData } from '@shared/models/page/page-data';
  23 +import { WidgetsBundle } from '@shared/models/widgets-bundle.model';
  24 +import { WidgetType } from '@shared/models/widget.models';
32 25 import { UtilsService } from '@core/services/utils.service';
33   -import { isFunction, isUndefined } from '@core/utils';
34 26 import { TranslateService } from '@ngx-translate/core';
35   -import { AuthPayload } from '@core/auth/auth.models';
36   -import cssjs from '@core/css/css';
37 27 import { ResourcesService } from '../services/resources.service';
38   -import { catchError, map, switchMap } from 'rxjs/operators';
39 28
40 29 @Injectable({
41 30 providedIn: 'root'
42 31 })
43 32 export class WidgetService {
44 33
45   - private cssParser = new cssjs();
46   -
47   - private widgetsInfoInMemoryCache = new Map<string, WidgetInfo>();
48   -
49   - private widgetsInfoFetchQueue = new Map<string, Array<Subject<WidgetInfo>>>();
50   -
51 34 constructor(
52 35 private http: HttpClient,
53 36 private utils: UtilsService,
54 37 private resources: ResourcesService,
55 38 private translate: TranslateService
56 39 ) {
57   - this.cssParser.testMode = false;
58 40 }
59 41
60 42 public getWidgetBundles(pageLink: PageLink, ignoreErrors: boolean = false,
... ... @@ -88,261 +70,4 @@ export class WidgetService {
88 70 return this.http.get<WidgetType>(`/api/widgetType?isSystem=${isSystem}&bundleAlias=${bundleAlias}&alias=${widgetTypeAlias}`,
89 71 defaultHttpOptions(ignoreLoading, ignoreErrors));
90 72 }
91   -
92   - public getWidgetInfo(bundleAlias: string, widgetTypeAlias: string, isSystem: boolean): Observable<WidgetInfo> {
93   - const widgetInfoSubject = new ReplaySubject<WidgetInfo>();
94   - const widgetInfo = this.getWidgetInfoFromCache(bundleAlias, widgetTypeAlias, isSystem);
95   - if (widgetInfo) {
96   - widgetInfoSubject.next(widgetInfo);
97   - widgetInfoSubject.complete();
98   - } else {
99   - if (this.utils.widgetEditMode) {
100   - // TODO:
101   - } else {
102   - const key = this.createWidgetInfoCacheKey(bundleAlias, widgetTypeAlias, isSystem);
103   - let fetchQueue = this.widgetsInfoFetchQueue.get(key);
104   - if (fetchQueue) {
105   - fetchQueue.push(widgetInfoSubject);
106   - } else {
107   - fetchQueue = new Array<Subject<WidgetInfo>>();
108   - this.widgetsInfoFetchQueue.set(key, fetchQueue);
109   - this.getWidgetType(bundleAlias, widgetTypeAlias, isSystem).subscribe(
110   - (widgetType) => {
111   - this.loadWidget(widgetType, bundleAlias, isSystem, widgetInfoSubject);
112   - },
113   - () => {
114   - widgetInfoSubject.next(MissingWidgetType);
115   - widgetInfoSubject.complete();
116   - this.resolveWidgetsInfoFetchQueue(key, MissingWidgetType);
117   - }
118   - );
119   - }
120   - }
121   - }
122   - return widgetInfoSubject.asObservable();
123   - }
124   -
125   - private loadWidget(widgetType: WidgetType, bundleAlias: string, isSystem: boolean, widgetInfoSubject: Subject<WidgetInfo>) {
126   - const widgetInfo = toWidgetInfo(widgetType);
127   - const key = this.createWidgetInfoCacheKey(bundleAlias, widgetInfo.alias, isSystem);
128   - this.loadWidgetResources(widgetInfo, bundleAlias, isSystem).subscribe(
129   - () => {
130   - let widgetControllerDescriptor: WidgetControllerDescriptor = null;
131   - try {
132   - widgetControllerDescriptor = this.createWidgetControllerDescriptor(widgetInfo, key);
133   - } catch (e) {
134   - const details = this.utils.parseException(e);
135   - const errorMessage = `Failed to compile widget script. \n Error: ${details.message}`;
136   - this.processWidgetLoadError([errorMessage], key, widgetInfoSubject);
137   - }
138   - if (widgetControllerDescriptor) {
139   - if (widgetControllerDescriptor.settingsSchema) {
140   - widgetInfo.typeSettingsSchema = widgetControllerDescriptor.settingsSchema;
141   - }
142   - if (widgetControllerDescriptor.dataKeySettingsSchema) {
143   - widgetInfo.typeDataKeySettingsSchema = widgetControllerDescriptor.dataKeySettingsSchema;
144   - }
145   - widgetInfo.typeParameters = widgetControllerDescriptor.typeParameters;
146   - widgetInfo.actionSources = widgetControllerDescriptor.actionSources;
147   - widgetInfo.widgetTypeFunction = widgetControllerDescriptor.widgetTypeFunction;
148   - this.putWidgetInfoToCache(widgetInfo, bundleAlias, widgetInfo.alias, isSystem);
149   - if (widgetInfoSubject) {
150   - widgetInfoSubject.next(widgetInfo);
151   - widgetInfoSubject.complete();
152   - }
153   - this.resolveWidgetsInfoFetchQueue(key, widgetInfo);
154   - }
155   - },
156   - (errorMessages: string[]) => {
157   - this.processWidgetLoadError(errorMessages, key, widgetInfoSubject);
158   - }
159   - );
160   - }
161   -
162   - private loadWidgetResources(widgetInfo: WidgetInfo, bundleAlias: string, isSystem: boolean): Observable<any> {
163   - const widgetNamespace = `widget-type-${(isSystem ? 'sys-' : '')}${bundleAlias}-${widgetInfo.alias}`;
164   - this.cssParser.cssPreviewNamespace = widgetNamespace;
165   - this.cssParser.createStyleElement(widgetNamespace, widgetInfo.templateCss);
166   - const resourceTasks: Observable<string>[] = [];
167   - if (widgetInfo.resources.length > 0) {
168   - widgetInfo.resources.forEach((resource) => {
169   - resourceTasks.push(
170   - this.resources.loadResource(resource.url).pipe(
171   - catchError(e => of(`Failed to load widget resource: '${resource.url}'`))
172   - )
173   - );
174   - });
175   - return forkJoin(resourceTasks).pipe(
176   - switchMap(msgs => {
177   - let errors: string[];
178   - if (msgs && msgs.length) {
179   - errors = msgs.filter(msg => msg && msg.length > 0);
180   - }
181   - if (errors && errors.length) {
182   - return throwError(errors);
183   - } else {
184   - return of(null);
185   - }
186   - }
187   - ));
188   - } else {
189   - return of(null);
190   - }
191   - }
192   -
193   - private createWidgetControllerDescriptor(widgetInfo: WidgetInfo, name: string): WidgetControllerDescriptor {
194   - let widgetTypeFunctionBody = `return function ${name} (ctx) {\n` +
195   - ' var self = this;\n' +
196   - ' self.ctx = ctx;\n\n'; /*+
197   -
198   - ' self.onInit = function() {\n\n' +
199   -
200   - ' }\n\n' +
201   -
202   - ' self.onDataUpdated = function() {\n\n' +
203   -
204   - ' }\n\n' +
205   -
206   - ' self.useCustomDatasources = function() {\n\n' +
207   -
208   - ' }\n\n' +
209   -
210   - ' self.typeParameters = function() {\n\n' +
211   - return {
212   - useCustomDatasources: false,
213   - maxDatasources: -1, //unlimited
214   - maxDataKeys: -1, //unlimited
215   - dataKeysOptional: false,
216   - stateData: false
217   - };
218   - ' }\n\n' +
219   -
220   - ' self.actionSources = function() {\n\n' +
221   - return {
222   - 'headerButton': {
223   - name: 'Header button',
224   - multiple: true
225   - }
226   - };
227   - }\n\n' +
228   - ' self.onResize = function() {\n\n' +
229   -
230   - ' }\n\n' +
231   -
232   - ' self.onEditModeChanged = function() {\n\n' +
233   -
234   - ' }\n\n' +
235   -
236   - ' self.onMobileModeChanged = function() {\n\n' +
237   -
238   - ' }\n\n' +
239   -
240   - ' self.getSettingsSchema = function() {\n\n' +
241   -
242   - ' }\n\n' +
243   -
244   - ' self.getDataKeySettingsSchema = function() {\n\n' +
245   -
246   - ' }\n\n' +
247   -
248   - ' self.onDestroy = function() {\n\n' +
249   -
250   - ' }\n\n' +
251   - '}';*/
252   -
253   - widgetTypeFunctionBody += widgetInfo.controllerScript;
254   - widgetTypeFunctionBody += '\n};\n';
255   -
256   - try {
257   -
258   - const widgetTypeFunction = new Function(widgetTypeFunctionBody);
259   - const widgetType = widgetTypeFunction.apply(this);
260   - const widgetTypeInstance: WidgetTypeInstance = new widgetType();
261   - const result: WidgetControllerDescriptor = {
262   - widgetTypeFunction: widgetType
263   - };
264   - if (isFunction(widgetTypeInstance.getSettingsSchema)) {
265   - result.settingsSchema = widgetTypeInstance.getSettingsSchema();
266   - }
267   - if (isFunction(widgetTypeInstance.getDataKeySettingsSchema)) {
268   - result.dataKeySettingsSchema = widgetTypeInstance.getDataKeySettingsSchema();
269   - }
270   - if (isFunction(widgetTypeInstance.typeParameters)) {
271   - result.typeParameters = widgetTypeInstance.typeParameters();
272   - } else {
273   - result.typeParameters = {};
274   - }
275   - if (isFunction(widgetTypeInstance.useCustomDatasources)) {
276   - result.typeParameters.useCustomDatasources = widgetTypeInstance.useCustomDatasources();
277   - } else {
278   - result.typeParameters.useCustomDatasources = false;
279   - }
280   - if (isUndefined(result.typeParameters.maxDatasources)) {
281   - result.typeParameters.maxDatasources = -1;
282   - }
283   - if (isUndefined(result.typeParameters.maxDataKeys)) {
284   - result.typeParameters.maxDataKeys = -1;
285   - }
286   - if (isUndefined(result.typeParameters.dataKeysOptional)) {
287   - result.typeParameters.dataKeysOptional = false;
288   - }
289   - if (isUndefined(result.typeParameters.stateData)) {
290   - result.typeParameters.stateData = false;
291   - }
292   - if (isFunction(widgetTypeInstance.actionSources)) {
293   - result.actionSources = widgetTypeInstance.actionSources();
294   - } else {
295   - result.actionSources = {};
296   - }
297   - for (const actionSourceId of Object.keys(widgetActionSources)) {
298   - result.actionSources[actionSourceId] = {...widgetActionSources[actionSourceId]};
299   - result.actionSources[actionSourceId].name = this.translate.instant(result.actionSources[actionSourceId].name);
300   - }
301   - return result;
302   - } catch (e) {
303   - this.utils.processWidgetException(e);
304   - throw e;
305   - }
306   - }
307   -
308   - private processWidgetLoadError(errorMessages: string[], cacheKey: string, widgetInfoSubject: Subject<WidgetInfo>) {
309   - const widgetInfo = {...ErrorWidgetType};
310   - errorMessages.forEach(error => {
311   - widgetInfo.templateHtml += `<div class="tb-widget-error-msg">${error}</div>`;
312   - });
313   - widgetInfo.templateHtml += '</div>';
314   - if (widgetInfoSubject) {
315   - widgetInfoSubject.next(widgetInfo);
316   - widgetInfoSubject.complete();
317   - }
318   - this.resolveWidgetsInfoFetchQueue(cacheKey, widgetInfo);
319   - }
320   -
321   - private resolveWidgetsInfoFetchQueue(key: string, widgetInfo: WidgetInfo) {
322   - const fetchQueue = this.widgetsInfoFetchQueue.get(key);
323   - if (fetchQueue) {
324   - fetchQueue.forEach(subject => {
325   - subject.next(widgetInfo);
326   - subject.complete();
327   - });
328   - this.widgetsInfoFetchQueue.delete(key);
329   - }
330   - }
331   -
332   - // Cache functions
333   -
334   - private createWidgetInfoCacheKey(bundleAlias: string, widgetTypeAlias: string, isSystem: boolean): string {
335   - return `${isSystem ? 'sys_' : ''}${bundleAlias}_${widgetTypeAlias}`;
336   - }
337   -
338   - private getWidgetInfoFromCache(bundleAlias: string, widgetTypeAlias: string, isSystem: boolean): WidgetInfo | undefined {
339   - const key = this.createWidgetInfoCacheKey(bundleAlias, widgetTypeAlias, isSystem);
340   - return this.widgetsInfoInMemoryCache.get(key);
341   - }
342   -
343   - private putWidgetInfoToCache(widgetInfo: WidgetInfo, bundleAlias: string, widgetTypeAlias: string, isSystem: boolean) {
344   - const key = this.createWidgetInfoCacheKey(bundleAlias, widgetTypeAlias, isSystem);
345   - this.widgetsInfoInMemoryCache.set(key, widgetInfo);
346   - }
347   -
348 73 }
... ...
ui-ngx/src/app/core/services/dynamic-component-factory.service.ts renamed from ui-ngx/src/app/modules/home/components/widget/dynamic-widget-component-factory.service.ts
... ... @@ -22,63 +22,79 @@ import {
22 22 Injector,
23 23 NgModule,
24 24 NgModuleRef,
25   - Type,
26   - ViewEncapsulation
  25 + OnDestroy,
  26 + Type
27 27 } from '@angular/core';
28   -import {
29   - DynamicWidgetComponent,
30   - DynamicWidgetComponentModule
31   -} from '@home/components/widget/dynamic-widget.component';
32   -import { CommonModule } from '@angular/common';
33   -import { SharedModule } from '@shared/shared.module';
34 28 import { Observable, ReplaySubject } from 'rxjs';
35   -import { HomeComponentsModule } from '../home-components.module';
36   -import { WidgetComponentsModule } from './widget-components.module';
  29 +import { CommonModule } from '@angular/common';
  30 +
  31 +export abstract class DynamicComponentModule implements OnDestroy {
  32 +
  33 + ngOnDestroy(): void {
  34 + console.log('Module destroyed!');
  35 + }
37 36
38   -interface DynamicWidgetComponentModuleData {
39   - moduleRef: NgModuleRef<DynamicWidgetComponentModule>;
40   - moduleType: Type<DynamicWidgetComponentModule>;
41 37 }
42 38
43   -@Injectable()
44   -export class DynamicWidgetComponentFactoryService {
  39 +interface DynamicComponentModuleData {
  40 + moduleRef: NgModuleRef<DynamicComponentModule>;
  41 + moduleType: Type<DynamicComponentModule>;
  42 +}
45 43
46   - private dynamicComponentModulesMap = new Map<ComponentFactory<DynamicWidgetComponent>, DynamicWidgetComponentModuleData>();
  44 +@Injectable(
  45 + {
  46 + providedIn: 'root'
  47 + }
  48 +)
  49 +export class DynamicComponentFactoryService {
  50 +
  51 + private dynamicComponentModulesMap = new Map<ComponentFactory<any>, DynamicComponentModuleData>();
47 52
48 53 constructor(private compiler: Compiler,
49 54 private injector: Injector) {
50 55 }
51 56
52   - public createDynamicWidgetComponentFactory(template: string): Observable<ComponentFactory<DynamicWidgetComponent>> {
53   - const dymamicWidgetComponentFactorySubject = new ReplaySubject<ComponentFactory<DynamicWidgetComponent>>();
54   - const comp = this.createDynamicWidgetComponent(template);
  57 + public createDynamicComponentFactory<T>(
  58 + componentType: Type<T>,
  59 + template: string,
  60 + modules?: Type<any>[]): Observable<ComponentFactory<T>> {
  61 + const dymamicComponentFactorySubject = new ReplaySubject<ComponentFactory<T>>();
  62 + const comp = this.createDynamicComponent(componentType, template);
  63 + let moduleImports = [CommonModule];
  64 + if (modules) {
  65 + moduleImports = [...moduleImports, ...modules];
  66 + }
55 67 // noinspection AngularInvalidImportedOrDeclaredSymbol,AngularInvalidEntryComponent
56 68 @NgModule({
57 69 declarations: [comp],
58 70 entryComponents: [comp],
59   - imports: [CommonModule, SharedModule, WidgetComponentsModule],
  71 + imports: moduleImports
60 72 })
61   - class DynamicWidgetComponentInstanceModule extends DynamicWidgetComponentModule {}
62   - this.compiler.compileModuleAsync(DynamicWidgetComponentInstanceModule).then(
63   - (module) => {
64   - const moduleRef = module.create(this.injector);
65   - const factory = moduleRef.componentFactoryResolver.resolveComponentFactory(comp);
66   - this.dynamicComponentModulesMap.set(factory, {
67   - moduleRef,
68   - moduleType: module.moduleType
69   - });
70   - dymamicWidgetComponentFactorySubject.next(factory);
71   - dymamicWidgetComponentFactorySubject.complete();
72   - }
73   - ).catch(
74   - (e) => {
75   - dymamicWidgetComponentFactorySubject.error(`Failed to create dynamic widget component factory: ${e}`);
76   - }
77   - );
78   - return dymamicWidgetComponentFactorySubject.asObservable();
  73 + class DynamicComponentInstanceModule extends DynamicComponentModule {}
  74 + try {
  75 + this.compiler.compileModuleAsync(DynamicComponentInstanceModule).then(
  76 + (module) => {
  77 + const moduleRef = module.create(this.injector);
  78 + const factory = moduleRef.componentFactoryResolver.resolveComponentFactory(comp);
  79 + this.dynamicComponentModulesMap.set(factory, {
  80 + moduleRef,
  81 + moduleType: module.moduleType
  82 + });
  83 + dymamicComponentFactorySubject.next(factory);
  84 + dymamicComponentFactorySubject.complete();
  85 + }
  86 + ).catch(
  87 + (e) => {
  88 + dymamicComponentFactorySubject.error(e);
  89 + }
  90 + );
  91 + } catch (e) {
  92 + dymamicComponentFactorySubject.error(e);
  93 + }
  94 + return dymamicComponentFactorySubject.asObservable();
79 95 }
80 96
81   - public destroyDynamicWidgetComponentFactory(factory: ComponentFactory<DynamicWidgetComponent>) {
  97 + public destroyDynamicComponentFactory<T>(factory: ComponentFactory<T>) {
82 98 const moduleData = this.dynamicComponentModulesMap.get(factory);
83 99 if (moduleData) {
84 100 moduleData.moduleRef.destroy();
... ... @@ -87,14 +103,11 @@ export class DynamicWidgetComponentFactoryService {
87 103 }
88 104 }
89 105
90   - private createDynamicWidgetComponent(template: string): Type<DynamicWidgetComponent> {
  106 + private createDynamicComponent<T>(componentType: Type<T>, template: string): Type<T> {
91 107 // noinspection AngularMissingOrInvalidDeclarationInModule
92   - @Component({
  108 + return Component({
93 109 template
94   - })
95   - class DynamicWidgetInstanceComponent extends DynamicWidgetComponent { }
96   -
97   - return DynamicWidgetInstanceComponent;
  110 + })(componentType);
98 111 }
99 112
100 113 }
... ...
... ... @@ -16,7 +16,6 @@
16 16
17 17 import { Inject, Injectable } from '@angular/core';
18 18 import { WINDOW } from '@core/services/window.service';
19   -import { WidgetInfo } from '@shared/models/widget.models';
20 19 import { ExceptionData } from '@app/shared/models/error.models';
21 20 import { isUndefined } from '@core/utils';
22 21 import { WindowMessage } from '@shared/models/window-message.model';
... ... @@ -30,7 +29,7 @@ export class UtilsService {
30 29
31 30 iframeMode = false;
32 31 widgetEditMode = false;
33   - editWidgetInfo: WidgetInfo = null;
  32 + editWidgetInfo: any = null;
34 33
35 34 constructor(@Inject(WINDOW) private window: Window,
36 35 private translate: TranslateService) {
... ...
... ... @@ -36,7 +36,8 @@ import { AddAttributeDialogComponent } from './attribute/add-attribute-dialog.co
36 36 import { EditAttributeValuePanelComponent } from './attribute/edit-attribute-value-panel.component';
37 37 import { DashboardComponent } from '@home/components/dashboard/dashboard.component';
38 38 import { WidgetComponent } from '@home/components/widget/widget.component';
39   -import { DynamicWidgetComponentFactoryService } from './widget/dynamic-widget-component-factory.service';
  39 +import { WidgetComponentService } from './widget/widget-component.service';
  40 +import { LegendComponent } from '@home/components/widget/legend.component';
40 41
41 42 @NgModule({
42 43 entryComponents: [
... ... @@ -69,7 +70,8 @@ import { DynamicWidgetComponentFactoryService } from './widget/dynamic-widget-co
69 70 AddAttributeDialogComponent,
70 71 EditAttributeValuePanelComponent,
71 72 DashboardComponent,
72   - WidgetComponent
  73 + WidgetComponent,
  74 + LegendComponent
73 75 ],
74 76 imports: [
75 77 CommonModule,
... ... @@ -88,10 +90,11 @@ import { DynamicWidgetComponentFactoryService } from './widget/dynamic-widget-co
88 90 AlarmDetailsDialogComponent,
89 91 AttributeTableComponent,
90 92 DashboardComponent,
91   - WidgetComponent
  93 + WidgetComponent,
  94 + LegendComponent
92 95 ],
93 96 providers: [
94   - DynamicWidgetComponentFactoryService
  97 + WidgetComponentService
95 98 ]
96 99 })
97 100 export class HomeComponentsModule { }
... ...
... ... @@ -21,24 +21,13 @@ import { AppState } from '@core/core.state';
21 21 import { WidgetContext, IDynamicWidgetComponent } from '@home/models/widget-component.models';
22 22 import { ExceptionData } from '@shared/models/error.models';
23 23
24   -export abstract class DynamicWidgetComponentModule implements OnDestroy {
25   -
26   - ngOnDestroy(): void {
27   - console.log('Module destroyed!');
28   - }
29   -
30   -}
31   -
32   -export abstract class DynamicWidgetComponent extends PageComponent implements IDynamicWidgetComponent, OnInit, OnDestroy {
  24 +export class DynamicWidgetComponent extends PageComponent implements IDynamicWidgetComponent, OnInit, OnDestroy {
33 25
34 26 @Input()
35 27 widgetContext: WidgetContext;
36 28
37 29 @Input()
38   - widgetErrorData: ExceptionData;
39   -
40   - @Input()
41   - loadingData: boolean;
  30 + errorMessages: string[];
42 31
43 32 [key: string]: any;
44 33
... ... @@ -51,7 +40,7 @@ export abstract class DynamicWidgetComponent extends PageComponent implements ID
51 40 }
52 41
53 42 ngOnDestroy(): void {
54   - console.log('Component destroyed!');
  43 + console.log('Widget component destroyed!');
55 44 }
56 45
57 46 clearRpcError() {
... ...
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { Injectable } from '@angular/core';
  18 +import { DynamicComponentFactoryService } from '@core/services/dynamic-component-factory.service';
  19 +import { WidgetService } from '@core/http/widget.service';
  20 +import { forkJoin, Observable, of, ReplaySubject, Subject, throwError } from 'rxjs';
  21 +import { WidgetInfo, MissingWidgetType, toWidgetInfo, WidgetTypeInstance, ErrorWidgetType } from '@home/models/widget-component.models';
  22 +import cssjs from '@core/css/css';
  23 +import { UtilsService } from '@core/services/utils.service';
  24 +import { ResourcesService } from '@core/services/resources.service';
  25 +import {
  26 + widgetActionSources,
  27 + WidgetControllerDescriptor,
  28 + WidgetType
  29 +} from '@shared/models/widget.models';
  30 +import { catchError, switchMap, map, mergeMap } from 'rxjs/operators';
  31 +import { isFunction, isUndefined } from '@core/utils';
  32 +import { TranslateService } from '@ngx-translate/core';
  33 +import { DynamicWidgetComponent } from '@home/components/widget/dynamic-widget.component';
  34 +import { SharedModule } from '@shared/shared.module';
  35 +import { WidgetComponentsModule } from '@home/components/widget/widget-components.module';
  36 +
  37 +@Injectable()
  38 +export class WidgetComponentService {
  39 +
  40 + private cssParser = new cssjs();
  41 +
  42 + private widgetsInfoInMemoryCache = new Map<string, WidgetInfo>();
  43 +
  44 + private widgetsInfoFetchQueue = new Map<string, Array<Subject<WidgetInfo>>>();
  45 +
  46 + private init$: Observable<any>;
  47 +
  48 + private missingWidgetType: WidgetInfo;
  49 + private errorWidgetType: WidgetInfo;
  50 +
  51 + constructor(private dynamicComponentFactoryService: DynamicComponentFactoryService,
  52 + private widgetService: WidgetService,
  53 + private utils: UtilsService,
  54 + private resources: ResourcesService,
  55 + private translate: TranslateService) {
  56 + this.cssParser.testMode = false;
  57 + this.init();
  58 + }
  59 +
  60 + private init(): Observable<any> {
  61 + if (this.init$) {
  62 + return this.init$;
  63 + } else {
  64 + this.missingWidgetType = {...MissingWidgetType};
  65 + this.errorWidgetType = {...ErrorWidgetType};
  66 + const initSubject = new ReplaySubject();
  67 + this.init$ = initSubject.asObservable();
  68 + const loadDefaultWidgetInfoTasks = [
  69 + this.loadWidgetResources(this.missingWidgetType, 'global-widget-missing-type'),
  70 + this.loadWidgetResources(this.errorWidgetType, 'global-widget-error-type'),
  71 + ];
  72 + forkJoin(loadDefaultWidgetInfoTasks).subscribe(
  73 + () => {
  74 + initSubject.next();
  75 + },
  76 + () => {
  77 + console.error('Failed to load default widget types!');
  78 + initSubject.error('Failed to load default widget types!');
  79 + }
  80 + );
  81 + return this.init$;
  82 + }
  83 + }
  84 +
  85 + public getWidgetInfo(bundleAlias: string, widgetTypeAlias: string, isSystem: boolean): Observable<WidgetInfo> {
  86 + return this.init().pipe(
  87 + mergeMap(() => this.getWidgetInfoInternal(bundleAlias, widgetTypeAlias, isSystem))
  88 + );
  89 + }
  90 +
  91 + private getWidgetInfoInternal(bundleAlias: string, widgetTypeAlias: string, isSystem: boolean): Observable<WidgetInfo> {
  92 + const widgetInfoSubject = new ReplaySubject<WidgetInfo>();
  93 + const widgetInfo = this.getWidgetInfoFromCache(bundleAlias, widgetTypeAlias, isSystem);
  94 + if (widgetInfo) {
  95 + widgetInfoSubject.next(widgetInfo);
  96 + widgetInfoSubject.complete();
  97 + } else {
  98 + if (this.utils.widgetEditMode) {
  99 + // TODO:
  100 + } else {
  101 + const key = this.createWidgetInfoCacheKey(bundleAlias, widgetTypeAlias, isSystem);
  102 + let fetchQueue = this.widgetsInfoFetchQueue.get(key);
  103 + if (fetchQueue) {
  104 + fetchQueue.push(widgetInfoSubject);
  105 + } else {
  106 + fetchQueue = new Array<Subject<WidgetInfo>>();
  107 + this.widgetsInfoFetchQueue.set(key, fetchQueue);
  108 + this.widgetService.getWidgetType(bundleAlias, widgetTypeAlias, isSystem).subscribe(
  109 + (widgetType) => {
  110 + this.loadWidget(widgetType, bundleAlias, isSystem, widgetInfoSubject);
  111 + },
  112 + () => {
  113 + widgetInfoSubject.next(this.missingWidgetType);
  114 + widgetInfoSubject.complete();
  115 + this.resolveWidgetsInfoFetchQueue(key, this.missingWidgetType);
  116 + }
  117 + );
  118 + }
  119 + }
  120 + }
  121 + return widgetInfoSubject.asObservable();
  122 + }
  123 +
  124 + private loadWidget(widgetType: WidgetType, bundleAlias: string, isSystem: boolean, widgetInfoSubject: Subject<WidgetInfo>) {
  125 + const widgetInfo = toWidgetInfo(widgetType);
  126 + const key = this.createWidgetInfoCacheKey(bundleAlias, widgetInfo.alias, isSystem);
  127 + let widgetControllerDescriptor: WidgetControllerDescriptor = null;
  128 + try {
  129 + widgetControllerDescriptor = this.createWidgetControllerDescriptor(widgetInfo, key);
  130 + } catch (e) {
  131 + const details = this.utils.parseException(e);
  132 + const errorMessage = `Failed to compile widget script. \n Error: ${details.message}`;
  133 + this.processWidgetLoadError([errorMessage], key, widgetInfoSubject);
  134 + }
  135 + if (widgetControllerDescriptor) {
  136 + const widgetNamespace = `widget-type-${(isSystem ? 'sys-' : '')}${bundleAlias}-${widgetInfo.alias}`;
  137 + this.loadWidgetResources(widgetInfo, widgetNamespace).subscribe(
  138 + () => {
  139 + if (widgetControllerDescriptor.settingsSchema) {
  140 + widgetInfo.typeSettingsSchema = widgetControllerDescriptor.settingsSchema;
  141 + }
  142 + if (widgetControllerDescriptor.dataKeySettingsSchema) {
  143 + widgetInfo.typeDataKeySettingsSchema = widgetControllerDescriptor.dataKeySettingsSchema;
  144 + }
  145 + widgetInfo.typeParameters = widgetControllerDescriptor.typeParameters;
  146 + widgetInfo.actionSources = widgetControllerDescriptor.actionSources;
  147 + widgetInfo.widgetTypeFunction = widgetControllerDescriptor.widgetTypeFunction;
  148 + this.putWidgetInfoToCache(widgetInfo, bundleAlias, widgetInfo.alias, isSystem);
  149 + if (widgetInfoSubject) {
  150 + widgetInfoSubject.next(widgetInfo);
  151 + widgetInfoSubject.complete();
  152 + }
  153 + this.resolveWidgetsInfoFetchQueue(key, widgetInfo);
  154 + },
  155 + (errorMessages: string[]) => {
  156 + this.processWidgetLoadError(errorMessages, key, widgetInfoSubject);
  157 + }
  158 + );
  159 + }
  160 + }
  161 +
  162 + private loadWidgetResources(widgetInfo: WidgetInfo, widgetNamespace: string): Observable<any> {
  163 + this.cssParser.cssPreviewNamespace = widgetNamespace;
  164 + this.cssParser.createStyleElement(widgetNamespace, widgetInfo.templateCss);
  165 + const resourceTasks: Observable<string>[] = [];
  166 + if (widgetInfo.resources.length > 0) {
  167 + widgetInfo.resources.forEach((resource) => {
  168 + resourceTasks.push(
  169 + this.resources.loadResource(resource.url).pipe(
  170 + catchError(e => of(`Failed to load widget resource: '${resource.url}'`))
  171 + )
  172 + );
  173 + });
  174 + }
  175 + resourceTasks.push(
  176 + this.dynamicComponentFactoryService.createDynamicComponentFactory(
  177 + class DynamicWidgetComponentInstance extends DynamicWidgetComponent {},
  178 + widgetInfo.templateHtml,
  179 + [SharedModule, WidgetComponentsModule]
  180 + ).pipe(
  181 + map((factory) => {
  182 + widgetInfo.componentFactory = factory;
  183 + return null;
  184 + }),
  185 + catchError(e => {
  186 + const details = this.utils.parseException(e);
  187 + const errorMessage = `Failed to compile widget html. \n Error: ${details.message}`;
  188 + return of(errorMessage);
  189 + })
  190 + )
  191 + );
  192 + return forkJoin(resourceTasks).pipe(
  193 + switchMap(msgs => {
  194 + let errors: string[];
  195 + if (msgs && msgs.length) {
  196 + errors = msgs.filter(msg => msg && msg.length > 0);
  197 + }
  198 + if (errors && errors.length) {
  199 + return throwError(errors);
  200 + } else {
  201 + return of(null);
  202 + }
  203 + }
  204 + ));
  205 + }
  206 +
  207 + private createWidgetControllerDescriptor(widgetInfo: WidgetInfo, name: string): WidgetControllerDescriptor {
  208 + let widgetTypeFunctionBody = `return function ${name} (ctx) {\n` +
  209 + ' var self = this;\n' +
  210 + ' self.ctx = ctx;\n\n'; /*+
  211 +
  212 + ' self.onInit = function() {\n\n' +
  213 +
  214 + ' }\n\n' +
  215 +
  216 + ' self.onDataUpdated = function() {\n\n' +
  217 +
  218 + ' }\n\n' +
  219 +
  220 + ' self.useCustomDatasources = function() {\n\n' +
  221 +
  222 + ' }\n\n' +
  223 +
  224 + ' self.typeParameters = function() {\n\n' +
  225 + return {
  226 + useCustomDatasources: false,
  227 + maxDatasources: -1, //unlimited
  228 + maxDataKeys: -1, //unlimited
  229 + dataKeysOptional: false,
  230 + stateData: false
  231 + };
  232 + ' }\n\n' +
  233 +
  234 + ' self.actionSources = function() {\n\n' +
  235 + return {
  236 + 'headerButton': {
  237 + name: 'Header button',
  238 + multiple: true
  239 + }
  240 + };
  241 + }\n\n' +
  242 + ' self.onResize = function() {\n\n' +
  243 +
  244 + ' }\n\n' +
  245 +
  246 + ' self.onEditModeChanged = function() {\n\n' +
  247 +
  248 + ' }\n\n' +
  249 +
  250 + ' self.onMobileModeChanged = function() {\n\n' +
  251 +
  252 + ' }\n\n' +
  253 +
  254 + ' self.getSettingsSchema = function() {\n\n' +
  255 +
  256 + ' }\n\n' +
  257 +
  258 + ' self.getDataKeySettingsSchema = function() {\n\n' +
  259 +
  260 + ' }\n\n' +
  261 +
  262 + ' self.onDestroy = function() {\n\n' +
  263 +
  264 + ' }\n\n' +
  265 + '}';*/
  266 +
  267 + widgetTypeFunctionBody += widgetInfo.controllerScript;
  268 + widgetTypeFunctionBody += '\n};\n';
  269 +
  270 + try {
  271 +
  272 + const widgetTypeFunction = new Function(widgetTypeFunctionBody);
  273 + const widgetType = widgetTypeFunction.apply(this);
  274 + const widgetTypeInstance: WidgetTypeInstance = new widgetType();
  275 + const result: WidgetControllerDescriptor = {
  276 + widgetTypeFunction: widgetType
  277 + };
  278 + if (isFunction(widgetTypeInstance.getSettingsSchema)) {
  279 + result.settingsSchema = widgetTypeInstance.getSettingsSchema();
  280 + }
  281 + if (isFunction(widgetTypeInstance.getDataKeySettingsSchema)) {
  282 + result.dataKeySettingsSchema = widgetTypeInstance.getDataKeySettingsSchema();
  283 + }
  284 + if (isFunction(widgetTypeInstance.typeParameters)) {
  285 + result.typeParameters = widgetTypeInstance.typeParameters();
  286 + } else {
  287 + result.typeParameters = {};
  288 + }
  289 + if (isFunction(widgetTypeInstance.useCustomDatasources)) {
  290 + result.typeParameters.useCustomDatasources = widgetTypeInstance.useCustomDatasources();
  291 + } else {
  292 + result.typeParameters.useCustomDatasources = false;
  293 + }
  294 + if (isUndefined(result.typeParameters.maxDatasources)) {
  295 + result.typeParameters.maxDatasources = -1;
  296 + }
  297 + if (isUndefined(result.typeParameters.maxDataKeys)) {
  298 + result.typeParameters.maxDataKeys = -1;
  299 + }
  300 + if (isUndefined(result.typeParameters.dataKeysOptional)) {
  301 + result.typeParameters.dataKeysOptional = false;
  302 + }
  303 + if (isUndefined(result.typeParameters.stateData)) {
  304 + result.typeParameters.stateData = false;
  305 + }
  306 + if (isFunction(widgetTypeInstance.actionSources)) {
  307 + result.actionSources = widgetTypeInstance.actionSources();
  308 + } else {
  309 + result.actionSources = {};
  310 + }
  311 + for (const actionSourceId of Object.keys(widgetActionSources)) {
  312 + result.actionSources[actionSourceId] = {...widgetActionSources[actionSourceId]};
  313 + result.actionSources[actionSourceId].name = this.translate.instant(result.actionSources[actionSourceId].name);
  314 + }
  315 + return result;
  316 + } catch (e) {
  317 + this.utils.processWidgetException(e);
  318 + throw e;
  319 + }
  320 + }
  321 +
  322 + private processWidgetLoadError(errorMessages: string[], cacheKey: string, widgetInfoSubject: Subject<WidgetInfo>) {
  323 + if (widgetInfoSubject) {
  324 + widgetInfoSubject.error({
  325 + widgetInfo: this.errorWidgetType,
  326 + errorMessages
  327 + });
  328 + }
  329 + this.resolveWidgetsInfoFetchQueue(cacheKey, this.errorWidgetType, errorMessages);
  330 + }
  331 +
  332 + private resolveWidgetsInfoFetchQueue(key: string, widgetInfo: WidgetInfo, errorMessages?: string[]) {
  333 + const fetchQueue = this.widgetsInfoFetchQueue.get(key);
  334 + if (fetchQueue) {
  335 + fetchQueue.forEach(subject => {
  336 + if (!errorMessages) {
  337 + subject.next(widgetInfo);
  338 + subject.complete();
  339 + } else {
  340 + subject.error({
  341 + widgetInfo,
  342 + errorMessages
  343 + });
  344 + }
  345 + });
  346 + this.widgetsInfoFetchQueue.delete(key);
  347 + }
  348 + }
  349 +
  350 + // Cache functions
  351 +
  352 + private createWidgetInfoCacheKey(bundleAlias: string, widgetTypeAlias: string, isSystem: boolean): string {
  353 + return `${isSystem ? 'sys_' : ''}${bundleAlias}_${widgetTypeAlias}`;
  354 + }
  355 +
  356 + private getWidgetInfoFromCache(bundleAlias: string, widgetTypeAlias: string, isSystem: boolean): WidgetInfo | undefined {
  357 + const key = this.createWidgetInfoCacheKey(bundleAlias, widgetTypeAlias, isSystem);
  358 + return this.widgetsInfoInMemoryCache.get(key);
  359 + }
  360 +
  361 + private putWidgetInfoToCache(widgetInfo: WidgetInfo, bundleAlias: string, widgetTypeAlias: string, isSystem: boolean) {
  362 + const key = this.createWidgetInfoCacheKey(bundleAlias, widgetTypeAlias, isSystem);
  363 + this.widgetsInfoInMemoryCache.set(key, widgetInfo);
  364 + }
  365 +
  366 +}
... ...
... ... @@ -25,14 +25,12 @@ import { LegendComponent } from '@home/components/widget/legend.component';
25 25 ],
26 26 declarations:
27 27 [
28   - LegendComponent
29 28 ],
30 29 imports: [
31 30 CommonModule,
32 31 SharedModule
33 32 ],
34 33 exports: [
35   - LegendComponent
36 34 ]
37 35 })
38 36 export class WidgetComponentsModule { }
... ...
... ... @@ -15,4 +15,16 @@
15 15 limitations under the License.
16 16
17 17 -->
18   -<ng-container #widgetContent></ng-container>
  18 +<div class="tb-absolute-fill tb-widget-error" *ngIf="widgetErrorData">
  19 + <span>Widget Error: {{ widgetErrorData.name + ": " + widgetErrorData.message}}</span>
  20 +</div>
  21 +<div class="tb-absolute-fill tb-widget-loading" [fxShow]="loadingData" fxLayout="column" fxLayoutAlign="center center">
  22 + <mat-spinner color="accent" md-mode="indeterminate" diameter="40"></mat-spinner>
  23 +</div>
  24 +<div class="tb-absolute-fill" [fxLayout]="legendContainerLayoutType">
  25 + <tb-legend *ngIf="displayLegend && isLegendFirst" [ngStyle]="legendStyle" [legendConfig]="legendConfig" [legendData]="legendData"></tb-legend>
  26 + <div fxFlex id="widget-container">
  27 + <ng-container #widgetContent></ng-container>
  28 + </div>
  29 + <tb-legend *ngIf="displayLegend && !isLegendFirst" [ngStyle]="legendStyle" [legendConfig]="legendConfig" [legendData]="legendData"></tb-legend>
  30 +</div>
... ...
... ... @@ -17,7 +17,6 @@
17 17 import {
18 18 AfterViewInit,
19 19 Component,
20   - ComponentFactory,
21 20 ComponentFactoryResolver,
22 21 ComponentRef,
23 22 ElementRef,
... ... @@ -38,34 +37,37 @@ import {
38 37 LegendPosition,
39 38 Widget,
40 39 WidgetActionDescriptor,
  40 + widgetActionSources,
41 41 WidgetActionType,
42   - WidgetInfo, WidgetResource,
43   - widgetType,
44   - WidgetTypeInstance,
45   - widgetActionSources
  42 + WidgetResource,
  43 + widgetType
46 44 } from '@shared/models/widget.models';
47 45 import { PageComponent } from '@shared/components/page.component';
48 46 import { Store } from '@ngrx/store';
49 47 import { AppState } from '@core/core.state';
50 48 import { WidgetService } from '@core/http/widget.service';
51 49 import { UtilsService } from '@core/services/utils.service';
52   -import { DynamicWidgetComponent } from '@home/components/widget/dynamic-widget.component';
53   -import { forkJoin, Observable, of, ReplaySubject, throwError } from 'rxjs';
54   -import { DynamicWidgetComponentFactoryService } from '@home/components/widget/dynamic-widget-component-factory.service';
  50 +import { forkJoin, Observable, of, throwError } from 'rxjs';
55 51 import { isDefined, objToBase64 } from '@core/utils';
56 52 import * as $ from 'jquery';
57   -import { WidgetContext, WidgetHeaderAction } from '@home/models/widget-component.models';
  53 +import {
  54 + IDynamicWidgetComponent,
  55 + WidgetContext,
  56 + WidgetHeaderAction,
  57 + WidgetInfo,
  58 + WidgetTypeInstance
  59 +} from '@home/models/widget-component.models';
58 60 import {
59 61 EntityInfo,
60 62 IWidgetSubscription,
61   - SubscriptionInfo,
62   - WidgetSubscriptionOptions,
63 63 StateObject,
64 64 StateParams,
65   - WidgetSubscriptionContext
  65 + SubscriptionInfo,
  66 + WidgetSubscriptionContext,
  67 + WidgetSubscriptionOptions
66 68 } from '@core/api/widget-api.models';
67 69 import { EntityId } from '@shared/models/id/entity-id';
68   -import { ActivatedRoute, Router, UrlSegment } from '@angular/router';
  70 +import { ActivatedRoute, Router } from '@angular/router';
69 71 import cssjs from '@core/css/css';
70 72 import { ResourcesService } from '@core/services/resources.service';
71 73 import { catchError, switchMap } from 'rxjs/operators';
... ... @@ -74,6 +76,7 @@ import { TimeService } from '@core/services/time.service';
74 76 import { DeviceService } from '@app/core/http/device.service';
75 77 import { AlarmService } from '@app/core/http/alarm.service';
76 78 import { ExceptionData } from '@shared/models/error.models';
  79 +import { WidgetComponentService } from './widget-component.service';
77 80
78 81 @Component({
79 82 selector: 'tb-widget',
... ... @@ -99,14 +102,22 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
99 102
100 103 widget: Widget;
101 104 widgetInfo: WidgetInfo;
  105 + errorMessages: string[];
102 106 widgetContext: WidgetContext;
103 107 widgetType: any;
104 108 widgetTypeInstance: WidgetTypeInstance;
105 109 widgetErrorData: ExceptionData;
  110 + loadingData: boolean;
106 111
107   - dynamicWidgetComponentFactory: ComponentFactory<DynamicWidgetComponent>;
108   - dynamicWidgetComponentRef: ComponentRef<DynamicWidgetComponent>;
109   - dynamicWidgetComponent: DynamicWidgetComponent;
  112 + displayLegend: boolean;
  113 + legendConfig: LegendConfig;
  114 + legendData: LegendData;
  115 + isLegendFirst: boolean;
  116 + legendContainerLayoutType: string;
  117 + legendStyle: {[klass: string]: any};
  118 +
  119 + dynamicWidgetComponentRef: ComponentRef<IDynamicWidgetComponent>;
  120 + dynamicWidgetComponent: IDynamicWidgetComponent;
110 121
111 122 subscriptionContext: WidgetSubscriptionContext;
112 123
... ... @@ -120,7 +131,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
120 131 constructor(protected store: Store<AppState>,
121 132 private route: ActivatedRoute,
122 133 private router: Router,
123   - private dynamicWidgetComponentFactoryService: DynamicWidgetComponentFactoryService,
  134 + private widgetComponentService: WidgetComponentService,
124 135 private componentFactoryResolver: ComponentFactoryResolver,
125 136 private elementRef: ElementRef,
126 137 private injector: Injector,
... ... @@ -134,8 +145,67 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
134 145 }
135 146
136 147 ngOnInit(): void {
  148 +
  149 + this.loadingData = true;
  150 +
137 151 this.widget = this.dashboardWidget.widget;
138 152
  153 + this.displayLegend = isDefined(this.widget.config.showLegend) ? this.widget.config.showLegend
  154 + : this.widget.type === widgetType.timeseries;
  155 +
  156 + this.legendContainerLayoutType = 'column';
  157 +
  158 + if (this.displayLegend) {
  159 + this.legendConfig = this.widget.config.legendConfig ||
  160 + {
  161 + position: LegendPosition.bottom,
  162 + showMin: false,
  163 + showMax: false,
  164 + showAvg: this.widget.type === widgetType.timeseries,
  165 + showTotal: false
  166 + };
  167 + this.legendData = {
  168 + keys: [],
  169 + data: []
  170 + };
  171 + if (this.legendConfig.position === LegendPosition.top ||
  172 + this.legendConfig.position === LegendPosition.bottom) {
  173 + this.legendContainerLayoutType = 'column';
  174 + } else {
  175 + this.legendContainerLayoutType = 'row';
  176 + }
  177 + switch (this.legendConfig.position) {
  178 + case LegendPosition.top:
  179 + this.legendStyle = {
  180 + paddingBottom: '8px',
  181 + maxHeight: '50%',
  182 + overflowY: 'auto'
  183 + };
  184 + break;
  185 + case LegendPosition.bottom:
  186 + this.legendStyle = {
  187 + paddingTop: '8px',
  188 + maxHeight: '50%',
  189 + overflowY: 'auto'
  190 + };
  191 + break;
  192 + case LegendPosition.left:
  193 + this.legendStyle = {
  194 + paddingRight: '0px',
  195 + maxWidth: '50%',
  196 + overflowY: 'auto'
  197 + };
  198 + break;
  199 + case LegendPosition.right:
  200 + this.legendStyle = {
  201 + paddingLeft: '0px',
  202 + maxWidth: '50%',
  203 + overflowY: 'auto'
  204 + };
  205 + break;
  206 + }
  207 + }
  208 +
139 209 const actionDescriptorsBySourceId: {[actionSourceId: string]: Array<WidgetActionDescriptor>} = {};
140 210 if (this.widget.config.actions) {
141 211 for (const actionSourceId of Object.keys(this.widget.config.actions)) {
... ... @@ -244,10 +314,15 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
244 314 aliasController: this.dashboard.aliasController
245 315 };
246 316
247   - this.widgetService.getWidgetInfo(this.widget.bundleAlias, this.widget.typeAlias, this.widget.isSystemType).subscribe(
  317 + this.widgetComponentService.getWidgetInfo(this.widget.bundleAlias, this.widget.typeAlias, this.widget.isSystemType).subscribe(
248 318 (widgetInfo) => {
249 319 this.widgetInfo = widgetInfo;
250 320 this.loadFromWidgetInfo();
  321 + },
  322 + (errorData) => {
  323 + this.widgetInfo = errorData.widgetInfo;
  324 + this.errorMessages = errorData.errorMessages;
  325 + this.loadFromWidgetInfo();
251 326 }
252 327 );
253 328
... ... @@ -363,14 +438,9 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
363 438 }
364 439
365 440 private initialize() {
366   - this.configureDynamicWidgetComponent().subscribe(
367   - () => {
368   - this.dynamicWidgetComponent.loadingData = false;
369   - },
370   - (error) => {
371   - // TODO:
372   - }
373   - );
  441 + this.configureDynamicWidgetComponent();
  442 + // TODO:
  443 + this.loadingData = false;
374 444 }
375 445
376 446 private destroyDynamicWidgetComponent() {
... ... @@ -381,127 +451,37 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
381 451 if (this.dynamicWidgetComponentRef) {
382 452 this.dynamicWidgetComponentRef.destroy();
383 453 }
384   - if (this.dynamicWidgetComponentFactory) {
385   - this.dynamicWidgetComponentFactoryService.destroyDynamicWidgetComponentFactory(this.dynamicWidgetComponentFactory);
386   - }
387 454 }
388 455
389 456 private handleWidgetException(e) {
390 457 console.error(e);
391 458 this.widgetErrorData = this.utils.processWidgetException(e);
392   - if (this.dynamicWidgetComponent) {
393   - this.dynamicWidgetComponent.widgetErrorData = this.widgetErrorData;
394   - }
395 459 }
396 460
397   - private configureDynamicWidgetComponent(): Observable<any> {
  461 + private configureDynamicWidgetComponent() {
  462 + this.widgetContentContainer.clear();
  463 + this.dynamicWidgetComponentRef = this.widgetContentContainer.createComponent(this.widgetInfo.componentFactory);
  464 + this.dynamicWidgetComponent = this.dynamicWidgetComponentRef.instance;
398 465
399   - const dynamicWidgetComponentSubject = new ReplaySubject();
  466 + this.dynamicWidgetComponent.widgetContext = this.widgetContext;
  467 + this.dynamicWidgetComponent.errorMessages = this.errorMessages;
400 468
401   - let html = '<div class="tb-absolute-fill tb-widget-error" *ngIf="widgetErrorData">' +
402   - '<span>Widget Error: {{ widgetErrorData.name + ": " + widgetErrorData.message}}</span>' +
403   - '</div>' +
404   - '<div class="tb-absolute-fill tb-widget-loading" [fxShow]="loadingData" fxLayout="column" fxLayoutAlign="center center">' +
405   - '<mat-spinner color="accent" md-mode="indeterminate" diameter="40"></mat-spinner>' +
406   - '</div>';
  469 + this.widgetContext.$scope = this.dynamicWidgetComponent;
407 470
408   - let containerHtml = `<div id="container">${this.widgetInfo.templateHtml}</div>`;
  471 + const containerElement = $(this.elementRef.nativeElement.querySelector('#widget-container'));
409 472
410   - const displayLegend = isDefined(this.widget.config.showLegend) ? this.widget.config.showLegend
411   - : this.widget.type === widgetType.timeseries;
  473 + this.widgetContext.$container = $('> ng-component', containerElement);
  474 + this.widgetContext.$container.css('display', 'block');
  475 + this.widgetContext.$container.attr('id', 'container');
  476 + this.widgetContext.$containerParent = $(containerElement);
412 477
413   - let legendConfig: LegendConfig;
414   - let legendData: LegendData;
415   - if (displayLegend) {
416   - legendConfig = this.widget.config.legendConfig ||
417   - {
418   - position: LegendPosition.bottom,
419   - showMin: false,
420   - showMax: false,
421   - showAvg: this.widget.type === widgetType.timeseries,
422   - showTotal: false
423   - };
424   - legendData = {
425   - keys: [],
426   - data: []
427   - };
428   - let layoutType;
429   - if (legendConfig.position === LegendPosition.top ||
430   - legendConfig.position === LegendPosition.bottom) {
431   - layoutType = 'column';
432   - } else {
433   - layoutType = 'row';
  478 + if (this.widgetSizeDetected) {
  479 + this.widgetContext.$container.css('height', this.widgetContext.height + 'px');
  480 + this.widgetContext.$container.css('width', this.widgetContext.width + 'px');
434 481 }
435   - let legendStyle;
436   - switch (legendConfig.position) {
437   - case LegendPosition.top:
438   - legendStyle = 'padding-bottom: 8px; max-height: 50%; overflow-y: auto;';
439   - break;
440   - case LegendPosition.bottom:
441   - legendStyle = 'padding-top: 8px; max-height: 50%; overflow-y: auto;';
442   - break;
443   - case LegendPosition.left:
444   - legendStyle = 'padding-right: 0px; max-width: 50%; overflow-y: auto;';
445   - break;
446   - case LegendPosition.right:
447   - legendStyle = 'padding-left: 0px; max-width: 50%; overflow-y: auto;';
448   - break;
449   - }
450   -
451   - const legendHtml = `<tb-legend style="${legendStyle}" [legendConfig]="legendConfig" [legendData]="legendData"></tb-legend>`;
452   - containerHtml = `<div fxFlex id="widget-container">${containerHtml}</div>`;
453   - html += `<div class="tb-absolute-fill" fxLayout="${layoutType}">`;
454   - if (legendConfig.position === LegendPosition.top ||
455   - legendConfig.position === LegendPosition.left) {
456   - html += legendHtml;
457   - html += containerHtml;
458   - } else {
459   - html += containerHtml;
460   - html += legendHtml;
461   - }
462   - html += '</div>';
463   - } else {
464   - html += containerHtml;
465   - }
466   -
467   - this.dynamicWidgetComponentFactoryService.createDynamicWidgetComponentFactory(html).subscribe(
468   - (componentFactory) => {
469   - this.dynamicWidgetComponentFactory = componentFactory;
470   - this.widgetContentContainer.clear();
471   - this.dynamicWidgetComponentRef = this.widgetContentContainer.createComponent(this.dynamicWidgetComponentFactory);
472   - this.dynamicWidgetComponent = this.dynamicWidgetComponentRef.instance;
473   -
474   - this.dynamicWidgetComponent.loadingData = true;
475   - this.dynamicWidgetComponent.widgetContext = this.widgetContext;
476   - this.dynamicWidgetComponent.widgetErrorData = this.widgetErrorData;
477   - this.dynamicWidgetComponent.displayLegend = displayLegend;
478   - this.dynamicWidgetComponent.legendConfig = legendConfig;
479   - this.dynamicWidgetComponent.legendData = legendData;
480   -
481   - this.widgetContext.$scope = this.dynamicWidgetComponent;
482 482
483   - const containerElement = displayLegend ? $(this.elementRef.nativeElement.querySelector('#widget-container'))
484   - : $(this.elementRef.nativeElement);
485   -
486   - this.widgetContext.$container = $('#container', containerElement);
487   - this.widgetContext.$containerParent = $(containerElement);
488   -
489   - if (this.widgetSizeDetected) {
490   - this.widgetContext.$container.css('height', this.widgetContext.height + 'px');
491   - this.widgetContext.$container.css('width', this.widgetContext.width + 'px');
492   - }
493   -
494   - // @ts-ignore
495   - addResizeListener(this.widgetContext.$containerParent[0], this.onResizeListener);
496   -
497   - dynamicWidgetComponentSubject.next();
498   - dynamicWidgetComponentSubject.complete();
499   - },
500   - (e) => {
501   - dynamicWidgetComponentSubject.error(e);
502   - }
503   - );
504   - return dynamicWidgetComponentSubject.asObservable();
  483 + // @ts-ignore
  484 + addResizeListener(this.widgetContext.$containerParent[0], this.onResizeListener);
505 485 }
506 486
507 487 private createSubscription(options: WidgetSubscriptionOptions, subscribe: boolean): Observable<IWidgetSubscription> {
... ...
... ... @@ -16,23 +16,30 @@
16 16
17 17 import { ExceptionData } from '@shared/models/error.models';
18 18 import { IDashboardComponent } from '@home/models/dashboard-component.models';
19   -import { WidgetActionDescriptor, WidgetConfig, WidgetConfigSettings, widgetType } from '@shared/models/widget.models';
  19 +import {
  20 + WidgetActionDescriptor,
  21 + WidgetActionSource,
  22 + WidgetConfig,
  23 + WidgetConfigSettings,
  24 + WidgetControllerDescriptor,
  25 + WidgetType,
  26 + widgetType,
  27 + WidgetTypeDescriptor,
  28 + WidgetTypeParameters
  29 +} from '@shared/models/widget.models';
20 30 import { Timewindow } from '@shared/models/time/time.models';
21 31 import {
22 32 EntityInfo,
23   - IWidgetSubscription,
24   - SubscriptionInfo,
25   - WidgetSubscriptionOptions,
26   - IStateController,
27 33 IAliasController,
28   - TimewindowFunctions,
29   - WidgetSubscriptionApi,
  34 + IStateController,
  35 + IWidgetSubscription,
  36 + IWidgetUtils,
30 37 RpcApi,
  38 + TimewindowFunctions,
31 39 WidgetActionsApi,
32   - IWidgetUtils
  40 + WidgetSubscriptionApi
33 41 } from '@core/api/widget-api.models';
34   -import { Observable } from 'rxjs';
35   -import { EntityId } from '@shared/models/id/entity-id';
  42 +import { ComponentFactory } from '@angular/core';
36 43
37 44 export interface IWidgetAction {
38 45 name: string;
... ... @@ -49,13 +56,6 @@ export interface WidgetAction extends IWidgetAction {
49 56 show: boolean;
50 57 }
51 58
52   -export interface IDynamicWidgetComponent {
53   - widgetContext: WidgetContext;
54   - widgetErrorData: ExceptionData;
55   - loadingData: boolean;
56   - [key: string]: any;
57   -}
58   -
59 59 export interface WidgetContext {
60 60 inited?: boolean;
61 61 $container?: any;
... ... @@ -87,3 +87,93 @@ export interface WidgetContext {
87 87 customHeaderActions?: Array<WidgetHeaderAction>;
88 88 widgetActions?: Array<WidgetAction>;
89 89 }
  90 +
  91 +export interface IDynamicWidgetComponent {
  92 + widgetContext: WidgetContext;
  93 + errorMessages: string[];
  94 + [key: string]: any;
  95 +}
  96 +
  97 +export interface WidgetInfo extends WidgetTypeDescriptor, WidgetControllerDescriptor {
  98 + widgetName: string;
  99 + alias: string;
  100 + typeSettingsSchema?: string;
  101 + typeDataKeySettingsSchema?: string;
  102 + componentFactory?: ComponentFactory<IDynamicWidgetComponent>;
  103 +}
  104 +
  105 +export const MissingWidgetType: WidgetInfo = {
  106 + type: widgetType.latest,
  107 + widgetName: 'Widget type not found',
  108 + alias: 'undefined',
  109 + sizeX: 8,
  110 + sizeY: 6,
  111 + resources: [],
  112 + templateHtml: '<div class="tb-widget-error-container">' +
  113 + '<div translate class="tb-widget-error-msg">widget.widget-type-not-found</div>' +
  114 + '</div>',
  115 + templateCss: '',
  116 + controllerScript: 'self.onInit = function() {}',
  117 + settingsSchema: '{}\n',
  118 + dataKeySettingsSchema: '{}\n',
  119 + defaultConfig: '{\n' +
  120 + '"title": "Widget type not found",\n' +
  121 + '"datasources": [],\n' +
  122 + '"settings": {}\n' +
  123 + '}\n'
  124 +};
  125 +
  126 +export const ErrorWidgetType: WidgetInfo = {
  127 + type: widgetType.latest,
  128 + widgetName: 'Error loading widget',
  129 + alias: 'error',
  130 + sizeX: 8,
  131 + sizeY: 6,
  132 + resources: [],
  133 + templateHtml: '<div class="tb-widget-error-container">' +
  134 + '<div translate class="tb-widget-error-msg">widget.widget-type-load-error</div>' +
  135 + '<div *ngFor="let error of errorMessages" class="tb-widget-error-msg">{{ error }}</div>' +
  136 + '</div>',
  137 + templateCss: '',
  138 + controllerScript: 'self.onInit = function() {}',
  139 + settingsSchema: '{}\n',
  140 + dataKeySettingsSchema: '{}\n',
  141 + defaultConfig: '{\n' +
  142 + '"title": "Widget failed to load",\n' +
  143 + '"datasources": [],\n' +
  144 + '"settings": {}\n' +
  145 + '}\n'
  146 +};
  147 +
  148 +export interface WidgetTypeInstance {
  149 + getSettingsSchema?: () => string;
  150 + getDataKeySettingsSchema?: () => string;
  151 + typeParameters?: () => WidgetTypeParameters;
  152 + useCustomDatasources?: () => boolean;
  153 + actionSources?: () => {[key: string]: WidgetActionSource};
  154 +
  155 + onInit?: () => void;
  156 + onDataUpdated?: () => void;
  157 + onResize?: () => void;
  158 + onEditModeChanged?: () => void;
  159 + onMobileModeChanged?: () => void;
  160 + onDestroy?: () => void;
  161 +}
  162 +
  163 +export function toWidgetInfo(widgetTypeEntity: WidgetType): WidgetInfo {
  164 + return {
  165 + widgetName: widgetTypeEntity.name,
  166 + alias: widgetTypeEntity.alias,
  167 + type: widgetTypeEntity.descriptor.type,
  168 + sizeX: widgetTypeEntity.descriptor.sizeX,
  169 + sizeY: widgetTypeEntity.descriptor.sizeY,
  170 + resources: widgetTypeEntity.descriptor.resources,
  171 + templateHtml: widgetTypeEntity.descriptor.templateHtml,
  172 + templateCss: widgetTypeEntity.descriptor.templateCss,
  173 + controllerScript: widgetTypeEntity.descriptor.controllerScript,
  174 + settingsSchema: widgetTypeEntity.descriptor.settingsSchema,
  175 + dataKeySettingsSchema: widgetTypeEntity.descriptor.dataKeySettingsSchema,
  176 + defaultConfig: widgetTypeEntity.descriptor.defaultConfig
  177 + };
  178 +}
  179 +
... ...
... ... @@ -25,13 +25,14 @@ import { ActivatedRoute } from '@angular/router';
25 25 import { Authority } from '@shared/models/authority.enum';
26 26 import { NULL_UUID } from '@shared/models/id/has-uuid';
27 27 import { Observable } from 'rxjs';
28   -import { toWidgetInfo, Widget, widgetType } from '@app/shared/models/widget.models';
  28 +import { Widget, widgetType } from '@app/shared/models/widget.models';
29 29 import { WidgetService } from '@core/http/widget.service';
30 30 import { map, share } from 'rxjs/operators';
31 31 import { DialogService } from '@core/services/dialog.service';
32 32 import { FooterFabButtons } from '@app/shared/components/footer-fab-buttons.component';
33 33 import { DashboardCallbacks, WidgetsData } from '@home/models/dashboard-component.models';
34 34 import { IAliasController } from '@app/core/api/widget-api.models';
  35 +import { toWidgetInfo } from '@home/models/widget-component.models';
35 36
36 37 @Component({
37 38 selector: 'tb-widget-library',
... ...
... ... @@ -14,11 +14,9 @@
14 14 /// limitations under the License.
15 15 ///
16 16
17   -import {BaseData} from '@shared/models/base-data';
18   -import {TenantId} from '@shared/models/id/tenant-id';
19   -import {WidgetsBundleId} from '@shared/models/id/widgets-bundle-id';
20   -import {WidgetTypeId} from '@shared/models/id/widget-type-id';
21   -import { AliasEntityType, EntityType, EntityTypeTranslation } from '@shared/models/entity-type.models';
  17 +import { BaseData } from '@shared/models/base-data';
  18 +import { TenantId } from '@shared/models/id/tenant-id';
  19 +import { WidgetTypeId } from '@shared/models/id/widget-type-id';
22 20 import { Timewindow } from '@shared/models/time/time.models';
23 21
24 22 export enum widgetType {
... ... @@ -139,86 +137,6 @@ export interface WidgetType extends BaseData<WidgetTypeId> {
139 137 descriptor: WidgetTypeDescriptor;
140 138 }
141 139
142   -export interface WidgetInfo extends WidgetTypeDescriptor, WidgetControllerDescriptor {
143   - widgetName: string;
144   - alias: string;
145   - typeSettingsSchema?: string;
146   - typeDataKeySettingsSchema?: string;
147   -}
148   -
149   -export const MissingWidgetType: WidgetInfo = {
150   - type: widgetType.latest,
151   - widgetName: 'Widget type not found',
152   - alias: 'undefined',
153   - sizeX: 8,
154   - sizeY: 6,
155   - resources: [],
156   - templateHtml: '<div class="tb-widget-error-container">' +
157   - '<div translate class="tb-widget-error-msg">widget.widget-type-not-found</div>' +
158   - '</div>',
159   - templateCss: '',
160   - controllerScript: 'self.onInit = function() {}',
161   - settingsSchema: '{}\n',
162   - dataKeySettingsSchema: '{}\n',
163   - defaultConfig: '{\n' +
164   - '"title": "Widget type not found",\n' +
165   - '"datasources": [],\n' +
166   - '"settings": {}\n' +
167   - '}\n'
168   -};
169   -
170   -export const ErrorWidgetType: WidgetInfo = {
171   - type: widgetType.latest,
172   - widgetName: 'Error loading widget',
173   - alias: 'error',
174   - sizeX: 8,
175   - sizeY: 6,
176   - resources: [],
177   - templateHtml: '<div class="tb-widget-error-container">' +
178   - '<div translate class="tb-widget-error-msg">widget.widget-type-load-error</div>',
179   - templateCss: '',
180   - controllerScript: 'self.onInit = function() {}',
181   - settingsSchema: '{}\n',
182   - dataKeySettingsSchema: '{}\n',
183   - defaultConfig: '{\n' +
184   - '"title": "Widget failed to load",\n' +
185   - '"datasources": [],\n' +
186   - '"settings": {}\n' +
187   - '}\n'
188   -};
189   -
190   -export interface WidgetTypeInstance {
191   - getSettingsSchema?: () => string;
192   - getDataKeySettingsSchema?: () => string;
193   - typeParameters?: () => WidgetTypeParameters;
194   - useCustomDatasources?: () => boolean;
195   - actionSources?: () => {[key: string]: WidgetActionSource};
196   -
197   - onInit?: () => void;
198   - onDataUpdated?: () => void;
199   - onResize?: () => void;
200   - onEditModeChanged?: () => void;
201   - onMobileModeChanged?: () => void;
202   - onDestroy?: () => void;
203   -}
204   -
205   -export function toWidgetInfo(widgetTypeEntity: WidgetType): WidgetInfo {
206   - return {
207   - widgetName: widgetTypeEntity.name,
208   - alias: widgetTypeEntity.alias,
209   - type: widgetTypeEntity.descriptor.type,
210   - sizeX: widgetTypeEntity.descriptor.sizeX,
211   - sizeY: widgetTypeEntity.descriptor.sizeY,
212   - resources: widgetTypeEntity.descriptor.resources,
213   - templateHtml: widgetTypeEntity.descriptor.templateHtml,
214   - templateCss: widgetTypeEntity.descriptor.templateCss,
215   - controllerScript: widgetTypeEntity.descriptor.controllerScript,
216   - settingsSchema: widgetTypeEntity.descriptor.settingsSchema,
217   - dataKeySettingsSchema: widgetTypeEntity.descriptor.dataKeySettingsSchema,
218   - defaultConfig: widgetTypeEntity.descriptor.defaultConfig
219   - };
220   -}
221   -
222 140 export enum LegendDirection {
223 141 column = 'column',
224 142 row = 'row'
... ...