Repository: lens Updated Branches: refs/heads/master dd33a6058 -> 867142113
http://git-wip-us.apache.org/repos/asf/lens/blob/86714211/lens-ui/app/components/QueryResultsComponent.js ---------------------------------------------------------------------- diff --git a/lens-ui/app/components/QueryResultsComponent.js b/lens-ui/app/components/QueryResultsComponent.js index 6e4b8c2..01f0e30 100644 --- a/lens-ui/app/components/QueryResultsComponent.js +++ b/lens-ui/app/components/QueryResultsComponent.js @@ -19,7 +19,7 @@ import React from 'react'; -import Loader from '../components/LoaderComponent'; +import Loader from './LoaderComponent'; import AdhocQueryStore from '../stores/AdhocQueryStore'; import UserStore from '../stores/UserStore'; import AdhocQueryActions from '../actions/AdhocQueryActions'; @@ -31,12 +31,10 @@ function getResults (props) { 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); } @@ -77,12 +75,12 @@ class QueryResults extends React.Component { return queryMap[b].submissionTime - queryMap[a].submissionTime; }) .map((queryHandle) => { - let query = queryMap[queryHandle]; + let query = queryMap[queryHandle]; - return ( - <QueryPreview key={query.queryHandle.handleId} {...query} /> - ); - }); // end of map + 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. @@ -93,9 +91,9 @@ class QueryResults extends React.Component { let queriesLength = Object.keys(this.state.queries).length; if (!queriesLength && !this.state.queriesReceived) { - queries = <Loader size="8px" margin="2px" />; + queries = <Loader size='8px' margin='2px' />; } else if (!queriesLength && this.state.queriesReceived) { - queries = <div className="alert alert-danger"> + queries = <div className='alert alert-danger'> <strong>Sorry</strong>, there were no queries to be shown. </div>; } @@ -105,9 +103,8 @@ class QueryResults extends React.Component { <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'}}> + <hr style={{marginTop: '6px'}}/> + <div> {queries} </div> </div> @@ -116,7 +113,7 @@ class QueryResults extends React.Component { } _onChange () { - this.setState({ queries: getQueries(), queriesReceived: true}); + this.setState({queries: getQueries(), queriesReceived: true}); } } http://git-wip-us.apache.org/repos/asf/lens/blob/86714211/lens-ui/app/components/RequireAuthenticationComponent.js ---------------------------------------------------------------------- diff --git a/lens-ui/app/components/RequireAuthenticationComponent.js b/lens-ui/app/components/RequireAuthenticationComponent.js index 9a755b0..7b1e956 100644 --- a/lens-ui/app/components/RequireAuthenticationComponent.js +++ b/lens-ui/app/components/RequireAuthenticationComponent.js @@ -29,9 +29,9 @@ let RequireAuthentication = (Component) => { } render () { - return <Component {...this.props} /> + return <Component {...this.props} />; } - } + }; }; export default RequireAuthentication; http://git-wip-us.apache.org/repos/asf/lens/blob/86714211/lens-ui/app/components/SavedQueriesComponent.js ---------------------------------------------------------------------- diff --git a/lens-ui/app/components/SavedQueriesComponent.js b/lens-ui/app/components/SavedQueriesComponent.js new file mode 100644 index 0000000..378fa13 --- /dev/null +++ b/lens-ui/app/components/SavedQueriesComponent.js @@ -0,0 +1,180 @@ +/** +* 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 AdhocQueryActions from '../actions/AdhocQueryActions'; +import SavedQueryStore from '../stores/SavedQueryStore'; +import UserStore from '../stores/UserStore'; +import Loader from './LoaderComponent'; +import SavedQueryPreview from './SavedQueryPreviewComponent'; + +class SavedQueries extends React.Component { + constructor (props) { + super(props); + this.state = { + loading: true, + savedQueries: null, + page: null, + totalPages: null + }; + + this._onChange = this._onChange.bind(this); + this.prev = this.prev.bind(this); + this.next = this.next.bind(this); + this._getPaginatedSavedQueries = this._getPaginatedSavedQueries.bind(this); + + let secretToken = UserStore.getUserDetails().secretToken; + let user = UserStore.getUserDetails().email; + AdhocQueryActions.getSavedQueries(secretToken, user); + } + + componentDidMount () { + SavedQueryStore.addChangeListener(this._onChange); + } + + componentWillUnmount () { + SavedQueryStore.removeChangeListener(this._onChange); + } + + render () { + let loading = this.state.loading ? <Loader size='4px' margin='2px'/> : null; + + let queries = !this.state.savedQueries ? null : + Object.keys(this.state.savedQueries).map(queryId => { + return <SavedQueryPreview key={'saved|' + queryId} + query={this.state.savedQueries[queryId]} />; + }); + + // no saved queries + if (!this.state.loading && !queries) { + queries = (<div className='alert-danger' style={{padding: '8px 5px'}}> + <strong>Sorry, we couldn't find any saved queries.</strong> + </div>); + } + + if (!this.state.loading && this.state.totalPages == 0) { + queries = (<div className='alert-danger' style={{padding: '8px 5px'}}> + <strong>You've not saved any query.</strong> + </div>); + } + + var pagination = ( + <div className='pull-right'> + <button onClick={this.prev} + className='btn btn-link glyphicon glyphicon-triangle-left'> + </button> + <small> + { this.state.page && this.state.totalPages && + (this.state.page + ' of ' + this.state.totalPages) + } + </small> + <button onClick={this.next} + className='btn btn-link glyphicon glyphicon-triangle-right'> + </button> + </div> + ); + + return ( + <section> + <div style={{border: '1px solid #dddddd', borderRadius: '4px', + padding: '0px 8px 8px 8px'}}> + <h3 style={{margin: '8px 10px'}}> + Saved Queries + {pagination} + </h3> + <hr style={{marginTop: '6px'}}/> + <div> + {loading} + {queries} + </div> + </div> + </section> + ); + } + + prev () { + if (this.state.page > 1) this._onChange(this.state.page - 1); + } + + next () { + if (this.state.page < this.state.totalPages) { + this._onChange(this.state.page + 1); + } + } + + _onChange (page) { + // done to filter success/error messages from store + if (typeof page === 'object') page = this.state.page; + + var PAGE_SIZE = 10; + page = page || this.state.page || 1; + var state = { + savedQueries: this._getPaginatedSavedQueries(page, PAGE_SIZE), + page: page, + totalPages: Math.ceil(SavedQueryStore.getTotalRecords() / PAGE_SIZE) + }; + state.loading = !!(!state.savedQueries && state.totalPages); + this.setState(state); + } + + _getPaginatedSavedQueries (pageNumber, pageSize) { + if (!pageNumber && !pageSize) return; + + var token = UserStore.getUserDetails().secretToken; + var email = UserStore.getUserDetails().email; + var savedQueries = SavedQueryStore.getSavedQueries(); + var savedQueriesLength = savedQueries && Object.keys(savedQueries).length; + var totalQueries = SavedQueryStore.getTotalRecords(); + var relevantSavedQueries = null; + var startIndex = (pageNumber - 1) * pageSize; + if (savedQueriesLength > startIndex) { + relevantSavedQueries = Object.keys(savedQueries) + .slice(startIndex, startIndex + pageSize); + if ((totalQueries != savedQueriesLength) && (relevantSavedQueries.length < pageSize) && totalQueries) { + // call backend + AdhocQueryActions.getSavedQueries(token, email, { + offset: startIndex, + pageSize: pageSize + }); + + this.setState({loading: true}); + } + } else { + // trigger action + if (!totalQueries) return; + + AdhocQueryActions.getSavedQueries(token, email, { + offset: startIndex, + pageSize: pageSize + }); + + this.setState({loading: true}); + } + + var filteredQueries = relevantSavedQueries && relevantSavedQueries.map(id => { + return savedQueries[id]; + }); + + return filteredQueries; + } + +} + +export default SavedQueries; http://git-wip-us.apache.org/repos/asf/lens/blob/86714211/lens-ui/app/components/SavedQueryPreviewComponent.js ---------------------------------------------------------------------- diff --git a/lens-ui/app/components/SavedQueryPreviewComponent.js b/lens-ui/app/components/SavedQueryPreviewComponent.js new file mode 100644 index 0000000..4f9459a --- /dev/null +++ b/lens-ui/app/components/SavedQueryPreviewComponent.js @@ -0,0 +1,136 @@ +/** +* 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 CodeMirror from 'codemirror'; +import 'codemirror/mode/sql/sql.js'; +import 'codemirror/addon/runmode/runmode.js'; + +import QueryParamRowComponent from './QueryParamRowComponent'; +import AdhocQueryActions from '../actions/AdhocQueryActions'; +import UserStore from '../stores/UserStore'; + +class SavedQueryPreview extends React.Component { + constructor (props) { + super(props); + this.state = { showDetail: false, queryParams: {} }; + this.toggleQueryDetails = this.toggleQueryDetails.bind(this); + this.runSavedQuery = this.runSavedQuery.bind(this); + this.update = this.update.bind(this); + this.props.query && this.props.query.parameters.forEach(param => { + this.state.queryParams[param.name] = param; + }); + } + + render () { + let query = this.props.query; + + if (!query.query) 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.query, + '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>); + }); + + let params = query && query.parameters.map(param => { + return <QueryParamRowComponent param={param} entryMode={true} + updateParam={this.update}/>; + }); + + let paramsTable = !params.length ? null : + (<table className='table table-striped table-condensed'> + <thead> + <tr><th>Param</th><th>Display Name</th><th>Data Type</th> + <th>Collection Type</th><th>Value</th></tr> + </thead> + <tbody> + {params} + </tbody> + </table>); + + return ( + <section ref='preview'> + <div className='panel panel-default'> + <div className='panel-heading blue-header' style={{cursor: 'pointer', padding: '2px 8px'}} + onClick={this.toggleQueryDetails}> + <h5 className='panel-title' style={{marginBottom: '4px'}}> + {query.name || 'Unnamed Query'} + </h5> + <small className='italics'> + { query.description || 'No description available' } + </small> + </div> + + {this.state.showDetail && ( + <div className='panel-body' style={{borderTop: '1px solid #cccccc', + padding: '0px'}} key={'preview' + query.id}> + <pre className='cm-s-default' style={{ + border: '0px', marginBottom: '0px'}}> + {codeTokens} + + <Link to='query' query={{savedquery: query.id}} + className='btn btn-default pull-right'> + <i className='fa fa-pencil fa-lg'></i> + </Link> + <button className='btn btn-default pull-right' onClick={this.runSavedQuery}> + <i className='fa fa-play fa-lg'></i> + </button> + </pre> + + {paramsTable} + </div> + )} + </div> + </section> + ); + } + + toggleQueryDetails () { + this.setState({ showDetail: !this.state.showDetail }); + } + + update (param) { + this.state.queryParams[param.name] = param.param; + } + + runSavedQuery () { + let secretToken = UserStore.getUserDetails().secretToken; + let parameters = Object.keys(this.state.queryParams).map(name => { + let object = {}; + object[name] = this.state.queryParams[name].defaultValue; + return object; + }); + AdhocQueryActions.runSavedQuery(secretToken, this.props.query.id, parameters); + } +} + +SavedQueryPreview.propTypes = { + query: React.PropTypes.object, + params: React.PropTypes.object +}; + +export default SavedQueryPreview; http://git-wip-us.apache.org/repos/asf/lens/blob/86714211/lens-ui/app/components/SidebarComponent.js ---------------------------------------------------------------------- diff --git a/lens-ui/app/components/SidebarComponent.js b/lens-ui/app/components/SidebarComponent.js index dcc8737..37a2c59 100644 --- a/lens-ui/app/components/SidebarComponent.js +++ b/lens-ui/app/components/SidebarComponent.js @@ -24,7 +24,7 @@ import Database from './DatabaseComponent'; import QueryOperations from './QueryOperationsComponent'; class Sidebar extends React.Component { - render() { + render () { return ( <section> <QueryOperations /> @@ -33,6 +33,6 @@ class Sidebar extends React.Component { </section> ); } -}; +} export default Sidebar; http://git-wip-us.apache.org/repos/asf/lens/blob/86714211/lens-ui/app/components/TableSchemaComponent.js ---------------------------------------------------------------------- diff --git a/lens-ui/app/components/TableSchemaComponent.js b/lens-ui/app/components/TableSchemaComponent.js index 67dc25a..7abab95 100644 --- a/lens-ui/app/components/TableSchemaComponent.js +++ b/lens-ui/app/components/TableSchemaComponent.js @@ -22,7 +22,7 @@ import React from 'react'; import TableStore from '../stores/TableStore'; import UserStore from '../stores/UserStore'; import AdhocQueryActions from '../actions/AdhocQueryActions'; -import Loader from '../components/LoaderComponent'; +import Loader from './LoaderComponent'; function getTable (tableName, database) { let tables = TableStore.getTables(database); @@ -71,15 +71,14 @@ class TableSchema extends React.Component { render () { let schemaSection = null; - if (this.state.table && !this.state.table.isLoaded) { - schemaSection = <Loader size="8px" margin="2px" />; + schemaSection = <Loader size='8px' margin='2px' />; } else { - schemaSection = (<div className="row"> - <div className="table-responsive"> - <table className="table table-striped"> + schemaSection = (<div className='row'> + <div className='table-responsive'> + <table className='table table-striped'> <thead> - <caption className="bg-primary text-center">Columns</caption> + <caption className='bg-primary text-center'>Columns</caption> <tr><th>Name</th><th>Type</th><th>Description</th></tr> </thead> <tbody> @@ -91,8 +90,9 @@ class TableSchema extends React.Component { <td>{col.type}</td> <td>{col.comment || 'No description available'}</td> </tr> - ) - })} + ); + }) + } </tbody> </table> </div> @@ -101,16 +101,15 @@ class TableSchema extends React.Component { return ( <section> - <div className="panel panel-default"> - <div className="panel-heading"> - <h3 className="panel-title">Schema Details: - <strong className="text-primary"> + <div className='panel panel-default'> + <div className='panel-heading'> + <h3 className='panel-title'>Schema Details: + <strong className='text-primary'> {this.props.query.database}.{this.props.params.tableName} </strong> </h3> </div> - <div className="panel-body" style={{overflowY: 'auto', - maxHeight: this.props.toggleQueryBox ? '260px': '480px'}}> + <div className='panel-body'> {schemaSection} </div> </div> @@ -128,4 +127,9 @@ class TableSchema extends React.Component { } } +TableSchema.propTypes = { + query: React.PropTypes.object, + params: React.PropTypes.object +}; + export default TableSchema; http://git-wip-us.apache.org/repos/asf/lens/blob/86714211/lens-ui/app/components/TableTreeComponent.js ---------------------------------------------------------------------- diff --git a/lens-ui/app/components/TableTreeComponent.js b/lens-ui/app/components/TableTreeComponent.js index 026e443..59965d4 100644 --- a/lens-ui/app/components/TableTreeComponent.js +++ b/lens-ui/app/components/TableTreeComponent.js @@ -21,12 +21,11 @@ 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 Loader from './LoaderComponent'; import '../styles/css/tree.css'; let filterString = ''; @@ -39,8 +38,7 @@ function getState (page, filterString, database) { } function getTables (page, filterString, database) { - - // get all the tables + // get all the native tables let tables = TableStore.getTables(database); let pageSize = 10; let allTables; @@ -49,11 +47,9 @@ function getTables (page, filterString, database) { 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; @@ -67,7 +63,7 @@ function getTables (page, filterString, database) { }); return { - totalPages: Math.ceil(allTables.length/pageSize), + totalPages: Math.ceil(allTables.length / pageSize), tables: pageTables }; } @@ -123,20 +119,20 @@ class TableTree extends React.Component { // construct tree tableTree = this.state.tables.map(table => { - let label = (<Link to="tableschema" params={{tableName: table.name}} + 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}> + defaultCollapsed={!table.isLoaded}> {table.isLoaded ? table.columns.map(col => { return ( - <div className="treeNode" key={name + '|' + col.name}> + <div className='treeNode' key={table.name + '|' + col.name}> {col.name} ({col.type}) </div> ); - }) : <Loader size="4px" margin="2px" />} + }) : <Loader size='4px' margin='2px' />} </TreeView> ); @@ -144,42 +140,41 @@ class TableTree extends React.Component { // show a loader when tree is loading if (this.state.loading) { - tableTree = <Loader size="4px" margin="2px" />; + 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> + tableTree = (<div className='alert-danger' style={{padding: '8px 5px'}}> + <strong>Sorry, we couldn't find any.</strong> </div>); } let pagination = this.state.tables.length ? ( <div> - <div className="text-center"> - <button className="btn btn-link glyphicon glyphicon-triangle-left page-back" + <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" + <button className='btn btn-link glyphicon glyphicon-triangle-right page-next' onClick={this.nextPage}> </button> </div> </div> - ) : - null; + ) : null; return ( <div> { !this.state.loading && - <div className="form-group"> - <input type="search" className="form-control" - placeholder="Type to filter tables" + <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'}}> + <div ref='tableTree' style={{maxHeight: '350px', overflowY: 'auto'}}> {tableTree} </div> </div> @@ -187,14 +182,12 @@ class TableTree extends React.Component { } _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; @@ -235,4 +228,8 @@ class TableTree extends React.Component { } +TableTree.propTypes = { + database: React.PropTypes.string.isRequired +}; + export default TableTree; http://git-wip-us.apache.org/repos/asf/lens/blob/86714211/lens-ui/app/constants/AdhocQueryConstants.js ---------------------------------------------------------------------- diff --git a/lens-ui/app/constants/AdhocQueryConstants.js b/lens-ui/app/constants/AdhocQueryConstants.js index 3c4f93a..ea8cbd0 100644 --- a/lens-ui/app/constants/AdhocQueryConstants.js +++ b/lens-ui/app/constants/AdhocQueryConstants.js @@ -17,7 +17,7 @@ * under the License. */ -import KeyMirror from 'keyMirror'; +import KeyMirror from 'keymirror'; const AdhocQueryConstants = KeyMirror({ RECEIVE_CUBES: null, @@ -45,7 +45,14 @@ const AdhocQueryConstants = KeyMirror({ RECEIVE_QUERY_FAILED: null, RECEIVE_DATABASES: null, - RECEIVE_DATABASES_FAILED: null + RECEIVE_DATABASES_FAILED: null, + + RECEIVE_QUERY_PARAMS_META: null, + + SAVE_QUERY_SUCCESS: null, + SAVE_QUERY_FAILED: null, + + RECEIVE_SAVED_QUERY: null }); export default AdhocQueryConstants; http://git-wip-us.apache.org/repos/asf/lens/blob/86714211/lens-ui/app/constants/AppConstants.js ---------------------------------------------------------------------- diff --git a/lens-ui/app/constants/AppConstants.js b/lens-ui/app/constants/AppConstants.js index 48cd93e..075cffb 100644 --- a/lens-ui/app/constants/AppConstants.js +++ b/lens-ui/app/constants/AppConstants.js @@ -17,11 +17,12 @@ * under the License. */ -import KeyMirror from 'keyMirror'; +import KeyMirror from 'keymirror'; const AppConstants = KeyMirror({ AUTHENTICATION_SUCCESS: null, - AUTHENTICATION_FAILED: null + AUTHENTICATION_FAILED: null, + RECEIVE_USERNAME: null }); export default AppConstants; http://git-wip-us.apache.org/repos/asf/lens/blob/86714211/lens-ui/app/dispatcher/AppDispatcher.js ---------------------------------------------------------------------- diff --git a/lens-ui/app/dispatcher/AppDispatcher.js b/lens-ui/app/dispatcher/AppDispatcher.js index 31b267c..b87320c 100644 --- a/lens-ui/app/dispatcher/AppDispatcher.js +++ b/lens-ui/app/dispatcher/AppDispatcher.js @@ -5,7 +5,6 @@ * 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'; http://git-wip-us.apache.org/repos/asf/lens/blob/86714211/lens-ui/app/stores/AdhocQueryStore.js ---------------------------------------------------------------------- diff --git a/lens-ui/app/stores/AdhocQueryStore.js b/lens-ui/app/stores/AdhocQueryStore.js index 7420270..3f880bf 100644 --- a/lens-ui/app/stores/AdhocQueryStore.js +++ b/lens-ui/app/stores/AdhocQueryStore.js @@ -33,7 +33,6 @@ var adhocDetails = { dbName: Config.dbName }; -// TODO remove this. function receiveQueryHandle (payload) { let id = payload.queryHandle.getElementsByTagName('handleId')[0].textContent; adhocDetails.queryHandle = id; @@ -69,7 +68,6 @@ function receiveQueryResult (payload) { 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; @@ -87,19 +85,13 @@ let AdhocQueryStore = assign({}, EventEmitter.prototype, { // always returns the last-run-query's handle getQueryHandle () { - return adhocDetails.queryHandle; - }, - - clearQueryHandle () { + let handle = adhocDetails.queryHandle; adhocDetails.queryHandle = null; + return handle; }, - getDbName () { - return adhocDetails.dbName - }, - - emitChange () { - this.emit(CHANGE_EVENT); + emitChange (hash) { + this.emit(CHANGE_EVENT, hash); }, addChangeListener (callback) { @@ -112,7 +104,7 @@ let AdhocQueryStore = assign({}, EventEmitter.prototype, { }); AppDispatcher.register((action) => { - switch(action.actionType) { + switch (action.actionType) { case AdhocQueryConstants.RECEIVE_QUERY_HANDLE: receiveQueryHandle(action.payload); @@ -132,6 +124,11 @@ AppDispatcher.register((action) => { case AdhocQueryConstants.RECEIVE_QUERY: receiveQuery(action.payload); AdhocQueryStore.emitChange(); + break; + + case AdhocQueryConstants.RECEIVE_QUERY_HANDLE_FAILED: + AdhocQueryStore.emitChange(action.payload); + break; } }); http://git-wip-us.apache.org/repos/asf/lens/blob/86714211/lens-ui/app/stores/CubeStore.js ---------------------------------------------------------------------- diff --git a/lens-ui/app/stores/CubeStore.js b/lens-ui/app/stores/CubeStore.js index 8b20b95..09b469f 100644 --- a/lens-ui/app/stores/CubeStore.js +++ b/lens-ui/app/stores/CubeStore.js @@ -44,7 +44,6 @@ function receiveCubeDetails (payload) { cubes[cubeDetails.name].isLoaded = true; } - let CHANGE_EVENT = 'change'; var cubes = {}; @@ -67,7 +66,7 @@ let CubeStore = assign({}, EventEmitter.prototype, { }); AppDispatcher.register((action) => { - switch(action.actionType) { + switch (action.actionType) { case AdhocQueryConstants.RECEIVE_CUBES: receiveCubes(action.payload); CubeStore.emitChange(); http://git-wip-us.apache.org/repos/asf/lens/blob/86714211/lens-ui/app/stores/DatabaseStore.js ---------------------------------------------------------------------- diff --git a/lens-ui/app/stores/DatabaseStore.js b/lens-ui/app/stores/DatabaseStore.js index 9f4490b..79894cf 100644 --- a/lens-ui/app/stores/DatabaseStore.js +++ b/lens-ui/app/stores/DatabaseStore.js @@ -26,7 +26,7 @@ function receiveDatabases (payload) { databases = []; databases = payload.databases.elements && - payload.databases.elements.slice() + payload.databases.elements.slice(); } let CHANGE_EVENT = 'change'; @@ -51,7 +51,7 @@ let DatabaseStore = assign({}, EventEmitter.prototype, { }); AppDispatcher.register((action) => { - switch(action.actionType) { + switch (action.actionType) { case AdhocQueryConstants.RECEIVE_DATABASES: receiveDatabases(action.payload); DatabaseStore.emitChange(); http://git-wip-us.apache.org/repos/asf/lens/blob/86714211/lens-ui/app/stores/SavedQueryStore.js ---------------------------------------------------------------------- diff --git a/lens-ui/app/stores/SavedQueryStore.js b/lens-ui/app/stores/SavedQueryStore.js new file mode 100644 index 0000000..fb2869b --- /dev/null +++ b/lens-ui/app/stores/SavedQueryStore.js @@ -0,0 +1,99 @@ +/** +* 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'; + +let savedQueries = {}; +let offset = 0; +let totalRecords = 0; +let CHANGE_EVENT = 'change'; + +function receiveSavedQueries (payload) { + payload && payload.resoures.forEach(query => { + savedQueries[query.id] = query; + }); + + totalRecords = payload && payload.totalCount; +} + +function receiveSavedQuery (payload) { + if (!savedQueries[payload.id]) totalRecords++; + savedQueries[payload.id] = payload; +} + +let SavedQueryStore = assign({}, EventEmitter.prototype, { + getSavedQueries () { + return savedQueries; + }, + + getTotalRecords () { + return totalRecords; + }, + + getOffset () { + return offset; + }, + + emitChange (hash) { + this.emit(CHANGE_EVENT, hash); + }, + + addChangeListener (callback) { + this.on(CHANGE_EVENT, callback); + }, + + removeChangeListener (callback) { + this.removeListener(CHANGE_EVENT, callback); + } +}); + +AppDispatcher.register((action) => { + switch (action.actionType) { + case AdhocQueryConstants.RECEIVE_SAVED_QUERIES: + receiveSavedQueries(action.payload); + SavedQueryStore.emitChange(); + break; + + case AdhocQueryConstants.RECEIVE_QUERY_PARAMS_META: + SavedQueryStore.emitChange({type: 'params', params: action.payload}); + break; + + case AdhocQueryConstants.SAVE_QUERY_SUCCESS: + SavedQueryStore.emitChange({ + type: 'success', + message: action.payload, + id: action.payload && action.payload.id + }); + break; + + case AdhocQueryConstants.SAVE_QUERY_FAILED: + SavedQueryStore.emitChange({type: 'failure', message: action.payload}); + break; + + case AdhocQueryConstants.RECEIVE_SAVED_QUERY: + receiveSavedQuery(action.payload); + SavedQueryStore.emitChange(); + break; + } +}); + +export default SavedQueryStore; http://git-wip-us.apache.org/repos/asf/lens/blob/86714211/lens-ui/app/stores/TableStore.js ---------------------------------------------------------------------- diff --git a/lens-ui/app/stores/TableStore.js b/lens-ui/app/stores/TableStore.js index 299d9e8..ee35b84 100644 --- a/lens-ui/app/stores/TableStore.js +++ b/lens-ui/app/stores/TableStore.js @@ -31,7 +31,7 @@ function receiveTables (payload) { } payload.tables.elements && - payload.tables.elements.forEach( table => { + payload.tables.elements.forEach(table => { if (!tables[database][table]) { tables[database][table] = { name: table, isLoaded: false }; } @@ -86,7 +86,7 @@ let TableStore = assign({}, EventEmitter.prototype, { }); AppDispatcher.register((action) => { - switch(action.actionType) { + switch (action.actionType) { case AdhocQueryConstants.RECEIVE_TABLES: receiveTables(action.payload); TableStore.emitChange(); http://git-wip-us.apache.org/repos/asf/lens/blob/86714211/lens-ui/app/stores/UserStore.js ---------------------------------------------------------------------- diff --git a/lens-ui/app/stores/UserStore.js b/lens-ui/app/stores/UserStore.js index 47da021..ee5506f 100644 --- a/lens-ui/app/stores/UserStore.js +++ b/lens-ui/app/stores/UserStore.js @@ -17,8 +17,6 @@ * under the License. */ -import React from 'react'; - import AppDispatcher from '../dispatcher/AppDispatcher'; import AppConstants from '../constants/AppConstants'; import assign from 'object-assign'; @@ -28,8 +26,7 @@ var CHANGE_EVENT = 'change'; var userDetails = { isUserLoggedIn: false, email: '', - secretToken: '', - publicKey: '' + secretToken: '' }; // keeping these methods out of the UserStore class as @@ -38,13 +35,10 @@ function authenticateUser (details) { userDetails = { isUserLoggedIn: true, email: details.email, - secretToken: new XMLSerializer().serializeToString(details.secretToken), - publicKey: details.secretToken.getElementsByTagName('publicId')[0] - .textContent + secretToken: new XMLSerializer().serializeToString(details.secretToken) }; // store the details in localStorage if available - if (window.localStorage) { let adhocCred = assign({}, userDetails, { timestamp: Date.now() }); window.localStorage.setItem('adhocCred', JSON.stringify(adhocCred)); @@ -52,7 +46,6 @@ function authenticateUser (details) { } function unauthenticateUser (details) { - // details contains error code and message // which are not stored but passsed along // during emitChange() @@ -69,12 +62,9 @@ function unauthenticateUser (details) { // 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')); @@ -114,7 +104,7 @@ var UserStore = assign({}, EventEmitter.prototype, { // registering callbacks with the dispatcher. So verbose?? I know right! AppDispatcher.register((action) => { - switch(action.actionType) { + switch (action.actionType) { case AppConstants.AUTHENTICATION_SUCCESS: authenticateUser(action.payload); UserStore.emitChange(); http://git-wip-us.apache.org/repos/asf/lens/blob/86714211/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 index 131ab46..7670703 100644 --- a/lens-ui/app/styles/css/global.css +++ b/lens-ui/app/styles/css/global.css @@ -16,3 +16,25 @@ * specific language governing permissions and limitations * under the License. */ + +.no-padding { + padding: 0px; +} + +.glyphicon.glyphicon-chevron-up, .glyphicon.glyphicon-chevron-down { + cursor: pointer; +} + +.italics { + font-style: italic; +} + +/*.panel-default > .blue-header { + background: rgba(51, 122, 183, 0.79); + color: white; +}*/ + +.panel-default > .blue-header:HOVER { + /*background: rgba(51, 122, 183, 1);*/ + transform: scale(1.018); +} http://git-wip-us.apache.org/repos/asf/lens/blob/86714211/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 index b400cfb..194e19e 100644 --- a/lens-ui/app/styles/css/login.css +++ b/lens-ui/app/styles/css/login.css @@ -17,8 +17,7 @@ * under the License. */ - - /*For login form*/ +/*For login form*/ .form-signin { max-width: 330px; padding: 15px; http://git-wip-us.apache.org/repos/asf/lens/blob/86714211/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 index a82165e..e29758e 100644 --- a/lens-ui/app/styles/css/query-component.css +++ b/lens-ui/app/styles/css/query-component.css @@ -17,8 +17,7 @@ * under the License. */ - - @media (max-width: 768px) { +@media (max-width: 768px) { .btn.responsive { width:100%; margin-bottom: 10px; http://git-wip-us.apache.org/repos/asf/lens/blob/86714211/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 index 402c9a0..61cc4e7 100644 --- a/lens-ui/app/styles/css/tree.css +++ b/lens-ui/app/styles/css/tree.css @@ -17,8 +17,7 @@ * under the License. */ - - .node { +.node { font-weight: bold; } http://git-wip-us.apache.org/repos/asf/lens/blob/86714211/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 index c0704dc..05cc30e 100644 --- a/lens-ui/app/styles/less/globals.less +++ b/lens-ui/app/styles/less/globals.less @@ -17,7 +17,6 @@ * under the License. */ - - // IMPORTS +// IMPORTS @import "~bootstrap/less/bootstrap.less"; http://git-wip-us.apache.org/repos/asf/lens/blob/86714211/lens-ui/app/utils/ErrorParser.js ---------------------------------------------------------------------- diff --git a/lens-ui/app/utils/ErrorParser.js b/lens-ui/app/utils/ErrorParser.js new file mode 100644 index 0000000..8bd0d90 --- /dev/null +++ b/lens-ui/app/utils/ErrorParser.js @@ -0,0 +1,53 @@ +/** +* 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. +*/ + +let ErrorParser = { + getMessage (errorXML) { + let errors = []; + + errors = Array.prototype.slice.call(errorXML.getElementsByTagName('error')) + .map(error => { + return { + code: error.getElementsByTagName('code')[0].textContent, + message: error.getElementsByTagName('message')[0].textContent + }; + }) + .sort((a, b) => { + return parseInt(a.code, 10) - parseInt(b.code, 10); + }) + // removes duplicate error messages + .filter((item, pos, array) => { + return !pos || (item.code != (array[pos - 1] && array[pos - 1].code)); + }) + // removes not so helpful `Internal Server Error` + .filter(error => { + return error.code != 1001; + }); + + if (errors && errors.length == 0) { + errors[0] = {}; + errors[0].code = 500; + errors[0].message = 'Oh snap! Something went wrong. Please try again later.'; + } + + return errors; + } +}; + +export default ErrorParser; http://git-wip-us.apache.org/repos/asf/lens/blob/86714211/lens-ui/index.html ---------------------------------------------------------------------- diff --git a/lens-ui/index.html b/lens-ui/index.html index 9c20fe9..62df140 100644 --- a/lens-ui/index.html +++ b/lens-ui/index.html @@ -80,6 +80,7 @@ } } </style> + <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.4.0/css/font-awesome.min.css"> </head> <body> http://git-wip-us.apache.org/repos/asf/lens/blob/86714211/lens-ui/package.json ---------------------------------------------------------------------- diff --git a/lens-ui/package.json b/lens-ui/package.json index 920b120..b21302b 100644 --- a/lens-ui/package.json +++ b/lens-ui/package.json @@ -1,7 +1,6 @@ { "name": "lens-ui", - "version": "1.0.0", - "description": "An exemplary front end solution for Apache LENS", + "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", @@ -13,39 +12,45 @@ "bootstrap": "^3.3.4", "classnames": "^2.1.2", "codemirror": "^5.3.0", + "cookie-parser": "~1.3.5", + "debug": "~2.2.0", + "express": "~4.12.4", + "express-session": "latest", "flux": "^2.0.3", "halogen": "^0.1.8", "keymirror": "^0.1.1", "lodash": "^3.9.1", "moment": "^2.10.3", + "morgan": "~1.5.3", "object-assign": "^2.0.0", "q": "^1.4.1", + "qwest": "^2.0.7", "react": "^0.13.3", - "react-bootstrap": "^0.22.6", + "react-bootstrap": "0.25.1", "react-router": "^0.13.3", "react-treeview": "^0.3.12", - "reqwest": "^1.1.5" + "react-widgets": "^2.8.0", + "serve-favicon": "~2.2.1" }, "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" + }, + "repository": { + "type": "git", + "url": "https://git-wip-us.apache.org/repos/asf/lens.git" } } http://git-wip-us.apache.org/repos/asf/lens/blob/86714211/lens-ui/server.js ---------------------------------------------------------------------- diff --git a/lens-ui/server.js b/lens-ui/server.js index e812018..736d862 100644 --- a/lens-ui/server.js +++ b/lens-ui/server.js @@ -19,41 +19,42 @@ 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']){ +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 +app.use(session({ + secret : 'SomethingYouKnow', + resave : false, + saveUninitialized : true })); -var fs = require('fs'); - +var fs = require('fs') +; app.use(express.static(path.resolve(__dirname, 'target', 'assets'))); +app.get('/health', function (req, res) { + res.status(200).send('Adhoc Query UI is up and running.'); +}); + app.get('/target/assets/*', function (req, res) { res.setHeader('Cache-Control', 'public'); res.end(fs.readFileSync(__dirname + req.path)); @@ -62,18 +63,18 @@ app.get('/target/assets/*', function (req, res) { 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.'); }); + target: process.env['lensserver'] + }, function (e) { + console.error('Proxy Error: ', e); + }); }); -app.get('*', function(req, res) { +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; +var server = app.listen(port, function (err) { + if (err) throw err; - console.log('Ad hoc UI server listening at port: ', port); + console.log('Ad hoc UI server listening at %s', port); }); http://git-wip-us.apache.org/repos/asf/lens/blob/86714211/lens-ui/webpack.config.js ---------------------------------------------------------------------- diff --git a/lens-ui/webpack.config.js b/lens-ui/webpack.config.js index ab4021f..9977049 100644 --- a/lens-ui/webpack.config.js +++ b/lens-ui/webpack.config.js @@ -20,7 +20,6 @@ var webpack = require('webpack'); var path = require('path'); - module.exports = { entry: { @@ -29,27 +28,30 @@ module.exports = { ] }, - output: { + output: { path: path.join(__dirname, 'target', 'assets'), - filename: 'bundle.js' - }, + filename: 'bundle.js' + }, plugins: [ new webpack.NoErrorsPlugin() ], + devtool: 'source-map', + resolve: { modulesDirectories: ['app', 'node_modules', __dirname] }, - module: { - loaders: [ + module: { + loaders: [ { test: /\.jsx?$/, loaders: ['babel'], include: path.join(__dirname, 'app') }, - { test: /\.css$/, loaders: ['style', 'css'] }, + { 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']} - ] - } + { test: /\.json$/, loaders: ['json'] }, + { test: /\.gif$/, loader: 'url-loader?mimetype=image/png' } + ] + } };