Browse Source

UI: Added device profile schedule setting for alarm setting

pull/3551/head
Vladyslav_Prykhodko 6 years ago
parent
commit
fd602dec7f
  1. 2
      application/src/main/resources/thingsboard.yml
  2. 5
      ui-ngx/angular.json
  3. 2
      ui-ngx/package.json
  4. 7
      ui-ngx/src/app/modules/home/components/home-components.module.ts
  5. 4
      ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.html
  6. 1
      ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.ts
  7. 224
      ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule.component.html
  8. 259
      ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule.component.ts
  9. 1
      ui-ngx/src/app/modules/home/components/profile/alarm/create-alarm-rules.component.html
  10. 50
      ui-ngx/src/app/shared/components/time/timezone-select.component.html
  11. 221
      ui-ngx/src/app/shared/components/time/timezone-select.component.ts
  12. 31
      ui-ngx/src/app/shared/models/device.models.ts
  13. 2
      ui-ngx/src/app/shared/models/time/time.models.ts
  14. 3
      ui-ngx/src/app/shared/shared.module.ts
  15. 27
      ui-ngx/src/assets/locale/locale.constant-en_US.json
  16. 19
      ui-ngx/yarn.lock

2
application/src/main/resources/thingsboard.yml

@ -478,7 +478,7 @@ spring:
database-platform: "${SPRING_JPA_DATABASE_PLATFORM:org.hibernate.dialect.PostgreSQLDialect}"
datasource:
driverClassName: "${SPRING_DRIVER_CLASS_NAME:org.postgresql.Driver}"
url: "${SPRING_DATASOURCE_URL:jdbc:postgresql://localhost:5432/thingsboard}"
url: "${SPRING_DATASOURCE_URL:jdbc:postgresql://localhost:5432/thingsboard_32}"
username: "${SPRING_DATASOURCE_USERNAME:postgres}"
password: "${SPRING_DATASOURCE_PASSWORD:postgres}"
hikari:

5
ui-ngx/angular.json

@ -137,7 +137,8 @@
"react-is",
"hoist-non-react-statics",
"classnames",
"raf"
"raf",
"moment-timezone"
]
},
"configurations": {
@ -248,4 +249,4 @@
"cli": {
"packageManager": "yarn"
}
}
}

2
ui-ngx/package.json

@ -63,6 +63,7 @@
"material-design-icons": "^3.0.1",
"messageformat": "^2.3.0",
"moment": "^2.27.0",
"moment-timezone": "^0.5.31",
"ngx-clipboard": "^13.0.1",
"ngx-color-picker": "^10.0.1",
"ngx-daterangepicker-material": "^4.0.1",
@ -109,6 +110,7 @@
"@types/leaflet-polylinedecorator": "^1.6.0",
"@types/leaflet.markercluster": "^1.4.2",
"@types/lodash": "^4.14.159",
"@types/moment-timezone": "^0.5.30",
"@types/raphael": "^2.3.0",
"@types/react": "^16.9.46",
"@types/react-dom": "^16.9.8",

7
ui-ngx/src/app/modules/home/components/home-components.module.ts

