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