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/09 06:17:36 UTC

[31/50] [abbrv] lens git commit: LENS-629 : A new improved web client for Lens

http://git-wip-us.apache.org/repos/asf/lens/blob/66f164b4/lens-ui/app/components/QueryDetailResultComponent.js
----------------------------------------------------------------------
diff --git a/lens-ui/app/components/QueryDetailResultComponent.js b/lens-ui/app/components/QueryDetailResultComponent.js
new file mode 100644
index 0000000..b969a4a
--- /dev/null
+++ b/lens-ui/app/components/QueryDetailResultComponent.js
@@ -0,0 +1,192 @@
+/**
+* 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 Loader from '../components/LoaderComponent';
+import AdhocQueryStore from '../stores/AdhocQueryStore';
+import AdhocQueryActions from '../actions/AdhocQueryActions';
+import UserStore from '../stores/UserStore';
+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') {
+    return true;
+  }
+  return false;
+}
+
+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';
+    AdhocQueryActions.getQueryResult(secretToken, handle, mode);
+  } else {
+    AdhocQueryActions.getQuery(secretToken, handle);
+  }
+}
+
+function constructTable (tableData) {
+  if (!tableData.columns && !tableData.results) return;
+  let header = tableData.columns.map(column => {
+    return <th>{ column.name }</th>;
+  });
+  let rows = tableData.results
+    .map(row => {
+      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
+  if (!rows.length) {
+    let colWidth = tableData.columns.length;
+    rows = <tr>
+        <td colSpan={colWidth} style={{color: 'red', textAlign: 'center'}}>
+          Result set no longer available with server.</td>
+      </tr>;
+  }
+
+  return (
+    <div class="table-responsive">
+      <table className="table table-striped table-condensed">
+        <thead>
+          <tr>{header}</tr>
+        </thead>
+        <tbody>{rows}</tbody>
+      </table>
+    </div>
+  );
+}
+
+class QueryDetailResult extends React.Component {
+  constructor (props) {
+    super(props);
+    this.state = { loading: true, queryResult: {}, query: null };
+    this._onChange = this._onChange.bind(this);
+    this.pollForResult = this.pollForResult.bind(this);
+  }
+
+  componentDidMount () {
+    let secretToken = UserStore.getUserDetails().secretToken;
+    this.pollForResult(secretToken, this.props.params.handle);
+
+    AdhocQueryStore.addChangeListener(this._onChange);
+  }
+
+  componentWillUnmount () {
+    clearInterval(interval);
+    AdhocQueryStore.removeChangeListener(this._onChange);
+  }
+
+  componentWillReceiveProps (props) {
+    this.state = { loading: true, queryResult: {}, query: null };
+    let secretToken = UserStore.getUserDetails().secretToken;
+    clearInterval(interval);
+    this.pollForResult(secretToken, props.params.handle);
+  }
+
+  render () {
+    let query = this.state.query;
+    let queryResult = this.state.queryResult;
+    let result = '';
+
+    // check if the query was persistent or in-memory
+    if (query && query.isPersistent && query.status.status === 'SUCCESSFUL') {
+      result = (<div className="text-center">
+        <a href={queryResult.downloadURL} download>
+          <span className="glyphicon glyphicon-download-alt	"></span> Click
+          here to download the results as a CSV file
+        </a>
+      </div>);
+    } else {
+      result = constructTable(this.state.queryResult);
+    }
+
+
+    if (this.state.loading) result = <Loader size="8px" margin="2px"></Loader>;
+
+    return (
+      <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>
+          <QueryPreview key={query && query.queryHandle.handleId}
+            {...query} />
+        </div>
+        {result}
+      </div>
+    </div>
+    );
+  }
+
+  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
+    // shouldn't but the backend API isn't stable enough, and if this call fails
+    // we'll not be able to show the results and it'll show a loader, thoughts?
+    fetchResult(secretToken, handle);
+
+    interval = setInterval(function () {
+      fetchResult(secretToken, handle);
+    }, 5000);
+  }
+
+  _onChange () {
+    let handle = this.props.params.handle;
+    let query = AdhocQueryStore.getQueries()[handle];
+    let result = AdhocQueryStore.getQueryResult(handle);
+    let loading = true;
+
+    let failed = query && query.status && query.status.status === 'FAILED';
+    let success = query && query.status && query.status.status === 'SUCCESSFUL';
+
+    if (failed || success && result) {
+      clearInterval(interval);
+      loading = false;
+    }
+
+    // check first if the query failed, clear the interval, and show it
+    // setState when query is successful AND we've the results OR it failed
+    let state = {
+      loading: loading,
+      queryResult: result || {}, // result can be undefined so guarding it
+      query: query
+    }
+
+    this.setState(state);
+
+  }
+}
+
+export default QueryDetailResult;

http://git-wip-us.apache.org/repos/asf/lens/blob/66f164b4/lens-ui/app/components/QueryOperationsComponent.js
----------------------------------------------------------------------
diff --git a/lens-ui/app/components/QueryOperationsComponent.js b/lens-ui/app/components/QueryOperationsComponent.js
new file mode 100644
index 0000000..a17a636
--- /dev/null
+++ b/lens-ui/app/components/QueryOperationsComponent.js
@@ -0,0 +1,87 @@
+/**
+* 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 { Link } from 'react-router';
+import ClassNames from 'classnames';
+
+class QueryOperations extends React.Component {
+  constructor () {
+    super();
+    this.state = { isCollapsed: false };
+    this.toggle = this.toggle.bind(this);
+  }
+
+  toggle () {
+    this.setState({ isCollapsed: !this.state.isCollapsed });
+  }
+
+  render () {
+    let collapseClass = ClassNames({
+      'pull-right': true,
+      'glyphicon': true,
+      'glyphicon-chevron-up': !this.state.isCollapsed,
+      'glyphicon-chevron-down': this.state.isCollapsed
+    });
+
+    let panelBodyClassName = ClassNames({
+      'panel-body': true,
+      'hide': this.state.isCollapsed
+    });
+
+    return (
+      <div className="panel panel-default">
+        <div className="panel-heading">
+          <h3 className="panel-title">
+            Queries
+            <span className={collapseClass} onClick={this.toggle}></span>
+          </h3>
+        </div>
+        <div className={panelBodyClassName}>
+          <ul style={{listStyle: 'none', paddingLeft: '0px',
+            marginBottom: '0px'}}>
+            <li><Link to="results">All</Link></li>
+            <li>
+              <Link to="results" query={{category: 'running'}}>
+                Running
+              </Link>
+            </li>
+            <li>
+              <Link to="results" query={{category: 'successful'}}>
+                Completed
+              </Link>
+            </li>
+            <li>
+              <Link to="results" query={{category: 'queued'}}>
+                Queued
+              </Link>
+            </li>
+            <li>
+              <Link to="results" query={{category: 'failed'}}>
+                Failed
+              </Link>
+            </li>
+          </ul>
+        </div>
+      </div>
+    );
+  }
+}
+
+export default QueryOperations;

http://git-wip-us.apache.org/repos/asf/lens/blob/66f164b4/lens-ui/app/components/QueryPreviewComponent.js
----------------------------------------------------------------------
diff --git a/lens-ui/app/components/QueryPreviewComponent.js b/lens-ui/app/components/QueryPreviewComponent.js
new file mode 100644
index 0000000..fabe383
--- /dev/null
+++ b/lens-ui/app/components/QueryPreviewComponent.js
@@ -0,0 +1,176 @@
+/**
+* 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 Moment from 'moment';
+import { Link } from 'react-router';
+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);
+    this.state = {showDetail: false};
+    this.toggleQueryDetails = this.toggleQueryDetails.bind(this);
+    this.cancelQuery = this.cancelQuery.bind(this);
+  }
+
+	render () {
+    let query = this.props;
+
+    if (!query.userQuery) return null;
+
+    // code below before the return prepares the data to render, turning out
+    // crude properties to glazed, garnished ones e.g. formatting of query
+    let codeTokens = [];
+
+    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
+    // TODO optimize this construct
+    let statusTypes = {
+      'EXECUTED': 'success',
+      'SUCCESSFUL': 'success',
+      'FAILED': 'danger',
+      'CANCELED': 'danger',
+      'CLOSED': 'warning',
+      'QUEUED': 'info',
+      'RUNNING': 'info'
+    };
+
+    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 seeResult = '';
+    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'}}>
+        See Result
+      </Link>);
+    }
+
+
+    return (
+      <section>
+        <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}>
+              {query.status.status}
+            </label>
+
+            {query.queryName && (
+              <label className="pull-right label label-primary"
+                style={{marginRight: '5px'}}>
+                {query.queryName}
+              </label>
+            )}
+
+          </pre>
+
+          {this.state.showDetail && (
+            <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>
+                  <strong>{ query.queryName || 'Not specified'}</strong>
+                </div>
+                <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>
+                  <strong>
+
+                    { executionTime > 0 ?
+                        Math.ceil(executionTime) +
+                          (executionTime > 1 ? ' mins': ' min') :
+                        'Still running'
+                    }
+                  </strong>
+                </div>
+              </div>
+              <div className="row">
+                <div
+                  className={'alert alert-' + statusTypes[query.status.status]}
+                  style={{marginBottom: '0px', padding: '5px 15px 5px 15px'}}>
+                    <p>
+                      <strong>{statusType}</strong>
+                      {statusMessage || query.status.status}
+
+                      {seeResult}
+
+                      <Link to="query" query={{handle: query.queryHandle.handleId}}
+                        className="pull-right">
+                        Edit Query
+                      </Link>
+
+                    </p>
+                </div>
+              </div>
+            </div>
+          )}
+        </div>
+      </section>
+    );
+  }
+
+  toggleQueryDetails () {
+    this.setState({ showDetail: !this.state.showDetail });
+  }
+
+  cancelQuery () {
+    let secretToken = UserStore.getUserDetails().secretToken;
+    let handle = this.props && this.props.queryHandle &&
+      this.props.queryHandle.handleId;
+
+    if (!handle)  return;
+
+    AdhocQueryActions.cancelQuery(secretToken, handle);
+  }
+}
+
+export default QueryPreview;

http://git-wip-us.apache.org/repos/asf/lens/blob/66f164b4/lens-ui/app/components/QueryResultsComponent.js
----------------------------------------------------------------------
diff --git a/lens-ui/app/components/QueryResultsComponent.js b/lens-ui/app/components/QueryResultsComponent.js
new file mode 100644
index 0000000..6e4b8c2
--- /dev/null
+++ b/lens-ui/app/components/QueryResultsComponent.js
@@ -0,0 +1,123 @@
+/**
+* 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 Loader from '../components/LoaderComponent';
+import AdhocQueryStore from '../stores/AdhocQueryStore';
+import UserStore from '../stores/UserStore';
+import AdhocQueryActions from '../actions/AdhocQueryActions';
+import QueryPreview from './QueryPreviewComponent';
+
+// this method fetches the results based on props.query.category
+function getResults (props) {
+  let email = UserStore.getUserDetails().email;
+  let secretToken = UserStore.getUserDetails().secretToken;
+
+  if (props.query.category) {
+
+    // fetch either running or completed results
+    AdhocQueryActions
+      .getQueries(secretToken, email, { state: props.query.category });
+  } else {
+
+    // fetch all
+    AdhocQueryActions.getQueries(secretToken, email);
+  }
+}
+
+function getQueries () {
+  return AdhocQueryStore.getQueries();
+}
+
+class QueryResults extends React.Component {
+  constructor (props) {
+    super(props);
+    this.state = { queries: {}, queriesReceived: false };
+    this._onChange = this._onChange.bind(this);
+
+    getResults(props);
+  }
+
+  componentDidMount () {
+    AdhocQueryStore.addChangeListener(this._onChange);
+  }
+
+  componentWillUnmount () {
+    AdhocQueryStore.removeChangeListener(this._onChange);
+  }
+
+  componentWillReceiveProps (props) {
+    getResults(props);
+    this.setState({queries: {}, queriesReceived: false});
+  }
+
+  render () {
+    let queries = '';
+
+    let queryMap = this.state.queries;
+    queries = Object.keys(queryMap)
+      .sort(function (a, b) {
+        return queryMap[b].submissionTime - queryMap[a].submissionTime;
+      })
+      .map((queryHandle) => {
+      let query = queryMap[queryHandle];
+
+      return (
+        <QueryPreview key={query.queryHandle.handleId} {...query} />
+      );
+    }); // end of map
+
+    // FIXME find a better way to do it.
+    // show a loader when queries are empty, or no queries.
+    // this is managed by seeing the length of queries and
+    // a state variable 'queriesReceived'.
+    // if queriesReceived is true and the length is 0, show no queries else
+    // show a loader
+    let queriesLength = Object.keys(this.state.queries).length;
+
+    if (!queriesLength && !this.state.queriesReceived) {
+      queries = <Loader size="8px" margin="2px" />;
+    } else if (!queriesLength && this.state.queriesReceived) {
+      queries = <div className="alert alert-danger">
+        <strong>Sorry</strong>, there were no queries to be shown.
+      </div>;
+    }
+
+    return (
+      <section>
+        <div style={{border: '1px solid #dddddd', borderRadius: '4px',
+          padding: '0px 8px 8px 8px'}}>
+          <h3 style={{margin: '8px 10px'}}>Results</h3>
+          <hr style={{marginTop: '6px' }}/>
+          <div style={{overflowY: 'auto',
+            maxHeight: this.props.toggleQueryBox ? '300px': '600px'}}>
+            {queries}
+          </div>
+        </div>
+      </section>
+    );
+  }
+
+  _onChange () {
+    this.setState({ queries: getQueries(), queriesReceived: true});
+  }
+}
+
+export default QueryResults;

http://git-wip-us.apache.org/repos/asf/lens/blob/66f164b4/lens-ui/app/components/RequireAuthenticationComponent.js
----------------------------------------------------------------------
diff --git a/lens-ui/app/components/RequireAuthenticationComponent.js b/lens-ui/app/components/RequireAuthenticationComponent.js
new file mode 100644
index 0000000..9a755b0
--- /dev/null
+++ b/lens-ui/app/components/RequireAuthenticationComponent.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.
+*/
+
+import React from 'react';
+import UserStore from '../stores/UserStore';
+
+let RequireAuthentication = (Component) => {
+  return class Authenticated extends React.Component {
+    static willTransitionTo (transition) {
+      if (!UserStore.isUserLoggedIn()) {
+        transition.redirect('/login', {}, {'nextPath': transition.path});
+      }
+    }
+
+    render () {
+      return <Component {...this.props} />
+    }
+  }
+};
+
+export default RequireAuthentication;

