diff --git a/superset-frontend/src/SqlLab/actions/sqlLab.ts b/superset-frontend/src/SqlLab/actions/sqlLab.ts index 883f50d40f1..302f0b82cb0 100644 --- a/superset-frontend/src/SqlLab/actions/sqlLab.ts +++ b/superset-frontend/src/SqlLab/actions/sqlLab.ts @@ -1712,7 +1712,7 @@ export function createDatasource( export function createCtasDatasource( vizOptions: Record, -): SqlLabThunkAction> { +): SqlLabThunkAction> { return (dispatch: AppDispatch) => { dispatch(createDatasourceStarted()); return SupersetClient.post({ @@ -1720,9 +1720,14 @@ export function createCtasDatasource( jsonPayload: vizOptions, }) .then(({ json }) => { - dispatch(createDatasourceSuccess(json.result)); + 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 json.result; + return result; }) .catch(() => { const errorMsg = t('An error occurred while creating the data source'); diff --git a/superset-frontend/src/SqlLab/components/EditorAutoSync/index.tsx b/superset-frontend/src/SqlLab/components/EditorAutoSync/index.tsx index 0a13bcf9c12..a0e1da1d7d0 100644 --- a/superset-frontend/src/SqlLab/components/EditorAutoSync/index.tsx +++ b/superset-frontend/src/SqlLab/components/EditorAutoSync/index.tsx @@ -19,7 +19,8 @@ import { useRef, useEffect, FC, useMemo } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; +import { useSelector } from 'react-redux'; +import { useAppDispatch } from 'src/views/store'; import { logging } from '@apache-superset/core/utils'; import { SqlLabRootState, @@ -86,7 +87,7 @@ const EditorAutoSync: FC = () => { const editorTabLastUpdatedAt = useSelector( state => state.sqlLab.editorTabLastUpdatedAt, ); - const dispatch = useDispatch(); + const dispatch = useAppDispatch(); const lastSavedTimestampRef = useRef(editorTabLastUpdatedAt); const currentQueryEditorId = useSelector( diff --git a/superset-frontend/src/SqlLab/components/EditorWrapper/index.tsx b/superset-frontend/src/SqlLab/components/EditorWrapper/index.tsx index aafaa234402..3a28a534b54 100644 --- a/superset-frontend/src/SqlLab/components/EditorWrapper/index.tsx +++ b/superset-frontend/src/SqlLab/components/EditorWrapper/index.tsx @@ -17,7 +17,8 @@ * under the License. */ import { useState, useEffect, useRef, useCallback, useMemo } from 'react'; -import { shallowEqual, useDispatch, useSelector } from 'react-redux'; +import { shallowEqual, useSelector } from 'react-redux'; +import { useAppDispatch } from 'src/views/store'; import { usePrevious } from '@superset-ui/core'; import { css, useTheme } from '@apache-superset/core/theme'; import { Global } from '@emotion/react'; @@ -136,7 +137,7 @@ const EditorWrapper = ({ height, hotkeys, }: EditorWrapperProps) => { - const dispatch = useDispatch(); + const dispatch = useAppDispatch(); const queryEditor = useQueryEditor(queryEditorId, [ 'id', 'dbId', diff --git a/superset-frontend/src/SqlLab/components/EditorWrapper/useKeywords.ts b/superset-frontend/src/SqlLab/components/EditorWrapper/useKeywords.ts index fa1a1974be9..ee0bdcb5a9b 100644 --- a/superset-frontend/src/SqlLab/components/EditorWrapper/useKeywords.ts +++ b/superset-frontend/src/SqlLab/components/EditorWrapper/useKeywords.ts @@ -17,7 +17,8 @@ * under the License. */ import { useEffect, useMemo, useRef } from 'react'; -import { useDispatch, useStore } from 'react-redux'; +import { useStore } from 'react-redux'; +import { useAppDispatch } from 'src/views/store'; import { t } from '@apache-superset/core/translation'; import { getExtensionsRegistry } from '@superset-ui/core'; @@ -68,7 +69,7 @@ export function useKeywords( catalog, schema, }); - const dispatch = useDispatch(); + const dispatch = useAppDispatch(); const hasFetchedKeywords = useRef(false); // skipFetch is used to prevent re-evaluating memoized keywords // due to updated api results by skip flag diff --git a/superset-frontend/src/SqlLab/components/ExploreCtasResultsButton/index.tsx b/superset-frontend/src/SqlLab/components/ExploreCtasResultsButton/index.tsx index b7f6978dde8..4cb4bcc0718 100644 --- a/superset-frontend/src/SqlLab/components/ExploreCtasResultsButton/index.tsx +++ b/superset-frontend/src/SqlLab/components/ExploreCtasResultsButton/index.tsx @@ -16,9 +16,10 @@ * specific language governing permissions and limitations * under the License. */ -import { useSelector, useDispatch } from 'react-redux'; +import { useSelector } from 'react-redux'; +import { useAppDispatch } from 'src/views/store'; import { t } from '@apache-superset/core/translation'; -import { JsonObject, VizType } from '@superset-ui/core'; +import { VizType } from '@superset-ui/core'; import { createCtasDatasource, addInfoToast, @@ -45,7 +46,7 @@ const ExploreCtasResultsButton = ({ const errorMessage = useSelector( (state: SqlLabRootState) => state.sqlLab.errorMessage, ); - const dispatch = useDispatch<(dispatch: any) => Promise>(); + const dispatch = useAppDispatch(); const buildVizOptions = { table_name: table, @@ -56,7 +57,7 @@ const ExploreCtasResultsButton = ({ const visualize = () => { dispatch(createCtasDatasource(buildVizOptions)) - .then((data: { table_id: number }) => { + .then(data => { const formData = { datasource: `${data.table_id}__table`, metrics: ['count'], diff --git a/superset-frontend/src/SqlLab/components/PopEditorTab/index.tsx b/superset-frontend/src/SqlLab/components/PopEditorTab/index.tsx index 4258cd5899a..24fecab9470 100644 --- a/superset-frontend/src/SqlLab/components/PopEditorTab/index.tsx +++ b/superset-frontend/src/SqlLab/components/PopEditorTab/index.tsx @@ -17,7 +17,8 @@ * under the License. */ import { useEffect, useState } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; +import { useSelector } from 'react-redux'; +import { useAppDispatch } from 'src/views/store'; import URI from 'urijs'; import { pick } from 'lodash'; import { useComponentDidUpdate } from '@superset-ui/core'; @@ -49,7 +50,7 @@ const PopEditorTab: React.FC<{ children?: React.ReactNode }> = ({ ({ sqlLab: { tabHistory } }) => tabHistory.slice(-1)[0], ); const [updatedUrl, setUpdatedUrl] = useState(SQL_LAB_URL); - const dispatch = useDispatch(); + const dispatch = useAppDispatch(); useComponentDidUpdate(() => { setQueryEditorId(assigned => assigned ?? activeQueryEditorId); if (activeQueryEditorId) { diff --git a/superset-frontend/src/SqlLab/components/QueryAutoRefresh/index.tsx b/superset-frontend/src/SqlLab/components/QueryAutoRefresh/index.tsx index 2bf1aca2843..ac1777ea3fe 100644 --- a/superset-frontend/src/SqlLab/components/QueryAutoRefresh/index.tsx +++ b/superset-frontend/src/SqlLab/components/QueryAutoRefresh/index.tsx @@ -17,7 +17,8 @@ * under the License. */ import { useRef } from 'react'; -import { useSelector, useDispatch } from 'react-redux'; +import { useSelector } from 'react-redux'; +import { useAppDispatch } from 'src/views/store'; import { isObject } from 'lodash'; import rison from 'rison'; import { @@ -82,7 +83,7 @@ function QueryAutoRefresh({ .map(({ id }) => id), ), ); - const dispatch = useDispatch(); + const dispatch = useAppDispatch(); const checkForRefresh = () => { const shouldRequestChecking = shouldCheckForQueries(queries); diff --git a/superset-frontend/src/SqlLab/components/QueryLimitSelect/index.tsx b/superset-frontend/src/SqlLab/components/QueryLimitSelect/index.tsx index 4c2803b7753..9812ff227e3 100644 --- a/superset-frontend/src/SqlLab/components/QueryLimitSelect/index.tsx +++ b/superset-frontend/src/SqlLab/components/QueryLimitSelect/index.tsx @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { useDispatch } from 'react-redux'; +import { useAppDispatch } from 'src/views/store'; import { t } from '@apache-superset/core/translation'; import { Dropdown, Button } from '@superset-ui/core/components'; import { Menu } from '@superset-ui/core/components/Menu'; @@ -75,7 +75,7 @@ const QueryLimitSelect = ({ maxRow, defaultQueryLimit, }: QueryLimitSelectProps) => { - const dispatch = useDispatch(); + const dispatch = useAppDispatch(); const queryEditor = useQueryEditor(queryEditorId, ['id', 'queryLimit']); const queryLimit = queryEditor.queryLimit || defaultQueryLimit; diff --git a/superset-frontend/src/SqlLab/components/QueryTable/index.tsx b/superset-frontend/src/SqlLab/components/QueryTable/index.tsx index 04f40378947..33c4342a5aa 100644 --- a/superset-frontend/src/SqlLab/components/QueryTable/index.tsx +++ b/superset-frontend/src/SqlLab/components/QueryTable/index.tsx @@ -30,7 +30,8 @@ import ProgressBar from '@superset-ui/core/components/ProgressBar'; import { t } from '@apache-superset/core/translation'; import { QueryResponse, QueryState } from '@superset-ui/core'; import { useTheme } from '@apache-superset/core/theme'; -import { shallowEqual, useDispatch, useSelector } from 'react-redux'; +import { shallowEqual, useSelector } from 'react-redux'; +import { useAppDispatch } from 'src/views/store'; import { queryEditorSetSql, @@ -92,7 +93,7 @@ const QueryTable = ({ latestQueryId, }: QueryTableProps) => { const theme = useTheme(); - const dispatch = useDispatch(); + const dispatch = useAppDispatch(); const [selectedQuery, setSelectedQuery] = useState( null, ); diff --git a/superset-frontend/src/SqlLab/components/ResultSet/index.tsx b/superset-frontend/src/SqlLab/components/ResultSet/index.tsx index d7c42fc2b2c..5637e3c940a 100644 --- a/superset-frontend/src/SqlLab/components/ResultSet/index.tsx +++ b/superset-frontend/src/SqlLab/components/ResultSet/index.tsx @@ -27,7 +27,8 @@ import { } from 'react'; import AutoSizer from 'react-virtualized-auto-sizer'; -import { shallowEqual, useDispatch, useSelector } from 'react-redux'; +import { shallowEqual, useSelector } from 'react-redux'; +import { useAppDispatch } from 'src/views/store'; import { useHistory } from 'react-router-dom'; import { pick } from 'lodash'; import { @@ -231,7 +232,7 @@ const ResultSet = ({ canCopyClipboardSqlLab: canCopyClipboard, } = usePermissions(); const history = useHistory(); - const dispatch = useDispatch(); + const dispatch = useAppDispatch(); const logAction = useLogAction({ queryId, sqlEditorId: query.sqlEditorId }); const { showConfirm, ConfirmModal } = useConfirmModal(); diff --git a/superset-frontend/src/SqlLab/components/SouthPane/index.tsx b/superset-frontend/src/SqlLab/components/SouthPane/index.tsx index 02e16d49c37..8094527eeba 100644 --- a/superset-frontend/src/SqlLab/components/SouthPane/index.tsx +++ b/superset-frontend/src/SqlLab/components/SouthPane/index.tsx @@ -17,7 +17,8 @@ * under the License. */ import { createRef, useCallback, useMemo } from 'react'; -import { shallowEqual, useDispatch, useSelector } from 'react-redux'; +import { shallowEqual, useSelector } from 'react-redux'; +import { useAppDispatch } from 'src/views/store'; import { nanoid } from 'nanoid'; import Tabs from '@superset-ui/core/components/Tabs'; import { t } from '@apache-superset/core/translation'; @@ -105,7 +106,7 @@ const SouthPane = ({ const { id, tabViewId } = useQueryEditor(queryEditorId, ['tabViewId']); const editorId = tabViewId ?? id; const theme = useTheme(); - const dispatch = useDispatch(); + const dispatch = useAppDispatch(); const viewItems = views.getViews(ViewLocations.sqllab.panels) || []; const { offline, tables } = useSelector( ({ sqlLab: { offline, tables } }: SqlLabRootState) => ({ diff --git a/superset-frontend/src/SqlLab/components/SqlEditor/index.tsx b/superset-frontend/src/SqlLab/components/SqlEditor/index.tsx index c8efac1e9bb..271f87538c7 100644 --- a/superset-frontend/src/SqlLab/components/SqlEditor/index.tsx +++ b/superset-frontend/src/SqlLab/components/SqlEditor/index.tsx @@ -30,7 +30,8 @@ import { import type { editors } from '@apache-superset/core'; import useEffectEvent from 'src/hooks/useEffectEvent'; -import { shallowEqual, useDispatch, useSelector } from 'react-redux'; +import { shallowEqual, useSelector } from 'react-redux'; +import { useAppDispatch } from 'src/views/store'; import AutoSizer from 'react-virtualized-auto-sizer'; import { t } from '@apache-superset/core/translation'; import { @@ -237,7 +238,7 @@ const SqlEditor: FC = ({ scheduleQueryWarning, }) => { const theme = useTheme(); - const dispatch = useDispatch(); + const dispatch = useAppDispatch(); const { database, latestQuery, currentQueryEditorId, hasSqlStatement } = useSelector< diff --git a/superset-frontend/src/SqlLab/components/SqlEditorLeftBar/index.tsx b/superset-frontend/src/SqlLab/components/SqlEditorLeftBar/index.tsx index e4297629271..7dfe60f289f 100644 --- a/superset-frontend/src/SqlLab/components/SqlEditorLeftBar/index.tsx +++ b/superset-frontend/src/SqlLab/components/SqlEditorLeftBar/index.tsx @@ -17,7 +17,7 @@ * under the License. */ import { useCallback, useState } from 'react'; -import { useDispatch } from 'react-redux'; +import { useAppDispatch } from 'src/views/store'; import { resetState } from 'src/SqlLab/actions/sqlLab'; import { @@ -69,7 +69,7 @@ const SqlEditorLeftBar = ({ queryEditorId }: SqlEditorLeftBarProps) => { const { db, catalog, schema, onDbChange, onCatalogChange, onSchemaChange } = dbSelectorProps; - const dispatch = useDispatch(); + const dispatch = useAppDispatch(); const shouldShowReset = window.location.search === '?reset=1'; // Modal state for Database/Catalog/Schema selector diff --git a/superset-frontend/src/SqlLab/components/SqlEditorTabHeader/index.tsx b/superset-frontend/src/SqlLab/components/SqlEditorTabHeader/index.tsx index 6753612a8e9..3f707d25957 100644 --- a/superset-frontend/src/SqlLab/components/SqlEditorTabHeader/index.tsx +++ b/superset-frontend/src/SqlLab/components/SqlEditorTabHeader/index.tsx @@ -19,7 +19,8 @@ import { useMemo, FC } from 'react'; import { bindActionCreators } from 'redux'; -import { useSelector, useDispatch, shallowEqual } from 'react-redux'; +import { useSelector, shallowEqual } from 'react-redux'; +import { useAppDispatch } from 'src/views/store'; import { MenuDotsDropdown } from '@superset-ui/core/components'; import { Menu, MenuItemType } from '@superset-ui/core/components/Menu'; import { t } from '@apache-superset/core/translation'; @@ -90,7 +91,7 @@ const SqlEditorTabHeader: FC = ({ queryEditor }) => { ); const StatusIcon = queryState ? STATE_ICONS[queryState] : STATE_ICONS.running; - const dispatch = useDispatch(); + const dispatch = useAppDispatch(); const actions = useMemo( () => bindActionCreators( diff --git a/superset-frontend/src/SqlLab/components/SqlEditorTopBar/useDatabaseSelector.ts b/superset-frontend/src/SqlLab/components/SqlEditorTopBar/useDatabaseSelector.ts index ee561e19c61..83d444418e3 100644 --- a/superset-frontend/src/SqlLab/components/SqlEditorTopBar/useDatabaseSelector.ts +++ b/superset-frontend/src/SqlLab/components/SqlEditorTopBar/useDatabaseSelector.ts @@ -17,7 +17,8 @@ * under the License. */ import { useEffect, useCallback, useMemo, useState } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; +import { useSelector } from 'react-redux'; +import { useAppDispatch } from 'src/views/store'; import { SqlLabRootState } from 'src/SqlLab/types'; import { @@ -41,7 +42,7 @@ export default function useDatabaseSelector(queryEditorId: string) { SqlLabRootState, SqlLabRootState['sqlLab']['databases'] >(({ sqlLab }) => sqlLab.databases); - const dispatch = useDispatch(); + const dispatch = useAppDispatch(); const queryEditor = useQueryEditor(queryEditorId, [ 'dbId', 'catalog', diff --git a/superset-frontend/src/SqlLab/components/TableElement/index.tsx b/superset-frontend/src/SqlLab/components/TableElement/index.tsx index 710f2eb4bdc..8ca87460387 100644 --- a/superset-frontend/src/SqlLab/components/TableElement/index.tsx +++ b/superset-frontend/src/SqlLab/components/TableElement/index.tsx @@ -17,7 +17,8 @@ * under the License. */ import { useState, useRef, useEffect } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; +import { useSelector } from 'react-redux'; +import { useAppDispatch } from 'src/views/store'; import type { QueryEditor, SqlLabRootState, Table } from 'src/SqlLab/types'; import { ButtonGroup, @@ -75,7 +76,7 @@ const Fade = styled.div` const TableElement = ({ table, ...props }: TableElementProps) => { const { dbId, catalog, schema, name, expanded, id } = table; const theme = useTheme(); - const dispatch = useDispatch(); + const dispatch = useAppDispatch(); const { currentData: tableMetadata, isSuccess: isMetadataSuccess, diff --git a/superset-frontend/src/SqlLab/components/TableExploreTree/index.tsx b/superset-frontend/src/SqlLab/components/TableExploreTree/index.tsx index 60ce2a2b47c..d6804771b7a 100644 --- a/superset-frontend/src/SqlLab/components/TableExploreTree/index.tsx +++ b/superset-frontend/src/SqlLab/components/TableExploreTree/index.tsx @@ -25,7 +25,8 @@ import { type ChangeEvent, useMemo, } from 'react'; -import { useSelector, useDispatch, shallowEqual } from 'react-redux'; +import { useSelector, shallowEqual } from 'react-redux'; +import { useAppDispatch } from 'src/views/store'; import { styled, css, useTheme } from '@apache-superset/core/theme'; import { t } from '@apache-superset/core/translation'; import AutoSizer from 'react-virtualized-auto-sizer'; @@ -163,7 +164,7 @@ const savePinnedSchemasToStorage = ( }; const TableExploreTree: React.FC = ({ queryEditorId }) => { - const dispatch = useDispatch(); + const dispatch = useAppDispatch(); const theme = useTheme(); const treeRef = useRef>(null); const tables = useSelector( diff --git a/superset-frontend/src/SqlLab/components/TableExploreTree/useTreeData.ts b/superset-frontend/src/SqlLab/components/TableExploreTree/useTreeData.ts index a61ea25c6ca..239f1f19c1c 100644 --- a/superset-frontend/src/SqlLab/components/TableExploreTree/useTreeData.ts +++ b/superset-frontend/src/SqlLab/components/TableExploreTree/useTreeData.ts @@ -17,7 +17,7 @@ * under the License. */ import { useMemo, useReducer, useCallback } from 'react'; -import { useDispatch } from 'react-redux'; +import { useAppDispatch } from 'src/views/store'; import { t } from '@apache-superset/core/translation'; import { Table, @@ -130,7 +130,7 @@ const useTreeData = ({ catalog, pinnedTables, }: UseTreeDataParams): UseTreeDataResult => { - const reduxDispatch = useDispatch(); + const reduxDispatch = useAppDispatch(); // Schema data from API const { currentData: schemaData, diff --git a/superset-frontend/src/SqlLab/components/TablePreview/index.tsx b/superset-frontend/src/SqlLab/components/TablePreview/index.tsx index 9fbcc3c1264..9e9b72c0c4c 100644 --- a/superset-frontend/src/SqlLab/components/TablePreview/index.tsx +++ b/superset-frontend/src/SqlLab/components/TablePreview/index.tsx @@ -17,7 +17,8 @@ * under the License. */ import { type FC, useCallback, useMemo, useRef, useState } from 'react'; -import { shallowEqual, useDispatch, useSelector } from 'react-redux'; +import { shallowEqual, useSelector } from 'react-redux'; +import { useAppDispatch } from 'src/views/store'; import { nanoid } from 'nanoid'; import { t } from '@apache-superset/core/translation'; import { ClientErrorObject, getExtensionsRegistry } from '@superset-ui/core'; @@ -110,7 +111,7 @@ const renderWell = (partitions: TableMetaData['partitions']) => { }; const TablePreview: FC = ({ dbId, catalog, schema, tableName }) => { - const dispatch = useDispatch(); + const dispatch = useAppDispatch(); const theme = useTheme(); const [databaseName, backend, disableDataPreview] = useSelector< SqlLabRootState, diff --git a/superset-frontend/src/views/store.ts b/superset-frontend/src/views/store.ts index e6e996f6aba..bf03cf35c02 100644 --- a/superset-frontend/src/views/store.ts +++ b/superset-frontend/src/views/store.ts @@ -22,12 +22,13 @@ import { createListenerMiddleware, StoreEnhancer, } from '@reduxjs/toolkit'; +import type { AnyAction } from 'redux'; import { useDispatch, useSelector, type TypedUseSelectorHook, } from 'react-redux'; -import thunk from 'redux-thunk'; +import thunk, { type ThunkDispatch } from 'redux-thunk'; import { api } from 'src/hooks/apiResources/queryApi'; import messageToastReducer from 'src/components/MessageToasts/reducers'; import charts from 'src/components/Chart/chartReducer'; @@ -188,6 +189,14 @@ export type RootState = ReturnType; // thunks resolve correctly), and `useAppSelector` infers `RootState` without // callers having to annotate every selector. Required ahead of the // react-redux v8+ bump, which tightens dispatch typing — see #39927. -export type AppDispatch = typeof store.dispatch; +// +// AppDispatch is declared as ThunkDispatch & store.dispatch rather than +// `typeof store.dispatch` because Superset annotates getMiddleware as +// ConfigureStoreOptions['middleware'], which erases the middleware tuple type +// and leaves store.dispatch typed as Dispatch. The intersection +// restores thunk support without requiring a wider refactor of the middleware +// setup. +export type AppDispatch = ThunkDispatch & + typeof store.dispatch; export const useAppDispatch: () => AppDispatch = useDispatch; export const useAppSelector: TypedUseSelectorHook = useSelector;