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> {
const entityFieldKeys: string[] = [];
const entityFieldKeys: string[] = [entityFields.createdTime.keyName];
const query = searchText.toLowerCase();
switch(entityType) {
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">
<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-select required formControlName="operation" placeholder="{{'filter.operation.operation' | translate}}">
<mat-option *ngFor="let operation of booleanOperations" [value]="operation">
@ -24,7 +24,7 @@
</mat-option>
</mat-select>
</mat-form-field>
<mat-checkbox fxFlex formControlName="value">
<mat-checkbox fxFlex="60" formControlName="value">
{{ (booleanFilterPredicateFormGroup.get('value').value ? 'value.true' : 'value.false') | translate }}
</mat-checkbox>
</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() userMode: boolean;
booleanFilterPredicateFormGroup: FormGroup;
booleanOperations = Object.keys(BooleanOperation);
@ -57,9 +55,6 @@ export class BooleanFilterPredicateComponent implements ControlValueAccessor, On
operation: [BooleanOperation.EQUAL, [Validators.required]],
value: [false]
});
if (this.userMode) {
this.booleanFilterPredicateFormGroup.get('operation').disable({emitEvent: false});
}
this.booleanFilterPredicateFormGroup.valueChanges.subscribe(() => {
this.updateModel();
});

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

