You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@lens.apache.org by ra...@apache.org on 2015/10/09 06:17:54 UTC
[49/50] [abbrv] lens git commit: LENS-782: UI support for the saved,
parametrized query feature
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' }
+ ]
+ }
};