-
Notifications
You must be signed in to change notification settings - Fork 14.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix: Keep Report modal open when there's an error #17988
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -25,6 +25,7 @@ import React, { | |
FunctionComponent, | ||
} from 'react'; | ||
import { t, SupersetTheme } from '@superset-ui/core'; | ||
import { getClientErrorObject } from 'src/utils/getClientErrorObject'; | ||
import { bindActionCreators } from 'redux'; | ||
import { connect, useDispatch, useSelector } from 'react-redux'; | ||
import { addReport, editReport } from 'src/reports/actions/reports'; | ||
|
@@ -72,6 +73,7 @@ export interface ReportObject { | |
working_timeout: number; | ||
creation_method: string; | ||
force_screenshot: boolean; | ||
error?: string; | ||
} | ||
|
||
interface ChartObject { | ||
|
@@ -88,8 +90,6 @@ interface ChartObject { | |
} | ||
|
||
interface ReportProps { | ||
addDangerToast: (msg: string) => void; | ||
addSuccessToast: (msg: string) => void; | ||
addReport: (report?: ReportObject) => {}; | ||
onHide: () => {}; | ||
onReportAdd: (report?: ReportObject) => {}; | ||
|
@@ -111,6 +111,7 @@ enum ActionType { | |
inputChange, | ||
fetched, | ||
reset, | ||
error, | ||
} | ||
|
||
type ReportActionType = | ||
|
@@ -124,6 +125,12 @@ type ReportActionType = | |
} | ||
| { | ||
type: ActionType.reset; | ||
} | ||
| { | ||
type: ActionType.error; | ||
payload: { | ||
name: string[]; | ||
}; | ||
}; | ||
|
||
const TEXT_BASED_VISUALIZATION_TYPES = [ | ||
|
@@ -161,6 +168,11 @@ const reportReducer = ( | |
}; | ||
case ActionType.reset: | ||
return { ...initialState }; | ||
case ActionType.error: | ||
return { | ||
...state, | ||
error: action.payload.name[0], | ||
}; | ||
default: | ||
return state; | ||
} | ||
|
@@ -181,11 +193,10 @@ const ReportModal: FunctionComponent<ReportProps> = ({ | |
const [currentReport, setCurrentReport] = useReducer< | ||
Reducer<Partial<ReportObject> | null, ReportActionType> | ||
>(reportReducer, null); | ||
const onChange = useCallback((type: any, payload: any) => { | ||
const onReducerChange = useCallback((type: any, payload: any) => { | ||
setCurrentReport({ type, payload }); | ||
}, []); | ||
const [error, setError] = useState<CronError>(); | ||
// const [isLoading, setLoading] = useState<boolean>(false); | ||
const [cronError, setCronError] = useState<CronError>(); | ||
const dispatch = useDispatch(); | ||
// Report fetch logic | ||
const reports = useSelector<any, AlertObject>(state => state.reports); | ||
|
@@ -205,9 +216,7 @@ const ReportModal: FunctionComponent<ReportProps> = ({ | |
}); | ||
} | ||
}, [reports]); | ||
const onClose = () => { | ||
onHide(); | ||
}; | ||
|
||
const onSave = async () => { | ||
// Create new Report | ||
const newReportValues: Partial<ReportObject> = { | ||
|
@@ -235,15 +244,19 @@ const ReportModal: FunctionComponent<ReportProps> = ({ | |
await dispatch( | ||
editReport(currentReport?.id, newReportValues as ReportObject), | ||
); | ||
onHide(); | ||
} else { | ||
await dispatch(addReport(newReportValues as ReportObject)); | ||
try { | ||
await dispatch(addReport(newReportValues as ReportObject)); | ||
onHide(); | ||
} catch (e) { | ||
const parsedError = await getClientErrorObject(e); | ||
const errorMessage = parsedError.message; | ||
onReducerChange(ActionType.error, errorMessage); | ||
} | ||
} | ||
|
||
if (onReportAdd) { | ||
onReportAdd(); | ||
} | ||
|
||
onClose(); | ||
if (onReportAdd) onReportAdd(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this the format preferred by prettier these days? I learned that braceless if clauses should be avoided because they can lead to subtle errors. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's not preferred by prettier, I thought it was just a prettier way to do it, haha. I didn't realize there were subtle errors possible with this though, thanks for this catch! I'll change it back. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think a single line is OK, since it's simple enough. The use that is discouraged is: if (onReportAdd)
onReportAdd(); Which I think I think it's fine to leave the way you did, I was just curious. :) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh okay awesome, I'll keep an eye out for that behavior in the future just in case! Thank you! 😁 |
||
}; | ||
|
||
const wrappedTitle = ( | ||
|
@@ -257,7 +270,7 @@ const ReportModal: FunctionComponent<ReportProps> = ({ | |
|
||
const renderModalFooter = ( | ||
<> | ||
<StyledFooterButton key="back" onClick={onClose}> | ||
<StyledFooterButton key="back" onClick={onHide}> | ||
{t('Cancel')} | ||
</StyledFooterButton> | ||
<StyledFooterButton | ||
|
@@ -279,7 +292,7 @@ const ReportModal: FunctionComponent<ReportProps> = ({ | |
<div className="inline-container"> | ||
<StyledRadioGroup | ||
onChange={(event: RadioChangeEvent) => { | ||
onChange(ActionType.inputChange, { | ||
onReducerChange(ActionType.inputChange, { | ||
name: 'report_format', | ||
value: event.target.value, | ||
}); | ||
|
@@ -305,7 +318,7 @@ const ReportModal: FunctionComponent<ReportProps> = ({ | |
return ( | ||
<StyledModal | ||
show={show} | ||
onHide={onClose} | ||
onHide={onHide} | ||
title={wrappedTitle} | ||
footer={renderModalFooter} | ||
width="432" | ||
|
@@ -316,18 +329,16 @@ const ReportModal: FunctionComponent<ReportProps> = ({ | |
id="name" | ||
name="name" | ||
value={currentReport?.name || ''} | ||
placeholder="Weekly Report" | ||
placeholder={t('Weekly Report')} | ||
required | ||
validationMethods={{ | ||
onChange: ({ target }: { target: HTMLInputElement }) => | ||
onChange(ActionType.inputChange, { | ||
onReducerChange(ActionType.inputChange, { | ||
name: target.name, | ||
value: target.value, | ||
}), | ||
}} | ||
errorMessage={ | ||
currentReport?.name === 'error' ? t('REPORT NAME ERROR') : '' | ||
} | ||
errorMessage={currentReport?.error || ''} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. how/when does this error get cleared out? Does it stay visible until they hit submit again? I'm not sure if we discussed how this should look @yousoph? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Currently the error stays until the user submits the form again. Once they hit submit it will rerun, if there's still an error it will stay open with that error. If the error is corrected it will close. I think it would be nice to have the error cleared as soon as the user fixes it, but I think that would require front end validation vs. just back end. |
||
label="Report Name" | ||
data-test="report-name-test" | ||
/> | ||
|
@@ -338,16 +349,16 @@ const ReportModal: FunctionComponent<ReportProps> = ({ | |
value={currentReport?.description || ''} | ||
validationMethods={{ | ||
onChange: ({ target }: { target: HTMLInputElement }) => | ||
onChange(ActionType.inputChange, { | ||
onReducerChange(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" | ||
errorMessage="" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. where do you display the error message? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I spoke with Sophie separately and she said that this field doesn't currently have any restrictions so it won't have any errors applicable to its field. I left this blank since |
||
label={t('Description')} | ||
placeholder={t( | ||
'Include a description that will be sent with your report', | ||
)} | ||
css={noBottomMargin} | ||
data-test="report-description-test" | ||
/> | ||
|
@@ -363,16 +374,16 @@ const ReportModal: FunctionComponent<ReportProps> = ({ | |
|
||
<StyledCronPicker | ||
clearButton={false} | ||
value={currentReport?.crontab || '0 12 * * 1'} | ||
value={t(currentReport?.crontab || '0 12 * * 1')} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think a translation is necessary here. |
||
setValue={(newValue: string) => { | ||
onChange(ActionType.inputChange, { | ||
onReducerChange(ActionType.inputChange, { | ||
name: 'crontab', | ||
value: newValue, | ||
}); | ||
}} | ||
onError={setError} | ||
onError={setCronError} | ||
/> | ||
<StyledCronError>{error}</StyledCronError> | ||
<StyledCronError>{cronError}</StyledCronError> | ||
<div | ||
className="control-label" | ||
css={(theme: SupersetTheme) => TimezoneHeaderStyle(theme)} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -19,7 +19,6 @@ | |
/* eslint camelcase: 0 */ | ||
import { t, SupersetClient } from '@superset-ui/core'; | ||
import rison from 'rison'; | ||
import { getClientErrorObject } from 'src/utils/getClientErrorObject'; | ||
import { | ||
addDangerToast, | ||
addSuccessToast, | ||
|
@@ -105,22 +104,10 @@ export const addReport = report => dispatch => | |
SupersetClient.post({ | ||
endpoint: `/api/v1/report/`, | ||
jsonPayload: report, | ||
}) | ||
.then(({ json }) => { | ||
dispatch({ type: ADD_REPORT, json }); | ||
dispatch(addSuccessToast(t('The report has been created'))); | ||
}) | ||
.catch(async e => { | ||
const parsedError = await getClientErrorObject(e); | ||
const errorMessage = parsedError.message; | ||
const errorArr = Object.keys(errorMessage); | ||
const error = errorMessage[errorArr[0]]; | ||
dispatch( | ||
addDangerToast( | ||
t('An error occurred while editing this report: %s', error), | ||
), | ||
); | ||
}); | ||
}).then(({ json }) => { | ||
dispatch({ type: ADD_REPORT, json }); | ||
dispatch(addSuccessToast(t('The report has been created'))); | ||
}); | ||
Comment on lines
+107
to
+110
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What happens when the endpoint returns an error? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I moved the catch functionality into the report modal here, this should handle any errors for this endpoint. Should I also put a catch in the action? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah, makes sense, because we're no longer showing the error in the toaster, but in the modal. Thanks for clarifying! :) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No problem! 😁 |
||
|
||
export const EDIT_REPORT = 'EDIT_REPORT'; | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
curious why the switch from onChange to onReducerChange. If I'm reading this right, when the reducer changes, you update the reducer?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In cases like this one, there was originally an
onChange
nested into anotheronChange
. I thought that since there were twoonChange
functions in the component it could get confusing, so I renamed the reducer'sonChange
toonReducerChange
to clarify thatonReducerChange
is specifically the function that the reducer uses to change state. Is that an unnecessary clarification, or is there maybe a better name I can use here?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, I gotcha. Normally the naming convention will be the element that is changing, rather than the function that is used to do the change, thus the confusion. Maybe something like
onInputChange
would make sense.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good call,
onInputChange
sounds a lot better! I'll change the naming once I reopen this to fix it.