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

View File

@ -17,22 +17,23 @@
-->
<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 fxFlex fxLayout="row">
<div fxFlex *ngIf="systemEscalation" style="padding: 0 10px;" translate>notification.first-recipient</div>
<div fxFlex *ngIf="!systemEscalation" fxLayout="row" style="align-items: center;">
<span style="padding: 0 10px;">After</span>
<div class="escalation" fxLayout="row" fxLayout.xs="column" fxLayoutAlign="center center" fxLayoutAlign.xs="start">
<div fxFlex fxLayout="row" ngStyle.xs="padding-top: 10px">
<div fxFlex [fxShow]="systemEscalation" class="escalation-padding" translate>notification.first-recipient</div>
<div fxFlex [fxShow]="!systemEscalation" fxLayout="row" fxLayout.xs="column" fxLayoutAlign="center center">
<span class="escalation-padding">After</span>
<tb-timeinterval
style="min-width: 100px;"
ngStyle.xs="padding: 0 10px;"
formControlName="delayInSec"
[disabledAdvanced]="true"
style="padding-top: 8px; width: 150px; min-width: 150px;"></tb-timeinterval>
<span fxFlex style="text-align: end; padding: 0 10px;" translate>notification.notify</span>
[disabledAdvanced]="true"></tb-timeinterval>
<span fxFlex class="escalation-notify" translate>notification.notify</span>
</div>
</div>
<div fxFlex style="padding: 0 10px;">
<div fxFlex class="escalation-padding">
<tb-entity-list
required
formControlName="notificationTargetId"
formControlName="targets"
[entityType]="entityType.NOTIFICATION_TARGET"
placeholderText="notification.add-target">
</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,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
Validator, Validators
Validator,
Validators
} from '@angular/forms';
import { UtilsService } from '@core/services/utils.service';
import { isDefinedAndNotNull } from '@core/utils';
import { Subscription } from 'rxjs';
import { Subject } from 'rxjs';
import { NonConfirmedNotificationEscalation } from '@shared/models/notification.models';
import { EntityType } from '@shared/models/entity-type.models';
import { takeUntil } from 'rxjs/operators';
@Component({
selector: 'tb-escalation-form',
templateUrl: './escalation-form.component.html',
styleUrls: [],
styleUrls: ['./escalation-form.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
@ -59,10 +61,10 @@ export class EscalationFormComponent implements ControlValueAccessor, OnInit, On
entityType = EntityType;
private modelValue: NonConfirmedNotificationEscalation;
private modelValue;
private propagateChange = null;
private propagateChangePending = false;
private valueChange$: Subscription = null;
private destroy$ = new Subject();
constructor(private utils: UtilsService,
private fb: FormBuilder) {
@ -84,19 +86,17 @@ export class EscalationFormComponent implements ControlValueAccessor, OnInit, On
ngOnInit() {
this.escalationFormGroup = this.fb.group(
{
delayInSec: [null],
notificationTargetId: [null, Validators.required],
delayInSec: [0],
targets: [null, Validators.required],
});
this.valueChange$ = this.escalationFormGroup.valueChanges.subscribe(() => {
this.updateModel();
});
this.escalationFormGroup.valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe(() => this.updateModel());
}
ngOnDestroy() {
if (this.valueChange$) {
this.valueChange$.unsubscribe();
this.valueChange$ = null;
}
this.destroy$.next();
this.destroy$.complete();
}
setDisabledState(isDisabled: boolean): void {

View File

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

View File

@ -15,224 +15,231 @@
limitations under the License.
-->
<mat-toolbar color="primary">
<h2>{{'notification.add-rule' | translate }}</h2>
<span fxFlex></span>
<button mat-icon-button
(click)="cancel()"
type="button">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
<div mat-dialog-content>
<mat-horizontal-stepper [linear]="true" labelPosition="end" #addNotificationRule [orientation]="(stepperOrientation | async)"
(selectionChange)="changeStep($event)">
<ng-template matStepperIcon="edit">
<mat-icon>check</mat-icon>
</ng-template>
<mat-step optional [stepControl]="ruleNotificationForm">
<ng-template matStepLabel>{{ 'notification.basic-settings' | translate }}</ng-template>
<form [formGroup]="ruleNotificationForm" style="padding-bottom: 16px;">
<mat-form-field class="mat-block">
<mat-label translate>notification.rule-name</mat-label>
<input matInput formControlName="name" required>
<mat-error *ngIf="ruleNotificationForm.get('name').hasError('required')">
{{ 'notification.rule-name-required' | translate }}
</mat-error>
</mat-form-field>
<tb-template-autocomplete
required
formControlName="templateId"
labelText="notification.template-name"
placeholderText="notification.template-name"
requiredText="notification.template-required">
</tb-template-autocomplete>
<mat-form-field class="mat-block">
<mat-label translate>notification.trigger.trigger</mat-label>
<mat-select formControlName="trigger" required>
<mat-option *ngFor="let trigger of triggerTypes" [value]="trigger">
{{ triggerTypeTranslationMap.get(trigger) | translate }}
</mat-option>
</mat-select>
<mat-error *ngIf="ruleNotificationForm.get('trigger').hasError('required')">
{{ 'notification.trigger.trigger-required' | translate }}
</mat-error>
</mat-form-field>
</form>
</mat-step>
<mat-step *ngIf="ruleNotificationForm.get('trigger').value === triggerType.ALARM"
[stepControl]="alarmTemplateForm">
<ng-template matStepLabel>{{ 'notification.type-settings' | translate }}</ng-template>
<form [formGroup]="alarmTemplateForm">
<fieldset class="fields-group">
<span class="fields-group-title" translate>notification.filter</span>
<mat-form-field fxFlex class="mat-block" floatLabel="always">
<mat-label translate>alarm.alarm-type-list</mat-label>
<mat-chip-list #alarmTypeChipList formControlName="alarmTypeList">
<mat-chip *ngFor="let type of alarmTypeList()" [selectable]="true"
[removable]="true" (removed)="removeAlarmType(type)">
{{type}}
<mat-icon matChipRemove>cancel</mat-icon>
</mat-chip>
<input placeholder="{{ !alarmTemplateForm.get('alarmTypeList').value?.length ? ('alarm.any-type' | translate) : '' }}"
[matChipInputFor]="alarmTypeChipList"
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
matChipInputAddOnBlur
(matChipInputTokenEnd)="addAlarmType($event)">
</mat-chip-list>
<section style="width: 800px; min-width: 100%; max-width: 100%">
<mat-toolbar color="primary">
<h2>{{'notification.add-rule' | translate }}</h2>
<span fxFlex></span>
<button mat-icon-button
(click)="cancel()"
type="button">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
<div mat-dialog-content ngStyle.xs="padding: 0;">
<mat-horizontal-stepper [linear]="true" labelPosition="end" #addNotificationRule [orientation]="(stepperOrientation | async)"
(selectionChange)="changeStep($event)">
<ng-template matStepperIcon="edit">
<mat-icon>check</mat-icon>
</ng-template>
<mat-step optional [stepControl]="ruleNotificationForm">
<ng-template matStepLabel>{{ 'notification.basic-settings' | translate }}</ng-template>
<form [formGroup]="ruleNotificationForm" style="padding-bottom: 16px;">
<mat-form-field class="mat-block">
<mat-label translate>notification.rule-name</mat-label>
<input matInput formControlName="name" required>
<mat-error *ngIf="ruleNotificationForm.get('name').hasError('required')">
{{ 'notification.rule-name-required' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>alarm.alarm-severity-list</mat-label>
<mat-chip-list #severitiesChipList
required>
<mat-chip *ngFor="let severity of alarmTemplateForm.get('alarmSeverityList').value"
[removable]="true" (removed)="onSeverityRemoved(severity)">
{{ alarmSeverityTranslationMap.get(severity) | translate }}
<mat-icon matChipRemove>cancel</mat-icon>
</mat-chip>
<input matInput
type="text"
placeholder="{{ !alarmTemplateForm.get('alarmSeverityList').value?.length ? ('alarm.any-severity' | translate) : '' }}"
style="max-width: 200px;"
#severityInput
(focusin)="onSeverityInputFocus()"
matAutocompleteOrigin
#origin="matAutocompleteOrigin"
(input)="severityInputChange.next(severityInput.value)"
[matAutocompleteConnectedTo]="origin"
[matAutocomplete]="severityAutocomplete"
[matChipInputFor]="severitiesChipList"
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
(matChipInputTokenEnd)="addSeverityFromChipInput($event)">
</mat-chip-list>
<mat-autocomplete #severityAutocomplete="matAutocomplete"
class="tb-autocomplete"
(optionSelected)="severitySelected($event)"
[displayWith]="displaySeverityFn.bind(this)">
<mat-option *ngFor="let severity of filteredDisplaySeverities | async" [value]="severity">
<span [innerHTML]="alarmSeverityTranslationMap.get(alarmSeverityEnum[severity]) | translate | highlight:severitySearchText"></span>
</mat-option>
<mat-option *ngIf="(filteredDisplaySeverities | async)?.length === 0" [value]="null" class="tb-not-found">
<div class="tb-not-found-content" (click)="$event.stopPropagation()">
<div *ngIf="!textIsNotEmpty(severitySearchText); else searchNotEmpty">
<span translate>notification.no-severity-found</span>
</div>
<ng-template #searchNotEmpty>
<span>
{{ translate.get('notification.no-severity-matching',
{severity: truncate.transform(severitySearchText, true, 6, &apos;...&apos;)}) | async }}
</span>
</ng-template>
</div>
</mat-option>
</mat-autocomplete>
</mat-form-field>
</fieldset>
<fieldset class="fields-group">
<span class="fields-group-title" translate>notification.clear-rule</span>
<mat-form-field fxFlex class="mat-block" floatLabel="always">
<mat-label translate>alarm.alarm-status-list</mat-label>
<mat-select formControlName="alarmStatusList"
placeholder="{{ !alarmTemplateForm.get('alarmStatusList').value?.length ? ('alarm.any-status' | translate) : '' }}">
<mat-option *ngFor="let searchStatus of alarmSearchStatuses" [value]="searchStatus">
{{ alarmSearchStatusTranslationMap.get(searchStatus) | translate }}
<tb-template-autocomplete
required
formControlName="templateId"
labelText="notification.template-name"
placeholderText="notification.template-name"
requiredText="notification.template-required">
</tb-template-autocomplete>
<mat-form-field class="mat-block">
<mat-label translate>notification.trigger.trigger</mat-label>
<mat-select formControlName="triggerType" required>
<mat-option *ngFor="let trigger of triggerTypes" [value]="trigger">
{{ triggerTypeTranslationMap.get(trigger) | translate }}
</mat-option>
</mat-select>
<mat-error *ngIf="ruleNotificationForm.get('triggerType').hasError('required')">
{{ 'notification.trigger.trigger-required' | translate }}
</mat-error>
</mat-form-field>
</fieldset>
</form>
</mat-step>
<mat-step optional [stepControl]="alarmTemplateForm"
*ngIf="ruleNotificationForm.get('triggerType').value === triggerType.ALARM">
<ng-template matStepLabel>{{ 'notification.type-settings' | translate }}</ng-template>
<form [formGroup]="alarmTemplateForm">
<fieldset class="fields-group">
<span class="fields-group-title" translate>notification.filter</span>
<mat-form-field fxFlex class="mat-block" floatLabel="always">
<mat-label translate>alarm.alarm-type-list</mat-label>
<mat-chip-list #alarmTypeChipList formControlName="alarmTypes">
<mat-chip *ngFor="let type of alarmTypeList()" [selectable]="true"
[removable]="true" (removed)="removeAlarmType(type)">
{{type}}
<mat-icon matChipRemove>cancel</mat-icon>
</mat-chip>
<input placeholder="{{ !alarmTemplateForm.get('alarmTypes').value?.length ? ('alarm.any-type' | translate) : '' }}"
[matChipInputFor]="alarmTypeChipList"
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
matChipInputAddOnBlur
(matChipInputTokenEnd)="addAlarmType($event)">
</mat-chip-list>
</mat-form-field>
<fieldset class="fields-group">
<span class="fields-group-title" translate>notification.hierarchy-of-receiving</span>
<tb-escalations-component formControlName="escalationConfig"></tb-escalations-component>
</fieldset>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>alarm.alarm-severity-list</mat-label>
<mat-chip-list #severitiesChipList formControlName="alarmSeverities"
required>
<mat-chip *ngFor="let severity of alarmTemplateForm.get('alarmSeverities').value"
[removable]="true" (removed)="onSeverityRemoved(severity)">
{{ alarmSeverityTranslationMap.get(severity) | translate }}
<mat-icon matChipRemove>cancel</mat-icon>
</mat-chip>
<input matInput
type="text"
placeholder="{{ !alarmTemplateForm.get('alarmSeverities').value?.length ? ('alarm.any-severity' | translate) : '' }}"
style="max-width: 200px;"
#severityInput
(focusin)="onSeverityInputFocus()"
matAutocompleteOrigin
#origin="matAutocompleteOrigin"
(input)="severityInputChange.next(severityInput.value)"
[matAutocompleteConnectedTo]="origin"
[matAutocomplete]="severityAutocomplete"
[matChipInputFor]="severitiesChipList"
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
(matChipInputTokenEnd)="addSeverityFromChipInput($event)">
</mat-chip-list>
<mat-autocomplete #severityAutocomplete="matAutocomplete"
class="tb-autocomplete"
(optionSelected)="severitySelected($event)"
[displayWith]="displaySeverityFn.bind(this)">
<mat-option *ngFor="let severity of filteredDisplaySeverities | async" [value]="severity">
<span [innerHTML]="alarmSeverityTranslationMap.get(alarmSeverityEnum[severity]) | translate | highlight:severitySearchText"></span>
</mat-option>
<mat-option *ngIf="(filteredDisplaySeverities | async)?.length === 0" [value]="null" class="tb-not-found">
<div class="tb-not-found-content" (click)="$event.stopPropagation()">
<div *ngIf="!textIsNotEmpty(severitySearchText); else searchNotEmpty">
<span translate>notification.no-severity-found</span>
</div>
<ng-template #searchNotEmpty>
<span>
{{ translate.get('notification.no-severity-matching',
{severity: truncate.transform(severitySearchText, true, 6, &apos;...&apos;)}) | async }}
</span>
</ng-template>
</div>
</mat-option>
</mat-autocomplete>
</mat-form-field>
</fieldset>
<mat-form-field class="mat-block">
<mat-label translate>notification.description</mat-label>
<input matInput formControlName="description">
</mat-form-field>
</form>
</mat-step>
<fieldset class="fields-group" formGroupName="clearRule">
<span class="fields-group-title" translate>notification.clear-rule</span>
<mat-form-field fxFlex class="mat-block" floatLabel="always">
<mat-label translate>alarm.alarm-status-list</mat-label>
<mat-select formControlName="alarmStatus"
placeholder="{{ !alarmTemplateForm.get('clearRule.alarmStatus').value?.length ? ('alarm.any-status' | translate) : '' }}">
<mat-option *ngFor="let searchStatus of alarmSearchStatuses" [value]="searchStatus">
{{ alarmSearchStatusTranslationMap.get(searchStatus) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</fieldset>
<mat-step optional *ngIf="ruleNotificationForm.get('trigger').value === triggerType.DEVICE_INACTIVITY"
[stepControl]="deviceInactivityTemplateForm">
<ng-template matStepLabel>{{ 'notification.type-settings' | translate }}</ng-template>
<form [formGroup]="deviceInactivityTemplateForm">
<span translate>notification.filter-by</span>
<div fxFlex fxLayoutAlign="center center">
<mat-button-toggle-group class="tb-notification-unread-toggle-group"
style="width: 250px;"
formControlName="filterByDevice">
<mat-button-toggle fxFlex [value]=true>{{ 'notification.device' | translate }}</mat-button-toggle>
<mat-button-toggle fxFlex [value]=false>{{ 'notification.device-profile' | translate }}</mat-button-toggle>
</mat-button-toggle-group>
</div>
<tb-device-profile-autocomplete
*ngIf="!deviceInactivityTemplateForm.get('filterByDevice').value"
formControlName="deviceProfileId"
[editProfileEnabled]="false">
</tb-device-profile-autocomplete>
<tb-entity-autocomplete
*ngIf="deviceInactivityTemplateForm.get('filterByDevice').value"
formControlName="deviceId"
[entityType]="entityType.DEVICE">
</tb-entity-autocomplete>
<tb-entity-list
required
formControlName="notificationTargetId"
[entityType]="entityType.NOTIFICATION_TARGET"
placeholderText="notification.target">
</tb-entity-list>
<mat-form-field class="mat-block">
<mat-label translate>notification.description</mat-label>
<input matInput formControlName="description">
</mat-form-field>
</form>
</mat-step>
<fieldset class="fields-group">
<span class="fields-group-title" translate>notification.hierarchy-of-receiving</span>
<tb-escalations-component formControlName="escalationTable"></tb-escalations-component>
</fieldset>
<mat-step optional *ngIf="ruleNotificationForm.get('trigger').value === triggerType.ENTITY_ACTION"
[stepControl]="entityActionTemplateForm">
<ng-template matStepLabel>{{ 'notification.type-settings' | translate }}</ng-template>
<form [formGroup]="entityActionTemplateForm">
<fieldset class="fields-group">
<span class="fields-group-title" translate>notification.filter</span>
<tb-entity-type-select required
showLabel
[allowedEntityTypes]="entityTypes"
formControlName="entityType">
</tb-entity-type-select>
<section formGroupName="status" fxLayout="column" fxLayoutGap="10px">
<span fxFlex translate>notification.status</span>
<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="deleted" translate>{{ 'notification.deleted' | translate }}</mat-checkbox>
</section>
</fieldset>
<tb-entity-list
required
formControlName="notificationTargetId"
[entityType]="entityType.NOTIFICATION_TARGET"
placeholderText="notification.target">
</tb-entity-list>
<mat-form-field class="mat-block">
<mat-label translate>notification.description</mat-label>
<input matInput formControlName="description">
</mat-form-field>
</form>
</mat-step>
</mat-horizontal-stepper>
</div>
<mat-divider></mat-divider>
<div mat-dialog-actions fxLayout="row">
<button mat-stroked-button *ngIf="selectedIndex > 0"
(click)="backStep()">{{ 'action.back' | translate }}</button>
<span fxFlex></span>
<button mat-raised-button
color="primary"
(click)="nextStep()">{{ nextStepLabel() | translate }}</button>
</div>
<mat-form-field class="mat-block">
<mat-label translate>notification.description</mat-label>
<input matInput formControlName="description">
</mat-form-field>
</form>
</mat-step>
<mat-step optional *ngIf="ruleNotificationForm.get('triggerType').value === triggerType.DEVICE_INACTIVITY"
[stepControl]="deviceInactivityTemplateForm">
<ng-template matStepLabel>{{ 'notification.type-settings' | translate }}</ng-template>
<form [formGroup]="deviceInactivityTemplateForm">
<span translate>notification.filter-by</span>
<div fxFlex fxLayoutAlign="center center">
<mat-button-toggle-group class="tb-notification-unread-toggle-group"
style="width: 250px;"
formControlName="filterByDevice">
<mat-button-toggle fxFlex [value]=true>{{ 'notification.device' | translate }}</mat-button-toggle>
<mat-button-toggle fxFlex [value]=false>{{ 'notification.device-profile' | translate }}</mat-button-toggle>
</mat-button-toggle-group>
</div>
<tb-entity-list
*ngIf="deviceInactivityTemplateForm.get('filterByDevice').value"
required
formControlName="devices"
[labelText]="translate.instant('notification.devices')"
[placeholderText]="translate.instant('notification.device')"
[entityType]="entityType.DEVICE">
</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
required
formControlName="targets"
[entityType]="entityType.NOTIFICATION_TARGET"
placeholderText="notification.target">
</tb-entity-list>
<mat-form-field class="mat-block">
<mat-label translate>notification.description</mat-label>
<input matInput formControlName="description">
</mat-form-field>
</form>
</mat-step>
<mat-step optional *ngIf="ruleNotificationForm.get('triggerType').value === triggerType.ENTITY_ACTION"
[stepControl]="entityActionTemplateForm">
<ng-template matStepLabel>{{ 'notification.type-settings' | translate }}</ng-template>
<form [formGroup]="entityActionTemplateForm">
<fieldset class="fields-group">
<span class="fields-group-title" translate>notification.filter</span>
<tb-entity-type-select required
showLabel
[allowedEntityTypes]="entityTypes"
formControlName="entityType">
</tb-entity-type-select>
<section fxLayout="column" fxLayoutGap="10px">
<span fxFlex translate>notification.status</span>
<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="deleted" translate>{{ 'notification.deleted' | translate }}</mat-checkbox>
</section>
</fieldset>
<tb-entity-list
required
formControlName="targets"
[entityType]="entityType.NOTIFICATION_TARGET"
placeholderText="notification.target">
</tb-entity-list>
<mat-form-field class="mat-block">
<mat-label translate>notification.description</mat-label>
<input matInput formControlName="description">
</mat-form-field>
</form>
</mat-step>
</mat-horizontal-stepper>
</div>
<mat-divider></mat-divider>
<div mat-dialog-actions fxLayout="row">
<button mat-stroked-button *ngIf="selectedIndex > 0"
(click)="backStep()">{{ 'action.back' | translate }}</button>
<span fxFlex></span>
<button mat-raised-button
color="primary"
(click)="nextStep()">{{ nextStepLabel() | translate }}</button>
</div>
</section>

View File

@ -17,9 +17,9 @@
:host {
::ng-deep{
width: 100%;
min-width: 800px !important;
max-width: 100%;
//width: 100%;
//min-width: 800px;
//max-width: 100%;
.mat-button-toggle-group.tb-notification-unread-toggle-group {
&.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 { FormBuilder, FormGroup, Validators } from '@angular/forms';
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 { 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 { MatStepper } from '@angular/material/stepper';
import { MediaBreakpoints } from '@shared/models/constants';
@ -34,12 +34,11 @@ 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
alarmSeverityTranslations,
AlarmStatus,
alarmStatusTranslations
} 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';
@ -80,11 +79,8 @@ export class RuleNotificationDialogComponent extends
alarmSeverityEnum = AlarmSeverity;
alarmSeverityTranslationMap = alarmSeverityTranslations;
alarmSearchStatuses = [AlarmSearchStatus.ACTIVE,
AlarmSearchStatus.CLEARED,
AlarmSearchStatus.ACK,
AlarmSearchStatus.UNACK];
alarmSearchStatusTranslationMap = alarmSearchStatusTranslations;
alarmSearchStatuses = Object.values(AlarmStatus);
alarmSearchStatusTranslationMap = alarmStatusTranslations;
entityType = EntityType;
entityTypes = Object.values(EntityType);
@ -97,7 +93,7 @@ export class RuleNotificationDialogComponent extends
severityInputChange = new Subject<string>();
private readonly destroy$ = new Subject<void>();
private destroy$ = new Subject();
constructor(protected store: Store<AppState>,
protected router: Router,
@ -120,39 +116,54 @@ export class RuleNotificationDialogComponent extends
this.ruleNotificationForm = this.fb.group({
name: [null, Validators.required],
templateId: [null, Validators.required],
trigger: [this.triggerType.ALARM, Validators.required],
configuration: this.fb.group({
escalationConfig: this.fb.group({
escalations: [[]]
}),
description: [null]
triggerType: [null, Validators.required],
recipientsConfig: this.fb.group({
triggerType: [],
}),
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({
alarmTypeList: [[], Validators.required],
alarmSeverityList: [[], Validators.required],
alarmStatusList: [[], Validators.required],
escalationConfig: [],
alarmTypes: [[], Validators.required],
alarmSeverities: [[], Validators.required],
clearRule: this.fb.group({
alarmStatus: []
}),
escalationTable: [],
description: ['']
});
this.deviceInactivityTemplateForm = this.fb.group({
filterByDevice: [true],
deviceId: [],
deviceProfileId: [],
notificationTargetId: [],
devices: [],
deviceProfiles: [],
targets: [[], Validators.required],
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({
entityType: [],
status: this.fb.group({
created: [false],
updated: [false],
deleted: [false]
}),
notificationTargetId: [],
created: [false],
updated: [false],
deleted: [false],
targets: [[], Validators.required],
description: ['']
});
@ -166,12 +177,12 @@ export class RuleNotificationDialogComponent extends
}
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);
if (index > -1) {
severities.splice(index, 1);
this.alarmTemplateForm.get('alarmSeverityList').setValue(severities);
this.alarmTemplateForm.get('alarmSeverityList').markAsDirty();
this.alarmTemplateForm.get('alarmSeverities').setValue(severities);
this.alarmTemplateForm.get('alarmSeverities').markAsDirty();
this.severitiesChipList.errorState = !severities.length;
}
}
@ -193,12 +204,12 @@ export class RuleNotificationDialogComponent extends
private addSeverity(existingSeverity: string): boolean {
if (existingSeverity) {
const displaySeverities: string[] = this.alarmTemplateForm.get('alarmSeverityList').value;
const displaySeverities: string[] = this.alarmTemplateForm.get('alarmSeverities').value;
const index = displaySeverities.indexOf(existingSeverity);
if (index === -1) {
displaySeverities.push(existingSeverity);
this.alarmTemplateForm.get('alarmSeverityList').setValue(displaySeverities);
this.alarmTemplateForm.get('alarmSeverityList').markAsDirty();
this.alarmTemplateForm.get('alarmSeverities').setValue(displaySeverities);
this.alarmTemplateForm.get('alarmSeverities').markAsDirty();
this.severitiesChipList.errorState = false;
return true;
}
@ -239,16 +250,16 @@ export class RuleNotificationDialogComponent extends
}
public alarmTypeList(): string[] {
return this.alarmTemplateForm.get('alarmTypeList').value;
return this.alarmTemplateForm.get('alarmTypes').value;
}
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);
if (index >= 0) {
types.splice(index, 1);
this.alarmTemplateForm.get('alarmTypeList').setValue(types);
this.alarmTemplateForm.get('alarmTypeList').markAsDirty();
this.alarmTemplateForm.get('alarmTypes').setValue(types);
this.alarmTemplateForm.get('alarmTypes').markAsDirty();
}
}
@ -256,12 +267,12 @@ export class RuleNotificationDialogComponent extends
const input = event.input;
const value = event.value;
const types: string[] = this.alarmTemplateForm.get('alarmTypeList').value;
const types: string[] = this.alarmTemplateForm.get('alarmTypes').value;
if ((value || '').trim()) {
types.push(value.trim());
this.alarmTemplateForm.get('alarmTypeList').setValue(types);
this.alarmTemplateForm.get('alarmTypeList').markAsDirty();
this.alarmTemplateForm.get('alarmTypes').setValue(types);
this.alarmTemplateForm.get('alarmTypes').markAsDirty();
}
if (input) {
@ -307,22 +318,26 @@ export class RuleNotificationDialogComponent extends
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;
// }
const 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;
delete formValue.triggerConfig.escalationTable;
} else if (triggerType === TriggerType.DEVICE_INACTIVITY) {
Object.assign(formValue.triggerConfig, this.deviceInactivityTemplateForm.value);
delete formValue.triggerConfig.filterByDevice;
} else {
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(
(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 { EntityId } from '@shared/models/id/entity-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 {
readonly id: NotificationId;
@ -100,18 +102,26 @@ export interface SlackConversation {
export interface NotificationRule extends Omit<BaseData<NotificationRuleId>, 'label'>{
tenantId: TenantId;
templateId: NotificationTemplateId;
// deliveryMethods: Array<NotificationDeliveryMethod>;
configuration: NotificationRuleConfig;
triggerType: TriggerType;
triggerConfig: NotificationRuleTriggerConfig;
recipientConfig: NotificationRuleRecipientConfig;
}
export interface NotificationRuleConfig {
initialNotificationTargetId: NotificationTargetId;
escalationConfig: NotificationEscalationConfig;
description?: string;
export interface NotificationRuleTriggerConfig {
alarmTypes?: Array<string>;
alarmSeverities?: Array<AlarmSeverity>;
clearRule?: AlarmStatus;
devices?: Array<string>;
devicesProfiles?: Array<string>;
entityType?: EntityType;
created?: boolean;
updated?: boolean;
deleted?: boolean;
}
export interface NotificationEscalationConfig {
escalations: Array<NonConfirmedNotificationEscalation>;
export interface NotificationRuleRecipientConfig {
targets?: Array<string>;
escalationTable?: {[key: string]: Array<string>};
}
export interface NonConfirmedNotificationEscalation {

View File

@ -2805,7 +2805,9 @@
"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",