Skip to content
This repository was archived by the owner on Nov 3, 2023. It is now read-only.

Commit

Permalink
Explore to SQL Lab (apache#5101)
Browse files Browse the repository at this point in the history
* WIP

* Working version

* Clean code

* Fix lint

* Fix unit test; show only for sqla

* Working on UX

* Dropdown working 66%

* Working but needs CSS

* Fixed table

* Fix lint

* Fix unit test

* Fix languages path

* Fixes

* Fix Javascript lint
  • Loading branch information
betodealmeida authored and timifasubaa committed Jul 25, 2018
1 parent 7e5ed2a commit a9f0e9c
Show file tree
Hide file tree
Showing 15 changed files with 152 additions and 37 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import ExploreActionButtons from

describe('ExploreActionButtons', () => {
const defaultProps = {
actions: {},
canDownload: 'True',
latestQueryFormData: {},
queryEndpoint: 'localhost',
Expand Down
19 changes: 19 additions & 0 deletions superset/assets/src/SqlLab/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
Expand Down
4 changes: 3 additions & 1 deletion superset/assets/src/SqlLab/components/TabbedSqlEditors.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
21 changes: 21 additions & 0 deletions superset/assets/src/chart/chartAction.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
100 changes: 70 additions & 30 deletions superset/assets/src/explore/components/DisplayQueryButton.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
});
Expand All @@ -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) {
Expand All @@ -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>
);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,15 @@ 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,
queryResponse: PropTypes.object,
};

export default function ExploreActionButtons({
canDownload, chartStatus, latestQueryFormData, queryResponse }) {
actions, canDownload, chartStatus, latestQueryFormData, queryResponse }) {
const exportToCSVClasses = cx('btn btn-default btn-sm', {
'disabled disabledButton': !canDownload,
});
Expand Down Expand Up @@ -59,6 +60,7 @@ export default function ExploreActionButtons({
queryResponse={queryResponse}
latestQueryFormData={latestQueryFormData}
chartStatus={chartStatus}
onOpenInEditor={actions.redirectSQLLab}
/>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,19 @@ 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>
Expand Down
6 changes: 6 additions & 0 deletions superset/connectors/base/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"""
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions superset/connectors/druid/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ def __html__(self):
@property
def data(self):
return {
'id': self.id,
'name': self.cluster_name,
'backend': 'druid',
}
Expand Down
4 changes: 4 additions & 0 deletions superset/connectors/sqla/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
1 change: 1 addition & 0 deletions superset/models/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -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':
Expand Down
3 changes: 2 additions & 1 deletion superset/views/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')

Expand Down

0 comments on commit a9f0e9c

Please sign in to comment.