From 7528fceeb236976da7fe65d8f5fa8eca065f54b0 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Wed, 28 Jun 2023 14:42:09 +0300 Subject: [PATCH 1/7] MicrosoftTeamsNotificationChannel --- .../controller/NotificationController.java | 31 ++-- .../DefaultNotificationCenter.java | 6 + .../MicrosoftTeamsNotificationChannel.java | 143 ++++++++++++++++++ .../notification/NotificationApiTest.java | 71 +++++++++ .../NotificationDeliveryMethod.java | 3 +- ...icrosoftTeamsNotificationTargetConfig.java | 33 ++++ .../targets/NotificationRecipient.java | 12 +- .../targets/NotificationTargetConfig.java | 3 +- .../targets/NotificationTargetType.java | 3 +- ...amsDeliveryMethodNotificationTemplate.java | 57 +++++++ 10 files changed, 347 insertions(+), 15 deletions(-) create mode 100644 application/src/main/java/org/thingsboard/server/service/notification/channels/MicrosoftTeamsNotificationChannel.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/MicrosoftTeamsNotificationTargetConfig.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/notification/template/MicrosoftTeamsDeliveryMethodNotificationTemplate.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..ed94c8fe69 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.targets.MicrosoftTeamsNotificationTargetConfig; 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; @@ -294,15 +295,27 @@ public class NotificationController extends BaseController { int recipientsCount; List recipientsPart; NotificationTargetType targetType = target.getConfiguration().getType(); - 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))); - recipientsCount = (int) recipients.getTotalElements(); - recipientsPart = recipients.getData().stream().map(r -> (NotificationRecipient) r).collect(Collectors.toList()); - } else { - recipientsCount = 1; - recipientsPart = List.of(((SlackNotificationTargetConfig) target.getConfiguration()).getConversation()); + switch (targetType) { + case PLATFORM_USERS: { + PageData recipients = notificationTargetService.findRecipientsForNotificationTargetConfig(user.getTenantId(), + (PlatformUsersNotificationTargetConfig) target.getConfiguration(), new PageLink(recipientsPreviewSize, 0, null, + new SortOrder("createdTime", SortOrder.Direction.DESC))); + recipientsCount = (int) recipients.getTotalElements(); + recipientsPart = recipients.getData().stream().map(r -> (NotificationRecipient) r).collect(Collectors.toList()); + break; + } + case SLACK: { + recipientsCount = 1; + recipientsPart = List.of(((SlackNotificationTargetConfig) target.getConfiguration()).getConversation()); + break; + } + case MICROSOFT_TEAMS: { + recipientsCount = 1; + recipientsPart = List.of(((MicrosoftTeamsNotificationTargetConfig) target.getConfiguration())); + break; + } + default: + throw new IllegalArgumentException("Target type " + targetType + " not supported"); } firstRecipient.putIfAbsent(targetType, !recipientsPart.isEmpty() ? recipientsPart.get(0) : null); for (NotificationRecipient recipient : recipientsPart) { 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..eb5818db59 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.targets.MicrosoftTeamsNotificationTargetConfig; 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; @@ -210,6 +211,11 @@ public class DefaultNotificationCenter extends AbstractSubscriptionService imple recipients = List.of(targetConfig.getConversation()); break; } + case MICROSOFT_TEAMS: { + MicrosoftTeamsNotificationTargetConfig targetConfig = (MicrosoftTeamsNotificationTargetConfig) target.getConfiguration(); + recipients = List.of(targetConfig); + break; + } default: { recipients = Collections.emptyList(); } diff --git a/application/src/main/java/org/thingsboard/server/service/notification/channels/MicrosoftTeamsNotificationChannel.java b/application/src/main/java/org/thingsboard/server/service/notification/channels/MicrosoftTeamsNotificationChannel.java new file mode 100644 index 0000000000..ceab93c888 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/notification/channels/MicrosoftTeamsNotificationChannel.java @@ -0,0 +1,143 @@ +package org.thingsboard.server.service.notification.channels; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.base.Strings; +import lombok.Data; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import org.apache.commons.lang3.StringUtils; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; +import org.thingsboard.server.common.data.notification.targets.MicrosoftTeamsNotificationTargetConfig; +import org.thingsboard.server.common.data.notification.template.MicrosoftTeamsDeliveryMethodNotificationTemplate; +import org.thingsboard.server.service.notification.NotificationProcessingContext; + +import java.time.Duration; +import java.time.temporal.ChronoUnit; +import java.util.List; + +@Component +@RequiredArgsConstructor +public class MicrosoftTeamsNotificationChannel implements NotificationChannel { + + @Setter + private RestTemplate restTemplate = new RestTemplateBuilder() + .setConnectTimeout(Duration.of(15, ChronoUnit.SECONDS)) + .setReadTimeout(Duration.of(15, ChronoUnit.SECONDS)) + .build(); + + @Override + public void sendNotification(MicrosoftTeamsNotificationTargetConfig targetConfig, MicrosoftTeamsDeliveryMethodNotificationTemplate processedTemplate, NotificationProcessingContext ctx) throws Exception { + if (StringUtils.isNotEmpty(processedTemplate.getCustomMessageCardJson())) { + restTemplate.postForEntity(targetConfig.getWebhookUrl(), processedTemplate.getCustomMessageCardJson(), String.class); + return; + } + + Message message = new Message(); + message.setThemeColor(Strings.emptyToNull(processedTemplate.getThemeColor())); + if (StringUtils.isEmpty(processedTemplate.getSubject())) { + message.setText(processedTemplate.getBody()); + } else { + message.setSummary(processedTemplate.getSubject()); + Message.Section section = new Message.Section(); + section.setActivityTitle(processedTemplate.getSubject()); + section.setActivitySubtitle(processedTemplate.getBody()); + message.setSections(List.of(section)); + } + if (processedTemplate.getButton() != null) { + var button = processedTemplate.getButton(); + Message.ActionCard actionCard = new Message.ActionCard(); + actionCard.setType("OpenUri"); + actionCard.setName(button.getName()); + var target = new Message.ActionCard.Target("default", button.getUri()); + actionCard.setTargets(List.of(target)); + message.setPotentialAction(List.of(actionCard)); + } + + restTemplate.postForEntity(targetConfig.getWebhookUrl(), message, String.class); + } + + @Override + public void check(TenantId tenantId) throws Exception { + } + + @Override + public NotificationDeliveryMethod getDeliveryMethod() { + return NotificationDeliveryMethod.MICROSOFT_TEAMS; + } + + @Data + public static class Message { + @JsonProperty("@type") + private final String type = "MessageCard"; + @JsonProperty("@context") + private final String context = "http://schema.org/extensions"; + private String themeColor; + private String summary; + private String text; + private List
sections; + private List potentialAction; + + @Data + public static class Section { + private String activityTitle; + private String activitySubtitle; + private String activityImage; + private List facts; + private boolean markdown; + + @Data + public static class Fact { + private final String name; + private final String value; + } + } + + @Data + @JsonInclude(JsonInclude.Include.NON_NULL) + public static class ActionCard { + @JsonProperty("@type") + private String type; // ActionCard, OpenUri + private String name; + private List inputs; // for ActionCard + private List actions; // for ActionCard + private List targets; + + @Data + public static class Input { + @JsonProperty("@type") + private String type; // TextInput, DateInput, MultichoiceInput + private String id; + private boolean isMultiple; + private String title; + private boolean isMultiSelect; + + @Data + public static class Choice { + private final String display; + private final String value; + } + } + + @Data + public static class Action { + @JsonProperty("@type") + private final String type; // HttpPOST + private final String name; + private final String target; // url + } + + @Data + public static class Target { + private final String os; + private final String uri; + } + } + + } + +} 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..be972b29c3 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 @@ -20,7 +20,10 @@ import org.assertj.core.data.Offset; import org.java_websocket.client.WebSocketClient; import org.junit.Before; import org.junit.Test; +import org.mockito.ArgumentCaptor; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.client.RestTemplate; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.api.NotificationCenter; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.id.NotificationTargetId; @@ -33,8 +36,10 @@ import org.thingsboard.server.common.data.notification.NotificationRequestPrevie import org.thingsboard.server.common.data.notification.NotificationRequestStats; import org.thingsboard.server.common.data.notification.NotificationRequestStatus; import org.thingsboard.server.common.data.notification.NotificationType; +import org.thingsboard.server.common.data.notification.info.AlarmNotificationInfo; 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.targets.MicrosoftTeamsNotificationTargetConfig; 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; @@ -44,6 +49,7 @@ import org.thingsboard.server.common.data.notification.targets.slack.SlackConver import org.thingsboard.server.common.data.notification.targets.slack.SlackNotificationTargetConfig; import org.thingsboard.server.common.data.notification.template.DeliveryMethodNotificationTemplate; import org.thingsboard.server.common.data.notification.template.EmailDeliveryMethodNotificationTemplate; +import org.thingsboard.server.common.data.notification.template.MicrosoftTeamsDeliveryMethodNotificationTemplate; import org.thingsboard.server.common.data.notification.template.NotificationTemplate; import org.thingsboard.server.common.data.notification.template.NotificationTemplateConfig; import org.thingsboard.server.common.data.notification.template.SlackDeliveryMethodNotificationTemplate; @@ -53,6 +59,7 @@ import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.dao.notification.NotificationDao; import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.service.executors.DbCallbackExecutorService; +import org.thingsboard.server.service.notification.channels.MicrosoftTeamsNotificationChannel; import org.thingsboard.server.service.ws.notification.cmd.UnreadNotificationsUpdate; import java.util.ArrayList; @@ -62,12 +69,17 @@ import java.util.Map; import java.util.concurrent.TimeUnit; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.in; import static org.assertj.core.api.InstanceOfAssertFactories.type; import static org.awaitility.Awaitility.await; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.timeout; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; @DaoSqlTest @Slf4j @@ -79,6 +91,8 @@ public class NotificationApiTest extends AbstractNotificationApiTest { private NotificationDao notificationDao; @Autowired private DbCallbackExecutorService executor; + @Autowired + private MicrosoftTeamsNotificationChannel microsoftTeamsNotificationChannel; @Before public void beforeEach() throws Exception { @@ -524,6 +538,63 @@ public class NotificationApiTest extends AbstractNotificationApiTest { assertThat(stats.getErrors().get(NotificationDeliveryMethod.SLACK).values()).containsExactly(errorMessage); } + @Test + public void testMicrosoftTeamsNotifications() throws Exception { + RestTemplate restTemplate = mock(RestTemplate.class); + microsoftTeamsNotificationChannel.setRestTemplate(restTemplate); + + String webhookUrl = "https://webhook.com/webhookb2/9628fa60-d873-11ed-913c-a196b1f9b445"; + var targetConfig = new MicrosoftTeamsNotificationTargetConfig(); + targetConfig.setWebhookUrl(webhookUrl); + targetConfig.setChannelName("My channel"); + NotificationTarget target = new NotificationTarget(); + target.setName("Microsoft Teams channel"); + target.setConfiguration(targetConfig); + target = saveNotificationTarget(target); + + var template = new MicrosoftTeamsDeliveryMethodNotificationTemplate(); + template.setEnabled(true); + template.setSubject("This is subject"); + template.setBody("This is text"); + template.setThemeColor("ff0000"); + var button = new MicrosoftTeamsDeliveryMethodNotificationTemplate.Button(); + button.setName("Go to ThingsBoard Cloud"); + button.setUri("https://thingsboard.cloud"); + template.setButton(button); + NotificationTemplate notificationTemplate = new NotificationTemplate(); + notificationTemplate.setName("Notification to Teams"); + notificationTemplate.setNotificationType(NotificationType.GENERAL); + NotificationTemplateConfig templateConfig = new NotificationTemplateConfig(); + templateConfig.setDeliveryMethodsTemplates(Map.of( + NotificationDeliveryMethod.MICROSOFT_TEAMS, template + )); + notificationTemplate.setConfiguration(templateConfig); + notificationTemplate = saveNotificationTemplate(notificationTemplate); + + NotificationRequest notificationRequest = NotificationRequest.builder() + .tenantId(tenantId) + .originatorEntityId(tenantAdminUserId) + .templateId(notificationTemplate.getId()) + .targets(List.of(target.getUuidId())) + .build(); + + NotificationRequestPreview preview = doPost("/api/notification/request/preview", notificationRequest, NotificationRequestPreview.class); + System.err.println(preview); + assertThat(preview.getRecipientsCountByTarget().get(target.getName())).isEqualTo(1); + assertThat(preview.getRecipientsPreview()).containsOnly(targetConfig.getChannelName()); + + var messageCaptor = ArgumentCaptor.forClass(MicrosoftTeamsNotificationChannel.Message.class); + doPost("/api/notification/request", notificationRequest, NotificationRequest.class); + verify(restTemplate, timeout(20000)).postForEntity(eq(webhookUrl), messageCaptor.capture(), any()); + + var message = messageCaptor.getValue(); + assertThat(message.getThemeColor()).isEqualTo(template.getThemeColor()); + assertThat(message.getSections().get(0).getActivityTitle()).isEqualTo(template.getSubject()); + assertThat(message.getSections().get(0).getActivitySubtitle()).isEqualTo(template.getBody()); + assertThat(message.getPotentialAction().get(0).getName()).isEqualTo(button.getName()); + assertThat(message.getPotentialAction().get(0).getTargets().get(0).getUri()).isEqualTo(button.getUri()); + } + 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/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..4007a11071 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 @@ -24,7 +24,8 @@ public enum NotificationDeliveryMethod { WEB("web"), EMAIL("email"), SMS("SMS"), - SLACK("Slack"); + SLACK("Slack"), + MICROSOFT_TEAMS("Microsoft Teams"); @Getter private final String name; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/MicrosoftTeamsNotificationTargetConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/MicrosoftTeamsNotificationTargetConfig.java new file mode 100644 index 0000000000..02e787c038 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/MicrosoftTeamsNotificationTargetConfig.java @@ -0,0 +1,33 @@ +package org.thingsboard.server.common.data.notification.targets; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotEmpty; + +@Data +@EqualsAndHashCode(callSuper = true) +public class MicrosoftTeamsNotificationTargetConfig extends NotificationTargetConfig implements NotificationRecipient { + + @NotBlank + private String webhookUrl; + @NotEmpty + private String channelName; + + @Override + public NotificationTargetType getType() { + return NotificationTargetType.MICROSOFT_TEAMS; + } + + @Override + public Object getId() { + return webhookUrl; + } + + @Override + public String getTitle() { + return channelName; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/NotificationRecipient.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/NotificationRecipient.java index 940d596332..3fbe7173fd 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/NotificationRecipient.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/NotificationRecipient.java @@ -21,10 +21,16 @@ public interface NotificationRecipient { String getTitle(); - String getFirstName(); + default String getFirstName() { + return null; + } - String getLastName(); + default String getLastName() { + return null; + } - String getEmail(); + default String getEmail() { + return null; + } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/NotificationTargetConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/NotificationTargetConfig.java index 231e419fb0..529167c637 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/NotificationTargetConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/NotificationTargetConfig.java @@ -30,7 +30,8 @@ import org.thingsboard.server.common.data.validation.NoXss; @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") @JsonSubTypes({ @Type(value = PlatformUsersNotificationTargetConfig.class, name = "PLATFORM_USERS"), - @Type(value = SlackNotificationTargetConfig.class, name = "SLACK") + @Type(value = SlackNotificationTargetConfig.class, name = "SLACK"), + @Type(value = MicrosoftTeamsNotificationTargetConfig.class, name = "MICROSOFT_TEAMS") }) @Data public abstract class NotificationTargetConfig { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/NotificationTargetType.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/NotificationTargetType.java index 1254654ecf..4995551d01 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/NotificationTargetType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/NotificationTargetType.java @@ -26,7 +26,8 @@ import java.util.Set; public enum NotificationTargetType { PLATFORM_USERS(Set.of(NotificationDeliveryMethod.WEB, NotificationDeliveryMethod.EMAIL, NotificationDeliveryMethod.SMS)), - SLACK(Set.of(NotificationDeliveryMethod.SLACK)); + SLACK(Set.of(NotificationDeliveryMethod.SLACK)), + MICROSOFT_TEAMS(Set.of(NotificationDeliveryMethod.MICROSOFT_TEAMS)); @Getter private final Set supportedDeliveryMethods; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/MicrosoftTeamsDeliveryMethodNotificationTemplate.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/MicrosoftTeamsDeliveryMethodNotificationTemplate.java new file mode 100644 index 0000000000..0dd75a86fe --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/MicrosoftTeamsDeliveryMethodNotificationTemplate.java @@ -0,0 +1,57 @@ +package org.thingsboard.server.common.data.notification.template; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.tuple.Pair; +import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; + +import java.util.List; +import java.util.function.Consumer; + +@Data +@EqualsAndHashCode(callSuper = true) +@NoArgsConstructor +@ToString(callSuper = true) +public class MicrosoftTeamsDeliveryMethodNotificationTemplate extends DeliveryMethodNotificationTemplate implements HasSubject { + + private String subject; + private String themeColor; + private Button button; + + private String customMessageCardJson; + + public MicrosoftTeamsDeliveryMethodNotificationTemplate(DeliveryMethodNotificationTemplate other) { + super(other); + } + + @Override + public NotificationDeliveryMethod getMethod() { + return NotificationDeliveryMethod.MICROSOFT_TEAMS; + } + + @Override + public MicrosoftTeamsDeliveryMethodNotificationTemplate copy() { + return new MicrosoftTeamsDeliveryMethodNotificationTemplate(this); + } + + @Override + public List getTemplatableValues() { + return List.of( + TemplatableValue.of(body, this::setBody), + TemplatableValue.of(subject, this::setSubject), + TemplatableValue.of(button, Button::getName, Button::setName), + TemplatableValue.of(button, Button::getUri, Button::setUri), + TemplatableValue.of(customMessageCardJson, this::setCustomMessageCardJson) + ); + } + + @Data + public static class Button { + private String name; + private String uri; + } + +} From 996f11da9ea0c5b8af8dc4fe6ef40b97b2461548 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Wed, 28 Jun 2023 15:43:26 +0300 Subject: [PATCH 2/7] Improvements for notification template processing --- .../NotificationProcessingContext.java | 36 ++++--------- .../MicrosoftTeamsNotificationChannel.java | 15 ++++++ .../notification/NotificationApiTest.java | 35 +++++++------ ...icrosoftTeamsNotificationTargetConfig.java | 15 ++++++ .../DeliveryMethodNotificationTemplate.java | 12 ++--- ...ailDeliveryMethodNotificationTemplate.java | 12 ++--- ...amsDeliveryMethodNotificationTemplate.java | 51 +++++++++++++------ ...ackDeliveryMethodNotificationTemplate.java | 6 +++ ...SmsDeliveryMethodNotificationTemplate.java | 6 +++ .../template/TemplatableValue.java | 46 +++++++++++++++++ ...WebDeliveryMethodNotificationTemplate.java | 15 +++--- 11 files changed, 175 insertions(+), 74 deletions(-) create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/notification/template/TemplatableValue.java 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..9782fb473f 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 @@ -18,7 +18,7 @@ package org.thingsboard.server.service.notification; import com.google.common.base.Strings; import lombok.Builder; import lombok.Getter; -import org.apache.commons.collections4.MapUtils; +import org.apache.commons.lang3.StringUtils; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; import org.thingsboard.server.common.data.notification.NotificationRequest; @@ -27,10 +27,8 @@ import org.thingsboard.server.common.data.notification.settings.NotificationDeli import org.thingsboard.server.common.data.notification.settings.NotificationSettings; import org.thingsboard.server.common.data.notification.targets.NotificationRecipient; import org.thingsboard.server.common.data.notification.template.DeliveryMethodNotificationTemplate; -import org.thingsboard.server.common.data.notification.template.HasSubject; import org.thingsboard.server.common.data.notification.template.NotificationTemplate; import org.thingsboard.server.common.data.notification.template.NotificationTemplateConfig; -import org.thingsboard.server.common.data.notification.template.WebDeliveryMethodNotificationTemplate; import org.thingsboard.server.common.data.util.TemplateUtils; import java.util.EnumMap; @@ -38,8 +36,6 @@ import java.util.HashMap; import java.util.Map; import java.util.Set; -import static org.apache.commons.lang3.StringUtils.isNotEmpty; - @SuppressWarnings("unchecked") public class NotificationProcessingContext { @@ -86,12 +82,11 @@ public class NotificationProcessingContext { public T getProcessedTemplate(NotificationDeliveryMethod deliveryMethod, NotificationRecipient recipient) { T template = (T) templates.get(deliveryMethod); - Map additionalTemplateContext = null; if (recipient != null) { - additionalTemplateContext = createTemplateContextForRecipient(recipient); - } - if (MapUtils.isNotEmpty(additionalTemplateContext) && template.containsAny(additionalTemplateContext.keySet().toArray(String[]::new))) { - template = processTemplate(template, additionalTemplateContext); + Map additionalTemplateContext = createTemplateContextForRecipient(recipient); + if (template.getTemplatableValues().stream().anyMatch(value -> value.containsParams(additionalTemplateContext.keySet()))) { + template = processTemplate(template, additionalTemplateContext); + } } return template; } @@ -107,22 +102,13 @@ public class NotificationProcessingContext { if (templateContext.isEmpty()) return template; template = (T) template.copy(); - template.setBody(TemplateUtils.processTemplate(template.getBody(), templateContext)); - if (template instanceof HasSubject) { - String subject = ((HasSubject) template).getSubject(); - ((HasSubject) template).setSubject(TemplateUtils.processTemplate(subject, templateContext)); - } - if (template instanceof WebDeliveryMethodNotificationTemplate) { - WebDeliveryMethodNotificationTemplate webNotificationTemplate = (WebDeliveryMethodNotificationTemplate) template; - String buttonText = webNotificationTemplate.getButtonText(); - if (isNotEmpty(buttonText)) { - webNotificationTemplate.setButtonText(TemplateUtils.processTemplate(buttonText, templateContext)); + template.getTemplatableValues().forEach(templatableValue -> { + String value = templatableValue.get(); + if (StringUtils.isNotEmpty(value)) { + value = TemplateUtils.processTemplate(value, templateContext); + templatableValue.set(value); } - String buttonLink = webNotificationTemplate.getButtonLink(); - if (isNotEmpty(buttonLink)) { - webNotificationTemplate.setButtonLink(TemplateUtils.processTemplate(buttonLink, templateContext)); - } - } + }); return template; } diff --git a/application/src/main/java/org/thingsboard/server/service/notification/channels/MicrosoftTeamsNotificationChannel.java b/application/src/main/java/org/thingsboard/server/service/notification/channels/MicrosoftTeamsNotificationChannel.java index ceab93c888..c628bf11f6 100644 --- a/application/src/main/java/org/thingsboard/server/service/notification/channels/MicrosoftTeamsNotificationChannel.java +++ b/application/src/main/java/org/thingsboard/server/service/notification/channels/MicrosoftTeamsNotificationChannel.java @@ -1,3 +1,18 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package org.thingsboard.server.service.notification.channels; import com.fasterxml.jackson.annotation.JsonInclude; 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 be972b29c3..946c15a68d 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 @@ -23,9 +23,10 @@ import org.junit.Test; import org.mockito.ArgumentCaptor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.client.RestTemplate; -import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.api.NotificationCenter; import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.NotificationTargetId; import org.thingsboard.server.common.data.notification.Notification; import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; @@ -36,7 +37,7 @@ import org.thingsboard.server.common.data.notification.NotificationRequestPrevie import org.thingsboard.server.common.data.notification.NotificationRequestStats; import org.thingsboard.server.common.data.notification.NotificationRequestStatus; import org.thingsboard.server.common.data.notification.NotificationType; -import org.thingsboard.server.common.data.notification.info.AlarmNotificationInfo; +import org.thingsboard.server.common.data.notification.info.EntityActionNotificationInfo; 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.targets.MicrosoftTeamsNotificationTargetConfig; @@ -66,20 +67,18 @@ 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; -import static org.assertj.core.api.Assertions.in; import static org.assertj.core.api.InstanceOfAssertFactories.type; import static org.awaitility.Awaitility.await; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.timeout; import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; @DaoSqlTest @Slf4j @@ -554,12 +553,13 @@ public class NotificationApiTest extends AbstractNotificationApiTest { var template = new MicrosoftTeamsDeliveryMethodNotificationTemplate(); template.setEnabled(true); - template.setSubject("This is subject"); - template.setBody("This is text"); + String templateParams = "${recipientTitle} - ${entityType}"; + template.setSubject("Subject: " + templateParams); + template.setBody("Body: " + templateParams); template.setThemeColor("ff0000"); var button = new MicrosoftTeamsDeliveryMethodNotificationTemplate.Button(); - button.setName("Go to ThingsBoard Cloud"); - button.setUri("https://thingsboard.cloud"); + button.setName("Button: " + templateParams); + button.setUri("https://" + templateParams); template.setButton(button); NotificationTemplate notificationTemplate = new NotificationTemplate(); notificationTemplate.setName("Notification to Teams"); @@ -576,23 +576,28 @@ public class NotificationApiTest extends AbstractNotificationApiTest { .originatorEntityId(tenantAdminUserId) .templateId(notificationTemplate.getId()) .targets(List.of(target.getUuidId())) + .info(EntityActionNotificationInfo.builder() + .entityId(new DeviceId(UUID.randomUUID())) // to test templatization + .actionType(ActionType.ADDED) + .userId(tenantAdminUserId.getId()) + .build()) .build(); NotificationRequestPreview preview = doPost("/api/notification/request/preview", notificationRequest, NotificationRequestPreview.class); - System.err.println(preview); assertThat(preview.getRecipientsCountByTarget().get(target.getName())).isEqualTo(1); assertThat(preview.getRecipientsPreview()).containsOnly(targetConfig.getChannelName()); var messageCaptor = ArgumentCaptor.forClass(MicrosoftTeamsNotificationChannel.Message.class); - doPost("/api/notification/request", notificationRequest, NotificationRequest.class); + notificationCenter.processNotificationRequest(tenantId, notificationRequest, null); verify(restTemplate, timeout(20000)).postForEntity(eq(webhookUrl), messageCaptor.capture(), any()); var message = messageCaptor.getValue(); + String expectedParams = "My channel - Device"; assertThat(message.getThemeColor()).isEqualTo(template.getThemeColor()); - assertThat(message.getSections().get(0).getActivityTitle()).isEqualTo(template.getSubject()); - assertThat(message.getSections().get(0).getActivitySubtitle()).isEqualTo(template.getBody()); - assertThat(message.getPotentialAction().get(0).getName()).isEqualTo(button.getName()); - assertThat(message.getPotentialAction().get(0).getTargets().get(0).getUri()).isEqualTo(button.getUri()); + assertThat(message.getSections().get(0).getActivityTitle()).isEqualTo("Subject: " + expectedParams); + assertThat(message.getSections().get(0).getActivitySubtitle()).isEqualTo("Body: " + expectedParams); + assertThat(message.getPotentialAction().get(0).getName()).isEqualTo("Button: " + expectedParams); + assertThat(message.getPotentialAction().get(0).getTargets().get(0).getUri()).isEqualTo("https://" + expectedParams); } private void checkFullNotificationsUpdate(UnreadNotificationsUpdate notificationsUpdate, String... expectedNotifications) { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/MicrosoftTeamsNotificationTargetConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/MicrosoftTeamsNotificationTargetConfig.java index 02e787c038..826f64bbd4 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/MicrosoftTeamsNotificationTargetConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/MicrosoftTeamsNotificationTargetConfig.java @@ -1,3 +1,18 @@ +/** + * 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.targets; import lombok.Data; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/DeliveryMethodNotificationTemplate.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/DeliveryMethodNotificationTemplate.java index ecb7f2d2b6..30316ef33b 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/DeliveryMethodNotificationTemplate.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/DeliveryMethodNotificationTemplate.java @@ -22,10 +22,10 @@ import com.fasterxml.jackson.annotation.JsonSubTypes.Type; import com.fasterxml.jackson.annotation.JsonTypeInfo; import lombok.Data; import lombok.NoArgsConstructor; -import org.apache.commons.lang3.StringUtils; import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; import javax.validation.constraints.NotEmpty; +import java.util.List; @JsonIgnoreProperties(ignoreUnknown = true) @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "method") @@ -33,7 +33,8 @@ import javax.validation.constraints.NotEmpty; @Type(name = "WEB", value = WebDeliveryMethodNotificationTemplate.class), @Type(name = "EMAIL", value = EmailDeliveryMethodNotificationTemplate.class), @Type(name = "SMS", value = SmsDeliveryMethodNotificationTemplate.class), - @Type(name = "SLACK", value = SlackDeliveryMethodNotificationTemplate.class) + @Type(name = "SLACK", value = SlackDeliveryMethodNotificationTemplate.class), + @Type(name = "MICROSOFT_TEAMS", value = MicrosoftTeamsDeliveryMethodNotificationTemplate.class) }) @Data @NoArgsConstructor @@ -41,7 +42,7 @@ public abstract class DeliveryMethodNotificationTemplate { private boolean enabled; @NotEmpty - private String body; + protected String body; public DeliveryMethodNotificationTemplate(DeliveryMethodNotificationTemplate other) { this.enabled = other.enabled; @@ -54,8 +55,7 @@ public abstract class DeliveryMethodNotificationTemplate { @JsonIgnore public abstract DeliveryMethodNotificationTemplate copy(); - public boolean containsAny(String... params) { - return StringUtils.containsAny(body, params); - } + @JsonIgnore + public abstract List getTemplatableValues(); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/EmailDeliveryMethodNotificationTemplate.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/EmailDeliveryMethodNotificationTemplate.java index fe909a104e..4ac2e1fb17 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/EmailDeliveryMethodNotificationTemplate.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/EmailDeliveryMethodNotificationTemplate.java @@ -19,12 +19,12 @@ import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; import lombok.ToString; -import org.apache.commons.lang3.StringUtils; import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; import org.thingsboard.server.common.data.validation.Length; import org.thingsboard.server.common.data.validation.NoXss; import javax.validation.constraints.NotEmpty; +import java.util.List; @Data @NoArgsConstructor @@ -37,6 +37,11 @@ public class EmailDeliveryMethodNotificationTemplate extends DeliveryMethodNotif @NotEmpty private String subject; + private final List templatableValues = List.of( + TemplatableValue.of(this::getBody, this::setBody), + TemplatableValue.of(this::getSubject, this::setSubject) + ); + public EmailDeliveryMethodNotificationTemplate(EmailDeliveryMethodNotificationTemplate other) { super(other); this.subject = other.subject; @@ -52,9 +57,4 @@ public class EmailDeliveryMethodNotificationTemplate extends DeliveryMethodNotif return new EmailDeliveryMethodNotificationTemplate(this); } - @Override - public boolean containsAny(String... params) { - return super.containsAny(params) || StringUtils.containsAny(subject, params); - } - } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/MicrosoftTeamsDeliveryMethodNotificationTemplate.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/MicrosoftTeamsDeliveryMethodNotificationTemplate.java index 0dd75a86fe..c29849d83e 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/MicrosoftTeamsDeliveryMethodNotificationTemplate.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/MicrosoftTeamsDeliveryMethodNotificationTemplate.java @@ -1,15 +1,27 @@ +/** + * 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.template; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; import lombok.ToString; -import org.apache.commons.lang3.StringUtils; -import org.apache.commons.lang3.tuple.Pair; import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; import java.util.List; -import java.util.function.Consumer; @Data @EqualsAndHashCode(callSuper = true) @@ -23,8 +35,22 @@ public class MicrosoftTeamsDeliveryMethodNotificationTemplate extends DeliveryMe private String customMessageCardJson; - public MicrosoftTeamsDeliveryMethodNotificationTemplate(DeliveryMethodNotificationTemplate other) { + private final List templatableValues = List.of( + TemplatableValue.of(this::getBody, this::setBody), + TemplatableValue.of(this::getSubject, this::setSubject), + TemplatableValue.of(() -> button != null ? button.getName() : null, + processed -> {if (button != null) button.setName(processed);}), + TemplatableValue.of(() -> button != null ? button.getUri() : null, + processed -> {if (button != null) button.setUri(processed);}), + TemplatableValue.of(this::getCustomMessageCardJson, this::setCustomMessageCardJson) + ); + + public MicrosoftTeamsDeliveryMethodNotificationTemplate(MicrosoftTeamsDeliveryMethodNotificationTemplate other) { super(other); + this.subject = other.subject; + this.themeColor = other.themeColor; + this.button = other.button != null ? new Button(other.button) : null; + this.customMessageCardJson = other.customMessageCardJson; } @Override @@ -37,21 +63,16 @@ public class MicrosoftTeamsDeliveryMethodNotificationTemplate extends DeliveryMe return new MicrosoftTeamsDeliveryMethodNotificationTemplate(this); } - @Override - public List getTemplatableValues() { - return List.of( - TemplatableValue.of(body, this::setBody), - TemplatableValue.of(subject, this::setSubject), - TemplatableValue.of(button, Button::getName, Button::setName), - TemplatableValue.of(button, Button::getUri, Button::setUri), - TemplatableValue.of(customMessageCardJson, this::setCustomMessageCardJson) - ); - } - @Data + @NoArgsConstructor public static class Button { private String name; private String uri; + + public Button(Button other) { + this.name = other.name; + this.uri = other.uri; + } } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/SlackDeliveryMethodNotificationTemplate.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/SlackDeliveryMethodNotificationTemplate.java index 8457677308..6a3790feb6 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/SlackDeliveryMethodNotificationTemplate.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/SlackDeliveryMethodNotificationTemplate.java @@ -22,12 +22,18 @@ import lombok.ToString; import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; import org.thingsboard.server.common.data.validation.NoXss; +import java.util.List; + @Data @NoArgsConstructor @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class SlackDeliveryMethodNotificationTemplate extends DeliveryMethodNotificationTemplate { + private final List templatableValues = List.of( + TemplatableValue.of(this::getBody, this::setBody) + ); + public SlackDeliveryMethodNotificationTemplate(DeliveryMethodNotificationTemplate other) { super(other); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/SmsDeliveryMethodNotificationTemplate.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/SmsDeliveryMethodNotificationTemplate.java index 7dc2e494f3..f4aab10e1e 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/SmsDeliveryMethodNotificationTemplate.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/SmsDeliveryMethodNotificationTemplate.java @@ -23,12 +23,18 @@ import org.thingsboard.server.common.data.notification.NotificationDeliveryMetho import org.thingsboard.server.common.data.validation.Length; import org.thingsboard.server.common.data.validation.NoXss; +import java.util.List; + @Data @NoArgsConstructor @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class SmsDeliveryMethodNotificationTemplate extends DeliveryMethodNotificationTemplate { + private final List templatableValues = List.of( + TemplatableValue.of(this::getBody, this::setBody) + ); + public SmsDeliveryMethodNotificationTemplate(SmsDeliveryMethodNotificationTemplate other) { super(other); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/TemplatableValue.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/TemplatableValue.java new file mode 100644 index 0000000000..25dfeb643a --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/TemplatableValue.java @@ -0,0 +1,46 @@ +/** + * 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.template; + +import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.StringUtils; + +import java.util.Collection; +import java.util.function.Consumer; +import java.util.function.Supplier; + +@RequiredArgsConstructor +public class TemplatableValue { + private final Supplier getter; + private final Consumer setter; + + public static TemplatableValue of(Supplier getter, Consumer setter) { + return new TemplatableValue(getter, setter); + } + + public String get() { + return getter.get(); + } + + public void set(String processed) { + setter.accept(processed); + } + + public boolean containsParams(Collection params) { + return StringUtils.containsAny(get(), params.toArray(String[]::new)); + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/WebDeliveryMethodNotificationTemplate.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/WebDeliveryMethodNotificationTemplate.java index f26ed00568..f45371f716 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/WebDeliveryMethodNotificationTemplate.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/WebDeliveryMethodNotificationTemplate.java @@ -23,12 +23,12 @@ import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; import lombok.ToString; -import org.apache.commons.lang3.StringUtils; import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; import org.thingsboard.server.common.data.validation.Length; import org.thingsboard.server.common.data.validation.NoXss; import javax.validation.constraints.NotEmpty; +import java.util.List; import java.util.Optional; @Data @@ -43,6 +43,13 @@ public class WebDeliveryMethodNotificationTemplate extends DeliveryMethodNotific private String subject; private JsonNode additionalConfig; + private final List templatableValues = List.of( + TemplatableValue.of(this::getBody, this::setBody), + TemplatableValue.of(this::getSubject, this::setSubject), + TemplatableValue.of(this::getButtonText, this::setButtonText), + TemplatableValue.of(this::getButtonLink, this::setButtonLink) + ); + public WebDeliveryMethodNotificationTemplate(WebDeliveryMethodNotificationTemplate other) { super(other); this.subject = other.subject; @@ -107,10 +114,4 @@ public class WebDeliveryMethodNotificationTemplate extends DeliveryMethodNotific return new WebDeliveryMethodNotificationTemplate(this); } - @Override - public boolean containsAny(String... params) { - return super.containsAny(params) || StringUtils.containsAny(subject, params) - || StringUtils.containsAny(getButtonText(), params) || StringUtils.containsAny(getButtonLink(), params); - } - } From 93e4070e515c07bd59eee4544c721643de18d344 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Tue, 25 Jul 2023 17:50:39 +0300 Subject: [PATCH 3/7] UI: Add UI to configuration notification in Microsoft team --- ...cipient-notification-dialog.component.html | 27 ++- ...cipient-notification-dialog.component.scss | 60 +---- ...recipient-notification-dialog.component.ts | 9 +- .../rule-notification-dialog.component.scss | 1 + .../sent/sent-error-dialog.component.scss | 1 + .../sent-notification-dialog.component.html | 229 ++++++++++++------ .../sent-notification-dialog.component.scss | 104 ++++---- .../template/template-configuration.ts | 30 ++- ...emplate-notification-dialog.component.html | 207 +++++++++++----- ...emplate-notification-dialog.component.scss | 51 ++-- .../app/shared/models/notification.models.ts | 39 ++- .../assets/locale/locale.constant-en_US.json | 10 +- 12 files changed, 477 insertions(+), 291 deletions(-) diff --git a/ui-ngx/src/app/modules/home/pages/notification/recipient/recipient-notification-dialog.component.html b/ui-ngx/src/app/modules/home/pages/notification/recipient/recipient-notification-dialog.component.html index 378407d862..9988adcc92 100644 --- a/ui-ngx/src/app/modules/home/pages/notification/recipient/recipient-notification-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/notification/recipient/recipient-notification-dialog.component.html @@ -61,12 +61,11 @@
- - {{ 'tenant.tenant' | translate }} - {{ 'tenant-profile.tenant-profile' | translate }} - + + {{ 'tenant.tenant' | translate }} + {{ 'tenant-profile.tenant-profile' | translate }} +
+
+ + notification.webhook-url + + + {{ 'notification.webhook-url-required' | translate }} + + + + notification.channel-name + + + {{ 'notification.channel-name-required' | translate }} + + +
notification.description diff --git a/ui-ngx/src/app/modules/home/pages/notification/recipient/recipient-notification-dialog.component.scss b/ui-ngx/src/app/modules/home/pages/notification/recipient/recipient-notification-dialog.component.scss index 8406cae3fe..8a6dec0b47 100644 --- a/ui-ngx/src/app/modules/home/pages/notification/recipient/recipient-notification-dialog.component.scss +++ b/ui-ngx/src/app/modules/home/pages/notification/recipient/recipient-notification-dialog.component.scss @@ -20,6 +20,7 @@ form.tb-dialog-container { min-width: 600px; max-height: 100vh; + color: rgba(0, 0, 0, 0.87); } .mat-dialog-content { @@ -30,6 +31,11 @@ display: block; padding-bottom: 6px; } + + .tb-notification-tenant-group { + width: 280px; + margin-bottom: 16px; + } } :host ::ng-deep { @@ -52,58 +58,4 @@ flex-direction: column; } } - - .mat-button-toggle-group.tb-notification-tenant-group { - &.mat-button-toggle-group-appearance-standard { - border: none; - border-radius: 18px; - margin-bottom: 14px; - - .mat-button-toggle + .mat-button-toggle { - border-left: none; - } - } - - .mat-button-toggle { - background: rgba(0, 0, 0, 0.06); - height: 36px; - align-items: center; - display: flex; - - .mat-button-toggle-ripple { - top: 2px; - left: 2px; - right: 2px; - bottom: 2px; - border-radius: 18px; - } - } - - .mat-button-toggle-button { - color: #959595; - } - - .mat-button-toggle-focus-overlay { - border-radius: 18px; - margin: 2px; - } - - .mat-button-toggle-checked .mat-button-toggle-button { - background-color: $tb-primary-color; - color: #fff; - border-radius: 18px; - margin-left: 2px; - margin-right: 2px; - } - - .mat-button-toggle-appearance-standard .mat-button-toggle-label-content { - line-height: 34px; - font-size: 16px; - font-weight: 500; - } - - .mat-button-toggle-checked.mat-button-toggle-appearance-standard:not(.mat-button-toggle-disabled):hover .mat-button-toggle-focus-overlay { - opacity: .01; - } - } } diff --git a/ui-ngx/src/app/modules/home/pages/notification/recipient/recipient-notification-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/notification/recipient/recipient-notification-dialog.component.ts index c07d8beb75..16ef8d12ae 100644 --- a/ui-ngx/src/app/modules/home/pages/notification/recipient/recipient-notification-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/pages/notification/recipient/recipient-notification-dialog.component.ts @@ -39,7 +39,6 @@ import { Authority } from '@shared/models/authority.enum'; import { AuthState } from '@core/auth/auth.models'; import { getCurrentAuthState } from '@core/auth/auth.selectors'; import { AuthUser } from '@shared/models/user.model'; -import { control } from 'leaflet'; export interface RecipientNotificationDialogData { target?: NotificationTarget; @@ -99,6 +98,8 @@ export class RecipientNotificationDialogComponent extends }), conversationType: [{value: SlackChanelType.PUBLIC_CHANNEL, disabled: true}], conversation: [{value: '', disabled: true}, Validators.required], + webhookUrl: [{value: '', disabled: true}, Validators.required], + channelName: [{value: '', disabled: true}, Validators.required], description: [null] }) }); @@ -116,6 +117,10 @@ export class RecipientNotificationDialogComponent extends this.targetNotificationForm.get('configuration.conversationType').enable({emitEvent: false}); this.targetNotificationForm.get('configuration.conversation').enable({emitEvent: false}); break; + case NotificationTargetType.MICROSOFT_TEAMS: + this.targetNotificationForm.get('configuration.webhookUrl').enable({emitEvent: false}); + this.targetNotificationForm.get('configuration.channelName').enable({emitEvent: false}); + break; } this.targetNotificationForm.get('configuration.type').enable({emitEvent: false}); this.targetNotificationForm.get('configuration.description').enable({emitEvent: false}); @@ -160,7 +165,7 @@ export class RecipientNotificationDialogComponent extends if (isDefinedAndNotNull(data.target)) { this.targetNotificationForm.patchValue(data.target, {emitEvent: false}); this.targetNotificationForm.get('configuration.type').updateValueAndValidity({onlySelf: true}); - if (this.isSysAdmin() && data.target.configuration.usersFilter.type === NotificationTargetConfigType.TENANT_ADMINISTRATORS) { + if (this.isSysAdmin() && data.target.configuration.usersFilter?.type === NotificationTargetConfigType.TENANT_ADMINISTRATORS) { this.targetNotificationForm.get('configuration.usersFilter.filterByTenants') .patchValue(!Array.isArray(this.data.target.configuration.usersFilter.tenantProfilesIds), {onlySelf: true}); } diff --git a/ui-ngx/src/app/modules/home/pages/notification/rule/rule-notification-dialog.component.scss b/ui-ngx/src/app/modules/home/pages/notification/rule/rule-notification-dialog.component.scss index 7e582d2b7d..438d44b664 100644 --- a/ui-ngx/src/app/modules/home/pages/notification/rule/rule-notification-dialog.component.scss +++ b/ui-ngx/src/app/modules/home/pages/notification/rule/rule-notification-dialog.component.scss @@ -97,6 +97,7 @@ flex-direction: column; height: 100%; padding: 0 !important; + color: rgba(0, 0, 0, 0.87); .mat-stepper-horizontal { display: flex; diff --git a/ui-ngx/src/app/modules/home/pages/notification/sent/sent-error-dialog.component.scss b/ui-ngx/src/app/modules/home/pages/notification/sent/sent-error-dialog.component.scss index 95301b4daa..52a40155e4 100644 --- a/ui-ngx/src/app/modules/home/pages/notification/sent/sent-error-dialog.component.scss +++ b/ui-ngx/src/app/modules/home/pages/notification/sent/sent-error-dialog.component.scss @@ -17,6 +17,7 @@ display: block; width: 600px; max-width: 100%; + color: rgba(0, 0, 0, 0.87); h6 { font-weight: 500; diff --git a/ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.component.html b/ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.component.html index 4ff13778e5..878131dd24 100644 --- a/ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.component.html @@ -27,7 +27,6 @@ -
{{ 'notification.compose' | translate }}
- - {{ 'notification.start-from-scratch' | translate }} - {{ 'notification.use-template' | translate }} - + + {{ 'notification.start-from-scratch' | translate }} + {{ 'notification.use-template' | translate }} +
-
-
- +
+
+ {{ 'icon.icon' | translate }} -
- +
+ - +
-
-
- - {{ 'notification.action-button' | translate }} - -
-
- - notification.button-text - - - {{ 'notification.button-text-required' | translate }} - - - {{ 'notification.button-text-max-length' | translate : - {length: webTemplateForm.get('additionalConfig.actionButtonConfig.text').getError('maxlength').requiredLength} - }} - - -
-
- - notification.action-type - - - {{ actionButtonLinkTypeTranslateMap.get(actionButtonLinkType) | translate }} - - - - - notification.link - - - {{ 'notification.link-required' | translate }} - - - - - - - - -
- - {{ 'notification.set-entity-from-notification' | translate }} - -
-
+
+
+ + + + + {{ 'notification.action-button' | translate }} + + + + +
+ + notification.button-text + + + {{ 'notification.button-text-required' | translate }} + + + {{ 'notification.button-text-max-length' | translate : + {length: webTemplateForm.get('additionalConfig.actionButtonConfig.text').getError('maxlength').requiredLength} + }} + + +
+
+ + notification.action-type + + + {{ actionButtonLinkTypeTranslateMap.get(actionButtonLinkType) | translate }} + + + + + notification.link + + + {{ 'notification.link-required' | translate }} + + + + + + + + +
+ + {{ 'notification.set-entity-from-notification' | translate }} + +
+
+
@@ -333,6 +340,78 @@ + + {{ 'notification.delivery-method.microsoft-teams' | translate }} +
+ {{ 'notification.input-fields-support-templatization' | translate}} + +
+
+ + notification.subject + + + + notification.message + + + {{ 'notification.message-required' | translate }} + + +
+
+
notification.theme-color
+ +
+
+ + + + + {{ 'notification.action-button' | translate }} + + + + +
+ + notification.button-text + + + {{ 'notification.button-text-required' | translate }} + + + {{ 'notification.button-text-max-length' | translate : + {length: microsoftTeamsTemplateForm.get('button.name').getError('maxlength').requiredLength} + }} + + + + notification.link + + + {{ 'notification.link-required' | translate }} + + +
+
+
+
+
+
+
{{ 'notification.review' | translate }} +
+
+ +
notification.delivery-method.microsoft-teams-preview
+
+
+
{{ preview.processedTemplates.MICROSOFT_TEAMS.subject }}
+ {{ preview.processedTemplates.MICROSOFT_TEAMS.body }} +
+
supervisor_account @@ -399,7 +488,7 @@
-
+
diff --git a/ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.component.scss b/ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.component.scss index 0702d6bf52..7d838eab70 100644 --- a/ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.component.scss +++ b/ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.component.scss @@ -16,13 +16,33 @@ @import "../../../../../../scss/constants"; @import "../../../../../../theme"; -:host-context(.tb-fullscreen-dialog .mat-mdc-dialog-container) { +:host { width: 820px; height: 100%; max-width: 100%; max-height: 100vh; - display: flex; - flex-direction: column; + display: grid; + grid-template-rows: min-content 4px minmax(auto, 1fr) min-content min-content; +} + +:host-context(.tb-fullscreen-dialog .mat-mdc-dialog-container) { + .mat-mdc-dialog-content { + grid-row: 3; + display: flex; + flex-direction: column; + height: 100%; + padding: 0; + color: rgba(0, 0, 0, 0.87); + } + + .tb-dialog-actions { + grid-row: 5; + display: flex; + } + + .mat-divider { + grid-row: 4; + } .tb-title { font-size: 16px; @@ -76,6 +96,7 @@ .delivery-method-container { display: inline-flex; flex: 1 1 calc(50% - 8px); + max-width: calc(50% - 8px); padding: 16px 12px; border: 1px solid rgba(0, 0, 0, 0.1); border-radius: 6px; @@ -178,17 +199,28 @@ line-height: 20px; overflow-x: auto; } + + &.mini { + font-size: 12px; + line-height: 1.25; + + .subject { + font-size: 14px; + line-height: 1.5; + padding-bottom: 4px; + } + } } } + + .tb-notification-use-template-toggle-group { + margin-bottom: 24px; + width: 320px; + } } :host ::ng-deep { .mat-mdc-dialog-content { - display: flex; - flex-direction: column; - height: 100%; - padding: 0 !important; - .mat-stepper-horizontal { display: flex; height: 100%; @@ -209,59 +241,9 @@ } } } - } - - .mat-button-toggle-group.tb-notification-use-template-toggle-group { - &.mat-button-toggle-group-appearance-standard { - border: none; - border-radius: 18px; - margin-bottom: 24px; - - .mat-button-toggle + .mat-button-toggle { - border-left: none; - } - } - - .mat-button-toggle { - background: rgba(0, 0, 0, 0.06); - height: 36px; - align-items: center; - display: flex; - - .mat-button-toggle-ripple { - top: 2px; - left: 2px; - right: 2px; - bottom: 2px; - border-radius: 18px; - } - } - .mat-button-toggle-button { - color: #959595; - } - - .mat-button-toggle-focus-overlay { - border-radius: 18px; - margin: 2px; - } - - .mat-button-toggle-checked .mat-button-toggle-button { - background-color: $tb-primary-color; - color: #fff; - border-radius: 18px; - margin-left: 2px; - margin-right: 2px; - } - - .mat-button-toggle-appearance-standard .mat-button-toggle-label-content { - line-height: 34px; - font-size: 16px; - font-weight: 500; - } - - .mat-button-toggle-checked.mat-button-toggle-appearance-standard:not(.mat-button-toggle-disabled):hover .mat-button-toggle-focus-overlay { - opacity: .01; + .tb-form-panel .mat-expansion-panel.tb-settings > .mat-expansion-panel-content > .mat-expansion-panel-body { + gap: 0; } } @@ -273,9 +255,7 @@ } } } -} -:host ::ng-deep { .delivery-methods-container { .delivery-method-container { &.interact * { diff --git a/ui-ngx/src/app/modules/home/pages/notification/template/template-configuration.ts b/ui-ngx/src/app/modules/home/pages/notification/template/template-configuration.ts index b3fa00ef72..dade64c22e 100644 --- a/ui-ngx/src/app/modules/home/pages/notification/template/template-configuration.ts +++ b/ui-ngx/src/app/modules/home/pages/notification/template/template-configuration.ts @@ -43,6 +43,7 @@ export abstract class TemplateConfiguration extends DialogComponent< emailTemplateForm: FormGroup; smsTemplateForm: FormGroup; slackTemplateForm: FormGroup; + microsoftTeamsTemplateForm: FormGroup; notificationDeliveryMethods = Object.keys(NotificationDeliveryMethod) as NotificationDeliveryMethod[]; notificationDeliveryMethodTranslateMap = NotificationDeliveryMethodTranslateMap; @@ -95,7 +96,7 @@ export abstract class TemplateConfiguration extends DialogComponent< icon: this.fb.group({ enabled: [false], icon: [{value: 'notifications', disabled: true}, Validators.required], - color: ['#757575'] + color: [{value: '#757575', disabled: true}] }), actionButtonConfig: this.fb.group({ enabled: [false], @@ -114,8 +115,10 @@ export abstract class TemplateConfiguration extends DialogComponent< ).subscribe((value) => { if (value) { this.webTemplateForm.get('additionalConfig.icon.icon').enable({emitEvent: false}); + this.webTemplateForm.get('additionalConfig.icon.color').enable({emitEvent: false}); } else { this.webTemplateForm.get('additionalConfig.icon.icon').disable({emitEvent: false}); + this.webTemplateForm.get('additionalConfig.icon.color').disable({emitEvent: false}); } }); @@ -163,11 +166,34 @@ export abstract class TemplateConfiguration extends DialogComponent< body: ['', Validators.required] }); + this.microsoftTeamsTemplateForm = this.fb.group({ + subject: [''], + body: ['', Validators.required], + themeColor: [''], + button: this.fb.group({ + enabled: [false], + name: [{value: '', disabled: true}, [Validators.required, Validators.maxLength(50)]], + uri: [{value: '', disabled: true}, Validators.required], + }), + }); + + this.microsoftTeamsTemplateForm.get('button.enabled').valueChanges.pipe( + takeUntil(this.destroy$) + ).subscribe((value) => { + if (value) { + this.microsoftTeamsTemplateForm.get('button').enable({emitEvent: false}); + } else { + this.microsoftTeamsTemplateForm.get('button').disable({emitEvent: false}); + this.microsoftTeamsTemplateForm.get('button.enabled').enable({emitEvent: false}); + } + }); + this.deliveryMethodFormsMap = new Map([ [NotificationDeliveryMethod.WEB, this.webTemplateForm], [NotificationDeliveryMethod.EMAIL, this.emailTemplateForm], [NotificationDeliveryMethod.SMS, this.smsTemplateForm], - [NotificationDeliveryMethod.SLACK, this.slackTemplateForm] + [NotificationDeliveryMethod.SLACK, this.slackTemplateForm], + [NotificationDeliveryMethod.MICROSOFT_TEAMS, this.microsoftTeamsTemplateForm] ]); } diff --git a/ui-ngx/src/app/modules/home/pages/notification/template/template-notification-dialog.component.html b/ui-ngx/src/app/modules/home/pages/notification/template/template-notification-dialog.component.html index 067258fa24..6d27ef4980 100644 --- a/ui-ngx/src/app/modules/home/pages/notification/template/template-notification-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/notification/template/template-notification-dialog.component.html @@ -27,7 +27,6 @@ -
-
-
- +
+
+ {{ 'icon.icon' | translate }} -
- +
+ - +
-
-
- - {{ 'notification.action-button' | translate }} - -
-
- - notification.button-text - - - {{ 'notification.button-text-required' | translate }} - - - {{ 'notification.button-text-max-length' | translate : - {length: webTemplateForm.get('additionalConfig.actionButtonConfig.text').getError('maxlength').requiredLength} - }} - - -
-
- - notification.action-type - - - {{ actionButtonLinkTypeTranslateMap.get(actionButtonLinkType) | translate }} - - - - - notification.link - - - {{ 'notification.link-required' | translate }} - - - - - - - - -
- - {{ 'notification.set-entity-from-notification' | translate }} - -
-
+
+
+ + + + + {{ 'notification.action-button' | translate }} + + + + +
+ + notification.button-text + + + {{ 'notification.button-text-required' | translate }} + + + {{ 'notification.button-text-max-length' | translate : + {length: webTemplateForm.get('additionalConfig.actionButtonConfig.text').getError('maxlength').requiredLength} + }} + + +
+
+ + notification.action-type + + + {{ actionButtonLinkTypeTranslateMap.get(actionButtonLinkType) | translate }} + + + + + notification.link + + + {{ 'notification.link-required' | translate }} + + + + + + + + +
+ + {{ 'notification.set-entity-from-notification' | translate }} + +
+
+
@@ -259,10 +267,81 @@ + + {{ 'notification.delivery-method.microsoft-teams' | translate }} +
+ {{ 'notification.input-fields-support-templatization' | translate}} + +
+
+ + notification.subject + + + + notification.message + + + {{ 'notification.message-required' | translate }} + + +
+
+
notification.theme-color
+ +
+
+ + + + + {{ 'notification.action-button' | translate }} + + + + +
+ + notification.button-text + + + {{ 'notification.button-text-required' | translate }} + + + {{ 'notification.button-text-max-length' | translate : + {length: microsoftTeamsTemplateForm.get('button.name').getError('maxlength').requiredLength} + }} + + + + notification.link + + + {{ 'notification.link-required' | translate }} + + +
+
+
+
+
+
+
-
+
diff --git a/ui-ngx/src/app/modules/home/pages/notification/template/template-notification-dialog.component.scss b/ui-ngx/src/app/modules/home/pages/notification/template/template-notification-dialog.component.scss index 1a5fa270a8..507054440d 100644 --- a/ui-ngx/src/app/modules/home/pages/notification/template/template-notification-dialog.component.scss +++ b/ui-ngx/src/app/modules/home/pages/notification/template/template-notification-dialog.component.scss @@ -16,13 +16,33 @@ @import "../../../../../../scss/constants"; @import "../../../../../../theme"; -:host-context(.tb-fullscreen-dialog .mat-mdc-dialog-container) { - width: 800px; +:host { + width: 840px; height: 100%; max-width: 100%; max-height: 100vh; - display: flex; - flex-direction: column; + display: grid; + grid-template-rows: min-content 4px minmax(auto, 1fr) min-content min-content; +} + +:host-context(.tb-fullscreen-dialog .mat-mdc-dialog-container) { + .mat-mdc-dialog-content { + grid-row: 3; + display: flex; + flex-direction: column; + height: 100%; + padding: 0; + color: rgba(0, 0, 0, 0.87); + } + + .tb-dialog-actions { + grid-row: 5; + display: flex; + } + + .mat-divider { + grid-row: 4; + } .tb-title { font-size: 16px; @@ -70,6 +90,7 @@ .delivery-method-container { display: inline-flex; flex: 1 1 calc(50% - 8px); + max-width: calc(50% - 8px); padding: 16px 12px; border: 1px solid rgba(0, 0, 0, 0.1); border-radius: 6px; @@ -80,28 +101,10 @@ } } } - - .additional-config-group { - padding: 16px 16px 4px; - margin-bottom: 12px; - border: 1px solid rgba(0, 0, 0, 0.1); - border-radius: 6px; - width: 100%; - height: 100%; - - .toggle { - margin-bottom: 12px; - } - } } :host ::ng-deep { .mat-mdc-dialog-content { - display: flex; - flex-direction: column; - height: 100%; - padding: 0 !important; - .mat-stepper-horizontal { display: flex; height: 100%; @@ -122,5 +125,9 @@ } } } + + .tb-form-panel .mat-expansion-panel.tb-settings > .mat-expansion-panel-content > .mat-expansion-panel-body { + gap: 0; + } } } diff --git a/ui-ngx/src/app/shared/models/notification.models.ts b/ui-ngx/src/app/shared/models/notification.models.ts index edafef4704..9414368029 100644 --- a/ui-ngx/src/app/shared/models/notification.models.ts +++ b/ui-ngx/src/app/shared/models/notification.models.ts @@ -246,7 +246,9 @@ export interface NotificationTarget extends Omit, configuration: NotificationTargetConfig; } -export interface NotificationTargetConfig extends Partial { +export interface NotificationTargetConfig extends Partial { description?: string; type: NotificationTargetType; } @@ -276,14 +278,21 @@ export interface SlackNotificationTargetConfig { conversationType: SlackChanelType; conversation: SlackConversation; } + +export interface MicrosoftTeamsNotificationTargetConfig { + webhookUrl: string; + channelName: string; +} export enum NotificationTargetType { PLATFORM_USERS = 'PLATFORM_USERS', - SLACK = 'SLACK' + SLACK = 'SLACK', + MICROSOFT_TEAMS = 'MICROSOFT_TEAMS' } export const NotificationTargetTypeTranslationMap = new Map([ [NotificationTargetType.PLATFORM_USERS, 'notification.platform-users'], - [NotificationTargetType.SLACK, 'notification.delivery-method.slack'] + [NotificationTargetType.SLACK, 'notification.delivery-method.slack'], + [NotificationTargetType.MICROSOFT_TEAMS, 'notification.delivery-method.microsoft-teams'], ]); export interface NotificationTemplate extends Omit, 'label'>, ExportableEntity { @@ -299,14 +308,17 @@ interface NotificationTemplateConfig { } export interface DeliveryMethodNotificationTemplate extends - Partial{ - body?: string; + Partial{ + body: string; enabled: boolean; method: NotificationDeliveryMethod; } interface WebDeliveryMethodNotificationTemplate { - subject?: string; + subject: string; additionalConfig: WebDeliveryMethodAdditionalConfig; } @@ -336,6 +348,15 @@ interface SlackDeliveryMethodNotificationTemplate { conversationId: string; } +interface MicrosoftTeamsDeliveryMethodNotificationTemplate { + subject?: string; + button: { + enabled: boolean; + name?: string; + uri?: string; + }; +} + export enum NotificationStatus { SENT = 'SENT', READ = 'READ' @@ -345,14 +366,16 @@ export enum NotificationDeliveryMethod { WEB = 'WEB', SMS = 'SMS', EMAIL = 'EMAIL', - SLACK = 'SLACK' + SLACK = 'SLACK', + MICROSOFT_TEAMS = 'MICROSOFT_TEAMS' } export const NotificationDeliveryMethodTranslateMap = new Map([ [NotificationDeliveryMethod.WEB, 'notification.delivery-method.web'], [NotificationDeliveryMethod.SMS, 'notification.delivery-method.sms'], [NotificationDeliveryMethod.EMAIL, 'notification.delivery-method.email'], - [NotificationDeliveryMethod.SLACK, 'notification.delivery-method.slack'] + [NotificationDeliveryMethod.SLACK, 'notification.delivery-method.slack'], + [NotificationDeliveryMethod.MICROSOFT_TEAMS, 'notification.delivery-method.microsoft-teams'], ]); export enum NotificationRequestStatus { 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 0f96a5f1dc..53e28f97ce 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -2914,6 +2914,8 @@ "email-preview": "Email notification preview", "slack": "Slack", "slack-preview": "Slack notification preview", + "microsoft-teams": "Microsoft teams", + "microsoft-teams-preview": "Microsoft teams notification preview", "sms": "SMS", "sms-preview": "SMS notification preview", "web": "Web", @@ -3079,6 +3081,7 @@ "tenant-profiles-list-rule-hint": "If the field is empty, the trigger will be applied to all tenant profiles", "tenants-list-rule-hint": "If the field is empty, the trigger will be applied to all tenants", "threshold": "Threshold", + "theme-color": "Theme color", "time": "Time", "track-rule-node-events": "Track rule node events", "trigger": { @@ -3100,7 +3103,12 @@ "updated": "Updated", "use-template": "Use template", "view-all": "View all", - "warning": "Warning" + "warning": "Warning", + "webhook-url": "Webhook URL", + "webhook-url-required": "Webhook URL is required", + "channel-name": "Chanel name", + "channel-name-required": "Chanel name is required" + }, "ota-update": { "add": "Add package", From 70666b6f93b062dc2f8845a1fd779de4750c93e4 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Wed, 26 Jul 2023 15:15:05 +0300 Subject: [PATCH 4/7] UI: Change model action button in Microsoft Teams notification --- .../sent-notification-dialog.component.html | 42 ++++++-- .../sent-notification-dialog.component.scss | 8 +- .../template/template-configuration.ts | 102 ++++++++---------- ...emplate-notification-dialog.component.html | 42 ++++++-- ...emplate-notification-dialog.component.scss | 8 +- .../app/shared/models/notification.models.ts | 26 +++-- 6 files changed, 138 insertions(+), 90 deletions(-) diff --git a/ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.component.html b/ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.component.html index 878131dd24..7358e06d77 100644 --- a/ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.component.html @@ -384,28 +384,54 @@ - +
notification.button-text - - + + {{ 'notification.button-text-required' | translate }} - + {{ 'notification.button-text-max-length' | translate : - {length: microsoftTeamsTemplateForm.get('button.name').getError('maxlength').requiredLength} + {length: microsoftTeamsTemplateForm.get('button.text').getError('maxlength').requiredLength} }} - +
+
+ + notification.action-type + + + {{ actionButtonLinkTypeTranslateMap.get(actionButtonLinkType) | translate }} + + + + notification.link - - + + {{ 'notification.link-required' | translate }} + + + + + +
+ + {{ 'notification.set-entity-from-notification' | translate }} +
diff --git a/ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.component.scss b/ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.component.scss index 7d838eab70..97eb99a9fc 100644 --- a/ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.component.scss +++ b/ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.component.scss @@ -242,8 +242,12 @@ } } - .tb-form-panel .mat-expansion-panel.tb-settings > .mat-expansion-panel-content > .mat-expansion-panel-body { - gap: 0; + .tb-form-panel .mat-expansion-panel.tb-settings { + padding: 11px 16px; + + & > .mat-expansion-panel-content > .mat-expansion-panel-body { + gap: 0; + } } } diff --git a/ui-ngx/src/app/modules/home/pages/notification/template/template-configuration.ts b/ui-ngx/src/app/modules/home/pages/notification/template/template-configuration.ts index dade64c22e..c2dc5355e8 100644 --- a/ui-ngx/src/app/modules/home/pages/notification/template/template-configuration.ts +++ b/ui-ngx/src/app/modules/home/pages/notification/template/template-configuration.ts @@ -98,15 +98,7 @@ export abstract class TemplateConfiguration extends DialogComponent< icon: [{value: 'notifications', disabled: true}, Validators.required], color: [{value: '#757575', disabled: true}] }), - actionButtonConfig: this.fb.group({ - enabled: [false], - text: [{value: '', disabled: true}, [Validators.required, Validators.maxLength(50)]], - linkType: [ActionButtonLinkType.LINK], - link: [{value: '', disabled: true}, Validators.required], - dashboardId: [{value: null, disabled: true}, Validators.required], - dashboardState: [{value: null, disabled: true}], - setEntityIdInState: [{value: true, disabled: true}], - }), + actionButtonConfig: this.createButtonConfigForm() }) }); @@ -122,37 +114,6 @@ export abstract class TemplateConfiguration extends DialogComponent< } }); - this.webTemplateForm.get('additionalConfig.actionButtonConfig.enabled').valueChanges.pipe( - takeUntil(this.destroy$) - ).subscribe((value) => { - if (value) { - this.webTemplateForm.get('additionalConfig.actionButtonConfig').enable({emitEvent: false}); - this.webTemplateForm.get('additionalConfig.actionButtonConfig.linkType').updateValueAndValidity({onlySelf: true}); - } else { - this.webTemplateForm.get('additionalConfig.actionButtonConfig').disable({emitEvent: false}); - this.webTemplateForm.get('additionalConfig.actionButtonConfig.enabled').enable({emitEvent: false}); - } - }); - - this.webTemplateForm.get('additionalConfig.actionButtonConfig.linkType').valueChanges.pipe( - takeUntil(this.destroy$) - ).subscribe((value) => { - const isEnabled = this.webTemplateForm.get('additionalConfig.actionButtonConfig.enabled').value; - if (isEnabled) { - if (value === ActionButtonLinkType.LINK) { - this.webTemplateForm.get('additionalConfig.actionButtonConfig.link').enable({emitEvent: false}); - this.webTemplateForm.get('additionalConfig.actionButtonConfig.dashboardId').disable({emitEvent: false}); - this.webTemplateForm.get('additionalConfig.actionButtonConfig.dashboardState').disable({emitEvent: false}); - this.webTemplateForm.get('additionalConfig.actionButtonConfig.setEntityIdInState').disable({emitEvent: false}); - } else { - this.webTemplateForm.get('additionalConfig.actionButtonConfig.link').disable({emitEvent: false}); - this.webTemplateForm.get('additionalConfig.actionButtonConfig.dashboardId').enable({emitEvent: false}); - this.webTemplateForm.get('additionalConfig.actionButtonConfig.dashboardState').enable({emitEvent: false}); - this.webTemplateForm.get('additionalConfig.actionButtonConfig.setEntityIdInState').enable({emitEvent: false}); - } - } - }); - this.emailTemplateForm = this.fb.group({ subject: ['', Validators.required], body: ['', Validators.required] @@ -170,22 +131,7 @@ export abstract class TemplateConfiguration extends DialogComponent< subject: [''], body: ['', Validators.required], themeColor: [''], - button: this.fb.group({ - enabled: [false], - name: [{value: '', disabled: true}, [Validators.required, Validators.maxLength(50)]], - uri: [{value: '', disabled: true}, Validators.required], - }), - }); - - this.microsoftTeamsTemplateForm.get('button.enabled').valueChanges.pipe( - takeUntil(this.destroy$) - ).subscribe((value) => { - if (value) { - this.microsoftTeamsTemplateForm.get('button').enable({emitEvent: false}); - } else { - this.microsoftTeamsTemplateForm.get('button').disable({emitEvent: false}); - this.microsoftTeamsTemplateForm.get('button.enabled').enable({emitEvent: false}); - } + button: this.createButtonConfigForm() }); this.deliveryMethodFormsMap = new Map([ @@ -224,4 +170,48 @@ export abstract class TemplateConfiguration extends DialogComponent< }); return deepTrim(template); } + + private createButtonConfigForm(): FormGroup { + const form = this.fb.group({ + enabled: [false], + text: [{value: '', disabled: true}, [Validators.required, Validators.maxLength(50)]], + linkType: [ActionButtonLinkType.LINK], + link: [{value: '', disabled: true}, Validators.required], + dashboardId: [{value: null, disabled: true}, Validators.required], + dashboardState: [{value: null, disabled: true}], + setEntityIdInState: [{value: true, disabled: true}], + }); + + form.get('enabled').valueChanges.pipe( + takeUntil(this.destroy$) + ).subscribe((value) => { + if (value) { + form.enable({emitEvent: false}); + form.get('linkType').updateValueAndValidity({onlySelf: true}); + } else { + form.disable({emitEvent: false}); + form.get('enabled').enable({emitEvent: false}); + } + }); + + form.get('linkType').valueChanges.pipe( + takeUntil(this.destroy$) + ).subscribe((value) => { + const isEnabled = form.get('enabled').value; + if (isEnabled) { + if (value === ActionButtonLinkType.LINK) { + form.get('link').enable({emitEvent: false}); + form.get('dashboardId').disable({emitEvent: false}); + form.get('dashboardState').disable({emitEvent: false}); + form.get('setEntityIdInState').disable({emitEvent: false}); + } else { + form.get('link').disable({emitEvent: false}); + form.get('dashboardId').enable({emitEvent: false}); + form.get('dashboardState').enable({emitEvent: false}); + form.get('setEntityIdInState').enable({emitEvent: false}); + } + } + }); + return form; + } } diff --git a/ui-ngx/src/app/modules/home/pages/notification/template/template-notification-dialog.component.html b/ui-ngx/src/app/modules/home/pages/notification/template/template-notification-dialog.component.html index 6d27ef4980..81ca756263 100644 --- a/ui-ngx/src/app/modules/home/pages/notification/template/template-notification-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/notification/template/template-notification-dialog.component.html @@ -310,28 +310,54 @@ - +
notification.button-text - - + + {{ 'notification.button-text-required' | translate }} - + {{ 'notification.button-text-max-length' | translate : - {length: microsoftTeamsTemplateForm.get('button.name').getError('maxlength').requiredLength} + {length: microsoftTeamsTemplateForm.get('button.text').getError('maxlength').requiredLength} }} - +
+
+ + notification.action-type + + + {{ actionButtonLinkTypeTranslateMap.get(actionButtonLinkType) | translate }} + + + + notification.link - - + + {{ 'notification.link-required' | translate }} + + + + + +
+ + {{ 'notification.set-entity-from-notification' | translate }} +
diff --git a/ui-ngx/src/app/modules/home/pages/notification/template/template-notification-dialog.component.scss b/ui-ngx/src/app/modules/home/pages/notification/template/template-notification-dialog.component.scss index 507054440d..cf41bae5d0 100644 --- a/ui-ngx/src/app/modules/home/pages/notification/template/template-notification-dialog.component.scss +++ b/ui-ngx/src/app/modules/home/pages/notification/template/template-notification-dialog.component.scss @@ -126,8 +126,12 @@ } } - .tb-form-panel .mat-expansion-panel.tb-settings > .mat-expansion-panel-content > .mat-expansion-panel-body { - gap: 0; + .tb-form-panel .mat-expansion-panel.tb-settings { + padding: 11px 16px; + + & > .mat-expansion-panel-content > .mat-expansion-panel-body { + gap: 0; + } } } } diff --git a/ui-ngx/src/app/shared/models/notification.models.ts b/ui-ngx/src/app/shared/models/notification.models.ts index 9414368029..047ec7b38d 100644 --- a/ui-ngx/src/app/shared/models/notification.models.ts +++ b/ui-ngx/src/app/shared/models/notification.models.ts @@ -328,15 +328,17 @@ interface WebDeliveryMethodAdditionalConfig { icon: string; color: string; }; - actionButtonConfig: { - enabled: boolean; - text: string; - linkType: ActionButtonLinkType; - link?: string; - dashboardId?: string; - dashboardState?: string; - setEntityIdInState?: boolean; - }; + actionButtonConfig: NotificationButtonConfig; +} + +interface NotificationButtonConfig { + enabled: boolean; + text: string; + linkType: ActionButtonLinkType; + link?: string; + dashboardId?: string; + dashboardState?: string; + setEntityIdInState?: boolean; } interface EmailDeliveryMethodNotificationTemplate { @@ -350,11 +352,7 @@ interface SlackDeliveryMethodNotificationTemplate { interface MicrosoftTeamsDeliveryMethodNotificationTemplate { subject?: string; - button: { - enabled: boolean; - name?: string; - uri?: string; - }; + button: NotificationButtonConfig; } export enum NotificationStatus { From 1a93db75e07258889195791c6eeeb8a3c4f76e40 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Fri, 4 Aug 2023 17:33:11 +0300 Subject: [PATCH 5/7] Improvements for Microsoft Teams notifications --- .../MicrosoftTeamsNotificationChannel.java | 58 ++++++++++++++----- .../system/DefaultSystemSecurityService.java | 2 +- .../notification/NotificationApiTest.java | 5 +- ...amsDeliveryMethodNotificationTemplate.java | 36 ++++++++---- 4 files changed, 73 insertions(+), 28 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/notification/channels/MicrosoftTeamsNotificationChannel.java b/application/src/main/java/org/thingsboard/server/service/notification/channels/MicrosoftTeamsNotificationChannel.java index c628bf11f6..ddb827c26e 100644 --- a/application/src/main/java/org/thingsboard/server/service/notification/channels/MicrosoftTeamsNotificationChannel.java +++ b/application/src/main/java/org/thingsboard/server/service/notification/channels/MicrosoftTeamsNotificationChannel.java @@ -17,28 +17,37 @@ package org.thingsboard.server.service.notification.channels; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.base.Strings; import lombok.Data; import lombok.RequiredArgsConstructor; import lombok.Setter; +import org.apache.commons.codec.binary.Base64; import org.apache.commons.lang3.StringUtils; import org.springframework.boot.web.client.RestTemplateBuilder; import org.springframework.stereotype.Component; import org.springframework.web.client.RestTemplate; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; +import org.thingsboard.server.common.data.notification.info.NotificationInfo; import org.thingsboard.server.common.data.notification.targets.MicrosoftTeamsNotificationTargetConfig; import org.thingsboard.server.common.data.notification.template.MicrosoftTeamsDeliveryMethodNotificationTemplate; +import org.thingsboard.server.common.data.notification.template.MicrosoftTeamsDeliveryMethodNotificationTemplate.Button.LinkType; import org.thingsboard.server.service.notification.NotificationProcessingContext; +import org.thingsboard.server.service.security.system.SystemSecurityService; import java.time.Duration; import java.time.temporal.ChronoUnit; import java.util.List; +import java.util.Optional; @Component @RequiredArgsConstructor public class MicrosoftTeamsNotificationChannel implements NotificationChannel { + private final SystemSecurityService systemSecurityService; + @Setter private RestTemplate restTemplate = new RestTemplateBuilder() .setConnectTimeout(Duration.of(15, ChronoUnit.SECONDS)) @@ -47,11 +56,6 @@ public class MicrosoftTeamsNotificationChannel implements NotificationChannel templatableValues = List.of( TemplatableValue.of(this::getBody, this::setBody), TemplatableValue.of(this::getSubject, this::setSubject), - TemplatableValue.of(() -> button != null ? button.getName() : null, - processed -> {if (button != null) button.setName(processed);}), - TemplatableValue.of(() -> button != null ? button.getUri() : null, - processed -> {if (button != null) button.setUri(processed);}), - TemplatableValue.of(this::getCustomMessageCardJson, this::setCustomMessageCardJson) + TemplatableValue.of(() -> button != null ? button.getText() : null, + processed -> { if (button != null) button.setText(processed); }), + TemplatableValue.of(() -> button != null ? button.getLink() : null, + processed -> { if (button != null) button.setLink(processed); }) ); public MicrosoftTeamsDeliveryMethodNotificationTemplate(MicrosoftTeamsDeliveryMethodNotificationTemplate other) { @@ -50,7 +48,6 @@ public class MicrosoftTeamsDeliveryMethodNotificationTemplate extends DeliveryMe this.subject = other.subject; this.themeColor = other.themeColor; this.button = other.button != null ? new Button(other.button) : null; - this.customMessageCardJson = other.customMessageCardJson; } @Override @@ -66,12 +63,27 @@ public class MicrosoftTeamsDeliveryMethodNotificationTemplate extends DeliveryMe @Data @NoArgsConstructor public static class Button { - private String name; - private String uri; + private boolean enabled; + private String text; + private LinkType linkType; + private String link; + + private UUID dashboardId; + private String dashboardState; + private boolean setEntityIdInState; public Button(Button other) { - this.name = other.name; - this.uri = other.uri; + this.enabled = other.enabled; + this.text = other.text; + this.linkType = other.linkType; + this.link = other.link; + this.dashboardId = other.dashboardId; + this.dashboardState = other.dashboardState; + this.setEntityIdInState = other.setEntityIdInState; + } + + public enum LinkType { + LINK, DASHBOARD } } From fb149b720c7564e9ba6c0624450134f2d3a6e6ff Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Mon, 7 Aug 2023 11:51:47 +0300 Subject: [PATCH 6/7] Minor improvements for Teams notifications --- .../channels/MicrosoftTeamsNotificationChannel.java | 6 +++++- .../security/system/DefaultSystemSecurityService.java | 2 +- .../server/service/notification/NotificationApiTest.java | 4 +++- ui-ngx/src/assets/locale/locale.constant-en_US.json | 5 ++--- 4 files changed, 11 insertions(+), 6 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/notification/channels/MicrosoftTeamsNotificationChannel.java b/application/src/main/java/org/thingsboard/server/service/notification/channels/MicrosoftTeamsNotificationChannel.java index ddb827c26e..8720505267 100644 --- a/application/src/main/java/org/thingsboard/server/service/notification/channels/MicrosoftTeamsNotificationChannel.java +++ b/application/src/main/java/org/thingsboard/server/service/notification/channels/MicrosoftTeamsNotificationChannel.java @@ -88,7 +88,11 @@ public class MicrosoftTeamsNotificationChannel implements NotificationChannel Date: Thu, 17 Aug 2023 12:02:10 +0300 Subject: [PATCH 7/7] Fix localization for microsoft-teams --- ui-ngx/src/assets/locale/locale.constant-en_US.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 562cc91926..8077350240 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -2966,8 +2966,8 @@ "email-preview": "Email notification preview", "slack": "Slack", "slack-preview": "Slack notification preview", - "microsoft-teams": "Microsoft teams", - "microsoft-teams-preview": "Microsoft teams notification preview", + "microsoft-teams": "Microsoft Teams", + "microsoft-teams-preview": "Microsoft Teams notification preview", "sms": "SMS", "sms-preview": "SMS notification preview", "web": "Web",