Commit 155b967b478f0d3462d06ba95a745255de2bf4be

Authored by Igor Kulikov
1 parent 628ce799

Improve device profile alarm rules UI

Showing 27 changed files with 992 additions and 458 deletions
... ... @@ -102,13 +102,16 @@ import { DeviceProfileAlarmComponent } from './profile/alarm/device-profile-alar
102 102 import { CreateAlarmRulesComponent } from './profile/alarm/create-alarm-rules.component';
103 103 import { AlarmRuleComponent } from './profile/alarm/alarm-rule.component';
104 104 import { AlarmRuleConditionComponent } from './profile/alarm/alarm-rule-condition.component';
105   -import { AlarmRuleKeyFiltersDialogComponent } from './profile/alarm/alarm-rule-key-filters-dialog.component';
106 105 import { FilterTextComponent } from './filter/filter-text.component';
107 106 import { AddDeviceProfileDialogComponent } from './profile/add-device-profile-dialog.component';
108 107 import { RuleChainAutocompleteComponent } from './rule-chain/rule-chain-autocomplete.component';
109 108 import { AlarmScheduleComponent } from './profile/alarm/alarm-schedule.component';
110 109 import { DeviceWizardDialogComponent } from './wizard/device-wizard-dialog.component';
111 110 import { DeviceCredentialsComponent } from './device/device-credentials.component';
  111 +import { AlarmScheduleInfoComponent } from './profile/alarm/alarm-schedule-info.component';
  112 +import { AlarmScheduleDialogComponent } from '@home/components/profile/alarm/alarm-schedule-dialog.component';
  113 +import { EditAlarmDetailsDialogComponent } from './profile/alarm/edit-alarm-details-dialog.component';
  114 +import { AlarmRuleConditionDialogComponent } from '@home/components/profile/alarm/alarm-rule-condition-dialog.component';
112 115
113 116 @NgModule({
114 117 declarations:
... ... @@ -190,7 +193,7 @@ import { DeviceCredentialsComponent } from './device/device-credentials.componen
190 193 DeviceProfileTransportConfigurationComponent,
191 194 CreateAlarmRulesComponent,
192 195 AlarmRuleComponent,
193   - AlarmRuleKeyFiltersDialogComponent,
  196 + AlarmRuleConditionDialogComponent,
194 197 AlarmRuleConditionComponent,
195 198 DeviceProfileAlarmComponent,
196 199 DeviceProfileAlarmsComponent,
... ... @@ -198,9 +201,12 @@ import { DeviceCredentialsComponent } from './device/device-credentials.componen
198 201 DeviceProfileDialogComponent,
199 202 AddDeviceProfileDialogComponent,
200 203 RuleChainAutocompleteComponent,
  204 + AlarmScheduleInfoComponent,
201 205 AlarmScheduleComponent,
202 206 DeviceWizardDialogComponent,
203   - DeviceCredentialsComponent
  207 + DeviceCredentialsComponent,
  208 + AlarmScheduleDialogComponent,
  209 + EditAlarmDetailsDialogComponent
204 210 ],
205 211 imports: [
206 212 CommonModule,
... ... @@ -271,7 +277,7 @@ import { DeviceCredentialsComponent } from './device/device-credentials.componen
271 277 DeviceProfileTransportConfigurationComponent,
272 278 CreateAlarmRulesComponent,
273 279 AlarmRuleComponent,
274   - AlarmRuleKeyFiltersDialogComponent,
  280 + AlarmRuleConditionDialogComponent,
275 281 AlarmRuleConditionComponent,
276 282 DeviceProfileAlarmComponent,
277 283 DeviceProfileAlarmsComponent,
... ... @@ -281,7 +287,10 @@ import { DeviceCredentialsComponent } from './device/device-credentials.componen
281 287 RuleChainAutocompleteComponent,
282 288 DeviceWizardDialogComponent,
283 289 DeviceCredentialsComponent,
284   - AlarmScheduleComponent
  290 + AlarmScheduleInfoComponent,
  291 + AlarmScheduleComponent,
  292 + AlarmScheduleDialogComponent,
  293 + EditAlarmDetailsDialogComponent
285 294 ],
286 295 providers: [
287 296 WidgetComponentService,
... ...
  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 +<form [formGroup]="conditionFormGroup" (ngSubmit)="save()" style="width: 700px;">
  19 + <mat-toolbar color="primary">
  20 + <h2>{{ (readonly ? 'device-profile.alarm-rule-condition' : 'device-profile.edit-alarm-rule-condition') | translate }}</h2>
  21 + <span fxFlex></span>
  22 + <button mat-icon-button
  23 + (click)="cancel()"
  24 + type="button">
  25 + <mat-icon class="material-icons">close</mat-icon>
  26 + </button>
  27 + </mat-toolbar>
  28 + <mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
  29 + </mat-progress-bar>
  30 + <div mat-dialog-content>
  31 + <fieldset [disabled]="isLoading$ | async">
  32 + <div fxFlex fxLayout="column">
  33 + <tb-key-filter-list
  34 + [displayUserParameters]="false"
  35 + [allowUserDynamicSource]="false"
  36 + [telemetryKeysOnly]="true"
  37 + formControlName="keyFilters">
  38 + </tb-key-filter-list>
  39 + <section formGroupName="spec" class="row">
  40 + <mat-form-field class="mat-block" hideRequiredMarker>
  41 + <mat-label translate>device-profile.condition-type</mat-label>
  42 + <mat-select formControlName="type" required>
  43 + <mat-option *ngFor="let alarmConditionType of alarmConditionTypes" [value]="alarmConditionType">
  44 + {{ alarmConditionTypeTranslation.get(alarmConditionType) | translate }}
  45 + </mat-option>
  46 + </mat-select>
  47 + <mat-error *ngIf="conditionFormGroup.get('spec.type').hasError('required')">
  48 + {{ 'device-profile.condition-type-required' | translate }}
  49 + </mat-error>
  50 + </mat-form-field>
  51 + <div fxLayout="row" fxLayoutGap="8px" *ngIf="conditionFormGroup.get('spec.type').value == AlarmConditionType.DURATION">
  52 + <mat-form-field class="mat-block" hideRequiredMarker fxFlex floatLabel="always">
  53 + <mat-label></mat-label>
  54 + <input type="number" required
  55 + step="1" min="1" max="2147483647" matInput
  56 + placeholder="{{ 'device-profile.condition-duration-value' | translate }}"
  57 + formControlName="value">
  58 + <mat-error *ngIf="conditionFormGroup.get('spec.value').hasError('required')">
  59 + {{ 'device-profile.condition-duration-value-required' | translate }}
  60 + </mat-error>
  61 + <mat-error *ngIf="conditionFormGroup.get('spec.value').hasError('min')">
  62 + {{ 'device-profile.condition-duration-value-range' | translate }}
  63 + </mat-error>
  64 + <mat-error *ngIf="conditionFormGroup.get('spec.value').hasError('max')">
  65 + {{ 'device-profile.condition-duration-value-range' | translate }}
  66 + </mat-error>
  67 + <mat-error *ngIf="conditionFormGroup.get('spec.value').hasError('pattern')">
  68 + {{ 'device-profile.condition-duration-value-pattern' | translate }}
  69 + </mat-error>
  70 + </mat-form-field>
  71 + <mat-form-field class="mat-block" hideRequiredMarker fxFlex floatLabel="always">
  72 + <mat-label></mat-label>
  73 + <mat-select formControlName="unit"
  74 + required
  75 + placeholder="{{ 'device-profile.condition-duration-time-unit' | translate }}">
  76 + <mat-option *ngFor="let timeUnit of timeUnits" [value]="timeUnit">
  77 + {{ timeUnitTranslations.get(timeUnit) | translate }}
  78 + </mat-option>
  79 + </mat-select>
  80 + <mat-error *ngIf="conditionFormGroup.get('spec.unit').hasError('required')">
  81 + {{ 'device-profile.condition-duration-time-unit-required' | translate }}
  82 + </mat-error>
  83 + </mat-form-field>
  84 + </div>
  85 + <div fxLayout="row" fxLayoutGap="8px" *ngIf="conditionFormGroup.get('spec.type').value == AlarmConditionType.REPEATING">
  86 + <mat-form-field class="mat-block" hideRequiredMarker fxFlex floatLabel="always">
  87 + <mat-label></mat-label>
  88 + <input type="number" required
  89 + step="1" min="1" max="2147483647" matInput
  90 + placeholder="{{ 'device-profile.condition-repeating-value' | translate }}"
  91 + formControlName="count">
  92 + <mat-error *ngIf="conditionFormGroup.get('spec.count').hasError('required')">
  93 + {{ 'device-profile.condition-repeating-value-required' | translate }}
  94 + </mat-error>
  95 + <mat-error *ngIf="conditionFormGroup.get('spec.count').hasError('min')">
  96 + {{ 'device-profile.condition-repeating-value-range' | translate }}
  97 + </mat-error>
  98 + <mat-error *ngIf="conditionFormGroup.get('spec.count').hasError('max')">
  99 + {{ 'device-profile.condition-repeating-value-range' | translate }}
  100 + </mat-error>
  101 + <mat-error *ngIf="conditionFormGroup.get('spec.count').hasError('pattern')">
  102 + {{ 'device-profile.condition-repeating-value-pattern' | translate }}
  103 + </mat-error>
  104 + </mat-form-field>
  105 + </div>
  106 + </section>
  107 + </div>
  108 + </fieldset>
  109 + </div>
  110 + <div mat-dialog-actions fxLayoutAlign="end center">
  111 + <button mat-raised-button color="primary"
  112 + *ngIf="!readonly"
  113 + type="submit"
  114 + [disabled]="(isLoading$ | async) || conditionFormGroup.invalid || !conditionFormGroup.dirty">
  115 + {{ 'action.save' | translate }}
  116 + </button>
  117 + <button mat-button color="primary"
  118 + type="button"
  119 + [disabled]="(isLoading$ | async)"
  120 + (click)="cancel()" cdkFocusInitial>
  121 + {{ (readonly ? 'action.close' : 'action.cancel') | translate }}
  122 + </button>
  123 + </div>
  124 +</form>
... ...
  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 + .row {
  18 + margin-top: 1em;
  19 + }
  20 +}
... ...
  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, Inject, OnInit, SkipSelf } from '@angular/core';
  18 +import { ErrorStateMatcher } from '@angular/material/core';
  19 +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
  20 +import { Store } from '@ngrx/store';
  21 +import { AppState } from '@core/core.state';
  22 +import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms';
  23 +import { Router } from '@angular/router';
  24 +import { DialogComponent } from '@app/shared/components/dialog.component';
  25 +import { UtilsService } from '@core/services/utils.service';
  26 +import { TranslateService } from '@ngx-translate/core';
  27 +import { KeyFilter, keyFilterInfosToKeyFilters, keyFiltersToKeyFilterInfos } from '@shared/models/query/query.models';
  28 +import { AlarmCondition, AlarmConditionType, AlarmConditionTypeTranslationMap } from '@shared/models/device.models';
  29 +import { TimeUnit, timeUnitTranslationMap } from '@shared/models/time/time.models';
  30 +
  31 +export interface AlarmRuleConditionDialogData {
  32 + readonly: boolean;
  33 + condition: AlarmCondition;
  34 +}
  35 +
  36 +@Component({
  37 + selector: 'tb-alarm-rule-condition-dialog',
  38 + templateUrl: './alarm-rule-condition-dialog.component.html',
  39 + providers: [{provide: ErrorStateMatcher, useExisting: AlarmRuleConditionDialogComponent}],
  40 + styleUrls: ['/alarm-rule-condition-dialog.component.scss']
  41 +})
  42 +export class AlarmRuleConditionDialogComponent extends DialogComponent<AlarmRuleConditionDialogComponent, AlarmCondition>
  43 + implements OnInit, ErrorStateMatcher {
  44 +
  45 + timeUnits = Object.keys(TimeUnit);
  46 + timeUnitTranslations = timeUnitTranslationMap;
  47 + alarmConditionTypes = Object.keys(AlarmConditionType);
  48 + AlarmConditionType = AlarmConditionType;
  49 + alarmConditionTypeTranslation = AlarmConditionTypeTranslationMap;
  50 +
  51 + readonly = this.data.readonly;
  52 + condition = this.data.condition;
  53 +
  54 + conditionFormGroup: FormGroup;
  55 +
  56 + submitted = false;
  57 +
  58 + constructor(protected store: Store<AppState>,
  59 + protected router: Router,
  60 + @Inject(MAT_DIALOG_DATA) public data: AlarmRuleConditionDialogData,
  61 + @SkipSelf() private errorStateMatcher: ErrorStateMatcher,
  62 + public dialogRef: MatDialogRef<AlarmRuleConditionDialogComponent, AlarmCondition>,
  63 + private fb: FormBuilder,
  64 + private utils: UtilsService,
  65 + public translate: TranslateService) {
  66 + super(store, router, dialogRef);
  67 +
  68 + this.conditionFormGroup = this.fb.group({
  69 + keyFilters: [keyFiltersToKeyFilterInfos(this.condition?.condition), Validators.required],
  70 + spec: this.fb.group({
  71 + type: [AlarmConditionType.SIMPLE, Validators.required],
  72 + unit: [{value: null, disable: true}, Validators.required],
  73 + value: [{value: null, disable: true}, [Validators.required, Validators.min(1), Validators.max(2147483647), Validators.pattern('[0-9]*')]],
  74 + count: [{value: null, disable: true}, [Validators.required, Validators.min(1), Validators.max(2147483647), Validators.pattern('[0-9]*')]]
  75 + })
  76 + });
  77 + this.conditionFormGroup.patchValue({spec: this.condition?.spec});
  78 + this.conditionFormGroup.get('spec.type').valueChanges.subscribe((type) => {
  79 + this.updateValidators(type, true, true);
  80 + });
  81 + if (this.readonly) {
  82 + this.conditionFormGroup.disable({emitEvent: false});
  83 + } else {
  84 + this.updateValidators(this.condition?.spec?.type);
  85 + }
  86 + }
  87 +
  88 + ngOnInit(): void {
  89 + }
  90 +
  91 + isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
  92 + const originalErrorState = this.errorStateMatcher.isErrorState(control, form);
  93 + const customErrorState = !!(control && control.invalid && this.submitted);
  94 + return originalErrorState || customErrorState;
  95 + }
  96 +
  97 + private updateValidators(type: AlarmConditionType, resetDuration = false, emitEvent = false) {
  98 + switch (type) {
  99 + case AlarmConditionType.DURATION:
  100 + this.conditionFormGroup.get('spec.value').enable();
  101 + this.conditionFormGroup.get('spec.unit').enable();
  102 + this.conditionFormGroup.get('spec.count').disable();
  103 + if (resetDuration) {
  104 + this.conditionFormGroup.get('spec').patchValue({
  105 + count: null
  106 + });
  107 + }
  108 + break;
  109 + case AlarmConditionType.REPEATING:
  110 + this.conditionFormGroup.get('spec.count').enable();
  111 + this.conditionFormGroup.get('spec.value').disable();
  112 + this.conditionFormGroup.get('spec.unit').disable();
  113 + if (resetDuration) {
  114 + this.conditionFormGroup.get('spec').patchValue({
  115 + value: null,
  116 + unit: null
  117 + });
  118 + }
  119 + break;
  120 + case AlarmConditionType.SIMPLE:
  121 + this.conditionFormGroup.get('spec.value').disable();
  122 + this.conditionFormGroup.get('spec.unit').disable();
  123 + this.conditionFormGroup.get('spec.count').disable();
  124 + if (resetDuration) {
  125 + this.conditionFormGroup.get('spec').patchValue({
  126 + value: null,
  127 + unit: null,
  128 + count: null
  129 + });
  130 + }
  131 + break;
  132 + }
  133 + this.conditionFormGroup.get('spec.value').updateValueAndValidity({emitEvent});
  134 + this.conditionFormGroup.get('spec.unit').updateValueAndValidity({emitEvent});
  135 + this.conditionFormGroup.get('spec.count').updateValueAndValidity({emitEvent});
  136 + }
  137 +
  138 + cancel(): void {
  139 + this.dialogRef.close(null);
  140 + }
  141 +
  142 + save(): void {
  143 + this.submitted = true;
  144 + this.condition = {
  145 + condition: keyFilterInfosToKeyFilters(this.conditionFormGroup.get('keyFilters').value),
  146 + spec: this.conditionFormGroup.get('spec').value
  147 + };
  148 + this.dialogRef.close(this.condition);
  149 + }
  150 +}
