Browse Source

Improve device profile alarm rules UI

pull/3576/head
Igor Kulikov 6 years ago
parent
commit
155b967b47
  1. 19
      ui-ngx/src/app/modules/home/components/home-components.module.ts
  2. 124
      ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition-dialog.component.html
  3. 20
      ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition-dialog.component.scss
  4. 150
      ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition-dialog.component.ts
  5. 8
      ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition.component.html
  6. 11
      ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition.component.scss
  7. 85
      ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition.component.ts
  8. 118
      ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.html
  9. 24
      ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.scss
  10. 93
      ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.ts
  11. 15
      ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule-dialog.component.html
  12. 34
      ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule-dialog.component.ts
  13. 33
      ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule-info.component.html
  14. 34
      ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule-info.component.scss
  15. 137
      ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule-info.component.ts
  16. 191
      ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule.component.html
  17. 83
      ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule.component.ts
  18. 1
      ui-ngx/src/app/modules/home/components/profile/alarm/create-alarm-rules.component.scss
  19. 2
      ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarm.component.html
  20. 1
      ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarm.component.ts
  21. 3
      ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarms.component.html
  22. 53
      ui-ngx/src/app/modules/home/components/profile/alarm/edit-alarm-details-dialog.component.html
  23. 80
      ui-ngx/src/app/modules/home/components/profile/alarm/edit-alarm-details-dialog.component.ts
  24. 28
      ui-ngx/src/app/modules/home/pages/device-profile/device-profile-tabs.component.html
  25. 61
      ui-ngx/src/app/shared/models/device.models.ts
  26. 32
      ui-ngx/src/app/shared/models/query/query.models.ts
  27. 10
      ui-ngx/src/assets/locale/locale.constant-en_US.json

19
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,

124
ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition-dialog.component.html

@ -0,0 +1,124 @@
<!--
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.
-->
<form [formGroup]="conditionFormGroup" (ngSubmit)="save()" style="width: 700px;">
<mat-toolbar color="primary">
<h2>{{ (readonly ? 'device-profile.alarm-rule-condition' : 'device-profile.edit-alarm-rule-condition') | translate }}</h2>
<span fxFlex></span>
<button mat-icon-button
(click)="cancel()"
type="button">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
<div mat-dialog-content>
<fieldset [disabled]="isLoading$ | async">
<div fxFlex fxLayout="column">
<tb-key-filter-list
[displayUserParameters]="false"
[allowUserDynamicSource]="false"
[telemetryKeysOnly]="true"
formControlName="keyFilters">
</tb-key-filter-list>
<section formGroupName="spec" class="row">
<mat-form-field class="mat-block" hideRequiredMarker>
<mat-label translate>device-profile.condition-type</mat-label>
<mat-select formControlName="type" required>
<mat-option *ngFor="let alarmConditionType of alarmConditionTypes" [value]="alarmConditionType">
{{ alarmConditionTypeTranslation.get(alarmConditionType) | translate }}
</mat-option>
</mat-select>
<mat-error *ngIf="conditionFormGroup.get('spec.type').hasError('required')">
{{ 'device-profile.condition-type-required' | translate }}
</mat-error>
</mat-form-field>
<div fxLayout="row" fxLayoutGap="8px" *ngIf="conditionFormGroup.get('spec.type').value == AlarmConditionType.DURATION">
<mat-form-field class="mat-block" hideRequiredMarker fxFlex floatLabel="always">
<mat-label></mat-label>
<input type="number" required
step="1" min="1" max="2147483647" matInput
placeholder="{{ 'device-profile.condition-duration-value' | translate }}"
formControlName="value">
<mat-error *ngIf="conditionFormGroup.get('spec.value').hasError('required')">
{{ 'device-profile.condition-duration-value-required' | translate }}
</mat-error>
<mat-error *ngIf="conditionFormGroup.get('spec.value').hasError('min')">
{{ 'device-profile.condition-duration-value-range' | translate }}
</mat-error>
<mat-error *ngIf="conditionFormGroup.get('spec.value').hasError('max')">
{{ 'device-profile.condition-duration-value-range' | translate }}
</mat-error>
<mat-error *ngIf="conditionFormGroup.get('spec.value').hasError('pattern')">
{{ 'device-profile.condition-duration-value-pattern' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field class="mat-block" hideRequiredMarker fxFlex floatLabel="always">
<mat-label></mat-label>
<mat-select formControlName="unit"
required
placeholder="{{ 'device-profile.condition-duration-time-unit' | translate }}">
<mat-option *ngFor="let timeUnit of timeUnits" [value]="timeUnit">
{{ timeUnitTranslations.get(timeUnit) | translate }}
</mat-option>
</mat-select>
<mat-error *ngIf="conditionFormGroup.get('spec.unit').hasError('required')">
{{ 'device-profile.condition-duration-time-unit-required' | translate }}
</mat-error>
</mat-form-field>
</div>
<div fxLayout="row" fxLayoutGap="8px" *ngIf="conditionFormGroup.get('spec.type').value == AlarmConditionType.REPEATING">
<mat-form-field class="mat-block" hideRequiredMarker fxFlex floatLabel="always">
<mat-label></mat-label>
<input type="number" required
step="1" min="1" max="2147483647" matInput
placeholder="{{ 'device-profile.condition-repeating-value' | translate }}"
formControlName="count">
<mat-error *ngIf="conditionFormGroup.get('spec.count').hasError('required')">
{{ 'device-profile.condition-repeating-value-required' | translate }}
</mat-error>
<mat-error *ngIf="conditionFormGroup.get('spec.count').hasError('min')">
{{ 'device-profile.condition-repeating-value-range' | translate }}
</mat-error>
<mat-error *ngIf="conditionFormGroup.get('spec.count').hasError('max')">
{{ 'device-profile.condition-repeating-value-range' | translate }}
</mat-error>
<mat-error *ngIf="conditionFormGroup.get('spec.count').hasError('pattern')">
{{ 'device-profile.condition-repeating-value-pattern' | translate }}
</mat-error>
</mat-form-field>
</div>
</section>
</div>
</fieldset>
</div>
<div mat-dialog-actions fxLayoutAlign="end center">
<button mat-raised-button color="primary"
*ngIf="!readonly"
type="submit"
[disabled]="(isLoading$ | async) || conditionFormGroup.invalid || !conditionFormGroup.dirty">
{{ 'action.save' | translate }}
</button>
<button mat-button color="primary"
type="button"
[disabled]="(isLoading$ | async)"
(click)="cancel()" cdkFocusInitial>
{{ (readonly ? 'action.close' : 'action.cancel') | translate }}
</button>
</div>
</form>

20
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;
}
}

