Added tenant profile configs for Calculated Fields

This commit is contained in:
mpetrov 2025-02-28 11:46:43 +02:00
parent 813f899801
commit e78fe241a0
12 changed files with 175 additions and 15 deletions

View File

@ -28,6 +28,8 @@ export interface SysParamsState {
userSettings: UserSettings; userSettings: UserSettings;
maxResourceSize: number; maxResourceSize: number;
maxDebugModeDurationMinutes: number; maxDebugModeDurationMinutes: number;
maxDataPointsPerRollingArg: number;
maxArgumentsPerCF: number;
ruleChainDebugPerTenantLimitsConfiguration?: string; ruleChainDebugPerTenantLimitsConfiguration?: string;
} }

View File

@ -31,6 +31,8 @@ const emptyUserAuthState: AuthPayload = {
persistDeviceStateToTelemetry: false, persistDeviceStateToTelemetry: false,
mobileQrEnabled: false, mobileQrEnabled: false,
maxResourceSize: 0, maxResourceSize: 0,
maxArgumentsPerCF: 0,
maxDataPointsPerRollingArg: 0,
maxDebugModeDurationMinutes: 0, maxDebugModeDurationMinutes: 0,
userSettings: initialUserSettings userSettings: initialUserSettings
}; };

View File

@ -119,9 +119,22 @@
<tb-error noMargin [error]="errorText | translate" class="flex h-9 items-center pl-3"/> <tb-error noMargin [error]="errorText | translate" class="flex h-9 items-center pl-3"/>
} }
</div> </div>
<div> <div class="flex h-9 justify-between">
<button type="button" mat-stroked-button color="primary" #button (click)="manageArgument($event, button)"> <button
type="button"
mat-stroked-button
color="primary"
#button
(click)="manageArgument($event, button)"
[disabled]="!!maxArgumentsPerCF && argumentsFormArray.length >= maxArgumentsPerCF"
>
{{ 'calculated-fields.add-argument' | translate }} {{ 'calculated-fields.add-argument' | translate }}
</button> </button>
@if (!!maxArgumentsPerCF && argumentsFormArray.length >= maxArgumentsPerCF) {
<div class="tb-form-hint tb-primary-fill max-args-warning flex items-center gap-2">
<mat-icon>warning</mat-icon>
<span>{{ 'calculated-fields.hint.max-args' | translate }}</span>
</div>
}
</div> </div>
</div> </div>

View File

