Просмотр исходного кода

Merge branch 'master' into rc/1.0.x

Yuki Takei 9 лет назад
Родитель
Сommit
e41e0e8d82

+ 2 - 2
.github/ISSUE_TEMPLATE.md

@@ -2,8 +2,8 @@ Environment
 ------------
 
 - [OS]
-- [node.js] x.x.x
-- [npm] y.y.y
+- [crowi-plus] x.x.x
+- [node.js] y.y.y
 - [browser] z.z.z
 
 

+ 2 - 0
config/env.dev.js

@@ -1,5 +1,7 @@
 module.exports = {
   NODE_ENV: 'development',
+  // REDIS_URL: 'redis://localhost:6379/crowi',
+  // ELASTICSEARCH_URI: 'http://localhost:9200/crowi',
   PLUGIN_NAMES_TOBE_LOADED: [
     // 'crowi-plugin-lsx',
     // 'crowi-plugin-pukiwiki-like-linker',

+ 20 - 0
lib/crowi/index.js

@@ -18,6 +18,7 @@ function Crowi (rootdir, env)
   var self = this;
 
   this.version = pkg.version;
+  this.runtimeVersions = undefined;   // initialized by scanRuntimeVersions()
 
   this.rootDir     = rootdir;
   this.pluginDir   = path.join(this.rootDir, 'node_modules') + sep;
@@ -82,6 +83,8 @@ Crowi.prototype.init = function() {
         return resolve();
       });
     });
