Browse Source

Merge pull request #8022 from ArtemDzhereleiko/AD/notification/rule

Notification rule
pull/8046/head
Vladyslav 4 years ago
committed by GitHub
parent
commit
d8860efe6a
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      ui-ngx/src/app/modules/home/pages/notification-center/inbox-table/inbox-table-header.component.html
  2. 6
      ui-ngx/src/app/modules/home/pages/notification-center/notification-center.component.html
  3. 10
      ui-ngx/src/app/modules/home/pages/notification-center/notification-center.module.ts
  4. 7
      ui-ngx/src/app/modules/home/pages/notification-center/notification-table/notification-table.component.ts
  5. 126
      ui-ngx/src/app/modules/home/pages/notification-center/notification-table/rule-table-config.ts
  6. 42
      ui-ngx/src/app/modules/home/pages/notification-center/rule-table/escalation-form.component.html
  7. 34
      ui-ngx/src/app/modules/home/pages/notification-center/rule-table/escalation-form.component.scss
  8. 140
      ui-ngx/src/app/modules/home/pages/notification-center/rule-table/escalation-form.component.ts
  9. 45
      ui-ngx/src/app/modules/home/pages/notification-center/rule-table/escalations.component.html
  10. 172
      ui-ngx/src/app/modules/home/pages/notification-center/rule-table/escalations.component.ts
  11. 245
      ui-ngx/src/app/modules/home/pages/notification-center/rule-table/rule-notification-dialog.component.html
  12. 85
      ui-ngx/src/app/modules/home/pages/notification-center/rule-table/rule-notification-dialog.component.scss
  13. 436
      ui-ngx/src/app/modules/home/pages/notification-center/rule-table/rule-notification-dialog.component.ts
  14. 24
      ui-ngx/src/app/modules/home/pages/notification-center/rule-table/rule-table-header.component.html
  15. 18
      ui-ngx/src/app/modules/home/pages/notification-center/rule-table/rule-table-header.component.scss
  16. 38
      ui-ngx/src/app/modules/home/pages/notification-center/rule-table/rule-table-header.component.ts
  17. 2
      ui-ngx/src/app/shared/components/time/timeinterval.component.html
  18. 2
      ui-ngx/src/app/shared/components/time/timeinterval.component.ts
  19. 37
      ui-ngx/src/app/shared/models/entity-type.models.ts
  20. 38
      ui-ngx/src/app/shared/models/notification.models.ts
  21. 51
      ui-ngx/src/assets/locale/locale.constant-en_US.json

2
ui-ngx/src/app/modules/home/pages/notification-center/inbox-table/inbox-table-header.component.html

@ -20,7 +20,7 @@
style="width: 180px;" style="width: 180px;"
[ngModel]="entitiesTableConfig.componentsData.unreadOnly" [ngModel]="entitiesTableConfig.componentsData.unreadOnly"
(ngModelChange)="changeUnreadOnly($event)"> (ngModelChange)="changeUnreadOnly($event)">
<mat-button-toggle fxFlex [value]=true>Unread</mat-button-toggle> <mat-button-toggle fxFlex [value]=true>{{ 'notification.unread' | translate }}</mat-button-toggle>
<mat-button-toggle fxFlex [value]=false>{{ 'notification.all' | translate }}</mat-button-toggle> <mat-button-toggle fxFlex [value]=false>{{ 'notification.all' | translate }}</mat-button-toggle>
</mat-button-toggle-group> </mat-button-toggle-group>

6
ui-ngx/src/app/modules/home/pages/notification-center/notification-center.component.html

@ -41,9 +41,9 @@
</tb-notification-table> </tb-notification-table>
</mat-tab> </mat-tab>
<mat-tab label="{{ 'notification.rules' | translate }}"> <mat-tab label="{{ 'notification.rules' | translate }}">
<ng-template matTabContent> <tb-notification-table
Content 1 - Loaded [notificationType]="entityType.NOTIFICATION_RULE">
</ng-template> </tb-notification-table>
</mat-tab> </mat-tab>
<mat-tab disabled> <mat-tab disabled>
<ng-template mat-tab-label> <ng-template mat-tab-label>

10
ui-ngx/src/app/modules/home/pages/notification-center/notification-center.module.ts

