Slack recipients preview and recipient params; refactoring

This commit is contained in:
ViacheslavKlimov 2023-04-03 12:21:09 +03:00
parent 299c159ca1
commit dc3ca22e63
19 changed files with 141 additions and 70 deletions

View File

@ -47,6 +47,7 @@ import org.thingsboard.server.common.data.notification.targets.NotificationRecip
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.platform.PlatformUsersNotificationTargetConfig;
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.NotificationTemplate;
import org.thingsboard.server.common.data.page.PageData;
@ -230,28 +231,34 @@ public class NotificationController extends BaseController {
preview.setProcessedTemplates(processedTemplates);
// generic permission
Set<User> recipientsPreview = new LinkedHashSet<>();
Set<String> recipientsPreview = new LinkedHashSet<>();
Map<String, Integer> recipientsCountByTarget = new HashMap<>();
List<NotificationTarget> targets = notificationTargetService.findNotificationTargetsByTenantIdAndIds(user.getTenantId(),
request.getTargets().stream().map(NotificationTargetId::new).collect(Collectors.toList()));
for (NotificationTarget target : targets) {
int recipientsCount;
List<NotificationRecipient> recipientsPart;
if (target.getConfiguration().getType() == NotificationTargetType.PLATFORM_USERS) {
PageData<User> recipients = notificationTargetService.findRecipientsForNotificationTargetConfig(user.getTenantId(),
(PlatformUsersNotificationTargetConfig) target.getConfiguration(), new PageLink(recipientsPreviewSize));
recipientsCount = (int) recipients.getTotalElements();
for (User recipient : recipients.getData()) {
recipientsPart = recipients.getData().stream().map(r -> (NotificationRecipient) r).collect(Collectors.toList());
} else {
recipientsCount = 1;
recipientsPart = List.of(((SlackNotificationTargetConfig) target.getConfiguration()).getConversation());
}
for (NotificationRecipient recipient : recipientsPart) {
if (recipientsPreview.size() < recipientsPreviewSize) {
recipientsPreview.add(recipient);
recipientsPreview.add(recipient.getTitle());
} else {
break;
}
}
} else {
recipientsCount = 1;
}
recipientsCountByTarget.put(target.getName(), recipientsCount);
}
preview.setRecipientsPreview(recipientsPreview);
preview.setRecipientsCountByTarget(recipientsCountByTarget);
preview.setTotalRecipientsCount(recipientsCountByTarget.values().stream().mapToInt(Integer::intValue).sum());

View File

@ -25,9 +25,10 @@ import org.thingsboard.rule.engine.api.NotificationCenter;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.NotificationRequestId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.notification.NotificationRequest;
import org.thingsboard.server.common.data.notification.NotificationRequestConfig;
import org.thingsboard.server.common.data.notification.NotificationRequestStats;
import org.thingsboard.server.common.data.notification.NotificationRequestStatus;
import org.thingsboard.server.common.data.page.PageDataIterable;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
@ -112,11 +113,9 @@ public class DefaultNotificationSchedulerService extends AbstractPartitionBasedS
notificationCenter.processNotificationRequest(tenantId, notificationRequest);
} catch (Exception e) {
log.error("Failed to process scheduled notification request {}", notificationRequest.getId(), e);
UserId senderId = notificationRequest.getSenderId();
if (senderId != null) {
notificationCenter.sendBasicNotification(tenantId, senderId, "Notification failure",
"Failed to process scheduled notification (request " + notificationRequest.getId() + "): " + e.getMessage());
}
NotificationRequestStats stats = new NotificationRequestStats();
stats.setError(e.getMessage());
notificationRequestService.updateNotificationRequest(tenantId, request.getId(), NotificationRequestStatus.SENT, stats);
}
});
scheduledNotificationRequests.remove(notificationRequest.getId());

View File

