mirror of
https://github.com/apache/superset.git
synced 2026-04-23 10:04:45 +00:00
624 lines
18 KiB
JavaScript
624 lines
18 KiB
JavaScript
/**
|
|
* 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 {
|
|
FeatureFlag,
|
|
isDefined,
|
|
SupersetClient,
|
|
t,
|
|
isFeatureEnabled,
|
|
getClientErrorObject,
|
|
} from '@superset-ui/core';
|
|
import { getControlsState } from 'src/explore/store';
|
|
import {
|
|
getAnnotationJsonUrl,
|
|
getExploreUrl,
|
|
getLegacyEndpointType,
|
|
buildV1ChartDataPayload,
|
|
getQuerySettings,
|
|
getChartDataUri,
|
|
} from 'src/explore/exploreUtils';
|
|
import { addDangerToast } from 'src/components/MessageToasts/actions';
|
|
import { logEvent } from 'src/logger/actions';
|
|
import { Logger, LOG_ACTIONS_LOAD_CHART } from 'src/logger/LogUtils';
|
|
import { allowCrossDomain as domainShardingEnabled } from 'src/utils/hostNamesConfig';
|
|
import { updateDataMask } from 'src/dataMask/actions';
|
|
import { waitForAsyncData } from 'src/middleware/asyncEvent';
|
|
import { safeStringify } from 'src/utils/safeStringify';
|
|
|
|
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 = {},
|
|
parseMethod,
|
|
) => {
|
|
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 },
|
|
parseMethod,
|
|
};
|
|
|
|
return SupersetClient.post(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,
|
|
parseMethod,
|
|
) => {
|
|
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) 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),
|
|
parseMethod,
|
|
};
|
|
|
|
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',
|
|
};
|
|
}
|
|
const [useLegacyApi, parseMethod] = getQuerySettings(formData);
|
|
if (useLegacyApi) {
|
|
return legacyChartDataRequest(
|
|
formData,
|
|
resultFormat,
|
|
resultType,
|
|
force,
|
|
method,
|
|
querySettings,
|
|
parseMethod,
|
|
);
|
|
}
|
|
return v1ChartDataRequest(
|
|
formData,
|
|
resultFormat,
|
|
resultType,
|
|
force,
|
|
querySettings,
|
|
setDataMask,
|
|
ownState,
|
|
parseMethod,
|
|
);
|
|
}
|
|
|
|
export function runAnnotationQuery({
|
|
annotation,
|
|
timeout,
|
|
formData = null,
|
|
key,
|
|
isDashboardRequest = false,
|
|
force = false,
|
|
}) {
|
|
return function (dispatch, getState) {
|
|
const { charts, common } = getState();
|
|
const sliceKey = key || Object.keys(charts)[0];
|
|
const queryTimeout = timeout || common.conf.SUPERSET_WEBSERVER_TIMEOUT;
|
|
|
|
// make a copy of formData, not modifying original formData
|
|
const fd = {
|
|
...(formData || charts[sliceKey].latestQueryFormData),
|
|
};
|
|
|
|
if (!annotation.sourceType) {
|
|
return Promise.resolve();
|
|
}
|
|
|
|
// In the original formData the `granularity` attribute represents the time grain (eg
|
|
// `P1D`), but in the request payload it corresponds to the name of the column where
|
|
// the time grain should be applied (eg, `Date`), so we need to move things around.
|
|
fd.time_grain_sqla = fd.time_grain_sqla || fd.granularity;
|
|
fd.granularity = fd.granularity_sqla;
|
|
|
|
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 url = getAnnotationJsonUrl(annotation.value, force);
|
|
const controller = new AbortController();
|
|
const { signal } = controller;
|
|
|
|
dispatch(annotationQueryStarted(annotation, controller, sliceKey));
|
|
|
|
const annotationIndex = fd?.annotation_layers?.findIndex(
|
|
it => it.name === annotation.name,
|
|
);
|
|
if (annotationIndex >= 0) {
|
|
fd.annotation_layers[annotationIndex].overrides = sliceFormData;
|
|
}
|
|
|
|
return SupersetClient.post({
|
|
url,
|
|
signal,
|
|
timeout: queryTimeout * 1000,
|
|
headers: { 'Content-Type': 'application/json' },
|
|
jsonPayload: buildV1ChartDataPayload({
|
|
formData: fd,
|
|
force,
|
|
resultFormat: 'json',
|
|
resultType: 'full',
|
|
}),
|
|
})
|
|
.then(({ json }) => {
|
|
const data = json?.result?.[0]?.annotation_data?.[annotation.name];
|
|
return dispatch(annotationQuerySuccess(annotation, { data }, 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 handleChartDataResponse(response, json, useLegacyApi) {
|
|
if (isFeatureEnabled(FeatureFlag.GlobalAsyncQueries)) {
|
|
// 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 (useLegacyApi) {
|
|
return waitForAsyncData(result[0]);
|
|
}
|
|
return waitForAsyncData(result);
|
|
default:
|
|
throw new Error(
|
|
`Received unexpected response status (${response.status}) while fetching chart data`,
|
|
);
|
|
}
|
|
}
|
|
return json.result;
|
|
}
|
|
|
|
export function exploreJSON(
|
|
formData,
|
|
force = false,
|
|
timeout,
|
|
key,
|
|
dashboardId,
|
|
ownState,
|
|
) {
|
|
return async (dispatch, getState) => {
|
|
const logStart = Logger.getTimestamp();
|
|
const controller = new AbortController();
|
|
const queryTimeout =
|
|
timeout || getState().common.conf.SUPERSET_WEBSERVER_TIMEOUT;
|
|
|
|
const requestParams = {
|
|
signal: controller.signal,
|
|
timeout: queryTimeout * 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: 'POST',
|
|
requestParams,
|
|
ownState,
|
|
});
|
|
|
|
dispatch(chartUpdateStarted(controller, formData, key));
|
|
|
|
const [useLegacyApi] = getQuerySettings(formData);
|
|
const chartDataRequestCaught = chartDataRequest
|
|
.then(({ response, json }) =>
|
|
handleChartDataResponse(response, json, useLegacyApi),
|
|
)
|
|
.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.GlobalAsyncQueries)) {
|
|
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 = useLegacyApi
|
|
? 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 POST_CHART_FORM_DATA = 'POST_CHART_FORM_DATA';
|
|
export function postChartFormData(
|
|
formData,
|
|
force = false,
|
|
timeout,
|
|
key,
|
|
dashboardId,
|
|
ownState,
|
|
) {
|
|
return exploreJSON(formData, force, timeout, key, dashboardId, ownState);
|
|
}
|
|
|
|
export function redirectSQLLab(formData, history) {
|
|
return dispatch => {
|
|
getChartDataRequest({ formData, resultFormat: 'json', resultType: 'query' })
|
|
.then(({ json }) => {
|
|
const redirectUrl = '/sqllab/';
|
|
const payload = {
|
|
datasourceKey: formData.datasource,
|
|
sql: json.result[0].query,
|
|
};
|
|
if (history) {
|
|
history.push({
|
|
pathname: redirectUrl,
|
|
state: {
|
|
requestedQuery: payload,
|
|
},
|
|
});
|
|
} else {
|
|
SupersetClient.postForm(redirectUrl, {
|
|
form_data: safeStringify(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,
|
|
),
|
|
);
|
|
};
|
|
}
|
|
|
|
export const getDatasourceSamples = async (
|
|
datasourceType,
|
|
datasourceId,
|
|
force,
|
|
jsonPayload,
|
|
perPage,
|
|
page,
|
|
) => {
|
|
try {
|
|
const searchParams = {
|
|
force,
|
|
datasource_type: datasourceType,
|
|
datasource_id: datasourceId,
|
|
};
|
|
|
|
if (isDefined(perPage) && isDefined(page)) {
|
|
searchParams.per_page = perPage;
|
|
searchParams.page = page;
|
|
}
|
|
|
|
const response = await SupersetClient.post({
|
|
endpoint: '/datasource/samples',
|
|
jsonPayload,
|
|
searchParams,
|
|
parseMethod: 'json-bigint',
|
|
});
|
|
|
|
return response.json.result;
|
|
} catch (err) {
|
|
const clientError = await getClientErrorObject(err);
|
|
throw new Error(
|
|
clientError.message || clientError.error || t('Sorry, an error occurred'),
|
|
{ cause: err },
|
|
);
|
|
}
|
|
};
|