From 1b1f3b4d7b52f8a9cc62181006a6e7938a17cca4 Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Mon, 23 Jan 2023 14:54:15 +0200 Subject: [PATCH 1/5] UI: Add rule table --- .../notification-center.component.html | 6 +- .../notification-center.module.ts | 6 +- .../notification-table.component.ts | 7 + .../notification-table/rule-table-config.ts | 120 ++++++++++++++++ .../rule-notification-dialog.component.html | 132 ++++++++++++++++++ .../rule-notification-dialog.component.scss | 29 ++++ .../rule-notification-dialog.component.ts | 109 +++++++++++++++ .../rule-table-header.component.html | 24 ++++ .../rule-table-header.component.scss | 18 +++ .../rule-table/rule-table-header.component.ts | 38 +++++ .../app/shared/models/notification.models.ts | 3 +- .../assets/locale/locale.constant-en_US.json | 13 +- 12 files changed, 499 insertions(+), 6 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/pages/notification-center/notification-table/rule-table-config.ts create mode 100644 ui-ngx/src/app/modules/home/pages/notification-center/rule-table/rule-notification-dialog.component.html create mode 100644 ui-ngx/src/app/modules/home/pages/notification-center/rule-table/rule-notification-dialog.component.scss create mode 100644 ui-ngx/src/app/modules/home/pages/notification-center/rule-table/rule-notification-dialog.component.ts create mode 100644 ui-ngx/src/app/modules/home/pages/notification-center/rule-table/rule-table-header.component.html create mode 100644 ui-ngx/src/app/modules/home/pages/notification-center/rule-table/rule-table-header.component.scss create mode 100644 ui-ngx/src/app/modules/home/pages/notification-center/rule-table/rule-table-header.component.ts diff --git a/ui-ngx/src/app/modules/home/pages/notification-center/notification-center.component.html b/ui-ngx/src/app/modules/home/pages/notification-center/notification-center.component.html index bd7925d7a1..eaa9a183a0 100644 --- a/ui-ngx/src/app/modules/home/pages/notification-center/notification-center.component.html +++ b/ui-ngx/src/app/modules/home/pages/notification-center/notification-center.component.html @@ -41,9 +41,9 @@ - - Content 1 - Loaded - + + diff --git a/ui-ngx/src/app/modules/home/pages/notification-center/notification-center.module.ts b/ui-ngx/src/app/modules/home/pages/notification-center/notification-center.module.ts index 59ce9cc77e..fd8d1a6723 100644 --- a/ui-ngx/src/app/modules/home/pages/notification-center/notification-center.module.ts +++ b/ui-ngx/src/app/modules/home/pages/notification-center/notification-center.module.ts @@ -42,6 +42,8 @@ import { TemplateAutocompleteComponent } from '@home/pages/notification-center/template-table/template-autocomplete.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'; @NgModule({ declarations: [ @@ -53,7 +55,9 @@ import { InboxTableHeaderComponent } from '@home/pages/notification-center/inbox TemplateTableHeaderComponent, NotificationTableComponent, TemplateAutocompleteComponent, - InboxTableHeaderComponent + InboxTableHeaderComponent, + RuleTableHeaderComponent, + RuleNotificationDialogComponent ], imports: [ CommonModule, diff --git a/ui-ngx/src/app/modules/home/pages/notification-center/notification-table/notification-table.component.ts b/ui-ngx/src/app/modules/home/pages/notification-center/notification-table/notification-table.component.ts index a07b5dbb46..10c3134459 100644 --- a/ui-ngx/src/app/modules/home/pages/notification-center/notification-table/notification-table.component.ts +++ b/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 { TemplateTableConfig } from '@home/pages/notification-center/notification-table/template-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({ selector: 'tb-notification-table', @@ -85,6 +86,12 @@ export class NotificationTableComponent implements OnInit { this.dialog, this.datePipe ); + case EntityType.NOTIFICATION_RULE: + return new RuleTableConfig( + this.notificationService, + this.translate, + this.dialog + ); } } } diff --git a/ui-ngx/src/app/modules/home/pages/notification-center/notification-table/rule-table-config.ts b/ui-ngx/src/app/modules/home/pages/notification-center/notification-table/rule-table-config.ts new file mode 100644 index 0000000000..732fa8e16e --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/notification-center/notification-table/rule-table-config.ts @@ -0,0 +1,120 @@ +/// +/// 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 +} 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 { + + 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; + + 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('name', 'notification.rule-name', '30%'), + new EntityTableColumn('templateId', 'notification.template', '30%', + (rule) => `${rule.templateId}`, + () => ({}), false), + new EntityTableColumn('configuration.description', 'notification.description', '40%', + (rule) => rule.configuration.description || '', + () => ({}), false) + ); + } + + private configureCellActions(): Array> { + return [{ + name: this.translate.instant('device.make-public'), + icon: 'edit', + isEnabled: () => true, + onAction: ($event, entity) => this.editTarget($event, entity) + }]; + } + + private editTarget($event: Event, rule: NotificationRule, isAdd = false) { + if ($event) { + $event.stopPropagation(); + } + this.dialog.open(RuleNotificationDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + isAdd, + rule + } + }).afterClosed() + .subscribe((res) => { + if (res) { + this.updateData(); + } + }); + } + + private onTargetAction(action: EntityAction): boolean { + switch (action.action) { + case 'add': + this.editTarget(action.event, action.entity, true); + return true; + } + return false; + } +} diff --git a/ui-ngx/src/app/modules/home/pages/notification-center/rule-table/rule-notification-dialog.component.html b/ui-ngx/src/app/modules/home/pages/notification-center/rule-table/rule-notification-dialog.component.html new file mode 100644 index 0000000000..a51900720f --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/notification-center/rule-table/rule-notification-dialog.component.html @@ -0,0 +1,132 @@ + +
+ +

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

+ + +
+ + +
+
+ + notification.rule-name + + + {{ 'notification.rule-name-required' | translate }} + + + + +
+ +
+ notification.hierarchy-of-receiving +
+
+ notification.rule + notification.recipient +
+
+
+
+
+
+ First recipient + After +
+
+ After + + +
+ + +
+ +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + notification.description + + +
+
+ +
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/pages/notification-center/rule-table/rule-notification-dialog.component.scss b/ui-ngx/src/app/modules/home/pages/notification-center/rule-table/rule-notification-dialog.component.scss new file mode 100644 index 0000000000..07d9023294 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/notification-center/rule-table/rule-notification-dialog.component.scss @@ -0,0 +1,29 @@ +/** + * 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 { + .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; + } + } +} diff --git a/ui-ngx/src/app/modules/home/pages/notification-center/rule-table/rule-notification-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/notification-center/rule-table/rule-notification-dialog.component.ts new file mode 100644 index 0000000000..b8bb7bd00e --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/notification-center/rule-table/rule-notification-dialog.component.ts @@ -0,0 +1,109 @@ +/// +/// 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, + NotificationTarget, + NotificationTargetConfigType, + NotificationTargetConfigTypeTranslateMap +} from '@shared/models/notification.models'; +import { Component, Inject, OnDestroy } 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 { deepTrim, isDefined } from '@core/utils'; +import { Subject } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; + +export interface RuleNotificationDialogData { + rule?: NotificationRule; + isAdd?: boolean; +} + +@Component({ + selector: 'tb-rule-notification-dialog', + templateUrl: './rule-notification-dialog.component.html', + styleUrls: ['rule-notification-dialog.component.scss'] +}) +export class RuleNotificationDialogComponent extends + DialogComponent implements OnDestroy { + + ruleNotificationForm: FormGroup; + // notificationTargetConfigType = NotificationTargetConfigType; + // notificationTargetConfigTypes: NotificationTargetConfigType[] = Object.values(NotificationTargetConfigType); + // notificationTargetConfigTypeTranslateMap = NotificationTargetConfigTypeTranslateMap; + entityType = EntityType; + isAdd = true; + + private readonly destroy$ = new Subject(); + + constructor(protected store: Store, + protected router: Router, + protected dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: RuleNotificationDialogData, + private fb: FormBuilder, + private notificationService: NotificationService) { + super(store, router, dialogRef); + + if (isDefined(data.isAdd)) { + this.isAdd = data.isAdd; + } + + this.ruleNotificationForm = this.fb.group({ + name: [null, Validators.required], + templateId: [null, Validators.required], + configuration: this.fb.group({ + ids: [], + interval: [], + // type: [NotificationTargetConfigType.ALL_USERS], + description: [null], + // usersIds: [{value: null, disabled: true}, Validators.required], + // customerId: [{value: null, disabled: true}, Validators.required], + // getCustomerIdFromOriginatorEntity: [{value: false, disabled: true}], + }) + }); + + // if (isDefined(data.target)) { + // this.targetNotificationForm.patchValue(data.target, {emitEvent: false}); + // this.targetNotificationForm.get('configuration.type').updateValueAndValidity({onlySelf: true}); + // } + } + + 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) + ); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/notification-center/rule-table/rule-table-header.component.html b/ui-ngx/src/app/modules/home/pages/notification-center/rule-table/rule-table-header.component.html new file mode 100644 index 0000000000..01c921cb5b --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/notification-center/rule-table/rule-table-header.component.html @@ -0,0 +1,24 @@ + +
+ +
diff --git a/ui-ngx/src/app/modules/home/pages/notification-center/rule-table/rule-table-header.component.scss b/ui-ngx/src/app/modules/home/pages/notification-center/rule-table/rule-table-header.component.scss new file mode 100644 index 0000000000..f3554a23f8 --- /dev/null +++ b/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%; +} diff --git a/ui-ngx/src/app/modules/home/pages/notification-center/rule-table/rule-table-header.component.ts b/ui-ngx/src/app/modules/home/pages/notification-center/rule-table/rule-table-header.component.ts new file mode 100644 index 0000000000..7107a3901d --- /dev/null +++ b/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 { + + constructor(protected store: Store) { + super(store); + } + + createTarget($event: Event) { + this.entitiesTableConfig.onEntityAction({event: $event, action: 'add', entity: null}); + } + +} diff --git a/ui-ngx/src/app/shared/models/notification.models.ts b/ui-ngx/src/app/shared/models/notification.models.ts index 83aa994c3c..44d5848e49 100644 --- a/ui-ngx/src/app/shared/models/notification.models.ts +++ b/ui-ngx/src/app/shared/models/notification.models.ts @@ -96,13 +96,14 @@ export interface SlackConversation { export interface NotificationRule extends Omit, 'label'>{ tenantId: TenantId; templateId: NotificationTemplateId; - deliveryMethods: Array; + // deliveryMethods: Array; configuration: NotificationRuleConfig; } export interface NotificationRuleConfig { initialNotificationTargetId: NotificationTargetId; escalationConfig: NotificationEscalationConfig; + description?: string; } export interface NotificationEscalationConfig { diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 55bd9ee662..34fe63b5bd 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -2680,6 +2680,7 @@ "button-text": "Button text", "button-text-required": "Button text is required", "compose": "Compose", + "add-rule": "Add rule", "copy-notification-template": "Copy notification template", "create-target": "Create target", "created-time": "Created time", @@ -2697,6 +2698,8 @@ "slack": "Slack", "sms": "SMS" }, + "delete-rule-text": "Be careful, after the confirmation the notification rule will become unrecoverable.", + "delete-rule-title": "Are you sure you want to delete the notification rule '{{ruleName}}'?", "description": "Description", "edit-notification-target": "Edit notification target", "edit-notification-template": "Edit notification template", @@ -2726,10 +2729,13 @@ "sent": "Sent" }, "review": "Review", + "no-rules-notification": "No rules notification", + "rule": "Rule", "rules": "Rules", "search-notification": "Search notification", "search-targets": "Search targets", "search-templates": "Search templates", + "search-rules": "Search rules", "send-notification": "Send notification", "send-test-notification": "Send test notification", "sent": "Sent", @@ -2750,6 +2756,7 @@ }, "targets": "Targets", "targets-required": "Targets is required", + "recipient": "Recipient", "template": "Template", "template-required": "Template is required", "template-type": { @@ -2761,7 +2768,11 @@ "type": "Type", "unread": "Unread", "view-all": "View al;", - "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" }, "ota-update": { "add": "Add package", From c3cc3423934e319d3da4f15af9fbc59119a6e41f Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Fri, 27 Jan 2023 15:47:08 +0200 Subject: [PATCH 2/5] UI: Add rule notification --- .../inbox-table-header.component.html | 2 +- .../notification-center.module.ts | 6 +- .../rule-table/escalation-form.component.html | 41 +++ .../rule-table/escalation-form.component.ts | 139 ++++++++ .../rule-table/escalations.component.html | 45 +++ .../rule-table/escalations.component.ts | 172 +++++++++ .../rule-notification-dialog.component.html | 332 ++++++++++++------ .../rule-notification-dialog.component.scss | 59 ++++ .../rule-notification-dialog.component.ts | 302 ++++++++++++++-- .../time/timeinterval.component.html | 2 +- .../components/time/timeinterval.component.ts | 2 + .../app/shared/models/entity-type.models.ts | 37 ++ .../app/shared/models/notification.models.ts | 12 + .../assets/locale/locale.constant-en_US.json | 36 +- 14 files changed, 1044 insertions(+), 143 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/pages/notification-center/rule-table/escalation-form.component.html create mode 100644 ui-ngx/src/app/modules/home/pages/notification-center/rule-table/escalation-form.component.ts create mode 100644 ui-ngx/src/app/modules/home/pages/notification-center/rule-table/escalations.component.html create mode 100644 ui-ngx/src/app/modules/home/pages/notification-center/rule-table/escalations.component.ts diff --git a/ui-ngx/src/app/modules/home/pages/notification-center/inbox-table/inbox-table-header.component.html b/ui-ngx/src/app/modules/home/pages/notification-center/inbox-table/inbox-table-header.component.html index aa34aed8ff..f3dc87ea90 100644 --- a/ui-ngx/src/app/modules/home/pages/notification-center/inbox-table/inbox-table-header.component.html +++ b/ui-ngx/src/app/modules/home/pages/notification-center/inbox-table/inbox-table-header.component.html @@ -20,7 +20,7 @@ style="width: 180px;" [ngModel]="entitiesTableConfig.componentsData.unreadOnly" (ngModelChange)="changeUnreadOnly($event)"> - Unread + {{ 'notification.unread' | translate }} {{ 'notification.all' | translate }} diff --git a/ui-ngx/src/app/modules/home/pages/notification-center/notification-center.module.ts b/ui-ngx/src/app/modules/home/pages/notification-center/notification-center.module.ts index fd8d1a6723..7154bc617f 100644 --- a/ui-ngx/src/app/modules/home/pages/notification-center/notification-center.module.ts +++ b/ui-ngx/src/app/modules/home/pages/notification-center/notification-center.module.ts @@ -44,6 +44,8 @@ import { import { InboxTableHeaderComponent } from '@home/pages/notification-center/inbox-table/inbox-table-header.component'; import { RuleTableHeaderComponent } from '@home/pages/notification-center/rule-table/rule-table-header.component'; import { RuleNotificationDialogComponent } from '@home/pages/notification-center/rule-table/rule-notification-dialog.component'; +import { EscalationsComponent } from '@home/pages/notification-center/rule-table/escalations.component'; +import { EscalationFormComponent } from '@home/pages/notification-center/rule-table/escalation-form.component'; @NgModule({ declarations: [ @@ -57,7 +59,9 @@ import { RuleNotificationDialogComponent } from '@home/pages/notification-center TemplateAutocompleteComponent, InboxTableHeaderComponent, RuleTableHeaderComponent, - RuleNotificationDialogComponent + RuleNotificationDialogComponent, + EscalationsComponent, + EscalationFormComponent ], imports: [ CommonModule, diff --git a/ui-ngx/src/app/modules/home/pages/notification-center/rule-table/escalation-form.component.html b/ui-ngx/src/app/modules/home/pages/notification-center/rule-table/escalation-form.component.html new file mode 100644 index 0000000000..ab12a8d541 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/notification-center/rule-table/escalation-form.component.html @@ -0,0 +1,41 @@ + + +
+
+
+
notification.first-recipient
+
+ After + + notification.notify +
+
+
+ + +
+
+
diff --git a/ui-ngx/src/app/modules/home/pages/notification-center/rule-table/escalation-form.component.ts b/ui-ngx/src/app/modules/home/pages/notification-center/rule-table/escalation-form.component.ts new file mode 100644 index 0000000000..f68f6e2bc5 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/notification-center/rule-table/escalation-form.component.ts @@ -0,0 +1,139 @@ +/// +/// Copyright © 2016-2022 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, Input, OnDestroy, OnInit } from '@angular/core'; +import { + ControlValueAccessor, + FormBuilder, + FormControl, + FormGroup, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + Validator, Validators +} from '@angular/forms'; +import { UtilsService } from '@core/services/utils.service'; +import { isDefinedAndNotNull } from '@core/utils'; +import { Subscription } from 'rxjs'; +import { NonConfirmedNotificationEscalation } from '@shared/models/notification.models'; +import { EntityType } from '@shared/models/entity-type.models'; + +@Component({ + selector: 'tb-escalation-form', + templateUrl: './escalation-form.component.html', + styleUrls: [], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => EscalationFormComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => EscalationFormComponent), + multi: true, + } + ] +}) +export class EscalationFormComponent implements ControlValueAccessor, OnInit, OnDestroy, Validator { + + @Input() + disabled: boolean; + + @Input() + systemEscalation = false; + + escalationFormGroup: FormGroup; + + entityType = EntityType; + + private modelValue: NonConfirmedNotificationEscalation; + private propagateChange = null; + private propagateChangePending = false; + private valueChange$: Subscription = null; + + constructor(private utils: UtilsService, + private fb: FormBuilder) { + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + if (this.propagateChangePending) { + this.propagateChangePending = false; + setTimeout(() => { + this.propagateChange(this.modelValue); + }, 0); + } + } + + registerOnTouched(fn: any): void { + } + + ngOnInit() { + this.escalationFormGroup = this.fb.group( + { + delayInSec: [null], + notificationTargetId: [null, Validators.required], + }); + this.valueChange$ = this.escalationFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + } + + ngOnDestroy() { + if (this.valueChange$) { + this.valueChange$.unsubscribe(); + this.valueChange$ = null; + } + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.escalationFormGroup.disable({emitEvent: false}); + } else { + this.escalationFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: NonConfirmedNotificationEscalation): void { + this.propagateChangePending = false; + this.modelValue = value; + if (isDefinedAndNotNull(this.modelValue)) { + this.escalationFormGroup.patchValue(this.modelValue, {emitEvent: false}); + } + if (!this.disabled && !this.escalationFormGroup.valid) { + this.updateModel(); + } + } + + public validate(c: FormControl) { + return (this.escalationFormGroup.valid) ? null : { + escalation: { + valid: false, + }, + }; + } + + private updateModel() { + const value = this.escalationFormGroup.value; + this.modelValue = {...this.modelValue, ...value}; + if (this.propagateChange) { + this.propagateChange(this.modelValue); + } else { + this.propagateChangePending = true; + } + } +} diff --git a/ui-ngx/src/app/modules/home/pages/notification-center/rule-table/escalations.component.html b/ui-ngx/src/app/modules/home/pages/notification-center/rule-table/escalations.component.html new file mode 100644 index 0000000000..e11a1227f7 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/notification-center/rule-table/escalations.component.html @@ -0,0 +1,45 @@ + +
+
+ + + +
+
+ notification.no-rule +
+
+ +
+
diff --git a/ui-ngx/src/app/modules/home/pages/notification-center/rule-table/escalations.component.ts b/ui-ngx/src/app/modules/home/pages/notification-center/rule-table/escalations.component.ts new file mode 100644 index 0000000000..fa8e90ce6c --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/notification-center/rule-table/escalations.component.ts @@ -0,0 +1,172 @@ +/// +/// Copyright © 2016-2022 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, Input, OnDestroy } from '@angular/core'; +import { + AbstractControl, + ControlValueAccessor, + FormArray, + FormBuilder, + FormGroup, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + ValidationErrors, + Validator, + Validators +} from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@app/core/core.state'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { Subscription } from 'rxjs'; +import { QueueInfo } from '@shared/models/queue.models'; +import { UtilsService } from '@core/services/utils.service'; +import { guid } from '@core/utils'; +import { NonConfirmedNotificationEscalation } from '@shared/models/notification.models'; + +@Component({ + selector: 'tb-escalations-component', + templateUrl: './escalations.component.html', + styleUrls: [], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => EscalationsComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => EscalationsComponent), + multi: true, + } + ] +}) +export class EscalationsComponent implements ControlValueAccessor, Validator, OnDestroy { + + escalationsFormGroup: FormGroup; + newEscalation = false; + + private requiredValue: boolean; + get required(): boolean { + return this.requiredValue; + } + @Input() + set required(value: boolean) { + this.requiredValue = coerceBooleanProperty(value); + } + + @Input() + disabled: boolean; + + private mainEscalaion = { + delayInSec: null, + notificationTargetId: null + }; + + private valueChangeSubscription$: Subscription = null; + + private propagateChange = (v: any) => { }; + + constructor(private store: Store, + private utils: UtilsService, + private fb: FormBuilder) { + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + ngOnDestroy() { + if (this.valueChangeSubscription$) { + this.valueChangeSubscription$.unsubscribe(); + } + } + + registerOnTouched(fn: any): void { + } + + ngOnInit() { + this.escalationsFormGroup = this.fb.group({ + escalations: this.fb.array([]) + }); + } + + get escalationsFormArray(): FormArray { + return this.escalationsFormGroup.get('escalations') as FormArray; + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.escalationsFormGroup.disable({emitEvent: false}); + } else { + this.escalationsFormGroup.enable({emitEvent: false}); + } + } + + writeValue(escalations: Array | null): void { + if (this.valueChangeSubscription$) { + this.valueChangeSubscription$.unsubscribe(); + } + const escalationsControls: Array = []; + if (escalations) { + escalations.forEach((escalation, index) => { + escalationsControls.push(this.fb.control(escalation, [Validators.required])); + }); + } else { + escalationsControls.push(this.fb.control(this.mainEscalaion, [Validators.required])); + } + this.escalationsFormGroup.setControl('escalations', this.fb.array(escalationsControls)); + if (this.disabled) { + this.escalationsFormGroup.disable({emitEvent: false}); + } else { + this.escalationsFormGroup.enable({emitEvent: false}); + } + this.valueChangeSubscription$ = this.escalationsFormGroup.valueChanges.subscribe(() => + this.updateModel() + ); + } + + public removeEscalation(index: number) { + (this.escalationsFormGroup.get('escalations') as FormArray).removeAt(index); + } + + public addEscalation() { + const escalation: NonConfirmedNotificationEscalation = { + delayInSec: null, + notificationTargetId: null + }; + this.newEscalation = true; + const escalationArray = this.escalationsFormGroup.get('escalations') as FormArray; + escalationArray.push(this.fb.control(escalation, [])); + this.escalationsFormGroup.updateValueAndValidity(); + if (!this.escalationsFormGroup.valid) { + this.updateModel(); + } + } + + public validate(c: AbstractControl): ValidationErrors | null { + return this.escalationsFormGroup.valid ? null : { + escalation: { + valid: false, + }, + }; + } + + private updateModel() { + const escalations: Array = this.escalationsFormGroup.get('escalations').value; + this.propagateChange(escalations); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/notification-center/rule-table/rule-notification-dialog.component.html b/ui-ngx/src/app/modules/home/pages/notification-center/rule-table/rule-notification-dialog.component.html index a51900720f..344f5a96ad 100644 --- a/ui-ngx/src/app/modules/home/pages/notification-center/rule-table/rule-notification-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/notification-center/rule-table/rule-notification-dialog.component.html @@ -15,118 +15,224 @@ limitations under the License. --> -
- -

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

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

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

+ + +
+ + +
+
+ + + check + + + {{ 'notification.basic-settings' | translate }} + + + notification.rule-name + + + {{ 'notification.rule-name-required' | translate }} + + + + + + notification.trigger.trigger + + + {{ triggerTypeTranslationMap.get(trigger) | translate }} + + + + {{ 'notification.trigger.trigger-required' | translate }} + + + + + + {{ 'notification.type-settings' | translate }} +
+
+ notification.filter + + alarm.alarm-type-list + + + {{type}} + cancel + + + + + + + alarm.alarm-severity-list + + + {{ alarmSeverityTranslationMap.get(severity) | translate }} + cancel + + + + + + + + +
+
+ notification.no-severity-found +
+ + + {{ translate.get('notification.no-severity-matching', + {severity: truncate.transform(severitySearchText, true, 6, '...')}) | async }} + + +
+
+
+
+
+ +
+ notification.clear-rule + + alarm.alarm-status-list + + + {{ alarmSearchStatusTranslationMap.get(searchStatus) | translate }} + + + +
+ +
+ notification.hierarchy-of-receiving + +
+ + + notification.description + + +
+
+ + + {{ 'notification.type-settings' | translate }} +
+ notification.filter-by +
+ + {{ 'notification.device' | translate }} + {{ 'notification.device-profile' | translate }} +
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - notification.description - - -
-
- -
- - -
- + + + + + + + + notification.description + + + + + + + {{ 'notification.type-settings' | translate }} +
+
+ notification.filter + + +
+ notification.status + {{ 'notification.created' | translate }} + {{ 'notification.updated' | translate }} + {{ 'notification.deleted' | translate }} +
+
+ + + + notification.description + + +
+
+ + + +
+ + + +
diff --git a/ui-ngx/src/app/modules/home/pages/notification-center/rule-table/rule-notification-dialog.component.scss b/ui-ngx/src/app/modules/home/pages/notification-center/rule-table/rule-notification-dialog.component.scss index 07d9023294..308c7170b2 100644 --- a/ui-ngx/src/app/modules/home/pages/notification-center/rule-table/rule-notification-dialog.component.scss +++ b/ui-ngx/src/app/modules/home/pages/notification-center/rule-table/rule-notification-dialog.component.scss @@ -13,8 +13,67 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +@import "../../../../../../theme"; :host { + ::ng-deep{ + width: 100%; + min-width: 800px !important; + max-width: 100%; + + .mat-button-toggle-group.tb-notification-unread-toggle-group { + &.mat-button-toggle-group-appearance-standard { + border: none; + border-radius: 14px; + + .mat-button-toggle + .mat-button-toggle { + border-left: none; + } + } + + .mat-button-toggle { + background: rgba(0, 0, 0, 0.06); + height: 28px; + align-items: center; + display: flex; + + .mat-button-toggle-ripple { + top: 2px; + left: 2px; + right: 2px; + bottom: 2px; + border-radius: 12px; + } + } + + .mat-button-toggle-button { + color: #959595; + } + + .mat-button-toggle-focus-overlay { + border-radius: 14px; + margin: 2px; + } + + .mat-button-toggle-checked .mat-button-toggle-button { + background-color: $tb-primary-color; + color: #fff; + border-radius: 14px; + margin-left: 2px; + margin-right: 2px; + } + + .mat-button-toggle-appearance-standard .mat-button-toggle-label-content { + line-height: 24px; + font-size: 16px; + font-weight: 500; + } + + .mat-button-toggle-checked.mat-button-toggle-appearance-standard:not(.mat-button-toggle-disabled):hover .mat-button-toggle-focus-overlay { + opacity: .01; + } + } + } .fields-group { padding: 0 16px 8px; margin-bottom: 10px; diff --git a/ui-ngx/src/app/modules/home/pages/notification-center/rule-table/rule-notification-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/notification-center/rule-table/rule-notification-dialog.component.ts index b8bb7bd00e..24f3068b13 100644 --- a/ui-ngx/src/app/modules/home/pages/notification-center/rule-table/rule-notification-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/pages/notification-center/rule-table/rule-notification-dialog.component.ts @@ -14,13 +14,8 @@ /// limitations under the License. /// -import { - NotificationRule, - NotificationTarget, - NotificationTargetConfigType, - NotificationTargetConfigTypeTranslateMap -} from '@shared/models/notification.models'; -import { Component, Inject, OnDestroy } from '@angular/core'; +import { NotificationRule, TriggerType, TriggerTypeTranslationMap } from '@shared/models/notification.models'; +import { Component, ElementRef, Inject, OnDestroy, ViewChild } from '@angular/core'; import { DialogComponent } from '@shared/components/dialog.component'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @@ -28,10 +23,26 @@ import { Router } from '@angular/router'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { NotificationService } from '@core/http/notification.service'; -import { EntityType } from '@shared/models/entity-type.models'; +import { EntityType, entityTypeTranslations } from '@shared/models/entity-type.models'; import { deepTrim, isDefined } from '@core/utils'; -import { Subject } from 'rxjs'; -import { takeUntil } from 'rxjs/operators'; +import { Observable, of, Subject } from 'rxjs'; +import { map, mergeMap, share, startWith } from 'rxjs/operators'; +import { StepperOrientation, StepperSelectionEvent } from '@angular/cdk/stepper'; +import { MatStepper } from '@angular/material/stepper'; +import { MediaBreakpoints } from '@shared/models/constants'; +import { BreakpointObserver } from '@angular/cdk/layout'; +import { MatChipInputEvent, MatChipList } from '@angular/material/chips'; +import { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes'; +import { + AlarmSearchStatus, + alarmSearchStatusTranslations, + AlarmSeverity, + alarmSeverityTranslations +} from '@shared/models/alarm.models'; +import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop'; +import { MatAutocomplete, MatAutocompleteSelectedEvent } from '@angular/material/autocomplete'; +import { TranslateService } from '@ngx-translate/core'; +import { TruncatePipe } from '@shared/pipe/truncate.pipe'; export interface RuleNotificationDialogData { rule?: NotificationRule; @@ -46,20 +57,56 @@ export interface RuleNotificationDialogData { export class RuleNotificationDialogComponent extends DialogComponent implements OnDestroy { + @ViewChild('addNotificationRule', {static: true}) addNotificationRule: MatStepper; + + @ViewChild('severitiesChipList') severitiesChipList: MatChipList; + @ViewChild('severityAutocomplete') severityAutocomplete: MatAutocomplete; + @ViewChild('severityInput') severityInput: ElementRef; + + readonly separatorKeysCodes: number[] = [ENTER, COMMA, SEMICOLON]; + + stepperOrientation: Observable; + ruleNotificationForm: FormGroup; - // notificationTargetConfigType = NotificationTargetConfigType; - // notificationTargetConfigTypes: NotificationTargetConfigType[] = Object.values(NotificationTargetConfigType); - // notificationTargetConfigTypeTranslateMap = NotificationTargetConfigTypeTranslateMap; + alarmTemplateForm: FormGroup; + deviceInactivityTemplateForm: FormGroup; + entityActionTemplateForm: FormGroup; + + triggerType = TriggerType; + triggerTypes: TriggerType[] = Object.values(TriggerType); + triggerTypeTranslationMap = TriggerTypeTranslationMap; + + alarmSeverities = Object.keys(AlarmSeverity); + alarmSeverityEnum = AlarmSeverity; + alarmSeverityTranslationMap = alarmSeverityTranslations; + + alarmSearchStatuses = [AlarmSearchStatus.ACTIVE, + AlarmSearchStatus.CLEARED, + AlarmSearchStatus.ACK, + AlarmSearchStatus.UNACK]; + alarmSearchStatusTranslationMap = alarmSearchStatusTranslations; + entityType = EntityType; + entityTypes = Object.values(EntityType); isAdd = true; + selectedIndex = 0; + + filteredDisplaySeverities: Observable>; + severitySearchText = ''; + + severityInputChange = new Subject(); + private readonly destroy$ = new Subject(); constructor(protected store: Store, protected router: Router, protected dialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) public data: RuleNotificationDialogData, + private breakpointObserver: BreakpointObserver, private fb: FormBuilder, + public translate: TranslateService, + public truncate: TruncatePipe, private notificationService: NotificationService) { super(store, router, dialogRef); @@ -67,24 +114,231 @@ export class RuleNotificationDialogComponent extends this.isAdd = data.isAdd; } + this.stepperOrientation = this.breakpointObserver.observe(MediaBreakpoints['gt-xs']) + .pipe(map(({matches}) => matches ? 'horizontal' : 'vertical')); + this.ruleNotificationForm = this.fb.group({ name: [null, Validators.required], templateId: [null, Validators.required], + trigger: [this.triggerType.ALARM, Validators.required], configuration: this.fb.group({ - ids: [], - interval: [], - // type: [NotificationTargetConfigType.ALL_USERS], - description: [null], - // usersIds: [{value: null, disabled: true}, Validators.required], - // customerId: [{value: null, disabled: true}, Validators.required], - // getCustomerIdFromOriginatorEntity: [{value: false, disabled: true}], + escalationConfig: this.fb.group({ + escalations: [[]] + }), + description: [null] }) }); - // if (isDefined(data.target)) { - // this.targetNotificationForm.patchValue(data.target, {emitEvent: false}); - // this.targetNotificationForm.get('configuration.type').updateValueAndValidity({onlySelf: true}); - // } + this.alarmTemplateForm = this.fb.group({ + alarmTypeList: [[], Validators.required], + alarmSeverityList: [[], Validators.required], + alarmStatusList: [[], Validators.required], + escalationConfig: [], + description: [''] + }); + + this.deviceInactivityTemplateForm = this.fb.group({ + filterByDevice: [true], + deviceId: [], + deviceProfileId: [], + notificationTargetId: [], + description: [''] + }); + + this.entityActionTemplateForm = this.fb.group({ + entityType: [], + status: this.fb.group({ + created: [false], + updated: [false], + deleted: [false] + }), + notificationTargetId: [], + description: [''] + }); + + this.filteredDisplaySeverities = this.severityInputChange + .pipe( + startWith(''), + map((value) => value ? value : ''), + mergeMap(name => this.fetchSeverities(name) ), + share() + ); + } + + onSeverityRemoved(severity: string): void { + const severities: string[] = this.alarmTemplateForm.get('alarmSeverityList').value; + const index = severities.indexOf(severity); + if (index > -1) { + severities.splice(index, 1); + this.alarmTemplateForm.get('alarmSeverityList').setValue(severities); + this.alarmTemplateForm.get('alarmSeverityList').markAsDirty(); + this.severitiesChipList.errorState = !severities.length; + } + } + + onSeverityInputFocus() { + this.severityInputChange.next(this.severityInput.nativeElement.value); + } + + addSeverityFromChipInput(event: MatChipInputEvent): void { + const value = event.value; + if ((value || '').trim()) { + const severityName = value.trim().toUpperCase(); + const existingSeverity = this.alarmSeverities.find(severity => severity.toUpperCase() === severityName); + if (this.addSeverity(existingSeverity)) { + this.clearSeverityInput(''); + } + } + } + + private addSeverity(existingSeverity: string): boolean { + if (existingSeverity) { + const displaySeverities: string[] = this.alarmTemplateForm.get('alarmSeverityList').value; + const index = displaySeverities.indexOf(existingSeverity); + if (index === -1) { + displaySeverities.push(existingSeverity); + this.alarmTemplateForm.get('alarmSeverityList').setValue(displaySeverities); + this.alarmTemplateForm.get('alarmSeverityList').markAsDirty(); + this.severitiesChipList.errorState = false; + return true; + } + } + return false; + } + + clearSeverityInput(value: string = '') { + this.severityInput.nativeElement.value = value; + this.severityInputChange.next(null); + setTimeout(() => { + this.severityInput.nativeElement.blur(); + this.severityInput.nativeElement.focus(); + }, 0); + } + + severitySelected(event: MatAutocompleteSelectedEvent): void { + this.addSeverity(event.option.value); + this.clearSeverityInput(''); + } + + displaySeverityFn(severity?: string): string | undefined { + return severity ? this.translate.instant(this.alarmSeverityTranslationMap.get(this.alarmSeverityEnum[severity])) : undefined; + } + + private fetchSeverities(searchText?: string): Observable> { + this.severitySearchText = searchText; + if (this.severitySearchText && this.severitySearchText.length) { + const search = this.severitySearchText.toUpperCase(); + return of(this.alarmSeverities.filter(severity => severity.toUpperCase().includes(search))); + } else { + return of(this.alarmSeverities); + } + } + + textIsNotEmpty(text: string): boolean { + return (text && text.length > 0); + } + + public alarmTypeList(): string[] { + return this.alarmTemplateForm.get('alarmTypeList').value; + } + + public removeAlarmType(type: string): void { + const types: string[] = this.alarmTemplateForm.get('alarmTypeList').value; + const index = types.indexOf(type); + if (index >= 0) { + types.splice(index, 1); + this.alarmTemplateForm.get('alarmTypeList').setValue(types); + this.alarmTemplateForm.get('alarmTypeList').markAsDirty(); + } + } + + public addAlarmType(event: MatChipInputEvent): void { + const input = event.input; + const value = event.value; + + const types: string[] = this.alarmTemplateForm.get('alarmTypeList').value; + + if ((value || '').trim()) { + types.push(value.trim()); + this.alarmTemplateForm.get('alarmTypeList').setValue(types); + this.alarmTemplateForm.get('alarmTypeList').markAsDirty(); + } + + if (input) { + input.value = ''; + } + } + + changeStep($event: StepperSelectionEvent) { + this.selectedIndex = $event.selectedIndex; + } + + backStep() { + this.addNotificationRule.previous(); + } + + nextStep() { + if (this.selectedIndex >= this.maxStepperIndex) { + this.add(); + } else { + this.addNotificationRule.next(); + } + } + + nextStepLabel(): string { + if (this.selectedIndex === 1 && this.selectedIndex < this.maxStepperIndex && this.alarmTemplateForm.pristine) { + return 'action.skip'; + } + if (this.selectedIndex === 2 && this.selectedIndex < this.maxStepperIndex && this.deviceInactivityTemplateForm.pristine) { + return 'action.skip'; + } + if (this.selectedIndex === 3 && this.selectedIndex < this.maxStepperIndex && this.entityActionTemplateForm.pristine) { + return 'action.skip'; + } + if (this.selectedIndex !== 0 && this.selectedIndex >= this.maxStepperIndex) { + return 'action.add'; + } + return 'action.next'; + } + + private get maxStepperIndex(): number { + return this.addNotificationRule?._steps?.length - 1; + } + + private add(): void { + if (this.allValid()) { + const formValue: NotificationRule = this.ruleNotificationForm.value; + // if (formValue.configuration.deliveryMethodsTemplates.PUSH.enabled) { + // Object.assign(formValue.configuration.deliveryMethodsTemplates.PUSH, this.pushTemplateForm.value); + // } else { + // delete formValue.configuration.deliveryMethodsTemplates.PUSH; + // } + // if (formValue.configuration.deliveryMethodsTemplates.EMAIL.enabled) { + // Object.assign(formValue.configuration.deliveryMethodsTemplates.EMAIL, this.emailTemplateForm.value); + // } else { + // delete formValue.configuration.deliveryMethodsTemplates.EMAIL; + // } + // if (formValue.configuration.deliveryMethodsTemplates.SMS.enabled) { + // Object.assign(formValue.configuration.deliveryMethodsTemplates.SMS, this.smsTemplateForm.value); + // } else { + // delete formValue.configuration.deliveryMethodsTemplates.SMS; + // } + this.notificationService.saveNotificationRule(deepTrim(formValue)).subscribe( + (target) => this.dialogRef.close(target) + ); + } + } + + private allValid(): boolean { + return !this.addNotificationRule.steps.find((item, index) => { + if (item.stepControl.invalid) { + item.interacted = true; + this.addNotificationRule.selectedIndex = index; + return true; + } else { + return false; + } + }); } ngOnDestroy() { diff --git a/ui-ngx/src/app/shared/components/time/timeinterval.component.html b/ui-ngx/src/app/shared/components/time/timeinterval.component.html index 6d40e2f491..01dac82750 100644 --- a/ui-ngx/src/app/shared/components/time/timeinterval.component.html +++ b/ui-ngx/src/app/shared/components/time/timeinterval.component.html @@ -51,7 +51,7 @@ -
+
diff --git a/ui-ngx/src/app/shared/components/time/timeinterval.component.ts b/ui-ngx/src/app/shared/components/time/timeinterval.component.ts index 093435add4..7a4f3d4153 100644 --- a/ui-ngx/src/app/shared/components/time/timeinterval.component.ts +++ b/ui-ngx/src/app/shared/components/time/timeinterval.component.ts @@ -78,6 +78,8 @@ export class TimeintervalComponent implements OnInit, ControlValueAccessor { this.hideFlagValue = val; } + @Input() disabledAdvanced = false; + @Output() hideFlagChange = new EventEmitter(); @Input() disabled: boolean; diff --git a/ui-ngx/src/app/shared/models/entity-type.models.ts b/ui-ngx/src/app/shared/models/entity-type.models.ts index 165e011d7f..0550523a1a 100644 --- a/ui-ngx/src/app/shared/models/entity-type.models.ts +++ b/ui-ngx/src/app/shared/models/entity-type.models.ts @@ -331,14 +331,51 @@ export const entityTypeTranslations = new Map([ + [TriggerType.ALARM, 'notification.trigger.alarm'], + [TriggerType.DEVICE_INACTIVITY, 'notification.trigger.device-inactivity'], + [TriggerType.ENTITY_ACTION, 'notification.trigger.entity-action'] +]); diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index ad27f8067b..50209b73b9 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -1952,7 +1952,14 @@ "list-of-edges": "{ count, plural, 1 {One edge} other {List of # edges} }", "edge-name-starts-with": "Edges whose names start with '{{prefix}}'", "type-tb-resource": "Resource", - "type-ota-package": "OTA package" + "type-ota-package": "OTA package", + "type-rpc": "RPC", + "type-queue": "Queue", + "type-notification": "Notification", + "type-notification-rule": "Notification rule", + "type-notification-target": "Notification recipient", + "type-notification-request": "Notification request", + "type-notification-template": "Notification template" }, "entity-field": { "created-time": "Created time", @@ -2768,11 +2775,34 @@ "type": "Type", "unread": "Unread", "view-all": "View all", - "web-settings": "Web settings" + "web-settings": "Web settings", "rule-name": "Rule name", "rule-name-required": "Name is required", "template-name": "Template name", - "hierarchy-of-receiving": "Hierarchy of receiving notifications" + "hierarchy-of-receiving": "Hierarchy of receiving notifications", + "trigger": { + "trigger": "Trigger", + "trigger-required": "Trigger is required", + "alarm": "Alarm", + "device-inactivity": "Device inactivity", + "entity-action": "Entity action" + }, + "trigger-settings": "Trigger settings", + "filter": "Filter", + "clear-rule": "Clear rule", + "no-severity-matching": "'{{severity}}' not found.", + "no-severity-found": "No severity found", + "first-recipient": "First recipient", + "notify": "notify", + "no-rule": "No rule configured", + "device": "Device", + "device-profile": "Device profile", + "filter-by": "Filter by", + "entity-type": "Entity type", + "created": "Created", + "updated": "Updated", + "deleted": "Deleted", + "type-settings": "Type settings" }, "ota-update": { "add": "Add package", From 7ea0c23c651125354f98c95f85d66ee70689bb7c Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Tue, 31 Jan 2023 02:03:51 +0200 Subject: [PATCH 3/5] UI: Norification rule --- .../notification-table/rule-table-config.ts | 15 +- .../rule-table/escalation-form.component.html | 21 +- .../rule-table/escalation-form.component.scss | 34 ++ .../rule-table/escalation-form.component.ts | 28 +- .../rule-table/escalations.component.ts | 62 +-- .../rule-notification-dialog.component.html | 433 +++++++++--------- .../rule-notification-dialog.component.scss | 6 +- .../rule-notification-dialog.component.ts | 135 +++--- .../app/shared/models/notification.models.ts | 34 +- .../assets/locale/locale.constant-en_US.json | 2 + 10 files changed, 419 insertions(+), 351 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/pages/notification-center/rule-table/escalation-form.component.scss diff --git a/ui-ngx/src/app/modules/home/pages/notification-center/notification-table/rule-table-config.ts b/ui-ngx/src/app/modules/home/pages/notification-center/notification-table/rule-table-config.ts index 732fa8e16e..b62c111509 100644 --- a/ui-ngx/src/app/modules/home/pages/notification-center/notification-table/rule-table-config.ts +++ b/ui-ngx/src/app/modules/home/pages/notification-center/notification-table/rule-table-config.ts @@ -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 { 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 { this.columns.push( new EntityTableColumn('name', 'notification.rule-name', '30%'), - new EntityTableColumn('templateId', 'notification.template', '30%', - (rule) => `${rule.templateId}`, + new EntityTableColumn('templateId', 'notification.template', '15%', + (rule) => `${rule.templateId.id}`, () => ({}), false), - new EntityTableColumn('configuration.description', 'notification.description', '40%', - (rule) => rule.configuration.description || '', - () => ({}), false) + new EntityTableColumn('triggerType', 'notification.trigger.trigger', '15%', + (rule) => this.translate.instant(TriggerTypeTranslationMap.get(rule.triggerType)) || '', + () => ({}), true) ); } diff --git a/ui-ngx/src/app/modules/home/pages/notification-center/rule-table/escalation-form.component.html b/ui-ngx/src/app/modules/home/pages/notification-center/rule-table/escalation-form.component.html index ab12a8d541..b6bac29dfb 100644 --- a/ui-ngx/src/app/modules/home/pages/notification-center/rule-table/escalation-form.component.html +++ b/ui-ngx/src/app/modules/home/pages/notification-center/rule-table/escalation-form.component.html @@ -17,22 +17,23 @@ -->
-
-
-
notification.first-recipient
-
- After +
+
+
notification.first-recipient
+
+ After - notification.notify + [disabledAdvanced]="true"> + notification.notify
-
+
diff --git a/ui-ngx/src/app/modules/home/pages/notification-center/rule-table/escalation-form.component.scss b/ui-ngx/src/app/modules/home/pages/notification-center/rule-table/escalation-form.component.scss new file mode 100644 index 0000000000..f80a0a7cef --- /dev/null +++ b/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; + } +} diff --git a/ui-ngx/src/app/modules/home/pages/notification-center/rule-table/escalation-form.component.ts b/ui-ngx/src/app/modules/home/pages/notification-center/rule-table/escalation-form.component.ts index f68f6e2bc5..3c0ebf5b14 100644 --- a/ui-ngx/src/app/modules/home/pages/notification-center/rule-table/escalation-form.component.ts +++ b/ui-ngx/src/app/modules/home/pages/notification-center/rule-table/escalation-form.component.ts @@ -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 { diff --git a/ui-ngx/src/app/modules/home/pages/notification-center/rule-table/escalations.component.ts b/ui-ngx/src/app/modules/home/pages/notification-center/rule-table/escalations.component.ts index fa8e90ce6c..578355e8d3 100644 --- a/ui-ngx/src/app/modules/home/pages/notification-center/rule-table/escalations.component.ts +++ b/ui-ngx/src/app/modules/home/pages/notification-center/rule-table/escalations.component.ts @@ -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 | null): void { - if (this.valueChangeSubscription$) { - this.valueChangeSubscription$.unsubscribe(); - } - const escalationsControls: Array = []; - if (escalations) { - escalations.forEach((escalation, index) => { - escalationsControls.push(this.fb.control(escalation, [Validators.required])); - }); - } else { - escalationsControls.push(this.fb.control(this.mainEscalaion, [Validators.required])); - } - this.escalationsFormGroup.setControl('escalations', this.fb.array(escalationsControls)); - if (this.disabled) { - this.escalationsFormGroup.disable({emitEvent: false}); + if (escalations?.length === this.escalationsFormArray.length) { + this.escalationsFormArray.patchValue(escalations, {emitEvent: false}); } else { - this.escalationsFormGroup.enable({emitEvent: false}); + const escalationsControls: Array = []; + if (escalations) { + escalations.forEach((escalation, index) => { + escalationsControls.push(this.fb.control(escalation, [Validators.required])); + }); + } else { + escalationsControls.push(this.fb.control(this.mainEscalaion, [Validators.required])); + } + this.escalationsFormGroup.setControl('escalations', this.fb.array(escalationsControls), {emitEvent: false}); + 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 = this.escalationsFormGroup.get('escalations').value; + const escalations = this.escalationsFormGroup.get('escalations').value; this.propagateChange(escalations); } } diff --git a/ui-ngx/src/app/modules/home/pages/notification-center/rule-table/rule-notification-dialog.component.html b/ui-ngx/src/app/modules/home/pages/notification-center/rule-table/rule-notification-dialog.component.html index 344f5a96ad..5a8c963b89 100644 --- a/ui-ngx/src/app/modules/home/pages/notification-center/rule-table/rule-notification-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/notification-center/rule-table/rule-notification-dialog.component.html @@ -15,224 +15,231 @@ limitations under the License. --> - - -

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

- - -
- - -
-
- - - check - - - {{ 'notification.basic-settings' | translate }} - - - notification.rule-name - - - {{ 'notification.rule-name-required' | translate }} - - - - - - notification.trigger.trigger - - - {{ triggerTypeTranslationMap.get(trigger) | translate }} - - - - {{ 'notification.trigger.trigger-required' | translate }} - - - - - - {{ 'notification.type-settings' | translate }} -
-
- notification.filter - - alarm.alarm-type-list - - - {{type}} - cancel - - - +
+ +

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

+ + +
+ + +
+
+ + + check + + + {{ 'notification.basic-settings' | translate }} + + + notification.rule-name + + + {{ 'notification.rule-name-required' | translate }} + - - - alarm.alarm-severity-list - - - {{ alarmSeverityTranslationMap.get(severity) | translate }} - cancel - - - - - - - - -
-
- notification.no-severity-found -
- - - {{ translate.get('notification.no-severity-matching', - {severity: truncate.transform(severitySearchText, true, 6, '...')}) | async }} - - -
-
-
-
-
- -
- notification.clear-rule - - alarm.alarm-status-list - - - {{ alarmSearchStatusTranslationMap.get(searchStatus) | translate }} + + + + notification.trigger.trigger + + + {{ triggerTypeTranslationMap.get(trigger) | translate }} + + {{ 'notification.trigger.trigger-required' | translate }} + -
+
+
+ + {{ 'notification.type-settings' | translate }} +
+
+ notification.filter + + alarm.alarm-type-list + + + {{type}} + cancel + + + + -
- notification.hierarchy-of-receiving - -
+ + alarm.alarm-severity-list + + + {{ alarmSeverityTranslationMap.get(severity) | translate }} + cancel + + + + + + + + +
+
+ notification.no-severity-found +
+ + + {{ translate.get('notification.no-severity-matching', + {severity: truncate.transform(severitySearchText, true, 6, '...')}) | async }} + + +
+
+
+
+
+ +
+ notification.clear-rule + + alarm.alarm-status-list + + + {{ alarmSearchStatusTranslationMap.get(searchStatus) | translate }} + + + +
+ +
+ notification.hierarchy-of-receiving + +
- - notification.description - - -
-
+ + notification.description + + + + - - {{ 'notification.type-settings' | translate }} -
- notification.filter-by -
- - {{ 'notification.device' | translate }} - {{ 'notification.device-profile' | translate }} - -
- - - - - - - - notification.description - - -
-
+ + {{ 'notification.type-settings' | translate }} +
+ notification.filter-by +
+ + {{ 'notification.device' | translate }} + {{ 'notification.device-profile' | translate }} + +
+ + + + + + + + notification.description + + +
+
- - {{ 'notification.type-settings' | translate }} -
-
- notification.filter - - -
- notification.status - {{ 'notification.created' | translate }} - {{ 'notification.updated' | translate }} - {{ 'notification.deleted' | translate }} -
-
- - - - notification.description - - -
-
-
-
- -
- - - -
+ + {{ 'notification.type-settings' | translate }} +
+
+ notification.filter + + +
+ notification.status + {{ 'notification.created' | translate }} + {{ 'notification.updated' | translate }} + {{ 'notification.deleted' | translate }} +
+
+ + + + notification.description + + +
+
+ +
+ +
+ + + +
+
diff --git a/ui-ngx/src/app/modules/home/pages/notification-center/rule-table/rule-notification-dialog.component.scss b/ui-ngx/src/app/modules/home/pages/notification-center/rule-table/rule-notification-dialog.component.scss index 308c7170b2..7368d011fb 100644 --- a/ui-ngx/src/app/modules/home/pages/notification-center/rule-table/rule-notification-dialog.component.scss +++ b/ui-ngx/src/app/modules/home/pages/notification-center/rule-table/rule-notification-dialog.component.scss @@ -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 { diff --git a/ui-ngx/src/app/modules/home/pages/notification-center/rule-table/rule-notification-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/notification-center/rule-table/rule-notification-dialog.component.ts index 24f3068b13..bef3aad67f 100644 --- a/ui-ngx/src/app/modules/home/pages/notification-center/rule-table/rule-notification-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/pages/notification-center/rule-table/rule-notification-dialog.component.ts @@ -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(); - private readonly destroy$ = new Subject(); + private destroy$ = new Subject(); constructor(protected store: Store, 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) ); diff --git a/ui-ngx/src/app/shared/models/notification.models.ts b/ui-ngx/src/app/shared/models/notification.models.ts index ad700f86f7..953a51e914 100644 --- a/ui-ngx/src/app/shared/models/notification.models.ts +++ b/ui-ngx/src/app/shared/models/notification.models.ts @@ -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, 'label'>{ tenantId: TenantId; templateId: NotificationTemplateId; - // deliveryMethods: Array; - configuration: NotificationRuleConfig; -} - -export interface NotificationRuleConfig { - initialNotificationTargetId: NotificationTargetId; - escalationConfig: NotificationEscalationConfig; - description?: string; -} - -export interface NotificationEscalationConfig { - escalations: Array; + triggerType: TriggerType; + triggerConfig: NotificationRuleTriggerConfig; + recipientConfig: NotificationRuleRecipientConfig; +} + +export interface NotificationRuleTriggerConfig { + alarmTypes?: Array; + alarmSeverities?: Array; + clearRule?: AlarmStatus; + devices?: Array; + devicesProfiles?: Array; + entityType?: EntityType; + created?: boolean; + updated?: boolean; + deleted?: boolean; +} + +export interface NotificationRuleRecipientConfig { + targets?: Array; + escalationTable?: {[key: string]: Array}; } export interface NonConfirmedNotificationEscalation { diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index c77fbc5b98..6b039e3e87 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -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", From 5d686b7dc69381beead94a48c182ece26136c9b0 Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Wed, 1 Feb 2023 02:02:27 +0200 Subject: [PATCH 4/5] UI: Notification rule --- .../notification-table/rule-table-config.ts | 27 +++++--- .../rule-table/escalation-form.component.html | 4 +- .../rule-table/escalation-form.component.ts | 1 + .../rule-notification-dialog.component.html | 10 +-- .../rule-notification-dialog.component.ts | 64 ++++++++++++++++++- .../app/shared/models/notification.models.ts | 3 +- .../assets/locale/locale.constant-en_US.json | 2 + 7 files changed, 90 insertions(+), 21 deletions(-) diff --git a/ui-ngx/src/app/modules/home/pages/notification-center/notification-table/rule-table-config.ts b/ui-ngx/src/app/modules/home/pages/notification-center/notification-table/rule-table-config.ts index b62c111509..b89bdada7d 100644 --- a/ui-ngx/src/app/modules/home/pages/notification-center/notification-table/rule-table-config.ts +++ b/ui-ngx/src/app/modules/home/pages/notification-center/notification-table/rule-table-config.ts @@ -62,7 +62,7 @@ export class RuleTableConfig extends EntityTableConfig { 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); @@ -70,25 +70,31 @@ export class RuleTableConfig extends EntityTableConfig { this.columns.push( new EntityTableColumn('name', 'notification.rule-name', '30%'), - new EntityTableColumn('templateId', 'notification.template', '15%', - (rule) => `${rule.templateId.id}`, - () => ({}), false), - new EntityTableColumn('triggerType', 'notification.trigger.trigger', '15%', + new EntityTableColumn('templateName', 'notification.template', '20%'), + new EntityTableColumn('triggerType', 'notification.trigger.trigger', '20%', (rule) => this.translate.instant(TriggerTypeTranslationMap.get(rule.triggerType)) || '', - () => ({}), true) + () => ({}), true), + new EntityTableColumn('additionalConfig.description', 'notification.description', '30%', + (target) => target.additionalConfig.description || '', + () => ({}), false) ); } private configureCellActions(): Array> { return [{ - name: this.translate.instant('device.make-public'), + 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.editTarget($event, entity) + onAction: ($event, entity) => this.editRule($event, entity) }]; } - private editTarget($event: Event, rule: NotificationRule, isAdd = false) { + private editRule($event: Event, rule: NotificationRule, isAdd = false, isCopy = false) { if ($event) { $event.stopPropagation(); } @@ -98,6 +104,7 @@ export class RuleTableConfig extends EntityTableConfig { panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], data: { isAdd, + isCopy, rule } }).afterClosed() @@ -111,7 +118,7 @@ export class RuleTableConfig extends EntityTableConfig { private onTargetAction(action: EntityAction): boolean { switch (action.action) { case 'add': - this.editTarget(action.event, action.entity, true); + this.editRule(action.event, action.entity, true); return true; } return false; diff --git a/ui-ngx/src/app/modules/home/pages/notification-center/rule-table/escalation-form.component.html b/ui-ngx/src/app/modules/home/pages/notification-center/rule-table/escalation-form.component.html index b6bac29dfb..397ced14d6 100644 --- a/ui-ngx/src/app/modules/home/pages/notification-center/rule-table/escalation-form.component.html +++ b/ui-ngx/src/app/modules/home/pages/notification-center/rule-table/escalation-form.component.html @@ -19,8 +19,8 @@
-
notification.first-recipient
-
+
notification.first-recipient
+
After
-

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

+

{{ dialogTitle | translate }}