@ -20,13 +20,10 @@ import lombok.Builder;
import lombok.Getter;
import org.apache.commons.collections4.MapUtils;
import org.apache.commons.lang3.StringUtils;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod;
import org.thingsboard.server.common.data.notification.NotificationRequest;
import org.thingsboard.server.common.data.notification.NotificationRequestStats;
import org.thingsboard.server.common.data.notification.info.RuleOriginatedNotificationInfo;
import org.thingsboard.server.common.data.notification.settings.NotificationDeliveryMethodConfig;
import org.thingsboard.server.common.data.notification.settings.NotificationSettings;
import org.thingsboard.server.common.data.notification.targets.NotificationRecipient;
@ -36,7 +33,6 @@ import org.thingsboard.server.common.data.notification.template.NotificationTemp
import org.thingsboard.server.common.data.notification.template.NotificationTemplateConfig;
import org.thingsboard.server.common.data.notification.template.WebDeliveryMethodNotificationTemplate;
import java.util.Collections;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.Map;
@ -155,23 +151,11 @@ public class NotificationProcessingContext {
}
private Map<String, String> createTemplateContextForRecipient(NotificationRecipient recipient) {
if (recipient instanceof User) {
User user = (User) recipient;
return Map.of(
"recipientEmail", user.getEmail(),
"recipientFirstName", Strings.nullToEmpty(user.getFirstName()),
"recipientLastName", Strings.nullToEmpty(user.getLastName())
"recipientEmail", Strings.nullToEmpty(recipient.getEmail()),
"recipientFirstName", Strings.nullToEmpty(recipient.getFirstName()),
"recipientLastName", Strings.nullToEmpty(recipient.getLastName())
);
}
return Collections.emptyMap();
}
public CustomerId getCustomerId() {
if (request.getInfo() instanceof RuleOriginatedNotificationInfo) {
return ((RuleOriginatedNotificationInfo) request.getInfo()).getAffectedCustomerId();
} else {
return null;
}
}
}

View File

@ -19,6 +19,7 @@ import com.google.common.util.concurrent.ListenableFuture;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import org.thingsboard.rule.engine.api.MailService;
import org.thingsboard.rule.engine.api.TbEmail;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod;
@ -36,7 +37,12 @@ public class EmailNotificationChannel implements NotificationChannel<User, Email
@Override
public ListenableFuture<Void> sendNotification(User recipient, EmailDeliveryMethodNotificationTemplate processedTemplate, NotificationProcessingContext ctx) {
return executor.submit(() -> {
mailService.sendEmail(recipient.getTenantId(), recipient.getEmail(), processedTemplate.getSubject(), processedTemplate.getBody());
mailService.send(recipient.getTenantId(), null, TbEmail.builder()
.to(recipient.getEmail())
.subject(processedTemplate.getSubject())
.body(processedTemplate.getBody())
.html(true)
.build());
return null;
});
}

View File