+  }).then(function() {
+    return self.scanRuntimeVersions();
   }).then(function() {
     return self.setupSearcher();
   }).then(function() {
@@ -213,6 +216,23 @@ Crowi.prototype.getIo = function() {
   return this.io;
 };
 
+Crowi.prototype.scanRuntimeVersions = function() {
+  var self = this
+    , check = require('check-node-version')
+    ;
+
+
+  return new Promise((resolve, reject) => {
+    check((err, result) => {
+      if (err) {
+        reject();
+      }
+      self.runtimeVersions = result;
+      resolve();
+    })
+  });
+}
+
 Crowi.prototype.getSearcher = function() {
   return this.searcher;
 };

+ 1 - 1
lib/models/user.js

@@ -658,7 +658,7 @@ module.exports = function(crowi) {
     newUser.email = email;
     newUser.setPassword(password);
     newUser.lang = lang;
-    newUser.isGravatarEnabled = true;   // Gravatar enabled in default
+    newUser.isGravatarEnabled = false;
     newUser.createdAt = Date.now();
     newUser.status = decideUserStatusOnRegistration();
 

+ 30 - 29
lib/routes/admin.js

@@ -229,37 +229,38 @@ module.exports = function(crowi, app) {
       return res.redirect('/admin');
     }
 
-    Promise.resolve().then(function() {
-      return new Promise(function(resolve, reject) {
-        search.deleteIndex()
-          .then(function(data) {
-            debug('Index deleted.');
-            resolve();
-          }).catch(function(err) {
-            debug('Delete index Error, but if it is initialize, its ok.', err);
-            resolve();
-          });
-      });
-    }).then(function() {
-      search.buildIndex()
-        .then(function(data) {
-          if (!data.errors) {
-            debug('Index created.');
-          }
-          return search.addAllPages();
-        })
+    return new Promise(function(resolve, reject) {
+      search.deleteIndex()
         .then(function(data) {
-          if (!data.errors) {
-            debug('Data is successfully indexed.');
-          } else {
-            debug('Data index error.', data.errors);
-          }
-        })
-        .catch(function(err) {
-          debug('Error', err);
+          debug('Index deleted.');
+          resolve();
+        }).catch(function(err) {
+          debug('Delete index Error, but if it is initialize, its ok.', err);
+          resolve();
         });
-
-      req.flash('successMessage', 'Now re-building index ... this takes a while.');
+    })
+    .then(function() {
+      return search.buildIndex()
+    })
+    .then(function(data) {
+      if (!data.errors) {
+        debug('Index created.');
+      }
+      return search.addAllPages();
+    })
+    .then(function(data) {
+      if (!data.errors) {
+        debug('Data is successfully indexed.');
+        req.flash('successMessage', 'Data is successfully indexed.');
+      } else {
+        debug('Data index error.', data.errors);
+        req.flash('errorMessage', `Data index error: ${data.errors}`);
+      }
+      return res.redirect('/admin/search');
+    })
+    .catch(function(err) {
+      debug('Error', err);
+      req.flash('errorMessage', `Error: ${err}`);
       return res.redirect('/admin/search');
     });
   };

+ 1 - 0
lib/util/search.js

@@ -359,6 +359,7 @@ SearchClient.prototype.appendCriteriaForKeywordContains = function(query, keywor
         // TODO: By user's i18n setting, change boost or search target fields
         fields: [
           "path_ja^2",
+          "path_en^2",
           "body_ja",
           // "path_en",
           // "body_en",

+ 14 - 0
lib/util/swigFunctions.js

@@ -5,6 +5,20 @@ module.exports = function(crowi, app, req, locals) {
     , User = crowi.model('User')
   ;
 
+  locals.nodeVersion = function() {
+    return crowi.runtimeVersions.node ? crowi.runtimeVersions.node.version : '-';
+  }
+  locals.npmVersion = function() {
+    return crowi.runtimeVersions.npm ? crowi.runtimeVersions.npm.version : '-';
+  }
+  locals.yarnVersion = function() {
+    return crowi.runtimeVersions.yarn ? crowi.runtimeVersions.yarn.version : '-';
+  }
+
+  locals.crowiVersion = function() {
+    return crowi.version;
+  }
+
   // token getter
   locals.csrf = function() {
     return req.csrfToken;

+ 20 - 0
lib/views/admin/index.html

@@ -29,6 +29,26 @@
       この画面はWiki管理者のみがアクセスできる画面です。<br>
       「ユーザー管理」から「管理者にする」ボタンを使ってユーザーをWiki管理者に任命することができます。
       </p>
+
+      <h3>システム情報</h3>
+      <table class="table table-bordered">
+        <tr>
+          <th class="col-sm-4">crowi-plus</th>
+          <td>{{ crowiVersion() }}</td>
+        </tr>
+        <tr>
+          <th>node.js</th>
+          <td>{{ nodeVersion() }}</td>
+        </tr>
+        <tr>
+          <th>npm</th>
+          <td>{{ npmVersion() }}</td>
+        </tr>
+        <tr>
+          <th>yarn</th>
+          <td>{{ yarnVersion() }}</td>
+        </tr>
+      </table>
     </div>
   </div>
 

+ 1 - 1
lib/views/layout/2column.html

@@ -20,7 +20,7 @@
     <footer class="">
       <p>
       <a href="" data-target="#help-modal" data-toggle="modal"><i class="fa fa-question-circle"></i> {{ t('Help') }}</a>
-      &copy; {{ now|date('Y') }} {{ config.crowi['app:title']|default('Crowi') }} <img src="/logo/100x11_g.png" alt="powered by Crowi"> </p>
+      &copy; {{ now|date('Y') }} crowi-plus {{ crowiVersion() }} <img src="/logo/100x11_g.png" alt="powered by Crowi"> </p>
     </footer>
   </div>
 </aside>

+ 1 - 1
lib/views/layout/single.html

@@ -21,7 +21,7 @@
   <footer class="">
     <p>
     <a href="" data-target="#help-modal" data-toggle="modal"><i class="fa fa-question-circle"></i> {{ t('Help') }}</a>
-    &copy; {{ now|date('Y') }} {{ config.crowi['app:title'] }} <img src="/logo/100x11_g.png" alt="powered by Crowi"> </p>
+    &copy; {{ now|date('Y') }} crowi-plus {{ crowiVersion() }} <img src="/logo/100x11_g.png" alt="powered by Crowi"> </p>
   </footer>
 </div>
 {% endblock %}

+ 3 - 0
package.json

@@ -56,6 +56,7 @@
     "body-parser": "^1.17.1",
     "bootstrap-sass": "~3.3.6",
     "botkit": "~0.1.1",
+    "check-node-version": "^2.0.1",
     "cli": "~1.0.1",
     "colors": "^1.1.2",
     "commander": "~2.9.0",
@@ -103,6 +104,8 @@
     "normalize-path": "^2.1.1",
     "optimize-js-plugin": "0.0.4",
     "react": "^15.4.2",
+    "react-bootstrap": "^0.31.0",
+    "react-bootstrap-typeahead": "^1.3.0",
     "react-clipboard.js": "^1.0.1",
     "react-dom": "^15.4.2",
     "redis": "^2.7.1",

+ 1 - 1
resource/css/_page.scss

@@ -90,7 +90,7 @@
         margin-bottom: 0;
 
         .btn-copy-container {
-          font-size: 0.8em;
+          font-size: 0.65em;
         }
 
         a.last-path {

+ 30 - 25
resource/css/_search.scss

@@ -1,3 +1,5 @@
+@import "./page_list";
+
 .search-listpage-icon {
   font-size: 16px;
   color: #999;
@@ -23,16 +25,14 @@
   vertical-align: bottom;
 }
 
-
 .search-top {
   .search-top-input-group {
-    .search-top-input {
+
+    // using react-bootstrap-typeahead
+    // see: https://github.com/ericgio/react-bootstrap-typeahead
+    .bootstrap-typeahead-input input {
       width: 400px;
     }
-    .btn {
-      padding: 6px 12px 7px;
-      margin-left: -3px;
-    }
 
     .search-top-clear {
       position: absolute;
@@ -40,27 +40,36 @@
       z-index: 10;
       width: 22px;
       height: 22px;
+      top: 7px;
       color: #ccc;
-      padding: 8px;
+      padding: 0;
     }
   }
-}
 
-.search-suggest {
-  // => dicided by JS
-  // top: 43px;
-  // left: 125px;
-  //display: none;
-  position: absolute;
-  width: 500px;
-  background: #fff;
-  border: solid 1px #ccc;
-  box-shadow: 0 0 1px rgba(0,0,0,.3);
-  padding: 16px;
+  // using react-bootstrap-typeahead
+  // see: https://github.com/ericgio/react-bootstrap-typeahead
+  .bootstrap-typeahead-menu {
+    li a span {
+      .picture {
+        width: 14px;
+        height: 14px;
+      }
 
+      .page-path {
+        display: inline;
+        padding: 0 4px;
+        color: inherit;
+      }
+
+      .page-list-meta {
+        font-size: .9em;
+        color: #999;
 
-  .searching {
-    color: #666;
+        >span {
+          margin-right: .3rem;
+        }
+      }
+    }
   }
 }
 
@@ -163,10 +172,6 @@
       width: 50%;
     }
     .search-box {
-      .search-suggest {
-        left: 2%;
-        width: 94%;
-      }
     }
   }
 }

+ 6 - 0
resource/css/crowi.scss

@@ -5,6 +5,12 @@
 $bootstrap-sass-asset-helper: true;
 @import "~bootstrap-sass/assets/stylesheets/bootstrap";
 
+// import react-bootstrap-typeahead styles
+@import '~react-bootstrap-typeahead/css/ClearButton';
+@import '~react-bootstrap-typeahead/css/Loader';
+@import '~react-bootstrap-typeahead/css/Token';
+@import '~react-bootstrap-typeahead/css/Typeahead';
+
 // crowi component
 @import 'admin';
 @import 'comment';

+ 4 - 2
resource/js/app.js

@@ -47,9 +47,11 @@ const componentMappings = {
   //'revision-history': <PageHistory pageId={pageId} />,
   //'page-comment': <PageComment />,
   'seen-user-list': <SeenUserList />,
-  'revision-path': <RevisionPath pagePath={pagePath} />,
-  'revision-url': <RevisionUrl pagePath={pagePath} url={location.href} />,
 };
+if (pagePath) {
+  componentMappings['revision-path'] = <RevisionPath pagePath={pagePath} />;
+  componentMappings['revision-url'] = <RevisionUrl pagePath={pagePath} url={location.href} />;
+}
 
 Object.keys(componentMappings).forEach((key) => {
   const elem = document.getElementById(key);

+ 2 - 2
resource/js/components/CopyButton.js

@@ -19,12 +19,12 @@ export default class CopyButton extends React.Component {
 
   render() {
     const containerStyle = {
-      verticalAlign: "top"
     }
     const style = Object.assign({
       fontSize: "0.8em",
       padding: "0 2px",
-      border: 'none'
+      border: 'none',
+      verticalAlign: 'text-top',
     }, this.props.buttonStyle);
 
     return (

+ 4 - 55
resource/js/components/HeaderSearchBox.js

@@ -3,71 +3,19 @@
 import React from 'react';
 
 import SearchForm from './HeaderSearchBox/SearchForm';
-import SearchSuggest from './HeaderSearchBox/SearchSuggest';
-import axios from 'axios'
+// import SearchSuggest from './HeaderSearchBox/SearchSuggest'; // omit since using react-bootstrap-typeahead in SearchForm
 
 export default class SearchBox extends React.Component {
 
   constructor(props) {
     super(props);
-
-    this.crowi = window.crowi; // FIXME
-
-    this.state = {
-      searchingKeyword: '',
-      searchedPages: [],
-      searchError: null,
-      searching: false,
-      focused: false,
-    }
-
-    this.search = this.search.bind(this);
-    this.isShown = this.isShown.bind(this);
-  }
-
-  isShown(focused) {
-    this.setState({focused: !!focused});
-  }
-
-  search(data) {
-    const keyword = data.keyword;
-    if (keyword === '') {
-      this.setState({
-        searchingKeyword: '',
-        searchedPages: [],
-      });
-
-      return true;
-    }
-
-    this.setState({
-      searchingKeyword: keyword,
-      searching: true,
-    });
-
-    this.crowi.apiGet('/search', {q: keyword})
-    .then(res => {
-      this.setState({
-        searchingKeyword: keyword,
-        searchedPages: res.data,
-        searching: false,
-        searchError: null,
-      });
-    }).catch(err => {
-      this.setState({
-        searchError: err,
-        searching: false,
-      });
-    });
   }
 
   render() {
     return (
       <div className="search-box">
-        <SearchForm
-          onSearchFormChanged={this.search}
-          isShown={this.isShown}
-          />
+        <SearchForm />
+        {/* omit since using react-bootstrap-typeahead in SearchForm
         <SearchSuggest
           searchingKeyword={this.state.searchingKeyword}
           searchedPages={this.state.searchedPages}
@@ -75,6 +23,7 @@ export default class SearchBox extends React.Component {
           searching={this.state.searching}
           focused={this.state.focused}
           />
+        */}
       </div>
     );
   }

+ 88 - 48
resource/js/components/HeaderSearchBox/SearchForm.js

@@ -1,4 +1,13 @@
 import React from 'react';
+import { FormGroup, Button, InputGroup } from 'react-bootstrap';
+
+import { AsyncTypeahead } from 'react-bootstrap-typeahead';
+
+import axios from 'axios'
+
+import UserPicture from '../User/UserPicture';
+import PageListMeta from '../PageList/PageListMeta';
+import PagePath from '../PageList/PagePath';
 
 // Header.SearchForm
 export default class SearchForm extends React.Component {
@@ -6,65 +15,91 @@ export default class SearchForm extends React.Component {
   constructor(props) {
     super(props);
 
+    this.crowi = window.crowi; // FIXME
+
     this.state = {
+      input: '',
       keyword: '',
       searchedKeyword: '',
+      pages: [],
+      searchError: null,
     };
 
-    this.handleChange = this.handleChange.bind(this);
-    this.handleFocus = this.handleFocus.bind(this);
-    this.handleBlur = this.handleBlur.bind(this);
+    this.search = this.search.bind(this);
     this.clearForm = this.clearForm.bind(this);
-    this.ticker = null;
+    this.getFormClearComponent = this.getFormClearComponent.bind(this);
+    this.renderMenuItemChildren = this.renderMenuItemChildren.bind(this);
+    this.onInputChange = this.onInputChange.bind(this);
+    this.onChange = this.onChange.bind(this);
   }
 
   componentDidMount() {
-    this.ticker = setInterval(this.searchFieldTicker.bind(this), this.props.pollInterval);
   }
 
   componentWillUnmount() {
-    clearInterval(this.ticker);
   }
 
-  search() {
-    if (this.state.searchedKeyword != this.state.keyword) {
-      this.props.onSearchFormChanged({keyword: this.state.keyword});
-      this.setState({searchedKeyword: this.state.keyword});
+  search(keyword) {
+
+    if (keyword === '') {
+      this.setState({
+        keyword: '',
+        searchedKeyword: '',
+      });
+      return;
     }
+
+    this.crowi.apiGet('/search', {q: keyword})
+      .then(res => {
+        this.setState({
+          keyword: '',
+          pages: res.data,
+        });
+      }).catch(err => {
+        this.setState({
+          searchError: err
+        });
+      });
   }
 
   getFormClearComponent() {
-    if (this.state.keyword !== '') {
-      return <a className="search-top-clear" onClick={this.clearForm}><i className="fa fa-times-circle" /></a>;
+    let isHidden = (this.state.input.length === 0);
 
-    } else {
-      return '';
-    }
+    return isHidden ? <span></span> : (
+      <a className="btn btn-link search-top-clear" onClick={this.clearForm} hidden={isHidden}>
+        <i className="fa fa-times-circle" />
+      </a>
+    );
   }
 
   clearForm() {
+    this._typeahead.getInstance().clear();
     this.setState({keyword: ''});
-    this.search();
   }
 
-  searchFieldTicker() {
-    this.search();
+  onInputChange(text) {
+    this.setState({input: text});
   }
 
-  handleFocus(event) {
-    this.props.isShown(true);
+  onChange(options) {
+    const page = options[0];  // should be single page selected
+    // navigate to page
+    window.location = page.path;
   }
 
-  handleBlur(event) {
-    //this.props.isShown(false);
-  }
-
-  handleChange(event) {
-    const keyword = event.target.value;
-    this.setState({keyword});
+  renderMenuItemChildren(option, props, index) {
+    const page = option;
+    return (
+      <span>
+        <UserPicture user={page.revision.author} />
+        <PagePath page={page} />
+        <PageListMeta page={page} />
+      </span>
+    );
   }
 
   render() {
+    const emptyLabel = (this.state.searchError !== null) ? 'Error on searching.' : 'No matches found.';
     const formClear = this.getFormClearComponent();
 
     return (
@@ -72,33 +107,38 @@ export default class SearchForm extends React.Component {
         action="/_search"
         className="search-form form-group input-group search-top-input-group"
       >
-        <input
-          autocomplete="off"
-          type="text"
-          className="search-top-input form-control"
-          placeholder="Search ... Page Title (Path) and Content"
-          name="q"
-          value={this.state.keyword}
-          onFocus={this.handleFocus}
-          onBlur={this.handleBlur}
-          onChange={this.handleChange}
-        />
-        <span className="input-group-btn">
-          <button type="submit" className="btn btn-default">
-            <i className="search-top-icon fa fa-search"></i>
-          </button>
-        </span>
-        {formClear}
+        <FormGroup>
+          <InputGroup>
+            <AsyncTypeahead
+              ref={ref => this._typeahead = ref}
+              name="q"
+              labelKey="path"
+              minLength={2}
+              options={this.state.pages}
+              placeholder="Search ... Page Title (Path) and Content"
+              submitFormOnEnter={true}
+              onSearch={this.search}
+              onInputChange={this.onInputChange}
+              onChange={this.onChange}
+              renderMenuItemChildren={this.renderMenuItemChildren}
+            />
+            {formClear}
+            <InputGroup.Button>
+              <Button type="submit">
+                <i className="search-top-icon fa fa-search"></i>
+              </Button >
+            </InputGroup.Button>
+          </InputGroup>
+        </FormGroup>
+
       </form>
+
     );
   }
 }
 
 SearchForm.propTypes = {
-  onSearchFormChanged: React.PropTypes.func.isRequired,
-  isShown: React.PropTypes.func.isRequired,
-  pollInterval: React.PropTypes.number,
 };
+
 SearchForm.defaultProps = {
-  pollInterval: 1000,
 };

+ 3 - 1
resource/js/components/Page/PageBody.js

@@ -15,7 +15,7 @@ export default class PageBody extends React.Component {
       body = this.props.page.revision.body;
     }
 
-    return { __html: this.crowiRenderer.render(body) };
+    return { __html: this.crowiRenderer.render(body, this.props.rendererOptions) };
   }
 
   render() {
@@ -33,10 +33,12 @@ export default class PageBody extends React.Component {
 PageBody.propTypes = {
   page: React.PropTypes.object.isRequired,
   pageBody: React.PropTypes.string,
+  rendererOptions: React.PropTypes.object
 };
 
 PageBody.defaultProps = {
   page: {},
   pageBody: '',
+  rendererOptions: {},
 };
 

+ 11 - 1
resource/js/components/SearchPage/SearchResultList.js

@@ -29,13 +29,23 @@ export default class SearchResultList extends React.Component {
   }
 
   render() {
+    var isEnabledLineBreaks = $('#content-main').data('linebreaks-enabled');
+
+    // generate options obj
+    var rendererOptions = {
+      // see: https://www.npmjs.com/package/marked
+      marked: {
+        breaks: isEnabledLineBreaks
+      }
+    };
+
     const resultList = this.props.pages.map((page) => {
       const pageBody = this.getHighlightBody(page.revision.body);
       return (
         <div id={page._id} key={page._id} className="search-result-page">
           <h2><a href={page.path}>{page.path}</a></h2>
           <div className="wiki">
-            <PageBody className="hige" page={page} pageBody={pageBody} />
+            <PageBody className="hige" page={page} pageBody={pageBody} rendererOptions={rendererOptions} />
           </div>
         </div>
       );

+ 37 - 40
resource/search/mappings.json

@@ -15,6 +15,14 @@
           "language":   "possessive_english"
         }
       },
+      "tokenizer": {
+        "ngram_tokenizer": {
+          "type": "ngram",
+          "min_gram": 2,
+          "max_gram": 3,
+          "token_chars": ["letter", "digit"]
+        }
+      },
       "analyzer": {
         "autocomplete": {
           "tokenizer":  "keyword",
@@ -23,8 +31,12 @@
             "nGram"
           ]
         },
+        "japanese": {
+          "tokenizer": "kuromoji_tokenizer",
+          "char_filter" : ["icu_normalizer"]
+        },
         "english": {
-          "tokenizer":  "standard",
+          "tokenizer": "ngram_tokenizer",
           "filter": [
             "english_possessive_stemmer",
             "lowercase",
@@ -39,81 +51,66 @@
     "users": {
       "properties" : {
         "name": {
-          "type": "string",
-          "analyzer": "autocomplete",
-          "include_in_all": false
+          "type": "text",
+          "analyzer": "autocomplete"
         }
       }
     },
     "pages": {
       "properties" : {
         "path": {
-          "type": "string",
+          "type": "text",
           "copy_to": ["path_raw", "path_ja", "path_en"],
-          "include_in_all": false,
-          "index": "not_analyzed"
+          "index": "false"
         },
         "path_raw": {
-          "type": "string",
-          "analyzer": "standard",
-          "include_in_all": false
+          "type": "text",
+          "analyzer": "standard"
         },
         "path_ja": {
-          "type": "string",
-          "analyzer": "kuromoji",
-          "include_in_all": false
+          "type": "text",
+          "analyzer": "japanese"
         },
         "path_en": {
-          "type": "string",
-          "analyzer": "english",
-          "include_in_all": false
+          "type": "text",
+          "analyzer": "english"
         },
         "body": {
-          "type": "string",
+          "type": "text",
           "copy_to": ["body_raw", "body_ja", "body_en"],
-          "include_in_all": false,
-          "index": "not_analyzed"
+          "index": "false"
         },
         "body_raw": {
-          "type": "string",
-          "analyzer": "standard",
-          "include_in_all": false
+          "type": "text",
+          "analyzer": "standard"
         },
         "body_ja": {
-          "type": "string",
-          "analyzer": "kuromoji",
-          "include_in_all": false
+          "type": "text",
+          "analyzer": "japanese"
         },
         "body_en": {
-          "type": "string",
-          "analyzer": "english",
-          "include_in_all": false
+          "type": "text",
+          "analyzer": "english"
         },
         "username": {
-          "type": "string",
-          "include_in_all": false
+          "type": "text"
         },
         "comment_count": {
-          "type": "integer",
-          "include_in_all": false
+          "type": "integer"
         },
         "bookmark_count": {
-          "type": "integer",
-          "include_in_all": false
+          "type": "integer"
         },
         "like_count": {
-          "type": "integer",
-          "include_in_all": false
+          "type": "integer"
         },
         "created_at": {
           "type": "date",
-          "format": "dateOptionalTime",
-          "include_in_all": false
+          "format": "dateOptionalTime"
         },
         "updated_at": {
           "type": "date",
-          "format": "dateOptionalTime",
-          "include_in_all": false
+          "format": "dateOptionalTime"
         }
       }
     }

+ 135 - 7
yarn.lock

@@ -714,7 +714,7 @@ babel-register@^6.24.1:
     mkdirp "^0.5.1"
     source-map-support "^0.4.2"
 
-babel-runtime@^6.18.0, babel-runtime@^6.22.0:
+babel-runtime@^6.11.6, babel-runtime@^6.18.0, babel-runtime@^6.22.0:
   version "6.23.0"
   resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.23.0.tgz#0a9489f144de70efb3ce4300accdb329e2fc543b"
   dependencies:
@@ -833,6 +833,10 @@ bl@~1.1.2:
   dependencies:
     readable-stream "~2.0.5"
 
+blacklist@^1.1.2:
+  version "1.1.4"
+  resolved "https://registry.yarnpkg.com/blacklist/-/blacklist-1.1.4.tgz#b2dd09d6177625b2caa69835a37b28995fa9a2f2"
+
 blob@0.0.4:
   version "0.0.4"
   resolved "https://registry.yarnpkg.com/blob/-/blob-0.0.4.tgz#bcf13052ca54463f30f9fc7e95b9a47630a94921"
@@ -1137,6 +1141,16 @@ charenc@~0.0.1:
   version "0.0.2"
   resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667"
 
+check-node-version@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/check-node-version/-/check-node-version-2.0.1.tgz#3f037dc17c79e24029c0c6185d9f5a0aa85d8810"
+  dependencies:
+    map-values "^1.0.1"
+    minimist "^1.2.0"
+    object-filter "^1.0.2"
+    run-parallel "^1.1.4"
+    semver "^5.0.3"
+
 chokidar@^1.4.3:
   version "1.6.1"
   resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-1.6.1.tgz#2f4447ab5e96e50fb3d789fd90d4c72e0e4c70c2"
@@ -1164,6 +1178,10 @@ clap@^1.0.9:
   dependencies:
     chalk "^1.1.3"
 
+classnames@^2.2.0, classnames@^2.2.5:
+  version "2.2.5"
+  resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.5.tgz#fb3801d453467649ef3603c7d61a02bd129bde6d"
+
 cli-table@^0.3.1:
   version "0.3.1"
   resolved "https://registry.yarnpkg.com/cli-table/-/cli-table-0.3.1.tgz#f53b05266a8b1a0b934b3d0821e6e2dc5914ae23"
@@ -1534,6 +1552,13 @@ create-hmac@^1.1.0, create-hmac@^1.1.2:
     create-hash "^1.1.0"
     inherits "^2.0.1"
 
+create-react-class@^15.5.x:
+  version "15.5.2"
+  resolved "https://registry.yarnpkg.com/create-react-class/-/create-react-class-15.5.2.tgz#6a8758348df660b88326a0e764d569f274aad681"
+  dependencies:
+    fbjs "^0.8.9"
+    object-assign "^4.1.1"
+
 cross-spawn@^3.0.0:
   version "3.0.1"
   resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-3.0.1.tgz#1256037ecb9f0c5f79e3d6ef135e30770184b982"
@@ -1824,6 +1849,10 @@ diffie-hellman@^5.0.0:
     miller-rabin "^4.0.0"
     randombytes "^2.0.0"
 
+dom-helpers@^3.2.0:
+  version "3.2.1"
+  resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-3.2.1.tgz#3203e07fed217bd1f424b019735582fc37b2825a"
+
 domain-browser@^1.1.1:
   version "1.1.7"
   resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.1.7.tgz#867aa4b093faa05f1de08c06f4d7b21fdf8698bc"
@@ -1988,7 +2017,7 @@ escape-html@~1.0.3:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
 
-escape-string-regexp@1.0.5, escape-string-regexp@^1.0.0, escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.3:
+escape-string-regexp@1.0.5, escape-string-regexp@^1.0.0, escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.3, escape-string-regexp@^1.0.5:
   version "1.0.5"
   resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
 
@@ -2790,7 +2819,7 @@ interpret@^1.0.0:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.0.2.tgz#f4f623f0bb7122f15f5717c8e254b8161b5c5b2d"
 
-invariant@^2.2.0:
+invariant@^2.1.0, invariant@^2.2.0, invariant@^2.2.1:
   version "2.2.2"
   resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.2.tgz#9e1f56ac0acdb6bf303306f338be3b204ae60360"
   dependencies:
@@ -3103,6 +3132,10 @@ kareem@1.2.1:
   version "1.2.1"
   resolved "https://registry.yarnpkg.com/kareem/-/kareem-1.2.1.tgz#acdb8c8119845834abbfa58ade1cf9dea63dc752"
 
+keycode@^2.1.2:
+  version "2.1.8"
+  resolved "https://registry.yarnpkg.com/keycode/-/keycode-2.1.8.tgz#94d2b7098215eff0e8f9a8931d5a59076c4532fb"
+
 keygrip@~1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/keygrip/-/keygrip-1.0.1.tgz#b02fa4816eef21a8c4b35ca9e52921ffc89a30e9"
@@ -3388,7 +3421,7 @@ lodash.uniq@^4.5.0:
   version "4.5.0"
   resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
 
-lodash@^4.0.0, lodash@^4.12.0, lodash@^4.14.0, lodash@^4.17.4, lodash@^4.2.0, lodash@^4.3.0, lodash@^4.5.1, lodash@^4.8.0:
+lodash@^4.0.0, lodash@^4.12.0, lodash@^4.14.0, lodash@^4.17.2, lodash@^4.17.4, lodash@^4.2.0, lodash@^4.3.0, lodash@^4.5.1, lodash@^4.8.0:
   version "4.17.4"
   resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae"
 
@@ -3449,6 +3482,10 @@ map-obj@^1.0.0, map-obj@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d"
 
+map-values@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/map-values/-/map-values-1.0.1.tgz#768b8e79c009bf2b64fee806e22a7b1c4190c990"
+
 marked-terminal@^1.6.2:
   version "1.7.0"
   resolved "https://registry.yarnpkg.com/marked-terminal/-/marked-terminal-1.7.0.tgz#c8c460881c772c7604b64367007ee5f77f125904"
@@ -4019,7 +4056,7 @@ object-assign@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-3.0.0.tgz#9bedd5ca0897949bca47e7ff408062d549f587f2"
 
-object-assign@^4.0.1, object-assign@^4.1.0:
+object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1:
   version "4.1.1"
   resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
 
@@ -4027,6 +4064,10 @@ object-component@0.0.3:
   version "0.0.3"
   resolved "https://registry.yarnpkg.com/object-component/-/object-component-0.0.3.tgz#f0c69aa50efc95b866c186f400a33769cb2f1291"
 
+object-filter@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/object-filter/-/object-filter-1.0.2.tgz#af0b797ffebeaf8a52c6637cedbe8816cfec1bc8"
+
 object-get@^2.0.2:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/object-get/-/object-get-2.1.0.tgz#722bbdb60039efa47cad3c6dc2ce51a85c02c5ae"
@@ -4510,7 +4551,7 @@ promise@^7.1.1:
   dependencies:
     asap "~2.0.3"
 
-prop-types@^15.5.7, prop-types@~15.5.7:
+prop-types@^15.5.6, prop-types@^15.5.7, prop-types@^15.5.8, prop-types@~15.5.7:
   version "15.5.8"
   resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.5.8.tgz#6b7b2e141083be38c8595aa51fc55775c7199394"
   dependencies:
@@ -4624,6 +4665,35 @@ rc@^1.1.7:
     minimist "^1.2.0"
     strip-json-comments "~2.0.1"
 
+react-bootstrap-typeahead@^1.3.0:
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/react-bootstrap-typeahead/-/react-bootstrap-typeahead-1.3.0.tgz#dc7383584d2f68b17f3c6d96b82ab32020934801"
+  dependencies:
+    classnames "^2.2.0"
+    invariant "^2.2.1"
+    lodash "^4.17.2"
+    react-highlighter "^0.3.3"
+    react-input-autosize "^1.1.0"
+    react-onclickoutside "^5.7.0"
+    react-overlays "^0.6.10"
+    react-prop-types "^0.4.0"
+    warning "^3.0.0"
+
+react-bootstrap@^0.31.0:
+  version "0.31.0"
+  resolved "https://registry.yarnpkg.com/react-bootstrap/-/react-bootstrap-0.31.0.tgz#bbca804c0404d9c640102b2b656ae4cd5bea35c8"
+  dependencies:
+    babel-runtime "^6.11.6"
+    classnames "^2.2.5"
+    dom-helpers "^3.2.0"
+    invariant "^2.2.1"
+    keycode "^2.1.2"
+    prop-types "^15.5.6"
+    react-overlays "^0.7.0"
+    react-prop-types "^0.4.0"
+    uncontrollable "^4.1.0"
+    warning "^3.0.0"
+
 react-clipboard.js@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/react-clipboard.js/-/react-clipboard.js-1.0.1.tgz#56dea0547c13977cd1a1650fc740e0349aa90bdf"
@@ -4639,6 +4709,48 @@ react-dom@^15.4.2:
     object-assign "^4.1.0"
     prop-types "~15.5.7"
 
+react-highlighter@^0.3.3:
+  version "0.3.3"
+  resolved "https://registry.yarnpkg.com/react-highlighter/-/react-highlighter-0.3.3.tgz#92f8a9e3948c503a215d8eb9a778cabac2fd0ea4"
+  dependencies:
+    blacklist "^1.1.2"
+    escape-string-regexp "^1.0.5"
+
+react-input-autosize@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/react-input-autosize/-/react-input-autosize-1.1.0.tgz#3fe1ac832387d8abab85f6051ceab1c9e5570853"
+
+react-onclickoutside@^5.7.0:
+  version "5.11.1"
+  resolved "https://registry.yarnpkg.com/react-onclickoutside/-/react-onclickoutside-5.11.1.tgz#00314e52567cf55faba94cabbacd119619070623"
+  dependencies:
+    create-react-class "^15.5.x"
+
+react-overlays@^0.6.10:
+  version "0.6.12"
+  resolved "https://registry.yarnpkg.com/react-overlays/-/react-overlays-0.6.12.tgz#a079c750cc429d7db4c7474a95b4b54033e255c3"
+  dependencies:
+    classnames "^2.2.5"
+    dom-helpers "^3.2.0"
+    react-prop-types "^0.4.0"
+    warning "^3.0.0"
+
+react-overlays@^0.7.0:
+  version "0.7.0"
+  resolved "https://registry.yarnpkg.com/react-overlays/-/react-overlays-0.7.0.tgz#531898ff566c7e5c7226ead2863b8cf9fbb5a981"
+  dependencies:
+    classnames "^2.2.5"
+    dom-helpers "^3.2.0"
+    prop-types "^15.5.8"
+    react-prop-types "^0.4.0"
+    warning "^3.0.0"
+
+react-prop-types@^0.4.0:
+  version "0.4.0"
+  resolved "https://registry.yarnpkg.com/react-prop-types/-/react-prop-types-0.4.0.tgz#f99b0bfb4006929c9af2051e7c1414a5c75b93d0"
+  dependencies:
+    warning "^3.0.0"
+
 react@^15.4.2:
   version "15.5.4"
   resolved "https://registry.yarnpkg.com/react/-/react-15.5.4.tgz#fa83eb01506ab237cdc1c8c3b1cea8de012bf047"
@@ -4976,6 +5088,10 @@ rndm@1.2.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/rndm/-/rndm-1.2.0.tgz#f33fe9cfb52bbfd520aa18323bc65db110a1b76c"
 
+run-parallel@^1.1.4:
+  version "1.1.6"
+  resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.1.6.tgz#29003c9a2163e01e2d2dfc90575f2c6c1d61a039"
+
 rx@2.3.24:
   version "2.3.24"
   resolved "https://registry.yarnpkg.com/rx/-/rx-2.3.24.tgz#14f950a4217d7e35daa71bbcbe58eff68ea4b2b7"
@@ -5018,7 +5134,7 @@ select@^1.1.2:
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/select/-/select-1.1.2.tgz#0e7350acdec80b1108528786ec1d4418d11b396d"
 
-"semver@2 || 3 || 4 || 5", semver@^5.1.0, semver@^5.3.0, semver@~5.3.0:
+"semver@2 || 3 || 4 || 5", semver@^5.0.3, semver@^5.1.0, semver@^5.3.0, semver@~5.3.0:
   version "5.3.0"
   resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f"
 
@@ -5633,6 +5749,12 @@ ultron@1.0.x:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/ultron/-/ultron-1.0.2.tgz#ace116ab557cd197386a4e88f4685378c8b2e4fa"
 
+uncontrollable@^4.1.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/uncontrollable/-/uncontrollable-4.1.0.tgz#e0358291252e1865222d90939b19f2f49f81c1a9"
+  dependencies:
+    invariant "^2.1.0"
+
 underscore@~1.7.0:
   version "1.7.0"
   resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.7.0.tgz#6bbaf0877500d36be34ecaa584e0db9fef035209"
@@ -5721,6 +5843,12 @@ ware@^1.3.0:
   dependencies:
     wrap-fn "^0.1.0"
 
+warning@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/warning/-/warning-3.0.0.tgz#32e5377cb572de4ab04753bdf8821c01ed605b7c"
+  dependencies:
+    loose-envify "^1.0.0"
+
 watchpack@^1.3.1:
   version "1.3.1"
   resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.3.1.tgz#7d8693907b28ce6013e7f3610aa2a1acf07dad87"