Commit fd602dec7f2fd754fde946b0727adebb3c2a093e

Authored by Vladyslav_Prykhodko
1 parent 1f5f411b

UI: Added device profile schedule setting for alarm setting

... ... @@ -478,7 +478,7 @@ spring:
478 478 database-platform: "${SPRING_JPA_DATABASE_PLATFORM:org.hibernate.dialect.PostgreSQLDialect}"
479 479 datasource:
480 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 482 username: "${SPRING_DATASOURCE_USERNAME:postgres}"
483 483 password: "${SPRING_DATASOURCE_PASSWORD:postgres}"
484 484 hikari:
... ...
... ... @@ -137,7 +137,8 @@
137 137 "react-is",
138 138 "hoist-non-react-statics",
139 139 "classnames",
140   - "raf"
  140 + "raf",
  141 + "moment-timezone"
141 142 ]
142 143 },
143 144 "configurations": {
... ... @@ -248,4 +249,4 @@
248 249 "cli": {
249 250 "packageManager": "yarn"
250 251 }
251   -}
\ No newline at end of file
  252 +}
... ...
... ... @@ -63,6 +63,7 @@
63 63 "material-design-icons": "^3.0.1",
64 64 "messageformat": "^2.3.0",
65 65 "moment": "^2.27.0",
  66 + "moment-timezone": "^0.5.31",
66 67 "ngx-clipboard": "^13.0.1",
67 68 "ngx-color-picker": "^10.0.1",
68 69 "ngx-daterangepicker-material": "^4.0.1",
... ... @@ -109,6 +110,7 @@
109 110 "@types/leaflet-polylinedecorator": "^1.6.0",
110 111 "@types/leaflet.markercluster": "^1.4.2",
111 112 "@types/lodash": "^4.14.159",
  113 + "@types/moment-timezone": "^0.5.30",
112 114 "@types/raphael": "^2.3.0",
113 115 "@types/react": "^16.9.46",
114 116 "@types/react-dom": "^16.9.8",
... ...
... ... @@ -107,6 +107,7 @@ import { AlarmRuleKeyFiltersDialogComponent } from './profile/alarm/alarm-rule-k
107 107 import { FilterTextComponent } from './filter/filter-text.component';
108 108 import { AddDeviceProfileDialogComponent } from './profile/add-device-profile-dialog.component';
109 109 import { RuleChainAutocompleteComponent } from './rule-chain/rule-chain-autocomplete.component';
  110 +import { AlarmScheduleComponent } from './profile/alarm/alarm-schedule.component';
110 111
111 112 @NgModule({
112 113 declarations:
... ... @@ -196,7 +197,8 @@ import { RuleChainAutocompleteComponent } from './rule-chain/rule-chain-autocomp
196 197 DeviceProfileComponent,
197 198 DeviceProfileDialogComponent,
198 199 AddDeviceProfileDialogComponent,
199   - RuleChainAutocompleteComponent
  200 + RuleChainAutocompleteComponent,
  201 + AlarmScheduleComponent
200 202 ],
201 203 imports: [
202 204 CommonModule,
... ... @@ -275,7 +277,8 @@ import { RuleChainAutocompleteComponent } from './rule-chain/rule-chain-autocomp
275 277 DeviceProfileComponent,
276 278 DeviceProfileDialogComponent,
277 279 AddDeviceProfileDialogComponent,
278   - RuleChainAutocompleteComponent
  280 + RuleChainAutocompleteComponent,
  281 + AlarmScheduleComponent
279 282 ],
280 283 providers: [
281 284 WidgetComponentService,
... ...
... ... @@ -93,7 +93,9 @@
93 93 </section>
94 94 </mat-tab>
95 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 99 </mat-tab>
98 100 <mat-tab label="{{ 'device-profile.alarm-rule-details' | translate }}">
99 101 <mat-form-field class="mat-block row">
... ...
... ... @@ -95,6 +95,7 @@ export class AlarmRuleComponent implements ControlValueAccessor, OnInit, Validat
95 95 count: [{value: null, disable: true}, [Validators.required, Validators.min(1), Validators.max(2147483647), Validators.pattern('[0-9]*')]]
96 96 })
97 97 }, Validators.required),
  98 + schedule: [null],
