diff --git a/superset-frontend/spec/javascripts/sqllab/QuerySearch_spec.jsx b/superset-frontend/spec/javascripts/sqllab/QuerySearch_spec.jsx index c650a4cdafe2..b3a6091d8881 100644 --- a/superset-frontend/spec/javascripts/sqllab/QuerySearch_spec.jsx +++ b/superset-frontend/spec/javascripts/sqllab/QuerySearch_spec.jsx @@ -26,9 +26,12 @@ import { supersetTheme, ThemeProvider } from '@superset-ui/core'; import { fireEvent, render, screen, act } from '@testing-library/react'; import '@testing-library/jest-dom/extend-expect'; import userEvent from '@testing-library/user-event'; +import { user } from './fixtures'; const mockStore = configureStore([thunk]); -const store = mockStore({}); +const store = mockStore({ + sqlLab: user, +}); const SEARCH_ENDPOINT = 'glob:*/superset/search_queries?*'; const USER_ENDPOINT = 'glob:*/api/v1/query/related/user'; diff --git a/superset-frontend/spec/javascripts/sqllab/QueryTable_spec.jsx b/superset-frontend/spec/javascripts/sqllab/QueryTable_spec.jsx index 6c963e22c834..fdce51102535 100644 --- a/superset-frontend/spec/javascripts/sqllab/QueryTable_spec.jsx +++ b/superset-frontend/spec/javascripts/sqllab/QueryTable_spec.jsx @@ -17,11 +17,14 @@ * under the License. */ import React from 'react'; -import { shallow } from 'enzyme'; +import thunk from 'redux-thunk'; +import configureStore from 'redux-mock-store'; +import { styledMount as mount } from 'spec/helpers/theming'; import QueryTable from 'src/SqlLab/components/QueryTable'; import TableView from 'src/components/TableView'; import { TableCollection } from 'src/components/dataViewCommon'; -import { queries } from './fixtures'; +import { Provider } from 'react-redux'; +import { queries, user } from './fixtures'; describe('QueryTable', () => { const mockedProps = { @@ -35,12 +38,18 @@ describe('QueryTable', () => { expect(React.isValidElement()).toBe(true); }); it('renders a proper table', () => { - const wrapper = shallow(); - const tableWrapper = wrapper - .find(TableView) - .shallow() - .find(TableCollection) - .shallow(); + const mockStore = configureStore([thunk]); + const store = mockStore({ + sqlLab: user, + }); + + const wrapper = mount( + + + , + ); + const tableWrapper = wrapper.find(TableView).find(TableCollection); + expect(wrapper.find(TableView)).toExist(); expect(tableWrapper.find('table')).toExist(); expect(tableWrapper.find('table').find('thead').find('tr')).toHaveLength(1); diff --git a/superset-frontend/spec/javascripts/sqllab/ResultSet_spec.jsx b/superset-frontend/spec/javascripts/sqllab/ResultSet_spec.jsx index 5860b2b58711..69e87f97e6b2 100644 --- a/superset-frontend/spec/javascripts/sqllab/ResultSet_spec.jsx +++ b/superset-frontend/spec/javascripts/sqllab/ResultSet_spec.jsx @@ -37,6 +37,7 @@ import { runningQuery, stoppedQuery, initialState, + user, } from './fixtures'; const mockStore = configureStore([thunk]); @@ -54,8 +55,10 @@ describe('ResultSet', () => { }, cache: true, query: queries[0], - height: 0, + height: 140, database: { allows_virtual_table_explore: true }, + user, + defaultQueryLimit: 1000, }; const stoppedQueryProps = { ...mockedProps, query: stoppedQuery }; const runningQueryProps = { ...mockedProps, query: runningQuery }; diff --git a/superset-frontend/spec/javascripts/sqllab/fixtures.ts b/superset-frontend/spec/javascripts/sqllab/fixtures.ts index fa3209cddd70..911a89d5fed7 100644 --- a/superset-frontend/spec/javascripts/sqllab/fixtures.ts +++ b/superset-frontend/spec/javascripts/sqllab/fixtures.ts @@ -208,6 +208,7 @@ export const queries = [ executedSql: null, changed_on: '2016-10-19T20:56:06', rows: 42, + queryLimit: 100, endDttm: 1476910566798, limit_reached: false, schema: 'test_schema', @@ -461,6 +462,18 @@ export const runningQuery = { }; export const cachedQuery = { ...queries[0], cached: true }; +export const user = { + createdOn: '2021-04-27T18:12:38.952304', + email: 'admin', + firstName: 'admin', + isActive: true, + lastName: 'admin', + permissions: {}, + roles: { Admin: Array(173) }, + userId: 1, + username: 'admin', +}; + export const initialState = { sqlLab: { offline: false, @@ -473,6 +486,7 @@ export const initialState = { workspaceQueries: [], queriesLastUpdate: 0, activeSouthPaneTab: 'Results', + user: { user }, }, messageToasts: [], common: { diff --git a/superset-frontend/src/SqlLab/components/QueryTable.jsx b/superset-frontend/src/SqlLab/components/QueryTable.jsx index 993aa0e5e3a6..e9414e59c216 100644 --- a/superset-frontend/src/SqlLab/components/QueryTable.jsx +++ b/superset-frontend/src/SqlLab/components/QueryTable.jsx @@ -22,8 +22,8 @@ import moment from 'moment'; import Card from 'src/components/Card'; import ProgressBar from 'src/components/ProgressBar'; import Label from 'src/components/Label'; -import { t } from '@superset-ui/core'; - +import { t, css } from '@superset-ui/core'; +import { useSelector } from 'react-redux'; import TableView from 'src/components/TableView'; import Button from 'src/components/Button'; import { fDuration } from 'src/modules/dates'; @@ -53,6 +53,10 @@ const openQuery = id => { window.open(url); }; +const StaticPosition = css` + position: static; +`; + const QueryTable = props => { const columns = useMemo( () => @@ -64,6 +68,8 @@ const QueryTable = props => { [props.columns], ); + const user = useSelector(({ sqlLab: { user } }) => user); + const data = useMemo(() => { const restoreSql = query => { props.actions.queryEditorSetSql({ id: query.sqlEditorId }, query.sql); @@ -129,7 +135,7 @@ const QueryTable = props => { ); q.sql = ( - + { modalBody={ ; saveModalAutocompleteValue: string; userDatasetOptions: DatasetOptionAutocomplete[]; + alertIsOpen: boolean; } // Making text render line breaks/tabs as is as monospace, @@ -103,6 +112,10 @@ const MonospaceDiv = styled.div` const ReturnedRows = styled.div` font-size: 13px; line-height: 24px; + .limitMessage { + color: ${({ theme }) => theme.colors.secondary.light1}; + margin-left: ${({ theme }) => theme.gridUnit * 2}px; + } `; const ResultSetControls = styled.div` display: flex; @@ -146,8 +159,8 @@ export default class ResultSet extends React.PureComponent< datasetToOverwrite: {}, saveModalAutocompleteValue: '', userDatasetOptions: [], + alertIsOpen: false, }; - this.changeSearch = this.changeSearch.bind(this); this.fetchResults = this.fetchResults.bind(this); this.popSelectStar = this.popSelectStar.bind(this); @@ -207,6 +220,14 @@ export default class ResultSet extends React.PureComponent< } } + calculateAlertRefHeight = (alertElement: HTMLElement | null) => { + if (alertElement) { + this.setState({ alertIsOpen: true }); + } else { + this.setState({ alertIsOpen: false }); + } + }; + getDefaultDatasetName = () => `${this.props.query.tab} ${moment().format('MM/DD/YYYY HH:mm:ss')}`; @@ -321,12 +342,8 @@ export default class ResultSet extends React.PureComponent< getUserDatasets = async (searchText = '') => { // Making sure that autocomplete input has a value before rendering the dropdown // Transforming the userDatasetsOwned data for SaveModalComponent) - const appContainer = document.getElementById('app'); - const bootstrapData = JSON.parse( - appContainer?.getAttribute('data-bootstrap') || '{}', - ); - - if (bootstrapData.user && bootstrapData.user.userId) { + const { userId } = this.props.user; + if (userId) { const queryParams = rison.encode({ filters: [ { @@ -337,7 +354,7 @@ export default class ResultSet extends React.PureComponent< { col: 'owners', opr: 'rel_m_m', - value: bootstrapData.user.userId, + value: userId, }, ], order_column: 'changed_on_delta_humanized', @@ -501,25 +518,105 @@ export default class ResultSet extends React.PureComponent< return
; } + onAlertClose = () => { + this.setState({ alertIsOpen: false }); + }; + renderRowsReturned() { - const { results, rows, queryLimit } = this.props.query; + const { results, rows, queryLimit, limitingFactor } = this.props.query; + let limitMessage; const limitReached = results?.displayLimitReached; + const isAdmin = !!this.props.user?.roles.Admin; + const displayMaxRowsReachedMessage = { + withAdmin: t( + `The number of results displayed is limited to %(rows)d by the configuration DISPLAY_MAX_ROWS. `, + { rows }, + ).concat( + t( + `Please add additional limits/filters or download to csv to see more rows up to the`, + ), + t(`the %(queryLimit)d limit.`, { queryLimit }), + ), + withoutAdmin: t( + `The number of results displayed is limited to %(rows)d. `, + { rows }, + ).concat( + t( + `Please add additional limits/filters, download to csv, or contact an admin`, + ), + t(`to see more rows up to the the %(queryLimit)d limit.`, { + queryLimit, + }), + ), + }; + const shouldUseDefaultDropdownAlert = + queryLimit === this.props.defaultQueryLimit && + limitingFactor === LIMITING_FACTOR.DROPDOWN; + + if (limitingFactor === LIMITING_FACTOR.QUERY && this.props.csv) { + limitMessage = ( + + {t( + `The number of rows displayed is limited to %(rows)d by the query`, + { rows }, + )} + + ); + } else if ( + limitingFactor === LIMITING_FACTOR.DROPDOWN && + !shouldUseDefaultDropdownAlert + ) { + limitMessage = ( + + {t( + `The number of rows displayed is limited to %(rows)d by the limit dropdown.`, + { rows }, + )} + + ); + } else if (limitingFactor === LIMITING_FACTOR.QUERY_AND_DROPDOWN) { + limitMessage = ( + + {t( + `The number of rows displayed is limited to %(rows)d by the query and limit dropdown.`, + { rows }, + )} + + ); + } return ( - {!limitReached && ( - + {!limitReached && !shouldUseDefaultDropdownAlert && ( + + {t(`%(rows)d rows returned`, { rows })} {limitMessage} + + )} + {!limitReached && shouldUseDefaultDropdownAlert && ( +
+ +
)} {limitReached && ( - +
+ +
)}
); @@ -527,10 +624,6 @@ export default class ResultSet extends React.PureComponent< render() { const { query } = this.props; - const height = Math.max( - 0, - this.props.search ? this.props.height - SEARCH_HEIGHT : this.props.height, - ); let sql; let exploreDBId = query.dbId; if (this.props.database && this.props.database.explore_database_id) { @@ -601,6 +694,9 @@ export default class ResultSet extends React.PureComponent< } if (query.state === 'success' && query.results) { const { results } = query; + const height = this.state.alertIsOpen + ? this.props.height - 70 + : this.props.height; let data; if (this.props.cache && query.cached) { ({ data } = this.state); diff --git a/superset-frontend/src/SqlLab/components/SouthPane/SouthPane.tsx b/superset-frontend/src/SqlLab/components/SouthPane/SouthPane.tsx index d04f3df6f1a1..f084fc4384fe 100644 --- a/superset-frontend/src/SqlLab/components/SouthPane/SouthPane.tsx +++ b/superset-frontend/src/SqlLab/components/SouthPane/SouthPane.tsx @@ -25,6 +25,7 @@ import { t, styled } from '@superset-ui/core'; import { isFeatureEnabled, FeatureFlag } from 'src/featureFlags'; import Label from 'src/components/Label'; +import { UserWithPermissionsAndRoles } from 'src/types/bootstrapTypes'; import QueryHistory from '../QueryHistory'; import ResultSet from '../ResultSet'; import { @@ -33,7 +34,7 @@ import { LOCALSTORAGE_MAX_QUERY_AGE_MS, } from '../../constants'; -const TAB_HEIGHT = 90; +const TAB_HEIGHT = 140; /* editorQueries are queries executed by users passed from SqlEditor component @@ -49,6 +50,8 @@ interface SouthPanePropTypes { databases: Record; offline?: boolean; displayLimit: number; + user: UserWithPermissionsAndRoles; + defaultQueryLimit: number; } const StyledPane = styled.div` @@ -61,6 +64,16 @@ const StyledPane = styled.div` height: 100%; display: flex; flex-direction: column; + .scrollable { + overflow-y: auto; + } + } + .ant-tabs-tabpane { + display: flex; + flex-direction: column; + .scrollable { + overflow-y: auto; + } } .tab-content { .alert { @@ -83,13 +96,14 @@ export default function SouthPane({ databases, offline = false, displayLimit, + user, + defaultQueryLimit, }: SouthPanePropTypes) { const innerTabContentHeight = height - TAB_HEIGHT; const southPaneRef = createRef(); const switchTab = (id: string) => { actions.setActiveSouthPaneTab(id); }; - const renderOfflineStatus = () => (