... ...
... ... @@ -15,8 +15,9 @@
15 15 limitations under the License.
16 16
17 17 -->
18   -<div fxLayout="column" fxFlex>
  18 +<div fxLayout="column" fxFlex [formGroup]="alarmRuleConditionFormGroup">
19 19 <div fxLayout="row" fxLayoutAlign="start center" style="min-height: 40px;">
  20 + <label class="tb-title" translate>device-profile.condition</label>
20 21 <span fxFlex></span>
21 22 <a mat-button color="primary"
22 23 type="button"
... ... @@ -27,9 +28,12 @@
27 28 </a>
28 29 </div>
29 30 <div class="tb-alarm-rule-condition" fxFlex fxLayout="column" fxLayoutAlign="center" (click)="openFilterDialog($event)">
30   - <tb-filter-text [formControl]="alarmRuleConditionControl"
  31 + <tb-filter-text formControlName="condition"
31 32 required
32 33 addFilterPrompt="{{'device-profile.enter-alarm-rule-condition-prompt' | translate}}">
33 34 </tb-filter-text>
  35 + <span class="tb-alarm-rule-condition-spec" [ngClass]="{disabled: this.disabled}" [innerHTML]="specText">
  36 + </span>
34 37 </div>
  38 +
35 39 </div>
... ...
... ... @@ -21,10 +21,15 @@
21 21 }
22 22 }
23 23 .tb-alarm-rule-condition {
24   - padding: 8px;
25   - border: 1px groove rgba(0, 0, 0, .25);
26   - border-radius: 4px;
27 24 cursor: pointer;
  25 + .tb-alarm-rule-condition-spec {
  26 + margin-top: 1em;
  27 + line-height: 1.8em;
  28 + padding: 4px;
  29 + &.disabled {
  30 + opacity: 0.7;
  31 + }
  32 + }
28 33 }
29 34 }
30 35
... ...
... ... @@ -18,20 +18,21 @@ import { Component, forwardRef, Input, OnInit } from '@angular/core';
18 18 import {
19 19 ControlValueAccessor,
20 20 FormBuilder,
21   - FormControl,
  21 + FormControl, FormGroup,
22 22 NG_VALIDATORS,
23 23 NG_VALUE_ACCESSOR,
24   - Validator
  24 + Validator, Validators
25 25 } from '@angular/forms';
26 26 import { MatDialog } from '@angular/material/dialog';
27   -import { KeyFilter } from '@shared/models/query/query.models';
28   -import { deepClone } from '@core/utils';
29   -import {
30   - AlarmRuleKeyFiltersDialogComponent,
31   - AlarmRuleKeyFiltersDialogData
32   -} from './alarm-rule-key-filters-dialog.component';
  27 +import { deepClone, isUndefined } from '@core/utils';
33 28 import { TranslateService } from '@ngx-translate/core';
34 29 import { DatePipe } from '@angular/common';
  30 +import { AlarmCondition, AlarmConditionSpec, AlarmConditionType } from '@shared/models/device.models';
  31 +import {
  32 + AlarmRuleConditionDialogComponent,
  33 + AlarmRuleConditionDialogData
  34 +} from '@home/components/profile/alarm/alarm-rule-condition-dialog.component';
  35 +import { TimeUnit } from '@shared/models/time/time.models';
