Browse Source

UI: Add rule notification

pull/8022/head
Artem Dzhereleiko 4 years ago
parent
commit
c3cc342393
  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.module.ts
  3. 41
      ui-ngx/src/app/modules/home/pages/notification-center/rule-table/escalation-form.component.html
  4. 139
      ui-ngx/src/app/modules/home/pages/notification-center/rule-table/escalation-form.component.ts
  5. 45
      ui-ngx/src/app/modules/home/pages/notification-center/rule-table/escalations.component.html
  6. 172
      ui-ngx/src/app/modules/home/pages/notification-center/rule-table/escalations.component.ts
  7. 332
      ui-ngx/src/app/modules/home/pages/notification-center/rule-table/rule-notification-dialog.component.html
  8. 59
      ui-ngx/src/app/modules/home/pages/notification-center/rule-table/rule-notification-dialog.component.scss
  9. 302
      ui-ngx/src/app/modules/home/pages/notification-center/rule-table/rule-notification-dialog.component.ts
  10. 2
      ui-ngx/src/app/shared/components/time/timeinterval.component.html
  11. 2
      ui-ngx/src/app/shared/components/time/timeinterval.component.ts
  12. 37
      ui-ngx/src/app/shared/models/entity-type.models.ts
  13. 12
      ui-ngx/src/app/shared/models/notification.models.ts
  14. 36
      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.module.ts

