refactor(ReportModal): simplify state reducer and improve error handling (#19942)
This commit is contained in:
parent
58e65ad5bb
commit
7b88ec7e25
|
|
@ -19,16 +19,15 @@
|
|||
import React, {
|
||||
useState,
|
||||
useEffect,
|
||||
useCallback,
|
||||
useReducer,
|
||||
Reducer,
|
||||
FunctionComponent,
|
||||
useCallback,
|
||||
useMemo,
|
||||
} from 'react';
|
||||
import { t, SupersetTheme } from '@superset-ui/core';
|
||||
import { getClientErrorObject } from 'src/utils/getClientErrorObject';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { addReport, editReport } from 'src/reports/actions/reports';
|
||||
import { AlertObject } from 'src/views/CRUD/alert/types';
|
||||
|
||||
import Alert from 'src/components/Alert';
|
||||
import TimezoneSelector from 'src/components/TimezoneSelector';
|
||||
|
|
@ -37,6 +36,12 @@ import Icons from 'src/components/Icons';
|
|||
import withToasts from 'src/components/MessageToasts/withToasts';
|
||||
import { CronError } from 'src/components/CronPicker';
|
||||
import { RadioChangeEvent } from 'src/components';
|
||||
import { ChartState } from 'src/explore/types';
|
||||
import {
|
||||
ReportCreationMethod,
|
||||
ReportRecipientType,
|
||||
ReportScheduleType,
|
||||
} from 'src/reports/types';
|
||||
import {
|
||||
antDErrorAlertStyles,
|
||||
StyledModal,
|
||||
|
|
@ -65,30 +70,17 @@ export interface ReportObject {
|
|||
log_retention: number;
|
||||
name: string;
|
||||
owners: number[];
|
||||
recipients: [{ recipient_config_json: { target: string }; type: string }];
|
||||
recipients: [
|
||||
{ recipient_config_json: { target: string }; type: ReportRecipientType },
|
||||
];
|
||||
report_format: string;
|
||||
timezone: string;
|
||||
type: string;
|
||||
type: ReportScheduleType;
|
||||
validator_config_json: {} | null;
|
||||
validator_type: string;
|
||||
working_timeout: number;
|
||||
creation_method: string;
|
||||
force_screenshot: boolean;
|
||||
error: string;
|
||||
}
|
||||
|
||||
interface ChartObject {
|
||||
id: number;
|
||||
chartAlert: string;
|
||||
chartStatus: string;
|
||||
chartUpdateEndTime: number;
|
||||
chartUpdateStartTime: number;
|
||||
latestQueryFormData: object;
|
||||
sliceFormData: Record<string, any>;
|
||||
queryController: { abort: () => {} };
|
||||
queriesResponse: object;
|
||||
triggerQuery: boolean;
|
||||
lastRendered: number;
|
||||
}
|
||||
|
||||
interface ReportProps {
|
||||
|
|
@ -98,40 +90,13 @@ interface ReportProps {
|
|||
show: boolean;
|
||||
userId: number;
|
||||
userEmail: string;
|
||||
chart?: ChartState;
|
||||
chartName?: string;
|
||||
dashboardId?: number;
|
||||
chart?: ChartObject;
|
||||
creationMethod: string;
|
||||
dashboardName?: string;
|
||||
creationMethod: ReportCreationMethod;
|
||||
}
|
||||
|
||||
interface ReportPayloadType {
|
||||
name: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
enum ActionType {
|
||||
inputChange,
|
||||
fetched,
|
||||
reset,
|
||||
error,
|
||||
}
|
||||
|
||||
type ReportActionType =
|
||||
| {
|
||||
type: ActionType.inputChange;
|
||||
payload: ReportPayloadType;
|
||||
}
|
||||
| {
|
||||
type: ActionType.fetched;
|
||||
payload: Partial<ReportObject>;
|
||||
}
|
||||
| {
|
||||
type: ActionType.reset;
|
||||
}
|
||||
| {
|
||||
type: ActionType.error;
|
||||
payload: { name?: string[] };
|
||||
};
|
||||
|
||||
const TEXT_BASED_VISUALIZATION_TYPES = [
|
||||
'pivot_table',
|
||||
'pivot_table_v2',
|
||||
|
|
@ -145,41 +110,16 @@ const NOTIFICATION_FORMATS = {
|
|||
CSV: 'CSV',
|
||||
};
|
||||
|
||||
const defaultErrorMsg = t(
|
||||
'We were unable to create your report. Please try again.',
|
||||
);
|
||||
const INITIAL_STATE = {
|
||||
crontab: '0 12 * * 1',
|
||||
};
|
||||
|
||||
const reportReducer = (
|
||||
state: Partial<ReportObject> | null,
|
||||
action: ReportActionType,
|
||||
): Partial<ReportObject> | null => {
|
||||
const initialState = {
|
||||
name: 'Weekly Report',
|
||||
crontab: '0 12 * * 1',
|
||||
};
|
||||
|
||||
switch (action.type) {
|
||||
case ActionType.inputChange:
|
||||
return {
|
||||
...initialState,
|
||||
...state,
|
||||
[action.payload.name]: action.payload.value,
|
||||
};
|
||||
case ActionType.fetched:
|
||||
return {
|
||||
...initialState,
|
||||
...action.payload,
|
||||
};
|
||||
case ActionType.reset:
|
||||
return { ...initialState };
|
||||
case ActionType.error:
|
||||
return {
|
||||
...state,
|
||||
error: action.payload?.name?.[0] || defaultErrorMsg,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
type ReportObjectState = Partial<ReportObject> & {
|
||||
error?: string;
|
||||
/**
|
||||
* Is submitting changes to the backend.
|
||||
*/
|
||||
isSubmitting?: boolean;
|
||||
};
|
||||
|
||||
const ReportModal: FunctionComponent<ReportProps> = ({
|
||||
|
|
@ -190,45 +130,64 @@ const ReportModal: FunctionComponent<ReportProps> = ({
|
|||
}) => {
|
||||
const vizType = props.chart?.sliceFormData?.viz_type;
|
||||
const isChart = !!props.chart;
|
||||
const defaultNotificationFormat =
|
||||
isChart && TEXT_BASED_VISUALIZATION_TYPES.includes(vizType)
|
||||
? NOTIFICATION_FORMATS.TEXT
|
||||
: NOTIFICATION_FORMATS.PNG;
|
||||
const [currentReport, setCurrentReport] = useReducer<
|
||||
Reducer<Partial<ReportObject> | null, ReportActionType>
|
||||
>(reportReducer, null);
|
||||
const onReducerChange = useCallback((type: any, payload: any) => {
|
||||
setCurrentReport({ type, payload });
|
||||
}, []);
|
||||
const isTextBasedChart =
|
||||
isChart && vizType && TEXT_BASED_VISUALIZATION_TYPES.includes(vizType);
|
||||
const defaultNotificationFormat = isTextBasedChart
|
||||
? NOTIFICATION_FORMATS.TEXT
|
||||
: NOTIFICATION_FORMATS.PNG;
|
||||
const entityName = props.dashboardName || props.chartName;
|
||||
const initialState: ReportObjectState = useMemo(
|
||||
() => ({
|
||||
...INITIAL_STATE,
|
||||
name: entityName
|
||||
? t('Weekly Report for %s', entityName)
|
||||
: t('Weekly Report'),
|
||||
}),
|
||||
[entityName],
|
||||
);
|
||||
|
||||
const reportReducer = useCallback(
|
||||
(state: ReportObjectState | null, action: 'reset' | ReportObjectState) => {
|
||||
if (action === 'reset') {
|
||||
return initialState;
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
...action,
|
||||
};
|
||||
},
|
||||
[initialState],
|
||||
);
|
||||
|
||||
const [currentReport, setCurrentReport] = useReducer(
|
||||
reportReducer,
|
||||
initialState,
|
||||
);
|
||||
const [cronError, setCronError] = useState<CronError>();
|
||||
|
||||
const dispatch = useDispatch();
|
||||
// Report fetch logic
|
||||
const reports = useSelector<any, AlertObject>(state => state.reports);
|
||||
const reports = useSelector<any, ReportObject>(state => state.reports);
|
||||
const isEditMode = reports && Object.keys(reports).length;
|
||||
|
||||
useEffect(() => {
|
||||
if (isEditMode) {
|
||||
const reportsIds = Object.keys(reports);
|
||||
const report = reports[reportsIds[0]];
|
||||
setCurrentReport({
|
||||
type: ActionType.fetched,
|
||||
payload: report,
|
||||
});
|
||||
setCurrentReport(report);
|
||||
} else {
|
||||
setCurrentReport({
|
||||
type: ActionType.reset,
|
||||
});
|
||||
setCurrentReport('reset');
|
||||
}
|
||||
}, [reports]);
|
||||
}, [isEditMode, reports]);
|
||||
|
||||
const onSave = async () => {
|
||||
// Create new Report
|
||||
const newReportValues: Partial<ReportObject> = {
|
||||
crontab: currentReport?.crontab,
|
||||
type: 'Report',
|
||||
active: true,
|
||||
force_screenshot: false,
|
||||
creation_method: props.creationMethod,
|
||||
dashboard: props.dashboardId,
|
||||
chart: props.chart?.id,
|
||||
description: currentReport?.description,
|
||||
name: currentReport?.name,
|
||||
owners: [props.userId],
|
||||
recipients: [
|
||||
{
|
||||
|
|
@ -236,28 +195,28 @@ const ReportModal: FunctionComponent<ReportProps> = ({
|
|||
type: 'Email',
|
||||
},
|
||||
],
|
||||
type: 'Report',
|
||||
creation_method: props.creationMethod,
|
||||
active: true,
|
||||
report_format: currentReport?.report_format || defaultNotificationFormat,
|
||||
timezone: currentReport?.timezone,
|
||||
force_screenshot: false,
|
||||
name: currentReport.name,
|
||||
description: currentReport.description,
|
||||
crontab: currentReport.crontab,
|
||||
report_format: currentReport.report_format || defaultNotificationFormat,
|
||||
timezone: currentReport.timezone,
|
||||
};
|
||||
|
||||
if (isEditMode) {
|
||||
await dispatch(
|
||||
editReport(currentReport?.id, newReportValues as ReportObject),
|
||||
);
|
||||
onHide();
|
||||
} else {
|
||||
try {
|
||||
setCurrentReport({ isSubmitting: true, error: undefined });
|
||||
try {
|
||||
if (isEditMode) {
|
||||
await dispatch(
|
||||
editReport(currentReport.id, newReportValues as ReportObject),
|
||||
);
|
||||
} else {
|
||||
await dispatch(addReport(newReportValues as ReportObject));
|
||||
onHide();
|
||||
} catch (e) {
|
||||
const { message } = await getClientErrorObject(e);
|
||||
onReducerChange(ActionType.error, message);
|
||||
}
|
||||
onHide();
|
||||
} catch (e) {
|
||||
const { error } = await getClientErrorObject(e);
|
||||
setCurrentReport({ error });
|
||||
}
|
||||
setCurrentReport({ isSubmitting: false });
|
||||
|
||||
if (onReportAdd) onReportAdd();
|
||||
};
|
||||
|
|
@ -266,7 +225,7 @@ const ReportModal: FunctionComponent<ReportProps> = ({
|
|||
<StyledIconWrapper>
|
||||
<Icons.Calendar />
|
||||
<span className="text">
|
||||
{isEditMode ? t('Edit Email Report') : t('New Email Report')}
|
||||
{isEditMode ? t('Edit email report') : t('Schedule a new email report')}
|
||||
</span>
|
||||
</StyledIconWrapper>
|
||||
);
|
||||
|
|
@ -280,7 +239,8 @@ const ReportModal: FunctionComponent<ReportProps> = ({
|
|||
key="submit"
|
||||
buttonStyle="primary"
|
||||
onClick={onSave}
|
||||
disabled={!currentReport?.name}
|
||||
disabled={!currentReport.name}
|
||||
loading={currentReport.isSubmitting}
|
||||
>
|
||||
{isEditMode ? t('Save') : t('Add')}
|
||||
</StyledFooterButton>
|
||||
|
|
@ -290,19 +250,16 @@ const ReportModal: FunctionComponent<ReportProps> = ({
|
|||
const renderMessageContentSection = (
|
||||
<>
|
||||
<StyledMessageContentTitle>
|
||||
<h4>{t('Message Content')}</h4>
|
||||
<h4>{t('Message content')}</h4>
|
||||
</StyledMessageContentTitle>
|
||||
<div className="inline-container">
|
||||
<StyledRadioGroup
|
||||
onChange={(event: RadioChangeEvent) => {
|
||||
onReducerChange(ActionType.inputChange, {
|
||||
name: 'report_format',
|
||||
value: event.target.value,
|
||||
});
|
||||
setCurrentReport({ report_format: event.target.value });
|
||||
}}
|
||||
value={currentReport?.report_format || defaultNotificationFormat}
|
||||
value={currentReport.report_format || defaultNotificationFormat}
|
||||
>
|
||||
{TEXT_BASED_VISUALIZATION_TYPES.includes(vizType) && (
|
||||
{isTextBasedChart && (
|
||||
<StyledRadio value={NOTIFICATION_FORMATS.TEXT}>
|
||||
{t('Text embedded in email')}
|
||||
</StyledRadio>
|
||||
|
|
@ -318,15 +275,6 @@ const ReportModal: FunctionComponent<ReportProps> = ({
|
|||
</>
|
||||
);
|
||||
|
||||
const errorAlert = () => (
|
||||
<Alert
|
||||
type="error"
|
||||
css={(theme: SupersetTheme) => antDErrorAlertStyles(theme)}
|
||||
message={t('Report Creation Error')}
|
||||
description={currentReport?.error}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledModal
|
||||
show={show}
|
||||
|
|
@ -340,15 +288,12 @@ const ReportModal: FunctionComponent<ReportProps> = ({
|
|||
<LabeledErrorBoundInput
|
||||
id="name"
|
||||
name="name"
|
||||
value={currentReport?.name || ''}
|
||||
placeholder={t('Weekly Report')}
|
||||
value={currentReport.name || ''}
|
||||
placeholder={initialState.name}
|
||||
required
|
||||
validationMethods={{
|
||||
onChange: ({ target }: { target: HTMLInputElement }) =>
|
||||
onReducerChange(ActionType.inputChange, {
|
||||
name: target.name,
|
||||
value: target.value,
|
||||
}),
|
||||
setCurrentReport({ name: target.value }),
|
||||
}}
|
||||
label="Report Name"
|
||||
data-test="report-name-test"
|
||||
|
|
@ -358,11 +303,9 @@ const ReportModal: FunctionComponent<ReportProps> = ({
|
|||
name="description"
|
||||
value={currentReport?.description || ''}
|
||||
validationMethods={{
|
||||
onChange: ({ target }: { target: HTMLInputElement }) =>
|
||||
onReducerChange(ActionType.inputChange, {
|
||||
name: target.name,
|
||||
value: target.value,
|
||||
}),
|
||||
onChange: ({ target }: { target: HTMLInputElement }) => {
|
||||
setCurrentReport({ description: target.value });
|
||||
},
|
||||
}}
|
||||
label={t('Description')}
|
||||
placeholder={t(
|
||||
|
|
@ -378,17 +321,16 @@ const ReportModal: FunctionComponent<ReportProps> = ({
|
|||
<h4 css={(theme: SupersetTheme) => SectionHeaderStyle(theme)}>
|
||||
{t('Schedule')}
|
||||
</h4>
|
||||
<p>{t('Scheduled reports will be sent to your email as a PNG')}</p>
|
||||
<p>
|
||||
{t('A screenshot of the dashboard will be sent to your email at')}
|
||||
</p>
|
||||
</StyledScheduleTitle>
|
||||
|
||||
<StyledCronPicker
|
||||
clearButton={false}
|
||||
value={t(currentReport?.crontab || '0 12 * * 1')}
|
||||
value={currentReport.crontab || '0 12 * * 1'}
|
||||
setValue={(newValue: string) => {
|
||||
onReducerChange(ActionType.inputChange, {
|
||||
name: 'crontab',
|
||||
value: newValue,
|
||||
});
|
||||
setCurrentReport({ crontab: newValue });
|
||||
}}
|
||||
onError={setCronError}
|
||||
/>
|
||||
|
|
@ -400,17 +342,25 @@ const ReportModal: FunctionComponent<ReportProps> = ({
|
|||
{t('Timezone')}
|
||||
</div>
|
||||
<TimezoneSelector
|
||||
timezone={currentReport.timezone}
|
||||
onTimezoneChange={value => {
|
||||
setCurrentReport({
|
||||
type: ActionType.inputChange,
|
||||
payload: { name: 'timezone', value },
|
||||
});
|
||||
setCurrentReport({ timezone: value });
|
||||
}}
|
||||
timezone={currentReport?.timezone}
|
||||
/>
|
||||
{isChart && renderMessageContentSection}
|
||||
</StyledBottomSection>
|
||||
{currentReport?.error && errorAlert()}
|
||||
{currentReport.error && (
|
||||
<Alert
|
||||
type="error"
|
||||
css={(theme: SupersetTheme) => antDErrorAlertStyles(theme)}
|
||||
message={
|
||||
isEditMode
|
||||
? t('Failed to update report')
|
||||
: t('Failed to create report')
|
||||
}
|
||||
description={currentReport.error}
|
||||
/>
|
||||
)}
|
||||
</StyledModal>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -111,7 +111,8 @@ export const StyledRadioGroup = styled(Radio.Group)`
|
|||
export const antDErrorAlertStyles = (theme: SupersetTheme) => css`
|
||||
border: ${theme.colors.error.base} 1px solid;
|
||||
padding: ${theme.gridUnit * 4}px;
|
||||
margin: ${theme.gridUnit * 8}px ${theme.gridUnit * 4}px;
|
||||
margin: ${theme.gridUnit * 4}px;
|
||||
margin-top: 0;
|
||||
color: ${theme.colors.error.dark2};
|
||||
.ant-alert-message {
|
||||
font-size: ${theme.typography.sizes.m}px;
|
||||
|
|
|
|||
|
|
@ -665,6 +665,7 @@ class Header extends React.PureComponent {
|
|||
userId={user.userId}
|
||||
userEmail={user.email}
|
||||
dashboardId={dashboardInfo.id}
|
||||
dashboardName={dashboardInfo.name}
|
||||
creationMethod="dashboards"
|
||||
/>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -24,7 +24,6 @@ import ReportModal from 'src/components/ReportModal';
|
|||
import { ExplorePageState } from 'src/explore/reducers/getInitialState';
|
||||
import DeleteModal from 'src/components/DeleteModal';
|
||||
import { deleteActiveReport } from 'src/reports/actions/reports';
|
||||
import { ChartState } from 'src/explore/types';
|
||||
|
||||
type ReportMenuItemsProps = {
|
||||
report: Record<string, any>;
|
||||
|
|
@ -41,16 +40,11 @@ export const ExploreReport = ({
|
|||
setIsDeleting,
|
||||
}: ReportMenuItemsProps) => {
|
||||
const dispatch = useDispatch();
|
||||
const chart = useSelector<ExplorePageState, ChartState | undefined>(state => {
|
||||
if (!state.charts) {
|
||||
return undefined;
|
||||
}
|
||||
const charts = Object.values(state.charts);
|
||||
if (charts.length > 0) {
|
||||
return charts[0];
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
const { chart, chartName } = useSelector((state: ExplorePageState) => ({
|
||||
chart: Object.values(state.charts || {})[0],
|
||||
chartName: state.explore.sliceName,
|
||||
}));
|
||||
|
||||
const { userId, email } = useSelector<
|
||||
ExplorePageState,
|
||||
{ userId?: number; email?: string }
|
||||
|
|
@ -69,6 +63,7 @@ export const ExploreReport = ({
|
|||
userId={userId}
|
||||
userEmail={email}
|
||||
chart={chart}
|
||||
chartName={chartName}
|
||||
creationMethod="charts"
|
||||
/>
|
||||
{isDeleting && (
|
||||
|
|
|
|||
|
|
@ -111,22 +111,14 @@ export const addReport = report => dispatch =>
|
|||
|
||||
export const EDIT_REPORT = 'EDIT_REPORT';
|
||||
|
||||
export function editReport(id, report) {
|
||||
return function (dispatch) {
|
||||
SupersetClient.put({
|
||||
endpoint: `/api/v1/report/${id}`,
|
||||
jsonPayload: report,
|
||||
})
|
||||
.then(({ json }) => {
|
||||
dispatch({ type: EDIT_REPORT, json });
|
||||
})
|
||||
.catch(() =>
|
||||
dispatch(
|
||||
addDangerToast(t('An error occurred while editing this report.')),
|
||||
),
|
||||
);
|
||||
};
|
||||
}
|
||||
export const editReport = (id, report) => dispatch =>
|
||||
SupersetClient.put({
|
||||
endpoint: `/api/v1/report/${id}`,
|
||||
jsonPayload: report,
|
||||
}).then(({ json }) => {
|
||||
dispatch({ type: EDIT_REPORT, json });
|
||||
dispatch(addSuccessToast(t('Report updated')));
|
||||
});
|
||||
|
||||
export function toggleActive(report, isActive) {
|
||||
return function toggleActiveThunk(dispatch) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Types mirroring enums in `superset/reports/models.py`:
|
||||
*/
|
||||
export type ReportScheduleType = 'Alert' | 'Report';
|
||||
export type ReportCreationMethod = 'charts' | 'dashboards' | 'alerts_reports';
|
||||
|
||||
export type ReportRecipientType = 'Email' | 'Slack';
|
||||
|
|
@ -29,15 +29,15 @@ export type ClientErrorObject = {
|
|||
error: string;
|
||||
errors?: SupersetError[];
|
||||
link?: string;
|
||||
// marshmallow field validation returns the error mssage in the format
|
||||
// of { field: [msg1, msg2] }
|
||||
message?: string;
|
||||
severity?: string;
|
||||
stacktrace?: string;
|
||||
statusText?: string;
|
||||
} & Partial<SupersetClientResponse>;
|
||||
|
||||
interface ResponseWithTimeout extends Response {
|
||||
// see rejectAfterTimeout.ts
|
||||
interface TimeoutError {
|
||||
statusText: 'timeout';
|
||||
timeout: number;
|
||||
}
|
||||
|
||||
|
|
@ -48,7 +48,13 @@ export function parseErrorJson(responseObject: JsonObject): ClientErrorObject {
|
|||
error.error = error.description = error.errors[0].message;
|
||||
error.link = error.errors[0]?.extra?.link;
|
||||
}
|
||||
|
||||
// Marshmallow field validation returns the error mssage in the format
|
||||
// of { message: { field1: [msg1, msg2], field2: [msg], } }
|
||||
if (error.message && typeof error.message === 'object' && !error.error) {
|
||||
error.error =
|
||||
Object.values(error.message as Record<string, string[]>)[0]?.[0] ||
|
||||
t('Invalid input');
|
||||
}
|
||||
if (error.stack) {
|
||||
error = {
|
||||
...error,
|
||||
|
|
@ -68,78 +74,95 @@ export function parseErrorJson(responseObject: JsonObject): ClientErrorObject {
|
|||
}
|
||||
|
||||
export function getClientErrorObject(
|
||||
response: SupersetClientResponse | ResponseWithTimeout | string,
|
||||
response:
|
||||
| SupersetClientResponse
|
||||
| TimeoutError
|
||||
| { response: Response }
|
||||
| string,
|
||||
): Promise<ClientErrorObject> {
|
||||
// takes a SupersetClientResponse as input, attempts to read response as Json if possible,
|
||||
// and returns a Promise that resolves to a plain object with error key and text value.
|
||||
return new Promise(resolve => {
|
||||
if (typeof response === 'string') {
|
||||
resolve({ error: response });
|
||||
} else {
|
||||
const responseObject =
|
||||
response instanceof Response ? response : response.response;
|
||||
if (responseObject && !responseObject.bodyUsed) {
|
||||
// attempt to read the body as json, and fallback to text. we must clone the
|
||||
// response in order to fallback to .text() because Response is single-read
|
||||
responseObject
|
||||
.clone()
|
||||
.json()
|
||||
.then(errorJson => {
|
||||
const error = { ...responseObject, ...errorJson };
|
||||
resolve(parseErrorJson(error));
|
||||
})
|
||||
.catch(() => {
|
||||
// fall back to reading as text
|
||||
responseObject.text().then((errorText: any) => {
|
||||
resolve({ ...responseObject, error: errorText });
|
||||
});
|
||||
});
|
||||
} else if (
|
||||
'statusText' in response &&
|
||||
response.statusText === 'timeout' &&
|
||||
'timeout' in response
|
||||
) {
|
||||
resolve({
|
||||
...responseObject,
|
||||
error: 'Request timed out',
|
||||
errors: [
|
||||
{
|
||||
error_type: ErrorTypeEnum.FRONTEND_TIMEOUT_ERROR,
|
||||
extra: {
|
||||
timeout: response.timeout / 1000,
|
||||
issue_codes: [
|
||||
{
|
||||
code: 1000,
|
||||
message: t(
|
||||
'Issue 1000 - The dataset is too large to query.',
|
||||
),
|
||||
},
|
||||
{
|
||||
code: 1001,
|
||||
message: t(
|
||||
'Issue 1001 - The database is under an unusual load.',
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
level: 'error',
|
||||
message: 'Request timed out',
|
||||
},
|
||||
],
|
||||
});
|
||||
} else {
|
||||
// fall back to Response.statusText or generic error of we cannot read the response
|
||||
let error = (response as any).statusText || (response as any).message;
|
||||
if (!error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('non-standard error:', response);
|
||||
error = t('An error occurred');
|
||||
}
|
||||
resolve({
|
||||
...responseObject,
|
||||
error,
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
response instanceof TypeError &&
|
||||
response.message === 'Failed to fetch'
|
||||
) {
|
||||
resolve({
|
||||
error: t('Network error'),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
'timeout' in response &&
|
||||
'statusText' in response &&
|
||||
response.statusText === 'timeout'
|
||||
) {
|
||||
resolve({
|
||||
...response,
|
||||
error: t('Request timed out'),
|
||||
errors: [
|
||||
{
|
||||
error_type: ErrorTypeEnum.FRONTEND_TIMEOUT_ERROR,
|
||||
extra: {
|
||||
timeout: response.timeout / 1000,
|
||||
issue_codes: [
|
||||
{
|
||||
code: 1000,
|
||||
message: t('Issue 1000 - The dataset is too large to query.'),
|
||||
},
|
||||
{
|
||||
code: 1001,
|
||||
message: t(
|
||||
'Issue 1001 - The database is under an unusual load.',
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
level: 'error',
|
||||
message: 'Request timed out',
|
||||
},
|
||||
],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const responseObject =
|
||||
response instanceof Response ? response : response.response;
|
||||
if (responseObject && !responseObject.bodyUsed) {
|
||||
// attempt to read the body as json, and fallback to text. we must clone the
|
||||
// response in order to fallback to .text() because Response is single-read
|
||||
responseObject
|
||||
.clone()
|
||||
.json()
|
||||
.then(errorJson => {
|
||||
const error = { ...responseObject, ...errorJson };
|
||||
resolve(parseErrorJson(error));
|
||||
})
|
||||
.catch(() => {
|
||||
// fall back to reading as text
|
||||
responseObject.text().then((errorText: any) => {
|
||||
resolve({ ...responseObject, error: errorText });
|
||||
});
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// fall back to Response.statusText or generic error of we cannot read the response
|
||||
let error = (response as any).statusText || (response as any).message;
|
||||
if (!error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('non-standard error:', response);
|
||||
error = t('An error occurred');
|
||||
}
|
||||
resolve({
|
||||
...responseObject,
|
||||
error,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ class ReportDataFormat(str, enum.Enum):
|
|||
TEXT = "TEXT"
|
||||
|
||||
|
||||
class ReportCreationMethodType(str, enum.Enum):
|
||||
class ReportCreationMethod(str, enum.Enum):
|
||||
CHARTS = "charts"
|
||||
DASHBOARDS = "dashboards"
|
||||
ALERTS_REPORTS = "alerts_reports"
|
||||
|
|
@ -112,7 +112,7 @@ class ReportSchedule(Model, AuditMixinNullable):
|
|||
active = Column(Boolean, default=True, index=True)
|
||||
crontab = Column(String(1000), nullable=False)
|
||||
creation_method = Column(
|
||||
String(255), server_default=ReportCreationMethodType.ALERTS_REPORTS
|
||||
String(255), server_default=ReportCreationMethod.ALERTS_REPORTS
|
||||
)
|
||||
timezone = Column(String(100), default="UTC", nullable=False)
|
||||
report_format = Column(String(50), default=ReportDataFormat.VISUALIZATION)
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ from marshmallow import ValidationError
|
|||
from superset.charts.dao import ChartDAO
|
||||
from superset.commands.base import BaseCommand
|
||||
from superset.dashboards.dao import DashboardDAO
|
||||
from superset.models.reports import ReportCreationMethodType
|
||||
from superset.models.reports import ReportCreationMethod
|
||||
from superset.reports.commands.exceptions import (
|
||||
ChartNotFoundValidationError,
|
||||
ChartNotSavedValidationError,
|
||||
|
|
@ -52,12 +52,12 @@ class BaseReportScheduleCommand(BaseCommand):
|
|||
dashboard_id = self._properties.get("dashboard")
|
||||
creation_method = self._properties.get("creation_method")
|
||||
|
||||
if creation_method == ReportCreationMethodType.CHARTS and not chart_id:
|
||||
if creation_method == ReportCreationMethod.CHARTS and not chart_id:
|
||||
# User has not saved chart yet in Explore view
|
||||
exceptions.append(ChartNotSavedValidationError())
|
||||
return
|
||||
|
||||
if creation_method == ReportCreationMethodType.DASHBOARDS and not dashboard_id:
|
||||
if creation_method == ReportCreationMethod.DASHBOARDS and not dashboard_id:
|
||||
exceptions.append(DashboardNotSavedValidationError())
|
||||
return
|
||||
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ from marshmallow import ValidationError
|
|||
from superset.commands.base import CreateMixin
|
||||
from superset.dao.exceptions import DAOCreateFailedError
|
||||
from superset.databases.dao import DatabaseDAO
|
||||
from superset.models.reports import ReportCreationMethodType, ReportScheduleType
|
||||
from superset.models.reports import ReportCreationMethod, ReportScheduleType
|
||||
from superset.reports.commands.base import BaseReportScheduleCommand
|
||||
from superset.reports.commands.exceptions import (
|
||||
DatabaseNotFoundValidationError,
|
||||
|
|
@ -73,7 +73,11 @@ class CreateReportScheduleCommand(CreateMixin, BaseReportScheduleCommand):
|
|||
if report_type and not ReportScheduleDAO.validate_update_uniqueness(
|
||||
name, report_type
|
||||
):
|
||||
exceptions.append(ReportScheduleNameUniquenessValidationError())
|
||||
exceptions.append(
|
||||
ReportScheduleNameUniquenessValidationError(
|
||||
report_type=report_type, name=name
|
||||
)
|
||||
)
|
||||
|
||||
# validate relation by report type
|
||||
if report_type == ReportScheduleType.ALERT:
|
||||
|
|
@ -93,7 +97,7 @@ class CreateReportScheduleCommand(CreateMixin, BaseReportScheduleCommand):
|
|||
# Validate that each chart or dashboard only has one report with
|
||||
# the respective creation method.
|
||||
if (
|
||||
creation_method != ReportCreationMethodType.ALERTS_REPORTS
|
||||
creation_method != ReportCreationMethod.ALERTS_REPORTS
|
||||
and not ReportScheduleDAO.validate_unique_creation_method(
|
||||
user_id, dashboard_id, chart_id
|
||||
)
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ from superset.commands.exceptions import (
|
|||
ForbiddenError,
|
||||
ValidationError,
|
||||
)
|
||||
from superset.models.reports import ReportScheduleType
|
||||
|
||||
|
||||
class DatabaseNotFoundValidationError(ValidationError):
|
||||
|
|
@ -163,13 +164,16 @@ class ReportScheduleNameUniquenessValidationError(ValidationError):
|
|||
Marshmallow validation error for Report Schedule name and type already exists
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__([_("Name must be unique")], field_name="name")
|
||||
def __init__(self, report_type: ReportScheduleType, name: str) -> None:
|
||||
message = _('A report named "%(name)s" already exists', name=name)
|
||||
if report_type == ReportScheduleType.ALERT:
|
||||
message = _('An alert named "%(name)s" already exists', name=name)
|
||||
super().__init__([message], field_name="name")
|
||||
|
||||
|
||||
class ReportScheduleCreationMethodUniquenessValidationError(CommandException):
|
||||
status = 409
|
||||
message = "Resource already has an attached report."
|
||||
message = _("Resource already has an attached report.")
|
||||
|
||||
|
||||
class AlertQueryMultipleRowsError(CommandException):
|
||||
|
|
|
|||
|
|
@ -86,9 +86,13 @@ class UpdateReportScheduleCommand(UpdateMixin, BaseReportScheduleCommand):
|
|||
|
||||
# Validate name type uniqueness
|
||||
if not ReportScheduleDAO.validate_update_uniqueness(
|
||||
name, report_type, report_schedule_id=self._model_id
|
||||
name, report_type, expect_id=self._model_id
|
||||
):
|
||||
exceptions.append(ReportScheduleNameUniquenessValidationError())
|
||||
exceptions.append(
|
||||
ReportScheduleNameUniquenessValidationError(
|
||||
report_type=report_type, name=name
|
||||
)
|
||||
)
|
||||
|
||||
if report_type == ReportScheduleType.ALERT:
|
||||
database_id = self._properties.get("database")
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ from superset.models.reports import (
|
|||
ReportExecutionLog,
|
||||
ReportRecipients,
|
||||
ReportSchedule,
|
||||
ReportScheduleType,
|
||||
ReportState,
|
||||
)
|
||||
|
||||
|
|
@ -133,23 +134,24 @@ class ReportScheduleDAO(BaseDAO):
|
|||
|
||||
@staticmethod
|
||||
def validate_update_uniqueness(
|
||||
name: str, report_type: str, report_schedule_id: Optional[int] = None
|
||||
name: str, report_type: ReportScheduleType, expect_id: Optional[int] = None
|
||||
) -> bool:
|
||||
"""
|
||||
Validate if this name and type is unique.
|
||||
|
||||
:param name: The report schedule name
|
||||
:param report_type: The report schedule type
|
||||
:param report_schedule_id: The report schedule current id
|
||||
(only for validating on updates)
|
||||
:param expect_id: The id of the expected report schedule with the
|
||||
name + type combination. Useful for validating existing report schedule.
|
||||
:return: bool
|
||||
"""
|
||||
query = db.session.query(ReportSchedule).filter(
|
||||
ReportSchedule.name == name, ReportSchedule.type == report_type
|
||||
found_id = (
|
||||
db.session.query(ReportSchedule.id)
|
||||
.filter(ReportSchedule.name == name, ReportSchedule.type == report_type)
|
||||
.limit(1)
|
||||
.scalar()
|
||||
)
|
||||
if report_schedule_id:
|
||||
query = query.filter(ReportSchedule.id != report_schedule_id)
|
||||
return not db.session.query(query.exists()).scalar()
|
||||
return found_id is None or found_id == expect_id
|
||||
|
||||
@classmethod
|
||||
def create(cls, properties: Dict[str, Any], commit: bool = True) -> Model:
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ from marshmallow_enum import EnumField
|
|||
from pytz import all_timezones
|
||||
|
||||
from superset.models.reports import (
|
||||
ReportCreationMethodType,
|
||||
ReportCreationMethod,
|
||||
ReportDataFormat,
|
||||
ReportRecipientType,
|
||||
ReportScheduleType,
|
||||
|
|
@ -164,7 +164,7 @@ class ReportSchedulePostSchema(Schema):
|
|||
)
|
||||
chart = fields.Integer(required=False, allow_none=True)
|
||||
creation_method = EnumField(
|
||||
ReportCreationMethodType,
|
||||
ReportCreationMethod,
|
||||
by_value=True,
|
||||
required=False,
|
||||
description=creation_method_description,
|
||||
|
|
@ -256,7 +256,7 @@ class ReportSchedulePutSchema(Schema):
|
|||
)
|
||||
chart = fields.Integer(required=False, allow_none=True)
|
||||
creation_method = EnumField(
|
||||
ReportCreationMethodType,
|
||||
ReportCreationMethod,
|
||||
by_value=True,
|
||||
allow_none=True,
|
||||
description=creation_method_description,
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ from superset.models.slice import Slice
|
|||
from superset.models.dashboard import Dashboard
|
||||
from superset.models.reports import (
|
||||
ReportSchedule,
|
||||
ReportCreationMethodType,
|
||||
ReportCreationMethod,
|
||||
ReportRecipients,
|
||||
ReportExecutionLog,
|
||||
ReportScheduleType,
|
||||
|
|
@ -452,7 +452,7 @@ class TestReportSchedulesApi(SupersetTestCase):
|
|||
"name": "new3",
|
||||
"description": "description",
|
||||
"crontab": "0 9 * * *",
|
||||
"creation_method": ReportCreationMethodType.ALERTS_REPORTS,
|
||||
"creation_method": ReportCreationMethod.ALERTS_REPORTS,
|
||||
"recipients": [
|
||||
{
|
||||
"type": ReportRecipientType.EMAIL,
|
||||
|
|
@ -499,7 +499,7 @@ class TestReportSchedulesApi(SupersetTestCase):
|
|||
"type": ReportScheduleType.ALERT,
|
||||
"name": "name3",
|
||||
"description": "description",
|
||||
"creation_method": ReportCreationMethodType.ALERTS_REPORTS,
|
||||
"creation_method": ReportCreationMethod.ALERTS_REPORTS,
|
||||
"crontab": "0 9 * * *",
|
||||
"chart": chart.id,
|
||||
"database": example_db.id,
|
||||
|
|
@ -508,7 +508,7 @@ class TestReportSchedulesApi(SupersetTestCase):
|
|||
rv = self.client.post(uri, json=report_schedule_data)
|
||||
assert rv.status_code == 422
|
||||
data = json.loads(rv.data.decode("utf-8"))
|
||||
assert data == {"message": {"name": ["Name must be unique"]}}
|
||||
assert data == {"message": {"name": ['An alert named "name3" already exists']}}
|
||||
|
||||
# Check that uniqueness is composed by name and type
|
||||
report_schedule_data = {
|
||||
|
|
@ -516,7 +516,7 @@ class TestReportSchedulesApi(SupersetTestCase):
|
|||
"name": "name3",
|
||||
"description": "description",
|
||||
"crontab": "0 9 * * *",
|
||||
"creation_method": ReportCreationMethodType.ALERTS_REPORTS,
|
||||
"creation_method": ReportCreationMethod.ALERTS_REPORTS,
|
||||
"chart": chart.id,
|
||||
}
|
||||
uri = "api/v1/report/"
|
||||
|
|
@ -546,7 +546,7 @@ class TestReportSchedulesApi(SupersetTestCase):
|
|||
"type": ReportScheduleType.REPORT,
|
||||
"name": "name3",
|
||||
"description": "description",
|
||||
"creation_method": ReportCreationMethodType.ALERTS_REPORTS,
|
||||
"creation_method": ReportCreationMethod.ALERTS_REPORTS,
|
||||
"crontab": "0 9 * * *",
|
||||
"chart": chart.id,
|
||||
"database": example_db.id,
|
||||
|
|
@ -560,7 +560,7 @@ class TestReportSchedulesApi(SupersetTestCase):
|
|||
"type": ReportScheduleType.ALERT,
|
||||
"name": "new3",
|
||||
"description": "description",
|
||||
"creation_method": ReportCreationMethodType.ALERTS_REPORTS,
|
||||
"creation_method": ReportCreationMethod.ALERTS_REPORTS,
|
||||
"crontab": "0 9 * * *",
|
||||
"recipients": [
|
||||
{
|
||||
|
|
@ -585,7 +585,7 @@ class TestReportSchedulesApi(SupersetTestCase):
|
|||
"type": ReportScheduleType.ALERT,
|
||||
"name": "new3",
|
||||
"description": "description",
|
||||
"creation_method": ReportCreationMethodType.ALERTS_REPORTS,
|
||||
"creation_method": ReportCreationMethod.ALERTS_REPORTS,
|
||||
"crontab": "0 9 * * *",
|
||||
"recipients": [
|
||||
{
|
||||
|
|
@ -609,7 +609,7 @@ class TestReportSchedulesApi(SupersetTestCase):
|
|||
"type": ReportScheduleType.ALERT,
|
||||
"name": "new3",
|
||||
"description": "description",
|
||||
"creation_method": ReportCreationMethodType.ALERTS_REPORTS,
|
||||
"creation_method": ReportCreationMethod.ALERTS_REPORTS,
|
||||
"crontab": "0 9 * * *",
|
||||
"recipients": [
|
||||
{
|
||||
|
|
@ -635,7 +635,7 @@ class TestReportSchedulesApi(SupersetTestCase):
|
|||
"type": ReportScheduleType.ALERT,
|
||||
"name": "new4",
|
||||
"description": "description",
|
||||
"creation_method": ReportCreationMethodType.ALERTS_REPORTS,
|
||||
"creation_method": ReportCreationMethod.ALERTS_REPORTS,
|
||||
"crontab": "0 9 * * *",
|
||||
"recipients": [
|
||||
{
|
||||
|
|
@ -661,7 +661,7 @@ class TestReportSchedulesApi(SupersetTestCase):
|
|||
"type": ReportScheduleType.ALERT,
|
||||
"name": "new5",
|
||||
"description": "description",
|
||||
"creation_method": ReportCreationMethodType.ALERTS_REPORTS,
|
||||
"creation_method": ReportCreationMethod.ALERTS_REPORTS,
|
||||
"crontab": "0 9 * * *",
|
||||
"recipients": [
|
||||
{
|
||||
|
|
@ -687,7 +687,7 @@ class TestReportSchedulesApi(SupersetTestCase):
|
|||
"type": ReportScheduleType.ALERT,
|
||||
"name": "new5",
|
||||
"description": "description",
|
||||
"creation_method": ReportCreationMethodType.ALERTS_REPORTS,
|
||||
"creation_method": ReportCreationMethod.ALERTS_REPORTS,
|
||||
"crontab": "0 9 * * *",
|
||||
"recipients": [
|
||||
{
|
||||
|
|
@ -714,7 +714,7 @@ class TestReportSchedulesApi(SupersetTestCase):
|
|||
"type": ReportScheduleType.ALERT,
|
||||
"name": "new5",
|
||||
"description": "description",
|
||||
"creation_method": ReportCreationMethodType.ALERTS_REPORTS,
|
||||
"creation_method": ReportCreationMethod.ALERTS_REPORTS,
|
||||
"crontab": "0 9 * * *",
|
||||
"recipients": [
|
||||
{
|
||||
|
|
@ -745,7 +745,7 @@ class TestReportSchedulesApi(SupersetTestCase):
|
|||
"type": ReportScheduleType.ALERT,
|
||||
"name": "new6",
|
||||
"description": "description",
|
||||
"creation_method": ReportCreationMethodType.ALERTS_REPORTS,
|
||||
"creation_method": ReportCreationMethod.ALERTS_REPORTS,
|
||||
"crontab": "0 9 * * *",
|
||||
"recipients": [
|
||||
{
|
||||
|
|
@ -784,7 +784,7 @@ class TestReportSchedulesApi(SupersetTestCase):
|
|||
"type": ReportScheduleType.REPORT,
|
||||
"name": "name3",
|
||||
"description": "description",
|
||||
"creation_method": ReportCreationMethodType.CHARTS,
|
||||
"creation_method": ReportCreationMethod.CHARTS,
|
||||
"crontab": "0 9 * * *",
|
||||
"chart": 0,
|
||||
}
|
||||
|
|
@ -812,7 +812,7 @@ class TestReportSchedulesApi(SupersetTestCase):
|
|||
"type": ReportScheduleType.REPORT,
|
||||
"name": "name3",
|
||||
"description": "description",
|
||||
"creation_method": ReportCreationMethodType.DASHBOARDS,
|
||||
"creation_method": ReportCreationMethod.DASHBOARDS,
|
||||
"crontab": "0 9 * * *",
|
||||
}
|
||||
uri = "api/v1/report/"
|
||||
|
|
@ -839,7 +839,7 @@ class TestReportSchedulesApi(SupersetTestCase):
|
|||
"type": ReportScheduleType.REPORT,
|
||||
"name": "name4",
|
||||
"description": "description",
|
||||
"creation_method": ReportCreationMethodType.CHARTS,
|
||||
"creation_method": ReportCreationMethod.CHARTS,
|
||||
"crontab": "0 9 * * *",
|
||||
"working_timeout": 3600,
|
||||
"chart": chart.id,
|
||||
|
|
@ -855,7 +855,7 @@ class TestReportSchedulesApi(SupersetTestCase):
|
|||
"type": ReportScheduleType.REPORT,
|
||||
"name": "name5",
|
||||
"description": "description",
|
||||
"creation_method": ReportCreationMethodType.CHARTS,
|
||||
"creation_method": ReportCreationMethod.CHARTS,
|
||||
"crontab": "0 9 * * *",
|
||||
"working_timeout": 3600,
|
||||
"chart": chart.id,
|
||||
|
|
@ -897,7 +897,7 @@ class TestReportSchedulesApi(SupersetTestCase):
|
|||
"type": ReportScheduleType.REPORT,
|
||||
"name": "name4",
|
||||
"description": "description",
|
||||
"creation_method": ReportCreationMethodType.DASHBOARDS,
|
||||
"creation_method": ReportCreationMethod.DASHBOARDS,
|
||||
"crontab": "0 9 * * *",
|
||||
"working_timeout": 3600,
|
||||
"dashboard": dashboard.id,
|
||||
|
|
@ -913,7 +913,7 @@ class TestReportSchedulesApi(SupersetTestCase):
|
|||
"type": ReportScheduleType.REPORT,
|
||||
"name": "name5",
|
||||
"description": "description",
|
||||
"creation_method": ReportCreationMethodType.DASHBOARDS,
|
||||
"creation_method": ReportCreationMethod.DASHBOARDS,
|
||||
"crontab": "0 9 * * *",
|
||||
"working_timeout": 3600,
|
||||
"dashboard": dashboard.id,
|
||||
|
|
@ -956,7 +956,7 @@ class TestReportSchedulesApi(SupersetTestCase):
|
|||
"name": "new3",
|
||||
"description": "description",
|
||||
"crontab": "0 9 * * *",
|
||||
"creation_method": ReportCreationMethodType.ALERTS_REPORTS,
|
||||
"creation_method": ReportCreationMethod.ALERTS_REPORTS,
|
||||
"chart": chart.id,
|
||||
"dashboard": dashboard.id,
|
||||
"database": example_db.id,
|
||||
|
|
@ -980,7 +980,7 @@ class TestReportSchedulesApi(SupersetTestCase):
|
|||
"type": ReportScheduleType.ALERT,
|
||||
"name": "new3",
|
||||
"description": "description",
|
||||
"creation_method": ReportCreationMethodType.ALERTS_REPORTS,
|
||||
"creation_method": ReportCreationMethod.ALERTS_REPORTS,
|
||||
"crontab": "0 9 * * *",
|
||||
"chart": chart.id,
|
||||
}
|
||||
|
|
@ -1006,7 +1006,7 @@ class TestReportSchedulesApi(SupersetTestCase):
|
|||
"type": ReportScheduleType.ALERT,
|
||||
"name": "new3",
|
||||
"description": "description",
|
||||
"creation_method": ReportCreationMethodType.ALERTS_REPORTS,
|
||||
"creation_method": ReportCreationMethod.ALERTS_REPORTS,
|
||||
"crontab": "0 9 * * *",
|
||||
"chart": chart_max_id + 1,
|
||||
"database": database_max_id + 1,
|
||||
|
|
@ -1029,7 +1029,7 @@ class TestReportSchedulesApi(SupersetTestCase):
|
|||
"name": "new3",
|
||||
"description": "description",
|
||||
"crontab": "0 9 * * *",
|
||||
"creation_method": ReportCreationMethodType.ALERTS_REPORTS,
|
||||
"creation_method": ReportCreationMethod.ALERTS_REPORTS,
|
||||
"dashboard": dashboard_max_id + 1,
|
||||
"database": examples_db.id,
|
||||
}
|
||||
|
|
@ -1197,7 +1197,7 @@ class TestReportSchedulesApi(SupersetTestCase):
|
|||
rv = self.client.put(uri, json=report_schedule_data)
|
||||
data = json.loads(rv.data.decode("utf-8"))
|
||||
assert rv.status_code == 422
|
||||
assert data == {"message": {"name": ["Name must be unique"]}}
|
||||
assert data == {"message": {"name": ['An alert named "name3" already exists']}}
|
||||
|
||||
@pytest.mark.usefixtures("create_report_schedules")
|
||||
def test_update_report_schedule_not_found(self):
|
||||
|
|
@ -1546,7 +1546,7 @@ class TestReportSchedulesApi(SupersetTestCase):
|
|||
"name": "new3",
|
||||
"description": "description",
|
||||
"crontab": "0 9 * * *",
|
||||
"creation_method": ReportCreationMethodType.ALERTS_REPORTS,
|
||||
"creation_method": ReportCreationMethod.ALERTS_REPORTS,
|
||||
"recipients": [
|
||||
{
|
||||
"type": ReportRecipientType.EMAIL,
|
||||
|
|
@ -1581,7 +1581,7 @@ class TestReportSchedulesApi(SupersetTestCase):
|
|||
"name": "new3",
|
||||
"description": "description",
|
||||
"crontab": "0 9 * * *",
|
||||
"creation_method": ReportCreationMethodType.ALERTS_REPORTS,
|
||||
"creation_method": ReportCreationMethod.ALERTS_REPORTS,
|
||||
"recipients": [
|
||||
{
|
||||
"type": ReportRecipientType.EMAIL,
|
||||
|
|
|
|||
Loading…
Reference in New Issue