Commit a3e2106888e22c4fc372174e7a04a705f6fddb5a

Authored by Vladyslav_Prykhodko
1 parent 55b5b3f2

UI: Added filter widget type

... ... @@ -248,11 +248,14 @@
248 248 headerHeightPx="120"
249 249 [isReadOnly]="true"
250 250 [isEdit]="false"
251   - [showBackStateIcon]="!!widgetsBundle?.title"
252   - (closeDetails)="onAddWidgetClosed()"
253   - (backStateDetails)="widgetBundleSelected(null)">
  251 + (closeDetails)="onAddWidgetClosed()">
  252 + <div class="prefix-title-buttons" [fxHide]="!widgetsBundle?.title" style="height: 28px; margin-right: 12px">
  253 + <button class="tb-mat-28" mat-icon-button type="button" (click)="widgetBundleSelected(null)">
  254 + <mat-icon>arrow_back</mat-icon>
  255 + </button>
  256 + </div>
254 257 <div class="header-pane" *ngIf="isAddingWidget">
255   - <div fxLayout="row">
  258 + <div fxLayout="row" fxLayoutGap="12px">
256 259 <tb-widgets-bundle-search fxFlex
257 260 [(ngModel)]="searchBundle"
258 261 placeholder="{{ (!widgetsBundle?.title ? 'widgets-bundle.search' : 'widget.search') | translate }}"
... ... @@ -260,10 +263,21 @@
260 263 </tb-widgets-bundle-search>
261 264 </div>
262 265 </div>
  266 + <div class="details-buttons" *ngIf="isAddingWidget">
  267 + <button mat-button mat-icon-button type="button"
  268 + *ngIf="widgetTypes.length > 1"
  269 + (click)="editWidgetsTypesToDisplay($event)"
  270 + matTooltip="{{ 'widget.filter' | translate }}"
  271 + matTooltipPosition="above">
  272 + <mat-icon>filter_list</mat-icon>
  273 + </button>
  274 + </div>
263 275 <tb-dashboard-widget-select *ngIf="isAddingWidget"
264 276 [aliasController]="dashboardCtx.aliasController"
265 277 [widgetsBundle]="widgetsBundle"
266 278 [searchBundle]="searchBundle"
  279 + [filterWidgetTypes]="filterWidgetTypes"
  280 + (widgetsTypes)="updateWidgetsTypes($event)"
267 281 (widgetsBundleSelected)="widgetBundleSelected($event)"
268 282 (widgetSelected)="addWidgetFromType($event)">
269 283 </tb-dashboard-widget-select>
... ...
... ... @@ -18,11 +18,14 @@ import {
18 18 ChangeDetectorRef,
19 19 Component,
20 20 Inject,
  21 + Injector,
21 22 Input,
22 23 NgZone,
23 24 OnDestroy,
24 25 OnInit,
  26 + StaticProvider,
25 27 ViewChild,
  28 + ViewContainerRef,
26 29 ViewEncapsulation
27 30 } from '@angular/core';
28 31 import { PageComponent } from '@shared/components/page.component';
... ... @@ -58,7 +61,14 @@ import { BreakpointObserver, BreakpointState } from '@angular/cdk/layout';
58 61 import { MediaBreakpoints } from '@shared/models/constants';
59 62 import { AuthUser } from '@shared/models/user.model';
60 63 import { getCurrentAuthState } from '@core/auth/auth.selectors';
61   -import { Widget, WidgetConfig, WidgetInfo, WidgetPosition, widgetTypesData } from '@app/shared/models/widget.models';
  64 +import {
  65 + Widget,
  66 + WidgetConfig,
  67 + WidgetInfo,
  68 + WidgetPosition,
  69 + widgetType,
  70 + widgetTypesData
  71 +} from '@shared/models/widget.models';
