mirror of
https://github.com/apache/superset.git
synced 2026-04-21 09:04:38 +00:00
Numerous improvements to SQL Lab (#1088)
* Improving the Visualize flow * Fixed the timer * CTAS * Expiclit engine handling * make tab full height, stretch for longer content (#1081) * Better error handling for queries * Hooked and fixed CSV export * Linting * Tying in the dttm in the viz flow * Indicator showing when going offline * Addressing comments, fixing the build * Fixing unit tests
This commit is contained in:
committed by
GitHub
parent
c20ee0c129
commit
1971bf653c
@@ -1,60 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Alert, Button } from 'react-bootstrap';
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import * as Actions from '../actions';
|
||||
import QueryLink from './QueryLink';
|
||||
|
||||
const LeftPane = (props) => {
|
||||
let queryElements;
|
||||
if (props.workspaceQueries.length > 0) {
|
||||
queryElements = props.workspaceQueries.map((q) => <QueryLink query={q} />);
|
||||
} else {
|
||||
queryElements = (
|
||||
<Alert bsStyle="info">
|
||||
Use the save button on the SQL editor to save a query
|
||||
into this section for future reference.
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<div className="panel panel-default LeftPane">
|
||||
<div className="panel-heading">
|
||||
<div className="panel-title">
|
||||
Saved Queries
|
||||
</div>
|
||||
</div>
|
||||
<div className="panel-body">
|
||||
{queryElements}
|
||||
</div>
|
||||
</div>
|
||||
<br /><br />
|
||||
<Button onClick={props.actions.resetState.bind(this)} bsStyle="danger">
|
||||
Reset State
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
LeftPane.propTypes = {
|
||||
workspaceQueries: React.PropTypes.array,
|
||||
actions: React.PropTypes.object,
|
||||
};
|
||||
|
||||
LeftPane.defaultProps = {
|
||||
workspaceQueries: [],
|
||||
};
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
workspaceQueries: state.workspaceQueries,
|
||||
};
|
||||
}
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators(Actions, dispatch),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(LeftPane);
|
||||
@@ -4,7 +4,8 @@ import { connect } from 'react-redux';
|
||||
import * as Actions from '../actions';
|
||||
|
||||
const $ = require('jquery');
|
||||
|
||||
const QUERY_UPDATE_FREQ = 1000;
|
||||
const QUERY_UPDATE_BUFFER_MS = 5000;
|
||||
|
||||
class QueryAutoRefresh extends React.Component {
|
||||
componentWillMount() {
|
||||
@@ -15,7 +16,7 @@ class QueryAutoRefresh extends React.Component {
|
||||
}
|
||||
startTimer() {
|
||||
if (!(this.timer)) {
|
||||
this.timer = setInterval(this.stopwatch.bind(this), 1000);
|
||||
this.timer = setInterval(this.stopwatch.bind(this), QUERY_UPDATE_FREQ);
|
||||
}
|
||||
}
|
||||
stopTimer() {
|
||||
@@ -23,12 +24,20 @@ class QueryAutoRefresh extends React.Component {
|
||||
this.timer = null;
|
||||
}
|
||||
stopwatch() {
|
||||
const url = '/caravel/queries/0';
|
||||
const url = '/caravel/queries/' + (this.props.queriesLastUpdate - QUERY_UPDATE_BUFFER_MS);
|
||||
// No updates in case of failure.
|
||||
$.getJSON(url, (data, status) => {
|
||||
if (status === 'success') {
|
||||
$.getJSON(url, (data) => {
|
||||
if (Object.keys(data).length > 0) {
|
||||
this.props.actions.refreshQueries(data);
|
||||
}
|
||||
if (!this.props.networkOn) {
|
||||
this.props.actions.setNetworkStatus(true);
|
||||
}
|
||||
})
|
||||
.fail(() => {
|
||||
if (this.props.networkOn) {
|
||||
this.props.actions.setNetworkStatus(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
render() {
|
||||
@@ -37,13 +46,18 @@ class QueryAutoRefresh extends React.Component {
|
||||
}
|
||||
QueryAutoRefresh.propTypes = {
|
||||
actions: React.PropTypes.object,
|
||||
queriesLastUpdate: React.PropTypes.integer,
|
||||
networkOn: React.PropTypes.boolean,
|
||||
};
|
||||
QueryAutoRefresh.defaultProps = {
|
||||
// queries: null,
|
||||
};
|
||||
|
||||
function mapStateToProps() {
|
||||
return {};
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
queriesLastUpdate: state.queriesLastUpdate,
|
||||
networkOn: state.networkOn,
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
|
||||
@@ -42,7 +42,7 @@ class QueryTable extends React.Component {
|
||||
q.duration = fDuration(q.startDttm, q.endDttm);
|
||||
}
|
||||
q.started = moment.utc(q.startDttm).format('HH:mm:ss');
|
||||
const source = q.ctas ? q.executedSql : q.sql;
|
||||
const source = (q.ctas) ? q.executedSql : q.sql;
|
||||
q.sql = (
|
||||
<SqlShrink sql={source} />
|
||||
);
|
||||
|
||||
@@ -43,7 +43,7 @@ class ResultSet extends React.Component {
|
||||
>
|
||||
<i className="fa fa-line-chart m-l-1" /> Visualize
|
||||
</Button>
|
||||
<Button bsSize="small">
|
||||
<Button bsSize="small" href={'/caravel/csv/' + this.props.query.id}>
|
||||
<i className="fa fa-file-text-o" /> .CSV
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
|
||||
@@ -1,44 +1,85 @@
|
||||
import { Alert, Tab, Tabs } from 'react-bootstrap';
|
||||
import { Alert, Button, Tab, Tabs } from 'react-bootstrap';
|
||||
import QueryHistory from './QueryHistory';
|
||||
import ResultSet from './ResultSet';
|
||||
import React from 'react';
|
||||
|
||||
const SouthPane = function (props) {
|
||||
let results = <div />;
|
||||
if (props.latestQuery) {
|
||||
if (props.latestQuery.state === 'running') {
|
||||
results = (
|
||||
<img className="loading" alt="Loading.." src="/static/assets/images/loading.gif" />
|
||||
);
|
||||
} else if (props.latestQuery.state === 'failed') {
|
||||
results = <Alert bsStyle="danger">{props.latestQuery.msg}</Alert>;
|
||||
} else if (props.latestQuery.state === 'success') {
|
||||
results = <ResultSet showControls query={props.latestQuery} />;
|
||||
}
|
||||
} else {
|
||||
results = <Alert bsStyle="info">Run a query to display results here</Alert>;
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import * as Actions from '../actions';
|
||||
import shortid from 'shortid';
|
||||
|
||||
class SouthPane extends React.Component {
|
||||
popSelectStar() {
|
||||
const qe = {
|
||||
id: shortid.generate(),
|
||||
title: this.props.latestQuery.tempTable,
|
||||
autorun: false,
|
||||
dbId: this.props.latestQuery.dbId,
|
||||
sql: `SELECT * FROM ${this.props.latestQuery.tempTable}`,
|
||||
};
|
||||
this.props.actions.addQueryEditor(qe);
|
||||
}
|
||||
return (
|
||||
<div className="SouthPane">
|
||||
<Tabs bsStyle="tabs">
|
||||
<Tab title="Results" eventKey={1}>
|
||||
<div style={{ overflow: 'auto' }}>
|
||||
{results}
|
||||
</div>
|
||||
</Tab>
|
||||
<Tab title="Query History" eventKey={2}>
|
||||
<QueryHistory />
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
render() {
|
||||
let results = <div />;
|
||||
const latestQuery = this.props.latestQuery;
|
||||
if (latestQuery) {
|
||||
if (['running', 'pending'].includes(latestQuery.state)) {
|
||||
results = (
|
||||
<img className="loading" alt="Loading.." src="/static/assets/images/loading.gif" />
|
||||
);
|
||||
} else if (latestQuery.state === 'failed') {
|
||||
results = <Alert bsStyle="danger">{latestQuery.errorMessage}</Alert>;
|
||||
} else if (latestQuery.state === 'success' && latestQuery.ctas) {
|
||||
results = (
|
||||
<div>
|
||||
<Alert bsStyle="info">
|
||||
Table [<strong>{latestQuery.tempTable}</strong>] was created
|
||||
</Alert>
|
||||
<p>
|
||||
<Button
|
||||
bsSize="small"
|
||||
className="m-r-5"
|
||||
onClick={this.popSelectStar.bind(this)}
|
||||
>
|
||||
Query in a new tab
|
||||
</Button>
|
||||
<Button bsSize="small">Visualize</Button>
|
||||
</p>
|
||||
</div>);
|
||||
} else if (latestQuery.state === 'success') {
|
||||
results = <ResultSet showControls search query={latestQuery} />;
|
||||
}
|
||||
} else {
|
||||
results = <Alert bsStyle="info">Run a query to display results here</Alert>;
|
||||
}
|
||||
return (
|
||||
<div className="SouthPane">
|
||||
<Tabs bsStyle="tabs">
|
||||
<Tab title="Results" eventKey={1}>
|
||||
<div style={{ overflow: 'auto' }}>
|
||||
{results}
|
||||
</div>
|
||||
</Tab>
|
||||
<Tab title="Query History" eventKey={2}>
|
||||
<QueryHistory />
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SouthPane.propTypes = {
|
||||
latestQuery: React.PropTypes.object,
|
||||
actions: React.PropTypes.object,
|
||||
};
|
||||
|
||||
SouthPane.defaultProps = {
|
||||
};
|
||||
|
||||
export default SouthPane;
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators(Actions, dispatch),
|
||||
};
|
||||
}
|
||||
export default connect(null, mapDispatchToProps)(SouthPane);
|
||||
|
||||
@@ -97,15 +97,19 @@ class SqlEditor extends React.Component {
|
||||
that.props.actions.querySuccess(query, results);
|
||||
}
|
||||
},
|
||||
error(err) {
|
||||
error(err, textStatus, errorThrown) {
|
||||
let msg;
|
||||
try {
|
||||
msg = err.responseJSON.error;
|
||||
} catch (e) {
|
||||
msg = (err.responseText) ? err.responseText : e;
|
||||
if (err.responseText !== undefined) {
|
||||
msg = err.responseText;
|
||||
}
|
||||
}
|
||||
if (typeof(msg) !== 'string') {
|
||||
msg = JSON.stringify(msg);
|
||||
if (textStatus === 'error' && errorThrown === '') {
|
||||
msg = 'Could not connect to server';
|
||||
} else if (msg === null) {
|
||||
msg = `[${textStatus}] ${errorThrown}`;
|
||||
}
|
||||
that.props.actions.queryFailed(query, msg);
|
||||
},
|
||||
@@ -135,6 +139,15 @@ class SqlEditor extends React.Component {
|
||||
ctasChanged(event) {
|
||||
this.setState({ ctas: event.target.value });
|
||||
}
|
||||
|
||||
sqlEditorHeight() {
|
||||
// quick hack to make the white bg of the tab stretch full height.
|
||||
const tabNavHeight = 40;
|
||||
const navBarHeight = 56;
|
||||
const mysteryVerticalHeight = 50;
|
||||
return window.innerHeight - tabNavHeight - navBarHeight - mysteryVerticalHeight;
|
||||
}
|
||||
|
||||
render() {
|
||||
let runButtons = (
|
||||
<ButtonGroup bsSize="small" className="inline m-r-5 pull-left">
|
||||
@@ -248,34 +261,30 @@ class SqlEditor extends React.Component {
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div className="SqlEditor">
|
||||
<div>
|
||||
<div>
|
||||
<Row>
|
||||
<Col md={3}>
|
||||
<SqlEditorLeft queryEditor={this.props.queryEditor} />
|
||||
</Col>
|
||||
<Col md={9}>
|
||||
<AceEditor
|
||||
mode="sql"
|
||||
name={this.props.queryEditor.id}
|
||||
theme="github"
|
||||
minLines={7}
|
||||
maxLines={30}
|
||||
onChange={this.textChange.bind(this)}
|
||||
height="200px"
|
||||
width="100%"
|
||||
editorProps={{ $blockScrolling: true }}
|
||||
enableBasicAutocompletion
|
||||
value={this.props.queryEditor.sql}
|
||||
/>
|
||||
{editorBottomBar}
|
||||
<br />
|
||||
<SouthPane latestQuery={this.props.latestQuery} sqlEditor={this} />
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
</div>
|
||||
<div className="SqlEditor" style={{ minHeight: this.sqlEditorHeight() }}>
|
||||
<Row>
|
||||
<Col md={3}>
|
||||
<SqlEditorLeft queryEditor={this.props.queryEditor} />
|
||||
</Col>
|
||||
<Col md={9}>
|
||||
<AceEditor
|
||||
mode="sql"
|
||||
name={this.props.queryEditor.id}
|
||||
theme="github"
|
||||
minLines={7}
|
||||
maxLines={30}
|
||||
onChange={this.textChange.bind(this)}
|
||||
height="200px"
|
||||
width="100%"
|
||||
editorProps={{ $blockScrolling: true }}
|
||||
enableBasicAutocompletion
|
||||
value={this.props.queryEditor.sql}
|
||||
/>
|
||||
{editorBottomBar}
|
||||
<br />
|
||||
<SouthPane latestQuery={this.props.latestQuery} sqlEditor={this} />
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { connect } from 'react-redux';
|
||||
import * as Actions from '../actions';
|
||||
import shortid from 'shortid';
|
||||
import Select from 'react-select';
|
||||
import { Button } from 'react-bootstrap';
|
||||
import { Label, Button } from 'react-bootstrap';
|
||||
import TableElement from './TableElement';
|
||||
|
||||
|
||||
@@ -116,10 +116,15 @@ class SqlEditorTopToolbar extends React.Component {
|
||||
});
|
||||
}
|
||||
render() {
|
||||
let networkAlert = null;
|
||||
if (!this.props.networkOn) {
|
||||
networkAlert = <p><Label bsStyle="danger">OFFLINE</Label></p>;
|
||||
}
|
||||
const tables = this.props.tables.filter((t) => (t.queryEditorId === this.props.queryEditor.id));
|
||||
const shouldShowReset = window.location.search === '?reset=1';
|
||||
return (
|
||||
<div className="clearfix sql-toolbar">
|
||||
{networkAlert}
|
||||
<div>
|
||||
<Select
|
||||
name="select-db"
|
||||
@@ -174,6 +179,7 @@ SqlEditorTopToolbar.propTypes = {
|
||||
queryEditor: React.PropTypes.object,
|
||||
tables: React.PropTypes.array,
|
||||
actions: React.PropTypes.object,
|
||||
networkOn: React.PropTypes.boolean,
|
||||
};
|
||||
|
||||
SqlEditorTopToolbar.defaultProps = {
|
||||
@@ -183,6 +189,7 @@ SqlEditorTopToolbar.defaultProps = {
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
tables: state.tables,
|
||||
networkOn: state.networkOn,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,8 @@ import SyntaxHighlighter from 'react-syntax-highlighter';
|
||||
import { github } from 'react-syntax-highlighter/dist/styles';
|
||||
|
||||
const SqlShrink = (props) => {
|
||||
let lines = props.sql.split('\n');
|
||||
const sql = props.sql || '';
|
||||
let lines = sql.split('\n');
|
||||
if (lines.length >= props.maxLines) {
|
||||
lines = lines.slice(0, props.maxLines);
|
||||
lines.push('{...}');
|
||||
|
||||
@@ -49,7 +49,6 @@ class QueryEditors extends React.Component {
|
||||
const editors = this.props.queryEditors.map((qe, i) => {
|
||||
let latestQuery = this.props.queries[qe.latestQueryId];
|
||||
const database = this.props.databases[qe.dbId];
|
||||
|
||||
const state = (latestQuery) ? latestQuery.state : '';
|
||||
const tabTitle = (
|
||||
<div>
|
||||
|
||||
@@ -27,7 +27,7 @@ class Timer extends React.Component {
|
||||
}
|
||||
stopwatch() {
|
||||
if (this.props && this.props.query) {
|
||||
const since = (this.props.query.endDttm) ? this.props.query.endDttm : now();
|
||||
const since = this.props.query.endDttm || now();
|
||||
const clockStr = fDuration(this.props.query.startDttm, since);
|
||||
this.setState({ clockStr });
|
||||
if (this.props.query.state !== 'running') {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { Button, Col, Modal } from 'react-bootstrap';
|
||||
import { Alert, Button, Col, Modal } from 'react-bootstrap';
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
@@ -9,17 +9,57 @@ import Select from 'react-select';
|
||||
import { Table } from 'reactable';
|
||||
import shortid from 'shortid';
|
||||
|
||||
const CHART_TYPES = [
|
||||
{ value: 'dist_bar', label: 'Distribution - Bar Chart', requiresTime: false },
|
||||
{ value: 'pie', label: 'Pie Chart', requiresTime: false },
|
||||
{ value: 'line', label: 'Time Series - Line Chart', requiresTime: true },
|
||||
{ value: 'bar', label: 'Time Series - Bar Chart', requiresTime: true },
|
||||
];
|
||||
|
||||
class VisualizeModal extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const uniqueId = shortid.generate();
|
||||
this.state = {
|
||||
chartType: 'line',
|
||||
datasourceName: shortid.generate(),
|
||||
chartType: CHART_TYPES[0],
|
||||
datasourceName: uniqueId,
|
||||
columns: {},
|
||||
hints: [],
|
||||
};
|
||||
this.validate();
|
||||
}
|
||||
validate() {
|
||||
const hints = [];
|
||||
const cols = this.mergedColumns();
|
||||
const re = /^\w+$/;
|
||||
Object.keys(cols).forEach((colName) => {
|
||||
if (!re.test(colName)) {
|
||||
hints.push(
|
||||
<div>
|
||||
"{colName}" is not right as a column name, please alias it
|
||||
(as in SELECT count(*) <strong>AS my_alias</strong>) using only
|
||||
alphanumeric characters and underscores
|
||||
</div>);
|
||||
}
|
||||
});
|
||||
if (this.state.chartType === null) {
|
||||
hints.push('Pick a chart type!');
|
||||
} else if (this.state.chartType.requiresTime) {
|
||||
let hasTime = false;
|
||||
for (const colName in cols) {
|
||||
const col = cols[colName];
|
||||
if (col.hasOwnProperty('is_date') && col.is_date) {
|
||||
hasTime = true;
|
||||
}
|
||||
}
|
||||
if (!hasTime) {
|
||||
hints.push('To use this chart type you need at least one column flagged as a date');
|
||||
}
|
||||
}
|
||||
this.setState({ hints });
|
||||
}
|
||||
changeChartType(option) {
|
||||
this.setState({ chartType: (option) ? option.value : null });
|
||||
this.setState({ chartType: option }, this.validate);
|
||||
}
|
||||
mergedColumns() {
|
||||
const columns = Object.assign({}, this.state.columns);
|
||||
@@ -34,7 +74,7 @@ class VisualizeModal extends React.Component {
|
||||
}
|
||||
visualize() {
|
||||
const vizOptions = {
|
||||
chartType: this.state.chartType,
|
||||
chartType: this.state.chartType.value,
|
||||
datasourceName: this.state.datasourceName,
|
||||
columns: this.state.columns,
|
||||
sql: this.props.query.sql,
|
||||
@@ -44,19 +84,20 @@ class VisualizeModal extends React.Component {
|
||||
}
|
||||
changeDatasourceName(event) {
|
||||
this.setState({ datasourceName: event.target.value });
|
||||
this.validate();
|
||||
}
|
||||
changeCheckbox(attr, col, event) {
|
||||
let columns = this.mergedColumns();
|
||||
const column = Object.assign({}, columns[col], { [attr]: event.target.checked });
|
||||
columns = Object.assign({}, columns, { [col]: column });
|
||||
this.setState({ columns });
|
||||
this.setState({ columns }, this.validate);
|
||||
}
|
||||
changeAggFunction(col, option) {
|
||||
let columns = this.mergedColumns();
|
||||
const val = (option) ? option.value : null;
|
||||
const column = Object.assign({}, columns[col], { agg: val });
|
||||
columns = Object.assign({}, columns, { [col]: column });
|
||||
this.setState({ columns });
|
||||
this.setState({ columns }, this.validate);
|
||||
}
|
||||
render() {
|
||||
if (!(this.props.query)) {
|
||||
@@ -94,28 +135,25 @@ class VisualizeModal extends React.Component {
|
||||
/>
|
||||
),
|
||||
}));
|
||||
const alerts = this.state.hints.map((hint) => (
|
||||
<Alert bsStyle="warning">{hint}</Alert>
|
||||
));
|
||||
const modal = (
|
||||
<div className="VisualizeModal">
|
||||
<Modal show={this.props.show} onHide={this.props.onHide}>
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title>
|
||||
Visualize <span className="alert alert-danger">under construction</span>
|
||||
</Modal.Title>
|
||||
<Modal.Title>Visualize</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
{alerts}
|
||||
<div className="row">
|
||||
<Col md={6}>
|
||||
Chart Type
|
||||
<Select
|
||||
name="select-chart-type"
|
||||
placeholder="[Chart Type]"
|
||||
options={[
|
||||
{ value: 'line', label: 'Time Series - Line Chart' },
|
||||
{ value: 'bar', label: 'Time Series - Bar Chart' },
|
||||
{ value: 'bar_dist', label: 'Distribution - Bar Chart' },
|
||||
{ value: 'pie', label: 'Pie Chart' },
|
||||
]}
|
||||
value={this.state.chartType}
|
||||
options={CHART_TYPES}
|
||||
value={(this.state.chartType) ? this.state.chartType.value : null}
|
||||
autosize={false}
|
||||
onChange={this.changeChartType.bind(this)}
|
||||
/>
|
||||
@@ -124,7 +162,7 @@ class VisualizeModal extends React.Component {
|
||||
Datasource Name
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
className="form-control input-sm"
|
||||
placeholder="datasource name"
|
||||
onChange={this.changeDatasourceName.bind(this)}
|
||||
value={this.state.datasourceName}
|
||||
@@ -140,6 +178,7 @@ class VisualizeModal extends React.Component {
|
||||
<Button
|
||||
onClick={this.visualize.bind(this)}
|
||||
bsStyle="primary"
|
||||
disabled={(this.state.hints.length > 0)}
|
||||
>
|
||||
Visualize
|
||||
</Button>
|
||||
|
||||
Reference in New Issue
Block a user