@ -44,6 +44,8 @@ import {
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 { 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 { 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: [
@ -57,7 +59,9 @@ import { RuleNotificationDialogComponent } from '@home/pages/notification-center
TemplateAutocompleteComponent, TemplateAutocompleteComponent,
InboxTableHeaderComponent, InboxTableHeaderComponent,
RuleTableHeaderComponent, RuleTableHeaderComponent,
RuleNotificationDialogComponent RuleNotificationDialogComponent,
EscalationsComponent,
EscalationFormComponent
], ],
imports: [ imports: [
CommonModule, CommonModule,

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

@ -0,0 +1,41 @@
<!--
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 fxLayout="row" style="align-items: center; min-height: 74px; border-radius: 8px; background: rgba(0,0,0,0.04);">
<div fxFlex fxLayout="row">
<div fxFlex *ngIf="systemEscalation" style="padding: 0 10px;" translate>notification.first-recipient</div>
<div fxFlex *ngIf="!systemEscalation" fxLayout="row" style="align-items: center;">
<span style="padding: 0 10px;">After</span>
<tb-timeinterval
formControlName="delayInSec"
[disabledAdvanced]="true"
style="padding-top: 8px; width: 150px; min-width: 150px;"></tb-timeinterval>
<span fxFlex style="text-align: end; padding: 0 10px;" translate>notification.notify</span>
</div>
</div>
<div fxFlex style="padding: 0 10px;">
<tb-entity-list
required
formControlName="notificationTargetId"
[entityType]="entityType.NOTIFICATION_TARGET"
placeholderText="notification.add-target">
</tb-entity-list>
</div>
</div>
</form>

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

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 { 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<AppState>,
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<NonConfirmedNotificationEscalation> | null): void {
if (this.valueChangeSubscription$) {
this.valueChangeSubscription$.unsubscribe();
}
const escalationsControls: Array<AbstractControl> = [];
if (escalations) {
escalations.forEach((escalation, index) => {
escalationsControls.push(this.fb.control(escalation, [Validators.required]));
});
} 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<NonConfirmedNotificationEscalation> = this.escalationsFormGroup.get('escalations').value;
this.propagateChange(escalations);
}
}

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

@ -15,118 +15,224 @@
limitations under the License. limitations under the License.
--> -->
<form [formGroup]="ruleNotificationForm" style="min-width: 560px">
<mat-toolbar color="primary">
<h2>{{ (isAdd ? 'notification.add-notification-target' : 'notification.edit-notification-target') | translate }}</h2>
<span fxFlex></span>
<button mat-icon-button
(click)="cancel()"
type="button">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
<div mat-dialog-content>
<mat-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>
<section formGroupName="configuration">
<fieldset class="fields-group"> <mat-toolbar color="primary">
<span class="fields-group-title" translate>notification.hierarchy-of-receiving</span> <h2>{{'notification.add-rule' | translate }}</h2>
<div class="rule-header"> <span fxFlex></span>
<div fxLayoutAlign="row"> <button mat-icon-button
<span fxFlex="30" style="padding-left: 12px" translate>notification.rule</span> (click)="cancel()"
<span fxFlex translate>notification.recipient</span> type="button">
</div> <mat-icon class="material-icons">close</mat-icon>
</div> </button>
<div class="rules" style="padding: 16px 0"> </mat-toolbar>
<div class="rule" fxFlex fxLayout="row" fxLayoutAlign="start center"> <mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
<div fxLayoutAlign="row" fxLayoutAlign="start center" style="display: inline-block; width: 100%; min-height: 64px; height: 100%; background: rgba(0,0,0, 0.04); border-radius: 8px;"> </mat-progress-bar>
<div fxFlex="30" *ngIf="false"> <div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
<span style="padding-left: 12px" >First recipient</span> <div mat-dialog-content>
<span style="padding-left: 12px" *ngIf="true">After</span> <mat-horizontal-stepper [linear]="true" labelPosition="end" #addNotificationRule [orientation]="(stepperOrientation | async)"
</div> (selectionChange)="changeStep($event)">
<div fxFlex="30" *ngIf="true"> <ng-template matStepperIcon="edit">
<span style="padding-left: 12px" *ngIf="true">After</span> <mat-icon>check</mat-icon>
<tb-timeinterval </ng-template>
formControlName="interval" <mat-step optional [stepControl]="ruleNotificationForm">
[hideFlag]="true" <ng-template matStepLabel>{{ 'notification.basic-settings' | translate }}</ng-template>
predefinedName="aggregation.group-interval"> <form [formGroup]="ruleNotificationForm" style="padding-bottom: 16px;">
</tb-timeinterval> <mat-form-field class="mat-block">
</div> <mat-label translate>notification.rule-name</mat-label>
<tb-entity-list <input matInput formControlName="name" required>
style="padding: 0 12px" <mat-error *ngIf="ruleNotificationForm.get('name').hasError('required')">
fxFlex {{ 'notification.rule-name-required' | translate }}
formControlName="ids" </mat-error>
[required]="true" </mat-form-field>
[entityType]="entityType.NOTIFICATION_TEMPLATE"> <tb-template-autocomplete
</tb-entity-list> required
</div> formControlName="templateId"
<button mat-icon-button> labelText="notification.template-name"
<mat-icon>close</mat-icon> placeholderText="notification.template-name"
</button> requiredText="notification.template-required">
</div> </tb-template-autocomplete>
<mat-form-field class="mat-block">
<mat-label translate>notification.trigger.trigger</mat-label>
<mat-select formControlName="trigger" required>
<mat-option *ngFor="let trigger of triggerTypes" [value]="trigger">
{{ triggerTypeTranslationMap.get(trigger) | translate }}
</mat-option>
</mat-select>
<mat-error *ngIf="ruleNotificationForm.get('trigger').hasError('required')">
{{ 'notification.trigger.trigger-required' | translate }}
</mat-error>
</mat-form-field>
</form>
</mat-step>
<mat-step *ngIf="ruleNotificationForm.get('trigger').value === triggerType.ALARM"
[stepControl]="alarmTemplateForm">
<ng-template matStepLabel>{{ 'notification.type-settings' | translate }}</ng-template>
<form [formGroup]="alarmTemplateForm">
<fieldset class="fields-group">
<span class="fields-group-title" translate>notification.filter</span>
<mat-form-field fxFlex class="mat-block" floatLabel="always">
<mat-label translate>alarm.alarm-type-list</mat-label>
<mat-chip-list #alarmTypeChipList formControlName="alarmTypeList">
<mat-chip *ngFor="let type of alarmTypeList()" [selectable]="true"
[removable]="true" (removed)="removeAlarmType(type)">
{{type}}
<mat-icon matChipRemove>cancel</mat-icon>
</mat-chip>
<input placeholder="{{ !alarmTemplateForm.get('alarmTypeList').value?.length ? ('alarm.any-type' | translate) : '' }}"
[matChipInputFor]="alarmTypeChipList"
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
matChipInputAddOnBlur
(matChipInputTokenEnd)="addAlarmType($event)">
</mat-chip-list>
</mat-form-field>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>alarm.alarm-severity-list</mat-label>
<mat-chip-list #severitiesChipList
required>
<mat-chip *ngFor="let severity of alarmTemplateForm.get('alarmSeverityList').value"
[removable]="true" (removed)="onSeverityRemoved(severity)">
{{ alarmSeverityTranslationMap.get(severity) | translate }}
<mat-icon matChipRemove>cancel</mat-icon>
</mat-chip>
<input matInput
type="text"
placeholder="{{ !alarmTemplateForm.get('alarmSeverityList').value?.length ? ('alarm.any-severity' | translate) : '' }}"
style="max-width: 200px;"
#severityInput
(focusin)="onSeverityInputFocus()"
matAutocompleteOrigin
#origin="matAutocompleteOrigin"
(input)="severityInputChange.next(severityInput.value)"
[matAutocompleteConnectedTo]="origin"
[matAutocomplete]="severityAutocomplete"
[matChipInputFor]="severitiesChipList"
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
(matChipInputTokenEnd)="addSeverityFromChipInput($event)">
</mat-chip-list>
<mat-autocomplete #severityAutocomplete="matAutocomplete"
class="tb-autocomplete"
(optionSelected)="severitySelected($event)"
[displayWith]="displaySeverityFn.bind(this)">
<mat-option *ngFor="let severity of filteredDisplaySeverities | async" [value]="severity">
<span [innerHTML]="alarmSeverityTranslationMap.get(alarmSeverityEnum[severity]) | translate | highlight:severitySearchText"></span>
</mat-option>
<mat-option *ngIf="(filteredDisplaySeverities | async)?.length === 0" [value]="null" class="tb-not-found">
<div class="tb-not-found-content" (click)="$event.stopPropagation()">
<div *ngIf="!textIsNotEmpty(severitySearchText); else searchNotEmpty">
<span translate>notification.no-severity-found</span>
</div>
<ng-template #searchNotEmpty>
<span>
{{ translate.get('notification.no-severity-matching',
{severity: truncate.transform(severitySearchText, true, 6, &apos;...&apos;)}) | async }}
</span>
</ng-template>
</div>
</mat-option>
</mat-autocomplete>
</mat-form-field>
</fieldset>
<fieldset class="fields-group">
<span class="fields-group-title" translate>notification.clear-rule</span>
<mat-form-field fxFlex class="mat-block" floatLabel="always">
<mat-label translate>alarm.alarm-status-list</mat-label>
<mat-select formControlName="alarmStatusList"
placeholder="{{ !alarmTemplateForm.get('alarmStatusList').value?.length ? ('alarm.any-status' | translate) : '' }}">
<mat-option *ngFor="let searchStatus of alarmSearchStatuses" [value]="searchStatus">
{{ alarmSearchStatusTranslationMap.get(searchStatus) | translate }}
</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="escalationConfig"></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('trigger').value === triggerType.DEVICE_INACTIVITY"
[stepControl]="deviceInactivityTemplateForm">
<ng-template matStepLabel>{{ 'notification.type-settings' | translate }}</ng-template>
<form [formGroup]="deviceInactivityTemplateForm">
<span translate>notification.filter-by</span>
<div fxFlex fxLayoutAlign="center center">
<mat-button-toggle-group class="tb-notification-unread-toggle-group"
style="width: 250px;"
formControlName="filterByDevice">
<mat-button-toggle fxFlex [value]=true>{{ 'notification.device' | translate }}</mat-button-toggle>
<mat-button-toggle fxFlex [value]=false>{{ 'notification.device-profile' | translate }}</mat-button-toggle>
</mat-button-toggle-group>
</div> </div>
<button mat-stroked-button color="primary"> <tb-device-profile-autocomplete
<div fxLayout="row" fxLayoutAlign="start center"> *ngIf="!deviceInactivityTemplateForm.get('filterByDevice').value"
<mat-icon>add</mat-icon>{{ 'notification.add-rule' | translate }} formControlName="deviceProfileId"
</div> [editProfileEnabled]="false">
</button> </tb-device-profile-autocomplete>
</fieldset> <tb-entity-autocomplete
<!-- <mat-form-field class="mat-block">--> *ngIf="deviceInactivityTemplateForm.get('filterByDevice').value"
<!-- <mat-label translate>notification.target-type.type</mat-label>--> formControlName="deviceId"
<!-- <mat-select formControlName="type">--> [entityType]="entityType.DEVICE">
<!-- <mat-option *ngFor="let type of notificationTargetConfigTypes" [value]="type">--> </tb-entity-autocomplete>
<!-- {{ notificationTargetConfigTypeTranslateMap.get(type) | translate }}--> <tb-entity-list
<!-- </mat-option>--> required
<!-- </mat-select>--> formControlName="notificationTargetId"
<!-- </mat-form-field>--> [entityType]="entityType.NOTIFICATION_TARGET"
<!-- <tb-entity-list--> placeholderText="notification.target">
<!-- required--> </tb-entity-list>
<!-- formControlName="usersIds"--> <mat-form-field class="mat-block">
<!-- [entityType]="entityType.USER"--> <mat-label translate>notification.description</mat-label>
<!-- *ngIf="targetNotificationForm.get('configuration.type').value === notificationTargetConfigType.USER_LIST">--> <input matInput formControlName="description">
<!-- </tb-entity-list>--> </mat-form-field>
<!-- <section--> </form>
<!-- *ngIf="targetNotificationForm.get('configuration.type').value === notificationTargetConfigType.CUSTOMER_USERS">--> </mat-step>
<!-- <mat-slide-toggle formControlName="getCustomerIdFromOriginatorEntity">-->
<!-- {{ 'notification.get-customer-id-from-originator' | translate }}--> <mat-step optional *ngIf="ruleNotificationForm.get('trigger').value === triggerType.ENTITY_ACTION"
<!-- </mat-slide-toggle>--> [stepControl]="entityActionTemplateForm">
<!-- <tb-entity-autocomplete--> <ng-template matStepLabel>{{ 'notification.type-settings' | translate }}</ng-template>
<!-- required--> <form [formGroup]="entityActionTemplateForm">
<!-- formControlName="customerId"--> <fieldset class="fields-group">
<!-- [entityType]="entityType.CUSTOMER"--> <span class="fields-group-title" translate>notification.filter</span>
<!-- *ngIf="!targetNotificationForm.get('configuration.getCustomerIdFromOriginatorEntity').value">--> <tb-entity-type-select required
<!-- </tb-entity-autocomplete>--> showLabel
<!-- </section>--> [allowedEntityTypes]="entityTypes"
<mat-form-field class="mat-block"> formControlName="entityType">
<mat-label translate>notification.description</mat-label> </tb-entity-type-select>
<input matInput formControlName="description"> <section formGroupName="status" fxLayout="column" fxLayoutGap="10px">
</mat-form-field> <span fxFlex translate>notification.status</span>
</section> <mat-checkbox fxFlex formControlName="created" translate>{{ 'notification.created' | translate }}</mat-checkbox>
</div> <mat-checkbox fxFlex formControlName="updated" translate>{{ 'notification.updated' | translate }}</mat-checkbox>
<mat-divider></mat-divider> <mat-checkbox fxFlex formControlName="deleted" translate>{{ 'notification.deleted' | translate }}</mat-checkbox>
<div mat-dialog-actions fxLayout="row" fxLayoutGap="8px" fxLayoutAlign="end"> </section>
<button mat-button </fieldset>
[disabled]="(isLoading$ | async)" <tb-entity-list
(click)="cancel()">{{ 'action.cancel' | translate }}</button> required
<button mat-raised-button formControlName="notificationTargetId"
[disabled]="(isLoading$ | async) || ruleNotificationForm.invalid || !ruleNotificationForm.dirty" [entityType]="entityType.NOTIFICATION_TARGET"
color="primary" placeholderText="notification.target">
(click)="save()">{{ (isAdd ? 'action.add' : 'action.save') | translate }}</button> </tb-entity-list>
</div> <mat-form-field class="mat-block">
</form> <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>

59
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 * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
@import "../../../../../../theme";
:host { :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 { .fields-group {
padding: 0 16px 8px; padding: 0 16px 8px;
margin-bottom: 10px; margin-bottom: 10px;

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

@ -14,13 +14,8 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { import { NotificationRule, TriggerType, TriggerTypeTranslationMap } from '@shared/models/notification.models';
NotificationRule, import { Component, ElementRef, Inject, OnDestroy, ViewChild } from '@angular/core';
NotificationTarget,
NotificationTargetConfigType,
NotificationTargetConfigTypeTranslateMap
} from '@shared/models/notification.models';
import { Component, Inject, OnDestroy } from '@angular/core';
import { DialogComponent } from '@shared/components/dialog.component'; import { DialogComponent } from '@shared/components/dialog.component';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; 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 { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { NotificationService } from '@core/http/notification.service'; import { NotificationService } from '@core/http/notification.service';
import { EntityType } from '@shared/models/entity-type.models'; import { EntityType, entityTypeTranslations } from '@shared/models/entity-type.models';
import { deepTrim, isDefined } from '@core/utils'; import { deepTrim, isDefined } from '@core/utils';
import { Subject } from 'rxjs'; import { Observable, of, Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators'; 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 { export interface RuleNotificationDialogData {
rule?: NotificationRule; rule?: NotificationRule;
@ -46,20 +57,56 @@ export interface RuleNotificationDialogData {
export class RuleNotificationDialogComponent extends export class RuleNotificationDialogComponent extends
DialogComponent<RuleNotificationDialogComponent, NotificationRule> implements OnDestroy { 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; ruleNotificationForm: FormGroup;
// notificationTargetConfigType = NotificationTargetConfigType; alarmTemplateForm: FormGroup;
// notificationTargetConfigTypes: NotificationTargetConfigType[] = Object.values(NotificationTargetConfigType); deviceInactivityTemplateForm: FormGroup;
// notificationTargetConfigTypeTranslateMap = NotificationTargetConfigTypeTranslateMap; 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; entityType = EntityType;
entityTypes = Object.values(EntityType);
isAdd = true; isAdd = true;
selectedIndex = 0;
filteredDisplaySeverities: Observable<Array<string>>;
severitySearchText = '';
severityInputChange = new Subject<string>();
private readonly destroy$ = new Subject<void>(); private readonly destroy$ = new Subject<void>();
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
protected router: Router, protected router: Router,
protected dialogRef: MatDialogRef<RuleNotificationDialogComponent, NotificationRule>, protected dialogRef: MatDialogRef<RuleNotificationDialogComponent, NotificationRule>,
@Inject(MAT_DIALOG_DATA) public data: RuleNotificationDialogData, @Inject(MAT_DIALOG_DATA) public data: RuleNotificationDialogData,
private breakpointObserver: BreakpointObserver,
private fb: FormBuilder, private fb: FormBuilder,
public translate: TranslateService,
public truncate: TruncatePipe,
private notificationService: NotificationService) { private notificationService: NotificationService) {
super(store, router, dialogRef); super(store, router, dialogRef);
@ -67,24 +114,231 @@ export class RuleNotificationDialogComponent extends
this.isAdd = 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({ this.ruleNotificationForm = this.fb.group({
name: [null, Validators.required], name: [null, Validators.required],
templateId: [null, Validators.required], templateId: [null, Validators.required],
trigger: [this.triggerType.ALARM, Validators.required],
configuration: this.fb.group({ configuration: this.fb.group({
ids: [], escalationConfig: this.fb.group({
interval: [], escalations: [[]]
// type: [NotificationTargetConfigType.ALL_USERS], }),
description: [null], 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.alarmTemplateForm = this.fb.group({
// this.targetNotificationForm.patchValue(data.target, {emitEvent: false}); alarmTypeList: [[], Validators.required],
// this.targetNotificationForm.get('configuration.type').updateValueAndValidity({onlySelf: true}); 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<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('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() { ngOnDestroy() {

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'
}
] ]
] ]
); );

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

@ -262,3 +262,15 @@ export const NotificationTemplateTypeTranslateMap = new Map<NotificationType, No
hint: 'hint' hint: 'hint'
}] }]
]); ]);
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']
]);

36
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} }", "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",
@ -2768,11 +2775,34 @@
"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": "Rule name",
"rule-name-required": "Name is required", "rule-name-required": "Name is required",
"template-name": "Template name", "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": { "ota-update": {
"add": "Add package", "add": "Add package",

Loading…
Cancel
Save