http://git-wip-us.apache.org/repos/asf/lens/blob/66f164b4/lens-ui/app/components/SidebarComponent.js
----------------------------------------------------------------------
diff --git a/lens-ui/app/components/SidebarComponent.js b/lens-ui/app/components/SidebarComponent.js
new file mode 100644
index 0000000..dcc8737
--- /dev/null
+++ b/lens-ui/app/components/SidebarComponent.js
@@ -0,0 +1,38 @@
+/**
+* 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 CubeTree from './CubeTreeComponent';
+import Database from './DatabaseComponent';
+import QueryOperations from './QueryOperationsComponent';
+
+class Sidebar extends React.Component {
+  render() {
+    return (
+      <section>
+        <QueryOperations />
+        <CubeTree />
+        <Database />
+      </section>
+    );
+  }
+};
+
+export default Sidebar;

http://git-wip-us.apache.org/repos/asf/lens/blob/66f164b4/lens-ui/app/components/TableSchemaComponent.js
----------------------------------------------------------------------
diff --git a/lens-ui/app/components/TableSchemaComponent.js b/lens-ui/app/components/TableSchemaComponent.js
new file mode 100644
index 0000000..67dc25a
--- /dev/null
+++ b/lens-ui/app/components/TableSchemaComponent.js
@@ -0,0 +1,131 @@
+/**
+* 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 TableStore from '../stores/TableStore';
+import UserStore from '../stores/UserStore';
+import AdhocQueryActions from '../actions/AdhocQueryActions';
+import Loader from '../components/LoaderComponent';
+
+function getTable (tableName, database) {
+  let tables = TableStore.getTables(database);
+  return tables && tables[tableName];
+}
+
+class TableSchema extends React.Component {
+  constructor (props) {
+    super(props);
+    this.state = {table: {}};
+    this._onChange = this._onChange.bind(this);
+
+    let secretToken = UserStore.getUserDetails().secretToken;
+    let tableName = props.params.tableName;
+    let database = props.query.database;
+    AdhocQueryActions.getTableDetails(secretToken, tableName, database);
+  }
+
+  componentDidMount () {
+    TableStore.addChangeListener(this._onChange);
+  }
+
+  componentWillUnmount () {
+    TableStore.removeChangeListener(this._onChange);
+  }
+
+  componentWillReceiveProps (props) {
+    let tableName = props.params.tableName;
+    let database = props.query.database;
+    if (!TableStore.getTables(database)[tableName].isLoaded) {
+      let secretToken = UserStore.getUserDetails().secretToken;
+
+      AdhocQueryActions
+        .getTableDetails(secretToken, tableName, database);
+
+      // set empty state as we do not have the loaded data.
+      this.setState({table: {}});
+      return;
+    }
+
+    this.setState({
+      table: TableStore.getTables(database)[tableName]
+    });
+  }
+
+  render () {
+    let schemaSection = null;
+
+
+    if (this.state.table && !this.state.table.isLoaded) {
+      schemaSection = <Loader size="8px" margin="2px" />;
+    } else {
+      schemaSection = (<div className="row">
+          <div className="table-responsive">
+            <table className="table table-striped">
+              <thead>
+              <caption className="bg-primary text-center">Columns</caption>
+                <tr><th>Name</th><th>Type</th><th>Description</th></tr>
+              </thead>
+              <tbody>
+                {this.state.table &&
+                  this.state.table.columns.map(col => {
+                    return (
+                      <tr key={this.state.table.name + '|' + col.name}>
+                        <td>{col.name}</td>
+                        <td>{col.type}</td>
+                        <td>{col.comment || 'No description available'}</td>
+                      </tr>
+                    )
+                })}
+              </tbody>
+            </table>
+          </div>
+        </div>);
+    }
+
+    return (
+      <section>
+        <div className="panel panel-default">
+          <div className="panel-heading">
+            <h3 className="panel-title">Schema Details: &nbsp;
+              <strong className="text-primary">
+                 {this.props.query.database}.{this.props.params.tableName}
+              </strong>
+            </h3>
+          </div>
+          <div className="panel-body" style={{overflowY: 'auto',
+            maxHeight: this.props.toggleQueryBox ? '260px': '480px'}}>
+            {schemaSection}
+          </div>
+        </div>
+
+      </section>
+
+    );
+  }
+
+  _onChange () {
+    this.setState({
+      table: getTable(this.props.params.tableName,
+        this.props.query.database)
+    });
+  }
+}
+
+export default TableSchema;

http://git-wip-us.apache.org/repos/asf/lens/blob/66f164b4/lens-ui/app/components/TableTreeComponent.js
----------------------------------------------------------------------
diff --git a/lens-ui/app/components/TableTreeComponent.js b/lens-ui/app/components/TableTreeComponent.js
new file mode 100644
index 0000000..026e443
--- /dev/null
+++ b/lens-ui/app/components/TableTreeComponent.js
@@ -0,0 +1,238 @@
+/**
+* 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 TreeView from 'react-treeview';
+import { Link } from 'react-router';
+import 'react-treeview/react-treeview.css';
+import ClassNames from 'classnames';
+
+import TableStore from '../stores/TableStore';
+import AdhocQueryActions from '../actions/AdhocQueryActions';
+import UserStore from '../stores/UserStore';
+import Loader from '../components/LoaderComponent';
+import '../styles/css/tree.css';
+
+let filterString = '';
+
+function getState (page, filterString, database) {
+  let state = getTables(page, filterString, database);
+  state.page = page;
+  state.loading = false;
+  return state;
+}
+
+function getTables (page, filterString, database) {
+
+  // get all the tables
+  let tables = TableStore.getTables(database);
+  let pageSize = 10;
+  let allTables;
+  let startIndex;
+  let relevantIndexes;
+  let pageTables;
+
+  if (!filterString) {
+
+    // no need for filtering
+    allTables = Object.keys(tables);
+  } else {
+
+    // filter
+    allTables = Object.keys(tables).map(name => {
+      if (name.match(filterString)) return name;
+    }).filter(name => { return !!name; });
+  }
+
+  startIndex = (page - 1) * pageSize;
+  relevantIndexes = allTables.slice(startIndex, startIndex + pageSize);
+  pageTables = relevantIndexes.map(name => {
+    return tables[name];
+  });
+
+  return {
+    totalPages: Math.ceil(allTables.length/pageSize),
+    tables: pageTables
+  };
+}
+
+class TableTree extends React.Component {
+  constructor (props) {
+    super(props);
+    this.state = {
+      tables: [],
+      totalPages: 0,
+      page: 0,
+      loading: true,
+      isCollapsed: false
+    };
+    this._onChange = this._onChange.bind(this);
+    this.prevPage = this.prevPage.bind(this);
+    this.nextPage = this.nextPage.bind(this);
+    this.toggle = this.toggle.bind(this);
+    this.validateClickEvent = this.validateClickEvent.bind(this);
+
+    if (!TableStore.getTables(props.database)) {
+      AdhocQueryActions
+        .getTables(UserStore.getUserDetails().secretToken, props.database);
+    } else {
+      let state = getState(1, '', props.database);
+      this.state = state;
+
+      // on page refresh only a single table is fetched, and hence we need to
+      // fetch others too.
+      if (!TableStore.areTablesCompletelyFetched(props.database)) {
+        AdhocQueryActions
+          .getTables(UserStore.getUserDetails().secretToken, props.database);
+      }
+    }
+  }
+
+  componentDidMount () {
+    TableStore.addChangeListener(this._onChange);
+
+    // listen for opening tree
+    this.refs.tableTree.getDOMNode()
+      .addEventListener('click', this.validateClickEvent);
+  }
+
+  componentWillUnmount () {
+    this.refs.tableTree.getDOMNode()
+      .removeEventListener('click', this.validateClickEvent);
+    TableStore.removeChangeListener(this._onChange);
+  }
+
+  render () {
+    let tableTree = '';
+
+    // construct tree
+    tableTree = this.state.tables.map(table => {
+      let label = (<Link to="tableschema" params={{tableName: table.name}}
+        title={table.name} query={{database: this.props.database}}>
+          {table.name}</Link>);
+      return (
+        <TreeView key={table.name} nodeLabel={label}
+          defaultCollapsed={true}>
+
+          {table.isLoaded ? table.columns.map(col => {
+            return (
+              <div className="treeNode" key={name + '|' + col.name}>
+                {col.name} ({col.type})
+              </div>
+            );
+          }) : <Loader size="4px" margin="2px" />}
+
+        </TreeView>
+      );
+    });
+
+    // show a loader when tree is loading
+    if (this.state.loading) {
+      tableTree = <Loader size="4px" margin="2px" />;
+    } else if (!this.state.tables.length) {
+      tableTree = (<div className="alert-danger" style={{padding: '8px 5px'}}>
+          <strong>Sorry, we couldn&#39;t find any tables.</strong>
+        </div>);
+    }
+
+    let pagination = this.state.tables.length ?
+      (
+        <div>
+          <div className="text-center">
+            <button className="btn btn-link glyphicon glyphicon-triangle-left page-back"
+              onClick={this.prevPage}>
+            </button>
+            <span>{this.state.page} of {this.state.totalPages}</span>
+            <button className="btn btn-link glyphicon glyphicon-triangle-right page-next"
+              onClick={this.nextPage}>
+            </button>
+          </div>
+        </div>
+      ) :
+      null;
+
+    return (
+      <div>
+        { !this.state.loading &&
+          <div className="form-group">
+            <input type="search" className="form-control"
+              placeholder="Type to filter tables"
+              onChange={this._filter.bind(this)}/>
+          </div>
+        }
+
+        {pagination}
+
+        <div ref="tableTree" style={{maxHeight: '350px', overflowY: 'auto'}}>
+          {tableTree}
+        </div>
+      </div>
+    );
+  }
+
+  _onChange (page) {
+
+    // so that page doesn't reset to beginning
+    page = page || this.state.page || 1;
+    this.setState(getState(page, filterString, this.props.database));
+  }
+
+  getDetails (tableName, database) {
+
+    // find the table
+    let table = this.state.tables.filter(table => {
+      return tableName === table.name;
+    });
+
+    if (table.length && table[0].isLoaded) return;
+
+    AdhocQueryActions
+      .getTableDetails(UserStore.getUserDetails().secretToken, tableName,
+        database);
+  }
+
+  _filter (event) {
+    filterString = event.target.value;
+    this._onChange();
+  }
+
+  prevPage () {
+    if (this.state.page - 1) this._onChange(this.state.page - 1);
+  }
+
+  nextPage () {
+    if (this.state.page < this.state.totalPages) {
+      this._onChange(this.state.page + 1);
+    }
+  }
+
+  toggle () {
+    this.setState({ isCollapsed: !this.state.isCollapsed });
+  }
+
+  validateClickEvent (e) {
+    if (e.target && e.target.nodeName === 'DIV' &&
+      e.target.nextElementSibling.nodeName === 'A') {
+      this.getDetails(e.target.nextElementSibling.textContent, this.props.database);
+    }
+  }
+
+}
+
+export default TableTree;

http://git-wip-us.apache.org/repos/asf/lens/blob/66f164b4/lens-ui/app/constants/AdhocQueryConstants.js
----------------------------------------------------------------------
diff --git a/lens-ui/app/constants/AdhocQueryConstants.js b/lens-ui/app/constants/AdhocQueryConstants.js
new file mode 100644
index 0000000..3c4f93a
--- /dev/null
+++ b/lens-ui/app/constants/AdhocQueryConstants.js
@@ -0,0 +1,51 @@
+/**
+* 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 KeyMirror from 'keyMirror';
+
+const AdhocQueryConstants = KeyMirror({
+  RECEIVE_CUBES: null,
+  RECEIVE_CUBES_FAILED: null,
+
+  RECEIVE_QUERY_HANDLE: null,
+  RECEIVE_QUERY_HANDLE_FAILED: null,
+
+  RECEIVE_CUBE_DETAILS: null,
+  RECEIVE_CUBE_DETAILS_FAILED: null,
+
+  RECEIVE_QUERIES: null,
+  RECEIVE_QUERIES_FAILED: null,
+
+  RECEIVE_QUERY_RESULT: null,
+  RECEIVE_QUERY_RESULT_FAILED: null,
+
+  RECEIVE_TABLES: null,
+  RECEIVE_TABLES_FAILED: null,
+
+  RECEIVE_TABLE_DETAILS: null,
+  RECEIVE_TABLE_DETAILS_FAILED: null,
+
+  RECEIVE_QUERY: null,
+  RECEIVE_QUERY_FAILED: null,
+
+  RECEIVE_DATABASES: null,
+  RECEIVE_DATABASES_FAILED: null
+});
+
+export default AdhocQueryConstants;

http://git-wip-us.apache.org/repos/asf/lens/blob/66f164b4/lens-ui/app/constants/AppConstants.js
----------------------------------------------------------------------
diff --git a/lens-ui/app/constants/AppConstants.js b/lens-ui/app/constants/AppConstants.js
new file mode 100644
index 0000000..48cd93e
--- /dev/null
+++ b/lens-ui/app/constants/AppConstants.js
@@ -0,0 +1,27 @@
+/**
+* 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 KeyMirror from 'keyMirror';
+
+const AppConstants = KeyMirror({
+  AUTHENTICATION_SUCCESS: null,
+  AUTHENTICATION_FAILED: null
+});
+
+export default AppConstants;

http://git-wip-us.apache.org/repos/asf/lens/blob/66f164b4/lens-ui/app/dispatcher/AppDispatcher.js
----------------------------------------------------------------------
diff --git a/lens-ui/app/dispatcher/AppDispatcher.js b/lens-ui/app/dispatcher/AppDispatcher.js
new file mode 100644
index 0000000..31b267c
--- /dev/null
+++ b/lens-ui/app/dispatcher/AppDispatcher.js
@@ -0,0 +1,15 @@
+/**
+ * Copyright (c) 2014-2015, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ */
+
+import { Dispatcher } from 'flux';
+
+const AppDispatcher = new Dispatcher();
+
+export default AppDispatcher;

