mirror of
https://github.com/apache/superset.git
synced 2026-04-20 08:34:37 +00:00
SQL Lab - A multi-tab SQL editor (#514)
* Carapal react mockup This is really just a mock up written in React to try different components. It could become scaffolding to build a prototype, or not. * Merging in Alanna's theme tweaks for SQL lab * Tweak the display of the alert message in navbar * Sketching the middleware refresh for Queries * Adjustments * Implement timer sync. * CTAS * Refactor the queries to be stored as a dict. (#994) * Download csv endpoint. (#992) * CSV download engdpoint. * Use lower case booleans. * Replcate loop with the object lookup by key. * First changes for the sync * Address comments * Fix query deletions. Update only the queries from the store. * Sync queries using tmp_id. * simplify * Fix the tests in the carapal. (#1023) * Sync queries using tmp_id. * Fix the unit tests * Bux fixes. Pass 2. * Tweakin' & linting * Adding alpha label to the SQL LAb navbar entry * Fixing the python unit tests
This commit is contained in:
committed by
GitHub
parent
f17cfcbfa2
commit
38b8db8051
41
caravel/assets/javascripts/SqlLab/components/Alerts.jsx
Normal file
41
caravel/assets/javascripts/SqlLab/components/Alerts.jsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import React from 'react';
|
||||
import { Alert } from 'react-bootstrap';
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import * as Actions from '../actions';
|
||||
|
||||
class Alerts extends React.Component {
|
||||
removeAlert(alert) {
|
||||
this.props.actions.removeAlert(alert);
|
||||
}
|
||||
render() {
|
||||
const alerts = this.props.alerts.map((alert) =>
|
||||
<Alert
|
||||
bsStyle={alert.bsStyle}
|
||||
style={{ width: '500px', textAlign: 'midddle', margin: '10px auto' }}
|
||||
>
|
||||
{alert.msg}
|
||||
<i
|
||||
className="fa fa-close pull-right"
|
||||
onClick={this.removeAlert.bind(this, alert)}
|
||||
style={{ cursor: 'pointer' }}
|
||||
/>
|
||||
</Alert>
|
||||
);
|
||||
return (
|
||||
<div>{alerts}</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Alerts.propTypes = {
|
||||
alerts: React.PropTypes.array,
|
||||
actions: React.PropTypes.object,
|
||||
};
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators(Actions, dispatch),
|
||||
};
|
||||
}
|
||||
export default connect(null, mapDispatchToProps)(Alerts);
|
||||
@@ -0,0 +1,48 @@
|
||||
import React from 'react';
|
||||
import { Button, OverlayTrigger, Tooltip } from 'react-bootstrap';
|
||||
|
||||
const ButtonWithTooltip = (props) => {
|
||||
let tooltip = (
|
||||
<Tooltip id="tooltip">
|
||||
{props.tooltip}
|
||||
</Tooltip>
|
||||
);
|
||||
return (
|
||||
<OverlayTrigger
|
||||
overlay={tooltip}
|
||||
delayShow={300}
|
||||
placement={props.placement}
|
||||
delayHide={150}
|
||||
>
|
||||
<Button
|
||||
onClick={props.onClick}
|
||||
bsStyle={props.bsStyle}
|
||||
bsSize={props.bsSize}
|
||||
disabled={props.disabled}
|
||||
className={props.className}
|
||||
>
|
||||
{props.children}
|
||||
</Button>
|
||||
</OverlayTrigger>
|
||||
);
|
||||
};
|
||||
|
||||
ButtonWithTooltip.defaultProps = {
|
||||
onClick: () => {},
|
||||
disabled: false,
|
||||
placement: 'top',
|
||||
bsStyle: 'default',
|
||||
};
|
||||
|
||||
ButtonWithTooltip.propTypes = {
|
||||
bsSize: React.PropTypes.string,
|
||||
bsStyle: React.PropTypes.string,
|
||||
children: React.PropTypes.element,
|
||||
className: React.PropTypes.string,
|
||||
disabled: React.PropTypes.bool,
|
||||
onClick: React.PropTypes.func,
|
||||
placement: React.PropTypes.string,
|
||||
tooltip: React.PropTypes.string,
|
||||
};
|
||||
|
||||
export default ButtonWithTooltip;
|
||||
62
caravel/assets/javascripts/SqlLab/components/LeftPane.jsx
Normal file
62
caravel/assets/javascripts/SqlLab/components/LeftPane.jsx
Normal file
@@ -0,0 +1,62 @@
|
||||
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';
|
||||
|
||||
import 'react-select/dist/react-select.css';
|
||||
|
||||
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);
|
||||
54
caravel/assets/javascripts/SqlLab/components/Link.jsx
Normal file
54
caravel/assets/javascripts/SqlLab/components/Link.jsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import React from 'react';
|
||||
import { OverlayTrigger, Tooltip } from 'react-bootstrap';
|
||||
|
||||
|
||||
class Link extends React.Component {
|
||||
render() {
|
||||
let tooltip = (
|
||||
<Tooltip id="tooltip">
|
||||
{this.props.tooltip}
|
||||
</Tooltip>
|
||||
);
|
||||
const link = (
|
||||
<a
|
||||
href={this.props.href}
|
||||
onClick={this.props.onClick}
|
||||
style={this.props.style}
|
||||
className={'Link ' + this.props.className}
|
||||
>
|
||||
{this.props.children}
|
||||
</a>
|
||||
);
|
||||
if (this.props.tooltip) {
|
||||
return (
|
||||
<OverlayTrigger
|
||||
overlay={tooltip}
|
||||
placement={this.props.placement}
|
||||
delayShow={300}
|
||||
delayHide={150}
|
||||
>
|
||||
{link}
|
||||
</OverlayTrigger>
|
||||
);
|
||||
}
|
||||
return link;
|
||||
}
|
||||
}
|
||||
Link.propTypes = {
|
||||
children: React.PropTypes.object,
|
||||
className: React.PropTypes.string,
|
||||
href: React.PropTypes.string,
|
||||
onClick: React.PropTypes.func,
|
||||
placement: React.PropTypes.string,
|
||||
style: React.PropTypes.object,
|
||||
tooltip: React.PropTypes.string,
|
||||
};
|
||||
Link.defaultProps = {
|
||||
disabled: false,
|
||||
href: '#',
|
||||
tooltip: null,
|
||||
placement: 'top',
|
||||
onClick: () => {},
|
||||
};
|
||||
|
||||
export default Link;
|
||||
@@ -0,0 +1,55 @@
|
||||
import React from 'react';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { connect } from 'react-redux';
|
||||
import * as Actions from '../actions';
|
||||
|
||||
const $ = require('jquery');
|
||||
|
||||
|
||||
class QueryAutoRefresh extends React.Component {
|
||||
componentWillMount() {
|
||||
this.startTimer();
|
||||
}
|
||||
componentWillUnmount() {
|
||||
this.stopTimer();
|
||||
}
|
||||
startTimer() {
|
||||
if (!(this.timer)) {
|
||||
this.timer = setInterval(this.stopwatch.bind(this), 5000);
|
||||
}
|
||||
}
|
||||
stopTimer() {
|
||||
clearInterval(this.timer);
|
||||
this.timer = null;
|
||||
}
|
||||
stopwatch() {
|
||||
const url = '/caravel/queries/0';
|
||||
// No updates in case of failure.
|
||||
$.getJSON(url, (data, status) => {
|
||||
if (status === 'success') {
|
||||
this.props.actions.refreshQueries(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
render() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
QueryAutoRefresh.propTypes = {
|
||||
actions: React.PropTypes.object,
|
||||
};
|
||||
QueryAutoRefresh.defaultProps = {
|
||||
// queries: null,
|
||||
};
|
||||
|
||||
function mapStateToProps() {
|
||||
return {};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators(Actions, dispatch),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(QueryAutoRefresh);
|
||||
@@ -0,0 +1,58 @@
|
||||
import React from 'react';
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import * as Actions from '../actions';
|
||||
|
||||
import QueryTable from './QueryTable';
|
||||
import { Alert } from 'react-bootstrap';
|
||||
|
||||
const QueryHistory = (props) => {
|
||||
const activeQeId = props.tabHistory[props.tabHistory.length - 1];
|
||||
const queriesArray = [];
|
||||
for (const id in props.queries) {
|
||||
if (props.queries[id].sqlEditorId === activeQeId) {
|
||||
queriesArray.push(props.queries[id]);
|
||||
}
|
||||
}
|
||||
if (queriesArray.length > 0) {
|
||||
return (
|
||||
<QueryTable
|
||||
columns={[
|
||||
'state', 'started', 'duration', 'progress',
|
||||
'rows', 'sql', 'output', 'actions',
|
||||
]}
|
||||
queries={queriesArray}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Alert bsStyle="info">
|
||||
No query history yet...
|
||||
</Alert>
|
||||
);
|
||||
};
|
||||
|
||||
QueryHistory.defaultProps = {
|
||||
queries: {},
|
||||
};
|
||||
|
||||
QueryHistory.propTypes = {
|
||||
queries: React.PropTypes.object,
|
||||
tabHistory: React.PropTypes.array,
|
||||
actions: React.PropTypes.object,
|
||||
};
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
queries: state.queries,
|
||||
tabHistory: state.tabHistory,
|
||||
};
|
||||
}
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators(Actions, dispatch),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(QueryHistory);
|
||||
65
caravel/assets/javascripts/SqlLab/components/QueryLink.jsx
Normal file
65
caravel/assets/javascripts/SqlLab/components/QueryLink.jsx
Normal file
@@ -0,0 +1,65 @@
|
||||
import React from 'react';
|
||||
import Link from './Link';
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import * as Actions from '../actions';
|
||||
import shortid from 'shortid';
|
||||
|
||||
// CSS
|
||||
import 'react-select/dist/react-select.css';
|
||||
|
||||
class QueryLink extends React.Component {
|
||||
popTab() {
|
||||
const qe = {
|
||||
id: shortid.generate(),
|
||||
title: this.props.query.title,
|
||||
dbId: this.props.query.dbId,
|
||||
autorun: false,
|
||||
sql: this.props.query.sql,
|
||||
};
|
||||
this.props.actions.addQueryEditor(qe);
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<div className="clearfix">
|
||||
<div className="pull-left">
|
||||
<a
|
||||
href="#"
|
||||
tooltip="Pop this query in a new tab"
|
||||
onClick={this.popTab.bind(this)}
|
||||
>
|
||||
{this.props.query.title}
|
||||
</a>
|
||||
</div>
|
||||
<div className="pull-right">
|
||||
<Link
|
||||
onClick={this.props.actions.removeWorkspaceQuery.bind(this, this.props.query)}
|
||||
tooltip="Remove query from workspace"
|
||||
href="#"
|
||||
>
|
||||
×
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
QueryLink.propTypes = {
|
||||
query: React.PropTypes.object,
|
||||
actions: React.PropTypes.object,
|
||||
};
|
||||
|
||||
QueryLink.defaultProps = {
|
||||
};
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators(Actions, dispatch),
|
||||
};
|
||||
}
|
||||
export default connect(null, mapDispatchToProps)(QueryLink);
|
||||
|
||||
73
caravel/assets/javascripts/SqlLab/components/QuerySearch.jsx
Normal file
73
caravel/assets/javascripts/SqlLab/components/QuerySearch.jsx
Normal file
@@ -0,0 +1,73 @@
|
||||
import React from 'react';
|
||||
import Select from 'react-select';
|
||||
import { Button } from 'react-bootstrap';
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import * as Actions from '../actions';
|
||||
import QueryTable from './QueryTable';
|
||||
|
||||
class QuerySearch extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
queryText: '',
|
||||
};
|
||||
}
|
||||
changeQueryText(value) {
|
||||
this.setState({ queryText: value });
|
||||
}
|
||||
render() {
|
||||
const queries = this.props.queries;
|
||||
return (
|
||||
<div>
|
||||
<div className="pane-cell pane-west m-t-5">
|
||||
<div className="panel panel-default Workspace">
|
||||
<div className="panel-heading">
|
||||
<h6>
|
||||
<i className="fa fa-search" /> Search Queries
|
||||
</h6>
|
||||
</div>
|
||||
<div className="panel-body">
|
||||
<input type="text" className="form-control" placeholder="Query Text" />
|
||||
<Select
|
||||
name="select-user"
|
||||
placeholder="[User]"
|
||||
options={['maxime_beauchemin', 'someone else']}
|
||||
value={'maxime_beauchemin'}
|
||||
className="m-t-10"
|
||||
autosize={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="pane-cell">
|
||||
<QueryTable
|
||||
columns={['state', 'started', 'duration', 'rows', 'sql', 'actions']}
|
||||
queries={queries}
|
||||
/>
|
||||
</div>
|
||||
<Button>Search!</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
QuerySearch.propTypes = {
|
||||
queries: React.PropTypes.array,
|
||||
};
|
||||
QuerySearch.defaultProps = {
|
||||
queries: [],
|
||||
};
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
queries: state.queries,
|
||||
};
|
||||
}
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators(Actions, dispatch),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(QuerySearch);
|
||||
142
caravel/assets/javascripts/SqlLab/components/QueryTable.jsx
Normal file
142
caravel/assets/javascripts/SqlLab/components/QueryTable.jsx
Normal file
@@ -0,0 +1,142 @@
|
||||
import React from 'react';
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import * as Actions from '../actions';
|
||||
|
||||
import moment from 'moment';
|
||||
import { Table } from 'reactable';
|
||||
import { ProgressBar } from 'react-bootstrap';
|
||||
|
||||
import SyntaxHighlighter from 'react-syntax-highlighter';
|
||||
import { github } from 'react-syntax-highlighter/dist/styles';
|
||||
|
||||
import Link from './Link';
|
||||
import VisualizeModal from './VisualizeModal';
|
||||
import { STATE_BSSTYLE_MAP } from '../common';
|
||||
import { fDuration } from '../../modules/dates';
|
||||
|
||||
|
||||
class QueryTable extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
showVisualizeModal: false,
|
||||
activeQuery: null,
|
||||
};
|
||||
}
|
||||
hideVisualizeModal() {
|
||||
this.setState({ showVisualizeModal: false });
|
||||
}
|
||||
showVisualizeModal(query) {
|
||||
this.setState({ showVisualizeModal: true });
|
||||
this.setState({ activeQuery: query });
|
||||
}
|
||||
restoreSql(query) {
|
||||
this.props.actions.queryEditorSetSql({ id: query.sqlEditorId }, query.sql);
|
||||
}
|
||||
notImplemented() {
|
||||
alert('Not implemented yet!');
|
||||
}
|
||||
render() {
|
||||
const data = this.props.queries.map((query) => {
|
||||
const q = Object.assign({}, query);
|
||||
if (q.endDttm) {
|
||||
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;
|
||||
q.sql = (
|
||||
<SyntaxHighlighter language="sql" style={github}>
|
||||
{source || ''}
|
||||
</SyntaxHighlighter>
|
||||
);
|
||||
q.output = q.tempTable;
|
||||
q.progress = (
|
||||
<ProgressBar
|
||||
style={{ width: '75px' }}
|
||||
striped
|
||||
now={q.progress}
|
||||
label={`${q.progress}%`}
|
||||
/>
|
||||
);
|
||||
let errorTooltip;
|
||||
if (q.errorMessage) {
|
||||
errorTooltip = (
|
||||
<Link tooltip={q.errorMessage}>
|
||||
<i className="fa fa-exclamation-circle text-danger" />
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
q.state = (
|
||||
<div>
|
||||
<span className={'m-r-3 label label-' + STATE_BSSTYLE_MAP[q.state]}>
|
||||
{q.state}
|
||||
</span>
|
||||
{errorTooltip}
|
||||
</div>
|
||||
);
|
||||
q.actions = (
|
||||
<div style={{ width: '75px' }}>
|
||||
<Link
|
||||
className="fa fa-line-chart m-r-3"
|
||||
tooltip="Visualize the data out of this query"
|
||||
onClick={this.showVisualizeModal.bind(this, query)}
|
||||
/>
|
||||
<Link
|
||||
className="fa fa-pencil m-r-3"
|
||||
onClick={this.restoreSql.bind(this, query)}
|
||||
tooltip="Overwrite text in editor with a query on this table"
|
||||
placement="top"
|
||||
/>
|
||||
<Link
|
||||
className="fa fa-plus-circle m-r-3"
|
||||
onClick={self.notImplemented}
|
||||
tooltip="Run query in a new tab"
|
||||
placement="top"
|
||||
/>
|
||||
<Link
|
||||
className="fa fa-trash m-r-3"
|
||||
tooltip="Remove query from log"
|
||||
onClick={this.props.actions.removeQuery.bind(this, query)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
return q;
|
||||
}).reverse();
|
||||
return (
|
||||
<div>
|
||||
<VisualizeModal
|
||||
show={this.state.showVisualizeModal}
|
||||
query={this.state.activeQuery}
|
||||
onHide={this.hideVisualizeModal.bind(this)}
|
||||
/>
|
||||
<Table
|
||||
columns={this.props.columns}
|
||||
className="table table-condensed"
|
||||
data={data}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
QueryTable.propTypes = {
|
||||
columns: React.PropTypes.array,
|
||||
actions: React.PropTypes.object,
|
||||
queries: React.PropTypes.array,
|
||||
};
|
||||
QueryTable.defaultProps = {
|
||||
columns: ['state', 'started', 'duration', 'progress', 'rows', 'sql', 'actions'],
|
||||
queries: [],
|
||||
};
|
||||
|
||||
function mapStateToProps() {
|
||||
return {};
|
||||
}
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators(Actions, dispatch),
|
||||
};
|
||||
}
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(QueryTable);
|
||||
101
caravel/assets/javascripts/SqlLab/components/ResultSet.jsx
Normal file
101
caravel/assets/javascripts/SqlLab/components/ResultSet.jsx
Normal file
@@ -0,0 +1,101 @@
|
||||
import React from 'react';
|
||||
import { Alert, Button, ButtonGroup } from 'react-bootstrap';
|
||||
import { Table } from 'reactable';
|
||||
|
||||
import VisualizeModal from './VisualizeModal';
|
||||
|
||||
|
||||
class ResultSet extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
searchText: '',
|
||||
showModal: false,
|
||||
};
|
||||
}
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
return (
|
||||
this.state.searchText !== nextState.searchText ||
|
||||
this.state.showModal !== nextState.showModal
|
||||
);
|
||||
}
|
||||
changeSearch(event) {
|
||||
this.setState({ searchText: event.target.value });
|
||||
}
|
||||
showModal() {
|
||||
this.setState({ showModal: true });
|
||||
}
|
||||
hideModal() {
|
||||
this.setState({ showModal: false });
|
||||
}
|
||||
render() {
|
||||
const results = this.props.query.results;
|
||||
let controls = <div className="noControls" />;
|
||||
if (this.props.showControls) {
|
||||
controls = (
|
||||
<div className="ResultSetControls">
|
||||
<div className="clearfix">
|
||||
<div className="pull-left">
|
||||
<ButtonGroup>
|
||||
<Button
|
||||
bsSize="small"
|
||||
onClick={this.showModal.bind(this)}
|
||||
>
|
||||
<i className="fa fa-line-chart m-l-1" /> Visualize
|
||||
</Button>
|
||||
<Button bsSize="small">
|
||||
<i className="fa fa-file-text-o" /> .CSV
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
<div className="pull-right">
|
||||
<input
|
||||
type="text"
|
||||
onChange={this.changeSearch.bind(this)}
|
||||
className="form-control input-sm"
|
||||
placeholder="Search Results"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (results && results.data.length > 0) {
|
||||
return (
|
||||
<div>
|
||||
<VisualizeModal
|
||||
show={this.state.showModal}
|
||||
query={this.props.query}
|
||||
onHide={this.hideModal.bind(this)}
|
||||
/>
|
||||
{controls}
|
||||
<div className="ResultSet">
|
||||
<Table
|
||||
data={results.data}
|
||||
columns={results.columns}
|
||||
sortable
|
||||
className="table table-condensed table-bordered"
|
||||
filterBy={this.state.searchText}
|
||||
filterable={results.columns}
|
||||
hideFilterInput
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (<Alert bsStyle="warning">The query returned no data</Alert>);
|
||||
}
|
||||
}
|
||||
ResultSet.propTypes = {
|
||||
query: React.PropTypes.object,
|
||||
showControls: React.PropTypes.boolean,
|
||||
search: React.PropTypes.boolean,
|
||||
searchText: React.PropTypes.string,
|
||||
};
|
||||
ResultSet.defaultProps = {
|
||||
showControls: true,
|
||||
search: true,
|
||||
searchText: '',
|
||||
};
|
||||
|
||||
export default ResultSet;
|
||||
46
caravel/assets/javascripts/SqlLab/components/SouthPane.jsx
Normal file
46
caravel/assets/javascripts/SqlLab/components/SouthPane.jsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import { Alert, Panel, 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>;
|
||||
}
|
||||
return (
|
||||
<Tabs bsStyle="tabs">
|
||||
<Tab title="Results" eventKey={1}>
|
||||
<Panel>
|
||||
<div style={{ overflow: 'auto' }}>
|
||||
{results}
|
||||
</div>
|
||||
</Panel>
|
||||
</Tab>
|
||||
<Tab title="Query History" eventKey={2}>
|
||||
<Panel>
|
||||
<QueryHistory />
|
||||
</Panel>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
);
|
||||
};
|
||||
|
||||
SouthPane.propTypes = {
|
||||
latestQuery: React.PropTypes.object,
|
||||
};
|
||||
|
||||
SouthPane.defaultProps = {
|
||||
};
|
||||
|
||||
export default SouthPane;
|
||||
292
caravel/assets/javascripts/SqlLab/components/SqlEditor.jsx
Normal file
292
caravel/assets/javascripts/SqlLab/components/SqlEditor.jsx
Normal file
@@ -0,0 +1,292 @@
|
||||
const $ = require('jquery');
|
||||
import { now } from '../../modules/dates';
|
||||
import React from 'react';
|
||||
import {
|
||||
Button,
|
||||
ButtonGroup,
|
||||
FormGroup,
|
||||
InputGroup,
|
||||
Form,
|
||||
FormControl,
|
||||
DropdownButton,
|
||||
Label,
|
||||
MenuItem,
|
||||
OverlayTrigger,
|
||||
Tooltip,
|
||||
} from 'react-bootstrap';
|
||||
|
||||
import AceEditor from 'react-ace';
|
||||
import 'brace/mode/sql';
|
||||
import 'brace/theme/github';
|
||||
import 'brace/ext/language_tools';
|
||||
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { connect } from 'react-redux';
|
||||
import * as Actions from '../actions';
|
||||
|
||||
import shortid from 'shortid';
|
||||
import ButtonWithTooltip from './ButtonWithTooltip';
|
||||
import SouthPane from './SouthPane';
|
||||
import Timer from './Timer';
|
||||
|
||||
import SqlEditorTopToolbar from './SqlEditorTopToolbar';
|
||||
|
||||
// CSS
|
||||
import 'react-select/dist/react-select.css';
|
||||
|
||||
class SqlEditor extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
autorun: props.queryEditor.autorun,
|
||||
sql: props.queryEditor.sql,
|
||||
ctas: '',
|
||||
};
|
||||
}
|
||||
componentDidMount() {
|
||||
this.onMount();
|
||||
}
|
||||
onMount() {
|
||||
if (this.state.autorun) {
|
||||
this.setState({ autorun: false });
|
||||
this.props.actions.queryEditorSetAutorun(this.props.queryEditor, false);
|
||||
this.startQuery();
|
||||
}
|
||||
}
|
||||
runQuery() {
|
||||
this.startQuery();
|
||||
}
|
||||
startQuery(runAsync = false, ctas = false) {
|
||||
const that = this;
|
||||
const query = {
|
||||
dbId: this.props.queryEditor.dbId,
|
||||
id: shortid.generate(),
|
||||
progress: 0,
|
||||
sql: this.props.queryEditor.sql,
|
||||
sqlEditorId: this.props.queryEditor.id,
|
||||
startDttm: now(),
|
||||
state: 'running',
|
||||
tab: this.props.queryEditor.title,
|
||||
};
|
||||
if (runAsync) {
|
||||
query.state = 'pending';
|
||||
}
|
||||
|
||||
// Execute the Query
|
||||
that.props.actions.startQuery(query);
|
||||
|
||||
const sqlJsonUrl = '/caravel/sql_json/';
|
||||
const sqlJsonRequest = {
|
||||
async: runAsync,
|
||||
client_id: query.id,
|
||||
database_id: this.props.queryEditor.dbId,
|
||||
json: true,
|
||||
schema: this.props.queryEditor.schema,
|
||||
select_as_cta: ctas,
|
||||
sql: this.props.queryEditor.sql,
|
||||
sql_editor_id: this.props.queryEditor.id,
|
||||
tab: this.props.queryEditor.title,
|
||||
tmp_table_name: this.state.ctas,
|
||||
};
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
dataType: 'json',
|
||||
url: sqlJsonUrl,
|
||||
data: sqlJsonRequest,
|
||||
success(results) {
|
||||
if (!runAsync) {
|
||||
that.props.actions.querySuccess(query, results);
|
||||
}
|
||||
},
|
||||
error(err) {
|
||||
let msg;
|
||||
try {
|
||||
msg = err.responseJSON.error;
|
||||
} catch (e) {
|
||||
msg = (err.responseText) ? err.responseText : e;
|
||||
}
|
||||
if (typeof(msg) !== 'string') {
|
||||
msg = JSON.stringify(msg);
|
||||
}
|
||||
that.props.actions.queryFailed(query, msg);
|
||||
},
|
||||
});
|
||||
}
|
||||
stopQuery() {
|
||||
this.props.actions.stopQuery(this.props.latestQuery);
|
||||
}
|
||||
createTableAs() {
|
||||
this.startQuery(true, true);
|
||||
}
|
||||
textChange(text) {
|
||||
this.setState({ sql: text });
|
||||
this.props.actions.queryEditorSetSql(this.props.queryEditor, text);
|
||||
}
|
||||
addWorkspaceQuery() {
|
||||
this.props.actions.addWorkspaceQuery({
|
||||
id: shortid.generate(),
|
||||
sql: this.state.sql,
|
||||
dbId: this.props.queryEditor.dbId,
|
||||
schema: this.props.queryEditor.schema,
|
||||
title: this.props.queryEditor.title,
|
||||
});
|
||||
}
|
||||
ctasChange() {}
|
||||
visualize() {}
|
||||
ctasChanged(event) {
|
||||
this.setState({ ctas: event.target.value });
|
||||
}
|
||||
render() {
|
||||
let runButtons = (
|
||||
<ButtonGroup bsSize="small" className="inline m-r-5 pull-left">
|
||||
<Button
|
||||
bsSize="small"
|
||||
bsStyle="primary"
|
||||
style={{ width: '100px' }}
|
||||
onClick={this.runQuery.bind(this)}
|
||||
disabled={!(this.props.queryEditor.dbId)}
|
||||
>
|
||||
<i className="fa fa-table" /> Run Query
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
);
|
||||
if (this.props.latestQuery && this.props.latestQuery.state === 'running') {
|
||||
runButtons = (
|
||||
<ButtonGroup bsSize="small" className="inline m-r-5 pull-left">
|
||||
<Button
|
||||
bsStyle="primary"
|
||||
bsSize="small"
|
||||
style={{ width: '100px' }}
|
||||
onClick={this.stopQuery.bind(this)}
|
||||
>
|
||||
<a className="fa fa-stop" /> Stop
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
);
|
||||
}
|
||||
const rightButtons = (
|
||||
<ButtonGroup className="inlineblock">
|
||||
<ButtonWithTooltip
|
||||
tooltip="Save this query in your workspace"
|
||||
placement="left"
|
||||
bsSize="small"
|
||||
onClick={this.addWorkspaceQuery.bind(this)}
|
||||
>
|
||||
<i className="fa fa-save" />
|
||||
</ButtonWithTooltip>
|
||||
<DropdownButton
|
||||
id="ddbtn-export"
|
||||
pullRight
|
||||
bsSize="small"
|
||||
title={<i className="fa fa-file-o" />}
|
||||
>
|
||||
<MenuItem
|
||||
onClick={this.notImplemented}
|
||||
>
|
||||
<i className="fa fa-file-text-o" /> export to .csv
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={this.notImplemented}
|
||||
>
|
||||
<i className="fa fa-file-code-o" /> export to .json
|
||||
</MenuItem>
|
||||
</DropdownButton>
|
||||
</ButtonGroup>
|
||||
);
|
||||
let limitWarning = null;
|
||||
const rowLimit = 1000;
|
||||
if (this.props.latestQuery && this.props.latestQuery.rows === rowLimit) {
|
||||
const tooltip = (
|
||||
<Tooltip id="tooltip">
|
||||
It appears that the number of rows in the query results displayed
|
||||
was limited on the server side to the {rowLimit} limit.
|
||||
</Tooltip>
|
||||
);
|
||||
limitWarning = (
|
||||
<OverlayTrigger placement="left" overlay={tooltip}>
|
||||
<Label bsStyle="warning" className="m-r-5">LIMIT</Label>
|
||||
</OverlayTrigger>
|
||||
);
|
||||
}
|
||||
const editorBottomBar = (
|
||||
<div className="sql-toolbar clearfix">
|
||||
<div className="pull-left">
|
||||
<Form inline>
|
||||
{runButtons}
|
||||
<FormGroup>
|
||||
<InputGroup>
|
||||
<FormControl
|
||||
type="text"
|
||||
bsSize="small"
|
||||
className="input-sm"
|
||||
placeholder="new table name"
|
||||
onChange={this.ctasChanged.bind(this)}
|
||||
/>
|
||||
<InputGroup.Button>
|
||||
<Button
|
||||
bsSize="small"
|
||||
disabled={this.state.ctas.length === 0}
|
||||
onClick={this.createTableAs.bind(this)}
|
||||
>
|
||||
<i className="fa fa-table" /> CTAS
|
||||
</Button>
|
||||
</InputGroup.Button>
|
||||
</InputGroup>
|
||||
</FormGroup>
|
||||
</Form>
|
||||
</div>
|
||||
<div className="pull-right">
|
||||
{limitWarning}
|
||||
<Timer query={this.props.latestQuery} />
|
||||
{rightButtons}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div className="SqlEditor">
|
||||
<div>
|
||||
<div>
|
||||
<SqlEditorTopToolbar queryEditor={this.props.queryEditor} />
|
||||
<AceEditor
|
||||
mode="sql"
|
||||
name={this.props.queryEditor.id}
|
||||
theme="github"
|
||||
minLines={5}
|
||||
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} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SqlEditor.propTypes = {
|
||||
queryEditor: React.PropTypes.object,
|
||||
actions: React.PropTypes.object,
|
||||
latestQuery: React.PropTypes.object,
|
||||
};
|
||||
|
||||
SqlEditor.defaultProps = {
|
||||
};
|
||||
|
||||
function mapStateToProps() {
|
||||
return {};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators(Actions, dispatch),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(SqlEditor);
|
||||
@@ -0,0 +1,271 @@
|
||||
const $ = window.$ = require('jquery');
|
||||
import React from 'react';
|
||||
import { Label, OverlayTrigger, Popover } from 'react-bootstrap';
|
||||
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { connect } from 'react-redux';
|
||||
import * as Actions from '../actions';
|
||||
import shortid from 'shortid';
|
||||
import Select from 'react-select';
|
||||
import Link from './Link';
|
||||
|
||||
// CSS
|
||||
import 'react-select/dist/react-select.css';
|
||||
|
||||
class SqlEditorTopToolbar extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
databaseLoading: false,
|
||||
databaseOptions: [],
|
||||
schemaLoading: false,
|
||||
schemaOptions: [],
|
||||
tableLoading: false,
|
||||
tableOptions: [],
|
||||
};
|
||||
}
|
||||
componentWillMount() {
|
||||
this.fetchDatabaseOptions();
|
||||
this.fetchSchemas();
|
||||
this.fetchTables();
|
||||
}
|
||||
getSql(table) {
|
||||
let cols = '';
|
||||
table.columns.forEach(function (col, i) {
|
||||
cols += col.name;
|
||||
if (i < table.columns.length - 1) {
|
||||
cols += ', ';
|
||||
}
|
||||
});
|
||||
return `SELECT ${cols}\nFROM ${table.name}`;
|
||||
}
|
||||
selectStar(table) {
|
||||
this.props.actions.queryEditorSetSql(this.props.queryEditor, this.getSql(table));
|
||||
}
|
||||
popTab(table) {
|
||||
const qe = {
|
||||
id: shortid.generate(),
|
||||
title: table.name,
|
||||
dbId: table.dbId,
|
||||
schema: table.schema,
|
||||
autorun: true,
|
||||
sql: this.getSql(table),
|
||||
};
|
||||
this.props.actions.addQueryEditor(qe);
|
||||
}
|
||||
fetchTables(dbId, schema) {
|
||||
const actualDbId = dbId || this.props.queryEditor.dbId;
|
||||
if (actualDbId) {
|
||||
const actualSchema = schema || this.props.queryEditor.schema;
|
||||
this.setState({ tableLoading: true });
|
||||
this.setState({ tableOptions: [] });
|
||||
const url = `/caravel/tables/${actualDbId}/${actualSchema}`;
|
||||
$.get(url, (data) => {
|
||||
let tableOptions = data.tables.map((s) => ({ value: s, label: s }));
|
||||
const views = data.views.map((s) => ({ value: s, label: '[view] ' + s }));
|
||||
tableOptions = [...tableOptions, ...views];
|
||||
this.setState({ tableOptions });
|
||||
this.setState({ tableLoading: false });
|
||||
});
|
||||
}
|
||||
}
|
||||
changeSchema(schemaOpt) {
|
||||
const schema = (schemaOpt) ? schemaOpt.value : null;
|
||||
this.props.actions.queryEditorSetSchema(this.props.queryEditor, schema);
|
||||
this.fetchTables(this.props.queryEditor.dbId, schema);
|
||||
}
|
||||
fetchSchemas(dbId) {
|
||||
const actualDbId = dbId || this.props.queryEditor.dbId;
|
||||
if (actualDbId) {
|
||||
this.setState({ schemaLoading: true });
|
||||
const url = `/databasetablesasync/api/read?_flt_0_id=${actualDbId}`;
|
||||
$.get(url, (data) => {
|
||||
const schemas = data.result[0].all_schema_names;
|
||||
const schemaOptions = schemas.map((s) => ({ value: s, label: s }));
|
||||
this.setState({ schemaOptions });
|
||||
this.setState({ schemaLoading: false });
|
||||
});
|
||||
}
|
||||
}
|
||||
changeDb(db) {
|
||||
const val = (db) ? db.value : null;
|
||||
this.setState({ schemaOptions: [] });
|
||||
this.props.actions.queryEditorSetDb(this.props.queryEditor, val);
|
||||
if (!(db)) {
|
||||
this.setState({ tableOptions: [] });
|
||||
return;
|
||||
}
|
||||
this.fetchTables(val, this.props.queryEditor.schema);
|
||||
this.fetchSchemas(val);
|
||||
}
|
||||
fetchDatabaseOptions() {
|
||||
this.setState({ databaseLoading: true });
|
||||
const url = '/databaseasync/api/read';
|
||||
$.get(url, (data) => {
|
||||
const options = data.result.map((db) => ({ value: db.id, label: db.database_name }));
|
||||
this.setState({ databaseOptions: options });
|
||||
this.setState({ databaseLoading: false });
|
||||
});
|
||||
}
|
||||
closePopover(ref) {
|
||||
this.refs[ref].hide();
|
||||
}
|
||||
changeTable(tableOpt) {
|
||||
const tableName = tableOpt.value;
|
||||
const qe = this.props.queryEditor;
|
||||
const url = `/caravel/table/${qe.dbId}/${tableName}/${qe.schema}/`;
|
||||
$.get(url, (data) => {
|
||||
this.props.actions.addTable({
|
||||
id: shortid.generate(),
|
||||
dbId: this.props.queryEditor.dbId,
|
||||
queryEditorId: this.props.queryEditor.id,
|
||||
name: data.name,
|
||||
schema: qe.schema,
|
||||
columns: data.columns,
|
||||
expanded: true,
|
||||
showPopup: true,
|
||||
});
|
||||
})
|
||||
.fail(() => {
|
||||
this.props.actions.addAlert({
|
||||
msg: 'Error occurred while fetching metadata',
|
||||
bsStyle: 'danger',
|
||||
});
|
||||
});
|
||||
}
|
||||
render() {
|
||||
const tables = this.props.tables.filter((t) => (t.queryEditorId === this.props.queryEditor.id));
|
||||
const tablesEls = tables.map((table) => {
|
||||
let cols = [];
|
||||
if (table.columns) {
|
||||
cols = table.columns.map((col) => (
|
||||
<div className="clearfix">
|
||||
<div className="pull-left m-r-10">{col.name}</div>
|
||||
<div className="pull-right text-muted"> {col.type}</div>
|
||||
</div>
|
||||
));
|
||||
}
|
||||
const popoverId = 'tblPopover_' + table.name;
|
||||
const popoverTop = (
|
||||
<div className="clearfix">
|
||||
<div className="pull-left">
|
||||
<Link
|
||||
className="fa fa-pencil"
|
||||
onClick={this.selectStar.bind(this, table)}
|
||||
tooltip="Overwrite text in editor with a query on this table"
|
||||
placement="left"
|
||||
href="#"
|
||||
/>
|
||||
<Link
|
||||
className="fa fa-plus-circle"
|
||||
onClick={this.popTab.bind(this, table)}
|
||||
tooltip="Run query in a new tab"
|
||||
placement="left"
|
||||
href="#"
|
||||
/>
|
||||
</div>
|
||||
<div className="pull-right">
|
||||
<Link
|
||||
className="fa fa-close"
|
||||
onClick={this.closePopover.bind(this, popoverId)}
|
||||
href="#"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
const popover = (
|
||||
<Popover
|
||||
id={popoverId}
|
||||
className="tablePopover"
|
||||
title={popoverTop}
|
||||
>
|
||||
{cols}
|
||||
</Popover>
|
||||
);
|
||||
return (
|
||||
<Label className="m-r-5 table-label" style={{ fontSize: '100%' }}>
|
||||
<OverlayTrigger
|
||||
trigger="click"
|
||||
placement="bottom"
|
||||
overlay={popover}
|
||||
ref={popoverId}
|
||||
>
|
||||
<span className="m-r-5" style={{ cursor: 'pointer' }}>
|
||||
{table.name}
|
||||
</span>
|
||||
</OverlayTrigger>
|
||||
<i
|
||||
className="fa fa-close"
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={this.props.actions.removeTable.bind(this, table)}
|
||||
/>
|
||||
</Label>
|
||||
);
|
||||
});
|
||||
return (
|
||||
<div className="clearfix sql-toolbar">
|
||||
<div className="pull-left m-r-5">
|
||||
<Select
|
||||
name="select-db"
|
||||
placeholder="[Database]"
|
||||
options={this.state.databaseOptions}
|
||||
value={this.props.queryEditor.dbId}
|
||||
isLoading={this.state.databaseLoading}
|
||||
autosize={false}
|
||||
onChange={this.changeDb.bind(this)}
|
||||
/>
|
||||
</div>
|
||||
<div className="pull-left m-r-5">
|
||||
<Select
|
||||
name="select-schema"
|
||||
placeholder="[Schema]"
|
||||
options={this.state.schemaOptions}
|
||||
value={this.props.queryEditor.schema}
|
||||
isLoading={this.state.schemaLoading}
|
||||
autosize={false}
|
||||
onChange={this.changeSchema.bind(this)}
|
||||
/>
|
||||
</div>
|
||||
<div className="pull-left m-r-5">
|
||||
<Select
|
||||
name="select-table"
|
||||
ref="selectTable"
|
||||
isLoading={this.state.tableLoading}
|
||||
placeholder="Add a table"
|
||||
autosize={false}
|
||||
value={this.state.tableName}
|
||||
onChange={this.changeTable.bind(this)}
|
||||
options={this.state.tableOptions}
|
||||
/>
|
||||
</div>
|
||||
<div className="pull-left m-r-5">
|
||||
{tablesEls}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SqlEditorTopToolbar.propTypes = {
|
||||
queryEditor: React.PropTypes.object,
|
||||
tables: React.PropTypes.array,
|
||||
actions: React.PropTypes.object,
|
||||
};
|
||||
|
||||
SqlEditorTopToolbar.defaultProps = {
|
||||
tables: [],
|
||||
};
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
tables: state.tables,
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators(Actions, dispatch),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(SqlEditorTopToolbar);
|
||||
@@ -0,0 +1,120 @@
|
||||
import React from 'react';
|
||||
import { DropdownButton, MenuItem, Tab, Tabs } from 'react-bootstrap';
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import * as Actions from '../actions';
|
||||
import SqlEditor from './SqlEditor';
|
||||
import shortid from 'shortid';
|
||||
|
||||
let queryCount = 1;
|
||||
|
||||
class QueryEditors extends React.Component {
|
||||
renameTab(qe) {
|
||||
const newTitle = prompt('Enter a new title for the tab');
|
||||
if (newTitle) {
|
||||
this.props.actions.queryEditorSetTitle(qe, newTitle);
|
||||
}
|
||||
}
|
||||
activeQueryEditor() {
|
||||
const qeid = this.props.tabHistory[this.props.tabHistory.length - 1];
|
||||
for (let i = 0; i < this.props.queryEditors.length; i++) {
|
||||
const qe = this.props.queryEditors[i];
|
||||
if (qe.id === qeid) {
|
||||
return qe;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
newQueryEditor() {
|
||||
queryCount++;
|
||||
const activeQueryEditor = this.activeQueryEditor();
|
||||
const qe = {
|
||||
id: shortid.generate(),
|
||||
title: `Untitled Query ${queryCount}`,
|
||||
dbId: (activeQueryEditor) ? activeQueryEditor.dbId : null,
|
||||
schema: (activeQueryEditor) ? activeQueryEditor.schema : null,
|
||||
autorun: false,
|
||||
sql: 'SELECT ...',
|
||||
};
|
||||
this.props.actions.addQueryEditor(qe);
|
||||
}
|
||||
handleSelect(key) {
|
||||
if (key === 'add_tab') {
|
||||
this.newQueryEditor();
|
||||
} else {
|
||||
this.props.actions.setActiveQueryEditor({ id: key });
|
||||
}
|
||||
}
|
||||
render() {
|
||||
const editors = this.props.queryEditors.map((qe, i) => {
|
||||
let latestQuery = this.props.queries[qe.latestQueryId];
|
||||
const state = (latestQuery) ? latestQuery.state : '';
|
||||
const tabTitle = (
|
||||
<div>
|
||||
<div className={'circle ' + state} /> {qe.title} {' '}
|
||||
<DropdownButton
|
||||
bsSize="small"
|
||||
id={'ddbtn-tab-' + i}
|
||||
>
|
||||
<MenuItem eventKey="1" onClick={this.props.actions.removeQueryEditor.bind(this, qe)}>
|
||||
<i className="fa fa-close" /> close tab
|
||||
</MenuItem>
|
||||
<MenuItem eventKey="2" onClick={this.renameTab.bind(this, qe)}>
|
||||
<i className="fa fa-i-cursor" /> rename tab
|
||||
</MenuItem>
|
||||
</DropdownButton>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<Tab
|
||||
key={qe.id}
|
||||
title={tabTitle}
|
||||
eventKey={qe.id}
|
||||
>
|
||||
<div className="panel panel-default">
|
||||
<div className="panel-body">
|
||||
<SqlEditor
|
||||
queryEditor={qe}
|
||||
latestQuery={latestQuery}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Tab>);
|
||||
});
|
||||
return (
|
||||
<Tabs
|
||||
bsStyle="tabs"
|
||||
activeKey={this.props.tabHistory[this.props.tabHistory.length - 1]}
|
||||
onSelect={this.handleSelect.bind(this)}
|
||||
>
|
||||
{editors}
|
||||
<Tab title={<div><i className="fa fa-plus-circle" /> </div>} eventKey="add_tab" />
|
||||
</Tabs>
|
||||
);
|
||||
}
|
||||
}
|
||||
QueryEditors.propTypes = {
|
||||
actions: React.PropTypes.object,
|
||||
queries: React.PropTypes.object,
|
||||
queryEditors: React.PropTypes.array,
|
||||
tabHistory: React.PropTypes.array,
|
||||
};
|
||||
QueryEditors.defaultProps = {
|
||||
tabHistory: [],
|
||||
queryEditors: [],
|
||||
};
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
queryEditors: state.queryEditors,
|
||||
queries: state.queries,
|
||||
tabHistory: state.tabHistory,
|
||||
};
|
||||
}
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators(Actions, dispatch),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(QueryEditors);
|
||||
@@ -0,0 +1,23 @@
|
||||
import React from 'react';
|
||||
import { BootstrapTable, TableHeaderColumn } from 'react-bootstrap-table';
|
||||
|
||||
const TableMetadata = function (props) {
|
||||
return (
|
||||
<BootstrapTable
|
||||
condensed
|
||||
data={props.table.columns}
|
||||
>
|
||||
<TableHeaderColumn dataField="id" isKey hidden>
|
||||
id
|
||||
</TableHeaderColumn>
|
||||
<TableHeaderColumn dataField="name">Name</TableHeaderColumn>
|
||||
<TableHeaderColumn dataField="type">Type</TableHeaderColumn>
|
||||
</BootstrapTable>
|
||||
);
|
||||
};
|
||||
|
||||
TableMetadata.propTypes = {
|
||||
table: React.PropTypes.object,
|
||||
};
|
||||
|
||||
export default TableMetadata;
|
||||
@@ -0,0 +1,102 @@
|
||||
import React from 'react';
|
||||
import { ButtonGroup } from 'react-bootstrap';
|
||||
import Link from './Link';
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import * as Actions from '../actions';
|
||||
import shortid from 'shortid';
|
||||
|
||||
// CSS
|
||||
import 'react-select/dist/react-select.css';
|
||||
|
||||
class TableWorkspaceElement extends React.Component {
|
||||
selectStar() {
|
||||
let cols = '';
|
||||
this.props.table.columns.forEach((col, i) => {
|
||||
cols += col.name;
|
||||
if (i < this.props.table.columns.length - 1) {
|
||||
cols += ', ';
|
||||
}
|
||||
});
|
||||
const sql = `SELECT ${cols}\nFROM ${this.props.table.name}`;
|
||||
const qe = {
|
||||
id: shortid.generate(),
|
||||
title: this.props.table.name,
|
||||
dbId: this.props.table.dbId,
|
||||
autorun: true,
|
||||
sql,
|
||||
};
|
||||
this.props.actions.addQueryEditor(qe);
|
||||
}
|
||||
render() {
|
||||
let metadata = null;
|
||||
let buttonToggle;
|
||||
if (!this.props.table.expanded) {
|
||||
buttonToggle = (
|
||||
<Link
|
||||
href="#"
|
||||
onClick={this.props.actions.expandTable.bind(this, this.props.table)}
|
||||
placement="right"
|
||||
tooltip="Collapse the table's structure information"
|
||||
>
|
||||
<i className="fa fa-minus" /> {this.props.table.name}
|
||||
</Link>
|
||||
);
|
||||
metadata = this.props.table.columns.map((col) =>
|
||||
<div className="clearfix">
|
||||
<span className="pull-left">{col.name}</span>
|
||||
<span className="pull-right">{col.type}</span>
|
||||
</div>
|
||||
);
|
||||
metadata = (
|
||||
<div style={{ 'margin-bottom': '5px' }}>{metadata}</div>
|
||||
);
|
||||
} else {
|
||||
buttonToggle = (
|
||||
<Link
|
||||
href="#"
|
||||
onClick={this.props.actions.collapseTable.bind(this, this.props.table)}
|
||||
placement="right"
|
||||
tooltip="Expand the table's structure information"
|
||||
>
|
||||
<i className="fa fa-plus" /> {this.props.table.name}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="ws-el">
|
||||
{buttonToggle}
|
||||
<ButtonGroup className="ws-el-controls pull-right">
|
||||
<Link
|
||||
className="fa fa-play"
|
||||
onClick={this.selectStar.bind(this)}
|
||||
tooltip="Run query in a new tab"
|
||||
href="#"
|
||||
/>
|
||||
<Link
|
||||
className="fa fa-trash"
|
||||
onClick={this.props.actions.removeTable.bind(this, this.props.table)}
|
||||
tooltip="Remove from workspace"
|
||||
href="#"
|
||||
/>
|
||||
</ButtonGroup>
|
||||
{metadata}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
TableWorkspaceElement.propTypes = {
|
||||
table: React.PropTypes.object,
|
||||
actions: React.PropTypes.object,
|
||||
};
|
||||
TableWorkspaceElement.defaultProps = {
|
||||
table: null,
|
||||
};
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators(Actions, dispatch),
|
||||
};
|
||||
}
|
||||
export default connect(null, mapDispatchToProps)(TableWorkspaceElement);
|
||||
|
||||
61
caravel/assets/javascripts/SqlLab/components/Timer.jsx
Normal file
61
caravel/assets/javascripts/SqlLab/components/Timer.jsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import React from 'react';
|
||||
import { now, fDuration } from '../../modules/dates';
|
||||
|
||||
import { STATE_BSSTYLE_MAP } from '../common.js';
|
||||
|
||||
class Timer extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
clockStr: '',
|
||||
};
|
||||
}
|
||||
componentWillMount() {
|
||||
this.startTimer();
|
||||
}
|
||||
componentWillUnmount() {
|
||||
this.stopTimer();
|
||||
}
|
||||
startTimer() {
|
||||
if (!(this.timer)) {
|
||||
this.timer = setInterval(this.stopwatch.bind(this), 30);
|
||||
}
|
||||
}
|
||||
stopTimer() {
|
||||
clearInterval(this.timer);
|
||||
this.timer = null;
|
||||
}
|
||||
stopwatch() {
|
||||
if (this.props && this.props.query) {
|
||||
const since = (this.props.query.endDttm) ? this.props.query.endDttm : now();
|
||||
const clockStr = fDuration(this.props.query.startDttm, since);
|
||||
this.setState({ clockStr });
|
||||
if (this.props.query.state !== 'running') {
|
||||
this.stopTimer();
|
||||
}
|
||||
}
|
||||
}
|
||||
render() {
|
||||
if (this.props.query && this.props.query.state === 'running') {
|
||||
this.startTimer();
|
||||
}
|
||||
let timerSpan = null;
|
||||
if (this.props && this.props.query) {
|
||||
const bsStyle = STATE_BSSTYLE_MAP[this.props.query.state];
|
||||
timerSpan = (
|
||||
<span className={'inlineBlock m-r-5 label label-' + bsStyle}>
|
||||
{this.state.clockStr}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return timerSpan;
|
||||
}
|
||||
}
|
||||
Timer.propTypes = {
|
||||
query: React.PropTypes.object,
|
||||
};
|
||||
Timer.defaultProps = {
|
||||
query: null,
|
||||
};
|
||||
|
||||
export default Timer;
|
||||
170
caravel/assets/javascripts/SqlLab/components/VisualizeModal.jsx
Normal file
170
caravel/assets/javascripts/SqlLab/components/VisualizeModal.jsx
Normal file
@@ -0,0 +1,170 @@
|
||||
import React from 'react';
|
||||
import { Button, Col, Modal } from 'react-bootstrap';
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import * as Actions from '../actions';
|
||||
|
||||
import Select from 'react-select';
|
||||
import { Table } from 'reactable';
|
||||
import shortid from 'shortid';
|
||||
|
||||
class VisualizeModal extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
chartType: 'line',
|
||||
datasourceName: shortid.generate(),
|
||||
columns: {},
|
||||
};
|
||||
}
|
||||
changeChartType(option) {
|
||||
this.setState({ chartType: (option) ? option.value : null });
|
||||
}
|
||||
mergedColumns() {
|
||||
const columns = Object.assign({}, this.state.columns);
|
||||
if (this.props.query && this.props.query.results.columns) {
|
||||
this.props.query.results.columns.forEach((col) => {
|
||||
if (columns[col] === undefined) {
|
||||
columns[col] = {};
|
||||
}
|
||||
});
|
||||
}
|
||||
return columns;
|
||||
}
|
||||
visualize() {
|
||||
const vizOptions = {
|
||||
chartType: this.state.chartType,
|
||||
datasourceName: this.state.datasourceName,
|
||||
columns: this.state.columns,
|
||||
sql: this.props.query.sql,
|
||||
dbId: this.props.query.dbId,
|
||||
};
|
||||
window.open('/caravel/sqllab_viz/?data=' + JSON.stringify(vizOptions));
|
||||
}
|
||||
changeDatasourceName(event) {
|
||||
this.setState({ datasourceName: event.target.value });
|
||||
}
|
||||
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 });
|
||||
}
|
||||
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 });
|
||||
}
|
||||
render() {
|
||||
if (!(this.props.query)) {
|
||||
return <div />;
|
||||
}
|
||||
const tableData = this.props.query.results.columns.map((col) => ({
|
||||
column: col,
|
||||
is_dimension: (
|
||||
<input
|
||||
type="checkbox"
|
||||
onChange={this.changeCheckbox.bind(this, 'is_dim', col)}
|
||||
checked={(this.state.columns[col]) ? this.state.columns[col].is_dim : false}
|
||||
className="form-control"
|
||||
/>
|
||||
),
|
||||
is_date: (
|
||||
<input
|
||||
type="checkbox"
|
||||
className="form-control"
|
||||
onChange={this.changeCheckbox.bind(this, 'is_date', col)}
|
||||
checked={(this.state.columns[col]) ? this.state.columns[col].is_date : false}
|
||||
/>
|
||||
),
|
||||
agg_func: (
|
||||
<Select
|
||||
options={[
|
||||
{ value: 'sum', label: 'SUM(x)' },
|
||||
{ value: 'min', label: 'MIN(x)' },
|
||||
{ value: 'max', label: 'MAX(x)' },
|
||||
{ value: 'avg', label: 'AVG(x)' },
|
||||
{ value: 'count_distinct', label: 'COUNT(DISTINCT x)' },
|
||||
]}
|
||||
onChange={this.changeAggFunction.bind(this, col)}
|
||||
value={(this.state.columns[col]) ? this.state.columns[col].agg : null}
|
||||
/>
|
||||
),
|
||||
}));
|
||||
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.Header>
|
||||
<Modal.Body>
|
||||
<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}
|
||||
autosize={false}
|
||||
onChange={this.changeChartType.bind(this)}
|
||||
/>
|
||||
</Col>
|
||||
<Col md={6}>
|
||||
Datasource Name
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
placeholder="datasource name"
|
||||
onChange={this.changeDatasourceName.bind(this)}
|
||||
value={this.state.datasourceName}
|
||||
/>
|
||||
</Col>
|
||||
</div>
|
||||
<hr />
|
||||
<Table
|
||||
className="table table-condensed"
|
||||
columns={['column', 'is_dimension', 'is_date', 'agg_func']}
|
||||
data={tableData}
|
||||
/>
|
||||
<Button
|
||||
onClick={this.visualize.bind(this)}
|
||||
bsStyle="primary"
|
||||
>
|
||||
Visualize
|
||||
</Button>
|
||||
</Modal.Body>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
return modal;
|
||||
}
|
||||
}
|
||||
VisualizeModal.propTypes = {
|
||||
query: React.PropTypes.object,
|
||||
show: React.PropTypes.boolean,
|
||||
onHide: React.PropTypes.function,
|
||||
};
|
||||
VisualizeModal.defaultProps = {
|
||||
show: false,
|
||||
};
|
||||
|
||||
function mapStateToProps() {
|
||||
return {};
|
||||
}
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators(Actions, dispatch),
|
||||
};
|
||||
}
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(VisualizeModal);
|
||||
Reference in New Issue
Block a user