Commit 155b967b478f0d3462d06ba95a745255de2bf4be
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, |
ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition-dialog.component.html
0 → 100644
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> |
ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition-dialog.component.scss
0 → 100644
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 | +} |
ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition-dialog.component.ts
0 → 100644
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 { |
@@ -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" |
ui-ngx/src/app/modules/home/components/profile/alarm/edit-alarm-details-dialog.component.html
0 → 100644
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> |
ui-ngx/src/app/modules/home/components/profile/alarm/edit-alarm-details-dialog.component.ts
0 → 100644
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" |