mirror of
https://github.com/apache/superset.git
synced 2024-09-18 19:49:37 -04:00
fix(sqllab): Invalid schema fetch by deprecated value (#22968)
This commit is contained in:
parent
a7bb14e433
commit
d3d59ee0ae
@ -41,8 +41,13 @@ const middlewares = [thunk];
|
|||||||
const mockStore = configureStore(middlewares);
|
const mockStore = configureStore(middlewares);
|
||||||
const store = mockStore(initialState);
|
const store = mockStore(initialState);
|
||||||
|
|
||||||
fetchMock.get('glob:*/api/v1/database/*/schemas/?*', { result: [] });
|
beforeEach(() => {
|
||||||
fetchMock.get('glob:*/api/v1/database/*/tables/*', {
|
fetchMock.get('glob:*/api/v1/database/?*', { result: [] });
|
||||||
|
fetchMock.get('glob:*/api/v1/database/*/schemas/?*', {
|
||||||
|
count: 2,
|
||||||
|
result: ['main', 'new_schema'],
|
||||||
|
});
|
||||||
|
fetchMock.get('glob:*/api/v1/database/*/tables/*', {
|
||||||
count: 1,
|
count: 1,
|
||||||
result: [
|
result: [
|
||||||
{
|
{
|
||||||
@ -50,6 +55,11 @@ fetchMock.get('glob:*/api/v1/database/*/tables/*', {
|
|||||||
value: 'ab_user',
|
value: 'ab_user',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
fetchMock.restore();
|
||||||
});
|
});
|
||||||
|
|
||||||
const renderAndWait = (props, store) =>
|
const renderAndWait = (props, store) =>
|
||||||
@ -110,8 +120,9 @@ test('should toggle the table when the header is clicked', async () => {
|
|||||||
userEvent.click(header);
|
userEvent.click(header);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(store.getActions()).toHaveLength(4);
|
expect(store.getActions()[store.getActions().length - 1].type).toEqual(
|
||||||
expect(store.getActions()[3].type).toEqual('COLLAPSE_TABLE');
|
'COLLAPSE_TABLE',
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -129,14 +140,55 @@ test('When changing database the table list must be updated', async () => {
|
|||||||
database_name: 'new_db',
|
database_name: 'new_db',
|
||||||
backend: 'postgresql',
|
backend: 'postgresql',
|
||||||
}}
|
}}
|
||||||
queryEditor={{ ...mockedProps.queryEditor, schema: 'new_schema' }}
|
queryEditorId={defaultQueryEditor.id}
|
||||||
tables={[{ ...mockedProps.tables[0], dbId: 2, name: 'new_table' }]}
|
tables={[{ ...mockedProps.tables[0], dbId: 2, name: 'new_table' }]}
|
||||||
/>,
|
/>,
|
||||||
{
|
{
|
||||||
useRedux: true,
|
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_db/i)).toBeInTheDocument();
|
||||||
expect(await screen.findByText(/new_table/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(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
@ -18,14 +18,13 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import fetchMock from 'fetch-mock';
|
||||||
import { render, screen, waitFor } from 'spec/helpers/testing-library';
|
import { render, screen, waitFor } from 'spec/helpers/testing-library';
|
||||||
import { SupersetClient } from '@superset-ui/core';
|
import { queryClient } from 'src/views/QueryProvider';
|
||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
import DatabaseSelector, { DatabaseSelectorProps } from '.';
|
import DatabaseSelector, { DatabaseSelectorProps } from '.';
|
||||||
import { EmptyStateSmall } from '../EmptyState';
|
import { EmptyStateSmall } from '../EmptyState';
|
||||||
|
|
||||||
const SupersetClientGet = jest.spyOn(SupersetClient, 'get');
|
|
||||||
|
|
||||||
const createProps = (): DatabaseSelectorProps => ({
|
const createProps = (): DatabaseSelectorProps => ({
|
||||||
db: {
|
db: {
|
||||||
id: 1,
|
id: 1,
|
||||||
@ -35,7 +34,7 @@ const createProps = (): DatabaseSelectorProps => ({
|
|||||||
formMode: false,
|
formMode: false,
|
||||||
isDatabaseSelectEnabled: true,
|
isDatabaseSelectEnabled: true,
|
||||||
readOnly: false,
|
readOnly: false,
|
||||||
schema: undefined,
|
schema: 'public',
|
||||||
sqlLabMode: true,
|
sqlLabMode: true,
|
||||||
getDbList: jest.fn(),
|
getDbList: jest.fn(),
|
||||||
handleError: jest.fn(),
|
handleError: jest.fn(),
|
||||||
@ -44,22 +43,7 @@ const createProps = (): DatabaseSelectorProps => ({
|
|||||||
onSchemasLoad: jest.fn(),
|
onSchemasLoad: jest.fn(),
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
const fakeDatabaseApiResult = {
|
||||||
jest.resetAllMocks();
|
|
||||||
SupersetClientGet.mockImplementation(
|
|
||||||
async ({ endpoint }: { endpoint: string }) => {
|
|
||||||
if (endpoint.includes('schemas')) {
|
|
||||||
return {
|
|
||||||
json: { result: ['information_schema', 'public'] },
|
|
||||||
} as any;
|
|
||||||
}
|
|
||||||
if (endpoint.includes('/function_names')) {
|
|
||||||
return {
|
|
||||||
json: { function_names: [] },
|
|
||||||
} as any;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
json: {
|
|
||||||
count: 2,
|
count: 2,
|
||||||
description_columns: {},
|
description_columns: {},
|
||||||
ids: [1, 2],
|
ids: [1, 2],
|
||||||
@ -158,10 +142,34 @@ beforeEach(() => {
|
|||||||
id: 2,
|
id: 2,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
};
|
||||||
} as any;
|
|
||||||
},
|
const fakeSchemaApiResult = {
|
||||||
);
|
count: 2,
|
||||||
|
result: ['information_schema', 'public'],
|
||||||
|
};
|
||||||
|
|
||||||
|
const fakeFunctionNamesApiResult = {
|
||||||
|
function_names: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const databaseApiRoute = 'glob:*/api/v1/database/?*';
|
||||||
|
const schemaApiRoute = 'glob:*/api/v1/database/*/schemas/?*';
|
||||||
|
const tablesApiRoute = 'glob:*/api/v1/database/*/tables/*';
|
||||||
|
|
||||||
|
function setupFetchMock() {
|
||||||
|
fetchMock.get(databaseApiRoute, fakeDatabaseApiResult);
|
||||||
|
fetchMock.get(schemaApiRoute, fakeSchemaApiResult);
|
||||||
|
fetchMock.get(tablesApiRoute, fakeFunctionNamesApiResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
queryClient.clear();
|
||||||
|
setupFetchMock();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
fetchMock.reset();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Should render', async () => {
|
test('Should render', async () => {
|
||||||
@ -175,6 +183,8 @@ test('Refresh should work', async () => {
|
|||||||
|
|
||||||
render(<DatabaseSelector {...props} />, { useRedux: true });
|
render(<DatabaseSelector {...props} />, { useRedux: true });
|
||||||
|
|
||||||
|
expect(fetchMock.calls(schemaApiRoute).length).toBe(0);
|
||||||
|
|
||||||
const select = screen.getByRole('combobox', {
|
const select = screen.getByRole('combobox', {
|
||||||
name: 'Select schema or type schema name',
|
name: 'Select schema or type schema name',
|
||||||
});
|
});
|
||||||
@ -182,23 +192,22 @@ test('Refresh should work', async () => {
|
|||||||
userEvent.click(select);
|
userEvent.click(select);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(SupersetClientGet).toBeCalledTimes(2);
|
expect(fetchMock.calls(databaseApiRoute).length).toBe(1);
|
||||||
expect(props.getDbList).toBeCalledTimes(0);
|
expect(fetchMock.calls(schemaApiRoute).length).toBe(1);
|
||||||
expect(props.handleError).toBeCalledTimes(0);
|
expect(props.handleError).toBeCalledTimes(0);
|
||||||
expect(props.onDbChange).toBeCalledTimes(0);
|
expect(props.onDbChange).toBeCalledTimes(0);
|
||||||
expect(props.onSchemaChange).toBeCalledTimes(0);
|
expect(props.onSchemaChange).toBeCalledTimes(0);
|
||||||
expect(props.onSchemasLoad).toBeCalledTimes(0);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// click schema reload
|
||||||
userEvent.click(screen.getByRole('button', { name: 'refresh' }));
|
userEvent.click(screen.getByRole('button', { name: 'refresh' }));
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(SupersetClientGet).toBeCalledTimes(3);
|
expect(fetchMock.calls(databaseApiRoute).length).toBe(1);
|
||||||
expect(props.getDbList).toBeCalledTimes(1);
|
expect(fetchMock.calls(schemaApiRoute).length).toBe(2);
|
||||||
expect(props.handleError).toBeCalledTimes(0);
|
expect(props.handleError).toBeCalledTimes(0);
|
||||||
expect(props.onDbChange).toBeCalledTimes(0);
|
expect(props.onDbChange).toBeCalledTimes(0);
|
||||||
expect(props.onSchemaChange).toBeCalledTimes(0);
|
expect(props.onSchemaChange).toBeCalledTimes(0);
|
||||||
expect(props.onSchemasLoad).toBeCalledTimes(2);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -214,9 +223,10 @@ test('Should database select display options', async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('should show empty state if there are no options', async () => {
|
test('should show empty state if there are no options', async () => {
|
||||||
SupersetClientGet.mockImplementation(
|
fetchMock.reset();
|
||||||
async () => ({ json: { result: [] } } as any),
|
fetchMock.get(databaseApiRoute, { result: [] });
|
||||||
);
|
fetchMock.get(schemaApiRoute, { result: [] });
|
||||||
|
fetchMock.get(tablesApiRoute, { result: [] });
|
||||||
const props = createProps();
|
const props = createProps();
|
||||||
render(
|
render(
|
||||||
<DatabaseSelector
|
<DatabaseSelector
|
||||||
|
@ -24,10 +24,7 @@ import Label from 'src/components/Label';
|
|||||||
import { FormLabel } from 'src/components/Form';
|
import { FormLabel } from 'src/components/Form';
|
||||||
import RefreshLabel from 'src/components/RefreshLabel';
|
import RefreshLabel from 'src/components/RefreshLabel';
|
||||||
import { useToasts } from 'src/components/MessageToasts/withToasts';
|
import { useToasts } from 'src/components/MessageToasts/withToasts';
|
||||||
import {
|
import { useSchemas, SchemaOption } from 'src/hooks/apiResources';
|
||||||
getClientErrorMessage,
|
|
||||||
getClientErrorObject,
|
|
||||||
} from 'src/utils/getClientErrorObject';
|
|
||||||
|
|
||||||
const DatabaseSelectorWrapper = styled.div`
|
const DatabaseSelectorWrapper = styled.div`
|
||||||
${({ theme }) => `
|
${({ theme }) => `
|
||||||
@ -86,8 +83,6 @@ export type DatabaseObject = {
|
|||||||
backend: string;
|
backend: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type SchemaValue = { label: string; value: string };
|
|
||||||
|
|
||||||
export interface DatabaseSelectorProps {
|
export interface DatabaseSelectorProps {
|
||||||
db?: DatabaseObject | null;
|
db?: DatabaseObject | null;
|
||||||
emptyState?: ReactNode;
|
emptyState?: ReactNode;
|
||||||
@ -119,6 +114,8 @@ const SelectLabel = ({
|
|||||||
</LabelStyle>
|
</LabelStyle>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const EMPTY_SCHEMA_OPTIONS: SchemaOption[] = [];
|
||||||
|
|
||||||
export default function DatabaseSelector({
|
export default function DatabaseSelector({
|
||||||
db,
|
db,
|
||||||
formMode = false,
|
formMode = false,
|
||||||
@ -134,13 +131,10 @@ export default function DatabaseSelector({
|
|||||||
schema,
|
schema,
|
||||||
sqlLabMode = false,
|
sqlLabMode = false,
|
||||||
}: DatabaseSelectorProps) {
|
}: DatabaseSelectorProps) {
|
||||||
const [loadingSchemas, setLoadingSchemas] = useState(false);
|
|
||||||
const [schemaOptions, setSchemaOptions] = useState<SchemaValue[]>([]);
|
|
||||||
const [currentDb, setCurrentDb] = useState<DatabaseValue | undefined>();
|
const [currentDb, setCurrentDb] = useState<DatabaseValue | undefined>();
|
||||||
const [currentSchema, setCurrentSchema] = useState<SchemaValue | undefined>(
|
const [currentSchema, setCurrentSchema] = useState<SchemaOption | undefined>(
|
||||||
schema ? { label: schema, value: schema } : undefined,
|
schema ? { label: schema, value: schema, title: schema } : undefined,
|
||||||
);
|
);
|
||||||
const [refresh, setRefresh] = useState(0);
|
|
||||||
const { addSuccessToast } = useToasts();
|
const { addSuccessToast } = useToasts();
|
||||||
|
|
||||||
const loadDatabases = useMemo(
|
const loadDatabases = useMemo(
|
||||||
@ -221,48 +215,37 @@ export default function DatabaseSelector({
|
|||||||
);
|
);
|
||||||
}, [db]);
|
}, [db]);
|
||||||
|
|
||||||
function changeSchema(schema: SchemaValue) {
|
function changeSchema(schema: SchemaOption | undefined) {
|
||||||
setCurrentSchema(schema);
|
setCurrentSchema(schema);
|
||||||
if (onSchemaChange) {
|
if (onSchemaChange) {
|
||||||
onSchemaChange(schema.value);
|
onSchemaChange(schema?.value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
const {
|
||||||
if (currentDb) {
|
data,
|
||||||
setLoadingSchemas(true);
|
isFetching: loadingSchemas,
|
||||||
const queryParams = rison.encode({ force: refresh > 0 });
|
isFetched,
|
||||||
const endpoint = `/api/v1/database/${currentDb.value}/schemas/?q=${queryParams}`;
|
refetch,
|
||||||
|
} = useSchemas({
|
||||||
|
dbId: currentDb?.value,
|
||||||
|
onSuccess: data => {
|
||||||
|
onSchemasLoad?.(data);
|
||||||
|
|
||||||
// TODO: Would be nice to add pagination in a follow-up. Needs endpoint changes.
|
if (data.length === 1) {
|
||||||
SupersetClient.get({ endpoint })
|
changeSchema(data[0]);
|
||||||
.then(({ json }) => {
|
} else if (!data.find(schemaOption => schema === schemaOption.value)) {
|
||||||
const options = json.result.map((s: string) => ({
|
changeSchema(undefined);
|
||||||
value: s,
|
|
||||||
label: s,
|
|
||||||
title: s,
|
|
||||||
}));
|
|
||||||
if (onSchemasLoad) {
|
|
||||||
onSchemasLoad(options);
|
|
||||||
}
|
}
|
||||||
setSchemaOptions(options);
|
|
||||||
setLoadingSchemas(false);
|
if (isFetched) {
|
||||||
if (options.length === 1) changeSchema(options[0]);
|
addSuccessToast('List refreshed');
|
||||||
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]);
|
},
|
||||||
|
onError: () => handleError(t('There was an error loading the schemas')),
|
||||||
|
});
|
||||||
|
|
||||||
|
const schemaOptions = data || EMPTY_SCHEMA_OPTIONS;
|
||||||
|
|
||||||
function changeDataBase(
|
function changeDataBase(
|
||||||
value: { label: string; value: number },
|
value: { label: string; value: number },
|
||||||
@ -309,7 +292,7 @@ export default function DatabaseSelector({
|
|||||||
function renderSchemaSelect() {
|
function renderSchemaSelect() {
|
||||||
const refreshIcon = !readOnly && (
|
const refreshIcon = !readOnly && (
|
||||||
<RefreshLabel
|
<RefreshLabel
|
||||||
onClick={() => setRefresh(refresh + 1)}
|
onClick={() => refetch()}
|
||||||
tooltipContent={t('Force refresh schema list')}
|
tooltipContent={t('Force refresh schema list')}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@ -323,7 +306,7 @@ export default function DatabaseSelector({
|
|||||||
name="select-schema"
|
name="select-schema"
|
||||||
notFoundContent={t('No compatible schema found')}
|
notFoundContent={t('No compatible schema found')}
|
||||||
placeholder={t('Select schema or type schema name')}
|
placeholder={t('Select schema or type schema name')}
|
||||||
onChange={item => changeSchema(item as SchemaValue)}
|
onChange={item => changeSchema(item as SchemaOption)}
|
||||||
options={schemaOptions}
|
options={schemaOptions}
|
||||||
showSearch
|
showSearch
|
||||||
value={currentSchema}
|
value={currentSchema}
|
||||||
|
@ -19,13 +19,12 @@
|
|||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { render, screen, waitFor, within } from 'spec/helpers/testing-library';
|
import { render, screen, waitFor, within } from 'spec/helpers/testing-library';
|
||||||
import { SupersetClient } from '@superset-ui/core';
|
import { queryClient } from 'src/views/QueryProvider';
|
||||||
|
import fetchMock from 'fetch-mock';
|
||||||
import { act } from 'react-dom/test-utils';
|
import { act } from 'react-dom/test-utils';
|
||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
import TableSelector, { TableSelectorMultiple } from '.';
|
import TableSelector, { TableSelectorMultiple } from '.';
|
||||||
|
|
||||||
const SupersetClientGet = jest.spyOn(SupersetClient, 'get');
|
|
||||||
|
|
||||||
const createProps = (props = {}) => ({
|
const createProps = (props = {}) => ({
|
||||||
database: {
|
database: {
|
||||||
id: 1,
|
id: 1,
|
||||||
@ -37,20 +36,13 @@ const createProps = (props = {}) => ({
|
|||||||
...props,
|
...props,
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
const getSchemaMockFunction = () =>
|
||||||
jest.clearAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
const getSchemaMockFunction = async () =>
|
|
||||||
({
|
({
|
||||||
json: {
|
|
||||||
result: ['schema_a', 'schema_b'],
|
result: ['schema_a', 'schema_b'],
|
||||||
},
|
|
||||||
} as any);
|
} as any);
|
||||||
|
|
||||||
const getTableMockFunction = async () =>
|
const getTableMockFunction = () =>
|
||||||
({
|
({
|
||||||
json: {
|
|
||||||
count: 4,
|
count: 4,
|
||||||
result: [
|
result: [
|
||||||
{ label: 'table_a', value: 'table_a' },
|
{ label: 'table_a', value: 'table_a' },
|
||||||
@ -58,16 +50,29 @@ const getTableMockFunction = async () =>
|
|||||||
{ label: 'table_c', value: 'table_c' },
|
{ label: 'table_c', value: 'table_c' },
|
||||||
{ label: 'table_d', value: 'table_d' },
|
{ label: 'table_d', value: 'table_d' },
|
||||||
],
|
],
|
||||||
},
|
|
||||||
} as any);
|
} as any);
|
||||||
|
|
||||||
|
const databaseApiRoute = 'glob:*/api/v1/database/?*';
|
||||||
|
const schemaApiRoute = 'glob:*/api/v1/database/*/schemas/?*';
|
||||||
|
const tablesApiRoute = 'glob:*/api/v1/database/*/tables/*';
|
||||||
|
|
||||||
const getSelectItemContainer = (select: HTMLElement) =>
|
const getSelectItemContainer = (select: HTMLElement) =>
|
||||||
select.parentElement?.parentElement?.getElementsByClassName(
|
select.parentElement?.parentElement?.getElementsByClassName(
|
||||||
'ant-select-selection-item',
|
'ant-select-selection-item',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
queryClient.clear();
|
||||||
|
fetchMock.get(databaseApiRoute, { result: [] });
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
fetchMock.reset();
|
||||||
|
});
|
||||||
|
|
||||||
test('renders with default props', async () => {
|
test('renders with default props', async () => {
|
||||||
SupersetClientGet.mockImplementation(getTableMockFunction);
|
fetchMock.get(schemaApiRoute, { result: [] });
|
||||||
|
fetchMock.get(tablesApiRoute, getTableMockFunction());
|
||||||
|
|
||||||
const props = createProps();
|
const props = createProps();
|
||||||
render(<TableSelector {...props} />, { useRedux: true });
|
render(<TableSelector {...props} />, { useRedux: true });
|
||||||
@ -88,7 +93,8 @@ test('renders with default props', async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('renders table options', async () => {
|
test('renders table options', async () => {
|
||||||
SupersetClientGet.mockImplementation(getTableMockFunction);
|
fetchMock.get(schemaApiRoute, { result: ['test_schema'] });
|
||||||
|
fetchMock.get(tablesApiRoute, getTableMockFunction());
|
||||||
|
|
||||||
const props = createProps();
|
const props = createProps();
|
||||||
render(<TableSelector {...props} />, { useRedux: true });
|
render(<TableSelector {...props} />, { useRedux: true });
|
||||||
@ -105,7 +111,8 @@ test('renders table options', async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('renders disabled without schema', async () => {
|
test('renders disabled without schema', async () => {
|
||||||
SupersetClientGet.mockImplementation(getTableMockFunction);
|
fetchMock.get(schemaApiRoute, { result: [] });
|
||||||
|
fetchMock.get(tablesApiRoute, getTableMockFunction());
|
||||||
|
|
||||||
const props = createProps();
|
const props = createProps();
|
||||||
render(<TableSelector {...props} schema={undefined} />, { useRedux: true });
|
render(<TableSelector {...props} schema={undefined} />, { useRedux: true });
|
||||||
@ -118,7 +125,7 @@ test('renders disabled without schema', async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('table options are notified after schema selection', async () => {
|
test('table options are notified after schema selection', async () => {
|
||||||
SupersetClientGet.mockImplementation(getSchemaMockFunction);
|
fetchMock.get(schemaApiRoute, getSchemaMockFunction());
|
||||||
|
|
||||||
const callback = jest.fn();
|
const callback = jest.fn();
|
||||||
const props = createProps({
|
const props = createProps({
|
||||||
@ -142,7 +149,7 @@ test('table options are notified after schema selection', async () => {
|
|||||||
await screen.findByRole('option', { name: 'schema_b' }),
|
await screen.findByRole('option', { name: 'schema_b' }),
|
||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
|
|
||||||
SupersetClientGet.mockImplementation(getTableMockFunction);
|
fetchMock.get(tablesApiRoute, getTableMockFunction());
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
userEvent.click(screen.getAllByText('schema_a')[1]);
|
userEvent.click(screen.getAllByText('schema_a')[1]);
|
||||||
@ -159,7 +166,8 @@ test('table options are notified after schema selection', async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('table select retain value if not in SQL Lab mode', async () => {
|
test('table select retain value if not in SQL Lab mode', async () => {
|
||||||
SupersetClientGet.mockImplementation(getTableMockFunction);
|
fetchMock.get(schemaApiRoute, { result: ['test_schema'] });
|
||||||
|
fetchMock.get(tablesApiRoute, getTableMockFunction());
|
||||||
|
|
||||||
const callback = jest.fn();
|
const callback = jest.fn();
|
||||||
const props = createProps({
|
const props = createProps({
|
||||||
@ -182,7 +190,7 @@ test('table select retain value if not in SQL Lab mode', async () => {
|
|||||||
await screen.findByRole('option', { name: 'table_a' }),
|
await screen.findByRole('option', { name: 'table_a' }),
|
||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
|
|
||||||
act(() => {
|
await waitFor(() => {
|
||||||
userEvent.click(screen.getAllByText('table_a')[1]);
|
userEvent.click(screen.getAllByText('table_a')[1]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -199,7 +207,8 @@ test('table select retain value if not in SQL Lab mode', async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('table multi select retain all the values selected', async () => {
|
test('table multi select retain all the values selected', async () => {
|
||||||
SupersetClientGet.mockImplementation(getTableMockFunction);
|
fetchMock.get(schemaApiRoute, { result: ['test_schema'] });
|
||||||
|
fetchMock.get(tablesApiRoute, getTableMockFunction());
|
||||||
|
|
||||||
const callback = jest.fn();
|
const callback = jest.fn();
|
||||||
const props = createProps({
|
const props = createProps({
|
||||||
@ -217,23 +226,19 @@ test('table multi select retain all the values selected', async () => {
|
|||||||
|
|
||||||
userEvent.click(tableSelect);
|
userEvent.click(tableSelect);
|
||||||
|
|
||||||
act(() => {
|
await waitFor(async () => {
|
||||||
const item = screen.getAllByText('table_b');
|
const item = await screen.findAllByText('table_b');
|
||||||
userEvent.click(item[item.length - 1]);
|
userEvent.click(item[item.length - 1]);
|
||||||
});
|
});
|
||||||
|
|
||||||
act(() => {
|
await waitFor(async () => {
|
||||||
const item = screen.getAllByText('table_c');
|
const item = await screen.findAllByText('table_c');
|
||||||
userEvent.click(item[item.length - 1]);
|
userEvent.click(item[item.length - 1]);
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(screen.getByRole('option', { name: 'table_b' })).toHaveAttribute(
|
const selection1 = await screen.findByRole('option', { name: 'table_b' });
|
||||||
'aria-selected',
|
expect(selection1).toHaveAttribute('aria-selected', 'true');
|
||||||
'true',
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(screen.getByRole('option', { name: 'table_c' })).toHaveAttribute(
|
const selection2 = await screen.findByRole('option', { name: 'table_c' });
|
||||||
'aria-selected',
|
expect(selection2).toHaveAttribute('aria-selected', 'true');
|
||||||
'true',
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
@ -275,26 +275,6 @@ const TableSelector: FunctionComponent<TableSelectorProps> = ({
|
|||||||
internalTableChange(value);
|
internalTableChange(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
function renderDatabaseSelector() {
|
|
||||||
return (
|
|
||||||
<DatabaseSelector
|
|
||||||
db={database}
|
|
||||||
emptyState={emptyState}
|
|
||||||
formMode={formMode}
|
|
||||||
getDbList={getDbList}
|
|
||||||
handleError={handleError}
|
|
||||||
onDbChange={readOnly ? undefined : internalDbChange}
|
|
||||||
onEmptyResults={onEmptyResults}
|
|
||||||
onSchemaChange={readOnly ? undefined : internalSchemaChange}
|
|
||||||
onSchemasLoad={onSchemasLoad}
|
|
||||||
schema={currentSchema}
|
|
||||||
sqlLabMode={sqlLabMode}
|
|
||||||
isDatabaseSelectEnabled={isDatabaseSelectEnabled && !readOnly}
|
|
||||||
readOnly={readOnly}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleFilterOption = useMemo(
|
const handleFilterOption = useMemo(
|
||||||
() => (search: string, option: TableOption) => {
|
() => (search: string, option: TableOption) => {
|
||||||
const searchValue = search.trim().toLowerCase();
|
const searchValue = search.trim().toLowerCase();
|
||||||
@ -346,7 +326,21 @@ const TableSelector: FunctionComponent<TableSelectorProps> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<TableSelectorWrapper>
|
<TableSelectorWrapper>
|
||||||
{renderDatabaseSelector()}
|
<DatabaseSelector
|
||||||
|
db={database}
|
||||||
|
emptyState={emptyState}
|
||||||
|
formMode={formMode}
|
||||||
|
getDbList={getDbList}
|
||||||
|
handleError={handleError}
|
||||||
|
onDbChange={readOnly ? undefined : internalDbChange}
|
||||||
|
onEmptyResults={onEmptyResults}
|
||||||
|
onSchemaChange={readOnly ? undefined : internalSchemaChange}
|
||||||
|
onSchemasLoad={onSchemasLoad}
|
||||||
|
schema={currentSchema}
|
||||||
|
sqlLabMode={sqlLabMode}
|
||||||
|
isDatabaseSelectEnabled={isDatabaseSelectEnabled && !readOnly}
|
||||||
|
readOnly={readOnly}
|
||||||
|
/>
|
||||||
{sqlLabMode && !formMode && <div className="divider" />}
|
{sqlLabMode && !formMode && <div className="divider" />}
|
||||||
{renderTableSelect()}
|
{renderTableSelect()}
|
||||||
</TableSelectorWrapper>
|
</TableSelectorWrapper>
|
||||||
|
@ -29,3 +29,4 @@ export {
|
|||||||
export * from './charts';
|
export * from './charts';
|
||||||
export * from './dashboards';
|
export * from './dashboards';
|
||||||
export * from './tables';
|
export * from './tables';
|
||||||
|
export * from './schemas';
|
||||||
|
138
superset-frontend/src/hooks/apiResources/schemas.test.ts
Normal file
138
superset-frontend/src/hooks/apiResources/schemas.test.ts
Normal file
@ -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);
|
||||||
|
});
|
||||||
|
});
|
80
superset-frontend/src/hooks/apiResources/schemas.ts
Normal file
80
superset-frontend/src/hooks/apiResources/schemas.ts
Normal file
@ -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<QueryData>;
|
||||||
|
}
|
||||||
|
|
||||||
|
type Params = FetchSchemasQueryParams &
|
||||||
|
Pick<UseQueryOptions<SchemaOption[]>, 'onSuccess' | 'onError'>;
|
||||||
|
|
||||||
|
export function useSchemas(options: Params) {
|
||||||
|
const { dbId, onSuccess, onError } = options || {};
|
||||||
|
const forceRefreshRef = useRef(false);
|
||||||
|
const params = { dbId };
|
||||||
|
const result = useQuery<QueryData, Error, SchemaOption[]>(
|
||||||
|
['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();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
@ -16,13 +16,13 @@
|
|||||||
* specific language governing permissions and limitations
|
* specific language governing permissions and limitations
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
|
import rison from 'rison';
|
||||||
|
import fetchMock from 'fetch-mock';
|
||||||
import { act, renderHook } from '@testing-library/react-hooks';
|
import { act, renderHook } from '@testing-library/react-hooks';
|
||||||
import { SupersetClient } from '@superset-ui/core';
|
|
||||||
import QueryProvider, { queryClient } from 'src/views/QueryProvider';
|
import QueryProvider, { queryClient } from 'src/views/QueryProvider';
|
||||||
import { useTables } from './tables';
|
import { useTables } from './tables';
|
||||||
|
|
||||||
const fakeApiResult = {
|
const fakeApiResult = {
|
||||||
json: {
|
|
||||||
count: 2,
|
count: 2,
|
||||||
result: [
|
result: [
|
||||||
{
|
{
|
||||||
@ -36,11 +36,9 @@ const fakeApiResult = {
|
|||||||
label: 'fake api label2',
|
label: 'fake api label2',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const fakeHasMoreApiResult = {
|
const fakeHasMoreApiResult = {
|
||||||
json: {
|
|
||||||
count: 4,
|
count: 4,
|
||||||
result: [
|
result: [
|
||||||
{
|
{
|
||||||
@ -54,40 +52,40 @@ const fakeHasMoreApiResult = {
|
|||||||
label: 'fake api label2',
|
label: 'fake api label2',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const fakeSchemaApiResult = ['schema1', 'schema2'];
|
||||||
|
|
||||||
const expectedData = {
|
const expectedData = {
|
||||||
options: [...fakeApiResult.json.result],
|
options: fakeApiResult.result,
|
||||||
hasMore: false,
|
hasMore: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
const expectedHasMoreData = {
|
const expectedHasMoreData = {
|
||||||
options: [...fakeHasMoreApiResult.json.result],
|
options: fakeHasMoreApiResult.result,
|
||||||
hasMore: true,
|
hasMore: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
jest.mock('@superset-ui/core', () => ({
|
|
||||||
SupersetClient: {
|
|
||||||
get: jest.fn().mockResolvedValue(fakeApiResult),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
describe('useTables hook', () => {
|
describe('useTables hook', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
(SupersetClient.get as jest.Mock).mockClear();
|
|
||||||
queryClient.clear();
|
queryClient.clear();
|
||||||
jest.useFakeTimers();
|
jest.useFakeTimers();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
fetchMock.reset();
|
||||||
jest.useRealTimers();
|
jest.useRealTimers();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns api response mapping json options', async () => {
|
test('returns api response mapping json options', async () => {
|
||||||
const expectDbId = 'db1';
|
const expectDbId = 'db1';
|
||||||
const expectedSchema = 'schemaA';
|
const expectedSchema = 'schema1';
|
||||||
const forceRefresh = false;
|
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(
|
const { result } = renderHook(
|
||||||
() =>
|
() =>
|
||||||
useTables({
|
useTables({
|
||||||
@ -101,29 +99,73 @@ describe('useTables hook', () => {
|
|||||||
await act(async () => {
|
await act(async () => {
|
||||||
jest.runAllTimers();
|
jest.runAllTimers();
|
||||||
});
|
});
|
||||||
expect(SupersetClient.get).toHaveBeenCalledTimes(1);
|
expect(fetchMock.calls(schemaApiRoute).length).toBe(1);
|
||||||
expect(SupersetClient.get).toHaveBeenCalledWith({
|
expect(
|
||||||
endpoint: `/api/v1/database/${expectDbId}/tables/?q=(force:!${
|
fetchMock.calls(
|
||||||
forceRefresh ? 't' : 'f'
|
`end:api/v1/database/${expectDbId}/tables/?q=${rison.encode({
|
||||||
},schema_name:${expectedSchema})`,
|
force: false,
|
||||||
});
|
schema_name: expectedSchema,
|
||||||
|
})}`,
|
||||||
|
).length,
|
||||||
|
).toBe(1);
|
||||||
expect(result.current.data).toEqual(expectedData);
|
expect(result.current.data).toEqual(expectedData);
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
result.current.refetch();
|
result.current.refetch();
|
||||||
});
|
});
|
||||||
expect(SupersetClient.get).toHaveBeenCalledTimes(2);
|
expect(fetchMock.calls(schemaApiRoute).length).toBe(1);
|
||||||
expect(SupersetClient.get).toHaveBeenCalledWith({
|
expect(
|
||||||
endpoint: `/api/v1/database/${expectDbId}/tables/?q=(force:!t,schema_name:${expectedSchema})`,
|
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);
|
expect(result.current.data).toEqual(expectedData);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns hasMore when total is larger than result size', async () => {
|
test('skips the deprecated schema option', async () => {
|
||||||
(SupersetClient.get as jest.Mock).mockResolvedValueOnce(
|
|
||||||
fakeHasMoreApiResult,
|
|
||||||
);
|
|
||||||
const expectDbId = 'db1';
|
const expectDbId = 'db1';
|
||||||
const expectedSchema = 'schemaA';
|
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 = '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(
|
const { result } = renderHook(
|
||||||
() =>
|
() =>
|
||||||
useTables({
|
useTables({
|
||||||
@ -137,13 +179,18 @@ describe('useTables hook', () => {
|
|||||||
await act(async () => {
|
await act(async () => {
|
||||||
jest.runAllTimers();
|
jest.runAllTimers();
|
||||||
});
|
});
|
||||||
expect(SupersetClient.get).toHaveBeenCalledTimes(1);
|
expect(fetchMock.calls(tableApiRoute).length).toBe(1);
|
||||||
expect(result.current.data).toEqual(expectedHasMoreData);
|
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 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(
|
const { result, rerender } = renderHook(
|
||||||
() =>
|
() =>
|
||||||
useTables({
|
useTables({
|
||||||
@ -157,15 +204,20 @@ describe('useTables hook', () => {
|
|||||||
await act(async () => {
|
await act(async () => {
|
||||||
jest.runAllTimers();
|
jest.runAllTimers();
|
||||||
});
|
});
|
||||||
expect(SupersetClient.get).toHaveBeenCalledTimes(1);
|
expect(fetchMock.calls(tableApiRoute).length).toBe(1);
|
||||||
rerender();
|
rerender();
|
||||||
expect(SupersetClient.get).toHaveBeenCalledTimes(1);
|
expect(fetchMock.calls(tableApiRoute).length).toBe(1);
|
||||||
expect(result.current.data).toEqual(expectedData);
|
expect(result.current.data).toEqual(expectedData);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns refreshed data after expires', async () => {
|
test('returns refreshed data after expires', async () => {
|
||||||
const expectDbId = 'db1';
|
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(
|
const { result, rerender } = renderHook(
|
||||||
() =>
|
() =>
|
||||||
useTables({
|
useTables({
|
||||||
@ -179,18 +231,18 @@ describe('useTables hook', () => {
|
|||||||
await act(async () => {
|
await act(async () => {
|
||||||
jest.runAllTimers();
|
jest.runAllTimers();
|
||||||
});
|
});
|
||||||
expect(SupersetClient.get).toHaveBeenCalledTimes(1);
|
expect(fetchMock.calls(tableApiRoute).length).toBe(1);
|
||||||
rerender();
|
rerender();
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
jest.runAllTimers();
|
jest.runAllTimers();
|
||||||
});
|
});
|
||||||
expect(SupersetClient.get).toHaveBeenCalledTimes(1);
|
expect(fetchMock.calls(tableApiRoute).length).toBe(1);
|
||||||
queryClient.clear();
|
queryClient.clear();
|
||||||
rerender();
|
rerender();
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
jest.runAllTimers();
|
jest.runAllTimers();
|
||||||
});
|
});
|
||||||
expect(SupersetClient.get).toHaveBeenCalledTimes(2);
|
expect(fetchMock.calls(tableApiRoute).length).toBe(2);
|
||||||
expect(result.current.data).toEqual(expectedData);
|
expect(result.current.data).toEqual(expectedData);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -16,11 +16,13 @@
|
|||||||
* specific language governing permissions and limitations
|
* specific language governing permissions and limitations
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import { useRef } from 'react';
|
import { useRef, useMemo } from 'react';
|
||||||
import { useQuery, UseQueryOptions } from 'react-query';
|
import { useQuery, UseQueryOptions } from 'react-query';
|
||||||
import rison from 'rison';
|
import rison from 'rison';
|
||||||
import { SupersetClient } from '@superset-ui/core';
|
import { SupersetClient } from '@superset-ui/core';
|
||||||
|
|
||||||
|
import { useSchemas } from './schemas';
|
||||||
|
|
||||||
export type FetchTablesQueryParams = {
|
export type FetchTablesQueryParams = {
|
||||||
dbId?: string | number;
|
dbId?: string | number;
|
||||||
schema?: string;
|
schema?: string;
|
||||||
@ -71,9 +73,16 @@ export function fetchTables({
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Params = FetchTablesQueryParams &
|
type Params = FetchTablesQueryParams &
|
||||||
Pick<UseQueryOptions, 'onSuccess' | 'onError'>;
|
Pick<UseQueryOptions<Data>, 'onSuccess' | 'onError'>;
|
||||||
|
|
||||||
export function useTables(options: Params) {
|
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 { dbId, schema, onSuccess, onError } = options || {};
|
||||||
const forceRefreshRef = useRef(false);
|
const forceRefreshRef = useRef(false);
|
||||||
const params = { dbId, schema };
|
const params = { dbId, schema };
|
||||||
@ -85,7 +94,9 @@ export function useTables(options: Params) {
|
|||||||
options: json.result,
|
options: json.result,
|
||||||
hasMore: json.count > json.result.length,
|
hasMore: json.count > json.result.length,
|
||||||
}),
|
}),
|
||||||
enabled: Boolean(dbId && schema),
|
enabled: Boolean(
|
||||||
|
dbId && schema && !isFetching && schemaOptionsMap.has(schema),
|
||||||
|
),
|
||||||
onSuccess,
|
onSuccess,
|
||||||
onError,
|
onError,
|
||||||
onSettled: () => {
|
onSettled: () => {
|
||||||
|
Loading…
Reference in New Issue
Block a user