@ -44,6 +44,8 @@ import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import static org.apache.commons.lang3.StringUtils.isNotEmpty;
@Service
@RequiredArgsConstructor
public class DefaultSlackService implements SlackService {
@ -80,7 +82,14 @@ public class DefaultSlackService implements SlackService {
.map(user -> {
SlackConversation conversation = new SlackConversation();
conversation.setId(user.getId());
conversation.setName(String.format("@%s (%s)", user.getName(), user.getRealName()));
conversation.setShortName(user.getName());
conversation.setWholeName(user.getProfile() != null ? user.getProfile().getRealNameNormalized() : user.getRealName());
conversation.setEmail(user.getProfile() != null ? user.getProfile().getEmail() : null);
String title = "@" + conversation.getShortName();
if (isNotEmpty(conversation.getWholeName()) && !conversation.getWholeName().equals(conversation.getShortName())) {
title += " (" + conversation.getWholeName() + ")";
}
conversation.setTitle(title);
return conversation;
})
.collect(Collectors.toList());
@ -99,7 +108,9 @@ public class DefaultSlackService implements SlackService {
.map(channel -> {
SlackConversation conversation = new SlackConversation();
conversation.setId(channel.getId());
conversation.setName("#" + channel.getName());
conversation.setShortName(channel.getName());
conversation.setWholeName(channel.getNameNormalized());
conversation.setTitle("#" + channel.getName());
return conversation;
})
.collect(Collectors.toList());
@ -111,7 +122,7 @@ public class DefaultSlackService implements SlackService {
public SlackConversation findConversation(TenantId tenantId, String token, SlackConversationType conversationType, String namePattern) {
List<SlackConversation> conversations = listConversations(tenantId, token, conversationType);
return conversations.stream()
.filter(conversation -> StringUtils.containsIgnoreCase(conversation.getName(), namePattern))
.filter(conversation -> StringUtils.containsIgnoreCase(conversation.getTitle(), namePattern))
.findFirst().orElse(null);
}

View File

@ -498,7 +498,10 @@ public class NotificationApiTest extends AbstractNotificationApiTest {
notificationTarget.setTenantId(tenantId);
notificationTarget.setName(conversationName + " in Slack");
SlackNotificationTargetConfig targetConfig = new SlackNotificationTargetConfig();
targetConfig.setConversation(new SlackConversation(conversationId, conversationName));
targetConfig.setConversation(SlackConversation.builder()
.id(conversationId)
.title(conversationName)
.build());
notificationTarget.setConfiguration(targetConfig);
notificationTarget = saveNotificationTarget(notificationTarget);

View File

@ -30,6 +30,8 @@ import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.validation.Length;
import org.thingsboard.server.common.data.validation.NoXss;
import static org.apache.commons.lang3.StringUtils.isNotEmpty;
@ApiModel
@EqualsAndHashCode(callSuper = true)
public class User extends SearchTextBasedWithAdditionalInfo<UserId> implements HasName, HasTenantId, HasCustomerId, NotificationRecipient {
@ -165,6 +167,24 @@ public class User extends SearchTextBasedWithAdditionalInfo<UserId> implements H
return getEmail();
}
@JsonIgnore
public String getTitle() {
String title = "";
if (isNotEmpty(firstName)) {
title += firstName;
}
if (isNotEmpty(lastName)) {
if (!title.isEmpty()) {
title += " ";
}
title += lastName;
}
if (title.isEmpty()) {
title = email;
}
return title;
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();

View File

@ -16,7 +16,6 @@
package org.thingsboard.server.common.data.notification;
import lombok.Data;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.notification.template.DeliveryMethodNotificationTemplate;
import java.util.Collection;
@ -28,6 +27,6 @@ public class NotificationRequestPreview {
private Map<NotificationDeliveryMethod, DeliveryMethodNotificationTemplate> processedTemplates;
private int totalRecipientsCount;
private Map<String, Integer> recipientsCountByTarget;
private Collection<User> recipientsPreview;
private Collection<String> recipientsPreview;
}

View File

@ -19,7 +19,6 @@ import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.notification.targets.NotificationRecipient;
import java.util.Collections;
@ -33,6 +32,7 @@ public class NotificationRequestStats {
private final Map<NotificationDeliveryMethod, AtomicInteger> sent;
private final Map<NotificationDeliveryMethod, Map<String, String>> errors;
private String error;
@JsonIgnore
private final Map<NotificationDeliveryMethod, Set<Object>> processedRecipients;
@ -44,9 +44,11 @@ public class NotificationRequestStats {
@JsonCreator
public NotificationRequestStats(@JsonProperty("sent") Map<NotificationDeliveryMethod, AtomicInteger> sent,
@JsonProperty("errors") Map<NotificationDeliveryMethod, Map<String, String>> errors) {
@JsonProperty("errors") Map<NotificationDeliveryMethod, Map<String, String>> errors,
@JsonProperty("error") String error) {
this.sent = sent;
this.errors = errors;
this.error = error;
this.processedRecipients = Collections.emptyMap();
}
@ -60,13 +62,7 @@ public class NotificationRequestStats {
return;
}
String errorMessage = error.getMessage();
String key;
if (recipient instanceof User) {
key = ((User) recipient).getEmail();
} else {
key = "";
}
errors.computeIfAbsent(deliveryMethod, k -> new ConcurrentHashMap<>()).put(key, errorMessage);
errors.computeIfAbsent(deliveryMethod, k -> new ConcurrentHashMap<>()).put(recipient.getTitle(), errorMessage);
}
public boolean contains(NotificationDeliveryMethod deliveryMethod, Object recipientId) {

View File

@ -19,4 +19,12 @@ public interface NotificationRecipient {
Object getId();
String getTitle();
String getFirstName();
String getLastName();
String getEmail();
}

View File

@ -15,21 +15,47 @@
*/
package org.thingsboard.server.common.data.notification.targets.slack;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.thingsboard.server.common.data.notification.targets.NotificationRecipient;
import javax.validation.constraints.NotEmpty;
import static org.apache.commons.lang3.StringUtils.isEmpty;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class SlackConversation implements NotificationRecipient {
@NotEmpty
private String id;
@NotEmpty
private String name;
private String title;
private String shortName;
private String wholeName;
private String email;
@JsonIgnore
@Override
public String getFirstName() {
String firstName = StringUtils.contains(wholeName, " ") ? wholeName.split(" ")[0] : wholeName;
if (isEmpty(firstName)) {
firstName = shortName;
}
return firstName;
}
@JsonIgnore
@Override
public String getLastName() {
return StringUtils.contains(wholeName, " ") ? wholeName.split(" ")[1] : null;
}
}

View File

@ -17,6 +17,7 @@ package org.thingsboard.server.dao.notification;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.HasId;
@ -32,9 +33,11 @@ import org.thingsboard.server.dao.entity.EntityDaoService;
import org.thingsboard.server.dao.notification.cache.NotificationRuleCacheKey;
import org.thingsboard.server.dao.notification.cache.NotificationRuleCacheValue;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
@ -96,6 +99,7 @@ public class DefaultNotificationRuleService extends AbstractCachedEntityService<
.getNotificationRules();
}
@Transactional
@Override
public void deleteNotificationRuleById(TenantId tenantId, NotificationRuleId id) {
NotificationRule notificationRule = findNotificationRuleById(tenantId, id);
@ -106,6 +110,14 @@ public class DefaultNotificationRuleService extends AbstractCachedEntityService<
@Override
public void deleteNotificationRulesByTenantId(TenantId tenantId) {
notificationRuleDao.removeByTenantId(tenantId);
List<NotificationRuleCacheKey> cacheKeys = Arrays.stream(NotificationRuleTriggerType.values())
.map(triggerType -> NotificationRuleCacheKey.builder()
.tenantId(tenantId)
.triggerType(triggerType)
.build())
.collect(Collectors.toList());
cache.evict(cacheKeys);
}
@Override

View File

@ -216,8 +216,8 @@ public class DefaultNotificationSettingsService implements NotificationSettingsS
List.of(affectedUser.getId()), "Send notification to user when any alarm was assigned to him");
NotificationTemplate ruleEngineComponentLifecycleFailureNotificationTemplate = createTemplate(tenantId, "Rule chain/node lifecycle failure notification", NotificationType.RULE_ENGINE_COMPONENT_LIFECYCLE_EVENT,
"${action:capitalize} failure in Rule chain '${ruleChainName}'",
"${componentType} '${componentName}' failed to ${action}",
"Rule chain '${ruleChainName}' - ${action} failure:<br/>${error}",
"warning", "Go to rule chain", "/ruleChains/${ruleChainId}");
RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig ruleEngineComponentLifecycleEventRuleTriggerConfig = new RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig();
ruleEngineComponentLifecycleEventRuleTriggerConfig.setRuleChains(null);

View File

@ -343,11 +343,8 @@
</div>
<mat-divider class="divider"></mat-divider>
<mat-chip-listbox>
<mat-chip *ngFor="let user of preview.recipientsPreview">
<span *ngIf="(user.firstName || user.lastName); else email">{{ user.firstName }}&nbsp;{{ user.lastName }}</span>
<ng-template #email>
{{ user.email }}
</ng-template>
<mat-chip *ngFor="let recipientTitle of preview.recipientsPreview">
<span>{{ recipientTitle }}</span>
</mat-chip>
</mat-chip-listbox>
</section>

View File

@ -172,7 +172,7 @@ export class SentTableConfigResolver implements Resolve<EntityTableConfig<Notifi
}
return `<div style="border-radius: 12px; height: 24px; line-height: 24px; padding: 0 10px; width: max-content; cursor: pointer;
background-color: #D12730; color: #fff; font-weight: 500; margin-left: 8px" class="stats">
${countError} ${this.translate.instant('notification.fails')} >
${this.translate.instant('notification.fails', {count: countError})}
</div>`;
}

View File

@ -33,7 +33,7 @@
[displayWith]="displaySlackConversationFn"
>
<mat-option *ngFor="let template of slackConversation$ | async" [value]="template" class="template-option">
<span [innerHTML]="template.name | highlight:slackSearchText"></span>
<span [innerHTML]="template.title | highlight:slackSearchText"></span>
</mat-option>
<mat-option *ngIf="!(slackConversation$ | async)?.length" [value]="null" class="tb-not-found">
<div class="tb-not-found-content" (click)="$event.stopPropagation()">

View File

@ -177,7 +177,7 @@ export class SlackConversationAutocompleteComponent implements ControlValueAcces
}
displaySlackConversationFn(slackConversation?: SlackConversation): string | undefined {
return slackConversation ? slackConversation.name : undefined;
return slackConversation ? slackConversation.title : undefined;
}
private fetchSlackConversation(searchText?: string): Observable<Array<SlackConversation>> {
@ -215,7 +215,7 @@ export class SlackConversationAutocompleteComponent implements ControlValueAcces
private createSlackConversationFilter(query: string): (key: SlackConversation) => boolean {
const lowercaseQuery = query.toLowerCase();
return key => key.name.toLowerCase().includes(lowercaseQuery);
return key => key.title.toLowerCase().includes(lowercaseQuery);
}
private clearSlackCache(): void {

View File

@ -72,7 +72,7 @@ export interface NotificationRequestPreview {
totalRecipientsCount: number;
recipientsCountByTarget: { [key in string]: number };
processedTemplates: { [key in NotificationDeliveryMethod]: DeliveryMethodNotificationTemplate };
recipientsPreview: Array<User>;
recipientsPreview: Array<string>;
}
export interface NotificationRequestStats {
@ -100,7 +100,10 @@ interface SlackNotificationDeliveryMethodConfig {
export interface SlackConversation {
id: string;
name: string;
title: string;
shortName: string;
wholeName: string;
email: string;
}
export interface NotificationRule extends Omit<BaseData<NotificationRuleId>, 'label'>{

View File

@ -2784,16 +2784,16 @@
"delivery-method": {
"delivery-method": "Delivery method",
"email": "Email",
"email-failed-sent": "Email messages failed sent",
"email-failed-sent": "Email",
"email-preview": "Email notification preview",
"slack": "Slack",
"slack-failed-sent": "Slack messages failed sent",
"slack-failed-sent": "Slack",
"slack-preview": "Slack notification preview",
"sms": "SMS",
"sms-failed-sent": "SMS messages failed sent",
"sms-failed-sent": "SMS",
"sms-preview": "SMS notification preview",
"web": "Web",
"web-failed-sent": "Web messages failed sent",
"web-failed-sent": "Web",
"web-preview": "Web notification preview"
},
"delivery-methods": "Delivery methods",
@ -2811,8 +2811,8 @@
"entity-action-trigger-settings": "Entity action trigger settings",
"entity-type": "Entity type",
"escalation-chain": "Escalation chain",
"failed-send": "Failed send",
"fails": "Fails",
"failed-send": "Sending failures",
"fails": "{ count, plural, =1 {1 fail} other {# fails} }",
"filter": "Filter",
"first-recipient": "First recipient",
"inactive": "Inactive",