Browse Source

UI: Dashboard filters - user mode

pull/3053/head
Igor Kulikov 6 years ago
parent
commit
c6f7862cbf
  1. 2
      ui-ngx/src/app/core/http/entity.service.ts
  2. 4
      ui-ngx/src/app/modules/home/components/filter/boolean-filter-predicate.component.html
  3. 5
      ui-ngx/src/app/modules/home/components/filter/boolean-filter-predicate.component.ts
  4. 2
      ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate-dialog.component.html
  5. 14
      ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate-dialog.component.ts
  6. 20
      ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate.component.ts
  7. 9
      ui-ngx/src/app/modules/home/components/filter/filter-dialog.component.html
  8. 3
      ui-ngx/src/app/modules/home/components/filter/filter-dialog.component.ts
  9. 23
      ui-ngx/src/app/modules/home/components/filter/filter-predicate-list.component.html
  10. 45
      ui-ngx/src/app/modules/home/components/filter/filter-predicate-list.component.ts
  11. 56
      ui-ngx/src/app/modules/home/components/filter/filter-predicate.component.html
  12. 23
      ui-ngx/src/app/modules/home/components/filter/filter-predicate.component.ts
  13. 1
      ui-ngx/src/app/modules/home/components/filter/filter-predicate.scss
  14. 62
      ui-ngx/src/app/modules/home/components/filter/filter-user-info-dialog.component.html
  15. 108
      ui-ngx/src/app/modules/home/components/filter/filter-user-info-dialog.component.ts
  16. 26
      ui-ngx/src/app/modules/home/components/filter/filter-user-info.component.html
  17. 104
      ui-ngx/src/app/modules/home/components/filter/filter-user-info.component.ts
  18. 3
      ui-ngx/src/app/modules/home/components/filter/filters-dialog.component.ts
  19. 11
      ui-ngx/src/app/modules/home/components/filter/filters-edit-panel.component.ts
  20. 4
      ui-ngx/src/app/modules/home/components/filter/filters-edit.component.ts
  21. 6
      ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.html
  22. 1
      ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.ts
  23. 8
      ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.html
  24. 3
      ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.ts
  25. 22
      ui-ngx/src/app/modules/home/components/filter/numeric-filter-predicate.component.html
  26. 8
      ui-ngx/src/app/modules/home/components/filter/numeric-filter-predicate.component.ts
  27. 4
      ui-ngx/src/app/modules/home/components/filter/string-filter-predicate.component.html
  28. 6
      ui-ngx/src/app/modules/home/components/filter/string-filter-predicate.component.ts
  29. 80
      ui-ngx/src/app/modules/home/components/filter/user-filter-dialog.component.html
  30. 118
      ui-ngx/src/app/modules/home/components/filter/user-filter-dialog.component.ts
  31. 11
      ui-ngx/src/app/modules/home/components/home-components.module.ts
  32. 5
      ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts
  33. 8
      ui-ngx/src/app/shared/components/time/datetime.component.html
  34. 7
      ui-ngx/src/app/shared/components/time/datetime.component.scss
  35. 3
      ui-ngx/src/app/shared/components/time/datetime.component.ts
  36. 60
      ui-ngx/src/app/shared/components/toast.directive.ts
  37. 162
      ui-ngx/src/app/shared/models/query/query.models.ts
  38. 36
      ui-ngx/src/assets/locale/locale.constant-en_US.json

2
ui-ngx/src/app/core/http/entity.service.ts

@ -576,7 +576,7 @@ export class EntityService {
} }
private getEntityFieldKeys (entityType: EntityType, searchText: string): Array<string> { private getEntityFieldKeys (entityType: EntityType, searchText: string): Array<string> {
const entityFieldKeys: string[] = []; const entityFieldKeys: string[] = [entityFields.createdTime.keyName];
const query = searchText.toLowerCase(); const query = searchText.toLowerCase();
switch(entityType) { switch(entityType) {
case EntityType.USER: case EntityType.USER:

4
ui-ngx/src/app/modules/home/components/filter/boolean-filter-predicate.component.html

@ -16,7 +16,7 @@
--> -->
<div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px" [formGroup]="booleanFilterPredicateFormGroup"> <div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px" [formGroup]="booleanFilterPredicateFormGroup">
<mat-form-field floatLabel="always" hideRequiredMarker fxFlex class="mat-block"> <mat-form-field floatLabel="always" hideRequiredMarker fxFlex="40" class="mat-block">
<mat-label></mat-label> <mat-label></mat-label>
<mat-select required formControlName="operation" placeholder="{{'filter.operation.operation' | translate}}"> <mat-select required formControlName="operation" placeholder="{{'filter.operation.operation' | translate}}">
<mat-option *ngFor="let operation of booleanOperations" [value]="operation"> <mat-option *ngFor="let operation of booleanOperations" [value]="operation">
@ -24,7 +24,7 @@
</mat-option> </mat-option>
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
<mat-checkbox fxFlex formControlName="value"> <mat-checkbox fxFlex="60" formControlName="value">
{{ (booleanFilterPredicateFormGroup.get('value').value ? 'value.true' : 'value.false') | translate }} {{ (booleanFilterPredicateFormGroup.get('value').value ? 'value.true' : 'value.false') | translate }}
</mat-checkbox> </mat-checkbox>
</div> </div>

5
ui-ngx/src/app/modules/home/components/filter/boolean-filter-predicate.component.ts

@ -39,8 +39,6 @@ export class BooleanFilterPredicateComponent implements ControlValueAccessor, On
@Input() disabled: boolean; @Input() disabled: boolean;
@Input() userMode: boolean;
booleanFilterPredicateFormGroup: FormGroup; booleanFilterPredicateFormGroup: FormGroup;
booleanOperations = Object.keys(BooleanOperation); booleanOperations = Object.keys(BooleanOperation);
@ -57,9 +55,6 @@ export class BooleanFilterPredicateComponent implements ControlValueAccessor, On
operation: [BooleanOperation.EQUAL, [Validators.required]], operation: [BooleanOperation.EQUAL, [Validators.required]],
value: [false] value: [false]
}); });
if (this.userMode) {
this.booleanFilterPredicateFormGroup.get('operation').disable({emitEvent: false});
}
this.booleanFilterPredicateFormGroup.valueChanges.subscribe(() => { this.booleanFilterPredicateFormGroup.valueChanges.subscribe(() => {
this.updateModel(); this.updateModel();
}); });

2
ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate-dialog.component.html

@ -36,9 +36,9 @@
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
<tb-filter-predicate-list <tb-filter-predicate-list
[userMode]="data.userMode"
[valueType]="data.valueType" [valueType]="data.valueType"
[operation]="complexFilterFormGroup.get('operation').value" [operation]="complexFilterFormGroup.get('operation').value"
[key]="data.key"
formControlName="predicates"> formControlName="predicates">
</tb-filter-predicate-list> </tb-filter-predicate-list>
</fieldset> </fieldset>

14
ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate-dialog.component.ts

