feat: Customizable email subject name (#26327)
Co-authored-by: Puridach Wutthihathaithamrong <>
This commit is contained in:
parent
bfb92976cb
commit
aa2b060da8
|
|
@ -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>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
@ -119,6 +119,7 @@ class ReportScheduleRestApi(BaseSupersetModelRestApi):
|
|||
"validator_config_json",
|
||||
"validator_type",
|
||||
"working_timeout",
|
||||
"email_subject",
|
||||
]
|
||||
show_select_columns = show_columns + [
|
||||
"chart.datasource_id",
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in New Issue