diff --git a/ui-ngx/src/app/modules/home/components/home-components.module.ts b/ui-ngx/src/app/modules/home/components/home-components.module.ts index 9bbac4154d..15d33f709c 100644 --- a/ui-ngx/src/app/modules/home/components/home-components.module.ts +++ b/ui-ngx/src/app/modules/home/components/home-components.module.ts @@ -102,13 +102,16 @@ import { DeviceProfileAlarmComponent } from './profile/alarm/device-profile-alar import { CreateAlarmRulesComponent } from './profile/alarm/create-alarm-rules.component'; import { AlarmRuleComponent } from './profile/alarm/alarm-rule.component'; import { AlarmRuleConditionComponent } from './profile/alarm/alarm-rule-condition.component'; -import { AlarmRuleKeyFiltersDialogComponent } from './profile/alarm/alarm-rule-key-filters-dialog.component'; 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'; import { DeviceWizardDialogComponent } from './wizard/device-wizard-dialog.component'; import { DeviceCredentialsComponent } from './device/device-credentials.component'; +import { AlarmScheduleInfoComponent } from './profile/alarm/alarm-schedule-info.component'; +import { AlarmScheduleDialogComponent } from '@home/components/profile/alarm/alarm-schedule-dialog.component'; +import { EditAlarmDetailsDialogComponent } from './profile/alarm/edit-alarm-details-dialog.component'; +import { AlarmRuleConditionDialogComponent } from '@home/components/profile/alarm/alarm-rule-condition-dialog.component'; @NgModule({ declarations: @@ -190,7 +193,7 @@ import { DeviceCredentialsComponent } from './device/device-credentials.componen DeviceProfileTransportConfigurationComponent, CreateAlarmRulesComponent, AlarmRuleComponent, - AlarmRuleKeyFiltersDialogComponent, + AlarmRuleConditionDialogComponent, AlarmRuleConditionComponent, DeviceProfileAlarmComponent, DeviceProfileAlarmsComponent, @@ -198,9 +201,12 @@ import { DeviceCredentialsComponent } from './device/device-credentials.componen DeviceProfileDialogComponent, AddDeviceProfileDialogComponent, RuleChainAutocompleteComponent, + AlarmScheduleInfoComponent, AlarmScheduleComponent, DeviceWizardDialogComponent, - DeviceCredentialsComponent + DeviceCredentialsComponent, + AlarmScheduleDialogComponent, + EditAlarmDetailsDialogComponent ], imports: [ CommonModule, @@ -271,7 +277,7 @@ import { DeviceCredentialsComponent } from './device/device-credentials.componen DeviceProfileTransportConfigurationComponent, CreateAlarmRulesComponent, AlarmRuleComponent, - AlarmRuleKeyFiltersDialogComponent, + AlarmRuleConditionDialogComponent, AlarmRuleConditionComponent, DeviceProfileAlarmComponent, DeviceProfileAlarmsComponent, @@ -281,7 +287,10 @@ import { DeviceCredentialsComponent } from './device/device-credentials.componen RuleChainAutocompleteComponent, DeviceWizardDialogComponent, DeviceCredentialsComponent, - AlarmScheduleComponent + AlarmScheduleInfoComponent, + AlarmScheduleComponent, + AlarmScheduleDialogComponent, + EditAlarmDetailsDialogComponent ], providers: [ WidgetComponentService, diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition-dialog.component.html b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition-dialog.component.html new file mode 100644 index 0000000000..7be952c68d --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition-dialog.component.html @@ -0,0 +1,124 @@ + +
+ +

{{ (readonly ? 'device-profile.alarm-rule-condition' : 'device-profile.edit-alarm-rule-condition') | translate }}

+ + +
+ + +
+
+
+ + +
+ + device-profile.condition-type + + + {{ alarmConditionTypeTranslation.get(alarmConditionType) | translate }} + + + + {{ 'device-profile.condition-type-required' | translate }} + + +
+ + + + + {{ 'device-profile.condition-duration-value-required' | translate }} + + + {{ 'device-profile.condition-duration-value-range' | translate }} + + + {{ 'device-profile.condition-duration-value-range' | translate }} + + + {{ 'device-profile.condition-duration-value-pattern' | translate }} + + + + + + + {{ timeUnitTranslations.get(timeUnit) | translate }} + + + + {{ 'device-profile.condition-duration-time-unit-required' | translate }} + + +
+
+ + + + + {{ 'device-profile.condition-repeating-value-required' | translate }} + + + {{ 'device-profile.condition-repeating-value-range' | translate }} + + + {{ 'device-profile.condition-repeating-value-range' | translate }} + + + {{ 'device-profile.condition-repeating-value-pattern' | translate }} + + +
+
+
+
+
+
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition-dialog.component.scss b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition-dialog.component.scss new file mode 100644 index 0000000000..4af573d021 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition-dialog.component.scss @@ -0,0 +1,20 @@ +/** + * 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. + */ +:host { + .row { + margin-top: 1em; + } +} diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition-dialog.component.ts b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition-dialog.component.ts new file mode 100644 index 0000000000..4055560b38 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition-dialog.component.ts @@ -0,0 +1,150 @@ +/// +/// 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, Inject, OnInit, SkipSelf } from '@angular/core'; +import { ErrorStateMatcher } from '@angular/material/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms'; +import { Router } from '@angular/router'; +import { DialogComponent } from '@app/shared/components/dialog.component'; +import { UtilsService } from '@core/services/utils.service'; +import { TranslateService } from '@ngx-translate/core'; +import { KeyFilter, keyFilterInfosToKeyFilters, keyFiltersToKeyFilterInfos } from '@shared/models/query/query.models'; +import { AlarmCondition, AlarmConditionType, AlarmConditionTypeTranslationMap } from '@shared/models/device.models'; +import { TimeUnit, timeUnitTranslationMap } from '@shared/models/time/time.models'; + +export interface AlarmRuleConditionDialogData { + readonly: boolean; + condition: AlarmCondition; +} + +@Component({ + selector: 'tb-alarm-rule-condition-dialog', + templateUrl: './alarm-rule-condition-dialog.component.html', + providers: [{provide: ErrorStateMatcher, useExisting: AlarmRuleConditionDialogComponent}], + styleUrls: ['/alarm-rule-condition-dialog.component.scss'] +}) +export class AlarmRuleConditionDialogComponent extends DialogComponent + implements OnInit, ErrorStateMatcher { + + timeUnits = Object.keys(TimeUnit); + timeUnitTranslations = timeUnitTranslationMap; + alarmConditionTypes = Object.keys(AlarmConditionType); + AlarmConditionType = AlarmConditionType; + alarmConditionTypeTranslation = AlarmConditionTypeTranslationMap; + + readonly = this.data.readonly; + condition = this.data.condition; + + conditionFormGroup: FormGroup; + + submitted = false; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: AlarmRuleConditionDialogData, + @SkipSelf() private errorStateMatcher: ErrorStateMatcher, + public dialogRef: MatDialogRef, + private fb: FormBuilder, + private utils: UtilsService, + public translate: TranslateService) { + super(store, router, dialogRef); + + this.conditionFormGroup = this.fb.group({ + keyFilters: [keyFiltersToKeyFilterInfos(this.condition?.condition), Validators.required], + spec: this.fb.group({ + type: [AlarmConditionType.SIMPLE, Validators.required], + unit: [{value: null, disable: true}, Validators.required], + value: [{value: null, disable: true}, [Validators.required, Validators.min(1), Validators.max(2147483647), Validators.pattern('[0-9]*')]], + count: [{value: null, disable: true}, [Validators.required, Validators.min(1), Validators.max(2147483647), Validators.pattern('[0-9]*')]] + }) + }); + this.conditionFormGroup.patchValue({spec: this.condition?.spec}); + this.conditionFormGroup.get('spec.type').valueChanges.subscribe((type) => { + this.updateValidators(type, true, true); + }); + if (this.readonly) { + this.conditionFormGroup.disable({emitEvent: false}); + } else { + this.updateValidators(this.condition?.spec?.type); + } + } + + ngOnInit(): void { + } + + isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { + const originalErrorState = this.errorStateMatcher.isErrorState(control, form); + const customErrorState = !!(control && control.invalid && this.submitted); + return originalErrorState || customErrorState; + } + + private updateValidators(type: AlarmConditionType, resetDuration = false, emitEvent = false) { + switch (type) { + case AlarmConditionType.DURATION: + this.conditionFormGroup.get('spec.value').enable(); + this.conditionFormGroup.get('spec.unit').enable(); + this.conditionFormGroup.get('spec.count').disable(); + if (resetDuration) { + this.conditionFormGroup.get('spec').patchValue({ + count: null + }); + } + break; + case AlarmConditionType.REPEATING: + this.conditionFormGroup.get('spec.count').enable(); + this.conditionFormGroup.get('spec.value').disable(); + this.conditionFormGroup.get('spec.unit').disable(); + if (resetDuration) { + this.conditionFormGroup.get('spec').patchValue({ + value: null, + unit: null + }); + } + break; + case AlarmConditionType.SIMPLE: + this.conditionFormGroup.get('spec.value').disable(); + this.conditionFormGroup.get('spec.unit').disable(); + this.conditionFormGroup.get('spec.count').disable(); + if (resetDuration) { + this.conditionFormGroup.get('spec').patchValue({ + value: null, + unit: null, + count: null + }); + } + break; + } + this.conditionFormGroup.get('spec.value').updateValueAndValidity({emitEvent}); + this.conditionFormGroup.get('spec.unit').updateValueAndValidity({emitEvent}); + this.conditionFormGroup.get('spec.count').updateValueAndValidity({emitEvent}); + } + + cancel(): void { + this.dialogRef.close(null); + } + + save(): void { + this.submitted = true; + this.condition = { + condition: keyFilterInfosToKeyFilters(this.conditionFormGroup.get('keyFilters').value), + spec: this.conditionFormGroup.get('spec').value + }; + this.dialogRef.close(this.condition); + } +} diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition.component.html b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition.component.html index 170182ded3..15d6173e0e 100644 --- a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition.component.html @@ -15,8 +15,9 @@ limitations under the License. --> -
+
+
- + +
+
diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition.component.scss b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition.component.scss index 1d654bc0f4..0b1ccabd65 100644 --- a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition.component.scss +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition.component.scss @@ -21,10 +21,15 @@ } } .tb-alarm-rule-condition { - padding: 8px; - border: 1px groove rgba(0, 0, 0, .25); - border-radius: 4px; cursor: pointer; + .tb-alarm-rule-condition-spec { + margin-top: 1em; + line-height: 1.8em; + padding: 4px; + &.disabled { + opacity: 0.7; + } + } } } diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition.component.ts b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition.component.ts index 7416b944eb..23e52cf8cd 100644 --- a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition.component.ts @@ -18,20 +18,21 @@ import { Component, forwardRef, Input, OnInit } from '@angular/core'; import { ControlValueAccessor, FormBuilder, - FormControl, + FormControl, FormGroup, NG_VALIDATORS, NG_VALUE_ACCESSOR, - Validator + Validator, Validators } from '@angular/forms'; import { MatDialog } from '@angular/material/dialog'; -import { KeyFilter } from '@shared/models/query/query.models'; -import { deepClone } from '@core/utils'; -import { - AlarmRuleKeyFiltersDialogComponent, - AlarmRuleKeyFiltersDialogData -} from './alarm-rule-key-filters-dialog.component'; +import { deepClone, isUndefined } from '@core/utils'; import { TranslateService } from '@ngx-translate/core'; import { DatePipe } from '@angular/common'; +import { AlarmCondition, AlarmConditionSpec, AlarmConditionType } from '@shared/models/device.models'; +import { + AlarmRuleConditionDialogComponent, + AlarmRuleConditionDialogData +} from '@home/components/profile/alarm/alarm-rule-condition-dialog.component'; +import { TimeUnit } from '@shared/models/time/time.models'; @Component({ selector: 'tb-alarm-rule-condition', @@ -55,9 +56,11 @@ export class AlarmRuleConditionComponent implements ControlValueAccessor, OnInit @Input() disabled: boolean; - alarmRuleConditionControl: FormControl; + alarmRuleConditionFormGroup: FormGroup; + + specText = ''; - private modelValue: Array; + private modelValue: AlarmCondition; private propagateChange = (v: any) => { }; @@ -75,25 +78,31 @@ export class AlarmRuleConditionComponent implements ControlValueAccessor, OnInit } ngOnInit() { - this.alarmRuleConditionControl = this.fb.control(null); + this.alarmRuleConditionFormGroup = this.fb.group({ + condition: [null, Validators.required], + spec: [null, Validators.required] + }); } setDisabledState(isDisabled: boolean): void { this.disabled = isDisabled; if (this.disabled) { - this.alarmRuleConditionControl.disable({emitEvent: false}); + this.alarmRuleConditionFormGroup.disable({emitEvent: false}); } else { - this.alarmRuleConditionControl.enable({emitEvent: false}); + this.alarmRuleConditionFormGroup.enable({emitEvent: false}); } } - writeValue(value: Array): void { + writeValue(value: AlarmCondition): void { this.modelValue = value; + if (this.modelValue !== null && isUndefined(this.modelValue?.spec)) { + this.modelValue = Object.assign(this.modelValue, {spec: {type: AlarmConditionType.SIMPLE}}); + } this.updateConditionInfo(); } public conditionSet() { - return this.modelValue && this.modelValue.length; + return this.modelValue && this.modelValue.condition.length; } public validate(c: FormControl) { @@ -108,13 +117,13 @@ export class AlarmRuleConditionComponent implements ControlValueAccessor, OnInit if ($event) { $event.stopPropagation(); } - this.dialog.open>(AlarmRuleKeyFiltersDialogComponent, { + this.dialog.open(AlarmRuleConditionDialogComponent, { disableClose: true, panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], data: { readonly: this.disabled, - keyFilters: this.disabled ? this.modelValue : deepClone(this.modelValue) + condition: this.disabled ? this.modelValue : deepClone(this.modelValue) } }).afterClosed().subscribe((result) => { if (result) { @@ -125,7 +134,45 @@ export class AlarmRuleConditionComponent implements ControlValueAccessor, OnInit } private updateConditionInfo() { - this.alarmRuleConditionControl.patchValue(this.modelValue); + this.alarmRuleConditionFormGroup.patchValue( + { + condition: this.modelValue?.condition, + spec: this.modelValue?.spec + } + ); + this.updateSpecText(); + } + + private updateSpecText() { + this.specText = ''; + if (this.modelValue && this.modelValue.spec) { + const spec = this.modelValue.spec; + switch (spec.type) { + case AlarmConditionType.SIMPLE: + break; + case AlarmConditionType.DURATION: + let duringText = ''; + switch (spec.unit) { + case TimeUnit.SECONDS: + duringText = this.translate.instant('timewindow.seconds', {seconds: spec.value}); + break; + case TimeUnit.MINUTES: + duringText = this.translate.instant('timewindow.minutes', {minutes: spec.value}); + break; + case TimeUnit.HOURS: + duringText = this.translate.instant('timewindow.hours', {hours: spec.value}); + break; + case TimeUnit.DAYS: + duringText = this.translate.instant('timewindow.days', {days: spec.value}); + break; + } + this.specText = this.translate.instant('device-profile.condition-during', {during: duringText}); + break; + case AlarmConditionType.REPEATING: + this.specText = this.translate.instant('device-profile.condition-repeat-times', {count: spec.count}); + break; + } + } } private updateModel() { diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.html b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.html index 66eb99f27e..3c49f61cf0 100644 --- a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.html @@ -16,90 +16,36 @@ -->
- - - - -
- - device-profile.condition-type - - - {{ alarmConditionTypeTranslation.get(alarmConditionType) | translate }} - - - - {{ 'device-profile.condition-type-required' | translate }} - - -
- - - - - {{ 'device-profile.condition-duration-value-required' | translate }} - - - {{ 'device-profile.condition-duration-value-range' | translate }} - - - {{ 'device-profile.condition-duration-value-range' | translate }} - - - {{ 'device-profile.condition-duration-value-pattern' | translate }} - - - - - - - {{ timeUnitTranslations.get(timeUnit) | translate }} - - - - {{ 'device-profile.condition-duration-time-unit-required' | translate }} - - -
-
- - - - - {{ 'device-profile.condition-repeating-value-required' | translate }} - - - {{ 'device-profile.condition-repeating-value-range' | translate }} - - - {{ 'device-profile.condition-repeating-value-range' | translate }} - - - {{ 'device-profile.condition-repeating-value-pattern' | translate }} - - -
-
-
- - - - - - - device-profile.alarm-details - - - -
+ + + + + + +
diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.scss b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.scss index 8af986af69..a52e6f312e 100644 --- a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.scss +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.scss @@ -17,5 +17,29 @@ .row { margin-top: 1em; } + .tb-alarm-rule-details { + a.mat-button { + &:hover, &:focus { + border-bottom: none; + } + } + .tb-alarm-rule-details-content { + min-height: 33px; + overflow: hidden; + white-space: pre; + line-height: 1.8em; + padding: 4px; + cursor: pointer; + &.collapsed { + max-height: 33px; + white-space: nowrap; + text-overflow: ellipsis; + } + &.disabled { + opacity: 0.7; + cursor: auto; + } + } + } } diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.ts b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.ts index fe2916b854..4c86f7dfbd 100644 --- a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.ts @@ -25,11 +25,14 @@ import { Validator, Validators } from '@angular/forms'; -import { AlarmConditionType, AlarmConditionTypeTranslationMap, AlarmRule } from '@shared/models/device.models'; +import { AlarmRule } from '@shared/models/device.models'; import { MatDialog } from '@angular/material/dialog'; -import { TimeUnit, timeUnitTranslationMap } from '@shared/models/time/time.models'; import { coerceBooleanProperty } from '@angular/cdk/coercion'; -import { isUndefined } from '@core/utils'; +import { isDefinedAndNotNull } from '@core/utils'; +import { + EditAlarmDetailsDialogComponent, + EditAlarmDetailsDialogData +} from '@home/components/profile/alarm/edit-alarm-details-dialog.component'; @Component({ selector: 'tb-alarm-rule', @@ -50,12 +53,6 @@ import { isUndefined } from '@core/utils'; }) export class AlarmRuleComponent implements ControlValueAccessor, OnInit, Validator { - timeUnits = Object.keys(TimeUnit); - timeUnitTranslations = timeUnitTranslationMap; - alarmConditionTypes = Object.keys(AlarmConditionType); - AlarmConditionType = AlarmConditionType; - alarmConditionTypeTranslation = AlarmConditionTypeTranslationMap; - @Input() disabled: boolean; @@ -72,6 +69,8 @@ export class AlarmRuleComponent implements ControlValueAccessor, OnInit, Validat alarmRuleFormGroup: FormGroup; + expandAlarmDetails = false; + private propagateChange = (v: any) => { }; constructor(private dialog: MatDialog, @@ -87,21 +86,10 @@ export class AlarmRuleComponent implements ControlValueAccessor, OnInit, Validat ngOnInit() { this.alarmRuleFormGroup = this.fb.group({ - condition: this.fb.group({ - condition: [null, Validators.required], - spec: this.fb.group({ - type: [AlarmConditionType.SIMPLE, Validators.required], - unit: [{value: null, disable: true}, Validators.required], - value: [{value: null, disable: true}, [Validators.required, Validators.min(1), Validators.max(2147483647), Validators.pattern('[0-9]*')]], - count: [{value: null, disable: true}, [Validators.required, Validators.min(1), Validators.max(2147483647), Validators.pattern('[0-9]*')]] - }) - }, Validators.required), + condition: [null, [Validators.required]], schedule: [null], alarmDetails: [null] }); - this.alarmRuleFormGroup.get('condition.spec.type').valueChanges.subscribe((type) => { - this.updateValidators(type, true, true); - }); this.alarmRuleFormGroup.valueChanges.subscribe(() => { this.updateModel(); }); @@ -118,11 +106,25 @@ export class AlarmRuleComponent implements ControlValueAccessor, OnInit, Validat writeValue(value: AlarmRule): void { this.modelValue = value; - if (this.modelValue !== null && isUndefined(this.modelValue?.condition?.spec)) { - this.modelValue = Object.assign(this.modelValue, {condition: {spec: {type: AlarmConditionType.SIMPLE}}}); - } this.alarmRuleFormGroup.reset(this.modelValue || undefined, {emitEvent: false}); - this.updateValidators(this.modelValue?.condition?.spec?.type); + } + + public openEditDetailsDialog($event: Event) { + if ($event) { + $event.stopPropagation(); + } + this.dialog.open(EditAlarmDetailsDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + alarmDetails: this.alarmRuleFormGroup.get('alarmDetails').value + } + }).afterClosed().subscribe((alarmDetails) => { + if (isDefinedAndNotNull(alarmDetails)) { + this.alarmRuleFormGroup.patchValue({alarmDetails}); + } + }); } public validate(c: FormControl) { @@ -133,47 +135,6 @@ export class AlarmRuleComponent implements ControlValueAccessor, OnInit, Validat }; } - private updateValidators(type: AlarmConditionType, resetDuration = false, emitEvent = false) { - switch (type) { - case AlarmConditionType.DURATION: - this.alarmRuleFormGroup.get('condition.spec.value').enable(); - this.alarmRuleFormGroup.get('condition.spec.unit').enable(); - this.alarmRuleFormGroup.get('condition.spec.count').disable(); - if (resetDuration) { - this.alarmRuleFormGroup.get('condition.spec').patchValue({ - count: null - }); - } - break; - case AlarmConditionType.REPEATING: - this.alarmRuleFormGroup.get('condition.spec.count').enable(); - this.alarmRuleFormGroup.get('condition.spec.value').disable(); - this.alarmRuleFormGroup.get('condition.spec.unit').disable(); - if (resetDuration) { - this.alarmRuleFormGroup.get('condition.spec').patchValue({ - value: null, - unit: null - }); - } - break; - case AlarmConditionType.SIMPLE: - this.alarmRuleFormGroup.get('condition.spec.value').disable(); - this.alarmRuleFormGroup.get('condition.spec.unit').disable(); - this.alarmRuleFormGroup.get('condition.spec.count').disable(); - if (resetDuration) { - this.alarmRuleFormGroup.get('condition.spec').patchValue({ - value: null, - unit: null, - count: null - }); - } - break; - } - this.alarmRuleFormGroup.get('condition.spec.value').updateValueAndValidity({emitEvent}); - this.alarmRuleFormGroup.get('condition.spec.unit').updateValueAndValidity({emitEvent}); - this.alarmRuleFormGroup.get('condition.spec.count').updateValueAndValidity({emitEvent}); - } - private updateModel() { const value = this.alarmRuleFormGroup.value; if (this.modelValue) { diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-key-filters-dialog.component.html b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule-dialog.component.html similarity index 74% rename from ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-key-filters-dialog.component.html rename to ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule-dialog.component.html index 26defe62ff..abe511fc3f 100644 --- a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-key-filters-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule-dialog.component.html @@ -15,9 +15,9 @@ limitations under the License. --> -
+ -

{{ (readonly ? 'device-profile.alarm-rule-condition' : 'device-profile.edit-alarm-rule-condition') | translate }}

+

{{ (readonly ? 'device-profile.schedule' : 'device-profile.edit-schedule') | translate }}

@@ -43,7 +40,7 @@ + + + +
+
+
+ + device-profile.alarm-details + + +
+
+
+
+ + +
+ diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/edit-alarm-details-dialog.component.ts b/ui-ngx/src/app/modules/home/components/profile/alarm/edit-alarm-details-dialog.component.ts new file mode 100644 index 0000000000..bc46bee51d --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/edit-alarm-details-dialog.component.ts @@ -0,0 +1,80 @@ +/// +/// 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, Inject, OnInit, SkipSelf } from '@angular/core'; +import { ErrorStateMatcher } from '@angular/material/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm } from '@angular/forms'; +import { Router } from '@angular/router'; +import { DialogComponent } from '@app/shared/components/dialog.component'; +import { UtilsService } from '@core/services/utils.service'; +import { TranslateService } from '@ngx-translate/core'; + +export interface EditAlarmDetailsDialogData { + alarmDetails: string; +} + +@Component({ + selector: 'tb-edit-alarm-details-dialog', + templateUrl: './edit-alarm-details-dialog.component.html', + providers: [{provide: ErrorStateMatcher, useExisting: EditAlarmDetailsDialogComponent}], + styleUrls: [] +}) +export class EditAlarmDetailsDialogComponent extends DialogComponent + implements OnInit, ErrorStateMatcher { + + alarmDetails = this.data.alarmDetails; + + editDetailsFormGroup: FormGroup; + + submitted = false; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: EditAlarmDetailsDialogData, + @SkipSelf() private errorStateMatcher: ErrorStateMatcher, + public dialogRef: MatDialogRef, + private fb: FormBuilder, + private utils: UtilsService, + public translate: TranslateService) { + super(store, router, dialogRef); + + this.editDetailsFormGroup = this.fb.group({ + alarmDetails: [this.alarmDetails] + }); + } + + ngOnInit(): void { + } + + isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { + const originalErrorState = this.errorStateMatcher.isErrorState(control, form); + const customErrorState = !!(control && control.invalid && this.submitted); + return originalErrorState || customErrorState; + } + + cancel(): void { + this.dialogRef.close(null); + } + + save(): void { + this.submitted = true; + this.alarmDetails = this.editDetailsFormGroup.get('alarmDetails').value; + this.dialogRef.close(this.alarmDetails); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/device-profile/device-profile-tabs.component.html b/ui-ngx/src/app/modules/home/pages/device-profile/device-profile-tabs.component.html index 437fc21c86..76dfe27511 100644 --- a/ui-ngx/src/app/modules/home/pages/device-profile/device-profile-tabs.component.html +++ b/ui-ngx/src/app/modules/home/pages/device-profile/device-profile-tabs.component.html @@ -61,29 +61,7 @@ - - - - - - - - - - - - - + + diff --git a/ui-ngx/src/app/shared/models/device.models.ts b/ui-ngx/src/app/shared/models/device.models.ts index d1fb40a9d5..4354521330 100644 --- a/ui-ngx/src/app/shared/models/device.models.ts +++ b/ui-ngx/src/app/shared/models/device.models.ts @@ -25,6 +25,8 @@ import { RuleChainId } from '@shared/models/id/rule-chain-id'; import { EntityInfoData } from '@shared/models/entity.models'; import { KeyFilter } from '@shared/models/query/query.models'; import { TimeUnit } from '@shared/models/time/time.models'; +import * as _moment from 'moment-timezone'; +import { AbstractControl, FormGroup } from '@angular/forms'; export enum DeviceProfileType { DEFAULT = 'DEFAULT' @@ -408,3 +410,62 @@ export interface ClaimResult { device: Device; response: ClaimResponse; } + +export const dayOfWeekTranslations = new Array( + 'device-profile.schedule-day.monday', + 'device-profile.schedule-day.tuesday', + 'device-profile.schedule-day.wednesday', + 'device-profile.schedule-day.thursday', + 'device-profile.schedule-day.friday', + 'device-profile.schedule-day.saturday', + 'device-profile.schedule-day.sunday' +); + +export function getDayString(day: number): string { + switch (day) { + case 0: + return 'device-profile.schedule-day.monday'; + case 1: + return this.translate.instant('device-profile.schedule-day.tuesday'); + case 2: + return this.translate.instant('device-profile.schedule-day.wednesday'); + case 3: + return this.translate.instant('device-profile.schedule-day.thursday'); + case 4: + return this.translate.instant('device-profile.schedule-day.friday'); + case 5: + return this.translate.instant('device-profile.schedule-day.saturday'); + case 6: + return this.translate.instant('device-profile.schedule-day.sunday'); + } +} + +export function timeOfDayToUTCTimestamp(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(); +} + +export function utcTimestampToTimeOfDay(time = 0): Date { + return new Date(time + new Date().getTimezoneOffset() * 60 * 1000); +} + +function timeOfDayToMoment(date: Date | number): _moment.Moment { + if (typeof date === 'number' || date === null) { + return _moment([1970, 0, 1, 0, 0, 0, 0]); + } + return _moment([1970, 0, 1, date.getHours(), date.getMinutes(), 0, 0]); +} + +export function getAlarmScheduleRangeText(startsOn: Date | number, endsOn: Date | number): string { + const start = timeOfDayToMoment(startsOn); + const end = timeOfDayToMoment(endsOn); + if (start < end) { + return `${start.format('hh:mm A')}${end.format('hh:mm A')}`; + } else if (start.valueOf() === 0 && end.valueOf() === 0 || start.isSame(_moment([1970, 0])) && end.isSame(_moment([1970, 0]))) { + return '12:00 AM12:00 PM'; + } + return `12:00 AM${end.format('hh:mm A')}` + + ` and ${start.format('hh:mm A')}12:00 PM`; +} diff --git a/ui-ngx/src/app/shared/models/query/query.models.ts b/ui-ngx/src/app/shared/models/query/query.models.ts index 3f4b6896db..ba86bce85b 100644 --- a/ui-ngx/src/app/shared/models/query/query.models.ts +++ b/ui-ngx/src/app/shared/models/query/query.models.ts @@ -476,21 +476,23 @@ export function keyFilterInfosToKeyFilters(keyFilterInfos: Array) export function keyFiltersToKeyFilterInfos(keyFilters: Array): Array { const keyFilterInfos: Array = []; const keyFilterInfoMap: {[infoKey: string]: KeyFilterInfo} = {}; - for (const keyFilter of keyFilters) { - const key = keyFilter.key; - const infoKey = key.key + key.type + keyFilter.valueType; - let keyFilterInfo = keyFilterInfoMap[infoKey]; - if (!keyFilterInfo) { - keyFilterInfo = { - key, - valueType: keyFilter.valueType, - predicates: [] - }; - keyFilterInfoMap[infoKey] = keyFilterInfo; - keyFilterInfos.push(keyFilterInfo); - } - if (keyFilter.predicate) { - keyFilterInfo.predicates.push(keyFilterPredicateToKeyFilterPredicateInfo(keyFilter.predicate)); + if (keyFilters) { + for (const keyFilter of keyFilters) { + const key = keyFilter.key; + const infoKey = key.key + key.type + keyFilter.valueType; + let keyFilterInfo = keyFilterInfoMap[infoKey]; + if (!keyFilterInfo) { + keyFilterInfo = { + key, + valueType: keyFilter.valueType, + predicates: [] + }; + keyFilterInfoMap[infoKey] = keyFilterInfo; + keyFilterInfos.push(keyFilterInfo); + } + if (keyFilter.predicate) { + keyFilterInfo.predicates.push(keyFilterPredicateToKeyFilterPredicateInfo(keyFilter.predicate)); + } } } return keyFilterInfos; diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 8f89fa8f39..9a5d71a2e6 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -55,7 +55,9 @@ "continue": "Continue", "discard-changes": "Discard Changes", "download": "Download", - "next-with-label": "Next: {{label}}" + "next-with-label": "Next: {{label}}", + "read-more": "Read more", + "hide": "Hide" }, "aggregation": { "aggregation": "Aggregation", @@ -932,15 +934,18 @@ "condition-type": "Condition type", "condition-type-simple": "Simple", "condition-type-duration": "Duration", + "condition-during": "During {{during}}", "condition-type-repeating": "Repeating", "condition-type-required": "Condition type is required.", "condition-repeating-value": "Count of events", "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.", + "condition-repeat-times": "Repeats { count, plural, 1 {1 time} other {# times} }", "schedule-type": "Scheduler type", "schedule-type-required": "Scheduler type is required.", "schedule": "Schedule", + "edit-schedule": "Edit alarm schedule", "schedule-any-time": "Active all the time", "schedule-specific-time": "Active at a specific time", "schedule-custom": "Custom", @@ -956,7 +961,8 @@ "schedule-days": "Days", "schedule-time": "Time", "schedule-time-from": "From", - "schedule-time-to": "To" + "schedule-time-to": "To", + "schedule-days-of-week-required": "At least one day of week should be selected." }, "dialog": { "close": "Close dialog"