UI: Norification rule

This commit is contained in:
Artem Dzhereleiko 2023-01-31 02:03:51 +02:00
parent 0e83c8ac74
commit 7ea0c23c65
10 changed files with 415 additions and 347 deletions

View File

@ -24,7 +24,7 @@ import { Direction } from '@shared/models/page/sort-order';
import { import {
NotificationRule, NotificationRule,
NotificationTarget, NotificationTarget,
NotificationTargetConfigTypeTranslateMap NotificationTargetConfigTypeTranslateMap, TriggerTypeTranslationMap
} from '@shared/models/notification.models'; } from '@shared/models/notification.models';
import { NotificationService } from '@core/http/notification.service'; import { NotificationService } from '@core/http/notification.service';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
@ -62,8 +62,7 @@ export class RuleTableConfig extends EntityTableConfig<NotificationRule> {
this.deleteEntityContent = () => this.translate.instant('notification.delete-rule-text'); this.deleteEntityContent = () => this.translate.instant('notification.delete-rule-text');
this.deleteEntity = id => this.notificationService.deleteNotificationRule(id.id); this.deleteEntity = id => this.notificationService.deleteNotificationRule(id.id);
this.cellActionDescriptors = this.configureCellActions(); // this.cellActionDescriptors = this.configureCellActions();
this.headerComponent = RuleTableHeaderComponent; this.headerComponent = RuleTableHeaderComponent;
this.onEntityAction = action => this.onTargetAction(action); this.onEntityAction = action => this.onTargetAction(action);
@ -71,12 +70,12 @@ export class RuleTableConfig extends EntityTableConfig<NotificationRule> {
this.columns.push( this.columns.push(
new EntityTableColumn<NotificationRule>('name', 'notification.rule-name', '30%'), new EntityTableColumn<NotificationRule>('name', 'notification.rule-name', '30%'),
new EntityTableColumn<NotificationRule>('templateId', 'notification.template', '30%', new EntityTableColumn<NotificationRule>('templateId', 'notification.template', '15%',
(rule) => `${rule.templateId}`, (rule) => `${rule.templateId.id}`,
() => ({}), false), () => ({}), false),
new EntityTableColumn<NotificationRule>('configuration.description', 'notification.description', '40%', new EntityTableColumn<NotificationRule>('triggerType', 'notification.trigger.trigger', '15%',
(rule) => rule.configuration.description || '', (rule) => this.translate.instant(TriggerTypeTranslationMap.get(rule.triggerType)) || '',
() => ({}), false) () => ({}), true)
); );
} }

View File

@ -17,22 +17,23 @@
--> -->
<form [formGroup]="escalationFormGroup" fxLayout="column"> <form [formGroup]="escalationFormGroup" fxLayout="column">
<div fxLayout="row" style="align-items: center; min-height: 74px; border-radius: 8px; background: rgba(0,0,0,0.04);"> <div class="escalation" fxLayout="row" fxLayout.xs="column" fxLayoutAlign="center center" fxLayoutAlign.xs="start">
<div fxFlex fxLayout="row"> <div fxFlex fxLayout="row" ngStyle.xs="padding-top: 10px">
<div fxFlex *ngIf="systemEscalation" style="padding: 0 10px;" translate>notification.first-recipient</div> <div fxFlex [fxShow]="systemEscalation" class="escalation-padding" translate>notification.first-recipient</div>
<div fxFlex *ngIf="!systemEscalation" fxLayout="row" style="align-items: center;"> <div fxFlex [fxShow]="!systemEscalation" fxLayout="row" fxLayout.xs="column" fxLayoutAlign="center center">
<span style="padding: 0 10px;">After</span> <span class="escalation-padding">After</span>
<tb-timeinterval <tb-timeinterval
style="min-width: 100px;"
ngStyle.xs="padding: 0 10px;"
formControlName="delayInSec" formControlName="delayInSec"
[disabledAdvanced]="true" [disabledAdvanced]="true"></tb-timeinterval>
style="padding-top: 8px; width: 150px; min-width: 150px;"></tb-timeinterval> <span fxFlex class="escalation-notify" translate>notification.notify</span>
<span fxFlex style="text-align: end; padding: 0 10px;" translate>notification.notify</span>
</div> </div>
</div> </div>
<div fxFlex style="padding: 0 10px;"> <div fxFlex class="escalation-padding">
<tb-entity-list <tb-entity-list
required required
formControlName="notificationTargetId" formControlName="targets"
[entityType]="entityType.NOTIFICATION_TARGET" [entityType]="entityType.NOTIFICATION_TARGET"
placeholderText="notification.add-target"> placeholderText="notification.add-target">
</tb-entity-list> </tb-entity-list>

View File

@ -0,0 +1,34 @@
/**
* 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.
*/
:host {
.escalation {
height: 100%;
min-height: 74px;
max-height: 100%;
border-radius: 8px;
background: rgba(0,0,0,0.04);
}
.escalation-padding {
padding: 0 10px;
}
.escalation-notify {
text-align: end;
padding: 0 10px;
}
}

View File

