added sms disabled feature for tenant profile configuration

This commit is contained in:
dashevchenko 2023-05-16 15:04:39 +03:00
parent 07ff1ab44a
commit a6c52809cd
17 changed files with 286 additions and 34 deletions

View File

@ -42,6 +42,7 @@ import org.thingsboard.server.common.data.AdminSettings;
import org.thingsboard.server.common.data.FeaturesInfo;
import org.thingsboard.server.common.data.SystemInfo;
import org.thingsboard.server.common.data.UpdateMessage;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.security.model.JwtPair;
@ -52,6 +53,7 @@ import org.thingsboard.server.common.data.sync.vc.AutoCommitSettings;
import org.thingsboard.server.common.data.sync.vc.RepositorySettings;
import org.thingsboard.server.common.data.sync.vc.RepositorySettingsInfo;
import org.thingsboard.server.common.data.sync.vc.VcUtils;
import org.thingsboard.server.dao.audit.AuditLogService;
import org.thingsboard.server.dao.settings.AdminSettingsService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsService;
@ -86,6 +88,7 @@ public class AdminController extends BaseController {
private final TbAutoCommitSettingsService autoCommitSettingsService;
private final UpdateService updateService;
private final SystemInfoService systemInfoService;
private final AuditLogService auditLogService;
@ApiOperation(value = "Get the Administration Settings object using key (getAdminSettings)",
notes = "Get the Administration Settings object using specified string key. Referencing non-existing key will cause an error." + SYSTEM_AUTHORITY_PARAGRAPH)
@ -202,8 +205,15 @@ public class AdminController extends BaseController {
public void sendTestSms(
@ApiParam(value = "A JSON value representing the Test SMS request.")
@RequestBody TestSmsRequest testSmsRequest) throws ThingsboardException {
accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.READ);
SecurityUser user = getCurrentUser();
accessControlService.checkPermission(user, Resource.ADMIN_SETTINGS, Operation.READ);
try {
smsService.sendTestSms(testSmsRequest);
auditLogService.logEntityAction(user.getTenantId(), user.getCustomerId(), user.getId(), user.getName(), user.getId(), user, ActionType.SMS_SENT, null, testSmsRequest.getNumberTo());
} catch (ThingsboardException e) {
auditLogService.logEntityAction(user.getTenantId(), user.getCustomerId(), user.getId(), user.getName(), user.getId(), user, ActionType.SMS_SENT, e, testSmsRequest.getNumberTo());
throw e;
}
}
@ApiOperation(value = "Get repository settings (getRepositorySettings)",

View File

@ -55,6 +55,10 @@ public class TenantApiUsageState extends BaseApiUsageState {
return tenantProfileData.getConfiguration().getProfileThreshold(key);
}
public boolean getProfileFeatureEnabled(ApiUsageRecordKey key) {
return tenantProfileData.getConfiguration().getProfileFeatureEnabled(key);
}
public long getProfileWarnThreshold(ApiUsageRecordKey key) {
return tenantProfileData.getConfiguration().getWarnThreshold(key);
}
@ -63,9 +67,11 @@ public class TenantApiUsageState extends BaseApiUsageState {
ApiUsageStateValue featureValue = ApiUsageStateValue.ENABLED;
for (ApiUsageRecordKey recordKey : ApiUsageRecordKey.getKeys(feature)) {
long value = get(recordKey);
boolean featureEnabled = getProfileFeatureEnabled(recordKey);
long threshold = getProfileThreshold(recordKey);
long warnThreshold = getProfileWarnThreshold(recordKey);
ApiUsageStateValue tmpValue;
if (featureEnabled) {
if (threshold == 0 || value == 0 || value < warnThreshold) {
tmpValue = ApiUsageStateValue.ENABLED;
} else if (value < threshold) {
@ -73,6 +79,9 @@ public class TenantApiUsageState extends BaseApiUsageState {
} else {
tmpValue = ApiUsageStateValue.DISABLED;
}
} else {
tmpValue = ApiUsageStateValue.DISABLED;
}
featureValue = ApiUsageStateValue.toMoreRestricted(featureValue, tmpValue);
}
return setFeatureValue(feature, featureValue) ? Pair.of(feature, featureValue) : null;

View File

@ -20,12 +20,14 @@ import org.springframework.stereotype.Service;
import org.thingsboard.rule.engine.api.SmsService;
import org.thingsboard.rule.engine.api.util.TbNodeUtils;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.security.model.mfa.account.SmsTwoFaAccountConfig;
import org.thingsboard.server.common.data.security.model.mfa.provider.SmsTwoFaProviderConfig;
import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderType;
import org.thingsboard.server.dao.audit.AuditLogService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.model.SecurityUser;
@ -36,10 +38,12 @@ import java.util.Map;
public class SmsTwoFaProvider extends OtpBasedTwoFaProvider<SmsTwoFaProviderConfig, SmsTwoFaAccountConfig> {
private final SmsService smsService;
private final AuditLogService auditLogService;
public SmsTwoFaProvider(CacheManager cacheManager, SmsService smsService) {
public SmsTwoFaProvider(CacheManager cacheManager, SmsService smsService, AuditLogService auditLogService) {
super(cacheManager);
this.smsService = smsService;
this.auditLogService = auditLogService;
}
@ -56,8 +60,13 @@ public class SmsTwoFaProvider extends OtpBasedTwoFaProvider<SmsTwoFaProviderConf
);
String message = TbNodeUtils.processTemplate(providerConfig.getSmsVerificationMessageTemplate(), messageData);
String phoneNumber = accountConfig.getPhoneNumber();
try {
smsService.sendSms(user.getTenantId(), user.getCustomerId(), new String[]{phoneNumber}, message);
auditLogService.logEntityAction(user.getTenantId(), user.getCustomerId(), user.getId(), user.getName(), user.getId(), user, ActionType.SMS_SENT, null, phoneNumber);
} catch (ThingsboardException e) {
auditLogService.logEntityAction(user.getTenantId(), user.getCustomerId(), user.getId(), user.getName(), user.getId(), user, ActionType.SMS_SENT, e, phoneNumber);
throw e;
}
}
@Override

View File

@ -34,6 +34,7 @@ import org.thingsboard.server.common.data.sms.config.TestSmsRequest;
import org.thingsboard.server.common.stats.TbApiUsageReportClient;
import org.thingsboard.server.dao.settings.AdminSettingsService;
import org.thingsboard.server.service.apiusage.TbApiUsageStateService;
import org.thingsboard.server.service.transport.DefaultTransportApiService;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
@ -86,7 +87,7 @@ public class DefaultSmsService implements SmsService {
}
}
private int sendSms(String numberTo, String message) throws ThingsboardException {
protected int sendSms(String numberTo, String message) throws ThingsboardException {
if (this.smsSender == null) {
throw new ThingsboardException("Unable to send SMS: no SMS provider configured!", ThingsboardErrorCode.GENERAL);
}
@ -130,7 +131,9 @@ public class DefaultSmsService implements SmsService {
private int sendSms(SmsSender smsSender, String numberTo, String message) throws ThingsboardException {
try {
return smsSender.sendSms(numberTo, message);
int sentSms = smsSender.sendSms(numberTo, message);
log.trace("Successfully sent sms to number: {}", numberTo);
return sentSms;
} catch (Exception e) {
throw handleException(e);
}

View File

@ -0,0 +1,164 @@
/**
* 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.
*/
package org.thingsboard.server.service.sms;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.springframework.test.context.TestPropertySource;
import org.testcontainers.shaded.org.apache.commons.lang3.RandomStringUtils;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.AdminSettings;
import org.thingsboard.server.common.data.FeaturesInfo;
import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.common.data.tenant.profile.TenantProfileConfiguration;
import org.thingsboard.server.common.data.tenant.profile.TenantProfileData;
import org.thingsboard.server.controller.AbstractControllerTest;
import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.dao.settings.AdminSettingsService;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doReturn;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@DaoSqlTest
@TestPropertySource(properties = {
"usage.stats.report.enabled=true",
"usage.stats.report.interval=1",
})
public class DefaultSmsServiceTest extends AbstractControllerTest {
@SpyBean
private DefaultSmsService defaultSmsService;
@Autowired
private AdminSettingsService adminSettingsService;
private TenantProfile tenantProfile;
@Before
public void before() throws Exception {
loginSysAdmin();
prepareSmsSystemSetting();
}
@After
public void after() throws Exception {
saveTenantProfileWitConfiguration(tenantProfile, new DefaultTenantProfileConfiguration());
adminSettingsService.deleteAdminSettingsByTenantIdAndKey(TenantId.SYS_TENANT_ID, "sms");
resetTokens();
}
@Test
public void testLimitSmsMessagingByTenantProfileSettings() throws Exception {
tenantProfile = getDefaultTenantProfile();
DefaultTenantProfileConfiguration config = createTenantProfileConfigurationWithSmsLimits(10, true);
saveTenantProfileWitConfiguration(tenantProfile, config);
for (int i = 0; i < 10; i++) {
doReturn(1).when(defaultSmsService).sendSms(any(), any());
defaultSmsService.sendSms(tenantId, null, new String[]{RandomStringUtils.randomNumeric(10)}, "Message");
}
//wait 1 sec so that api usage state is updated
TimeUnit.SECONDS.sleep(1);
assertThrows(RuntimeException.class, () -> {
defaultSmsService.sendSms(tenantId, null, new String[]{RandomStringUtils.randomNumeric(10)}, "Message");
}, "SMS sending is disabled due to API limits!");
}
@Test
public void testLimitSmsMessagingIfSmsDisabled() throws Exception {
tenantProfile = getDefaultTenantProfile();
DefaultTenantProfileConfiguration config = createTenantProfileConfigurationWithSmsLimits(0, false);
saveTenantProfileWitConfiguration(tenantProfile, config);
TimeUnit.SECONDS.sleep(1);
assertThrows(RuntimeException.class, () -> {
defaultSmsService.sendSms(tenantId, null, new String[]{RandomStringUtils.randomNumeric(10)}, "Message");
}, "SMS sending is disabled due to API limits!");
//enable sms messaging
DefaultTenantProfileConfiguration config2 = createTenantProfileConfigurationWithSmsLimits(0, true);
saveTenantProfileWitConfiguration(tenantProfile, config2);
TimeUnit.SECONDS.sleep(1);
for (int i = 0; i < 10; i++) {
doReturn(1).when(defaultSmsService).sendSms(any(), any());
defaultSmsService.sendSms(tenantId, null, new String[]{RandomStringUtils.randomNumeric(10)}, "Message");
}
}
private TenantProfile getDefaultTenantProfile() throws Exception {
PageLink pageLink = new PageLink(17);
PageData<TenantProfile> pageData = doGetTypedWithPageLink("/api/tenantProfiles?",
new TypeReference<>(){}, pageLink);
Assert.assertFalse(pageData.hasNext());
Assert.assertEquals(1, pageData.getTotalElements());
List<TenantProfile> tenantProfiles = new ArrayList<>(pageData.getData());
Optional<TenantProfile> optionalDefaultProfile = tenantProfiles.stream().filter(TenantProfile::isDefault).reduce((a, b) -> null);
Assert.assertTrue(optionalDefaultProfile.isPresent());
return optionalDefaultProfile.get();
}
private DefaultTenantProfileConfiguration createTenantProfileConfigurationWithSmsLimits(Integer maxSms, Boolean smsEnabled) {
DefaultTenantProfileConfiguration.DefaultTenantProfileConfigurationBuilder builder = DefaultTenantProfileConfiguration.builder();
builder.maxSms(maxSms);
builder.smsEnabled(smsEnabled);
return builder.build();
}
private void saveTenantProfileWitConfiguration(TenantProfile tenantProfile, TenantProfileConfiguration tenantProfileConfiguration) {
TenantProfileData tenantProfileData = tenantProfile.getProfileData();
tenantProfileData.setConfiguration(tenantProfileConfiguration);
TenantProfile savedTenantProfile = doPost("/api/tenantProfile", tenantProfile, TenantProfile.class);
Assert.assertNotNull(savedTenantProfile);
}
private void prepareSmsSystemSetting() throws Exception {
if (doGet("/api/admin/settings/sms").andReturn().getResponse().getStatus() == 404) {
AdminSettings adminSettings = new AdminSettings();
ObjectNode value = JacksonUtil.newObjectNode();
value.put("numberFrom", "+12543223870");
value.put("accountSid", "testAcc");
value.put("accountToken", "testToken");
value.put("type", "TWILIO");
adminSettings.setKey("sms");
adminSettings.setJsonValue(value);
doPost("/api/admin/settings", adminSettings).andExpect(status().isOk());
}
}
}

View File

@ -38,6 +38,7 @@ public class UsageInfo {
private long maxEmails;
private long sms;
private long maxSms;
private boolean smsEnabled;
private long alarms;
private long maxAlarms;
}

View File

@ -53,7 +53,8 @@ public enum ActionType {
UNASSIGNED_FROM_EDGE(false),
ADDED_COMMENT(false),
UPDATED_COMMENT(false),
DELETED_COMMENT(false);
DELETED_COMMENT(false),
SMS_SENT(false);
private final boolean isRead;

View File

@ -57,6 +57,7 @@ public class DefaultTenantProfileConfiguration implements TenantProfileConfigura
private long maxDPStorageDays;
private int maxRuleNodeExecutionsPerMessage;
private long maxEmails;
private Boolean smsEnabled;
private long maxSms;
private long maxCreatedAlarms;
@ -105,6 +106,16 @@ public class DefaultTenantProfileConfiguration implements TenantProfileConfigura
return 0L;
}
@Override
public boolean getProfileFeatureEnabled(ApiUsageRecordKey key) {
switch (key) {
case SMS_EXEC_COUNT:
return smsEnabled == null || smsEnabled;
default:
return true;
}
}
@Override
public long getWarnThreshold(ApiUsageRecordKey key) {
return (long) (getProfileThreshold(key) * (warnThreshold > 0.0 ? warnThreshold : 0.8));

View File

@ -37,6 +37,9 @@ public interface TenantProfileConfiguration {
@JsonIgnore
long getProfileThreshold(ApiUsageRecordKey key);
@JsonIgnore
boolean getProfileFeatureEnabled(ApiUsageRecordKey key);
@JsonIgnore
long getWarnThreshold(ApiUsageRecordKey key);

View File

@ -326,6 +326,10 @@ public class AuditLogServiceImpl implements AuditLogService {
actionData.put("unassignedEdgeId", strEdgeId);
actionData.put("unassignedEdgeName", strEdgeName);
break;
case SMS_SENT:
String number = extractParameter(String.class, 0, additionalInfo);
actionData.put("recipientNumber", number);
break;
}
return actionData;
}

View File

@ -69,6 +69,7 @@ public class BasicUsageInfoService implements UsageInfoService {
usageInfo.setMaxJsExecutions(profileConfiguration.getMaxJSExecutions());
usageInfo.setMaxEmails(profileConfiguration.getMaxEmails());
usageInfo.setMaxSms(profileConfiguration.getMaxSms());
usageInfo.setSmsEnabled(profileConfiguration.getSmsEnabled());
ApiUsageState apiUsageState = apiUsageStateService.findTenantApiUsageState(tenantId);
if (apiUsageState != null) {
Collection<String> keys = Arrays.asList(

View File

@ -253,7 +253,22 @@
<legend class="group-title">
{{ 'tenant-profile.alarms-and-notifications' | translate }} <span translate>tenant-profile.unlimited</span>
</legend>
<div class="fields-element" fxFlex fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="16px">
<mat-slide-toggle class="slide-toggle-element" fxFlex formControlName="smsEnabled">
{{ 'tenant-profile.sms-enabled' | translate }}
</mat-slide-toggle>
<mat-form-field *ngIf="defaultTenantProfileConfigurationFormGroup.get('smsEnabled').value" fxFlex class="mat-block" appearance="fill">
<mat-label translate>tenant-profile.max-sms</mat-label>
<input matInput required min="0" step="1"
formControlName="maxSms"
type="number">
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxSms').hasError('required')">
{{ 'tenant-profile.max-sms-required' | translate}}
</mat-error>
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxSms').hasError('min')">
{{ 'tenant-profile.max-sms-range' | translate}}
</mat-error>
</mat-form-field>
<div fxFlex fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="16px">
<mat-form-field fxFlex class="mat-block" appearance="fill">
<mat-label translate>tenant-profile.max-emails</mat-label>
<input matInput required min="0" step="1"
@ -279,21 +294,6 @@
</mat-error>
</mat-form-field>
</div>
<div fxFlex fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="16px">
<mat-form-field fxFlex class="mat-block" appearance="fill">
<mat-label translate>tenant-profile.max-sms</mat-label>
<input matInput required min="0" step="1"
formControlName="maxSms"
type="number">
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxSms').hasError('required')">
{{ 'tenant-profile.max-sms-required' | translate}}
</mat-error>
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxSms').hasError('min')">
{{ 'tenant-profile.max-sms-range' | translate}}
</mat-error>
</mat-form-field>
<div fxFlex></div>
</div>
</fieldset>
<fieldset class="fields-group">

View File

@ -18,6 +18,10 @@
padding-top: 15px;
}
.slide-toggle-element {
margin: 7px 0 13px;
}
.group-title > span {
color: rgba(0, 0, 0, 0.54);
}

View File

@ -14,7 +14,7 @@
/// limitations under the License.
///
import { Component, forwardRef, Input, OnInit } from '@angular/core';
import { Component, forwardRef, Input, OnDestroy, OnInit } from '@angular/core';
import { ControlValueAccessor, UntypedFormBuilder, UntypedFormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms';
import { Store } from '@ngrx/store';
import { AppState } from '@app/core/core.state';
@ -22,6 +22,8 @@ import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { DefaultTenantProfileConfiguration, TenantProfileConfiguration } from '@shared/models/tenant.model';
import { isDefinedAndNotNull } from '@core/utils';
import { RateLimitsType } from './rate-limits/rate-limits.models';
import { takeUntil } from 'rxjs/operators';
import { Subject } from 'rxjs';
@Component({
selector: 'tb-default-tenant-profile-configuration',
@ -33,11 +35,12 @@ import { RateLimitsType } from './rate-limits/rate-limits.models';
multi: true
}]
})
export class DefaultTenantProfileConfigurationComponent implements ControlValueAccessor, OnInit {
export class DefaultTenantProfileConfigurationComponent implements ControlValueAccessor, OnInit, OnDestroy {
defaultTenantProfileConfigurationFormGroup: UntypedFormGroup;
private requiredValue: boolean;
private destroy$ = new Subject<void>();
get required(): boolean {
return this.requiredValue;
}
@ -81,7 +84,8 @@ export class DefaultTenantProfileConfigurationComponent implements ControlValueA
maxDPStorageDays: [null, [Validators.required, Validators.min(0)]],
maxRuleNodeExecutionsPerMessage: [null, [Validators.required, Validators.min(0)]],
maxEmails: [null, [Validators.required, Validators.min(0)]],
maxSms: [null, [Validators.required, Validators.min(0)]],
maxSms: [null, []],
smsEnabled: [null, []],
maxCreatedAlarms: [null, [Validators.required, Validators.min(0)]],
defaultStorageTtlDays: [null, [Validators.required, Validators.min(0)]],
alarmsTtlDays: [null, [Validators.required, Validators.min(0)]],
@ -100,11 +104,33 @@ export class DefaultTenantProfileConfigurationComponent implements ControlValueA
wsUpdatesPerSessionRateLimit: [null, []],
cassandraQueryTenantRateLimitsConfiguration: [null, []]
});
this.defaultTenantProfileConfigurationFormGroup.get('smsEnabled').valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe((value: boolean) => {
this.maxSmsValidation(value);
}
);
this.defaultTenantProfileConfigurationFormGroup.valueChanges.subscribe(() => {
this.updateModel();
});
}
private maxSmsValidation(smsEnabled: boolean) {
if (smsEnabled) {
this.defaultTenantProfileConfigurationFormGroup.get('maxSms').addValidators([Validators.required, Validators.min(0)]);
} else {
this.defaultTenantProfileConfigurationFormGroup.get('maxSms').clearValidators();
}
this.defaultTenantProfileConfigurationFormGroup.get('maxSms').updateValueAndValidity({emitEvent: false});
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
@ -126,6 +152,7 @@ export class DefaultTenantProfileConfigurationComponent implements ControlValueA
writeValue(value: DefaultTenantProfileConfiguration | null): void {
if (isDefinedAndNotNull(value)) {
this.maxSmsValidation(value.smsEnabled);
this.defaultTenantProfileConfigurationFormGroup.patchValue(value, {emitEvent: false});
}
}

View File

@ -62,7 +62,8 @@ export enum ActionType {
TIMESERIES_UPDATED = 'TIMESERIES_UPDATED',
TIMESERIES_DELETED = 'TIMESERIES_DELETED',
ASSIGNED_TO_EDGE = 'ASSIGNED_TO_EDGE',
UNASSIGNED_FROM_EDGE = 'UNASSIGNED_FROM_EDGE'
UNASSIGNED_FROM_EDGE = 'UNASSIGNED_FROM_EDGE',
SMS_SENT = 'SMS_SENT'
}
export enum ActionStatus {
@ -105,7 +106,8 @@ export const actionTypeTranslations = new Map<ActionType, string>(
[ActionType.TIMESERIES_UPDATED, 'audit-log.type-timeseries-updated'],
[ActionType.TIMESERIES_DELETED, 'audit-log.type-timeseries-deleted'],
[ActionType.ASSIGNED_TO_EDGE, 'audit-log.type-assigned-to-edge'],
[ActionType.UNASSIGNED_FROM_EDGE, 'audit-log.type-unassigned-from-edge']
[ActionType.UNASSIGNED_FROM_EDGE, 'audit-log.type-unassigned-from-edge'],
[ActionType.SMS_SENT, 'audit-log.type-sms-sent'],
]
);

View File

@ -54,6 +54,7 @@ export interface DefaultTenantProfileConfiguration {
maxRuleNodeExecutionsPerMessage: number;
maxEmails: number;
maxSms: number;
smsEnabled: boolean;
maxCreatedAlarms: number;
tenantServerRestLimitsConfiguration: string;
@ -105,6 +106,7 @@ export function createTenantProfileConfiguration(type: TenantProfileType): Tenan
maxRuleNodeExecutionsPerMessage: 0,
maxEmails: 0,
maxSms: 0,
smsEnabled: true,
maxCreatedAlarms: 0,
tenantServerRestLimitsConfiguration: '',
customerServerRestLimitsConfiguration: '',

View File

@ -807,7 +807,8 @@
"type-provision-success": "Device provisioned",
"type-provision-failure": "Device provisioning was failed",
"type-timeseries-updated": "Telemetry updated",
"type-timeseries-deleted": "Telemetry deleted"
"type-timeseries-deleted": "Telemetry deleted",
"type-sms-sent": "SMS sent"
},
"confirm-on-exit": {
"message": "You have unsaved changes. Are you sure you want to leave this page?",