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 | 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, | ... | ... |
... | ... | @@ -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,70 @@ 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) => { |
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 | 152 | this.loadedModules[url].error(new Error(`Unable to compile module from url: ${url}`)); |
98 | 153 | delete this.loadedModules[url]; |
99 | - }); | |
154 | + }); | |
100 | 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 | 157 | delete this.loadedModules[url]; |
103 | 158 | } |
104 | 159 | }, | ... | ... |
... | ... | @@ -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 => { | ... | ... |
... | ... | @@ -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)" | ... | ... |
... | ... | @@ -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", | ... | ... |