http://git-wip-us.apache.org/repos/asf/lens/blob/66f164b4/lens-ui/app/stores/AdhocQueryStore.js
----------------------------------------------------------------------
diff --git a/lens-ui/app/stores/AdhocQueryStore.js b/lens-ui/app/stores/AdhocQueryStore.js
new file mode 100644
index 0000000..7420270
--- /dev/null
+++ b/lens-ui/app/stores/AdhocQueryStore.js
@@ -0,0 +1,138 @@
+/**
+* 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 assign from 'object-assign';
+import { EventEmitter } from 'events';
+
+import AppDispatcher from '../dispatcher/AppDispatcher';
+import AdhocQueryConstants from '../constants/AdhocQueryConstants';
+import Config from 'config.json';
+
+var CHANGE_EVENT = 'change';
+var adhocDetails = {
+
+  queryHandle: null,
+  queries: {},
+  queryResults: {}, // map with handle being the key
+  dbName: Config.dbName
+};
+
+// TODO remove this.
+function receiveQueryHandle (payload) {
+  let id = payload.queryHandle.getElementsByTagName('handleId')[0].textContent;
+  adhocDetails.queryHandle = id;
+}
+
+function receiveQueries (payload) {
+  let queries = payload.queries;
+  let queryObjects = {};
+
+  queries.forEach((query) => {
+    queryObjects[query.queryHandle.handleId] = query;
+  });
+
+  adhocDetails.queries = queryObjects;
+}
+
+function receiveQuery (payload) {
+  let query = payload.query;
+  adhocDetails.queries[query.queryHandle.handleId] = query;
+}
+
+function receiveQueryResult (payload) {
+  let queryResult = {};
+  queryResult.type = payload && payload.type;
+
+  if (queryResult.type === 'INMEMORY') {
+    let resultRows = payload.queryResult && payload.queryResult.rows &&
+      payload.queryResult.rows.rows || [];
+    let columns = payload.columns && payload.columns.columns &&
+      payload.columns.columns.columns;
+
+    adhocDetails.queryResults[payload.handle] = {};
+    adhocDetails.queryResults[payload.handle].results = resultRows;
+    adhocDetails.queryResults[payload.handle].columns = columns;
+  } else {
+
+    // persistent
+    adhocDetails.queryResults[payload.handle] = {};
+    adhocDetails.queryResults[payload.handle].downloadURL = payload.downloadURL;
+  }
+}
+
+let AdhocQueryStore = assign({}, EventEmitter.prototype, {
+  getQueries () {
+    return adhocDetails.queries;
+  },
+
+  getQueryResult (handle) {
+    return adhocDetails.queryResults[handle];
+  },
+
+  // always returns the last-run-query's handle
+  getQueryHandle () {
+    return adhocDetails.queryHandle;
+  },
+
+  clearQueryHandle () {
+    adhocDetails.queryHandle = null;
+  },
+
+  getDbName () {
+    return adhocDetails.dbName
+  },
+
+  emitChange () {
+    this.emit(CHANGE_EVENT);
+  },
+
+  addChangeListener (callback) {
+    this.on(CHANGE_EVENT, callback);
+  },
+
+  removeChangeListener (callback) {
+    this.removeListener(CHANGE_EVENT, callback);
+  }
+});
+
+AppDispatcher.register((action) => {
+  switch(action.actionType) {
+
+    case AdhocQueryConstants.RECEIVE_QUERY_HANDLE:
+      receiveQueryHandle(action.payload);
+      AdhocQueryStore.emitChange();
+      break;
+
+    case AdhocQueryConstants.RECEIVE_QUERIES:
+      receiveQueries(action.payload);
+      AdhocQueryStore.emitChange();
+      break;
+
+    case AdhocQueryConstants.RECEIVE_QUERY_RESULT:
+      receiveQueryResult(action.payload);
+      AdhocQueryStore.emitChange();
+      break;
+
+    case AdhocQueryConstants.RECEIVE_QUERY:
+      receiveQuery(action.payload);
+      AdhocQueryStore.emitChange();
+  }
+});
+
+export default AdhocQueryStore;

http://git-wip-us.apache.org/repos/asf/lens/blob/66f164b4/lens-ui/app/stores/CubeStore.js
----------------------------------------------------------------------
diff --git a/lens-ui/app/stores/CubeStore.js b/lens-ui/app/stores/CubeStore.js
new file mode 100644
index 0000000..8b20b95
--- /dev/null
+++ b/lens-ui/app/stores/CubeStore.js
@@ -0,0 +1,84 @@
+/**
+* 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 AppDispatcher from '../dispatcher/AppDispatcher';
+import AdhocQueryConstants from '../constants/AdhocQueryConstants';
+import assign from 'object-assign';
+import { EventEmitter } from 'events';
+
+// private methods
+function receiveCubes (payload) {
+  payload.cubes.elements && payload.cubes.elements.forEach(cube => {
+    if (!cubes[cube]) {
+      cubes[cube] = { name: cube, isLoaded: false };
+    }
+  });
+}
+
+function receiveCubeDetails (payload) {
+  let cubeDetails = payload.cubeDetails;
+
+  let dimensions = cubeDetails.dim_attributes &&
+    cubeDetails.dim_attributes.dim_attribute;
+  let measures = cubeDetails.measures &&
+    cubeDetails.measures.measure;
+
+  cubes[cubeDetails.name].measures = measures;
+  cubes[cubeDetails.name].dimensions = dimensions;
+  cubes[cubeDetails.name].isLoaded = true;
+}
+
+
+let CHANGE_EVENT = 'change';
+var cubes = {};
+
+let CubeStore = assign({}, EventEmitter.prototype, {
+  getCubes () {
+    return cubes;
+  },
+
+  emitChange () {
+    this.emit(CHANGE_EVENT);
+  },
+
+  addChangeListener (callback) {
+    this.on(CHANGE_EVENT, callback);
+  },
+
+  removeChangeListener (callback) {
+    this.removeListener(CHANGE_EVENT, callback);
+  }
+});
+
+AppDispatcher.register((action) => {
+  switch(action.actionType) {
+    case AdhocQueryConstants.RECEIVE_CUBES:
+      receiveCubes(action.payload);
+      CubeStore.emitChange();
+      break;
+
+    case AdhocQueryConstants.RECEIVE_CUBE_DETAILS:
+      receiveCubeDetails(action.payload);
+      CubeStore.emitChange();
+      break;
+
+  }
+});
+
+export default CubeStore;

http://git-wip-us.apache.org/repos/asf/lens/blob/66f164b4/lens-ui/app/stores/DatabaseStore.js
----------------------------------------------------------------------
diff --git a/lens-ui/app/stores/DatabaseStore.js b/lens-ui/app/stores/DatabaseStore.js
new file mode 100644
index 0000000..9f4490b
--- /dev/null
+++ b/lens-ui/app/stores/DatabaseStore.js
@@ -0,0 +1,62 @@
+/**
+* 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 AppDispatcher from '../dispatcher/AppDispatcher';
+import AdhocQueryConstants from '../constants/AdhocQueryConstants';
+import assign from 'object-assign';
+import { EventEmitter } from 'events';
+
+function receiveDatabases (payload) {
+  databases = [];
+
+  databases = payload.databases.elements &&
+    payload.databases.elements.slice()
+}
+
+let CHANGE_EVENT = 'change';
+var databases = [];
+
+let DatabaseStore = assign({}, EventEmitter.prototype, {
+  getDatabases () {
+    return databases;
+  },
+
+  emitChange () {
+    this.emit(CHANGE_EVENT);
+  },
+
+  addChangeListener (callback) {
+    this.on(CHANGE_EVENT, callback);
+  },
+
+  removeChangeListener (callback) {
+    this.removeListener(CHANGE_EVENT, callback);
+  }
+});
+
+AppDispatcher.register((action) => {
+  switch(action.actionType) {
+    case AdhocQueryConstants.RECEIVE_DATABASES:
+      receiveDatabases(action.payload);
+      DatabaseStore.emitChange();
+      break;
+  }
+});
+
+export default DatabaseStore;

http://git-wip-us.apache.org/repos/asf/lens/blob/66f164b4/lens-ui/app/stores/TableStore.js
----------------------------------------------------------------------
diff --git a/lens-ui/app/stores/TableStore.js b/lens-ui/app/stores/TableStore.js
new file mode 100644
index 0000000..299d9e8
--- /dev/null
+++ b/lens-ui/app/stores/TableStore.js
@@ -0,0 +1,102 @@
+/**
+* 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 AppDispatcher from '../dispatcher/AppDispatcher';
+import AdhocQueryConstants from '../constants/AdhocQueryConstants';
+import assign from 'object-assign';
+import { EventEmitter } from 'events';
+
+function receiveTables (payload) {
+  let database = payload.database;
+
+  if (!tables[database]) {
+    tables[database] = {};
+    tableCompleteness[database] = true;
+  }
+
+  payload.tables.elements &&
+    payload.tables.elements.forEach( table => {
+      if (!tables[database][table]) {
+        tables[database][table] = { name: table, isLoaded: false };
+      }
+    });
+}
+
+function receiveTableDetails (payload) {
+  if (payload.tableDetails) {
+    let database = payload.database;
+    let name = payload.tableDetails.name;
+    let table = assign({}, payload.tableDetails);
+    let columns = table.columns && table.columns.column || [];
+    table.columns = columns;
+
+    // check if tables contains the database and table entry,
+    // it won't be present when user directly arrived on this link.
+    if (!tables[database]) {
+      tables[database] = {};
+    }
+
+    if (!tables[database][name]) tables[database][name] = {};
+
+    tables[database][name] = table;
+    tables[database][name].isLoaded = true;
+  }
+}
+
+let CHANGE_EVENT = 'change';
+var tables = {};
+var tableCompleteness = {};
+
+let TableStore = assign({}, EventEmitter.prototype, {
+  getTables (database) {
+    return tables[database];
+  },
+
+  areTablesCompletelyFetched (database) {
+    return tableCompleteness[database];
+  },
+
+  emitChange () {
+    this.emit(CHANGE_EVENT);
+  },
+
+  addChangeListener (callback) {
+    this.on(CHANGE_EVENT, callback);
+  },
+
+  removeChangeListener (callback) {
+    this.removeListener(CHANGE_EVENT, callback);
+  }
+});
+
+AppDispatcher.register((action) => {
+  switch(action.actionType) {
+    case AdhocQueryConstants.RECEIVE_TABLES:
+      receiveTables(action.payload);
+      TableStore.emitChange();
+      break;
+
+    case AdhocQueryConstants.RECEIVE_TABLE_DETAILS:
+      receiveTableDetails(action.payload);
+      TableStore.emitChange();
+      break;
+  }
+});
+
+export default TableStore;

http://git-wip-us.apache.org/repos/asf/lens/blob/66f164b4/lens-ui/app/stores/UserStore.js
----------------------------------------------------------------------
diff --git a/lens-ui/app/stores/UserStore.js b/lens-ui/app/stores/UserStore.js
new file mode 100644
index 0000000..47da021
--- /dev/null
+++ b/lens-ui/app/stores/UserStore.js
@@ -0,0 +1,132 @@
+/**
+* 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 AppDispatcher from '../dispatcher/AppDispatcher';
+import AppConstants from '../constants/AppConstants';
+import assign from 'object-assign';
+import { EventEmitter } from 'events';
+
+var CHANGE_EVENT = 'change';
+var userDetails = {
+  isUserLoggedIn: false,
+  email: '',
+  secretToken: '',
+  publicKey: ''
+};
+
+// keeping these methods out of the UserStore class as
+// components shouldn't lay their hands on them ;)
+function authenticateUser (details) {
+  userDetails = {
+    isUserLoggedIn: true,
+    email: details.email,
+    secretToken: new XMLSerializer().serializeToString(details.secretToken),
+    publicKey: details.secretToken.getElementsByTagName('publicId')[0]
+      .textContent
+  };
+
+  // store the details in localStorage if available
+
+  if (window.localStorage) {
+    let adhocCred = assign({}, userDetails, { timestamp: Date.now() });
+    window.localStorage.setItem('adhocCred', JSON.stringify(adhocCred));
+  }
+}
+
+function unauthenticateUser (details) {
+
+  // details contains error code and message
+  // which are not stored but passsed along
+  // during emitChange()
+  userDetails = {
+    isUserLoggedIn: false,
+    email: '',
+    secretToken: ''
+  };
+
+  // remove from localStorage as well
+  if (window.localStorage) localStorage.setItem('adhocCred', '');
+}
+
+// exposing only necessary methods for the components.
+var UserStore = assign({}, EventEmitter.prototype, {
+  isUserLoggedIn () {
+
+    if (userDetails && userDetails.isUserLoggedIn) {
+
+      return userDetails.isUserLoggedIn;
+    } else if (window.localStorage && localStorage.getItem('adhocCred')) {
+
+      // check in localstorage
+      let credentials = JSON.parse(localStorage.getItem('adhocCred'));
+
+      // check if it's valid or not
+      if (Date.now() - credentials.timestamp > 1800000) return false;
+
+      delete credentials.timestamp;
+      userDetails = assign({}, credentials);
+
+      return userDetails.isUserLoggedIn;
+    }
+
+    return false;
+  },
+
+  getUserDetails () {
+    return userDetails;
+  },
+
+  logout () {
+    unauthenticateUser();
+    this.emitChange();
+  },
+
+  emitChange (errorHash) {
+    this.emit(CHANGE_EVENT, errorHash);
+  },
+
+  addChangeListener (callback) {
+    this.on(CHANGE_EVENT, callback);
+  },
+
+  removeChangeListener (callback) {
+    this.removeListener(CHANGE_EVENT, callback);
+  }
+});
+
+// registering callbacks with the dispatcher. So verbose?? I know right!
+AppDispatcher.register((action) => {
+  switch(action.actionType) {
+    case AppConstants.AUTHENTICATION_SUCCESS:
+      authenticateUser(action.payload);
+      UserStore.emitChange();
+      break;
+
+    case AppConstants.AUTHENTICATION_FAILED:
+      unauthenticateUser(action.payload);
+
+      // action.payload => { responseCode, responseMessage }
+      UserStore.emitChange(action.payload);
+      break;
+  }
+});
+
+export default UserStore;

http://git-wip-us.apache.org/repos/asf/lens/blob/66f164b4/lens-ui/app/styles/css/global.css
----------------------------------------------------------------------
diff --git a/lens-ui/app/styles/css/global.css b/lens-ui/app/styles/css/global.css
new file mode 100644
index 0000000..131ab46
--- /dev/null
+++ b/lens-ui/app/styles/css/global.css
@@ -0,0 +1,18 @@
+/**
+* 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.
+*/

