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:
+ <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'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>