@ -42,6 +42,10 @@ import {
TemplateAutocompleteComponent TemplateAutocompleteComponent
} from '@home/pages/notification-center/template-table/template-autocomplete.component'; } from '@home/pages/notification-center/template-table/template-autocomplete.component';
import { InboxTableHeaderComponent } from '@home/pages/notification-center/inbox-table/inbox-table-header.component'; 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({ @NgModule({
declarations: [ declarations: [
@ -53,7 +57,11 @@ import { InboxTableHeaderComponent } from '@home/pages/notification-center/inbox
TemplateTableHeaderComponent, TemplateTableHeaderComponent,
NotificationTableComponent, NotificationTableComponent,
TemplateAutocompleteComponent, TemplateAutocompleteComponent,
InboxTableHeaderComponent InboxTableHeaderComponent,
RuleTableHeaderComponent,
RuleNotificationDialogComponent,
EscalationsComponent,
EscalationFormComponent
], ],
imports: [ imports: [
CommonModule, CommonModule,

7
ui-ngx/src/app/modules/home/pages/notification-center/notification-table/notification-table.component.ts

@ -26,6 +26,7 @@ import { InboxTableConfig } from '@home/pages/notification-center/notification-t
import { DatePipe } from '@angular/common'; import { DatePipe } from '@angular/common';
import { TemplateTableConfig } from '@home/pages/notification-center/notification-table/template-table-config'; import { TemplateTableConfig } from '@home/pages/notification-center/notification-table/template-table-config';
import { RequestTableConfig } from '@home/pages/notification-center/notification-table/request-table-config'; import { RequestTableConfig } from '@home/pages/notification-center/notification-table/request-table-config';
import { RuleTableConfig } from '@home/pages/notification-center/notification-table/rule-table-config';
@Component({ @Component({
selector: 'tb-notification-table', selector: 'tb-notification-table',
@ -85,6 +86,12 @@ export class NotificationTableComponent implements OnInit {
this.dialog, this.dialog,
this.datePipe this.datePipe
); );
case EntityType.NOTIFICATION_RULE:
return new RuleTableConfig(
this.notificationService,
this.translate,
this.dialog
);
} }
} }
} }

126
ui-ngx/src/app/modules/home/pages/notification-center/notification-table/rule-table-config.ts

@ -0,0 +1,126 @@
///
/// 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 {
CellActionDescriptor,
EntityTableColumn,
EntityTableConfig
} from '@home/models/entity/entities-table-config.models';
import { EntityTypeResource } from '@shared/models/entity-type.models';
import { Direction } from '@shared/models/page/sort-order';
import {
NotificationRule,
NotificationTarget,
NotificationTargetConfigTypeTranslateMap, TriggerTypeTranslationMap
} from '@shared/models/notification.models';
import { NotificationService } from '@core/http/notification.service';
import { TranslateService } from '@ngx-translate/core';
import {
TargetNotificationDialogComponent,
TargetsNotificationDialogData
} from '@home/pages/notification-center/targets-table/target-notification-dialog.componet';
import { MatDialog } from '@angular/material/dialog';
import { EntityAction } from '@home/models/entity/entity-component.models';
import {
TargetTableHeaderComponent
} from '@home/pages/notification-center/targets-table/target-table-header.component';
import { RuleTableHeaderComponent } from '@home/pages/notification-center/rule-table/rule-table-header.component';
import {
RuleNotificationDialogComponent,
RuleNotificationDialogData
} from '@home/pages/notification-center/rule-table/rule-notification-dialog.component';
export class RuleTableConfig extends EntityTableConfig<NotificationRule> {
constructor(private notificationService: NotificationService,
private translate: TranslateService,
private dialog: MatDialog) {
super();
this.loadDataOnInit = false;
this.entityTranslations = {
noEntities: 'notification.no-rules-notification',
search: 'notification.search-rules'
};
this.entityResources = {} as EntityTypeResource<NotificationRule>;
this.entitiesFetchFunction = pageLink => this.notificationService.getNotificationRules(pageLink);
this.deleteEntityTitle = rule => this.translate.instant('notification.delete-rule-title', {ruleName: rule.name});
this.deleteEntityContent = () => this.translate.instant('notification.delete-rule-text');
this.deleteEntity = id => this.notificationService.deleteNotificationRule(id.id);
this.cellActionDescriptors = this.configureCellActions();
this.headerComponent = RuleTableHeaderComponent;
this.onEntityAction = action => this.onTargetAction(action);
this.defaultSortOrder = {property: 'name', direction: Direction.ASC};
this.columns.push(
new EntityTableColumn<NotificationRule>('name', 'notification.rule-name', '30%'),
new EntityTableColumn<NotificationRule>('templateName', 'notification.template', '20%'),
new EntityTableColumn<NotificationRule>('triggerType', 'notification.trigger.trigger', '20%',
(rule) => this.translate.instant(TriggerTypeTranslationMap.get(rule.triggerType)) || '',
() => ({}), true),
new EntityTableColumn<NotificationRule>('additionalConfig.description', 'notification.description', '30%',
(target) => target.additionalConfig.description || '',
() => ({}), false)
);
}
private configureCellActions(): Array<CellActionDescriptor<NotificationRule>> {
return [{
name: this.translate.instant('notification.copy-rule'),
icon: 'content_copy',
isEnabled: () => true,
onAction: ($event, entity) => this.editRule($event, entity, false, true)
}, {
name: this.translate.instant('notification.edit-rule'),
icon: 'edit',
isEnabled: () => true,
onAction: ($event, entity) => this.editRule($event, entity)
}];
}
private editRule($event: Event, rule: NotificationRule, isAdd = false, isCopy = false) {
if ($event) {
$event.stopPropagation();
}
this.dialog.open<RuleNotificationDialogComponent, RuleNotificationDialogData,
NotificationRule>(RuleNotificationDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
isAdd,
isCopy,
rule
}
}).afterClosed()
.subscribe((res) => {
if (res) {
this.updateData();
}
});
}
private onTargetAction(action: EntityAction<NotificationRule>): boolean {
switch (action.action) {
case 'add':
this.editRule(action.event, action.entity, true);
return true;
}
return false;
}
}

