Browse Source

Rate limits implementation

pull/6979/head
Artem Babak 4 years ago
parent
commit
48e5aed0b6
  1. 16
      ui-ngx/src/app/modules/home/components/home-components.module.ts
  2. 118
      ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.html
  3. 3
      ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.ts
  4. 52
      ui-ngx/src/app/modules/home/components/profile/tenant/rate-limits/rate-limits-details-dialog.component.html
  5. 83
      ui-ngx/src/app/modules/home/components/profile/tenant/rate-limits/rate-limits-details-dialog.component.ts
  6. 61
      ui-ngx/src/app/modules/home/components/profile/tenant/rate-limits/rate-limits-list.component.html
  7. 47
      ui-ngx/src/app/modules/home/components/profile/tenant/rate-limits/rate-limits-list.component.scss
  8. 161
      ui-ngx/src/app/modules/home/components/profile/tenant/rate-limits/rate-limits-list.component.ts
  9. 19
      ui-ngx/src/app/modules/home/components/profile/tenant/rate-limits/rate-limits-text.component.html
  10. 49
      ui-ngx/src/app/modules/home/components/profile/tenant/rate-limits/rate-limits-text.component.scss
  11. 98
      ui-ngx/src/app/modules/home/components/profile/tenant/rate-limits/rate-limits-text.component.ts
  12. 34
      ui-ngx/src/app/modules/home/components/profile/tenant/rate-limits/rate-limits.component.html
  13. 31
      ui-ngx/src/app/modules/home/components/profile/tenant/rate-limits/rate-limits.component.scss
  14. 153
      ui-ngx/src/app/modules/home/components/profile/tenant/rate-limits/rate-limits.component.ts
  15. 137
      ui-ngx/src/app/shared/models/rate-limits.models.ts
  16. 36
      ui-ngx/src/assets/locale/locale.constant-en_US.json

16
ui-ngx/src/app/modules/home/components/home-components.module.ts

