From 78b9d7b978a14819abb17c80df259a32e34ffc9e Mon Sep 17 00:00:00 2001 From: Maxime Beauchemin Date: Sat, 21 May 2016 22:07:37 -0700 Subject: [PATCH 1/7] 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. --- caravel/assets/javascripts/SqlLab/TODO.md | 24 ++ caravel/assets/javascripts/SqlLab/actions.js | 102 +++++++ .../SqlLab/components/ButtonWithTooltip.jsx | 48 +++ .../SqlLab/components/LeftPane.jsx | 71 +++++ .../javascripts/SqlLab/components/Link.jsx | 52 ++++ .../SqlLab/components/QueryLink.jsx | 60 ++++ .../SqlLab/components/QueryLog.jsx | 51 ++++ .../SqlLab/components/QuerySearch.jsx | 74 +++++ .../SqlLab/components/QueryTable.jsx | 170 +++++++++++ .../SqlLab/components/ResultSet.jsx | 54 ++++ .../SqlLab/components/SouthPane.jsx | 44 +++ .../SqlLab/components/SqlEditor.jsx | 233 +++++++++++++++ .../SqlLab/components/SqlEditorTopToolbar.jsx | 280 ++++++++++++++++++ .../SqlLab/components/TabbedSqlEditors.jsx | 116 ++++++++ .../SqlLab/components/TableMetadata.jsx | 23 ++ .../components/TableWorkspaceElement.jsx | 103 +++++++ .../javascripts/SqlLab/components/Timer.jsx | 62 ++++ caravel/assets/javascripts/SqlLab/index.jsx | 52 ++++ caravel/assets/javascripts/SqlLab/main.css | 249 ++++++++++++++++ caravel/assets/javascripts/SqlLab/reducers.js | 145 +++++++++ .../javascripts/dashboard/Dashboard.jsx | 1 + caravel/assets/package.json | 19 +- .../assets/stylesheets/less/variables.less | 4 +- caravel/assets/webpack.config.js | 1 + caravel/models.py | 51 +++- caravel/templates/caravel/basic.html | 2 +- caravel/utils.py | 13 +- caravel/views.py | 104 +++++-- tests/core_tests.py | 4 + 29 files changed, 2175 insertions(+), 37 deletions(-) create mode 100644 caravel/assets/javascripts/SqlLab/TODO.md create mode 100644 caravel/assets/javascripts/SqlLab/actions.js create mode 100644 caravel/assets/javascripts/SqlLab/components/ButtonWithTooltip.jsx create mode 100644 caravel/assets/javascripts/SqlLab/components/LeftPane.jsx create mode 100644 caravel/assets/javascripts/SqlLab/components/Link.jsx create mode 100644 caravel/assets/javascripts/SqlLab/components/QueryLink.jsx create mode 100644 caravel/assets/javascripts/SqlLab/components/QueryLog.jsx create mode 100644 caravel/assets/javascripts/SqlLab/components/QuerySearch.jsx create mode 100644 caravel/assets/javascripts/SqlLab/components/QueryTable.jsx create mode 100644 caravel/assets/javascripts/SqlLab/components/ResultSet.jsx create mode 100644 caravel/assets/javascripts/SqlLab/components/SouthPane.jsx create mode 100644 caravel/assets/javascripts/SqlLab/components/SqlEditor.jsx create mode 100644 caravel/assets/javascripts/SqlLab/components/SqlEditorTopToolbar.jsx create mode 100644 caravel/assets/javascripts/SqlLab/components/TabbedSqlEditors.jsx create mode 100644 caravel/assets/javascripts/SqlLab/components/TableMetadata.jsx create mode 100644 caravel/assets/javascripts/SqlLab/components/TableWorkspaceElement.jsx create mode 100644 caravel/assets/javascripts/SqlLab/components/Timer.jsx create mode 100644 caravel/assets/javascripts/SqlLab/index.jsx create mode 100644 caravel/assets/javascripts/SqlLab/main.css create mode 100644 caravel/assets/javascripts/SqlLab/reducers.js diff --git a/caravel/assets/javascripts/SqlLab/TODO.md b/caravel/assets/javascripts/SqlLab/TODO.md new file mode 100644 index 0000000000000..8a83c57c46302 --- /dev/null +++ b/caravel/assets/javascripts/SqlLab/TODO.md @@ -0,0 +1,24 @@ +# Design +* Query Log, search, filter on active tab only +* Where to make the limit clear? + +# TODO +* collapse sql beyond 10 lines +* add [Visualize] icon to modal +* Security per-database +* Overwrite workspace query +* Async +* Refactor timer in to its own thing + + +## 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 + +# PROJECT +* Write Runbook +* Confirm backups +* merge chef branch diff --git a/caravel/assets/javascripts/SqlLab/actions.js b/caravel/assets/javascripts/SqlLab/actions.js new file mode 100644 index 0000000000000..eb2b6d511bd42 --- /dev/null +++ b/caravel/assets/javascripts/SqlLab/actions.js @@ -0,0 +1,102 @@ +export const RESET_STATE = 'RESET_STATE'; +export const ADD_QUERY_EDITOR = 'ADD_QUERY_EDITOR'; +export const REMOVE_QUERY_EDITOR = 'REMOVE_QUERY_EDITOR'; +export const ADD_TABLE = 'ADD_TABLE'; +export const REMOVE_TABLE = 'REMOVE_TABLE'; +export const START_QUERY = 'START_QUERY'; +export const STOP_QUERY = 'STOP_QUERY'; +export const END_QUERY = 'END_QUERY'; +export const REMOVE_QUERY = 'REMOVE_QUERY'; +export const EXPAND_TABLE = 'EXPAND_TABLE'; +export const COLLAPSE_TABLE = 'COLLAPSE_TABLE'; +export const QUERY_SUCCESS = 'QUERY_SUCCESS'; +export const QUERY_FAILED = 'QUERY_FAILED'; +export const QUERY_EDITOR_SETDB = 'QUERY_EDITOR_SETDB'; +export const QUERY_EDITOR_SET_SCHEMA = 'QUERY_EDITOR_SET_SCHEMA'; +export const QUERY_EDITOR_SET_TITLE = 'QUERY_EDITOR_SET_TITLE'; +export const QUERY_EDITOR_SET_AUTORUN = 'QUERY_EDITOR_SET_AUTORUN'; +export const QUERY_EDITOR_SET_SQL = 'QUERY_EDITOR_SET_SQL'; +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 function resetState() { + return { type: RESET_STATE }; +} + +export function addQueryEditor(queryEditor) { + return { type: ADD_QUERY_EDITOR, queryEditor }; +} + +export function setActiveQueryEditor(queryEditor) { + return { type: SET_ACTIVE_QUERY_EDITOR, queryEditor }; +} + +export function removeQueryEditor(queryEditor) { + return { type: REMOVE_QUERY_EDITOR, queryEditor }; +} + +export function removeQuery(query) { + return { type: REMOVE_QUERY, query }; +} + +export function queryEditorSetDb(queryEditor, dbId) { + return { type: QUERY_EDITOR_SETDB, queryEditor, dbId }; +} + +export function queryEditorSetSchema(queryEditor, schema) { + return { type: QUERY_EDITOR_SET_SCHEMA, queryEditor, schema }; +} + +export function queryEditorSetAutorun(queryEditor, autorun) { + return { type: QUERY_EDITOR_SET_AUTORUN, queryEditor, autorun }; +} + +export function queryEditorSetTitle(queryEditor, title) { + return { type: QUERY_EDITOR_SET_TITLE, queryEditor, title }; +} + +export function queryEditorSetSql(queryEditor, sql) { + return { type: QUERY_EDITOR_SET_SQL, queryEditor, sql }; +} + +export function addTable(table) { + return { type: ADD_TABLE, table }; +} + +export function expandTable(table) { + return { type: EXPAND_TABLE, table }; +} + +export function collapseTable(table) { + return { type: COLLAPSE_TABLE, table }; +} + +export function removeTable(table) { + return { type: REMOVE_TABLE, table }; +} + +export function startQuery(query) { + return { type: START_QUERY, query }; +} + +export function stopQuery(query) { + return { type: STOP_QUERY, query }; +} + +export function querySuccess(query, results) { + return { type: QUERY_SUCCESS, query, results }; +} + +export function queryFailed(query, msg) { + return { type: QUERY_FAILED, query, msg }; +} + +export function addWorkspaceQuery(query) { + return { type: ADD_WORKSPACE_QUERY, query }; +} + +export function removeWorkspaceQuery(query) { + return { type: REMOVE_WORKSPACE_QUERY, query }; +} diff --git a/caravel/assets/javascripts/SqlLab/components/ButtonWithTooltip.jsx b/caravel/assets/javascripts/SqlLab/components/ButtonWithTooltip.jsx new file mode 100644 index 0000000000000..36220e6b4ff0c --- /dev/null +++ b/caravel/assets/javascripts/SqlLab/components/ButtonWithTooltip.jsx @@ -0,0 +1,48 @@ +import React from 'react'; +import { Button, OverlayTrigger, Tooltip } from 'react-bootstrap'; + + +class ButtonWithTooltip extends React.Component { + render() { + let tooltip = ( + + {this.props.tooltip} + + ); + return ( + + + + ); + } +} +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; diff --git a/caravel/assets/javascripts/SqlLab/components/LeftPane.jsx b/caravel/assets/javascripts/SqlLab/components/LeftPane.jsx new file mode 100644 index 0000000000000..27e441aac11bf --- /dev/null +++ b/caravel/assets/javascripts/SqlLab/components/LeftPane.jsx @@ -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) => ); + } else { + queryElements = ( + + Use the save button on the SQL editor to save a query into this section for + future reference + + ); + } + return ( +
+
+
+ + SQL Lab +
+
+
+
+
+ + + + Saved Queries +
+
+ {queryElements} +
+
+ +
+
+
+ ); + } +} +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); diff --git a/caravel/assets/javascripts/SqlLab/components/Link.jsx b/caravel/assets/javascripts/SqlLab/components/Link.jsx new file mode 100644 index 0000000000000..7c88b5a929cab --- /dev/null +++ b/caravel/assets/javascripts/SqlLab/components/Link.jsx @@ -0,0 +1,52 @@ +import React from 'react'; +import { OverlayTrigger, Tooltip } from 'react-bootstrap'; + + +class Link extends React.Component { + render() { + let tooltip = ( + + {this.props.tooltip} + + ); + const link = ( + + {this.props.children} + + ); + if (this.props.tooltip) { + return ( + + {link} + + ); + } + 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; diff --git a/caravel/assets/javascripts/SqlLab/components/QueryLink.jsx b/caravel/assets/javascripts/SqlLab/components/QueryLink.jsx new file mode 100644 index 0000000000000..26a5d39bead7e --- /dev/null +++ b/caravel/assets/javascripts/SqlLab/components/QueryLink.jsx @@ -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 ( +
+ {this.props.query.title} + + + + +
+ ); + } +} + +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); + diff --git a/caravel/assets/javascripts/SqlLab/components/QueryLog.jsx b/caravel/assets/javascripts/SqlLab/components/QueryLog.jsx new file mode 100644 index 0000000000000..bc83a4627c6b6 --- /dev/null +++ b/caravel/assets/javascripts/SqlLab/components/QueryLog.jsx @@ -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 ( + + ); + } + return ( + + No query history yet... + + ); + } +} +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); diff --git a/caravel/assets/javascripts/SqlLab/components/QuerySearch.jsx b/caravel/assets/javascripts/SqlLab/components/QuerySearch.jsx new file mode 100644 index 0000000000000..e0352f9363cb4 --- /dev/null +++ b/caravel/assets/javascripts/SqlLab/components/QuerySearch.jsx @@ -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 ( + +
+
+
+
+ Search Queries +
+
+
+ + + ({ + column: col, + is_dimension: , + is_date: , + agg_func: ( +
+ + ); + } +} +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); diff --git a/caravel/assets/javascripts/SqlLab/components/ResultSet.jsx b/caravel/assets/javascripts/SqlLab/components/ResultSet.jsx new file mode 100644 index 0000000000000..c388872321796 --- /dev/null +++ b/caravel/assets/javascripts/SqlLab/components/ResultSet.jsx @@ -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 =
; + if (this.props.showControls) { + controls = ( +
+
+
+ + +
+
+ +
+
+
+ ); + } + if (results.data.length > 0) { + return ( +
+ {controls} +
+ + ); + } + return (The query returned no data); + } +} +ResultSet.propTypes = { + query: React.PropTypes.object, + showControls: React.PropTypes.boolean, + search: React.PropTypes.boolean, +}; +ResultSet.defaultProps = { + showControls: true, + search: true, +}; + +export default ResultSet; diff --git a/caravel/assets/javascripts/SqlLab/components/SouthPane.jsx b/caravel/assets/javascripts/SqlLab/components/SouthPane.jsx new file mode 100644 index 0000000000000..d193ab9c76b86 --- /dev/null +++ b/caravel/assets/javascripts/SqlLab/components/SouthPane.jsx @@ -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 = ( + Loading.. + ); + } else if (this.props.latestQuery.state === 'failed') { + results =
{this.props.latestQuery.msg}
; + } else if (this.props.latestQuery.state === 'success') { + results = ; + } + } else { + results =
Run a query to display results here
; + } + return ( + + +
+ {results} +
+
+ + + +
+ ); + } +} + +SouthPane.propTypes = { + latestQuery: React.PropTypes.object, +}; + +SouthPane.defaultProps = { +}; + +export default SouthPane; diff --git a/caravel/assets/javascripts/SqlLab/components/SqlEditor.jsx b/caravel/assets/javascripts/SqlLab/components/SqlEditor.jsx new file mode 100644 index 0000000000000..ba097a00d959f --- /dev/null +++ b/caravel/assets/javascripts/SqlLab/components/SqlEditor.jsx @@ -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 = ( + + + + ); + if (this.props.latestQuery && this.props.latestQuery.state === 'running') { + runButtons = ( + + + + ); + } + const rightButtons = ( + + +   + + }> + + export to .csv + + + export to .json + + + + ); + let limitWarning = null; + const row_limit = 1000; + if (this.props.latestQuery && this.props.latestQuery.rows === row_limit) { + const tooltip = ( + + It appears that the number of rows in the query results displayed + was limited on the server side to the {row_limit} limit. + + ); + limitWarning = ( + + + + ); + } + const editorBottomBar = ( +
+
+ {runButtons} + + + +
+
+ {limitWarning} + + {rightButtons} +
+
+ ); + return ( +
+
+
+ + + {editorBottomBar} +
+ +
+
+
+
+ ); + } +} + +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); diff --git a/caravel/assets/javascripts/SqlLab/components/SqlEditorTopToolbar.jsx b/caravel/assets/javascripts/SqlLab/components/SqlEditorTopToolbar.jsx new file mode 100644 index 0000000000000..0190241d3386c --- /dev/null +++ b/caravel/assets/javascripts/SqlLab/components/SqlEditorTopToolbar.jsx @@ -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) => ( +
+
{col.name}
+
{col.type}
+
+ )); + } + const popoverId = 'tblPopover_' + table.name; + const popoverTop = ( +
+
+ + +
+
+ +
+
+ ); + const popover = ( + + {cols} + + ); + return ( + + ); + }); + return ( +
+
+ +
+
+ -
({ - column: col, - is_dimension: , - is_date: , - agg_func: ( -
@@ -27,14 +55,24 @@ class ResultSet extends React.Component { } if (results.data.length > 0) { return ( -
- {controls} -
+ + {controls} +
+
+ ); } @@ -45,10 +83,12 @@ ResultSet.propTypes = { query: React.PropTypes.object, showControls: React.PropTypes.boolean, search: React.PropTypes.boolean, + searchText: React.PropTypes.string, }; ResultSet.defaultProps = { showControls: true, search: true, + searchText: '', }; export default ResultSet; diff --git a/caravel/assets/javascripts/SqlLab/components/SouthPane.jsx b/caravel/assets/javascripts/SqlLab/components/SouthPane.jsx index d193ab9c76b86..5631872305fbc 100644 --- a/caravel/assets/javascripts/SqlLab/components/SouthPane.jsx +++ b/caravel/assets/javascripts/SqlLab/components/SouthPane.jsx @@ -1,38 +1,36 @@ import { Tab, Tabs } from 'react-bootstrap'; -import QueryLog from './QueryLog'; +import QueryHistory from './QueryHistory'; 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 = ( - Loading.. - ); - } else if (this.props.latestQuery.state === 'failed') { - results =
{this.props.latestQuery.msg}
; - } else if (this.props.latestQuery.state === 'success') { - results = ; - } - } else { - results =
Run a query to display results here
; +const SouthPane = (props) => { + let results; + if (props.latestQuery) { + if (props.latestQuery.state === 'running') { + results = ( + Loading.. + ); + } else if (props.latestQuery.state === 'failed') { + results =
{props.latestQuery.msg}
; + } else if (props.latestQuery.state === 'success') { + results = ; } - return ( - - -
- {results} -
-
- - - -
- ); + } else { + results =
Run a query to display results here
; } -} + return ( + + +
+ {results} +
+
+ + + +
+ ); +}; SouthPane.propTypes = { latestQuery: React.PropTypes.object, diff --git a/caravel/assets/javascripts/SqlLab/components/SqlEditor.jsx b/caravel/assets/javascripts/SqlLab/components/SqlEditor.jsx index ba097a00d959f..3746e11f5a043 100644 --- a/caravel/assets/javascripts/SqlLab/components/SqlEditor.jsx +++ b/caravel/assets/javascripts/SqlLab/components/SqlEditor.jsx @@ -1,6 +1,14 @@ const $ = window.$ = require('jquery'); import React from 'react'; -import { Button, ButtonGroup, DropdownButton, Label, MenuItem, OverlayTrigger, Tooltip } from 'react-bootstrap'; +import { + Button, + ButtonGroup, + DropdownButton, + Label, + MenuItem, + OverlayTrigger, + Tooltip, +} from 'react-bootstrap'; import AceEditor from 'react-ace'; import 'brace/mode/sql'; @@ -29,6 +37,9 @@ class SqlEditor extends React.Component { }; } componentDidMount() { + this.onMount(); + } + onMount() { if (this.state.autorun) { this.setState({ autorun: false }); this.props.actions.queryEditorSetAutorun(this.props.queryEditor, false); @@ -94,12 +105,9 @@ class SqlEditor extends React.Component { this.props.actions.stopQuery(this.props.latestQuery); } textChange(text) { - this.setState({ sql: 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(), @@ -152,12 +160,12 @@ class SqlEditor extends React.Component { ); let limitWarning = null; - const row_limit = 1000; - if (this.props.latestQuery && this.props.latestQuery.rows === row_limit) { + const rowLimit = 1000; + if (this.props.latestQuery && this.props.latestQuery.rows === rowLimit) { const tooltip = ( It appears that the number of rows in the query results displayed - was limited on the server side to the {row_limit} limit. + was limited on the server side to the {rowLimit} limit. ); limitWarning = ( @@ -219,9 +227,8 @@ SqlEditor.propTypes = { SqlEditor.defaultProps = { }; -function mapStateToProps(state) { - return { - }; +function mapStateToProps() { + return {}; } function mapDispatchToProps(dispatch) { diff --git a/caravel/assets/javascripts/SqlLab/components/SqlEditorTopToolbar.jsx b/caravel/assets/javascripts/SqlLab/components/SqlEditorTopToolbar.jsx index 0190241d3386c..87948d42a9446 100644 --- a/caravel/assets/javascripts/SqlLab/components/SqlEditorTopToolbar.jsx +++ b/caravel/assets/javascripts/SqlLab/components/SqlEditorTopToolbar.jsx @@ -123,9 +123,6 @@ class SqlEditorTopToolbar extends React.Component { that.setState({ databaseLoading: false }); }); } - notImplemented() { - alert('Not implemented'); - } closePopover(ref) { this.refs[ref].hide(); } diff --git a/caravel/assets/javascripts/SqlLab/components/TabbedSqlEditors.jsx b/caravel/assets/javascripts/SqlLab/components/TabbedSqlEditors.jsx index 76186d11e5d56..c7d6b2478bc69 100644 --- a/caravel/assets/javascripts/SqlLab/components/TabbedSqlEditors.jsx +++ b/caravel/assets/javascripts/SqlLab/components/TabbedSqlEditors.jsx @@ -35,10 +35,9 @@ class QueryEditors extends React.Component { } } render() { - const that = this; const editors = this.props.queryEditors.map((qe, i) => { let latestQuery; - that.props.queries.forEach((q) => { + this.props.queries.forEach((q) => { if (q.id === qe.latestQueryId) { latestQuery = q; } @@ -53,10 +52,10 @@ class QueryEditors extends React.Component { className="no-shadow" id="bg-vertical-dropdown-1" > - + close tab - + rename tab @@ -90,8 +89,9 @@ class QueryEditors extends React.Component { } QueryEditors.propTypes = { actions: React.PropTypes.object, - tabHistory: React.PropTypes.array, + queries: React.PropTypes.array, queryEditors: React.PropTypes.array, + tabHistory: React.PropTypes.array, workspaceDatabase: React.PropTypes.object, }; QueryEditors.defaultProps = { diff --git a/caravel/assets/javascripts/SqlLab/components/TableWorkspaceElement.jsx b/caravel/assets/javascripts/SqlLab/components/TableWorkspaceElement.jsx index e552c8b54cd81..d43229ad8ccfd 100644 --- a/caravel/assets/javascripts/SqlLab/components/TableWorkspaceElement.jsx +++ b/caravel/assets/javascripts/SqlLab/components/TableWorkspaceElement.jsx @@ -12,10 +12,9 @@ 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) { + this.props.table.columns.forEach((col, i) => { cols += col.name; - if (i < that.props.table.columns.length - 1) { + if (i < this.props.table.columns.length - 1) { cols += ', '; } }); diff --git a/caravel/assets/javascripts/SqlLab/components/VisualizeModal.jsx b/caravel/assets/javascripts/SqlLab/components/VisualizeModal.jsx new file mode 100644 index 0000000000000..c0ecc902d1aab --- /dev/null +++ b/caravel/assets/javascripts/SqlLab/components/VisualizeModal.jsx @@ -0,0 +1,93 @@ +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 { Table } from 'reactable'; + +class VisualizeModal extends React.Component { + constructor(props) { + super(props); + this.state = { + chartType: null, + }; + } + changeChartType(event) { + this.setState({ chartType: event.target.value }); + } + render() { + if (!(this.props.query)) { + return
; + } + const cols = this.props.query.results.columns; + const modal = ( +
+ + + Visualize (mock) + + + Not functional - Work in progress! +
+
({ + column: col, + is_dimension: , + is_date: , + agg_func: ( +