refactor: introduce react-query on api resource hook (#21240)

This commit is contained in:
JUST.in DO IT 2022-09-01 13:39:34 -07:00 committed by GitHub
parent 1aeb8fd6b7
commit 65a11b6f45
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 561 additions and 117 deletions

View File

@ -111,6 +111,7 @@
"react-lines-ellipsis": "^0.15.0",
"react-loadable": "^5.5.0",
"react-markdown": "^4.3.1",
"react-query": "^3.39.2",
"react-redux": "^7.2.0",
"react-resize-detector": "^6.7.6",
"react-reverse-portal": "^2.0.1",
@ -37290,6 +37291,11 @@
"node": ">=0.10.0"
}
},
"node_modules/js-sha3": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz",
"integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q=="
},
"node_modules/js-string-escape": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/js-string-escape/-/js-string-escape-1.0.1.tgz",
@ -45584,6 +45590,55 @@
"react-dom": "^16.6.0 || ^17.0.0"
}
},
"node_modules/react-query": {
"version": "3.39.2",
"resolved": "https://registry.npmjs.org/react-query/-/react-query-3.39.2.tgz",
"integrity": "sha512-F6hYDKyNgDQfQOuR1Rsp3VRzJnWHx6aRnnIZHMNGGgbL3SBgpZTDg8MQwmxOgpCAoqZJA+JSNCydF1xGJqKOCA==",
"dependencies": {
"@babel/runtime": "^7.5.5",
"broadcast-channel": "^3.4.1",
"match-sorter": "^6.0.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
},
"peerDependenciesMeta": {
"react-dom": {
"optional": true
},
"react-native": {
"optional": true
}
}
},
"node_modules/react-query/node_modules/broadcast-channel": {
"version": "3.7.0",
"resolved": "https://registry.npmjs.org/broadcast-channel/-/broadcast-channel-3.7.0.tgz",
"integrity": "sha512-cIAKJXAxGJceNZGTZSBzMxzyOn72cVgPnKx4dc6LRjQgbaJUQqhy5rzL3zbMxkMWsGKkv2hSFkPRMEXfoMZ2Mg==",
"dependencies": {
"@babel/runtime": "^7.7.2",
"detect-node": "^2.1.0",
"js-sha3": "0.8.0",
"microseconds": "0.2.0",
"nano-time": "1.0.0",
"oblivious-set": "1.0.0",
"rimraf": "3.0.2",
"unload": "2.2.0"
}
},
"node_modules/react-query/node_modules/unload": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/unload/-/unload-2.2.0.tgz",
"integrity": "sha512-B60uB5TNBLtN6/LsgAf3udH9saB5p7gqJwcFfbOEZ8BcBHnGwCf6G/TGiEqkRAxX7zAFIUtzdrXQSdL3Q/wqNA==",
"dependencies": {
"@babel/runtime": "^7.6.2",
"detect-node": "^2.0.4"
}
},
"node_modules/react-redux": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.0.tgz",
@ -86719,6 +86774,11 @@
"resolved": "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.6.tgz",
"integrity": "sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g=="
},
"js-sha3": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz",
"integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q=="
},
"js-string-escape": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/js-string-escape/-/js-string-escape-1.0.1.tgz",
@ -93219,6 +93279,42 @@
"react-popper": "^2.2.4"
}
},
"react-query": {
"version": "3.39.2",
"resolved": "https://registry.npmjs.org/react-query/-/react-query-3.39.2.tgz",
"integrity": "sha512-F6hYDKyNgDQfQOuR1Rsp3VRzJnWHx6aRnnIZHMNGGgbL3SBgpZTDg8MQwmxOgpCAoqZJA+JSNCydF1xGJqKOCA==",
"requires": {
"@babel/runtime": "^7.5.5",
"broadcast-channel": "^3.4.1",
"match-sorter": "^6.0.2"
},
"dependencies": {
"broadcast-channel": {
"version": "3.7.0",
"resolved": "https://registry.npmjs.org/broadcast-channel/-/broadcast-channel-3.7.0.tgz",
"integrity": "sha512-cIAKJXAxGJceNZGTZSBzMxzyOn72cVgPnKx4dc6LRjQgbaJUQqhy5rzL3zbMxkMWsGKkv2hSFkPRMEXfoMZ2Mg==",
"requires": {
"@babel/runtime": "^7.7.2",
"detect-node": "^2.1.0",
"js-sha3": "0.8.0",
"microseconds": "0.2.0",
"nano-time": "1.0.0",
"oblivious-set": "1.0.0",
"rimraf": "3.0.2",
"unload": "2.2.0"
}
},
"unload": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/unload/-/unload-2.2.0.tgz",
"integrity": "sha512-B60uB5TNBLtN6/LsgAf3udH9saB5p7gqJwcFfbOEZ8BcBHnGwCf6G/TGiEqkRAxX7zAFIUtzdrXQSdL3Q/wqNA==",
"requires": {
"@babel/runtime": "^7.6.2",
"detect-node": "^2.0.4"
}
}
}
},
"react-redux": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.0.tgz",