@ -22,18 +22,20 @@ import {
FormGroup, FormGroup,
NG_VALIDATORS, NG_VALIDATORS,
NG_VALUE_ACCESSOR, NG_VALUE_ACCESSOR,
Validator, Validators Validator,
Validators
} from '@angular/forms'; } from '@angular/forms';
import { UtilsService } from '@core/services/utils.service'; import { UtilsService } from '@core/services/utils.service';
import { isDefinedAndNotNull } from '@core/utils'; import { isDefinedAndNotNull } from '@core/utils';
import { Subscription } from 'rxjs'; import { Subject } from 'rxjs';
import { NonConfirmedNotificationEscalation } from '@shared/models/notification.models'; import { NonConfirmedNotificationEscalation } from '@shared/models/notification.models';
import { EntityType } from '@shared/models/entity-type.models'; import { EntityType } from '@shared/models/entity-type.models';
import { takeUntil } from 'rxjs/operators';
@Component({ @Component({
selector: 'tb-escalation-form', selector: 'tb-escalation-form',
templateUrl: './escalation-form.component.html', templateUrl: './escalation-form.component.html',
styleUrls: [], styleUrls: ['./escalation-form.component.scss'],
providers: [ providers: [
{ {
provide: NG_VALUE_ACCESSOR, provide: NG_VALUE_ACCESSOR,
@ -59,10 +61,10 @@ export class EscalationFormComponent implements ControlValueAccessor, OnInit, On
entityType = EntityType; entityType = EntityType;
private modelValue: NonConfirmedNotificationEscalation; private modelValue;
private propagateChange = null; private propagateChange = null;
private propagateChangePending = false; private propagateChangePending = false;
private valueChange$: Subscription = null; private destroy$ = new Subject();
constructor(private utils: UtilsService, constructor(private utils: UtilsService,
private fb: FormBuilder) { private fb: FormBuilder) {
@ -84,19 +86,17 @@ export class EscalationFormComponent implements ControlValueAccessor, OnInit, On
ngOnInit() { ngOnInit() {
this.escalationFormGroup = this.fb.group( this.escalationFormGroup = this.fb.group(
{ {
delayInSec: [null], delayInSec: [0],
notificationTargetId: [null, Validators.required], targets: [null, Validators.required],
});
this.valueChange$ = this.escalationFormGroup.valueChanges.subscribe(() => {
this.updateModel();
}); });
this.escalationFormGroup.valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe(() => this.updateModel());
} }
ngOnDestroy() { ngOnDestroy() {
if (this.valueChange$) { this.destroy$.next();
this.valueChange$.unsubscribe(); this.destroy$.complete();
this.valueChange$ = null;
}
} }
setDisabledState(isDisabled: boolean): void { setDisabledState(isDisabled: boolean): void {

View File

@ -30,11 +30,10 @@ import {
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@app/core/core.state'; import { AppState } from '@app/core/core.state';
import { coerceBooleanProperty } from '@angular/cdk/coercion'; import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { Subscription } from 'rxjs'; import { Subject } from 'rxjs';
import { QueueInfo } from '@shared/models/queue.models';
import { UtilsService } from '@core/services/utils.service'; import { UtilsService } from '@core/services/utils.service';
import { guid } from '@core/utils';
import { NonConfirmedNotificationEscalation } from '@shared/models/notification.models'; import { NonConfirmedNotificationEscalation } from '@shared/models/notification.models';
import { takeUntil } from 'rxjs/operators';
@Component({ @Component({
selector: 'tb-escalations-component', selector: 'tb-escalations-component',
@ -71,11 +70,11 @@ export class EscalationsComponent implements ControlValueAccessor, Validator, On
disabled: boolean; disabled: boolean;
private mainEscalaion = { private mainEscalaion = {
delayInSec: null, delayInSec: 0,
notificationTargetId: null targets: null
}; };
private valueChangeSubscription$: Subscription = null; private destroy$ = new Subject();
private propagateChange = (v: any) => { }; private propagateChange = (v: any) => { };
@ -89,9 +88,8 @@ export class EscalationsComponent implements ControlValueAccessor, Validator, On
} }
ngOnDestroy() { ngOnDestroy() {
if (this.valueChangeSubscription$) { this.destroy$.next();
this.valueChangeSubscription$.unsubscribe(); this.destroy$.complete();
}
} }
registerOnTouched(fn: any): void { registerOnTouched(fn: any): void {
@ -101,6 +99,10 @@ export class EscalationsComponent implements ControlValueAccessor, Validator, On
this.escalationsFormGroup = this.fb.group({ this.escalationsFormGroup = this.fb.group({
escalations: this.fb.array([]) escalations: this.fb.array([])
}); });
this.escalationsFormGroup.valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe(() => this.updateModel());
} }
get escalationsFormArray(): FormArray { get escalationsFormArray(): FormArray {
@ -117,9 +119,9 @@ export class EscalationsComponent implements ControlValueAccessor, Validator, On
} }
writeValue(escalations: Array<NonConfirmedNotificationEscalation> | null): void { writeValue(escalations: Array<NonConfirmedNotificationEscalation> | null): void {
if (this.valueChangeSubscription$) { if (escalations?.length === this.escalationsFormArray.length) {
this.valueChangeSubscription$.unsubscribe(); this.escalationsFormArray.patchValue(escalations, {emitEvent: false});
} } else {
const escalationsControls: Array<AbstractControl> = []; const escalationsControls: Array<AbstractControl> = [];
if (escalations) { if (escalations) {
escalations.forEach((escalation, index) => { escalations.forEach((escalation, index) => {
@ -128,15 +130,13 @@ export class EscalationsComponent implements ControlValueAccessor, Validator, On
} else { } else {
escalationsControls.push(this.fb.control(this.mainEscalaion, [Validators.required])); escalationsControls.push(this.fb.control(this.mainEscalaion, [Validators.required]));
} }
this.escalationsFormGroup.setControl('escalations', this.fb.array(escalationsControls)); this.escalationsFormGroup.setControl('escalations', this.fb.array(escalationsControls), {emitEvent: false});
if (this.disabled) { if (this.disabled) {
this.escalationsFormGroup.disable({emitEvent: false}); this.escalationsFormGroup.disable({emitEvent: false});
} else { } else {
this.escalationsFormGroup.enable({emitEvent: false}); this.escalationsFormGroup.enable({emitEvent: false});
} }
this.valueChangeSubscription$ = this.escalationsFormGroup.valueChanges.subscribe(() => }
this.updateModel()
);
} }
public removeEscalation(index: number) { public removeEscalation(index: number) {
@ -144,9 +144,9 @@ export class EscalationsComponent implements ControlValueAccessor, Validator, On
} }
public addEscalation() { public addEscalation() {
const escalation: NonConfirmedNotificationEscalation = { const escalation = {
delayInSec: null, delayInSec: 0,
notificationTargetId: null targets: null
}; };
this.newEscalation = true; this.newEscalation = true;
const escalationArray = this.escalationsFormGroup.get('escalations') as FormArray; const escalationArray = this.escalationsFormGroup.get('escalations') as FormArray;
@ -166,7 +166,7 @@ export class EscalationsComponent implements ControlValueAccessor, Validator, On
} }
private updateModel() { private updateModel() {
const escalations: Array<NonConfirmedNotificationEscalation> = this.escalationsFormGroup.get('escalations').value; const escalations = this.escalationsFormGroup.get('escalations').value;
this.propagateChange(escalations); this.propagateChange(escalations);
} }
} }

View File

@ -15,8 +15,8 @@
limitations under the License. limitations under the License.
--> -->
<section style="width: 800px; min-width: 100%; max-width: 100%">
<mat-toolbar color="primary"> <mat-toolbar color="primary">
<h2>{{'notification.add-rule' | translate }}</h2> <h2>{{'notification.add-rule' | translate }}</h2>
<span fxFlex></span> <span fxFlex></span>
<button mat-icon-button <button mat-icon-button
@ -24,11 +24,11 @@
type="button"> type="button">
<mat-icon class="material-icons">close</mat-icon> <mat-icon class="material-icons">close</mat-icon>
</button> </button>
</mat-toolbar> </mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async"> <mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar> </mat-progress-bar>
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div> <div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
<div mat-dialog-content> <div mat-dialog-content ngStyle.xs="padding: 0;">
<mat-horizontal-stepper [linear]="true" labelPosition="end" #addNotificationRule [orientation]="(stepperOrientation | async)" <mat-horizontal-stepper [linear]="true" labelPosition="end" #addNotificationRule [orientation]="(stepperOrientation | async)"
(selectionChange)="changeStep($event)"> (selectionChange)="changeStep($event)">
<ng-template matStepperIcon="edit"> <ng-template matStepperIcon="edit">
@ -53,32 +53,32 @@
</tb-template-autocomplete> </tb-template-autocomplete>
<mat-form-field class="mat-block"> <mat-form-field class="mat-block">
<mat-label translate>notification.trigger.trigger</mat-label> <mat-label translate>notification.trigger.trigger</mat-label>
<mat-select formControlName="trigger" required> <mat-select formControlName="triggerType" required>
<mat-option *ngFor="let trigger of triggerTypes" [value]="trigger"> <mat-option *ngFor="let trigger of triggerTypes" [value]="trigger">
{{ triggerTypeTranslationMap.get(trigger) | translate }} {{ triggerTypeTranslationMap.get(trigger) | translate }}
</mat-option> </mat-option>
</mat-select> </mat-select>
<mat-error *ngIf="ruleNotificationForm.get('trigger').hasError('required')"> <mat-error *ngIf="ruleNotificationForm.get('triggerType').hasError('required')">
{{ 'notification.trigger.trigger-required' | translate }} {{ 'notification.trigger.trigger-required' | translate }}
</mat-error> </mat-error>
</mat-form-field> </mat-form-field>
</form> </form>
</mat-step> </mat-step>
<mat-step *ngIf="ruleNotificationForm.get('trigger').value === triggerType.ALARM" <mat-step optional [stepControl]="alarmTemplateForm"
[stepControl]="alarmTemplateForm"> *ngIf="ruleNotificationForm.get('triggerType').value === triggerType.ALARM">
<ng-template matStepLabel>{{ 'notification.type-settings' | translate }}</ng-template> <ng-template matStepLabel>{{ 'notification.type-settings' | translate }}</ng-template>
<form [formGroup]="alarmTemplateForm"> <form [formGroup]="alarmTemplateForm">
<fieldset class="fields-group"> <fieldset class="fields-group">
<span class="fields-group-title" translate>notification.filter</span> <span class="fields-group-title" translate>notification.filter</span>
<mat-form-field fxFlex class="mat-block" floatLabel="always"> <mat-form-field fxFlex class="mat-block" floatLabel="always">
<mat-label translate>alarm.alarm-type-list</mat-label> <mat-label translate>alarm.alarm-type-list</mat-label>
<mat-chip-list #alarmTypeChipList formControlName="alarmTypeList"> <mat-chip-list #alarmTypeChipList formControlName="alarmTypes">
<mat-chip *ngFor="let type of alarmTypeList()" [selectable]="true" <mat-chip *ngFor="let type of alarmTypeList()" [selectable]="true"
[removable]="true" (removed)="removeAlarmType(type)"> [removable]="true" (removed)="removeAlarmType(type)">
{{type}} {{type}}
<mat-icon matChipRemove>cancel</mat-icon> <mat-icon matChipRemove>cancel</mat-icon>
</mat-chip> </mat-chip>
<input placeholder="{{ !alarmTemplateForm.get('alarmTypeList').value?.length ? ('alarm.any-type' | translate) : '' }}" <input placeholder="{{ !alarmTemplateForm.get('alarmTypes').value?.length ? ('alarm.any-type' | translate) : '' }}"
[matChipInputFor]="alarmTypeChipList" [matChipInputFor]="alarmTypeChipList"
[matChipInputSeparatorKeyCodes]="separatorKeysCodes" [matChipInputSeparatorKeyCodes]="separatorKeysCodes"
matChipInputAddOnBlur matChipInputAddOnBlur
@ -88,16 +88,16 @@
<mat-form-field fxFlex class="mat-block"> <mat-form-field fxFlex class="mat-block">
<mat-label translate>alarm.alarm-severity-list</mat-label> <mat-label translate>alarm.alarm-severity-list</mat-label>
<mat-chip-list #severitiesChipList <mat-chip-list #severitiesChipList formControlName="alarmSeverities"
required> required>
<mat-chip *ngFor="let severity of alarmTemplateForm.get('alarmSeverityList').value" <mat-chip *ngFor="let severity of alarmTemplateForm.get('alarmSeverities').value"
[removable]="true" (removed)="onSeverityRemoved(severity)"> [removable]="true" (removed)="onSeverityRemoved(severity)">
{{ alarmSeverityTranslationMap.get(severity) | translate }} {{ alarmSeverityTranslationMap.get(severity) | translate }}
<mat-icon matChipRemove>cancel</mat-icon> <mat-icon matChipRemove>cancel</mat-icon>
</mat-chip> </mat-chip>
<input matInput <input matInput
type="text" type="text"
placeholder="{{ !alarmTemplateForm.get('alarmSeverityList').value?.length ? ('alarm.any-severity' | translate) : '' }}" placeholder="{{ !alarmTemplateForm.get('alarmSeverities').value?.length ? ('alarm.any-severity' | translate) : '' }}"
style="max-width: 200px;" style="max-width: 200px;"
#severityInput #severityInput
(focusin)="onSeverityInputFocus()" (focusin)="onSeverityInputFocus()"
@ -134,12 +134,12 @@
</mat-form-field> </mat-form-field>
</fieldset> </fieldset>
<fieldset class="fields-group"> <fieldset class="fields-group" formGroupName="clearRule">
<span class="fields-group-title" translate>notification.clear-rule</span> <span class="fields-group-title" translate>notification.clear-rule</span>
<mat-form-field fxFlex class="mat-block" floatLabel="always"> <mat-form-field fxFlex class="mat-block" floatLabel="always">
<mat-label translate>alarm.alarm-status-list</mat-label> <mat-label translate>alarm.alarm-status-list</mat-label>
<mat-select formControlName="alarmStatusList" <mat-select formControlName="alarmStatus"
placeholder="{{ !alarmTemplateForm.get('alarmStatusList').value?.length ? ('alarm.any-status' | translate) : '' }}"> placeholder="{{ !alarmTemplateForm.get('clearRule.alarmStatus').value?.length ? ('alarm.any-status' | translate) : '' }}">
<mat-option *ngFor="let searchStatus of alarmSearchStatuses" [value]="searchStatus"> <mat-option *ngFor="let searchStatus of alarmSearchStatuses" [value]="searchStatus">
{{ alarmSearchStatusTranslationMap.get(searchStatus) | translate }} {{ alarmSearchStatusTranslationMap.get(searchStatus) | translate }}
</mat-option> </mat-option>
@ -149,7 +149,7 @@
<fieldset class="fields-group"> <fieldset class="fields-group">
<span class="fields-group-title" translate>notification.hierarchy-of-receiving</span> <span class="fields-group-title" translate>notification.hierarchy-of-receiving</span>
<tb-escalations-component formControlName="escalationConfig"></tb-escalations-component> <tb-escalations-component formControlName="escalationTable"></tb-escalations-component>
</fieldset> </fieldset>
<mat-form-field class="mat-block"> <mat-form-field class="mat-block">
@ -159,7 +159,7 @@
</form> </form>
</mat-step> </mat-step>
<mat-step optional *ngIf="ruleNotificationForm.get('trigger').value === triggerType.DEVICE_INACTIVITY" <mat-step optional *ngIf="ruleNotificationForm.get('triggerType').value === triggerType.DEVICE_INACTIVITY"
[stepControl]="deviceInactivityTemplateForm"> [stepControl]="deviceInactivityTemplateForm">
<ng-template matStepLabel>{{ 'notification.type-settings' | translate }}</ng-template> <ng-template matStepLabel>{{ 'notification.type-settings' | translate }}</ng-template>
<form [formGroup]="deviceInactivityTemplateForm"> <form [formGroup]="deviceInactivityTemplateForm">
@ -172,19 +172,25 @@
<mat-button-toggle fxFlex [value]=false>{{ 'notification.device-profile' | translate }}</mat-button-toggle> <mat-button-toggle fxFlex [value]=false>{{ 'notification.device-profile' | translate }}</mat-button-toggle>
</mat-button-toggle-group> </mat-button-toggle-group>
</div> </div>
<tb-device-profile-autocomplete <tb-entity-list
*ngIf="!deviceInactivityTemplateForm.get('filterByDevice').value"
formControlName="deviceProfileId"
[editProfileEnabled]="false">
</tb-device-profile-autocomplete>
<tb-entity-autocomplete
*ngIf="deviceInactivityTemplateForm.get('filterByDevice').value" *ngIf="deviceInactivityTemplateForm.get('filterByDevice').value"
formControlName="deviceId" required
formControlName="devices"
[labelText]="translate.instant('notification.devices')"
[placeholderText]="translate.instant('notification.device')"
[entityType]="entityType.DEVICE"> [entityType]="entityType.DEVICE">
</tb-entity-autocomplete> </tb-entity-list>
<tb-entity-list
*ngIf="!deviceInactivityTemplateForm.get('filterByDevice').value"
required
formControlName="deviceProfiles"
[labelText]="translate.instant('notification.device-profiles')"
[placeholderText]="translate.instant('notification.device-profile')"
[entityType]="entityType.DEVICE_PROFILE">
</tb-entity-list>
<tb-entity-list <tb-entity-list
required required
formControlName="notificationTargetId" formControlName="targets"
[entityType]="entityType.NOTIFICATION_TARGET" [entityType]="entityType.NOTIFICATION_TARGET"
placeholderText="notification.target"> placeholderText="notification.target">
</tb-entity-list> </tb-entity-list>
@ -195,7 +201,7 @@
</form> </form>
</mat-step> </mat-step>
<mat-step optional *ngIf="ruleNotificationForm.get('trigger').value === triggerType.ENTITY_ACTION" <mat-step optional *ngIf="ruleNotificationForm.get('triggerType').value === triggerType.ENTITY_ACTION"
[stepControl]="entityActionTemplateForm"> [stepControl]="entityActionTemplateForm">
<ng-template matStepLabel>{{ 'notification.type-settings' | translate }}</ng-template> <ng-template matStepLabel>{{ 'notification.type-settings' | translate }}</ng-template>
<form [formGroup]="entityActionTemplateForm"> <form [formGroup]="entityActionTemplateForm">
@ -206,7 +212,7 @@
[allowedEntityTypes]="entityTypes" [allowedEntityTypes]="entityTypes"
formControlName="entityType"> formControlName="entityType">
</tb-entity-type-select> </tb-entity-type-select>
<section formGroupName="status" fxLayout="column" fxLayoutGap="10px"> <section fxLayout="column" fxLayoutGap="10px">
<span fxFlex translate>notification.status</span> <span fxFlex translate>notification.status</span>
<mat-checkbox fxFlex formControlName="created" translate>{{ 'notification.created' | translate }}</mat-checkbox> <mat-checkbox fxFlex formControlName="created" translate>{{ 'notification.created' | translate }}</mat-checkbox>
<mat-checkbox fxFlex formControlName="updated" translate>{{ 'notification.updated' | translate }}</mat-checkbox> <mat-checkbox fxFlex formControlName="updated" translate>{{ 'notification.updated' | translate }}</mat-checkbox>
@ -215,7 +221,7 @@
</fieldset> </fieldset>
<tb-entity-list <tb-entity-list
required required
formControlName="notificationTargetId" formControlName="targets"
[entityType]="entityType.NOTIFICATION_TARGET" [entityType]="entityType.NOTIFICATION_TARGET"
placeholderText="notification.target"> placeholderText="notification.target">
</tb-entity-list> </tb-entity-list>
@ -226,13 +232,14 @@
</form> </form>
</mat-step> </mat-step>
</mat-horizontal-stepper> </mat-horizontal-stepper>
</div> </div>
<mat-divider></mat-divider> <mat-divider></mat-divider>
<div mat-dialog-actions fxLayout="row"> <div mat-dialog-actions fxLayout="row">
<button mat-stroked-button *ngIf="selectedIndex > 0" <button mat-stroked-button *ngIf="selectedIndex > 0"
(click)="backStep()">{{ 'action.back' | translate }}</button> (click)="backStep()">{{ 'action.back' | translate }}</button>
<span fxFlex></span> <span fxFlex></span>
<button mat-raised-button <button mat-raised-button
color="primary" color="primary"
(click)="nextStep()">{{ nextStepLabel() | translate }}</button> (click)="nextStep()">{{ nextStepLabel() | translate }}</button>
</div> </div>
</section>

View File

@ -17,9 +17,9 @@
:host { :host {
::ng-deep{ ::ng-deep{
width: 100%; //width: 100%;
min-width: 800px !important; //min-width: 800px;
max-width: 100%; //max-width: 100%;
.mat-button-toggle-group.tb-notification-unread-toggle-group { .mat-button-toggle-group.tb-notification-unread-toggle-group {
&.mat-button-toggle-group-appearance-standard { &.mat-button-toggle-group-appearance-standard {

View File

@ -23,10 +23,10 @@ import { Router } from '@angular/router';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { NotificationService } from '@core/http/notification.service'; import { NotificationService } from '@core/http/notification.service';
import { EntityType, entityTypeTranslations } from '@shared/models/entity-type.models'; import { EntityType } from '@shared/models/entity-type.models';
import { deepTrim, isDefined } from '@core/utils'; import { deepTrim, isDefined } from '@core/utils';
import { Observable, of, Subject } from 'rxjs'; import { Observable, of, Subject } from 'rxjs';
import { map, mergeMap, share, startWith } from 'rxjs/operators'; import { map, mergeMap, share, startWith, takeUntil } from 'rxjs/operators';
import { StepperOrientation, StepperSelectionEvent } from '@angular/cdk/stepper'; import { StepperOrientation, StepperSelectionEvent } from '@angular/cdk/stepper';
import { MatStepper } from '@angular/material/stepper'; import { MatStepper } from '@angular/material/stepper';
import { MediaBreakpoints } from '@shared/models/constants'; import { MediaBreakpoints } from '@shared/models/constants';
@ -34,12 +34,11 @@ import { BreakpointObserver } from '@angular/cdk/layout';
import { MatChipInputEvent, MatChipList } from '@angular/material/chips'; import { MatChipInputEvent, MatChipList } from '@angular/material/chips';
import { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes'; import { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes';
import { import {
AlarmSearchStatus,
alarmSearchStatusTranslations,
AlarmSeverity, AlarmSeverity,
alarmSeverityTranslations alarmSeverityTranslations,
AlarmStatus,
alarmStatusTranslations
} from '@shared/models/alarm.models'; } from '@shared/models/alarm.models';
import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';
import { MatAutocomplete, MatAutocompleteSelectedEvent } from '@angular/material/autocomplete'; import { MatAutocomplete, MatAutocompleteSelectedEvent } from '@angular/material/autocomplete';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { TruncatePipe } from '@shared/pipe/truncate.pipe'; import { TruncatePipe } from '@shared/pipe/truncate.pipe';
@ -80,11 +79,8 @@ export class RuleNotificationDialogComponent extends
alarmSeverityEnum = AlarmSeverity; alarmSeverityEnum = AlarmSeverity;
alarmSeverityTranslationMap = alarmSeverityTranslations; alarmSeverityTranslationMap = alarmSeverityTranslations;
alarmSearchStatuses = [AlarmSearchStatus.ACTIVE, alarmSearchStatuses = Object.values(AlarmStatus);
AlarmSearchStatus.CLEARED, alarmSearchStatusTranslationMap = alarmStatusTranslations;
AlarmSearchStatus.ACK,
AlarmSearchStatus.UNACK];
alarmSearchStatusTranslationMap = alarmSearchStatusTranslations;
entityType = EntityType; entityType = EntityType;
entityTypes = Object.values(EntityType); entityTypes = Object.values(EntityType);
@ -97,7 +93,7 @@ export class RuleNotificationDialogComponent extends
severityInputChange = new Subject<string>(); severityInputChange = new Subject<string>();
private readonly destroy$ = new Subject<void>(); private destroy$ = new Subject();
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
protected router: Router, protected router: Router,
@ -120,39 +116,54 @@ export class RuleNotificationDialogComponent extends
this.ruleNotificationForm = this.fb.group({ this.ruleNotificationForm = this.fb.group({
name: [null, Validators.required], name: [null, Validators.required],
templateId: [null, Validators.required], templateId: [null, Validators.required],
trigger: [this.triggerType.ALARM, Validators.required], triggerType: [null, Validators.required],
configuration: this.fb.group({ recipientsConfig: this.fb.group({
escalationConfig: this.fb.group({ triggerType: [],
escalations: [[]]
}), }),
description: [null] triggerConfig: this.fb.group({
triggerType: []
}) })
}); });
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({ this.alarmTemplateForm = this.fb.group({
alarmTypeList: [[], Validators.required], alarmTypes: [[], Validators.required],
alarmSeverityList: [[], Validators.required], alarmSeverities: [[], Validators.required],
alarmStatusList: [[], Validators.required], clearRule: this.fb.group({
escalationConfig: [], alarmStatus: []
}),
escalationTable: [],
description: [''] description: ['']
}); });
this.deviceInactivityTemplateForm = this.fb.group({ this.deviceInactivityTemplateForm = this.fb.group({
filterByDevice: [true], filterByDevice: [true],
deviceId: [], devices: [],
deviceProfileId: [], deviceProfiles: [],
notificationTargetId: [], targets: [[], Validators.required],
description: [''] description: ['']
}); });
this.deviceInactivityTemplateForm.get('filterByDevice').valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe(
value => this.deviceInactivityTemplateForm.get(value ? 'deviceProfiles' : 'devices').patchValue(null, {emitEvent: false})
);
this.entityActionTemplateForm = this.fb.group({ this.entityActionTemplateForm = this.fb.group({
entityType: [], entityType: [],
status: this.fb.group({
created: [false], created: [false],
updated: [false], updated: [false],
deleted: [false] deleted: [false],
}), targets: [[], Validators.required],
notificationTargetId: [],
description: [''] description: ['']
}); });
@ -166,12 +177,12 @@ export class RuleNotificationDialogComponent extends
} }
onSeverityRemoved(severity: string): void { onSeverityRemoved(severity: string): void {
const severities: string[] = this.alarmTemplateForm.get('alarmSeverityList').value; const severities: string[] = this.alarmTemplateForm.get('alarmSeverities').value;
const index = severities.indexOf(severity); const index = severities.indexOf(severity);
if (index > -1) { if (index > -1) {
severities.splice(index, 1); severities.splice(index, 1);
this.alarmTemplateForm.get('alarmSeverityList').setValue(severities); this.alarmTemplateForm.get('alarmSeverities').setValue(severities);
this.alarmTemplateForm.get('alarmSeverityList').markAsDirty(); this.alarmTemplateForm.get('alarmSeverities').markAsDirty();
this.severitiesChipList.errorState = !severities.length; this.severitiesChipList.errorState = !severities.length;
} }
} }
@ -193,12 +204,12 @@ export class RuleNotificationDialogComponent extends
private addSeverity(existingSeverity: string): boolean { private addSeverity(existingSeverity: string): boolean {
if (existingSeverity) { if (existingSeverity) {
const displaySeverities: string[] = this.alarmTemplateForm.get('alarmSeverityList').value; const displaySeverities: string[] = this.alarmTemplateForm.get('alarmSeverities').value;
const index = displaySeverities.indexOf(existingSeverity); const index = displaySeverities.indexOf(existingSeverity);
if (index === -1) { if (index === -1) {
displaySeverities.push(existingSeverity); displaySeverities.push(existingSeverity);
this.alarmTemplateForm.get('alarmSeverityList').setValue(displaySeverities); this.alarmTemplateForm.get('alarmSeverities').setValue(displaySeverities);
this.alarmTemplateForm.get('alarmSeverityList').markAsDirty(); this.alarmTemplateForm.get('alarmSeverities').markAsDirty();
this.severitiesChipList.errorState = false; this.severitiesChipList.errorState = false;
return true; return true;
} }
@ -239,16 +250,16 @@ export class RuleNotificationDialogComponent extends
} }
public alarmTypeList(): string[] { public alarmTypeList(): string[] {
return this.alarmTemplateForm.get('alarmTypeList').value; return this.alarmTemplateForm.get('alarmTypes').value;
} }
public removeAlarmType(type: string): void { public removeAlarmType(type: string): void {
const types: string[] = this.alarmTemplateForm.get('alarmTypeList').value; const types: string[] = this.alarmTemplateForm.get('alarmTypes').value;
const index = types.indexOf(type); const index = types.indexOf(type);
if (index >= 0) { if (index >= 0) {
types.splice(index, 1); types.splice(index, 1);
this.alarmTemplateForm.get('alarmTypeList').setValue(types); this.alarmTemplateForm.get('alarmTypes').setValue(types);
this.alarmTemplateForm.get('alarmTypeList').markAsDirty(); this.alarmTemplateForm.get('alarmTypes').markAsDirty();
} }
} }
@ -256,12 +267,12 @@ export class RuleNotificationDialogComponent extends
const input = event.input; const input = event.input;
const value = event.value; const value = event.value;
const types: string[] = this.alarmTemplateForm.get('alarmTypeList').value; const types: string[] = this.alarmTemplateForm.get('alarmTypes').value;
if ((value || '').trim()) { if ((value || '').trim()) {
types.push(value.trim()); types.push(value.trim());
this.alarmTemplateForm.get('alarmTypeList').setValue(types); this.alarmTemplateForm.get('alarmTypes').setValue(types);
this.alarmTemplateForm.get('alarmTypeList').markAsDirty(); this.alarmTemplateForm.get('alarmTypes').markAsDirty();
} }
if (input) { if (input) {
@ -307,22 +318,26 @@ export class RuleNotificationDialogComponent extends
private add(): void { private add(): void {
if (this.allValid()) { if (this.allValid()) {
const formValue: NotificationRule = this.ruleNotificationForm.value; const formValue = this.ruleNotificationForm.value;
// if (formValue.configuration.deliveryMethodsTemplates.PUSH.enabled) { const triggerType = this.ruleNotificationForm.get('triggerType').value;
// Object.assign(formValue.configuration.deliveryMethodsTemplates.PUSH, this.pushTemplateForm.value); if (triggerType === TriggerType.ALARM) {
// } else { Object.assign(formValue.triggerConfig, this.alarmTemplateForm.value);
// delete formValue.configuration.deliveryMethodsTemplates.PUSH; const parsedEscalationTable = {};
// } this.alarmTemplateForm.get('escalationTable').value.forEach(
// if (formValue.configuration.deliveryMethodsTemplates.EMAIL.enabled) { escalation => parsedEscalationTable[escalation.delayInSec] = escalation.targets
// Object.assign(formValue.configuration.deliveryMethodsTemplates.EMAIL, this.emailTemplateForm.value); );
// } else { formValue.recipientsConfig.escalationTable = parsedEscalationTable;
// delete formValue.configuration.deliveryMethodsTemplates.EMAIL; delete formValue.triggerConfig.escalationTable;
// } } else if (triggerType === TriggerType.DEVICE_INACTIVITY) {
// if (formValue.configuration.deliveryMethodsTemplates.SMS.enabled) { Object.assign(formValue.triggerConfig, this.deviceInactivityTemplateForm.value);
// Object.assign(formValue.configuration.deliveryMethodsTemplates.SMS, this.smsTemplateForm.value); delete formValue.triggerConfig.filterByDevice;
// } else { } else {
// delete formValue.configuration.deliveryMethodsTemplates.SMS; Object.assign(formValue.triggerConfig, this.entityActionTemplateForm.value);
// } }
if (triggerType === TriggerType.DEVICE_INACTIVITY || triggerType === TriggerType.ENTITY_ACTION) {
formValue.recipientsConfig.targets = this.entityActionTemplateForm.get('targets').value;
delete formValue.triggerConfig.trigger;
}
this.notificationService.saveNotificationRule(deepTrim(formValue)).subscribe( this.notificationService.saveNotificationRule(deepTrim(formValue)).subscribe(
(target) => this.dialogRef.close(target) (target) => this.dialogRef.close(target)
); );

View File

@ -24,6 +24,8 @@ import { NotificationTargetId } from '@shared/models/id/notification-target-id';
import { NotificationTemplateId } from '@shared/models/id/notification-template-id'; import { NotificationTemplateId } from '@shared/models/id/notification-template-id';
import { EntityId } from '@shared/models/id/entity-id'; import { EntityId } from '@shared/models/id/entity-id';
import { NotificationRuleId } from '@shared/models/id/notification-rule-id'; import { NotificationRuleId } from '@shared/models/id/notification-rule-id';
import { AlarmSeverity, AlarmStatus } from '@shared/models/alarm.models';
import { EntityType } from '@shared/models/entity-type.models';
export interface Notification { export interface Notification {
readonly id: NotificationId; readonly id: NotificationId;
@ -100,18 +102,26 @@ export interface SlackConversation {
export interface NotificationRule extends Omit<BaseData<NotificationRuleId>, 'label'>{ export interface NotificationRule extends Omit<BaseData<NotificationRuleId>, 'label'>{
tenantId: TenantId; tenantId: TenantId;
templateId: NotificationTemplateId; templateId: NotificationTemplateId;
// deliveryMethods: Array<NotificationDeliveryMethod>; triggerType: TriggerType;
configuration: NotificationRuleConfig; triggerConfig: NotificationRuleTriggerConfig;
recipientConfig: NotificationRuleRecipientConfig;
} }
export interface NotificationRuleConfig { export interface NotificationRuleTriggerConfig {
initialNotificationTargetId: NotificationTargetId; alarmTypes?: Array<string>;
escalationConfig: NotificationEscalationConfig; alarmSeverities?: Array<AlarmSeverity>;
description?: string; clearRule?: AlarmStatus;
devices?: Array<string>;
devicesProfiles?: Array<string>;
entityType?: EntityType;
created?: boolean;
updated?: boolean;
deleted?: boolean;
} }
export interface NotificationEscalationConfig { export interface NotificationRuleRecipientConfig {
escalations: Array<NonConfirmedNotificationEscalation>; targets?: Array<string>;
escalationTable?: {[key: string]: Array<string>};
} }
export interface NonConfirmedNotificationEscalation { export interface NonConfirmedNotificationEscalation {

View File

@ -2805,7 +2805,9 @@
"notify": "notify", "notify": "notify",
"no-rule": "No rule configured", "no-rule": "No rule configured",
"device": "Device", "device": "Device",
"devices": "Devices",
"device-profile": "Device profile", "device-profile": "Device profile",
"device-profiles": "Device profiles",
"filter-by": "Filter by", "filter-by": "Filter by",
"entity-type": "Entity type", "entity-type": "Entity type",
"created": "Created", "created": "Created",