Browse Source

Merge branch 'develop/3.5.2' of github.com:thingsboard/thingsboard into develop/3.5.2

pull/9116/head
Igor Kulikov 3 years ago
parent
commit
9ae7db87f4
  1. 31
      application/src/main/java/org/thingsboard/server/controller/NotificationController.java
  2. 14
      application/src/main/java/org/thingsboard/server/service/edge/DefaultEdgeNotificationService.java
  3. 16
      application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java
  4. 4
      application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java
  5. 2
      application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java
  6. 67
      application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/TenantMsgConstructor.java
  7. 48
      application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/TenantProfileMsgConstructor.java
  8. 51
      application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/TenantEdgeEventFetcher.java
  9. 12
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java
  10. 57
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/tenant/TenantEdgeProcessor.java
  11. 53
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/tenant/TenantProfileEdgeProcessor.java
  12. 2
      application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/DefaultEdgeRequestsService.java
  13. 1
      application/src/main/java/org/thingsboard/server/service/entitiy/tenant/profile/TbTenantProfileService.java
  14. 6
      application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationCenter.java
  15. 36
      application/src/main/java/org/thingsboard/server/service/notification/NotificationProcessingContext.java
  16. 194
      application/src/main/java/org/thingsboard/server/service/notification/channels/MicrosoftTeamsNotificationChannel.java
  17. 4
      application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java
  18. 41
      application/src/test/java/org/thingsboard/server/controller/EdgeControllerTest.java
  19. 34
      application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java
  20. 7
      application/src/test/java/org/thingsboard/server/edge/DeviceEdgeTest.java
  21. 90
      application/src/test/java/org/thingsboard/server/edge/TenantEdgeTest.java
  22. 54
      application/src/test/java/org/thingsboard/server/edge/TenantProfileEdgeTest.java
  23. 12
      application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java
  24. 78
      application/src/test/java/org/thingsboard/server/service/notification/NotificationApiTest.java
  25. 3
      common/dao-api/src/main/java/org/thingsboard/server/dao/edge/EdgeService.java
  26. 57
      common/data/src/main/java/org/thingsboard/server/common/data/EdgeUtils.java
  27. 2
      common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileData.java
  28. 46
      common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventType.java
  29. 14
      common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java
  30. 3
      common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationDeliveryMethod.java
  31. 48
      common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/MicrosoftTeamsNotificationTargetConfig.java
  32. 12
      common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/NotificationRecipient.java
  33. 3
      common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/NotificationTargetConfig.java
  34. 3
      common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/NotificationTargetType.java
  35. 12
      common/data/src/main/java/org/thingsboard/server/common/data/notification/template/DeliveryMethodNotificationTemplate.java
  36. 12
      common/data/src/main/java/org/thingsboard/server/common/data/notification/template/EmailDeliveryMethodNotificationTemplate.java
  37. 90
      common/data/src/main/java/org/thingsboard/server/common/data/notification/template/MicrosoftTeamsDeliveryMethodNotificationTemplate.java
  38. 6
      common/data/src/main/java/org/thingsboard/server/common/data/notification/template/SlackDeliveryMethodNotificationTemplate.java
  39. 6
      common/data/src/main/java/org/thingsboard/server/common/data/notification/template/SmsDeliveryMethodNotificationTemplate.java
  40. 46
      common/data/src/main/java/org/thingsboard/server/common/data/notification/template/TemplatableValue.java
  41. 15
      common/data/src/main/java/org/thingsboard/server/common/data/notification/template/WebDeliveryMethodNotificationTemplate.java
  42. 2
      common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java
  43. 4
      common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/TenantProfileConfiguration.java
  44. 5
      common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/TenantProfileData.java
  45. 7
      common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/TenantProfileQueueConfiguration.java
  46. 32
      common/edge-api/src/main/proto/edge.proto
  47. 10
      dao/src/main/java/org/thingsboard/server/dao/edge/EdgeDao.java
  48. 17
      dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java
  49. 4
      dao/src/main/java/org/thingsboard/server/dao/sql/edge/EdgeRepository.java
  50. 9
      dao/src/main/java/org/thingsboard/server/dao/sql/edge/JpaEdgeDao.java
  51. 5
      dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileServiceImpl.java
  52. 4
      dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java
  53. 2
      dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java
  54. 28
      dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java
  55. 2
      dao/src/test/java/org/thingsboard/server/dao/service/DeviceServiceTest.java
  56. 19
      dao/src/test/java/org/thingsboard/server/dao/service/EdgeServiceTest.java
  57. 2
      dao/src/test/java/org/thingsboard/server/dao/service/UserServiceTest.java
  58. 2
      rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js
  59. 27
      ui-ngx/src/app/modules/home/pages/notification/recipient/recipient-notification-dialog.component.html
  60. 60
      ui-ngx/src/app/modules/home/pages/notification/recipient/recipient-notification-dialog.component.scss
  61. 9
      ui-ngx/src/app/modules/home/pages/notification/recipient/recipient-notification-dialog.component.ts
  62. 1
      ui-ngx/src/app/modules/home/pages/notification/rule/rule-notification-dialog.component.scss
  63. 1
      ui-ngx/src/app/modules/home/pages/notification/sent/sent-error-dialog.component.scss
  64. 255
      ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.component.html
  65. 104
      ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.component.scss
  66. 100
      ui-ngx/src/app/modules/home/pages/notification/template/template-configuration.ts
  67. 233
      ui-ngx/src/app/modules/home/pages/notification/template/template-notification-dialog.component.html
  68. 55
      ui-ngx/src/app/modules/home/pages/notification/template/template-notification-dialog.component.scss
  69. 55
      ui-ngx/src/app/shared/models/notification.models.ts
  70. 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.NotificationRequestPreview;