@ -24,14 +24,14 @@ import { Router } from '@angular/router';
import { DialogComponent } from '@app/shared/components/dialog.component'; import { DialogComponent } from '@app/shared/components/dialog.component';
import { import {
BooleanOperation, booleanOperationTranslationMap, BooleanOperation, booleanOperationTranslationMap,
ComplexFilterPredicate, ComplexOperation, complexOperationTranslationMap, ComplexFilterPredicate, ComplexFilterPredicateInfo, ComplexOperation, complexOperationTranslationMap,
EntityKeyValueType, EntityKeyValueType,
FilterPredicateType FilterPredicateType, KeyFilterPredicateInfo
} from '@shared/models/query/query.models'; } from '@shared/models/query/query.models';
export interface ComplexFilterPredicateDialogData { export interface ComplexFilterPredicateDialogData {
complexPredicate: ComplexFilterPredicate; complexPredicate: ComplexFilterPredicateInfo;
userMode: boolean; key: string;
disabled: boolean; disabled: boolean;
isAdd: boolean; isAdd: boolean;
valueType: EntityKeyValueType; valueType: EntityKeyValueType;
@ -44,7 +44,7 @@ export interface ComplexFilterPredicateDialogData {
styleUrls: [] styleUrls: []
}) })
export class ComplexFilterPredicateDialogComponent extends export class ComplexFilterPredicateDialogComponent extends
DialogComponent<ComplexFilterPredicateDialogComponent, ComplexFilterPredicate> DialogComponent<ComplexFilterPredicateDialogComponent, ComplexFilterPredicateInfo>
implements OnInit, ErrorStateMatcher { implements OnInit, ErrorStateMatcher {
complexFilterFormGroup: FormGroup; complexFilterFormGroup: FormGroup;
@ -61,7 +61,7 @@ export class ComplexFilterPredicateDialogComponent extends
protected router: Router, protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: ComplexFilterPredicateDialogData, @Inject(MAT_DIALOG_DATA) public data: ComplexFilterPredicateDialogData,
@SkipSelf() private errorStateMatcher: ErrorStateMatcher, @SkipSelf() private errorStateMatcher: ErrorStateMatcher,
public dialogRef: MatDialogRef<ComplexFilterPredicateDialogComponent, ComplexFilterPredicate>, public dialogRef: MatDialogRef<ComplexFilterPredicateDialogComponent, ComplexFilterPredicateInfo>,
private fb: FormBuilder) { private fb: FormBuilder) {
super(store, router, dialogRef); super(store, router, dialogRef);
@ -91,7 +91,7 @@ export class ComplexFilterPredicateDialogComponent extends
save(): void { save(): void {
this.submitted = true; this.submitted = true;
if (this.complexFilterFormGroup.valid) { if (this.complexFilterFormGroup.valid) {
const predicate: ComplexFilterPredicate = this.complexFilterFormGroup.getRawValue(); const predicate: ComplexFilterPredicateInfo = this.complexFilterFormGroup.getRawValue();
predicate.type = FilterPredicateType.COMPLEX; predicate.type = FilterPredicateType.COMPLEX;
this.dialogRef.close(predicate); this.dialogRef.close(predicate);
} }

20
ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate.component.ts

@ -16,7 +16,11 @@
import { Component, forwardRef, Input, OnInit } from '@angular/core'; import { Component, forwardRef, Input, OnInit } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { ComplexFilterPredicate, EntityKeyValueType } from '@shared/models/query/query.models'; import {
ComplexFilterPredicate,
ComplexFilterPredicateInfo,
EntityKeyValueType
} from '@shared/models/query/query.models';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { import {
ComplexFilterPredicateDialogComponent, ComplexFilterPredicateDialogComponent,
@ -40,13 +44,13 @@ export class ComplexFilterPredicateComponent implements ControlValueAccessor, On
@Input() disabled: boolean; @Input() disabled: boolean;
@Input() userMode: boolean;
@Input() valueType: EntityKeyValueType; @Input() valueType: EntityKeyValueType;
@Input() key: string;
private propagateChange = null; private propagateChange = null;
private complexFilterPredicate: ComplexFilterPredicate; private complexFilterPredicate: ComplexFilterPredicateInfo;
constructor(private dialog: MatDialog) { constructor(private dialog: MatDialog) {
} }
@ -65,21 +69,21 @@ export class ComplexFilterPredicateComponent implements ControlValueAccessor, On
this.disabled = isDisabled; this.disabled = isDisabled;
} }
writeValue(predicate: ComplexFilterPredicate): void { writeValue(predicate: ComplexFilterPredicateInfo): void {
this.complexFilterPredicate = predicate; this.complexFilterPredicate = predicate;
} }
private openComplexFilterDialog() { private openComplexFilterDialog() {
this.dialog.open<ComplexFilterPredicateDialogComponent, ComplexFilterPredicateDialogData, this.dialog.open<ComplexFilterPredicateDialogComponent, ComplexFilterPredicateDialogData,
ComplexFilterPredicate>(ComplexFilterPredicateDialogComponent, { ComplexFilterPredicateInfo>(ComplexFilterPredicateDialogComponent, {
disableClose: true, disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: { data: {
complexPredicate: deepClone(this.complexFilterPredicate), complexPredicate: deepClone(this.complexFilterPredicate),
disabled: this.disabled, disabled: this.disabled,
userMode: this.userMode,
valueType: this.valueType, valueType: this.valueType,
isAdd: false isAdd: false,
key: this.key
} }
}).afterClosed().subscribe( }).afterClosed().subscribe(
(result) => { (result) => {

9
ui-ngx/src/app/modules/home/components/filter/filter-dialog.component.html

@ -17,7 +17,7 @@
--> -->
<form [formGroup]="filterFormGroup" (ngSubmit)="save()" style="width: 700px;"> <form [formGroup]="filterFormGroup" (ngSubmit)="save()" style="width: 700px;">
<mat-toolbar color="primary"> <mat-toolbar color="primary">
<h2>{{ userMode ? filter.filter : ((isAdd ? 'filter.add' : 'filter.edit') | translate) }}</h2> <h2>{{ (isAdd ? 'filter.add' : 'filter.edit') | translate }}</h2>
<span fxFlex></span> <span fxFlex></span>
<button mat-icon-button <button mat-icon-button
(click)="cancel()" (click)="cancel()"
@ -30,7 +30,7 @@
<div mat-dialog-content> <div mat-dialog-content>
<fieldset [disabled]="isLoading$ | async"> <fieldset [disabled]="isLoading$ | async">
<div fxFlex fxLayout="column"> <div fxFlex fxLayout="column">
<div fxLayout="row" [fxShow]="!userMode"> <div fxLayout="row">
<mat-form-field fxFlex class="mat-block"> <mat-form-field fxFlex class="mat-block">
<mat-label translate>filter.name</mat-label> <mat-label translate>filter.name</mat-label>
<input matInput formControlName="filter" required> <input matInput formControlName="filter" required>
@ -49,8 +49,7 @@
</section> </section>
</div> </div>
<tb-key-filter-list <tb-key-filter-list
formControlName="keyFilters" formControlName="keyFilters">
[userMode]="userMode">
</tb-key-filter-list> </tb-key-filter-list>
</div> </div>
</fieldset> </fieldset>
@ -59,7 +58,7 @@
<button mat-raised-button color="primary" <button mat-raised-button color="primary"
type="submit" type="submit"
[disabled]="(isLoading$ | async) || filterFormGroup.invalid || !filterFormGroup.dirty"> [disabled]="(isLoading$ | async) || filterFormGroup.invalid || !filterFormGroup.dirty">
{{ (userMode ? 'action.update' : (isAdd ? 'action.add' : 'action.update')) | translate }} {{ (isAdd ? 'action.add' : 'action.update') | translate }}
</button> </button>
<button mat-button color="primary" <button mat-button color="primary"
type="button" type="button"

3
ui-ngx/src/app/modules/home/components/filter/filter-dialog.component.ts

@ -36,7 +36,6 @@ import { Filter, Filters } from '@shared/models/query/query.models';
export interface FilterDialogData { export interface FilterDialogData {
isAdd: boolean; isAdd: boolean;
userMode: boolean;
filters: Filters | Array<Filter>; filters: Filters | Array<Filter>;
filter?: Filter; filter?: Filter;
} }
@ -51,7 +50,6 @@ export class FilterDialogComponent extends DialogComponent<FilterDialogComponent
implements OnInit, ErrorStateMatcher { implements OnInit, ErrorStateMatcher {
isAdd: boolean; isAdd: boolean;
userMode: boolean;
filters: Array<Filter>; filters: Array<Filter>;
filter: Filter; filter: Filter;
@ -70,7 +68,6 @@ export class FilterDialogComponent extends DialogComponent<FilterDialogComponent
public translate: TranslateService) { public translate: TranslateService) {
super(store, router, dialogRef); super(store, router, dialogRef);
this.isAdd = data.isAdd; this.isAdd = data.isAdd;
this.userMode = data.userMode;
if (Array.isArray(data.filters)) { if (Array.isArray(data.filters)) {
this.filters = data.filters; this.filters = data.filters;
} else { } else {

23
ui-ngx/src/app/modules/home/components/filter/filter-predicate-list.component.html

@ -25,13 +25,16 @@
<div fxLayout="row"> <div fxLayout="row">
<span fxFlex="8"></span> <span fxFlex="8"></span>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px" fxFlex="92"> <div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px" fxFlex="92">
<div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px"> <div fxFlex fxLayout="row" fxLayoutGap="8px">
<label fxFlex translate class="tb-title no-padding">filter.operation.operation</label> <div fxFlex="40" fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<label *ngIf="valueType === valueTypeEnum.STRING" <label fxFlex translate class="tb-title no-padding">filter.operation.operation</label>
translate class="tb-title no-padding" style="min-width: 70px;">filter.ignore-case</label> <label *ngIf="valueType === valueTypeEnum.STRING"
translate class="tb-title no-padding" style="min-width: 70px;">filter.ignore-case</label>
</div>
<label fxFlex="60" translate class="tb-title no-padding">filter.value</label>
</div> </div>
<label fxFlex translate class="tb-title no-padding">filter.value</label> <label translate class="tb-title no-padding" style="width: 60px;">filter.user-parameters</label>
<span [fxShow]="!disabled && !userMode" style="min-width: 40px;">&nbsp;</span> <span [fxShow]="!disabled" style="min-width: 40px;">&nbsp;</span>
</div> </div>
</div> </div>
<mat-divider></mat-divider> <mat-divider></mat-divider>
@ -46,12 +49,12 @@
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px" fxFlex> <div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px" fxFlex>
<tb-filter-predicate <tb-filter-predicate
fxFlex fxFlex
[userMode]="userMode"
[valueType]="valueType" [valueType]="valueType"
[key]="key"
[formControl]="predicateControl"> [formControl]="predicateControl">
</tb-filter-predicate> </tb-filter-predicate>
<button mat-icon-button color="primary" <button mat-icon-button color="primary"
[fxShow]="!disabled && !userMode" [fxShow]="!disabled"
type="button" type="button"
(click)="removePredicate($index)" (click)="removePredicate($index)"
matTooltip="{{ 'filter.remove-filter' | translate }}" matTooltip="{{ 'filter.remove-filter' | translate }}"
@ -67,7 +70,7 @@
</div> </div>
<div style="margin-top: 16px;" fxLayout="row" fxLayoutGap="8px"> <div style="margin-top: 16px;" fxLayout="row" fxLayoutGap="8px">
<button mat-button mat-raised-button color="primary" <button mat-button mat-raised-button color="primary"
[fxShow]="!disabled && !userMode" [fxShow]="!disabled"
(click)="addPredicate(false)" (click)="addPredicate(false)"
type="button" type="button"
matTooltip="{{ 'filter.add-filter' | translate }}" matTooltip="{{ 'filter.add-filter' | translate }}"
@ -75,7 +78,7 @@
{{ 'action.add' | translate }} {{ 'action.add' | translate }}
</button> </button>
<button mat-button mat-raised-button color="primary" <button mat-button mat-raised-button color="primary"
[fxShow]="!disabled && !userMode" [fxShow]="!disabled"
(click)="addPredicate(true)" (click)="addPredicate(true)"
type="button" type="button"
matTooltip="{{ 'filter.add-complex-filter' | translate }}" matTooltip="{{ 'filter.add-complex-filter' | translate }}"

45
ui-ngx/src/app/modules/home/components/filter/filter-predicate-list.component.ts

@ -26,17 +26,19 @@ import {
} from '@angular/forms'; } from '@angular/forms';
import { Observable, of, Subscription } from 'rxjs'; import { Observable, of, Subscription } from 'rxjs';
import { import {
ComplexFilterPredicate, ComplexFilterPredicateInfo,
ComplexOperation, complexOperationTranslationMap, ComplexOperation,
createDefaultFilterPredicate, complexOperationTranslationMap,
createDefaultFilterPredicateInfo,
EntityKeyValueType, EntityKeyValueType,
KeyFilterPredicate KeyFilterPredicateInfo
} from '@shared/models/query/query.models'; } from '@shared/models/query/query.models';
import { import {
ComplexFilterPredicateDialogComponent, ComplexFilterPredicateDialogComponent,
ComplexFilterPredicateDialogData ComplexFilterPredicateDialogData
} from '@home/components/filter/complex-filter-predicate-dialog.component'; } from '@home/components/filter/complex-filter-predicate-dialog.component';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { map } from 'rxjs/operators';
@Component({ @Component({
selector: 'tb-filter-predicate-list', selector: 'tb-filter-predicate-list',
@ -54,10 +56,10 @@ export class FilterPredicateListComponent implements ControlValueAccessor, OnIni
@Input() disabled: boolean; @Input() disabled: boolean;
@Input() userMode: boolean;
@Input() valueType: EntityKeyValueType; @Input() valueType: EntityKeyValueType;
@Input() key: string;
@Input() operation: ComplexOperation = ComplexOperation.AND; @Input() operation: ComplexOperation = ComplexOperation.AND;
filterListFormGroup: FormGroup; filterListFormGroup: FormGroup;
@ -100,7 +102,7 @@ export class FilterPredicateListComponent implements ControlValueAccessor, OnIni
} }
} }
writeValue(predicates: Array<KeyFilterPredicate>): void { writeValue(predicates: Array<KeyFilterPredicateInfo>): void {
if (this.valueChangeSubscription) { if (this.valueChangeSubscription) {
this.valueChangeSubscription.unsubscribe(); this.valueChangeSubscription.unsubscribe();
} }
@ -127,10 +129,10 @@ export class FilterPredicateListComponent implements ControlValueAccessor, OnIni
public addPredicate(complex: boolean) { public addPredicate(complex: boolean) {
const predicatesFormArray = this.filterListFormGroup.get('predicates') as FormArray; const predicatesFormArray = this.filterListFormGroup.get('predicates') as FormArray;
const predicate = createDefaultFilterPredicate(this.valueType, complex); const predicate = createDefaultFilterPredicateInfo(this.valueType, complex);
let observable: Observable<KeyFilterPredicate>; let observable: Observable<KeyFilterPredicateInfo>;
if (complex) { if (complex) {
observable = this.openComplexFilterDialog(predicate as ComplexFilterPredicate); observable = this.openComplexFilterDialog(predicate);
} else { } else {
observable = of(predicate); observable = of(predicate);
} }
@ -141,24 +143,33 @@ export class FilterPredicateListComponent implements ControlValueAccessor, OnIni
}); });
} }
private openComplexFilterDialog(predicate: ComplexFilterPredicate): Observable<KeyFilterPredicate> { private openComplexFilterDialog(predicate: KeyFilterPredicateInfo): Observable<KeyFilterPredicateInfo> {
return this.dialog.open<ComplexFilterPredicateDialogComponent, ComplexFilterPredicateDialogData, return this.dialog.open<ComplexFilterPredicateDialogComponent, ComplexFilterPredicateDialogData,
ComplexFilterPredicate>(ComplexFilterPredicateDialogComponent, { ComplexFilterPredicateInfo>(ComplexFilterPredicateDialogComponent, {
disableClose: true, disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: { data: {
complexPredicate: predicate, complexPredicate: predicate.keyFilterPredicate as ComplexFilterPredicateInfo,
disabled: this.disabled, disabled: this.disabled,
userMode: this.userMode,
valueType: this.valueType, valueType: this.valueType,
key: this.key,
isAdd: true isAdd: true
} }
}).afterClosed(); }).afterClosed().pipe(
map((result) => {
if (result) {
predicate.keyFilterPredicate = result;
return predicate;
} else {
return null;
}
})
);
} }
private updateModel() { private updateModel() {
const predicates: Array<KeyFilterPredicate> = this.filterListFormGroup.getRawValue().predicates; const predicates: Array<KeyFilterPredicateInfo> = this.filterListFormGroup.getRawValue().predicates;
if (predicates.length) { if (this.filterListFormGroup.valid && predicates.length) {
this.propagateChange(predicates); this.propagateChange(predicates);
} else { } else {
this.propagateChange(null); this.propagateChange(null);

56
ui-ngx/src/app/modules/home/components/filter/filter-predicate.component.html

@ -15,28 +15,36 @@
limitations under the License. limitations under the License.
--> -->
<div fxLayout="column" [formGroup]="filterPredicateFormGroup"
[ngSwitch]="type"> <div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px" [formGroup]="filterPredicateFormGroup">
<ng-template [ngSwitchCase]="filterPredicateType.STRING"> <div fxFlex fxLayout="column" [ngSwitch]="type">
<tb-string-filter-predicate [userMode]="userMode" <ng-template [ngSwitchCase]="filterPredicateType.STRING">
formControlName="predicate"> <tb-string-filter-predicate formControlName="predicate">
</tb-string-filter-predicate> </tb-string-filter-predicate>
</ng-template> </ng-template>
<ng-template [ngSwitchCase]="filterPredicateType.NUMERIC"> <ng-template [ngSwitchCase]="filterPredicateType.NUMERIC">
<tb-numeric-filter-predicate [userMode]="userMode" <tb-numeric-filter-predicate [valueType]="valueType"
formControlName="predicate"> formControlName="predicate">
</tb-numeric-filter-predicate> </tb-numeric-filter-predicate>
</ng-template> </ng-template>
<ng-template [ngSwitchCase]="filterPredicateType.BOOLEAN"> <ng-template [ngSwitchCase]="filterPredicateType.BOOLEAN">
<tb-boolean-filter-predicate [userMode]="userMode" <tb-boolean-filter-predicate formControlName="predicate">
formControlName="predicate"> </tb-boolean-filter-predicate>
</tb-boolean-filter-predicate> </ng-template>
</ng-template> <ng-template [ngSwitchCase]="filterPredicateType.COMPLEX">
<ng-template [ngSwitchCase]="filterPredicateType.COMPLEX"> <tb-complex-filter-predicate
<tb-complex-filter-predicate [key]="key"
[valueType]="valueType" [valueType]="valueType"
[userMode]="userMode" formControlName="predicate">
formControlName="predicate"> </tb-complex-filter-predicate>
</tb-complex-filter-predicate> </ng-template>
</ng-template> </div>
<tb-filter-user-info *ngIf="type !== filterPredicateType.COMPLEX"
style="width: 60px;"
fxLayout="row" fxLayoutAlign="center"
[valueType]="valueType"
[operation]="filterPredicateFormGroup.get('predicate').value?.operation"
[key]="key"
formControlName="userInfo">
</tb-filter-user-info>
</div> </div>

23
ui-ngx/src/app/modules/home/components/filter/filter-predicate.component.ts

@ -18,7 +18,7 @@ import { Component, forwardRef, Input, OnInit } from '@angular/core';
import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms';
import { import {
EntityKeyValueType, EntityKeyValueType,
FilterPredicateType, KeyFilterPredicate FilterPredicateType, KeyFilterPredicate, KeyFilterPredicateInfo
} from '@shared/models/query/query.models'; } from '@shared/models/query/query.models';
@Component({ @Component({
@ -37,10 +37,10 @@ export class FilterPredicateComponent implements ControlValueAccessor, OnInit {
@Input() disabled: boolean; @Input() disabled: boolean;
@Input() userMode: boolean;
@Input() valueType: EntityKeyValueType; @Input() valueType: EntityKeyValueType;
@Input() key: string;
filterPredicateFormGroup: FormGroup; filterPredicateFormGroup: FormGroup;
type: FilterPredicateType; type: FilterPredicateType;
@ -54,7 +54,8 @@ export class FilterPredicateComponent implements ControlValueAccessor, OnInit {
ngOnInit(): void { ngOnInit(): void {
this.filterPredicateFormGroup = this.fb.group({ this.filterPredicateFormGroup = this.fb.group({
predicate: [null, [Validators.required]] predicate: [null, [Validators.required]],
userInfo: [null, []]
}); });
this.filterPredicateFormGroup.valueChanges.subscribe(() => { this.filterPredicateFormGroup.valueChanges.subscribe(() => {
this.updateModel(); this.updateModel();
@ -77,15 +78,19 @@ export class FilterPredicateComponent implements ControlValueAccessor, OnInit {
} }
} }
writeValue(predicate: KeyFilterPredicate): void { writeValue(predicate: KeyFilterPredicateInfo): void {
this.type = predicate.type; this.type = predicate.keyFilterPredicate.type;
this.filterPredicateFormGroup.get('predicate').patchValue(predicate, {emitEvent: false}); this.filterPredicateFormGroup.get('predicate').patchValue(predicate.keyFilterPredicate, {emitEvent: false});
this.filterPredicateFormGroup.get('userInfo').patchValue(predicate.userInfo, {emitEvent: false});
} }
private updateModel() { private updateModel() {
let predicate: KeyFilterPredicate = null; let predicate: KeyFilterPredicateInfo = null;
if (this.filterPredicateFormGroup.valid) { if (this.filterPredicateFormGroup.valid) {
predicate = this.filterPredicateFormGroup.getRawValue().predicate; predicate = {
keyFilterPredicate: this.filterPredicateFormGroup.getRawValue().predicate,
userInfo: this.filterPredicateFormGroup.getRawValue().userInfo
};
} }
this.propagateChange(predicate); this.propagateChange(predicate);
} }

1
ui-ngx/src/app/modules/home/components/filter/filter-predicate.scss

@ -20,6 +20,7 @@
.mat-form-field-infix { .mat-form-field-infix {
border-top-width: 0.2em; border-top-width: 0.2em;
width: auto; width: auto;
min-width: auto;
} }
.mat-form-field-underline { .mat-form-field-underline {
bottom: 0; bottom: 0;

62
ui-ngx/src/app/modules/home/components/filter/filter-user-info-dialog.component.html

@ -0,0 +1,62 @@
<!--
Copyright © 2016-2020 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<form [formGroup]="filterUserInfoFormGroup" (ngSubmit)="save()" style="width: 500px;">
<mat-toolbar color="primary">
<h2 translate>filter.edit-filter-user-params</h2>
<span fxFlex></span>
<button mat-icon-button
(click)="cancel()"
type="button">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<div mat-dialog-content>
<fieldset [disabled]="isLoading$ | async" fxLayout="column">
<mat-checkbox fxFlex formControlName="editable" style="margin-bottom: 16px;">
{{ 'filter.editable' | translate }}
</mat-checkbox>
<div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>filter.display-label</mat-label>
<input matInput formControlName="label">
</mat-form-field>
<mat-checkbox formControlName="autogeneratedLabel">
{{ 'filter.autogenerated-label' | translate }}
</mat-checkbox>
</div>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>filter.order-priority</mat-label>
<input matInput type="number" formControlName="order">
</mat-form-field>
</fieldset>
</div>
<div mat-dialog-actions fxLayoutAlign="end center">
<button mat-raised-button color="primary"
type="submit"
[disabled]="(isLoading$ | async) || filterUserInfoFormGroup.invalid || !filterUserInfoFormGroup.dirty">
{{ 'action.update' | translate }}
</button>
<button mat-button color="primary"
type="button"
[disabled]="(isLoading$ | async)"
(click)="cancel()"
cdkFocusInitial>
{{ 'action.cancel' | translate }}
</button>
</div>
</form>

108
ui-ngx/src/app/modules/home/components/filter/filter-user-info-dialog.component.ts

@ -0,0 +1,108 @@
///
/// Copyright © 2016-2020 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, Inject, OnInit, SkipSelf } from '@angular/core';
import { ErrorStateMatcher } from '@angular/material/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms';
import { Router } from '@angular/router';
import { DialogComponent } from '@app/shared/components/dialog.component';
import {
BooleanOperation,
EntityKeyValueType, generateUserFilterValueLabel,
KeyFilterPredicateUserInfo, NumericOperation,
StringOperation
} from '@shared/models/query/query.models';
import { TranslateService } from '@ngx-translate/core';
export interface FilterUserInfoDialogData {
key: string;
valueType: EntityKeyValueType;
operation: StringOperation | BooleanOperation | NumericOperation;
keyFilterPredicateUserInfo: KeyFilterPredicateUserInfo;
}
@Component({
selector: 'tb-filter-user-info-dialog',
templateUrl: './filter-user-info-dialog.component.html',
providers: [{provide: ErrorStateMatcher, useExisting: FilterUserInfoDialogComponent}],
styleUrls: []
})
export class FilterUserInfoDialogComponent extends
DialogComponent<FilterUserInfoDialogComponent, KeyFilterPredicateUserInfo>
implements OnInit, ErrorStateMatcher {
filterUserInfoFormGroup: FormGroup;
submitted = false;
constructor(protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: FilterUserInfoDialogData,
@SkipSelf() private errorStateMatcher: ErrorStateMatcher,
public dialogRef: MatDialogRef<FilterUserInfoDialogComponent, KeyFilterPredicateUserInfo>,
private fb: FormBuilder,
private translate: TranslateService) {
super(store, router, dialogRef);
this.filterUserInfoFormGroup = this.fb.group(
{
editable: [this.data.keyFilterPredicateUserInfo.editable],
label: [this.data.keyFilterPredicateUserInfo.label],
autogeneratedLabel: [this.data.keyFilterPredicateUserInfo.autogeneratedLabel],
order: [this.data.keyFilterPredicateUserInfo.order]
}
);
this.onAutogeneratedLabelChange();
this.filterUserInfoFormGroup.get('autogeneratedLabel').valueChanges.subscribe(() => {
this.onAutogeneratedLabelChange();
});
}
private onAutogeneratedLabelChange() {
const autogeneratedLabel: boolean = this.filterUserInfoFormGroup.get('autogeneratedLabel').value;
if (autogeneratedLabel) {
const generatedLabel = generateUserFilterValueLabel(this.data.key, this.data.valueType, this.data.operation, this.translate);
this.filterUserInfoFormGroup.get('label').patchValue(generatedLabel, {emitEvent: false});
this.filterUserInfoFormGroup.get('label').disable({emitEvent: false});
} else {
this.filterUserInfoFormGroup.get('label').enable({emitEvent: false});
}
}
ngOnInit(): void {
}
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
const originalErrorState = this.errorStateMatcher.isErrorState(control, form);
const customErrorState = !!(control && control.invalid && this.submitted);
return originalErrorState || customErrorState;
}
cancel(): void {
this.dialogRef.close(null);
}
save(): void {
this.submitted = true;
if (this.filterUserInfoFormGroup.valid) {
const keyFilterPredicateUserInfo: KeyFilterPredicateUserInfo = this.filterUserInfoFormGroup.getRawValue();
this.dialogRef.close(keyFilterPredicateUserInfo);
}
}
}

26
ui-ngx/src/app/modules/home/components/filter/filter-user-info.component.html

@ -0,0 +1,26 @@
<!--
Copyright © 2016-2020 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<button mat-icon-button color="primary"
class="tb-mat-32"
[fxShow]="!disabled"
type="button"
(click)="openFilterUserInfoDialog()"
matTooltip="{{ 'filter.edit-filter-user-params' | translate }}"
matTooltipPosition="above">
<mat-icon>settings</mat-icon>
</button>

104
ui-ngx/src/app/modules/home/components/filter/filter-user-info.component.ts

@ -0,0 +1,104 @@
///
/// Copyright © 2016-2020 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, forwardRef, Input, OnInit } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import {
BooleanOperation,
EntityKeyValueType,
KeyFilterPredicateUserInfo, NumericOperation,
StringOperation
} from '@shared/models/query/query.models';
import { MatDialog } from '@angular/material/dialog';
import {
FilterUserInfoDialogComponent,
FilterUserInfoDialogData
} from '@home/components/filter/filter-user-info-dialog.component';
import { deepClone } from '@core/utils';
@Component({
selector: 'tb-filter-user-info',
templateUrl: './filter-user-info.component.html',
styleUrls: [],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => FilterUserInfoComponent),
multi: true
}
]
})
export class FilterUserInfoComponent implements ControlValueAccessor, OnInit {
@Input() disabled: boolean;
@Input() key: string;
@Input() operation: StringOperation | BooleanOperation | NumericOperation;
@Input() valueType: EntityKeyValueType;
private propagateChange = null;
private keyFilterPredicateUserInfo: KeyFilterPredicateUserInfo;
constructor(private dialog: MatDialog) {
}
ngOnInit(): void {
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
setDisabledState?(isDisabled: boolean): void {
this.disabled = isDisabled;
}
writeValue(keyFilterPredicateUserInfo: KeyFilterPredicateUserInfo): void {
this.keyFilterPredicateUserInfo = keyFilterPredicateUserInfo;
}
private openFilterUserInfoDialog() {
this.dialog.open<FilterUserInfoDialogComponent, FilterUserInfoDialogData,
KeyFilterPredicateUserInfo>(FilterUserInfoDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
keyFilterPredicateUserInfo: deepClone(this.keyFilterPredicateUserInfo),
valueType: this.valueType,
key: this.key,
operation: this.operation
}
}).afterClosed().subscribe(
(result) => {
if (result) {
this.keyFilterPredicateUserInfo = result;
this.updateModel();
}
}
);
}
private updateModel() {
this.propagateChange(this.keyFilterPredicateUserInfo);
}
}

3
ui-ngx/src/app/modules/home/components/filter/filters-dialog.component.ts

@ -184,8 +184,7 @@ export class FiltersDialogComponent extends DialogComponent<FiltersDialogCompone
data: { data: {
isAdd, isAdd,
filters: filtersArray, filters: filtersArray,
filter: isAdd ? null : deepClone(filter), filter: isAdd ? null : deepClone(filter)
userMode: false
} }
}).afterClosed().subscribe((result) => { }).afterClosed().subscribe((result) => {
if (result) { if (result) {

11
ui-ngx/src/app/modules/home/components/filter/filters-edit-panel.component.ts

@ -18,8 +18,8 @@ import { Component, Inject, InjectionToken } from '@angular/core';
import { IAliasController } from '@core/api/widget-api.models'; import { IAliasController } from '@core/api/widget-api.models';
import { Filter, FilterInfo } from '@shared/models/query/query.models'; import { Filter, FilterInfo } from '@shared/models/query/query.models';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { FilterDialogComponent, FilterDialogData } from '@home/components/filter/filter-dialog.component';
import { deepClone } from '@core/utils'; import { deepClone } from '@core/utils';
import { UserFilterDialogComponent, UserFilterDialogData } from '@home/components/filter/user-filter-dialog.component';
export const FILTER_EDIT_PANEL_DATA = new InjectionToken<any>('FiltersEditPanelData'); export const FILTER_EDIT_PANEL_DATA = new InjectionToken<any>('FiltersEditPanelData');
@ -44,15 +44,12 @@ export class FiltersEditPanelComponent {
public editFilter(filterId: string, filter: FilterInfo) { public editFilter(filterId: string, filter: FilterInfo) {
const singleFilter: Filter = {id: filterId, ...deepClone(filter)}; const singleFilter: Filter = {id: filterId, ...deepClone(filter)};
this.dialog.open<FilterDialogComponent, FilterDialogData, this.dialog.open<UserFilterDialogComponent, UserFilterDialogData,
Filter>(FilterDialogComponent, { Filter>(UserFilterDialogComponent, {
disableClose: true, disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: { data: {
isAdd: false, filter: singleFilter
filters: [],
filter: singleFilter,
userMode: true
} }
}).afterClosed().subscribe( }).afterClosed().subscribe(
(result) => { (result) => {

4
ui-ngx/src/app/modules/home/components/filter/filters-edit.component.ts

@ -22,7 +22,7 @@ import { TranslateService } from '@ngx-translate/core';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { BreakpointObserver } from '@angular/cdk/layout'; import { BreakpointObserver } from '@angular/cdk/layout';
import { deepClone } from '@core/utils'; import { deepClone } from '@core/utils';
import { FilterInfo } from '@shared/models/query/query.models'; import { FilterInfo, isFilterEditable } from '@shared/models/query/query.models';
import { import {
FILTER_EDIT_PANEL_DATA, FILTER_EDIT_PANEL_DATA,
FiltersEditPanelComponent, FiltersEditPanelComponent,
@ -144,7 +144,7 @@ export class FiltersEditComponent implements OnInit, OnDestroy {
this.hasEditableFilters = false; this.hasEditableFilters = false;
for (const filterId of Object.keys(allFilters)) { for (const filterId of Object.keys(allFilters)) {
const filterInfo = this.aliasController.getFilterInfo(filterId); const filterInfo = this.aliasController.getFilterInfo(filterId);
if (filterInfo && filterInfo.editable) { if (filterInfo && isFilterEditable(filterInfo)) {
this.filtersInfo[filterId] = deepClone(filterInfo); this.filtersInfo[filterId] = deepClone(filterInfo);
this.hasEditableFilters = true; this.hasEditableFilters = true;
} }

6
ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.html

@ -17,7 +17,7 @@
--> -->
<form [formGroup]="keyFilterFormGroup" (ngSubmit)="save()" style="width: 700px;"> <form [formGroup]="keyFilterFormGroup" (ngSubmit)="save()" style="width: 700px;">
<mat-toolbar color="primary"> <mat-toolbar color="primary">
<h2>{{data.userMode ? data.keyFilter.key.key : ((data.isAdd ? 'filter.add-key-filter' : 'filter.edit-key-filter') | translate)}}</h2> <h2>{{(data.isAdd ? 'filter.add-key-filter' : 'filter.edit-key-filter') | translate}}</h2>
<span fxFlex></span> <span fxFlex></span>
<button mat-icon-button <button mat-icon-button
(click)="cancel()" (click)="cancel()"
@ -27,7 +27,7 @@
</mat-toolbar> </mat-toolbar>
<div mat-dialog-content> <div mat-dialog-content>
<fieldset [disabled]="isLoading$ | async" fxLayout="column"> <fieldset [disabled]="isLoading$ | async" fxLayout="column">
<section fxLayout="row" fxLayoutGap="8px" class="entity-key" [fxShow]="!data.userMode"> <section fxLayout="row" fxLayoutGap="8px" class="entity-key">
<section fxFlex="70" fxLayout="row" formGroupName="key" fxLayoutGap="8px"> <section fxFlex="70" fxLayout="row" formGroupName="key" fxLayoutGap="8px">
<mat-form-field fxFlex="60" class="mat-block"> <mat-form-field fxFlex="60" class="mat-block">
<mat-label translate>filter.key-name</mat-label> <mat-label translate>filter.key-name</mat-label>
@ -63,8 +63,8 @@
</mat-form-field> </mat-form-field>
</section> </section>
<tb-filter-predicate-list *ngIf="keyFilterFormGroup.get('valueType').value" <tb-filter-predicate-list *ngIf="keyFilterFormGroup.get('valueType').value"
[userMode]="data.userMode"
[valueType]="keyFilterFormGroup.get('valueType').value" [valueType]="keyFilterFormGroup.get('valueType').value"
[key]="keyFilterFormGroup.get('key.key').value"
formControlName="predicates"> formControlName="predicates">
</tb-filter-predicate-list> </tb-filter-predicate-list>
</fieldset> </fieldset>

1
ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.ts

@ -34,7 +34,6 @@ import { TranslateService } from '@ngx-translate/core';
export interface KeyFilterDialogData { export interface KeyFilterDialogData {
keyFilter: KeyFilterInfo; keyFilter: KeyFilterInfo;
userMode: boolean;
isAdd: boolean; isAdd: boolean;
} }

8
ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.html

@ -27,8 +27,8 @@
<div fxLayout="row" fxLayoutAlign="start center" fxFlex="92"> <div fxLayout="row" fxLayoutAlign="start center" fxFlex="92">
<label fxFlex translate class="tb-title no-padding">filter.key-name</label> <label fxFlex translate class="tb-title no-padding">filter.key-name</label>
<label fxFlex translate class="tb-title no-padding">filter.key-type.key-type</label> <label fxFlex translate class="tb-title no-padding">filter.key-type.key-type</label>
<span [fxShow]="!disabled && !userMode" style="min-width: 80px;">&nbsp;</span> <span [fxShow]="!disabled" style="min-width: 80px;">&nbsp;</span>
<span [fxShow]="disabled || userMode" style="min-width: 40px;">&nbsp;</span> <span [fxShow]="disabled" style="min-width: 40px;">&nbsp;</span>
</div> </div>
</div> </div>
<mat-divider></mat-divider> <mat-divider></mat-divider>
@ -51,7 +51,7 @@
<mat-icon>edit</mat-icon> <mat-icon>edit</mat-icon>
</button> </button>
<button mat-icon-button color="primary" <button mat-icon-button color="primary"
[fxShow]="!disabled && !userMode" [fxShow]="!disabled"
type="button" type="button"
(click)="removeKeyFilter($index)" (click)="removeKeyFilter($index)"
matTooltip="{{ 'filter.remove-key-filter' | translate }}" matTooltip="{{ 'filter.remove-key-filter' | translate }}"
@ -68,7 +68,7 @@
</div> </div>
<div style="margin-top: 16px;"> <div style="margin-top: 16px;">
<button mat-button mat-raised-button color="primary" <button mat-button mat-raised-button color="primary"
[fxShow]="!disabled && !userMode" [fxShow]="!disabled"
(click)="addKeyFilter()" (click)="addKeyFilter()"
type="button" type="button"
matTooltip="{{ 'filter.add-key-filter' | translate }}" matTooltip="{{ 'filter.add-key-filter' | translate }}"

3
ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.ts

@ -46,8 +46,6 @@ export class KeyFilterListComponent implements ControlValueAccessor, OnInit {
@Input() disabled: boolean; @Input() disabled: boolean;
@Input() userMode: boolean;
keyFilterListFormGroup: FormGroup; keyFilterListFormGroup: FormGroup;
entityKeyTypeTranslations = entityKeyTypeTranslationMap; entityKeyTypeTranslations = entityKeyTypeTranslationMap;
@ -150,7 +148,6 @@ export class KeyFilterListComponent implements ControlValueAccessor, OnInit {
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: { data: {
keyFilter: keyFilter ? deepClone(keyFilter): null, keyFilter: keyFilter ? deepClone(keyFilter): null,
userMode: this.userMode,
isAdd isAdd
} }
}).afterClosed(); }).afterClosed();

22
ui-ngx/src/app/modules/home/components/filter/numeric-filter-predicate.component.html

@ -16,7 +16,7 @@
--> -->
<div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px" [formGroup]="numericFilterPredicateFormGroup"> <div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px" [formGroup]="numericFilterPredicateFormGroup">
<mat-form-field floatLabel="always" hideRequiredMarker fxFlex class="mat-block"> <mat-form-field floatLabel="always" hideRequiredMarker fxFlex="40" class="mat-block">
<mat-label></mat-label> <mat-label></mat-label>
<mat-select required formControlName="operation" placeholder="{{'filter.operation.operation' | translate}}"> <mat-select required formControlName="operation" placeholder="{{'filter.operation.operation' | translate}}">
<mat-option *ngFor="let operation of numericOperations" [value]="operation"> <mat-option *ngFor="let operation of numericOperations" [value]="operation">
@ -24,9 +24,19 @@
</mat-option> </mat-option>
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
<mat-form-field floatLabel="always" hideRequiredMarker fxFlex class="mat-block"> <div fxFlex="60" fxLayout="column" [ngSwitch]="valueType">
<mat-label></mat-label> <ng-template [ngSwitchCase]="valueTypeEnum.NUMERIC">
<input required type="number" matInput formControlName="value" <mat-form-field floatLabel="always" hideRequiredMarker fxFlex class="mat-block">
placeholder="{{'filter.value' | translate}}"> <mat-label></mat-label>
</mat-form-field> <input required type="number" matInput formControlName="value"
placeholder="{{'filter.value' | translate}}">
</mat-form-field>
</ng-template>
<ng-template [ngSwitchCase]="valueTypeEnum.DATE_TIME">
<tb-datetime fxFlex formControlName="value"
dateText="filter.date"
timeText="filter.time"
required [showLabel]="false"></tb-datetime>
</ng-template>
</div>
</div> </div>

8
ui-ngx/src/app/modules/home/components/filter/numeric-filter-predicate.component.ts

@ -17,6 +17,7 @@
import { Component, forwardRef, Input, OnInit } from '@angular/core'; import { Component, forwardRef, Input, OnInit } from '@angular/core';
import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms';
import { import {
EntityKeyValueType,
FilterPredicateType, NumericFilterPredicate, NumericOperation, numericOperationTranslationMap, FilterPredicateType, NumericFilterPredicate, NumericOperation, numericOperationTranslationMap,
} from '@shared/models/query/query.models'; } from '@shared/models/query/query.models';
import { isDefined } from '@core/utils'; import { isDefined } from '@core/utils';
@ -37,10 +38,12 @@ export class NumericFilterPredicateComponent implements ControlValueAccessor, On
@Input() disabled: boolean; @Input() disabled: boolean;
@Input() userMode: boolean; @Input() valueType: EntityKeyValueType;
numericFilterPredicateFormGroup: FormGroup; numericFilterPredicateFormGroup: FormGroup;
valueTypeEnum = EntityKeyValueType;
numericOperations = Object.keys(NumericOperation); numericOperations = Object.keys(NumericOperation);
numericOperationEnum = NumericOperation; numericOperationEnum = NumericOperation;
numericOperationTranslations = numericOperationTranslationMap; numericOperationTranslations = numericOperationTranslationMap;
@ -55,9 +58,6 @@ export class NumericFilterPredicateComponent implements ControlValueAccessor, On
operation: [NumericOperation.EQUAL, [Validators.required]], operation: [NumericOperation.EQUAL, [Validators.required]],
value: [0, [Validators.required]] value: [0, [Validators.required]]
}); });
if (this.userMode) {
this.numericFilterPredicateFormGroup.get('operation').disable({emitEvent: false});
}
this.numericFilterPredicateFormGroup.valueChanges.subscribe(() => { this.numericFilterPredicateFormGroup.valueChanges.subscribe(() => {
this.updateModel(); this.updateModel();
}); });

4
ui-ngx/src/app/modules/home/components/filter/string-filter-predicate.component.html

@ -16,7 +16,7 @@
--> -->
<div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px" [formGroup]="stringFilterPredicateFormGroup"> <div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px" [formGroup]="stringFilterPredicateFormGroup">
<div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px"> <div fxFlex="40" fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<mat-form-field floatLabel="always" hideRequiredMarker fxFlex class="mat-block"> <mat-form-field floatLabel="always" hideRequiredMarker fxFlex class="mat-block">
<mat-label></mat-label> <mat-label></mat-label>
<mat-select required formControlName="operation" placeholder="{{'filter.operation.operation' | translate}}"> <mat-select required formControlName="operation" placeholder="{{'filter.operation.operation' | translate}}">
@ -28,7 +28,7 @@
<mat-checkbox fxLayout="row" fxLayoutAlign="center" formControlName="ignoreCase" style="min-width: 70px;"> <mat-checkbox fxLayout="row" fxLayoutAlign="center" formControlName="ignoreCase" style="min-width: 70px;">
</mat-checkbox> </mat-checkbox>
</div> </div>
<mat-form-field floatLabel="always" hideRequiredMarker fxFlex class="mat-block"> <mat-form-field floatLabel="always" hideRequiredMarker fxFlex="60" class="mat-block">
<mat-label></mat-label> <mat-label></mat-label>
<input matInput formControlName="value" placeholder="{{'filter.value' | translate}}"> <input matInput formControlName="value" placeholder="{{'filter.value' | translate}}">
</mat-form-field> </mat-form-field>

6
ui-ngx/src/app/modules/home/components/filter/string-filter-predicate.component.ts

@ -39,8 +39,6 @@ export class StringFilterPredicateComponent implements ControlValueAccessor, OnI
@Input() disabled: boolean; @Input() disabled: boolean;
@Input() userMode: boolean;
stringFilterPredicateFormGroup: FormGroup; stringFilterPredicateFormGroup: FormGroup;
stringOperations = Object.keys(StringOperation); stringOperations = Object.keys(StringOperation);
@ -58,10 +56,6 @@ export class StringFilterPredicateComponent implements ControlValueAccessor, OnI
value: [''], value: [''],
ignoreCase: [false] ignoreCase: [false]
}); });
if (this.userMode) {
this.stringFilterPredicateFormGroup.get('operation').disable({emitEvent: false});
this.stringFilterPredicateFormGroup.get('ignoreCase').disable({emitEvent: false});
}
this.stringFilterPredicateFormGroup.valueChanges.subscribe(() => { this.stringFilterPredicateFormGroup.valueChanges.subscribe(() => {
this.updateModel(); this.updateModel();
}); });

80
ui-ngx/src/app/modules/home/components/filter/user-filter-dialog.component.html

@ -0,0 +1,80 @@
<!--
Copyright © 2016-2020 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<form [formGroup]="userFilterFormGroup" (ngSubmit)="save()" style="width: 400px;">
<mat-toolbar color="primary">
<h2>{{ filter.filter }}</h2>
<span fxFlex></span>
<button mat-icon-button
(click)="cancel()"
type="button">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
<div mat-dialog-content>
<fieldset [disabled]="isLoading$ | async">
<div fxFlex fxLayout="column">
<div fxFlex fxLayout="row" fxLayoutAlign="start center"
formArrayName="userInputs"
*ngFor="let userInputControl of userInputsFormArray().controls; let $index = index">
<div fxFlex fxLayout="column"
[ngSwitch]="userInputControl.get('valueType').value">
<ng-template [ngSwitchCase]="valueTypeEnum.STRING">
<mat-form-field fxFlex class="mat-block">
<mat-label>{{ userInputControl.get('label').value }}</mat-label>
<input matInput [formControl]="userInputControl.get('value')">
</mat-form-field>
</ng-template>
<ng-template [ngSwitchCase]="valueTypeEnum.NUMERIC">
<mat-form-field fxFlex class="mat-block">
<mat-label>{{ userInputControl.get('label').value }}</mat-label>
<input required type="number" matInput [formControl]="userInputControl.get('value')">
</mat-form-field>
</ng-template>
<ng-template [ngSwitchCase]="valueTypeEnum.DATE_TIME">
<label class="tb-title no-padding tb-required">{{ userInputControl.get('label').value }}</label>
<tb-datetime fxFlex [formControl]="userInputControl.get('value')"
dateText="filter.date"
timeText="filter.time"
required [showLabel]="false"></tb-datetime>
</ng-template>
<ng-template [ngSwitchCase]="valueTypeEnum.BOOLEAN">
<mat-checkbox labelPosition="before" fxFlex [formControl]="userInputControl.get('value')">
{{ userInputControl.get('label').value }}
</mat-checkbox>
</ng-template>
</div>
</div>
</div>
</fieldset>
</div>
<div mat-dialog-actions fxLayoutAlign="end center">
<button mat-raised-button color="primary"
type="submit"
[disabled]="(isLoading$ | async) || userFilterFormGroup.invalid || !userFilterFormGroup.dirty">
{{ 'action.update' | translate }}
</button>
<button mat-button color="primary"
type="button"
[disabled]="(isLoading$ | async)"
(click)="cancel()" cdkFocusInitial>
{{ 'action.cancel' | translate }}
</button>
</div>
</form>

118
ui-ngx/src/app/modules/home/components/filter/user-filter-dialog.component.ts

@ -0,0 +1,118 @@
///
/// Copyright © 2016-2020 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, Inject, OnInit, SkipSelf } from '@angular/core';
import { ErrorStateMatcher } from '@angular/material/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import {
AbstractControl, FormArray,
FormBuilder,
FormControl,
FormGroup,
FormGroupDirective,
NgForm,
Validators
} from '@angular/forms';
import { Router } from '@angular/router';
import { DialogComponent } from '@app/shared/components/dialog.component';
import { TranslateService } from '@ngx-translate/core';
import {
EntityKeyValueType,
Filter,
filterToUserFilterInfoList,
UserFilterInputInfo
} from '@shared/models/query/query.models';
export interface UserFilterDialogData {
filter: Filter;
}
@Component({
selector: 'tb-user-filter-dialog',
templateUrl: './user-filter-dialog.component.html',
providers: [{provide: ErrorStateMatcher, useExisting: UserFilterDialogComponent}],
styleUrls: []
})
export class UserFilterDialogComponent extends DialogComponent<UserFilterDialogComponent, Filter>
implements OnInit, ErrorStateMatcher {
filter: Filter;
userFilterFormGroup: FormGroup;
valueTypeEnum = EntityKeyValueType;
submitted = false;
constructor(protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: UserFilterDialogData,
@SkipSelf() private errorStateMatcher: ErrorStateMatcher,
public dialogRef: MatDialogRef<UserFilterDialogComponent, Filter>,
private fb: FormBuilder,
public translate: TranslateService) {
super(store, router, dialogRef);
this.filter = data.filter;
const userInputs = filterToUserFilterInfoList(this.filter, translate);
const userInputControls: Array<AbstractControl> = [];
for (const userInput of userInputs) {
userInputControls.push(this.createUserInputFormControl(userInput));
}
this.userFilterFormGroup = this.fb.group({
userInputs: this.fb.array(userInputControls)
});
}
private createUserInputFormControl(userInput: UserFilterInputInfo): AbstractControl {
const userInputControl = this.fb.group({
label: [userInput.label],
valueType: [userInput.valueType],
value: [(userInput.info.keyFilterPredicate as any).value,
userInput.valueType === EntityKeyValueType.NUMERIC ||
userInput.valueType === EntityKeyValueType.DATE_TIME ? [Validators.required] : []]
});
userInputControl.get('value').valueChanges.subscribe(value => {
(userInput.info.keyFilterPredicate as any).value = value;
});
return userInputControl;
}
userInputsFormArray(): FormArray {
return this.userFilterFormGroup.get('userInputs') as FormArray;
}
ngOnInit(): void {
}
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
const originalErrorState = this.errorStateMatcher.isErrorState(control, form);
const customErrorState = !!(control && control.invalid && this.submitted);
return originalErrorState || customErrorState;
}
cancel(): void {
this.dialogRef.close(null);
}
save(): void {
this.submitted = true;
this.dialogRef.close(this.filter);
}
}

11
ui-ngx/src/app/modules/home/components/home-components.module.ts

@ -80,6 +80,9 @@ import { FilterDialogComponent } from '@home/components/filter/filter-dialog.com
import { FilterSelectComponent } from './filter/filter-select.component'; import { FilterSelectComponent } from './filter/filter-select.component';
import { FiltersEditComponent } from '@home/components/filter/filters-edit.component'; import { FiltersEditComponent } from '@home/components/filter/filters-edit.component';
import { FiltersEditPanelComponent } from '@home/components/filter/filters-edit-panel.component'; import { FiltersEditPanelComponent } from '@home/components/filter/filters-edit-panel.component';
import { UserFilterDialogComponent } from '@home/components/filter/user-filter-dialog.component';
import { FilterUserInfoComponent } from './filter/filter-user-info.component';
import { FilterUserInfoDialogComponent } from './filter/filter-user-info-dialog.component';
@NgModule({ @NgModule({
declarations: declarations:
@ -142,7 +145,10 @@ import { FiltersEditPanelComponent } from '@home/components/filter/filters-edit-
FiltersDialogComponent, FiltersDialogComponent,
FilterSelectComponent, FilterSelectComponent,
FiltersEditComponent, FiltersEditComponent,
FiltersEditPanelComponent FiltersEditPanelComponent,
UserFilterDialogComponent,
FilterUserInfoComponent,
FilterUserInfoDialogComponent
], ],
imports: [ imports: [
CommonModule, CommonModule,
@ -197,7 +203,8 @@ import { FiltersEditPanelComponent } from '@home/components/filter/filters-edit-
FilterDialogComponent, FilterDialogComponent,
FiltersDialogComponent, FiltersDialogComponent,
FilterSelectComponent, FilterSelectComponent,
FiltersEditComponent FiltersEditComponent,
UserFilterDialogComponent
], ],
providers: [ providers: [
WidgetComponentService, WidgetComponentService,

5
ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts

@ -633,7 +633,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
} else { } else {
let label: string = chip; let label: string = chip;
if (type === DataKeyType.alarm || type === DataKeyType.entityField) { if (type === DataKeyType.alarm || type === DataKeyType.entityField) {
const keyField = type === DataKeyType.alarm ? alarmFields[label] : entityFields[chip];; const keyField = type === DataKeyType.alarm ? alarmFields[label] : entityFields[chip];
if (keyField) { if (keyField) {
label = this.translate.instant(keyField.name); label = this.translate.instant(keyField.name);
} }
@ -729,8 +729,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
data: { data: {
isAdd: true, isAdd: true,
filters: this.filters, filters: this.filters,
filter: singleFilter, filter: singleFilter
userMode: false
} }
}).afterClosed().pipe( }).afterClosed().pipe(
tap((result) => { tap((result) => {

8
ui-ngx/src/app/shared/components/time/datetime.component.html

@ -16,8 +16,8 @@
--> -->
<section fxLayout="row" fxLayoutAlign="start start" fxLayoutGap="16px"> <section fxLayout="row" fxLayoutAlign="start start" fxLayoutGap="16px">
<mat-form-field> <mat-form-field [floatLabel]="showLabel ? 'auto' : 'always'" [hideRequiredMarker]="!showLabel" [ngClass]="{'no-label': !showLabel}">
<mat-placeholder>{{ dateText | translate }}</mat-placeholder> <mat-placeholder *ngIf="showLabel">{{ dateText | translate }}</mat-placeholder>
<mat-datetimepicker-toggle [for]="datePicker" matPrefix></mat-datetimepicker-toggle> <mat-datetimepicker-toggle [for]="datePicker" matPrefix></mat-datetimepicker-toggle>
<mat-datetimepicker #datePicker type="date" openOnFocus="true"></mat-datetimepicker> <mat-datetimepicker #datePicker type="date" openOnFocus="true"></mat-datetimepicker>
<input [min]="minDateValue" [max]="maxDateValue" <input [min]="minDateValue" [max]="maxDateValue"
@ -26,8 +26,8 @@
matInput [(ngModel)]="date" matInput [(ngModel)]="date"
[matDatetimepicker]="datePicker" (ngModelChange)="onDateChange()"> [matDatetimepicker]="datePicker" (ngModelChange)="onDateChange()">
</mat-form-field> </mat-form-field>
<mat-form-field> <mat-form-field [floatLabel]="showLabel ? 'auto' : 'always'" [hideRequiredMarker]="!showLabel" [ngClass]="{'no-label': !showLabel}">
<mat-placeholder>{{ timeText | translate }}</mat-placeholder> <mat-placeholder *ngIf="showLabel">{{ timeText | translate }}</mat-placeholder>
<mat-datetimepicker-toggle [for]="timePicker" matPrefix></mat-datetimepicker-toggle> <mat-datetimepicker-toggle [for]="timePicker" matPrefix></mat-datetimepicker-toggle>
<mat-datetimepicker #timePicker type="time" openOnFocus="true"></mat-datetimepicker> <mat-datetimepicker #timePicker type="time" openOnFocus="true"></mat-datetimepicker>
<input [min]="minDateValue" [max]="maxDateValue" <input [min]="minDateValue" [max]="maxDateValue"

7
ui-ngx/src/app/shared/components/time/datetime.component.scss

@ -24,4 +24,11 @@
width: auto; width: auto;
min-width: 100px; min-width: 100px;
} }
mat-form-field {
&.no-label {
.mat-form-field-infix {
border-top-width: 0.2em;
}
}
}
} }

3
ui-ngx/src/app/shared/components/time/datetime.component.ts

@ -50,6 +50,9 @@ export class DatetimeComponent implements OnInit, ControlValueAccessor {
@Input() @Input()
timeText: string; timeText: string;
@Input()
showLabel = true;
minDateValue: Date | null; minDateValue: Date | null;
@Input() @Input()

60
ui-ngx/src/app/shared/components/toast.directive.ts

@ -71,10 +71,10 @@ export class ToastDirective implements AfterViewInit, OnDestroy {
if (this.shouldDisplayMessage(notificationMessage)) { if (this.shouldDisplayMessage(notificationMessage)) {
this.currentMessage = notificationMessage; this.currentMessage = notificationMessage;
const isGtSm = this.breakpointObserver.isMatched(MediaBreakpoints['gt-sm']); const isGtSm = this.breakpointObserver.isMatched(MediaBreakpoints['gt-sm']);
if (isGtSm) { if (isGtSm && this.toastTarget !== 'root') {
this.showToastPanel(notificationMessage); this.showToastPanel(notificationMessage);
} else { } else {
this.showSnackBar(notificationMessage); this.showSnackBar(notificationMessage, isGtSm);
} }
} }
} }
@ -180,13 +180,14 @@ export class ToastDirective implements AfterViewInit, OnDestroy {
}); });
} }
private showSnackBar(notificationMessage: NotificationMessage) { private showSnackBar(notificationMessage: NotificationMessage, isGtSm: boolean) {
const data: ToastPanelData = { const data: ToastPanelData = {
notification: notificationMessage notification: notificationMessage,
parent: this.elementRef
}; };
const config: MatSnackBarConfig = { const config: MatSnackBarConfig = {
horizontalPosition: notificationMessage.horizontalPosition || 'left', horizontalPosition: notificationMessage.horizontalPosition || 'left',
verticalPosition: 'bottom', verticalPosition: !isGtSm ? 'bottom' : (notificationMessage.verticalPosition || 'top'),
viewContainerRef: this.viewContainerRef, viewContainerRef: this.viewContainerRef,
duration: notificationMessage.duration, duration: notificationMessage.duration,
panelClass: notificationMessage.panelClass, panelClass: notificationMessage.panelClass,
@ -248,6 +249,7 @@ export class ToastDirective implements AfterViewInit, OnDestroy {
interface ToastPanelData { interface ToastPanelData {
notification: NotificationMessage; notification: NotificationMessage;
parent?: ElementRef;
} }
import { import {
@ -259,6 +261,7 @@ import {
style, style,
animate, animate,
} from '@angular/animations'; } from '@angular/animations';
import { onParentScrollOrWindowResize } from '@core/utils';
export const toastAnimations: { export const toastAnimations: {
readonly showHideToast: AnimationTriggerMetadata; readonly showHideToast: AnimationTriggerMetadata;
@ -285,6 +288,10 @@ export class TbSnackBarComponent implements AfterViewInit, OnDestroy {
@ViewChild('actionButton', {static: true}) actionButton: MatButton; @ViewChild('actionButton', {static: true}) actionButton: MatButton;
private parentEl: HTMLElement;
private snackBarContainerEl: HTMLElement;
private parentScrollSubscription: Subscription = null;
public notification: NotificationMessage; public notification: NotificationMessage;
animationState: ToastAnimationState; animationState: ToastAnimationState;
@ -296,6 +303,7 @@ export class TbSnackBarComponent implements AfterViewInit, OnDestroy {
constructor(@Inject(MAT_SNACK_BAR_DATA) constructor(@Inject(MAT_SNACK_BAR_DATA)
private data: ToastPanelData, private data: ToastPanelData,
private elementRef: ElementRef,
@Optional() @Optional()
private snackBarRef: MatSnackBarRef<TbSnackBarComponent>, private snackBarRef: MatSnackBarRef<TbSnackBarComponent>,
@Optional() @Optional()
@ -305,9 +313,51 @@ export class TbSnackBarComponent implements AfterViewInit, OnDestroy {
} }
ngAfterViewInit() { ngAfterViewInit() {
if (this.snackBarRef) {
this.parentEl = this.data.parent.nativeElement;
this.snackBarContainerEl = this.elementRef.nativeElement.parentNode;
this.snackBarContainerEl.style.position = 'absolute';
this.updateContainerRect();
this.updatePosition(this.snackBarRef.containerInstance.snackBarConfig);
const snackBarComponent = this;
this.parentScrollSubscription = onParentScrollOrWindowResize(this.parentEl).subscribe(() => {
snackBarComponent.updateContainerRect();
});
}
}
private updatePosition(config: MatSnackBarConfig) {
const isRtl = config.direction === 'rtl';
const isLeft = (config.horizontalPosition === 'left' ||
(config.horizontalPosition === 'start' && !isRtl) ||
(config.horizontalPosition === 'end' && isRtl));
const isRight = !isLeft && config.horizontalPosition !== 'center';
if (isLeft) {
this.snackBarContainerEl.style.justifyContent = 'flex-start';
} else if (isRight) {
this.snackBarContainerEl.style.justifyContent = 'flex-end';
} else {
this.snackBarContainerEl.style.justifyContent = 'center';
}
if (config.verticalPosition === 'top') {
this.snackBarContainerEl.style.alignItems = 'flex-start';
} else {
this.snackBarContainerEl.style.alignItems = 'flex-end';
}
}
private updateContainerRect() {
const viewportOffset = this.parentEl.getBoundingClientRect();
this.snackBarContainerEl.style.top = viewportOffset.top + 'px';
this.snackBarContainerEl.style.left = viewportOffset.left + 'px';
this.snackBarContainerEl.style.width = viewportOffset.width + 'px';
this.snackBarContainerEl.style.height = viewportOffset.height + 'px';
} }
ngOnDestroy() { ngOnDestroy() {
if (this.parentScrollSubscription) {
this.parentScrollSubscription.unsubscribe();
}
} }
action(): void { action(): void {

162
ui-ngx/src/app/shared/models/query/query.models.ts

@ -22,7 +22,8 @@ import { EntityInfo } from '@shared/models/entity.models';
import { EntityType } from '@shared/models/entity-type.models'; import { EntityType } from '@shared/models/entity-type.models';
import { Datasource, DatasourceType } from '@shared/models/widget.models'; import { Datasource, DatasourceType } from '@shared/models/widget.models';
import { PageData } from '@shared/models/page/page-data'; import { PageData } from '@shared/models/page/page-data';
import { isEqual } from '@core/utils'; import { isDefined, isEqual } from '@core/utils';
import { TranslateService } from '@ngx-translate/core';
export enum EntityKeyType { export enum EntityKeyType {
ATTRIBUTE = 'ATTRIBUTE', ATTRIBUTE = 'ATTRIBUTE',
@ -63,7 +64,8 @@ export interface EntityKey {
export enum EntityKeyValueType { export enum EntityKeyValueType {
STRING = 'STRING', STRING = 'STRING',
NUMERIC = 'NUMERIC', NUMERIC = 'NUMERIC',
BOOLEAN = 'BOOLEAN' BOOLEAN = 'BOOLEAN',
DATE_TIME = 'DATE_TIME'
} }
export interface EntityKeyValueTypeData { export interface EntityKeyValueTypeData {
@ -93,6 +95,13 @@ export const entityKeyValueTypesMap = new Map<EntityKeyValueType, EntityKeyValue
name: 'filter.value-type.boolean', name: 'filter.value-type.boolean',
icon: 'mdi:checkbox-marked-outline' icon: 'mdi:checkbox-marked-outline'
} }
],
[
EntityKeyValueType.DATE_TIME,
{
name: 'filter.value-type.date-time',
icon: 'mdi:calendar-clock'
}
] ]
] ]
); );
@ -102,12 +111,26 @@ export function entityKeyValueTypeToFilterPredicateType(valueType: EntityKeyValu
case EntityKeyValueType.STRING: case EntityKeyValueType.STRING:
return FilterPredicateType.STRING; return FilterPredicateType.STRING;
case EntityKeyValueType.NUMERIC: case EntityKeyValueType.NUMERIC:
case EntityKeyValueType.DATE_TIME:
return FilterPredicateType.NUMERIC; return FilterPredicateType.NUMERIC;
case EntityKeyValueType.BOOLEAN: case EntityKeyValueType.BOOLEAN:
return FilterPredicateType.BOOLEAN; return FilterPredicateType.BOOLEAN;
} }
} }
export function createDefaultFilterPredicateInfo(valueType: EntityKeyValueType, complex: boolean): KeyFilterPredicateInfo {
const predicate = createDefaultFilterPredicate(valueType, complex);
return {
keyFilterPredicate: predicate,
userInfo: {
editable: true,
label: '',
autogeneratedLabel: true,
order: 0
}
};
}
export function createDefaultFilterPredicate(valueType: EntityKeyValueType, complex: boolean): KeyFilterPredicate { export function createDefaultFilterPredicate(valueType: EntityKeyValueType, complex: boolean): KeyFilterPredicate {
const predicate = { const predicate = {
type: complex ? FilterPredicateType.COMPLEX : entityKeyValueTypeToFilterPredicateType(valueType) type: complex ? FilterPredicateType.COMPLEX : entityKeyValueTypeToFilterPredicateType(valueType)
@ -120,7 +143,7 @@ export function createDefaultFilterPredicate(valueType: EntityKeyValueType, comp
break; break;
case FilterPredicateType.NUMERIC: case FilterPredicateType.NUMERIC:
predicate.operation = NumericOperation.EQUAL; predicate.operation = NumericOperation.EQUAL;
predicate.value = 0; predicate.value = valueType === EntityKeyValueType.DATE_TIME ? Date.now() : 0;
break; break;
case FilterPredicateType.BOOLEAN: case FilterPredicateType.BOOLEAN:
predicate.operation = BooleanOperation.EQUAL; predicate.operation = BooleanOperation.EQUAL;
@ -224,16 +247,33 @@ export interface BooleanFilterPredicate {
value: boolean; value: boolean;
} }
export interface ComplexFilterPredicate { export interface BaseComplexFilterPredicate<T extends KeyFilterPredicate | KeyFilterPredicateInfo> {
type: FilterPredicateType.COMPLEX, type: FilterPredicateType.COMPLEX,
operation: ComplexOperation; operation: ComplexOperation;
predicates: Array<KeyFilterPredicate>; predicates: Array<T>;
} }
export type ComplexFilterPredicate = BaseComplexFilterPredicate<KeyFilterPredicate>;
export type ComplexFilterPredicateInfo = BaseComplexFilterPredicate<KeyFilterPredicateInfo>;
export type KeyFilterPredicate = StringFilterPredicate | export type KeyFilterPredicate = StringFilterPredicate |
NumericFilterPredicate | NumericFilterPredicate |
BooleanFilterPredicate | BooleanFilterPredicate |
ComplexFilterPredicate; ComplexFilterPredicate |
ComplexFilterPredicateInfo;
export interface KeyFilterPredicateUserInfo {
editable: boolean;
label: string;
autogeneratedLabel: boolean;
order?: number;
}
export interface KeyFilterPredicateInfo {
keyFilterPredicate: KeyFilterPredicate;
userInfo: KeyFilterPredicateUserInfo;
}
export interface KeyFilter { export interface KeyFilter {
key: EntityKey; key: EntityKey;
@ -243,7 +283,7 @@ export interface KeyFilter {
export interface KeyFilterInfo { export interface KeyFilterInfo {
key: EntityKey; key: EntityKey;
valueType: EntityKeyValueType; valueType: EntityKeyValueType;
predicates: Array<KeyFilterPredicate>; predicates: Array<KeyFilterPredicateInfo>;
} }
export interface FilterInfo { export interface FilterInfo {
@ -264,7 +304,7 @@ export function filterInfoToKeyFilters(filter: FilterInfo): Array<KeyFilter> {
for (const predicate of keyFilterInfo.predicates) { for (const predicate of keyFilterInfo.predicates) {
const keyFilter: KeyFilter = { const keyFilter: KeyFilter = {
key, key,
predicate predicate: keyFilterPredicateInfoToKeyFilterPredicate(predicate)
}; };
keyFilters.push(keyFilter); keyFilters.push(keyFilter);
} }
@ -272,6 +312,112 @@ export function filterInfoToKeyFilters(filter: FilterInfo): Array<KeyFilter> {
return keyFilters; return keyFilters;
} }
export function keyFilterPredicateInfoToKeyFilterPredicate(keyFilterPredicateInfo: KeyFilterPredicateInfo): KeyFilterPredicate {
let keyFilterPredicate = keyFilterPredicateInfo.keyFilterPredicate;
if (keyFilterPredicate.type === FilterPredicateType.COMPLEX) {
const complexInfo = keyFilterPredicate as ComplexFilterPredicateInfo;
const predicates = complexInfo.predicates.map((predicateInfo => keyFilterPredicateInfoToKeyFilterPredicate(predicateInfo)));
keyFilterPredicate = {
type: FilterPredicateType.COMPLEX,
operation: complexInfo.operation,
predicates
} as ComplexFilterPredicate;
}
return keyFilterPredicate;
}
export function isFilterEditable(filter: FilterInfo): boolean {
if (filter.editable) {
return filter.keyFilters.some(value => isKeyFilterInfoEditable(value));
} else {
return false;
}
}
export function isKeyFilterInfoEditable(keyFilterInfo: KeyFilterInfo): boolean {
return keyFilterInfo.predicates.some(value => isPredicateInfoEditable(value));
}
export function isPredicateInfoEditable(predicateInfo: KeyFilterPredicateInfo): boolean {
if (predicateInfo.keyFilterPredicate.type === FilterPredicateType.COMPLEX) {
const complexFilterPredicateInfo: ComplexFilterPredicateInfo = predicateInfo.keyFilterPredicate as ComplexFilterPredicateInfo;
return complexFilterPredicateInfo.predicates.some(value => isPredicateInfoEditable(value));
} else {
return predicateInfo.userInfo.editable;
}
}
export interface UserFilterInputInfo {
label: string;
valueType: EntityKeyValueType;
info: KeyFilterPredicateInfo;
}
export function filterToUserFilterInfoList(filter: Filter, translate: TranslateService): Array<UserFilterInputInfo> {
const result = filter.keyFilters.map((keyFilterInfo => keyFilterInfoToUserFilterInfoList(keyFilterInfo, translate)));
let userInputs: Array<UserFilterInputInfo> = [].concat.apply([], result);
userInputs = userInputs.sort((input1, input2) => {
const order1 = isDefined(input1.info.userInfo.order) ? input1.info.userInfo.order : 0;
const order2 = isDefined(input2.info.userInfo.order) ? input2.info.userInfo.order : 0;
return order1 - order2;
});
return userInputs;
}
export function keyFilterInfoToUserFilterInfoList(keyFilterInfo: KeyFilterInfo, translate: TranslateService): Array<UserFilterInputInfo> {
const result = keyFilterInfo.predicates.map((predicateInfo => predicateInfoToUserFilterInfoList(keyFilterInfo.key,
keyFilterInfo.valueType, predicateInfo, translate)));
return [].concat.apply([], result);
}
export function predicateInfoToUserFilterInfoList(key: EntityKey,
valueType: EntityKeyValueType,
predicateInfo: KeyFilterPredicateInfo,
translate: TranslateService): Array<UserFilterInputInfo> {
if (predicateInfo.keyFilterPredicate.type === FilterPredicateType.COMPLEX) {
const complexFilterPredicateInfo: ComplexFilterPredicateInfo = predicateInfo.keyFilterPredicate as ComplexFilterPredicateInfo;
const result = complexFilterPredicateInfo.predicates.map((predicateInfo1 =>
predicateInfoToUserFilterInfoList(key, valueType, predicateInfo1, translate)));
return [].concat.apply([], result);
} else {
if (predicateInfo.userInfo.editable) {
const userInput: UserFilterInputInfo = {
info: predicateInfo,
label: predicateInfo.userInfo.label,
valueType
};
if (predicateInfo.userInfo.autogeneratedLabel) {
userInput.label = generateUserFilterValueLabel(key.key, valueType,
predicateInfo.keyFilterPredicate.operation, translate);
}
return [userInput];
} else {
return [];
}
}
}
export function generateUserFilterValueLabel(key: string, valueType: EntityKeyValueType,
operation: StringOperation | BooleanOperation | NumericOperation,
translate: TranslateService) {
let label = key;
let operationTranslationKey: string;
switch (valueType) {
case EntityKeyValueType.STRING:
operationTranslationKey = stringOperationTranslationMap.get(operation as StringOperation);
break;
case EntityKeyValueType.NUMERIC:
case EntityKeyValueType.DATE_TIME:
operationTranslationKey = numericOperationTranslationMap.get(operation as NumericOperation);
break;
case EntityKeyValueType.BOOLEAN:
operationTranslationKey = booleanOperationTranslationMap.get(operation as BooleanOperation);
break;
}
label += ' ' + translate.instant(operationTranslationKey);
return label;
}
export interface Filter extends FilterInfo { export interface Filter extends FilterInfo {
id: string; id: string;
} }

36
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -1174,18 +1174,18 @@
"filter-required": "Filter is required.", "filter-required": "Filter is required.",
"operation": { "operation": {
"operation": "Operation", "operation": "Operation",
"equal": "Equal", "equal": "equal",
"not-equal": "Not equal", "not-equal": "not equal",
"starts-with": "Starts with", "starts-with": "starts with",
"ends-with": "Ends with", "ends-with": "ends with",
"contains": "Contains", "contains": "contains",
"not-contain": "Not contain", "not-contain": "not contain",
"greater": "Greater than", "greater": "greater than",
"less": "Less than", "less": "less than",
"greater-or-equal": "Greater or equal", "greater-or-equal": "greater or equal",
"less-or-equal": "Less or equal", "less-or-equal": "less or equal",
"and": "And", "and": "and",
"or": "Or" "or": "or"
}, },
"ignore-case": "Ignore case", "ignore-case": "Ignore case",
"value": "Value", "value": "Value",
@ -1196,6 +1196,11 @@
"add-complex": "Add complex", "add-complex": "Add complex",
"complex-filter": "Complex filter", "complex-filter": "Complex filter",
"edit-complex-filter": "Edit complex filter", "edit-complex-filter": "Edit complex filter",
"edit-filter-user-params": "Edit filter predicate user parameters",
"user-parameters": "User parameters",
"display-label": "Label to display",
"autogenerated-label": "Auto generate label",
"order-priority": "Field order priority",
"key-filter": "Key filter", "key-filter": "Key filter",
"key-filters": "Key filters", "key-filters": "Key filters",
"key-name": "Key name", "key-name": "Key name",
@ -1210,7 +1215,8 @@
"value-type": "Value type", "value-type": "Value type",
"string": "String", "string": "String",
"numeric": "Numeric", "numeric": "Numeric",
"boolean": "Boolean" "boolean": "Boolean",
"date-time": "Datetime"
}, },
"value-type-required": "Key value type is required.", "value-type-required": "Key value type is required.",
"key-value-type-change-title": "Are you sure you want to change key value type?", "key-value-type-change-title": "Are you sure you want to change key value type?",
@ -1218,7 +1224,9 @@
"no-key-filters": "No key filters configured", "no-key-filters": "No key filters configured",
"add-key-filter": "Add key filter", "add-key-filter": "Add key filter",
"remove-key-filter": "Remove key filter", "remove-key-filter": "Remove key filter",
"edit-key-filter": "Edit key filter" "edit-key-filter": "Edit key filter",
"date": "Date",
"time": "Time"
}, },
"fullscreen": { "fullscreen": {
"expand": "Expand to fullscreen", "expand": "Expand to fullscreen",

Loading…
Cancel
Save