Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[explore] add "View samples" modal to action buttons #5770

Merged
merged 3 commits into from
Sep 20, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,6 @@ describe('DisplayQueryButton', () => {
});
it('renders a dropdown', () => {
const wrapper = mount(<DisplayQueryButton {...defaultProps} />);
expect(wrapper.find(ModalTrigger)).to.have.lengthOf(2);
expect(wrapper.find(ModalTrigger)).to.have.lengthOf(3);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -54,5 +54,6 @@ describe('TableElement', () => {
wrapper.find('.table-remove').simulate('click');
expect(wrapper.state().expanded).to.equal(false);
expect(mockedActions.removeDataPreview.called).to.equal(true);
expect(mockedActions.removeTable.called).to.equal(true);
});
});
103 changes: 78 additions & 25 deletions superset/assets/src/explore/components/DisplayQueryButton.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ 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 { DropdownButton, MenuItem, Row, Col, FormControl } from 'react-bootstrap';
import { Table } from 'reactable';
import $ from 'jquery';

import CopyToClipboard from './../../components/CopyToClipboard';
import { getExploreUrlAndPayload } from '../exploreUtils';
Expand All @@ -17,14 +17,13 @@ import Loading from '../../components/Loading';
import ModalTrigger from './../../components/ModalTrigger';
import Button from '../../components/Button';
import { t } from '../../locales';
import RowCountLabel from './RowCountLabel';

registerLanguage('markdown', markdown);
registerLanguage('html', html);
registerLanguage('sql', sql);
registerLanguage('json', json);

const $ = (window.$ = require('jquery'));