import org.thingsboard.server.common.data.notification.settings.NotificationSettings; import org.thingsboard.server.common.data.notification.settings.NotificationSettings;
import org.thingsboard.server.common.data.notification.settings.UserNotificationSettings; import org.thingsboard.server.common.data.notification.settings.UserNotificationSettings;
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.NotificationRecipient;
import org.thingsboard.server.common.data.notification.targets.NotificationTarget; import org.thingsboard.server.common.data.notification.targets.NotificationTarget;
import org.thingsboard.server.common.data.notification.targets.NotificationTargetType; import org.thingsboard.server.common.data.notification.targets.NotificationTargetType;
@ -295,15 +296,27 @@ public class NotificationController extends BaseController {
int recipientsCount; int recipientsCount;
List<NotificationRecipient> recipientsPart; List<NotificationRecipient> recipientsPart;
NotificationTargetType targetType = target.getConfiguration().getType(); NotificationTargetType targetType = target.getConfiguration().getType();
if (targetType == NotificationTargetType.PLATFORM_USERS) { switch (targetType) {
PageData<User> recipients = notificationTargetService.findRecipientsForNotificationTargetConfig(user.getTenantId(), case PLATFORM_USERS: {
(PlatformUsersNotificationTargetConfig) target.getConfiguration(), new PageLink(recipientsPreviewSize, 0, null, PageData<User> recipients = notificationTargetService.findRecipientsForNotificationTargetConfig(user.getTenantId(),
SortOrder.BY_CREATED_TIME_DESC)); (PlatformUsersNotificationTargetConfig) target.getConfiguration(), new PageLink(recipientsPreviewSize, 0, null,
recipientsCount = (int) recipients.getTotalElements(); SortOrder.BY_CREATED_TIME_DESC));
recipientsPart = recipients.getData().stream().map(r -> (NotificationRecipient) r).collect(Collectors.toList()); recipientsCount = (int) recipients.getTotalElements();
} else { recipientsPart = recipients.getData().stream().map(r -> (NotificationRecipient) r).collect(Collectors.toList());
recipientsCount = 1; break;
recipientsPart = List.of(((SlackNotificationTargetConfig) target.getConfiguration()).getConversation()); }
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); firstRecipient.putIfAbsent(targetType, !recipientsPart.isEmpty() ? recipientsPart.get(0) : null);
for (NotificationRecipient recipient : recipientsPart) { for (NotificationRecipient recipient : recipientsPart) {

14
application/src/main/java/org/thingsboard/server/service/edge/DefaultEdgeNotificationService.java

@ -49,6 +49,8 @@ import org.thingsboard.server.service.edge.rpc.processor.ota.OtaPackageEdgeProce
import org.thingsboard.server.service.edge.rpc.processor.queue.QueueEdgeProcessor; import org.thingsboard.server.service.edge.rpc.processor.queue.QueueEdgeProcessor;
import org.thingsboard.server.service.edge.rpc.processor.relation.RelationEdgeProcessor; import org.thingsboard.server.service.edge.rpc.processor.relation.RelationEdgeProcessor;
import org.thingsboard.server.service.edge.rpc.processor.rule.RuleChainEdgeProcessor; import org.thingsboard.server.service.edge.rpc.processor.rule.RuleChainEdgeProcessor;
import org.thingsboard.server.service.edge.rpc.processor.tenant.TenantEdgeProcessor;
import org.thingsboard.server.service.edge.rpc.processor.tenant.TenantProfileEdgeProcessor;
import org.thingsboard.server.service.edge.rpc.processor.user.UserEdgeProcessor; import org.thingsboard.server.service.edge.rpc.processor.user.UserEdgeProcessor;
import org.thingsboard.server.service.edge.rpc.processor.widget.WidgetBundleEdgeProcessor; import org.thingsboard.server.service.edge.rpc.processor.widget.WidgetBundleEdgeProcessor;
import org.thingsboard.server.service.edge.rpc.processor.widget.WidgetTypeEdgeProcessor; import org.thingsboard.server.service.edge.rpc.processor.widget.WidgetTypeEdgeProcessor;
@ -111,6 +113,12 @@ public class DefaultEdgeNotificationService implements EdgeNotificationService {
@Autowired @Autowired
private QueueEdgeProcessor queueProcessor; private QueueEdgeProcessor queueProcessor;
@Autowired
private TenantEdgeProcessor tenantEdgeProcessor;
@Autowired
private TenantProfileEdgeProcessor tenantProfileEdgeProcessor;
@Autowired @Autowired
private AlarmEdgeProcessor alarmProcessor; private AlarmEdgeProcessor alarmProcessor;
@ -201,6 +209,12 @@ public class DefaultEdgeNotificationService implements EdgeNotificationService {
case RELATION: case RELATION:
future = relationProcessor.processRelationNotification(tenantId, edgeNotificationMsg); future = relationProcessor.processRelationNotification(tenantId, edgeNotificationMsg);
break; break;
case TENANT:
future = tenantEdgeProcessor.processEntityNotification(tenantId, edgeNotificationMsg);
break;
case TENANT_PROFILE:
future = tenantProfileEdgeProcessor.processEntityNotification(tenantId, edgeNotificationMsg);
break;
default: default:
log.warn("Edge event type [{}] is not designed to be pushed to edge", type); log.warn("Edge event type [{}] is not designed to be pushed to edge", type);
future = Futures.immediateFuture(null); future = Futures.immediateFuture(null);

16
application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java

@ -35,6 +35,8 @@ import org.thingsboard.server.dao.ota.OtaPackageService;
import org.thingsboard.server.dao.queue.QueueService; import org.thingsboard.server.dao.queue.QueueService;
import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.dao.settings.AdminSettingsService; import org.thingsboard.server.dao.settings.AdminSettingsService;
import org.thingsboard.server.dao.tenant.TenantProfileService;
import org.thingsboard.server.dao.tenant.TenantService;
import org.thingsboard.server.dao.user.UserService; import org.thingsboard.server.dao.user.UserService;
import org.thingsboard.server.dao.widget.WidgetsBundleService; import org.thingsboard.server.dao.widget.WidgetsBundleService;
import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.queue.util.TbCoreComponent;
@ -55,6 +57,8 @@ import org.thingsboard.server.service.edge.rpc.processor.relation.RelationEdgePr
import org.thingsboard.server.service.edge.rpc.processor.rule.RuleChainEdgeProcessor; import org.thingsboard.server.service.edge.rpc.processor.rule.RuleChainEdgeProcessor;
import org.thingsboard.server.service.edge.rpc.processor.settings.AdminSettingsEdgeProcessor; import org.thingsboard.server.service.edge.rpc.processor.settings.AdminSettingsEdgeProcessor;
import org.thingsboard.server.service.edge.rpc.processor.telemetry.TelemetryEdgeProcessor; import org.thingsboard.server.service.edge.rpc.processor.telemetry.TelemetryEdgeProcessor;
import org.thingsboard.server.service.edge.rpc.processor.tenant.TenantEdgeProcessor;
import org.thingsboard.server.service.edge.rpc.processor.tenant.TenantProfileEdgeProcessor;
import org.thingsboard.server.service.edge.rpc.processor.user.UserEdgeProcessor; import org.thingsboard.server.service.edge.rpc.processor.user.UserEdgeProcessor;
import org.thingsboard.server.service.edge.rpc.processor.widget.WidgetBundleEdgeProcessor; import org.thingsboard.server.service.edge.rpc.processor.widget.WidgetBundleEdgeProcessor;
import org.thingsboard.server.service.edge.rpc.processor.widget.WidgetTypeEdgeProcessor; import org.thingsboard.server.service.edge.rpc.processor.widget.WidgetTypeEdgeProcessor;
@ -122,6 +126,12 @@ public class EdgeContextComponent {
@Autowired @Autowired
private OtaPackageService otaPackageService; private OtaPackageService otaPackageService;
@Autowired
private TenantService tenantService;
@Autowired
private TenantProfileService tenantProfileService;
@Autowired @Autowired
private QueueService queueService; private QueueService queueService;
@ -179,6 +189,12 @@ public class EdgeContextComponent {
@Autowired @Autowired
private QueueEdgeProcessor queueEdgeProcessor; private QueueEdgeProcessor queueEdgeProcessor;
@Autowired
private TenantEdgeProcessor tenantEdgeProcessor;
@Autowired
private TenantProfileEdgeProcessor tenantProfileEdgeProcessor;
@Autowired @Autowired
private EdgeMsgConstructor edgeMsgConstructor; private EdgeMsgConstructor edgeMsgConstructor;

4
application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java

@ -645,6 +645,10 @@ public final class EdgeGrpcSession implements Closeable {
return ctx.getOtaPackageEdgeProcessor().convertOtaPackageEventToDownlink(edgeEvent); return ctx.getOtaPackageEdgeProcessor().convertOtaPackageEventToDownlink(edgeEvent);
case QUEUE: case QUEUE:
return ctx.getQueueEdgeProcessor().convertQueueEventToDownlink(edgeEvent); return ctx.getQueueEdgeProcessor().convertQueueEventToDownlink(edgeEvent);
case TENANT:
return ctx.getTenantEdgeProcessor().convertTenantEventToDownlink(edgeEvent);
case TENANT_PROFILE:
return ctx.getTenantProfileEdgeProcessor().convertTenantProfileEventToDownlink(edgeEvent);
default: default:
log.warn("Unsupported edge event type [{}]", edgeEvent); log.warn("Unsupported edge event type [{}]", edgeEvent);
return null; return null;

2
application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java

@ -34,6 +34,7 @@ import org.thingsboard.server.service.edge.rpc.fetch.QueuesEdgeEventFetcher;
import org.thingsboard.server.service.edge.rpc.fetch.RuleChainsEdgeEventFetcher; import org.thingsboard.server.service.edge.rpc.fetch.RuleChainsEdgeEventFetcher;
import org.thingsboard.server.service.edge.rpc.fetch.SystemWidgetsBundlesEdgeEventFetcher; import org.thingsboard.server.service.edge.rpc.fetch.SystemWidgetsBundlesEdgeEventFetcher;
import org.thingsboard.server.service.edge.rpc.fetch.TenantAdminUsersEdgeEventFetcher; import org.thingsboard.server.service.edge.rpc.fetch.TenantAdminUsersEdgeEventFetcher;
import org.thingsboard.server.service.edge.rpc.fetch.TenantEdgeEventFetcher;
import org.thingsboard.server.service.edge.rpc.fetch.TenantWidgetsBundlesEdgeEventFetcher; import org.thingsboard.server.service.edge.rpc.fetch.TenantWidgetsBundlesEdgeEventFetcher;
import java.util.LinkedList; import java.util.LinkedList;
@ -51,6 +52,7 @@ public class EdgeSyncCursor {
fetchers.add(new QueuesEdgeEventFetcher(ctx.getQueueService())); fetchers.add(new QueuesEdgeEventFetcher(ctx.getQueueService()));
fetchers.add(new RuleChainsEdgeEventFetcher(ctx.getRuleChainService())); fetchers.add(new RuleChainsEdgeEventFetcher(ctx.getRuleChainService()));
fetchers.add(new AdminSettingsEdgeEventFetcher(ctx.getAdminSettingsService(), ctx.getFreemarkerConfig())); fetchers.add(new AdminSettingsEdgeEventFetcher(ctx.getAdminSettingsService(), ctx.getFreemarkerConfig()));
fetchers.add(new TenantEdgeEventFetcher(ctx.getTenantService()));
fetchers.add(new TenantAdminUsersEdgeEventFetcher(ctx.getUserService())); fetchers.add(new TenantAdminUsersEdgeEventFetcher(ctx.getUserService()));
Customer publicCustomer = ctx.getCustomerService().findOrCreatePublicCustomer(edge.getTenantId()); Customer publicCustomer = ctx.getCustomerService().findOrCreatePublicCustomer(edge.getTenantId());
fetchers.add(new CustomerEdgeEventFetcher(publicCustomer.getId())); fetchers.add(new CustomerEdgeEventFetcher(publicCustomer.getId()));

67
application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/TenantMsgConstructor.java

@ -0,0 +1,67 @@
/**
* 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.edge.rpc.constructor;
import org.springframework.stereotype.Component;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.gen.edge.v1.TenantUpdateMsg;
import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
import org.thingsboard.server.queue.util.TbCoreComponent;
@Component
@TbCoreComponent
public class TenantMsgConstructor {
public TenantUpdateMsg constructTenantUpdateMsg(UpdateMsgType msgType, Tenant tenant) {
TenantUpdateMsg.Builder builder = TenantUpdateMsg.newBuilder()
.setMsgType(msgType)
.setIdMSB(tenant.getId().getId().getMostSignificantBits())
.setIdLSB(tenant.getId().getId().getLeastSignificantBits())
.setTitle(tenant.getTitle())
.setProfileIdMSB(tenant.getTenantProfileId().getId().getMostSignificantBits())
.setProfileIdLSB(tenant.getTenantProfileId().getId().getLeastSignificantBits())
.setRegion(tenant.getRegion());
if (tenant.getCountry() != null) {
builder.setCountry(tenant.getCountry());
}
if (tenant.getState() != null) {
builder.setState(tenant.getState());
}
if (tenant.getCity() != null) {
builder.setCity(tenant.getCity());
}
if (tenant.getAddress() != null) {
builder.setAddress(tenant.getAddress());
}
if (tenant.getAddress2() != null) {
builder.setAddress2(tenant.getAddress2());
}
if (tenant.getZip() != null) {
builder.setZip(tenant.getZip());
}
if (tenant.getPhone() != null) {
builder.setPhone(tenant.getPhone());
}
if (tenant.getEmail() != null) {
builder.setEmail(tenant.getEmail());
}
if (tenant.getAdditionalInfo() != null) {
builder.setAdditionalInfo(JacksonUtil.toString(tenant.getAdditionalInfo()));
}
return builder.build();
}
}

48
application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/TenantProfileMsgConstructor.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.service.edge.rpc.constructor;
import com.google.protobuf.ByteString;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.gen.edge.v1.TenantProfileUpdateMsg;
import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
import org.thingsboard.server.queue.util.DataDecodingEncodingService;
import org.thingsboard.server.queue.util.TbCoreComponent;
@Component
@TbCoreComponent
public class TenantProfileMsgConstructor {
@Autowired
private DataDecodingEncodingService dataDecodingEncodingService;
public TenantProfileUpdateMsg constructTenantProfileUpdateMsg(UpdateMsgType msgType, TenantProfile tenantProfile) {
TenantProfileUpdateMsg.Builder builder = TenantProfileUpdateMsg.newBuilder()
.setMsgType(msgType)
.setIdMSB(tenantProfile.getId().getId().getMostSignificantBits())
.setIdLSB(tenantProfile.getId().getId().getLeastSignificantBits())
.setName(tenantProfile.getName())
.setDefault(tenantProfile.isDefault())
.setIsolatedRuleChain(tenantProfile.isIsolatedTbRuleEngine())
.setProfileDataBytes(ByteString.copyFrom(dataDecodingEncodingService.encode(tenantProfile.getProfileData())));
if (tenantProfile.getDescription() != null) {
builder.setDescription(tenantProfile.getDescription());
}
return builder.build();
}
}

51
application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/TenantEdgeEventFetcher.java

@ -0,0 +1,51 @@
/**
* 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.edge.rpc.fetch;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.server.common.data.EdgeUtils;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.edge.EdgeEvent;
import org.thingsboard.server.common.data.edge.EdgeEventActionType;
import org.thingsboard.server.common.data.edge.EdgeEventType;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.dao.tenant.TenantService;
import java.util.List;
@AllArgsConstructor
@Slf4j
public class TenantEdgeEventFetcher extends BasePageableEdgeEventFetcher<Tenant> {
private final TenantService tenantService;
@Override
PageData<Tenant> fetchPageData(TenantId tenantId, Edge edge, PageLink pageLink) {
Tenant tenant = tenantService.findTenantById(tenantId);
// returns PageData object to be in sync with other fetchers
return new PageData<>(List.of(tenant), 1, 1, false);
}
@Override
EdgeEvent constructEdgeEvent(TenantId tenantId, Edge edge, Tenant entity) {
return EdgeUtils.constructEdgeEvent(tenantId, edge.getId(), EdgeEventType.TENANT,
EdgeEventActionType.UPDATED, entity.getId(), null);
}
}

12
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java

@ -71,6 +71,7 @@ import org.thingsboard.server.dao.queue.QueueService;
import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.DataValidator;
import org.thingsboard.server.dao.tenant.TenantProfileService;
import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.dao.tenant.TenantService;
import org.thingsboard.server.dao.user.UserService; import org.thingsboard.server.dao.user.UserService;
import org.thingsboard.server.dao.widget.WidgetTypeService; import org.thingsboard.server.dao.widget.WidgetTypeService;
@ -94,6 +95,8 @@ import org.thingsboard.server.service.edge.rpc.constructor.OtaPackageMsgConstruc
import org.thingsboard.server.service.edge.rpc.constructor.QueueMsgConstructor; import org.thingsboard.server.service.edge.rpc.constructor.QueueMsgConstructor;
import org.thingsboard.server.service.edge.rpc.constructor.RelationMsgConstructor; import org.thingsboard.server.service.edge.rpc.constructor.RelationMsgConstructor;
import org.thingsboard.server.service.edge.rpc.constructor.RuleChainMsgConstructor; import org.thingsboard.server.service.edge.rpc.constructor.RuleChainMsgConstructor;
import org.thingsboard.server.service.edge.rpc.constructor.TenantMsgConstructor;
import org.thingsboard.server.service.edge.rpc.constructor.TenantProfileMsgConstructor;
import org.thingsboard.server.service.edge.rpc.constructor.UserMsgConstructor; import org.thingsboard.server.service.edge.rpc.constructor.UserMsgConstructor;
import org.thingsboard.server.service.edge.rpc.constructor.WidgetTypeMsgConstructor; import org.thingsboard.server.service.edge.rpc.constructor.WidgetTypeMsgConstructor;
import org.thingsboard.server.service.edge.rpc.constructor.WidgetsBundleMsgConstructor; import org.thingsboard.server.service.edge.rpc.constructor.WidgetsBundleMsgConstructor;
@ -151,6 +154,9 @@ public abstract class BaseEdgeProcessor {
@Autowired @Autowired
protected TenantService tenantService; protected TenantService tenantService;
@Autowired
protected TenantProfileService tenantProfileService;
@Autowired @Autowired
protected EdgeService edgeService; protected EdgeService edgeService;
@ -260,6 +266,12 @@ public abstract class BaseEdgeProcessor {
@Autowired @Autowired
protected AssetProfileMsgConstructor assetProfileMsgConstructor; protected AssetProfileMsgConstructor assetProfileMsgConstructor;
@Autowired
protected TenantMsgConstructor tenantMsgConstructor;
@Autowired
protected TenantProfileMsgConstructor tenantProfileMsgConstructor;
@Autowired @Autowired
protected WidgetsBundleMsgConstructor widgetsBundleMsgConstructor; protected WidgetsBundleMsgConstructor widgetsBundleMsgConstructor;

57
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/tenant/TenantEdgeProcessor.java

@ -0,0 +1,57 @@
/**
* 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.edge.rpc.processor.tenant;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.EdgeUtils;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.edge.EdgeEvent;
import org.thingsboard.server.common.data.edge.EdgeEventActionType;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.gen.edge.v1.DownlinkMsg;
import org.thingsboard.server.gen.edge.v1.TenantProfileUpdateMsg;
import org.thingsboard.server.gen.edge.v1.TenantUpdateMsg;
import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor;
@Component
@Slf4j
@TbCoreComponent
public class TenantEdgeProcessor extends BaseEdgeProcessor {
public DownlinkMsg convertTenantEventToDownlink(EdgeEvent edgeEvent) {
TenantId tenantId = new TenantId(edgeEvent.getEntityId());
DownlinkMsg downlinkMsg = null;
if (EdgeEventActionType.UPDATED.equals(edgeEvent.getAction())) {
Tenant tenant = tenantService.findTenantById(tenantId);
if (tenant != null) {
UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction());
TenantUpdateMsg tenantUpdateMsg = tenantMsgConstructor.constructTenantUpdateMsg(msgType, tenant);
TenantProfile tenantProfile = tenantProfileService.findTenantProfileById(tenantId, tenant.getTenantProfileId());
TenantProfileUpdateMsg tenantProfileUpdateMsg = tenantProfileMsgConstructor.constructTenantProfileUpdateMsg(msgType, tenantProfile);
downlinkMsg = DownlinkMsg.newBuilder()
.setDownlinkMsgId(EdgeUtils.nextPositiveInt())
.addTenantUpdateMsg(tenantUpdateMsg)
.addTenantProfileUpdateMsg(tenantProfileUpdateMsg)
.build();
}
}
return downlinkMsg;
}
}

53
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/tenant/TenantProfileEdgeProcessor.java

@ -0,0 +1,53 @@
/**
* 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.edge.rpc.processor.tenant;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.EdgeUtils;
import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.edge.EdgeEvent;
import org.thingsboard.server.common.data.edge.EdgeEventActionType;
import org.thingsboard.server.common.data.id.TenantProfileId;
import org.thingsboard.server.gen.edge.v1.DownlinkMsg;
import org.thingsboard.server.gen.edge.v1.TenantProfileUpdateMsg;
import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor;
@Component
@Slf4j
@TbCoreComponent
public class TenantProfileEdgeProcessor extends BaseEdgeProcessor {
public DownlinkMsg convertTenantProfileEventToDownlink(EdgeEvent edgeEvent) {
TenantProfileId tenantProfileId = new TenantProfileId(edgeEvent.getEntityId());
DownlinkMsg downlinkMsg = null;
if (EdgeEventActionType.UPDATED.equals(edgeEvent.getAction())) {
TenantProfile tenantProfile = tenantProfileService.findTenantProfileById(edgeEvent.getTenantId(), tenantProfileId);
if (tenantProfile != null) {
UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction());
TenantProfileUpdateMsg tenantProfileUpdateMsg =
tenantProfileMsgConstructor.constructTenantProfileUpdateMsg(msgType, tenantProfile);
downlinkMsg = DownlinkMsg.newBuilder()
.setDownlinkMsgId(EdgeUtils.nextPositiveInt())
.addTenantProfileUpdateMsg(tenantProfileUpdateMsg)
.build();
}
}
return downlinkMsg;
}
}

2
application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/DefaultEdgeRequestsService.java

@ -167,6 +167,8 @@ public class DefaultEdgeRequestsService implements EdgeRequestsService {
attributes.put(attr.getKey(), attr.getDoubleValue().get()); attributes.put(attr.getKey(), attr.getDoubleValue().get());
} else if (attr.getDataType() == DataType.LONG && attr.getLongValue().isPresent()) { } else if (attr.getDataType() == DataType.LONG && attr.getLongValue().isPresent()) {
attributes.put(attr.getKey(), attr.getLongValue().get()); attributes.put(attr.getKey(), attr.getLongValue().get());
} else if (attr.getDataType() == DataType.JSON && attr.getJsonValue().isPresent()) {
attributes.set(attr.getKey(), JacksonUtil.toJsonNode(attr.getJsonValue().get()));
} else { } else {
attributes.put(attr.getKey(), attr.getValueAsString()); attributes.put(attr.getKey(), attr.getValueAsString());
} }

1
application/src/main/java/org/thingsboard/server/service/entitiy/tenant/profile/TbTenantProfileService.java

@ -20,6 +20,7 @@ import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
public interface TbTenantProfileService { public interface TbTenantProfileService {
TenantProfile save(TenantId tenantId, TenantProfile tenantProfile, TenantProfile oldTenantProfile) throws ThingsboardException; TenantProfile save(TenantId tenantId, TenantProfile tenantProfile, TenantProfile oldTenantProfile) throws ThingsboardException;
void delete(TenantId tenantId, TenantProfile tenantProfile) throws ThingsboardException; void delete(TenantId tenantId, TenantProfile tenantProfile) throws ThingsboardException;

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.NotificationStatus;
import org.thingsboard.server.common.data.notification.info.RuleOriginatedNotificationInfo; import org.thingsboard.server.common.data.notification.info.RuleOriginatedNotificationInfo;
import org.thingsboard.server.common.data.notification.settings.NotificationSettings; import org.thingsboard.server.common.data.notification.settings.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.settings.UserNotificationSettings;
import org.thingsboard.server.common.data.notification.targets.NotificationRecipient; 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.NotificationTarget;
@ -211,6 +212,11 @@ public class DefaultNotificationCenter extends AbstractSubscriptionService imple
recipients = List.of(targetConfig.getConversation()); recipients = List.of(targetConfig.getConversation());
break; break;
} }
case MICROSOFT_TEAMS: {
MicrosoftTeamsNotificationTargetConfig targetConfig = (MicrosoftTeamsNotificationTargetConfig) target.getConfiguration();
recipients = List.of(targetConfig);
break;
}
default: { default: {
recipients = Collections.emptyList(); 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 com.google.common.base.Strings;
import lombok.Builder; import lombok.Builder;
import lombok.Getter; 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.id.TenantId;
import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod;
import org.thingsboard.server.common.data.notification.NotificationRequest; 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.settings.NotificationSettings;
import org.thingsboard.server.common.data.notification.targets.NotificationRecipient; 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.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.NotificationTemplate;
import org.thingsboard.server.common.data.notification.template.NotificationTemplateConfig; 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 org.thingsboard.server.common.data.util.TemplateUtils;
import java.util.EnumMap; import java.util.EnumMap;
@ -39,8 +37,6 @@ import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import static org.apache.commons.lang3.StringUtils.isNotEmpty;
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public class NotificationProcessingContext { public class NotificationProcessingContext {
@ -90,12 +86,11 @@ public class NotificationProcessingContext {
public <T extends DeliveryMethodNotificationTemplate> T getProcessedTemplate(NotificationDeliveryMethod deliveryMethod, NotificationRecipient recipient) { public <T extends DeliveryMethodNotificationTemplate> T getProcessedTemplate(NotificationDeliveryMethod deliveryMethod, NotificationRecipient recipient) {
T template = (T) templates.get(deliveryMethod); T template = (T) templates.get(deliveryMethod);
Map<String, String> additionalTemplateContext = null;
if (recipient != null) { if (recipient != null) {
additionalTemplateContext = createTemplateContextForRecipient(recipient); Map<String, String> additionalTemplateContext = createTemplateContextForRecipient(recipient);
} if (template.getTemplatableValues().stream().anyMatch(value -> value.containsParams(additionalTemplateContext.keySet()))) {
if (MapUtils.isNotEmpty(additionalTemplateContext) && template.containsAny(additionalTemplateContext.keySet().toArray(String[]::new))) { template = processTemplate(template, additionalTemplateContext);
template = processTemplate(template, additionalTemplateContext); }
} }
return template; return template;
} }
@ -111,22 +106,13 @@ public class NotificationProcessingContext {
if (templateContext.isEmpty()) return template; if (templateContext.isEmpty()) return template;
template = (T) template.copy(); template = (T) template.copy();
template.setBody(TemplateUtils.processTemplate(template.getBody(), templateContext)); template.getTemplatableValues().forEach(templatableValue -> {
if (template instanceof HasSubject) { String value = templatableValue.get();
String subject = ((HasSubject) template).getSubject(); if (StringUtils.isNotEmpty(value)) {
((HasSubject) template).setSubject(TemplateUtils.processTemplate(subject, templateContext)); value = TemplateUtils.processTemplate(value, templateContext);
} templatableValue.set(value);
if (template instanceof WebDeliveryMethodNotificationTemplate) {
WebDeliveryMethodNotificationTemplate webNotificationTemplate = (WebDeliveryMethodNotificationTemplate) template;
String buttonText = webNotificationTemplate.getButtonText();
if (isNotEmpty(buttonText)) {
webNotificationTemplate.setButtonText(TemplateUtils.processTemplate(buttonText, templateContext));
} }
String buttonLink = webNotificationTemplate.getButtonLink(); });
if (isNotEmpty(buttonLink)) {
webNotificationTemplate.setButtonLink(TemplateUtils.processTemplate(buttonLink, templateContext));
}
}
return template; 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"); JsonNode prohibitDifferentUrl = generalSettings.getJsonValue().get("prohibitDifferentUrl");
if (prohibitDifferentUrl != null && prohibitDifferentUrl.asBoolean()) { if ((prohibitDifferentUrl != null && prohibitDifferentUrl.asBoolean()) || httpServletRequest == null) {
baseUrl = generalSettings.getJsonValue().get("baseUrl").asText(); baseUrl = generalSettings.getJsonValue().get("baseUrl").asText();
} }
if (StringUtils.isEmpty(baseUrl)) { if (StringUtils.isEmpty(baseUrl) && httpServletRequest != null) {
baseUrl = MiscUtils.constructBaseUrl(httpServletRequest); baseUrl = MiscUtils.constructBaseUrl(httpServletRequest);
} }

41
application/src/test/java/org/thingsboard/server/controller/EdgeControllerTest.java

@ -46,6 +46,8 @@ import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.TenantProfileId;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.Authority;
@ -63,6 +65,8 @@ import org.thingsboard.server.gen.edge.v1.DeviceUpdateMsg;
import org.thingsboard.server.gen.edge.v1.QueueUpdateMsg; import org.thingsboard.server.gen.edge.v1.QueueUpdateMsg;
import org.thingsboard.server.gen.edge.v1.RuleChainUpdateMsg; import org.thingsboard.server.gen.edge.v1.RuleChainUpdateMsg;
import org.thingsboard.server.gen.edge.v1.SyncCompletedMsg; import org.thingsboard.server.gen.edge.v1.SyncCompletedMsg;
import org.thingsboard.server.gen.edge.v1.TenantProfileUpdateMsg;
import org.thingsboard.server.gen.edge.v1.TenantUpdateMsg;
import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
import org.thingsboard.server.gen.edge.v1.UserCredentialsUpdateMsg; import org.thingsboard.server.gen.edge.v1.UserCredentialsUpdateMsg;
import org.thingsboard.server.gen.edge.v1.UserUpdateMsg; import org.thingsboard.server.gen.edge.v1.UserUpdateMsg;
@ -70,6 +74,7 @@ import org.thingsboard.server.gen.edge.v1.UserUpdateMsg;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
@ -855,7 +860,7 @@ public class EdgeControllerTest extends AbstractControllerTest {
EdgeImitator edgeImitator = new EdgeImitator(EDGE_HOST, EDGE_PORT, edge.getRoutingKey(), edge.getSecret()); EdgeImitator edgeImitator = new EdgeImitator(EDGE_HOST, EDGE_PORT, edge.getRoutingKey(), edge.getSecret());
edgeImitator.ignoreType(UserCredentialsUpdateMsg.class); edgeImitator.ignoreType(UserCredentialsUpdateMsg.class);
edgeImitator.expectMessageAmount(21); edgeImitator.expectMessageAmount(23);
edgeImitator.connect(); edgeImitator.connect();
assertThat(edgeImitator.waitForMessages()).as("await for messages on first connect").isTrue(); assertThat(edgeImitator.waitForMessages()).as("await for messages on first connect").isTrue();
@ -868,7 +873,7 @@ public class EdgeControllerTest extends AbstractControllerTest {
Assert.assertTrue(popAssetMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "Test Sync Edge Asset 1")); Assert.assertTrue(popAssetMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "Test Sync Edge Asset 1"));
Assert.assertTrue(edgeImitator.getDownlinkMsgs().isEmpty()); Assert.assertTrue(edgeImitator.getDownlinkMsgs().isEmpty());
edgeImitator.expectMessageAmount(16); edgeImitator.expectMessageAmount(18);
doPost("/api/edge/sync/" + edge.getId()); doPost("/api/edge/sync/" + edge.getId());
assertThat(edgeImitator.waitForMessages()).as("await for messages after edge sync rest api call").isTrue(); assertThat(edgeImitator.waitForMessages()).as("await for messages after edge sync rest api call").isTrue();
@ -905,6 +910,8 @@ public class EdgeControllerTest extends AbstractControllerTest {
Assert.assertTrue(popDeviceMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "Test Sync Edge Device 1")); Assert.assertTrue(popDeviceMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "Test Sync Edge Device 1"));
Assert.assertTrue(popAssetProfileMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "test")); Assert.assertTrue(popAssetProfileMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "test"));
Assert.assertTrue(popAssetMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "Test Sync Edge Asset 1")); Assert.assertTrue(popAssetMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "Test Sync Edge Asset 1"));
Assert.assertTrue(popTenantMsg(edgeImitator.getDownlinkMsgs(), tenantId));
Assert.assertTrue(popTenantProfileMsg(edgeImitator.getDownlinkMsgs(), tenantProfileId));
Assert.assertTrue(popSyncCompletedMsg(edgeImitator.getDownlinkMsgs())); Assert.assertTrue(popSyncCompletedMsg(edgeImitator.getDownlinkMsgs()));
} }
@ -1036,6 +1043,36 @@ public class EdgeControllerTest extends AbstractControllerTest {
return false; return false;
} }
private boolean popTenantMsg(List<AbstractMessage> messages, TenantId tenantId1) {
for (AbstractMessage message : messages) {
if (message instanceof TenantUpdateMsg) {
TenantUpdateMsg tenantUpdateMsg = (TenantUpdateMsg) message;
TenantId tenantIdMsg = new TenantId(new UUID(tenantUpdateMsg.getIdMSB(), tenantUpdateMsg.getIdLSB()));
if (UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE.equals(tenantUpdateMsg.getMsgType())
&& tenantId1.equals(tenantIdMsg)) {
messages.remove(message);
return true;
}
}
}
return false;
}
private boolean popTenantProfileMsg(List<AbstractMessage> messages, TenantProfileId tenantProfileId) {
for (AbstractMessage message : messages) {
if (message instanceof TenantProfileUpdateMsg) {
TenantProfileUpdateMsg tenantProfileUpdateMsg = (TenantProfileUpdateMsg) message;
TenantProfileId tenantProfileIdMsg = new TenantProfileId(new UUID(tenantProfileUpdateMsg.getIdMSB(), tenantProfileUpdateMsg.getIdLSB()));
if (UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE.equals(tenantProfileUpdateMsg.getMsgType())
&& tenantProfileId.equals(tenantProfileIdMsg)) {
messages.remove(message);
return true;
}
}
}
return false;
}
private boolean popSyncCompletedMsg(List<AbstractMessage> messages) { private boolean popSyncCompletedMsg(List<AbstractMessage> messages) {
for (AbstractMessage message : messages) { for (AbstractMessage message : messages) {
if (message instanceof SyncCompletedMsg) { if (message instanceof SyncCompletedMsg) {

34
application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java

@ -36,6 +36,7 @@ import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.OtaPackageInfo; import org.thingsboard.server.common.data.OtaPackageInfo;
import org.thingsboard.server.common.data.SaveOtaPackageInfoRequest; import org.thingsboard.server.common.data.SaveOtaPackageInfoRequest;
import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.alarm.AlarmSeverity;
import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.asset.Asset;
@ -84,6 +85,8 @@ import org.thingsboard.server.gen.edge.v1.RuleChainMetadataRequestMsg;
import org.thingsboard.server.gen.edge.v1.RuleChainMetadataUpdateMsg; import org.thingsboard.server.gen.edge.v1.RuleChainMetadataUpdateMsg;
import org.thingsboard.server.gen.edge.v1.RuleChainUpdateMsg; import org.thingsboard.server.gen.edge.v1.RuleChainUpdateMsg;
import org.thingsboard.server.gen.edge.v1.SyncCompletedMsg; import org.thingsboard.server.gen.edge.v1.SyncCompletedMsg;
import org.thingsboard.server.gen.edge.v1.TenantProfileUpdateMsg;
import org.thingsboard.server.gen.edge.v1.TenantUpdateMsg;
import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
import org.thingsboard.server.gen.edge.v1.UplinkMsg; import org.thingsboard.server.gen.edge.v1.UplinkMsg;
import org.thingsboard.server.gen.edge.v1.UserUpdateMsg; import org.thingsboard.server.gen.edge.v1.UserUpdateMsg;
@ -126,7 +129,7 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest {
installation(); installation();
edgeImitator = new EdgeImitator("localhost", 7070, edge.getRoutingKey(), edge.getSecret()); edgeImitator = new EdgeImitator("localhost", 7070, edge.getRoutingKey(), edge.getSecret());
edgeImitator.expectMessageAmount(22); edgeImitator.expectMessageAmount(24);
edgeImitator.connect(); edgeImitator.connect();
requestEdgeRuleChainMetadata(); requestEdgeRuleChainMetadata();
@ -257,6 +260,12 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest {
// 1 message from user fetcher // 1 message from user fetcher
validateUsers(); validateUsers();
// 1 from tenant fetcher
validateTenant();
// 1 from tenant profile fetcher
validateTenantProfile();
// 1 message sync completed // 1 message sync completed
validateSyncCompleted(); validateSyncCompleted();
} }
@ -267,6 +276,29 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest {
testAutoGeneratedCodeByProtobuf(configuration); testAutoGeneratedCodeByProtobuf(configuration);
} }
private void validateTenant() throws Exception {
Optional<TenantUpdateMsg> tenantUpdateMsgOpt = edgeImitator.findMessageByType(TenantUpdateMsg.class);
Assert.assertTrue(tenantUpdateMsgOpt.isPresent());
TenantUpdateMsg tenantUpdateMsg = tenantUpdateMsgOpt.get();
Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, tenantUpdateMsg.getMsgType());
UUID tenantUUID = new UUID(tenantUpdateMsg.getIdMSB(), tenantUpdateMsg.getIdLSB());
Tenant tenant = doGet("/api/tenant/" + tenantUUID, Tenant.class);
Assert.assertNotNull(tenant);
testAutoGeneratedCodeByProtobuf(tenantUpdateMsg);
}
private void validateTenantProfile() throws Exception {
Optional<TenantProfileUpdateMsg> tenantProfileUpdateMsgOpt = edgeImitator.findMessageByType(TenantProfileUpdateMsg.class);
Assert.assertTrue(tenantProfileUpdateMsgOpt.isPresent());
TenantProfileUpdateMsg tenantProfileUpdateMsg = tenantProfileUpdateMsgOpt.get();
Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, tenantProfileUpdateMsg.getMsgType());
UUID tenantProfileUUID = new UUID(tenantProfileUpdateMsg.getIdMSB(), tenantProfileUpdateMsg.getIdLSB());
Tenant tenant = doGet("/api/tenant/" + tenantId.getId(), Tenant.class);
Assert.assertNotNull(tenant);
Assert.assertEquals(tenantProfileUUID, tenant.getTenantProfileId().getId());
testAutoGeneratedCodeByProtobuf(tenantProfileUpdateMsg);
}
private void validateDeviceProfiles() throws Exception { private void validateDeviceProfiles() throws Exception {
List<DeviceProfileUpdateMsg> deviceProfileUpdateMsgList = edgeImitator.findAllMessagesByType(DeviceProfileUpdateMsg.class); List<DeviceProfileUpdateMsg> deviceProfileUpdateMsgList = edgeImitator.findAllMessagesByType(DeviceProfileUpdateMsg.class);
// default msg device profile from fetcher // default msg device profile from fetcher

7
application/src/test/java/org/thingsboard/server/edge/DeviceEdgeTest.java

@ -384,8 +384,10 @@ public class DeviceEdgeTest extends AbstractEdgeTest {
"inactivityTimeout", "3600000"); "inactivityTimeout", "3600000");
sendAttributesRequestAndVerify(device, DataConstants.SHARED_SCOPE, "{\"key2\":\"value2\"}", sendAttributesRequestAndVerify(device, DataConstants.SHARED_SCOPE, "{\"key2\":\"value2\"}",
"key2", "value2"); "key2", "value2");
sendAttributesRequestAndVerify(device, DataConstants.SERVER_SCOPE, "{\"jsonKey\":{\"nestedJsonKey\":\"nestedJsonValue\"}}",
"jsonKey", "{\"nestedJsonKey\":\"nestedJsonValue\"}");
doDelete("/api/plugins/telemetry/DEVICE/" + device.getUuidId() + "/" + DataConstants.SERVER_SCOPE, "keys", "key1, inactivityTimeout"); doDelete("/api/plugins/telemetry/DEVICE/" + device.getUuidId() + "/" + DataConstants.SERVER_SCOPE, "keys", "key1, inactivityTimeout, jsonKey");
doDelete("/api/plugins/telemetry/DEVICE/" + device.getUuidId() + "/" + DataConstants.SHARED_SCOPE, "keys", "key2"); doDelete("/api/plugins/telemetry/DEVICE/" + device.getUuidId() + "/" + DataConstants.SHARED_SCOPE, "keys", "key2");
} }
@ -650,6 +652,9 @@ public class DeviceEdgeTest extends AbstractEdgeTest {
case LONG_V: case LONG_V:
Assert.assertEquals(Long.parseLong(expectedValue), keyValueProto.getLongV()); Assert.assertEquals(Long.parseLong(expectedValue), keyValueProto.getLongV());
break; break;
case JSON_V:
Assert.assertEquals(JacksonUtil.toJsonNode(expectedValue), JacksonUtil.toJsonNode(keyValueProto.getJsonV()));
break;
default: default:
Assert.fail("Unexpected data type: " + keyValueProto.getType()); Assert.fail("Unexpected data type: " + keyValueProto.getType());
} }

90
application/src/test/java/org/thingsboard/server/edge/TenantEdgeTest.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.edge;
import org.junit.Assert;
import org.junit.Test;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.id.TenantProfileId;
import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.gen.edge.v1.TenantProfileUpdateMsg;
import org.thingsboard.server.gen.edge.v1.TenantUpdateMsg;
import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
import java.util.Optional;
import java.util.UUID;
@DaoSqlTest
public class TenantEdgeTest extends AbstractEdgeTest {
@Test
public void testUpdateTenant() throws Exception {
loginSysAdmin();
// save current value into tmp to revert after test
Tenant savedTenant = doGet("/api/tenant/" + tenantId, Tenant.class);
// updated edge tenant
savedTenant.setTitle("Updated Title for Tenant Edge Test");
edgeImitator.expectMessageAmount(2); // expect tenant and tenant profile update msg
savedTenant = doPost("/api/tenant", savedTenant, Tenant.class);
Assert.assertTrue(edgeImitator.waitForMessages());
Optional<TenantUpdateMsg> tenantUpdateMsgOpt = edgeImitator.findMessageByType(TenantUpdateMsg.class);
Assert.assertTrue(tenantUpdateMsgOpt.isPresent());
TenantUpdateMsg tenantUpdateMsg = tenantUpdateMsgOpt.get();
Optional<TenantProfileUpdateMsg> tenantProfileUpdateMsgOpt = edgeImitator.findMessageByType(TenantProfileUpdateMsg.class);
Assert.assertTrue(tenantProfileUpdateMsgOpt.isPresent());
TenantProfileUpdateMsg tenantProfileUpdateMsg = tenantProfileUpdateMsgOpt.get();
Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, tenantUpdateMsg.getMsgType());
Assert.assertEquals(savedTenant.getUuidId().getMostSignificantBits(), tenantUpdateMsg.getIdMSB());
Assert.assertEquals(savedTenant.getUuidId().getLeastSignificantBits(), tenantUpdateMsg.getIdLSB());
Assert.assertEquals(savedTenant.getTitle(), tenantUpdateMsg.getTitle());
Assert.assertEquals("Updated Title for Tenant Edge Test", tenantUpdateMsg.getTitle());
Assert.assertEquals(savedTenant.getTenantProfileId(), new TenantProfileId
(new UUID(tenantUpdateMsg.getProfileIdMSB(), tenantUpdateMsg.getProfileIdLSB())));
Assert.assertEquals(savedTenant.getTenantProfileId(), new TenantProfileId
(new UUID(tenantProfileUpdateMsg.getIdMSB(), tenantProfileUpdateMsg.getIdLSB())));
//change tenant profile for tenant
TenantProfile tenantProfile = createTenantProfile();
savedTenant.setTenantProfileId(tenantProfile.getId());
edgeImitator.expectMessageAmount(2); // expect tenant and tenant profile update msg
savedTenant = doPost("/api/tenant", savedTenant, Tenant.class);
Assert.assertTrue(edgeImitator.waitForMessages());
tenantUpdateMsgOpt = edgeImitator.findMessageByType(TenantUpdateMsg.class);
Assert.assertTrue(tenantUpdateMsgOpt.isPresent());
tenantUpdateMsg = tenantUpdateMsgOpt.get();
tenantProfileUpdateMsgOpt = edgeImitator.findMessageByType(TenantProfileUpdateMsg.class);
Assert.assertTrue(tenantProfileUpdateMsgOpt.isPresent());
tenantProfileUpdateMsg = tenantProfileUpdateMsgOpt.get();
// tenant update
Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, tenantUpdateMsg.getMsgType());
Assert.assertEquals(savedTenant.getUuidId().getMostSignificantBits(), tenantUpdateMsg.getIdMSB());
Assert.assertEquals(savedTenant.getUuidId().getLeastSignificantBits(), tenantUpdateMsg.getIdLSB());
Assert.assertEquals(savedTenant.getTitle(), tenantUpdateMsg.getTitle());
Assert.assertEquals(savedTenant.getTenantProfileId(), new TenantProfileId
(new UUID(tenantUpdateMsg.getProfileIdMSB(), tenantUpdateMsg.getProfileIdLSB())));
Assert.assertEquals(savedTenant.getTenantProfileId(), new TenantProfileId
(new UUID(tenantProfileUpdateMsg.getIdMSB(), tenantProfileUpdateMsg.getIdLSB())));
}
private TenantProfile createTenantProfile() {
TenantProfile tenantProfile = new TenantProfile();
tenantProfile.setName("TestEdge tenant profile");
return doPost("/api/tenantProfile", tenantProfile, TenantProfile.class);
}
}

54
application/src/test/java/org/thingsboard/server/edge/TenantProfileEdgeTest.java

@ -0,0 +1,54 @@
/**
* 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.edge;
import com.google.protobuf.AbstractMessage;
import org.junit.Assert;
import org.junit.Test;
import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.gen.edge.v1.TenantProfileUpdateMsg;
import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
@DaoSqlTest
public class TenantProfileEdgeTest extends AbstractEdgeTest {
@Test
public void testTenantProfiles() throws Exception {
loginSysAdmin();
// save current values into tmp to revert after test
TenantProfile edgeTenantProfile = doGet("/api/tenantProfile/" + tenantProfileId.getId(), TenantProfile.class);
// updated edge tenant profile
edgeTenantProfile.setName("Tenant Profile Edge Test");
edgeTenantProfile.setDescription("Updated tenant profile Edge Test");
edgeImitator.expectMessageAmount(1);
edgeTenantProfile = doPost("/api/tenantProfile", edgeTenantProfile, TenantProfile.class);
Assert.assertTrue(edgeImitator.waitForMessages());
AbstractMessage latestMessage = edgeImitator.getLatestMessage();
Assert.assertTrue(latestMessage instanceof TenantProfileUpdateMsg);
TenantProfileUpdateMsg tenantProfileUpdateMsg = (TenantProfileUpdateMsg) latestMessage;
Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, tenantProfileUpdateMsg.getMsgType());
Assert.assertEquals(edgeTenantProfile.getUuidId().getMostSignificantBits(), tenantProfileUpdateMsg.getIdMSB());
Assert.assertEquals(edgeTenantProfile.getUuidId().getLeastSignificantBits(), tenantProfileUpdateMsg.getIdLSB());
Assert.assertEquals(edgeTenantProfile.getDescription(), tenantProfileUpdateMsg.getDescription());
Assert.assertEquals("Updated tenant profile Edge Test", tenantProfileUpdateMsg.getDescription());
Assert.assertEquals("Tenant Profile Edge Test", tenantProfileUpdateMsg.getName());
loginTenantAdmin();
}
}

12
application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java

@ -47,6 +47,8 @@ import org.thingsboard.server.gen.edge.v1.QueueUpdateMsg;
import org.thingsboard.server.gen.edge.v1.RelationUpdateMsg; import org.thingsboard.server.gen.edge.v1.RelationUpdateMsg;
import org.thingsboard.server.gen.edge.v1.RuleChainMetadataUpdateMsg; import org.thingsboard.server.gen.edge.v1.RuleChainMetadataUpdateMsg;
import org.thingsboard.server.gen.edge.v1.RuleChainUpdateMsg; import org.thingsboard.server.gen.edge.v1.RuleChainUpdateMsg;
import org.thingsboard.server.gen.edge.v1.TenantProfileUpdateMsg;
import org.thingsboard.server.gen.edge.v1.TenantUpdateMsg;
import org.thingsboard.server.gen.edge.v1.UplinkMsg; import org.thingsboard.server.gen.edge.v1.UplinkMsg;
import org.thingsboard.server.gen.edge.v1.UplinkResponseMsg; import org.thingsboard.server.gen.edge.v1.UplinkResponseMsg;
import org.thingsboard.server.gen.edge.v1.UserCredentialsUpdateMsg; import org.thingsboard.server.gen.edge.v1.UserCredentialsUpdateMsg;
@ -290,6 +292,16 @@ public class EdgeImitator {
result.add(saveDownlinkMsg(queueUpdateMsg)); result.add(saveDownlinkMsg(queueUpdateMsg));
} }
} }
if (downlinkMsg.getTenantUpdateMsgCount() > 0) {
for (TenantUpdateMsg tenantUpdateMsg : downlinkMsg.getTenantUpdateMsgList()) {
result.add(saveDownlinkMsg(tenantUpdateMsg));
}
}
if (downlinkMsg.getTenantProfileUpdateMsgCount() > 0) {
for (TenantProfileUpdateMsg tenantProfileUpdateMsg : downlinkMsg.getTenantProfileUpdateMsgList()) {
result.add(saveDownlinkMsg(tenantProfileUpdateMsg));
}
}
if (downlinkMsg.hasEdgeConfiguration()) { if (downlinkMsg.hasEdgeConfiguration()) {
result.add(saveDownlinkMsg(downlinkMsg.getEdgeConfiguration())); result.add(saveDownlinkMsg(downlinkMsg.getEdgeConfiguration()));
} }

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.java_websocket.client.WebSocketClient;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.client.RestTemplate;
import org.thingsboard.rule.engine.api.NotificationCenter; import org.thingsboard.rule.engine.api.NotificationCenter;
import org.thingsboard.server.common.data.User; 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.NotificationRuleId;
import org.thingsboard.server.common.data.id.NotificationTargetId; import org.thingsboard.server.common.data.id.NotificationTargetId;
import org.thingsboard.server.common.data.notification.Notification; 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.NotificationRequestStats;
import org.thingsboard.server.common.data.notification.NotificationRequestStatus; import org.thingsboard.server.common.data.notification.NotificationRequestStatus;
import org.thingsboard.server.common.data.notification.NotificationType; 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.NotificationSettings;
import org.thingsboard.server.common.data.notification.settings.SlackNotificationDeliveryMethodConfig; 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.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.NotificationTarget;
import org.thingsboard.server.common.data.notification.targets.platform.CustomerUsersFilter; import org.thingsboard.server.common.data.notification.targets.platform.CustomerUsersFilter;
import org.thingsboard.server.common.data.notification.targets.platform.PlatformUsersNotificationTargetConfig; 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.targets.slack.SlackNotificationTargetConfig;
import org.thingsboard.server.common.data.notification.template.DeliveryMethodNotificationTemplate; 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.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.NotificationTemplate;
import org.thingsboard.server.common.data.notification.template.NotificationTemplateConfig; import org.thingsboard.server.common.data.notification.template.NotificationTemplateConfig;
import org.thingsboard.server.common.data.notification.template.SlackDeliveryMethodNotificationTemplate; 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.notification.NotificationDao;
import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.service.executors.DbCallbackExecutorService; 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 org.thingsboard.server.service.ws.notification.cmd.UnreadNotificationsUpdate;
import java.util.ArrayList; 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.any;
import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doThrow; 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.verify;
@DaoSqlTest @DaoSqlTest
@ -83,6 +94,8 @@ public class NotificationApiTest extends AbstractNotificationApiTest {
private NotificationDao notificationDao; private NotificationDao notificationDao;
@Autowired @Autowired
private DbCallbackExecutorService executor; private DbCallbackExecutorService executor;
@Autowired
private MicrosoftTeamsNotificationChannel microsoftTeamsNotificationChannel;
@Before @Before
public void beforeEach() throws Exception { public void beforeEach() throws Exception {
@ -588,6 +601,71 @@ public class NotificationApiTest extends AbstractNotificationApiTest {
assertThat(stats.getErrors().get(NotificationDeliveryMethod.SLACK).values()).containsExactly(errorMessage); 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 { private NotificationRequestStats submitNotificationRequestAndWait(NotificationRequest notificationRequest) throws Exception {
SettableFuture<NotificationRequestStats> future = SettableFuture.create(); SettableFuture<NotificationRequestStats> future = SettableFuture.create();
notificationCenter.processNotificationRequest(notificationRequest.getTenantId(), notificationRequest, future::set); notificationCenter.processNotificationRequest(notificationRequest.getTenantId(), notificationRequest, future::set);

3
common/dao-api/src/main/java/org/thingsboard/server/dao/edge/EdgeService.java

@ -24,6 +24,7 @@ import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.TenantProfileId;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.dao.entity.EntityDaoService; import org.thingsboard.server.dao.entity.EntityDaoService;
@ -83,6 +84,8 @@ public interface EdgeService extends EntityDaoService {
PageData<Edge> findEdgesByTenantIdAndEntityId(TenantId tenantId, EntityId ruleChainId, PageLink pageLink); PageData<Edge> findEdgesByTenantIdAndEntityId(TenantId tenantId, EntityId ruleChainId, PageLink pageLink);
PageData<Edge> findEdgesByTenantProfileId(TenantProfileId tenantProfileId, PageLink pageLink);
List<EdgeId> findAllRelatedEdgeIds(TenantId tenantId, EntityId entityId); List<EdgeId> findAllRelatedEdgeIds(TenantId tenantId, EntityId entityId);
PageData<EdgeId> findRelatedEdgeIdsByEntityId(TenantId tenantId, EntityId entityId, PageLink pageLink); PageData<EdgeId> findRelatedEdgeIdsByEntityId(TenantId tenantId, EntityId entityId, PageLink pageLink);

57
common/data/src/main/java/org/thingsboard/server/common/data/EdgeUtils.java

@ -25,60 +25,35 @@ import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import java.util.EnumMap;
import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.ThreadLocalRandom;
@Slf4j @Slf4j
public final class EdgeUtils { public final class EdgeUtils {
private static final int STACK_TRACE_LIMIT = 10; private static final EnumMap<EntityType, EdgeEventType> entityTypeEdgeEventTypeEnumMap;
private EdgeUtils() {
}
public static EdgeEventType getEdgeEventTypeByEntityType(EntityType entityType) { static {
switch (entityType) { entityTypeEdgeEventTypeEnumMap = new EnumMap<>(EntityType.class);
case EDGE: for (EdgeEventType edgeEventType : EdgeEventType.values()) {
return EdgeEventType.EDGE; if (edgeEventType.getEntityType() != null) {
case DEVICE: entityTypeEdgeEventTypeEnumMap.put(edgeEventType.getEntityType(), edgeEventType);
return EdgeEventType.DEVICE; }
case DEVICE_PROFILE:
return EdgeEventType.DEVICE_PROFILE;
case ASSET:
return EdgeEventType.ASSET;
case ASSET_PROFILE:
return EdgeEventType.ASSET_PROFILE;
case ENTITY_VIEW:
return EdgeEventType.ENTITY_VIEW;
case DASHBOARD:
return EdgeEventType.DASHBOARD;
case USER:
return EdgeEventType.USER;
case RULE_CHAIN:
return EdgeEventType.RULE_CHAIN;
case ALARM:
return EdgeEventType.ALARM;
case TENANT:
return EdgeEventType.TENANT;
case CUSTOMER:
return EdgeEventType.CUSTOMER;
case WIDGETS_BUNDLE:
return EdgeEventType.WIDGETS_BUNDLE;
case WIDGET_TYPE:
return EdgeEventType.WIDGET_TYPE;
case OTA_PACKAGE:
return EdgeEventType.OTA_PACKAGE;
case QUEUE:
return EdgeEventType.QUEUE;
default:
log.warn("Unsupported entity type [{}]", entityType);
return null;
} }
} }
private static final int STACK_TRACE_LIMIT = 10;
private EdgeUtils() {}
public static int nextPositiveInt() { public static int nextPositiveInt() {
return ThreadLocalRandom.current().nextInt(0, Integer.MAX_VALUE); return ThreadLocalRandom.current().nextInt(0, Integer.MAX_VALUE);
} }
public static EdgeEventType getEdgeEventTypeByEntityType(EntityType entityType) {
return entityTypeEdgeEventTypeEnumMap.get(entityType);
}
public static EdgeEvent constructEdgeEvent(TenantId tenantId, public static EdgeEvent constructEdgeEvent(TenantId tenantId,
EdgeId edgeId, EdgeId edgeId,
EdgeEventType type, EdgeEventType type,

2
common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileData.java

@ -27,6 +27,8 @@ import java.util.List;
@Data @Data
public class DeviceProfileData implements Serializable { public class DeviceProfileData implements Serializable {
private static final long serialVersionUID = -3864805547939495272L;
@ApiModelProperty(position = 1, value = "JSON object of device profile configuration") @ApiModelProperty(position = 1, value = "JSON object of device profile configuration")
private DeviceProfileConfiguration configuration; private DeviceProfileConfiguration configuration;
@Valid @Valid

46
common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventType.java

@ -16,32 +16,38 @@
package org.thingsboard.server.common.data.edge; package org.thingsboard.server.common.data.edge;
import lombok.Getter; import lombok.Getter;
import org.thingsboard.server.common.data.EntityType;
@Getter @Getter
public enum EdgeEventType { public enum EdgeEventType {
DASHBOARD(false), DASHBOARD(false, EntityType.DASHBOARD),
ASSET(false), ASSET(false, EntityType.ASSET),
DEVICE(false), DEVICE(false, EntityType.DEVICE),
DEVICE_PROFILE(true), DEVICE_PROFILE(true, EntityType.DEVICE_PROFILE),
ASSET_PROFILE(true), ASSET_PROFILE(true, EntityType.ASSET_PROFILE),
ENTITY_VIEW(false), ENTITY_VIEW(false, EntityType.ENTITY_VIEW),
ALARM(false), ALARM(false, EntityType.ALARM),
RULE_CHAIN(false), RULE_CHAIN(false, EntityType.RULE_CHAIN),
RULE_CHAIN_METADATA(false), RULE_CHAIN_METADATA(false, null),
EDGE(false), EDGE(false, EntityType.EDGE),
USER(true), USER(true, EntityType.USER),
CUSTOMER(true), CUSTOMER(true, EntityType.CUSTOMER),
RELATION(true), RELATION(true, null),
TENANT(true), TENANT(true, EntityType.TENANT),
WIDGETS_BUNDLE(true), TENANT_PROFILE(true, EntityType.TENANT_PROFILE),
WIDGET_TYPE(true), WIDGETS_BUNDLE(true, EntityType.WIDGETS_BUNDLE),
ADMIN_SETTINGS(true), WIDGET_TYPE(true, EntityType.WIDGET_TYPE),
OTA_PACKAGE(true), ADMIN_SETTINGS(true, null),
QUEUE(true); OTA_PACKAGE(true, EntityType.OTA_PACKAGE),
QUEUE(true, EntityType.QUEUE);
private final boolean allEdgesRelated; private final boolean allEdgesRelated;
EdgeEventType(boolean allEdgesRelated) { private final EntityType entityType;
EdgeEventType(boolean allEdgesRelated, EntityType entityType) {
this.allEdgesRelated = allEdgesRelated; this.allEdgesRelated = allEdgesRelated;
this.entityType = entityType;
} }
} }

14
common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java

@ -115,10 +115,6 @@ public class EntityIdFactory {
return new DashboardId(uuid); return new DashboardId(uuid);
case DEVICE: case DEVICE:
return new DeviceId(uuid); return new DeviceId(uuid);
case DEVICE_PROFILE:
return new DeviceProfileId(uuid);
case ASSET_PROFILE:
return new AssetProfileId(uuid);
case ASSET: case ASSET:
return new AssetId(uuid); return new AssetId(uuid);
case ALARM: case ALARM:
@ -131,12 +127,18 @@ public class EntityIdFactory {
return new WidgetsBundleId(uuid); return new WidgetsBundleId(uuid);
case WIDGET_TYPE: case WIDGET_TYPE:
return new WidgetTypeId(uuid); return new WidgetTypeId(uuid);
case DEVICE_PROFILE:
return new DeviceProfileId(uuid);
case ASSET_PROFILE:
return new AssetProfileId(uuid);
case TENANT_PROFILE:
return new TenantProfileId(uuid);
case OTA_PACKAGE: case OTA_PACKAGE:
return new OtaPackageId(uuid); return new OtaPackageId(uuid);
case QUEUE:
return new QueueId(uuid);
case EDGE: case EDGE:
return new EdgeId(uuid); return new EdgeId(uuid);
case QUEUE:
return new QueueId(uuid);
} }
throw new IllegalArgumentException("EdgeEventType " + edgeEventType + " is not supported!"); throw new IllegalArgumentException("EdgeEventType " + edgeEventType + " is not supported!");
} }

3
common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationDeliveryMethod.java

@ -24,7 +24,8 @@ public enum NotificationDeliveryMethod {
WEB("web"), WEB("web"),
EMAIL("email"), EMAIL("email"),
SMS("SMS"), SMS("SMS"),
SLACK("Slack"); SLACK("Slack"),
MICROSOFT_TEAMS("Microsoft Teams");
@Getter @Getter
private final String name; 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 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") @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
@JsonSubTypes({ @JsonSubTypes({
@Type(value = PlatformUsersNotificationTargetConfig.class, name = "PLATFORM_USERS"), @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 @Data
public abstract class NotificationTargetConfig { 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 { public enum NotificationTargetType {
PLATFORM_USERS(Set.of(NotificationDeliveryMethod.WEB, NotificationDeliveryMethod.EMAIL, NotificationDeliveryMethod.SMS)), 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 @Getter
private final Set<NotificationDeliveryMethod> supportedDeliveryMethods; 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 com.fasterxml.jackson.annotation.JsonTypeInfo;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod;
import javax.validation.constraints.NotEmpty; import javax.validation.constraints.NotEmpty;
import java.util.List;
@JsonIgnoreProperties(ignoreUnknown = true) @JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "method") @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "method")
@ -33,7 +33,8 @@ import javax.validation.constraints.NotEmpty;
@Type(name = "WEB", value = WebDeliveryMethodNotificationTemplate.class), @Type(name = "WEB", value = WebDeliveryMethodNotificationTemplate.class),
@Type(name = "EMAIL", value = EmailDeliveryMethodNotificationTemplate.class), @Type(name = "EMAIL", value = EmailDeliveryMethodNotificationTemplate.class),
@Type(name = "SMS", value = SmsDeliveryMethodNotificationTemplate.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 @Data
@NoArgsConstructor @NoArgsConstructor
@ -41,7 +42,7 @@ public abstract class DeliveryMethodNotificationTemplate {
private boolean enabled; private boolean enabled;
@NotEmpty @NotEmpty
private String body; protected String body;
public DeliveryMethodNotificationTemplate(DeliveryMethodNotificationTemplate other) { public DeliveryMethodNotificationTemplate(DeliveryMethodNotificationTemplate other) {
this.enabled = other.enabled; this.enabled = other.enabled;
@ -54,8 +55,7 @@ public abstract class DeliveryMethodNotificationTemplate {
@JsonIgnore @JsonIgnore
public abstract DeliveryMethodNotificationTemplate copy(); public abstract DeliveryMethodNotificationTemplate copy();
public boolean containsAny(String... params) { @JsonIgnore
return StringUtils.containsAny(body, params); 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.EqualsAndHashCode;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import lombok.ToString; import lombok.ToString;
import org.apache.commons.lang3.StringUtils;
import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod;
import org.thingsboard.server.common.data.validation.Length; import org.thingsboard.server.common.data.validation.Length;
import org.thingsboard.server.common.data.validation.NoXss; import org.thingsboard.server.common.data.validation.NoXss;
import javax.validation.constraints.NotEmpty; import javax.validation.constraints.NotEmpty;
import java.util.List;
@Data @Data
@NoArgsConstructor @NoArgsConstructor
@ -37,6 +37,11 @@ public class EmailDeliveryMethodNotificationTemplate extends DeliveryMethodNotif
@NotEmpty @NotEmpty
private String subject; 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) { public EmailDeliveryMethodNotificationTemplate(EmailDeliveryMethodNotificationTemplate other) {
super(other); super(other);
this.subject = other.subject; this.subject = other.subject;
@ -52,9 +57,4 @@ public class EmailDeliveryMethodNotificationTemplate extends DeliveryMethodNotif
return new EmailDeliveryMethodNotificationTemplate(this); 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.notification.NotificationDeliveryMethod;
import org.thingsboard.server.common.data.validation.NoXss; import org.thingsboard.server.common.data.validation.NoXss;
import java.util.List;
@Data @Data
@NoArgsConstructor @NoArgsConstructor
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true) @ToString(callSuper = true)
public class SlackDeliveryMethodNotificationTemplate extends DeliveryMethodNotificationTemplate { public class SlackDeliveryMethodNotificationTemplate extends DeliveryMethodNotificationTemplate {
private final List<TemplatableValue> templatableValues = List.of(
TemplatableValue.of(this::getBody, this::setBody)
);
public SlackDeliveryMethodNotificationTemplate(DeliveryMethodNotificationTemplate other) { public SlackDeliveryMethodNotificationTemplate(DeliveryMethodNotificationTemplate other) {
super(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.Length;
import org.thingsboard.server.common.data.validation.NoXss; import org.thingsboard.server.common.data.validation.NoXss;
import java.util.List;
@Data @Data
@NoArgsConstructor @NoArgsConstructor
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true) @ToString(callSuper = true)
public class SmsDeliveryMethodNotificationTemplate extends DeliveryMethodNotificationTemplate { public class SmsDeliveryMethodNotificationTemplate extends DeliveryMethodNotificationTemplate {
private final List<TemplatableValue> templatableValues = List.of(
TemplatableValue.of(this::getBody, this::setBody)
);
public SmsDeliveryMethodNotificationTemplate(SmsDeliveryMethodNotificationTemplate other) { public SmsDeliveryMethodNotificationTemplate(SmsDeliveryMethodNotificationTemplate other) {
super(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.EqualsAndHashCode;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import lombok.ToString; import lombok.ToString;
import org.apache.commons.lang3.StringUtils;
import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod;
import org.thingsboard.server.common.data.validation.Length; import org.thingsboard.server.common.data.validation.Length;
import org.thingsboard.server.common.data.validation.NoXss; import org.thingsboard.server.common.data.validation.NoXss;
import javax.validation.constraints.NotEmpty; import javax.validation.constraints.NotEmpty;
import java.util.List;
import java.util.Optional; import java.util.Optional;
@Data @Data
@ -43,6 +43,13 @@ public class WebDeliveryMethodNotificationTemplate extends DeliveryMethodNotific
private String subject; private String subject;
private JsonNode additionalConfig; 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) { public WebDeliveryMethodNotificationTemplate(WebDeliveryMethodNotificationTemplate other) {
super(other); super(other);
this.subject = other.subject; this.subject = other.subject;
@ -107,10 +114,4 @@ public class WebDeliveryMethodNotificationTemplate extends DeliveryMethodNotific
return new WebDeliveryMethodNotificationTemplate(this); 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);
}
} }

2
common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java

@ -29,6 +29,8 @@ import org.thingsboard.server.common.data.TenantProfileType;
@Data @Data
public class DefaultTenantProfileConfiguration implements TenantProfileConfiguration { public class DefaultTenantProfileConfiguration implements TenantProfileConfiguration {
private static final long serialVersionUID = -7134932690332578595L;
private long maxDevices; private long maxDevices;
private long maxAssets; private long maxAssets;
private long maxCustomers; private long maxCustomers;

4
common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/TenantProfileConfiguration.java

@ -22,6 +22,8 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo;
import org.thingsboard.server.common.data.ApiUsageRecordKey; import org.thingsboard.server.common.data.ApiUsageRecordKey;
import org.thingsboard.server.common.data.TenantProfileType; import org.thingsboard.server.common.data.TenantProfileType;
import java.io.Serializable;
@JsonIgnoreProperties(ignoreUnknown = true) @JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo( @JsonTypeInfo(
use = JsonTypeInfo.Id.NAME, use = JsonTypeInfo.Id.NAME,
@ -29,7 +31,7 @@ import org.thingsboard.server.common.data.TenantProfileType;
property = "type") property = "type")
@JsonSubTypes({ @JsonSubTypes({
@JsonSubTypes.Type(value = DefaultTenantProfileConfiguration.class, name = "DEFAULT")}) @JsonSubTypes.Type(value = DefaultTenantProfileConfiguration.class, name = "DEFAULT")})
public interface TenantProfileConfiguration { public interface TenantProfileConfiguration extends Serializable {
@JsonIgnore @JsonIgnore
TenantProfileType getType(); TenantProfileType getType();

5
common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/TenantProfileData.java

@ -19,11 +19,14 @@ import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty; import io.swagger.annotations.ApiModelProperty;
import lombok.Data; import lombok.Data;
import java.io.Serializable;
import java.util.List; import java.util.List;
@ApiModel @ApiModel
@Data @Data
public class TenantProfileData { public class TenantProfileData implements Serializable {
private static final long serialVersionUID = -3642550257035920976L;
@ApiModelProperty(position = 1, value = "Complex JSON object that contains profile settings: max devices, max assets, rate limits, etc.") @ApiModelProperty(position = 1, value = "Complex JSON object that contains profile settings: max devices, max assets, rate limits, etc.")
private TenantProfileConfiguration configuration; private TenantProfileConfiguration configuration;

7
common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/TenantProfileQueueConfiguration.java

@ -20,8 +20,13 @@ import lombok.Data;
import org.thingsboard.server.common.data.queue.ProcessingStrategy; import org.thingsboard.server.common.data.queue.ProcessingStrategy;
import org.thingsboard.server.common.data.queue.SubmitStrategy; import org.thingsboard.server.common.data.queue.SubmitStrategy;
import java.io.Serializable;
@Data @Data
public class TenantProfileQueueConfiguration { public class TenantProfileQueueConfiguration implements Serializable {
private static final long serialVersionUID = -546600745123197362L;
private String name; private String name;
private String topic; private String topic;
private int pollInterval; private int pollInterval;

32
common/edge-api/src/main/proto/edge.proto

@ -383,6 +383,36 @@ message UserCredentialsUpdateMsg {
string password = 4; string password = 4;
} }
message TenantUpdateMsg {
UpdateMsgType msgType = 1;
int64 idMSB = 2;
int64 idLSB = 3;
string title = 4;
int64 profileIdMSB = 5;
int64 profileIdLSB = 6;
string region = 7;
optional string country = 8;
optional string state = 9;
optional string city = 10;
optional string address = 11;
optional string address2 = 12;
optional string zip = 13;
optional string phone = 14;
optional string email = 15;
optional string additionalInfo = 16;
}
message TenantProfileUpdateMsg {
UpdateMsgType msgType = 1;
int64 idMSB = 2;
int64 idLSB = 3;
string name = 4;
optional string description = 5;
bool default = 6;
bool isolatedRuleChain = 7;
bytes profileDataBytes = 8;
}
message RuleChainMetadataRequestMsg { message RuleChainMetadataRequestMsg {
int64 ruleChainIdMSB = 1; int64 ruleChainIdMSB = 1;
int64 ruleChainIdLSB = 2; int64 ruleChainIdLSB = 2;
@ -574,5 +604,7 @@ message DownlinkMsg {
repeated QueueUpdateMsg queueUpdateMsg = 23; repeated QueueUpdateMsg queueUpdateMsg = 23;
repeated AssetProfileUpdateMsg assetProfileUpdateMsg = 24; repeated AssetProfileUpdateMsg assetProfileUpdateMsg = 24;
EdgeConfiguration edgeConfiguration = 25; EdgeConfiguration edgeConfiguration = 25;
repeated TenantUpdateMsg tenantUpdateMsg = 26;
repeated TenantProfileUpdateMsg tenantProfileUpdateMsg = 27;
} }

10
dao/src/main/java/org/thingsboard/server/dao/edge/EdgeDao.java

@ -170,4 +170,12 @@ public interface EdgeDao extends Dao<Edge> {
*/ */
PageData<Edge> findEdgesByTenantIdAndEntityId(UUID tenantId, UUID entityId, EntityType entityType, PageLink pageLink); PageData<Edge> findEdgesByTenantIdAndEntityId(UUID tenantId, UUID entityId, EntityType entityType, PageLink pageLink);
} /**
* Find edges by tenantProfileId.
*
* @param tenantProfileId the tenantProfileId
* @return the list of edge objects
*/
PageData<Edge> findEdgesByTenantProfileId(UUID tenantProfileId, PageLink pageLink);
}

17
dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java

@ -33,6 +33,7 @@ import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.EntitySubtype;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.Edge;
@ -45,8 +46,10 @@ import org.thingsboard.server.common.data.id.HasId;
import org.thingsboard.server.common.data.id.IdBased; import org.thingsboard.server.common.data.id.IdBased;
import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.TenantProfileId;
import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageDataIterable;
import org.thingsboard.server.common.data.page.PageDataIterableByTenantIdEntityId; import org.thingsboard.server.common.data.page.PageDataIterableByTenantIdEntityId;
import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntityRelation;
@ -61,6 +64,7 @@ import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.DataValidator;
import org.thingsboard.server.dao.service.PaginatedRemover; import org.thingsboard.server.dao.service.PaginatedRemover;
import org.thingsboard.server.dao.service.Validator; import org.thingsboard.server.dao.service.Validator;
import org.thingsboard.server.dao.tenant.TenantService;
import org.thingsboard.server.dao.user.UserService; import org.thingsboard.server.dao.user.UserService;
import javax.annotation.Nullable; import javax.annotation.Nullable;
@ -100,6 +104,9 @@ public class EdgeServiceImpl extends AbstractCachedEntityService<EdgeCacheKey, E
@Autowired @Autowired
private RelationService relationService; private RelationService relationService;
@Autowired
private TenantService tenantService;
@Autowired @Autowired
private DataValidator<Edge> edgeValidator; private DataValidator<Edge> edgeValidator;
@ -385,6 +392,14 @@ public class EdgeServiceImpl extends AbstractCachedEntityService<EdgeCacheKey, E
return edgeDao.findEdgesByTenantIdAndEntityId(tenantId.getId(), entityId.getId(), entityId.getEntityType(), pageLink); return edgeDao.findEdgesByTenantIdAndEntityId(tenantId.getId(), entityId.getId(), entityId.getEntityType(), pageLink);
} }
@Override
public PageData<Edge> findEdgesByTenantProfileId(TenantProfileId tenantProfileId, PageLink pageLink) {
log.trace("Executing findEdgesByTenantProfileId, tenantProfileId [{}], pageLink [{}]", tenantProfileId, pageLink);
Validator.validateId(tenantProfileId, "Incorrect tenantProfileId " + tenantProfileId);
validatePageLink(pageLink);
return edgeDao.findEdgesByTenantProfileId(tenantProfileId.getId(), pageLink);
}
private PaginatedRemover<TenantId, Edge> tenantEdgesRemover = private PaginatedRemover<TenantId, Edge> tenantEdgesRemover =
new PaginatedRemover<TenantId, Edge>() { new PaginatedRemover<TenantId, Edge>() {
@ -459,6 +474,8 @@ public class EdgeServiceImpl extends AbstractCachedEntityService<EdgeCacheKey, E
} else { } else {
return convertToEdgeIds(findEdgesByTenantIdAndCustomerId(tenantId, userById.getCustomerId(), pageLink)); return convertToEdgeIds(findEdgesByTenantIdAndCustomerId(tenantId, userById.getCustomerId(), pageLink));
} }
case TENANT_PROFILE:
return convertToEdgeIds(findEdgesByTenantProfileId(new TenantProfileId(entityId.getId()), pageLink));
default: default:
log.warn("[{}] Unsupported entity type {}", tenantId, entityId.getEntityType()); log.warn("[{}] Unsupported entity type {}", tenantId, entityId.getEntityType());
return createEmptyEdgeIdPageData(); return createEmptyEdgeIdPageData();

4
dao/src/main/java/org/thingsboard/server/dao/sql/edge/EdgeRepository.java

@ -120,6 +120,10 @@ public interface EdgeRepository extends JpaRepository<EdgeEntity, UUID> {
@Param("searchText") String searchText, @Param("searchText") String searchText,
Pageable pageable); Pageable pageable);
@Query("SELECT ee FROM EdgeEntity ee, TenantEntity te WHERE ee.tenantId = te.id AND te.tenantProfileId = :tenantProfileId ")
Page<EdgeEntity> findByTenantProfileId(@Param("tenantProfileId") UUID tenantProfileId,
Pageable pageable);
@Query("SELECT DISTINCT d.type FROM EdgeEntity d WHERE d.tenantId = :tenantId") @Query("SELECT DISTINCT d.type FROM EdgeEntity d WHERE d.tenantId = :tenantId")
List<String> findTenantEdgeTypes(@Param("tenantId") UUID tenantId); List<String> findTenantEdgeTypes(@Param("tenantId") UUID tenantId);

9
dao/src/main/java/org/thingsboard/server/dao/sql/edge/JpaEdgeDao.java

@ -184,6 +184,15 @@ public class JpaEdgeDao extends JpaAbstractDao<EdgeEntity, Edge> implements Edge
DaoUtil.toPageable(pageLink))); DaoUtil.toPageable(pageLink)));
} }
@Override
public PageData<Edge> findEdgesByTenantProfileId(UUID tenantProfileId, PageLink pageLink) {
log.debug("Try to find edges by tenantProfileId [{}], pageLink [{}]", tenantProfileId, pageLink);
return DaoUtil.toPageData(
edgeRepository.findByTenantProfileId(
tenantProfileId,
DaoUtil.toPageable(pageLink)));
}
private List<EntitySubtype> convertTenantEdgeTypesToDto(UUID tenantId, List<String> types) { private List<EntitySubtype> convertTenantEdgeTypesToDto(UUID tenantId, List<String> types) {
List<EntitySubtype> list = Collections.emptyList(); List<EntitySubtype> list = Collections.emptyList();
if (types != null && !types.isEmpty()) { if (types != null && !types.isEmpty()) {

5
dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileServiceImpl.java

@ -32,6 +32,8 @@ import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.common.data.tenant.profile.TenantProfileData; import org.thingsboard.server.common.data.tenant.profile.TenantProfileData;
import org.thingsboard.server.dao.entity.AbstractCachedEntityService; import org.thingsboard.server.dao.entity.AbstractCachedEntityService;
import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent;
import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent;
import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.DataValidator;
import org.thingsboard.server.dao.service.PaginatedRemover; import org.thingsboard.server.dao.service.PaginatedRemover;
@ -92,6 +94,8 @@ public class TenantProfileServiceImpl extends AbstractCachedEntityService<Tenant
try { try {
savedTenantProfile = tenantProfileDao.save(tenantId, tenantProfile); savedTenantProfile = tenantProfileDao.save(tenantId, tenantProfile);
publishEvictEvent(new TenantProfileEvictEvent(savedTenantProfile.getId(), savedTenantProfile.isDefault())); publishEvictEvent(new TenantProfileEvictEvent(savedTenantProfile.getId(), savedTenantProfile.isDefault()));
eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(tenantId)
.entityId(savedTenantProfile.getId()).added(tenantProfile.getId() == null).build());
} catch (Exception t) { } catch (Exception t) {
handleEvictEvent(new TenantProfileEvictEvent(null, tenantProfile.isDefault())); handleEvictEvent(new TenantProfileEvictEvent(null, tenantProfile.isDefault()));
ConstraintViolationException e = extractConstraintViolationException(t).orElse(null); ConstraintViolationException e = extractConstraintViolationException(t).orElse(null);
@ -128,6 +132,7 @@ public class TenantProfileServiceImpl extends AbstractCachedEntityService<Tenant
} }
deleteEntityRelations(tenantId, tenantProfileId); deleteEntityRelations(tenantId, tenantProfileId);
publishEvictEvent(new TenantProfileEvictEvent(tenantProfileId, isDefault)); publishEvictEvent(new TenantProfileEvictEvent(tenantProfileId, isDefault));
eventPublisher.publishEvent(DeleteEntityEvent.builder().tenantId(tenantId).entityId(tenantProfileId).build());
} }
@Override @Override

4
dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java

@ -40,6 +40,8 @@ import org.thingsboard.server.dao.dashboard.DashboardService;
import org.thingsboard.server.dao.device.DeviceProfileService; import org.thingsboard.server.dao.device.DeviceProfileService;
import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.entity.AbstractCachedEntityService; import org.thingsboard.server.dao.entity.AbstractCachedEntityService;
import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent;
import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent;
import org.thingsboard.server.dao.notification.NotificationRequestService; import org.thingsboard.server.dao.notification.NotificationRequestService;
import org.thingsboard.server.dao.notification.NotificationRuleService; import org.thingsboard.server.dao.notification.NotificationRuleService;
import org.thingsboard.server.dao.notification.NotificationSettingsService; import org.thingsboard.server.dao.notification.NotificationSettingsService;
@ -191,6 +193,7 @@ public class TenantServiceImpl extends AbstractCachedEntityService<TenantId, Ten
boolean create = tenant.getId() == null; boolean create = tenant.getId() == null;
Tenant savedTenant = tenantDao.save(tenant.getId(), tenant); Tenant savedTenant = tenantDao.save(tenant.getId(), tenant);
publishEvictEvent(new TenantEvictEvent(savedTenant.getId(), create)); publishEvictEvent(new TenantEvictEvent(savedTenant.getId(), create));
eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(TenantId.SYS_TENANT_ID).entityId(savedTenant.getId()).added(create).build());
if (tenant.getId() == null) { if (tenant.getId() == null) {
deviceProfileService.createDefaultDeviceProfile(savedTenant.getId()); deviceProfileService.createDefaultDeviceProfile(savedTenant.getId());
assetProfileService.createDefaultAssetProfile(savedTenant.getId()); assetProfileService.createDefaultAssetProfile(savedTenant.getId());
@ -236,6 +239,7 @@ public class TenantServiceImpl extends AbstractCachedEntityService<TenantId, Ten
adminSettingsService.deleteAdminSettingsByTenantId(tenantId); adminSettingsService.deleteAdminSettingsByTenantId(tenantId);
tenantDao.removeById(tenantId, tenantId.getId()); tenantDao.removeById(tenantId, tenantId.getId());
publishEvictEvent(new TenantEvictEvent(tenantId, true)); publishEvictEvent(new TenantEvictEvent(tenantId, true));
eventPublisher.publishEvent(DeleteEntityEvent.builder().tenantId(TenantId.SYS_TENANT_ID).entityId(tenantId).build());
deleteEntityRelations(tenantId, tenantId); deleteEntityRelations(tenantId, tenantId);
} }

