mirror of
https://github.com/apache/superset.git
synced 2024-09-06 13:57:40 -04:00
fix(sqllab): flaky json explore modal due to over-rendering (#26156)
This commit is contained in:
parent
39c6488463
commit
f30f685eb5
@ -45,7 +45,7 @@ export interface QueryAutoRefreshProps {
|
|||||||
|
|
||||||
// returns true if the Query.state matches one of the specifc values indicating the query is still processing on server
|
// returns true if the Query.state matches one of the specifc values indicating the query is still processing on server
|
||||||
export const isQueryRunning = (q: Query): boolean =>
|
export const isQueryRunning = (q: Query): boolean =>
|
||||||
runningQueryStateList.includes(q?.state);
|
runningQueryStateList.includes(q?.state) && !q?.resultsKey;
|
||||||
|
|
||||||
// returns true if at least one query is running and within the max age to poll timeframe
|
// returns true if at least one query is running and within the max age to poll timeframe
|
||||||
export const shouldCheckForQueries = (queryList: QueryDictionary): boolean => {
|
export const shouldCheckForQueries = (queryList: QueryDictionary): boolean => {
|
||||||
|
@ -19,9 +19,10 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { render, screen } from 'spec/helpers/testing-library';
|
import { render, screen } from 'spec/helpers/testing-library';
|
||||||
import QueryHistory from 'src/SqlLab/components/QueryHistory';
|
import QueryHistory from 'src/SqlLab/components/QueryHistory';
|
||||||
|
import { initialState } from 'src/SqlLab/fixtures';
|
||||||
|
|
||||||
const mockedProps = {
|
const mockedProps = {
|
||||||
queries: [],
|
queryEditorId: 123,
|
||||||
displayLimit: 1000,
|
displayLimit: 1000,
|
||||||
latestQueryId: 'yhMUZCGb',
|
latestQueryId: 'yhMUZCGb',
|
||||||
};
|
};
|
||||||
@ -32,7 +33,7 @@ const setup = (overrides = {}) => (
|
|||||||
|
|
||||||
describe('QueryHistory', () => {
|
describe('QueryHistory', () => {
|
||||||
it('Renders an empty state for query history', () => {
|
it('Renders an empty state for query history', () => {
|
||||||
render(setup());
|
render(setup(), { useRedux: true, initialState });
|
||||||
|
|
||||||
const emptyStateText = screen.getByText(
|
const emptyStateText = screen.getByText(
|
||||||
/run a query to display query history/i,
|
/run a query to display query history/i,
|
||||||
|
@ -16,13 +16,15 @@
|
|||||||
* specific language governing permissions and limitations
|
* specific language governing permissions and limitations
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import React from 'react';
|
import React, { useMemo } from 'react';
|
||||||
|
import { shallowEqual, useSelector } from 'react-redux';
|
||||||
import { EmptyStateMedium } from 'src/components/EmptyState';
|
import { EmptyStateMedium } from 'src/components/EmptyState';
|
||||||
import { t, styled, QueryResponse } from '@superset-ui/core';
|
import { t, styled } from '@superset-ui/core';
|
||||||
import QueryTable from 'src/SqlLab/components/QueryTable';
|
import QueryTable from 'src/SqlLab/components/QueryTable';
|
||||||
|
import { SqlLabRootState } from 'src/SqlLab/types';
|
||||||
|
|
||||||
interface QueryHistoryProps {
|
interface QueryHistoryProps {
|
||||||
queries: QueryResponse[];
|
queryEditorId: string | number;
|
||||||
displayLimit: number;
|
displayLimit: number;
|
||||||
latestQueryId: string | undefined;
|
latestQueryId: string | undefined;
|
||||||
}
|
}
|
||||||
@ -39,11 +41,23 @@ const StyledEmptyStateWrapper = styled.div`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const QueryHistory = ({
|
const QueryHistory = ({
|
||||||
queries,
|
queryEditorId,
|
||||||
displayLimit,
|
displayLimit,
|
||||||
latestQueryId,
|
latestQueryId,
|
||||||
}: QueryHistoryProps) =>
|
}: QueryHistoryProps) => {
|
||||||
queries.length > 0 ? (
|
const queries = useSelector(
|
||||||
|
({ sqlLab: { queries } }: SqlLabRootState) => queries,
|
||||||
|
shallowEqual,
|
||||||
|
);
|
||||||
|
const editorQueries = useMemo(
|
||||||
|
() =>
|
||||||
|
Object.values(queries).filter(
|
||||||
|
({ sqlEditorId }) => String(sqlEditorId) === String(queryEditorId),
|
||||||
|
),
|
||||||
|
[queries, queryEditorId],
|
||||||
|
);
|
||||||
|
|
||||||
|
return editorQueries.length > 0 ? (
|
||||||
<QueryTable
|
<QueryTable
|
||||||
columns={[
|
columns={[
|
||||||
'state',
|
'state',
|
||||||
@ -55,7 +69,7 @@ const QueryHistory = ({
|
|||||||
'results',
|
'results',
|
||||||
'actions',
|
'actions',
|
||||||
]}
|
]}
|
||||||
queries={queries}
|
queries={editorQueries}
|
||||||
displayLimit={displayLimit}
|
displayLimit={displayLimit}
|
||||||
latestQueryId={latestQueryId}
|
latestQueryId={latestQueryId}
|
||||||
/>
|
/>
|
||||||
@ -67,5 +81,6 @@ const QueryHistory = ({
|
|||||||
/>
|
/>
|
||||||
</StyledEmptyStateWrapper>
|
</StyledEmptyStateWrapper>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default QueryHistory;
|
export default QueryHistory;
|
||||||
|
@ -251,8 +251,7 @@ const QueryTable = ({
|
|||||||
modalBody={
|
modalBody={
|
||||||
<ResultSet
|
<ResultSet
|
||||||
showSql
|
showSql
|
||||||
user={user}
|
queryId={query.id}
|
||||||
query={query}
|
|
||||||
height={400}
|
height={400}
|
||||||
displayLimit={displayLimit}
|
displayLimit={displayLimit}
|
||||||
defaultQueryLimit={1000}
|
defaultQueryLimit={1000}
|
||||||
|
@ -37,65 +37,91 @@ import {
|
|||||||
|
|
||||||
const mockedProps = {
|
const mockedProps = {
|
||||||
cache: true,
|
cache: true,
|
||||||
query: queries[0],
|
queryId: queries[0].id,
|
||||||
height: 140,
|
height: 140,
|
||||||
database: { allows_virtual_table_explore: true },
|
database: { allows_virtual_table_explore: true },
|
||||||
user,
|
displayLimit: 1000,
|
||||||
defaultQueryLimit: 1000,
|
defaultQueryLimit: 1000,
|
||||||
};
|
};
|
||||||
const stoppedQueryProps = { ...mockedProps, query: stoppedQuery };
|
const stoppedQueryState = {
|
||||||
const runningQueryProps = { ...mockedProps, query: runningQuery };
|
...initialState,
|
||||||
const fetchingQueryProps = {
|
sqlLab: {
|
||||||
...mockedProps,
|
...initialState.sqlLab,
|
||||||
query: {
|
queries: {
|
||||||
dbId: 1,
|
[stoppedQuery.id]: stoppedQuery,
|
||||||
cached: false,
|
|
||||||
ctas: false,
|
|
||||||
id: 'ryhHUZCGb',
|
|
||||||
progress: 100,
|
|
||||||
state: 'fetching',
|
|
||||||
startDttm: Date.now() - 500,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const cachedQueryProps = { ...mockedProps, query: cachedQuery };
|
|
||||||
const failedQueryWithErrorMessageProps = {
|
|
||||||
...mockedProps,
|
|
||||||
query: failedQueryWithErrorMessage,
|
|
||||||
};
|
|
||||||
const failedQueryWithErrorsProps = {
|
|
||||||
...mockedProps,
|
|
||||||
query: failedQueryWithErrors,
|
|
||||||
};
|
|
||||||
const newProps = {
|
|
||||||
query: {
|
|
||||||
cached: false,
|
|
||||||
resultsKey: 'new key',
|
|
||||||
results: {
|
|
||||||
data: [{ a: 1 }],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
const runningQueryState = {
|
||||||
|
...initialState,
|
||||||
|
sqlLab: {
|
||||||
|
...initialState.sqlLab,
|
||||||
|
queries: {
|
||||||
|
[runningQuery.id]: runningQuery,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const fetchingQueryState = {
|
||||||
|
...initialState,
|
||||||
|
sqlLab: {
|
||||||
|
...initialState.sqlLab,
|
||||||
|
queries: {
|
||||||
|
[mockedProps.queryId]: {
|
||||||
|
dbId: 1,
|
||||||
|
cached: false,
|
||||||
|
ctas: false,
|
||||||
|
id: 'ryhHUZCGb',
|
||||||
|
progress: 100,
|
||||||
|
state: 'fetching',
|
||||||
|
startDttm: Date.now() - 500,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const cachedQueryState = {
|
||||||
|
...initialState,
|
||||||
|
sqlLab: {
|
||||||
|
...initialState.sqlLab,
|
||||||
|
queries: {
|
||||||
|
[cachedQuery.id]: cachedQuery,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const failedQueryWithErrorMessageState = {
|
||||||
|
...initialState,
|
||||||
|
sqlLab: {
|
||||||
|
...initialState.sqlLab,
|
||||||
|
queries: {
|
||||||
|
[failedQueryWithErrorMessage.id]: failedQueryWithErrorMessage,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const failedQueryWithErrorsState = {
|
||||||
|
...initialState,
|
||||||
|
sqlLab: {
|
||||||
|
...initialState.sqlLab,
|
||||||
|
queries: {
|
||||||
|
[failedQueryWithErrors.id]: failedQueryWithErrors,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const newProps = {
|
||||||
|
displayLimit: 1001,
|
||||||
|
};
|
||||||
const asyncQueryProps = {
|
const asyncQueryProps = {
|
||||||
...mockedProps,
|
...mockedProps,
|
||||||
database: { allow_run_async: true },
|
database: { allow_run_async: true },
|
||||||
};
|
};
|
||||||
const asyncRefetchDataPreviewProps = {
|
|
||||||
...asyncQueryProps,
|
const reRunQueryEndpoint = 'glob:*/api/v1/sqllab/execute/';
|
||||||
query: {
|
|
||||||
state: 'success',
|
|
||||||
results: undefined,
|
|
||||||
isDataPreview: true,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const asyncRefetchResultsTableProps = {
|
|
||||||
...asyncQueryProps,
|
|
||||||
query: {
|
|
||||||
state: 'success',
|
|
||||||
results: undefined,
|
|
||||||
resultsKey: 'async results key',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
fetchMock.get('glob:*/api/v1/dataset/?*', { result: [] });
|
fetchMock.get('glob:*/api/v1/dataset/?*', { result: [] });
|
||||||
|
fetchMock.post(reRunQueryEndpoint, { result: [] });
|
||||||
|
fetchMock.get('glob:*/api/v1/sqllab/results/*', { result: [] });
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fetchMock.resetHistory();
|
||||||
|
});
|
||||||
|
|
||||||
const middlewares = [thunk];
|
const middlewares = [thunk];
|
||||||
const mockStore = configureStore(middlewares);
|
const mockStore = configureStore(middlewares);
|
||||||
@ -107,25 +133,47 @@ const setup = (props?: any, store?: Store) =>
|
|||||||
|
|
||||||
describe('ResultSet', () => {
|
describe('ResultSet', () => {
|
||||||
test('renders a Table', async () => {
|
test('renders a Table', async () => {
|
||||||
const { getByTestId } = setup(mockedProps, mockStore(initialState));
|
const { getByTestId } = setup(
|
||||||
|
mockedProps,
|
||||||
|
mockStore({
|
||||||
|
...initialState,
|
||||||
|
user,
|
||||||
|
sqlLab: {
|
||||||
|
...initialState.sqlLab,
|
||||||
|
queries: {
|
||||||
|
[queries[0].id]: queries[0],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
const table = getByTestId('table-container');
|
const table = getByTestId('table-container');
|
||||||
expect(table).toBeInTheDocument();
|
expect(table).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should render success query', async () => {
|
test('should render success query', async () => {
|
||||||
|
const query = queries[0];
|
||||||
const { queryAllByText, getByTestId } = setup(
|
const { queryAllByText, getByTestId } = setup(
|
||||||
mockedProps,
|
mockedProps,
|
||||||
mockStore(initialState),
|
mockStore({
|
||||||
|
...initialState,
|
||||||
|
user,
|
||||||
|
sqlLab: {
|
||||||
|
...initialState.sqlLab,
|
||||||
|
queries: {
|
||||||
|
[query.id]: query,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
const table = getByTestId('table-container');
|
const table = getByTestId('table-container');
|
||||||
expect(table).toBeInTheDocument();
|
expect(table).toBeInTheDocument();
|
||||||
|
|
||||||
const firstColumn = queryAllByText(
|
const firstColumn = queryAllByText(
|
||||||
mockedProps.query.results?.columns[0].column_name ?? '',
|
query.results?.columns[0].column_name ?? '',
|
||||||
)[0];
|
)[0];
|
||||||
const secondColumn = queryAllByText(
|
const secondColumn = queryAllByText(
|
||||||
mockedProps.query.results?.columns[1].column_name ?? '',
|
query.results?.columns[1].column_name ?? '',
|
||||||
)[0];
|
)[0];
|
||||||
expect(firstColumn).toBeInTheDocument();
|
expect(firstColumn).toBeInTheDocument();
|
||||||
expect(secondColumn).toBeInTheDocument();
|
expect(secondColumn).toBeInTheDocument();
|
||||||
@ -135,12 +183,24 @@ describe('ResultSet', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('should render empty results', async () => {
|
test('should render empty results', async () => {
|
||||||
const props = {
|
const query = {
|
||||||
...mockedProps,
|
...queries[0],
|
||||||
query: { ...mockedProps.query, results: { data: [] } },
|
results: { data: [] },
|
||||||
};
|
};
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
setup(props, mockStore(initialState));
|
setup(
|
||||||
|
mockedProps,
|
||||||
|
mockStore({
|
||||||
|
...initialState,
|
||||||
|
user,
|
||||||
|
sqlLab: {
|
||||||
|
...initialState.sqlLab,
|
||||||
|
queries: {
|
||||||
|
[query.id]: query,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const alert = screen.getByRole('alert');
|
const alert = screen.getByRole('alert');
|
||||||
@ -149,42 +209,70 @@ describe('ResultSet', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('should call reRunQuery if timed out', async () => {
|
test('should call reRunQuery if timed out', async () => {
|
||||||
const store = mockStore(initialState);
|
const query = {
|
||||||
const propsWithError = {
|
...queries[0],
|
||||||
...mockedProps,
|
errorMessage: 'Your session timed out',
|
||||||
query: { ...queries[0], errorMessage: 'Your session timed out' },
|
|
||||||
};
|
};
|
||||||
|
const store = mockStore({
|
||||||
|
...initialState,
|
||||||
|
user,
|
||||||
|
sqlLab: {
|
||||||
|
...initialState.sqlLab,
|
||||||
|
queries: {
|
||||||
|
[query.id]: query,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
setup(propsWithError, store);
|
expect(fetchMock.calls(reRunQueryEndpoint)).toHaveLength(0);
|
||||||
|
setup(mockedProps, store);
|
||||||
expect(store.getActions()).toHaveLength(1);
|
expect(store.getActions()).toHaveLength(1);
|
||||||
expect(store.getActions()[0].query.errorMessage).toEqual(
|
expect(store.getActions()[0].query.errorMessage).toEqual(
|
||||||
'Your session timed out',
|
'Your session timed out',
|
||||||
);
|
);
|
||||||
expect(store.getActions()[0].type).toEqual('START_QUERY');
|
expect(store.getActions()[0].type).toEqual('START_QUERY');
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(fetchMock.calls(reRunQueryEndpoint)).toHaveLength(1),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should not call reRunQuery if no error', async () => {
|
test('should not call reRunQuery if no error', async () => {
|
||||||
const store = mockStore(initialState);
|
const query = queries[0];
|
||||||
|
const store = mockStore({
|
||||||
|
...initialState,
|
||||||
|
user,
|
||||||
|
sqlLab: {
|
||||||
|
...initialState.sqlLab,
|
||||||
|
queries: {
|
||||||
|
[query.id]: query,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
setup(mockedProps, store);
|
setup(mockedProps, store);
|
||||||
expect(store.getActions()).toEqual([]);
|
expect(store.getActions()).toEqual([]);
|
||||||
|
expect(fetchMock.calls(reRunQueryEndpoint)).toHaveLength(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should render cached query', async () => {
|
test('should render cached query', async () => {
|
||||||
const store = mockStore(initialState);
|
const store = mockStore(cachedQueryState);
|
||||||
const { rerender } = setup(cachedQueryProps, store);
|
const { rerender } = setup(
|
||||||
|
{ ...mockedProps, queryId: cachedQuery.id },
|
||||||
|
store,
|
||||||
|
);
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
rerender(<ResultSet {...newProps} />);
|
rerender(<ResultSet {...mockedProps} {...newProps} />);
|
||||||
expect(store.getActions()).toHaveLength(2);
|
expect(store.getActions()).toHaveLength(1);
|
||||||
expect(store.getActions()[0].query.results).toEqual(
|
expect(store.getActions()[0].query.results).toEqual(cachedQuery.results);
|
||||||
cachedQueryProps.query.results,
|
|
||||||
);
|
|
||||||
expect(store.getActions()[0].type).toEqual('CLEAR_QUERY_RESULTS');
|
expect(store.getActions()[0].type).toEqual('CLEAR_QUERY_RESULTS');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should render stopped query', async () => {
|
test('should render stopped query', async () => {
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
setup(stoppedQueryProps, mockStore(initialState));
|
setup(
|
||||||
|
{ ...mockedProps, queryId: stoppedQuery.id },
|
||||||
|
mockStore(stoppedQueryState),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const alert = screen.getByRole('alert');
|
const alert = screen.getByRole('alert');
|
||||||
@ -192,15 +280,18 @@ describe('ResultSet', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('should render running/pending/fetching query', async () => {
|
test('should render running/pending/fetching query', async () => {
|
||||||
const { getByTestId } = setup(runningQueryProps, mockStore(initialState));
|
const { getByTestId } = setup(
|
||||||
|
{ ...mockedProps, queryId: runningQuery.id },
|
||||||
|
mockStore(runningQueryState),
|
||||||
|
);
|
||||||
const progressBar = getByTestId('progress-bar');
|
const progressBar = getByTestId('progress-bar');
|
||||||
expect(progressBar).toBeInTheDocument();
|
expect(progressBar).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should render fetching w/ 100 progress query', async () => {
|
test('should render fetching w/ 100 progress query', async () => {
|
||||||
const { getByRole, getByText } = setup(
|
const { getByRole, getByText } = setup(
|
||||||
fetchingQueryProps,
|
mockedProps,
|
||||||
mockStore(initialState),
|
mockStore(fetchingQueryState),
|
||||||
);
|
);
|
||||||
const loading = getByRole('status');
|
const loading = getByRole('status');
|
||||||
expect(loading).toBeInTheDocument();
|
expect(loading).toBeInTheDocument();
|
||||||
@ -209,7 +300,10 @@ describe('ResultSet', () => {
|
|||||||
|
|
||||||
test('should render a failed query with an error message', async () => {
|
test('should render a failed query with an error message', async () => {
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
setup(failedQueryWithErrorMessageProps, mockStore(initialState));
|
setup(
|
||||||
|
{ ...mockedProps, queryId: failedQueryWithErrorMessage.id },
|
||||||
|
mockStore(failedQueryWithErrorMessageState),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(screen.getByText('Database error')).toBeInTheDocument();
|
expect(screen.getByText('Database error')).toBeInTheDocument();
|
||||||
@ -218,44 +312,129 @@ describe('ResultSet', () => {
|
|||||||
|
|
||||||
test('should render a failed query with an errors object', async () => {
|
test('should render a failed query with an errors object', async () => {
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
setup(failedQueryWithErrorsProps, mockStore(initialState));
|
setup(
|
||||||
|
{ ...mockedProps, queryId: failedQueryWithErrors.id },
|
||||||
|
mockStore(failedQueryWithErrorsState),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
expect(screen.getByText('Database error')).toBeInTheDocument();
|
expect(screen.getByText('Database error')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('renders if there is no limit in query.results but has queryLimit', async () => {
|
test('renders if there is no limit in query.results but has queryLimit', async () => {
|
||||||
|
const query = {
|
||||||
|
...queries[0],
|
||||||
|
};
|
||||||
|
await waitFor(() => {
|
||||||
|
setup(
|
||||||
|
mockedProps,
|
||||||
|
mockStore({
|
||||||
|
...initialState,
|
||||||
|
user,
|
||||||
|
sqlLab: {
|
||||||
|
...initialState.sqlLab,
|
||||||
|
queries: {
|
||||||
|
[query.id]: query,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
const { getByRole } = setup(mockedProps, mockStore(initialState));
|
const { getByRole } = setup(mockedProps, mockStore(initialState));
|
||||||
expect(getByRole('table')).toBeInTheDocument();
|
expect(getByRole('table')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('renders if there is a limit in query.results but not queryLimit', async () => {
|
test('renders if there is a limit in query.results but not queryLimit', async () => {
|
||||||
const props = { ...mockedProps, query: queryWithNoQueryLimit };
|
const props = { ...mockedProps, queryId: queryWithNoQueryLimit.id };
|
||||||
const { getByRole } = setup(props, mockStore(initialState));
|
const { getByRole } = setup(
|
||||||
|
props,
|
||||||
|
mockStore({
|
||||||
|
...initialState,
|
||||||
|
user,
|
||||||
|
sqlLab: {
|
||||||
|
...initialState.sqlLab,
|
||||||
|
queries: {
|
||||||
|
[queryWithNoQueryLimit.id]: queryWithNoQueryLimit,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
expect(getByRole('table')).toBeInTheDocument();
|
expect(getByRole('table')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Async queries - renders "Fetch data preview" button when data preview has no results', () => {
|
test('Async queries - renders "Fetch data preview" button when data preview has no results', () => {
|
||||||
setup(asyncRefetchDataPreviewProps, mockStore(initialState));
|
const asyncRefetchDataPreviewQuery = {
|
||||||
|
...queries[0],
|
||||||
|
state: 'success',
|
||||||
|
results: undefined,
|
||||||
|
isDataPreview: true,
|
||||||
|
};
|
||||||
|
setup(
|
||||||
|
{ ...asyncQueryProps, queryId: asyncRefetchDataPreviewQuery.id },
|
||||||
|
mockStore({
|
||||||
|
...initialState,
|
||||||
|
user,
|
||||||
|
sqlLab: {
|
||||||
|
...initialState.sqlLab,
|
||||||
|
queries: {
|
||||||
|
[asyncRefetchDataPreviewQuery.id]: asyncRefetchDataPreviewQuery,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
expect(
|
expect(
|
||||||
screen.getByRole('button', {
|
screen.getByRole('button', {
|
||||||
name: /fetch data preview/i,
|
name: /fetch data preview/i,
|
||||||
}),
|
}),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
expect(screen.queryByRole('grid')).toBe(null);
|
expect(screen.queryByRole('table')).toBe(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Async queries - renders "Refetch results" button when a query has no results', () => {
|
test('Async queries - renders "Refetch results" button when a query has no results', () => {
|
||||||
setup(asyncRefetchResultsTableProps, mockStore(initialState));
|
const asyncRefetchResultsTableQuery = {
|
||||||
|
...queries[0],
|
||||||
|
state: 'success',
|
||||||
|
results: undefined,
|
||||||
|
resultsKey: 'async results key',
|
||||||
|
};
|
||||||
|
|
||||||
|
setup(
|
||||||
|
{ ...asyncQueryProps, queryId: asyncRefetchResultsTableQuery.id },
|
||||||
|
mockStore({
|
||||||
|
...initialState,
|
||||||
|
user,
|
||||||
|
sqlLab: {
|
||||||
|
...initialState.sqlLab,
|
||||||
|
queries: {
|
||||||
|
[asyncRefetchResultsTableQuery.id]: asyncRefetchResultsTableQuery,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
expect(
|
expect(
|
||||||
screen.getByRole('button', {
|
screen.getByRole('button', {
|
||||||
name: /refetch results/i,
|
name: /refetch results/i,
|
||||||
}),
|
}),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
expect(screen.queryByRole('grid')).toBe(null);
|
expect(screen.queryByRole('table')).toBe(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Async queries - renders on the first call', () => {
|
test('Async queries - renders on the first call', () => {
|
||||||
setup(asyncQueryProps, mockStore(initialState));
|
const query = {
|
||||||
|
...queries[0],
|
||||||
|
};
|
||||||
|
setup(
|
||||||
|
{ ...asyncQueryProps, queryId: query.id },
|
||||||
|
mockStore({
|
||||||
|
...initialState,
|
||||||
|
user,
|
||||||
|
sqlLab: {
|
||||||
|
...initialState.sqlLab,
|
||||||
|
queries: {
|
||||||
|
[query.id]: query,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
expect(screen.getByRole('table')).toBeVisible();
|
expect(screen.getByRole('table')).toBeVisible();
|
||||||
expect(
|
expect(
|
||||||
screen.queryByRole('button', {
|
screen.queryByRole('button', {
|
||||||
|
@ -17,14 +17,14 @@
|
|||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import React, { useCallback, useEffect, useState } from 'react';
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
import { useDispatch } from 'react-redux';
|
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
|
||||||
import { useHistory } from 'react-router-dom';
|
import { useHistory } from 'react-router-dom';
|
||||||
|
import pick from 'lodash/pick';
|
||||||
import ButtonGroup from 'src/components/ButtonGroup';
|
import ButtonGroup from 'src/components/ButtonGroup';
|
||||||
import Alert from 'src/components/Alert';
|
import Alert from 'src/components/Alert';
|
||||||
import Button from 'src/components/Button';
|
import Button from 'src/components/Button';
|
||||||
import shortid from 'shortid';
|
import shortid from 'shortid';
|
||||||
import {
|
import {
|
||||||
QueryResponse,
|
|
||||||
QueryState,
|
QueryState,
|
||||||
styled,
|
styled,
|
||||||
t,
|
t,
|
||||||
@ -41,8 +41,7 @@ import {
|
|||||||
ISimpleColumn,
|
ISimpleColumn,
|
||||||
SaveDatasetModal,
|
SaveDatasetModal,
|
||||||
} from 'src/SqlLab/components/SaveDatasetModal';
|
} from 'src/SqlLab/components/SaveDatasetModal';
|
||||||
import { UserWithPermissionsAndRoles } from 'src/types/bootstrapTypes';
|
import { EXPLORE_CHART_DEFAULT, SqlLabRootState } from 'src/SqlLab/types';
|
||||||
import { EXPLORE_CHART_DEFAULT } from 'src/SqlLab/types';
|
|
||||||
import { mountExploreUrl } from 'src/explore/exploreUtils';
|
import { mountExploreUrl } from 'src/explore/exploreUtils';
|
||||||
import { postFormData } from 'src/explore/exploreUtils/formData';
|
import { postFormData } from 'src/explore/exploreUtils/formData';
|
||||||
import ProgressBar from 'src/components/ProgressBar';
|
import ProgressBar from 'src/components/ProgressBar';
|
||||||
@ -82,12 +81,11 @@ export interface ResultSetProps {
|
|||||||
database?: Record<string, any>;
|
database?: Record<string, any>;
|
||||||
displayLimit: number;
|
displayLimit: number;
|
||||||
height: number;
|
height: number;
|
||||||
query: QueryResponse;
|
queryId: string;
|
||||||
search?: boolean;
|
search?: boolean;
|
||||||
showSql?: boolean;
|
showSql?: boolean;
|
||||||
showSqlInline?: boolean;
|
showSqlInline?: boolean;
|
||||||
visualize?: boolean;
|
visualize?: boolean;
|
||||||
user: UserWithPermissionsAndRoles;
|
|
||||||
defaultQueryLimit: number;
|
defaultQueryLimit: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -145,14 +143,44 @@ const ResultSet = ({
|
|||||||
database = {},
|
database = {},
|
||||||
displayLimit,
|
displayLimit,
|
||||||
height,
|
height,
|
||||||
query,
|
queryId,
|
||||||
search = true,
|
search = true,
|
||||||
showSql = false,
|
showSql = false,
|
||||||
showSqlInline = false,
|
showSqlInline = false,
|
||||||
visualize = true,
|
visualize = true,
|
||||||
user,
|
|
||||||
defaultQueryLimit,
|
defaultQueryLimit,
|
||||||
}: ResultSetProps) => {
|
}: ResultSetProps) => {
|
||||||
|
const user = useSelector(({ user }: SqlLabRootState) => user, shallowEqual);
|
||||||
|
const query = useSelector(
|
||||||
|
({ sqlLab: { queries } }: SqlLabRootState) =>
|
||||||
|
pick(queries[queryId], [
|
||||||
|
'id',
|
||||||
|
'errorMessage',
|
||||||
|
'cached',
|
||||||
|
'results',
|
||||||
|
'resultsKey',
|
||||||
|
'dbId',
|
||||||
|
'tab',
|
||||||
|
'sql',
|
||||||
|
'templateParams',
|
||||||
|
'schema',
|
||||||
|
'rows',
|
||||||
|
'queryLimit',
|
||||||
|
'limitingFactor',
|
||||||
|
'trackingUrl',
|
||||||
|
'state',
|
||||||
|
'errors',
|
||||||
|
'link',
|
||||||
|
'ctas',
|
||||||
|
'ctas_method',
|
||||||
|
'tempSchema',
|
||||||
|
'tempTable',
|
||||||
|
'isDataPreview',
|
||||||
|
'progress',
|
||||||
|
'extra',
|
||||||
|
]),
|
||||||
|
shallowEqual,
|
||||||
|
);
|
||||||
const ResultTable =
|
const ResultTable =
|
||||||
extensionsRegistry.get('sqleditor.extension.resultTable') ??
|
extensionsRegistry.get('sqleditor.extension.resultTable') ??
|
||||||
FilterableTable;
|
FilterableTable;
|
||||||
@ -179,8 +207,8 @@ const ResultSet = ({
|
|||||||
reRunQueryIfSessionTimeoutErrorOnMount();
|
reRunQueryIfSessionTimeoutErrorOnMount();
|
||||||
}, [reRunQueryIfSessionTimeoutErrorOnMount]);
|
}, [reRunQueryIfSessionTimeoutErrorOnMount]);
|
||||||
|
|
||||||
const fetchResults = (query: QueryResponse) => {
|
const fetchResults = (q: typeof query) => {
|
||||||
dispatch(fetchQueryResults(query, displayLimit));
|
dispatch(fetchQueryResults(q, displayLimit));
|
||||||
};
|
};
|
||||||
|
|
||||||
const prevQuery = usePrevious(query);
|
const prevQuery = usePrevious(query);
|
||||||
@ -479,7 +507,7 @@ const ResultSet = ({
|
|||||||
<ResultlessStyles>
|
<ResultlessStyles>
|
||||||
<ErrorMessageWithStackTrace
|
<ErrorMessageWithStackTrace
|
||||||
title={t('Database error')}
|
title={t('Database error')}
|
||||||
error={query?.errors?.[0]}
|
error={query?.extra?.errors?.[0] || query?.errors?.[0]}
|
||||||
subtitle={<MonospaceDiv>{query.errorMessage}</MonospaceDiv>}
|
subtitle={<MonospaceDiv>{query.errorMessage}</MonospaceDiv>}
|
||||||
copyText={query.errorMessage || undefined}
|
copyText={query.errorMessage || undefined}
|
||||||
link={query.link}
|
link={query.link}
|
||||||
@ -662,4 +690,4 @@ const ResultSet = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ResultSet;
|
export default React.memo(ResultSet);
|
||||||
|
@ -0,0 +1,135 @@
|
|||||||
|
/**
|
||||||
|
* 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 { render } from 'spec/helpers/testing-library';
|
||||||
|
import { initialState, table, defaultQueryEditor } from 'src/SqlLab/fixtures';
|
||||||
|
import { denormalizeTimestamp } from '@superset-ui/core';
|
||||||
|
import { LOCALSTORAGE_MAX_QUERY_AGE_MS } from 'src/SqlLab/constants';
|
||||||
|
import Results from './Results';
|
||||||
|
|
||||||
|
const mockedProps = {
|
||||||
|
queryEditorId: defaultQueryEditor.id,
|
||||||
|
latestQueryId: 'LCly_kkIN',
|
||||||
|
height: 1,
|
||||||
|
displayLimit: 1,
|
||||||
|
defaultQueryLimit: 100,
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockedEmptyProps = {
|
||||||
|
queryEditorId: 'random_id',
|
||||||
|
latestQueryId: 'empty_query_id',
|
||||||
|
height: 100,
|
||||||
|
displayLimit: 100,
|
||||||
|
defaultQueryLimit: 100,
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockedExpiredProps = {
|
||||||
|
...mockedEmptyProps,
|
||||||
|
latestQueryId: 'expired_query_id',
|
||||||
|
};
|
||||||
|
|
||||||
|
const latestQueryProgressMsg = 'LATEST QUERY MESSAGE - LCly_kkIN';
|
||||||
|
const expireDateTime = Date.now() - LOCALSTORAGE_MAX_QUERY_AGE_MS - 1;
|
||||||
|
|
||||||
|
const mockState = {
|
||||||
|
...initialState,
|
||||||
|
sqlLab: {
|
||||||
|
...initialState,
|
||||||
|
offline: false,
|
||||||
|
tables: [
|
||||||
|
{
|
||||||
|
...table,
|
||||||
|
dataPreviewQueryId: '2g2_iRFMl',
|
||||||
|
queryEditorId: defaultQueryEditor.id,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
databases: {},
|
||||||
|
queries: {
|
||||||
|
LCly_kkIN: {
|
||||||
|
cached: false,
|
||||||
|
changed_on: denormalizeTimestamp(new Date().toISOString()),
|
||||||
|
db: 'main',
|
||||||
|
dbId: 1,
|
||||||
|
id: 'LCly_kkIN',
|
||||||
|
startDttm: Date.now(),
|
||||||
|
sqlEditorId: defaultQueryEditor.id,
|
||||||
|
extra: { progress: latestQueryProgressMsg },
|
||||||
|
sql: 'select * from table1',
|
||||||
|
},
|
||||||
|
lXJa7F9_r: {
|
||||||
|
cached: false,
|
||||||
|
changed_on: denormalizeTimestamp(new Date(1559238500401).toISOString()),
|
||||||
|
db: 'main',
|
||||||
|
dbId: 1,
|
||||||
|
id: 'lXJa7F9_r',
|
||||||
|
startDttm: 1559238500401,
|
||||||
|
sqlEditorId: defaultQueryEditor.id,
|
||||||
|
sql: 'select * from table2',
|
||||||
|
},
|
||||||
|
'2g2_iRFMl': {
|
||||||
|
cached: false,
|
||||||
|
changed_on: denormalizeTimestamp(new Date(1559238506925).toISOString()),
|
||||||
|
db: 'main',
|
||||||
|
dbId: 1,
|
||||||
|
id: '2g2_iRFMl',
|
||||||
|
startDttm: 1559238506925,
|
||||||
|
sqlEditorId: defaultQueryEditor.id,
|
||||||
|
sql: 'select * from table3',
|
||||||
|
},
|
||||||
|
expired_query_id: {
|
||||||
|
cached: false,
|
||||||
|
changed_on: denormalizeTimestamp(
|
||||||
|
new Date(expireDateTime).toISOString(),
|
||||||
|
),
|
||||||
|
db: 'main',
|
||||||
|
dbId: 1,
|
||||||
|
id: 'expired_query_id',
|
||||||
|
startDttm: expireDateTime,
|
||||||
|
sqlEditorId: defaultQueryEditor.id,
|
||||||
|
sql: 'select * from table4',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
test('Renders an empty state for results', async () => {
|
||||||
|
const { getByText } = render(<Results {...mockedEmptyProps} />, {
|
||||||
|
useRedux: true,
|
||||||
|
initialState: mockState,
|
||||||
|
});
|
||||||
|
const emptyStateText = getByText(/run a query to display results/i);
|
||||||
|
expect(emptyStateText).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Renders an empty state for expired results', async () => {
|
||||||
|
const { getByText } = render(<Results {...mockedExpiredProps} />, {
|
||||||
|
useRedux: true,
|
||||||
|
initialState: mockState,
|
||||||
|
});
|
||||||
|
const emptyStateText = getByText(/run a query to display results/i);
|
||||||
|
expect(emptyStateText).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should pass latest query down to ResultSet component', async () => {
|
||||||
|
const { getByText } = render(<Results {...mockedProps} />, {
|
||||||
|
useRedux: true,
|
||||||
|
initialState: mockState,
|
||||||
|
});
|
||||||
|
expect(getByText(latestQueryProgressMsg)).toBeVisible();
|
||||||
|
});
|
106
superset-frontend/src/SqlLab/components/SouthPane/Results.tsx
Normal file
106
superset-frontend/src/SqlLab/components/SouthPane/Results.tsx
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
/**
|
||||||
|
* 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 { shallowEqual, useSelector } from 'react-redux';
|
||||||
|
import Alert from 'src/components/Alert';
|
||||||
|
import { EmptyStateMedium } from 'src/components/EmptyState';
|
||||||
|
import { FeatureFlag, styled, t, isFeatureEnabled } from '@superset-ui/core';
|
||||||
|
|
||||||
|
import { SqlLabRootState } from 'src/SqlLab/types';
|
||||||
|
import ResultSet from '../ResultSet';
|
||||||
|
import { LOCALSTORAGE_MAX_QUERY_AGE_MS } from '../../constants';
|
||||||
|
|
||||||
|
const EXTRA_HEIGHT_RESULTS = 8; // we need extra height in RESULTS tab. because the height from props was calculated based on PREVIEW tab.
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
latestQueryId: string;
|
||||||
|
height: number;
|
||||||
|
displayLimit: number;
|
||||||
|
defaultQueryLimit: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const StyledEmptyStateWrapper = styled.div`
|
||||||
|
height: 100%;
|
||||||
|
.ant-empty-image img {
|
||||||
|
margin-right: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin-right: 28px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Results: React.FC<Props> = ({
|
||||||
|
latestQueryId,
|
||||||
|
height,
|
||||||
|
displayLimit,
|
||||||
|
defaultQueryLimit,
|
||||||
|
}) => {
|
||||||
|
const databases = useSelector(
|
||||||
|
({ sqlLab: { databases } }: SqlLabRootState) => databases,
|
||||||
|
shallowEqual,
|
||||||
|
);
|
||||||
|
const latestQuery = useSelector(
|
||||||
|
({ sqlLab: { queries } }: SqlLabRootState) => queries[latestQueryId || ''],
|
||||||
|
shallowEqual,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
!latestQuery ||
|
||||||
|
Date.now() - latestQuery.startDttm > LOCALSTORAGE_MAX_QUERY_AGE_MS
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<StyledEmptyStateWrapper>
|
||||||
|
<EmptyStateMedium
|
||||||
|
title={t('Run a query to display results')}
|
||||||
|
image="document.svg"
|
||||||
|
/>
|
||||||
|
</StyledEmptyStateWrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
isFeatureEnabled(FeatureFlag.SQLLAB_BACKEND_PERSISTENCE) &&
|
||||||
|
latestQuery.state === 'success' &&
|
||||||
|
!latestQuery.resultsKey &&
|
||||||
|
!latestQuery.results
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<Alert
|
||||||
|
type="warning"
|
||||||
|
message={t('No stored results found, you need to re-run your query')}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ResultSet
|
||||||
|
search
|
||||||
|
queryId={latestQuery.id}
|
||||||
|
height={height + EXTRA_HEIGHT_RESULTS}
|
||||||
|
database={databases[latestQuery.dbId]}
|
||||||
|
displayLimit={displayLimit}
|
||||||
|
defaultQueryLimit={defaultQueryLimit}
|
||||||
|
showSql
|
||||||
|
showSqlInline
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Results;
|
@ -17,15 +17,12 @@
|
|||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import configureStore from 'redux-mock-store';
|
import { render } from 'spec/helpers/testing-library';
|
||||||
import thunk from 'redux-thunk';
|
import SouthPane from 'src/SqlLab/components/SouthPane';
|
||||||
import { render, screen, waitFor } from 'spec/helpers/testing-library';
|
|
||||||
import SouthPane, { SouthPaneProps } from 'src/SqlLab/components/SouthPane';
|
|
||||||
import '@testing-library/jest-dom/extend-expect';
|
import '@testing-library/jest-dom/extend-expect';
|
||||||
import { STATUS_OPTIONS } from 'src/SqlLab/constants';
|
import { STATUS_OPTIONS } from 'src/SqlLab/constants';
|
||||||
import { initialState, table, defaultQueryEditor } from 'src/SqlLab/fixtures';
|
import { initialState, table, defaultQueryEditor } from 'src/SqlLab/fixtures';
|
||||||
import { denormalizeTimestamp } from '@superset-ui/core';
|
import { denormalizeTimestamp } from '@superset-ui/core';
|
||||||
import { Store } from 'redux';
|
|
||||||
|
|
||||||
const mockedProps = {
|
const mockedProps = {
|
||||||
queryEditorId: defaultQueryEditor.id,
|
queryEditorId: defaultQueryEditor.id,
|
||||||
@ -37,29 +34,32 @@ const mockedProps = {
|
|||||||
|
|
||||||
const mockedEmptyProps = {
|
const mockedEmptyProps = {
|
||||||
queryEditorId: 'random_id',
|
queryEditorId: 'random_id',
|
||||||
latestQueryId: '',
|
latestQueryId: 'empty_query_id',
|
||||||
height: 100,
|
height: 100,
|
||||||
displayLimit: 100,
|
displayLimit: 100,
|
||||||
defaultQueryLimit: 100,
|
defaultQueryLimit: 100,
|
||||||
};
|
};
|
||||||
|
|
||||||
jest.mock('src/SqlLab/components/SqlEditorLeftBar', () => jest.fn());
|
|
||||||
|
|
||||||
const latestQueryProgressMsg = 'LATEST QUERY MESSAGE - LCly_kkIN';
|
const latestQueryProgressMsg = 'LATEST QUERY MESSAGE - LCly_kkIN';
|
||||||
|
|
||||||
const middlewares = [thunk];
|
const mockState = {
|
||||||
const mockStore = configureStore(middlewares);
|
|
||||||
const store = mockStore({
|
|
||||||
...initialState,
|
...initialState,
|
||||||
sqlLab: {
|
sqlLab: {
|
||||||
...initialState,
|
...initialState.sqlLab,
|
||||||
offline: false,
|
offline: false,
|
||||||
tables: [
|
tables: [
|
||||||
{
|
{
|
||||||
...table,
|
...table,
|
||||||
|
name: 'table3',
|
||||||
dataPreviewQueryId: '2g2_iRFMl',
|
dataPreviewQueryId: '2g2_iRFMl',
|
||||||
queryEditorId: defaultQueryEditor.id,
|
queryEditorId: defaultQueryEditor.id,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
...table,
|
||||||
|
name: 'table4',
|
||||||
|
dataPreviewQueryId: 'erWdqEWPm',
|
||||||
|
queryEditorId: defaultQueryEditor.id,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
databases: {},
|
databases: {},
|
||||||
queries: {
|
queries: {
|
||||||
@ -72,6 +72,7 @@ const store = mockStore({
|
|||||||
startDttm: Date.now(),
|
startDttm: Date.now(),
|
||||||
sqlEditorId: defaultQueryEditor.id,
|
sqlEditorId: defaultQueryEditor.id,
|
||||||
extra: { progress: latestQueryProgressMsg },
|
extra: { progress: latestQueryProgressMsg },
|
||||||
|
sql: 'select * from table1',
|
||||||
},
|
},
|
||||||
lXJa7F9_r: {
|
lXJa7F9_r: {
|
||||||
cached: false,
|
cached: false,
|
||||||
@ -81,6 +82,7 @@ const store = mockStore({
|
|||||||
id: 'lXJa7F9_r',
|
id: 'lXJa7F9_r',
|
||||||
startDttm: 1559238500401,
|
startDttm: 1559238500401,
|
||||||
sqlEditorId: defaultQueryEditor.id,
|
sqlEditorId: defaultQueryEditor.id,
|
||||||
|
sql: 'select * from table2',
|
||||||
},
|
},
|
||||||
'2g2_iRFMl': {
|
'2g2_iRFMl': {
|
||||||
cached: false,
|
cached: false,
|
||||||
@ -90,6 +92,7 @@ const store = mockStore({
|
|||||||
id: '2g2_iRFMl',
|
id: '2g2_iRFMl',
|
||||||
startDttm: 1559238506925,
|
startDttm: 1559238506925,
|
||||||
sqlEditorId: defaultQueryEditor.id,
|
sqlEditorId: defaultQueryEditor.id,
|
||||||
|
sql: 'select * from table3',
|
||||||
},
|
},
|
||||||
erWdqEWPm: {
|
erWdqEWPm: {
|
||||||
cached: false,
|
cached: false,
|
||||||
@ -99,44 +102,38 @@ const store = mockStore({
|
|||||||
id: 'erWdqEWPm',
|
id: 'erWdqEWPm',
|
||||||
startDttm: 1559238516395,
|
startDttm: 1559238516395,
|
||||||
sqlEditorId: defaultQueryEditor.id,
|
sqlEditorId: defaultQueryEditor.id,
|
||||||
|
sql: 'select * from table4',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
};
|
||||||
const setup = (props: SouthPaneProps, store: Store) =>
|
|
||||||
render(<SouthPane {...props} />, {
|
test('should render offline when the state is offline', async () => {
|
||||||
|
const { getByText } = render(<SouthPane {...mockedEmptyProps} />, {
|
||||||
useRedux: true,
|
useRedux: true,
|
||||||
...(store && { store }),
|
initialState: {
|
||||||
|
...initialState,
|
||||||
|
sqlLab: {
|
||||||
|
...initialState.sqlLab,
|
||||||
|
offline: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('SouthPane', () => {
|
expect(getByText(STATUS_OPTIONS.offline)).toBeVisible();
|
||||||
const renderAndWait = (props: SouthPaneProps, store: Store) =>
|
});
|
||||||
waitFor(async () => setup(props, store));
|
|
||||||
|
|
||||||
it('Renders an empty state for results', async () => {
|
test('should render tabs for table preview queries', () => {
|
||||||
await renderAndWait(mockedEmptyProps, store);
|
const { getAllByRole } = render(<SouthPane {...mockedProps} />, {
|
||||||
const emptyStateText = screen.getByText(/run a query to display results/i);
|
useRedux: true,
|
||||||
expect(emptyStateText).toBeVisible();
|
initialState: mockState,
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render offline when the state is offline', async () => {
|
const tabs = getAllByRole('tab');
|
||||||
await renderAndWait(
|
expect(tabs).toHaveLength(mockState.sqlLab.tables.length + 2);
|
||||||
mockedEmptyProps,
|
expect(tabs[0]).toHaveTextContent('Results');
|
||||||
mockStore({
|
expect(tabs[1]).toHaveTextContent('Query history');
|
||||||
...initialState,
|
mockState.sqlLab.tables.forEach(({ name }, index) => {
|
||||||
sqlLab: {
|
expect(tabs[index + 2]).toHaveTextContent(`Preview: \`${name}\``);
|
||||||
...initialState.sqlLab,
|
|
||||||
offline: true,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(screen.getByText(STATUS_OPTIONS.offline)).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should pass latest query down to ResultSet component', async () => {
|
|
||||||
await renderAndWait(mockedProps, store);
|
|
||||||
|
|
||||||
expect(screen.getByText(latestQueryProgressMsg)).toBeVisible();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -19,10 +19,8 @@
|
|||||||
import React, { createRef, useMemo } from 'react';
|
import React, { createRef, useMemo } from 'react';
|
||||||
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
|
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
|
||||||
import shortid from 'shortid';
|
import shortid from 'shortid';
|
||||||
import Alert from 'src/components/Alert';
|
|
||||||
import Tabs from 'src/components/Tabs';
|
import Tabs from 'src/components/Tabs';
|
||||||
import { EmptyStateMedium } from 'src/components/EmptyState';
|
import { styled, t } from '@superset-ui/core';
|
||||||
import { FeatureFlag, styled, t, isFeatureEnabled } from '@superset-ui/core';
|
|
||||||
|
|
||||||
import { setActiveSouthPaneTab } from 'src/SqlLab/actions/sqlLab';
|
import { setActiveSouthPaneTab } from 'src/SqlLab/actions/sqlLab';
|
||||||
|
|
||||||
@ -33,11 +31,11 @@ import ResultSet from '../ResultSet';
|
|||||||
import {
|
import {
|
||||||
STATUS_OPTIONS,
|
STATUS_OPTIONS,
|
||||||
STATE_TYPE_MAP,
|
STATE_TYPE_MAP,
|
||||||
LOCALSTORAGE_MAX_QUERY_AGE_MS,
|
|
||||||
STATUS_OPTIONS_LOCALIZED,
|
STATUS_OPTIONS_LOCALIZED,
|
||||||
} from '../../constants';
|
} from '../../constants';
|
||||||
|
import Results from './Results';
|
||||||
|
|
||||||
const TAB_HEIGHT = 140;
|
const TAB_HEIGHT = 130;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
editorQueries are queries executed by users passed from SqlEditor component
|
editorQueries are queries executed by users passed from SqlEditor component
|
||||||
@ -85,18 +83,6 @@ const StyledPane = styled.div<StyledPaneProps>`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const EXTRA_HEIGHT_RESULTS = 24; // we need extra height in RESULTS tab. because the height from props was calculated based on PREVIEW tab.
|
|
||||||
const StyledEmptyStateWrapper = styled.div`
|
|
||||||
height: 100%;
|
|
||||||
.ant-empty-image img {
|
|
||||||
margin-right: 28px;
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
margin-right: 28px;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const SouthPane = ({
|
const SouthPane = ({
|
||||||
queryEditorId,
|
queryEditorId,
|
||||||
latestQueryId,
|
latestQueryId,
|
||||||
@ -105,128 +91,43 @@ const SouthPane = ({
|
|||||||
defaultQueryLimit,
|
defaultQueryLimit,
|
||||||
}: SouthPaneProps) => {
|
}: SouthPaneProps) => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const user = useSelector(({ user }: SqlLabRootState) => user, shallowEqual);
|
const { offline, tables } = useSelector(
|
||||||
const { databases, offline, queries, tables } = useSelector(
|
({ sqlLab: { offline, tables } }: SqlLabRootState) => ({
|
||||||
({ sqlLab: { databases, offline, queries, tables } }: SqlLabRootState) => ({
|
|
||||||
databases,
|
|
||||||
offline,
|
offline,
|
||||||
queries,
|
|
||||||
tables,
|
tables,
|
||||||
}),
|
}),
|
||||||
shallowEqual,
|
shallowEqual,
|
||||||
);
|
);
|
||||||
const editorQueries = useMemo(
|
const queries = useSelector(
|
||||||
() =>
|
({ sqlLab: { queries } }: SqlLabRootState) => Object.keys(queries),
|
||||||
Object.values(queries).filter(
|
shallowEqual,
|
||||||
({ sqlEditorId }) => sqlEditorId === queryEditorId,
|
|
||||||
),
|
|
||||||
[queries, queryEditorId],
|
|
||||||
);
|
);
|
||||||
const dataPreviewQueries = useMemo(
|
|
||||||
() =>
|
|
||||||
tables
|
|
||||||
.filter(
|
|
||||||
({ dataPreviewQueryId, queryEditorId: qeId }) =>
|
|
||||||
dataPreviewQueryId &&
|
|
||||||
queryEditorId === qeId &&
|
|
||||||
queries[dataPreviewQueryId],
|
|
||||||
)
|
|
||||||
.map(({ name, dataPreviewQueryId }) => ({
|
|
||||||
...queries[dataPreviewQueryId || ''],
|
|
||||||
tableName: name,
|
|
||||||
})),
|
|
||||||
[queries, queryEditorId, tables],
|
|
||||||
);
|
|
||||||
const latestQuery = useMemo(
|
|
||||||
() => editorQueries.find(({ id }) => id === latestQueryId),
|
|
||||||
[editorQueries, latestQueryId],
|
|
||||||
);
|
|
||||||
|
|
||||||
const activeSouthPaneTab =
|
const activeSouthPaneTab =
|
||||||
useSelector<SqlLabRootState, string>(
|
useSelector<SqlLabRootState, string>(
|
||||||
state => state.sqlLab.activeSouthPaneTab as string,
|
state => state.sqlLab.activeSouthPaneTab as string,
|
||||||
) ?? 'Results';
|
) ?? 'Results';
|
||||||
|
|
||||||
|
const querySet = useMemo(() => new Set(queries), [queries]);
|
||||||
|
const dataPreviewQueries = useMemo(
|
||||||
|
() =>
|
||||||
|
tables.filter(
|
||||||
|
({ dataPreviewQueryId, queryEditorId: qeId }) =>
|
||||||
|
dataPreviewQueryId &&
|
||||||
|
queryEditorId === qeId &&
|
||||||
|
querySet.has(dataPreviewQueryId),
|
||||||
|
),
|
||||||
|
[queryEditorId, tables, querySet],
|
||||||
|
);
|
||||||
const innerTabContentHeight = height - TAB_HEIGHT;
|
const innerTabContentHeight = height - TAB_HEIGHT;
|
||||||
const southPaneRef = createRef<HTMLDivElement>();
|
const southPaneRef = createRef<HTMLDivElement>();
|
||||||
const switchTab = (id: string) => {
|
const switchTab = (id: string) => {
|
||||||
dispatch(setActiveSouthPaneTab(id));
|
dispatch(setActiveSouthPaneTab(id));
|
||||||
};
|
};
|
||||||
const renderOfflineStatus = () => (
|
|
||||||
|
return offline ? (
|
||||||
<Label className="m-r-3" type={STATE_TYPE_MAP[STATUS_OPTIONS.offline]}>
|
<Label className="m-r-3" type={STATE_TYPE_MAP[STATUS_OPTIONS.offline]}>
|
||||||
{STATUS_OPTIONS_LOCALIZED.offline}
|
{STATUS_OPTIONS_LOCALIZED.offline}
|
||||||
</Label>
|
</Label>
|
||||||
);
|
|
||||||
|
|
||||||
const renderResults = () => {
|
|
||||||
let results;
|
|
||||||
if (latestQuery) {
|
|
||||||
if (latestQuery?.extra?.errors) {
|
|
||||||
latestQuery.errors = latestQuery.extra.errors;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
isFeatureEnabled(FeatureFlag.SQLLAB_BACKEND_PERSISTENCE) &&
|
|
||||||
latestQuery.state === 'success' &&
|
|
||||||
!latestQuery.resultsKey &&
|
|
||||||
!latestQuery.results
|
|
||||||
) {
|
|
||||||
results = (
|
|
||||||
<Alert
|
|
||||||
type="warning"
|
|
||||||
message={t(
|
|
||||||
'No stored results found, you need to re-run your query',
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
if (Date.now() - latestQuery.startDttm <= LOCALSTORAGE_MAX_QUERY_AGE_MS) {
|
|
||||||
results = (
|
|
||||||
<ResultSet
|
|
||||||
search
|
|
||||||
query={latestQuery}
|
|
||||||
user={user}
|
|
||||||
height={innerTabContentHeight + EXTRA_HEIGHT_RESULTS}
|
|
||||||
database={databases[latestQuery.dbId]}
|
|
||||||
displayLimit={displayLimit}
|
|
||||||
defaultQueryLimit={defaultQueryLimit}
|
|
||||||
showSql
|
|
||||||
showSqlInline
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
results = (
|
|
||||||
<StyledEmptyStateWrapper>
|
|
||||||
<EmptyStateMedium
|
|
||||||
title={t('Run a query to display results')}
|
|
||||||
image="document.svg"
|
|
||||||
/>
|
|
||||||
</StyledEmptyStateWrapper>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return results;
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderDataPreviewTabs = () =>
|
|
||||||
dataPreviewQueries.map(query => (
|
|
||||||
<Tabs.TabPane
|
|
||||||
tab={t('Preview: `%s`', decodeURIComponent(query.tableName))}
|
|
||||||
key={query.id}
|
|
||||||
>
|
|
||||||
<ResultSet
|
|
||||||
query={query}
|
|
||||||
visualize={false}
|
|
||||||
csv={false}
|
|
||||||
cache
|
|
||||||
user={user}
|
|
||||||
height={innerTabContentHeight}
|
|
||||||
displayLimit={displayLimit}
|
|
||||||
defaultQueryLimit={defaultQueryLimit}
|
|
||||||
/>
|
|
||||||
</Tabs.TabPane>
|
|
||||||
));
|
|
||||||
return offline ? (
|
|
||||||
renderOfflineStatus()
|
|
||||||
) : (
|
) : (
|
||||||
<StyledPane
|
<StyledPane
|
||||||
data-test="south-pane"
|
data-test="south-pane"
|
||||||
@ -243,16 +144,41 @@ const SouthPane = ({
|
|||||||
animated={false}
|
animated={false}
|
||||||
>
|
>
|
||||||
<Tabs.TabPane tab={t('Results')} key="Results">
|
<Tabs.TabPane tab={t('Results')} key="Results">
|
||||||
{renderResults()}
|
{latestQueryId && (
|
||||||
|
<Results
|
||||||
|
height={innerTabContentHeight}
|
||||||
|
latestQueryId={latestQueryId}
|
||||||
|
displayLimit={displayLimit}
|
||||||
|
defaultQueryLimit={defaultQueryLimit}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Tabs.TabPane>
|
</Tabs.TabPane>
|
||||||
<Tabs.TabPane tab={t('Query history')} key="History">
|
<Tabs.TabPane tab={t('Query history')} key="History">
|
||||||
<QueryHistory
|
<QueryHistory
|
||||||
queries={editorQueries}
|
queryEditorId={queryEditorId}
|
||||||
displayLimit={displayLimit}
|
displayLimit={displayLimit}
|
||||||
latestQueryId={latestQueryId}
|
latestQueryId={latestQueryId}
|
||||||
/>
|
/>
|
||||||
</Tabs.TabPane>
|
</Tabs.TabPane>
|
||||||
{renderDataPreviewTabs()}
|
{dataPreviewQueries.map(
|
||||||
|
({ name, dataPreviewQueryId }) =>
|
||||||
|
dataPreviewQueryId && (
|
||||||
|
<Tabs.TabPane
|
||||||
|
tab={t('Preview: `%s`', decodeURIComponent(name))}
|
||||||
|
key={dataPreviewQueryId}
|
||||||
|
>
|
||||||
|
<ResultSet
|
||||||
|
queryId={dataPreviewQueryId}
|
||||||
|
visualize={false}
|
||||||
|
csv={false}
|
||||||
|
cache
|
||||||
|
height={innerTabContentHeight}
|
||||||
|
displayLimit={displayLimit}
|
||||||
|
defaultQueryLimit={defaultQueryLimit}
|
||||||
|
/>
|
||||||
|
</Tabs.TabPane>
|
||||||
|
),
|
||||||
|
)}
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</StyledPane>
|
</StyledPane>
|
||||||
);
|
);
|
||||||
|
@ -145,8 +145,8 @@ describe('SqlEditor', () => {
|
|||||||
(SqlEditorLeftBar as jest.Mock).mockImplementation(() => (
|
(SqlEditorLeftBar as jest.Mock).mockImplementation(() => (
|
||||||
<div data-test="mock-sql-editor-left-bar" />
|
<div data-test="mock-sql-editor-left-bar" />
|
||||||
));
|
));
|
||||||
(ResultSet as jest.Mock).mockClear();
|
(ResultSet as unknown as jest.Mock).mockClear();
|
||||||
(ResultSet as jest.Mock).mockImplementation(() => (
|
(ResultSet as unknown as jest.Mock).mockImplementation(() => (
|
||||||
<div data-test="mock-result-set" />
|
<div data-test="mock-result-set" />
|
||||||
));
|
));
|
||||||
});
|
});
|
||||||
@ -182,7 +182,8 @@ describe('SqlEditor', () => {
|
|||||||
const editor = await findByTestId('react-ace');
|
const editor = await findByTestId('react-ace');
|
||||||
const sql = 'select *';
|
const sql = 'select *';
|
||||||
const renderCount = (SqlEditorLeftBar as jest.Mock).mock.calls.length;
|
const renderCount = (SqlEditorLeftBar as jest.Mock).mock.calls.length;
|
||||||
const renderCountForSouthPane = (ResultSet as jest.Mock).mock.calls.length;
|
const renderCountForSouthPane = (ResultSet as unknown as jest.Mock).mock
|
||||||
|
.calls.length;
|
||||||
expect(SqlEditorLeftBar).toHaveBeenCalledTimes(renderCount);
|
expect(SqlEditorLeftBar).toHaveBeenCalledTimes(renderCount);
|
||||||
expect(ResultSet).toHaveBeenCalledTimes(renderCountForSouthPane);
|
expect(ResultSet).toHaveBeenCalledTimes(renderCountForSouthPane);
|
||||||
fireEvent.change(editor, { target: { value: sql } });
|
fireEvent.change(editor, { target: { value: sql } });
|
||||||
|
@ -345,7 +345,7 @@ export default function sqlLabReducer(state = {}, action) {
|
|||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
const alts = {
|
const alts = {
|
||||||
endDttm: now(),
|
endDttm: action?.results?.query?.endDttm || now(),
|
||||||
progress: 100,
|
progress: 100,
|
||||||
results: action.results,
|
results: action.results,
|
||||||
rows: action?.results?.query?.rows || 0,
|
rows: action?.results?.query?.rows || 0,
|
||||||
@ -674,7 +674,14 @@ export default function sqlLabReducer(state = {}, action) {
|
|||||||
if (!change) {
|
if (!change) {
|
||||||
newQueries = state.queries;
|
newQueries = state.queries;
|
||||||
}
|
}
|
||||||
return { ...state, queries: newQueries, queriesLastUpdate };
|
return {
|
||||||
|
...state,
|
||||||
|
queries: newQueries,
|
||||||
|
queriesLastUpdate:
|
||||||
|
queriesLastUpdate > state.queriesLastUpdate
|
||||||
|
? queriesLastUpdate
|
||||||
|
: Date.now(),
|
||||||
|
};
|
||||||
},
|
},
|
||||||
[actions.CLEAR_INACTIVE_QUERIES]() {
|
[actions.CLEAR_INACTIVE_QUERIES]() {
|
||||||
const { queries } = state;
|
const { queries } = state;
|
||||||
@ -701,7 +708,11 @@ export default function sqlLabReducer(state = {}, action) {
|
|||||||
},
|
},
|
||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
return { ...state, queries: cleanedQueries };
|
return {
|
||||||
|
...state,
|
||||||
|
queries: cleanedQueries,
|
||||||
|
queriesLastUpdate: Date.now(),
|
||||||
|
};
|
||||||
},
|
},
|
||||||
[actions.SET_USER_OFFLINE]() {
|
[actions.SET_USER_OFFLINE]() {
|
||||||
return { ...state, offline: action.offline };
|
return { ...state, offline: action.offline };
|
||||||
|
@ -20,6 +20,7 @@ import { QueryState } from '@superset-ui/core';
|
|||||||
import sqlLabReducer from 'src/SqlLab/reducers/sqlLab';
|
import sqlLabReducer from 'src/SqlLab/reducers/sqlLab';
|
||||||
import * as actions from 'src/SqlLab/actions/sqlLab';
|
import * as actions from 'src/SqlLab/actions/sqlLab';
|
||||||
import { table, initialState as mockState } from '../fixtures';
|
import { table, initialState as mockState } from '../fixtures';
|
||||||
|
import { QUERY_UPDATE_FREQ } from '../components/QueryAutoRefresh';
|
||||||
|
|
||||||
const initialState = mockState.sqlLab;
|
const initialState = mockState.sqlLab;
|
||||||
|
|
||||||
@ -404,6 +405,7 @@ describe('sqlLabReducer', () => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
it('updates queries that have already been completed', () => {
|
it('updates queries that have already been completed', () => {
|
||||||
|
const current = Date.now();
|
||||||
newState = sqlLabReducer(
|
newState = sqlLabReducer(
|
||||||
{
|
{
|
||||||
...newState,
|
...newState,
|
||||||
@ -418,9 +420,10 @@ describe('sqlLabReducer', () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
actions.clearInactiveQueries(Date.now()),
|
actions.clearInactiveQueries(QUERY_UPDATE_FREQ),
|
||||||
);
|
);
|
||||||
expect(newState.queries.abcd.state).toBe(QueryState.SUCCESS);
|
expect(newState.queries.abcd.state).toBe(QueryState.SUCCESS);
|
||||||
|
expect(newState.queriesLastUpdate).toBeGreaterThanOrEqual(current);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user