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:
Maxime Beauchemin
2016-05-21 22:07:37 -07:00
parent 23a5463208
commit 07a6a0a630
29 changed files with 2174 additions and 38 deletions

View File

@@ -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;

View 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);

View 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;

View 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);

View 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);

View 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);

View 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);

View 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;

View 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;

View 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" />&nbsp;
</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);

View File

@@ -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);

View File

@@ -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" />&nbsp;</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);

View File

@@ -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;

View File

@@ -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);

View 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;