http://git-wip-us.apache.org/repos/asf/lens/blob/66f164b4/lens-ui/app/styles/css/login.css
----------------------------------------------------------------------
diff --git a/lens-ui/app/styles/css/login.css b/lens-ui/app/styles/css/login.css
new file mode 100644
index 0000000..b400cfb
--- /dev/null
+++ b/lens-ui/app/styles/css/login.css
@@ -0,0 +1,57 @@
+/**
+* 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.
+*/
+
+
+ /*For login form*/
+.form-signin {
+  max-width: 330px;
+  padding: 15px;
+  margin: 0 auto;
+}
+.form-signin .form-signin-heading,
+.form-signin .checkbox {
+  margin-bottom: 10px;
+}
+.form-signin .checkbox {
+  font-weight: normal;
+}
+.form-signin .form-control {
+  position: relative;
+  height: auto;
+  -webkit-box-sizing: border-box;
+     -moz-box-sizing: border-box;
+          box-sizing: border-box;
+  padding: 10px;
+  font-size: 16px;
+}
+.form-signin .form-control:focus {
+  z-index: 2;
+}
+.form-signin input[type="email"] {
+  margin-bottom: -1px;
+  border-bottom-right-radius: 0;
+  border-bottom-left-radius: 0;
+}
+.form-signin input[type="password"] {
+  margin-bottom: 10px;
+  border-top-left-radius: 0;
+  border-top-right-radius: 0;
+}
+
+/*login style ends*/

