You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by vi...@apache.org on 2021/08/16 05:49:01 UTC
[superset] 11/34: feat: add chart image info to reports from charts
(#16158)
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 <es...@gmail.com>
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: '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 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',
}}
/>