150
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<AlarmRuleConditionDialogComponent, AlarmCondition>
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<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: AlarmRuleConditionDialogData,
@SkipSelf() private errorStateMatcher: ErrorStateMatcher,
public dialogRef: MatDialogRef<AlarmRuleConditionDialogComponent, AlarmCondition>,
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);
}
}

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

@ -15,8 +15,9 @@
limitations under the License.
-->
<div fxLayout="column" fxFlex>
<div fxLayout="column" fxFlex [formGroup]="alarmRuleConditionFormGroup">
<div fxLayout="row" fxLayoutAlign="start center" style="min-height: 40px;">
<label class="tb-title" translate>device-profile.condition</label>
<span fxFlex></span>
<a mat-button color="primary"
type="button"
@ -27,9 +28,12 @@
</a>
</div>
<div class="tb-alarm-rule-condition" fxFlex fxLayout="column" fxLayoutAlign="center" (click)="openFilterDialog($event)">
<tb-filter-text [formControl]="alarmRuleConditionControl"
<tb-filter-text formControlName="condition"
required
addFilterPrompt="{{'device-profile.enter-alarm-rule-condition-prompt' | translate}}">
</tb-filter-text>
<span class="tb-alarm-rule-condition-spec" [ngClass]="{disabled: this.disabled}" [innerHTML]="specText">
</span>
</div>
</div>

11
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;
}
}
}
}

