Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,22 @@
import { isValidElement } from 'react';
import thunk from 'redux-thunk';
import configureStore from 'redux-mock-store';
import userEvent from '@testing-library/user-event';
import QueryTable from 'src/SqlLab/components/QueryTable';
import { runningQuery, successfulQuery, user } from 'src/SqlLab/fixtures';
import { render, screen } from 'spec/helpers/testing-library';
import { render, screen, waitFor } from 'spec/helpers/testing-library';

const mockedProps = {
queries: [runningQuery, successfulQuery],
displayLimit: 100,
latestQueryId: 'ryhMUZCGb',
};

const queryWithResults = {
...successfulQuery,
resultsKey: 'test-results-key-123',
};

// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('QueryTable', () => {
test('is valid', () => {
Expand Down Expand Up @@ -92,4 +98,93 @@ describe('QueryTable', () => {
),
).toHaveLength(1);
});

test('renders View button when query has resultsKey', () => {
const mockStore = configureStore([thunk]);
const propsWithResults = {
...mockedProps,
columns: ['started', 'duration', 'rows', 'results'],
queries: [queryWithResults],
};
render(<QueryTable {...propsWithResults} />, {
store: mockStore({ user, sqlLab: { queries: {} } }),
});

expect(screen.getByRole('button', { name: /view/i })).toBeInTheDocument();
});

test('does not render View button when query has no resultsKey', () => {
const mockStore = configureStore([thunk]);
const queryWithoutResults = {
...successfulQuery,
resultsKey: null,
};
const propsWithoutResults = {
...mockedProps,
columns: ['started', 'duration', 'rows', 'results'],
queries: [queryWithoutResults],
};
render(<QueryTable {...propsWithoutResults} />, {
store: mockStore({ user, sqlLab: { queries: {} } }),
});

expect(
screen.queryByRole('button', { name: /view/i }),
).not.toBeInTheDocument();
});

test('clicking View button opens data preview modal', async () => {
const mockStore = configureStore([thunk]);
const propsWithResults = {
...mockedProps,
columns: ['started', 'duration', 'rows', 'results'],
queries: [queryWithResults],
};
render(<QueryTable {...propsWithResults} />, {
store: mockStore({
user,
sqlLab: {
queries: {
[queryWithResults.id]: queryWithResults,
},
},
}),
});

const viewButton = screen.getByRole('button', { name: /view/i });
await userEvent.click(viewButton);

expect(await screen.findByText('Data preview')).toBeInTheDocument();
});

