LENS-782: UI support for the saved, parametrized query feature
Project: http://git-wip-us.apache.org/repos/asf/lens/repo Commit: http://git-wip-us.apache.org/repos/asf/lens/commit/86714211 Tree: http://git-wip-us.apache.org/repos/asf/lens/tree/86714211 Diff: http://git-wip-us.apache.org/repos/asf/lens/diff/86714211 Branch: refs/heads/current-release-line Commit: 867142113fbbe2d0793e7da156cc4ef190c22811 Parents: dd33a60 Author: Ankeet Maini <[email protected]> Authored: Thu Oct 8 22:25:12 2015 +0530 Committer: raju <[email protected]> Committed: Thu Oct 8 22:25:12 2015 +0530 ---------------------------------------------------------------------- lens-ui/app/actions/AdhocQueryActions.js | 179 ++++++- lens-ui/app/adapters/AdhocQueryAdapter.js | 80 +++- lens-ui/app/adapters/BaseAdapter.js | 93 ++-- lens-ui/app/adapters/XMLAdapter.js | 37 ++ lens-ui/app/app.js | 30 +- lens-ui/app/components/AdhocQueryComponent.js | 42 +- lens-ui/app/components/AppComponent.js | 9 +- lens-ui/app/components/CubeSchemaComponent.js | 36 +- lens-ui/app/components/CubeTreeComponent.js | 42 +- lens-ui/app/components/DatabaseComponent.js | 18 +- lens-ui/app/components/HeaderComponent.js | 37 +- lens-ui/app/components/LoaderComponent.js | 2 +- lens-ui/app/components/LoginComponent.js | 24 +- lens-ui/app/components/LogoutComponent.js | 5 +- lens-ui/app/components/QueryBoxComponent.js | 469 ++++++++++++++----- .../components/QueryDetailResultComponent.js | 37 +- .../app/components/QueryOperationsComponent.js | 21 +- .../app/components/QueryParamRowComponent.js | 173 +++++++ lens-ui/app/components/QueryParamsComponent.js | 130 +++++ lens-ui/app/components/QueryPreviewComponent.js | 57 ++- lens-ui/app/components/QueryResultsComponent.js | 25 +- .../RequireAuthenticationComponent.js | 4 +- lens-ui/app/components/SavedQueriesComponent.js | 180 +++++++ .../components/SavedQueryPreviewComponent.js | 136 ++++++ lens-ui/app/components/SidebarComponent.js | 4 +- lens-ui/app/components/TableSchemaComponent.js | 34 +- lens-ui/app/components/TableTreeComponent.js | 47 +- lens-ui/app/constants/AdhocQueryConstants.js | 11 +- lens-ui/app/constants/AppConstants.js | 5 +- lens-ui/app/dispatcher/AppDispatcher.js | 1 - lens-ui/app/stores/AdhocQueryStore.js | 23 +- lens-ui/app/stores/CubeStore.js | 3 +- lens-ui/app/stores/DatabaseStore.js | 4 +- lens-ui/app/stores/SavedQueryStore.js | 99 ++++ lens-ui/app/stores/TableStore.js | 4 +- lens-ui/app/stores/UserStore.js | 16 +- lens-ui/app/styles/css/global.css | 22 + lens-ui/app/styles/css/login.css | 3 +- lens-ui/app/styles/css/query-component.css | 3 +- lens-ui/app/styles/css/tree.css | 3 +- lens-ui/app/styles/less/globals.less | 3 +- lens-ui/app/utils/ErrorParser.js | 53 +++ lens-ui/index.html | 1 + lens-ui/package.json | 23 +- lens-ui/server.js | 39 +- lens-ui/webpack.config.js | 22 +- 46 files changed, 1760 insertions(+), 529 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/lens/blob/86714211/lens-ui/app/actions/AdhocQueryActions.js ---------------------------------------------------------------------- diff --git a/lens-ui/app/actions/AdhocQueryActions.js b/lens-ui/app/actions/AdhocQueryActions.js index 8c2d109..284c781 100644 --- a/lens-ui/app/actions/AdhocQueryActions.js +++ b/lens-ui/app/actions/AdhocQueryActions.js @@ -20,6 +20,48 @@ import AppDispatcher from '../dispatcher/AppDispatcher'; import AdhocQueryConstants from '../constants/AdhocQueryConstants'; import AdhocQueryAdapter from '../adapters/AdhocQueryAdapter'; +import ErrorParser from '../utils/ErrorParser'; +import _ from 'lodash'; + +function _executeQuery (secretToken, query, queryName) { + AdhocQueryAdapter.executeQuery(secretToken, query, queryName) + .then(queryHandle => { + AppDispatcher.dispatch({ + actionType: AdhocQueryConstants.RECEIVE_QUERY_HANDLE, + payload: { queryHandle: queryHandle } + }); + }, (error) => { + // error details contain array of objects {code, message} + var errorDetails = ErrorParser.getMessage(error); + AppDispatcher.dispatch({ + actionType: AdhocQueryConstants.RECEIVE_QUERY_HANDLE_FAILED, + payload: { + type: 'Error', + texts: errorDetails + } + }); + }); +} + +function _saveQuery (secretToken, user, query, options) { + AdhocQueryAdapter.saveQuery(secretToken, user, query, options) + .then(response => { + AppDispatcher.dispatch({ + actionType: AdhocQueryConstants.SAVE_QUERY_SUCCESS, + payload: { + type: 'Success', + text: 'Query was successfully saved!', + id: response.id + } + }); + }, error => { + error = error.error; + AppDispatcher.dispatch({ + actionType: AdhocQueryConstants.SAVE_QUERY_FAILED, + payload: {type: 'Error', text: error.code + ': ' + error.message} + }); + }).catch(e => { console.error(e); }); +} let AdhocQueryActions = { getDatabases (secretToken) { @@ -30,7 +72,6 @@ let AdhocQueryActions = { payload: { databases: databases } }); }, function (error) { - AppDispatcher.dispatch({ actionType: AdhocQueryConstants.RECEIVE_DATABASES_FAILED, payload: { @@ -49,7 +90,6 @@ let AdhocQueryActions = { payload: { cubes: cubes } }); }, function (error) { - // propagating the error message, couldn't fetch cubes AppDispatcher.dispatch({ actionType: AdhocQueryConstants.RECEIVE_CUBES_FAILED, @@ -61,22 +101,117 @@ let AdhocQueryActions = { }); }, - executeQuery (secretToken, query, queryName) { - AdhocQueryAdapter.executeQuery(secretToken, query, queryName) - .then(function (queryHandle) { + getSavedQueries (secretToken, user, options) { + AdhocQueryAdapter.getSavedQueries(secretToken, user, options) + .then(savedQueries => { AppDispatcher.dispatch({ - actionType: AdhocQueryConstants.RECEIVE_QUERY_HANDLE, - payload: { queryHandle: queryHandle } + actionType: AdhocQueryConstants.RECEIVE_SAVED_QUERIES, + payload: savedQueries }); - }, function (error) { + }); + }, + + getSavedQueryById (secretToken, id) { + AdhocQueryAdapter.getSavedQueryById(secretToken, id) + .then(savedQuery => { + AppDispatcher.dispatch({ + actionType: AdhocQueryConstants.RECEIVE_SAVED_QUERY, + payload: savedQuery + }); + }); + }, + + updateSavedQuery (secretToken, user, query, options, id) { + AdhocQueryAdapter.getParams(secretToken, query).then(response => { + let serverParams = response.parameters + .map(item => item.name) + .sort(); + let clientParams = options && options.parameters && options.parameters + .map(item => item.name) + .sort(); + if (!clientParams) clientParams = []; + if (_.isEqual(serverParams, clientParams)) { + AdhocQueryAdapter.updateSavedQuery(secretToken, user, query, options, id) + .then(response => { + AppDispatcher.dispatch({ + actionType: AdhocQueryConstants.SAVE_QUERY_SUCCESS, + payload: { + type: 'Success', + text: 'Query was successfully updated!', + id: response.id + } + }); + }, error => { + error = error.error; + AppDispatcher.dispatch({ + actionType: AdhocQueryConstants.SAVE_QUERY_FAILED, + payload: {type: 'Error', text: error.code + ': ' + error.message} + }); + }).catch(e => { console.error(e); }); + } else { + // get parameters' meta + AppDispatcher.dispatch({ + actionType: AdhocQueryConstants.RECEIVE_QUERY_PARAMS_META, + payload: response.parameters + }); + } + }); + }, + + saveQuery (secretToken, user, query, options) { + AdhocQueryAdapter.getParams(secretToken, query).then(response => { + let serverParams = response.parameters + .map(item => item.name) + .sort(); + let clientParams = options && options.parameters && options.parameters + .map(item => item.name) + .sort(); + if (!clientParams) clientParams = []; + if (_.isEqual(serverParams, clientParams)) { + _saveQuery(secretToken, user, query, options); + } else { + // get parameters' meta + AppDispatcher.dispatch({ + actionType: AdhocQueryConstants.RECEIVE_QUERY_PARAMS_META, + payload: response.parameters + }); + } + }, error => { + AppDispatcher.dispatch({ + actionType: AdhocQueryConstants.RECEIVE_QUERY_HANDLE_FAILED, + payload: { + type: 'Error', + text: 'Please enable Saved Queries feature in the LENS Server to proceed.' + } + }); + }); + }, + + // first calls parameters API and sees if the query has any params, + // as we can't run a query with params, it needs to be saved first. + runQuery (secretToken, query, queryName) { + AdhocQueryAdapter.getParams(secretToken, query).then(response => { + if (!response.parameters.length) { + _executeQuery(secretToken, query, queryName); + } else { + // ask user to save the query maybe? AppDispatcher.dispatch({ actionType: AdhocQueryConstants.RECEIVE_QUERY_HANDLE_FAILED, payload: { - responseCode: error.status, - responseMessage: error.statusText + type: 'Error', + text: 'You can\'t run a query with parameters, save it first.' } }); + } + }, error => { + AppDispatcher.dispatch({ + actionType: AdhocQueryConstants.RECEIVE_QUERY_HANDLE_FAILED, + payload: { + type: 'Error', + text: 'Please enable Saved Queries feature in the LENS Server to proceed.' + } }); + }); }, getCubeDetails (secretToken, cubeName) { @@ -138,11 +273,9 @@ let AdhocQueryActions = { .then(function (result) { let payload; if (Object.prototype.toString.call(result).match('String')) { - // persistent payload = { downloadURL: result, type: 'PERSISTENT', handle: handle }; } else if (Object.prototype.toString.call(result).match('Array')) { - // in-memory gives array payload = { queryResult: result[0], @@ -174,7 +307,6 @@ let AdhocQueryActions = { payload: { tables: tables, database: database } }); }, function (error) { - // propagating the error message, couldn't fetch cubes AppDispatcher.dispatch({ actionType: AdhocQueryConstants.RECEIVE_TABLES_FAILED, @@ -207,6 +339,27 @@ let AdhocQueryActions = { cancelQuery (secretToken, handle) { AdhocQueryAdapter.cancelQuery(secretToken, handle); // TODO finish this up + }, + + runSavedQuery (secretToken, id, params) { + AdhocQueryAdapter.runSavedQuery(secretToken, id, params) + .then(handle => { + AppDispatcher.dispatch({ + actionType: AdhocQueryConstants.RECEIVE_QUERY_HANDLE, + payload: { queryHandle: handle } + }); + }, (error) => { + // error response contains an error XML with code, message and + // stacktrace + AppDispatcher.dispatch({ + actionType: AdhocQueryConstants.RECEIVE_QUERY_HANDLE_FAILED, + payload: { + type: 'Error', + text: error.getElementsByTagName('code')[0].textContent + ': ' + + error.getElementsByTagName('message')[0].textContent + } + }); + }); } }; http://git-wip-us.apache.org/repos/asf/lens/blob/86714211/lens-ui/app/adapters/AdhocQueryAdapter.js ---------------------------------------------------------------------- diff --git a/lens-ui/app/adapters/AdhocQueryAdapter.js b/lens-ui/app/adapters/AdhocQueryAdapter.js index 98f9b49..24e72ef 100644 --- a/lens-ui/app/adapters/AdhocQueryAdapter.js +++ b/lens-ui/app/adapters/AdhocQueryAdapter.js @@ -24,10 +24,14 @@ import Config from 'config.json'; let baseUrl = Config.baseURL; let urls = { - 'getDatabases': 'metastore/databases', - 'getCubes': 'metastore/cubes', - 'query': 'queryapi/queries', // POST on this to execute, GET to fetch all - 'getTables': 'metastore/nativetables' + getDatabases: 'metastore/databases', + getCubes: 'metastore/cubes', + query: 'queryapi/queries', // POST on this to execute, GET to fetch all + getTables: 'metastore/nativetables', + getSavedQueries: 'queryapi/savedqueries', + parameters: 'queryapi/savedqueries/parameters', + saveQuery: 'queryapi/savedqueries', // POST to save, PUT to update, {id} for GET + runSavedQuery: 'queryapi/savedqueries' }; let AdhocQueryAdapter = { @@ -54,13 +58,39 @@ let AdhocQueryAdapter = { formData.append('query', query); formData.append('operation', 'execute'); - if (queryName) formData.append('queryName', queryName); + if (queryName) formData.append('queryName', queryName); return BaseAdapter.post(url, formData, { contentType: 'multipart/form-data' }); }, + saveQuery (secretToken, user, query, options) { + let url = baseUrl + urls.saveQuery; + let queryToBeSaved = { + owner: user, + name: options.name || '', + query: query, + description: options.description || '', + parameters: options.parameters || [] + }; + + return BaseAdapter.post(url, queryToBeSaved); + }, + + updateSavedQuery (secretToken, user, query, options, id) { + let url = baseUrl + urls.saveQuery + '/' + id; + let queryToBeSaved = { + owner: user, + name: options.name || '', + query: query, + description: options.description || '', + parameters: options.parameters || [] + }; + + return BaseAdapter.put(url, queryToBeSaved); + }, + getQuery (secretToken, handle) { let url = baseUrl + urls.query + '/' + handle; return BaseAdapter.get(url, {sessionid: secretToken}); @@ -79,9 +109,8 @@ let AdhocQueryAdapter = { return BaseAdapter.get(url, queryOptions) .then(function (queryHandles) { - // FIXME limiting to 10 for now - //let handles = queryHandles.slice(0, 10); + // let handles = queryHandles.slice(0, 10); return Promise.all(queryHandles.map((handle) => { return BaseAdapter.get(url + '/' + handle.handleId, { sessionid: secretToken, @@ -92,13 +121,12 @@ let AdhocQueryAdapter = { }, getQueryResult (secretToken, handle, queryMode) { - // on page refresh, the store won't have queryMode so fetch query // this is needed as we won't know in which mode the query was fired if (!queryMode) { this.getQuery(secretToken, handle).then((query) => { queryMode = query.isPersistent; - queryMode = queryMode ? 'PERSISTENT': 'INMEMORY'; + queryMode = queryMode ? 'PERSISTENT' : 'INMEMORY'; return this._inMemoryOrPersistent(secretToken, handle, queryMode); }); } else { @@ -106,8 +134,6 @@ let AdhocQueryAdapter = { } }, - // a method used only internally to figure out - // whether to fetch INMEMORY or PERSISTENT results _inMemoryOrPersistent (secretToken, handle, queryMode) { return queryMode === 'PERSISTENT' ? this.getDownloadURL(secretToken, handle) : @@ -139,6 +165,11 @@ let AdhocQueryAdapter = { return Promise.resolve(downloadURL); }, + getSavedQueryById (secretToken, id) { + let url = baseUrl + urls.saveQuery + '/' + id; + return BaseAdapter.get(url, {sessionid: secretToken}); + }, + getInMemoryResults (secretToken, handle) { let resultUrl = baseUrl + urls.query + '/' + handle + '/resultset'; let results = BaseAdapter.get(resultUrl, { @@ -151,6 +182,33 @@ let AdhocQueryAdapter = { }); return Promise.all([results, meta]); + }, + + getSavedQueries (secretToken, user, options = {}) { + let url = baseUrl + urls.getSavedQueries; + return BaseAdapter.get(url, { + user: user, + sessionid: secretToken, + start: options.offset || 0, + count: options.pageSize || 10 + }); + }, + + getParams (secretToken, query) { + let url = baseUrl + urls.parameters; + return BaseAdapter.get(url, {query: query}); + }, + + runSavedQuery (secretToken, id, params) { + let queryParamString = BaseAdapter.jsonToQueryParams(params); + let url = baseUrl + urls.runSavedQuery + '/' + id + queryParamString; + + let formData = new FormData(); + formData.append('sessionid', secretToken); + + return BaseAdapter.post(url, formData, { + contentType: 'multipart/form-data' + }); } }; http://git-wip-us.apache.org/repos/asf/lens/blob/86714211/lens-ui/app/adapters/BaseAdapter.js ---------------------------------------------------------------------- diff --git a/lens-ui/app/adapters/BaseAdapter.js b/lens-ui/app/adapters/BaseAdapter.js index 81b9ddc..b22eefc 100644 --- a/lens-ui/app/adapters/BaseAdapter.js +++ b/lens-ui/app/adapters/BaseAdapter.js @@ -17,74 +17,69 @@ * under the License. */ -import reqwest from 'reqwest'; +import reqwest from 'qwest'; import Promise from 'bluebird'; import Config from 'config.json'; +import XMLAdapter from './XMLAdapter'; function makeReqwest (url, method, data, options = {}) { - let reqwestOptions = { - url: url, - method: method, - contentType: 'application/json', - type: 'json', - headers: {} - }; - + let reqwestOptions = { headers: {}, timeout: 200000 }; // a large enough for native tables if (Config.headers) reqwestOptions.headers = Config.headers; + reqwestOptions.responseType = !options.contentType ? 'json' : 'document'; - // delete Content-Type and add Accept - reqwestOptions.headers['Accept'] = 'application/json'; - delete reqwestOptions.headers['Content-Type']; - if (data) reqwestOptions.data = data; - if (options.contentType === 'multipart/form-data') { - reqwestOptions.processData = false; - reqwestOptions.contentType = 'multipart/form-data'; - - // because server can't handle JSON response on POST - delete reqwestOptions.type; - delete reqwestOptions.headers['Accept']; + if (reqwestOptions.responseType !== 'document') { + if (method === 'post' || method === 'put') reqwestOptions.dataType = 'json'; + } else { + delete reqwestOptions.headers['Content-Type']; } - return new Promise ((resolve, reject) => { - reqwest(reqwestOptions) - .then ((response) => { + return new Promise((resolve, reject) => { + reqwest[method](url, data, reqwestOptions) + .then((response) => { + response = reqwestOptions.responseType === 'json' ? + response.response : + XMLAdapter.stringToXML(response.response); + resolve(response); }, (error) => { - reject(error); - }); + let response = error.responseType !== 'json' ? + XMLAdapter.stringToXML(error.response) : + error.response; + reject(response); + }).catch(e => console.error(e)); }); } -function deleteRequest (url, dataArray) { - return makeReqwest(url, 'delete', dataArray); -} - -function get (url, dataArray, options) { - return makeReqwest(url, 'get', dataArray, options); -} - -// TODO need to fix this unused 'options'. What params can it have? -function postJson (url, data, options = {}) { - return makeReqwest(url, 'post', data, {contentType: 'application/json'}); -} - -function postFormData (url, data, options = {}) { - return makeReqwest(url, 'post', data, options); -} - let BaseAdapter = { - get: get, + get (url, data, options) { + return makeReqwest(url, 'get', data, options); + }, post (url, data, options = {}) { - if (options.contentType) { - return postFormData(url, data, options); - } else { - return postJson(url, data, options); - } + return makeReqwest(url, 'post', data, options); + }, + + put (url, data, options = {}) { + return makeReqwest(url, 'put', data, options); }, - delete: deleteRequest + delete (url, data) { + return makeReqwest(url, 'delete', data); + }, + + jsonToQueryParams (json) { + // if json is an array? + var queryParams = '?'; + if (!Object.prototype.toString.call(json).match('Array')) json = [json]; + + json.forEach(object => { + Object.keys(object).forEach(key => { + queryParams += key + '=' + object[key] + '&'; + }); + }); + return queryParams.slice(0, -1); + } }; export default BaseAdapter; http://git-wip-us.apache.org/repos/asf/lens/blob/86714211/lens-ui/app/adapters/XMLAdapter.js ---------------------------------------------------------------------- diff --git a/lens-ui/app/adapters/XMLAdapter.js b/lens-ui/app/adapters/XMLAdapter.js new file mode 100644 index 0000000..a206a1e --- /dev/null +++ b/lens-ui/app/adapters/XMLAdapter.js @@ -0,0 +1,37 @@ +/** +* 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. +*/ + +// converts string to XML and vice-versa +// http://stackoverflow.com/questions/3054108/how-to-convert-string-to-xml-object-in-javascript + +let XMLAdapter = { + stringToXML (string) { + if (window.DOMParser) { + return new DOMParser().parseFromString(string, 'text/xml'); + } + + // IE? + var xmlDoc = new window.ActiveXObject('Microsoft.XMLDOM'); + xmlDoc.async = 'false'; + xmlDoc.loadXML(string); + return xmlDoc; + } +}; + +export default XMLAdapter; http://git-wip-us.apache.org/repos/asf/lens/blob/86714211/lens-ui/app/app.js ---------------------------------------------------------------------- diff --git a/lens-ui/app/app.js b/lens-ui/app/app.js index 3e389a7..73cb511 100644 --- a/lens-ui/app/app.js +++ b/lens-ui/app/app.js @@ -19,32 +19,36 @@ import React from 'react'; import Router from 'react-router'; -import { DefaultRoute, Route, RouteHandler } from 'react-router'; +import { DefaultRoute, Route } from 'react-router'; +import './styles/less/globals.less'; +import './styles/css/global.css'; + +import Login from './components/LoginComponent'; +import Logout from './components/LogoutComponent'; import About from './components/AboutComponent'; import App from './components/AppComponent'; import AdhocQuery from './components/AdhocQueryComponent'; -import Login from './components/LoginComponent'; -import Logout from './components/LogoutComponent'; import QueryResults from './components/QueryResultsComponent'; import CubeSchema from './components/CubeSchemaComponent'; import QueryDetailResult from './components/QueryDetailResultComponent'; import TableSchema from './components/TableSchemaComponent'; -import LoginActions from './actions/LoginActions'; +import SavedQueries from './components/SavedQueriesComponent'; let routes = ( - <Route name="app" path="/" handler={App} > - <Route name="login" handler={Login}/> - <Route name="logout" handler={Logout}/> - <Route name="query" path="query" handler={AdhocQuery} > - <Route name="results" handler={QueryResults}/> - <Route name="result" path="/results/:handle" handler={QueryDetailResult}/> - <Route name="cubeschema" path="schema/cube/:cubeName" handler={CubeSchema}/> - <Route name="tableschema" path="schema/table/:tableName" + <Route name='app' path='/' handler={App} > + <Route name='login' handler={Login}/> + <Route name='logout' handler={Logout}/> + <Route name='query' path='query' handler={AdhocQuery} > + <Route name='results' handler={QueryResults}/> + <Route name='savedqueries' handler={SavedQueries}/> + <Route name='result' path='/results/:handle' handler={QueryDetailResult}/> + <Route name='cubeschema' path='schema/cube/:cubeName' handler={CubeSchema}/> + <Route name='tableschema' path='schema/table/:tableName' handler={TableSchema}/> </Route> - <Route name="about" handler={About} /> + <Route name='about' handler={About} /> <DefaultRoute handler={AdhocQuery} /> </Route> ); http://git-wip-us.apache.org/repos/asf/lens/blob/86714211/lens-ui/app/components/AdhocQueryComponent.js ---------------------------------------------------------------------- diff --git a/lens-ui/app/components/AdhocQueryComponent.js b/lens-ui/app/components/AdhocQueryComponent.js index 66ddf75..32fab33 100644 --- a/lens-ui/app/components/AdhocQueryComponent.js +++ b/lens-ui/app/components/AdhocQueryComponent.js @@ -25,50 +25,22 @@ import Sidebar from './SidebarComponent'; import RequireAuthentication from './RequireAuthenticationComponent'; class AdhocQuery extends React.Component { - constructor (props) { - super(props); - this.state = {toggleQueryBox: true}; // show box when true, hide on false - this.toggleQueryBox = this.toggleQueryBox.bind(this); - } - - render() { - let toggleButtonClass = this.state.toggleQueryBox ? 'default' : 'primary'; - + render () { return ( - <section className="row"> - <div className="col-md-4"> + <section className='row'> + <div className='col-md-4'> <Sidebar /> </div> - <div className="col-md-8"> - <div className="panel panel-default"> - <div className="panel-heading"> - <h3 className="panel-title"> - Compose - <button - className={'btn btn-xs pull-right btn-' + toggleButtonClass} - onClick={this.toggleQueryBox}> - {this.state.toggleQueryBox ? 'Hide': 'Show'} Query Box - </button> - </h3> - </div> - <div className="panel-body" style={{padding: '0px'}}> - <QueryBox toggleQueryBox={this.state.toggleQueryBox} {...this.props}/> - </div> - </div> + <div className='col-md-8'> + <QueryBox {...this.props}/> - <RouteHandler toggleQueryBox={this.state.toggleQueryBox}/> + <RouteHandler/> </div> </section> ); } - - // FIXME persist the state in the URL as well - toggleQueryBox () { - this.setState({toggleQueryBox: !this.state.toggleQueryBox}); - } -}; +} let AuthenticatedAdhocQuery = RequireAuthentication(AdhocQuery); - export default AuthenticatedAdhocQuery; http://git-wip-us.apache.org/repos/asf/lens/blob/86714211/lens-ui/app/components/AppComponent.js ---------------------------------------------------------------------- diff --git a/lens-ui/app/components/AppComponent.js b/lens-ui/app/components/AppComponent.js index d7a38f9..216eed4 100644 --- a/lens-ui/app/components/AppComponent.js +++ b/lens-ui/app/components/AppComponent.js @@ -22,14 +22,13 @@ import { RouteHandler } from 'react-router'; import Header from './HeaderComponent'; -export default class AppComponent extends React.Component { - - render() { +export default class App extends React.Component { + render () { return ( <section> <Header /> - <div className="container-fluid"> + <div className='container-fluid'> <RouteHandler /> </div> </section> @@ -37,4 +36,4 @@ export default class AppComponent extends React.Component { } } -export default AppComponent; +export default App; http://git-wip-us.apache.org/repos/asf/lens/blob/86714211/lens-ui/app/components/CubeSchemaComponent.js ---------------------------------------------------------------------- diff --git a/lens-ui/app/components/CubeSchemaComponent.js b/lens-ui/app/components/CubeSchemaComponent.js index 593c54a..c56cb15 100644 --- a/lens-ui/app/components/CubeSchemaComponent.js +++ b/lens-ui/app/components/CubeSchemaComponent.js @@ -41,9 +41,9 @@ function constructMeasureTable (cubeName, measures) { }); return ( - <div class="table-responsive"> - <table className="table table-striped table-condensed"> - <caption className="bg-primary text-center">Measures</caption> + <div className='table-responsive'> + <table className='table table-striped table-condensed'> + <caption className='bg-primary text-center'>Measures</caption> <thead> <tr> <th>Name</th> @@ -76,9 +76,9 @@ function constructDimensionTable (cubeName, dimensions) { }); return ( - <div class="table-responsive"> - <table className="table table-striped"> - <caption className="bg-primary text-center">Dimensions</caption> + <div className='table-responsive'> + <table className='table table-striped'> + <caption className='bg-primary text-center'>Dimensions</caption> <thead> <tr> <th>Name</th> @@ -119,6 +119,7 @@ class CubeSchema extends React.Component { } componentWillReceiveProps (props) { + // TODO are props updated automatically, unlike state? let cubeName = props.params.cubeName; let cube = getCubes()[cubeName]; @@ -132,7 +133,6 @@ class CubeSchema extends React.Component { // empty the previous state this.setState({ cube: {} }); - } render () { @@ -140,21 +140,17 @@ class CubeSchema extends React.Component { // this will be empty if it's the first time so show a loader if (!this.state.cube.isLoaded) { - schemaSection = <Loader size="8px" margin="2px" />; + schemaSection = <Loader size='8px' margin='2px' />; } else { - // if we have cube state let cube = this.state.cube; if (this.props.query.type === 'measures') { - // show only measures schemaSection = constructMeasureTable(cube.name, cube.measures); } else if (this.props.query.type === 'dimensions') { - // show only dimensions schemaSection = constructDimensionTable(cube.name, cube.dimensions); } else { - // show both measures, dimensions schemaSection = ( <div> @@ -170,16 +166,15 @@ class CubeSchema extends React.Component { return ( <section> - <div className="panel panel-default"> - <div className="panel-heading"> - <h3 className="panel-title">Schema Details: - <strong className="text-primary"> + <div className='panel panel-default'> + <div className='panel-heading'> + <h3 className='panel-title'>Schema Details: + <strong className='text-primary'> {this.props.params.cubeName} </strong> </h3> </div> - <div className="panel-body" style={{overflowY: 'auto', - maxHeight: this.props.toggleQueryBox ? '260px': '480px'}}> + <div className='panel-body'> {schemaSection} </div> </div> @@ -193,4 +188,9 @@ class CubeSchema extends React.Component { } } +CubeSchema.propTypes = { + query: React.PropTypes.object, + params: React.PropTypes.object +}; + export default CubeSchema; http://git-wip-us.apache.org/repos/asf/lens/blob/86714211/lens-ui/app/components/CubeTreeComponent.js ---------------------------------------------------------------------- diff --git a/lens-ui/app/components/CubeTreeComponent.js b/lens-ui/app/components/CubeTreeComponent.js index 241c12f..e348898 100644 --- a/lens-ui/app/components/CubeTreeComponent.js +++ b/lens-ui/app/components/CubeTreeComponent.js @@ -18,7 +18,6 @@ */ import React from 'react'; -import Alert from 'react-bootstrap'; import TreeView from 'react-treeview'; import assign from 'object-assign'; import { Link } from 'react-router'; @@ -66,8 +65,7 @@ class CubeTree extends React.Component { CubeStore.removeChangeListener(this._onChange); } - render() { - + render () { // cube tree structure sample // [{ // name: 'Cube-1', @@ -79,50 +77,56 @@ class CubeTree extends React.Component { var cubeTree = Object.keys(this.state.cubes).map((cubeName, i) => { let cube = cubeHash[cubeName]; - let label = <Link to="cubeschema" params={{cubeName: cubeName}}> - <span className="node">{cube.name}</span> + let label = <Link to='cubeschema' params={{cubeName: cubeName}}> + <span className='node'>{cube.name}</span> </Link>; - let measureLabel = <Link to="cubeschema" params={{cubeName: cubeName}} + let measureLabel = <Link to='cubeschema' params={{cubeName: cubeName}} query={{type: 'measures'}}> - <span className="quiet">Measures</span> + <span className='quiet'>Measures</span> </Link>; - let dimensionLabel = <Link to="cubeschema" params={{cubeName: cubeName}} + let dimensionLabel = <Link to='cubeschema' params={{cubeName: cubeName}} query={{type: 'dimensions'}}> - <span className="quiet">Dimensions</span> - </Link> + <span className='quiet'>Dimensions</span> + </Link>; return ( <TreeView key={cube.name + '|' + i} nodeLabel={label} - defaultCollapsed={true} onClick={this.getDetails.bind(this, cube.name)}> + defaultCollapsed={!cube.isLoaded} onClick={this.getDetails.bind(this, cube.name)}> <TreeView key={cube.name + '|measures'} nodeLabel={measureLabel} defaultCollapsed={!cube.isLoaded}> { cube.measures ? cube.measures.map(measure => { return ( - <div key={measure.name} className="treeNode measureNode"> + <div key={measure.name} className='treeNode measureNode'> {measure.name} ({measure.default_aggr}) </div> ); - }): null } + }) : null } </TreeView > <TreeView key={cube.name + '|dimensions'} nodeLabel={dimensionLabel} defaultCollapsed={!cube.isLoaded}> { cube.dimensions ? cube.dimensions.map(dimension => { return ( - <div className="treeNode dimensionNode" key={dimension.name}> + <div className='treeNode dimensionNode' key={dimension.name}> {dimension.name} </div> ); - }): null } + }) : null } </TreeView > </TreeView > ); }); - if (this.state.loading) cubeTree = <Loader size="4px" margin="2px"/>; + if (this.state.loading) { + cubeTree = <Loader size='4px' margin='2px'/>; + } else if (!this.state.cubes.length) { + cubeTree = (<div className='alert-danger' style={{padding: '8px 5px'}}> + <strong>Sorry, we couldn't find any cubes.</strong> + </div>); + } let collapseClass = ClassNames({ 'pull-right': true, @@ -137,9 +141,9 @@ class CubeTree extends React.Component { }); return ( - <div className="panel panel-default"> - <div className="panel-heading"> - <h3 className="panel-title"> + <div className='panel panel-default'> + <div className='panel-heading'> + <h3 className='panel-title'> Cubes <span className={collapseClass} onClick={this.toggle}></span> </h3> http://git-wip-us.apache.org/repos/asf/lens/blob/86714211/lens-ui/app/components/DatabaseComponent.js ---------------------------------------------------------------------- diff --git a/lens-ui/app/components/DatabaseComponent.js b/lens-ui/app/components/DatabaseComponent.js index 09ee2eb..09c9e7b 100644 --- a/lens-ui/app/components/DatabaseComponent.js +++ b/lens-ui/app/components/DatabaseComponent.js @@ -70,28 +70,28 @@ class DatabaseComponent extends React.Component { }); databaseComponent = (<div> - <label className="control-label" id="db">Select a Database</label> - <select className="form-control" id="db" onChange={this.setDatabase}> - <option value="">Select</option> + <label className='control-label' id='db'>Select a Database</label> + <select className='form-control' id='db' onChange={this.setDatabase}> + <option value=''>Select</option> {this.state.databases.map(database => { - return <option value={database}>{database}</option>; + return <option key={database} value={database}>{database}</option>; })} </select> </div>); if (this.state.loading) { - databaseComponent = <Loader size="4px" margin="2px"></Loader>; + databaseComponent = <Loader size='4px' margin='2px' />; } else if (!this.state.databases.length) { - databaseComponent = (<div className="alert-danger" + databaseComponent = (<div className='alert-danger' style={{padding: '8px 5px'}}> <strong>Sorry, we couldn't find any databases.</strong> </div>); } return ( - <div className="panel panel-default"> - <div className="panel-heading"> - <h3 className="panel-title"> + <div className='panel panel-default'> + <div className='panel-heading'> + <h3 className='panel-title'> Tables <span className={collapseClass} onClick={this.toggle}></span> </h3> http://git-wip-us.apache.org/repos/asf/lens/blob/86714211/lens-ui/app/components/HeaderComponent.js ---------------------------------------------------------------------- diff --git a/lens-ui/app/components/HeaderComponent.js b/lens-ui/app/components/HeaderComponent.js index 5ec3397..4f8d0f1 100644 --- a/lens-ui/app/components/HeaderComponent.js +++ b/lens-ui/app/components/HeaderComponent.js @@ -18,12 +18,9 @@ */ import React from 'react'; -import Router from 'react-router'; import { Link } from 'react-router'; -import Logout from './LogoutComponent'; import UserStore from '../stores/UserStore'; -import Config from 'config.json'; class Header extends React.Component { constructor () { @@ -47,29 +44,29 @@ class Header extends React.Component { render () { return ( - <nav className="navbar navbar-inverse navbar-static-top"> - <div className="container"> - <div className="navbar-header"> - <button type="button" className="navbar-toggle collapsed" - data-toggle="collapse" data-target="#navbar" - aria-expanded="false" aria-controls="navbar"> - <span className="sr-only">Toggle navigation</span> - <span className="icon-bar"></span> - <span className="icon-bar"></span> - <span className="icon-bar"></span> + <nav className='navbar navbar-inverse navbar-static-top'> + <div className='container'> + <div className='navbar-header'> + <button type='button' className='navbar-toggle collapsed' + data-toggle='collapse' data-target='#navbar' + aria-expanded='false' aria-controls='navbar'> + <span className='sr-only'>Toggle navigation</span> + <span className='icon-bar'></span> + <span className='icon-bar'></span> + <span className='icon-bar'></span> </button> - <Link className="navbar-brand" to="app">LENS Query<sup>β</sup></Link> + <Link className='navbar-brand' to='app'>LENS Query<sup>β</sup></Link> </div> - <div id="navbar" className="collapse navbar-collapse"> - <ul className="nav navbar-nav"> - <li><Link to="about">About</Link></li> + <div id='navbar' className='collapse navbar-collapse'> + <ul className='nav navbar-nav'> + <li><Link to='about'>About</Link></li> </ul> { this.state.userName && - (<ul className="nav navbar-nav navbar-right"> + (<ul className='nav navbar-nav navbar-right'> <li> - <Link to="logout" className="glyphicon glyphicon-log-out" - title="Logout"> + <Link to='logout' className='glyphicon glyphicon-log-out' + title='Logout'> <span> {this.state.userName}</span> </Link> </li> http://git-wip-us.apache.org/repos/asf/lens/blob/86714211/lens-ui/app/components/LoaderComponent.js ---------------------------------------------------------------------- diff --git a/lens-ui/app/components/LoaderComponent.js b/lens-ui/app/components/LoaderComponent.js index ba11c64..72b8f45 100644 --- a/lens-ui/app/components/LoaderComponent.js +++ b/lens-ui/app/components/LoaderComponent.js @@ -25,7 +25,7 @@ class Loader extends React.Component { render () { return ( <section style={{margin: '0 auto', maxWidth: '12%'}}> - <GridLoader {...this.props} color="#337ab7"/> + <GridLoader {...this.props} color='#337ab7'/> </section> ); } http://git-wip-us.apache.org/repos/asf/lens/blob/86714211/lens-ui/app/components/LoginComponent.js ---------------------------------------------------------------------- diff --git a/lens-ui/app/components/LoginComponent.js b/lens-ui/app/components/LoginComponent.js index cf95af9..c0840e1 100644 --- a/lens-ui/app/components/LoginComponent.js +++ b/lens-ui/app/components/LoginComponent.js @@ -53,19 +53,19 @@ class Login extends React.Component { render () { return ( - <section class="row" style={{margin: 'auto'}}> - <form className="form-signin" onSubmit={this.handleSubmit}> - <h2 className="form-signin-heading">Sign in</h2> - <label for="inputEmail" className="sr-only">Email address</label> - <input ref="email" id="inputEmail" className="form-control" - placeholder="Email address" required autofocus/> - <label for="inputPassword" className="sr-only">Password</label> - <input ref="pass" type="password" id="inputPassword" - className="form-control" placeholder="Password" required/> - <button className="btn btn-primary btn-block" - type="submit">Sign in</button> + <section className='row' style={{margin: 'auto'}}> + <form className='form-signin' onSubmit={this.handleSubmit}> + <h2 className='form-signin-heading'>Sign in</h2> + <label htmlFor='inputEmail' className='sr-only'>Email address</label> + <input ref='email' id='inputEmail' className='form-control' + placeholder='Email address' required autoFocus/> + <label htmlFor='inputPassword' className='sr-only'>Password</label> + <input ref='pass' type='password' id='inputPassword' + className='form-control' placeholder='Password' required/> + <button className='btn btn-primary btn-block' + type='submit'>Sign in</button> {this.state.error && ( - <div className="alert-danger text-center" + <div className='alert-danger text-center' style={{marginTop: '5px', padding: '0px 3px'}}> <h5>Sorry, authentication failed.</h5> <small>{this.state.errorMessage}</small> http://git-wip-us.apache.org/repos/asf/lens/blob/86714211/lens-ui/app/components/LogoutComponent.js ---------------------------------------------------------------------- diff --git a/lens-ui/app/components/LogoutComponent.js b/lens-ui/app/components/LogoutComponent.js index 3fc1627..9680a76 100644 --- a/lens-ui/app/components/LogoutComponent.js +++ b/lens-ui/app/components/LogoutComponent.js @@ -19,7 +19,6 @@ import React from 'react'; import { Link } from 'react-router'; -import Config from 'config.json'; import UserStore from '../stores/UserStore'; @@ -31,9 +30,9 @@ class Logout extends React.Component { render () { return ( - <div className="jumbotron text-center"> + <div className='jumbotron text-center'> <h3>You've succesfully logged out.</h3> - <p><Link to="/">Login</Link> to use this awesome app!</p> + <p><Link to='/'>Login</Link> to use this awesome app!</p> </div> ); } http://git-wip-us.apache.org/repos/asf/lens/blob/86714211/lens-ui/app/components/QueryBoxComponent.js ---------------------------------------------------------------------- diff --git a/lens-ui/app/components/QueryBoxComponent.js b/lens-ui/app/components/QueryBoxComponent.js index 6d5843c..6f4eeb7 100644 --- a/lens-ui/app/components/QueryBoxComponent.js +++ b/lens-ui/app/components/QueryBoxComponent.js @@ -18,7 +18,9 @@ */ import React from 'react'; +import ClassNames from 'classnames'; import CodeMirror from 'codemirror'; +import assign from 'object-assign'; import 'codemirror/lib/codemirror.css'; import 'codemirror/addon/edit/matchbrackets.js'; import 'codemirror/addon/hint/sql-hint.js'; @@ -32,12 +34,23 @@ 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 @@ -45,8 +58,7 @@ function setLimit (query) { // 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); + if (temp.slice(-1)[0] > 1000) temp.splice(-1, 1, 1000); query = temp.join(' '); } else { query += ' limit 1000'; @@ -66,169 +78,214 @@ function setCode (code) { // TODO improve this. // this takes in the query handle and writes the query // used from Edit Query link -function fetchQuery (props) { - if (props.query && props.query.handle) { - let query = AdhocQueryStore.getQueries()[props.query.handle]; +function fetchQueryForEdit (props) { + let query = AdhocQueryStore.getQueries()[props.query.handle]; - if (query) { - setCode(query.userQuery); - } + 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, + matchBrackets: true, autofocus: true, lineWrapping: true }); } -function updateAutoComplete () { - - // add autocomplete hints to the query box - let hints = {}; - - // cubes - let cubes = CubeStore.getCubes(); // hashmap - Object.keys(cubes).forEach((cubeName) => { - let cube = cubes[cubeName]; - hints[cubeName] = []; - - if (cube.measures && cube.measures.length) { - cube.measures.forEach((measure) => { - hints[cubeName].push(measure.name); - }); - } - if (cube.dimensions && cube.dimensions.length) { - cube.dimensions.forEach((dimension) => { - hints[cubeName].push(dimension.name); - }); - } - }); - - // 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; - hints[qualifiedName] = []; - hints[tableName] = []; - - if (table.columns && table.columns.length) { - table.columns.forEach((col) => { - hints[qualifiedName].push(col.name); - hints[tableName].push(col.name); - hints[col.name] = []; - }); - } - }); - }); - - codeMirror.options.hintOptions = { tables: hints }; -} - 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.state = { querySubmitted: false, isRunQueryDisabled: true }; + 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 = { + 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 + }; } 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', () => { - codeMirror.getValue() ? - this.state.isRunQueryDisabled = false : - this.state.isRunQueryDisabled = true; + this.state.isRunQueryDisabled = !codeMirror.getValue(); + this.setState(this.state); this._onChange(); }); // to remove the previous query's submission notification codeMirror.on('focus', () => { - this.state.querySubmitted = false; + this.setState({ clientMessage: null }); }); // add Cmd + Enter to fire runQuery - codeMirror.setOption("extraKeys", { - 'Cmd-Enter': (instance) => { - this.runQuery(); - }, - 'Ctrl-Space': 'autocomplete' - }); + codeMirror.setOption('extraKeys', { + 'Cmd-Enter': instance => { this.runQuery(); }, + 'Ctrl-Space': 'autocomplete', + 'Ctrl-S': instance => { this.saveQuery(); } + }); AdhocQueryStore.addChangeListener(this._onChange); - CubeStore.addChangeListener(this._onChange); - TableStore.addChangeListener(this._onChange); + CubeStore.addChangeListener(this._onChangeCubeStore); + TableStore.addChangeListener(this._onChangeTableStore); + SavedQueryStore.addChangeListener(this._onChangeSavedQueryStore); } componentWillUnmount () { - AdhocQueryStore.addChangeListener(this._onChange); - CubeStore.addChangeListener(this._onChange); - TableStore.addChangeListener(this._onChange); + AdhocQueryStore.removeChangeListener(this._onChange); + CubeStore.removeChangeListener(this._onChangeCubeStore); + TableStore.removeChangeListener(this._onChangeTableStore); + SavedQueryStore.removeChangeListener(this._onChangeSavedQueryStore); } componentWillReceiveProps (props) { - fetchQuery(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, + isModeEdit: true + }); + } + } } render () { - let queryBoxClass = this.props.toggleQueryBox ? '': 'hide'; + 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 ( - <section className={queryBoxClass}> - <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-6 col-md-6 col-sm-6 col-xs-12"> - {this.state.querySubmitted && ( - <div className="alert alert-info" style={{padding: '5px 4px', - marginBottom: '0px'}}> - Query has been submitted. Results are on their way! + <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}/> + } + + { 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> - )} - </div> - <div className="col-lg-2 col-md-2 col-sm-2 col-xs-12"> - <button className="btn btn-primary responsive" - onClick={this.runQuery} disabled={this.state.isRunQueryDisabled}> - Run Query - </button> - </div> + } + </section> </div> - </section> + </div> ); } + saveOrUpdate () { + !this.state.isModeEdit ? this.saveQuery() : this.updateQuery(); + } + runQuery () { let queryName = this.refs.queryName.getDOMNode().value; let secretToken = UserStore.getUserDetails().secretToken; @@ -237,14 +294,60 @@ class QueryBox extends React.Component { // set limit if mode is in-memory if (!Config.isPersistent) query = setLimit(query); - AdhocQueryActions.executeQuery(secretToken, query, queryName); + AdhocQueryActions.runQuery(secretToken, query, queryName); // show user the query was posted successfully and empty the queryName - this.state.querySubmitted = true; + this.setState({ clientMessage: clientMessages.runQuery }); this.refs.queryName.getDOMNode().value = ''; } - _onChange () { + updateQuery (params) { + let query = this._getSavedQueryDetails(params); + if (!query) return; + AdhocQueryActions + .updateSavedQuery(query.secretToken, query.user, query.query, query.params, this.state.savedQueryId); + this.setState({ clientMessage: clientMessages.updateQuery }); + } + + saveQuery (params) { + let query = this._getSavedQueryDetails(params); + if (!query) return; + AdhocQueryActions + .saveQuery(query.secretToken, query.user, query.query, query.params); + this.setState({ clientMessage: clientMessages.saveQuery }); + } + + // 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(); + + params = assign({}, params); + params.name = queryName; + + return { + secretToken: secretToken, + user: user, + query: query, + params: params + }; + } + + _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. @@ -252,27 +355,147 @@ class QueryBox extends React.Component { // clicked, and its action updates the store with query-handle. let handle = AdhocQueryStore.getQueryHandle(); if (handle) { - - // clear it else detail result component will be rendered - // every time the store emits a change event. - AdhocQueryStore.clearQueryHandle(); + this.setState({ clientMessage: null }); var { router } = this.context; router.transitionTo('result', {handle: handle}); } + } - // TODO remove this. - // check if handle was passed as query param, and if that - // query was fetched and available in store now. - // if (this.props && this.props.query.handle) { - // - // let query = AdhocQueryStore.getQueries()[this.props.query.handle]; - // if (query) setCode(query.userQuery); - // } + _onChangeCubeStore () { + // cubes + let cubes = CubeStore.getCubes(); // 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; + + switch (hash.type) { + case 'failure': + this.state.clientMessage = null; + this.state.serverMessage = hash.message; + break; + + case 'success': + this.state.clientMessage = null; + this.state.serverMessage = hash.message; + // make the mode of QueryBox back to normal, if it's in Edit + if (this.state.isModeEdit) { + this.state.isModeEdit = false; + } + + // 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 (this.state.runImmediately && hash.id) { + this.runSavedQuery(hash.id); + this.state.runImmediately = false; + } + + // make params null + this.state.params = null; + + break; + + case 'params': + this.state.params = hash.params; + break; + } - updateAutoComplete(); this.setState(this.state); } + + 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.setState({params: null, clientMessage: null}); + } + + saveParams (params) { // contains parameters, description et all + this.state.params = assign(this.state.params, params.parameters); + this.state.runImmediately = params.runImmediately; + + // edit or save new, only state variable will tell + !this.state.isModeEdit ? this.saveQuery(params) : this.updateQuery(params); + } + + cancel () { + setCode(''); + this.refs.queryName.getDOMNode().value = ''; + this.setState({ + 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 + }); + } } QueryBox.contextTypes = { http://git-wip-us.apache.org/repos/asf/lens/blob/86714211/lens-ui/app/components/QueryDetailResultComponent.js ---------------------------------------------------------------------- diff --git a/lens-ui/app/components/QueryDetailResultComponent.js b/lens-ui/app/components/QueryDetailResultComponent.js index b969a4a..096adde 100644 --- a/lens-ui/app/components/QueryDetailResultComponent.js +++ b/lens-ui/app/components/QueryDetailResultComponent.js @@ -19,7 +19,7 @@ import React from 'react'; -import Loader from '../components/LoaderComponent'; +import Loader from './LoaderComponent'; import AdhocQueryStore from '../stores/AdhocQueryStore'; import AdhocQueryActions from '../actions/AdhocQueryActions'; import UserStore from '../stores/UserStore'; @@ -28,7 +28,6 @@ import QueryPreview from './QueryPreviewComponent'; let interval = null; function isResultAvailableOnServer (handle) { - // always check before polling let query = AdhocQueryStore.getQueries()[handle]; if (query && query.status && query.status.status === 'SUCCESSFUL') { @@ -38,9 +37,6 @@ function isResultAvailableOnServer (handle) { } function fetchResult (secretToken, handle) { - - // this condition checks the query object, else - // we fetch it with the handle that we have if (isResultAvailableOnServer(handle)) { let query = AdhocQueryStore.getQueries()[handle]; let mode = query.isPersistent ? 'PERSISTENT' : 'INMEMORY'; @@ -60,7 +56,7 @@ function constructTable (tableData) { return (<tr>{row.values.values.map(cell => { return <td>{(cell && cell.value) || <span style={{color: 'red'}}>NULL</span>}</td>; })}</tr>); - }); + }); // in case the results are empty, happens when LENS server has restarted // all in-memory results are wiped clean @@ -73,8 +69,8 @@ function constructTable (tableData) { } return ( - <div class="table-responsive"> - <table className="table table-striped table-condensed"> + <div className='table-responsive'> + <table className='table table-striped table-condensed'> <thead> <tr>{header}</tr> </thead> @@ -118,9 +114,9 @@ class QueryDetailResult extends React.Component { // check if the query was persistent or in-memory if (query && query.isPersistent && query.status.status === 'SUCCESSFUL') { - result = (<div className="text-center"> + result = (<div className='text-center'> <a href={queryResult.downloadURL} download> - <span className="glyphicon glyphicon-download-alt "></span> Click + <span className='glyphicon glyphicon-download-alt '></span> Click here to download the results as a CSV file </a> </div>); @@ -128,16 +124,14 @@ class QueryDetailResult extends React.Component { result = constructTable(this.state.queryResult); } - - if (this.state.loading) result = <Loader size="8px" margin="2px"></Loader>; + if (this.state.loading) result = <Loader size='8px' margin='2px' />; return ( - <div className="panel panel-default"> - <div className="panel-heading"> - <h3 className="panel-title">Query Result</h3> + <div className='panel panel-default'> + <div className='panel-heading'> + <h3 className='panel-title'>Query Result</h3> </div> - <div className="panel-body" style={{overflowY: 'auto', padding: '0px', - maxHeight: this.props.toggleQueryBox ? '260px': '480px'}}> + <div className='panel-body no-padding'> <div> <QueryPreview key={query && query.queryHandle.handleId} {...query} /> @@ -149,7 +143,6 @@ class QueryDetailResult extends React.Component { } pollForResult (secretToken, handle) { - // fetch results immediately if present, don't wait for 5 seconds // in setInterval below. // FIXME if I put a return in if construct, setInterval won't execute which @@ -182,11 +175,15 @@ class QueryDetailResult extends React.Component { loading: loading, queryResult: result || {}, // result can be undefined so guarding it query: query - } + }; this.setState(state); - } } +QueryDetailResult.propTypes = { + query: React.PropTypes.object, + params: React.PropTypes.object +}; + export default QueryDetailResult; http://git-wip-us.apache.org/repos/asf/lens/blob/86714211/lens-ui/app/components/QueryOperationsComponent.js ---------------------------------------------------------------------- diff --git a/lens-ui/app/components/QueryOperationsComponent.js b/lens-ui/app/components/QueryOperationsComponent.js index a17a636..e4cc1e7 100644 --- a/lens-ui/app/components/QueryOperationsComponent.js +++ b/lens-ui/app/components/QueryOperationsComponent.js @@ -46,9 +46,9 @@ class QueryOperations extends React.Component { }); return ( - <div className="panel panel-default"> - <div className="panel-heading"> - <h3 className="panel-title"> + <div className='panel panel-default'> + <div className='panel-heading'> + <h3 className='panel-title'> Queries <span className={collapseClass} onClick={this.toggle}></span> </h3> @@ -56,27 +56,32 @@ class QueryOperations extends React.Component { <div className={panelBodyClassName}> <ul style={{listStyle: 'none', paddingLeft: '0px', marginBottom: '0px'}}> - <li><Link to="results">All</Link></li> + <li><Link to='results'>All</Link></li> <li> - <Link to="results" query={{category: 'running'}}> + <Link to='results' query={{category: 'running'}}> Running </Link> </li> <li> - <Link to="results" query={{category: 'successful'}}> + <Link to='results' query={{category: 'successful'}}> Completed </Link> </li> <li> - <Link to="results" query={{category: 'queued'}}> + <Link to='results' query={{category: 'queued'}}> Queued </Link> </li> <li> - <Link to="results" query={{category: 'failed'}}> + <Link to='results' query={{category: 'failed'}}> Failed </Link> </li> + <li> + <Link to='savedqueries'> + Saved Queries + </Link> + </li> </ul> </div> </div> http://git-wip-us.apache.org/repos/asf/lens/blob/86714211/lens-ui/app/components/QueryParamRowComponent.js ---------------------------------------------------------------------- diff --git a/lens-ui/app/components/QueryParamRowComponent.js b/lens-ui/app/components/QueryParamRowComponent.js new file mode 100644 index 0000000..fb5f5da --- /dev/null +++ b/lens-ui/app/components/QueryParamRowComponent.js @@ -0,0 +1,173 @@ +/** +* 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 { Multiselect } from 'react-widgets'; +import assign from 'object-assign'; +import 'react-widgets/dist/css/core.css'; +import 'react-widgets/dist/css/react-widgets.css'; + +// returns true/false if the default value is correct +// and also returns the value +function validate (val, dataType) { + // if (dataType === 'NUMBER' && !window.isNaN(val)) return [true, val]; + // if (dataType === 'BOOLEAN' && (val === 'true' || val === 'false')) { + // return [true, val]; + // } + // if (dataType === 'STRING' && typeof val === 'string') return [true, val]; + + return [true, val]; +} + +class QueryParamRow extends React.Component { + constructor (props) { + super(props); + + // state being decided by mode of use of this component + // `entryMode` is used by the SavedQueryPreviewComponent, + // to just add values and run the saved query. + if (props.entryMode) { + this.state = assign({}, props.param); + } else { + this.state = assign({}, props.param, { + dataType: 'STRING', + collectionType: 'SINGLE', + displayName: props.param.name + }); + } + + this.changeDisplayName = this.changeDisplayName.bind(this); + this.changeDataType = this.changeDataType.bind(this); + this.changeCollectionType = this.changeCollectionType.bind(this); + this.changeDefaultValue = this.changeDefaultValue.bind(this); + this.addDefaultValue = this.addDefaultValue.bind(this); + this.preventEnter = this.preventEnter.bind(this); + } + + componentWillReceiveProps (props) { + this.setState(assign({}, props.param)); + } + + componentWillUpdate (props, state) { + this.props.updateParam({ + name: props.param.name, + param: state + }); + } + + render () { + let param = this.props.param; + + return ( + <tr> + <td>{param.name}</td> + <td> + { this.props.entryMode ? param.displayName : + <input type='text' className='form-control' required defaultValue={param.name} + placeholder='display name' onChange={this.changeDisplayName}/> + } + </td> + <td> + { this.props.entryMode ? param.dataType : + <select className='form-control' defaultValue='STRING' + onChange={this.changeDataType}> + <option value='STRING'>String</option> + <option value='NUMBER'>Number</option> + <option value='BOOLEAN'>Boolean</option> + </select> + } + </td> + <td> + { this.props.entryMode ? param.collectionType : + <select className='form-control' required defaultValue='SINGLE' + onChange={this.changeCollectionType}> + <option value='SINGLE'>Single</option> + <option value='MULTIPLE'>Multiple</option> + </select> + } + + </td> + <td> + { !this.props.entryMode && (this.state.collectionType === 'SINGLE' ? + <input type='text' className='form-control' required value={this.state.defaultValue} + placeholder='default value' onChange={this.changeDefaultValue}/> : + <Multiselect messages={{createNew: 'Enter to add'}} + onCreate={this.addDefaultValue} + defaultValue={this.state.defaultValue} onKeyDown={this.preventEnter} + /> + )} + + { this.props.entryMode && (param.collectionType === 'SINGLE' ? + <input type='text' className='form-control' required value={this.state.defaultValue} + placeholder='default value' onChange={this.changeDefaultValue}/> : + <Multiselect messages={{createNew: 'Enter to add'}} + onCreate={this.addDefaultValue} + defaultValue={this.state.defaultValue} onKeyDown={this.preventEnter} + /> + )} + </td> + </tr> + ); + } + + // these methods change the default values + // called by normal input + changeDefaultValue (e) { + let val = validate(e.target.value, this.state.dataType); + + if (val[0]) this.setState({defaultValue: val[1]}); + } + + // called my multiselect + addDefaultValue (item) { + let val = validate(item, this.state.dataType); + + if (val[0]) { + this.state.defaultValue.push(val[1]); + this.setState(this.state); + } + } + + preventEnter (e) { + if (e.keyCode == 13) e.preventDefault(); + } + + changeDataType (e) { + let val = this.state.collectionType === 'SINGLE' ? null : []; + this.setState({dataType: e.target.value, defaultValue: val}); + } + + changeCollectionType (e) { + let val = e.target.value === 'MULTIPLE' ? [] : null; + this.setState({defaultValue: val}); + this.setState({collectionType: e.target.value}); + } + + changeDisplayName (e) { + this.setState({displayName: e.target.value}); + } +} + +QueryParamRow.propTypes = { + param: React.PropTypes.object.isRequired, + updateParam: React.PropTypes.func.isRequired, + entryMode: React.PropTypes.boolean +}; + +export default QueryParamRow; http://git-wip-us.apache.org/repos/asf/lens/blob/86714211/lens-ui/app/components/QueryParamsComponent.js ---------------------------------------------------------------------- diff --git a/lens-ui/app/components/QueryParamsComponent.js b/lens-ui/app/components/QueryParamsComponent.js new file mode 100644 index 0000000..a49e338 --- /dev/null +++ b/lens-ui/app/components/QueryParamsComponent.js @@ -0,0 +1,130 @@ +/** +* 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 { Button, Input } from 'react-bootstrap'; +import _ from 'lodash'; + +import QueryParamRow from './QueryParamRowComponent'; + +class QueryParams extends React.Component { + constructor (props) { + super(props); + this.state = {description: '', childrenParams: {}, runImmediately: false}; + + this.close = this.close.bind(this); + this.save = this.save.bind(this); + this.update = this.update.bind(this); + this.handleChange = this.handleChange.bind(this); + this.handleCheck = this.handleCheck.bind(this); + this._getChildrenParams = this._getChildrenParams.bind(this); + } + + componentWillReceiveProps (props) { + if (!_.isEqual(props.params, this.props.params)) { + this.state.childrenParams = {}; + } + } + + render () { + let params = this.props.params && this.props.params.map((param, index) => { + return <QueryParamRow key={param.name} param={param} updateParam={this.update}/>; + }); + + if (!params) return null; + + return ( + <form onSubmit={this.save} style={{padding: '10px', boxShadow: '2px 2px 2px 2px grey', + marginTop: '6px', backgroundColor: 'rgba(255, 255, 0, 0.1)'}}> + <h3> + Query Parameters + </h3> + <table className='table table-striped'> + <thead> + <tr> + <th>Parameter</th> + <th>Display Name</th> + <th>Data Type</th> + <th>Collection Type</th> + <th>Value</th> + </tr> + </thead> + <tbody> + {params} + </tbody> + </table> + <div className='form-group'> + <label className='sr-only' htmlFor='queryDescription'>Description</label> + <input type='text' className='form-control' style={{fontWeight: 'normal'}} + onChange={this.handleChange} id='queryDescription' + placeholder='(Optional description) e.g. This awesome query does magic along with its job.' + /> + </div> + <div> + <Input type='checkbox' label='Run after saving' + onChange={this.handleCheck} /> + + </div> + <Button bsStyle='primary' type='submit'>Save</Button> + <Button onClick={this.close} style={{marginLeft: '4px'}}>Cancel</Button> + </form> + ); + } + + close () { + this.props.close(); + } + + save (e) { + e.preventDefault(); + var parameters = this._getChildrenParams(); + this.props.saveParams({ + parameters: parameters, + description: this.state.description, + runImmediately: this.state.runImmediately + }); + } + + _getChildrenParams () { + return Object.keys(this.state.childrenParams).map(name => { + return this.state.childrenParams[name]; + }); + } + + handleChange (e) { + this.setState({description: e.target.value}); + } + + handleCheck (e) { + this.setState({runImmediately: e.target.checked}); + } + + // called by the child component {name, param} + update (param) { + this.state.childrenParams[param.name] = param.param; + } +} + +QueryParams.propTypes = { + params: React.PropTypes.array.isRequired, + close: React.PropTypes.func.isRequired, + saveParams: React.PropTypes.func.isRequired +}; + +export default QueryParams; http://git-wip-us.apache.org/repos/asf/lens/blob/86714211/lens-ui/app/components/QueryPreviewComponent.js ---------------------------------------------------------------------- diff --git a/lens-ui/app/components/QueryPreviewComponent.js b/lens-ui/app/components/QueryPreviewComponent.js index fabe383..a29f2d8 100644 --- a/lens-ui/app/components/QueryPreviewComponent.js +++ b/lens-ui/app/components/QueryPreviewComponent.js @@ -24,13 +24,12 @@ import CodeMirror from 'codemirror'; import 'codemirror/mode/sql/sql.js'; import 'codemirror/addon/runmode/runmode.js'; -import Loader from '../components/LoaderComponent'; import UserStore from '../stores/UserStore'; import AdhocQueryActions from '../actions/AdhocQueryActions'; class QueryPreview extends React.Component { constructor (props) { - super (props); + super(props); this.state = {showDetail: false}; this.toggleQueryDetails = this.toggleQueryDetails.bind(this); this.cancelQuery = this.cancelQuery.bind(this); @@ -48,11 +47,9 @@ class QueryPreview extends React.Component { CodeMirror .runMode(query.userQuery, 'text/x-mysql', function (text, style) { - // this method is called for every token and gives the // token and style class for it. codeTokens.push(<span className={'cm-' + style}>{text}</span>); - }); // figuring out the className for query status @@ -70,36 +67,35 @@ class QueryPreview extends React.Component { let statusClass = 'label-' + statusTypes[query.status.status] || 'label-info'; let handle = query.queryHandle.handleId; - let executionTime = (query.finishTime - query.submissionTime)/(1000*60); - let statusType = query.status.status === 'ERROR'? 'Error: ' : 'Status: '; + let executionTime = (query.finishTime - query.submissionTime) / (1000 * 60); + let statusType = query.status.status === 'ERROR' ? 'Error: ' : 'Status: '; let seeResult = ''; - let statusMessage = query.status.status === 'SUCCESSFUL'? + let statusMessage = query.status.status === 'SUCCESSFUL' ? query.status.statusMessage : query.status.errorMessage; if (query.status.status === 'SUCCESSFUL') { - seeResult = (<Link to="result" params={{handle: handle}} - className="btn btn-success btn-xs pull-right" style={{marginLeft: '5px'}}> + seeResult = (<Link to='result' params={{handle: handle}} + className='btn btn-success btn-xs pull-right' style={{marginLeft: '5px'}}> See Result </Link>); } - return ( <section> - <div className="panel panel-default"> - <pre className="cm-s-default" style={{cursor: 'pointer', + <div className='panel panel-default'> + <pre className='cm-s-default' style={{cursor: 'pointer', border: '0px', marginBottom: '0px'}} onClick={this.toggleQueryDetails}> {codeTokens} - <label className={"pull-right label " + statusClass}> + <label className={'pull-right label ' + statusClass}> {query.status.status} </label> {query.queryName && ( - <label className="pull-right label label-primary" + <label className='pull-right label label-primary' style={{marginRight: '5px'}}> {query.queryName} </label> @@ -108,32 +104,31 @@ class QueryPreview extends React.Component { </pre> {this.state.showDetail && ( - <div className="panel-body" style={{borderTop: '1px solid #cccccc', + <div className='panel-body' style={{borderTop: '1px solid #cccccc', paddingBottom: '0px'}} key={'preview' + handle}> - <div className="row"> - <div className="col-lg-4 col-sm-4"> - <span className="text-muted">Name </span> + <div className='row'> + <div className='col-lg-4 col-sm-4'> + <span className='text-muted'>Name </span> <strong>{ query.queryName || 'Not specified'}</strong> </div> - <div className="col-lg-4 col-sm-4"> - <span className="text-muted">Submitted </span> + <div className='col-lg-4 col-sm-4'> + <span className='text-muted'>Submitted </span> <strong> { Moment(query.submissionTime).format('Do MMM YY, hh:mm:ss a')} </strong> </div> - <div className="col-lg-4 col-sm-4"> - <span className="text-muted">Execution time </span> + <div className='col-lg-4 col-sm-4'> + <span className='text-muted'>Execution time </span> <strong> { executionTime > 0 ? - Math.ceil(executionTime) + - (executionTime > 1 ? ' mins': ' min') : - 'Still running' + Math.ceil(executionTime) + + (executionTime > 1 ? ' mins' : ' min') : 'Still running' } </strong> </div> </div> - <div className="row"> + <div className='row'> <div className={'alert alert-' + statusTypes[query.status.status]} style={{marginBottom: '0px', padding: '5px 15px 5px 15px'}}> @@ -143,8 +138,8 @@ class QueryPreview extends React.Component { {seeResult} - <Link to="query" query={{handle: query.queryHandle.handleId}} - className="pull-right"> + <Link to='query' query={{handle: query.queryHandle.handleId}} + className='pull-right'> Edit Query </Link> @@ -167,10 +162,14 @@ class QueryPreview extends React.Component { let handle = this.props && this.props.queryHandle && this.props.queryHandle.handleId; - if (!handle) return; + if (!handle) return; AdhocQueryActions.cancelQuery(secretToken, handle); } } +QueryPreview.propTypes = { + queryHandle: React.PropTypes.string +}; + export default QueryPreview;