2
dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java

@ -141,7 +141,7 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic
} }
eventPublisher.publishEvent(SaveEntityEvent.builder() eventPublisher.publishEvent(SaveEntityEvent.builder()
.tenantId(tenantId == null ? TenantId.SYS_TENANT_ID : tenantId) .tenantId(tenantId == null ? TenantId.SYS_TENANT_ID : tenantId)
.entity(user) .entity(savedUser)
.entityId(savedUser.getId()) .entityId(savedUser.getId())
.added(user.getId() == null).build()); .added(user.getId() == null).build());
return savedUser; return savedUser;

28
dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java

@ -59,6 +59,7 @@ import java.nio.ByteBuffer;
import java.util.Comparator; import java.util.Comparator;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.Objects;
import java.util.UUID; import java.util.UUID;
import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotNull;
@ -80,7 +81,7 @@ public abstract class AbstractServiceTest {
@Before @Before
public void beforeAbstractService() { public void beforeAbstractService() {
tenantId = createTenant(); tenantId = createTenant().getId();
} }
@After @After
@ -107,29 +108,10 @@ public abstract class AbstractServiceTest {
.data(JacksonUtil.toString(readFromResource("TestJsonData.json"))) .data(JacksonUtil.toString(readFromResource("TestJsonData.json")))
.build(); .build();
} }
//
// private ComponentDescriptor getOrCreateDescriptor(ComponentScope scope, ComponentType type, String clazz, String configurationDescriptorResource) throws IOException {
// return getOrCreateDescriptor(scope, type, clazz, configurationDescriptorResource, null);
// }
//
// private ComponentDescriptor getOrCreateDescriptor(ComponentScope scope, ComponentType type, String clazz, String configurationDescriptorResource, String actions) throws IOException {
// ComponentDescriptor descriptor = componentDescriptorService.findByClazz(clazz);
// if (descriptor == null) {
// descriptor = new ComponentDescriptor();
// descriptor.setName("test");
// descriptor.setClazz(clazz);
// descriptor.setScope(scope);
// descriptor.setType(type);
// descriptor.setActions(actions);
// descriptor.setConfigurationDescriptor(readFromResource(configurationDescriptorResource));
// componentDescriptorService.saveComponent(descriptor);
// }
// return descriptor;
// }
public JsonNode readFromResource(String resourceName) throws IOException { public JsonNode readFromResource(String resourceName) throws IOException {
try (InputStream is = this.getClass().getClassLoader().getResourceAsStream(resourceName)){ try (InputStream is = this.getClass().getClassLoader().getResourceAsStream(resourceName)){
return JacksonUtil.fromBytes(is.readAllBytes()); return JacksonUtil.fromBytes(Objects.requireNonNull(is).readAllBytes());
} }
} }
@ -172,12 +154,12 @@ public abstract class AbstractServiceTest {
return assetProfile; return assetProfile;
} }
public TenantId createTenant() { public Tenant createTenant() {
Tenant tenant = new Tenant(); Tenant tenant = new Tenant();
tenant.setTitle("My tenant " + UUID.randomUUID()); tenant.setTitle("My tenant " + UUID.randomUUID());
Tenant savedTenant = tenantService.saveTenant(tenant); Tenant savedTenant = tenantService.saveTenant(tenant);
assertNotNull(savedTenant); assertNotNull(savedTenant);
return savedTenant.getId(); return savedTenant;
} }
protected Edge constructEdge(TenantId tenantId, String name, String type) { protected Edge constructEdge(TenantId tenantId, String name, String type) {

2
dao/src/test/java/org/thingsboard/server/dao/service/DeviceServiceTest.java

@ -78,7 +78,7 @@ public class DeviceServiceTest extends AbstractServiceTest {
@Before @Before
public void before() { public void before() {
anotherTenantId = createTenant(); anotherTenantId = createTenant().getId();
} }
@After @After

19
dao/src/test/java/org/thingsboard/server/dao/service/EdgeServiceTest.java

@ -650,4 +650,23 @@ public class EdgeServiceTest extends AbstractServiceTest {
Assert.assertEquals("{\"Rule Chain #3\":[\"Rule Chain #1\",\"Rule Chain #2\"]}", missingToRelatedRuleChains); Assert.assertEquals("{\"Rule Chain #3\":[\"Rule Chain #1\",\"Rule Chain #2\"]}", missingToRelatedRuleChains);
} }
@Test
public void testFindEdgesByTenantProfileId() {
Tenant tenant1 = createTenant();
Tenant tenant2 = createTenant();
Assert.assertNotNull(tenant1);
Assert.assertNotNull(tenant2);
Edge edge1 = constructEdge(tenant1.getId(), "Tenant1 edge", "default");
Edge edge2 = constructEdge(tenant2.getId(), "Tenant2 edge", "default");
Edge savedEdge1 = edgeService.saveEdge(edge1);
Edge savedEdge2 = edgeService.saveEdge(edge2);
Assert.assertNotNull(savedEdge1);
Assert.assertNotNull(savedEdge2);
Assert.assertEquals(tenant1.getTenantProfileId(), tenant2.getTenantProfileId());
PageData<Edge> edgesPageData = edgeService.findEdgesByTenantProfileId(tenant2.getTenantProfileId(),
new PageLink(1000));
Assert.assertEquals(2, edgesPageData.getTotalElements());
}
} }

