diff --git a/ui-ngx/src/app/modules/home/pages/notification-center/inbox-table/inbox-table-header.component.html b/ui-ngx/src/app/modules/home/pages/notification-center/inbox-table/inbox-table-header.component.html index aa34aed8ff..f3dc87ea90 100644 --- a/ui-ngx/src/app/modules/home/pages/notification-center/inbox-table/inbox-table-header.component.html +++ b/ui-ngx/src/app/modules/home/pages/notification-center/inbox-table/inbox-table-header.component.html @@ -20,7 +20,7 @@ style="width: 180px;" [ngModel]="entitiesTableConfig.componentsData.unreadOnly" (ngModelChange)="changeUnreadOnly($event)"> - Unread + {{ 'notification.unread' | translate }} {{ 'notification.all' | translate }} 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 fd8d1a6723..7154bc617f 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 @@ -44,6 +44,8 @@ import { import { InboxTableHeaderComponent } from '@home/pages/notification-center/inbox-table/inbox-table-header.component'; import { RuleTableHeaderComponent } from '@home/pages/notification-center/rule-table/rule-table-header.component'; import { RuleNotificationDialogComponent } 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'; @NgModule({ declarations: [ @@ -57,7 +59,9 @@ import { RuleNotificationDialogComponent } from '@home/pages/notification-center TemplateAutocompleteComponent, InboxTableHeaderComponent, RuleTableHeaderComponent, - RuleNotificationDialogComponent + RuleNotificationDialogComponent, + EscalationsComponent, + EscalationFormComponent ], imports: [ CommonModule, diff --git a/ui-ngx/src/app/modules/home/pages/notification-center/rule-table/escalation-form.component.html b/ui-ngx/src/app/modules/home/pages/notification-center/rule-table/escalation-form.component.html new file mode 100644 index 0000000000..ab12a8d541 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/notification-center/rule-table/escalation-form.component.html @@ -0,0 +1,41 @@ + + +
+
+
+
notification.first-recipient
+
+ After + + notification.notify +
+
+
+ + +
+
+
diff --git a/ui-ngx/src/app/modules/home/pages/notification-center/rule-table/escalation-form.component.ts b/ui-ngx/src/app/modules/home/pages/notification-center/rule-table/escalation-form.component.ts new file mode 100644 index 0000000000..f68f6e2bc5 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/notification-center/rule-table/escalation-form.component.ts @@ -0,0 +1,139 @@ +/// +/// 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, OnDestroy, OnInit } from '@angular/core'; +import { + ControlValueAccessor, + FormBuilder, + FormControl, + FormGroup, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + Validator, Validators +} from '@angular/forms'; +import { UtilsService } from '@core/services/utils.service'; +import { isDefinedAndNotNull } from '@core/utils'; +import { Subscription } from 'rxjs'; +import { NonConfirmedNotificationEscalation } from '@shared/models/notification.models'; +import { EntityType } from '@shared/models/entity-type.models'; + +@Component({ + selector: 'tb-escalation-form', + templateUrl: './escalation-form.component.html', + styleUrls: [], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => EscalationFormComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => EscalationFormComponent), + multi: true, + } + ] +}) +export class EscalationFormComponent implements ControlValueAccessor, OnInit, OnDestroy, Validator { + + @Input() + disabled: boolean; + + @Input() + systemEscalation = false; + + escalationFormGroup: FormGroup; + + entityType = EntityType; + + private modelValue: NonConfirmedNotificationEscalation; + private propagateChange = null; + private propagateChangePending = false; + private valueChange$: Subscription = null; + + constructor(private utils: UtilsService, + private fb: FormBuilder) { + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + if (this.propagateChangePending) { + this.propagateChangePending = false; + setTimeout(() => { + this.propagateChange(this.modelValue); + }, 0); + } + } + + registerOnTouched(fn: any): void { + } + + ngOnInit() { + this.escalationFormGroup = this.fb.group( + { + delayInSec: [null], + notificationTargetId: [null, Validators.required], + }); + this.valueChange$ = this.escalationFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + } + + ngOnDestroy() { + if (this.valueChange$) { + this.valueChange$.unsubscribe(); + this.valueChange$ = null; + } + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.escalationFormGroup.disable({emitEvent: false}); + } else { + this.escalationFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: NonConfirmedNotificationEscalation): void { + this.propagateChangePending = false; + this.modelValue = value; + if (isDefinedAndNotNull(this.modelValue)) { + this.escalationFormGroup.patchValue(this.modelValue, {emitEvent: false}); + } + if (!this.disabled && !this.escalationFormGroup.valid) { + this.updateModel(); + } + } + + public validate(c: FormControl) { + return (this.escalationFormGroup.valid) ? null : { + escalation: { + valid: false, + }, + }; + } + + private updateModel() { + const value = this.escalationFormGroup.value; + this.modelValue = {...this.modelValue, ...value}; + if (this.propagateChange) { + this.propagateChange(this.modelValue); + } else { + this.propagateChangePending = true; + } + } +} diff --git a/ui-ngx/src/app/modules/home/pages/notification-center/rule-table/escalations.component.html b/ui-ngx/src/app/modules/home/pages/notification-center/rule-table/escalations.component.html new file mode 100644 index 0000000000..e11a1227f7 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/notification-center/rule-table/escalations.component.html @@ -0,0 +1,45 @@ + +
+
+ + + +
+
+ notification.no-rule +
+
+ +
+
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 new file mode 100644 index 0000000000..fa8e90ce6c --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/notification-center/rule-table/escalations.component.ts @@ -0,0 +1,172 @@ +/// +/// 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, OnDestroy } from '@angular/core'; +import { + AbstractControl, + ControlValueAccessor, + FormArray, + FormBuilder, + FormGroup, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + ValidationErrors, + Validator, + Validators +} from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@app/core/core.state'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { Subscription } from 'rxjs'; +import { QueueInfo } from '@shared/models/queue.models'; +import { UtilsService } from '@core/services/utils.service'; +import { guid } from '@core/utils'; +import { NonConfirmedNotificationEscalation } from '@shared/models/notification.models'; + +@Component({ + selector: 'tb-escalations-component', + templateUrl: './escalations.component.html', + styleUrls: [], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => EscalationsComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => EscalationsComponent), + multi: true, + } + ] +}) +export class EscalationsComponent implements ControlValueAccessor, Validator, OnDestroy { + + escalationsFormGroup: FormGroup; + newEscalation = false; + + private requiredValue: boolean; + get required(): boolean { + return this.requiredValue; + } + @Input() + set required(value: boolean) { + this.requiredValue = coerceBooleanProperty(value); + } + + @Input() + disabled: boolean; + + private mainEscalaion = { + delayInSec: null, + notificationTargetId: null + }; + + private valueChangeSubscription$: Subscription = null; + + private propagateChange = (v: any) => { }; + + constructor(private store: Store, + private utils: UtilsService, + private fb: FormBuilder) { + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + ngOnDestroy() { + if (this.valueChangeSubscription$) { + this.valueChangeSubscription$.unsubscribe(); + } + } + + registerOnTouched(fn: any): void { + } + + ngOnInit() { + this.escalationsFormGroup = this.fb.group({ + escalations: this.fb.array([]) + }); + } + + get escalationsFormArray(): FormArray { + return this.escalationsFormGroup.get('escalations') as FormArray; + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.escalationsFormGroup.disable({emitEvent: false}); + } else { + this.escalationsFormGroup.enable({emitEvent: false}); + } + } + + writeValue(escalations: Array | null): void { + if (this.valueChangeSubscription$) { + this.valueChangeSubscription$.unsubscribe(); + } + const escalationsControls: Array = []; + if (escalations) { + escalations.forEach((escalation, index) => { + escalationsControls.push(this.fb.control(escalation, [Validators.required])); + }); + } else { + escalationsControls.push(this.fb.control(this.mainEscalaion, [Validators.required])); + } + this.escalationsFormGroup.setControl('escalations', this.fb.array(escalationsControls)); + if (this.disabled) { + this.escalationsFormGroup.disable({emitEvent: false}); + } else { + this.escalationsFormGroup.enable({emitEvent: false}); + } + this.valueChangeSubscription$ = this.escalationsFormGroup.valueChanges.subscribe(() => + this.updateModel() + ); + } + + public removeEscalation(index: number) { + (this.escalationsFormGroup.get('escalations') as FormArray).removeAt(index); + } + + public addEscalation() { + const escalation: NonConfirmedNotificationEscalation = { + delayInSec: null, + notificationTargetId: null + }; + this.newEscalation = true; + const escalationArray = this.escalationsFormGroup.get('escalations') as FormArray; + escalationArray.push(this.fb.control(escalation, [])); + this.escalationsFormGroup.updateValueAndValidity(); + if (!this.escalationsFormGroup.valid) { + this.updateModel(); + } + } + + public validate(c: AbstractControl): ValidationErrors | null { + return this.escalationsFormGroup.valid ? null : { + escalation: { + valid: false, + }, + }; + } + + private updateModel() { + const escalations: Array = this.escalationsFormGroup.get('escalations').value; + 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 a51900720f..344f5a96ad 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 @@ -15,118 +15,224 @@ limitations under the License. --> -
- -

{{ (isAdd ? 'notification.add-notification-target' : 'notification.edit-notification-target') | translate }}

- - -
- - -
-
- - notification.rule-name - - - {{ 'notification.rule-name-required' | translate }} - - - - -
-
- notification.hierarchy-of-receiving -
-
- notification.rule - notification.recipient -
-
-
-
-
-
- First recipient - After -
-
- After - - -
- - -
- -
+ +

{{'notification.add-rule' | translate }}

+ + +
+ + +
+
+ + + check + + + {{ 'notification.basic-settings' | translate }} + + + notification.rule-name + + + {{ 'notification.rule-name-required' | translate }} + + + + + + notification.trigger.trigger + + + {{ triggerTypeTranslationMap.get(trigger) | translate }} + + + + {{ 'notification.trigger.trigger-required' | translate }} + + + + + + {{ 'notification.type-settings' | translate }} +
+
+ notification.filter + + alarm.alarm-type-list + + + {{type}} + cancel + + + + + + + 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.hierarchy-of-receiving + +
+ + + notification.description + + +
+
+ + + {{ 'notification.type-settings' | translate }} +
+ notification.filter-by +
+ + {{ 'notification.device' | translate }} + {{ 'notification.device-profile' | translate }} +
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - notification.description - - -
-
- -
- - -
- + + + + + + + + notification.description + + + + + + + {{ 'notification.type-settings' | translate }} +
+
+ notification.filter + + +
+ notification.status + {{ 'notification.created' | translate }} + {{ 'notification.updated' | translate }} + {{ 'notification.deleted' | translate }} +
+
+ + + + 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 07d9023294..308c7170b2 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 @@ -13,8 +13,67 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +@import "../../../../../../theme"; :host { + ::ng-deep{ + width: 100%; + min-width: 800px !important; + max-width: 100%; + + .mat-button-toggle-group.tb-notification-unread-toggle-group { + &.mat-button-toggle-group-appearance-standard { + border: none; + border-radius: 14px; + + .mat-button-toggle + .mat-button-toggle { + border-left: none; + } + } + + .mat-button-toggle { + background: rgba(0, 0, 0, 0.06); + height: 28px; + align-items: center; + display: flex; + + .mat-button-toggle-ripple { + top: 2px; + left: 2px; + right: 2px; + bottom: 2px; + border-radius: 12px; + } + } + + .mat-button-toggle-button { + color: #959595; + } + + .mat-button-toggle-focus-overlay { + border-radius: 14px; + margin: 2px; + } + + .mat-button-toggle-checked .mat-button-toggle-button { + background-color: $tb-primary-color; + color: #fff; + border-radius: 14px; + margin-left: 2px; + margin-right: 2px; + } + + .mat-button-toggle-appearance-standard .mat-button-toggle-label-content { + line-height: 24px; + font-size: 16px; + font-weight: 500; + } + + .mat-button-toggle-checked.mat-button-toggle-appearance-standard:not(.mat-button-toggle-disabled):hover .mat-button-toggle-focus-overlay { + opacity: .01; + } + } + } .fields-group { padding: 0 16px 8px; margin-bottom: 10px; 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 b8bb7bd00e..24f3068b13 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 @@ -14,13 +14,8 @@ /// limitations under the License. /// -import { - NotificationRule, - NotificationTarget, - NotificationTargetConfigType, - NotificationTargetConfigTypeTranslateMap -} from '@shared/models/notification.models'; -import { Component, Inject, OnDestroy } from '@angular/core'; +import { NotificationRule, TriggerType, TriggerTypeTranslationMap } from '@shared/models/notification.models'; +import { Component, ElementRef, Inject, OnDestroy, ViewChild } from '@angular/core'; import { DialogComponent } from '@shared/components/dialog.component'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @@ -28,10 +23,26 @@ import { Router } from '@angular/router'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { NotificationService } from '@core/http/notification.service'; -import { EntityType } from '@shared/models/entity-type.models'; +import { EntityType, entityTypeTranslations } from '@shared/models/entity-type.models'; import { deepTrim, isDefined } from '@core/utils'; -import { Subject } from 'rxjs'; -import { takeUntil } from 'rxjs/operators'; +import { Observable, of, Subject } from 'rxjs'; +import { map, mergeMap, share, startWith } 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 { + AlarmSearchStatus, + alarmSearchStatusTranslations, + AlarmSeverity, + alarmSeverityTranslations +} from '@shared/models/alarm.models'; +import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop'; +import { MatAutocomplete, MatAutocompleteSelectedEvent } from '@angular/material/autocomplete'; +import { TranslateService } from '@ngx-translate/core'; +import { TruncatePipe } from '@shared/pipe/truncate.pipe'; export interface RuleNotificationDialogData { rule?: NotificationRule; @@ -46,20 +57,56 @@ export interface RuleNotificationDialogData { export class RuleNotificationDialogComponent extends DialogComponent implements OnDestroy { + @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; - // notificationTargetConfigType = NotificationTargetConfigType; - // notificationTargetConfigTypes: NotificationTargetConfigType[] = Object.values(NotificationTargetConfigType); - // notificationTargetConfigTypeTranslateMap = NotificationTargetConfigTypeTranslateMap; + alarmTemplateForm: FormGroup; + deviceInactivityTemplateForm: FormGroup; + entityActionTemplateForm: FormGroup; + + triggerType = TriggerType; + triggerTypes: TriggerType[] = Object.values(TriggerType); + triggerTypeTranslationMap = TriggerTypeTranslationMap; + + alarmSeverities = Object.keys(AlarmSeverity); + alarmSeverityEnum = AlarmSeverity; + alarmSeverityTranslationMap = alarmSeverityTranslations; + + alarmSearchStatuses = [AlarmSearchStatus.ACTIVE, + AlarmSearchStatus.CLEARED, + AlarmSearchStatus.ACK, + AlarmSearchStatus.UNACK]; + alarmSearchStatusTranslationMap = alarmSearchStatusTranslations; + entityType = EntityType; + entityTypes = Object.values(EntityType); isAdd = true; + selectedIndex = 0; + + filteredDisplaySeverities: Observable>; + severitySearchText = ''; + + severityInputChange = new Subject(); + private readonly destroy$ = new Subject(); constructor(protected store: Store, protected router: Router, protected dialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) public data: RuleNotificationDialogData, + private breakpointObserver: BreakpointObserver, private fb: FormBuilder, + public translate: TranslateService, + public truncate: TruncatePipe, private notificationService: NotificationService) { super(store, router, dialogRef); @@ -67,24 +114,231 @@ export class RuleNotificationDialogComponent extends this.isAdd = data.isAdd; } + this.stepperOrientation = this.breakpointObserver.observe(MediaBreakpoints['gt-xs']) + .pipe(map(({matches}) => matches ? 'horizontal' : 'vertical')); + this.ruleNotificationForm = this.fb.group({ name: [null, Validators.required], templateId: [null, Validators.required], + trigger: [this.triggerType.ALARM, Validators.required], configuration: this.fb.group({ - ids: [], - interval: [], - // type: [NotificationTargetConfigType.ALL_USERS], - description: [null], - // usersIds: [{value: null, disabled: true}, Validators.required], - // customerId: [{value: null, disabled: true}, Validators.required], - // getCustomerIdFromOriginatorEntity: [{value: false, disabled: true}], + escalationConfig: this.fb.group({ + escalations: [[]] + }), + description: [null] }) }); - // if (isDefined(data.target)) { - // this.targetNotificationForm.patchValue(data.target, {emitEvent: false}); - // this.targetNotificationForm.get('configuration.type').updateValueAndValidity({onlySelf: true}); - // } + this.alarmTemplateForm = this.fb.group({ + alarmTypeList: [[], Validators.required], + alarmSeverityList: [[], Validators.required], + alarmStatusList: [[], Validators.required], + escalationConfig: [], + description: [''] + }); + + this.deviceInactivityTemplateForm = this.fb.group({ + filterByDevice: [true], + deviceId: [], + deviceProfileId: [], + notificationTargetId: [], + description: [''] + }); + + this.entityActionTemplateForm = this.fb.group({ + entityType: [], + status: this.fb.group({ + created: [false], + updated: [false], + deleted: [false] + }), + notificationTargetId: [], + description: [''] + }); + + this.filteredDisplaySeverities = this.severityInputChange + .pipe( + startWith(''), + map((value) => value ? value : ''), + mergeMap(name => this.fetchSeverities(name) ), + share() + ); + } + + onSeverityRemoved(severity: string): void { + const severities: string[] = this.alarmTemplateForm.get('alarmSeverityList').value; + const index = severities.indexOf(severity); + if (index > -1) { + severities.splice(index, 1); + this.alarmTemplateForm.get('alarmSeverityList').setValue(severities); + this.alarmTemplateForm.get('alarmSeverityList').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(''); + } + } + } + + private addSeverity(existingSeverity: string): boolean { + if (existingSeverity) { + const displaySeverities: string[] = this.alarmTemplateForm.get('alarmSeverityList').value; + const index = displaySeverities.indexOf(existingSeverity); + if (index === -1) { + displaySeverities.push(existingSeverity); + this.alarmTemplateForm.get('alarmSeverityList').setValue(displaySeverities); + this.alarmTemplateForm.get('alarmSeverityList').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('alarmTypeList').value; + } + + public removeAlarmType(type: string): void { + const types: string[] = this.alarmTemplateForm.get('alarmTypeList').value; + const index = types.indexOf(type); + if (index >= 0) { + types.splice(index, 1); + this.alarmTemplateForm.get('alarmTypeList').setValue(types); + this.alarmTemplateForm.get('alarmTypeList').markAsDirty(); + } + } + + public addAlarmType(event: MatChipInputEvent): void { + const input = event.input; + const value = event.value; + + const types: string[] = this.alarmTemplateForm.get('alarmTypeList').value; + + if ((value || '').trim()) { + types.push(value.trim()); + this.alarmTemplateForm.get('alarmTypeList').setValue(types); + this.alarmTemplateForm.get('alarmTypeList').markAsDirty(); + } + + if (input) { + input.value = ''; + } + } + + changeStep($event: StepperSelectionEvent) { + this.selectedIndex = $event.selectedIndex; + } + + backStep() { + this.addNotificationRule.previous(); + } + + nextStep() { + if (this.selectedIndex >= this.maxStepperIndex) { + this.add(); + } else { + this.addNotificationRule.next(); + } + } + + 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) { + return 'action.add'; + } + return 'action.next'; + } + + private get maxStepperIndex(): number { + return this.addNotificationRule?._steps?.length - 1; + } + + private add(): void { + if (this.allValid()) { + const formValue: NotificationRule = this.ruleNotificationForm.value; + // if (formValue.configuration.deliveryMethodsTemplates.PUSH.enabled) { + // Object.assign(formValue.configuration.deliveryMethodsTemplates.PUSH, this.pushTemplateForm.value); + // } else { + // delete formValue.configuration.deliveryMethodsTemplates.PUSH; + // } + // if (formValue.configuration.deliveryMethodsTemplates.EMAIL.enabled) { + // Object.assign(formValue.configuration.deliveryMethodsTemplates.EMAIL, this.emailTemplateForm.value); + // } else { + // delete formValue.configuration.deliveryMethodsTemplates.EMAIL; + // } + // if (formValue.configuration.deliveryMethodsTemplates.SMS.enabled) { + // Object.assign(formValue.configuration.deliveryMethodsTemplates.SMS, this.smsTemplateForm.value); + // } else { + // delete formValue.configuration.deliveryMethodsTemplates.SMS; + // } + this.notificationService.saveNotificationRule(deepTrim(formValue)).subscribe( + (target) => this.dialogRef.close(target) + ); + } + } + + private allValid(): boolean { + return !this.addNotificationRule.steps.find((item, index) => { + if (item.stepControl.invalid) { + item.interacted = true; + this.addNotificationRule.selectedIndex = index; + return true; + } else { + return false; + } + }); } ngOnDestroy() { diff --git a/ui-ngx/src/app/shared/components/time/timeinterval.component.html b/ui-ngx/src/app/shared/components/time/timeinterval.component.html index 6d40e2f491..01dac82750 100644 --- a/ui-ngx/src/app/shared/components/time/timeinterval.component.html +++ b/ui-ngx/src/app/shared/components/time/timeinterval.component.html @@ -51,7 +51,7 @@ -
+
diff --git a/ui-ngx/src/app/shared/components/time/timeinterval.component.ts b/ui-ngx/src/app/shared/components/time/timeinterval.component.ts index 093435add4..7a4f3d4153 100644 --- a/ui-ngx/src/app/shared/components/time/timeinterval.component.ts +++ b/ui-ngx/src/app/shared/components/time/timeinterval.component.ts @@ -78,6 +78,8 @@ export class TimeintervalComponent implements OnInit, ControlValueAccessor { this.hideFlagValue = val; } + @Input() disabledAdvanced = false; + @Output() hideFlagChange = new EventEmitter(); @Input() disabled: boolean; diff --git a/ui-ngx/src/app/shared/models/entity-type.models.ts b/ui-ngx/src/app/shared/models/entity-type.models.ts index 165e011d7f..0550523a1a 100644 --- a/ui-ngx/src/app/shared/models/entity-type.models.ts +++ b/ui-ngx/src/app/shared/models/entity-type.models.ts @@ -331,14 +331,51 @@ export const entityTypeTranslations = new Map([ + [TriggerType.ALARM, 'notification.trigger.alarm'], + [TriggerType.DEVICE_INACTIVITY, 'notification.trigger.device-inactivity'], + [TriggerType.ENTITY_ACTION, 'notification.trigger.entity-action'] +]); 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 ad27f8067b..50209b73b9 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -1952,7 +1952,14 @@ "list-of-edges": "{ count, plural, 1 {One edge} other {List of # edges} }", "edge-name-starts-with": "Edges whose names start with '{{prefix}}'", "type-tb-resource": "Resource", - "type-ota-package": "OTA package" + "type-ota-package": "OTA package", + "type-rpc": "RPC", + "type-queue": "Queue", + "type-notification": "Notification", + "type-notification-rule": "Notification rule", + "type-notification-target": "Notification recipient", + "type-notification-request": "Notification request", + "type-notification-template": "Notification template" }, "entity-field": { "created-time": "Created time", @@ -2768,11 +2775,34 @@ "type": "Type", "unread": "Unread", "view-all": "View all", - "web-settings": "Web settings" + "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" + "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" + }, + "trigger-settings": "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", + "device-profile": "Device profile", + "filter-by": "Filter by", + "entity-type": "Entity type", + "created": "Created", + "updated": "Updated", + "deleted": "Deleted", + "type-settings": "Type settings" }, "ota-update": { "add": "Add package",