diff --git a/superset-frontend/src/features/alerts/AlertReportModal.tsx b/superset-frontend/src/features/alerts/AlertReportModal.tsx index 8c71a7f6a7f43..aad229955212e 100644 --- a/superset-frontend/src/features/alerts/AlertReportModal.tsx +++ b/superset-frontend/src/features/alerts/AlertReportModal.tsx @@ -98,6 +98,7 @@ export interface AlertReportModalProps { const DEFAULT_WORKING_TIMEOUT = 3600; const DEFAULT_CRON_VALUE = '0 0 * * *'; // every day const DEFAULT_RETENTION = 90; +const EMAIL_REGEX = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/; const DEFAULT_NOTIFICATION_METHODS: NotificationMethodOption[] = [ NotificationMethodOption.Email, @@ -372,6 +373,7 @@ export const TRANSLATIONS = { WORKING_TIMEOUT_ERROR_TEXT: t('working timeout'), RECIPIENTS_ERROR_TEXT: t('recipients'), EMAIL_SUBJECT_ERROR_TEXT: t('email subject'), + EMAIL_VALIDATION_ERROR_TEXT: t('invalid email'), ERROR_TOOLTIP_MESSAGE: t( 'Not all required fields are complete. Please provide the following:', ), @@ -621,6 +623,8 @@ const AlertReportModal: FunctionComponent = ({ recipients.push({ recipient_config_json: { target: setting.recipients, + ccTarget: setting.cc, + bccTarget: setting.bcc, }, type: setting.method, }); @@ -1014,6 +1018,31 @@ const AlertReportModal: FunctionComponent = ({ return hasInfo; }; + const checkEmailFormat = () => { + if (!notificationSettings.length) { + return true; + } + + const validateEmails = (emails: string): boolean => { + if (!emails) return true; // No emails to validate + return emails + .split(/[,;]/) + .every(email => EMAIL_REGEX.test(email.trim())); + }; + + // Use array method to check conditions + return notificationSettings.every(setting => { + if (!!setting.method && setting.method === 'Email') { + return ( + (!setting.recipients?.length || validateEmails(setting.recipients)) && + (!setting.cc || validateEmails(setting.cc)) && + (!setting.bcc || validateEmails(setting.bcc)) + ); + } + return true; // Non-Email methods are considered valid + }); + }; + const validateGeneralSection = () => { const errors = []; if (!currentAlert?.name?.length) { @@ -1069,13 +1098,24 @@ const AlertReportModal: FunctionComponent = ({ }; const validateNotificationSection = () => { + const errors = []; const hasErrors = !checkNotificationSettings(); - const errors = hasErrors ? [TRANSLATIONS.RECIPIENTS_ERROR_TEXT] : []; + + if (hasErrors) { + errors.push(TRANSLATIONS.RECIPIENTS_ERROR_TEXT); + } else { + // Check for email format errors + const hasValidationErrors = !checkEmailFormat(); + if (hasValidationErrors) { + errors.push(TRANSLATIONS.EMAIL_VALIDATION_ERROR_TEXT); + } + } if (emailError) { errors.push(TRANSLATIONS.EMAIL_SUBJECT_ERROR_TEXT); } + // Update validation status with combined errors updateValidationStatus(Sections.Notification, errors); }; @@ -1132,6 +1172,8 @@ const AlertReportModal: FunctionComponent = ({ setNotificationSettings([ { recipients: '', + cc: '', + bcc: '', options: allowedNotificationMethods, method: NotificationMethodOption.Email, }, @@ -1153,6 +1195,8 @@ const AlertReportModal: FunctionComponent = ({ // @ts-ignore: Type not assignable recipients: config.target || setting.recipient_config_json, options: allowedNotificationMethods, + cc: config.ccTarget || '', + bcc: config.bccTarget || '', }; }); diff --git a/superset-frontend/src/features/alerts/components/NotificationMethod.test.tsx b/superset-frontend/src/features/alerts/components/NotificationMethod.test.tsx index 4b4e46c69e272..292e8d9afc4c1 100644 --- a/superset-frontend/src/features/alerts/components/NotificationMethod.test.tsx +++ b/superset-frontend/src/features/alerts/components/NotificationMethod.test.tsx @@ -80,8 +80,8 @@ describe('NotificationMethod', () => { />, ); - const deleteButton = screen.getByRole('button'); - userEvent.click(deleteButton); + const deleteButton = document.querySelector('.delete-button'); + if (deleteButton) userEvent.click(deleteButton); expect(mockOnRemove).toHaveBeenCalledWith(1); }); diff --git a/superset-frontend/src/features/alerts/components/NotificationMethod.tsx b/superset-frontend/src/features/alerts/components/NotificationMethod.tsx index 85e26f777ab2a..5028c14f13c22 100644 --- a/superset-frontend/src/features/alerts/components/NotificationMethod.tsx +++ b/superset-frontend/src/features/alerts/components/NotificationMethod.tsx @@ -44,34 +44,75 @@ import { import { StyledInputContainer } from '../AlertReportModal'; const StyledNotificationMethod = styled.div` - margin-bottom: 10px; + ${({ theme }) => ` + margin-bottom: ${theme.gridUnit * 3}px; - .input-container { - textarea { - height: auto; + .input-container { + textarea { + height: auto; + } + + &.error { + input { + border-color: ${theme.colors.error.base}; + } + } + + .helper { + margin-top: ${theme.gridUnit * 2}px; + font-size: ${theme.typography.sizes.s}px; + color: ${theme.colors.grayscale.base}; + } } - &.error { - input { - border-color: ${({ theme }) => theme.colors.error.base}; + .inline-container { + margin-bottom: ${theme.gridUnit * 2}px; + + > div { + margin: 0px; + } + + .delete-button { + margin-left: ${theme.gridUnit * 2}px; + padding-top: ${theme.gridUnit}px; } } - } - .inline-container { - margin-bottom: 10px; + .ghost-button { + color: ${theme.colors.primary.dark1}; + display: inline-flex; + align-items: center; + font-size: ${theme.typography.sizes.s}px; + cursor: pointer; + margin-top: ${theme.gridUnit}px; + + .icon { + width: ${theme.gridUnit * 3}px; + height: ${theme.gridUnit * 3}px; + font-size: ${theme.typography.sizes.s}px; + margin-right: ${theme.gridUnit}px; + } + } - > div { - margin: 0; + .ghost-button + .ghost-button { + margin-left: ${theme.gridUnit * 4}px; } - .delete-button { - margin-left: 10px; - padding-top: 3px; + .ghost-button:first-child[style*='none'] + .ghost-button { + margin-left: 0px; /* Remove margin when the first button is hidden */ } - } + `} `; +const TRANSLATIONS = { + EMAIL_CC_NAME: t('CC recipients'), + EMAIL_BCC_NAME: t('BCC recipients'), + EMAIL_SUBJECT_NAME: t('Email subject name (optional)'), + EMAIL_SUBJECT_ERROR_TEXT: t( + 'Please enter valid text. Spaces alone are not permitted.', + ), +}; + interface NotificationMethodProps { setting?: NotificationSetting | null; index: number; @@ -85,13 +126,6 @@ interface NotificationMethodProps { 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 mapSlackValues = ({ method, recipientValue, @@ -164,7 +198,7 @@ export const NotificationMethod: FunctionComponent = ({ defaultSubject, setErrorSubject, }) => { - const { method, recipients, options } = setting || {}; + const { method, recipients, cc, bcc, options } = setting || {}; const [recipientValue, setRecipientValue] = useState( recipients || '', ); @@ -172,6 +206,10 @@ export const NotificationMethod: FunctionComponent = ({ { label: string; value: string }[] >([]); const [error, setError] = useState(false); + const [ccVisible, setCcVisible] = useState(!!cc); + const [bccVisible, setBccVisible] = useState(!!bcc); + const [ccValue, setCcValue] = useState(cc || ''); + const [bccValue, setBccValue] = useState(bcc || ''); const theme = useTheme(); const [slackOptions, setSlackOptions] = useState([ { @@ -188,11 +226,16 @@ export const NotificationMethod: FunctionComponent = ({ }) => { // Since we're swapping the method, reset the recipients setRecipientValue(''); + setCcValue(''); + setBccValue(''); + if (onUpdate && setting) { const updatedSetting = { ...setting, method: selected.value, recipients: '', + cc: '', + bcc: '', }; onUpdate(index, updatedSetting); @@ -333,11 +376,49 @@ export const NotificationMethod: FunctionComponent = ({ } }; + const onCcChange = (event: React.ChangeEvent) => { + const { target } = event; + + setCcValue(target.value); + + if (onUpdate) { + const updatedSetting = { + ...setting, + cc: target.value, + }; + + onUpdate(index, updatedSetting); + } + }; + + const onBccChange = (event: React.ChangeEvent) => { + const { target } = event; + + setBccValue(target.value); + + if (onUpdate) { + const updatedSetting = { + ...setting, + bcc: target.value, + }; + + onUpdate(index, updatedSetting); + } + }; + // Set recipients if (!!recipients && recipientValue !== recipients) { setRecipientValue(recipients); } + if (!!cc && ccValue !== cc) { + setCcValue(cc); + } + + if (!!bcc && bccValue !== bcc) { + setBccValue(bcc); + } + return (
@@ -418,14 +499,16 @@ export const NotificationMethod: FunctionComponent = ({ <>