View File

@ -175,6 +175,7 @@
"react-lines-ellipsis": "^0.15.0",
"react-loadable": "^5.5.0",
"react-markdown": "^4.3.1",
"react-query": "^3.39.2",
"react-redux": "^7.2.0",
"react-resize-detector": "^6.7.6",
"react-reverse-portal": "^2.0.1",

View File

@ -34,12 +34,14 @@ import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import reducerIndex from 'spec/helpers/reducerIndex';
import { QueryParamProvider } from 'use-query-params';
import QueryProvider from 'src/views/QueryProvider';
type Options = Omit<RenderOptions, 'queries'> & {
useRedux?: boolean;
useDnd?: boolean;
useQueryParams?: boolean;
useRouter?: boolean;
useQuery?: boolean;
initialState?: {};
reducers?: {};
store?: Store;
@ -50,6 +52,7 @@ function createWrapper(options?: Options) {
useDnd,
useRedux,
useQueryParams,
useQuery = true,
useRouter,
initialState,
reducers,
@ -85,6 +88,10 @@ function createWrapper(options?: Options) {
result = <BrowserRouter>{result}</BrowserRouter>;
}
if (useQuery) {
result = <QueryProvider>{result}</QueryProvider>;
}
return result;
};
}

View File

@ -23,6 +23,7 @@ import thunkMiddleware from 'redux-thunk';
import { hot } from 'react-hot-loader/root';
import { ThemeProvider } from '@superset-ui/core';
import { GlobalStyles } from 'src/GlobalStyles';
import QueryProvider from 'src/views/QueryProvider';
import {
initFeatureFlags,
isFeatureEnabled,
@ -134,12 +135,14 @@ if (sqlLabMenu) {
}
const Application = () => (
<Provider store={store}>
<ThemeProvider theme={theme}>
<GlobalStyles />
<App />
</ThemeProvider>
</Provider>
<QueryProvider>
<Provider store={store}>
<ThemeProvider theme={theme}>
<GlobalStyles />
<App />
</ThemeProvider>
</Provider>
</QueryProvider>
);
export default hot(Application);

View File

@ -31,6 +31,7 @@ import {
import AceEditorWrapper from 'src/SqlLab/components/AceEditorWrapper';
import ConnectedSouthPane from 'src/SqlLab/components/SouthPane/state';
import SqlEditor from 'src/SqlLab/components/SqlEditor';
import QueryProvider from 'src/views/QueryProvider';
import SqlEditorLeftBar from 'src/SqlLab/components/SqlEditorLeftBar';
import { AntdDropdown } from 'src/components';
import {
@ -101,9 +102,11 @@ describe('SqlEditor', () => {
const buildWrapper = (props = {}) =>
mount(
<Provider store={store}>
<SqlEditor {...mockedProps} {...props} />
</Provider>,
<QueryProvider>
<Provider store={store}>
<SqlEditor {...mockedProps} {...props} />
</Provider>
</QueryProvider>,
{
wrappingComponent: ThemeProvider,
wrappingComponentProps: { theme: supersetTheme },

View File

@ -31,6 +31,7 @@ import TabbedSqlEditors from 'src/SqlLab/components/TabbedSqlEditors';
import SqlEditor from 'src/SqlLab/components/SqlEditor';
import { table, initialState } from 'src/SqlLab/fixtures';
import { newQueryTabName } from 'src/SqlLab/utils/newQueryTabName';
import QueryProvider from 'src/views/QueryProvider';
fetchMock.get('glob:*/api/v1/database/*', {});
fetchMock.get('glob:*/savedqueryviewapi/api/get/*', {});
@ -89,9 +90,11 @@ describe('TabbedSqlEditors', () => {
const mountWithAct = async () =>
act(async () => {
mount(
<Provider store={store}>
<TabbedSqlEditors {...mockedProps} />
</Provider>,
<QueryProvider>
<Provider store={store}>
<TabbedSqlEditors {...mockedProps} />
</Provider>
</QueryProvider>,
{
wrappingComponent: ThemeProvider,
wrappingComponentProps: { theme: supersetTheme },

View File

@ -32,6 +32,7 @@ import { DatasourceModal } from 'src/components/Datasource';
import DatasourceEditor from 'src/components/Datasource/DatasourceEditor';
import * as featureFlags from 'src/featureFlags';
import mockDatasource from 'spec/fixtures/mockDatasource';
import QueryProvider from 'src/views/QueryProvider';
const mockStore = configureStore([thunk]);
const store = mockStore({});
@ -53,9 +54,11 @@ const mockedProps = {
async function mountAndWait(props = mockedProps) {
const mounted = mount(
<Provider store={store}>
<DatasourceModal {...props} />
</Provider>,
<QueryProvider>
<Provider store={store}>
<DatasourceModal {...props} />
</Provider>
</QueryProvider>,
{
wrappingComponent: ThemeProvider,
wrappingComponentProps: { theme: supersetTheme },

View File

@ -25,7 +25,7 @@ import React, {
} from 'react';
import { SelectValue } from 'antd/lib/select';
import { styled, SupersetClient, t } from '@superset-ui/core';
import { styled, t } from '@superset-ui/core';
import { Select } from 'src/components';
import { FormLabel } from 'src/components/Form';
import Icons from 'src/components/Icons';
@ -37,6 +37,7 @@ import CertifiedBadge from 'src/components/CertifiedBadge';
import WarningIconWithTooltip from 'src/components/WarningIconWithTooltip';
import { useToasts } from 'src/components/MessageToasts/withToasts';
import { SchemaOption } from 'src/SqlLab/types';
import { useTables, Table } from 'src/hooks/apiResources';
const TableSelectorWrapper = styled.div`
${({ theme }) => `
@ -101,19 +102,6 @@ interface TableSelectorProps {
tableSelectMode?: 'single' | 'multiple';
}
interface Table {
label: string;
value: string;
type: string;
extra?: {
certification?: {
certified_by: string;
details: string;
};
warning_markdown?: string;
};
}
interface TableOption {
label: JSX.Element;
text: string;
@ -147,6 +135,15 @@ const TableOption = ({ table }: { table: Table }) => {
);
};
function renderSelectRow(select: ReactNode, refreshBtn: ReactNode) {
return (
<div className="section">
<span className="select">{select}</span>
<span className="refresh">{refreshBtn}</span>
</div>
);
}
const TableSelector: FunctionComponent<TableSelectorProps> = ({
database,
emptyState,
@ -166,34 +163,50 @@ const TableSelector: FunctionComponent<TableSelectorProps> = ({
tableValue = undefined,
onTableSelectChange,
}) => {
const [currentDatabase, setCurrentDatabase] = useState<
DatabaseObject | null | undefined
>(database);
const { addSuccessToast } = useToasts();
const [currentSchema, setCurrentSchema] = useState<string | undefined>(
schema,
);
const [tableOptions, setTableOptions] = useState<TableOption[]>([]);
const [tableSelectValue, setTableSelectValue] = useState<
SelectValue | undefined
>(undefined);
const [refresh, setRefresh] = useState(0);
const [previousRefresh, setPreviousRefresh] = useState(0);
const [loadingTables, setLoadingTables] = useState(false);
const { addSuccessToast } = useToasts();
const {
data,
isFetching: loadingTables,
isFetched,
refetch,
} = useTables({
dbId: database?.id,
schema: currentSchema,
onSuccess: (data: { options: Table[] }) => {
onTablesLoad?.(data.options);
if (isFetched) {
addSuccessToast('List updated');
}
},
onError: () => handleError(t('There was an error loading the tables')),
});
const tableOptions = useMemo<TableOption[]>(
() =>
data
? data.options.map(table => ({
value: table.value,
label: <TableOption table={table} />,
text: table.label,
}))
: [],
[data],
);
useEffect(() => {
// reset selections
if (database === undefined) {
setCurrentDatabase(undefined);
setCurrentSchema(undefined);
setTableSelectValue(undefined);
}
}, [database, tableSelectMode]);
useEffect(() => {
setCurrentDatabase(database);
}, [database]);
useEffect(() => {
if (tableSelectMode === 'single') {
setTableSelectValue(
@ -208,56 +221,6 @@ const TableSelector: FunctionComponent<TableSelectorProps> = ({
}
}, [tableOptions, tableValue, tableSelectMode]);
useEffect(() => {
if (currentDatabase && currentSchema) {
setLoadingTables(true);
const encodedSchema = encodeURIComponent(currentSchema);
const forceRefresh = refresh !== previousRefresh;
// TODO: Would be nice to add pagination in a follow-up. Needs endpoint changes.
const endpoint = encodeURI(
`/superset/tables/${currentDatabase.id}/${encodedSchema}/undefined/${forceRefresh}/`,
);
if (previousRefresh !== refresh) {
setPreviousRefresh(refresh);
}
SupersetClient.get({ endpoint })
.then(({ json }) => {
const options: TableOption[] = json.options.map((table: Table) => {
const option: TableOption = {
value: table.value,
label: <TableOption table={table} />,
text: table.label,
};
return option;
});
onTablesLoad?.(json.options);
setTableOptions(options);
setLoadingTables(false);
if (forceRefresh) addSuccessToast('List updated');
})
.catch(() => {
setLoadingTables(false);
handleError(t('There was an error loading the tables'));
});
}
// We are using the refresh state to re-trigger the query
// previousRefresh should be out of dependencies array
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentDatabase, currentSchema, onTablesLoad, setTableOptions, refresh]);
function renderSelectRow(select: ReactNode, refreshBtn: ReactNode) {
return (
<div className="section">
<span className="select">{select}</span>
<span className="refresh">{refreshBtn}</span>
</div>
);
}
const internalTableChange = (
selectedOptions: TableOption | TableOption[] | undefined,
) => {
@ -274,7 +237,6 @@ const TableSelector: FunctionComponent<TableSelectorProps> = ({
};
const internalDbChange = (db: DatabaseObject) => {
setCurrentDatabase(db);
if (onDbChange) {
onDbChange(db);
}
@ -286,14 +248,15 @@ const TableSelector: FunctionComponent<TableSelectorProps> = ({
onSchemaChange(schema);
}
internalTableChange(undefined);
const value = tableSelectMode === 'single' ? undefined : [];
internalTableChange(value);
};
function renderDatabaseSelector() {
return (
<DatabaseSelector
key={currentDatabase?.id}
db={currentDatabase}
key={database?.id}
db={database}
emptyState={emptyState}
formMode={formMode}
getDbList={getDbList}
@ -353,7 +316,7 @@ const TableSelector: FunctionComponent<TableSelectorProps> = ({
const refreshLabel = !formMode && !readOnly && (
<RefreshLabel
onClick={() => setRefresh(refresh + 1)}
onClick={() => refetch()}
tooltipContent={t('Force refresh table list')}
/>
);

View File

@ -28,3 +28,4 @@ export {
// different files for different resource types.
export * from './charts';
export * from './dashboards';
export * from './tables';

View File

@ -0,0 +1,221 @@
/**
* 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 { 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: {
options: [
{
id: 1,
name: 'fake api result1',
label: 'fake api label1',
},
{
id: 2,
name: 'fake api result2',
label: 'fake api label2',
},
],
tableLength: 2,
},
};
const fakeHasMoreApiResult = {
json: {
options: [
{
id: 1,
name: 'fake api result1',
label: 'fake api label1',
},
{
id: 2,
name: 'fake api result2',
label: 'fake api label2',
},
],
tableLength: 4,
},
};
const expectedData = {
...fakeApiResult.json,
hasMore: false,
};
const expectedHasMoreData = {
...fakeHasMoreApiResult.json,
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(() => {
jest.useRealTimers();
});
it('returns api response mapping json options', async () => {
const expectDbId = 'db1';
const expectedSchema = 'schemaA';
const forceRefresh = false;
const { result } = renderHook(
() =>
useTables({
dbId: expectDbId,
schema: expectedSchema,
}),
{
wrapper: QueryProvider,
},
);
await act(async () => {
jest.runAllTimers();
});
expect(SupersetClient.get).toHaveBeenCalledTimes(1);
expect(SupersetClient.get).toHaveBeenCalledWith({
endpoint: `/superset/tables/${expectDbId}/${expectedSchema}/undefined/${forceRefresh}/`,
});
expect(result.current.data).toEqual(expectedData);
await act(async () => {
result.current.refetch();
});
expect(SupersetClient.get).toHaveBeenCalledTimes(2);
expect(SupersetClient.get).toHaveBeenCalledWith({
endpoint: `/superset/tables/${expectDbId}/${expectedSchema}/undefined/true/`,
});
expect(result.current.data).toEqual(expectedData);
});
it('returns api response for search keyword', async () => {
const expectDbId = 'db1';
const expectedSchema = 'schemaA';
const expectedKeyword = 'my work';
const forceRefresh = false;
renderHook(
() =>
useTables({
dbId: expectDbId,
schema: expectedSchema,
keyword: expectedKeyword,
}),
{
wrapper: QueryProvider,
},
);
await act(async () => {
jest.runAllTimers();
});
expect(SupersetClient.get).toHaveBeenCalledTimes(1);
expect(SupersetClient.get).toHaveBeenCalledWith({
endpoint: `/superset/tables/${expectDbId}/${expectedSchema}/${encodeURIComponent(
expectedKeyword,
)}/${forceRefresh}/`,
});
});
it('returns hasMore when total is larger than result size', async () => {
(SupersetClient.get as jest.Mock).mockResolvedValueOnce(
fakeHasMoreApiResult,
);
const expectDbId = 'db1';
const expectedSchema = 'schemaA';
const { result } = renderHook(
() =>
useTables({
dbId: expectDbId,
schema: expectedSchema,
}),
{
wrapper: QueryProvider,
},
);
await act(async () => {
jest.runAllTimers();
});
expect(SupersetClient.get).toHaveBeenCalledTimes(1);
expect(result.current.data).toEqual(expectedHasMoreData);
});
it('returns cached data without api request', async () => {
const expectDbId = 'db1';
const expectedSchema = 'schemaA';
const { result, rerender } = renderHook(
() =>
useTables({
dbId: expectDbId,
schema: expectedSchema,
}),
{
wrapper: QueryProvider,
},
);
await act(async () => {
jest.runAllTimers();
});
expect(SupersetClient.get).toHaveBeenCalledTimes(1);
rerender();
expect(SupersetClient.get).toHaveBeenCalledTimes(1);
expect(result.current.data).toEqual(expectedData);
});
it('returns refreshed data after expires', async () => {
const expectDbId = 'db1';
const expectedSchema = 'schemaA';
const { result, rerender } = renderHook(
() =>
useTables({
dbId: expectDbId,
schema: expectedSchema,
}),
{
wrapper: QueryProvider,
},
);
await act(async () => {
jest.runAllTimers();
});
expect(SupersetClient.get).toHaveBeenCalledTimes(1);
rerender();
await act(async () => {
jest.runAllTimers();
});
expect(SupersetClient.get).toHaveBeenCalledTimes(1);
queryClient.clear();
rerender();
await act(async () => {
jest.runAllTimers();
});
expect(SupersetClient.get).toHaveBeenCalledTimes(2);
expect(result.current.data).toEqual(expectedData);
});
});

View File

@ -0,0 +1,97 @@
/**
* 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 { SupersetClient } from '@superset-ui/core';
export type FetchTablesQueryParams = {
dbId?: string | number;
schema?: string;
forceRefresh?: boolean;
keyword?: string;
};
export interface Table {
label: string;
value: string;
type: string;
extra?: {
certification?: {
certified_by: string;
details: string;
};
warning_markdown?: string;
};
}
type QueryData = {
json: { options: Table[]; tableLength: number };
response: Response;
};
export type Data = QueryData['json'] & {
hasMore: boolean;
};
export function fetchTables({
dbId,
schema,
forceRefresh,
keyword,
}: FetchTablesQueryParams) {
const encodedSchema = schema ? encodeURIComponent(schema) : '';
const encodedKeyword = keyword ? encodeURIComponent(keyword) : 'undefined';
// TODO: Would be nice to add pagination in a follow-up. Needs endpoint changes.
const endpoint = `/superset/tables/${
dbId ?? 'undefined'
}/${encodedSchema}/${encodedKeyword}/${forceRefresh}/`;
return SupersetClient.get({ endpoint }) as Promise<QueryData>;
}
type Params = FetchTablesQueryParams &
Pick<UseQueryOptions, 'onSuccess' | 'onError'>;
export function useTables(options: Params) {
const { dbId, schema, keyword, onSuccess, onError } = options || {};
const forceRefreshRef = useRef(false);
const params = { dbId, schema, keyword };
const result = useQuery<QueryData, Error, Data>(
['tables', { dbId, schema, keyword }],
() => fetchTables({ ...params, forceRefresh: forceRefreshRef.current }),
{
select: ({ json }) => ({
...json,
hasMore: json.tableLength > json.options.length,
}),
enabled: Boolean(dbId && schema),
onSuccess,
onError,
onSettled: () => {
forceRefreshRef.current = false;
},
},
);
return {
...result,
refetch: () => {
forceRefreshRef.current = true;
return result.refetch();
},
};
}

View File

@ -36,6 +36,7 @@ import { routes, isFrontendRoute } from 'src/views/routes';
import { Logger } from 'src/logger/LogUtils';
import { RootContextProviders } from './RootContextProviders';
import { ScrollToTop } from './ScrollToTop';
import QueryProvider from './QueryProvider';
setupApp();
setupPlugins();
@ -60,26 +61,28 @@ const LocationPathnameLogger = () => {
};
const App = () => (
<Router>
<ScrollToTop />
<LocationPathnameLogger />
<RootContextProviders>
<GlobalStyles />
<Menu data={menu} isFrontendRoute={isFrontendRoute} />
<Switch>
{routes.map(({ path, Component, props = {}, Fallback = Loading }) => (
<Route path={path} key={path}>
<Suspense fallback={<Fallback />}>
<ErrorBoundary>
<Component user={user} {...props} />
</ErrorBoundary>
</Suspense>
</Route>
))}
</Switch>
<ToastContainer />
</RootContextProviders>
</Router>
<QueryProvider>
<Router>
<ScrollToTop />
<LocationPathnameLogger />
<RootContextProviders>
<GlobalStyles />
<Menu data={menu} isFrontendRoute={isFrontendRoute} />
<Switch>
{routes.map(({ path, Component, props = {}, Fallback = Loading }) => (
<Route path={path} key={path}>
<Suspense fallback={<Fallback />}>
<ErrorBoundary>
<Component user={user} {...props} />
</ErrorBoundary>
</Suspense>
</Route>
))}
</Switch>
<ToastContainer />
</RootContextProviders>
</Router>
</QueryProvider>
);
export default hot(App);

View File

@ -0,0 +1,43 @@
/**
* 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 React from 'react';
import { QueryClient, QueryClientProvider } from 'react-query';
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: Infinity,
retry: false,
retryOnMount: false,
refetchOnMount: false,
refetchOnReconnect: false,
refetchOnWindowFocus: false,
},
},
});
type Props = {
children: React.ReactNode;
};
const Queryprovider: React.FC<Props> = ({ children }) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
export default Queryprovider;