85
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<KeyFilter>;
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<KeyFilter>): 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, AlarmRuleKeyFiltersDialogData,
Array<KeyFilter>>(AlarmRuleKeyFiltersDialogComponent, {
this.dialog.open<AlarmRuleConditionDialogComponent, AlarmRuleConditionDialogData,
AlarmCondition>(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() {

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

@ -16,90 +16,36 @@
-->
<div fxLayout="column" [formGroup]="alarmRuleFormGroup">
<mat-tab-group>
<mat-tab label="{{ 'device-profile.condition' | translate }}" formGroupName="condition">
<tb-alarm-rule-condition fxFlex class="row"
formControlName="condition">
</tb-alarm-rule-condition>
<section formGroupName="spec" class="row">
<mat-form-field class="mat-block" hideRequiredMarker>
<mat-label translate>device-profile.condition-type</mat-label>
<mat-select formControlName="type" required>
<mat-option *ngFor="let alarmConditionType of alarmConditionTypes" [value]="alarmConditionType">
{{ alarmConditionTypeTranslation.get(alarmConditionType) | translate }}
</mat-option>
</mat-select>
<mat-error *ngIf="alarmRuleFormGroup.get('condition.spec.type').hasError('required')">
{{ 'device-profile.condition-type-required' | translate }}
</mat-error>
</mat-form-field>
<div fxLayout="row" fxLayoutGap="8px" *ngIf="alarmRuleFormGroup.get('condition.spec.type').value == AlarmConditionType.DURATION">
<mat-form-field class="mat-block" hideRequiredMarker fxFlex floatLabel="always">
<mat-label></mat-label>
<input type="number" required
step="1" min="1" max="2147483647" matInput
placeholder="{{ 'device-profile.condition-duration-value' | translate }}"
formControlName="value">
<mat-error *ngIf="alarmRuleFormGroup.get('condition.spec.value').hasError('required')">
{{ 'device-profile.condition-duration-value-required' | translate }}
</mat-error>
<mat-error *ngIf="alarmRuleFormGroup.get('condition.spec.value').hasError('min')">
{{ 'device-profile.condition-duration-value-range' | translate }}
</mat-error>
<mat-error *ngIf="alarmRuleFormGroup.get('condition.spec.value').hasError('max')">
{{ 'device-profile.condition-duration-value-range' | translate }}
</mat-error>
<mat-error *ngIf="alarmRuleFormGroup.get('condition.spec.value').hasError('pattern')">
{{ 'device-profile.condition-duration-value-pattern' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field class="mat-block" hideRequiredMarker fxFlex floatLabel="always">
<mat-label></mat-label>
<mat-select formControlName="unit"
required
placeholder="{{ 'device-profile.condition-duration-time-unit' | translate }}">
<mat-option *ngFor="let timeUnit of timeUnits" [value]="timeUnit">
{{ timeUnitTranslations.get(timeUnit) | translate }}
</mat-option>
</mat-select>
<mat-error *ngIf="alarmRuleFormGroup.get('condition.spec.unit').hasError('required')">
{{ 'device-profile.condition-duration-time-unit-required' | translate }}
</mat-error>
</mat-form-field>
</div>
<div fxLayout="row" fxLayoutGap="8px" *ngIf="alarmRuleFormGroup.get('condition.spec.type').value == AlarmConditionType.REPEATING">
<mat-form-field class="mat-block" hideRequiredMarker fxFlex floatLabel="always">
<mat-label></mat-label>
<input type="number" required
step="1" min="1" max="2147483647" matInput
placeholder="{{ 'device-profile.condition-repeating-value' | translate }}"
formControlName="count">
<mat-error *ngIf="alarmRuleFormGroup.get('condition.spec.count').hasError('required')">
{{ 'device-profile.condition-repeating-value-required' | translate }}
</mat-error>
<mat-error *ngIf="alarmRuleFormGroup.get('condition.spec.count').hasError('min')">
{{ 'device-profile.condition-repeating-value-range' | translate }}
</mat-error>
<mat-error *ngIf="alarmRuleFormGroup.get('condition.spec.count').hasError('max')">
{{ 'device-profile.condition-repeating-value-range' | translate }}
</mat-error>
<mat-error *ngIf="alarmRuleFormGroup.get('condition.spec.count').hasError('pattern')">
{{ 'device-profile.condition-repeating-value-pattern' | translate }}
</mat-error>
</mat-form-field>
</div>
</section>
</mat-tab>
<mat-tab label="{{ 'device-profile.schedule' | translate }}">
<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">
<mat-label translate>device-profile.alarm-details</mat-label>
<textarea matInput formControlName="alarmDetails" rows="5"></textarea>
</mat-form-field>
</mat-tab>
</mat-tab-group>
<tb-alarm-rule-condition fxFlex class="row"
formControlName="condition">
</tb-alarm-rule-condition>
<mat-divider class="row"></mat-divider>
<tb-alarm-schedule-info fxFlex class="row"
formControlName="schedule">
</tb-alarm-schedule-info>
<mat-divider class="row"></mat-divider>
<div fxLayout="column" fxFlex class="tb-alarm-rule-details row">
<div fxLayout="row" fxLayoutAlign="start center" style="min-height: 40px;">
<label class="tb-title" translate>device-profile.alarm-rule-details</label>
<span fxFlex></span>
<a mat-button color="primary"
*ngIf="!disabled"
type="button"
(click)="openEditDetailsDialog($event)"
matTooltip="{{ 'action.edit' | translate }}"
matTooltipPosition="above">
{{ 'action.edit' | translate }}
</a>
</div>
<div fxLayout="row" fxLayoutAlign="start start">
<div class="tb-alarm-rule-details-content" [ngClass]="{disabled: this.disabled, collapsed: !this.expandAlarmDetails}"
(click)="!disabled ? openEditDetailsDialog($event) : {}"
fxFlex [innerHTML]="alarmRuleFormGroup.get('alarmDetails').value"></div>
<a mat-button color="primary"
type="button"
(click)="expandAlarmDetails = !expandAlarmDetails">
{{ (expandAlarmDetails ? 'action.hide' : 'action.read-more') | translate }}
</a>
</div>
</div>
</div>

24
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;
}
}
}
}

93
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, EditAlarmDetailsDialogData,
string>(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) {

15
ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-key-filters-dialog.component.html → ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule-dialog.component.html

@ -15,9 +15,9 @@
limitations under the License.
-->
<form [formGroup]="keyFiltersFormGroup" (ngSubmit)="save()" style="width: 700px;">
<form [formGroup]="alarmScheduleFormGroup" (ngSubmit)="save()" style="width: 800px;">
<mat-toolbar color="primary">
<h2>{{ (readonly ? 'device-profile.alarm-rule-condition' : 'device-profile.edit-alarm-rule-condition') | translate }}</h2>
<h2>{{ (readonly ? 'device-profile.schedule' : 'device-profile.edit-schedule') | translate }}</h2>
<span fxFlex></span>
<button mat-icon-button
(click)="cancel()"
@ -30,12 +30,9 @@
<div mat-dialog-content>
<fieldset [disabled]="isLoading$ | async">
<div fxFlex fxLayout="column">
<tb-key-filter-list
[displayUserParameters]="false"
[allowUserDynamicSource]="false"
[telemetryKeysOnly]="true"
formControlName="keyFilters">
</tb-key-filter-list>
<tb-alarm-schedule
formControlName="alarmSchedule">
</tb-alarm-schedule>
</div>
</fieldset>
</div>
@ -43,7 +40,7 @@
<button mat-raised-button color="primary"
*ngIf="!readonly"
type="submit"
[disabled]="(isLoading$ | async) || keyFiltersFormGroup.invalid || !keyFiltersFormGroup.dirty">
[disabled]="(isLoading$ | async) || alarmScheduleFormGroup.invalid || !alarmScheduleFormGroup.dirty">
{{ 'action.save' | translate }}
</button>
<button mat-button color="primary"

34
ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-key-filters-dialog.component.ts → ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule-dialog.component.ts

@ -19,49 +19,49 @@ 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 { 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';
import { KeyFilter, keyFilterInfosToKeyFilters, keyFiltersToKeyFilterInfos } from '@shared/models/query/query.models';
import { AlarmSchedule } from '@shared/models/device.models';
export interface AlarmRuleKeyFiltersDialogData {
export interface AlarmScheduleDialogData {
readonly: boolean;
keyFilters: Array<KeyFilter>;
alarmSchedule: AlarmSchedule;
}
@Component({
selector: 'tb-alarm-rule-key-filters-dialog',
templateUrl: './alarm-rule-key-filters-dialog.component.html',
providers: [{provide: ErrorStateMatcher, useExisting: AlarmRuleKeyFiltersDialogComponent}],
selector: 'tb-alarm-schedule-dialog',
templateUrl: './alarm-schedule-dialog.component.html',
providers: [{provide: ErrorStateMatcher, useExisting: AlarmScheduleDialogComponent}],
styleUrls: []
})
export class AlarmRuleKeyFiltersDialogComponent extends DialogComponent<AlarmRuleKeyFiltersDialogComponent, Array<KeyFilter>>
export class AlarmScheduleDialogComponent extends DialogComponent<AlarmScheduleDialogComponent, AlarmSchedule>
implements OnInit, ErrorStateMatcher {
readonly = this.data.readonly;
keyFilters = this.data.keyFilters;
alarmSchedule = this.data.alarmSchedule;
keyFiltersFormGroup: FormGroup;
alarmScheduleFormGroup: FormGroup;
submitted = false;
constructor(protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: AlarmRuleKeyFiltersDialogData,
@Inject(MAT_DIALOG_DATA) public data: AlarmScheduleDialogData,
@SkipSelf() private errorStateMatcher: ErrorStateMatcher,
public dialogRef: MatDialogRef<AlarmRuleKeyFiltersDialogComponent, Array<KeyFilter>>,
public dialogRef: MatDialogRef<AlarmScheduleDialogComponent, AlarmSchedule>,
private fb: FormBuilder,
private utils: UtilsService,
public translate: TranslateService) {
super(store, router, dialogRef);
this.keyFiltersFormGroup = this.fb.group({
keyFilters: [keyFiltersToKeyFilterInfos(this.keyFilters), Validators.required]
this.alarmScheduleFormGroup = this.fb.group({
alarmSchedule: [this.alarmSchedule]
});
if (this.readonly) {
this.keyFiltersFormGroup.disable({emitEvent: false});
this.alarmScheduleFormGroup.disable({emitEvent: false});
}
}
@ -80,7 +80,7 @@ export class AlarmRuleKeyFiltersDialogComponent extends DialogComponent<AlarmRul
save(): void {
this.submitted = true;
this.keyFilters = keyFilterInfosToKeyFilters(this.keyFiltersFormGroup.get('keyFilters').value);
this.dialogRef.close(this.keyFilters);
this.alarmSchedule = this.alarmScheduleFormGroup.get('alarmSchedule').value;
this.dialogRef.close(this.alarmSchedule);
}
}

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

@ -0,0 +1,33 @@
<!--
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.
-->
<div fxLayout="column" fxFlex>
<div fxLayout="row" fxLayoutAlign="start center" style="min-height: 40px;">
<label class="tb-title" translate>device-profile.schedule</label>
<span fxFlex></span>
<a mat-button color="primary"
type="button"
(click)="openScheduleDialog($event)"
matTooltip="{{ (disabled ? 'action.view' : 'action.edit') | translate }}"
matTooltipPosition="above">
{{ (disabled ? 'action.view' : 'action.edit' ) | translate }}
</a>
</div>
<sapn class="tb-alarm-rule-schedule" [ngClass]="{disabled: this.disabled}" (click)="openScheduleDialog($event)"
[innerHTML]="scheduleText">
</sapn>
</div>

34
ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule-info.component.scss

@ -0,0 +1,34 @@
/**
* 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 {
display: flex;
a.mat-button {
&:hover, &:focus {
border-bottom: none;
}
}
.tb-alarm-rule-schedule {
line-height: 1.8em;
padding: 4px;
cursor: pointer;
&.disabled {
opacity: 0.7;
}
.nowrap {
white-space: nowrap;
}
}
}

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

@ -0,0 +1,137 @@
///
/// 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 { ChangeDetectorRef, Component, forwardRef, Input, OnInit } from '@angular/core';
import { ControlValueAccessor, FormBuilder, NG_VALUE_ACCESSOR } from '@angular/forms';
import {
AlarmSchedule,
AlarmScheduleType,
AlarmScheduleTypeTranslationMap, dayOfWeekTranslations,
getAlarmScheduleRangeText, utcTimestampToTimeOfDay
} from '@shared/models/device.models';
import { MatDialog } from '@angular/material/dialog';
import {
AlarmScheduleDialogComponent,
AlarmScheduleDialogData
} from '@home/components/profile/alarm/alarm-schedule-dialog.component';
import { deepClone, isDefinedAndNotNull } from '@core/utils';
import { TranslateService } from '@ngx-translate/core';
@Component({
selector: 'tb-alarm-schedule-info',
templateUrl: './alarm-schedule-info.component.html',
styleUrls: ['./alarm-schedule-info.component.scss'],
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => AlarmScheduleInfoComponent),
multi: true
}]
})
export class AlarmScheduleInfoComponent implements ControlValueAccessor, OnInit {
@Input()
disabled: boolean;
private modelValue: AlarmSchedule;
scheduleText = '';
private propagateChange = (v: any) => { };
constructor(private dialog: MatDialog,
private translate: TranslateService,
private cd: ChangeDetectorRef) {
}
ngOnInit(): void {
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
}
writeValue(value: AlarmSchedule): void {
this.modelValue = value;
this.updateScheduleText();
}
private updateScheduleText() {
let schedule = this.modelValue;
if (!isDefinedAndNotNull(schedule)) {
schedule = {
type: AlarmScheduleType.ANY_TIME
};
}
this.scheduleText = '';
switch (schedule.type) {
case AlarmScheduleType.ANY_TIME:
this.scheduleText = this.translate.instant('device-profile.schedule-any-time');
break;
case AlarmScheduleType.SPECIFIC_TIME:
for (const day of schedule.daysOfWeek) {
if (this.scheduleText.length) {
this.scheduleText += ', ';
}
this.scheduleText += this.translate.instant(dayOfWeekTranslations[day - 1]);
}
this.scheduleText += ' <b>' + getAlarmScheduleRangeText(utcTimestampToTimeOfDay(schedule.startsOn),
utcTimestampToTimeOfDay(schedule.endsOn)) + '</b>';
break;
case AlarmScheduleType.CUSTOM:
for (const item of schedule.items) {
if (item.enabled) {
if (this.scheduleText.length) {
this.scheduleText += '<br/>';
}
this.scheduleText += this.translate.instant(dayOfWeekTranslations[item.dayOfWeek - 1]);
this.scheduleText += ' <b>' + getAlarmScheduleRangeText(utcTimestampToTimeOfDay(item.startsOn),
utcTimestampToTimeOfDay(item.endsOn)) + '</b>';
}
}
break;
}
}
public openScheduleDialog($event: Event) {
if ($event) {
$event.stopPropagation();
}
this.dialog.open<AlarmScheduleDialogComponent, AlarmScheduleDialogData,
AlarmSchedule>(AlarmScheduleDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
readonly: this.disabled,
alarmSchedule: this.disabled ? this.modelValue : deepClone(this.modelValue)
}
}).afterClosed().subscribe((result) => {
if (result) {
this.modelValue = result;
this.propagateChange(this.modelValue);
this.updateScheduleText();
this.cd.detectChanges();
}
});
}
}

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

@ -35,33 +35,20 @@
</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-md="row" fxLayoutGap="16px" style="padding-bottom: 16px;">
<div fxLayout="column" fxLayout.gt-md="row" fxLayoutGap="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 *ngFor="let day of firstRowDays" [formControl]="weeklyRepeatControl(day)">
{{ dayOfWeekTranslationsArray[day] | 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 *ngFor="let day of secondRowDays" [formControl]="weeklyRepeatControl(day)">
{{ dayOfWeekTranslationsArray[day] | translate }}
</mat-checkbox>
</div>
</div>
<tb-error style="display: block;" [error]="alarmScheduleForm.get('daysOfWeek').hasError('dayOfWeeks')
? ('device-profile.schedule-days-of-week-required' | translate) : ''"></tb-error>
<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">
<div fxLayout="row" fxLayoutGap="8px" fxFlex.gt-md>
@ -87,169 +74,35 @@
</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" formArrayName="items" fxLayoutGap="1em">
<div fxLayout.xs="column" fxLayout="row" fxLayoutGap="8px" formGroupName="0" fxLayoutAlign="start center" fxLayoutAlign.xs="center start">
<mat-checkbox formControlName="enabled" fxFlex="17" (change)="changeCustomScheduler($event, 0)">
{{ 'device-profile.schedule-day.monday' | translate }}
</mat-checkbox>
<div fxLayout="row" fxLayoutGap="8px" fxFlex>
<mat-form-field fxFlex.xs fxFlex.sm="100px" fxFlex.md="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.xs fxFlex.sm="100px" fxFlex.md="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 fxFlex fxLayoutAlign="center center"
style="text-align: center"
[innerHTML]="getSchedulerRangeText(itemsSchedulerForm.at(0))">
</div>
</div>
<div fxLayout.xs="column" fxLayout="row" fxLayoutGap="8px" formGroupName="1" fxLayoutAlign="start center" fxLayoutAlign.xs="center start">
<mat-checkbox formControlName="enabled" fxFlex="17" (change)="changeCustomScheduler($event, 1)">
{{ 'device-profile.schedule-day.tuesday' | translate }}
</mat-checkbox>
<div fxLayout="row" fxLayoutGap="8px" fxFlex>
<mat-form-field fxFlex.xs fxFlex.sm="100px" fxFlex.md="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.xs fxFlex.sm="100px" fxFlex.md="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 fxFlex fxLayoutAlign="center center"
style="text-align: center"
[innerHTML]="getSchedulerRangeText(itemsSchedulerForm.at(1))">
</div>
</div>
<div fxLayout.xs="column" fxLayout="row" fxLayoutGap="8px" formGroupName="2" fxLayoutAlign="start center" fxLayoutAlign.xs="center start">
<mat-checkbox formControlName="enabled" fxFlex="17" (change)="changeCustomScheduler($event, 2)">
{{ 'device-profile.schedule-day.wednesday' | translate }}
</mat-checkbox>
<div fxLayout="row" fxLayoutGap="8px" fxFlex>
<mat-form-field fxFlex.xs fxFlex.sm="100px" fxFlex.md="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.xs fxFlex.sm="100px" fxFlex.md="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 fxFlex fxLayoutAlign="center center"
style="text-align: center"
[innerHTML]="getSchedulerRangeText(itemsSchedulerForm.at(2))">
</div>
</div>
<div fxLayout.xs="column" fxLayout="row" fxLayoutGap="8px" formGroupName="3" fxLayoutAlign="start center" fxLayoutAlign.xs="center start">
<mat-checkbox formControlName="enabled" fxFlex="17" (change)="changeCustomScheduler($event, 3)">
{{ 'device-profile.schedule-day.thursday' | translate }}
</mat-checkbox>
<div fxLayout="row" fxLayoutGap="8px" fxFlex>
<mat-form-field fxFlex.xs fxFlex.sm="100px" fxFlex.md="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.xs fxFlex.sm="100px" fxFlex.md="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 fxFlex fxLayoutAlign="center center"
style="text-align: center"
[innerHTML]="getSchedulerRangeText(itemsSchedulerForm.at(3))">
</div>
</div>
<div fxLayout.xs="column" fxLayout="row" fxLayoutGap="8px" formGroupName="4" fxLayoutAlign="start center" fxLayoutAlign.xs="center start">
<mat-checkbox formControlName="enabled" fxFlex="17" (change)="changeCustomScheduler($event, 4)">
{{ 'device-profile.schedule-day.friday' | translate }}
</mat-checkbox>
<div fxLayout="row" fxLayoutGap="8px" fxFlex>
<mat-form-field fxFlex.xs fxFlex.sm="100px" fxFlex.md="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.xs fxFlex.sm="100px" fxFlex.md="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 fxFlex fxLayoutAlign="center center"
style="text-align: center"
[innerHTML]="getSchedulerRangeText(itemsSchedulerForm.at(4))">
</div>
</div>
<div fxLayout.xs="column" fxLayout="row" fxLayoutGap="8px" formGroupName="5" fxLayoutAlign="start center" fxLayoutAlign.xs="center start">
<mat-checkbox formControlName="enabled" fxFlex="17" (change)="changeCustomScheduler($event, 5)">
{{ 'device-profile.schedule-day.saturday' | translate }}
</mat-checkbox>
<div fxLayout="row" fxLayoutGap="8px" fxFlex>
<mat-form-field fxFlex.xs fxFlex.sm="100px" fxFlex.md="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.xs fxFlex.sm="100px" fxFlex.md="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 fxFlex fxLayoutAlign="center center"
style="text-align: center"
[innerHTML]="getSchedulerRangeText(itemsSchedulerForm.at(5))">
</div>
</div>
<div fxLayout.xs="column" fxLayout="row" fxLayoutGap="8px" formGroupName="6" fxLayoutAlign="start center" fxLayoutAlign.xs="center start">
<mat-checkbox formControlName="enabled" fxFlex="17" (change)="changeCustomScheduler($event, 6)">
{{ 'device-profile.schedule-day.sunday' | translate }}
<div *ngFor="let day of allDays" fxLayout="column" formArrayName="items" fxLayoutGap="1em">
<div fxLayout.xs="column" fxLayout="row" fxLayoutGap="8px" [formGroupName]="''+day" fxLayoutAlign="start center" fxLayoutAlign.xs="center start">
<mat-checkbox formControlName="enabled" fxFlex="17" (change)="changeCustomScheduler($event, day)">
{{ dayOfWeekTranslationsArray[day] | translate }}
</mat-checkbox>
<div fxLayout="row" fxLayoutGap="8px" fxFlex>
<mat-form-field fxFlex.xs fxFlex.sm="100px" fxFlex.md="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-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.xs fxFlex.sm="100px" fxFlex.md="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-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>
<div fxFlex fxLayoutAlign="center center"
style="text-align: center"
[innerHTML]="getSchedulerRangeText(itemsSchedulerForm.at(6))">
[innerHTML]="getSchedulerRangeText(itemsSchedulerForm.at(day))">
</div>
</div>
</div>
<tb-error style="display: block;" [error]="alarmScheduleForm.get('items').hasError('dayOfWeeks')
? ('device-profile.schedule-days-of-week-required' | translate) : ''"></tb-error>
</section>
</div>
</section>

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

@ -28,7 +28,12 @@ import {
Validator,
Validators
} from '@angular/forms';
import { AlarmSchedule, AlarmScheduleType, AlarmScheduleTypeTranslationMap } from '@shared/models/device.models';
import {
AlarmSchedule,
AlarmScheduleType,
AlarmScheduleTypeTranslationMap,
dayOfWeekTranslations, getAlarmScheduleRangeText, timeOfDayToUTCTimestamp, utcTimestampToTimeOfDay
} from '@shared/models/device.models';
import { isDefined, isDefinedAndNotNull } from '@core/utils';
import * as _moment from 'moment-timezone';
import { MatCheckboxChange } from '@angular/material/checkbox';
@ -59,11 +64,18 @@ export class AlarmScheduleComponent implements ControlValueAccessor, Validator,
alarmScheduleType = AlarmScheduleType;
alarmScheduleTypeTranslate = AlarmScheduleTypeTranslationMap;
dayOfWeekTranslationsArray = dayOfWeekTranslations;
allDays = Array(7).fill(0).map((x, i) => i);
firstRowDays = Array(4).fill(0).map((x, i) => i);
secondRowDays = Array(3).fill(0).map((x, i) => i + 4);
private modelValue: AlarmSchedule;
private defaultItems = Array.from({length: 7}, (value, i) => ({
enabled: true,
dayOfWeek: i
dayOfWeek: i + 1
}));
private propagateChange = (v: any) => { };
@ -75,10 +87,10 @@ export class AlarmScheduleComponent implements ControlValueAccessor, Validator,
this.alarmScheduleForm = this.fb.group({
type: [AlarmScheduleType.ANY_TIME, Validators.required],
timezone: [null, Validators.required],
daysOfWeek: this.fb.array(new Array(7).fill(false)),
daysOfWeek: this.fb.array(new Array(7).fill(false), this.validateDayOfWeeks),
startsOn: [0, Validators.required],
endsOn: [0, Validators.required],
items: this.fb.array(Array.from({length: 7}, (value, i) => this.defaultItemsScheduler(i)))
items: this.fb.array(Array.from({length: 7}, (value, i) => this.defaultItemsScheduler(i)), this.validateItems)
});
this.alarmScheduleForm.get('type').valueChanges.subscribe((type) => {
this.alarmScheduleForm.reset({type, items: this.defaultItems, timezone: this.defaultTimezone}, {emitEvent: false});
@ -90,6 +102,26 @@ export class AlarmScheduleComponent implements ControlValueAccessor, Validator,
});
}
validateDayOfWeeks(control: AbstractControl): ValidationErrors | null {
const dayOfWeeks: boolean[] = control.value;
if (!dayOfWeeks || !dayOfWeeks.length || !dayOfWeeks.find(v => v === true)) {
return {
dayOfWeeks: true
};
}
return null;
}
validateItems(control: AbstractControl): ValidationErrors | null {
const items: any[] = control.value;
if (!items || !items.length || !items.find(v => v.enabled === true)) {
return {
dayOfWeeks: true
};
}
return null;
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
@ -123,8 +155,8 @@ export class AlarmScheduleComponent implements ControlValueAccessor, Validator,
type: this.modelValue.type,
timezone: this.modelValue.timezone,
daysOfWeek,
startsOn: this.timestampToTime(this.modelValue.startsOn),
endsOn: this.timestampToTime(this.modelValue.endsOn)
startsOn: utcTimestampToTimeOfDay(this.modelValue.startsOn),
endsOn: utcTimestampToTimeOfDay(this.modelValue.endsOn)
}, {emitEvent: false});
break;
case AlarmScheduleType.CUSTOM:
@ -136,8 +168,8 @@ export class AlarmScheduleComponent implements ControlValueAccessor, Validator,
this.disabledSelectedTime(item.enabled, index);
alarmDays.push({
enabled: item.enabled,
startsOn: this.timestampToTime(item.startsOn),
endsOn: this.timestampToTime(item.endsOn)
startsOn: utcTimestampToTimeOfDay(item.startsOn),
endsOn: utcTimestampToTimeOfDay(item.endsOn)
});
});
this.alarmScheduleForm.patchValue({
@ -202,15 +234,15 @@ export class AlarmScheduleComponent implements ControlValueAccessor, Validator,
.filter(day => !!day);
}
if (isDefined(value.startsOn) && value.startsOn !== 0) {
value.startsOn = this.timeToTimestampUTC(value.startsOn);
value.startsOn = timeOfDayToUTCTimestamp(value.startsOn);
}
if (isDefined(value.endsOn) && value.endsOn !== 0) {
value.endsOn = this.timeToTimestampUTC(value.endsOn);
value.endsOn = timeOfDayToUTCTimestamp(value.endsOn);
}
if (isDefined(value.items)){
value.items = this.alarmScheduleForm.getRawValue().items;
value.items = value.items.map((item) => {
return { ...item, startsOn: this.timeToTimestampUTC(item.startsOn), endsOn: this.timeToTimestampUTC(item.endsOn)};
return { ...item, startsOn: timeOfDayToUTCTimestamp(item.startsOn), endsOn: timeOfDayToUTCTimestamp(item.endsOn)};
});
}
this.modelValue = value;
@ -218,21 +250,11 @@ export class AlarmScheduleComponent implements ControlValueAccessor, Validator,
}
}
private timeToTimestampUTC(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],
dayOfWeek: [index + 1],
startsOn: [0, Validators.required],
endsOn: [0, Validators.required]
});
@ -253,23 +275,8 @@ export class AlarmScheduleComponent implements ControlValueAccessor, Validator,
}
}
private timeToMoment(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]);
}
getSchedulerRangeText(control: FormGroup | AbstractControl): string {
const start = this.timeToMoment(control.get('startsOn').value);
const end = this.timeToMoment(control.get('endsOn').value);
if (start < end) {
return `<span><span class="nowrap">${start.format('hh:mm A')}</span> – <span class="nowrap">${end.format('hh:mm A')}</span></span>`;
} else if (start.valueOf() === 0 && end.valueOf() === 0 || start.isSame(_moment([1970, 0])) && end.isSame(_moment([1970, 0]))) {
return '<span><span class="nowrap">12:00 AM</span> – <span class="nowrap">12:00 PM</span></span>';
}
return `<span><span class="nowrap">12:00 AM</span> – <span class="nowrap">${end.format('hh:mm A')}</span>` +
` and <span class="nowrap">${start.format('hh:mm A')}</span> – <span class="nowrap">12:00 PM</span></span>`;
return getAlarmScheduleRangeText(control.get('startsOn').value, control.get('endsOn').value);
}
get itemsSchedulerForm(): FormArray {

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

@ -19,6 +19,7 @@
border: 2px groove rgba(0, 0, 0, .45);
border-radius: 4px;
padding: 8px;
min-width: 0;
}
}

2
ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarm.component.html

@ -56,7 +56,7 @@
<mat-checkbox formControlName="propagate" style="display: block; padding-bottom: 16px;">
{{ 'device-profile.propagate-alarm' | translate }}
</mat-checkbox>
<section *ngIf="alarmFormGroup.get('propagate').value === true">
<section *ngIf="alarmFormGroup.get('propagate').value === true" style="padding-bottom: 1em;">
<mat-form-field floatLabel="always" class="mat-block">
<mat-label translate>device-profile.alarm-rule-relation-types-list</mat-label>
<mat-chip-list #relationTypesChipList [disabled]="disabled">

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

@ -57,6 +57,7 @@ export class DeviceProfileAlarmComponent implements ControlValueAccessor, OnInit
separatorKeysCodes = [ENTER, COMMA, SEMICOLON];
@Input()
expanded = false;
private modelValue: DeviceProfileAlarm;

3
ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarms.component.html

@ -21,6 +21,7 @@
let $index = index; last as isLast;"
fxLayout="column" [ngStyle]="!isLast ? {paddingBottom: '8px'} : {}">
<tb-device-profile-alarm [formControl]="alarmControl"
[expanded]="$index === 0"
(removeAlarm)="removeAlarm($index)">
</tb-device-profile-alarm>
</div>
@ -29,7 +30,7 @@
<span translate fxLayoutAlign="center center"
class="tb-prompt">device-profile.no-alarm-rules</span>
</div>
<div *ngIf="!disabled" fxFlex fxLayout="row" fxLayoutAlign="end center"
<div *ngIf="!disabled" fxFlex fxLayout="row" fxLayoutAlign="start center"
style="padding-top: 16px;">
<button mat-raised-button color="primary"
type="button"

53
ui-ngx/src/app/modules/home/components/profile/alarm/edit-alarm-details-dialog.component.html

@ -0,0 +1,53 @@
<!--
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.
-->
<form [formGroup]="editDetailsFormGroup" (ngSubmit)="save()" style="width: 800px;">
<mat-toolbar color="primary">
<h2>{{ 'device-profile.alarm-rule-details' | translate }}</h2>
<span fxFlex></span>
<button mat-icon-button
(click)="cancel()"
type="button">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
<div mat-dialog-content>
<fieldset [disabled]="isLoading$ | async">
<div fxFlex fxLayout="column">
<mat-form-field class="mat-block">
<mat-label translate>device-profile.alarm-details</mat-label>
<textarea matInput formControlName="alarmDetails" rows="5"></textarea>
</mat-form-field>
</div>
</fieldset>
</div>
<div mat-dialog-actions fxLayoutAlign="end center">
<button mat-raised-button color="primary"
type="submit"
[disabled]="(isLoading$ | async) || editDetailsFormGroup.invalid || !editDetailsFormGroup.dirty">
{{ 'action.save' | translate }}
</button>
<button mat-button color="primary"
type="button"
[disabled]="(isLoading$ | async)"
(click)="cancel()" cdkFocusInitial>
{{ 'action.cancel' | translate }}
</button>
</div>
</form>

80
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<EditAlarmDetailsDialogComponent, string>
implements OnInit, ErrorStateMatcher {
alarmDetails = this.data.alarmDetails;
editDetailsFormGroup: FormGroup;
submitted = false;
constructor(protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: EditAlarmDetailsDialogData,
@SkipSelf() private errorStateMatcher: ErrorStateMatcher,
public dialogRef: MatDialogRef<EditAlarmDetailsDialogComponent, string>,
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);
}
}

28
ui-ngx/src/app/modules/home/pages/device-profile/device-profile-tabs.component.html

@ -61,29 +61,7 @@
</div>
</div>
</mat-tab>
<mat-tab *ngIf="entity && !isEdit"
label="{{ 'attribute.attributes' | translate }}" #attributesTab="matTab">
<tb-attribute-table [defaultAttributeScope]="attributeScopes.SERVER_SCOPE"
[active]="attributesTab.isActive"
[entityId]="entity.id"
[entityName]="entity.name">
</tb-attribute-table>
</mat-tab>
<mat-tab *ngIf="entity && !isEdit"
label="{{ 'attribute.latest-telemetry' | translate }}" #telemetryTab="matTab">
<tb-attribute-table [defaultAttributeScope]="latestTelemetryTypes.LATEST_TELEMETRY"
disableAttributeScopeSelection
[active]="telemetryTab.isActive"
[entityId]="entity.id"
[entityName]="entity.name">
</tb-attribute-table>
</mat-tab>
<mat-tab *ngIf="entity && !isEdit"
label="{{ 'alarm.alarms' | translate }}" #alarmsTab="matTab">
<tb-alarm-table [active]="alarmsTab.isActive" [entityId]="entity.id"></tb-alarm-table>
</mat-tab>
<mat-tab *ngIf="entity && !isEdit"
label="{{ 'tenant.events' | translate }}" #eventsTab="matTab">
<tb-event-table [defaultEventType]="eventTypes.ERROR" [active]="eventsTab.isActive" [tenantId]="nullUid"
[entityId]="entity.id"></tb-event-table>
<mat-tab *ngIf="entity"
label="{{ 'audit-log.audit-logs' | translate }}" #auditLogsTab="matTab">
<tb-audit-log-table detailsMode="true" [active]="auditLogsTab.isActive" [auditLogMode]="auditLogModes.ENTITY" [entityId]="entity.id"></tb-audit-log-table>
</mat-tab>

61
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<string>(
'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 `<span><span class="nowrap">${start.format('hh:mm A')}</span> – <span class="nowrap">${end.format('hh:mm A')}</span></span>`;
} else if (start.valueOf() === 0 && end.valueOf() === 0 || start.isSame(_moment([1970, 0])) && end.isSame(_moment([1970, 0]))) {
return '<span><span class="nowrap">12:00 AM</span> – <span class="nowrap">12:00 PM</span></span>';
}
return `<span><span class="nowrap">12:00 AM</span> – <span class="nowrap">${end.format('hh:mm A')}</span>` +
` and <span class="nowrap">${start.format('hh:mm A')}</span> – <span class="nowrap">12:00 PM</span></span>`;
}

32
ui-ngx/src/app/shared/models/query/query.models.ts

@ -476,21 +476,23 @@ export function keyFilterInfosToKeyFilters(keyFilterInfos: Array<KeyFilterInfo>)
export function keyFiltersToKeyFilterInfos(keyFilters: Array<KeyFilter>): Array<KeyFilterInfo> {
const keyFilterInfos: Array<KeyFilterInfo> = [];
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;

10
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 <b>{{during}}</b>",
"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 <b>{ count, plural, 1 {1 time} other {# times} }</b>",
"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"

Loading…
Cancel
Save