Repository: lens Updated Branches: refs/heads/master 669e87272 -> 66f164b47
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>
