This is an automated email from the ASF dual-hosted git repository. villebro pushed a commit to branch 1.3 in repository https://gitbox.apache.org/repos/asf/superset.git
commit bab7e7a8424814cfe781c429b17ab433c21a434e Author: Elizabeth Thompson <[email protected]> AuthorDate: Tue Aug 10 15:11:10 2021 -0700 feat: add chart image info to reports from charts (#16158) * refetch reports on props update * add chart types to reports (cherry picked from commit a3102488a1eced3e97be636aa2cf8941d8e1ee6a) --- .../src/components/ReportModal/index.test.tsx | 7 ++ .../src/components/ReportModal/index.tsx | 103 ++++++++++++++++----- .../src/components/ReportModal/styles.tsx | 28 ++++++ .../src/components/TimezoneSelector/index.tsx | 2 +- .../src/dashboard/components/Header/index.jsx | 12 +++ .../src/explore/components/ExploreChartHeader.jsx | 2 +- 6 files changed, 130 insertions(+), 24 deletions(-) diff --git a/superset-frontend/src/components/ReportModal/index.test.tsx b/superset-frontend/src/components/ReportModal/index.test.tsx index 27488dc..99b1ead 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: '[email protected]', 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 ec2ee4a..fbdb751 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; } +interface ReportPayloadType { + name: string; + value: string; +} + enum ActionType { - textChange, inputChange, fetched, reset, } -interface ReportPayloadType { - name: string; - value: string; -} - 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<ReportObject> | null, action: ReportActionType, ): Partial<ReportObject> | 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<ReportProps> = ({ show = false, ...props }) => { + const vizType = props.props.chart?.sliceFormData?.viz_type; const [currentReport, setCurrentReport] = useReducer< Reducer<Partial<ReportObject> | null, ReportActionType> >(reportReducer, null); @@ -166,7 +194,6 @@ const ReportModal: FunctionComponent<ReportProps> = ({ } }, [reports]); const onClose = () => { - // setLoading(false); onHide(); }; const onSave = async () => { @@ -174,7 +201,7 @@ const ReportModal: FunctionComponent<ReportProps> = ({ const newReportValues: Partial<ReportObject> = { 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<ReportProps> = ({ 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<ReportProps> = ({ const renderModalFooter = ( <> <StyledFooterButton key="back" onClick={onClose}> - Cancel + {t('Cancel')} </StyledFooterButton> <StyledFooterButton key="submit" @@ -230,6 +257,37 @@ const ReportModal: FunctionComponent<ReportProps> = ({ </> ); + const renderMessageContentSection = ( + <> + <StyledMessageContentTitle> + <h4>{t('Message Content')}</h4> + </StyledMessageContentTitle> + <div className="inline-container"> + <StyledRadioGroup + onChange={(event: RadioChangeEvent) => { + onChange(ActionType.inputChange, { + name: 'report_format', + value: event.target.value, + }); + }} + value={currentReport?.report_format || DEFAULT_NOTIFICATION_FORMAT} + > + {TEXT_BASED_VISUALIZATION_TYPES.includes(vizType) && ( + <StyledRadio value="TEXT"> + {t('Text embedded in email')} + </StyledRadio> + )} + <StyledRadio value="PNG"> + {t('Image (PNG) embedded in email')} + </StyledRadio> + <StyledRadio value="CSV"> + {t('Formatted CSV attached in email')} + </StyledRadio> + </StyledRadioGroup> + </div> + </> + ); + return ( <StyledModal show={show} @@ -248,7 +306,7 @@ const ReportModal: FunctionComponent<ReportProps> = ({ 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<ReportProps> = ({ 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<ReportProps> = ({ <StyledBottomSection> <StyledScheduleTitle> <h4 css={(theme: SupersetTheme) => SectionHeaderStyle(theme)}> - Schedule + {t('Schedule')} </h4> - <p>Scheduled reports will be sent to your email as a PNG</p> + <p>{t('Scheduled reports will be sent to your email as a PNG')}</p> </StyledScheduleTitle> - <CronPicker + <StyledCronPicker clearButton={false} value={currentReport?.crontab || '0 12 * * 1'} setValue={(newValue: string) => { - onChange(ActionType.textChange, { + onChange(ActionType.inputChange, { name: 'crontab', value: newValue, }); @@ -310,12 +368,13 @@ const ReportModal: FunctionComponent<ReportProps> = ({ <TimezoneSelector onTimezoneChange={value => { setCurrentReport({ - type: ActionType.textChange, + type: ActionType.inputChange, payload: { name: 'timezone', value }, }); }} timezone={currentReport?.timezone} /> + {props.props.chart && renderMessageContentSection} </StyledBottomSection> </StyledModal> ); diff --git a/superset-frontend/src/components/ReportModal/styles.tsx b/superset-frontend/src/components/ReportModal/styles.tsx index d9b7458..cd68b27 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 b63bf41..73c6f1f 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 3b93485..e990ace 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 7b19d22..57632d4 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', }} />
