UI: External angular modules for widget development

This commit is contained in:
Igor Kulikov 2020-07-15 12:44:16 +03:00
parent 2ca577e9c5
commit 21d7350efc
8 changed files with 176 additions and 33 deletions

View File

@ -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,

View File

@ -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, '-');

View File

@ -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<any> } = {};
private loadedModules: { [url: string]: ReplaySubject<ComponentFactory<any>[]> } = {};
private loadedModules: { [url: string]: ReplaySubject<Type<any>[]> } = {};
private loadedFactories: { [url: string]: ReplaySubject<ComponentFactory<any>[]> } = {};
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<any> {
@ -60,12 +64,12 @@ export class ResourcesService {
return this.loadResourceByType(fileType, url);
}
public loadModule(url: string, modulesMap: {[key: string]: any}): Observable<ComponentFactory<any>[]> {
if (this.loadedModules[url]) {
return this.loadedModules[url].asObservable();
public loadFactories(url: string, modulesMap: {[key: string]: any}): Observable<ComponentFactory<any>[]> {
if (this.loadedFactories[url]) {
return this.loadedFactories[url].asObservable();
}
const subject = new ReplaySubject<ComponentFactory<any>[]>();
this.loadedModules[url] = subject;
this.loadedFactories[url] = subject;
if (modulesMap) {
for (const moduleId of Object.keys(modulesMap)) {
SystemJS.set(moduleId, modulesMap[moduleId]);
@ -86,7 +90,58 @@ export class ResourcesService {
c.ngModuleFactory.create(this.injector);
componentFactories.push(...c.componentFactories);
}
this.loadedModules[url].next(componentFactories);
this.loadedFactories[url].next(componentFactories);
this.loadedFactories[url].complete();
} catch (e) {
this.loadedFactories[url].error(new Error(`Unable to init module from url: ${url}`));
delete this.loadedFactories[url];
}
},
(e) => {
this.loadedFactories[url].error(new Error(`Unable to compile module from url: ${url}`));
delete this.loadedFactories[url];
});
} else {
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<Type<any>[]> {
if (this.loadedModules[url]) {
return this.loadedModules[url].asObservable();
}
const subject = new ReplaySubject<Type<any>[]>();
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<ModuleWithComponentFactories<any>>[] = [];
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}`));
@ -98,7 +153,7 @@ export class ResourcesService {
delete this.loadedModules[url];
});
} else {
this.loadedModules[url].error(new Error(`Module '${url}' doesn't have default export!`));
this.loadedModules[url].error(new Error(`Module '${url}' doesn't have default export or not NgModule!`));
delete this.loadedModules[url];
}
},

View File

@ -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,20 +255,58 @@ export class WidgetComponentService {
this.cssParser.cssPreviewNamespace = widgetNamespace;
this.cssParser.createStyleElement(widgetNamespace, widgetInfo.templateCss);
const resourceTasks: Observable<string>[] = [];
const modulesTasks: Observable<Type<any>[] | 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}'`))
)
);
});
}
);
let modulesObservable: Observable<string | Type<any>[]>;
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<any>[][]).flat();
if (modules && modules.length) {
resModules = resModules.concat(modules);
}
return resModules;
}
})
);
} else {
modulesObservable = modules && modules.length ? of(modules) : of([]);
}
resourceTasks.push(
this.dynamicComponentFactoryService.createDynamicComponentFactory(
modulesObservable.pipe(
mergeMap((resolvedModules) => {
if (typeof resolvedModules === 'string') {
return of(resolvedModules);
} else {
return this.dynamicComponentFactoryService.createDynamicComponentFactory(
class DynamicWidgetComponentInstance extends DynamicWidgetComponent {},
widgetInfo.templateHtml,
modules
resolvedModules
).pipe(
map((factory) => {
widgetInfo.componentFactory = factory;
@ -243,6 +318,8 @@ export class WidgetComponentService {
return of(errorMessage);
})
)
}
}))
);
return forkJoin(resourceTasks).pipe(
switchMap(msgs => {

View File

@ -129,6 +129,10 @@
(ngModelChange)="isDirty = true"
placeholder="{{ 'widget.resource-url' | translate }}"/>
</mat-form-field>
<mat-checkbox [(ngModel)]="resource.isModule"
(ngModelChange)="isDirty = true">
{{ 'widget.resource-is-module' | translate }}
</mat-checkbox>
<button mat-icon-button color="primary"
[disabled]="isLoading$ | async"
(click)="removeResource(i)"

View File

@ -53,6 +53,7 @@ export interface NodeScriptTestDialogData {
msgType?: string;
}
// @dynamic
@Component({
selector: 'tb-node-script-test-dialog',
templateUrl: './node-script-test-dialog.component.html',

View File

@ -114,6 +114,7 @@ export const widgetTypesData = new Map<widgetType, WidgetTypeData>(
export interface WidgetResource {
url: string;
isModule?: boolean;
}
export interface WidgetActionSource {

View File

@ -1787,6 +1787,7 @@
"type": "Widget type",
"resources": "Resources",
"resource-url": "JavaScript/CSS URL",
"resource-is-module": "Is module",
"remove-resource": "Remove resource",
"add-resource": "Add resource",
"html": "HTML",