test('modal closes when exiting', async () => {
const mockStore = configureStore([thunk]);
const propsWithResults = {
...mockedProps,
columns: ['started', 'duration', 'rows', 'results'],
queries: [queryWithResults],
};
render(<QueryTable {...propsWithResults} />, {
store: mockStore({
user,
sqlLab: {
queries: {
[queryWithResults.id]: queryWithResults,
},
},
}),
});

const viewButton = screen.getByRole('button', { name: /view/i });
await userEvent.click(viewButton);

expect(await screen.findByText('Data preview')).toBeInTheDocument();

const closeButton = screen.getByRole('button', { name: /close/i });
await userEvent.click(closeButton);

await waitFor(() => {
expect(screen.queryByText('Data preview')).not.toBeInTheDocument();
});
});
});
110 changes: 82 additions & 28 deletions superset-frontend/src/SqlLab/components/QueryTable/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { useMemo, ReactNode } from 'react';
import { useMemo, ReactNode, useState, useRef } from 'react';
import {
Card,
Button,
Expand All @@ -27,24 +27,25 @@ import {
TableView,
} from '@superset-ui/core/components';
import ProgressBar from '@superset-ui/core/components/ProgressBar';
import { t, QueryResponse } from '@superset-ui/core';
import { t, QueryResponse, QueryState } from '@superset-ui/core';
import { useTheme } from '@apache-superset/core/ui';
import { useDispatch, useSelector } from 'react-redux';
import { shallowEqual, useDispatch, useSelector } from 'react-redux';

import {
queryEditorSetSql,
cloneQueryToNewTab,
fetchQueryResults,
clearQueryResults,
removeQuery,
startQuery,
} from 'src/SqlLab/actions/sqlLab';
import { fDuration, extendedDayjs } from '@superset-ui/core/utils/dates';
import { SqlLabRootState } from 'src/SqlLab/types';
import { UserWithPermissionsAndRoles as User } from 'src/types/bootstrapTypes';
import { makeUrl } from 'src/utils/pathUtils';
import ResultSet from '../ResultSet';
import HighlightedSql from '../HighlightedSql';
import { StaticPosition, StyledTooltip } from './styles';
import { StaticPosition, StyledTooltip, ModalResultSetWrapper } from './styles';

interface QueryTableQuery extends Omit<
QueryResponse,
Expand Down Expand Up @@ -82,6 +83,15 @@ const QueryTable = ({
}: QueryTableProps) => {
const theme = useTheme();
const dispatch = useDispatch();
const [selectedQuery, setSelectedQuery] = useState<QueryResponse | null>(
null,
);
const selectedQueryRef = useRef<QueryResponse | null>(null);
const modalRef = useRef<{
close: () => void;
Comment thread
geido marked this conversation as resolved.
open: (e: React.MouseEvent) => void;
showModal: boolean;
} | null>(null);

const QUERY_HISTORY_TABLE_HEADERS_LOCALIZED = {
state: t('State'),
Expand Down Expand Up @@ -116,6 +126,14 @@ const QueryTable = ({
);

const user = useSelector<SqlLabRootState, User>(state => state.user);
const reduxQueries = useSelector<
SqlLabRootState,
Record<string, QueryResponse>
>(state => state.sqlLab?.queries ?? {}, shallowEqual);

const openAsyncResults = (query: QueryResponse, displayLimit: number) => {
dispatch(fetchQueryResults(query, displayLimit));
};

const data = useMemo(() => {
const restoreSql = (query: QueryResponse) => {
Expand All @@ -128,10 +146,6 @@ const QueryTable = ({
dispatch(cloneQueryToNewTab(query, true));
};

const openAsyncResults = (query: QueryResponse, displayLimit: number) => {
dispatch(fetchQueryResults(query, displayLimit));
};

const statusAttributes = {
success: {
config: {
Expand Down Expand Up @@ -289,26 +303,17 @@ const QueryTable = ({
);
if (q.resultsKey) {
q.results = (
<ModalTrigger
className="ResultsModal"
triggerNode={
<Button buttonSize="xsmall" buttonStyle="secondary">
{t('View')}
</Button>
}
modalTitle={t('Data preview')}
beforeOpen={() => openAsyncResults(query, displayLimit)}
onExit={() => dispatch(clearQueryResults(query))}
modalBody={
<ResultSet
showSql
queryId={query.id}
displayLimit={displayLimit}
defaultQueryLimit={1000}
/>
}
responsive
/>
<Button
buttonSize="xsmall"
buttonStyle="secondary"
onClick={(e: React.MouseEvent) => {
selectedQueryRef.current = query;
setSelectedQuery(query);
modalRef.current?.open(e);
}}
>
{t('View')}
</Button>
);
} else {
q.results = <></>;
Expand Down Expand Up @@ -365,6 +370,55 @@ const QueryTable = ({

return (
<div className="QueryTable">
<ModalTrigger
ref={modalRef}
triggerNode={null}
className="ResultsModal"
modalTitle={t('Data preview')}
beforeOpen={() => {
const query = selectedQueryRef.current;
if (query) {
const existingQuery = reduxQueries[query.id];
if (!existingQuery?.sql && query.sql) {
dispatch(startQuery({ ...query, sql: query.sql }, false));
}
openAsyncResults(query, displayLimit);
}
}}
onExit={() => {
const query = selectedQueryRef.current;
if (query) {
dispatch(clearQueryResults(query));
selectedQueryRef.current = null;
setSelectedQuery(null);
}
}}
modalBody={
selectedQuery ? (
<ModalResultSetWrapper>
{(() => {
const height =
reduxQueries[selectedQuery.id]?.state ===
QueryState.Success &&
reduxQueries[selectedQuery.id]?.results
? Math.floor(window.innerHeight * 0.5)
: undefined;
return (
<ResultSet
showSql
queryId={selectedQuery.id}
displayLimit={displayLimit}
defaultQueryLimit={1000}
useFixedHeight
height={height}
/>
);
})()}
</ModalResultSetWrapper>
) : null
}
responsive
/>
<TableView
columns={columnsOfTable}
data={data}
Expand Down
7 changes: 7 additions & 0 deletions superset-frontend/src/SqlLab/components/QueryTable/styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,10 @@ export const StyledTooltip = styled(IconTooltip)`
}
}
`;

export const ModalResultSetWrapper = styled.div`
display: flex;
flex-direction: column;
overflow: hidden;
max-height: 50vh;
`;
50 changes: 29 additions & 21 deletions superset-frontend/src/SqlLab/components/ResultSet/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -104,12 +104,14 @@ export interface ResultSetProps {
csv?: boolean;
database?: Record<string, any>;
displayLimit: number;
height?: number;
queryId: string;
search?: boolean;
showSql?: boolean;
showSqlInline?: boolean;
visualize?: boolean;
defaultQueryLimit: number;
useFixedHeight?: boolean;
}

const ResultContainer = styled.div`
Expand Down Expand Up @@ -177,12 +179,14 @@ const ResultSet = ({
csv = true,
database = {},
displayLimit,
height,
queryId,
search = true,
showSql = false,
showSqlInline = false,
visualize = true,
defaultQueryLimit,
useFixedHeight = false,
}: ResultSetProps) => {
const user = useSelector(({ user }: SqlLabRootState) => user, shallowEqual);
const streamingThreshold = useSelector(
Expand Down Expand Up @@ -711,6 +715,16 @@ const ResultSet = ({
LocalStorageKeys.SqllabIsRenderHtmlEnabled,
true,
);

const tableProps = {
data,
queryId: query.id,
orderedColumnKeys: results.columns.map(col => col.column_name),
filterText: searchText,
expandedColumns,
allowHTML,
};

return (
<>
<ResultContainer>
Expand Down Expand Up @@ -753,27 +767,21 @@ const ResultSet = ({
{sql}
</>
)}
<div
css={css`
flex: 1 1 auto;
`}
>
<AutoSizer disableWidth>
{({ height }) => (
<ResultTable
data={data}
queryId={query.id}
orderedColumnKeys={results.columns.map(
col => col.column_name,
)}
height={height}
filterText={searchText}
expandedColumns={expandedColumns}
allowHTML={allowHTML}
/>
)}
</AutoSizer>
</div>
{useFixedHeight && height !== undefined ? (
<ResultTable {...tableProps} height={height} />
) : (
<div
css={css`
flex: 1 1 auto;
`}
>
<AutoSizer disableWidth>
{({ height: autoHeight }) => (
<ResultTable {...tableProps} height={autoHeight} />
)}
</AutoSizer>
</div>
)}
</ResultContainer>
<StreamingExportModal
visible={showStreamingModal}
Expand Down
Loading