Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add slackv2 notification #29264

Merged
merged 7 commits into from
Jul 17, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions UPDATING.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ assists people when migrating to a new version.
translations inside the python package. This includes the .mo files needed by pybabel on the
backend, as well as the .json files used by the frontend. If you were doing anything before
as part of your bundling to expose translation packages, it's probably not needed anymore.
- [29264](https://github.com/apache/superset/pull/29264) Slack has updated its file upload api, and we are now supporting this new api in Superset, although the Slack api is not backward compatible. The original Slack integration is deprecated and we will require a new Slack scope `channels:read` to be added to Slack workspaces in order to use this new api. In an upcoming release, we will make this new Slack scope mandatory and remove the old Slack functionality.

### Potential Downtime

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import fetchMock from 'fetch-mock';
import { render, screen, waitFor, within } from 'spec/helpers/testing-library';
import { buildErrorTooltipMessage } from './buildErrorTooltipMessage';
import AlertReportModal, { AlertReportModalProps } from './AlertReportModal';
import { AlertObject } from './types';
import { AlertObject, NotificationMethodOption } from './types';

jest.mock('@superset-ui/core', () => ({
...jest.requireActual('@superset-ui/core'),
Expand All @@ -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'],
}),
}));

Expand Down Expand Up @@ -135,7 +135,7 @@ const validAlert: AlertObject = {
],
recipients: [
{
type: 'Email',
type: NotificationMethodOption.Email,
recipient_config_json: { target: 'test@user.com' },
},
],
Expand Down
24 changes: 20 additions & 4 deletions superset-frontend/src/features/alerts/AlertReportModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,9 @@ const DEFAULT_WORKING_TIMEOUT = 3600;
const DEFAULT_CRON_VALUE = '0 0 * * *'; // every day
const DEFAULT_RETENTION = 90;

