Browse Source

Merge pull request #8843 from thingsboard/feature/microsoft-teams-notifications

Notifications via Microsoft Teams
pull/9116/head
Andrew Shvayka 3 years ago
committed by GitHub
parent
commit
d9dee141c1
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 31
      application/src/main/java/org/thingsboard/server/controller/NotificationController.java
  2. 6
      application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationCenter.java
  3. 36
      application/src/main/java/org/thingsboard/server/service/notification/NotificationProcessingContext.java
  4. 194
      application/src/main/java/org/thingsboard/server/service/notification/channels/MicrosoftTeamsNotificationChannel.java
  5. 4
      application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java
  6. 78
      application/src/test/java/org/thingsboard/server/service/notification/NotificationApiTest.java
  7. 3
      common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationDeliveryMethod.java
  8. 48
      common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/MicrosoftTeamsNotificationTargetConfig.java
  9. 12
      common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/NotificationRecipient.java
  10. 3
      common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/NotificationTargetConfig.java
  11. 3
      common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/NotificationTargetType.java
  12. 12
      common/data/src/main/java/org/thingsboard/server/common/data/notification/template/DeliveryMethodNotificationTemplate.java
  13. 12
      common/data/src/main/java/org/thingsboard/server/common/data/notification/template/EmailDeliveryMethodNotificationTemplate.java
  14. 90
      common/data/src/main/java/org/thingsboard/server/common/data/notification/template/MicrosoftTeamsDeliveryMethodNotificationTemplate.java
  15. 6
      common/data/src/main/java/org/thingsboard/server/common/data/notification/template/SlackDeliveryMethodNotificationTemplate.java
  16. 6
      common/data/src/main/java/org/thingsboard/server/common/data/notification/template/SmsDeliveryMethodNotificationTemplate.java
  17. 46
      common/data/src/main/java/org/thingsboard/server/common/data/notification/template/TemplatableValue.java
  18. 15
      common/data/src/main/java/org/thingsboard/server/common/data/notification/template/WebDeliveryMethodNotificationTemplate.java
  19. 27
      ui-ngx/src/app/modules/home/pages/notification/recipient/recipient-notification-dialog.component.html
  20. 60
      ui-ngx/src/app/modules/home/pages/notification/recipient/recipient-notification-dialog.component.scss
  21. 9
      ui-ngx/src/app/modules/home/pages/notification/recipient/recipient-notification-dialog.component.ts
  22. 1
      ui-ngx/src/app/modules/home/pages/notification/rule/rule-notification-dialog.component.scss
  23. 1
      ui-ngx/src/app/modules/home/pages/notification/sent/sent-error-dialog.component.scss
  24. 255
      ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.component.html
  25. 104
      ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.component.scss
  26. 100
      ui-ngx/src/app/modules/home/pages/notification/template/template-configuration.ts
  27. 233
      ui-ngx/src/app/modules/home/pages/notification/template/template-notification-dialog.component.html
  28. 55
      ui-ngx/src/app/modules/home/pages/notification/template/template-notification-dialog.component.scss
  29. 55
      ui-ngx/src/app/shared/models/notification.models.ts
  30. 7
      ui-ngx/src/assets/locale/locale.constant-en_US.json

31
application/src/main/java/org/thingsboard/server/controller/NotificationController.java