2
dao/src/test/java/org/thingsboard/server/dao/service/UserServiceTest.java

@ -204,7 +204,7 @@ public class UserServiceTest extends AbstractServiceTest {
Assert.assertEquals(1, users.size()); Assert.assertEquals(1, users.size());
Assert.assertEquals(tenantAdminUser, users.get(0)); Assert.assertEquals(tenantAdminUser, users.get(0));
TenantId secondTenantId = createTenant(); TenantId secondTenantId = createTenant().getId();
List<User> tenantAdmins = new ArrayList<>(); List<User> tenantAdmins = new ArrayList<>();
for (int i = 0; i < 124; i++) { for (int i = 0; i < 124; i++) {

2
rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js

File diff suppressed because one or more lines are too long

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"> <ng-container *ngSwitchCase="notificationTargetConfigType.TENANT_ADMINISTRATORS">
<section *ngIf="isSysAdmin()"> <section *ngIf="isSysAdmin()">
<div fxFlex fxLayoutAlign="center center"> <div fxFlex fxLayoutAlign="center center">
<mat-button-toggle-group class="tb-notification-tenant-group" <tb-toggle-select class="tb-notification-tenant-group" appearance="fill"
style="width: 280px;" formControlName="filterByTenants">
formControlName="filterByTenants"> <tb-toggle-option [value]="true">{{ 'tenant.tenant' | translate }}</tb-toggle-option>
<mat-button-toggle fxFlex [value]=true>{{ 'tenant.tenant' | translate }}</mat-button-toggle> <tb-toggle-option [value]="false">{{ 'tenant-profile.tenant-profile' | translate }}</tb-toggle-option>
<mat-button-toggle fxFlex [value]=false>{{ 'tenant-profile.tenant-profile' | translate }}</mat-button-toggle> </tb-toggle-select>
</mat-button-toggle-group>
</div> </div>
<ng-container *ngIf="targetNotificationForm.get('configuration.usersFilter.filterByTenants').value; else tenantProfiles"> <ng-container *ngIf="targetNotificationForm.get('configuration.usersFilter.filterByTenants').value; else tenantProfiles">
<tb-entity-list <tb-entity-list
@ -121,6 +120,22 @@
[slackChanelType]="targetNotificationForm.get('configuration.conversationType').value"> [slackChanelType]="targetNotificationForm.get('configuration.conversationType').value">
</tb-slack-conversation-autocomplete> </tb-slack-conversation-autocomplete>
</section> </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-form-field class="mat-block">
<mat-label translate>notification.description</mat-label> <mat-label translate>notification.description</mat-label>
<input matInput formControlName="description"> <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 { form.tb-dialog-container {
min-width: 600px; min-width: 600px;
max-height: 100vh; max-height: 100vh;
color: rgba(0, 0, 0, 0.87);
} }
.mat-dialog-content { .mat-dialog-content {
@ -29,6 +30,11 @@
display: block; display: block;
padding-bottom: 6px; padding-bottom: 6px;
} }
.tb-notification-tenant-group {
width: 280px;
margin-bottom: 16px;
}
} }
:host ::ng-deep { :host ::ng-deep {
@ -51,58 +57,4 @@
flex-direction: column; 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 { AuthState } from '@core/auth/auth.models';
import { getCurrentAuthState } from '@core/auth/auth.selectors'; import { getCurrentAuthState } from '@core/auth/auth.selectors';
import { AuthUser } from '@shared/models/user.model'; import { AuthUser } from '@shared/models/user.model';
import { control } from 'leaflet';
export interface RecipientNotificationDialogData { export interface RecipientNotificationDialogData {
target?: NotificationTarget; target?: NotificationTarget;
@ -99,6 +98,8 @@ export class RecipientNotificationDialogComponent extends
}), }),
conversationType: [{value: SlackChanelType.PUBLIC_CHANNEL, disabled: true}], conversationType: [{value: SlackChanelType.PUBLIC_CHANNEL, disabled: true}],
conversation: [{value: '', disabled: true}, Validators.required], conversation: [{value: '', disabled: true}, Validators.required],
webhookUrl: [{value: '', disabled: true}, Validators.required],
channelName: [{value: '', disabled: true}, Validators.required],
description: [null] description: [null]
}) })
}); });
@ -116,6 +117,10 @@ export class RecipientNotificationDialogComponent extends
this.targetNotificationForm.get('configuration.conversationType').enable({emitEvent: false}); this.targetNotificationForm.get('configuration.conversationType').enable({emitEvent: false});
this.targetNotificationForm.get('configuration.conversation').enable({emitEvent: false}); this.targetNotificationForm.get('configuration.conversation').enable({emitEvent: false});
break; 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.type').enable({emitEvent: false});
this.targetNotificationForm.get('configuration.description').enable({emitEvent: false}); this.targetNotificationForm.get('configuration.description').enable({emitEvent: false});
@ -160,7 +165,7 @@ export class RecipientNotificationDialogComponent extends
if (isDefinedAndNotNull(data.target)) { if (isDefinedAndNotNull(data.target)) {
this.targetNotificationForm.patchValue(data.target, {emitEvent: false}); this.targetNotificationForm.patchValue(data.target, {emitEvent: false});
this.targetNotificationForm.get('configuration.type').updateValueAndValidity({onlySelf: true}); 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') this.targetNotificationForm.get('configuration.usersFilter.filterByTenants')
.patchValue(!Array.isArray(this.data.target.configuration.usersFilter.tenantProfilesIds), {onlySelf: true}); .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; flex-direction: column;
height: 100%; height: 100%;
padding: 0 !important; padding: 0 !important;
color: rgba(0, 0, 0, 0.87);
.mat-stepper-horizontal { .mat-stepper-horizontal {
display: flex; display: flex;

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

@ -17,6 +17,7 @@
display: block; display: block;
width: 600px; width: 600px;
max-width: 100%; max-width: 100%;
color: rgba(0, 0, 0, 0.87);
h6 { h6 {
font-weight: 500; 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-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async"> <mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar> </mat-progress-bar>
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
<div mat-dialog-content> <div mat-dialog-content>
<mat-horizontal-stepper linear #createNotification <mat-horizontal-stepper linear #createNotification
[labelPosition]="(stepperLabelPosition | async)" [labelPosition]="(stepperLabelPosition | async)"
@ -40,12 +39,11 @@
<ng-template matStepLabel>{{ 'notification.compose' | translate }}</ng-template> <ng-template matStepLabel>{{ 'notification.compose' | translate }}</ng-template>
<form [formGroup]="notificationRequestForm"> <form [formGroup]="notificationRequestForm">
<div fxLayout="row" fxLayoutAlign="center"> <div fxLayout="row" fxLayoutAlign="center">
<mat-button-toggle-group class="tb-notification-use-template-toggle-group" <tb-toggle-select class="tb-notification-use-template-toggle-group" appearance="fill"
style="width: 320px;" formControlName="useTemplate">
formControlName="useTemplate"> <tb-toggle-option [value]="false">{{ 'notification.start-from-scratch' | translate }}</tb-toggle-option>
<mat-button-toggle fxFlex [value]=false>{{ 'notification.start-from-scratch' | translate }}</mat-button-toggle> <tb-toggle-option [value]="true">{{ 'notification.use-template' | translate }}</tb-toggle-option>
<mat-button-toggle fxFlex [value]=true>{{ 'notification.use-template' | translate }}</mat-button-toggle> </tb-toggle-select>
</mat-button-toggle-group>
</div> </div>
<div *ngIf="notificationRequestForm.get('useTemplate').value; else scratchTemplate"> <div *ngIf="notificationRequestForm.get('useTemplate').value; else scratchTemplate">
<tb-template-autocomplete <tb-template-autocomplete
@ -172,73 +170,82 @@
{{ 'notification.message-required' | translate }} {{ 'notification.message-required' | translate }}
</mat-error> </mat-error>
</mat-form-field> </mat-form-field>
<section formGroupName="additionalConfig"> <section formGroupName="additionalConfig" class="tb-form-panel no-padding no-border">
<section formGroupName="icon" class="additional-config-group"> <div class="tb-form-row space-between" formGroupName="icon">
<mat-slide-toggle formControlName="enabled" class="toggle"> <mat-slide-toggle formControlName="enabled" class="mat-slide">
{{ 'icon.icon' | translate }} {{ 'icon.icon' | translate }}
</mat-slide-toggle> </mat-slide-toggle>
<div *ngIf="webTemplateForm.get('additionalConfig.icon.enabled').value" <div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
fxLayout="row" fxLayoutGap.gt-xs="8px" fxLayout.xs="column"> <tb-material-icon-select asBoxInput
<tb-material-icon-select formControlName="icon" required fxFlex> [color]="webTemplateForm.get('additionalConfig.icon.color').value"
formControlName="icon">
</tb-material-icon-select> </tb-material-icon-select>
<tb-color-input formControlName="color" fxFlex> <tb-color-input asBoxInput
formControlName="color">
</tb-color-input> </tb-color-input>
</div> </div>
</section> </div>
<section formGroupName="actionButtonConfig" class="additional-config-group"> <div class="tb-form-panel tb-slide-toggle stroked" formGroupName="actionButtonConfig">
<mat-slide-toggle formControlName="enabled" class="toggle"> <mat-expansion-panel class="tb-settings" [expanded]="webTemplateForm.get('additionalConfig.actionButtonConfig.enabled').value">
{{ 'notification.action-button' | translate }} <mat-expansion-panel-header fxLayout="row wrap" class="fill-width">
</mat-slide-toggle> <mat-panel-title fxFlex="60">
<div *ngIf="webTemplateForm.get('additionalConfig.actionButtonConfig.enabled').value"> <mat-slide-toggle class="mat-slide" formControlName="enabled" (click)="$event.stopPropagation()"
<div fxLayout="row" fxLayoutGap.gt-xs="8px" fxLayout.xs="column"> fxLayoutAlign="center">
<mat-form-field class="mat-block" fxFlex> {{ 'notification.action-button' | translate }}
<mat-label translate>notification.button-text</mat-label> </mat-slide-toggle>
<input matInput formControlName="text" required> </mat-panel-title>
<mat-error *ngIf="webTemplateForm.get('additionalConfig.actionButtonConfig.text').hasError('required')"> </mat-expansion-panel-header>
{{ 'notification.button-text-required' | translate }} <ng-template matExpansionPanelContent class="tb-extension-panel">
</mat-error> <div fxLayout="row" fxLayoutGap.gt-xs="8px" fxLayout.xs="column">
<mat-error *ngIf="webTemplateForm.get('additionalConfig.actionButtonConfig.text').hasError('maxlength')"> <mat-form-field class="mat-block" fxFlex>
{{ 'notification.button-text-max-length' | translate : <mat-label translate>notification.button-text</mat-label>
{length: webTemplateForm.get('additionalConfig.actionButtonConfig.text').getError('maxlength').requiredLength} <input matInput formControlName="text" required>
}} <mat-error *ngIf="webTemplateForm.get('additionalConfig.actionButtonConfig.text').hasError('required')">
</mat-error> {{ 'notification.button-text-required' | translate }}
</mat-form-field> </mat-error>
</div> <mat-error *ngIf="webTemplateForm.get('additionalConfig.actionButtonConfig.text').hasError('maxlength')">
<div fxLayout="row" fxLayoutGap.gt-xs="8px" fxLayout.xs="column"> {{ 'notification.button-text-max-length' | translate :
<mat-form-field fxFlex="30" fxFlex.xs="100"> {length: webTemplateForm.get('additionalConfig.actionButtonConfig.text').getError('maxlength').requiredLength}
<mat-label translate>notification.action-type</mat-label> }}
<mat-select formControlName="linkType"> </mat-error>
<mat-option *ngFor="let actionButtonLinkType of actionButtonLinkTypes" [value]="actionButtonLinkType"> </mat-form-field>
{{ actionButtonLinkTypeTranslateMap.get(actionButtonLinkType) | translate }} </div>
</mat-option> <div fxLayout="row" fxLayoutGap.gt-xs="8px" fxLayout.xs="column">
</mat-select> <mat-form-field fxFlex="30" fxFlex.xs="100">
</mat-form-field> <mat-label translate>notification.action-type</mat-label>
<mat-form-field fxFlex <mat-select formControlName="linkType">
*ngIf="webTemplateForm.get('additionalConfig.actionButtonConfig.linkType').value === actionButtonLinkType.LINK; else dashboardSelector"> <mat-option *ngFor="let actionButtonLinkType of actionButtonLinkTypes" [value]="actionButtonLinkType">
<mat-label translate>notification.link</mat-label> {{ actionButtonLinkTypeTranslateMap.get(actionButtonLinkType) | translate }}
<input matInput formControlName="link" required> </mat-option>
<mat-error *ngIf="webTemplateForm.get('additionalConfig.actionButtonConfig.link').hasError('required')"> </mat-select>
{{ 'notification.link-required' | translate }} </mat-form-field>
</mat-error> <mat-form-field fxFlex
</mat-form-field> *ngIf="webTemplateForm.get('additionalConfig.actionButtonConfig.linkType').value === actionButtonLinkType.LINK; else dashboardSelector">
<ng-template #dashboardSelector> <mat-label translate>notification.link</mat-label>
<tb-dashboard-autocomplete <input matInput formControlName="link" required>
fxFlex="35" fxFlex.xs="100" <mat-error *ngIf="webTemplateForm.get('additionalConfig.actionButtonConfig.link').hasError('required')">
required {{ 'notification.link-required' | translate }}
formControlName="dashboardId"> </mat-error>
</tb-dashboard-autocomplete> </mat-form-field>
<tb-dashboard-state-autocomplete fxFlex="35" fxFlex.xs="100" <ng-template #dashboardSelector>
[dashboardId]="webTemplateForm.get('additionalConfig.actionButtonConfig.dashboardId').value" <tb-dashboard-autocomplete
formControlName="dashboardState"> fxFlex="35" fxFlex.xs="100"
</tb-dashboard-state-autocomplete> required
</ng-template> formControlName="dashboardId">
</div> </tb-dashboard-autocomplete>
<mat-slide-toggle formControlName="setEntityIdInState" class="toggle" <tb-dashboard-state-autocomplete fxFlex="35" fxFlex.xs="100"
*ngIf="webTemplateForm.get('additionalConfig.actionButtonConfig.linkType').value === actionButtonLinkType.DASHBOARD"> [dashboardId]="webTemplateForm.get('additionalConfig.actionButtonConfig.dashboardId').value"
{{ 'notification.set-entity-from-notification' | translate }} formControlName="dashboardState">
</mat-slide-toggle> </tb-dashboard-state-autocomplete>
</div> </ng-template>
</section> </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> </section>
</form> </form>
</mat-step> </mat-step>
@ -333,6 +340,104 @@
</mat-form-field> </mat-form-field>
</form> </form>
</mat-step> </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> <mat-step>
<ng-template matStepLabel>{{ 'notification.review' | translate }}</ng-template> <ng-template matStepLabel>{{ 'notification.review' | translate }}</ng-template>
<mat-progress-spinner color="warn" mode="indeterminate" <mat-progress-spinner color="warn" mode="indeterminate"
@ -377,6 +482,16 @@
{{ preview.processedTemplates.SLACK.body }} {{ preview.processedTemplates.SLACK.body }}
</div> </div>
</section> </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"> <section class="preview-group">
<div fxLayout="row" fxLayoutGap="8px" fxLayoutAlign="start center"> <div fxLayout="row" fxLayoutGap="8px" fxLayoutAlign="start center">
<mat-icon class="tb-mat-18">supervisor_account</mat-icon> <mat-icon class="tb-mat-18">supervisor_account</mat-icon>
@ -399,7 +514,7 @@
</mat-horizontal-stepper> </mat-horizontal-stepper>
</div> </div>
<mat-divider></mat-divider> <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" <button mat-stroked-button *ngIf="selectedIndex > 0"
(click)="backStep()">{{ 'action.back' | translate }}</button> (click)="backStep()">{{ 'action.back' | translate }}</button>
<span fxFlex></span> <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"; @import "../../../../../../scss/constants";
:host-context(.tb-fullscreen-dialog .mat-mdc-dialog-container) { :host {
width: 820px; width: 820px;
height: 100%; height: 100%;
max-width: 100%; max-width: 100%;
max-height: 100vh; max-height: 100vh;
display: flex; display: grid;
flex-direction: column; 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 { .tb-title {
font-size: 16px; font-size: 16px;
@ -75,6 +95,7 @@
.delivery-method-container { .delivery-method-container {
display: inline-flex; display: inline-flex;
flex: 1 1 calc(50% - 8px); flex: 1 1 calc(50% - 8px);
max-width: calc(50% - 8px);
padding: 16px 12px; padding: 16px 12px;
border: 1px solid rgba(0, 0, 0, 0.1); border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 6px; border-radius: 6px;
@ -177,17 +198,28 @@
line-height: 20px; line-height: 20px;
overflow-x: auto; 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 { :host ::ng-deep {
.mat-mdc-dialog-content { .mat-mdc-dialog-content {
display: flex;
flex-direction: column;
height: 100%;
padding: 0 !important;
.mat-stepper-horizontal { .mat-stepper-horizontal {
display: flex; display: flex;
height: 100%; height: 100%;
@ -208,60 +240,14 @@
} }
} }
} }
}
.mat-button-toggle-group.tb-notification-use-template-toggle-group { .tb-form-panel .mat-expansion-panel.tb-settings {
&.mat-button-toggle-group-appearance-standard { padding: 11px 16px;
border: none;
border-radius: 18px;
margin-bottom: 24px;
.mat-button-toggle + .mat-button-toggle { & > .mat-expansion-panel-content > .mat-expansion-panel-body {
border-left: none; 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 { .preview-group {
@ -272,9 +258,7 @@
} }
} }
} }
}
:host ::ng-deep {
.delivery-methods-container { .delivery-methods-container {
.delivery-method-container { .delivery-method-container {
&.interact * { &.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; emailTemplateForm: FormGroup;
smsTemplateForm: FormGroup; smsTemplateForm: FormGroup;
slackTemplateForm: FormGroup; slackTemplateForm: FormGroup;
microsoftTeamsTemplateForm: FormGroup;
notificationDeliveryMethods = Object.keys(NotificationDeliveryMethod) as NotificationDeliveryMethod[]; notificationDeliveryMethods = Object.keys(NotificationDeliveryMethod) as NotificationDeliveryMethod[];
notificationDeliveryMethodTranslateMap = NotificationDeliveryMethodTranslateMap; notificationDeliveryMethodTranslateMap = NotificationDeliveryMethodTranslateMap;
@ -95,17 +96,9 @@ export abstract class TemplateConfiguration<T, R = any> extends DialogComponent<
icon: this.fb.group({ icon: this.fb.group({
enabled: [false], enabled: [false],
icon: [{value: 'notifications', disabled: true}, Validators.required], icon: [{value: 'notifications', disabled: true}, Validators.required],
color: ['#757575'] 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()
}) })
}); });
@ -114,39 +107,10 @@ export abstract class TemplateConfiguration<T, R = any> extends DialogComponent<
).subscribe((value) => { ).subscribe((value) => {
if (value) { if (value) {
this.webTemplateForm.get('additionalConfig.icon.icon').enable({emitEvent: false}); this.webTemplateForm.get('additionalConfig.icon.icon').enable({emitEvent: false});
this.webTemplateForm.get('additionalConfig.icon.color').enable({emitEvent: false});
} else { } else {
this.webTemplateForm.get('additionalConfig.icon.icon').disable({emitEvent: false}); this.webTemplateForm.get('additionalConfig.icon.icon').disable({emitEvent: false});
} this.webTemplateForm.get('additionalConfig.icon.color').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});
}
} }
}); });
@ -163,11 +127,19 @@ export abstract class TemplateConfiguration<T, R = any> extends DialogComponent<
body: ['', Validators.required] body: ['', Validators.required]
}); });
this.microsoftTeamsTemplateForm = this.fb.group({
subject: [''],
body: ['', Validators.required],
themeColor: [''],
button: this.createButtonConfigForm()
});
this.deliveryMethodFormsMap = new Map<NotificationDeliveryMethod, FormGroup>([ this.deliveryMethodFormsMap = new Map<NotificationDeliveryMethod, FormGroup>([
[NotificationDeliveryMethod.WEB, this.webTemplateForm], [NotificationDeliveryMethod.WEB, this.webTemplateForm],
[NotificationDeliveryMethod.EMAIL, this.emailTemplateForm], [NotificationDeliveryMethod.EMAIL, this.emailTemplateForm],
[NotificationDeliveryMethod.SMS, this.smsTemplateForm], [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); 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-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async"> <mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar> </mat-progress-bar>
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
<div mat-dialog-content> <div mat-dialog-content>
<mat-horizontal-stepper linear #notificationTemplateStepper <mat-horizontal-stepper linear #notificationTemplateStepper
[labelPosition]="(stepperLabelPosition | async)" [labelPosition]="(stepperLabelPosition | async)"
@ -101,73 +100,82 @@
{{ 'notification.message-required' | translate }} {{ 'notification.message-required' | translate }}
</mat-error> </mat-error>
</mat-form-field> </mat-form-field>
<section formGroupName="additionalConfig"> <section formGroupName="additionalConfig" class="tb-form-panel no-padding no-border">
<section formGroupName="icon" class="additional-config-group"> <div class="tb-form-row space-between" formGroupName="icon">
<mat-slide-toggle formControlName="enabled" class="toggle"> <mat-slide-toggle formControlName="enabled" class="mat-slide">
{{ 'icon.icon' | translate }} {{ 'icon.icon' | translate }}
</mat-slide-toggle> </mat-slide-toggle>
<div *ngIf="webTemplateForm.get('additionalConfig.icon.enabled').value" <div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
fxLayout="row" fxLayoutGap.gt-xs="8px" fxLayout.xs="column"> <tb-material-icon-select asBoxInput
<tb-material-icon-select formControlName="icon" required fxFlex> [color]="webTemplateForm.get('additionalConfig.icon.color').value"
formControlName="icon">
</tb-material-icon-select> </tb-material-icon-select>
<tb-color-input formControlName="color" fxFlex> <tb-color-input asBoxInput
formControlName="color">
</tb-color-input> </tb-color-input>
</div> </div>
</section> </div>
<section formGroupName="actionButtonConfig" class="additional-config-group"> <div class="tb-form-panel tb-slide-toggle stroked" formGroupName="actionButtonConfig">
<mat-slide-toggle formControlName="enabled" class="toggle"> <mat-expansion-panel class="tb-settings" [expanded]="webTemplateForm.get('additionalConfig.actionButtonConfig.enabled').value">
{{ 'notification.action-button' | translate }} <mat-expansion-panel-header fxLayout="row wrap" class="fill-width">
</mat-slide-toggle> <mat-panel-title fxFlex="60">
<div *ngIf="webTemplateForm.get('additionalConfig.actionButtonConfig.enabled').value"> <mat-slide-toggle class="mat-slide" formControlName="enabled" (click)="$event.stopPropagation()"
<div fxLayout="row" fxLayoutGap.gt-xs="8px" fxLayout.xs="column"> fxLayoutAlign="center">
<mat-form-field class="mat-block" fxFlex> {{ 'notification.action-button' | translate }}
<mat-label translate>notification.button-text</mat-label> </mat-slide-toggle>
<input matInput formControlName="text" required> </mat-panel-title>
<mat-error *ngIf="webTemplateForm.get('additionalConfig.actionButtonConfig.text').hasError('required')"> </mat-expansion-panel-header>
{{ 'notification.button-text-required' | translate }} <ng-template matExpansionPanelContent class="tb-extension-panel">
</mat-error> <div fxLayout="row" fxLayoutGap.gt-xs="8px" fxLayout.xs="column">
<mat-error *ngIf="webTemplateForm.get('additionalConfig.actionButtonConfig.text').hasError('maxlength')"> <mat-form-field class="mat-block" fxFlex>
{{ 'notification.button-text-max-length' | translate : <mat-label translate>notification.button-text</mat-label>
{length: webTemplateForm.get('additionalConfig.actionButtonConfig.text').getError('maxlength').requiredLength} <input matInput formControlName="text" required>
}} <mat-error *ngIf="webTemplateForm.get('additionalConfig.actionButtonConfig.text').hasError('required')">
</mat-error> {{ 'notification.button-text-required' | translate }}
</mat-form-field> </mat-error>
</div> <mat-error *ngIf="webTemplateForm.get('additionalConfig.actionButtonConfig.text').hasError('maxlength')">
<div fxLayout="row" fxLayoutGap.gt-xs="8px" fxLayout.xs="column"> {{ 'notification.button-text-max-length' | translate :
<mat-form-field fxFlex="30" fxFlex.xs="100"> {length: webTemplateForm.get('additionalConfig.actionButtonConfig.text').getError('maxlength').requiredLength}
<mat-label translate>notification.action-type</mat-label> }}
<mat-select formControlName="linkType"> </mat-error>
<mat-option *ngFor="let actionButtonLinkType of actionButtonLinkTypes" [value]="actionButtonLinkType"> </mat-form-field>
{{ actionButtonLinkTypeTranslateMap.get(actionButtonLinkType) | translate }} </div>
</mat-option> <div fxLayout="row" fxLayoutGap.gt-xs="8px" fxLayout.xs="column">
</mat-select> <mat-form-field fxFlex="30" fxFlex.xs="100">
</mat-form-field> <mat-label translate>notification.action-type</mat-label>
<mat-form-field fxFlex <mat-select formControlName="linkType">
*ngIf="webTemplateForm.get('additionalConfig.actionButtonConfig.linkType').value === actionButtonLinkType.LINK; else dashboardSelector"> <mat-option *ngFor="let actionButtonLinkType of actionButtonLinkTypes" [value]="actionButtonLinkType">
<mat-label translate>notification.link</mat-label> {{ actionButtonLinkTypeTranslateMap.get(actionButtonLinkType) | translate }}
<input matInput formControlName="link" required> </mat-option>
<mat-error *ngIf="webTemplateForm.get('additionalConfig.actionButtonConfig.link').hasError('required')"> </mat-select>
{{ 'notification.link-required' | translate }} </mat-form-field>
</mat-error> <mat-form-field fxFlex
</mat-form-field> *ngIf="webTemplateForm.get('additionalConfig.actionButtonConfig.linkType').value === actionButtonLinkType.LINK; else dashboardSelector">
<ng-template #dashboardSelector> <mat-label translate>notification.link</mat-label>
<tb-dashboard-autocomplete <input matInput formControlName="link" required>
fxFlex="35" fxFlex.xs="100" <mat-error *ngIf="webTemplateForm.get('additionalConfig.actionButtonConfig.link').hasError('required')">
required {{ 'notification.link-required' | translate }}
formControlName="dashboardId"> </mat-error>
</tb-dashboard-autocomplete> </mat-form-field>
<tb-dashboard-state-autocomplete fxFlex="35" fxFlex.xs="100" <ng-template #dashboardSelector>
[dashboardId]="webTemplateForm.get('additionalConfig.actionButtonConfig.dashboardId').value" <tb-dashboard-autocomplete
formControlName="dashboardState"> fxFlex="35" fxFlex.xs="100"
</tb-dashboard-state-autocomplete> required
</ng-template> formControlName="dashboardId">
</div> </tb-dashboard-autocomplete>
<mat-slide-toggle formControlName="setEntityIdInState" class="toggle" <tb-dashboard-state-autocomplete fxFlex="35" fxFlex.xs="100"
*ngIf="webTemplateForm.get('additionalConfig.actionButtonConfig.linkType').value === actionButtonLinkType.DASHBOARD"> [dashboardId]="webTemplateForm.get('additionalConfig.actionButtonConfig.dashboardId').value"
{{ 'notification.set-entity-from-notification' | translate }} formControlName="dashboardState">
</mat-slide-toggle> </tb-dashboard-state-autocomplete>
</div> </ng-template>
</section> </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> </section>
</form> </form>
</mat-step> </mat-step>
@ -259,10 +267,107 @@
</mat-form-field> </mat-form-field>
</form> </form>
</mat-step> </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> </mat-horizontal-stepper>
</div> </div>
<mat-divider></mat-divider> <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" <button mat-stroked-button *ngIf="selectedIndex > 0"
(click)="backStep()">{{ 'action.back' | translate }}</button> (click)="backStep()">{{ 'action.back' | translate }}</button>
<span fxFlex></span> <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 "../../../../../../scss/constants";
@import "../../../../../../theme"; @import "../../../../../../theme";
:host-context(.tb-fullscreen-dialog .mat-mdc-dialog-container) { :host {
width: 800px; width: 840px;
height: 100%; height: 100%;
max-width: 100%; max-width: 100%;
max-height: 100vh; max-height: 100vh;
display: flex; display: grid;
flex-direction: column; 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 { .tb-title {
font-size: 16px; font-size: 16px;
@ -70,6 +90,7 @@
.delivery-method-container { .delivery-method-container {
display: inline-flex; display: inline-flex;
flex: 1 1 calc(50% - 8px); flex: 1 1 calc(50% - 8px);
max-width: calc(50% - 8px);
padding: 16px 12px; padding: 16px 12px;
border: 1px solid rgba(0, 0, 0, 0.1); border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 6px; 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 { :host ::ng-deep {
.mat-mdc-dialog-content { .mat-mdc-dialog-content {
display: flex;
flex-direction: column;
height: 100%;
padding: 0 !important;
.mat-stepper-horizontal { .mat-stepper-horizontal {
display: flex; display: flex;
height: 100%; 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; configuration: NotificationTargetConfig;
} }
export interface NotificationTargetConfig extends Partial<PlatformUsersNotificationTargetConfig & SlackNotificationTargetConfig> { export interface NotificationTargetConfig extends Partial<PlatformUsersNotificationTargetConfig
& SlackNotificationTargetConfig
& MicrosoftTeamsNotificationTargetConfig> {
description?: string; description?: string;
type: NotificationTargetType; type: NotificationTargetType;
} }
@ -276,14 +278,21 @@ export interface SlackNotificationTargetConfig {
conversationType: SlackChanelType; conversationType: SlackChanelType;
conversation: SlackConversation; conversation: SlackConversation;
} }
export interface MicrosoftTeamsNotificationTargetConfig {
webhookUrl: string;
channelName: string;
}
export enum NotificationTargetType { export enum NotificationTargetType {
PLATFORM_USERS = 'PLATFORM_USERS', PLATFORM_USERS = 'PLATFORM_USERS',
SLACK = 'SLACK' SLACK = 'SLACK',
MICROSOFT_TEAMS = 'MICROSOFT_TEAMS'
} }
export const NotificationTargetTypeTranslationMap = new Map<NotificationTargetType, string>([ export const NotificationTargetTypeTranslationMap = new Map<NotificationTargetType, string>([
[NotificationTargetType.PLATFORM_USERS, 'notification.platform-users'], [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> { export interface NotificationTemplate extends Omit<BaseData<NotificationTemplateId>, 'label'>, ExportableEntity<NotificationTemplateId> {
@ -299,14 +308,17 @@ interface NotificationTemplateConfig {
} }
export interface DeliveryMethodNotificationTemplate extends export interface DeliveryMethodNotificationTemplate extends
Partial<WebDeliveryMethodNotificationTemplate & EmailDeliveryMethodNotificationTemplate & SlackDeliveryMethodNotificationTemplate>{ Partial<WebDeliveryMethodNotificationTemplate
body?: string; & EmailDeliveryMethodNotificationTemplate
& SlackDeliveryMethodNotificationTemplate
& MicrosoftTeamsDeliveryMethodNotificationTemplate>{
body: string;
enabled: boolean; enabled: boolean;
method: NotificationDeliveryMethod; method: NotificationDeliveryMethod;
} }
interface WebDeliveryMethodNotificationTemplate { interface WebDeliveryMethodNotificationTemplate {
subject?: string; subject: string;
additionalConfig: WebDeliveryMethodAdditionalConfig; additionalConfig: WebDeliveryMethodAdditionalConfig;
} }
@ -316,15 +328,17 @@ interface WebDeliveryMethodAdditionalConfig {
icon: string; icon: string;
color: string; color: string;
}; };
actionButtonConfig: { actionButtonConfig: NotificationButtonConfig;
enabled: boolean; }
text: string;
linkType: ActionButtonLinkType; interface NotificationButtonConfig {
link?: string; enabled: boolean;
dashboardId?: string; text: string;
dashboardState?: string; linkType: ActionButtonLinkType;
setEntityIdInState?: boolean; link?: string;
}; dashboardId?: string;
dashboardState?: string;
setEntityIdInState?: boolean;
} }
interface EmailDeliveryMethodNotificationTemplate { interface EmailDeliveryMethodNotificationTemplate {
@ -336,6 +350,11 @@ interface SlackDeliveryMethodNotificationTemplate {
conversationId: string; conversationId: string;
} }
interface MicrosoftTeamsDeliveryMethodNotificationTemplate {
subject?: string;
button: NotificationButtonConfig;
}
export enum NotificationStatus { export enum NotificationStatus {
SENT = 'SENT', SENT = 'SENT',
READ = 'READ' READ = 'READ'
@ -345,14 +364,16 @@ export enum NotificationDeliveryMethod {
WEB = 'WEB', WEB = 'WEB',
SMS = 'SMS', SMS = 'SMS',
EMAIL = 'EMAIL', EMAIL = 'EMAIL',
SLACK = 'SLACK' SLACK = 'SLACK',
MICROSOFT_TEAMS = 'MICROSOFT_TEAMS'
} }
export const NotificationDeliveryMethodTranslateMap = new Map<NotificationDeliveryMethod, string>([ export const NotificationDeliveryMethodTranslateMap = new Map<NotificationDeliveryMethod, string>([
[NotificationDeliveryMethod.WEB, 'notification.delivery-method.web'], [NotificationDeliveryMethod.WEB, 'notification.delivery-method.web'],
[NotificationDeliveryMethod.SMS, 'notification.delivery-method.sms'], [NotificationDeliveryMethod.SMS, 'notification.delivery-method.sms'],
[NotificationDeliveryMethod.EMAIL, 'notification.delivery-method.email'], [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 { export enum NotificationRequestStatus {

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

@ -2967,6 +2967,8 @@
"email-preview": "Email notification preview", "email-preview": "Email notification preview",
"slack": "Slack", "slack": "Slack",
"slack-preview": "Slack notification preview", "slack-preview": "Slack notification preview",
"microsoft-teams": "Microsoft Teams",
"microsoft-teams-preview": "Microsoft Teams notification preview",
"sms": "SMS", "sms": "SMS",
"sms-preview": "SMS notification preview", "sms-preview": "SMS notification preview",
"web": "Web", "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", "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", "tenants-list-rule-hint": "If the field is empty, the trigger will be applied to all tenants",
"threshold": "Threshold", "threshold": "Threshold",
"theme-color": "Theme color",
"time": "Time", "time": "Time",
"track-rule-node-events": "Track rule node events", "track-rule-node-events": "Track rule node events",
"trigger": { "trigger": {
@ -3154,6 +3157,10 @@
"use-template": "Use template", "use-template": "Use template",
"view-all": "View all", "view-all": "View all",
"warning": "Warning", "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": { "settings": {
"notification-settings": "Notification settings", "notification-settings": "Notification settings",
"reset-all": "Reset all settings", "reset-all": "Reset all settings",

Loading…
Cancel
Save