From ab43bbbc2115bbee6847e8f3a1eda0aaeb5db826 Mon Sep 17 00:00:00 2001 From: Maxime Beauchemin Date: Tue, 16 Aug 2016 21:00:23 -0700 Subject: [PATCH] More goodness --- caravel/assets/javascripts/SqlLab/TODO.md | 16 +- caravel/assets/javascripts/SqlLab/actions.js | 10 ++ .../javascripts/SqlLab/components/Alerts.jsx | 40 +++++ .../SqlLab/components/LeftPane.jsx | 8 +- .../SqlLab/components/SqlEditor.jsx | 17 +- .../SqlLab/components/SqlEditorTopToolbar.jsx | 49 ++---- .../SqlLab/components/TabbedSqlEditors.jsx | 19 +- .../SqlLab/components/VisualizeModal.jsx | 162 +++++++++++++----- caravel/assets/javascripts/SqlLab/index.jsx | 26 ++- caravel/assets/javascripts/SqlLab/main.css | 4 + caravel/assets/javascripts/SqlLab/reducers.js | 14 +- caravel/assets/package.json | 2 +- caravel/config.py | 3 + caravel/views.py | 6 + 14 files changed, 265 insertions(+), 111 deletions(-) create mode 100644 caravel/assets/javascripts/SqlLab/components/Alerts.jsx diff --git a/caravel/assets/javascripts/SqlLab/TODO.md b/caravel/assets/javascripts/SqlLab/TODO.md index 8a83c57c463..b8af8bab43e 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 a2361368d11..0c2d3b9f131 100644 --- a/caravel/assets/javascripts/SqlLab/actions.js +++ b/caravel/assets/javascripts/SqlLab/actions.js @@ -20,6 +20,8 @@ export const SET_WORKSPACE_DB = 'SET_WORKSPACE_DB'; 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 @@ export function addQueryEditor(queryEditor) { 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 00000000000..6ba16f5e393 --- /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.msg} + + + ); + return ( +
{alerts}
+ ); + } +} + +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 cebeb282880..0cccdb1ce2b 100644 --- a/caravel/assets/javascripts/SqlLab/components/LeftPane.jsx +++ b/caravel/assets/javascripts/SqlLab/components/LeftPane.jsx @@ -4,8 +4,8 @@ import { connect } from 'react-redux'; 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 @@ const LeftPane = (props) => { + diff --git a/caravel/assets/javascripts/SqlLab/components/SqlEditor.jsx b/caravel/assets/javascripts/SqlLab/components/SqlEditor.jsx index 3746e11f5a0..5e35a1792fa 100644 --- a/caravel/assets/javascripts/SqlLab/components/SqlEditor.jsx +++ b/caravel/assets/javascripts/SqlLab/components/SqlEditor.jsx @@ -46,25 +46,12 @@ class SqlEditor extends React.Component { 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 @@ class SqlEditor extends React.Component { }; 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 87948d42a94..3e42365e906 100644 --- a/caravel/assets/javascripts/SqlLab/components/SqlEditorTopToolbar.jsx +++ b/caravel/assets/javascripts/SqlLab/components/SqlEditorTopToolbar.jsx @@ -29,19 +29,6 @@ class SqlEditorTopToolbar extends React.Component { 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 @@ class SqlEditorTopToolbar extends React.Component { 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 @@ class SqlEditorTopToolbar extends React.Component { 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 @@ class SqlEditorTopToolbar extends React.Component { } 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 @@ class SqlEditorTopToolbar extends React.Component { } 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 c7d6b2478bc..6613ed82cd6 100644 --- a/caravel/assets/javascripts/SqlLab/components/TabbedSqlEditors.jsx +++ b/caravel/assets/javascripts/SqlLab/components/TabbedSqlEditors.jsx @@ -15,13 +15,24 @@ class QueryEditors extends React.Component { 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 @@ class QueryEditors extends React.Component { @@ -92,7 +103,6 @@ QueryEditors.propTypes = { queries: React.PropTypes.array, queryEditors: React.PropTypes.array, tabHistory: React.PropTypes.array, - workspaceDatabase: React.PropTypes.object, }; QueryEditors.defaultProps = { tabHistory: [], @@ -103,7 +113,6 @@ function mapStateToProps(state) { 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 c0ecc902d1a..05f53613a27 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 * as Actions from '../actions'; 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
; } - const cols = this.props.query.results.columns; + const tableData = this.props.query.results.columns.map((col) => ({ + column: col, + is_dimension: ( + + ), + is_date: ( + + ), + agg_func: ( + - ({ - column: col, - is_dimension: , - is_date: , - agg_func: ( - + + + Datasource Name + + +
+
+ diff --git a/caravel/assets/javascripts/SqlLab/index.jsx b/caravel/assets/javascripts/SqlLab/index.jsx index af948e98850..c7de02a5cd0 100644 --- a/caravel/assets/javascripts/SqlLab/index.jsx +++ b/caravel/assets/javascripts/SqlLab/index.jsx @@ -4,13 +4,16 @@ require('bootstrap'); 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 @@ let store = createStore(sqlLabReducer, initialState, compose(persistState(), win // 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 (
+
@@ -41,8 +45,21 @@ const App = React.createClass({
); - }, -}); + } +} + +function mapStateToProps(state) { + return { + alerts: state.alerts, + }; +} +function mapDispatchToProps(dispatch) { + return { + actions: bindActionCreators(Actions, dispatch), + }; +} + +App = connect(mapStateToProps, mapDispatchToProps)(App); render( @@ -50,3 +67,4 @@ render( , document.getElementById('app') ); + diff --git a/caravel/assets/javascripts/SqlLab/main.css b/caravel/assets/javascripts/SqlLab/main.css index cfab266deb0..e8bbb2f4489 100644 --- a/caravel/assets/javascripts/SqlLab/main.css +++ b/caravel/assets/javascripts/SqlLab/main.css @@ -250,3 +250,7 @@ div.tablePopover:hover { 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 1c18fca6bba..4636a8445ba 100644 --- a/caravel/assets/javascripts/SqlLab/reducers.js +++ b/caravel/assets/javascripts/SqlLab/reducers.js @@ -12,11 +12,12 @@ const defaultQueryEditor = { }; export const initialState = { - queryEditors: [defaultQueryEditor], + alerts: [], queries: [], + queryEditors: [defaultQueryEditor], + tabHistory: [defaultQueryEditor.id], tables: [], workspaceQueries: [], - tabHistory: [defaultQueryEditor.id], }; @@ -46,6 +47,9 @@ function removeFromArr(state, arrKey, obj, idKey = 'id') { } 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 @@ export const sqlLabReducer = function (state, action) { [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 81078b72564..cddfc853fb9 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 869dd4c35f9..fd2062e68ec 100644 --- a/caravel/config.py +++ b/caravel/config.py @@ -201,6 +201,9 @@ CELERY_CONFIG = CeleryConfig """ 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 2fdc418946e..22f6b8fc1be 100755 --- a/caravel/views.py +++ b/caravel/views.py @@ -1258,6 +1258,12 @@ class Caravel(BaseCaravelView): dash_save_perm=dash_save_perm, 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//") @log_this