mirror of https://github.com/apache/superset.git
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
|
||||
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
|
||||
export const shouldCheckForQueries = (queryList: QueryDictionary): boolean => {
|
||||
|
|
|
@ -19,9 +19,10 @@
|
|||
import React from 'react';
|
||||
import { render, screen } from 'spec/helpers/testing-library';
|
||||
import QueryHistory from 'src/SqlLab/components/QueryHistory';
|
||||
import { initialState } from 'src/SqlLab/fixtures';
|
||||
|
||||
const mockedProps = {
|
||||
queries: [],
|
||||
queryEditorId: 123,
|
||||
displayLimit: 1000,
|
||||
latestQueryId: 'yhMUZCGb',
|
||||
};
|
||||
|
@ -32,7 +33,7 @@ const setup = (overrides = {}) => (
|
|||
|
||||
describe('QueryHistory', () => {
|
||||
it('Renders an empty state for query history', () => {
|
||||
render(setup());
|
||||
render(setup(), { useRedux: true, initialState });
|
||||
|
||||
const emptyStateText = screen.getByText(
|
||||
/run a query to display query history/i,
|
||||
|
|
|
@ -16,13 +16,15 @@
|
|||
* specific language governing permissions and limitations
|
||||
* 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 { t, styled, QueryResponse } from '@superset-ui/core';
|
||||
import { t, styled } from '@superset-ui/core';
|
||||
import QueryTable from 'src/SqlLab/components/QueryTable';
|
||||
import { SqlLabRootState } from 'src/SqlLab/types';
|
||||
|
||||
interface QueryHistoryProps {
|
||||
queries: QueryResponse[];
|
||||
queryEditorId: string | number;
|
||||
displayLimit: number;
|
||||
latestQueryId: string | undefined;
|
||||
}
|
||||
|
@ -39,11 +41,23 @@ const StyledEmptyStateWrapper = styled.div`
|
|||
`;
|
||||
|
||||
const QueryHistory = ({
|
||||
queries,
|
||||
queryEditorId,
|
||||
displayLimit,
|
||||
latestQueryId,
|
||||
}: QueryHistoryProps) =>
|
||||
queries.length > 0 ? (
|
||||
}: QueryHistoryProps) => {
|
||||
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
|
||||
columns={[
|
||||
'state',
|
||||
|
@ -55,7 +69,7 @@ const QueryHistory = ({
|
|||
'results',
|
||||
'actions',
|
||||
]}
|
||||
queries={queries}
|
||||
queries={editorQueries}
|
||||
displayLimit={displayLimit}
|
||||
latestQueryId={latestQueryId}
|
||||
/>
|
||||
|
@ -67,5 +81,6 @@ const QueryHistory = ({
|
|||
/>
|
||||
</StyledEmptyStateWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default QueryHistory;
|
||||
|
|
|
@ -251,8 +251,7 @@ const QueryTable = ({
|
|||
modalBody={
|
||||
<ResultSet
|
||||
showSql
|
||||
user={user}
|
||||
query={query}
|
||||
queryId={query.id}
|
||||
height={400}
|
||||
displayLimit={displayLimit}
|
||||
defaultQueryLimit={1000}
|
||||
|
|
|
@ -37,65 +37,91 @@ import {
|
|||
|
||||
const mockedProps = {
|
||||
cache: true,
|
||||
query: queries[0],
|
||||
queryId: queries[0].id,
|
||||
height: 140,
|
||||
database: { allows_virtual_table_explore: true },
|
||||
user,
|
||||
displayLimit: 1000,
|
||||
defaultQueryLimit: 1000,
|
||||
};
|
||||
const stoppedQueryProps = { ...mockedProps, query: stoppedQuery };
|
||||
const runningQueryProps = { ...mockedProps, query: runningQuery };
|
||||
const fetchingQueryProps = {
|
||||
...mockedProps,
|
||||
query: {
|
||||
dbId: 1,
|
||||
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 stoppedQueryState = {
|
||||
...initialState,
|
||||
sqlLab: {
|
||||
...initialState.sqlLab,
|
||||
queries: {
|
||||
[stoppedQuery.id]: stoppedQuery,
|
||||
},
|
||||
},
|
||||
};
|
||||
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 = {
|
||||
...mockedProps,
|
||||
database: { allow_run_async: true },
|
||||
};
|
||||
const asyncRefetchDataPreviewProps = {
|
||||
...asyncQueryProps,
|
||||
query: {
|
||||
state: 'success',
|
||||
results: undefined,
|
||||
isDataPreview: true,
|
||||
},
|
||||
};
|
||||
const asyncRefetchResultsTableProps = {
|
||||
...asyncQueryProps,
|
||||
query: {
|
||||
state: 'success',
|
||||
results: undefined,
|
||||
resultsKey: 'async results key',
|
||||
},
|
||||
};
|
||||
|
||||
const reRunQueryEndpoint = 'glob:*/api/v1/sqllab/execute/';
|
||||
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 mockStore = configureStore(middlewares);
|
||||
|
@ -107,25 +133,47 @@ const setup = (props?: any, store?: Store) =>
|
|||
|
||||
describe('ResultSet', () => {
|
||||
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');
|
||||
expect(table).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should render success query', async () => {
|
||||
const query = queries[0];
|
||||
const { queryAllByText, getByTestId } = setup(
|
||||
mockedProps,
|
||||
mockStore(initialState),
|
||||
mockStore({
|
||||
...initialState,
|
||||
user,
|
||||
sqlLab: {
|
||||
...initialState.sqlLab,
|
||||
queries: {
|
||||
[query.id]: query,
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const table = getByTestId('table-container');
|
||||
expect(table).toBeInTheDocument();
|
||||
|
||||
const firstColumn = queryAllByText(
|
||||
mockedProps.query.results?.columns[0].column_name ?? '',
|
||||
query.results?.columns[0].column_name ?? '',
|
||||
)[0];
|
||||
const secondColumn = queryAllByText(
|
||||
mockedProps.query.results?.columns[1].column_name ?? '',
|
||||
query.results?.columns[1].column_name ?? '',
|
||||
)[0];
|
||||
expect(firstColumn).toBeInTheDocument();
|
||||
expect(secondColumn).toBeInTheDocument();
|
||||
|
@ -135,12 +183,24 @@ describe('ResultSet', () => {
|
|||
});
|
||||
|
||||
test('should render empty results', async () => {
|
||||
const props = {
|
||||
...mockedProps,
|
||||
query: { ...mockedProps.query, results: { data: [] } },
|
||||
const query = {
|
||||
...queries[0],
|
||||
results: { data: [] },
|
||||
};
|
||||
await waitFor(() => {
|
||||
setup(props, mockStore(initialState));
|
||||
setup(
|
||||
mockedProps,
|
||||
mockStore({
|
||||
...initialState,
|
||||
user,
|
||||
sqlLab: {
|
||||
...initialState.sqlLab,
|
||||
queries: {
|
||||
[query.id]: query,
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
const alert = screen.getByRole('alert');
|
||||
|
@ -149,42 +209,70 @@ describe('ResultSet', () => {
|
|||
});
|
||||
|
||||
test('should call reRunQuery if timed out', async () => {
|
||||
const store = mockStore(initialState);
|
||||
const propsWithError = {
|
||||
...mockedProps,
|
||||
query: { ...queries[0], errorMessage: 'Your session timed out' },
|
||||
const 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()[0].query.errorMessage).toEqual(
|
||||
'Your session timed out',
|
||||
);
|
||||
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 () => {
|
||||
const store = mockStore(initialState);
|
||||
const query = queries[0];
|
||||
const store = mockStore({
|
||||
...initialState,
|
||||
user,
|
||||
sqlLab: {
|
||||
...initialState.sqlLab,
|
||||
queries: {
|
||||
[query.id]: query,
|
||||
},
|
||||
},
|
||||
});
|
||||
setup(mockedProps, store);
|
||||
expect(store.getActions()).toEqual([]);
|
||||
expect(fetchMock.calls(reRunQueryEndpoint)).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('should render cached query', async () => {
|
||||
const store = mockStore(initialState);
|
||||
const { rerender } = setup(cachedQueryProps, store);
|
||||
const store = mockStore(cachedQueryState);
|
||||
const { rerender } = setup(
|
||||
{ ...mockedProps, queryId: cachedQuery.id },
|
||||
store,
|
||||
);
|
||||
|
||||
// @ts-ignore
|
||||
rerender(<ResultSet {...newProps} />);
|
||||
expect(store.getActions()).toHaveLength(2);
|
||||
expect(store.getActions()[0].query.results).toEqual(
|
||||
cachedQueryProps.query.results,
|
||||
);
|
||||
rerender(<ResultSet {...mockedProps} {...newProps} />);
|
||||
expect(store.getActions()).toHaveLength(1);
|
||||
expect(store.getActions()[0].query.results).toEqual(cachedQuery.results);
|
||||
expect(store.getActions()[0].type).toEqual('CLEAR_QUERY_RESULTS');
|
||||
});
|
||||
|
||||
test('should render stopped query', async () => {
|
||||
await waitFor(() => {
|
||||
setup(stoppedQueryProps, mockStore(initialState));
|
||||
setup(
|
||||
{ ...mockedProps, queryId: stoppedQuery.id },
|
||||
mockStore(stoppedQueryState),
|
||||
);
|
||||
});
|
||||
|
||||
const alert = screen.getByRole('alert');
|
||||
|
@ -192,15 +280,18 @@ describe('ResultSet', () => {
|
|||
});
|
||||
|
||||
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');
|
||||
expect(progressBar).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should render fetching w/ 100 progress query', async () => {
|
||||
const { getByRole, getByText } = setup(
|
||||
fetchingQueryProps,
|
||||
mockStore(initialState),
|
||||
mockedProps,
|
||||
mockStore(fetchingQueryState),
|
||||
);
|
||||
const loading = getByRole('status');
|
||||
expect(loading).toBeInTheDocument();
|
||||
|
@ -209,7 +300,10 @@ describe('ResultSet', () => {
|
|||
|
||||
test('should render a failed query with an error message', async () => {
|
||||
await waitFor(() => {
|
||||
setup(failedQueryWithErrorMessageProps, mockStore(initialState));
|
||||
setup(
|
||||
{ ...mockedProps, queryId: failedQueryWithErrorMessage.id },
|
||||
mockStore(failedQueryWithErrorMessageState),
|
||||
);
|
||||
});
|
||||
|
||||
expect(screen.getByText('Database error')).toBeInTheDocument();
|
||||
|
@ -218,44 +312,129 @@ describe('ResultSet', () => {
|
|||
|
||||
test('should render a failed query with an errors object', async () => {
|
||||
await waitFor(() => {
|
||||
setup(failedQueryWithErrorsProps, mockStore(initialState));
|
||||
setup(
|
||||
{ ...mockedProps, queryId: failedQueryWithErrors.id },
|
||||
mockStore(failedQueryWithErrorsState),
|
||||
);
|
||||
});
|
||||
expect(screen.getByText('Database error')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
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));
|
||||
expect(getByRole('table')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('renders if there is a limit in query.results but not queryLimit', async () => {
|
||||
const props = { ...mockedProps, query: queryWithNoQueryLimit };
|
||||
const { getByRole } = setup(props, mockStore(initialState));
|
||||
const props = { ...mockedProps, queryId: queryWithNoQueryLimit.id };
|
||||
const { getByRole } = setup(
|
||||
props,
|
||||
mockStore({
|
||||
...initialState,
|
||||
user,
|
||||
sqlLab: {
|
||||
...initialState.sqlLab,
|
||||
queries: {
|
||||
[queryWithNoQueryLimit.id]: queryWithNoQueryLimit,
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
expect(getByRole('table')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
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(
|
||||
screen.getByRole('button', {
|
||||
name: /fetch data preview/i,
|
||||
}),
|
||||
).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', () => {
|
||||
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(
|
||||
screen.getByRole('button', {
|
||||
name: /refetch results/i,
|
||||
}),
|
||||
).toBeVisible();
|
||||
expect(screen.queryByRole('grid')).toBe(null);
|
||||
expect(screen.queryByRole('table')).toBe(null);
|
||||
});
|
||||
|
||||
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.queryByRole('button', {
|
||||
|
|
|
@ -17,14 +17,14 @@
|
|||
* under the License.
|
||||
*/
|
||||
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 pick from 'lodash/pick';
|
||||
import ButtonGroup from 'src/components/ButtonGroup';
|
||||
import Alert from 'src/components/Alert';
|
||||
import Button from 'src/components/Button';
|
||||
import shortid from 'shortid';
|
||||
import {
|
||||
QueryResponse,
|
||||
QueryState,
|
||||
styled,
|
||||
t,
|
||||
|
@ -41,8 +41,7 @@ import {
|
|||
ISimpleColumn,
|
||||
SaveDatasetModal,
|
||||
} from 'src/SqlLab/components/SaveDatasetModal';
|
||||
import { UserWithPermissionsAndRoles } from 'src/types/bootstrapTypes';
|
||||
import { EXPLORE_CHART_DEFAULT } from 'src/SqlLab/types';
|
||||
import { EXPLORE_CHART_DEFAULT, SqlLabRootState } from 'src/SqlLab/types';
|
||||
import { mountExploreUrl } from 'src/explore/exploreUtils';
|
||||
import { postFormData } from 'src/explore/exploreUtils/formData';
|
||||
import ProgressBar from 'src/components/ProgressBar';
|
||||
|
@ -82,12 +81,11 @@ export interface ResultSetProps {
|
|||
database?: Record<string, any>;
|
||||
displayLimit: number;
|
||||
height: number;
|
||||
query: QueryResponse;
|
||||
queryId: string;
|
||||
search?: boolean;
|
||||
showSql?: boolean;
|
||||
showSqlInline?: boolean;
|
||||
visualize?: boolean;
|
||||
user: UserWithPermissionsAndRoles;
|
||||
defaultQueryLimit: number;
|
||||
}
|
||||
|
||||
|
@ -145,14 +143,44 @@ const ResultSet = ({
|
|||
database = {},
|
||||
displayLimit,
|
||||
height,
|
||||
query,
|
||||
queryId,
|
||||
search = true,
|
||||
showSql = false,
|
||||
showSqlInline = false,
|
||||
visualize = true,
|
||||
user,
|
||||
defaultQueryLimit,
|
||||
}: 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 =
|
||||
extensionsRegistry.get('sqleditor.extension.resultTable') ??
|
||||
FilterableTable;
|
||||
|
@ -179,8 +207,8 @@ const ResultSet = ({
|
|||
reRunQueryIfSessionTimeoutErrorOnMount();
|
||||
}, [reRunQueryIfSessionTimeoutErrorOnMount]);
|
||||
|
||||
const fetchResults = (query: QueryResponse) => {
|
||||
dispatch(fetchQueryResults(query, displayLimit));
|
||||
const fetchResults = (q: typeof query) => {
|
||||
dispatch(fetchQueryResults(q, displayLimit));
|
||||
};
|
||||
|
||||
const prevQuery = usePrevious(query);
|
||||
|
@ -479,7 +507,7 @@ const ResultSet = ({
|
|||
<ResultlessStyles>
|
||||
<ErrorMessageWithStackTrace
|
||||
title={t('Database error')}
|
||||
error={query?.errors?.[0]}
|
||||
error={query?.extra?.errors?.[0] || query?.errors?.[0]}
|
||||
subtitle={<MonospaceDiv>{query.errorMessage}</MonospaceDiv>}
|
||||
copyText={query.errorMessage || undefined}
|
||||
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();
|
||||
});
|
|
@ -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.
|
||||
*/
|
||||
import React from 'react';
|
||||
import configureStore from 'redux-mock-store';
|
||||
import thunk from 'redux-thunk';
|
||||
import { render, screen, waitFor } from 'spec/helpers/testing-library';
|
||||
import SouthPane, { SouthPaneProps } from 'src/SqlLab/components/SouthPane';
|
||||
import { render } from 'spec/helpers/testing-library';
|
||||
import SouthPane from 'src/SqlLab/components/SouthPane';
|
||||
import '@testing-library/jest-dom/extend-expect';
|
||||
import { STATUS_OPTIONS } from 'src/SqlLab/constants';
|
||||
import { initialState, table, defaultQueryEditor } from 'src/SqlLab/fixtures';
|
||||
import { denormalizeTimestamp } from '@superset-ui/core';
|
||||
import { Store } from 'redux';
|
||||
|
||||
const mockedProps = {
|
||||
queryEditorId: defaultQueryEditor.id,
|
||||
|
@ -37,29 +34,32 @@ const mockedProps = {
|
|||
|
||||
const mockedEmptyProps = {
|
||||
queryEditorId: 'random_id',
|
||||
latestQueryId: '',
|
||||
latestQueryId: 'empty_query_id',
|
||||
height: 100,
|
||||
displayLimit: 100,
|
||||
defaultQueryLimit: 100,
|
||||
};
|
||||
|
||||
jest.mock('src/SqlLab/components/SqlEditorLeftBar', () => jest.fn());
|
||||
|
||||
const latestQueryProgressMsg = 'LATEST QUERY MESSAGE - LCly_kkIN';
|
||||
|
||||
const middlewares = [thunk];
|
||||
const mockStore = configureStore(middlewares);
|
||||
const store = mockStore({
|
||||
const mockState = {
|
||||
...initialState,
|
||||
sqlLab: {
|
||||
...initialState,
|
||||
...initialState.sqlLab,
|
||||
offline: false,
|
||||
tables: [
|
||||
{
|
||||
...table,
|
||||
name: 'table3',
|
||||
dataPreviewQueryId: '2g2_iRFMl',
|
||||
queryEditorId: defaultQueryEditor.id,
|
||||
},
|
||||
{
|
||||
...table,
|
||||
name: 'table4',
|
||||
dataPreviewQueryId: 'erWdqEWPm',
|
||||
queryEditorId: defaultQueryEditor.id,
|
||||
},
|
||||
],
|
||||
databases: {},
|
||||
queries: {
|
||||
|
@ -72,6 +72,7 @@ const store = mockStore({
|
|||
startDttm: Date.now(),
|
||||
sqlEditorId: defaultQueryEditor.id,
|
||||
extra: { progress: latestQueryProgressMsg },
|
||||
sql: 'select * from table1',
|
||||
},
|
||||
lXJa7F9_r: {
|
||||
cached: false,
|
||||
|
@ -81,6 +82,7 @@ const store = mockStore({
|
|||
id: 'lXJa7F9_r',
|
||||
startDttm: 1559238500401,
|
||||
sqlEditorId: defaultQueryEditor.id,
|
||||
sql: 'select * from table2',
|
||||
},
|
||||
'2g2_iRFMl': {
|
||||
cached: false,
|
||||
|
@ -90,6 +92,7 @@ const store = mockStore({
|
|||
id: '2g2_iRFMl',
|
||||
startDttm: 1559238506925,
|
||||
sqlEditorId: defaultQueryEditor.id,
|
||||
sql: 'select * from table3',
|
||||
},
|
||||
erWdqEWPm: {
|
||||
cached: false,
|
||||
|
@ -99,44 +102,38 @@ const store = mockStore({
|
|||
id: 'erWdqEWPm',
|
||||
startDttm: 1559238516395,
|
||||
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,
|
||||
...(store && { store }),
|
||||
initialState: {
|
||||
...initialState,
|
||||
sqlLab: {
|
||||
...initialState.sqlLab,
|
||||
offline: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
describe('SouthPane', () => {
|
||||
const renderAndWait = (props: SouthPaneProps, store: Store) =>
|
||||
waitFor(async () => setup(props, store));
|
||||
expect(getByText(STATUS_OPTIONS.offline)).toBeVisible();
|
||||
});
|
||||
|
||||
it('Renders an empty state for results', async () => {
|
||||
await renderAndWait(mockedEmptyProps, store);
|
||||
const emptyStateText = screen.getByText(/run a query to display results/i);
|
||||
expect(emptyStateText).toBeVisible();
|
||||
test('should render tabs for table preview queries', () => {
|
||||
const { getAllByRole } = render(<SouthPane {...mockedProps} />, {
|
||||
useRedux: true,
|
||||
initialState: mockState,
|
||||
});
|
||||
|
||||
it('should render offline when the state is offline', async () => {
|
||||
await renderAndWait(
|
||||
mockedEmptyProps,
|
||||
mockStore({
|
||||
...initialState,
|
||||
sqlLab: {
|
||||
...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();
|
||||
const tabs = getAllByRole('tab');
|
||||
expect(tabs).toHaveLength(mockState.sqlLab.tables.length + 2);
|
||||
expect(tabs[0]).toHaveTextContent('Results');
|
||||
expect(tabs[1]).toHaveTextContent('Query history');
|
||||
mockState.sqlLab.tables.forEach(({ name }, index) => {
|
||||
expect(tabs[index + 2]).toHaveTextContent(`Preview: \`${name}\``);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -19,10 +19,8 @@
|
|||
import React, { createRef, useMemo } from 'react';
|
||||
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
|
||||
import shortid from 'shortid';
|
||||
import Alert from 'src/components/Alert';
|
||||
import Tabs from 'src/components/Tabs';
|
||||
import { EmptyStateMedium } from 'src/components/EmptyState';
|
||||
import { FeatureFlag, styled, t, isFeatureEnabled } from '@superset-ui/core';
|
||||
import { styled, t } from '@superset-ui/core';
|
||||
|
||||
import { setActiveSouthPaneTab } from 'src/SqlLab/actions/sqlLab';
|
||||
|
||||
|
@ -33,11 +31,11 @@ import ResultSet from '../ResultSet';
|
|||
import {
|
||||
STATUS_OPTIONS,
|
||||
STATE_TYPE_MAP,
|
||||
LOCALSTORAGE_MAX_QUERY_AGE_MS,
|
||||
STATUS_OPTIONS_LOCALIZED,
|
||||
} from '../../constants';
|
||||
import Results from './Results';
|
||||
|
||||
const TAB_HEIGHT = 140;
|
||||
const TAB_HEIGHT = 130;
|
||||
|
||||
/*
|
||||
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 = ({
|
||||
queryEditorId,
|
||||
latestQueryId,
|
||||
|
@ -105,128 +91,43 @@ const SouthPane = ({
|
|||
defaultQueryLimit,
|
||||
}: SouthPaneProps) => {
|
||||
const dispatch = useDispatch();
|
||||
const user = useSelector(({ user }: SqlLabRootState) => user, shallowEqual);
|
||||
const { databases, offline, queries, tables } = useSelector(
|
||||
({ sqlLab: { databases, offline, queries, tables } }: SqlLabRootState) => ({
|
||||
databases,
|
||||
const { offline, tables } = useSelector(
|
||||
({ sqlLab: { offline, tables } }: SqlLabRootState) => ({
|
||||
offline,
|
||||
queries,
|
||||
tables,
|
||||
}),
|
||||
shallowEqual,
|
||||
);
|
||||
const editorQueries = useMemo(
|
||||
() =>
|
||||
Object.values(queries).filter(
|
||||
({ sqlEditorId }) => sqlEditorId === queryEditorId,
|
||||
),
|
||||
[queries, queryEditorId],
|
||||
const queries = useSelector(
|
||||
({ sqlLab: { queries } }: SqlLabRootState) => Object.keys(queries),
|
||||
shallowEqual,
|
||||
);
|
||||
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 =
|
||||
useSelector<SqlLabRootState, string>(
|
||||
state => state.sqlLab.activeSouthPaneTab as string,
|
||||
) ?? '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 southPaneRef = createRef<HTMLDivElement>();
|
||||
const switchTab = (id: string) => {
|
||||
dispatch(setActiveSouthPaneTab(id));
|
||||
};
|
||||
const renderOfflineStatus = () => (
|
||||
|
||||
return offline ? (
|
||||
<Label className="m-r-3" type={STATE_TYPE_MAP[STATUS_OPTIONS.offline]}>
|
||||
{STATUS_OPTIONS_LOCALIZED.offline}
|
||||
</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
|
||||
data-test="south-pane"
|
||||
|
@ -243,16 +144,41 @@ const SouthPane = ({
|
|||
animated={false}
|
||||
>
|
||||
<Tabs.TabPane tab={t('Results')} key="Results">
|
||||
{renderResults()}
|
||||
{latestQueryId && (
|
||||
<Results
|
||||
height={innerTabContentHeight}
|
||||
latestQueryId={latestQueryId}
|
||||
displayLimit={displayLimit}
|
||||
defaultQueryLimit={defaultQueryLimit}
|
||||
/>
|
||||
)}
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane tab={t('Query history')} key="History">
|
||||
<QueryHistory
|
||||
queries={editorQueries}
|
||||
queryEditorId={queryEditorId}
|
||||
displayLimit={displayLimit}
|
||||
latestQueryId={latestQueryId}
|
||||
/>
|
||||
</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>
|
||||
</StyledPane>
|
||||
);
|
||||
|
|
|
@ -145,8 +145,8 @@ describe('SqlEditor', () => {
|
|||
(SqlEditorLeftBar as jest.Mock).mockImplementation(() => (
|
||||
<div data-test="mock-sql-editor-left-bar" />
|
||||
));
|
||||
(ResultSet as jest.Mock).mockClear();
|
||||
(ResultSet as jest.Mock).mockImplementation(() => (
|
||||
(ResultSet as unknown as jest.Mock).mockClear();
|
||||
(ResultSet as unknown as jest.Mock).mockImplementation(() => (
|
||||
<div data-test="mock-result-set" />
|
||||
));
|
||||
});
|
||||
|
@ -182,7 +182,8 @@ describe('SqlEditor', () => {
|
|||
const editor = await findByTestId('react-ace');
|
||||
const sql = 'select *';
|
||||
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(ResultSet).toHaveBeenCalledTimes(renderCountForSouthPane);
|
||||
fireEvent.change(editor, { target: { value: sql } });
|
||||
|
|
|
@ -345,7 +345,7 @@ export default function sqlLabReducer(state = {}, action) {
|
|||
return state;
|
||||
}
|
||||
const alts = {
|
||||
endDttm: now(),
|
||||
endDttm: action?.results?.query?.endDttm || now(),
|
||||
progress: 100,
|
||||
results: action.results,
|
||||
rows: action?.results?.query?.rows || 0,
|
||||
|
@ -674,7 +674,14 @@ export default function sqlLabReducer(state = {}, action) {
|
|||
if (!change) {
|
||||
newQueries = state.queries;
|
||||
}
|
||||
return { ...state, queries: newQueries, queriesLastUpdate };
|
||||
return {
|
||||
...state,
|
||||
queries: newQueries,
|
||||
queriesLastUpdate:
|
||||
queriesLastUpdate > state.queriesLastUpdate
|
||||
? queriesLastUpdate
|
||||
: Date.now(),
|
||||
};
|
||||
},
|
||||
[actions.CLEAR_INACTIVE_QUERIES]() {
|
||||
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]() {
|
||||
return { ...state, offline: action.offline };
|
||||
|
|
|
@ -20,6 +20,7 @@ import { QueryState } from '@superset-ui/core';
|
|||
import sqlLabReducer from 'src/SqlLab/reducers/sqlLab';
|
||||
import * as actions from 'src/SqlLab/actions/sqlLab';
|
||||
import { table, initialState as mockState } from '../fixtures';
|
||||
import { QUERY_UPDATE_FREQ } from '../components/QueryAutoRefresh';
|
||||
|
||||
const initialState = mockState.sqlLab;
|
||||
|
||||
|
@ -404,6 +405,7 @@ describe('sqlLabReducer', () => {
|
|||
};
|
||||
});
|
||||
it('updates queries that have already been completed', () => {
|
||||
const current = Date.now();
|
||||
newState = sqlLabReducer(
|
||||
{
|
||||
...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.queriesLastUpdate).toBeGreaterThanOrEqual(current);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue