UI: Dashboard filters - user mode

This commit is contained in:
Igor Kulikov 2020-07-02 15:42:58 +03:00
parent 25dae17671
commit c6f7862cbf
38 changed files with 905 additions and 168 deletions

View File

@ -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:

View File

@ -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>

View File

@ -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();
});

View File

@ -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>

View File

@ -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);
}

View File

@ -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) => {

View File

@ -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"

View File

@ -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 {

View File

@ -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 }}"

View File

@ -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);

View File

@ -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>

View File

@ -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);
}

View File

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

View File

@ -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>

View File

@ -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);
}
}
}

View File

@ -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>

View File

@ -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);
}
}

View File

@ -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) {

View File

@ -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) => {

View File

@ -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;
}

View File

@ -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>

View File

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

View File

@ -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 }}"

View File

@ -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();

View File

@ -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>

View File

@ -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();
});

View File

@ -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>

View File

@ -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();
});

View File

@ -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>

View File

@ -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);
}
}

View File

@ -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,

View File

@ -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) => {

View File

@ -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"

View File

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

View File

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

View File

@ -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 {

View File

@ -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;
}

View File

@ -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",