Commit 3a830eeb9ac9747618200ce249f6669063fb7a35

Authored by Igor Kulikov
1 parent 969a686c

UI: Device profile alarm rules

  1 +<!--
  2 +
  3 + Copyright © 2016-2020 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<div class="tb-filter-text" [ngClass]="{disabled: disabled, required: requiredClass}"
  19 + [innerHTML]="filterText"></div>
... ...
  1 +/**
  2 + * Copyright © 2016-2020 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +:host {
  17 + .tb-filter-text {
  18 + overflow-y: auto;
  19 + &.disabled {
  20 + opacity: 0.7;
  21 + }
  22 + &.required {
  23 + color: #f44336;
  24 + }
  25 + }
  26 +}
  27 +
  28 +:host ::ng-deep {
  29 + .tb-filter-text {
  30 + line-height: 1.8em;
  31 + span {
  32 + display: inline-block;
  33 + vertical-align: middle;
  34 + line-height: 1.4em;
  35 + }
  36 + .tb-filter-predicate {
  37 + padding-right: 4px;
  38 + padding-left: 4px;
  39 + }
  40 + .tb-filter-entity-key, .tb-filter-value, .tb-filter-dynamic-source {
  41 + font-weight: bold;
  42 + border: 1px groove rgba(0, 0, 0, .25);
  43 + border-radius: 4px;
  44 + padding-left: 4px;
  45 + padding-right: 4px;
  46 + }
  47 + .tb-filter-entity-key, .tb-filter-value {
  48 + white-space: nowrap;
  49 + overflow: hidden;
  50 + text-overflow: ellipsis;
  51 + max-width: 150px;
  52 + }
  53 + .tb-filter-dynamic-source {
  54 + }
  55 + .tb-filter-entity-key {
  56 + color: #305680;
  57 + }
  58 + .tb-filter-value {
  59 + color: #ff5722;
  60 + }
  61 + .tb-filter-simple-operation {
  62 + font-size: 0.9em;
  63 + }
  64 + .tb-filter-complex-operation {
  65 + text-transform: uppercase;
  66 + font-weight: bold;
  67 + }
  68 + .tb-filter-dynamic-value {
  69 + .tb-filter-dynamic-source, .tb-filter-value {
  70 + color: #0c959c;
  71 + }
  72 + }
  73 + .tb-filter-bracket {
  74 + .tb-left-bracket, .tb-right-bracket {
  75 + font-size: 1.2em;
  76 + }
  77 + }
  78 + }
  79 +}
... ...
  1 +///
  2 +/// Copyright © 2016-2020 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { Component, forwardRef, Input, OnInit } from '@angular/core';
  18 +import { ControlValueAccessor, FormBuilder, NG_VALUE_ACCESSOR } from '@angular/forms';
  19 +import { MatDialog } from '@angular/material/dialog';
  20 +import { KeyFilter, keyFiltersToText } from '@shared/models/query/query.models';
  21 +import { TranslateService } from '@ngx-translate/core';
  22 +import { DatePipe } from '@angular/common';
  23 +import { coerceBooleanProperty } from '@angular/cdk/coercion';
  24 +
  25 +@Component({
  26 + selector: 'tb-filter-text',
  27 + templateUrl: './filter-text.component.html',
  28 + styleUrls: ['./filter-text.component.scss'],
  29 + providers: [
  30 + {
  31 + provide: NG_VALUE_ACCESSOR,
  32 + useExisting: forwardRef(() => FilterTextComponent),
  33 + multi: true
  34 + }
  35 + ]
  36 +})
  37 +export class FilterTextComponent implements ControlValueAccessor, OnInit {
  38 +
  39 + private requiredValue: boolean;
  40 + get required(): boolean {
  41 + return this.requiredValue;
  42 + }
  43 + @Input()
  44 + set required(value: boolean) {
  45 + this.requiredValue = coerceBooleanProperty(value);
  46 + }
  47 +
  48 + @Input()
  49 + disabled: boolean;
  50 +
  51 + @Input()
  52 + noFilterText = this.translate.instant('filter.no-filter-text');
  53 +
  54 + @Input()
  55 + addFilterPrompt = this.translate.instant('filter.add-filter-prompt');
  56 +
  57 + requiredClass = false;
  58 +
  59 + private filterText: string;
  60 +
  61 + private propagateChange = (v: any) => { };
  62 +
  63 + constructor(private dialog: MatDialog,
  64 + private fb: FormBuilder,
  65 + private translate: TranslateService,
  66 + private datePipe: DatePipe) {
  67 + }
  68 +
  69 + registerOnChange(fn: any): void {
  70 + this.propagateChange = fn;
  71 + }
  72 +
  73 + registerOnTouched(fn: any): void {
  74 + }
  75 +
  76 + ngOnInit() {
  77 + }
  78 +
  79 + setDisabledState(isDisabled: boolean): void {
  80 + this.disabled = isDisabled;
  81 + }
  82 +
  83 + writeValue(value: Array<KeyFilter>): void {
  84 + this.updateFilterText(value);
  85 + }
  86 +
  87 + private updateFilterText(value: Array<KeyFilter>) {
  88 + this.requiredClass = false;
  89 + if (value && value.length) {
  90 + this.filterText = keyFiltersToText(this.translate, this.datePipe, value);
  91 + } else {
  92 + if (this.required && !this.disabled) {
  93 + this.filterText = this.addFilterPrompt;
  94 + this.requiredClass = true;
  95 + } else {
  96 + this.filterText = this.noFilterText;
  97 + }
  98 + }
  99 + }
  100 +
  101 +}
... ...
... ... @@ -104,6 +104,7 @@ import { CreateAlarmRulesComponent } from './profile/alarm/create-alarm-rules.co
104 104 import { AlarmRuleComponent } from './profile/alarm/alarm-rule.component';
105 105 import { AlarmRuleConditionComponent } from './profile/alarm/alarm-rule-condition.component';
106 106 import { AlarmRuleKeyFiltersDialogComponent } from './profile/alarm/alarm-rule-key-filters-dialog.component';
  107 +import { FilterTextComponent } from './filter/filter-text.component';
107 108
108 109 @NgModule({
109 110 declarations:
... ... @@ -165,6 +166,7 @@ import { AlarmRuleKeyFiltersDialogComponent } from './profile/alarm/alarm-rule-k
165 166 FilterDialogComponent,
166 167 FiltersDialogComponent,
167 168 FilterSelectComponent,
  169 + FilterTextComponent,
168 170 FiltersEditComponent,
169 171 FiltersEditPanelComponent,
170 172 UserFilterDialogComponent,
... ... @@ -245,6 +247,7 @@ import { AlarmRuleKeyFiltersDialogComponent } from './profile/alarm/alarm-rule-k
245 247 FilterDialogComponent,
246 248 FiltersDialogComponent,
247 249 FilterSelectComponent,
  250 + FilterTextComponent,
248 251 FiltersEditComponent,
249 252 UserFilterDialogComponent,
250 253 TenantProfileAutocompleteComponent,
... ...
... ... @@ -15,16 +15,22 @@
15 15 limitations under the License.
16 16
17 17 -->
18   -<mat-form-field class="mat-block" (click)="openFilterDialog($event)" floatLabel="always" hideRequiredMarker>
19   - <mat-label></mat-label>
20   - <input readonly
21   - required matInput [formControl]="alarmRuleConditionControl"
22   - placeholder="{{'device-profile.enter-alarm-rule-condition-prompt' | translate}}">
23   - <a matSuffix mat-icon-button color="primary"
24   - type="button"
25   - (click)="openFilterDialog($event)"
26   - matTooltip="{{ (disabled ? 'action.view' : 'action.edit') | translate }}"
27   - matTooltipPosition="above">
28   - <mat-icon>{{ disabled ? 'more_vert' : 'edit' }}</mat-icon>
29   - </a>
30   -</mat-form-field>
  18 +<div fxLayout="column" fxFlex>
  19 + <div fxLayout="row" fxLayoutAlign="start center" style="min-height: 40px;">
  20 + <div class="tb-small" translate>device-profile.alarm-rule-condition</div>
  21 + <span fxFlex></span>
  22 + <a mat-button color="primary"
  23 + type="button"
  24 + (click)="openFilterDialog($event)"
  25 + matTooltip="{{ (disabled ? 'action.view' : 'action.edit') | translate }}"
  26 + matTooltipPosition="above">
  27 + {{ (disabled ? 'action.view' : (conditionSet() ? 'action.edit' : 'action.add')) | translate }}
  28 + </a>
  29 + </div>
  30 + <div class="tb-alarm-rule-condition" fxFlex fxLayout="column" fxLayoutAlign="center" (click)="openFilterDialog($event)">
  31 + <tb-filter-text [formControl]="alarmRuleConditionControl"
  32 + required
  33 + addFilterPrompt="{{'device-profile.enter-alarm-rule-condition-prompt' | translate}}">
  34 + </tb-filter-text>
  35 + </div>
  36 +</div>
... ...
... ... @@ -14,9 +14,24 @@
14 14 * limitations under the License.
15 15 */
16 16 :host {
17   - a.mat-icon-button {
  17 + display: flex;
  18 + a.mat-button {
18 19 &:hover, &:focus {
19 20 border-bottom: none;
20 21 }
21 22 }
  23 + .tb-alarm-rule-condition {
  24 + padding: 8px;
  25 + border: 1px groove rgba(0, 0, 0, .25);
  26 + border-radius: 4px;
  27 + cursor: pointer;
  28 + }
  29 +}
  30 +
  31 +:host ::ng-deep {
  32 + .tb-alarm-rule-condition {
  33 + .tb-filter-text {
  34 + max-height: 200px;
  35 + }
  36 + }
22 37 }
... ...
... ... @@ -30,6 +30,8 @@ import {
30 30 AlarmRuleKeyFiltersDialogComponent,
31 31 AlarmRuleKeyFiltersDialogData
32 32 } from './alarm-rule-key-filters-dialog.component';
  33 +import { TranslateService } from '@ngx-translate/core';
  34 +import { DatePipe } from '@angular/common';
33 35
34 36 @Component({
35 37 selector: 'tb-alarm-rule-condition',
... ... @@ -60,7 +62,9 @@ export class AlarmRuleConditionComponent implements ControlValueAccessor, OnInit
60 62 private propagateChange = (v: any) => { };
61 63
62 64 constructor(private dialog: MatDialog,
63   - private fb: FormBuilder) {
  65 + private fb: FormBuilder,
  66 + private translate: TranslateService,
  67 + private datePipe: DatePipe) {
64 68 }
65 69
66 70 registerOnChange(fn: any): void {
... ... @@ -76,6 +80,11 @@ export class AlarmRuleConditionComponent implements ControlValueAccessor, OnInit
76 80
77 81 setDisabledState(isDisabled: boolean): void {
78 82 this.disabled = isDisabled;
  83 + if (this.disabled) {
  84 + this.alarmRuleConditionControl.disable({emitEvent: false});
  85 + } else {
  86 + this.alarmRuleConditionControl.enable({emitEvent: false});
  87 + }
79 88 }
80 89
81 90 writeValue(value: Array<KeyFilter>): void {
... ... @@ -83,8 +92,12 @@ export class AlarmRuleConditionComponent implements ControlValueAccessor, OnInit
83 92 this.updateConditionInfo();
84 93 }
85 94
  95 + public conditionSet() {
  96 + return this.modelValue && this.modelValue.length;
  97 + }
  98 +
86 99 public validate(c: FormControl) {
87   - return (this.modelValue && this.modelValue.length) ? null : {
  100 + return this.conditionSet() ? null : {
88 101 alarmRuleCondition: {
89 102 valid: false,
90 103 },
... ... @@ -112,11 +125,7 @@ export class AlarmRuleConditionComponent implements ControlValueAccessor, OnInit
112 125 }
113 126
114 127 private updateConditionInfo() {
115   - if (this.modelValue && this.modelValue.length) {
116   - this.alarmRuleConditionControl.patchValue('Condition set');
117   - } else {
118   - this.alarmRuleConditionControl.patchValue(null);
119   - }
  128 + this.alarmRuleConditionControl.patchValue(this.modelValue);
120 129 }
121 130
122 131 private updateModel() {
... ...
... ... @@ -16,56 +16,56 @@
16 16
17 17 -->
18 18 <div fxLayout="column" [formGroup]="alarmRuleFormGroup">
19   - <div formGroupName="condition" fxLayout="row" fxLayoutAlign="start" fxLayoutGap="8px" fxFlex>
20   - <div fxLayout="column" fxFlex>
  19 + <div formGroupName="condition" fxLayout="row" fxLayoutGap="8px" fxFlex>
  20 + <tb-alarm-rule-condition fxFlex
  21 + formControlName="condition">
  22 + </tb-alarm-rule-condition>
  23 + <div fxLayout="column">
21 24 <div fxLayout="row" fxLayoutAlign="start center" style="min-height: 40px;">
22   - <label class="tb-small" translate>device-profile.alarm-rule-condition</label>
23   - </div>
24   - <tb-alarm-rule-condition fxFlex
25   - formControlName="condition">
26   - </tb-alarm-rule-condition>
27   - </div>
28   - <div fxLayout="column" style="min-width: 250px;">
29   - <div fxLayout="row" fxLayoutAlign="start center" style="min-height: 40px;">
30   - <label fxFlex class="tb-small" translate>device-profile.condition-duration</label>
  25 + <div class="tb-small" translate>device-profile.condition-duration</div>
  26 + <span fxFlex></span>
31 27 <mat-slide-toggle [disabled]="disabled"
  28 + color="primary"
32 29 [ngModelOptions]="{standalone: true}"
33 30 (ngModelChange)="enableDurationChanged($event)"
34 31 [ngModel]="enableDuration">
35 32 </mat-slide-toggle>
36 33 </div>
37   - <div fxLayout="row" fxLayoutGap="8px" [fxShow]="enableDuration">
38   - <mat-form-field class="mat-block duration-value-field" hideRequiredMarker floatLabel="always">
39   - <mat-label></mat-label>
40   - <input type="number"
41   - [required]="enableDuration"
42   - step="1"
43   - min="1" max="2147483647" matInput
44   - placeholder="{{ 'device-profile.condition-duration-value' | translate }}"
45   - formControlName="durationValue">
46   - <mat-error *ngIf="alarmRuleFormGroup.get('condition').get('durationValue').hasError('required')">
47   - {{ 'device-profile.condition-duration-value-required' | translate }}
48   - </mat-error>
49   - <mat-error *ngIf="alarmRuleFormGroup.get('condition').get('durationValue').hasError('min')">
50   - {{ 'device-profile.condition-duration-value-range' | translate }}
51   - </mat-error>
52   - <mat-error *ngIf="alarmRuleFormGroup.get('condition').get('durationValue').hasError('max')">
53   - {{ 'device-profile.condition-duration-value-range' | translate }}
54   - </mat-error>
55   - </mat-form-field>
56   - <mat-form-field class="mat-block duration-unit-field" hideRequiredMarker floatLabel="always">
57   - <mat-label></mat-label>
58   - <mat-select formControlName="durationUnit"
59   - [required]="enableDuration"
60   - placeholder="{{ 'device-profile.condition-duration-time-unit' | translate }}">
61   - <mat-option *ngFor="let timeUnit of timeUnits" [value]="timeUnit">
62   - {{ timeUnitTranslations.get(timeUnit) | translate }}
63   - </mat-option>
64   - </mat-select>
65   - <mat-error *ngIf="alarmRuleFormGroup.get('condition').get('durationUnit').hasError('required')">
66   - {{ 'device-profile.condition-duration-time-unit-required' | translate }}
67   - </mat-error>
68   - </mat-form-field>
  34 + <div class="tb-condition-duration" fxFlex fxLayout="row" fxLayoutGap="8px">
  35 + <span style="min-width: 250px;" [fxShow]="!enableDuration"></span>
  36 + <div style="min-width: 250px;" fxLayout="row" fxLayoutGap="8px" [fxShow]="enableDuration">
  37 + <mat-form-field class="mat-block duration-value-field" hideRequiredMarker floatLabel="always">
  38 + <mat-label></mat-label>
  39 + <input type="number"
  40 + [required]="enableDuration"
  41 + step="1"
  42 + min="1" max="2147483647" matInput
  43 + placeholder="{{ 'device-profile.condition-duration-value' | translate }}"
  44 + formControlName="durationValue">
  45 + <mat-error *ngIf="alarmRuleFormGroup.get('condition').get('durationValue').hasError('required')">
  46 + {{ 'device-profile.condition-duration-value-required' | translate }}
  47 + </mat-error>
  48 + <mat-error *ngIf="alarmRuleFormGroup.get('condition').get('durationValue').hasError('min')">
  49 + {{ 'device-profile.condition-duration-value-range' | translate }}
  50 + </mat-error>
  51 + <mat-error *ngIf="alarmRuleFormGroup.get('condition').get('durationValue').hasError('max')">
  52 + {{ 'device-profile.condition-duration-value-range' | translate }}
  53 + </mat-error>
  54 + </mat-form-field>
  55 + <mat-form-field class="mat-block duration-unit-field" hideRequiredMarker floatLabel="always">
  56 + <mat-label></mat-label>
  57 + <mat-select formControlName="durationUnit"
  58 + [required]="enableDuration"
  59 + placeholder="{{ 'device-profile.condition-duration-time-unit' | translate }}">
  60 + <mat-option *ngFor="let timeUnit of timeUnits" [value]="timeUnit">
  61 + {{ timeUnitTranslations.get(timeUnit) | translate }}
  62 + </mat-option>
  63 + </mat-select>
  64 + <mat-error *ngIf="alarmRuleFormGroup.get('condition').get('durationUnit').hasError('required')">
  65 + {{ 'device-profile.condition-duration-time-unit-required' | translate }}
  66 + </mat-error>
  67 + </mat-form-field>
  68 + </div>
69 69 </div>
70 70 </div>
71 71 </div>
... ... @@ -73,7 +73,7 @@
73 73 <mat-expansion-panel-header>
74 74 <mat-panel-title>
75 75 <div fxFlex fxLayout="row" fxLayoutAlign="end center">
76   - <div class="tb-small" translate>device-profile.advanced-settings</div>
  76 + <div class="tb-small" translate>device-profile.alarm-rule-details</div>
77 77 </div>
78 78 </mat-panel-title>
79 79 </mat-expansion-panel-header>
... ...
... ... @@ -14,6 +14,11 @@
14 14 * limitations under the License.
15 15 */
16 16 :host {
  17 + .tb-condition-duration {
  18 + padding: 8px;
  19 + border: 1px groove rgba(0, 0, 0, .25);
  20 + border-radius: 4px;
  21 + }
17 22 .mat-expansion-panel.advanced-settings {
18 23 box-shadow: none;
19 24 border: none;
... ...
... ... @@ -36,7 +36,7 @@
36 36 <tb-alarm-rule formControlName="alarmRule" required fxFlex>
37 37 </tb-alarm-rule>
38 38 </div>
39   - <button *ngIf="!disabled && createAlarmRulesFormArray().controls.length > 1"
  39 + <button *ngIf="!disabled"
40 40 mat-icon-button color="primary" style="min-width: 40px;"
41 41 type="button"
42 42 (click)="removeCreateAlarmRule($index)"
... ... @@ -45,6 +45,10 @@
45 45 <mat-icon>remove_circle_outline</mat-icon>
46 46 </button>
47 47 </div>
  48 + <div *ngIf="disabled && !createAlarmRulesFormArray().controls.length">
  49 + <span translate fxLayoutAlign="center center"
  50 + class="tb-prompt">device-profile.no-create-alarm-rules</span>
  51 + </div>
48 52 <div fxLayout="row" *ngIf="!disabled">
49 53 <button mat-stroked-button color="primary"
50 54 type="button"
... ...
... ... @@ -142,7 +142,7 @@ export class CreateAlarmRulesComponent implements ControlValueAccessor, OnInit,
142 142 }
143 143
144 144 public validate(c: FormControl) {
145   - return (this.createAlarmRulesFormGroup.valid && this.createAlarmRulesFormGroup.get('createAlarmRules').value.length) ? null : {
  145 + return (this.createAlarmRulesFormGroup.valid) ? null : {
146 146 createAlarmRules: {
147 147 valid: false,
148 148 },
... ...
... ... @@ -66,6 +66,10 @@
66 66 <mat-icon>remove_circle_outline</mat-icon>
67 67 </button>
68 68 </div>
  69 + <div *ngIf="disabled && !alarmFormGroup.get('clearRule').value">
  70 + <span translate fxLayoutAlign="center center"
  71 + class="tb-prompt">device-profile.no-clear-alarm-rule</span>
  72 + </div>
69 73 <div fxLayout="row" *ngIf="!disabled"
70 74 [fxShow]="!alarmFormGroup.get('clearRule').value">
71 75 <button mat-stroked-button color="primary"
... ...
... ... @@ -32,8 +32,7 @@
32 32 (click)="addAlarm()"
33 33 matTooltip="{{ 'device-profile.add-alarm-rule' | translate }}"
34 34 matTooltipPosition="above">
35   - <mat-icon>add</mat-icon>
36   - <span translate>action.add</span>
  35 + <span translate>device-profile.add-alarm-rule</span>
37 36 </button>
38 37 </div>
39 38 </div>
... ...
... ... @@ -39,7 +39,7 @@
39 39 required>
40 40 </tb-device-profile-transport-configuration>
41 41 </mat-expansion-panel>
42   - <mat-expansion-panel [expanded]="false">
  42 + <mat-expansion-panel [expanded]="true">
43 43 <mat-expansion-panel-header>
44 44 <mat-panel-title>
45 45 <div>{{'device-profile.alarm-rules' | translate:
... ...
... ... @@ -25,6 +25,8 @@ import { PageData } from '@shared/models/page/page-data';
25 25 import { isDefined, isEqual } from '@core/utils';
26 26 import { TranslateService } from '@ngx-translate/core';
27 27 import { AlarmInfo, AlarmSearchStatus, AlarmSeverity } from '../alarm.models';
  28 +import { Filter } from '@material-ui/icons';
  29 +import { DatePipe } from '@angular/common';
28 30
29 31 export enum EntityKeyType {
30 32 ATTRIBUTE = 'ATTRIBUTE',
... ... @@ -358,6 +360,98 @@ export interface FiltersInfo {
358 360 datasourceFilters: {[datasourceIndex: number]: FilterInfo};
359 361 }
360 362
  363 +export function keyFiltersToText(translate: TranslateService, datePipe: DatePipe, keyFilters: Array<KeyFilter>): string {
  364 + const filtersText = keyFilters.map(keyFilter =>
  365 + keyFilterToText(translate, datePipe, keyFilter,
  366 + keyFilters.length > 1 ? ComplexOperation.AND : undefined));
  367 + let result: string;
  368 + if (filtersText.length > 1) {
  369 + const andText = translate.instant('filter.operation.and');
  370 + result = filtersText.join(' <span class="tb-filter-complex-operation">' + andText + '</span> ');
  371 + } else {
  372 + result = filtersText[0];
  373 + }
  374 + return result;
  375 +}
  376 +
  377 +export function keyFilterToText(translate: TranslateService, datePipe: DatePipe, keyFilter: KeyFilter,
  378 + parentComplexOperation?: ComplexOperation): string {
  379 + const keyFilterPredicate = keyFilter.predicate;
  380 + return keyFilterPredicateToText(translate, datePipe, keyFilter, keyFilterPredicate, parentComplexOperation);
  381 +}
  382 +
  383 +export function keyFilterPredicateToText(translate: TranslateService,
  384 + datePipe: DatePipe,
  385 + keyFilter: KeyFilter,
  386 + keyFilterPredicate: KeyFilterPredicate,
  387 + parentComplexOperation?: ComplexOperation): string {
  388 + if (keyFilterPredicate.type === FilterPredicateType.COMPLEX) {
  389 + const complexPredicate = keyFilterPredicate as ComplexFilterPredicate;
  390 + const complexOperation = complexPredicate.operation;
  391 + const complexPredicatesText =
  392 + complexPredicate.predicates.map(predicate => keyFilterPredicateToText(translate, datePipe, keyFilter, predicate, complexOperation));
  393 + if (complexPredicatesText.length > 1) {
  394 + const operationText = translate.instant(complexOperationTranslationMap.get(complexOperation));
  395 + let result = complexPredicatesText.join(' <span class="tb-filter-complex-operation">' + operationText + '</span> ');
  396 + if (complexOperation === ComplexOperation.OR && parentComplexOperation && ComplexOperation.OR !== parentComplexOperation) {
  397 + result = `<span class="tb-filter-bracket"><span class="tb-left-bracket">(</span>${result}<span class="tb-right-bracket">)</span></span>`;
  398 + }
  399 + return result;
  400 + } else {
  401 + return complexPredicatesText[0];
  402 + }
  403 + } else {
  404 + return simpleKeyFilterPredicateToText(translate, datePipe, keyFilter, keyFilterPredicate);
  405 + }
  406 +}
  407 +
  408 +function simpleKeyFilterPredicateToText(translate: TranslateService,
  409 + datePipe: DatePipe,
  410 + keyFilter: KeyFilter,
  411 + keyFilterPredicate: StringFilterPredicate |
  412 + NumericFilterPredicate |
  413 + BooleanFilterPredicate): string {
  414 + const key = keyFilter.key.key;
  415 + let operation: string;
  416 + let value: string;
  417 + const val = keyFilterPredicate.value;
  418 + const dynamicValue = !!val.dynamicValue && !!val.dynamicValue.sourceType;
  419 + if (dynamicValue) {
  420 + value = '<span class="tb-filter-dynamic-value"><span class="tb-filter-dynamic-source">' +
  421 + translate.instant(dynamicValueSourceTypeTranslationMap.get(val.dynamicValue.sourceType)) + '</span>';
  422 + value += '.<span class="tb-filter-value">' + val.dynamicValue.sourceAttribute + '</span></span>';
  423 + }
  424 + switch (keyFilterPredicate.type) {
  425 + case FilterPredicateType.STRING:
  426 + operation = translate.instant(stringOperationTranslationMap.get(keyFilterPredicate.operation));
  427 + if (keyFilterPredicate.ignoreCase) {
  428 + operation += ' ' + translate.instant('filter.ignore-case');
  429 + }
  430 + if (!dynamicValue) {
  431 + value = `'${keyFilterPredicate.value.defaultValue}'`;
  432 + }
  433 + break;
  434 + case FilterPredicateType.NUMERIC:
  435 + operation = translate.instant(numericOperationTranslationMap.get(keyFilterPredicate.operation));
  436 + if (!dynamicValue) {
  437 + if (keyFilter.valueType === EntityKeyValueType.DATE_TIME) {
  438 + value = datePipe.transform(keyFilterPredicate.value.defaultValue, 'yyyy-MM-dd HH:mm');
  439 + } else {
  440 + value = keyFilterPredicate.value.defaultValue + '';
  441 + }
  442 + }
  443 + break;
  444 + case FilterPredicateType.BOOLEAN:
  445 + operation = translate.instant(booleanOperationTranslationMap.get(keyFilterPredicate.operation));
  446 + value = translate.instant(keyFilterPredicate.value.defaultValue ? 'value.true' : 'value.false');
  447 + break;
  448 + }
  449 + if (!dynamicValue) {
  450 + value = `<span class="tb-filter-value">${value}</span>`;
  451 + }
  452 + return `<span class="tb-filter-predicate"><span class="tb-filter-entity-key">${key}</span> <span class="tb-filter-simple-operation">${operation}</span> ${value}</span>`;
  453 +}
  454 +
361 455 export function keyFilterInfosToKeyFilters(keyFilterInfos: Array<KeyFilterInfo>): Array<KeyFilter> {
362 456 const keyFilters: Array<KeyFilter> = [];
363 457 for (const keyFilterInfo of keyFilterInfos) {
... ...
... ... @@ -813,15 +813,16 @@
813 813 "alarm-rules": "Alarm rules ({{count}})",
814 814 "add-alarm-rule": "Add alarm rule",
815 815 "edit-alarm-rule": "Edit alarm rule",
816   - "alarm-rule-details": "Alarm rule details",
817 816 "alarm-type": "Alarm type",
818 817 "alarm-type-required": "Alarm type is required.",
819 818 "alarm-type-pattern-hint": "Alarm type pattern, use <code>${metaKeyName}</code> to substitute variables from metadata",
820 819 "create-alarm-pattern": "Create <b>{{alarmType}}</b> alarm",
821 820 "create-alarm-rules": "Create alarm rules",
  821 + "no-create-alarm-rules": "No create conditions configured",
822 822 "clear-alarm-rule": "Clear alarm rule",
823   - "add-create-alarm-rule": "Add create alarm rule",
824   - "add-clear-alarm-rule": "Add clear alarm rule",
  823 + "no-clear-alarm-rule": "No clear condition configured",
  824 + "add-create-alarm-rule": "Add create condition",
  825 + "add-clear-alarm-rule": "Add clear condition",
825 826 "select-alarm-severity": "Select alarm severity",
826 827 "alarm-severity-required": "Alarm severity is required.",
827 828 "condition-duration": "Condition duration",
... ... @@ -831,6 +832,7 @@
831 832 "condition-duration-value-required": "Duration value is required.",
832 833 "condition-duration-time-unit-required": "Time unit is required.",
833 834 "advanced-settings": "Advanced settings",
  835 + "alarm-rule-details": "Details",
834 836 "propagate-alarm": "Propagate alarm",
835 837 "alarm-details": "Alarm details",
836 838 "alarm-rule-condition": "Alarm rule condition",
... ... @@ -1277,6 +1279,8 @@
1277 1279 "filter": "Filter",
1278 1280 "editable": "Editable",
1279 1281 "no-filters-found": "No filters found.",
  1282 + "no-filter-text": "No filter specified",
  1283 + "add-filter-prompt": "Please add filter",
1280 1284 "no-filter-matching": "'{{filter}}' not found.",
1281 1285 "create-new-filter": "Create a new one!",
1282 1286 "filter-required": "Filter is required.",
... ... @@ -1295,7 +1299,7 @@
1295 1299 "and": "and",
1296 1300 "or": "or"
1297 1301 },
1298   - "ignore-case": "Ignore case",
  1302 + "ignore-case": "ignore case",
1299 1303 "value": "Value",
1300 1304 "remove-filter": "Remove filter",
1301 1305 "no-filters": "No filters configured",
... ...