This is an automated email from the ASF dual-hosted git repository. michaelsmolina pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/superset.git
The following commit(s) were added to refs/heads/master by this push: new e729b2dbb4 fix: SQL Lab tab events (#35105) e729b2dbb4 is described below commit e729b2dbb42ada7feb6484a2236be73932b630bc Author: Michael S. Molina <70410625+michael-s-mol...@users.noreply.github.com> AuthorDate: Thu Sep 11 17:53:51 2025 -0300 fix: SQL Lab tab events (#35105) --- superset-frontend/src/SqlLab/actions/sqlLab.js | 2 + .../src/SqlLab/actions/sqlLab.test.js | 4 ++ superset-frontend/src/SqlLab/fixtures.ts | 4 ++ .../src/SqlLab/reducers/getInitialState.ts | 4 ++ superset-frontend/src/SqlLab/types.ts | 1 + .../SqlLab/utils/reduxStateToLocalStorageHelper.ts | 1 + superset-frontend/src/core/sqlLab.ts | 63 ++++++++++++++++------ .../src/hooks/apiResources/sqlEditorTabs.test.ts | 1 + 8 files changed, 65 insertions(+), 15 deletions(-) diff --git a/superset-frontend/src/SqlLab/actions/sqlLab.js b/superset-frontend/src/SqlLab/actions/sqlLab.js index 1bff50b5b5..019a1fe3ef 100644 --- a/superset-frontend/src/SqlLab/actions/sqlLab.js +++ b/superset-frontend/src/SqlLab/actions/sqlLab.js @@ -395,6 +395,7 @@ export function runQueryFromSqlEditor( dbId: qe.dbId, sql: qe.selectedText || qe.sql, sqlEditorId: qe.tabViewId ?? qe.id, + immutableId: qe.immutableId, tab: qe.name, catalog: qe.catalog, schema: qe.schema, @@ -537,6 +538,7 @@ export function addQueryEditor(queryEditor) { const newQueryEditor = { ...queryEditor, id: nanoid(11), + immutableId: nanoid(11), loaded: true, inLocalStorage: true, }; diff --git a/superset-frontend/src/SqlLab/actions/sqlLab.test.js b/superset-frontend/src/SqlLab/actions/sqlLab.test.js index 4f344c973b..0280a283a6 100644 --- a/superset-frontend/src/SqlLab/actions/sqlLab.test.js +++ b/superset-frontend/src/SqlLab/actions/sqlLab.test.js @@ -441,6 +441,7 @@ describe('async actions', () => { queryLimit: undefined, maxRow: undefined, id: 'abcd', + immutableId: 'abcd', templateParams: undefined, inLocalStorage: true, loaded: true, @@ -570,6 +571,7 @@ describe('async actions', () => { type: actions.ADD_QUERY_EDITOR, queryEditor: { ...queryEditor, + immutableId: 'abcd', inLocalStorage: true, loaded: true, }, @@ -597,6 +599,7 @@ describe('async actions', () => { type: actions.ADD_QUERY_EDITOR, queryEditor: { id: 'abcd', + immutableId: 'abcd', sql: expect.stringContaining('SELECT ...'), name: `Untitled Query 7`, dbId: defaultQueryEditor.dbId, @@ -753,6 +756,7 @@ describe('async actions', () => { queryEditor: { ...queryEditor, id: 'abcd', + immutableId: 'abcd', loaded: true, inLocalStorage: true, }, diff --git a/superset-frontend/src/SqlLab/fixtures.ts b/superset-frontend/src/SqlLab/fixtures.ts index 9ac8bed6e7..3ebc1db598 100644 --- a/superset-frontend/src/SqlLab/fixtures.ts +++ b/superset-frontend/src/SqlLab/fixtures.ts @@ -188,6 +188,7 @@ export const table = { export const defaultQueryEditor = { version: LatestQueryEditorVersion, id: 'dfsadfs', + immutableId: 'immutable-id', autorun: false, dbId: 1, latestQueryId: null, @@ -204,6 +205,7 @@ export const defaultQueryEditor = { export const extraQueryEditor1 = { ...defaultQueryEditor, id: 'diekd23', + immutableId: 'immutable-id', sql: 'SELECT *\nFROM\nWHERE\nLIMIT', name: 'Untitled Query 2', selectedText: 'SELECT', @@ -212,6 +214,7 @@ export const extraQueryEditor1 = { export const extraQueryEditor2 = { ...defaultQueryEditor, id: 'owkdi998', + immutableId: 'immutable-id', sql: '', name: 'Untitled Query 3', }; @@ -219,6 +222,7 @@ export const extraQueryEditor2 = { export const extraQueryEditor3 = { ...defaultQueryEditor, id: 'kvk23', + immutableId: 'immutable-id', sql: '', name: 'Untitled Query 4', tabViewId: 37, diff --git a/superset-frontend/src/SqlLab/reducers/getInitialState.ts b/superset-frontend/src/SqlLab/reducers/getInitialState.ts index 91386e3fe6..7d9f0fddac 100644 --- a/superset-frontend/src/SqlLab/reducers/getInitialState.ts +++ b/superset-frontend/src/SqlLab/reducers/getInitialState.ts @@ -17,6 +17,7 @@ * under the License. */ import { t } from '@superset-ui/core'; +import { nanoid } from 'nanoid'; import type { BootstrapData } from 'src/types/bootstrapTypes'; import type { InitialState } from 'src/hooks/apiResources/sqlLab'; import { @@ -55,6 +56,7 @@ export default function getInitialState({ let queryEditors: Record<string, QueryEditor> = {}; const defaultQueryEditor = { version: LatestQueryEditorVersion, + immutableId: nanoid(11), loaded: true, name: t('Untitled query'), sql: '', @@ -78,6 +80,7 @@ export default function getInitialState({ queryEditor = { version: activeTab.extra_json?.version ?? QueryEditorVersion.V1, id: id.toString(), + immutableId: activeTab.extra_json?.immutableId ?? nanoid(11), loaded: true, name: activeTab.label, sql: activeTab.sql || '', @@ -100,6 +103,7 @@ export default function getInitialState({ queryEditor = { ...defaultQueryEditor, id: id.toString(), + immutableId: nanoid(11), loaded: false, name: label, dbId: undefined, diff --git a/superset-frontend/src/SqlLab/types.ts b/superset-frontend/src/SqlLab/types.ts index 9665a88fae..5532f155b7 100644 --- a/superset-frontend/src/SqlLab/types.ts +++ b/superset-frontend/src/SqlLab/types.ts @@ -49,6 +49,7 @@ export interface CursorPosition { export interface QueryEditor { version: QueryEditorVersion; id: string; + immutableId: string; dbId?: number; name: string; title?: string; // keep it optional for backward compatibility diff --git a/superset-frontend/src/SqlLab/utils/reduxStateToLocalStorageHelper.ts b/superset-frontend/src/SqlLab/utils/reduxStateToLocalStorageHelper.ts index e8e1e15673..683b082b38 100644 --- a/superset-frontend/src/SqlLab/utils/reduxStateToLocalStorageHelper.ts +++ b/superset-frontend/src/SqlLab/utils/reduxStateToLocalStorageHelper.ts @@ -45,6 +45,7 @@ const PERSISTENT_QUERY_EDITOR_KEYS = new Set([ 'dbId', 'height', 'id', + 'immutableId', 'latestQueryId', 'northPercent', 'queryLimit', diff --git a/superset-frontend/src/core/sqlLab.ts b/superset-frontend/src/core/sqlLab.ts index e59bb65742..03321e4565 100644 --- a/superset-frontend/src/core/sqlLab.ts +++ b/superset-frontend/src/core/sqlLab.ts @@ -197,6 +197,16 @@ export class QueryErrorResultContext } } +const getActiveEditorImmutableId = () => { + const { sqlLab }: { sqlLab: SqlLabRootState['sqlLab'] } = store.getState(); + const { queryEditors, tabHistory } = sqlLab; + const activeEditorId = tabHistory[tabHistory.length - 1]; + const activeEditor = queryEditors.find( + editor => editor.id === activeEditorId, + ); + return activeEditor?.immutableId; +}; + const activeEditorId = () => { const { sqlLab }: { sqlLab: SqlLabRootState['sqlLab'] } = store.getState(); const { tabHistory } = sqlLab; @@ -225,19 +235,40 @@ const getCurrentTab: typeof sqlLabType.getCurrentTab = () => { return undefined; }; -const predicate = ( - actionType: string, - currentTabOnly: boolean = true, -): AnyListenerPredicate<RootState> => { - // Uses closure to capture the active editor ID at the time the listener is created - const id = activeEditorId(); - return action => - // Compares the original id with the current active editor ID - action.type === actionType && (!currentTabOnly || activeEditorId() === id); +const predicate = (actionType: string): AnyListenerPredicate<RootState> => { + // Capture the immutable ID of the active editor at the time the listener is created + // This ID never changes for a tab, ensuring stable event routing + const registrationImmutableId = getActiveEditorImmutableId(); + + return action => { + if (action.type !== actionType) return false; + + // If we don't have a registration ID, don't filter events + if (!registrationImmutableId) return true; + + // For query events, use the immutableId directly from the action payload + if (action.query?.immutableId) { + return action.query.immutableId === registrationImmutableId; + } + + // For tab events, we need to find the immutable ID of the affected tab + if (action.queryEditor?.id) { + const { sqlLab }: { sqlLab: SqlLabRootState['sqlLab'] } = + store.getState(); + const { queryEditors } = sqlLab; + const queryEditor = queryEditors.find( + editor => editor.id === action.queryEditor.id, + ); + return queryEditor?.immutableId === registrationImmutableId; + } + + // Fallback: do not allow the event if we can't determine the source + return false; + }; }; export const onDidQueryRun: typeof sqlLabType.onDidQueryRun = ( - listener: (editor: sqlLabType.QueryContext) => void, + listener: (queryContext: sqlLabType.QueryContext) => void, thisArgs?: any, ): Disposable => createActionListener( @@ -272,11 +303,11 @@ export const onDidQueryRun: typeof sqlLabType.onDidQueryRun = ( ); export const onDidQuerySuccess: typeof sqlLabType.onDidQuerySuccess = ( - listener: (query: sqlLabType.QueryResultContext) => void, + listener: (queryResultContext: sqlLabType.QueryResultContext) => void, thisArgs?: any, ): Disposable => createActionListener( - predicate(QUERY_SUCCESS, false), + predicate(QUERY_SUCCESS), listener, (action: ReturnType<typeof querySuccess>) => { const { query, results } = action; @@ -323,7 +354,7 @@ export const onDidQuerySuccess: typeof sqlLabType.onDidQuerySuccess = ( ); export const onDidQueryStop: typeof sqlLabType.onDidQueryStop = ( - listener: (query: sqlLabType.QueryContext) => void, + listener: (queryContext: sqlLabType.QueryContext) => void, thisArgs?: any, ): Disposable => createActionListener( @@ -356,11 +387,13 @@ export const onDidQueryStop: typeof sqlLabType.onDidQueryStop = ( ); export const onDidQueryFail: typeof sqlLabType.onDidQueryFail = ( - listener: (query: sqlLabType.QueryErrorResultContext) => void, + listener: ( + queryErrorResultContext: sqlLabType.QueryErrorResultContext, + ) => void, thisArgs?: any, ): Disposable => createActionListener( - predicate(QUERY_FAILED, false), + predicate(QUERY_FAILED), listener, (action: ReturnType<typeof createQueryFailedAction>) => { const { query, msg: errorMessage, errors } = action; diff --git a/superset-frontend/src/hooks/apiResources/sqlEditorTabs.test.ts b/superset-frontend/src/hooks/apiResources/sqlEditorTabs.test.ts index 42f738f3c9..5d6c9a55a9 100644 --- a/superset-frontend/src/hooks/apiResources/sqlEditorTabs.test.ts +++ b/superset-frontend/src/hooks/apiResources/sqlEditorTabs.test.ts @@ -33,6 +33,7 @@ import { const expectedQueryEditor = { version: LatestQueryEditorVersion, id: '123', + immutableId: 'immutable-id', dbId: 456, name: 'tab 1', sql: 'SELECT * from example_table',