diff --git a/superset-frontend/src/SqlLab/components/SqlEditorLeftBar/SqlEditorLeftBar.test.jsx b/superset-frontend/src/SqlLab/components/SqlEditorLeftBar/SqlEditorLeftBar.test.jsx index a4774cd390afa..519da3d320eea 100644 --- a/superset-frontend/src/SqlLab/components/SqlEditorLeftBar/SqlEditorLeftBar.test.jsx +++ b/superset-frontend/src/SqlLab/components/SqlEditorLeftBar/SqlEditorLeftBar.test.jsx @@ -41,15 +41,24 @@ const middlewares = [thunk]; const mockStore = configureStore(middlewares); const store = mockStore(initialState); -fetchMock.get('glob:*/api/v1/database/*/schemas/?*', { result: [] }); -fetchMock.get('glob:*/api/v1/database/*/tables/*', { - count: 1, - result: [ - { - label: 'ab_user', - value: 'ab_user', - }, - ], +beforeEach(() => { + fetchMock.get('glob:*/api/v1/database/*/schemas/?*', { + result: ['main', 'new_schema'], + }); + fetchMock.get('glob:*/api/v1/database/**', { result: [] }); + fetchMock.get('glob:*/api/v1/database/*/tables/*', { + count: 1, + result: [ + { + label: 'ab_user', + value: 'ab_user', + }, + ], + }); +}); + +afterEach(() => { + fetchMock.restore(); }); const renderAndWait = (props, store) => @@ -110,8 +119,9 @@ test('should toggle the table when the header is clicked', async () => { userEvent.click(header); await waitFor(() => { - expect(store.getActions()).toHaveLength(4); - expect(store.getActions()[3].type).toEqual('COLLAPSE_TABLE'); + expect(store.getActions()[store.getActions().length - 1].type).toEqual( + 'COLLAPSE_TABLE', + ); }); }); @@ -129,14 +139,55 @@ test('When changing database the table list must be updated', async () => { database_name: 'new_db', backend: 'postgresql', }} - queryEditor={{ ...mockedProps.queryEditor, schema: 'new_schema' }} + queryEditorId={defaultQueryEditor.id} tables={[{ ...mockedProps.tables[0], dbId: 2, name: 'new_table' }]} />, { useRedux: true, - initialState, + store: mockStore({ + ...initialState, + sqlLab: { + ...initialState.sqlLab, + unsavedQueryEditor: { + id: defaultQueryEditor.id, + schema: 'new_schema', + }, + }, + }), }, ); expect(await screen.findByText(/new_db/i)).toBeInTheDocument(); expect(await screen.findByText(/new_table/i)).toBeInTheDocument(); }); + +test('ignore schema api when current schema is deprecated', async () => { + const invalidSchemaName = 'None'; + const { rerender } = await renderAndWait( + mockedProps, + mockStore({ + ...initialState, + sqlLab: { + ...initialState.sqlLab, + unsavedQueryEditor: { + id: defaultQueryEditor.id, + schema: invalidSchemaName, + }, + }, + }), + ); + + expect(await screen.findByText(/Database/i)).toBeInTheDocument(); + expect(screen.queryByText(/None/i)).toBeInTheDocument(); + expect(fetchMock.calls()).not.toContainEqual( + expect.arrayContaining([ + expect.stringContaining( + `/tables/${mockedProps.database.id}/${invalidSchemaName}/`, + ), + ]), + ); + rerender(); + // Deselect the deprecated schema selection + await waitFor(() => + expect(screen.queryByText(/None/i)).not.toBeInTheDocument(), + ); +}); diff --git a/superset-frontend/src/components/DatabaseSelector/index.tsx b/superset-frontend/src/components/DatabaseSelector/index.tsx index e14bad09a89b3..d78a94bde60fc 100644 --- a/superset-frontend/src/components/DatabaseSelector/index.tsx +++ b/superset-frontend/src/components/DatabaseSelector/index.tsx @@ -24,10 +24,7 @@ import Label from 'src/components/Label'; import { FormLabel } from 'src/components/Form'; import RefreshLabel from 'src/components/RefreshLabel'; import { useToasts } from 'src/components/MessageToasts/withToasts'; -import { - getClientErrorMessage, - getClientErrorObject, -} from 'src/utils/getClientErrorObject'; +import { useSchemas, SchemaOption } from 'src/hooks/apiResources'; const DatabaseSelectorWrapper = styled.div` ${({ theme }) => ` @@ -86,8 +83,6 @@ export type DatabaseObject = { backend: string; }; -type SchemaValue = { label: string; value: string }; - export interface DatabaseSelectorProps { db?: DatabaseObject | null; emptyState?: ReactNode; @@ -119,6 +114,8 @@ const SelectLabel = ({ ); +const EMPTY_SCHEMA_OPTIONS: SchemaOption[] = []; + export default function DatabaseSelector({ db, formMode = false, @@ -134,13 +131,10 @@ export default function DatabaseSelector({ schema, sqlLabMode = false, }: DatabaseSelectorProps) { - const [loadingSchemas, setLoadingSchemas] = useState(false); - const [schemaOptions, setSchemaOptions] = useState([]); const [currentDb, setCurrentDb] = useState(); - const [currentSchema, setCurrentSchema] = useState( - schema ? { label: schema, value: schema } : undefined, + const [currentSchema, setCurrentSchema] = useState( + schema ? { label: schema, value: schema, title: schema } : undefined, ); - const [refresh, setRefresh] = useState(0); const { addSuccessToast } = useToasts(); const loadDatabases = useMemo( @@ -221,48 +215,37 @@ export default function DatabaseSelector({ ); }, [db]); - function changeSchema(schema: SchemaValue) { + function changeSchema(schema: SchemaOption | undefined) { setCurrentSchema(schema); if (onSchemaChange) { - onSchemaChange(schema.value); + onSchemaChange(schema?.value); } } - useEffect(() => { - if (currentDb) { - setLoadingSchemas(true); - const queryParams = rison.encode({ force: refresh > 0 }); - const endpoint = `/api/v1/database/${currentDb.value}/schemas/?q=${queryParams}`; + const { + data, + isFetching: loadingSchemas, + isFetched, + refetch, + } = useSchemas({ + dbId: currentDb?.value, + onSuccess: data => { + onSchemasLoad?.(data); - // TODO: Would be nice to add pagination in a follow-up. Needs endpoint changes. - SupersetClient.get({ endpoint }) - .then(({ json }) => { - const options = json.result.map((s: string) => ({ - value: s, - label: s, - title: s, - })); - if (onSchemasLoad) { - onSchemasLoad(options); - } - setSchemaOptions(options); - setLoadingSchemas(false); - if (options.length === 1) changeSchema(options[0]); - if (refresh > 0) addSuccessToast(t('List refreshed')); - }) - .catch(err => { - setLoadingSchemas(false); - getClientErrorObject(err).then(clientError => { - handleError( - getClientErrorMessage( - t('There was an error loading the schemas'), - clientError, - ), - ); - }); - }); - } - }, [currentDb, onSchemasLoad, refresh]); + if (data.length === 1) { + changeSchema(data[0]); + } else if (!data.find(schemaOption => schema === schemaOption.value)) { + changeSchema(undefined); + } + + if (isFetched) { + addSuccessToast('List refreshed'); + } + }, + onError: () => handleError(t('There was an error loading the schemas')), + }); + + const schemaOptions = data || EMPTY_SCHEMA_OPTIONS; function changeDataBase( value: { label: string; value: number }, @@ -309,7 +292,7 @@ export default function DatabaseSelector({ function renderSchemaSelect() { const refreshIcon = !readOnly && ( setRefresh(refresh + 1)} + onClick={() => refetch()} tooltipContent={t('Force refresh schema list')} /> ); @@ -323,7 +306,7 @@ export default function DatabaseSelector({ name="select-schema" notFoundContent={t('No compatible schema found')} placeholder={t('Select schema or type schema name')} - onChange={item => changeSchema(item as SchemaValue)} + onChange={item => changeSchema(item as SchemaOption)} options={schemaOptions} showSearch value={currentSchema} diff --git a/superset-frontend/src/components/TableSelector/index.tsx b/superset-frontend/src/components/TableSelector/index.tsx index 1cf65bbc688af..ffb45cc8fed9f 100644 --- a/superset-frontend/src/components/TableSelector/index.tsx +++ b/superset-frontend/src/components/TableSelector/index.tsx @@ -275,26 +275,6 @@ const TableSelector: FunctionComponent = ({ internalTableChange(value); }; - function renderDatabaseSelector() { - return ( - - ); - } - const handleFilterOption = useMemo( () => (search: string, option: TableOption) => { const searchValue = search.trim().toLowerCase(); @@ -346,7 +326,21 @@ const TableSelector: FunctionComponent = ({ return ( - {renderDatabaseSelector()} + {sqlLabMode && !formMode &&
} {renderTableSelect()} diff --git a/superset-frontend/src/hooks/apiResources/index.ts b/superset-frontend/src/hooks/apiResources/index.ts index 32a6418dc07c4..81d77b5d11a50 100644 --- a/superset-frontend/src/hooks/apiResources/index.ts +++ b/superset-frontend/src/hooks/apiResources/index.ts @@ -29,3 +29,4 @@ export { export * from './charts'; export * from './dashboards'; export * from './tables'; +export * from './schemas'; diff --git a/superset-frontend/src/hooks/apiResources/schemas.test.ts b/superset-frontend/src/hooks/apiResources/schemas.test.ts new file mode 100644 index 0000000000000..59d00a5dc71bc --- /dev/null +++ b/superset-frontend/src/hooks/apiResources/schemas.test.ts @@ -0,0 +1,138 @@ +/** + * 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 rison from 'rison'; +import fetchMock from 'fetch-mock'; +import { act, renderHook } from '@testing-library/react-hooks'; +import QueryProvider, { queryClient } from 'src/views/QueryProvider'; +import { useSchemas } from './schemas'; + +const fakeApiResult = { + result: ['test schema 1', 'test schema b'], +}; + +const expectedResult = fakeApiResult.result.map((value: string) => ({ + value, + label: value, + title: value, +})); + +describe('useSchemas hook', () => { + beforeEach(() => { + queryClient.clear(); + jest.useFakeTimers(); + }); + + afterEach(() => { + fetchMock.reset(); + jest.useRealTimers(); + }); + + test('returns api response mapping json result', async () => { + const expectDbId = 'db1'; + const forceRefresh = false; + const schemaApiRoute = `glob:*/api/v1/database/${expectDbId}/schemas/*`; + fetchMock.get(schemaApiRoute, fakeApiResult); + const { result } = renderHook( + () => + useSchemas({ + dbId: expectDbId, + }), + { + wrapper: QueryProvider, + }, + ); + await act(async () => { + jest.runAllTimers(); + }); + expect(fetchMock.calls(schemaApiRoute).length).toBe(1); + expect( + fetchMock.calls( + `end:/api/v1/database/${expectDbId}/schemas/?q=${rison.encode({ + force: forceRefresh, + })}`, + ).length, + ).toBe(1); + expect(result.current.data).toEqual(expectedResult); + await act(async () => { + result.current.refetch(); + }); + expect(fetchMock.calls(schemaApiRoute).length).toBe(2); + expect( + fetchMock.calls( + `end:/api/v1/database/${expectDbId}/schemas/?q=${rison.encode({ + force: true, + })}`, + ).length, + ).toBe(1); + expect(result.current.data).toEqual(expectedResult); + }); + + test('returns cached data without api request', async () => { + const expectDbId = 'db1'; + const schemaApiRoute = `glob:*/api/v1/database/${expectDbId}/schemas/*`; + fetchMock.get(schemaApiRoute, fakeApiResult); + const { result, rerender } = renderHook( + () => + useSchemas({ + dbId: expectDbId, + }), + { + wrapper: QueryProvider, + }, + ); + await act(async () => { + jest.runAllTimers(); + }); + expect(fetchMock.calls(schemaApiRoute).length).toBe(1); + rerender(); + expect(fetchMock.calls(schemaApiRoute).length).toBe(1); + expect(result.current.data).toEqual(expectedResult); + }); + + it('returns refreshed data after expires', async () => { + const expectDbId = 'db1'; + const schemaApiRoute = `glob:*/api/v1/database/${expectDbId}/schemas/*`; + fetchMock.get(schemaApiRoute, fakeApiResult); + const { result, rerender } = renderHook( + () => + useSchemas({ + dbId: expectDbId, + }), + { + wrapper: QueryProvider, + }, + ); + await act(async () => { + jest.runAllTimers(); + }); + expect(fetchMock.calls(schemaApiRoute).length).toBe(1); + rerender(); + await act(async () => { + jest.runAllTimers(); + }); + expect(fetchMock.calls(schemaApiRoute).length).toBe(1); + queryClient.clear(); + rerender(); + await act(async () => { + jest.runAllTimers(); + }); + expect(fetchMock.calls(schemaApiRoute).length).toBe(2); + expect(result.current.data).toEqual(expectedResult); + }); +}); diff --git a/superset-frontend/src/hooks/apiResources/schemas.ts b/superset-frontend/src/hooks/apiResources/schemas.ts new file mode 100644 index 0000000000000..34a50ca4e9896 --- /dev/null +++ b/superset-frontend/src/hooks/apiResources/schemas.ts @@ -0,0 +1,80 @@ +/** + * 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 { useRef } from 'react'; +import { useQuery, UseQueryOptions } from 'react-query'; +import rison from 'rison'; +import { SupersetClient } from '@superset-ui/core'; + +export type FetchSchemasQueryParams = { + dbId?: string | number; + forceRefresh?: boolean; +}; + +type QueryData = { + json: { result: string[] }; + response: Response; +}; + +export type SchemaOption = { + value: string; + label: string; + title: string; +}; + +export function fetchSchemas({ dbId, forceRefresh }: FetchSchemasQueryParams) { + const queryParams = rison.encode({ force: forceRefresh }); + // TODO: Would be nice to add pagination in a follow-up. Needs endpoint changes. + const endpoint = `/api/v1/database/${dbId}/schemas/?q=${queryParams}`; + return SupersetClient.get({ endpoint }) as Promise; +} + +type Params = FetchSchemasQueryParams & + Pick, 'onSuccess' | 'onError'>; + +export function useSchemas(options: Params) { + const { dbId, onSuccess, onError } = options || {}; + const forceRefreshRef = useRef(false); + const params = { dbId }; + const result = useQuery( + ['schemas', { dbId }], + () => fetchSchemas({ ...params, forceRefresh: forceRefreshRef.current }), + { + select: ({ json }) => + json.result.map((value: string) => ({ + value, + label: value, + title: value, + })), + enabled: Boolean(dbId), + onSuccess, + onError, + onSettled: () => { + forceRefreshRef.current = false; + }, + }, + ); + + return { + ...result, + refetch: () => { + forceRefreshRef.current = true; + return result.refetch(); + }, + }; +} diff --git a/superset-frontend/src/hooks/apiResources/tables.test.ts b/superset-frontend/src/hooks/apiResources/tables.test.ts index 49305f5f62ebb..8cc0791d50694 100644 --- a/superset-frontend/src/hooks/apiResources/tables.test.ts +++ b/superset-frontend/src/hooks/apiResources/tables.test.ts @@ -16,78 +16,76 @@ * specific language governing permissions and limitations * under the License. */ +import rison from 'rison'; +import fetchMock from 'fetch-mock'; import { act, renderHook } from '@testing-library/react-hooks'; -import { SupersetClient } from '@superset-ui/core'; import QueryProvider, { queryClient } from 'src/views/QueryProvider'; import { useTables } from './tables'; const fakeApiResult = { - json: { - count: 2, - result: [ - { - id: 1, - name: 'fake api result1', - label: 'fake api label1', - }, - { - id: 2, - name: 'fake api result2', - label: 'fake api label2', - }, - ], - }, + count: 2, + result: [ + { + id: 1, + name: 'fake api result1', + label: 'fake api label1', + }, + { + id: 2, + name: 'fake api result2', + label: 'fake api label2', + }, + ], }; const fakeHasMoreApiResult = { - json: { - count: 4, - result: [ - { - id: 1, - name: 'fake api result1', - label: 'fake api label1', - }, - { - id: 2, - name: 'fake api result2', - label: 'fake api label2', - }, - ], - }, + count: 4, + result: [ + { + id: 1, + name: 'fake api result1', + label: 'fake api label1', + }, + { + id: 2, + name: 'fake api result2', + label: 'fake api label2', + }, + ], }; +const fakeSchemaApiResult = ['schema1', 'schema2']; + const expectedData = { - options: [...fakeApiResult.json.result], + options: fakeApiResult.result, hasMore: false, }; const expectedHasMoreData = { - options: [...fakeHasMoreApiResult.json.result], + options: fakeHasMoreApiResult.result, hasMore: true, }; -jest.mock('@superset-ui/core', () => ({ - SupersetClient: { - get: jest.fn().mockResolvedValue(fakeApiResult), - }, -})); - describe('useTables hook', () => { beforeEach(() => { - (SupersetClient.get as jest.Mock).mockClear(); queryClient.clear(); jest.useFakeTimers(); }); afterEach(() => { + fetchMock.reset(); jest.useRealTimers(); }); - it('returns api response mapping json options', async () => { + test('returns api response mapping json options', async () => { const expectDbId = 'db1'; - const expectedSchema = 'schemaA'; - const forceRefresh = false; + const expectedSchema = 'schema1'; + const schemaApiRoute = `glob:*/api/v1/database/${expectDbId}/schemas/*`; + const tableApiRoute = `glob:*/api/v1/database/${expectDbId}/tables/?q=*`; + fetchMock.get(tableApiRoute, fakeApiResult); + fetchMock.get(schemaApiRoute, { + result: fakeSchemaApiResult, + }); const { result } = renderHook( () => useTables({ @@ -101,29 +99,73 @@ describe('useTables hook', () => { await act(async () => { jest.runAllTimers(); }); - expect(SupersetClient.get).toHaveBeenCalledTimes(1); - expect(SupersetClient.get).toHaveBeenCalledWith({ - endpoint: `/api/v1/database/${expectDbId}/tables/?q=(force:!${ - forceRefresh ? 't' : 'f' - },schema_name:${expectedSchema})`, - }); + expect(fetchMock.calls(schemaApiRoute).length).toBe(1); + expect( + fetchMock.calls( + `end:api/v1/database/${expectDbId}/tables/?q=${rison.encode({ + force: false, + schema_name: expectedSchema, + })}`, + ).length, + ).toBe(1); expect(result.current.data).toEqual(expectedData); await act(async () => { result.current.refetch(); }); - expect(SupersetClient.get).toHaveBeenCalledTimes(2); - expect(SupersetClient.get).toHaveBeenCalledWith({ - endpoint: `/api/v1/database/${expectDbId}/tables/?q=(force:!t,schema_name:${expectedSchema})`, - }); + expect(fetchMock.calls(schemaApiRoute).length).toBe(1); + expect( + fetchMock.calls( + `end:api/v1/database/${expectDbId}/tables/?q=${rison.encode({ + force: true, + schema_name: expectedSchema, + })}`, + ).length, + ).toBe(1); expect(result.current.data).toEqual(expectedData); }); - it('returns hasMore when total is larger than result size', async () => { - (SupersetClient.get as jest.Mock).mockResolvedValueOnce( - fakeHasMoreApiResult, + test('skips the deprecated schema option', async () => { + const expectDbId = 'db1'; + const unexpectedSchema = 'invalid schema'; + const schemaApiRoute = `glob:*/api/v1/database/${expectDbId}/schemas/*`; + const tableApiRoute = `glob:*/api/v1/database/${expectDbId}/tables/?q=*`; + fetchMock.get(tableApiRoute, fakeApiResult); + fetchMock.get(schemaApiRoute, { + result: fakeSchemaApiResult, + }); + const { result } = renderHook( + () => + useTables({ + dbId: expectDbId, + schema: unexpectedSchema, + }), + { + wrapper: QueryProvider, + }, ); + await act(async () => { + jest.runAllTimers(); + }); + expect(fetchMock.calls(schemaApiRoute).length).toBe(1); + expect(result.current.data).toEqual(undefined); + expect( + fetchMock.calls( + `end:api/v1/database/${expectDbId}/tables/?q=${rison.encode({ + force: false, + schema_name: unexpectedSchema, + })}`, + ).length, + ).toBe(0); + }); + + test('returns hasMore when total is larger than result size', async () => { const expectDbId = 'db1'; - const expectedSchema = 'schemaA'; + const expectedSchema = 'schema2'; + const tableApiRoute = `glob:*/api/v1/database/${expectDbId}/tables/?q=*`; + fetchMock.get(tableApiRoute, fakeHasMoreApiResult); + fetchMock.get(`glob:*/api/v1/database/${expectDbId}/schemas/*`, { + result: fakeSchemaApiResult, + }); const { result } = renderHook( () => useTables({ @@ -137,13 +179,18 @@ describe('useTables hook', () => { await act(async () => { jest.runAllTimers(); }); - expect(SupersetClient.get).toHaveBeenCalledTimes(1); + expect(fetchMock.calls(tableApiRoute).length).toBe(1); expect(result.current.data).toEqual(expectedHasMoreData); }); - it('returns cached data without api request', async () => { + test('returns cached data without api request', async () => { const expectDbId = 'db1'; - const expectedSchema = 'schemaA'; + const expectedSchema = 'schema1'; + const tableApiRoute = `glob:*/api/v1/database/${expectDbId}/tables/?q=*`; + fetchMock.get(tableApiRoute, fakeApiResult); + fetchMock.get(`glob:*/api/v1/database/${expectDbId}/schemas/*`, { + result: fakeSchemaApiResult, + }); const { result, rerender } = renderHook( () => useTables({ @@ -157,15 +204,20 @@ describe('useTables hook', () => { await act(async () => { jest.runAllTimers(); }); - expect(SupersetClient.get).toHaveBeenCalledTimes(1); + expect(fetchMock.calls(tableApiRoute).length).toBe(1); rerender(); - expect(SupersetClient.get).toHaveBeenCalledTimes(1); + expect(fetchMock.calls(tableApiRoute).length).toBe(1); expect(result.current.data).toEqual(expectedData); }); - it('returns refreshed data after expires', async () => { + test('returns refreshed data after expires', async () => { const expectDbId = 'db1'; - const expectedSchema = 'schemaA'; + const expectedSchema = 'schema1'; + const tableApiRoute = `glob:*/api/v1/database/${expectDbId}/tables/?q=*`; + fetchMock.get(tableApiRoute, fakeApiResult); + fetchMock.get(`glob:*/api/v1/database/${expectDbId}/schemas/*`, { + result: fakeSchemaApiResult, + }); const { result, rerender } = renderHook( () => useTables({ @@ -179,18 +231,18 @@ describe('useTables hook', () => { await act(async () => { jest.runAllTimers(); }); - expect(SupersetClient.get).toHaveBeenCalledTimes(1); + expect(fetchMock.calls(tableApiRoute).length).toBe(1); rerender(); await act(async () => { jest.runAllTimers(); }); - expect(SupersetClient.get).toHaveBeenCalledTimes(1); + expect(fetchMock.calls(tableApiRoute).length).toBe(1); queryClient.clear(); rerender(); await act(async () => { jest.runAllTimers(); }); - expect(SupersetClient.get).toHaveBeenCalledTimes(2); + expect(fetchMock.calls(tableApiRoute).length).toBe(2); expect(result.current.data).toEqual(expectedData); }); }); diff --git a/superset-frontend/src/hooks/apiResources/tables.ts b/superset-frontend/src/hooks/apiResources/tables.ts index f65cdf375ad45..34d286c9456a2 100644 --- a/superset-frontend/src/hooks/apiResources/tables.ts +++ b/superset-frontend/src/hooks/apiResources/tables.ts @@ -16,11 +16,13 @@ * specific language governing permissions and limitations * under the License. */ -import { useRef } from 'react'; +import { useRef, useMemo } from 'react'; import { useQuery, UseQueryOptions } from 'react-query'; import rison from 'rison'; import { SupersetClient } from '@superset-ui/core'; +import { useSchemas } from './schemas'; + export type FetchTablesQueryParams = { dbId?: string | number; schema?: string; @@ -71,9 +73,16 @@ export function fetchTables({ } type Params = FetchTablesQueryParams & - Pick; + Pick, 'onSuccess' | 'onError'>; export function useTables(options: Params) { + const { data: schemaOptions, isFetching } = useSchemas({ + dbId: options.dbId, + }); + const schemaOptionsMap = useMemo( + () => new Set(schemaOptions?.map(({ value }) => value)), + [schemaOptions], + ); const { dbId, schema, onSuccess, onError } = options || {}; const forceRefreshRef = useRef(false); const params = { dbId, schema }; @@ -85,7 +94,9 @@ export function useTables(options: Params) { options: json.result, hasMore: json.count > json.result.length, }), - enabled: Boolean(dbId && schema), + enabled: Boolean( + dbId && schema && !isFetching && schemaOptionsMap.has(schema), + ), onSuccess, onError, onSettled: () => {