You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@lens.apache.org by ra...@apache.org on 2015/10/08 18:56:23 UTC
[2/2] lens git commit: LENS-782: UI support for the saved,
parametrized query feature
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/master
Commit: 867142113fbbe2d0793e7da156cc4ef190c22811
Parents: dd33a60
Author: Ankeet Maini <an...@gmail.com>
Authored: Thu Oct 8 22:25:12 2015 +0530
Committer: raju <ra...@apache.org>
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;