This is an automated email from the ASF dual-hosted git repository.

elizabeth pushed a commit to branch selfSubscribeReports
in repository https://gitbox.apache.org/repos/asf/superset.git

commit 0e3088ab49f1a683ea6cc7d7db1ad68993b1a35d
Author: Lyndsi Kay Williams <[email protected]>
AuthorDate: Fri Jul 16 11:16:08 2021 -0500

    style: Rough draft of email report modal (#15666)
    
    * clears errors when closing out of modal (#15623)
    
    * add test + fix types
    
    * fix lint errors
    
    * Building ReportModal component
    
    * Continued ReportModal creation
    
    * Visual details updated
    
    * CronError style
    
    * Very basic testing added
    
    Co-authored-by: AAfghahi <[email protected]>
---
 .../ReportModal/EmailReportModal.test.jsx          |  58 ++++++
 .../src/components/ReportModal/index.tsx           | 220 +++++++++++++++++++++
 .../src/dashboard/components/Header/index.jsx      |  41 ++++
 .../src/dashboard/containers/DashboardHeader.jsx   |   1 +
 4 files changed, 320 insertions(+)

diff --git 
a/superset-frontend/src/components/ReportModal/EmailReportModal.test.jsx 
b/superset-frontend/src/components/ReportModal/EmailReportModal.test.jsx
new file mode 100644
index 0000000..a003e97
--- /dev/null
+++ b/superset-frontend/src/components/ReportModal/EmailReportModal.test.jsx
@@ -0,0 +1,58 @@
+/**
+ * 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.
+ */
+import React from 'react';
+import userEvent from '@testing-library/user-event';
+import {
+  render,
+  screen,
+  // within,
+  // cleanup,
+  // act,
+} from 'spec/helpers/testing-library';
+import ReportModal from '.';
+
+describe('Email Report Modal', () => {
+  it('inputs respond correctly', () => {
+    render(<ReportModal show />);
+
+    // ----- Report name textbox
+    // Initial value
+    const reportNameTextbox = screen.getByTestId('report-name-test');
+    expect(reportNameTextbox).toHaveDisplayValue('Weekly Report');
+    // Type in the textbox and assert that it worked
+    userEvent.type(reportNameTextbox, 'Report name text test');
+    expect(reportNameTextbox).toHaveDisplayValue('Report name text test');
+
+    // ----- Report description textbox
+    // Initial value
+    const reportDescriptionTextbox = screen.getByTestId(
+      'report-description-test',
+    );
+    expect(reportDescriptionTextbox).toHaveDisplayValue('');
+    // Type in the textbox and assert that it worked
+    userEvent.type(reportDescriptionTextbox, 'Report description text test');
+    expect(reportDescriptionTextbox).toHaveDisplayValue(
+      'Report description text test',
+    );
+
+    // ----- Crontab
+    const crontabInputs = screen.getAllByRole('combobox');
+    expect(crontabInputs).toHaveLength(4);
+  });
+});
diff --git a/superset-frontend/src/components/ReportModal/index.tsx 
b/superset-frontend/src/components/ReportModal/index.tsx
new file mode 100644
index 0000000..8fbc50f
--- /dev/null
+++ b/superset-frontend/src/components/ReportModal/index.tsx
@@ -0,0 +1,220 @@
+/**
+ * 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.
+ */
+import React, {
+  useState,
+  useCallback,
+  useReducer,
+  Reducer,
+  FunctionComponent,
+} from 'react';
+import { styled, css, t } from '@superset-ui/core';
+
+import LabeledErrorBoundInput from 
'src/components/Form/LabeledErrorBoundInput';
+import Icons from 'src/components/Icons';
+import Modal from 'src/components/Modal';
+import { CronPicker, CronError } from 'src/components/CronPicker';
+
+interface ReportProps {
+  onHide: () => {};
+  show: boolean;
+  props: any;
+}
+
+interface ReportObject {
+  active: boolean;
+  crontab: string;
+  dashboard: number;
+  description?: string;
+  log_retention: number;
+  name: string;
+  owners: number[];
+  recipients: [{ recipient_config_json: { target: string }; type: string }];
+  report_format: string;
+  type: string;
+  validator_config_json: {} | null;
+  validator_type: string;
+  working_timeout: number;
+}
+
+enum ActionType {
+  textChange,
+  inputChange,
+  fetched,
+}
+
+interface ReportPayloadType {
+  name: string;
+  description: string;
+  crontab: string;
+  value: string;
+}
+
+type ReportActionType =
+  | {
+      type: ActionType.textChange | ActionType.inputChange;
+      payload: ReportPayloadType;
+    }
+  | {
+      type: ActionType.fetched;
+      payload: Partial<ReportObject>;
+    };
+
+const reportReducer = (
+  state: Partial<ReportObject> | null,
+  action: ReportActionType,
+): Partial<ReportObject> | null => {
+  const trimmedState = {
+    ...(state || {}),
+  };
+
+  switch (action.type) {
+    case ActionType.textChange:
+      return {
+        ...trimmedState,
+        [action.payload.name]: action.payload.value,
+      };
+    default:
+      return state;
+  }
+};
+
+const StyledModal = styled(Modal)`
+  .ant-modal-body {
+    padding: 0;
+  }
+`;
+
+const StyledTopSection = styled.div`
+  padding: ${({ theme }) => theme.gridUnit * 4}px;
+`;
+
+const StyledBottomSection = styled.div`
+  border-top: 1px solid ${({ theme }) => theme.colors.grayscale.light2};
+  padding: ${({ theme }) => theme.gridUnit * 4}px;
+`;
+
+const StyledIconWrapper = styled.span`
+  span {
+    margin-right: ${({ theme }) => theme.gridUnit * 2}px;
+    vertical-align: middle;
+  }
+  .text {
+    vertical-align: middle;
+  }
+`;
+
+const StyledScheduleTitle = styled.div`
+  margin-bottom: ${({ theme }) => theme.gridUnit * 7}px;
+`;
+
+const StyledCronError = styled.p`
+  color: ${({ theme }) => theme.colors.error.base};
+`;
+
+const noBottomMargin = css`
+  margin-bottom: 0;
+`;
+
+const ReportModal: FunctionComponent<ReportProps> = ({
+  show = false,
+  onHide,
+  props,
+}) => {
+  const [currentReport, setCurrentReport] = useReducer<
+    Reducer<Partial<ReportObject> | null, ReportActionType>
+  >(reportReducer, null);
+  const onChange = useCallback((type: any, payload: any) => {
+    setCurrentReport({ type, payload } as ReportActionType);
+  }, []);
+  const [error, setError] = useState<CronError>();
+
+  const wrappedTitle = (
+    <StyledIconWrapper>
+      <Icons.Calendar />
+      <span className="text">{t('New Email Report')}</span>
+    </StyledIconWrapper>
+  );
+
+  return (
+    <StyledModal show={show} onHide={onHide} title={wrappedTitle}>
+      <StyledTopSection>
+        <LabeledErrorBoundInput
+          id="name"
+          name="name"
+          value={currentReport?.name || 'Weekly Report'}
+          required
+          validationMethods={{
+            onChange: ({ target }: { target: HTMLInputElement }) =>
+              onChange(ActionType.textChange, {
+                name: target.name,
+                value: target.value,
+              }),
+          }}
+          errorMessage={
+            currentReport?.name === 'error' ? t('REPORT NAME ERROR') : ''
+          }
+          label="Report Name"
+          data-test="report-name-test"
+        />
+
+        <LabeledErrorBoundInput
+          id="description"
+          name="description"
+          value={currentReport?.description || ''}
+          validationMethods={{
+            onChange: ({ target }: { target: HTMLInputElement }) =>
+              onChange(ActionType.textChange, {
+                name: target.name,
+                value: target.value,
+              }),
+          }}
+          errorMessage={
+            currentReport?.description === 'error' ? t('DESCRIPTION ERROR') : 
''
+          }
+          label="Description"
+          placeholder="Include a description that will be sent with your 
report"
+          css={noBottomMargin}
+          data-test="report-description-test"
+        />
+      </StyledTopSection>
+
+      <StyledBottomSection>
+        <StyledScheduleTitle>
+          <h1>Schedule</h1>
+          <p>Scheduled reports will be sent to your email as a PNG</p>
+        </StyledScheduleTitle>
+
+        <CronPicker
+          clearButton={false}
+          value={currentReport?.crontab || '0 12 * * 1'}
+          setValue={(newValue: string) => {
+            onChange(ActionType.textChange, {
+              name: 'crontab',
+              value: newValue,
+            });
+          }}
+          onError={setError}
+        />
+        <StyledCronError>{error}</StyledCronError>
+      </StyledBottomSection>
+    </StyledModal>
+  );
+};
+
+export default ReportModal;
diff --git a/superset-frontend/src/dashboard/components/Header/index.jsx 
b/superset-frontend/src/dashboard/components/Header/index.jsx
index b459577..9acff98 100644
--- a/superset-frontend/src/dashboard/components/Header/index.jsx
+++ b/superset-frontend/src/dashboard/components/Header/index.jsx
@@ -38,6 +38,7 @@ import HeaderActionsDropdown from 
'src/dashboard/components/Header/HeaderActions
 import PublishedStatus from 'src/dashboard/components/PublishedStatus';
 import UndoRedoKeyListeners from 
'src/dashboard/components/UndoRedoKeyListeners';
 import PropertiesModal from 'src/dashboard/components/PropertiesModal';
+import ReportModal from 'src/components/ReportModal';
 import { chartPropShape } from 'src/dashboard/util/propShapes';
 import {
   UNDO_LIMIT,
@@ -52,6 +53,7 @@ const propTypes = {
   addDangerToast: PropTypes.func.isRequired,
   addWarningToast: PropTypes.func.isRequired,
   userId: PropTypes.number,
+  userEmail: PropTypes.string,
   dashboardInfo: PropTypes.object.isRequired,
   dashboardTitle: PropTypes.string.isRequired,
   dataMask: PropTypes.object.isRequired,
@@ -123,6 +125,7 @@ const StyledDashboardHeader = styled.div`
     flex-wrap: nowrap;
     .action-button {
       font-size: ${({ theme }) => theme.typography.sizes.xl}px;
+      margin-left: ${({ theme }) => theme.gridUnit * 2.5}px;
     }
   }
 `;
@@ -138,6 +141,7 @@ class Header extends React.PureComponent {
       didNotifyMaxUndoHistoryToast: false,
       emphasizeUndo: false,
       showingPropertiesModal: false,
+      showingReportModal: false,
     };
 
     this.handleChangeText = this.handleChangeText.bind(this);
@@ -149,6 +153,8 @@ class Header extends React.PureComponent {
     this.overwriteDashboard = this.overwriteDashboard.bind(this);
     this.showPropertiesModal = this.showPropertiesModal.bind(this);
     this.hidePropertiesModal = this.hidePropertiesModal.bind(this);
+    this.showReportModal = this.showReportModal.bind(this);
+    this.hideReportModal = this.hideReportModal.bind(this);
   }
 
   componentDidMount() {
@@ -351,6 +357,14 @@ class Header extends React.PureComponent {
     this.setState({ showingPropertiesModal: false });
   }
 
+  showReportModal() {
+    this.setState({ showingReportModal: true });
+  }
+
+  hideReportModal() {
+    this.setState({ showingReportModal: false });
+  }
+
   render() {
     const {
       dashboardTitle,
@@ -371,6 +385,7 @@ class Header extends React.PureComponent {
       editMode,
       isPublished,
       userId,
+      userEmail,
       dashboardInfo,
       hasUnsavedChanges,
       isLoading,
@@ -499,6 +514,20 @@ class Header extends React.PureComponent {
             </>
           )}
 
+          {!editMode && (
+            <>
+              <span
+                role="button"
+                title={t('Schedule email report')}
+                tabIndex={0}
+                className="action-button"
+                onClick={this.showReportModal}
+              >
+                <Icon name="calendar" />
+              </span>
+            </>
+          )}
+
           {this.state.showingPropertiesModal && (
             <PropertiesModal
               dashboardId={dashboardInfo.id}
@@ -527,6 +556,18 @@ class Header extends React.PureComponent {
             />
           )}
 
+          {this.state.showingReportModal && (
+            <ReportModal
+              show={this.state.showingReportModal}
+              onHide={this.hideReportModal}
+              props={{
+                userId,
+                userEmail,
+                dashboardId: dashboardInfo.id,
+              }}
+            />
+          )}
+
           <HeaderActionsDropdown
             addSuccessToast={this.props.addSuccessToast}
             addDangerToast={this.props.addDangerToast}
diff --git a/superset-frontend/src/dashboard/containers/DashboardHeader.jsx 
b/superset-frontend/src/dashboard/containers/DashboardHeader.jsx
index f54f0f0..b099494 100644
--- a/superset-frontend/src/dashboard/containers/DashboardHeader.jsx
+++ b/superset-frontend/src/dashboard/containers/DashboardHeader.jsx
@@ -82,6 +82,7 @@ function mapStateToProps({
     charts,
     dataMask,
     userId: user.userId,
+    userEmail: user.email,
     isStarred: !!dashboardState.isStarred,
     isPublished: !!dashboardState.isPublished,
     isLoading: isDashboardLoading(charts),

Reply via email to