Commit 21d7350efc08dcec339f02d091dde55727e54d82

Authored by Igor Kulikov
1 parent 2ca577e9

UI: External angular modules for widget development

@@ -25,6 +25,10 @@ const PROXY_CONFIG = { @@ -25,6 +25,10 @@ const PROXY_CONFIG = {
25 "target": `http://${ruleNodeUiforwardHost}:${ruleNodeUiforwardPort}`, 25 "target": `http://${ruleNodeUiforwardHost}:${ruleNodeUiforwardPort}`,
26 "secure": false, 26 "secure": false,
27 }, 27 },
  28 + "/static": {
  29 + "target": "http://localhost:8080",
  30 + "secure": false,
  31 + },
28 "/api/ws": { 32 "/api/ws": {
29 "target": "ws://localhost:8080", 33 "target": "ws://localhost:8080",
30 "ws": true, 34 "ws": true,
@@ -239,7 +239,7 @@ export class RuleChainService { @@ -239,7 +239,7 @@ export class RuleChainService {
239 }); 239 });
240 } 240 }
241 if (moduleResource) { 241 if (moduleResource) {
242 - tasks.push(this.resourcesService.loadModule(moduleResource, ruleNodeConfigResourcesModulesMap).pipe( 242 + tasks.push(this.resourcesService.loadFactories(moduleResource, ruleNodeConfigResourcesModulesMap).pipe(
243 map((res) => { 243 map((res) => {
244 if (nodeDefinition.configDirective && nodeDefinition.configDirective.length) { 244 if (nodeDefinition.configDirective && nodeDefinition.configDirective.length) {
245 const selector = snakeCase(nodeDefinition.configDirective, '-'); 245 const selector = snakeCase(nodeDefinition.configDirective, '-');
@@ -25,6 +25,8 @@ import { @@ -25,6 +25,8 @@ import {
25 } from '@angular/core'; 25 } from '@angular/core';
26 import { DOCUMENT } from '@angular/common'; 26 import { DOCUMENT } from '@angular/common';
27 import { forkJoin, Observable, ReplaySubject, throwError } from 'rxjs'; 27 import { forkJoin, Observable, ReplaySubject, throwError } from 'rxjs';
  28 +import { HttpClient } from '@angular/common/http';
  29 +import { objToBase64 } from '@core/utils';
28 30
29 declare const SystemJS; 31 declare const SystemJS;
30 32
@@ -34,12 +36,14 @@ declare const SystemJS; @@ -34,12 +36,14 @@ declare const SystemJS;
34 export class ResourcesService { 36 export class ResourcesService {
35 37
36 private loadedResources: { [url: string]: ReplaySubject<any> } = {}; 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 private anchor = this.document.getElementsByTagName('head')[0] || this.document.getElementsByTagName('body')[0]; 42 private anchor = this.document.getElementsByTagName('head')[0] || this.document.getElementsByTagName('body')[0];
40 43
41 constructor(@Inject(DOCUMENT) private readonly document: any, 44 constructor(@Inject(DOCUMENT) private readonly document: any,
42 private compiler: Compiler, 45 private compiler: Compiler,
  46 + private http: HttpClient,
43 private injector: Injector) {} 47 private injector: Injector) {}
44 48
45 public loadResource(url: string): Observable<any> { 49 public loadResource(url: string): Observable<any> {
@@ -60,12 +64,12 @@ export class ResourcesService { @@ -60,12 +64,12 @@ export class ResourcesService {
60 return this.loadResourceByType(fileType, url); 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 const subject = new ReplaySubject<ComponentFactory<any>[]>(); 71 const subject = new ReplaySubject<ComponentFactory<any>[]>();
68 - this.loadedModules[url] = subject; 72 + this.loadedFactories[url] = subject;
69 if (modulesMap) { 73 if (modulesMap) {
70 for (const moduleId of Object.keys(modulesMap)) { 74 for (const moduleId of Object.keys(modulesMap)) {
71 SystemJS.set(moduleId, modulesMap[moduleId]); 75 SystemJS.set(moduleId, modulesMap[moduleId]);
@@ -86,19 +90,70 @@ export class ResourcesService { @@ -86,19 +90,70 @@ export class ResourcesService {
86 c.ngModuleFactory.create(this.injector); 90 c.ngModuleFactory.create(this.injector);
87 componentFactories.push(...c.componentFactories); 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 } catch (e) { 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 (e) => { 100 (e) => {
  101 + this.loadedFactories[url].error(new Error(`Unable to compile module from url: ${url}`));
  102 + delete this.loadedFactories[url];
  103 + });
  104 + } else {
  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 + let modules;
  131 + try {
  132 + modules = this.extractNgModules(module);
  133 + } catch (e) {}
  134 + if (modules && modules.length) {
  135 + const tasks: Promise<ModuleWithComponentFactories<any>>[] = [];
  136 + for (const m of modules) {
  137 + tasks.push(this.compiler.compileModuleAndAllComponentsAsync(m));
  138 + }
  139 + forkJoin(tasks).subscribe((compiled) => {
  140 + try {
  141 + for (const c of compiled) {
  142 + c.ngModuleFactory.create(this.injector);
  143 + }
  144 + this.loadedModules[url].next(modules);
  145 + this.loadedModules[url].complete();
  146 + } catch (e) {
  147 + this.loadedModules[url].error(new Error(`Unable to init module from url: ${url}`));
  148 + delete this.loadedModules[url];
  149 + }
  150 + },
  151 + (e) => {
97 this.loadedModules[url].error(new Error(`Unable to compile module from url: ${url}`)); 152 this.loadedModules[url].error(new Error(`Unable to compile module from url: ${url}`));
98 delete this.loadedModules[url]; 153 delete this.loadedModules[url];
99 - }); 154 + });
100 } else { 155 } else {
101 - this.loadedModules[url].error(new Error(`Module '${url}' doesn't have default export!`)); 156 + this.loadedModules[url].error(new Error(`Module '${url}' doesn't have default export or not NgModule!`));
102 delete this.loadedModules[url]; 157 delete this.loadedModules[url];
103 } 158 }
104 }, 159 },
@@ -41,6 +41,43 @@ import { NULL_UUID } from '@shared/models/id/has-uuid'; @@ -41,6 +41,43 @@ import { NULL_UUID } from '@shared/models/id/has-uuid';
41 import { WidgetTypeId } from '@app/shared/models/id/widget-type-id'; 41 import { WidgetTypeId } from '@app/shared/models/id/widget-type-id';
42 import { TenantId } from '@app/shared/models/id/tenant-id'; 42 import { TenantId } from '@app/shared/models/id/tenant-id';
43 import { SharedModule } from '@shared/shared.module'; 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 // @dynamic 82 // @dynamic
46 @Injectable() 83 @Injectable()
@@ -105,8 +142,8 @@ export class WidgetComponentService { @@ -105,8 +142,8 @@ export class WidgetComponentService {
105 const initSubject = new ReplaySubject(); 142 const initSubject = new ReplaySubject();
106 this.init$ = initSubject.asObservable(); 143 this.init$ = initSubject.asObservable();
107 const loadDefaultWidgetInfoTasks = [ 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 forkJoin(loadDefaultWidgetInfoTasks).subscribe( 148 forkJoin(loadDefaultWidgetInfoTasks).subscribe(
112 () => { 149 () => {
@@ -218,31 +255,71 @@ export class WidgetComponentService { @@ -218,31 +255,71 @@ export class WidgetComponentService {
218 this.cssParser.cssPreviewNamespace = widgetNamespace; 255 this.cssParser.cssPreviewNamespace = widgetNamespace;
219 this.cssParser.createStyleElement(widgetNamespace, widgetInfo.templateCss); 256 this.cssParser.createStyleElement(widgetNamespace, widgetInfo.templateCss);
220 const resourceTasks: Observable<string>[] = []; 257 const resourceTasks: Observable<string>[] = [];
  258 + const modulesTasks: Observable<Type<any>[] | string>[] = [];
221 if (widgetInfo.resources.length > 0) { 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 resourceTasks.push( 272 resourceTasks.push(
224 this.resources.loadResource(resource.url).pipe( 273 this.resources.loadResource(resource.url).pipe(
225 catchError(e => of(`Failed to load widget resource: '${resource.url}'`)) 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 resourceTasks.push( 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 return forkJoin(resourceTasks).pipe( 324 return forkJoin(resourceTasks).pipe(
248 switchMap(msgs => { 325 switchMap(msgs => {
@@ -129,6 +129,10 @@ @@ -129,6 +129,10 @@
129 (ngModelChange)="isDirty = true" 129 (ngModelChange)="isDirty = true"
130 placeholder="{{ 'widget.resource-url' | translate }}"/> 130 placeholder="{{ 'widget.resource-url' | translate }}"/>
131 </mat-form-field> 131 </mat-form-field>
  132 + <mat-checkbox [(ngModel)]="resource.isModule"
  133 + (ngModelChange)="isDirty = true">
  134 + {{ 'widget.resource-is-module' | translate }}
  135 + </mat-checkbox>
132 <button mat-icon-button color="primary" 136 <button mat-icon-button color="primary"
133 [disabled]="isLoading$ | async" 137 [disabled]="isLoading$ | async"
134 (click)="removeResource(i)" 138 (click)="removeResource(i)"
@@ -53,6 +53,7 @@ export interface NodeScriptTestDialogData { @@ -53,6 +53,7 @@ export interface NodeScriptTestDialogData {
53 msgType?: string; 53 msgType?: string;
54 } 54 }
55 55
  56 +// @dynamic
56 @Component({ 57 @Component({
57 selector: 'tb-node-script-test-dialog', 58 selector: 'tb-node-script-test-dialog',
58 templateUrl: './node-script-test-dialog.component.html', 59 templateUrl: './node-script-test-dialog.component.html',
@@ -114,6 +114,7 @@ export const widgetTypesData = new Map<widgetType, WidgetTypeData>( @@ -114,6 +114,7 @@ export const widgetTypesData = new Map<widgetType, WidgetTypeData>(
114 114
115 export interface WidgetResource { 115 export interface WidgetResource {
116 url: string; 116 url: string;
  117 + isModule?: boolean;
117 } 118 }
118 119
119 export interface WidgetActionSource { 120 export interface WidgetActionSource {
@@ -1787,6 +1787,7 @@ @@ -1787,6 +1787,7 @@
1787 "type": "Widget type", 1787 "type": "Widget type",
1788 "resources": "Resources", 1788 "resources": "Resources",
1789 "resource-url": "JavaScript/CSS URL", 1789 "resource-url": "JavaScript/CSS URL",
  1790 + "resource-is-module": "Is module",
1790 "remove-resource": "Remove resource", 1791 "remove-resource": "Remove resource",
1791 "add-resource": "Add resource", 1792 "add-resource": "Add resource",
1792 "html": "HTML", 1793 "html": "HTML",