mirror of
https://github.com/apache/superset.git
synced 2026-05-07 17:04:58 +00:00
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.
This commit is contained in:
@@ -0,0 +1,48 @@
|
||||
import React from 'react';
|
||||
import { Button, OverlayTrigger, Tooltip } from 'react-bootstrap';
|
||||
|
||||
|
||||
class ButtonWithTooltip extends React.Component {
|
||||
render() {
|
||||
let tooltip = (
|
||||
<Tooltip id="tooltip">
|
||||
{this.props.tooltip}
|
||||
</Tooltip>
|
||||
);
|
||||
return (
|
||||
<OverlayTrigger
|
||||
overlay={tooltip}
|
||||
delayShow={300}
|
||||
placement={this.props.placement}
|
||||
delayHide={150}
|
||||
>
|
||||
<Button
|
||||
onClick={this.props.onClick}
|
||||
bsStyle={this.props.bsStyle}
|
||||
disabled={this.props.disabled}
|
||||
className={this.props.className}
|
||||
>
|
||||
{this.props.children}
|
||||
</Button>
|
||||
</OverlayTrigger>
|
||||
);
|
||||
}
|
||||
}
|
||||
ButtonWithTooltip.defaultProps = {
|
||||
onClick: () => {},
|
||||
disabled: false,
|
||||
placement: 'top',
|
||||
bsStyle: 'default',
|
||||
};
|
||||
|
||||
ButtonWithTooltip.propTypes = {
|
||||
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;
|
||||
71
caravel/assets/javascripts/SqlLab/components/LeftPane.jsx
Normal file
71
caravel/assets/javascripts/SqlLab/components/LeftPane.jsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import React from 'react';
|
||||
import { Alert, Button, Label } from 'react-bootstrap';
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import * as Actions from '../actions';
|
||||
import QueryLink from './QueryLink';
|
||||
|
||||
// CSS
|
||||
import 'react-select/dist/react-select.css';
|
||||
|
||||
class LeftPane extends React.Component {
|
||||
render() {
|
||||
let queryElements;
|
||||
if (this.props.workspaceQueries.length > 0) {
|
||||
queryElements = this.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 className="panel panel-default LeftPane">
|
||||
<div className="panel-heading">
|
||||
<h6 className="m-r-10">
|
||||
<i className="fa fa-flask" />
|
||||
SQL Lab <Label bsStyle="danger">ALPHA</Label>
|
||||
</h6>
|
||||
</div>
|
||||
<div className="panel-body">
|
||||
<div>
|
||||
<h6>
|
||||
<span className="fa-stack">
|
||||
<i className="fa fa-database fa-stack-lg"></i>
|
||||
<i className="fa fa-search fa-stack-1x"></i>
|
||||
</span> Saved Queries
|
||||
</h6>
|
||||
<div>
|
||||
{queryElements}
|
||||
</div>
|
||||
<hr />
|
||||
<Button onClick={this.props.actions.resetState.bind(this)}>
|
||||
Reset State
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
LeftPane.propTypes = {
|
||||
workspaceQueries: React.PropTypes.array,
|
||||
};
|
||||
LeftPane.defaultProps = {
|
||||
workspaceQueries: [],
|
||||
};
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
workspaceQueries: state.workspaceQueries,
|
||||
};
|
||||
}
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators(Actions, dispatch),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(LeftPane);
|
||||
52
caravel/assets/javascripts/SqlLab/components/Link.jsx
Normal file
52
caravel/assets/javascripts/SqlLab/components/Link.jsx
Normal file
@@ -0,0 +1,52 @@
|
||||
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}
|
||||
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 = {
|
||||
className: React.PropTypes.string,
|
||||
href: React.PropTypes.string,
|
||||
onClick: React.PropTypes.func,
|
||||
tooltip: React.PropTypes.string,
|
||||
placement: React.PropTypes.string,
|
||||
children: React.PropTypes.object,
|
||||
};
|
||||
Link.defaultProps = {
|
||||
disabled: false,
|
||||
href: '#',
|
||||
tooltip: null,
|
||||
placement: 'top',
|
||||
onClick: () => {},
|
||||
};
|
||||
|
||||
export default Link;
|
||||
60
caravel/assets/javascripts/SqlLab/components/QueryLink.jsx
Normal file
60
caravel/assets/javascripts/SqlLab/components/QueryLink.jsx
Normal file
@@ -0,0 +1,60 @@
|
||||
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 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 className="ws-el">
|
||||
{this.props.query.title}
|
||||
<ButtonGroup className="ws-el-controls pull-right">
|
||||
<Link
|
||||
className="fa fa-plus-circle"
|
||||
onClick={this.popTab.bind(this)}
|
||||
tooltip="Pop this query in a new tab"
|
||||
href="#"
|
||||
/>
|
||||
<Link
|
||||
className="fa fa-trash"
|
||||
onClick={this.props.actions.removeWorkspaceQuery.bind(this, this.props.query)}
|
||||
tooltip="Remove query from workspace"
|
||||
href="#"
|
||||
/>
|
||||
</ButtonGroup>
|
||||
</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);
|
||||
|
||||
51
caravel/assets/javascripts/SqlLab/components/QueryLog.jsx
Normal file
51
caravel/assets/javascripts/SqlLab/components/QueryLog.jsx
Normal file
@@ -0,0 +1,51 @@
|
||||
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';
|
||||
|
||||
class QueryLog extends React.Component {
|
||||
render() {
|
||||
const activeQeId = this.props.tabHistory[this.props.tabHistory.length - 1];
|
||||
const queries = this.props.queries.filter((q) => (q.sqlEditorId === activeQeId));
|
||||
if (queries.length > 0) {
|
||||
return (
|
||||
<QueryTable
|
||||
columns={['state', 'started', 'duration', 'rows', 'sql', 'actions']}
|
||||
queries={queries}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Alert bsStyle="info">
|
||||
No query history yet...
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
}
|
||||
QueryLog.defaultProps = {
|
||||
queries: [],
|
||||
};
|
||||
|
||||
QueryLog.propTypes = {
|
||||
queries: React.PropTypes.array,
|
||||
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)(QueryLog);
|
||||
74
caravel/assets/javascripts/SqlLab/components/QuerySearch.jsx
Normal file
74
caravel/assets/javascripts/SqlLab/components/QuerySearch.jsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import React from 'react';
|
||||
import SplitPane from 'react-split-pane';
|
||||
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 (
|
||||
<SplitPane split="vertical" minSize={200} defaultSize={300}>
|
||||
<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>
|
||||
</SplitPane>
|
||||
);
|
||||
}
|
||||
}
|
||||
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);
|
||||
170
caravel/assets/javascripts/SqlLab/components/QueryTable.jsx
Normal file
170
caravel/assets/javascripts/SqlLab/components/QueryTable.jsx
Normal file
@@ -0,0 +1,170 @@
|
||||
import React from 'react';
|
||||
import { Alert, Modal } from 'react-bootstrap';
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import * as Actions from '../actions';
|
||||
|
||||
import Select from 'react-select';
|
||||
|
||||
import moment from 'moment';
|
||||
import { Table } from 'reactable';
|
||||
|
||||
import SyntaxHighlighter from 'react-syntax-highlighter';
|
||||
import { github } from 'react-syntax-highlighter/dist/styles';
|
||||
|
||||
import Link from './Link';
|
||||
|
||||
// TODO move to CSS
|
||||
const STATE_COLOR_MAP = {
|
||||
failed: 'red',
|
||||
running: 'lime',
|
||||
success: 'green',
|
||||
};
|
||||
|
||||
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.state.activeQuery = query;
|
||||
}
|
||||
changeChartType(event) {
|
||||
}
|
||||
render() {
|
||||
const data = this.props.queries.map((query) => {
|
||||
const q = Object.assign({}, query);
|
||||
const since = (q.endDttm) ? q.endDttm : new Date();
|
||||
let duration = since.valueOf() - q.startDttm.valueOf();
|
||||
duration = moment.utc(duration);
|
||||
if (q.endDttm) {
|
||||
q.duration = duration.format('HH:mm:ss.SS');
|
||||
}
|
||||
q.started = moment(q.startDttm).format('HH:mm:ss');
|
||||
q.sql = <SyntaxHighlighter language="sql" style={github}>{q.sql}</SyntaxHighlighter>;
|
||||
q.state = (
|
||||
<span
|
||||
className="label label-default"
|
||||
style={{ backgroundColor: STATE_COLOR_MAP[q.state] }}
|
||||
>
|
||||
{q.state}
|
||||
</span>
|
||||
);
|
||||
q.actions = (
|
||||
<div>
|
||||
<Link
|
||||
className="fa fa-line-chart fa-lg"
|
||||
tooltip="Visualize the data out of this query"
|
||||
onClick={this.showVisualizeModal.bind(this, query)}
|
||||
href="#"
|
||||
/>
|
||||
<Link
|
||||
className="fa fa-plus-circle"
|
||||
tooltip="Pop a tab containing this query"
|
||||
href="#"
|
||||
/>
|
||||
<Link
|
||||
className="fa fa-trash"
|
||||
href="#"
|
||||
tooltip="Remove query from log"
|
||||
onClick={this.props.actions.removeQuery.bind(this, query)}
|
||||
/>
|
||||
<Link
|
||||
className="fa fa-map-pin"
|
||||
tooltip="Pin this query to the top of this query log"
|
||||
href="#"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
return q;
|
||||
}).reverse();
|
||||
let visualizeModalBody;
|
||||
if (this.state.activeQuery) {
|
||||
const cols = this.state.activeQuery.results.columns;
|
||||
visualizeModalBody = (
|
||||
<div>
|
||||
<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={null}
|
||||
autosize={false}
|
||||
onChange={this.changeChartType.bind(this)}
|
||||
/>
|
||||
<Table
|
||||
className="table table-condensed"
|
||||
columns={['column', 'is_dimension', 'is_date', 'agg_func']}
|
||||
data={cols.map((col) => ({
|
||||
column: col,
|
||||
is_dimension: <input type="checkbox" className="form-control" />,
|
||||
is_date: <input type="checkbox" className="form-control" />,
|
||||
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)' },
|
||||
]}
|
||||
/>
|
||||
),
|
||||
}))}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<Modal show={this.state.showVisualizeModal} onHide={this.hideVisualizeModal.bind(this)}>
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title>Visualize (mock)</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<Alert bsStyle="danger">Not functional - Work in progress!</Alert>
|
||||
{visualizeModalBody}
|
||||
</Modal.Body>
|
||||
</Modal>
|
||||
<Table
|
||||
columns={['state', 'started', 'duration', 'rows', 'sql', 'actions']}
|
||||
className="table table-condensed"
|
||||
data={data}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
QueryTable.propTypes = {
|
||||
columns: React.PropTypes.array,
|
||||
actions: React.PropTypes.object,
|
||||
queries: React.PropTypes.object,
|
||||
};
|
||||
QueryTable.defaultProps = {
|
||||
columns: ['state', 'started', 'duration', 'rows', 'sql', 'actions'],
|
||||
queries: [],
|
||||
};
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
};
|
||||
}
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators(Actions, dispatch),
|
||||
};
|
||||
}
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(QueryTable);
|
||||
54
caravel/assets/javascripts/SqlLab/components/ResultSet.jsx
Normal file
54
caravel/assets/javascripts/SqlLab/components/ResultSet.jsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import React from 'react';
|
||||
import { Alert, Button } from 'react-bootstrap';
|
||||
import { Table } from 'reactable';
|
||||
|
||||
|
||||
class ResultSet extends React.Component {
|
||||
shouldComponentUpdate() {
|
||||
return 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">
|
||||
<Button className="m-r-5"><i className="fa fa-line-chart" />Visualize</Button>
|
||||
<Button className="m-r-5"><i className="fa fa-file-text-o" />.CSV</Button>
|
||||
</div>
|
||||
<div className="pull-right">
|
||||
<input type="text" className="form-control" placeholder="Search Results" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (results.data.length > 0) {
|
||||
return (
|
||||
<div className="ResultSet">
|
||||
{controls}
|
||||
<Table
|
||||
data={results.data}
|
||||
columns={results.columns}
|
||||
sortable
|
||||
className="table table-condensed table-bordered"
|
||||
/>
|
||||
</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,
|
||||
};
|
||||
ResultSet.defaultProps = {
|
||||
showControls: true,
|
||||
search: true,
|
||||
};
|
||||
|
||||
export default ResultSet;
|
||||
44
caravel/assets/javascripts/SqlLab/components/SouthPane.jsx
Normal file
44
caravel/assets/javascripts/SqlLab/components/SouthPane.jsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import { Tab, Tabs } from 'react-bootstrap';
|
||||
import QueryLog from './QueryLog';
|
||||
import ResultSet from './ResultSet';
|
||||
import React from 'react';
|
||||
|
||||
class SouthPane extends React.Component {
|
||||
render() {
|
||||
let results;
|
||||
if (this.props.latestQuery) {
|
||||
if (this.props.latestQuery.state === 'running') {
|
||||
results = (
|
||||
<img className="loading" alt="Loading.." src="/static/assets/images/loading.gif" />
|
||||
);
|
||||
} else if (this.props.latestQuery.state === 'failed') {
|
||||
results = <div className="alert alert-danger">{this.props.latestQuery.msg}</div>;
|
||||
} else if (this.props.latestQuery.state === 'success') {
|
||||
results = <ResultSet showControls query={this.props.latestQuery} />;
|
||||
}
|
||||
} else {
|
||||
results = <div className="alert alert-info">Run a query to display results here</div>;
|
||||
}
|
||||
return (
|
||||
<Tabs bsStyle="pills">
|
||||
<Tab title="Results" eventKey={1}>
|
||||
<div style={{ overflow: 'auto' }}>
|
||||
{results}
|
||||
</div>
|
||||
</Tab>
|
||||
<Tab title="Query Log" eventKey={2}>
|
||||
<QueryLog />
|
||||
</Tab>
|
||||
</Tabs>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SouthPane.propTypes = {
|
||||
latestQuery: React.PropTypes.object,
|
||||
};
|
||||
|
||||
SouthPane.defaultProps = {
|
||||
};
|
||||
|
||||
export default SouthPane;
|
||||
233
caravel/assets/javascripts/SqlLab/components/SqlEditor.jsx
Normal file
233
caravel/assets/javascripts/SqlLab/components/SqlEditor.jsx
Normal file
@@ -0,0 +1,233 @@
|
||||
const $ = window.$ = require('jquery');
|
||||
import React from 'react';
|
||||
import { Button, ButtonGroup, 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,
|
||||
};
|
||||
}
|
||||
componentDidMount() {
|
||||
if (this.state.autorun) {
|
||||
this.setState({ autorun: false });
|
||||
this.props.actions.queryEditorSetAutorun(this.props.queryEditor, false);
|
||||
this.startQuery();
|
||||
}
|
||||
}
|
||||
getTableOptions(input, callback) {
|
||||
const url = '/tableasync/api/read?_oc_DatabaseAsync=database_name&_od_DatabaseAsync=asc';
|
||||
$.get(url, function (data) {
|
||||
const options = [];
|
||||
for (let i = 0; i < data.pks.length; i++) {
|
||||
options.push({ value: data.pks[i], label: data.result[i].table_name });
|
||||
}
|
||||
callback(null, {
|
||||
options,
|
||||
cache: false,
|
||||
});
|
||||
});
|
||||
}
|
||||
startQuery() {
|
||||
const that = this;
|
||||
const query = {
|
||||
id: shortid.generate(),
|
||||
sqlEditorId: this.props.queryEditor.id,
|
||||
sql: this.state.sql,
|
||||
state: 'running',
|
||||
tab: this.props.queryEditor.title,
|
||||
dbId: this.props.queryEditor.dbId,
|
||||
startDttm: new Date(),
|
||||
};
|
||||
const url = '/caravel/sql_json/';
|
||||
const data = {
|
||||
sql: this.state.sql,
|
||||
database_id: this.props.queryEditor.dbId,
|
||||
schema: this.props.queryEditor.schema,
|
||||
json: true,
|
||||
};
|
||||
this.props.actions.startQuery(query);
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
dataType: 'json',
|
||||
url,
|
||||
data,
|
||||
success(results) {
|
||||
try {
|
||||
that.props.actions.querySuccess(query, results);
|
||||
} catch (e) {
|
||||
that.props.actions.queryFailed(query, e);
|
||||
}
|
||||
},
|
||||
error(err) {
|
||||
let msg = '';
|
||||
try {
|
||||
msg = err.responseJSON.error;
|
||||
} catch (e) {
|
||||
msg = (err.responseText) ? err.responseText : e;
|
||||
}
|
||||
that.props.actions.queryFailed(query, msg);
|
||||
},
|
||||
});
|
||||
}
|
||||
stopQuery() {
|
||||
this.props.actions.stopQuery(this.props.latestQuery);
|
||||
}
|
||||
textChange(text) {
|
||||
this.setState({ sql: text })
|
||||
this.props.actions.queryEditorSetSql(this.props.queryEditor, text);
|
||||
}
|
||||
notImplemented() {
|
||||
alert('Not implemented');
|
||||
}
|
||||
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() {}
|
||||
render() {
|
||||
let runButtons = (
|
||||
<ButtonGroup className="inline m-r-5">
|
||||
<Button onClick={this.startQuery.bind(this)} disabled={!(this.props.queryEditor.dbId)}>
|
||||
<i className="fa fa-table" /> Run
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
);
|
||||
if (this.props.latestQuery && this.props.latestQuery.state === 'running') {
|
||||
runButtons = (
|
||||
<ButtonGroup className="inline m-r-5">
|
||||
<Button 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"
|
||||
onClick={this.addWorkspaceQuery.bind(this)}
|
||||
>
|
||||
<i className="fa fa-save" />
|
||||
</ButtonWithTooltip>
|
||||
<DropdownButton id="ddbtn-export" pullRight 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 row_limit = 1000;
|
||||
if (this.props.latestQuery && this.props.latestQuery.rows === row_limit) {
|
||||
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 {row_limit} limit.
|
||||
</Tooltip>
|
||||
);
|
||||
limitWarning = (
|
||||
<OverlayTrigger placement="left" overlay={tooltip}>
|
||||
<Label bsStyle="warning" className="m-r-5">LIMIT</Label>
|
||||
</OverlayTrigger>
|
||||
);
|
||||
}
|
||||
const editorBottomBar = (
|
||||
<div className="clearfix sql-toolbar padded">
|
||||
<div className="pull-left">
|
||||
{runButtons}
|
||||
<span className="inlineblock valignTop" style={{ height: '20px' }}>
|
||||
<input type="text" className="form-control" placeholder="CREATE TABLE AS" />
|
||||
</span>
|
||||
</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.title}
|
||||
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}
|
||||
<div className="padded">
|
||||
<SouthPane latestQuery={this.props.latestQuery} sqlEditor={this} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SqlEditor.propTypes = {
|
||||
queryEditor: React.PropTypes.object,
|
||||
actions: React.PropTypes.object,
|
||||
latestQuery: React.PropTypes.object,
|
||||
};
|
||||
|
||||
SqlEditor.defaultProps = {
|
||||
};
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators(Actions, dispatch),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(SqlEditor);
|
||||
@@ -0,0 +1,280 @@
|
||||
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();
|
||||
}
|
||||
getTableOptions(input, callback) {
|
||||
const url = '/tableasync/api/read?_oc_DatabaseAsync=database_name&_od_DatabaseAsync=asc';
|
||||
$.get(url, function (data) {
|
||||
const options = [];
|
||||
for (let i = 0; i < data.pks.length; i++) {
|
||||
options.push({ value: data.pks[i], label: data.result[i].table_name });
|
||||
}
|
||||
callback(null, {
|
||||
options,
|
||||
cache: false,
|
||||
});
|
||||
});
|
||||
}
|
||||
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;
|
||||
const that = this;
|
||||
this.setState({ tableLoading: true });
|
||||
this.setState({ tableOptions: [] });
|
||||
const url = `/caravel/tables/${actualDbId}/${actualSchema}`;
|
||||
$.get(url, function (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];
|
||||
that.setState({ tableOptions });
|
||||
that.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 that = this;
|
||||
const actualDbId = dbId || this.props.queryEditor.dbId;
|
||||
if (actualDbId) {
|
||||
this.setState({ schemaLoading: true });
|
||||
const url = `/databasetablesasync/api/read?_flt_0_id=${actualDbId}`;
|
||||
$.get(url, function (data) {
|
||||
const schemas = data.result[0].all_schema_names;
|
||||
const schemaOptions = schemas.map((s) => ({ value: s, label: s }));
|
||||
that.setState({ schemaOptions });
|
||||
that.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 that = this;
|
||||
const url = '/databaseasync/api/read';
|
||||
$.get(url, function (data) {
|
||||
const options = data.result.map((db) => ({ value: db.id, label: db.database_name }));
|
||||
that.setState({ databaseOptions: options });
|
||||
that.setState({ databaseLoading: false });
|
||||
});
|
||||
}
|
||||
notImplemented() {
|
||||
alert('Not implemented');
|
||||
}
|
||||
closePopover(ref) {
|
||||
this.refs[ref].hide();
|
||||
}
|
||||
changeTable(tableOpt) {
|
||||
const tableName = tableOpt.value;
|
||||
const that = this;
|
||||
const qe = this.props.queryEditor;
|
||||
const url = `/caravel/table/${qe.dbId}/${tableName}/${qe.schema}/`;
|
||||
$.get(url, function (data) {
|
||||
that.props.actions.addTable({
|
||||
id: shortid.generate(),
|
||||
dbId: that.props.queryEditor.dbId,
|
||||
queryEditorId: that.props.queryEditor.id,
|
||||
name: data.name,
|
||||
schema: qe.schema,
|
||||
columns: data.columns,
|
||||
expanded: true,
|
||||
showPopup: false,
|
||||
});
|
||||
});
|
||||
}
|
||||
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 padded">
|
||||
<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,116 @@
|
||||
import React from 'react';
|
||||
import { DropdownButton, MenuItem, Panel, 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);
|
||||
}
|
||||
}
|
||||
newQueryEditor() {
|
||||
queryCount++;
|
||||
const dbId = (this.props.workspaceDatabase) ? this.props.workspaceDatabase.id : null;
|
||||
const qe = {
|
||||
id: shortid.generate(),
|
||||
title: `Query ${queryCount}`,
|
||||
dbId,
|
||||
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 that = this;
|
||||
const editors = this.props.queryEditors.map((qe, i) => {
|
||||
let latestQuery;
|
||||
that.props.queries.forEach((q) => {
|
||||
if (q.id === qe.latestQueryId) {
|
||||
latestQuery = q;
|
||||
}
|
||||
});
|
||||
const state = (latestQuery) ? latestQuery.state : '';
|
||||
const tabTitle = (
|
||||
<div>
|
||||
<div className={'circle ' + state} /> {qe.title} {' '}
|
||||
<DropdownButton
|
||||
bsSize="small"
|
||||
id={'ddbtn-tab-' + i}
|
||||
className="no-shadow"
|
||||
id="bg-vertical-dropdown-1"
|
||||
>
|
||||
<MenuItem eventKey="1" onClick={that.props.actions.removeQueryEditor.bind(that, qe)}>
|
||||
<i className="fa fa-close" /> close tab
|
||||
</MenuItem>
|
||||
<MenuItem eventKey="2" onClick={that.renameTab.bind(that, qe)}>
|
||||
<i className="fa fa-i-cursor" /> rename tab
|
||||
</MenuItem>
|
||||
</DropdownButton>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<Tab
|
||||
key={qe.id}
|
||||
title={tabTitle}
|
||||
eventKey={qe.id}
|
||||
>
|
||||
<Panel className="nopadding">
|
||||
<SqlEditor
|
||||
queryEditor={qe}
|
||||
latestQuery={latestQuery}
|
||||
/>
|
||||
</Panel>
|
||||
</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,
|
||||
tabHistory: React.PropTypes.array,
|
||||
queryEditors: React.PropTypes.array,
|
||||
workspaceDatabase: React.PropTypes.object,
|
||||
};
|
||||
QueryEditors.defaultProps = {
|
||||
tabHistory: [],
|
||||
queryEditors: [],
|
||||
};
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
queryEditors: state.queryEditors,
|
||||
queries: state.queries,
|
||||
workspaceDatabase: state.workspaceDatabase,
|
||||
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,103 @@
|
||||
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 = '';
|
||||
const that = this;
|
||||
this.props.table.columns.forEach(function (col, i) {
|
||||
cols += col.name;
|
||||
if (i < that.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);
|
||||
|
||||
62
caravel/assets/javascripts/SqlLab/components/Timer.jsx
Normal file
62
caravel/assets/javascripts/SqlLab/components/Timer.jsx
Normal file
@@ -0,0 +1,62 @@
|
||||
import React from 'react';
|
||||
import moment from 'moment';
|
||||
|
||||
|
||||
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) {
|
||||
let fromDttm = this.props.query.endDttm || new Date();
|
||||
fromDttm = moment(fromDttm);
|
||||
let duration = fromDttm - moment(this.props.query.startDttm).valueOf();
|
||||
duration = moment.utc(duration);
|
||||
const clockStr = duration.format('HH:mm:ss.SS');
|
||||
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) {
|
||||
timerSpan = (
|
||||
<span className={'label label-warning inlineBlock m-r-5 ' + this.props.query.state}>
|
||||
{this.state.clockStr}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return timerSpan;
|
||||
}
|
||||
}
|
||||
Timer.propTypes = {
|
||||
query: React.PropTypes.object,
|
||||
};
|
||||
Timer.defaultProps = {
|
||||
query: null,
|
||||
};
|
||||
|
||||
export default Timer;
|
||||
Reference in New Issue
Block a user