From 034832ddbe9c54b970df73ae4e9c26b0f1d675fc Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Thu, 2 Feb 2023 15:25:04 +0200 Subject: [PATCH] UI: Refactoring alarm rule --- .../notification-center.module.ts | 6 +- .../alarm-severities-list.component.html | 64 ++++ .../alarm-severities-list.component.ts | 214 +++++++++++++ .../rule-table/alarm-type-list.component.html | 36 +++ .../rule-table/alarm-type-list.component.ts | 132 ++++++++ .../rule-table/escalations.component.ts | 19 +- .../rule-notification-dialog.component.html | 219 ++++++------- .../rule-notification-dialog.component.scss | 31 +- .../rule-notification-dialog.component.ts | 299 ++++-------------- .../template-autocomplete.component.ts | 2 + .../template-notification-dialog.component.ts | 2 +- .../app/shared/models/notification.models.ts | 4 +- .../assets/locale/locale.constant-en_US.json | 100 +++--- 13 files changed, 689 insertions(+), 439 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/pages/notification-center/rule-table/alarm-severities-list.component.html create mode 100644 ui-ngx/src/app/modules/home/pages/notification-center/rule-table/alarm-severities-list.component.ts create mode 100644 ui-ngx/src/app/modules/home/pages/notification-center/rule-table/alarm-type-list.component.html create mode 100644 ui-ngx/src/app/modules/home/pages/notification-center/rule-table/alarm-type-list.component.ts diff --git a/ui-ngx/src/app/modules/home/pages/notification-center/notification-center.module.ts b/ui-ngx/src/app/modules/home/pages/notification-center/notification-center.module.ts index c94ed3e73b..bbd8d2f939 100644 --- a/ui-ngx/src/app/modules/home/pages/notification-center/notification-center.module.ts +++ b/ui-ngx/src/app/modules/home/pages/notification-center/notification-center.module.ts @@ -48,6 +48,8 @@ import { } from '@home/pages/notification-center/rule-table/rule-notification-dialog.component'; import { EscalationsComponent } from '@home/pages/notification-center/rule-table/escalations.component'; import { EscalationFormComponent } from '@home/pages/notification-center/rule-table/escalation-form.component'; +import { AlarmTypeListComponent } from '@home/pages/notification-center/rule-table/alarm-type-list.component'; +import { AlarmSeveritiesListComponent } from '@home/pages/notification-center/rule-table/alarm-severities-list.component'; @NgModule({ declarations: [ @@ -63,7 +65,9 @@ import { EscalationFormComponent } from '@home/pages/notification-center/rule-ta RuleTableHeaderComponent, RuleNotificationDialogComponent, EscalationsComponent, - EscalationFormComponent + EscalationFormComponent, + AlarmTypeListComponent, + AlarmSeveritiesListComponent ], imports: [ CommonModule, diff --git a/ui-ngx/src/app/modules/home/pages/notification-center/rule-table/alarm-severities-list.component.html b/ui-ngx/src/app/modules/home/pages/notification-center/rule-table/alarm-severities-list.component.html new file mode 100644 index 0000000000..4c134bc84e --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/notification-center/rule-table/alarm-severities-list.component.html @@ -0,0 +1,64 @@ + +
+ + alarm.alarm-severity-list + + + {{ alarmSeverityTranslationMap.get(severity) | translate }} + close + + + + + + + + +
+
+ notification.no-severity-found +
+ + + {{ 'notification.no-severity-matching' | translate :{severity: truncate.transform(searchText, true, 6, '...')} }} + + +
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/pages/notification-center/rule-table/alarm-severities-list.component.ts b/ui-ngx/src/app/modules/home/pages/notification-center/rule-table/alarm-severities-list.component.ts new file mode 100644 index 0000000000..730c4cd6d4 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/notification-center/rule-table/alarm-severities-list.component.ts @@ -0,0 +1,214 @@ +/// +/// Copyright © 2016-2022 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, ElementRef, forwardRef, Input, ViewChild } from '@angular/core'; +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { MatChipInputEvent } from '@angular/material/chips'; +import { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes'; +import { AlarmSeverity, alarmSeverityTranslations } from '@shared/models/alarm.models'; +import { TranslateService } from '@ngx-translate/core'; +import { TruncatePipe } from '@shared/pipe/truncate.pipe'; +import { Observable } from 'rxjs'; +import { map, share } from 'rxjs/operators'; +import { MatAutocompleteSelectedEvent, MatAutocompleteTrigger } from '@angular/material/autocomplete'; +import { FloatLabelType } from '@angular/material/form-field/form-field'; + +@Component({ + selector: 'tb-alarm-severities-list', + templateUrl: './alarm-severities-list.component.html', + styleUrls: [], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => AlarmSeveritiesListComponent), + multi: true + } + ] +}) +export class AlarmSeveritiesListComponent implements ControlValueAccessor{ + + @ViewChild('severityInput', {static: true}) severityInput: ElementRef; + @ViewChild('severityInput', {read: MatAutocompleteTrigger, static: true}) autocompleteTrigger: MatAutocompleteTrigger; + + alarmSeveritiesForm: FormGroup; + + alarmSeverityTranslationMap = alarmSeverityTranslations; + private alarmSeverities = Object.keys(AlarmSeverity); + alarmSeverity = AlarmSeverity; + + readonly separatorKeysCodes: number[] = [ENTER, COMMA, SEMICOLON]; + + private modelValue: Array | null; + + private requiredValue: boolean; + get required(): boolean { + return this.requiredValue; + } + @Input() + set required(value: boolean) { + const newVal = coerceBooleanProperty(value); + if (this.requiredValue !== newVal) { + this.requiredValue = newVal; + this.updateValidators(); + } + } + + @Input() + disabled: boolean; + + @Input() + flotLabel: FloatLabelType = 'auto'; + + searchText = ''; + filteredDisplaySeverities: Observable>; + + private dirty = false; + + private propagateChange = (v: any) => { }; + + constructor(private fb: FormBuilder, + private translate: TranslateService, + public truncate: TruncatePipe) { + this.alarmSeveritiesForm = this.fb.group({ + alarmSeverities: [null, this.required ? [Validators.required] : []], + alarmSeverity: [''] + }); + + this.filteredDisplaySeverities = this.alarmSeveritiesForm.get('alarmSeverity').valueChanges + .pipe( + map((value) => value ? value : ''), + map(name => this.fetchSeverities(name) ), + share() + ); + } + + updateValidators() { + this.alarmSeveritiesForm.get('alarmSeverities').setValidators(this.required ? [Validators.required] : []); + this.alarmSeveritiesForm.get('alarmSeverities').updateValueAndValidity(); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.alarmSeveritiesForm.disable({emitEvent: false}); + } else { + this.alarmSeveritiesForm.enable({emitEvent: false}); + } + } + + writeValue(value: Array | null): void { + this.searchText = ''; + if (value != null && value.length > 0) { + this.modelValue = [...value]; + this.alarmSeveritiesForm.get('alarmSeverities').setValue(value); + } else { + this.alarmSeveritiesForm.get('alarmSeverities').setValue(null); + this.modelValue = null; + } + this.dirty = true; + } + + addSeverityFromAutocomplete(event: MatAutocompleteSelectedEvent) { + this.addSeverity(event.option.value); + } + + addSeverityFromChipInput(event: MatChipInputEvent): void { + const alarmSeverity = event.value || ''; + const result = this.fetchSeverities(alarmSeverity.trim()); + if (result.length === 1) { + this.addSeverity(result[0]); + } + } + + private addSeverity(alarmSeverity: string) { + if (alarmSeverity) { + if (!this.modelValue || this.modelValue.indexOf(alarmSeverity) === -1) { + if (!this.modelValue) { + this.modelValue = []; + } + this.modelValue.push(alarmSeverity); + this.alarmSeveritiesForm.get('alarmSeverities').setValue(this.modelValue); + } + this.propagateChange(this.modelValue); + if (!this.fetchSeverities().length) { + this.clear('', true); + this.autocompleteTrigger.closePanel(); + } else { + this.clear(); + } + } + } + + onSeverityRemoved(alarmSeverity: string) { + const index = this.modelValue.indexOf(alarmSeverity); + if (index >= 0) { + this.modelValue.splice(index, 1); + if (!this.modelValue.length) { + this.modelValue = null; + } + this.alarmSeveritiesForm.get('alarmSeverities').setValue(this.modelValue); + this.propagateChange(this.modelValue); + this.clear(this.severityInput.nativeElement.value, true); + this.autocompleteTrigger.closePanel(); + } + } + + displaySeverityFn(severity?: string): string | undefined { + return severity ? this.translate.instant(alarmSeverityTranslations.get(AlarmSeverity[severity])) : undefined; + } + + onFocus() { + if (this.dirty) { + this.alarmSeveritiesForm.get('alarmSeverity').updateValueAndValidity({onlySelf: true, emitEvent: true}); + this.dirty = false; + } + } + + textIsNotEmpty(text: string): boolean { + return (text && text.length > 0); + } + + private fetchSeverities(searchText?: string): Array { + this.searchText = searchText; + const allowAlarmSeverity = this.alarmSeverities.filter(severity => !this.modelValue?.includes(severity)); + if (this.textIsNotEmpty(this.searchText)) { + const search = this.searchText.toUpperCase(); + return allowAlarmSeverity.filter(severity => severity.toUpperCase().includes(search)); + } else { + return allowAlarmSeverity; + } + } + + private clear(value: string = '', ignoreFocus = false) { + this.severityInput.nativeElement.value = value; + this.alarmSeveritiesForm.get('alarmSeverity').patchValue(value); + if (!ignoreFocus) { + setTimeout(() => { + this.severityInput.nativeElement.blur(); + this.severityInput.nativeElement.focus(); + }, 0); + } + } + +} diff --git a/ui-ngx/src/app/modules/home/pages/notification-center/rule-table/alarm-type-list.component.html b/ui-ngx/src/app/modules/home/pages/notification-center/rule-table/alarm-type-list.component.html new file mode 100644 index 0000000000..22d9a4278a --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/notification-center/rule-table/alarm-type-list.component.html @@ -0,0 +1,36 @@ + +
+ + alarm.alarm-type-list + + + {{type}} + close + + + + +
diff --git a/ui-ngx/src/app/modules/home/pages/notification-center/rule-table/alarm-type-list.component.ts b/ui-ngx/src/app/modules/home/pages/notification-center/rule-table/alarm-type-list.component.ts new file mode 100644 index 0000000000..7e3e5056f3 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/notification-center/rule-table/alarm-type-list.component.ts @@ -0,0 +1,132 @@ +/// +/// Copyright © 2016-2022 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, Input } from '@angular/core'; +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { MatChipInputEvent } from '@angular/material/chips'; +import { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes'; + +@Component({ + selector: 'tb-alarm-type-list', + templateUrl: './alarm-type-list.component.html', + styleUrls: [], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => AlarmTypeListComponent), + multi: true + } + ] +}) +export class AlarmTypeListComponent implements ControlValueAccessor{ + + alarmTypeForm: FormGroup; + private modelValue: Array | null; + + readonly separatorKeysCodes: number[] = [ENTER, COMMA, SEMICOLON]; + + private requiredValue: boolean; + get required(): boolean { + return this.requiredValue; + } + @Input() + set required(value: boolean) { + const newVal = coerceBooleanProperty(value); + if (this.requiredValue !== newVal) { + this.requiredValue = newVal; + this.updateValidators(); + } + } + + @Input() + disabled: boolean; + + private propagateChange = (v: any) => { }; + + constructor(private fb: FormBuilder) { + this.alarmTypeForm = this.fb.group({ + alarmTypes: [null, this.required ? [Validators.required] : []] + }); + } + + updateValidators() { + this.alarmTypeForm.get('alarmTypes').setValidators(this.required ? [Validators.required] : []); + this.alarmTypeForm.get('alarmTypes').updateValueAndValidity(); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.alarmTypeForm.disable({emitEvent: false}); + } else { + this.alarmTypeForm.enable({emitEvent: false}); + } + } + + writeValue(value: Array | null): void { + if (value != null && value.length > 0) { + this.modelValue = [...value]; + this.alarmTypeForm.get('alarmTypes').setValue(value); + } else { + this.alarmTypeForm.get('alarmTypes').setValue(null); + this.modelValue = null; + } + } + + addAlarmType(event: MatChipInputEvent): void { + let alarmType = event.value || ''; + const input = event.chipInput.inputElement; + alarmType = alarmType.trim(); + if (alarmType) { + if (!this.modelValue || this.modelValue.indexOf(alarmType) === -1) { + if (!this.modelValue) { + this.modelValue = []; + } + this.modelValue.push(alarmType); + this.alarmTypeForm.get('alarmTypes').setValue(this.modelValue); + } + this.propagateChange(this.modelValue); + if (input) { + input.value = ''; + } + } + } + + removeAlarmType(alarmType: string) { + const index = this.modelValue.indexOf(alarmType); + if (index >= 0) { + this.modelValue.splice(index, 1); + if (!this.modelValue.length) { + this.modelValue = null; + } + this.alarmTypeForm.get('alarmTypes').setValue(this.modelValue); + this.propagateChange(this.modelValue); + } + } + + get alarmTypeList(): string[] { + return this.alarmTypeForm.get('alarmTypes').value; + } + +} diff --git a/ui-ngx/src/app/modules/home/pages/notification-center/rule-table/escalations.component.ts b/ui-ngx/src/app/modules/home/pages/notification-center/rule-table/escalations.component.ts index 578355e8d3..86e289323b 100644 --- a/ui-ngx/src/app/modules/home/pages/notification-center/rule-table/escalations.component.ts +++ b/ui-ngx/src/app/modules/home/pages/notification-center/rule-table/escalations.component.ts @@ -118,13 +118,17 @@ export class EscalationsComponent implements ControlValueAccessor, Validator, On } } - writeValue(escalations: Array | null): void { - if (escalations?.length === this.escalationsFormArray.length) { - this.escalationsFormArray.patchValue(escalations, {emitEvent: false}); + writeValue(escalations: {[key: string]: Array} | null): void { + const escalationParse: Array = []; + for (const escalation in escalations) { + escalationParse.push({delayInSec: Number(escalation), targets: escalations[escalation]}); + } + if (escalationParse?.length === this.escalationsFormArray.length) { + this.escalationsFormArray.patchValue(escalationParse, {emitEvent: false}); } else { const escalationsControls: Array = []; - if (escalations) { - escalations.forEach((escalation, index) => { + if (escalationParse) { + escalationParse.forEach((escalation, index) => { escalationsControls.push(this.fb.control(escalation, [Validators.required])); }); } else { @@ -166,7 +170,10 @@ export class EscalationsComponent implements ControlValueAccessor, Validator, On } private updateModel() { - const escalations = this.escalationsFormGroup.get('escalations').value; + const escalations = {}; + this.escalationsFormGroup.get('escalations').value.forEach( + escalation => escalations[escalation.delayInSec] = escalation.targets + ); this.propagateChange(escalations); } } diff --git a/ui-ngx/src/app/modules/home/pages/notification-center/rule-table/rule-notification-dialog.component.html b/ui-ngx/src/app/modules/home/pages/notification-center/rule-table/rule-notification-dialog.component.html index 055cf71ffc..d37f976cf9 100644 --- a/ui-ngx/src/app/modules/home/pages/notification-center/rule-table/rule-notification-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/notification-center/rule-table/rule-notification-dialog.component.html @@ -67,94 +67,46 @@ *ngIf="ruleNotificationForm.get('triggerType').value === triggerType.ALARM"> {{ 'notification.alarm-trigger-settings' | translate }}
-
- notification.filter - - alarm.alarm-type-list - - - {{type}} - cancel - - - - +
+
+ notification.filter + + - - alarm.alarm-severity-list - - - {{ alarmSeverityTranslationMap.get(severity) | translate }} - cancel - - - - - - - - -
-
- notification.no-severity-found -
- - - {{ translate.get('notification.no-severity-matching', - {severity: truncate.transform(severitySearchText, true, 6, '...')}) | async }} - - -
-
-
-
-
+ + +
-
- notification.clear-rule - - alarm.alarm-status-list - - - {{ alarmSearchStatusTranslationMap.get(searchStatus) | translate }} - - - -
+
+ notification.clear-rule + + alarm.alarm-status-list + + {{ 'alarm.any-status' | translate }} + + {{ alarmSearchStatusTranslationMap.get(searchStatus) | translate }} + + + +
+ -
+
notification.hierarchy-of-receiving
- - - notification.description - - + +
+
+ + notification.description + + +
@@ -162,42 +114,51 @@ [stepControl]="deviceInactivityTemplateForm"> {{ 'notification.device-inactivity-trigger-settings' | translate }}
- notification.filter-by -
- - {{ 'notification.device' | translate }} - {{ 'notification.device-profile' | translate }} - -
- - - +
+ notification.filter-by +
+ + {{ 'notification.device' | translate }} + {{ 'notification.device-profile' | translate }} + +
+ + + +
notification.device-list-rule-hint
+
+ + + +
notification.device-profiles-list-rule-hint
+
+
+
+ formControlName="targets" + [entityType]="entityType.NOTIFICATION_TARGET" + placeholderText="{{ 'notification.target' | translate }}"> - - - - - notification.description - - +
+ +
+
+ + notification.description + + +
@@ -205,7 +166,7 @@ [stepControl]="entityActionTemplateForm"> {{ 'notification.entity-action-trigger-settings' | translate }}
-
+
notification.filter {{ 'notification.deleted' | translate }}
- - - - notification.description - - +
+ + +
+ +
+
+ + notification.description + + +
diff --git a/ui-ngx/src/app/modules/home/pages/notification-center/rule-table/rule-notification-dialog.component.scss b/ui-ngx/src/app/modules/home/pages/notification-center/rule-table/rule-notification-dialog.component.scss index 73c41c0683..36cac344e8 100644 --- a/ui-ngx/src/app/modules/home/pages/notification-center/rule-table/rule-notification-dialog.component.scss +++ b/ui-ngx/src/app/modules/home/pages/notification-center/rule-table/rule-notification-dialog.component.scss @@ -24,6 +24,24 @@ } } + .tb-hint { + margin-top: -1.25em; + padding: 0; + } + + .fields-group { + padding: 0 16px 8px; + margin-bottom: 10px; + border: 1px groove rgba(0, 0, 0, .25); + border-radius: 4px; + + legend { + margin-bottom: 8px; + color: rgba(0, 0, 0, .7); + width: fit-content; + } + } + ::ng-deep { .mat-dialog-content { @@ -119,17 +137,4 @@ } } } - - .fields-group { - padding: 0 16px 8px; - margin-bottom: 10px; - border: 1px groove rgba(0, 0, 0, .25); - border-radius: 4px; - - legend { - margin-bottom: 8px; - color: rgba(0, 0, 0, .7); - width: fit-content; - } - } } diff --git a/ui-ngx/src/app/modules/home/pages/notification-center/rule-table/rule-notification-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/notification-center/rule-table/rule-notification-dialog.component.ts index e8de97ab6c..d1634e0d4e 100644 --- a/ui-ngx/src/app/modules/home/pages/notification-center/rule-table/rule-notification-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/pages/notification-center/rule-table/rule-notification-dialog.component.ts @@ -15,7 +15,7 @@ /// import { NotificationRule, TriggerType, TriggerTypeTranslationMap } from '@shared/models/notification.models'; -import { Component, ElementRef, Inject, OnDestroy, ViewChild } from '@angular/core'; +import { Component, Inject, OnDestroy, ViewChild } from '@angular/core'; import { DialogComponent } from '@shared/components/dialog.component'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @@ -25,23 +25,14 @@ import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { NotificationService } from '@core/http/notification.service'; import { EntityType } from '@shared/models/entity-type.models'; import { deepClone, deepTrim, isDefined } from '@core/utils'; -import { Observable, of, Subject } from 'rxjs'; -import { map, mergeMap, share, startWith, takeUntil } from 'rxjs/operators'; +import { Observable, Subject } from 'rxjs'; +import { map, takeUntil } from 'rxjs/operators'; import { StepperOrientation, StepperSelectionEvent } from '@angular/cdk/stepper'; import { MatStepper } from '@angular/material/stepper'; import { MediaBreakpoints } from '@shared/models/constants'; import { BreakpointObserver } from '@angular/cdk/layout'; -import { MatChipInputEvent, MatChipList } from '@angular/material/chips'; -import { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes'; -import { - AlarmSeverity, - alarmSeverityTranslations, - AlarmStatus, - alarmStatusTranslations -} from '@shared/models/alarm.models'; -import { MatAutocomplete, MatAutocompleteSelectedEvent } from '@angular/material/autocomplete'; +import { AlarmStatus, alarmStatusTranslations } from '@shared/models/alarm.models'; import { TranslateService } from '@ngx-translate/core'; -import { TruncatePipe } from '@shared/pipe/truncate.pipe'; export interface RuleNotificationDialogData { rule?: NotificationRule; @@ -59,12 +50,6 @@ export class RuleNotificationDialogComponent extends @ViewChild('addNotificationRule', {static: true}) addNotificationRule: MatStepper; - @ViewChild('severitiesChipList') severitiesChipList: MatChipList; - @ViewChild('severityAutocomplete') severityAutocomplete: MatAutocomplete; - @ViewChild('severityInput') severityInput: ElementRef; - - readonly separatorKeysCodes: number[] = [ENTER, COMMA, SEMICOLON]; - stepperOrientation: Observable; ruleNotificationForm: FormGroup; @@ -76,10 +61,6 @@ export class RuleNotificationDialogComponent extends triggerTypes: TriggerType[] = Object.values(TriggerType); triggerTypeTranslationMap = TriggerTypeTranslationMap; - alarmSeverities = Object.keys(AlarmSeverity); - alarmSeverityEnum = AlarmSeverity; - alarmSeverityTranslationMap = alarmSeverityTranslations; - alarmSearchStatuses: AlarmStatus[] = Object.values(AlarmStatus); alarmSearchStatusTranslationMap = alarmStatusTranslations; @@ -89,17 +70,14 @@ export class RuleNotificationDialogComponent extends selectedIndex = 0; - filteredDisplaySeverities: Observable>; - severitySearchText = ''; - dialogTitle = 'notification.edit-rule'; - severityInputChange = new Subject(); - private destroy$ = new Subject(); private readonly ruleNotification: NotificationRule; + private triggerTypeFormsMap: Map; + constructor(protected store: Store, protected router: Router, protected dialogRef: MatDialogRef, @@ -107,7 +85,6 @@ export class RuleNotificationDialogComponent extends private breakpointObserver: BreakpointObserver, private fb: FormBuilder, public translate: TranslateService, - public truncate: TruncatePipe, private notificationService: NotificationService) { super(store, router, dialogRef); @@ -122,72 +99,66 @@ export class RuleNotificationDialogComponent extends name: [null, Validators.required], templateId: [null, Validators.required], triggerType: [TriggerType.ALARM, Validators.required], - recipientsConfig: this.fb.group({ - triggerType: [], - }), - triggerConfig: this.fb.group({ - triggerType: [] - }), + recipientsConfig: [null], + triggerConfig: [null], additionalConfig: this.fb.group({ - description: [] + description: [''] }) }); - this.ruleNotificationForm.get('triggerType').valueChanges.pipe( - takeUntil(this.destroy$) - ).subscribe( - value => { - this.ruleNotificationForm.get('triggerConfig').patchValue({triggerType: value}, {emitEvent: false}); - this.ruleNotificationForm.get('recipientsConfig').patchValue({triggerType: value}, {emitEvent: false}); - } - ); - this.alarmTemplateForm = this.fb.group({ - alarmTypes: [null], - alarmSeverities: [null, Validators.required], - clearRule: this.fb.group({ - alarmStatus: [] + triggerConfig: this.fb.group({ + alarmTypes: [null], + alarmSeverities: [null, Validators.required], + clearRule: this.fb.group({ + alarmStatus: [null] + }) }), - escalationTable: [], - description: [''] + recipientsConfig: this.fb.group({ + escalationTable: [] + }) }); this.deviceInactivityTemplateForm = this.fb.group({ - filterByDevice: [true], - devices: [null, Validators.required], - deviceProfiles: [{value: null, disabled: true}, Validators.required], - targets: [[], Validators.required], - description: [''] + triggerConfig: this.fb.group({ + filterByDevice: [true], + devices: [null], + deviceProfiles: [{value: null, disabled: true}] + }), + recipientsConfig: this.fb.group({ + targets: [[], Validators.required] + }) }); - this.deviceInactivityTemplateForm.get('filterByDevice').valueChanges.pipe( + this.deviceInactivityTemplateForm.get('triggerConfig.filterByDevice').valueChanges.pipe( takeUntil(this.destroy$) ).subscribe(value => { if (value) { - this.deviceInactivityTemplateForm.get('devices').enable({emitEvent: false}); - this.deviceInactivityTemplateForm.get('deviceProfiles').disable({emitEvent: false}); + this.deviceInactivityTemplateForm.get('triggerConfig.devices').enable({emitEvent: false}); + this.deviceInactivityTemplateForm.get('triggerConfig.deviceProfiles').disable({emitEvent: false}); } else { - this.deviceInactivityTemplateForm.get('deviceProfiles').enable({emitEvent: false}); - this.deviceInactivityTemplateForm.get('devices').disable({emitEvent: false}); + this.deviceInactivityTemplateForm.get('triggerConfig.deviceProfiles').enable({emitEvent: false}); + this.deviceInactivityTemplateForm.get('triggerConfig.devices').disable({emitEvent: false}); } }); this.entityActionTemplateForm = this.fb.group({ - entityType: [EntityType.DEVICE], - created: [false], - updated: [false], - deleted: [false], - targets: [[], Validators.required], - description: [''] + triggerConfig: this.fb.group({ + entityType: [EntityType.DEVICE], + created: [false], + updated: [false], + deleted: [false] + }), + recipientsConfig: this.fb.group({ + targets: [[], Validators.required] + }) }); - this.filteredDisplaySeverities = this.severityInputChange - .pipe( - startWith(''), - map((value) => value ? value : ''), - mergeMap(name => this.fetchSeverities(name) ), - share() - ); + this.triggerTypeFormsMap = new Map([ + [TriggerType.ALARM, this.alarmTemplateForm], + [TriggerType.DEVICE_INACTIVITY, this.deviceInactivityTemplateForm], + [TriggerType.ENTITY_ACTION, this.entityActionTemplateForm] + ]); if (data.isAdd || data.isCopy) { this.dialogTitle = 'notification.add-rule'; @@ -200,145 +171,15 @@ export class RuleNotificationDialogComponent extends } this.ruleNotificationForm.reset({}, {emitEvent: false}); this.ruleNotificationForm.patchValue(this.ruleNotification, {emitEvent: false}); - if (this.ruleNotification.triggerType === TriggerType.ALARM) { - const parsedEscalationTable = []; - const escalationTable = this.ruleNotification.recipientsConfig.escalationTable; - for (const escalation in escalationTable) { - parsedEscalationTable.push({delayInSec: escalation, targets: escalationTable[escalation]}); - } - this.alarmTemplateForm.patchValue({ - escalationTable: parsedEscalationTable, - description: this.ruleNotification.additionalConfig.description, - ...this.ruleNotification.triggerConfig - }, {emitEvent: false}); - } else if (this.ruleNotification.triggerType === TriggerType.DEVICE_INACTIVITY) { - this.deviceInactivityTemplateForm.patchValue({ - filterByDevice: !!this.ruleNotification.triggerConfig.devices, - deviceProfiles: this.ruleNotification.triggerConfig.deviceProfiles, - devices: this.ruleNotification.triggerConfig.devices, - targets: this.ruleNotification.recipientsConfig.targets, - description: this.ruleNotification.additionalConfig.description - }, {emitEvent: false}); - this.deviceInactivityTemplateForm.get('filterByDevice').updateValueAndValidity({onlySelf: true}); - } else { - this.entityActionTemplateForm.patchValue({ - targets: this.ruleNotification.recipientsConfig.targets, - description: this.ruleNotification.additionalConfig.description, - ...this.ruleNotification.triggerConfig - }, {emitEvent: false}); - } - } - } - - onSeverityRemoved(severity: string): void { - const severities: string[] = this.alarmTemplateForm.get('alarmSeverities').value; - const index = severities.indexOf(severity); - if (index > -1) { - severities.splice(index, 1); - this.alarmTemplateForm.get('alarmSeverities').setValue(severities); - this.alarmTemplateForm.get('alarmSeverities').markAsDirty(); - this.severitiesChipList.errorState = !severities.length; - } - } - - onSeverityInputFocus() { - this.severityInputChange.next(this.severityInput.nativeElement.value); - } - - addSeverityFromChipInput(event: MatChipInputEvent): void { - const value = event.value; - if ((value || '').trim()) { - const severityName = value.trim().toUpperCase(); - const existingSeverity = this.alarmSeverities.find(severity => severity.toUpperCase() === severityName); - if (this.addSeverity(existingSeverity)) { - this.clearSeverityInput(''); + const currentForm = this.triggerTypeFormsMap.get(this.ruleNotification.triggerType); + currentForm.patchValue(this.ruleNotification, {emitEvent: false}); + if (this.ruleNotification.triggerType === TriggerType.DEVICE_INACTIVITY) { + this.deviceInactivityTemplateForm.get('triggerConfig.filterByDevice') + .patchValue(!!this.ruleNotification.triggerConfig.devices, {onlySelf: true}); } } } - private addSeverity(existingSeverity: string): boolean { - if (existingSeverity) { - const displaySeverities: string[] = this.alarmTemplateForm.get('alarmSeverities').value; - const index = displaySeverities.indexOf(existingSeverity); - if (index === -1) { - displaySeverities.push(existingSeverity); - this.alarmTemplateForm.get('alarmSeverities').setValue(displaySeverities); - this.alarmTemplateForm.get('alarmSeverities').markAsDirty(); - this.severitiesChipList.errorState = false; - return true; - } - } - return false; - } - - clearSeverityInput(value: string = '') { - this.severityInput.nativeElement.value = value; - this.severityInputChange.next(null); - setTimeout(() => { - this.severityInput.nativeElement.blur(); - this.severityInput.nativeElement.focus(); - }, 0); - } - - severitySelected(event: MatAutocompleteSelectedEvent): void { - this.addSeverity(event.option.value); - this.clearSeverityInput(''); - } - - displaySeverityFn(severity?: string): string | undefined { - return severity ? this.translate.instant(this.alarmSeverityTranslationMap.get(this.alarmSeverityEnum[severity])) : undefined; - } - - private fetchSeverities(searchText?: string): Observable> { - this.severitySearchText = searchText; - if (this.severitySearchText && this.severitySearchText.length) { - const search = this.severitySearchText.toUpperCase(); - return of(this.alarmSeverities.filter(severity => severity.toUpperCase().includes(search))); - } else { - return of(this.alarmSeverities); - } - } - - textIsNotEmpty(text: string): boolean { - return (text && text.length > 0); - } - - public alarmTypeList(): string[] { - return this.alarmTemplateForm.get('alarmTypes').value; - } - - public removeAlarmType(type: string): void { - const types: string[] = this.alarmTemplateForm.get('alarmTypes').value; - const index = types.indexOf(type); - if (index >= 0) { - types.splice(index, 1); - this.alarmTemplateForm.get('alarmTypes').setValue(types); - this.alarmTemplateForm.get('alarmTypes').markAsDirty(); - } - } - - public addAlarmType(event: MatChipInputEvent): void { - const input = event.chipInput.inputElement; - let value = event.value || ''; - - if (value.trim()) { - value = value.trim(); - let types: string[] = this.alarmTemplateForm.get('alarmTypes').value; - if (!types || types.indexOf(value) === -1) { - if (!types) { - types = []; - } - types.push(value); - this.alarmTemplateForm.get('alarmTypes').setValue(types); - this.alarmTemplateForm.get('alarmTypes').markAsDirty(); - } - } - - if (input) { - input.value = ''; - } - } - changeStep($event: StepperSelectionEvent) { this.selectedIndex = $event.selectedIndex; } @@ -356,16 +197,7 @@ export class RuleNotificationDialogComponent extends } nextStepLabel(): string { - if (this.selectedIndex === 1 && this.selectedIndex < this.maxStepperIndex && this.alarmTemplateForm.pristine) { - return 'action.skip'; - } - if (this.selectedIndex === 2 && this.selectedIndex < this.maxStepperIndex && this.deviceInactivityTemplateForm.pristine) { - return 'action.skip'; - } - if (this.selectedIndex === 3 && this.selectedIndex < this.maxStepperIndex && this.entityActionTemplateForm.pristine) { - return 'action.skip'; - } - if (this.selectedIndex !== 0 && this.selectedIndex >= this.maxStepperIndex) { + if (this.selectedIndex !== 0) { return (this.data.isAdd || this.data.isCopy) ? 'action.add' : 'action.save'; } return 'action.next'; @@ -378,30 +210,15 @@ export class RuleNotificationDialogComponent extends private add(): void { if (this.allValid()) { let formValue = this.ruleNotificationForm.value; - const triggerType = this.ruleNotificationForm.get('triggerType').value; - if (triggerType === TriggerType.ALARM) { - Object.assign(formValue.triggerConfig, this.alarmTemplateForm.value); - const parsedEscalationTable = {}; - this.alarmTemplateForm.get('escalationTable').value.forEach( - escalation => parsedEscalationTable[escalation.delayInSec] = escalation.targets - ); - formValue.recipientsConfig.escalationTable = parsedEscalationTable; - formValue.additionalConfig.description = this.alarmTemplateForm.get('description').value; - delete formValue.triggerConfig.escalationTable; - } else if (triggerType === TriggerType.DEVICE_INACTIVITY) { - Object.assign(formValue.triggerConfig, this.deviceInactivityTemplateForm.value); - formValue.recipientsConfig.targets = this.deviceInactivityTemplateForm.get('targets').value; - formValue.additionalConfig.description = this.deviceInactivityTemplateForm.get('description').value; + const triggerType: TriggerType = this.ruleNotificationForm.get('triggerType').value; + const currentForm = this.triggerTypeFormsMap.get(triggerType); + Object.assign(formValue, currentForm.value); + if (triggerType === TriggerType.DEVICE_INACTIVITY) { delete formValue.triggerConfig.filterByDevice; - } else { - Object.assign(formValue.triggerConfig, this.entityActionTemplateForm.value); - formValue.recipientsConfig.targets = this.entityActionTemplateForm.get('targets').value; - formValue.additionalConfig.description = this.entityActionTemplateForm.get('description').value; - } - if (triggerType === TriggerType.DEVICE_INACTIVITY || triggerType === TriggerType.ENTITY_ACTION) { - delete formValue.triggerConfig.trigger; } - if (this.ruleNotification) { + formValue.recipientsConfig.triggerType = triggerType; + formValue.triggerConfig.triggerType = triggerType; + if (this.ruleNotification && !this.data.isCopy) { formValue = {...this.ruleNotification, ...formValue}; } this.notificationService.saveNotificationRule(deepTrim(formValue)).subscribe( diff --git a/ui-ngx/src/app/modules/home/pages/notification-center/template-table/template-autocomplete.component.ts b/ui-ngx/src/app/modules/home/pages/notification-center/template-table/template-autocomplete.component.ts index fbbeaac453..20601b98f3 100644 --- a/ui-ngx/src/app/modules/home/pages/notification-center/template-table/template-autocomplete.component.ts +++ b/ui-ngx/src/app/modules/home/pages/notification-center/template-table/template-autocomplete.component.ts @@ -215,5 +215,7 @@ export class TemplateAutocompleteComponent implements ControlValueAccessor, OnIn private reset() { this.selectTemplateFormGroup.get('templateName').patchValue('', {emitEvent: false}); + this.updateView(null); + this.dirty = true; } } diff --git a/ui-ngx/src/app/modules/home/pages/notification-center/template-table/template-notification-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/notification-center/template-table/template-notification-dialog.component.ts index 55b29ff9f7..f09d8d67ca 100644 --- a/ui-ngx/src/app/modules/home/pages/notification-center/template-table/template-notification-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/pages/notification-center/template-table/template-notification-dialog.component.ts @@ -243,7 +243,7 @@ export class TemplateNotificationDialogComponent delete template.configuration.deliveryMethodsTemplates[method]; } }); - if (this.templateNotification) { + if (this.templateNotification && !this.data.isCopy) { template = {...this.templateNotification, ...template}; } this.notificationService.saveNotificationTemplate(deepTrim(template)).subscribe( diff --git a/ui-ngx/src/app/shared/models/notification.models.ts b/ui-ngx/src/app/shared/models/notification.models.ts index 1a06ecdca1..ec0609f15a 100644 --- a/ui-ngx/src/app/shared/models/notification.models.ts +++ b/ui-ngx/src/app/shared/models/notification.models.ts @@ -118,12 +118,12 @@ export interface NotificationRuleTriggerConfig { export interface NotificationRuleRecipientConfig { targets?: Array; - escalationTable?: {[key: string]: Array}; + escalationTable?: {[key: number]: Array}; } export interface NonConfirmedNotificationEscalation { delayInSec: number; - notificationTargetId: NotificationTargetId; + targets: Array; } export interface NotificationTarget extends Omit, 'label'>{ 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 fbbb490553..d93531e9d7 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -2687,29 +2687,34 @@ "action-button": "Action button", "add-notification-target": "Add notification recipient", "add-notification-template": "Add notification template", + "add-rule": "Add rule", "add-target": "Add recipient", "add-template": "Add template", - "at-least-one-should-be-selected": "At least one should be selected", + "alarm-trigger-settings": "Alarm trigger settings", "all": "All", + "at-least-one-should-be-selected": "At least one should be selected", "basic-settings": "Basic settings", "button-text": "Button text", "button-text-required": "Button text is required", + "clear-rule": "Clear rule", "compose": "Compose", "conversation": "Conversation", "conversation-required": "Conversation is required", - "add-rule": "Add rule", - "edit-rule": "Edit rule", - "copy-rule": "Copy rule", "copy-notification-template": "Copy notification template", + "copy-rule": "Copy rule", "copy-template": "Copy template", "create-target": "Create recipient", + "created": "Created", "created-time": "Created time", "delete-request-text": "Be careful, after the confirmation the notification request will become unrecoverable.", "delete-request-title": "Are you sure you want to delete the notification request?", + "delete-rule-text": "Be careful, after the confirmation the notification rule will become unrecoverable.", + "delete-rule-title": "Are you sure you want to delete the notification rule '{{ruleName}}'?", "delete-target-text": "Be careful, after the confirmation the notification recipient will become unrecoverable.", "delete-target-title": "Are you sure you want to delete the notification recipient '{{targetName}}'?", "delete-template-text": "Be careful, after the confirmation the notification template will become unrecoverable.", "delete-template-title": "Are you sure you want to delete the notification template '{{templateName}}'?", + "deleted": "Deleted", "delivery-method": "Delivery method", "delivery-method-type": { "email": "Email", @@ -2717,14 +2722,26 @@ "slack": "Slack", "sms": "SMS" }, - "delete-rule-text": "Be careful, after the confirmation the notification rule will become unrecoverable.", - "delete-rule-title": "Are you sure you want to delete the notification rule '{{ruleName}}'?", "delivery-methods": "Delivery methods", "description": "Description", + "device": "Device", + "device-inactivity-trigger-settings": "Device inactive trigger settings", + "device-list-rule-hint": "If the field is empty, the trigger will be applied to all devices", + "device-profile": "Device profile", + "device-profiles": "Device profiles", + "device-profiles-list-rule-hint": "If the field is empty, the trigger will be applied to all device profiles", + "devices": "Devices", "edit-notification-target": "Edit notification recipient", "edit-notification-template": "Edit notification template", + "edit-rule": "Edit rule", "edit-template": "Edit template", "email-settings": "Email settings", + "entity-action-trigger-settings": "Entity action trigger settings", + "entity-type": "Entity type", + "filter": "Filter", + "filter-by": "Filter by", + "first-recipient": "First recipient", + "hierarchy-of-receiving": "Hierarchy of receiving notifications", "inbox": "Inbox", "link": "Link", "link-required": "Link is required", @@ -2738,12 +2755,19 @@ "no-notification-request": "No notification request", "no-notification-templates": "Not found notification templates", "no-notifications-yet": "No notifications yet", + "no-rule": "No rule configured", + "no-rules-notification": "No rules notification", + "no-severity-found": "No severity found", + "no-severity-matching": "'{{severity}}' not found.", "no-targets-notification": "No recipients notification", "not-found-slack-recipient": "Not found slack recipient", "notification": "Notification", "notification-center": "Notification center", "notification-target": "Notification recipient", + "notify": "notify", "notify-again": "Notify again", + "platform-users": "Platform users", + "recipient": "Recipient", "recipient-type": "Recipient type", "recipients-count": "{ count, plural, 1 {1 Recipient} other {# Recipients} }", "request-status": { @@ -2752,15 +2776,15 @@ "sent": "Sent" }, "review": "Review", - "no-rules-notification": "No rules notification", "rule": "Rule", + "rule-name": "Rule name", + "rule-name-required": "Name is required", "rules": "Rules", - "platform-users": "Platform users", "scheduler-later": "Schedule for later", "search-notification": "Search notification", + "search-rules": "Search rules", "search-targets": "Search recipients", "search-templates": "Search templates", - "search-rules": "Search rules", "send-notification": "Send notification", "send-test-notification": "Send test notification", "sent": "Sent", @@ -2783,64 +2807,42 @@ "target-type": { "all-users": "All users", "customer-users": "Customer users", + "originator-entity-owner-users": "Users of originator entity owner", "user-filters": "User filter", - "user-list": "User list", - "originator-entity-owner-users": "Users of originator entity owner" + "user-list": "User list" }, "targets": "Recipients", "targets-required": "Recipients is required", - "recipient": "Recipient", "template": "Template", + "template-hint": { + "alarm": "Available params: ${alarmType}, ${alarmSeverity}, ${alarmStatus}, ${alarmOriginatorEntityType}, ${alarmOriginatorId}, ${alarmId}", + "device-inactivity": "Available params: ${deviceName}, ${deviceType}, ${deviceId}", + "entity-action": "Available params: ${actionType}, ${entityType}, ${entityName}, ${entityId}, ${originatorUserName}, ${originatorUserId}", + "general": "Available params: ${recipientEmail}, ${recipientFirstName}, ${recipientLastName}" + }, + "template-name": "Template name", "template-required": "Template is required", "template-type": { "alarm": "Alarm", - "general": "General", "device-inactivity": "Device inactivity", - "entity-action": "Entity action" - }, - "template-hint": { - "general": "Available params: ${recipientEmail}, ${recipientFirstName}, ${recipientLastName}", - "alarm": "Available params: ${alarmType}, ${alarmSeverity}, ${alarmStatus}, ${alarmOriginatorEntityType}, ${alarmOriginatorId}, ${alarmId}", - "device-inactivity": "Available params: ${deviceName}, ${deviceType}, ${deviceId}", - "entity-action": "Available params: ${actionType}, ${entityType}, ${entityName}, ${entityId}, ${originatorUserName}, ${originatorUserId}" + "entity-action": "Entity action", + "general": "General" }, "templates": "Templates", "text": "Text", "time": "Time", - "type": "Type", - "unread": "Unread", - "view-all": "View all", - "web-settings": "Web settings", - "rule-name": "Rule name", - "rule-name-required": "Name is required", - "template-name": "Template name", - "hierarchy-of-receiving": "Hierarchy of receiving notifications", "trigger": { - "trigger": "Trigger", - "trigger-required": "Trigger is required", "alarm": "Alarm", "device-inactivity": "Device inactivity", - "entity-action": "Entity action" + "entity-action": "Entity action", + "trigger": "Trigger", + "trigger-required": "Trigger is required" }, - "alarm-trigger-settings": "Alarm trigger settings", - "device-inactivity-trigger-settings": "Device inactive trigger settings", - "entity-action-trigger-settings": "Entity action trigger settings", - "filter": "Filter", - "clear-rule": "Clear rule", - "no-severity-matching": "'{{severity}}' not found.", - "no-severity-found": "No severity found", - "first-recipient": "First recipient", - "notify": "notify", - "no-rule": "No rule configured", - "device": "Device", - "devices": "Devices", - "device-profile": "Device profile", - "device-profiles": "Device profiles", - "filter-by": "Filter by", - "entity-type": "Entity type", - "created": "Created", + "type": "Type", + "unread": "Unread", "updated": "Updated", - "deleted": "Deleted" + "view-all": "View all", + "web-settings": "Web settings" }, "ota-update": { "add": "Add package",