mirror of
https://github.com/apache/superset.git
synced 2026-04-18 15:44:57 +00:00
383 lines
10 KiB
TypeScript
383 lines
10 KiB
TypeScript
/**
|
|
* 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,
|
|
useEffect,
|
|
useCallback,
|
|
useReducer,
|
|
Reducer,
|
|
FunctionComponent,
|
|
} from 'react';
|
|
import { t, SupersetTheme } from '@superset-ui/core';
|
|
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 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 { 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 {
|
|
id?: number;
|
|
active: boolean;
|
|
crontab: string;
|
|
dashboard?: number;
|
|
chart?: number;
|
|
description?: string;
|
|
log_retention: number;
|
|
name: string;
|
|
owners: number[];
|
|
recipients: [{ recipient_config_json: { target: string }; type: string }];
|
|
report_format: string;
|
|
timezone: string;
|
|
type: string;
|
|
validator_config_json: {} | null;
|
|
validator_type: string;
|
|
working_timeout: number;
|
|
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;
|
|
addReport: (report?: ReportObject) => {};
|
|
onHide: () => {};
|
|
onReportAdd: (report?: ReportObject) => {};
|
|
show: boolean;
|
|
userId: number;
|
|
userEmail: string;
|
|
dashboardId?: number;
|
|
chart?: ChartObject;
|
|
creationMethod: string;
|
|
props: any;
|
|
}
|
|
|
|
interface ReportPayloadType {
|
|
name: string;
|
|
value: string;
|
|
}
|
|
|
|
enum ActionType {
|
|
inputChange,
|
|
fetched,
|
|
reset,
|
|
}
|
|
|
|
type ReportActionType =
|
|
| {
|
|
type: ActionType.inputChange;
|
|
payload: ReportPayloadType;
|
|
}
|
|
| {
|
|
type: ActionType.fetched;
|
|
payload: Partial<ReportObject>;
|
|
}
|
|
| {
|
|
type: ActionType.reset;
|
|
};
|
|
|
|
const DEFAULT_NOTIFICATION_FORMAT = 'TEXT';
|
|
const TEXT_BASED_VISUALIZATION_TYPES: string[] = [];
|
|
|
|
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.inputChange:
|
|
return {
|
|
...initialState,
|
|
[action.payload.name]: action.payload.value,
|
|
};
|
|
case ActionType.fetched:
|
|
return {
|
|
...action.payload,
|
|
};
|
|
case ActionType.reset:
|
|
return null;
|
|
default:
|
|
return state;
|
|
}
|
|
};
|
|
|
|
const ReportModal: FunctionComponent<ReportProps> = ({
|
|
onReportAdd,
|
|
onHide,
|
|
show = false,
|
|
...props
|
|
}) => {
|
|
const vizType = props.props.chart?.sliceFormData?.viz_type;
|
|
const [currentReport, setCurrentReport] = useReducer<
|
|
Reducer<Partial<ReportObject> | null, ReportActionType>
|
|
>(reportReducer, null);
|
|
const onChange = useCallback((type: any, payload: any) => {
|
|
setCurrentReport({ type, payload });
|
|
}, []);
|
|
const [error, setError] = useState<CronError>();
|
|
// const [isLoading, setLoading] = useState<boolean>(false);
|
|
const dispatch = useDispatch();
|
|
// Report fetch logic
|
|
const reports = useSelector<any, AlertObject>(state => state.reports);
|
|
const isEditMode = reports && Object.keys(reports).length;
|
|
useEffect(() => {
|
|
if (isEditMode) {
|
|
const reportsIds = Object.keys(reports);
|
|
const report = reports[reportsIds[0]];
|
|
setCurrentReport({
|
|
type: ActionType.fetched,
|
|
payload: report,
|
|
});
|
|
} else {
|
|
setCurrentReport({
|
|
type: ActionType.reset,
|
|
});
|
|
}
|
|
}, [reports]);
|
|
const onClose = () => {
|
|
onHide();
|
|
};
|
|
const onSave = async () => {
|
|
// Create new Report
|
|
const newReportValues: Partial<ReportObject> = {
|
|
crontab: currentReport?.crontab,
|
|
dashboard: props.props.dashboardId,
|
|
chart: props.props.chart?.id,
|
|
description: currentReport?.description,
|
|
name: currentReport?.name,
|
|
owners: [props.props.userId],
|
|
recipients: [
|
|
{
|
|
recipient_config_json: { target: props.props.userEmail },
|
|
type: 'Email',
|
|
},
|
|
],
|
|
type: 'Report',
|
|
creation_method: props.props.creationMethod,
|
|
active: true,
|
|
report_format: currentReport?.report_format,
|
|
timezone: currentReport?.timezone,
|
|
};
|
|
|
|
if (isEditMode) {
|
|
await dispatch(
|
|
editReport(currentReport?.id, newReportValues as ReportObject),
|
|
);
|
|
} else {
|
|
await dispatch(addReport(newReportValues as ReportObject));
|
|
}
|
|
|
|
if (onReportAdd) {
|
|
onReportAdd();
|
|
}
|
|
|
|
onClose();
|
|
};
|
|
|
|
const wrappedTitle = (
|
|
<StyledIconWrapper>
|
|
<Icons.Calendar />
|
|
<span className="text">
|
|
{isEditMode ? t('Edit Email Report') : t('New Email Report')}
|
|
</span>
|
|
</StyledIconWrapper>
|
|
);
|
|
|
|
const renderModalFooter = (
|
|
<>
|
|
<StyledFooterButton key="back" onClick={onClose}>
|
|
{t('Cancel')}
|
|
</StyledFooterButton>
|
|
<StyledFooterButton
|
|
key="submit"
|
|
buttonStyle="primary"
|
|
onClick={onSave}
|
|
disabled={!currentReport?.name}
|
|
>
|
|
{isEditMode ? t('Save') : t('Add')}
|
|
</StyledFooterButton>
|
|
</>
|
|
);
|
|
|
|
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}
|
|
onHide={onClose}
|
|
title={wrappedTitle}
|
|
footer={renderModalFooter}
|
|
width="432"
|
|
centered
|
|
>
|
|
<StyledTopSection>
|
|
<LabeledErrorBoundInput
|
|
id="name"
|
|
name="name"
|
|
value={currentReport?.name || ''}
|
|
placeholder="Weekly Report"
|
|
required
|
|
validationMethods={{
|
|
onChange: ({ target }: { target: HTMLInputElement }) =>
|
|
onChange(ActionType.inputChange, {
|
|
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.inputChange, {
|
|
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>
|
|
<h4 css={(theme: SupersetTheme) => SectionHeaderStyle(theme)}>
|
|
{t('Schedule')}
|
|
</h4>
|
|
<p>{t('Scheduled reports will be sent to your email as a PNG')}</p>
|
|
</StyledScheduleTitle>
|
|
|
|
<StyledCronPicker
|
|
clearButton={false}
|
|
value={currentReport?.crontab || '0 12 * * 1'}
|
|
setValue={(newValue: string) => {
|
|
onChange(ActionType.inputChange, {
|
|
name: 'crontab',
|
|
value: newValue,
|
|
});
|
|
}}
|
|
onError={setError}
|
|
/>
|
|
<StyledCronError>{error}</StyledCronError>
|
|
<div
|
|
className="control-label"
|
|
css={(theme: SupersetTheme) => TimezoneHeaderStyle(theme)}
|
|
>
|
|
{t('Timezone')}
|
|
</div>
|
|
<TimezoneSelector
|
|
onTimezoneChange={value => {
|
|
setCurrentReport({
|
|
type: ActionType.inputChange,
|
|
payload: { name: 'timezone', value },
|
|
});
|
|
}}
|
|
timezone={currentReport?.timezone}
|
|
/>
|
|
{props.props.chart && renderMessageContentSection}
|
|
</StyledBottomSection>
|
|
</StyledModal>
|
|
);
|
|
};
|
|
|
|
const mapDispatchToProps = (dispatch: any) =>
|
|
bindActionCreators({ addReport, editReport }, dispatch);
|
|
|
|
export default connect(null, mapDispatchToProps)(withToasts(ReportModal));
|