This is an automated email from the ASF dual-hosted git repository.

diegopucci pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git


The following commit(s) were added to refs/heads/master by this push:
     new aa2b060da8 feat: Customizable email subject name (#26327)
aa2b060da8 is described below

commit aa2b060da8069bedd4b33a075be1be8f282dcb2f
Author: Puridach wutthihathaithamrong 
<[email protected]>
AuthorDate: Thu May 16 20:04:42 2024 +0700

    feat: Customizable email subject name (#26327)
    
    Co-authored-by: Puridach Wutthihathaithamrong <>
---
 .../src/features/alerts/AlertReportModal.tsx       |  46 +++++++++
 .../alerts/components/NotificationMethod.tsx       | 104 ++++++++++++++++++---
 superset-frontend/src/features/alerts/types.ts     |   1 +
 superset/commands/report/execute.py                |  21 +++--
 ...d56ffb_add_subject_column_to_report_schedule.py |  41 ++++++++
 superset/reports/api.py                            |   1 +
 superset/reports/models.py                         |   2 +
 superset/reports/schemas.py                        |  17 ++++
 8 files changed, 209 insertions(+), 24 deletions(-)

diff --git a/superset-frontend/src/features/alerts/AlertReportModal.tsx 
b/superset-frontend/src/features/alerts/AlertReportModal.tsx
index fd0cf7eda9..ac2f6a5650 100644
--- a/superset-frontend/src/features/alerts/AlertReportModal.tsx
+++ b/superset-frontend/src/features/alerts/AlertReportModal.tsx
@@ -367,6 +367,7 @@ export const TRANSLATIONS = {
   CRONTAB_ERROR_TEXT: t('crontab'),
   WORKING_TIMEOUT_ERROR_TEXT: t('working timeout'),
   RECIPIENTS_ERROR_TEXT: t('recipients'),
+  EMAIL_SUBJECT_ERROR_TEXT: t('email subject'),
   ERROR_TOOLTIP_MESSAGE: t(
     'Not all required fields are complete. Please provide the following:',
   ),
@@ -491,6 +492,9 @@ const AlertReportModal: 
FunctionComponent<AlertReportModalProps> = ({
   const [notificationSettings, setNotificationSettings] = useState<
     NotificationSetting[]
   >([]);
+  const [emailSubject, setEmailSubject] = useState<string>('');
+  const [emailError, setEmailError] = useState(false);
+
   const onNotificationAdd = () => {
     setNotificationSettings([
       ...notificationSettings,
@@ -543,6 +547,7 @@ const AlertReportModal: 
FunctionComponent<AlertReportModalProps> = ({
     owners: [],
     recipients: [],
     sql: '',
+    email_subject: '',
     validator_config_json: {},
     validator_type: '',
     force_screenshot: false,
@@ -888,6 +893,10 @@ const AlertReportModal: 
FunctionComponent<AlertReportModalProps> = ({
     const parsedValue = type === 'number' ? parseInt(value, 10) || null : 
value;
 
     updateAlertState(name, parsedValue);
+
+    if (name === 'name') {
+      updateEmailSubject();
+    }
   };
 
   const onCustomWidthChange = (value: number | null | undefined) => {
@@ -1058,6 +1067,11 @@ const AlertReportModal: 
FunctionComponent<AlertReportModalProps> = ({
   const validateNotificationSection = () => {
     const hasErrors = !checkNotificationSettings();
     const errors = hasErrors ? [TRANSLATIONS.RECIPIENTS_ERROR_TEXT] : [];
+
+    if (emailError) {
+      errors.push(TRANSLATIONS.EMAIL_SUBJECT_ERROR_TEXT);
+    }
+
     updateValidationStatus(Sections.Notification, errors);
   };
 
@@ -1199,6 +1213,7 @@ const AlertReportModal: 
FunctionComponent<AlertReportModalProps> = ({
   const currentAlertSafe = currentAlert || {};
   useEffect(() => {
     validateAll();
+    updateEmailSubject();
   }, [
     currentAlertSafe.name,
     currentAlertSafe.owners,
@@ -1212,6 +1227,7 @@ const AlertReportModal: 
FunctionComponent<AlertReportModalProps> = ({
     contentType,
     notificationSettings,
     conditionNotNull,
+    emailError,
   ]);
   useEffect(() => {
     enforceValidation();
@@ -1243,6 +1259,32 @@ const AlertReportModal: 
FunctionComponent<AlertReportModalProps> = ({
     return titleText;
   };
 
+  const updateEmailSubject = () => {
+    if (contentType === 'chart') {
+      if (currentAlert?.name || currentAlert?.chart?.label) {
+        setEmailSubject(
+          `${currentAlert?.name}: ${currentAlert?.chart?.label || ''}`,
+        );
+      } else {
+        setEmailSubject('');
+      }
+    } else if (contentType === 'dashboard') {
+      if (currentAlert?.name || currentAlert?.dashboard?.label) {
+        setEmailSubject(
+          `${currentAlert?.name}: ${currentAlert?.dashboard?.label || ''}`,
+        );
+      } else {
+        setEmailSubject('');
+      }
+    } else {
+      setEmailSubject('');
+    }
+  };
+
+  const handleErrorUpdate = (hasError: boolean) => {
+    setEmailError(hasError);
+  };
+
   return (
     <StyledModal
       className="no-content-padding"
@@ -1690,6 +1732,10 @@ const AlertReportModal: 
FunctionComponent<AlertReportModalProps> = ({
                 key={`NotificationMethod-${i}`}
                 onUpdate={updateNotificationSetting}
                 onRemove={removeNotificationSetting}
+                onInputChange={onInputChange}
+                email_subject={currentAlert?.email_subject || ''}
+                defaultSubject={emailSubject || ''}
+                setErrorSubject={handleErrorUpdate}
               />
             </StyledNotificationMethodWrapper>
           ))}
diff --git 
a/superset-frontend/src/features/alerts/components/NotificationMethod.tsx 
b/superset-frontend/src/features/alerts/components/NotificationMethod.tsx
index b50988700e..614018c12a 100644
--- a/superset-frontend/src/features/alerts/components/NotificationMethod.tsx
+++ b/superset-frontend/src/features/alerts/components/NotificationMethod.tsx
@@ -30,6 +30,12 @@ const StyledNotificationMethod = styled.div`
     textarea {
       height: auto;
     }
+
+    &.error {
+      input {
+        border-color: ${({ theme }) => theme.colors.error.base};
+      }
+    }
   }
 
   .inline-container {
@@ -51,18 +57,36 @@ interface NotificationMethodProps {
   index: number;
   onUpdate?: (index: number, updatedSetting: NotificationSetting) => void;
   onRemove?: (index: number) => void;
+  onInputChange?: (
+    event: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>,
+  ) => void;
+  email_subject: string;
+  defaultSubject: string;
+  setErrorSubject: (hasError: boolean) => void;
 }
 
+const TRANSLATIONS = {
+  EMAIL_SUBJECT_NAME: t('Email subject name (optional)'),
+  EMAIL_SUBJECT_ERROR_TEXT: t(
+    'Please enter valid text. Spaces alone are not permitted.',
+  ),
+};
+
 export const NotificationMethod: FunctionComponent<NotificationMethodProps> = 
({
   setting = null,
   index,
   onUpdate,
   onRemove,
+  onInputChange,
+  email_subject,
+  defaultSubject,
+  setErrorSubject,
 }) => {
   const { method, recipients, options } = setting || {};
   const [recipientValue, setRecipientValue] = useState<string>(
     recipients || '',
   );
+  const [error, setError] = useState(false);
   const theme = useTheme();
 
   if (!setting) {
@@ -100,6 +124,22 @@ export const NotificationMethod: 
FunctionComponent<NotificationMethodProps> = ({
     }
   };
 
+  const onSubjectChange = (
+    event: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>,
+  ) => {
+    const { value } = event.target;
+
+    if (onInputChange) {
+      onInputChange(event);
+    }
+
+    const hasError = value.length > 0 && value.trim().length === 0;
+    setError(hasError);
+    if (setErrorSubject) {
+      setErrorSubject(hasError);
+    }
+  };
+
   // Set recipients
   if (!!recipients && recipientValue !== recipients) {
     setRecipientValue(recipients);
@@ -138,23 +178,57 @@ export const NotificationMethod: 
FunctionComponent<NotificationMethodProps> = ({
         </StyledInputContainer>
       </div>
       {method !== undefined ? (
-        <StyledInputContainer>
-          <div className="control-label">
-            {t('%s recipients', method)}
-            <span className="required">*</span>
+        <>
+          <div className="inline-container">
+            <StyledInputContainer>
+              {method === 'Email' ? (
+                <>
+                  <div className="control-label">
+                    {TRANSLATIONS.EMAIL_SUBJECT_NAME}
+                  </div>
+                  <div className={`input-container ${error ? 'error' : ''}`}>
+                    <input
+                      type="text"
+                      name="email_subject"
+                      value={email_subject}
+                      placeholder={defaultSubject}
+                      onChange={onSubjectChange}
+                    />
+                  </div>
+                  {error && (
+                    <div
+                      style={{
+                        color: theme.colors.error.base,
+                        fontSize: theme.gridUnit * 3,
+                      }}
+                    >
+                      {TRANSLATIONS.EMAIL_SUBJECT_ERROR_TEXT}
+                    </div>
+                  )}
+                </>
+              ) : null}
+            </StyledInputContainer>
           </div>
-          <div className="input-container">
-            <textarea
-              name="recipients"
-              data-test="recipients"
-              value={recipientValue}
-              onChange={onRecipientsChange}
-            />
+          <div className="inline-container">
+            <StyledInputContainer>
+              <div className="control-label">
+                {t('%s recipients', method)}
+                <span className="required">*</span>
+              </div>
+              <div className="input-container">
+                <textarea
+                  name="recipients"
+                  data-test="recipients"
+                  value={recipientValue}
+                  onChange={onRecipientsChange}
+                />
+              </div>
+              <div className="helper">
+                {t('Recipients are separated by "," or ";"')}
+              </div>
+            </StyledInputContainer>
           </div>
-          <div className="helper">
-            {t('Recipients are separated by "," or ";"')}
-          </div>
-        </StyledInputContainer>
+        </>
       ) : null}
     </StyledNotificationMethod>
   );
diff --git a/superset-frontend/src/features/alerts/types.ts 
b/superset-frontend/src/features/alerts/types.ts
index 6dd57ccf0a..932a744663 100644
--- a/superset-frontend/src/features/alerts/types.ts
+++ b/superset-frontend/src/features/alerts/types.ts
@@ -79,6 +79,7 @@ export type AlertObject = {
   dashboard_id?: number;
   database?: MetaObject;
   description?: string;
+  email_subject?: string;
   error?: string;
   force_screenshot: boolean;
   grace_period?: number;
diff --git a/superset/commands/report/execute.py 
b/superset/commands/report/execute.py
index 42563c72d7..d521ac161f 100644
--- a/superset/commands/report/execute.py
+++ b/superset/commands/report/execute.py
@@ -391,16 +391,19 @@ class BaseReportState:
         ):
             embedded_data = self._get_embedded_data()
 
-        if self._report_schedule.chart:
-            name = (
-                f"{self._report_schedule.name}: "
-                f"{self._report_schedule.chart.slice_name}"
-            )
+        if self._report_schedule.email_subject:
+            name = self._report_schedule.email_subject
         else:
-            name = (
-                f"{self._report_schedule.name}: "
-                f"{self._report_schedule.dashboard.dashboard_title}"
-            )
+            if self._report_schedule.chart:
+                name = (
+                    f"{self._report_schedule.name}: "
+                    f"{self._report_schedule.chart.slice_name}"
+                )
+            else:
+                name = (
+                    f"{self._report_schedule.name}: "
+                    f"{self._report_schedule.dashboard.dashboard_title}"
+                )
 
         return NotificationContent(
             name=name,
diff --git 
a/superset/migrations/versions/2024-05-10_11-09_9621c6d56ffb_add_subject_column_to_report_schedule.py
 
b/superset/migrations/versions/2024-05-10_11-09_9621c6d56ffb_add_subject_column_to_report_schedule.py
new file mode 100644
index 0000000000..7af2038f64
--- /dev/null
+++ 
b/superset/migrations/versions/2024-05-10_11-09_9621c6d56ffb_add_subject_column_to_report_schedule.py
@@ -0,0 +1,41 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you 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.
+"""add subject column to report schedule
+
+Revision ID: 9621c6d56ffb
+Revises: 4081be5b6b74
+Create Date: 2024-05-10 11:09:12.046862
+
+"""
+
+import sqlalchemy as sa
+from alembic import op
+
+# revision identifiers, used by Alembic.
+revision = "9621c6d56ffb"
+down_revision = "4081be5b6b74"
+
+
+def upgrade():
+    op.add_column(
+        "report_schedule",
+        sa.Column("email_subject", sa.String(length=255), nullable=True),
+    )
+
+
+def downgrade():
+    op.drop_column("report_schedule", "email_subject")
diff --git a/superset/reports/api.py b/superset/reports/api.py
index 8238213fef..4a298b564d 100644
--- a/superset/reports/api.py
+++ b/superset/reports/api.py
@@ -119,6 +119,7 @@ class ReportScheduleRestApi(BaseSupersetModelRestApi):
         "validator_config_json",
         "validator_type",
         "working_timeout",
+        "email_subject",
     ]
     show_select_columns = show_columns + [
         "chart.datasource_id",
diff --git a/superset/reports/models.py b/superset/reports/models.py
index 63f904876f..3627a2ebf4 100644
--- a/superset/reports/models.py
+++ b/superset/reports/models.py
@@ -173,6 +173,8 @@ class ReportSchedule(AuditMixinNullable, ExtraJSONMixin, 
Model):
 
     extra: ReportScheduleExtra  # type: ignore
 
+    email_subject = Column(String(255))
+
     def __repr__(self) -> str:
         return str(self.name)
 
diff --git a/superset/reports/schemas.py b/superset/reports/schemas.py
index 64b56ec35a..f57a663e53 100644
--- a/superset/reports/schemas.py
+++ b/superset/reports/schemas.py
@@ -54,6 +54,7 @@ type_description = "The report schedule type"
 name_description = "The report schedule name."
 # :)
 description_description = "Use a nice description to give context to this 
Alert/Report"
+email_subject_description = "The report schedule subject line"
 context_markdown_description = "Markdown description"
 crontab_description = (
     "A CRON expression."
@@ -146,6 +147,14 @@ class ReportSchedulePostSchema(Schema):
         allow_none=True,
         required=False,
     )
+    email_subject = fields.String(
+        metadata={
+            "description": email_subject_description,
+            "example": "[Report]  Report name: Dashboard or chart name",
+        },
+        allow_none=True,
+        required=False,
+    )
     context_markdown = fields.String(
         metadata={"description": context_markdown_description},
         allow_none=True,
@@ -272,6 +281,14 @@ class ReportSchedulePutSchema(Schema):
         allow_none=True,
         required=False,
     )
+    email_subject = fields.String(
+        metadata={
+            "description": email_subject_description,
+            "example": "[Report]  Report name: Dashboard or chart name",
+        },
+        allow_none=True,
+        required=False,
+    )
     context_markdown = fields.String(
         metadata={"description": context_markdown_description},
         allow_none=True,

Reply via email to