feat(GUI): dynamic finish page (#1368)

* feat(GUI): dynamic finish page

We implement an externally loaded dynamic finish page in React with
`react2angular`. If the Internet connection is unreliable or unavailable, or a
non-200 HTTP response is returned we display a fallback default finish banner.

Change-Type: minor
Changelog-Entry: Implement a dynamic finish page.
Signed-off-by: Juan Cruz Viotti <jviotti@openmailbox.org>
This commit is contained in:
Benedict Aas 2017-05-12 06:07:24 +01:00 committed by Juan Cruz Viotti
parent 5c33abca21
commit 2c26b4c6ac
10 changed files with 560 additions and 36 deletions

View File

@ -59,6 +59,7 @@ const app = angular.module('Etcher', [
require('./components/svg-icon/svg-icon'),
require('./components/update-notifier/update-notifier'),
require('./components/warning-modal/warning-modal'),
require('./components/safe-webview/safe-webview'),
// Pages
require('./pages/main/main'),
@ -296,3 +297,12 @@ app.controller('HeaderController', function(SelectionStateModel, OSOpenExternalS
};
});
app.controller('StateController', function($state) {
/**
* @param {string} state - state page
*/
this.is = $state.is;
});

12
lib/gui/assets/love.svg Normal file
View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg viewBox="0 0 50 46" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 43.2 (39069) - http://www.bohemiancoding.com/sketch -->
<title>like</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Steps" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="like" fill-rule="nonzero" fill="#F55F50">
<path d="M24.85,8.126 C26.868,3.343 31.478,0.001 36.84,0.001 C44.063,0.001 49.265,6.18 49.919,13.544 C49.919,13.544 50.272,15.372 49.495,18.663 C48.437,23.145 45.95,27.127 42.597,30.166 L24.85,46 L7.402,30.165 C4.049,27.127 1.562,23.144 0.504,18.662 C-0.273,15.371 0.08,13.543 0.08,13.543 C0.734,6.179 5.936,0 13.159,0 C18.522,0 22.832,3.343 24.85,8.126 Z" id="Shape"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 876 B

View File

@ -0,0 +1,173 @@
/*
* Copyright 2017 resin.io
*
* Licensed 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.
*/
'use strict';
const _ = require('lodash');
const electron = require('electron');
const angular = require('angular');
const react = require('react');
const propTypes = require('prop-types');
const react2angular = require('react2angular').react2angular;
const analytics = require('../../modules/analytics');
const packageJSON = require('../../../../package.json');
const MODULE_NAME = 'Etcher.Components.SafeWebview';
const angularSafeWebview = angular.module(MODULE_NAME, []);
/**
* @summary Webviews that time out
* @type {Object}
* @public
*/
class SafeWebview extends react.PureComponent {
/**
* @param {Object} props - React element properties
*/
constructor(props) {
super(props);
this.state = {
shouldLoad: true
};
// Events steal 'this'
this.didFailLoad = _.bind(this.didFailLoad, this);
this.didGetResponseDetails = _.bind(this.didGetResponseDetails, this);
}
/**
* @returns {react.Element}
*/
render() {
if (this.state.shouldLoad) {
const url = new URL(this.props.src);
// We set the 'etcher-version' GET parameter here.
url.searchParams.set('etcher-version', packageJSON.version);
return react.createElement('webview', {
ref: 'webview',
src: url.href
}, []);
}
// We have to return null explicitly, undefined is an error in React.
return null;
}
/**
* @summary Add the Webview events if there is an element
*/
componentDidMount() {
// There is no element to add events to if 'shouldLoad' is false.
if (this.state.shouldLoad) {
// Events React is unaware of have to be handled manually
this.refs.webview.addEventListener('did-fail-load', this.didFailLoad);
this.refs.webview.addEventListener('did-get-response-details', this.didGetResponseDetails);
this.refs.webview.addEventListener('new-window', this.constructor.newWindow);
this.refs.webview.addEventListener('console-message', this.constructor.consoleMessage);
}
}
/**
* @summary Remove the Webview events if there is an element
*/
componentWillUnmount() {
// There is no element to remove events from if 'shouldLoad' is false.
if (this.state.shouldLoad) {
// Events React is unaware of have to be handled manually
this.refs.webview.removeEventListener('did-fail-load', this.didFailLoad);
this.refs.webview.removeEventListener('did-get-response-details', this.didGetResponseDetails);
this.refs.webview.removeEventListener('new-window', this.constructor.newWindow);
this.refs.webview.removeEventListener('console-message', this.constructor.consoleMessage);
}
}
/**
* @summary Set the element state to hidden
*/
didFailLoad() {
this.setState({
shouldLoad: false
});
}
/**
* @summary Set the element state depending on the HTTP response code
* @param {Event} event - Event object
*/
didGetResponseDetails(event) {
const HTTP_OK = 200;
const HTTP_ERR = 400;
if (event.httpResponseCode < HTTP_OK || event.httpResponseCode >= HTTP_ERR) {
this.setState({
shouldLoad: false
});
}
}
/**
* @param {Event} event - event object
*/
static newWindow(event) {
const url = new URL(event.url);
if (_.every([
url.protocol === 'http:' || url.protocol === 'https:',
event.disposition === 'foreground-tab'
])) {
electron.shell.openExternal(url.href);
}
}
/**
* @param {Event} event - event object
*/
static consoleMessage(event) {
const ERROR_LEVEL = 2;
if (event.level < ERROR_LEVEL) {
analytics.logEvent(event.message);
} else if (event.level === ERROR_LEVEL) {
analytics.logException(event.message);
}
}
}
SafeWebview.propTypes = {
/**
* @summary The website source URL
*/
src: propTypes.string.isRequired
};
angularSafeWebview.component('safeWebview', react2angular(SafeWebview));
module.exports = MODULE_NAME;

View File

@ -6604,6 +6604,9 @@ body {
* See the License for the specific language governing permissions and
* limitations under the License.
*/
.page-finish {
margin-top: -25px; }
.page-finish .button-label {
margin: 0 auto 15px;
max-width: 165px; }
@ -6612,7 +6615,11 @@ body {
min-width: 170px; }
.page-finish .title {
color: #fff; }
color: #fff;
font-weight: bold; }
.page-finish .huge-title {
font-size: 3.5em; }
.page-finish .label {
display: inline-block; }
@ -6629,6 +6636,54 @@ body {
padding: 0px;
min-width: 2px; }
.page-finish .center {
display: flex;
align-items: center;
justify-content: center; }
.page-finish .box > div {
margin-bottom: 20px; }
.page-finish .box > div > button {
margin-right: 20px; }
.page-finish webview {
width: 800px;
height: 300px;
position: absolute;
top: 80px;
left: 0;
z-index: 9001; }
.page-finish .fallback-banner {
padding-top: 35px;
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
height: 300px;
position: absolute;
top: 125px;
left: 0;
background-color: #535760;
color: #fff;
font-weight: 300; }
.page-finish .fallback-banner .caption {
display: flex; }
.page-finish .fallback-banner .caption-big {
font-size: 30px; }
.page-finish .fallback-banner .caption-small {
font-size: 20px;
padding: 15px; }
.page-finish .fallback-banner .footer-right {
padding: 0;
border-bottom: 1px dashed; }
.page-finish .fallback-banner .svg-icon {
margin-left: 0.3em;
margin-right: 0.3em; }
.page-finish .fallback-banner .section-footer {
border-top: none;
height: 39px; }
body {
letter-spacing: 1px; }
@ -6660,6 +6715,19 @@ body {
right: 0;
top: 50%; }
.section-loader webview {
flex: 0 1;
height: 0;
width: 0; }
.section-loader.isFinish webview {
flex: initial;
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 255px; }
.wrapper {
height: 100%;
margin: 20px 60px; }

View File

@ -30,7 +30,8 @@
<main class="wrapper" ui-view></main>
<footer class="section-footer">
<footer class="section-footer" ng-controller="StateController as state"
ng-hide="state.is('success')">
<svg-icon path="../../../assets/etcher.svg"
width="83px"
height="13px"
@ -49,5 +50,14 @@
manifest-bind="version"
os-open-external="https://github.com/resin-io/etcher/blob/master/CHANGELOG.md"></span>
</footer>
<div class="section-loader"
ng-controller="StateController as state"
ng-class="{
isFinish: state.is('success')
}">
<safe-webview
src="'https://etcher.io/success-banner/'"></safe-webview>
</div>
</body>
</html>

View File

@ -14,6 +14,10 @@
* limitations under the License.
*/
.page-finish {
margin-top: -25px;
}
.page-finish .button-label {
margin: 0 auto $spacing-medium;
@ -27,6 +31,11 @@
.page-finish .title {
color: $palette-theme-dark-foreground;
font-weight: bold;
}
.page-finish .huge-title {
font-size: 3.5em;
}
.page-finish .label {
@ -48,3 +57,69 @@
padding: 0px;
min-width: 2px;
}
.page-finish .center {
display: flex;
align-items: center;
justify-content: center;
}
.page-finish .box > div {
margin-bottom: 20px;
> button {
margin-right: 20px;
}
}
.page-finish webview {
width: 800px;
height: 300px;
position: absolute;
top: 80px;
left: 0;
z-index: 9001;
}
.page-finish .fallback-banner {
padding-top: 35px;
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
height: 300px;
position: absolute;
top: 125px;
left: 0;
background-color: $palette-theme-dark-background;
color: $palette-theme-dark-foreground;
font-weight: 300;
.caption {
display: flex;
}
.caption-big {
font-size: 30px;
}
.caption-small {
font-size: 20px;
padding: 15px;
}
.footer-right {
padding: 0;
border-bottom: 1px dashed;
}
.svg-icon {
margin-left: 0.3em;
margin-right: 0.3em
}
.section-footer {
border-top: none;
height: 39px;
}
}

View File

@ -1,35 +1,42 @@
<div class="page-finish row around-xs">
<div class="col-xs">
<div class="box text-center">
<h3 class="title"><span class="tick tick--success space-right-tiny"></span> Flash Complete!</h3>
<p class="soft space-vertical-small" ng-show="finish.settings.get('unmountOnSuccess')">Safely ejected and ready for use</p>
<div class="row center-xs space-vertical-medium">
<div class="col-xs-4 space-medium">
<div class="box">
<p class="soft button-label">Would you like to flash the same image?</p>
<button class="button button-primary button-brick" ng-click="finish.restart({ preserveImage: true })">
Use <b>same</b> image
</button>
</div>
</div>
<div class="col-xs separator-xs"></div>
<div class="col-xs-4 space-medium">
<div class="box">
<p class="soft button-label">Would you like to flash a different image?</p>
<button class="button button-primary button-brick" ng-click="finish.restart()">
Use <b>different</b> image
</button>
</div>
</div>
<div class="box center">
<div class="col-xs-5">
<h3 class="title"><span class="tick tick--success space-right-tiny"></span> Flash Complete!</h3>
</div>
<span class="label label-big label-inset"
ng-if="finish.checksum">CRC32 CHECKSUM : <b>{{ ::finish.checksum }}</b></span>
<div class="col-xs-4">
<button class="button button-primary button-brick" ng-click="finish.restart({ preserveImage: true })">
<b>Flash Another</b>
</button>
</div>
</div>
<div class="box center">
<div class="fallback-banner">
<div class="caption caption-big">Thanks for using
<svg-icon path="../../../assets/etcher.svg"
width="150px"
height="auto"
os-open-external="https://etcher.io?ref=etcher_offline_banner"></svg-icon>
</div>
<div class="caption caption-small">
made with
<svg-icon path="../../../assets/love.svg"
width="20px"
height="auto"></svg-icon>
by
<svg-icon path="../../../assets/resin.svg"
width="100px"
height="auto"
os-open-external="https://resin.io?ref=etcher"></svg-icon>
</div>
<div class="section-footer">
<span class="caption footer-right"
manifest-bind="version"
os-open-external="https://github.com/resin-io/etcher/blob/master/CHANGELOG.md"></span>
</div>
</div>
</div>
</div>
</div>

View File

@ -86,6 +86,23 @@ body {
}
}
.section-loader {
webview {
flex: 0 1;
height: 0;
width: 0;
}
&.isFinish webview {
flex: initial;
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 255px;
}
}
.wrapper {
height: 100%;
margin: 20px 60px;

160
npm-shrinkwrap.json generated
View File

@ -2,6 +2,51 @@
"name": "etcher",
"version": "1.0.0-rc.5",
"dependencies": {
"@types/angular": {
"version": "1.6.17",
"from": "@types/angular@>=1.6.16 <2.0.0",
"resolved": "https://registry.npmjs.org/@types/angular/-/angular-1.6.17.tgz"
},
"@types/jquery": {
"version": "2.0.43",
"from": "@types/jquery@*",
"resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-2.0.43.tgz"
},
"@types/lodash": {
"version": "4.14.64",
"from": "@types/lodash@*",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.64.tgz"
},
"@types/lodash.assign": {
"version": "4.2.2",
"from": "@types/lodash.assign@>=4.2.2 <5.0.0",
"resolved": "https://registry.npmjs.org/@types/lodash.assign/-/lodash.assign-4.2.2.tgz"
},
"@types/lodash.frompairs": {
"version": "4.0.2",
"from": "@types/lodash.frompairs@>=4.0.2 <5.0.0",
"resolved": "https://registry.npmjs.org/@types/lodash.frompairs/-/lodash.frompairs-4.0.2.tgz"
},
"@types/lodash.mapvalues": {
"version": "4.6.2",
"from": "@types/lodash.mapvalues@>=4.6.2 <5.0.0",
"resolved": "https://registry.npmjs.org/@types/lodash.mapvalues/-/lodash.mapvalues-4.6.2.tgz"
},
"@types/lodash.some": {
"version": "4.6.2",
"from": "@types/lodash.some@>=4.6.2 <5.0.0",
"resolved": "https://registry.npmjs.org/@types/lodash.some/-/lodash.some-4.6.2.tgz"
},
"@types/react": {
"version": "15.0.24",
"from": "@types/react@>=15.0.23 <16.0.0",
"resolved": "https://registry.npmjs.org/@types/react/-/react-15.0.24.tgz"
},
"@types/react-dom": {
"version": "15.5.0",
"from": "@types/react-dom@>=15.5.0 <16.0.0",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-15.5.0.tgz"
},
"abbrev": {
"version": "1.1.0",
"from": "abbrev@>=1.0.0 <2.0.0",
@ -298,8 +343,7 @@
"asap": {
"version": "2.0.5",
"from": "asap@>=2.0.3 <2.1.0",
"resolved": "https://registry.npmjs.org/asap/-/asap-2.0.5.tgz",
"dev": true
"resolved": "https://registry.npmjs.org/asap/-/asap-2.0.5.tgz"
},
"asar": {
"version": "0.10.0",
@ -1935,6 +1979,11 @@
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.4.0.tgz",
"dev": true
},
"encoding": {
"version": "0.1.12",
"from": "encoding@>=0.1.11 <0.2.0",
"resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz"
},
"end-of-stream": {
"version": "1.4.0",
"from": "end-of-stream@>=1.0.0 <2.0.0",
@ -2364,6 +2413,18 @@
"resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
"dev": true
},
"fbjs": {
"version": "0.8.12",
"from": "fbjs@>=0.8.9 <0.9.0",
"resolved": "https://registry.npmjs.org/fbjs/-/fbjs-0.8.12.tgz",
"dependencies": {
"core-js": {
"version": "1.2.7",
"from": "core-js@>=1.0.0 <2.0.0",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-1.2.7.tgz"
}
}
},
"fd-slicer": {
"version": "1.0.1",
"from": "fd-slicer@>=1.0.1 <1.1.0",
@ -3129,8 +3190,7 @@
"iconv-lite": {
"version": "0.4.15",
"from": "iconv-lite@>=0.4.5 <0.5.0",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.15.tgz",
"dev": true
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.15.tgz"
},
"ieee754": {
"version": "1.1.6",
@ -3478,6 +3538,11 @@
"resolved": "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.0.0.tgz",
"dev": true
},
"is-stream": {
"version": "1.1.0",
"from": "is-stream@>=1.0.1 <2.0.0",
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz"
},
"is-typedarray": {
"version": "1.0.0",
"from": "is-typedarray@>=1.0.0 <1.1.0",
@ -3523,6 +3588,11 @@
"resolved": "https://registry.npmjs.org/isobject/-/isobject-0.2.0.tgz",
"dev": true
},
"isomorphic-fetch": {
"version": "2.2.1",
"from": "isomorphic-fetch@>=2.1.1 <3.0.0",
"resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz"
},
"isstream": {
"version": "0.1.2",
"from": "isstream@>=0.1.1 <0.2.0",
@ -3800,6 +3870,11 @@
"resolved": "https://registry.npmjs.org/lodash.foreach/-/lodash.foreach-4.5.0.tgz",
"dev": true
},
"lodash.frompairs": {
"version": "4.0.1",
"from": "lodash.frompairs@>=4.0.1 <5.0.0",
"resolved": "https://registry.npmjs.org/lodash.frompairs/-/lodash.frompairs-4.0.1.tgz"
},
"lodash.get": {
"version": "4.4.2",
"from": "lodash.get@>=4.0.0 <5.0.0",
@ -3841,6 +3916,11 @@
"from": "lodash.keys@>=4.0.0 <5.0.0",
"resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-4.0.7.tgz"
},
"lodash.mapvalues": {
"version": "4.6.0",
"from": "lodash.mapvalues@>=4.6.0 <5.0.0",
"resolved": "https://registry.npmjs.org/lodash.mapvalues/-/lodash.mapvalues-4.6.0.tgz"
},
"lodash.memoize": {
"version": "3.0.4",
"from": "lodash.memoize@>=3.0.3 <3.1.0",
@ -3858,6 +3938,11 @@
"from": "lodash.rest@>=4.0.0 <5.0.0",
"resolved": "https://registry.npmjs.org/lodash.rest/-/lodash.rest-4.0.3.tgz"
},
"lodash.some": {
"version": "4.6.0",
"from": "lodash.some@>=4.6.0 <5.0.0",
"resolved": "https://registry.npmjs.org/lodash.some/-/lodash.some-4.6.0.tgz"
},
"lodash.template": {
"version": "4.4.0",
"from": "lodash.template@>=4.2.2 <5.0.0",
@ -4947,6 +5032,18 @@
"resolved": "https://registry.npmjs.org/netmask/-/netmask-1.0.6.tgz",
"dev": true
},
"ngcomponent": {
"version": "3.0.1",
"from": "ngcomponent@>=3.0.1 <4.0.0",
"resolved": "https://registry.npmjs.org/ngcomponent/-/ngcomponent-3.0.1.tgz",
"dependencies": {
"lodash.assign": {
"version": "4.2.0",
"from": "lodash.assign@>=4.2.0 <5.0.0",
"resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-4.2.0.tgz"
}
}
},
"nock": {
"version": "9.0.9",
"from": "nock@9.0.9",
@ -4972,6 +5069,11 @@
"from": "node-cmd@>=1.1.1",
"resolved": "https://registry.npmjs.org/node-cmd/-/node-cmd-1.1.1.tgz"
},
"node-fetch": {
"version": "1.6.3",
"from": "node-fetch@>=1.0.1 <2.0.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.6.3.tgz"
},
"node-gyp": {
"version": "3.5.0",
"from": "node-gyp@>=3.3.1 <4.0.0",
@ -5530,8 +5632,7 @@
"promise": {
"version": "7.1.1",
"from": "promise@>=7.1.1 <7.2.0",
"resolved": "https://registry.npmjs.org/promise/-/promise-7.1.1.tgz",
"dev": true
"resolved": "https://registry.npmjs.org/promise/-/promise-7.1.1.tgz"
},
"prompt": {
"version": "1.0.0",
@ -5539,6 +5640,23 @@
"resolved": "https://registry.npmjs.org/prompt/-/prompt-1.0.0.tgz",
"dev": true
},
"prop-types": {
"version": "15.5.9",
"from": "prop-types@>=15.5.4 <16.0.0",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.5.9.tgz",
"dependencies": {
"js-tokens": {
"version": "3.0.1",
"from": "js-tokens@>=3.0.0 <4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.1.tgz"
},
"loose-envify": {
"version": "1.3.1",
"from": "loose-envify@>=1.3.1 <2.0.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.3.1.tgz"
}
}
},
"propagate": {
"version": "0.4.0",
"from": "propagate@0.4.0",
@ -5640,6 +5758,21 @@
"resolved": "https://registry.npmjs.org/rcedit/-/rcedit-0.4.0.tgz",
"dev": true
},
"react": {
"version": "15.5.4",
"from": "react@15.5.4",
"resolved": "https://registry.npmjs.org/react/-/react-15.5.4.tgz"
},
"react-dom": {
"version": "15.5.4",
"from": "react-dom@15.5.4",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-15.5.4.tgz"
},
"react2angular": {
"version": "1.1.3",
"from": "react2angular@1.1.3",
"resolved": "https://registry.npmjs.org/react2angular/-/react2angular-1.1.3.tgz"
},
"read": {
"version": "1.0.7",
"from": "read@>=1.0.0 <1.1.0",
@ -6128,6 +6261,11 @@
"resolved": "https://registry.npmjs.org/set-getter/-/set-getter-0.1.0.tgz",
"dev": true
},
"setimmediate": {
"version": "1.0.5",
"from": "setimmediate@>=1.0.5 <2.0.0",
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz"
},
"sha.js": {
"version": "2.4.8",
"from": "sha.js@>=2.3.6 <3.0.0",
@ -6828,6 +6966,11 @@
"resolved": "https://registry.npmjs.org/typical/-/typical-2.6.0.tgz",
"dev": true
},
"ua-parser-js": {
"version": "0.7.12",
"from": "ua-parser-js@>=0.7.9 <0.8.0",
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.12.tgz"
},
"udif": {
"version": "0.9.0",
"from": "udif@latest",
@ -7083,6 +7226,11 @@
"from": "wcwidth@>=1.0.0 <2.0.0",
"resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz"
},
"whatwg-fetch": {
"version": "2.0.3",
"from": "whatwg-fetch@>=0.10.0",
"resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-2.0.3.tgz"
},
"which": {
"version": "1.2.10",
"from": "which@>=1.2.8 <2.0.0",

View File

@ -94,6 +94,10 @@
"node-ipc": "^8.9.2",
"node-stream-zip": "^1.3.4",
"path-is-inside": "^1.0.2",
"prop-types": "^15.5.4",
"react": "15.5.4",
"react-dom": "15.5.4",
"react2angular": "1.1.3",
"redux": "^3.5.2",
"redux-localstorage": "^0.4.1",
"request": "^2.81.0",