@ -45,6 +45,7 @@ import org.thingsboard.server.common.data.notification.NotificationRequestInfo;
import org.thingsboard.server.common.data.notification.NotificationRequestPreview;
import org.thingsboard.server.common.data.notification.settings.NotificationSettings;
import org.thingsboard.server.common.data.notification.settings.UserNotificationSettings;
import org.thingsboard.server.common.data.notification.targets.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;
@ -295,15 +296,27 @@ public class NotificationController extends BaseController {
int recipientsCount;
List<NotificationRecipient> recipientsPart;
NotificationTargetType targetType = target.getConfiguration().getType();
if (targetType == NotificationTargetType.PLATFORM_USERS) {
PageData<User> recipients = notificationTargetService.findRecipientsForNotificationTargetConfig(user.getTenantId(),
(PlatformUsersNotificationTargetConfig) target.getConfiguration(), new PageLink(recipientsPreviewSize, 0, null,
SortOrder.BY_CREATED_TIME_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<User> recipients = notificationTargetService.findRecipientsForNotificationTargetConfig(user.getTenantId(),
(PlatformUsersNotificationTargetConfig) target.getConfiguration(), new PageLink(recipientsPreviewSize, 0, null,
SortOrder.BY_CREATED_TIME_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) {

6
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.settings.UserNotificationSettings;
import org.thingsboard.server.common.data.notification.targets.NotificationRecipient;
import org.thingsboard.server.common.data.notification.targets.NotificationTarget;
@ -211,6 +212,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();
}

36
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;
@ -28,10 +28,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;
@ -39,8 +37,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 {
@ -90,12 +86,11 @@ public class NotificationProcessingContext {
public <T extends DeliveryMethodNotificationTemplate> T getProcessedTemplate(NotificationDeliveryMethod deliveryMethod, NotificationRecipient recipient) {
T template = (T) templates.get(deliveryMethod);
Map<String, String> additionalTemplateContext = null;
if (recipient != null) {
additionalTemplateContext = createTemplateContextForRecipient(recipient);
}
if (MapUtils.isNotEmpty(additionalTemplateContext) && template.containsAny(additionalTemplateContext.keySet().toArray(String[]::new))) {
template = processTemplate(template, additionalTemplateContext);
Map<String, String> additionalTemplateContext = createTemplateContextForRecipient(recipient);
if (template.getTemplatableValues().stream().anyMatch(value -> value.containsParams(additionalTemplateContext.keySet()))) {
template = processTemplate(template, additionalTemplateContext);
}
}
return template;
}
@ -111,22 +106,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;
}

194
application/src/main/java/org/thingsboard/server/service/notification/channels/MicrosoftTeamsNotificationChannel.java

@ -0,0 +1,194 @@
/**
* 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;
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<MicrosoftTeamsNotificationTargetConfig, MicrosoftTeamsDeliveryMethodNotificationTemplate> {
private final SystemSecurityService systemSecurityService;
@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 {
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));
}
var button = processedTemplate.getButton();
if (button != null && button.isEnabled()) {
String uri;
if (button.getLinkType() == LinkType.DASHBOARD) {
String state = null;
if (button.isSetEntityIdInState() || StringUtils.isNotEmpty(button.getDashboardState())) {
ObjectNode stateObject = JacksonUtil.newObjectNode();
if (button.isSetEntityIdInState()) {
stateObject.putObject("params")
.set("entityId", Optional.ofNullable(ctx.getRequest().getInfo())
.map(NotificationInfo::getStateEntityId)
.map(JacksonUtil::valueToTree)
.orElse(null));
} else {
stateObject.putObject("params");
}
if (StringUtils.isNotEmpty(button.getDashboardState())) {
stateObject.put("id", button.getDashboardState());
}
state = Base64.encodeBase64String(JacksonUtil.OBJECT_MAPPER.writeValueAsBytes(List.of(stateObject)));
}
String baseUrl = systemSecurityService.getBaseUrl(ctx.getTenantId(), null, null);
if (StringUtils.isEmpty(baseUrl)) {
throw new IllegalStateException("Failed to determine base url to construct dashboard link");
}
uri = baseUrl + "/dashboards/" + button.getDashboardId();
if (state != null) {
uri += "?state=" + state;
}
} else {
uri = button.getLink();
}
if (StringUtils.isNotBlank(uri) && button.getText() != null) {
Message.ActionCard actionCard = new Message.ActionCard();
actionCard.setType("OpenUri");
actionCard.setName(button.getText());
var target = new Message.ActionCard.Target("default", uri);
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<Section> sections;
private List<ActionCard> potentialAction;
@Data
public static class Section {
private String activityTitle;
private String activitySubtitle;
private String activityImage;
private List<Fact> 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<Input> inputs; // for ActionCard
private List<Action> actions; // for ActionCard
private List<Target> 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;
}
}
}
}

4
application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java

@ -249,11 +249,11 @@ public class DefaultSystemSecurityService implements SystemSecurityService {
JsonNode prohibitDifferentUrl = generalSettings.getJsonValue().get("prohibitDifferentUrl");
if (prohibitDifferentUrl != null && prohibitDifferentUrl.asBoolean()) {
if ((prohibitDifferentUrl != null && prohibitDifferentUrl.asBoolean()) || httpServletRequest == null) {
baseUrl = generalSettings.getJsonValue().get("baseUrl").asText();
}
if (StringUtils.isEmpty(baseUrl)) {
if (StringUtils.isEmpty(baseUrl) && httpServletRequest != null) {
baseUrl = MiscUtils.constructBaseUrl(httpServletRequest);
}

78
application/src/test/java/org/thingsboard/server/service/notification/NotificationApiTest.java

@ -21,9 +21,13 @@ 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.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.NotificationRuleId;
import org.thingsboard.server.common.data.id.NotificationTargetId;
import org.thingsboard.server.common.data.notification.Notification;
@ -35,9 +39,11 @@ 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.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.settings.UserNotificationSettings;
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;
@ -47,6 +53,8 @@ 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.MicrosoftTeamsDeliveryMethodNotificationTemplate.Button.LinkType;
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;
@ -56,6 +64,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;
@ -71,6 +80,8 @@ import static org.awaitility.Awaitility.await;
import static org.mockito.ArgumentMatchers.any;
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;
@DaoSqlTest
@ -83,6 +94,8 @@ public class NotificationApiTest extends AbstractNotificationApiTest {
private NotificationDao notificationDao;
@Autowired
private DbCallbackExecutorService executor;
@Autowired
private MicrosoftTeamsNotificationChannel microsoftTeamsNotificationChannel;
@Before
public void beforeEach() throws Exception {
@ -588,6 +601,71 @@ 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);
String templateParams = "${recipientTitle} - ${entityType}";
template.setSubject("Subject: " + templateParams);
template.setBody("Body: " + templateParams);
template.setThemeColor("ff0000");
var button = new MicrosoftTeamsDeliveryMethodNotificationTemplate.Button();
button.setEnabled(true);
button.setText("Button: " + templateParams);
button.setLinkType(LinkType.LINK);
button.setLink("https://" + templateParams);
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()))
.info(EntityActionNotificationInfo.builder()
.entityId(new DeviceId(UUID.randomUUID()))
.actionType(ActionType.ADDED)
.userId(tenantAdminUserId.getId())
.build())
.build();
NotificationRequestPreview preview = doPost("/api/notification/request/preview", notificationRequest, NotificationRequestPreview.class);
assertThat(preview.getRecipientsCountByTarget().get(target.getName())).isEqualTo(1);
assertThat(preview.getRecipientsPreview()).containsOnly(targetConfig.getChannelName());
var messageCaptor = ArgumentCaptor.forClass(MicrosoftTeamsNotificationChannel.Message.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("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 NotificationRequestStats submitNotificationRequestAndWait(NotificationRequest notificationRequest) throws Exception {
SettableFuture<NotificationRequestStats> future = SettableFuture.create();
notificationCenter.processNotificationRequest(notificationRequest.getTenantId(), notificationRequest, future::set);

3
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;

48
common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/MicrosoftTeamsNotificationTargetConfig.java

@ -0,0 +1,48 @@
/**
* 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;
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;
}
}

12
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;
}
}

3
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 {

3
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<NotificationDeliveryMethod> supportedDeliveryMethods;

12
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<TemplatableValue> getTemplatableValues();
}

12
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<TemplatableValue> 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);
}
}

90
common/data/src/main/java/org/thingsboard/server/common/data/notification/template/MicrosoftTeamsDeliveryMethodNotificationTemplate.java

@ -0,0 +1,90 @@
/**
* 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.thingsboard.server.common.data.notification.NotificationDeliveryMethod;
import java.util.List;
import java.util.UUID;
@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 final List<TemplatableValue> templatableValues = List.of(
TemplatableValue.of(this::getBody, this::setBody),
TemplatableValue.of(this::getSubject, this::setSubject),
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) {
super(other);
this.subject = other.subject;
this.themeColor = other.themeColor;
this.button = other.button != null ? new Button(other.button) : null;
}
@Override
public NotificationDeliveryMethod getMethod() {
return NotificationDeliveryMethod.MICROSOFT_TEAMS;
}
@Override
public MicrosoftTeamsDeliveryMethodNotificationTemplate copy() {
return new MicrosoftTeamsDeliveryMethodNotificationTemplate(this);
}
@Data
@NoArgsConstructor
public static class Button {
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.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
}
}
}

6
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<TemplatableValue> templatableValues = List.of(
TemplatableValue.of(this::getBody, this::setBody)
);
public SlackDeliveryMethodNotificationTemplate(DeliveryMethodNotificationTemplate other) {
super(other);
}

6
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<TemplatableValue> templatableValues = List.of(
TemplatableValue.of(this::getBody, this::setBody)
);
public SmsDeliveryMethodNotificationTemplate(SmsDeliveryMethodNotificationTemplate other) {
super(other);
}

46
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<String> getter;
private final Consumer<String> setter;
public static TemplatableValue of(Supplier<String> getter, Consumer<String> setter) {
return new TemplatableValue(getter, setter);
}
public String get() {
return getter.get();
}
public void set(String processed) {
setter.accept(processed);
}
public boolean containsParams(Collection<String> params) {
return StringUtils.containsAny(get(), params.toArray(String[]::new));
}
}

15
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<TemplatableValue> 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);
}
}

27
ui-ngx/src/app/modules/home/pages/notification/recipient/recipient-notification-dialog.component.html

@ -61,12 +61,11 @@
<ng-container *ngSwitchCase="notificationTargetConfigType.TENANT_ADMINISTRATORS">
<section *ngIf="isSysAdmin()">
<div fxFlex fxLayoutAlign="center center">
<mat-button-toggle-group class="tb-notification-tenant-group"
style="width: 280px;"
formControlName="filterByTenants">
<mat-button-toggle fxFlex [value]=true>{{ 'tenant.tenant' | translate }}</mat-button-toggle>
<mat-button-toggle fxFlex [value]=false>{{ 'tenant-profile.tenant-profile' | translate }}</mat-button-toggle>
</mat-button-toggle-group>
<tb-toggle-select class="tb-notification-tenant-group" appearance="fill"
formControlName="filterByTenants">
<tb-toggle-option [value]="true">{{ 'tenant.tenant' | translate }}</tb-toggle-option>
<tb-toggle-option [value]="false">{{ 'tenant-profile.tenant-profile' | translate }}</tb-toggle-option>
</tb-toggle-select>
</div>
<ng-container *ngIf="targetNotificationForm.get('configuration.usersFilter.filterByTenants').value; else tenantProfiles">
<tb-entity-list
@ -121,6 +120,22 @@
[slackChanelType]="targetNotificationForm.get('configuration.conversationType').value">
</tb-slack-conversation-autocomplete>
</section>
<section *ngIf="targetNotificationForm.get('configuration.type').value === notificationTargetType.MICROSOFT_TEAMS">
<mat-form-field class="mat-block">
<mat-label translate>notification.webhook-url</mat-label>
<input matInput formControlName="webhookUrl">
<mat-error *ngIf="targetNotificationForm.get('configuration.webhookUrl').hasError('required')">
{{ 'notification.webhook-url-required' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field class="mat-block">
<mat-label translate>notification.channel-name</mat-label>
<input matInput formControlName="channelName">
<mat-error *ngIf="targetNotificationForm.get('configuration.channelName').hasError('required')">
{{ 'notification.channel-name-required' | translate }}
</mat-error>
</mat-form-field>
</section>
<mat-form-field class="mat-block">
<mat-label translate>notification.description</mat-label>
<input matInput formControlName="description">

60
ui-ngx/src/app/modules/home/pages/notification/recipient/recipient-notification-dialog.component.scss

@ -19,6 +19,7 @@
form.tb-dialog-container {
min-width: 600px;
max-height: 100vh;
color: rgba(0, 0, 0, 0.87);
}
.mat-dialog-content {
@ -29,6 +30,11 @@
display: block;
padding-bottom: 6px;
}
.tb-notification-tenant-group {
width: 280px;
margin-bottom: 16px;
}
}
:host ::ng-deep {
@ -51,58 +57,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;
}
}
}

9
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});
}

1
ui-ngx/src/app/modules/home/pages/notification/rule/rule-notification-dialog.component.scss

@ -96,6 +96,7 @@
flex-direction: column;
height: 100%;
padding: 0 !important;
color: rgba(0, 0, 0, 0.87);
.mat-stepper-horizontal {
display: flex;

1
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;

255
ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.component.html

@ -27,7 +27,6 @@
</mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
<div mat-dialog-content>
<mat-horizontal-stepper linear #createNotification
[labelPosition]="(stepperLabelPosition | async)"
@ -40,12 +39,11 @@
<ng-template matStepLabel>{{ 'notification.compose' | translate }}</ng-template>
<form [formGroup]="notificationRequestForm">
<div fxLayout="row" fxLayoutAlign="center">
<mat-button-toggle-group class="tb-notification-use-template-toggle-group"
style="width: 320px;"
formControlName="useTemplate">
<mat-button-toggle fxFlex [value]=false>{{ 'notification.start-from-scratch' | translate }}</mat-button-toggle>
<mat-button-toggle fxFlex [value]=true>{{ 'notification.use-template' | translate }}</mat-button-toggle>
</mat-button-toggle-group>
<tb-toggle-select class="tb-notification-use-template-toggle-group" appearance="fill"
formControlName="useTemplate">
<tb-toggle-option [value]="false">{{ 'notification.start-from-scratch' | translate }}</tb-toggle-option>
<tb-toggle-option [value]="true">{{ 'notification.use-template' | translate }}</tb-toggle-option>
</tb-toggle-select>
</div>
<div *ngIf="notificationRequestForm.get('useTemplate').value; else scratchTemplate">
<tb-template-autocomplete
@ -172,73 +170,82 @@
{{ 'notification.message-required' | translate }}
</mat-error>
</mat-form-field>
<section formGroupName="additionalConfig">
<section formGroupName="icon" class="additional-config-group">
<mat-slide-toggle formControlName="enabled" class="toggle">
<section formGroupName="additionalConfig" class="tb-form-panel no-padding no-border">
<div class="tb-form-row space-between" formGroupName="icon">
<mat-slide-toggle formControlName="enabled" class="mat-slide">
{{ 'icon.icon' | translate }}
</mat-slide-toggle>
<div *ngIf="webTemplateForm.get('additionalConfig.icon.enabled').value"
fxLayout="row" fxLayoutGap.gt-xs="8px" fxLayout.xs="column">
<tb-material-icon-select formControlName="icon" required fxFlex>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<tb-material-icon-select asBoxInput
[color]="webTemplateForm.get('additionalConfig.icon.color').value"
formControlName="icon">
</tb-material-icon-select>
<tb-color-input formControlName="color" fxFlex>
<tb-color-input asBoxInput
formControlName="color">
</tb-color-input>
</div>
</section>
<section formGroupName="actionButtonConfig" class="additional-config-group">
<mat-slide-toggle formControlName="enabled" class="toggle">
{{ 'notification.action-button' | translate }}
</mat-slide-toggle>
<div *ngIf="webTemplateForm.get('additionalConfig.actionButtonConfig.enabled').value">
<div fxLayout="row" fxLayoutGap.gt-xs="8px" fxLayout.xs="column">
<mat-form-field class="mat-block" fxFlex>
<mat-label translate>notification.button-text</mat-label>
<input matInput formControlName="text" required>
<mat-error *ngIf="webTemplateForm.get('additionalConfig.actionButtonConfig.text').hasError('required')">
{{ 'notification.button-text-required' | translate }}
</mat-error>
<mat-error *ngIf="webTemplateForm.get('additionalConfig.actionButtonConfig.text').hasError('maxlength')">
{{ 'notification.button-text-max-length' | translate :
{length: webTemplateForm.get('additionalConfig.actionButtonConfig.text').getError('maxlength').requiredLength}
}}
</mat-error>
</mat-form-field>
</div>
<div fxLayout="row" fxLayoutGap.gt-xs="8px" fxLayout.xs="column">
<mat-form-field fxFlex="30" fxFlex.xs="100">
<mat-label translate>notification.action-type</mat-label>
<mat-select formControlName="linkType">
<mat-option *ngFor="let actionButtonLinkType of actionButtonLinkTypes" [value]="actionButtonLinkType">
{{ actionButtonLinkTypeTranslateMap.get(actionButtonLinkType) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field fxFlex
*ngIf="webTemplateForm.get('additionalConfig.actionButtonConfig.linkType').value === actionButtonLinkType.LINK; else dashboardSelector">
<mat-label translate>notification.link</mat-label>
<input matInput formControlName="link" required>
<mat-error *ngIf="webTemplateForm.get('additionalConfig.actionButtonConfig.link').hasError('required')">
{{ 'notification.link-required' | translate }}
</mat-error>
</mat-form-field>
<ng-template #dashboardSelector>
<tb-dashboard-autocomplete
fxFlex="35" fxFlex.xs="100"
required
formControlName="dashboardId">
</tb-dashboard-autocomplete>
<tb-dashboard-state-autocomplete fxFlex="35" fxFlex.xs="100"
[dashboardId]="webTemplateForm.get('additionalConfig.actionButtonConfig.dashboardId').value"
formControlName="dashboardState">
</tb-dashboard-state-autocomplete>
</ng-template>
</div>
<mat-slide-toggle formControlName="setEntityIdInState" class="toggle"
*ngIf="webTemplateForm.get('additionalConfig.actionButtonConfig.linkType').value === actionButtonLinkType.DASHBOARD">
{{ 'notification.set-entity-from-notification' | translate }}
</mat-slide-toggle>
</div>
</section>
</div>
<div class="tb-form-panel tb-slide-toggle stroked" formGroupName="actionButtonConfig">
<mat-expansion-panel class="tb-settings" [expanded]="webTemplateForm.get('additionalConfig.actionButtonConfig.enabled').value">
<mat-expansion-panel-header fxLayout="row wrap" class="fill-width">
<mat-panel-title fxFlex="60">
<mat-slide-toggle class="mat-slide" formControlName="enabled" (click)="$event.stopPropagation()"
fxLayoutAlign="center">
{{ 'notification.action-button' | translate }}
</mat-slide-toggle>
</mat-panel-title>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent class="tb-extension-panel">
<div fxLayout="row" fxLayoutGap.gt-xs="8px" fxLayout.xs="column">
<mat-form-field class="mat-block" fxFlex>
<mat-label translate>notification.button-text</mat-label>
<input matInput formControlName="text" required>
<mat-error *ngIf="webTemplateForm.get('additionalConfig.actionButtonConfig.text').hasError('required')">
{{ 'notification.button-text-required' | translate }}
</mat-error>
<mat-error *ngIf="webTemplateForm.get('additionalConfig.actionButtonConfig.text').hasError('maxlength')">
{{ 'notification.button-text-max-length' | translate :
{length: webTemplateForm.get('additionalConfig.actionButtonConfig.text').getError('maxlength').requiredLength}
}}
</mat-error>
</mat-form-field>
</div>
<div fxLayout="row" fxLayoutGap.gt-xs="8px" fxLayout.xs="column">
<mat-form-field fxFlex="30" fxFlex.xs="100">
<mat-label translate>notification.action-type</mat-label>
<mat-select formControlName="linkType">
<mat-option *ngFor="let actionButtonLinkType of actionButtonLinkTypes" [value]="actionButtonLinkType">
{{ actionButtonLinkTypeTranslateMap.get(actionButtonLinkType) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field fxFlex
*ngIf="webTemplateForm.get('additionalConfig.actionButtonConfig.linkType').value === actionButtonLinkType.LINK; else dashboardSelector">
<mat-label translate>notification.link</mat-label>
<input matInput formControlName="link" required>
<mat-error *ngIf="webTemplateForm.get('additionalConfig.actionButtonConfig.link').hasError('required')">
{{ 'notification.link-required' | translate }}
</mat-error>
</mat-form-field>
<ng-template #dashboardSelector>
<tb-dashboard-autocomplete
fxFlex="35" fxFlex.xs="100"
required
formControlName="dashboardId">
</tb-dashboard-autocomplete>
<tb-dashboard-state-autocomplete fxFlex="35" fxFlex.xs="100"
[dashboardId]="webTemplateForm.get('additionalConfig.actionButtonConfig.dashboardId').value"
formControlName="dashboardState">
</tb-dashboard-state-autocomplete>
</ng-template>
</div>
<mat-slide-toggle formControlName="setEntityIdInState" class="toggle"
*ngIf="webTemplateForm.get('additionalConfig.actionButtonConfig.linkType').value === actionButtonLinkType.DASHBOARD">
{{ 'notification.set-entity-from-notification' | translate }}
</mat-slide-toggle>
</ng-template>
</mat-expansion-panel>
</div>
</section>
</form>
</mat-step>
@ -333,6 +340,104 @@
</mat-form-field>
</form>
</mat-step>
<mat-step *ngIf="!notificationRequestForm.get('useTemplate').value &&
notificationRequestForm.get('template.configuration.deliveryMethodsTemplates.MICROSOFT_TEAMS.enabled').value"
[stepControl]="microsoftTeamsTemplateForm">
<ng-template matStepLabel>{{ 'notification.delivery-method.microsoft-teams' | translate }}</ng-template>
<div class="tb-hint-available-params mat-body-2">
<span class="content">{{ 'notification.input-fields-support-templatization' | translate}}</span>
<span tb-help-popup="{{ notificationTemplateTypeTranslateMap.get(templateNotificationForm.get('notificationType').value).helpId }}"
tb-help-popup-placement="bottom"
trigger-style="letter-spacing:0.25px"
[tb-help-popup-style]="{maxWidth: '800px'}"
trigger-text="{{ 'notification.see-documentation' | translate }}"></span>
</div>
<form [formGroup]="microsoftTeamsTemplateForm">
<mat-form-field class="mat-block">
<mat-label translate>notification.subject</mat-label>
<input matInput formControlName="subject">
</mat-form-field>
<mat-form-field class="mat-block">
<mat-label translate>notification.message</mat-label>
<textarea matInput
cdkTextareaAutosize
cols="1"
cdkAutosizeMinRows="1"
formControlName="body">
</textarea>
<mat-error *ngIf="microsoftTeamsTemplateForm.get('body').hasError('required')">
{{ 'notification.message-required' | translate }}
</mat-error>
</mat-form-field>
<div class="tb-form-panel no-padding no-border">
<div class="tb-form-row space-between">
<div translate>notification.theme-color</div>
<tb-color-input asBoxInput formControlName="themeColor"></tb-color-input>
</div>
<div class="tb-form-panel tb-slide-toggle stroked" formGroupName="button">
<mat-expansion-panel class="tb-settings" [expanded]="microsoftTeamsTemplateForm.get('button.enabled').value">
<mat-expansion-panel-header fxLayout="row wrap" class="fill-width">
<mat-panel-title fxFlex="60">
<mat-slide-toggle class="mat-slide" formControlName="enabled" (click)="$event.stopPropagation()"
fxLayoutAlign="center">
{{ 'notification.action-button' | translate }}
</mat-slide-toggle>
</mat-panel-title>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent class="tb-extension-panel">
<div fxLayout="row" fxLayoutGap.gt-xs="8px" fxLayout.xs="column">
<mat-form-field class="mat-block" fxFlex>
<mat-label translate>notification.button-text</mat-label>
<input matInput formControlName="text" required>
<mat-error *ngIf="microsoftTeamsTemplateForm.get('button.text').hasError('required')">
{{ 'notification.button-text-required' | translate }}
</mat-error>
<mat-error *ngIf="microsoftTeamsTemplateForm.get('button.text').hasError('maxlength')">
{{ 'notification.button-text-max-length' | translate :
{length: microsoftTeamsTemplateForm.get('button.text').getError('maxlength').requiredLength}
}}
</mat-error>
</mat-form-field>
</div>
<div fxLayout="row" fxLayoutGap.gt-xs="8px" fxLayout.xs="column">
<mat-form-field fxFlex="30" fxFlex.xs="100">
<mat-label translate>notification.action-type</mat-label>
<mat-select formControlName="linkType">
<mat-option *ngFor="let actionButtonLinkType of actionButtonLinkTypes" [value]="actionButtonLinkType">
{{ actionButtonLinkTypeTranslateMap.get(actionButtonLinkType) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field fxFlex
*ngIf="microsoftTeamsTemplateForm.get('button.linkType').value === actionButtonLinkType.LINK; else dashboardSelector">
<mat-label translate>notification.link</mat-label>
<input matInput formControlName="link" required>
<mat-error *ngIf="microsoftTeamsTemplateForm.get('button.link').hasError('required')">
{{ 'notification.link-required' | translate }}
</mat-error>
</mat-form-field>
<ng-template #dashboardSelector>
<tb-dashboard-autocomplete
fxFlex="35" fxFlex.xs="100"
required
formControlName="dashboardId">
</tb-dashboard-autocomplete>
<tb-dashboard-state-autocomplete fxFlex="35" fxFlex.xs="100"
[dashboardId]="microsoftTeamsTemplateForm.get('button.dashboardId').value"
formControlName="dashboardState">
</tb-dashboard-state-autocomplete>
</ng-template>
</div>
<mat-slide-toggle formControlName="setEntityIdInState" class="toggle"
*ngIf="microsoftTeamsTemplateForm.get('button.linkType').value === actionButtonLinkType.DASHBOARD">
{{ 'notification.set-entity-from-notification' | translate }}
</mat-slide-toggle>
</ng-template>
</mat-expansion-panel>
</div>
</div>
</form>
</mat-step>
<mat-step>
<ng-template matStepLabel>{{ 'notification.review' | translate }}</ng-template>
<mat-progress-spinner color="warn" mode="indeterminate"
@ -377,6 +482,16 @@
{{ preview.processedTemplates.SLACK.body }}
</div>
</section>
<section class="preview-group notification" *ngIf="preview.processedTemplates.MICROSOFT_TEAMS?.enabled">
<div fxLayout="row" fxLayoutGap="8px" fxLayoutAlign="start center">
<mat-icon class="tb-mat-18" svgIcon="mdi:microsoft-teams"></mat-icon>
<div class="group-title" translate>notification.delivery-method.microsoft-teams-preview</div>
</div>
<div class="notification-content mini">
<div class="subject">{{ preview.processedTemplates.MICROSOFT_TEAMS.subject }}</div>
{{ preview.processedTemplates.MICROSOFT_TEAMS.body }}
</div>
</section>
<section class="preview-group">
<div fxLayout="row" fxLayoutGap="8px" fxLayoutAlign="start center">
<mat-icon class="tb-mat-18">supervisor_account</mat-icon>
@ -399,7 +514,7 @@
</mat-horizontal-stepper>
</div>
<mat-divider></mat-divider>
<div mat-dialog-actions fxLayout="row">
<div mat-dialog-actions class="tb-dialog-actions">
<button mat-stroked-button *ngIf="selectedIndex > 0"
(click)="backStep()">{{ 'action.back' | translate }}</button>
<span fxFlex></span>

104
ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.component.scss

@ -15,13 +15,33 @@
*/
@import "../../../../../../scss/constants";
: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;
@ -75,6 +95,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;
@ -177,17 +198,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%;
@ -208,60 +240,14 @@
}
}
}
}
.mat-button-toggle-group.tb-notification-use-template-toggle-group {
&.mat-button-toggle-group-appearance-standard {
border: none;
border-radius: 18px;
margin-bottom: 24px;
.tb-form-panel .mat-expansion-panel.tb-settings {
padding: 11px 16px;
.mat-button-toggle + .mat-button-toggle {
border-left: none;
& > .mat-expansion-panel-content > .mat-expansion-panel-body {
gap: 0;
}
}
.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;
}
}
.preview-group {
@ -272,9 +258,7 @@
}
}
}
}
:host ::ng-deep {
.delivery-methods-container {
.delivery-method-container {
&.interact * {

100
ui-ngx/src/app/modules/home/pages/notification/template/template-configuration.ts

@ -43,6 +43,7 @@ export abstract class TemplateConfiguration<T, R = any> extends DialogComponent<
emailTemplateForm: FormGroup;
smsTemplateForm: FormGroup;
slackTemplateForm: FormGroup;
microsoftTeamsTemplateForm: FormGroup;
notificationDeliveryMethods = Object.keys(NotificationDeliveryMethod) as NotificationDeliveryMethod[];
notificationDeliveryMethodTranslateMap = NotificationDeliveryMethodTranslateMap;
@ -95,17 +96,9 @@ export abstract class TemplateConfiguration<T, R = any> extends DialogComponent<
icon: this.fb.group({
enabled: [false],
icon: [{value: 'notifications', disabled: true}, Validators.required],
color: ['#757575']
}),
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}],
color: [{value: '#757575', disabled: true}]
}),
actionButtonConfig: this.createButtonConfigForm()
})
});
@ -114,39 +107,10 @@ export abstract class TemplateConfiguration<T, R = any> 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.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.webTemplateForm.get('additionalConfig.icon.color').disable({emitEvent: false});
}
});
@ -163,11 +127,19 @@ export abstract class TemplateConfiguration<T, R = any> extends DialogComponent<
body: ['', Validators.required]
});
this.microsoftTeamsTemplateForm = this.fb.group({
subject: [''],
body: ['', Validators.required],
themeColor: [''],
button: this.createButtonConfigForm()
});
this.deliveryMethodFormsMap = new Map<NotificationDeliveryMethod, FormGroup>([
[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]
]);
}
@ -198,4 +170,48 @@ export abstract class TemplateConfiguration<T, R = any> 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;
}
}

