diff --git a/ui-ngx/proxy.conf.js b/ui-ngx/proxy.conf.js index 243f260870..9d9c01f7cc 100644 --- a/ui-ngx/proxy.conf.js +++ b/ui-ngx/proxy.conf.js @@ -25,6 +25,10 @@ const PROXY_CONFIG = { "target": `http://${ruleNodeUiforwardHost}:${ruleNodeUiforwardPort}`, "secure": false, }, + "/static": { + "target": "http://localhost:8080", + "secure": false, + }, "/api/ws": { "target": "ws://localhost:8080", "ws": true, diff --git a/ui-ngx/src/app/core/http/rule-chain.service.ts b/ui-ngx/src/app/core/http/rule-chain.service.ts index c93d67c58b..2ac567ac30 100644 --- a/ui-ngx/src/app/core/http/rule-chain.service.ts +++ b/ui-ngx/src/app/core/http/rule-chain.service.ts @@ -239,7 +239,7 @@ export class RuleChainService { }); } if (moduleResource) { - tasks.push(this.resourcesService.loadModule(moduleResource, ruleNodeConfigResourcesModulesMap).pipe( + tasks.push(this.resourcesService.loadFactories(moduleResource, ruleNodeConfigResourcesModulesMap).pipe( map((res) => { if (nodeDefinition.configDirective && nodeDefinition.configDirective.length) { const selector = snakeCase(nodeDefinition.configDirective, '-'); diff --git a/ui-ngx/src/app/core/services/resources.service.ts b/ui-ngx/src/app/core/services/resources.service.ts index 16667ec64c..9a1b948753 100644 --- a/ui-ngx/src/app/core/services/resources.service.ts +++ b/ui-ngx/src/app/core/services/resources.service.ts @@ -25,6 +25,8 @@ import { } from '@angular/core'; import { DOCUMENT } from '@angular/common'; import { forkJoin, Observable, ReplaySubject, throwError } from 'rxjs'; +import { HttpClient } from '@angular/common/http'; +import { objToBase64 } from '@core/utils'; declare const SystemJS; @@ -34,12 +36,14 @@ declare const SystemJS; export class ResourcesService { private loadedResources: { [url: string]: ReplaySubject } = {}; - private loadedModules: { [url: string]: ReplaySubject[]> } = {}; + private loadedModules: { [url: string]: ReplaySubject[]> } = {}; + private loadedFactories: { [url: string]: ReplaySubject[]> } = {}; private anchor = this.document.getElementsByTagName('head')[0] || this.document.getElementsByTagName('body')[0]; constructor(@Inject(DOCUMENT) private readonly document: any, private compiler: Compiler, + private http: HttpClient, private injector: Injector) {} public loadResource(url: string): Observable { @@ -60,12 +64,12 @@ export class ResourcesService { return this.loadResourceByType(fileType, url); } - public loadModule(url: string, modulesMap: {[key: string]: any}): Observable[]> { - if (this.loadedModules[url]) { - return this.loadedModules[url].asObservable(); + public loadFactories(url: string, modulesMap: {[key: string]: any}): Observable[]> { + if (this.loadedFactories[url]) { + return this.loadedFactories[url].asObservable(); } const subject = new ReplaySubject[]>(); - this.loadedModules[url] = subject; + this.loadedFactories[url] = subject; if (modulesMap) { for (const moduleId of Object.keys(modulesMap)) { SystemJS.set(moduleId, modulesMap[moduleId]); @@ -86,19 +90,70 @@ export class ResourcesService { c.ngModuleFactory.create(this.injector); componentFactories.push(...c.componentFactories); } - this.loadedModules[url].next(componentFactories); - this.loadedModules[url].complete(); + this.loadedFactories[url].next(componentFactories); + this.loadedFactories[url].complete(); } catch (e) { - this.loadedModules[url].error(new Error(`Unable to init module from url: ${url}`)); - delete this.loadedModules[url]; + this.loadedFactories[url].error(new Error(`Unable to init module from url: ${url}`)); + delete this.loadedFactories[url]; } }, (e) => { - this.loadedModules[url].error(new Error(`Unable to compile module from url: ${url}`)); - delete this.loadedModules[url]; + this.loadedFactories[url].error(new Error(`Unable to compile module from url: ${url}`)); + delete this.loadedFactories[url]; }); } else { - this.loadedModules[url].error(new Error(`Module '${url}' doesn't have default export!`)); + this.loadedFactories[url].error(new Error(`Module '${url}' doesn't have default export!`)); + delete this.loadedFactories[url]; + } + }, + (e) => { + this.loadedFactories[url].error(new Error(`Unable to load module from url: ${url}`)); + delete this.loadedFactories[url]; + } + ); + return subject.asObservable(); + } + + public loadModules(url: string, modulesMap: {[key: string]: any}): Observable[]> { + if (this.loadedModules[url]) { + return this.loadedModules[url].asObservable(); + } + const subject = new ReplaySubject[]>(); + this.loadedModules[url] = subject; + if (modulesMap) { + for (const moduleId of Object.keys(modulesMap)) { + SystemJS.set(moduleId, modulesMap[moduleId]); + } + } + SystemJS.import(url).then( + (module) => { + let modules; + try { + modules = this.extractNgModules(module); + } catch (e) {} + if (modules && modules.length) { + const tasks: Promise>[] = []; + for (const m of modules) { + tasks.push(this.compiler.compileModuleAndAllComponentsAsync(m)); + } + forkJoin(tasks).subscribe((compiled) => { + try { + for (const c of compiled) { + c.ngModuleFactory.create(this.injector); + } + this.loadedModules[url].next(modules); + this.loadedModules[url].complete(); + } catch (e) { + this.loadedModules[url].error(new Error(`Unable to init module from url: ${url}`)); + delete this.loadedModules[url]; + } + }, + (e) => { + this.loadedModules[url].error(new Error(`Unable to compile module from url: ${url}`)); + delete this.loadedModules[url]; + }); + } else { + this.loadedModules[url].error(new Error(`Module '${url}' doesn't have default export or not NgModule!`)); delete this.loadedModules[url]; } }, diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts b/ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts index b4035be6d8..cbcf86807d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts @@ -41,6 +41,43 @@ import { NULL_UUID } from '@shared/models/id/has-uuid'; import { WidgetTypeId } from '@app/shared/models/id/widget-type-id'; import { TenantId } from '@app/shared/models/id/tenant-id'; import { SharedModule } from '@shared/shared.module'; +import * as AngularCore from '@angular/core'; +import * as AngularCommon from '@angular/common'; +import * as AngularForms from '@angular/forms'; +import * as AngularRouter from '@angular/router'; +import * as AngularCdkKeycodes from '@angular/cdk/keycodes'; +import * as AngularCdkCoercion from '@angular/cdk/coercion'; +import * as AngularMaterialChips from '@angular/material/chips'; +import * as AngularMaterialAutocomplete from '@angular/material/autocomplete'; +import * as AngularMaterialDialog from '@angular/material/dialog'; +import * as NgrxStore from '@ngrx/store'; +import * as RxJs from 'rxjs'; +import * as RxJsOperators from 'rxjs/operators'; +import * as TranslateCore from '@ngx-translate/core'; +import * as TbCore from '@core/public-api'; +import * as TbShared from '@shared/public-api'; +import * as _moment from 'moment'; + +declare const SystemJS; + +const widgetResourcesModulesMap = { + '@angular/core': SystemJS.newModule(AngularCore), + '@angular/common': SystemJS.newModule(AngularCommon), + '@angular/forms': SystemJS.newModule(AngularForms), + '@angular/router': SystemJS.newModule(AngularRouter), + '@angular/cdk/keycodes': SystemJS.newModule(AngularCdkKeycodes), + '@angular/cdk/coercion': SystemJS.newModule(AngularCdkCoercion), + '@angular/material/chips': SystemJS.newModule(AngularMaterialChips), + '@angular/material/autocomplete': SystemJS.newModule(AngularMaterialAutocomplete), + '@angular/material/dialog': SystemJS.newModule(AngularMaterialDialog), + '@ngrx/store': SystemJS.newModule(NgrxStore), + rxjs: SystemJS.newModule(RxJs), + 'rxjs/operators': SystemJS.newModule(RxJsOperators), + '@ngx-translate/core': SystemJS.newModule(TranslateCore), + '@core/public-api': SystemJS.newModule(TbCore), + '@shared/public-api': SystemJS.newModule(TbShared), + moment: SystemJS.newModule(_moment) +}; // @dynamic @Injectable() @@ -105,8 +142,8 @@ export class WidgetComponentService { const initSubject = new ReplaySubject(); this.init$ = initSubject.asObservable(); const loadDefaultWidgetInfoTasks = [ - this.loadWidgetResources(this.missingWidgetType, 'global-widget-missing-type', [SharedModule]), - this.loadWidgetResources(this.errorWidgetType, 'global-widget-error-type', [SharedModule]), + this.loadWidgetResources(this.missingWidgetType, 'global-widget-missing-type', [SharedModule, WidgetComponentsModule]), + this.loadWidgetResources(this.errorWidgetType, 'global-widget-error-type', [SharedModule, WidgetComponentsModule]), ]; forkJoin(loadDefaultWidgetInfoTasks).subscribe( () => { @@ -218,31 +255,71 @@ export class WidgetComponentService { this.cssParser.cssPreviewNamespace = widgetNamespace; this.cssParser.createStyleElement(widgetNamespace, widgetInfo.templateCss); const resourceTasks: Observable[] = []; + const modulesTasks: Observable[] | string>[] = []; if (widgetInfo.resources.length > 0) { - widgetInfo.resources.forEach((resource) => { + widgetInfo.resources.filter(r => r.isModule).forEach( + (resource) => { + modulesTasks.push( + this.resources.loadModules(resource.url, widgetResourcesModulesMap).pipe( + catchError((e: Error) => of(e?.message ? e.message : `Failed to load widget resource module: '${resource.url}'`)) + ) + ); + } + ); + } + widgetInfo.resources.filter(r => !r.isModule).forEach( + (resource) => { resourceTasks.push( this.resources.loadResource(resource.url).pipe( catchError(e => of(`Failed to load widget resource: '${resource.url}'`)) ) ); - }); - } - resourceTasks.push( - this.dynamicComponentFactoryService.createDynamicComponentFactory( - class DynamicWidgetComponentInstance extends DynamicWidgetComponent {}, - widgetInfo.templateHtml, - modules - ).pipe( - map((factory) => { - widgetInfo.componentFactory = factory; - return null; - }), - catchError(e => { - const details = this.utils.parseException(e); - const errorMessage = `Failed to compile widget html. \n Error: ${details.message}`; - return of(errorMessage); + } + ); + + let modulesObservable: Observable[]>; + if (modulesTasks.length) { + modulesObservable = forkJoin(modulesTasks).pipe( + map(res => { + const msg = res.find(r => typeof r === 'string'); + if (msg) { + return msg as string; + } else { + let resModules = (res as Type[][]).flat(); + if (modules && modules.length) { + resModules = resModules.concat(modules); + } + return resModules; + } }) - ) + ); + } else { + modulesObservable = modules && modules.length ? of(modules) : of([]); + } + + resourceTasks.push( + modulesObservable.pipe( + mergeMap((resolvedModules) => { + if (typeof resolvedModules === 'string') { + return of(resolvedModules); + } else { + return this.dynamicComponentFactoryService.createDynamicComponentFactory( + class DynamicWidgetComponentInstance extends DynamicWidgetComponent {}, + widgetInfo.templateHtml, + resolvedModules + ).pipe( + map((factory) => { + widgetInfo.componentFactory = factory; + return null; + }), + catchError(e => { + const details = this.utils.parseException(e); + const errorMessage = `Failed to compile widget html. \n Error: ${details.message}`; + return of(errorMessage); + }) + ) + } + })) ); return forkJoin(resourceTasks).pipe( switchMap(msgs => { diff --git a/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.html b/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.html index 51c7236db2..a5a43a2ecc 100644 --- a/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.html +++ b/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.html @@ -129,6 +129,10 @@ (ngModelChange)="isDirty = true" placeholder="{{ 'widget.resource-url' | translate }}"/> + + {{ 'widget.resource-is-module' | translate }} +