@ -107,6 +107,7 @@ import { AlarmRuleKeyFiltersDialogComponent } from './profile/alarm/alarm-rule-k
import { FilterTextComponent } from './filter/filter-text.component';
import { AddDeviceProfileDialogComponent } from './profile/add-device-profile-dialog.component';
import { RuleChainAutocompleteComponent } from './rule-chain/rule-chain-autocomplete.component';
import { AlarmScheduleComponent } from './profile/alarm/alarm-schedule.component';
@NgModule({
declarations:
@ -196,7 +197,8 @@ import { RuleChainAutocompleteComponent } from './rule-chain/rule-chain-autocomp
DeviceProfileComponent,
DeviceProfileDialogComponent,
AddDeviceProfileDialogComponent,
RuleChainAutocompleteComponent
RuleChainAutocompleteComponent,
AlarmScheduleComponent
],
imports: [
CommonModule,
@ -275,7 +277,8 @@ import { RuleChainAutocompleteComponent } from './rule-chain/rule-chain-autocomp
DeviceProfileComponent,
DeviceProfileDialogComponent,
AddDeviceProfileDialogComponent,
RuleChainAutocompleteComponent
RuleChainAutocompleteComponent,
AlarmScheduleComponent
],
providers: [
WidgetComponentService,

4
ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.html

@ -93,7 +93,9 @@
</section>
</mat-tab>
<mat-tab label="{{ 'device-profile.schedule' | translate }}">
<div class="row">{{ 'device-profile.schedule' | translate }}</div>
<tb-alarm-schedule fxFlex class="row"
formControlName="schedule">
</tb-alarm-schedule>
</mat-tab>
<mat-tab label="{{ 'device-profile.alarm-rule-details' | translate }}">
<mat-form-field class="mat-block row">

1
ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.ts

@ -95,6 +95,7 @@ export class AlarmRuleComponent implements ControlValueAccessor, OnInit, Validat
count: [{value: null, disable: true}, [Validators.required, Validators.min(1), Validators.max(2147483647), Validators.pattern('[0-9]*')]]
})
}, Validators.required),
schedule: [null],
alarmDetails: [null]
});
this.alarmRuleFormGroup.get('condition.spec.type').valueChanges.subscribe((type) => {

224
ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule.component.html

@ -0,0 +1,224 @@
<!--
Copyright © 2016-2020 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<section [formGroup]="alarmScheduleForm" fxLayout="column">
<mat-form-field class="mat-block" hideRequiredMarker floatLabel="always">
<mat-label> </mat-label>
<mat-select formControlName="type" required placeholder="{{ 'device-profile.schedule-type' | translate }}">
<mat-option *ngFor="let alarmScheduleType of alarmScheduleTypes" [value]="alarmScheduleType">
{{ alarmScheduleTypeTranslate.get(alarmScheduleType) | translate }}
</mat-option>
</mat-select>
<mat-error *ngIf="alarmScheduleForm.get('type').hasError('required')">
{{ 'device-profile.schedule-type-required' | translate }}
</mat-error>
</mat-form-field>
<div *ngIf="alarmScheduleForm.get('type').value !== alarmScheduleType.ANY_TIME">
<tb-timezone-select
[defaultTimezone]="defaultTimezone"
required
formControlName="timezone">
</tb-timezone-select>
<section *ngIf="alarmScheduleForm.get('type').value === alarmScheduleType.SPECIFIC_TIME">
<div class="tb-small" style="margin-bottom: 0.5em" translate>device-profile.schedule-days</div>
<div fxLayout="column" fxLayout.gt-sm="row" fxLayoutGap="16px" style="padding-bottom: 16px;">
<div fxLayout="row" fxLayoutGap="16px">
<mat-checkbox [formControl]="weeklyRepeatControl(0)">
{{ 'device-profile.schedule-day.monday' | translate }}
</mat-checkbox>
<mat-checkbox [formControl]="weeklyRepeatControl(1)">
{{ 'device-profile.schedule-day.tuesday' | translate }}
</mat-checkbox>
<mat-checkbox [formControl]="weeklyRepeatControl(2)">
{{ 'device-profile.schedule-day.wednesday' | translate }}
</mat-checkbox>
<mat-checkbox [formControl]="weeklyRepeatControl(3)">
{{ 'device-profile.schedule-day.thursday' | translate }}
</mat-checkbox>
</div>
<div fxLayout="row" fxLayoutGap="16px">
<mat-checkbox [formControl]="weeklyRepeatControl(4)">
{{ 'device-profile.schedule-day.friday' | translate }}
</mat-checkbox>
<mat-checkbox [formControl]="weeklyRepeatControl(5)">
{{ 'device-profile.schedule-day.saturday' | translate }}
</mat-checkbox>
<mat-checkbox [formControl]="weeklyRepeatControl(6)">
{{ 'device-profile.schedule-day.sunday' | translate }}
</mat-checkbox>
</div>
</div>
<div class="tb-small" style="margin-bottom: 0.5em" translate>device-profile.schedule-time</div>
<div fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px">
<mat-form-field fxFlex>
<mat-label translate>device-profile.schedule-time-from</mat-label>
<mat-datetimepicker-toggle [for]="startTimePicker" matPrefix></mat-datetimepicker-toggle>
<mat-datetimepicker #startTimePicker type="time" openOnFocus="true"></mat-datetimepicker>
<input required matInput formControlName="startsOn" [matDatetimepicker]="startTimePicker">
</mat-form-field>
<mat-form-field fxFlex>
<mat-label translate>device-profile.schedule-time-to</mat-label>
<mat-datetimepicker-toggle [for]="endTimePicker" matPrefix></mat-datetimepicker-toggle>
<mat-datetimepicker #endTimePicker type="time" openOnFocus="true"></mat-datetimepicker>
<input required matInput formControlName="endsOn" [matDatetimepicker]="endTimePicker">
</mat-form-field>
</div>
</section>
<section *ngIf="alarmScheduleForm.get('type').value === alarmScheduleType.CUSTOM">
<div class="tb-small" style="margin-bottom: 0.5em" translate>device-profile.schedule-days</div>
<div fxLayout="column" fxLayout.gt-sm="row" fxLayoutGap.gt-sm="16px" formArrayName="items">
<div fxLayout="column" fxFlex fxFlex.gt-sm="50">
<div fxLayout="row" fxLayoutGap="8px" formGroupName="0" fxLayoutAlign="start center">
<mat-checkbox formControlName="enabled" fxFlex="40" (change)="changeCustomScheduler($event, 0)">
{{ 'device-profile.schedule-day.monday' | translate }}
</mat-checkbox>
<div fxLayout="row" fxLayoutGap="8px" fxFlex>
<mat-form-field fxFlex="100px">
<mat-label translate>device-profile.schedule-time-from</mat-label>
<mat-datetimepicker-toggle [for]="startTimePicker1" matPrefix></mat-datetimepicker-toggle>
<mat-datetimepicker #startTimePicker1 type="time" openOnFocus="true"></mat-datetimepicker>
<input required matInput formControlName="startsOn" [matDatetimepicker]="startTimePicker1">
</mat-form-field>
<mat-form-field fxFlex="100px">
<mat-label translate>device-profile.schedule-time-to</mat-label>
<mat-datetimepicker-toggle [for]="endTimePicker1" matPrefix></mat-datetimepicker-toggle>
<mat-datetimepicker #endTimePicker1 type="time" openOnFocus="true"></mat-datetimepicker>
<input required matInput formControlName="endsOn" [matDatetimepicker]="endTimePicker1">
</mat-form-field>
</div>
</div>
<div fxLayout="row" fxLayoutGap="8px" formGroupName="1" fxLayoutAlign="start center">
<mat-checkbox formControlName="enabled" fxFlex="40" (change)="changeCustomScheduler($event, 1)">
{{ 'device-profile.schedule-day.tuesday' | translate }}
</mat-checkbox>
<div fxLayout="row" fxLayoutGap="8px" fxFlex>
<mat-form-field fxFlex="100px">
<mat-label translate>device-profile.schedule-time-from</mat-label>
<mat-datetimepicker-toggle [for]="startTimePicker2" matPrefix></mat-datetimepicker-toggle>
<mat-datetimepicker #startTimePicker2 type="time" openOnFocus="true"></mat-datetimepicker>
<input required matInput formControlName="startsOn" [matDatetimepicker]="startTimePicker2">
</mat-form-field>
<mat-form-field fxFlex="100px">
<mat-label translate>device-profile.schedule-time-to</mat-label>
<mat-datetimepicker-toggle [for]="endTimePicker2" matPrefix></mat-datetimepicker-toggle>
<mat-datetimepicker #endTimePicker2 type="time" openOnFocus="true"></mat-datetimepicker>
<input required matInput formControlName="endsOn" [matDatetimepicker]="endTimePicker2">
</mat-form-field>
</div>
</div>
<div fxLayout="row" fxLayoutGap="8px" formGroupName="2" fxLayoutAlign="start center">
<mat-checkbox formControlName="enabled" fxFlex="40" (change)="changeCustomScheduler($event, 2)">
{{ 'device-profile.schedule-day.wednesday' | translate }}
</mat-checkbox>
<div fxLayout="row" fxLayoutGap="8px" fxFlex>
<mat-form-field fxFlex="100px">
<mat-label translate>device-profile.schedule-time-from</mat-label>
<mat-datetimepicker-toggle [for]="startTimePicker3" matPrefix></mat-datetimepicker-toggle>
<mat-datetimepicker #startTimePicker3 type="time" openOnFocus="true"></mat-datetimepicker>
<input required matInput formControlName="startsOn" [matDatetimepicker]="startTimePicker3">
</mat-form-field>
<mat-form-field fxFlex="100px">
<mat-label translate>device-profile.schedule-time-to</mat-label>
<mat-datetimepicker-toggle [for]="endTimePicker3" matPrefix></mat-datetimepicker-toggle>
<mat-datetimepicker #endTimePicker3 type="time" openOnFocus="true"></mat-datetimepicker>
<input required matInput formControlName="endsOn" [matDatetimepicker]="endTimePicker3">
</mat-form-field>
</div>
</div>
<div fxLayout="row" fxLayoutGap="8px" formGroupName="3" fxLayoutAlign="start center">
<mat-checkbox formControlName="enabled" fxFlex="40" (change)="changeCustomScheduler($event, 3)">
{{ 'device-profile.schedule-day.thursday' | translate }}
</mat-checkbox>
<div fxLayout="row" fxLayoutGap="8px" fxFlex>
<mat-form-field fxFlex="100px">
<mat-label translate>device-profile.schedule-time-from</mat-label>
<mat-datetimepicker-toggle [for]="startTimePicker4" matPrefix></mat-datetimepicker-toggle>
<mat-datetimepicker #startTimePicker4 type="time" openOnFocus="true"></mat-datetimepicker>
<input required matInput formControlName="startsOn" [matDatetimepicker]="startTimePicker4">
</mat-form-field>
<mat-form-field fxFlex="100px">
<mat-label translate>device-profile.schedule-time-to</mat-label>
<mat-datetimepicker-toggle [for]="endTimePicker4" matPrefix></mat-datetimepicker-toggle>
<mat-datetimepicker #endTimePicker4 type="time" openOnFocus="true"></mat-datetimepicker>
<input required matInput formControlName="endsOn" [matDatetimepicker]="endTimePicker4">
</mat-form-field>
</div>
</div>
</div>
<div fxLayout="column" fxFlex fxFlex.gt-sm="50">
<div fxLayout="row" fxLayoutGap="8px" formGroupName="4" fxLayoutAlign="start center">
<mat-checkbox formControlName="enabled" fxFlex="40" (change)="changeCustomScheduler($event, 4)">
{{ 'device-profile.schedule-day.friday' | translate }}
</mat-checkbox>
<div fxLayout="row" fxLayoutGap="8px" fxFlex>
<mat-form-field fxFlex="100px">
<mat-label translate>device-profile.schedule-time-from</mat-label>
<mat-datetimepicker-toggle [for]="startTimePicker5" matPrefix></mat-datetimepicker-toggle>
<mat-datetimepicker #startTimePicker5 type="time" openOnFocus="true"></mat-datetimepicker>
<input required matInput formControlName="startsOn" [matDatetimepicker]="startTimePicker5">
</mat-form-field>
<mat-form-field fxFlex="100px">
<mat-label translate>device-profile.schedule-time-to</mat-label>
<mat-datetimepicker-toggle [for]="endTimePicker5" matPrefix></mat-datetimepicker-toggle>
<mat-datetimepicker #endTimePicker5 type="time" openOnFocus="true"></mat-datetimepicker>
<input required matInput formControlName="endsOn" [matDatetimepicker]="endTimePicker5">
</mat-form-field>
</div>
</div>
<div fxLayout="row" fxLayoutGap="8px" formGroupName="5" fxLayoutAlign="start center">
<mat-checkbox formControlName="enabled" fxFlex="40" (change)="changeCustomScheduler($event, 5)">
{{ 'device-profile.schedule-day.saturday' | translate }}
</mat-checkbox>
<div fxLayout="row" fxLayoutGap="8px" fxFlex>
<mat-form-field fxFlex="100px">
<mat-label translate>device-profile.schedule-time-from</mat-label>
<mat-datetimepicker-toggle [for]="startTimePicker6" matPrefix></mat-datetimepicker-toggle>
<mat-datetimepicker #startTimePicker6 type="time" openOnFocus="true"></mat-datetimepicker>
<input required matInput formControlName="startsOn" [matDatetimepicker]="startTimePicker6">
</mat-form-field>
<mat-form-field fxFlex="100px">
<mat-label translate>device-profile.schedule-time-to</mat-label>
<mat-datetimepicker-toggle [for]="endTimePicker6" matPrefix></mat-datetimepicker-toggle>
<mat-datetimepicker #endTimePicker6 type="time" openOnFocus="true"></mat-datetimepicker>
<input required matInput formControlName="endsOn" [matDatetimepicker]="endTimePicker6">
</mat-form-field>
</div>
</div>
<div fxLayout="row" fxLayoutGap="8px" formGroupName="6" fxLayoutAlign="start center">
<mat-checkbox formControlName="enabled" fxFlex="40" (change)="changeCustomScheduler($event, 6)">
{{ 'device-profile.schedule-day.sunday' | translate }}
</mat-checkbox>
<div fxLayout="row" fxLayoutGap="8px" fxFlex>
<mat-form-field fxFlex="100px">
<mat-label translate>device-profile.schedule-time-from</mat-label>
<mat-datetimepicker-toggle [for]="startTimePicker7" matPrefix></mat-datetimepicker-toggle>
<mat-datetimepicker #startTimePicker7 type="time" openOnFocus="true"></mat-datetimepicker>
<input required matInput formControlName="startsOn" [matDatetimepicker]="startTimePicker7">
</mat-form-field>
<mat-form-field fxFlex="100px">
<mat-label translate>device-profile.schedule-time-to</mat-label>
<mat-datetimepicker-toggle [for]="endTimePicker7" matPrefix></mat-datetimepicker-toggle>
<mat-datetimepicker #endTimePicker7 type="time" openOnFocus="true"></mat-datetimepicker>
<input required matInput formControlName="endsOn" [matDatetimepicker]="endTimePicker7">
</mat-form-field>
</div>
</div>
</div>
</div>
</section>
</div>
</section>

259
ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule.component.ts

@ -0,0 +1,259 @@
///
/// Copyright © 2016-2020 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, forwardRef, Input, OnInit } from '@angular/core';
import {
ControlValueAccessor,
FormArray,
FormBuilder,
FormControl,
FormGroup,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
ValidationErrors,
Validator,
Validators
} from '@angular/forms';
import { AlarmSchedule, AlarmScheduleType, AlarmScheduleTypeTranslationMap } from '@shared/models/device.models';
import { isDefined, isDefinedAndNotNull } from '@core/utils';
import * as _moment from 'moment-timezone';
import { MatCheckboxChange } from '@angular/material/checkbox';
@Component({
selector: 'tb-alarm-schedule',
templateUrl: './alarm-schedule.component.html',
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => AlarmScheduleComponent),
multi: true
}, {
provide: NG_VALIDATORS,
useExisting: forwardRef(() => AlarmScheduleComponent),
multi: true
}]
})
export class AlarmScheduleComponent implements ControlValueAccessor, Validator, OnInit {
@Input()
disabled: boolean;
alarmScheduleForm: FormGroup;
defaultTimezone = _moment.tz.guess();
alarmScheduleTypes = Object.keys(AlarmScheduleType);
alarmScheduleType = AlarmScheduleType;
alarmScheduleTypeTranslate = AlarmScheduleTypeTranslationMap;
private modelValue: AlarmSchedule;
private defaultItems = Array.from({length: 7}, (value, i) => ({
enabled: true,
dayOfWeek: i
}));
private propagateChange = (v: any) => { };
constructor(private fb: FormBuilder) {
}
ngOnInit(): void {
this.alarmScheduleForm = this.fb.group({
type: [AlarmScheduleType.ANY_TIME, Validators.required],
timezone: [null, Validators.required],
daysOfWeek: this.fb.array(new Array(7).fill(false)),
startsOn: [0, Validators.required],
endsOn: [0, Validators.required],
items: this.fb.array(Array.from({length: 7}, (value, i) => this.defaultItemsScheduler(i)))
});
this.alarmScheduleForm.get('type').valueChanges.subscribe((type) => {
this.alarmScheduleForm.reset({type, items: this.defaultItems}, {emitEvent: false});
this.updateValidators(type, true);
this.alarmScheduleForm.updateValueAndValidity();
});
this.alarmScheduleForm.valueChanges.subscribe(() => {
this.updateModel();
});
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (this.disabled) {
this.alarmScheduleForm.disable({emitEvent: false});
} else {
this.alarmScheduleForm.enable({emitEvent: false});
}
}
writeValue(value: AlarmSchedule): void {
this.modelValue = value;
if (!isDefinedAndNotNull(this.modelValue)) {
this.modelValue = {
type: AlarmScheduleType.ANY_TIME
};
}
switch (this.modelValue.type) {
case AlarmScheduleType.SPECIFIC_TIME:
let daysOfWeek = new Array(7).fill(false);
if (isDefined(this.modelValue.daysOfWeek)) {
daysOfWeek = daysOfWeek.map((item, index) => this.modelValue.daysOfWeek.indexOf(index + 1) > -1);
}
this.alarmScheduleForm.patchValue({
type: this.modelValue.type,
timezone: this.modelValue.timezone,
daysOfWeek,
startsOn: this.timestampToTime(this.modelValue.startsOn),
endsOn: this.timestampToTime(this.modelValue.endsOn)
}, {emitEvent: false});
break;
case AlarmScheduleType.CUSTOM:
if (this.modelValue.items) {
const alarmDays = [];
this.modelValue.items
.sort((a, b) => a.dayOfWeek - b.dayOfWeek)
.forEach((item, index) => {
if (item.enabled) {
this.itemsSchedulerForm.at(index).get('startsOn').enable({emitEvent: false});
this.itemsSchedulerForm.at(index).get('endsOn').enable({emitEvent: false});
} else {
this.itemsSchedulerForm.at(index).get('startsOn').disable({emitEvent: false});
this.itemsSchedulerForm.at(index).get('endsOn').disable({emitEvent: false});
}
alarmDays.push({
enabled: item.enabled,
startsOn: this.timestampToTime(item.startsOn),
endsOn: this.timestampToTime(item.endsOn)
});
});
this.alarmScheduleForm.patchValue({
type: this.modelValue.type,
timezone: this.modelValue.timezone,
items: alarmDays
}, {emitEvent: false});
}
break;
default:
this.alarmScheduleForm.patchValue(this.modelValue || undefined, {emitEvent: false});
}
this.updateValidators(this.modelValue.type);
}
validate(control: FormGroup): ValidationErrors | null {
return this.alarmScheduleForm.valid ? null : {
alarmScheduler: {
valid: false
}
};
}
weeklyRepeatControl(index: number): FormControl {
return (this.alarmScheduleForm.get('daysOfWeek') as FormArray).at(index) as FormControl;
}
private updateValidators(type: AlarmScheduleType, changedType = false){
switch (type){
case AlarmScheduleType.ANY_TIME:
this.alarmScheduleForm.get('timezone').disable({emitEvent: false});
this.alarmScheduleForm.get('daysOfWeek').disable({emitEvent: false});
this.alarmScheduleForm.get('startsOn').disable({emitEvent: false});
this.alarmScheduleForm.get('endsOn').disable({emitEvent: false});
this.alarmScheduleForm.get('items').disable({emitEvent: false});
break;
case AlarmScheduleType.SPECIFIC_TIME:
this.alarmScheduleForm.get('timezone').enable({emitEvent: false});
this.alarmScheduleForm.get('daysOfWeek').enable({emitEvent: false});
this.alarmScheduleForm.get('startsOn').enable({emitEvent: false});
this.alarmScheduleForm.get('endsOn').enable({emitEvent: false});
this.alarmScheduleForm.get('items').disable({emitEvent: false});
break;
case AlarmScheduleType.CUSTOM:
this.alarmScheduleForm.get('timezone').enable({emitEvent: false});
this.alarmScheduleForm.get('daysOfWeek').disable({emitEvent: false});
this.alarmScheduleForm.get('startsOn').disable({emitEvent: false});
this.alarmScheduleForm.get('endsOn').disable({emitEvent: false});
if (changedType) {
this.alarmScheduleForm.get('items').enable({emitEvent: false});
}
break;
}
}
private updateModel() {
const value = this.alarmScheduleForm.value;
if (this.modelValue) {
if (isDefined(value.daysOfWeek)) {
value.daysOfWeek = value.daysOfWeek
.map((day: boolean, index: number) => day ? index + 1 : null)
.filter(day => !!day);
}
if (isDefined(value.startsOn) && value.startsOn !== 0) {
value.startsOn = this.timeToTimestamp(value.startsOn);
}
if (isDefined(value.endsOn) && value.endsOn !== 0) {
value.endsOn = this.timeToTimestamp(value.endsOn);
}
if (isDefined(value.items)){
value.items = this.alarmScheduleForm.getRawValue().items;
value.items = value.items.map((item) => {
return { ...item, startsOn: this.timeToTimestamp(item.startsOn), endsOn: this.timeToTimestamp(item.endsOn)};
});
}
this.modelValue = value;
this.propagateChange(this.modelValue);
}
}
private timeToTimestamp(date: Date | number): number {
if (typeof date === 'number' || date === null) {
return 0;
}
return _moment.utc([1970, 0, 1, date.getHours(), date.getMinutes(), date.getSeconds(), 0]).valueOf();
}
private timestampToTime(time = 0): Date {
return new Date(time + new Date().getTimezoneOffset() * 60 * 1000);
}
private defaultItemsScheduler(index): FormGroup {
return this.fb.group({
enabled: [true],
dayOfWeek: [index],
startsOn: [0, Validators.required],
endsOn: [0, Validators.required]
});
}
changeCustomScheduler($event: MatCheckboxChange, index: number) {
const value = $event.checked;
if (value) {
this.itemsSchedulerForm.at(index).get('startsOn').enable({emitEvent: false});
this.itemsSchedulerForm.at(index).get('endsOn').enable();
} else {
this.itemsSchedulerForm.at(index).get('startsOn').disable({emitEvent: false});
this.itemsSchedulerForm.at(index).get('endsOn').disable();
}
}
private get itemsSchedulerForm(): FormArray {
return this.alarmScheduleForm.get('items') as FormArray;
}
}

