diff --git a/superset-frontend/packages/superset-ui-core/src/components/Icons/AntdEnhanced.tsx b/superset-frontend/packages/superset-ui-core/src/components/Icons/AntdEnhanced.tsx index b1914d3e267..947a5ff3926 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/Icons/AntdEnhanced.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/Icons/AntdEnhanced.tsx @@ -115,6 +115,7 @@ import { PlusSquareOutlined, PlusOutlined, ProfileOutlined, + PushpinFilled, PushpinOutlined, QuestionCircleOutlined, ReloadOutlined, @@ -270,6 +271,7 @@ const AntdIcons = { PlusSquareOutlined, PlusOutlined, ProfileOutlined, + PushpinFilled, PushpinOutlined, ReloadOutlined, QuestionCircleOutlined, diff --git a/superset-frontend/src/SqlLab/components/ColumnElement/index.tsx b/superset-frontend/src/SqlLab/components/ColumnElement/index.tsx index 59a029904f0..7606149e71b 100644 --- a/superset-frontend/src/SqlLab/components/ColumnElement/index.tsx +++ b/superset-frontend/src/SqlLab/components/ColumnElement/index.tsx @@ -74,13 +74,16 @@ interface ColumnElementProps { keys?: { type: ColumnKeyTypeType }[]; type: string; }; + actions?: ReactNode; } -const NowrapDiv = styled.div` +const ColumnType = styled.div` white-space: nowrap; + color: ${({ theme }) => theme.colorTextDescription}; + font-size: ${({ theme }) => theme.fontSizeSM}px; `; -const ColumnElement = ({ column }: ColumnElementProps) => { +const ColumnElement = ({ column, actions }: ColumnElementProps) => { let columnName: ReactNode = column.name; let icons; if (column.keys && column.keys.length > 0) { @@ -110,10 +113,9 @@ const ColumnElement = ({ column }: ColumnElementProps) => {
{columnName} {icons} + {actions}
- - {column.type} - + {column.type} ); }; diff --git a/superset-frontend/src/SqlLab/components/EditorWrapper/useKeywords.test.ts b/superset-frontend/src/SqlLab/components/EditorWrapper/useKeywords.test.ts index 60c6d5095f5..be285fa4d3d 100644 --- a/superset-frontend/src/SqlLab/components/EditorWrapper/useKeywords.test.ts +++ b/superset-frontend/src/SqlLab/components/EditorWrapper/useKeywords.test.ts @@ -257,6 +257,8 @@ test('returns column keywords among selected tables', async () => { }, ); + // Both columns should be present since all cached table metadata + // for this database is included in autocomplete await waitFor(() => expect(result.current).toContainEqual( expect.objectContaining({ @@ -268,31 +270,14 @@ test('returns column keywords among selected tables', async () => { ), ); - expect(result.current).not.toContainEqual( + expect(result.current).toContainEqual( expect.objectContaining({ name: unexpectedColumn, + value: unexpectedColumn, + score: COLUMN_AUTOCOMPLETE_SCORE, + meta: 'column', }), ); - - act(() => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - storeWithSqlLab.dispatch( - addTable( - { id: expectQueryEditorId } as any, - unexpectedTable, - expectCatalog, - expectSchema, - ) as any, - ); - }); - - await waitFor(() => - expect(result.current).toContainEqual( - expect.objectContaining({ - name: unexpectedColumn, - }), - ), - ); }); test('returns long keywords with detail', async () => { diff --git a/superset-frontend/src/SqlLab/components/EditorWrapper/useKeywords.ts b/superset-frontend/src/SqlLab/components/EditorWrapper/useKeywords.ts index ce1896e0886..fa1a1974be9 100644 --- a/superset-frontend/src/SqlLab/components/EditorWrapper/useKeywords.ts +++ b/superset-frontend/src/SqlLab/components/EditorWrapper/useKeywords.ts @@ -17,7 +17,7 @@ * under the License. */ import { useEffect, useMemo, useRef } from 'react'; -import { useSelector, useDispatch, shallowEqual, useStore } from 'react-redux'; +import { useDispatch, useStore } from 'react-redux'; import { t } from '@apache-superset/core/translation'; import { getExtensionsRegistry } from '@superset-ui/core'; @@ -30,15 +30,10 @@ import { COLUMN_AUTOCOMPLETE_SCORE, SQL_FUNCTIONS_AUTOCOMPLETE_SCORE, } from 'src/SqlLab/constants'; -import { - schemaEndpoints, - tableEndpoints, - skipToken, -} from 'src/hooks/apiResources'; +import { schemaEndpoints } from 'src/hooks/apiResources'; import { api } from 'src/hooks/apiResources/queryApi'; import { useDatabaseFunctionsQuery } from 'src/hooks/apiResources/databaseFunctions'; import useEffectEvent from 'src/hooks/useEffectEvent'; -import { SqlLabRootState } from 'src/SqlLab/types'; type Params = { queryEditorId: string | number; @@ -51,7 +46,6 @@ type Params = { const EMPTY_LIST = [] as typeof sqlKeywords; const { useQueryState: useSchemasQueryState } = schemaEndpoints.schemas; -const { useQueryState: useTablesQueryState } = tableEndpoints.tables; const getHelperText = (value: string) => value.length > 30 && { @@ -87,16 +81,6 @@ export function useKeywords( }, { skip: skipFetch || !dbId }, ); - const { currentData: tableData } = useTablesQueryState( - { - dbId, - catalog, - schema, - forceRefresh: false, - }, - { skip: skipFetch || !dbId || !schema }, - ); - const { currentData: functionNames, isError } = useDatabaseFunctionsQuery( { dbId }, { skip: skipFetch || !dbId }, @@ -110,41 +94,64 @@ export function useKeywords( } }, [dispatch, isError]); - const tablesForColumnMetadata = useSelector( - ({ sqlLab }) => - skip - ? [] - : (sqlLab?.tables ?? []) - .filter(table => table.queryEditorId === queryEditorId) - .map(table => table.name), - shallowEqual, - ); - const store = useStore(); const apiState = store.getState()[api.reducerPath]; + // Normalize catalog for comparison (null/undefined both mean "no catalog") + const normalizedCatalog = catalog ?? null; + + // Collect all table names from all cached table-list queries for this database/catalog. + // This includes tables from any schema the user has expanded in the tree. + const allCachedTables = useMemo(() => { + if (skipFetch || !dbId || !apiState) return []; + const tables: { value: string; label: string; schema: string }[] = []; + const seen = new Set(); + const queries = apiState.queries ?? {}; + for (const entry of Object.values(queries) as any[]) { + const arg = entry?.originalArgs; + if ( + arg?.dbId === dbId && + (arg?.catalog ?? null) === normalizedCatalog && + entry?.status === 'fulfilled' && + entry?.data?.options + ) { + for (const table of entry.data.options) { + const key = `${arg.schema}.${table.value}`; + if (!seen.has(key)) { + seen.add(key); + tables.push({ + value: table.value, + label: table.label ?? table.value, + schema: arg.schema, + }); + } + } + } + } + return tables; + }, [dbId, normalizedCatalog, apiState, skipFetch]); + + // Collect column names from all cached table-metadata queries for this database/catalog. + // This includes columns from any table the user has expanded in the tree. const allColumns = useMemo(() => { + if (skipFetch || !dbId || !apiState) return []; const columns = new Set(); - tablesForColumnMetadata.forEach(table => { - tableEndpoints.tableMetadata - .select( - dbId && schema - ? { - dbId, - catalog, - schema, - table, - } - : skipToken, - )({ - [api.reducerPath]: apiState, - }) - .data?.columns?.forEach(({ name }) => { - columns.add(name); - }); - }); + const queries = apiState.queries ?? {}; + for (const entry of Object.values(queries) as any[]) { + const arg = entry?.originalArgs; + if ( + entry?.status === 'fulfilled' && + entry?.data?.columns && + arg?.dbId === dbId && + (arg?.catalog ?? null) === normalizedCatalog + ) { + for (const col of entry.data.columns) { + columns.add(col.name); + } + } + } return [...columns]; - }, [dbId, catalog, schema, apiState, tablesForColumnMetadata]); + }, [dbId, normalizedCatalog, apiState, skipFetch]); const insertMatch = useEffectEvent((editor: Editor, data: any) => { if (data.meta === 'table') { @@ -153,7 +160,7 @@ export function useKeywords( { id: String(queryEditorId), dbId: dbId as number, tabViewId }, data.value, catalog ?? null, - schema ?? '', + data.schema ?? schema ?? '', false, // Don't auto-expand/switch tabs when adding via autocomplete ), ); @@ -187,9 +194,10 @@ export function useKeywords( const tableKeywords = useMemo( () => - (tableData?.options ?? []).map(({ value, label }) => ({ + allCachedTables.map(({ value, label, schema: tableSchema }) => ({ name: label, value, + schema: tableSchema, score: TABLE_AUTOCOMPLETE_SCORE, meta: 'table', completer: { @@ -197,7 +205,7 @@ export function useKeywords( }, ...getHelperText(value), })), - [tableData?.options, insertMatch], + [allCachedTables, insertMatch], ); const columnKeywords = useMemo( diff --git a/superset-frontend/src/SqlLab/components/QueryLimitSelect/index.tsx b/superset-frontend/src/SqlLab/components/QueryLimitSelect/index.tsx index 9dadb22b091..4c2803b7753 100644 --- a/superset-frontend/src/SqlLab/components/QueryLimitSelect/index.tsx +++ b/superset-frontend/src/SqlLab/components/QueryLimitSelect/index.tsx @@ -89,7 +89,7 @@ const QueryLimitSelect = ({ >