42
ui-ngx/src/app/modules/home/pages/notification-center/rule-table/escalation-form.component.html

@ -0,0 +1,42 @@
<!--
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.
-->
<form [formGroup]="escalationFormGroup" fxLayout="column">
<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 *ngIf="systemEscalation" class="escalation-padding" translate>notification.first-recipient</div>
<div fxFlex *ngIf="!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"></tb-timeinterval>
<span fxFlex class="escalation-notify" translate>notification.notify</span>
</div>
</div>
<div fxFlex class="escalation-padding">
<tb-entity-list
required
formControlName="targets"
[entityType]="entityType.NOTIFICATION_TARGET"
placeholderText="notification.add-target">
</tb-entity-list>
</div>
</div>
</form>

34
ui-ngx/src/app/modules/home/pages/notification-center/rule-table/escalation-form.component.scss

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

140
ui-ngx/src/app/modules/home/pages/notification-center/rule-table/escalation-form.component.ts

@ -0,0 +1,140 @@
///
/// 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 { 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: ['./escalation-form.component.scss'],
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;
private propagateChange = null;
private propagateChangePending = false;
private destroy$ = new Subject();
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: [0],
targets: [null, Validators.required],
});
this.escalationFormGroup.valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe(() => this.updateModel());
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
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;
this.modelValue.delayInSec = +value.delayInSec;
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;
}
}
}

45
ui-ngx/src/app/modules/home/pages/notification-center/rule-table/escalations.component.html

@ -0,0 +1,45 @@
<!--
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.
-->
<div fxLayout="column">
<div class="tb-tenant-profile-queues"
fxLayout="row" style="align-items: center"
*ngFor="let escalationControl of escalationsFormArray.controls;
let $index = index; last as isLast;"
[ngStyle]="!isLast ? {paddingBottom: '8px'} : {}">
<tb-escalation-form fxFlex [formControl]="escalationControl" [systemEscalation]="$index === 0"></tb-escalation-form>
<span *ngIf="$index === 0" style="width: 40px;"></span>
<button *ngIf="!($index === 0) && !disabled" mat-icon-button style="min-width: 40px;"
type="button"
(click)="removeEscalation($index)"
matTooltip="{{ 'action.remove' | translate }}"
matTooltipPosition="above">
<mat-icon>close</mat-icon>
</button>
</div>
<div *ngIf="!escalationsFormArray.controls.length">
<span translate fxLayoutAlign="center center"
class="tb-prompt">notification.no-rule</span>
</div>
<div *ngIf="!disabled" style="padding-top: 16px;">
<button mat-raised-button color="primary"
type="button"
(click)="addEscalation()">
<span translate>notification.add-rule</span>
</button>
</div>
</div>