@ -36,9 +36,9 @@
</mat-select>
</mat-form-field>
<tb-filter-predicate-list
[userMode]="data.userMode"
[valueType]="data.valueType"
[operation]="complexFilterFormGroup.get('operation').value"
[key]="data.key"
formControlName="predicates">
</tb-filter-predicate-list>
</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 {
BooleanOperation, booleanOperationTranslationMap,
ComplexFilterPredicate, ComplexOperation, complexOperationTranslationMap,
ComplexFilterPredicate, ComplexFilterPredicateInfo, ComplexOperation, complexOperationTranslationMap,
EntityKeyValueType,
FilterPredicateType
FilterPredicateType, KeyFilterPredicateInfo
} from '@shared/models/query/query.models';
export interface ComplexFilterPredicateDialogData {
complexPredicate: ComplexFilterPredicate;
userMode: boolean;
complexPredicate: ComplexFilterPredicateInfo;
key: string;
disabled: boolean;
isAdd: boolean;
valueType: EntityKeyValueType;
@ -44,7 +44,7 @@ export interface ComplexFilterPredicateDialogData {
styleUrls: []
})
export class ComplexFilterPredicateDialogComponent extends
DialogComponent<ComplexFilterPredicateDialogComponent, ComplexFilterPredicate>
DialogComponent<ComplexFilterPredicateDialogComponent, ComplexFilterPredicateInfo>
implements OnInit, ErrorStateMatcher {
complexFilterFormGroup: FormGroup;
@ -61,7 +61,7 @@ export class ComplexFilterPredicateDialogComponent extends
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: ComplexFilterPredicateDialogData,
@SkipSelf() private errorStateMatcher: ErrorStateMatcher,
public dialogRef: MatDialogRef<ComplexFilterPredicateDialogComponent, ComplexFilterPredicate>,
public dialogRef: MatDialogRef<ComplexFilterPredicateDialogComponent, ComplexFilterPredicateInfo>,
private fb: FormBuilder) {
super(store, router, dialogRef);
@ -91,7 +91,7 @@ export class ComplexFilterPredicateDialogComponent extends
save(): void {
this.submitted = true;
if (this.complexFilterFormGroup.valid) {
const predicate: ComplexFilterPredicate = this.complexFilterFormGroup.getRawValue();
const predicate: ComplexFilterPredicateInfo = this.complexFilterFormGroup.getRawValue();
predicate.type = FilterPredicateType.COMPLEX;
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 { 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 {
ComplexFilterPredicateDialogComponent,
@ -40,13 +44,13 @@ export class ComplexFilterPredicateComponent implements ControlValueAccessor, On
@Input() disabled: boolean;
@Input() userMode: boolean;
@Input() valueType: EntityKeyValueType;
@Input() key: string;
private propagateChange = null;
private complexFilterPredicate: ComplexFilterPredicate;
private complexFilterPredicate: ComplexFilterPredicateInfo;
constructor(private dialog: MatDialog) {
}
@ -65,21 +69,21 @@ export class ComplexFilterPredicateComponent implements ControlValueAccessor, On
this.disabled = isDisabled;
}
writeValue(predicate: ComplexFilterPredicate): void {
writeValue(predicate: ComplexFilterPredicateInfo): void {
this.complexFilterPredicate = predicate;
}
private openComplexFilterDialog() {
this.dialog.open<ComplexFilterPredicateDialogComponent, ComplexFilterPredicateDialogData,
ComplexFilterPredicate>(ComplexFilterPredicateDialogComponent, {
ComplexFilterPredicateInfo>(ComplexFilterPredicateDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
complexPredicate: deepClone(this.complexFilterPredicate),
disabled: this.disabled,
userMode: this.userMode,
valueType: this.valueType,
isAdd: false
isAdd: false,
key: this.key
}
}).afterClosed().subscribe(
(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;">
<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>
<button mat-icon-button
(click)="cancel()"
@ -30,7 +30,7 @@
<div mat-dialog-content>
<fieldset [disabled]="isLoading$ | async">
<div fxFlex fxLayout="column">
<div fxLayout="row" [fxShow]="!userMode">
<div fxLayout="row">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>filter.name</mat-label>
<input matInput formControlName="filter" required>
@ -49,8 +49,7 @@
</section>
</div>
<tb-key-filter-list
formControlName="keyFilters"
[userMode]="userMode">
formControlName="keyFilters">
</tb-key-filter-list>
</div>
</fieldset>
@ -59,7 +58,7 @@
<button mat-raised-button color="primary"
type="submit"
[disabled]="(isLoading$ | async) || filterFormGroup.invalid || !filterFormGroup.dirty">
{{ (userMode ? 'action.update' : (isAdd ? 'action.add' : 'action.update')) | translate }}
{{ (isAdd ? 'action.add' : 'action.update') | translate }}
</button>
<button mat-button color="primary"
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 {
isAdd: boolean;
userMode: boolean;
filters: Filters | Array<Filter>;
filter?: Filter;
}
@ -51,7 +50,6 @@ export class FilterDialogComponent extends DialogComponent<FilterDialogComponent
implements OnInit, ErrorStateMatcher {
isAdd: boolean;
userMode: boolean;
filters: Array<Filter>;
filter: Filter;
@ -70,7 +68,6 @@ export class FilterDialogComponent extends DialogComponent<FilterDialogComponent
public translate: TranslateService) {
super(store, router, dialogRef);
this.isAdd = data.isAdd;
this.userMode = data.userMode;
if (Array.isArray(data.filters)) {
this.filters = data.filters;
} else {

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

@ -25,13 +25,16 @@
<div fxLayout="row">
<span fxFlex="8"></span>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px" fxFlex="92">
<div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<label fxFlex translate class="tb-title no-padding">filter.operation.operation</label>
<label *ngIf="valueType === valueTypeEnum.STRING"
translate class="tb-title no-padding" style="min-width: 70px;">filter.ignore-case</label>
<div fxFlex fxLayout="row" fxLayoutGap="8px">
<div fxFlex="40" fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<label fxFlex translate class="tb-title no-padding">filter.operation.operation</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>
<label fxFlex translate class="tb-title no-padding">filter.value</label>
<span [fxShow]="!disabled && !userMode" style="min-width: 40px;">&nbsp;</span>
<label translate class="tb-title no-padding" style="width: 60px;">filter.user-parameters</label>
<span [fxShow]="!disabled" style="min-width: 40px;">&nbsp;</span>
</div>
</div>
<mat-divider></mat-divider>
@ -46,12 +49,12 @@
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px" fxFlex>
<tb-filter-predicate
fxFlex
[userMode]="userMode"
[valueType]="valueType"
[key]="key"
[formControl]="predicateControl">
</tb-filter-predicate>
<button mat-icon-button color="primary"
[fxShow]="!disabled && !userMode"
[fxShow]="!disabled"
type="button"
(click)="removePredicate($index)"
matTooltip="{{ 'filter.remove-filter' | translate }}"
@ -67,7 +70,7 @@
</div>
<div style="margin-top: 16px;" fxLayout="row" fxLayoutGap="8px">
<button mat-button mat-raised-button color="primary"
[fxShow]="!disabled && !userMode"
[fxShow]="!disabled"
(click)="addPredicate(false)"
type="button"
matTooltip="{{ 'filter.add-filter' | translate }}"
@ -75,7 +78,7 @@
{{ 'action.add' | translate }}
</button>
<button mat-button mat-raised-button color="primary"
[fxShow]="!disabled && !userMode"
[fxShow]="!disabled"
(click)="addPredicate(true)"
type="button"
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';
import { Observable, of, Subscription } from 'rxjs';
import {
ComplexFilterPredicate,
ComplexOperation, complexOperationTranslationMap,
createDefaultFilterPredicate,
ComplexFilterPredicateInfo,
ComplexOperation,
complexOperationTranslationMap,
createDefaultFilterPredicateInfo,
EntityKeyValueType,
KeyFilterPredicate
KeyFilterPredicateInfo
} from '@shared/models/query/query.models';
import {
ComplexFilterPredicateDialogComponent,
ComplexFilterPredicateDialogData
} from '@home/components/filter/complex-filter-predicate-dialog.component';
import { MatDialog } from '@angular/material/dialog';
import { map } from 'rxjs/operators';
@Component({
selector: 'tb-filter-predicate-list',
@ -54,10 +56,10 @@ export class FilterPredicateListComponent implements ControlValueAccessor, OnIni
@Input() disabled: boolean;
@Input() userMode: boolean;
@Input() valueType: EntityKeyValueType;
@Input() key: string;
@Input() operation: ComplexOperation = ComplexOperation.AND;
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) {
this.valueChangeSubscription.unsubscribe();
}
@ -127,10 +129,10 @@ export class FilterPredicateListComponent implements ControlValueAccessor, OnIni
public addPredicate(complex: boolean) {
const predicatesFormArray = this.filterListFormGroup.get('predicates') as FormArray;
const predicate = createDefaultFilterPredicate(this.valueType, complex);
let observable: Observable<KeyFilterPredicate>;
const predicate = createDefaultFilterPredicateInfo(this.valueType, complex);
let observable: Observable<KeyFilterPredicateInfo>;
if (complex) {
observable = this.openComplexFilterDialog(predicate as ComplexFilterPredicate);
observable = this.openComplexFilterDialog(predicate);
} else {
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,
ComplexFilterPredicate>(ComplexFilterPredicateDialogComponent, {
ComplexFilterPredicateInfo>(ComplexFilterPredicateDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
complexPredicate: predicate,
complexPredicate: predicate.keyFilterPredicate as ComplexFilterPredicateInfo,
disabled: this.disabled,
userMode: this.userMode,
valueType: this.valueType,
key: this.key,
isAdd: true
}
}).afterClosed();
}).afterClosed().pipe(
map((result) => {
if (result) {
predicate.keyFilterPredicate = result;
return predicate;
} else {
return null;
}
})
);
}
private updateModel() {
const predicates: Array<KeyFilterPredicate> = this.filterListFormGroup.getRawValue().predicates;
if (predicates.length) {
const predicates: Array<KeyFilterPredicateInfo> = this.filterListFormGroup.getRawValue().predicates;
if (this.filterListFormGroup.valid && predicates.length) {
this.propagateChange(predicates);
} else {
this.propagateChange(null);

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

@ -15,28 +15,36 @@
limitations under the License.
-->
<div fxLayout="column" [formGroup]="filterPredicateFormGroup"
[ngSwitch]="type">
<ng-template [ngSwitchCase]="filterPredicateType.STRING">
<tb-string-filter-predicate [userMode]="userMode"
formControlName="predicate">
</tb-string-filter-predicate>
</ng-template>
<ng-template [ngSwitchCase]="filterPredicateType.NUMERIC">
<tb-numeric-filter-predicate [userMode]="userMode"
formControlName="predicate">
</tb-numeric-filter-predicate>
</ng-template>
<ng-template [ngSwitchCase]="filterPredicateType.BOOLEAN">
<tb-boolean-filter-predicate [userMode]="userMode"
formControlName="predicate">
</tb-boolean-filter-predicate>
</ng-template>
<ng-template [ngSwitchCase]="filterPredicateType.COMPLEX">
<tb-complex-filter-predicate
[valueType]="valueType"
[userMode]="userMode"
formControlName="predicate">
</tb-complex-filter-predicate>
</ng-template>
<div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px" [formGroup]="filterPredicateFormGroup">
<div fxFlex fxLayout="column" [ngSwitch]="type">
<ng-template [ngSwitchCase]="filterPredicateType.STRING">
<tb-string-filter-predicate formControlName="predicate">
</tb-string-filter-predicate>
</ng-template>
<ng-template [ngSwitchCase]="filterPredicateType.NUMERIC">
<tb-numeric-filter-predicate [valueType]="valueType"
formControlName="predicate">
</tb-numeric-filter-predicate>
</ng-template>
<ng-template [ngSwitchCase]="filterPredicateType.BOOLEAN">
<tb-boolean-filter-predicate formControlName="predicate">
</tb-boolean-filter-predicate>
</ng-template>
<ng-template [ngSwitchCase]="filterPredicateType.COMPLEX">
<tb-complex-filter-predicate
[key]="key"
[valueType]="valueType"
formControlName="predicate">
</tb-complex-filter-predicate>
</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>

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

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

@ -20,6 +20,7 @@
.mat-form-field-infix {
border-top-width: 0.2em;
width: auto;
min-width: auto;
}
.mat-form-field-underline {
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: {
isAdd,
filters: filtersArray,
filter: isAdd ? null : deepClone(filter),
userMode: false
filter: isAdd ? null : deepClone(filter)
}
}).afterClosed().subscribe((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 { Filter, FilterInfo } from '@shared/models/query/query.models';
import { MatDialog } from '@angular/material/dialog';
import { FilterDialogComponent, FilterDialogData } from '@home/components/filter/filter-dialog.component';
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');
@ -44,15 +44,12 @@ export class FiltersEditPanelComponent {
public editFilter(filterId: string, filter: FilterInfo) {
const singleFilter: Filter = {id: filterId, ...deepClone(filter)};
this.dialog.open<FilterDialogComponent, FilterDialogData,
Filter>(FilterDialogComponent, {
this.dialog.open<UserFilterDialogComponent, UserFilterDialogData,
Filter>(UserFilterDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
isAdd: false,
filters: [],
filter: singleFilter,
userMode: true
filter: singleFilter
}
}).afterClosed().subscribe(
(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 { BreakpointObserver } from '@angular/cdk/layout';
import { deepClone } from '@core/utils';
import { FilterInfo } from '@shared/models/query/query.models';
import { FilterInfo, isFilterEditable } from '@shared/models/query/query.models';
import {
FILTER_EDIT_PANEL_DATA,
FiltersEditPanelComponent,
@ -144,7 +144,7 @@ export class FiltersEditComponent implements OnInit, OnDestroy {
this.hasEditableFilters = false;
for (const filterId of Object.keys(allFilters)) {
const filterInfo = this.aliasController.getFilterInfo(filterId);
if (filterInfo && filterInfo.editable) {
if (filterInfo && isFilterEditable(filterInfo)) {
this.filtersInfo[filterId] = deepClone(filterInfo);
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;">
<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>
<button mat-icon-button
(click)="cancel()"
@ -27,7 +27,7 @@
</mat-toolbar>
<div mat-dialog-content>
<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">
<mat-form-field fxFlex="60" class="mat-block">
<mat-label translate>filter.key-name</mat-label>
@ -63,8 +63,8 @@
</mat-form-field>
</section>
<tb-filter-predicate-list *ngIf="keyFilterFormGroup.get('valueType').value"
[userMode]="data.userMode"
[valueType]="keyFilterFormGroup.get('valueType').value"
[key]="keyFilterFormGroup.get('key.key').value"
formControlName="predicates">
</tb-filter-predicate-list>
</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 {
keyFilter: KeyFilterInfo;
userMode: 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">
<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>
<span [fxShow]="!disabled && !userMode" style="min-width: 80px;">&nbsp;</span>
<span [fxShow]="disabled || userMode" style="min-width: 40px;">&nbsp;</span>
<span [fxShow]="!disabled" style="min-width: 80px;">&nbsp;</span>
<span [fxShow]="disabled" style="min-width: 40px;">&nbsp;</span>
</div>
</div>
<mat-divider></mat-divider>
@ -51,7 +51,7 @@
<mat-icon>edit</mat-icon>
</button>
<button mat-icon-button color="primary"
[fxShow]="!disabled && !userMode"
[fxShow]="!disabled"
type="button"
(click)="removeKeyFilter($index)"
matTooltip="{{ 'filter.remove-key-filter' | translate }}"
@ -68,7 +68,7 @@
</div>
<div style="margin-top: 16px;">
<button mat-button mat-raised-button color="primary"
[fxShow]="!disabled && !userMode"
[fxShow]="!disabled"
(click)="addKeyFilter()"
type="button"
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() userMode: boolean;
keyFilterListFormGroup: FormGroup;
entityKeyTypeTranslations = entityKeyTypeTranslationMap;
@ -150,7 +148,6 @@ export class KeyFilterListComponent implements ControlValueAccessor, OnInit {
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
keyFilter: keyFilter ? deepClone(keyFilter): null,
userMode: this.userMode,
isAdd
}
}).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">
<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-select required formControlName="operation" placeholder="{{'filter.operation.operation' | translate}}">
<mat-option *ngFor="let operation of numericOperations" [value]="operation">
@ -24,9 +24,19 @@
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field floatLabel="always" hideRequiredMarker fxFlex class="mat-block">
<mat-label></mat-label>
<input required type="number" matInput formControlName="value"
placeholder="{{'filter.value' | translate}}">
</mat-form-field>
<div fxFlex="60" fxLayout="column" [ngSwitch]="valueType">
<ng-template [ngSwitchCase]="valueTypeEnum.NUMERIC">
<mat-form-field floatLabel="always" hideRequiredMarker fxFlex class="mat-block">
<mat-label></mat-label>
<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>

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 { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms';
import {
EntityKeyValueType,
FilterPredicateType, NumericFilterPredicate, NumericOperation, numericOperationTranslationMap,
} from '@shared/models/query/query.models';
import { isDefined } from '@core/utils';
@ -37,10 +38,12 @@ export class NumericFilterPredicateComponent implements ControlValueAccessor, On
@Input() disabled: boolean;
@Input() userMode: boolean;
@Input() valueType: EntityKeyValueType;
numericFilterPredicateFormGroup: FormGroup;
valueTypeEnum = EntityKeyValueType;
numericOperations = Object.keys(NumericOperation);
numericOperationEnum = NumericOperation;
numericOperationTranslations = numericOperationTranslationMap;
@ -55,9 +58,6 @@ export class NumericFilterPredicateComponent implements ControlValueAccessor, On
operation: [NumericOperation.EQUAL, [Validators.required]],
value: [0, [Validators.required]]
});
if (this.userMode) {
this.numericFilterPredicateFormGroup.get('operation').disable({emitEvent: false});
}
this.numericFilterPredicateFormGroup.valueChanges.subscribe(() => {
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">
<div fxFlex="40" fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<mat-form-field floatLabel="always" hideRequiredMarker fxFlex class="mat-block">
<mat-label></mat-label>
<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>
</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>
<input matInput formControlName="value" placeholder="{{'filter.value' | translate}}">
</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() userMode: boolean;
stringFilterPredicateFormGroup: FormGroup;
stringOperations = Object.keys(StringOperation);
@ -58,10 +56,6 @@ export class StringFilterPredicateComponent implements ControlValueAccessor, OnI
value: [''],
ignoreCase: [false]
});
if (this.userMode) {
this.stringFilterPredicateFormGroup.get('operation').disable({emitEvent: false});
this.stringFilterPredicateFormGroup.get('ignoreCase').disable({emitEvent: false});
}
this.stringFilterPredicateFormGroup.valueChanges.subscribe(() => {
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 { FiltersEditComponent } from '@home/components/filter/filters-edit.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({
declarations:
@ -142,7 +145,10 @@ import { FiltersEditPanelComponent } from '@home/components/filter/filters-edit-
FiltersDialogComponent,
FilterSelectComponent,
FiltersEditComponent,
FiltersEditPanelComponent
FiltersEditPanelComponent,
UserFilterDialogComponent,
FilterUserInfoComponent,
FilterUserInfoDialogComponent
],
imports: [
CommonModule,
@ -197,7 +203,8 @@ import { FiltersEditPanelComponent } from '@home/components/filter/filters-edit-
FilterDialogComponent,
FiltersDialogComponent,
FilterSelectComponent,
FiltersEditComponent
FiltersEditComponent,
UserFilterDialogComponent
],
providers: [
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 {
let label: string = chip;
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) {
label = this.translate.instant(keyField.name);
}
@ -729,8 +729,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
data: {
isAdd: true,
filters: this.filters,
filter: singleFilter,
userMode: false
filter: singleFilter
}
}).afterClosed().pipe(
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">
<mat-form-field>
<mat-placeholder>{{ dateText | translate }}</mat-placeholder>
<mat-form-field [floatLabel]="showLabel ? 'auto' : 'always'" [hideRequiredMarker]="!showLabel" [ngClass]="{'no-label': !showLabel}">
<mat-placeholder *ngIf="showLabel">{{ dateText | translate }}</mat-placeholder>
<mat-datetimepicker-toggle [for]="datePicker" matPrefix></mat-datetimepicker-toggle>
<mat-datetimepicker #datePicker type="date" openOnFocus="true"></mat-datetimepicker>
<input [min]="minDateValue" [max]="maxDateValue"
@ -26,8 +26,8 @@
matInput [(ngModel)]="date"
[matDatetimepicker]="datePicker" (ngModelChange)="onDateChange()">
</mat-form-field>
<mat-form-field>
<mat-placeholder>{{ timeText | translate }}</mat-placeholder>
<mat-form-field [floatLabel]="showLabel ? 'auto' : 'always'" [hideRequiredMarker]="!showLabel" [ngClass]="{'no-label': !showLabel}">
<mat-placeholder *ngIf="showLabel">{{ timeText | translate }}</mat-placeholder>
<mat-datetimepicker-toggle [for]="timePicker" matPrefix></mat-datetimepicker-toggle>
<mat-datetimepicker #timePicker type="time" openOnFocus="true"></mat-datetimepicker>
<input [min]="minDateValue" [max]="maxDateValue"

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

@ -24,4 +24,11 @@
width: auto;
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()
timeText: string;
@Input()
showLabel = true;
minDateValue: Date | null;
@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)) {
this.currentMessage = notificationMessage;
const isGtSm = this.breakpointObserver.isMatched(MediaBreakpoints['gt-sm']);
if (isGtSm) {
if (isGtSm && this.toastTarget !== 'root') {
this.showToastPanel(notificationMessage);
} 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 = {
notification: notificationMessage
notification: notificationMessage,
parent: this.elementRef
};
const config: MatSnackBarConfig = {
horizontalPosition: notificationMessage.horizontalPosition || 'left',
verticalPosition: 'bottom',
verticalPosition: !isGtSm ? 'bottom' : (notificationMessage.verticalPosition || 'top'),
viewContainerRef: this.viewContainerRef,
duration: notificationMessage.duration,
panelClass: notificationMessage.panelClass,
@ -248,6 +249,7 @@ export class ToastDirective implements AfterViewInit, OnDestroy {
interface ToastPanelData {
notification: NotificationMessage;
parent?: ElementRef;
}
import {
@ -259,6 +261,7 @@ import {
style,
animate,
} from '@angular/animations';
import { onParentScrollOrWindowResize } from '@core/utils';
export const toastAnimations: {
readonly showHideToast: AnimationTriggerMetadata;
@ -285,6 +288,10 @@ export class TbSnackBarComponent implements AfterViewInit, OnDestroy {
@ViewChild('actionButton', {static: true}) actionButton: MatButton;
private parentEl: HTMLElement;
private snackBarContainerEl: HTMLElement;
private parentScrollSubscription: Subscription = null;
public notification: NotificationMessage;
animationState: ToastAnimationState;
@ -296,6 +303,7 @@ export class TbSnackBarComponent implements AfterViewInit, OnDestroy {
constructor(@Inject(MAT_SNACK_BAR_DATA)
private data: ToastPanelData,
private elementRef: ElementRef,
@Optional()
private snackBarRef: MatSnackBarRef<TbSnackBarComponent>,
@Optional()
@ -305,9 +313,51 @@ export class TbSnackBarComponent implements AfterViewInit, OnDestroy {
}
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() {
if (this.parentScrollSubscription) {
this.parentScrollSubscription.unsubscribe();
}
}
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 { Datasource, DatasourceType } from '@shared/models/widget.models';
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 {
ATTRIBUTE = 'ATTRIBUTE',
@ -63,7 +64,8 @@ export interface EntityKey {
export enum EntityKeyValueType {
STRING = 'STRING',
NUMERIC = 'NUMERIC',
BOOLEAN = 'BOOLEAN'
BOOLEAN = 'BOOLEAN',
DATE_TIME = 'DATE_TIME'
}
export interface EntityKeyValueTypeData {
@ -93,6 +95,13 @@ export const entityKeyValueTypesMap = new Map<EntityKeyValueType, EntityKeyValue
name: 'filter.value-type.boolean',
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:
return FilterPredicateType.STRING;
case EntityKeyValueType.NUMERIC:
case EntityKeyValueType.DATE_TIME:
return FilterPredicateType.NUMERIC;
case EntityKeyValueType.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 {
const predicate = {
type: complex ? FilterPredicateType.COMPLEX : entityKeyValueTypeToFilterPredicateType(valueType)
@ -120,7 +143,7 @@ export function createDefaultFilterPredicate(valueType: EntityKeyValueType, comp
break;
case FilterPredicateType.NUMERIC:
predicate.operation = NumericOperation.EQUAL;
predicate.value = 0;
predicate.value = valueType === EntityKeyValueType.DATE_TIME ? Date.now() : 0;
break;
case FilterPredicateType.BOOLEAN:
predicate.operation = BooleanOperation.EQUAL;
@ -224,16 +247,33 @@ export interface BooleanFilterPredicate {
value: boolean;
}
export interface ComplexFilterPredicate {
export interface BaseComplexFilterPredicate<T extends KeyFilterPredicate | KeyFilterPredicateInfo> {
type: FilterPredicateType.COMPLEX,
operation: ComplexOperation;
predicates: Array<KeyFilterPredicate>;
predicates: Array<T>;
}
export type ComplexFilterPredicate = BaseComplexFilterPredicate<KeyFilterPredicate>;
export type ComplexFilterPredicateInfo = BaseComplexFilterPredicate<KeyFilterPredicateInfo>;
export type KeyFilterPredicate = StringFilterPredicate |
NumericFilterPredicate |
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 {
key: EntityKey;
@ -243,7 +283,7 @@ export interface KeyFilter {
export interface KeyFilterInfo {
key: EntityKey;
valueType: EntityKeyValueType;
predicates: Array<KeyFilterPredicate>;
predicates: Array<KeyFilterPredicateInfo>;
}
export interface FilterInfo {
@ -264,7 +304,7 @@ export function filterInfoToKeyFilters(filter: FilterInfo): Array<KeyFilter> {
for (const predicate of keyFilterInfo.predicates) {
const keyFilter: KeyFilter = {
key,
predicate
predicate: keyFilterPredicateInfoToKeyFilterPredicate(predicate)
};
keyFilters.push(keyFilter);
}
@ -272,6 +312,112 @@ export function filterInfoToKeyFilters(filter: FilterInfo): Array<KeyFilter> {
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 {
id: string;
}

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

@ -1174,18 +1174,18 @@
"filter-required": "Filter is required.",
"operation": {
"operation": "Operation",
"equal": "Equal",
"not-equal": "Not equal",
"starts-with": "Starts with",
"ends-with": "Ends with",
"contains": "Contains",
"not-contain": "Not contain",
"greater": "Greater than",
"less": "Less than",
"greater-or-equal": "Greater or equal",
"less-or-equal": "Less or equal",
"and": "And",
"or": "Or"
"equal": "equal",
"not-equal": "not equal",
"starts-with": "starts with",
"ends-with": "ends with",
"contains": "contains",
"not-contain": "not contain",
"greater": "greater than",
"less": "less than",
"greater-or-equal": "greater or equal",
"less-or-equal": "less or equal",
"and": "and",
"or": "or"
},
"ignore-case": "Ignore case",
"value": "Value",
@ -1196,6 +1196,11 @@
"add-complex": "Add complex",
"complex-filter": "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-filters": "Key filters",
"key-name": "Key name",
@ -1210,7 +1215,8 @@
"value-type": "Value type",
"string": "String",
"numeric": "Numeric",
"boolean": "Boolean"
"boolean": "Boolean",
"date-time": "Datetime"
},
"value-type-required": "Key value type is required.",
"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",
"add-key-filter": "Add 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": {
"expand": "Expand to fullscreen",

Loading…
Cancel
Save