diff --git a/superset-frontend/spec/javascripts/explore/components/DisplayQueryButton_spec.jsx b/superset-frontend/spec/javascripts/explore/components/ExploreAdditionalActionsMenu.jsx similarity index 81% rename from superset-frontend/spec/javascripts/explore/components/DisplayQueryButton_spec.jsx rename to superset-frontend/spec/javascripts/explore/components/ExploreAdditionalActionsMenu.jsx index 6a6757142fbc..09380586c3c3 100644 --- a/superset-frontend/spec/javascripts/explore/components/DisplayQueryButton_spec.jsx +++ b/superset-frontend/spec/javascripts/explore/components/ExploreAdditionalActionsMenu.jsx @@ -20,9 +20,9 @@ import React from 'react'; import { mount, shallow } from 'enzyme'; import { supersetTheme, ThemeProvider } from '@superset-ui/core'; import { Dropdown, Menu } from 'src/common/components'; -import { DisplayQueryButton } from 'src/explore/components/DisplayQueryButton'; +import ExploreAdditionalActionsMenu from 'src/explore/components/ExploreAdditionalActionsMenu'; -describe('DisplayQueryButton', () => { +describe('ExploreAdditionalActionsMenu', () => { const defaultProps = { animation: false, queryResponse: { @@ -38,12 +38,12 @@ describe('DisplayQueryButton', () => { }; it('is valid', () => { - expect(React.isValidElement()).toBe( - true, - ); + expect( + React.isValidElement(), + ).toBe(true); }); - it('renders a dropdown with 3 itens', () => { - const wrapper = mount(, { + it('renders a dropdown with 3 items', () => { + const wrapper = mount(, { wrappingComponent: ThemeProvider, wrappingComponentProps: { theme: supersetTheme, diff --git a/superset-frontend/src/dashboard/components/SliceHeader/SliceHeader.test.tsx b/superset-frontend/src/dashboard/components/SliceHeader/SliceHeader.test.tsx index b036ef696c56..e846bd57d071 100644 --- a/superset-frontend/src/dashboard/components/SliceHeader/SliceHeader.test.tsx +++ b/superset-frontend/src/dashboard/components/SliceHeader/SliceHeader.test.tsx @@ -157,6 +157,7 @@ const createProps = () => ({ forceRefresh: jest.fn(), exploreChart: jest.fn(), exportCSV: jest.fn(), + formData: {}, }); test('Should render', () => { diff --git a/superset-frontend/src/dashboard/components/SliceHeader/index.tsx b/superset-frontend/src/dashboard/components/SliceHeader/index.tsx index cb82080eddd2..27d9e8000bf4 100644 --- a/superset-frontend/src/dashboard/components/SliceHeader/index.tsx +++ b/superset-frontend/src/dashboard/components/SliceHeader/index.tsx @@ -56,10 +56,11 @@ type SliceHeaderProps = { addDangerToast: Function; handleToggleFullSize: Function; chartStatus: string; + formData: object; }; -const annoationsLoading = t('Annotation layers are still loading.'); -const annoationsError = t('One ore more annotation layers failed loading.'); +const annotationsLoading = t('Annotation layers are still loading.'); +const annotationsError = t('One ore more annotation layers failed loading.'); const CrossFilterIcon = styled(Icon)` fill: ${({ theme }) => theme.colors.grayscale.light5}; @@ -95,6 +96,7 @@ const SliceHeader: FC = ({ handleToggleFullSize, isFullSize, chartStatus, + formData, }) => { // TODO: change to indicator field after it will be implemented const crossFilterValue = useSelector( @@ -120,11 +122,11 @@ const SliceHeader: FC = ({ @@ -133,11 +135,11 @@ const SliceHeader: FC = ({ @@ -183,6 +185,7 @@ const SliceHeader: FC = ({ handleToggleFullSize={handleToggleFullSize} isFullSize={isFullSize} chartStatus={chartStatus} + formData={formData} /> )} diff --git a/superset-frontend/src/dashboard/components/SliceHeaderControls/index.jsx b/superset-frontend/src/dashboard/components/SliceHeaderControls/index.jsx index daea11aea031..9c3d025864b0 100644 --- a/superset-frontend/src/dashboard/components/SliceHeaderControls/index.jsx +++ b/superset-frontend/src/dashboard/components/SliceHeaderControls/index.jsx @@ -33,6 +33,8 @@ import { getActiveFilters } from 'src/dashboard/util/activeDashboardFilters'; import { FeatureFlag, isFeatureEnabled } from 'src/featureFlags'; import CrossFilterScopingModal from 'src/dashboard/components/CrossFilterScopingModal/CrossFilterScopingModal'; import Icons from 'src/components/Icons'; +import ModalTrigger from 'src/components/ModalTrigger'; +import ViewQueryModal from 'src/explore/components/controls/ViewQueryModal'; const propTypes = { slice: PropTypes.object.isRequired, @@ -76,6 +78,7 @@ const MENU_KEYS = { EXPORT_CSV: 'export_csv', RESIZE_LABEL: 'resize_label', DOWNLOAD_AS_IMAGE: 'download_as_image', + VIEW_QUERY: 'view_query', }; const VerticalDotsContainer = styled.div` @@ -256,6 +259,21 @@ class SliceHeaderControls extends React.PureComponent { )} + {this.props.supersetCanExplore && ( + + {t('View query')} + } + modalTitle={t('View query')} + modalBody={ + + } + responsive + /> + + )} + {supersetCanShare && ( {/* diff --git a/superset-frontend/src/explore/components/ExploreActionButtons.tsx b/superset-frontend/src/explore/components/ExploreActionButtons.tsx index 339d2b3b7071..3b020ae17000 100644 --- a/superset-frontend/src/explore/components/ExploreActionButtons.tsx +++ b/superset-frontend/src/explore/components/ExploreActionButtons.tsx @@ -25,8 +25,8 @@ import copyTextToClipboard from 'src/utils/copy'; import withToasts from 'src/messageToasts/enhancers/withToasts'; import { useUrlShortener } from 'src/common/hooks/useUrlShortener'; import EmbedCodeButton from './EmbedCodeButton'; -import ConnectedDisplayQueryButton from './DisplayQueryButton'; import { exportChart, getExploreLongUrl } from '../exploreUtils'; +import ExploreAdditionalActionsMenu from './ExploreAdditionalActionsMenu'; type ActionButtonProps = { icon: React.ReactElement; @@ -39,7 +39,7 @@ type ActionButtonProps = { }; type ExploreActionButtonsProps = { - actions: { redirectSQLLab: Function; openPropertiesModal: Function }; + actions: { redirectSQLLab: () => void; openPropertiesModal: () => void }; canDownloadCSV: boolean; chartStatus: string; latestQueryFormData: {}; @@ -90,7 +90,6 @@ const ExploreActionButtons = (props: ExploreActionButtonsProps) => { canDownloadCSV, chartStatus, latestQueryFormData, - queriesResponse, slice, addDangerToast, } = props; @@ -178,8 +177,7 @@ const ExploreActionButtons = (props: ExploreActionButtonsProps) => { /> )} - ({ latestQueryFormData: { @@ -90,15 +90,15 @@ fetchMock.post( }, ); -test('Shoud render a button', () => { +test('Should render a button', () => { const props = createProps(); - render(, { useRedux: true }); + render(, { useRedux: true }); expect(screen.getByRole('button')).toBeInTheDocument(); }); -test('Shoud open a menu', () => { +test('Should open a menu', () => { const props = createProps(); - render(, { + render(, { useRedux: true, }); @@ -122,9 +122,9 @@ test('Shoud open a menu', () => { ).toBeInTheDocument(); }); -test('Shoud call onOpenPropertiesModal when click on "Edit properties"', () => { +test('Should call onOpenPropertiesModal when click on "Edit properties"', () => { const props = createProps(); - render(, { + render(, { useRedux: true, }); expect(props.onOpenInEditor).toBeCalledTimes(0); @@ -133,10 +133,10 @@ test('Shoud call onOpenPropertiesModal when click on "Edit properties"', () => { expect(props.onOpenPropertiesModal).toBeCalledTimes(1); }); -test('Shoud call getChartDataRequest when click on "View query"', async () => { +test('Should call getChartDataRequest when click on "View query"', async () => { const props = createProps(); const getChartDataRequest = jest.spyOn(chartAction, 'getChartDataRequest'); - render(, { + render(, { useRedux: true, }); @@ -150,9 +150,9 @@ test('Shoud call getChartDataRequest when click on "View query"', async () => { await waitFor(() => expect(getChartDataRequest).toBeCalledTimes(1)); }); -test('Shoud call onOpenInEditor when click on "Run in SQL Lab"', () => { +test('Should call onOpenInEditor when click on "Run in SQL Lab"', () => { const props = createProps(); - render(, { + render(, { useRedux: true, }); @@ -164,10 +164,10 @@ test('Shoud call onOpenInEditor when click on "Run in SQL Lab"', () => { expect(props.onOpenInEditor).toBeCalledTimes(1); }); -test('Shoud call downloadAsImage when click on "Download as image"', () => { +test('Should call downloadAsImage when click on "Download as image"', () => { const props = createProps(); const spy = jest.spyOn(downloadAsImage, 'default'); - render(, { + render(, { useRedux: true, }); diff --git a/superset-frontend/src/explore/components/DisplayQueryButton/index.jsx b/superset-frontend/src/explore/components/ExploreAdditionalActionsMenu/index.jsx similarity index 51% rename from superset-frontend/src/explore/components/DisplayQueryButton/index.jsx rename to superset-frontend/src/explore/components/ExploreAdditionalActionsMenu/index.jsx index 56f74d6e3a5e..dc936a516915 100644 --- a/superset-frontend/src/explore/components/DisplayQueryButton/index.jsx +++ b/superset-frontend/src/explore/components/ExploreAdditionalActionsMenu/index.jsx @@ -16,31 +16,16 @@ * specific language governing permissions and limitations * under the License. */ -import React, { useState } from 'react'; +import React from 'react'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; import PropTypes from 'prop-types'; -import SyntaxHighlighter from 'react-syntax-highlighter/dist/cjs/light'; -import htmlSyntax from 'react-syntax-highlighter/dist/cjs/languages/hljs/htmlbars'; -import markdownSyntax from 'react-syntax-highlighter/dist/cjs/languages/hljs/markdown'; -import sqlSyntax from 'react-syntax-highlighter/dist/cjs/languages/hljs/sql'; -import jsonSyntax from 'react-syntax-highlighter/dist/cjs/languages/hljs/json'; -import github from 'react-syntax-highlighter/dist/cjs/styles/hljs/github'; -import { styled, t } from '@superset-ui/core'; +import { t } from '@superset-ui/core'; import { Dropdown, Menu } from 'src/common/components'; -import { getClientErrorObject } from 'src/utils/getClientErrorObject'; -import CopyToClipboard from 'src/components/CopyToClipboard'; -import { getChartDataRequest } from 'src/chart/chartAction'; import downloadAsImage from 'src/utils/downloadAsImage'; -import Loading from 'src/components/Loading'; import ModalTrigger from 'src/components/ModalTrigger'; import { sliceUpdated } from 'src/explore/actions/exploreActions'; -import { CopyButton } from 'src/explore/components/DataTableControl'; - -SyntaxHighlighter.registerLanguage('markdown', markdownSyntax); -SyntaxHighlighter.registerLanguage('html', htmlSyntax); -SyntaxHighlighter.registerLanguage('sql', sqlSyntax); -SyntaxHighlighter.registerLanguage('json', jsonSyntax); +import ViewQueryModal from '../controls/ViewQueryModal'; const propTypes = { onOpenPropertiesModal: PropTypes.func, @@ -54,53 +39,12 @@ const MENU_KEYS = { EDIT_PROPERTIES: 'edit_properties', RUN_IN_SQL_LAB: 'run_in_sql_lab', DOWNLOAD_AS_IMAGE: 'download_as_image', + VIEW_QUERY: 'view_query', }; -const CopyButtonViewQuery = styled(CopyButton)` - && { - margin: 0 0 ${({ theme }) => theme.gridUnit}px; - } -`; - -export const DisplayQueryButton = props => { +const ExploreAdditionalActionsMenu = props => { const { datasource } = props.latestQueryFormData; - - const [language, setLanguage] = useState(null); - const [query, setQuery] = useState(null); - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(null); - const [sqlSupported] = useState( - datasource && datasource.split('__')[1] === 'table', - ); - - const beforeOpen = resultType => { - setIsLoading(true); - - getChartDataRequest({ - formData: props.latestQueryFormData, - resultFormat: 'json', - resultType, - }) - .then(response => { - // Only displaying the first query is currently supported - const result = response.result[0]; - setLanguage(result.language); - setQuery(result.query); - setIsLoading(false); - setError(null); - }) - .catch(response => { - getClientErrorObject(response).then( - ({ error, message, statusText }) => { - setError( - error || message || statusText || t('Sorry, An error occurred'), - ); - setIsLoading(false); - }, - ); - }); - }; - + const sqlSupported = datasource && datasource.split('__')[1] === 'table'; const handleMenuClick = ({ key, domEvent }) => { const { slice, onOpenInEditor, latestQueryFormData } = props; switch (key) { @@ -124,34 +68,6 @@ export const DisplayQueryButton = props => { } }; - const renderQueryModalBody = () => { - if (isLoading) { - return ; - } - if (error) { - return
{error}
; - } - if (query) { - return ( -
- - - - } - /> - - {query} - -
- ); - } - return null; - }; - const { slice } = props; return ( { {t('Edit properties')} )} - + {t('View query')} } modalTitle={t('View query')} - beforeOpen={() => beforeOpen('query')} - modalBody={renderQueryModalBody()} + modalBody={ + + } responsive /> @@ -198,10 +117,10 @@ export const DisplayQueryButton = props => { ); }; -DisplayQueryButton.propTypes = propTypes; +ExploreAdditionalActionsMenu.propTypes = propTypes; function mapDispatchToProps(dispatch) { return bindActionCreators({ sliceUpdated }, dispatch); } -export default connect(null, mapDispatchToProps)(DisplayQueryButton); +export default connect(null, mapDispatchToProps)(ExploreAdditionalActionsMenu); diff --git a/superset-frontend/src/explore/components/controls/ViewQueryModal.tsx b/superset-frontend/src/explore/components/controls/ViewQueryModal.tsx new file mode 100644 index 000000000000..1234613b5052 --- /dev/null +++ b/superset-frontend/src/explore/components/controls/ViewQueryModal.tsx @@ -0,0 +1,112 @@ +/** + * 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, { useEffect, useState } from 'react'; +import { styled, t } from '@superset-ui/core'; +import SyntaxHighlighter from 'react-syntax-highlighter/dist/cjs/light'; +import github from 'react-syntax-highlighter/dist/cjs/styles/hljs/github'; +import CopyToClipboard from 'src/components/CopyToClipboard'; +import Loading from 'src/components/Loading'; +import { CopyButton } from 'src/explore/components/DataTableControl'; +import { getClientErrorObject } from 'src/utils/getClientErrorObject'; +import { getChartDataRequest } from 'src/chart/chartAction'; +import markdownSyntax from 'react-syntax-highlighter/dist/cjs/languages/hljs/markdown'; +import htmlSyntax from 'react-syntax-highlighter/dist/cjs/languages/hljs/htmlbars'; +import sqlSyntax from 'react-syntax-highlighter/dist/cjs/languages/hljs/sql'; +import jsonSyntax from 'react-syntax-highlighter/dist/cjs/languages/hljs/json'; + +const CopyButtonViewQuery = styled(CopyButton)` + && { + margin: 0 0 ${({ theme }) => theme.gridUnit}px; + } +`; + +SyntaxHighlighter.registerLanguage('markdown', markdownSyntax); +SyntaxHighlighter.registerLanguage('html', htmlSyntax); +SyntaxHighlighter.registerLanguage('sql', sqlSyntax); +SyntaxHighlighter.registerLanguage('json', jsonSyntax); + +interface Props { + latestQueryFormData: object; +} + +const ViewQueryModal: React.FC = props => { + const [language, setLanguage] = useState(null); + const [query, setQuery] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + const loadChartData = (resultType: string) => { + setIsLoading(true); + getChartDataRequest({ + formData: props.latestQueryFormData, + resultFormat: 'json', + resultType, + }) + .then(response => { + // Only displaying the first query is currently supported + const result = response.result[0]; + setLanguage(result.language); + setQuery(result.query); + setIsLoading(false); + setError(null); + }) + .catch(response => { + getClientErrorObject(response).then(({ error, message }) => { + setError( + error || + message || + response.statusText || + t('Sorry, An error occurred'), + ); + setIsLoading(false); + }); + }); + }; + useEffect(() => { + loadChartData('query'); + }, [props.latestQueryFormData]); + + if (isLoading) { + return ; + } + if (error) { + return
{error}
; + } + if (query) { + return ( +
+ + + + } + /> + + {query} + +
+ ); + } + return null; +}; + +export default ViewQueryModal;