172
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 { Subject } from 'rxjs';
import { UtilsService } from '@core/services/utils.service';
import { NonConfirmedNotificationEscalation } from '@shared/models/notification.models';
import { takeUntil } from 'rxjs/operators';
@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: 0,
targets: null
};
private destroy$ = new Subject();
private propagateChange = (v: any) => { };
constructor(private store: Store<AppState>,
private utils: UtilsService,
private fb: FormBuilder) {
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
registerOnTouched(fn: any): void {
}
ngOnInit() {
this.escalationsFormGroup = this.fb.group({
escalations: this.fb.array([])
});
this.escalationsFormGroup.valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe(() => this.updateModel());
}
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<NonConfirmedNotificationEscalation> | null): void {
if (escalations?.length === this.escalationsFormArray.length) {
this.escalationsFormArray.patchValue(escalations, {emitEvent: false});
} else {
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});
}
}
}
public removeEscalation(index: number) {
(this.escalationsFormGroup.get('escalations') as FormArray).removeAt(index);
}
public addEscalation() {
const escalation = {
delayInSec: 0,
targets: 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 = this.escalationsFormGroup.get('escalations').value;
this.propagateChange(escalations);
}
}

245
ui-ngx/src/app/modules/home/pages/notification-center/rule-table/rule-notification-dialog.component.html

