Skip to content

Commit

Permalink
chore(explore): Update chart save to use API endpoints (#20498)
Browse files Browse the repository at this point in the history
* Update SaveModal to use v1 API instead of POST /explore.

* Refactor SaveModal tests and remove obsolete ones.

* Fix redirect to /explore on save.

* Add toasts (but they don't work).

* Move logic inside try block, clarify unary-plus use.

* Add tests.

* Fix owners bug in updateSlice, enable navigation to dashboard w/o reload.

* Fix toasts.

* Fix translated strings.

* Fix unintended removal from dashboard bug.

* Fix native filters bug.

* Don't refresh Explore page after saving

* Use JSON payload shorthand.

* Prevent current dashboards being overwritten on viz type change.
  • Loading branch information
codyml authored Jul 13, 2022
1 parent c3ac612 commit b1020e3
Show file tree
Hide file tree
Showing 9 changed files with 734 additions and 393 deletions.
4 changes: 4 additions & 0 deletions superset-frontend/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,10 @@ export const URL_PARAMS = {
name: 'show_database_modal',
type: 'boolean',
},
saveAction: {
name: 'save_action',
type: 'string',
},
} as const;

export const RESERVED_CHART_URL_PARAMS: string[] = [
Expand Down
52 changes: 30 additions & 22 deletions superset-frontend/src/explore/ExplorePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,50 +16,58 @@
* specific language governing permissions and limitations
* under the License.
*/
import React, { useEffect, useState } from 'react';
import React, { useEffect, useRef, useState } from 'react';
import { useDispatch } from 'react-redux';
import { useLocation } from 'react-router-dom';
import { makeApi, t } from '@superset-ui/core';
import Loading from 'src/components/Loading';
import { addDangerToast } from 'src/components/MessageToasts/actions';
import { isNullish } from 'src/utils/common';
import { getUrlParam } from 'src/utils/urlUtils';
import { URL_PARAMS } from 'src/constants';
import { getParsedExploreURLParams } from './exploreUtils/getParsedExploreURLParams';
import { hydrateExplore } from './actions/hydrateExplore';
import ExploreViewContainer from './components/ExploreViewContainer';
import { ExploreResponsePayload } from './types';
import { fallbackExploreInitialData } from './fixtures';
import { addDangerToast } from '../components/MessageToasts/actions';
import { isNullish } from '../utils/common';

const loadErrorMessage = t('Failed to load chart data.');

const fetchExploreData = () => {
const exploreUrlParams = getParsedExploreURLParams();
return makeApi<{}, ExploreResponsePayload>({
const fetchExploreData = (exploreUrlParams: URLSearchParams) =>
makeApi<{}, ExploreResponsePayload>({
method: 'GET',
endpoint: 'api/v1/explore/',
})(exploreUrlParams);
};

const ExplorePage = () => {
const [isLoaded, setIsLoaded] = useState(false);
const isExploreInitialized = useRef(false);
const dispatch = useDispatch();
const location = useLocation();

useEffect(() => {
fetchExploreData()
.then(({ result }) => {
if (isNullish(result.dataset?.id) && isNullish(result.dataset?.uid)) {
const exploreUrlParams = getParsedExploreURLParams(location);
const isSaveAction = !!getUrlParam(URL_PARAMS.saveAction);
if (!isExploreInitialized.current || isSaveAction) {
fetchExploreData(exploreUrlParams)
.then(({ result }) => {
if (isNullish(result.dataset?.id) && isNullish(result.dataset?.uid)) {
dispatch(hydrateExplore(fallbackExploreInitialData));
dispatch(addDangerToast(loadErrorMessage));
} else {
dispatch(hydrateExplore(result));
}
})
.catch(() => {
dispatch(hydrateExplore(fallbackExploreInitialData));
dispatch(addDangerToast(loadErrorMessage));
} else {
dispatch(hydrateExplore(result));
}
})
.catch(() => {
dispatch(hydrateExplore(fallbackExploreInitialData));
dispatch(addDangerToast(loadErrorMessage));
})
.finally(() => {
setIsLoaded(true);
});
}, [dispatch]);
})
.finally(() => {
setIsLoaded(true);
isExploreInitialized.current = true;
});
}
}, [dispatch, location]);

if (!isLoaded) {
return <Loading />;
Expand Down
1 change: 1 addition & 0 deletions superset-frontend/src/explore/actions/hydrateExplore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ export const hydrateExplore =
controlsTransferred: [],
standalone: getUrlParam(URL_PARAMS.standalone),
force: getUrlParam(URL_PARAMS.force),
sliceDashboards: initialFormData.dashboards,
};

// apply initial mapStateToProps for all controls, must execute AFTER
Expand Down
165 changes: 135 additions & 30 deletions superset-frontend/src/explore/actions/saveModalActions.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@
* specific language governing permissions and limitations
* under the License.
*/
import { SupersetClient } from '@superset-ui/core';
import { buildV1ChartDataPayload, getExploreUrl } from '../exploreUtils';
import { SupersetClient, t } from '@superset-ui/core';
import { addSuccessToast } from 'src/components/MessageToasts/actions';
import { buildV1ChartDataPayload } from '../exploreUtils';

export const FETCH_DASHBOARDS_SUCCEEDED = 'FETCH_DASHBOARDS_SUCCEEDED';
export function fetchDashboardsSucceeded(choices) {
Expand Down Expand Up @@ -60,36 +61,140 @@ export function removeSaveModalAlert() {
return { type: REMOVE_SAVE_MODAL_ALERT };
}

export function saveSlice(formData, requestParams) {
return dispatch => {
let url = getExploreUrl({
formData,
endpointType: 'base',
force: false,
curUrl: null,
requestParams,
export const getSlicePayload = (
sliceName,
formDataWithNativeFilters,
owners,
) => {
const formData = {
...formDataWithNativeFilters,
adhoc_filters: formDataWithNativeFilters.adhoc_filters?.filter(
f => !f.isExtra,
),
};

const [datasourceId, datasourceType] = formData.datasource.split('__');
const payload = {
params: JSON.stringify(formData),
slice_name: sliceName,
viz_type: formData.viz_type,
datasource_id: parseInt(datasourceId, 10),
datasource_type: datasourceType,
dashboards: formData.dashboards,
owners,
query_context: JSON.stringify(
buildV1ChartDataPayload({
formData,
force: false,
resultFormat: 'json',
resultType: 'full',
setDataMask: null,
ownState: null,
}),
),
};
return payload;
};

const addToasts = (isNewSlice, sliceName, addedToDashboard) => {
const toasts = [];
if (isNewSlice) {
toasts.push(addSuccessToast(t('Chart [%s] has been saved', sliceName)));
} else {
toasts.push(
addSuccessToast(t('Chart [%s] has been overwritten', sliceName)),
);
}

if (addedToDashboard) {
if (addedToDashboard.new) {
toasts.push(
addSuccessToast(
t(
'Dashboard [%s] just got created and chart [%s] was added to it',
addedToDashboard.title,
sliceName,
),
),
);
} else {
toasts.push(
addSuccessToast(
t(
'Chart [%s] was added to dashboard [%s]',
sliceName,
addedToDashboard.title,
),
),
);
}
}

return toasts;
};

// Update existing slice
export const updateSlice =
({ slice_id: sliceId, owners }, sliceName, formData, addedToDashboard) =>
async dispatch => {
try {
const response = await SupersetClient.put({
endpoint: `/api/v1/chart/${sliceId}`,
jsonPayload: getSlicePayload(sliceName, formData, owners),
});

dispatch(saveSliceSuccess());
addToasts(false, sliceName, addedToDashboard).map(dispatch);
return response.json;
} catch (error) {
dispatch(saveSliceFailed());
throw error;
}
};

// Create new slice
export const createSlice =
(sliceName, formData, addedToDashboard) => async dispatch => {
try {
const response = await SupersetClient.post({
endpoint: `/api/v1/chart/`,
jsonPayload: getSlicePayload(sliceName, formData),
});

dispatch(saveSliceSuccess());
addToasts(true, sliceName, addedToDashboard).map(dispatch);
return response.json;
} catch (error) {
dispatch(saveSliceFailed());
throw error;
}
};

// Create new dashboard
export const createDashboard = dashboardName => async dispatch => {
try {
const response = await SupersetClient.post({
endpoint: `/api/v1/dashboard/`,
jsonPayload: { dashboard_title: dashboardName },
});

// TODO: This will be removed in the next PR that will change the logic to save a slice
url = url.replace('/explore', '/superset/explore');
return response.json;
} catch (error) {
dispatch(saveSliceFailed());
throw error;
}
};

// Save the query context so we can re-generate the data from Python
// for alerts and reports
const queryContext = buildV1ChartDataPayload({
formData,
force: false,
resultFormat: 'json',
resultType: 'full',
// Get existing dashboard from ID
export const getDashboard = dashboardId => async dispatch => {
try {
const response = await SupersetClient.get({
endpoint: `/api/v1/dashboard/${dashboardId}`,
});

return SupersetClient.post({
url,
postPayload: { form_data: formData, query_context: queryContext },
})
.then(response => {
dispatch(saveSliceSuccess(response.json));
return response.json;
})
.catch(() => dispatch(saveSliceFailed()));
};
}
return response.json;
} catch (error) {
dispatch(saveSliceFailed());
throw error;
}
};
Loading

0 comments on commit b1020e3

Please sign in to comment.