Skip to content

Commit

Permalink
style: Rough draft of email report modal (#15666)
Browse files Browse the repository at this point in the history
* 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 <48933336+AAfghahi@users.noreply.github.com>
  • Loading branch information
2 people authored and eschutho committed Jul 26, 2021
1 parent b81f120 commit 1be8119
Show file tree
Hide file tree
Showing 4 changed files with 320 additions and 0 deletions.
@@ -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);
});
});
220 changes: 220 additions & 0 deletions 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;
41 changes: 41 additions & 0 deletions superset-frontend/src/dashboard/components/Header/index.jsx
Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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;
}
}
`;
Expand All @@ -141,6 +144,7 @@ class Header extends React.PureComponent {
didNotifyMaxUndoHistoryToast: false,
emphasizeUndo: false,
showingPropertiesModal: false,
showingReportModal: false,
};

this.handleChangeText = this.handleChangeText.bind(this);
Expand All @@ -152,6 +156,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() {
Expand Down Expand Up @@ -354,6 +360,14 @@ class Header extends React.PureComponent {
this.setState({ showingPropertiesModal: false });
}

showReportModal() {
this.setState({ showingReportModal: true });
}

hideReportModal() {
this.setState({ showingReportModal: false });
}

render() {
const {
dashboardTitle,
Expand All @@ -374,6 +388,7 @@ class Header extends React.PureComponent {
editMode,
isPublished,
userId,
userEmail,
dashboardInfo,
hasUnsavedChanges,
isLoading,
Expand Down Expand Up @@ -502,6 +517,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}
Expand Down Expand Up @@ -530,6 +559,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}
Expand Down
Expand Up @@ -82,6 +82,7 @@ function mapStateToProps({
charts,
dataMask,
userId: user.userId,
userEmail: user.email,
isStarred: !!dashboardState.isStarred,
isPublished: !!dashboardState.isPublished,
isLoading: isDashboardLoading(charts),
Expand Down

0 comments on commit 1be8119

Please sign in to comment.