233
ui-ngx/src/app/modules/home/pages/notification/template/template-notification-dialog.component.html

@ -27,7 +27,6 @@
</mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
<div mat-dialog-content>
<mat-horizontal-stepper linear #notificationTemplateStepper
[labelPosition]="(stepperLabelPosition | async)"
@ -101,73 +100,82 @@
{{ 'notification.message-required' | translate }}
</mat-error>
</mat-form-field>
<section formGroupName="additionalConfig">
<section formGroupName="icon" class="additional-config-group">
<mat-slide-toggle formControlName="enabled" class="toggle">
<section formGroupName="additionalConfig" class="tb-form-panel no-padding no-border">
<div class="tb-form-row space-between" formGroupName="icon">
<mat-slide-toggle formControlName="enabled" class="mat-slide">
{{ 'icon.icon' | translate }}
</mat-slide-toggle>
<div *ngIf="webTemplateForm.get('additionalConfig.icon.enabled').value"
fxLayout="row" fxLayoutGap.gt-xs="8px" fxLayout.xs="column">
<tb-material-icon-select formControlName="icon" required fxFlex>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<tb-material-icon-select asBoxInput
[color]="webTemplateForm.get('additionalConfig.icon.color').value"
formControlName="icon">
</tb-material-icon-select>
<tb-color-input formControlName="color" fxFlex>
<tb-color-input asBoxInput
formControlName="color">
</tb-color-input>
</div>
</section>
<section formGroupName="actionButtonConfig" class="additional-config-group">
<mat-slide-toggle formControlName="enabled" class="toggle">
{{ 'notification.action-button' | translate }}
</mat-slide-toggle>
<div *ngIf="webTemplateForm.get('additionalConfig.actionButtonConfig.enabled').value">
<div fxLayout="row" fxLayoutGap.gt-xs="8px" fxLayout.xs="column">
<mat-form-field class="mat-block" fxFlex>
<mat-label translate>notification.button-text</mat-label>
<input matInput formControlName="text" required>
<mat-error *ngIf="webTemplateForm.get('additionalConfig.actionButtonConfig.text').hasError('required')">
{{ 'notification.button-text-required' | translate }}
</mat-error>
<mat-error *ngIf="webTemplateForm.get('additionalConfig.actionButtonConfig.text').hasError('maxlength')">
{{ 'notification.button-text-max-length' | translate :
{length: webTemplateForm.get('additionalConfig.actionButtonConfig.text').getError('maxlength').requiredLength}
}}
</mat-error>
</mat-form-field>
</div>
<div fxLayout="row" fxLayoutGap.gt-xs="8px" fxLayout.xs="column">
<mat-form-field fxFlex="30" fxFlex.xs="100">
<mat-label translate>notification.action-type</mat-label>
<mat-select formControlName="linkType">
<mat-option *ngFor="let actionButtonLinkType of actionButtonLinkTypes" [value]="actionButtonLinkType">
{{ actionButtonLinkTypeTranslateMap.get(actionButtonLinkType) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field fxFlex
*ngIf="webTemplateForm.get('additionalConfig.actionButtonConfig.linkType').value === actionButtonLinkType.LINK; else dashboardSelector">
<mat-label translate>notification.link</mat-label>
<input matInput formControlName="link" required>
<mat-error *ngIf="webTemplateForm.get('additionalConfig.actionButtonConfig.link').hasError('required')">
{{ 'notification.link-required' | translate }}
</mat-error>
</mat-form-field>
<ng-template #dashboardSelector>
<tb-dashboard-autocomplete
fxFlex="35" fxFlex.xs="100"
required
formControlName="dashboardId">
</tb-dashboard-autocomplete>
<tb-dashboard-state-autocomplete fxFlex="35" fxFlex.xs="100"
[dashboardId]="webTemplateForm.get('additionalConfig.actionButtonConfig.dashboardId').value"
formControlName="dashboardState">
</tb-dashboard-state-autocomplete>
</ng-template>
</div>
<mat-slide-toggle formControlName="setEntityIdInState" class="toggle"
*ngIf="webTemplateForm.get('additionalConfig.actionButtonConfig.linkType').value === actionButtonLinkType.DASHBOARD">
{{ 'notification.set-entity-from-notification' | translate }}
</mat-slide-toggle>
</div>
</section>
</div>
<div class="tb-form-panel tb-slide-toggle stroked" formGroupName="actionButtonConfig">
<mat-expansion-panel class="tb-settings" [expanded]="webTemplateForm.get('additionalConfig.actionButtonConfig.enabled').value">
<mat-expansion-panel-header fxLayout="row wrap" class="fill-width">
<mat-panel-title fxFlex="60">
<mat-slide-toggle class="mat-slide" formControlName="enabled" (click)="$event.stopPropagation()"
fxLayoutAlign="center">
{{ 'notification.action-button' | translate }}
</mat-slide-toggle>
</mat-panel-title>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent class="tb-extension-panel">
<div fxLayout="row" fxLayoutGap.gt-xs="8px" fxLayout.xs="column">
<mat-form-field class="mat-block" fxFlex>
<mat-label translate>notification.button-text</mat-label>
<input matInput formControlName="text" required>
<mat-error *ngIf="webTemplateForm.get('additionalConfig.actionButtonConfig.text').hasError('required')">
{{ 'notification.button-text-required' | translate }}
</mat-error>
<mat-error *ngIf="webTemplateForm.get('additionalConfig.actionButtonConfig.text').hasError('maxlength')">
{{ 'notification.button-text-max-length' | translate :
{length: webTemplateForm.get('additionalConfig.actionButtonConfig.text').getError('maxlength').requiredLength}
}}
</mat-error>
</mat-form-field>
</div>
<div fxLayout="row" fxLayoutGap.gt-xs="8px" fxLayout.xs="column">
<mat-form-field fxFlex="30" fxFlex.xs="100">
<mat-label translate>notification.action-type</mat-label>
<mat-select formControlName="linkType">
<mat-option *ngFor="let actionButtonLinkType of actionButtonLinkTypes" [value]="actionButtonLinkType">
{{ actionButtonLinkTypeTranslateMap.get(actionButtonLinkType) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field fxFlex
*ngIf="webTemplateForm.get('additionalConfig.actionButtonConfig.linkType').value === actionButtonLinkType.LINK; else dashboardSelector">
<mat-label translate>notification.link</mat-label>
<input matInput formControlName="link" required>
<mat-error *ngIf="webTemplateForm.get('additionalConfig.actionButtonConfig.link').hasError('required')">
{{ 'notification.link-required' | translate }}
</mat-error>
</mat-form-field>
<ng-template #dashboardSelector>
<tb-dashboard-autocomplete
fxFlex="35" fxFlex.xs="100"
required
formControlName="dashboardId">
</tb-dashboard-autocomplete>
<tb-dashboard-state-autocomplete fxFlex="35" fxFlex.xs="100"
[dashboardId]="webTemplateForm.get('additionalConfig.actionButtonConfig.dashboardId').value"
formControlName="dashboardState">
</tb-dashboard-state-autocomplete>
</ng-template>
</div>
<mat-slide-toggle formControlName="setEntityIdInState" class="toggle"
*ngIf="webTemplateForm.get('additionalConfig.actionButtonConfig.linkType').value === actionButtonLinkType.DASHBOARD">
{{ 'notification.set-entity-from-notification' | translate }}
</mat-slide-toggle>
</ng-template>
</mat-expansion-panel>
</div>
</section>
</form>
</mat-step>
@ -259,10 +267,107 @@
</mat-form-field>
</form>
</mat-step>
<mat-step *ngIf="templateNotificationForm.get('configuration.deliveryMethodsTemplates.MICROSOFT_TEAMS.enabled').value"
[stepControl]="microsoftTeamsTemplateForm">
<ng-template matStepLabel>{{ 'notification.delivery-method.microsoft-teams' | translate }}</ng-template>
<div class="tb-hint-available-params mat-body-2">
<span class="content">{{ 'notification.input-fields-support-templatization' | translate}}</span>
<span tb-help-popup="{{ notificationTemplateTypeTranslateMap.get(templateNotificationForm.get('notificationType').value).helpId }}"
tb-help-popup-placement="bottom"
trigger-style="letter-spacing:0.25px"
[tb-help-popup-style]="{maxWidth: '800px'}"
trigger-text="{{ 'notification.see-documentation' | translate }}"></span>
</div>
<form [formGroup]="microsoftTeamsTemplateForm">
<mat-form-field class="mat-block">
<mat-label translate>notification.subject</mat-label>
<input matInput formControlName="subject">
</mat-form-field>
<mat-form-field class="mat-block">
<mat-label translate>notification.message</mat-label>
<textarea matInput
cdkTextareaAutosize
cols="1"
cdkAutosizeMinRows="1"
formControlName="body">
</textarea>
<mat-error *ngIf="microsoftTeamsTemplateForm.get('body').hasError('required')">
{{ 'notification.message-required' | translate }}
</mat-error>
</mat-form-field>
<div class="tb-form-panel no-padding no-border">
<div class="tb-form-row space-between">
<div translate>notification.theme-color</div>
<tb-color-input asBoxInput formControlName="themeColor"></tb-color-input>
</div>
<div class="tb-form-panel tb-slide-toggle stroked" formGroupName="button">
<mat-expansion-panel class="tb-settings" [expanded]="microsoftTeamsTemplateForm.get('button.enabled').value">
<mat-expansion-panel-header fxLayout="row wrap" class="fill-width">
<mat-panel-title fxFlex="60">
<mat-slide-toggle class="mat-slide" formControlName="enabled" (click)="$event.stopPropagation()"
fxLayoutAlign="center">
{{ 'notification.action-button' | translate }}
</mat-slide-toggle>
</mat-panel-title>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent class="tb-extension-panel">
<div fxLayout="row" fxLayoutGap.gt-xs="8px" fxLayout.xs="column">
<mat-form-field class="mat-block" fxFlex>
<mat-label translate>notification.button-text</mat-label>
<input matInput formControlName="text" required>
<mat-error *ngIf="microsoftTeamsTemplateForm.get('button.text').hasError('required')">
{{ 'notification.button-text-required' | translate }}
</mat-error>
<mat-error *ngIf="microsoftTeamsTemplateForm.get('button.text').hasError('maxlength')">
{{ 'notification.button-text-max-length' | translate :
{length: microsoftTeamsTemplateForm.get('button.text').getError('maxlength').requiredLength}
}}
</mat-error>
</mat-form-field>
</div>
<div fxLayout="row" fxLayoutGap.gt-xs="8px" fxLayout.xs="column">
<mat-form-field fxFlex="30" fxFlex.xs="100">
<mat-label translate>notification.action-type</mat-label>
<mat-select formControlName="linkType">
<mat-option *ngFor="let actionButtonLinkType of actionButtonLinkTypes" [value]="actionButtonLinkType">
{{ actionButtonLinkTypeTranslateMap.get(actionButtonLinkType) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field fxFlex
*ngIf="microsoftTeamsTemplateForm.get('button.linkType').value === actionButtonLinkType.LINK; else dashboardSelector">
<mat-label translate>notification.link</mat-label>
<input matInput formControlName="link" required>
<mat-error *ngIf="microsoftTeamsTemplateForm.get('button.link').hasError('required')">
{{ 'notification.link-required' | translate }}
</mat-error>
</mat-form-field>
<ng-template #dashboardSelector>
<tb-dashboard-autocomplete
fxFlex="35" fxFlex.xs="100"
required
formControlName="dashboardId">
</tb-dashboard-autocomplete>
<tb-dashboard-state-autocomplete fxFlex="35" fxFlex.xs="100"
[dashboardId]="microsoftTeamsTemplateForm.get('button.dashboardId').value"
formControlName="dashboardState">
</tb-dashboard-state-autocomplete>
</ng-template>
</div>
<mat-slide-toggle formControlName="setEntityIdInState" class="toggle"
*ngIf="microsoftTeamsTemplateForm.get('button.linkType').value === actionButtonLinkType.DASHBOARD">
{{ 'notification.set-entity-from-notification' | translate }}
</mat-slide-toggle>
</ng-template>
</mat-expansion-panel>
</div>
</div>
</form>
</mat-step>
</mat-horizontal-stepper>
</div>
<mat-divider></mat-divider>
<div mat-dialog-actions fxLayout="row">
<div mat-dialog-actions class="tb-dialog-actions">
<button mat-stroked-button *ngIf="selectedIndex > 0"
(click)="backStep()">{{ 'action.back' | translate }}</button>
<span fxFlex></span>

55
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,13 @@
}
}
}
.tb-form-panel .mat-expansion-panel.tb-settings {
padding: 11px 16px;
& > .mat-expansion-panel-content > .mat-expansion-panel-body {
gap: 0;
}
}
}
}

55
ui-ngx/src/app/shared/models/notification.models.ts

@ -246,7 +246,9 @@ export interface NotificationTarget extends Omit<BaseData<NotificationTargetId>,
configuration: NotificationTargetConfig;
}
export interface NotificationTargetConfig extends Partial<PlatformUsersNotificationTargetConfig & SlackNotificationTargetConfig> {
export interface NotificationTargetConfig extends Partial<PlatformUsersNotificationTargetConfig
& SlackNotificationTargetConfig
& MicrosoftTeamsNotificationTargetConfig> {
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, string>([
[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<BaseData<NotificationTemplateId>, 'label'>, ExportableEntity<NotificationTemplateId> {
@ -299,14 +308,17 @@ interface NotificationTemplateConfig {
}
export interface DeliveryMethodNotificationTemplate extends
Partial<WebDeliveryMethodNotificationTemplate & EmailDeliveryMethodNotificationTemplate & SlackDeliveryMethodNotificationTemplate>{
body?: string;
Partial<WebDeliveryMethodNotificationTemplate
& EmailDeliveryMethodNotificationTemplate
& SlackDeliveryMethodNotificationTemplate
& MicrosoftTeamsDeliveryMethodNotificationTemplate>{
body: string;
enabled: boolean;
method: NotificationDeliveryMethod;
}
interface WebDeliveryMethodNotificationTemplate {
subject?: string;
subject: string;
additionalConfig: WebDeliveryMethodAdditionalConfig;
}
@ -316,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 {
@ -336,6 +350,11 @@ interface SlackDeliveryMethodNotificationTemplate {
conversationId: string;
}
interface MicrosoftTeamsDeliveryMethodNotificationTemplate {
subject?: string;
button: NotificationButtonConfig;
}
export enum NotificationStatus {
SENT = 'SENT',
READ = 'READ'
@ -345,14 +364,16 @@ export enum NotificationDeliveryMethod {
WEB = 'WEB',
SMS = 'SMS',
EMAIL = 'EMAIL',
SLACK = 'SLACK'
SLACK = 'SLACK',
MICROSOFT_TEAMS = 'MICROSOFT_TEAMS'
}
export const NotificationDeliveryMethodTranslateMap = new Map<NotificationDeliveryMethod, string>([
[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 {

7
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -2967,6 +2967,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",
@ -3132,6 +3134,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": {
@ -3154,6 +3157,10 @@
"use-template": "Use template",
"view-all": "View all",
"warning": "Warning",
"webhook-url": "Webhook URL",
"webhook-url-required": "Webhook URL is required",
"channel-name": "Channel name",
"channel-name-required": "Channel name is required",
"settings": {
"notification-settings": "Notification settings",
"reset-all": "Reset all settings",

Loading…
Cancel
Save