const DEFAULT_NOTIFICATION_METHODS: NotificationMethodOption[] = ['Email'];
const DEFAULT_NOTIFICATION_METHODS: NotificationMethodOption[] = [
NotificationMethodOption.Email,
];
const DEFAULT_NOTIFICATION_FORMAT = 'PNG';
const CONDITIONS = [
{
Expand Down Expand Up @@ -517,7 +519,7 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({
]);

setNotificationAddState(
notificationSettings.length === allowedNotificationMethods.length
notificationSettings.length === allowedNotificationMethodsCount
? 'hidden'
: 'disabled',
);
Expand Down Expand Up @@ -1131,7 +1133,7 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({
{
recipients: '',
options: allowedNotificationMethods,
method: 'Email',
method: NotificationMethodOption.Email,
},
]);
setNotificationAddState('active');
Expand Down Expand Up @@ -1235,6 +1237,20 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({
enforceValidation();
}, [validationStatus]);

const allowedNotificationMethodsCount = useMemo(
() =>
allowedNotificationMethods.reduce((accum: string[], setting: string) => {
if (
accum.some(nm => nm.includes('slack')) &&
setting.toLowerCase().includes('slack')
) {
return accum;
}
return [...accum, setting.toLowerCase()];
}, []).length,
[allowedNotificationMethods],
);

// Show/hide
if (isHidden && show) {
setIsHidden(false);
Expand Down Expand Up @@ -1743,7 +1759,7 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({
))}
{
// Prohibit 'add notification method' button if only one present
allowedNotificationMethods.length > notificationSettings.length && (
allowedNotificationMethodsCount > notificationSettings.length && (
<NotificationMethodAdd
data-test="notification-add"
status={notificationAddState}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
/**
* 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.
*/
import { render, screen, fireEvent } from 'spec/helpers/testing-library';
import { NotificationMethod, mapSlackOptions } from './NotificationMethod';
import { NotificationMethodOption, NotificationSetting } from '../types';

const mockOnUpdate = jest.fn();
const mockOnRemove = jest.fn();
const mockOnInputChange = jest.fn();
const mockSetErrorSubject = jest.fn();

const mockSetting: NotificationSetting = {
method: NotificationMethodOption.Email,
recipients: 'test@example.com',
options: [
NotificationMethodOption.Email,
NotificationMethodOption.Slack,
NotificationMethodOption.SlackV2,
],
};

const mockEmailSubject = 'Test Subject';
const mockDefaultSubject = 'Default Subject';

describe('NotificationMethod', () => {
beforeEach(() => {
jest.clearAllMocks();
});

it('should render the component', () => {
render(
<NotificationMethod
setting={mockSetting}
index={0}
onUpdate={mockOnUpdate}
onRemove={mockOnRemove}
onInputChange={mockOnInputChange}
email_subject={mockEmailSubject}
defaultSubject={mockDefaultSubject}
setErrorSubject={mockSetErrorSubject}
/>,
);

expect(screen.getByText('Notification Method')).toBeInTheDocument();
expect(
screen.getByText('Email subject name (optional)'),
).toBeInTheDocument();
expect(screen.getByText('Email recipients')).toBeInTheDocument();
});

it('should call onRemove when the delete button is clicked', () => {
render(
<NotificationMethod
setting={mockSetting}
index={1}
onUpdate={mockOnUpdate}
onRemove={mockOnRemove}
onInputChange={mockOnInputChange}
email_subject={mockEmailSubject}
defaultSubject={mockDefaultSubject}
setErrorSubject={mockSetErrorSubject}
/>,
);

const deleteButton = screen.getByRole('button');
fireEvent.click(deleteButton);

expect(mockOnRemove).toHaveBeenCalledWith(1);
});
// Should update recipient value when input changes.
eschutho marked this conversation as resolved.
Show resolved Hide resolved

it('should update recipient value when input changes', () => {
render(
<NotificationMethod
setting={mockSetting}
index={0}
onUpdate={mockOnUpdate}
onRemove={mockOnRemove}
onInputChange={mockOnInputChange}
email_subject={mockEmailSubject}
defaultSubject={mockDefaultSubject}
setErrorSubject={mockSetErrorSubject}
/>,
);

const recipientsInput = screen.getByTestId('recipients');
fireEvent.change(recipientsInput, {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use userEvent here instead and everywhere else?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can in two places, but it doesn't work in the others.

target: { value: 'test1@example.com' },
});

expect(mockOnUpdate).toHaveBeenCalledWith(0, {
...mockSetting,
recipients: 'test1@example.com',
});
});

it('should call onRecipientsChange when the recipients value is changed', () => {
render(
<NotificationMethod
setting={mockSetting}
index={0}
onUpdate={mockOnUpdate}
onRemove={mockOnRemove}
onInputChange={mockOnInputChange}
email_subject={mockEmailSubject}
defaultSubject={mockDefaultSubject}
setErrorSubject={mockSetErrorSubject}
/>,
);

const recipientsInput = screen.getByTestId('recipients');
fireEvent.change(recipientsInput, {
target: { value: 'test1@example.com' },
});

expect(mockOnUpdate).toHaveBeenCalledWith(0, {
...mockSetting,
recipients: 'test1@example.com',
});
});
// correctly maps recipients when method is SlackV2
eschutho marked this conversation as resolved.
Show resolved Hide resolved
it('should correctly map recipients when method is SlackV2', () => {
const method = 'SlackV2';
const recipientValue = 'user1,user2';
const slackOptions = {
data: [
{ label: 'User One', value: 'user1' },
{ label: 'User Two', value: 'user2' },
],
};

const result = mapSlackOptions({ method, recipientValue, slackOptions });

expect(result).toEqual([
{ label: 'User One', value: 'user1' },
{ label: 'User Two', value: 'user2' },
]);
});
// handles empty recipientValue string
eschutho marked this conversation as resolved.
Show resolved Hide resolved
it('should return an empty array when recipientValue is an empty string', () => {
const method = 'SlackV2';
const recipientValue = '';
const slackOptions = {
data: [
{ label: 'User One', value: 'user1' },
{ label: 'User Two', value: 'user2' },
{ label: 'User Three', value: 'user3' },
],
};

const result = mapSlackOptions({ method, recipientValue, slackOptions });

expect(result).toEqual([]);
});
// Ensure that the mapSlackOptions function correctly maps recipients when the method is Slack with updated recipient values
it('should correctly map recipients when method is Slack with updated recipient values', () => {
const method = 'Slack';
const recipientValue = 'User One,User Two';
const slackOptions = {
data: [
{ label: 'User One', value: 'user1' },
{ label: 'User Two', value: 'user2' },
{ label: 'User Three', value: 'user3' },
],
};

const result = mapSlackOptions({ method, recipientValue, slackOptions });

expect(result).toEqual([
{ label: 'User One', value: 'user1' },
{ label: 'User Two', value: 'user2' },
]);
});
});
Loading
Loading