mirror of
https://github.com/apache/superset.git
synced 2026-04-23 18:14:56 +00:00
chore: Moves Chart to the components folder (#19029)
This commit is contained in:
committed by
GitHub
parent
2cc5678dfc
commit
04a36d5c92
597
superset-frontend/src/components/Chart/chartAction.js
Normal file
597
superset-frontend/src/components/Chart/chartAction.js
Normal file
@@ -0,0 +1,597 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
/* eslint no-undef: 'error' */
|
||||
/* eslint no-param-reassign: ["error", { "props": false }] */
|
||||
import moment from 'moment';
|
||||
import { t, SupersetClient } from '@superset-ui/core';
|
||||
import { getControlsState } from 'src/explore/store';
|
||||
import { isFeatureEnabled, FeatureFlag } from 'src/featureFlags';
|
||||
import {
|
||||
getAnnotationJsonUrl,
|
||||
getExploreUrl,
|
||||
getLegacyEndpointType,
|
||||
buildV1ChartDataPayload,
|
||||
postForm,
|
||||
shouldUseLegacyApi,
|
||||
getChartDataUri,
|
||||
} from 'src/explore/exploreUtils';
|
||||
import {
|
||||
requiresQuery,
|
||||
ANNOTATION_SOURCE_TYPES,
|
||||
} from 'src/modules/AnnotationTypes';
|
||||
|
||||
import { addDangerToast } from 'src/components/MessageToasts/actions';
|
||||
import { logEvent } from 'src/logger/actions';
|
||||
import { Logger, LOG_ACTIONS_LOAD_CHART } from 'src/logger/LogUtils';
|
||||
import { getClientErrorObject } from 'src/utils/getClientErrorObject';
|
||||
import { allowCrossDomain as domainShardingEnabled } from 'src/utils/hostNamesConfig';
|
||||
import { updateDataMask } from 'src/dataMask/actions';
|
||||
import { waitForAsyncData } from 'src/middleware/asyncEvent';
|
||||
|
||||
export const CHART_UPDATE_STARTED = 'CHART_UPDATE_STARTED';
|
||||
export function chartUpdateStarted(queryController, latestQueryFormData, key) {
|
||||
return {
|
||||
type: CHART_UPDATE_STARTED,
|
||||
queryController,
|
||||
latestQueryFormData,
|
||||
key,
|
||||
};
|
||||
}
|
||||
|
||||
export const CHART_UPDATE_SUCCEEDED = 'CHART_UPDATE_SUCCEEDED';
|
||||
export function chartUpdateSucceeded(queriesResponse, key) {
|
||||
return { type: CHART_UPDATE_SUCCEEDED, queriesResponse, key };
|
||||
}
|
||||
|
||||
export const CHART_UPDATE_STOPPED = 'CHART_UPDATE_STOPPED';
|
||||
export function chartUpdateStopped(key) {
|
||||
return { type: CHART_UPDATE_STOPPED, key };
|
||||
}
|
||||
|
||||
export const CHART_UPDATE_FAILED = 'CHART_UPDATE_FAILED';
|
||||
export function chartUpdateFailed(queriesResponse, key) {
|
||||
return { type: CHART_UPDATE_FAILED, queriesResponse, key };
|
||||
}
|
||||
|
||||
export const CHART_RENDERING_FAILED = 'CHART_RENDERING_FAILED';
|
||||
export function chartRenderingFailed(error, key, stackTrace) {
|
||||
return { type: CHART_RENDERING_FAILED, error, key, stackTrace };
|
||||
}
|
||||
|
||||
export const CHART_RENDERING_SUCCEEDED = 'CHART_RENDERING_SUCCEEDED';
|
||||
export function chartRenderingSucceeded(key) {
|
||||
return { type: CHART_RENDERING_SUCCEEDED, key };
|
||||
}
|
||||
|
||||
export const REMOVE_CHART = 'REMOVE_CHART';
|
||||
export function removeChart(key) {
|
||||
return { type: REMOVE_CHART, key };
|
||||
}
|
||||
|
||||
export const ANNOTATION_QUERY_SUCCESS = 'ANNOTATION_QUERY_SUCCESS';
|
||||
export function annotationQuerySuccess(annotation, queryResponse, key) {
|
||||
return { type: ANNOTATION_QUERY_SUCCESS, annotation, queryResponse, key };
|
||||
}
|
||||
|
||||
export const ANNOTATION_QUERY_STARTED = 'ANNOTATION_QUERY_STARTED';
|
||||
export function annotationQueryStarted(annotation, queryController, key) {
|
||||
return { type: ANNOTATION_QUERY_STARTED, annotation, queryController, key };
|
||||
}
|
||||
|
||||
export const ANNOTATION_QUERY_FAILED = 'ANNOTATION_QUERY_FAILED';
|
||||
export function annotationQueryFailed(annotation, queryResponse, key) {
|
||||
return { type: ANNOTATION_QUERY_FAILED, annotation, queryResponse, key };
|
||||
}
|
||||
|
||||
export const DYNAMIC_PLUGIN_CONTROLS_READY = 'DYNAMIC_PLUGIN_CONTROLS_READY';
|
||||
export const dynamicPluginControlsReady = () => (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const controlsState = getControlsState(
|
||||
state.explore,
|
||||
state.explore.form_data,
|
||||
);
|
||||
dispatch({
|
||||
type: DYNAMIC_PLUGIN_CONTROLS_READY,
|
||||
key: controlsState.slice_id.value,
|
||||
controlsState,
|
||||
});
|
||||
};
|
||||
|
||||
const legacyChartDataRequest = async (
|
||||
formData,
|
||||
resultFormat,
|
||||
resultType,
|
||||
force,
|
||||
method = 'POST',
|
||||
requestParams = {},
|
||||
) => {
|
||||
const endpointType = getLegacyEndpointType({ resultFormat, resultType });
|
||||
const allowDomainSharding =
|
||||
// eslint-disable-next-line camelcase
|
||||
domainShardingEnabled && requestParams?.dashboard_id;
|
||||
const url = getExploreUrl({
|
||||
formData,
|
||||
endpointType,
|
||||
force,
|
||||
allowDomainSharding,
|
||||
method,
|
||||
requestParams: requestParams.dashboard_id
|
||||
? { dashboard_id: requestParams.dashboard_id }
|
||||
: {},
|
||||
});
|
||||
const querySettings = {
|
||||
...requestParams,
|
||||
url,
|
||||
postPayload: { form_data: formData },
|
||||
};
|
||||
|
||||
const clientMethod =
|
||||
'GET' && isFeatureEnabled(FeatureFlag.CLIENT_CACHE)
|
||||
? SupersetClient.get
|
||||
: SupersetClient.post;
|
||||
return clientMethod(querySettings).then(({ json, response }) =>
|
||||
// Make the legacy endpoint return a payload that corresponds to the
|
||||
// V1 chart data endpoint response signature.
|
||||
({
|
||||
response,
|
||||
json: { result: [json] },
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const v1ChartDataRequest = async (
|
||||
formData,
|
||||
resultFormat,
|
||||
resultType,
|
||||
force,
|
||||
requestParams,
|
||||
setDataMask,
|
||||
ownState,
|
||||
) => {
|
||||
const payload = buildV1ChartDataPayload({
|
||||
formData,
|
||||
resultType,
|
||||
resultFormat,
|
||||
force,
|
||||
setDataMask,
|
||||
ownState,
|
||||
});
|
||||
|
||||
// The dashboard id is added to query params for tracking purposes
|
||||
const { slice_id: sliceId } = formData;
|
||||
const { dashboard_id: dashboardId } = requestParams;
|
||||
|
||||
const qs = {};
|
||||
if (sliceId !== undefined) qs.form_data = `{"slice_id":${sliceId}}`;
|
||||
if (dashboardId !== undefined) qs.dashboard_id = dashboardId;
|
||||
if (force !== false) qs.force = force;
|
||||
|
||||
const allowDomainSharding =
|
||||
// eslint-disable-next-line camelcase
|
||||
domainShardingEnabled && requestParams?.dashboard_id;
|
||||
const url = getChartDataUri({
|
||||
path: '/api/v1/chart/data',
|
||||
qs,
|
||||
allowDomainSharding,
|
||||
}).toString();
|
||||
|
||||
const querySettings = {
|
||||
...requestParams,
|
||||
url,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
};
|
||||
|
||||
return SupersetClient.post(querySettings);
|
||||
};
|
||||
|
||||
export async function getChartDataRequest({
|
||||
formData,
|
||||
setDataMask = () => {},
|
||||
resultFormat = 'json',
|
||||
resultType = 'full',
|
||||
force = false,
|
||||
method = 'POST',
|
||||
requestParams = {},
|
||||
ownState = {},
|
||||
}) {
|
||||
let querySettings = {
|
||||
...requestParams,
|
||||
};
|
||||
|
||||
if (domainShardingEnabled) {
|
||||
querySettings = {
|
||||
...querySettings,
|
||||
mode: 'cors',
|
||||
credentials: 'include',
|
||||
};
|
||||
}
|
||||
|
||||
if (shouldUseLegacyApi(formData)) {
|
||||
return legacyChartDataRequest(
|
||||
formData,
|
||||
resultFormat,
|
||||
resultType,
|
||||
force,
|
||||
method,
|
||||
querySettings,
|
||||
);
|
||||
}
|
||||
return v1ChartDataRequest(
|
||||
formData,
|
||||
resultFormat,
|
||||
resultType,
|
||||
force,
|
||||
querySettings,
|
||||
setDataMask,
|
||||
ownState,
|
||||
);
|
||||
}
|
||||
|
||||
export function runAnnotationQuery({
|
||||
annotation,
|
||||
timeout = 60,
|
||||
formData = null,
|
||||
key,
|
||||
isDashboardRequest = false,
|
||||
force = false,
|
||||
}) {
|
||||
return function (dispatch, getState) {
|
||||
const sliceKey = key || Object.keys(getState().charts)[0];
|
||||
// make a copy of formData, not modifying original formData
|
||||
const fd = {
|
||||
...(formData || getState().charts[sliceKey].latestQueryFormData),
|
||||
};
|
||||
|
||||
if (!requiresQuery(annotation.sourceType)) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
const granularity = fd.time_grain_sqla || fd.granularity;
|
||||
fd.time_grain_sqla = granularity;
|
||||
fd.granularity = granularity;
|
||||
const overridesKeys = Object.keys(annotation.overrides);
|
||||
if (overridesKeys.includes('since') || overridesKeys.includes('until')) {
|
||||
annotation.overrides = {
|
||||
...annotation.overrides,
|
||||
time_range: null,
|
||||
};
|
||||
}
|
||||
const sliceFormData = Object.keys(annotation.overrides).reduce(
|
||||
(d, k) => ({
|
||||
...d,
|
||||
[k]: annotation.overrides[k] || fd[k],
|
||||
}),
|
||||
{},
|
||||
);
|
||||
|
||||
if (!isDashboardRequest && fd) {
|
||||
const hasExtraFilters = fd.extra_filters && fd.extra_filters.length > 0;
|
||||
sliceFormData.extra_filters = hasExtraFilters
|
||||
? fd.extra_filters
|
||||
: undefined;
|
||||
}
|
||||
|
||||
const isNative = annotation.sourceType === ANNOTATION_SOURCE_TYPES.NATIVE;
|
||||
const url = getAnnotationJsonUrl(
|
||||
annotation.value,
|
||||
sliceFormData,
|
||||
isNative,
|
||||
force,
|
||||
);
|
||||
const controller = new AbortController();
|
||||
const { signal } = controller;
|
||||
|
||||
dispatch(annotationQueryStarted(annotation, controller, sliceKey));
|
||||
|
||||
return SupersetClient.get({
|
||||
url,
|
||||
signal,
|
||||
timeout: timeout * 1000,
|
||||
})
|
||||
.then(({ json }) =>
|
||||
dispatch(annotationQuerySuccess(annotation, json, sliceKey)),
|
||||
)
|
||||
.catch(response =>
|
||||
getClientErrorObject(response).then(err => {
|
||||
if (err.statusText === 'timeout') {
|
||||
dispatch(
|
||||
annotationQueryFailed(
|
||||
annotation,
|
||||
{ error: 'Query timeout' },
|
||||
sliceKey,
|
||||
),
|
||||
);
|
||||
} else if ((err.error || '').toLowerCase().includes('no data')) {
|
||||
dispatch(annotationQuerySuccess(annotation, err, sliceKey));
|
||||
} else if (err.statusText !== 'abort') {
|
||||
dispatch(annotationQueryFailed(annotation, err, sliceKey));
|
||||
}
|
||||
}),
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export const TRIGGER_QUERY = 'TRIGGER_QUERY';
|
||||
export function triggerQuery(value = true, key) {
|
||||
return { type: TRIGGER_QUERY, value, key };
|
||||
}
|
||||
|
||||
// this action is used for forced re-render without fetch data
|
||||
export const RENDER_TRIGGERED = 'RENDER_TRIGGERED';
|
||||
export function renderTriggered(value, key) {
|
||||
return { type: RENDER_TRIGGERED, value, key };
|
||||
}
|
||||
|
||||
export const UPDATE_QUERY_FORM_DATA = 'UPDATE_QUERY_FORM_DATA';
|
||||
export function updateQueryFormData(value, key) {
|
||||
return { type: UPDATE_QUERY_FORM_DATA, value, key };
|
||||
}
|
||||
|
||||
// in the sql lab -> explore flow, user can inline edit chart title,
|
||||
// then the chart will be assigned a new slice_id
|
||||
export const UPDATE_CHART_ID = 'UPDATE_CHART_ID';
|
||||
export function updateChartId(newId, key = 0) {
|
||||
return { type: UPDATE_CHART_ID, newId, key };
|
||||
}
|
||||
|
||||
export const ADD_CHART = 'ADD_CHART';
|
||||
export function addChart(chart, key) {
|
||||
return { type: ADD_CHART, chart, key };
|
||||
}
|
||||
|
||||
export function exploreJSON(
|
||||
formData,
|
||||
force = false,
|
||||
timeout = 60,
|
||||
key,
|
||||
method,
|
||||
dashboardId,
|
||||
ownState,
|
||||
) {
|
||||
return async dispatch => {
|
||||
const logStart = Logger.getTimestamp();
|
||||
const controller = new AbortController();
|
||||
|
||||
const requestParams = {
|
||||
signal: controller.signal,
|
||||
timeout: timeout * 1000,
|
||||
};
|
||||
if (dashboardId) requestParams.dashboard_id = dashboardId;
|
||||
|
||||
const setDataMask = dataMask => {
|
||||
dispatch(updateDataMask(formData.slice_id, dataMask));
|
||||
};
|
||||
const chartDataRequest = getChartDataRequest({
|
||||
setDataMask,
|
||||
formData,
|
||||
resultFormat: 'json',
|
||||
resultType: 'full',
|
||||
force,
|
||||
method,
|
||||
requestParams,
|
||||
ownState,
|
||||
});
|
||||
|
||||
dispatch(chartUpdateStarted(controller, formData, key));
|
||||
|
||||
const chartDataRequestCaught = chartDataRequest
|
||||
.then(({ response, json }) => {
|
||||
if (isFeatureEnabled(FeatureFlag.GLOBAL_ASYNC_QUERIES)) {
|
||||
// deal with getChartDataRequest transforming the response data
|
||||
const result = 'result' in json ? json.result : json;
|
||||
switch (response.status) {
|
||||
case 200:
|
||||
// Query results returned synchronously, meaning query was already cached.
|
||||
return Promise.resolve(result);
|
||||
case 202:
|
||||
// Query is running asynchronously and we must await the results
|
||||
if (shouldUseLegacyApi(formData)) {
|
||||
return waitForAsyncData(result[0]);
|
||||
}
|
||||
return waitForAsyncData(result);
|
||||
default:
|
||||
throw new Error(
|
||||
`Received unexpected response status (${response.status}) while fetching chart data`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return json.result;
|
||||
})
|
||||
.then(queriesResponse => {
|
||||
queriesResponse.forEach(resultItem =>
|
||||
dispatch(
|
||||
logEvent(LOG_ACTIONS_LOAD_CHART, {
|
||||
slice_id: key,
|
||||
applied_filters: resultItem.applied_filters,
|
||||
is_cached: resultItem.is_cached,
|
||||
force_refresh: force,
|
||||
row_count: resultItem.rowcount,
|
||||
datasource: formData.datasource,
|
||||
start_offset: logStart,
|
||||
ts: new Date().getTime(),
|
||||
duration: Logger.getTimestamp() - logStart,
|
||||
has_extra_filters:
|
||||
formData.extra_filters && formData.extra_filters.length > 0,
|
||||
viz_type: formData.viz_type,
|
||||
data_age: resultItem.is_cached
|
||||
? moment(new Date()).diff(moment.utc(resultItem.cached_dttm))
|
||||
: null,
|
||||
}),
|
||||
),
|
||||
);
|
||||
return dispatch(chartUpdateSucceeded(queriesResponse, key));
|
||||
})
|
||||
.catch(response => {
|
||||
if (isFeatureEnabled(FeatureFlag.GLOBAL_ASYNC_QUERIES)) {
|
||||
return dispatch(chartUpdateFailed([response], key));
|
||||
}
|
||||
|
||||
const appendErrorLog = (errorDetails, isCached) => {
|
||||
dispatch(
|
||||
logEvent(LOG_ACTIONS_LOAD_CHART, {
|
||||
slice_id: key,
|
||||
has_err: true,
|
||||
is_cached: isCached,
|
||||
error_details: errorDetails,
|
||||
datasource: formData.datasource,
|
||||
start_offset: logStart,
|
||||
ts: new Date().getTime(),
|
||||
duration: Logger.getTimestamp() - logStart,
|
||||
}),
|
||||
);
|
||||
};
|
||||
if (response.name === 'AbortError') {
|
||||
appendErrorLog('abort');
|
||||
return dispatch(chartUpdateStopped(key));
|
||||
}
|
||||
return getClientErrorObject(response).then(parsedResponse => {
|
||||
if (response.statusText === 'timeout') {
|
||||
appendErrorLog('timeout');
|
||||
} else {
|
||||
appendErrorLog(parsedResponse.error, parsedResponse.is_cached);
|
||||
}
|
||||
return dispatch(chartUpdateFailed([parsedResponse], key));
|
||||
});
|
||||
});
|
||||
|
||||
// only retrieve annotations when calling the legacy API
|
||||
const annotationLayers = shouldUseLegacyApi(formData)
|
||||
? formData.annotation_layers || []
|
||||
: [];
|
||||
const isDashboardRequest = dashboardId > 0;
|
||||
|
||||
return Promise.all([
|
||||
chartDataRequestCaught,
|
||||
dispatch(triggerQuery(false, key)),
|
||||
dispatch(updateQueryFormData(formData, key)),
|
||||
...annotationLayers.map(annotation =>
|
||||
dispatch(
|
||||
runAnnotationQuery({
|
||||
annotation,
|
||||
timeout,
|
||||
formData,
|
||||
key,
|
||||
isDashboardRequest,
|
||||
force,
|
||||
}),
|
||||
),
|
||||
),
|
||||
]);
|
||||
};
|
||||
}
|
||||
|
||||
export const GET_SAVED_CHART = 'GET_SAVED_CHART';
|
||||
export function getSavedChart(
|
||||
formData,
|
||||
force = false,
|
||||
timeout = 60,
|
||||
key,
|
||||
dashboardId,
|
||||
ownState,
|
||||
) {
|
||||
/*
|
||||
* Perform a GET request to `/explore_json`.
|
||||
*
|
||||
* This will return the payload of a saved chart, optionally filtered by
|
||||
* ad-hoc or extra filters from dashboards. Eg:
|
||||
*
|
||||
* GET /explore_json?{"chart_id":1}
|
||||
* GET /explore_json?{"chart_id":1,"extra_filters":"..."}
|
||||
*
|
||||
*/
|
||||
return exploreJSON(
|
||||
formData,
|
||||
force,
|
||||
timeout,
|
||||
key,
|
||||
'GET',
|
||||
dashboardId,
|
||||
ownState,
|
||||
);
|
||||
}
|
||||
|
||||
export const POST_CHART_FORM_DATA = 'POST_CHART_FORM_DATA';
|
||||
export function postChartFormData(
|
||||
formData,
|
||||
force = false,
|
||||
timeout = 60,
|
||||
key,
|
||||
dashboardId,
|
||||
ownState,
|
||||
) {
|
||||
/*
|
||||
* Perform a POST request to `/explore_json`.
|
||||
*
|
||||
* This will post the form data to the endpoint, returning a new chart.
|
||||
*
|
||||
*/
|
||||
return exploreJSON(
|
||||
formData,
|
||||
force,
|
||||
timeout,
|
||||
key,
|
||||
'POST',
|
||||
dashboardId,
|
||||
ownState,
|
||||
);
|
||||
}
|
||||
|
||||
export function redirectSQLLab(formData) {
|
||||
return dispatch => {
|
||||
getChartDataRequest({ formData, resultFormat: 'json', resultType: 'query' })
|
||||
.then(({ json }) => {
|
||||
const redirectUrl = '/superset/sqllab/';
|
||||
const payload = {
|
||||
datasourceKey: formData.datasource,
|
||||
sql: json.result[0].query,
|
||||
};
|
||||
postForm(redirectUrl, payload);
|
||||
})
|
||||
.catch(() =>
|
||||
dispatch(addDangerToast(t('An error occurred while loading the SQL'))),
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export function refreshChart(chartKey, force, dashboardId) {
|
||||
return (dispatch, getState) => {
|
||||
const chart = (getState().charts || {})[chartKey];
|
||||
const timeout =
|
||||
getState().dashboardInfo.common.conf.SUPERSET_WEBSERVER_TIMEOUT;
|
||||
|
||||
if (
|
||||
!chart.latestQueryFormData ||
|
||||
Object.keys(chart.latestQueryFormData).length === 0
|
||||
) {
|
||||
return;
|
||||
}
|
||||
dispatch(
|
||||
postChartFormData(
|
||||
chart.latestQueryFormData,
|
||||
force,
|
||||
timeout,
|
||||
chart.id,
|
||||
dashboardId,
|
||||
getState().dataMask[chart.id]?.ownState,
|
||||
),
|
||||
);
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user