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 (
);
+ } 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 && }
+
);
}
}
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')