Commit 10ddb5f8f37581611df15cb154127ce1f4d683bd

Authored by Igor Kulikov
1 parent 67dd34db

Fix timeseries widget (invoke data updated callback from data aggregator on init…

…ial data). Improve widget selector.
... ... @@ -155,6 +155,7 @@ export class DataAggregator {
155 155 }
156 156
157 157 public onData(data: SubscriptionDataHolder, update: boolean, history: boolean, detectChanges: boolean) {
  158 + this.updatedData = true;
158 159 if (!this.dataReceived || this.resetPending) {
159 160 let updateIntervalScheduledTime = true;
160 161 if (!this.dataReceived) {
... ... @@ -183,7 +184,6 @@ export class DataAggregator {
183 184 this.onInterval(history, detectChanges);
184 185 }
185 186 }
186   - this.updatedData = true;
187 187 }
188 188
189 189 private onInterval(history?: boolean, detectChanges?: boolean) {
... ...
... ... @@ -55,6 +55,8 @@ export class WidgetService {
55 55 private systemWidgetsBundles: Array<WidgetsBundle>;
56 56 private tenantWidgetsBundles: Array<WidgetsBundle>;
57 57
  58 + private widgetTypeInfosCache = new Map<string, Array<WidgetTypeInfo>>();
  59 +
58 60 private loadWidgetsBundleCacheSubject: ReplaySubject<any>;
59 61
60 62 constructor(
... ... @@ -137,8 +139,15 @@ export class WidgetService {
137 139
138 140 public getBundleWidgetTypeInfos(bundleAlias: string, isSystem: boolean,
139 141 config?: RequestConfig): Observable<Array<WidgetTypeInfo>> {
140   - return this.http.get<Array<WidgetTypeInfo>>(`/api/widgetTypesInfos?isSystem=${isSystem}&bundleAlias=${bundleAlias}`,
141   - defaultHttpOptionsFromConfig(config));
  142 + const key = bundleAlias + (isSystem ? '_sys' : '');
  143 + if (this.widgetTypeInfosCache.has(key)) {
  144 + return of(this.widgetTypeInfosCache.get(key));
  145 + } else {
  146 + return this.http.get<Array<WidgetTypeInfo>>(`/api/widgetTypesInfos?isSystem=${isSystem}&bundleAlias=${bundleAlias}`,
  147 + defaultHttpOptionsFromConfig(config)).pipe(
  148 + tap((res) => this.widgetTypeInfosCache.set(key, res) )
  149 + );
  150 + }
142 151 }
143 152
144 153 public loadBundleLibraryWidgets(bundleAlias: string, isSystem: boolean,
... ... @@ -305,6 +314,7 @@ export class WidgetService {
305 314 this.systemWidgetsBundles = undefined;
306 315 this.tenantWidgetsBundles = undefined;
307 316 this.loadWidgetsBundleCacheSubject = undefined;
  317 + this.widgetTypeInfosCache.clear();
308 318 }
309 319
310 320 }
... ...
... ... @@ -242,8 +242,9 @@
242 242 </tb-edit-widget>
243 243 </tb-details-panel>
244 244 <tb-details-panel *ngIf="!isAddingWidgetClosed && !widgetEditMode" fxFlex
245   - headerTitle="{{
246   - (!widgetsBundle?.title ? 'widget.select-widgets-bundle' : 'dashboard.select-widget-value') | translate: widgetsBundle
  245 + headerTitle="{{ isAddingWidget ?
  246 + ((!dashboardWidgetSelectComponent?.widgetsBundle ?
  247 + 'widget.select-widgets-bundle' : 'dashboard.select-widget-value') | translate: dashboardWidgetSelectComponent?.widgetsBundle) : ''
247 248 }}"
248 249 headerHeightPx="64"
249 250 [isShowSearch]="true"
... ... @@ -252,34 +253,33 @@
252 253 backgroundColor="#cfd8dc"
253 254 (closeDetails)="onAddWidgetClosed()"
254 255 (closeSearch)="onCloseSearchBundle()">
255   - <div class="prefix-title-buttons" [fxHide]="!widgetsBundle?.title" style="height: 28px; margin-right: 12px">
256   - <button class="tb-mat-28" mat-icon-button type="button" (click)="widgetBundleSelected(null)">
  256 + <div class="prefix-title-buttons" [fxShow]="(isAddingWidget && dashboardWidgetSelectComponent?.widgetsBundle) ? true : false" style="height: 28px; margin-right: 12px">
  257 + <button class="tb-mat-28" mat-icon-button type="button" (click)="clearSelectedWidgetBundle()">
257 258 <mat-icon>arrow_back</mat-icon>
258 259 </button>
259 260 </div>
260 261 <div class="search-pane" *ngIf="isAddingWidget" fxLayout="row">
261 262 <tb-widgets-bundle-search fxFlex
262 263 [(ngModel)]="searchBundle"
263   - placeholder="{{ (!widgetsBundle?.title ? 'widgets-bundle.search' : 'widget.search') | translate }}"
  264 + placeholder="{{ (!dashboardWidgetSelectComponent?.widgetsBundle ? 'widgets-bundle.search' : 'widget.search') | translate }}"
264 265 (ngModelChange)="searchBundle = $event">
265 266 </tb-widgets-bundle-search>
266 267 </div>
267 268 <div class="details-buttons" *ngIf="isAddingWidget">
268 269 <button mat-button mat-icon-button type="button"
269   - *ngIf="widgetTypes.length > 1"
  270 + *ngIf="dashboardWidgetSelectComponent?.widgetTypes.size > 1"
270 271 (click)="editWidgetsTypesToDisplay($event)"
271 272 matTooltip="{{ 'widget.filter' | translate }}"
272 273 matTooltipPosition="above">
273 274 <mat-icon>filter_list</mat-icon>
274 275 </button>
275 276 </div>
276   - <tb-dashboard-widget-select *ngIf="isAddingWidget"
  277 + <tb-dashboard-widget-select #dashboardWidgetSelect
  278 + *ngIf="isAddingWidget"
277 279 [aliasController]="dashboardCtx.aliasController"
278   - [widgetsBundle]="widgetsBundle"
279 280 [searchBundle]="searchBundle"
280 281 [filterWidgetTypes]="filterWidgetTypes"
281   - (widgetsTypes)="updateWidgetsTypes($event)"
282   - (widgetsBundleSelected)="widgetBundleSelected($event)"
  282 + (widgetsBundleSelected)="widgetBundleSelected()"
283 283 (widgetSelected)="addWidgetFromType($event)">
284 284 </tb-dashboard-widget-select>
285 285 </tb-details-panel>
... ...
... ... @@ -121,6 +121,7 @@ import {
121 121 DisplayWidgetTypesPanelData,
122 122 WidgetTypes
123 123 } from '@home/components/dashboard-page/widget-types-panel.component';
  124 +import { DashboardWidgetSelectComponent } from '@home/components/dashboard-page/dashboard-widget-select.component';
124 125
125 126 // @dynamic
126 127 @Component({
... ... @@ -167,9 +168,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
167 168 forceDashboardMobileMode = false;
168 169 isAddingWidget = false;
169 170 isAddingWidgetClosed = true;
170   - widgetsBundle: WidgetsBundle = null;
171 171 searchBundle = '';
172   - widgetTypes: WidgetTypes[] = [];
173 172 filterWidgetTypes: widgetType[] = null;
174 173
175 174 isToolbarOpened = false;
... ... @@ -267,6 +266,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
267 266
268 267 @ViewChild('tbEditWidget') editWidgetComponent: EditWidgetComponent;
269 268
  269 + @ViewChild('dashboardWidgetSelect') dashboardWidgetSelectComponent: DashboardWidgetSelectComponent;
  270 +
270 271 constructor(protected store: Store<AppState>,
271 272 @Inject(WINDOW) private window: Window,
272 273 private breakpointObserver: BreakpointObserver,
... ... @@ -367,7 +368,6 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
367 368 this.forceDashboardMobileMode = false;
368 369 this.isAddingWidget = false;
369 370 this.isAddingWidgetClosed = true;
370   - this.widgetsBundle = null;
371 371
372 372 this.isToolbarOpened = false;
373 373 this.isToolbarOpenedAnimate = false;
... ... @@ -885,7 +885,6 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
885 885
886 886 addWidgetFromType(widget: WidgetInfo) {
887 887 this.onAddWidgetClosed();
888   - this.widgetTypes = [];
889 888 this.searchBundle = '';
890 889 this.widgetComponentService.getWidgetInfo(widget.bundleAlias, widget.typeAlias, widget.isSystemType).subscribe(
891 890 (widgetTypeInfo) => {
... ... @@ -1153,16 +1152,13 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
1153 1152 return widgetContextActions;
1154 1153 }
1155 1154
1156   - widgetBundleSelected(bundle: WidgetsBundle){
1157   - this.widgetsBundle = bundle;
1158   - this.widgetTypes = [];
  1155 + widgetBundleSelected(){
1159 1156 this.searchBundle = '';
1160 1157 }
1161 1158
1162   - updateWidgetsTypes(types: Set<widgetType>) {
1163   - this.widgetTypes = Array.from(types.values()).map(type => {
1164   - return {type, display: true};
1165   - });
  1159 + clearSelectedWidgetBundle() {
  1160 + this.searchBundle = '';
  1161 + this.dashboardWidgetSelectComponent.widgetsBundle = null;
1166 1162 }
1167 1163
1168 1164 editWidgetsTypesToDisplay($event: Event) {
... ... @@ -1191,7 +1187,9 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
1191 1187 {
1192 1188 provide: DISPLAY_WIDGET_TYPES_PANEL_DATA,
1193 1189 useValue: {
1194   - types: this.widgetTypes,
  1190 + types: Array.from(this.dashboardWidgetSelectComponent.widgetTypes.values()).map(type => {
  1191 + return {type, display: true};
  1192 + }),
1195 1193 typesUpdated: (newTypes) => {
1196 1194 this.filterWidgetTypes = newTypes.filter(type => type.display).map(type => type.type);
1197 1195 }
... ...
... ... @@ -17,7 +17,7 @@
17 17 -->
18 18 <div class="widget-select">
19 19 <div *ngIf="widgetsBundle; else bundles">
20   - <div *ngIf="(widgets$ | async)?.length; else emptyBundle" fxFlexFill fxLayoutGap="12px grid" fxLayout="row wrap">
  20 + <div *ngIf="(widgets$ | async)?.length; else loadingWidgets" fxFlexFill fxLayoutGap="12px grid" fxLayout="row wrap">
21 21 <div *ngFor="let widget of widgets$ | async" class="mat-card-container">
22 22 <mat-card fxFlexFill fxLayout="row" fxLayoutGap="16px" (click)="onWidgetClicked($event, widget)">
23 23 <div class="preview-container" fxFlex="45">
... ... @@ -33,15 +33,24 @@
33 33 </mat-card>
34 34 </div>
35 35 </div>
36   - <ng-template #emptyBundle>
37   - <span translate
38   - style="display: flex;"
39   - fxLayoutAlign="center center"
40   - class="mat-headline tb-absolute-fill">widgets-bundle.empty</span>
  36 + <ng-template #loadingWidgets>
  37 + <div *ngIf="loadingWidgets$ | async; else emptyBundle" fxLayout="column"
  38 + fxLayoutAlign="center center" class="tb-absolute-fill">
  39 + <span class="mat-headline" style="padding-bottom: 20px;">
  40 + {{ 'widget.loading-widgets' | translate }}
  41 + </span>
  42 + <mat-spinner color="accent" strokeWidth="5"></mat-spinner>
  43 + </div>
  44 + <ng-template #emptyBundle>
  45 + <span translate
  46 + style="display: flex;"
  47 + fxLayoutAlign="center center"
  48 + class="mat-headline tb-absolute-fill">widgets-bundle.empty</span>
  49 + </ng-template>
41 50 </ng-template>
42 51 </div>
43 52 <ng-template #bundles>
44   - <div fxFlexFill fxLayoutGap="12px grid" fxLayout="row wrap">
  53 + <div *ngIf="(widgetsBundles$ | async)?.length; else loadingWidgetBundles" fxFlexFill fxLayoutGap="12px grid" fxLayout="row wrap">
45 54 <div *ngFor="let widgetsBundle of widgetsBundles$ | async" class="mat-card-container">
46 55 <mat-card fxFlexFill fxLayout="row" fxLayoutGap="16px" (click)="selectBundle($event, widgetsBundle)">
47 56 <div class="preview-container" fxFlex="45">
... ... @@ -57,5 +66,20 @@
57 66 </mat-card>
58 67 </div>
59 68 </div>
  69 + <ng-template #loadingWidgetBundles>
  70 + <div *ngIf="loadingWidgetBundles$ | async; else noWidgetBundles" fxLayout="column"
  71 + fxLayoutAlign="center center" class="tb-absolute-fill">
  72 + <span class="mat-headline" style="padding-bottom: 20px;">
  73 + {{ 'widgets-bundle.loading-widgets-bundles' | translate }}
  74 + </span>
  75 + <mat-spinner strokeWidth="5"></mat-spinner>
  76 + </div>
  77 + <ng-template #noWidgetBundles>
  78 + <span translate
  79 + style="display: flex;"
  80 + fxLayoutAlign="center center"
  81 + class="mat-headline tb-absolute-fill">widgets-bundle.no-widgets-bundles-text</span>
  82 + </ng-template>
  83 + </ng-template>
60 84 </ng-template>
61 85 </div>
... ...
... ... @@ -29,8 +29,15 @@
29 29 flex: 0 0 100%;
30 30 max-width: 100%;
31 31
  32 + &:hover {
  33 + .mat-card {
  34 + box-shadow: 0 2px 6px 6px rgb(0 0 0 / 20%), 0 1px 4px 2px rgb(0 0 0 / 14%), 0 1px 6px 0 rgb(0 0 0 / 12%)
  35 + }
  36 + }
  37 +
32 38 .mat-card {
33 39 cursor: pointer;
  40 + transition: box-shadow 0.2s;
34 41
35 42 .preview-container {
36 43 text-align: center;
... ...
... ... @@ -20,8 +20,7 @@ import { IAliasController } from '@core/api/widget-api.models';
20 20 import { NULL_UUID } from '@shared/models/id/has-uuid';
21 21 import { WidgetService } from '@core/http/widget.service';
22 22 import { WidgetInfo, widgetType } from '@shared/models/widget.models';
23   -import { toWidgetInfo } from '@home/models/widget-component.models';
24   -import { distinctUntilChanged, map, publishReplay, refCount, switchMap, tap } from 'rxjs/operators';
  23 +import { distinctUntilChanged, map, publishReplay, refCount, share, switchMap, tap } from 'rxjs/operators';
25 24 import { BehaviorSubject, combineLatest, Observable, of } from 'rxjs';
26 25 import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
27 26 import { isDefinedAndNotNull } from '@core/utils';
... ... @@ -37,18 +36,28 @@ export class DashboardWidgetSelectComponent implements OnInit {
37 36 private filterWidgetTypes$ = new BehaviorSubject<Array<widgetType>>(null);
38 37 private widgetsInfo: Observable<Array<WidgetInfo>>;
39 38 private widgetsBundleValue: WidgetsBundle;
40   - private widgetsType = new Set<widgetType>();
  39 + widgetTypes = new Set<widgetType>();
41 40
42 41 widgets$: Observable<Array<WidgetInfo>>;
  42 + loadingWidgetsSubject: BehaviorSubject<boolean> = new BehaviorSubject(false);
  43 + loadingWidgets$ = this.loadingWidgetsSubject.pipe(
  44 + share()
  45 + );
43 46 widgetsBundles$: Observable<Array<WidgetsBundle>>;
  47 + loadingWidgetBundlesSubject: BehaviorSubject<boolean> = new BehaviorSubject(true);
  48 + loadingWidgetBundles$ = this.loadingWidgetBundlesSubject.pipe(
  49 + share()
  50 + );
44 51
45   - @Input()
46 52 set widgetsBundle(widgetBundle: WidgetsBundle) {
47   - this.widgetsInfo = null;
48   - this.widgetsType.clear();
49   - this.widgetsTypes.emit(this.widgetsType);
50   - this.filterWidgetTypes$.next(null);
51   - this.widgetsBundleValue = widgetBundle;
  53 + if (this.widgetsBundleValue !== widgetBundle) {
  54 + this.widgetsBundleValue = widgetBundle;
  55 + if (widgetBundle === null) {
  56 + this.widgetTypes.clear();
  57 + }
  58 + this.filterWidgetTypes$.next(null);
  59 + this.widgetsInfo = null;
  60 + }
52 61 }
53 62
54 63 get widgetsBundle(): WidgetsBundle {
... ... @@ -74,14 +83,8 @@ export class DashboardWidgetSelectComponent implements OnInit {
74 83 @Output()
75 84 widgetsBundleSelected: EventEmitter<WidgetsBundle> = new EventEmitter<WidgetsBundle>();
76 85
77   - @Output()
78   - widgetsTypes: EventEmitter<Set<widgetType>> = new EventEmitter<Set<widgetType>>();
79   -
80 86 constructor(private widgetsService: WidgetService,
81 87 private sanitizer: DomSanitizer) {
82   - }
83   -
84   - ngOnInit(): void {
85 88 this.widgetsBundles$ = this.search$.asObservable().pipe(
86 89 distinctUntilChanged(),
87 90 switchMap(search => this.fetchWidgetBundle(search))
... ... @@ -92,27 +95,41 @@ export class DashboardWidgetSelectComponent implements OnInit {
92 95 );
93 96 }
94 97
  98 + ngOnInit(): void {
  99 + }
  100 +
95 101 private getWidgets(): Observable<Array<WidgetInfo>> {
96 102 if (!this.widgetsInfo) {
97 103 if (this.widgetsBundle !== null) {
98 104 const bundleAlias = this.widgetsBundle.alias;
99 105 const isSystem = this.widgetsBundle.tenantId.id === NULL_UUID;
  106 + this.loadingWidgetsSubject.next(true);
100 107 this.widgetsInfo = this.widgetsService.getBundleWidgetTypeInfos(bundleAlias, isSystem).pipe(
101   - map(widgets => widgets.sort((a, b) => b.createdTime - a.createdTime)),
102   - map(widgets => widgets.map((widgetTypeInfo) => {
103   - this.widgetsType.add(widgetTypeInfo.widgetType);
104   - const widget: WidgetInfo = {
105   - isSystemType: isSystem,
106   - bundleAlias,
107   - typeAlias: widgetTypeInfo.alias,
108   - type: widgetTypeInfo.widgetType,
109   - title: widgetTypeInfo.name,
110   - image: widgetTypeInfo.image,
111   - description: widgetTypeInfo.description
112   - };
113   - return widget;
114   - })),
115   - tap(() => this.widgetsTypes.emit(this.widgetsType)),
  108 + map(widgets => {
  109 + widgets = widgets.sort((a, b) => b.createdTime - a.createdTime);
  110 + const widgetTypes = new Set<widgetType>();
  111 + const widgetInfos = widgets.map((widgetTypeInfo) => {
  112 + widgetTypes.add(widgetTypeInfo.widgetType);
  113 + const widget: WidgetInfo = {
  114 + isSystemType: isSystem,
  115 + bundleAlias,
  116 + typeAlias: widgetTypeInfo.alias,
  117 + type: widgetTypeInfo.widgetType,
  118 + title: widgetTypeInfo.name,
  119 + image: widgetTypeInfo.image,
  120 + description: widgetTypeInfo.description
  121 + };
  122 + return widget;
  123 + }
  124 + );
  125 + setTimeout(() => {
  126 + this.widgetTypes = widgetTypes;
  127 + });
  128 + return widgetInfos;
  129 + }),
  130 + tap(() => {
  131 + this.loadingWidgetsSubject.next(false);
  132 + }),
116 133 publishReplay(1),
117 134 refCount()
118 135 );
... ... @@ -147,6 +164,7 @@ export class DashboardWidgetSelectComponent implements OnInit {
147 164
148 165 private getWidgetsBundle(): Observable<Array<WidgetsBundle>> {
149 166 return this.widgetsService.getAllWidgetsBundles().pipe(
  167 + tap(() => this.loadingWidgetBundlesSubject.next(false)),
150 168 publishReplay(1),
151 169 refCount()
152 170 );
... ...
... ... @@ -2266,7 +2266,8 @@
2266 2266 "data-overflow": "Widget displays {{count}} out of {{total}} entities",
2267 2267 "alarm-data-overflow": "Widget displays alarms for {{allowedEntities}} (maximum allowed) entities out of {{totalEntities}} entities",
2268 2268 "search": "Search widget",
2269   - "filter": "Widget filter type"
  2269 + "filter": "Widget filter type",
  2270 + "loading-widgets": "Loading widgets..."
2270 2271 },
2271 2272 "widget-action": {
2272 2273 "header-button": "Widget header button",
... ... @@ -2318,7 +2319,8 @@
2318 2319 "invalid-widgets-bundle-file-error": "Unable to import widgets bundle: Invalid widgets bundle data structure.",
2319 2320 "search": "Search widget bundles",
2320 2321 "selected-widgets-bundles": "{ count, plural, 1 {1 widgets bundle} other {# widgets bundles} } selected",
2321   - "open-widgets-bundle": "Open widgets bundle"
  2322 + "open-widgets-bundle": "Open widgets bundle",
  2323 + "loading-widgets-bundles": "Loading widgets bundles..."
2322 2324 },
2323 2325 "widget-config": {
2324 2326 "data": "Data",
... ...