http://git-wip-us.apache.org/repos/asf/lens/blob/66f164b4/lens-ui/app/styles/css/query-component.css
----------------------------------------------------------------------
diff --git a/lens-ui/app/styles/css/query-component.css b/lens-ui/app/styles/css/query-component.css
new file mode 100644
index 0000000..a82165e
--- /dev/null
+++ b/lens-ui/app/styles/css/query-component.css
@@ -0,0 +1,34 @@
+/**
+* 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.
+*/
+
+
+ @media (max-width: 768px) {
+    .btn.responsive {
+        width:100%;
+        margin-bottom: 10px;
+    }
+}
+
+div.CodeMirror {
+  max-height: 150px;
+}
+
+li.CodeMirror-hint {
+  max-width: 100%;
+}

http://git-wip-us.apache.org/repos/asf/lens/blob/66f164b4/lens-ui/app/styles/css/tree.css
----------------------------------------------------------------------
diff --git a/lens-ui/app/styles/css/tree.css b/lens-ui/app/styles/css/tree.css
new file mode 100644
index 0000000..402c9a0
--- /dev/null
+++ b/lens-ui/app/styles/css/tree.css
@@ -0,0 +1,51 @@
+/**
+* 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.
+*/
+
+
+ .node {
+  font-weight: bold;
+}
+
+.treeNode.measureNode, .treeNode.childNode {
+  color: blue;
+}
+
+.treeNode.dimensionNode {
+  color: darkgreen;
+}
+
+.quiet {
+  color: #666;
+}
+
+.treeNode:hover {
+  background-color: #eee;
+}
+
+.page-next, .page-back {
+  margin: 2px 8px;
+  cursor: pointer;
+}
+
+div.tree-view {
+  -o-text-overflow: ellipsis;   /* Opera */
+  text-overflow:    ellipsis;   /* IE, Safari (WebKit) */
+  overflow:hidden;              /* don't show excess chars */
+  white-space:nowrap;           /* force single line */
+}

