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()).to.equal(true); }); - it('renders a button and a modal', () => { + it('renders a dropdown', () => { const wrapper = mount(); - 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 58db1a75c3..405f70cc41 100644 --- a/superset/assets/src/SqlLab/actions.js +++ b/superset/assets/src/SqlLab/actions.js @@ -420,6 +420,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 0c5b272996..9ec7271e60 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 ; } else if (this.state.error) { @@ -115,17 +106,66 @@ export default class DisplayQueryButton extends React.PureComponent { } return null; } + renderResultsModalBody() { + if (this.state.isLoading) { + return (Loading...); + } else if (this.state.error) { + return
{this.state.error}
; + } 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) => ( + {k} + )); + return ( + + {headers} + + ); + } + return null; + } render() { return ( - View Query} - modalTitle={t('Query')} - bsSize="large" - beforeOpen={this.beforeOpen} - modalBody={this.renderModalBody()} - /> + + View query} + modalTitle={t('View query')} + bsSize="large" + beforeOpen={this.beforeOpen} + modalBody={this.renderQueryModalBody()} + eventKey="1" + /> + View results} + modalTitle={t('View results')} + bsSize="large" + beforeOpen={this.beforeOpen} + modalBody={this.renderResultsModalBody()} + eventKey="2" + /> + {this.state.sqlSupported && + Run in SQL Lab + } + ); } } 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} /> ); 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' }} />
+ {this.props.datasource.type === 'table' && + + {t('Run SQL queries against this datasource')} + + } + > + + + + } {this.renderDatasource()} {this.renderModal()} 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 @@ class BaseDatasource(AuditMixinNullable, ImportMixin): '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 @@ class BaseDatasource(AuditMixinNullable, ImportMixin): '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 de38ee9c99..0977c0c6b2 100644 --- a/superset/connectors/druid/models.py +++ b/superset/connectors/druid/models.py @@ -109,6 +109,7 @@ class DruidCluster(Model, AuditMixinNullable, ImportMixin): @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 @@ class SqlaTable(Model, BaseDatasource): '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 @@ class Database(Model, AuditMixinNullable, ImportMixin): @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 3994283b71..e64f1ff4fb 100755 --- a/superset/views/core.py +++ b/superset/views/core.py @@ -1072,7 +1072,8 @@ class Superset(BaseSupersetView): 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')