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',
             }}
           />