You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by el...@apache.org on 2021/07/16 16:17:00 UTC
[superset] branch selfSubscribeReports updated: style: Rough draft
of email report modal (#15666)
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
The following commit(s) were added to refs/heads/selfSubscribeReports by this push:
new d31357f style: Rough draft of email report modal (#15666)
d31357f is described below
commit d31357fd239ca115cb078ad59316aa3346cc7dfa
Author: Lyndsi Kay Williams <55...@users.noreply.github.com>
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)
* fix: avoid fetching favorite status for anonymous user (#15590)
* avoid fetching favorite status for anonymous user
* 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 <48...@users.noreply.github.com>
Co-authored-by: aspedrosa <as...@ua.pt>
---
.../ReportModal/EmailReportModal.test.jsx | 58 ++++++
.../src/components/ReportModal/index.tsx | 220 +++++++++++++++++++++
.../dashboard/components/Header/Header.test.tsx | 19 +-
.../HeaderActionsDropdown.test.tsx | 2 +-
.../src/dashboard/components/Header/index.jsx | 45 ++++-
.../src/dashboard/components/Header/types.ts | 3 +-
.../src/dashboard/containers/DashboardHeader.jsx | 4 +-
.../CRUD/data/database/DatabaseModal/index.tsx | 2 +
8 files changed, 348 insertions(+), 5 deletions(-)
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/Header.test.tsx b/superset-frontend/src/dashboard/components/Header/Header.test.tsx
index d5393b2..88d22b8 100644
--- a/superset-frontend/src/dashboard/components/Header/Header.test.tsx
+++ b/superset-frontend/src/dashboard/components/Header/Header.test.tsx
@@ -32,12 +32,13 @@ const createProps = () => ({
dash_edit_perm: false,
dash_save_perm: false,
dash_share_perm: false,
- userId: 1,
+ userId: '1',
metadata: {},
common: {
conf: {},
},
},
+ userId: 1,
dashboardTitle: 'Dashboard Title',
charts: {},
layout: {},
@@ -248,6 +249,22 @@ test('should render the selected fave icon', () => {
).toBeInTheDocument();
});
+test('should NOT render the fave icon on anonymous user', () => {
+ const mockedProps = createProps();
+ const anonymousUserProps = {
+ ...mockedProps,
+ userId: undefined,
+ };
+ render(setup(anonymousUserProps));
+ expect(mockedProps.fetchFaveStar).not.toHaveBeenCalled();
+ expect(() =>
+ screen.getByRole('img', { name: 'favorite-unselected' }),
+ ).toThrowError('Unable to find');
+ expect(() =>
+ screen.getByRole('img', { name: 'favorite-selected' }),
+ ).toThrowError('Unable to find');
+});
+
test('should fave', async () => {
const mockedProps = createProps();
render(setup(mockedProps));
diff --git a/superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/HeaderActionsDropdown.test.tsx b/superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/HeaderActionsDropdown.test.tsx
index 7885d1b..1b1cb30 100644
--- a/superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/HeaderActionsDropdown.test.tsx
+++ b/superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/HeaderActionsDropdown.test.tsx
@@ -32,7 +32,7 @@ const createProps = () => ({
id: 1,
dash_edit_perm: true,
dash_save_perm: true,
- userId: 1,
+ userId: '1',
metadata: {},
common: {
conf: {},
diff --git a/superset-frontend/src/dashboard/components/Header/index.jsx b/superset-frontend/src/dashboard/components/Header/index.jsx
index 0f730c7..0875ab9 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,
@@ -51,6 +52,8 @@ const propTypes = {
addSuccessToast: PropTypes.func.isRequired,
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,
@@ -119,6 +122,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;
}
}
`;
@@ -134,6 +138,7 @@ class Header extends React.PureComponent {
didNotifyMaxUndoHistoryToast: false,
emphasizeUndo: false,
showingPropertiesModal: false,
+ showingReportModal: false,
};
this.handleChangeText = this.handleChangeText.bind(this);
@@ -145,6 +150,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() {
@@ -347,6 +354,14 @@ class Header extends React.PureComponent {
this.setState({ showingPropertiesModal: false });
}
+ showReportModal() {
+ this.setState({ showingReportModal: true });
+ }
+
+ hideReportModal() {
+ this.setState({ showingReportModal: false });
+ }
+
render() {
const {
dashboardTitle,
@@ -366,6 +381,8 @@ class Header extends React.PureComponent {
updateCss,
editMode,
isPublished,
+ userId,
+ userEmail,
dashboardInfo,
hasUnsavedChanges,
isLoading,
@@ -404,7 +421,7 @@ class Header extends React.PureComponent {
canEdit={userCanEdit}
canSave={userCanSaveAs}
/>
- {dashboardInfo.userId && (
+ {userId && (
<FaveStar
itemId={dashboardInfo.id}
fetchFaveStar={this.props.fetchFaveStar}
@@ -494,6 +511,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}
@@ -522,6 +553,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/components/Header/types.ts b/superset-frontend/src/dashboard/components/Header/types.ts
index 5580136..3c8e8cf 100644
--- a/superset-frontend/src/dashboard/components/Header/types.ts
+++ b/superset-frontend/src/dashboard/components/Header/types.ts
@@ -22,7 +22,7 @@ import { ChartState } from 'src/explore/types';
interface DashboardInfo {
id: number;
- userId: number;
+ userId: string | undefined;
dash_edit_perm: boolean;
dash_save_perm: boolean;
metadata?: Record<string, any>;
@@ -65,6 +65,7 @@ export interface HeaderProps {
charts: ChartState | {};
colorScheme?: string;
customCss: string;
+ userId: number | undefined;
dashboardInfo: DashboardInfo;
dashboardTitle: string;
setColorSchemeAndUnsavedChanges: () => void;
diff --git a/superset-frontend/src/dashboard/containers/DashboardHeader.jsx b/superset-frontend/src/dashboard/containers/DashboardHeader.jsx
index 2322ca7..b099494 100644
--- a/superset-frontend/src/dashboard/containers/DashboardHeader.jsx
+++ b/superset-frontend/src/dashboard/containers/DashboardHeader.jsx
@@ -63,6 +63,7 @@ function mapStateToProps({
dashboardInfo,
charts,
dataMask,
+ user,
}) {
return {
dashboardInfo,
@@ -80,7 +81,8 @@ function mapStateToProps({
colorScheme: dashboardState.colorScheme,
charts,
dataMask,
- userId: dashboardInfo.userId,
+ userId: user.userId,
+ userEmail: user.email,
isStarred: !!dashboardState.isStarred,
isPublished: !!dashboardState.isPublished,
isLoading: isDashboardLoading(charts),
diff --git a/superset-frontend/src/views/CRUD/data/database/DatabaseModal/index.tsx b/superset-frontend/src/views/CRUD/data/database/DatabaseModal/index.tsx
index 662a178..eecdf73 100644
--- a/superset-frontend/src/views/CRUD/data/database/DatabaseModal/index.tsx
+++ b/superset-frontend/src/views/CRUD/data/database/DatabaseModal/index.tsx
@@ -355,6 +355,7 @@ const DatabaseModal: FunctionComponent<DatabaseModalProps> = ({
fetchResource,
createResource,
updateResource,
+ clearError,
} = useSingleViewResource<DatabaseObject>(
'database',
t('database'),
@@ -399,6 +400,7 @@ const DatabaseModal: FunctionComponent<DatabaseModalProps> = ({
setDB({ type: ActionType.reset });
setHasConnectedDb(false);
setValidationErrors(null); // reset validation errors on close
+ clearError();
setEditNewDb(false);
onHide();
};