Showing
14 changed files
with
688 additions
and
594 deletions
@@ -24,7 +24,11 @@ | @@ -24,7 +24,11 @@ | ||
24 | "tsConfig": "src/tsconfig.app.json", | 24 | "tsConfig": "src/tsconfig.app.json", |
25 | "assets": [ | 25 | "assets": [ |
26 | "src/thingsboard.ico", | 26 | "src/thingsboard.ico", |
27 | - "src/assets" | 27 | + "src/assets", |
28 | + { "glob": "worker-html.js", "input": "./node_modules/ace-builds/src-min/", "output": "/" }, | ||
29 | + { "glob": "worker-css.js", "input": "./node_modules/ace-builds/src-min/", "output": "/" }, | ||
30 | + { "glob": "worker-json.js", "input": "./node_modules/ace-builds/src-min/", "output": "/" }, | ||
31 | + { "glob": "worker-javascript.js", "input": "./node_modules/ace-builds/src-min/", "output": "/" } | ||
28 | ], | 32 | ], |
29 | "styles": [ | 33 | "styles": [ |
30 | "src/styles.scss" | 34 | "src/styles.scss" |
@@ -49,11 +53,7 @@ | @@ -49,11 +53,7 @@ | ||
49 | "node_modules/ace-builds/src-min/snippets/css.js", | 53 | "node_modules/ace-builds/src-min/snippets/css.js", |
50 | "node_modules/ace-builds/src-min/snippets/json.js", | 54 | "node_modules/ace-builds/src-min/snippets/json.js", |
51 | "node_modules/ace-builds/src-min/snippets/java.js", | 55 | "node_modules/ace-builds/src-min/snippets/java.js", |
52 | - "node_modules/ace-builds/src-min/snippets/javascript.js", | ||
53 | - { "bundleName": "worker-html", "input": "node_modules/ace-builds/src-min/worker-html.js" }, | ||
54 | - { "bundleName": "worker-css", "input": "node_modules/ace-builds/src-min/worker-css.js" }, | ||
55 | - { "bundleName": "worker-json", "input": "node_modules/ace-builds/src-min/worker-json.js" }, | ||
56 | - { "bundleName": "worker-javascript", "input": "node_modules/ace-builds/src-min/worker-javascript.js" } | 56 | + "node_modules/ace-builds/src-min/snippets/javascript.js" |
57 | ], | 57 | ], |
58 | "es5BrowserSupport": true, | 58 | "es5BrowserSupport": true, |
59 | "customWebpackConfig": { | 59 | "customWebpackConfig": { |
@@ -34,7 +34,7 @@ module.exports = { | @@ -34,7 +34,7 @@ module.exports = { | ||
34 | new CompressionPlugin({ | 34 | new CompressionPlugin({ |
35 | filename: "[path].gz[query]", | 35 | filename: "[path].gz[query]", |
36 | algorithm: "gzip", | 36 | algorithm: "gzip", |
37 | - test: /\.js$|\.css$|\.svg$|\.ttf$|\.woff$|\.woff2$|\.eot$|\.json$/, | 37 | + test: /\.js$|\.css$|\.html$|\.svg?.+$|\.jpg$|\.ttf?.+$|\.woff?.+$|\.eot?.+$|\.json$/, |
38 | threshold: 10240, | 38 | threshold: 10240, |
39 | minRatio: 0.8, | 39 | minRatio: 0.8, |
40 | deleteOriginalAssets: false | 40 | deleteOriginalAssets: false |
@@ -14,47 +14,29 @@ | @@ -14,47 +14,29 @@ | ||
14 | /// limitations under the License. | 14 | /// limitations under the License. |
15 | /// | 15 | /// |
16 | 16 | ||
17 | -import {Injectable} from '@angular/core'; | ||
18 | -import {defaultHttpOptions} from './http-utils'; | ||
19 | -import { Observable, ReplaySubject, Subject, of, forkJoin, throwError } from 'rxjs/index'; | ||
20 | -import {HttpClient} from '@angular/common/http'; | ||
21 | -import {PageLink} from '@shared/models/page/page-link'; | ||
22 | -import {PageData} from '@shared/models/page/page-data'; | ||
23 | -import {WidgetsBundle} from '@shared/models/widgets-bundle.model'; | ||
24 | -import { | ||
25 | - WidgetControllerDescriptor, | ||
26 | - WidgetInfo, | ||
27 | - WidgetType, | ||
28 | - WidgetTypeInstance, | ||
29 | - widgetActionSources, | ||
30 | - MissingWidgetType, toWidgetInfo, ErrorWidgetType | ||
31 | -} from '@shared/models/widget.models'; | 17 | +import { Injectable } from '@angular/core'; |
18 | +import { defaultHttpOptions } from './http-utils'; | ||
19 | +import { Observable } from 'rxjs/index'; | ||
20 | +import { HttpClient } from '@angular/common/http'; | ||
21 | +import { PageLink } from '@shared/models/page/page-link'; | ||
22 | +import { PageData } from '@shared/models/page/page-data'; | ||
23 | +import { WidgetsBundle } from '@shared/models/widgets-bundle.model'; | ||
24 | +import { WidgetType } from '@shared/models/widget.models'; | ||
32 | import { UtilsService } from '@core/services/utils.service'; | 25 | import { UtilsService } from '@core/services/utils.service'; |
33 | -import { isFunction, isUndefined } from '@core/utils'; | ||
34 | import { TranslateService } from '@ngx-translate/core'; | 26 | import { TranslateService } from '@ngx-translate/core'; |
35 | -import { AuthPayload } from '@core/auth/auth.models'; | ||
36 | -import cssjs from '@core/css/css'; | ||
37 | import { ResourcesService } from '../services/resources.service'; | 27 | import { ResourcesService } from '../services/resources.service'; |
38 | -import { catchError, map, switchMap } from 'rxjs/operators'; | ||
39 | 28 | ||
40 | @Injectable({ | 29 | @Injectable({ |
41 | providedIn: 'root' | 30 | providedIn: 'root' |
42 | }) | 31 | }) |
43 | export class WidgetService { | 32 | export class WidgetService { |
44 | 33 | ||
45 | - private cssParser = new cssjs(); | ||
46 | - | ||
47 | - private widgetsInfoInMemoryCache = new Map<string, WidgetInfo>(); | ||
48 | - | ||
49 | - private widgetsInfoFetchQueue = new Map<string, Array<Subject<WidgetInfo>>>(); | ||
50 | - | ||
51 | constructor( | 34 | constructor( |
52 | private http: HttpClient, | 35 | private http: HttpClient, |
53 | private utils: UtilsService, | 36 | private utils: UtilsService, |
54 | private resources: ResourcesService, | 37 | private resources: ResourcesService, |
55 | private translate: TranslateService | 38 | private translate: TranslateService |
56 | ) { | 39 | ) { |
57 | - this.cssParser.testMode = false; | ||
58 | } | 40 | } |
59 | 41 | ||
60 | public getWidgetBundles(pageLink: PageLink, ignoreErrors: boolean = false, | 42 | public getWidgetBundles(pageLink: PageLink, ignoreErrors: boolean = false, |
@@ -88,261 +70,4 @@ export class WidgetService { | @@ -88,261 +70,4 @@ export class WidgetService { | ||
88 | return this.http.get<WidgetType>(`/api/widgetType?isSystem=${isSystem}&bundleAlias=${bundleAlias}&alias=${widgetTypeAlias}`, | 70 | return this.http.get<WidgetType>(`/api/widgetType?isSystem=${isSystem}&bundleAlias=${bundleAlias}&alias=${widgetTypeAlias}`, |
89 | defaultHttpOptions(ignoreLoading, ignoreErrors)); | 71 | defaultHttpOptions(ignoreLoading, ignoreErrors)); |
90 | } | 72 | } |
91 | - | ||
92 | - public getWidgetInfo(bundleAlias: string, widgetTypeAlias: string, isSystem: boolean): Observable<WidgetInfo> { | ||
93 | - const widgetInfoSubject = new ReplaySubject<WidgetInfo>(); | ||
94 | - const widgetInfo = this.getWidgetInfoFromCache(bundleAlias, widgetTypeAlias, isSystem); | ||
95 | - if (widgetInfo) { | ||
96 | - widgetInfoSubject.next(widgetInfo); | ||
97 | - widgetInfoSubject.complete(); | ||
98 | - } else { | ||
99 | - if (this.utils.widgetEditMode) { | ||
100 | - // TODO: | ||
101 | - } else { | ||
102 | - const key = this.createWidgetInfoCacheKey(bundleAlias, widgetTypeAlias, isSystem); | ||
103 | - let fetchQueue = this.widgetsInfoFetchQueue.get(key); | ||
104 | - if (fetchQueue) { | ||
105 | - fetchQueue.push(widgetInfoSubject); | ||
106 | - } else { | ||
107 | - fetchQueue = new Array<Subject<WidgetInfo>>(); | ||
108 | - this.widgetsInfoFetchQueue.set(key, fetchQueue); | ||
109 | - this.getWidgetType(bundleAlias, widgetTypeAlias, isSystem).subscribe( | ||
110 | - (widgetType) => { | ||
111 | - this.loadWidget(widgetType, bundleAlias, isSystem, widgetInfoSubject); | ||
112 | - }, | ||
113 | - () => { | ||
114 | - widgetInfoSubject.next(MissingWidgetType); | ||
115 | - widgetInfoSubject.complete(); | ||
116 | - this.resolveWidgetsInfoFetchQueue(key, MissingWidgetType); | ||
117 | - } | ||
118 | - ); | ||
119 | - } | ||
120 | - } | ||
121 | - } | ||
122 | - return widgetInfoSubject.asObservable(); | ||
123 | - } | ||
124 | - | ||
125 | - private loadWidget(widgetType: WidgetType, bundleAlias: string, isSystem: boolean, widgetInfoSubject: Subject<WidgetInfo>) { | ||
126 | - const widgetInfo = toWidgetInfo(widgetType); | ||
127 | - const key = this.createWidgetInfoCacheKey(bundleAlias, widgetInfo.alias, isSystem); | ||
128 | - this.loadWidgetResources(widgetInfo, bundleAlias, isSystem).subscribe( | ||
129 | - () => { | ||
130 | - let widgetControllerDescriptor: WidgetControllerDescriptor = null; | ||
131 | - try { | ||
132 | - widgetControllerDescriptor = this.createWidgetControllerDescriptor(widgetInfo, key); | ||
133 | - } catch (e) { | ||
134 | - const details = this.utils.parseException(e); | ||
135 | - const errorMessage = `Failed to compile widget script. \n Error: ${details.message}`; | ||
136 | - this.processWidgetLoadError([errorMessage], key, widgetInfoSubject); | ||
137 | - } | ||
138 | - if (widgetControllerDescriptor) { | ||
139 | - if (widgetControllerDescriptor.settingsSchema) { | ||
140 | - widgetInfo.typeSettingsSchema = widgetControllerDescriptor.settingsSchema; | ||
141 | - } | ||
142 | - if (widgetControllerDescriptor.dataKeySettingsSchema) { | ||
143 | - widgetInfo.typeDataKeySettingsSchema = widgetControllerDescriptor.dataKeySettingsSchema; | ||
144 | - } | ||
145 | - widgetInfo.typeParameters = widgetControllerDescriptor.typeParameters; | ||
146 | - widgetInfo.actionSources = widgetControllerDescriptor.actionSources; | ||
147 | - widgetInfo.widgetTypeFunction = widgetControllerDescriptor.widgetTypeFunction; | ||
148 | - this.putWidgetInfoToCache(widgetInfo, bundleAlias, widgetInfo.alias, isSystem); | ||
149 | - if (widgetInfoSubject) { | ||
150 | - widgetInfoSubject.next(widgetInfo); | ||
151 | - widgetInfoSubject.complete(); | ||
152 | - } | ||
153 | - this.resolveWidgetsInfoFetchQueue(key, widgetInfo); | ||
154 | - } | ||
155 | - }, | ||
156 | - (errorMessages: string[]) => { | ||
157 | - this.processWidgetLoadError(errorMessages, key, widgetInfoSubject); | ||
158 | - } | ||
159 | - ); | ||
160 | - } | ||
161 | - | ||
162 | - private loadWidgetResources(widgetInfo: WidgetInfo, bundleAlias: string, isSystem: boolean): Observable<any> { | ||
163 | - const widgetNamespace = `widget-type-${(isSystem ? 'sys-' : '')}${bundleAlias}-${widgetInfo.alias}`; | ||
164 | - this.cssParser.cssPreviewNamespace = widgetNamespace; | ||
165 | - this.cssParser.createStyleElement(widgetNamespace, widgetInfo.templateCss); | ||
166 | - const resourceTasks: Observable<string>[] = []; | ||
167 | - if (widgetInfo.resources.length > 0) { | ||
168 | - widgetInfo.resources.forEach((resource) => { | ||
169 | - resourceTasks.push( | ||
170 | - this.resources.loadResource(resource.url).pipe( | ||
171 | - catchError(e => of(`Failed to load widget resource: '${resource.url}'`)) | ||
172 | - ) | ||
173 | - ); | ||
174 | - }); | ||
175 | - return forkJoin(resourceTasks).pipe( | ||
176 | - switchMap(msgs => { | ||
177 | - let errors: string[]; | ||
178 | - if (msgs && msgs.length) { | ||
179 | - errors = msgs.filter(msg => msg && msg.length > 0); | ||
180 | - } | ||
181 | - if (errors && errors.length) { | ||
182 | - return throwError(errors); | ||
183 | - } else { | ||
184 | - return of(null); | ||
185 | - } | ||
186 | - } | ||
187 | - )); | ||
188 | - } else { | ||
189 | - return of(null); | ||
190 | - } | ||
191 | - } | ||
192 | - | ||
193 | - private createWidgetControllerDescriptor(widgetInfo: WidgetInfo, name: string): WidgetControllerDescriptor { | ||
194 | - let widgetTypeFunctionBody = `return function ${name} (ctx) {\n` + | ||
195 | - ' var self = this;\n' + | ||
196 | - ' self.ctx = ctx;\n\n'; /*+ | ||
197 | - | ||
198 | - ' self.onInit = function() {\n\n' + | ||
199 | - | ||
200 | - ' }\n\n' + | ||
201 | - | ||
202 | - ' self.onDataUpdated = function() {\n\n' + | ||
203 | - | ||
204 | - ' }\n\n' + | ||
205 | - | ||
206 | - ' self.useCustomDatasources = function() {\n\n' + | ||
207 | - | ||
208 | - ' }\n\n' + | ||
209 | - | ||
210 | - ' self.typeParameters = function() {\n\n' + | ||
211 | - return { | ||
212 | - useCustomDatasources: false, | ||
213 | - maxDatasources: -1, //unlimited | ||
214 | - maxDataKeys: -1, //unlimited | ||
215 | - dataKeysOptional: false, | ||
216 | - stateData: false | ||
217 | - }; | ||
218 | - ' }\n\n' + | ||
219 | - | ||
220 | - ' self.actionSources = function() {\n\n' + | ||
221 | - return { | ||
222 | - 'headerButton': { | ||
223 | - name: 'Header button', | ||
224 | - multiple: true | ||
225 | - } | ||
226 | - }; | ||
227 | - }\n\n' + | ||
228 | - ' self.onResize = function() {\n\n' + | ||
229 | - | ||
230 | - ' }\n\n' + | ||
231 | - | ||
232 | - ' self.onEditModeChanged = function() {\n\n' + | ||
233 | - | ||
234 | - ' }\n\n' + | ||
235 | - | ||
236 | - ' self.onMobileModeChanged = function() {\n\n' + | ||
237 | - | ||
238 | - ' }\n\n' + | ||
239 | - | ||
240 | - ' self.getSettingsSchema = function() {\n\n' + | ||
241 | - | ||
242 | - ' }\n\n' + | ||
243 | - | ||
244 | - ' self.getDataKeySettingsSchema = function() {\n\n' + | ||
245 | - | ||
246 | - ' }\n\n' + | ||
247 | - | ||
248 | - ' self.onDestroy = function() {\n\n' + | ||
249 | - | ||
250 | - ' }\n\n' + | ||
251 | - '}';*/ | ||
252 | - | ||
253 | - widgetTypeFunctionBody += widgetInfo.controllerScript; | ||
254 | - widgetTypeFunctionBody += '\n};\n'; | ||
255 | - | ||
256 | - try { | ||
257 | - | ||
258 | - const widgetTypeFunction = new Function(widgetTypeFunctionBody); | ||
259 | - const widgetType = widgetTypeFunction.apply(this); | ||
260 | - const widgetTypeInstance: WidgetTypeInstance = new widgetType(); | ||
261 | - const result: WidgetControllerDescriptor = { | ||
262 | - widgetTypeFunction: widgetType | ||
263 | - }; | ||
264 | - if (isFunction(widgetTypeInstance.getSettingsSchema)) { | ||
265 | - result.settingsSchema = widgetTypeInstance.getSettingsSchema(); | ||
266 | - } | ||
267 | - if (isFunction(widgetTypeInstance.getDataKeySettingsSchema)) { | ||
268 | - result.dataKeySettingsSchema = widgetTypeInstance.getDataKeySettingsSchema(); | ||
269 | - } | ||
270 | - if (isFunction(widgetTypeInstance.typeParameters)) { | ||
271 | - result.typeParameters = widgetTypeInstance.typeParameters(); | ||
272 | - } else { | ||
273 | - result.typeParameters = {}; | ||
274 | - } | ||
275 | - if (isFunction(widgetTypeInstance.useCustomDatasources)) { | ||
276 | - result.typeParameters.useCustomDatasources = widgetTypeInstance.useCustomDatasources(); | ||
277 | - } else { | ||
278 | - result.typeParameters.useCustomDatasources = false; | ||
279 | - } | ||
280 | - if (isUndefined(result.typeParameters.maxDatasources)) { | ||
281 | - result.typeParameters.maxDatasources = -1; | ||
282 | - } | ||
283 | - if (isUndefined(result.typeParameters.maxDataKeys)) { | ||
284 | - result.typeParameters.maxDataKeys = -1; | ||
285 | - } | ||
286 | - if (isUndefined(result.typeParameters.dataKeysOptional)) { | ||
287 | - result.typeParameters.dataKeysOptional = false; | ||
288 | - } | ||
289 | - if (isUndefined(result.typeParameters.stateData)) { | ||
290 | - result.typeParameters.stateData = false; | ||
291 | - } | ||
292 | - if (isFunction(widgetTypeInstance.actionSources)) { | ||
293 | - result.actionSources = widgetTypeInstance.actionSources(); | ||
294 | - } else { | ||
295 | - result.actionSources = {}; | ||
296 | - } | ||
297 | - for (const actionSourceId of Object.keys(widgetActionSources)) { | ||
298 | - result.actionSources[actionSourceId] = {...widgetActionSources[actionSourceId]}; | ||
299 | - result.actionSources[actionSourceId].name = this.translate.instant(result.actionSources[actionSourceId].name); | ||
300 | - } | ||
301 | - return result; | ||
302 | - } catch (e) { | ||
303 | - this.utils.processWidgetException(e); | ||
304 | - throw e; | ||
305 | - } | ||
306 | - } | ||
307 | - | ||
308 | - private processWidgetLoadError(errorMessages: string[], cacheKey: string, widgetInfoSubject: Subject<WidgetInfo>) { | ||
309 | - const widgetInfo = {...ErrorWidgetType}; | ||
310 | - errorMessages.forEach(error => { | ||
311 | - widgetInfo.templateHtml += `<div class="tb-widget-error-msg">${error}</div>`; | ||
312 | - }); | ||
313 | - widgetInfo.templateHtml += '</div>'; | ||
314 | - if (widgetInfoSubject) { | ||
315 | - widgetInfoSubject.next(widgetInfo); | ||
316 | - widgetInfoSubject.complete(); | ||
317 | - } | ||
318 | - this.resolveWidgetsInfoFetchQueue(cacheKey, widgetInfo); | ||
319 | - } | ||
320 | - | ||
321 | - private resolveWidgetsInfoFetchQueue(key: string, widgetInfo: WidgetInfo) { | ||
322 | - const fetchQueue = this.widgetsInfoFetchQueue.get(key); | ||
323 | - if (fetchQueue) { | ||
324 | - fetchQueue.forEach(subject => { | ||
325 | - subject.next(widgetInfo); | ||
326 | - subject.complete(); | ||
327 | - }); | ||
328 | - this.widgetsInfoFetchQueue.delete(key); | ||
329 | - } | ||
330 | - } | ||
331 | - | ||
332 | - // Cache functions | ||
333 | - | ||
334 | - private createWidgetInfoCacheKey(bundleAlias: string, widgetTypeAlias: string, isSystem: boolean): string { | ||
335 | - return `${isSystem ? 'sys_' : ''}${bundleAlias}_${widgetTypeAlias}`; | ||
336 | - } | ||
337 | - | ||
338 | - private getWidgetInfoFromCache(bundleAlias: string, widgetTypeAlias: string, isSystem: boolean): WidgetInfo | undefined { | ||
339 | - const key = this.createWidgetInfoCacheKey(bundleAlias, widgetTypeAlias, isSystem); | ||
340 | - return this.widgetsInfoInMemoryCache.get(key); | ||
341 | - } | ||
342 | - | ||
343 | - private putWidgetInfoToCache(widgetInfo: WidgetInfo, bundleAlias: string, widgetTypeAlias: string, isSystem: boolean) { | ||
344 | - const key = this.createWidgetInfoCacheKey(bundleAlias, widgetTypeAlias, isSystem); | ||
345 | - this.widgetsInfoInMemoryCache.set(key, widgetInfo); | ||
346 | - } | ||
347 | - | ||
348 | } | 73 | } |
ui-ngx/src/app/core/services/dynamic-component-factory.service.ts
renamed from
ui-ngx/src/app/modules/home/components/widget/dynamic-widget-component-factory.service.ts
@@ -22,63 +22,79 @@ import { | @@ -22,63 +22,79 @@ import { | ||
22 | Injector, | 22 | Injector, |
23 | NgModule, | 23 | NgModule, |
24 | NgModuleRef, | 24 | NgModuleRef, |
25 | - Type, | ||
26 | - ViewEncapsulation | 25 | + OnDestroy, |
26 | + Type | ||
27 | } from '@angular/core'; | 27 | } from '@angular/core'; |
28 | -import { | ||
29 | - DynamicWidgetComponent, | ||
30 | - DynamicWidgetComponentModule | ||
31 | -} from '@home/components/widget/dynamic-widget.component'; | ||
32 | -import { CommonModule } from '@angular/common'; | ||
33 | -import { SharedModule } from '@shared/shared.module'; | ||
34 | import { Observable, ReplaySubject } from 'rxjs'; | 28 | import { Observable, ReplaySubject } from 'rxjs'; |
35 | -import { HomeComponentsModule } from '../home-components.module'; | ||
36 | -import { WidgetComponentsModule } from './widget-components.module'; | 29 | +import { CommonModule } from '@angular/common'; |
30 | + | ||
31 | +export abstract class DynamicComponentModule implements OnDestroy { | ||
32 | + | ||
33 | + ngOnDestroy(): void { | ||
34 | + console.log('Module destroyed!'); | ||
35 | + } | ||
37 | 36 | ||
38 | -interface DynamicWidgetComponentModuleData { | ||
39 | - moduleRef: NgModuleRef<DynamicWidgetComponentModule>; | ||
40 | - moduleType: Type<DynamicWidgetComponentModule>; | ||
41 | } | 37 | } |
42 | 38 | ||
43 | -@Injectable() | ||
44 | -export class DynamicWidgetComponentFactoryService { | 39 | +interface DynamicComponentModuleData { |
40 | + moduleRef: NgModuleRef<DynamicComponentModule>; | ||
41 | + moduleType: Type<DynamicComponentModule>; | ||
42 | +} | ||
45 | 43 | ||
46 | - private dynamicComponentModulesMap = new Map<ComponentFactory<DynamicWidgetComponent>, DynamicWidgetComponentModuleData>(); | 44 | +@Injectable( |
45 | + { | ||
46 | + providedIn: 'root' | ||
47 | + } | ||
48 | +) | ||
49 | +export class DynamicComponentFactoryService { | ||
50 | + | ||
51 | + private dynamicComponentModulesMap = new Map<ComponentFactory<any>, DynamicComponentModuleData>(); | ||
47 | 52 | ||
48 | constructor(private compiler: Compiler, | 53 | constructor(private compiler: Compiler, |
49 | private injector: Injector) { | 54 | private injector: Injector) { |
50 | } | 55 | } |
51 | 56 | ||
52 | - public createDynamicWidgetComponentFactory(template: string): Observable<ComponentFactory<DynamicWidgetComponent>> { | ||
53 | - const dymamicWidgetComponentFactorySubject = new ReplaySubject<ComponentFactory<DynamicWidgetComponent>>(); | ||
54 | - const comp = this.createDynamicWidgetComponent(template); | 57 | + public createDynamicComponentFactory<T>( |
58 | + componentType: Type<T>, | ||
59 | + template: string, | ||
60 | + modules?: Type<any>[]): Observable<ComponentFactory<T>> { | ||
61 | + const dymamicComponentFactorySubject = new ReplaySubject<ComponentFactory<T>>(); | ||
62 | + const comp = this.createDynamicComponent(componentType, template); | ||
63 | + let moduleImports = [CommonModule]; | ||
64 | + if (modules) { | ||
65 | + moduleImports = [...moduleImports, ...modules]; | ||
66 | + } | ||
55 | // noinspection AngularInvalidImportedOrDeclaredSymbol,AngularInvalidEntryComponent | 67 | // noinspection AngularInvalidImportedOrDeclaredSymbol,AngularInvalidEntryComponent |
56 | @NgModule({ | 68 | @NgModule({ |
57 | declarations: [comp], | 69 | declarations: [comp], |
58 | entryComponents: [comp], | 70 | entryComponents: [comp], |
59 | - imports: [CommonModule, SharedModule, WidgetComponentsModule], | 71 | + imports: moduleImports |
60 | }) | 72 | }) |
61 | - class DynamicWidgetComponentInstanceModule extends DynamicWidgetComponentModule {} | ||
62 | - this.compiler.compileModuleAsync(DynamicWidgetComponentInstanceModule).then( | ||
63 | - (module) => { | ||
64 | - const moduleRef = module.create(this.injector); | ||
65 | - const factory = moduleRef.componentFactoryResolver.resolveComponentFactory(comp); | ||
66 | - this.dynamicComponentModulesMap.set(factory, { | ||
67 | - moduleRef, | ||
68 | - moduleType: module.moduleType | ||
69 | - }); | ||
70 | - dymamicWidgetComponentFactorySubject.next(factory); | ||
71 | - dymamicWidgetComponentFactorySubject.complete(); | ||
72 | - } | ||
73 | - ).catch( | ||
74 | - (e) => { | ||
75 | - dymamicWidgetComponentFactorySubject.error(`Failed to create dynamic widget component factory: ${e}`); | ||
76 | - } | ||
77 | - ); | ||
78 | - return dymamicWidgetComponentFactorySubject.asObservable(); | 73 | + class DynamicComponentInstanceModule extends DynamicComponentModule {} |
74 | + try { | ||
75 | + this.compiler.compileModuleAsync(DynamicComponentInstanceModule).then( | ||
76 | + (module) => { | ||
77 | + const moduleRef = module.create(this.injector); | ||
78 | + const factory = moduleRef.componentFactoryResolver.resolveComponentFactory(comp); | ||
79 | + this.dynamicComponentModulesMap.set(factory, { | ||
80 | + moduleRef, | ||
81 | + moduleType: module.moduleType | ||
82 | + }); | ||
83 | + dymamicComponentFactorySubject.next(factory); | ||
84 | + dymamicComponentFactorySubject.complete(); | ||
85 | + } | ||
86 | + ).catch( | ||
87 | + (e) => { | ||
88 | + dymamicComponentFactorySubject.error(e); | ||
89 | + } | ||
90 | + ); | ||
91 | + } catch (e) { | ||
92 | + dymamicComponentFactorySubject.error(e); | ||
93 | + } | ||
94 | + return dymamicComponentFactorySubject.asObservable(); | ||
79 | } | 95 | } |
80 | 96 | ||
81 | - public destroyDynamicWidgetComponentFactory(factory: ComponentFactory<DynamicWidgetComponent>) { | 97 | + public destroyDynamicComponentFactory<T>(factory: ComponentFactory<T>) { |
82 | const moduleData = this.dynamicComponentModulesMap.get(factory); | 98 | const moduleData = this.dynamicComponentModulesMap.get(factory); |
83 | if (moduleData) { | 99 | if (moduleData) { |
84 | moduleData.moduleRef.destroy(); | 100 | moduleData.moduleRef.destroy(); |
@@ -87,14 +103,11 @@ export class DynamicWidgetComponentFactoryService { | @@ -87,14 +103,11 @@ export class DynamicWidgetComponentFactoryService { | ||
87 | } | 103 | } |
88 | } | 104 | } |
89 | 105 | ||
90 | - private createDynamicWidgetComponent(template: string): Type<DynamicWidgetComponent> { | 106 | + private createDynamicComponent<T>(componentType: Type<T>, template: string): Type<T> { |
91 | // noinspection AngularMissingOrInvalidDeclarationInModule | 107 | // noinspection AngularMissingOrInvalidDeclarationInModule |
92 | - @Component({ | 108 | + return Component({ |
93 | template | 109 | template |
94 | - }) | ||
95 | - class DynamicWidgetInstanceComponent extends DynamicWidgetComponent { } | ||
96 | - | ||
97 | - return DynamicWidgetInstanceComponent; | 110 | + })(componentType); |
98 | } | 111 | } |
99 | 112 | ||
100 | } | 113 | } |
@@ -16,7 +16,6 @@ | @@ -16,7 +16,6 @@ | ||
16 | 16 | ||
17 | import { Inject, Injectable } from '@angular/core'; | 17 | import { Inject, Injectable } from '@angular/core'; |
18 | import { WINDOW } from '@core/services/window.service'; | 18 | import { WINDOW } from '@core/services/window.service'; |
19 | -import { WidgetInfo } from '@shared/models/widget.models'; | ||
20 | import { ExceptionData } from '@app/shared/models/error.models'; | 19 | import { ExceptionData } from '@app/shared/models/error.models'; |
21 | import { isUndefined } from '@core/utils'; | 20 | import { isUndefined } from '@core/utils'; |
22 | import { WindowMessage } from '@shared/models/window-message.model'; | 21 | import { WindowMessage } from '@shared/models/window-message.model'; |
@@ -30,7 +29,7 @@ export class UtilsService { | @@ -30,7 +29,7 @@ export class UtilsService { | ||
30 | 29 | ||
31 | iframeMode = false; | 30 | iframeMode = false; |
32 | widgetEditMode = false; | 31 | widgetEditMode = false; |
33 | - editWidgetInfo: WidgetInfo = null; | 32 | + editWidgetInfo: any = null; |
34 | 33 | ||
35 | constructor(@Inject(WINDOW) private window: Window, | 34 | constructor(@Inject(WINDOW) private window: Window, |
36 | private translate: TranslateService) { | 35 | private translate: TranslateService) { |
@@ -36,7 +36,8 @@ import { AddAttributeDialogComponent } from './attribute/add-attribute-dialog.co | @@ -36,7 +36,8 @@ import { AddAttributeDialogComponent } from './attribute/add-attribute-dialog.co | ||
36 | import { EditAttributeValuePanelComponent } from './attribute/edit-attribute-value-panel.component'; | 36 | import { EditAttributeValuePanelComponent } from './attribute/edit-attribute-value-panel.component'; |
37 | import { DashboardComponent } from '@home/components/dashboard/dashboard.component'; | 37 | import { DashboardComponent } from '@home/components/dashboard/dashboard.component'; |
38 | import { WidgetComponent } from '@home/components/widget/widget.component'; | 38 | import { WidgetComponent } from '@home/components/widget/widget.component'; |
39 | -import { DynamicWidgetComponentFactoryService } from './widget/dynamic-widget-component-factory.service'; | 39 | +import { WidgetComponentService } from './widget/widget-component.service'; |
40 | +import { LegendComponent } from '@home/components/widget/legend.component'; | ||
40 | 41 | ||
41 | @NgModule({ | 42 | @NgModule({ |
42 | entryComponents: [ | 43 | entryComponents: [ |
@@ -69,7 +70,8 @@ import { DynamicWidgetComponentFactoryService } from './widget/dynamic-widget-co | @@ -69,7 +70,8 @@ import { DynamicWidgetComponentFactoryService } from './widget/dynamic-widget-co | ||
69 | AddAttributeDialogComponent, | 70 | AddAttributeDialogComponent, |
70 | EditAttributeValuePanelComponent, | 71 | EditAttributeValuePanelComponent, |
71 | DashboardComponent, | 72 | DashboardComponent, |
72 | - WidgetComponent | 73 | + WidgetComponent, |
74 | + LegendComponent | ||
73 | ], | 75 | ], |
74 | imports: [ | 76 | imports: [ |
75 | CommonModule, | 77 | CommonModule, |
@@ -88,10 +90,11 @@ import { DynamicWidgetComponentFactoryService } from './widget/dynamic-widget-co | @@ -88,10 +90,11 @@ import { DynamicWidgetComponentFactoryService } from './widget/dynamic-widget-co | ||
88 | AlarmDetailsDialogComponent, | 90 | AlarmDetailsDialogComponent, |
89 | AttributeTableComponent, | 91 | AttributeTableComponent, |
90 | DashboardComponent, | 92 | DashboardComponent, |
91 | - WidgetComponent | 93 | + WidgetComponent, |
94 | + LegendComponent | ||
92 | ], | 95 | ], |
93 | providers: [ | 96 | providers: [ |
94 | - DynamicWidgetComponentFactoryService | 97 | + WidgetComponentService |
95 | ] | 98 | ] |
96 | }) | 99 | }) |
97 | export class HomeComponentsModule { } | 100 | export class HomeComponentsModule { } |
@@ -21,24 +21,13 @@ import { AppState } from '@core/core.state'; | @@ -21,24 +21,13 @@ import { AppState } from '@core/core.state'; | ||
21 | import { WidgetContext, IDynamicWidgetComponent } from '@home/models/widget-component.models'; | 21 | import { WidgetContext, IDynamicWidgetComponent } from '@home/models/widget-component.models'; |
22 | import { ExceptionData } from '@shared/models/error.models'; | 22 | import { ExceptionData } from '@shared/models/error.models'; |
23 | 23 | ||
24 | -export abstract class DynamicWidgetComponentModule implements OnDestroy { | ||
25 | - | ||
26 | - ngOnDestroy(): void { | ||
27 | - console.log('Module destroyed!'); | ||
28 | - } | ||
29 | - | ||
30 | -} | ||
31 | - | ||
32 | -export abstract class DynamicWidgetComponent extends PageComponent implements IDynamicWidgetComponent, OnInit, OnDestroy { | 24 | +export class DynamicWidgetComponent extends PageComponent implements IDynamicWidgetComponent, OnInit, OnDestroy { |
33 | 25 | ||
34 | @Input() | 26 | @Input() |
35 | widgetContext: WidgetContext; | 27 | widgetContext: WidgetContext; |
36 | 28 | ||
37 | @Input() | 29 | @Input() |
38 | - widgetErrorData: ExceptionData; | ||
39 | - | ||
40 | - @Input() | ||
41 | - loadingData: boolean; | 30 | + errorMessages: string[]; |
42 | 31 | ||
43 | [key: string]: any; | 32 | [key: string]: any; |
44 | 33 | ||
@@ -51,7 +40,7 @@ export abstract class DynamicWidgetComponent extends PageComponent implements ID | @@ -51,7 +40,7 @@ export abstract class DynamicWidgetComponent extends PageComponent implements ID | ||
51 | } | 40 | } |
52 | 41 | ||
53 | ngOnDestroy(): void { | 42 | ngOnDestroy(): void { |
54 | - console.log('Component destroyed!'); | 43 | + console.log('Widget component destroyed!'); |
55 | } | 44 | } |
56 | 45 | ||
57 | clearRpcError() { | 46 | clearRpcError() { |
1 | +/// | ||
2 | +/// Copyright © 2016-2019 The Thingsboard Authors | ||
3 | +/// | ||
4 | +/// Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | +/// you may not use this file except in compliance with the License. | ||
6 | +/// You may obtain a copy of the License at | ||
7 | +/// | ||
8 | +/// http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | +/// | ||
10 | +/// Unless required by applicable law or agreed to in writing, software | ||
11 | +/// distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | +/// See the License for the specific language governing permissions and | ||
14 | +/// limitations under the License. | ||
15 | +/// | ||
16 | + | ||
17 | +import { Injectable } from '@angular/core'; | ||
18 | +import { DynamicComponentFactoryService } from '@core/services/dynamic-component-factory.service'; | ||
19 | +import { WidgetService } from '@core/http/widget.service'; | ||
20 | +import { forkJoin, Observable, of, ReplaySubject, Subject, throwError } from 'rxjs'; | ||
21 | +import { WidgetInfo, MissingWidgetType, toWidgetInfo, WidgetTypeInstance, ErrorWidgetType } from '@home/models/widget-component.models'; | ||
22 | +import cssjs from '@core/css/css'; | ||
23 | +import { UtilsService } from '@core/services/utils.service'; | ||
24 | +import { ResourcesService } from '@core/services/resources.service'; | ||
25 | +import { | ||
26 | + widgetActionSources, | ||
27 | + WidgetControllerDescriptor, | ||
28 | + WidgetType | ||
29 | +} from '@shared/models/widget.models'; | ||
30 | +import { catchError, switchMap, map, mergeMap } from 'rxjs/operators'; | ||
31 | +import { isFunction, isUndefined } from '@core/utils'; | ||
32 | +import { TranslateService } from '@ngx-translate/core'; | ||
33 | +import { DynamicWidgetComponent } from '@home/components/widget/dynamic-widget.component'; | ||
34 | +import { SharedModule } from '@shared/shared.module'; | ||
35 | +import { WidgetComponentsModule } from '@home/components/widget/widget-components.module'; | ||
36 | + | ||
37 | +@Injectable() | ||
38 | +export class WidgetComponentService { | ||
39 | + | ||
40 | + private cssParser = new cssjs(); | ||
41 | + | ||
42 | + private widgetsInfoInMemoryCache = new Map<string, WidgetInfo>(); | ||
43 | + | ||
44 | + private widgetsInfoFetchQueue = new Map<string, Array<Subject<WidgetInfo>>>(); | ||
45 | + | ||
46 | + private init$: Observable<any>; | ||
47 | + | ||
48 | + private missingWidgetType: WidgetInfo; | ||
49 | + private errorWidgetType: WidgetInfo; | ||
50 | + | ||
51 | + constructor(private dynamicComponentFactoryService: DynamicComponentFactoryService, | ||
52 | + private widgetService: WidgetService, | ||
53 | + private utils: UtilsService, | ||
54 | + private resources: ResourcesService, | ||
55 | + private translate: TranslateService) { | ||
56 | + this.cssParser.testMode = false; | ||
57 | + this.init(); | ||
58 | + } | ||
59 | + | ||
60 | + private init(): Observable<any> { | ||
61 | + if (this.init$) { | ||
62 | + return this.init$; | ||
63 | + } else { | ||
64 | + this.missingWidgetType = {...MissingWidgetType}; | ||
65 | + this.errorWidgetType = {...ErrorWidgetType}; | ||
66 | + const initSubject = new ReplaySubject(); | ||
67 | + this.init$ = initSubject.asObservable(); | ||
68 | + const loadDefaultWidgetInfoTasks = [ | ||
69 | + this.loadWidgetResources(this.missingWidgetType, 'global-widget-missing-type'), | ||
70 | + this.loadWidgetResources(this.errorWidgetType, 'global-widget-error-type'), | ||
71 | + ]; | ||
72 | + forkJoin(loadDefaultWidgetInfoTasks).subscribe( | ||
73 | + () => { | ||
74 | + initSubject.next(); | ||
75 | + }, | ||
76 | + () => { | ||
77 | + console.error('Failed to load default widget types!'); | ||
78 | + initSubject.error('Failed to load default widget types!'); | ||
79 | + } | ||
80 | + ); | ||
81 | + return this.init$; | ||
82 | + } | ||
83 | + } | ||
84 | + | ||
85 | + public getWidgetInfo(bundleAlias: string, widgetTypeAlias: string, isSystem: boolean): Observable<WidgetInfo> { | ||
86 | + return this.init().pipe( | ||
87 | + mergeMap(() => this.getWidgetInfoInternal(bundleAlias, widgetTypeAlias, isSystem)) | ||
88 | + ); | ||
89 | + } | ||
90 | + | ||
91 | + private getWidgetInfoInternal(bundleAlias: string, widgetTypeAlias: string, isSystem: boolean): Observable<WidgetInfo> { | ||
92 | + const widgetInfoSubject = new ReplaySubject<WidgetInfo>(); | ||
93 | + const widgetInfo = this.getWidgetInfoFromCache(bundleAlias, widgetTypeAlias, isSystem); | ||
94 | + if (widgetInfo) { | ||
95 | + widgetInfoSubject.next(widgetInfo); | ||
96 | + widgetInfoSubject.complete(); | ||
97 | + } else { | ||
98 | + if (this.utils.widgetEditMode) { | ||
99 | + // TODO: | ||
100 | + } else { | ||
101 | + const key = this.createWidgetInfoCacheKey(bundleAlias, widgetTypeAlias, isSystem); | ||
102 | + let fetchQueue = this.widgetsInfoFetchQueue.get(key); | ||
103 | + if (fetchQueue) { | ||
104 | + fetchQueue.push(widgetInfoSubject); | ||
105 | + } else { | ||
106 | + fetchQueue = new Array<Subject<WidgetInfo>>(); | ||
107 | + this.widgetsInfoFetchQueue.set(key, fetchQueue); | ||
108 | + this.widgetService.getWidgetType(bundleAlias, widgetTypeAlias, isSystem).subscribe( | ||
109 | + (widgetType) => { | ||
110 | + this.loadWidget(widgetType, bundleAlias, isSystem, widgetInfoSubject); | ||
111 | + }, | ||
112 | + () => { | ||
113 | + widgetInfoSubject.next(this.missingWidgetType); | ||
114 | + widgetInfoSubject.complete(); | ||
115 | + this.resolveWidgetsInfoFetchQueue(key, this.missingWidgetType); | ||
116 | + } | ||
117 | + ); | ||
118 | + } | ||
119 | + } | ||
120 | + } | ||
121 | + return widgetInfoSubject.asObservable(); | ||
122 | + } | ||
123 | + | ||
124 | + private loadWidget(widgetType: WidgetType, bundleAlias: string, isSystem: boolean, widgetInfoSubject: Subject<WidgetInfo>) { | ||
125 | + const widgetInfo = toWidgetInfo(widgetType); | ||
126 | + const key = this.createWidgetInfoCacheKey(bundleAlias, widgetInfo.alias, isSystem); | ||
127 | + let widgetControllerDescriptor: WidgetControllerDescriptor = null; | ||
128 | + try { | ||
129 | + widgetControllerDescriptor = this.createWidgetControllerDescriptor(widgetInfo, key); | ||
130 | + } catch (e) { | ||
131 | + const details = this.utils.parseException(e); | ||
132 | + const errorMessage = `Failed to compile widget script. \n Error: ${details.message}`; | ||
133 | + this.processWidgetLoadError([errorMessage], key, widgetInfoSubject); | ||
134 | + } | ||
135 | + if (widgetControllerDescriptor) { | ||
136 | + const widgetNamespace = `widget-type-${(isSystem ? 'sys-' : '')}${bundleAlias}-${widgetInfo.alias}`; | ||
137 | + this.loadWidgetResources(widgetInfo, widgetNamespace).subscribe( | ||
138 | + () => { | ||
139 | + if (widgetControllerDescriptor.settingsSchema) { | ||
140 | + widgetInfo.typeSettingsSchema = widgetControllerDescriptor.settingsSchema; | ||
141 | + } | ||
142 | + if (widgetControllerDescriptor.dataKeySettingsSchema) { | ||
143 | + widgetInfo.typeDataKeySettingsSchema = widgetControllerDescriptor.dataKeySettingsSchema; | ||
144 | + } | ||
145 | + widgetInfo.typeParameters = widgetControllerDescriptor.typeParameters; | ||
146 | + widgetInfo.actionSources = widgetControllerDescriptor.actionSources; | ||
147 | + widgetInfo.widgetTypeFunction = widgetControllerDescriptor.widgetTypeFunction; | ||
148 | + this.putWidgetInfoToCache(widgetInfo, bundleAlias, widgetInfo.alias, isSystem); | ||
149 | + if (widgetInfoSubject) { | ||
150 | + widgetInfoSubject.next(widgetInfo); | ||
151 | + widgetInfoSubject.complete(); | ||
152 | + } | ||
153 | + this.resolveWidgetsInfoFetchQueue(key, widgetInfo); | ||
154 | + }, | ||
155 | + (errorMessages: string[]) => { | ||
156 | + this.processWidgetLoadError(errorMessages, key, widgetInfoSubject); | ||
157 | + } | ||
158 | + ); | ||
159 | + } | ||
160 | + } | ||
161 | + | ||
162 | + private loadWidgetResources(widgetInfo: WidgetInfo, widgetNamespace: string): Observable<any> { | ||
163 | + this.cssParser.cssPreviewNamespace = widgetNamespace; | ||
164 | + this.cssParser.createStyleElement(widgetNamespace, widgetInfo.templateCss); | ||
165 | + const resourceTasks: Observable<string>[] = []; | ||
166 | + if (widgetInfo.resources.length > 0) { | ||
167 | + widgetInfo.resources.forEach((resource) => { | ||
168 | + resourceTasks.push( | ||
169 | + this.resources.loadResource(resource.url).pipe( | ||
170 | + catchError(e => of(`Failed to load widget resource: '${resource.url}'`)) | ||
171 | + ) | ||
172 | + ); | ||
173 | + }); | ||
174 | + } | ||
175 | + resourceTasks.push( | ||
176 | + this.dynamicComponentFactoryService.createDynamicComponentFactory( | ||
177 | + class DynamicWidgetComponentInstance extends DynamicWidgetComponent {}, | ||
178 | + widgetInfo.templateHtml, | ||
179 | + [SharedModule, WidgetComponentsModule] | ||
180 | + ).pipe( | ||
181 | + map((factory) => { | ||
182 | + widgetInfo.componentFactory = factory; | ||
183 | + return null; | ||
184 | + }), | ||
185 | + catchError(e => { | ||
186 | + const details = this.utils.parseException(e); | ||
187 | + const errorMessage = `Failed to compile widget html. \n Error: ${details.message}`; | ||
188 | + return of(errorMessage); | ||
189 | + }) | ||
190 | + ) | ||
191 | + ); | ||
192 | + return forkJoin(resourceTasks).pipe( | ||
193 | + switchMap(msgs => { | ||
194 | + let errors: string[]; | ||
195 | + if (msgs && msgs.length) { | ||
196 | + errors = msgs.filter(msg => msg && msg.length > 0); | ||
197 | + } | ||
198 | + if (errors && errors.length) { | ||
199 | + return throwError(errors); | ||
200 | + } else { | ||
201 | + return of(null); | ||
202 | + } | ||
203 | + } | ||
204 | + )); | ||
205 | + } | ||
206 | + | ||
207 | + private createWidgetControllerDescriptor(widgetInfo: WidgetInfo, name: string): WidgetControllerDescriptor { | ||
208 | + let widgetTypeFunctionBody = `return function ${name} (ctx) {\n` + | ||
209 | + ' var self = this;\n' + | ||
210 | + ' self.ctx = ctx;\n\n'; /*+ | ||
211 | + | ||
212 | + ' self.onInit = function() {\n\n' + | ||
213 | + | ||
214 | + ' }\n\n' + | ||
215 | + | ||
216 | + ' self.onDataUpdated = function() {\n\n' + | ||
217 | + | ||
218 | + ' }\n\n' + | ||
219 | + | ||
220 | + ' self.useCustomDatasources = function() {\n\n' + | ||
221 | + | ||
222 | + ' }\n\n' + | ||
223 | + | ||
224 | + ' self.typeParameters = function() {\n\n' + | ||
225 | + return { | ||
226 | + useCustomDatasources: false, | ||
227 | + maxDatasources: -1, //unlimited | ||
228 | + maxDataKeys: -1, //unlimited | ||
229 | + dataKeysOptional: false, | ||
230 | + stateData: false | ||
231 | + }; | ||
232 | + ' }\n\n' + | ||
233 | + | ||
234 | + ' self.actionSources = function() {\n\n' + | ||
235 | + return { | ||
236 | + 'headerButton': { | ||
237 | + name: 'Header button', | ||
238 | + multiple: true | ||
239 | + } | ||
240 | + }; | ||
241 | + }\n\n' + | ||
242 | + ' self.onResize = function() {\n\n' + | ||
243 | + | ||
244 | + ' }\n\n' + | ||
245 | + | ||
246 | + ' self.onEditModeChanged = function() {\n\n' + | ||
247 | + | ||
248 | + ' }\n\n' + | ||
249 | + | ||
250 | + ' self.onMobileModeChanged = function() {\n\n' + | ||
251 | + | ||
252 | + ' }\n\n' + | ||
253 | + | ||
254 | + ' self.getSettingsSchema = function() {\n\n' + | ||
255 | + | ||
256 | + ' }\n\n' + | ||
257 | + | ||
258 | + ' self.getDataKeySettingsSchema = function() {\n\n' + | ||
259 | + | ||
260 | + ' }\n\n' + | ||
261 | + | ||
262 | + ' self.onDestroy = function() {\n\n' + | ||
263 | + | ||
264 | + ' }\n\n' + | ||
265 | + '}';*/ | ||
266 | + | ||
267 | + widgetTypeFunctionBody += widgetInfo.controllerScript; | ||
268 | + widgetTypeFunctionBody += '\n};\n'; | ||
269 | + | ||
270 | + try { | ||
271 | + | ||
272 | + const widgetTypeFunction = new Function(widgetTypeFunctionBody); | ||
273 | + const widgetType = widgetTypeFunction.apply(this); | ||
274 | + const widgetTypeInstance: WidgetTypeInstance = new widgetType(); | ||
275 | + const result: WidgetControllerDescriptor = { | ||
276 | + widgetTypeFunction: widgetType | ||
277 | + }; | ||
278 | + if (isFunction(widgetTypeInstance.getSettingsSchema)) { | ||
279 | + result.settingsSchema = widgetTypeInstance.getSettingsSchema(); | ||
280 | + } | ||
281 | + if (isFunction(widgetTypeInstance.getDataKeySettingsSchema)) { | ||
282 | + result.dataKeySettingsSchema = widgetTypeInstance.getDataKeySettingsSchema(); | ||
283 | + } | ||
284 | + if (isFunction(widgetTypeInstance.typeParameters)) { | ||
285 | + result.typeParameters = widgetTypeInstance.typeParameters(); | ||
286 | + } else { | ||
287 | + result.typeParameters = {}; | ||
288 | + } | ||
289 | + if (isFunction(widgetTypeInstance.useCustomDatasources)) { | ||
290 | + result.typeParameters.useCustomDatasources = widgetTypeInstance.useCustomDatasources(); | ||
291 | + } else { | ||
292 | + result.typeParameters.useCustomDatasources = false; | ||
293 | + } | ||
294 | + if (isUndefined(result.typeParameters.maxDatasources)) { | ||
295 | + result.typeParameters.maxDatasources = -1; | ||
296 | + } | ||
297 | + if (isUndefined(result.typeParameters.maxDataKeys)) { | ||
298 | + result.typeParameters.maxDataKeys = -1; | ||
299 | + } | ||
300 | + if (isUndefined(result.typeParameters.dataKeysOptional)) { | ||
301 | + result.typeParameters.dataKeysOptional = false; | ||
302 | + } | ||
303 | + if (isUndefined(result.typeParameters.stateData)) { | ||
304 | + result.typeParameters.stateData = false; | ||
305 | + } | ||
306 | + if (isFunction(widgetTypeInstance.actionSources)) { | ||
307 | + result.actionSources = widgetTypeInstance.actionSources(); | ||
308 | + } else { | ||
309 | + result.actionSources = {}; | ||
310 | + } | ||
311 | + for (const actionSourceId of Object.keys(widgetActionSources)) { | ||
312 | + result.actionSources[actionSourceId] = {...widgetActionSources[actionSourceId]}; | ||
313 | + result.actionSources[actionSourceId].name = this.translate.instant(result.actionSources[actionSourceId].name); | ||
314 | + } | ||
315 | + return result; | ||
316 | + } catch (e) { | ||
317 | + this.utils.processWidgetException(e); | ||
318 | + throw e; | ||
319 | + } | ||
320 | + } | ||
321 | + | ||
322 | + private processWidgetLoadError(errorMessages: string[], cacheKey: string, widgetInfoSubject: Subject<WidgetInfo>) { | ||
323 | + if (widgetInfoSubject) { | ||
324 | + widgetInfoSubject.error({ | ||
325 | + widgetInfo: this.errorWidgetType, | ||
326 | + errorMessages | ||
327 | + }); | ||
328 | + } | ||
329 | + this.resolveWidgetsInfoFetchQueue(cacheKey, this.errorWidgetType, errorMessages); | ||
330 | + } | ||
331 | + | ||
332 | + private resolveWidgetsInfoFetchQueue(key: string, widgetInfo: WidgetInfo, errorMessages?: string[]) { | ||
333 | + const fetchQueue = this.widgetsInfoFetchQueue.get(key); | ||
334 | + if (fetchQueue) { | ||
335 | + fetchQueue.forEach(subject => { | ||
336 | + if (!errorMessages) { | ||
337 | + subject.next(widgetInfo); | ||
338 | + subject.complete(); | ||
339 | + } else { | ||
340 | + subject.error({ | ||
341 | + widgetInfo, | ||
342 | + errorMessages | ||
343 | + }); | ||
344 | + } | ||
345 | + }); | ||
346 | + this.widgetsInfoFetchQueue.delete(key); | ||
347 | + } | ||
348 | + } | ||
349 | + | ||
350 | + // Cache functions | ||
351 | + | ||
352 | + private createWidgetInfoCacheKey(bundleAlias: string, widgetTypeAlias: string, isSystem: boolean): string { | ||
353 | + return `${isSystem ? 'sys_' : ''}${bundleAlias}_${widgetTypeAlias}`; | ||
354 | + } | ||
355 | + | ||
356 | + private getWidgetInfoFromCache(bundleAlias: string, widgetTypeAlias: string, isSystem: boolean): WidgetInfo | undefined { | ||
357 | + const key = this.createWidgetInfoCacheKey(bundleAlias, widgetTypeAlias, isSystem); | ||
358 | + return this.widgetsInfoInMemoryCache.get(key); | ||
359 | + } | ||
360 | + | ||
361 | + private putWidgetInfoToCache(widgetInfo: WidgetInfo, bundleAlias: string, widgetTypeAlias: string, isSystem: boolean) { | ||
362 | + const key = this.createWidgetInfoCacheKey(bundleAlias, widgetTypeAlias, isSystem); | ||
363 | + this.widgetsInfoInMemoryCache.set(key, widgetInfo); | ||
364 | + } | ||
365 | + | ||
366 | +} |
@@ -25,14 +25,12 @@ import { LegendComponent } from '@home/components/widget/legend.component'; | @@ -25,14 +25,12 @@ import { LegendComponent } from '@home/components/widget/legend.component'; | ||
25 | ], | 25 | ], |
26 | declarations: | 26 | declarations: |
27 | [ | 27 | [ |
28 | - LegendComponent | ||
29 | ], | 28 | ], |
30 | imports: [ | 29 | imports: [ |
31 | CommonModule, | 30 | CommonModule, |
32 | SharedModule | 31 | SharedModule |
33 | ], | 32 | ], |
34 | exports: [ | 33 | exports: [ |
35 | - LegendComponent | ||
36 | ] | 34 | ] |
37 | }) | 35 | }) |
38 | export class WidgetComponentsModule { } | 36 | export class WidgetComponentsModule { } |
@@ -15,4 +15,16 @@ | @@ -15,4 +15,16 @@ | ||
15 | limitations under the License. | 15 | limitations under the License. |
16 | 16 | ||
17 | --> | 17 | --> |
18 | -<ng-container #widgetContent></ng-container> | 18 | +<div class="tb-absolute-fill tb-widget-error" *ngIf="widgetErrorData"> |
19 | + <span>Widget Error: {{ widgetErrorData.name + ": " + widgetErrorData.message}}</span> | ||
20 | +</div> | ||
21 | +<div class="tb-absolute-fill tb-widget-loading" [fxShow]="loadingData" fxLayout="column" fxLayoutAlign="center center"> | ||
22 | + <mat-spinner color="accent" md-mode="indeterminate" diameter="40"></mat-spinner> | ||
23 | +</div> | ||
24 | +<div class="tb-absolute-fill" [fxLayout]="legendContainerLayoutType"> | ||
25 | + <tb-legend *ngIf="displayLegend && isLegendFirst" [ngStyle]="legendStyle" [legendConfig]="legendConfig" [legendData]="legendData"></tb-legend> | ||
26 | + <div fxFlex id="widget-container"> | ||
27 | + <ng-container #widgetContent></ng-container> | ||
28 | + </div> | ||
29 | + <tb-legend *ngIf="displayLegend && !isLegendFirst" [ngStyle]="legendStyle" [legendConfig]="legendConfig" [legendData]="legendData"></tb-legend> | ||
30 | +</div> |
@@ -17,7 +17,6 @@ | @@ -17,7 +17,6 @@ | ||
17 | import { | 17 | import { |
18 | AfterViewInit, | 18 | AfterViewInit, |
19 | Component, | 19 | Component, |
20 | - ComponentFactory, | ||
21 | ComponentFactoryResolver, | 20 | ComponentFactoryResolver, |
22 | ComponentRef, | 21 | ComponentRef, |
23 | ElementRef, | 22 | ElementRef, |
@@ -38,34 +37,37 @@ import { | @@ -38,34 +37,37 @@ import { | ||
38 | LegendPosition, | 37 | LegendPosition, |
39 | Widget, | 38 | Widget, |
40 | WidgetActionDescriptor, | 39 | WidgetActionDescriptor, |
40 | + widgetActionSources, | ||
41 | WidgetActionType, | 41 | WidgetActionType, |
42 | - WidgetInfo, WidgetResource, | ||
43 | - widgetType, | ||
44 | - WidgetTypeInstance, | ||
45 | - widgetActionSources | 42 | + WidgetResource, |
43 | + widgetType | ||
46 | } from '@shared/models/widget.models'; | 44 | } from '@shared/models/widget.models'; |
47 | import { PageComponent } from '@shared/components/page.component'; | 45 | import { PageComponent } from '@shared/components/page.component'; |
48 | import { Store } from '@ngrx/store'; | 46 | import { Store } from '@ngrx/store'; |
49 | import { AppState } from '@core/core.state'; | 47 | import { AppState } from '@core/core.state'; |
50 | import { WidgetService } from '@core/http/widget.service'; | 48 | import { WidgetService } from '@core/http/widget.service'; |
51 | import { UtilsService } from '@core/services/utils.service'; | 49 | import { UtilsService } from '@core/services/utils.service'; |
52 | -import { DynamicWidgetComponent } from '@home/components/widget/dynamic-widget.component'; | ||
53 | -import { forkJoin, Observable, of, ReplaySubject, throwError } from 'rxjs'; | ||
54 | -import { DynamicWidgetComponentFactoryService } from '@home/components/widget/dynamic-widget-component-factory.service'; | 50 | +import { forkJoin, Observable, of, throwError } from 'rxjs'; |
55 | import { isDefined, objToBase64 } from '@core/utils'; | 51 | import { isDefined, objToBase64 } from '@core/utils'; |
56 | import * as $ from 'jquery'; | 52 | import * as $ from 'jquery'; |
57 | -import { WidgetContext, WidgetHeaderAction } from '@home/models/widget-component.models'; | 53 | +import { |
54 | + IDynamicWidgetComponent, | ||
55 | + WidgetContext, | ||
56 | + WidgetHeaderAction, | ||
57 | + WidgetInfo, | ||
58 | + WidgetTypeInstance | ||
59 | +} from '@home/models/widget-component.models'; | ||
58 | import { | 60 | import { |
59 | EntityInfo, | 61 | EntityInfo, |
60 | IWidgetSubscription, | 62 | IWidgetSubscription, |
61 | - SubscriptionInfo, | ||
62 | - WidgetSubscriptionOptions, | ||
63 | StateObject, | 63 | StateObject, |
64 | StateParams, | 64 | StateParams, |
65 | - WidgetSubscriptionContext | 65 | + SubscriptionInfo, |
66 | + WidgetSubscriptionContext, | ||
67 | + WidgetSubscriptionOptions | ||
66 | } from '@core/api/widget-api.models'; | 68 | } from '@core/api/widget-api.models'; |
67 | import { EntityId } from '@shared/models/id/entity-id'; | 69 | import { EntityId } from '@shared/models/id/entity-id'; |
68 | -import { ActivatedRoute, Router, UrlSegment } from '@angular/router'; | 70 | +import { ActivatedRoute, Router } from '@angular/router'; |
69 | import cssjs from '@core/css/css'; | 71 | import cssjs from '@core/css/css'; |
70 | import { ResourcesService } from '@core/services/resources.service'; | 72 | import { ResourcesService } from '@core/services/resources.service'; |
71 | import { catchError, switchMap } from 'rxjs/operators'; | 73 | import { catchError, switchMap } from 'rxjs/operators'; |
@@ -74,6 +76,7 @@ import { TimeService } from '@core/services/time.service'; | @@ -74,6 +76,7 @@ import { TimeService } from '@core/services/time.service'; | ||
74 | import { DeviceService } from '@app/core/http/device.service'; | 76 | import { DeviceService } from '@app/core/http/device.service'; |
75 | import { AlarmService } from '@app/core/http/alarm.service'; | 77 | import { AlarmService } from '@app/core/http/alarm.service'; |
76 | import { ExceptionData } from '@shared/models/error.models'; | 78 | import { ExceptionData } from '@shared/models/error.models'; |
79 | +import { WidgetComponentService } from './widget-component.service'; | ||
77 | 80 | ||
78 | @Component({ | 81 | @Component({ |
79 | selector: 'tb-widget', | 82 | selector: 'tb-widget', |
@@ -99,14 +102,22 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI | @@ -99,14 +102,22 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI | ||
99 | 102 | ||
100 | widget: Widget; | 103 | widget: Widget; |
101 | widgetInfo: WidgetInfo; | 104 | widgetInfo: WidgetInfo; |
105 | + errorMessages: string[]; | ||
102 | widgetContext: WidgetContext; | 106 | widgetContext: WidgetContext; |
103 | widgetType: any; | 107 | widgetType: any; |
104 | widgetTypeInstance: WidgetTypeInstance; | 108 | widgetTypeInstance: WidgetTypeInstance; |
105 | widgetErrorData: ExceptionData; | 109 | widgetErrorData: ExceptionData; |
110 | + loadingData: boolean; | ||
106 | 111 | ||
107 | - dynamicWidgetComponentFactory: ComponentFactory<DynamicWidgetComponent>; | ||
108 | - dynamicWidgetComponentRef: ComponentRef<DynamicWidgetComponent>; | ||
109 | - dynamicWidgetComponent: DynamicWidgetComponent; | 112 | + displayLegend: boolean; |
113 | + legendConfig: LegendConfig; | ||
114 | + legendData: LegendData; | ||
115 | + isLegendFirst: boolean; | ||
116 | + legendContainerLayoutType: string; | ||
117 | + legendStyle: {[klass: string]: any}; | ||
118 | + | ||
119 | + dynamicWidgetComponentRef: ComponentRef<IDynamicWidgetComponent>; | ||
120 | + dynamicWidgetComponent: IDynamicWidgetComponent; | ||
110 | 121 | ||
111 | subscriptionContext: WidgetSubscriptionContext; | 122 | subscriptionContext: WidgetSubscriptionContext; |
112 | 123 | ||
@@ -120,7 +131,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI | @@ -120,7 +131,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI | ||
120 | constructor(protected store: Store<AppState>, | 131 | constructor(protected store: Store<AppState>, |
121 | private route: ActivatedRoute, | 132 | private route: ActivatedRoute, |
122 | private router: Router, | 133 | private router: Router, |
123 | - private dynamicWidgetComponentFactoryService: DynamicWidgetComponentFactoryService, | 134 | + private widgetComponentService: WidgetComponentService, |
124 | private componentFactoryResolver: ComponentFactoryResolver, | 135 | private componentFactoryResolver: ComponentFactoryResolver, |
125 | private elementRef: ElementRef, | 136 | private elementRef: ElementRef, |
126 | private injector: Injector, | 137 | private injector: Injector, |
@@ -134,8 +145,67 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI | @@ -134,8 +145,67 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI | ||
134 | } | 145 | } |
135 | 146 | ||
136 | ngOnInit(): void { | 147 | ngOnInit(): void { |
148 | + | ||
149 | + this.loadingData = true; | ||
150 | + | ||
137 | this.widget = this.dashboardWidget.widget; | 151 | this.widget = this.dashboardWidget.widget; |
138 | 152 | ||
153 | + this.displayLegend = isDefined(this.widget.config.showLegend) ? this.widget.config.showLegend | ||
154 | + : this.widget.type === widgetType.timeseries; | ||
155 | + | ||
156 | + this.legendContainerLayoutType = 'column'; | ||
157 | + | ||
158 | + if (this.displayLegend) { | ||
159 | + this.legendConfig = this.widget.config.legendConfig || | ||
160 | + { | ||
161 | + position: LegendPosition.bottom, | ||
162 | + showMin: false, | ||
163 | + showMax: false, | ||
164 | + showAvg: this.widget.type === widgetType.timeseries, | ||
165 | + showTotal: false | ||
166 | + }; | ||
167 | + this.legendData = { | ||
168 | + keys: [], | ||
169 | + data: [] | ||
170 | + }; | ||
171 | + if (this.legendConfig.position === LegendPosition.top || | ||
172 | + this.legendConfig.position === LegendPosition.bottom) { | ||
173 | + this.legendContainerLayoutType = 'column'; | ||
174 | + } else { | ||
175 | + this.legendContainerLayoutType = 'row'; | ||
176 | + } | ||
177 | + switch (this.legendConfig.position) { | ||
178 | + case LegendPosition.top: | ||
179 | + this.legendStyle = { | ||
180 | + paddingBottom: '8px', | ||
181 | + maxHeight: '50%', | ||
182 | + overflowY: 'auto' | ||
183 | + }; | ||
184 | + break; | ||
185 | + case LegendPosition.bottom: | ||
186 | + this.legendStyle = { | ||
187 | + paddingTop: '8px', | ||
188 | + maxHeight: '50%', | ||
189 | + overflowY: 'auto' | ||
190 | + }; | ||
191 | + break; | ||
192 | + case LegendPosition.left: | ||
193 | + this.legendStyle = { | ||
194 | + paddingRight: '0px', | ||
195 | + maxWidth: '50%', | ||
196 | + overflowY: 'auto' | ||
197 | + }; | ||
198 | + break; | ||
199 | + case LegendPosition.right: | ||
200 | + this.legendStyle = { | ||
201 | + paddingLeft: '0px', | ||
202 | + maxWidth: '50%', | ||
203 | + overflowY: 'auto' | ||
204 | + }; | ||
205 | + break; | ||
206 | + } | ||
207 | + } | ||
208 | + | ||
139 | const actionDescriptorsBySourceId: {[actionSourceId: string]: Array<WidgetActionDescriptor>} = {}; | 209 | const actionDescriptorsBySourceId: {[actionSourceId: string]: Array<WidgetActionDescriptor>} = {}; |
140 | if (this.widget.config.actions) { | 210 | if (this.widget.config.actions) { |
141 | for (const actionSourceId of Object.keys(this.widget.config.actions)) { | 211 | for (const actionSourceId of Object.keys(this.widget.config.actions)) { |
@@ -244,10 +314,15 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI | @@ -244,10 +314,15 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI | ||
244 | aliasController: this.dashboard.aliasController | 314 | aliasController: this.dashboard.aliasController |
245 | }; | 315 | }; |
246 | 316 | ||
247 | - this.widgetService.getWidgetInfo(this.widget.bundleAlias, this.widget.typeAlias, this.widget.isSystemType).subscribe( | 317 | + this.widgetComponentService.getWidgetInfo(this.widget.bundleAlias, this.widget.typeAlias, this.widget.isSystemType).subscribe( |
248 | (widgetInfo) => { | 318 | (widgetInfo) => { |
249 | this.widgetInfo = widgetInfo; | 319 | this.widgetInfo = widgetInfo; |
250 | this.loadFromWidgetInfo(); | 320 | this.loadFromWidgetInfo(); |
321 | + }, | ||
322 | + (errorData) => { | ||
323 | + this.widgetInfo = errorData.widgetInfo; | ||
324 | + this.errorMessages = errorData.errorMessages; | ||
325 | + this.loadFromWidgetInfo(); | ||
251 | } | 326 | } |
252 | ); | 327 | ); |
253 | 328 | ||
@@ -363,14 +438,9 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI | @@ -363,14 +438,9 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI | ||
363 | } | 438 | } |
364 | 439 | ||
365 | private initialize() { | 440 | private initialize() { |
366 | - this.configureDynamicWidgetComponent().subscribe( | ||
367 | - () => { | ||
368 | - this.dynamicWidgetComponent.loadingData = false; | ||
369 | - }, | ||
370 | - (error) => { | ||
371 | - // TODO: | ||
372 | - } | ||
373 | - ); | 441 | + this.configureDynamicWidgetComponent(); |
442 | + // TODO: | ||
443 | + this.loadingData = false; | ||
374 | } | 444 | } |
375 | 445 | ||
376 | private destroyDynamicWidgetComponent() { | 446 | private destroyDynamicWidgetComponent() { |
@@ -381,127 +451,37 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI | @@ -381,127 +451,37 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI | ||
381 | if (this.dynamicWidgetComponentRef) { | 451 | if (this.dynamicWidgetComponentRef) { |
382 | this.dynamicWidgetComponentRef.destroy(); | 452 | this.dynamicWidgetComponentRef.destroy(); |
383 | } | 453 | } |
384 | - if (this.dynamicWidgetComponentFactory) { | ||
385 | - this.dynamicWidgetComponentFactoryService.destroyDynamicWidgetComponentFactory(this.dynamicWidgetComponentFactory); | ||
386 | - } | ||
387 | } | 454 | } |
388 | 455 | ||
389 | private handleWidgetException(e) { | 456 | private handleWidgetException(e) { |
390 | console.error(e); | 457 | console.error(e); |
391 | this.widgetErrorData = this.utils.processWidgetException(e); | 458 | this.widgetErrorData = this.utils.processWidgetException(e); |
392 | - if (this.dynamicWidgetComponent) { | ||
393 | - this.dynamicWidgetComponent.widgetErrorData = this.widgetErrorData; | ||
394 | - } | ||
395 | } | 459 | } |
396 | 460 | ||
397 | - private configureDynamicWidgetComponent(): Observable<any> { | 461 | + private configureDynamicWidgetComponent() { |
462 | + this.widgetContentContainer.clear(); | ||
463 | + this.dynamicWidgetComponentRef = this.widgetContentContainer.createComponent(this.widgetInfo.componentFactory); | ||
464 | + this.dynamicWidgetComponent = this.dynamicWidgetComponentRef.instance; | ||
398 | 465 | ||
399 | - const dynamicWidgetComponentSubject = new ReplaySubject(); | 466 | + this.dynamicWidgetComponent.widgetContext = this.widgetContext; |
467 | + this.dynamicWidgetComponent.errorMessages = this.errorMessages; | ||
400 | 468 | ||
401 | - let html = '<div class="tb-absolute-fill tb-widget-error" *ngIf="widgetErrorData">' + | ||
402 | - '<span>Widget Error: {{ widgetErrorData.name + ": " + widgetErrorData.message}}</span>' + | ||
403 | - '</div>' + | ||
404 | - '<div class="tb-absolute-fill tb-widget-loading" [fxShow]="loadingData" fxLayout="column" fxLayoutAlign="center center">' + | ||
405 | - '<mat-spinner color="accent" md-mode="indeterminate" diameter="40"></mat-spinner>' + | ||
406 | - '</div>'; | 469 | + this.widgetContext.$scope = this.dynamicWidgetComponent; |
407 | 470 | ||
408 | - let containerHtml = `<div id="container">${this.widgetInfo.templateHtml}</div>`; | 471 | + const containerElement = $(this.elementRef.nativeElement.querySelector('#widget-container')); |
409 | 472 | ||
410 | - const displayLegend = isDefined(this.widget.config.showLegend) ? this.widget.config.showLegend | ||
411 | - : this.widget.type === widgetType.timeseries; | 473 | + this.widgetContext.$container = $('> ng-component', containerElement); |
474 | + this.widgetContext.$container.css('display', 'block'); | ||
475 | + this.widgetContext.$container.attr('id', 'container'); | ||
476 | + this.widgetContext.$containerParent = $(containerElement); | ||
412 | 477 | ||
413 | - let legendConfig: LegendConfig; | ||
414 | - let legendData: LegendData; | ||
415 | - if (displayLegend) { | ||
416 | - legendConfig = this.widget.config.legendConfig || | ||
417 | - { | ||
418 | - position: LegendPosition.bottom, | ||
419 | - showMin: false, | ||
420 | - showMax: false, | ||
421 | - showAvg: this.widget.type === widgetType.timeseries, | ||
422 | - showTotal: false | ||
423 | - }; | ||
424 | - legendData = { | ||
425 | - keys: [], | ||
426 | - data: [] | ||
427 | - }; | ||
428 | - let layoutType; | ||
429 | - if (legendConfig.position === LegendPosition.top || | ||
430 | - legendConfig.position === LegendPosition.bottom) { | ||
431 | - layoutType = 'column'; | ||
432 | - } else { | ||
433 | - layoutType = 'row'; | 478 | + if (this.widgetSizeDetected) { |
479 | + this.widgetContext.$container.css('height', this.widgetContext.height + 'px'); | ||
480 | + this.widgetContext.$container.css('width', this.widgetContext.width + 'px'); | ||
434 | } | 481 | } |
435 | - let legendStyle; | ||
436 | - switch (legendConfig.position) { | ||
437 | - case LegendPosition.top: | ||
438 | - legendStyle = 'padding-bottom: 8px; max-height: 50%; overflow-y: auto;'; | ||
439 | - break; | ||
440 | - case LegendPosition.bottom: | ||
441 | - legendStyle = 'padding-top: 8px; max-height: 50%; overflow-y: auto;'; | ||
442 | - break; | ||
443 | - case LegendPosition.left: | ||
444 | - legendStyle = 'padding-right: 0px; max-width: 50%; overflow-y: auto;'; | ||
445 | - break; | ||
446 | - case LegendPosition.right: | ||
447 | - legendStyle = 'padding-left: 0px; max-width: 50%; overflow-y: auto;'; | ||
448 | - break; | ||
449 | - } | ||
450 | - | ||
451 | - const legendHtml = `<tb-legend style="${legendStyle}" [legendConfig]="legendConfig" [legendData]="legendData"></tb-legend>`; | ||
452 | - containerHtml = `<div fxFlex id="widget-container">${containerHtml}</div>`; | ||
453 | - html += `<div class="tb-absolute-fill" fxLayout="${layoutType}">`; | ||
454 | - if (legendConfig.position === LegendPosition.top || | ||
455 | - legendConfig.position === LegendPosition.left) { | ||
456 | - html += legendHtml; | ||
457 | - html += containerHtml; | ||
458 | - } else { | ||
459 | - html += containerHtml; | ||
460 | - html += legendHtml; | ||
461 | - } | ||
462 | - html += '</div>'; | ||
463 | - } else { | ||
464 | - html += containerHtml; | ||
465 | - } | ||
466 | - | ||
467 | - this.dynamicWidgetComponentFactoryService.createDynamicWidgetComponentFactory(html).subscribe( | ||
468 | - (componentFactory) => { | ||
469 | - this.dynamicWidgetComponentFactory = componentFactory; | ||
470 | - this.widgetContentContainer.clear(); | ||
471 | - this.dynamicWidgetComponentRef = this.widgetContentContainer.createComponent(this.dynamicWidgetComponentFactory); | ||
472 | - this.dynamicWidgetComponent = this.dynamicWidgetComponentRef.instance; | ||
473 | - | ||
474 | - this.dynamicWidgetComponent.loadingData = true; | ||
475 | - this.dynamicWidgetComponent.widgetContext = this.widgetContext; | ||
476 | - this.dynamicWidgetComponent.widgetErrorData = this.widgetErrorData; | ||
477 | - this.dynamicWidgetComponent.displayLegend = displayLegend; | ||
478 | - this.dynamicWidgetComponent.legendConfig = legendConfig; | ||
479 | - this.dynamicWidgetComponent.legendData = legendData; | ||
480 | - | ||
481 | - this.widgetContext.$scope = this.dynamicWidgetComponent; | ||
482 | 482 | ||
483 | - const containerElement = displayLegend ? $(this.elementRef.nativeElement.querySelector('#widget-container')) | ||
484 | - : $(this.elementRef.nativeElement); | ||
485 | - | ||
486 | - this.widgetContext.$container = $('#container', containerElement); | ||
487 | - this.widgetContext.$containerParent = $(containerElement); | ||
488 | - | ||
489 | - if (this.widgetSizeDetected) { | ||
490 | - this.widgetContext.$container.css('height', this.widgetContext.height + 'px'); | ||
491 | - this.widgetContext.$container.css('width', this.widgetContext.width + 'px'); | ||
492 | - } | ||
493 | - | ||
494 | - // @ts-ignore | ||
495 | - addResizeListener(this.widgetContext.$containerParent[0], this.onResizeListener); | ||
496 | - | ||
497 | - dynamicWidgetComponentSubject.next(); | ||
498 | - dynamicWidgetComponentSubject.complete(); | ||
499 | - }, | ||
500 | - (e) => { | ||
501 | - dynamicWidgetComponentSubject.error(e); | ||
502 | - } | ||
503 | - ); | ||
504 | - return dynamicWidgetComponentSubject.asObservable(); | 483 | + // @ts-ignore |
484 | + addResizeListener(this.widgetContext.$containerParent[0], this.onResizeListener); | ||
505 | } | 485 | } |
506 | 486 | ||
507 | private createSubscription(options: WidgetSubscriptionOptions, subscribe: boolean): Observable<IWidgetSubscription> { | 487 | private createSubscription(options: WidgetSubscriptionOptions, subscribe: boolean): Observable<IWidgetSubscription> { |
@@ -16,23 +16,30 @@ | @@ -16,23 +16,30 @@ | ||
16 | 16 | ||
17 | import { ExceptionData } from '@shared/models/error.models'; | 17 | import { ExceptionData } from '@shared/models/error.models'; |
18 | import { IDashboardComponent } from '@home/models/dashboard-component.models'; | 18 | import { IDashboardComponent } from '@home/models/dashboard-component.models'; |
19 | -import { WidgetActionDescriptor, WidgetConfig, WidgetConfigSettings, widgetType } from '@shared/models/widget.models'; | 19 | +import { |
20 | + WidgetActionDescriptor, | ||
21 | + WidgetActionSource, | ||
22 | + WidgetConfig, | ||
23 | + WidgetConfigSettings, | ||
24 | + WidgetControllerDescriptor, | ||
25 | + WidgetType, | ||
26 | + widgetType, | ||
27 | + WidgetTypeDescriptor, | ||
28 | + WidgetTypeParameters | ||
29 | +} from '@shared/models/widget.models'; | ||
20 | import { Timewindow } from '@shared/models/time/time.models'; | 30 | import { Timewindow } from '@shared/models/time/time.models'; |
21 | import { | 31 | import { |
22 | EntityInfo, | 32 | EntityInfo, |
23 | - IWidgetSubscription, | ||
24 | - SubscriptionInfo, | ||
25 | - WidgetSubscriptionOptions, | ||
26 | - IStateController, | ||
27 | IAliasController, | 33 | IAliasController, |
28 | - TimewindowFunctions, | ||
29 | - WidgetSubscriptionApi, | 34 | + IStateController, |
35 | + IWidgetSubscription, | ||
36 | + IWidgetUtils, | ||
30 | RpcApi, | 37 | RpcApi, |
38 | + TimewindowFunctions, | ||
31 | WidgetActionsApi, | 39 | WidgetActionsApi, |
32 | - IWidgetUtils | 40 | + WidgetSubscriptionApi |
33 | } from '@core/api/widget-api.models'; | 41 | } from '@core/api/widget-api.models'; |
34 | -import { Observable } from 'rxjs'; | ||
35 | -import { EntityId } from '@shared/models/id/entity-id'; | 42 | +import { ComponentFactory } from '@angular/core'; |
36 | 43 | ||
37 | export interface IWidgetAction { | 44 | export interface IWidgetAction { |
38 | name: string; | 45 | name: string; |
@@ -49,13 +56,6 @@ export interface WidgetAction extends IWidgetAction { | @@ -49,13 +56,6 @@ export interface WidgetAction extends IWidgetAction { | ||
49 | show: boolean; | 56 | show: boolean; |
50 | } | 57 | } |
51 | 58 | ||
52 | -export interface IDynamicWidgetComponent { | ||
53 | - widgetContext: WidgetContext; | ||
54 | - widgetErrorData: ExceptionData; | ||
55 | - loadingData: boolean; | ||
56 | - [key: string]: any; | ||
57 | -} | ||
58 | - | ||
59 | export interface WidgetContext { | 59 | export interface WidgetContext { |
60 | inited?: boolean; | 60 | inited?: boolean; |
61 | $container?: any; | 61 | $container?: any; |
@@ -87,3 +87,93 @@ export interface WidgetContext { | @@ -87,3 +87,93 @@ export interface WidgetContext { | ||
87 | customHeaderActions?: Array<WidgetHeaderAction>; | 87 | customHeaderActions?: Array<WidgetHeaderAction>; |
88 | widgetActions?: Array<WidgetAction>; | 88 | widgetActions?: Array<WidgetAction>; |
89 | } | 89 | } |
90 | + | ||
91 | +export interface IDynamicWidgetComponent { | ||
92 | + widgetContext: WidgetContext; | ||
93 | + errorMessages: string[]; | ||
94 | + [key: string]: any; | ||
95 | +} | ||
96 | + | ||
97 | +export interface WidgetInfo extends WidgetTypeDescriptor, WidgetControllerDescriptor { | ||
98 | + widgetName: string; | ||
99 | + alias: string; | ||
100 | + typeSettingsSchema?: string; | ||
101 | + typeDataKeySettingsSchema?: string; | ||
102 | + componentFactory?: ComponentFactory<IDynamicWidgetComponent>; | ||
103 | +} | ||
104 | + | ||
105 | +export const MissingWidgetType: WidgetInfo = { | ||
106 | + type: widgetType.latest, | ||
107 | + widgetName: 'Widget type not found', | ||
108 | + alias: 'undefined', | ||
109 | + sizeX: 8, | ||
110 | + sizeY: 6, | ||
111 | + resources: [], | ||
112 | + templateHtml: '<div class="tb-widget-error-container">' + | ||
113 | + '<div translate class="tb-widget-error-msg">widget.widget-type-not-found</div>' + | ||
114 | + '</div>', | ||
115 | + templateCss: '', | ||
116 | + controllerScript: 'self.onInit = function() {}', | ||
117 | + settingsSchema: '{}\n', | ||
118 | + dataKeySettingsSchema: '{}\n', | ||
119 | + defaultConfig: '{\n' + | ||
120 | + '"title": "Widget type not found",\n' + | ||
121 | + '"datasources": [],\n' + | ||
122 | + '"settings": {}\n' + | ||
123 | + '}\n' | ||
124 | +}; | ||
125 | + | ||
126 | +export const ErrorWidgetType: WidgetInfo = { | ||
127 | + type: widgetType.latest, | ||
128 | + widgetName: 'Error loading widget', | ||
129 | + alias: 'error', | ||
130 | + sizeX: 8, | ||
131 | + sizeY: 6, | ||
132 | + resources: [], | ||
133 | + templateHtml: '<div class="tb-widget-error-container">' + | ||
134 | + '<div translate class="tb-widget-error-msg">widget.widget-type-load-error</div>' + | ||
135 | + '<div *ngFor="let error of errorMessages" class="tb-widget-error-msg">{{ error }}</div>' + | ||
136 | + '</div>', | ||
137 | + templateCss: '', | ||
138 | + controllerScript: 'self.onInit = function() {}', | ||
139 | + settingsSchema: '{}\n', | ||
140 | + dataKeySettingsSchema: '{}\n', | ||
141 | + defaultConfig: '{\n' + | ||
142 | + '"title": "Widget failed to load",\n' + | ||
143 | + '"datasources": [],\n' + | ||
144 | + '"settings": {}\n' + | ||
145 | + '}\n' | ||
146 | +}; | ||
147 | + | ||
148 | +export interface WidgetTypeInstance { | ||
149 | + getSettingsSchema?: () => string; | ||
150 | + getDataKeySettingsSchema?: () => string; | ||
151 | + typeParameters?: () => WidgetTypeParameters; | ||
152 | + useCustomDatasources?: () => boolean; | ||
153 | + actionSources?: () => {[key: string]: WidgetActionSource}; | ||
154 | + | ||
155 | + onInit?: () => void; | ||
156 | + onDataUpdated?: () => void; | ||
157 | + onResize?: () => void; | ||
158 | + onEditModeChanged?: () => void; | ||
159 | + onMobileModeChanged?: () => void; | ||
160 | + onDestroy?: () => void; | ||
161 | +} | ||
162 | + | ||
163 | +export function toWidgetInfo(widgetTypeEntity: WidgetType): WidgetInfo { | ||
164 | + return { | ||
165 | + widgetName: widgetTypeEntity.name, | ||
166 | + alias: widgetTypeEntity.alias, | ||
167 | + type: widgetTypeEntity.descriptor.type, | ||
168 | + sizeX: widgetTypeEntity.descriptor.sizeX, | ||
169 | + sizeY: widgetTypeEntity.descriptor.sizeY, | ||
170 | + resources: widgetTypeEntity.descriptor.resources, | ||
171 | + templateHtml: widgetTypeEntity.descriptor.templateHtml, | ||
172 | + templateCss: widgetTypeEntity.descriptor.templateCss, | ||
173 | + controllerScript: widgetTypeEntity.descriptor.controllerScript, | ||
174 | + settingsSchema: widgetTypeEntity.descriptor.settingsSchema, | ||
175 | + dataKeySettingsSchema: widgetTypeEntity.descriptor.dataKeySettingsSchema, | ||
176 | + defaultConfig: widgetTypeEntity.descriptor.defaultConfig | ||
177 | + }; | ||
178 | +} | ||
179 | + |
@@ -25,13 +25,14 @@ import { ActivatedRoute } from '@angular/router'; | @@ -25,13 +25,14 @@ import { ActivatedRoute } from '@angular/router'; | ||
25 | import { Authority } from '@shared/models/authority.enum'; | 25 | import { Authority } from '@shared/models/authority.enum'; |
26 | import { NULL_UUID } from '@shared/models/id/has-uuid'; | 26 | import { NULL_UUID } from '@shared/models/id/has-uuid'; |
27 | import { Observable } from 'rxjs'; | 27 | import { Observable } from 'rxjs'; |
28 | -import { toWidgetInfo, Widget, widgetType } from '@app/shared/models/widget.models'; | 28 | +import { Widget, widgetType } from '@app/shared/models/widget.models'; |
29 | import { WidgetService } from '@core/http/widget.service'; | 29 | import { WidgetService } from '@core/http/widget.service'; |
30 | import { map, share } from 'rxjs/operators'; | 30 | import { map, share } from 'rxjs/operators'; |
31 | import { DialogService } from '@core/services/dialog.service'; | 31 | import { DialogService } from '@core/services/dialog.service'; |
32 | import { FooterFabButtons } from '@app/shared/components/footer-fab-buttons.component'; | 32 | import { FooterFabButtons } from '@app/shared/components/footer-fab-buttons.component'; |
33 | import { DashboardCallbacks, WidgetsData } from '@home/models/dashboard-component.models'; | 33 | import { DashboardCallbacks, WidgetsData } from '@home/models/dashboard-component.models'; |
34 | import { IAliasController } from '@app/core/api/widget-api.models'; | 34 | import { IAliasController } from '@app/core/api/widget-api.models'; |
35 | +import { toWidgetInfo } from '@home/models/widget-component.models'; | ||
35 | 36 | ||
36 | @Component({ | 37 | @Component({ |
37 | selector: 'tb-widget-library', | 38 | selector: 'tb-widget-library', |
@@ -14,11 +14,9 @@ | @@ -14,11 +14,9 @@ | ||
14 | /// limitations under the License. | 14 | /// limitations under the License. |
15 | /// | 15 | /// |
16 | 16 | ||
17 | -import {BaseData} from '@shared/models/base-data'; | ||
18 | -import {TenantId} from '@shared/models/id/tenant-id'; | ||
19 | -import {WidgetsBundleId} from '@shared/models/id/widgets-bundle-id'; | ||
20 | -import {WidgetTypeId} from '@shared/models/id/widget-type-id'; | ||
21 | -import { AliasEntityType, EntityType, EntityTypeTranslation } from '@shared/models/entity-type.models'; | 17 | +import { BaseData } from '@shared/models/base-data'; |
18 | +import { TenantId } from '@shared/models/id/tenant-id'; | ||
19 | +import { WidgetTypeId } from '@shared/models/id/widget-type-id'; | ||
22 | import { Timewindow } from '@shared/models/time/time.models'; | 20 | import { Timewindow } from '@shared/models/time/time.models'; |
23 | 21 | ||
24 | export enum widgetType { | 22 | export enum widgetType { |
@@ -139,86 +137,6 @@ export interface WidgetType extends BaseData<WidgetTypeId> { | @@ -139,86 +137,6 @@ export interface WidgetType extends BaseData<WidgetTypeId> { | ||
139 | descriptor: WidgetTypeDescriptor; | 137 | descriptor: WidgetTypeDescriptor; |
140 | } | 138 | } |
141 | 139 | ||
142 | -export interface WidgetInfo extends WidgetTypeDescriptor, WidgetControllerDescriptor { | ||
143 | - widgetName: string; | ||
144 | - alias: string; | ||
145 | - typeSettingsSchema?: string; | ||
146 | - typeDataKeySettingsSchema?: string; | ||
147 | -} | ||
148 | - | ||
149 | -export const MissingWidgetType: WidgetInfo = { | ||
150 | - type: widgetType.latest, | ||
151 | - widgetName: 'Widget type not found', | ||
152 | - alias: 'undefined', | ||
153 | - sizeX: 8, | ||
154 | - sizeY: 6, | ||
155 | - resources: [], | ||
156 | - templateHtml: '<div class="tb-widget-error-container">' + | ||
157 | - '<div translate class="tb-widget-error-msg">widget.widget-type-not-found</div>' + | ||
158 | - '</div>', | ||
159 | - templateCss: '', | ||
160 | - controllerScript: 'self.onInit = function() {}', | ||
161 | - settingsSchema: '{}\n', | ||
162 | - dataKeySettingsSchema: '{}\n', | ||
163 | - defaultConfig: '{\n' + | ||
164 | - '"title": "Widget type not found",\n' + | ||
165 | - '"datasources": [],\n' + | ||
166 | - '"settings": {}\n' + | ||
167 | - '}\n' | ||
168 | -}; | ||
169 | - | ||
170 | -export const ErrorWidgetType: WidgetInfo = { | ||
171 | - type: widgetType.latest, | ||
172 | - widgetName: 'Error loading widget', | ||
173 | - alias: 'error', | ||
174 | - sizeX: 8, | ||
175 | - sizeY: 6, | ||
176 | - resources: [], | ||
177 | - templateHtml: '<div class="tb-widget-error-container">' + | ||
178 | - '<div translate class="tb-widget-error-msg">widget.widget-type-load-error</div>', | ||
179 | - templateCss: '', | ||
180 | - controllerScript: 'self.onInit = function() {}', | ||
181 | - settingsSchema: '{}\n', | ||
182 | - dataKeySettingsSchema: '{}\n', | ||
183 | - defaultConfig: '{\n' + | ||
184 | - '"title": "Widget failed to load",\n' + | ||
185 | - '"datasources": [],\n' + | ||
186 | - '"settings": {}\n' + | ||
187 | - '}\n' | ||
188 | -}; | ||
189 | - | ||
190 | -export interface WidgetTypeInstance { | ||
191 | - getSettingsSchema?: () => string; | ||
192 | - getDataKeySettingsSchema?: () => string; | ||
193 | - typeParameters?: () => WidgetTypeParameters; | ||
194 | - useCustomDatasources?: () => boolean; | ||
195 | - actionSources?: () => {[key: string]: WidgetActionSource}; | ||
196 | - | ||
197 | - onInit?: () => void; | ||
198 | - onDataUpdated?: () => void; | ||
199 | - onResize?: () => void; | ||
200 | - onEditModeChanged?: () => void; | ||
201 | - onMobileModeChanged?: () => void; | ||
202 | - onDestroy?: () => void; | ||
203 | -} | ||
204 | - | ||
205 | -export function toWidgetInfo(widgetTypeEntity: WidgetType): WidgetInfo { | ||
206 | - return { | ||
207 | - widgetName: widgetTypeEntity.name, | ||
208 | - alias: widgetTypeEntity.alias, | ||
209 | - type: widgetTypeEntity.descriptor.type, | ||
210 | - sizeX: widgetTypeEntity.descriptor.sizeX, | ||
211 | - sizeY: widgetTypeEntity.descriptor.sizeY, | ||
212 | - resources: widgetTypeEntity.descriptor.resources, | ||
213 | - templateHtml: widgetTypeEntity.descriptor.templateHtml, | ||
214 | - templateCss: widgetTypeEntity.descriptor.templateCss, | ||
215 | - controllerScript: widgetTypeEntity.descriptor.controllerScript, | ||
216 | - settingsSchema: widgetTypeEntity.descriptor.settingsSchema, | ||
217 | - dataKeySettingsSchema: widgetTypeEntity.descriptor.dataKeySettingsSchema, | ||
218 | - defaultConfig: widgetTypeEntity.descriptor.defaultConfig | ||
219 | - }; | ||
220 | -} | ||
221 | - | ||
222 | export enum LegendDirection { | 140 | export enum LegendDirection { |
223 | column = 'column', | 141 | column = 'column', |
224 | row = 'row' | 142 | row = 'row' |