From 9add2962a64e6c1eb139d77802d09306a6ee0072 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Wed, 10 May 2023 18:20:36 +0300 Subject: [PATCH 01/33] Push-notifications to mobile --- application/pom.xml | 4 + .../controller/SystemInfoController.java | 2 +- .../channels/MobileNotificationChannel.java | 109 ++++++++++++++++++ .../NotificationDeliveryMethod.java | 3 +- ...obileNotificationDeliveryMethodConfig.java | 35 ++++++ .../NotificationDeliveryMethodConfig.java | 3 +- .../targets/NotificationTargetType.java | 2 +- .../DeliveryMethodNotificationTemplate.java | 3 +- ...ileDeliveryMethodNotificationTemplate.java | 56 +++++++++ pom.xml | 6 + 10 files changed, 218 insertions(+), 5 deletions(-) create mode 100644 application/src/main/java/org/thingsboard/server/service/notification/channels/MobileNotificationChannel.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/notification/settings/MobileNotificationDeliveryMethodConfig.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/notification/template/MobileDeliveryMethodNotificationTemplate.java diff --git a/application/pom.xml b/application/pom.xml index 3093dcfb57..f76a0709c7 100644 --- a/application/pom.xml +++ b/application/pom.xml @@ -354,6 +354,10 @@ com.slack.api slack-api-client + + com.google.firebase + firebase-admin + diff --git a/application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java b/application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java index e586f079ca..86131b41af 100644 --- a/application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java +++ b/application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java @@ -149,7 +149,7 @@ public class SystemInfoController extends BaseController { infoObject.put("artifact", buildProperties.getArtifact()); infoObject.put("name", buildProperties.getName()); } else { - infoObject.put("version", "unknown"); + infoObject.put("version", "3.5.5"); } return infoObject; } diff --git a/application/src/main/java/org/thingsboard/server/service/notification/channels/MobileNotificationChannel.java b/application/src/main/java/org/thingsboard/server/service/notification/channels/MobileNotificationChannel.java new file mode 100644 index 0000000000..75ff7866ab --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/notification/channels/MobileNotificationChannel.java @@ -0,0 +1,109 @@ +/** + * 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.databind.JsonNode; +import com.google.auth.oauth2.GoogleCredentials; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.firebase.FirebaseApp; +import com.google.firebase.FirebaseOptions; +import com.google.firebase.messaging.FirebaseMessaging; +import com.google.firebase.messaging.Message; +import com.google.firebase.messaging.Notification; +import lombok.RequiredArgsConstructor; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; +import org.thingsboard.server.common.data.notification.settings.MobileNotificationDeliveryMethodConfig; +import org.thingsboard.server.common.data.notification.settings.NotificationSettings; +import org.thingsboard.server.common.data.notification.template.MobileDeliveryMethodNotificationTemplate; +import org.thingsboard.server.dao.notification.NotificationSettingsService; +import org.thingsboard.server.service.executors.ExternalCallExecutorService; +import org.thingsboard.server.service.notification.NotificationProcessingContext; + +import java.nio.charset.StandardCharsets; +import java.util.Optional; + +@Component +@RequiredArgsConstructor +public class MobileNotificationChannel implements NotificationChannel { + + private final ExternalCallExecutorService executor; + private final NotificationSettingsService notificationSettingsService; + + @Override + public ListenableFuture sendNotification(User recipient, MobileDeliveryMethodNotificationTemplate processedTemplate, NotificationProcessingContext ctx) { + String fcmToken = Optional.ofNullable(recipient.getAdditionalInfo()) + .map(info -> info.get("fcmToken")).filter(JsonNode::isTextual).map(JsonNode::asText) + .orElse(null); + if (StringUtils.isEmpty(fcmToken)) { + return Futures.immediateFailedFuture(new RuntimeException("User doesn't have the mobile app installed")); + } + + MobileNotificationDeliveryMethodConfig config = ctx.getDeliveryMethodConfig(NotificationDeliveryMethod.MOBILE); + return executor.submit(() -> { + FirebaseOptions firebaseOptions = FirebaseOptions.builder() + .setCredentials(GoogleCredentials.fromStream(IOUtils.toInputStream(config.getFirebaseServiceAccountCredentials(), StandardCharsets.UTF_8))) + .build(); + String appName = ctx.getTenantId().toString(); + + FirebaseApp firebaseApp = FirebaseApp.getApps().stream() + .filter(app -> app.getName().equals(appName)) + .findFirst().orElseGet(() -> { + try { + return FirebaseApp.initializeApp(firebaseOptions, appName); + } catch (IllegalStateException e) { + return FirebaseApp.getInstance(appName); + } + }); + FirebaseMessaging firebaseMessaging; + try { + firebaseMessaging = FirebaseMessaging.getInstance(firebaseApp); + } catch (IllegalArgumentException e) { + // because of concurrency issues: FirebaseMessaging.getInstance lazily loads FirebaseMessagingService + firebaseMessaging = FirebaseMessaging.getInstance(firebaseApp); + } + + Message message = Message.builder() + .setNotification(Notification.builder() + .setTitle(processedTemplate.getSubject()) + .setBody(processedTemplate.getBody()) + .build()) + .setToken(fcmToken) + .build(); + firebaseMessaging.send(message); + return null; + }); + } + + @Override + public void check(TenantId tenantId) throws Exception { + NotificationSettings settings = notificationSettingsService.findNotificationSettings(tenantId); + if (!settings.getDeliveryMethodsConfigs().containsKey(NotificationDeliveryMethod.MOBILE)) { + throw new RuntimeException("Push-notifications to mobile are not configured"); + } + } + + @Override + public NotificationDeliveryMethod getDeliveryMethod() { + return NotificationDeliveryMethod.MOBILE; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationDeliveryMethod.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationDeliveryMethod.java index 4a2c4657d5..5b9e84d37d 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationDeliveryMethod.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationDeliveryMethod.java @@ -24,7 +24,8 @@ public enum NotificationDeliveryMethod { WEB("web"), EMAIL("email"), SMS("SMS"), - SLACK("Slack"); + SLACK("Slack"), + MOBILE("mobile"); @Getter private final String name; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/settings/MobileNotificationDeliveryMethodConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/settings/MobileNotificationDeliveryMethodConfig.java new file mode 100644 index 0000000000..4e443227ee --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/settings/MobileNotificationDeliveryMethodConfig.java @@ -0,0 +1,35 @@ +/** + * 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.settings; + +import lombok.Data; +import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; + +import javax.validation.constraints.NotEmpty; + +@Data +public class MobileNotificationDeliveryMethodConfig implements NotificationDeliveryMethodConfig { + + private String firebaseServiceAccountCredentialsFileName; + @NotEmpty + private String firebaseServiceAccountCredentials; + + @Override + public NotificationDeliveryMethod getMethod() { + return NotificationDeliveryMethod.MOBILE; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/settings/NotificationDeliveryMethodConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/settings/NotificationDeliveryMethodConfig.java index 962eba6d06..28020d6c4e 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/settings/NotificationDeliveryMethodConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/settings/NotificationDeliveryMethodConfig.java @@ -27,7 +27,8 @@ import java.io.Serializable; @JsonIgnoreProperties(ignoreUnknown = true) @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "method") @JsonSubTypes({ - @Type(name = "SLACK", value = SlackNotificationDeliveryMethodConfig.class) + @Type(name = "SLACK", value = SlackNotificationDeliveryMethodConfig.class), + @Type(name = "MOBILE", value = MobileNotificationDeliveryMethodConfig.class) }) public interface NotificationDeliveryMethodConfig extends Serializable { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/NotificationTargetType.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/NotificationTargetType.java index 1254654ecf..2180679cb2 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/NotificationTargetType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/NotificationTargetType.java @@ -25,7 +25,7 @@ import java.util.Set; @RequiredArgsConstructor public enum NotificationTargetType { - PLATFORM_USERS(Set.of(NotificationDeliveryMethod.WEB, NotificationDeliveryMethod.EMAIL, NotificationDeliveryMethod.SMS)), + PLATFORM_USERS(Set.of(NotificationDeliveryMethod.WEB, NotificationDeliveryMethod.EMAIL, NotificationDeliveryMethod.SMS, NotificationDeliveryMethod.MOBILE)), SLACK(Set.of(NotificationDeliveryMethod.SLACK)); @Getter diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/DeliveryMethodNotificationTemplate.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/DeliveryMethodNotificationTemplate.java index ecb7f2d2b6..f033fe5092 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/DeliveryMethodNotificationTemplate.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/DeliveryMethodNotificationTemplate.java @@ -33,7 +33,8 @@ import javax.validation.constraints.NotEmpty; @Type(name = "WEB", value = WebDeliveryMethodNotificationTemplate.class), @Type(name = "EMAIL", value = EmailDeliveryMethodNotificationTemplate.class), @Type(name = "SMS", value = SmsDeliveryMethodNotificationTemplate.class), - @Type(name = "SLACK", value = SlackDeliveryMethodNotificationTemplate.class) + @Type(name = "SLACK", value = SlackDeliveryMethodNotificationTemplate.class), + @Type(name = "MOBILE", value = MobileDeliveryMethodNotificationTemplate.class) }) @Data @NoArgsConstructor diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/MobileDeliveryMethodNotificationTemplate.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/MobileDeliveryMethodNotificationTemplate.java new file mode 100644 index 0000000000..91786db48a --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/MobileDeliveryMethodNotificationTemplate.java @@ -0,0 +1,56 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.notification.template; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; +import org.apache.commons.lang3.StringUtils; +import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; + +import javax.validation.constraints.NotEmpty; + +@Data +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class MobileDeliveryMethodNotificationTemplate extends DeliveryMethodNotificationTemplate implements HasSubject { + + @NotEmpty + private String subject; + + public MobileDeliveryMethodNotificationTemplate(MobileDeliveryMethodNotificationTemplate other) { + super(other); + this.subject = other.subject; + } + + @Override + public NotificationDeliveryMethod getMethod() { + return NotificationDeliveryMethod.MOBILE; + } + + @Override + public MobileDeliveryMethodNotificationTemplate copy() { + return new MobileDeliveryMethodNotificationTemplate(this); + } + + @Override + public boolean containsAny(String... params) { + return super.containsAny(params) || StringUtils.containsAny(subject, params); + } + +} diff --git a/pom.xml b/pom.xml index f78a4826bc..f58e80480a 100755 --- a/pom.xml +++ b/pom.xml @@ -150,6 +150,7 @@ 2.21.0 2.12.0 1.12.1 + 8.0.1 3.4.0 @@ -1987,6 +1988,11 @@ slack-api-client ${slack-api.version} + + com.google.firebase + firebase-admin + ${firebase-admin.version} + org.eclipse.jgit org.eclipse.jgit From 1c624949737d4c006558f644196077f4a39a30ef Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Wed, 10 May 2023 18:20:58 +0300 Subject: [PATCH 02/33] UI: push-notifications to mobile --- .../pages/admin/sms-provider.component.html | 25 ++++++++++- .../pages/admin/sms-provider.component.ts | 18 +++++--- .../sent-notification-dialog.component.html | 43 +++++++++++++++++++ .../sent/sent-notification-dialog.componet.ts | 3 +- .../template/template-configuration.ts | 9 +++- ...emplate-notification-dialog.component.html | 26 +++++++++++ .../app/shared/models/notification.models.ts | 19 ++++++-- .../assets/locale/locale.constant-en_US.json | 9 +++- 8 files changed, 135 insertions(+), 17 deletions(-) diff --git a/ui-ngx/src/app/modules/home/pages/admin/sms-provider.component.html b/ui-ngx/src/app/modules/home/pages/admin/sms-provider.component.html index 0ea223e8b8..0f21bd0fd9 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/sms-provider.component.html +++ b/ui-ngx/src/app/modules/home/pages/admin/sms-provider.component.html @@ -58,7 +58,7 @@
-
+
@@ -66,8 +66,29 @@
+
+
+
+ + + admin.mobile-settings + + + + + +
+
+
+ + +
-
diff --git a/ui-ngx/src/app/modules/home/pages/admin/sms-provider.component.ts b/ui-ngx/src/app/modules/home/pages/admin/sms-provider.component.ts index e56f02c4e4..99eea3ab23 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/sms-provider.component.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/sms-provider.component.ts @@ -42,7 +42,7 @@ export class SmsProviderComponent extends PageComponent implements HasConfirmFor smsProvider: FormGroup; private adminSettings: AdminSettings; - slackSettingsForm: FormGroup; + notificationSettingsForm: FormGroup; private notificationSettings: NotificationSettings; private readonly authUser: AuthUser; @@ -60,7 +60,7 @@ export class SmsProviderComponent extends PageComponent implements HasConfirmFor this.notificationService.getNotificationSettings().subscribe( (settings) => { this.notificationSettings = settings; - this.slackSettingsForm.reset(this.notificationSettings); + this.notificationSettingsForm.reset(this.notificationSettings); } ); if (this.isSysAdmin()) { @@ -108,24 +108,28 @@ export class SmsProviderComponent extends PageComponent implements HasConfirmFor } confirmForm(): FormGroup { - return this.smsProvider.dirty ? this.smsProvider : this.slackSettingsForm; + return this.smsProvider.dirty ? this.smsProvider : this.notificationSettingsForm; } private buildGeneralServerSettingsForm() { - this.slackSettingsForm = this.fb.group({ + this.notificationSettingsForm = this.fb.group({ deliveryMethodsConfigs: this.fb.group({ SLACK: this.fb.group({ botToken: [''] + }), + MOBILE: this.fb.group({ + firebaseServiceAccountCredentialsFileName: [''], + firebaseServiceAccountCredentials: [''] }) }) }); - this.registerDisableOnLoadFormControl(this.slackSettingsForm.get('deliveryMethodsConfigs')); + this.registerDisableOnLoadFormControl(this.notificationSettingsForm.get('deliveryMethodsConfigs')); } saveNotification(): void { this.notificationSettings = deepTrim({ ...this.notificationSettings, - ...this.slackSettingsForm.value + ...this.notificationSettingsForm.value }); // eslint-disable-next-line guard-for-in for (const method in this.notificationSettings.deliveryMethodsConfigs) { @@ -138,7 +142,7 @@ export class SmsProviderComponent extends PageComponent implements HasConfirmFor } this.notificationService.saveNotificationSettings(this.notificationSettings).subscribe(setting => { this.notificationSettings = setting; - this.slackSettingsForm.reset(this.notificationSettings); + this.notificationSettingsForm.reset(this.notificationSettings); }); } diff --git a/ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.component.html b/ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.component.html index 27b8968344..e5ebab3947 100644 --- a/ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.component.html @@ -334,6 +334,40 @@ + + {{ 'notification.delivery-method.mobile' | translate }} +
+ {{ 'notification.input-field-support-templatization' | translate}} + +
+
+ + notification.subject + + + {{ 'notification.subject-required' | translate }} + + + + notification.message + + + {{ 'notification.message-required' | translate }} + + +
+
{{ 'notification.review' | translate }} +
+
+ +
notification.delivery-method.mobile-preview
+
+
+ {{ preview.processedTemplates.MOBILE.body }} +
+
supervisor_account diff --git a/ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.componet.ts b/ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.componet.ts index 4846cd063d..938ec2756f 100644 --- a/ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.componet.ts +++ b/ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.componet.ts @@ -315,7 +315,7 @@ export class SentNotificationDialogComponent extends if(this.isSysAdmin()) { return true; } else if (this.isTenantAdmin()) { - return deliveryMethod === NotificationDeliveryMethod.SLACK; + return deliveryMethod === NotificationDeliveryMethod.SLACK || deliveryMethod === NotificationDeliveryMethod.MOBILE; } return false; } @@ -330,6 +330,7 @@ export class SentNotificationDialogComponent extends return '/settings/outgoing-mail'; case NotificationDeliveryMethod.SMS: case NotificationDeliveryMethod.SLACK: + case NotificationDeliveryMethod.MOBILE: return '/settings/notifications'; } } diff --git a/ui-ngx/src/app/modules/home/pages/notification/template/template-configuration.ts b/ui-ngx/src/app/modules/home/pages/notification/template/template-configuration.ts index b3fa00ef72..90115738a6 100644 --- a/ui-ngx/src/app/modules/home/pages/notification/template/template-configuration.ts +++ b/ui-ngx/src/app/modules/home/pages/notification/template/template-configuration.ts @@ -43,6 +43,7 @@ export abstract class TemplateConfiguration extends DialogComponent< emailTemplateForm: FormGroup; smsTemplateForm: FormGroup; slackTemplateForm: FormGroup; + mobileTemplateForm: FormGroup; notificationDeliveryMethods = Object.keys(NotificationDeliveryMethod) as NotificationDeliveryMethod[]; notificationDeliveryMethodTranslateMap = NotificationDeliveryMethodTranslateMap; @@ -163,11 +164,17 @@ export abstract class TemplateConfiguration extends DialogComponent< body: ['', Validators.required] }); + this.mobileTemplateForm = this.fb.group({ + subject: ['', Validators.required], + body: ['', Validators.required] + }); + this.deliveryMethodFormsMap = new Map([ [NotificationDeliveryMethod.WEB, this.webTemplateForm], [NotificationDeliveryMethod.EMAIL, this.emailTemplateForm], [NotificationDeliveryMethod.SMS, this.smsTemplateForm], - [NotificationDeliveryMethod.SLACK, this.slackTemplateForm] + [NotificationDeliveryMethod.SLACK, this.slackTemplateForm], + [NotificationDeliveryMethod.MOBILE, this.mobileTemplateForm] ]); } diff --git a/ui-ngx/src/app/modules/home/pages/notification/template/template-notification-dialog.component.html b/ui-ngx/src/app/modules/home/pages/notification/template/template-notification-dialog.component.html index 067258fa24..1a623af7b9 100644 --- a/ui-ngx/src/app/modules/home/pages/notification/template/template-notification-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/notification/template/template-notification-dialog.component.html @@ -259,6 +259,32 @@ + + {{ 'notification.delivery-method.mobile' | translate }} +
+ {{ 'notification.input-field-support-templatization' | translate}} + +
+
+ + notification.message + + + {{ 'notification.message-required' | translate }} + + +
+
diff --git a/ui-ngx/src/app/shared/models/notification.models.ts b/ui-ngx/src/app/shared/models/notification.models.ts index fe554ba7f2..55dcb35b66 100644 --- a/ui-ngx/src/app/shared/models/notification.models.ts +++ b/ui-ngx/src/app/shared/models/notification.models.ts @@ -89,7 +89,7 @@ export interface NotificationSettings { deliveryMethodsConfigs: { [key in NotificationDeliveryMethod]: NotificationDeliveryMethodConfig }; } -export interface NotificationDeliveryMethodConfig extends Partial{ +export interface NotificationDeliveryMethodConfig extends Partial{ enabled: boolean; method: NotificationDeliveryMethod; } @@ -98,6 +98,10 @@ interface SlackNotificationDeliveryMethodConfig { botToken: string; } +interface MobileNotificationDeliveryMethodConfig { + firebaseServiceAccountCredentials: string; +} + export interface SlackConversation { id: string; title: string; @@ -292,7 +296,8 @@ interface NotificationTemplateConfig { } export interface DeliveryMethodNotificationTemplate extends - Partial{ + Partial { body?: string; enabled: boolean; method: NotificationDeliveryMethod; @@ -329,6 +334,10 @@ interface SlackDeliveryMethodNotificationTemplate { conversationId: string; } +interface MobileDeliveryMethodNotificationTemplate { + subject: string; +} + export enum NotificationStatus { SENT = 'SENT', READ = 'READ' @@ -338,14 +347,16 @@ export enum NotificationDeliveryMethod { WEB = 'WEB', SMS = 'SMS', EMAIL = 'EMAIL', - SLACK = 'SLACK' + SLACK = 'SLACK', + MOBILE = 'MOBILE' } export const NotificationDeliveryMethodTranslateMap = new Map([ [NotificationDeliveryMethod.WEB, 'notification.delivery-method.web'], [NotificationDeliveryMethod.SMS, 'notification.delivery-method.sms'], [NotificationDeliveryMethod.EMAIL, 'notification.delivery-method.email'], - [NotificationDeliveryMethod.SLACK, 'notification.delivery-method.slack'] + [NotificationDeliveryMethod.SLACK, 'notification.delivery-method.slack'], + [NotificationDeliveryMethod.MOBILE, 'notification.delivery-method.mobile'] ]); export enum NotificationRequestStatus { diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index bcaa4a183f..14734c1a57 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -420,7 +420,10 @@ "notifications-settings": "Notifications settings", "slack-api-token": "Slack API token", "slack": "Slack", - "slack-settings": "Slack settings" + "slack-settings": "Slack settings", + "mobile-settings": "Mobile settings", + "firebase-service-account-file": "Firebase service account credentials JSON file", + "select-firebase-service-account-file": "Drag and drop your Firebase service account credentials file or " }, "alarm": { "alarm": "Alarm", @@ -2847,7 +2850,9 @@ "sms": "SMS", "sms-preview": "SMS notification preview", "web": "Web", - "web-preview": "Web notification preview" + "web-preview": "Web notification preview", + "mobile": "Mobile", + "mobile-preview": "Mobile notification preview" }, "delivery-method-not-configure-click": "Delivery method is not configured. Click to setup.", "delivery-method-not-configure-contact": "Delivery method is not configured. Contact your system administrator.", From 592ab5858bc704005644a873e3f6d91fe8c244af Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Thu, 11 May 2023 11:41:27 +0300 Subject: [PATCH 03/33] Revert SystemInfoController changes --- .../org/thingsboard/server/controller/SystemInfoController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java b/application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java index 86131b41af..e586f079ca 100644 --- a/application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java +++ b/application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java @@ -149,7 +149,7 @@ public class SystemInfoController extends BaseController { infoObject.put("artifact", buildProperties.getArtifact()); infoObject.put("name", buildProperties.getName()); } else { - infoObject.put("version", "3.5.5"); + infoObject.put("version", "unknown"); } return infoObject; } From fa7fcda2c79424fedf82f394a5d248b493ae6351 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Mon, 12 Jun 2023 14:20:27 +0300 Subject: [PATCH 04/33] Fix merge issues --- .../channels/MobileNotificationChannel.java | 67 +++++++++---------- 1 file changed, 30 insertions(+), 37 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/notification/channels/MobileNotificationChannel.java b/application/src/main/java/org/thingsboard/server/service/notification/channels/MobileNotificationChannel.java index 75ff7866ab..fe515f2218 100644 --- a/application/src/main/java/org/thingsboard/server/service/notification/channels/MobileNotificationChannel.java +++ b/application/src/main/java/org/thingsboard/server/service/notification/channels/MobileNotificationChannel.java @@ -17,8 +17,6 @@ package org.thingsboard.server.service.notification.channels; import com.fasterxml.jackson.databind.JsonNode; import com.google.auth.oauth2.GoogleCredentials; -import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseOptions; import com.google.firebase.messaging.FirebaseMessaging; @@ -35,7 +33,6 @@ import org.thingsboard.server.common.data.notification.settings.MobileNotificati import org.thingsboard.server.common.data.notification.settings.NotificationSettings; import org.thingsboard.server.common.data.notification.template.MobileDeliveryMethodNotificationTemplate; import org.thingsboard.server.dao.notification.NotificationSettingsService; -import org.thingsboard.server.service.executors.ExternalCallExecutorService; import org.thingsboard.server.service.notification.NotificationProcessingContext; import java.nio.charset.StandardCharsets; @@ -45,52 +42,48 @@ import java.util.Optional; @RequiredArgsConstructor public class MobileNotificationChannel implements NotificationChannel { - private final ExternalCallExecutorService executor; private final NotificationSettingsService notificationSettingsService; @Override - public ListenableFuture sendNotification(User recipient, MobileDeliveryMethodNotificationTemplate processedTemplate, NotificationProcessingContext ctx) { + public void sendNotification(User recipient, MobileDeliveryMethodNotificationTemplate processedTemplate, NotificationProcessingContext ctx) throws Exception { String fcmToken = Optional.ofNullable(recipient.getAdditionalInfo()) .map(info -> info.get("fcmToken")).filter(JsonNode::isTextual).map(JsonNode::asText) .orElse(null); if (StringUtils.isEmpty(fcmToken)) { - return Futures.immediateFailedFuture(new RuntimeException("User doesn't have the mobile app installed")); + throw new RuntimeException("User doesn't have the mobile app installed"); } MobileNotificationDeliveryMethodConfig config = ctx.getDeliveryMethodConfig(NotificationDeliveryMethod.MOBILE); - return executor.submit(() -> { - FirebaseOptions firebaseOptions = FirebaseOptions.builder() - .setCredentials(GoogleCredentials.fromStream(IOUtils.toInputStream(config.getFirebaseServiceAccountCredentials(), StandardCharsets.UTF_8))) - .build(); - String appName = ctx.getTenantId().toString(); + FirebaseOptions firebaseOptions = FirebaseOptions.builder() + .setCredentials(GoogleCredentials.fromStream(IOUtils.toInputStream(config.getFirebaseServiceAccountCredentials(), StandardCharsets.UTF_8))) + .build(); + String appName = ctx.getTenantId().toString(); - FirebaseApp firebaseApp = FirebaseApp.getApps().stream() - .filter(app -> app.getName().equals(appName)) - .findFirst().orElseGet(() -> { - try { - return FirebaseApp.initializeApp(firebaseOptions, appName); - } catch (IllegalStateException e) { - return FirebaseApp.getInstance(appName); - } - }); - FirebaseMessaging firebaseMessaging; - try { - firebaseMessaging = FirebaseMessaging.getInstance(firebaseApp); - } catch (IllegalArgumentException e) { - // because of concurrency issues: FirebaseMessaging.getInstance lazily loads FirebaseMessagingService - firebaseMessaging = FirebaseMessaging.getInstance(firebaseApp); - } + FirebaseApp firebaseApp = FirebaseApp.getApps().stream() + .filter(app -> app.getName().equals(appName)) + .findFirst().orElseGet(() -> { + try { + return FirebaseApp.initializeApp(firebaseOptions, appName); + } catch (IllegalStateException e) { + return FirebaseApp.getInstance(appName); + } + }); + FirebaseMessaging firebaseMessaging; + try { + firebaseMessaging = FirebaseMessaging.getInstance(firebaseApp); + } catch (IllegalArgumentException e) { + // because of concurrency issues: FirebaseMessaging.getInstance lazily loads FirebaseMessagingService + firebaseMessaging = FirebaseMessaging.getInstance(firebaseApp); + } - Message message = Message.builder() - .setNotification(Notification.builder() - .setTitle(processedTemplate.getSubject()) - .setBody(processedTemplate.getBody()) - .build()) - .setToken(fcmToken) - .build(); - firebaseMessaging.send(message); - return null; - }); + Message message = Message.builder() + .setNotification(Notification.builder() + .setTitle(processedTemplate.getSubject()) + .setBody(processedTemplate.getBody()) + .build()) + .setToken(fcmToken) + .build(); + firebaseMessaging.send(message); } @Override From 7dd5cada3501787e96d784981e8f16cdf47041b0 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Mon, 12 Jun 2023 18:16:24 +0300 Subject: [PATCH 05/33] UI: Refactoring and fix notification delivery method in mobile --- .../pages/admin/sms-provider.component.html | 79 ++++++++++--------- .../sent-notification-dialog.component.html | 25 +++--- .../sent-notification-dialog.component.scss | 57 +------------ .../sent/sent-notification-dialog.componet.ts | 8 +- ...emplate-notification-dialog.component.html | 7 ++ ...emplate-notification-dialog.component.scss | 3 +- .../app/shared/models/notification.models.ts | 4 +- 7 files changed, 78 insertions(+), 105 deletions(-) diff --git a/ui-ngx/src/app/modules/home/pages/admin/sms-provider.component.html b/ui-ngx/src/app/modules/home/pages/admin/sms-provider.component.html index 0f21bd0fd9..6b878f5d4d 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/sms-provider.component.html +++ b/ui-ngx/src/app/modules/home/pages/admin/sms-provider.component.html @@ -26,7 +26,7 @@
- +
- - - - admin.slack-settings - - -
-
- - -
- - -
+ +
+ + + + admin.slack-settings + + +
+
+ + +
+
admin.slack-api-token - +
-
- - - - - admin.mobile-settings - - - - - -
-
-
- + + + admin.mobile-settings + + + + + +
+ + label="{{ 'admin.firebase-service-account-file' | translate }}" + accept=".json,application/json" + allowedExtensions="json" + [existingFileName]="notificationSettingsForm.get('deliveryMethodsConfigs.MOBILE.firebaseServiceAccountCredentialsFileName')?.value" + (fileNameChanged)="notificationSettingsForm?.get('deliveryMethodsConfigs.MOBILE.firebaseServiceAccountCredentialsFileName').patchValue($event)">
-
-
-
-
-
- + + +
+ + diff --git a/ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.component.html b/ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.component.html index a12f70139b..5be232104b 100644 --- a/ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.component.html @@ -39,13 +39,19 @@ {{ 'notification.compose' | translate }}
-
- - {{ 'notification.start-from-scratch' | translate }} - {{ 'notification.use-template' | translate }} - +
+ +
- +
notification.delivery-method.mobile-preview
- {{ preview.processedTemplates.MOBILE.body }} +
{{ preview.processedTemplates.MOBILE.subject }}
+
{{ preview.processedTemplates.MOBILE.body }}
diff --git a/ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.component.scss b/ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.component.scss index 0702d6bf52..4623ab1637 100644 --- a/ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.component.scss +++ b/ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.component.scss @@ -17,7 +17,7 @@ @import "../../../../../../theme"; :host-context(.tb-fullscreen-dialog .mat-mdc-dialog-container) { - width: 820px; + width: 930px; height: 100%; max-width: 100%; max-height: 100vh; @@ -76,6 +76,7 @@ .delivery-method-container { display: inline-flex; flex: 1 1 calc(50% - 8px); + max-width: calc(50% - 8px); padding: 16px 12px; border: 1px solid rgba(0, 0, 0, 0.1); border-radius: 6px; @@ -211,58 +212,8 @@ } } - .mat-button-toggle-group.tb-notification-use-template-toggle-group { - &.mat-button-toggle-group-appearance-standard { - border: none; - border-radius: 18px; - margin-bottom: 24px; - - .mat-button-toggle + .mat-button-toggle { - border-left: none; - } - } - - .mat-button-toggle { - background: rgba(0, 0, 0, 0.06); - height: 36px; - align-items: center; - display: flex; - - .mat-button-toggle-ripple { - top: 2px; - left: 2px; - right: 2px; - bottom: 2px; - border-radius: 18px; - } - } - - .mat-button-toggle-button { - color: #959595; - } - - .mat-button-toggle-focus-overlay { - border-radius: 18px; - margin: 2px; - } - - .mat-button-toggle-checked .mat-button-toggle-button { - background-color: $tb-primary-color; - color: #fff; - border-radius: 18px; - margin-left: 2px; - margin-right: 2px; - } - - .mat-button-toggle-appearance-standard .mat-button-toggle-label-content { - line-height: 34px; - font-size: 16px; - font-weight: 500; - } - - .mat-button-toggle-checked.mat-button-toggle-appearance-standard:not(.mat-button-toggle-disabled):hover .mat-button-toggle-focus-overlay { - opacity: .01; - } + .tb-notification-use-template-toggle-group { + margin-bottom: 24px; } .preview-group { diff --git a/ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.componet.ts b/ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.componet.ts index 938ec2756f..674da8c5e8 100644 --- a/ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.componet.ts +++ b/ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.componet.ts @@ -90,7 +90,7 @@ export class SentNotificationDialogComponent extends protected fb: FormBuilder, private notificationService: NotificationService, private dialog: MatDialog, - private translate: TranslateService) { + public translate: TranslateService) { super(store, router, dialogRef, fb); this.notificationDeliveryMethods.forEach(method => { @@ -309,13 +309,17 @@ export class SentNotificationDialogComponent extends } allowConfigureDeliveryMethod(deliveryMethod: NotificationDeliveryMethod): boolean { + const tenantAllowConfigureDeliveryMethod = new Set([ + NotificationDeliveryMethod.SLACK, + NotificationDeliveryMethod.MOBILE + ]); if (deliveryMethod === NotificationDeliveryMethod.WEB) { return false; } if(this.isSysAdmin()) { return true; } else if (this.isTenantAdmin()) { - return deliveryMethod === NotificationDeliveryMethod.SLACK || deliveryMethod === NotificationDeliveryMethod.MOBILE; + return tenantAllowConfigureDeliveryMethod.has(deliveryMethod); } return false; } diff --git a/ui-ngx/src/app/modules/home/pages/notification/template/template-notification-dialog.component.html b/ui-ngx/src/app/modules/home/pages/notification/template/template-notification-dialog.component.html index 1a623af7b9..784f36157b 100644 --- a/ui-ngx/src/app/modules/home/pages/notification/template/template-notification-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/notification/template/template-notification-dialog.component.html @@ -271,6 +271,13 @@ trigger-text="{{ 'notification.see-documentation' | translate }}">
+ + notification.subject + + + {{ 'notification.subject-required' | translate }} + + notification.message - - {{ 'notification.message-required' | translate }} - - -
-
- - {{ 'icon.icon' | translate }} - -
- - - - -
-
-
- - - - - {{ 'notification.action-button' | translate }} - - - - -
- - notification.button-text - - - {{ 'notification.button-text-required' | translate }} - - - {{ 'notification.button-text-max-length' | translate : - {length: webTemplateForm.get('additionalConfig.actionButtonConfig.text').getError('maxlength').requiredLength} - }} - - -
-
- - notification.action-type - - - {{ actionButtonLinkTypeTranslateMap.get(actionButtonLinkType) | translate }} - - - - - notification.link - - - {{ 'notification.link-required' | translate }} - - - - - - - - -
- - {{ 'notification.set-entity-from-notification' | translate }} - -
-
-
-
- - - - {{ 'notification.delivery-method.email' | translate }} - -
- {{ 'notification.input-fields-support-templatization' | translate}} - -
-
- - notification.subject - - - {{ 'notification.subject-required' | translate }} - - - notification.message - - - {{ 'notification.message-required' | translate }} - -
-
-
- - {{ 'notification.delivery-method.sms' | translate }} -
- {{ 'notification.input-field-support-templatization' | translate}} - -
-
- - notification.message - - - {{ 'notification.message-required' | translate }} - - - {{ 'notification.message-max-length' | translate : - {length: smsTemplateForm.get('body').getError('maxlength').requiredLength} - }} - - -
-
- - {{ 'notification.delivery-method.slack' | translate }} -
- {{ 'notification.input-field-support-templatization' | translate}} - -
-
- - notification.message - - - {{ 'notification.message-required' | translate }} - - -
-
- - {{ 'notification.delivery-method.microsoft-teams' | translate }} -
- {{ 'notification.input-fields-support-templatization' | translate}} - -
-
- - notification.subject - - - - notification.message - - - {{ 'notification.message-required' | translate }} - - -
-
-
notification.theme-color
- -
-
- - - - - {{ 'notification.action-button' | translate }} - - - - -
- - notification.button-text - - - {{ 'notification.button-text-required' | translate }} - - - {{ 'notification.button-text-max-length' | translate : - {length: microsoftTeamsTemplateForm.get('button.text').getError('maxlength').requiredLength} - }} - - -
-
- - notification.action-type - - - {{ actionButtonLinkTypeTranslateMap.get(actionButtonLinkType) | translate }} - - - - - notification.link - - - {{ 'notification.link-required' | translate }} - - - - - - - - -
- - {{ 'notification.set-entity-from-notification' | translate }} - -
-
-
-
-
-
- - {{ 'notification.delivery-method.mobile-app' | translate }} -
- {{ 'notification.input-field-support-templatization' | translate}} - -
-
- - notification.subject - - - {{ 'notification.subject-required' | translate }} - - - - notification.message - - - {{ 'notification.message-required' | translate }} - - + + {{ 'notification.compose' | translate }} + + +
diff --git a/ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.component.scss b/ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.component.scss index c91c33a9d0..b5527b9adc 100644 --- a/ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.component.scss +++ b/ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.component.scss @@ -16,7 +16,7 @@ @import "../../../../../../scss/constants"; :host { - width: 930px; + width: 775px; height: 100%; max-width: 100%; max-height: 100vh; diff --git a/ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.componet.ts b/ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.componet.ts index 0262ef88cb..98fd3ba61d 100644 --- a/ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.componet.ts +++ b/ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.componet.ts @@ -24,7 +24,6 @@ import { import { Component, Inject, OnDestroy, ViewChild } from '@angular/core'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; -import { Router } from '@angular/router'; import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog'; import { AbstractControl, FormBuilder, FormGroup, Validators } from '@angular/forms'; import { NotificationService } from '@core/http/notification.service'; @@ -47,6 +46,7 @@ import { Authority } from '@shared/models/authority.enum'; import { AuthUser } from '@shared/models/user.model'; import { getCurrentAuthUser } from '@core/auth/auth.selectors'; import { TranslateService } from '@ngx-translate/core'; +import { Router } from '@angular/router'; export interface RequestNotificationDialogData { request?: NotificationRequest; @@ -98,7 +98,7 @@ export class SentNotificationDialogComponent extends } }); - this.stepperOrientation = this.breakpointObserver.observe(MediaBreakpoints['gt-sm']) + this.stepperOrientation = this.breakpointObserver.observe(MediaBreakpoints['gt-xs']) .pipe(map(({matches}) => matches ? 'horizontal' : 'vertical')); this.notificationRequestForm = this.fb.group({ @@ -150,11 +150,9 @@ export class SentNotificationDialogComponent extends let useTemplate = true; if (isDefinedAndNotNull(this.data.request.template)) { useTemplate = false; - // eslint-disable-next-line guard-for-in - for (const method in this.data.request.template.configuration.deliveryMethodsTemplates) { - this.deliveryMethodFormsMap.get(NotificationDeliveryMethod[method]) - .patchValue(this.data.request.template.configuration.deliveryMethodsTemplates[method]); - } + this.notificationTemplateConfigurationForm.patchValue({ + deliveryMethodsTemplates: this.data.request.template.configuration.deliveryMethodsTemplates + }, {emitEvent: false}); } this.notificationRequestForm.get('useTemplate').setValue(useTemplate, {onlySelf : true}); } @@ -178,6 +176,9 @@ export class SentNotificationDialogComponent extends changeStep($event: StepperSelectionEvent) { this.selectedIndex = $event.selectedIndex; + if ($event.previouslySelectedIndex > $event.selectedIndex) { + $event.previouslySelectedStep.interacted = false; + } if (this.selectedIndex === this.maxStepperIndex) { this.getPreview(); } diff --git a/ui-ngx/src/app/modules/home/pages/notification/sent/sent-table-config.resolver.ts b/ui-ngx/src/app/modules/home/pages/notification/sent/sent-table-config.resolver.ts index e4395cdb72..97b353555a 100644 --- a/ui-ngx/src/app/modules/home/pages/notification/sent/sent-table-config.resolver.ts +++ b/ui-ngx/src/app/modules/home/pages/notification/sent/sent-table-config.resolver.ts @@ -21,7 +21,7 @@ import { EntityTableConfig } from '@home/models/entity/entities-table-config.models'; import { - NotificationDeliveryMethodTranslateMap, + NotificationDeliveryMethodInfoMap, NotificationRequest, NotificationRequestInfo, NotificationRequestStats, @@ -94,7 +94,7 @@ export class SentTableConfigResolver implements Resolve this.requestStatusStyle(request.status)), new EntityTableColumn('deliveryMethods', 'notification.delivery-method.delivery-method', '15%', (request) => request.deliveryMethods - .map((deliveryMethod) => this.translate.instant(NotificationDeliveryMethodTranslateMap.get(deliveryMethod))).join(', '), + .map((deliveryMethod) => this.translate.instant(NotificationDeliveryMethodInfoMap.get(deliveryMethod).name)).join(', '), () => ({}), false), new EntityTableColumn('templateName', 'notification.template', '70%') ); diff --git a/ui-ngx/src/app/modules/home/pages/notification/settings/notification-settings.component.html b/ui-ngx/src/app/modules/home/pages/notification/settings/notification-settings.component.html index 502ea1f828..90482be72f 100644 --- a/ui-ngx/src/app/modules/home/pages/notification/settings/notification-settings.component.html +++ b/ui-ngx/src/app/modules/home/pages/notification/settings/notification-settings.component.html @@ -54,7 +54,7 @@ (change)="changeInstanceTypeCheckBox($event.checked, deliveryMethods)" [indeterminate]="getIndeterminate(deliveryMethods)" (click)="$event.stopPropagation()"> - {{ notificationDeliveryMethodTranslateMap.get(deliveryMethods) | translate }} + {{ notificationDeliveryMethodInfoMap.get(deliveryMethods).name | translate }}
diff --git a/ui-ngx/src/app/modules/home/pages/notification/settings/notification-settings.component.ts b/ui-ngx/src/app/modules/home/pages/notification/settings/notification-settings.component.ts index 4d839541c8..c2d3afe890 100644 --- a/ui-ngx/src/app/modules/home/pages/notification/settings/notification-settings.component.ts +++ b/ui-ngx/src/app/modules/home/pages/notification/settings/notification-settings.component.ts @@ -25,7 +25,7 @@ import { ActivatedRoute } from '@angular/router'; import { deepClone, isDefinedAndNotNull } from '@core/utils'; import { NotificationDeliveryMethod, - NotificationDeliveryMethodTranslateMap, + NotificationDeliveryMethodInfoMap, NotificationUserSettings } from '@shared/models/notification.models'; import { NotificationService } from '@core/http/notification.service'; @@ -41,7 +41,7 @@ export class NotificationSettingsComponent extends PageComponent implements OnIn notificationSettings: UntypedFormGroup; notificationDeliveryMethods: NotificationDeliveryMethod[]; - notificationDeliveryMethodTranslateMap = NotificationDeliveryMethodTranslateMap; + notificationDeliveryMethodInfoMap = NotificationDeliveryMethodInfoMap; private deliveryMethods = new Set([ NotificationDeliveryMethod.SLACK, diff --git a/ui-ngx/src/app/modules/home/pages/notification/template/notification-template-configuration.component.html b/ui-ngx/src/app/modules/home/pages/notification/template/notification-template-configuration.component.html new file mode 100644 index 0000000000..a856f1eb7d --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/notification/template/notification-template-configuration.component.html @@ -0,0 +1,398 @@ + +
+
notification.customize-messages
+
+ {{ 'notification.input-fields-support-templatization' | translate}} + +
+
+
+
+ + + + {{ NotificationDeliveryMethodInfoMap.get(NotificationDeliveryMethod.WEB).icon }} + {{ NotificationDeliveryMethodInfoMap.get(NotificationDeliveryMethod.WEB).name | translate }} + + + + + notification.subject + + + {{ 'notification.subject-required' | translate }} + + + + notification.message + + + {{ 'notification.message-required' | translate }} + + +
+
+ + {{ 'icon.icon' | translate }} + +
+ + + + +
+
+
+ + + + + {{ 'notification.action-button' | translate }} + + + + +
+ + notification.button-text + + + {{ 'notification.button-text-required' | translate }} + + + {{ 'notification.button-text-max-length' | translate : + {length: templateConfigurationForm.get('WEB.additionalConfig.actionButtonConfig.text').getError('maxlength').requiredLength} + }} + + +
+
+ + notification.action-type + + + {{ actionButtonLinkTypeTranslateMap.get(actionButtonLinkType) | translate }} + + + + + notification.link + + + {{ 'notification.link-required' | translate }} + + + + + + + + +
+ + {{ 'notification.set-entity-from-notification' | translate }} + +
+
+
+
+
+
+
+
+ + + + {{ NotificationDeliveryMethodInfoMap.get(NotificationDeliveryMethod.MOBILE_APP).icon }} + {{ NotificationDeliveryMethodInfoMap.get(NotificationDeliveryMethod.MOBILE_APP).name | translate }} + + + + + notification.subject + + + {{ 'notification.subject-required' | translate }} + + + + notification.message + + + {{ 'notification.message-required' | translate }} + + +
+ + + + + {{ 'notification.open-dashboard-on-click-notification' | translate }} + + + + +
+ + + + +
+ + {{ 'notification.set-entity-from-notification' | translate }} + +
+
+
+
+
+
+
+ + + + {{ NotificationDeliveryMethodInfoMap.get(NotificationDeliveryMethod.SMS).icon }} + {{ NotificationDeliveryMethodInfoMap.get(NotificationDeliveryMethod.SMS).name | translate }} + + + + + notification.message + + + + {{ 'notification.message-required' | translate }} + + + {{ 'notification.message-max-length' | translate : + {length: templateConfigurationForm.get('SMS.body').getError('maxlength').requiredLength} + }} + + + + +
+
+ + + + {{ NotificationDeliveryMethodInfoMap.get(NotificationDeliveryMethod.MICROSOFT_TEAMS).icon }} + {{ NotificationDeliveryMethodInfoMap.get(NotificationDeliveryMethod.MICROSOFT_TEAMS).name | translate }} + + + + + notification.subject + + + + notification.message + + + {{ 'notification.message-required' | translate }} + + +
+
+
notification.theme-color
+ +
+
+ + + + + {{ 'notification.action-button' | translate }} + + + + +
+ + notification.button-text + + + {{ 'notification.button-text-required' | translate }} + + + {{'notification.button-text-max-length' | translate : + {length: templateConfigurationForm.get('MICROSOFT_TEAMS.button.text').getError('maxlength').requiredLength} + }} + + +
+
+ + notification.action-type + + + {{ actionButtonLinkTypeTranslateMap.get(actionButtonLinkType) | translate }} + + + + + notification.link + + + {{ 'notification.link-required' | translate }} + + + + + + + + +
+ + {{ 'notification.set-entity-from-notification' | translate }} + +
+
+
+
+
+
+
+
+ + + + {{ NotificationDeliveryMethodInfoMap.get(NotificationDeliveryMethod.SLACK).icon }} + {{ NotificationDeliveryMethodInfoMap.get(NotificationDeliveryMethod.SLACK).name | translate }} + + + + + notification.message + + + {{ 'notification.message-required' | translate }} + + + + +
+
+ + + + {{ NotificationDeliveryMethodInfoMap.get(NotificationDeliveryMethod.EMAIL).icon }} + {{ NotificationDeliveryMethodInfoMap.get(NotificationDeliveryMethod.EMAIL).name | translate }} + + + + + notification.subject + + + {{ 'notification.subject-required' | translate }} + + + notification.message + + + + {{ 'notification.message-required' | translate }} + + + +
+
diff --git a/ui-ngx/src/app/modules/home/pages/notification/template/notification-template-configuration.component.scss b/ui-ngx/src/app/modules/home/pages/notification/template/notification-template-configuration.component.scss new file mode 100644 index 0000000000..b69b950a42 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/notification/template/notification-template-configuration.component.scss @@ -0,0 +1,66 @@ +/** + * Copyright © 2016-2024 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. + */ +:host { + .tb-template-header { + margin-bottom: 12px; + .tb-hint-available-params { + color: rgba(0,0,0,0.54); + letter-spacing: 0.4px; + + .content { + vertical-align: middle; + } + } + } + + .template-tittle { + gap: 12px; + font-weight: normal; + tb-icon { + color: rgba(0, 0, 0, .38); + } + } + + .tb-mat-error { + font-size: 13px; + } + + .tb-title { + font-size: 16px; + line-height: 24px; + + &.tb-required::after { + font-size: initial; + content: "*"; + } + + &.tb-error { + color: var(--mdc-theme-error, #f44336); + + &.tb-required::after { + color: var(--mdc-theme-error, #f44336); + } + } + } +} + +:host ::ng-deep { + .tb-form-panel .mat-expansion-panel.tb-settings { + & > .mat-expansion-panel-content > .mat-expansion-panel-body { + gap: 0; + } + } +} diff --git a/ui-ngx/src/app/modules/home/pages/notification/template/notification-template-configuration.component.ts b/ui-ngx/src/app/modules/home/pages/notification/template/notification-template-configuration.component.ts new file mode 100644 index 0000000000..5be07216d5 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/notification/template/notification-template-configuration.component.ts @@ -0,0 +1,312 @@ +/// +/// Copyright © 2016-2024 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. +/// + +import { Component, forwardRef, Input, OnDestroy } from '@angular/core'; +import { + ControlValueAccessor, + FormBuilder, + FormGroup, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + ValidationErrors, + Validator, + Validators +} from '@angular/forms'; +import { + ActionButtonLinkType, + ActionButtonLinkTypeTranslateMap, + DeliveryMethodsTemplates, + NotificationDeliveryMethod, + NotificationDeliveryMethodInfoMap, + NotificationTemplateTypeTranslateMap, + NotificationType +} from '@shared/models/notification.models'; +import { takeUntil } from 'rxjs/operators'; +import { Subject } from 'rxjs'; +import { isDefinedAndNotNull } from '@core/utils'; +import { coerceBoolean } from '@shared/decorators/coercion'; + +@Component({ + selector: 'tb-template-configuration', + templateUrl: './notification-template-configuration.component.html', + styleUrls: ['./notification-template-configuration.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => NotificationTemplateConfigurationComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => NotificationTemplateConfigurationComponent), + multi: true, + } + ] +}) +export class NotificationTemplateConfigurationComponent implements OnDestroy, ControlValueAccessor, Validator { + + templateConfigurationForm: FormGroup; + + NotificationDeliveryMethodInfoMap = NotificationDeliveryMethodInfoMap; + + @Input() + set predefinedDeliveryMethodsTemplate(value: Partial) { + if (isDefinedAndNotNull(value)) { + this.templateConfigurationForm.patchValue(value, {emitEvent: false}); + this.updateDisabledForms(); + this.templateConfigurationForm.updateValueAndValidity(); + } + } + + @Input() + notificationType: NotificationType; + + @Input() + @coerceBoolean() + interacted: boolean; + + readonly NotificationDeliveryMethod = NotificationDeliveryMethod; + readonly NotificationTemplateTypeTranslateMap = NotificationTemplateTypeTranslateMap; + + readonly actionButtonLinkType = ActionButtonLinkType; + readonly actionButtonLinkTypes = Object.keys(ActionButtonLinkType) as ActionButtonLinkType[]; + readonly actionButtonLinkTypeTranslateMap = ActionButtonLinkTypeTranslateMap; + + tinyMceOptions: Record = { + base_url: '/assets/tinymce', + suffix: '.min', + plugins: ['link table image imagetools code fullscreen'], + menubar: 'edit insert tools view format table', + toolbar: 'fontselect fontsizeselect | formatselect | bold italic strikethrough forecolor backcolor ' + + '| link | table | image | alignleft aligncenter alignright alignjustify ' + + '| numlist bullist outdent indent | removeformat | code | fullscreen', + toolbar_mode: 'sliding', + height: 400, + autofocus: false, + branding: false + }; + + private propagateChange = (v: any) => { }; + private readonly destroy$ = new Subject(); + + constructor(private fb: FormBuilder) { + this.templateConfigurationForm = this.buildForm(); + this.templateConfigurationForm.valueChanges.pipe( + takeUntil(this.destroy$) + ).subscribe((value) => { + this.propagateChange(value); + }); + } + + ngOnDestroy() { + this.destroy$.next(); + this.destroy$.complete(); + } + + writeValue(value: any) { + this.templateConfigurationForm.patchValue(value, {emitEvent: false}); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean) { + if (isDisabled) { + this.templateConfigurationForm.disable({emitEvent: false}); + } else { + this.updateDisabledForms(); + } + } + + validate(): ValidationErrors { + return this.templateConfigurationForm.valid ? null : { + templateConfiguration: { + valid: false, + }, + }; + } + + private updateDisabledForms(){ + Object.values(NotificationDeliveryMethod).forEach((method) => { + const form = this.templateConfigurationForm.get(method); + if (!form.get('enabled').value) { + form.disable({emitEvent: false}); + } else { + form.enable({emitEvent: false}); + switch (method) { + case NotificationDeliveryMethod.WEB: + form.get('additionalConfig.icon.enabled').updateValueAndValidity({onlySelf: true}); + form.get('additionalConfig.actionButtonConfig.enabled').updateValueAndValidity({onlySelf: true}); + break; + case NotificationDeliveryMethod.MICROSOFT_TEAMS: + form.get('button.enabled').updateValueAndValidity({onlySelf: true}); + break; + case NotificationDeliveryMethod.MOBILE_APP: + form.get('additionalConfig.onClick.enabled').updateValueAndValidity({onlySelf: true}); + } + } + }); + } + + private buildForm(): FormGroup { + const form = this.fb.group({}); + + Object.values(NotificationDeliveryMethod).forEach((method) => { + form.addControl(method, this.buildDeliveryMethodControl(method), {emitEvent: false}); + }); + + return form; + } + + private buildDeliveryMethodControl(deliveryMethod: NotificationDeliveryMethod): FormGroup { + let deliveryMethodForm: FormGroup; + switch (deliveryMethod) { + case NotificationDeliveryMethod.WEB: + deliveryMethodForm = this.fb.group({ + subject: ['', Validators.required], + body: ['', Validators.required], + additionalConfig: this.fb.group({ + icon: this.fb.group({ + enabled: [false], + icon: [{value: 'notifications', disabled: true}, Validators.required], + color: [{value: '#757575', disabled: true}] + }), + actionButtonConfig: this.createButtonConfigForm() + }) + }); + + deliveryMethodForm.get('additionalConfig.icon.enabled').valueChanges.pipe( + takeUntil(this.destroy$) + ).subscribe((value) => { + if (value) { + deliveryMethodForm.get('additionalConfig.icon.icon').enable({emitEvent: false}); + deliveryMethodForm.get('additionalConfig.icon.color').enable({emitEvent: false}); + } else { + deliveryMethodForm.get('additionalConfig.icon.icon').disable({emitEvent: false}); + deliveryMethodForm.get('additionalConfig.icon.color').disable({emitEvent: false}); + } + }); + break; + case NotificationDeliveryMethod.EMAIL: + deliveryMethodForm = this.fb.group({ + subject: ['', Validators.required], + body: ['', Validators.required] + }); + break; + case NotificationDeliveryMethod.SMS: + deliveryMethodForm = this.fb.group({ + body: ['', [Validators.required, Validators.maxLength(320)]] + }); + break; + case NotificationDeliveryMethod.SLACK: + deliveryMethodForm = this.fb.group({ + body: ['', Validators.required] + }); + break; + case NotificationDeliveryMethod.MOBILE_APP: + deliveryMethodForm = this.fb.group({ + subject: ['', Validators.required], + body: ['', Validators.required], + additionalConfig: this.fb.group({ + onClick: this.fb.group({ + enabled: [false], + linkType: [{value: ActionButtonLinkType.DASHBOARD, disabled: true}], + dashboardId: [{value: null, disabled: true}, Validators.required], + dashboardState: [{value: null, disabled: true}], + setEntityIdInState: [{value: true, disabled: true}] + }) + }) + }); + deliveryMethodForm.get('additionalConfig.onClick.enabled').valueChanges.pipe( + takeUntil(this.destroy$) + ).subscribe((value) => { + if (value) { + deliveryMethodForm.get('additionalConfig.onClick.linkType').enable({emitEvent: false}); + deliveryMethodForm.get('additionalConfig.onClick.dashboardId').enable({emitEvent: false}); + deliveryMethodForm.get('additionalConfig.onClick.dashboardState').enable({emitEvent: false}); + deliveryMethodForm.get('additionalConfig.onClick.setEntityIdInState').enable({emitEvent: false}); + } else { + deliveryMethodForm.get('additionalConfig.onClick.linkType').disable({emitEvent: false}); + deliveryMethodForm.get('additionalConfig.onClick.dashboardId').disable({emitEvent: false}); + deliveryMethodForm.get('additionalConfig.onClick.dashboardState').disable({emitEvent: false}); + deliveryMethodForm.get('additionalConfig.onClick.setEntityIdInState').disable({emitEvent: false}); + } + }); + break; + case NotificationDeliveryMethod.MICROSOFT_TEAMS: + deliveryMethodForm = this.fb.group({ + subject: [''], + body: ['', Validators.required], + themeColor: [''], + button: this.createButtonConfigForm() + }); + break; + default: + throw new Error(`Not configured templated for notification delivery method: ${deliveryMethod}`); + } + deliveryMethodForm.addControl('enabled', this.fb.control(false), {emitEvent: false}); + deliveryMethodForm.addControl('method', this.fb.control(deliveryMethod), {emitEvent: false}); + return deliveryMethodForm; + } + + private createButtonConfigForm(): FormGroup { + const form = this.fb.group({ + enabled: [false], + text: [{value: '', disabled: true}, [Validators.required, Validators.maxLength(50)]], + linkType: [ActionButtonLinkType.LINK], + link: [{value: '', disabled: true}, Validators.required], + dashboardId: [{value: null, disabled: true}, Validators.required], + dashboardState: [{value: null, disabled: true}], + setEntityIdInState: [{value: true, disabled: true}] + }); + + form.get('enabled').valueChanges.pipe( + takeUntil(this.destroy$) + ).subscribe((value) => { + if (value) { + form.enable({emitEvent: false}); + form.get('linkType').updateValueAndValidity({onlySelf: true}); + } else { + form.disable({emitEvent: false}); + form.get('enabled').enable({emitEvent: false}); + } + }); + + form.get('linkType').valueChanges.pipe( + takeUntil(this.destroy$) + ).subscribe((value) => { + const isEnabled = form.get('enabled').value; + if (isEnabled) { + if (value === ActionButtonLinkType.LINK) { + form.get('link').enable({emitEvent: false}); + form.get('dashboardId').disable({emitEvent: false}); + form.get('dashboardState').disable({emitEvent: false}); + form.get('setEntityIdInState').disable({emitEvent: false}); + } else { + form.get('link').disable({emitEvent: false}); + form.get('dashboardId').enable({emitEvent: false}); + form.get('dashboardState').enable({emitEvent: false}); + form.get('setEntityIdInState').enable({emitEvent: false}); + } + } + }); + return form; + } +} diff --git a/ui-ngx/src/app/modules/home/pages/notification/template/template-configuration.ts b/ui-ngx/src/app/modules/home/pages/notification/template/template-configuration.ts index a53a3eada5..3da842661d 100644 --- a/ui-ngx/src/app/modules/home/pages/notification/template/template-configuration.ts +++ b/ui-ngx/src/app/modules/home/pages/notification/template/template-configuration.ts @@ -16,10 +16,9 @@ import { FormBuilder, FormGroup, ValidationErrors, Validators } from '@angular/forms'; import { - ActionButtonLinkType, - ActionButtonLinkTypeTranslateMap, + DeliveryMethodsTemplates, NotificationDeliveryMethod, - NotificationDeliveryMethodTranslateMap, + NotificationDeliveryMethodInfoMap, NotificationTemplate, NotificationTemplateTypeTranslateMap, NotificationType @@ -27,47 +26,25 @@ import { import { takeUntil } from 'rxjs/operators'; import { Subject } from 'rxjs'; import { Directive, OnDestroy } from '@angular/core'; +import { deepClone, deepTrim } from '@core/utils'; import { DialogComponent } from '@shared/components/dialog.component'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { Router } from '@angular/router'; import { MatDialogRef } from '@angular/material/dialog'; -import { deepClone, deepTrim } from '@core/utils'; -import tinymce from 'tinymce'; @Directive() // tslint:disable-next-line:directive-class-suffix export abstract class TemplateConfiguration extends DialogComponent implements OnDestroy{ templateNotificationForm: FormGroup; - webTemplateForm: FormGroup; - emailTemplateForm: FormGroup; - smsTemplateForm: FormGroup; - slackTemplateForm: FormGroup; - microsoftTeamsTemplateForm: FormGroup; - mobileTemplateForm: FormGroup; + notificationTemplateConfigurationForm: FormGroup; notificationDeliveryMethods = Object.keys(NotificationDeliveryMethod) as NotificationDeliveryMethod[]; - notificationDeliveryMethodTranslateMap = NotificationDeliveryMethodTranslateMap; + notificationDeliveryMethodInfoMap = NotificationDeliveryMethodInfoMap; notificationTemplateTypeTranslateMap = NotificationTemplateTypeTranslateMap; - actionButtonLinkType = ActionButtonLinkType; - actionButtonLinkTypes = Object.keys(ActionButtonLinkType) as ActionButtonLinkType[]; - actionButtonLinkTypeTranslateMap = ActionButtonLinkTypeTranslateMap; - - tinyMceOptions: Record = { - base_url: '/assets/tinymce', - suffix: '.min', - plugins: ['link table image imagetools code fullscreen'], - menubar: 'edit insert tools view format table', - toolbar: 'fontselect fontsizeselect | formatselect | bold italic strikethrough forecolor backcolor ' + - '| link | table | image | alignleft aligncenter alignright alignjustify ' + - '| numlist bullist outdent indent | removeformat | code | fullscreen', - toolbar_mode: 'sliding', - height: 400, - autofocus: false, - branding: false - }; + deliveryConfiguration: Partial; protected readonly destroy$ = new Subject(); @@ -87,69 +64,22 @@ export abstract class TemplateConfiguration extends DialogComponent< }) }); - this.notificationDeliveryMethods.forEach(method => { - (this.templateNotificationForm.get('configuration.deliveryMethodsTemplates') as FormGroup) - .addControl(method, this.fb.group({enabled: method === NotificationDeliveryMethod.WEB}), {emitEvent: false}); - }); - - this.webTemplateForm = this.fb.group({ - subject: ['', Validators.required], - body: ['', Validators.required], - additionalConfig: this.fb.group({ - icon: this.fb.group({ - enabled: [false], - icon: [{value: 'notifications', disabled: true}, Validators.required], - color: [{value: '#757575', disabled: true}] - }), - actionButtonConfig: this.createButtonConfigForm() - }) - }); - - this.webTemplateForm.get('additionalConfig.icon.enabled').valueChanges.pipe( + this.templateNotificationForm.get('configuration.deliveryMethodsTemplates').valueChanges.pipe( takeUntil(this.destroy$) ).subscribe((value) => { - if (value) { - this.webTemplateForm.get('additionalConfig.icon.icon').enable({emitEvent: false}); - this.webTemplateForm.get('additionalConfig.icon.color').enable({emitEvent: false}); - } else { - this.webTemplateForm.get('additionalConfig.icon.icon').disable({emitEvent: false}); - this.webTemplateForm.get('additionalConfig.icon.color').disable({emitEvent: false}); - } + this.deliveryConfiguration = value; }); - this.emailTemplateForm = this.fb.group({ - subject: ['', Validators.required], - body: ['', Validators.required] + this.notificationTemplateConfigurationForm = this.fb.group({ + deliveryMethodsTemplates: null }); - this.smsTemplateForm = this.fb.group({ - body: ['', [Validators.required, Validators.maxLength(320)]] - }); - - this.slackTemplateForm = this.fb.group({ - body: ['', Validators.required] - }); - - this.mobileTemplateForm = this.fb.group({ - subject: ['', Validators.required], - body: ['', Validators.required] - }); - - this.microsoftTeamsTemplateForm = this.fb.group({ - subject: [''], - body: ['', Validators.required], - themeColor: [''], - button: this.createButtonConfigForm() + this.notificationDeliveryMethods.forEach(method => { + (this.templateNotificationForm.get('configuration.deliveryMethodsTemplates') as FormGroup) + .addControl(method, this.fb.group({enabled: method === NotificationDeliveryMethod.WEB}), {emitEvent: false}); }); - this.deliveryMethodFormsMap = new Map([ - [NotificationDeliveryMethod.WEB, this.webTemplateForm], - [NotificationDeliveryMethod.EMAIL, this.emailTemplateForm], - [NotificationDeliveryMethod.SMS, this.smsTemplateForm], - [NotificationDeliveryMethod.SLACK, this.slackTemplateForm], - [NotificationDeliveryMethod.MICROSOFT_TEAMS, this.microsoftTeamsTemplateForm], - [NotificationDeliveryMethod.MOBILE_APP, this.mobileTemplateForm] - ]); + this.deliveryConfiguration = this.templateNotificationForm.get('configuration.deliveryMethodsTemplates').value; } ngOnDestroy() { @@ -169,58 +99,8 @@ export abstract class TemplateConfiguration extends DialogComponent< } protected getNotificationTemplateValue(): NotificationTemplate { - const template: NotificationTemplate = deepClone(this.templateNotificationForm.value); - this.notificationDeliveryMethods.forEach(method => { - if (template.configuration.deliveryMethodsTemplates[method]?.enabled) { - Object.assign(template.configuration.deliveryMethodsTemplates[method], this.deliveryMethodFormsMap.get(method).value, {method}); - } else { - delete template.configuration.deliveryMethodsTemplates[method]; - } - }); + const template = deepClone(this.templateNotificationForm.value); + template.configuration = deepClone(this.notificationTemplateConfigurationForm.value); return deepTrim(template); } - - private createButtonConfigForm(): FormGroup { - const form = this.fb.group({ - enabled: [false], - text: [{value: '', disabled: true}, [Validators.required, Validators.maxLength(50)]], - linkType: [ActionButtonLinkType.LINK], - link: [{value: '', disabled: true}, Validators.required], - dashboardId: [{value: null, disabled: true}, Validators.required], - dashboardState: [{value: null, disabled: true}], - setEntityIdInState: [{value: true, disabled: true}], - }); - - form.get('enabled').valueChanges.pipe( - takeUntil(this.destroy$) - ).subscribe((value) => { - if (value) { - form.enable({emitEvent: false}); - form.get('linkType').updateValueAndValidity({onlySelf: true}); - } else { - form.disable({emitEvent: false}); - form.get('enabled').enable({emitEvent: false}); - } - }); - - form.get('linkType').valueChanges.pipe( - takeUntil(this.destroy$) - ).subscribe((value) => { - const isEnabled = form.get('enabled').value; - if (isEnabled) { - if (value === ActionButtonLinkType.LINK) { - form.get('link').enable({emitEvent: false}); - form.get('dashboardId').disable({emitEvent: false}); - form.get('dashboardState').disable({emitEvent: false}); - form.get('setEntityIdInState').disable({emitEvent: false}); - } else { - form.get('link').disable({emitEvent: false}); - form.get('dashboardId').enable({emitEvent: false}); - form.get('dashboardState').enable({emitEvent: false}); - form.get('setEntityIdInState').enable({emitEvent: false}); - } - } - }); - return form; - } } diff --git a/ui-ngx/src/app/modules/home/pages/notification/template/template-notification-dialog.component.html b/ui-ngx/src/app/modules/home/pages/notification/template/template-notification-dialog.component.html index 3df482f633..09e673bb4b 100644 --- a/ui-ngx/src/app/modules/home/pages/notification/template/template-notification-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/notification/template/template-notification-dialog.component.html @@ -29,14 +29,14 @@
check - {{ 'notification.basic-settings' | translate }} + {{ 'notification.setup' | translate }}
notification.name @@ -62,339 +62,22 @@ class="delivery-method-container" [formGroupName]="deliveryMethods"> - {{ notificationDeliveryMethodTranslateMap.get(deliveryMethods) | translate }} + {{ notificationDeliveryMethodInfoMap.get(deliveryMethods).name | translate }}
- - {{ 'notification.delivery-method.web' | translate }} -
- {{ 'notification.input-fields-support-templatization' | translate}} - -
-
- - notification.subject - - - {{ 'notification.subject-required' | translate }} - - - - notification.message - - - {{ 'notification.message-required' | translate }} - - -
-
- - {{ 'icon.icon' | translate }} - -
- - - - -
-
-
- - - - - {{ 'notification.action-button' | translate }} - - - - -
- - notification.button-text - - - {{ 'notification.button-text-required' | translate }} - - - {{ 'notification.button-text-max-length' | translate : - {length: webTemplateForm.get('additionalConfig.actionButtonConfig.text').getError('maxlength').requiredLength} - }} - - -
-
- - notification.action-type - - - {{ actionButtonLinkTypeTranslateMap.get(actionButtonLinkType) | translate }} - - - - - notification.link - - - {{ 'notification.link-required' | translate }} - - - - - - - - -
- - {{ 'notification.set-entity-from-notification' | translate }} - -
-
-
-
-
-
- - {{ 'notification.delivery-method.email' | translate }} - -
- {{ 'notification.input-fields-support-templatization' | translate}} - -
-
- - notification.subject - - - {{ 'notification.subject-required' | translate }} - - - notification.message - - - {{ 'notification.message-required' | translate }} - -
-
-
- - {{ 'notification.delivery-method.sms' | translate }} -
- {{ 'notification.input-field-support-templatization' | translate}} - -
-
- - notification.message - - - {{ 'notification.message-required' | translate }} - - - {{ 'notification.message-max-length' | translate : - {length: smsTemplateForm.get('body').getError('maxlength').requiredLength} - }} - - -
-
- - {{ 'notification.delivery-method.slack' | translate }} -
- {{ 'notification.input-field-support-templatization' | translate}} - -
-
- - notification.message - - - {{ 'notification.message-required' | translate }} - - -
-
- - {{ 'notification.delivery-method.microsoft-teams' | translate }} -
- {{ 'notification.input-fields-support-templatization' | translate}} - -
-
- - notification.subject - - - - notification.message - - - {{ 'notification.message-required' | translate }} - - -
-
-
notification.theme-color
- -
-
- - - - - {{ 'notification.action-button' | translate }} - - - - -
- - notification.button-text - - - {{ 'notification.button-text-required' | translate }} - - - {{ 'notification.button-text-max-length' | translate : - {length: microsoftTeamsTemplateForm.get('button.text').getError('maxlength').requiredLength} - }} - - -
-
- - notification.action-type - - - {{ actionButtonLinkTypeTranslateMap.get(actionButtonLinkType) | translate }} - - - - - notification.link - - - {{ 'notification.link-required' | translate }} - - - - - - - - -
- - {{ 'notification.set-entity-from-notification' | translate }} - -
-
-
-
-
-
- - {{ 'notification.delivery-method.mobile-app' | translate }} -
- {{ 'notification.input-field-support-templatization' | translate}} - -
-
- - notification.subject - - - {{ 'notification.subject-required' | translate }} - - - - notification.message - - - {{ 'notification.message-required' | translate }} - - + + {{ 'notification.compose' | translate }} + + +
diff --git a/ui-ngx/src/app/modules/home/pages/notification/template/template-notification-dialog.component.scss b/ui-ngx/src/app/modules/home/pages/notification/template/template-notification-dialog.component.scss index 51afcfbe33..0e28d5d5e3 100644 --- a/ui-ngx/src/app/modules/home/pages/notification/template/template-notification-dialog.component.scss +++ b/ui-ngx/src/app/modules/home/pages/notification/template/template-notification-dialog.component.scss @@ -17,7 +17,7 @@ @import "../../../../../../theme"; :host { - width: 840px; + width: 775px; height: 100%; max-width: 100%; max-height: 100vh; @@ -62,25 +62,10 @@ } } - .tb-mat-error { - font-size: 13px; - } - .tb-hint { padding: 0 0 8px; } - .tb-hint-available-params { - border-radius: 6px; - background-color: rgba(48, 86, 128, 0.04); - margin-bottom: 8px; - padding: 8px 16px; - - .content { - vertical-align: middle; - } - } - .delivery-methods-container { margin-bottom: 20px; display: flex; @@ -125,13 +110,5 @@ } } } - - .tb-form-panel .mat-expansion-panel.tb-settings { - padding: 11px 16px; - - & > .mat-expansion-panel-content > .mat-expansion-panel-body { - gap: 0; - } - } } } diff --git a/ui-ngx/src/app/modules/home/pages/notification/template/template-notification-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/notification/template/template-notification-dialog.component.ts index cda2309ffd..a2d62c023a 100644 --- a/ui-ngx/src/app/modules/home/pages/notification/template/template-notification-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/pages/notification/template/template-notification-dialog.component.ts @@ -14,13 +14,13 @@ /// limitations under the License. /// -import { NotificationDeliveryMethod, NotificationTemplate, NotificationType } from '@shared/models/notification.models'; +import { NotificationTemplate, NotificationType } from '@shared/models/notification.models'; import { Component, Inject, OnDestroy, ViewChild } from '@angular/core'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { Router } from '@angular/router'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; -import { FormBuilder } from '@angular/forms'; +import { FormBuilder, FormGroup } from '@angular/forms'; import { NotificationService } from '@core/http/notification.service'; import { deepClone, isDefinedAndNotNull } from '@core/utils'; import { Observable } from 'rxjs'; @@ -31,8 +31,7 @@ import { BreakpointObserver } from '@angular/cdk/layout'; import { MediaBreakpoints } from '@shared/models/constants'; import { TranslateService } from '@ngx-translate/core'; import { TemplateConfiguration } from '@home/pages/notification/template/template-configuration'; -import { AuthState } from '@core/auth/auth.models'; -import { getCurrentAuthState } from '@core/auth/auth.selectors'; +import { getCurrentAuthUser } from '@core/auth/auth.selectors'; import { AuthUser } from '@shared/models/user.model'; import { Authority } from '@shared/models/authority.enum'; @@ -62,9 +61,10 @@ export class TemplateNotificationDialogComponent selectedIndex = 0; hideSelectType = false; + notificationTemplateConfigurationForm: FormGroup; + private readonly templateNotification: NotificationTemplate; - private authState: AuthState = getCurrentAuthState(this.store); - private authUser: AuthUser = this.authState.authUser; + private authUser: AuthUser = getCurrentAuthUser(this.store); constructor(protected store: Store, protected router: Router, @@ -78,7 +78,7 @@ export class TemplateNotificationDialogComponent this.notificationTypes = this.allowNotificationType(); - this.stepperOrientation = this.breakpointObserver.observe(MediaBreakpoints['gt-sm']) + this.stepperOrientation = this.breakpointObserver.observe(MediaBreakpoints['gt-xs']) .pipe(map(({matches}) => matches ? 'horizontal' : 'vertical')); if (isDefinedAndNotNull(this.data?.predefinedType)) { @@ -99,11 +99,9 @@ export class TemplateNotificationDialogComponent } this.templateNotificationForm.reset({}, {emitEvent: false}); this.templateNotificationForm.patchValue(this.templateNotification, {emitEvent: false}); - // eslint-disable-next-line guard-for-in - for (const method in this.templateNotification.configuration.deliveryMethodsTemplates) { - this.deliveryMethodFormsMap.get(NotificationDeliveryMethod[method]) - .patchValue(this.templateNotification.configuration.deliveryMethodsTemplates[method]); - } + this.notificationTemplateConfigurationForm.patchValue({ + deliveryMethodsTemplates: this.templateNotification.configuration.deliveryMethodsTemplates + }, {emitEvent: false}); } } @@ -119,6 +117,9 @@ export class TemplateNotificationDialogComponent changeStep($event: StepperSelectionEvent) { this.selectedIndex = $event.selectedIndex; + if ($event.previouslySelectedIndex > $event.selectedIndex) { + $event.previouslySelectedStep.interacted = false; + } } backStep() { diff --git a/ui-ngx/src/app/shared/components/notification/template-autocomplete.component.html b/ui-ngx/src/app/shared/components/notification/template-autocomplete.component.html index 4298cef045..97a17f07a2 100644 --- a/ui-ngx/src/app/shared/components/notification/template-autocomplete.component.html +++ b/ui-ngx/src/app/shared/components/notification/template-autocomplete.component.html @@ -51,7 +51,7 @@ - {{ notificationDeliveryMethodTranslateMap.get(method.key) | translate }} + {{ notificationDeliveryMethodInfoMap.get(method.key).name | translate }} diff --git a/ui-ngx/src/app/shared/components/notification/template-autocomplete.component.ts b/ui-ngx/src/app/shared/components/notification/template-autocomplete.component.ts index 9c83b3443c..12fcae7693 100644 --- a/ui-ngx/src/app/shared/components/notification/template-autocomplete.component.ts +++ b/ui-ngx/src/app/shared/components/notification/template-autocomplete.component.ts @@ -28,7 +28,7 @@ import { PageLink } from '@shared/models/page/page-link'; import { Direction } from '@shared/models/page/sort-order'; import { emptyPageData } from '@shared/models/page/page-data'; import { - NotificationDeliveryMethodTranslateMap, + NotificationDeliveryMethodInfoMap, NotificationTemplate, NotificationType } from '@shared/models/notification.models'; @@ -55,7 +55,7 @@ import { coerceBoolean } from '@shared/decorators/coercion'; }) export class TemplateAutocompleteComponent implements ControlValueAccessor, OnInit { - notificationDeliveryMethodTranslateMap = NotificationDeliveryMethodTranslateMap; + notificationDeliveryMethodInfoMap = NotificationDeliveryMethodInfoMap; selectTemplateFormGroup: FormGroup; @Input() diff --git a/ui-ngx/src/app/shared/components/slack-conversation-autocomplete.component.ts b/ui-ngx/src/app/shared/components/slack-conversation-autocomplete.component.ts index 0d73933c8f..39da44a13c 100644 --- a/ui-ngx/src/app/shared/components/slack-conversation-autocomplete.component.ts +++ b/ui-ngx/src/app/shared/components/slack-conversation-autocomplete.component.ts @@ -24,11 +24,7 @@ import { TranslateService } from '@ngx-translate/core'; import { coerceBooleanProperty } from '@angular/cdk/coercion'; import { EntityService } from '@core/http/entity.service'; import { TruncatePipe } from '@shared/pipe/truncate.pipe'; -import { - NotificationDeliveryMethodTranslateMap, - SlackChanelType, - SlackConversation -} from '@shared/models/notification.models'; +import { SlackChanelType, SlackConversation } from '@shared/models/notification.models'; import { NotificationService } from '@core/http/notification.service'; import { isEqual } from '@core/utils'; @@ -44,7 +40,6 @@ import { isEqual } from '@core/utils'; }) export class SlackConversationAutocompleteComponent implements ControlValueAccessor, OnInit, OnChanges { - notificationDeliveryMethodTranslateMap = NotificationDeliveryMethodTranslateMap; conversationSlackFormGroup: FormGroup; @Input() diff --git a/ui-ngx/src/app/shared/models/notification.models.ts b/ui-ngx/src/app/shared/models/notification.models.ts index 4d352d352d..44451bc800 100644 --- a/ui-ngx/src/app/shared/models/notification.models.ts +++ b/ui-ngx/src/app/shared/models/notification.models.ts @@ -311,9 +311,11 @@ export interface NotificationTemplate extends Omit([ - [NotificationDeliveryMethod.WEB, 'notification.delivery-method.web'], - [NotificationDeliveryMethod.SMS, 'notification.delivery-method.sms'], - [NotificationDeliveryMethod.EMAIL, 'notification.delivery-method.email'], - [NotificationDeliveryMethod.SLACK, 'notification.delivery-method.slack'], - [NotificationDeliveryMethod.MOBILE_APP, 'notification.delivery-method.mobile-app'], - [NotificationDeliveryMethod.MICROSOFT_TEAMS, 'notification.delivery-method.microsoft-teams'] +export const NotificationDeliveryMethodInfoMap = new Map([ + [NotificationDeliveryMethod.WEB, + { + name: 'notification.delivery-method.web', + icon: 'mdi:bell-badge' + } + ], + [NotificationDeliveryMethod.SMS, + { + name: 'notification.delivery-method.sms', + icon: 'mdi:message-processing' + } + ], + [NotificationDeliveryMethod.EMAIL, + { + name: 'notification.delivery-method.email', + icon: 'mdi:email' + }], + [NotificationDeliveryMethod.SLACK, + { + name: 'notification.delivery-method.slack', + icon: 'mdi:slack' + } + ], + [NotificationDeliveryMethod.MOBILE_APP, + { + name: 'notification.delivery-method.mobile-app', + icon: 'mdi:cellphone-text' + }], + [NotificationDeliveryMethod.MICROSOFT_TEAMS, + { + name: 'notification.delivery-method.microsoft-teams', + icon: 'mdi:microsoft-teams' + }] ]); export enum NotificationRequestStatus { diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 47d6d97556..f22a599aa8 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -3233,7 +3233,6 @@ "new-platform-version-trigger-settings": "New platform version trigger settings", "rate-limits-trigger-settings": "Exceeded rate limits trigger settings", "at-least-one-should-be-selected": "At least one should be selected", - "basic-settings": "Basic settings", "button-text": "Button text", "button-text-required": "Button text is required", "button-text-max-length": "Button text should be less than or equal to {{ length }} characters", @@ -3245,6 +3244,7 @@ "copy-template": "Copy template", "create-new": "Create new", "created": "Created", + "customize-messages": "Customize messages", "delete-notification-text": "Be careful, after the confirmation the notification will become unrecoverable.", "delete-notification-title": "Are you sure you want to delete the notification?", "delete-notifications-text": "Be careful, after the confirmation notifications will become unrecoverable.", @@ -3353,6 +3353,7 @@ "notify-only-user-comments": "Notify only user comments", "only-rule-chain-lifecycle-failures": "Only rule chain lifecycle failures", "only-rule-node-lifecycle-failures": "Only rule node lifecycle failures", + "open-dashboard-on-click-notification": "Open dashboard on click notification", "platform-users": "Platform users", "rate-limits": "Rate limits", "rate-limits-hint": "If the field is empty, the trigger will be applied to all rate limits", @@ -3405,6 +3406,7 @@ "selected-template": "{ count, plural, =1 {1 template} other {# templates} } selected", "send-notification": "Send notification", "sent": "Sent", + "setup": "Setup", "notification-sent": "Notifications / Sent", "set-entity-from-notification": "Set entity from notification to dashboard state", "slack-chanel-type": "Slack channel type", From fcf5921b7e43185e2dc39843069b6d1b5369aa71 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Wed, 14 Feb 2024 15:59:41 +0200 Subject: [PATCH 25/33] UI: Clear style code in notification component --- .../sent-notification-dialog.component.html | 108 ++++++++--------- .../sent-notification-dialog.component.scss | 53 ++------ .../sent/sent-notification-dialog.componet.ts | 2 +- ...tion-template-configuration.component.html | 114 +++++++++--------- ...tion-template-configuration.component.scss | 10 +- ...emplate-notification-dialog.component.html | 4 +- ...emplate-notification-dialog.component.scss | 18 +-- .../app/shared/models/notification.models.ts | 4 +- 8 files changed, 132 insertions(+), 181 deletions(-) diff --git a/ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.component.html b/ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.component.html index 71daee5343..8f5d41de34 100644 --- a/ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.component.html @@ -39,18 +39,10 @@ {{ 'notification.compose' | translate }}
- - + + {{ 'notification.start-from-scratch' | translate }} + {{ 'notification.use-template' | translate }} +
-
notification.at-least-one-should-be-selected
+
notification.at-least-one-should-be-selected