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)

* 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 <48933336+AAfghahi@users.noreply.github.com>
Co-authored-by: aspedrosa <aspedrosa@ua.pt>
  • Loading branch information
3 people committed Jul 16, 2021
1 parent 4d1672f commit d31357f
Show file tree
Hide file tree
Showing 8 changed files with 348 additions and 5 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;
Expand Up @@ -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: {},
Expand Down Expand Up @@ -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));
Expand Down
Expand Up @@ -32,7 +32,7 @@ const createProps = () => ({
id: 1,
dash_edit_perm: true,
dash_save_perm: true,
userId: 1,
userId: '1',
metadata: {},
common: {
conf: {},
Expand Down

0 comments on commit d31357f

Please sign in to comment.