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: &nbsp;
-              <strong className="text-primary">
+        <div className='panel panel-default'>
+          <div className='panel-heading'>
+            <h3 className='panel-title'>Schema Details: &nbsp;
+              <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&#39;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&#39;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>&beta;</sup></Link>
+            <Link className='navbar-brand' to='app'>LENS Query<sup>&beta;</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&#39;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;