98 99 alarmDetails: [null]
99 100 });
100 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 34 {{ 'device-profile.alarm-severity-required' | translate }}
35 35 </mat-error>
36 36 </mat-form-field>
  37 + <mat-divider></mat-divider>
37 38 <tb-alarm-rule formControlName="alarmRule" required fxFlex>
38 39 </tb-alarm-rule>
39 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 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 269 export interface AlarmRule {
240 270 condition: AlarmCondition;
241 271 alarmDetails?: string;
  272 + schedule?: AlarmSchedule;
242 273 }
243 274
244 275 export interface DeviceProfileAlarm {
... ...
... ... @@ -467,7 +467,6 @@ export const defaultTimeIntervals = new Array<TimeInterval>(
467 467 );
468 468
469 469 export enum TimeUnit {
470   - MILLISECONDS = 'MILLISECONDS',
471 470 SECONDS = 'SECONDS',
472 471 MINUTES = 'MINUTES',
473 472 HOURS = 'HOURS',
... ... @@ -476,7 +475,6 @@ export enum TimeUnit {
476 475
477 476 export const timeUnitTranslationMap = new Map<TimeUnit, string>(
478 477 [
479   - [TimeUnit.MILLISECONDS, 'timeunit.milliseconds'],
480 478 [TimeUnit.SECONDS, 'timeunit.seconds'],
481 479 [TimeUnit.MINUTES, 'timeunit.minutes'],
482 480 [TimeUnit.HOURS, 'timeunit.hours'],
... ...
... ... @@ -134,6 +134,7 @@ import { HistorySelectorComponent } from './components/time/history-selector/his
134 134 import { EntityGatewaySelectComponent } from '@shared/components/entity/entity-gateway-select.component';
135 135 import { QueueTypeListComponent } from '@shared/components/queue/queue-type-list.component';
136 136 import { ContactComponent } from '@shared/components/contact.component';
  137 +import { TimezoneSelectComponent } from '@shared/components/time/timezone-select.component';
137 138
138 139 @NgModule({
139 140 providers: [
... ... @@ -172,6 +173,7 @@ import { ContactComponent } from '@shared/components/contact.component';
172 173 DashboardSelectPanelComponent,
173 174 DatetimePeriodComponent,
174 175 DatetimeComponent,
  176 + TimezoneSelectComponent,
175 177 ValueInputComponent,
176 178 DashboardAutocompleteComponent,
177 179 EntitySubTypeAutocompleteComponent,
... ... @@ -292,6 +294,7 @@ import { ContactComponent } from '@shared/components/contact.component';
292 294 DashboardSelectComponent,
293 295 DatetimePeriodComponent,
294 296 DatetimeComponent,
  297 + TimezoneSelectComponent,
295 298 DashboardAutocompleteComponent,
296 299 EntitySubTypeAutocompleteComponent,
297 300 EntitySubTypeSelectComponent,
... ...
... ... @@ -856,7 +856,25 @@
856 856 "condition-repeating-value-range": "Count of events should be in a range from 1 to 2147483647.",
857 857 "condition-repeating-value-pattern": "Count of events should be integers.",
858 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 879 "dialog": {
862 880 "close": "Close dialog"
... ... @@ -1742,6 +1760,12 @@
1742 1760 "help": "Help",
1743 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 1769 "queue": {
1746 1770 "select_name": "Select queue name",
1747 1771 "name": "Queue Name",
... ... @@ -1821,7 +1845,6 @@
1821 1845 "advanced": "Advanced"
1822 1846 },
1823 1847 "timeunit": {
1824   - "milliseconds": "Milliseconds",
1825 1848 "seconds": "Seconds",
1826 1849 "minutes": "Minutes",
1827 1850 "hours": "Hours",
... ...
... ... @@ -1428,6 +1428,13 @@
1428 1428 resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d"
1429 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 1438 "@types/mousetrap@^1.6.0":
1432 1439 version "1.6.3"
1433 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 6296 resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
6290 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 6311 moment@^2.27.0:
6293 6312 version "2.27.0"
6294 6313 resolved "https://registry.yarnpkg.com/moment/-/moment-2.27.0.tgz#8bff4e3e26a236220dfe3e36de756b6ebaa0105d"
... ...