mirror of
https://github.com/apache/superset.git
synced 2024-09-06 22:07:34 -04:00
feat: add chart image info to reports from charts (#16158)
* refetch reports on props update * add chart types to reports
This commit is contained in:
parent
5e64d65a8b
commit
a3102488a1
@ -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', () => {
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
enum ActionType {
|
||||
textChange,
|
||||
inputChange,
|
||||
fetched,
|
||||
reset,
|
||||
}
|
||||
|
||||
interface ReportPayloadType {
|
||||
name: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
enum ActionType {
|
||||
inputChange,
|
||||
fetched,
|
||||
reset,
|
||||
}
|
||||
|
||||
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>
|
||||
);
|
||||
|
@ -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;
|
||||
`;
|
||||
|
@ -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'],
|
||||
|
@ -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() {
|
||||
|
@ -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',
|
||||
}}
|
||||
/>
|
||||
|
Loading…
Reference in New Issue
Block a user