From 014497cd89a2a3d8731548bf1a0bfdaf12c0d204 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Fri, 26 May 2023 15:04:23 +0300 Subject: [PATCH 01/78] User-level notification settings --- .../controller/NotificationController.java | 9 +++ .../DefaultNotificationCenter.java | 11 ++++ .../NotificationSettingsService.java | 7 ++ .../NotificationDeliveryMethod.java | 6 ++ .../settings/UserNotificationSettings.java | 64 +++++++++++++++++++ .../DefaultNotificationSettingsService.java | 26 ++++++++ 6 files changed, 123 insertions(+) create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/notification/settings/UserNotificationSettings.java 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..0d9319ffa5 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; @@ -437,4 +438,12 @@ public class NotificationController extends BaseController { return notificationCenter.getAvailableDeliveryMethods(user.getTenantId()); } + + @PostMapping("/notification/settings/user") + public UserNotificationSettings saveUserNotificationSettings(@RequestBody @Valid UserNotificationSettings settings, + @AuthenticationPrincipal SecurityUser user) { + notificationSettingsService.saveUserNotificationSettings(user.getTenantId(), user.getId(), settings); + return settings; + } + } 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..71a842b441 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 @@ -37,8 +37,10 @@ import org.thingsboard.server.common.data.notification.NotificationRequestConfig import org.thingsboard.server.common.data.notification.NotificationRequestStats; import org.thingsboard.server.common.data.notification.NotificationRequestStatus; import org.thingsboard.server.common.data.notification.NotificationStatus; +import org.thingsboard.server.common.data.notification.NotificationType; 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; @@ -238,6 +240,15 @@ public class DefaultNotificationCenter extends AbstractSubscriptionService imple if (ctx.getStats().contains(deliveryMethod, recipient.getId())) { throw new AlreadySentException(); } + if (recipient instanceof User) { + NotificationType notificationType = ctx.getNotificationTemplate().getNotificationType(); + UserNotificationSettings settings = notificationSettingsService.getUserNotificationSettings(ctx.getTenantId(), (User) recipient); + Set enabledDeliveryMethods = settings.getEnabledDeliveryMethods(notificationType); + if (!enabledDeliveryMethods.contains(deliveryMethod)) { + throw new RuntimeException("User disabled " + deliveryMethod.getName() + " notifications of this type"); + } + } + NotificationChannel notificationChannel = channels.get(deliveryMethod); DeliveryMethodNotificationTemplate processedTemplate = ctx.getProcessedTemplate(deliveryMethod, recipient); 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..bb53b1a828 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 @@ -15,8 +15,11 @@ */ package org.thingsboard.server.dao.notification; +import org.thingsboard.server.common.data.User; 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 +27,10 @@ public interface NotificationSettingsService { NotificationSettings findNotificationSettings(TenantId tenantId); + void saveUserNotificationSettings(TenantId tenantId, UserId userId, UserNotificationSettings settings); + + UserNotificationSettings getUserNotificationSettings(TenantId tenantId, User user); + void createDefaultNotificationConfigs(TenantId tenantId); void updateDefaultNotificationConfigs(TenantId tenantId); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationDeliveryMethod.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationDeliveryMethod.java index 4a2c4657d5..ac878adac6 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationDeliveryMethod.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationDeliveryMethod.java @@ -18,6 +18,10 @@ package org.thingsboard.server.common.data.notification; import lombok.Getter; import lombok.RequiredArgsConstructor; +import java.util.Arrays; +import java.util.Set; +import java.util.stream.Collectors; + @RequiredArgsConstructor public enum NotificationDeliveryMethod { @@ -29,4 +33,6 @@ public enum NotificationDeliveryMethod { @Getter private final String name; + public static final Set values = Arrays.stream(values()).collect(Collectors.toSet()); + } 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..efe5194585 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/settings/UserNotificationSettings.java @@ -0,0 +1,64 @@ +/** + * 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.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.util.CollectionsUtil; + +import java.util.Collections; +import java.util.Map; +import java.util.Set; + +@Data +public class UserNotificationSettings { + + private final Map prefs; + + @JsonCreator + public UserNotificationSettings(@JsonProperty("prefs") Map prefs) { + this.prefs = prefs; + } + + public static final UserNotificationSettings DEFAULT = new UserNotificationSettings(Collections.emptyMap()); + + public Set getEnabledDeliveryMethods(NotificationType notificationType) { + NotificationTypePrefs prefs; + if (this.prefs == null || (prefs = this.prefs.get(notificationType)) == null) { + return NotificationDeliveryMethod.values; + } + if (prefs.isEnabled()) { + Set deliveryMethods = prefs.getEnabledDeliveryMethods(); + if (CollectionsUtil.isNotEmpty(deliveryMethods)) { + return deliveryMethods; + } else { + return NotificationDeliveryMethod.values; + } + } else { + return Collections.emptySet(); + } + } + + @Data + public static class NotificationTypePrefs { + private boolean enabled; + private Set enabledDeliveryMethods; + } + +} 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..90ccd095d6 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 @@ -15,6 +15,8 @@ */ package org.thingsboard.server.dao.notification; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; import lombok.RequiredArgsConstructor; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.Cacheable; @@ -24,9 +26,12 @@ import org.springframework.transaction.annotation.Transactional; 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.User; 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.targets.NotificationTarget; import org.thingsboard.server.common.data.notification.targets.platform.AffectedTenantAdministratorsFilter; import org.thingsboard.server.common.data.notification.targets.platform.AffectedUserFilter; @@ -39,6 +44,7 @@ import org.thingsboard.server.common.data.notification.targets.platform.UsersFil import org.thingsboard.server.common.data.notification.targets.platform.UsersFilterType; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.settings.AdminSettingsService; +import org.thingsboard.server.dao.user.UserService; import java.util.Collections; import java.util.List; @@ -52,6 +58,7 @@ public class DefaultNotificationSettingsService implements NotificationSettingsS private final NotificationTargetService notificationTargetService; private final NotificationTemplateService notificationTemplateService; private final DefaultNotifications defaultNotifications; + private final UserService userService; private static final String SETTINGS_KEY = "notifications"; @@ -81,6 +88,25 @@ public class DefaultNotificationSettingsService implements NotificationSettingsS }); } + @Override + public void saveUserNotificationSettings(TenantId tenantId, UserId userId, UserNotificationSettings settings) { + User user = userService.findUserById(tenantId, userId); + ObjectNode additionalInfo = (ObjectNode) Optional.ofNullable(user.getAdditionalInfo()).orElseGet(JacksonUtil::newObjectNode); + additionalInfo.set("notificationSettings", JacksonUtil.valueToTree(settings)); + user.setAdditionalInfo(additionalInfo); + userService.saveUser(user); + } + + @Override + public UserNotificationSettings getUserNotificationSettings(TenantId tenantId, User user) { + // TODO: decide whether to use user_settings or store it in the additionalInfo not to make more DB requests + JsonNode notificationSettings = user.getAdditionalInfo().get("notificationSettings"); + if (notificationSettings == null || notificationSettings.isNull()) { + return UserNotificationSettings.DEFAULT; + } + return JacksonUtil.treeToValue(notificationSettings, UserNotificationSettings.class); + } + @Transactional(propagation = Propagation.NOT_SUPPORTED) // so that parent transaction is not aborted on method failure @Override public void createDefaultNotificationConfigs(TenantId tenantId) { From ab93f6266d714d7247785a756cf0c274f1c5338b Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Tue, 20 Jun 2023 12:10:28 +0300 Subject: [PATCH 02/78] Add getUserNotificationSettings api --- .../server/controller/NotificationController.java | 8 ++++++++ 1 file changed, 8 insertions(+) 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 0d9319ffa5..a7072f6a9d 100644 --- a/application/src/main/java/org/thingsboard/server/controller/NotificationController.java +++ b/application/src/main/java/org/thingsboard/server/controller/NotificationController.java @@ -440,10 +440,18 @@ public class NotificationController extends BaseController { @PostMapping("/notification/settings/user") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") public UserNotificationSettings saveUserNotificationSettings(@RequestBody @Valid UserNotificationSettings settings, @AuthenticationPrincipal SecurityUser user) { notificationSettingsService.saveUserNotificationSettings(user.getTenantId(), user.getId(), settings); return settings; } + @GetMapping("/notification/settings/user") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + public UserNotificationSettings getUserNotificationSettings(@AuthenticationPrincipal SecurityUser user) { + return notificationSettingsService.getUserNotificationSettings(user.getTenantId(), + userService.findUserById(user.getTenantId(), user.getId())); + } + } From bbcf4b1ff122b67e38949911fcd7e52c51459970 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Tue, 20 Jun 2023 18:07:37 +0300 Subject: [PATCH 03/78] Fix MockNotificationSettingsService --- .../service/notification/MockNotificationSettingsService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/src/test/java/org/thingsboard/server/service/notification/MockNotificationSettingsService.java b/application/src/test/java/org/thingsboard/server/service/notification/MockNotificationSettingsService.java index a49f8dc8cb..860596961e 100644 --- a/application/src/test/java/org/thingsboard/server/service/notification/MockNotificationSettingsService.java +++ b/application/src/test/java/org/thingsboard/server/service/notification/MockNotificationSettingsService.java @@ -26,7 +26,7 @@ import org.thingsboard.server.dao.settings.AdminSettingsService; public class MockNotificationSettingsService extends DefaultNotificationSettingsService { public MockNotificationSettingsService(AdminSettingsService adminSettingsService) { - super(adminSettingsService, null, null, null); + super(adminSettingsService, null, null, null, null); } @Override From d49a6c3e921661f466b336fa629e02e0d952565b Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Fri, 23 Jun 2023 16:11:21 +0300 Subject: [PATCH 04/78] Notification settings for rules --- .../controller/NotificationController.java | 4 +- .../DefaultNotificationCenter.java | 8 ++- .../NotificationSettingsService.java | 2 +- .../settings/UserNotificationSettings.java | 54 +++++++++++-------- .../server/common/data/page/SortOrder.java | 2 + .../DefaultNotificationSettingsService.java | 52 +++++++++++++++--- 6 files changed, 85 insertions(+), 37 deletions(-) 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 a7072f6a9d..7fac0fe393 100644 --- a/application/src/main/java/org/thingsboard/server/controller/NotificationController.java +++ b/application/src/main/java/org/thingsboard/server/controller/NotificationController.java @@ -298,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.byCreatedTimeDesc)); recipientsCount = (int) recipients.getTotalElements(); recipientsPart = recipients.getData().stream().map(r -> (NotificationRecipient) r).collect(Collectors.toList()); } else { @@ -451,7 +451,7 @@ public class NotificationController extends BaseController { @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") public UserNotificationSettings getUserNotificationSettings(@AuthenticationPrincipal SecurityUser user) { return notificationSettingsService.getUserNotificationSettings(user.getTenantId(), - userService.findUserById(user.getTenantId(), user.getId())); + userService.findUserById(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 71a842b441..8f6a6dc177 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 @@ -37,7 +37,6 @@ import org.thingsboard.server.common.data.notification.NotificationRequestConfig import org.thingsboard.server.common.data.notification.NotificationRequestStats; import org.thingsboard.server.common.data.notification.NotificationRequestStatus; import org.thingsboard.server.common.data.notification.NotificationStatus; -import org.thingsboard.server.common.data.notification.NotificationType; 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; @@ -240,10 +239,9 @@ public class DefaultNotificationCenter extends AbstractSubscriptionService imple if (ctx.getStats().contains(deliveryMethod, recipient.getId())) { throw new AlreadySentException(); } - if (recipient instanceof User) { - NotificationType notificationType = ctx.getNotificationTemplate().getNotificationType(); - UserNotificationSettings settings = notificationSettingsService.getUserNotificationSettings(ctx.getTenantId(), (User) recipient); - Set enabledDeliveryMethods = settings.getEnabledDeliveryMethods(notificationType); + if (recipient instanceof User && ctx.getRequest().getRuleId() != null) { + UserNotificationSettings settings = notificationSettingsService.getUserNotificationSettings(ctx.getTenantId(), (User) recipient, false); + Set enabledDeliveryMethods = settings.getEnabledDeliveryMethods(ctx.getRequest().getRuleId()); if (!enabledDeliveryMethods.contains(deliveryMethod)) { throw new RuntimeException("User disabled " + deliveryMethod.getName() + " notifications of this type"); } 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 bb53b1a828..33771ab71e 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 @@ -29,7 +29,7 @@ public interface NotificationSettingsService { void saveUserNotificationSettings(TenantId tenantId, UserId userId, UserNotificationSettings settings); - UserNotificationSettings getUserNotificationSettings(TenantId tenantId, User user); + UserNotificationSettings getUserNotificationSettings(TenantId tenantId, User user, boolean format); void createDefaultNotificationConfigs(TenantId tenantId); 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 index efe5194585..9e2f2740ab 100644 --- 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 @@ -18,47 +18,55 @@ package org.thingsboard.server.common.data.notification.settings; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Data; +import org.thingsboard.server.common.data.id.NotificationRuleId; import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; -import org.thingsboard.server.common.data.notification.NotificationType; -import org.thingsboard.server.common.data.util.CollectionsUtil; +import org.thingsboard.server.common.data.notification.rule.NotificationRule; +import javax.validation.Valid; +import javax.validation.constraints.NotNull; import java.util.Collections; -import java.util.Map; +import java.util.List; import java.util.Set; +import java.util.UUID; @Data public class UserNotificationSettings { - private final Map prefs; + @NotNull + @Valid + private final List prefs; + + public static final UserNotificationSettings DEFAULT = new UserNotificationSettings(Collections.emptyList()); @JsonCreator - public UserNotificationSettings(@JsonProperty("prefs") Map prefs) { + public UserNotificationSettings(@JsonProperty("prefs") List prefs) { this.prefs = prefs; } - public static final UserNotificationSettings DEFAULT = new UserNotificationSettings(Collections.emptyMap()); - - public Set getEnabledDeliveryMethods(NotificationType notificationType) { - NotificationTypePrefs prefs; - if (this.prefs == null || (prefs = this.prefs.get(notificationType)) == null) { - return NotificationDeliveryMethod.values; - } - if (prefs.isEnabled()) { - Set deliveryMethods = prefs.getEnabledDeliveryMethods(); - if (CollectionsUtil.isNotEmpty(deliveryMethods)) { - return deliveryMethods; - } else { - return NotificationDeliveryMethod.values; - } - } else { - return Collections.emptySet(); - } + public Set getEnabledDeliveryMethods(NotificationRuleId ruleId) { + return prefs.stream() + .filter(pref -> pref.getRuleId().equals(ruleId.getId())).findFirst() + .map(pref -> pref.isEnabled() ? pref.getEnabledDeliveryMethods() : Collections.emptySet()) + .orElse(NotificationDeliveryMethod.values); } @Data - public static class NotificationTypePrefs { + public static class NotificationPref { + @NotNull + private UUID ruleId; + private String ruleName; private boolean enabled; + @NotNull private Set enabledDeliveryMethods; + + public static NotificationPref createDefault(NotificationRule rule) { + NotificationPref pref = new NotificationPref(); + pref.setRuleId(rule.getUuidId()); + pref.setRuleName(rule.getName()); + pref.setEnabled(true); + pref.setEnabledDeliveryMethods(NotificationDeliveryMethod.values); + return pref; + } } } 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..b99cb93eed 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 byCreatedTimeDesc = new SortOrder("createdTime", Direction.DESC); + } 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 90ccd095d6..af9687840a 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 @@ -20,6 +20,7 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import lombok.RequiredArgsConstructor; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.Cacheable; +import org.springframework.data.domain.Sort; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; @@ -30,8 +31,10 @@ import org.thingsboard.server.common.data.User; 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.rule.NotificationRule; 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; @@ -43,12 +46,19 @@ 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.page.SortOrder; import org.thingsboard.server.dao.settings.AdminSettingsService; import org.thingsboard.server.dao.user.UserService; +import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Optional; +import java.util.UUID; + +import static java.util.function.Predicate.not; @Service @RequiredArgsConstructor @@ -57,6 +67,7 @@ public class DefaultNotificationSettingsService implements NotificationSettingsS private final AdminSettingsService adminSettingsService; private final NotificationTargetService notificationTargetService; private final NotificationTemplateService notificationTemplateService; + private final NotificationRuleService notificationRuleService; private final DefaultNotifications defaultNotifications; private final UserService userService; @@ -98,13 +109,42 @@ public class DefaultNotificationSettingsService implements NotificationSettingsS } @Override - public UserNotificationSettings getUserNotificationSettings(TenantId tenantId, User user) { - // TODO: decide whether to use user_settings or store it in the additionalInfo not to make more DB requests - JsonNode notificationSettings = user.getAdditionalInfo().get("notificationSettings"); - if (notificationSettings == null || notificationSettings.isNull()) { - return UserNotificationSettings.DEFAULT; + public UserNotificationSettings getUserNotificationSettings(TenantId tenantId, User user, boolean format) { + UserNotificationSettings settings = Optional.ofNullable(user.getAdditionalInfo().get("notificationSettings")) + .filter(not(JsonNode::isNull)) + .map(json -> JacksonUtil.treeToValue(json, UserNotificationSettings.class)) + .orElse(null); + if (!format) { + if (settings != null) { + return settings; + } else { + return UserNotificationSettings.DEFAULT; + } + } + + Map rules = new HashMap<>(); + notificationRuleService.findNotificationRulesByTenantId(tenantId, new PageLink(Integer.MAX_VALUE, 0,null, SortOrder.byCreatedTimeDesc)) + .getData().forEach(rule -> rules.put(rule.getUuidId(), rule)); + + List prefs = new ArrayList<>(); + if (settings == null) { + rules.values().forEach(rule -> { + prefs.add(NotificationPref.createDefault(rule)); + }); + } else { + settings.getPrefs().forEach(pref -> { + NotificationRule rule = rules.remove(pref.getRuleId()); + if (rule == null) { + return; + } + pref.setRuleName(rule.getName()); + prefs.add(pref); + }); + rules.values().forEach(rule -> { + prefs.add(NotificationPref.createDefault(rule)); + }); } - return JacksonUtil.treeToValue(notificationSettings, UserNotificationSettings.class); + return new UserNotificationSettings(prefs); } @Transactional(propagation = Propagation.NOT_SUPPORTED) // so that parent transaction is not aborted on method failure From 1d45ea3e8be898a86f1f73150e9a16d2fa8589da Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Fri, 23 Jun 2023 16:23:42 +0300 Subject: [PATCH 05/78] Fix MockNotificationSettingsService --- .../service/notification/MockNotificationSettingsService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/src/test/java/org/thingsboard/server/service/notification/MockNotificationSettingsService.java b/application/src/test/java/org/thingsboard/server/service/notification/MockNotificationSettingsService.java index 860596961e..6c3a02051e 100644 --- a/application/src/test/java/org/thingsboard/server/service/notification/MockNotificationSettingsService.java +++ b/application/src/test/java/org/thingsboard/server/service/notification/MockNotificationSettingsService.java @@ -26,7 +26,7 @@ import org.thingsboard.server.dao.settings.AdminSettingsService; public class MockNotificationSettingsService extends DefaultNotificationSettingsService { public MockNotificationSettingsService(AdminSettingsService adminSettingsService) { - super(adminSettingsService, null, null, null, null); + super(adminSettingsService, null, null, null, null, null); } @Override From 8dcd32e4b3fefc15f5ebfe0c1f60cbea64d38c28 Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Mon, 26 Jun 2023 10:44:07 +0300 Subject: [PATCH 06/78] UI: User notification settings --- .../src/app/core/http/notification.service.ts | 9 + ui-ngx/src/app/core/services/menu.service.ts | 100 +++++++++++ .../home/menu/side-menu.component.html | 2 +- .../pages/account/account-routing.module.ts | 113 ++++++++++++ .../home/pages/account/account.module.ts | 31 ++++ .../modules/home/pages/home-pages.module.ts | 4 +- .../pages/notification/notification.module.ts | 12 +- .../notification-setting-form.component.html | 42 +++++ .../notification-setting-form.component.scss | 24 +++ .../notification-setting-form.component.ts | 139 +++++++++++++++ .../notification-settings-routing.modules.ts | 63 +++++++ .../notification-settings.component.html | 80 +++++++++ .../notification-settings.component.scss | 37 ++++ .../notification-settings.component.ts | 167 ++++++++++++++++++ .../components/user-menu.component.html | 10 +- .../shared/components/user-menu.component.ts | 8 +- .../app/shared/models/notification.models.ts | 12 ++ .../assets/locale/locale.constant-en_US.json | 17 +- 18 files changed, 857 insertions(+), 13 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/pages/account/account-routing.module.ts create mode 100644 ui-ngx/src/app/modules/home/pages/account/account.module.ts create mode 100644 ui-ngx/src/app/modules/home/pages/notification/settings/notification-setting-form.component.html create mode 100644 ui-ngx/src/app/modules/home/pages/notification/settings/notification-setting-form.component.scss create mode 100644 ui-ngx/src/app/modules/home/pages/notification/settings/notification-setting-form.component.ts create mode 100644 ui-ngx/src/app/modules/home/pages/notification/settings/notification-settings-routing.modules.ts create mode 100644 ui-ngx/src/app/modules/home/pages/notification/settings/notification-settings.component.html create mode 100644 ui-ngx/src/app/modules/home/pages/notification/settings/notification-settings.component.scss create mode 100644 ui-ngx/src/app/modules/home/pages/notification/settings/notification-settings.component.ts 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/core/services/menu.service.ts b/ui-ngx/src/app/core/services/menu.service.ts index b33c552eb1..2025f6e389 100644 --- a/ui-ngx/src/app/core/services/menu.service.ts +++ b/ui-ngx/src/app/core/services/menu.service.ts @@ -138,6 +138,42 @@ export class MenuService { } ] }, + { + id: 'account', + name: 'profile.profile', + type: 'link', + path: '/account', + disabled: true, + icon: 'mdi:message-badge', + isMdiIcon: true, + pages: [ + { + id: 'personal_info', + name: 'account.personal-info', + fullName: 'account.personal-info', + type: 'link', + path: '/account/profile', + icon: 'mdi:badge-account-horizontal', + isMdiIcon: true + }, + { + id: 'security', + name: 'security.security', + fullName: 'security.security', + type: 'link', + path: '/account/security', + icon: 'lock' + }, + { + id: 'notificationSettings', + name: 'account.notification-settings', + fullName: 'account.notification-settings', + type: 'link', + path: '/account/notificationSettings', + icon: 'settings' + } + ] + }, { id: 'notifications_center', name: 'notification.notification-center', @@ -518,6 +554,42 @@ export class MenuService { } ] }, + { + id: 'account', + name: 'profile.profile', + type: 'link', + path: '/account', + disabled: true, + icon: 'mdi:message-badge', + isMdiIcon: true, + pages: [ + { + id: 'personal_info', + name: 'account.personal-info', + fullName: 'account.personal-info', + type: 'link', + path: '/account/profile', + icon: 'mdi:badge-account-horizontal', + isMdiIcon: true + }, + { + id: 'security', + name: 'security.security', + fullName: 'security.security', + type: 'link', + path: '/account/security', + icon: 'lock' + }, + { + id: 'notificationSettings', + name: 'account.notification-settings', + fullName: 'account.notification-settings', + type: 'link', + path: '/account/notificationSettings', + icon: 'settings' + } + ] + }, { id: 'notifications_center', name: 'notification.notification-center', @@ -853,6 +925,34 @@ export class MenuService { icon: 'view_quilt' } ] + }, + { + id: 'account', + name: 'profile.profile', + type: 'link', + path: '/account', + disabled: true, + icon: 'mdi:message-badge', + isMdiIcon: true, + pages: [ + { + id: 'personal_info', + name: 'account.personal-info', + fullName: 'account.personal-info', + type: 'link', + path: '/account/profile', + icon: 'mdi:badge-account-horizontal', + isMdiIcon: true + }, + { + id: 'security', + name: 'security.security', + fullName: 'security.security', + type: 'link', + path: '/account/security', + icon: 'lock' + } + ] } ); if (authState.edgesSupportEnabled) { diff --git a/ui-ngx/src/app/modules/home/menu/side-menu.component.html b/ui-ngx/src/app/modules/home/menu/side-menu.component.html index b41b05ca76..68dfdf6c61 100644 --- a/ui-ngx/src/app/modules/home/menu/side-menu.component.html +++ b/ui-ngx/src/app/modules/home/menu/side-menu.component.html @@ -16,7 +16,7 @@ -->
    -
  • +
  • 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 new file mode 100644 index 0000000000..aa89466682 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/account/account-routing.module.ts @@ -0,0 +1,113 @@ +/// +/// 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 { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { RouterTabsComponent } from '@home/components/router-tabs.component'; +import { Authority } from '@shared/models/authority.enum'; +import { ProfileComponent } from '@home/pages/profile/profile.component'; +import { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard'; +import { SecurityComponent } from '@home/pages/security/security.component'; +import { UserTwoFAProvidersResolver } from '@home/pages/security/security-routing.module'; +import { NotificationSettingsComponent } from '@home/pages/notification/settings/notification-settings.component'; +import { + NotificationUserSettingsResolver +} from '@home/pages/notification/settings/notification-settings-routing.modules'; +import { UserProfileResolver } from '@home/pages/profile/profile-routing.module'; + +const routes: Routes = [ + { + path: 'account', + component: RouterTabsComponent, + data: { + auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN, Authority.CUSTOMER_USER], + breadcrumb: { + label: 'account.account', + icon: 'account_circle' + } + }, + children: [ + { + path: '', + children: [], + data: { + auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN, Authority.CUSTOMER_USER], + redirectTo: '/account/profile', + } + }, + { + path: 'profile', + component: ProfileComponent, + canDeactivate: [ConfirmOnExitGuard], + data: { + auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN, Authority.CUSTOMER_USER], + title: 'account.personal-info', + breadcrumb: { + label: 'account.personal-info', + icon: 'mdi:badge-account-horizontal', + } + }, + resolve: { + user: UserProfileResolver + } + }, + { + path: 'security', + component: SecurityComponent, + canDeactivate: [ConfirmOnExitGuard], + data: { + auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN, Authority.CUSTOMER_USER], + title: 'security.security', + breadcrumb: { + label: 'security.security', + icon: 'lock' + } + }, + resolve: { + user: UserProfileResolver, + providers: UserTwoFAProvidersResolver + } + }, + { + path: 'notificationSettings', + component: NotificationSettingsComponent, + canDeactivate: [ConfirmOnExitGuard], + data: { + auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN], + title: 'account.notification-settings', + breadcrumb: { + label: 'account.notification-settings', + icon: 'settings' + } + }, + resolve: { + userSettings: NotificationUserSettingsResolver + } + } + ] + } +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], + providers: [ + UserProfileResolver, + UserTwoFAProvidersResolver, + NotificationUserSettingsResolver + ] +}) +export class AccountRoutingModule { } diff --git a/ui-ngx/src/app/modules/home/pages/account/account.module.ts b/ui-ngx/src/app/modules/home/pages/account/account.module.ts new file mode 100644 index 0000000000..b11a34df41 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/account/account.module.ts @@ -0,0 +1,31 @@ +/// +/// 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 { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { SharedModule } from '@shared/shared.module'; +import { AccountRoutingModule } from '@home/pages/account/account-routing.module'; + +@NgModule({ + declarations: [ + ], + imports: [ + CommonModule, + SharedModule, + AccountRoutingModule + ] +}) +export class AccountModule { } diff --git a/ui-ngx/src/app/modules/home/pages/home-pages.module.ts b/ui-ngx/src/app/modules/home/pages/home-pages.module.ts index b1f4715c33..005c47f787 100644 --- a/ui-ngx/src/app/modules/home/pages/home-pages.module.ts +++ b/ui-ngx/src/app/modules/home/pages/home-pages.module.ts @@ -42,6 +42,7 @@ import { AlarmModule } from '@home/pages/alarm/alarm.module'; import { EntitiesModule } from '@home/pages/entities/entities.module'; import { FeaturesModule } from '@home/pages/features/features.module'; import { NotificationModule } from '@home/pages/notification/notification.module'; +import { AccountModule } from '@home/pages/account/account.module'; @NgModule({ exports: [ @@ -70,7 +71,8 @@ import { NotificationModule } from '@home/pages/notification/notification.module ApiUsageModule, OtaUpdateModule, UserModule, - VcModule + VcModule, + AccountModule ] }) export class HomePagesModule { } 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..f041e632fa 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,13 @@ 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 { + NotificationSettingsRoutingModules +} from '@home/pages/notification/settings/notification-settings-routing.modules'; +import { + NotificationSettingFormComponent +} from '@home/pages/notification/settings/notification-setting-form.component'; @NgModule({ declarations: [ @@ -49,12 +56,15 @@ import { RuleTableHeaderComponent } from '@home/pages/notification/rule/rule-tab EscalationFormComponent, EscalationsComponent, RuleNotificationDialogComponent, - RuleTableHeaderComponent + RuleTableHeaderComponent, + NotificationSettingsComponent, + NotificationSettingFormComponent ], imports: [ CommonModule, SharedModule, NotificationRoutingModule, + NotificationSettingsRoutingModules, HomeComponentsModule ] }) 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..9aa733bee5 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/notification/settings/notification-setting-form.component.html @@ -0,0 +1,42 @@ + +
    +
    +
    + + + {{notificationSettingsFormGroup.get('ruleName').value}} + +
    +
    +
    + +
    +
    +
    +
    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..7e79419969 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/notification/settings/notification-setting-form.component.ts @@ -0,0 +1,139 @@ +/// +/// 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, 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() + allowDeliveryMethods = []; + + notificationSettingsFormGroup: UntypedFormGroup; + + notificationDeliveryMethod = NotificationDeliveryMethod; + notificationDeliveryMethodMap = Object.keys(NotificationDeliveryMethod) as NotificationDeliveryMethod[]; + + private modelValue; + private propagateChange = null; + private propagateChangePending = false; + private valueChange$: Subscription = null; + + constructor(private utils: UtilsService, + private fb: UntypedFormBuilder) { + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + if (this.propagateChangePending) { + this.propagateChangePending = false; + setTimeout(() => { + this.propagateChange(this.modelValue); + }, 0); + } + } + + registerOnTouched(fn: any): void { + } + + ngOnInit() { + this.notificationSettingsFormGroup = this.fb.group( + { + ruleId: [], + ruleName: [''], + enabled: [true], + enabledDeliveryMethods: [] + }); + 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, + {emitEvent: true}); + } + + getChecked(deliveryMethod: NotificationDeliveryMethod): boolean { + return this.notificationSettingsFormGroup.get('enabledDeliveryMethods').value.includes(deliveryMethod); + } + + toggleDeliviryMethod(deliveryMethod: NotificationDeliveryMethod) { + const enabledDeliveryMethods = this.notificationSettingsFormGroup.get('enabledDeliveryMethods').value; + if (enabledDeliveryMethods.includes(deliveryMethod)) { + enabledDeliveryMethods.splice(enabledDeliveryMethods.indexOf(deliveryMethod), 1); + } else { + enabledDeliveryMethods.push(deliveryMethod); + } + this.notificationSettingsFormGroup.get('enabledDeliveryMethods').patchValue(enabledDeliveryMethods); + } + + writeValue(value: NotificationUserSetting): void { + this.propagateChangePending = false; + this.modelValue = value; + if (isDefinedAndNotNull(this.modelValue)) { + this.notificationSettingsFormGroup.patchValue(this.modelValue, {emitEvent: false}); + if (!this.disabled && !this.notificationSettingsFormGroup.valid) { + this.updateModel(); + } + } + } + + private updateModel() { + const value = this.notificationSettingsFormGroup.value; + this.modelValue = {...this.modelValue, ...value}; + if (this.propagateChange) { + this.propagateChange(this.modelValue); + } else { + this.propagateChangePending = true; + } + } +} 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..346d67649d --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/notification/settings/notification-settings-routing.modules.ts @@ -0,0 +1,63 @@ +/// +/// 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 { Resolve, RouterModule, Routes } from '@angular/router'; +import { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard'; +import { Authority } from '@shared/models/authority.enum'; +import { Injectable, NgModule } from '@angular/core'; +import { NotificationSettingsComponent } from '@home/pages/notification/settings/notification-settings.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { Observable } from 'rxjs'; +import { NotificationService } from '@core/http/notification.service'; + +@Injectable() +export class NotificationUserSettingsResolver implements Resolve { + + constructor(private store: Store, + private notificationService: NotificationService) { + } + + resolve(): Observable { + return this.notificationService.getNotificationUserSettings(); + } +} + +const routes: 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: NotificationUserSettingsResolver + } + } +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], + providers: [NotificationUserSettingsResolver] +}) +export class NotificationSettingsRoutingModules { } 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..ed1b9e3d52 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/notification/settings/notification-settings.component.html @@ -0,0 +1,80 @@ + +
    + + +
    +
    + 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..12263158d1 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/notification/settings/notification-settings.component.scss @@ -0,0 +1,37 @@ +/** + * 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-section { + margin-bottom: 16px; + border: 1px solid rgba(0, 0, 0, 0.12); + overflow-y: hidden; + overflow-x: scroll; + &-block { + min-width: 700px; + } + } + } +} 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..183ebb9a99 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/notification/settings/notification-settings.component.ts @@ -0,0 +1,167 @@ +/// +/// 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 = Object.keys(NotificationDeliveryMethod) as 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.notificationService.getAvailableDeliveryMethods({ignoreLoading: true}).subscribe(allowMethods => { + this.allowNotificationDeliveryMethods = allowMethods; + }); + + this.buildNotificationSettingsForm(); + this.patchNotificationSettings(this.route.snapshot.data.userSettings); + } + + private buildNotificationSettingsForm() { + this.notificationSettings = this.fb.group({ + prefs: this.fb.array([]) + }); + } + + private patchNotificationSettings(settings: NotificationUserSettings) { + const notificationSettingsControls: Array = []; + if (settings.prefs) { + settings.prefs.forEach((setting) => { + notificationSettingsControls.push(this.fb.control(setting, [Validators.required])); + }); + } + this.notificationSettings.setControl('prefs', this.fb.array(notificationSettingsControls), {emitEvent: false}); + } + + 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.route.snapshot.data.userSettings; + const notificationSettingsControls: Array = []; + this.notificationSettings.reset({}); + if (settings.prefs) { + settings.prefs.forEach((setting) => { + setting.enabled = true; + setting.enabledDeliveryMethods = this.notificationDeliveryMethods; + 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.includes(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.includes(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 => { + if (value && !notificationType.enabledDeliveryMethods.includes(deliveryMethod)) { + notificationType.enabledDeliveryMethods.push(deliveryMethod); + } else if (!value && notificationType.enabledDeliveryMethods.includes(deliveryMethod)) { + notificationType.enabledDeliveryMethods.splice(notificationType.enabledDeliveryMethods.indexOf(deliveryMethod), 1); + } + }); + } 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 { + this.notificationService.saveNotificationUserSettings(this.notificationSettings.getRawValue()).subscribe( + (userSettings) => { + this.notificationSettings.get('prefs').reset({}); + this.patchNotificationSettings(userSettings); + } + ); + } + + confirmForm(): UntypedFormGroup { + return this.notificationSettings; + } +} diff --git a/ui-ngx/src/app/shared/components/user-menu.component.html b/ui-ngx/src/app/shared/components/user-menu.component.html index a25a0fec25..5d032d83ba 100644 --- a/ui-ngx/src/app/shared/components/user-menu.component.html +++ b/ui-ngx/src/app/shared/components/user-menu.component.html @@ -30,12 +30,12 @@
    - + + + + - - - -