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,13 +102,16 @@ import { DeviceProfileAlarmComponent } from './profile/alarm/device-profile-alar
102 import { CreateAlarmRulesComponent } from './profile/alarm/create-alarm-rules.component'; 102 import { CreateAlarmRulesComponent } from './profile/alarm/create-alarm-rules.component';
103 import { AlarmRuleComponent } from './profile/alarm/alarm-rule.component'; 103 import { AlarmRuleComponent } from './profile/alarm/alarm-rule.component';
104 import { AlarmRuleConditionComponent } from './profile/alarm/alarm-rule-condition.component'; 104 import { AlarmRuleConditionComponent } from './profile/alarm/alarm-rule-condition.component';
105 -import { AlarmRuleKeyFiltersDialogComponent } from './profile/alarm/alarm-rule-key-filters-dialog.component';  
106 import { FilterTextComponent } from './filter/filter-text.component'; 105 import { FilterTextComponent } from './filter/filter-text.component';
107 import { AddDeviceProfileDialogComponent } from './profile/add-device-profile-dialog.component'; 106 import { AddDeviceProfileDialogComponent } from './profile/add-device-profile-dialog.component';
108 import { RuleChainAutocompleteComponent } from './rule-chain/rule-chain-autocomplete.component'; 107 import { RuleChainAutocompleteComponent } from './rule-chain/rule-chain-autocomplete.component';
109 import { AlarmScheduleComponent } from './profile/alarm/alarm-schedule.component'; 108 import { AlarmScheduleComponent } from './profile/alarm/alarm-schedule.component';
110 import { DeviceWizardDialogComponent } from './wizard/device-wizard-dialog.component'; 109 import { DeviceWizardDialogComponent } from './wizard/device-wizard-dialog.component';
111 import { DeviceCredentialsComponent } from './device/device-credentials.component'; 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 @NgModule({ 116 @NgModule({
114 declarations: 117 declarations:
@@ -190,7 +193,7 @@ import { DeviceCredentialsComponent } from './device/device-credentials.componen @@ -190,7 +193,7 @@ import { DeviceCredentialsComponent } from './device/device-credentials.componen
190 DeviceProfileTransportConfigurationComponent, 193 DeviceProfileTransportConfigurationComponent,
191 CreateAlarmRulesComponent, 194 CreateAlarmRulesComponent,
192 AlarmRuleComponent, 195 AlarmRuleComponent,
193 - AlarmRuleKeyFiltersDialogComponent, 196 + AlarmRuleConditionDialogComponent,
194 AlarmRuleConditionComponent, 197 AlarmRuleConditionComponent,
195 DeviceProfileAlarmComponent, 198 DeviceProfileAlarmComponent,
196 DeviceProfileAlarmsComponent, 199 DeviceProfileAlarmsComponent,
@@ -198,9 +201,12 @@ import { DeviceCredentialsComponent } from './device/device-credentials.componen @@ -198,9 +201,12 @@ import { DeviceCredentialsComponent } from './device/device-credentials.componen
198 DeviceProfileDialogComponent, 201 DeviceProfileDialogComponent,
199 AddDeviceProfileDialogComponent, 202 AddDeviceProfileDialogComponent,
200 RuleChainAutocompleteComponent, 203 RuleChainAutocompleteComponent,
  204 + AlarmScheduleInfoComponent,
201 AlarmScheduleComponent, 205 AlarmScheduleComponent,
202 DeviceWizardDialogComponent, 206 DeviceWizardDialogComponent,
203 - DeviceCredentialsComponent 207 + DeviceCredentialsComponent,
  208 + AlarmScheduleDialogComponent,
  209 + EditAlarmDetailsDialogComponent
204 ], 210 ],
205 imports: [ 211 imports: [
206 CommonModule, 212 CommonModule,
@@ -271,7 +277,7 @@ import { DeviceCredentialsComponent } from './device/device-credentials.componen @@ -271,7 +277,7 @@ import { DeviceCredentialsComponent } from './device/device-credentials.componen
271 DeviceProfileTransportConfigurationComponent, 277 DeviceProfileTransportConfigurationComponent,
272 CreateAlarmRulesComponent, 278 CreateAlarmRulesComponent,
273 AlarmRuleComponent, 279 AlarmRuleComponent,
274 - AlarmRuleKeyFiltersDialogComponent, 280 + AlarmRuleConditionDialogComponent,
275 AlarmRuleConditionComponent, 281 AlarmRuleConditionComponent,
276 DeviceProfileAlarmComponent, 282 DeviceProfileAlarmComponent,
277 DeviceProfileAlarmsComponent, 283 DeviceProfileAlarmsComponent,
@@ -281,7 +287,10 @@ import { DeviceCredentialsComponent } from './device/device-credentials.componen @@ -281,7 +287,10 @@ import { DeviceCredentialsComponent } from './device/device-credentials.componen
281 RuleChainAutocompleteComponent, 287 RuleChainAutocompleteComponent,
282 DeviceWizardDialogComponent, 288 DeviceWizardDialogComponent,
283 DeviceCredentialsComponent, 289 DeviceCredentialsComponent,
284 - AlarmScheduleComponent 290 + AlarmScheduleInfoComponent,
  291 + AlarmScheduleComponent,
  292 + AlarmScheduleDialogComponent,
  293 + EditAlarmDetailsDialogComponent
285 ], 294 ],
286 providers: [ 295 providers: [
287 WidgetComponentService, 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,8 +15,9 @@
15 limitations under the License. 15 limitations under the License.
16 16
17 --> 17 -->
18 -<div fxLayout="column" fxFlex> 18 +<div fxLayout="column" fxFlex [formGroup]="alarmRuleConditionFormGroup">
19 <div fxLayout="row" fxLayoutAlign="start center" style="min-height: 40px;"> 19 <div fxLayout="row" fxLayoutAlign="start center" style="min-height: 40px;">
  20 + <label class="tb-title" translate>device-profile.condition</label>
20 <span fxFlex></span> 21 <span fxFlex></span>
21 <a mat-button color="primary" 22 <a mat-button color="primary"
22 type="button" 23 type="button"
@@ -27,9 +28,12 @@ @@ -27,9 +28,12 @@
27 </a> 28 </a>
28 </div> 29 </div>
29 <div class="tb-alarm-rule-condition" fxFlex fxLayout="column" fxLayoutAlign="center" (click)="openFilterDialog($event)"> 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 required 32 required
32 addFilterPrompt="{{'device-profile.enter-alarm-rule-condition-prompt' | translate}}"> 33 addFilterPrompt="{{'device-profile.enter-alarm-rule-condition-prompt' | translate}}">
33 </tb-filter-text> 34 </tb-filter-text>
  35 + <span class="tb-alarm-rule-condition-spec" [ngClass]="{disabled: this.disabled}" [innerHTML]="specText">
  36 + </span>
34 </div> 37 </div>
  38 +
35 </div> 39 </div>
@@ -21,10 +21,15 @@ @@ -21,10 +21,15 @@
21 } 21 }
22 } 22 }
23 .tb-alarm-rule-condition { 23 .tb-alarm-rule-condition {
24 - padding: 8px;  
25 - border: 1px groove rgba(0, 0, 0, .25);  
26 - border-radius: 4px;  
27 cursor: pointer; 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,20 +18,21 @@ import { Component, forwardRef, Input, OnInit } from '@angular/core';
18 import { 18 import {
19 ControlValueAccessor, 19 ControlValueAccessor,
20 FormBuilder, 20 FormBuilder,
21 - FormControl, 21 + FormControl, FormGroup,
22 NG_VALIDATORS, 22 NG_VALIDATORS,
23 NG_VALUE_ACCESSOR, 23 NG_VALUE_ACCESSOR,
24 - Validator 24 + Validator, Validators
25 } from '@angular/forms'; 25 } from '@angular/forms';
26 import { MatDialog } from '@angular/material/dialog'; 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 import { TranslateService } from '@ngx-translate/core'; 28 import { TranslateService } from '@ngx-translate/core';
34 import { DatePipe } from '@angular/common'; 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 @Component({ 37 @Component({
37 selector: 'tb-alarm-rule-condition', 38 selector: 'tb-alarm-rule-condition',
@@ -55,9 +56,11 @@ export class AlarmRuleConditionComponent implements ControlValueAccessor, OnInit @@ -55,9 +56,11 @@ export class AlarmRuleConditionComponent implements ControlValueAccessor, OnInit
55 @Input() 56 @Input()
56 disabled: boolean; 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 private propagateChange = (v: any) => { }; 65 private propagateChange = (v: any) => { };
63 66
@@ -75,25 +78,31 @@ export class AlarmRuleConditionComponent implements ControlValueAccessor, OnInit @@ -75,25 +78,31 @@ export class AlarmRuleConditionComponent implements ControlValueAccessor, OnInit
75 } 78 }
76 79
77 ngOnInit() { 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 setDisabledState(isDisabled: boolean): void { 87 setDisabledState(isDisabled: boolean): void {
82 this.disabled = isDisabled; 88 this.disabled = isDisabled;
83 if (this.disabled) { 89 if (this.disabled) {
84 - this.alarmRuleConditionControl.disable({emitEvent: false}); 90 + this.alarmRuleConditionFormGroup.disable({emitEvent: false});
85 } else { 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 this.modelValue = value; 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 this.updateConditionInfo(); 101 this.updateConditionInfo();
93 } 102 }
94 103
95 public conditionSet() { 104 public conditionSet() {
96 - return this.modelValue && this.modelValue.length; 105 + return this.modelValue && this.modelValue.condition.length;
97 } 106 }
98 107
99 public validate(c: FormControl) { 108 public validate(c: FormControl) {
@@ -108,13 +117,13 @@ export class AlarmRuleConditionComponent implements ControlValueAccessor, OnInit @@ -108,13 +117,13 @@ export class AlarmRuleConditionComponent implements ControlValueAccessor, OnInit
108 if ($event) { 117 if ($event) {
109 $event.stopPropagation(); 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 disableClose: true, 122 disableClose: true,
114 panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], 123 panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
115 data: { 124 data: {
116 readonly: this.disabled, 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 }).afterClosed().subscribe((result) => { 128 }).afterClosed().subscribe((result) => {
120 if (result) { 129 if (result) {
@@ -125,7 +134,45 @@ export class AlarmRuleConditionComponent implements ControlValueAccessor, OnInit @@ -125,7 +134,45 @@ export class AlarmRuleConditionComponent implements ControlValueAccessor, OnInit
125 } 134 }
126 135
127 private updateConditionInfo() { 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 private updateModel() { 178 private updateModel() {
@@ -16,90 +16,36 @@ @@ -16,90 +16,36 @@
16 16
17 --> 17 -->
18 <div fxLayout="column" [formGroup]="alarmRuleFormGroup"> 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 </div> 51 </div>
@@ -17,5 +17,29 @@ @@ -17,5 +17,29 @@
17 .row { 17 .row {
18 margin-top: 1em; 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,11 +25,14 @@ import {
25 Validator, 25 Validator,
26 Validators 26 Validators
27 } from '@angular/forms'; 27 } from '@angular/forms';
28 -import { AlarmConditionType, AlarmConditionTypeTranslationMap, AlarmRule } from '@shared/models/device.models'; 28 +import { AlarmRule } from '@shared/models/device.models';
29 import { MatDialog } from '@angular/material/dialog'; 29 import { MatDialog } from '@angular/material/dialog';
30 -import { TimeUnit, timeUnitTranslationMap } from '@shared/models/time/time.models';  
31 import { coerceBooleanProperty } from '@angular/cdk/coercion'; 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 @Component({ 37 @Component({
35 selector: 'tb-alarm-rule', 38 selector: 'tb-alarm-rule',
@@ -50,12 +53,6 @@ import { isUndefined } from '@core/utils'; @@ -50,12 +53,6 @@ import { isUndefined } from '@core/utils';
50 }) 53 })
51 export class AlarmRuleComponent implements ControlValueAccessor, OnInit, Validator { 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 @Input() 56 @Input()
60 disabled: boolean; 57 disabled: boolean;
61 58
@@ -72,6 +69,8 @@ export class AlarmRuleComponent implements ControlValueAccessor, OnInit, Validat @@ -72,6 +69,8 @@ export class AlarmRuleComponent implements ControlValueAccessor, OnInit, Validat
72 69
73 alarmRuleFormGroup: FormGroup; 70 alarmRuleFormGroup: FormGroup;
74 71
  72 + expandAlarmDetails = false;
  73 +
75 private propagateChange = (v: any) => { }; 74 private propagateChange = (v: any) => { };
76 75
77 constructor(private dialog: MatDialog, 76 constructor(private dialog: MatDialog,
@@ -87,21 +86,10 @@ export class AlarmRuleComponent implements ControlValueAccessor, OnInit, Validat @@ -87,21 +86,10 @@ export class AlarmRuleComponent implements ControlValueAccessor, OnInit, Validat
87 86
88 ngOnInit() { 87 ngOnInit() {
89 this.alarmRuleFormGroup = this.fb.group({ 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 schedule: [null], 90 schedule: [null],
100 alarmDetails: [null] 91 alarmDetails: [null]
101 }); 92 });
102 - this.alarmRuleFormGroup.get('condition.spec.type').valueChanges.subscribe((type) => {  
103 - this.updateValidators(type, true, true);  
104 - });  
105 this.alarmRuleFormGroup.valueChanges.subscribe(() => { 93 this.alarmRuleFormGroup.valueChanges.subscribe(() => {
106 this.updateModel(); 94 this.updateModel();
107 }); 95 });
@@ -118,11 +106,25 @@ export class AlarmRuleComponent implements ControlValueAccessor, OnInit, Validat @@ -118,11 +106,25 @@ export class AlarmRuleComponent implements ControlValueAccessor, OnInit, Validat
118 106
119 writeValue(value: AlarmRule): void { 107 writeValue(value: AlarmRule): void {
120 this.modelValue = value; 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 this.alarmRuleFormGroup.reset(this.modelValue || undefined, {emitEvent: false}); 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 public validate(c: FormControl) { 130 public validate(c: FormControl) {
@@ -133,47 +135,6 @@ export class AlarmRuleComponent implements ControlValueAccessor, OnInit, Validat @@ -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 private updateModel() { 138 private updateModel() {
178 const value = this.alarmRuleFormGroup.value; 139 const value = this.alarmRuleFormGroup.value;
179 if (this.modelValue) { 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,9 +15,9 @@
15 limitations under the License. 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 <mat-toolbar color="primary"> 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 <span fxFlex></span> 21 <span fxFlex></span>
22 <button mat-icon-button 22 <button mat-icon-button
23 (click)="cancel()" 23 (click)="cancel()"
@@ -30,12 +30,9 @@ @@ -30,12 +30,9 @@
30 <div mat-dialog-content> 30 <div mat-dialog-content>
31 <fieldset [disabled]="isLoading$ | async"> 31 <fieldset [disabled]="isLoading$ | async">
32 <div fxFlex fxLayout="column"> 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 </div> 36 </div>
40 </fieldset> 37 </fieldset>
41 </div> 38 </div>
@@ -43,7 +40,7 @@ @@ -43,7 +40,7 @@
43 <button mat-raised-button color="primary" 40 <button mat-raised-button color="primary"
44 *ngIf="!readonly" 41 *ngIf="!readonly"
45 type="submit" 42 type="submit"
46 - [disabled]="(isLoading$ | async) || keyFiltersFormGroup.invalid || !keyFiltersFormGroup.dirty"> 43 + [disabled]="(isLoading$ | async) || alarmScheduleFormGroup.invalid || !alarmScheduleFormGroup.dirty">
47 {{ 'action.save' | translate }} 44 {{ 'action.save' | translate }}
48 </button> 45 </button>
49 <button mat-button color="primary" 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,49 +19,49 @@ import { ErrorStateMatcher } from '@angular/material/core';
19 import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; 19 import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
20 import { Store } from '@ngrx/store'; 20 import { Store } from '@ngrx/store';
21 import { AppState } from '@core/core.state'; 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 import { Router } from '@angular/router'; 23 import { Router } from '@angular/router';
24 import { DialogComponent } from '@app/shared/components/dialog.component'; 24 import { DialogComponent } from '@app/shared/components/dialog.component';
25 import { UtilsService } from '@core/services/utils.service'; 25 import { UtilsService } from '@core/services/utils.service';
26 import { TranslateService } from '@ngx-translate/core'; 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 readonly: boolean; 30 readonly: boolean;
31 - keyFilters: Array<KeyFilter>; 31 + alarmSchedule: AlarmSchedule;
32 } 32 }
33 33
34 @Component({ 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 styleUrls: [] 38 styleUrls: []
39 }) 39 })
40 -export class AlarmRuleKeyFiltersDialogComponent extends DialogComponent<AlarmRuleKeyFiltersDialogComponent, Array<KeyFilter>> 40 +export class AlarmScheduleDialogComponent extends DialogComponent<AlarmScheduleDialogComponent, AlarmSchedule>
41 implements OnInit, ErrorStateMatcher { 41 implements OnInit, ErrorStateMatcher {
42 42
43 readonly = this.data.readonly; 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 submitted = false; 48 submitted = false;
49 49
50 constructor(protected store: Store<AppState>, 50 constructor(protected store: Store<AppState>,
51 protected router: Router, 51 protected router: Router,
52 - @Inject(MAT_DIALOG_DATA) public data: AlarmRuleKeyFiltersDialogData, 52 + @Inject(MAT_DIALOG_DATA) public data: AlarmScheduleDialogData,
53 @SkipSelf() private errorStateMatcher: ErrorStateMatcher, 53 @SkipSelf() private errorStateMatcher: ErrorStateMatcher,
54 - public dialogRef: MatDialogRef<AlarmRuleKeyFiltersDialogComponent, Array<KeyFilter>>, 54 + public dialogRef: MatDialogRef<AlarmScheduleDialogComponent, AlarmSchedule>,
55 private fb: FormBuilder, 55 private fb: FormBuilder,
56 private utils: UtilsService, 56 private utils: UtilsService,
57 public translate: TranslateService) { 57 public translate: TranslateService) {
58 super(store, router, dialogRef); 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 if (this.readonly) { 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,7 +80,7 @@ export class AlarmRuleKeyFiltersDialogComponent extends DialogComponent<AlarmRul
80 80
81 save(): void { 81 save(): void {
82 this.submitted = true; 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,33 +35,20 @@
35 </tb-timezone-select> 35 </tb-timezone-select>
36 <section *ngIf="alarmScheduleForm.get('type').value === alarmScheduleType.SPECIFIC_TIME"> 36 <section *ngIf="alarmScheduleForm.get('type').value === alarmScheduleType.SPECIFIC_TIME">
37 <div class="tb-small" style="margin-bottom: 0.5em" translate>device-profile.schedule-days</div> 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 <div fxLayout="row" fxLayoutGap="16px"> 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 </mat-checkbox> 42 </mat-checkbox>
52 </div> 43 </div>
53 <div fxLayout="row" fxLayoutGap="16px"> 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 </mat-checkbox> 47 </mat-checkbox>
63 </div> 48 </div>
64 </div> 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 <div class="tb-small" style="margin-bottom: 0.5em" translate>device-profile.schedule-time</div> 52 <div class="tb-small" style="margin-bottom: 0.5em" translate>device-profile.schedule-time</div>
66 <div fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px"> 53 <div fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px">
67 <div fxLayout="row" fxLayoutGap="8px" fxFlex.gt-md> 54 <div fxLayout="row" fxLayoutGap="8px" fxFlex.gt-md>
@@ -87,169 +74,35 @@ @@ -87,169 +74,35 @@
87 </section> 74 </section>
88 <section *ngIf="alarmScheduleForm.get('type').value === alarmScheduleType.CUSTOM"> 75 <section *ngIf="alarmScheduleForm.get('type').value === alarmScheduleType.CUSTOM">
89 <div class="tb-small" style="margin-bottom: 0.5em" translate>device-profile.schedule-days</div> 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 </mat-checkbox> 82 </mat-checkbox>
233 <div fxLayout="row" fxLayoutGap="8px" fxFlex> 83 <div fxLayout="row" fxLayoutGap="8px" fxFlex>
234 <mat-form-field fxFlex.xs fxFlex.sm="100px" fxFlex.md="100px"> 84 <mat-form-field fxFlex.xs fxFlex.sm="100px" fxFlex.md="100px">
235 <mat-label translate>device-profile.schedule-time-from</mat-label> 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 </mat-form-field> 89 </mat-form-field>
240 <mat-form-field fxFlex.xs fxFlex.sm="100px" fxFlex.md="100px"> 90 <mat-form-field fxFlex.xs fxFlex.sm="100px" fxFlex.md="100px">
241 <mat-label translate>device-profile.schedule-time-to</mat-label> 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 </mat-form-field> 95 </mat-form-field>
246 </div> 96 </div>
247 <div fxFlex fxLayoutAlign="center center" 97 <div fxFlex fxLayoutAlign="center center"
248 style="text-align: center" 98 style="text-align: center"
249 - [innerHTML]="getSchedulerRangeText(itemsSchedulerForm.at(6))"> 99 + [innerHTML]="getSchedulerRangeText(itemsSchedulerForm.at(day))">
250 </div> 100 </div>
251 </div> 101 </div>
252 </div> 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 </section> 106 </section>
254 </div> 107 </div>
255 </section> 108 </section>
@@ -28,7 +28,12 @@ import { @@ -28,7 +28,12 @@ import {
28 Validator, 28 Validator,
29 Validators 29 Validators
30 } from '@angular/forms'; 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 import { isDefined, isDefinedAndNotNull } from '@core/utils'; 37 import { isDefined, isDefinedAndNotNull } from '@core/utils';
33 import * as _moment from 'moment-timezone'; 38 import * as _moment from 'moment-timezone';
34 import { MatCheckboxChange } from '@angular/material/checkbox'; 39 import { MatCheckboxChange } from '@angular/material/checkbox';
@@ -59,11 +64,18 @@ export class AlarmScheduleComponent implements ControlValueAccessor, Validator, @@ -59,11 +64,18 @@ export class AlarmScheduleComponent implements ControlValueAccessor, Validator,
59 alarmScheduleType = AlarmScheduleType; 64 alarmScheduleType = AlarmScheduleType;
60 alarmScheduleTypeTranslate = AlarmScheduleTypeTranslationMap; 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 private modelValue: AlarmSchedule; 74 private modelValue: AlarmSchedule;
63 75
64 private defaultItems = Array.from({length: 7}, (value, i) => ({ 76 private defaultItems = Array.from({length: 7}, (value, i) => ({
65 enabled: true, 77 enabled: true,
66 - dayOfWeek: i 78 + dayOfWeek: i + 1
67 })); 79 }));
68 80
69 private propagateChange = (v: any) => { }; 81 private propagateChange = (v: any) => { };
@@ -75,10 +87,10 @@ export class AlarmScheduleComponent implements ControlValueAccessor, Validator, @@ -75,10 +87,10 @@ export class AlarmScheduleComponent implements ControlValueAccessor, Validator,
75 this.alarmScheduleForm = this.fb.group({ 87 this.alarmScheduleForm = this.fb.group({
76 type: [AlarmScheduleType.ANY_TIME, Validators.required], 88 type: [AlarmScheduleType.ANY_TIME, Validators.required],
77 timezone: [null, Validators.required], 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 startsOn: [0, Validators.required], 91 startsOn: [0, Validators.required],
80 endsOn: [0, Validators.required], 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 this.alarmScheduleForm.get('type').valueChanges.subscribe((type) => { 95 this.alarmScheduleForm.get('type').valueChanges.subscribe((type) => {
84 this.alarmScheduleForm.reset({type, items: this.defaultItems, timezone: this.defaultTimezone}, {emitEvent: false}); 96 this.alarmScheduleForm.reset({type, items: this.defaultItems, timezone: this.defaultTimezone}, {emitEvent: false});
@@ -90,6 +102,26 @@ export class AlarmScheduleComponent implements ControlValueAccessor, Validator, @@ -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 registerOnChange(fn: any): void { 125 registerOnChange(fn: any): void {
94 this.propagateChange = fn; 126 this.propagateChange = fn;
95 } 127 }
@@ -123,8 +155,8 @@ export class AlarmScheduleComponent implements ControlValueAccessor, Validator, @@ -123,8 +155,8 @@ export class AlarmScheduleComponent implements ControlValueAccessor, Validator,
123 type: this.modelValue.type, 155 type: this.modelValue.type,
124 timezone: this.modelValue.timezone, 156 timezone: this.modelValue.timezone,
125 daysOfWeek, 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 }, {emitEvent: false}); 160 }, {emitEvent: false});
129 break; 161 break;
130 case AlarmScheduleType.CUSTOM: 162 case AlarmScheduleType.CUSTOM:
@@ -136,8 +168,8 @@ export class AlarmScheduleComponent implements ControlValueAccessor, Validator, @@ -136,8 +168,8 @@ export class AlarmScheduleComponent implements ControlValueAccessor, Validator,
136 this.disabledSelectedTime(item.enabled, index); 168 this.disabledSelectedTime(item.enabled, index);
137 alarmDays.push({ 169 alarmDays.push({
138 enabled: item.enabled, 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 this.alarmScheduleForm.patchValue({ 175 this.alarmScheduleForm.patchValue({
@@ -202,15 +234,15 @@ export class AlarmScheduleComponent implements ControlValueAccessor, Validator, @@ -202,15 +234,15 @@ export class AlarmScheduleComponent implements ControlValueAccessor, Validator,
202 .filter(day => !!day); 234 .filter(day => !!day);
203 } 235 }
204 if (isDefined(value.startsOn) && value.startsOn !== 0) { 236 if (isDefined(value.startsOn) && value.startsOn !== 0) {
205 - value.startsOn = this.timeToTimestampUTC(value.startsOn); 237 + value.startsOn = timeOfDayToUTCTimestamp(value.startsOn);
206 } 238 }
207 if (isDefined(value.endsOn) && value.endsOn !== 0) { 239 if (isDefined(value.endsOn) && value.endsOn !== 0) {
208 - value.endsOn = this.timeToTimestampUTC(value.endsOn); 240 + value.endsOn = timeOfDayToUTCTimestamp(value.endsOn);
209 } 241 }
210 if (isDefined(value.items)){ 242 if (isDefined(value.items)){
211 value.items = this.alarmScheduleForm.getRawValue().items; 243 value.items = this.alarmScheduleForm.getRawValue().items;
212 value.items = value.items.map((item) => { 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 this.modelValue = value; 248 this.modelValue = value;
@@ -218,21 +250,11 @@ export class AlarmScheduleComponent implements ControlValueAccessor, Validator, @@ -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 private defaultItemsScheduler(index): FormGroup { 254 private defaultItemsScheduler(index): FormGroup {
233 return this.fb.group({ 255 return this.fb.group({
234 enabled: [true], 256 enabled: [true],
235 - dayOfWeek: [index], 257 + dayOfWeek: [index + 1],
236 startsOn: [0, Validators.required], 258 startsOn: [0, Validators.required],
237 endsOn: [0, Validators.required] 259 endsOn: [0, Validators.required]
238 }); 260 });
@@ -253,23 +275,8 @@ export class AlarmScheduleComponent implements ControlValueAccessor, Validator, @@ -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 getSchedulerRangeText(control: FormGroup | AbstractControl): string { 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 get itemsSchedulerForm(): FormArray { 282 get itemsSchedulerForm(): FormArray {
@@ -19,6 +19,7 @@ @@ -19,6 +19,7 @@
19 border: 2px groove rgba(0, 0, 0, .45); 19 border: 2px groove rgba(0, 0, 0, .45);
20 border-radius: 4px; 20 border-radius: 4px;
21 padding: 8px; 21 padding: 8px;
  22 + min-width: 0;
22 } 23 }
23 } 24 }
24 25
@@ -56,7 +56,7 @@ @@ -56,7 +56,7 @@
56 <mat-checkbox formControlName="propagate" style="display: block; padding-bottom: 16px;"> 56 <mat-checkbox formControlName="propagate" style="display: block; padding-bottom: 16px;">
57 {{ 'device-profile.propagate-alarm' | translate }} 57 {{ 'device-profile.propagate-alarm' | translate }}
58 </mat-checkbox> 58 </mat-checkbox>
59 - <section *ngIf="alarmFormGroup.get('propagate').value === true"> 59 + <section *ngIf="alarmFormGroup.get('propagate').value === true" style="padding-bottom: 1em;">
60 <mat-form-field floatLabel="always" class="mat-block"> 60 <mat-form-field floatLabel="always" class="mat-block">
61 <mat-label translate>device-profile.alarm-rule-relation-types-list</mat-label> 61 <mat-label translate>device-profile.alarm-rule-relation-types-list</mat-label>
62 <mat-chip-list #relationTypesChipList [disabled]="disabled"> 62 <mat-chip-list #relationTypesChipList [disabled]="disabled">
@@ -57,6 +57,7 @@ export class DeviceProfileAlarmComponent implements ControlValueAccessor, OnInit @@ -57,6 +57,7 @@ export class DeviceProfileAlarmComponent implements ControlValueAccessor, OnInit
57 57
58 separatorKeysCodes = [ENTER, COMMA, SEMICOLON]; 58 separatorKeysCodes = [ENTER, COMMA, SEMICOLON];
59 59
  60 + @Input()
60 expanded = false; 61 expanded = false;
61 62
62 private modelValue: DeviceProfileAlarm; 63 private modelValue: DeviceProfileAlarm;
@@ -21,6 +21,7 @@ @@ -21,6 +21,7 @@
21 let $index = index; last as isLast;" 21 let $index = index; last as isLast;"
22 fxLayout="column" [ngStyle]="!isLast ? {paddingBottom: '8px'} : {}"> 22 fxLayout="column" [ngStyle]="!isLast ? {paddingBottom: '8px'} : {}">
23 <tb-device-profile-alarm [formControl]="alarmControl" 23 <tb-device-profile-alarm [formControl]="alarmControl"
  24 + [expanded]="$index === 0"
24 (removeAlarm)="removeAlarm($index)"> 25 (removeAlarm)="removeAlarm($index)">
25 </tb-device-profile-alarm> 26 </tb-device-profile-alarm>
26 </div> 27 </div>
@@ -29,7 +30,7 @@ @@ -29,7 +30,7 @@
29 <span translate fxLayoutAlign="center center" 30 <span translate fxLayoutAlign="center center"
30 class="tb-prompt">device-profile.no-alarm-rules</span> 31 class="tb-prompt">device-profile.no-alarm-rules</span>
31 </div> 32 </div>
32 - <div *ngIf="!disabled" fxFlex fxLayout="row" fxLayoutAlign="end center" 33 + <div *ngIf="!disabled" fxFlex fxLayout="row" fxLayoutAlign="start center"
33 style="padding-top: 16px;"> 34 style="padding-top: 16px;">
34 <button mat-raised-button color="primary" 35 <button mat-raised-button color="primary"
35 type="button" 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,29 +61,7 @@
61 </div> 61 </div>
62 </div> 62 </div>
63 </mat-tab> 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 </mat-tab> 67 </mat-tab>
@@ -25,6 +25,8 @@ import { RuleChainId } from '@shared/models/id/rule-chain-id'; @@ -25,6 +25,8 @@ import { RuleChainId } from '@shared/models/id/rule-chain-id';
25 import { EntityInfoData } from '@shared/models/entity.models'; 25 import { EntityInfoData } from '@shared/models/entity.models';
26 import { KeyFilter } from '@shared/models/query/query.models'; 26 import { KeyFilter } from '@shared/models/query/query.models';
27 import { TimeUnit } from '@shared/models/time/time.models'; 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 export enum DeviceProfileType { 31 export enum DeviceProfileType {
30 DEFAULT = 'DEFAULT' 32 DEFAULT = 'DEFAULT'
@@ -408,3 +410,62 @@ export interface ClaimResult { @@ -408,3 +410,62 @@ export interface ClaimResult {
408 device: Device; 410 device: Device;
409 response: ClaimResponse; 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,21 +476,23 @@ export function keyFilterInfosToKeyFilters(keyFilterInfos: Array<KeyFilterInfo>)
476 export function keyFiltersToKeyFilterInfos(keyFilters: Array<KeyFilter>): Array<KeyFilterInfo> { 476 export function keyFiltersToKeyFilterInfos(keyFilters: Array<KeyFilter>): Array<KeyFilterInfo> {
477 const keyFilterInfos: Array<KeyFilterInfo> = []; 477 const keyFilterInfos: Array<KeyFilterInfo> = [];
478 const keyFilterInfoMap: {[infoKey: string]: KeyFilterInfo} = {}; 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 return keyFilterInfos; 498 return keyFilterInfos;
@@ -55,7 +55,9 @@ @@ -55,7 +55,9 @@
55 "continue": "Continue", 55 "continue": "Continue",
56 "discard-changes": "Discard Changes", 56 "discard-changes": "Discard Changes",
57 "download": "Download", 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 "aggregation": { 62 "aggregation": {
61 "aggregation": "Aggregation", 63 "aggregation": "Aggregation",
@@ -932,15 +934,18 @@ @@ -932,15 +934,18 @@
932 "condition-type": "Condition type", 934 "condition-type": "Condition type",
933 "condition-type-simple": "Simple", 935 "condition-type-simple": "Simple",
934 "condition-type-duration": "Duration", 936 "condition-type-duration": "Duration",
  937 + "condition-during": "During <b>{{during}}</b>",
935 "condition-type-repeating": "Repeating", 938 "condition-type-repeating": "Repeating",
936 "condition-type-required": "Condition type is required.", 939 "condition-type-required": "Condition type is required.",
937 "condition-repeating-value": "Count of events", 940 "condition-repeating-value": "Count of events",
938 "condition-repeating-value-range": "Count of events should be in a range from 1 to 2147483647.", 941 "condition-repeating-value-range": "Count of events should be in a range from 1 to 2147483647.",
939 "condition-repeating-value-pattern": "Count of events should be integers.", 942 "condition-repeating-value-pattern": "Count of events should be integers.",
940 "condition-repeating-value-required": "Count of events is required.", 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 "schedule-type": "Scheduler type", 945 "schedule-type": "Scheduler type",
942 "schedule-type-required": "Scheduler type is required.", 946 "schedule-type-required": "Scheduler type is required.",
943 "schedule": "Schedule", 947 "schedule": "Schedule",
  948 + "edit-schedule": "Edit alarm schedule",
944 "schedule-any-time": "Active all the time", 949 "schedule-any-time": "Active all the time",
945 "schedule-specific-time": "Active at a specific time", 950 "schedule-specific-time": "Active at a specific time",
946 "schedule-custom": "Custom", 951 "schedule-custom": "Custom",
@@ -956,7 +961,8 @@ @@ -956,7 +961,8 @@
956 "schedule-days": "Days", 961 "schedule-days": "Days",
957 "schedule-time": "Time", 962 "schedule-time": "Time",
958 "schedule-time-from": "From", 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 "dialog": { 967 "dialog": {
962 "close": "Close dialog" 968 "close": "Close dialog"