More goodness
diff --git a/caravel/assets/javascripts/SqlLab/TODO.md b/caravel/assets/javascripts/SqlLab/TODO.md
index 8a83c57..b8af8ba 100644
--- a/caravel/assets/javascripts/SqlLab/TODO.md
+++ b/caravel/assets/javascripts/SqlLab/TODO.md
@@ -1,22 +1,14 @@
-# Design
-* Query Log, search, filter on active tab only
-* Where to make the limit clear?
# TODO
+* Figure out how to organize the left panel, integrate Search
* collapse sql beyond 10 lines
-* add [Visualize] icon to modal
-* Security per-database
-* Overwrite workspace query
-* Async
-* Refactor timer in to its own thing
-
+* Security per-database (dropdown)
+* Get a to work
## Cosmetic
-* use icons for datatypes
* SqlEditor buttons
* use react-bootstrap-prompt for query title input
-* make input:text more self-evident
-* Tab cosmetic in theme
+* Make tabs look great
# PROJECT
* Write Runbook
diff --git a/caravel/assets/javascripts/SqlLab/actions.js b/caravel/assets/javascripts/SqlLab/actions.js
index a236136..0c2d3b9 100644
--- a/caravel/assets/javascripts/SqlLab/actions.js
+++ b/caravel/assets/javascripts/SqlLab/actions.js
@@ -20,6 +20,8 @@
export const ADD_WORKSPACE_QUERY = 'ADD_WORKSPACE_QUERY';
export const REMOVE_WORKSPACE_QUERY = 'REMOVE_WORKSPACE_QUERY';
export const SET_ACTIVE_QUERY_EDITOR = 'SET_ACTIVE_QUERY_EDITOR';
+export const ADD_ALERT = 'ADD_ALERT';
+export const REMOVE_ALERT = 'REMOVE_ALERT';
export function resetState() {
return { type: RESET_STATE };
@@ -29,6 +31,14 @@
return { type: ADD_QUERY_EDITOR, queryEditor };
}
+export function addAlert(alert) {
+ return { type: ADD_ALERT, alert };
+}
+
+export function removeAlert(alert) {
+ return { type: REMOVE_ALERT, alert };
+}
+
export function setActiveQueryEditor(queryEditor) {
return { type: SET_ACTIVE_QUERY_EDITOR, queryEditor };
}
diff --git a/caravel/assets/javascripts/SqlLab/components/Alerts.jsx b/caravel/assets/javascripts/SqlLab/components/Alerts.jsx
new file mode 100644
index 0000000..6ba16f5
--- /dev/null
+++ b/caravel/assets/javascripts/SqlLab/components/Alerts.jsx
@@ -0,0 +1,40 @@
+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,
+};
+
+function mapDispatchToProps(dispatch) {
+ return {
+ actions: bindActionCreators(Actions, dispatch),
+ };
+}
+export default connect(null, mapDispatchToProps)(Alerts);
diff --git a/caravel/assets/javascripts/SqlLab/components/LeftPane.jsx b/caravel/assets/javascripts/SqlLab/components/LeftPane.jsx
index cebeb28..0cccdb1 100644
--- a/caravel/assets/javascripts/SqlLab/components/LeftPane.jsx
+++ b/caravel/assets/javascripts/SqlLab/components/LeftPane.jsx
@@ -4,8 +4,8 @@
import { bindActionCreators } from 'redux';
import * as Actions from '../actions';
import QueryLink from './QueryLink';
+import shortid from 'shortid';
-// CSS
import 'react-select/dist/react-select.css';
const LeftPane = (props) => {
@@ -43,6 +43,12 @@
<Button onClick={props.actions.resetState.bind(this)}>
Reset State
</Button>
+ <Button onClick={props.actions.addAlert.bind(this, {
+ msg: 'This info alert is a demo alert',
+ bsStyle: 'info',
+ })}>
+ Add Alert
+ </Button>
</div>
</div>
</div>
diff --git a/caravel/assets/javascripts/SqlLab/components/SqlEditor.jsx b/caravel/assets/javascripts/SqlLab/components/SqlEditor.jsx
index 3746e11..5e35a17 100644
--- a/caravel/assets/javascripts/SqlLab/components/SqlEditor.jsx
+++ b/caravel/assets/javascripts/SqlLab/components/SqlEditor.jsx
@@ -46,25 +46,12 @@
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,
+ sql: this.props.queryEditor.sql,
state: 'running',
tab: this.props.queryEditor.title,
dbId: this.props.queryEditor.dbId,
@@ -72,7 +59,7 @@
};
const url = '/caravel/sql_json/';
const data = {
- sql: this.state.sql,
+ sql: this.props.queryEditor.sql,
database_id: this.props.queryEditor.dbId,
schema: this.props.queryEditor.schema,
json: true,
diff --git a/caravel/assets/javascripts/SqlLab/components/SqlEditorTopToolbar.jsx b/caravel/assets/javascripts/SqlLab/components/SqlEditorTopToolbar.jsx
index 87948d4..3e42365 100644
--- a/caravel/assets/javascripts/SqlLab/components/SqlEditorTopToolbar.jsx
+++ b/caravel/assets/javascripts/SqlLab/components/SqlEditorTopToolbar.jsx
@@ -29,19 +29,6 @@
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) {
@@ -70,16 +57,15 @@
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) {
+ $.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];
- that.setState({ tableOptions });
- that.setState({ tableLoading: false });
+ this.setState({ tableOptions });
+ this.setState({ tableLoading: false });
});
}
}
@@ -89,16 +75,15 @@
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) {
+ $.get(url, (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 });
+ this.setState({ schemaOptions });
+ this.setState({ schemaLoading: false });
});
}
}
@@ -115,12 +100,11 @@
}
fetchDatabaseOptions() {
this.setState({ databaseLoading: true });
- const that = this;
const url = '/databaseasync/api/read';
- $.get(url, function (data) {
+ $.get(url, (data) => {
const options = data.result.map((db) => ({ value: db.id, label: db.database_name }));
- that.setState({ databaseOptions: options });
- that.setState({ databaseLoading: false });
+ this.setState({ databaseOptions: options });
+ this.setState({ databaseLoading: false });
});
}
closePopover(ref) {
@@ -128,20 +112,25 @@
}
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({
+ $.get(url, (data) => {
+ this.props.actions.addTable({
id: shortid.generate(),
- dbId: that.props.queryEditor.dbId,
- queryEditorId: that.props.queryEditor.id,
+ dbId: this.props.queryEditor.dbId,
+ queryEditorId: this.props.queryEditor.id,
name: data.name,
schema: qe.schema,
columns: data.columns,
expanded: true,
showPopup: false,
});
+ })
+ .fail((err) => {
+ this.props.actions.addAlert({
+ msg: 'Error occurred while fetching metadata',
+ bsStyle: 'danger',
+ });
});
}
render() {
diff --git a/caravel/assets/javascripts/SqlLab/components/TabbedSqlEditors.jsx b/caravel/assets/javascripts/SqlLab/components/TabbedSqlEditors.jsx
index c7d6b24..6613ed8 100644
--- a/caravel/assets/javascripts/SqlLab/components/TabbedSqlEditors.jsx
+++ b/caravel/assets/javascripts/SqlLab/components/TabbedSqlEditors.jsx
@@ -15,13 +15,24 @@
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;
+ }
+ }
+ }
newQueryEditor() {
queryCount++;
- const dbId = (this.props.workspaceDatabase) ? this.props.workspaceDatabase.id : null;
+ const activeQueryEditor = this.activeQueryEditor();
+ console.log(activeQueryEditor);
const qe = {
id: shortid.generate(),
title: `Query ${queryCount}`,
- dbId,
+ dbId: (activeQueryEditor) ? activeQueryEditor.dbId : null,
+ schema: (activeQueryEditor) ? activeQueryEditor.schema : null,
autorun: false,
sql: 'SELECT ...',
};
@@ -49,7 +60,7 @@
<DropdownButton
bsSize="small"
id={'ddbtn-tab-' + i}
- className="no-shadow"
+ className="no-shadow tab-caret"
id="bg-vertical-dropdown-1"
>
<MenuItem eventKey="1" onClick={this.props.actions.removeQueryEditor.bind(this, qe)}>
@@ -92,7 +103,6 @@
queries: React.PropTypes.array,
queryEditors: React.PropTypes.array,
tabHistory: React.PropTypes.array,
- workspaceDatabase: React.PropTypes.object,
};
QueryEditors.defaultProps = {
tabHistory: [],
@@ -103,7 +113,6 @@
return {
queryEditors: state.queryEditors,
queries: state.queries,
- workspaceDatabase: state.workspaceDatabase,
tabHistory: state.tabHistory,
};
}
diff --git a/caravel/assets/javascripts/SqlLab/components/VisualizeModal.jsx b/caravel/assets/javascripts/SqlLab/components/VisualizeModal.jsx
index c0ecc90..05f5361 100644
--- a/caravel/assets/javascripts/SqlLab/components/VisualizeModal.jsx
+++ b/caravel/assets/javascripts/SqlLab/components/VisualizeModal.jsx
@@ -1,5 +1,5 @@
import React from 'react';
-import { Alert, Modal } from 'react-bootstrap';
+import { Alert, Button, Grid, Row, Col, Modal } from 'react-bootstrap';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
@@ -7,65 +7,145 @@
import Select from 'react-select';
import { Table } from 'reactable';
+import shortid from 'shortid';
+
+const $ = require('jquery');
class VisualizeModal extends React.Component {
constructor(props) {
super(props);
this.state = {
- chartType: null,
+ chartType: 'line',
+ datasourceName: shortid.generate(),
+ columns: {},
};
}
- changeChartType(event) {
- this.setState({ chartType: event.target.value });
+ 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,
+ };
+ window.open('/caravel/sqllab_viz/?' + $.param(vizOptions));
+ }
+ changeDatasourceName(event) {
+ this.setState({ datasourceName: event.target.value });
+ }
+ changeCheckbox(attr, col, event) {
+ console.log([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: option.value });
+ columns = Object.assign({}, columns, { [col]: column });
+ this.setState({ columns });
}
render() {
+ console.log(this.state);
if (!(this.props.query)) {
return <div />;
}
- const cols = this.props.query.results.columns;
+ 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 (mock)</Modal.Title>
+ <Modal.Title>
+ Visualize <span className="alert alert-danger">under construction</span>
+ </Modal.Title>
</Modal.Header>
<Modal.Body>
- <Alert bsStyle="danger">Not functional - Work in progress!</Alert>
- <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={this.state.chartType}
- 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 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>
diff --git a/caravel/assets/javascripts/SqlLab/index.jsx b/caravel/assets/javascripts/SqlLab/index.jsx
index af948e9..c7de02a 100644
--- a/caravel/assets/javascripts/SqlLab/index.jsx
+++ b/caravel/assets/javascripts/SqlLab/index.jsx
@@ -4,13 +4,16 @@
import React from 'react';
import { render } from 'react-dom';
+import { connect } from 'react-redux';
+import { bindActionCreators } from 'redux';
+import * as Actions from './actions';
import SplitPane from 'react-split-pane';
-
import { Label, Tab, Tabs } from 'react-bootstrap';
import LeftPane from './components/LeftPane';
import TabbedSqlEditors from './components/TabbedSqlEditors';
+import Alerts from './components/Alerts';
import { compose, createStore } from 'redux';
import { Provider } from 'react-redux';
@@ -25,11 +28,12 @@
// jquery hack to highlight the navbar menu
$('a[href="/caravel/sqllab"]').parent().addClass('active');
-const App = React.createClass({
+class App extends React.Component {
render() {
return (
<div className="App SqlLab">
<div className="container-fluid">
+ <Alerts alerts={this.props.alerts} />
<SplitPane split="vertical" minSize={200} defaultSize={300}>
<div className="pane-cell pane-west m-t-5">
<LeftPane />
@@ -41,8 +45,21 @@
</div>
</div>
);
- },
-});
+ }
+}
+
+function mapStateToProps(state) {
+ return {
+ alerts: state.alerts,
+ };
+}
+function mapDispatchToProps(dispatch) {
+ return {
+ actions: bindActionCreators(Actions, dispatch),
+ };
+}
+
+App = connect(mapStateToProps, mapDispatchToProps)(App);
render(
<Provider store={store}>
@@ -50,3 +67,4 @@
</Provider>,
document.getElementById('app')
);
+
diff --git a/caravel/assets/javascripts/SqlLab/main.css b/caravel/assets/javascripts/SqlLab/main.css
index cfab266..e8bbb2f 100644
--- a/caravel/assets/javascripts/SqlLab/main.css
+++ b/caravel/assets/javascripts/SqlLab/main.css
@@ -250,3 +250,7 @@
padding-bottom: 3px;
padding-top: 3px;
}
+button.tab-caret {
+ padding: 5px !important;
+ border-color: transparent;
+}
diff --git a/caravel/assets/javascripts/SqlLab/reducers.js b/caravel/assets/javascripts/SqlLab/reducers.js
index 1c18fca..4636a84 100644
--- a/caravel/assets/javascripts/SqlLab/reducers.js
+++ b/caravel/assets/javascripts/SqlLab/reducers.js
@@ -12,11 +12,12 @@
};
export const initialState = {
- queryEditors: [defaultQueryEditor],
+ alerts: [],
queries: [],
+ queryEditors: [defaultQueryEditor],
+ tabHistory: [defaultQueryEditor.id],
tables: [],
workspaceQueries: [],
- tabHistory: [defaultQueryEditor.id],
};
@@ -46,6 +47,9 @@
}
function addToArr(state, arrKey, obj) {
+ if (!(obj.id)) {
+ obj.id = shortid.generate();
+ }
const newState = {};
newState[arrKey] = [...state[arrKey], Object.assign({}, obj)];
return Object.assign({}, state, newState);
@@ -137,6 +141,12 @@
[actions.REMOVE_WORKSPACE_QUERY]() {
return removeFromArr(state, 'workspaceQueries', action.query);
},
+ [actions.ADD_ALERT]() {
+ return addToArr(state, 'alerts', action.alert);
+ },
+ [actions.REMOVE_ALERT]() {
+ return removeFromArr(state, 'alerts', action.alert);
+ },
};
if (action.type in actionHandlers) {
return actionHandlers[action.type]();
diff --git a/caravel/assets/package.json b/caravel/assets/package.json
index 81078b7..cddfc85 100644
--- a/caravel/assets/package.json
+++ b/caravel/assets/package.json
@@ -8,7 +8,7 @@
},
"scripts": {
"test": "npm run lint && mocha --compilers js:babel-core/register --required spec/helpers/browser.js spec/**/*_spec.*",
- "dev": "NODE_ENV=dev webpack -d --watch --colors",
+ "dev": "NODE_ENV=dev webpack -d --watch --colors --progress",
"prod": "NODE_ENV=production webpack -p --colors --progress",
"lint": "npm run --silent lint:js",
"lint:js": "eslint --ignore-path=.eslintignore --ext .js ."
diff --git a/caravel/config.py b/caravel/config.py
index 869dd4c..fd2062e 100644
--- a/caravel/config.py
+++ b/caravel/config.py
@@ -201,6 +201,9 @@
"""
CELERY_CONFIG = None
+# The db id here results in selecting this one as a default in SQL Lab
+DEFAULT_DB_ID = None
+
try:
from caravel_config import * # noqa
except ImportError:
diff --git a/caravel/views.py b/caravel/views.py
index 2fdc418..22f6b8f 100755
--- a/caravel/views.py
+++ b/caravel/views.py
@@ -1259,6 +1259,12 @@
dash_edit_perm=dash_edit_perm)
@has_access
+ @expose("/sqllab_viz/")
+ @log_this
+ def sqllab_viz(self):
+ return json.dumps(request.args.to_dict(), indent=4)
+
+ @has_access
@expose("/sql/<database_id>/")
@log_this
def sql(self, database_id):