@ -30,6 +30,12 @@
} }
} }
.max-args-warning {
.mat-icon {
color: #FAA405;
}
}
.tb-form-table-row-cell-buttons { .tb-form-table-row-cell-buttons {
--mat-badge-legacy-small-size-container-size: 8px; --mat-badge-legacy-small-size-container-size: 8px;
--mat-badge-small-size-container-overlap-offset: -5px; --mat-badge-small-size-container-overlap-offset: -5px;

View File

@ -55,6 +55,9 @@ import { TbPopoverComponent } from '@shared/components/popover.component';
import { TbTableDatasource } from '@shared/components/table/table-datasource.abstract'; import { TbTableDatasource } from '@shared/components/table/table-datasource.abstract';
import { EntityService } from '@core/http/entity.service'; import { EntityService } from '@core/http/entity.service';
import { MatSort } from '@angular/material/sort'; import { MatSort } from '@angular/material/sort';
import { getCurrentAuthState } from '@core/auth/auth.selectors';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
@Component({ @Component({
selector: 'tb-calculated-field-arguments-table', selector: 'tb-calculated-field-arguments-table',
@ -93,6 +96,7 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces
readonly ArgumentEntityType = ArgumentEntityType; readonly ArgumentEntityType = ArgumentEntityType;
readonly ArgumentType = ArgumentType; readonly ArgumentType = ArgumentType;
readonly CalculatedFieldType = CalculatedFieldType; readonly CalculatedFieldType = CalculatedFieldType;
readonly maxArgumentsPerCF = getCurrentAuthState(this.store).maxArgumentsPerCF;
private popoverComponent: TbPopoverComponent<CalculatedFieldArgumentPanelComponent>; private popoverComponent: TbPopoverComponent<CalculatedFieldArgumentPanelComponent>;
private propagateChange: (argumentsObj: Record<string, CalculatedFieldArgument>) => void = () => {}; private propagateChange: (argumentsObj: Record<string, CalculatedFieldArgument>) => void = () => {};
@ -105,6 +109,7 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces
private renderer: Renderer2, private renderer: Renderer2,
private entityService: EntityService, private entityService: EntityService,
private destroyRef: DestroyRef, private destroyRef: DestroyRef,
private store: Store<AppState>
) { ) {
this.argumentsFormArray.valueChanges.pipe(takeUntilDestroyed()).subscribe(value => { this.argumentsFormArray.valueChanges.pipe(takeUntilDestroyed()).subscribe(value => {
this.updateEntityNameMap(value); this.updateEntityNameMap(value);

View File

@ -166,10 +166,19 @@
formControlName="timeWindow" formControlName="timeWindow"
/> />
</div> </div>
<div class="tb-form-row limit-field-row"> @if (maxDataPointsPerRollingArg) {
<div class="fixed-title-width">{{ 'calculated-fields.limit' | translate }}</div> <div class="tb-form-row limit-field-row">
<tb-datapoints-limit class="w-full flex-1" formControlName="limit"/> <div class="fixed-title-width">{{ 'calculated-fields.limit' | translate }}</div>
</div> <div class="limit-slider-container flex w-full flex-1 flex-row items-center justify-start">
<mat-slider class="flex-1" min="1" max="{{maxDataPointsPerRollingArg}}">
<input matSliderThumb formControlName="limit" [value]="argumentFormGroup.get('limit').value"/>
</mat-slider>
<mat-form-field class="limit-slider-value" subscriptSizing="dynamic" appearance="outline">
<input matInput formControlName="limit" type="number" step="1" [value]="argumentFormGroup.get('limit').value" min="1" max="{{maxDataPointsPerRollingArg}}"/>
</mat-form-field>
</div>
</div>
}
} }
</div> </div>
</div> </div>

View File

@ -28,9 +28,7 @@ $panel-width: 520px;
min-width: 120px; min-width: 120px;
} }
} }
}
:host ::ng-deep {
.limit-field-row { .limit-field-row {
@media screen and (max-width: $panel-width) { @media screen and (max-width: $panel-width) {
display: flex; display: flex;
@ -42,7 +40,9 @@ $panel-width: 520px;
} }
} }
} }
}
:host ::ng-deep {
.time-interval-field { .time-interval-field {
.advanced-input { .advanced-input {
flex-direction: column; flex-direction: column;

View File

@ -38,7 +38,9 @@ import { EntityFilter } from '@shared/models/query/query.models';
import { AliasFilterType } from '@shared/models/alias.models'; import { AliasFilterType } from '@shared/models/alias.models';
import { merge } from 'rxjs'; import { merge } from 'rxjs';
import { MINUTE } from '@shared/models/time/time.models'; import { MINUTE } from '@shared/models/time/time.models';
import { TimeService } from '@core/services/time.service'; import { getCurrentAuthState } from '@core/auth/auth.selectors';
import { AppState } from '@core/core.state';
import { Store } from '@ngrx/store';
@Component({ @Component({
selector: 'tb-calculated-field-argument-panel', selector: 'tb-calculated-field-argument-panel',
@ -58,7 +60,8 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit {
argumentsDataApplied = output<{ value: CalculatedFieldArgumentValue, index: number }>(); argumentsDataApplied = output<{ value: CalculatedFieldArgumentValue, index: number }>();
readonly defaultLimit = Math.max(this.timeService.getMinDatapointsLimit(), Math.floor(this.timeService.getMaxDatapointsLimit() / 10)); readonly maxDataPointsPerRollingArg = getCurrentAuthState(this.store).maxDataPointsPerRollingArg;
readonly defaultLimit = Math.floor(this.maxDataPointsPerRollingArg / 10);
argumentFormGroup = this.fb.group({ argumentFormGroup = this.fb.group({
argumentName: ['', [Validators.required, this.uniqNameRequired(), Validators.pattern(charsWithNumRegex), Validators.maxLength(255)]], argumentName: ['', [Validators.required, this.uniqNameRequired(), Validators.pattern(charsWithNumRegex), Validators.maxLength(255)]],
@ -72,7 +75,7 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit {
scope: [{ value: AttributeScope.SERVER_SCOPE, disabled: true }, [Validators.required]], scope: [{ value: AttributeScope.SERVER_SCOPE, disabled: true }, [Validators.required]],
}), }),
defaultValue: ['', [Validators.pattern(noLeadTrailSpacesRegex)]], defaultValue: ['', [Validators.pattern(noLeadTrailSpacesRegex)]],
limit: [this.defaultLimit], limit: [{ value: this.defaultLimit, disabled: !this.maxDataPointsPerRollingArg }],
timeWindow: [MINUTE * 15], timeWindow: [MINUTE * 15],
}); });
@ -96,7 +99,7 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit {
private fb: FormBuilder, private fb: FormBuilder,
private cd: ChangeDetectorRef, private cd: ChangeDetectorRef,
private popover: TbPopoverComponent<CalculatedFieldArgumentPanelComponent>, private popover: TbPopoverComponent<CalculatedFieldArgumentPanelComponent>,
private timeService: TimeService private store: Store<AppState>
) { ) {
this.observeEntityFilterChanges(); this.observeEntityFilterChanges();
this.observeEntityTypeChanges() this.observeEntityTypeChanges()

View File

@ -229,6 +229,92 @@
</ng-template> </ng-template>
</mat-expansion-panel> </mat-expansion-panel>
</fieldset> </fieldset>
<fieldset class="fields-group">
<legend class="group-title">
{{ 'tenant-profile.calculated-fields' | translate }} <span translate>tenant-profile.unlimited</span>
</legend>
<div class="fields-element flex flex-1 flex-row xs:flex-col gt-xs:gap-4">
<mat-form-field class="mat-block flex-1" appearance="fill" subscriptSizing="dynamic">
<mat-label translate>tenant-profile.max-calculated-fields</mat-label>
<input matInput required min="0" step="1"
formControlName="maxCalculatedFieldsPerEntity"
type="number">
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxCalculatedFieldsPerEntity').hasError('required')">
{{ 'tenant-profile.max-calculated-fields-required' | translate}}
</mat-error>
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxCalculatedFieldsPerEntity').hasError('min')">
{{ 'tenant-profile.max-calculated-fields-range' | translate}}
</mat-error>
<mat-hint></mat-hint>
</mat-form-field>
<mat-form-field class="mat-block flex-1" appearance="fill" subscriptSizing="dynamic">
<mat-label translate>tenant-profile.max-data-points-per-rolling-arg</mat-label>
<input matInput required min="0" step="1"
formControlName="maxDataPointsPerRollingArg"
type="number">
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxDataPointsPerRollingArg').hasError('required')">
{{ 'tenant-profile.max-data-points-per-rolling-arg-required' | translate}}
</mat-error>
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxDataPointsPerRollingArg').hasError('min')">
{{ 'tenant-profile.max-data-points-per-rolling-arg-range' | translate}}
</mat-error>
<mat-hint></mat-hint>
</mat-form-field>
</div>
<div class="flex flex-1 flex-row xs:flex-col gt-xs:gap-4">
<mat-form-field class="mat-block flex-1" appearance="fill" subscriptSizing="dynamic">
<mat-label translate>tenant-profile.max-arguments-per-cf</mat-label>
<input matInput required min="0" step="1"
formControlName="maxArgumentsPerCF"
type="number">
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxArgumentsPerCF').hasError('required')">
{{ 'tenant-profile.max-arguments-per-cf-required' | translate}}
</mat-error>
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxArgumentsPerCF').hasError('min')">
{{ 'tenant-profile.max-arguments-per-cf-range' | translate}}
</mat-error>
<mat-hint></mat-hint>
</mat-form-field>
<div class="flex-1"></div>
</div>
<mat-expansion-panel class="configuration-panel">
<mat-expansion-panel-header>
<mat-panel-description class="flex items-stretch justify-end" translate>
tenant-profile.advanced-settings
</mat-panel-description>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<div class="flex flex-1 flex-row xs:flex-col gt-xs:gap-4">
<mat-form-field class="mat-block flex-1" appearance="fill" subscriptSizing="dynamic">
<mat-label translate>tenant-profile.max-state-size</mat-label>
<input matInput required min="0" step="1"
formControlName="maxStateSizeInKBytes"
type="number">
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxStateSizeInKBytes').hasError('required')">
{{ 'tenant-profile.max-state-size-required' | translate}}
</mat-error>
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxStateSizeInKBytes').hasError('min')">
{{ 'tenant-profile.max-state-size-range' | translate}}
</mat-error>
<mat-hint></mat-hint>
</mat-form-field>
<mat-form-field class="mat-block flex-1" appearance="fill" subscriptSizing="dynamic">
<mat-label translate>tenant-profile.max-value-argument-size</mat-label>
<input matInput required min="0" step="1"
formControlName="maxSingleValueArgumentSizeInKBytes"
type="number">
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxSingleValueArgumentSizeInKBytes').hasError('required')">
{{ 'tenant-profile.max-value-argument-size-required' | translate}}
</mat-error>
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxSingleValueArgumentSizeInKBytes').hasError('min')">
{{ 'tenant-profile.max-value-argument-size-range' | translate}}
</mat-error>
<mat-hint></mat-hint>
</mat-form-field>
</div>
</ng-template>
</mat-expansion-panel>
</fieldset>
<fieldset class="fields-group"> <fieldset class="fields-group">
<legend class="group-title"> <legend class="group-title">
@ -638,6 +724,12 @@
[type]="rateLimitsType.EDGE_UPLINK_MESSAGES_PER_EDGE_RATE_LIMIT"> [type]="rateLimitsType.EDGE_UPLINK_MESSAGES_PER_EDGE_RATE_LIMIT">
</tb-rate-limits> </tb-rate-limits>
</div> </div>
<div class="flex flex-1 flex-row xs:flex-col gt-xs:gap-4">
<tb-rate-limits class="flex-1" formControlName="calculatedFieldDebugEventsRateLimit"
[type]="rateLimitsType.CALCULATED_FIELD_DEBUG_EVENT_RATE_LIMIT">
</tb-rate-limits>
<div class="flex-1"></div>
</div>
</ng-template> </ng-template>
</mat-expansion-panel> </mat-expansion-panel>
</fieldset> </fieldset>

View File

@ -118,7 +118,13 @@ export class DefaultTenantProfileConfigurationComponent implements ControlValueA
edgeEventRateLimits: [null, []], edgeEventRateLimits: [null, []],
edgeEventRateLimitsPerEdge: [null, []], edgeEventRateLimitsPerEdge: [null, []],
edgeUplinkMessagesRateLimits: [null, []], edgeUplinkMessagesRateLimits: [null, []],
edgeUplinkMessagesRateLimitsPerEdge: [null, []] edgeUplinkMessagesRateLimitsPerEdge: [null, []],
maxCalculatedFieldsPerEntity: [null, [Validators.required, Validators.min(0)]],
maxArgumentsPerCF: [null, [Validators.required, Validators.min(0)]],
maxDataPointsPerRollingArg: [null, [Validators.required, Validators.min(0)]],
maxStateSizeInKBytes: [null, [Validators.required, Validators.min(0)]],
calculatedFieldDebugEventsRateLimit: [null, []],
maxSingleValueArgumentSizeInKBytes: [null, [Validators.required, Validators.min(0)]],
}); });
this.defaultTenantProfileConfigurationFormGroup.get('smsEnabled').valueChanges.pipe( this.defaultTenantProfileConfigurationFormGroup.get('smsEnabled').valueChanges.pipe(

View File

@ -45,7 +45,8 @@ export enum RateLimitsType {
EDGE_EVENTS_RATE_LIMIT = 'EDGE_EVENTS_RATE_LIMIT', EDGE_EVENTS_RATE_LIMIT = 'EDGE_EVENTS_RATE_LIMIT',
EDGE_EVENTS_PER_EDGE_RATE_LIMIT = 'EDGE_EVENTS_PER_EDGE_RATE_LIMIT', EDGE_EVENTS_PER_EDGE_RATE_LIMIT = 'EDGE_EVENTS_PER_EDGE_RATE_LIMIT',
EDGE_UPLINK_MESSAGES_RATE_LIMIT = 'EDGE_UPLINK_MESSAGES_RATE_LIMIT', EDGE_UPLINK_MESSAGES_RATE_LIMIT = 'EDGE_UPLINK_MESSAGES_RATE_LIMIT',
EDGE_UPLINK_MESSAGES_PER_EDGE_RATE_LIMIT = 'EDGE_UPLINK_MESSAGES_PER_EDGE_RATE_LIMIT' EDGE_UPLINK_MESSAGES_PER_EDGE_RATE_LIMIT = 'EDGE_UPLINK_MESSAGES_PER_EDGE_RATE_LIMIT',
CALCULATED_FIELD_DEBUG_EVENT_RATE_LIMIT = 'CALCULATED_FIELD_DEBUG_EVENT_RATE_LIMIT',
} }
export const rateLimitsLabelTranslationMap = new Map<RateLimitsType, string>( export const rateLimitsLabelTranslationMap = new Map<RateLimitsType, string>(
@ -74,6 +75,7 @@ export const rateLimitsLabelTranslationMap = new Map<RateLimitsType, string>(
[RateLimitsType.EDGE_EVENTS_PER_EDGE_RATE_LIMIT, 'tenant-profile.rate-limits.edge-events-per-edge-rate-limit'], [RateLimitsType.EDGE_EVENTS_PER_EDGE_RATE_LIMIT, 'tenant-profile.rate-limits.edge-events-per-edge-rate-limit'],
[RateLimitsType.EDGE_UPLINK_MESSAGES_RATE_LIMIT, 'tenant-profile.rate-limits.edge-uplink-messages-rate-limit'], [RateLimitsType.EDGE_UPLINK_MESSAGES_RATE_LIMIT, 'tenant-profile.rate-limits.edge-uplink-messages-rate-limit'],
[RateLimitsType.EDGE_UPLINK_MESSAGES_PER_EDGE_RATE_LIMIT, 'tenant-profile.rate-limits.edge-uplink-messages-per-edge-rate-limit'], [RateLimitsType.EDGE_UPLINK_MESSAGES_PER_EDGE_RATE_LIMIT, 'tenant-profile.rate-limits.edge-uplink-messages-per-edge-rate-limit'],
[RateLimitsType.CALCULATED_FIELD_DEBUG_EVENT_RATE_LIMIT, 'tenant-profile.rate-limits.calculated-field-debug-event-rate-limit'],
] ]
); );
@ -103,6 +105,7 @@ export const rateLimitsDialogTitleTranslationMap = new Map<RateLimitsType, strin
[RateLimitsType.EDGE_EVENTS_PER_EDGE_RATE_LIMIT, 'tenant-profile.rate-limits.edit-edge-events-per-edge-rate-limit'], [RateLimitsType.EDGE_EVENTS_PER_EDGE_RATE_LIMIT, 'tenant-profile.rate-limits.edit-edge-events-per-edge-rate-limit'],
[RateLimitsType.EDGE_UPLINK_MESSAGES_RATE_LIMIT, 'tenant-profile.rate-limits.edit-edge-uplink-messages-rate-limit'], [RateLimitsType.EDGE_UPLINK_MESSAGES_RATE_LIMIT, 'tenant-profile.rate-limits.edit-edge-uplink-messages-rate-limit'],
[RateLimitsType.EDGE_UPLINK_MESSAGES_PER_EDGE_RATE_LIMIT, 'tenant-profile.rate-limits.edit-edge-uplink-messages-per-edge-rate-limit'], [RateLimitsType.EDGE_UPLINK_MESSAGES_PER_EDGE_RATE_LIMIT, 'tenant-profile.rate-limits.edit-edge-uplink-messages-per-edge-rate-limit'],
[RateLimitsType.CALCULATED_FIELD_DEBUG_EVENT_RATE_LIMIT, 'tenant-profile.rate-limits.edit-calculated-field-debug-event-rate-limit']
] ]
); );

View File

@ -1068,7 +1068,8 @@
"argument-name-pattern": "Argument name is invalid.", "argument-name-pattern": "Argument name is invalid.",
"argument-name-duplicate": "Argument with such name already exists.", "argument-name-duplicate": "Argument with such name already exists.",
"argument-name-max-length": "Argument name should be less than 256 characters.", "argument-name-max-length": "Argument name should be less than 256 characters.",
"argument-type-required": "Argument type is required." "argument-type-required": "Argument type is required.",
"max-args": "Maximum number of arguments reached."
} }
}, },
"confirm-on-exit": { "confirm-on-exit": {
@ -5474,6 +5475,7 @@
"entities": "Entities", "entities": "Entities",
"rule-engine": "Rule Engine", "rule-engine": "Rule Engine",
"time-to-live": "Time-to-live", "time-to-live": "Time-to-live",
"calculated-fields": "Calculated fields",
"alarms-and-notifications": "Alarms and notifications", "alarms-and-notifications": "Alarms and notifications",
"ota-files-in-bytes": "Files", "ota-files-in-bytes": "Files",
"ws-title": "WS", "ws-title": "WS",
@ -5526,6 +5528,21 @@
"tenant-entity-import-rate-limit": "Entity version load", "tenant-entity-import-rate-limit": "Entity version load",
"tenant-notification-request-rate-limit": "Notification requests", "tenant-notification-request-rate-limit": "Notification requests",
"tenant-notification-requests-per-rule-rate-limit": "Notification requests per notification rule", "tenant-notification-requests-per-rule-rate-limit": "Notification requests per notification rule",
"max-calculated-fields": "Maximum number of calculated fields per entity",
"max-calculated-fields-range": "Maximum number of calculated fields per entity can't be negative",
"max-calculated-fields-required": "Maximum number of calculated fields per entity is required",
"max-data-points-per-rolling-arg": "Maximum number of data points in a time series rolling arguments",
"max-data-points-per-rolling-arg-range": "Maximum number of data points in a time series rolling arguments can't be negative",
"max-data-points-per-rolling-arg-required": "Maximum number of data points in a time series rolling arguments is required",
"max-arguments-per-cf": "Maximum number of arguments per calculated field",
"max-arguments-per-cf-range": "Maximum number of arguments per calculated field can't be negative",
"max-arguments-per-cf-required": "Maximum number of arguments per calculated field is required",
"max-state-size": "Maximum size of the state in KB",
"max-state-size-range": "Maximum size of the state in KB can't be negative",
"max-state-size-required": "Maximum size of the state in KB is required",
"max-value-argument-size": "Maximum size of the single value argument in KB",
"max-value-argument-size-range": "Maximum size of the single value argument in KB can't be negative",
"max-value-argument-size-required": "Maximum size of the single value argument in KB is required",
"max-transport-messages": "Transport messages maximum number", "max-transport-messages": "Transport messages maximum number",
"max-transport-messages-required": "Transport messages maximum number is required.", "max-transport-messages-required": "Transport messages maximum number is required.",
"max-transport-messages-range": "Transport messages maximum number can't be negative", "max-transport-messages-range": "Transport messages maximum number can't be negative",
@ -5597,6 +5614,8 @@
"advanced-settings": "Advanced settings", "advanced-settings": "Advanced settings",
"edit-limit": "Edit limit", "edit-limit": "Edit limit",
"but-less-than": "but less than", "but-less-than": "but less than",
"calculated-field-debug-event-rate-limit": "Calculated field debug events",
"edit-calculated-field-debug-event-rate-limit": "Edit calculated field debug events rate limits",
"edit-transport-tenant-msg-title": "Edit transport tenant messages rate limits", "edit-transport-tenant-msg-title": "Edit transport tenant messages rate limits",
"edit-transport-tenant-telemetry-msg-title": "Edit transport tenant telemetry messages rate limits", "edit-transport-tenant-telemetry-msg-title": "Edit transport tenant telemetry messages rate limits",
"edit-transport-tenant-telemetry-data-points-title": "Edit transport tenant telemetry data points rate limits", "edit-transport-tenant-telemetry-data-points-title": "Edit transport tenant telemetry data points rate limits",