1
ui-ngx/src/app/modules/home/components/profile/alarm/create-alarm-rules.component.html

@ -34,6 +34,7 @@
{{ 'device-profile.alarm-severity-required' | translate }}
</mat-error>
</mat-form-field>
<mat-divider></mat-divider>
<tb-alarm-rule formControlName="alarmRule" required fxFlex>
</tb-alarm-rule>
</div>

50
ui-ngx/src/app/shared/components/time/timezone-select.component.html

@ -0,0 +1,50 @@
<!--
Copyright © 2016-2020 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<mat-form-field [formGroup]="selectTimezoneFormGroup" fxFlex class="mat-block">
<mat-label translate>timezone.timezone</mat-label>
<input matInput type="text" placeholder="{{ 'timezone.select-timezone' | translate }}"
#timezoneInput
formControlName="timezone"
(focusin)="onFocus()"
[required]="required"
[matAutocomplete]="timezoneAutocomplete">
<button *ngIf="selectTimezoneFormGroup.get('timezone').value && !disabled"
type="button" style="margin-right: 1px"
matSuffix mat-button mat-icon-button aria-label="Clear"
(mousedown)="ignoreClosePanel = true"
(click)="clear()">
<mat-icon class="material-icons">close</mat-icon>
</button>
<mat-autocomplete class="tb-autocomplete"
#timezoneAutocomplete="matAutocomplete"
(closed)="onPanelClosed()"
(optionSelected)="ignoreClosePanel = true"
[displayWith]="displayTimezoneFn">
<mat-option *ngFor="let timezone of filteredTimezones | async" [value]="timezone">
<span [innerHTML]="displayTimezoneFn(timezone) | highlight:searchText"></span>
</mat-option>
<mat-option *ngIf="!(filteredTimezones | async)?.length" [value]="null">
<span>
{{ translate.get('timezone.no-timezones-matching', {timezone: searchText}) | async }}
</span>
</mat-option>
</mat-autocomplete>
<mat-error *ngIf="selectTimezoneFormGroup.get('timezone').hasError('required')">
{{ 'timezone.timezone-required' | translate }}
</mat-error>
</mat-form-field>

