From d1bdaa5704561e7e4b5bce42a848d7a6e8d93a5a Mon Sep 17 00:00:00 2001 From: Elizabeth Thompson Date: Thu, 13 Jun 2024 09:39:03 -0700 Subject: [PATCH] add slackv2 notification --- .../src/utils/featureFlags.ts | 1 + .../features/alerts/AlertReportModal.test.tsx | 2 +- .../alerts/components/NotificationMethod.tsx | 163 +++++++++++++++--- .../alerts/components/RecipientIcon.tsx | 4 + .../src/features/alerts/types.ts | 3 +- superset/commands/report/execute.py | 68 +++++++- superset/config.py | 1 + superset/reports/api.py | 67 ++++++- superset/reports/models.py | 1 + superset/reports/notifications/__init__.py | 7 + superset/reports/notifications/exceptions.py | 8 + superset/reports/notifications/slack.py | 132 ++++---------- superset/reports/notifications/slack_mixin.py | 116 +++++++++++++ superset/reports/notifications/slackv2.py | 133 ++++++++++++++ superset/reports/schemas.py | 6 + superset/tasks/cron_util.py | 9 +- superset/tasks/scheduler.py | 36 ++-- superset/utils/slack.py | 56 ++++++ superset/views/base.py | 1 + 19 files changed, 673 insertions(+), 141 deletions(-) create mode 100644 superset/reports/notifications/slack_mixin.py create mode 100644 superset/reports/notifications/slackv2.py diff --git a/superset-frontend/packages/superset-ui-core/src/utils/featureFlags.ts b/superset-frontend/packages/superset-ui-core/src/utils/featureFlags.ts index b3af431d52bba..8ffc4f845d97b 100644 --- a/superset-frontend/packages/superset-ui-core/src/utils/featureFlags.ts +++ b/superset-frontend/packages/superset-ui-core/src/utils/featureFlags.ts @@ -25,6 +25,7 @@ export enum FeatureFlag { AlertsAttachReports = 'ALERTS_ATTACH_REPORTS', AlertReports = 'ALERT_REPORTS', AlertReportTabs = 'ALERT_REPORT_TABS', + AlertReportSlackV2 = 'ALERT_REPORT_SLACK_V2', AllowFullCsvExport = 'ALLOW_FULL_CSV_EXPORT', AvoidColorsCollision = 'AVOID_COLORS_COLLISION', ChartPluginsExperimental = 'CHART_PLUGINS_EXPERIMENTAL', diff --git a/superset-frontend/src/features/alerts/AlertReportModal.test.tsx b/superset-frontend/src/features/alerts/AlertReportModal.test.tsx index e32d13ab635f2..17047b7a404e4 100644 --- a/superset-frontend/src/features/alerts/AlertReportModal.test.tsx +++ b/superset-frontend/src/features/alerts/AlertReportModal.test.tsx @@ -30,7 +30,7 @@ jest.mock('@superset-ui/core', () => ({ jest.mock('src/features/databases/state.ts', () => ({ useCommonConf: () => ({ - ALERT_REPORTS_NOTIFICATION_METHODS: ['Email', 'Slack'], + ALERT_REPORTS_NOTIFICATION_METHODS: ['Email', 'Slack', 'SlackV2'], }), })); diff --git a/superset-frontend/src/features/alerts/components/NotificationMethod.tsx b/superset-frontend/src/features/alerts/components/NotificationMethod.tsx index b2d780423bc13..dd766baa93f7f 100644 --- a/superset-frontend/src/features/alerts/components/NotificationMethod.tsx +++ b/superset-frontend/src/features/alerts/components/NotificationMethod.tsx @@ -16,11 +16,27 @@ * specific language governing permissions and limitations * under the License. */ -import { FunctionComponent, useState, ChangeEvent } from 'react'; +import { + FunctionComponent, + useState, + ChangeEvent, + useEffect, + useMemo, +} from 'react'; -import { styled, t, useTheme } from '@superset-ui/core'; -import { Select } from 'src/components'; +import { + FeatureFlag, + SupersetClient, + isFeatureEnabled, + styled, + t, + useTheme, +} from '@superset-ui/core'; +import rison from 'rison'; +import { AsyncSelect, Select } from 'src/components'; import Icons from 'src/components/Icons'; +import { set } from 'lodash'; +import { option } from 'yargs'; import { NotificationMethodOption, NotificationSetting } from '../types'; import { StyledInputContainer } from '../AlertReportModal'; @@ -87,20 +103,92 @@ export const NotificationMethod: FunctionComponent = ({ const [recipientValue, setRecipientValue] = useState( recipients || '', ); + const [slackRecipients, setSlackRecipients] = useState< + { label: string; value: string }[] + >([]); const [error, setError] = useState(false); const theme = useTheme(); + const [useSlackV1, setUseSlackV1] = useState(false); + + const mapChannelsToOptions = (result: { name: any; id: any }[]) => + result.map((result: { name: any; id: any }) => ({ + label: result.name, + value: result.id, + })); + + const loadChannels = async ( + search_string: string | undefined = '', + ): Promise<{ + data?: { label: any; value: any }[]; + totalCount?: number; + }> => { + const query = rison.encode({ search_string }); + const endpoint = `/api/v1/report/slack_channels?q=${query}`; + return SupersetClient.get({ endpoint }) + .then(({ json }) => { + const { result, count } = json; + + const options: { label: any; value: any }[] = + mapChannelsToOptions(result); + + return { + data: options, + totalCount: (count ?? options.length) as number, + }; + }) + .catch(() => { + // Fallback to slack v1 if slack v2 is not compatible + setUseSlackV1(true); + return {}; + }); + }; + + useEffect(() => { + // fetch slack channel names from + // ids on first load + if (method && ['Slack', 'SlackV2'].includes(method)) { + loadChannels(recipients).then(response => { + setSlackRecipients(response.data || []); + onMethodChange({ label: 'Slack', value: 'SlackV2' }); + }); + } + }, []); + + const formattedOptions = useMemo( + () => + (options || []) + .filter( + method => + (isFeatureEnabled(FeatureFlag.AlertReportSlackV2) && + !useSlackV1 && + method === 'SlackV2') || + ((!isFeatureEnabled(FeatureFlag.AlertReportSlackV2) || + useSlackV1) && + method === 'Slack') || + method === 'Email', + ) + .map(method => ({ + label: method === 'SlackV2' ? 'Slack' : method, + value: method, + })), + [options], + ); + if (!setting) { return null; } - const onMethodChange = (method: NotificationMethodOption) => { + const onMethodChange = (selected: { + label: string; + value: NotificationMethodOption; + }) => { // Since we're swapping the method, reset the recipients setRecipientValue(''); if (onUpdate) { const updatedSetting = { ...setting, - method, + method: selected.value, recipients: '', }; @@ -123,6 +211,21 @@ export const NotificationMethod: FunctionComponent = ({ } }; + const onSlackRecipientsChange = ( + recipients: { label: string; value: string }[], + ) => { + setSlackRecipients(recipients); + + if (onUpdate) { + const updatedSetting = { + ...setting, + recipients: recipients?.map(obj => obj.value).join(','), + }; + + onUpdate(index, updatedSetting); + } + }; + const onSubjectChange = ( event: ChangeEvent, ) => { @@ -153,15 +256,12 @@ export const NotificationMethod: FunctionComponent = ({