You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@superset.apache.org by GitBox <gi...@apache.org> on 2018/07/16 22:00:50 UTC

[GitHub] mistercrunch closed pull request #5101: Explore to SQL Lab

mistercrunch closed pull request #5101: Explore to SQL Lab
URL: https://github.com/apache/incubator-superset/pull/5101
 
 
   

This is a PR merged from a forked repository.
As GitHub hides the original diff on merge, it is displayed below for
the sake of provenance:

As this is a foreign pull request (from a fork), the diff is supplied
below (as it won't show otherwise due to GitHub magic):

diff --git a/superset/assets/spec/javascripts/explore/components/DisplayQueryButton_spec.jsx b/superset/assets/spec/javascripts/explore/components/DisplayQueryButton_spec.jsx
index 196e07751c..49c41d3703 100644
--- a/superset/assets/spec/javascripts/explore/components/DisplayQueryButton_spec.jsx
+++ b/superset/assets/spec/javascripts/explore/components/DisplayQueryButton_spec.jsx
@@ -16,15 +16,17 @@ describe('DisplayQueryButton', () => {
     },
     chartStatus: 'success',
     queryEndpoint: 'localhost',
+    latestQueryFormData: {
+      datasource: '1__table',
+    },
   };
 
   it('is valid', () => {
     expect(React.isValidElement(<DisplayQueryButton {...defaultProps} />)).to.equal(true);
   });
-  it('renders a button and a modal', () => {
+  it('renders a dropdown', () => {
     const wrapper = mount(<DisplayQueryButton {...defaultProps} />);
-    expect(wrapper.find(ModalTrigger)).to.have.lengthOf(1);
-    wrapper.find('.modal-trigger').simulate('click');
-    expect(wrapper.find(Modal)).to.have.lengthOf(1);
+    expect(wrapper.find(ModalTrigger)).to.have.lengthOf(2);
+    expect(wrapper.find(Modal)).to.have.lengthOf(2);
   });
 });
diff --git a/superset/assets/spec/javascripts/explore/components/ExploreActionButtons_spec.jsx b/superset/assets/spec/javascripts/explore/components/ExploreActionButtons_spec.jsx
index 03be96fecf..5e701f2fb2 100644
--- a/superset/assets/spec/javascripts/explore/components/ExploreActionButtons_spec.jsx
+++ b/superset/assets/spec/javascripts/explore/components/ExploreActionButtons_spec.jsx
@@ -7,6 +7,7 @@ import ExploreActionButtons from
 
 describe('ExploreActionButtons', () => {
   const defaultProps = {
+    actions: {},
     canDownload: 'True',
     latestQueryFormData: {},
     queryEndpoint: 'localhost',
diff --git a/superset/assets/src/SqlLab/actions.js b/superset/assets/src/SqlLab/actions.js
index 540bfe7b03..7139b99bf4 100644
--- a/superset/assets/src/SqlLab/actions.js
+++ b/superset/assets/src/SqlLab/actions.js
@@ -413,6 +413,25 @@ export function popSavedQuery(saveQueryId) {
     });
   };
 }
+export function popDatasourceQuery(datasourceKey, sql) {
+  return function (dispatch) {
+    $.ajax({
+      type: 'GET',
+      url: `/superset/fetch_datasource_metadata?datasourceKey=${datasourceKey}`,
+      success: (metadata) => {
+        const queryEditorProps = {
+          title: 'Query ' + metadata.name,
+          dbId: metadata.database.id,
+          schema: metadata.schema,
+          autorun: sql !== undefined,
+          sql: sql || metadata.select_star,
+        };
+        dispatch(addQueryEditor(queryEditorProps));
+      },
+      error: () => notify.error(t("The datasource couldn't be loaded")),
+    });
+  };
+}
 
 export function createDatasourceStarted() {
   return { type: CREATE_DATASOURCE_STARTED };
diff --git a/superset/assets/src/SqlLab/components/TabbedSqlEditors.jsx b/superset/assets/src/SqlLab/components/TabbedSqlEditors.jsx
index b9acc160d5..97003a67d6 100644
--- a/superset/assets/src/SqlLab/components/TabbedSqlEditors.jsx
+++ b/superset/assets/src/SqlLab/components/TabbedSqlEditors.jsx
@@ -41,11 +41,13 @@ class TabbedSqlEditors extends React.PureComponent {
   }
   componentDidMount() {
     const query = URI(window.location).search(true);
-    if (query.id || query.sql || query.savedQueryId) {
+    if (query.id || query.sql || query.savedQueryId || query.datasourceKey) {
       if (query.id) {
         this.props.actions.popStoredQuery(query.id);
       } else if (query.savedQueryId) {
         this.props.actions.popSavedQuery(query.savedQueryId);
+      } else if (query.datasourceKey) {
+        this.props.actions.popDatasourceQuery(query.datasourceKey, query.sql);
       } else if (query.sql) {
         let dbId = query.dbid;
         if (dbId) {
diff --git a/superset/assets/src/chart/chartAction.js b/superset/assets/src/chart/chartAction.js
index ead159036f..821b410cab 100644
--- a/superset/assets/src/chart/chartAction.js
+++ b/superset/assets/src/chart/chartAction.js
@@ -201,6 +201,27 @@ export function runQuery(formData, force = false, timeout = 60, key) {
   };
 }
 
+export function redirectSQLLab(formData) {
+  return function () {
+    const { url } = getExploreUrlAndPayload({ formData, endpointType: 'query' });
+    $.ajax({
+      type: 'GET',
+      url,
+      success: (response) => {
+        const redirectUrl = new URL(window.location);
+        redirectUrl.pathname = '/superset/sqllab';
+        for (const k of redirectUrl.searchParams.keys()) {
+          redirectUrl.searchParams.delete(k);
+        }
+        redirectUrl.searchParams.set('datasourceKey', formData.datasource);
+        redirectUrl.searchParams.set('sql', response.query);
+        window.open(redirectUrl.href, '_blank');
+      },
+      error: () => notify.error(t("The SQL couldn't be loaded")),
+    });
+  };
+}
+
 export function refreshChart(chart, force, timeout) {
   return (dispatch) => {
     if (!chart.latestQueryFormData || Object.keys(chart.latestQueryFormData).length === 0) {
diff --git a/superset/assets/src/explore/components/DisplayQueryButton.jsx b/superset/assets/src/explore/components/DisplayQueryButton.jsx
index 334ec78439..e098643af5 100644
--- a/superset/assets/src/explore/components/DisplayQueryButton.jsx
+++ b/superset/assets/src/explore/components/DisplayQueryButton.jsx
@@ -6,6 +6,10 @@ import markdown from 'react-syntax-highlighter/languages/hljs/markdown';
 import sql from 'react-syntax-highlighter/languages/hljs/sql';
 import json from 'react-syntax-highlighter/languages/hljs/json';
 import github from 'react-syntax-highlighter/styles/hljs/github';
+import { DropdownButton, MenuItem } from 'react-bootstrap';
+import { BootstrapTable, TableHeaderColumn } from 'react-bootstrap-table';
+import 'react-bootstrap-table/css/react-bootstrap-table.css';
+
 import CopyToClipboard from './../../components/CopyToClipboard';
 import { getExploreUrlAndPayload } from '../exploreUtils';
 
@@ -22,6 +26,7 @@ registerLanguage('json', json);
 const $ = (window.$ = require('jquery'));
 
 const propTypes = {
+  onOpenInEditor: PropTypes.func,
   animation: PropTypes.bool,
   queryResponse: PropTypes.object,
   chartStatus: PropTypes.string,
@@ -37,21 +42,14 @@ export default class DisplayQueryButton extends React.PureComponent {
     this.state = {
       language: null,
       query: null,
+      data: null,
       isLoading: false,
       error: null,
+      sqlSupported: props.latestQueryFormData.datasource.split('__')[1] === 'table',
     };
     this.beforeOpen = this.beforeOpen.bind(this);
-    this.fetchQuery = this.fetchQuery.bind(this);
-  }
-  setStateFromQueryResponse() {
-    const qr = this.props.queryResponse;
-    this.setState({
-      language: qr.language,
-      query: qr.query,
-      isLoading: false,
-    });
   }
-  fetchQuery() {
+  beforeOpen() {
     this.setState({ isLoading: true });
     const { url, payload } = getExploreUrlAndPayload({
       formData: this.props.latestQueryFormData,
@@ -67,6 +65,7 @@ export default class DisplayQueryButton extends React.PureComponent {
         this.setState({
           language: data.language,
           query: data.query,
+          data: data.data,
           isLoading: false,
           error: null,
         });
@@ -79,18 +78,10 @@ export default class DisplayQueryButton extends React.PureComponent {
       },
     });
   }
-  beforeOpen() {
-    if (
-      ['loading', null].indexOf(this.props.chartStatus) >= 0 ||
-      !this.props.queryResponse ||
-      !this.props.queryResponse.query
-    ) {
-      this.fetchQuery();
-    } else {
-      this.setStateFromQueryResponse();
-    }
+  redirectSQLLab() {
+    this.props.onOpenInEditor(this.props.latestQueryFormData);
   }
-  renderModalBody() {
+  renderQueryModalBody() {
     if (this.state.isLoading) {
       return <Loading />;
     } else if (this.state.error) {
@@ -115,17 +106,66 @@ export default class DisplayQueryButton extends React.PureComponent {
     }
     return null;
   }
+  renderResultsModalBody() {
+    if (this.state.isLoading) {
+      return (<img
+        className="loading"
+        alt="Loading..."
+        src="/static/assets/images/loading.gif"
+      />);
+    } else if (this.state.error) {
+      return <pre>{this.state.error}</pre>;
+    } else if (this.state.data) {
+      if (this.state.data.length === 0) {
+        return 'No data';
+      }
+      const headers = Object.keys(this.state.data[0]).map((k, i) => (
+        <TableHeaderColumn key={k} dataField={k} isKey={i === 0} dataSort>{k}</TableHeaderColumn>
+      ));
+      return (
+        <BootstrapTable
+          height="auto"
+          data={this.state.data}
+          striped
+          hover
+          condensed
+        >
+          {headers}
+        </BootstrapTable>
+      );
+    }
+    return null;
+  }
   render() {
     return (
-      <ModalTrigger
-        animation={this.props.animation}
-        isButton
-        triggerNode={<span>View Query</span>}
-        modalTitle={t('Query')}
-        bsSize="large"
-        beforeOpen={this.beforeOpen}
-        modalBody={this.renderModalBody()}
-      />
+      <DropdownButton title={t('Query')} bsSize="sm" pullRight id="query">
+        <ModalTrigger
+          isMenuItem
+          animation={this.props.animation}
+          triggerNode={<span>View query</span>}
+          modalTitle={t('View query')}
+          bsSize="large"
+          beforeOpen={this.beforeOpen}
+          modalBody={this.renderQueryModalBody()}
+          eventKey="1"
+        />
+        <ModalTrigger
+          isMenuItem
+          animation={this.props.animation}
+          triggerNode={<span>View results</span>}
+          modalTitle={t('View results')}
+          bsSize="large"
+          beforeOpen={this.beforeOpen}
+          modalBody={this.renderResultsModalBody()}
+          eventKey="2"
+        />
+        {this.state.sqlSupported && <MenuItem
+          eventKey="3"
+          onClick={this.redirectSQLLab.bind(this)}
+        >
+          Run in SQL Lab
+        </MenuItem>}
+      </DropdownButton>
     );
   }
 }
diff --git a/superset/assets/src/explore/components/ExploreActionButtons.jsx b/superset/assets/src/explore/components/ExploreActionButtons.jsx
index f383d66b88..1e604240e1 100644
--- a/superset/assets/src/explore/components/ExploreActionButtons.jsx
+++ b/superset/assets/src/explore/components/ExploreActionButtons.jsx
@@ -8,6 +8,7 @@ import { t } from '../../locales';
 import { exportChart, getExploreLongUrl } from '../exploreUtils';
 
 const propTypes = {
+  actions: PropTypes.object.isRequired,
   canDownload: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]).isRequired,
   chartStatus: PropTypes.string,
   latestQueryFormData: PropTypes.object,
@@ -15,7 +16,7 @@ const propTypes = {
 };
 
 export default function ExploreActionButtons({
-    canDownload, chartStatus, latestQueryFormData, queryResponse }) {
+    actions, canDownload, chartStatus, latestQueryFormData, queryResponse }) {
   const exportToCSVClasses = cx('btn btn-default btn-sm', {
     'disabled disabledButton': !canDownload,
   });
@@ -59,6 +60,7 @@ export default function ExploreActionButtons({
         queryResponse={queryResponse}
         latestQueryFormData={latestQueryFormData}
         chartStatus={chartStatus}
+        onOpenInEditor={actions.redirectSQLLab}
       />
     </div>
   );
diff --git a/superset/assets/src/explore/components/ExploreChartHeader.jsx b/superset/assets/src/explore/components/ExploreChartHeader.jsx
index 3825335f1d..e211ca9d91 100644
--- a/superset/assets/src/explore/components/ExploreChartHeader.jsx
+++ b/superset/assets/src/explore/components/ExploreChartHeader.jsx
@@ -133,6 +133,7 @@ class ExploreChartHeader extends React.PureComponent {
             style={{ fontSize: '10px', marginRight: '5px' }}
           />
           <ExploreActionButtons
+            actions={this.props.actions}
             slice={this.props.slice}
             canDownload={this.props.can_download}
             chartStatus={chartStatus}
diff --git a/superset/assets/src/explore/components/ExploreViewContainer.jsx b/superset/assets/src/explore/components/ExploreViewContainer.jsx
index 3eada5ce28..315cc80ad2 100644
--- a/superset/assets/src/explore/components/ExploreViewContainer.jsx
+++ b/superset/assets/src/explore/components/ExploreViewContainer.jsx
@@ -271,6 +271,7 @@ class ExploreViewContainer extends React.Component {
               loading={this.props.chart.chartStatus === 'loading'}
               chartIsStale={this.state.chartIsStale}
               errorMessage={this.renderErrorMessage()}
+              datasourceType={this.props.datasource_type}
             />
             <br />
             <ControlPanelsContainer
diff --git a/superset/assets/src/explore/components/controls/DatasourceControl.jsx b/superset/assets/src/explore/components/controls/DatasourceControl.jsx
index d63d6fe806..ea1dd14e83 100644
--- a/superset/assets/src/explore/components/controls/DatasourceControl.jsx
+++ b/superset/assets/src/explore/components/controls/DatasourceControl.jsx
@@ -220,6 +220,19 @@ export default class DatasourceControl extends React.PureComponent {
             />
           </a>
         </OverlayTrigger>
+        {this.props.datasource.type === 'table' &&
+          <OverlayTrigger
+            placement="right"
+            overlay={
+              <Tooltip id={'datasource-sqllab'}>
+                {t('Run SQL queries against this datasource')}
+              </Tooltip>
+            }
+          >
+            <a href={'/superset/sqllab?datasourceKey=' + this.props.value}>
+              <i className="fa fa-flask m-r-5" />
+            </a>
+          </OverlayTrigger>}
         <Collapse in={this.state.showDatasource}>{this.renderDatasource()}</Collapse>
         {this.renderModal()}
       </div>
diff --git a/superset/connectors/base/models.py b/superset/connectors/base/models.py
index b306299ccf..213f89597e 100644
--- a/superset/connectors/base/models.py
+++ b/superset/connectors/base/models.py
@@ -152,6 +152,10 @@ def short_data(self):
             'creator': str(self.created_by),
         }
 
+    @property
+    def select_star(self):
+        pass
+
     @property
     def data(self):
         """Data representation of the datasource sent to the frontend"""
@@ -185,6 +189,8 @@ def data(self):
             'metrics': [o.data for o in self.metrics],
             'columns': [o.data for o in self.columns],
             'verbose_map': verbose_map,
+            'schema': self.schema,
+            'select_star': self.select_star,
         }
 
     @staticmethod
diff --git a/superset/connectors/druid/models.py b/superset/connectors/druid/models.py
index 6fb44b3088..c40a115e73 100644
--- a/superset/connectors/druid/models.py
+++ b/superset/connectors/druid/models.py
@@ -109,6 +109,7 @@ def __html__(self):
     @property
     def data(self):
         return {
+            'id': self.id,
             'name': self.cluster_name,
             'backend': 'druid',
         }
diff --git a/superset/connectors/sqla/models.py b/superset/connectors/sqla/models.py
index 2cb0e95a17..205df32b09 100644
--- a/superset/connectors/sqla/models.py
+++ b/superset/connectors/sqla/models.py
@@ -369,6 +369,10 @@ def time_column_grains(self):
             'time_grains': [grain.name for grain in self.database.grains()],
         }
 
+    @property
+    def select_star(self):
+        return self.database.select_star(self.name, show_cols=True)
+
     def get_col(self, col_name):
         columns = self.columns
         for col in columns:
diff --git a/superset/models/core.py b/superset/models/core.py
index 4e195ae41d..5cc3ee1b3f 100644
--- a/superset/models/core.py
+++ b/superset/models/core.py
@@ -647,6 +647,7 @@ def name(self):
     @property
     def data(self):
         return {
+            'id': self.id,
             'name': self.database_name,
             'backend': self.backend,
             'allow_multi_schema_metadata_fetch':
diff --git a/superset/views/core.py b/superset/views/core.py
index 0e51eac044..9636d7275f 100755
--- a/superset/views/core.py
+++ b/superset/views/core.py
@@ -1072,7 +1072,8 @@ def get_query_string_response(self, viz_obj):
             json.dumps({
                 'query': query,
                 'language': viz_obj.datasource.query_language,
-            }),
+                'data': viz_obj.get_df().to_dict('records'),
+            }, default=utils.json_iso_dttm_ser),
             status=200,
             mimetype='application/json')
 


 

----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on GitHub and use the
URL above to go to the specific comment.
 
For queries about this service, please contact Infrastructure at:
users@infra.apache.org


With regards,
Apache Git Services

---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org