fix: Explore misleading save action (#24862)

This commit is contained in:
Michael S. Molina 2023-08-02 15:46:03 -03:00 committed by GitHub
parent f7e76d02b7
commit bf1b1a4c46
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 141 additions and 127 deletions

View File

@ -49,11 +49,6 @@ export function saveSliceSuccess(data) {
return { type: SAVE_SLICE_SUCCESS, data }; return { type: SAVE_SLICE_SUCCESS, data };
} }
export const REMOVE_SAVE_MODAL_ALERT = 'REMOVE_SAVE_MODAL_ALERT';
export function removeSaveModalAlert() {
return { type: REMOVE_SAVE_MODAL_ALERT };
}
const extractAddHocFiltersFromFormData = formDataToHandle => const extractAddHocFiltersFromFormData = formDataToHandle =>
Object.entries(formDataToHandle).reduce( Object.entries(formDataToHandle).reduce(
(acc, [key, value]) => (acc, [key, value]) =>

View File

@ -24,7 +24,6 @@ import { bindActionCreators } from 'redux';
import { shallow } from 'enzyme'; import { shallow } from 'enzyme';
import { Radio } from 'src/components/Radio'; import { Radio } from 'src/components/Radio';
import Button from 'src/components/Button'; import Button from 'src/components/Button';
import sinon from 'sinon';
import fetchMock from 'fetch-mock'; import fetchMock from 'fetch-mock';
import * as saveModalActions from 'src/explore/actions/saveModalActions'; import * as saveModalActions from 'src/explore/actions/saveModalActions';
@ -131,7 +130,7 @@ test('renders a Modal with the right set of components', () => {
expect(footerWrapper.find(Button)).toHaveLength(3); expect(footerWrapper.find(Button)).toHaveLength(3);
}); });
test('renders the right footer buttons when existing dashboard selected', () => { test('renders the right footer buttons', () => {
const wrapper = getWrapper(); const wrapper = getWrapper();
const footerWrapper = shallow(wrapper.find(StyledModal).props().footer); const footerWrapper = shallow(wrapper.find(StyledModal).props().footer);
const saveAndGoDash = footerWrapper const saveAndGoDash = footerWrapper
@ -142,18 +141,43 @@ test('renders the right footer buttons when existing dashboard selected', () =>
expect(saveAndGoDash.props.children).toBe('Save & go to dashboard'); expect(saveAndGoDash.props.children).toBe('Save & go to dashboard');
}); });
test('renders the right footer buttons when new dashboard selected', () => { test('does not render a message when overriding', () => {
const wrapper = getWrapper();
wrapper.setState({
action: 'overwrite',
});
expect(
wrapper.find('[message="A new chart will be created."]'),
).not.toExist();
});
test('renders a message when saving as', () => {
const wrapper = getWrapper();
wrapper.setState({
action: 'saveas',
});
expect(wrapper.find('[message="A new chart will be created."]')).toExist();
});
test('renders a message when a new dashboard is selected', () => {
const wrapper = getWrapper(); const wrapper = getWrapper();
wrapper.setState({ wrapper.setState({
dashboard: { label: 'Test new dashboard', value: 'Test new dashboard' }, dashboard: { label: 'Test new dashboard', value: 'Test new dashboard' },
}); });
const footerWrapper = shallow(wrapper.find(StyledModal).props().footer); expect(
const saveAndGoDash = footerWrapper wrapper.find('[message="A new dashboard will be created."]'),
.find('#btn_modal_save_goto_dash') ).toExist();
.getElement(); });
const save = footerWrapper.find('#btn_modal_save').getElement();
expect(save.props.children).toBe('Save to new dashboard'); test('renders a message when saving as with new dashboard', () => {
expect(saveAndGoDash.props.children).toBe('Save & go to new dashboard'); const wrapper = getWrapper();
wrapper.setState({
action: 'saveas',
dashboard: { label: 'Test new dashboard', value: 'Test new dashboard' },
});
expect(
wrapper.find('[message="A new chart and dashboard will be created."]'),
).toExist();
}); });
test('disables overwrite option for new slice', () => { test('disables overwrite option for new slice', () => {
@ -197,17 +221,6 @@ test('updates slice name and selected dashboard', () => {
expect(wrapper.state().dashboard.value).toBe(dashboardId); expect(wrapper.state().dashboard.value).toBe(dashboardId);
}); });
test('removes alert', () => {
sinon.spy(defaultProps.actions, 'removeSaveModalAlert');
const wrapper = getWrapper();
wrapper.setProps({ alert: 'old alert' });
wrapper.instance().removeAlert();
expect(defaultProps.actions.removeSaveModalAlert.callCount).toBe(1);
expect(wrapper.state().alert).toBeNull();
defaultProps.actions.removeSaveModalAlert.restore();
});
test('set dataset name when chart source is query', () => { test('set dataset name when chart source is query', () => {
const wrapper = getWrapper(queryDefaultProps, queryStore); const wrapper = getWrapper(queryDefaultProps, queryStore);
expect(wrapper.find('[data-test="new-dataset-name"]')).toExist(); expect(wrapper.find('[data-test="new-dataset-name"]')).toExist();

View File

@ -67,7 +67,6 @@ interface SaveModalProps extends RouteComponentProps {
type SaveModalState = { type SaveModalState = {
newSliceName?: string; newSliceName?: string;
datasetName: string; datasetName: string;
alert: string | null;
action: SaveActionType; action: SaveActionType;
isLoading: boolean; isLoading: boolean;
saveStatus?: string | null; saveStatus?: string | null;
@ -92,7 +91,6 @@ class SaveModal extends React.Component<SaveModalProps, SaveModalState> {
this.state = { this.state = {
newSliceName: props.sliceName, newSliceName: props.sliceName,
datasetName: props.datasource?.name, datasetName: props.datasource?.name,
alert: null,
action: this.canOverwriteSlice() ? 'overwrite' : 'saveas', action: this.canOverwriteSlice() ? 'overwrite' : 'saveas',
isLoading: false, isLoading: false,
vizType: props.form_data?.viz_type, vizType: props.form_data?.viz_type,
@ -103,7 +101,6 @@ class SaveModal extends React.Component<SaveModalProps, SaveModalState> {
this.changeAction = this.changeAction.bind(this); this.changeAction = this.changeAction.bind(this);
this.saveOrOverwrite = this.saveOrOverwrite.bind(this); this.saveOrOverwrite = this.saveOrOverwrite.bind(this);
this.isNewDashboard = this.isNewDashboard.bind(this); this.isNewDashboard = this.isNewDashboard.bind(this);
this.removeAlert = this.removeAlert.bind(this);
this.onHide = this.onHide.bind(this); this.onHide = this.onHide.bind(this);
} }
@ -163,8 +160,7 @@ class SaveModal extends React.Component<SaveModalProps, SaveModalState> {
} }
async saveOrOverwrite(gotodash: boolean) { async saveOrOverwrite(gotodash: boolean) {
this.setState({ alert: null, isLoading: true }); this.setState({ isLoading: true });
this.props.actions.removeSaveModalAlert();
// Create or retrieve dashboard // Create or retrieve dashboard
type DashboardGetResponse = { type DashboardGetResponse = {
@ -324,89 +320,115 @@ class SaveModal extends React.Component<SaveModalProps, SaveModalState> {
}; };
}; };
renderSaveChartModal = () => ( renderSaveChartModal = () => {
<Form data-test="save-modal-body" layout="vertical"> const info = this.info();
{(this.state.alert || this.props.alert) && ( return (
<Alert <Form data-test="save-modal-body" layout="vertical">
type="warning" <FormItem data-test="radio-group">
message={this.state.alert || this.props.alert} <Radio
onClose={this.removeAlert} id="overwrite-radio"
/> disabled={!this.canOverwriteSlice()}
)} checked={this.state.action === 'overwrite'}
<FormItem data-test="radio-group"> onChange={() => this.changeAction('overwrite')}
<Radio data-test="save-overwrite-radio"
id="overwrite-radio" >
disabled={!this.canOverwriteSlice()} {t('Save (Overwrite)')}
checked={this.state.action === 'overwrite'} </Radio>
onChange={() => this.changeAction('overwrite')} <Radio
data-test="save-overwrite-radio" id="saveas-radio"
> data-test="saveas-radio"
{t('Save (Overwrite)')} checked={this.state.action === 'saveas'}
</Radio> onChange={() => this.changeAction('saveas')}
<Radio >
id="saveas-radio" {t('Save as...')}
data-test="saveas-radio" </Radio>
checked={this.state.action === 'saveas'} </FormItem>
onChange={() => this.changeAction('saveas')} <hr />
> <FormItem label={t('Chart name')} required>
{t('Save as...')}
</Radio>
</FormItem>
<hr />
<FormItem label={t('Chart name')} required>
<Input
name="new_slice_name"
type="text"
placeholder="Name"
value={this.state.newSliceName}
onChange={this.onSliceNameChange}
data-test="new-chart-name"
/>
</FormItem>
{this.props.datasource?.type === 'query' && (
<FormItem label={t('Dataset Name')} required>
<InfoTooltipWithTrigger
tooltip={t('A reusable dataset will be saved with your chart.')}
placement="right"
/>
<Input <Input
name="dataset_name" name="new_slice_name"
type="text" type="text"
placeholder="Dataset Name" placeholder="Name"
value={this.state.datasetName} value={this.state.newSliceName}
onChange={this.handleDatasetNameChange} onChange={this.onSliceNameChange}
data-test="new-dataset-name" data-test="new-chart-name"
/> />
</FormItem> </FormItem>
)} {this.props.datasource?.type === 'query' && (
{!( <FormItem label={t('Dataset Name')} required>
isFeatureEnabled(FeatureFlag.DASHBOARD_NATIVE_FILTERS) && <InfoTooltipWithTrigger
this.state.vizType === 'filter_box' tooltip={t('A reusable dataset will be saved with your chart.')}
) && ( placement="right"
<FormItem />
label={t('Add to dashboard')} <Input
data-test="save-chart-modal-select-dashboard-form" name="dataset_name"
> type="text"
<AsyncSelect placeholder="Dataset Name"
allowClear value={this.state.datasetName}
allowNewOptions onChange={this.handleDatasetNameChange}
ariaLabel={t('Select a dashboard')} data-test="new-dataset-name"
options={this.loadDashboards} />
onChange={this.onDashboardChange} </FormItem>
value={this.state.dashboard} )}
placeholder={ {!(
<div> isFeatureEnabled(FeatureFlag.DASHBOARD_NATIVE_FILTERS) &&
<b>{t('Select')}</b> this.state.vizType === 'filter_box'
{t(' a dashboard OR ')} ) && (
<b>{t('create')}</b> <FormItem
{t(' a new one')} label={t('Add to dashboard')}
</div> data-test="save-chart-modal-select-dashboard-form"
} >
<AsyncSelect
allowClear
allowNewOptions
ariaLabel={t('Select a dashboard')}
options={this.loadDashboards}
onChange={this.onDashboardChange}
value={this.state.dashboard}
placeholder={
<div>
<b>{t('Select')}</b>
{t(' a dashboard OR ')}
<b>{t('create')}</b>
{t(' a new one')}
</div>
}
/>
</FormItem>
)}
{info && <Alert type="info" message={info} closable={false} />}
{this.props.alert && (
<Alert
css={{ marginTop: info ? 16 : undefined }}
type="warning"
message={this.props.alert}
closable={false}
/> />
</FormItem> )}
)} </Form>
</Form> );
); };
info = () => {
const isNewDashboard = this.isNewDashboard();
let chartWillBeCreated = false;
if (
this.props.slice &&
(this.state.action !== 'overwrite' || !this.canOverwriteSlice())
) {
chartWillBeCreated = true;
}
if (chartWillBeCreated && isNewDashboard) {
return t('A new chart and dashboard will be created.');
}
if (chartWillBeCreated) {
return t('A new chart will be created.');
}
if (isNewDashboard) {
return t('A new dashboard will be created.');
}
return null;
};
renderFooter = () => ( renderFooter = () => (
<div data-test="save-modal-footer"> <div data-test="save-modal-footer">
@ -426,9 +448,7 @@ class SaveModal extends React.Component<SaveModalProps, SaveModalState> {
} }
onClick={() => this.saveOrOverwrite(true)} onClick={() => this.saveOrOverwrite(true)}
> >
{this.isNewDashboard() {t('Save & go to dashboard')}
? t('Save & go to new dashboard')
: t('Save & go to dashboard')}
</Button> </Button>
<Button <Button
id="btn_modal_save" id="btn_modal_save"
@ -443,22 +463,11 @@ class SaveModal extends React.Component<SaveModalProps, SaveModalState> {
} }
data-test="btn-modal-save" data-test="btn-modal-save"
> >
{!this.canOverwriteSlice() && this.props.slice {t('Save')}
? t('Save as new chart')
: this.isNewDashboard()
? t('Save to new dashboard')
: t('Save')}
</Button> </Button>
</div> </div>
); );
removeAlert() {
if (this.props.alert) {
this.props.actions.removeSaveModalAlert();
}
this.setState({ alert: null });
}
render() { render() {
return ( return (
<StyledModal <StyledModal

View File

@ -40,9 +40,6 @@ export default function saveModalReducer(state = {}, action) {
[actions.SAVE_SLICE_SUCCESS](data) { [actions.SAVE_SLICE_SUCCESS](data) {
return { ...state, data }; return { ...state, data };
}, },
[actions.REMOVE_SAVE_MODAL_ALERT]() {
return { ...state, saveModalAlert: null };
},
[HYDRATE_EXPLORE]() { [HYDRATE_EXPLORE]() {
return { ...action.data.saveModal }; return { ...action.data.saveModal };
}, },