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

Merge branch 'master' into rc/1.0.x

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

+ 44 - 0
.github/ISSUE_TEMPLATE.md

@@ -0,0 +1,44 @@
+Environment
+------------
+
+- [OS]
+- [node.js] x.x.x
+- [npm] y.y.y
+- [browser] z.z.z
+
+
+How to reproduce? (再現手順)
+---------------------------
+
+1. process 1
+
+    ```bash
+    
+    ```
+
+1. process 2
+1. process 3
+
+
+What's happen? (症状)
+---------------------
+
+- symptom
+
+```
+Stack Trace
+```
+
+
+What is the expected result? (期待される動作)
+-------------------------------------------
+
+- 
+- 
+
+
+Note
+----
+
+- 
+- 

+ 28 - 0
CHANGES.md

@@ -1,6 +1,34 @@
 CHANGES
 ========
 
+## 1.0.6
+
+* Fix: https access to Gravatar
+* Fix: server watching crash with `Error: read ECONNRESET` on Google Chrome
+
+## 1.0.5
+
+* Feature: Ensure to use Gravatar for profile image
+
+## 1.0.4
+
+* Improvement: Detach code blocks before preProcess
+* Support: Ensure to deploy to Heroku with INSTALL_PLUGINS env
+* Support: Ensure to load plugins easily when development
+
+## 1.0.3
+
+* Improvement: Adjust styles
+
+## 1.0.2
+
+* Improvement: For lsx 
+
+## 1.0.1
+
+* Feature: Custom CSS
+* Support: Notify build failure to Slask
+
 ## 1.0.0
 
 * Feature: Plugin mechanism

+ 50 - 7
README.md

@@ -5,14 +5,14 @@
 </p>
 
 