@ -166,6 +166,10 @@ import { EntityTypesVersionLoadComponent } from '@home/components/vc/entity-type
import { ComplexVersionLoadComponent } from '@home/components/vc/complex-version-load.component';
import { RemoveOtherEntitiesConfirmComponent } from '@home/components/vc/remove-other-entities-confirm.component';
import { AutoCommitSettingsComponent } from '@home/components/vc/auto-commit-settings.component';
import { RateLimitsListComponent } from '@home/components/profile/tenant/rate-limits/rate-limits-list.component';
import { RateLimitsComponent } from '@home/components/profile/tenant/rate-limits/rate-limits.component';
import { RateLimitsTextComponent } from '@home/components/profile/tenant/rate-limits/rate-limits-text.component';
import { RateLimitsDetailsDialogComponent } from '@home/components/profile/tenant/rate-limits/rate-limits-details-dialog.component';
@NgModule({
declarations:
@ -302,7 +306,11 @@ import { AutoCommitSettingsComponent } from '@home/components/vc/auto-commit-set
EntityTypesVersionLoadComponent,
ComplexVersionLoadComponent,
RemoveOtherEntitiesConfirmComponent,
AutoCommitSettingsComponent
AutoCommitSettingsComponent,
RateLimitsListComponent,
RateLimitsComponent,
RateLimitsTextComponent,
RateLimitsDetailsDialogComponent
],
imports: [
CommonModule,
@ -432,7 +440,11 @@ import { AutoCommitSettingsComponent } from '@home/components/vc/auto-commit-set
EntityTypesVersionLoadComponent,
ComplexVersionLoadComponent,
RemoveOtherEntitiesConfirmComponent,
AutoCommitSettingsComponent
AutoCommitSettingsComponent,
RateLimitsListComponent,
RateLimitsComponent,
RateLimitsTextComponent,
RateLimitsDetailsDialogComponent
],
providers: [
WidgetComponentService,

118
ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.html

@ -418,38 +418,24 @@
</fieldset>
<fieldset class="fields-group">
<legend class="group-title">Rate limits</legend>
<legend class="group-title">
{{ 'tenant-profile.rate-limits.rate-limits' | translate }}
</legend>
<div fxFlex fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="16px">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>tenant-profile.transport-tenant-msg-rate-limit</mat-label>
<input matInput formControlName="transportTenantMsgRateLimit">
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('transportTenantMsgRateLimit').hasError('pattern')">
{{ 'tenant-profile.incorrect-pattern-for-rate-limits' | translate}}
</mat-error>
</mat-form-field>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>tenant-profile.transport-device-msg-rate-limit</mat-label>
<input matInput formControlName="transportDeviceMsgRateLimit">
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('transportDeviceMsgRateLimit').hasError('pattern')">
{{ 'tenant-profile.incorrect-pattern-for-rate-limits' | translate}}
</mat-error>
</mat-form-field>
<tb-rate-limits fxFlex="50" formControlName="transportTenantMsgRateLimit"
[type]="rateLimitsType.TENANT_MESSAGES">
</tb-rate-limits>
<tb-rate-limits fxFlex="50" formControlName="transportDeviceMsgRateLimit"
[type]="rateLimitsType.DEVICE_MESSAGES">
</tb-rate-limits>
</div>
<div fxFlex fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="16px">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>tenant-profile.transport-tenant-telemetry-msg-rate-limit</mat-label>
<input matInput formControlName="transportTenantTelemetryMsgRateLimit">
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('transportTenantTelemetryMsgRateLimit').hasError('pattern')">
{{ 'tenant-profile.incorrect-pattern-for-rate-limits' | translate}}
</mat-error>
</mat-form-field>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>tenant-profile.transport-device-telemetry-msg-rate-limit</mat-label>
<input matInput formControlName="transportDeviceTelemetryMsgRateLimit">
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('transportTenantTelemetryMsgRateLimit').hasError('pattern')">
{{ 'tenant-profile.incorrect-pattern-for-rate-limits' | translate}}
</mat-error>
</mat-form-field>
<tb-rate-limits fxFlex="50" formControlName="transportTenantTelemetryMsgRateLimit"
[type]="rateLimitsType.TENANT_TELEMETRY_MESSAGES">
</tb-rate-limits>
<tb-rate-limits fxFlex="50" formControlName="transportDeviceTelemetryMsgRateLimit"
[type]="rateLimitsType.DEVICE_TELEMETRY_MESSAGES">
</tb-rate-limits>
</div>
<mat-expansion-panel class="configuration-panel">
<mat-expansion-panel-header>
@ -459,40 +445,28 @@
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<div fxFlex fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="16px">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>tenant-profile.transport-tenant-telemetry-data-points-rate-limit</mat-label>
<input matInput formControlName="transportTenantTelemetryDataPointsRateLimit">
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('transportTenantTelemetryDataPointsRateLimit').hasError('pattern')">
{{ 'tenant-profile.incorrect-pattern-for-rate-limits' | translate}}
</mat-error>
</mat-form-field>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>tenant-profile.transport-device-telemetry-data-points-rate-limit</mat-label>
<input matInput formControlName="transportDeviceTelemetryDataPointsRateLimit">
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('transportDeviceTelemetryDataPointsRateLimit').hasError('pattern')">
{{ 'tenant-profile.incorrect-pattern-for-rate-limits' | translate}}
</mat-error>
</mat-form-field>
<tb-rate-limits fxFlex="50" formControlName="transportTenantTelemetryDataPointsRateLimit"
[type]="rateLimitsType.TENANT_TELEMETRY_DATA_POINTS">
</tb-rate-limits>
<tb-rate-limits fxFlex="50" formControlName="transportDeviceTelemetryDataPointsRateLimit"
[type]="rateLimitsType.DEVICE_TELEMETRY_DATA_POINTS">
</tb-rate-limits>
</div>
<div fxFlex fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="16px">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>tenant-profile.tenant-rest-limits</mat-label>
<input matInput
formControlName="tenantServerRestLimitsConfiguration"
type="text">
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('tenantServerRestLimitsConfiguration').hasError('pattern')">
{{ 'tenant-profile.incorrect-pattern-for-rate-limits' | translate}}
</mat-error>
</mat-form-field>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>tenant-profile.customer-rest-limits</mat-label>
<input matInput
formControlName="customerServerRestLimitsConfiguration"
type="text">
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('customerServerRestLimitsConfiguration').hasError('pattern')">
{{ 'tenant-profile.incorrect-pattern-for-rate-limits' | translate}}
</mat-error>
</mat-form-field>
<tb-rate-limits fxFlex="50" formControlName="tenantServerRestLimitsConfiguration"
[type]="rateLimitsType.TENANT_SERVER_REST_LIMITS_CONFIGURATION">
</tb-rate-limits>
<tb-rate-limits fxFlex="50" formControlName="customerServerRestLimitsConfiguration"
[type]="rateLimitsType.CUSTOMER_SERVER_REST_LIMITS_CONFIGURATION">
</tb-rate-limits>
</div>
<div fxFlex fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="16px">
<tb-rate-limits fxFlex="50" formControlName="wsUpdatesPerSessionRateLimit"
[type]="rateLimitsType.WS_UPDATE_PER_SESSION_RATE_LIMIT">
</tb-rate-limits>
<tb-rate-limits fxFlex="50" formControlName="cassandraQueryTenantRateLimitsConfiguration"
[type]="rateLimitsType.CASSANDRA_QUERY_TENANT_RATE_LIMITS_CONFIGURATION">
</tb-rate-limits>
</div>
<div fxFlex fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="16px">
<mat-form-field fxFlex class="mat-block">
@ -503,22 +477,12 @@
<mat-label translate>tenant-profile.tenant-entity-import-rate-limit</mat-label>
<input matInput formControlName="tenantEntityImportRateLimit">
</mat-form-field>
</div>
<div fxFlex fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="16px">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>tenant-profile.ws-limit-updates-per-session</mat-label>
<input matInput type="text" formControlName="wsUpdatesPerSessionRateLimit">
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('wsUpdatesPerSessionRateLimit').hasError('pattern')">
{{ 'tenant-profile.incorrect-pattern-for-rate-limits' | translate}}
</mat-error>
</mat-form-field>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>tenant-profile.cassandra-tenant-limits-configuration</mat-label>
<input matInput type="text" formControlName="cassandraQueryTenantRateLimitsConfiguration">
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('cassandraQueryTenantRateLimitsConfiguration').hasError('pattern')">
{{ 'tenant-profile.incorrect-pattern-for-rate-limits' | translate}}
</mat-error>
</mat-form-field>
<!-- <tb-rate-limits fxFlex="50" formControlName="tenantEntityExportRateLimit"
[type]="rateLimitsType.TENANT_ENTITY_EXPORT_RATE_LIMIT">
</tb-rate-limits>
<tb-rate-limits fxFlex="50" formControlName="tenantEntityImportRateLimit"
[type]="rateLimitsType.TENANT_ENTITY_IMPORT_RATE_LIMIT">
</tb-rate-limits>-->
</div>
</ng-template>
</mat-expansion-panel>

3
ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.ts

@ -21,6 +21,7 @@ import { AppState } from '@app/core/core.state';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { DefaultTenantProfileConfiguration, TenantProfileConfiguration } from '@shared/models/tenant.model';
import { isDefinedAndNotNull } from '@core/utils';
import { RateLimitsType } from '@shared/models/rate-limits.models';
@Component({
selector: 'tb-default-tenant-profile-configuration',
@ -49,6 +50,8 @@ export class DefaultTenantProfileConfigurationComponent implements ControlValueA
@Input()
disabled: boolean;
rateLimitsType = RateLimitsType;
private propagateChange = (v: any) => { };
constructor(private store: Store<AppState>,

52
ui-ngx/src/app/modules/home/components/profile/tenant/rate-limits/rate-limits-details-dialog.component.html

@ -0,0 +1,52 @@
<!--
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]="editDetailsFormGroup" style="width: 800px;">
<mat-toolbar color="primary">
<h2 translate>{{ title }}</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 mat-dialog-content>
<fieldset [disabled]="isLoading$ | async">
<div fxFlex fxLayout="column">
<tb-rate-limits-list
formControlName="rateLimits"
[disabled]="isLoading$ | async">
</tb-rate-limits-list>
</div>
</fieldset>
</div>
<div mat-dialog-actions fxLayoutAlign="end center">
<button mat-button color="primary"
type="button"
[disabled]="(isLoading$ | async)"
(click)="cancel()" cdkFocusInitial>
{{ 'action.cancel' | translate }}
</button>
<button *ngIf="!data.readonly" mat-raised-button color="primary" (click)="save()"
[disabled]="(isLoading$ | async) || editDetailsFormGroup.invalid || !editDetailsFormGroup.dirty">
{{ 'action.save' | translate }}
</button>
</div>
</form>

83
ui-ngx/src/app/modules/home/components/profile/tenant/rate-limits/rate-limits-details-dialog.component.ts

@ -0,0 +1,83 @@
///
/// 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, Inject, OnInit, SkipSelf } from '@angular/core';
import { ErrorStateMatcher } from '@angular/material/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm } from '@angular/forms';
import { Router } from '@angular/router';
import { DialogComponent } from '@app/shared/components/dialog.component';
import { TranslateService } from '@ngx-translate/core';
export interface RateLimitsDetailsDialogData {
rateLimits: string;
title: string;
readonly: boolean;
}
@Component({
templateUrl: './rate-limits-details-dialog.component.html',
providers: [{provide: ErrorStateMatcher, useExisting: RateLimitsDetailsDialogComponent}]
})
export class RateLimitsDetailsDialogComponent extends DialogComponent<RateLimitsDetailsDialogComponent, any>
implements OnInit, ErrorStateMatcher {
editDetailsFormGroup: FormGroup;
submitted: boolean = false;
rateLimits: string = this.data.rateLimits;
title: string = this.data.title;
constructor(protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: RateLimitsDetailsDialogData,
@SkipSelf() private errorStateMatcher: ErrorStateMatcher,
public dialogRef: MatDialogRef<RateLimitsDetailsDialogComponent, any>,
private fb: FormBuilder,
public translate: TranslateService) {
super(store, router, dialogRef);
this.editDetailsFormGroup = this.fb.group({
rateLimits: [this.rateLimits, []]
});
if (this.data.readonly) {
this.editDetailsFormGroup.disable();
}
}
ngOnInit(): void {
}
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
const originalErrorState = this.errorStateMatcher.isErrorState(control, form);
const customErrorState = !!(control && control.invalid && this.submitted);
return originalErrorState || customErrorState;
}
cancel(): void {
this.dialogRef.close(null);
}
save(): void {
this.submitted = true;
this.dialogRef.close(this.editDetailsFormGroup.get('rateLimits').value);
}
}

61
ui-ngx/src/app/modules/home/components/profile/tenant/rate-limits/rate-limits-list.component.html

@ -0,0 +1,61 @@
<!--
Copyright © 2016-2022 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<section fxFlex fxLayout="column" [formGroup]="rateLimitsListFormGroup">
<div fxFlex fxLayout="column" [formGroup]="rateLimit" *ngFor="let rateLimit of rateLimitsFormArray().controls; let $index = index">
<div class="tb-rate-limits-operation" *ngIf="$index > 0 && rateLimitsFormArray().controls.length > 1" translate>
tenant-profile.rate-limits.but-less-than
</div>
<div fxFlex fxLayout="row" fxLayoutGap="8px">
<mat-form-field fxFlex hideRequiredMarker appearance="fill" class="mat-block">
<mat-label translate>tenant-profile.rate-limits.number-of-messages</mat-label>
<input matInput placeholder="{{ 'tenant-profile.rate-limits.number-of-messages' | translate }}"
type="number" min="1" step="1" formControlName="value" required>
<mat-error *ngIf="rateLimit.get('value').hasError('required')">
{{ 'tenant-profile.rate-limits.number-of-messages-required' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field fxFlex hideRequiredMarker appearance="fill" class="mat-block">
<mat-label translate>tenant-profile.rate-limits.per-seconds</mat-label>
<input matInput placeholder="{{ 'tenant-profile.rate-limits.per-seconds' | translate }}"
type="number" min="1" step="1" formControlName="time" required>
<mat-error *ngIf="rateLimit.get('time').hasError('required')">
{{ 'tenant-profile.rate-limits.per-seconds-required' | translate }}
</mat-error>
</mat-form-field>
<button mat-icon-button type="button" color="primary"
*ngIf="!rateLimitsFormArray().disabled"
(click)="removeRateLimits($index)"
[matTooltip]="'tenant-profile.rate-limits.remove-limit' | translate" matTooltipPosition="above">
<mat-icon>remove_circle_outline</mat-icon>
</button>
</div>
</div>
<div>
<button mat-raised-button color="primary" *ngIf="!rateLimitsFormArray().disabled"
(click)="addRateLimits()"
[matTooltip]="'tenant-profile.rate-limits.add-limit' | translate" matTooltipPosition="above">
<span translate>tenant-profile.rate-limits.add-limit</span>
</button>
</div>
<div fxFlex fxLayoutGap="8px" fxLayout="column" class="tb-rate-limits-preview" [ngClass]="{'tb-rate-limits-preview-short': !disabled}">
<span translate>tenant-profile.rate-limits.preview</span>
<div>
<tb-rate-limits-text [formControl]="rateLimitsControl"></tb-rate-limits-text>
</div>
</div>
</section>

47
ui-ngx/src/app/modules/home/components/profile/tenant/rate-limits/rate-limits-list.component.scss

@ -0,0 +1,47 @@
/**
* 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 {
button {
min-width: 40px;
mat-icon {
margin-top: 0.6em;
}
}
.tb-rate-limits-preview {
margin-top: 1.5em;
span {
padding-left: 1em;
}
div {
border: 1px groove rgba(0, 0, 0, .25);
border-radius: 4px;
padding: 1em;
}
}
.tb-rate-limits-operation {
font-size: 12px;
color: rgba(0,0,0,.54);
margin-bottom: 16px;
}
}
:host ::ng-deep {
mat-form-field {
.mat-form-field-wrapper {
padding-bottom: 1em;
}
}
}

161
ui-ngx/src/app/modules/home/components/profile/tenant/rate-limits/rate-limits-list.component.ts

@ -0,0 +1,161 @@
///
/// 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, OnInit } from '@angular/core';
import {
ControlValueAccessor,
FormArray,
FormBuilder,
FormControl,
FormGroup,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
ValidationErrors,
Validator,
Validators
} from '@angular/forms';
import { Subscription } from 'rxjs';
import {
RateLimits,
rateLimitsArrayToString,
stringToRateLimitsArray
} from '@shared/models/rate-limits.models';
import { isDefinedAndNotNull } from '@core/utils';
@Component({
selector: 'tb-rate-limits-list',
templateUrl: './rate-limits-list.component.html',
styleUrls: ['./rate-limits-list.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => RateLimitsListComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => RateLimitsListComponent),
multi: true
}
]
})
export class RateLimitsListComponent implements ControlValueAccessor, Validator, OnInit {
@Input() disabled: boolean;
rateLimitsListFormGroup: FormGroup;
rateLimitsControl: FormControl;
private propagateChange = (v: any) => { };
private valueChangeSubscription: Subscription = null;
constructor(private fb: FormBuilder) {
}
ngOnInit(): void {
this.rateLimitsListFormGroup = this.fb.group({});
this.rateLimitsListFormGroup.addControl('rateLimits',
this.fb.array([]));
this.rateLimitsControl = this.fb.control(null);
this.rateLimitsListFormGroup.valueChanges.subscribe((value) => {
this.updateView(value?.rateLimits);
}
);
}
rateLimitsFormArray(): FormArray {
return this.rateLimitsListFormGroup.get('rateLimits') as FormArray;
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
setDisabledState?(isDisabled: boolean): void {
this.disabled = isDisabled;
if (this.disabled) {
this.rateLimitsListFormGroup.disable({emitEvent: false});
this.rateLimitsControl.disable({emitEvent: false});
} else {
this.rateLimitsListFormGroup.enable({emitEvent: false});
this.rateLimitsControl.enable({emitEvent: false});
}
}
validate(): ValidationErrors | null {
return this.rateLimitsListFormGroup.valid && this.rateLimitsControl.valid ? null : {
rateLimitsList: {valid: false}
};
}
writeValue(value: string) {
if (this.valueChangeSubscription) {
this.valueChangeSubscription.unsubscribe();
}
const rateLimitsControls: Array<FormGroup> = [];
if (value) {
let rateLimitsArray = value.split(',');
for (let i = 0; i < rateLimitsArray.length; i++) {
let valueTime = rateLimitsArray[i].split(':');
let value = valueTime[0];
let time = valueTime[1];
const rateLimitsControl = this.fb.group({
value: [value, [Validators.required]],
time: [time, [Validators.required]]
});
if (this.disabled) {
rateLimitsControl.disable();
}
rateLimitsControls.push(rateLimitsControl);
}
}
this.rateLimitsListFormGroup.setControl('rateLimits', this.fb.array(rateLimitsControls));
this.rateLimitsControl.patchValue(stringToRateLimitsArray(value), {emitEvent: false});
this.valueChangeSubscription = this.rateLimitsListFormGroup.valueChanges.subscribe((value) => {
this.updateView(value?.rateLimits);
});
}
public removeRateLimits(index: number) {
(this.rateLimitsListFormGroup.get('rateLimits') as FormArray).removeAt(index);
}
public addRateLimits() {
const rateLimitsArray = this.rateLimitsListFormGroup.get('rateLimits') as FormArray;
rateLimitsArray.push(this.fb.group({
value: [null, [Validators.required]],
time: [null, [Validators.required]]
}));
this.rateLimitsListFormGroup.updateValueAndValidity();
}
updateView(rateLimitsArray: Array<RateLimits>) {
if (rateLimitsArray.length > 0) {
const notNullRateLimits = rateLimitsArray.filter(rateLimits => isDefinedAndNotNull(rateLimits.value) && isDefinedAndNotNull(rateLimits.time));
const rateLimitsString = rateLimitsArrayToString(notNullRateLimits);
this.propagateChange(rateLimitsString);
this.rateLimitsControl.patchValue(stringToRateLimitsArray(rateLimitsString), {emitEvent: false});
} else {
this.propagateChange(null);
this.rateLimitsControl.patchValue(null, {emitEvent: false});
}
}
}

19
ui-ngx/src/app/modules/home/components/profile/tenant/rate-limits/rate-limits-text.component.html

@ -0,0 +1,19 @@
<!--
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 class="tb-rate-limits-text" [ngClass]="{disabled: disabled, required: requiredClass, nowrap: nowrap}"
[innerHTML]="rateLimitsText"></div>

49
ui-ngx/src/app/modules/home/components/profile/tenant/rate-limits/rate-limits-text.component.scss

@ -0,0 +1,49 @@
/**
* 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 {
.tb-rate-limits-text {
overflow: hidden;
&.disabled {
opacity: 0.7;
}
&.required {
color: #f44336;
padding: 0 4px;
}
&.nowrap {
white-space: nowrap;
overflow: hidden;
}
}
}
:host ::ng-deep {
.tb-rate-limits-text {
span {
font-size: 14px;
line-height: 1.8em;
}
.tb-rate-limits-value {
font-weight: bold;
border: 1px groove rgba(0, 0, 0, .25);
border-radius: 4px;
padding-left: 4px;
padding-right: 4px;
color: #305680;
}
}
}

98
ui-ngx/src/app/modules/home/components/profile/tenant/rate-limits/rate-limits-text.component.ts

@ -0,0 +1,98 @@
///
/// 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, OnInit } from '@angular/core';
import { ControlValueAccessor, FormBuilder, NG_VALUE_ACCESSOR } from '@angular/forms';
import { MatDialog } from '@angular/material/dialog';
import { TranslateService } from '@ngx-translate/core';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { RateLimits, rateLimitsArrayToHtml } from '@shared/models/rate-limits.models';
@Component({
selector: 'tb-rate-limits-text',
templateUrl: './rate-limits-text.component.html',
styleUrls: ['./rate-limits-text.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => RateLimitsTextComponent),
multi: true
}
]
})
export class RateLimitsTextComponent implements ControlValueAccessor, OnInit {
private requiredValue: boolean;
get required(): boolean {
return this.requiredValue;
}
@Input()
set required(value: boolean) {
this.requiredValue = coerceBooleanProperty(value);
}
@Input()
disabled: boolean;
@Input()
noRateLimitsText = this.translate.instant('tenant-profile.rate-limits.not-set');
@Input()
nowrap = false;
requiredClass = false;
public rateLimitsText: string;
private propagateChange = (v: any) => { };
constructor(private dialog: MatDialog,
private fb: FormBuilder,
private translate: TranslateService) {
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
ngOnInit() {
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
}
writeValue(value: Array<RateLimits>): void {
this.updateText(value);
}
private updateText(value: Array<RateLimits>) {
this.requiredClass = false;
if (value && value.length) {
this.rateLimitsText = rateLimitsArrayToHtml(this.translate, value);
} else {
if (this.required && !this.disabled) {
this.requiredClass = true;
} else {
this.rateLimitsText = this.noRateLimitsText;
}
}
}
}

34
ui-ngx/src/app/modules/home/components/profile/tenant/rate-limits/rate-limits.component.html

@ -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.
-->
<div fxFlex fxLayout="row" fxLayoutGap="8px">
<fieldset class="fieldset-element">
<legend class="legend-element">{{ label | translate }}</legend>
<div [formGroup]="rateLimitsFormGroup" fxFlex fxLayout="row" fxLayoutGap="8px"
(click)="onClick($event, 'edit')"
[matTooltip]="'tenant-profile.rate-limits.edit-limit' | translate" matTooltipPosition="above">
<tb-rate-limits-text formControlName="rateLimits"></tb-rate-limits-text>
</div>
</fieldset>
<div style="padding-top: 12px">
<button mat-icon-button color="primary" type="button" *ngIf="!disabled"
(click)="onClick($event, 'add')"
[matTooltip]="'tenant-profile.rate-limits.add-limit' | translate" matTooltipPosition="above">
<mat-icon>add_circle_outline</mat-icon>
</button>
</div>
</div>

31
ui-ngx/src/app/modules/home/components/profile/tenant/rate-limits/rate-limits.component.scss

@ -0,0 +1,31 @@
/**
* 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 {
padding: 12px 0 12px 0;
.fieldset-element {
cursor: pointer;
padding: 0.5em;
border: 1px groove rgba(0, 0, 0, 0.25);
border-radius: 4px;
width: 100%;
.legend-element {
color: rgba(0, 0, 0, 0.54);
font-size: 12px;
}
}
}

153
ui-ngx/src/app/modules/home/components/profile/tenant/rate-limits/rate-limits.component.ts

@ -0,0 +1,153 @@
///
/// 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, OnInit } from '@angular/core';
import {
ControlValueAccessor,
FormBuilder,
FormControl,
FormGroup,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
Validator
} from '@angular/forms';
import { MatDialog } from '@angular/material/dialog';
import {
RateLimitsDetailsDialogComponent,
RateLimitsDetailsDialogData
} from '@home/components/profile/tenant/rate-limits/rate-limits-details-dialog.component';
import {
addRateLimitTranslationMap,
editRateLimitTranslationMap,
rateLimitLabelTranslationMap,
RateLimitsType,
stringToRateLimitsArray
} from '@shared/models/rate-limits.models';
@Component({
selector: 'tb-rate-limits',
templateUrl: './rate-limits.component.html',
styleUrls: ['./rate-limits.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => RateLimitsComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => RateLimitsComponent),
multi: true,
}
]
})
export class RateLimitsComponent implements ControlValueAccessor, OnInit, Validator {
@Input()
disabled: boolean;
@Input()
type: RateLimitsType;
label: string;
rateLimitsFormGroup: FormGroup;
private modelValue: string;
private propagateChange = null;
constructor(private dialog: MatDialog,
private fb: FormBuilder) {
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
ngOnInit() {
this.label = rateLimitLabelTranslationMap.get(this.type);
this.rateLimitsFormGroup = this.fb.group({
rateLimits: [null, []]
});
}
setDisabledState(isDisabled: boolean) {
this.disabled = isDisabled;
if (this.disabled) {
this.rateLimitsFormGroup.disable({emitEvent: false});
} else {
this.rateLimitsFormGroup.enable({emitEvent: false});
}
}
writeValue(value: string) {
this.modelValue = value;
this.updateRateLimitsInfo();
}
public validate(c: FormControl) {
return null;
}
public onClick($event: Event, action: string) {
if ($event) {
$event.stopPropagation();
}
const title = this.setTitle(action);
this.dialog.open<RateLimitsDetailsDialogComponent, RateLimitsDetailsDialogData,
string>(RateLimitsDetailsDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
rateLimits: this.modelValue,
title,
readonly: this.disabled
}
}).afterClosed().subscribe((result) => {
if (result) {
this.modelValue = result;
this.updateModel();
}
});
}
private setTitle(action: string): string {
switch (action) {
case 'add':
return addRateLimitTranslationMap.get(this.type);
case 'edit':
return editRateLimitTranslationMap.get(this.type);
}
}
private updateRateLimitsInfo() {
this.rateLimitsFormGroup.patchValue(
{
rateLimits: stringToRateLimitsArray(this.modelValue)
}
);
}
private updateModel() {
this.updateRateLimitsInfo();
this.propagateChange(this.modelValue);
}
}

137
ui-ngx/src/app/shared/models/rate-limits.models.ts

@ -0,0 +1,137 @@
///
/// 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 { TranslateService } from '@ngx-translate/core';
export interface RateLimits {
value: string;
time: string;
}
export enum RateLimitsType {
DEVICE_MESSAGES = 'DEVICE_MESSAGES',
DEVICE_TELEMETRY_MESSAGES = 'DEVICE_TELEMETRY_MESSAGES',
DEVICE_TELEMETRY_DATA_POINTS = 'DEVICE_TELEMETRY_DATA_POINTS',
TENANT_MESSAGES = 'TENANT_MESSAGES',
TENANT_TELEMETRY_MESSAGES = 'TENANT_TELEMETRY_MESSAGES',
TENANT_TELEMETRY_DATA_POINTS = 'TENANT_TELEMETRY_DATA_POINTS',
TENANT_SERVER_REST_LIMITS_CONFIGURATION = 'TENANT_SERVER_REST_LIMITS_CONFIGURATION',
CUSTOMER_SERVER_REST_LIMITS_CONFIGURATION = 'CUSTOMER_SERVER_REST_LIMITS_CONFIGURATION',
WS_UPDATE_PER_SESSION_RATE_LIMIT = 'WS_UPDATE_PER_SESSION_RATE_LIMIT',
CASSANDRA_QUERY_TENANT_RATE_LIMITS_CONFIGURATION = 'CASSANDRA_QUERY_TENANT_RATE_LIMITS_CONFIGURATION',
}
export const rateLimitLabelTranslationMap = new Map<RateLimitsType, string>(
[
[RateLimitsType.TENANT_MESSAGES, 'tenant-profile.rate-limits.transport-tenant-msg'],
[RateLimitsType.TENANT_TELEMETRY_MESSAGES, 'tenant-profile.rate-limits.transport-tenant-telemetry-msg'],
[RateLimitsType.TENANT_TELEMETRY_DATA_POINTS, 'tenant-profile.rate-limits.transport-tenant-telemetry-data-points'],
[RateLimitsType.DEVICE_MESSAGES, 'tenant-profile.rate-limits.transport-device-msg'],
[RateLimitsType.DEVICE_TELEMETRY_MESSAGES, 'tenant-profile.rate-limits.transport-device-telemetry-msg'],
[RateLimitsType.DEVICE_TELEMETRY_DATA_POINTS, 'tenant-profile.rate-limits.transport-device-telemetry-data-points'],
[RateLimitsType.TENANT_SERVER_REST_LIMITS_CONFIGURATION, 'tenant-profile.transport-tenant-msg-rate-limit'],
[RateLimitsType.CUSTOMER_SERVER_REST_LIMITS_CONFIGURATION, 'tenant-profile.customer-rest-limits'],
[RateLimitsType.WS_UPDATE_PER_SESSION_RATE_LIMIT, 'tenant-profile.ws-limit-updates-per-session'],
[RateLimitsType.CASSANDRA_QUERY_TENANT_RATE_LIMITS_CONFIGURATION, 'tenant-profile.cassandra-tenant-limits-configuration'],
]
);
export const editRateLimitTranslationMap = new Map<RateLimitsType, string>(
[
[RateLimitsType.TENANT_MESSAGES, 'tenant-profile.rate-limits.edit-transport-tenant-msg-title'],
[RateLimitsType.TENANT_TELEMETRY_MESSAGES, 'tenant-profile.rate-limits.edit-transport-tenant-telemetry-msg-title'],
[RateLimitsType.TENANT_TELEMETRY_DATA_POINTS, 'tenant-profile.rate-limits.edit-transport-tenant-telemetry-data-points-title'],
[RateLimitsType.DEVICE_MESSAGES, 'tenant-profile.rate-limits.edit-transport-device-msg-title'],
[RateLimitsType.DEVICE_TELEMETRY_MESSAGES, 'tenant-profile.rate-limits.edit-transport-device-telemetry-msg-title'],
[RateLimitsType.DEVICE_TELEMETRY_DATA_POINTS, 'tenant-profile.rate-limits.edit-transport-device-telemetry-data-points-title'],
[RateLimitsType.TENANT_SERVER_REST_LIMITS_CONFIGURATION, 'tenant-profile.rate-limits.edit-transport-device-telemetry-data-points-title'],
[RateLimitsType.CUSTOMER_SERVER_REST_LIMITS_CONFIGURATION, 'tenant-profile.rate-limits.edit-transport-device-telemetry-data-points-title'],
[RateLimitsType.WS_UPDATE_PER_SESSION_RATE_LIMIT, 'tenant-profile.rate-limits.edit-transport-device-telemetry-data-points-title'],
[RateLimitsType.CASSANDRA_QUERY_TENANT_RATE_LIMITS_CONFIGURATION, 'tenant-profile.rate-limits.edit-transport-device-telemetry-data-points-title'],
]
);
export const addRateLimitTranslationMap = new Map<RateLimitsType, string>(
[
[RateLimitsType.TENANT_MESSAGES, 'tenant-profile.rate-limits.add-transport-tenant-msg-title'],
[RateLimitsType.TENANT_TELEMETRY_MESSAGES, 'tenant-profile.rate-limits.add-transport-tenant-telemetry-msg-title'],
[RateLimitsType.TENANT_TELEMETRY_DATA_POINTS, 'tenant-profile.rate-limits.add-transport-tenant-telemetry-data-points-title'],
[RateLimitsType.DEVICE_MESSAGES, 'tenant-profile.rate-limits.add-transport-device-msg-title'],
[RateLimitsType.DEVICE_TELEMETRY_MESSAGES, 'tenant-profile.rate-limits.add-transport-device-telemetry-msg-title'],
[RateLimitsType.DEVICE_TELEMETRY_DATA_POINTS, 'tenant-profile.rate-limits.add-transport-device-telemetry-data-points-title'],
[RateLimitsType.TENANT_SERVER_REST_LIMITS_CONFIGURATION, 'tenant-profile.rate-limits.add-transport-device-telemetry-data-points-title'],
[RateLimitsType.CUSTOMER_SERVER_REST_LIMITS_CONFIGURATION, 'tenant-profile.rate-limits.add-transport-device-telemetry-data-points-title'],
[RateLimitsType.WS_UPDATE_PER_SESSION_RATE_LIMIT, 'tenant-profile.rate-limits.add-transport-device-telemetry-data-points-title'],
[RateLimitsType.CASSANDRA_QUERY_TENANT_RATE_LIMITS_CONFIGURATION, 'tenant-profile.rate-limits.add-transport-device-telemetry-data-points-title'],
]
);
export function stringToRateLimitsArray(rateLimits: string): Array<RateLimits> {
const result: Array<RateLimits> = [];
if (rateLimits?.length > 0) {
let rateLimitsArrays = rateLimits.split(',');
for (let i = 0; i < rateLimitsArrays.length; i++) {
let valueTime = rateLimitsArrays[i].split(':');
let value = valueTime[0];
let time = valueTime[1];
const rateLimitControl = {
value,
time
}
result.push(rateLimitControl);
}
}
return result;
}
export function rateLimitsArrayToString(rateLimits: Array<RateLimits>): string {
let result = '';
for (let i = 0; i < rateLimits.length; i++) {
result = result.concat(rateLimits[i].value, ':', rateLimits[i].time);
if ((rateLimits.length > 1) && (i !== rateLimits.length - 1)) {
result = result.concat(',');
}
}
return result;
}
export function rateLimitsArrayToHtml(translate: TranslateService, rateLimitsArray: Array<RateLimits>): string {
const rateLimitsHtml = rateLimitsArray.map((rateLimits, index) => {
const isLast: boolean = index === rateLimitsArray.length-1;
return rateLimitsToHtml(translate, rateLimits, isLast);
});
let result: string;
if (rateLimitsHtml.length > 1) {
const butLessThanText = translate.instant('tenant-profile.rate-limits.but-less-than');
result = rateLimitsHtml.join(' <span class="disabled">' + butLessThanText + '</span> ');
} else {
result = rateLimitsHtml[0];
}
return result;
}
function rateLimitsToHtml(translate: TranslateService, rateLimit: RateLimits, isLast: boolean): string {
const value = rateLimit.value;
const time = rateLimit.time;
const operation = translate.instant('tenant-profile.rate-limits.messages-per');
const seconds = translate.instant('tenant-profile.rate-limits.sec');
const comma = isLast ? '' : ',';
const result = `<span class="tb-rate-limits-value">${value}</span>
<span>${operation}</span>
<span class="tb-rate-limits-value"> ${time}</span>
<span>${seconds}${comma}</span><br>`;
return result;
}

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

@ -3187,7 +3187,41 @@
"ws-limit-max-subscriptions-per-customer": "Subscriptions per customer maximum number",
"ws-limit-max-subscriptions-per-regular-user": "Subscriptions per regular user maximum number",
"ws-limit-max-subscriptions-per-public-user": "Subscriptions per public user maximum number",
"ws-limit-updates-per-session": "WS updates per session"
"ws-limit-updates-per-session": "WS updates per session",
"rate-limits": {
"add-limit": "Add limit",
"add-transport-tenant-msg-title": "Add transport tenant messages rate limit",
"add-transport-tenant-telemetry-msg-title": "Add transport tenant telemetry messages rate limit",
"add-transport-tenant-telemetry-data-points-title": "Add transport tenant telemetry data points rate limit",
"add-transport-device-msg-title": "Add transport device messages rate limit",
"add-transport-device-telemetry-msg-title": "Add transport device telemetry messages rate limit",
"add-transport-device-telemetry-data-points-title": "Add transport device telemetry data points rate limit",
"advanced-settings": "Advanced settings",
"edit-limit": "Edit limit",
"but-less-than": "but less than",
"edit-transport-tenant-msg-title": "Edit transport tenant messages rate limit",
"edit-transport-tenant-telemetry-msg-title": "Edit transport tenant telemetry messages rate limit",
"edit-transport-tenant-telemetry-data-points-title": "Edit transport tenant telemetry data points rate limit",
"edit-transport-device-msg-title": "Edit transport device messages rate limit",
"edit-transport-device-telemetry-msg-title": "Edit transport device telemetry messages rate limit",
"edit-transport-device-telemetry-data-points-title": "Edit transport device telemetry data points rate limit",
"messages-per": "messages per",
"not-set": "Not set",
"number-of-messages": "Number of messages",
"number-of-messages-required": "Number of messages is required.",
"preview": "Preview",
"per-seconds": "Per seconds",
"per-seconds-required": "Time rate is required.",
"rate-limits": "Rate limits",
"remove-limit": "Remove rate limit",
"transport-tenant-msg": "Transport tenant messages",
"transport-tenant-telemetry-msg": "Transport tenant telemetry messages",
"transport-tenant-telemetry-data-points": "Transport tenant telemetry data points",
"transport-device-msg": "Transport device messages",
"transport-device-telemetry-msg": "Transport device telemetry messages",
"transport-device-telemetry-data-points": "Transport device telemetry data points",
"sec": "sec"
}
},
"timeinterval": {
"seconds-interval": "{ seconds, plural, 1 {1 second} other {# seconds} }",

Loading…
Cancel
Save