From a3102488a1eced3e97be636aa2cf8941d8e1ee6a Mon Sep 17 00:00:00 2001 From: Elizabeth Thompson Date: Tue, 10 Aug 2021 15:11:10 -0700 Subject: [PATCH] feat: add chart image info to reports from charts (#16158) * refetch reports on props update * add chart types to reports --- .../src/components/ReportModal/index.test.tsx | 7 ++ .../src/components/ReportModal/index.tsx | 105 ++++++++++++++---- .../src/components/ReportModal/styles.tsx | 28 +++++ .../src/components/TimezoneSelector/index.tsx | 2 +- .../src/dashboard/components/Header/index.jsx | 12 ++ .../explore/components/ExploreChartHeader.jsx | 2 +- 6 files changed, 131 insertions(+), 25 deletions(-) diff --git a/superset-frontend/src/components/ReportModal/index.test.tsx b/superset-frontend/src/components/ReportModal/index.test.tsx index 27488dcdff..99b1eadcc4 100644 --- a/superset-frontend/src/components/ReportModal/index.test.tsx +++ b/superset-frontend/src/components/ReportModal/index.test.tsx @@ -38,6 +38,13 @@ const defaultProps = { userEmail: 'test@test.com', dashboardId: 1, creationMethod: 'charts_dashboards', + props: { + chart: { + sliceFormData: { + viz_type: 'table', + }, + }, + }, }; describe('Email Report Modal', () => { diff --git a/superset-frontend/src/components/ReportModal/index.tsx b/superset-frontend/src/components/ReportModal/index.tsx index ec2ee4ab32..fbdb751e3e 100644 --- a/superset-frontend/src/components/ReportModal/index.tsx +++ b/superset-frontend/src/components/ReportModal/index.tsx @@ -29,22 +29,28 @@ import { bindActionCreators } from 'redux'; import { connect, useDispatch, useSelector } from 'react-redux'; import { addReport, editReport } from 'src/reports/actions/reports'; import { AlertObject } from 'src/views/CRUD/alert/types'; -import LabeledErrorBoundInput from 'src/components/Form/LabeledErrorBoundInput'; + import TimezoneSelector from 'src/components/TimezoneSelector'; +import LabeledErrorBoundInput from 'src/components/Form/LabeledErrorBoundInput'; import Icons from 'src/components/Icons'; import withToasts from 'src/messageToasts/enhancers/withToasts'; -import { CronPicker, CronError } from 'src/components/CronPicker'; +import { CronError } from 'src/components/CronPicker'; +import { RadioChangeEvent } from 'src/common/components'; import { StyledModal, StyledTopSection, StyledBottomSection, StyledIconWrapper, StyledScheduleTitle, + StyledCronPicker, StyledCronError, noBottomMargin, StyledFooterButton, TimezoneHeaderStyle, SectionHeaderStyle, + StyledMessageContentTitle, + StyledRadio, + StyledRadioGroup, } from './styles'; interface ReportObject { @@ -67,6 +73,19 @@ interface ReportObject { creation_method: string; } +interface ChartObject { + id: number; + chartAlert: string; + chartStatus: string; + chartUpdateEndTime: number; + chartUpdateStartTime: number; + latestQueryFormData: object; + queryController: { abort: () => {} }; + queriesResponse: object; + triggerQuery: boolean; + lastRendered: number; +} + interface ReportProps { addDangerToast: (msg: string) => void; addSuccessToast: (msg: string) => void; @@ -77,26 +96,25 @@ interface ReportProps { userId: number; userEmail: string; dashboardId?: number; - chartId?: number; + chart?: ChartObject; creationMethod: string; props: any; } -enum ActionType { - textChange, - inputChange, - fetched, - reset, -} - interface ReportPayloadType { name: string; value: string; } +enum ActionType { + inputChange, + fetched, + reset, +} + type ReportActionType = | { - type: ActionType.textChange | ActionType.inputChange; + type: ActionType.inputChange; payload: ReportPayloadType; } | { @@ -107,17 +125,26 @@ type ReportActionType = type: ActionType.reset; }; +const DEFAULT_NOTIFICATION_FORMAT = 'TEXT'; +const TEXT_BASED_VISUALIZATION_TYPES = [ + 'pivot_table', + 'pivot_table_v2', + 'table', + 'paired_ttest', +]; + const reportReducer = ( state: Partial | null, action: ReportActionType, ): Partial | null => { const initialState = { name: state?.name || 'Weekly Report', + report_format: state?.report_format || DEFAULT_NOTIFICATION_FORMAT, ...(state || {}), }; switch (action.type) { - case ActionType.textChange: + case ActionType.inputChange: return { ...initialState, [action.payload.name]: action.payload.value, @@ -139,6 +166,7 @@ const ReportModal: FunctionComponent = ({ show = false, ...props }) => { + const vizType = props.props.chart?.sliceFormData?.viz_type; const [currentReport, setCurrentReport] = useReducer< Reducer | null, ReportActionType> >(reportReducer, null); @@ -166,7 +194,6 @@ const ReportModal: FunctionComponent = ({ } }, [reports]); const onClose = () => { - // setLoading(false); onHide(); }; const onSave = async () => { @@ -174,7 +201,7 @@ const ReportModal: FunctionComponent = ({ const newReportValues: Partial = { crontab: currentReport?.crontab, dashboard: props.props.dashboardId, - chart: props.props.chartId, + chart: props.props.chart?.id, description: currentReport?.description, name: currentReport?.name, owners: [props.props.userId], @@ -187,9 +214,9 @@ const ReportModal: FunctionComponent = ({ type: 'Report', creation_method: props.props.creationMethod, active: true, + report_format: currentReport?.report_format, }; - // setLoading(true); if (isEditMode) { await dispatch( editReport(currentReport?.id, newReportValues as ReportObject), @@ -217,7 +244,7 @@ const ReportModal: FunctionComponent = ({ const renderModalFooter = ( <> - Cancel + {t('Cancel')} = ({ ); + const renderMessageContentSection = ( + <> + +

{t('Message Content')}

+
+
+ { + onChange(ActionType.inputChange, { + name: 'report_format', + value: event.target.value, + }); + }} + value={currentReport?.report_format || DEFAULT_NOTIFICATION_FORMAT} + > + {TEXT_BASED_VISUALIZATION_TYPES.includes(vizType) && ( + + {t('Text embedded in email')} + + )} + + {t('Image (PNG) embedded in email')} + + + {t('Formatted CSV attached in email')} + + +
+ + ); + return ( = ({ required validationMethods={{ onChange: ({ target }: { target: HTMLInputElement }) => - onChange(ActionType.textChange, { + onChange(ActionType.inputChange, { name: target.name, value: target.value, }), @@ -266,7 +324,7 @@ const ReportModal: FunctionComponent = ({ value={currentReport?.description || ''} validationMethods={{ onChange: ({ target }: { target: HTMLInputElement }) => - onChange(ActionType.textChange, { + onChange(ActionType.inputChange, { name: target.name, value: target.value, }), @@ -284,16 +342,16 @@ const ReportModal: FunctionComponent = ({

SectionHeaderStyle(theme)}> - Schedule + {t('Schedule')}

-

Scheduled reports will be sent to your email as a PNG

+

{t('Scheduled reports will be sent to your email as a PNG')}

- { - onChange(ActionType.textChange, { + onChange(ActionType.inputChange, { name: 'crontab', value: newValue, }); @@ -310,12 +368,13 @@ const ReportModal: FunctionComponent = ({ { setCurrentReport({ - type: ActionType.textChange, + type: ActionType.inputChange, payload: { name: 'timezone', value }, }); }} timezone={currentReport?.timezone} /> + {props.props.chart && renderMessageContentSection}
); diff --git a/superset-frontend/src/components/ReportModal/styles.tsx b/superset-frontend/src/components/ReportModal/styles.tsx index d9b7458cc1..cd68b271eb 100644 --- a/superset-frontend/src/components/ReportModal/styles.tsx +++ b/superset-frontend/src/components/ReportModal/styles.tsx @@ -20,11 +20,17 @@ import { styled, css, SupersetTheme } from '@superset-ui/core'; import Modal from 'src/components/Modal'; import Button from 'src/components/Button'; +import { Radio } from 'src/components/Radio'; +import { CronPicker } from 'src/components/CronPicker'; export const StyledModal = styled(Modal)` .ant-modal-body { padding: 0; } + + h4 { + font-weight: 600; + } `; export const StyledTopSection = styled.div` @@ -61,6 +67,14 @@ export const StyledIconWrapper = styled.span` export const StyledScheduleTitle = styled.div` margin-bottom: ${({ theme }) => theme.gridUnit * 7}px; + + h4 { + margin-bottom: ${({ theme }) => theme.gridUnit * 3}px; + } +`; + +export const StyledCronPicker = styled(CronPicker)` + margin-bottom: ${({ theme }) => theme.gridUnit * 3}px; `; export const StyledCronError = styled.p` @@ -83,3 +97,17 @@ export const SectionHeaderStyle = (theme: SupersetTheme) => css` margin: ${theme.gridUnit * 3}px 0; font-weight: ${theme.typography.weights.bold}; `; + +export const StyledMessageContentTitle = styled.div` + margin: ${({ theme }) => theme.gridUnit * 8}px 0 + ${({ theme }) => theme.gridUnit * 4}px; +`; + +export const StyledRadio = styled(Radio)` + display: block; + line-height: ${({ theme }) => theme.gridUnit * 8}px; +`; + +export const StyledRadioGroup = styled(Radio.Group)` + margin-left: ${({ theme }) => theme.gridUnit * 0.5}px; +`; diff --git a/superset-frontend/src/components/TimezoneSelector/index.tsx b/superset-frontend/src/components/TimezoneSelector/index.tsx index b63bf41eb5..73c6f1fc86 100644 --- a/superset-frontend/src/components/TimezoneSelector/index.tsx +++ b/superset-frontend/src/components/TimezoneSelector/index.tsx @@ -23,7 +23,7 @@ import moment from 'moment-timezone'; import { NativeGraySelect as Select } from 'src/components/Select'; const DEFAULT_TIMEZONE = 'GMT Standard Time'; -const MIN_SELECT_WIDTH = '375px'; +const MIN_SELECT_WIDTH = '400px'; const offsetsToName = { '-300-240': ['Eastern Standard Time', 'Eastern Daylight Time'], diff --git a/superset-frontend/src/dashboard/components/Header/index.jsx b/superset-frontend/src/dashboard/components/Header/index.jsx index 3b934859ee..e990ace4ab 100644 --- a/superset-frontend/src/dashboard/components/Header/index.jsx +++ b/superset-frontend/src/dashboard/components/Header/index.jsx @@ -177,11 +177,13 @@ class Header extends React.PureComponent { 'dashboard_id', 'dashboards', dashboardInfo.id, + user.email, ); } } UNSAFE_componentWillReceiveProps(nextProps) { + const { user } = this.props; if ( UNDO_LIMIT - nextProps.undoLength <= 0 && !this.state.didNotifyMaxUndoHistoryToast @@ -195,6 +197,16 @@ class Header extends React.PureComponent { ) { this.props.setMaxUndoHistoryExceeded(); } + if (user && nextProps.dashboardInfo.id !== this.props.dashboardInfo.id) { + // this is in case there is an anonymous user. + this.props.fetchUISpecificReport( + user.userId, + 'dashboard_id', + 'dashboards', + nextProps.dashboardInfo.id, + user.email, + ); + } } componentWillUnmount() { diff --git a/superset-frontend/src/explore/components/ExploreChartHeader.jsx b/superset-frontend/src/explore/components/ExploreChartHeader.jsx index 7b19d22188..57632d4b95 100644 --- a/superset-frontend/src/explore/components/ExploreChartHeader.jsx +++ b/superset-frontend/src/explore/components/ExploreChartHeader.jsx @@ -295,7 +295,7 @@ export class ExploreChartHeader extends React.PureComponent { props={{ userId: this.props.user.userId, userEmail: this.props.user.email, - chartId: this.props.chart.id, + chart: this.props.chart, creationMethod: 'charts', }} />