@ -0,0 +1,245 @@
<!--
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.
-->
<section style="width: 800px; min-width: 100%; max-width: 100%">
<mat-toolbar color="primary">
<h2>{{ dialogTitle | 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>
<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>
</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>
<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>
<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>
<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-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]="'notification.devices'"
[placeholderText]="'notification.device'"
[entityType]="entityType.DEVICE">
</tb-entity-list>
<tb-entity-list
*ngIf="!deviceInactivityTemplateForm.get('filterByDevice').value"
required
formControlName="deviceProfiles"
[labelText]="'notification.device-profiles'"
[placeholderText]="'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>

85
ui-ngx/src/app/modules/home/pages/notification-center/rule-table/rule-notification-dialog.component.scss

@ -0,0 +1,85 @@
/**
* 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 "../../../../../../theme";
:host {
::ng-deep{
.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;
border: 1px groove rgba(0, 0, 0, .25);
border-radius: 4px;
.fields-group-title {
display: inline-block;
font-weight: 500;
padding: 12px 0;
}
}
}

436
ui-ngx/src/app/modules/home/pages/notification-center/rule-table/rule-notification-dialog.component.ts

@ -0,0 +1,436 @@
///
/// 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 { 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';
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 { deepClone, deepTrim, isDefined } from '@core/utils';
import { Observable, of, Subject } from 'rxjs';
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';
import { BreakpointObserver } from '@angular/cdk/layout';
import { MatChipInputEvent, MatChipList } from '@angular/material/chips';
import { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes';
import {
AlarmSeverity,
alarmSeverityTranslations,
AlarmStatus,
alarmStatusTranslations
} from '@shared/models/alarm.models';
import { MatAutocomplete, MatAutocompleteSelectedEvent } from '@angular/material/autocomplete';
import { TranslateService } from '@ngx-translate/core';
import { TruncatePipe } from '@shared/pipe/truncate.pipe';
export interface RuleNotificationDialogData {
rule?: NotificationRule;
isAdd?: boolean;
isCopy?: boolean;
}
@Component({
selector: 'tb-rule-notification-dialog',
templateUrl: './rule-notification-dialog.component.html',
styleUrls: ['rule-notification-dialog.component.scss']
})
export class RuleNotificationDialogComponent extends
DialogComponent<RuleNotificationDialogComponent, NotificationRule> implements OnDestroy {
@ViewChild('addNotificationRule', {static: true}) addNotificationRule: MatStepper;
@ViewChild('severitiesChipList') severitiesChipList: MatChipList;
@ViewChild('severityAutocomplete') severityAutocomplete: MatAutocomplete;
@ViewChild('severityInput') severityInput: ElementRef<HTMLInputElement>;
readonly separatorKeysCodes: number[] = [ENTER, COMMA, SEMICOLON];
stepperOrientation: Observable<StepperOrientation>;
ruleNotificationForm: FormGroup;
alarmTemplateForm: FormGroup;
deviceInactivityTemplateForm: FormGroup;
entityActionTemplateForm: FormGroup;
triggerType = TriggerType;
triggerTypes: TriggerType[] = Object.values(TriggerType);
triggerTypeTranslationMap = TriggerTypeTranslationMap;
alarmSeverities = Object.keys(AlarmSeverity);
alarmSeverityEnum = AlarmSeverity;
alarmSeverityTranslationMap = alarmSeverityTranslations;
alarmSearchStatuses = Object.values(AlarmStatus);
alarmSearchStatusTranslationMap = alarmStatusTranslations;
entityType = EntityType;
entityTypes = Object.values(EntityType);
isAdd = true;
selectedIndex = 0;
filteredDisplaySeverities: Observable<Array<string>>;
severitySearchText = '';
dialogTitle = 'notification.edit-rule';
severityInputChange = new Subject<string>();
private destroy$ = new Subject();
private readonly ruleNotification: NotificationRule;
constructor(protected store: Store<AppState>,
protected router: Router,
protected dialogRef: MatDialogRef<RuleNotificationDialogComponent, NotificationRule>,
@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);
if (isDefined(data.isAdd)) {
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],
triggerType: [null, Validators.required],
recipientsConfig: this.fb.group({
triggerType: [],
}),
triggerConfig: this.fb.group({
triggerType: []
}),
additionalConfig: this.fb.group({
description: []
})
});
this.ruleNotificationForm.get('triggerType').valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe(
value => {
this.ruleNotificationForm.get('triggerConfig').patchValue({triggerType: value}, {emitEvent: false});
this.ruleNotificationForm.get('recipientsConfig').patchValue({triggerType: value}, {emitEvent: false});
}
);
this.alarmTemplateForm = this.fb.group({
alarmTypes: [[], Validators.required],
alarmSeverities: [[], Validators.required],
clearRule: this.fb.group({
alarmStatus: []
}),
escalationTable: [],
description: ['']
});
this.deviceInactivityTemplateForm = this.fb.group({
filterByDevice: [true],
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: [],
created: [false],
updated: [false],
deleted: [false],
targets: [[], Validators.required],
description: ['']
});
this.filteredDisplaySeverities = this.severityInputChange
.pipe(
startWith(''),
map((value) => value ? value : ''),
mergeMap(name => this.fetchSeverities(name) ),
share()
);
if (data.isAdd || data.isCopy) {
this.dialogTitle = 'notification.add-rule';
}
this.ruleNotification = deepClone(this.data.rule);
if (this.ruleNotification) {
if (this.data.isCopy) {
this.ruleNotification.name += ` (${this.translate.instant('action.copy')})`;
}
this.ruleNotificationForm.reset({}, {emitEvent: false});
this.ruleNotificationForm.patchValue(this.ruleNotification, {emitEvent: false});
if (this.ruleNotification.triggerType === TriggerType.ALARM) {
const parsedEscalationTable = [];
const escalationTable = this.ruleNotification.recipientsConfig.escalationTable;
for (const escalation in escalationTable) {
parsedEscalationTable.push({delayInSec: escalation, targets: escalationTable[escalation]});
}
this.alarmTemplateForm.patchValue({
escalationTable: parsedEscalationTable,
alarmTypes: this.ruleNotification.triggerConfig.alarmTypes,
alarmSeverities: this.ruleNotification.triggerConfig.alarmSeverities,
clearRule: this.ruleNotification.triggerConfig.clearRule,
description: this.ruleNotification.additionalConfig.description
}, {emitEvent: false});
} else if (this.ruleNotification.triggerType === TriggerType.DEVICE_INACTIVITY) {
this.deviceInactivityTemplateForm.patchValue({
filterByDevice: !!this.ruleNotification.triggerConfig.devices,
deviceProfiles: this.ruleNotification.triggerConfig.devicesProfiles,
devices: this.ruleNotification.triggerConfig.devices,
targets: this.ruleNotification.recipientsConfig.targets,
description: this.ruleNotification.additionalConfig.description
}, {emitEvent: false});
} else {
this.entityActionTemplateForm.patchValue({
entityType: this.ruleNotification.triggerConfig.entityType,
created: this.ruleNotification.triggerConfig.created,
updated: this.ruleNotification.triggerConfig.updated,
deleted: this.ruleNotification.triggerConfig.deleted,
targets: this.ruleNotification.recipientsConfig.targets
}, {emitEvent: false});
}
}
}
onSeverityRemoved(severity: string): void {
const severities: string[] = this.alarmTemplateForm.get('alarmSeverities').value;
const index = severities.indexOf(severity);
if (index > -1) {
severities.splice(index, 1);
this.alarmTemplateForm.get('alarmSeverities').setValue(severities);
this.alarmTemplateForm.get('alarmSeverities').markAsDirty();
this.severitiesChipList.errorState = !severities.length;
}
}
onSeverityInputFocus() {
this.severityInputChange.next(this.severityInput.nativeElement.value);
}
addSeverityFromChipInput(event: MatChipInputEvent): void {
const value = event.value;
if ((value || '').trim()) {
const severityName = value.trim().toUpperCase();
const existingSeverity = this.alarmSeverities.find(severity => severity.toUpperCase() === severityName);
if (this.addSeverity(existingSeverity)) {
this.clearSeverityInput('');
}
}
}
private addSeverity(existingSeverity: string): boolean {
if (existingSeverity) {
const displaySeverities: string[] = this.alarmTemplateForm.get('alarmSeverities').value;
const index = displaySeverities.indexOf(existingSeverity);
if (index === -1) {
displaySeverities.push(existingSeverity);
this.alarmTemplateForm.get('alarmSeverities').setValue(displaySeverities);
this.alarmTemplateForm.get('alarmSeverities').markAsDirty();
this.severitiesChipList.errorState = false;
return true;
}
}
return false;
}
clearSeverityInput(value: string = '') {
this.severityInput.nativeElement.value = value;
this.severityInputChange.next(null);
setTimeout(() => {
this.severityInput.nativeElement.blur();
this.severityInput.nativeElement.focus();
}, 0);
}
severitySelected(event: MatAutocompleteSelectedEvent): void {
this.addSeverity(event.option.value);
this.clearSeverityInput('');
}
displaySeverityFn(severity?: string): string | undefined {
return severity ? this.translate.instant(this.alarmSeverityTranslationMap.get(this.alarmSeverityEnum[severity])) : undefined;
}
private fetchSeverities(searchText?: string): Observable<Array<string>> {
this.severitySearchText = searchText;
if (this.severitySearchText && this.severitySearchText.length) {
const search = this.severitySearchText.toUpperCase();
return of(this.alarmSeverities.filter(severity => severity.toUpperCase().includes(search)));
} else {
return of(this.alarmSeverities);
}
}
textIsNotEmpty(text: string): boolean {
return (text && text.length > 0);
}
public alarmTypeList(): string[] {
return this.alarmTemplateForm.get('alarmTypes').value;
}
public removeAlarmType(type: string): void {
const types: string[] = this.alarmTemplateForm.get('alarmTypes').value;
const index = types.indexOf(type);
if (index >= 0) {
types.splice(index, 1);
this.alarmTemplateForm.get('alarmTypes').setValue(types);
this.alarmTemplateForm.get('alarmTypes').markAsDirty();
}
}
public addAlarmType(event: MatChipInputEvent): void {
const input = event.input;
const value = event.value;
const types: string[] = this.alarmTemplateForm.get('alarmTypes').value;
if ((value || '').trim()) {
types.push(value.trim());
this.alarmTemplateForm.get('alarmTypes').setValue(types);
this.alarmTemplateForm.get('alarmTypes').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()) {
let formValue = this.ruleNotificationForm.value;
const triggerType = this.ruleNotificationForm.get('triggerType').value;
if (triggerType === TriggerType.ALARM) {
Object.assign(formValue.triggerConfig, this.alarmTemplateForm.value);
const parsedEscalationTable = {};
this.alarmTemplateForm.get('escalationTable').value.forEach(
escalation => parsedEscalationTable[escalation.delayInSec] = escalation.targets
);
formValue.recipientsConfig.escalationTable = parsedEscalationTable;
formValue.additionalConfig.description = this.alarmTemplateForm.get('description').value;
delete formValue.triggerConfig.escalationTable;
} else if (triggerType === TriggerType.DEVICE_INACTIVITY) {
Object.assign(formValue.triggerConfig, this.deviceInactivityTemplateForm.value);
formValue.recipientsConfig.targets = this.deviceInactivityTemplateForm.get('targets').value;
formValue.additionalConfig.description = this.deviceInactivityTemplateForm.get('description').value;
delete formValue.triggerConfig.filterByDevice;
} else {
Object.assign(formValue.triggerConfig, this.entityActionTemplateForm.value);
formValue.recipientsConfig.targets = this.entityActionTemplateForm.get('targets').value;
formValue.additionalConfig.description = this.entityActionTemplateForm.get('description').value;
}
if (triggerType === TriggerType.DEVICE_INACTIVITY || triggerType === TriggerType.ENTITY_ACTION) {
delete formValue.triggerConfig.trigger;
}
if (this.ruleNotification) {
formValue = {...this.ruleNotification, ...formValue};
}
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() {
super.ngOnDestroy();
this.destroy$.next();
this.destroy$.complete();
}
cancel(): void {
this.dialogRef.close(null);
}
save() {
let formValue = deepTrim(this.ruleNotificationForm.value);
if (isDefined(this.data.rule)) {
formValue = Object.assign({}, this.data.rule, formValue);
}
this.notificationService.saveNotificationRule(formValue).subscribe(
(rule) => this.dialogRef.close(rule)
);
}
}

24
ui-ngx/src/app/modules/home/pages/notification-center/rule-table/rule-table-header.component.html

@ -0,0 +1,24 @@
<!--
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.
-->
<div fxFlex="100" fxLayout="row" fxLayoutAlign="end center">
<button mat-stroked-button color="primary" (click)="createTarget($event)">
<div fxLayout="row" fxLayoutAlign="start center">
<mat-icon>add</mat-icon>{{ 'notification.add-rule' | translate }}
</div>
</button>
</div>

18
ui-ngx/src/app/modules/home/pages/notification-center/rule-table/rule-table-header.component.scss

@ -0,0 +1,18 @@
/**
* 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{
width: 100%;
}

38
ui-ngx/src/app/modules/home/pages/notification-center/rule-table/rule-table-header.component.ts

@ -0,0 +1,38 @@
///
/// 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 } from '@angular/core';
import { EntityTableHeaderComponent } from '@home/components/entity/entity-table-header.component';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { NotificationRule } from '@shared/models/notification.models';
@Component({
selector: 'tb-rule-table-header',
templateUrl: './rule-table-header.component.html',
styleUrls: ['rule-table-header.component.scss']
})
export class RuleTableHeaderComponent extends EntityTableHeaderComponent<NotificationRule> {
constructor(protected store: Store<AppState>) {
super(store);
}
createTarget($event: Event) {
this.entitiesTableConfig.onEntityAction({event: $event, action: 'add', entity: null});
}
}

2
ui-ngx/src/app/shared/components/time/timeinterval.component.html

@ -51,7 +51,7 @@
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
</section> </section>
<section fxLayout="column" fxLayoutAlign="center center" [fxShow]="(isEdit || !hideFlag)"> <section fxLayout="column" fxLayoutAlign="center center" [fxShow]="(isEdit || !hideFlag) && !disabledAdvanced">
<label class="tb-small advanced-label" translate>timeinterval.advanced</label> <label class="tb-small advanced-label" translate>timeinterval.advanced</label>
<mat-slide-toggle [disabled]="hideFlag || disabled" class="advanced-switch" [(ngModel)]="advanced" (ngModelChange)="onAdvancedChange()"></mat-slide-toggle> <mat-slide-toggle [disabled]="hideFlag || disabled" class="advanced-switch" [(ngModel)]="advanced" (ngModelChange)="onAdvancedChange()"></mat-slide-toggle>
</section> </section>

2
ui-ngx/src/app/shared/components/time/timeinterval.component.ts

@ -78,6 +78,8 @@ export class TimeintervalComponent implements OnInit, ControlValueAccessor {
this.hideFlagValue = val; this.hideFlagValue = val;
} }
@Input() disabledAdvanced = false;
@Output() hideFlagChange = new EventEmitter<boolean>(); @Output() hideFlagChange = new EventEmitter<boolean>();
@Input() disabled: boolean; @Input() disabled: boolean;

37
ui-ngx/src/app/shared/models/entity-type.models.ts

@ -331,14 +331,51 @@ export const entityTypeTranslations = new Map<EntityType | AliasEntityType, Enti
selectedEntities: 'ota-update.selected-package' selectedEntities: 'ota-update.selected-package'
} }
], ],
[
EntityType.RPC,
{
type: 'entity.type-rpc'
}
],
[ [
EntityType.QUEUE, EntityType.QUEUE,
{ {
type: 'entity.type-queue',
add: 'queue.add', add: 'queue.add',
search: 'queue.search', search: 'queue.search',
details: 'queue.details', details: 'queue.details',
selectedEntities: 'queue.selected-queues' selectedEntities: 'queue.selected-queues'
} }
],
[
EntityType.NOTIFICATION,
{
type: 'entity.type-notification'
}
],
[
EntityType.NOTIFICATION_REQUEST,
{
type: 'entity.type-notification-request'
}
],
[
EntityType.NOTIFICATION_RULE,
{
type: 'entity.type-notification-rule'
}
],
[
EntityType.NOTIFICATION_TARGET,
{
type: 'entity.type-notification-target'
}
],
[
EntityType.NOTIFICATION_TEMPLATE,
{
type: 'entity.type-notification-template'
}
] ]
] ]
); );

38
ui-ngx/src/app/shared/models/notification.models.ts

@ -23,6 +23,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;
@ -96,17 +98,27 @@ 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;
recipientsConfig: NotificationRuleRecipientConfig;
additionalConfig: {description: string};
} }
export interface NotificationRuleConfig { export interface NotificationRuleTriggerConfig {
initialNotificationTargetId: NotificationTargetId; alarmTypes?: Array<string>;
escalationConfig: NotificationEscalationConfig; alarmSeverities?: Array<AlarmSeverity>;
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 {
@ -300,3 +312,15 @@ export const NotificationTemplateTypeTranslateMap = new Map<NotificationType, No
} }
] ]
]); ]);
export enum TriggerType {
ALARM = 'ALARM',
DEVICE_INACTIVITY = 'DEVICE_INACTIVITY',
ENTITY_ACTION = 'ENTITY_ACTION'
}
export const TriggerTypeTranslationMap = new Map<TriggerType, string>([
[TriggerType.ALARM, 'notification.trigger.alarm'],
[TriggerType.DEVICE_INACTIVITY, 'notification.trigger.device-inactivity'],
[TriggerType.ENTITY_ACTION, 'notification.trigger.entity-action']
]);

51
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -1960,7 +1960,14 @@
"list-of-edges": "{ count, plural, 1 {One edge} other {List of # edges} }", "list-of-edges": "{ count, plural, 1 {One edge} other {List of # edges} }",
"edge-name-starts-with": "Edges whose names start with '{{prefix}}'", "edge-name-starts-with": "Edges whose names start with '{{prefix}}'",
"type-tb-resource": "Resource", "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": { "entity-field": {
"created-time": "Created time", "created-time": "Created time",
@ -2682,14 +2689,17 @@
"add-notification-template": "Add notification template", "add-notification-template": "Add notification template",
"add-target": "Add recipient", "add-target": "Add recipient",
"add-template": "Add template", "add-template": "Add template",
"all": "All",
"at-least-one-should-be-selected": "At least one should be selected", "at-least-one-should-be-selected": "At least one should be selected",
"all": "All",
"basic-settings": "Basic settings", "basic-settings": "Basic settings",
"button-text": "Button text", "button-text": "Button text",
"button-text-required": "Button text is required", "button-text-required": "Button text is required",
"compose": "Compose", "compose": "Compose",
"conversation": "Conversation", "conversation": "Conversation",
"conversation-required": "Conversation is required", "conversation-required": "Conversation is required",
"add-rule": "Add rule",
"edit-rule": "Edit rule",
"copy-rule": "Copy rule",
"copy-notification-template": "Copy notification template", "copy-notification-template": "Copy notification template",
"copy-template": "Copy template", "copy-template": "Copy template",
"create-target": "Create recipient", "create-target": "Create recipient",
@ -2707,6 +2717,8 @@
"slack": "Slack", "slack": "Slack",
"sms": "SMS" "sms": "SMS"
}, },
"delete-rule-text": "Be careful, after the confirmation the notification rule will become unrecoverable.",
"delete-rule-title": "Are you sure you want to delete the notification rule '{{ruleName}}'?",
"delivery-methods": "Delivery methods", "delivery-methods": "Delivery methods",
"description": "Description", "description": "Description",
"edit-notification-target": "Edit notification recipient", "edit-notification-target": "Edit notification recipient",
@ -2740,12 +2752,15 @@
"sent": "Sent" "sent": "Sent"
}, },
"review": "Review", "review": "Review",
"no-rules-notification": "No rules notification",
"rule": "Rule",
"rules": "Rules", "rules": "Rules",
"platform-users": "Platform users", "platform-users": "Platform users",
"scheduler-later": "Schedule for later", "scheduler-later": "Schedule for later",
"search-notification": "Search notification", "search-notification": "Search notification",
"search-targets": "Search recipients", "search-targets": "Search recipients",
"search-templates": "Search templates", "search-templates": "Search templates",
"search-rules": "Search rules",
"send-notification": "Send notification", "send-notification": "Send notification",
"send-test-notification": "Send test notification", "send-test-notification": "Send test notification",
"sent": "Sent", "sent": "Sent",
@ -2774,6 +2789,7 @@
}, },
"targets": "Recipients", "targets": "Recipients",
"targets-required": "Recipients is required", "targets-required": "Recipients is required",
"recipient": "Recipient",
"template": "Template", "template": "Template",
"template-required": "Template is required", "template-required": "Template is required",
"template-type": { "template-type": {
@ -2794,7 +2810,36 @@
"type": "Type", "type": "Type",
"unread": "Unread", "unread": "Unread",
"view-all": "View all", "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",
"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",
"devices": "Devices",
"device-profile": "Device profile",
"device-profiles": "Device profiles",
"filter-by": "Filter by",
"entity-type": "Entity type",
"created": "Created",
"updated": "Updated",
"deleted": "Deleted",
"type-settings": "Type settings"
}, },
"ota-update": { "ota-update": {
"add": "Add package", "add": "Add package",

Loading…
Cancel
Save