35 36
36 37 @Component({
37 38 selector: 'tb-alarm-rule-condition',
... ... @@ -55,9 +56,11 @@ export class AlarmRuleConditionComponent implements ControlValueAccessor, OnInit
55 56 @Input()
56 57 disabled: boolean;
57 58
58   - alarmRuleConditionControl: FormControl;
  59 + alarmRuleConditionFormGroup: FormGroup;
  60 +
  61 + specText = '';
59 62
60   - private modelValue: Array<KeyFilter>;
  63 + private modelValue: AlarmCondition;
61 64
62 65 private propagateChange = (v: any) => { };
63 66
... ... @@ -75,25 +78,31 @@ export class AlarmRuleConditionComponent implements ControlValueAccessor, OnInit
75 78 }
76 79
77 80 ngOnInit() {
78   - this.alarmRuleConditionControl = this.fb.control(null);
  81 + this.alarmRuleConditionFormGroup = this.fb.group({
  82 + condition: [null, Validators.required],
  83 + spec: [null, Validators.required]
  84 + });
79 85 }
80 86
81 87 setDisabledState(isDisabled: boolean): void {
82 88 this.disabled = isDisabled;
83 89 if (this.disabled) {
84   - this.alarmRuleConditionControl.disable({emitEvent: false});
  90 + this.alarmRuleConditionFormGroup.disable({emitEvent: false});
85 91 } else {
86   - this.alarmRuleConditionControl.enable({emitEvent: false});
  92 + this.alarmRuleConditionFormGroup.enable({emitEvent: false});
87 93 }
88 94 }
89 95
90   - writeValue(value: Array<KeyFilter>): void {
  96 + writeValue(value: AlarmCondition): void {
91 97 this.modelValue = value;
  98 + if (this.modelValue !== null && isUndefined(this.modelValue?.spec)) {
  99 + this.modelValue = Object.assign(this.modelValue, {spec: {type: AlarmConditionType.SIMPLE}});
  100 + }
92 101 this.updateConditionInfo();
93 102 }
94 103
95 104 public conditionSet() {
96   - return this.modelValue && this.modelValue.length;
  105 + return this.modelValue && this.modelValue.condition.length;
97 106 }
98 107
99 108 public validate(c: FormControl) {
... ... @@ -108,13 +117,13 @@ export class AlarmRuleConditionComponent implements ControlValueAccessor, OnInit
108 117 if ($event) {
109 118 $event.stopPropagation();
110 119 }
111   - this.dialog.open<AlarmRuleKeyFiltersDialogComponent, AlarmRuleKeyFiltersDialogData,
112   - Array<KeyFilter>>(AlarmRuleKeyFiltersDialogComponent, {
  120 + this.dialog.open<AlarmRuleConditionDialogComponent, AlarmRuleConditionDialogData,
  121 + AlarmCondition>(AlarmRuleConditionDialogComponent, {
113 122 disableClose: true,
114 123 panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
115 124 data: {
116 125 readonly: this.disabled,
117   - keyFilters: this.disabled ? this.modelValue : deepClone(this.modelValue)
  126 + condition: this.disabled ? this.modelValue : deepClone(this.modelValue)
118 127 }
119 128 }).afterClosed().subscribe((result) => {
120 129 if (result) {
... ... @@ -125,7 +134,45 @@ export class AlarmRuleConditionComponent implements ControlValueAccessor, OnInit
125 134 }
126 135
127 136 private updateConditionInfo() {
128   - this.alarmRuleConditionControl.patchValue(this.modelValue);
  137 + this.alarmRuleConditionFormGroup.patchValue(
  138 + {
  139 + condition: this.modelValue?.condition,
  140 + spec: this.modelValue?.spec
  141 + }
  142 + );
  143 + this.updateSpecText();
  144 + }
  145 +
  146 + private updateSpecText() {
  147 + this.specText = '';
  148 + if (this.modelValue && this.modelValue.spec) {
  149 + const spec = this.modelValue.spec;
  150 + switch (spec.type) {
  151 + case AlarmConditionType.SIMPLE:
  152 + break;
  153 + case AlarmConditionType.DURATION:
  154 + let duringText = '';
  155 + switch (spec.unit) {
  156 + case TimeUnit.SECONDS:
  157 + duringText = this.translate.instant('timewindow.seconds', {seconds: spec.value});
  158 + break;
  159 + case TimeUnit.MINUTES:
  160 + duringText = this.translate.instant('timewindow.minutes', {minutes: spec.value});
  161 + break;
  162 + case TimeUnit.HOURS:
  163 + duringText = this.translate.instant('timewindow.hours', {hours: spec.value});
  164 + break;
  165 + case TimeUnit.DAYS:
  166 + duringText = this.translate.instant('timewindow.days', {days: spec.value});
  167 + break;
  168 + }
  169 + this.specText = this.translate.instant('device-profile.condition-during', {during: duringText});
  170 + break;
  171 + case AlarmConditionType.REPEATING:
  172 + this.specText = this.translate.instant('device-profile.condition-repeat-times', {count: spec.count});
  173 + break;
  174 + }
  175 + }
129 176 }
130 177
131 178 private updateModel() {
... ...
... ... @@ -16,90 +16,36 @@
16 16
17 17 -->
18 18 <div fxLayout="column" [formGroup]="alarmRuleFormGroup">
19   - <mat-tab-group>
20   - <mat-tab label="{{ 'device-profile.condition' | translate }}" formGroupName="condition">
21   - <tb-alarm-rule-condition fxFlex class="row"
22   - formControlName="condition">
23   - </tb-alarm-rule-condition>
24   - <section formGroupName="spec" class="row">
25   - <mat-form-field class="mat-block" hideRequiredMarker>
26   - <mat-label translate>device-profile.condition-type</mat-label>
27   - <mat-select formControlName="type" required>
28   - <mat-option *ngFor="let alarmConditionType of alarmConditionTypes" [value]="alarmConditionType">
29   - {{ alarmConditionTypeTranslation.get(alarmConditionType) | translate }}
30   - </mat-option>
31   - </mat-select>
32   - <mat-error *ngIf="alarmRuleFormGroup.get('condition.spec.type').hasError('required')">
33   - {{ 'device-profile.condition-type-required' | translate }}
34   - </mat-error>
35   - </mat-form-field>
36   - <div fxLayout="row" fxLayoutGap="8px" *ngIf="alarmRuleFormGroup.get('condition.spec.type').value == AlarmConditionType.DURATION">
37   - <mat-form-field class="mat-block" hideRequiredMarker fxFlex floatLabel="always">
38   - <mat-label></mat-label>
39   - <input type="number" required
40   - step="1" min="1" max="2147483647" matInput
41   - placeholder="{{ 'device-profile.condition-duration-value' | translate }}"
42   - formControlName="value">
43   - <mat-error *ngIf="alarmRuleFormGroup.get('condition.spec.value').hasError('required')">
44   - {{ 'device-profile.condition-duration-value-required' | translate }}
45   - </mat-error>
46   - <mat-error *ngIf="alarmRuleFormGroup.get('condition.spec.value').hasError('min')">
47   - {{ 'device-profile.condition-duration-value-range' | translate }}
48   - </mat-error>
49   - <mat-error *ngIf="alarmRuleFormGroup.get('condition.spec.value').hasError('max')">
50   - {{ 'device-profile.condition-duration-value-range' | translate }}
51   - </mat-error>
52   - <mat-error *ngIf="alarmRuleFormGroup.get('condition.spec.value').hasError('pattern')">
53   - {{ 'device-profile.condition-duration-value-pattern' | translate }}
54   - </mat-error>
55   - </mat-form-field>
56   - <mat-form-field class="mat-block" hideRequiredMarker fxFlex floatLabel="always">
57   - <mat-label></mat-label>
58   - <mat-select formControlName="unit"
59   - required
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.spec.unit').hasError('required')">
66   - {{ 'device-profile.condition-duration-time-unit-required' | translate }}
67   - </mat-error>
68   - </mat-form-field>
69   - </div>
70   - <div fxLayout="row" fxLayoutGap="8px" *ngIf="alarmRuleFormGroup.get('condition.spec.type').value == AlarmConditionType.REPEATING">
71   - <mat-form-field class="mat-block" hideRequiredMarker fxFlex floatLabel="always">
72   - <mat-label></mat-label>
73   - <input type="number" required
74   - step="1" min="1" max="2147483647" matInput
75   - placeholder="{{ 'device-profile.condition-repeating-value' | translate }}"
76   - formControlName="count">
77   - <mat-error *ngIf="alarmRuleFormGroup.get('condition.spec.count').hasError('required')">
78   - {{ 'device-profile.condition-repeating-value-required' | translate }}
79   - </mat-error>
80   - <mat-error *ngIf="alarmRuleFormGroup.get('condition.spec.count').hasError('min')">
81   - {{ 'device-profile.condition-repeating-value-range' | translate }}
82   - </mat-error>
83   - <mat-error *ngIf="alarmRuleFormGroup.get('condition.spec.count').hasError('max')">
84   - {{ 'device-profile.condition-repeating-value-range' | translate }}
85   - </mat-error>
86   - <mat-error *ngIf="alarmRuleFormGroup.get('condition.spec.count').hasError('pattern')">
87   - {{ 'device-profile.condition-repeating-value-pattern' | translate }}
88   - </mat-error>
89   - </mat-form-field>
90   - </div>
91   - </section>
92   - </mat-tab>
93   - <mat-tab label="{{ 'device-profile.schedule' | translate }}">
94   - <tb-alarm-schedule fxFlex class="row"
95   - formControlName="schedule">
96   - </tb-alarm-schedule>
97   - </mat-tab>
98   - <mat-tab label="{{ 'device-profile.alarm-rule-details' | translate }}">
99   - <mat-form-field class="mat-block row">
100   - <mat-label translate>device-profile.alarm-details</mat-label>
101   - <textarea matInput formControlName="alarmDetails" rows="5"></textarea>
102   - </mat-form-field>
103   - </mat-tab>
104   - </mat-tab-group>
  19 + <tb-alarm-rule-condition fxFlex class="row"
  20 + formControlName="condition">
  21 + </tb-alarm-rule-condition>
  22 + <mat-divider class="row"></mat-divider>
  23 + <tb-alarm-schedule-info fxFlex class="row"
  24 + formControlName="schedule">
  25 + </tb-alarm-schedule-info>
  26 + <mat-divider class="row"></mat-divider>
  27 + <div fxLayout="column" fxFlex class="tb-alarm-rule-details row">
  28 + <div fxLayout="row" fxLayoutAlign="start center" style="min-height: 40px;">
  29 + <label class="tb-title" translate>device-profile.alarm-rule-details</label>
  30 + <span fxFlex></span>
  31 + <a mat-button color="primary"
  32 + *ngIf="!disabled"
  33 + type="button"
  34 + (click)="openEditDetailsDialog($event)"
  35 + matTooltip="{{ 'action.edit' | translate }}"
  36 + matTooltipPosition="above">
  37 + {{ 'action.edit' | translate }}
  38 + </a>
  39 + </div>
  40 + <div fxLayout="row" fxLayoutAlign="start start">
  41 + <div class="tb-alarm-rule-details-content" [ngClass]="{disabled: this.disabled, collapsed: !this.expandAlarmDetails}"
  42 + (click)="!disabled ? openEditDetailsDialog($event) : {}"
  43 + fxFlex [innerHTML]="alarmRuleFormGroup.get('alarmDetails').value"></div>
  44 + <a mat-button color="primary"
  45 + type="button"
  46 + (click)="expandAlarmDetails = !expandAlarmDetails">
  47 + {{ (expandAlarmDetails ? 'action.hide' : 'action.read-more') | translate }}
  48 + </a>
  49 + </div>
  50 + </div>
105 51 </div>
... ...
... ... @@ -17,5 +17,29 @@
17 17 .row {
18 18 margin-top: 1em;
19 19 }
  20 + .tb-alarm-rule-details {
  21 + a.mat-button {
  22 + &:hover, &:focus {
  23 + border-bottom: none;
  24 + }
  25 + }
  26 + .tb-alarm-rule-details-content {
  27 + min-height: 33px;
  28 + overflow: hidden;
  29 + white-space: pre;
  30 + line-height: 1.8em;
  31 + padding: 4px;
  32 + cursor: pointer;
  33 + &.collapsed {
  34 + max-height: 33px;
  35 + white-space: nowrap;
  36 + text-overflow: ellipsis;
  37 + }
  38 + &.disabled {
  39 + opacity: 0.7;
  40 + cursor: auto;
  41 + }
  42 + }
  43 + }
20 44 }
21 45
... ...
... ... @@ -25,11 +25,14 @@ import {
25 25 Validator,
26 26 Validators
27 27 } from '@angular/forms';
28   -import { AlarmConditionType, AlarmConditionTypeTranslationMap, AlarmRule } from '@shared/models/device.models';
  28 +import { AlarmRule } from '@shared/models/device.models';
29 29 import { MatDialog } from '@angular/material/dialog';
30   -import { TimeUnit, timeUnitTranslationMap } from '@shared/models/time/time.models';
31 30 import { coerceBooleanProperty } from '@angular/cdk/coercion';
32   -import { isUndefined } from '@core/utils';
  31 +import { isDefinedAndNotNull } from '@core/utils';
  32 +import {
  33 + EditAlarmDetailsDialogComponent,
  34 + EditAlarmDetailsDialogData
  35 +} from '@home/components/profile/alarm/edit-alarm-details-dialog.component';
33 36
34 37 @Component({
35 38 selector: 'tb-alarm-rule',
... ... @@ -50,12 +53,6 @@ import { isUndefined } from '@core/utils';
50 53 })
51 54 export class AlarmRuleComponent implements ControlValueAccessor, OnInit, Validator {
52 55
53   - timeUnits = Object.keys(TimeUnit);
54   - timeUnitTranslations = timeUnitTranslationMap;
55   - alarmConditionTypes = Object.keys(AlarmConditionType);
56   - AlarmConditionType = AlarmConditionType;
57   - alarmConditionTypeTranslation = AlarmConditionTypeTranslationMap;
58   -
59 56 @Input()
60 57 disabled: boolean;
61 58
... ... @@ -72,6 +69,8 @@ export class AlarmRuleComponent implements ControlValueAccessor, OnInit, Validat
72 69
73 70 alarmRuleFormGroup: FormGroup;
74 71
  72 + expandAlarmDetails = false;
  73 +
75 74 private propagateChange = (v: any) => { };
76 75
77 76 constructor(private dialog: MatDialog,
... ... @@ -87,21 +86,10 @@ export class AlarmRuleComponent implements ControlValueAccessor, OnInit, Validat
87 86
88 87 ngOnInit() {
89 88 this.alarmRuleFormGroup = this.fb.group({
90   - condition: this.fb.group({
91   - condition: [null, Validators.required],
92   - spec: this.fb.group({
93   - type: [AlarmConditionType.SIMPLE, Validators.required],
94   - unit: [{value: null, disable: true}, Validators.required],
95   - value: [{value: null, disable: true}, [Validators.required, Validators.min(1), Validators.max(2147483647), Validators.pattern('[0-9]*')]],
96   - count: [{value: null, disable: true}, [Validators.required, Validators.min(1), Validators.max(2147483647), Validators.pattern('[0-9]*')]]
97   - })
98   - }, Validators.required),
  89 + condition: [null, [Validators.required]],
99 90 schedule: [null],
100 91 alarmDetails: [null]
101 92 });
102   - this.alarmRuleFormGroup.get('condition.spec.type').valueChanges.subscribe((type) => {
103   - this.updateValidators(type, true, true);
104   - });
105 93 this.alarmRuleFormGroup.valueChanges.subscribe(() => {
106 94 this.updateModel();
107 95 });
... ... @@ -118,11 +106,25 @@ export class AlarmRuleComponent implements ControlValueAccessor, OnInit, Validat
118 106
119 107 writeValue(value: AlarmRule): void {
120 108 this.modelValue = value;
121   - if (this.modelValue !== null && isUndefined(this.modelValue?.condition?.spec)) {
122   - this.modelValue = Object.assign(this.modelValue, {condition: {spec: {type: AlarmConditionType.SIMPLE}}});
123   - }
124 109 this.alarmRuleFormGroup.reset(this.modelValue || undefined, {emitEvent: false});
125   - this.updateValidators(this.modelValue?.condition?.spec?.type);
  110 + }
  111 +
  112 + public openEditDetailsDialog($event: Event) {
  113 + if ($event) {
  114 + $event.stopPropagation();
  115 + }
  116 + this.dialog.open<EditAlarmDetailsDialogComponent, EditAlarmDetailsDialogData,
  117 + string>(EditAlarmDetailsDialogComponent, {
  118 + disableClose: true,
  119 + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
  120 + data: {
  121 + alarmDetails: this.alarmRuleFormGroup.get('alarmDetails').value
  122 + }
  123 + }).afterClosed().subscribe((alarmDetails) => {
  124 + if (isDefinedAndNotNull(alarmDetails)) {
  125 + this.alarmRuleFormGroup.patchValue({alarmDetails});
  126 + }
  127 + });
126 128 }
127 129
128 130 public validate(c: FormControl) {
... ... @@ -133,47 +135,6 @@ export class AlarmRuleComponent implements ControlValueAccessor, OnInit, Validat
133 135 };
134 136 }
135 137
136   - private updateValidators(type: AlarmConditionType, resetDuration = false, emitEvent = false) {
137   - switch (type) {
138   - case AlarmConditionType.DURATION:
139   - this.alarmRuleFormGroup.get('condition.spec.value').enable();
140   - this.alarmRuleFormGroup.get('condition.spec.unit').enable();
141   - this.alarmRuleFormGroup.get('condition.spec.count').disable();
142   - if (resetDuration) {
143   - this.alarmRuleFormGroup.get('condition.spec').patchValue({
144   - count: null
145   - });
146   - }
147   - break;
148   - case AlarmConditionType.REPEATING:
149   - this.alarmRuleFormGroup.get('condition.spec.count').enable();
150   - this.alarmRuleFormGroup.get('condition.spec.value').disable();
151   - this.alarmRuleFormGroup.get('condition.spec.unit').disable();
152   - if (resetDuration) {
153   - this.alarmRuleFormGroup.get('condition.spec').patchValue({
154   - value: null,
155   - unit: null
156   - });
157   - }
158   - break;
159   - case AlarmConditionType.SIMPLE:
160   - this.alarmRuleFormGroup.get('condition.spec.value').disable();
161   - this.alarmRuleFormGroup.get('condition.spec.unit').disable();
162   - this.alarmRuleFormGroup.get('condition.spec.count').disable();
163   - if (resetDuration) {
164   - this.alarmRuleFormGroup.get('condition.spec').patchValue({
165   - value: null,
166   - unit: null,
167   - count: null
168   - });
169   - }
170   - break;
171   - }
172   - this.alarmRuleFormGroup.get('condition.spec.value').updateValueAndValidity({emitEvent});
173   - this.alarmRuleFormGroup.get('condition.spec.unit').updateValueAndValidity({emitEvent});
174   - this.alarmRuleFormGroup.get('condition.spec.count').updateValueAndValidity({emitEvent});
175   - }
176   -
177 138 private updateModel() {
178 139 const value = this.alarmRuleFormGroup.value;
179 140 if (this.modelValue) {
... ...
ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule-dialog.component.html renamed from ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-key-filters-dialog.component.html
... ... @@ -15,9 +15,9 @@
15 15 limitations under the License.
16 16
17 17 -->
18   -<form [formGroup]="keyFiltersFormGroup" (ngSubmit)="save()" style="width: 700px;">
  18 +<form [formGroup]="alarmScheduleFormGroup" (ngSubmit)="save()" style="width: 800px;">
19 19 <mat-toolbar color="primary">
20   - <h2>{{ (readonly ? 'device-profile.alarm-rule-condition' : 'device-profile.edit-alarm-rule-condition') | translate }}</h2>
  20 + <h2>{{ (readonly ? 'device-profile.schedule' : 'device-profile.edit-schedule') | translate }}</h2>
21 21 <span fxFlex></span>
22 22 <button mat-icon-button
23 23 (click)="cancel()"
... ... @@ -30,12 +30,9 @@
30 30 <div mat-dialog-content>
31 31 <fieldset [disabled]="isLoading$ | async">
32 32 <div fxFlex fxLayout="column">
33   - <tb-key-filter-list
34   - [displayUserParameters]="false"
35   - [allowUserDynamicSource]="false"
36   - [telemetryKeysOnly]="true"
37   - formControlName="keyFilters">
38   - </tb-key-filter-list>
  33 + <tb-alarm-schedule
  34 + formControlName="alarmSchedule">
  35 + </tb-alarm-schedule>
39 36 </div>
40 37 </fieldset>
41 38 </div>
... ... @@ -43,7 +40,7 @@
43 40 <button mat-raised-button color="primary"
44 41 *ngIf="!readonly"
45 42 type="submit"
46   - [disabled]="(isLoading$ | async) || keyFiltersFormGroup.invalid || !keyFiltersFormGroup.dirty">
  43 + [disabled]="(isLoading$ | async) || alarmScheduleFormGroup.invalid || !alarmScheduleFormGroup.dirty">
47 44 {{ 'action.save' | translate }}
48 45 </button>
49 46 <button mat-button color="primary"
... ...
ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule-dialog.component.ts renamed from ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-key-filters-dialog.component.ts
... ... @@ -19,49 +19,49 @@ import { ErrorStateMatcher } from '@angular/material/core';
19 19 import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
20 20 import { Store } from '@ngrx/store';
21 21 import { AppState } from '@core/core.state';
22   -import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms';
  22 +import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm } from '@angular/forms';
23 23 import { Router } from '@angular/router';
24 24 import { DialogComponent } from '@app/shared/components/dialog.component';
25 25 import { UtilsService } from '@core/services/utils.service';
26 26 import { TranslateService } from '@ngx-translate/core';
27   -import { KeyFilter, keyFilterInfosToKeyFilters, keyFiltersToKeyFilterInfos } from '@shared/models/query/query.models';
  27 +import { AlarmSchedule } from '@shared/models/device.models';
28 28
29   -export interface AlarmRuleKeyFiltersDialogData {
  29 +export interface AlarmScheduleDialogData {
30 30 readonly: boolean;
31   - keyFilters: Array<KeyFilter>;
  31 + alarmSchedule: AlarmSchedule;
32 32 }
33 33
34 34 @Component({
35   - selector: 'tb-alarm-rule-key-filters-dialog',
36   - templateUrl: './alarm-rule-key-filters-dialog.component.html',
37   - providers: [{provide: ErrorStateMatcher, useExisting: AlarmRuleKeyFiltersDialogComponent}],
  35 + selector: 'tb-alarm-schedule-dialog',
  36 + templateUrl: './alarm-schedule-dialog.component.html',
  37 + providers: [{provide: ErrorStateMatcher, useExisting: AlarmScheduleDialogComponent}],
38 38 styleUrls: []
39 39 })
40   -export class AlarmRuleKeyFiltersDialogComponent extends DialogComponent<AlarmRuleKeyFiltersDialogComponent, Array<KeyFilter>>
  40 +export class AlarmScheduleDialogComponent extends DialogComponent<AlarmScheduleDialogComponent, AlarmSchedule>
41 41 implements OnInit, ErrorStateMatcher {
42 42
43 43 readonly = this.data.readonly;
44   - keyFilters = this.data.keyFilters;
  44 + alarmSchedule = this.data.alarmSchedule;
45 45
46   - keyFiltersFormGroup: FormGroup;
  46 + alarmScheduleFormGroup: FormGroup;
47 47
48 48 submitted = false;
49 49
50 50 constructor(protected store: Store<AppState>,
51 51 protected router: Router,
52   - @Inject(MAT_DIALOG_DATA) public data: AlarmRuleKeyFiltersDialogData,
  52 + @Inject(MAT_DIALOG_DATA) public data: AlarmScheduleDialogData,
53 53 @SkipSelf() private errorStateMatcher: ErrorStateMatcher,
54   - public dialogRef: MatDialogRef<AlarmRuleKeyFiltersDialogComponent, Array<KeyFilter>>,
  54 + public dialogRef: MatDialogRef<AlarmScheduleDialogComponent, AlarmSchedule>,
55 55 private fb: FormBuilder,
56 56 private utils: UtilsService,
57 57 public translate: TranslateService) {
58 58 super(store, router, dialogRef);
59 59
60   - this.keyFiltersFormGroup = this.fb.group({
61   - keyFilters: [keyFiltersToKeyFilterInfos(this.keyFilters), Validators.required]
  60 + this.alarmScheduleFormGroup = this.fb.group({
  61 + alarmSchedule: [this.alarmSchedule]
62 62 });
63 63 if (this.readonly) {
64   - this.keyFiltersFormGroup.disable({emitEvent: false});
  64 + this.alarmScheduleFormGroup.disable({emitEvent: false});
65 65 }
66 66 }
67 67
... ... @@ -80,7 +80,7 @@ export class AlarmRuleKeyFiltersDialogComponent extends DialogComponent<AlarmRul
80 80
81 81 save(): void {
82 82 this.submitted = true;
83   - this.keyFilters = keyFilterInfosToKeyFilters(this.keyFiltersFormGroup.get('keyFilters').value);
84   - this.dialogRef.close(this.keyFilters);
  83 + this.alarmSchedule = this.alarmScheduleFormGroup.get('alarmSchedule').value;
  84 + this.dialogRef.close(this.alarmSchedule);
85 85 }
86 86 }
... ...
  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 fxLayout="column" fxFlex>
  19 + <div fxLayout="row" fxLayoutAlign="start center" style="min-height: 40px;">
  20 + <label class="tb-title" translate>device-profile.schedule</label>
  21 + <span fxFlex></span>
  22 + <a mat-button color="primary"
  23 + type="button"
  24 + (click)="openScheduleDialog($event)"
  25 + matTooltip="{{ (disabled ? 'action.view' : 'action.edit') | translate }}"
  26 + matTooltipPosition="above">
  27 + {{ (disabled ? 'action.view' : 'action.edit' ) | translate }}
  28 + </a>
  29 + </div>
  30 + <sapn class="tb-alarm-rule-schedule" [ngClass]="{disabled: this.disabled}" (click)="openScheduleDialog($event)"
  31 + [innerHTML]="scheduleText">
  32 + </sapn>
  33 +</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 + display: flex;
  18 + a.mat-button {
  19 + &:hover, &:focus {
  20 + border-bottom: none;
  21 + }
  22 + }
  23 + .tb-alarm-rule-schedule {
  24 + line-height: 1.8em;
  25 + padding: 4px;
  26 + cursor: pointer;
  27 + &.disabled {
  28 + opacity: 0.7;
  29 + }
  30 + .nowrap {
  31 + white-space: nowrap;
  32 + }
  33 + }
  34 +}
... ...
  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 { ChangeDetectorRef, Component, forwardRef, Input, OnInit } from '@angular/core';
  18 +import { ControlValueAccessor, FormBuilder, NG_VALUE_ACCESSOR } from '@angular/forms';
  19 +import {
  20 + AlarmSchedule,
  21 + AlarmScheduleType,
  22 + AlarmScheduleTypeTranslationMap, dayOfWeekTranslations,
  23 + getAlarmScheduleRangeText, utcTimestampToTimeOfDay
  24 +} from '@shared/models/device.models';
  25 +import { MatDialog } from '@angular/material/dialog';
  26 +import {
  27 + AlarmScheduleDialogComponent,
  28 + AlarmScheduleDialogData
  29 +} from '@home/components/profile/alarm/alarm-schedule-dialog.component';
  30 +import { deepClone, isDefinedAndNotNull } from '@core/utils';
  31 +import { TranslateService } from '@ngx-translate/core';
  32 +
  33 +@Component({
  34 + selector: 'tb-alarm-schedule-info',
  35 + templateUrl: './alarm-schedule-info.component.html',
  36 + styleUrls: ['./alarm-schedule-info.component.scss'],
  37 + providers: [{
  38 + provide: NG_VALUE_ACCESSOR,
  39 + useExisting: forwardRef(() => AlarmScheduleInfoComponent),
  40 + multi: true
  41 + }]
  42 +})
  43 +export class AlarmScheduleInfoComponent implements ControlValueAccessor, OnInit {
  44 +
  45 + @Input()
  46 + disabled: boolean;
  47 +
  48 + private modelValue: AlarmSchedule;
  49 +
  50 + scheduleText = '';
  51 +
  52 + private propagateChange = (v: any) => { };
  53 +
  54 + constructor(private dialog: MatDialog,
  55 + private translate: TranslateService,
  56 + private cd: ChangeDetectorRef) {
  57 + }
  58 +
  59 + ngOnInit(): void {
  60 + }
  61 +
  62 + registerOnChange(fn: any): void {
  63 + this.propagateChange = fn;
  64 + }
  65 +
  66 + registerOnTouched(fn: any): void {
  67 + }
  68 +
  69 + setDisabledState(isDisabled: boolean): void {
  70 + this.disabled = isDisabled;
  71 + }
  72 +
  73 + writeValue(value: AlarmSchedule): void {
  74 + this.modelValue = value;
  75 + this.updateScheduleText();
  76 + }
  77 +
  78 + private updateScheduleText() {
  79 + let schedule = this.modelValue;
  80 + if (!isDefinedAndNotNull(schedule)) {
  81 + schedule = {
  82 + type: AlarmScheduleType.ANY_TIME
  83 + };
  84 + }
  85 + this.scheduleText = '';
  86 + switch (schedule.type) {
  87 + case AlarmScheduleType.ANY_TIME:
  88 + this.scheduleText = this.translate.instant('device-profile.schedule-any-time');
  89 + break;
  90 + case AlarmScheduleType.SPECIFIC_TIME:
  91 + for (const day of schedule.daysOfWeek) {
  92 + if (this.scheduleText.length) {
  93 + this.scheduleText += ', ';
  94 + }
  95 + this.scheduleText += this.translate.instant(dayOfWeekTranslations[day - 1]);
  96 + }
  97 + this.scheduleText += ' <b>' + getAlarmScheduleRangeText(utcTimestampToTimeOfDay(schedule.startsOn),
  98 + utcTimestampToTimeOfDay(schedule.endsOn)) + '</b>';
  99 + break;
  100 + case AlarmScheduleType.CUSTOM:
  101 + for (const item of schedule.items) {
  102 + if (item.enabled) {
  103 + if (this.scheduleText.length) {
  104 + this.scheduleText += '<br/>';
  105 + }
  106 + this.scheduleText += this.translate.instant(dayOfWeekTranslations[item.dayOfWeek - 1]);
  107 + this.scheduleText += ' <b>' + getAlarmScheduleRangeText(utcTimestampToTimeOfDay(item.startsOn),
  108 + utcTimestampToTimeOfDay(item.endsOn)) + '</b>';
  109 + }
  110 + }
  111 + break;
  112 + }
  113 + }
  114 +
  115 + public openScheduleDialog($event: Event) {
  116 + if ($event) {
  117 + $event.stopPropagation();
  118 + }
  119 + this.dialog.open<AlarmScheduleDialogComponent, AlarmScheduleDialogData,
  120 + AlarmSchedule>(AlarmScheduleDialogComponent, {
  121 + disableClose: true,
  122 + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
  123 + data: {
  124 + readonly: this.disabled,
  125 + alarmSchedule: this.disabled ? this.modelValue : deepClone(this.modelValue)
  126 + }
  127 + }).afterClosed().subscribe((result) => {
  128 + if (result) {
  129 + this.modelValue = result;
  130 + this.propagateChange(this.modelValue);
  131 + this.updateScheduleText();
  132 + this.cd.detectChanges();
  133 + }
  134 + });
  135 + }
  136 +
  137 +}
... ...
... ... @@ -35,33 +35,20 @@
35 35 </tb-timezone-select>
36 36 <section *ngIf="alarmScheduleForm.get('type').value === alarmScheduleType.SPECIFIC_TIME">
37 37 <div class="tb-small" style="margin-bottom: 0.5em" translate>device-profile.schedule-days</div>
38   - <div fxLayout="column" fxLayout.gt-md="row" fxLayoutGap="16px" style="padding-bottom: 16px;">
  38 + <div fxLayout="column" fxLayout.gt-md="row" fxLayoutGap="16px">
39 39 <div fxLayout="row" fxLayoutGap="16px">
40   - <mat-checkbox [formControl]="weeklyRepeatControl(0)">
41   - {{ 'device-profile.schedule-day.monday' | translate }}
42   - </mat-checkbox>
43   - <mat-checkbox [formControl]="weeklyRepeatControl(1)">
44   - {{ 'device-profile.schedule-day.tuesday' | translate }}
45   - </mat-checkbox>
46   - <mat-checkbox [formControl]="weeklyRepeatControl(2)">
47   - {{ 'device-profile.schedule-day.wednesday' | translate }}
48   - </mat-checkbox>
49   - <mat-checkbox [formControl]="weeklyRepeatControl(3)">
50   - {{ 'device-profile.schedule-day.thursday' | translate }}
  40 + <mat-checkbox *ngFor="let day of firstRowDays" [formControl]="weeklyRepeatControl(day)">
  41 + {{ dayOfWeekTranslationsArray[day] | translate }}
51 42 </mat-checkbox>
52 43 </div>
53 44 <div fxLayout="row" fxLayoutGap="16px">
54   - <mat-checkbox [formControl]="weeklyRepeatControl(4)">
55   - {{ 'device-profile.schedule-day.friday' | translate }}
56   - </mat-checkbox>
57   - <mat-checkbox [formControl]="weeklyRepeatControl(5)">
58   - {{ 'device-profile.schedule-day.saturday' | translate }}
59   - </mat-checkbox>
60   - <mat-checkbox [formControl]="weeklyRepeatControl(6)">
61   - {{ 'device-profile.schedule-day.sunday' | translate }}
  45 + <mat-checkbox *ngFor="let day of secondRowDays" [formControl]="weeklyRepeatControl(day)">
  46 + {{ dayOfWeekTranslationsArray[day] | translate }}
62 47 </mat-checkbox>
63 48 </div>
64 49 </div>
  50 + <tb-error style="display: block;" [error]="alarmScheduleForm.get('daysOfWeek').hasError('dayOfWeeks')
  51 + ? ('device-profile.schedule-days-of-week-required' | translate) : ''"></tb-error>
65 52 <div class="tb-small" style="margin-bottom: 0.5em" translate>device-profile.schedule-time</div>
66 53 <div fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px">
67 54 <div fxLayout="row" fxLayoutGap="8px" fxFlex.gt-md>
... ... @@ -87,169 +74,35 @@
87 74 </section>
88 75 <section *ngIf="alarmScheduleForm.get('type').value === alarmScheduleType.CUSTOM">
89 76 <div class="tb-small" style="margin-bottom: 0.5em" translate>device-profile.schedule-days</div>
90   - <div fxLayout="column" formArrayName="items" fxLayoutGap="1em">
91   - <div fxLayout.xs="column" fxLayout="row" fxLayoutGap="8px" formGroupName="0" fxLayoutAlign="start center" fxLayoutAlign.xs="center start">
92   - <mat-checkbox formControlName="enabled" fxFlex="17" (change)="changeCustomScheduler($event, 0)">
93   - {{ 'device-profile.schedule-day.monday' | translate }}
94   - </mat-checkbox>
95   - <div fxLayout="row" fxLayoutGap="8px" fxFlex>
96   - <mat-form-field fxFlex.xs fxFlex.sm="100px" fxFlex.md="100px">
97   - <mat-label translate>device-profile.schedule-time-from</mat-label>
98   - <mat-datetimepicker-toggle [for]="startTimePicker1" matPrefix></mat-datetimepicker-toggle>
99   - <mat-datetimepicker #startTimePicker1 type="time" openOnFocus="true"></mat-datetimepicker>
100   - <input required matInput formControlName="startsOn" [matDatetimepicker]="startTimePicker1">
101   - </mat-form-field>
102   - <mat-form-field fxFlex.xs fxFlex.sm="100px" fxFlex.md="100px">
103   - <mat-label translate>device-profile.schedule-time-to</mat-label>
104   - <mat-datetimepicker-toggle [for]="endTimePicker1" matPrefix></mat-datetimepicker-toggle>
105   - <mat-datetimepicker #endTimePicker1 type="time" openOnFocus="true"></mat-datetimepicker>
106   - <input required matInput formControlName="endsOn" [matDatetimepicker]="endTimePicker1">
107   - </mat-form-field>
108   - </div>
109   - <div fxFlex fxLayoutAlign="center center"
110   - style="text-align: center"
111   - [innerHTML]="getSchedulerRangeText(itemsSchedulerForm.at(0))">
112   - </div>
113   - </div>
114   - <div fxLayout.xs="column" fxLayout="row" fxLayoutGap="8px" formGroupName="1" fxLayoutAlign="start center" fxLayoutAlign.xs="center start">
115   - <mat-checkbox formControlName="enabled" fxFlex="17" (change)="changeCustomScheduler($event, 1)">
116   - {{ 'device-profile.schedule-day.tuesday' | translate }}
117   - </mat-checkbox>
118   - <div fxLayout="row" fxLayoutGap="8px" fxFlex>
119   - <mat-form-field fxFlex.xs fxFlex.sm="100px" fxFlex.md="100px">
120   - <mat-label translate>device-profile.schedule-time-from</mat-label>
121   - <mat-datetimepicker-toggle [for]="startTimePicker2" matPrefix></mat-datetimepicker-toggle>
122   - <mat-datetimepicker #startTimePicker2 type="time" openOnFocus="true"></mat-datetimepicker>
123   - <input required matInput formControlName="startsOn" [matDatetimepicker]="startTimePicker2">
124   - </mat-form-field>
125   - <mat-form-field fxFlex.xs fxFlex.sm="100px" fxFlex.md="100px">
126   - <mat-label translate>device-profile.schedule-time-to</mat-label>
127   - <mat-datetimepicker-toggle [for]="endTimePicker2" matPrefix></mat-datetimepicker-toggle>
128   - <mat-datetimepicker #endTimePicker2 type="time" openOnFocus="true"></mat-datetimepicker>
129   - <input required matInput formControlName="endsOn" [matDatetimepicker]="endTimePicker2">
130   - </mat-form-field>
131   - </div>
132   - <div fxFlex fxLayoutAlign="center center"
133   - style="text-align: center"
134   - [innerHTML]="getSchedulerRangeText(itemsSchedulerForm.at(1))">
135   - </div>
136   - </div>
137   - <div fxLayout.xs="column" fxLayout="row" fxLayoutGap="8px" formGroupName="2" fxLayoutAlign="start center" fxLayoutAlign.xs="center start">
138   - <mat-checkbox formControlName="enabled" fxFlex="17" (change)="changeCustomScheduler($event, 2)">
139   - {{ 'device-profile.schedule-day.wednesday' | translate }}
140   - </mat-checkbox>
141   - <div fxLayout="row" fxLayoutGap="8px" fxFlex>
142   - <mat-form-field fxFlex.xs fxFlex.sm="100px" fxFlex.md="100px">
143   - <mat-label translate>device-profile.schedule-time-from</mat-label>
144   - <mat-datetimepicker-toggle [for]="startTimePicker3" matPrefix></mat-datetimepicker-toggle>
145   - <mat-datetimepicker #startTimePicker3 type="time" openOnFocus="true"></mat-datetimepicker>
146   - <input required matInput formControlName="startsOn" [matDatetimepicker]="startTimePicker3">
147   - </mat-form-field>
148   - <mat-form-field fxFlex.xs fxFlex.sm="100px" fxFlex.md="100px">
149   - <mat-label translate>device-profile.schedule-time-to</mat-label>
150   - <mat-datetimepicker-toggle [for]="endTimePicker3" matPrefix></mat-datetimepicker-toggle>
151   - <mat-datetimepicker #endTimePicker3 type="time" openOnFocus="true"></mat-datetimepicker>
152   - <input required matInput formControlName="endsOn" [matDatetimepicker]="endTimePicker3">
153   - </mat-form-field>
154   - </div>
155   - <div fxFlex fxLayoutAlign="center center"
156   - style="text-align: center"
157   - [innerHTML]="getSchedulerRangeText(itemsSchedulerForm.at(2))">
158   - </div>
159   - </div>
160   - <div fxLayout.xs="column" fxLayout="row" fxLayoutGap="8px" formGroupName="3" fxLayoutAlign="start center" fxLayoutAlign.xs="center start">
161   - <mat-checkbox formControlName="enabled" fxFlex="17" (change)="changeCustomScheduler($event, 3)">
162   - {{ 'device-profile.schedule-day.thursday' | translate }}
163   - </mat-checkbox>
164   - <div fxLayout="row" fxLayoutGap="8px" fxFlex>
165   - <mat-form-field fxFlex.xs fxFlex.sm="100px" fxFlex.md="100px">
166   - <mat-label translate>device-profile.schedule-time-from</mat-label>
167   - <mat-datetimepicker-toggle [for]="startTimePicker4" matPrefix></mat-datetimepicker-toggle>
168   - <mat-datetimepicker #startTimePicker4 type="time" openOnFocus="true"></mat-datetimepicker>
169   - <input required matInput formControlName="startsOn" [matDatetimepicker]="startTimePicker4">
170   - </mat-form-field>
171   - <mat-form-field fxFlex.xs fxFlex.sm="100px" fxFlex.md="100px">
172   - <mat-label translate>device-profile.schedule-time-to</mat-label>
173   - <mat-datetimepicker-toggle [for]="endTimePicker4" matPrefix></mat-datetimepicker-toggle>
174   - <mat-datetimepicker #endTimePicker4 type="time" openOnFocus="true"></mat-datetimepicker>
175   - <input required matInput formControlName="endsOn" [matDatetimepicker]="endTimePicker4">
176   - </mat-form-field>
177   - </div>
178   - <div fxFlex fxLayoutAlign="center center"
179   - style="text-align: center"
180   - [innerHTML]="getSchedulerRangeText(itemsSchedulerForm.at(3))">
181   - </div>
182   - </div>
183   - <div fxLayout.xs="column" fxLayout="row" fxLayoutGap="8px" formGroupName="4" fxLayoutAlign="start center" fxLayoutAlign.xs="center start">
184   - <mat-checkbox formControlName="enabled" fxFlex="17" (change)="changeCustomScheduler($event, 4)">
185   - {{ 'device-profile.schedule-day.friday' | translate }}
186   - </mat-checkbox>
187   - <div fxLayout="row" fxLayoutGap="8px" fxFlex>
188   - <mat-form-field fxFlex.xs fxFlex.sm="100px" fxFlex.md="100px">
189   - <mat-label translate>device-profile.schedule-time-from</mat-label>
190   - <mat-datetimepicker-toggle [for]="startTimePicker5" matPrefix></mat-datetimepicker-toggle>
191   - <mat-datetimepicker #startTimePicker5 type="time" openOnFocus="true"></mat-datetimepicker>
192   - <input required matInput formControlName="startsOn" [matDatetimepicker]="startTimePicker5">
193   - </mat-form-field>
194   - <mat-form-field fxFlex.xs fxFlex.sm="100px" fxFlex.md="100px">
195   - <mat-label translate>device-profile.schedule-time-to</mat-label>
196   - <mat-datetimepicker-toggle [for]="endTimePicker5" matPrefix></mat-datetimepicker-toggle>
197   - <mat-datetimepicker #endTimePicker5 type="time" openOnFocus="true"></mat-datetimepicker>
198   - <input required matInput formControlName="endsOn" [matDatetimepicker]="endTimePicker5">
199   - </mat-form-field>
200   - </div>
201   - <div fxFlex fxLayoutAlign="center center"
202   - style="text-align: center"
203   - [innerHTML]="getSchedulerRangeText(itemsSchedulerForm.at(4))">
204   - </div>
205   - </div>
206   - <div fxLayout.xs="column" fxLayout="row" fxLayoutGap="8px" formGroupName="5" fxLayoutAlign="start center" fxLayoutAlign.xs="center start">
207   - <mat-checkbox formControlName="enabled" fxFlex="17" (change)="changeCustomScheduler($event, 5)">
208   - {{ 'device-profile.schedule-day.saturday' | translate }}
209   - </mat-checkbox>
210   - <div fxLayout="row" fxLayoutGap="8px" fxFlex>
211   - <mat-form-field fxFlex.xs fxFlex.sm="100px" fxFlex.md="100px">
212   - <mat-label translate>device-profile.schedule-time-from</mat-label>
213   - <mat-datetimepicker-toggle [for]="startTimePicker6" matPrefix></mat-datetimepicker-toggle>
214   - <mat-datetimepicker #startTimePicker6 type="time" openOnFocus="true"></mat-datetimepicker>
215   - <input required matInput formControlName="startsOn" [matDatetimepicker]="startTimePicker6">
216   - </mat-form-field>
217   - <mat-form-field fxFlex.xs fxFlex.sm="100px" fxFlex.md="100px">
218   - <mat-label translate>device-profile.schedule-time-to</mat-label>
219   - <mat-datetimepicker-toggle [for]="endTimePicker6" matPrefix></mat-datetimepicker-toggle>
220   - <mat-datetimepicker #endTimePicker6 type="time" openOnFocus="true"></mat-datetimepicker>
221   - <input required matInput formControlName="endsOn" [matDatetimepicker]="endTimePicker6">
222   - </mat-form-field>
223   - </div>
224   - <div fxFlex fxLayoutAlign="center center"
225   - style="text-align: center"
226   - [innerHTML]="getSchedulerRangeText(itemsSchedulerForm.at(5))">
227   - </div>
228   - </div>
229   - <div fxLayout.xs="column" fxLayout="row" fxLayoutGap="8px" formGroupName="6" fxLayoutAlign="start center" fxLayoutAlign.xs="center start">
230   - <mat-checkbox formControlName="enabled" fxFlex="17" (change)="changeCustomScheduler($event, 6)">
231   - {{ 'device-profile.schedule-day.sunday' | translate }}
  77 +
  78 + <div *ngFor="let day of allDays" fxLayout="column" formArrayName="items" fxLayoutGap="1em">
  79 + <div fxLayout.xs="column" fxLayout="row" fxLayoutGap="8px" [formGroupName]="''+day" fxLayoutAlign="start center" fxLayoutAlign.xs="center start">
  80 + <mat-checkbox formControlName="enabled" fxFlex="17" (change)="changeCustomScheduler($event, day)">
  81 + {{ dayOfWeekTranslationsArray[day] | translate }}
232 82 </mat-checkbox>
233 83 <div fxLayout="row" fxLayoutGap="8px" fxFlex>
234 84 <mat-form-field fxFlex.xs fxFlex.sm="100px" fxFlex.md="100px">
235 85 <mat-label translate>device-profile.schedule-time-from</mat-label>
236   - <mat-datetimepicker-toggle [for]="startTimePicker7" matPrefix></mat-datetimepicker-toggle>
237   - <mat-datetimepicker #startTimePicker7 type="time" openOnFocus="true"></mat-datetimepicker>
238   - <input required matInput formControlName="startsOn" [matDatetimepicker]="startTimePicker7">
  86 + <mat-datetimepicker-toggle [for]="startTimePicker" matPrefix></mat-datetimepicker-toggle>
  87 + <mat-datetimepicker #startTimePicker type="time" openOnFocus="true"></mat-datetimepicker>
  88 + <input required matInput formControlName="startsOn" [matDatetimepicker]="startTimePicker">
239 89 </mat-form-field>
240 90 <mat-form-field fxFlex.xs fxFlex.sm="100px" fxFlex.md="100px">
241 91 <mat-label translate>device-profile.schedule-time-to</mat-label>
242   - <mat-datetimepicker-toggle [for]="endTimePicker7" matPrefix></mat-datetimepicker-toggle>
243   - <mat-datetimepicker #endTimePicker7 type="time" openOnFocus="true"></mat-datetimepicker>
244   - <input required matInput formControlName="endsOn" [matDatetimepicker]="endTimePicker7">
  92 + <mat-datetimepicker-toggle [for]="endTimePicker" matPrefix></mat-datetimepicker-toggle>
  93 + <mat-datetimepicker #endTimePicker type="time" openOnFocus="true"></mat-datetimepicker>
  94 + <input required matInput formControlName="endsOn" [matDatetimepicker]="endTimePicker">
245 95 </mat-form-field>
246 96 </div>
247 97 <div fxFlex fxLayoutAlign="center center"
248 98 style="text-align: center"
249   - [innerHTML]="getSchedulerRangeText(itemsSchedulerForm.at(6))">
  99 + [innerHTML]="getSchedulerRangeText(itemsSchedulerForm.at(day))">
250 100 </div>
251 101 </div>
252 102 </div>
  103 +
  104 + <tb-error style="display: block;" [error]="alarmScheduleForm.get('items').hasError('dayOfWeeks')
  105 + ? ('device-profile.schedule-days-of-week-required' | translate) : ''"></tb-error>
253 106 </section>
254 107 </div>
255 108 </section>
... ...
... ... @@ -28,7 +28,12 @@ import {
28 28 Validator,
29 29 Validators
30 30 } from '@angular/forms';
31   -import { AlarmSchedule, AlarmScheduleType, AlarmScheduleTypeTranslationMap } from '@shared/models/device.models';
  31 +import {
  32 + AlarmSchedule,
  33 + AlarmScheduleType,
  34 + AlarmScheduleTypeTranslationMap,
  35 + dayOfWeekTranslations, getAlarmScheduleRangeText, timeOfDayToUTCTimestamp, utcTimestampToTimeOfDay
  36 +} from '@shared/models/device.models';
32 37 import { isDefined, isDefinedAndNotNull } from '@core/utils';
33 38 import * as _moment from 'moment-timezone';
34 39 import { MatCheckboxChange } from '@angular/material/checkbox';
... ... @@ -59,11 +64,18 @@ export class AlarmScheduleComponent implements ControlValueAccessor, Validator,
59 64 alarmScheduleType = AlarmScheduleType;
60 65 alarmScheduleTypeTranslate = AlarmScheduleTypeTranslationMap;
61 66
  67 + dayOfWeekTranslationsArray = dayOfWeekTranslations;
  68 +
  69 + allDays = Array(7).fill(0).map((x, i) => i);
  70 +
  71 + firstRowDays = Array(4).fill(0).map((x, i) => i);
  72 + secondRowDays = Array(3).fill(0).map((x, i) => i + 4);
  73 +
62 74 private modelValue: AlarmSchedule;
63 75
64 76 private defaultItems = Array.from({length: 7}, (value, i) => ({
65 77 enabled: true,
66   - dayOfWeek: i
  78 + dayOfWeek: i + 1
67 79 }));
68 80
69 81 private propagateChange = (v: any) => { };
... ... @@ -75,10 +87,10 @@ export class AlarmScheduleComponent implements ControlValueAccessor, Validator,
75 87 this.alarmScheduleForm = this.fb.group({
76 88 type: [AlarmScheduleType.ANY_TIME, Validators.required],
77 89 timezone: [null, Validators.required],
78   - daysOfWeek: this.fb.array(new Array(7).fill(false)),
  90 + daysOfWeek: this.fb.array(new Array(7).fill(false), this.validateDayOfWeeks),
79 91 startsOn: [0, Validators.required],
80 92 endsOn: [0, Validators.required],
81   - items: this.fb.array(Array.from({length: 7}, (value, i) => this.defaultItemsScheduler(i)))
  93 + items: this.fb.array(Array.from({length: 7}, (value, i) => this.defaultItemsScheduler(i)), this.validateItems)
82 94 });
83 95 this.alarmScheduleForm.get('type').valueChanges.subscribe((type) => {
84 96 this.alarmScheduleForm.reset({type, items: this.defaultItems, timezone: this.defaultTimezone}, {emitEvent: false});
... ... @@ -90,6 +102,26 @@ export class AlarmScheduleComponent implements ControlValueAccessor, Validator,
90 102 });
91 103 }
92 104
  105 + validateDayOfWeeks(control: AbstractControl): ValidationErrors | null {
  106 + const dayOfWeeks: boolean[] = control.value;
  107 + if (!dayOfWeeks || !dayOfWeeks.length || !dayOfWeeks.find(v => v === true)) {
  108 + return {
  109 + dayOfWeeks: true
  110 + };
  111 + }
  112 + return null;
  113 + }
  114 +
  115 + validateItems(control: AbstractControl): ValidationErrors | null {
  116 + const items: any[] = control.value;
  117 + if (!items || !items.length || !items.find(v => v.enabled === true)) {
  118 + return {
  119 + dayOfWeeks: true
  120 + };
  121 + }
  122 + return null;
  123 + }
  124 +
93 125 registerOnChange(fn: any): void {
94 126 this.propagateChange = fn;
95 127 }
... ... @@ -123,8 +155,8 @@ export class AlarmScheduleComponent implements ControlValueAccessor, Validator,
123 155 type: this.modelValue.type,
124 156 timezone: this.modelValue.timezone,
125 157 daysOfWeek,
126   - startsOn: this.timestampToTime(this.modelValue.startsOn),
127   - endsOn: this.timestampToTime(this.modelValue.endsOn)
  158 + startsOn: utcTimestampToTimeOfDay(this.modelValue.startsOn),
  159 + endsOn: utcTimestampToTimeOfDay(this.modelValue.endsOn)
128 160 }, {emitEvent: false});
129 161 break;
130 162 case AlarmScheduleType.CUSTOM:
... ... @@ -136,8 +168,8 @@ export class AlarmScheduleComponent implements ControlValueAccessor, Validator,
136 168 this.disabledSelectedTime(item.enabled, index);
137 169 alarmDays.push({
138 170 enabled: item.enabled,
139   - startsOn: this.timestampToTime(item.startsOn),
140   - endsOn: this.timestampToTime(item.endsOn)
  171 + startsOn: utcTimestampToTimeOfDay(item.startsOn),
  172 + endsOn: utcTimestampToTimeOfDay(item.endsOn)
141 173 });
142 174 });
143 175 this.alarmScheduleForm.patchValue({
... ... @@ -202,15 +234,15 @@ export class AlarmScheduleComponent implements ControlValueAccessor, Validator,
202 234 .filter(day => !!day);
203 235 }
204 236 if (isDefined(value.startsOn) && value.startsOn !== 0) {
205   - value.startsOn = this.timeToTimestampUTC(value.startsOn);
  237 + value.startsOn = timeOfDayToUTCTimestamp(value.startsOn);
206 238 }
207 239 if (isDefined(value.endsOn) && value.endsOn !== 0) {
208   - value.endsOn = this.timeToTimestampUTC(value.endsOn);
  240 + value.endsOn = timeOfDayToUTCTimestamp(value.endsOn);
209 241 }
210 242 if (isDefined(value.items)){
211 243 value.items = this.alarmScheduleForm.getRawValue().items;
212 244 value.items = value.items.map((item) => {
213   - return { ...item, startsOn: this.timeToTimestampUTC(item.startsOn), endsOn: this.timeToTimestampUTC(item.endsOn)};
  245 + return { ...item, startsOn: timeOfDayToUTCTimestamp(item.startsOn), endsOn: timeOfDayToUTCTimestamp(item.endsOn)};
214 246 });
215 247 }
216 248 this.modelValue = value;
... ... @@ -218,21 +250,11 @@ export class AlarmScheduleComponent implements ControlValueAccessor, Validator,
218 250 }
219 251 }
220 252
221   - private timeToTimestampUTC(date: Date | number): number {
222   - if (typeof date === 'number' || date === null) {
223   - return 0;
224   - }
225   - return _moment.utc([1970, 0, 1, date.getHours(), date.getMinutes(), date.getSeconds(), 0]).valueOf();
226   - }
227   -
228   - private timestampToTime(time = 0): Date {
229   - return new Date(time + new Date().getTimezoneOffset() * 60 * 1000);
230   - }
231 253
232 254 private defaultItemsScheduler(index): FormGroup {
233 255 return this.fb.group({
234 256 enabled: [true],
235   - dayOfWeek: [index],
  257 + dayOfWeek: [index + 1],
236 258 startsOn: [0, Validators.required],
237 259 endsOn: [0, Validators.required]
238 260 });
... ... @@ -253,23 +275,8 @@ export class AlarmScheduleComponent implements ControlValueAccessor, Validator,
253 275 }
254 276 }
255 277
256   - private timeToMoment(date: Date | number): _moment.Moment {
257   - if (typeof date === 'number' || date === null) {
258   - return _moment([1970, 0, 1, 0, 0, 0, 0]);
259   - }
260   - return _moment([1970, 0, 1, date.getHours(), date.getMinutes(), 0, 0]);
261   - }
262   -
263 278 getSchedulerRangeText(control: FormGroup | AbstractControl): string {
264   - const start = this.timeToMoment(control.get('startsOn').value);
265   - const end = this.timeToMoment(control.get('endsOn').value);
266   - if (start < end) {
267   - return `<span><span class="nowrap">${start.format('hh:mm A')}</span> – <span class="nowrap">${end.format('hh:mm A')}</span></span>`;
268   - } else if (start.valueOf() === 0 && end.valueOf() === 0 || start.isSame(_moment([1970, 0])) && end.isSame(_moment([1970, 0]))) {
269   - return '<span><span class="nowrap">12:00 AM</span> – <span class="nowrap">12:00 PM</span></span>';
270   - }
271   - return `<span><span class="nowrap">12:00 AM</span> – <span class="nowrap">${end.format('hh:mm A')}</span>` +
272   - ` and <span class="nowrap">${start.format('hh:mm A')}</span> – <span class="nowrap">12:00 PM</span></span>`;
  279 + return getAlarmScheduleRangeText(control.get('startsOn').value, control.get('endsOn').value);
273 280 }
274 281
275 282 get itemsSchedulerForm(): FormArray {
... ...
... ... @@ -19,6 +19,7 @@
19 19 border: 2px groove rgba(0, 0, 0, .45);
20 20 border-radius: 4px;
21 21 padding: 8px;
  22 + min-width: 0;
22 23 }
23 24 }
24 25
... ...
... ... @@ -56,7 +56,7 @@
56 56 <mat-checkbox formControlName="propagate" style="display: block; padding-bottom: 16px;">
57 57 {{ 'device-profile.propagate-alarm' | translate }}
58 58 </mat-checkbox>
59   - <section *ngIf="alarmFormGroup.get('propagate').value === true">
  59 + <section *ngIf="alarmFormGroup.get('propagate').value === true" style="padding-bottom: 1em;">
60 60 <mat-form-field floatLabel="always" class="mat-block">
61 61 <mat-label translate>device-profile.alarm-rule-relation-types-list</mat-label>
62 62 <mat-chip-list #relationTypesChipList [disabled]="disabled">
... ...
... ... @@ -57,6 +57,7 @@ export class DeviceProfileAlarmComponent implements ControlValueAccessor, OnInit
57 57
58 58 separatorKeysCodes = [ENTER, COMMA, SEMICOLON];
59 59
  60 + @Input()
60 61 expanded = false;
61 62
62 63 private modelValue: DeviceProfileAlarm;
... ...
... ... @@ -21,6 +21,7 @@
21 21 let $index = index; last as isLast;"
22 22 fxLayout="column" [ngStyle]="!isLast ? {paddingBottom: '8px'} : {}">
23 23 <tb-device-profile-alarm [formControl]="alarmControl"
  24 + [expanded]="$index === 0"
24 25 (removeAlarm)="removeAlarm($index)">
25 26 </tb-device-profile-alarm>
26 27 </div>
... ... @@ -29,7 +30,7 @@
29 30 <span translate fxLayoutAlign="center center"
30 31 class="tb-prompt">device-profile.no-alarm-rules</span>
31 32 </div>
32   - <div *ngIf="!disabled" fxFlex fxLayout="row" fxLayoutAlign="end center"
  33 + <div *ngIf="!disabled" fxFlex fxLayout="row" fxLayoutAlign="start center"
33 34 style="padding-top: 16px;">
34 35 <button mat-raised-button color="primary"
35 36 type="button"
... ...
  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 +<form [formGroup]="editDetailsFormGroup" (ngSubmit)="save()" style="width: 800px;">
  19 + <mat-toolbar color="primary">
  20 + <h2>{{ 'device-profile.alarm-rule-details' | translate }}</h2>
  21 + <span fxFlex></span>
  22 + <button mat-icon-button
  23 + (click)="cancel()"
  24 + type="button">
  25 + <mat-icon class="material-icons">close</mat-icon>
  26 + </button>
  27 + </mat-toolbar>
  28 + <mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
  29 + </mat-progress-bar>
  30 + <div mat-dialog-content>
  31 + <fieldset [disabled]="isLoading$ | async">
  32 + <div fxFlex fxLayout="column">
  33 + <mat-form-field class="mat-block">
  34 + <mat-label translate>device-profile.alarm-details</mat-label>
  35 + <textarea matInput formControlName="alarmDetails" rows="5"></textarea>
  36 + </mat-form-field>
  37 + </div>
  38 + </fieldset>
  39 + </div>
  40 + <div mat-dialog-actions fxLayoutAlign="end center">
  41 + <button mat-raised-button color="primary"
  42 + type="submit"
  43 + [disabled]="(isLoading$ | async) || editDetailsFormGroup.invalid || !editDetailsFormGroup.dirty">
  44 + {{ 'action.save' | translate }}
  45 + </button>
  46 + <button mat-button color="primary"
  47 + type="button"
  48 + [disabled]="(isLoading$ | async)"
  49 + (click)="cancel()" cdkFocusInitial>
  50 + {{ 'action.cancel' | translate }}
  51 + </button>
  52 + </div>
  53 +</form>
... ...
  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, Inject, OnInit, SkipSelf } from '@angular/core';
  18 +import { ErrorStateMatcher } from '@angular/material/core';
  19 +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
  20 +import { Store } from '@ngrx/store';
  21 +import { AppState } from '@core/core.state';
  22 +import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm } from '@angular/forms';
  23 +import { Router } from '@angular/router';
  24 +import { DialogComponent } from '@app/shared/components/dialog.component';
  25 +import { UtilsService } from '@core/services/utils.service';
  26 +import { TranslateService } from '@ngx-translate/core';
  27 +
  28 +export interface EditAlarmDetailsDialogData {
  29 + alarmDetails: string;
  30 +}
  31 +
  32 +@Component({
  33 + selector: 'tb-edit-alarm-details-dialog',
  34 + templateUrl: './edit-alarm-details-dialog.component.html',
  35 + providers: [{provide: ErrorStateMatcher, useExisting: EditAlarmDetailsDialogComponent}],
  36 + styleUrls: []
  37 +})
  38 +export class EditAlarmDetailsDialogComponent extends DialogComponent<EditAlarmDetailsDialogComponent, string>
  39 + implements OnInit, ErrorStateMatcher {
  40 +
  41 + alarmDetails = this.data.alarmDetails;
  42 +
  43 + editDetailsFormGroup: FormGroup;
  44 +
  45 + submitted = false;
  46 +
  47 + constructor(protected store: Store<AppState>,
  48 + protected router: Router,
  49 + @Inject(MAT_DIALOG_DATA) public data: EditAlarmDetailsDialogData,
  50 + @SkipSelf() private errorStateMatcher: ErrorStateMatcher,
  51 + public dialogRef: MatDialogRef<EditAlarmDetailsDialogComponent, string>,
  52 + private fb: FormBuilder,
  53 + private utils: UtilsService,
  54 + public translate: TranslateService) {
  55 + super(store, router, dialogRef);
  56 +
  57 + this.editDetailsFormGroup = this.fb.group({
  58 + alarmDetails: [this.alarmDetails]
  59 + });
  60 + }
  61 +
  62 + ngOnInit(): void {
  63 + }
  64 +
  65 + isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
  66 + const originalErrorState = this.errorStateMatcher.isErrorState(control, form);
  67 + const customErrorState = !!(control && control.invalid && this.submitted);
  68 + return originalErrorState || customErrorState;
  69 + }
  70 +
  71 + cancel(): void {
  72 + this.dialogRef.close(null);
  73 + }
  74 +
  75 + save(): void {
  76 + this.submitted = true;
  77 + this.alarmDetails = this.editDetailsFormGroup.get('alarmDetails').value;
  78 + this.dialogRef.close(this.alarmDetails);
  79 + }
  80 +}
... ...
... ... @@ -61,29 +61,7 @@
61 61 </div>
62 62 </div>
63 63 </mat-tab>
64   -<mat-tab *ngIf="entity && !isEdit"
65   - label="{{ 'attribute.attributes' | translate }}" #attributesTab="matTab">
66   - <tb-attribute-table [defaultAttributeScope]="attributeScopes.SERVER_SCOPE"
67   - [active]="attributesTab.isActive"
68   - [entityId]="entity.id"
69   - [entityName]="entity.name">
70   - </tb-attribute-table>
71   -</mat-tab>
72   -<mat-tab *ngIf="entity && !isEdit"
73   - label="{{ 'attribute.latest-telemetry' | translate }}" #telemetryTab="matTab">
74   - <tb-attribute-table [defaultAttributeScope]="latestTelemetryTypes.LATEST_TELEMETRY"
75   - disableAttributeScopeSelection
76   - [active]="telemetryTab.isActive"
77   - [entityId]="entity.id"
78   - [entityName]="entity.name">
79   - </tb-attribute-table>
80   -</mat-tab>
81   -<mat-tab *ngIf="entity && !isEdit"
82   - label="{{ 'alarm.alarms' | translate }}" #alarmsTab="matTab">
83   - <tb-alarm-table [active]="alarmsTab.isActive" [entityId]="entity.id"></tb-alarm-table>
84   -</mat-tab>
85   -<mat-tab *ngIf="entity && !isEdit"
86   - label="{{ 'tenant.events' | translate }}" #eventsTab="matTab">
87   - <tb-event-table [defaultEventType]="eventTypes.ERROR" [active]="eventsTab.isActive" [tenantId]="nullUid"
88   - [entityId]="entity.id"></tb-event-table>
  64 +<mat-tab *ngIf="entity"
  65 + label="{{ 'audit-log.audit-logs' | translate }}" #auditLogsTab="matTab">
  66 + <tb-audit-log-table detailsMode="true" [active]="auditLogsTab.isActive" [auditLogMode]="auditLogModes.ENTITY" [entityId]="entity.id"></tb-audit-log-table>
89 67 </mat-tab>
... ...
... ... @@ -25,6 +25,8 @@ import { RuleChainId } from '@shared/models/id/rule-chain-id';
25 25 import { EntityInfoData } from '@shared/models/entity.models';
26 26 import { KeyFilter } from '@shared/models/query/query.models';
27 27 import { TimeUnit } from '@shared/models/time/time.models';
  28 +import * as _moment from 'moment-timezone';
  29 +import { AbstractControl, FormGroup } from '@angular/forms';
28 30
29 31 export enum DeviceProfileType {
30 32 DEFAULT = 'DEFAULT'
... ... @@ -408,3 +410,62 @@ export interface ClaimResult {
408 410 device: Device;
409 411 response: ClaimResponse;
410 412 }
  413 +
  414 +export const dayOfWeekTranslations = new Array<string>(
  415 + 'device-profile.schedule-day.monday',
  416 + 'device-profile.schedule-day.tuesday',
  417 + 'device-profile.schedule-day.wednesday',
  418 + 'device-profile.schedule-day.thursday',
  419 + 'device-profile.schedule-day.friday',
  420 + 'device-profile.schedule-day.saturday',
  421 + 'device-profile.schedule-day.sunday'
  422 +);
  423 +
  424 +export function getDayString(day: number): string {
  425 + switch (day) {
  426 + case 0:
  427 + return 'device-profile.schedule-day.monday';
  428 + case 1:
  429 + return this.translate.instant('device-profile.schedule-day.tuesday');
  430 + case 2:
  431 + return this.translate.instant('device-profile.schedule-day.wednesday');
  432 + case 3:
  433 + return this.translate.instant('device-profile.schedule-day.thursday');
  434 + case 4:
  435 + return this.translate.instant('device-profile.schedule-day.friday');
  436 + case 5:
  437 + return this.translate.instant('device-profile.schedule-day.saturday');
  438 + case 6:
  439 + return this.translate.instant('device-profile.schedule-day.sunday');
  440 + }
  441 +}
  442 +
  443 +export function timeOfDayToUTCTimestamp(date: Date | number): number {
  444 + if (typeof date === 'number' || date === null) {
  445 + return 0;
  446 + }
  447 + return _moment.utc([1970, 0, 1, date.getHours(), date.getMinutes(), date.getSeconds(), 0]).valueOf();
  448 +}
  449 +
  450 +export function utcTimestampToTimeOfDay(time = 0): Date {
  451 + return new Date(time + new Date().getTimezoneOffset() * 60 * 1000);
  452 +}
  453 +
  454 +function timeOfDayToMoment(date: Date | number): _moment.Moment {
  455 + if (typeof date === 'number' || date === null) {
  456 + return _moment([1970, 0, 1, 0, 0, 0, 0]);
  457 + }
  458 + return _moment([1970, 0, 1, date.getHours(), date.getMinutes(), 0, 0]);
  459 +}
  460 +
  461 +export function getAlarmScheduleRangeText(startsOn: Date | number, endsOn: Date | number): string {
  462 + const start = timeOfDayToMoment(startsOn);
  463 + const end = timeOfDayToMoment(endsOn);
  464 + if (start < end) {
  465 + return `<span><span class="nowrap">${start.format('hh:mm A')}</span> – <span class="nowrap">${end.format('hh:mm A')}</span></span>`;
  466 + } else if (start.valueOf() === 0 && end.valueOf() === 0 || start.isSame(_moment([1970, 0])) && end.isSame(_moment([1970, 0]))) {
  467 + return '<span><span class="nowrap">12:00 AM</span> – <span class="nowrap">12:00 PM</span></span>';
  468 + }
  469 + return `<span><span class="nowrap">12:00 AM</span> – <span class="nowrap">${end.format('hh:mm A')}</span>` +
  470 + ` and <span class="nowrap">${start.format('hh:mm A')}</span> – <span class="nowrap">12:00 PM</span></span>`;
  471 +}
... ...
... ... @@ -476,21 +476,23 @@ export function keyFilterInfosToKeyFilters(keyFilterInfos: Array<KeyFilterInfo>)
476 476 export function keyFiltersToKeyFilterInfos(keyFilters: Array<KeyFilter>): Array<KeyFilterInfo> {
477 477 const keyFilterInfos: Array<KeyFilterInfo> = [];
478 478 const keyFilterInfoMap: {[infoKey: string]: KeyFilterInfo} = {};
479   - for (const keyFilter of keyFilters) {
480   - const key = keyFilter.key;
481   - const infoKey = key.key + key.type + keyFilter.valueType;
482   - let keyFilterInfo = keyFilterInfoMap[infoKey];
483   - if (!keyFilterInfo) {
484   - keyFilterInfo = {
485   - key,
486   - valueType: keyFilter.valueType,
487   - predicates: []
488   - };
489   - keyFilterInfoMap[infoKey] = keyFilterInfo;
490   - keyFilterInfos.push(keyFilterInfo);
491   - }
492   - if (keyFilter.predicate) {
493   - keyFilterInfo.predicates.push(keyFilterPredicateToKeyFilterPredicateInfo(keyFilter.predicate));
  479 + if (keyFilters) {
  480 + for (const keyFilter of keyFilters) {
  481 + const key = keyFilter.key;
  482 + const infoKey = key.key + key.type + keyFilter.valueType;
  483 + let keyFilterInfo = keyFilterInfoMap[infoKey];
  484 + if (!keyFilterInfo) {
  485 + keyFilterInfo = {
  486 + key,
  487 + valueType: keyFilter.valueType,
  488 + predicates: []
  489 + };
  490 + keyFilterInfoMap[infoKey] = keyFilterInfo;
  491 + keyFilterInfos.push(keyFilterInfo);
  492 + }
  493 + if (keyFilter.predicate) {
  494 + keyFilterInfo.predicates.push(keyFilterPredicateToKeyFilterPredicateInfo(keyFilter.predicate));
  495 + }
494 496 }
495 497 }
496 498 return keyFilterInfos;
... ...
... ... @@ -55,7 +55,9 @@
55 55 "continue": "Continue",
56 56 "discard-changes": "Discard Changes",
57 57 "download": "Download",
58   - "next-with-label": "Next: {{label}}"
  58 + "next-with-label": "Next: {{label}}",
  59 + "read-more": "Read more",
  60 + "hide": "Hide"
59 61 },
60 62 "aggregation": {
61 63 "aggregation": "Aggregation",
... ... @@ -932,15 +934,18 @@
932 934 "condition-type": "Condition type",
933 935 "condition-type-simple": "Simple",
934 936 "condition-type-duration": "Duration",
  937 + "condition-during": "During <b>{{during}}</b>",
935 938 "condition-type-repeating": "Repeating",
936 939 "condition-type-required": "Condition type is required.",
937 940 "condition-repeating-value": "Count of events",
938 941 "condition-repeating-value-range": "Count of events should be in a range from 1 to 2147483647.",
939 942 "condition-repeating-value-pattern": "Count of events should be integers.",
940 943 "condition-repeating-value-required": "Count of events is required.",
  944 + "condition-repeat-times": "Repeats <b>{ count, plural, 1 {1 time} other {# times} }</b>",
941 945 "schedule-type": "Scheduler type",
942 946 "schedule-type-required": "Scheduler type is required.",
943 947 "schedule": "Schedule",
  948 + "edit-schedule": "Edit alarm schedule",
944 949 "schedule-any-time": "Active all the time",
945 950 "schedule-specific-time": "Active at a specific time",
946 951 "schedule-custom": "Custom",
... ... @@ -956,7 +961,8 @@
956 961 "schedule-days": "Days",
957 962 "schedule-time": "Time",
958 963 "schedule-time-from": "From",
959   - "schedule-time-to": "To"
  964 + "schedule-time-to": "To",
  965 + "schedule-days-of-week-required": "At least one day of week should be selected."
960 966 },
961 967 "dialog": {
962 968 "close": "Close dialog"
... ...