Commit 2eb93dac7e0c83b98fa01e63f4409be00ed3295c
1 parent
b60b3144
Widget Editor: Save and save as actions.
Showing
24 changed files
with
821 additions
and
53 deletions
@@ -505,6 +505,7 @@ export class WidgetSubscription implements IWidgetSubscription { | @@ -505,6 +505,7 @@ export class WidgetSubscription implements IWidgetSubscription { | ||
505 | 505 | ||
506 | private alarmsSubscribe() { | 506 | private alarmsSubscribe() { |
507 | // TODO: | 507 | // TODO: |
508 | + this.notifyDataLoaded(); | ||
508 | } | 509 | } |
509 | 510 | ||
510 | 511 |
@@ -16,7 +16,7 @@ | @@ -16,7 +16,7 @@ | ||
16 | 16 | ||
17 | import { Injectable } from '@angular/core'; | 17 | import { Injectable } from '@angular/core'; |
18 | import { defaultHttpOptions } from './http-utils'; | 18 | import { defaultHttpOptions } from './http-utils'; |
19 | -import { Observable } from 'rxjs/index'; | 19 | +import { Observable, Subject, of, ReplaySubject } from 'rxjs/index'; |
20 | import { HttpClient } from '@angular/common/http'; | 20 | import { HttpClient } from '@angular/common/http'; |
21 | import { PageLink } from '@shared/models/page/page-link'; | 21 | import { PageLink } from '@shared/models/page/page-link'; |
22 | import { PageData } from '@shared/models/page/page-data'; | 22 | import { PageData } from '@shared/models/page/page-data'; |
@@ -25,20 +25,57 @@ import { WidgetType, widgetType, WidgetTypeData, widgetTypesData } from '@shared | @@ -25,20 +25,57 @@ import { WidgetType, widgetType, WidgetTypeData, widgetTypesData } from '@shared | ||
25 | import { UtilsService } from '@core/services/utils.service'; | 25 | import { UtilsService } from '@core/services/utils.service'; |
26 | import { TranslateService } from '@ngx-translate/core'; | 26 | import { TranslateService } from '@ngx-translate/core'; |
27 | import { ResourcesService } from '../services/resources.service'; | 27 | import { ResourcesService } from '../services/resources.service'; |
28 | -import { toWidgetInfo, WidgetInfo } from '@app/modules/home/models/widget-component.models'; | ||
29 | -import { map } from 'rxjs/operators'; | 28 | +import { toWidgetInfo, WidgetInfo, toWidgetType } from '@app/modules/home/models/widget-component.models'; |
29 | +import { map, tap, mergeMap, filter } from 'rxjs/operators'; | ||
30 | +import { WidgetTypeId } from '@shared/models/id/widget-type-id'; | ||
31 | +import { NULL_UUID } from '@shared/models/id/has-uuid'; | ||
32 | +import { ActivationEnd, Router } from '@angular/router'; | ||
30 | 33 | ||
31 | @Injectable({ | 34 | @Injectable({ |
32 | providedIn: 'root' | 35 | providedIn: 'root' |
33 | }) | 36 | }) |
34 | export class WidgetService { | 37 | export class WidgetService { |
35 | 38 | ||
39 | + private widgetTypeUpdatedSubject = new Subject<WidgetType>(); | ||
40 | + private widgetsBundleDeletedSubject = new Subject<WidgetsBundle>(); | ||
41 | + | ||
42 | + private allWidgetsBundles: Array<WidgetsBundle>; | ||
43 | + private systemWidgetsBundles: Array<WidgetsBundle>; | ||
44 | + private tenantWidgetsBundles: Array<WidgetsBundle>; | ||
45 | + | ||
36 | constructor( | 46 | constructor( |
37 | private http: HttpClient, | 47 | private http: HttpClient, |
38 | private utils: UtilsService, | 48 | private utils: UtilsService, |
39 | private resources: ResourcesService, | 49 | private resources: ResourcesService, |
40 | - private translate: TranslateService | 50 | + private translate: TranslateService, |
51 | + private router: Router | ||
41 | ) { | 52 | ) { |
53 | + this.router.events.pipe(filter(event => event instanceof ActivationEnd)).subscribe( | ||
54 | + () => { | ||
55 | + this.invalidateWidgetsBundleCache(); | ||
56 | + } | ||
57 | + ); | ||
58 | + } | ||
59 | + | ||
60 | + public getAllWidgetsBundles(ignoreErrors: boolean = false, | ||
61 | + ignoreLoading: boolean = false): Observable<Array<WidgetsBundle>> { | ||
62 | + return this.loadWidgetsBundleCache(ignoreErrors, ignoreLoading).pipe( | ||
63 | + map(() => this.allWidgetsBundles) | ||
64 | + ); | ||
65 | + } | ||
66 | + | ||
67 | + public getSystemWidgetsBundles(ignoreErrors: boolean = false, | ||
68 | + ignoreLoading: boolean = false): Observable<Array<WidgetsBundle>> { | ||
69 | + return this.loadWidgetsBundleCache(ignoreErrors, ignoreLoading).pipe( | ||
70 | + map(() => this.systemWidgetsBundles) | ||
71 | + ); | ||
72 | + } | ||
73 | + | ||
74 | + public getTenantWidgetsBundles(ignoreErrors: boolean = false, | ||
75 | + ignoreLoading: boolean = false): Observable<Array<WidgetsBundle>> { | ||
76 | + return this.loadWidgetsBundleCache(ignoreErrors, ignoreLoading).pipe( | ||
77 | + map(() => this.tenantWidgetsBundles) | ||
78 | + ); | ||
42 | } | 79 | } |
43 | 80 | ||
44 | public getWidgetBundles(pageLink: PageLink, ignoreErrors: boolean = false, | 81 | public getWidgetBundles(pageLink: PageLink, ignoreErrors: boolean = false, |
@@ -54,11 +91,26 @@ export class WidgetService { | @@ -54,11 +91,26 @@ export class WidgetService { | ||
54 | 91 | ||
55 | public saveWidgetsBundle(widgetsBundle: WidgetsBundle, | 92 | public saveWidgetsBundle(widgetsBundle: WidgetsBundle, |
56 | ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<WidgetsBundle> { | 93 | ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<WidgetsBundle> { |
57 | - return this.http.post<WidgetsBundle>('/api/widgetsBundle', widgetsBundle, defaultHttpOptions(ignoreLoading, ignoreErrors)); | 94 | + return this.http.post<WidgetsBundle>('/api/widgetsBundle', widgetsBundle, |
95 | + defaultHttpOptions(ignoreLoading, ignoreErrors)).pipe( | ||
96 | + tap(() => { | ||
97 | + this.invalidateWidgetsBundleCache(); | ||
98 | + }) | ||
99 | + ); | ||
58 | } | 100 | } |
59 | 101 | ||
60 | public deleteWidgetsBundle(widgetsBundleId: string, ignoreErrors: boolean = false, ignoreLoading: boolean = false) { | 102 | public deleteWidgetsBundle(widgetsBundleId: string, ignoreErrors: boolean = false, ignoreLoading: boolean = false) { |
61 | - return this.http.delete(`/api/widgetsBundle/${widgetsBundleId}`, defaultHttpOptions(ignoreLoading, ignoreErrors)); | 103 | + return this.getWidgetsBundle(widgetsBundleId, ignoreErrors, ignoreLoading).pipe( |
104 | + mergeMap((widgetsBundle) => { | ||
105 | + return this.http.delete(`/api/widgetsBundle/${widgetsBundleId}`, | ||
106 | + defaultHttpOptions(ignoreLoading, ignoreErrors)).pipe( | ||
107 | + tap(() => { | ||
108 | + this.invalidateWidgetsBundleCache(); | ||
109 | + this.widgetsBundleDeletedSubject.next(widgetsBundle); | ||
110 | + }) | ||
111 | + ); | ||
112 | + } | ||
113 | + )); | ||
62 | } | 114 | } |
63 | 115 | ||
64 | public getBundleWidgetTypes(bundleAlias: string, isSystem: boolean, | 116 | public getBundleWidgetTypes(bundleAlias: string, isSystem: boolean, |
@@ -73,6 +125,41 @@ export class WidgetService { | @@ -73,6 +125,41 @@ export class WidgetService { | ||
73 | defaultHttpOptions(ignoreLoading, ignoreErrors)); | 125 | defaultHttpOptions(ignoreLoading, ignoreErrors)); |
74 | } | 126 | } |
75 | 127 | ||
128 | + public saveWidgetType(widgetInfo: WidgetInfo, | ||
129 | + id: WidgetTypeId, | ||
130 | + bundleAlias: string, | ||
131 | + ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<WidgetType> { | ||
132 | + const widgetTypeInstance = toWidgetType(widgetInfo, id, undefined, bundleAlias); | ||
133 | + return this.http.post<WidgetType>('/api/widgetType', widgetTypeInstance, | ||
134 | + defaultHttpOptions(ignoreLoading, ignoreErrors)).pipe( | ||
135 | + tap((savedWidgetType) => { | ||
136 | + this.widgetTypeUpdatedSubject.next(savedWidgetType); | ||
137 | + })); | ||
138 | + } | ||
139 | + | ||
140 | + public saveImportedWidgetType(widgetTypeInstance: WidgetType, | ||
141 | + ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<WidgetType> { | ||
142 | + return this.http.post<WidgetType>('/api/widgetType', widgetTypeInstance, | ||
143 | + defaultHttpOptions(ignoreLoading, ignoreErrors)).pipe( | ||
144 | + tap((savedWidgetType) => { | ||
145 | + this.widgetTypeUpdatedSubject.next(savedWidgetType); | ||
146 | + })); | ||
147 | + } | ||
148 | + | ||
149 | + public deleteWidgetType(bundleAlias: string, widgetTypeAlias: string, isSystem: boolean, | ||
150 | + ignoreErrors: boolean = false, ignoreLoading: boolean = false) { | ||
151 | + return this.getWidgetType(bundleAlias, widgetTypeAlias, isSystem, ignoreErrors, ignoreLoading).pipe( | ||
152 | + mergeMap((widgetTypeInstance) => { | ||
153 | + return this.http.delete(`/api/widgetType/${widgetTypeInstance.id.id}`, | ||
154 | + defaultHttpOptions(ignoreLoading, ignoreErrors)).pipe( | ||
155 | + tap(() => { | ||
156 | + this.widgetTypeUpdatedSubject.next(widgetTypeInstance); | ||
157 | + }) | ||
158 | + ); | ||
159 | + } | ||
160 | + )); | ||
161 | + } | ||
162 | + | ||
76 | public getWidgetTypeById(widgetTypeId: string, | 163 | public getWidgetTypeById(widgetTypeId: string, |
77 | ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<WidgetType> { | 164 | ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<WidgetType> { |
78 | return this.http.get<WidgetType>(`/api/widgetType/${widgetTypeId}`, | 165 | return this.http.get<WidgetType>(`/api/widgetType/${widgetTypeId}`, |
@@ -90,5 +177,55 @@ export class WidgetService { | @@ -90,5 +177,55 @@ export class WidgetService { | ||
90 | return widgetInfo; | 177 | return widgetInfo; |
91 | }) | 178 | }) |
92 | ); | 179 | ); |
180 | + } | ||
181 | + | ||
182 | + public onWidgetTypeUpdated(): Observable<WidgetType> { | ||
183 | + return this.widgetTypeUpdatedSubject.asObservable(); | ||
184 | + } | ||
185 | + | ||
186 | + public onWidgetBundleDeleted(): Observable<WidgetsBundle> { | ||
187 | + return this.widgetsBundleDeletedSubject.asObservable(); | ||
188 | + } | ||
189 | + | ||
190 | + private loadWidgetsBundleCache(ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<any> { | ||
191 | + if (!this.allWidgetsBundles) { | ||
192 | + const loadWidgetsBundleCacheSubject = new ReplaySubject(); | ||
193 | + this.http.get<Array<WidgetsBundle>>('/api/widgetsBundles', | ||
194 | + defaultHttpOptions(ignoreLoading, ignoreErrors)).subscribe( | ||
195 | + (allWidgetsBundles) => { | ||
196 | + this.allWidgetsBundles = allWidgetsBundles; | ||
197 | + this.systemWidgetsBundles = new Array<WidgetsBundle>(); | ||
198 | + this.tenantWidgetsBundles = new Array<WidgetsBundle>(); | ||
199 | + this.allWidgetsBundles = this.allWidgetsBundles.sort((wb1, wb2) => { | ||
200 | + let res = wb1.title.localeCompare(wb2.title); | ||
201 | + if (res === 0) { | ||
202 | + res = wb2.createdTime - wb1.createdTime; | ||
203 | + } | ||
204 | + return res; | ||
205 | + }); | ||
206 | + this.allWidgetsBundles.forEach((widgetsBundle) => { | ||
207 | + if (widgetsBundle.tenantId.id === NULL_UUID) { | ||
208 | + this.systemWidgetsBundles.push(widgetsBundle); | ||
209 | + } else { | ||
210 | + this.tenantWidgetsBundles.push(widgetsBundle); | ||
211 | + } | ||
212 | + }); | ||
213 | + loadWidgetsBundleCacheSubject.next(); | ||
214 | + loadWidgetsBundleCacheSubject.complete(); | ||
215 | + }, | ||
216 | + () => { | ||
217 | + loadWidgetsBundleCacheSubject.error(null); | ||
218 | + }); | ||
219 | + return loadWidgetsBundleCacheSubject.asObservable(); | ||
220 | + } else { | ||
221 | + return of(null); | ||
93 | } | 222 | } |
223 | + } | ||
224 | + | ||
225 | + private invalidateWidgetsBundleCache() { | ||
226 | + this.allWidgetsBundles = undefined; | ||
227 | + this.systemWidgetsBundles = undefined; | ||
228 | + this.tenantWidgetsBundles = undefined; | ||
229 | + } | ||
230 | + | ||
94 | } | 231 | } |
@@ -84,7 +84,7 @@ export class UtilsService { | @@ -84,7 +84,7 @@ export class UtilsService { | ||
84 | } | 84 | } |
85 | 85 | ||
86 | public processWidgetException(exception: any): ExceptionData { | 86 | public processWidgetException(exception: any): ExceptionData { |
87 | - const data = this.parseException(exception, -5); | 87 | + const data = this.parseException(exception, -6); |
88 | if (this.widgetEditMode) { | 88 | if (this.widgetEditMode) { |
89 | const message: WindowMessage = { | 89 | const message: WindowMessage = { |
90 | type: 'widgetException', | 90 | type: 'widgetException', |
@@ -308,7 +308,13 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo | @@ -308,7 +308,13 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo | ||
308 | $event.stopPropagation(); | 308 | $event.stopPropagation(); |
309 | } | 309 | } |
310 | if (this.isRemoveActionEnabled && this.callbacks && this.callbacks.onRemoveWidget) { | 310 | if (this.isRemoveActionEnabled && this.callbacks && this.callbacks.onRemoveWidget) { |
311 | - this.callbacks.onRemoveWidget($event, widget.widget); | 311 | + this.callbacks.onRemoveWidget($event, widget.widget).subscribe( |
312 | + (result) => { | ||
313 | + if (result) { | ||
314 | + this.dashboardWidgets.removeWidget(widget.widget); | ||
315 | + } | ||
316 | + } | ||
317 | + ); | ||
312 | } | 318 | } |
313 | } | 319 | } |
314 | 320 |
@@ -18,16 +18,19 @@ import { Inject, Injectable } from '@angular/core'; | @@ -18,16 +18,19 @@ import { Inject, Injectable } from '@angular/core'; | ||
18 | import { DynamicComponentFactoryService } from '@core/services/dynamic-component-factory.service'; | 18 | import { DynamicComponentFactoryService } from '@core/services/dynamic-component-factory.service'; |
19 | import { WidgetService } from '@core/http/widget.service'; | 19 | import { WidgetService } from '@core/http/widget.service'; |
20 | import { forkJoin, Observable, of, ReplaySubject, Subject, throwError } from 'rxjs'; | 20 | import { forkJoin, Observable, of, ReplaySubject, Subject, throwError } from 'rxjs'; |
21 | -import { WidgetInfo, MissingWidgetType, toWidgetInfo, WidgetTypeInstance, ErrorWidgetType } from '@home/models/widget-component.models'; | 21 | +import { |
22 | + ErrorWidgetType, | ||
23 | + MissingWidgetType, | ||
24 | + toWidgetInfo, | ||
25 | + toWidgetType, | ||
26 | + WidgetInfo, | ||
27 | + WidgetTypeInstance | ||
28 | +} from '@home/models/widget-component.models'; | ||
22 | import cssjs from '@core/css/css'; | 29 | import cssjs from '@core/css/css'; |
23 | import { UtilsService } from '@core/services/utils.service'; | 30 | import { UtilsService } from '@core/services/utils.service'; |
24 | import { ResourcesService } from '@core/services/resources.service'; | 31 | 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'; | 32 | +import { widgetActionSources, WidgetControllerDescriptor, WidgetType } from '@shared/models/widget.models'; |
33 | +import { catchError, map, mergeMap, switchMap } from 'rxjs/operators'; | ||
31 | import { isFunction, isUndefined } from '@core/utils'; | 34 | import { isFunction, isUndefined } from '@core/utils'; |
32 | import { TranslateService } from '@ngx-translate/core'; | 35 | import { TranslateService } from '@ngx-translate/core'; |
33 | import { DynamicWidgetComponent } from '@home/components/widget/dynamic-widget.component'; | 36 | import { DynamicWidgetComponent } from '@home/components/widget/dynamic-widget.component'; |
@@ -37,6 +40,9 @@ import { WINDOW } from '@core/services/window.service'; | @@ -37,6 +40,9 @@ import { WINDOW } from '@core/services/window.service'; | ||
37 | 40 | ||
38 | import * as tinycolor from 'tinycolor2'; | 41 | import * as tinycolor from 'tinycolor2'; |
39 | import { TbFlot } from './lib/flot-widget'; | 42 | import { TbFlot } from './lib/flot-widget'; |
43 | +import { NULL_UUID } from '@shared/models/id/has-uuid'; | ||
44 | +import { WidgetTypeId } from '@app/shared/models/id/widget-type-id'; | ||
45 | +import { TenantId } from '@app/shared/models/id/tenant-id'; | ||
40 | 46 | ||
41 | // declare var jQuery: any; | 47 | // declare var jQuery: any; |
42 | 48 | ||
@@ -53,6 +59,7 @@ export class WidgetComponentService { | @@ -53,6 +59,7 @@ export class WidgetComponentService { | ||
53 | 59 | ||
54 | private missingWidgetType: WidgetInfo; | 60 | private missingWidgetType: WidgetInfo; |
55 | private errorWidgetType: WidgetInfo; | 61 | private errorWidgetType: WidgetInfo; |
62 | + private editingWidgetType: WidgetType; | ||
56 | 63 | ||
57 | constructor(@Inject(WINDOW) private window: Window, | 64 | constructor(@Inject(WINDOW) private window: Window, |
58 | private dynamicComponentFactoryService: DynamicComponentFactoryService, | 65 | private dynamicComponentFactoryService: DynamicComponentFactoryService, |
@@ -68,6 +75,15 @@ export class WidgetComponentService { | @@ -68,6 +75,15 @@ export class WidgetComponentService { | ||
68 | this.window.TbFlot = TbFlot; | 75 | this.window.TbFlot = TbFlot; |
69 | 76 | ||
70 | this.cssParser.testMode = false; | 77 | this.cssParser.testMode = false; |
78 | + | ||
79 | + this.widgetService.onWidgetTypeUpdated().subscribe((widgetType) => { | ||
80 | + this.deleteWidgetInfoFromCache(widgetType.bundleAlias, widgetType.alias, widgetType.tenantId.id === NULL_UUID); | ||
81 | + }); | ||
82 | + | ||
83 | + this.widgetService.onWidgetBundleDeleted().subscribe((widgetsBundle) => { | ||
84 | + this.deleteWidgetsBundleFromCache(widgetsBundle.alias, widgetsBundle.tenantId.id === NULL_UUID); | ||
85 | + }); | ||
86 | + | ||
71 | this.init(); | 87 | this.init(); |
72 | } | 88 | } |
73 | 89 | ||
@@ -77,6 +93,24 @@ export class WidgetComponentService { | @@ -77,6 +93,24 @@ export class WidgetComponentService { | ||
77 | } else { | 93 | } else { |
78 | this.missingWidgetType = {...MissingWidgetType}; | 94 | this.missingWidgetType = {...MissingWidgetType}; |
79 | this.errorWidgetType = {...ErrorWidgetType}; | 95 | this.errorWidgetType = {...ErrorWidgetType}; |
96 | + if (this.utils.widgetEditMode) { | ||
97 | + this.editingWidgetType = toWidgetType( | ||
98 | + { | ||
99 | + widgetName: this.utils.editWidgetInfo.widgetName, | ||
100 | + alias: 'customWidget', | ||
101 | + type: this.utils.editWidgetInfo.type, | ||
102 | + sizeX: this.utils.editWidgetInfo.sizeX, | ||
103 | + sizeY: this.utils.editWidgetInfo.sizeY, | ||
104 | + resources: this.utils.editWidgetInfo.resources, | ||
105 | + templateHtml: this.utils.editWidgetInfo.templateHtml, | ||
106 | + templateCss: this.utils.editWidgetInfo.templateCss, | ||
107 | + controllerScript: this.utils.editWidgetInfo.controllerScript, | ||
108 | + settingsSchema: this.utils.editWidgetInfo.settingsSchema, | ||
109 | + dataKeySettingsSchema: this.utils.editWidgetInfo.dataKeySettingsSchema, | ||
110 | + defaultConfig: this.utils.editWidgetInfo.defaultConfig | ||
111 | + }, new WidgetTypeId('1'), new TenantId( NULL_UUID ), 'customWidgetBundle' | ||
112 | + ); | ||
113 | + } | ||
80 | const initSubject = new ReplaySubject(); | 114 | const initSubject = new ReplaySubject(); |
81 | this.init$ = initSubject.asObservable(); | 115 | this.init$ = initSubject.asObservable(); |
82 | const loadDefaultWidgetInfoTasks = [ | 116 | const loadDefaultWidgetInfoTasks = [ |
@@ -110,7 +144,7 @@ export class WidgetComponentService { | @@ -110,7 +144,7 @@ export class WidgetComponentService { | ||
110 | widgetInfoSubject.complete(); | 144 | widgetInfoSubject.complete(); |
111 | } else { | 145 | } else { |
112 | if (this.utils.widgetEditMode) { | 146 | if (this.utils.widgetEditMode) { |
113 | - // TODO: | 147 | + this.loadWidget(this.editingWidgetType, bundleAlias, isSystem, widgetInfoSubject); |
114 | } else { | 148 | } else { |
115 | const key = this.createWidgetInfoCacheKey(bundleAlias, widgetTypeAlias, isSystem); | 149 | const key = this.createWidgetInfoCacheKey(bundleAlias, widgetTypeAlias, isSystem); |
116 | let fetchQueue = this.widgetsInfoFetchQueue.get(key); | 150 | let fetchQueue = this.widgetsInfoFetchQueue.get(key); |
@@ -377,4 +411,17 @@ export class WidgetComponentService { | @@ -377,4 +411,17 @@ export class WidgetComponentService { | ||
377 | this.widgetsInfoInMemoryCache.set(key, widgetInfo); | 411 | this.widgetsInfoInMemoryCache.set(key, widgetInfo); |
378 | } | 412 | } |
379 | 413 | ||
414 | + private deleteWidgetInfoFromCache(bundleAlias: string, widgetTypeAlias: string, isSystem: boolean) { | ||
415 | + const key = this.createWidgetInfoCacheKey(bundleAlias, widgetTypeAlias, isSystem); | ||
416 | + this.widgetsInfoInMemoryCache.delete(key); | ||
417 | + } | ||
418 | + | ||
419 | + private deleteWidgetsBundleFromCache(bundleAlias: string, isSystem: boolean) { | ||
420 | + const key = (isSystem ? 'sys_' : '') + bundleAlias; | ||
421 | + this.widgetsInfoInMemoryCache.forEach((widgetInfo, cacheKey) => { | ||
422 | + if (cacheKey.startsWith(key)) { | ||
423 | + this.widgetsInfoInMemoryCache.delete(cacheKey); | ||
424 | + } | ||
425 | + }); | ||
426 | + } | ||
380 | } | 427 | } |
@@ -847,13 +847,16 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI | @@ -847,13 +847,16 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI | ||
847 | createSubscriptionSubject.error(null); | 847 | createSubscriptionSubject.error(null); |
848 | } | 848 | } |
849 | ); | 849 | ); |
850 | + this.cd.detectChanges(); | ||
850 | } else if (this.widget.type === widgetType.static) { | 851 | } else if (this.widget.type === widgetType.static) { |
851 | this.loadingData = false; | 852 | this.loadingData = false; |
852 | createSubscriptionSubject.next(); | 853 | createSubscriptionSubject.next(); |
853 | createSubscriptionSubject.complete(); | 854 | createSubscriptionSubject.complete(); |
855 | + this.cd.detectChanges(); | ||
854 | } else { | 856 | } else { |
855 | createSubscriptionSubject.next(); | 857 | createSubscriptionSubject.next(); |
856 | createSubscriptionSubject.complete(); | 858 | createSubscriptionSubject.complete(); |
859 | + this.cd.detectChanges(); | ||
857 | } | 860 | } |
858 | return createSubscriptionSubject.asObservable(); | 861 | return createSubscriptionSubject.asObservable(); |
859 | } | 862 | } |
@@ -33,7 +33,7 @@ export interface WidgetsData { | @@ -33,7 +33,7 @@ export interface WidgetsData { | ||
33 | export interface DashboardCallbacks { | 33 | export interface DashboardCallbacks { |
34 | onEditWidget?: ($event: Event, widget: Widget) => void; | 34 | onEditWidget?: ($event: Event, widget: Widget) => void; |
35 | onExportWidget?: ($event: Event, widget: Widget) => void; | 35 | onExportWidget?: ($event: Event, widget: Widget) => void; |
36 | - onRemoveWidget?: ($event: Event, widget: Widget) => void; | 36 | + onRemoveWidget?: ($event: Event, widget: Widget) => Observable<boolean>; |
37 | onWidgetMouseDown?: ($event: Event, widget: Widget) => void; | 37 | onWidgetMouseDown?: ($event: Event, widget: Widget) => void; |
38 | onWidgetClicked?: ($event: Event, widget: Widget) => void; | 38 | onWidgetClicked?: ($event: Event, widget: Widget) => void; |
39 | prepareDashboardContextMenu?: ($event: Event) => void; | 39 | prepareDashboardContextMenu?: ($event: Event) => void; |
@@ -43,6 +43,8 @@ import { | @@ -43,6 +43,8 @@ import { | ||
43 | import { ComponentFactory } from '@angular/core'; | 43 | import { ComponentFactory } from '@angular/core'; |
44 | import { HttpErrorResponse } from '@angular/common/http'; | 44 | import { HttpErrorResponse } from '@angular/common/http'; |
45 | import { RafService } from '@core/services/raf.service'; | 45 | import { RafService } from '@core/services/raf.service'; |
46 | +import { WidgetTypeId } from '@shared/models/id/widget-type-id'; | ||
47 | +import { TenantId } from '@shared/models/id/tenant-id'; | ||
46 | 48 | ||
47 | export interface IWidgetAction { | 49 | export interface IWidgetAction { |
48 | name: string; | 50 | name: string; |
@@ -192,3 +194,25 @@ export function toWidgetInfo(widgetTypeEntity: WidgetType): WidgetInfo { | @@ -192,3 +194,25 @@ export function toWidgetInfo(widgetTypeEntity: WidgetType): WidgetInfo { | ||
192 | }; | 194 | }; |
193 | } | 195 | } |
194 | 196 | ||
197 | +export function toWidgetType(widgetInfo: WidgetInfo, id: WidgetTypeId, tenantId: TenantId, bundleAlias: string): WidgetType { | ||
198 | + const descriptor: WidgetTypeDescriptor = { | ||
199 | + type: widgetInfo.type, | ||
200 | + sizeX: widgetInfo.sizeX, | ||
201 | + sizeY: widgetInfo.sizeY, | ||
202 | + resources: widgetInfo.resources, | ||
203 | + templateHtml: widgetInfo.templateHtml, | ||
204 | + templateCss: widgetInfo.templateCss, | ||
205 | + controllerScript: widgetInfo.controllerScript, | ||
206 | + settingsSchema: widgetInfo.settingsSchema, | ||
207 | + dataKeySettingsSchema: widgetInfo.dataKeySettingsSchema, | ||
208 | + defaultConfig: widgetInfo.defaultConfig | ||
209 | + }; | ||
210 | + return { | ||
211 | + id, | ||
212 | + tenantId, | ||
213 | + bundleAlias, | ||
214 | + alias: widgetInfo.alias, | ||
215 | + name: widgetInfo.widgetName, | ||
216 | + descriptor | ||
217 | + }; | ||
218 | +} |
@@ -51,6 +51,7 @@ import { Subscription } from 'rxjs'; | @@ -51,6 +51,7 @@ import { Subscription } from 'rxjs'; | ||
51 | import { FooterFabButtons } from '@shared/components/footer-fab-buttons.component'; | 51 | import { FooterFabButtons } from '@shared/components/footer-fab-buttons.component'; |
52 | import { IStateController } from '@core/api/widget-api.models'; | 52 | import { IStateController } from '@core/api/widget-api.models'; |
53 | import { DashboardUtilsService } from '@core/services/dashboard-utils.service'; | 53 | import { DashboardUtilsService } from '@core/services/dashboard-utils.service'; |
54 | +import { DashboardService } from '@core/http/dashboard.service'; | ||
54 | 55 | ||
55 | @Component({ | 56 | @Component({ |
56 | selector: 'tb-dashboard-page', | 57 | selector: 'tb-dashboard-page', |
@@ -173,7 +174,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC | @@ -173,7 +174,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC | ||
173 | private dashboardUtils: DashboardUtilsService, | 174 | private dashboardUtils: DashboardUtilsService, |
174 | private authService: AuthService, | 175 | private authService: AuthService, |
175 | private entityService: EntityService, | 176 | private entityService: EntityService, |
176 | - private dialogService: DialogService) { | 177 | + private dialogService: DialogService, |
178 | + private dashboardService: DashboardService) { | ||
177 | super(store); | 179 | super(store); |
178 | 180 | ||
179 | this.rxSubscriptions.push(this.route.data.subscribe( | 181 | this.rxSubscriptions.push(this.route.data.subscribe( |
@@ -460,6 +462,11 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC | @@ -460,6 +462,11 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC | ||
460 | this.setEditMode(!this.isEdit, true); | 462 | this.setEditMode(!this.isEdit, true); |
461 | } | 463 | } |
462 | 464 | ||
465 | + public saveDashboard() { | ||
466 | + this.setEditMode(false, false); | ||
467 | + this.notifyDashboardUpdated(); | ||
468 | + } | ||
469 | + | ||
463 | public openDashboardState(state: string, openRightLayout: boolean) { | 470 | public openDashboardState(state: string, openRightLayout: boolean) { |
464 | const layoutsData = this.dashboardUtils.getStateLayoutsData(this.dashboard, state); | 471 | const layoutsData = this.dashboardUtils.getStateLayoutsData(this.dashboard, state); |
465 | if (layoutsData) { | 472 | if (layoutsData) { |
@@ -514,8 +521,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC | @@ -514,8 +521,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC | ||
514 | private setEditMode(isEdit: boolean, revert: boolean) { | 521 | private setEditMode(isEdit: boolean, revert: boolean) { |
515 | this.isEdit = isEdit; | 522 | this.isEdit = isEdit; |
516 | if (this.isEdit) { | 523 | if (this.isEdit) { |
517 | - // TODO: | ||
518 | - // this.dashboardCtx.stateController.preserveState(); | 524 | + this.dashboardCtx.stateController.preserveState(); |
519 | this.prevDashboard = deepClone(this.dashboard); | 525 | this.prevDashboard = deepClone(this.dashboard); |
520 | } else { | 526 | } else { |
521 | if (this.widgetEditMode) { | 527 | if (this.widgetEditMode) { |
@@ -549,4 +555,20 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC | @@ -549,4 +555,20 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC | ||
549 | private entityAliasesUpdated() { | 555 | private entityAliasesUpdated() { |
550 | this.dashboardCtx.aliasController.updateEntityAliases(this.dashboard.configuration.entityAliases); | 556 | this.dashboardCtx.aliasController.updateEntityAliases(this.dashboard.configuration.entityAliases); |
551 | } | 557 | } |
558 | + | ||
559 | + private notifyDashboardUpdated() { | ||
560 | + if (this.widgetEditMode) { | ||
561 | + const widget = this.layouts.main.layoutCtx.widgets[0]; | ||
562 | + const layout = this.layouts.main.layoutCtx.widgetLayouts[widget.id]; | ||
563 | + widget.sizeX = layout.sizeX; | ||
564 | + widget.sizeY = layout.sizeY; | ||
565 | + const message: WindowMessage = { | ||
566 | + type: 'widgetEditUpdated', | ||
567 | + data: widget | ||
568 | + }; | ||
569 | + this.window.parent.postMessage(JSON.stringify(message), '*'); | ||
570 | + } else { | ||
571 | + this.dashboardService.saveDashboard(this.dashboard); | ||
572 | + } | ||
573 | + } | ||
552 | } | 574 | } |
@@ -77,4 +77,7 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo | @@ -77,4 +77,7 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo | ||
77 | setResizing(layoutVisibilityChanged: boolean) { | 77 | setResizing(layoutVisibilityChanged: boolean) { |
78 | } | 78 | } |
79 | 79 | ||
80 | + resetHighlight() { | ||
81 | + } | ||
82 | + | ||
80 | } | 83 | } |
1 | +<!-- | ||
2 | + | ||
3 | + Copyright © 2016-2019 The Thingsboard Authors | ||
4 | + | ||
5 | + Licensed under the Apache License, Version 2.0 (the "License"); | ||
6 | + you may not use this file except in compliance with the License. | ||
7 | + You may obtain a copy of the License at | ||
8 | + | ||
9 | + http://www.apache.org/licenses/LICENSE-2.0 | ||
10 | + | ||
11 | + Unless required by applicable law or agreed to in writing, software | ||
12 | + distributed under the License is distributed on an "AS IS" BASIS, | ||
13 | + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
14 | + See the License for the specific language governing permissions and | ||
15 | + limitations under the License. | ||
16 | + | ||
17 | +--> | ||
18 | +<form #saveWidgetTypeAsForm="ngForm" | ||
19 | + [formGroup]="saveWidgetTypeAsFormGroup"(ngSubmit)="saveAs()"> | ||
20 | + <mat-toolbar fxLayout="row" color="primary"> | ||
21 | + <h2 translate>widget.save-widget-type-as</h2> | ||
22 | + <span fxFlex></span> | ||
23 | + <button mat-button mat-icon-button | ||
24 | + (click)="cancel()" | ||
25 | + type="button"> | ||
26 | + <mat-icon class="material-icons">close</mat-icon> | ||
27 | + </button> | ||
28 | + </mat-toolbar> | ||
29 | + <mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async"> | ||
30 | + </mat-progress-bar> | ||
31 | + <div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div> | ||
32 | + <div mat-dialog-content> | ||
33 | + <fieldset> | ||
34 | + <span translate>widget.save-widget-type-as-text</span> | ||
35 | + <mat-form-field class="mat-block"> | ||
36 | + <mat-label translate>widget.title</mat-label> | ||
37 | + <input matInput formControlName="title" required> | ||
38 | + <mat-error *ngIf="saveWidgetTypeAsFormGroup.get('title').hasError('required')"> | ||
39 | + {{ 'widget.title-required' | translate }} | ||
40 | + </mat-error> | ||
41 | + </mat-form-field> | ||
42 | + <tb-widgets-bundle-select fxFlex | ||
43 | + formControlName="widgetsBundle" | ||
44 | + required | ||
45 | + bundlesScope="{{bundlesScope}}"> | ||
46 | + </tb-widgets-bundle-select> | ||
47 | + </fieldset> | ||
48 | + </div> | ||
49 | + <div mat-dialog-actions fxLayout="row"> | ||
50 | + <span fxFlex></span> | ||
51 | + <button mat-button mat-raised-button color="primary" | ||
52 | + type="submit" | ||
53 | + [disabled]="(isLoading$ | async) || saveWidgetTypeAsForm.invalid | ||
54 | + || !saveWidgetTypeAsForm.dirty"> | ||
55 | + {{ 'action.saveAs' | translate }} | ||
56 | + </button> | ||
57 | + <button mat-button color="primary" | ||
58 | + style="margin-right: 20px;" | ||
59 | + type="button" | ||
60 | + [disabled]="(isLoading$ | async)" | ||
61 | + (click)="cancel()" cdkFocusInitial> | ||
62 | + {{ 'action.cancel' | translate }} | ||
63 | + </button> | ||
64 | + </div> | ||
65 | +</form> |
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 { Component, OnInit } from '@angular/core'; | ||
18 | +import { MatDialogRef } from '@angular/material'; | ||
19 | +import { Store } from '@ngrx/store'; | ||
20 | +import { AppState } from '@core/core.state'; | ||
21 | +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; | ||
22 | +import { DialogComponent } from '@shared/components/dialog.component'; | ||
23 | +import { Router } from '@angular/router'; | ||
24 | +import { WidgetsBundle } from '@shared/models/widgets-bundle.model'; | ||
25 | +import { getCurrentAuthUser } from '@core/auth/auth.selectors'; | ||
26 | +import { Authority } from '@shared/models/authority.enum'; | ||
27 | + | ||
28 | +export interface SaveWidgetTypeAsDialogResult { | ||
29 | + widgetName: string; | ||
30 | + bundleId: string; | ||
31 | + bundleAlias: string; | ||
32 | +} | ||
33 | + | ||
34 | +@Component({ | ||
35 | + selector: 'tb-save-widget-type-as-dialog', | ||
36 | + templateUrl: './save-widget-type-as-dialog.component.html', | ||
37 | + styleUrls: [] | ||
38 | +}) | ||
39 | +export class SaveWidgetTypeAsDialogComponent extends | ||
40 | + DialogComponent<SaveWidgetTypeAsDialogComponent, SaveWidgetTypeAsDialogResult> implements OnInit { | ||
41 | + | ||
42 | + saveWidgetTypeAsFormGroup: FormGroup; | ||
43 | + | ||
44 | + bundlesScope: string; | ||
45 | + | ||
46 | + constructor(protected store: Store<AppState>, | ||
47 | + protected router: Router, | ||
48 | + public dialogRef: MatDialogRef<SaveWidgetTypeAsDialogComponent, SaveWidgetTypeAsDialogResult>, | ||
49 | + public fb: FormBuilder) { | ||
50 | + super(store, router, dialogRef); | ||
51 | + | ||
52 | + const authUser = getCurrentAuthUser(store); | ||
53 | + if (authUser.authority === Authority.TENANT_ADMIN) { | ||
54 | + this.bundlesScope = 'tenant'; | ||
55 | + } else { | ||
56 | + this.bundlesScope = 'system'; | ||
57 | + } | ||
58 | + } | ||
59 | + | ||
60 | + ngOnInit(): void { | ||
61 | + this.saveWidgetTypeAsFormGroup = this.fb.group({ | ||
62 | + title: [null, [Validators.required]], | ||
63 | + widgetsBundle: [null, [Validators.required]] | ||
64 | + }); | ||
65 | + } | ||
66 | + | ||
67 | + cancel(): void { | ||
68 | + this.dialogRef.close(null); | ||
69 | + } | ||
70 | + | ||
71 | + saveAs(): void { | ||
72 | + const widgetName: string = this.saveWidgetTypeAsFormGroup.get('title').value; | ||
73 | + const widgetsBundle: WidgetsBundle = this.saveWidgetTypeAsFormGroup.get('widgetsBundle').value; | ||
74 | + const result: SaveWidgetTypeAsDialogResult = { | ||
75 | + widgetName, | ||
76 | + bundleId: widgetsBundle.id.id, | ||
77 | + bundleAlias: widgetsBundle.alias | ||
78 | + }; | ||
79 | + this.dialogRef.close(result); | ||
80 | + } | ||
81 | +} |
@@ -19,9 +19,10 @@ | @@ -19,9 +19,10 @@ | ||
19 | <div fxFlex fxLayout="column"> | 19 | <div fxFlex fxLayout="column"> |
20 | <div fxFlex fxLayout="column" tb-fullscreen [fullscreen]="fullscreen"> | 20 | <div fxFlex fxLayout="column" tb-fullscreen [fullscreen]="fullscreen"> |
21 | <mat-toolbar class="mat-elevation-z1 tb-edit-toolbar mat-hue-3" fxLayoutGap="16px"> | 21 | <mat-toolbar class="mat-elevation-z1 tb-edit-toolbar mat-hue-3" fxLayoutGap="16px"> |
22 | - <mat-form-field floatLabel="always" class="tb-widget-title"> | 22 | + <mat-form-field floatLabel="always" hideRequiredMarker class="tb-widget-title"> |
23 | <mat-label></mat-label> | 23 | <mat-label></mat-label> |
24 | - <input [disabled]="isReadOnly" matInput [(ngModel)]="widget.widgetName" (ngModelChange)="isDirty = true" | 24 | + <input [disabled]="isReadOnly" matInput required |
25 | + [(ngModel)]="widget.widgetName" (ngModelChange)="isDirty = true" | ||
25 | placeholder="{{ 'widget.title' | translate }}"/> | 26 | placeholder="{{ 'widget.title' | translate }}"/> |
26 | </mat-form-field> | 27 | </mat-form-field> |
27 | <mat-form-field> | 28 | <mat-form-field> |
@@ -238,14 +239,15 @@ | @@ -238,14 +239,15 @@ | ||
238 | </div> | 239 | </div> |
239 | <div tb-fullscreen [fullscreen]="iFrameFullscreen" style="width: 100%; height: 100%;"> | 240 | <div tb-fullscreen [fullscreen]="iFrameFullscreen" style="width: 100%; height: 100%;"> |
240 | <iframe #widgetIFrame frameborder="0" height="100%" width="100%"></iframe> | 241 | <iframe #widgetIFrame frameborder="0" height="100%" width="100%"></iframe> |
241 | - <button mat-button mat-icon-button | ||
242 | - class="tb-fullscreen-button-style" | ||
243 | - style="position: absolute; top: 10px; left: 10px; bottom: initial;" | ||
244 | - (click)="iFrameFullscreen = !iFrameFullscreen" | ||
245 | - matTooltip="{{(iFrameFullscreen ? 'fullscreen.exit' : 'fullscreen.expand') | translate}}" | ||
246 | - matTooltipPosition="above"> | ||
247 | - <mat-icon>{{ iFrameFullscreen ? 'fullscreen_exit' : 'fullscreen' }}</mat-icon> | ||
248 | - </button> | 242 | + <div style="position: absolute; top: 10px; left: 10px; bottom: initial;"> |
243 | + <button mat-button mat-icon-button | ||
244 | + class="tb-fullscreen-button-style" | ||
245 | + (click)="iFrameFullscreen = !iFrameFullscreen" | ||
246 | + matTooltip="{{(iFrameFullscreen ? 'fullscreen.exit' : 'fullscreen.expand') | translate}}" | ||
247 | + matTooltipPosition="above"> | ||
248 | + <mat-icon>{{ iFrameFullscreen ? 'fullscreen_exit' : 'fullscreen' }}</mat-icon> | ||
249 | + </button> | ||
250 | + </div> | ||
249 | </div> | 251 | </div> |
250 | </div> | 252 | </div> |
251 | </div> | 253 | </div> |
@@ -20,9 +20,9 @@ import { WidgetsBundle } from '@shared/models/widgets-bundle.model'; | @@ -20,9 +20,9 @@ import { WidgetsBundle } from '@shared/models/widgets-bundle.model'; | ||
20 | import { Store } from '@ngrx/store'; | 20 | import { Store } from '@ngrx/store'; |
21 | import { AppState } from '@core/core.state'; | 21 | import { AppState } from '@core/core.state'; |
22 | import { WidgetService } from '@core/http/widget.service'; | 22 | import { WidgetService } from '@core/http/widget.service'; |
23 | -import { WidgetInfo } from '@home/models/widget-component.models'; | 23 | +import { toWidgetInfo, WidgetInfo } from '@home/models/widget-component.models'; |
24 | import { WidgetConfig, widgetType, WidgetType, widgetTypesData, Widget } from '@shared/models/widget.models'; | 24 | import { WidgetConfig, widgetType, WidgetType, widgetTypesData, Widget } from '@shared/models/widget.models'; |
25 | -import { ActivatedRoute } from '@angular/router'; | 25 | +import { ActivatedRoute, Router } from '@angular/router'; |
26 | import { deepClone } from '@core/utils'; | 26 | import { deepClone } from '@core/utils'; |
27 | import { HasDirtyFlag } from '@core/guards/confirm-on-exit.guard'; | 27 | import { HasDirtyFlag } from '@core/guards/confirm-on-exit.guard'; |
28 | import { AuthUser } from '@shared/models/user.model'; | 28 | import { AuthUser } from '@shared/models/user.model'; |
@@ -40,6 +40,13 @@ import { WindowMessage } from '@shared/models/window-message.model'; | @@ -40,6 +40,13 @@ import { WindowMessage } from '@shared/models/window-message.model'; | ||
40 | import { ExceptionData } from '@shared/models/error.models'; | 40 | import { ExceptionData } from '@shared/models/error.models'; |
41 | import Timeout = NodeJS.Timeout; | 41 | import Timeout = NodeJS.Timeout; |
42 | import { ActionNotificationHide, ActionNotificationShow } from '@core/notification/notification.actions'; | 42 | import { ActionNotificationHide, ActionNotificationShow } from '@core/notification/notification.actions'; |
43 | +import { MatDialog } from '@angular/material/dialog'; | ||
44 | +import { SelectWidgetTypeDialogComponent } from '@home/pages/widget/select-widget-type-dialog.component'; | ||
45 | +import { | ||
46 | + SaveWidgetTypeAsDialogComponent, | ||
47 | + SaveWidgetTypeAsDialogResult | ||
48 | +} from '@home/pages/widget/save-widget-type-as-dialog.component'; | ||
49 | +import { Subscription } from 'rxjs'; | ||
43 | 50 | ||
44 | @Component({ | 51 | @Component({ |
45 | selector: 'tb-widget-editor', | 52 | selector: 'tb-widget-editor', |
@@ -131,25 +138,37 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe | @@ -131,25 +138,37 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe | ||
131 | 138 | ||
132 | saveWidgetTimeout: Timeout; | 139 | saveWidgetTimeout: Timeout; |
133 | 140 | ||
141 | + private rxSubscriptions = new Array<Subscription>(); | ||
142 | + | ||
134 | constructor(protected store: Store<AppState>, | 143 | constructor(protected store: Store<AppState>, |
135 | @Inject(WINDOW) private window: Window, | 144 | @Inject(WINDOW) private window: Window, |
136 | private route: ActivatedRoute, | 145 | private route: ActivatedRoute, |
146 | + private router: Router, | ||
137 | private widgetService: WidgetService, | 147 | private widgetService: WidgetService, |
138 | private hotkeysService: HotkeysService, | 148 | private hotkeysService: HotkeysService, |
139 | private translate: TranslateService, | 149 | private translate: TranslateService, |
140 | - private raf: RafService) { | 150 | + private raf: RafService, |
151 | + private dialog: MatDialog) { | ||
141 | super(store); | 152 | super(store); |
142 | 153 | ||
143 | this.authUser = getCurrentAuthUser(store); | 154 | this.authUser = getCurrentAuthUser(store); |
144 | 155 | ||
145 | - this.widgetsBundle = this.route.snapshot.data.widgetsBundle; | 156 | + this.rxSubscriptions.push(this.route.data.subscribe( |
157 | + (data) => { | ||
158 | + this.init(data); | ||
159 | + } | ||
160 | + )); | ||
161 | + } | ||
162 | + | ||
163 | + private init(data: any) { | ||
164 | + this.widgetsBundle = data.widgetsBundle; | ||
146 | if (this.authUser.authority === Authority.TENANT_ADMIN) { | 165 | if (this.authUser.authority === Authority.TENANT_ADMIN) { |
147 | this.isReadOnly = !this.widgetsBundle || this.widgetsBundle.tenantId.id === NULL_UUID; | 166 | this.isReadOnly = !this.widgetsBundle || this.widgetsBundle.tenantId.id === NULL_UUID; |
148 | } else { | 167 | } else { |
149 | this.isReadOnly = this.authUser.authority !== Authority.SYS_ADMIN; | 168 | this.isReadOnly = this.authUser.authority !== Authority.SYS_ADMIN; |
150 | } | 169 | } |
151 | - this.widgetType = this.route.snapshot.data.widgetEditorData.widgetType; | ||
152 | - this.widget = this.route.snapshot.data.widgetEditorData.widget; | 170 | + this.widgetType = data.widgetEditorData.widgetType; |
171 | + this.widget = data.widgetEditorData.widget; | ||
153 | if (this.widgetType) { | 172 | if (this.widgetType) { |
154 | const config = JSON.parse(this.widget.defaultConfig); | 173 | const config = JSON.parse(this.widget.defaultConfig); |
155 | this.widget.defaultConfig = JSON.stringify(config); | 174 | this.widget.defaultConfig = JSON.stringify(config); |
@@ -176,6 +195,10 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe | @@ -176,6 +195,10 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe | ||
176 | // @ts-ignore | 195 | // @ts-ignore |
177 | removeResizeListener(resizeListener.element, resizeListener.resizeListener); | 196 | removeResizeListener(resizeListener.element, resizeListener.resizeListener); |
178 | }); | 197 | }); |
198 | + this.rxSubscriptions.forEach((subscription) => { | ||
199 | + subscription.unsubscribe(); | ||
200 | + }); | ||
201 | + this.rxSubscriptions.length = 0; | ||
179 | } | 202 | } |
180 | 203 | ||
181 | private initHotKeys(): void { | 204 | private initHotKeys(): void { |
@@ -448,13 +471,52 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe | @@ -448,13 +471,52 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe | ||
448 | } | 471 | } |
449 | 472 | ||
450 | private commitSaveWidget() { | 473 | private commitSaveWidget() { |
451 | - // TODO: | ||
452 | - this.saveWidgetPending = false; | 474 | + const id = (this.widgetType && this.widgetType.id) ? this.widgetType.id : undefined; |
475 | + this.widgetService.saveWidgetType(this.widget, id, this.widgetsBundle.alias).subscribe( | ||
476 | + (widgetTypeInstance) => { | ||
477 | + this.setWidgetType(widgetTypeInstance); | ||
478 | + this.saveWidgetPending = false; | ||
479 | + this.store.dispatch(new ActionNotificationShow( | ||
480 | + {message: this.translate.instant('widget.widget-saved'), type: 'success', duration: 500})); | ||
481 | + }, | ||
482 | + () => { | ||
483 | + this.saveWidgetPending = false; | ||
484 | + } | ||
485 | + ); | ||
453 | } | 486 | } |
454 | 487 | ||
455 | private commitSaveWidgetAs() { | 488 | private commitSaveWidgetAs() { |
456 | - // TODO: | ||
457 | - this.saveWidgetAsPending = false; | 489 | + this.dialog.open<SaveWidgetTypeAsDialogComponent, any, |
490 | + SaveWidgetTypeAsDialogResult>(SaveWidgetTypeAsDialogComponent, { | ||
491 | + disableClose: true, | ||
492 | + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'] | ||
493 | + }).afterClosed().subscribe( | ||
494 | + (saveWidgetAsData) => { | ||
495 | + if (saveWidgetAsData) { | ||
496 | + this.widget.widgetName = saveWidgetAsData.widgetName; | ||
497 | + this.widget.alias = undefined; | ||
498 | + const config = JSON.parse(this.widget.defaultConfig); | ||
499 | + config.title = this.widget.widgetName; | ||
500 | + this.widget.defaultConfig = JSON.stringify(config); | ||
501 | + this.isDirty = false; | ||
502 | + this.widgetService.saveWidgetType(this.widget, undefined, saveWidgetAsData.bundleAlias).subscribe( | ||
503 | + (widgetTypeInstance) => { | ||
504 | + this.router.navigateByUrl(`/widgets-bundles/${saveWidgetAsData.bundleId}/widgetTypes/${widgetTypeInstance.id.id}`); | ||
505 | + } | ||
506 | + ); | ||
507 | + } | ||
508 | + this.saveWidgetAsPending = false; | ||
509 | + } | ||
510 | + ); | ||
511 | + } | ||
512 | + | ||
513 | + private setWidgetType(widgetTypeInstance: WidgetType) { | ||
514 | + this.widgetType = widgetTypeInstance; | ||
515 | + this.widget = toWidgetInfo(this.widgetType); | ||
516 | + const config = JSON.parse(this.widget.defaultConfig); | ||
517 | + this.widget.defaultConfig = JSON.stringify(config); | ||
518 | + this.origWidget = deepClone(this.widget); | ||
519 | + this.isDirty = false; | ||
458 | } | 520 | } |
459 | 521 | ||
460 | applyWidgetScript(): void { | 522 | applyWidgetScript(): void { |
@@ -73,7 +73,7 @@ export class WidgetsTypesDataResolver implements Resolve<WidgetsData> { | @@ -73,7 +73,7 @@ export class WidgetsTypesDataResolver implements Resolve<WidgetsData> { | ||
73 | } | 73 | } |
74 | return result; | 74 | return result; |
75 | }); | 75 | }); |
76 | - const widgetTypes = new Array<Widget>(types.length); | 76 | + const widgetTypes = new Array<Widget>(); |
77 | let top = 0; | 77 | let top = 0; |
78 | const lastTop = [0, 0, 0]; | 78 | const lastTop = [0, 0, 0]; |
79 | let col = 0; | 79 | let col = 0; |
@@ -15,7 +15,7 @@ | @@ -15,7 +15,7 @@ | ||
15 | limitations under the License. | 15 | limitations under the License. |
16 | 16 | ||
17 | --> | 17 | --> |
18 | -<section [fxShow]="!(isLoading$ | async) && (widgetTypes$ | async)?.length === 0" fxLayoutAlign="center center" | 18 | +<section [fxShow]="!(isLoading$ | async) && widgetsData.widgets.length === 0" fxLayoutAlign="center center" |
19 | style="text-transform: uppercase; display: flex; z-index: 1;" | 19 | style="text-transform: uppercase; display: flex; z-index: 1;" |
20 | class="tb-absolute-fill"> | 20 | class="tb-absolute-fill"> |
21 | <button mat-button *ngIf="!isReadOnly" class="tb-add-new-widget" (click)="addWidgetType($event)"> | 21 | <button mat-button *ngIf="!isReadOnly" class="tb-add-new-widget" (click)="addWidgetType($event)"> |
@@ -27,7 +27,8 @@ | @@ -27,7 +27,8 @@ | ||
27 | style="text-transform: uppercase; display: flex;" | 27 | style="text-transform: uppercase; display: flex;" |
28 | class="mat-headline tb-absolute-fill">widgets-bundle.empty</span> | 28 | class="mat-headline tb-absolute-fill">widgets-bundle.empty</span> |
29 | </section> | 29 | </section> |
30 | -<tb-dashboard [aliasController]="aliasController" | 30 | +<tb-dashboard #dashboard |
31 | + [aliasController]="aliasController" | ||
31 | [widgets]="widgetsData.widgets" | 32 | [widgets]="widgetsData.widgets" |
32 | [widgetLayouts]="widgetsData.widgetLayouts" | 33 | [widgetLayouts]="widgetsData.widgetLayouts" |
33 | [isEdit]="false" | 34 | [isEdit]="false" |
@@ -14,7 +14,7 @@ | @@ -14,7 +14,7 @@ | ||
14 | /// limitations under the License. | 14 | /// limitations under the License. |
15 | /// | 15 | /// |
16 | 16 | ||
17 | -import { Component, OnInit } from '@angular/core'; | 17 | +import { Component, OnInit, ViewChild } from '@angular/core'; |
18 | import { Store } from '@ngrx/store'; | 18 | import { Store } from '@ngrx/store'; |
19 | import { AppState } from '@core/core.state'; | 19 | import { AppState } from '@core/core.state'; |
20 | import { PageComponent } from '@shared/components/page.component'; | 20 | import { PageComponent } from '@shared/components/page.component'; |
@@ -24,13 +24,13 @@ import { WidgetsBundle } from '@shared/models/widgets-bundle.model'; | @@ -24,13 +24,13 @@ import { WidgetsBundle } from '@shared/models/widgets-bundle.model'; | ||
24 | import { ActivatedRoute, Router } from '@angular/router'; | 24 | import { ActivatedRoute, Router } 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, of } from 'rxjs'; |
28 | import { 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, mergeMap, 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, IDashboardComponent, 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 | import { toWidgetInfo } from '@home/models/widget-component.models'; |
36 | import { DummyAliasController } from '@core/api/alias-controller'; | 36 | import { DummyAliasController } from '@core/api/alias-controller'; |
@@ -41,6 +41,7 @@ import { | @@ -41,6 +41,7 @@ import { | ||
41 | import { DeviceCredentials } from '@shared/models/device.models'; | 41 | import { DeviceCredentials } from '@shared/models/device.models'; |
42 | import { MatDialog } from '@angular/material/dialog'; | 42 | import { MatDialog } from '@angular/material/dialog'; |
43 | import { SelectWidgetTypeDialogComponent } from '@home/pages/widget/select-widget-type-dialog.component'; | 43 | import { SelectWidgetTypeDialogComponent } from '@home/pages/widget/select-widget-type-dialog.component'; |
44 | +import { TranslateService } from '@ngx-translate/core'; | ||
44 | 45 | ||
45 | @Component({ | 46 | @Component({ |
46 | selector: 'tb-widget-library', | 47 | selector: 'tb-widget-library', |
@@ -86,12 +87,15 @@ export class WidgetLibraryComponent extends PageComponent implements OnInit { | @@ -86,12 +87,15 @@ export class WidgetLibraryComponent extends PageComponent implements OnInit { | ||
86 | 87 | ||
87 | aliasController: IAliasController = new DummyAliasController(); | 88 | aliasController: IAliasController = new DummyAliasController(); |
88 | 89 | ||
90 | + @ViewChild('dashboard', {static: true}) dashboard: IDashboardComponent; | ||
91 | + | ||
89 | constructor(protected store: Store<AppState>, | 92 | constructor(protected store: Store<AppState>, |
90 | private route: ActivatedRoute, | 93 | private route: ActivatedRoute, |
91 | private router: Router, | 94 | private router: Router, |
92 | private widgetService: WidgetService, | 95 | private widgetService: WidgetService, |
93 | private dialogService: DialogService, | 96 | private dialogService: DialogService, |
94 | - private dialog: MatDialog) { | 97 | + private dialog: MatDialog, |
98 | + private translate: TranslateService) { | ||
95 | super(store); | 99 | super(store); |
96 | 100 | ||
97 | this.authUser = getCurrentAuthUser(store); | 101 | this.authUser = getCurrentAuthUser(store); |
@@ -146,11 +150,32 @@ export class WidgetLibraryComponent extends PageComponent implements OnInit { | @@ -146,11 +150,32 @@ export class WidgetLibraryComponent extends PageComponent implements OnInit { | ||
146 | this.dialogService.todo(); | 150 | this.dialogService.todo(); |
147 | } | 151 | } |
148 | 152 | ||
149 | - removeWidgetType($event: Event, widget: Widget): void { | 153 | + removeWidgetType($event: Event, widget: Widget): Observable<boolean> { |
150 | if ($event) { | 154 | if ($event) { |
151 | $event.stopPropagation(); | 155 | $event.stopPropagation(); |
152 | } | 156 | } |
153 | - this.dialogService.todo(); | 157 | + return this.dialogService.confirm( |
158 | + this.translate.instant('widget.remove-widget-type-title', {widgetName: widget.config.title}), | ||
159 | + this.translate.instant('widget.remove-widget-type-text'), | ||
160 | + this.translate.instant('action.no'), | ||
161 | + this.translate.instant('action.yes'), | ||
162 | + ).pipe( | ||
163 | + mergeMap((result) => { | ||
164 | + if (result) { | ||
165 | + return this.widgetService.deleteWidgetType(widget.bundleAlias, widget.typeAlias, widget.isSystemType); | ||
166 | + } else { | ||
167 | + return of(false); | ||
168 | + } | ||
169 | + }), | ||
170 | + map((result) => { | ||
171 | + if (result !== false) { | ||
172 | + this.widgetsData.widgets.splice(this.widgetsData.widgets.indexOf(widget), 1); | ||
173 | + return true; | ||
174 | + } else { | ||
175 | + return false; | ||
176 | + } | ||
177 | + } | ||
178 | + )); | ||
154 | } | 179 | } |
155 | 180 | ||
156 | } | 181 | } |
@@ -23,17 +23,20 @@ import {HomeComponentsModule} from '@modules/home/components/home-components.mod | @@ -23,17 +23,20 @@ import {HomeComponentsModule} from '@modules/home/components/home-components.mod | ||
23 | import { WidgetLibraryComponent } from './widget-library.component'; | 23 | import { WidgetLibraryComponent } from './widget-library.component'; |
24 | import { WidgetEditorComponent } from '@home/pages/widget/widget-editor.component'; | 24 | import { WidgetEditorComponent } from '@home/pages/widget/widget-editor.component'; |
25 | import { SelectWidgetTypeDialogComponent } from '@home/pages/widget/select-widget-type-dialog.component'; | 25 | import { SelectWidgetTypeDialogComponent } from '@home/pages/widget/select-widget-type-dialog.component'; |
26 | +import { SaveWidgetTypeAsDialogComponent } from './save-widget-type-as-dialog.component'; | ||
26 | 27 | ||
27 | @NgModule({ | 28 | @NgModule({ |
28 | entryComponents: [ | 29 | entryComponents: [ |
29 | WidgetsBundleComponent, | 30 | WidgetsBundleComponent, |
30 | - SelectWidgetTypeDialogComponent | 31 | + SelectWidgetTypeDialogComponent, |
32 | + SaveWidgetTypeAsDialogComponent | ||
31 | ], | 33 | ], |
32 | declarations: [ | 34 | declarations: [ |
33 | WidgetsBundleComponent, | 35 | WidgetsBundleComponent, |
34 | WidgetLibraryComponent, | 36 | WidgetLibraryComponent, |
35 | WidgetEditorComponent, | 37 | WidgetEditorComponent, |
36 | - SelectWidgetTypeDialogComponent | 38 | + SelectWidgetTypeDialogComponent, |
39 | + SaveWidgetTypeAsDialogComponent | ||
37 | ], | 40 | ], |
38 | imports: [ | 41 | imports: [ |
39 | CommonModule, | 42 | CommonModule, |
@@ -38,6 +38,7 @@ import { | @@ -38,6 +38,7 @@ import { | ||
38 | DashboardSelectPanelComponent, | 38 | DashboardSelectPanelComponent, |
39 | DashboardSelectPanelData | 39 | DashboardSelectPanelData |
40 | } from './dashboard-select-panel.component'; | 40 | } from './dashboard-select-panel.component'; |
41 | +import { NULL_UUID } from '@shared/models/id/has-uuid'; | ||
41 | 42 | ||
42 | @Component({ | 43 | @Component({ |
43 | selector: 'tb-dashboard-select', | 44 | selector: 'tb-dashboard-select', |
@@ -200,7 +201,7 @@ export class DashboardSelectComponent implements ControlValueAccessor, OnInit { | @@ -200,7 +201,7 @@ export class DashboardSelectComponent implements ControlValueAccessor, OnInit { | ||
200 | let dashboardsObservable: Observable<PageData<DashboardInfo>>; | 201 | let dashboardsObservable: Observable<PageData<DashboardInfo>>; |
201 | const authUser = getCurrentAuthUser(this.store); | 202 | const authUser = getCurrentAuthUser(this.store); |
202 | if (this.dashboardsScope === 'customer' || authUser.authority === Authority.CUSTOMER_USER) { | 203 | if (this.dashboardsScope === 'customer' || authUser.authority === Authority.CUSTOMER_USER) { |
203 | - if (this.customerId) { | 204 | + if (this.customerId && this.customerId !== NULL_UUID) { |
204 | dashboardsObservable = this.dashboardService.getCustomerDashboards(this.customerId, pageLink, false, true); | 205 | dashboardsObservable = this.dashboardService.getCustomerDashboards(this.customerId, pageLink, false, true); |
205 | } else { | 206 | } else { |
206 | dashboardsObservable = of(emptyPageData()); | 207 | dashboardsObservable = of(emptyPageData()); |
1 | +<!-- | ||
2 | + | ||
3 | + Copyright © 2016-2019 The Thingsboard Authors | ||
4 | + | ||
5 | + Licensed under the Apache License, Version 2.0 (the "License"); | ||
6 | + you may not use this file except in compliance with the License. | ||
7 | + You may obtain a copy of the License at | ||
8 | + | ||
9 | + http://www.apache.org/licenses/LICENSE-2.0 | ||
10 | + | ||
11 | + Unless required by applicable law or agreed to in writing, software | ||
12 | + distributed under the License is distributed on an "AS IS" BASIS, | ||
13 | + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
14 | + See the License for the specific language governing permissions and | ||
15 | + limitations under the License. | ||
16 | + | ||
17 | +--> | ||
18 | +<mat-form-field floatLabel="always" hideRequiredMarker class="mat-block"> | ||
19 | + <mat-label></mat-label> | ||
20 | + <mat-select [required]="required" | ||
21 | + [disabled]="disabled" | ||
22 | + [(ngModel)]="widgetsBundle" | ||
23 | + matInput | ||
24 | + panelClass="tb-widgets-bundle-select" | ||
25 | + placeholder="{{ 'widget.select-widgets-bundle' | translate }}" | ||
26 | + (ngModelChange)="widgetsBundleChanged()"> | ||
27 | + <mat-option *ngFor="let widgetsBundle of widgetsBundles$ | async" [value]="widgetsBundle"> | ||
28 | + <div class="tb-bundle-item"> | ||
29 | + <span>{{widgetsBundle.title}}</span> | ||
30 | + <span translate class="tb-bundle-system" *ngIf="isSystem(item)">widgets-bundle.system</span> | ||
31 | + </div> | ||
32 | + </mat-option> | ||
33 | + </mat-select> | ||
34 | +</mat-form-field> |
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 | +tb-widgets-bundle-select { | ||
18 | + mat-select { | ||
19 | + margin: 0; | ||
20 | + } | ||
21 | + | ||
22 | + .tb-bundle-item { | ||
23 | + height: 24px; | ||
24 | + line-height: 24px; | ||
25 | + } | ||
26 | +} | ||
27 | + | ||
28 | +.tb-widgets-bundle-select { | ||
29 | + .tb-bundle-item { | ||
30 | + height: 48px; | ||
31 | + line-height: 48px; | ||
32 | + } | ||
33 | +} | ||
34 | + | ||
35 | +tb-widgets-bundle-select, | ||
36 | +.tb-widgets-bundle-select { | ||
37 | + .mat-select-value-text { | ||
38 | + display: block; | ||
39 | + width: 100%; | ||
40 | + } | ||
41 | + | ||
42 | + .tb-bundle-item { | ||
43 | + display: inline-block; | ||
44 | + width: 100%; | ||
45 | + | ||
46 | + span { | ||
47 | + display: inline-block; | ||
48 | + vertical-align: middle; | ||
49 | + } | ||
50 | + | ||
51 | + .tb-bundle-system { | ||
52 | + float: right; | ||
53 | + font-size: .8rem; | ||
54 | + opacity: .8; | ||
55 | + } | ||
56 | + } | ||
57 | + | ||
58 | + mat-option { | ||
59 | + height: auto !important; | ||
60 | + white-space: normal !important; | ||
61 | + } | ||
62 | +} | ||
63 | + | ||
64 | +mat-toolbar { | ||
65 | + tb-widgets-bundle-select { | ||
66 | + mat-select { | ||
67 | + background: rgba(255, 255, 255, .2); | ||
68 | + padding: 5px 20px; | ||
69 | + | ||
70 | + .mat-select-value-text { | ||
71 | + font-size: 1.2rem; | ||
72 | + color: #fff; | ||
73 | + | ||
74 | + span:first-child::after { | ||
75 | + color: #fff; | ||
76 | + } | ||
77 | + } | ||
78 | + | ||
79 | + .mat-select-value.mat-select-placeholder { | ||
80 | + color: #fff; | ||
81 | + opacity: .8; | ||
82 | + } | ||
83 | + } | ||
84 | + | ||
85 | + mat-select.ng-invalid.ng-touched { | ||
86 | + .mat-select-value-text { | ||
87 | + color: #fff !important; | ||
88 | + } | ||
89 | + } | ||
90 | + } | ||
91 | +} |
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 { Component, forwardRef, Input, OnChanges, OnInit, ViewEncapsulation, SimpleChanges } from '@angular/core'; | ||
18 | +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; | ||
19 | +import { Observable } from 'rxjs'; | ||
20 | +import { share, tap } from 'rxjs/operators'; | ||
21 | +import { Store } from '@ngrx/store'; | ||
22 | +import { AppState } from '@app/core/core.state'; | ||
23 | +import { coerceBooleanProperty } from '@angular/cdk/coercion'; | ||
24 | +import { WidgetsBundle } from '@shared/models/widgets-bundle.model'; | ||
25 | +import { WidgetService } from '@core/http/widget.service'; | ||
26 | +import { isDefined } from '@core/utils'; | ||
27 | +import { NULL_UUID } from '@shared/models/id/has-uuid'; | ||
28 | + | ||
29 | +@Component({ | ||
30 | + selector: 'tb-widgets-bundle-select', | ||
31 | + templateUrl: './widgets-bundle-select.component.html', | ||
32 | + styleUrls: ['./widgets-bundle-select.component.scss'], | ||
33 | + providers: [{ | ||
34 | + provide: NG_VALUE_ACCESSOR, | ||
35 | + useExisting: forwardRef(() => WidgetsBundleSelectComponent), | ||
36 | + multi: true | ||
37 | + }], | ||
38 | + encapsulation: ViewEncapsulation.None | ||
39 | +}) | ||
40 | +export class WidgetsBundleSelectComponent implements ControlValueAccessor, OnInit, OnChanges { | ||
41 | + | ||
42 | + @Input() | ||
43 | + bundlesScope: 'system' | 'tenant'; | ||
44 | + | ||
45 | + @Input() | ||
46 | + selectFirstBundle: boolean; | ||
47 | + | ||
48 | + @Input() | ||
49 | + selectBundleAlias: string; | ||
50 | + | ||
51 | + private requiredValue: boolean; | ||
52 | + get required(): boolean { | ||
53 | + return this.requiredValue; | ||
54 | + } | ||
55 | + @Input() | ||
56 | + set required(value: boolean) { | ||
57 | + this.requiredValue = coerceBooleanProperty(value); | ||
58 | + } | ||
59 | + | ||
60 | + @Input() | ||
61 | + disabled: boolean; | ||
62 | + | ||
63 | + widgetsBundles$: Observable<Array<WidgetsBundle>>; | ||
64 | + | ||
65 | + widgetsBundles: Array<WidgetsBundle>; | ||
66 | + | ||
67 | + widgetsBundle: WidgetsBundle | null; | ||
68 | + | ||
69 | + private propagateChange = (v: any) => { }; | ||
70 | + | ||
71 | + constructor(private store: Store<AppState>, | ||
72 | + private widgetService: WidgetService) { | ||
73 | + } | ||
74 | + | ||
75 | + registerOnChange(fn: any): void { | ||
76 | + this.propagateChange = fn; | ||
77 | + } | ||
78 | + | ||
79 | + registerOnTouched(fn: any): void { | ||
80 | + } | ||
81 | + | ||
82 | + ngOnInit() { | ||
83 | + this.widgetsBundles$ = this.getWidgetsBundles().pipe( | ||
84 | + tap((widgetsBundles) => { | ||
85 | + this.widgetsBundles = widgetsBundles; | ||
86 | + if (this.selectFirstBundle) { | ||
87 | + if (widgetsBundles.length > 0) { | ||
88 | + if (this.widgetsBundle !== widgetsBundles[0]) { | ||
89 | + this.widgetsBundle = widgetsBundles[0]; | ||
90 | + this.updateView(); | ||
91 | + } else if (isDefined(this.selectBundleAlias)) { | ||
92 | + this.selectWidgetsBundleByAlias(this.selectBundleAlias); | ||
93 | + } | ||
94 | + } | ||
95 | + } | ||
96 | + }), | ||
97 | + share() | ||
98 | + ); | ||
99 | + } | ||
100 | + | ||
101 | + ngOnChanges(changes: SimpleChanges): void { | ||
102 | + for (const propName of Object.keys(changes)) { | ||
103 | + const change = changes[propName]; | ||
104 | + if (!change.firstChange && change.currentValue !== change.previousValue) { | ||
105 | + if (propName === 'selectBundleAlias') { | ||
106 | + this.selectWidgetsBundleByAlias(this.selectBundleAlias); | ||
107 | + } | ||
108 | + } | ||
109 | + } | ||
110 | + } | ||
111 | + | ||
112 | + setDisabledState(isDisabled: boolean): void { | ||
113 | + this.disabled = isDisabled; | ||
114 | + } | ||
115 | + | ||
116 | + writeValue(value: WidgetsBundle | null): void { | ||
117 | + this.widgetsBundle = value; | ||
118 | + } | ||
119 | + | ||
120 | + widgetsBundleChanged() { | ||
121 | + this.updateView(); | ||
122 | + } | ||
123 | + | ||
124 | + isSystem(item: WidgetsBundle) { | ||
125 | + return item && item.tenantId.id === NULL_UUID; | ||
126 | + } | ||
127 | + | ||
128 | + private selectWidgetsBundleByAlias(alias: string) { | ||
129 | + if (this.widgetsBundles && alias) { | ||
130 | + const found = this.widgetsBundles.find((widgetsBundle) => widgetsBundle.alias === alias); | ||
131 | + if (found && this.widgetsBundle !== found) { | ||
132 | + this.widgetsBundle = found; | ||
133 | + this.updateView(); | ||
134 | + } | ||
135 | + } | ||
136 | + } | ||
137 | + | ||
138 | + private updateView() { | ||
139 | + this.propagateChange(this.widgetsBundle); | ||
140 | + } | ||
141 | + | ||
142 | + private getWidgetsBundles(): Observable<Array<WidgetsBundle>> { | ||
143 | + let widgetsBundlesObservable: Observable<Array<WidgetsBundle>>; | ||
144 | + if (this.bundlesScope) { | ||
145 | + if (this.bundlesScope === 'system') { | ||
146 | + widgetsBundlesObservable = this.widgetService.getSystemWidgetsBundles(); | ||
147 | + } else if (this.bundlesScope === 'tenant') { | ||
148 | + widgetsBundlesObservable = this.widgetService.getTenantWidgetsBundles(); | ||
149 | + } | ||
150 | + } else { | ||
151 | + widgetsBundlesObservable = this.widgetService.getAllWidgetsBundles(); | ||
152 | + } | ||
153 | + return widgetsBundlesObservable; | ||
154 | + } | ||
155 | + | ||
156 | +} |
@@ -94,6 +94,7 @@ import { MatSpinner } from '@angular/material/progress-spinner'; | @@ -94,6 +94,7 @@ import { MatSpinner } from '@angular/material/progress-spinner'; | ||
94 | import { FabToolbarComponent, FabActionsDirective, FabTriggerDirective } from './components/fab-toolbar.component'; | 94 | import { FabToolbarComponent, FabActionsDirective, FabTriggerDirective } from './components/fab-toolbar.component'; |
95 | import { DashboardSelectPanelComponent } from '@shared/components/dashboard-select-panel.component'; | 95 | import { DashboardSelectPanelComponent } from '@shared/components/dashboard-select-panel.component'; |
96 | import { DashboardSelectComponent } from '@shared/components/dashboard-select.component'; | 96 | import { DashboardSelectComponent } from '@shared/components/dashboard-select.component'; |
97 | +import { WidgetsBundleSelectComponent } from './components/widgets-bundle-select.component'; | ||
97 | 98 | ||
98 | @NgModule({ | 99 | @NgModule({ |
99 | providers: [ | 100 | providers: [ |
@@ -145,6 +146,7 @@ import { DashboardSelectComponent } from '@shared/components/dashboard-select.co | @@ -145,6 +146,7 @@ import { DashboardSelectComponent } from '@shared/components/dashboard-select.co | ||
145 | FabTriggerDirective, | 146 | FabTriggerDirective, |
146 | FabActionsDirective, | 147 | FabActionsDirective, |
147 | FabToolbarComponent, | 148 | FabToolbarComponent, |
149 | + WidgetsBundleSelectComponent, | ||
148 | NospacePipe, | 150 | NospacePipe, |
149 | MillisecondsToTimeStringPipe, | 151 | MillisecondsToTimeStringPipe, |
150 | EnumToArrayPipe, | 152 | EnumToArrayPipe, |
@@ -226,6 +228,7 @@ import { DashboardSelectComponent } from '@shared/components/dashboard-select.co | @@ -226,6 +228,7 @@ import { DashboardSelectComponent } from '@shared/components/dashboard-select.co | ||
226 | FabTriggerDirective, | 228 | FabTriggerDirective, |
227 | FabActionsDirective, | 229 | FabActionsDirective, |
228 | FabToolbarComponent, | 230 | FabToolbarComponent, |
231 | + WidgetsBundleSelectComponent, | ||
229 | ValueInputComponent, | 232 | ValueInputComponent, |
230 | MatButtonModule, | 233 | MatButtonModule, |
231 | MatCheckboxModule, | 234 | MatCheckboxModule, |