diff --git a/application/src/main/java/org/thingsboard/server/controller/NotificationController.java b/application/src/main/java/org/thingsboard/server/controller/NotificationController.java index 0f4a288e33..f72b2dacae 100644 --- a/application/src/main/java/org/thingsboard/server/controller/NotificationController.java +++ b/application/src/main/java/org/thingsboard/server/controller/NotificationController.java @@ -44,6 +44,7 @@ import org.thingsboard.server.common.data.notification.NotificationRequest; import org.thingsboard.server.common.data.notification.NotificationRequestInfo; import org.thingsboard.server.common.data.notification.NotificationRequestPreview; import org.thingsboard.server.common.data.notification.settings.NotificationSettings; +import org.thingsboard.server.common.data.notification.settings.UserNotificationSettings; import org.thingsboard.server.common.data.notification.targets.NotificationRecipient; import org.thingsboard.server.common.data.notification.targets.NotificationTarget; import org.thingsboard.server.common.data.notification.targets.NotificationTargetType; @@ -297,7 +298,7 @@ public class NotificationController extends BaseController { if (targetType == NotificationTargetType.PLATFORM_USERS) { PageData recipients = notificationTargetService.findRecipientsForNotificationTargetConfig(user.getTenantId(), (PlatformUsersNotificationTargetConfig) target.getConfiguration(), new PageLink(recipientsPreviewSize, 0, null, - new SortOrder("createdTime", SortOrder.Direction.DESC))); + SortOrder.BY_CREATED_TIME_DESC)); recipientsCount = (int) recipients.getTotalElements(); recipientsPart = recipients.getData().stream().map(r -> (NotificationRecipient) r).collect(Collectors.toList()); } else { @@ -431,10 +432,23 @@ public class NotificationController extends BaseController { notes = "Returns the list of delivery methods that are properly configured and are allowed to be used for sending notifications." + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) @GetMapping("/notification/deliveryMethods") - @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") public Set getAvailableDeliveryMethods(@AuthenticationPrincipal SecurityUser user) throws ThingsboardException { - accessControlService.checkPermission(user, Resource.ADMIN_SETTINGS, Operation.READ); return notificationCenter.getAvailableDeliveryMethods(user.getTenantId()); } + + @PostMapping("/notification/settings/user") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + public UserNotificationSettings saveUserNotificationSettings(@RequestBody @Valid UserNotificationSettings settings, + @AuthenticationPrincipal SecurityUser user) { + return notificationSettingsService.saveUserNotificationSettings(user.getTenantId(), user.getId(), settings); + } + + @GetMapping("/notification/settings/user") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + public UserNotificationSettings getUserNotificationSettings(@AuthenticationPrincipal SecurityUser user) { + return notificationSettingsService.getUserNotificationSettings(user.getTenantId(), user.getId(), true); + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationCenter.java b/application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationCenter.java index cb77cc0948..042d5cbf00 100644 --- a/application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationCenter.java +++ b/application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationCenter.java @@ -39,6 +39,7 @@ import org.thingsboard.server.common.data.notification.NotificationRequestStatus import org.thingsboard.server.common.data.notification.NotificationStatus; import org.thingsboard.server.common.data.notification.info.RuleOriginatedNotificationInfo; import org.thingsboard.server.common.data.notification.settings.NotificationSettings; +import org.thingsboard.server.common.data.notification.settings.UserNotificationSettings; import org.thingsboard.server.common.data.notification.targets.NotificationRecipient; import org.thingsboard.server.common.data.notification.targets.NotificationTarget; import org.thingsboard.server.common.data.notification.targets.platform.PlatformUsersNotificationTargetConfig; @@ -237,7 +238,17 @@ public class DefaultNotificationCenter extends AbstractSubscriptionService imple private void processForRecipient(NotificationDeliveryMethod deliveryMethod, NotificationRecipient recipient, NotificationProcessingContext ctx) throws Exception { if (ctx.getStats().contains(deliveryMethod, recipient.getId())) { throw new AlreadySentException(); + } else { + ctx.getStats().reportProcessed(deliveryMethod, recipient.getId()); } + + if (recipient instanceof User) { + UserNotificationSettings settings = notificationSettingsService.getUserNotificationSettings(ctx.getTenantId(), ((User) recipient).getId(), false); + if (!settings.isEnabled(ctx.getNotificationType(), deliveryMethod)) { + throw new RuntimeException("User disabled " + deliveryMethod.getName() + " notifications of this type"); + } + } + NotificationChannel notificationChannel = channels.get(deliveryMethod); DeliveryMethodNotificationTemplate processedTemplate = ctx.getProcessedTemplate(deliveryMethod, recipient); @@ -251,7 +262,7 @@ public class DefaultNotificationCenter extends AbstractSubscriptionService imple Notification notification = Notification.builder() .requestId(request.getId()) .recipientId(recipient.getId()) - .type(ctx.getNotificationTemplate().getNotificationType()) + .type(ctx.getNotificationType()) .subject(processedTemplate.getSubject()) .text(processedTemplate.getBody()) .additionalConfig(processedTemplate.getAdditionalConfig()) diff --git a/application/src/main/java/org/thingsboard/server/service/notification/NotificationProcessingContext.java b/application/src/main/java/org/thingsboard/server/service/notification/NotificationProcessingContext.java index 27a9cabe43..c4d8895266 100644 --- a/application/src/main/java/org/thingsboard/server/service/notification/NotificationProcessingContext.java +++ b/application/src/main/java/org/thingsboard/server/service/notification/NotificationProcessingContext.java @@ -23,6 +23,7 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; import org.thingsboard.server.common.data.notification.NotificationRequest; import org.thingsboard.server.common.data.notification.NotificationRequestStats; +import org.thingsboard.server.common.data.notification.NotificationType; import org.thingsboard.server.common.data.notification.settings.NotificationDeliveryMethodConfig; import org.thingsboard.server.common.data.notification.settings.NotificationSettings; import org.thingsboard.server.common.data.notification.targets.NotificationRecipient; @@ -52,6 +53,8 @@ public class NotificationProcessingContext { private final Set deliveryMethods; @Getter private final NotificationTemplate notificationTemplate; + @Getter + private final NotificationType notificationType; private final Map templates; @Getter @@ -65,6 +68,7 @@ public class NotificationProcessingContext { this.deliveryMethods = deliveryMethods; this.settings = settings; this.notificationTemplate = template; + this.notificationType = template.getNotificationType(); this.templates = new EnumMap<>(NotificationDeliveryMethod.class); this.stats = new NotificationRequestStats(); init(); diff --git a/application/src/test/java/org/thingsboard/server/service/notification/NotificationApiTest.java b/application/src/test/java/org/thingsboard/server/service/notification/NotificationApiTest.java index 851550eee7..e9c0000565 100644 --- a/application/src/test/java/org/thingsboard/server/service/notification/NotificationApiTest.java +++ b/application/src/test/java/org/thingsboard/server/service/notification/NotificationApiTest.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.service.notification; +import com.google.common.util.concurrent.SettableFuture; import lombok.extern.slf4j.Slf4j; import org.assertj.core.data.Offset; import org.java_websocket.client.WebSocketClient; @@ -23,6 +24,7 @@ import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.thingsboard.rule.engine.api.NotificationCenter; import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.id.NotificationRuleId; import org.thingsboard.server.common.data.id.NotificationTargetId; import org.thingsboard.server.common.data.notification.Notification; import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; @@ -35,6 +37,7 @@ import org.thingsboard.server.common.data.notification.NotificationRequestStatus import org.thingsboard.server.common.data.notification.NotificationType; import org.thingsboard.server.common.data.notification.settings.NotificationSettings; import org.thingsboard.server.common.data.notification.settings.SlackNotificationDeliveryMethodConfig; +import org.thingsboard.server.common.data.notification.settings.UserNotificationSettings; import org.thingsboard.server.common.data.notification.targets.NotificationTarget; import org.thingsboard.server.common.data.notification.targets.platform.CustomerUsersFilter; import org.thingsboard.server.common.data.notification.targets.platform.PlatformUsersNotificationTargetConfig; @@ -59,6 +62,7 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.UUID; import java.util.concurrent.TimeUnit; import static org.assertj.core.api.Assertions.assertThat; @@ -470,6 +474,66 @@ public class NotificationApiTest extends AbstractNotificationApiTest { assertThat(stats.getSent().get(NotificationDeliveryMethod.WEB)).hasValue(1); } + @Test + public void testUserNotificationSettings() throws Exception { + var entityActionNotificationPref = new UserNotificationSettings.NotificationPref(); + entityActionNotificationPref.setEnabled(true); + entityActionNotificationPref.setEnabledDeliveryMethods(Map.of( + NotificationDeliveryMethod.WEB, true, + NotificationDeliveryMethod.SMS, false, + NotificationDeliveryMethod.EMAIL, false + )); + + var entitiesLimitNotificationPref = new UserNotificationSettings.NotificationPref(); + entitiesLimitNotificationPref.setEnabled(true); + entitiesLimitNotificationPref.setEnabledDeliveryMethods(Map.of( + NotificationDeliveryMethod.SMS, true, + NotificationDeliveryMethod.WEB, false, + NotificationDeliveryMethod.EMAIL, false + )); + + var apiUsageLimitNotificationPref = new UserNotificationSettings.NotificationPref(); + apiUsageLimitNotificationPref.setEnabled(false); + apiUsageLimitNotificationPref.setEnabledDeliveryMethods(Map.of( + NotificationDeliveryMethod.WEB, true, + NotificationDeliveryMethod.SMS, false, + NotificationDeliveryMethod.EMAIL, false + )); + + UserNotificationSettings settings = new UserNotificationSettings(Map.of( + NotificationType.ENTITY_ACTION, entityActionNotificationPref, + NotificationType.ENTITIES_LIMIT, entitiesLimitNotificationPref, + NotificationType.API_USAGE_LIMIT, apiUsageLimitNotificationPref + )); + doPost("/api/notification/settings/user", settings, UserNotificationSettings.class); + + var entityActionNotificationTemplate = createNotificationTemplate(NotificationType.ENTITY_ACTION, "Entity action", "Entity action", NotificationDeliveryMethod.WEB); + var entitiesLimitNotificationTemplate = createNotificationTemplate(NotificationType.ENTITIES_LIMIT, "Entities limit", "Entities limit", NotificationDeliveryMethod.WEB); + var apiUsageLimitNotificationTemplate = createNotificationTemplate(NotificationType.API_USAGE_LIMIT, "API usage limit", "API usage limit", NotificationDeliveryMethod.WEB); + NotificationTarget target = createNotificationTarget(tenantAdminUserId); + + NotificationRequest notificationRequest = NotificationRequest.builder() + .tenantId(tenantId) + .templateId(entityActionNotificationTemplate.getId()) + .originatorEntityId(tenantAdminUserId) + .targets(List.of(target.getUuidId())) + .ruleId(new NotificationRuleId(UUID.randomUUID())) // to trigger user settings check + .build(); + NotificationRequestStats stats = submitNotificationRequestAndWait(notificationRequest); + assertThat(stats.getErrors()).isEmpty(); + assertThat(stats.getSent().get(NotificationDeliveryMethod.WEB).get()).isOne(); + + notificationRequest.setTemplateId(entitiesLimitNotificationTemplate.getId()); + stats = submitNotificationRequestAndWait(notificationRequest); + assertThat(stats.getSent().get(NotificationDeliveryMethod.WEB)).matches(n -> n == null || n.get() == 0); + assertThat(stats.getErrors().get(NotificationDeliveryMethod.WEB).values()).first().asString().contains("disabled"); + + notificationRequest.setTemplateId(apiUsageLimitNotificationTemplate.getId()); + stats = submitNotificationRequestAndWait(notificationRequest); + assertThat(stats.getSent().get(NotificationDeliveryMethod.WEB)).matches(n -> n == null || n.get() == 0); + assertThat(stats.getErrors().get(NotificationDeliveryMethod.WEB).values()).first().asString().contains("disabled"); + } + @Test public void testSlackNotifications() throws Exception { NotificationSettings settings = new NotificationSettings(); @@ -524,6 +588,12 @@ public class NotificationApiTest extends AbstractNotificationApiTest { assertThat(stats.getErrors().get(NotificationDeliveryMethod.SLACK).values()).containsExactly(errorMessage); } + private NotificationRequestStats submitNotificationRequestAndWait(NotificationRequest notificationRequest) throws Exception { + SettableFuture future = SettableFuture.create(); + notificationCenter.processNotificationRequest(notificationRequest.getTenantId(), notificationRequest, future::set); + return future.get(30, TimeUnit.SECONDS); + } + private void checkFullNotificationsUpdate(UnreadNotificationsUpdate notificationsUpdate, String... expectedNotifications) { assertThat(notificationsUpdate.getNotifications()).extracting(Notification::getText).containsOnly(expectedNotifications); assertThat(notificationsUpdate.getNotifications()).extracting(Notification::getType).containsOnly(DEFAULT_NOTIFICATION_TYPE); diff --git a/application/src/test/java/org/thingsboard/server/service/notification/MockNotificationSettingsService.java b/application/src/test/java/org/thingsboard/server/service/notification/TestNotificationSettingsService.java similarity index 60% rename from application/src/test/java/org/thingsboard/server/service/notification/MockNotificationSettingsService.java rename to application/src/test/java/org/thingsboard/server/service/notification/TestNotificationSettingsService.java index a49f8dc8cb..9b4ced8567 100644 --- a/application/src/test/java/org/thingsboard/server/service/notification/MockNotificationSettingsService.java +++ b/application/src/test/java/org/thingsboard/server/service/notification/TestNotificationSettingsService.java @@ -19,14 +19,20 @@ import org.springframework.context.annotation.Primary; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.dao.notification.DefaultNotificationSettingsService; +import org.thingsboard.server.dao.notification.NotificationTargetService; +import org.thingsboard.server.dao.notification.NotificationTemplateService; import org.thingsboard.server.dao.settings.AdminSettingsService; +import org.thingsboard.server.dao.user.UserSettingsService; @Service @Primary -public class MockNotificationSettingsService extends DefaultNotificationSettingsService { +public class TestNotificationSettingsService extends DefaultNotificationSettingsService { - public MockNotificationSettingsService(AdminSettingsService adminSettingsService) { - super(adminSettingsService, null, null, null); + public TestNotificationSettingsService(AdminSettingsService adminSettingsService, + NotificationTargetService notificationTargetService, + NotificationTemplateService notificationTemplateService, + UserSettingsService userSettingsService) { + super(adminSettingsService, notificationTargetService, notificationTemplateService, null, userSettingsService); } @Override diff --git a/application/src/test/resources/application-test.properties b/application/src/test/resources/application-test.properties index 99055e0e5f..d96af8a910 100644 --- a/application/src/test/resources/application-test.properties +++ b/application/src/test/resources/application-test.properties @@ -68,3 +68,5 @@ sql.ttl.audit_logs.ttl=2592000 sql.edge_events.partition_size=168 sql.ttl.edge_events.edge_event_ttl=2592000 + +server.log_controller_error_stack_trace=false diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/notification/NotificationSettingsService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/notification/NotificationSettingsService.java index a5433915b3..9fca174b3a 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/notification/NotificationSettingsService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/notification/NotificationSettingsService.java @@ -16,7 +16,9 @@ package org.thingsboard.server.dao.notification; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.notification.settings.NotificationSettings; +import org.thingsboard.server.common.data.notification.settings.UserNotificationSettings; public interface NotificationSettingsService { @@ -24,6 +26,10 @@ public interface NotificationSettingsService { NotificationSettings findNotificationSettings(TenantId tenantId); + UserNotificationSettings saveUserNotificationSettings(TenantId tenantId, UserId userId, UserNotificationSettings settings); + + UserNotificationSettings getUserNotificationSettings(TenantId tenantId, UserId userId, boolean format); + void createDefaultNotificationConfigs(TenantId tenantId); void updateDefaultNotificationConfigs(TenantId tenantId); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationRequestStats.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationRequestStats.java index 619e1ad38f..691ecf8bc1 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationRequestStats.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationRequestStats.java @@ -54,7 +54,6 @@ public class NotificationRequestStats { public void reportSent(NotificationDeliveryMethod deliveryMethod, NotificationRecipient recipient) { sent.computeIfAbsent(deliveryMethod, k -> new AtomicInteger()).incrementAndGet(); - processedRecipients.computeIfAbsent(deliveryMethod, k -> ConcurrentHashMap.newKeySet()).add(recipient.getId()); } public void reportError(NotificationDeliveryMethod deliveryMethod, Throwable error, NotificationRecipient recipient) { @@ -68,6 +67,10 @@ public class NotificationRequestStats { errors.computeIfAbsent(deliveryMethod, k -> new ConcurrentHashMap<>()).put(recipient.getTitle(), errorMessage); } + public void reportProcessed(NotificationDeliveryMethod deliveryMethod, Object recipientId) { + processedRecipients.computeIfAbsent(deliveryMethod, k -> ConcurrentHashMap.newKeySet()).add(recipientId); + } + public boolean contains(NotificationDeliveryMethod deliveryMethod, Object recipientId) { Set processedRecipients = this.processedRecipients.get(deliveryMethod); return processedRecipients != null && processedRecipients.contains(recipientId); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/settings/UserNotificationSettings.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/settings/UserNotificationSettings.java new file mode 100644 index 0000000000..0ad7cc6d78 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/settings/UserNotificationSettings.java @@ -0,0 +1,82 @@ +/** + * 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.common.data.notification.settings; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; +import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; +import org.thingsboard.server.common.data.notification.NotificationType; +import org.thingsboard.server.common.data.notification.targets.NotificationTargetType; + +import javax.validation.Valid; +import javax.validation.constraints.AssertTrue; +import javax.validation.constraints.NotNull; +import java.util.Collections; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +@Data +public class UserNotificationSettings { + + @NotNull + @Valid + private final Map prefs; + + public static final UserNotificationSettings DEFAULT = new UserNotificationSettings(Collections.emptyMap()); + + public static final Set deliveryMethods = NotificationTargetType.PLATFORM_USERS.getSupportedDeliveryMethods(); + + @JsonCreator + public UserNotificationSettings(@JsonProperty("prefs") Map prefs) { + this.prefs = prefs; + } + + public boolean isEnabled(NotificationType notificationType, NotificationDeliveryMethod deliveryMethod) { + NotificationPref pref = prefs.get(notificationType); + if (pref == null) { + return true; + } + if (!pref.isEnabled()) { + return false; + } + return pref.getEnabledDeliveryMethods().getOrDefault(deliveryMethod, true); + } + + @Data + public static class NotificationPref { + private boolean enabled; + @NotNull + private Map enabledDeliveryMethods; + + public static NotificationPref createDefault() { + NotificationPref pref = new NotificationPref(); + pref.setEnabled(true); + pref.setEnabledDeliveryMethods(deliveryMethods.stream().collect(Collectors.toMap(v -> v, v -> true))); + return pref; + } + + @JsonIgnore + @AssertTrue(message = "Only email, Web and SMS delivery methods are allowed") + public boolean isValid() { + return enabledDeliveryMethods.entrySet().stream() + .allMatch(entry -> deliveryMethods.contains(entry.getKey()) && entry.getValue() != null); + } + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/page/SortOrder.java b/common/data/src/main/java/org/thingsboard/server/common/data/page/SortOrder.java index 5c540995aa..62a6377519 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/page/SortOrder.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/page/SortOrder.java @@ -36,4 +36,6 @@ public class SortOrder { ASC, DESC } + public static final SortOrder BY_CREATED_TIME_DESC = new SortOrder("createdTime", Direction.DESC); + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/settings/UserSettingsType.java b/common/data/src/main/java/org/thingsboard/server/common/data/settings/UserSettingsType.java index b19dbbbaee..cd627821ac 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/settings/UserSettingsType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/settings/UserSettingsType.java @@ -19,7 +19,7 @@ import lombok.Getter; public enum UserSettingsType { - GENERAL, VISITED_DASHBOARDS(true), QUICK_LINKS, DOC_LINKS, DASHBOARDS, GETTING_STARTED; + GENERAL, VISITED_DASHBOARDS(true), QUICK_LINKS, DOC_LINKS, DASHBOARDS, GETTING_STARTED, NOTIFICATIONS; @Getter private final boolean reserved; diff --git a/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationSettingsService.java b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationSettingsService.java index 4262decfed..4faed9cb20 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationSettingsService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationSettingsService.java @@ -16,6 +16,7 @@ package org.thingsboard.server.dao.notification; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; @@ -25,8 +26,11 @@ import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.AdminSettings; import org.thingsboard.server.common.data.CacheConstants; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.notification.NotificationType; import org.thingsboard.server.common.data.notification.settings.NotificationSettings; +import org.thingsboard.server.common.data.notification.settings.UserNotificationSettings; +import org.thingsboard.server.common.data.notification.settings.UserNotificationSettings.NotificationPref; import org.thingsboard.server.common.data.notification.targets.NotificationTarget; import org.thingsboard.server.common.data.notification.targets.platform.AffectedTenantAdministratorsFilter; import org.thingsboard.server.common.data.notification.targets.platform.AffectedUserFilter; @@ -38,20 +42,28 @@ import org.thingsboard.server.common.data.notification.targets.platform.TenantAd import org.thingsboard.server.common.data.notification.targets.platform.UsersFilter; import org.thingsboard.server.common.data.notification.targets.platform.UsersFilterType; import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.settings.UserSettings; +import org.thingsboard.server.common.data.settings.UserSettingsType; import org.thingsboard.server.dao.settings.AdminSettingsService; +import org.thingsboard.server.dao.user.UserSettingsService; import java.util.Collections; +import java.util.EnumMap; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.Optional; @Service @RequiredArgsConstructor +@Slf4j public class DefaultNotificationSettingsService implements NotificationSettingsService { private final AdminSettingsService adminSettingsService; private final NotificationTargetService notificationTargetService; private final NotificationTemplateService notificationTemplateService; private final DefaultNotifications defaultNotifications; + private final UserSettingsService userSettingsService; private static final String SETTINGS_KEY = "notifications"; @@ -81,6 +93,58 @@ public class DefaultNotificationSettingsService implements NotificationSettingsS }); } + @Override + public UserNotificationSettings saveUserNotificationSettings(TenantId tenantId, UserId userId, UserNotificationSettings settings) { + UserSettings userSettings = new UserSettings(); + userSettings.setUserId(userId); + userSettings.setType(UserSettingsType.NOTIFICATIONS); + userSettings.setSettings(JacksonUtil.valueToTree(settings)); + userSettingsService.saveUserSettings(tenantId, userSettings); + return formatUserNotificationSettings(settings); + } + + @Override + public UserNotificationSettings getUserNotificationSettings(TenantId tenantId, UserId userId, boolean format) { + UserSettings userSettings = userSettingsService.findUserSettings(tenantId, userId, UserSettingsType.NOTIFICATIONS); + UserNotificationSettings settings = null; + if (userSettings != null) { + try { + settings = JacksonUtil.treeToValue(userSettings.getSettings(), UserNotificationSettings.class); + } catch (Exception e) { + log.warn("Failed to parse notification settings for user {}", userId, e); + } + } + if (settings == null) { + settings = UserNotificationSettings.DEFAULT; + } + if (format) { + settings = formatUserNotificationSettings(settings); + } + return settings; + } + + private UserNotificationSettings formatUserNotificationSettings(UserNotificationSettings settings) { + Map prefs = new EnumMap<>(NotificationType.class); + if (settings != null) { + prefs.putAll(settings.getPrefs()); + } + NotificationPref defaultPref = NotificationPref.createDefault(); + for (NotificationType notificationType : NotificationType.values()) { + NotificationPref pref = prefs.get(notificationType); + if (pref == null) { + prefs.put(notificationType, defaultPref); + } else { + var enabledDeliveryMethods = new LinkedHashMap<>(pref.getEnabledDeliveryMethods()); + // in case a new delivery method was added to the platform + UserNotificationSettings.deliveryMethods.forEach(deliveryMethod -> { + enabledDeliveryMethods.putIfAbsent(deliveryMethod, true); + }); + pref.setEnabledDeliveryMethods(enabledDeliveryMethods); + } + } + return new UserNotificationSettings(prefs); + } + @Transactional(propagation = Propagation.NOT_SUPPORTED) // so that parent transaction is not aborted on method failure @Override public void createDefaultNotificationConfigs(TenantId tenantId) { diff --git a/ui-ngx/src/app/core/http/notification.service.ts b/ui-ngx/src/app/core/http/notification.service.ts index ec64621aeb..20e10c91e2 100644 --- a/ui-ngx/src/app/core/http/notification.service.ts +++ b/ui-ngx/src/app/core/http/notification.service.ts @@ -31,6 +31,7 @@ import { NotificationTarget, NotificationTemplate, NotificationType, + NotificationUserSettings, SlackChanelType, SlackConversation } from '@shared/models/notification.models'; @@ -174,4 +175,12 @@ export class NotificationService { } return this.http.get>(url, defaultHttpOptionsFromConfig(config)); } + + public getNotificationUserSettings(config?: RequestConfig): Observable { + return this.http.get(`/api/notification/settings/user`, defaultHttpOptionsFromConfig(config)); + } + + public saveNotificationUserSettings(settings: NotificationUserSettings, config?: RequestConfig): Observable { + return this.http.post('/api/notification/settings/user', settings, defaultHttpOptionsFromConfig(config)); + } } diff --git a/ui-ngx/src/app/modules/home/pages/account/account-routing.module.ts b/ui-ngx/src/app/modules/home/pages/account/account-routing.module.ts index 9fcaecddfd..e2c997f654 100644 --- a/ui-ngx/src/app/modules/home/pages/account/account-routing.module.ts +++ b/ui-ngx/src/app/modules/home/pages/account/account-routing.module.ts @@ -23,6 +23,9 @@ import { profileRoutes } from '@home/pages/profile/profile-routing.module'; import { getCurrentAuthState } from '@core/auth/auth.selectors'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; +import { + notificationUserSettingsRoutes +} from '@home/pages/notification/settings/notification-settings-routing.modules'; const routes: Routes = [ { @@ -49,7 +52,8 @@ const routes: Routes = [ } }, ...profileRoutes, - ...securityRoutes + ...securityRoutes, + ...notificationUserSettingsRoutes ] } ]; diff --git a/ui-ngx/src/app/modules/home/pages/notification/notification.module.ts b/ui-ngx/src/app/modules/home/pages/notification/notification.module.ts index 5ab7bfa957..389bf358d6 100644 --- a/ui-ngx/src/app/modules/home/pages/notification/notification.module.ts +++ b/ui-ngx/src/app/modules/home/pages/notification/notification.module.ts @@ -35,6 +35,10 @@ import { EscalationFormComponent } from '@home/pages/notification/rule/escalatio import { EscalationsComponent } from '@home/pages/notification/rule/escalations.component'; import { RuleNotificationDialogComponent } from '@home/pages/notification/rule/rule-notification-dialog.component'; import { RuleTableHeaderComponent } from '@home/pages/notification/rule/rule-table-header.component'; +import { NotificationSettingsComponent } from '@home/pages/notification/settings/notification-settings.component'; +import { + NotificationSettingFormComponent +} from '@home/pages/notification/settings/notification-setting-form.component'; @NgModule({ declarations: [ @@ -49,7 +53,9 @@ import { RuleTableHeaderComponent } from '@home/pages/notification/rule/rule-tab EscalationFormComponent, EscalationsComponent, RuleNotificationDialogComponent, - RuleTableHeaderComponent + RuleTableHeaderComponent, + NotificationSettingsComponent, + NotificationSettingFormComponent ], imports: [ CommonModule, diff --git a/ui-ngx/src/app/modules/home/pages/notification/settings/notification-setting-form.component.html b/ui-ngx/src/app/modules/home/pages/notification/settings/notification-setting-form.component.html new file mode 100644 index 0000000000..284cd67fa1 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/notification/settings/notification-setting-form.component.html @@ -0,0 +1,40 @@ + +
+
+
+ + + {{notificationTemplateTypeTranslateMap.get(notificationSettingsFormGroup.get('name').value)?.name | translate}} + + +
+
+
+ +
+
+
+
diff --git a/ui-ngx/src/app/modules/home/pages/notification/settings/notification-setting-form.component.scss b/ui-ngx/src/app/modules/home/pages/notification/settings/notification-setting-form.component.scss new file mode 100644 index 0000000000..ee37181a09 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/notification/settings/notification-setting-form.component.scss @@ -0,0 +1,24 @@ +/** + * 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. + */ +:host { + .notification-type { + font-size: 14px; + + &-disabled { + color: rgba(0, 0, 0, 0.38) + } + } +} diff --git a/ui-ngx/src/app/modules/home/pages/notification/settings/notification-setting-form.component.ts b/ui-ngx/src/app/modules/home/pages/notification/settings/notification-setting-form.component.ts new file mode 100644 index 0000000000..76e9babf00 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/notification/settings/notification-setting-form.component.ts @@ -0,0 +1,126 @@ +/// +/// 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. +/// + +import { Component, forwardRef, Input, OnDestroy, OnInit } from '@angular/core'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; +import { UtilsService } from '@core/services/utils.service'; +import { isDefinedAndNotNull } from '@core/utils'; +import { Subscription } from 'rxjs'; +import { + NotificationDeliveryMethod, + NotificationTemplateTypeTranslateMap, + NotificationUserSetting +} from '@shared/models/notification.models'; + +@Component({ + selector: 'tb-notification-setting-form', + templateUrl: './notification-setting-form.component.html', + styleUrls: ['./notification-setting-form.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => NotificationSettingFormComponent), + multi: true + } + ] +}) +export class NotificationSettingFormComponent implements ControlValueAccessor, OnInit, OnDestroy { + + @Input() + disabled: boolean; + + @Input() + deliveryMethods: NotificationDeliveryMethod[] = []; + + @Input() + allowDeliveryMethods: NotificationDeliveryMethod[] = []; + + notificationSettingsFormGroup: UntypedFormGroup; + + notificationTemplateTypeTranslateMap = NotificationTemplateTypeTranslateMap; + + private propagateChange = null; + + private valueChange$: Subscription = null; + + constructor(private utils: UtilsService, + private fb: UntypedFormBuilder) { + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + ngOnInit() { + const deliveryMethod = {}; + this.deliveryMethods.forEach(value => { + deliveryMethod[value] = true; + }); + this.notificationSettingsFormGroup = this.fb.group( + { + name: [''], + enabled: [true], + enabledDeliveryMethods: this.fb.group({ + ...deliveryMethod + }) + }); + this.valueChange$ = this.notificationSettingsFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + } + + ngOnDestroy() { + if (this.valueChange$) { + this.valueChange$.unsubscribe(); + this.valueChange$ = null; + } + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.notificationSettingsFormGroup.disable({emitEvent: false}); + } else { + this.notificationSettingsFormGroup.enable({emitEvent: false}); + } + } + + toggleEnabled() { + this.notificationSettingsFormGroup.get('enabled').patchValue(!this.notificationSettingsFormGroup.get('enabled').value); + } + + getChecked(deliveryMethod: NotificationDeliveryMethod): boolean { + return this.notificationSettingsFormGroup.get('enabledDeliveryMethods').get(deliveryMethod).value; + } + + toggleDeliviryMethod(deliveryMethod: NotificationDeliveryMethod) { + this.notificationSettingsFormGroup.get('enabledDeliveryMethods').get(deliveryMethod) + .patchValue(!this.notificationSettingsFormGroup.get('enabledDeliveryMethods').get(deliveryMethod).value); + } + + writeValue(value: NotificationUserSetting): void { + if (isDefinedAndNotNull(value)) { + this.notificationSettingsFormGroup.patchValue(value, {emitEvent: false}); + } + } + + private updateModel() { + this.propagateChange(this.notificationSettingsFormGroup.value); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/notification/settings/notification-settings-routing.modules.ts b/ui-ngx/src/app/modules/home/pages/notification/settings/notification-settings-routing.modules.ts new file mode 100644 index 0000000000..6a65e554bf --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/notification/settings/notification-settings-routing.modules.ts @@ -0,0 +1,41 @@ +/// +/// 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. +/// + +import { Routes } from '@angular/router'; +import { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard'; +import { Authority } from '@shared/models/authority.enum'; +import { inject, NgModule } from '@angular/core'; +import { NotificationSettingsComponent } from '@home/pages/notification/settings/notification-settings.component'; +import { NotificationService } from '@core/http/notification.service'; + +export const notificationUserSettingsRoutes: Routes = [ + { + path: 'notificationSettings', + component: NotificationSettingsComponent, + canDeactivate: [ConfirmOnExitGuard], + data: { + auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN, Authority.CUSTOMER_USER], + title: 'account.notification-settings', + breadcrumb: { + label: 'account.notification-settings', + icon: 'settings' + } + }, + resolve: { + userSettings: () => inject(NotificationService).getNotificationUserSettings() + } + } +]; diff --git a/ui-ngx/src/app/modules/home/pages/notification/settings/notification-settings.component.html b/ui-ngx/src/app/modules/home/pages/notification/settings/notification-settings.component.html new file mode 100644 index 0000000000..c531f10d78 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/notification/settings/notification-settings.component.html @@ -0,0 +1,84 @@ + +
+ + +
+
+ notification.settings.notification-settings +
+
+ +
+
+
+ + +
+ +
+
+
+
+
+ + notification.settings.type + +
+
+
+ + {{ notificationDeliveryMethodTranslateMap.get(deliveryMethods) | translate }} + +
+
+
+ +
+ + + +
+
+
+
+
+
+ +
+
+
diff --git a/ui-ngx/src/app/modules/home/pages/notification/settings/notification-settings.component.scss b/ui-ngx/src/app/modules/home/pages/notification/settings/notification-settings.component.scss new file mode 100644 index 0000000000..a91c57ed65 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/notification/settings/notification-settings.component.scss @@ -0,0 +1,41 @@ +/** + * 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. + */ +@import "../../../../../../scss/constants"; + +:host { + .mat-mdc-card.settings-card { + margin: 8px; + @media #{$mat-gt-sm} { + width: 60%; + } + .mat-headline-5 { + margin: 0; + } + .notification-form { + height: 100%; + min-height: min-content; + max-height: min-content; + } + .notification-section { + height: 100%; + border: 1px solid rgba(0, 0, 0, 0.12); + overflow: scroll; + &-block { + min-width: 470px; + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/pages/notification/settings/notification-settings.component.ts b/ui-ngx/src/app/modules/home/pages/notification/settings/notification-settings.component.ts new file mode 100644 index 0000000000..c28e030eec --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/notification/settings/notification-settings.component.ts @@ -0,0 +1,188 @@ +/// +/// 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. +/// + +import { Component, OnInit } from '@angular/core'; +import { PageComponent } from '@shared/components/page.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { AbstractControl, UntypedFormArray, UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; +import { HasConfirmForm } from '@core/guards/confirm-on-exit.guard'; +import { TranslateService } from '@ngx-translate/core'; +import { ActivatedRoute } from '@angular/router'; +import { deepClone, isDefinedAndNotNull } from '@core/utils'; +import { + NotificationDeliveryMethod, + NotificationDeliveryMethodTranslateMap, + NotificationUserSettings +} from '@shared/models/notification.models'; +import { NotificationService } from '@core/http/notification.service'; +import { DialogService } from '@core/services/dialog.service'; + +@Component({ + selector: 'tb-notification-settings', + templateUrl: './notification-settings.component.html', + styleUrls: ['./notification-settings.component.scss'] +}) +export class NotificationSettingsComponent extends PageComponent implements OnInit, HasConfirmForm { + + notificationSettings: UntypedFormGroup; + + notificationDeliveryMethods: NotificationDeliveryMethod[]; + notificationDeliveryMethodTranslateMap = NotificationDeliveryMethodTranslateMap; + + allowNotificationDeliveryMethods: Array; + + constructor(protected store: Store, + private route: ActivatedRoute, + private translate: TranslateService, + private dialogService: DialogService, + private notificationService: NotificationService, + private fb: UntypedFormBuilder,) { + super(store); + } + + ngOnInit() { + this.notificationDeliveryMethods = this.getNotificationDeliveryMethods(); + + this.notificationService.getAvailableDeliveryMethods({ignoreLoading: true}).subscribe(allowMethods => { + this.allowNotificationDeliveryMethods = allowMethods; + }); + + this.buildNotificationSettingsForm(); + this.patchNotificationSettings(this.route.snapshot.data.userSettings); + } + + private getNotificationDeliveryMethods(): NotificationDeliveryMethod[] { + const deliveryMethods = new Set([ + NotificationDeliveryMethod.SLACK + ]); + return Object.values(NotificationDeliveryMethod).filter(type => !deliveryMethods.has(type)); + } + + private buildNotificationSettingsForm() { + this.notificationSettings = this.fb.group({ + prefs: this.fb.array([]) + }); + } + + private patchNotificationSettings(settings: NotificationUserSettings) { + const notificationSettingsControls: Array = []; + let preparedSettings; + if (settings.prefs) { + preparedSettings = this.prepareNotificationSettings(settings.prefs); + preparedSettings.forEach((setting) => { + setting.enabledDeliveryMethods = Object.assign( + this.notificationDeliveryMethods.reduce((a, v) => ({ ...a, [v]: true}), {}), + setting.enabledDeliveryMethods + ); + notificationSettingsControls.push(this.fb.control(setting, [Validators.required])); + }); + } + this.notificationSettings.setControl('prefs', this.fb.array(notificationSettingsControls), {emitEvent: false}); + } + + private prepareNotificationSettings(prefs: any) { + return Object.entries(prefs).map((value: any) => { + value[1].name = value[0]; + return value[1]; + }); + } + + resetSettings() { + this.dialogService.confirm( + this.translate.instant('notification.settings.reset-all-title'), + this.translate.instant('notification.settings.reset-all-text'), + this.translate.instant('action.no'), + this.translate.instant('action.yes'), + true + ).subscribe( + result => { + if (result) { + const settings = this.prepareNotificationSettings(this.route.snapshot.data.userSettings.prefs); + const notificationSettingsControls: Array = []; + this.notificationSettings.reset({}); + if (settings) { + settings.forEach((setting) => { + setting.enabled = true; + setting.enabledDeliveryMethods = this.notificationDeliveryMethods.reduce((a, v) => ({ ...a, [v]: true}), {}); + notificationSettingsControls.push(this.fb.control(setting, [Validators.required])); + }); + } + this.notificationSettings.setControl('prefs', this.fb.array(notificationSettingsControls), {emitEvent: false}); + this.save(); + } + } + ); + } + + getChecked = (method: NotificationDeliveryMethod = null): boolean => { + const type = this.notificationSettings.get('prefs').value; + if (isDefinedAndNotNull(method)) { + return isDefinedAndNotNull(type) && type.every(resource => resource.enabledDeliveryMethods[method]); + } + return isDefinedAndNotNull(type) && type.every(resource => resource.enabled); + }; + + getSomeChecked = () => { + const type = this.notificationSettings.get('prefs').value; + return isDefinedAndNotNull(type) && type.some(resource => resource.enabled); + }; + + getIndeterminate = (deliveryMethod: NotificationDeliveryMethod = null): boolean => { + const type = this.notificationSettings.get('prefs').value; + if (isDefinedAndNotNull(type)) { + const checkedResource = isDefinedAndNotNull(deliveryMethod) ? + type.filter(resource => resource.enabledDeliveryMethods[deliveryMethod]) : + type.filter(resource => resource.enabled); + return checkedResource.length !== 0 && checkedResource.length !== type.length; + } + return false; + }; + + changeInstanceTypeCheckBox = (value: boolean, deliveryMethod: NotificationDeliveryMethod = null): void => { + const type = deepClone(this.notificationSettings.get('prefs').value); + if (isDefinedAndNotNull(deliveryMethod)) { + type.forEach(notificationType => notificationType.enabledDeliveryMethods[deliveryMethod] = value); + } else { + type.forEach(notificationType => notificationType.enabled = value); + } + this.notificationSettings.get('prefs').patchValue(type); + this.notificationSettings.markAsDirty(); + }; + + get notificationSettingsFormArray(): UntypedFormArray { + return this.notificationSettings.get('prefs') as UntypedFormArray; + } + + save(): void { + const settings = {prefs: {}}; + this.notificationSettings.getRawValue().prefs.forEach(value => { + const key = value.name; + delete value.name; + settings.prefs[key] = value; + }); + this.notificationService.saveNotificationUserSettings(settings).subscribe( + (userSettings) => { + this.notificationSettings.get('prefs').reset({}); + this.patchNotificationSettings(userSettings); + } + ); + } + + confirmForm(): UntypedFormGroup { + return this.notificationSettings; + } +} diff --git a/ui-ngx/src/app/shared/models/notification.models.ts b/ui-ngx/src/app/shared/models/notification.models.ts index edafef4704..0d1a46443d 100644 --- a/ui-ngx/src/app/shared/models/notification.models.ts +++ b/ui-ngx/src/app/shared/models/notification.models.ts @@ -591,3 +591,12 @@ export const TriggerTypeTranslationMap = new Map([ [TriggerType.NEW_PLATFORM_VERSION, 'notification.trigger.new-platform-version'], [TriggerType.RATE_LIMITS, 'notification.trigger.rate-limits'], ]); + +export interface NotificationUserSettings { + prefs: {[key: string]: NotificationUserSetting}; +} + +export interface NotificationUserSetting { + enabled: boolean; + enabledDeliveryMethods: {[key: string]: boolean}; +} diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 52d95afded..d161526958 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -11,7 +11,8 @@ "permission-denied-text": "You don't have permission to perform this operation!" }, "account": { - "account": "Account" + "account": "Account", + "notification-settings": "Notification settings" }, "action": { "activate": "Activate", @@ -3151,7 +3152,17 @@ "updated": "Updated", "use-template": "Use template", "view-all": "View all", - "warning": "Warning" + "warning": "Warning", + "settings": { + "notification-settings": "Notification settings", + "reset-all": "Reset all settings", + "reset-all-title": "Are you sure you want to reset form?", + "reset-all-text": "After the confirmation, the settings form will reset to the default value and save.", + "type": "Type", + "enable-all": "Enable all", + "disable-all": "Disable all", + "delivery-not-configured": "Delivery method is not configured" + } }, "ota-update": { "add": "Add package",