http://git-wip-us.apache.org/repos/asf/lens/blob/66f164b4/lens-ui/app/styles/less/globals.less
----------------------------------------------------------------------
diff --git a/lens-ui/app/styles/less/globals.less b/lens-ui/app/styles/less/globals.less
new file mode 100644
index 0000000..c0704dc
--- /dev/null
+++ b/lens-ui/app/styles/less/globals.less
@@ -0,0 +1,23 @@
+/**
+* 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.
+*/
+
+
+ // IMPORTS
+
+@import "~bootstrap/less/bootstrap.less";

http://git-wip-us.apache.org/repos/asf/lens/blob/66f164b4/lens-ui/config.json
----------------------------------------------------------------------
diff --git a/lens-ui/config.json b/lens-ui/config.json
new file mode 100644
index 0000000..3316bf6
--- /dev/null
+++ b/lens-ui/config.json
@@ -0,0 +1,4 @@
+{
+  "isPersistent": true,
+  "baseURL": "/serverproxy/"
+}

http://git-wip-us.apache.org/repos/asf/lens/blob/66f164b4/lens-ui/index.html
----------------------------------------------------------------------
diff --git a/lens-ui/index.html b/lens-ui/index.html
new file mode 100644
index 0000000..9c20fe9
--- /dev/null
+++ b/lens-ui/index.html
@@ -0,0 +1,100 @@
+<!--
+* 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.
+-->
+
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="utf-8">
+    <meta http-equiv="X-UA-Compatible" content="IE=edge">
+    <meta name="viewport" content="width=device-width, initial-scale=1">
+    <base href="/">
+    <!-- The above 3 meta tags *must* come first in the head; any other head content must come *after* these tags -->
+    <title>LENS UI</title>
+
+    <!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries -->
+    <!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
+    <!--[if lt IE 9]>
+      <script src="https://oss.maxcdn.com/html5shiv/3.7.2/html5shiv.min.js"></script>
+      <script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
+    <![endif]-->
+
+    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css">
+
+    <!-- style for the loader till JavaScript downloads-->
+    <style>
+      .loading-no-js {
+        position: absolute;
+        top: 50%;
+        left: 50%;
+        transform: translate(-50%, -50%);
+      }
+      .loading-bar {
+        display: inline-block;
+        width: 6px;
+        height: 36px;
+        border-radius: 4px;
+        animation: loading 1s ease-in-out infinite;
+      }
+      .loading-bar:nth-child(1) {
+        background-color: #3498db;
+        animation-delay: 0;
+      }
+      .loading-bar:nth-child(2) {
+        background-color: #c0392b;
+        animation-delay: 0.09s;
+      }
+      .loading-bar:nth-child(3) {
+        background-color: #f1c40f;
+        animation-delay: .18s;
+      }
+      .loading-bar:nth-child(4) {
+        background-color: #27ae60;
+        animation-delay: .27s;
+      }
+
+      @keyframes loading {
+        0% {
+          transform: scale(1);
+        }
+        20% {
+          transform: scale(1, 2.2);
+        }
+        40% {
+          transform: scale(1);
+        }
+      }
+    </style>
+  </head>
+  <body>
+
+  <div class="loading-no-js" id="loader-no-js">
+    <div class="loading-bar"></div>
+    <div class="loading-bar"></div>
+    <div class="loading-bar"></div>
+    <div class="loading-bar"></div>
+  </div>
+
+    <!-- everything goes into this section. Do anything but touch this. I dare you!-->
+    <section id="app">
+
+    </section>
+
+    <script src="target/assets/bundle.js"></script>
+  </body>
+</html>

