Commit 43ed086023dbabd9a0b299e542f471614377b770

Authored by Andrii Shvaika
2 parents bd9ec316 349909be

Merge remote-tracking branch 'origin/master'

... ... @@ -72,6 +72,7 @@ public class EntityKeyMapping {
72 72 private static final String PHONE = "phone";
73 73
74 74 public static final List<String> commonEntityFields = Arrays.asList(CREATED_TIME, ENTITY_TYPE, NAME);
  75 + public static final List<String> dashboardEntityFields = Arrays.asList(CREATED_TIME, ENTITY_TYPE, TITLE);
75 76 public static final List<String> labeledEntityFields = Arrays.asList(CREATED_TIME, ENTITY_TYPE, NAME, TYPE, LABEL);
76 77 public static final List<String> contactBasedEntityFields = Arrays.asList(CREATED_TIME, ENTITY_TYPE, EMAIL, TITLE, COUNTRY, STATE, CITY, ADDRESS, ADDRESS_2, ZIP, PHONE);
77 78
... ... @@ -89,7 +90,7 @@ public class EntityKeyMapping {
89 90
90 91 allowedEntityFieldMap.put(EntityType.USER, new HashSet<>(Arrays.asList(CREATED_TIME, FIRST_NAME, LAST_NAME, EMAIL)));
91 92
92   - allowedEntityFieldMap.put(EntityType.DASHBOARD, new HashSet<>(commonEntityFields));
  93 + allowedEntityFieldMap.put(EntityType.DASHBOARD, new HashSet<>(dashboardEntityFields));
93 94 allowedEntityFieldMap.put(EntityType.RULE_CHAIN, new HashSet<>(commonEntityFields));
94 95 allowedEntityFieldMap.put(EntityType.RULE_NODE, new HashSet<>(commonEntityFields));
95 96 allowedEntityFieldMap.put(EntityType.WIDGET_TYPE, new HashSet<>(commonEntityFields));
... ... @@ -118,12 +119,12 @@ public class EntityKeyMapping {
118 119 contactBasedAliases.put(LABEL, TITLE);
119 120 aliases.put(EntityType.TENANT, contactBasedAliases);
120 121 aliases.put(EntityType.CUSTOMER, contactBasedAliases);
  122 + aliases.put(EntityType.DASHBOARD, contactBasedAliases);
121 123 Map<String, String> commonEntityAliases = new HashMap<>();
122 124 commonEntityAliases.put(TITLE, NAME);
123 125 aliases.put(EntityType.DEVICE, commonEntityAliases);
124 126 aliases.put(EntityType.ASSET, commonEntityAliases);
125 127 aliases.put(EntityType.ENTITY_VIEW, commonEntityAliases);
126   - aliases.put(EntityType.DASHBOARD, commonEntityAliases);
127 128 aliases.put(EntityType.WIDGETS_BUNDLE, commonEntityAliases);
128 129
129 130 Map<String, String> userEntityAliases = new HashMap<>();
... ...
... ... @@ -8997,10 +8997,10 @@
8997 8997 "integrity": "sha512-4O3GWAYJaauMCILm07weko2rHA8a4kjn7+8Lg4s1d7SxwS/3IpkVD/GljbRrIJ1c1W/XGJ3GbuK7RyYZEJChhw=="
8998 8998 },
8999 8999 "ngx-flowchart": {
9000   - "version": "git://github.com/thingsboard/ngx-flowchart.git#a4157b0eef2eb3646ef920447c7b06b39d54f87f",
  9000 + "version": "git://github.com/thingsboard/ngx-flowchart.git#7a02f4748b5e7821a883c903107af5f20415d026",
9001 9001 "from": "git://github.com/thingsboard/ngx-flowchart.git#master",
9002 9002 "requires": {
9003   - "tslib": "^1.10.0"
  9003 + "tslib": "^1.13.0"
9004 9004 },
9005 9005 "dependencies": {
9006 9006 "tslib": {
... ...
... ... @@ -25,6 +25,10 @@ const PROXY_CONFIG = {
25 25 "target": `http://${ruleNodeUiforwardHost}:${ruleNodeUiforwardPort}`,
26 26 "secure": false,
27 27 },
  28 + "/static": {
  29 + "target": "http://localhost:8080",
  30 + "secure": false,
  31 + },
28 32 "/api/ws": {
29 33 "target": "ws://localhost:8080",
30 34 "ws": true,
... ...
... ... @@ -337,6 +337,7 @@ export class WidgetSubscription implements IWidgetSubscription {
337 337 }
338 338
339 339 private initDataSubscription(): Observable<any> {
  340 + this.notifyDataLoading();
340 341 const initDataSubscriptionSubject = new ReplaySubject(1);
341 342 this.loadStDiff().subscribe(() => {
342 343 if (!this.ctx.aliasController) {
... ... @@ -431,6 +432,7 @@ export class WidgetSubscription implements IWidgetSubscription {
431 432 });
432 433 this.configureLoadedData();
433 434 this.hasResolvedData = this.datasources.length > 0;
  435 + this.updateDataTimewindow();
434 436 this.notifyDataLoaded();
435 437 this.onDataUpdated(true);
436 438 })
... ... @@ -731,6 +733,7 @@ export class WidgetSubscription implements IWidgetSubscription {
731 733 if (this.type === widgetType.alarm) {
732 734 this.updateAlarmDataSubscription();
733 735 } else {
  736 + this.notifyDataLoading();
734 737 this.dataSubscribe();
735 738 }
736 739 }
... ... @@ -831,13 +834,21 @@ export class WidgetSubscription implements IWidgetSubscription {
831 834 }
832 835 }
833 836
834   - private dataSubscribe() {
  837 + private updateDataTimewindow() {
835 838 if (!this.hasDataPageLink) {
836 839 if (this.type === widgetType.timeseries && this.timeWindowConfig) {
837 840 this.updateRealtimeSubscription();
838 841 if (this.comparisonEnabled) {
839 842 this.updateSubscriptionForComparison();
840 843 }
  844 + }
  845 + }
  846 + }
  847 +
  848 + private dataSubscribe() {
  849 + if (!this.hasDataPageLink) {
  850 + if (this.type === widgetType.timeseries && this.timeWindowConfig) {
  851 + this.updateDataTimewindow();
841 852 if (this.subscriptionTimewindow.fixedWindow) {
842 853 this.onDataUpdated();
843 854 }
... ... @@ -1025,6 +1036,11 @@ export class WidgetSubscription implements IWidgetSubscription {
1025 1036 }
1026 1037 }
1027 1038
  1039 + private notifyDataLoading() {
  1040 + this.loadingData = true;
  1041 + this.callbacks.dataLoading(this);
  1042 + }
  1043 +
1028 1044 private notifyDataLoaded() {
1029 1045 this.loadingData = false;
1030 1046 this.callbacks.dataLoading(this);
... ... @@ -1268,6 +1284,7 @@ export class WidgetSubscription implements IWidgetSubscription {
1268 1284 if (this.caulculateLegendData) {
1269 1285 this.updateLegend(index, data.data, detectChanges);
1270 1286 }
  1287 + this.notifyDataLoaded();
1271 1288 this.onDataUpdated(detectChanges);
1272 1289 }
1273 1290 }
... ...
... ... @@ -239,7 +239,7 @@ export class RuleChainService {
239 239 });
240 240 }
241 241 if (moduleResource) {
242   - tasks.push(this.resourcesService.loadModule(moduleResource, ruleNodeConfigResourcesModulesMap).pipe(
  242 + tasks.push(this.resourcesService.loadFactories(moduleResource, ruleNodeConfigResourcesModulesMap).pipe(
243 243 map((res) => {
244 244 if (nodeDefinition.configDirective && nodeDefinition.configDirective.length) {
245 245 const selector = snakeCase(nodeDefinition.configDirective, '-');
... ...
... ... @@ -25,6 +25,8 @@ import {
25 25 } from '@angular/core';
26 26 import { DOCUMENT } from '@angular/common';
27 27 import { forkJoin, Observable, ReplaySubject, throwError } from 'rxjs';
  28 +import { HttpClient } from '@angular/common/http';
  29 +import { objToBase64 } from '@core/utils';
28 30
29 31 declare const SystemJS;
30 32
... ... @@ -34,12 +36,14 @@ declare const SystemJS;
34 36 export class ResourcesService {
35 37
36 38 private loadedResources: { [url: string]: ReplaySubject<any> } = {};
37   - private loadedModules: { [url: string]: ReplaySubject<ComponentFactory<any>[]> } = {};
  39 + private loadedModules: { [url: string]: ReplaySubject<Type<any>[]> } = {};
  40 + private loadedFactories: { [url: string]: ReplaySubject<ComponentFactory<any>[]> } = {};
38 41
39 42 private anchor = this.document.getElementsByTagName('head')[0] || this.document.getElementsByTagName('body')[0];
40 43
41 44 constructor(@Inject(DOCUMENT) private readonly document: any,
42 45 private compiler: Compiler,
  46 + private http: HttpClient,
43 47 private injector: Injector) {}
44 48
45 49 public loadResource(url: string): Observable<any> {
... ... @@ -60,12 +64,12 @@ export class ResourcesService {
60 64 return this.loadResourceByType(fileType, url);
61 65 }
62 66
63   - public loadModule(url: string, modulesMap: {[key: string]: any}): Observable<ComponentFactory<any>[]> {
64   - if (this.loadedModules[url]) {
65   - return this.loadedModules[url].asObservable();
  67 + public loadFactories(url: string, modulesMap: {[key: string]: any}): Observable<ComponentFactory<any>[]> {
  68 + if (this.loadedFactories[url]) {
  69 + return this.loadedFactories[url].asObservable();
66 70 }
67 71 const subject = new ReplaySubject<ComponentFactory<any>[]>();
68   - this.loadedModules[url] = subject;
  72 + this.loadedFactories[url] = subject;
69 73 if (modulesMap) {
70 74 for (const moduleId of Object.keys(modulesMap)) {
71 75 SystemJS.set(moduleId, modulesMap[moduleId]);
... ... @@ -86,19 +90,76 @@ export class ResourcesService {
86 90 c.ngModuleFactory.create(this.injector);
87 91 componentFactories.push(...c.componentFactories);
88 92 }
89   - this.loadedModules[url].next(componentFactories);
90   - this.loadedModules[url].complete();
  93 + this.loadedFactories[url].next(componentFactories);
  94 + this.loadedFactories[url].complete();
91 95 } catch (e) {
92   - this.loadedModules[url].error(new Error(`Unable to init module from url: ${url}`));
93   - delete this.loadedModules[url];
  96 + this.loadedFactories[url].error(new Error(`Unable to init module from url: ${url}`));
  97 + delete this.loadedFactories[url];
94 98 }
95 99 },
96 100 (e) => {
97   - this.loadedModules[url].error(new Error(`Unable to compile module from url: ${url}`));
98   - delete this.loadedModules[url];
  101 + this.loadedFactories[url].error(new Error(`Unable to compile module from url: ${url}`));
  102 + delete this.loadedFactories[url];
99 103 });
100 104 } else {
101   - this.loadedModules[url].error(new Error(`Module '${url}' doesn't have default export!`));
  105 + this.loadedFactories[url].error(new Error(`Module '${url}' doesn't have default export!`));
  106 + delete this.loadedFactories[url];
  107 + }
  108 + },
  109 + (e) => {
  110 + this.loadedFactories[url].error(new Error(`Unable to load module from url: ${url}`));
  111 + delete this.loadedFactories[url];
  112 + }
  113 + );
  114 + return subject.asObservable();
  115 + }
  116 +
  117 + public loadModules(url: string, modulesMap: {[key: string]: any}): Observable<Type<any>[]> {
  118 + if (this.loadedModules[url]) {
  119 + return this.loadedModules[url].asObservable();
  120 + }
  121 + const subject = new ReplaySubject<Type<any>[]>();
  122 + this.loadedModules[url] = subject;
  123 + if (modulesMap) {
  124 + for (const moduleId of Object.keys(modulesMap)) {
  125 + SystemJS.set(moduleId, modulesMap[moduleId]);
  126 + }
  127 + }
  128 + SystemJS.import(url).then(
  129 + (module) => {
  130 + try {
  131 + let modules;
  132 + try {
  133 + modules = this.extractNgModules(module);
  134 + } catch (e) {
  135 + }
  136 + if (modules && modules.length) {
  137 + const tasks: Promise<ModuleWithComponentFactories<any>>[] = [];
  138 + for (const m of modules) {
  139 + tasks.push(this.compiler.compileModuleAndAllComponentsAsync(m));
  140 + }
  141 + forkJoin(tasks).subscribe((compiled) => {
  142 + try {
  143 + for (const c of compiled) {
  144 + c.ngModuleFactory.create(this.injector);
  145 + }
  146 + this.loadedModules[url].next(modules);
  147 + this.loadedModules[url].complete();
  148 + } catch (e) {
  149 + this.loadedModules[url].error(new Error(`Unable to init module from url: ${url}`));
  150 + delete this.loadedModules[url];
  151 + }
  152 + },
  153 + (e) => {
  154 + this.loadedModules[url].error(new Error(`Unable to compile module from url: ${url}`));
  155 + delete this.loadedModules[url];
  156 + });
  157 + } else {
  158 + this.loadedModules[url].error(new Error(`Module '${url}' doesn't have default export or not NgModule!`));
  159 + delete this.loadedModules[url];
  160 + }
  161 + } catch (e) {
  162 + this.loadedModules[url].error(new Error(`Unable to load module from url: ${url}`));
102 163 delete this.loadedModules[url];
103 164 }
104 165 },
... ...
... ... @@ -330,7 +330,7 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
330 330 this.defaultPageSize = pageSize;
331 331 }
332 332 this.pageSizeOptions = [this.defaultPageSize, this.defaultPageSize * 2, this.defaultPageSize * 3];
333   - this.pageLink.pageSize = this.displayPagination ? this.defaultPageSize : Number.POSITIVE_INFINITY;
  333 + this.pageLink.pageSize = this.displayPagination ? this.defaultPageSize : 1024;
334 334
335 335 this.pageLink.searchPropagatedAlarms = isDefined(this.widgetConfig.searchPropagatedAlarms)
336 336 ? this.widgetConfig.searchPropagatedAlarms : true;
... ... @@ -535,10 +535,15 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
535 535 } else {
536 536 this.pageLink.page = 0;
537 537 }
538   - this.pageLink.sortOrder = {
539   - key: findEntityKeyByColumnDef(this.sort.active, this.columns),
540   - direction: Direction[this.sort.direction.toUpperCase()]
541   - };
  538 + const key = findEntityKeyByColumnDef(this.sort.active, this.columns);
  539 + if (key) {
  540 + this.pageLink.sortOrder = {
  541 + key,
  542 + direction: Direction[this.sort.direction.toUpperCase()]
  543 + };
  544 + } else {
  545 + this.pageLink.sortOrder = null;
  546 + }
542 547 const sortOrderLabel = fromEntityColumnDef(this.sort.active, this.columns);
543 548 const keyFilters: KeyFilter[] = null; // TODO:
544 549 this.alarmsDatasource.loadAlarms(this.pageLink, sortOrderLabel, keyFilters);
... ...
... ... @@ -240,7 +240,7 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
240 240 this.defaultPageSize = pageSize;
241 241 }
242 242 this.pageSizeOptions = [this.defaultPageSize, this.defaultPageSize * 2, this.defaultPageSize * 3];
243   - this.pageLink.pageSize = this.displayPagination ? this.defaultPageSize : Number.POSITIVE_INFINITY;
  243 + this.pageLink.pageSize = this.displayPagination ? this.defaultPageSize : 1024;
244 244
245 245 const cssString = constructTableCssString(this.widgetConfig);
246 246 const cssParser = new cssjs();
... ... @@ -460,10 +460,15 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
460 460 } else {
461 461 this.pageLink.page = 0;
462 462 }
463   - this.pageLink.sortOrder = {
464   - key: findEntityKeyByColumnDef(this.sort.active, this.columns),
465   - direction: Direction[this.sort.direction.toUpperCase()]
466   - };
  463 + const key = findEntityKeyByColumnDef(this.sort.active, this.columns);
  464 + if (key) {
  465 + this.pageLink.sortOrder = {
  466 + key,
  467 + direction: Direction[this.sort.direction.toUpperCase()]
  468 + };
  469 + } else {
  470 + this.pageLink.sortOrder = null;
  471 + }
467 472 const sortOrderLabel = fromEntityColumnDef(this.sort.active, this.columns);
468 473 const keyFilters: KeyFilter[] = null; // TODO:
469 474 this.entityDatasource.loadEntities(this.pageLink, sortOrderLabel, keyFilters);
... ...
... ... @@ -159,7 +159,7 @@ function parseTemplate(template: string, data: { $datasource?: Datasource, [key:
159 159 }
160 160 template = createLabelFromDatasource(data.$datasource, template);
161 161
162   - let match = varsRegex.exec(template);
  162 + let match = /\${([^}]*)}/g.exec(template);
163 163 while (match !== null) {
164 164 const variable = match[0];
165 165 let label = match[1];
... ... @@ -186,7 +186,7 @@ function parseTemplate(template: string, data: { $datasource?: Datasource, [key:
186 186 textValue = value;
187 187 }
188 188 template = template.split(variable).join(textValue);
189   - match = varsRegex.exec(template);
  189 + match = /\${([^}]*)}/g.exec(template);
190 190 }
191 191
192 192 let actionTags: string;
... ...
... ... @@ -96,7 +96,10 @@ export function entityDataSortOrderFromString(strSortOrder: string, columns: Ent
96 96 if (!property && !property.length) {
97 97 return null;
98 98 }
99   - const column = findColumnByLabel(property, columns);
  99 + let column = findColumnByLabel(property, columns);
  100 + if (!column) {
  101 + column = findColumnByName(property, columns);
  102 + }
100 103 if (column && column.entityKey) {
101 104 return {key: column.entityKey, direction};
102 105 }
... ... @@ -113,13 +116,22 @@ export function findColumnByEntityKey(key: EntityKey, columns: EntityColumn[]):
113 116 }
114 117
115 118 export function findEntityKeyByColumnDef(def: string, columns: EntityColumn[]): EntityKey {
116   - return findColumnByDef(def, columns).entityKey;
  119 + if (def) {
  120 + const column = findColumnByDef(def, columns);
  121 + return column ? column.entityKey : null;
  122 + } else {
  123 + return null;
  124 + }
117 125 }
118 126
119 127 export function findColumn(searchProperty: string, searchValue: string, columns: EntityColumn[]): EntityColumn {
120 128 return columns.find(theColumn => theColumn[searchProperty] === searchValue);
121 129 }
122 130
  131 +export function findColumnByName(name: string, columns: EntityColumn[]): EntityColumn {
  132 + return findColumn('name', name, columns);
  133 +}
  134 +
123 135 export function findColumnByLabel(label: string, columns: EntityColumn[]): EntityColumn {
124 136 let column: EntityColumn;
125 137 const alarmColumns = columns.filter(c => c.type === DataKeyType.alarm);
... ...
... ... @@ -41,6 +41,43 @@ import { NULL_UUID } from '@shared/models/id/has-uuid';
41 41 import { WidgetTypeId } from '@app/shared/models/id/widget-type-id';
42 42 import { TenantId } from '@app/shared/models/id/tenant-id';
43 43 import { SharedModule } from '@shared/shared.module';
  44 +import * as AngularCore from '@angular/core';
  45 +import * as AngularCommon from '@angular/common';
  46 +import * as AngularForms from '@angular/forms';
  47 +import * as AngularRouter from '@angular/router';
  48 +import * as AngularCdkKeycodes from '@angular/cdk/keycodes';
  49 +import * as AngularCdkCoercion from '@angular/cdk/coercion';
  50 +import * as AngularMaterialChips from '@angular/material/chips';
  51 +import * as AngularMaterialAutocomplete from '@angular/material/autocomplete';
  52 +import * as AngularMaterialDialog from '@angular/material/dialog';
  53 +import * as NgrxStore from '@ngrx/store';
  54 +import * as RxJs from 'rxjs';
  55 +import * as RxJsOperators from 'rxjs/operators';
  56 +import * as TranslateCore from '@ngx-translate/core';
  57 +import * as TbCore from '@core/public-api';
  58 +import * as TbShared from '@shared/public-api';
  59 +import * as _moment from 'moment';
  60 +
  61 +declare const SystemJS;
  62 +
  63 +const widgetResourcesModulesMap = {
  64 + '@angular/core': SystemJS.newModule(AngularCore),
  65 + '@angular/common': SystemJS.newModule(AngularCommon),
  66 + '@angular/forms': SystemJS.newModule(AngularForms),
  67 + '@angular/router': SystemJS.newModule(AngularRouter),
  68 + '@angular/cdk/keycodes': SystemJS.newModule(AngularCdkKeycodes),
  69 + '@angular/cdk/coercion': SystemJS.newModule(AngularCdkCoercion),
  70 + '@angular/material/chips': SystemJS.newModule(AngularMaterialChips),
  71 + '@angular/material/autocomplete': SystemJS.newModule(AngularMaterialAutocomplete),
  72 + '@angular/material/dialog': SystemJS.newModule(AngularMaterialDialog),
  73 + '@ngrx/store': SystemJS.newModule(NgrxStore),
  74 + rxjs: SystemJS.newModule(RxJs),
  75 + 'rxjs/operators': SystemJS.newModule(RxJsOperators),
  76 + '@ngx-translate/core': SystemJS.newModule(TranslateCore),
  77 + '@core/public-api': SystemJS.newModule(TbCore),
  78 + '@shared/public-api': SystemJS.newModule(TbShared),
  79 + moment: SystemJS.newModule(_moment)
  80 +};
44 81
45 82 // @dynamic
46 83 @Injectable()
... ... @@ -105,8 +142,8 @@ export class WidgetComponentService {
105 142 const initSubject = new ReplaySubject();
106 143 this.init$ = initSubject.asObservable();
107 144 const loadDefaultWidgetInfoTasks = [
108   - this.loadWidgetResources(this.missingWidgetType, 'global-widget-missing-type', [SharedModule]),
109   - this.loadWidgetResources(this.errorWidgetType, 'global-widget-error-type', [SharedModule]),
  145 + this.loadWidgetResources(this.missingWidgetType, 'global-widget-missing-type', [SharedModule, WidgetComponentsModule]),
  146 + this.loadWidgetResources(this.errorWidgetType, 'global-widget-error-type', [SharedModule, WidgetComponentsModule]),
110 147 ];
111 148 forkJoin(loadDefaultWidgetInfoTasks).subscribe(
112 149 () => {
... ... @@ -218,31 +255,71 @@ export class WidgetComponentService {
218 255 this.cssParser.cssPreviewNamespace = widgetNamespace;
219 256 this.cssParser.createStyleElement(widgetNamespace, widgetInfo.templateCss);
220 257 const resourceTasks: Observable<string>[] = [];
  258 + const modulesTasks: Observable<Type<any>[] | string>[] = [];
221 259 if (widgetInfo.resources.length > 0) {
222   - widgetInfo.resources.forEach((resource) => {
  260 + widgetInfo.resources.filter(r => r.isModule).forEach(
  261 + (resource) => {
  262 + modulesTasks.push(
  263 + this.resources.loadModules(resource.url, widgetResourcesModulesMap).pipe(
  264 + catchError((e: Error) => of(e?.message ? e.message : `Failed to load widget resource module: '${resource.url}'`))
  265 + )
  266 + );
  267 + }
  268 + );
  269 + }
  270 + widgetInfo.resources.filter(r => !r.isModule).forEach(
  271 + (resource) => {
223 272 resourceTasks.push(
224 273 this.resources.loadResource(resource.url).pipe(
225 274 catchError(e => of(`Failed to load widget resource: '${resource.url}'`))
226 275 )
227 276 );
228   - });
  277 + }
  278 + );
  279 +
  280 + let modulesObservable: Observable<string | Type<any>[]>;
  281 + if (modulesTasks.length) {
  282 + modulesObservable = forkJoin(modulesTasks).pipe(
  283 + map(res => {
  284 + const msg = res.find(r => typeof r === 'string');
  285 + if (msg) {
  286 + return msg as string;
  287 + } else {
  288 + let resModules = (res as Type<any>[][]).flat();
  289 + if (modules && modules.length) {
  290 + resModules = resModules.concat(modules);
  291 + }
  292 + return resModules;
  293 + }
  294 + })
  295 + );
  296 + } else {
  297 + modulesObservable = modules && modules.length ? of(modules) : of([]);
229 298 }
  299 +
230 300 resourceTasks.push(
231   - this.dynamicComponentFactoryService.createDynamicComponentFactory(
232   - class DynamicWidgetComponentInstance extends DynamicWidgetComponent {},
233   - widgetInfo.templateHtml,
234   - modules
235   - ).pipe(
236   - map((factory) => {
237   - widgetInfo.componentFactory = factory;
238   - return null;
239   - }),
240   - catchError(e => {
241   - const details = this.utils.parseException(e);
242   - const errorMessage = `Failed to compile widget html. \n Error: ${details.message}`;
243   - return of(errorMessage);
244   - })
245   - )
  301 + modulesObservable.pipe(
  302 + mergeMap((resolvedModules) => {
  303 + if (typeof resolvedModules === 'string') {
  304 + return of(resolvedModules);
  305 + } else {
  306 + return this.dynamicComponentFactoryService.createDynamicComponentFactory(
  307 + class DynamicWidgetComponentInstance extends DynamicWidgetComponent {},
  308 + widgetInfo.templateHtml,
  309 + resolvedModules
  310 + ).pipe(
  311 + map((factory) => {
  312 + widgetInfo.componentFactory = factory;
  313 + return null;
  314 + }),
  315 + catchError(e => {
  316 + const details = this.utils.parseException(e);
  317 + const errorMessage = `Failed to compile widget html. \n Error: ${details.message}`;
  318 + return of(errorMessage);
  319 + })
  320 + )
  321 + }
  322 + }))
246 323 );
247 324 return forkJoin(resourceTasks).pipe(
248 325 switchMap(msgs => {
... ...
... ... @@ -794,16 +794,10 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
794 794 options.useDashboardTimewindow = true;
795 795 }
796 796 }
797   - let datasource: Datasource;
798 797 if (options.type === widgetType.alarm) {
799   - datasource = this.entityService.createAlarmSourceFromSubscriptionInfo(subscriptionsInfo[0]);
  798 + options.alarmSource = this.entityService.createAlarmSourceFromSubscriptionInfo(subscriptionsInfo[0]);
800 799 } else {
801   - datasource = this.entityService.createDatasourcesFromSubscriptionsInfo(subscriptionsInfo);
802   - }
803   - if (options.type === widgetType.alarm) {
804   - options.alarmSource = datasource;
805   - } else {
806   - options.datasources = [datasource];
  800 + options.datasources = this.entityService.createDatasourcesFromSubscriptionsInfo(subscriptionsInfo);
807 801 }
808 802 this.createSubscription(options, subscribe).subscribe(
809 803 (subscription) => {
... ...
... ... @@ -129,6 +129,10 @@
129 129 (ngModelChange)="isDirty = true"
130 130 placeholder="{{ 'widget.resource-url' | translate }}"/>
131 131 </mat-form-field>
  132 + <mat-checkbox [(ngModel)]="resource.isModule"
  133 + (ngModelChange)="isDirty = true">
  134 + {{ 'widget.resource-is-module' | translate }}
  135 + </mat-checkbox>
132 136 <button mat-icon-button color="primary"
133 137 [disabled]="isLoading$ | async"
134 138 (click)="removeResource(i)"
... ...
... ... @@ -53,6 +53,7 @@ export interface NodeScriptTestDialogData {
53 53 msgType?: string;
54 54 }
55 55
  56 +// @dynamic
56 57 @Component({
57 58 selector: 'tb-node-script-test-dialog',
58 59 templateUrl: './node-script-test-dialog.component.html',
... ...
... ... @@ -114,6 +114,7 @@ export const widgetTypesData = new Map<widgetType, WidgetTypeData>(
114 114
115 115 export interface WidgetResource {
116 116 url: string;
  117 + isModule?: boolean;
117 118 }
118 119
119 120 export interface WidgetActionSource {
... ...
... ... @@ -1787,6 +1787,7 @@
1787 1787 "type": "Widget type",
1788 1788 "resources": "Resources",
1789 1789 "resource-url": "JavaScript/CSS URL",
  1790 + "resource-is-module": "Is module",
1790 1791 "remove-resource": "Remove resource",
1791 1792 "add-resource": "Add resource",
1792 1793 "html": "HTML",
... ...