Files
superset2/superset-frontend/src/SqlLab/actions/sqlLab.ts
2026-05-20 15:36:27 -07:00

1739 lines
51 KiB
TypeScript

/**
* 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.
*/
import { nanoid } from 'nanoid';
import rison from 'rison';
import type { AnyAction } from 'redux';
import type { ThunkAction, ThunkDispatch } from 'redux-thunk';
import type { QueryColumn, SupersetError } from '@superset-ui/core';
import {
FeatureFlag,
SupersetClient,
isFeatureEnabled,
COMMON_ERR_MESSAGES,
getClientErrorObject,
} from '@superset-ui/core';
import { t } from '@apache-superset/core/translation';
import { invert, mapKeys } from 'lodash';
import { now } from '@superset-ui/core/utils/dates';
import {
addDangerToast as addDangerToastAction,
addInfoToast as addInfoToastAction,
addSuccessToast as addSuccessToastAction,
addWarningToast as addWarningToastAction,
} from 'src/components/MessageToasts/actions';
import { LOG_ACTIONS_SQLLAB_FETCH_FAILED_QUERY } from 'src/logger/LogUtils';
import type { BootstrapData } from 'src/types/bootstrapTypes';
import getBootstrapData from 'src/utils/getBootstrapData';
import { logEvent } from 'src/logger/actions';
import type { QueryEditor, SqlLabRootState, Table } from '../types';
import { newQueryTabName } from '../utils/newQueryTabName';
import getInitialState from '../reducers/getInitialState';
import { rehydratePersistedState } from '../utils/reduxStateToLocalStorageHelper';
import { PREVIEW_QUERY_LIMIT } from '../constants';
import { EMPTY_STATE_QE_ID } from '../hooks/useQueryEditor';
// Type definitions for SqlLab actions
export interface Query {
id: string;
dbId?: number;
sql: string;
sqlEditorId?: string | null;
sqlEditorImmutableId?: string;
tab?: string | null;
catalog?: string | null;
schema?: string | null;
tempTable?: string | null;
templateParams?: string;
queryLimit?: number;
runAsync?: boolean;
ctas?: boolean;
ctas_method?: string;
isDataPreview?: boolean;
progress?: number;
startDttm?: number;
endDttm?: number;
state?: string;
cached?: boolean;
resultsKey?: string | null;
updateTabState?: boolean;
tableName?: string;
link?: string;
inLocalStorage?: boolean;
executedSql?: string;
query_id?: number;
}
export interface Database {
id: number;
allow_run_async: boolean;
disable_data_preview?: boolean;
}
/**
* Query result data from the SQL execute API (matches QueryResultSchema)
*/
export interface SqlExecuteQueryResult {
endDttm?: number;
executedSql?: string;
limit?: number;
limitingFactor?: string;
tempTable?: string | null;
progress?: number;
state?: string;
[key: string]: unknown;
}
/**
* Response from /api/v1/sqllab/execute/
* This matches QueryExecutionResponseSchema from the backend
*/
export interface SqlExecuteResponse {
status?: string;
data: Record<string, unknown>[];
columns: QueryColumn[];
selected_columns?: QueryColumn[];
expanded_columns?: QueryColumn[];
query: SqlExecuteQueryResult;
query_id?: number;
}
export const RESET_STATE = 'RESET_STATE';
export const ADD_QUERY_EDITOR = 'ADD_QUERY_EDITOR';
export const UPDATE_QUERY_EDITOR = 'UPDATE_QUERY_EDITOR';
export const QUERY_EDITOR_SAVED = 'QUERY_EDITOR_SAVED';
export const CLONE_QUERY_TO_NEW_TAB = 'CLONE_QUERY_TO_NEW_TAB';
export const REMOVE_QUERY_EDITOR = 'REMOVE_QUERY_EDITOR';
export const MERGE_TABLE = 'MERGE_TABLE';
export const REMOVE_TABLES = 'REMOVE_TABLES';
export const END_QUERY = 'END_QUERY';
export const REMOVE_QUERY = 'REMOVE_QUERY';
export const EXPAND_TABLE = 'EXPAND_TABLE';
export const COLLAPSE_TABLE = 'COLLAPSE_TABLE';
export const QUERY_EDITOR_SETDB = 'QUERY_EDITOR_SETDB';
export const QUERY_EDITOR_SET_CATALOG = 'QUERY_EDITOR_SET_CATALOG';
export const QUERY_EDITOR_SET_SCHEMA = 'QUERY_EDITOR_SET_SCHEMA';
export const QUERY_EDITOR_SET_TITLE = 'QUERY_EDITOR_SET_TITLE';
export const QUERY_EDITOR_SET_AUTORUN = 'QUERY_EDITOR_SET_AUTORUN';
export const QUERY_EDITOR_SET_SQL = 'QUERY_EDITOR_SET_SQL';
export const QUERY_EDITOR_SET_CURSOR_POSITION =
'QUERY_EDITOR_SET_CURSOR_POSITION';
export const QUERY_EDITOR_SET_QUERY_LIMIT = 'QUERY_EDITOR_SET_QUERY_LIMIT';
export const QUERY_EDITOR_SET_TEMPLATE_PARAMS =
'QUERY_EDITOR_SET_TEMPLATE_PARAMS';
export const QUERY_EDITOR_SET_SELECTED_TEXT = 'QUERY_EDITOR_SET_SELECTED_TEXT';
export const QUERY_EDITOR_SET_FUNCTION_NAMES =
'QUERY_EDITOR_SET_FUNCTION_NAMES';
export const QUERY_EDITOR_PERSIST_HEIGHT = 'QUERY_EDITOR_PERSIST_HEIGHT';
export const QUERY_EDITOR_TOGGLE_LEFT_BAR = 'QUERY_EDITOR_TOGGLE_LEFT_BAR';
export const MIGRATE_QUERY_EDITOR = 'MIGRATE_QUERY_EDITOR';
export const MIGRATE_TABLE = 'MIGRATE_TABLE';
export const MIGRATE_QUERY = 'MIGRATE_QUERY';
export const SET_DATABASES = 'SET_DATABASES';
export const SET_ACTIVE_QUERY_EDITOR = 'SET_ACTIVE_QUERY_EDITOR';
export const LOAD_QUERY_EDITOR = 'LOAD_QUERY_EDITOR';
export const SET_TABLES = 'SET_TABLES';
export const SET_ACTIVE_SOUTHPANE_TAB = 'SET_ACTIVE_SOUTHPANE_TAB';
export const REFRESH_QUERIES = 'REFRESH_QUERIES';
export const SET_USER_OFFLINE = 'SET_USER_OFFLINE';
export const RUN_QUERY = 'RUN_QUERY';
export const START_QUERY = 'START_QUERY';
export const STOP_QUERY = 'STOP_QUERY';
export const REQUEST_QUERY_RESULTS = 'REQUEST_QUERY_RESULTS';
export const QUERY_SUCCESS = 'QUERY_SUCCESS';
export const QUERY_FAILED = 'QUERY_FAILED';
export const CLEAR_INACTIVE_QUERIES = 'CLEAR_INACTIVE_QUERIES';
export const CLEAR_QUERY_RESULTS = 'CLEAR_QUERY_RESULTS';
export const REMOVE_DATA_PREVIEW = 'REMOVE_DATA_PREVIEW';
export const CHANGE_DATA_PREVIEW_ID = 'CHANGE_DATA_PREVIEW_ID';
export const COST_ESTIMATE_STARTED = 'COST_ESTIMATE_STARTED';
export const COST_ESTIMATE_RETURNED = 'COST_ESTIMATE_RETURNED';
export const COST_ESTIMATE_FAILED = 'COST_ESTIMATE_FAILED';
export const CREATE_DATASOURCE_STARTED = 'CREATE_DATASOURCE_STARTED';
export const CREATE_DATASOURCE_SUCCESS = 'CREATE_DATASOURCE_SUCCESS';
export const CREATE_DATASOURCE_FAILED = 'CREATE_DATASOURCE_FAILED';
export const SET_EDITOR_TAB_LAST_UPDATE = 'SET_EDITOR_TAB_LAST_UPDATE';
export const SET_LAST_UPDATED_ACTIVE_TAB = 'SET_LAST_UPDATED_ACTIVE_TAB';
export const CLEAR_DESTROYED_QUERY_EDITOR = 'CLEAR_DESTROYED_QUERY_EDITOR';
// SqlLab action interface with optional properties for reducer compatibility.
// Uses explicit property types instead of `any` for better type safety.
export interface SqlLabAction {
type: string;
// Query editor related
queryEditor?: QueryEditor | Partial<QueryEditor>;
alterations?: Partial<QueryEditor>;
oldQueryEditor?: QueryEditor;
newQueryEditor?: QueryEditor;
queryEditorId?: string;
// Query related
query?: Query | Partial<Query>;
queries?: Query[];
alteredQueries?: Record<string, Partial<Query>>;
queryId?: string;
// Table related
table?: Table | Partial<Table>;
tables?: Table[];
oldTable?: Table;
newTable?: Table;
// Database related
databases?: Database[] | Record<string, Database>;
dbId?: number;
// State and config
sqlLabInitialState?: SqlLabRootState['sqlLab'];
sql?: string | null;
name?: string;
catalog?: string | null;
schema?: string | null;
autorun?: boolean;
templateParams?: string;
queryLimit?: number;
position?: { row: number; column: number };
functionNames?: string[];
northPercent?: number;
southPercent?: number;
hideLeftBar?: boolean;
// Results and errors
results?: SqlExecuteResponse;
errors?: SupersetError[];
error?: string;
msg?: string;
link?: string;
err?: string;
// Misc
tabId?: string | number;
timestamp?: number;
interval?: number;
offline?: boolean;
datasource?: unknown;
clientId?: string;
result?: Record<string, unknown>;
prepend?: boolean;
json?: Record<string, unknown>;
oldQueryId?: string;
newQuery?: { id: string };
}
// Use AnyAction for ThunkAction/ThunkDispatch to maintain compatibility with
// redux-mock-store and standard Redux patterns. SqlLabAction is used for plain
// action creator return types where the shape is known.
type SqlLabThunkAction<R = void> = ThunkAction<
R,
SqlLabRootState,
undefined,
AnyAction
>;
type AppDispatch = ThunkDispatch<SqlLabRootState, undefined, AnyAction>;
type GetState = () => SqlLabRootState;
export const addInfoToast = addInfoToastAction;
export const addSuccessToast = addSuccessToastAction;
export const addDangerToast = addDangerToastAction;
export const addWarningToast = addWarningToastAction;
export const CtasEnum = {
Table: 'TABLE',
View: 'VIEW',
} as const;
const ERR_MSG_CANT_LOAD_QUERY = t("The query couldn't be loaded");
// a map of SavedQuery field names to the different names used client-side,
// because for now making the names consistent is too complicated
// so it might as well only happen in one place
const queryClientMapping: Record<string, string> = {
id: 'remoteId',
db_id: 'dbId',
label: 'name',
template_parameters: 'templateParams',
};
const queryServerMapping = invert(queryClientMapping);
// uses a mapping like those above to convert object key names to another style
const fieldConverter =
(mapping: Record<string, string>) =>
<T extends Record<string, unknown>>(obj: T): Record<string, unknown> =>
mapKeys(obj, (_value, key) => (key in mapping ? mapping[key] : key));
export const convertQueryToServer = fieldConverter(queryServerMapping);
export const convertQueryToClient = fieldConverter(queryClientMapping);
export function getUpToDateQuery(
rootState: SqlLabRootState,
queryEditor: Partial<QueryEditor>,
key?: string,
): QueryEditor {
const {
sqlLab: { unsavedQueryEditor, queryEditors },
} = rootState;
const id = key ?? queryEditor.id;
return {
id,
...queryEditors.find((qe: QueryEditor) => qe.id === id),
...(id === unsavedQueryEditor.id && unsavedQueryEditor),
} as QueryEditor;
}
export function resetState(data?: Record<string, unknown>): SqlLabThunkAction {
return (dispatch: AppDispatch, getState: GetState) => {
const { common } = getState();
const initialState = getInitialState({
...getBootstrapData(),
common,
...(data as Partial<BootstrapData>),
});
dispatch({
type: RESET_STATE,
sqlLabInitialState: initialState.sqlLab as SqlLabRootState['sqlLab'],
});
rehydratePersistedState(
dispatch,
initialState as unknown as SqlLabRootState,
);
};
}
export function updateQueryEditor(
alterations: Partial<QueryEditor>,
): SqlLabAction {
return { type: UPDATE_QUERY_EDITOR, alterations };
}
export function setEditorTabLastUpdate(timestamp: number): SqlLabAction {
return { type: SET_EDITOR_TAB_LAST_UPDATE, timestamp };
}
export function scheduleQuery(
query: Record<string, unknown>,
): SqlLabThunkAction<Promise<unknown>> {
return (dispatch: AppDispatch) =>
SupersetClient.post({
endpoint: '/api/v1/saved_query/',
jsonPayload: query,
stringify: false,
})
.then(() =>
dispatch(
addSuccessToast(
t(
'Your query has been scheduled. To see details of your query, navigate to Saved queries',
),
),
),
)
.catch(() =>
dispatch(addDangerToast(t('Your query could not be scheduled'))),
);
}
export function estimateQueryCost(
queryEditor: QueryEditor,
): SqlLabThunkAction<Promise<unknown[]>> {
return (dispatch: AppDispatch, getState: GetState) => {
const { dbId, catalog, schema, sql, selectedText, templateParams } =
getUpToDateQuery(getState(), queryEditor);
const requestSql = selectedText || sql;
const postPayload = {
database_id: dbId,
catalog,
schema,
sql: requestSql,
template_params: JSON.parse(templateParams || '{}'),
};
return Promise.all([
dispatch({ type: COST_ESTIMATE_STARTED, query: queryEditor }),
SupersetClient.post({
endpoint: '/api/v1/sqllab/estimate/',
body: JSON.stringify(postPayload),
headers: { 'Content-Type': 'application/json' },
})
.then(({ json }) =>
dispatch({ type: COST_ESTIMATE_RETURNED, query: queryEditor, json }),
)
.catch(response =>
getClientErrorObject(response).then(error => {
const message =
error.error ||
error.statusText ||
t('Failed at retrieving results');
return dispatch({
type: COST_ESTIMATE_FAILED,
query: queryEditor,
error: message,
});
}),
),
]);
};
}
export function clearInactiveQueries(interval: number): SqlLabAction {
return { type: CLEAR_INACTIVE_QUERIES, interval };
}
export function startQuery(query: Query, runPreviewOnly?: boolean) {
Object.assign(query, {
id: query.id ? query.id : nanoid(11),
progress: 0,
startDttm: now(),
state: 'pending',
cached: false,
});
return { type: START_QUERY, query, runPreviewOnly } as const;
}
export function querySuccess(query: Query, results: SqlExecuteResponse) {
return { type: QUERY_SUCCESS, query, results } as const;
}
export function logFailedQuery(
query: Query,
errors?: SupersetError[],
): SqlLabThunkAction<void> {
return function (dispatch) {
const eventData = {
has_err: true,
start_offset: query.startDttm,
ts: new Date().getTime(),
};
errors?.forEach(({ error_type: errorType, message, extra }) => {
const issueCodes = (
extra as { issue_codes?: { code: number }[] }
)?.issue_codes?.map(({ code }) => code) || [-1];
dispatch(
logEvent(LOG_ACTIONS_SQLLAB_FETCH_FAILED_QUERY, {
...eventData,
error_type: errorType,
issue_codes: issueCodes,
error_details: message,
}),
);
});
};
}
export function createQueryFailedAction(
query: Query,
msg: string,
link?: string,
errors?: SupersetError[],
) {
return { type: QUERY_FAILED, query, msg, link, errors } as const;
}
export function queryFailed(
query: Query,
msg: string,
link?: string,
errors?: SupersetError[],
): SqlLabThunkAction<void> {
return function (dispatch) {
dispatch(logFailedQuery(query, errors));
dispatch(createQueryFailedAction(query, msg, link, errors));
};
}
export function stopQuery(query: Query) {
return { type: STOP_QUERY, query } as const;
}
export function clearQueryResults(query: Query): SqlLabAction {
return { type: CLEAR_QUERY_RESULTS, query };
}
export function removeDataPreview(table: Table): SqlLabAction {
return { type: REMOVE_DATA_PREVIEW, table };
}
export function requestQueryResults(query: Query): SqlLabAction {
return { type: REQUEST_QUERY_RESULTS, query };
}
export function fetchQueryResults(
query: Query,
displayLimit?: number,
timeoutInMs?: number,
): SqlLabThunkAction<Promise<unknown>> {
return function (dispatch: AppDispatch, getState: GetState) {
const { SQLLAB_QUERY_RESULT_TIMEOUT } = getState().common?.conf ?? {};
dispatch(requestQueryResults(query));
const queryParams = rison.encode({
key: query.resultsKey,
rows: displayLimit || null,
});
const timeout = timeoutInMs ?? SQLLAB_QUERY_RESULT_TIMEOUT;
const controller = new AbortController();
return SupersetClient.get({
endpoint: `/api/v1/sqllab/results/?q=${queryParams}`,
parseMethod: 'json-bigint',
...(timeout && { timeout, signal: controller.signal }),
})
.then(({ json }) => {
dispatch(querySuccess(query, json as SqlExecuteResponse));
})
.catch(response => {
controller.abort();
getClientErrorObject(response).then(error => {
const message =
error.error ||
error.statusText ||
t('Failed at retrieving results');
return dispatch(
queryFailed(query, message, error.link, error.errors),
);
});
});
};
}
export function runQuery(
query: Query,
runPreviewOnly?: boolean,
): SqlLabThunkAction<Promise<unknown>> {
return function (dispatch: AppDispatch) {
dispatch(startQuery(query, runPreviewOnly));
const postPayload = {
client_id: query.id,
database_id: query.dbId,
runAsync: query.runAsync,
catalog: query.catalog,
schema: query.schema,
sql: query.sql,
sql_editor_id: query.sqlEditorId,
tab: query.tab,
tmp_table_name: query.tempTable,
select_as_cta: query.ctas,
ctas_method: query.ctas_method,
templateParams: query.templateParams,
queryLimit: query.queryLimit,
expand_data: true,
};
const search = window.location.search || '';
return SupersetClient.post({
endpoint: `/api/v1/sqllab/execute/${search}`,
body: JSON.stringify(postPayload),
headers: { 'Content-Type': 'application/json' },
parseMethod: 'json-bigint',
})
.then(({ json }) => {
if (!query.runAsync) {
dispatch(querySuccess(query, json as SqlExecuteResponse));
}
})
.catch(response =>
getClientErrorObject(response).then(error => {
let message =
error.error ||
error.message ||
error.statusText ||
t('Unknown error');
if (message.includes('CSRF token')) {
message = t(COMMON_ERR_MESSAGES.SESSION_TIMED_OUT);
}
dispatch(queryFailed(query, message, error.link, error.errors));
}),
);
};
}
export function runQueryFromSqlEditor(
database: Database | null,
queryEditor: QueryEditor,
defaultQueryLimit: number,
tempTable?: string,
ctas?: boolean,
ctasMethod?: string,
): SqlLabThunkAction {
return function (dispatch: AppDispatch, getState: GetState) {
const qe = getUpToDateQuery(getState(), queryEditor, queryEditor.id);
const query: Query = {
id: nanoid(11),
dbId: qe.dbId,
sql: qe.selectedText || qe.sql,
sqlEditorId: qe.tabViewId ?? qe.id,
sqlEditorImmutableId: qe.immutableId,
tab: qe.name,
catalog: qe.catalog,
schema: qe.schema,
tempTable,
templateParams: qe.templateParams,
queryLimit: qe.queryLimit || defaultQueryLimit,
runAsync: database ? database.allow_run_async : false,
ctas,
ctas_method: ctasMethod,
updateTabState: !qe.selectedText,
};
dispatch(runQuery(query));
};
}
export function reRunQuery(query: Query): SqlLabThunkAction {
// run Query with a new id
return function (dispatch: AppDispatch) {
dispatch(runQuery({ ...query, id: nanoid(11) }));
};
}
export function postStopQuery(
query: Query,
): SqlLabThunkAction<Promise<unknown>> {
return function (dispatch: AppDispatch) {
return SupersetClient.post({
endpoint: '/api/v1/query/stop',
body: JSON.stringify({ client_id: query.id }),
headers: { 'Content-Type': 'application/json' },
})
.then(() => dispatch(stopQuery(query)))
.then(() => dispatch(addSuccessToast(t('Query was stopped.'))))
.catch(() => dispatch(addDangerToast(t('Failed to stop query.'))));
};
}
export function setDatabases(databases: Database[]): SqlLabAction {
return { type: SET_DATABASES, databases };
}
function migrateTable(
table: Table,
queryEditorId: string,
dispatch: AppDispatch,
): Promise<unknown> {
return SupersetClient.post({
endpoint: encodeURI('/tableschemaview/'),
postPayload: { table: { ...table, queryEditorId } },
})
.then(({ json }) => {
const newTable = {
...table,
id: json.id,
queryEditorId,
};
return dispatch({ type: MIGRATE_TABLE, oldTable: table, newTable });
})
.catch(() =>
dispatch(
addWarningToast(
t(
'Unable to migrate table schema state to backend. Superset will retry ' +
'later. Please contact your administrator if this problem persists.',
),
),
),
);
}
function migrateQuery(
queryId: string,
queryEditorId: string,
dispatch: AppDispatch,
): Promise<unknown> {
return SupersetClient.post({
endpoint: encodeURI(`/tabstateview/${queryEditorId}/migrate_query`),
postPayload: { queryId },
})
.then(() => dispatch({ type: MIGRATE_QUERY, queryId, queryEditorId }))
.catch(() =>
dispatch(
addWarningToast(
t(
'Unable to migrate query state to backend. Superset will retry later. ' +
'Please contact your administrator if this problem persists.',
),
),
),
);
}
/**
* Persist QueryEditor from local storage to backend tab state.
* This ensures that when new tabs are created, query editors are
* asynchronously stored in local storage and periodically synchronized
* with the backend.
* When switching to persistence mode, the QueryEditors previously
* stored in local storage will also be synchronized to the backend
* through syncQueryEditor.
*/
export function syncQueryEditor(
queryEditor: QueryEditor,
): SqlLabThunkAction<Promise<unknown>> {
return function (dispatch: AppDispatch, getState: GetState) {
const { tables, queries } = getState().sqlLab;
const localStorageTables = tables.filter(
(table: Table) =>
table.inLocalStorage && table.queryEditorId === queryEditor.id,
);
const localStorageQueries = Object.values(queries).filter(
query => query.inLocalStorage && query.sqlEditorId === queryEditor.id,
);
return SupersetClient.post({
endpoint: '/tabstateview/',
postPayload: { queryEditor },
})
.then(({ json }) => {
const newQueryEditor = {
...queryEditor,
inLocalStorage: false,
loaded: true,
tabViewId: json.id.toString(),
};
dispatch({
type: MIGRATE_QUERY_EDITOR,
oldQueryEditor: queryEditor,
newQueryEditor,
});
return Promise.all([
...localStorageTables.map((table: Table) =>
migrateTable(table, newQueryEditor.tabViewId!, dispatch),
),
...localStorageQueries.map((query: Query) =>
migrateQuery(query.id, newQueryEditor.tabViewId!, dispatch),
),
]);
})
.catch(() =>
dispatch(
addWarningToast(
t(
'Unable to migrate query editor state to backend. Superset will retry ' +
'later. Please contact your administrator if this problem persists.',
),
),
),
);
};
}
export function addQueryEditor(
queryEditor: Partial<QueryEditor>,
): SqlLabAction {
const newQueryEditor = {
...queryEditor,
id: nanoid(11),
immutableId: nanoid(11),
loaded: true,
inLocalStorage: true,
};
return {
type: ADD_QUERY_EDITOR,
queryEditor: newQueryEditor,
};
}
export function addNewQueryEditor(): SqlLabThunkAction<SqlLabAction> {
return function (dispatch: AppDispatch, getState: GetState) {
const {
sqlLab: { queryEditors, tabHistory, unsavedQueryEditor, databases },
common,
} = getState();
const defaultDbId = common.conf.SQLLAB_DEFAULT_DBID as number | undefined;
const activeQueryEditor = queryEditors.find(
(qe: QueryEditor) => qe.id === tabHistory[tabHistory.length - 1],
) ?? { id: EMPTY_STATE_QE_ID };
const dbIds = Object.values(databases).map(
(database: { id: number }) => database.id,
);
const firstDbId = dbIds.length > 0 ? Math.min(...dbIds) : undefined;
const { dbId, catalog, schema, queryLimit, autorun } = {
...queryEditors[0],
...activeQueryEditor,
...(unsavedQueryEditor.id === activeQueryEditor?.id &&
unsavedQueryEditor),
} as Partial<QueryEditor>;
const warning = isFeatureEnabled(FeatureFlag.SqllabBackendPersistence)
? ''
: t(
'-- Note: Unless you save your query, these tabs will NOT persist if you clear your cookies or change browsers.\n\n',
);
const name = newQueryTabName(
queryEditors?.map((qe: QueryEditor) => ({
...qe,
...(qe.id === unsavedQueryEditor.id && unsavedQueryEditor),
})) || [],
);
return dispatch(
addQueryEditor({
dbId: dbId || defaultDbId || firstDbId,
catalog,
schema,
autorun: autorun ?? false,
sql: `${warning}SELECT ...`,
queryLimit: queryLimit || common.conf.DEFAULT_SQLLAB_LIMIT,
name,
}),
);
};
}
export function cloneQueryToNewTab(
query: Query,
autorun: boolean,
): SqlLabThunkAction<SqlLabAction> {
return function (dispatch: AppDispatch, getState: GetState) {
const state = getState();
const { queryEditors, unsavedQueryEditor, tabHistory } = state.sqlLab;
const sourceQueryEditor = {
...queryEditors.find(
(qe: QueryEditor) => qe.id === tabHistory[tabHistory.length - 1],
),
...(tabHistory[tabHistory.length - 1] === unsavedQueryEditor.id &&
unsavedQueryEditor),
};
const queryEditor = {
name: t('Copy of %s', sourceQueryEditor.name),
dbId: query.dbId,
catalog: query.catalog,
schema: query.schema ?? undefined,
autorun,
sql: query.sql,
queryLimit: sourceQueryEditor.queryLimit,
templateParams: sourceQueryEditor.templateParams,
};
return dispatch(addQueryEditor(queryEditor));
};
}
export function setLastUpdatedActiveTab(queryEditorId: string): SqlLabAction {
return {
type: SET_LAST_UPDATED_ACTIVE_TAB,
queryEditorId,
};
}
export function setActiveQueryEditor(queryEditor: QueryEditor): SqlLabAction {
return {
type: SET_ACTIVE_QUERY_EDITOR,
queryEditor,
};
}
export function switchQueryEditor(goBackward = false): SqlLabThunkAction {
return function (dispatch: AppDispatch, getState: GetState) {
const { sqlLab } = getState();
const { queryEditors, tabHistory } = sqlLab;
const qeid = tabHistory[tabHistory.length - 1];
const currentIndex = queryEditors.findIndex(
(qe: QueryEditor) => qe.id === qeid,
);
const nextIndex = goBackward
? currentIndex - 1 + queryEditors.length
: currentIndex + 1;
const newQueryEditor = queryEditors[nextIndex % queryEditors.length];
dispatch(setActiveQueryEditor(newQueryEditor));
};
}
export function loadQueryEditor(queryEditor: QueryEditor): SqlLabAction {
return { type: LOAD_QUERY_EDITOR, queryEditor };
}
export interface TableSchema {
description: {
columns: unknown[];
selectStar: string;
primaryKey: unknown;
foreignKeys: unknown[];
indexes: unknown[];
dataPreviewQueryId: string;
} | null;
database_id: number;
tab_state_id: number;
catalog: string;
schema: string;
table: string;
expanded: boolean;
id: string;
}
export function setTables(tableSchemas: TableSchema[]): SqlLabAction {
const tables = tableSchemas
.filter((tableSchema: TableSchema) => tableSchema.description !== null)
.map((tableSchema: TableSchema) => {
const {
columns,
selectStar,
primaryKey,
foreignKeys,
indexes,
dataPreviewQueryId,
} = tableSchema.description!;
return {
dbId: tableSchema.database_id,
queryEditorId: tableSchema.tab_state_id.toString(),
catalog: tableSchema.catalog,
schema: tableSchema.schema,
name: tableSchema.table,
expanded: tableSchema.expanded,
id: tableSchema.id,
dataPreviewQueryId,
columns,
selectStar,
primaryKey,
foreignKeys,
indexes,
isMetadataLoading: false,
isExtraMetadataLoading: false,
};
});
return { type: SET_TABLES, tables };
}
export function fetchQueryEditor(
queryEditor: QueryEditor,
displayLimit: number,
): SqlLabThunkAction {
return function (dispatch: AppDispatch) {
const queryEditorId = queryEditor.tabViewId ?? queryEditor.id;
SupersetClient.get({
endpoint: encodeURI(`/tabstateview/${queryEditorId}`),
})
.then(({ json }) => {
const loadedQueryEditor = {
id: json.id.toString(),
loaded: true,
name: json.label,
sql: json.sql,
selectedText: null,
latestQueryId: json.latest_query?.id,
autorun: json.autorun,
dbId: json.database_id,
templateParams: json.template_params,
catalog: json.catalog,
schema: json.schema,
queryLimit: json.query_limit,
remoteId: json.saved_query?.id,
hideLeftBar: json.hide_left_bar,
};
dispatch(loadQueryEditor(loadedQueryEditor as unknown as QueryEditor));
dispatch(setTables(json.table_schemas || []));
if (json.latest_query?.resultsKey) {
dispatch(fetchQueryResults(json.latest_query, displayLimit));
}
})
.catch(response => {
if (response.status !== 404) {
return dispatch(
addDangerToast(t('An error occurred while fetching tab state')),
);
}
return dispatch({ type: REMOVE_QUERY_EDITOR, queryEditor });
});
};
}
export function setActiveSouthPaneTab(tabId: string): SqlLabAction {
return { type: SET_ACTIVE_SOUTHPANE_TAB, tabId };
}
export function toggleLeftBar(queryEditor: QueryEditor): SqlLabAction {
const hideLeftBar = !queryEditor.hideLeftBar;
return {
type: QUERY_EDITOR_TOGGLE_LEFT_BAR,
queryEditor,
hideLeftBar,
};
}
export function clearDestoryedQueryEditor(queryEditorId: string): SqlLabAction {
return { type: CLEAR_DESTROYED_QUERY_EDITOR, queryEditorId };
}
export function removeQueryEditor(queryEditor: QueryEditor): SqlLabAction {
return { type: REMOVE_QUERY_EDITOR, queryEditor };
}
export function removeAllOtherQueryEditors(
queryEditor: QueryEditor,
): SqlLabThunkAction {
return function (dispatch: AppDispatch, getState: GetState) {
const { sqlLab } = getState();
sqlLab.queryEditors?.forEach((otherQueryEditor: QueryEditor) => {
if (otherQueryEditor.id !== queryEditor.id) {
dispatch(removeQueryEditor(otherQueryEditor));
}
});
};
}
export function removeQuery(query: Query): SqlLabThunkAction<Promise<unknown>> {
return function (dispatch: AppDispatch) {
const queryEditorId = query.sqlEditorId ?? query.id;
const sync = isFeatureEnabled(FeatureFlag.SqllabBackendPersistence)
? SupersetClient.delete({
endpoint: encodeURI(
`/tabstateview/${queryEditorId}/query/${query.id}`,
),
})
: Promise.resolve();
return sync
.then(() => dispatch({ type: REMOVE_QUERY, query }))
.catch(() =>
dispatch(
addDangerToast(
t(
'An error occurred while removing query. Please contact your administrator.',
),
),
),
);
};
}
export function queryEditorSetDb(
queryEditor: Partial<QueryEditor>,
dbId: number,
): SqlLabAction {
return { type: QUERY_EDITOR_SETDB, queryEditor, dbId };
}
export function queryEditorSetCatalog(
queryEditor: Partial<QueryEditor> | null,
catalog: string | null,
): SqlLabAction {
return {
type: QUERY_EDITOR_SET_CATALOG,
queryEditor: queryEditor || {},
catalog,
};
}
export function queryEditorSetSchema(
queryEditor: Partial<QueryEditor> | null,
schema: string | null,
): SqlLabAction {
return {
type: QUERY_EDITOR_SET_SCHEMA,
queryEditor: queryEditor || {},
schema,
};
}
export function queryEditorSetAutorun(
queryEditor: Partial<QueryEditor>,
autorun: boolean,
): SqlLabAction {
return { type: QUERY_EDITOR_SET_AUTORUN, queryEditor, autorun };
}
export function queryEditorSetTitle(
queryEditor: Partial<QueryEditor>,
name: string,
id: string,
): SqlLabAction {
return {
type: QUERY_EDITOR_SET_TITLE,
queryEditor: { ...queryEditor, id },
name,
};
}
export function saveQuery(
query: Partial<QueryEditor>,
clientId: string,
): SqlLabThunkAction<Promise<Record<string, unknown> | void>> {
const { id: _id, ...payload } = convertQueryToServer(
query as Record<string, unknown>,
);
return (dispatch: AppDispatch) =>
SupersetClient.post({
endpoint: '/api/v1/saved_query/',
jsonPayload: convertQueryToServer(payload as Record<string, unknown>),
})
.then(result => {
const savedQuery = convertQueryToClient({
id: result.json.id,
...result.json.result,
});
dispatch({
type: QUERY_EDITOR_SAVED,
query,
clientId,
result: savedQuery,
});
dispatch(queryEditorSetTitle(query, query.name ?? '', clientId));
return savedQuery;
})
.catch(() =>
dispatch(addDangerToast(t('Your query could not be saved'))),
);
}
export const addSavedQueryToTabState =
(
queryEditor: QueryEditor,
savedQuery: { remoteId: string },
): SqlLabThunkAction<Promise<unknown>> =>
(dispatch: AppDispatch) => {
const queryEditorId = queryEditor.tabViewId ?? queryEditor.id;
const sync = isFeatureEnabled(FeatureFlag.SqllabBackendPersistence)
? SupersetClient.put({
endpoint: `/tabstateview/${queryEditorId}`,
postPayload: { saved_query_id: savedQuery.remoteId },
})
: Promise.resolve();
return sync
.catch(() => {
dispatch(addDangerToast(t('Your query was not properly saved')));
})
.then(() => {
dispatch(addSuccessToast(t('Your query was saved')));
});
};
export function updateSavedQuery(
query: Partial<QueryEditor>,
clientId: string,
): SqlLabThunkAction<Promise<unknown>> {
const { id: _id, ...payload } = convertQueryToServer(
query as Record<string, unknown>,
);
return (dispatch: AppDispatch) =>
SupersetClient.put({
endpoint: `/api/v1/saved_query/${query.remoteId}`,
jsonPayload: convertQueryToServer(payload as Record<string, unknown>),
})
.then(() => {
dispatch(addSuccessToast(t('Your query was updated')));
dispatch(queryEditorSetTitle(query, query.name ?? '', clientId));
})
.catch(e => {
const message = t('Your query could not be updated');
// eslint-disable-next-line no-console
console.error(message, e);
dispatch(addDangerToast(message));
})
.then(() => {
dispatch(updateQueryEditor(query));
});
}
export function queryEditorSetSql(
queryEditor: Partial<QueryEditor>,
sql: string,
queryId?: string,
): SqlLabAction {
return { type: QUERY_EDITOR_SET_SQL, queryEditor, sql, queryId };
}
export function queryEditorSetCursorPosition(
queryEditor: Partial<QueryEditor>,
position: { row: number; column: number },
): SqlLabAction {
return { type: QUERY_EDITOR_SET_CURSOR_POSITION, queryEditor, position };
}
export function queryEditorSetAndSaveSql(
targetQueryEditor: Partial<QueryEditor>,
sql: string,
queryId?: string,
): SqlLabThunkAction<Promise<unknown>> {
return function (dispatch: AppDispatch, getState: GetState) {
const queryEditor = getUpToDateQuery(getState(), targetQueryEditor);
// saved query and set tab state use this action
dispatch(queryEditorSetSql(queryEditor, sql, queryId));
const queryEditorId = queryEditor.tabViewId ?? queryEditor.id;
if (isFeatureEnabled(FeatureFlag.SqllabBackendPersistence)) {
return SupersetClient.put({
endpoint: encodeURI(`/tabstateview/${queryEditorId}`),
postPayload: { sql, latest_query_id: queryId },
}).catch(() =>
dispatch(
addDangerToast(
t(
'An error occurred while storing your query in the backend. To ' +
'avoid losing your changes, please save your query using the ' +
'"Save Query" button.',
),
),
),
);
}
return Promise.resolve();
};
}
export function formatQuery(
queryEditor: QueryEditor,
): SqlLabThunkAction<Promise<unknown>> {
return function (dispatch: AppDispatch, getState: GetState) {
const { sql, dbId, templateParams } = getUpToDateQuery(
getState(),
queryEditor,
);
const body: {
sql: string;
database_id?: number;
template_params?: string;
} = { sql };
// Include database_id and template_params if available for Jinja processing
if (dbId) {
body.database_id = dbId;
}
if (templateParams) {
// Send templateParams as a JSON string to match the backend schema
body.template_params =
typeof templateParams === 'string'
? templateParams
: JSON.stringify(templateParams);
}
return SupersetClient.post({
endpoint: `/api/v1/sqllab/format_sql/`,
body: JSON.stringify(body),
headers: { 'Content-Type': 'application/json' },
}).then(({ json }) => {
dispatch(queryEditorSetSql(queryEditor, json.result));
});
};
}
export function queryEditorSetQueryLimit(
queryEditor: Partial<QueryEditor>,
queryLimit: number,
): SqlLabAction {
return {
type: QUERY_EDITOR_SET_QUERY_LIMIT,
queryEditor,
queryLimit,
};
}
export function queryEditorSetTemplateParams(
queryEditor: Partial<QueryEditor>,
templateParams: string,
): SqlLabAction {
return {
type: QUERY_EDITOR_SET_TEMPLATE_PARAMS,
queryEditor,
templateParams,
};
}
export function queryEditorSetSelectedText(
queryEditor: Partial<QueryEditor>,
sql: string | null,
): SqlLabAction {
return { type: QUERY_EDITOR_SET_SELECTED_TEXT, queryEditor, sql };
}
export function mergeTable(
table: Partial<Table>,
query?: Query,
prepend?: boolean,
): SqlLabAction {
return { type: MERGE_TABLE, table, query, prepend };
}
export function addTable(
queryEditor: Partial<QueryEditor>,
tableName: string,
catalogName: string | null,
schemaName: string,
expanded = true,
): SqlLabThunkAction {
return function (dispatch: AppDispatch, getState: GetState) {
const { dbId } = getUpToDateQuery(getState(), queryEditor, queryEditor.id);
const table = {
dbId,
queryEditorId: queryEditor.tabViewId ?? queryEditor.id,
catalog: catalogName,
schema: schemaName,
name: tableName,
};
dispatch(
mergeTable({
...table,
id: nanoid(11),
expanded,
}),
);
};
}
export interface NewTable {
id?: string;
dbId: number | string;
catalog?: string | null;
schema?: string;
name: string;
queryEditorId?: string;
selectStar?: string;
previewQueryId?: string;
}
export function runTablePreviewQuery(
newTable: NewTable,
runPreviewOnly?: boolean,
): SqlLabThunkAction<Promise<unknown>> {
return function (dispatch: AppDispatch, getState: GetState) {
const {
sqlLab: { databases },
} = getState();
const database = databases[newTable.dbId];
const { dbId, catalog, schema } = newTable;
if (database && !database.disable_data_preview) {
const dataPreviewQuery: Query = {
id: newTable.previewQueryId ?? nanoid(11),
dbId: dbId as number | undefined,
catalog: catalog ?? undefined,
schema: schema ?? '',
sql: newTable.selectStar ?? '',
tableName: newTable.name,
sqlEditorId: null,
tab: '',
runAsync: database.allow_run_async,
ctas: false,
isDataPreview: true,
queryLimit: PREVIEW_QUERY_LIMIT,
};
if (runPreviewOnly) {
return dispatch(runQuery(dataPreviewQuery, runPreviewOnly));
}
return Promise.all([
dispatch(
mergeTable(
{
id: newTable.id,
dbId: newTable.dbId as number | undefined,
catalog: newTable.catalog ?? undefined,
schema: newTable.schema ?? '',
name: newTable.name,
queryEditorId: newTable.queryEditorId,
dataPreviewQueryId: dataPreviewQuery.id,
},
dataPreviewQuery,
),
),
dispatch(runQuery(dataPreviewQuery)),
]);
}
return Promise.resolve();
};
}
export interface TableMetaData {
columns?: unknown[];
selectStar?: string;
primaryKey?: unknown;
foreignKeys?: unknown[];
indexes?: unknown[];
}
export function syncTable(
table: Table,
tableMetadata: TableMetaData,
finalQueryEditorId?: string,
): SqlLabThunkAction<Promise<unknown>> {
return function (dispatch: AppDispatch) {
const finalTable = { ...table, queryEditorId: finalQueryEditorId };
const sync = isFeatureEnabled(FeatureFlag.SqllabBackendPersistence)
? SupersetClient.post({
endpoint: encodeURI('/tableschemaview/'),
postPayload: { table: { ...tableMetadata, ...finalTable } },
})
: Promise.resolve({ json: { id: table.id } });
return sync
.then(({ json: resultJson }) => {
const newTable = { ...table, id: `${resultJson.id}` };
dispatch(
mergeTable({
...newTable,
expanded: true,
initialized: true,
}),
);
})
.catch(() =>
dispatch(
addDangerToast(
t(
'An error occurred while fetching table metadata. ' +
'Please contact your administrator.',
),
),
),
);
};
}
export function changeDataPreviewId(
oldQueryId: string,
newQuery: Query,
): SqlLabAction {
return { type: CHANGE_DATA_PREVIEW_ID, oldQueryId, newQuery };
}
export function reFetchQueryResults(query: Query): SqlLabThunkAction {
return function (dispatch: AppDispatch) {
const newQuery: Query = {
id: nanoid(),
dbId: query.dbId,
sql: query.sql,
tableName: query.tableName,
sqlEditorId: null,
tab: '',
runAsync: false,
ctas: false,
queryLimit: query.queryLimit,
isDataPreview: query.isDataPreview,
};
dispatch(runQuery(newQuery));
dispatch(changeDataPreviewId(query.id, newQuery));
};
}
export function expandTable(table: Table): SqlLabThunkAction<Promise<unknown>> {
return function (dispatch: AppDispatch) {
const sync =
isFeatureEnabled(FeatureFlag.SqllabBackendPersistence) &&
table.initialized
? SupersetClient.post({
endpoint: encodeURI(`/tableschemaview/${table.id}/expanded`),
postPayload: { expanded: true },
})
: Promise.resolve();
return sync
.then(() => dispatch({ type: EXPAND_TABLE, table }))
.catch(() =>
dispatch(
addDangerToast(
t(
'An error occurred while expanding the table schema. ' +
'Please contact your administrator.',
),
),
),
);
};
}
export function collapseTable(
table: Table,
): SqlLabThunkAction<Promise<unknown>> {
return function (dispatch: AppDispatch) {
const sync =
isFeatureEnabled(FeatureFlag.SqllabBackendPersistence) &&
table.initialized
? SupersetClient.post({
endpoint: encodeURI(`/tableschemaview/${table.id}/expanded`),
postPayload: { expanded: false },
})
: Promise.resolve();
return sync
.then(() => dispatch({ type: COLLAPSE_TABLE, table }))
.catch(() =>
dispatch(
addDangerToast(
t(
'An error occurred while collapsing the table schema. ' +
'Please contact your administrator.',
),
),
),
);
};
}
export function removeTables(
tables: Table[],
): SqlLabThunkAction<Promise<unknown>> {
return function (dispatch: AppDispatch) {
const tablesToRemove = tables?.filter(Boolean) ?? [];
const sync = isFeatureEnabled(FeatureFlag.SqllabBackendPersistence)
? Promise.all(
tablesToRemove.map((table: Table) =>
table.initialized
? SupersetClient.delete({
endpoint: encodeURI(`/tableschemaview/${table.id}`),
})
: Promise.resolve(),
),
)
: Promise.resolve();
return sync
.then(() => dispatch({ type: REMOVE_TABLES, tables: tablesToRemove }))
.catch(() =>
dispatch(
addDangerToast(
t(
'An error occurred while removing the table schema. ' +
'Please contact your administrator.',
),
),
),
);
};
}
export function refreshQueries(
alteredQueries: Record<string, Query>,
): SqlLabAction {
return { type: REFRESH_QUERIES, alteredQueries };
}
export function setUserOffline(offline: boolean): SqlLabAction {
return { type: SET_USER_OFFLINE, offline };
}
export function persistEditorHeight(
queryEditor: QueryEditor,
northPercent: number,
southPercent: number,
): SqlLabAction {
return {
type: QUERY_EDITOR_PERSIST_HEIGHT,
queryEditor,
northPercent,
southPercent,
};
}
export function popPermalink(key: string): SqlLabThunkAction<Promise<unknown>> {
return function (dispatch: AppDispatch) {
return SupersetClient.get({ endpoint: `/api/v1/sqllab/permalink/${key}` })
.then(({ json }) =>
dispatch(
addQueryEditor({
name: json.name ? json.name : t('Shared query'),
dbId: json.dbId ? parseInt(json.dbId, 10) : undefined,
catalog: json.catalog ?? null,
schema: json.schema ?? undefined,
autorun: json.autorun ? json.autorun : false,
sql: json.sql ? json.sql : 'SELECT ...',
templateParams: json.templateParams,
}),
),
)
.catch(() => dispatch(addDangerToast(ERR_MSG_CANT_LOAD_QUERY)));
};
}
export function popStoredQuery(
urlId: string,
): SqlLabThunkAction<Promise<unknown>> {
return function (dispatch: AppDispatch) {
return SupersetClient.get({
endpoint: `/api/v1/sqllab/permalink/kv:${urlId}`,
})
.then(({ json }) =>
dispatch(
addQueryEditor({
name: json.name ? json.name : t('Shared query'),
dbId: json.dbId ? parseInt(json.dbId, 10) : undefined,
catalog: json.catalog ?? null,
schema: json.schema ?? undefined,
autorun: json.autorun ? json.autorun : false,
sql: json.sql ? json.sql : 'SELECT ...',
templateParams: json.templateParams,
}),
),
)
.catch(() => dispatch(addDangerToast(ERR_MSG_CANT_LOAD_QUERY)));
};
}
export function popSavedQuery(
saveQueryId: string,
): SqlLabThunkAction<Promise<unknown>> {
return function (dispatch: AppDispatch) {
return SupersetClient.get({
endpoint: `/api/v1/saved_query/${saveQueryId}`,
})
.then(({ json }) => {
const queryEditorProps = {
...convertQueryToClient(json.result),
loaded: true,
autorun: false,
} as Record<string, unknown>;
const tmpAdaptedProps = {
name: queryEditorProps.name as string,
dbId: (queryEditorProps.database as { id: number }).id,
catalog: queryEditorProps.catalog as string,
schema: queryEditorProps.schema as string,
sql: queryEditorProps.sql as string,
templateParams: queryEditorProps.templateParams as string,
remoteId: queryEditorProps.remoteId as number | null,
};
return dispatch(addQueryEditor(tmpAdaptedProps));
})
.catch(() => dispatch(addDangerToast(ERR_MSG_CANT_LOAD_QUERY)));
};
}
export function popQuery(queryId: string): SqlLabThunkAction<Promise<unknown>> {
return function (dispatch: AppDispatch) {
return SupersetClient.get({
endpoint: `/api/v1/query/${queryId}`,
})
.then(({ json }) => {
const queryData = json.result;
const queryEditorProps = {
dbId: queryData.database.id,
catalog: queryData.catalog,
schema: queryData.schema,
sql: queryData.sql,
name: t('Copy of %s', queryData.tab_name),
autorun: false,
};
return dispatch(addQueryEditor(queryEditorProps));
})
.catch(() => dispatch(addDangerToast(ERR_MSG_CANT_LOAD_QUERY)));
};
}
export function popDatasourceQuery(
datasourceKey: string,
sql?: string,
): SqlLabThunkAction<Promise<unknown>> {
return function (dispatch: AppDispatch) {
const QUERY_TEXT = t('Query');
const datasetId = datasourceKey.split('__')[0];
const queryParams = rison.encode({
keys: ['none'],
columns: ['name', 'schema', 'database.id', 'select_star'],
});
return SupersetClient.get({
endpoint: `/api/v1/dataset/${datasetId}?q=${queryParams}`,
})
.then(({ json }) =>
dispatch(
addQueryEditor({
name: `${QUERY_TEXT} ${json.result.name}`,
dbId: json.result.database.id,
schema: json.result.schema,
autorun: sql !== undefined,
sql: sql || json.result.select_star,
}),
),
)
.catch(() =>
dispatch(addDangerToast(t("The datasource couldn't be loaded"))),
);
};
}
export function createDatasourceStarted(): SqlLabAction {
return { type: CREATE_DATASOURCE_STARTED };
}
export function createDatasourceSuccess(data: { id: number }): SqlLabAction {
const datasource = `${data.id}__table`;
return { type: CREATE_DATASOURCE_SUCCESS, datasource };
}
export function createDatasourceFailed(err: string): SqlLabAction {
return { type: CREATE_DATASOURCE_FAILED, err };
}
export interface VizOptions {
dbId: number;
catalog?: string | null;
schema: string;
datasourceName: string;
sql: string;
templateParams?: string;
}
export function createDatasource(
vizOptions: VizOptions,
): SqlLabThunkAction<Promise<unknown>> {
return (dispatch: AppDispatch) => {
dispatch(createDatasourceStarted());
const { dbId, catalog, schema, datasourceName, sql, templateParams } =
vizOptions;
return SupersetClient.post({
endpoint: '/api/v1/dataset/',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
database: dbId,
catalog,
schema,
sql,
table_name: datasourceName,
is_managed_externally: false,
external_url: null,
template_params: templateParams,
}),
})
.then(({ json }) => {
dispatch(createDatasourceSuccess(json as { id: number }));
return Promise.resolve(json);
})
.catch(error => {
getClientErrorObject(error).then(e => {
dispatch(addDangerToast(e.error));
});
dispatch(
createDatasourceFailed(
t('An error occurred while creating the data source'),
),
);
return Promise.reject();
});
};
}
export function createCtasDatasource(
vizOptions: Record<string, unknown>,
): SqlLabThunkAction<Promise<{ table_id: number }>> {
return (dispatch: AppDispatch) => {
dispatch(createDatasourceStarted());
return SupersetClient.post({
endpoint: '/api/v1/dataset/get_or_create/',
jsonPayload: vizOptions,
})
.then(({ json }) => {
const result = json.result as { table_id: number };
// The endpoint's `result.table_id` IS the dataset id; normalize so
// createDatasourceSuccess's `${data.id}__table` resolves correctly.
// Without this, the CTAS Explore button silently produced
// `"undefined__table"` because `result.id` doesn't exist.
dispatch(createDatasourceSuccess({ id: result.table_id }));
return result;
})
.catch(() => {
const errorMsg = t('An error occurred while creating the data source');
dispatch(createDatasourceFailed(errorMsg));
return Promise.reject(new Error(errorMsg));
});
};
}