http://git-wip-us.apache.org/repos/asf/lens/blob/66f164b4/lens-ui/package.json
----------------------------------------------------------------------
diff --git a/lens-ui/package.json b/lens-ui/package.json
new file mode 100644
index 0000000..920b120
--- /dev/null
+++ b/lens-ui/package.json
@@ -0,0 +1,51 @@
+{
+  "name": "lens-ui",
+  "version": "1.0.0",
+  "description": "An exemplary front end solution for Apache LENS",
+  "main": "app/app.js",
+  "scripts": {
+    "start": "NODE_ENV=production node_modules/webpack/bin/webpack.js -p && lensserver='http://0.0.0.0:9999/lensapi/' port=8082 node server.js",
+    "dev": "lensserver='http://0.0.0.0:9999/lensapi/' port=8082 node server.js & node_modules/webpack/bin/webpack.js --watch",
+    "deploy": "NODE_ENV=production node_modules/webpack/bin/webpack.js -p"
+  },
+  "dependencies": {
+    "bluebird": "^2.9.34",
+    "bootstrap": "^3.3.4",
+    "classnames": "^2.1.2",
+    "codemirror": "^5.3.0",
+    "flux": "^2.0.3",
+    "halogen": "^0.1.8",
+    "keymirror": "^0.1.1",
+    "lodash": "^3.9.1",
+    "moment": "^2.10.3",
+    "object-assign": "^2.0.0",
+    "q": "^1.4.1",
+    "react": "^0.13.3",
+    "react-bootstrap": "^0.22.6",
+    "react-router": "^0.13.3",
+    "react-treeview": "^0.3.12",
+    "reqwest": "^1.1.5"
+  },
+  "devDependencies": {
+    "autoprefixer-loader": "^1.2.0",
+    "babel-core": "^5.4.3",
+    "babel-loader": "^5.3.2",
+    "babel-runtime": "^5.7.0",
+    "body-parser": "^1.13.2",
+    "cookie-parser": "^1.3.5",
+    "css-loader": "^0.13.1",
+    "express": "^4.12.4",
+    "express-session": "^1.11.3",
+    "file-loader": "^0.8.1",
+    "http-proxy": "^1.11.1",
+    "json-loader": "^0.5.2",
+    "less": "^2.5.0",
+    "less-loader": "^2.2.0",
+    "morgan": "^1.6.1",
+    "node-libs-browser": "^0.5.0",
+    "serve-favicon": "^2.3.0",
+    "style-loader": "^0.12.2",
+    "url-loader": "^0.5.5",
+    "webpack": "^1.9.7"
+  }
+}

