Showing
9 changed files
with
193 additions
and
14 deletions
@@ -242,7 +242,9 @@ | @@ -242,7 +242,9 @@ | ||
242 | </tb-edit-widget> | 242 | </tb-edit-widget> |
243 | </tb-details-panel> | 243 | </tb-details-panel> |
244 | <tb-details-panel *ngIf="!isAddingWidgetClosed && !widgetEditMode" fxFlex | 244 | <tb-details-panel *ngIf="!isAddingWidgetClosed && !widgetEditMode" fxFlex |
245 | - headerTitle="{{'dashboard.select-widget-title' | translate}}" | 245 | + headerTitle="{{ |
246 | + (!widgetsBundle?.title ? 'widget.select-widgets-bundle' : 'dashboard.select-widget-value') | translate: widgetsBundle | ||
247 | + }}" | ||
246 | headerHeightPx="120" | 248 | headerHeightPx="120" |
247 | [isReadOnly]="true" | 249 | [isReadOnly]="true" |
248 | [isEdit]="false" | 250 | [isEdit]="false" |
@@ -252,17 +254,17 @@ | @@ -252,17 +254,17 @@ | ||
252 | <div class="header-pane" *ngIf="isAddingWidget"> | 254 | <div class="header-pane" *ngIf="isAddingWidget"> |
253 | <div fxLayout="row"> | 255 | <div fxLayout="row"> |
254 | <!-- <span class="tb-details-subtitle">{{ 'widgets-bundle.current' | translate }}</span>--> | 256 | <!-- <span class="tb-details-subtitle">{{ 'widgets-bundle.current' | translate }}</span>--> |
255 | - <tb-widgets-bundle-select fxFlex | ||
256 | - required | ||
257 | - [selectFirstBundle]="false" | ||
258 | - [(ngModel)]="widgetsBundle" | ||
259 | - (ngModelChange)="widgetsBundle = $event"> | ||
260 | - </tb-widgets-bundle-select> | 257 | + <tb-widgets-bundle-search fxFlex |
258 | + [(ngModel)]="searchBundle" | ||
259 | + [placeholder]="!widgetsBundle?.title ? 'Search widgets bundle' : 'Search widget'" | ||
260 | + (ngModelChange)="searchBundle = $event"> | ||
261 | + </tb-widgets-bundle-search> | ||
261 | </div> | 262 | </div> |
262 | </div> | 263 | </div> |
263 | <tb-dashboard-widget-select *ngIf="isAddingWidget" | 264 | <tb-dashboard-widget-select *ngIf="isAddingWidget" |
264 | [aliasController]="dashboardCtx.aliasController" | 265 | [aliasController]="dashboardCtx.aliasController" |
265 | [widgetsBundle]="widgetsBundle" | 266 | [widgetsBundle]="widgetsBundle" |
267 | + [searchBundle]="searchBundle" | ||
266 | (widgetsBundleSelected)="widgetBundleSelected($event)" | 268 | (widgetsBundleSelected)="widgetBundleSelected($event)" |
267 | (widgetSelected)="addWidgetFromType($event)"> | 269 | (widgetSelected)="addWidgetFromType($event)"> |
268 | </tb-dashboard-widget-select> | 270 | </tb-dashboard-widget-select> |
@@ -146,6 +146,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC | @@ -146,6 +146,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC | ||
146 | isAddingWidget = false; | 146 | isAddingWidget = false; |
147 | isAddingWidgetClosed = true; | 147 | isAddingWidgetClosed = true; |
148 | widgetsBundle: WidgetsBundle = null; | 148 | widgetsBundle: WidgetsBundle = null; |
149 | + searchBundle = ''; | ||
149 | 150 | ||
150 | isToolbarOpened = false; | 151 | isToolbarOpened = false; |
151 | isToolbarOpenedAnimate = false; | 152 | isToolbarOpenedAnimate = false; |
@@ -21,8 +21,8 @@ import { NULL_UUID } from '@shared/models/id/has-uuid'; | @@ -21,8 +21,8 @@ import { NULL_UUID } from '@shared/models/id/has-uuid'; | ||
21 | import { WidgetService } from '@core/http/widget.service'; | 21 | import { WidgetService } from '@core/http/widget.service'; |
22 | import { Widget } from '@shared/models/widget.models'; | 22 | import { Widget } from '@shared/models/widget.models'; |
23 | import { toWidgetInfo } from '@home/models/widget-component.models'; | 23 | import { toWidgetInfo } from '@home/models/widget-component.models'; |
24 | -import { share } from 'rxjs/operators'; | ||
25 | -import { Observable } from 'rxjs'; | 24 | +import { distinctUntilChanged, map, mergeMap, publishReplay, refCount, share } from 'rxjs/operators'; |
25 | +import { BehaviorSubject, Observable, of } from 'rxjs'; | ||
26 | import { DomSanitizer, SafeUrl } from '@angular/platform-browser'; | 26 | import { DomSanitizer, SafeUrl } from '@angular/platform-browser'; |
27 | import { isDefinedAndNotNull } from '@core/utils'; | 27 | import { isDefinedAndNotNull } from '@core/utils'; |
28 | 28 | ||
@@ -33,12 +33,19 @@ import { isDefinedAndNotNull } from '@core/utils'; | @@ -33,12 +33,19 @@ import { isDefinedAndNotNull } from '@core/utils'; | ||
33 | }) | 33 | }) |
34 | export class DashboardWidgetSelectComponent implements OnInit, OnChanges { | 34 | export class DashboardWidgetSelectComponent implements OnInit, OnChanges { |
35 | 35 | ||
36 | + private search$ = new BehaviorSubject<string>(''); | ||
37 | + | ||
36 | @Input() | 38 | @Input() |
37 | widgetsBundle: WidgetsBundle; | 39 | widgetsBundle: WidgetsBundle; |
38 | 40 | ||
39 | @Input() | 41 | @Input() |
40 | aliasController: IAliasController; | 42 | aliasController: IAliasController; |
41 | 43 | ||
44 | + @Input() | ||
45 | + set searchBundle(search: string) { | ||
46 | + this.search$.next(search); | ||
47 | + } | ||
48 | + | ||
42 | @Output() | 49 | @Output() |
43 | widgetSelected: EventEmitter<Widget> = new EventEmitter<Widget>(); | 50 | widgetSelected: EventEmitter<Widget> = new EventEmitter<Widget>(); |
44 | 51 | ||
@@ -49,13 +56,20 @@ export class DashboardWidgetSelectComponent implements OnInit, OnChanges { | @@ -49,13 +56,20 @@ export class DashboardWidgetSelectComponent implements OnInit, OnChanges { | ||
49 | 56 | ||
50 | widgetsBundles$: Observable<Array<WidgetsBundle>>; | 57 | widgetsBundles$: Observable<Array<WidgetsBundle>>; |
51 | 58 | ||
59 | + widgets$: Observable<Array<Widget>>; | ||
60 | + | ||
52 | constructor(private widgetsService: WidgetService, | 61 | constructor(private widgetsService: WidgetService, |
53 | private sanitizer: DomSanitizer) { | 62 | private sanitizer: DomSanitizer) { |
54 | } | 63 | } |
55 | 64 | ||
56 | ngOnInit(): void { | 65 | ngOnInit(): void { |
57 | - this.widgetsBundles$ = this.widgetsService.getAllWidgetsBundles().pipe( | ||
58 | - share() | 66 | + this.widgetsBundles$ = this.search$.asObservable().pipe( |
67 | + distinctUntilChanged(), | ||
68 | + mergeMap(search => this.fetchWidgetBundle(search)) | ||
69 | + ); | ||
70 | + this.widgets$ = this.search$.asObservable().pipe( | ||
71 | + distinctUntilChanged(), | ||
72 | + mergeMap(search => this.fetchWidget(search)) | ||
59 | ); | 73 | ); |
60 | } | 74 | } |
61 | 75 | ||
@@ -121,6 +135,7 @@ export class DashboardWidgetSelectComponent implements OnInit, OnChanges { | @@ -121,6 +135,7 @@ export class DashboardWidgetSelectComponent implements OnInit, OnChanges { | ||
121 | selectBundle($event: Event, bundle: WidgetsBundle) { | 135 | selectBundle($event: Event, bundle: WidgetsBundle) { |
122 | $event.preventDefault(); | 136 | $event.preventDefault(); |
123 | this.widgetsBundle = bundle; | 137 | this.widgetsBundle = bundle; |
138 | + this.search$.next(''); | ||
124 | this.widgetsBundleSelected.emit(bundle); | 139 | this.widgetsBundleSelected.emit(bundle); |
125 | } | 140 | } |
126 | 141 | ||
@@ -131,4 +146,32 @@ export class DashboardWidgetSelectComponent implements OnInit, OnChanges { | @@ -131,4 +146,32 @@ export class DashboardWidgetSelectComponent implements OnInit, OnChanges { | ||
131 | return '/assets/widget-preview-empty.svg'; | 146 | return '/assets/widget-preview-empty.svg'; |
132 | } | 147 | } |
133 | 148 | ||
149 | + private getWidgetsBundle(): Observable<Array<WidgetsBundle>> { | ||
150 | + return this.widgetsService.getAllWidgetsBundles().pipe( | ||
151 | + publishReplay(1), | ||
152 | + refCount() | ||
153 | + ); | ||
154 | + } | ||
155 | + | ||
156 | + private fetchWidgetBundle(search: string): Observable<Array<WidgetsBundle>> { | ||
157 | + return this.getWidgetsBundle().pipe( | ||
158 | + map(bundles => search ? bundles.filter( | ||
159 | + bundle => ( | ||
160 | + bundle.title?.toLowerCase().includes(search.toLowerCase()) || | ||
161 | + bundle.description?.toLowerCase().includes(search.toLowerCase()) | ||
162 | + )) : bundles | ||
163 | + ) | ||
164 | + ); | ||
165 | + } | ||
166 | + | ||
167 | + private fetchWidget(search: string): Observable<Array<Widget>> { | ||
168 | + return of(this.widgets).pipe( | ||
169 | + map(widgets => search ? widgets.filter( | ||
170 | + widget => ( | ||
171 | + widget.title?.toLowerCase().includes(search.toLowerCase()) || | ||
172 | + widget.description?.toLowerCase().includes(search.toLowerCase()) | ||
173 | + )) : widgets | ||
174 | + ) | ||
175 | + ); | ||
176 | + } | ||
134 | } | 177 | } |
@@ -31,7 +31,7 @@ | @@ -31,7 +31,7 @@ | ||
31 | </span> | 31 | </span> |
32 | </div> | 32 | </div> |
33 | <ng-content select=".details-buttons"></ng-content> | 33 | <ng-content select=".details-buttons"></ng-content> |
34 | - <button mat-button mat-icon-button (click)="onCloseDetails()"> | 34 | + <button mat-button cdkFocusInitial mat-icon-button (click)="onCloseDetails()"> |
35 | <mat-icon class="material-icons">close</mat-icon> | 35 | <mat-icon class="material-icons">close</mat-icon> |
36 | </button> | 36 | </button> |
37 | </div> | 37 | </div> |
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 class="input-wrapper" fxLayoutAlign="start center" fxLayoutGap="8px"> | ||
19 | + <mat-icon>search</mat-icon> | ||
20 | + <input type="text" [(ngModel)]="searchText" (ngModelChange)="updateSearchText()" [placeholder]=placeholder> | ||
21 | + <button mat-button *ngIf="searchText" mat-icon-button (click)="clear()"> | ||
22 | + <mat-icon>close</mat-icon> | ||
23 | + </button> | ||
24 | +</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 | +.input-wrapper { | ||
17 | + background: hsla(0, 0%, 100%, .2); | ||
18 | + padding: 5px 0 5px 10px; | ||
19 | + height: 40px; | ||
20 | + | ||
21 | + input { | ||
22 | + width: 100%; | ||
23 | + height: 100%; | ||
24 | + padding: 0; | ||
25 | + font-size: 20px; | ||
26 | + outline: none; | ||
27 | + border: none; | ||
28 | + background-color: transparent; | ||
29 | + color: #fff; | ||
30 | + | ||
31 | + &::placeholder { | ||
32 | + color: #fff; | ||
33 | + opacity: .8; | ||
34 | + line-height: 26px; | ||
35 | + } | ||
36 | + } | ||
37 | +} | ||
38 | + |
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, ElementRef, forwardRef, Input, ViewChild, ViewEncapsulation } from '@angular/core'; | ||
18 | +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; | ||
19 | + | ||
20 | +@Component({ | ||
21 | + selector: 'tb-widgets-bundle-search', | ||
22 | + templateUrl: './widgets-bundle-search.component.html', | ||
23 | + styleUrls: ['./widgets-bundle-search.component.scss'], | ||
24 | + providers: [{ | ||
25 | + provide: NG_VALUE_ACCESSOR, | ||
26 | + useExisting: forwardRef(() => WidgetsBundleSearchComponent), | ||
27 | + multi: true | ||
28 | + }], | ||
29 | + encapsulation: ViewEncapsulation.None | ||
30 | +}) | ||
31 | +export class WidgetsBundleSearchComponent implements ControlValueAccessor { | ||
32 | + | ||
33 | + searchText: string; | ||
34 | + | ||
35 | + @Input() placeholder: string; | ||
36 | + | ||
37 | + @ViewChild('searchInput') searchInput: ElementRef<HTMLInputElement>; | ||
38 | + | ||
39 | + private propagateChange = (v: any) => { }; | ||
40 | + | ||
41 | + constructor() { | ||
42 | + } | ||
43 | + | ||
44 | + registerOnChange(fn: any): void { | ||
45 | + this.propagateChange = fn; | ||
46 | + } | ||
47 | + | ||
48 | + registerOnTouched(fn: any): void { | ||
49 | + } | ||
50 | + | ||
51 | + writeValue(value: string | null): void { | ||
52 | + this.searchText = value; | ||
53 | + } | ||
54 | + | ||
55 | + updateSearchText(): void { | ||
56 | + this.updateView(); | ||
57 | + } | ||
58 | + | ||
59 | + private updateView() { | ||
60 | + this.propagateChange(this.searchText); | ||
61 | + } | ||
62 | + | ||
63 | + clear(): void { | ||
64 | + this.searchText = ''; | ||
65 | + this.updateView(); | ||
66 | + } | ||
67 | +} |
@@ -138,6 +138,7 @@ import { QueueTypeListComponent } from '@shared/components/queue/queue-type-list | @@ -138,6 +138,7 @@ import { QueueTypeListComponent } from '@shared/components/queue/queue-type-list | ||
138 | import { ContactComponent } from '@shared/components/contact.component'; | 138 | import { ContactComponent } from '@shared/components/contact.component'; |
139 | import { TimezoneSelectComponent } from '@shared/components/time/timezone-select.component'; | 139 | import { TimezoneSelectComponent } from '@shared/components/time/timezone-select.component'; |
140 | import { FileSizePipe } from '@shared/pipe/file-size.pipe'; | 140 | import { FileSizePipe } from '@shared/pipe/file-size.pipe'; |
141 | +import { WidgetsBundleSearchComponent } from '@shared/components/widgets-bundle-search.component'; | ||
141 | 142 | ||
142 | @NgModule({ | 143 | @NgModule({ |
143 | providers: [ | 144 | providers: [ |
@@ -227,7 +228,8 @@ import { FileSizePipe } from '@shared/pipe/file-size.pipe'; | @@ -227,7 +228,8 @@ import { FileSizePipe } from '@shared/pipe/file-size.pipe'; | ||
227 | JsonObjectEditDialogComponent, | 228 | JsonObjectEditDialogComponent, |
228 | HistorySelectorComponent, | 229 | HistorySelectorComponent, |
229 | EntityGatewaySelectComponent, | 230 | EntityGatewaySelectComponent, |
230 | - ContactComponent | 231 | + ContactComponent, |
232 | + WidgetsBundleSearchComponent | ||
231 | ], | 233 | ], |
232 | imports: [ | 234 | imports: [ |
233 | CommonModule, | 235 | CommonModule, |
@@ -395,7 +397,8 @@ import { FileSizePipe } from '@shared/pipe/file-size.pipe'; | @@ -395,7 +397,8 @@ import { FileSizePipe } from '@shared/pipe/file-size.pipe'; | ||
395 | JsonObjectEditDialogComponent, | 397 | JsonObjectEditDialogComponent, |
396 | HistorySelectorComponent, | 398 | HistorySelectorComponent, |
397 | EntityGatewaySelectComponent, | 399 | EntityGatewaySelectComponent, |
398 | - ContactComponent | 400 | + ContactComponent, |
401 | + WidgetsBundleSearchComponent | ||
399 | ] | 402 | ] |
400 | }) | 403 | }) |
401 | export class SharedModule { } | 404 | export class SharedModule { } |
@@ -646,6 +646,7 @@ | @@ -646,6 +646,7 @@ | ||
646 | "add-widget": "Add new widget", | 646 | "add-widget": "Add new widget", |
647 | "title": "Title", | 647 | "title": "Title", |
648 | "select-widget-title": "Select widget", | 648 | "select-widget-title": "Select widget", |
649 | + "select-widget-value": "{{title}}: select widget", | ||
649 | "select-widget-subtitle": "List of available widget types", | 650 | "select-widget-subtitle": "List of available widget types", |
650 | "delete": "Delete dashboard", | 651 | "delete": "Delete dashboard", |
651 | "title-required": "Title is required.", | 652 | "title-required": "Title is required.", |