Commit fd602dec7f2fd754fde946b0727adebb3c2a093e
1 parent
1f5f411b
UI: Added device profile schedule setting for alarm setting
Showing
16 changed files
with
848 additions
and
10 deletions
@@ -478,7 +478,7 @@ spring: | @@ -478,7 +478,7 @@ spring: | ||
478 | database-platform: "${SPRING_JPA_DATABASE_PLATFORM:org.hibernate.dialect.PostgreSQLDialect}" | 478 | database-platform: "${SPRING_JPA_DATABASE_PLATFORM:org.hibernate.dialect.PostgreSQLDialect}" |
479 | datasource: | 479 | datasource: |
480 | driverClassName: "${SPRING_DRIVER_CLASS_NAME:org.postgresql.Driver}" | 480 | driverClassName: "${SPRING_DRIVER_CLASS_NAME:org.postgresql.Driver}" |
481 | - url: "${SPRING_DATASOURCE_URL:jdbc:postgresql://localhost:5432/thingsboard}" | 481 | + url: "${SPRING_DATASOURCE_URL:jdbc:postgresql://localhost:5432/thingsboard_32}" |
482 | username: "${SPRING_DATASOURCE_USERNAME:postgres}" | 482 | username: "${SPRING_DATASOURCE_USERNAME:postgres}" |
483 | password: "${SPRING_DATASOURCE_PASSWORD:postgres}" | 483 | password: "${SPRING_DATASOURCE_PASSWORD:postgres}" |
484 | hikari: | 484 | hikari: |
@@ -137,7 +137,8 @@ | @@ -137,7 +137,8 @@ | ||
137 | "react-is", | 137 | "react-is", |
138 | "hoist-non-react-statics", | 138 | "hoist-non-react-statics", |
139 | "classnames", | 139 | "classnames", |
140 | - "raf" | 140 | + "raf", |
141 | + "moment-timezone" | ||
141 | ] | 142 | ] |
142 | }, | 143 | }, |
143 | "configurations": { | 144 | "configurations": { |
@@ -248,4 +249,4 @@ | @@ -248,4 +249,4 @@ | ||
248 | "cli": { | 249 | "cli": { |
249 | "packageManager": "yarn" | 250 | "packageManager": "yarn" |
250 | } | 251 | } |
251 | -} | ||
252 | +} |
@@ -63,6 +63,7 @@ | @@ -63,6 +63,7 @@ | ||
63 | "material-design-icons": "^3.0.1", | 63 | "material-design-icons": "^3.0.1", |
64 | "messageformat": "^2.3.0", | 64 | "messageformat": "^2.3.0", |
65 | "moment": "^2.27.0", | 65 | "moment": "^2.27.0", |
66 | + "moment-timezone": "^0.5.31", | ||
66 | "ngx-clipboard": "^13.0.1", | 67 | "ngx-clipboard": "^13.0.1", |
67 | "ngx-color-picker": "^10.0.1", | 68 | "ngx-color-picker": "^10.0.1", |
68 | "ngx-daterangepicker-material": "^4.0.1", | 69 | "ngx-daterangepicker-material": "^4.0.1", |
@@ -109,6 +110,7 @@ | @@ -109,6 +110,7 @@ | ||
109 | "@types/leaflet-polylinedecorator": "^1.6.0", | 110 | "@types/leaflet-polylinedecorator": "^1.6.0", |
110 | "@types/leaflet.markercluster": "^1.4.2", | 111 | "@types/leaflet.markercluster": "^1.4.2", |
111 | "@types/lodash": "^4.14.159", | 112 | "@types/lodash": "^4.14.159", |
113 | + "@types/moment-timezone": "^0.5.30", | ||
112 | "@types/raphael": "^2.3.0", | 114 | "@types/raphael": "^2.3.0", |
113 | "@types/react": "^16.9.46", | 115 | "@types/react": "^16.9.46", |
114 | "@types/react-dom": "^16.9.8", | 116 | "@types/react-dom": "^16.9.8", |
@@ -107,6 +107,7 @@ import { AlarmRuleKeyFiltersDialogComponent } from './profile/alarm/alarm-rule-k | @@ -107,6 +107,7 @@ import { AlarmRuleKeyFiltersDialogComponent } from './profile/alarm/alarm-rule-k | ||
107 | import { FilterTextComponent } from './filter/filter-text.component'; | 107 | import { FilterTextComponent } from './filter/filter-text.component'; |
108 | import { AddDeviceProfileDialogComponent } from './profile/add-device-profile-dialog.component'; | 108 | import { AddDeviceProfileDialogComponent } from './profile/add-device-profile-dialog.component'; |
109 | import { RuleChainAutocompleteComponent } from './rule-chain/rule-chain-autocomplete.component'; | 109 | import { RuleChainAutocompleteComponent } from './rule-chain/rule-chain-autocomplete.component'; |
110 | +import { AlarmScheduleComponent } from './profile/alarm/alarm-schedule.component'; | ||
110 | 111 | ||
111 | @NgModule({ | 112 | @NgModule({ |
112 | declarations: | 113 | declarations: |
@@ -196,7 +197,8 @@ import { RuleChainAutocompleteComponent } from './rule-chain/rule-chain-autocomp | @@ -196,7 +197,8 @@ import { RuleChainAutocompleteComponent } from './rule-chain/rule-chain-autocomp | ||
196 | DeviceProfileComponent, | 197 | DeviceProfileComponent, |
197 | DeviceProfileDialogComponent, | 198 | DeviceProfileDialogComponent, |
198 | AddDeviceProfileDialogComponent, | 199 | AddDeviceProfileDialogComponent, |
199 | - RuleChainAutocompleteComponent | 200 | + RuleChainAutocompleteComponent, |
201 | + AlarmScheduleComponent | ||
200 | ], | 202 | ], |
201 | imports: [ | 203 | imports: [ |
202 | CommonModule, | 204 | CommonModule, |
@@ -275,7 +277,8 @@ import { RuleChainAutocompleteComponent } from './rule-chain/rule-chain-autocomp | @@ -275,7 +277,8 @@ import { RuleChainAutocompleteComponent } from './rule-chain/rule-chain-autocomp | ||
275 | DeviceProfileComponent, | 277 | DeviceProfileComponent, |
276 | DeviceProfileDialogComponent, | 278 | DeviceProfileDialogComponent, |
277 | AddDeviceProfileDialogComponent, | 279 | AddDeviceProfileDialogComponent, |
278 | - RuleChainAutocompleteComponent | 280 | + RuleChainAutocompleteComponent, |
281 | + AlarmScheduleComponent | ||
279 | ], | 282 | ], |
280 | providers: [ | 283 | providers: [ |
281 | WidgetComponentService, | 284 | WidgetComponentService, |
@@ -93,7 +93,9 @@ | @@ -93,7 +93,9 @@ | ||
93 | </section> | 93 | </section> |
94 | </mat-tab> | 94 | </mat-tab> |
95 | <mat-tab label="{{ 'device-profile.schedule' | translate }}"> | 95 | <mat-tab label="{{ 'device-profile.schedule' | translate }}"> |
96 | - <div class="row">{{ 'device-profile.schedule' | translate }}</div> | 96 | + <tb-alarm-schedule fxFlex class="row" |
97 | + formControlName="schedule"> | ||
98 | + </tb-alarm-schedule> | ||
97 | </mat-tab> | 99 | </mat-tab> |
98 | <mat-tab label="{{ 'device-profile.alarm-rule-details' | translate }}"> | 100 | <mat-tab label="{{ 'device-profile.alarm-rule-details' | translate }}"> |
99 | <mat-form-field class="mat-block row"> | 101 | <mat-form-field class="mat-block row"> |
@@ -95,6 +95,7 @@ export class AlarmRuleComponent implements ControlValueAccessor, OnInit, Validat | @@ -95,6 +95,7 @@ export class AlarmRuleComponent implements ControlValueAccessor, OnInit, Validat | ||
95 | count: [{value: null, disable: true}, [Validators.required, Validators.min(1), Validators.max(2147483647), Validators.pattern('[0-9]*')]] | 95 | count: [{value: null, disable: true}, [Validators.required, Validators.min(1), Validators.max(2147483647), Validators.pattern('[0-9]*')]] |
96 | }) | 96 | }) |
97 | }, Validators.required), | 97 | }, Validators.required), |
98 | + schedule: [null], | ||
98 | alarmDetails: [null] | 99 | alarmDetails: [null] |
99 | }); | 100 | }); |
100 | this.alarmRuleFormGroup.get('condition.spec.type').valueChanges.subscribe((type) => { | 101 | this.alarmRuleFormGroup.get('condition.spec.type').valueChanges.subscribe((type) => { |
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 | +<section [formGroup]="alarmScheduleForm" fxLayout="column"> | ||
19 | + <mat-form-field class="mat-block" hideRequiredMarker floatLabel="always"> | ||
20 | + <mat-label> </mat-label> | ||
21 | + <mat-select formControlName="type" required placeholder="{{ 'device-profile.schedule-type' | translate }}"> | ||
22 | + <mat-option *ngFor="let alarmScheduleType of alarmScheduleTypes" [value]="alarmScheduleType"> | ||
23 | + {{ alarmScheduleTypeTranslate.get(alarmScheduleType) | translate }} | ||
24 | + </mat-option> | ||
25 | + </mat-select> | ||
26 | + <mat-error *ngIf="alarmScheduleForm.get('type').hasError('required')"> | ||
27 | + {{ 'device-profile.schedule-type-required' | translate }} | ||
28 | + </mat-error> | ||
29 | + </mat-form-field> | ||
30 | + <div *ngIf="alarmScheduleForm.get('type').value !== alarmScheduleType.ANY_TIME"> | ||
31 | + <tb-timezone-select | ||
32 | + [defaultTimezone]="defaultTimezone" | ||
33 | + required | ||
34 | + formControlName="timezone"> | ||
35 | + </tb-timezone-select> | ||
36 | + <section *ngIf="alarmScheduleForm.get('type').value === alarmScheduleType.SPECIFIC_TIME"> | ||
37 | + <div class="tb-small" style="margin-bottom: 0.5em" translate>device-profile.schedule-days</div> | ||
38 | + <div fxLayout="column" fxLayout.gt-sm="row" fxLayoutGap="16px" style="padding-bottom: 16px;"> | ||
39 | + <div fxLayout="row" fxLayoutGap="16px"> | ||
40 | + <mat-checkbox [formControl]="weeklyRepeatControl(0)"> | ||
41 | + {{ 'device-profile.schedule-day.monday' | translate }} | ||
42 | + </mat-checkbox> | ||
43 | + <mat-checkbox [formControl]="weeklyRepeatControl(1)"> | ||
44 | + {{ 'device-profile.schedule-day.tuesday' | translate }} | ||
45 | + </mat-checkbox> | ||
46 | + <mat-checkbox [formControl]="weeklyRepeatControl(2)"> | ||
47 | + {{ 'device-profile.schedule-day.wednesday' | translate }} | ||
48 | + </mat-checkbox> | ||
49 | + <mat-checkbox [formControl]="weeklyRepeatControl(3)"> | ||
50 | + {{ 'device-profile.schedule-day.thursday' | translate }} | ||
51 | + </mat-checkbox> | ||
52 | + </div> | ||
53 | + <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 }} | ||
62 | + </mat-checkbox> | ||
63 | + </div> | ||
64 | + </div> | ||
65 | + <div class="tb-small" style="margin-bottom: 0.5em" translate>device-profile.schedule-time</div> | ||
66 | + <div fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px"> | ||
67 | + <mat-form-field fxFlex> | ||
68 | + <mat-label translate>device-profile.schedule-time-from</mat-label> | ||
69 | + <mat-datetimepicker-toggle [for]="startTimePicker" matPrefix></mat-datetimepicker-toggle> | ||
70 | + <mat-datetimepicker #startTimePicker type="time" openOnFocus="true"></mat-datetimepicker> | ||
71 | + <input required matInput formControlName="startsOn" [matDatetimepicker]="startTimePicker"> | ||
72 | + </mat-form-field> | ||
73 | + <mat-form-field fxFlex> | ||
74 | + <mat-label translate>device-profile.schedule-time-to</mat-label> | ||
75 | + <mat-datetimepicker-toggle [for]="endTimePicker" matPrefix></mat-datetimepicker-toggle> | ||
76 | + <mat-datetimepicker #endTimePicker type="time" openOnFocus="true"></mat-datetimepicker> | ||
77 | + <input required matInput formControlName="endsOn" [matDatetimepicker]="endTimePicker"> | ||
78 | + </mat-form-field> | ||
79 | + </div> | ||
80 | + </section> | ||
81 | + <section *ngIf="alarmScheduleForm.get('type').value === alarmScheduleType.CUSTOM"> | ||
82 | + <div class="tb-small" style="margin-bottom: 0.5em" translate>device-profile.schedule-days</div> | ||
83 | + <div fxLayout="column" fxLayout.gt-sm="row" fxLayoutGap.gt-sm="16px" formArrayName="items"> | ||
84 | + <div fxLayout="column" fxFlex fxFlex.gt-sm="50"> | ||
85 | + <div fxLayout="row" fxLayoutGap="8px" formGroupName="0" fxLayoutAlign="start center"> | ||
86 | + <mat-checkbox formControlName="enabled" fxFlex="40" (change)="changeCustomScheduler($event, 0)"> | ||
87 | + {{ 'device-profile.schedule-day.monday' | translate }} | ||
88 | + </mat-checkbox> | ||
89 | + <div fxLayout="row" fxLayoutGap="8px" fxFlex> | ||
90 | + <mat-form-field fxFlex="100px"> | ||
91 | + <mat-label translate>device-profile.schedule-time-from</mat-label> | ||
92 | + <mat-datetimepicker-toggle [for]="startTimePicker1" matPrefix></mat-datetimepicker-toggle> | ||
93 | + <mat-datetimepicker #startTimePicker1 type="time" openOnFocus="true"></mat-datetimepicker> | ||
94 | + <input required matInput formControlName="startsOn" [matDatetimepicker]="startTimePicker1"> | ||
95 | + </mat-form-field> | ||
96 | + <mat-form-field fxFlex="100px"> | ||
97 | + <mat-label translate>device-profile.schedule-time-to</mat-label> | ||
98 | + <mat-datetimepicker-toggle [for]="endTimePicker1" matPrefix></mat-datetimepicker-toggle> | ||
99 | + <mat-datetimepicker #endTimePicker1 type="time" openOnFocus="true"></mat-datetimepicker> | ||
100 | + <input required matInput formControlName="endsOn" [matDatetimepicker]="endTimePicker1"> | ||
101 | + </mat-form-field> | ||
102 | + </div> | ||
103 | + </div> | ||
104 | + <div fxLayout="row" fxLayoutGap="8px" formGroupName="1" fxLayoutAlign="start center"> | ||
105 | + <mat-checkbox formControlName="enabled" fxFlex="40" (change)="changeCustomScheduler($event, 1)"> | ||
106 | + {{ 'device-profile.schedule-day.tuesday' | translate }} | ||
107 | + </mat-checkbox> | ||
108 | + <div fxLayout="row" fxLayoutGap="8px" fxFlex> | ||
109 | + <mat-form-field fxFlex="100px"> | ||
110 | + <mat-label translate>device-profile.schedule-time-from</mat-label> | ||
111 | + <mat-datetimepicker-toggle [for]="startTimePicker2" matPrefix></mat-datetimepicker-toggle> | ||
112 | + <mat-datetimepicker #startTimePicker2 type="time" openOnFocus="true"></mat-datetimepicker> | ||
113 | + <input required matInput formControlName="startsOn" [matDatetimepicker]="startTimePicker2"> | ||
114 | + </mat-form-field> | ||
115 | + <mat-form-field fxFlex="100px"> | ||
116 | + <mat-label translate>device-profile.schedule-time-to</mat-label> | ||
117 | + <mat-datetimepicker-toggle [for]="endTimePicker2" matPrefix></mat-datetimepicker-toggle> | ||
118 | + <mat-datetimepicker #endTimePicker2 type="time" openOnFocus="true"></mat-datetimepicker> | ||
119 | + <input required matInput formControlName="endsOn" [matDatetimepicker]="endTimePicker2"> | ||
120 | + </mat-form-field> | ||
121 | + </div> | ||
122 | + </div> | ||
123 | + <div fxLayout="row" fxLayoutGap="8px" formGroupName="2" fxLayoutAlign="start center"> | ||
124 | + <mat-checkbox formControlName="enabled" fxFlex="40" (change)="changeCustomScheduler($event, 2)"> | ||
125 | + {{ 'device-profile.schedule-day.wednesday' | translate }} | ||
126 | + </mat-checkbox> | ||
127 | + <div fxLayout="row" fxLayoutGap="8px" fxFlex> | ||
128 | + <mat-form-field fxFlex="100px"> | ||
129 | + <mat-label translate>device-profile.schedule-time-from</mat-label> | ||
130 | + <mat-datetimepicker-toggle [for]="startTimePicker3" matPrefix></mat-datetimepicker-toggle> | ||
131 | + <mat-datetimepicker #startTimePicker3 type="time" openOnFocus="true"></mat-datetimepicker> | ||
132 | + <input required matInput formControlName="startsOn" [matDatetimepicker]="startTimePicker3"> | ||
133 | + </mat-form-field> | ||
134 | + <mat-form-field fxFlex="100px"> | ||
135 | + <mat-label translate>device-profile.schedule-time-to</mat-label> | ||
136 | + <mat-datetimepicker-toggle [for]="endTimePicker3" matPrefix></mat-datetimepicker-toggle> | ||
137 | + <mat-datetimepicker #endTimePicker3 type="time" openOnFocus="true"></mat-datetimepicker> | ||
138 | + <input required matInput formControlName="endsOn" [matDatetimepicker]="endTimePicker3"> | ||
139 | + </mat-form-field> | ||
140 | + </div> | ||
141 | + </div> | ||
142 | + <div fxLayout="row" fxLayoutGap="8px" formGroupName="3" fxLayoutAlign="start center"> | ||
143 | + <mat-checkbox formControlName="enabled" fxFlex="40" (change)="changeCustomScheduler($event, 3)"> | ||
144 | + {{ 'device-profile.schedule-day.thursday' | translate }} | ||
145 | + </mat-checkbox> | ||
146 | + <div fxLayout="row" fxLayoutGap="8px" fxFlex> | ||
147 | + <mat-form-field fxFlex="100px"> | ||
148 | + <mat-label translate>device-profile.schedule-time-from</mat-label> | ||
149 | + <mat-datetimepicker-toggle [for]="startTimePicker4" matPrefix></mat-datetimepicker-toggle> | ||
150 | + <mat-datetimepicker #startTimePicker4 type="time" openOnFocus="true"></mat-datetimepicker> | ||
151 | + <input required matInput formControlName="startsOn" [matDatetimepicker]="startTimePicker4"> | ||
152 | + </mat-form-field> | ||
153 | + <mat-form-field fxFlex="100px"> | ||
154 | + <mat-label translate>device-profile.schedule-time-to</mat-label> | ||
155 | + <mat-datetimepicker-toggle [for]="endTimePicker4" matPrefix></mat-datetimepicker-toggle> | ||
156 | + <mat-datetimepicker #endTimePicker4 type="time" openOnFocus="true"></mat-datetimepicker> | ||
157 | + <input required matInput formControlName="endsOn" [matDatetimepicker]="endTimePicker4"> | ||
158 | + </mat-form-field> | ||
159 | + </div> | ||
160 | + </div> | ||
161 | + </div> | ||
162 | + <div fxLayout="column" fxFlex fxFlex.gt-sm="50"> | ||
163 | + <div fxLayout="row" fxLayoutGap="8px" formGroupName="4" fxLayoutAlign="start center"> | ||
164 | + <mat-checkbox formControlName="enabled" fxFlex="40" (change)="changeCustomScheduler($event, 4)"> | ||
165 | + {{ 'device-profile.schedule-day.friday' | translate }} | ||
166 | + </mat-checkbox> | ||
167 | + <div fxLayout="row" fxLayoutGap="8px" fxFlex> | ||
168 | + <mat-form-field fxFlex="100px"> | ||
169 | + <mat-label translate>device-profile.schedule-time-from</mat-label> | ||
170 | + <mat-datetimepicker-toggle [for]="startTimePicker5" matPrefix></mat-datetimepicker-toggle> | ||
171 | + <mat-datetimepicker #startTimePicker5 type="time" openOnFocus="true"></mat-datetimepicker> | ||
172 | + <input required matInput formControlName="startsOn" [matDatetimepicker]="startTimePicker5"> | ||
173 | + </mat-form-field> | ||
174 | + <mat-form-field fxFlex="100px"> | ||
175 | + <mat-label translate>device-profile.schedule-time-to</mat-label> | ||
176 | + <mat-datetimepicker-toggle [for]="endTimePicker5" matPrefix></mat-datetimepicker-toggle> | ||
177 | + <mat-datetimepicker #endTimePicker5 type="time" openOnFocus="true"></mat-datetimepicker> | ||
178 | + <input required matInput formControlName="endsOn" [matDatetimepicker]="endTimePicker5"> | ||
179 | + </mat-form-field> | ||
180 | + </div> | ||
181 | + </div> | ||
182 | + <div fxLayout="row" fxLayoutGap="8px" formGroupName="5" fxLayoutAlign="start center"> | ||
183 | + <mat-checkbox formControlName="enabled" fxFlex="40" (change)="changeCustomScheduler($event, 5)"> | ||
184 | + {{ 'device-profile.schedule-day.saturday' | translate }} | ||
185 | + </mat-checkbox> | ||
186 | + <div fxLayout="row" fxLayoutGap="8px" fxFlex> | ||
187 | + <mat-form-field fxFlex="100px"> | ||
188 | + <mat-label translate>device-profile.schedule-time-from</mat-label> | ||
189 | + <mat-datetimepicker-toggle [for]="startTimePicker6" matPrefix></mat-datetimepicker-toggle> | ||
190 | + <mat-datetimepicker #startTimePicker6 type="time" openOnFocus="true"></mat-datetimepicker> | ||
191 | + <input required matInput formControlName="startsOn" [matDatetimepicker]="startTimePicker6"> | ||
192 | + </mat-form-field> | ||
193 | + <mat-form-field fxFlex="100px"> | ||
194 | + <mat-label translate>device-profile.schedule-time-to</mat-label> | ||
195 | + <mat-datetimepicker-toggle [for]="endTimePicker6" matPrefix></mat-datetimepicker-toggle> | ||
196 | + <mat-datetimepicker #endTimePicker6 type="time" openOnFocus="true"></mat-datetimepicker> | ||
197 | + <input required matInput formControlName="endsOn" [matDatetimepicker]="endTimePicker6"> | ||
198 | + </mat-form-field> | ||
199 | + </div> | ||
200 | + </div> | ||
201 | + <div fxLayout="row" fxLayoutGap="8px" formGroupName="6" fxLayoutAlign="start center"> | ||
202 | + <mat-checkbox formControlName="enabled" fxFlex="40" (change)="changeCustomScheduler($event, 6)"> | ||
203 | + {{ 'device-profile.schedule-day.sunday' | translate }} | ||
204 | + </mat-checkbox> | ||
205 | + <div fxLayout="row" fxLayoutGap="8px" fxFlex> | ||
206 | + <mat-form-field fxFlex="100px"> | ||
207 | + <mat-label translate>device-profile.schedule-time-from</mat-label> | ||
208 | + <mat-datetimepicker-toggle [for]="startTimePicker7" matPrefix></mat-datetimepicker-toggle> | ||
209 | + <mat-datetimepicker #startTimePicker7 type="time" openOnFocus="true"></mat-datetimepicker> | ||
210 | + <input required matInput formControlName="startsOn" [matDatetimepicker]="startTimePicker7"> | ||
211 | + </mat-form-field> | ||
212 | + <mat-form-field fxFlex="100px"> | ||
213 | + <mat-label translate>device-profile.schedule-time-to</mat-label> | ||
214 | + <mat-datetimepicker-toggle [for]="endTimePicker7" matPrefix></mat-datetimepicker-toggle> | ||
215 | + <mat-datetimepicker #endTimePicker7 type="time" openOnFocus="true"></mat-datetimepicker> | ||
216 | + <input required matInput formControlName="endsOn" [matDatetimepicker]="endTimePicker7"> | ||
217 | + </mat-form-field> | ||
218 | + </div> | ||
219 | + </div> | ||
220 | + </div> | ||
221 | + </div> | ||
222 | + </section> | ||
223 | + </div> | ||
224 | +</section> |
1 | +/// | ||
2 | +/// Copyright © 2016-2020 The Thingsboard Authors | ||
3 | +/// | ||
4 | +/// Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | +/// you may not use this file except in compliance with the License. | ||
6 | +/// You may obtain a copy of the License at | ||
7 | +/// | ||
8 | +/// http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | +/// | ||
10 | +/// Unless required by applicable law or agreed to in writing, software | ||
11 | +/// distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | +/// See the License for the specific language governing permissions and | ||
14 | +/// limitations under the License. | ||
15 | +/// | ||
16 | + | ||
17 | +import { Component, forwardRef, Input, OnInit } from '@angular/core'; | ||
18 | +import { | ||
19 | + ControlValueAccessor, | ||
20 | + FormArray, | ||
21 | + FormBuilder, | ||
22 | + FormControl, | ||
23 | + FormGroup, | ||
24 | + NG_VALIDATORS, | ||
25 | + NG_VALUE_ACCESSOR, | ||
26 | + ValidationErrors, | ||
27 | + Validator, | ||
28 | + Validators | ||
29 | +} from '@angular/forms'; | ||
30 | +import { AlarmSchedule, AlarmScheduleType, AlarmScheduleTypeTranslationMap } from '@shared/models/device.models'; | ||
31 | +import { isDefined, isDefinedAndNotNull } from '@core/utils'; | ||
32 | +import * as _moment from 'moment-timezone'; | ||
33 | +import { MatCheckboxChange } from '@angular/material/checkbox'; | ||
34 | + | ||
35 | +@Component({ | ||
36 | + selector: 'tb-alarm-schedule', | ||
37 | + templateUrl: './alarm-schedule.component.html', | ||
38 | + providers: [{ | ||
39 | + provide: NG_VALUE_ACCESSOR, | ||
40 | + useExisting: forwardRef(() => AlarmScheduleComponent), | ||
41 | + multi: true | ||
42 | + }, { | ||
43 | + provide: NG_VALIDATORS, | ||
44 | + useExisting: forwardRef(() => AlarmScheduleComponent), | ||
45 | + multi: true | ||
46 | + }] | ||
47 | +}) | ||
48 | +export class AlarmScheduleComponent implements ControlValueAccessor, Validator, OnInit { | ||
49 | + @Input() | ||
50 | + disabled: boolean; | ||
51 | + | ||
52 | + alarmScheduleForm: FormGroup; | ||
53 | + | ||
54 | + defaultTimezone = _moment.tz.guess(); | ||
55 | + | ||
56 | + alarmScheduleTypes = Object.keys(AlarmScheduleType); | ||
57 | + alarmScheduleType = AlarmScheduleType; | ||
58 | + alarmScheduleTypeTranslate = AlarmScheduleTypeTranslationMap; | ||
59 | + | ||
60 | + private modelValue: AlarmSchedule; | ||
61 | + | ||
62 | + private defaultItems = Array.from({length: 7}, (value, i) => ({ | ||
63 | + enabled: true, | ||
64 | + dayOfWeek: i | ||
65 | + })); | ||
66 | + | ||
67 | + private propagateChange = (v: any) => { }; | ||
68 | + | ||
69 | + constructor(private fb: FormBuilder) { | ||
70 | + } | ||
71 | + | ||
72 | + ngOnInit(): void { | ||
73 | + this.alarmScheduleForm = this.fb.group({ | ||
74 | + type: [AlarmScheduleType.ANY_TIME, Validators.required], | ||
75 | + timezone: [null, Validators.required], | ||
76 | + daysOfWeek: this.fb.array(new Array(7).fill(false)), | ||
77 | + startsOn: [0, Validators.required], | ||
78 | + endsOn: [0, Validators.required], | ||
79 | + items: this.fb.array(Array.from({length: 7}, (value, i) => this.defaultItemsScheduler(i))) | ||
80 | + }); | ||
81 | + this.alarmScheduleForm.get('type').valueChanges.subscribe((type) => { | ||
82 | + this.alarmScheduleForm.reset({type, items: this.defaultItems}, {emitEvent: false}); | ||
83 | + this.updateValidators(type, true); | ||
84 | + this.alarmScheduleForm.updateValueAndValidity(); | ||
85 | + }); | ||
86 | + this.alarmScheduleForm.valueChanges.subscribe(() => { | ||
87 | + this.updateModel(); | ||
88 | + }); | ||
89 | + } | ||
90 | + | ||
91 | + registerOnChange(fn: any): void { | ||
92 | + this.propagateChange = fn; | ||
93 | + } | ||
94 | + | ||
95 | + registerOnTouched(fn: any): void { | ||
96 | + } | ||
97 | + | ||
98 | + setDisabledState(isDisabled: boolean): void { | ||
99 | + this.disabled = isDisabled; | ||
100 | + if (this.disabled) { | ||
101 | + this.alarmScheduleForm.disable({emitEvent: false}); | ||
102 | + } else { | ||
103 | + this.alarmScheduleForm.enable({emitEvent: false}); | ||
104 | + } | ||
105 | + } | ||
106 | + | ||
107 | + writeValue(value: AlarmSchedule): void { | ||
108 | + this.modelValue = value; | ||
109 | + if (!isDefinedAndNotNull(this.modelValue)) { | ||
110 | + this.modelValue = { | ||
111 | + type: AlarmScheduleType.ANY_TIME | ||
112 | + }; | ||
113 | + } | ||
114 | + switch (this.modelValue.type) { | ||
115 | + case AlarmScheduleType.SPECIFIC_TIME: | ||
116 | + let daysOfWeek = new Array(7).fill(false); | ||
117 | + if (isDefined(this.modelValue.daysOfWeek)) { | ||
118 | + daysOfWeek = daysOfWeek.map((item, index) => this.modelValue.daysOfWeek.indexOf(index + 1) > -1); | ||
119 | + } | ||
120 | + this.alarmScheduleForm.patchValue({ | ||
121 | + type: this.modelValue.type, | ||
122 | + timezone: this.modelValue.timezone, | ||
123 | + daysOfWeek, | ||
124 | + startsOn: this.timestampToTime(this.modelValue.startsOn), | ||
125 | + endsOn: this.timestampToTime(this.modelValue.endsOn) | ||
126 | + }, {emitEvent: false}); | ||
127 | + break; | ||
128 | + case AlarmScheduleType.CUSTOM: | ||
129 | + if (this.modelValue.items) { | ||
130 | + const alarmDays = []; | ||
131 | + this.modelValue.items | ||
132 | + .sort((a, b) => a.dayOfWeek - b.dayOfWeek) | ||
133 | + .forEach((item, index) => { | ||
134 | + if (item.enabled) { | ||
135 | + this.itemsSchedulerForm.at(index).get('startsOn').enable({emitEvent: false}); | ||
136 | + this.itemsSchedulerForm.at(index).get('endsOn').enable({emitEvent: false}); | ||
137 | + } else { | ||
138 | + this.itemsSchedulerForm.at(index).get('startsOn').disable({emitEvent: false}); | ||
139 | + this.itemsSchedulerForm.at(index).get('endsOn').disable({emitEvent: false}); | ||
140 | + } | ||
141 | + alarmDays.push({ | ||
142 | + enabled: item.enabled, | ||
143 | + startsOn: this.timestampToTime(item.startsOn), | ||
144 | + endsOn: this.timestampToTime(item.endsOn) | ||
145 | + }); | ||
146 | + }); | ||
147 | + this.alarmScheduleForm.patchValue({ | ||
148 | + type: this.modelValue.type, | ||
149 | + timezone: this.modelValue.timezone, | ||
150 | + items: alarmDays | ||
151 | + }, {emitEvent: false}); | ||
152 | + } | ||
153 | + break; | ||
154 | + default: | ||
155 | + this.alarmScheduleForm.patchValue(this.modelValue || undefined, {emitEvent: false}); | ||
156 | + } | ||
157 | + this.updateValidators(this.modelValue.type); | ||
158 | + } | ||
159 | + | ||
160 | + validate(control: FormGroup): ValidationErrors | null { | ||
161 | + return this.alarmScheduleForm.valid ? null : { | ||
162 | + alarmScheduler: { | ||
163 | + valid: false | ||
164 | + } | ||
165 | + }; | ||
166 | + } | ||
167 | + | ||
168 | + weeklyRepeatControl(index: number): FormControl { | ||
169 | + return (this.alarmScheduleForm.get('daysOfWeek') as FormArray).at(index) as FormControl; | ||
170 | + } | ||
171 | + | ||
172 | + private updateValidators(type: AlarmScheduleType, changedType = false){ | ||
173 | + switch (type){ | ||
174 | + case AlarmScheduleType.ANY_TIME: | ||
175 | + this.alarmScheduleForm.get('timezone').disable({emitEvent: false}); | ||
176 | + this.alarmScheduleForm.get('daysOfWeek').disable({emitEvent: false}); | ||
177 | + this.alarmScheduleForm.get('startsOn').disable({emitEvent: false}); | ||
178 | + this.alarmScheduleForm.get('endsOn').disable({emitEvent: false}); | ||
179 | + this.alarmScheduleForm.get('items').disable({emitEvent: false}); | ||
180 | + break; | ||
181 | + case AlarmScheduleType.SPECIFIC_TIME: | ||
182 | + this.alarmScheduleForm.get('timezone').enable({emitEvent: false}); | ||
183 | + this.alarmScheduleForm.get('daysOfWeek').enable({emitEvent: false}); | ||
184 | + this.alarmScheduleForm.get('startsOn').enable({emitEvent: false}); | ||
185 | + this.alarmScheduleForm.get('endsOn').enable({emitEvent: false}); | ||
186 | + this.alarmScheduleForm.get('items').disable({emitEvent: false}); | ||
187 | + break; | ||
188 | + case AlarmScheduleType.CUSTOM: | ||
189 | + this.alarmScheduleForm.get('timezone').enable({emitEvent: false}); | ||
190 | + this.alarmScheduleForm.get('daysOfWeek').disable({emitEvent: false}); | ||
191 | + this.alarmScheduleForm.get('startsOn').disable({emitEvent: false}); | ||
192 | + this.alarmScheduleForm.get('endsOn').disable({emitEvent: false}); | ||
193 | + if (changedType) { | ||
194 | + this.alarmScheduleForm.get('items').enable({emitEvent: false}); | ||
195 | + } | ||
196 | + break; | ||
197 | + } | ||
198 | + } | ||
199 | + | ||
200 | + private updateModel() { | ||
201 | + const value = this.alarmScheduleForm.value; | ||
202 | + if (this.modelValue) { | ||
203 | + if (isDefined(value.daysOfWeek)) { | ||
204 | + value.daysOfWeek = value.daysOfWeek | ||
205 | + .map((day: boolean, index: number) => day ? index + 1 : null) | ||
206 | + .filter(day => !!day); | ||
207 | + } | ||
208 | + if (isDefined(value.startsOn) && value.startsOn !== 0) { | ||
209 | + value.startsOn = this.timeToTimestamp(value.startsOn); | ||
210 | + } | ||
211 | + if (isDefined(value.endsOn) && value.endsOn !== 0) { | ||
212 | + value.endsOn = this.timeToTimestamp(value.endsOn); | ||
213 | + } | ||
214 | + if (isDefined(value.items)){ | ||
215 | + value.items = this.alarmScheduleForm.getRawValue().items; | ||
216 | + value.items = value.items.map((item) => { | ||
217 | + return { ...item, startsOn: this.timeToTimestamp(item.startsOn), endsOn: this.timeToTimestamp(item.endsOn)}; | ||
218 | + }); | ||
219 | + } | ||
220 | + this.modelValue = value; | ||
221 | + this.propagateChange(this.modelValue); | ||
222 | + } | ||
223 | + } | ||
224 | + | ||
225 | + private timeToTimestamp(date: Date | number): number { | ||
226 | + if (typeof date === 'number' || date === null) { | ||
227 | + return 0; | ||
228 | + } | ||
229 | + return _moment.utc([1970, 0, 1, date.getHours(), date.getMinutes(), date.getSeconds(), 0]).valueOf(); | ||
230 | + } | ||
231 | + | ||
232 | + private timestampToTime(time = 0): Date { | ||
233 | + return new Date(time + new Date().getTimezoneOffset() * 60 * 1000); | ||
234 | + } | ||
235 | + | ||
236 | + private defaultItemsScheduler(index): FormGroup { | ||
237 | + return this.fb.group({ | ||
238 | + enabled: [true], | ||
239 | + dayOfWeek: [index], | ||
240 | + startsOn: [0, Validators.required], | ||
241 | + endsOn: [0, Validators.required] | ||
242 | + }); | ||
243 | + } | ||
244 | + | ||
245 | + changeCustomScheduler($event: MatCheckboxChange, index: number) { | ||
246 | + const value = $event.checked; | ||
247 | + if (value) { | ||
248 | + this.itemsSchedulerForm.at(index).get('startsOn').enable({emitEvent: false}); | ||
249 | + this.itemsSchedulerForm.at(index).get('endsOn').enable(); | ||
250 | + } else { | ||
251 | + this.itemsSchedulerForm.at(index).get('startsOn').disable({emitEvent: false}); | ||
252 | + this.itemsSchedulerForm.at(index).get('endsOn').disable(); | ||
253 | + } | ||
254 | + } | ||
255 | + | ||
256 | + private get itemsSchedulerForm(): FormArray { | ||
257 | + return this.alarmScheduleForm.get('items') as FormArray; | ||
258 | + } | ||
259 | +} |
@@ -34,6 +34,7 @@ | @@ -34,6 +34,7 @@ | ||
34 | {{ 'device-profile.alarm-severity-required' | translate }} | 34 | {{ 'device-profile.alarm-severity-required' | translate }} |
35 | </mat-error> | 35 | </mat-error> |
36 | </mat-form-field> | 36 | </mat-form-field> |
37 | + <mat-divider></mat-divider> | ||
37 | <tb-alarm-rule formControlName="alarmRule" required fxFlex> | 38 | <tb-alarm-rule formControlName="alarmRule" required fxFlex> |
38 | </tb-alarm-rule> | 39 | </tb-alarm-rule> |
39 | </div> | 40 | </div> |
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 | +<mat-form-field [formGroup]="selectTimezoneFormGroup" fxFlex class="mat-block"> | ||
19 | + <mat-label translate>timezone.timezone</mat-label> | ||
20 | + <input matInput type="text" placeholder="{{ 'timezone.select-timezone' | translate }}" | ||
21 | + #timezoneInput | ||
22 | + formControlName="timezone" | ||
23 | + (focusin)="onFocus()" | ||
24 | + [required]="required" | ||
25 | + [matAutocomplete]="timezoneAutocomplete"> | ||
26 | + <button *ngIf="selectTimezoneFormGroup.get('timezone').value && !disabled" | ||
27 | + type="button" style="margin-right: 1px" | ||
28 | + matSuffix mat-button mat-icon-button aria-label="Clear" | ||
29 | + (mousedown)="ignoreClosePanel = true" | ||
30 | + (click)="clear()"> | ||
31 | + <mat-icon class="material-icons">close</mat-icon> | ||
32 | + </button> | ||
33 | + <mat-autocomplete class="tb-autocomplete" | ||
34 | + #timezoneAutocomplete="matAutocomplete" | ||
35 | + (closed)="onPanelClosed()" | ||
36 | + (optionSelected)="ignoreClosePanel = true" | ||
37 | + [displayWith]="displayTimezoneFn"> | ||
38 | + <mat-option *ngFor="let timezone of filteredTimezones | async" [value]="timezone"> | ||
39 | + <span [innerHTML]="displayTimezoneFn(timezone) | highlight:searchText"></span> | ||
40 | + </mat-option> | ||
41 | + <mat-option *ngIf="!(filteredTimezones | async)?.length" [value]="null"> | ||
42 | + <span> | ||
43 | + {{ translate.get('timezone.no-timezones-matching', {timezone: searchText}) | async }} | ||
44 | + </span> | ||
45 | + </mat-option> | ||
46 | + </mat-autocomplete> | ||
47 | + <mat-error *ngIf="selectTimezoneFormGroup.get('timezone').hasError('required')"> | ||
48 | + {{ 'timezone.timezone-required' | translate }} | ||
49 | + </mat-error> | ||
50 | +</mat-form-field> |
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 { AfterViewInit, Component, forwardRef, Input, NgZone, OnInit, ViewChild } from '@angular/core'; | ||
18 | +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms'; | ||
19 | +import { Observable, of } from 'rxjs'; | ||
20 | +import { map, mergeMap, share, tap } from 'rxjs/operators'; | ||
21 | +import { Store } from '@ngrx/store'; | ||
22 | +import { AppState } from '@app/core/core.state'; | ||
23 | +import { TranslateService } from '@ngx-translate/core'; | ||
24 | +import { coerceBooleanProperty } from '@angular/cdk/coercion'; | ||
25 | +import * as _moment from 'moment-timezone'; | ||
26 | +import { MatAutocompleteTrigger } from '@angular/material/autocomplete'; | ||
27 | + | ||
28 | +interface TimezoneInfo { | ||
29 | + id: string; | ||
30 | + name: string; | ||
31 | + offset: string; | ||
32 | + nOffset: number; | ||
33 | +} | ||
34 | + | ||
35 | +@Component({ | ||
36 | + selector: 'tb-timezone-select', | ||
37 | + templateUrl: './timezone-select.component.html', | ||
38 | + styleUrls: [], | ||
39 | + providers: [{ | ||
40 | + provide: NG_VALUE_ACCESSOR, | ||
41 | + useExisting: forwardRef(() => TimezoneSelectComponent), | ||
42 | + multi: true | ||
43 | + }] | ||
44 | +}) | ||
45 | +export class TimezoneSelectComponent implements ControlValueAccessor, OnInit, AfterViewInit { | ||
46 | + | ||
47 | + selectTimezoneFormGroup: FormGroup; | ||
48 | + | ||
49 | + modelValue: string | null; | ||
50 | + | ||
51 | + defaultTimezoneId: string = null; | ||
52 | + | ||
53 | + defaultTimezoneInfo: TimezoneInfo = null; | ||
54 | + | ||
55 | + timezones: TimezoneInfo[] = _moment.tz.names().map((zoneName) => { | ||
56 | + const tz = _moment.tz(zoneName); | ||
57 | + return { | ||
58 | + id: zoneName, | ||
59 | + name: zoneName.replace(/_/g, ' '), | ||
60 | + offset: `UTC${tz.format('Z')}`, | ||
61 | + nOffset: tz.utcOffset() | ||
62 | + } | ||
63 | + }); | ||
64 | + | ||
65 | + @Input() | ||
66 | + set defaultTimezone(timezone: string) { | ||
67 | + if (this.defaultTimezoneId !== timezone) { | ||
68 | + this.defaultTimezoneId = timezone; | ||
69 | + if (this.defaultTimezoneId) { | ||
70 | + this.defaultTimezoneInfo = | ||
71 | + this.timezones.find((timezoneInfo) => timezoneInfo.id === this.defaultTimezoneId); | ||
72 | + } else { | ||
73 | + this.defaultTimezoneInfo = null; | ||
74 | + } | ||
75 | + } | ||
76 | + } | ||
77 | + | ||
78 | + private requiredValue: boolean; | ||
79 | + get required(): boolean { | ||
80 | + return this.requiredValue; | ||
81 | + } | ||
82 | + @Input() | ||
83 | + set required(value: boolean) { | ||
84 | + this.requiredValue = coerceBooleanProperty(value); | ||
85 | + } | ||
86 | + | ||
87 | + @Input() | ||
88 | + disabled: boolean; | ||
89 | + | ||
90 | + @ViewChild('timezoneInput', {static: true, read: MatAutocompleteTrigger}) timezoneInputTrigger: MatAutocompleteTrigger; | ||
91 | + | ||
92 | + filteredTimezones: Observable<Array<TimezoneInfo>>; | ||
93 | + | ||
94 | + searchText = ''; | ||
95 | + | ||
96 | + ignoreClosePanel = false; | ||
97 | + | ||
98 | + private dirty = false; | ||
99 | + | ||
100 | + private propagateChange = (v: any) => { }; | ||
101 | + | ||
102 | + constructor(private store: Store<AppState>, | ||
103 | + public translate: TranslateService, | ||
104 | + private ngZone: NgZone, | ||
105 | + private fb: FormBuilder) { | ||
106 | + this.selectTimezoneFormGroup = this.fb.group({ | ||
107 | + timezone: [null] | ||
108 | + }); | ||
109 | + } | ||
110 | + | ||
111 | + registerOnChange(fn: any): void { | ||
112 | + this.propagateChange = fn; | ||
113 | + } | ||
114 | + | ||
115 | + registerOnTouched(fn: any): void { | ||
116 | + } | ||
117 | + | ||
118 | + ngOnInit() { | ||
119 | + this.filteredTimezones = this.selectTimezoneFormGroup.get('timezone').valueChanges | ||
120 | + .pipe( | ||
121 | + tap(value => { | ||
122 | + let modelValue; | ||
123 | + if (typeof value === 'string' || !value) { | ||
124 | + modelValue = null; | ||
125 | + } else { | ||
126 | + modelValue = value.id; | ||
127 | + } | ||
128 | + this.updateView(modelValue); | ||
129 | + if (value === null) { | ||
130 | + this.clear(); | ||
131 | + } | ||
132 | + }), | ||
133 | + map(value => value ? (typeof value === 'string' ? value : value.name) : ''), | ||
134 | + mergeMap(name => this.fetchTimezones(name) ), | ||
135 | + share() | ||
136 | + ); | ||
137 | + } | ||
138 | + | ||
139 | + ngAfterViewInit(): void { | ||
140 | + } | ||
141 | + | ||
142 | + setDisabledState(isDisabled: boolean): void { | ||
143 | + this.disabled = isDisabled; | ||
144 | + if (this.disabled) { | ||
145 | + this.selectTimezoneFormGroup.disable({emitEvent: false}); | ||
146 | + } else { | ||
147 | + this.selectTimezoneFormGroup.enable({emitEvent: false}); | ||
148 | + } | ||
149 | + } | ||
150 | + | ||
151 | + writeValue(value: string | null): void { | ||
152 | + this.searchText = ''; | ||
153 | + let foundTimezone: TimezoneInfo = null; | ||
154 | + if (value !== null) { | ||
155 | + foundTimezone = this.timezones.find(timezoneInfo => timezoneInfo.id === value); | ||
156 | + } | ||
157 | + if (foundTimezone !== null) { | ||
158 | + this.modelValue = value; | ||
159 | + this.selectTimezoneFormGroup.get('timezone').patchValue(foundTimezone, {emitEvent: false}); | ||
160 | + } else { | ||
161 | + if (this.defaultTimezoneInfo) { | ||
162 | + this.selectTimezoneFormGroup.get('timezone').patchValue(this.defaultTimezoneInfo, {emitEvent: false}); | ||
163 | + setTimeout(() => { | ||
164 | + this.updateView(this.defaultTimezoneInfo.id); | ||
165 | + }, 0); | ||
166 | + } else { | ||
167 | + this.modelValue = null; | ||
168 | + this.selectTimezoneFormGroup.get('timezone').patchValue('', {emitEvent: false}); | ||
169 | + } | ||
170 | + } | ||
171 | + this.dirty = true; | ||
172 | + } | ||
173 | + | ||
174 | + onFocus() { | ||
175 | + if (this.dirty) { | ||
176 | + this.selectTimezoneFormGroup.get('timezone').updateValueAndValidity({onlySelf: true, emitEvent: true}); | ||
177 | + this.dirty = false; | ||
178 | + } | ||
179 | + } | ||
180 | + | ||
181 | + onPanelClosed() { | ||
182 | + if (this.ignoreClosePanel) { | ||
183 | + this.ignoreClosePanel = false; | ||
184 | + } else { | ||
185 | + if (!this.modelValue && this.defaultTimezoneInfo) { | ||
186 | + this.ngZone.run(() => { | ||
187 | + this.selectTimezoneFormGroup.get('timezone').reset(this.defaultTimezoneInfo, {emitEvent: true}); | ||
188 | + }); | ||
189 | + } | ||
190 | + } | ||
191 | + } | ||
192 | + | ||
193 | + updateView(value: string | null) { | ||
194 | + if (this.modelValue !== value) { | ||
195 | + this.modelValue = value; | ||
196 | + this.propagateChange(this.modelValue); | ||
197 | + } | ||
198 | + } | ||
199 | + | ||
200 | + displayTimezoneFn(timezone?: TimezoneInfo): string | undefined { | ||
201 | + return timezone ? `${timezone.name} (${timezone.offset})` : undefined; | ||
202 | + } | ||
203 | + | ||
204 | + fetchTimezones(searchText?: string): Observable<Array<TimezoneInfo>> { | ||
205 | + this.searchText = searchText; | ||
206 | + let result = this.timezones; | ||
207 | + if (searchText && searchText.length) { | ||
208 | + result = this.timezones.filter((timezoneInfo) => | ||
209 | + timezoneInfo.name.toLowerCase().includes(searchText.toLowerCase())); | ||
210 | + } | ||
211 | + return of(result); | ||
212 | + } | ||
213 | + | ||
214 | + clear() { | ||
215 | + this.selectTimezoneFormGroup.get('timezone').patchValue('', {emitEvent: true}); | ||
216 | + setTimeout(() => { | ||
217 | + this.timezoneInputTrigger.openPanel(); | ||
218 | + }, 0); | ||
219 | + } | ||
220 | + | ||
221 | +} |
@@ -236,9 +236,40 @@ export interface AlarmCondition { | @@ -236,9 +236,40 @@ export interface AlarmCondition { | ||
236 | spec?: AlarmConditionSpec; | 236 | spec?: AlarmConditionSpec; |
237 | } | 237 | } |
238 | 238 | ||
239 | +export enum AlarmScheduleType { | ||
240 | + ANY_TIME = 'ANY_TIME', | ||
241 | + SPECIFIC_TIME = 'SPECIFIC_TIME', | ||
242 | + CUSTOM = 'CUSTOM' | ||
243 | +} | ||
244 | + | ||
245 | +export const AlarmScheduleTypeTranslationMap = new Map<AlarmScheduleType, string>( | ||
246 | + [ | ||
247 | + [AlarmScheduleType.ANY_TIME, 'device-profile.schedule-any-time'], | ||
248 | + [AlarmScheduleType.SPECIFIC_TIME, 'device-profile.schedule-specific-time'], | ||
249 | + [AlarmScheduleType.CUSTOM, 'device-profile.schedule-custom'] | ||
250 | + ] | ||
251 | +); | ||
252 | + | ||
253 | +export interface AlarmSchedule{ | ||
254 | + type: AlarmScheduleType; | ||
255 | + timezone?: string; | ||
256 | + daysOfWeek?: number[]; | ||
257 | + startsOn?: number; | ||
258 | + endsOn?: number; | ||
259 | + items?: CustomTimeSchedulerItem[]; | ||
260 | +} | ||
261 | + | ||
262 | +export interface CustomTimeSchedulerItem{ | ||
263 | + enabled: boolean; | ||
264 | + dayOfWeek: number; | ||
265 | + startsOn: number; | ||
266 | + endsOn: number; | ||
267 | +} | ||
268 | + | ||
239 | export interface AlarmRule { | 269 | export interface AlarmRule { |
240 | condition: AlarmCondition; | 270 | condition: AlarmCondition; |
241 | alarmDetails?: string; | 271 | alarmDetails?: string; |
272 | + schedule?: AlarmSchedule; | ||
242 | } | 273 | } |
243 | 274 | ||
244 | export interface DeviceProfileAlarm { | 275 | export interface DeviceProfileAlarm { |
@@ -467,7 +467,6 @@ export const defaultTimeIntervals = new Array<TimeInterval>( | @@ -467,7 +467,6 @@ export const defaultTimeIntervals = new Array<TimeInterval>( | ||
467 | ); | 467 | ); |
468 | 468 | ||
469 | export enum TimeUnit { | 469 | export enum TimeUnit { |
470 | - MILLISECONDS = 'MILLISECONDS', | ||
471 | SECONDS = 'SECONDS', | 470 | SECONDS = 'SECONDS', |
472 | MINUTES = 'MINUTES', | 471 | MINUTES = 'MINUTES', |
473 | HOURS = 'HOURS', | 472 | HOURS = 'HOURS', |
@@ -476,7 +475,6 @@ export enum TimeUnit { | @@ -476,7 +475,6 @@ export enum TimeUnit { | ||
476 | 475 | ||
477 | export const timeUnitTranslationMap = new Map<TimeUnit, string>( | 476 | export const timeUnitTranslationMap = new Map<TimeUnit, string>( |
478 | [ | 477 | [ |
479 | - [TimeUnit.MILLISECONDS, 'timeunit.milliseconds'], | ||
480 | [TimeUnit.SECONDS, 'timeunit.seconds'], | 478 | [TimeUnit.SECONDS, 'timeunit.seconds'], |
481 | [TimeUnit.MINUTES, 'timeunit.minutes'], | 479 | [TimeUnit.MINUTES, 'timeunit.minutes'], |
482 | [TimeUnit.HOURS, 'timeunit.hours'], | 480 | [TimeUnit.HOURS, 'timeunit.hours'], |
@@ -134,6 +134,7 @@ import { HistorySelectorComponent } from './components/time/history-selector/his | @@ -134,6 +134,7 @@ import { HistorySelectorComponent } from './components/time/history-selector/his | ||
134 | import { EntityGatewaySelectComponent } from '@shared/components/entity/entity-gateway-select.component'; | 134 | import { EntityGatewaySelectComponent } from '@shared/components/entity/entity-gateway-select.component'; |
135 | import { QueueTypeListComponent } from '@shared/components/queue/queue-type-list.component'; | 135 | import { QueueTypeListComponent } from '@shared/components/queue/queue-type-list.component'; |
136 | import { ContactComponent } from '@shared/components/contact.component'; | 136 | import { ContactComponent } from '@shared/components/contact.component'; |
137 | +import { TimezoneSelectComponent } from '@shared/components/time/timezone-select.component'; | ||
137 | 138 | ||
138 | @NgModule({ | 139 | @NgModule({ |
139 | providers: [ | 140 | providers: [ |
@@ -172,6 +173,7 @@ import { ContactComponent } from '@shared/components/contact.component'; | @@ -172,6 +173,7 @@ import { ContactComponent } from '@shared/components/contact.component'; | ||
172 | DashboardSelectPanelComponent, | 173 | DashboardSelectPanelComponent, |
173 | DatetimePeriodComponent, | 174 | DatetimePeriodComponent, |
174 | DatetimeComponent, | 175 | DatetimeComponent, |
176 | + TimezoneSelectComponent, | ||
175 | ValueInputComponent, | 177 | ValueInputComponent, |
176 | DashboardAutocompleteComponent, | 178 | DashboardAutocompleteComponent, |
177 | EntitySubTypeAutocompleteComponent, | 179 | EntitySubTypeAutocompleteComponent, |
@@ -292,6 +294,7 @@ import { ContactComponent } from '@shared/components/contact.component'; | @@ -292,6 +294,7 @@ import { ContactComponent } from '@shared/components/contact.component'; | ||
292 | DashboardSelectComponent, | 294 | DashboardSelectComponent, |
293 | DatetimePeriodComponent, | 295 | DatetimePeriodComponent, |
294 | DatetimeComponent, | 296 | DatetimeComponent, |
297 | + TimezoneSelectComponent, | ||
295 | DashboardAutocompleteComponent, | 298 | DashboardAutocompleteComponent, |
296 | EntitySubTypeAutocompleteComponent, | 299 | EntitySubTypeAutocompleteComponent, |
297 | EntitySubTypeSelectComponent, | 300 | EntitySubTypeSelectComponent, |
@@ -856,7 +856,25 @@ | @@ -856,7 +856,25 @@ | ||
856 | "condition-repeating-value-range": "Count of events should be in a range from 1 to 2147483647.", | 856 | "condition-repeating-value-range": "Count of events should be in a range from 1 to 2147483647.", |
857 | "condition-repeating-value-pattern": "Count of events should be integers.", | 857 | "condition-repeating-value-pattern": "Count of events should be integers.", |
858 | "condition-repeating-value-required": "Count of events is required.", | 858 | "condition-repeating-value-required": "Count of events is required.", |
859 | - "schedule": "Schedule" | 859 | + "schedule-type": "Scheduler type", |
860 | + "schedule-type-required": "Scheduler type is required.", | ||
861 | + "schedule": "Schedule", | ||
862 | + "schedule-any-time": "Active all the time", | ||
863 | + "schedule-specific-time": "Active at a specific time", | ||
864 | + "schedule-custom": "Custom", | ||
865 | + "schedule-day": { | ||
866 | + "monday": "Monday", | ||
867 | + "tuesday": "Tuesday", | ||
868 | + "wednesday": "Wednesday", | ||
869 | + "thursday": "Thursday", | ||
870 | + "friday": "Friday", | ||
871 | + "saturday": "Saturday", | ||
872 | + "sunday": "Sunday" | ||
873 | + }, | ||
874 | + "schedule-days": "Days", | ||
875 | + "schedule-time": "Time", | ||
876 | + "schedule-time-from": "From", | ||
877 | + "schedule-time-to": "To" | ||
860 | }, | 878 | }, |
861 | "dialog": { | 879 | "dialog": { |
862 | "close": "Close dialog" | 880 | "close": "Close dialog" |
@@ -1742,6 +1760,12 @@ | @@ -1742,6 +1760,12 @@ | ||
1742 | "help": "Help", | 1760 | "help": "Help", |
1743 | "reset-debug-mode": "Reset debug mode in all nodes" | 1761 | "reset-debug-mode": "Reset debug mode in all nodes" |
1744 | }, | 1762 | }, |
1763 | + "timezone": { | ||
1764 | + "timezone": "Timezone", | ||
1765 | + "select-timezone": "Select timezone", | ||
1766 | + "no-timezones-matching": "No timezones matching '{{timezone}}' were found.", | ||
1767 | + "timezone-required": "Timezone is required." | ||
1768 | + }, | ||
1745 | "queue": { | 1769 | "queue": { |
1746 | "select_name": "Select queue name", | 1770 | "select_name": "Select queue name", |
1747 | "name": "Queue Name", | 1771 | "name": "Queue Name", |
@@ -1821,7 +1845,6 @@ | @@ -1821,7 +1845,6 @@ | ||
1821 | "advanced": "Advanced" | 1845 | "advanced": "Advanced" |
1822 | }, | 1846 | }, |
1823 | "timeunit": { | 1847 | "timeunit": { |
1824 | - "milliseconds": "Milliseconds", | ||
1825 | "seconds": "Seconds", | 1848 | "seconds": "Seconds", |
1826 | "minutes": "Minutes", | 1849 | "minutes": "Minutes", |
1827 | "hours": "Hours", | 1850 | "hours": "Hours", |
@@ -1428,6 +1428,13 @@ | @@ -1428,6 +1428,13 @@ | ||
1428 | resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" | 1428 | resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" |
1429 | integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA== | 1429 | integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA== |
1430 | 1430 | ||
1431 | +"@types/moment-timezone@^0.5.30": | ||
1432 | + version "0.5.30" | ||
1433 | + resolved "https://registry.yarnpkg.com/@types/moment-timezone/-/moment-timezone-0.5.30.tgz#340ed45fe3e715f4a011f5cfceb7cb52aad46fc7" | ||
1434 | + integrity sha512-aDVfCsjYnAQaV/E9Qc24C5Njx1CoDjXsEgkxtp9NyXDpYu4CCbmclb6QhWloS9UTU/8YROUEEdEkWI0D7DxnKg== | ||
1435 | + dependencies: | ||
1436 | + moment-timezone "*" | ||
1437 | + | ||
1431 | "@types/mousetrap@^1.6.0": | 1438 | "@types/mousetrap@^1.6.0": |
1432 | version "1.6.3" | 1439 | version "1.6.3" |
1433 | resolved "https://registry.yarnpkg.com/@types/mousetrap/-/mousetrap-1.6.3.tgz#3159a01a2b21c9155a3d8f85588885d725dc987d" | 1440 | resolved "https://registry.yarnpkg.com/@types/mousetrap/-/mousetrap-1.6.3.tgz#3159a01a2b21c9155a3d8f85588885d725dc987d" |
@@ -6289,6 +6296,18 @@ mkdirp@^1.0.3, mkdirp@^1.0.4: | @@ -6289,6 +6296,18 @@ mkdirp@^1.0.3, mkdirp@^1.0.4: | ||
6289 | resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" | 6296 | resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" |
6290 | integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== | 6297 | integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== |
6291 | 6298 | ||
6299 | +moment-timezone@*, moment-timezone@^0.5.31: | ||
6300 | + version "0.5.31" | ||
6301 | + resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.31.tgz#9c40d8c5026f0c7ab46eda3d63e49c155148de05" | ||
6302 | + integrity sha512-+GgHNg8xRhMXfEbv81iDtrVeTcWt0kWmTEY1XQK14dICTXnWJnT0dxdlPspwqF3keKMVPXwayEsk1DI0AA/jdA== | ||
6303 | + dependencies: | ||
6304 | + moment ">= 2.9.0" | ||
6305 | + | ||
6306 | +"moment@>= 2.9.0": | ||
6307 | + version "2.29.0" | ||
6308 | + resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.0.tgz#fcbef955844d91deb55438613ddcec56e86a3425" | ||
6309 | + integrity sha512-z6IJ5HXYiuxvFTI6eiQ9dm77uE0gyy1yXNApVHqTcnIKfY9tIwEjlzsZ6u1LQXvVgKeTnv9Xm7NDvJ7lso3MtA== | ||
6310 | + | ||
6292 | moment@^2.27.0: | 6311 | moment@^2.27.0: |
6293 | version "2.27.0" | 6312 | version "2.27.0" |
6294 | resolved "https://registry.yarnpkg.com/moment/-/moment-2.27.0.tgz#8bff4e3e26a236220dfe3e36de756b6ebaa0105d" | 6313 | resolved "https://registry.yarnpkg.com/moment/-/moment-2.27.0.tgz#8bff4e3e26a236220dfe3e36de756b6ebaa0105d" |