Browse Source

UI: Add support new notification rule Rate limits and improve string items list

pull/8702/head
Vladyslav_Prykhodko 3 years ago
parent
commit
4dbe1cf004
  1. 2
      ui-ngx/src/app/modules/common/modules-map.ts
  2. 10
      ui-ngx/src/app/modules/home/pages/notification/rule/rule-notification-dialog.component.html
  3. 11
      ui-ngx/src/app/modules/home/pages/notification/rule/rule-notification-dialog.component.ts
  4. 65
      ui-ngx/src/app/shared/components/string-items-list.component.html
  5. 167
      ui-ngx/src/app/shared/components/string-items-list.component.ts
  6. 43
      ui-ngx/src/app/shared/models/limited-api.models.ts
  7. 7
      ui-ngx/src/app/shared/models/notification.models.ts
  8. 1
      ui-ngx/src/app/shared/models/public-api.ts
  9. 17
      ui-ngx/src/assets/locale/locale.constant-en_US.json

2
ui-ngx/src/app/modules/common/modules-map.ts

@ -177,6 +177,7 @@ import * as CopyButtonComponent from '@shared/components/button/copy-button.comp
import * as TogglePasswordComponent from '@shared/components/button/toggle-password.component';
import * as ProtobufContentComponent from '@shared/components/protobuf-content.component';
import * as SlackConversationAutocompleteComponent from '@shared/components/slack-conversation-autocomplete.component';
import * as StringItemsListComponent from '@shared/components/string-items-list.component';
import * as AddEntityDialogComponent from '@home/components/entity/add-entity-dialog.component';
import * as EntitiesTableComponent from '@home/components/entity/entities-table.component';
@ -472,6 +473,7 @@ class ModulesMap implements IModulesMap {
'@shared/components/button/toggle-password.component': TogglePasswordComponent,
'@shared/components/protobuf-content.component': ProtobufContentComponent,
'@shared/components/slack-conversation-autocomplete.component': SlackConversationAutocompleteComponent,
'@shared/components/string-items-list.component': StringItemsListComponent,
'@home/components/entity/add-entity-dialog.component': AddEntityDialogComponent,
'@home/components/entity/entities-table.component': EntitiesTableComponent,

10
ui-ngx/src/app/modules/home/pages/notification/rule/rule-notification-dialog.component.html

@ -499,6 +499,16 @@
<mat-step *ngIf="ruleNotificationForm.get('triggerType').value === triggerType.RATE_LIMITS"
[stepControl]="rateLimitsTemplateForm">
<ng-template matStepLabel>{{ 'notification.rate-limits-trigger-settings' | translate }}</ng-template>
<form [formGroup]="rateLimitsTemplateForm">
<section formGroupName="triggerConfig">
<tb-string-items-list
label="{{ 'notification.rate-limits' | translate }}"
hint="{{ 'notification.api-feature-hint' | translate }}"
[predefinedValues]="limitedApis"
formControlName="apis">
</tb-string-items-list>
</section>
</form>
<form [formGroup]="ruleNotificationForm">
<section formGroupName="additionalConfig">
<mat-form-field class="mat-block">

11
ui-ngx/src/app/modules/home/pages/notification/rule/rule-notification-dialog.component.ts

@ -66,6 +66,8 @@ import {
ApiUsageStateValue,
ApiUsageStateValueTranslationMap
} from '@shared/models/api-usage.models';
import { LimitedApi, LimitedApiTranslationMap } from '@shared/models/limited-api.models';
import { StringItemsOption } from '@shared/components/string-items-list.component';
export interface RuleNotificationDialogData {
rule?: NotificationRule;
@ -130,6 +132,8 @@ export class RuleNotificationDialogComponent extends
apiFeatures: ApiFeature[] = Object.values(ApiFeature);
apiFeatureTranslationMap = ApiFeatureTranslationMap;
limitedApis: StringItemsOption[];
entityType = EntityType;
isAdd = true;
@ -172,6 +176,11 @@ export class RuleNotificationDialogComponent extends
this.isAdd = data.isAdd;
}
this.limitedApis = Object.values(LimitedApi).map(value => ({
name: this.translate.instant(LimitedApiTranslationMap.get(value)),
value
}));
this.stepperOrientation = this.breakpointObserver.observe(MediaBreakpoints['gt-xs'])
.pipe(map(({matches}) => matches ? 'horizontal' : 'vertical'));
@ -305,7 +314,7 @@ export class RuleNotificationDialogComponent extends
this.rateLimitsTemplateForm = this.fb.group({
triggerConfig: this.fb.group({
apis: []
})
});

65
ui-ngx/src/app/shared/components/string-items-list.component.html

@ -15,24 +15,47 @@
limitations under the License.
-->
<section [formGroup]="stringItemsForm">
<mat-form-field fxFlex class="mat-block" [floatLabel]="floatLabel" [appearance]="appearance" [subscriptSizing]="subscriptSizing">
<mat-label *ngIf="label">{{ label }}</mat-label>
<mat-chip-grid #itemsChipList formControlName="items" [required]="required">
<mat-chip-row *ngFor="let item of stringItemsList"
[removable]="!disabled"
[contentEditable]="editable && !disabled"
(removed)="removeItems(item)">
{{item}}
<mat-icon matChipRemove *ngIf="!disabled">close</mat-icon>
</mat-chip-row>
<input placeholder="{{ placeholder }}"
[matChipInputFor]="itemsChipList"
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
matChipInputAddOnBlur
(matChipInputTokenEnd)="addItem($event)">
</mat-chip-grid>
<mat-hint>{{ hint }}</mat-hint>
<mat-error *ngIf="stringItemsForm.get('items').hasError('required')">{{ requiredText }}</mat-error>
</mat-form-field>
</section>
<mat-form-field [formGroup]="stringItemsForm" fxFlex class="mat-block"
[floatLabel]="floatLabel"
[appearance]="appearance"
[subscriptSizing]="subscriptSizing">
<mat-label *ngIf="label">{{ label }}</mat-label>
<mat-chip-grid #itemsChipList formControlName="items" [required]="required">
<mat-chip-row *ngFor="let item of itemList"
[removable]="!disabled"
[contentEditable]="editable && !disabled"
(removed)="removeItems(item)">
{{ item.name }}
<mat-icon matChipRemove *ngIf="!disabled">close</mat-icon>
</mat-chip-row>
<input matInput type="text"
placeholder="{{ placeholder }}"
style="max-width: 300px;min-width: 250px"
#stringItemInput
(focusin)="onFocus()"
formControlName="item"
#origin="matAutocompleteOrigin"
[matChipInputFor]="itemsChipList"
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
matAutocompleteOrigin
matChipInputAddOnBlur
(matChipInputTokenEnd)="addItem($event)"
[matAutocompleteConnectedTo]="origin"
[matAutocomplete]="stringItemAutocomplete"
[matAutocompleteDisabled]="!predefinedValues?.length">
</mat-chip-grid>
<mat-autocomplete #stringItemAutocomplete="matAutocomplete"
[displayWith]="displayValueFn"
class="tb-autocomplete">
<mat-option *ngFor="let value of filteredValues | async" [value]="value">
<span [innerHTML]="value.name | highlight:searchText"></span>
</mat-option>
<mat-option *ngIf="!(filteredValues | async)?.length" [value]="null">
{{ 'common.not-found' | translate }}
</mat-option>
</mat-autocomplete>
<mat-hint>{{ hint }}</mat-hint>
<mat-error *ngIf="stringItemsForm.get('items').hasError('required')">
{{ requiredText }}
</mat-error>
</mat-form-field>

167
ui-ngx/src/app/shared/components/string-items-list.component.ts

@ -14,13 +14,27 @@
/// limitations under the License.
///
import { Component, forwardRef, Input } from '@angular/core';
import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms';
import { Component, ElementRef, forwardRef, Input, OnInit, ViewChild } from '@angular/core';
import {
AbstractControl,
ControlValueAccessor,
FormBuilder,
FormGroup,
NG_VALUE_ACCESSOR,
Validators
} from '@angular/forms';
import { MatChipInputEvent } from '@angular/material/chips';
import { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes';
import { FloatLabelType, MatFormFieldAppearance, SubscriptSizing } from '@angular/material/form-field';
import { coerceBoolean } from '@shared/decorators/coercion';
import { coerceArray, coerceBoolean } from '@shared/decorators/coercion';
import { Observable, of } from 'rxjs';
import { filter, mergeMap, share, tap } from 'rxjs/operators';
import { MatAutocompleteTrigger } from '@angular/material/autocomplete';
export interface StringItemsOption {
name: string;
value: any;
}
@Component({
selector: 'tb-string-items-list',
templateUrl: './string-items-list.component.html',
@ -33,17 +47,29 @@ import { coerceBoolean } from '@shared/decorators/coercion';
}
]
})
export class StringItemsListComponent implements ControlValueAccessor{
export class StringItemsListComponent implements ControlValueAccessor, OnInit {
stringItemsForm: FormGroup;
filteredValues: Observable<Array<StringItemsOption>>;
searchText = '';
itemList: StringItemsOption[] = [];
private modelValue: Array<string> | null;
readonly separatorKeysCodes: number[] = [ENTER, COMMA, SEMICOLON];
@ViewChild('stringItemInput', {static: true}) stringItemInput: ElementRef<HTMLInputElement>;
@ViewChild(MatAutocompleteTrigger) autocomplete: MatAutocompleteTrigger;
private requiredValue: boolean;
get required(): boolean {
return this.requiredValue;
}
@Input()
@coerceBoolean()
set required(value: boolean) {
@ -80,19 +106,52 @@ export class StringItemsListComponent implements ControlValueAccessor{
editable = false;
@Input()
subscriptSizing: SubscriptSizing = 'fixed'
subscriptSizing: SubscriptSizing = 'fixed';
private propagateChange = (v: any) => { };
@Input()
@coerceArray()
predefinedValues: StringItemsOption[];
get itemsControl(): AbstractControl {
return this.stringItemsForm.get('items');
}
get itemControl(): AbstractControl {
return this.stringItemsForm.get('item');
}
private propagateChange = (v: any) => {
};
private dirty = false;
constructor(private fb: FormBuilder) {
this.stringItemsForm = this.fb.group({
items: [null, this.required ? [Validators.required] : []]
item: [null],
items: [null]
});
}
ngOnInit() {
if (this.predefinedValues) {
this.filteredValues = this.itemControl.valueChanges
.pipe(
tap((value) => {
if (value && typeof value !== 'string') {
this.add(value);
} else if (value === null) {
this.clear();
}
}),
filter((value) => typeof value === 'string'),
mergeMap(name => this.fetchValues(name)),
share()
);
}
}
updateValidators() {
this.stringItemsForm.get('items').setValidators(this.required ? [Validators.required] : []);
this.stringItemsForm.get('items').updateValueAndValidity();
this.itemsControl.setValidators(this.required ? [Validators.required] : []);
this.itemsControl.updateValueAndValidity();
}
registerOnChange(fn: any): void {
@ -112,48 +171,100 @@ export class StringItemsListComponent implements ControlValueAccessor{
}
writeValue(value: Array<string> | null): void {
this.searchText = '';
if (value != null && value.length > 0) {
this.modelValue = [...value];
this.stringItemsForm.get('items').setValue(value);
this.itemList = [];
if (this.predefinedValues) {
value.forEach(item => {
const findItem = this.predefinedValues.find(option => option.value === item);
if (findItem) {
this.itemList.push(findItem);
}
});
} else {
value.forEach(item => this.itemList.push({value: item, name: item}));
}
this.itemsControl.setValue(this.itemList, {emitEvents: false});
} else {
this.stringItemsForm.get('items').setValue(null);
this.itemsControl.setValue(null, {emitEvents: false});
this.modelValue = null;
this.itemList = [];
}
this.dirty = true;
}
addItem(event: MatChipInputEvent): void {
let item = event.value || '';
const input = event.chipInput.inputElement;
item = item.trim();
const item = event.value?.trim() ?? '';
if (item) {
if (!this.modelValue || this.modelValue.indexOf(item) === -1) {
if (!this.modelValue) {
this.modelValue = [];
if (this.predefinedValues) {
const findItems = this.predefinedValues
.filter(value => value.name.toLowerCase().includes(item.toLowerCase()));
if (findItems.length === 1) {
this.add(findItems[0]);
}
this.modelValue.push(item);
this.stringItemsForm.get('items').setValue(this.modelValue);
}
this.propagateChange(this.modelValue);
if (input) {
input.value = '';
} else {
this.add({value: item, name: item});
}
}
}
removeItems(item: string) {
const index = this.modelValue.indexOf(item);
removeItems(item: StringItemsOption) {
const index = this.modelValue.indexOf(item.value);
if (index >= 0) {
this.modelValue.splice(index, 1);
this.itemList.splice(index, 1);
if (!this.modelValue.length) {
this.modelValue = null;
}
this.stringItemsForm.get('items').setValue(this.modelValue);
this.itemsControl.setValue(this.itemList);
this.propagateChange(this.modelValue);
this.autocomplete?.closePanel();
}
}
onFocus() {
if (this.dirty) {
this.itemControl.updateValueAndValidity({onlySelf: true, emitEvent: true});
this.dirty = false;
}
}
displayValueFn(values?: StringItemsOption): string | undefined {
return values ? values.name : undefined;
}
private add(item: StringItemsOption) {
if (!this.modelValue || this.modelValue.indexOf(item.value) === -1) {
if (!this.modelValue) {
this.modelValue = [];
}
this.modelValue.push(item.value);
this.itemList.push(item);
this.itemsControl.setValue(this.itemList);
}
this.propagateChange(this.modelValue);
this.clear();
}
get stringItemsList(): string[] {
return this.stringItemsForm.get('items').value;
private fetchValues(searchText?: string): Observable<Array<StringItemsOption>> {
if (!this.predefinedValues?.length) {
return of([]);
}
this.searchText = searchText;
let result = this.predefinedValues;
if (searchText && searchText.length) {
result = this.predefinedValues.filter(option => option.name.toLowerCase().includes(searchText.toLowerCase()));
}
return of(result);
}
private clear(value: string = '') {
this.stringItemInput.nativeElement.value = value;
this.itemControl.patchValue(value, {emitEvent: true});
setTimeout(() => {
this.stringItemInput.nativeElement.blur();
this.stringItemInput.nativeElement.focus();
}, 0);
}
}

43
ui-ngx/src/app/shared/models/limited-api.models.ts

@ -0,0 +1,43 @@
///
/// Copyright © 2016-2023 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.
///
export enum LimitedApi {
ENTITY_EXPORT = 'ENTITY_EXPORT',
ENTITY_IMPORT = 'ENTITY_IMPORT',
NOTIFICATION_REQUESTS = 'NOTIFICATION_REQUESTS',
NOTIFICATION_REQUESTS_PER_RULE = 'NOTIFICATION_REQUESTS_PER_RULE',
REST_REQUESTS_PER_TENANT = 'REST_REQUESTS_PER_TENANT',
REST_REQUESTS_PER_CUSTOMER = 'REST_REQUESTS_PER_CUSTOMER',
WS_UPDATES_PER_SESSION = 'WS_UPDATES_PER_SESSION',
CASSANDRA_QUERIES = 'CASSANDRA_QUERIES',
TRANSPORT_MESSAGES_PER_TENANT = 'TRANSPORT_MESSAGES_PER_TENANT',
TRANSPORT_MESSAGES_PER_DEVICE = 'TRANSPORT_MESSAGES_PER_DEVICE'
}
export const LimitedApiTranslationMap = new Map<LimitedApi, string>(
[
[LimitedApi.ENTITY_EXPORT, 'api-limit.entity-version-creation'],
[LimitedApi.ENTITY_IMPORT, 'api-limit.entity-version-load'],
[LimitedApi.NOTIFICATION_REQUESTS, 'api-limit.notification-requests'],
[LimitedApi.NOTIFICATION_REQUESTS_PER_RULE, 'api-limit.notification-requests-per-rule'],
[LimitedApi.REST_REQUESTS_PER_TENANT, 'api-limit.rest-api-requests'],
[LimitedApi.REST_REQUESTS_PER_CUSTOMER, 'api-limit.rest-api-requests-per-customer'],
[LimitedApi.WS_UPDATES_PER_SESSION, 'api-limit.ws-updates-per-session'],
[LimitedApi.CASSANDRA_QUERIES, 'api-limit.cassandra-queries'],
[LimitedApi.TRANSPORT_MESSAGES_PER_TENANT, 'api-limit.transport-messages'],
[LimitedApi.TRANSPORT_MESSAGES_PER_DEVICE, 'api-limit.transport-messages-per-device']
]
);

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

@ -26,6 +26,7 @@ import { NotificationRuleId } from '@shared/models/id/notification-rule-id';
import { AlarmSearchStatus, AlarmSeverity, AlarmStatus } from '@shared/models/alarm.models';
import { EntityType } from '@shared/models/entity-type.models';
import { ApiFeature, ApiUsageStateValue } from '@shared/models/api-usage.models';
import { LimitedApi } from '@shared/models/limited-api.models';
export interface Notification {
readonly id: NotificationId;
@ -119,7 +120,7 @@ export interface NotificationRule extends Omit<BaseData<NotificationRuleId>, 'la
export type NotificationRuleTriggerConfig = Partial<AlarmNotificationRuleTriggerConfig & DeviceInactivityNotificationRuleTriggerConfig &
EntityActionNotificationRuleTriggerConfig & AlarmCommentNotificationRuleTriggerConfig & AlarmAssignmentNotificationRuleTriggerConfig &
RuleEngineLifecycleEventNotificationRuleTriggerConfig & EntitiesLimitNotificationRuleTriggerConfig &
ApiUsageLimitNotificationRuleTriggerConfig>;
ApiUsageLimitNotificationRuleTriggerConfig & RateLimitsNotificationRuleTriggerConfig>;
export interface AlarmNotificationRuleTriggerConfig {
alarmTypes?: Array<string>;
@ -178,6 +179,10 @@ export interface ApiUsageLimitNotificationRuleTriggerConfig {
notifyOn: ApiUsageStateValue[];
}
export interface RateLimitsNotificationRuleTriggerConfig {
apis: LimitedApi[];
}
export enum ComponentLifecycleEvent {
STARTED = 'STARTED',
UPDATED = 'UPDATED',

1
ui-ngx/src/app/shared/models/public-api.ts

@ -37,6 +37,7 @@ export * from './entity-type.models';
export * from './entity-view.models';
export * from './error.models';
export * from './event.models';
export * from './limited-api.models';
export * from './login.models';
export * from './material.models';
export * from './notification.models';

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

@ -756,6 +756,18 @@
"view-details": "View details",
"view-statistics": "View statistics"
},
"api-limit": {
"cassandra-queries": "Cassandra queries",
"entity-version-creation": "Entity version creation",
"entity-version-load": "Entity version load",
"notification-requests": "Notification requests",
"notification-requests-per-rule": "Notification requests per rule",
"rest-api-requests": "REST API requests",
"rest-api-requests-per-customer": "REST API requests per customer",
"transport-messages": "Transport messages",
"transport-messages-per-device": "Transport messages per device",
"ws-updates-per-session": "WS updates per session"
},
"audit-log": {
"audit": "Audit",
"audit-logs": "Audit Logs",
@ -839,7 +851,8 @@
"created-time": "Created time",
"loading": "Loading...",
"proceed": "Proceed",
"open-details-page": "Open details page"
"open-details-page": "Open details page",
"not-found": "Not found"
},
"content-type": {
"json": "Json",
@ -2936,6 +2949,8 @@
"only-rule-chain-lifecycle-failures": "Only rule chain lifecycle failures",
"only-rule-node-lifecycle-failures": "Only rule node lifecycle failures",
"platform-users": "Platform users",
"rate-limits": "Rate limits",
"rate-limits-hint": "If the field is empty, the trigger will be applied to all rate limits",
"recipient": "Recipient",
"recipient-group": "Recipient group",
"recipient-type": {

Loading…
Cancel
Save