http://git-wip-us.apache.org/repos/asf/lens/blob/66f164b4/lens-ui/pom.xml
----------------------------------------------------------------------
diff --git a/lens-ui/pom.xml b/lens-ui/pom.xml
new file mode 100644
index 0000000..69bcee5
--- /dev/null
+++ b/lens-ui/pom.xml
@@ -0,0 +1,79 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+  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.
+-->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+  <name>Lens UI</name>
+  <parent>
+    <artifactId>apache-lens</artifactId>
+    <groupId>org.apache.lens</groupId>
+    <version>2.4.0-beta-SNAPSHOT</version>
+  </parent>
+
+  <artifactId>lens-ui</artifactId>
+  <packaging>pom</packaging>
+  <description>Lens UI client</description>
+
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-antrun-plugin</artifactId>
+        <version>${antrun.plugin.version}</version>
+        <configuration>
+          <target>
+            <zip destfile="target/${project.artifactId}-${project.version}" basedir="${project.basedir}" excludes="target/**" />
+          </target>
+        </configuration>
+      </plugin>
+      <plugin>
+        <groupId>com.github.eirslett</groupId>
+        <artifactId>frontend-maven-plugin</artifactId>
+        <version>${frontend.maven.plugin}</version>
+        <executions>
+          <execution>
+            <id>install node and npm</id>
+            <goals>
+              <goal>install-node-and-npm</goal>
+            </goals>
+            <configuration>
+              <nodeVersion>${nodeVersion}</nodeVersion>
+              <npmVersion>${npmVersion}</npmVersion>
+              <nodeDownloadRoot>https://nodejs.org/dist/</nodeDownloadRoot>
+              <npmDownloadRoot>http://registry.npmjs.org/npm/-/</npmDownloadRoot>
+              <installDirectory>node</installDirectory>
+            </configuration>
+          </execution>
+          <execution>
+            <id>npm install</id>
+            <goals>
+              <goal>npm</goal>
+            </goals>
+            <!-- Optional configuration which provides for running any npm command -->
+            <configuration>
+              <arguments>install</arguments>
+            </configuration>
+          </execution>
+        </executions>
+      </plugin>
+    </plugins>
+  </build>
+</project>

http://git-wip-us.apache.org/repos/asf/lens/blob/66f164b4/lens-ui/server.js
----------------------------------------------------------------------
diff --git a/lens-ui/server.js b/lens-ui/server.js
new file mode 100644
index 0000000..e812018
--- /dev/null
+++ b/lens-ui/server.js
@@ -0,0 +1,79 @@
+/**
+* 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.
+*/
+
+var express = require('express');
+var path = require('path');
+var favicon = require('serve-favicon');
+var logger = require('morgan');
+var cookieParser = require('cookie-parser');
+var bodyParser = require('body-parser');
+var session = require('express-session');
+
+var app = express();
+var httpProxy = require('http-proxy');
+var proxy = httpProxy.createProxyServer();
+var port = process.env['port'] || 8082;
+
+app.use(logger('dev'));
+app.use(bodyParser.json());
+app.use(bodyParser.urlencoded({ extended: false }));
+app.use(cookieParser());
+
+process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = '0';
+
+if(!process.env['lensserver']){
+  throw new Error('Specify LENS Server address in `lensserver` argument');
+}
+
+console.log('Using this as your LENS Server Address: ', process.env['lensserver']);
+console.log('If this seems wrong, please edit `lensserver` argument in package.json. Do not forget to append http://\n');
+
+app.use( session({
+   secret            : 'SomethingYouKnow',
+   resave            : false,
+   saveUninitialized : true
+}));
+
+var fs = require('fs');
+
+app.use(express.static(path.resolve(__dirname, 'target', 'assets')));
+
+app.get('/target/assets/*', function (req, res) {
+  res.setHeader('Cache-Control', 'public');
+  res.end(fs.readFileSync(__dirname + req.path));
+});
+
+app.all('/serverproxy/*', function (req, res) {
+  req.url = req.url.replace('serverproxy', '');
+  proxy.web(req, res, {
+      target: process.env['lensserver']
+  }, function (e) { console.log('Handled error.'); });
+});
+
+app.get('*', function(req, res) {
+  res.end(fs.readFileSync(__dirname + '/index.html'));
+});
+
+var server = app.listen(port, function(err) {
+  if(err) throw err;
+
+  var port = server.address().port;
+
+  console.log('Ad hoc UI server listening at port: ', port);
+});

http://git-wip-us.apache.org/repos/asf/lens/blob/66f164b4/lens-ui/webpack.config.js
----------------------------------------------------------------------
diff --git a/lens-ui/webpack.config.js b/lens-ui/webpack.config.js
new file mode 100644
index 0000000..ab4021f
--- /dev/null
+++ b/lens-ui/webpack.config.js
@@ -0,0 +1,55 @@
+/**
+* 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.
+*/
+
+var webpack = require('webpack');
+var path = require('path');
+
+
+module.exports = {
+
+  entry: {
+    app: [
+      './app/app.js'
+    ]
+  },
+
+	output: {
+    path: path.join(__dirname, 'target', 'assets'),
+		filename: 'bundle.js'
+	},
+
+  plugins: [
+    new webpack.NoErrorsPlugin()
+  ],
+
+  resolve: {
+    modulesDirectories: ['app', 'node_modules', __dirname]
+  },
+
+	module: {
+		loaders: [
+      { test: /\.jsx?$/, loaders: ['babel'], include: path.join(__dirname, 'app') },
+			{ test: /\.css$/, loaders: ['style', 'css'] },
+      { test: /\.less$/, loaders: ['style', 'css', 'autoprefixer', 'less'] },
+      { test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/, loaders: ['url?limit=10000&minetype=application/font-woff'] },
+      { test: /\.(ttf|eot|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/, loaders: ['file'] },
+      { test: /\.json$/, loaders: ['json']}
+		]
+	}
+};

http://git-wip-us.apache.org/repos/asf/lens/blob/66f164b4/pom.xml
----------------------------------------------------------------------
diff --git a/pom.xml b/pom.xml
index 10f9bc1..dd26713 100644
--- a/pom.xml
+++ b/pom.xml
@@ -107,6 +107,11 @@
     <antrun.plugin.version>1.8</antrun.plugin.version>
     <cobertura.plugin.version>2.7</cobertura.plugin.version>
 
+    <!-- UI -->
+    <frontend.maven.plugin>0.0.23</frontend.maven.plugin>
+    <nodeVersion>v4.0.0</nodeVersion>
+    <npmVersion>2.7.6</npmVersion>
+
     <!-- debian -->
     <mvn.deb.build.dir>${project.build.directory}/debian</mvn.deb.build.dir>
 
@@ -547,6 +552,8 @@
             <exclude>**/*.iml</exclude>
             <exclude>**/.classpath</exclude>
             <exclude>**/.project</exclude>
+            <exclude>**/node/**</exclude>
+            <exclude>**/node_modules/**</exclude>
             <exclude>**/.checkstyle</exclude>
             <exclude>**/.settings/**</exclude>
             <exclude>**/maven-eclipse.xml</exclude>
@@ -567,6 +574,7 @@
             <exclude>**/codemirror.min.*</exclude>
             <exclude>**/*.js</exclude>
             <exclude>**/*.properties</exclude>
+            <exclude>**/*.json</exclude>
           </excludes>
         </configuration>
         <executions>
@@ -1522,6 +1530,7 @@
     <module>lens-ml-lib</module>
     <module>lens-ml-dist</module>
     <module>lens-regression</module>
+    <module>lens-ui</module>
   </modules>
 
   <profiles>