Browse Source

Device activity notification rule; fix default notification configs

pull/8265/head
ViacheslavKlimov 4 years ago
parent
commit
de24af159c
  1. 20
      application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/DeviceActivityTriggerProcessor.java
  2. 4
      application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/EntityActionTriggerProcessor.java
  3. 2
      common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationType.java
  4. 4
      common/data/src/main/java/org/thingsboard/server/common/data/notification/info/DeviceActivityNotificationInfo.java
  5. 14
      common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/DeviceActivityNotificationRuleTriggerConfig.java
  6. 4
      common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/EntityActionNotificationRuleTriggerConfig.java
  7. 2
      common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/NotificationRuleTriggerConfig.java
  8. 2
      common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/NotificationRuleTriggerType.java
  9. 34
      dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationSettingsService.java
  10. 3
      dao/src/test/java/org/thingsboard/server/dao/service/BaseAdminSettingsServiceTest.java
  11. 4
      ui-ngx/src/assets/help/en_US/notification/device_activity.md

20
application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/DeviceInactivityTriggerProcessor.java → application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/DeviceActivityTriggerProcessor.java

@ -22,9 +22,10 @@ import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.notification.info.DeviceInactivityNotificationInfo; import org.thingsboard.server.common.data.notification.info.DeviceActivityNotificationInfo;
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.rule.trigger.DeviceInactivityNotificationRuleTriggerConfig; import org.thingsboard.server.common.data.notification.rule.trigger.DeviceActivityNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.DeviceActivityNotificationRuleTriggerConfig.DeviceEvent;
import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType; import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType;
import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.dao.notification.trigger.RuleEngineMsgTrigger; import org.thingsboard.server.dao.notification.trigger.RuleEngineMsgTrigger;
@ -34,12 +35,16 @@ import java.util.Set;
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
public class DeviceInactivityTriggerProcessor implements RuleEngineMsgNotificationRuleTriggerProcessor<DeviceInactivityNotificationRuleTriggerConfig> { public class DeviceActivityTriggerProcessor implements RuleEngineMsgNotificationRuleTriggerProcessor<DeviceActivityNotificationRuleTriggerConfig> {
private final TbDeviceProfileCache deviceProfileCache; private final TbDeviceProfileCache deviceProfileCache;
@Override @Override
public boolean matchesFilter(RuleEngineMsgTrigger trigger, DeviceInactivityNotificationRuleTriggerConfig triggerConfig) { public boolean matchesFilter(RuleEngineMsgTrigger trigger, DeviceActivityNotificationRuleTriggerConfig triggerConfig) {
DeviceEvent event = trigger.getMsg().getType().equals(DataConstants.ACTIVITY_EVENT) ? DeviceEvent.ACTIVE : DeviceEvent.INACTIVE;
if (!triggerConfig.getNotifyOn().contains(event)) {
return false;
}
DeviceId deviceId = (DeviceId) trigger.getMsg().getOriginator(); DeviceId deviceId = (DeviceId) trigger.getMsg().getOriginator();
if (CollectionUtils.isNotEmpty(triggerConfig.getDevices())) { if (CollectionUtils.isNotEmpty(triggerConfig.getDevices())) {
return triggerConfig.getDevices().contains(deviceId.getId()); return triggerConfig.getDevices().contains(deviceId.getId());
@ -54,7 +59,8 @@ public class DeviceInactivityTriggerProcessor implements RuleEngineMsgNotificati
@Override @Override
public RuleOriginatedNotificationInfo constructNotificationInfo(RuleEngineMsgTrigger trigger) { public RuleOriginatedNotificationInfo constructNotificationInfo(RuleEngineMsgTrigger trigger) {
TbMsg msg = trigger.getMsg(); TbMsg msg = trigger.getMsg();
return DeviceInactivityNotificationInfo.builder() return DeviceActivityNotificationInfo.builder()
.eventType(trigger.getMsg().getType().equals(DataConstants.ACTIVITY_EVENT) ? "active" : "inactive")
.deviceId(msg.getOriginator().getId()) .deviceId(msg.getOriginator().getId())
.deviceName(msg.getMetaData().getValue("deviceName")) .deviceName(msg.getMetaData().getValue("deviceName"))
.deviceType(msg.getMetaData().getValue("deviceType")) .deviceType(msg.getMetaData().getValue("deviceType"))
@ -65,12 +71,12 @@ public class DeviceInactivityTriggerProcessor implements RuleEngineMsgNotificati
@Override @Override
public NotificationRuleTriggerType getTriggerType() { public NotificationRuleTriggerType getTriggerType() {
return NotificationRuleTriggerType.DEVICE_INACTIVITY; return NotificationRuleTriggerType.DEVICE_ACTIVITY;
} }
@Override @Override
public Set<String> getSupportedMsgTypes() { public Set<String> getSupportedMsgTypes() {
return Set.of(DataConstants.INACTIVITY_EVENT); return Set.of(DataConstants.ACTIVITY_EVENT, DataConstants.INACTIVITY_EVENT);
} }
} }

4
application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/EntityActionTriggerProcessor.java

@ -30,6 +30,8 @@ import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.UUID; import java.util.UUID;
import static org.apache.commons.collections.CollectionUtils.isEmpty;
@Service @Service
public class EntityActionTriggerProcessor implements RuleEngineMsgNotificationRuleTriggerProcessor<EntityActionNotificationRuleTriggerConfig> { public class EntityActionTriggerProcessor implements RuleEngineMsgNotificationRuleTriggerProcessor<EntityActionNotificationRuleTriggerConfig> {
@ -51,7 +53,7 @@ public class EntityActionTriggerProcessor implements RuleEngineMsgNotificationRu
} else { } else {
return false; return false;
} }
return triggerConfig.getEntityType() == null || getEntityType(trigger.getMsg()) == triggerConfig.getEntityType(); return isEmpty(triggerConfig.getEntityTypes()) || triggerConfig.getEntityTypes().contains(getEntityType(trigger.getMsg()));
} }
@Override @Override

2
common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationType.java

@ -19,7 +19,7 @@ public enum NotificationType {
GENERAL, GENERAL,
ALARM, ALARM,
DEVICE_INACTIVITY, DEVICE_ACTIVITY,
ENTITY_ACTION, ENTITY_ACTION,
ALARM_COMMENT, ALARM_COMMENT,
RULE_ENGINE_COMPONENT_LIFECYCLE_EVENT, RULE_ENGINE_COMPONENT_LIFECYCLE_EVENT,

4
common/data/src/main/java/org/thingsboard/server/common/data/notification/info/DeviceInactivityNotificationInfo.java → common/data/src/main/java/org/thingsboard/server/common/data/notification/info/DeviceActivityNotificationInfo.java

@ -32,8 +32,9 @@ import static org.thingsboard.server.common.data.util.CollectionsUtil.mapOf;
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
@Builder @Builder
public class DeviceInactivityNotificationInfo implements RuleOriginatedNotificationInfo { public class DeviceActivityNotificationInfo implements RuleOriginatedNotificationInfo {
private String eventType;
private UUID deviceId; private UUID deviceId;
private String deviceName; private String deviceName;
private String deviceLabel; private String deviceLabel;
@ -43,6 +44,7 @@ public class DeviceInactivityNotificationInfo implements RuleOriginatedNotificat
@Override @Override
public Map<String, String> getTemplateData() { public Map<String, String> getTemplateData() {
return mapOf( return mapOf(
"eventType", eventType,
"deviceId", deviceId.toString(), "deviceId", deviceId.toString(),
"deviceName", deviceName, "deviceName", deviceName,
"deviceLabel", deviceLabel, "deviceLabel", deviceLabel,

14
common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/DeviceInactivityNotificationRuleTriggerConfig.java → common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/DeviceActivityNotificationRuleTriggerConfig.java

@ -17,19 +17,25 @@ package org.thingsboard.server.common.data.notification.rule.trigger;
import lombok.Data; import lombok.Data;
import javax.validation.constraints.NotEmpty;
import java.util.Set; import java.util.Set;
import java.util.UUID; import java.util.UUID;
@Data @Data
public class DeviceInactivityNotificationRuleTriggerConfig implements NotificationRuleTriggerConfig { public class DeviceActivityNotificationRuleTriggerConfig implements NotificationRuleTriggerConfig {
private Set<UUID> devices; private Set<UUID> devices;
private Set<UUID> deviceProfiles; private Set<UUID> deviceProfiles; // set either devices or profiles
// set either devices or profiles @NotEmpty
private Set<DeviceEvent> notifyOn;
@Override @Override
public NotificationRuleTriggerType getTriggerType() { public NotificationRuleTriggerType getTriggerType() {
return NotificationRuleTriggerType.DEVICE_INACTIVITY; return NotificationRuleTriggerType.DEVICE_ACTIVITY;
}
public enum DeviceEvent {
ACTIVE, INACTIVE
} }
} }

4
common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/EntityActionNotificationRuleTriggerConfig.java

@ -18,10 +18,12 @@ package org.thingsboard.server.common.data.notification.rule.trigger;
import lombok.Data; import lombok.Data;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import java.util.Set;
@Data @Data
public class EntityActionNotificationRuleTriggerConfig implements NotificationRuleTriggerConfig { public class EntityActionNotificationRuleTriggerConfig implements NotificationRuleTriggerConfig {
private EntityType entityType; // maybe add name filter ? private Set<EntityType> entityTypes; // maybe add name filter ?
private boolean created; private boolean created;
private boolean updated; private boolean updated;
private boolean deleted; private boolean deleted;

2
common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/NotificationRuleTriggerConfig.java

@ -26,7 +26,7 @@ import java.io.Serializable;
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "triggerType") @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "triggerType")
@JsonSubTypes({ @JsonSubTypes({
@Type(value = AlarmNotificationRuleTriggerConfig.class, name = "ALARM"), @Type(value = AlarmNotificationRuleTriggerConfig.class, name = "ALARM"),
@Type(value = DeviceInactivityNotificationRuleTriggerConfig.class, name = "DEVICE_INACTIVITY"), @Type(value = DeviceActivityNotificationRuleTriggerConfig.class, name = "DEVICE_INACTIVITY"),
@Type(value = EntityActionNotificationRuleTriggerConfig.class, name = "ENTITY_ACTION"), @Type(value = EntityActionNotificationRuleTriggerConfig.class, name = "ENTITY_ACTION"),
@Type(value = AlarmCommentNotificationRuleTriggerConfig.class, name = "ALARM_COMMENT"), @Type(value = AlarmCommentNotificationRuleTriggerConfig.class, name = "ALARM_COMMENT"),
@Type(value = RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig.class, name = "RULE_ENGINE_COMPONENT_LIFECYCLE_EVENT"), @Type(value = RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig.class, name = "RULE_ENGINE_COMPONENT_LIFECYCLE_EVENT"),

2
common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/NotificationRuleTriggerType.java

@ -22,7 +22,7 @@ public enum NotificationRuleTriggerType {
ALARM, ALARM,
ALARM_COMMENT, ALARM_COMMENT,
DEVICE_INACTIVITY, DEVICE_ACTIVITY,
ENTITY_ACTION, ENTITY_ACTION,
RULE_ENGINE_COMPONENT_LIFECYCLE_EVENT, RULE_ENGINE_COMPONENT_LIFECYCLE_EVENT,
ALARM_ASSIGNMENT, ALARM_ASSIGNMENT,

34
dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationSettingsService.java

@ -40,7 +40,8 @@ import org.thingsboard.server.common.data.notification.rule.trigger.AlarmAssignm
import org.thingsboard.server.common.data.notification.rule.trigger.AlarmCommentNotificationRuleTriggerConfig; import org.thingsboard.server.common.data.notification.rule.trigger.AlarmCommentNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.AlarmNotificationRuleTriggerConfig; import org.thingsboard.server.common.data.notification.rule.trigger.AlarmNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.AlarmNotificationRuleTriggerConfig.AlarmAction; import org.thingsboard.server.common.data.notification.rule.trigger.AlarmNotificationRuleTriggerConfig.AlarmAction;
import org.thingsboard.server.common.data.notification.rule.trigger.DeviceInactivityNotificationRuleTriggerConfig; import org.thingsboard.server.common.data.notification.rule.trigger.DeviceActivityNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.DeviceActivityNotificationRuleTriggerConfig.DeviceEvent;
import org.thingsboard.server.common.data.notification.rule.trigger.EntitiesLimitNotificationRuleTriggerConfig; import org.thingsboard.server.common.data.notification.rule.trigger.EntitiesLimitNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.EntityActionNotificationRuleTriggerConfig; import org.thingsboard.server.common.data.notification.rule.trigger.EntityActionNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerConfig; import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerConfig;
@ -155,26 +156,27 @@ public class DefaultNotificationSettingsService implements NotificationSettingsS
NotificationTemplate deviceActionNotificationTemplate = createTemplate(tenantId, "Device action notification", NotificationType.ENTITY_ACTION, NotificationTemplate deviceActionNotificationTemplate = createTemplate(tenantId, "Device action notification", NotificationType.ENTITY_ACTION,
"${entityType} was ${actionType}", "${entityType} was ${actionType}",
"${entityType} '${entityName}' was ${actionType} by user ${userEmail}", "${entityType} '${entityName}' was ${actionType} by user ${userEmail}",
"info", "Go to Device", "/devices/${entityId}"); "info", "Go to device", "/devices/${entityId}");
EntityActionNotificationRuleTriggerConfig deviceActionRuleTriggerConfig = new EntityActionNotificationRuleTriggerConfig(); EntityActionNotificationRuleTriggerConfig deviceActionRuleTriggerConfig = new EntityActionNotificationRuleTriggerConfig();
deviceActionRuleTriggerConfig.setEntityType(EntityType.DEVICE); deviceActionRuleTriggerConfig.setEntityTypes(Set.of(EntityType.DEVICE));
deviceActionRuleTriggerConfig.setCreated(true); deviceActionRuleTriggerConfig.setCreated(true);
deviceActionRuleTriggerConfig.setUpdated(false); deviceActionRuleTriggerConfig.setUpdated(false);
deviceActionRuleTriggerConfig.setDeleted(true); deviceActionRuleTriggerConfig.setDeleted(false);
createRule(tenantId, "Device created or deleted", deviceActionNotificationTemplate.getId(), deviceActionRuleTriggerConfig, createRule(tenantId, "Device created", deviceActionNotificationTemplate.getId(), deviceActionRuleTriggerConfig,
List.of(originatorEntityOwnerUsers.getId()), "Send notification to tenant admins or customer users " + List.of(originatorEntityOwnerUsers.getId()), "Send notification to tenant admins or customer users " +
"when device is created or deleted"); "when device is created");
NotificationTemplate deviceInactivityNotificationTemplate = createTemplate(tenantId, "Device inactivity notification", NotificationType.DEVICE_INACTIVITY, NotificationTemplate deviceActivityNotificationTemplate = createTemplate(tenantId, "Device activity notification", NotificationType.DEVICE_ACTIVITY,
"Device '${deviceName}' inactive", "Device '${deviceName}' became ${eventType}",
"Device '${deviceName}' with type '${deviceType}' became inactive", "Device '${deviceName}' of type '${deviceType}' is now ${eventType}",
"info", "Go to Device", "/devices/${deviceId}"); "info", "Go to device", "/devices/${deviceId}");
DeviceInactivityNotificationRuleTriggerConfig deviceInactivityRuleTriggerConfig = new DeviceInactivityNotificationRuleTriggerConfig(); DeviceActivityNotificationRuleTriggerConfig deviceActivityRuleTriggerConfig = new DeviceActivityNotificationRuleTriggerConfig();
deviceInactivityRuleTriggerConfig.setDevices(null); deviceActivityRuleTriggerConfig.setDevices(null);
deviceInactivityRuleTriggerConfig.setDeviceProfiles(null); deviceActivityRuleTriggerConfig.setDeviceProfiles(null);
createRule(tenantId, "Device became inactive", deviceInactivityNotificationTemplate.getId(), deviceInactivityRuleTriggerConfig, deviceActivityRuleTriggerConfig.setNotifyOn(Set.of(DeviceEvent.ACTIVE, DeviceEvent.INACTIVE));
createRule(tenantId, "Device activity status change", deviceActivityNotificationTemplate.getId(), deviceActivityRuleTriggerConfig,
List.of(originatorEntityOwnerUsers.getId()), "Send notification to tenant admins or customer users " + List.of(originatorEntityOwnerUsers.getId()), "Send notification to tenant admins or customer users " +
"when any device became inactive"); "when any device changes its activity state");
NotificationTemplate alarmCommentNotificationTemplate = createTemplate(tenantId, "Alarm comment notification", NotificationType.ALARM_COMMENT, NotificationTemplate alarmCommentNotificationTemplate = createTemplate(tenantId, "Alarm comment notification", NotificationType.ALARM_COMMENT,
"Comment on '${alarmType}' alarm", "Comment on '${alarmType}' alarm",
@ -205,7 +207,7 @@ public class DefaultNotificationSettingsService implements NotificationSettingsS
NotificationTemplate ruleEngineComponentLifecycleFailureNotificationTemplate = createTemplate(tenantId, "Rule chain/node lifecycle failure notification", NotificationType.RULE_ENGINE_COMPONENT_LIFECYCLE_EVENT, NotificationTemplate ruleEngineComponentLifecycleFailureNotificationTemplate = createTemplate(tenantId, "Rule chain/node lifecycle failure notification", NotificationType.RULE_ENGINE_COMPONENT_LIFECYCLE_EVENT,
"${componentType} '${componentName}' failed to ${action}", "${componentType} '${componentName}' failed to ${action}",
"Rule chain '${ruleChainName}' - ${action} failure:<br/>${error}", "Rule chain '${ruleChainName}' - ${action} failure:<br/>${error}",
"warning", "Go to Rule chain", "/ruleChains/${ruleChainId}"); "warning", "Go to rule chain", "/ruleChains/${ruleChainId}");
RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig ruleEngineComponentLifecycleEventRuleTriggerConfig = new RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig(); RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig ruleEngineComponentLifecycleEventRuleTriggerConfig = new RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig();
ruleEngineComponentLifecycleEventRuleTriggerConfig.setRuleChains(null); ruleEngineComponentLifecycleEventRuleTriggerConfig.setRuleChains(null);
ruleEngineComponentLifecycleEventRuleTriggerConfig.setRuleChainEvents(Set.of(ComponentLifecycleEvent.STARTED, ComponentLifecycleEvent.UPDATED, ComponentLifecycleEvent.STOPPED)); ruleEngineComponentLifecycleEventRuleTriggerConfig.setRuleChainEvents(Set.of(ComponentLifecycleEvent.STARTED, ComponentLifecycleEvent.UPDATED, ComponentLifecycleEvent.STOPPED));

3
dao/src/test/java/org/thingsboard/server/dao/service/BaseAdminSettingsServiceTest.java

@ -21,6 +21,7 @@ import org.junit.Assert;
import org.junit.Test; import org.junit.Test;
import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Assertions;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.testcontainers.shaded.org.apache.commons.lang3.RandomStringUtils;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.AdminSettings; import org.thingsboard.server.common.data.AdminSettings;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
@ -85,7 +86,7 @@ public abstract class BaseAdminSettingsServiceTest extends AbstractServiceTest {
@Test @Test
public void whenSavingAdminSettingsWithAlreadyExistingKey_thenReturnError() { public void whenSavingAdminSettingsWithAlreadyExistingKey_thenReturnError() {
String key = "mySettings"; String key = RandomStringUtils.randomAlphanumeric(15);
ObjectNode value = JacksonUtil.newObjectNode().put("test", "test"); ObjectNode value = JacksonUtil.newObjectNode().put("test", "test");
AdminSettings systemSettings = new AdminSettings(); AdminSettings systemSettings = new AdminSettings();

4
ui-ngx/src/assets/help/en_US/notification/device_inactivity.md → ui-ngx/src/assets/help/en_US/notification/device_activity.md

@ -15,7 +15,7 @@ Available template parameters:
* *deviceName* - the device name; * *deviceName* - the device name;
* *deviceLabel* - the device label; * *deviceLabel* - the device label;
* *deviceType* - the device type; * *deviceType* - the device type;
* *actionType* - one of: 'inactive', 'active'; // TODO: fix and rename the type * *eventType* - one of: 'inactive', 'active';
Parameter names must be wrapped using `${...}`. For example: `${recipientFirstName}`. Parameter names must be wrapped using `${...}`. For example: `${recipientFirstName}`.
You may also modify the value of the parameter with one of the suffixes: You may also modify the value of the parameter with one of the suffixes:
@ -50,7 +50,7 @@ Device 'Sensor T1' inactive
The following template: The following template:
```text ```text
${deviceType:capitalize} '${deviceName}' became inactive ${deviceType:capitalize} '${deviceName}' became ${eventType}
{:copy-code} {:copy-code}
``` ```
Loading…
Cancel
Save