Skip to content

Commit

Permalink
feat: add slackv2 notification (apache#29264)
Browse files Browse the repository at this point in the history
  • Loading branch information
eschutho committed Jul 17, 2024
1 parent c0d46eb commit 6dbfe2a
Show file tree
Hide file tree
Showing 33 changed files with 1,666 additions and 555 deletions.
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
14 changes: 14 additions & 0 deletions superset-frontend/src/components/Select/Select.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,20 @@ test('does not add a new option if the value is already in the options', async (
expect(options).toHaveLength(1);
});

test('does not add new options when the value is in a nested/grouped option', async () => {
const options = [
{
label: 'Group',
options: [OPTIONS[0]],
},
];
render(<Select {...defaultProps} options={options} value={OPTIONS[0]} />);
await open();
expect(await findSelectOption(OPTIONS[0].label)).toBeInTheDocument();
const selectOptions = await findAllSelectOptions();
expect(selectOptions).toHaveLength(1);
});

test('inverts the selection', async () => {
render(<Select {...defaultProps} invertSelection />);
await open();
Expand Down
12 changes: 11 additions & 1 deletion superset-frontend/src/components/Select/Select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -182,8 +182,18 @@ const Select = forwardRef(

// add selected values to options list if they are not in it
const fullSelectOptions = useMemo(() => {
// check to see if selectOptions are grouped
let groupedOptions: SelectOptionsType;
if (selectOptions.some(opt => opt.options)) {
groupedOptions = selectOptions.reduce(
(acc, group) => [...acc, ...group.options],
[] as SelectOptionsType,
);
}
const missingValues: SelectOptionsType = ensureIsArray(selectValue)
.filter(opt => !hasOption(getValue(opt), selectOptions))
.filter(
opt => !hasOption(getValue(opt), groupedOptions || selectOptions),
)
.map(opt =>
isLabeledValue(opt) ? opt : { value: opt, label: String(opt) },
);
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: '[email protected]' },
},
],
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,183 @@
/**
* 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 { fireEvent, render, screen } from 'spec/helpers/testing-library';
import userEvent from '@testing-library/user-event';

import { NotificationMethod, mapSlackValues } 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: '[email protected]',
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');
userEvent.click(deleteButton);

expect(mockOnRemove).toHaveBeenCalledWith(1);
});

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, {
target: { value: '[email protected]' },
});

expect(mockOnUpdate).toHaveBeenCalledWith(0, {
...mockSetting,
recipients: '[email protected]',
});
});

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: '[email protected]' },
});

expect(mockOnUpdate).toHaveBeenCalledWith(0, {
...mockSetting,
recipients: '[email protected]',
});
});

it('should correctly map recipients when method is SlackV2', () => {
const method = 'SlackV2';
const recipientValue = 'user1,user2';
const slackOptions: { label: string; value: string }[] = [
{ label: 'User One', value: 'user1' },
{ label: 'User Two', value: 'user2' },
];

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

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

it('should return an empty array when recipientValue is an empty string', () => {
const method = 'SlackV2';
const recipientValue = '';
const slackOptions: { label: string; value: string }[] = [
{ label: 'User One', value: 'user1' },
{ label: 'User Two', value: 'user2' },
];

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

expect(result).toEqual([]);
});

it('should correctly map recipients when method is Slack with updated recipient values', () => {
const method = 'Slack';
const recipientValue = 'User One,User Two';
const slackOptions: { label: string; value: string }[] = [
{ label: 'User One', value: 'user1' },
{ label: 'User Two', value: 'user2' },
];

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

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

0 comments on commit 6dbfe2a

Please sign in to comment.