From 4f3e29b43339f44189fe4ea7a906326823468e3f Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Tue, 14 Jul 2020 16:14:38 +0300 Subject: [PATCH 1/3] UI: Fix pageLink without pagination. Minor fixes --- ui-ngx/package-lock.json | 4 ++-- .../src/app/core/api/widget-subscription.ts | 19 ++++++++++++++++++- .../lib/alarms-table-widget.component.ts | 15 ++++++++++----- .../lib/entities-table-widget.component.ts | 15 ++++++++++----- .../components/widget/lib/maps/maps-utils.ts | 4 ++-- .../widget/lib/table-widget.models.ts | 16 ++++++++++++++-- 6 files changed, 56 insertions(+), 17 deletions(-) diff --git a/ui-ngx/package-lock.json b/ui-ngx/package-lock.json index d4b7aa7e59..2ff4ae7175 100644 --- a/ui-ngx/package-lock.json +++ b/ui-ngx/package-lock.json @@ -8997,10 +8997,10 @@ "integrity": "sha512-4O3GWAYJaauMCILm07weko2rHA8a4kjn7+8Lg4s1d7SxwS/3IpkVD/GljbRrIJ1c1W/XGJ3GbuK7RyYZEJChhw==" }, "ngx-flowchart": { - "version": "git://github.com/thingsboard/ngx-flowchart.git#a4157b0eef2eb3646ef920447c7b06b39d54f87f", + "version": "git://github.com/thingsboard/ngx-flowchart.git#7a02f4748b5e7821a883c903107af5f20415d026", "from": "git://github.com/thingsboard/ngx-flowchart.git#master", "requires": { - "tslib": "^1.10.0" + "tslib": "^1.13.0" }, "dependencies": { "tslib": { diff --git a/ui-ngx/src/app/core/api/widget-subscription.ts b/ui-ngx/src/app/core/api/widget-subscription.ts index 54988774aa..eb5b5cd780 100644 --- a/ui-ngx/src/app/core/api/widget-subscription.ts +++ b/ui-ngx/src/app/core/api/widget-subscription.ts @@ -337,6 +337,7 @@ export class WidgetSubscription implements IWidgetSubscription { } private initDataSubscription(): Observable { + this.notifyDataLoading(); const initDataSubscriptionSubject = new ReplaySubject(1); this.loadStDiff().subscribe(() => { if (!this.ctx.aliasController) { @@ -431,6 +432,7 @@ export class WidgetSubscription implements IWidgetSubscription { }); this.configureLoadedData(); this.hasResolvedData = this.datasources.length > 0; + this.updateDataTimewindow(); this.notifyDataLoaded(); this.onDataUpdated(true); }) @@ -731,6 +733,7 @@ export class WidgetSubscription implements IWidgetSubscription { if (this.type === widgetType.alarm) { this.updateAlarmDataSubscription(); } else { + this.notifyDataLoading(); this.dataSubscribe(); } } @@ -831,13 +834,21 @@ export class WidgetSubscription implements IWidgetSubscription { } } - private dataSubscribe() { + private updateDataTimewindow() { if (!this.hasDataPageLink) { if (this.type === widgetType.timeseries && this.timeWindowConfig) { this.updateRealtimeSubscription(); if (this.comparisonEnabled) { this.updateSubscriptionForComparison(); } + } + } + } + + private dataSubscribe() { + if (!this.hasDataPageLink) { + if (this.type === widgetType.timeseries && this.timeWindowConfig) { + this.updateDataTimewindow(); if (this.subscriptionTimewindow.fixedWindow) { this.onDataUpdated(); } @@ -1025,6 +1036,11 @@ export class WidgetSubscription implements IWidgetSubscription { } } + private notifyDataLoading() { + this.loadingData = true; + this.callbacks.dataLoading(this); + } + private notifyDataLoaded() { this.loadingData = false; this.callbacks.dataLoading(this); @@ -1268,6 +1284,7 @@ export class WidgetSubscription implements IWidgetSubscription { if (this.caulculateLegendData) { this.updateLegend(index, data.data, detectChanges); } + this.notifyDataLoaded(); this.onDataUpdated(detectChanges); } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts index 4ba0aed569..c88d23fa10 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts @@ -330,7 +330,7 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, this.defaultPageSize = pageSize; } this.pageSizeOptions = [this.defaultPageSize, this.defaultPageSize * 2, this.defaultPageSize * 3]; - this.pageLink.pageSize = this.displayPagination ? this.defaultPageSize : Number.POSITIVE_INFINITY; + this.pageLink.pageSize = this.displayPagination ? this.defaultPageSize : 1024; this.pageLink.searchPropagatedAlarms = isDefined(this.widgetConfig.searchPropagatedAlarms) ? this.widgetConfig.searchPropagatedAlarms : true; @@ -535,10 +535,15 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, } else { this.pageLink.page = 0; } - this.pageLink.sortOrder = { - key: findEntityKeyByColumnDef(this.sort.active, this.columns), - direction: Direction[this.sort.direction.toUpperCase()] - }; + const key = findEntityKeyByColumnDef(this.sort.active, this.columns); + if (key) { + this.pageLink.sortOrder = { + key, + direction: Direction[this.sort.direction.toUpperCase()] + }; + } else { + this.pageLink.sortOrder = null; + } const sortOrderLabel = fromEntityColumnDef(this.sort.active, this.columns); const keyFilters: KeyFilter[] = null; // TODO: this.alarmsDatasource.loadAlarms(this.pageLink, sortOrderLabel, keyFilters); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts index 5bf040ed0d..77aff9c658 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts @@ -240,7 +240,7 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni this.defaultPageSize = pageSize; } this.pageSizeOptions = [this.defaultPageSize, this.defaultPageSize * 2, this.defaultPageSize * 3]; - this.pageLink.pageSize = this.displayPagination ? this.defaultPageSize : Number.POSITIVE_INFINITY; + this.pageLink.pageSize = this.displayPagination ? this.defaultPageSize : 1024; const cssString = constructTableCssString(this.widgetConfig); const cssParser = new cssjs(); @@ -460,10 +460,15 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni } else { this.pageLink.page = 0; } - this.pageLink.sortOrder = { - key: findEntityKeyByColumnDef(this.sort.active, this.columns), - direction: Direction[this.sort.direction.toUpperCase()] - }; + const key = findEntityKeyByColumnDef(this.sort.active, this.columns); + if (key) { + this.pageLink.sortOrder = { + key, + direction: Direction[this.sort.direction.toUpperCase()] + }; + } else { + this.pageLink.sortOrder = null; + } const sortOrderLabel = fromEntityColumnDef(this.sort.active, this.columns); const keyFilters: KeyFilter[] = null; // TODO: this.entityDatasource.loadEntities(this.pageLink, sortOrderLabel, keyFilters); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/maps-utils.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/maps-utils.ts index 0dad455478..576e594df6 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/maps-utils.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/maps-utils.ts @@ -159,7 +159,7 @@ function parseTemplate(template: string, data: { $datasource?: Datasource, [key: } template = createLabelFromDatasource(data.$datasource, template); - let match = varsRegex.exec(template); + let match = /\${([^}]*)}/g.exec(template); while (match !== null) { const variable = match[0]; let label = match[1]; @@ -186,7 +186,7 @@ function parseTemplate(template: string, data: { $datasource?: Datasource, [key: textValue = value; } template = template.split(variable).join(textValue); - match = varsRegex.exec(template); + match = /\${([^}]*)}/g.exec(template); } let actionTags: string; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts index dd295da76d..a6edfb86a5 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts @@ -96,7 +96,10 @@ export function entityDataSortOrderFromString(strSortOrder: string, columns: Ent if (!property && !property.length) { return null; } - const column = findColumnByLabel(property, columns); + let column = findColumnByLabel(property, columns); + if (!column) { + column = findColumnByName(property, columns); + } if (column && column.entityKey) { return {key: column.entityKey, direction}; } @@ -113,13 +116,22 @@ export function findColumnByEntityKey(key: EntityKey, columns: EntityColumn[]): } export function findEntityKeyByColumnDef(def: string, columns: EntityColumn[]): EntityKey { - return findColumnByDef(def, columns).entityKey; + if (def) { + const column = findColumnByDef(def, columns); + return column ? column.entityKey : null; + } else { + return null; + } } export function findColumn(searchProperty: string, searchValue: string, columns: EntityColumn[]): EntityColumn { return columns.find(theColumn => theColumn[searchProperty] === searchValue); } +export function findColumnByName(name: string, columns: EntityColumn[]): EntityColumn { + return findColumn('name', name, columns); +} + export function findColumnByLabel(label: string, columns: EntityColumn[]): EntityColumn { let column: EntityColumn; const alarmColumns = columns.filter(c => c.type === DataKeyType.alarm); From 21d7350efc08dcec339f02d091dde55727e54d82 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Wed, 15 Jul 2020 12:44:16 +0300 Subject: [PATCH 2/3] UI: External angular modules for widget development --- ui-ngx/proxy.conf.js | 4 + .../src/app/core/http/rule-chain.service.ts | 2 +- .../app/core/services/resources.service.ts | 77 ++++++++++-- .../widget/widget-component.service.ts | 115 +++++++++++++++--- .../pages/widget/widget-editor.component.html | 4 + .../node-script-test-dialog.component.ts | 1 + ui-ngx/src/app/shared/models/widget.models.ts | 1 + .../assets/locale/locale.constant-en_US.json | 1 + 8 files changed, 174 insertions(+), 31 deletions(-) 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.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[]> { + 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!`)); + 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}'`)) ) ); - }); + } + ); + + 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( - 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); - }) - ) + 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 }} +