Commit 21d7350efc08dcec339f02d091dde55727e54d82
1 parent
2ca577e9
UI: External angular modules for widget development
Showing
8 changed files
with
174 additions
and
31 deletions
@@ -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", |