blob: 49b999f4bbc91be62ad5a31eaf8846d44912280c [file] [log] [blame]
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import ClassNames from 'classnames';
import CodeMirror from 'codemirror';
import assign from 'object-assign';
import _ from 'lodash';
import 'codemirror/lib/codemirror.css';
import 'codemirror/addon/edit/matchbrackets.js';
import 'codemirror/addon/hint/sql-hint.js';
import 'codemirror/addon/hint/show-hint.css';
import 'codemirror/addon/hint/show-hint.js';
import 'codemirror/mode/sql/sql.js';
import UserStore from '../stores/UserStore';
import AdhocQueryActions from '../actions/AdhocQueryActions';
import AdhocQueryStore from '../stores/AdhocQueryStore';
import CubeStore from '../stores/CubeStore';
import TableStore from '../stores/TableStore';
import DatabaseStore from '../stores/DatabaseStore';
import SavedQueryStore from '../stores/SavedQueryStore';
import QueryParams from './QueryParamsComponent';
import Config from 'config.json';
import '../styles/css/query-component.css';
// keeping a handle to CodeMirror instance,
// to be used to retrieve the contents of the editor
let codeMirror = null;
let codeMirrorHints = {};
// list of possible client messages
let clientMessages = {
runQuery: 'Running your query...',
saveQuery: 'Saving your query...',
noName: 'Name is mandatory for a saved query.',
updateQuery: 'Updating saved query...'
};
function setLimit (query) {
// since pagination is not enabled on server, limit the query to 1000
// check if user has specified existing limit > 1000, change it to 1000
// dumb way, checking only last two words for `limit <number>` pattern
let temp = query.split(' ');
if (temp.slice(-2)[0].toLowerCase() === 'limit') {
if (temp.slice(-1)[0] > 1000) temp.splice(-1, 1, 1000);
query = temp.join(' ');
} else {
query += ' limit 1000';
}
return query;
}
function setCode (code) {
if (codeMirror) {
codeMirror.setValue(code);
codeMirror.focus();
}
}
function getEmptyState () {
return {
clientMessage: null, // to give user instant ack
isRunQueryDisabled: true,
serverMessage: null, // type (success or error), text as keys
isCollapsed: false,
params: null,
isModeEdit: false,
savedQueryId: null,
runImmediately: false,
description: ''
};
}
// used to populate the query box when user wants to edit a query
// TODO improve this.
// this takes in the query handle and writes the query
// used from Edit Query link
function fetchQueryForEdit (props) {
let query = AdhocQueryStore.getQueries()[props.query.handle];
if (query) {
setCode(query.userQuery);
}
}
function setupCodeMirror (domNode) {
// instantiating CodeMirror intance with some properties.
codeMirror = CodeMirror.fromTextArea(domNode, {
mode: 'text/x-sql',
indentWithTabs: true,
smartIndent: true,
lineNumbers: true,
matchBrackets: true,
autofocus: true,
lineWrapping: true
});
}
class QueryBox extends React.Component {
constructor (props) {
super(props);
this.runQuery = this.runQuery.bind(this);
this.saveQuery = this.saveQuery.bind(this);
this._onChange = this._onChange.bind(this);
this.toggle = this.toggle.bind(this);
this.closeParamBox = this.closeParamBox.bind(this);
this.saveParams = this.saveParams.bind(this);
this._onChangeSavedQueryStore = this._onChangeSavedQueryStore.bind(this);
this._getSavedQueryDetails = this._getSavedQueryDetails.bind(this);
this.cancel = this.cancel.bind(this);
this.saveOrUpdate = this.saveOrUpdate.bind(this);
this.runSavedQuery = this.runSavedQuery.bind(this);
this.state = getEmptyState();
}
componentDidMount () {
var editor = this.refs.queryEditor.getDOMNode();
setupCodeMirror(editor);
// disable 'Run Query' button when editor is empty
// TODO: debounce this, as it'll happen on every key press. :(
codeMirror.on('change', () => {
this.state.isRunQueryDisabled = !codeMirror.getValue();
this.setState(this.state);
this._onChange();
});
// to remove the previous query's submission notification
codeMirror.on('focus', () => {
this.setState({ clientMessage: null });
});
// add Cmd + Enter to fire runQuery
codeMirror.setOption('extraKeys', {
'Cmd-Enter': instance => { this.runQuery(); },
'Ctrl-Space': 'autocomplete',
'Ctrl-S': instance => { this.saveQuery(); }
});
AdhocQueryStore.addChangeListener(this._onChange);
CubeStore.addChangeListener(this._onChangeCubeStore);
TableStore.addChangeListener(this._onChangeTableStore);
SavedQueryStore.addChangeListener(this._onChangeSavedQueryStore);
}
componentWillUnmount () {
AdhocQueryStore.removeChangeListener(this._onChange);
CubeStore.removeChangeListener(this._onChangeCubeStore);
TableStore.removeChangeListener(this._onChangeTableStore);
SavedQueryStore.removeChangeListener(this._onChangeSavedQueryStore);
}
componentWillReceiveProps (props) {
// normal query
if (props.query && props.query.handle) {
fetchQueryForEdit(props);
// clear saved query state
this.setState({
params: null,
savedQueryId: null,
isModeEdit: false
});
// saved query
} else if (props.query && props.query.savedquery) {
let queryId = props.query.savedquery;
let savedQuery = SavedQueryStore.getSavedQueries()[queryId];
if (savedQuery) {
setCode(savedQuery.query);
this.refs.queryName.getDOMNode().value = savedQuery.name;
this.setState({
params: savedQuery.parameters,
savedQueryId: savedQuery.id,
description: savedQuery.description,
isModeEdit: true
});
}
}
}
render () {
let collapseClass = ClassNames({
'pull-right': true,
'glyphicon': true,
'glyphicon-chevron-up': !this.state.isCollapsed,
'glyphicon-chevron-down': this.state.isCollapsed
});
let panelBodyClassName = ClassNames({
'panel-body': true,
'hide': this.state.isCollapsed
});
let notificationClass = ClassNames({
'alert': true,
'alert-danger': this.state.serverMessage && this.state.serverMessage.type === 'Error',
'alert-success': this.state.serverMessage && this.state.serverMessage.type !== 'Error'
});
return (
<div className='panel panel-default'>
<div className='panel-heading'>
<h3 className='panel-title'>
{this.state.isModeEdit ? 'Edit' : 'Compose'}
<span className={collapseClass} onClick={this.toggle}></span>
</h3>
</div>
<div className={panelBodyClassName} style={{padding: '0px'}}>
<section>
<div style={{borderBottom: '1px solid #dddddd'}}>
<textarea ref='queryEditor'></textarea>
</div>
<div className='row' style={{padding: '6px 8px '}}>
<div className='col-lg-4 col-md-4 col-sm-4 col-xs-12'>
<input type='text' className='form-control'
placeholder='Query Name (optional)' ref='queryName'/>
</div>
<div className='col-lg-5 col-md-5 col-sm-5 col-xs-12'>
{this.state.clientMessage && (
<div className='alert alert-info' style={{padding: '5px 4px',
marginBottom: '0px'}}>
{this.state.clientMessage}
</div>
)}
</div>
<div className='col-lg-3 col-md-3 col-sm-3 col-xs-12'>
<button className='btn btn-default' style={{marginRight: '4px'}}
onClick={this.saveOrUpdate} disabled={this.state.isRunQueryDisabled}
title='Save'>
<i className='fa fa-save fa-lg'></i>
</button>
<button className='btn btn-default' title='Run'
onClick={this.runQuery} style={{marginRight: '4px'}}
disabled={this.state.isRunQueryDisabled}>
<i className='fa fa-play fa-lg'></i>
</button>
<button className='btn btn-default' onClick={this.cancel}
title='Clear'>
<i className='fa fa-ban fa-lg'></i>
</button>
</div>
</div>
{ this.state.params && !!this.state.params.length &&
<QueryParams params={this.state.params} close={this.closeParamBox}
saveParams={this.saveParams} description={this.state.description}/>
}
{ this.state.serverMessage &&
<div className={notificationClass} style={{marginBottom: '0px'}}>
{this.state.serverMessage.text}
{ this.state.serverMessage.texts &&
this.state.serverMessage.texts.map(e => {
return (
<li style={{listStyleType: 'none'}}>
<strong>{e.code}</strong>: <span>{e.message}</span>
</li>
);
})
}
</div>
}
</section>
</div>
</div>
);
}
saveOrUpdate () {
!this.state.isModeEdit ? this.saveQuery() : this.updateQuery();
}
runQuery () {
let queryName = this.refs.queryName.getDOMNode().value;
let secretToken = UserStore.getUserDetails().secretToken;
let query = codeMirror.getValue();
// set limit if mode is in-memory
if (!Config.isPersistent) query = setLimit(query);
AdhocQueryActions.runQuery(secretToken, query, queryName);
// show user the query was posted successfully and empty the queryName
this.setState({ clientMessage: clientMessages.runQuery });
this.refs.queryName.getDOMNode().value = '';
}
updateQuery (params) {
let query = this._getSavedQueryDetails(params);
if (!query) return;
var options = {
parameters: query.parameters,
description: query.description,
name: query.name
};
AdhocQueryActions
.updateSavedQuery(query.secretToken, query.user, query.query,
options, this.state.savedQueryId);
this.setState({
clientMessage: clientMessages.updateQuery,
runImmediately: params && params.runImmediately
});
}
saveQuery (params) {
let query = this._getSavedQueryDetails(params);
if (!query) return;
var options = {
parameters: query.parameters,
description: query.description,
name: query.name
};
AdhocQueryActions
.saveQuery(query.secretToken, query.user, query.query, options);
this.setState({
clientMessage: clientMessages.saveQuery,
runImmediately: params && params.runImmediately
});
}
// internal which is called during save saved query & edit saved query
_getSavedQueryDetails (params) {
let queryName = this.refs.queryName.getDOMNode().value;
if (!queryName) {
this.setState({clientMessage: clientMessages.noName});
return;
}
let secretToken = UserStore.getUserDetails().secretToken;
let user = UserStore.getUserDetails().email;
let query = codeMirror.getValue();
return {
secretToken: secretToken,
user: user,
query: query,
parameters: params && params.parameters,
description: params && params.description,
name: queryName
};
}
_onChange (hash) { // can be error/success OR it can be saved query params
if (hash && hash.type) {
this.setState({serverMessage: hash, clientMessage: null});
if (hash.type === 'Error') return;
} else {
this.setState({serverMessage: null});
}
// renders the detail result component if server
// replied with a query handle.
// this should ideally happen only when the 'Run Query' button is
// clicked, and its action updates the store with query-handle.
let handle = AdhocQueryStore.getQueryHandle();
if (handle) {
this.setState({ clientMessage: null });
var { router } = this.context;
router.transitionTo('result', {handle: handle});
}
}
_onChangeCubeStore () {
// cubes
let cubes = CubeStore.getCubes(UserStore.currentDatabase()); // hashmap
Object.keys(cubes).forEach((cubeName) => {
let cube = cubes[cubeName];
codeMirrorHints[cubeName] = [];
if (cube.measures && cube.measures.length) {
cube.measures.forEach((measure) => {
codeMirrorHints[cubeName].push(measure.name);
});
}
if (cube.dimensions && cube.dimensions.length) {
cube.dimensions.forEach((dimension) => {
codeMirrorHints[cubeName].push(dimension.name);
});
}
});
codeMirror.options.hintOptions = { tables: codeMirrorHints };
}
_onChangeTableStore () {
// tables
let databases = DatabaseStore.getDatabases() || [];
let tables = databases.map(db => {
if (TableStore.getTables(db)) {
return {
database: db,
tables: TableStore.getTables(db)
};
}
}).filter(item => { return !!item; }); // filtering undefined items
tables.forEach(tableObject => {
Object.keys(tableObject.tables).forEach(tableName => {
let table = tableObject.tables[tableName];
let qualifiedName = tableObject.database + '.' + tableName;
codeMirrorHints[qualifiedName] = [];
codeMirrorHints[tableName] = [];
if (table.columns && table.columns.length) {
table.columns.forEach((col) => {
codeMirrorHints[qualifiedName].push(col.name);
codeMirrorHints[tableName].push(col.name);
codeMirrorHints[col.name] = [];
});
}
});
});
codeMirror.options.hintOptions = { tables: codeMirrorHints };
}
_onChangeSavedQueryStore (hash) {
if (!hash) return;
var newState = _.assign({}, this.state);
switch (hash.type) {
case 'failure':
newState.clientMessage = null;
newState.serverMessage = hash.message;
break;
case 'success':
// trigger to fetch the edited from server again
let token = UserStore.getUserDetails().secretToken;
if (hash.id) AdhocQueryActions.getSavedQueryById(token, hash.id);
// means the query was saved successfully.
// run immediately?
if (newState.runImmediately && hash.id) {
this.runSavedQuery(hash.id);
newState.runImmediately = false;
}
// empty the state, clean the slate
setCode('');
this.refs.queryName.getDOMNode().value = '';
newState = getEmptyState();
newState.serverMessage = hash.message;
break;
case 'params':
newState.params = hash.params.map(param => {
return {
name: param.name,
dataType: param.dataType || 'STRING',
collectionType: param.collectionType || 'SINGLE',
defaultValue: param.defaultValue || null,
displayName: param.displayName || param.name
};
});
break;
}
this.setState(newState);
}
runSavedQuery (id) {
let secretToken = UserStore.getUserDetails().secretToken;
let parameters = this.state.params.map(param => {
let object = {};
object[param.name] = param.defaultValue;
return object;
});
AdhocQueryActions.runSavedQuery(secretToken, id, parameters);
}
toggle () {
this.setState({isCollapsed: !this.state.isCollapsed});
}
closeParamBox () {
this.cancel();
}
saveParams (params) {
!this.state.isModeEdit ? this.saveQuery(params) : this.updateQuery(params);
}
cancel () {
setCode('');
this.refs.queryName.getDOMNode().value = '';
this.setState(getEmptyState());
}
}
QueryBox.contextTypes = {
router: React.PropTypes.func
};
export default QueryBox;