62 72 import { environment as env } from '@env/environment';
63 73 import { Authority } from '@shared/models/authority.enum';
64 74 import { DialogService } from '@core/services/dialog.service';
... ... @@ -80,7 +90,10 @@ import {
80 90 import { EntityAliases } from '@app/shared/models/alias.models';
81 91 import { EditWidgetComponent } from '@home/components/dashboard-page/edit-widget.component';
82 92 import { WidgetsBundle } from '@shared/models/widgets-bundle.model';
83   -import { AddWidgetDialogComponent, AddWidgetDialogData } from '@home/components/dashboard-page/add-widget-dialog.component';
  93 +import {
  94 + AddWidgetDialogComponent,
  95 + AddWidgetDialogData
  96 +} from '@home/components/dashboard-page/add-widget-dialog.component';
84 97 import { TranslateService } from '@ngx-translate/core';
85 98 import {
86 99 ManageDashboardLayoutsDialogComponent,
... ... @@ -99,6 +112,14 @@ import { ImportExportService } from '@home/components/import-export/import-expor
99 112 import { AuthState } from '@app/core/auth/auth.models';
100 113 import { FiltersDialogComponent, FiltersDialogData } from '@home/components/filter/filters-dialog.component';
101 114 import { Filters } from '@shared/models/query/query.models';
  115 +import { ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay';
  116 +import { ComponentPortal } from '@angular/cdk/portal';
  117 +import {
  118 + DISPLAY_WIDGET_TYPES_PANEL_DATA,
  119 + DisplayWidgetTypesPanelComponent,
  120 + DisplayWidgetTypesPanelData,
  121 + WidgetTypes
  122 +} from '@home/components/dashboard-page/widget-types-panel.component';
102 123
103 124 // @dynamic
104 125 @Component({
... ... @@ -147,6 +168,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
147 168 isAddingWidgetClosed = true;
148 169 widgetsBundle: WidgetsBundle = null;
149 170 searchBundle = '';
  171 + widgetTypes: WidgetTypes[] = [];
  172 + filterWidgetTypes: widgetType[] = null;
150 173
151 174 isToolbarOpened = false;
152 175 isToolbarOpenedAnimate = false;
... ... @@ -261,6 +284,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
261 284 private dialog: MatDialog,
262 285 private translate: TranslateService,
263 286 private ngZone: NgZone,
  287 + private overlay: Overlay,
  288 + private viewContainerRef: ViewContainerRef,
264 289 private cd: ChangeDetectorRef) {
265 290 super(store);
266 291
... ... @@ -1121,6 +1146,55 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
1121 1146
1122 1147 widgetBundleSelected(bundle: WidgetsBundle){
1123 1148 this.widgetsBundle = bundle;
  1149 + this.widgetTypes = [];
1124 1150 this.searchBundle = '';
1125 1151 }
  1152 +
  1153 + updateWidgetsTypes(types: Set<widgetType>) {
  1154 + this.widgetTypes = Array.from(types.values()).map(type => {
  1155 + return {type, display: true};
  1156 + });
  1157 + }
  1158 +
  1159 + editWidgetsTypesToDisplay($event: Event) {
  1160 + if ($event) {
  1161 + $event.stopPropagation();
  1162 + }
  1163 + const target = $event.target || $event.currentTarget;
  1164 + const config = new OverlayConfig();
  1165 + config.backdropClass = 'cdk-overlay-transparent-backdrop';
  1166 + config.hasBackdrop = true;
  1167 + const connectedPosition: ConnectedPosition = {
  1168 + originX: 'end',
  1169 + originY: 'bottom',
  1170 + overlayX: 'end',
  1171 + overlayY: 'top'
  1172 + };
  1173 + config.positionStrategy = this.overlay.position().flexibleConnectedTo(target as HTMLElement)
  1174 + .withPositions([connectedPosition]);
  1175 +
  1176 + const overlayRef = this.overlay.create(config);
  1177 + overlayRef.backdropClick().subscribe(() => {
  1178 + overlayRef.dispose();
  1179 + });
  1180 +
  1181 + const providers: StaticProvider[] = [
  1182 + {
  1183 + provide: DISPLAY_WIDGET_TYPES_PANEL_DATA,
  1184 + useValue: {
  1185 + types: this.widgetTypes,
  1186 + typesUpdated: (newTypes) => {
  1187 + this.filterWidgetTypes = newTypes.filter(type => type.display).map(type => type.type);
  1188 + }
  1189 + } as DisplayWidgetTypesPanelData
  1190 + },
  1191 + {
  1192 + provide: OverlayRef,
  1193 + useValue: overlayRef
  1194 + }
  1195 + ];
  1196 + const injector = Injector.create({parent: this.viewContainerRef.injector, providers});
  1197 + overlayRef.attach(new ComponentPortal(DisplayWidgetTypesPanelComponent, this.viewContainerRef, injector));
  1198 + this.cd.detectChanges();
  1199 + }
1126 1200 }
... ...
... ... @@ -19,10 +19,10 @@ import { WidgetsBundle } from '@shared/models/widgets-bundle.model';
19 19 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   -import { WidgetInfo } from '@shared/models/widget.models';
  22 +import { WidgetInfo, widgetType } from '@shared/models/widget.models';
23 23 import { toWidgetInfo } from '@home/models/widget-component.models';
24   -import { distinctUntilChanged, map, publishReplay, refCount, switchMap } from 'rxjs/operators';
25   -import { BehaviorSubject, Observable, of } from 'rxjs';
  24 +import { distinctUntilChanged, map, publishReplay, refCount, switchMap, tap } from 'rxjs/operators';
  25 +import { BehaviorSubject, combineLatest, Observable, of } from 'rxjs';
26 26 import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
27 27 import { isDefinedAndNotNull } from '@core/utils';
28 28
... ... @@ -34,15 +34,20 @@ import { isDefinedAndNotNull } from '@core/utils';
34 34 export class DashboardWidgetSelectComponent implements OnInit {
35 35
36 36 private search$ = new BehaviorSubject<string>('');
37   - private widgetsTypes: Observable<Array<WidgetInfo>>;
  37 + private filterWidgetTypes$ = new BehaviorSubject<Array<widgetType>>(null);
  38 + private widgetsInfo: Observable<Array<WidgetInfo>>;
38 39 private widgetsBundleValue: WidgetsBundle;
  40 + private widgetsType = new Set<widgetType>();
39 41
40 42 widgets$: Observable<Array<WidgetInfo>>;
41 43 widgetsBundles$: Observable<Array<WidgetsBundle>>;
42 44
43 45 @Input()
44 46 set widgetsBundle(widgetBundle: WidgetsBundle) {
45   - this.widgetsTypes = null;
  47 + this.widgetsInfo = null;
  48 + this.widgetsType.clear();
  49 + this.widgetsTypes.emit(this.widgetsType);
  50 + this.filterWidgetTypes$.next(null);
46 51 this.widgetsBundleValue = widgetBundle;
47 52 }
48 53
... ... @@ -58,12 +63,20 @@ export class DashboardWidgetSelectComponent implements OnInit {
58 63 this.search$.next(search);
59 64 }
60 65
  66 + @Input()
  67 + set filterWidgetTypes(widgetTypes: Array<widgetType>) {
  68 + this.filterWidgetTypes$.next(widgetTypes);
  69 + }
  70 +
61 71 @Output()
62 72 widgetSelected: EventEmitter<WidgetInfo> = new EventEmitter<WidgetInfo>();
63 73
64 74 @Output()
65 75 widgetsBundleSelected: EventEmitter<WidgetsBundle> = new EventEmitter<WidgetsBundle>();
66 76
  77 + @Output()
  78 + widgetsTypes: EventEmitter<Set<widgetType>> = new EventEmitter<Set<widgetType>>();
  79 +
67 80 constructor(private widgetsService: WidgetService,
68 81 private sanitizer: DomSanitizer) {
69 82 }
... ... @@ -73,21 +86,22 @@ export class DashboardWidgetSelectComponent implements OnInit {
73 86 distinctUntilChanged(),
74 87 switchMap(search => this.fetchWidgetBundle(search))
75 88 );
76   - this.widgets$ = this.search$.asObservable().pipe(
77   - distinctUntilChanged(),
78   - switchMap(search => this.fetchWidget(search))
  89 + this.widgets$ = combineLatest([this.search$.asObservable(), this.filterWidgetTypes$.asObservable()]).pipe(
  90 + distinctUntilChanged((oldValue, newValue) => JSON.stringify(oldValue) === JSON.stringify(newValue)),
  91 + switchMap(search => this.fetchWidget(...search))
79 92 );
80 93 }
81 94
82 95 private getWidgets(): Observable<Array<WidgetInfo>> {
83   - if (!this.widgetsTypes) {
  96 + if (!this.widgetsInfo) {
84 97 if (this.widgetsBundle !== null) {
85 98 const bundleAlias = this.widgetsBundle.alias;
86 99 const isSystem = this.widgetsBundle.tenantId.id === NULL_UUID;
87   - this.widgetsTypes = this.widgetsService.getBundleWidgetTypes(bundleAlias, isSystem).pipe(
  100 + this.widgetsInfo = this.widgetsService.getBundleWidgetTypes(bundleAlias, isSystem).pipe(
88 101 map(widgets => widgets.sort((a, b) => b.createdTime - a.createdTime)),
89 102 map(widgets => widgets.map((type) => {
90 103 const widgetTypeInfo = toWidgetInfo(type);
  104 + this.widgetsType.add(widgetTypeInfo.type);
91 105 const widget: WidgetInfo = {
92 106 isSystemType: isSystem,
93 107 bundleAlias,
... ... @@ -99,14 +113,15 @@ export class DashboardWidgetSelectComponent implements OnInit {
99 113 };
100 114 return widget;
101 115 })),
  116 + tap(() => this.widgetsTypes.emit(this.widgetsType)),
102 117 publishReplay(1),
103 118 refCount()
104 119 );
105 120 } else {
106   - this.widgetsTypes = of([]);
  121 + this.widgetsInfo = of([]);
107 122 }
108 123 }
109   - return this.widgetsTypes;
  124 + return this.widgetsInfo;
110 125 }
111 126
112 127 onWidgetClicked($event: Event, widget: WidgetInfo): void {
... ... @@ -149,8 +164,9 @@ export class DashboardWidgetSelectComponent implements OnInit {
149 164 );
150 165 }
151 166
152   - private fetchWidget(search: string): Observable<Array<WidgetInfo>> {
  167 + private fetchWidget(search: string, filter: widgetType[]): Observable<Array<WidgetInfo>> {
153 168 return this.getWidgets().pipe(
  169 + map(widgets => filter ? widgets.filter((widget) => filter.includes(widget.type)) : widgets),
154 170 map(widgets => search ? widgets.filter(
155 171 widget => (
156 172 widget.title?.toLowerCase().includes(search.toLowerCase()) ||
... ...
  1 +<!--
  2 +
  3 + Copyright © 2016-2021 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 +<div fxLayout="column" class="mat-content mat-padding">
  19 + <mat-checkbox style="padding-bottom: 8px;" [(ngModel)]="type.display" *ngFor="let type of types"
  20 + (ngModelChange)="update()">
  21 + {{ 'widget.' + type.type | translate }}
  22 + </mat-checkbox>
  23 +</div>
... ...
  1 +/**
  2 + * Copyright © 2016-2021 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 +:host {
  17 + width: 100%;
  18 + height: 100%;
  19 + min-width: 200px;
  20 + overflow: hidden;
  21 + background: #fff;
  22 + border-radius: 4px;
  23 + box-shadow:
  24 + 0 7px 8px -4px rgba(0, 0, 0, .2),
  25 + 0 13px 19px 2px rgba(0, 0, 0, .14),
  26 + 0 5px 24px 4px rgba(0, 0, 0, .12);
  27 +
  28 + .mat-content {
  29 + overflow: hidden;
  30 + background-color: #fff;
  31 + }
  32 +
  33 + .mat-padding {
  34 + padding: 16px;
  35 + }
  36 +}
... ...
  1 +///
  2 +/// Copyright © 2016-2021 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, Inject, InjectionToken } from '@angular/core';
  18 +import { widgetType } from '@shared/models/widget.models';
  19 +
  20 +export const DISPLAY_WIDGET_TYPES_PANEL_DATA = new InjectionToken<any>('DisplayWidgetTypesPanelData');
  21 +
  22 +export interface WidgetTypes {
  23 + type: widgetType;
  24 + display: boolean;
  25 +}
  26 +
  27 +export interface DisplayWidgetTypesPanelData {
  28 + types: WidgetTypes[];
  29 + typesUpdated: (columns: WidgetTypes[]) => void;
  30 +}
  31 +
  32 +@Component({
  33 + selector: 'tb-widget-types-panel',
  34 + templateUrl: './widget-types-panel.component.html',
  35 + styleUrls: ['./widget-types-panel.component.scss']
  36 +})
  37 +export class DisplayWidgetTypesPanelComponent {
  38 +
  39 + types: WidgetTypes[];
  40 +
  41 + constructor(@Inject(DISPLAY_WIDGET_TYPES_PANEL_DATA) public data: DisplayWidgetTypesPanelData) {
  42 + this.types = this.data.types;
  43 + }
  44 +
  45 + public update() {
  46 + this.data.typesUpdated(this.types);
  47 + }
  48 +}
... ...
... ... @@ -19,10 +19,8 @@
19 19 <mat-toolbar class="details-toolbar" color="primary" [ngStyle]="{height: headerHeightPx+'px'}">
20 20 <div class="mat-toolbar-tools" fxLayout="row" fxLayoutAlign="start center" style="height: 100%;">
21 21 <div class="mat-toolbar-tools tb-details-title-header" fxFlex fxLayout="column" fxLayoutAlign="start start">
22   - <div class="tb-details-title" fxLayout="row" fxLayoutGap="8px" fxLayoutAlign="start center">
23   - <button class="tb-mat-28" mat-icon-button type="button" [fxHide]="!showBackStateIcon" (click)="onBackStateDetails()">
24   - <mat-icon>arrow_back</mat-icon>
25   - </button>
  22 + <div class="tb-details-title" fxLayout="row" fxLayoutAlign="start center">
  23 + <ng-content select=".prefix-title-buttons"></ng-content>
26 24 <span>{{ headerTitle }}</span>
27 25 </div>
28 26 <span class="tb-details-subtitle">{{ headerSubtitle }}</span>
... ...
... ... @@ -32,7 +32,6 @@ export class DetailsPanelComponent extends PageComponent {
32 32 @Input() headerSubtitle = '';
33 33 @Input() isReadOnly = false;
34 34 @Input() isAlwaysEdit = false;
35   - @Input() showBackStateIcon = false;
36 35
37 36 theFormValue: FormGroup;
38 37
... ... @@ -51,8 +50,6 @@ export class DetailsPanelComponent extends PageComponent {
51 50 toggleDetailsEditMode = new EventEmitter<boolean>();
52 51 @Output()
53 52 applyDetails = new EventEmitter<void>();
54   - @Output()
55   - backStateDetails = new EventEmitter<void>();
56 53
57 54 isEditValue = false;
58 55
... ... @@ -78,10 +75,6 @@ export class DetailsPanelComponent extends PageComponent {
78 75 this.closeDetails.emit();
79 76 }
80 77
81   - onBackStateDetails() {
82   - this.backStateDetails.emit();
83   - }
84   -
85 78 onToggleDetailsEditMode() {
86 79 if (!this.isAlwaysEdit) {
87 80 this.isEdit = !this.isEdit;
... ...
... ... @@ -132,6 +132,7 @@ import { ManageDashboardStatesDialogComponent } from '@home/components/dashboard
132 132 import { DashboardStateDialogComponent } from '@home/components/dashboard-page/states/dashboard-state-dialog.component';
133 133 import { EmbedDashboardDialogComponent } from '@home/components/widget/dialog/embed-dashboard-dialog.component';
134 134 import { EMBED_DASHBOARD_DIALOG_TOKEN } from '@home/components/widget/dialog/embed-dashboard-dialog-token';
  135 +import { DisplayWidgetTypesPanelComponent } from '@home/components/dashboard-page/widget-types-panel.component';
135 136
136 137 @NgModule({
137 138 declarations:
... ... @@ -244,7 +245,8 @@ import { EMBED_DASHBOARD_DIALOG_TOKEN } from '@home/components/widget/dialog/emb
244 245 DashboardSettingsDialogComponent,
245 246 ManageDashboardStatesDialogComponent,
246 247 DashboardStateDialogComponent,
247   - EmbedDashboardDialogComponent
  248 + EmbedDashboardDialogComponent,
  249 + DisplayWidgetTypesPanelComponent
248 250 ],
249 251 imports: [
250 252 CommonModule,
... ... @@ -345,7 +347,8 @@ import { EMBED_DASHBOARD_DIALOG_TOKEN } from '@home/components/widget/dialog/emb
345 347 DashboardSettingsDialogComponent,
346 348 ManageDashboardStatesDialogComponent,
347 349 DashboardStateDialogComponent,
348   - EmbedDashboardDialogComponent
  350 + EmbedDashboardDialogComponent,
  351 + DisplayWidgetTypesPanelComponent
349 352 ],
350 353 providers: [
351 354 WidgetComponentService,
... ...
... ... @@ -18,7 +18,7 @@
18 18 <div class="input-wrapper" fxLayoutAlign="start center" fxLayoutGap="8px">
19 19 <mat-icon>search</mat-icon>
20 20 <input type="text" [(ngModel)]="searchText" (ngModelChange)="updateSearchText()" [placeholder]=placeholder>
21   - <button mat-button *ngIf="searchText" mat-icon-button (click)="clear()">
  21 + <button mat-button type="button" *ngIf="searchText" mat-icon-button (click)="clear()">
22 22 <mat-icon>close</mat-icon>
23 23 </button>
24 24 </div>
... ...
... ... @@ -2265,7 +2265,8 @@
2265 2265 "no-data": "No data to display on widget",
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   - "search": "Search widget"
  2268 + "search": "Search widget",
  2269 + "filter": "Widget filter type"
2269 2270 },
2270 2271 "widget-action": {
2271 2272 "header-button": "Widget header button",
... ...
... ... @@ -753,6 +753,9 @@ mat-label {
753 753 &.tb-mat-20 {
754 754 @include tb-mat-icon-size(20);
755 755 }
  756 + &.tb-mat-28 {
  757 + @include tb-mat-icon-size(28);
  758 + }
756 759 &.tb-mat-32 {
757 760 @include tb-mat-icon-size(32);
758 761 }
... ...