blob: 74ed410f96de12e755a170e6805a5af937f8a024 [file] [log] [blame]
/**
* 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 '../featureFlags';
import {
getAnnotationJsonUrl,
getExploreUrl,
getLegacyEndpointType,
buildV1ChartDataPayload,
postForm,
shouldUseLegacyApi,
getChartDataUri,
} from '../explore/exploreUtils';
import {
requiresQuery,
ANNOTATION_SOURCE_TYPES,
} from '../modules/AnnotationTypes';
import { addDangerToast } from '../messageToasts/actions';
import { logEvent } from '../logger/actions';
import { Logger, LOG_ACTIONS_LOAD_CHART } from '../logger/LogUtils';
import { getClientErrorObject } from '../utils/getClientErrorObject';
import { allowCrossDomain as domainShardingEnabled } from '../utils/hostNamesConfig';
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_UPDATE_QUEUED = 'CHART_UPDATE_QUEUED';
export function chartUpdateQueued(asyncJobMeta, key) {
return { type: CHART_UPDATE_QUEUED, asyncJobMeta, 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 }) =>
// Make the legacy endpoint return a payload that corresponds to the
// V1 chart data endpoint response signature.
({
result: [json],
}),
);
};
const v1ChartDataRequest = async (
formData,
resultFormat,
resultType,
force,
requestParams,
) => {
const payload = buildV1ChartDataPayload({
formData,
resultType,
resultFormat,
force,
});
// 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).then(({ json }) => json);
};
export async function getChartDataRequest({
formData,
resultFormat = 'json',
resultType = 'full',
force = false,
method = 'POST',
requestParams = {},
}) {
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,
);
}
export function runAnnotationQuery(
annotation,
timeout = 60,
formData = null,
key,
isDashboardRequest = 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);
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,
) {
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 chartDataRequest = getChartDataRequest({
formData,
resultFormat: 'json',
resultType: 'full',
force,
method,
requestParams,
});
dispatch(chartUpdateStarted(controller, formData, key));
const chartDataRequestCaught = chartDataRequest
.then(response => {
const queriesResponse = response.result;
if (isFeatureEnabled(FeatureFlag.GLOBAL_ASYNC_QUERIES)) {
// deal with getChartDataRequest transforming the response data
const result = 'result' in response ? response.result[0] : response;
return dispatch(chartUpdateQueued(result, key));
}
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 => {
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(x =>
dispatch(
runAnnotationQuery(x, timeout, formData, key, isDashboardRequest),
),
),
]);
};
}
export const GET_SAVED_CHART = 'GET_SAVED_CHART';
export function getSavedChart(
formData,
force = false,
timeout = 60,
key,
dashboardId,
) {
/*
* 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);
}
export const POST_CHART_FORM_DATA = 'POST_CHART_FORM_DATA';
export function postChartFormData(
formData,
force = false,
timeout = 60,
key,
dashboardId,
) {
/*
* 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);
}
export function redirectSQLLab(formData) {
return dispatch => {
getChartDataRequest({ formData, resultFormat: 'json', resultType: 'query' })
.then(({ result }) => {
const redirectUrl = '/superset/sqllab';
const payload = {
datasourceKey: formData.datasource,
sql: 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,
),
);
};
}