221
ui-ngx/src/app/shared/components/time/timezone-select.component.ts

@ -0,0 +1,221 @@
///
/// Copyright © 2016-2020 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { AfterViewInit, Component, forwardRef, Input, NgZone, OnInit, ViewChild } from '@angular/core';
import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms';
import { Observable, of } from 'rxjs';
import { map, mergeMap, share, tap } from 'rxjs/operators';
import { Store } from '@ngrx/store';
import { AppState } from '@app/core/core.state';
import { TranslateService } from '@ngx-translate/core';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import * as _moment from 'moment-timezone';
import { MatAutocompleteTrigger } from '@angular/material/autocomplete';
interface TimezoneInfo {
id: string;
name: string;
offset: string;
nOffset: number;
}
@Component({
selector: 'tb-timezone-select',
templateUrl: './timezone-select.component.html',
styleUrls: [],
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => TimezoneSelectComponent),
multi: true
}]
})
export class TimezoneSelectComponent implements ControlValueAccessor, OnInit, AfterViewInit {
selectTimezoneFormGroup: FormGroup;
modelValue: string | null;
defaultTimezoneId: string = null;
defaultTimezoneInfo: TimezoneInfo = null;
timezones: TimezoneInfo[] = _moment.tz.names().map((zoneName) => {
const tz = _moment.tz(zoneName);
return {
id: zoneName,
name: zoneName.replace(/_/g, ' '),
offset: `UTC${tz.format('Z')}`,
nOffset: tz.utcOffset()
}
});
@Input()
set defaultTimezone(timezone: string) {
if (this.defaultTimezoneId !== timezone) {
this.defaultTimezoneId = timezone;
if (this.defaultTimezoneId) {
this.defaultTimezoneInfo =
this.timezones.find((timezoneInfo) => timezoneInfo.id === this.defaultTimezoneId);
} else {
this.defaultTimezoneInfo = null;
}
}
}
private requiredValue: boolean;
get required(): boolean {
return this.requiredValue;
}
@Input()
set required(value: boolean) {
this.requiredValue = coerceBooleanProperty(value);
}
@Input()
disabled: boolean;
@ViewChild('timezoneInput', {static: true, read: MatAutocompleteTrigger}) timezoneInputTrigger: MatAutocompleteTrigger;
filteredTimezones: Observable<Array<TimezoneInfo>>;
searchText = '';
ignoreClosePanel = false;
private dirty = false;
private propagateChange = (v: any) => { };
constructor(private store: Store<AppState>,
public translate: TranslateService,
private ngZone: NgZone,
private fb: FormBuilder) {
this.selectTimezoneFormGroup = this.fb.group({
timezone: [null]
});
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
ngOnInit() {
this.filteredTimezones = this.selectTimezoneFormGroup.get('timezone').valueChanges
.pipe(
tap(value => {
let modelValue;
if (typeof value === 'string' || !value) {
modelValue = null;
} else {
modelValue = value.id;
}
this.updateView(modelValue);
if (value === null) {
this.clear();
}
}),
map(value => value ? (typeof value === 'string' ? value : value.name) : ''),
mergeMap(name => this.fetchTimezones(name) ),
share()
);
}
ngAfterViewInit(): void {
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (this.disabled) {
this.selectTimezoneFormGroup.disable({emitEvent: false});
} else {
this.selectTimezoneFormGroup.enable({emitEvent: false});
}
}
writeValue(value: string | null): void {
this.searchText = '';
let foundTimezone: TimezoneInfo = null;
if (value !== null) {
foundTimezone = this.timezones.find(timezoneInfo => timezoneInfo.id === value);
}
if (foundTimezone !== null) {
this.modelValue = value;
this.selectTimezoneFormGroup.get('timezone').patchValue(foundTimezone, {emitEvent: false});
} else {
if (this.defaultTimezoneInfo) {
this.selectTimezoneFormGroup.get('timezone').patchValue(this.defaultTimezoneInfo, {emitEvent: false});
setTimeout(() => {
this.updateView(this.defaultTimezoneInfo.id);
}, 0);
} else {
this.modelValue = null;
this.selectTimezoneFormGroup.get('timezone').patchValue('', {emitEvent: false});
}
}
this.dirty = true;
}
onFocus() {
if (this.dirty) {
this.selectTimezoneFormGroup.get('timezone').updateValueAndValidity({onlySelf: true, emitEvent: true});
this.dirty = false;
}
}
onPanelClosed() {
if (this.ignoreClosePanel) {
this.ignoreClosePanel = false;
} else {
if (!this.modelValue && this.defaultTimezoneInfo) {
this.ngZone.run(() => {
this.selectTimezoneFormGroup.get('timezone').reset(this.defaultTimezoneInfo, {emitEvent: true});
});
}
}
}
updateView(value: string | null) {
if (this.modelValue !== value) {
this.modelValue = value;
this.propagateChange(this.modelValue);
}
}
displayTimezoneFn(timezone?: TimezoneInfo): string | undefined {
return timezone ? `${timezone.name} (${timezone.offset})` : undefined;
}
fetchTimezones(searchText?: string): Observable<Array<TimezoneInfo>> {
this.searchText = searchText;
let result = this.timezones;
if (searchText && searchText.length) {
result = this.timezones.filter((timezoneInfo) =>
timezoneInfo.name.toLowerCase().includes(searchText.toLowerCase()));
}
return of(result);
}
clear() {
this.selectTimezoneFormGroup.get('timezone').patchValue('', {emitEvent: true});
setTimeout(() => {
this.timezoneInputTrigger.openPanel();
}, 0);
}
}

31
ui-ngx/src/app/shared/models/device.models.ts

@ -236,9 +236,40 @@ export interface AlarmCondition {
spec?: AlarmConditionSpec;
}
export enum AlarmScheduleType {
ANY_TIME = 'ANY_TIME',
SPECIFIC_TIME = 'SPECIFIC_TIME',
CUSTOM = 'CUSTOM'
}
export const AlarmScheduleTypeTranslationMap = new Map<AlarmScheduleType, string>(
[
[AlarmScheduleType.ANY_TIME, 'device-profile.schedule-any-time'],
[AlarmScheduleType.SPECIFIC_TIME, 'device-profile.schedule-specific-time'],
[AlarmScheduleType.CUSTOM, 'device-profile.schedule-custom']
]
);
export interface AlarmSchedule{
type: AlarmScheduleType;
timezone?: string;
daysOfWeek?: number[];
startsOn?: number;
endsOn?: number;
items?: CustomTimeSchedulerItem[];
}
export interface CustomTimeSchedulerItem{
enabled: boolean;
dayOfWeek: number;
startsOn: number;
endsOn: number;
}
export interface AlarmRule {
condition: AlarmCondition;
alarmDetails?: string;
schedule?: AlarmSchedule;
}
export interface DeviceProfileAlarm {

2
ui-ngx/src/app/shared/models/time/time.models.ts

@ -467,7 +467,6 @@ export const defaultTimeIntervals = new Array<TimeInterval>(
);
export enum TimeUnit {
MILLISECONDS = 'MILLISECONDS',
SECONDS = 'SECONDS',
MINUTES = 'MINUTES',
HOURS = 'HOURS',
@ -476,7 +475,6 @@ export enum TimeUnit {
export const timeUnitTranslationMap = new Map<TimeUnit, string>(
[
[TimeUnit.MILLISECONDS, 'timeunit.milliseconds'],
[TimeUnit.SECONDS, 'timeunit.seconds'],
[TimeUnit.MINUTES, 'timeunit.minutes'],
[TimeUnit.HOURS, 'timeunit.hours'],

3
ui-ngx/src/app/shared/shared.module.ts

@ -134,6 +134,7 @@ import { HistorySelectorComponent } from './components/time/history-selector/his
import { EntityGatewaySelectComponent } from '@shared/components/entity/entity-gateway-select.component';
import { QueueTypeListComponent } from '@shared/components/queue/queue-type-list.component';
import { ContactComponent } from '@shared/components/contact.component';
import { TimezoneSelectComponent } from '@shared/components/time/timezone-select.component';
@NgModule({
providers: [
@ -172,6 +173,7 @@ import { ContactComponent } from '@shared/components/contact.component';
DashboardSelectPanelComponent,
DatetimePeriodComponent,
DatetimeComponent,
TimezoneSelectComponent,
ValueInputComponent,
DashboardAutocompleteComponent,
EntitySubTypeAutocompleteComponent,
@ -292,6 +294,7 @@ import { ContactComponent } from '@shared/components/contact.component';
DashboardSelectComponent,
DatetimePeriodComponent,
DatetimeComponent,
TimezoneSelectComponent,
DashboardAutocompleteComponent,
EntitySubTypeAutocompleteComponent,
EntitySubTypeSelectComponent,

27
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -856,7 +856,25 @@
"condition-repeating-value-range": "Count of events should be in a range from 1 to 2147483647.",
"condition-repeating-value-pattern": "Count of events should be integers.",
"condition-repeating-value-required": "Count of events is required.",
"schedule": "Schedule"
"schedule-type": "Scheduler type",
"schedule-type-required": "Scheduler type is required.",
"schedule": "Schedule",
"schedule-any-time": "Active all the time",
"schedule-specific-time": "Active at a specific time",
"schedule-custom": "Custom",
"schedule-day": {
"monday": "Monday",
"tuesday": "Tuesday",
"wednesday": "Wednesday",
"thursday": "Thursday",
"friday": "Friday",
"saturday": "Saturday",
"sunday": "Sunday"
},
"schedule-days": "Days",
"schedule-time": "Time",
"schedule-time-from": "From",
"schedule-time-to": "To"
},
"dialog": {
"close": "Close dialog"
@ -1742,6 +1760,12 @@
"help": "Help",
"reset-debug-mode": "Reset debug mode in all nodes"
},
"timezone": {
"timezone": "Timezone",
"select-timezone": "Select timezone",
"no-timezones-matching": "No timezones matching '{{timezone}}' were found.",
"timezone-required": "Timezone is required."
},
"queue": {
"select_name": "Select queue name",
"name": "Queue Name",
@ -1821,7 +1845,6 @@
"advanced": "Advanced"
},
"timeunit": {
"milliseconds": "Milliseconds",
"seconds": "Seconds",
"minutes": "Minutes",
"hours": "Hours",

19
ui-ngx/yarn.lock

@ -1428,6 +1428,13 @@
resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d"
integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==
"@types/moment-timezone@^0.5.30":
version "0.5.30"
resolved "https://registry.yarnpkg.com/@types/moment-timezone/-/moment-timezone-0.5.30.tgz#340ed45fe3e715f4a011f5cfceb7cb52aad46fc7"
integrity sha512-aDVfCsjYnAQaV/E9Qc24C5Njx1CoDjXsEgkxtp9NyXDpYu4CCbmclb6QhWloS9UTU/8YROUEEdEkWI0D7DxnKg==
dependencies:
moment-timezone "*"
"@types/mousetrap@^1.6.0":
version "1.6.3"
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:
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
moment-timezone@*, moment-timezone@^0.5.31:
version "0.5.31"
resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.31.tgz#9c40d8c5026f0c7ab46eda3d63e49c155148de05"
integrity sha512-+GgHNg8xRhMXfEbv81iDtrVeTcWt0kWmTEY1XQK14dICTXnWJnT0dxdlPspwqF3keKMVPXwayEsk1DI0AA/jdA==
dependencies:
moment ">= 2.9.0"
"moment@>= 2.9.0":
version "2.29.0"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.0.tgz#fcbef955844d91deb55438613ddcec56e86a3425"
integrity sha512-z6IJ5HXYiuxvFTI6eiQ9dm77uE0gyy1yXNApVHqTcnIKfY9tIwEjlzsZ6u1LQXvVgKeTnv9Xm7NDvJ7lso3MtA==
moment@^2.27.0:
version "2.27.0"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.27.0.tgz#8bff4e3e26a236220dfe3e36de756b6ebaa0105d"

Loading…
Cancel
Save