fix: Disable Slack notification method if no api token (#16367)

* feat: allow company choose alert/report notification methods

* fix: disable Slack as notification method if no api token

* fix unit test

* improve typing
This commit is contained in:
Grace Guo 2021-08-24 17:28:50 -07:00 committed by GitHub
parent 5e472980a6
commit 78d8089b18
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 55 additions and 27 deletions

View File

@ -326,9 +326,10 @@ describe('AlertReportModal', () => {
}); });
await waitForComponentToPaint(wrapper); await waitForComponentToPaint(wrapper);
// use default config: only show Email as notification option.
expect( expect(
wrapper.find('[data-test="notification-add"]').props().status, wrapper.find('[data-test="notification-add"]').props().status,
).toEqual('disabled'); ).toEqual('hidden');
act(() => { act(() => {
wrapper wrapper
.find('[data-test="select-delivery-method"]') .find('[data-test="select-delivery-method"]')

View File

@ -43,10 +43,9 @@ import { FeatureFlag, isFeatureEnabled } from 'src/featureFlags';
import withToasts from 'src/messageToasts/enhancers/withToasts'; import withToasts from 'src/messageToasts/enhancers/withToasts';
import Owner from 'src/types/Owner'; import Owner from 'src/types/Owner';
import TextAreaControl from 'src/explore/components/controls/TextAreaControl'; import TextAreaControl from 'src/explore/components/controls/TextAreaControl';
import { AlertReportCronScheduler } from './components/AlertReportCronScheduler'; import { useCommonConf } from 'src/views/CRUD/data/database/state';
import { NotificationMethod } from './components/NotificationMethod';
import { import {
NotificationMethodOption,
AlertObject, AlertObject,
ChartObject, ChartObject,
DashboardObject, DashboardObject,
@ -54,7 +53,9 @@ import {
MetaObject, MetaObject,
Operator, Operator,
Recipient, Recipient,
} from './types'; } from 'src/views/CRUD/alert/types';
import { AlertReportCronScheduler } from './components/AlertReportCronScheduler';
import { NotificationMethod } from './components/NotificationMethod';
const TIMEOUT_MIN = 1; const TIMEOUT_MIN = 1;
const TEXT_BASED_VISUALIZATION_TYPES = [ const TEXT_BASED_VISUALIZATION_TYPES = [
@ -78,7 +79,7 @@ interface AlertReportModalProps {
show: boolean; show: boolean;
} }
const NOTIFICATION_METHODS: NotificationMethod[] = ['Email', 'Slack']; const DEFAULT_NOTIFICATION_METHODS: NotificationMethodOption[] = ['Email'];
const DEFAULT_NOTIFICATION_FORMAT = 'PNG'; const DEFAULT_NOTIFICATION_FORMAT = 'PNG';
const CONDITIONS = [ const CONDITIONS = [
{ {
@ -381,12 +382,10 @@ const NotificationMethodAdd: FunctionComponent<NotificationMethodAddProps> = ({
); );
}; };
type NotificationMethod = 'Email' | 'Slack';
type NotificationSetting = { type NotificationSetting = {
method?: NotificationMethod; method?: NotificationMethodOption;
recipients: string; recipients: string;
options: NotificationMethod[]; options: NotificationMethodOption[];
}; };
const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({
@ -397,6 +396,10 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({
alert = null, alert = null,
isReport = false, isReport = false,
}) => { }) => {
const conf = useCommonConf();
const allowedNotificationMethods: NotificationMethodOption[] =
conf?.ALERT_REPORTS_NOTIFICATION_METHODS || DEFAULT_NOTIFICATION_METHODS;
const [disableSave, setDisableSave] = useState<boolean>(true); const [disableSave, setDisableSave] = useState<boolean>(true);
const [ const [
currentAlert, currentAlert,
@ -435,12 +438,14 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({
settings.push({ settings.push({
recipients: '', recipients: '',
options: NOTIFICATION_METHODS, // TODO: Need better logic for this options: allowedNotificationMethods,
}); });
setNotificationSettings(settings); setNotificationSettings(settings);
setNotificationAddState( setNotificationAddState(
settings.length === NOTIFICATION_METHODS.length ? 'hidden' : 'disabled', settings.length === allowedNotificationMethods.length
? 'hidden'
: 'disabled',
); );
}; };
@ -910,16 +915,18 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({
? JSON.parse(setting.recipient_config_json) ? JSON.parse(setting.recipient_config_json)
: {}; : {};
return { return {
method: setting.type as NotificationMethod, method: setting.type,
// @ts-ignore: Type not assignable // @ts-ignore: Type not assignable
recipients: config.target || setting.recipient_config_json, recipients: config.target || setting.recipient_config_json,
options: NOTIFICATION_METHODS as NotificationMethod[], // Need better logic for this options: allowedNotificationMethods,
}; };
}); });
setNotificationSettings(settings); setNotificationSettings(settings);
setNotificationAddState( setNotificationAddState(
settings.length === NOTIFICATION_METHODS.length ? 'hidden' : 'active', settings.length === allowedNotificationMethods.length
? 'hidden'
: 'active',
); );
setContentType(resource.chart ? 'chart' : 'dashboard'); setContentType(resource.chart ? 'chart' : 'dashboard');
setReportFormat( setReportFormat(
@ -1309,6 +1316,7 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({
<NotificationMethod <NotificationMethod
setting={notificationSetting} setting={notificationSetting}
index={i} index={i}
key={`NotificationMethod-${i}`}
onUpdate={updateNotificationSetting} onUpdate={updateNotificationSetting}
onRemove={removeNotificationSetting} onRemove={removeNotificationSetting}
/> />

View File

@ -20,6 +20,7 @@ import React, { FunctionComponent, useState } from 'react';
import { styled, t, useTheme } from '@superset-ui/core'; import { styled, t, useTheme } from '@superset-ui/core';
import { Select } from 'src/components'; import { Select } from 'src/components';
import Icons from 'src/components/Icons'; import Icons from 'src/components/Icons';
import { NotificationMethodOption } from 'src/views/CRUD/alert/types';
import { StyledInputContainer } from '../AlertReportModal'; import { StyledInputContainer } from '../AlertReportModal';
const StyledNotificationMethod = styled.div` const StyledNotificationMethod = styled.div`
@ -49,12 +50,10 @@ const StyledNotificationMethod = styled.div`
} }
`; `;
type NotificationMethod = 'Email' | 'Slack';
type NotificationSetting = { type NotificationSetting = {
method?: NotificationMethod; method?: NotificationMethodOption;
recipients: string; recipients: string;
options: NotificationMethod[]; options: NotificationMethodOption[];
}; };
interface NotificationMethodProps { interface NotificationMethodProps {
@ -80,7 +79,7 @@ export const NotificationMethod: FunctionComponent<NotificationMethodProps> = ({
return null; return null;
} }
const onMethodChange = (method: NotificationMethod) => { const onMethodChange = (method: NotificationMethodOption) => {
// Since we're swapping the method, reset the recipients // Since we're swapping the method, reset the recipients
setRecipientValue(''); setRecipientValue('');
if (onUpdate) { if (onUpdate) {
@ -126,10 +125,12 @@ export const NotificationMethod: FunctionComponent<NotificationMethodProps> = ({
data-test="select-delivery-method" data-test="select-delivery-method"
onChange={onMethodChange} onChange={onMethodChange}
placeholder={t('Select Delivery Method')} placeholder={t('Select Delivery Method')}
options={(options || []).map((method: NotificationMethod) => ({ options={(options || []).map(
label: method, (method: NotificationMethodOption) => ({
value: method, label: method,
}))} value: method,
}),
)}
value={method} value={method}
/> />
</div> </div>

View File

@ -40,11 +40,13 @@ export type DatabaseObject = {
id: number; id: number;
}; };
export type NotificationMethodOption = 'Email' | 'Slack';
export type Recipient = { export type Recipient = {
recipient_config_json: { recipient_config_json: {
target: string; target: string;
}; };
type: string; type: NotificationMethodOption;
}; };
export type MetaObject = { export type MetaObject = {

View File

@ -21,5 +21,5 @@ import { useSelector } from 'react-redux';
import { ViewState } from 'src/views/types'; import { ViewState } from 'src/views/types';
export function useCommonConf() { export function useCommonConf() {
return useSelector((state: ViewState) => state.common.conf); return useSelector((state: ViewState) => state?.common?.conf);
} }

View File

@ -16,11 +16,14 @@
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
import { NotificationMethodOption } from './CRUD/alert/types';
export interface ViewState { export interface ViewState {
common: { common: {
conf: { conf: {
SQLALCHEMY_DOCS_URL: string; SQLALCHEMY_DOCS_URL: string;
SQLALCHEMY_DISPLAY_TEXT: string; SQLALCHEMY_DISPLAY_TEXT: string;
ALERT_REPORTS_NOTIFICATION_METHODS: NotificationMethodOption[];
}; };
}; };
messageToast: Array<Object>; messageToast: Array<Object>;

View File

@ -70,6 +70,7 @@ from superset.exceptions import (
SupersetSecurityException, SupersetSecurityException,
) )
from superset.models.helpers import ImportExportMixin from superset.models.helpers import ImportExportMixin
from superset.models.reports import ReportRecipientType
from superset.translations.utils import get_language_pack from superset.translations.utils import get_language_pack
from superset.typing import FlaskResponse from superset.typing import FlaskResponse
from superset.utils import core as utils from superset.utils import core as utils
@ -346,9 +347,21 @@ def common_bootstrap_payload() -> Dict[str, Any]:
messages = get_flashed_messages(with_categories=True) messages = get_flashed_messages(with_categories=True)
locale = str(get_locale()) locale = str(get_locale())
# should not expose API TOKEN to frontend
frontend_config = {k: conf.get(k) for k in FRONTEND_CONF_KEYS}
if conf.get("SLACK_API_TOKEN"):
frontend_config["ALERT_REPORTS_NOTIFICATION_METHODS"] = [
ReportRecipientType.EMAIL,
ReportRecipientType.SLACK,
]
else:
frontend_config["ALERT_REPORTS_NOTIFICATION_METHODS"] = [
ReportRecipientType.EMAIL,
]
bootstrap_data = { bootstrap_data = {
"flash_messages": messages, "flash_messages": messages,
"conf": {k: conf.get(k) for k in FRONTEND_CONF_KEYS}, "conf": frontend_config,
"locale": locale, "locale": locale,
"language_pack": get_language_pack(locale), "language_pack": get_language_pack(locale),
"feature_flags": get_feature_flags(), "feature_flags": get_feature_flags(),