const propTypes = {
onOpenInEditor: PropTypes.func,
animation: PropTypes.bool,
Expand All @@ -46,15 +45,17 @@ export default class DisplayQueryButton extends React.PureComponent {
data: null,
isLoading: false,
error: null,
filterText: '',
sqlSupported: datasource && datasource.split('__')[1] === 'table',
};
this.beforeOpen = this.beforeOpen.bind(this);
this.changeFilterText = this.changeFilterText.bind(this);
}
beforeOpen() {
beforeOpen(endpointType) {
this.setState({ isLoading: true });
const { url, payload } = getExploreUrlAndPayload({
formData: this.props.latestQueryFormData,
endpointType: 'query',
endpointType,
});
$.ajax({
type: 'POST',
Expand All @@ -79,6 +80,9 @@ export default class DisplayQueryButton extends React.PureComponent {
},
});
}
changeFilterText(event) {
this.setState({ filterText: event.target.value });
}
redirectSQLLab() {
this.props.onOpenInEditor(this.props.latestQueryFormData);
}
Expand Down Expand Up @@ -111,7 +115,7 @@ export default class DisplayQueryButton extends React.PureComponent {
if (this.state.isLoading) {
return (<img
className="loading"
alt="Loading..."
alt={t('Loading...')}
src="/static/assets/images/loading.gif"
/>);
} else if (this.state.error) {
Expand All @@ -120,33 +124,72 @@ export default class DisplayQueryButton extends React.PureComponent {
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 this.renderDataTable(this.state.data);
}
return null;
}
renderDataTable(data) {
return (
<div style={{ overflow: 'auto' }}>
<Row>
<Col md={9}>
<RowCountLabel rowcount={data.length} suffix={t('rows retrieved')} />
</Col>
<Col md={3}>
<FormControl
placeholder={t('Search')}
bsSize="sm"
value={this.state.filterText}
onChange={this.changeFilterText}
style={{ paddingBottom: '5px' }}
/>
</Col>
</Row>
<Table
className="table table-condensed"
sortable
data={data}
hideFilterInput
filterBy={this.state.filterText}
filterable={data.length ? Object.keys(data[0]) : null}
noDataText={t('No data')}
/>
</div>
);
}
renderSamplesModalBody() {
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) {
return this.renderDataTable(this.state.data);
}
return null;
}
render() {
return (
<DropdownButton title={t('Query')} bsSize="sm" pullRight id="query">
<DropdownButton
noCaret
title={
<span>
<i className="fa fa-bars" />&nbsp;
</span>}
bsSize="sm"
pullRight
id="query"
>
<ModalTrigger
isMenuItem
animation={this.props.animation}
triggerNode={<span>{t('View query')}</span>}
modalTitle={t('View query')}
bsSize="large"
beforeOpen={this.beforeOpen}
beforeOpen={() => this.beforeOpen('query')}
modalBody={this.renderQueryModalBody()}
eventKey="1"
/>
Expand All @@ -156,10 +199,20 @@ export default class DisplayQueryButton extends React.PureComponent {
triggerNode={<span>{t('View results')}</span>}
modalTitle={t('View results')}
bsSize="large"
beforeOpen={this.beforeOpen}
beforeOpen={() => this.beforeOpen('results')}
modalBody={this.renderResultsModalBody()}
eventKey="2"
/>
<ModalTrigger
isMenuItem
animation={this.props.animation}
triggerNode={<span>{t('View samples')}</span>}
modalTitle={t('View samples')}
bsSize="large"
beforeOpen={() => this.beforeOpen('samples')}
modalBody={this.renderSamplesModalBody()}
eventKey="2"
/>
{this.state.sqlSupported && <MenuItem
eventKey="3"
onClick={this.redirectSQLLab.bind(this)}
Expand Down
7 changes: 5 additions & 2 deletions superset/assets/src/explore/components/RowCountLabel.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,15 @@ import TooltipWrapper from '../../components/TooltipWrapper';
const propTypes = {
rowcount: PropTypes.number,
limit: PropTypes.number,
rows: PropTypes.string,
suffix: PropTypes.string,
};

const defaultProps = {
suffix: t('rows'),
};

export default function RowCountLabel({ rowcount, limit }) {
export default function RowCountLabel({ rowcount, limit, suffix }) {
const limitReached = rowcount === limit;
const bsStyle = (limitReached || rowcount === 0) ? 'warning' : 'default';
const formattedRowCount = defaultNumberFormatter(rowcount);
Expand All @@ -32,7 +35,7 @@ export default function RowCountLabel({ rowcount, limit }) {
bsStyle={bsStyle}
style={{ fontSize: '10px', marginRight: '5px', cursor: 'pointer' }}
>
{formattedRowCount} rows
{formattedRowCount}{' '}{suffix}
</Label>
</TooltipWrapper>
);
Expand Down
8 changes: 7 additions & 1 deletion superset/assets/src/explore/exploreUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export function getAnnotationJsonUrl(slice_id, form_data, isNative) {
export function getURIDirectory(formData, endpointType = 'base') {
// Building the directory part of the URI
let directory = '/superset/explore/';
if (['json', 'csv', 'query'].indexOf(endpointType) >= 0) {
if (['json', 'csv', 'query', 'results', 'samples'].indexOf(endpointType) >= 0) {
directory = '/superset/explore_json/';
}
return directory;
Expand Down Expand Up @@ -81,6 +81,12 @@ export function getExploreUrlAndPayload({
if (endpointType === 'query') {
search.query = 'true';
}
if (endpointType === 'results') {
search.results = 'true';
}
if (endpointType === 'samples') {
search.samples = 'true';
}
const paramNames = Object.keys(requestParams);
if (paramNames.length) {
paramNames.forEach((name) => {
Expand Down
7 changes: 5 additions & 2 deletions superset/assets/stylesheets/superset.less
Original file line number Diff line number Diff line change
Expand Up @@ -425,8 +425,8 @@ g.annotation-container {
font: normal normal normal 14px/1 FontAwesome;
content: "\f0dc";
position: absolute;
top: 17px;
right: 15px;
top: 6px;
right: 5px;
color: @brand-primary;
}
.reactable-header-sort-asc::before{
Expand All @@ -437,6 +437,9 @@ g.annotation-container {
content: "\f0dd";
color: @brand-primary;
}
tr.reactable-column-header th.reactable-header-sortable {
padding-right: 17px;
}

.explore-chart-overlay {
position: absolute;
Expand Down
6 changes: 3 additions & 3 deletions superset/views/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@

from datetime import datetime
import functools
import json
import logging
import traceback

Expand All @@ -19,6 +18,7 @@
from flask_babel import get_locale
from flask_babel import gettext as __
from flask_babel import lazy_gettext as _
import simplejson as json
import yaml

from superset import conf, db, security_manager, utils
Expand Down Expand Up @@ -52,7 +52,7 @@ def json_error_response(msg=None, status=500, stacktrace=None, payload=None, lin
payload['link'] = link

return Response(
json.dumps(payload, default=utils.json_iso_dttm_ser),
json.dumps(payload, default=utils.json_iso_dttm_ser, ignore_nan=True),
status=status, mimetype='application/json')


Expand Down Expand Up @@ -95,7 +95,7 @@ class BaseSupersetView(BaseView):

def json_response(self, obj, status=200):
return Response(
json.dumps(obj, default=utils.json_int_dttm_ser),
json.dumps(obj, default=utils.json_int_dttm_ser, ignore_nan=True),
status=status,
mimetype='application/json')

Expand Down
69 changes: 50 additions & 19 deletions superset/views/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -1048,17 +1048,26 @@ def get_query_string_response(self, viz_obj):
else:
query = 'No query.'

return Response(
json.dumps({
'query': query,
'language': viz_obj.datasource.query_language,
'data': viz_obj.get_df().to_dict('records'), # TODO, split into endpoint
}, default=utils.json_iso_dttm_ser),
status=200,
mimetype='application/json')
return self.json_response({
'query': query,
'language': viz_obj.datasource.query_language,
})

def get_raw_results(self, viz_obj):
return self.json_response({
'data': viz_obj.get_df().to_dict('records'),
})

def get_samples(self, viz_obj):
return self.json_response({
'data': viz_obj.get_samples(),
})

def generate_json(self, datasource_type, datasource_id, form_data,
csv=False, query=False, force=False):
def generate_json(
self, datasource_type, datasource_id, form_data,
csv=False, query=False, force=False, results=False,
samples=False,
):
try:
viz_obj = self.get_viz(
datasource_type=datasource_type,
Expand Down Expand Up @@ -1088,6 +1097,12 @@ def generate_json(self, datasource_type, datasource_id, form_data,
if query:
return self.get_query_string_response(viz_obj)

if results:
return self.get_raw_results(viz_obj)

if samples:
return self.get_samples(viz_obj)

try:
payload = viz_obj.get_payload()
except SupersetException as se:
Expand Down Expand Up @@ -1154,10 +1169,22 @@ def annotation_json(self, layer_id):
@expose('/explore_json/<datasource_type>/<datasource_id>/', methods=['GET', 'POST'])
@expose('/explore_json/', methods=['GET', 'POST'])
def explore_json(self, datasource_type=None, datasource_id=None):
"""Serves all request that GET or POST form_data

This endpoint evolved to be the entry point of many different
requests that GETs or POSTs a form_data.

`self.generate_json` receives this input and returns different
payloads based on the request args in the first block

TODO: break into one endpoint for each return shape"""
csv = request.args.get('csv') == 'true'
query = request.args.get('query') == 'true'
results = request.args.get('results') == 'true'
samples = request.args.get('samples') == 'true'
force = request.args.get('force') == 'true'

try:
csv = request.args.get('csv') == 'true'
query = request.args.get('query') == 'true'
force = request.args.get('force') == 'true'
form_data = self.get_form_data()[0]
datasource_id, datasource_type = self.datasource_info(
datasource_id, datasource_type, form_data)
Expand All @@ -1166,12 +1193,16 @@ def explore_json(self, datasource_type=None, datasource_id=None):
return json_error_response(
utils.error_msg_from_exception(e),
stacktrace=traceback.format_exc())
return self.generate_json(datasource_type=datasource_type,
datasource_id=datasource_id,
form_data=form_data,
csv=csv,
query=query,
force=force)
return self.generate_json(
datasource_type=datasource_type,
datasource_id=datasource_id,
form_data=form_data,
csv=csv,
query=query,
results=results,
force=force,
samples=samples,
)

@log_this
@has_access
Expand Down
Loading