From bc6859a99d137989bb8180393c3968a10bd2c5ed Mon Sep 17 00:00:00 2001 From: "Michael S. Molina" <70410625+michael-s-molina@users.noreply.github.com> Date: Tue, 16 Sep 2025 08:21:16 -0300 Subject: [PATCH] refactor: Organizes the src/core folder (#35119) --- .../packages/superset-core/package.json | 2 +- .../index.ts} | 0 .../core/{commands.ts => commands/index.ts} | 2 +- .../{environment.ts => environment/index.ts} | 0 .../{extensions.ts => extensions/index.ts} | 0 superset-frontend/src/core/index.ts | 25 +- .../src/core/{core.ts => models.ts} | 73 +-- superset-frontend/src/core/sqlLab.ts | 537 ------------------ superset-frontend/src/core/sqlLab/index.ts | 343 +++++++++++ superset-frontend/src/core/sqlLab/models.ts | 235 ++++++++ .../src/extensions/ExtensionsManager.ts | 2 +- 11 files changed, 606 insertions(+), 613 deletions(-) rename superset-frontend/src/core/{authentication.ts => authentication/index.ts} (100%) rename superset-frontend/src/core/{commands.ts => commands/index.ts} (98%) rename superset-frontend/src/core/{environment.ts => environment/index.ts} (100%) rename superset-frontend/src/core/{extensions.ts => extensions/index.ts} (100%) rename superset-frontend/src/core/{core.ts => models.ts} (62%) delete mode 100644 superset-frontend/src/core/sqlLab.ts create mode 100644 superset-frontend/src/core/sqlLab/index.ts create mode 100644 superset-frontend/src/core/sqlLab/models.ts diff --git a/superset-frontend/packages/superset-core/package.json b/superset-frontend/packages/superset-core/package.json index f7c5865f648..ddbefb9c414 100644 --- a/superset-frontend/packages/superset-core/package.json +++ b/superset-frontend/packages/superset-core/package.json @@ -1,6 +1,6 @@ { "name": "@apache-superset/core", - "version": "0.0.1-rc3", + "version": "0.0.1-rc4", "description": "This package contains UI elements, APIs, and utility functions used by Superset.", "sideEffects": false, "main": "lib/index.js", diff --git a/superset-frontend/src/core/authentication.ts b/superset-frontend/src/core/authentication/index.ts similarity index 100% rename from superset-frontend/src/core/authentication.ts rename to superset-frontend/src/core/authentication/index.ts diff --git a/superset-frontend/src/core/commands.ts b/superset-frontend/src/core/commands/index.ts similarity index 98% rename from superset-frontend/src/core/commands.ts rename to superset-frontend/src/core/commands/index.ts index c507754839a..ec48eb9f147 100644 --- a/superset-frontend/src/core/commands.ts +++ b/superset-frontend/src/core/commands/index.ts @@ -18,7 +18,7 @@ */ import { logging } from '@superset-ui/core'; import type { commands as commandsType } from '@apache-superset/core'; -import { Disposable } from './core'; +import { Disposable } from '../models'; const commandRegistry: Map any> = new Map(); diff --git a/superset-frontend/src/core/environment.ts b/superset-frontend/src/core/environment/index.ts similarity index 100% rename from superset-frontend/src/core/environment.ts rename to superset-frontend/src/core/environment/index.ts diff --git a/superset-frontend/src/core/extensions.ts b/superset-frontend/src/core/extensions/index.ts similarity index 100% rename from superset-frontend/src/core/extensions.ts rename to superset-frontend/src/core/extensions/index.ts diff --git a/superset-frontend/src/core/index.ts b/superset-frontend/src/core/index.ts index e5cd32b2f94..32080bd9490 100644 --- a/superset-frontend/src/core/index.ts +++ b/superset-frontend/src/core/index.ts @@ -16,9 +16,32 @@ * specific language governing permissions and limitations * under the License. */ +import { core as coreType } from '@apache-superset/core'; +import { getExtensionsContextValue } from '../extensions/ExtensionsContextUtils'; +import { Disposable } from './models'; + +export const registerViewProvider: typeof coreType.registerViewProvider = ( + id, + viewProvider, +) => { + const { registerViewProvider: register, unregisterViewProvider: unregister } = + getExtensionsContextValue(); + register(id, viewProvider); + return new Disposable(() => unregister(id)); +}; + +const { GenericDataType } = coreType; + +export const core: typeof coreType = { + GenericDataType, + registerViewProvider, + Disposable, +}; + export * from './authentication'; -export * from './core'; export * from './commands'; export * from './extensions'; export * from './environment'; +export * from './models'; export * from './sqlLab'; +export * from './utils'; diff --git a/superset-frontend/src/core/core.ts b/superset-frontend/src/core/models.ts similarity index 62% rename from superset-frontend/src/core/core.ts rename to superset-frontend/src/core/models.ts index 14c87d06a0d..c0a4b721aaa 100644 --- a/superset-frontend/src/core/core.ts +++ b/superset-frontend/src/core/models.ts @@ -16,10 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { core as coreType, sqlLab as sqlLabType } from '@apache-superset/core'; -import { getExtensionsContextValue } from '../extensions/ExtensionsContextUtils'; - -const { GenericDataType } = coreType; +import { core as coreType } from '@apache-superset/core'; export class Table implements coreType.Table { name: string; @@ -114,71 +111,3 @@ export class Disposable implements coreType.Disposable { export class ExtensionContext implements coreType.ExtensionContext { disposables: coreType.Disposable[] = []; } - -export class Panel implements sqlLabType.Panel { - id: string; - - constructor(id: string) { - this.id = id; - } -} - -export class Editor implements sqlLabType.Editor { - content: string; - - databaseId: number; - - schema: string; - - // TODO: Check later if we'll use objects instead of strings. - catalog: string | null; - - table: string | null; - - constructor( - content: string, - databaseId: number, - catalog: string | null = null, - schema = '', - table: string | null = null, - ) { - this.content = content; - this.databaseId = databaseId; - this.catalog = catalog; - this.schema = schema; - this.table = table; - } -} - -export class Tab implements sqlLabType.Tab { - id: string; - - title: string; - - editor: Editor; - - panels: Panel[]; - - constructor(id: string, title: string, editor: Editor, panels: Panel[] = []) { - this.id = id; - this.title = title; - this.editor = editor; - this.panels = panels; - } -} - -const registerViewProvider: typeof coreType.registerViewProvider = ( - id, - viewProvider, -) => { - const { registerViewProvider: register, unregisterViewProvider: unregister } = - getExtensionsContextValue(); - register(id, viewProvider); - return new Disposable(() => unregister(id)); -}; - -export const core: typeof coreType = { - GenericDataType, - registerViewProvider, - Disposable, -}; diff --git a/superset-frontend/src/core/sqlLab.ts b/superset-frontend/src/core/sqlLab.ts deleted file mode 100644 index 03321e45651..00000000000 --- a/superset-frontend/src/core/sqlLab.ts +++ /dev/null @@ -1,537 +0,0 @@ -/** - * 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 { sqlLab as sqlLabType, core as coreType } from '@apache-superset/core'; -import { - QUERY_FAILED, - QUERY_SUCCESS, - QUERY_EDITOR_SETDB, - querySuccess, - startQuery, - START_QUERY, - stopQuery, - STOP_QUERY, - createQueryFailedAction, -} from 'src/SqlLab/actions/sqlLab'; -import { RootState, store } from 'src/views/store'; -import { AnyListenerPredicate } from '@reduxjs/toolkit'; -import type { SqlLabRootState } from 'src/SqlLab/types'; -import { Disposable, Editor, Panel, Tab } from './core'; -import { createActionListener } from './utils'; - -const { CTASMethod } = sqlLabType; - -export class CTAS implements sqlLabType.CTAS { - method: sqlLabType.CTASMethod; - - tempTable: string; - - constructor(asView: boolean, tempTable: string) { - this.method = asView ? CTASMethod.View : CTASMethod.Table; - this.tempTable = tempTable; - } -} - -export class QueryContext implements sqlLabType.QueryContext { - clientId: string; - - ctas: sqlLabType.CTAS | null; - - editor: Editor; - - requestedLimit: number | null; - - runAsync: boolean; - - startDttm: number; - - tab: Tab; - - private templateParams: string; - - private parsedParams: Record; - - constructor( - clientId: string, - tab: Tab, - runAsync: boolean, - startDttm: number, - options: { - templateParams?: string; - ctasMethod?: string; - tempTable?: string; - requestedLimit?: number; - } = {}, - ) { - this.clientId = clientId; - this.tab = tab; - this.runAsync = runAsync; - this.startDttm = startDttm; - this.requestedLimit = options.requestedLimit ?? null; - this.ctas = options.tempTable - ? new CTAS(options.ctasMethod === CTASMethod.View, options.tempTable) - : null; - this.templateParams = options.templateParams ?? ''; - } - - /** - * A custom accessor is used to process JSON parsing only - * when necessary for better performance. - */ - get templateParameters() { - if (this.parsedParams) { - return this.parsedParams; - } - - let parsed = {}; - try { - parsed = JSON.parse(this.templateParams); - } catch (e) { - // ignore invalid format string. - } - this.parsedParams = parsed; - - return parsed; - } -} - -export class QueryResultContext - extends QueryContext - implements sqlLabType.QueryResultContext -{ - appliedLimit: number; - - appliedLimitingFactor: string; - - endDttm: number; - - executedSql: string; - - remoteId: number; - - result: sqlLabType.QueryResult; - - constructor( - clientId: string, - remoteId: number, - executedSql: string, - columns: sqlLabType.QueryResult['columns'], - data: sqlLabType.QueryResult['data'], - tab: Tab, - runAsync: boolean, - startDttm: number, - endDttm: number, - options: { - appliedLimit?: number; - appliedLimitingFactor?: string; - templateParams?: string; - ctasMethod?: string; - tempTable?: string; - requestedLimit?: number; - } = {}, - ) { - const { appliedLimit, appliedLimitingFactor, ...opt } = options; - super(clientId, tab, runAsync, startDttm, opt); - this.remoteId = remoteId; - this.executedSql = executedSql; - this.endDttm = endDttm; - this.result = { - columns, - data, - }; - this.appliedLimit = appliedLimit ?? data.length; - this.appliedLimitingFactor = options.appliedLimitingFactor ?? ''; - } -} - -export class QueryErrorResultContext - extends QueryContext - implements sqlLabType.QueryErrorResultContext -{ - endDttm: number; - - errorMessage: string; - - errors: coreType.SupersetError[] | null; - - executedSql: string | null; - - constructor( - clientId: string, - errorMessage: string, - errors: coreType.SupersetError[], - tab: Tab, - runAsync: boolean, - startDttm: number, - options: { - ctasMethod?: string; - executedSql?: string; - endDttm?: number; - templateParams?: string; - tempTable?: string; - requestedLimist?: number; - queryId?: number; - } = {}, - ) { - const { queryId, executedSql, endDttm, ...opt } = options; - super(clientId, tab, runAsync, startDttm, opt); - this.executedSql = executedSql ?? null; - this.errorMessage = errorMessage; - this.errors = errors; - this.endDttm = endDttm ?? Date.now(); - } -} - -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; - return tabHistory[tabHistory.length - 1]; -}; - -const getCurrentTab: typeof sqlLabType.getCurrentTab = () => { - const { sqlLab }: { sqlLab: SqlLabRootState['sqlLab'] } = store.getState(); - const { queryEditors } = sqlLab; - const queryEditor = queryEditors.find( - editor => editor.id === activeEditorId(), - ); - if (queryEditor) { - const { id, name } = queryEditor; - const editor = new Editor( - queryEditor.sql, - queryEditor.dbId!, - queryEditor.catalog, - queryEditor.schema, - null, // TODO: Populate table if needed - ); - const panels: Panel[] = []; // TODO: Populate panels - - return new Tab(id, name, editor, panels); - } - return undefined; -}; - -const predicate = (actionType: string): AnyListenerPredicate => { - // 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: (queryContext: sqlLabType.QueryContext) => void, - thisArgs?: any, -): Disposable => - createActionListener( - predicate(START_QUERY), - listener, - (action: ReturnType) => { - const { query } = action; - const { - id, - dbId, - catalog, - schema, - sql, - startDttm, - ctas_method: ctasMethod, - runAsync, - tempTable, - templateParams, - queryLimit, - } = query; - const editor = new Editor(sql, dbId, catalog, schema); - const panels: Panel[] = []; // TODO: Populate panels - const tab = new Tab(query.sqlEditorId, query.tab, editor, panels); - return new QueryContext(id, tab, runAsync, startDttm, { - ctasMethod, - tempTable, - templateParams, - requestedLimit: queryLimit, - }); - }, - thisArgs, - ); - -export const onDidQuerySuccess: typeof sqlLabType.onDidQuerySuccess = ( - listener: (queryResultContext: sqlLabType.QueryResultContext) => void, - thisArgs?: any, -): Disposable => - createActionListener( - predicate(QUERY_SUCCESS), - listener, - (action: ReturnType) => { - const { query, results } = action; - const { - id, - dbId, - catalog, - schema, - sql, - startDttm, - ctas_method: ctasMethod, - runAsync, - templateParams, - } = query; - const { - query_id: queryId, - columns, - data, - query: { endDttm, executedSql, tempTable, limit, limitingFactor }, - } = results; - const editor = new Editor(sql, dbId, catalog, schema); - const panels: Panel[] = []; // TODO: Populate panels - const tab = new Tab(query.sqlEditorId, query.tab, editor, panels); - return new QueryResultContext( - id, - queryId, - executedSql ?? sql, - columns, - data, - tab, - runAsync, - startDttm, - endDttm, - { - ctasMethod, - tempTable, - templateParams, - appliedLimit: limit, - appliedLimitingFactor: limitingFactor, - }, - ); - }, - thisArgs, - ); - -export const onDidQueryStop: typeof sqlLabType.onDidQueryStop = ( - listener: (queryContext: sqlLabType.QueryContext) => void, - thisArgs?: any, -): Disposable => - createActionListener( - predicate(STOP_QUERY), - listener, - (action: ReturnType) => { - const { query } = action; - const { - id, - dbId, - catalog, - schema, - sql, - startDttm, - ctas_method: ctasMethod, - runAsync, - tempTable, - templateParams, - } = query; - const editor = new Editor(sql, dbId, catalog, schema); - const panels: Panel[] = []; // TODO: Populate panels - const tab = new Tab(query.sqlEditorId, query.tab, editor, panels); - return new QueryContext(id, tab, runAsync, startDttm, { - ctasMethod, - tempTable, - templateParams, - }); - }, - thisArgs, - ); - -export const onDidQueryFail: typeof sqlLabType.onDidQueryFail = ( - listener: ( - queryErrorResultContext: sqlLabType.QueryErrorResultContext, - ) => void, - thisArgs?: any, -): Disposable => - createActionListener( - predicate(QUERY_FAILED), - listener, - (action: ReturnType) => { - const { query, msg: errorMessage, errors } = action; - const { - id, - dbId, - catalog, - endDttm, - executedSql, - schema, - sql, - startDttm, - ctas_method: ctasMethod, - runAsync, - templateParams, - query_id: queryId, - tempTable, - } = query; - const editor = new Editor(sql, dbId, catalog, schema); - const panels: Panel[] = []; // TODO: Populate panels - const tab = new Tab(query.sqlEditorId, query.tab, editor, panels); - return new QueryErrorResultContext( - id, - errorMessage, - errors, - tab, - runAsync, - startDttm, - { - queryId, - executedSql, - endDttm, - ctasMethod, - tempTable, - templateParams, - }, - ); - }, - thisArgs, - ); - -export const onDidChangeEditorDatabase: typeof sqlLabType.onDidChangeEditorDatabase = - (listener: (e: number) => void, thisArgs?: any): Disposable => - createActionListener( - predicate(QUERY_EDITOR_SETDB), - listener, - (action: { - type: string; - dbId?: number; - queryEditor: { dbId: number }; - }) => action.dbId || action.queryEditor.dbId, - thisArgs, - ); - -const onDidChangeEditorContent: typeof sqlLabType.onDidChangeEditorContent = - () => { - throw new Error('Not implemented yet'); - }; - -const onDidChangeEditorCatalog: typeof sqlLabType.onDidChangeEditorCatalog = - () => { - throw new Error('Not implemented yet'); - }; - -const onDidChangeEditorSchema: typeof sqlLabType.onDidChangeEditorSchema = - () => { - throw new Error('Not implemented yet'); - }; - -const onDidChangeEditorTable: typeof sqlLabType.onDidChangeEditorTable = () => { - throw new Error('Not implemented yet'); -}; - -const onDidClosePanel: typeof sqlLabType.onDidClosePanel = () => { - throw new Error('Not implemented yet'); -}; - -const onDidChangeActivePanel: typeof sqlLabType.onDidChangeActivePanel = () => { - throw new Error('Not implemented yet'); -}; - -const onDidChangeTabTitle: typeof sqlLabType.onDidChangeTabTitle = () => { - throw new Error('Not implemented yet'); -}; - -const getDatabases: typeof sqlLabType.getDatabases = () => { - throw new Error('Not implemented yet'); -}; - -const getTabs: typeof sqlLabType.getTabs = () => { - throw new Error('Not implemented yet'); -}; - -const onDidCloseTab: typeof sqlLabType.onDidCloseTab = () => { - throw new Error('Not implemented yet'); -}; - -const onDidChangeActiveTab: typeof sqlLabType.onDidChangeActiveTab = () => { - throw new Error('Not implemented yet'); -}; - -const onDidRefreshDatabases: typeof sqlLabType.onDidRefreshDatabases = () => { - throw new Error('Not implemented yet'); -}; - -const onDidRefreshCatalogs: typeof sqlLabType.onDidRefreshCatalogs = () => { - throw new Error('Not implemented yet'); -}; - -const onDidRefreshSchemas: typeof sqlLabType.onDidRefreshSchemas = () => { - throw new Error('Not implemented yet'); -}; - -const onDidRefreshTables: typeof sqlLabType.onDidRefreshTables = () => { - throw new Error('Not implemented yet'); -}; - -export const sqlLab: typeof sqlLabType = { - CTASMethod, - getCurrentTab, - onDidChangeEditorContent, - onDidChangeEditorDatabase, - onDidChangeEditorCatalog, - onDidChangeEditorSchema, - onDidChangeEditorTable, - onDidClosePanel, - onDidChangeActivePanel, - onDidChangeTabTitle, - onDidQueryRun, - onDidQueryStop, - onDidQueryFail, - onDidQuerySuccess, - getDatabases, - getTabs, - onDidCloseTab, - onDidChangeActiveTab, - onDidRefreshDatabases, - onDidRefreshCatalogs, - onDidRefreshSchemas, - onDidRefreshTables, -}; diff --git a/superset-frontend/src/core/sqlLab/index.ts b/superset-frontend/src/core/sqlLab/index.ts new file mode 100644 index 00000000000..a81c32572b0 --- /dev/null +++ b/superset-frontend/src/core/sqlLab/index.ts @@ -0,0 +1,343 @@ +/** + * 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 { sqlLab as sqlLabType } from '@apache-superset/core'; +import { + QUERY_FAILED, + QUERY_SUCCESS, + QUERY_EDITOR_SETDB, + querySuccess, + startQuery, + START_QUERY, + stopQuery, + STOP_QUERY, + createQueryFailedAction, +} from 'src/SqlLab/actions/sqlLab'; +import { RootState, store } from 'src/views/store'; +import { AnyListenerPredicate } from '@reduxjs/toolkit'; +import memoizeOne from 'memoize-one'; +import type { SqlLabRootState } from 'src/SqlLab/types'; +import { Disposable } from '../models'; +import { createActionListener } from '../utils'; +import { + Panel, + Editor, + Tab, + QueryContext, + QueryResultContext, + QueryErrorResultContext, +} from './models'; + +const { CTASMethod } = sqlLabType; + +const getSqlLabState = () => { + const { sqlLab }: { sqlLab: SqlLabRootState['sqlLab'] } = store.getState(); + return sqlLab; +}; + +const activeEditorId = () => { + const { tabHistory } = getSqlLabState(); + return tabHistory[tabHistory.length - 1]; +}; + +const findQueryEditor = (editorId: string) => { + const { queryEditors } = getSqlLabState(); + return queryEditors.find(editor => editor.id === editorId); +}; + +const createTab = ( + id: string, + name: string, + sql: string, + dbId: number, + catalog?: string, + schema?: string, + table?: any, +) => { + const editor = new Editor(sql, dbId, catalog, schema, table); + const panels: Panel[] = []; // TODO: Populate panels + return new Tab(id, name, editor, panels); +}; + +const notImplemented = (): never => { + throw new Error('Not implemented yet'); +}; + +function extractBaseData(action: any): { + baseParams: [string, Tab, boolean, number]; + options: { + ctasMethod?: string; + tempTable?: string; + templateParams?: string; + requestedLimit?: number; + }; +} { + const { query } = action; + const { + id, + sql, + startDttm, + runAsync, + dbId, + catalog, + schema, + sqlEditorId, + tab: tabName, + ctas_method: ctasMethod, + tempTable, + templateParams, + queryLimit, + } = query; + + const tab = createTab(sqlEditorId, tabName, sql, dbId, catalog, schema); + + return { + baseParams: [id, tab, runAsync ?? false, startDttm ?? 0], + options: { + ctasMethod, + tempTable, + templateParams, + requestedLimit: queryLimit, + }, + }; +} + +function createQueryContext( + action: ReturnType | ReturnType, +): QueryContext { + const { baseParams, options } = extractBaseData(action); + return new QueryContext(...baseParams, options); +} + +function createQueryResultContext( + action: ReturnType, +): QueryResultContext { + const { baseParams, options } = extractBaseData(action); + const [_, tab] = baseParams; + const { results } = action; + const { + query_id: queryId, + columns, + data, + query: { + endDttm, + executedSql, + tempTable: resultTempTable, + limit, + limitingFactor, + }, + } = results; + + return new QueryResultContext( + ...baseParams, + queryId, + executedSql ?? tab.editor.content, + columns, + data, + endDttm, + { + ...options, + tempTable: resultTempTable || options.tempTable, + appliedLimit: limit, + appliedLimitingFactor: limitingFactor, + }, + ); +} + +function createQueryErrorContext( + action: ReturnType, +): QueryErrorResultContext { + const { baseParams, options } = extractBaseData(action); + const { msg: errorMessage, errors, query } = action; + const { endDttm, executedSql, query_id: queryId } = query; + + return new QueryErrorResultContext(...baseParams, errorMessage, errors, { + ...options, + queryId, + executedSql: executedSql ?? null, + endDttm: endDttm ?? Date.now(), + }); +} + +const getCurrentTab: typeof sqlLabType.getCurrentTab = () => { + const queryEditor = findQueryEditor(activeEditorId()); + if (queryEditor) { + const { id, name, sql, dbId, catalog, schema } = queryEditor; + return createTab( + id, + name, + sql, + dbId!, + catalog ?? undefined, + schema ?? undefined, + undefined, + ); + } + return undefined; +}; + +const getActiveEditorImmutableId = () => { + const { tabHistory } = getSqlLabState(); + const activeEditorId = tabHistory[tabHistory.length - 1]; + const activeEditor = findQueryEditor(activeEditorId); + return activeEditor?.immutableId; +}; + +// Memoized version to avoid repeated store lookups when active editor hasn't changed +const getActiveEditorId = memoizeOne(getActiveEditorImmutableId); + +const predicate = (actionType: string): AnyListenerPredicate => { + // 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 = getActiveEditorId(); + + 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 queryEditor = findQueryEditor(action.queryEditor.id); + return queryEditor?.immutableId === registrationImmutableId; + } + + // Fallback: do not allow the event if we can't determine the source + return false; + }; +}; + +const onDidQueryRun: typeof sqlLabType.onDidQueryRun = ( + listener: (queryContext: sqlLabType.QueryContext) => void, + thisArgs?: any, +): Disposable => + createActionListener( + predicate(START_QUERY), + listener, + (action: ReturnType) => createQueryContext(action), + thisArgs, + ); + +const onDidQuerySuccess: typeof sqlLabType.onDidQuerySuccess = ( + listener: (queryResultContext: sqlLabType.QueryResultContext) => void, + thisArgs?: any, +): Disposable => + createActionListener( + predicate(QUERY_SUCCESS), + listener, + (action: ReturnType) => + createQueryResultContext(action), + thisArgs, + ); + +const onDidQueryStop: typeof sqlLabType.onDidQueryStop = ( + listener: (queryContext: sqlLabType.QueryContext) => void, + thisArgs?: any, +): Disposable => + createActionListener( + predicate(STOP_QUERY), + listener, + (action: ReturnType) => createQueryContext(action), + thisArgs, + ); + +const onDidQueryFail: typeof sqlLabType.onDidQueryFail = ( + listener: ( + queryErrorResultContext: sqlLabType.QueryErrorResultContext, + ) => void, + thisArgs?: any, +): Disposable => + createActionListener( + predicate(QUERY_FAILED), + listener, + (action: ReturnType) => + createQueryErrorContext(action), + thisArgs, + ); + +const onDidChangeEditorDatabase: typeof sqlLabType.onDidChangeEditorDatabase = ( + listener: (e: number) => void, + thisArgs?: any, +): Disposable => + createActionListener( + predicate(QUERY_EDITOR_SETDB), + listener, + (action: { type: string; dbId?: number; queryEditor: { dbId: number } }) => + action.dbId || action.queryEditor.dbId, + thisArgs, + ); + +const onDidChangeEditorContent: typeof sqlLabType.onDidChangeEditorContent = + notImplemented; +const onDidChangeEditorCatalog: typeof sqlLabType.onDidChangeEditorCatalog = + notImplemented; +const onDidChangeEditorSchema: typeof sqlLabType.onDidChangeEditorSchema = + notImplemented; +const onDidChangeEditorTable: typeof sqlLabType.onDidChangeEditorTable = + notImplemented; +const onDidClosePanel: typeof sqlLabType.onDidClosePanel = notImplemented; +const onDidChangeActivePanel: typeof sqlLabType.onDidChangeActivePanel = + notImplemented; +const onDidChangeTabTitle: typeof sqlLabType.onDidChangeTabTitle = + notImplemented; +const getDatabases: typeof sqlLabType.getDatabases = notImplemented; +const getTabs: typeof sqlLabType.getTabs = notImplemented; +const onDidCloseTab: typeof sqlLabType.onDidCloseTab = notImplemented; +const onDidChangeActiveTab: typeof sqlLabType.onDidChangeActiveTab = + notImplemented; +const onDidRefreshDatabases: typeof sqlLabType.onDidRefreshDatabases = + notImplemented; +const onDidRefreshCatalogs: typeof sqlLabType.onDidRefreshCatalogs = + notImplemented; +const onDidRefreshSchemas: typeof sqlLabType.onDidRefreshSchemas = + notImplemented; +const onDidRefreshTables: typeof sqlLabType.onDidRefreshTables = notImplemented; + +export const sqlLab: typeof sqlLabType = { + CTASMethod, + getCurrentTab, + onDidChangeEditorContent, + onDidChangeEditorDatabase, + onDidChangeEditorCatalog, + onDidChangeEditorSchema, + onDidChangeEditorTable, + onDidClosePanel, + onDidChangeActivePanel, + onDidChangeTabTitle, + onDidQueryRun, + onDidQueryStop, + onDidQueryFail, + onDidQuerySuccess, + getDatabases, + getTabs, + onDidCloseTab, + onDidChangeActiveTab, + onDidRefreshDatabases, + onDidRefreshCatalogs, + onDidRefreshSchemas, + onDidRefreshTables, +}; + +// Export all models +export * from './models'; diff --git a/superset-frontend/src/core/sqlLab/models.ts b/superset-frontend/src/core/sqlLab/models.ts new file mode 100644 index 00000000000..a6863aa5ce8 --- /dev/null +++ b/superset-frontend/src/core/sqlLab/models.ts @@ -0,0 +1,235 @@ +/** + * 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 { sqlLab as sqlLabType, core as coreType } from '@apache-superset/core'; + +const { CTASMethod } = sqlLabType; + +export class Panel implements sqlLabType.Panel { + id: string; + + constructor(id: string) { + this.id = id; + } +} + +export class Editor implements sqlLabType.Editor { + content: string; + + databaseId: number; + + schema: string; + + // TODO: Check later if we'll use objects instead of strings. + catalog: string | null; + + table: string | null; + + constructor( + content: string, + databaseId: number, + catalog: string | null = null, + schema = '', + table: string | null = null, + ) { + this.content = content; + this.databaseId = databaseId; + this.catalog = catalog; + this.schema = schema; + this.table = table; + } +} + +export class Tab implements sqlLabType.Tab { + id: string; + + title: string; + + editor: Editor; + + panels: Panel[]; + + constructor(id: string, title: string, editor: Editor, panels: Panel[] = []) { + this.id = id; + this.title = title; + this.editor = editor; + this.panels = panels; + } +} + +export class CTAS implements sqlLabType.CTAS { + method: sqlLabType.CTASMethod; + + tempTable: string; + + constructor(asView: boolean, tempTable: string) { + this.method = asView ? CTASMethod.View : CTASMethod.Table; + this.tempTable = tempTable; + } +} + +export class QueryContext implements sqlLabType.QueryContext { + clientId: string; + + ctas: sqlLabType.CTAS | null; + + editor: Editor; + + requestedLimit: number | null; + + runAsync: boolean; + + startDttm: number; + + tab: Tab; + + private templateParams: string; + + private parsedParams: Record; + + constructor( + clientId: string, + tab: Tab, + runAsync: boolean, + startDttm: number, + options: { + templateParams?: string; + ctasMethod?: string; + tempTable?: string; + requestedLimit?: number; + } = {}, + ) { + this.clientId = clientId; + this.tab = tab; + this.editor = tab.editor; + this.runAsync = runAsync; + this.startDttm = startDttm; + this.requestedLimit = options.requestedLimit ?? null; + this.ctas = options.tempTable + ? new CTAS(options.ctasMethod === CTASMethod.View, options.tempTable) + : null; + this.templateParams = options.templateParams ?? ''; + } + + /** + * A custom accessor is used to process JSON parsing only + * when necessary for better performance. + */ + get templateParameters() { + if (this.parsedParams) { + return this.parsedParams; + } + + let parsed = {}; + try { + parsed = JSON.parse(this.templateParams); + } catch (e) { + // ignore invalid format string. + } + this.parsedParams = parsed; + + return parsed; + } +} + +export class QueryResultContext + extends QueryContext + implements sqlLabType.QueryResultContext +{ + appliedLimit: number; + + appliedLimitingFactor: string; + + endDttm: number; + + executedSql: string; + + remoteId: number; + + result: sqlLabType.QueryResult; + + constructor( + clientId: string, + tab: Tab, + runAsync: boolean, + startDttm: number, + remoteId: number, + executedSql: string, + columns: sqlLabType.QueryResult['columns'], + data: sqlLabType.QueryResult['data'], + endDttm: number, + options: { + appliedLimit?: number; + appliedLimitingFactor?: string; + templateParams?: string; + ctasMethod?: string; + tempTable?: string; + requestedLimit?: number; + } = {}, + ) { + const { appliedLimit, appliedLimitingFactor, ...opt } = options; + super(clientId, tab, runAsync, startDttm, opt); + this.remoteId = remoteId; + this.executedSql = executedSql; + this.endDttm = endDttm; + this.result = { + columns, + data, + }; + this.appliedLimit = appliedLimit ?? data.length; + this.appliedLimitingFactor = options.appliedLimitingFactor ?? ''; + } +} + +export class QueryErrorResultContext + extends QueryContext + implements sqlLabType.QueryErrorResultContext +{ + endDttm: number; + + errorMessage: string; + + errors: coreType.SupersetError[] | null; + + executedSql: string | null; + + constructor( + clientId: string, + tab: Tab, + runAsync: boolean, + startDttm: number, + errorMessage: string, + errors: coreType.SupersetError[], + options: { + ctasMethod?: string; + executedSql?: string; + endDttm?: number; + templateParams?: string; + tempTable?: string; + requestedLimit?: number; + queryId?: number; + } = {}, + ) { + const { queryId, executedSql, endDttm, ...opt } = options; + super(clientId, tab, runAsync, startDttm, opt); + this.executedSql = executedSql ?? null; + this.errorMessage = errorMessage; + this.errors = errors; + this.endDttm = endDttm ?? Date.now(); + } +} diff --git a/superset-frontend/src/extensions/ExtensionsManager.ts b/superset-frontend/src/extensions/ExtensionsManager.ts index 6b2d3ef4d5a..49de8d705b9 100644 --- a/superset-frontend/src/extensions/ExtensionsManager.ts +++ b/superset-frontend/src/extensions/ExtensionsManager.ts @@ -23,7 +23,7 @@ import { logging, } from '@superset-ui/core'; import type { contributions, core } from '@apache-superset/core'; -import { ExtensionContext } from '../core/core'; +import { ExtensionContext } from '../core/models'; class ExtensionsManager { private static instance: ExtensionsManager;