-crowi-plus [![Chat on Slack](https://crowi-plus-slackin.weseek.co.jp/badge.svg)](https://crowi-plus-slackin.weseek.co.jp/)
+crowi-plus [![Chat on Slack](https://crowi-plus-slackin.weseek.co.jp/badge.svg)][slackin]
 ===========
 
 [![wercker status](https://app.wercker.com/status/39cdc49d067d65c39cb35d52ceae6dc1/s/master "wercker status")](https://app.wercker.com/project/byKey/39cdc49d067d65c39cb35d52ceae6dc1)
 [![dependencies status](https://david-dm.org/weseek/crowi-plus.svg)](https://david-dm.org/weseek/crowi-plus)
 [![MIT License](http://img.shields.io/badge/license-MIT-blue.svg?style=flat)](LICENSE)
 
-This is **crowi-plus** that is the fork of [Crowi](https://github.com/crowi/crowi), is [perfectly compatible with official](https://github.com/weseek/crowi-plus/wiki/Questions-and-Answers#does-crowi-plus-have-compatibility-with-official-crowi), and has been enhanced with the following points:
+This is **crowi-plus** that is the fork of [Crowi][crowi], is [perfectly compatible with official](https://github.com/weseek/crowi-plus/wiki/Questions-and-Answers#does-crowi-plus-have-compatibility-with-official-crowi), and has been enhanced with the following points:
 
 * Pluggable
   * Find plugins from [npm](https://www.npmjs.com/browse/keyword/crowi-plugin) or [github](https://github.com/search?q=topic%3Acrowi-plugin)!
@@ -22,8 +22,8 @@ This is **crowi-plus** that is the fork of [Crowi](https://github.com/crowi/crow
 * Secure
   * Upgrade jQuery to 3.x
   * Upgrade other insecure libs
-* [Docker Ready](https://hub.docker.com/r/weseek/crowi-plus/)
-* [Docker Compose Ready](https://github.com/weseek/crowi-plus-docker-compose)
+* [Docker Ready][dockerhub]
+* [Docker Compose Ready][docker-compose]
 * [Added miscellaneous features](https://github.com/weseek/crowi-plus/wiki/Additional-Features)
 * Developer-friendly
   * Less compile time
@@ -48,7 +48,7 @@ cd crowi-plus
 docker-compose up
 ```
 
-see also [weseek/crowi-plus-docker-compose](https://github.com/weseek/crowi-plus-docker-compose)
+see also [weseek/crowi-plus-docker-compose][docker-compose]
 
 On-premise
 ----------
@@ -148,14 +148,57 @@ npm test
 ```
 
 Documentation
---------------
+==============
 
 * [github wiki pages](https://github.com/weseek/crowi-plus/wiki)
   * [Questions and Answers](https://github.com/weseek/crowi-plus/wiki/Questions-and-Answers)
 
 
-License
+Contributing
+============
+
+Found a Bug?
+-------------
+
+If you find a bug in the source code, you can help us by
+[submitting an issue][issues] to our [GitHub Repository][crowi-plus]. Even better, you can
+[submit a Pull Request][pulls] with a fix.
+
+Missing a Feature?
+-------------------
+
+You can *request* a new feature by [submitting an issue][issues] to our GitHub
+Repository. If you would like to *implement* a new feature, please submit an issue with
+a proposal for your work first, to be sure that we can use it.
+Please consider what kind of change it is:
+
+* For a **Major Feature**, first open an issue and outline your proposal so that it can be
+discussed. This will also allow us to better coordinate our efforts, prevent duplication of work,
+and help you to craft the change so that it is successfully accepted into the project.
+* **Small Features** can be crafted and directly [submitted as a Pull Request][pulls].
+
+Language
 ---------
 
+Write issues and PRs in Engulish or Japanese.
+
+Discussion
+-----------
+
+If you have something to ask or want to discuss, [join to our Slack team][slackin] and talk about anything, anytime.
+
+
+License
+=======
+
 * The MIT License (MIT)
 * See LICENSE file.
+
+
+[crowi]: https://github.com/crowi/crowi
+[crowi-plus]: https://github.com/weseek/crowi-plus
+[issues]: https://github.com/weseek/crowi-plus/issues
+[pulls]: https://github.com/weseek/crowi-plus/pulls
+[dockerhub]: https://hub.docker.com/r/weseek/crowi-plus
+[docker-compose]: https://github.com/weseek/crowi-plus-docker-compose
+[slackin]: https://crowi-plus-slackin.weseek.co.jp/

+ 30 - 1
lib/crowi/dev.js

@@ -1,7 +1,9 @@
 const debug = require('debug')('crowi:crowi:dev');
 const path = require('path');
 const webpack = require('webpack');
-const helpers = require('./helpers')
+const helpers = require('./helpers');
+
+const LRWebSocketServer = require('livereload-server/lib/server');
 
 class CrowiDev {
 
@@ -16,6 +18,33 @@ class CrowiDev {
   }
 
   init() {
+    this.hackLRWebSocketServer();
+  }
+
+  /**
+   * prevent to crash socket with:
+   * -------------------------------------------------
+   * Error: read ECONNRESET
+   *     at exports._errnoException (util.js:1022:11)
+   *     at TCP.onread (net.js:569:26)
+   * -------------------------------------------------
+   *
+   * @see https://github.com/napcs/node-livereload/pull/15
+   *
+   * @memberOf CrowiDev
+   */
+  hackLRWebSocketServer() {
+    const orgCreateConnection = LRWebSocketServer.prototype._createConnection;
+
+    // replace https://github.com/livereload/livereload-server/blob/v0.2.3/lib/server.coffee#L74
+    LRWebSocketServer.prototype._createConnection = function(socket) {
+      // call original method with substituting 'this' obj
+      orgCreateConnection.call(this, socket);
+
+      socket.on('error', (err) => {
+        console.warn(`[WARN] Worthless error in client socket: '${err}'`);
+      });
+    }
   }
 
   /**

+ 0 - 8
lib/util/middlewares.js

@@ -127,14 +127,6 @@ exports.swigFilters = function(app, swig) {
         .replace(/\n/g, '<br>');
     });
 
-    swig.setFilter('insertSpaceToEachSlashes', function(string) {
-      if (string == '/') {
-        return string;
-      }
-
-      return string.replace(/\//g, ' / ');
-    });
-
     swig.setFilter('removeLastSlash', function(string) {
       if (string == '/') {
         return string;

+ 4 - 2
lib/views/page.html

@@ -14,7 +14,7 @@
 
 
     <div class="flex-title-line">
-      <h1 class="title flex-item-title" id="revision-path">{{ path|insertSpaceToEachSlashes }}</h1>
+      <h1 class="title flex-item-title" id="revision-path"></h1>
       {% if page %}
       <div class="flex-item-action">
         <a href="#" title="Bookmark" class="bookmark-link" id="bookmark-button" data-csrftoken="{{ csrf() }}" data-bookmarked="0"><i class="fa fa-star-o"></i></a>
@@ -28,12 +28,14 @@
       </div>
       {% endif %}
     </div>
+
+    <div id="revision-url" class="url-line"></div>
   </header>
   {% else %}
   {# trash/* #}
   <header id="page-header">
     <div class="flex-title-line">
-      <h1 class="title flex-item-title">{{ path|insertSpaceToEachSlashes }}</h1>
+      <h1 class="title flex-item-title"></h1>
       <div class="flex-item-action">
         <a href="#" title="Bookmark" class="bookmark-link" id="bookmark-button" data-csrftoken="{{ csrf() }}" data-bookmarked="0"><i class="fa fa-star-o"></i></a>
       </div>

+ 7 - 7
lib/views/page_list.html

@@ -16,15 +16,15 @@
   <header class="portal-header {% if page %}has-page{% endif %}">
     <div class="flex-title-line">
       <h1 class="title flex-item-title">
-        <span id="revision-path">{{ path|insertSpaceToEachSlashes }}</span>
+        <span id="revision-path"></span>
         {% if searchConfigured() && !isTopPage() && !isTrashPage() %}
         <form class="input-group search-input-group hidden-xs hidden-sm" data-toggle="tooltip" data-placement="bottom" title="{{ path }} 以下から検索" id="search-listpage-form">
-          <input type="text" class="search-listpage-input form-control" data-path="{{ path }}" id="search-listpage-input">
-          <span class="input-group-btn search-listpage-submit-group">
-            <button type="submit" class="btn btn-default" id="search-listpage-submit">
-              <i class="fa fa-search"></i>
-            </button>
-          </span>
+          <div class="input-group">
+            <input id="#search-listpage-input" type="text" class="form-control" data-path="{{ path }}" placeholder="Search for...">
+            <span class="input-group-btn">
+              <button class="btn btn-default"><i class="fa fa-search"></i></button>
+            </span>
+          </div><!-- /input-group -->
           <a class="search-listpage-clear" id="search-listpage-clear"><i class="fa fa-times-circle"></i></a>
         </form>
         {% endif %}

+ 1 - 1
lib/views/user_page.html

@@ -7,7 +7,7 @@
 {% if pageUser %}
 
 <div class="header-wrap">
-  <h1 class="title" id="revision-path">{{ path|insertSpaceToEachSlashes }}</h1>
+  <h1 class="title" id="revision-path"></h1>
   <div class="user-page-header">
   {% if page %}
     <a href="#" title="Bookmark" class="bookmark-link" id="bookmark-button" data-csrftoken="{{ csrf() }}" data-bookmarked="0"><i class="fa fa-star-o"></i></a>

+ 1 - 0
package.json

@@ -103,6 +103,7 @@
     "normalize-path": "^2.1.1",
     "optimize-js-plugin": "0.0.4",
     "react": "^15.4.2",
+    "react-clipboard.js": "^1.0.1",
     "react-dom": "^15.4.2",
     "redis": "^2.7.1",
     "reveal.js": "~3.4.0",

+ 1 - 1
resource/css/_layout.scss

@@ -73,7 +73,7 @@
       }
 
       .content-main {
-        padding: 16px;
+        padding: 8px 16px;
       }
     }
 

+ 12 - 1
resource/css/_page.scss

@@ -65,6 +65,7 @@
       }
 
       .flex-title-line {
+        line-height: 1;
         display: -webkit-flex;
         display: flex;
         -webkit-align-items: center;
@@ -88,7 +89,11 @@
         margin-top: 0;
         margin-bottom: 0;
 
-        a:last-child {
+        .btn-copy-container {
+          font-size: 0.8em;
+        }
+
+        a.last-path {
           color: #D1E2E4;
           opacity: .4;
 
@@ -96,6 +101,12 @@
             color: inherit;
           }
         }
+
+      }
+
+      .url-line {
+        color: #999;
+        font-size: 1rem;
       }
     }
 

+ 1 - 12
resource/css/_search.scss

@@ -17,21 +17,10 @@
 
 .search-input-group {
   display: inline-block;
+  margin-left: 20px;
   margin-bottom: 0;
   width: 200px;
   vertical-align: bottom;
-
-  .search-listpage-submit-group {
-    position: absolute;
-    width: 30px;
-    height: 30px;
-    padding-left: 197px;
-    border-radius: 0;
-
-    > .btn {
-      padding: 6px 12px 7px;
-    }
-  }
 }
 
 

+ 6 - 0
resource/js/app.js

@@ -9,6 +9,8 @@ import SearchPage       from './components/SearchPage';
 import PageListSearch   from './components/PageListSearch';
 import PageHistory      from './components/PageHistory';
 import SeenUserList     from './components/SeenUserList';
+import RevisionPath     from './components/Page/RevisionPath';
+import RevisionUrl      from './components/Page/RevisionUrl';
 //import PageComment  from './components/PageComment';
 
 if (!window) {
@@ -17,8 +19,10 @@ if (!window) {
 
 const mainContent = document.querySelector('#content-main');
 let pageId = null;
+let pagePath;
 if (mainContent !== null) {
   pageId = mainContent.attributes['data-page-id'].value;
+  pagePath = mainContent.attributes['data-path'].value;
 }
 
 // FIXME
@@ -43,6 +47,8 @@ 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} />,
 };
 
 Object.keys(componentMappings).forEach((key) => {

+ 54 - 0
resource/js/components/CopyButton.js

@@ -0,0 +1,54 @@
+import React from 'react';
+import ClipboardButton from 'react-clipboard.js';
+
+export default class CopyButton extends React.Component {
+
+  constructor(props) {
+    super(props);
+
+    this.showToolTip = this.showToolTip.bind(this);
+  }
+
+  showToolTip() {
+    const buttonId = `#${this.props.buttonId}`;
+    $(buttonId).tooltip('show');
+    setTimeout(() => {
+      $(buttonId).tooltip('hide');
+    }, 1000);
+  }
+
+  render() {
+    const containerStyle = {
+      verticalAlign: "top"
+    }
+    const style = Object.assign({
+      fontSize: "0.8em",
+      padding: "0 2px",
+      border: 'none'
+    }, this.props.buttonStyle);
+
+    return (
+      <span className="btn-copy-container" style={containerStyle}>
+        <ClipboardButton className={this.props.buttonClassName}
+            button-id={this.props.buttonId} button-data-toggle="tooltip" button-title="copied!" button-data-placement="bottom" button-data-trigger="manual"
+            button-style={style}
+            data-clipboard-text={this.props.text} onSuccess={this.showToolTip}>
+
+          <i className={this.props.iconClassName}></i>
+        </ClipboardButton>
+      </span>
+    );
+  }
+}
+
+CopyButton.propTypes = {
+  text: React.PropTypes.string.isRequired,
+  buttonId: React.PropTypes.string.isRequired,
+  buttonClassName: React.PropTypes.string.isRequired,
+  buttonStyle: React.PropTypes.object,
+  iconClassName: React.PropTypes.string.isRequired,
+};
+CopyButton.defaultProps = {
+  buttonId: 'btnCopy',
+  buttonStyle: {},
+};

+ 0 - 64
resource/js/components/Page/PagePath.js

@@ -1,64 +0,0 @@
-import React from 'react';
-
-export default class PagePath extends React.Component {
-
-  // Original Crowi.linkPath
-  /*
-  Crowi.linkPath = function(revisionPath) {
-    var $revisionPath = revisionPath || '#revision-path';
-    var $title = $($revisionPath);
-    var pathData = $('#content-main').data('path');
-
-    if (!pathData) {
-      return ;
-    }
-
-    var realPath = pathData.trim();
-    if (realPath.substr(-1, 1) == '/') {
-      realPath = realPath.substr(0, realPath.length - 1);
-    }
-
-    var path = '';
-    var pathHtml = '';
-    var splittedPath = realPath.split(/\//);
-    splittedPath.shift();
-    splittedPath.forEach(function(sub) {
-      path += '/';
-      pathHtml += ' <a href="' + path + '">/</a> ';
-      if (sub) {
-        path += sub;
-        pathHtml += '<a href="' + path + '">' + sub + '</a>';
-      }
-    });
-    if (path.substr(-1, 1) != '/') {
-      path += '/';
-      pathHtml += ' <a href="' + path + '" class="last-path">/</a>';
-    }
-    $title.html(pathHtml);
-  };
-  */
-
-  linkPath(path) {
-    return path;
-  }
-
-  render() {
-    const page = this.props.page;
-    const shortPath = this.getShortPath(page.path);
-    const pathPrefix = page.path.replace(new RegExp(shortPath + '(/)?$'), '');
-
-    return (
-      <span className="page-path">
-        {pathPrefix}<strong>{shortPath}</strong>
-      </span>
-    );
-  }
-}
-
-PagePath.propTypes = {
-  page: React.PropTypes.object.isRequired,
-};
-
-PagePath.defaultProps = {
-  page: {},
-};

+ 90 - 0
resource/js/components/Page/RevisionPath.js

@@ -0,0 +1,90 @@
+import React from 'react';
+import CopyButton from '../CopyButton';
+
+export default class RevisionPath extends React.Component {
+
+  constructor(props) {
+    super(props);
+
+    this.state = {
+      pages: [],
+      isListPage: false,
+    };
+  }
+
+  componentWillMount() {
+    // whether list page or not
+    const isListPage = this.props.pagePath.match(/\/$/);
+    this.setState({ isListPage });
+
+    // generate pages obj
+    let splitted = this.props.pagePath.split(/\//);
+    splitted.shift();   // omit first element with shift()
+    if (splitted[splitted.length-1] === '') {
+      splitted.pop();   // omit last element with unshift()
+    }
+
+    let pages = [];
+    let parentPath = '/';
+    splitted.forEach((pageName) => {
+      pages.push({
+        pagePath: parentPath + pageName,
+        pageName: pageName,
+      });
+      parentPath += pageName + '/';
+    });
+
+    this.setState({ pages });
+  }
+
+  showToolTip() {
+    $('#btnCopy').tooltip('show');
+    setTimeout(() => {
+      $('#btnCopy').tooltip('hide');
+    }, 1000);
+  }
+
+  render() {
+    // define style
+    const rootStyle = {
+      marginRight: "0.2em",
+    }
+    const separatorStyle = {
+      marginLeft: "0.2em",
+      marginRight: "0.2em",
+    }
+
+    const pageLength = this.state.pages.length;
+
+    const afterElements = [];
+    this.state.pages.forEach((page, index) => {
+      const isLastElement = (index == pageLength-1);
+
+      // add elements
+      afterElements.push(
+        <span key={page.pagePath} className="path-segment">
+          <a href={page.pagePath}>{page.pageName}</a>
+        </span>);
+      afterElements.push(
+        <span key={page.pagePath+'/'} className="separator" style={separatorStyle}>
+          <a href={page.pagePath+'/'} className={(isLastElement && !this.state.isListPage) ? 'last-path' : ''}>/</a>
+        </span>
+      );
+    });
+
+    return (
+      <span>
+        <span className="separator" style={rootStyle}>
+          <a href="/">/</a>
+        </span>
+        {afterElements}
+        <CopyButton buttonId="btnCopyRevisionPath" text={this.props.pagePath}
+            buttonClassName="btn btn-default" iconClassName="fa fa-clone text-muted" />
+      </span>
+    );
+  }
+}
+
+RevisionPath.propTypes = {
+  pagePath: React.PropTypes.string.isRequired,
+};

+ 32 - 0
resource/js/components/Page/RevisionUrl.js

@@ -0,0 +1,32 @@
+import React from 'react';
+import CopyButton from '../CopyButton';
+
+export default class RevisionUrl extends React.Component {
+
+  showToolTip() {
+    $('#btnCopy').tooltip('show');
+    setTimeout(() => {
+      $('#btnCopy').tooltip('hide');
+    }, 1000);
+  }
+
+  render() {
+    const buttonStyle = {
+      fontSize: "1em"
+    }
+
+    const text = this.props.pagePath + '\n' + this.props.url;
+    return (
+      <span>
+        {this.props.url}
+        <CopyButton buttonId="btnCopyRevisionUrl" text={text}
+            buttonClassName="btn btn-default" buttonStyle={buttonStyle} iconClassName="fa fa-link text-muted" />
+      </span>
+    );
+  }
+}
+
+RevisionUrl.propTypes = {
+  pagePath: React.PropTypes.string.isRequired,
+  url: React.PropTypes.string.isRequired,
+};

+ 1 - 2
resource/js/components/User/UserPicture.js

@@ -7,7 +7,6 @@ export default class UserPicture extends React.Component {
   getUserPicture(user) {
     // gravatar
     if (user.isGravatarEnabled === true) {
-      console.log(user.username + ": isGravatarEnabled true");
       return this.generateGravatarSrc(user);
     }
     // uploaded image
@@ -21,7 +20,7 @@ export default class UserPicture extends React.Component {
 
   generateGravatarSrc(user) {
     const hash = md5(user.email.trim().toLowerCase());
-    return `http://www.gravatar.com/avatar/${hash}`;
+    return `https://www.gravatar.com/avatar/${hash}`;
   }
 
   getClassName() {

+ 0 - 35
resource/js/legacy/crowi.js

@@ -18,39 +18,6 @@ Crowi.createErrorView = function(msg) {
   $('#main').prepend($('<p class="alert-message error">' + msg + '</p>'));
 };
 
-Crowi.linkPath = function(revisionPath) {
-  var $revisionPath = revisionPath || '#revision-path';
-  var $title = $($revisionPath);
-  var pathData = $('#content-main').data('path');
-
-  if (!pathData) {
-    return ;
-  }
-
-  var realPath = pathData.trim();
-  if (realPath.substr(-1, 1) == '/') {
-    realPath = realPath.substr(0, realPath.length - 1);
-  }
-
-  var path = '';
-  var pathHtml = '';
-  var splittedPath = realPath.split(/\//);
-  splittedPath.shift();
-  splittedPath.forEach(function(sub) {
-    path += '/';
-    pathHtml += ' <a href="' + Crowi.escape(path) + '">/</a> ';
-    if (sub) {
-      path += sub;
-      pathHtml += '<a href="' + Crowi.escape(path) + '">' + Crowi.escape(sub) + '</a>';
-    }
-  });
-  if (path.substr(-1, 1) != '/') {
-    path += '/';
-    pathHtml += ' <a href="' + Crowi.escape(path) + '" class="last-path">/</a>';
-  }
-  $title.html(pathHtml);
-};
-
 Crowi.correctHeaders = function(contentId) {
   // h1 ~ h6 の id 名を補正する
   var $content = $(contentId || '#revision-body-content');
@@ -194,8 +161,6 @@ $(function() {
     }
   };
 
-  Crowi.linkPath();
-
   $('[data-toggle="popover"]').popover();
   $('[data-toggle="tooltip"]').tooltip();
   $('[data-tooltip-stay]').tooltip('show');

+ 32 - 0
yarn.lock

@@ -1184,6 +1184,14 @@ cli@~1.0.1:
     exit "0.1.2"
     glob "^7.1.1"
 
+clipboard@^1.4.0:
+  version "1.6.1"
+  resolved "https://registry.yarnpkg.com/clipboard/-/clipboard-1.6.1.tgz#65c5b654812466b0faab82dc6ba0f1d2f8e4be53"
+  dependencies:
+    good-listener "^1.2.0"
+    select "^1.1.2"
+    tiny-emitter "^1.0.0"
+
 cliui@^2.1.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/cliui/-/cliui-2.1.0.tgz#4b475760ff80264c762c3a1719032e91c7fea0d1"
@@ -1756,6 +1764,10 @@ delayed-stream@~1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
 
+delegate@^3.1.2:
+  version "3.1.2"
+  resolved "https://registry.yarnpkg.com/delegate/-/delegate-3.1.2.tgz#1e1bc6f5cadda6cb6cbf7e6d05d0bcdd5712aebe"
+
 delegates@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a"
@@ -2461,6 +2473,12 @@ globule@^1.0.0:
     lodash "~4.16.4"
     minimatch "~3.0.2"
 
+good-listener@^1.2.0:
+  version "1.2.2"
+  resolved "https://registry.yarnpkg.com/good-listener/-/good-listener-1.2.2.tgz#d53b30cdf9313dffb7dc9a0d477096aa6d145c50"
+  dependencies:
+    delegate "^3.1.2"
+
 google-auth-library@~0.9.7:
   version "0.9.10"
   resolved "https://registry.yarnpkg.com/google-auth-library/-/google-auth-library-0.9.10.tgz#4993dc07bb4834b8ca0350213a6873a32c6051b9"
@@ -4606,6 +4624,12 @@ rc@^1.1.7:
     minimist "^1.2.0"
     strip-json-comments "~2.0.1"
 
+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"
+  dependencies:
+    clipboard "^1.4.0"
+
 react-dom@^15.4.2:
   version "15.5.4"
   resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-15.5.4.tgz#ba0c28786fd52ed7e4f2135fe0288d462aef93da"
@@ -4990,6 +5014,10 @@ sax@~1.2.1:
   version "1.2.2"
   resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.2.tgz#fd8631a23bc7826bef5d871bdb87378c95647828"
 
+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:
   version "5.3.0"
   resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f"
@@ -5474,6 +5502,10 @@ timers-browserify@^2.0.2:
   dependencies:
     setimmediate "^1.0.4"
 
+tiny-emitter@^1.0.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/tiny-emitter/-/tiny-emitter-1.2.0.tgz#6dc845052cb08ebefc1874723b58f24a648c3b6f"
+
 tinycolor@0.x:
   version "0.0.1"
   resolved "https://registry.yarnpkg.com/tinycolor/-/tinycolor-0.0.1.tgz#320b5a52d83abb5978d81a3e887d4aefb15a6164"