OSDN Git Service

cp code from chain
authorYongfeng LI <wliyongfeng@gmail.com>
Tue, 26 Dec 2017 08:02:35 +0000 (16:02 +0800)
committerYongfeng LI <wliyongfeng@gmail.com>
Tue, 26 Dec 2017 08:02:35 +0000 (16:02 +0800)
294 files changed:
.babelrc [new file with mode: 0644]
.bootstraprc [new file with mode: 0644]
.eslintignore [new file with mode: 0644]
.eslintrc.json [new file with mode: 0644]
.gitignore [new file with mode: 0644]
README.md [new file with mode: 0644]
bin/dependencies.js [new file with mode: 0755]
bin/generate.js [new file with mode: 0755]
package-lock.json [new file with mode: 0644]
package.json [new file with mode: 0644]
src/Root.jsx [new file with mode: 0644]
src/actions.js [new file with mode: 0644]
src/app.js [new file with mode: 0644]
src/configureStore.js [new file with mode: 0644]
src/features/accessControl/actions.js [new file with mode: 0644]
src/features/accessControl/components/AccessControlList.jsx [new file with mode: 0644]
src/features/accessControl/components/AccessControlList.scss [new file with mode: 0644]
src/features/accessControl/components/EditPolicies.jsx [new file with mode: 0644]
src/features/accessControl/components/EditPolicies.scss [new file with mode: 0644]
src/features/accessControl/components/GrantListItem.jsx [new file with mode: 0644]
src/features/accessControl/components/NewCertificate.jsx [new file with mode: 0644]
src/features/accessControl/components/NewCertificate.scss [new file with mode: 0644]
src/features/accessControl/components/NewToken.jsx [new file with mode: 0644]
src/features/accessControl/components/TokenCreateModal.jsx [new file with mode: 0644]
src/features/accessControl/constants.js [new file with mode: 0644]
src/features/accessControl/reducers.js [new file with mode: 0644]
src/features/accessControl/routes.js [new file with mode: 0644]
src/features/accessControl/selectors.js [new file with mode: 0644]
src/features/accounts/actions.js [new file with mode: 0644]
src/features/accounts/components/AccountShow.jsx [new file with mode: 0644]
src/features/accounts/components/AccountUpdate.jsx [new file with mode: 0644]
src/features/accounts/components/List.jsx [new file with mode: 0644]
src/features/accounts/components/ListItem.jsx [new file with mode: 0644]
src/features/accounts/components/New.jsx [new file with mode: 0644]
src/features/accounts/components/index.js [new file with mode: 0644]
src/features/accounts/index.js [new file with mode: 0644]
src/features/accounts/reducers.js [new file with mode: 0644]
src/features/accounts/routes.js [new file with mode: 0644]
src/features/app/actions.js [new file with mode: 0644]
src/features/app/components/Config/Config.jsx [new file with mode: 0644]
src/features/app/components/Container.jsx [new file with mode: 0644]
src/features/app/components/Loading/Loading.jsx [new file with mode: 0644]
src/features/app/components/Loading/Loading.scss [new file with mode: 0644]
src/features/app/components/Login/Login.jsx [new file with mode: 0644]
src/features/app/components/Login/Login.scss [new file with mode: 0644]
src/features/app/components/Main/Main.jsx [new file with mode: 0644]
src/features/app/components/Main/Main.scss [new file with mode: 0644]
src/features/app/components/Modal/Modal.jsx [new file with mode: 0644]
src/features/app/components/Modal/Modal.scss [new file with mode: 0644]
src/features/app/components/Navigation/Navigation.jsx [new file with mode: 0644]
src/features/app/components/Navigation/Navigation.scss [new file with mode: 0644]
src/features/app/components/SecondaryNavigation/SecondaryNavigation.jsx [new file with mode: 0644]
src/features/app/components/SecondaryNavigation/SecondaryNavigation.scss [new file with mode: 0644]
src/features/app/components/Sync/Sync.jsx [new file with mode: 0644]
src/features/app/components/Sync/Sync.scss [new file with mode: 0644]
src/features/app/components/index.js [new file with mode: 0644]
src/features/app/index.js [new file with mode: 0644]
src/features/app/reducers.js [new file with mode: 0644]
src/features/app/utils.jsx [new file with mode: 0644]
src/features/assets/actions.js [new file with mode: 0644]
src/features/assets/components/AssetShow.jsx [new file with mode: 0644]
src/features/assets/components/AssetUpdate.jsx [new file with mode: 0644]
src/features/assets/components/List.jsx [new file with mode: 0644]
src/features/assets/components/ListItem.jsx [new file with mode: 0644]
src/features/assets/components/New.jsx [new file with mode: 0644]
src/features/assets/components/index.js [new file with mode: 0644]
src/features/assets/index.js [new file with mode: 0644]
src/features/assets/reducers.js [new file with mode: 0644]
src/features/assets/routes.js [new file with mode: 0644]
src/features/balances/actions.js [new file with mode: 0644]
src/features/balances/components/List.jsx [new file with mode: 0644]
src/features/balances/components/ListItem.jsx [new file with mode: 0644]
src/features/balances/components/index.js [new file with mode: 0644]
src/features/balances/index.js [new file with mode: 0644]
src/features/balances/reducers.js [new file with mode: 0644]
src/features/balances/routes.js [new file with mode: 0644]
src/features/configuration/actions.js [new file with mode: 0644]
src/features/configuration/components/Index/Index.jsx [new file with mode: 0644]
src/features/configuration/components/Index/Index.scss [new file with mode: 0644]
src/features/configuration/components/index.js [new file with mode: 0644]
src/features/configuration/index.js [new file with mode: 0644]
src/features/configuration/routes.js [new file with mode: 0644]
src/features/core/actions.js [new file with mode: 0644]
src/features/core/components/CoreIndex/CoreIndex.jsx [new file with mode: 0644]
src/features/core/components/CoreIndex/CoreIndex.scss [new file with mode: 0644]
src/features/core/components/index.js [new file with mode: 0644]
src/features/core/index.js [new file with mode: 0644]
src/features/core/reducers.js [new file with mode: 0644]
src/features/core/routes.js [new file with mode: 0644]
src/features/mockhsm/actions.js [new file with mode: 0644]
src/features/mockhsm/components/List.jsx [new file with mode: 0644]
src/features/mockhsm/components/ListItem.jsx [new file with mode: 0644]
src/features/mockhsm/components/New.jsx [new file with mode: 0644]
src/features/mockhsm/components/index.js [new file with mode: 0644]
src/features/mockhsm/index.js [new file with mode: 0644]
src/features/mockhsm/reducers.js [new file with mode: 0644]
src/features/mockhsm/routes.js [new file with mode: 0644]
src/features/shared/actions/create.js [new file with mode: 0644]
src/features/shared/actions/index.js [new file with mode: 0644]
src/features/shared/actions/list.js [new file with mode: 0644]
src/features/shared/actions/update.js [new file with mode: 0644]
src/features/shared/components/Autocomplete/AccountAlias.jsx [new file with mode: 0644]
src/features/shared/components/Autocomplete/AssetAlias.jsx [new file with mode: 0644]
src/features/shared/components/Autocomplete/AutocompleteField.jsx [new file with mode: 0644]
src/features/shared/components/Autocomplete/AutocompleteField.scss [new file with mode: 0644]
src/features/shared/components/Autocomplete/index.js [new file with mode: 0644]
src/features/shared/components/BaseList/BaseList.jsx [new file with mode: 0644]
src/features/shared/components/BaseList/EmptyList.jsx [new file with mode: 0644]
src/features/shared/components/BaseList/EmptyList.scss [new file with mode: 0644]
src/features/shared/components/BaseNew.jsx [new file with mode: 0644]
src/features/shared/components/BaseShow.jsx [new file with mode: 0644]
src/features/shared/components/BaseUpdate.jsx [new file with mode: 0644]
src/features/shared/components/CheckboxField/CheckboxField.jsx [new file with mode: 0644]
src/features/shared/components/CheckboxField/CheckboxField.scss [new file with mode: 0644]
src/features/shared/components/CopyableBlock/CopyableBlock.jsx [new file with mode: 0644]
src/features/shared/components/CopyableBlock/CopyableBlock.scss [new file with mode: 0644]
src/features/shared/components/EmptyContent/EmptyContent.jsx [new file with mode: 0644]
src/features/shared/components/EmptyContent/EmptyContent.scss [new file with mode: 0644]
src/features/shared/components/ErrorBanner/ErrorBanner.jsx [new file with mode: 0644]
src/features/shared/components/ErrorBanner/ErrorBanner.scss [new file with mode: 0644]
src/features/shared/components/FieldLabel/FieldLabel.jsx [new file with mode: 0644]
src/features/shared/components/FieldLabel/FieldLabel.scss [new file with mode: 0644]
src/features/shared/components/Flash/Flash.jsx [new file with mode: 0644]
src/features/shared/components/Flash/Flash.scss [new file with mode: 0644]
src/features/shared/components/FormContainer/FormContainer.jsx [new file with mode: 0644]
src/features/shared/components/FormContainer/FormContainer.scss [new file with mode: 0644]
src/features/shared/components/FormSection/FormSection.jsx [new file with mode: 0644]
src/features/shared/components/FormSection/FormSection.scss [new file with mode: 0644]
src/features/shared/components/HiddenField.jsx [new file with mode: 0644]
src/features/shared/components/JsonField/JsonField.jsx [new file with mode: 0644]
src/features/shared/components/JsonField/JsonField.scss [new file with mode: 0644]
src/features/shared/components/KeyConfiguration.jsx [new file with mode: 0644]
src/features/shared/components/KeyValueTable/KeyValueTable.jsx [new file with mode: 0644]
src/features/shared/components/KeyValueTable/KeyValueTable.scss [new file with mode: 0644]
src/features/shared/components/NotFound.jsx [new file with mode: 0644]
src/features/shared/components/ObjectSelectorField/ObjectSelectorField.jsx [new file with mode: 0644]
src/features/shared/components/ObjectSelectorField/ObjectSelectorField.scss [new file with mode: 0644]
src/features/shared/components/PageContent/PageContent.jsx [new file with mode: 0644]
src/features/shared/components/PageContent/PageContent.scss [new file with mode: 0644]
src/features/shared/components/PageTitle/PageTitle.jsx [new file with mode: 0644]
src/features/shared/components/PageTitle/PageTitle.scss [new file with mode: 0644]
src/features/shared/components/Pagination/Pagination.jsx [new file with mode: 0644]
src/features/shared/components/Pagination/Pagination.scss [new file with mode: 0644]
src/features/shared/components/RawJsonButton.jsx [new file with mode: 0644]
src/features/shared/components/RelativeTime.jsx [new file with mode: 0644]
src/features/shared/components/RoutingContainer.jsx [new file with mode: 0644]
src/features/shared/components/SearchBar/SearchBar.jsx [new file with mode: 0644]
src/features/shared/components/SearchBar/SearchBar.scss [new file with mode: 0644]
src/features/shared/components/Section/Section.jsx [new file with mode: 0644]
src/features/shared/components/Section/Section.scss [new file with mode: 0644]
src/features/shared/components/SelectField.jsx [new file with mode: 0644]
src/features/shared/components/SubmitIndicator/SubmitIndicator.jsx [new file with mode: 0644]
src/features/shared/components/SubmitIndicator/SubmitIndicator.scss [new file with mode: 0644]
src/features/shared/components/TableList/TableList.jsx [new file with mode: 0644]
src/features/shared/components/TableList/TableList.scss [new file with mode: 0644]
src/features/shared/components/TextField.jsx [new file with mode: 0644]
src/features/shared/components/XpubField/XpubField.jsx [new file with mode: 0644]
src/features/shared/components/XpubField/XpubField.scss [new file with mode: 0644]
src/features/shared/components/index.js [new file with mode: 0644]
src/features/shared/index.js [new file with mode: 0644]
src/features/shared/reducers.js [new file with mode: 0644]
src/features/shared/routes.js [new file with mode: 0644]
src/features/testnet/actions.js [new file with mode: 0644]
src/features/testnet/index.js [new file with mode: 0644]
src/features/testnet/reducers.js [new file with mode: 0644]
src/features/testnet/utils.js [new file with mode: 0644]
src/features/transactionFeeds/actions.js [new file with mode: 0644]
src/features/transactionFeeds/components/List.jsx [new file with mode: 0644]
src/features/transactionFeeds/components/ListItem.jsx [new file with mode: 0644]
src/features/transactionFeeds/components/New.jsx [new file with mode: 0644]
src/features/transactionFeeds/components/index.js [new file with mode: 0644]
src/features/transactionFeeds/index.js [new file with mode: 0644]
src/features/transactionFeeds/reducers.js [new file with mode: 0644]
src/features/transactionFeeds/routes.js [new file with mode: 0644]
src/features/transactions/actions.js [new file with mode: 0644]
src/features/transactions/components/GeneratedTxHex/GeneratedTxHex.jsx [new file with mode: 0644]
src/features/transactions/components/GeneratedTxHex/GeneratedTxHex.scss [new file with mode: 0644]
src/features/transactions/components/List.jsx [new file with mode: 0644]
src/features/transactions/components/ListItem/ListItem.jsx [new file with mode: 0644]
src/features/transactions/components/ListItem/ListItem.scss [new file with mode: 0644]
src/features/transactions/components/New/FormActionItem.jsx [new file with mode: 0644]
src/features/transactions/components/New/FormActionItem.scss [new file with mode: 0644]
src/features/transactions/components/New/New.jsx [new file with mode: 0644]
src/features/transactions/components/New/New.scss [new file with mode: 0644]
src/features/transactions/components/Show.jsx [new file with mode: 0644]
src/features/transactions/components/Summary/Summary.jsx [new file with mode: 0644]
src/features/transactions/components/Summary/Summary.scss [new file with mode: 0644]
src/features/transactions/components/index.js [new file with mode: 0644]
src/features/transactions/index.js [new file with mode: 0644]
src/features/transactions/reducers.js [new file with mode: 0644]
src/features/transactions/routes.js [new file with mode: 0644]
src/features/tutorial/actions.js [new file with mode: 0644]
src/features/tutorial/components/Tutorial.jsx [new file with mode: 0644]
src/features/tutorial/components/TutorialForm/TutorialForm.jsx [new file with mode: 0644]
src/features/tutorial/components/TutorialForm/TutorialForm.scss [new file with mode: 0644]
src/features/tutorial/components/TutorialHeader/TutorialHeader.jsx [new file with mode: 0644]
src/features/tutorial/components/TutorialHeader/TutorialHeader.scss [new file with mode: 0644]
src/features/tutorial/components/TutorialInfo/TutorialInfo.jsx [new file with mode: 0644]
src/features/tutorial/components/TutorialInfo/TutorialInfo.scss [new file with mode: 0644]
src/features/tutorial/components/TutorialModal/TutorialModal.jsx [new file with mode: 0644]
src/features/tutorial/components/TutorialModal/TutorialModal.scss [new file with mode: 0644]
src/features/tutorial/index.js [new file with mode: 0644]
src/features/tutorial/reducers.js [new file with mode: 0644]
src/features/tutorial/steps.json [new file with mode: 0644]
src/features/unspents/actions.js [new file with mode: 0644]
src/features/unspents/components/List.jsx [new file with mode: 0644]
src/features/unspents/components/ListItem.jsx [new file with mode: 0644]
src/features/unspents/components/index.js [new file with mode: 0644]
src/features/unspents/index.js [new file with mode: 0644]
src/features/unspents/reducers.js [new file with mode: 0644]
src/features/unspents/routes.js [new file with mode: 0644]
src/reducers.js [new file with mode: 0644]
src/routes.js [new file with mode: 0644]
src/utility/buildInOutDisplay.js [new file with mode: 0644]
src/utility/clipboard.js [new file with mode: 0644]
src/utility/componentClassNames.js [new file with mode: 0644]
src/utility/disableAutocomplete.js [new file with mode: 0644]
src/utility/environment.js [new file with mode: 0644]
src/utility/localStorage.js [new file with mode: 0644]
src/utility/string.js [new file with mode: 0644]
src/utility/time.js [new file with mode: 0644]
static/.DS_Store [new file with mode: 0644]
static/fonts/nitti-normal.woff [new file with mode: 0644]
static/fonts/nittigrotesk-medium.woff [new file with mode: 0644]
static/fonts/nittigrotesk-normal.woff [new file with mode: 0644]
static/images/chain-favicon.png [new file with mode: 0644]
static/images/chevron-blue.png [new file with mode: 0644]
static/images/chevron-green.png [new file with mode: 0644]
static/images/chevron.png [new file with mode: 0644]
static/images/config/join-active.png [new file with mode: 0644]
static/images/config/join.png [new file with mode: 0644]
static/images/config/new-active.png [new file with mode: 0644]
static/images/config/new.png [new file with mode: 0644]
static/images/config/testnet-active.png [new file with mode: 0644]
static/images/config/testnet.png [new file with mode: 0644]
static/images/empty/account.svg [new file with mode: 0644]
static/images/empty/asset.svg [new file with mode: 0644]
static/images/empty/balance.svg [new file with mode: 0644]
static/images/empty/client_access_token.svg [new file with mode: 0644]
static/images/empty/mockhsm.svg [new file with mode: 0644]
static/images/empty/network_access_token.svg [new file with mode: 0644]
static/images/empty/transaction.svg [new file with mode: 0644]
static/images/empty/transactionFeed.svg [new file with mode: 0644]
static/images/empty/unspent.svg [new file with mode: 0644]
static/images/favicon.png [new file with mode: 0644]
static/images/logo-bytom-white.png [new file with mode: 0644]
static/images/logo-shadowed.png [new file with mode: 0644]
static/images/logo-white.png [new file with mode: 0644]
static/images/navigation/account-active.png [new file with mode: 0644]
static/images/navigation/account.png [new file with mode: 0644]
static/images/navigation/asset-active.png [new file with mode: 0644]
static/images/navigation/asset.png [new file with mode: 0644]
static/images/navigation/balance-active.png [new file with mode: 0644]
static/images/navigation/balance.png [new file with mode: 0644]
static/images/navigation/client-active.png [new file with mode: 0644]
static/images/navigation/client.png [new file with mode: 0644]
static/images/navigation/core-active.png [new file with mode: 0644]
static/images/navigation/core.png [new file with mode: 0644]
static/images/navigation/docs.png [new file with mode: 0644]
static/images/navigation/error.png [new file with mode: 0644]
static/images/navigation/feed-active.png [new file with mode: 0644]
static/images/navigation/feed.png [new file with mode: 0644]
static/images/navigation/help.png [new file with mode: 0644]
static/images/navigation/logout.png [new file with mode: 0644]
static/images/navigation/mockhsm-active.png [new file with mode: 0644]
static/images/navigation/mockhsm.png [new file with mode: 0644]
static/images/navigation/network-active.png [new file with mode: 0644]
static/images/navigation/network.png [new file with mode: 0644]
static/images/navigation/settings.png [new file with mode: 0644]
static/images/navigation/transaction-active.png [new file with mode: 0644]
static/images/navigation/transaction.png [new file with mode: 0644]
static/images/navigation/tutorial-active.png [new file with mode: 0644]
static/images/navigation/tutorial.png [new file with mode: 0644]
static/images/navigation/unspent-active.png [new file with mode: 0644]
static/images/navigation/unspent.png [new file with mode: 0644]
static/images/search.png [new file with mode: 0644]
static/images/sum.png [new file with mode: 0644]
static/styles/_body.scss [new file with mode: 0644]
static/styles/_bootstrap-overrides.scss [new file with mode: 0644]
static/styles/app.scss [new file with mode: 0644]
static/styles/resources.scss [new file with mode: 0644]
test/.eslintrc.json [new file with mode: 0644]
test/conf/wdio.browserstack.js [new file with mode: 0644]
test/conf/wdio.local.js [new file with mode: 0644]
test/helpers.js [new file with mode: 0644]
test/specs/accounts.js [new file with mode: 0644]
test/specs/app.js [new file with mode: 0644]
test/specs/assets.js [new file with mode: 0644]
test/specs/extended/empty.js [new file with mode: 0644]
test/specs/keys.js [new file with mode: 0644]
test/specs/transactions.js [new file with mode: 0644]
webpack/webpack.app.js [new file with mode: 0644]
webpack/webpack.base.js [new file with mode: 0644]
webpack/webpack.dll.js [new file with mode: 0644]

diff --git a/.babelrc b/.babelrc
new file mode 100644 (file)
index 0000000..c91e0a0
--- /dev/null
+++ b/.babelrc
@@ -0,0 +1,9 @@
+{
+  "presets": ["es2015", "react"],
+  "plugins": ["transform-object-rest-spread"],
+  "env": {
+    "development": {
+      "presets": ["react-hmre"]
+    }
+  }
+}
diff --git a/.bootstraprc b/.bootstraprc
new file mode 100644 (file)
index 0000000..30a33bf
--- /dev/null
@@ -0,0 +1,52 @@
+{
+  bootstrapVersion: 3,
+
+  // This gets loaded before bootstrap/variables
+  preBootstrapCustomizations: ./static/styles/_bootstrap-overrides.scss,
+
+  // Imports app styles into project
+  appStyles: ./static/styles/app.scss,
+
+  env: {
+    development: {
+      extractStyles: false
+    },
+    production: {
+      extractStyles: true
+    }
+  },
+
+  styleLoaders: ['style', 'css', 'postcss', 'sass'],
+
+  // Bootstrap styles
+  styles: {
+    mixins: true,
+    normalize: true,
+    glyphicons: true,
+    scaffolding: true,
+    type: true,
+    code: true,
+    grid: true,
+    tables: true,
+    forms: true,
+    buttons: true,
+    alerts: true,
+    labels: true,
+    dropdowns: true,
+    input-groups: true,
+    navs: true,
+    navbar: true,
+    wells: true,
+    jumbotron: true,
+    pagination: true,
+    pager: true,
+    panels: true,
+    close: true,
+    utilities: true,
+    responsive-utilities: true,
+    button-groups: true
+  },
+
+  // Bootstrap scripts
+  scripts: false
+}
diff --git a/.eslintignore b/.eslintignore
new file mode 100644 (file)
index 0000000..56d155a
--- /dev/null
@@ -0,0 +1,7 @@
+**/*.svg
+**/*.png
+**/*.ttf
+**/*.scss
+**/*.woff
+**/*.md
+**/*.json
diff --git a/.eslintrc.json b/.eslintrc.json
new file mode 100644 (file)
index 0000000..7beef12
--- /dev/null
@@ -0,0 +1,28 @@
+{
+  "env": {
+    "browser": true,
+    "es6": true,
+    "commonjs": true
+  },
+  "plugins": [
+    "react"
+  ],
+  "extends": "eslint:recommended",
+  "parserOptions": {
+    "ecmaVersion": 6,
+    "sourceType": "module",
+    "ecmaFeatures": {
+        "jsx": true,
+        "experimentalObjectRestSpread": true
+    }
+  },
+  "rules": {
+    "quotes": ["error", "single", {"avoidEscape": true}],
+    "jsx-quotes": ["error", "prefer-single"],
+    "indent": ["error", 2, {"SwitchCase": 1}],
+    "linebreak-style": ["error", "unix"],
+    "semi": ["error", "never"],
+    "react/jsx-uses-react": "error",
+    "react/jsx-uses-vars": "error"
+  }
+}
diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..2fc8115
--- /dev/null
@@ -0,0 +1,6 @@
+node_modules
+npm-debug.log
+.env
+public
+errorShots
+.idea
diff --git a/README.md b/README.md
new file mode 100644 (file)
index 0000000..2a2e307
--- /dev/null
+++ b/README.md
@@ -0,0 +1,130 @@
+# Chain Core Dashboard
+
+## Development
+
+#### Setup
+
+Install Node.js:
+
+```
+brew install node
+```
+
+Install dependencies:
+
+```
+npm install
+```
+
+Start the development server with
+
+```
+npm start
+```
+
+By default, the development server uses the following environment variables
+with default values to connect to a local Chain Core instance:
+
+```
+API_URL=http://localhost:3000/api
+PROXY_API_HOST=http://localhost:1999
+```
+
+#### Style Guide
+
+We use `eslint` to maintain a consistent code style. To check the source
+directory with `eslint`, run:
+
+```
+npm run lint src
+```
+
+#### Tests
+
+The Chain Core Dashboard has a series of integration tests that can be run
+against a running core. First, start Chain Core and Dashboard on their default
+ports of 1999 and 3000 respective. Then, start tests with the command:
+
+```
+npm test
+```
+
+There an extended test suite that can be run with:
+
+```
+npm run testExtended
+```
+
+(Note: The extended test suite can take significantly longer to run, as test
+  files cannot be run in parallel).
+
+### React + Redux
+
+#### ES6
+
+Babel is used to transpile the latest ES6 syntax into a format understood by
+both Node.js and browsers. To get an ES6-compatible REPL (or run a one-off script)
+you can use the `babel-node` command:
+
+`$(npm bin)/babel-node`
+
+#### Redux Actions
+
+To inspect and debug Redux actions, we recommend the "Redux DevTools" Chrome
+extension:
+
+https://chrome.google.com/webstore/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd
+
+
+#### Creating new components
+
+To generate a new component with a connected stylesheet, use the following
+command:
+
+```
+npm run generate-component Common/MyComponent
+```
+
+The above command will create two new files in the `src/components` directory:
+
+```
+src/components/Common/MyComponent/MyComponent.jsx
+src/components/Common/MyComponent/MyComponent.scss
+```
+
+with `MyComponent.scss` imported as a stylesheet into `MyComponent.jsx`.
+
+Additionally, if there is an `index.js` file in `src/components/Common`, it
+will appropriately add the newly created component to the index exports.
+
+
+## Production
+
+In production environments, Chain Core Dashboard is served from within `cored`. The contents
+of the application are packaged into a single Go source file that maps generated
+filenames to file contents.
+
+To deploy an updated dashboard to production:
+
+1. Package the dashboard in production mode using `webpack` with:
+
+    ```sh
+    $ npm run build
+    ```
+
+2. Bundle the packaged output into an updated `dashboard.go`:
+
+    ```sh
+    $ go install ./cmd/gobundle
+    $ gobundle -package dashboard dashboard/public > generated/dashboard/dashboard.go
+    $ gofmt -w generated/dashboard/dashboard.go
+    ```
+
+3. Commit the resulting `dashboard.go`, then rebuild and start `cored`
+
+    ```sh
+    $ go install ./cmd/cored
+    $ cored
+    ```
+
+    Dashboard will be served at the root path from the `cored` server.
diff --git a/bin/dependencies.js b/bin/dependencies.js
new file mode 100755 (executable)
index 0000000..a369a69
--- /dev/null
@@ -0,0 +1,47 @@
+// Original from https://github.com/mxstbr/react-boilerplate/
+
+/*eslint-env node*/
+
+// No need to build the DLL in production
+if (process.env.NODE_ENV === 'production') {
+  process.exit(0)
+}
+
+require('shelljs/global')
+
+const path = require('path')
+const fs = require('fs')
+const exists = fs.existsSync
+const writeFile = fs.writeFileSync
+
+const pkg = require(path.join(process.cwd(), 'package.json'))
+const outputPath = path.join(process.cwd(), 'node_modules/dashboard-dlls')
+const dllManifestPath = path.join(outputPath, 'package.json')
+
+/**
+ * I use node_modules/react-boilerplate-dlls by default just because
+ * it isn't going to be version controlled and babel wont try to parse it.
+ */
+mkdir('-p', outputPath)
+
+echo('Building the Webpack DLL...')
+
+/**
+ * Create a manifest so npm install doesn't warn us
+ */
+if (!exists(dllManifestPath)) {
+  writeFile(
+    dllManifestPath,
+    JSON.stringify({
+      name: 'react-boilerplate-dlls',
+      private: true,
+      author: pkg.author,
+      repository: pkg.repository,
+      version: pkg.version,
+    }),
+    'utf8'
+  )
+}
+
+// the BUILDING_DLL env var is set to avoid confusing the development environment
+exec('webpack --display-chunks --display-error-details --color --config webpack/webpack.dll.js')
diff --git a/bin/generate.js b/bin/generate.js
new file mode 100755 (executable)
index 0000000..a374b05
--- /dev/null
@@ -0,0 +1,92 @@
+import { cd, mkdir, echo, ShellString, cat, test } from 'shelljs'
+import commandLineCommands from 'command-line-commands'
+import commandLineArgs from 'command-line-args'
+
+const validCommands = [ 'component' ]
+const { command, argv } = commandLineCommands(validCommands)
+
+function template(strings, ...keys) {
+  return (function(...values) {
+    var dict = values[values.length - 1] || {}
+    var result = [strings[0]]
+    keys.forEach(function(key, i) {
+      var value = Number.isInteger(key) ? values[key] : dict[key]
+      result.push(value, strings[i + 1])
+    })
+    return result.join('')
+  })
+}
+
+const baseJsx =
+template`import React from 'react'
+import styles from './${0}.scss'
+
+class ${0} extends React.Component {
+  render() {
+    return (
+      <div className={styles.base}>
+        <p>This is a ${0}</p>
+      </div>
+    )
+  }
+}
+
+export default ${0}
+`
+
+const baseScss =
+`.base {
+  background: red;
+}
+`
+
+switch (command) {
+  case 'component': {
+
+    let optionDefinitions = [
+      { name: 'path', type: String, defaultOption: true }
+    ]
+
+    let options = commandLineArgs(optionDefinitions, argv)
+
+    let path = options.path
+    if (path === undefined) {
+      echo('No component name specified.\nUsage: `npm run generate-component My/Name`')
+      break
+    }
+
+    cd('src')
+    mkdir('-p', path)
+
+    let name = path.split('/').pop()
+    ShellString(baseJsx(name)).to(path + `/${name}.jsx`)
+    ShellString(baseScss).to(path + `/${name}.scss`)
+
+    let indexPath = path.split('/').slice(0,-1).join('/') + '/index.js'
+    if (test('-f', indexPath)) {
+      let contents = cat(indexPath)
+
+      let importString = `import ${name} from './${name}/${name}'`
+      let exportString = `  ${name},\n`
+
+      if (contents.indexOf(importString) < 0) {
+        contents = `${importString}\n${contents}`
+      }
+      if (contents.indexOf(exportString) < 0) {
+        let exportTarget = 'export {\n'
+        let position = contents.indexOf(exportTarget)  + exportTarget.length
+
+        contents = contents.slice(0, position)
+          + exportString
+          + contents.slice(position)
+      }
+
+      ShellString(contents).to(indexPath)
+    }
+    break
+  }
+  default: {
+    echo(`Unknown command: ${command}`)
+    break
+  }
+}
diff --git a/package-lock.json b/package-lock.json
new file mode 100644 (file)
index 0000000..e474428
--- /dev/null
@@ -0,0 +1,8651 @@
+{
+  "name": "dashboard",
+  "version": "1.0.0",
+  "lockfileVersion": 1,
+  "requires": true,
+  "dependencies": {
+    "abbrev": {
+      "version": "1.1.1",
+      "resolved": "http://registry.npm.taobao.org/abbrev/download/abbrev-1.1.1.tgz",
+      "integrity": "sha1-+PLIh60Qv2f2NPAFtph/7TF5qsg=",
+      "dev": true
+    },
+    "accepts": {
+      "version": "1.3.4",
+      "resolved": "http://registry.npm.taobao.org/accepts/download/accepts-1.3.4.tgz",
+      "integrity": "sha1-hiRnWMfdbSGmR0/whKR0DsBesh8=",
+      "dev": true,
+      "requires": {
+        "mime-types": "2.1.17",
+        "negotiator": "0.6.1"
+      }
+    },
+    "acorn": {
+      "version": "5.2.1",
+      "resolved": "http://registry.npm.taobao.org/acorn/download/acorn-5.2.1.tgz",
+      "integrity": "sha1-MXrHghgmwixwLWYYmrg1lnXxNdc=",
+      "dev": true
+    },
+    "acorn-jsx": {
+      "version": "3.0.1",
+      "resolved": "http://registry.npm.taobao.org/acorn-jsx/download/acorn-jsx-3.0.1.tgz",
+      "integrity": "sha1-r9+UiPsezvyDSPb7IvRk4ypYs2s=",
+      "dev": true,
+      "requires": {
+        "acorn": "3.3.0"
+      },
+      "dependencies": {
+        "acorn": {
+          "version": "3.3.0",
+          "resolved": "http://registry.npm.taobao.org/acorn/download/acorn-3.3.0.tgz",
+          "integrity": "sha1-ReN/s56No/JbruP/U2niu18iAXo=",
+          "dev": true
+        }
+      }
+    },
+    "agent-base": {
+      "version": "2.1.1",
+      "resolved": "http://registry.npm.taobao.org/agent-base/download/agent-base-2.1.1.tgz",
+      "integrity": "sha1-1t4Q1a9hMtW9aSQn1G/FOFOQlMc=",
+      "dev": true,
+      "requires": {
+        "extend": "3.0.1",
+        "semver": "5.0.3"
+      },
+      "dependencies": {
+        "semver": {
+          "version": "5.0.3",
+          "resolved": "http://registry.npm.taobao.org/semver/download/semver-5.0.3.tgz",
+          "integrity": "sha1-d0Zt5YnNXTyV8TiqeLxWmjy10no=",
+          "dev": true
+        }
+      }
+    },
+    "ajv": {
+      "version": "5.5.2",
+      "resolved": "http://registry.npm.taobao.org/ajv/download/ajv-5.5.2.tgz",
+      "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=",
+      "dev": true,
+      "requires": {
+        "co": "4.6.0",
+        "fast-deep-equal": "1.0.0",
+        "fast-json-stable-stringify": "2.0.0",
+        "json-schema-traverse": "0.3.1"
+      }
+    },
+    "ajv-keywords": {
+      "version": "1.5.1",
+      "resolved": "http://registry.npm.taobao.org/ajv-keywords/download/ajv-keywords-1.5.1.tgz",
+      "integrity": "sha1-MU3QpLM2j609/NxU7eYXG4htrzw=",
+      "dev": true
+    },
+    "align-text": {
+      "version": "0.1.4",
+      "resolved": "http://registry.npm.taobao.org/align-text/download/align-text-0.1.4.tgz",
+      "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=",
+      "dev": true,
+      "requires": {
+        "kind-of": "3.2.2",
+        "longest": "1.0.1",
+        "repeat-string": "1.6.1"
+      }
+    },
+    "amdefine": {
+      "version": "1.0.1",
+      "resolved": "http://registry.npm.taobao.org/amdefine/download/amdefine-1.0.1.tgz",
+      "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=",
+      "dev": true
+    },
+    "ansi-escapes": {
+      "version": "1.4.0",
+      "resolved": "http://registry.npm.taobao.org/ansi-escapes/download/ansi-escapes-1.4.0.tgz",
+      "integrity": "sha1-06ioOzGapneTZisT52HHkRQiMG4=",
+      "dev": true
+    },
+    "ansi-html": {
+      "version": "0.0.7",
+      "resolved": "http://registry.npm.taobao.org/ansi-html/download/ansi-html-0.0.7.tgz",
+      "integrity": "sha1-gTWEAhliqenm/QOflA0S9WynhZ4=",
+      "dev": true
+    },
+    "ansi-regex": {
+      "version": "2.1.1",
+      "resolved": "http://registry.npm.taobao.org/ansi-regex/download/ansi-regex-2.1.1.tgz",
+      "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8="
+    },
+    "ansi-styles": {
+      "version": "2.2.1",
+      "resolved": "http://registry.npm.taobao.org/ansi-styles/download/ansi-styles-2.2.1.tgz",
+      "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4="
+    },
+    "anymatch": {
+      "version": "1.3.2",
+      "resolved": "http://registry.npm.taobao.org/anymatch/download/anymatch-1.3.2.tgz",
+      "integrity": "sha1-VT3Lj5HjyImEXf26NMd3IbkLnXo=",
+      "dev": true,
+      "requires": {
+        "micromatch": "2.3.11",
+        "normalize-path": "2.1.1"
+      }
+    },
+    "aproba": {
+      "version": "1.2.0",
+      "resolved": "http://registry.npm.taobao.org/aproba/download/aproba-1.2.0.tgz",
+      "integrity": "sha1-aALmJk79GMeQobDVF/DyYnvyyUo=",
+      "dev": true
+    },
+    "archiver": {
+      "version": "1.0.0",
+      "resolved": "http://registry.npm.taobao.org/archiver/download/archiver-1.0.0.tgz",
+      "integrity": "sha1-3h1hCC6Ud1W1mbs7wnEwpMhZ/IM=",
+      "dev": true,
+      "requires": {
+        "archiver-utils": "1.3.0",
+        "async": "1.5.2",
+        "buffer-crc32": "0.2.13",
+        "glob": "7.1.2",
+        "lodash": "4.17.4",
+        "readable-stream": "2.3.3",
+        "tar-stream": "1.5.2",
+        "zip-stream": "1.2.0"
+      },
+      "dependencies": {
+        "glob": {
+          "version": "7.1.2",
+          "resolved": "http://registry.npm.taobao.org/glob/download/glob-7.1.2.tgz",
+          "integrity": "sha1-wZyd+aAocC1nhhI4SmVSQExjbRU=",
+          "dev": true,
+          "requires": {
+            "fs.realpath": "1.0.0",
+            "inflight": "1.0.6",
+            "inherits": "2.0.3",
+            "minimatch": "3.0.4",
+            "once": "1.4.0",
+            "path-is-absolute": "1.0.1"
+          }
+        }
+      }
+    },
+    "archiver-utils": {
+      "version": "1.3.0",
+      "resolved": "http://registry.npm.taobao.org/archiver-utils/download/archiver-utils-1.3.0.tgz",
+      "integrity": "sha1-5QtMCccL89aA4y/xt5lOn52JUXQ=",
+      "dev": true,
+      "requires": {
+        "glob": "7.1.2",
+        "graceful-fs": "4.1.11",
+        "lazystream": "1.0.0",
+        "lodash": "4.17.4",
+        "normalize-path": "2.1.1",
+        "readable-stream": "2.3.3"
+      },
+      "dependencies": {
+        "glob": {
+          "version": "7.1.2",
+          "resolved": "http://registry.npm.taobao.org/glob/download/glob-7.1.2.tgz",
+          "integrity": "sha1-wZyd+aAocC1nhhI4SmVSQExjbRU=",
+          "dev": true,
+          "requires": {
+            "fs.realpath": "1.0.0",
+            "inflight": "1.0.6",
+            "inherits": "2.0.3",
+            "minimatch": "3.0.4",
+            "once": "1.4.0",
+            "path-is-absolute": "1.0.1"
+          }
+        }
+      }
+    },
+    "are-we-there-yet": {
+      "version": "1.1.4",
+      "resolved": "http://registry.npm.taobao.org/are-we-there-yet/download/are-we-there-yet-1.1.4.tgz",
+      "integrity": "sha1-u13KOCu5TwXhUZQ3PRb9O6HKEQ0=",
+      "dev": true,
+      "requires": {
+        "delegates": "1.0.0",
+        "readable-stream": "2.3.3"
+      }
+    },
+    "argparse": {
+      "version": "1.0.9",
+      "resolved": "http://registry.npm.taobao.org/argparse/download/argparse-1.0.9.tgz",
+      "integrity": "sha1-c9g7wmP4bpf4zE9rrhsOkKfSLIY=",
+      "dev": true,
+      "requires": {
+        "sprintf-js": "1.0.3"
+      }
+    },
+    "arr-diff": {
+      "version": "2.0.0",
+      "resolved": "http://registry.npm.taobao.org/arr-diff/download/arr-diff-2.0.0.tgz",
+      "integrity": "sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8=",
+      "dev": true,
+      "requires": {
+        "arr-flatten": "1.1.0"
+      }
+    },
+    "arr-flatten": {
+      "version": "1.1.0",
+      "resolved": "http://registry.npm.taobao.org/arr-flatten/download/arr-flatten-1.1.0.tgz",
+      "integrity": "sha1-NgSLv/TntH4TZkQxbJlmnqWukfE=",
+      "dev": true
+    },
+    "array-back": {
+      "version": "1.0.4",
+      "resolved": "http://registry.npm.taobao.org/array-back/download/array-back-1.0.4.tgz",
+      "integrity": "sha1-ZEun8JX3/898Q7Xw3DnTwfA8Bjs=",
+      "dev": true,
+      "requires": {
+        "typical": "2.6.1"
+      }
+    },
+    "array-find-index": {
+      "version": "1.0.2",
+      "resolved": "http://registry.npm.taobao.org/array-find-index/download/array-find-index-1.0.2.tgz",
+      "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=",
+      "dev": true
+    },
+    "array-flatten": {
+      "version": "1.1.1",
+      "resolved": "http://registry.npm.taobao.org/array-flatten/download/array-flatten-1.1.1.tgz",
+      "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=",
+      "dev": true
+    },
+    "array-union": {
+      "version": "1.0.2",
+      "resolved": "http://registry.npm.taobao.org/array-union/download/array-union-1.0.2.tgz",
+      "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=",
+      "dev": true,
+      "requires": {
+        "array-uniq": "1.0.3"
+      }
+    },
+    "array-uniq": {
+      "version": "1.0.3",
+      "resolved": "http://registry.npm.taobao.org/array-uniq/download/array-uniq-1.0.3.tgz",
+      "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=",
+      "dev": true
+    },
+    "array-unique": {
+      "version": "0.2.1",
+      "resolved": "http://registry.npm.taobao.org/array-unique/download/array-unique-0.2.1.tgz",
+      "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=",
+      "dev": true
+    },
+    "arrify": {
+      "version": "1.0.1",
+      "resolved": "http://registry.npm.taobao.org/arrify/download/arrify-1.0.1.tgz",
+      "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=",
+      "dev": true
+    },
+    "asap": {
+      "version": "2.0.6",
+      "resolved": "http://registry.npm.taobao.org/asap/download/asap-2.0.6.tgz",
+      "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY="
+    },
+    "asn1": {
+      "version": "0.2.3",
+      "resolved": "http://registry.npm.taobao.org/asn1/download/asn1-0.2.3.tgz",
+      "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=",
+      "dev": true
+    },
+    "assert": {
+      "version": "1.4.1",
+      "resolved": "http://registry.npm.taobao.org/assert/download/assert-1.4.1.tgz",
+      "integrity": "sha1-mZEtWRg2tab1s0XA8H7vwI/GXZE=",
+      "dev": true,
+      "requires": {
+        "util": "0.10.3"
+      }
+    },
+    "assert-plus": {
+      "version": "1.0.0",
+      "resolved": "http://registry.npm.taobao.org/assert-plus/download/assert-plus-1.0.0.tgz",
+      "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=",
+      "dev": true
+    },
+    "assertion-error": {
+      "version": "1.0.2",
+      "resolved": "http://registry.npm.taobao.org/assertion-error/download/assertion-error-1.0.2.tgz",
+      "integrity": "sha1-E8pRXYYgbaC6xm6DTdOX2HWBCUw=",
+      "dev": true
+    },
+    "async": {
+      "version": "1.5.2",
+      "resolved": "http://registry.npm.taobao.org/async/download/async-1.5.2.tgz",
+      "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=",
+      "dev": true
+    },
+    "async-each": {
+      "version": "1.0.1",
+      "resolved": "http://registry.npm.taobao.org/async-each/download/async-each-1.0.1.tgz",
+      "integrity": "sha1-GdOGodntxufByF04iu28xW0zYC0=",
+      "dev": true
+    },
+    "async-foreach": {
+      "version": "0.1.3",
+      "resolved": "http://registry.npm.taobao.org/async-foreach/download/async-foreach-0.1.3.tgz",
+      "integrity": "sha1-NhIfhFwFeBct5Bmpfb6x0W7DRUI=",
+      "dev": true
+    },
+    "asynckit": {
+      "version": "0.4.0",
+      "resolved": "http://registry.npm.taobao.org/asynckit/download/asynckit-0.4.0.tgz",
+      "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=",
+      "dev": true
+    },
+    "atob": {
+      "version": "1.1.3",
+      "resolved": "http://registry.npm.taobao.org/atob/download/atob-1.1.3.tgz",
+      "integrity": "sha1-lfE2KbEsOlGl0hWr3OKqnzL4B3M=",
+      "dev": true
+    },
+    "autoprefixer": {
+      "version": "6.7.7",
+      "resolved": "http://registry.npm.taobao.org/autoprefixer/download/autoprefixer-6.7.7.tgz",
+      "integrity": "sha1-Hb0cg1ZY41zj+ZhAmdsAWFx4IBQ=",
+      "dev": true,
+      "requires": {
+        "browserslist": "1.7.7",
+        "caniuse-db": "1.0.30000784",
+        "normalize-range": "0.1.2",
+        "num2fraction": "1.2.2",
+        "postcss": "5.2.18",
+        "postcss-value-parser": "3.3.0"
+      }
+    },
+    "aws-sign2": {
+      "version": "0.7.0",
+      "resolved": "http://registry.npm.taobao.org/aws-sign2/download/aws-sign2-0.7.0.tgz",
+      "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=",
+      "dev": true
+    },
+    "aws4": {
+      "version": "1.6.0",
+      "resolved": "http://registry.npm.taobao.org/aws4/download/aws4-1.6.0.tgz",
+      "integrity": "sha1-g+9cqGCysy5KDe7e6MdxudtXRx4=",
+      "dev": true
+    },
+    "babel-cli": {
+      "version": "6.14.0",
+      "resolved": "http://registry.npm.taobao.org/babel-cli/download/babel-cli-6.14.0.tgz",
+      "integrity": "sha1-y8d4rR/05YyHuH1+CJk5k8LTt18=",
+      "dev": true,
+      "requires": {
+        "babel-core": "6.26.0",
+        "babel-polyfill": "6.16.0",
+        "babel-register": "6.22.0",
+        "babel-runtime": "6.26.0",
+        "bin-version-check": "2.1.0",
+        "chalk": "1.1.1",
+        "chokidar": "1.7.0",
+        "commander": "2.12.2",
+        "convert-source-map": "1.5.1",
+        "fs-readdir-recursive": "0.1.2",
+        "glob": "5.0.15",
+        "lodash": "4.17.4",
+        "log-symbols": "1.0.2",
+        "output-file-sync": "1.1.2",
+        "path-exists": "1.0.0",
+        "path-is-absolute": "1.0.1",
+        "request": "2.83.0",
+        "slash": "1.0.0",
+        "source-map": "0.5.7",
+        "v8flags": "2.1.1"
+      },
+      "dependencies": {
+        "babel-core": {
+          "version": "6.26.0",
+          "resolved": "http://registry.npm.taobao.org/babel-core/download/babel-core-6.26.0.tgz",
+          "integrity": "sha1-rzL3izGm/O8RnIew/Y2XU/A6C7g=",
+          "dev": true,
+          "requires": {
+            "babel-code-frame": "6.26.0",
+            "babel-generator": "6.26.0",
+            "babel-helpers": "6.24.1",
+            "babel-messages": "6.23.0",
+            "babel-register": "6.26.0",
+            "babel-runtime": "6.26.0",
+            "babel-template": "6.26.0",
+            "babel-traverse": "6.26.0",
+            "babel-types": "6.26.0",
+            "babylon": "6.18.0",
+            "convert-source-map": "1.5.1",
+            "debug": "2.6.9",
+            "json5": "0.5.1",
+            "lodash": "4.17.4",
+            "minimatch": "3.0.4",
+            "path-is-absolute": "1.0.1",
+            "private": "0.1.8",
+            "slash": "1.0.0",
+            "source-map": "0.5.7"
+          },
+          "dependencies": {
+            "babel-register": {
+              "version": "6.26.0",
+              "resolved": "http://registry.npm.taobao.org/babel-register/download/babel-register-6.26.0.tgz",
+              "integrity": "sha1-btAhFz4vy0htestFxgCahW9kcHE=",
+              "dev": true,
+              "requires": {
+                "babel-core": "6.26.0",
+                "babel-runtime": "6.26.0",
+                "core-js": "2.5.3",
+                "home-or-tmp": "2.0.0",
+                "lodash": "4.17.4",
+                "mkdirp": "0.5.1",
+                "source-map-support": "0.4.18"
+              }
+            }
+          }
+        },
+        "chalk": {
+          "version": "1.1.1",
+          "resolved": "http://registry.npm.taobao.org/chalk/download/chalk-1.1.1.tgz",
+          "integrity": "sha1-UJr7ZwZudJn36zU1x3RFdyri0Bk=",
+          "dev": true,
+          "requires": {
+            "ansi-styles": "2.2.1",
+            "escape-string-regexp": "1.0.5",
+            "has-ansi": "2.0.0",
+            "strip-ansi": "3.0.1",
+            "supports-color": "2.0.0"
+          }
+        },
+        "supports-color": {
+          "version": "2.0.0",
+          "resolved": "http://registry.npm.taobao.org/supports-color/download/supports-color-2.0.0.tgz",
+          "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=",
+          "dev": true
+        }
+      }
+    },
+    "babel-code-frame": {
+      "version": "6.26.0",
+      "resolved": "http://registry.npm.taobao.org/babel-code-frame/download/babel-code-frame-6.26.0.tgz",
+      "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=",
+      "requires": {
+        "chalk": "1.1.3",
+        "esutils": "2.0.2",
+        "js-tokens": "3.0.2"
+      }
+    },
+    "babel-core": {
+      "version": "6.11.4",
+      "resolved": "http://registry.npm.taobao.org/babel-core/download/babel-core-6.11.4.tgz",
+      "integrity": "sha1-zFWk8yOaoFD4Ut1ov/yV+IaEgxY=",
+      "dev": true,
+      "requires": {
+        "babel-code-frame": "6.26.0",
+        "babel-generator": "6.26.0",
+        "babel-helpers": "6.24.1",
+        "babel-messages": "6.23.0",
+        "babel-register": "6.22.0",
+        "babel-runtime": "6.26.0",
+        "babel-template": "6.26.0",
+        "babel-traverse": "6.26.0",
+        "babel-types": "6.26.0",
+        "babylon": "6.18.0",
+        "convert-source-map": "1.5.1",
+        "debug": "2.6.9",
+        "json5": "0.4.0",
+        "lodash": "4.17.4",
+        "minimatch": "3.0.4",
+        "path-exists": "1.0.0",
+        "path-is-absolute": "1.0.1",
+        "private": "0.1.8",
+        "shebang-regex": "1.0.0",
+        "slash": "1.0.0",
+        "source-map": "0.5.7"
+      },
+      "dependencies": {
+        "json5": {
+          "version": "0.4.0",
+          "resolved": "http://registry.npm.taobao.org/json5/download/json5-0.4.0.tgz",
+          "integrity": "sha1-BUNS5MTIDIbAkjh31EneF2pzLI0=",
+          "dev": true
+        }
+      }
+    },
+    "babel-eslint": {
+      "version": "7.0.0",
+      "resolved": "http://registry.npm.taobao.org/babel-eslint/download/babel-eslint-7.0.0.tgz",
+      "integrity": "sha1-VOUbQDP1SsgTJuzqTGRqd5k1GW0=",
+      "dev": true,
+      "requires": {
+        "babel-traverse": "6.26.0",
+        "babel-types": "6.26.0",
+        "babylon": "6.18.0",
+        "lodash.pickby": "4.6.0"
+      }
+    },
+    "babel-generator": {
+      "version": "6.26.0",
+      "resolved": "http://registry.npm.taobao.org/babel-generator/download/babel-generator-6.26.0.tgz",
+      "integrity": "sha1-rBriAHC3n248odMmlhMFN3TyDcU=",
+      "requires": {
+        "babel-messages": "6.23.0",
+        "babel-runtime": "6.26.0",
+        "babel-types": "6.26.0",
+        "detect-indent": "4.0.0",
+        "jsesc": "1.3.0",
+        "lodash": "4.17.4",
+        "source-map": "0.5.7",
+        "trim-right": "1.0.1"
+      }
+    },
+    "babel-helper-builder-react-jsx": {
+      "version": "6.26.0",
+      "resolved": "http://registry.npm.taobao.org/babel-helper-builder-react-jsx/download/babel-helper-builder-react-jsx-6.26.0.tgz",
+      "integrity": "sha1-Of+DE7dci2Xc7/HzHTg+D/KkCKA=",
+      "dev": true,
+      "requires": {
+        "babel-runtime": "6.26.0",
+        "babel-types": "6.26.0",
+        "esutils": "2.0.2"
+      }
+    },
+    "babel-helper-call-delegate": {
+      "version": "6.24.1",
+      "resolved": "http://registry.npm.taobao.org/babel-helper-call-delegate/download/babel-helper-call-delegate-6.24.1.tgz",
+      "integrity": "sha1-7Oaqzdx25Bw0YfiL/Fdb0Nqi340=",
+      "dev": true,
+      "requires": {
+        "babel-helper-hoist-variables": "6.24.1",
+        "babel-runtime": "6.26.0",
+        "babel-traverse": "6.26.0",
+        "babel-types": "6.26.0"
+      }
+    },
+    "babel-helper-define-map": {
+      "version": "6.26.0",
+      "resolved": "http://registry.npm.taobao.org/babel-helper-define-map/download/babel-helper-define-map-6.26.0.tgz",
+      "integrity": "sha1-pfVtq0GiX5fstJjH66ypgZ+Vvl8=",
+      "dev": true,
+      "requires": {
+        "babel-helper-function-name": "6.24.1",
+        "babel-runtime": "6.26.0",
+        "babel-types": "6.26.0",
+        "lodash": "4.17.4"
+      }
+    },
+    "babel-helper-function-name": {
+      "version": "6.24.1",
+      "resolved": "http://registry.npm.taobao.org/babel-helper-function-name/download/babel-helper-function-name-6.24.1.tgz",
+      "integrity": "sha1-00dbjAPtmCQqJbSDUasYOZ01gKk=",
+      "dev": true,
+      "requires": {
+        "babel-helper-get-function-arity": "6.24.1",
+        "babel-runtime": "6.26.0",
+        "babel-template": "6.26.0",
+        "babel-traverse": "6.26.0",
+        "babel-types": "6.26.0"
+      }
+    },
+    "babel-helper-get-function-arity": {
+      "version": "6.24.1",
+      "resolved": "http://registry.npm.taobao.org/babel-helper-get-function-arity/download/babel-helper-get-function-arity-6.24.1.tgz",
+      "integrity": "sha1-j3eCqpNAfEHTqlCQj4mwMbG2hT0=",
+      "dev": true,
+      "requires": {
+        "babel-runtime": "6.26.0",
+        "babel-types": "6.26.0"
+      }
+    },
+    "babel-helper-hoist-variables": {
+      "version": "6.24.1",
+      "resolved": "http://registry.npm.taobao.org/babel-helper-hoist-variables/download/babel-helper-hoist-variables-6.24.1.tgz",
+      "integrity": "sha1-HssnaJydJVE+rbyZFKc/VAi+enY=",
+      "dev": true,
+      "requires": {
+        "babel-runtime": "6.26.0",
+        "babel-types": "6.26.0"
+      }
+    },
+    "babel-helper-optimise-call-expression": {
+      "version": "6.24.1",
+      "resolved": "http://registry.npm.taobao.org/babel-helper-optimise-call-expression/download/babel-helper-optimise-call-expression-6.24.1.tgz",
+      "integrity": "sha1-96E0J7qfc/j0+pk8VKl4gtEkQlc=",
+      "dev": true,
+      "requires": {
+        "babel-runtime": "6.26.0",
+        "babel-types": "6.26.0"
+      }
+    },
+    "babel-helper-regex": {
+      "version": "6.26.0",
+      "resolved": "http://registry.npm.taobao.org/babel-helper-regex/download/babel-helper-regex-6.26.0.tgz",
+      "integrity": "sha1-MlxZ+QL4LyS3T6zu0DY5VPZJXnI=",
+      "dev": true,
+      "requires": {
+        "babel-runtime": "6.26.0",
+        "babel-types": "6.26.0",
+        "lodash": "4.17.4"
+      }
+    },
+    "babel-helper-replace-supers": {
+      "version": "6.24.1",
+      "resolved": "http://registry.npm.taobao.org/babel-helper-replace-supers/download/babel-helper-replace-supers-6.24.1.tgz",
+      "integrity": "sha1-v22/5Dk40XNpohPKiov3S2qQqxo=",
+      "dev": true,
+      "requires": {
+        "babel-helper-optimise-call-expression": "6.24.1",
+        "babel-messages": "6.23.0",
+        "babel-runtime": "6.26.0",
+        "babel-template": "6.26.0",
+        "babel-traverse": "6.26.0",
+        "babel-types": "6.26.0"
+      }
+    },
+    "babel-helpers": {
+      "version": "6.24.1",
+      "resolved": "http://registry.npm.taobao.org/babel-helpers/download/babel-helpers-6.24.1.tgz",
+      "integrity": "sha1-NHHenK7DiOXIUOWX5Yom3fN2ArI=",
+      "requires": {
+        "babel-runtime": "6.26.0",
+        "babel-template": "6.26.0"
+      }
+    },
+    "babel-loader": {
+      "version": "6.2.10",
+      "resolved": "http://registry.npm.taobao.org/babel-loader/download/babel-loader-6.2.10.tgz",
+      "integrity": "sha1-re/CskIyDNXRXmWzHOoOixsC1LA=",
+      "dev": true,
+      "requires": {
+        "find-cache-dir": "0.1.1",
+        "loader-utils": "0.2.17",
+        "mkdirp": "0.5.1",
+        "object-assign": "4.1.1"
+      }
+    },
+    "babel-messages": {
+      "version": "6.23.0",
+      "resolved": "http://registry.npm.taobao.org/babel-messages/download/babel-messages-6.23.0.tgz",
+      "integrity": "sha1-8830cDhYA1sqKVHG7F7fbGLyYw4=",
+      "requires": {
+        "babel-runtime": "6.26.0"
+      }
+    },
+    "babel-plugin-check-es2015-constants": {
+      "version": "6.22.0",
+      "resolved": "http://registry.npm.taobao.org/babel-plugin-check-es2015-constants/download/babel-plugin-check-es2015-constants-6.22.0.tgz",
+      "integrity": "sha1-NRV7EBQm/S/9PaP3XH0ekYNbv4o=",
+      "dev": true,
+      "requires": {
+        "babel-runtime": "6.26.0"
+      }
+    },
+    "babel-plugin-react-transform": {
+      "version": "2.0.2",
+      "resolved": "http://registry.npm.taobao.org/babel-plugin-react-transform/download/babel-plugin-react-transform-2.0.2.tgz",
+      "integrity": "sha1-UVu/qZaJOYEULZCx+bFjXeKZUQk=",
+      "dev": true,
+      "requires": {
+        "lodash": "4.17.4"
+      }
+    },
+    "babel-plugin-syntax-flow": {
+      "version": "6.18.0",
+      "resolved": "http://registry.npm.taobao.org/babel-plugin-syntax-flow/download/babel-plugin-syntax-flow-6.18.0.tgz",
+      "integrity": "sha1-TDqyCiryaqIM0lmVw5jE63AxDI0=",
+      "dev": true
+    },
+    "babel-plugin-syntax-jsx": {
+      "version": "6.18.0",
+      "resolved": "http://registry.npm.taobao.org/babel-plugin-syntax-jsx/download/babel-plugin-syntax-jsx-6.18.0.tgz",
+      "integrity": "sha1-CvMqmm4Tyno/1QaeYtew9Y0NiUY=",
+      "dev": true
+    },
+    "babel-plugin-syntax-object-rest-spread": {
+      "version": "6.13.0",
+      "resolved": "http://registry.npm.taobao.org/babel-plugin-syntax-object-rest-spread/download/babel-plugin-syntax-object-rest-spread-6.13.0.tgz",
+      "integrity": "sha1-/WU28rzhODb/o6VFjEkDpZe7O/U=",
+      "dev": true
+    },
+    "babel-plugin-transform-es2015-arrow-functions": {
+      "version": "6.22.0",
+      "resolved": "http://registry.npm.taobao.org/babel-plugin-transform-es2015-arrow-functions/download/babel-plugin-transform-es2015-arrow-functions-6.22.0.tgz",
+      "integrity": "sha1-RSaSy3EdX3ncf4XkQM5BufJE0iE=",
+      "dev": true,
+      "requires": {
+        "babel-runtime": "6.26.0"
+      }
+    },
+    "babel-plugin-transform-es2015-block-scoped-functions": {
+      "version": "6.22.0",
+      "resolved": "http://registry.npm.taobao.org/babel-plugin-transform-es2015-block-scoped-functions/download/babel-plugin-transform-es2015-block-scoped-functions-6.22.0.tgz",
+      "integrity": "sha1-u8UbSflk1wy42OC5ToICRs46YUE=",
+      "dev": true,
+      "requires": {
+        "babel-runtime": "6.26.0"
+      }
+    },
+    "babel-plugin-transform-es2015-block-scoping": {
+      "version": "6.26.0",
+      "resolved": "http://registry.npm.taobao.org/babel-plugin-transform-es2015-block-scoping/download/babel-plugin-transform-es2015-block-scoping-6.26.0.tgz",
+      "integrity": "sha1-1w9SmcEwjQXBL0Y4E7CgnnOxiV8=",
+      "dev": true,
+      "requires": {
+        "babel-runtime": "6.26.0",
+        "babel-template": "6.26.0",
+        "babel-traverse": "6.26.0",
+        "babel-types": "6.26.0",
+        "lodash": "4.17.4"
+      }
+    },
+    "babel-plugin-transform-es2015-classes": {
+      "version": "6.24.1",
+      "resolved": "http://registry.npm.taobao.org/babel-plugin-transform-es2015-classes/download/babel-plugin-transform-es2015-classes-6.24.1.tgz",
+      "integrity": "sha1-WkxYpQyclGHlZLSyo7+ryXolhNs=",
+      "dev": true,
+      "requires": {
+        "babel-helper-define-map": "6.26.0",
+        "babel-helper-function-name": "6.24.1",
+        "babel-helper-optimise-call-expression": "6.24.1",
+        "babel-helper-replace-supers": "6.24.1",
+        "babel-messages": "6.23.0",
+        "babel-runtime": "6.26.0",
+        "babel-template": "6.26.0",
+        "babel-traverse": "6.26.0",
+        "babel-types": "6.26.0"
+      }
+    },
+    "babel-plugin-transform-es2015-computed-properties": {
+      "version": "6.24.1",
+      "resolved": "http://registry.npm.taobao.org/babel-plugin-transform-es2015-computed-properties/download/babel-plugin-transform-es2015-computed-properties-6.24.1.tgz",
+      "integrity": "sha1-b+Ko0WiV1WNPTNmZttNICjCBWbM=",
+      "dev": true,
+      "requires": {
+        "babel-runtime": "6.26.0",
+        "babel-template": "6.26.0"
+      }
+    },
+    "babel-plugin-transform-es2015-destructuring": {
+      "version": "6.23.0",
+      "resolved": "http://registry.npm.taobao.org/babel-plugin-transform-es2015-destructuring/download/babel-plugin-transform-es2015-destructuring-6.23.0.tgz",
+      "integrity": "sha1-mXux8auWf2gtKwh2/jWNYOdlxW0=",
+      "dev": true,
+      "requires": {
+        "babel-runtime": "6.26.0"
+      }
+    },
+    "babel-plugin-transform-es2015-duplicate-keys": {
+      "version": "6.24.1",
+      "resolved": "http://registry.npm.taobao.org/babel-plugin-transform-es2015-duplicate-keys/download/babel-plugin-transform-es2015-duplicate-keys-6.24.1.tgz",
+      "integrity": "sha1-c+s9MQypaePvnskcU3QabxV2Qj4=",
+      "dev": true,
+      "requires": {
+        "babel-runtime": "6.26.0",
+        "babel-types": "6.26.0"
+      }
+    },
+    "babel-plugin-transform-es2015-for-of": {
+      "version": "6.23.0",
+      "resolved": "http://registry.npm.taobao.org/babel-plugin-transform-es2015-for-of/download/babel-plugin-transform-es2015-for-of-6.23.0.tgz",
+      "integrity": "sha1-9HyVsrYT3x0+zC/bdXNiPHUkhpE=",
+      "dev": true,
+      "requires": {
+        "babel-runtime": "6.26.0"
+      }
+    },
+    "babel-plugin-transform-es2015-function-name": {
+      "version": "6.24.1",
+      "resolved": "http://registry.npm.taobao.org/babel-plugin-transform-es2015-function-name/download/babel-plugin-transform-es2015-function-name-6.24.1.tgz",
+      "integrity": "sha1-g0yJhTvDaxrw86TF26qU/Y6sqos=",
+      "dev": true,
+      "requires": {
+        "babel-helper-function-name": "6.24.1",
+        "babel-runtime": "6.26.0",
+        "babel-types": "6.26.0"
+      }
+    },
+    "babel-plugin-transform-es2015-literals": {
+      "version": "6.22.0",
+      "resolved": "http://registry.npm.taobao.org/babel-plugin-transform-es2015-literals/download/babel-plugin-transform-es2015-literals-6.22.0.tgz",
+      "integrity": "sha1-T1SgLWzWbPkVKAAZox0xklN3yi4=",
+      "dev": true,
+      "requires": {
+        "babel-runtime": "6.26.0"
+      }
+    },
+    "babel-plugin-transform-es2015-modules-commonjs": {
+      "version": "6.26.0",
+      "resolved": "http://registry.npm.taobao.org/babel-plugin-transform-es2015-modules-commonjs/download/babel-plugin-transform-es2015-modules-commonjs-6.26.0.tgz",
+      "integrity": "sha1-DYOUApt9xqvhqX7xgeAHWN0uXYo=",
+      "dev": true,
+      "requires": {
+        "babel-plugin-transform-strict-mode": "6.24.1",
+        "babel-runtime": "6.26.0",
+        "babel-template": "6.26.0",
+        "babel-types": "6.26.0"
+      }
+    },
+    "babel-plugin-transform-es2015-object-super": {
+      "version": "6.24.1",
+      "resolved": "http://registry.npm.taobao.org/babel-plugin-transform-es2015-object-super/download/babel-plugin-transform-es2015-object-super-6.24.1.tgz",
+      "integrity": "sha1-JM72muIcuDp/hgPa0CH1cusnj40=",
+      "dev": true,
+      "requires": {
+        "babel-helper-replace-supers": "6.24.1",
+        "babel-runtime": "6.26.0"
+      }
+    },
+    "babel-plugin-transform-es2015-parameters": {
+      "version": "6.24.1",
+      "resolved": "http://registry.npm.taobao.org/babel-plugin-transform-es2015-parameters/download/babel-plugin-transform-es2015-parameters-6.24.1.tgz",
+      "integrity": "sha1-V6w1GrScrxSpfNE7CfZv3wpiXys=",
+      "dev": true,
+      "requires": {
+        "babel-helper-call-delegate": "6.24.1",
+        "babel-helper-get-function-arity": "6.24.1",
+        "babel-runtime": "6.26.0",
+        "babel-template": "6.26.0",
+        "babel-traverse": "6.26.0",
+        "babel-types": "6.26.0"
+      }
+    },
+    "babel-plugin-transform-es2015-shorthand-properties": {
+      "version": "6.24.1",
+      "resolved": "http://registry.npm.taobao.org/babel-plugin-transform-es2015-shorthand-properties/download/babel-plugin-transform-es2015-shorthand-properties-6.24.1.tgz",
+      "integrity": "sha1-JPh11nIch2YbvZmkYi5R8U3jiqA=",
+      "dev": true,
+      "requires": {
+        "babel-runtime": "6.26.0",
+        "babel-types": "6.26.0"
+      }
+    },
+    "babel-plugin-transform-es2015-spread": {
+      "version": "6.22.0",
+      "resolved": "http://registry.npm.taobao.org/babel-plugin-transform-es2015-spread/download/babel-plugin-transform-es2015-spread-6.22.0.tgz",
+      "integrity": "sha1-1taKmfia7cRTbIGlQujdnxdG+NE=",
+      "dev": true,
+      "requires": {
+        "babel-runtime": "6.26.0"
+      }
+    },
+    "babel-plugin-transform-es2015-sticky-regex": {
+      "version": "6.24.1",
+      "resolved": "http://registry.npm.taobao.org/babel-plugin-transform-es2015-sticky-regex/download/babel-plugin-transform-es2015-sticky-regex-6.24.1.tgz",
+      "integrity": "sha1-AMHNsaynERLN8M9hJsLta0V8zbw=",
+      "dev": true,
+      "requires": {
+        "babel-helper-regex": "6.26.0",
+        "babel-runtime": "6.26.0",
+        "babel-types": "6.26.0"
+      }
+    },
+    "babel-plugin-transform-es2015-template-literals": {
+      "version": "6.22.0",
+      "resolved": "http://registry.npm.taobao.org/babel-plugin-transform-es2015-template-literals/download/babel-plugin-transform-es2015-template-literals-6.22.0.tgz",
+      "integrity": "sha1-qEs0UPfp+PH2g51taH2oS7EjbY0=",
+      "dev": true,
+      "requires": {
+        "babel-runtime": "6.26.0"
+      }
+    },
+    "babel-plugin-transform-es2015-typeof-symbol": {
+      "version": "6.23.0",
+      "resolved": "http://registry.npm.taobao.org/babel-plugin-transform-es2015-typeof-symbol/download/babel-plugin-transform-es2015-typeof-symbol-6.23.0.tgz",
+      "integrity": "sha1-3sCfHN3/lLUqxz1QXITfWdzOs3I=",
+      "dev": true,
+      "requires": {
+        "babel-runtime": "6.26.0"
+      }
+    },
+    "babel-plugin-transform-es2015-unicode-regex": {
+      "version": "6.24.1",
+      "resolved": "http://registry.npm.taobao.org/babel-plugin-transform-es2015-unicode-regex/download/babel-plugin-transform-es2015-unicode-regex-6.24.1.tgz",
+      "integrity": "sha1-04sS9C6nMj9yk4fxinxa4frrNek=",
+      "dev": true,
+      "requires": {
+        "babel-helper-regex": "6.26.0",
+        "babel-runtime": "6.26.0",
+        "regexpu-core": "2.0.0"
+      }
+    },
+    "babel-plugin-transform-flow-strip-types": {
+      "version": "6.22.0",
+      "resolved": "http://registry.npm.taobao.org/babel-plugin-transform-flow-strip-types/download/babel-plugin-transform-flow-strip-types-6.22.0.tgz",
+      "integrity": "sha1-hMtnKTXUNxT9wyvOhFaNh0Qc988=",
+      "dev": true,
+      "requires": {
+        "babel-plugin-syntax-flow": "6.18.0",
+        "babel-runtime": "6.26.0"
+      }
+    },
+    "babel-plugin-transform-object-rest-spread": {
+      "version": "6.8.0",
+      "resolved": "http://registry.npm.taobao.org/babel-plugin-transform-object-rest-spread/download/babel-plugin-transform-object-rest-spread-6.8.0.tgz",
+      "integrity": "sha1-A9EwjiV6nY4agVrh/T2yG96/CNk=",
+      "dev": true,
+      "requires": {
+        "babel-plugin-syntax-object-rest-spread": "6.13.0",
+        "babel-runtime": "6.26.0"
+      }
+    },
+    "babel-plugin-transform-react-display-name": {
+      "version": "6.25.0",
+      "resolved": "http://registry.npm.taobao.org/babel-plugin-transform-react-display-name/download/babel-plugin-transform-react-display-name-6.25.0.tgz",
+      "integrity": "sha1-Z+K/Hx6ck6sI25Z5LgU5K/LMKNE=",
+      "dev": true,
+      "requires": {
+        "babel-runtime": "6.26.0"
+      }
+    },
+    "babel-plugin-transform-react-jsx": {
+      "version": "6.24.1",
+      "resolved": "http://registry.npm.taobao.org/babel-plugin-transform-react-jsx/download/babel-plugin-transform-react-jsx-6.24.1.tgz",
+      "integrity": "sha1-hAoCjn30YN/DotKfDA2R9jduZqM=",
+      "dev": true,
+      "requires": {
+        "babel-helper-builder-react-jsx": "6.26.0",
+        "babel-plugin-syntax-jsx": "6.18.0",
+        "babel-runtime": "6.26.0"
+      }
+    },
+    "babel-plugin-transform-react-jsx-source": {
+      "version": "6.22.0",
+      "resolved": "http://registry.npm.taobao.org/babel-plugin-transform-react-jsx-source/download/babel-plugin-transform-react-jsx-source-6.22.0.tgz",
+      "integrity": "sha1-ZqwSFT9c0tF7PBkmj0vwGX9E7NY=",
+      "dev": true,
+      "requires": {
+        "babel-plugin-syntax-jsx": "6.18.0",
+        "babel-runtime": "6.26.0"
+      }
+    },
+    "babel-plugin-transform-regenerator": {
+      "version": "6.26.0",
+      "resolved": "http://registry.npm.taobao.org/babel-plugin-transform-regenerator/download/babel-plugin-transform-regenerator-6.26.0.tgz",
+      "integrity": "sha1-4HA2lvveJ/Cj78rPi03KL3s6jy8=",
+      "dev": true,
+      "requires": {
+        "regenerator-transform": "0.10.1"
+      }
+    },
+    "babel-plugin-transform-strict-mode": {
+      "version": "6.24.1",
+      "resolved": "http://registry.npm.taobao.org/babel-plugin-transform-strict-mode/download/babel-plugin-transform-strict-mode-6.24.1.tgz",
+      "integrity": "sha1-1fr3qleKZbvlkc9e2uBKDGcCB1g=",
+      "dev": true,
+      "requires": {
+        "babel-runtime": "6.26.0",
+        "babel-types": "6.26.0"
+      }
+    },
+    "babel-polyfill": {
+      "version": "6.16.0",
+      "resolved": "http://registry.npm.taobao.org/babel-polyfill/download/babel-polyfill-6.16.0.tgz",
+      "integrity": "sha1-LUUCHfh+JqN0ttTRqcZZZNF/JCI=",
+      "requires": {
+        "babel-runtime": "6.26.0",
+        "core-js": "2.5.3",
+        "regenerator-runtime": "0.9.6"
+      }
+    },
+    "babel-preset-es2015": {
+      "version": "6.9.0",
+      "resolved": "http://registry.npm.taobao.org/babel-preset-es2015/download/babel-preset-es2015-6.9.0.tgz",
+      "integrity": "sha1-leRxasRIHfswmZy1wRGBThraD0E=",
+      "dev": true,
+      "requires": {
+        "babel-plugin-check-es2015-constants": "6.22.0",
+        "babel-plugin-transform-es2015-arrow-functions": "6.22.0",
+        "babel-plugin-transform-es2015-block-scoped-functions": "6.22.0",
+        "babel-plugin-transform-es2015-block-scoping": "6.26.0",
+        "babel-plugin-transform-es2015-classes": "6.24.1",
+        "babel-plugin-transform-es2015-computed-properties": "6.24.1",
+        "babel-plugin-transform-es2015-destructuring": "6.23.0",
+        "babel-plugin-transform-es2015-duplicate-keys": "6.24.1",
+        "babel-plugin-transform-es2015-for-of": "6.23.0",
+        "babel-plugin-transform-es2015-function-name": "6.24.1",
+        "babel-plugin-transform-es2015-literals": "6.22.0",
+        "babel-plugin-transform-es2015-modules-commonjs": "6.26.0",
+        "babel-plugin-transform-es2015-object-super": "6.24.1",
+        "babel-plugin-transform-es2015-parameters": "6.24.1",
+        "babel-plugin-transform-es2015-shorthand-properties": "6.24.1",
+        "babel-plugin-transform-es2015-spread": "6.22.0",
+        "babel-plugin-transform-es2015-sticky-regex": "6.24.1",
+        "babel-plugin-transform-es2015-template-literals": "6.22.0",
+        "babel-plugin-transform-es2015-typeof-symbol": "6.23.0",
+        "babel-plugin-transform-es2015-unicode-regex": "6.24.1",
+        "babel-plugin-transform-regenerator": "6.26.0"
+      }
+    },
+    "babel-preset-react": {
+      "version": "6.5.0",
+      "resolved": "http://registry.npm.taobao.org/babel-preset-react/download/babel-preset-react-6.5.0.tgz",
+      "integrity": "sha1-0yiaoOMI29SLchD5l3EB8Pluvh8=",
+      "dev": true,
+      "requires": {
+        "babel-plugin-syntax-flow": "6.18.0",
+        "babel-plugin-syntax-jsx": "6.18.0",
+        "babel-plugin-transform-flow-strip-types": "6.22.0",
+        "babel-plugin-transform-react-display-name": "6.25.0",
+        "babel-plugin-transform-react-jsx": "6.24.1",
+        "babel-plugin-transform-react-jsx-source": "6.22.0"
+      }
+    },
+    "babel-preset-react-hmre": {
+      "version": "1.1.1",
+      "resolved": "http://registry.npm.taobao.org/babel-preset-react-hmre/download/babel-preset-react-hmre-1.1.1.tgz",
+      "integrity": "sha1-0hbmDLW41Mhz4Z7Q9U6v8UN7xJI=",
+      "dev": true,
+      "requires": {
+        "babel-plugin-react-transform": "2.0.2",
+        "react-transform-catch-errors": "1.0.2",
+        "react-transform-hmr": "1.0.4",
+        "redbox-react": "1.5.0"
+      }
+    },
+    "babel-register": {
+      "version": "6.22.0",
+      "resolved": "http://registry.npm.taobao.org/babel-register/download/babel-register-6.22.0.tgz",
+      "integrity": "sha1-ph3YOXX5ykqefW7/MFlJTNXqTGM=",
+      "dev": true,
+      "requires": {
+        "babel-core": "6.26.0",
+        "babel-runtime": "6.26.0",
+        "core-js": "2.5.3",
+        "home-or-tmp": "2.0.0",
+        "lodash": "4.17.4",
+        "mkdirp": "0.5.1",
+        "source-map-support": "0.4.18"
+      },
+      "dependencies": {
+        "babel-core": {
+          "version": "6.26.0",
+          "resolved": "http://registry.npm.taobao.org/babel-core/download/babel-core-6.26.0.tgz",
+          "integrity": "sha1-rzL3izGm/O8RnIew/Y2XU/A6C7g=",
+          "requires": {
+            "babel-code-frame": "6.26.0",
+            "babel-generator": "6.26.0",
+            "babel-helpers": "6.24.1",
+            "babel-messages": "6.23.0",
+            "babel-runtime": "6.26.0",
+            "babel-template": "6.26.0",
+            "babel-traverse": "6.26.0",
+            "babel-types": "6.26.0",
+            "babylon": "6.18.0",
+            "convert-source-map": "1.5.1",
+            "debug": "2.6.9",
+            "json5": "0.5.1",
+            "lodash": "4.17.4",
+            "minimatch": "3.0.4",
+            "path-is-absolute": "1.0.1",
+            "private": "0.1.8",
+            "slash": "1.0.0",
+            "source-map": "0.5.7"
+          }
+        },
+        "babel-register": {
+          "version": "6.26.0",
+          "resolved": "http://registry.npm.taobao.org/babel-register/download/babel-register-6.26.0.tgz",
+          "integrity": "sha1-btAhFz4vy0htestFxgCahW9kcHE=",
+          "requires": {
+            "babel-core": "6.26.0",
+            "babel-runtime": "6.26.0",
+            "core-js": "2.5.3",
+            "home-or-tmp": "2.0.0",
+            "lodash": "4.17.4",
+            "mkdirp": "0.5.1",
+            "source-map-support": "0.4.18"
+          }
+        }
+      }
+    },
+    "babel-runtime": {
+      "version": "6.26.0",
+      "resolved": "http://registry.npm.taobao.org/babel-runtime/download/babel-runtime-6.26.0.tgz",
+      "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=",
+      "requires": {
+        "core-js": "2.5.3",
+        "regenerator-runtime": "0.11.1"
+      },
+      "dependencies": {
+        "regenerator-runtime": {
+          "version": "0.11.1",
+          "resolved": "http://registry.npm.taobao.org/regenerator-runtime/download/regenerator-runtime-0.11.1.tgz",
+          "integrity": "sha1-vgWtf5v30i4Fb5cmzuUBf78Z4uk="
+        }
+      }
+    },
+    "babel-template": {
+      "version": "6.26.0",
+      "resolved": "http://registry.npm.taobao.org/babel-template/download/babel-template-6.26.0.tgz",
+      "integrity": "sha1-3gPi0WOWsGn0bdn/+FIfsaDjXgI=",
+      "requires": {
+        "babel-runtime": "6.26.0",
+        "babel-traverse": "6.26.0",
+        "babel-types": "6.26.0",
+        "babylon": "6.18.0",
+        "lodash": "4.17.4"
+      }
+    },
+    "babel-traverse": {
+      "version": "6.26.0",
+      "resolved": "http://registry.npm.taobao.org/babel-traverse/download/babel-traverse-6.26.0.tgz",
+      "integrity": "sha1-RqnL1+3MYsjlwGTi0tjQ9ANXZu4=",
+      "requires": {
+        "babel-code-frame": "6.26.0",
+        "babel-messages": "6.23.0",
+        "babel-runtime": "6.26.0",
+        "babel-types": "6.26.0",
+        "babylon": "6.18.0",
+        "debug": "2.6.9",
+        "globals": "9.18.0",
+        "invariant": "2.2.2",
+        "lodash": "4.17.4"
+      }
+    },
+    "babel-types": {
+      "version": "6.26.0",
+      "resolved": "http://registry.npm.taobao.org/babel-types/download/babel-types-6.26.0.tgz",
+      "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=",
+      "requires": {
+        "babel-runtime": "6.26.0",
+        "esutils": "2.0.2",
+        "lodash": "4.17.4",
+        "to-fast-properties": "1.0.3"
+      }
+    },
+    "babylon": {
+      "version": "6.18.0",
+      "resolved": "http://registry.npm.taobao.org/babylon/download/babylon-6.18.0.tgz",
+      "integrity": "sha1-ry87iPpvXB5MY00aD46sT1WzleM="
+    },
+    "balanced-match": {
+      "version": "1.0.0",
+      "resolved": "http://registry.npm.taobao.org/balanced-match/download/balanced-match-1.0.0.tgz",
+      "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
+    },
+    "base64-js": {
+      "version": "1.2.1",
+      "resolved": "http://registry.npm.taobao.org/base64-js/download/base64-js-1.2.1.tgz",
+      "integrity": "sha1-qRlH2h9KUW6jjltOwOw3c2deCIY=",
+      "dev": true
+    },
+    "bcrypt-pbkdf": {
+      "version": "1.0.1",
+      "resolved": "http://registry.npm.taobao.org/bcrypt-pbkdf/download/bcrypt-pbkdf-1.0.1.tgz",
+      "integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=",
+      "dev": true,
+      "optional": true,
+      "requires": {
+        "tweetnacl": "0.14.5"
+      }
+    },
+    "big.js": {
+      "version": "3.2.0",
+      "resolved": "http://registry.npm.taobao.org/big.js/download/big.js-3.2.0.tgz",
+      "integrity": "sha1-pfwpi4G54Nyi5FiCR4S2XFK6WI4=",
+      "dev": true
+    },
+    "bin-version": {
+      "version": "1.0.4",
+      "resolved": "http://registry.npm.taobao.org/bin-version/download/bin-version-1.0.4.tgz",
+      "integrity": "sha1-nrSY7m/Xb3q5p8FgQ2+JV5Q1144=",
+      "dev": true,
+      "requires": {
+        "find-versions": "1.2.1"
+      }
+    },
+    "bin-version-check": {
+      "version": "2.1.0",
+      "resolved": "http://registry.npm.taobao.org/bin-version-check/download/bin-version-check-2.1.0.tgz",
+      "integrity": "sha1-5OXfKQuQaffRETJAMe/BP90RpbA=",
+      "dev": true,
+      "requires": {
+        "bin-version": "1.0.4",
+        "minimist": "1.2.0",
+        "semver": "4.3.6",
+        "semver-truncate": "1.1.2"
+      },
+      "dependencies": {
+        "minimist": {
+          "version": "1.2.0",
+          "resolved": "http://registry.npm.taobao.org/minimist/download/minimist-1.2.0.tgz",
+          "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
+          "dev": true
+        }
+      }
+    },
+    "binary-extensions": {
+      "version": "1.11.0",
+      "resolved": "http://registry.npm.taobao.org/binary-extensions/download/binary-extensions-1.11.0.tgz",
+      "integrity": "sha1-RqoXUftqL5PuXmibsQh9SxTGwgU=",
+      "dev": true
+    },
+    "bl": {
+      "version": "1.2.1",
+      "resolved": "http://registry.npm.taobao.org/bl/download/bl-1.2.1.tgz",
+      "integrity": "sha1-ysMo977kVzDUBLaSID/LWQ4XLV4=",
+      "dev": true,
+      "requires": {
+        "readable-stream": "2.3.3"
+      }
+    },
+    "block-stream": {
+      "version": "0.0.9",
+      "resolved": "http://registry.npm.taobao.org/block-stream/download/block-stream-0.0.9.tgz",
+      "integrity": "sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo=",
+      "dev": true,
+      "requires": {
+        "inherits": "2.0.3"
+      }
+    },
+    "bluebird": {
+      "version": "3.5.1",
+      "resolved": "http://registry.npm.taobao.org/bluebird/download/bluebird-3.5.1.tgz",
+      "integrity": "sha1-2VUfnemPH82h5oPRfukaBgLuLrk=",
+      "dev": true
+    },
+    "body-parser": {
+      "version": "1.18.2",
+      "resolved": "http://registry.npm.taobao.org/body-parser/download/body-parser-1.18.2.tgz",
+      "integrity": "sha1-h2eKGdhLR9hZuDGZvVm84iKxBFQ=",
+      "dev": true,
+      "requires": {
+        "bytes": "3.0.0",
+        "content-type": "1.0.4",
+        "debug": "2.6.9",
+        "depd": "1.1.1",
+        "http-errors": "1.6.2",
+        "iconv-lite": "0.4.19",
+        "on-finished": "2.3.0",
+        "qs": "6.5.1",
+        "raw-body": "2.3.2",
+        "type-is": "1.6.15"
+      }
+    },
+    "boom": {
+      "version": "4.3.1",
+      "resolved": "http://registry.npm.taobao.org/boom/download/boom-4.3.1.tgz",
+      "integrity": "sha1-T4owBctKfjiJ90kDD9JbluAdLjE=",
+      "dev": true,
+      "requires": {
+        "hoek": "4.2.0"
+      }
+    },
+    "bootstrap-loader": {
+      "version": "1.0.10",
+      "resolved": "http://registry.npm.taobao.org/bootstrap-loader/download/bootstrap-loader-1.0.10.tgz",
+      "integrity": "sha1-Z/ESwkDrXYp7nQF3kXmNSX0fIgY=",
+      "dev": true,
+      "requires": {
+        "chalk": "1.1.3",
+        "escape-regexp": "0.0.1",
+        "js-yaml": "3.10.0",
+        "loader-utils": "0.2.17",
+        "resolve": "1.5.0",
+        "semver": "5.4.1",
+        "strip-json-comments": "1.0.4"
+      },
+      "dependencies": {
+        "semver": {
+          "version": "5.4.1",
+          "resolved": "http://registry.npm.taobao.org/semver/download/semver-5.4.1.tgz",
+          "integrity": "sha1-4FnAnYVx8FQII3M0M1BdOi8AsY4=",
+          "dev": true
+        }
+      }
+    },
+    "bootstrap-sass": {
+      "version": "3.3.7",
+      "resolved": "http://registry.npm.taobao.org/bootstrap-sass/download/bootstrap-sass-3.3.7.tgz",
+      "integrity": "sha1-ZZbHq0D2Y3OTMjqwvIDQZPxjBJg="
+    },
+    "brace": {
+      "version": "0.8.0",
+      "resolved": "http://registry.npm.taobao.org/brace/download/brace-0.8.0.tgz",
+      "integrity": "sha1-6CbG1QVMrl9getexyBI23SzwGXg=",
+      "requires": {
+        "w3c-blob": "0.0.1"
+      }
+    },
+    "brace-expansion": {
+      "version": "1.1.8",
+      "resolved": "http://registry.npm.taobao.org/brace-expansion/download/brace-expansion-1.1.8.tgz",
+      "integrity": "sha1-wHshHHyVLsH479Uad+8NHTmQopI=",
+      "requires": {
+        "balanced-match": "1.0.0",
+        "concat-map": "0.0.1"
+      }
+    },
+    "braces": {
+      "version": "1.8.5",
+      "resolved": "http://registry.npm.taobao.org/braces/download/braces-1.8.5.tgz",
+      "integrity": "sha1-uneWLhLf+WnWt2cR6RS3N4V79qc=",
+      "dev": true,
+      "requires": {
+        "expand-range": "1.8.2",
+        "preserve": "0.2.0",
+        "repeat-element": "1.1.2"
+      }
+    },
+    "browser-stdout": {
+      "version": "1.3.0",
+      "resolved": "http://registry.npm.taobao.org/browser-stdout/download/browser-stdout-1.3.0.tgz",
+      "integrity": "sha1-81HTKWnTL6XXpVZxVCY9korjvR8=",
+      "dev": true
+    },
+    "browserify-aes": {
+      "version": "0.4.0",
+      "resolved": "http://registry.npm.taobao.org/browserify-aes/download/browserify-aes-0.4.0.tgz",
+      "integrity": "sha1-BnFJtmjfMcS1hTPgLQHoBthgjiw=",
+      "dev": true,
+      "requires": {
+        "inherits": "2.0.3"
+      }
+    },
+    "browserify-zlib": {
+      "version": "0.1.4",
+      "resolved": "http://registry.npm.taobao.org/browserify-zlib/download/browserify-zlib-0.1.4.tgz",
+      "integrity": "sha1-uzX4pRn2AOD6a4SFJByXnQFB+y0=",
+      "dev": true,
+      "requires": {
+        "pako": "0.2.9"
+      }
+    },
+    "browserslist": {
+      "version": "1.7.7",
+      "resolved": "http://registry.npm.taobao.org/browserslist/download/browserslist-1.7.7.tgz",
+      "integrity": "sha1-C9dnBCWL6CmyOYu1Dkti0aFmsLk=",
+      "dev": true,
+      "requires": {
+        "caniuse-db": "1.0.30000784",
+        "electron-to-chromium": "1.3.30"
+      }
+    },
+    "browserstack-local": {
+      "version": "1.3.0",
+      "resolved": "http://registry.npm.taobao.org/browserstack-local/download/browserstack-local-1.3.0.tgz",
+      "integrity": "sha1-/oDvBc6JVMTWJuNQLrYtYJA7Qjc=",
+      "dev": true,
+      "requires": {
+        "https-proxy-agent": "1.0.0",
+        "is-running": "2.1.0",
+        "sinon": "1.17.7",
+        "temp-fs": "0.9.9"
+      }
+    },
+    "btoa": {
+      "version": "1.1.2",
+      "resolved": "http://registry.npm.taobao.org/btoa/download/btoa-1.1.2.tgz",
+      "integrity": "sha1-PkC4FmP4HS3WWWpMtxSo3BbPq+A="
+    },
+    "buffer": {
+      "version": "4.9.1",
+      "resolved": "http://registry.npm.taobao.org/buffer/download/buffer-4.9.1.tgz",
+      "integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=",
+      "dev": true,
+      "requires": {
+        "base64-js": "1.2.1",
+        "ieee754": "1.1.8",
+        "isarray": "1.0.0"
+      }
+    },
+    "buffer-crc32": {
+      "version": "0.2.13",
+      "resolved": "http://registry.npm.taobao.org/buffer-crc32/download/buffer-crc32-0.2.13.tgz",
+      "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=",
+      "dev": true
+    },
+    "builtin-modules": {
+      "version": "1.1.1",
+      "resolved": "http://registry.npm.taobao.org/builtin-modules/download/builtin-modules-1.1.1.tgz",
+      "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=",
+      "dev": true
+    },
+    "builtin-status-codes": {
+      "version": "3.0.0",
+      "resolved": "http://registry.npm.taobao.org/builtin-status-codes/download/builtin-status-codes-3.0.0.tgz",
+      "integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=",
+      "dev": true
+    },
+    "bytes": {
+      "version": "3.0.0",
+      "resolved": "http://registry.npm.taobao.org/bytes/download/bytes-3.0.0.tgz",
+      "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=",
+      "dev": true
+    },
+    "caller-path": {
+      "version": "0.1.0",
+      "resolved": "http://registry.npm.taobao.org/caller-path/download/caller-path-0.1.0.tgz",
+      "integrity": "sha1-lAhe9jWB7NPaqSREqP6U6CV3dR8=",
+      "dev": true,
+      "requires": {
+        "callsites": "0.2.0"
+      }
+    },
+    "callsites": {
+      "version": "0.2.0",
+      "resolved": "http://registry.npm.taobao.org/callsites/download/callsites-0.2.0.tgz",
+      "integrity": "sha1-r6uWJikQp/M8GaV3WCXGnzTjUMo=",
+      "dev": true
+    },
+    "camelcase": {
+      "version": "2.1.1",
+      "resolved": "http://registry.npm.taobao.org/camelcase/download/camelcase-2.1.1.tgz",
+      "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=",
+      "dev": true
+    },
+    "camelcase-keys": {
+      "version": "2.1.0",
+      "resolved": "http://registry.npm.taobao.org/camelcase-keys/download/camelcase-keys-2.1.0.tgz",
+      "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=",
+      "dev": true,
+      "requires": {
+        "camelcase": "2.1.1",
+        "map-obj": "1.0.1"
+      }
+    },
+    "caniuse-db": {
+      "version": "1.0.30000784",
+      "resolved": "http://registry.npm.taobao.org/caniuse-db/download/caniuse-db-1.0.30000784.tgz",
+      "integrity": "sha1-G+lQEtlInHcZB0+BruV9vf/mNhs=",
+      "dev": true
+    },
+    "caseless": {
+      "version": "0.12.0",
+      "resolved": "http://registry.npm.taobao.org/caseless/download/caseless-0.12.0.tgz",
+      "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=",
+      "dev": true
+    },
+    "center-align": {
+      "version": "0.1.3",
+      "resolved": "http://registry.npm.taobao.org/center-align/download/center-align-0.1.3.tgz",
+      "integrity": "sha1-qg0yYptu6XIgBBHL1EYckHvCt60=",
+      "dev": true,
+      "requires": {
+        "align-text": "0.1.4",
+        "lazy-cache": "1.0.4"
+      }
+    },
+    "chai": {
+      "version": "3.5.0",
+      "resolved": "http://registry.npm.taobao.org/chai/download/chai-3.5.0.tgz",
+      "integrity": "sha1-TQJjewZ/6Vi9v906QOxW/vc3Mkc=",
+      "dev": true,
+      "requires": {
+        "assertion-error": "1.0.2",
+        "deep-eql": "0.1.3",
+        "type-detect": "1.0.0"
+      }
+    },
+    "chai-as-promised": {
+      "version": "6.0.0",
+      "resolved": "http://registry.npm.taobao.org/chai-as-promised/download/chai-as-promised-6.0.0.tgz",
+      "integrity": "sha1-GgKkM6byTa+sY7nJb6FoTbGqjaY=",
+      "dev": true,
+      "requires": {
+        "check-error": "1.0.2"
+      }
+    },
+    "chain-sdk": {
+      "version": "1.2.1",
+      "resolved": "http://registry.npm.taobao.org/chain-sdk/download/chain-sdk-1.2.1.tgz",
+      "integrity": "sha1-YbAHdLIlfyc/70LHO1xye/HyJJw=",
+      "requires": {
+        "btoa": "1.1.2",
+        "fetch-ponyfill": "3.0.2",
+        "uuid": "3.0.1"
+      },
+      "dependencies": {
+        "uuid": {
+          "version": "3.0.1",
+          "resolved": "http://registry.npm.taobao.org/uuid/download/uuid-3.0.1.tgz",
+          "integrity": "sha1-ZUS7ot/ajBzxfmKaOjBeK7H+5sE="
+        }
+      }
+    },
+    "chalk": {
+      "version": "1.1.3",
+      "resolved": "http://registry.npm.taobao.org/chalk/download/chalk-1.1.3.tgz",
+      "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
+      "requires": {
+        "ansi-styles": "2.2.1",
+        "escape-string-regexp": "1.0.5",
+        "has-ansi": "2.0.0",
+        "strip-ansi": "3.0.1",
+        "supports-color": "2.0.0"
+      },
+      "dependencies": {
+        "supports-color": {
+          "version": "2.0.0",
+          "resolved": "http://registry.npm.taobao.org/supports-color/download/supports-color-2.0.0.tgz",
+          "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc="
+        }
+      }
+    },
+    "check-error": {
+      "version": "1.0.2",
+      "resolved": "http://registry.npm.taobao.org/check-error/download/check-error-1.0.2.tgz",
+      "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=",
+      "dev": true
+    },
+    "chokidar": {
+      "version": "1.7.0",
+      "resolved": "http://registry.npm.taobao.org/chokidar/download/chokidar-1.7.0.tgz",
+      "integrity": "sha1-eY5ol3gVHIB2tLNg5e3SjNortGg=",
+      "dev": true,
+      "requires": {
+        "anymatch": "1.3.2",
+        "async-each": "1.0.1",
+        "fsevents": "1.1.3",
+        "glob-parent": "2.0.0",
+        "inherits": "2.0.3",
+        "is-binary-path": "1.0.1",
+        "is-glob": "2.0.1",
+        "path-is-absolute": "1.0.1",
+        "readdirp": "2.1.0"
+      }
+    },
+    "circular-json": {
+      "version": "0.3.3",
+      "resolved": "http://registry.npm.taobao.org/circular-json/download/circular-json-0.3.3.tgz",
+      "integrity": "sha1-gVyZ6oT2gJUp0vRXkb34JxE1LWY=",
+      "dev": true
+    },
+    "classnames": {
+      "version": "2.2.5",
+      "resolved": "http://registry.npm.taobao.org/classnames/download/classnames-2.2.5.tgz",
+      "integrity": "sha1-+zgB1FNGdknvNgPH1hoCvRKb3m0="
+    },
+    "clean-css": {
+      "version": "3.4.28",
+      "resolved": "http://registry.npm.taobao.org/clean-css/download/clean-css-3.4.28.tgz",
+      "integrity": "sha1-vxlF6C/ICPVWlebd6uwBQA79A/8=",
+      "dev": true,
+      "requires": {
+        "commander": "2.8.1",
+        "source-map": "0.4.4"
+      },
+      "dependencies": {
+        "commander": {
+          "version": "2.8.1",
+          "resolved": "http://registry.npm.taobao.org/commander/download/commander-2.8.1.tgz",
+          "integrity": "sha1-Br42f+v9oMMwqh4qBy09yXYkJdQ=",
+          "dev": true,
+          "requires": {
+            "graceful-readlink": "1.0.1"
+          }
+        },
+        "source-map": {
+          "version": "0.4.4",
+          "resolved": "http://registry.npm.taobao.org/source-map/download/source-map-0.4.4.tgz",
+          "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=",
+          "dev": true,
+          "requires": {
+            "amdefine": "1.0.1"
+          }
+        }
+      }
+    },
+    "cli-cursor": {
+      "version": "1.0.2",
+      "resolved": "http://registry.npm.taobao.org/cli-cursor/download/cli-cursor-1.0.2.tgz",
+      "integrity": "sha1-ZNo/fValRBLll5S9Ytw1KV6PKYc=",
+      "dev": true,
+      "requires": {
+        "restore-cursor": "1.0.1"
+      }
+    },
+    "cli-width": {
+      "version": "2.2.0",
+      "resolved": "http://registry.npm.taobao.org/cli-width/download/cli-width-2.2.0.tgz",
+      "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=",
+      "dev": true
+    },
+    "cliui": {
+      "version": "2.1.0",
+      "resolved": "http://registry.npm.taobao.org/cliui/download/cliui-2.1.0.tgz",
+      "integrity": "sha1-S0dXYP+AJkx2LDoXGQMukcf+oNE=",
+      "dev": true,
+      "requires": {
+        "center-align": "0.1.3",
+        "right-align": "0.1.3",
+        "wordwrap": "0.0.2"
+      },
+      "dependencies": {
+        "wordwrap": {
+          "version": "0.0.2",
+          "resolved": "http://registry.npm.taobao.org/wordwrap/download/wordwrap-0.0.2.tgz",
+          "integrity": "sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8=",
+          "dev": true
+        }
+      }
+    },
+    "clone": {
+      "version": "1.0.3",
+      "resolved": "http://registry.npm.taobao.org/clone/download/clone-1.0.3.tgz",
+      "integrity": "sha1-KY1+IjFmD0DAA8LtMUDezz9TCF8=",
+      "dev": true
+    },
+    "co": {
+      "version": "4.6.0",
+      "resolved": "http://registry.npm.taobao.org/co/download/co-4.6.0.tgz",
+      "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=",
+      "dev": true
+    },
+    "code-point-at": {
+      "version": "1.1.0",
+      "resolved": "http://registry.npm.taobao.org/code-point-at/download/code-point-at-1.1.0.tgz",
+      "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=",
+      "dev": true
+    },
+    "combined-stream": {
+      "version": "1.0.5",
+      "resolved": "http://registry.npm.taobao.org/combined-stream/download/combined-stream-1.0.5.tgz",
+      "integrity": "sha1-k4NwpXtKUd6ix3wV1cX9+JUWQAk=",
+      "dev": true,
+      "requires": {
+        "delayed-stream": "1.0.0"
+      }
+    },
+    "command-line-args": {
+      "version": "3.0.5",
+      "resolved": "http://registry.npm.taobao.org/command-line-args/download/command-line-args-3.0.5.tgz",
+      "integrity": "sha1-W9StReeYPlwTRJGOQCgO4mk8WsA=",
+      "dev": true,
+      "requires": {
+        "array-back": "1.0.4",
+        "feature-detect-es6": "1.4.0",
+        "find-replace": "1.0.3",
+        "typical": "2.6.1"
+      }
+    },
+    "command-line-commands": {
+      "version": "1.0.4",
+      "resolved": "http://registry.npm.taobao.org/command-line-commands/download/command-line-commands-1.0.4.tgz",
+      "integrity": "sha1-A0+bFntRiK+9z2su+7FQ/IRCwys=",
+      "dev": true,
+      "requires": {
+        "array-back": "1.0.4",
+        "feature-detect-es6": "1.4.0"
+      }
+    },
+    "commander": {
+      "version": "2.12.2",
+      "resolved": "http://registry.npm.taobao.org/commander/download/commander-2.12.2.tgz",
+      "integrity": "sha1-D1lGxCftnsDZGka7ne9T5UZQ5VU=",
+      "dev": true
+    },
+    "commondir": {
+      "version": "1.0.1",
+      "resolved": "http://registry.npm.taobao.org/commondir/download/commondir-1.0.1.tgz",
+      "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=",
+      "dev": true
+    },
+    "commonmark": {
+      "version": "0.24.0",
+      "resolved": "http://registry.npm.taobao.org/commonmark/download/commonmark-0.24.0.tgz",
+      "integrity": "sha1-uA3gGCxUY1VkOqFdsSv7KCNoJ48=",
+      "requires": {
+        "entities": "1.1.1",
+        "mdurl": "1.0.1",
+        "string.prototype.repeat": "0.2.0"
+      }
+    },
+    "commonmark-react-renderer": {
+      "version": "4.3.4",
+      "resolved": "http://registry.npm.taobao.org/commonmark-react-renderer/download/commonmark-react-renderer-4.3.4.tgz",
+      "integrity": "sha1-KfNFNXlRqzbrOG1F6mvAgAbz/5s=",
+      "requires": {
+        "lodash.assign": "4.2.0",
+        "lodash.isplainobject": "4.0.6",
+        "pascalcase": "0.1.1",
+        "xss-filters": "1.2.7"
+      }
+    },
+    "compress-commons": {
+      "version": "1.2.2",
+      "resolved": "http://registry.npm.taobao.org/compress-commons/download/compress-commons-1.2.2.tgz",
+      "integrity": "sha1-UkqfEJA/OoEzibAiXSfEi7dRiQ8=",
+      "dev": true,
+      "requires": {
+        "buffer-crc32": "0.2.13",
+        "crc32-stream": "2.0.0",
+        "normalize-path": "2.1.1",
+        "readable-stream": "2.3.3"
+      }
+    },
+    "concat-map": {
+      "version": "0.0.1",
+      "resolved": "http://registry.npm.taobao.org/concat-map/download/concat-map-0.0.1.tgz",
+      "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
+    },
+    "concat-stream": {
+      "version": "1.6.0",
+      "resolved": "http://registry.npm.taobao.org/concat-stream/download/concat-stream-1.6.0.tgz",
+      "integrity": "sha1-CqxmL9Ur54lk1VMvaUeE5wEQrPc=",
+      "dev": true,
+      "requires": {
+        "inherits": "2.0.3",
+        "readable-stream": "2.3.3",
+        "typedarray": "0.0.6"
+      }
+    },
+    "connect-history-api-fallback": {
+      "version": "1.5.0",
+      "resolved": "http://registry.npm.taobao.org/connect-history-api-fallback/download/connect-history-api-fallback-1.5.0.tgz",
+      "integrity": "sha1-sGhzk0vF40T+9hGhlqb6rgruAVo=",
+      "dev": true
+    },
+    "console-browserify": {
+      "version": "1.1.0",
+      "resolved": "http://registry.npm.taobao.org/console-browserify/download/console-browserify-1.1.0.tgz",
+      "integrity": "sha1-8CQcRXMKn8YyOyBtvzjtx0HQuxA=",
+      "dev": true,
+      "requires": {
+        "date-now": "0.1.4"
+      }
+    },
+    "console-control-strings": {
+      "version": "1.1.0",
+      "resolved": "http://registry.npm.taobao.org/console-control-strings/download/console-control-strings-1.1.0.tgz",
+      "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=",
+      "dev": true
+    },
+    "constants-browserify": {
+      "version": "1.0.0",
+      "resolved": "http://registry.npm.taobao.org/constants-browserify/download/constants-browserify-1.0.0.tgz",
+      "integrity": "sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U=",
+      "dev": true
+    },
+    "contains-path": {
+      "version": "0.1.0",
+      "resolved": "http://registry.npm.taobao.org/contains-path/download/contains-path-0.1.0.tgz",
+      "integrity": "sha1-/ozxhP9mcLa67wGp1IYaXL7EEgo=",
+      "dev": true
+    },
+    "content-disposition": {
+      "version": "0.5.2",
+      "resolved": "http://registry.npm.taobao.org/content-disposition/download/content-disposition-0.5.2.tgz",
+      "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ=",
+      "dev": true
+    },
+    "content-type": {
+      "version": "1.0.4",
+      "resolved": "http://registry.npm.taobao.org/content-type/download/content-type-1.0.4.tgz",
+      "integrity": "sha1-4TjMdeBAxyexlm/l5fjJruJW/js=",
+      "dev": true
+    },
+    "convert-source-map": {
+      "version": "1.5.1",
+      "resolved": "http://registry.npm.taobao.org/convert-source-map/download/convert-source-map-1.5.1.tgz",
+      "integrity": "sha1-uCeAl7m8IpNl3lxiz1/K7YtVmeU="
+    },
+    "cookie": {
+      "version": "0.3.1",
+      "resolved": "http://registry.npm.taobao.org/cookie/download/cookie-0.3.1.tgz",
+      "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=",
+      "dev": true
+    },
+    "cookie-signature": {
+      "version": "1.0.6",
+      "resolved": "http://registry.npm.taobao.org/cookie-signature/download/cookie-signature-1.0.6.tgz",
+      "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=",
+      "dev": true
+    },
+    "core-js": {
+      "version": "2.5.3",
+      "resolved": "http://registry.npm.taobao.org/core-js/download/core-js-2.5.3.tgz",
+      "integrity": "sha1-isw4NFgk8W2DZbfJtCWRaOjtYD4="
+    },
+    "core-util-is": {
+      "version": "1.0.2",
+      "resolved": "http://registry.npm.taobao.org/core-util-is/download/core-util-is-1.0.2.tgz",
+      "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=",
+      "dev": true
+    },
+    "cosmiconfig": {
+      "version": "2.2.2",
+      "resolved": "http://registry.npm.taobao.org/cosmiconfig/download/cosmiconfig-2.2.2.tgz",
+      "integrity": "sha1-YXPOvVb6wELB9DkO33r2wHx8uJI=",
+      "dev": true,
+      "requires": {
+        "is-directory": "0.3.1",
+        "js-yaml": "3.10.0",
+        "minimist": "1.2.0",
+        "object-assign": "4.1.1",
+        "os-homedir": "1.0.2",
+        "parse-json": "2.2.0",
+        "require-from-string": "1.2.1"
+      },
+      "dependencies": {
+        "minimist": {
+          "version": "1.2.0",
+          "resolved": "http://registry.npm.taobao.org/minimist/download/minimist-1.2.0.tgz",
+          "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
+          "dev": true
+        }
+      }
+    },
+    "crc": {
+      "version": "3.5.0",
+      "resolved": "http://registry.npm.taobao.org/crc/download/crc-3.5.0.tgz",
+      "integrity": "sha1-mLi6fUiWZbo5efWbITgTdBAaGWQ=",
+      "dev": true
+    },
+    "crc32-stream": {
+      "version": "2.0.0",
+      "resolved": "http://registry.npm.taobao.org/crc32-stream/download/crc32-stream-2.0.0.tgz",
+      "integrity": "sha1-483TtN8xaN10494/u8t7KX/pCPQ=",
+      "dev": true,
+      "requires": {
+        "crc": "3.5.0",
+        "readable-stream": "2.3.3"
+      }
+    },
+    "create-react-class": {
+      "version": "15.6.2",
+      "resolved": "http://registry.npm.taobao.org/create-react-class/download/create-react-class-15.6.2.tgz",
+      "integrity": "sha1-zx7RXxKq1/FO9fLf4F5sQvke8Co=",
+      "requires": {
+        "fbjs": "0.8.16",
+        "loose-envify": "1.3.1",
+        "object-assign": "4.1.1"
+      }
+    },
+    "cross-spawn": {
+      "version": "3.0.1",
+      "resolved": "http://registry.npm.taobao.org/cross-spawn/download/cross-spawn-3.0.1.tgz",
+      "integrity": "sha1-ElYDfsufDF9549bvE14wdwGEuYI=",
+      "dev": true,
+      "requires": {
+        "lru-cache": "4.1.1",
+        "which": "1.3.0"
+      },
+      "dependencies": {
+        "lru-cache": {
+          "version": "4.1.1",
+          "resolved": "http://registry.npm.taobao.org/lru-cache/download/lru-cache-4.1.1.tgz",
+          "integrity": "sha1-Yi4y6CSItJJ5EUpPns9F581rulU=",
+          "dev": true,
+          "requires": {
+            "pseudomap": "1.0.2",
+            "yallist": "2.1.2"
+          }
+        }
+      }
+    },
+    "cryptiles": {
+      "version": "3.1.2",
+      "resolved": "http://registry.npm.taobao.org/cryptiles/download/cryptiles-3.1.2.tgz",
+      "integrity": "sha1-qJ+7Ig9c4l7FboxKqKT9e1sNKf4=",
+      "dev": true,
+      "requires": {
+        "boom": "5.2.0"
+      },
+      "dependencies": {
+        "boom": {
+          "version": "5.2.0",
+          "resolved": "http://registry.npm.taobao.org/boom/download/boom-5.2.0.tgz",
+          "integrity": "sha1-XdnabuOl8wIHdDYpDLcX0/SlTgI=",
+          "dev": true,
+          "requires": {
+            "hoek": "4.2.0"
+          }
+        }
+      }
+    },
+    "crypto-browserify": {
+      "version": "3.3.0",
+      "resolved": "http://registry.npm.taobao.org/crypto-browserify/download/crypto-browserify-3.3.0.tgz",
+      "integrity": "sha1-ufx1u0oO1h3PHNXa6W6zDJw+UGw=",
+      "dev": true,
+      "requires": {
+        "browserify-aes": "0.4.0",
+        "pbkdf2-compat": "2.0.1",
+        "ripemd160": "0.2.0",
+        "sha.js": "2.2.6"
+      },
+      "dependencies": {
+        "sha.js": {
+          "version": "2.2.6",
+          "resolved": "http://registry.npm.taobao.org/sha.js/download/sha.js-2.2.6.tgz",
+          "integrity": "sha1-F93t3F9yL7ZlAWWIlUYZd4ZzFbo=",
+          "dev": true
+        }
+      }
+    },
+    "css": {
+      "version": "2.2.1",
+      "resolved": "http://registry.npm.taobao.org/css/download/css-2.2.1.tgz",
+      "integrity": "sha1-c6TIHehdtmTU7mdPfUcIXjstVdw=",
+      "dev": true,
+      "requires": {
+        "inherits": "2.0.3",
+        "source-map": "0.1.43",
+        "source-map-resolve": "0.3.1",
+        "urix": "0.1.0"
+      },
+      "dependencies": {
+        "source-map": {
+          "version": "0.1.43",
+          "resolved": "http://registry.npm.taobao.org/source-map/download/source-map-0.1.43.tgz",
+          "integrity": "sha1-wkvBRspRfBRx9drL4lcbK3+eM0Y=",
+          "dev": true,
+          "requires": {
+            "amdefine": "1.0.1"
+          }
+        }
+      }
+    },
+    "css-loader": {
+      "version": "0.14.5",
+      "resolved": "http://registry.npm.taobao.org/css-loader/download/css-loader-0.14.5.tgz",
+      "integrity": "sha1-1lY1tyrcSHrIGKLni1u5/spTUq0=",
+      "dev": true,
+      "requires": {
+        "clean-css": "3.4.28",
+        "fastparse": "1.1.1",
+        "loader-utils": "0.2.17",
+        "source-list-map": "0.1.8"
+      }
+    },
+    "css-parse": {
+      "version": "2.0.0",
+      "resolved": "http://registry.npm.taobao.org/css-parse/download/css-parse-2.0.0.tgz",
+      "integrity": "sha1-pGjuZnwW2BzPBcWMONKpfHgNv9Q=",
+      "dev": true,
+      "requires": {
+        "css": "2.2.1"
+      }
+    },
+    "css-value": {
+      "version": "0.0.1",
+      "resolved": "http://registry.npm.taobao.org/css-value/download/css-value-0.0.1.tgz",
+      "integrity": "sha1-Xv1sLupeof1rasV+wEJ7GEUkJOo=",
+      "dev": true
+    },
+    "currently-unhandled": {
+      "version": "0.4.1",
+      "resolved": "http://registry.npm.taobao.org/currently-unhandled/download/currently-unhandled-0.4.1.tgz",
+      "integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=",
+      "dev": true,
+      "requires": {
+        "array-find-index": "1.0.2"
+      }
+    },
+    "d": {
+      "version": "1.0.0",
+      "resolved": "http://registry.npm.taobao.org/d/download/d-1.0.0.tgz",
+      "integrity": "sha1-dUu1v+VUUdpppYuU1F9MWwRi1Y8=",
+      "dev": true,
+      "requires": {
+        "es5-ext": "0.10.37"
+      }
+    },
+    "dashdash": {
+      "version": "1.14.1",
+      "resolved": "http://registry.npm.taobao.org/dashdash/download/dashdash-1.14.1.tgz",
+      "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=",
+      "dev": true,
+      "requires": {
+        "assert-plus": "1.0.0"
+      }
+    },
+    "date-now": {
+      "version": "0.1.4",
+      "resolved": "http://registry.npm.taobao.org/date-now/download/date-now-0.1.4.tgz",
+      "integrity": "sha1-6vQ5/U1ISK105cx9vvIAZyueNFs=",
+      "dev": true
+    },
+    "debug": {
+      "version": "2.6.9",
+      "resolved": "http://registry.npm.taobao.org/debug/download/debug-2.6.9.tgz",
+      "integrity": "sha1-XRKFFd8TT/Mn6QpMk/Tgd6U2NB8=",
+      "requires": {
+        "ms": "2.0.0"
+      }
+    },
+    "decamelize": {
+      "version": "1.2.0",
+      "resolved": "http://registry.npm.taobao.org/decamelize/download/decamelize-1.2.0.tgz",
+      "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=",
+      "dev": true
+    },
+    "deep-eql": {
+      "version": "0.1.3",
+      "resolved": "http://registry.npm.taobao.org/deep-eql/download/deep-eql-0.1.3.tgz",
+      "integrity": "sha1-71WKyrjeJSBs1xOQbXTlaTDrafI=",
+      "dev": true,
+      "requires": {
+        "type-detect": "0.1.1"
+      },
+      "dependencies": {
+        "type-detect": {
+          "version": "0.1.1",
+          "resolved": "http://registry.npm.taobao.org/type-detect/download/type-detect-0.1.1.tgz",
+          "integrity": "sha1-C6XsKohWQORw6k6FBZcZANrFiCI=",
+          "dev": true
+        }
+      }
+    },
+    "deep-equal": {
+      "version": "1.0.1",
+      "resolved": "http://registry.npm.taobao.org/deep-equal/download/deep-equal-1.0.1.tgz",
+      "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU="
+    },
+    "deep-is": {
+      "version": "0.1.3",
+      "resolved": "http://registry.npm.taobao.org/deep-is/download/deep-is-0.1.3.tgz",
+      "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=",
+      "dev": true
+    },
+    "deepmerge": {
+      "version": "0.2.10",
+      "resolved": "http://registry.npm.taobao.org/deepmerge/download/deepmerge-0.2.10.tgz",
+      "integrity": "sha1-iQa/nlJaT78bIDsq/LRkAkmCEhk=",
+      "dev": true
+    },
+    "define-properties": {
+      "version": "1.1.2",
+      "resolved": "http://registry.npm.taobao.org/define-properties/download/define-properties-1.1.2.tgz",
+      "integrity": "sha1-g6c/L+pWmJj7c3GTyPhzyvbUXJQ=",
+      "dev": true,
+      "requires": {
+        "foreach": "2.0.5",
+        "object-keys": "1.0.11"
+      }
+    },
+    "del": {
+      "version": "2.2.2",
+      "resolved": "http://registry.npm.taobao.org/del/download/del-2.2.2.tgz",
+      "integrity": "sha1-wSyYHQZ4RshLyvhiz/kw2Qf/0ag=",
+      "dev": true,
+      "requires": {
+        "globby": "5.0.0",
+        "is-path-cwd": "1.0.0",
+        "is-path-in-cwd": "1.0.0",
+        "object-assign": "4.1.1",
+        "pify": "2.3.0",
+        "pinkie-promise": "2.0.1",
+        "rimraf": "2.6.2"
+      }
+    },
+    "delayed-stream": {
+      "version": "1.0.0",
+      "resolved": "http://registry.npm.taobao.org/delayed-stream/download/delayed-stream-1.0.0.tgz",
+      "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=",
+      "dev": true
+    },
+    "delegates": {
+      "version": "1.0.0",
+      "resolved": "http://registry.npm.taobao.org/delegates/download/delegates-1.0.0.tgz",
+      "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=",
+      "dev": true
+    },
+    "depd": {
+      "version": "1.1.1",
+      "resolved": "http://registry.npm.taobao.org/depd/download/depd-1.1.1.tgz",
+      "integrity": "sha1-V4O04cRZ8G+lyif5kfPQbnoxA1k=",
+      "dev": true
+    },
+    "destroy": {
+      "version": "1.0.4",
+      "resolved": "http://registry.npm.taobao.org/destroy/download/destroy-1.0.4.tgz",
+      "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=",
+      "dev": true
+    },
+    "detect-indent": {
+      "version": "4.0.0",
+      "resolved": "http://registry.npm.taobao.org/detect-indent/download/detect-indent-4.0.0.tgz",
+      "integrity": "sha1-920GQ1LN9Docts5hnE7jqUdd4gg=",
+      "requires": {
+        "repeating": "2.0.1"
+      }
+    },
+    "diff": {
+      "version": "1.4.0",
+      "resolved": "http://registry.npm.taobao.org/diff/download/diff-1.4.0.tgz",
+      "integrity": "sha1-fyjS657nsVqX79ic5j3P2qPMur8=",
+      "dev": true
+    },
+    "doctrine": {
+      "version": "1.5.0",
+      "resolved": "http://registry.npm.taobao.org/doctrine/download/doctrine-1.5.0.tgz",
+      "integrity": "sha1-N53Ocw9hZvds76TmcHoVmwLFpvo=",
+      "dev": true,
+      "requires": {
+        "esutils": "2.0.2",
+        "isarray": "1.0.0"
+      }
+    },
+    "dom-helpers": {
+      "version": "3.3.1",
+      "resolved": "http://registry.npm.taobao.org/dom-helpers/download/dom-helpers-3.3.1.tgz",
+      "integrity": "sha1-/BpOFf/fYN3eA6SAqcD+zoId1KY="
+    },
+    "dom-walk": {
+      "version": "0.1.1",
+      "resolved": "http://registry.npm.taobao.org/dom-walk/download/dom-walk-0.1.1.tgz",
+      "integrity": "sha1-ZyIm3HTI95mtNTB9+TaroRrNYBg=",
+      "dev": true
+    },
+    "domain-browser": {
+      "version": "1.1.7",
+      "resolved": "http://registry.npm.taobao.org/domain-browser/download/domain-browser-1.1.7.tgz",
+      "integrity": "sha1-hnqksJP6oF8d4IwG9NeyH9+GmLw=",
+      "dev": true
+    },
+    "ecc-jsbn": {
+      "version": "0.1.1",
+      "resolved": "http://registry.npm.taobao.org/ecc-jsbn/download/ecc-jsbn-0.1.1.tgz",
+      "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=",
+      "dev": true,
+      "optional": true,
+      "requires": {
+        "jsbn": "0.1.1"
+      }
+    },
+    "ee-first": {
+      "version": "1.1.1",
+      "resolved": "http://registry.npm.taobao.org/ee-first/download/ee-first-1.1.1.tgz",
+      "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=",
+      "dev": true
+    },
+    "ejs": {
+      "version": "2.5.7",
+      "resolved": "http://registry.npm.taobao.org/ejs/download/ejs-2.5.7.tgz",
+      "integrity": "sha1-zIcsFoiArjxxiXYv1f/ACJbJUYo=",
+      "dev": true
+    },
+    "electron-releases": {
+      "version": "2.1.0",
+      "resolved": "http://registry.npm.taobao.org/electron-releases/download/electron-releases-2.1.0.tgz",
+      "integrity": "sha1-xWFL+BHxds48g242igYleCNB/U4=",
+      "dev": true
+    },
+    "electron-to-chromium": {
+      "version": "1.3.30",
+      "resolved": "http://registry.npm.taobao.org/electron-to-chromium/download/electron-to-chromium-1.3.30.tgz",
+      "integrity": "sha1-lmb1MqZFhmUfxWpyUTaS6CDQaoA=",
+      "dev": true,
+      "requires": {
+        "electron-releases": "2.1.0"
+      }
+    },
+    "emojis-list": {
+      "version": "2.1.0",
+      "resolved": "http://registry.npm.taobao.org/emojis-list/download/emojis-list-2.1.0.tgz",
+      "integrity": "sha1-TapNnbAPmBmIDHn6RXrlsJof04k=",
+      "dev": true
+    },
+    "encodeurl": {
+      "version": "1.0.1",
+      "resolved": "http://registry.npm.taobao.org/encodeurl/download/encodeurl-1.0.1.tgz",
+      "integrity": "sha1-eePVhlU0aQn+bw9Fpd5oEDspTSA=",
+      "dev": true
+    },
+    "encoding": {
+      "version": "0.1.12",
+      "resolved": "http://registry.npm.taobao.org/encoding/download/encoding-0.1.12.tgz",
+      "integrity": "sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=",
+      "requires": {
+        "iconv-lite": "0.4.19"
+      }
+    },
+    "end-of-stream": {
+      "version": "1.4.0",
+      "resolved": "http://registry.npm.taobao.org/end-of-stream/download/end-of-stream-1.4.0.tgz",
+      "integrity": "sha1-epDYM+/abPpurA9JSduw+tOmMgY=",
+      "dev": true,
+      "requires": {
+        "once": "1.4.0"
+      }
+    },
+    "enhanced-resolve": {
+      "version": "0.9.1",
+      "resolved": "http://registry.npm.taobao.org/enhanced-resolve/download/enhanced-resolve-0.9.1.tgz",
+      "integrity": "sha1-TW5omzcl+GCQknzMhs2fFjW4ni4=",
+      "dev": true,
+      "requires": {
+        "graceful-fs": "4.1.11",
+        "memory-fs": "0.2.0",
+        "tapable": "0.1.10"
+      },
+      "dependencies": {
+        "memory-fs": {
+          "version": "0.2.0",
+          "resolved": "http://registry.npm.taobao.org/memory-fs/download/memory-fs-0.2.0.tgz",
+          "integrity": "sha1-8rslNovBIeORwlIN6Slpyu4KApA=",
+          "dev": true
+        }
+      }
+    },
+    "entities": {
+      "version": "1.1.1",
+      "resolved": "http://registry.npm.taobao.org/entities/download/entities-1.1.1.tgz",
+      "integrity": "sha1-blwtClYhtdra7O+AuQ7ftc13cvA="
+    },
+    "errno": {
+      "version": "0.1.6",
+      "resolved": "http://registry.npm.taobao.org/errno/download/errno-0.1.6.tgz",
+      "integrity": "sha1-w4bOimKD8U/AlWO3FWCQjJv1MCY=",
+      "dev": true,
+      "requires": {
+        "prr": "1.0.1"
+      }
+    },
+    "error-ex": {
+      "version": "1.3.1",
+      "resolved": "http://registry.npm.taobao.org/error-ex/download/error-ex-1.3.1.tgz",
+      "integrity": "sha1-+FWobOYa3E6GIcPNoh56dhLDqNw=",
+      "dev": true,
+      "requires": {
+        "is-arrayish": "0.2.1"
+      }
+    },
+    "error-stack-parser": {
+      "version": "1.3.6",
+      "resolved": "http://registry.npm.taobao.org/error-stack-parser/download/error-stack-parser-1.3.6.tgz",
+      "integrity": "sha1-4Oc7k+QXE40c18C3RrGkoUhUwpI=",
+      "dev": true,
+      "requires": {
+        "stackframe": "0.3.1"
+      }
+    },
+    "es5-ext": {
+      "version": "0.10.37",
+      "resolved": "http://registry.npm.taobao.org/es5-ext/download/es5-ext-0.10.37.tgz",
+      "integrity": "sha1-DudB0Ui4AGm6J9AgOTdWryV978M=",
+      "dev": true,
+      "requires": {
+        "es6-iterator": "2.0.3",
+        "es6-symbol": "3.1.1"
+      }
+    },
+    "es6-iterator": {
+      "version": "2.0.3",
+      "resolved": "http://registry.npm.taobao.org/es6-iterator/download/es6-iterator-2.0.3.tgz",
+      "integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=",
+      "dev": true,
+      "requires": {
+        "d": "1.0.0",
+        "es5-ext": "0.10.37",
+        "es6-symbol": "3.1.1"
+      }
+    },
+    "es6-map": {
+      "version": "0.1.5",
+      "resolved": "http://registry.npm.taobao.org/es6-map/download/es6-map-0.1.5.tgz",
+      "integrity": "sha1-kTbgUD3MBqMBaQ8LsU/042TpSfA=",
+      "dev": true,
+      "requires": {
+        "d": "1.0.0",
+        "es5-ext": "0.10.37",
+        "es6-iterator": "2.0.3",
+        "es6-set": "0.1.5",
+        "es6-symbol": "3.1.1",
+        "event-emitter": "0.3.5"
+      }
+    },
+    "es6-set": {
+      "version": "0.1.5",
+      "resolved": "http://registry.npm.taobao.org/es6-set/download/es6-set-0.1.5.tgz",
+      "integrity": "sha1-0rPsXU2ADO2BjbU40ol02wpzzLE=",
+      "dev": true,
+      "requires": {
+        "d": "1.0.0",
+        "es5-ext": "0.10.37",
+        "es6-iterator": "2.0.3",
+        "es6-symbol": "3.1.1",
+        "event-emitter": "0.3.5"
+      }
+    },
+    "es6-symbol": {
+      "version": "3.1.1",
+      "resolved": "http://registry.npm.taobao.org/es6-symbol/download/es6-symbol-3.1.1.tgz",
+      "integrity": "sha1-vwDvT9q2uhtG7Le2KbTH7VcVzHc=",
+      "dev": true,
+      "requires": {
+        "d": "1.0.0",
+        "es5-ext": "0.10.37"
+      }
+    },
+    "es6-weak-map": {
+      "version": "2.0.2",
+      "resolved": "http://registry.npm.taobao.org/es6-weak-map/download/es6-weak-map-2.0.2.tgz",
+      "integrity": "sha1-XjqzIlH/0VOKH45f+hNXdy+S2W8=",
+      "dev": true,
+      "requires": {
+        "d": "1.0.0",
+        "es5-ext": "0.10.37",
+        "es6-iterator": "2.0.3",
+        "es6-symbol": "3.1.1"
+      }
+    },
+    "escape-html": {
+      "version": "1.0.3",
+      "resolved": "http://registry.npm.taobao.org/escape-html/download/escape-html-1.0.3.tgz",
+      "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=",
+      "dev": true
+    },
+    "escape-regexp": {
+      "version": "0.0.1",
+      "resolved": "http://registry.npm.taobao.org/escape-regexp/download/escape-regexp-0.0.1.tgz",
+      "integrity": "sha1-9EvaEtRbvfnLf4Yu5+SCez3TIlQ=",
+      "dev": true
+    },
+    "escape-string-regexp": {
+      "version": "1.0.5",
+      "resolved": "http://registry.npm.taobao.org/escape-string-regexp/download/escape-string-regexp-1.0.5.tgz",
+      "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ="
+    },
+    "escope": {
+      "version": "3.6.0",
+      "resolved": "http://registry.npm.taobao.org/escope/download/escope-3.6.0.tgz",
+      "integrity": "sha1-4Bl16BJ4GhY6ba392AOY3GTIicM=",
+      "dev": true,
+      "requires": {
+        "es6-map": "0.1.5",
+        "es6-weak-map": "2.0.2",
+        "esrecurse": "4.2.0",
+        "estraverse": "4.2.0"
+      }
+    },
+    "eslint": {
+      "version": "3.7.1",
+      "resolved": "http://registry.npm.taobao.org/eslint/download/eslint-3.7.1.tgz",
+      "integrity": "sha1-f6qEWZ4P6kIvBLwy20kFQFGj8Ro=",
+      "dev": true,
+      "requires": {
+        "chalk": "1.1.3",
+        "concat-stream": "1.6.0",
+        "debug": "2.6.9",
+        "doctrine": "1.5.0",
+        "escope": "3.6.0",
+        "espree": "3.5.2",
+        "estraverse": "4.2.0",
+        "esutils": "2.0.2",
+        "file-entry-cache": "2.0.0",
+        "glob": "7.1.2",
+        "globals": "9.18.0",
+        "ignore": "3.3.7",
+        "imurmurhash": "0.1.4",
+        "inquirer": "0.12.0",
+        "is-my-json-valid": "2.17.1",
+        "is-resolvable": "1.0.1",
+        "js-yaml": "3.10.0",
+        "json-stable-stringify": "1.0.1",
+        "levn": "0.3.0",
+        "lodash": "4.17.4",
+        "mkdirp": "0.5.1",
+        "natural-compare": "1.4.0",
+        "optionator": "0.8.2",
+        "path-is-inside": "1.0.2",
+        "pluralize": "1.2.1",
+        "progress": "1.1.8",
+        "require-uncached": "1.0.3",
+        "shelljs": "0.6.1",
+        "strip-bom": "3.0.0",
+        "strip-json-comments": "1.0.4",
+        "table": "3.8.3",
+        "text-table": "0.2.0",
+        "user-home": "2.0.0"
+      },
+      "dependencies": {
+        "glob": {
+          "version": "7.1.2",
+          "resolved": "http://registry.npm.taobao.org/glob/download/glob-7.1.2.tgz",
+          "integrity": "sha1-wZyd+aAocC1nhhI4SmVSQExjbRU=",
+          "dev": true,
+          "requires": {
+            "fs.realpath": "1.0.0",
+            "inflight": "1.0.6",
+            "inherits": "2.0.3",
+            "minimatch": "3.0.4",
+            "once": "1.4.0",
+            "path-is-absolute": "1.0.1"
+          }
+        },
+        "pluralize": {
+          "version": "1.2.1",
+          "resolved": "http://registry.npm.taobao.org/pluralize/download/pluralize-1.2.1.tgz",
+          "integrity": "sha1-0aIUg/0iu0HlihL6NCGCMUCJfEU=",
+          "dev": true
+        },
+        "shelljs": {
+          "version": "0.6.1",
+          "resolved": "http://registry.npm.taobao.org/shelljs/download/shelljs-0.6.1.tgz",
+          "integrity": "sha1-7GIRvtGSBEIIj+D3Cyg3Iy7SyKg=",
+          "dev": true
+        },
+        "strip-bom": {
+          "version": "3.0.0",
+          "resolved": "http://registry.npm.taobao.org/strip-bom/download/strip-bom-3.0.0.tgz",
+          "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=",
+          "dev": true
+        },
+        "user-home": {
+          "version": "2.0.0",
+          "resolved": "http://registry.npm.taobao.org/user-home/download/user-home-2.0.0.tgz",
+          "integrity": "sha1-nHC/2Babwdy/SGBODwS4tJzenp8=",
+          "dev": true,
+          "requires": {
+            "os-homedir": "1.0.2"
+          }
+        }
+      }
+    },
+    "eslint-plugin-react": {
+      "version": "6.4.1",
+      "resolved": "http://registry.npm.taobao.org/eslint-plugin-react/download/eslint-plugin-react-6.4.1.tgz",
+      "integrity": "sha1-fRqt50fbFYkvce7h/qSt35e8+is=",
+      "dev": true,
+      "requires": {
+        "doctrine": "1.5.0",
+        "jsx-ast-utils": "1.4.1"
+      }
+    },
+    "espree": {
+      "version": "3.5.2",
+      "resolved": "http://registry.npm.taobao.org/espree/download/espree-3.5.2.tgz",
+      "integrity": "sha1-dWrai5eenc/NswqtjRqTBKkF4co=",
+      "dev": true,
+      "requires": {
+        "acorn": "5.2.1",
+        "acorn-jsx": "3.0.1"
+      }
+    },
+    "esprima": {
+      "version": "4.0.0",
+      "resolved": "http://registry.npm.taobao.org/esprima/download/esprima-4.0.0.tgz",
+      "integrity": "sha1-RJnt3NERDgshi6zy+n9/WfVcqAQ=",
+      "dev": true
+    },
+    "esrecurse": {
+      "version": "4.2.0",
+      "resolved": "http://registry.npm.taobao.org/esrecurse/download/esrecurse-4.2.0.tgz",
+      "integrity": "sha1-+pVo2Y04I/mkHZHpAtyrnqblsWM=",
+      "dev": true,
+      "requires": {
+        "estraverse": "4.2.0",
+        "object-assign": "4.1.1"
+      }
+    },
+    "estraverse": {
+      "version": "4.2.0",
+      "resolved": "http://registry.npm.taobao.org/estraverse/download/estraverse-4.2.0.tgz",
+      "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=",
+      "dev": true
+    },
+    "esutils": {
+      "version": "2.0.2",
+      "resolved": "http://registry.npm.taobao.org/esutils/download/esutils-2.0.2.tgz",
+      "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs="
+    },
+    "etag": {
+      "version": "1.8.1",
+      "resolved": "http://registry.npm.taobao.org/etag/download/etag-1.8.1.tgz",
+      "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=",
+      "dev": true
+    },
+    "event-emitter": {
+      "version": "0.3.5",
+      "resolved": "http://registry.npm.taobao.org/event-emitter/download/event-emitter-0.3.5.tgz",
+      "integrity": "sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk=",
+      "dev": true,
+      "requires": {
+        "d": "1.0.0",
+        "es5-ext": "0.10.37"
+      }
+    },
+    "eventemitter3": {
+      "version": "1.2.0",
+      "resolved": "http://registry.npm.taobao.org/eventemitter3/download/eventemitter3-1.2.0.tgz",
+      "integrity": "sha1-HIaZHYFq0eUEdQ5zh0Ik7PO+xQg=",
+      "dev": true
+    },
+    "events": {
+      "version": "1.1.1",
+      "resolved": "http://registry.npm.taobao.org/events/download/events-1.1.1.tgz",
+      "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=",
+      "dev": true
+    },
+    "exit-hook": {
+      "version": "1.1.1",
+      "resolved": "http://registry.npm.taobao.org/exit-hook/download/exit-hook-1.1.1.tgz",
+      "integrity": "sha1-8FyiM7SMBdVP/wd2XfhQfpXAL/g=",
+      "dev": true
+    },
+    "expand-brackets": {
+      "version": "0.1.5",
+      "resolved": "http://registry.npm.taobao.org/expand-brackets/download/expand-brackets-0.1.5.tgz",
+      "integrity": "sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s=",
+      "dev": true,
+      "requires": {
+        "is-posix-bracket": "0.1.1"
+      }
+    },
+    "expand-range": {
+      "version": "1.8.2",
+      "resolved": "http://registry.npm.taobao.org/expand-range/download/expand-range-1.8.2.tgz",
+      "integrity": "sha1-opnv/TNf4nIeuujiV+x5ZE/IUzc=",
+      "dev": true,
+      "requires": {
+        "fill-range": "2.2.3"
+      }
+    },
+    "express": {
+      "version": "4.16.2",
+      "resolved": "http://registry.npm.taobao.org/express/download/express-4.16.2.tgz",
+      "integrity": "sha1-41xt/i1kt9ygpc1PIXgb4ymeB2w=",
+      "dev": true,
+      "requires": {
+        "accepts": "1.3.4",
+        "array-flatten": "1.1.1",
+        "body-parser": "1.18.2",
+        "content-disposition": "0.5.2",
+        "content-type": "1.0.4",
+        "cookie": "0.3.1",
+        "cookie-signature": "1.0.6",
+        "debug": "2.6.9",
+        "depd": "1.1.1",
+        "encodeurl": "1.0.1",
+        "escape-html": "1.0.3",
+        "etag": "1.8.1",
+        "finalhandler": "1.1.0",
+        "fresh": "0.5.2",
+        "merge-descriptors": "1.0.1",
+        "methods": "1.1.2",
+        "on-finished": "2.3.0",
+        "parseurl": "1.3.2",
+        "path-to-regexp": "0.1.7",
+        "proxy-addr": "2.0.2",
+        "qs": "6.5.1",
+        "range-parser": "1.2.0",
+        "safe-buffer": "5.1.1",
+        "send": "0.16.1",
+        "serve-static": "1.13.1",
+        "setprototypeof": "1.1.0",
+        "statuses": "1.3.1",
+        "type-is": "1.6.15",
+        "utils-merge": "1.0.1",
+        "vary": "1.1.2"
+      }
+    },
+    "extend": {
+      "version": "3.0.1",
+      "resolved": "http://registry.npm.taobao.org/extend/download/extend-3.0.1.tgz",
+      "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=",
+      "dev": true
+    },
+    "external-editor": {
+      "version": "1.1.1",
+      "resolved": "http://registry.npm.taobao.org/external-editor/download/external-editor-1.1.1.tgz",
+      "integrity": "sha1-Etew24UPf/fnCBuvQAVwAGDEYAs=",
+      "dev": true,
+      "requires": {
+        "extend": "3.0.1",
+        "spawn-sync": "1.0.15",
+        "tmp": "0.0.29"
+      }
+    },
+    "extglob": {
+      "version": "0.3.2",
+      "resolved": "http://registry.npm.taobao.org/extglob/download/extglob-0.3.2.tgz",
+      "integrity": "sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE=",
+      "dev": true,
+      "requires": {
+        "is-extglob": "1.0.0"
+      }
+    },
+    "extract-text-webpack-plugin": {
+      "version": "1.0.1",
+      "resolved": "http://registry.npm.taobao.org/extract-text-webpack-plugin/download/extract-text-webpack-plugin-1.0.1.tgz",
+      "integrity": "sha1-yVvzy6rEnclvHcbgclSfu2VMzSw=",
+      "dev": true,
+      "requires": {
+        "async": "1.5.2",
+        "loader-utils": "0.2.17",
+        "webpack-sources": "0.1.5"
+      }
+    },
+    "extsprintf": {
+      "version": "1.3.0",
+      "resolved": "http://registry.npm.taobao.org/extsprintf/download/extsprintf-1.3.0.tgz",
+      "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=",
+      "dev": true
+    },
+    "fast-deep-equal": {
+      "version": "1.0.0",
+      "resolved": "http://registry.npm.taobao.org/fast-deep-equal/download/fast-deep-equal-1.0.0.tgz",
+      "integrity": "sha1-liVqO8l1WV6zbYLpkp0GDYk0Of8=",
+      "dev": true
+    },
+    "fast-json-stable-stringify": {
+      "version": "2.0.0",
+      "resolved": "http://registry.npm.taobao.org/fast-json-stable-stringify/download/fast-json-stable-stringify-2.0.0.tgz",
+      "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=",
+      "dev": true
+    },
+    "fast-levenshtein": {
+      "version": "2.0.6",
+      "resolved": "http://registry.npm.taobao.org/fast-levenshtein/download/fast-levenshtein-2.0.6.tgz",
+      "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=",
+      "dev": true
+    },
+    "fastparse": {
+      "version": "1.1.1",
+      "resolved": "http://registry.npm.taobao.org/fastparse/download/fastparse-1.1.1.tgz",
+      "integrity": "sha1-0eJkOzipTXWDtHkGDmxK/8lAcfg=",
+      "dev": true
+    },
+    "fbjs": {
+      "version": "0.8.16",
+      "resolved": "http://registry.npm.taobao.org/fbjs/download/fbjs-0.8.16.tgz",
+      "integrity": "sha1-XmdDL1UNxBtXK/VYR7ispk5TN9s=",
+      "requires": {
+        "core-js": "1.2.7",
+        "isomorphic-fetch": "2.2.1",
+        "loose-envify": "1.3.1",
+        "object-assign": "4.1.1",
+        "promise": "7.3.1",
+        "setimmediate": "1.0.5",
+        "ua-parser-js": "0.7.17"
+      },
+      "dependencies": {
+        "core-js": {
+          "version": "1.2.7",
+          "resolved": "http://registry.npm.taobao.org/core-js/download/core-js-1.2.7.tgz",
+          "integrity": "sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY="
+        }
+      }
+    },
+    "fd-slicer": {
+      "version": "1.0.1",
+      "resolved": "http://registry.npm.taobao.org/fd-slicer/download/fd-slicer-1.0.1.tgz",
+      "integrity": "sha1-i1vL2ewyfFBBv5qwI/1nUPEXfmU=",
+      "dev": true,
+      "requires": {
+        "pend": "1.2.0"
+      }
+    },
+    "feature-detect-es6": {
+      "version": "1.4.0",
+      "resolved": "http://registry.npm.taobao.org/feature-detect-es6/download/feature-detect-es6-1.4.0.tgz",
+      "integrity": "sha1-gml8LyXxxF+h2IXE10xb4wjaZSc=",
+      "dev": true,
+      "requires": {
+        "array-back": "1.0.4"
+      }
+    },
+    "fetch-ponyfill": {
+      "version": "3.0.2",
+      "resolved": "http://registry.npm.taobao.org/fetch-ponyfill/download/fetch-ponyfill-3.0.2.tgz",
+      "integrity": "sha1-Cf8FOT/zFb6vzUZZQO1r3JqB/hk=",
+      "requires": {
+        "node-fetch": "1.6.3"
+      }
+    },
+    "fibers": {
+      "version": "2.0.0",
+      "resolved": "http://registry.npm.taobao.org/fibers/download/fibers-2.0.0.tgz",
+      "integrity": "sha1-8m0Krx+ZmV++HLPzQO+sCL2p3Es=",
+      "dev": true
+    },
+    "figures": {
+      "version": "1.7.0",
+      "resolved": "http://registry.npm.taobao.org/figures/download/figures-1.7.0.tgz",
+      "integrity": "sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4=",
+      "dev": true,
+      "requires": {
+        "escape-string-regexp": "1.0.5",
+        "object-assign": "4.1.1"
+      }
+    },
+    "file-entry-cache": {
+      "version": "2.0.0",
+      "resolved": "http://registry.npm.taobao.org/file-entry-cache/download/file-entry-cache-2.0.0.tgz",
+      "integrity": "sha1-w5KZDD5oR4PYOLjISkXYoEhFg2E=",
+      "dev": true,
+      "requires": {
+        "flat-cache": "1.3.0",
+        "object-assign": "4.1.1"
+      }
+    },
+    "file-loader": {
+      "version": "0.9.0",
+      "resolved": "http://registry.npm.taobao.org/file-loader/download/file-loader-0.9.0.tgz",
+      "integrity": "sha1-HS2t3UJM5tGwfP4/eXMb7TYXq0I=",
+      "dev": true,
+      "requires": {
+        "loader-utils": "0.2.17"
+      }
+    },
+    "filename-regex": {
+      "version": "2.0.1",
+      "resolved": "http://registry.npm.taobao.org/filename-regex/download/filename-regex-2.0.1.tgz",
+      "integrity": "sha1-wcS5vuPglyXdsQa3XB4wH+LxiyY=",
+      "dev": true
+    },
+    "fill-range": {
+      "version": "2.2.3",
+      "resolved": "http://registry.npm.taobao.org/fill-range/download/fill-range-2.2.3.tgz",
+      "integrity": "sha1-ULd9/X5Gm8dJJHCWNpn+eoSFpyM=",
+      "dev": true,
+      "requires": {
+        "is-number": "2.1.0",
+        "isobject": "2.1.0",
+        "randomatic": "1.1.7",
+        "repeat-element": "1.1.2",
+        "repeat-string": "1.6.1"
+      }
+    },
+    "finalhandler": {
+      "version": "1.1.0",
+      "resolved": "http://registry.npm.taobao.org/finalhandler/download/finalhandler-1.1.0.tgz",
+      "integrity": "sha1-zgtoVbRYU+eRsvzGgARtiCU91/U=",
+      "dev": true,
+      "requires": {
+        "debug": "2.6.9",
+        "encodeurl": "1.0.1",
+        "escape-html": "1.0.3",
+        "on-finished": "2.3.0",
+        "parseurl": "1.3.2",
+        "statuses": "1.3.1",
+        "unpipe": "1.0.0"
+      }
+    },
+    "find-cache-dir": {
+      "version": "0.1.1",
+      "resolved": "http://registry.npm.taobao.org/find-cache-dir/download/find-cache-dir-0.1.1.tgz",
+      "integrity": "sha1-yN765XyKUqinhPnjHFfHQumToLk=",
+      "dev": true,
+      "requires": {
+        "commondir": "1.0.1",
+        "mkdirp": "0.5.1",
+        "pkg-dir": "1.0.0"
+      }
+    },
+    "find-replace": {
+      "version": "1.0.3",
+      "resolved": "http://registry.npm.taobao.org/find-replace/download/find-replace-1.0.3.tgz",
+      "integrity": "sha1-uI5zZNLZyVlVnziMZmcNYTBEH6A=",
+      "dev": true,
+      "requires": {
+        "array-back": "1.0.4",
+        "test-value": "2.1.0"
+      }
+    },
+    "find-root": {
+      "version": "1.1.0",
+      "resolved": "http://registry.npm.taobao.org/find-root/download/find-root-1.1.0.tgz",
+      "integrity": "sha1-q8/Iunb3CMQql7PWhbfpRQv7nOQ=",
+      "dev": true
+    },
+    "find-up": {
+      "version": "1.1.2",
+      "resolved": "http://registry.npm.taobao.org/find-up/download/find-up-1.1.2.tgz",
+      "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=",
+      "dev": true,
+      "requires": {
+        "path-exists": "2.1.0",
+        "pinkie-promise": "2.0.1"
+      },
+      "dependencies": {
+        "path-exists": {
+          "version": "2.1.0",
+          "resolved": "http://registry.npm.taobao.org/path-exists/download/path-exists-2.1.0.tgz",
+          "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=",
+          "dev": true,
+          "requires": {
+            "pinkie-promise": "2.0.1"
+          }
+        }
+      }
+    },
+    "find-versions": {
+      "version": "1.2.1",
+      "resolved": "http://registry.npm.taobao.org/find-versions/download/find-versions-1.2.1.tgz",
+      "integrity": "sha1-y96fEuOFdaCvG+G5osXV/Y8Ya2I=",
+      "dev": true,
+      "requires": {
+        "array-uniq": "1.0.3",
+        "get-stdin": "4.0.1",
+        "meow": "3.7.0",
+        "semver-regex": "1.0.0"
+      }
+    },
+    "flat-cache": {
+      "version": "1.3.0",
+      "resolved": "http://registry.npm.taobao.org/flat-cache/download/flat-cache-1.3.0.tgz",
+      "integrity": "sha1-0wMLMrOBVPTjt+nHCfSQ9++XxIE=",
+      "dev": true,
+      "requires": {
+        "circular-json": "0.3.3",
+        "del": "2.2.2",
+        "graceful-fs": "4.1.11",
+        "write": "0.2.1"
+      }
+    },
+    "for-in": {
+      "version": "1.0.2",
+      "resolved": "http://registry.npm.taobao.org/for-in/download/for-in-1.0.2.tgz",
+      "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=",
+      "dev": true
+    },
+    "for-own": {
+      "version": "0.1.5",
+      "resolved": "http://registry.npm.taobao.org/for-own/download/for-own-0.1.5.tgz",
+      "integrity": "sha1-UmXGgaTylNq78XyVCbZ2OqhFEM4=",
+      "dev": true,
+      "requires": {
+        "for-in": "1.0.2"
+      }
+    },
+    "foreach": {
+      "version": "2.0.5",
+      "resolved": "http://registry.npm.taobao.org/foreach/download/foreach-2.0.5.tgz",
+      "integrity": "sha1-C+4AUBiusmDQo6865ljdATbsG5k=",
+      "dev": true
+    },
+    "forever-agent": {
+      "version": "0.6.1",
+      "resolved": "http://registry.npm.taobao.org/forever-agent/download/forever-agent-0.6.1.tgz",
+      "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=",
+      "dev": true
+    },
+    "form-data": {
+      "version": "2.3.1",
+      "resolved": "http://registry.npm.taobao.org/form-data/download/form-data-2.3.1.tgz",
+      "integrity": "sha1-b7lPvXGIUwbXPRXMSX/kzE7NRL8=",
+      "dev": true,
+      "requires": {
+        "asynckit": "0.4.0",
+        "combined-stream": "1.0.5",
+        "mime-types": "2.1.17"
+      }
+    },
+    "formatio": {
+      "version": "1.1.1",
+      "resolved": "http://registry.npm.taobao.org/formatio/download/formatio-1.1.1.tgz",
+      "integrity": "sha1-XtPM1jZVEJc4NGXZlhmRAOhhYek=",
+      "dev": true,
+      "requires": {
+        "samsam": "1.1.2"
+      }
+    },
+    "forwarded": {
+      "version": "0.1.2",
+      "resolved": "http://registry.npm.taobao.org/forwarded/download/forwarded-0.1.2.tgz",
+      "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=",
+      "dev": true
+    },
+    "fresh": {
+      "version": "0.5.2",
+      "resolved": "http://registry.npm.taobao.org/fresh/download/fresh-0.5.2.tgz",
+      "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=",
+      "dev": true
+    },
+    "fs-extra": {
+      "version": "0.30.0",
+      "resolved": "http://registry.npm.taobao.org/fs-extra/download/fs-extra-0.30.0.tgz",
+      "integrity": "sha1-8jP/zAjU2n1DLapEl3aYnbHfk/A=",
+      "dev": true,
+      "requires": {
+        "graceful-fs": "4.1.11",
+        "jsonfile": "2.4.0",
+        "klaw": "1.3.1",
+        "path-is-absolute": "1.0.1",
+        "rimraf": "2.6.2"
+      }
+    },
+    "fs-readdir-recursive": {
+      "version": "0.1.2",
+      "resolved": "http://registry.npm.taobao.org/fs-readdir-recursive/download/fs-readdir-recursive-0.1.2.tgz",
+      "integrity": "sha1-MVtPuMHKW4xH3v7zGdBz2tNWgFk=",
+      "dev": true
+    },
+    "fs.realpath": {
+      "version": "1.0.0",
+      "resolved": "http://registry.npm.taobao.org/fs.realpath/download/fs.realpath-1.0.0.tgz",
+      "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
+      "dev": true
+    },
+    "fsevents": {
+      "version": "1.1.3",
+      "resolved": "http://registry.npm.taobao.org/fsevents/download/fsevents-1.1.3.tgz",
+      "integrity": "sha1-EfgjGPX+e7LNIpZaEI6TBiCCFtg=",
+      "dev": true,
+      "optional": true,
+      "requires": {
+        "nan": "2.8.0",
+        "node-pre-gyp": "0.6.39"
+      },
+      "dependencies": {
+        "abbrev": {
+          "version": "1.1.0",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "ajv": {
+          "version": "4.11.8",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "co": "4.6.0",
+            "json-stable-stringify": "1.0.1"
+          }
+        },
+        "ansi-regex": {
+          "version": "2.1.1",
+          "bundled": true,
+          "dev": true
+        },
+        "aproba": {
+          "version": "1.1.1",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "are-we-there-yet": {
+          "version": "1.1.4",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "delegates": "1.0.0",
+            "readable-stream": "2.2.9"
+          }
+        },
+        "asn1": {
+          "version": "0.2.3",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "assert-plus": {
+          "version": "0.2.0",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "asynckit": {
+          "version": "0.4.0",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "aws-sign2": {
+          "version": "0.6.0",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "aws4": {
+          "version": "1.6.0",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "balanced-match": {
+          "version": "0.4.2",
+          "bundled": true,
+          "dev": true
+        },
+        "bcrypt-pbkdf": {
+          "version": "1.0.1",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "tweetnacl": "0.14.5"
+          }
+        },
+        "block-stream": {
+          "version": "0.0.9",
+          "bundled": true,
+          "dev": true,
+          "requires": {
+            "inherits": "2.0.3"
+          }
+        },
+        "boom": {
+          "version": "2.10.1",
+          "bundled": true,
+          "dev": true,
+          "requires": {
+            "hoek": "2.16.3"
+          }
+        },
+        "brace-expansion": {
+          "version": "1.1.7",
+          "bundled": true,
+          "dev": true,
+          "requires": {
+            "balanced-match": "0.4.2",
+            "concat-map": "0.0.1"
+          }
+        },
+        "buffer-shims": {
+          "version": "1.0.0",
+          "bundled": true,
+          "dev": true
+        },
+        "caseless": {
+          "version": "0.12.0",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "co": {
+          "version": "4.6.0",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "code-point-at": {
+          "version": "1.1.0",
+          "bundled": true,
+          "dev": true
+        },
+        "combined-stream": {
+          "version": "1.0.5",
+          "bundled": true,
+          "dev": true,
+          "requires": {
+            "delayed-stream": "1.0.0"
+          }
+        },
+        "concat-map": {
+          "version": "0.0.1",
+          "bundled": true,
+          "dev": true
+        },
+        "console-control-strings": {
+          "version": "1.1.0",
+          "bundled": true,
+          "dev": true
+        },
+        "core-util-is": {
+          "version": "1.0.2",
+          "bundled": true,
+          "dev": true
+        },
+        "cryptiles": {
+          "version": "2.0.5",
+          "bundled": true,
+          "dev": true,
+          "requires": {
+            "boom": "2.10.1"
+          }
+        },
+        "dashdash": {
+          "version": "1.14.1",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "assert-plus": "1.0.0"
+          },
+          "dependencies": {
+            "assert-plus": {
+              "version": "1.0.0",
+              "bundled": true,
+              "dev": true,
+              "optional": true
+            }
+          }
+        },
+        "debug": {
+          "version": "2.6.8",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "ms": "2.0.0"
+          }
+        },
+        "deep-extend": {
+          "version": "0.4.2",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "delayed-stream": {
+          "version": "1.0.0",
+          "bundled": true,
+          "dev": true
+        },
+        "delegates": {
+          "version": "1.0.0",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "detect-libc": {
+          "version": "1.0.2",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "ecc-jsbn": {
+          "version": "0.1.1",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "jsbn": "0.1.1"
+          }
+        },
+        "extend": {
+          "version": "3.0.1",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "extsprintf": {
+          "version": "1.0.2",
+          "bundled": true,
+          "dev": true
+        },
+        "forever-agent": {
+          "version": "0.6.1",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "form-data": {
+          "version": "2.1.4",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "asynckit": "0.4.0",
+            "combined-stream": "1.0.5",
+            "mime-types": "2.1.15"
+          }
+        },
+        "fs.realpath": {
+          "version": "1.0.0",
+          "bundled": true,
+          "dev": true
+        },
+        "fstream": {
+          "version": "1.0.11",
+          "bundled": true,
+          "dev": true,
+          "requires": {
+            "graceful-fs": "4.1.11",
+            "inherits": "2.0.3",
+            "mkdirp": "0.5.1",
+            "rimraf": "2.6.1"
+          }
+        },
+        "fstream-ignore": {
+          "version": "1.0.5",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "fstream": "1.0.11",
+            "inherits": "2.0.3",
+            "minimatch": "3.0.4"
+          }
+        },
+        "gauge": {
+          "version": "2.7.4",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "aproba": "1.1.1",
+            "console-control-strings": "1.1.0",
+            "has-unicode": "2.0.1",
+            "object-assign": "4.1.1",
+            "signal-exit": "3.0.2",
+            "string-width": "1.0.2",
+            "strip-ansi": "3.0.1",
+            "wide-align": "1.1.2"
+          }
+        },
+        "getpass": {
+          "version": "0.1.7",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "assert-plus": "1.0.0"
+          },
+          "dependencies": {
+            "assert-plus": {
+              "version": "1.0.0",
+              "bundled": true,
+              "dev": true,
+              "optional": true
+            }
+          }
+        },
+        "glob": {
+          "version": "7.1.2",
+          "bundled": true,
+          "dev": true,
+          "requires": {
+            "fs.realpath": "1.0.0",
+            "inflight": "1.0.6",
+            "inherits": "2.0.3",
+            "minimatch": "3.0.4",
+            "once": "1.4.0",
+            "path-is-absolute": "1.0.1"
+          }
+        },
+        "graceful-fs": {
+          "version": "4.1.11",
+          "bundled": true,
+          "dev": true
+        },
+        "har-schema": {
+          "version": "1.0.5",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "har-validator": {
+          "version": "4.2.1",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "ajv": "4.11.8",
+            "har-schema": "1.0.5"
+          }
+        },
+        "has-unicode": {
+          "version": "2.0.1",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "hawk": {
+          "version": "3.1.3",
+          "bundled": true,
+          "dev": true,
+          "requires": {
+            "boom": "2.10.1",
+            "cryptiles": "2.0.5",
+            "hoek": "2.16.3",
+            "sntp": "1.0.9"
+          }
+        },
+        "hoek": {
+          "version": "2.16.3",
+          "bundled": true,
+          "dev": true
+        },
+        "http-signature": {
+          "version": "1.1.1",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "assert-plus": "0.2.0",
+            "jsprim": "1.4.0",
+            "sshpk": "1.13.0"
+          }
+        },
+        "inflight": {
+          "version": "1.0.6",
+          "bundled": true,
+          "dev": true,
+          "requires": {
+            "once": "1.4.0",
+            "wrappy": "1.0.2"
+          }
+        },
+        "inherits": {
+          "version": "2.0.3",
+          "bundled": true,
+          "dev": true
+        },
+        "ini": {
+          "version": "1.3.4",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "is-fullwidth-code-point": {
+          "version": "1.0.0",
+          "bundled": true,
+          "dev": true,
+          "requires": {
+            "number-is-nan": "1.0.1"
+          }
+        },
+        "is-typedarray": {
+          "version": "1.0.0",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "isarray": {
+          "version": "1.0.0",
+          "bundled": true,
+          "dev": true
+        },
+        "isstream": {
+          "version": "0.1.2",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "jodid25519": {
+          "version": "1.0.2",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "jsbn": "0.1.1"
+          }
+        },
+        "jsbn": {
+          "version": "0.1.1",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "json-schema": {
+          "version": "0.2.3",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "json-stable-stringify": {
+          "version": "1.0.1",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "jsonify": "0.0.0"
+          }
+        },
+        "json-stringify-safe": {
+          "version": "5.0.1",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "jsonify": {
+          "version": "0.0.0",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "jsprim": {
+          "version": "1.4.0",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "assert-plus": "1.0.0",
+            "extsprintf": "1.0.2",
+            "json-schema": "0.2.3",
+            "verror": "1.3.6"
+          },
+          "dependencies": {
+            "assert-plus": {
+              "version": "1.0.0",
+              "bundled": true,
+              "dev": true,
+              "optional": true
+            }
+          }
+        },
+        "mime-db": {
+          "version": "1.27.0",
+          "bundled": true,
+          "dev": true
+        },
+        "mime-types": {
+          "version": "2.1.15",
+          "bundled": true,
+          "dev": true,
+          "requires": {
+            "mime-db": "1.27.0"
+          }
+        },
+        "minimatch": {
+          "version": "3.0.4",
+          "bundled": true,
+          "dev": true,
+          "requires": {
+            "brace-expansion": "1.1.7"
+          }
+        },
+        "minimist": {
+          "version": "0.0.8",
+          "bundled": true,
+          "dev": true
+        },
+        "mkdirp": {
+          "version": "0.5.1",
+          "bundled": true,
+          "dev": true,
+          "requires": {
+            "minimist": "0.0.8"
+          }
+        },
+        "ms": {
+          "version": "2.0.0",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "node-pre-gyp": {
+          "version": "0.6.39",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "detect-libc": "1.0.2",
+            "hawk": "3.1.3",
+            "mkdirp": "0.5.1",
+            "nopt": "4.0.1",
+            "npmlog": "4.1.0",
+            "rc": "1.2.1",
+            "request": "2.81.0",
+            "rimraf": "2.6.1",
+            "semver": "5.3.0",
+            "tar": "2.2.1",
+            "tar-pack": "3.4.0"
+          }
+        },
+        "nopt": {
+          "version": "4.0.1",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "abbrev": "1.1.0",
+            "osenv": "0.1.4"
+          }
+        },
+        "npmlog": {
+          "version": "4.1.0",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "are-we-there-yet": "1.1.4",
+            "console-control-strings": "1.1.0",
+            "gauge": "2.7.4",
+            "set-blocking": "2.0.0"
+          }
+        },
+        "number-is-nan": {
+          "version": "1.0.1",
+          "bundled": true,
+          "dev": true
+        },
+        "oauth-sign": {
+          "version": "0.8.2",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "object-assign": {
+          "version": "4.1.1",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "once": {
+          "version": "1.4.0",
+          "bundled": true,
+          "dev": true,
+          "requires": {
+            "wrappy": "1.0.2"
+          }
+        },
+        "os-homedir": {
+          "version": "1.0.2",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "os-tmpdir": {
+          "version": "1.0.2",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "osenv": {
+          "version": "0.1.4",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "os-homedir": "1.0.2",
+            "os-tmpdir": "1.0.2"
+          }
+        },
+        "path-is-absolute": {
+          "version": "1.0.1",
+          "bundled": true,
+          "dev": true
+        },
+        "performance-now": {
+          "version": "0.2.0",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "process-nextick-args": {
+          "version": "1.0.7",
+          "bundled": true,
+          "dev": true
+        },
+        "punycode": {
+          "version": "1.4.1",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "qs": {
+          "version": "6.4.0",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "rc": {
+          "version": "1.2.1",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "deep-extend": "0.4.2",
+            "ini": "1.3.4",
+            "minimist": "1.2.0",
+            "strip-json-comments": "2.0.1"
+          },
+          "dependencies": {
+            "minimist": {
+              "version": "1.2.0",
+              "bundled": true,
+              "dev": true,
+              "optional": true
+            }
+          }
+        },
+        "readable-stream": {
+          "version": "2.2.9",
+          "bundled": true,
+          "dev": true,
+          "requires": {
+            "buffer-shims": "1.0.0",
+            "core-util-is": "1.0.2",
+            "inherits": "2.0.3",
+            "isarray": "1.0.0",
+            "process-nextick-args": "1.0.7",
+            "string_decoder": "1.0.1",
+            "util-deprecate": "1.0.2"
+          }
+        },
+        "request": {
+          "version": "2.81.0",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "aws-sign2": "0.6.0",
+            "aws4": "1.6.0",
+            "caseless": "0.12.0",
+            "combined-stream": "1.0.5",
+            "extend": "3.0.1",
+            "forever-agent": "0.6.1",
+            "form-data": "2.1.4",
+            "har-validator": "4.2.1",
+            "hawk": "3.1.3",
+            "http-signature": "1.1.1",
+            "is-typedarray": "1.0.0",
+            "isstream": "0.1.2",
+            "json-stringify-safe": "5.0.1",
+            "mime-types": "2.1.15",
+            "oauth-sign": "0.8.2",
+            "performance-now": "0.2.0",
+            "qs": "6.4.0",
+            "safe-buffer": "5.0.1",
+            "stringstream": "0.0.5",
+            "tough-cookie": "2.3.2",
+            "tunnel-agent": "0.6.0",
+            "uuid": "3.0.1"
+          }
+        },
+        "rimraf": {
+          "version": "2.6.1",
+          "bundled": true,
+          "dev": true,
+          "requires": {
+            "glob": "7.1.2"
+          }
+        },
+        "safe-buffer": {
+          "version": "5.0.1",
+          "bundled": true,
+          "dev": true
+        },
+        "semver": {
+          "version": "5.3.0",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "set-blocking": {
+          "version": "2.0.0",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "signal-exit": {
+          "version": "3.0.2",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "sntp": {
+          "version": "1.0.9",
+          "bundled": true,
+          "dev": true,
+          "requires": {
+            "hoek": "2.16.3"
+          }
+        },
+        "sshpk": {
+          "version": "1.13.0",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "asn1": "0.2.3",
+            "assert-plus": "1.0.0",
+            "bcrypt-pbkdf": "1.0.1",
+            "dashdash": "1.14.1",
+            "ecc-jsbn": "0.1.1",
+            "getpass": "0.1.7",
+            "jodid25519": "1.0.2",
+            "jsbn": "0.1.1",
+            "tweetnacl": "0.14.5"
+          },
+          "dependencies": {
+            "assert-plus": {
+              "version": "1.0.0",
+              "bundled": true,
+              "dev": true,
+              "optional": true
+            }
+          }
+        },
+        "string-width": {
+          "version": "1.0.2",
+          "bundled": true,
+          "dev": true,
+          "requires": {
+            "code-point-at": "1.1.0",
+            "is-fullwidth-code-point": "1.0.0",
+            "strip-ansi": "3.0.1"
+          }
+        },
+        "string_decoder": {
+          "version": "1.0.1",
+          "bundled": true,
+          "dev": true,
+          "requires": {
+            "safe-buffer": "5.0.1"
+          }
+        },
+        "stringstream": {
+          "version": "0.0.5",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "strip-ansi": {
+          "version": "3.0.1",
+          "bundled": true,
+          "dev": true,
+          "requires": {
+            "ansi-regex": "2.1.1"
+          }
+        },
+        "strip-json-comments": {
+          "version": "2.0.1",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "tar": {
+          "version": "2.2.1",
+          "bundled": true,
+          "dev": true,
+          "requires": {
+            "block-stream": "0.0.9",
+            "fstream": "1.0.11",
+            "inherits": "2.0.3"
+          }
+        },
+        "tar-pack": {
+          "version": "3.4.0",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "debug": "2.6.8",
+            "fstream": "1.0.11",
+            "fstream-ignore": "1.0.5",
+            "once": "1.4.0",
+            "readable-stream": "2.2.9",
+            "rimraf": "2.6.1",
+            "tar": "2.2.1",
+            "uid-number": "0.0.6"
+          }
+        },
+        "tough-cookie": {
+          "version": "2.3.2",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "punycode": "1.4.1"
+          }
+        },
+        "tunnel-agent": {
+          "version": "0.6.0",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "safe-buffer": "5.0.1"
+          }
+        },
+        "tweetnacl": {
+          "version": "0.14.5",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "uid-number": {
+          "version": "0.0.6",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "util-deprecate": {
+          "version": "1.0.2",
+          "bundled": true,
+          "dev": true
+        },
+        "uuid": {
+          "version": "3.0.1",
+          "bundled": true,
+          "dev": true,
+          "optional": true
+        },
+        "verror": {
+          "version": "1.3.6",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "extsprintf": "1.0.2"
+          }
+        },
+        "wide-align": {
+          "version": "1.1.2",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "string-width": "1.0.2"
+          }
+        },
+        "wrappy": {
+          "version": "1.0.2",
+          "bundled": true,
+          "dev": true
+        }
+      }
+    },
+    "fstream": {
+      "version": "1.0.11",
+      "resolved": "http://registry.npm.taobao.org/fstream/download/fstream-1.0.11.tgz",
+      "integrity": "sha1-XB+x8RdHcRTwYyoOtLcbPLD9MXE=",
+      "dev": true,
+      "requires": {
+        "graceful-fs": "4.1.11",
+        "inherits": "2.0.3",
+        "mkdirp": "0.5.1",
+        "rimraf": "2.6.2"
+      }
+    },
+    "function-bind": {
+      "version": "1.1.1",
+      "resolved": "http://registry.npm.taobao.org/function-bind/download/function-bind-1.1.1.tgz",
+      "integrity": "sha1-pWiZ0+o8m6uHS7l3O3xe3pL0iV0=",
+      "dev": true
+    },
+    "gauge": {
+      "version": "2.7.4",
+      "resolved": "http://registry.npm.taobao.org/gauge/download/gauge-2.7.4.tgz",
+      "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=",
+      "dev": true,
+      "requires": {
+        "aproba": "1.2.0",
+        "console-control-strings": "1.1.0",
+        "has-unicode": "2.0.1",
+        "object-assign": "4.1.1",
+        "signal-exit": "3.0.2",
+        "string-width": "1.0.2",
+        "strip-ansi": "3.0.1",
+        "wide-align": "1.1.2"
+      }
+    },
+    "gaze": {
+      "version": "1.1.2",
+      "resolved": "http://registry.npm.taobao.org/gaze/download/gaze-1.1.2.tgz",
+      "integrity": "sha1-hHIkZ3rbiHDWeSV+0ziP22HkAQU=",
+      "dev": true,
+      "requires": {
+        "globule": "1.2.0"
+      }
+    },
+    "generate-function": {
+      "version": "2.0.0",
+      "resolved": "http://registry.npm.taobao.org/generate-function/download/generate-function-2.0.0.tgz",
+      "integrity": "sha1-aFj+fAlpt9TpCTM3ZHrHn2DfvnQ=",
+      "dev": true
+    },
+    "generate-object-property": {
+      "version": "1.2.0",
+      "resolved": "http://registry.npm.taobao.org/generate-object-property/download/generate-object-property-1.2.0.tgz",
+      "integrity": "sha1-nA4cQDCM6AT0eDYYuTf6iPmdUNA=",
+      "dev": true,
+      "requires": {
+        "is-property": "1.0.2"
+      }
+    },
+    "get-caller-file": {
+      "version": "1.0.2",
+      "resolved": "http://registry.npm.taobao.org/get-caller-file/download/get-caller-file-1.0.2.tgz",
+      "integrity": "sha1-9wLmMSfn4jHBYKgMFVSstw1QR+U=",
+      "dev": true
+    },
+    "get-stdin": {
+      "version": "4.0.1",
+      "resolved": "http://registry.npm.taobao.org/get-stdin/download/get-stdin-4.0.1.tgz",
+      "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=",
+      "dev": true
+    },
+    "getpass": {
+      "version": "0.1.7",
+      "resolved": "http://registry.npm.taobao.org/getpass/download/getpass-0.1.7.tgz",
+      "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=",
+      "dev": true,
+      "requires": {
+        "assert-plus": "1.0.0"
+      }
+    },
+    "glob": {
+      "version": "5.0.15",
+      "resolved": "http://registry.npm.taobao.org/glob/download/glob-5.0.15.tgz",
+      "integrity": "sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=",
+      "dev": true,
+      "requires": {
+        "inflight": "1.0.6",
+        "inherits": "2.0.3",
+        "minimatch": "3.0.4",
+        "once": "1.4.0",
+        "path-is-absolute": "1.0.1"
+      }
+    },
+    "glob-base": {
+      "version": "0.3.0",
+      "resolved": "http://registry.npm.taobao.org/glob-base/download/glob-base-0.3.0.tgz",
+      "integrity": "sha1-27Fk9iIbHAscz4Kuoyi0l98Oo8Q=",
+      "dev": true,
+      "requires": {
+        "glob-parent": "2.0.0",
+        "is-glob": "2.0.1"
+      }
+    },
+    "glob-parent": {
+      "version": "2.0.0",
+      "resolved": "http://registry.npm.taobao.org/glob-parent/download/glob-parent-2.0.0.tgz",
+      "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=",
+      "dev": true,
+      "requires": {
+        "is-glob": "2.0.1"
+      }
+    },
+    "global": {
+      "version": "4.3.2",
+      "resolved": "http://registry.npm.taobao.org/global/download/global-4.3.2.tgz",
+      "integrity": "sha1-52mJJopsdMOJCLEwWxD8DjlOnQ8=",
+      "dev": true,
+      "requires": {
+        "min-document": "2.19.0",
+        "process": "0.5.2"
+      }
+    },
+    "globals": {
+      "version": "9.18.0",
+      "resolved": "http://registry.npm.taobao.org/globals/download/globals-9.18.0.tgz",
+      "integrity": "sha1-qjiWs+abSH8X4x7SFD1pqOMMLYo="
+    },
+    "globby": {
+      "version": "5.0.0",
+      "resolved": "http://registry.npm.taobao.org/globby/download/globby-5.0.0.tgz",
+      "integrity": "sha1-69hGZ8oNuzMLmbz8aOrCvFQ3Dg0=",
+      "dev": true,
+      "requires": {
+        "array-union": "1.0.2",
+        "arrify": "1.0.1",
+        "glob": "7.1.2",
+        "object-assign": "4.1.1",
+        "pify": "2.3.0",
+        "pinkie-promise": "2.0.1"
+      },
+      "dependencies": {
+        "glob": {
+          "version": "7.1.2",
+          "resolved": "http://registry.npm.taobao.org/glob/download/glob-7.1.2.tgz",
+          "integrity": "sha1-wZyd+aAocC1nhhI4SmVSQExjbRU=",
+          "dev": true,
+          "requires": {
+            "fs.realpath": "1.0.0",
+            "inflight": "1.0.6",
+            "inherits": "2.0.3",
+            "minimatch": "3.0.4",
+            "once": "1.4.0",
+            "path-is-absolute": "1.0.1"
+          }
+        }
+      }
+    },
+    "globule": {
+      "version": "1.2.0",
+      "resolved": "http://registry.npm.taobao.org/globule/download/globule-1.2.0.tgz",
+      "integrity": "sha1-HcScaCLdnoovoAuiopUAboZkvQk=",
+      "dev": true,
+      "requires": {
+        "glob": "7.1.2",
+        "lodash": "4.17.4",
+        "minimatch": "3.0.4"
+      },
+      "dependencies": {
+        "glob": {
+          "version": "7.1.2",
+          "resolved": "http://registry.npm.taobao.org/glob/download/glob-7.1.2.tgz",
+          "integrity": "sha1-wZyd+aAocC1nhhI4SmVSQExjbRU=",
+          "dev": true,
+          "requires": {
+            "fs.realpath": "1.0.0",
+            "inflight": "1.0.6",
+            "inherits": "2.0.3",
+            "minimatch": "3.0.4",
+            "once": "1.4.0",
+            "path-is-absolute": "1.0.1"
+          }
+        }
+      }
+    },
+    "graceful-fs": {
+      "version": "4.1.11",
+      "resolved": "http://registry.npm.taobao.org/graceful-fs/download/graceful-fs-4.1.11.tgz",
+      "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=",
+      "dev": true
+    },
+    "graceful-readlink": {
+      "version": "1.0.1",
+      "resolved": "http://registry.npm.taobao.org/graceful-readlink/download/graceful-readlink-1.0.1.tgz",
+      "integrity": "sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=",
+      "dev": true
+    },
+    "growl": {
+      "version": "1.8.1",
+      "resolved": "http://registry.npm.taobao.org/growl/download/growl-1.8.1.tgz",
+      "integrity": "sha1-Sy3sjZB+k9szZiTc7AGDUC+MlCg=",
+      "dev": true
+    },
+    "har-schema": {
+      "version": "2.0.0",
+      "resolved": "http://registry.npm.taobao.org/har-schema/download/har-schema-2.0.0.tgz",
+      "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=",
+      "dev": true
+    },
+    "har-validator": {
+      "version": "5.0.3",
+      "resolved": "http://registry.npm.taobao.org/har-validator/download/har-validator-5.0.3.tgz",
+      "integrity": "sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0=",
+      "dev": true,
+      "requires": {
+        "ajv": "5.5.2",
+        "har-schema": "2.0.0"
+      }
+    },
+    "has-ansi": {
+      "version": "2.0.0",
+      "resolved": "http://registry.npm.taobao.org/has-ansi/download/has-ansi-2.0.0.tgz",
+      "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=",
+      "requires": {
+        "ansi-regex": "2.1.1"
+      }
+    },
+    "has-flag": {
+      "version": "1.0.0",
+      "resolved": "http://registry.npm.taobao.org/has-flag/download/has-flag-1.0.0.tgz",
+      "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=",
+      "dev": true
+    },
+    "has-symbols": {
+      "version": "1.0.0",
+      "resolved": "http://registry.npm.taobao.org/has-symbols/download/has-symbols-1.0.0.tgz",
+      "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=",
+      "dev": true
+    },
+    "has-unicode": {
+      "version": "2.0.1",
+      "resolved": "http://registry.npm.taobao.org/has-unicode/download/has-unicode-2.0.1.tgz",
+      "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=",
+      "dev": true
+    },
+    "hawk": {
+      "version": "6.0.2",
+      "resolved": "http://registry.npm.taobao.org/hawk/download/hawk-6.0.2.tgz",
+      "integrity": "sha1-r02RTrBl+bXOTZ0RwcshJu7MMDg=",
+      "dev": true,
+      "requires": {
+        "boom": "4.3.1",
+        "cryptiles": "3.1.2",
+        "hoek": "4.2.0",
+        "sntp": "2.1.0"
+      }
+    },
+    "he": {
+      "version": "1.1.1",
+      "resolved": "http://registry.npm.taobao.org/he/download/he-1.1.1.tgz",
+      "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=",
+      "dev": true
+    },
+    "history": {
+      "version": "2.1.2",
+      "resolved": "http://registry.npm.taobao.org/history/download/history-2.1.2.tgz",
+      "integrity": "sha1-SqLeiXoOSGfkU5hDvm7Nsphr/ew=",
+      "requires": {
+        "deep-equal": "1.0.1",
+        "invariant": "2.2.2",
+        "query-string": "3.0.3",
+        "warning": "2.1.0"
+      },
+      "dependencies": {
+        "warning": {
+          "version": "2.1.0",
+          "resolved": "http://registry.npm.taobao.org/warning/download/warning-2.1.0.tgz",
+          "integrity": "sha1-ISINnGOvx3qMkhEeARr3Bc4MaQE=",
+          "requires": {
+            "loose-envify": "1.3.1"
+          }
+        }
+      }
+    },
+    "hjs-webpack": {
+      "version": "8.3.0",
+      "resolved": "http://registry.npm.taobao.org/hjs-webpack/download/hjs-webpack-8.3.0.tgz",
+      "integrity": "sha1-2TGrPW8s0x+kFK371xdfpOZ+LrE=",
+      "dev": true,
+      "requires": {
+        "connect-history-api-fallback": "1.5.0",
+        "contains-path": "0.1.0",
+        "express": "4.16.2",
+        "extract-text-webpack-plugin": "1.0.1",
+        "find-root": "1.1.0",
+        "http-proxy-middleware": "0.16.0",
+        "lodash.assign": "4.2.0",
+        "lodash.defaults": "4.2.0",
+        "lodash.pick": "4.4.0",
+        "rimraf": "2.6.2",
+        "webpack": "1.15.0",
+        "webpack-dev-middleware": "1.12.2",
+        "webpack-hot-middleware": "2.21.0"
+      }
+    },
+    "hoek": {
+      "version": "4.2.0",
+      "resolved": "http://registry.npm.taobao.org/hoek/download/hoek-4.2.0.tgz",
+      "integrity": "sha1-ctnQdU9/4lyi0BrY+PmpRJqJUm0=",
+      "dev": true
+    },
+    "hoist-non-react-statics": {
+      "version": "1.2.0",
+      "resolved": "http://registry.npm.taobao.org/hoist-non-react-statics/download/hoist-non-react-statics-1.2.0.tgz",
+      "integrity": "sha1-qkSM8JhtVcxAdzsXF0t90GbLfPs="
+    },
+    "home-or-tmp": {
+      "version": "2.0.0",
+      "resolved": "http://registry.npm.taobao.org/home-or-tmp/download/home-or-tmp-2.0.0.tgz",
+      "integrity": "sha1-42w/LSyufXRqhX440Y1fMqeILbg=",
+      "requires": {
+        "os-homedir": "1.0.2",
+        "os-tmpdir": "1.0.2"
+      }
+    },
+    "hosted-git-info": {
+      "version": "2.5.0",
+      "resolved": "http://registry.npm.taobao.org/hosted-git-info/download/hosted-git-info-2.5.0.tgz",
+      "integrity": "sha1-bWDjSzq7yDEwYsO3mO+NkBoHrzw=",
+      "dev": true
+    },
+    "html-entities": {
+      "version": "1.2.1",
+      "resolved": "http://registry.npm.taobao.org/html-entities/download/html-entities-1.2.1.tgz",
+      "integrity": "sha1-DfKTUfByEWNRXfueVUPl9u7VFi8=",
+      "dev": true
+    },
+    "http-errors": {
+      "version": "1.6.2",
+      "resolved": "http://registry.npm.taobao.org/http-errors/download/http-errors-1.6.2.tgz",
+      "integrity": "sha1-CgAsyFcHGSp+eUbO7cERVfYOxzY=",
+      "dev": true,
+      "requires": {
+        "depd": "1.1.1",
+        "inherits": "2.0.3",
+        "setprototypeof": "1.0.3",
+        "statuses": "1.3.1"
+      },
+      "dependencies": {
+        "setprototypeof": {
+          "version": "1.0.3",
+          "resolved": "http://registry.npm.taobao.org/setprototypeof/download/setprototypeof-1.0.3.tgz",
+          "integrity": "sha1-ZlZ+NwQ+608E2RvWWMDL77VbjgQ=",
+          "dev": true
+        }
+      }
+    },
+    "http-proxy": {
+      "version": "1.16.2",
+      "resolved": "http://registry.npm.taobao.org/http-proxy/download/http-proxy-1.16.2.tgz",
+      "integrity": "sha1-Bt/ykpUr9k2+hHH6nfcwZtTzd0I=",
+      "dev": true,
+      "requires": {
+        "eventemitter3": "1.2.0",
+        "requires-port": "1.0.0"
+      }
+    },
+    "http-proxy-middleware": {
+      "version": "0.16.0",
+      "resolved": "http://registry.npm.taobao.org/http-proxy-middleware/download/http-proxy-middleware-0.16.0.tgz",
+      "integrity": "sha1-aHm48RUaMondMBNyuB2N0CbjFis=",
+      "dev": true,
+      "requires": {
+        "http-proxy": "1.16.2",
+        "is-glob": "2.0.1",
+        "lodash": "4.17.4",
+        "micromatch": "2.3.11"
+      }
+    },
+    "http-signature": {
+      "version": "1.2.0",
+      "resolved": "http://registry.npm.taobao.org/http-signature/download/http-signature-1.2.0.tgz",
+      "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=",
+      "dev": true,
+      "requires": {
+        "assert-plus": "1.0.0",
+        "jsprim": "1.4.1",
+        "sshpk": "1.13.1"
+      }
+    },
+    "https-browserify": {
+      "version": "0.0.1",
+      "resolved": "http://registry.npm.taobao.org/https-browserify/download/https-browserify-0.0.1.tgz",
+      "integrity": "sha1-P5E2XKvmC3ftDruiS0VOPgnZWoI=",
+      "dev": true
+    },
+    "https-proxy-agent": {
+      "version": "1.0.0",
+      "resolved": "http://registry.npm.taobao.org/https-proxy-agent/download/https-proxy-agent-1.0.0.tgz",
+      "integrity": "sha1-NffabEjOTdv6JkiRrFk+5f+GceY=",
+      "dev": true,
+      "requires": {
+        "agent-base": "2.1.1",
+        "debug": "2.6.9",
+        "extend": "3.0.1"
+      }
+    },
+    "humanize-duration": {
+      "version": "3.12.0",
+      "resolved": "http://registry.npm.taobao.org/humanize-duration/download/humanize-duration-3.12.0.tgz",
+      "integrity": "sha1-vi+zBi9derwIkucVtd0r0VLHBV4=",
+      "dev": true
+    },
+    "iconv-lite": {
+      "version": "0.4.19",
+      "resolved": "http://registry.npm.taobao.org/iconv-lite/download/iconv-lite-0.4.19.tgz",
+      "integrity": "sha1-90aPYBNfXl2tM5nAqBvpoWA6CCs="
+    },
+    "ieee754": {
+      "version": "1.1.8",
+      "resolved": "http://registry.npm.taobao.org/ieee754/download/ieee754-1.1.8.tgz",
+      "integrity": "sha1-vjPUCsEO8ZJnAfbwii2G+/0a0+Q=",
+      "dev": true
+    },
+    "ignore": {
+      "version": "3.3.7",
+      "resolved": "http://registry.npm.taobao.org/ignore/download/ignore-3.3.7.tgz",
+      "integrity": "sha1-YSKJv7PCIOGGpYEYYY1b6MG6sCE=",
+      "dev": true
+    },
+    "imurmurhash": {
+      "version": "0.1.4",
+      "resolved": "http://registry.npm.taobao.org/imurmurhash/download/imurmurhash-0.1.4.tgz",
+      "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=",
+      "dev": true
+    },
+    "in-publish": {
+      "version": "2.0.0",
+      "resolved": "http://registry.npm.taobao.org/in-publish/download/in-publish-2.0.0.tgz",
+      "integrity": "sha1-4g/146KvwmkDILbcVSaCqcf631E=",
+      "dev": true
+    },
+    "indent-string": {
+      "version": "2.1.0",
+      "resolved": "http://registry.npm.taobao.org/indent-string/download/indent-string-2.1.0.tgz",
+      "integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=",
+      "dev": true,
+      "requires": {
+        "repeating": "2.0.1"
+      }
+    },
+    "indexof": {
+      "version": "0.0.1",
+      "resolved": "http://registry.npm.taobao.org/indexof/download/indexof-0.0.1.tgz",
+      "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=",
+      "dev": true
+    },
+    "inflight": {
+      "version": "1.0.6",
+      "resolved": "http://registry.npm.taobao.org/inflight/download/inflight-1.0.6.tgz",
+      "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
+      "dev": true,
+      "requires": {
+        "once": "1.4.0",
+        "wrappy": "1.0.2"
+      }
+    },
+    "inherits": {
+      "version": "2.0.3",
+      "resolved": "http://registry.npm.taobao.org/inherits/download/inherits-2.0.3.tgz",
+      "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
+    },
+    "inquirer": {
+      "version": "0.12.0",
+      "resolved": "http://registry.npm.taobao.org/inquirer/download/inquirer-0.12.0.tgz",
+      "integrity": "sha1-HvK/1jUE3wvHV4X/+MLEHfEvB34=",
+      "dev": true,
+      "requires": {
+        "ansi-escapes": "1.4.0",
+        "ansi-regex": "2.1.1",
+        "chalk": "1.1.3",
+        "cli-cursor": "1.0.2",
+        "cli-width": "2.2.0",
+        "figures": "1.7.0",
+        "lodash": "4.17.4",
+        "readline2": "1.0.1",
+        "run-async": "0.1.0",
+        "rx-lite": "3.1.2",
+        "string-width": "1.0.2",
+        "strip-ansi": "3.0.1",
+        "through": "2.3.8"
+      }
+    },
+    "interpret": {
+      "version": "0.6.6",
+      "resolved": "http://registry.npm.taobao.org/interpret/download/interpret-0.6.6.tgz",
+      "integrity": "sha1-/s16GOfOXKar+5U+H4YhOknxYls=",
+      "dev": true
+    },
+    "invariant": {
+      "version": "2.2.2",
+      "resolved": "http://registry.npm.taobao.org/invariant/download/invariant-2.2.2.tgz",
+      "integrity": "sha1-nh9WrArNtr8wMwbzOL47IErmA2A=",
+      "requires": {
+        "loose-envify": "1.3.1"
+      }
+    },
+    "invert-kv": {
+      "version": "1.0.0",
+      "resolved": "http://registry.npm.taobao.org/invert-kv/download/invert-kv-1.0.0.tgz",
+      "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=",
+      "dev": true
+    },
+    "ipaddr.js": {
+      "version": "1.5.2",
+      "resolved": "http://registry.npm.taobao.org/ipaddr.js/download/ipaddr.js-1.5.2.tgz",
+      "integrity": "sha1-1LUFvemUaYfM8PxY2QEP+WB+P6A=",
+      "dev": true
+    },
+    "is-absolute": {
+      "version": "0.1.7",
+      "resolved": "http://registry.npm.taobao.org/is-absolute/download/is-absolute-0.1.7.tgz",
+      "integrity": "sha1-hHSREZ/MtftDYhfMc39/qtUPYD8=",
+      "dev": true,
+      "requires": {
+        "is-relative": "0.1.3"
+      }
+    },
+    "is-arrayish": {
+      "version": "0.2.1",
+      "resolved": "http://registry.npm.taobao.org/is-arrayish/download/is-arrayish-0.2.1.tgz",
+      "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=",
+      "dev": true
+    },
+    "is-binary-path": {
+      "version": "1.0.1",
+      "resolved": "http://registry.npm.taobao.org/is-binary-path/download/is-binary-path-1.0.1.tgz",
+      "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=",
+      "dev": true,
+      "requires": {
+        "binary-extensions": "1.11.0"
+      }
+    },
+    "is-buffer": {
+      "version": "1.1.6",
+      "resolved": "http://registry.npm.taobao.org/is-buffer/download/is-buffer-1.1.6.tgz",
+      "integrity": "sha1-76ouqdqg16suoTqXsritUf776L4=",
+      "dev": true
+    },
+    "is-builtin-module": {
+      "version": "1.0.0",
+      "resolved": "http://registry.npm.taobao.org/is-builtin-module/download/is-builtin-module-1.0.0.tgz",
+      "integrity": "sha1-VAVy0096wxGfj3bDDLwbHgN6/74=",
+      "dev": true,
+      "requires": {
+        "builtin-modules": "1.1.1"
+      }
+    },
+    "is-directory": {
+      "version": "0.3.1",
+      "resolved": "http://registry.npm.taobao.org/is-directory/download/is-directory-0.3.1.tgz",
+      "integrity": "sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE=",
+      "dev": true
+    },
+    "is-dotfile": {
+      "version": "1.0.3",
+      "resolved": "http://registry.npm.taobao.org/is-dotfile/download/is-dotfile-1.0.3.tgz",
+      "integrity": "sha1-pqLzL/0t+wT1yiXs0Pa4PPeYoeE=",
+      "dev": true
+    },
+    "is-equal-shallow": {
+      "version": "0.1.3",
+      "resolved": "http://registry.npm.taobao.org/is-equal-shallow/download/is-equal-shallow-0.1.3.tgz",
+      "integrity": "sha1-IjgJj8Ih3gvPpdnqxMRdY4qhxTQ=",
+      "dev": true,
+      "requires": {
+        "is-primitive": "2.0.0"
+      }
+    },
+    "is-extendable": {
+      "version": "0.1.1",
+      "resolved": "http://registry.npm.taobao.org/is-extendable/download/is-extendable-0.1.1.tgz",
+      "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=",
+      "dev": true
+    },
+    "is-extglob": {
+      "version": "1.0.0",
+      "resolved": "http://registry.npm.taobao.org/is-extglob/download/is-extglob-1.0.0.tgz",
+      "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=",
+      "dev": true
+    },
+    "is-finite": {
+      "version": "1.0.2",
+      "resolved": "http://registry.npm.taobao.org/is-finite/download/is-finite-1.0.2.tgz",
+      "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=",
+      "requires": {
+        "number-is-nan": "1.0.1"
+      }
+    },
+    "is-fullwidth-code-point": {
+      "version": "1.0.0",
+      "resolved": "http://registry.npm.taobao.org/is-fullwidth-code-point/download/is-fullwidth-code-point-1.0.0.tgz",
+      "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
+      "dev": true,
+      "requires": {
+        "number-is-nan": "1.0.1"
+      }
+    },
+    "is-glob": {
+      "version": "2.0.1",
+      "resolved": "http://registry.npm.taobao.org/is-glob/download/is-glob-2.0.1.tgz",
+      "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=",
+      "dev": true,
+      "requires": {
+        "is-extglob": "1.0.0"
+      }
+    },
+    "is-my-json-valid": {
+      "version": "2.17.1",
+      "resolved": "http://registry.npm.taobao.org/is-my-json-valid/download/is-my-json-valid-2.17.1.tgz",
+      "integrity": "sha1-PamJFKcKIvCoVj7xURokbG/FVHE=",
+      "dev": true,
+      "requires": {
+        "generate-function": "2.0.0",
+        "generate-object-property": "1.2.0",
+        "jsonpointer": "4.0.1",
+        "xtend": "4.0.1"
+      }
+    },
+    "is-number": {
+      "version": "2.1.0",
+      "resolved": "http://registry.npm.taobao.org/is-number/download/is-number-2.1.0.tgz",
+      "integrity": "sha1-Afy7s5NGOlSPL0ZszhbezknbkI8=",
+      "dev": true,
+      "requires": {
+        "kind-of": "3.2.2"
+      }
+    },
+    "is-path-cwd": {
+      "version": "1.0.0",
+      "resolved": "http://registry.npm.taobao.org/is-path-cwd/download/is-path-cwd-1.0.0.tgz",
+      "integrity": "sha1-0iXsIxMuie3Tj9p2dHLmLmXxEG0=",
+      "dev": true
+    },
+    "is-path-in-cwd": {
+      "version": "1.0.0",
+      "resolved": "http://registry.npm.taobao.org/is-path-in-cwd/download/is-path-in-cwd-1.0.0.tgz",
+      "integrity": "sha1-ZHdYK4IU1gI0YJRWcAO+ip6sBNw=",
+      "dev": true,
+      "requires": {
+        "is-path-inside": "1.0.1"
+      }
+    },
+    "is-path-inside": {
+      "version": "1.0.1",
+      "resolved": "http://registry.npm.taobao.org/is-path-inside/download/is-path-inside-1.0.1.tgz",
+      "integrity": "sha1-jvW33lBDej/cprToZe96pVy0gDY=",
+      "dev": true,
+      "requires": {
+        "path-is-inside": "1.0.2"
+      }
+    },
+    "is-posix-bracket": {
+      "version": "0.1.1",
+      "resolved": "http://registry.npm.taobao.org/is-posix-bracket/download/is-posix-bracket-0.1.1.tgz",
+      "integrity": "sha1-MzTceXdDaOkvAW5vvAqI9c1ua8Q=",
+      "dev": true
+    },
+    "is-primitive": {
+      "version": "2.0.0",
+      "resolved": "http://registry.npm.taobao.org/is-primitive/download/is-primitive-2.0.0.tgz",
+      "integrity": "sha1-IHurkWOEmcB7Kt8kCkGochADRXU=",
+      "dev": true
+    },
+    "is-promise": {
+      "version": "2.1.0",
+      "resolved": "http://registry.npm.taobao.org/is-promise/download/is-promise-2.1.0.tgz",
+      "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o="
+    },
+    "is-property": {
+      "version": "1.0.2",
+      "resolved": "http://registry.npm.taobao.org/is-property/download/is-property-1.0.2.tgz",
+      "integrity": "sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ=",
+      "dev": true
+    },
+    "is-relative": {
+      "version": "0.1.3",
+      "resolved": "http://registry.npm.taobao.org/is-relative/download/is-relative-0.1.3.tgz",
+      "integrity": "sha1-kF/uiuhvRbPsYUvDwVyGnfCHboI=",
+      "dev": true
+    },
+    "is-resolvable": {
+      "version": "1.0.1",
+      "resolved": "http://registry.npm.taobao.org/is-resolvable/download/is-resolvable-1.0.1.tgz",
+      "integrity": "sha1-rMoc022+RLl0uSQyFVWnC6A7HPQ=",
+      "dev": true
+    },
+    "is-running": {
+      "version": "2.1.0",
+      "resolved": "http://registry.npm.taobao.org/is-running/download/is-running-2.1.0.tgz",
+      "integrity": "sha1-MKc/9cw4VOT8JUkICen1q/jeCeA=",
+      "dev": true
+    },
+    "is-stream": {
+      "version": "1.1.0",
+      "resolved": "http://registry.npm.taobao.org/is-stream/download/is-stream-1.1.0.tgz",
+      "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ="
+    },
+    "is-typedarray": {
+      "version": "1.0.0",
+      "resolved": "http://registry.npm.taobao.org/is-typedarray/download/is-typedarray-1.0.0.tgz",
+      "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=",
+      "dev": true
+    },
+    "is-utf8": {
+      "version": "0.2.1",
+      "resolved": "http://registry.npm.taobao.org/is-utf8/download/is-utf8-0.2.1.tgz",
+      "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=",
+      "dev": true
+    },
+    "isarray": {
+      "version": "1.0.0",
+      "resolved": "http://registry.npm.taobao.org/isarray/download/isarray-1.0.0.tgz",
+      "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=",
+      "dev": true
+    },
+    "isexe": {
+      "version": "2.0.0",
+      "resolved": "http://registry.npm.taobao.org/isexe/download/isexe-2.0.0.tgz",
+      "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=",
+      "dev": true
+    },
+    "isobject": {
+      "version": "2.1.0",
+      "resolved": "http://registry.npm.taobao.org/isobject/download/isobject-2.1.0.tgz",
+      "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=",
+      "dev": true,
+      "requires": {
+        "isarray": "1.0.0"
+      }
+    },
+    "isomorphic-fetch": {
+      "version": "2.2.1",
+      "resolved": "http://registry.npm.taobao.org/isomorphic-fetch/download/isomorphic-fetch-2.2.1.tgz",
+      "integrity": "sha1-YRrhrPFPXoH3KVB0coGf6XM1WKk=",
+      "requires": {
+        "node-fetch": "1.6.3",
+        "whatwg-fetch": "2.0.3"
+      }
+    },
+    "isstream": {
+      "version": "0.1.2",
+      "resolved": "http://registry.npm.taobao.org/isstream/download/isstream-0.1.2.tgz",
+      "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=",
+      "dev": true
+    },
+    "jade": {
+      "version": "0.26.3",
+      "resolved": "http://registry.npm.taobao.org/jade/download/jade-0.26.3.tgz",
+      "integrity": "sha1-jxDXl32NefL2/4YqgbBRPMslaGw=",
+      "dev": true,
+      "requires": {
+        "commander": "0.6.1",
+        "mkdirp": "0.3.0"
+      },
+      "dependencies": {
+        "commander": {
+          "version": "0.6.1",
+          "resolved": "http://registry.npm.taobao.org/commander/download/commander-0.6.1.tgz",
+          "integrity": "sha1-+mihT2qUXVTbvlDYzbMyDp47GgY=",
+          "dev": true
+        },
+        "mkdirp": {
+          "version": "0.3.0",
+          "resolved": "http://registry.npm.taobao.org/mkdirp/download/mkdirp-0.3.0.tgz",
+          "integrity": "sha1-G79asbqCevI1dRQ0kEJkVfSB/h4=",
+          "dev": true
+        }
+      }
+    },
+    "js-base64": {
+      "version": "2.4.0",
+      "resolved": "http://registry.npm.taobao.org/js-base64/download/js-base64-2.4.0.tgz",
+      "integrity": "sha1-nlZv7mJHUaHXIMlmzWIm0p1AJao=",
+      "dev": true
+    },
+    "js-tokens": {
+      "version": "3.0.2",
+      "resolved": "http://registry.npm.taobao.org/js-tokens/download/js-tokens-3.0.2.tgz",
+      "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls="
+    },
+    "js-yaml": {
+      "version": "3.10.0",
+      "resolved": "http://registry.npm.taobao.org/js-yaml/download/js-yaml-3.10.0.tgz",
+      "integrity": "sha1-LnhEFka9RoLpY/IrbpKCPDCcYtw=",
+      "dev": true,
+      "requires": {
+        "argparse": "1.0.9",
+        "esprima": "4.0.0"
+      }
+    },
+    "jsbn": {
+      "version": "0.1.1",
+      "resolved": "http://registry.npm.taobao.org/jsbn/download/jsbn-0.1.1.tgz",
+      "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=",
+      "dev": true,
+      "optional": true
+    },
+    "jsesc": {
+      "version": "1.3.0",
+      "resolved": "http://registry.npm.taobao.org/jsesc/download/jsesc-1.3.0.tgz",
+      "integrity": "sha1-RsP+yMGJKxKwgz25vHYiF226s0s="
+    },
+    "json-loader": {
+      "version": "0.5.7",
+      "resolved": "http://registry.npm.taobao.org/json-loader/download/json-loader-0.5.7.tgz",
+      "integrity": "sha1-3KFKcCNf+C8KyaOr62DTN6NlGF0=",
+      "dev": true
+    },
+    "json-schema": {
+      "version": "0.2.3",
+      "resolved": "http://registry.npm.taobao.org/json-schema/download/json-schema-0.2.3.tgz",
+      "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=",
+      "dev": true
+    },
+    "json-schema-traverse": {
+      "version": "0.3.1",
+      "resolved": "http://registry.npm.taobao.org/json-schema-traverse/download/json-schema-traverse-0.3.1.tgz",
+      "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=",
+      "dev": true
+    },
+    "json-stable-stringify": {
+      "version": "1.0.1",
+      "resolved": "http://registry.npm.taobao.org/json-stable-stringify/download/json-stable-stringify-1.0.1.tgz",
+      "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=",
+      "dev": true,
+      "requires": {
+        "jsonify": "0.0.0"
+      }
+    },
+    "json-stringify-safe": {
+      "version": "5.0.1",
+      "resolved": "http://registry.npm.taobao.org/json-stringify-safe/download/json-stringify-safe-5.0.1.tgz",
+      "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=",
+      "dev": true
+    },
+    "json5": {
+      "version": "0.5.1",
+      "resolved": "http://registry.npm.taobao.org/json5/download/json5-0.5.1.tgz",
+      "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE="
+    },
+    "jsonfile": {
+      "version": "2.4.0",
+      "resolved": "http://registry.npm.taobao.org/jsonfile/download/jsonfile-2.4.0.tgz",
+      "integrity": "sha1-NzaitCi4e72gzIO1P6PWM6NcKug=",
+      "dev": true,
+      "requires": {
+        "graceful-fs": "4.1.11"
+      }
+    },
+    "jsonify": {
+      "version": "0.0.0",
+      "resolved": "http://registry.npm.taobao.org/jsonify/download/jsonify-0.0.0.tgz",
+      "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=",
+      "dev": true
+    },
+    "jsonpointer": {
+      "version": "4.0.1",
+      "resolved": "http://registry.npm.taobao.org/jsonpointer/download/jsonpointer-4.0.1.tgz",
+      "integrity": "sha1-T9kss04OnbPInIYi7PUfm5eMbLk=",
+      "dev": true
+    },
+    "jsprim": {
+      "version": "1.4.1",
+      "resolved": "http://registry.npm.taobao.org/jsprim/download/jsprim-1.4.1.tgz",
+      "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=",
+      "dev": true,
+      "requires": {
+        "assert-plus": "1.0.0",
+        "extsprintf": "1.3.0",
+        "json-schema": "0.2.3",
+        "verror": "1.10.0"
+      }
+    },
+    "jsx-ast-utils": {
+      "version": "1.4.1",
+      "resolved": "http://registry.npm.taobao.org/jsx-ast-utils/download/jsx-ast-utils-1.4.1.tgz",
+      "integrity": "sha1-OGchPo3Xm/Ho8jAMDPwe+xgsDfE=",
+      "dev": true
+    },
+    "keycode": {
+      "version": "2.1.9",
+      "resolved": "http://registry.npm.taobao.org/keycode/download/keycode-2.1.9.tgz",
+      "integrity": "sha1-lkojxU5IiUBbSGGlyfBIDUUUHfo="
+    },
+    "kind-of": {
+      "version": "3.2.2",
+      "resolved": "http://registry.npm.taobao.org/kind-of/download/kind-of-3.2.2.tgz",
+      "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+      "dev": true,
+      "requires": {
+        "is-buffer": "1.1.6"
+      }
+    },
+    "klaw": {
+      "version": "1.3.1",
+      "resolved": "http://registry.npm.taobao.org/klaw/download/klaw-1.3.1.tgz",
+      "integrity": "sha1-QIhDO0azsbolnXh4XY6W9zugJDk=",
+      "dev": true,
+      "requires": {
+        "graceful-fs": "4.1.11"
+      }
+    },
+    "lazy-cache": {
+      "version": "1.0.4",
+      "resolved": "http://registry.npm.taobao.org/lazy-cache/download/lazy-cache-1.0.4.tgz",
+      "integrity": "sha1-odePw6UEdMuAhF07O24dpJpEbo4=",
+      "dev": true
+    },
+    "lazystream": {
+      "version": "1.0.0",
+      "resolved": "http://registry.npm.taobao.org/lazystream/download/lazystream-1.0.0.tgz",
+      "integrity": "sha1-9plf4PggOS9hOWvolGJAe7dxaOQ=",
+      "dev": true,
+      "requires": {
+        "readable-stream": "2.3.3"
+      }
+    },
+    "lcid": {
+      "version": "1.0.0",
+      "resolved": "http://registry.npm.taobao.org/lcid/download/lcid-1.0.0.tgz",
+      "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=",
+      "dev": true,
+      "requires": {
+        "invert-kv": "1.0.0"
+      }
+    },
+    "levn": {
+      "version": "0.3.0",
+      "resolved": "http://registry.npm.taobao.org/levn/download/levn-0.3.0.tgz",
+      "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=",
+      "dev": true,
+      "requires": {
+        "prelude-ls": "1.1.2",
+        "type-check": "0.3.2"
+      }
+    },
+    "load-json-file": {
+      "version": "1.1.0",
+      "resolved": "http://registry.npm.taobao.org/load-json-file/download/load-json-file-1.1.0.tgz",
+      "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=",
+      "dev": true,
+      "requires": {
+        "graceful-fs": "4.1.11",
+        "parse-json": "2.2.0",
+        "pify": "2.3.0",
+        "pinkie-promise": "2.0.1",
+        "strip-bom": "2.0.0"
+      }
+    },
+    "loader-utils": {
+      "version": "0.2.17",
+      "resolved": "http://registry.npm.taobao.org/loader-utils/download/loader-utils-0.2.17.tgz",
+      "integrity": "sha1-+G5jdNQyBabmxg6RlvF8Apm/s0g=",
+      "dev": true,
+      "requires": {
+        "big.js": "3.2.0",
+        "emojis-list": "2.1.0",
+        "json5": "0.5.1",
+        "object-assign": "4.1.1"
+      }
+    },
+    "lodash": {
+      "version": "4.17.4",
+      "resolved": "http://registry.npm.taobao.org/lodash/download/lodash-4.17.4.tgz",
+      "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4="
+    },
+    "lodash-es": {
+      "version": "4.17.4",
+      "resolved": "http://registry.npm.taobao.org/lodash-es/download/lodash-es-4.17.4.tgz",
+      "integrity": "sha1-3MHXVS4VCgZABzupyzHXDwMpUOc="
+    },
+    "lodash._baseassign": {
+      "version": "3.2.0",
+      "resolved": "http://registry.npm.taobao.org/lodash._baseassign/download/lodash._baseassign-3.2.0.tgz",
+      "integrity": "sha1-jDigmVAPIVrQnlnxci/QxSv+Ck4=",
+      "dev": true,
+      "requires": {
+        "lodash._basecopy": "3.0.1",
+        "lodash.keys": "3.1.2"
+      }
+    },
+    "lodash._basecopy": {
+      "version": "3.0.1",
+      "resolved": "http://registry.npm.taobao.org/lodash._basecopy/download/lodash._basecopy-3.0.1.tgz",
+      "integrity": "sha1-jaDmqHbPNEwK2KVIghEd08XHyjY=",
+      "dev": true
+    },
+    "lodash._bindcallback": {
+      "version": "3.0.1",
+      "resolved": "http://registry.npm.taobao.org/lodash._bindcallback/download/lodash._bindcallback-3.0.1.tgz",
+      "integrity": "sha1-5THCdkTPi1epnhftlbNcdIeJOS4=",
+      "dev": true
+    },
+    "lodash._createassigner": {
+      "version": "3.1.1",
+      "resolved": "http://registry.npm.taobao.org/lodash._createassigner/download/lodash._createassigner-3.1.1.tgz",
+      "integrity": "sha1-g4pbri/aymOsIt7o4Z+k5taXCxE=",
+      "dev": true,
+      "requires": {
+        "lodash._bindcallback": "3.0.1",
+        "lodash._isiterateecall": "3.0.9",
+        "lodash.restparam": "3.6.1"
+      }
+    },
+    "lodash._getnative": {
+      "version": "3.9.1",
+      "resolved": "http://registry.npm.taobao.org/lodash._getnative/download/lodash._getnative-3.9.1.tgz",
+      "integrity": "sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U=",
+      "dev": true
+    },
+    "lodash._isiterateecall": {
+      "version": "3.0.9",
+      "resolved": "http://registry.npm.taobao.org/lodash._isiterateecall/download/lodash._isiterateecall-3.0.9.tgz",
+      "integrity": "sha1-UgOte6Ql+uhCRg5pbbnPPmqsBXw=",
+      "dev": true
+    },
+    "lodash.assign": {
+      "version": "4.2.0",
+      "resolved": "http://registry.npm.taobao.org/lodash.assign/download/lodash.assign-4.2.0.tgz",
+      "integrity": "sha1-DZnzzNem0mHRm9rrkkUAXShYCOc="
+    },
+    "lodash.clonedeep": {
+      "version": "4.5.0",
+      "resolved": "http://registry.npm.taobao.org/lodash.clonedeep/download/lodash.clonedeep-4.5.0.tgz",
+      "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=",
+      "dev": true
+    },
+    "lodash.defaults": {
+      "version": "4.2.0",
+      "resolved": "http://registry.npm.taobao.org/lodash.defaults/download/lodash.defaults-4.2.0.tgz",
+      "integrity": "sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw=",
+      "dev": true
+    },
+    "lodash.isarguments": {
+      "version": "3.1.0",
+      "resolved": "http://registry.npm.taobao.org/lodash.isarguments/download/lodash.isarguments-3.1.0.tgz",
+      "integrity": "sha1-L1c9hcaiQon/AGY7SRwdM4/zRYo=",
+      "dev": true
+    },
+    "lodash.isarray": {
+      "version": "3.0.4",
+      "resolved": "http://registry.npm.taobao.org/lodash.isarray/download/lodash.isarray-3.0.4.tgz",
+      "integrity": "sha1-eeTriMNqgSKvhvhEqpvNhRtfu1U=",
+      "dev": true
+    },
+    "lodash.isequal": {
+      "version": "4.5.0",
+      "resolved": "http://registry.npm.taobao.org/lodash.isequal/download/lodash.isequal-4.5.0.tgz",
+      "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA="
+    },
+    "lodash.isplainobject": {
+      "version": "4.0.6",
+      "resolved": "http://registry.npm.taobao.org/lodash.isplainobject/download/lodash.isplainobject-4.0.6.tgz",
+      "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs="
+    },
+    "lodash.keys": {
+      "version": "3.1.2",
+      "resolved": "http://registry.npm.taobao.org/lodash.keys/download/lodash.keys-3.1.2.tgz",
+      "integrity": "sha1-TbwEcrFWvlCgsoaFXRvQsMZWCYo=",
+      "dev": true,
+      "requires": {
+        "lodash._getnative": "3.9.1",
+        "lodash.isarguments": "3.1.0",
+        "lodash.isarray": "3.0.4"
+      }
+    },
+    "lodash.pick": {
+      "version": "4.4.0",
+      "resolved": "http://registry.npm.taobao.org/lodash.pick/download/lodash.pick-4.4.0.tgz",
+      "integrity": "sha1-UvBWEP/53tQiYRRB7R/BI6AwAbM=",
+      "dev": true
+    },
+    "lodash.pickby": {
+      "version": "4.6.0",
+      "resolved": "http://registry.npm.taobao.org/lodash.pickby/download/lodash.pickby-4.6.0.tgz",
+      "integrity": "sha1-feoh2MGNdwOifHBMFdO4SmfjOv8=",
+      "dev": true
+    },
+    "lodash.restparam": {
+      "version": "3.6.1",
+      "resolved": "http://registry.npm.taobao.org/lodash.restparam/download/lodash.restparam-3.6.1.tgz",
+      "integrity": "sha1-k2pOMJ7zMKdkXtQUWYbIWuWyCAU=",
+      "dev": true
+    },
+    "log-symbols": {
+      "version": "1.0.2",
+      "resolved": "http://registry.npm.taobao.org/log-symbols/download/log-symbols-1.0.2.tgz",
+      "integrity": "sha1-N2/3tY6jCGoPCfrMdGF+ylAeGhg=",
+      "dev": true,
+      "requires": {
+        "chalk": "1.1.3"
+      }
+    },
+    "lolex": {
+      "version": "1.3.2",
+      "resolved": "http://registry.npm.taobao.org/lolex/download/lolex-1.3.2.tgz",
+      "integrity": "sha1-fD2mL/yzDw9agKJWbKJORdigHzE=",
+      "dev": true
+    },
+    "longest": {
+      "version": "1.0.1",
+      "resolved": "http://registry.npm.taobao.org/longest/download/longest-1.0.1.tgz",
+      "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=",
+      "dev": true
+    },
+    "loose-envify": {
+      "version": "1.3.1",
+      "resolved": "http://registry.npm.taobao.org/loose-envify/download/loose-envify-1.3.1.tgz",
+      "integrity": "sha1-0aitM/qc4OcT1l/dCsi3SNR4yEg=",
+      "requires": {
+        "js-tokens": "3.0.2"
+      }
+    },
+    "loud-rejection": {
+      "version": "1.6.0",
+      "resolved": "http://registry.npm.taobao.org/loud-rejection/download/loud-rejection-1.6.0.tgz",
+      "integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=",
+      "dev": true,
+      "requires": {
+        "currently-unhandled": "0.4.1",
+        "signal-exit": "3.0.2"
+      }
+    },
+    "lru-cache": {
+      "version": "2.7.3",
+      "resolved": "http://registry.npm.taobao.org/lru-cache/download/lru-cache-2.7.3.tgz",
+      "integrity": "sha1-bUUk6LlV+V1PW1iFHOId1y+06VI=",
+      "dev": true
+    },
+    "map-obj": {
+      "version": "1.0.1",
+      "resolved": "http://registry.npm.taobao.org/map-obj/download/map-obj-1.0.1.tgz",
+      "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=",
+      "dev": true
+    },
+    "mdurl": {
+      "version": "1.0.1",
+      "resolved": "http://registry.npm.taobao.org/mdurl/download/mdurl-1.0.1.tgz",
+      "integrity": "sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4="
+    },
+    "media-typer": {
+      "version": "0.3.0",
+      "resolved": "http://registry.npm.taobao.org/media-typer/download/media-typer-0.3.0.tgz",
+      "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=",
+      "dev": true
+    },
+    "memory-fs": {
+      "version": "0.3.0",
+      "resolved": "http://registry.npm.taobao.org/memory-fs/download/memory-fs-0.3.0.tgz",
+      "integrity": "sha1-e8xrYp46Q+hx1+Kaymrop/FcuyA=",
+      "dev": true,
+      "requires": {
+        "errno": "0.1.6",
+        "readable-stream": "2.3.3"
+      }
+    },
+    "meow": {
+      "version": "3.7.0",
+      "resolved": "http://registry.npm.taobao.org/meow/download/meow-3.7.0.tgz",
+      "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=",
+      "dev": true,
+      "requires": {
+        "camelcase-keys": "2.1.0",
+        "decamelize": "1.2.0",
+        "loud-rejection": "1.6.0",
+        "map-obj": "1.0.1",
+        "minimist": "1.2.0",
+        "normalize-package-data": "2.4.0",
+        "object-assign": "4.1.1",
+        "read-pkg-up": "1.0.1",
+        "redent": "1.0.0",
+        "trim-newlines": "1.0.0"
+      },
+      "dependencies": {
+        "minimist": {
+          "version": "1.2.0",
+          "resolved": "http://registry.npm.taobao.org/minimist/download/minimist-1.2.0.tgz",
+          "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
+          "dev": true
+        }
+      }
+    },
+    "merge-descriptors": {
+      "version": "1.0.1",
+      "resolved": "http://registry.npm.taobao.org/merge-descriptors/download/merge-descriptors-1.0.1.tgz",
+      "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=",
+      "dev": true
+    },
+    "methods": {
+      "version": "1.1.2",
+      "resolved": "http://registry.npm.taobao.org/methods/download/methods-1.1.2.tgz",
+      "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=",
+      "dev": true
+    },
+    "micromatch": {
+      "version": "2.3.11",
+      "resolved": "http://registry.npm.taobao.org/micromatch/download/micromatch-2.3.11.tgz",
+      "integrity": "sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU=",
+      "dev": true,
+      "requires": {
+        "arr-diff": "2.0.0",
+        "array-unique": "0.2.1",
+        "braces": "1.8.5",
+        "expand-brackets": "0.1.5",
+        "extglob": "0.3.2",
+        "filename-regex": "2.0.1",
+        "is-extglob": "1.0.0",
+        "is-glob": "2.0.1",
+        "kind-of": "3.2.2",
+        "normalize-path": "2.1.1",
+        "object.omit": "2.0.1",
+        "parse-glob": "3.0.4",
+        "regex-cache": "0.4.4"
+      }
+    },
+    "mime": {
+      "version": "1.4.1",
+      "resolved": "http://registry.npm.taobao.org/mime/download/mime-1.4.1.tgz",
+      "integrity": "sha1-Eh+evEnjdm8xGnbh+hyAA8SwOqY=",
+      "dev": true
+    },
+    "mime-db": {
+      "version": "1.30.0",
+      "resolved": "http://registry.npm.taobao.org/mime-db/download/mime-db-1.30.0.tgz",
+      "integrity": "sha1-dMZD2i3Z1qRTmZY0ZbJtXKfXHwE=",
+      "dev": true
+    },
+    "mime-types": {
+      "version": "2.1.17",
+      "resolved": "http://registry.npm.taobao.org/mime-types/download/mime-types-2.1.17.tgz",
+      "integrity": "sha1-Cdejk/A+mVp5+K+Fe3Cp4KsWVXo=",
+      "dev": true,
+      "requires": {
+        "mime-db": "1.30.0"
+      }
+    },
+    "min-document": {
+      "version": "2.19.0",
+      "resolved": "http://registry.npm.taobao.org/min-document/download/min-document-2.19.0.tgz",
+      "integrity": "sha1-e9KC4/WELtKVu3SM3Z8f+iyCRoU=",
+      "dev": true,
+      "requires": {
+        "dom-walk": "0.1.1"
+      }
+    },
+    "minimatch": {
+      "version": "3.0.4",
+      "resolved": "http://registry.npm.taobao.org/minimatch/download/minimatch-3.0.4.tgz",
+      "integrity": "sha1-UWbihkV/AzBgZL5Ul+jbsMPTIIM=",
+      "requires": {
+        "brace-expansion": "1.1.8"
+      }
+    },
+    "minimist": {
+      "version": "0.0.8",
+      "resolved": "http://registry.npm.taobao.org/minimist/download/minimist-0.0.8.tgz",
+      "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0="
+    },
+    "mkdirp": {
+      "version": "0.5.1",
+      "resolved": "http://registry.npm.taobao.org/mkdirp/download/mkdirp-0.5.1.tgz",
+      "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
+      "requires": {
+        "minimist": "0.0.8"
+      }
+    },
+    "mocha": {
+      "version": "2.2.5",
+      "resolved": "http://registry.npm.taobao.org/mocha/download/mocha-2.2.5.tgz",
+      "integrity": "sha1-07cqT+SeyUOTU/GsiT28Qw2ZMUA=",
+      "dev": true,
+      "requires": {
+        "commander": "2.3.0",
+        "debug": "2.0.0",
+        "diff": "1.4.0",
+        "escape-string-regexp": "1.0.2",
+        "glob": "3.2.3",
+        "growl": "1.8.1",
+        "jade": "0.26.3",
+        "mkdirp": "0.5.0",
+        "supports-color": "1.2.1"
+      },
+      "dependencies": {
+        "commander": {
+          "version": "2.3.0",
+          "resolved": "http://registry.npm.taobao.org/commander/download/commander-2.3.0.tgz",
+          "integrity": "sha1-/UMOiJgy7DU7ms0d4hfBHLPu+HM=",
+          "dev": true
+        },
+        "debug": {
+          "version": "2.0.0",
+          "resolved": "http://registry.npm.taobao.org/debug/download/debug-2.0.0.tgz",
+          "integrity": "sha1-ib2d9nMrUSVrxnBTQrugLtEhMe8=",
+          "dev": true,
+          "requires": {
+            "ms": "0.6.2"
+          }
+        },
+        "escape-string-regexp": {
+          "version": "1.0.2",
+          "resolved": "http://registry.npm.taobao.org/escape-string-regexp/download/escape-string-regexp-1.0.2.tgz",
+          "integrity": "sha1-Tbwv5nTnGUnK8/smlc5/LcHZqNE=",
+          "dev": true
+        },
+        "glob": {
+          "version": "3.2.3",
+          "resolved": "http://registry.npm.taobao.org/glob/download/glob-3.2.3.tgz",
+          "integrity": "sha1-4xPusknHr/qlxHUoaw4RW1mDlGc=",
+          "dev": true,
+          "requires": {
+            "graceful-fs": "2.0.3",
+            "inherits": "2.0.3",
+            "minimatch": "0.2.14"
+          }
+        },
+        "graceful-fs": {
+          "version": "2.0.3",
+          "resolved": "http://registry.npm.taobao.org/graceful-fs/download/graceful-fs-2.0.3.tgz",
+          "integrity": "sha1-fNLNsiiko/Nule+mzBQt59GhNtA=",
+          "dev": true
+        },
+        "minimatch": {
+          "version": "0.2.14",
+          "resolved": "http://registry.npm.taobao.org/minimatch/download/minimatch-0.2.14.tgz",
+          "integrity": "sha1-x054BXT2PG+aCQ6Q775u9TpqdWo=",
+          "dev": true,
+          "requires": {
+            "lru-cache": "2.7.3",
+            "sigmund": "1.0.1"
+          }
+        },
+        "mkdirp": {
+          "version": "0.5.0",
+          "resolved": "http://registry.npm.taobao.org/mkdirp/download/mkdirp-0.5.0.tgz",
+          "integrity": "sha1-HXMHam35hs2TROFecfzAWkyavxI=",
+          "dev": true,
+          "requires": {
+            "minimist": "0.0.8"
+          }
+        },
+        "ms": {
+          "version": "0.6.2",
+          "resolved": "http://registry.npm.taobao.org/ms/download/ms-0.6.2.tgz",
+          "integrity": "sha1-2JwhJMb9wTU9Zai3e/GqxLGTcIw=",
+          "dev": true
+        },
+        "supports-color": {
+          "version": "1.2.1",
+          "resolved": "http://registry.npm.taobao.org/supports-color/download/supports-color-1.2.1.tgz",
+          "integrity": "sha1-Eu4hUHCGzZjBBY2ewPSsR2t687I=",
+          "dev": true
+        }
+      }
+    },
+    "moment": {
+      "version": "2.14.1",
+      "resolved": "http://registry.npm.taobao.org/moment/download/moment-2.14.1.tgz",
+      "integrity": "sha1-s1snxH5X7S3ccAU9awe+zbKRdBw="
+    },
+    "moment-timezone": {
+      "version": "0.5.14",
+      "resolved": "http://registry.npm.taobao.org/moment-timezone/download/moment-timezone-0.5.14.tgz",
+      "integrity": "sha1-TrOP+VOLgBCLpGekWPPtQmjM/LE=",
+      "requires": {
+        "moment": "2.14.1"
+      }
+    },
+    "ms": {
+      "version": "2.0.0",
+      "resolved": "http://registry.npm.taobao.org/ms/download/ms-2.0.0.tgz",
+      "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
+    },
+    "mute-stream": {
+      "version": "0.0.5",
+      "resolved": "http://registry.npm.taobao.org/mute-stream/download/mute-stream-0.0.5.tgz",
+      "integrity": "sha1-j7+rsKmKJT0xhDMfno3rc3L6xsA=",
+      "dev": true
+    },
+    "nan": {
+      "version": "2.8.0",
+      "resolved": "http://registry.npm.taobao.org/nan/download/nan-2.8.0.tgz",
+      "integrity": "sha1-7XFfP+neArV6XmJS2QqWZ14fCFo=",
+      "dev": true
+    },
+    "natural-compare": {
+      "version": "1.4.0",
+      "resolved": "http://registry.npm.taobao.org/natural-compare/download/natural-compare-1.4.0.tgz",
+      "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=",
+      "dev": true
+    },
+    "negotiator": {
+      "version": "0.6.1",
+      "resolved": "http://registry.npm.taobao.org/negotiator/download/negotiator-0.6.1.tgz",
+      "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=",
+      "dev": true
+    },
+    "nock": {
+      "version": "4.0.0",
+      "resolved": "http://registry.npm.taobao.org/nock/download/nock-4.0.0.tgz",
+      "integrity": "sha1-2+ZbJeh6wVZVpRLOq2ZGdQuEoHc=",
+      "dev": true,
+      "requires": {
+        "chai": "3.5.0",
+        "debug": "2.6.9",
+        "deep-equal": "1.0.1",
+        "json-stringify-safe": "5.0.1",
+        "lodash": "2.4.1",
+        "mkdirp": "0.5.1",
+        "propagate": "0.3.1"
+      },
+      "dependencies": {
+        "lodash": {
+          "version": "2.4.1",
+          "resolved": "http://registry.npm.taobao.org/lodash/download/lodash-2.4.1.tgz",
+          "integrity": "sha1-W3cjA03aTSYuWkb7LFjXzCL3FCA=",
+          "dev": true
+        }
+      }
+    },
+    "node-fetch": {
+      "version": "1.6.3",
+      "resolved": "http://registry.npm.taobao.org/node-fetch/download/node-fetch-1.6.3.tgz",
+      "integrity": "sha1-3CNO3WSJmC1Y6PDbT2lQKavNjAQ=",
+      "requires": {
+        "encoding": "0.1.12",
+        "is-stream": "1.1.0"
+      }
+    },
+    "node-gyp": {
+      "version": "3.6.2",
+      "resolved": "http://registry.npm.taobao.org/node-gyp/download/node-gyp-3.6.2.tgz",
+      "integrity": "sha1-m/vlRWIoYoSDjnUOrAUpWFP6HGA=",
+      "dev": true,
+      "requires": {
+        "fstream": "1.0.11",
+        "glob": "7.1.2",
+        "graceful-fs": "4.1.11",
+        "minimatch": "3.0.4",
+        "mkdirp": "0.5.1",
+        "nopt": "3.0.6",
+        "npmlog": "4.1.2",
+        "osenv": "0.1.4",
+        "request": "2.83.0",
+        "rimraf": "2.6.2",
+        "semver": "5.3.0",
+        "tar": "2.2.1",
+        "which": "1.3.0"
+      },
+      "dependencies": {
+        "glob": {
+          "version": "7.1.2",
+          "resolved": "http://registry.npm.taobao.org/glob/download/glob-7.1.2.tgz",
+          "integrity": "sha1-wZyd+aAocC1nhhI4SmVSQExjbRU=",
+          "dev": true,
+          "requires": {
+            "fs.realpath": "1.0.0",
+            "inflight": "1.0.6",
+            "inherits": "2.0.3",
+            "minimatch": "3.0.4",
+            "once": "1.4.0",
+            "path-is-absolute": "1.0.1"
+          }
+        },
+        "semver": {
+          "version": "5.3.0",
+          "resolved": "http://registry.npm.taobao.org/semver/download/semver-5.3.0.tgz",
+          "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=",
+          "dev": true
+        }
+      }
+    },
+    "node-libs-browser": {
+      "version": "0.7.0",
+      "resolved": "http://registry.npm.taobao.org/node-libs-browser/download/node-libs-browser-0.7.0.tgz",
+      "integrity": "sha1-PicsCBnjCJNeJmdECNevDhSRuDs=",
+      "dev": true,
+      "requires": {
+        "assert": "1.4.1",
+        "browserify-zlib": "0.1.4",
+        "buffer": "4.9.1",
+        "console-browserify": "1.1.0",
+        "constants-browserify": "1.0.0",
+        "crypto-browserify": "3.3.0",
+        "domain-browser": "1.1.7",
+        "events": "1.1.1",
+        "https-browserify": "0.0.1",
+        "os-browserify": "0.2.1",
+        "path-browserify": "0.0.0",
+        "process": "0.11.10",
+        "punycode": "1.4.1",
+        "querystring-es3": "0.2.1",
+        "readable-stream": "2.3.3",
+        "stream-browserify": "2.0.1",
+        "stream-http": "2.7.2",
+        "string_decoder": "0.10.31",
+        "timers-browserify": "2.0.4",
+        "tty-browserify": "0.0.0",
+        "url": "0.11.0",
+        "util": "0.10.3",
+        "vm-browserify": "0.0.4"
+      },
+      "dependencies": {
+        "process": {
+          "version": "0.11.10",
+          "resolved": "http://registry.npm.taobao.org/process/download/process-0.11.10.tgz",
+          "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=",
+          "dev": true
+        },
+        "string_decoder": {
+          "version": "0.10.31",
+          "resolved": "http://registry.npm.taobao.org/string_decoder/download/string_decoder-0.10.31.tgz",
+          "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=",
+          "dev": true
+        }
+      }
+    },
+    "node-sass": {
+      "version": "3.8.0",
+      "resolved": "http://registry.npm.taobao.org/node-sass/download/node-sass-3.8.0.tgz",
+      "integrity": "sha1-7A+JrmYl4dmQ3H/3E7J16hXf7gU=",
+      "dev": true,
+      "requires": {
+        "async-foreach": "0.1.3",
+        "chalk": "1.1.3",
+        "cross-spawn": "3.0.1",
+        "gaze": "1.1.2",
+        "get-stdin": "4.0.1",
+        "glob": "7.1.2",
+        "in-publish": "2.0.0",
+        "lodash.clonedeep": "4.5.0",
+        "meow": "3.7.0",
+        "mkdirp": "0.5.1",
+        "nan": "2.8.0",
+        "node-gyp": "3.6.2",
+        "request": "2.83.0",
+        "sass-graph": "2.2.4"
+      },
+      "dependencies": {
+        "glob": {
+          "version": "7.1.2",
+          "resolved": "http://registry.npm.taobao.org/glob/download/glob-7.1.2.tgz",
+          "integrity": "sha1-wZyd+aAocC1nhhI4SmVSQExjbRU=",
+          "dev": true,
+          "requires": {
+            "fs.realpath": "1.0.0",
+            "inflight": "1.0.6",
+            "inherits": "2.0.3",
+            "minimatch": "3.0.4",
+            "once": "1.4.0",
+            "path-is-absolute": "1.0.1"
+          }
+        }
+      }
+    },
+    "noop2": {
+      "version": "2.0.0",
+      "resolved": "http://registry.npm.taobao.org/noop2/download/noop2-2.0.0.tgz",
+      "integrity": "sha1-S2NgFemIK1R4PAK0EvaZ2MXNCls=",
+      "dev": true
+    },
+    "nopt": {
+      "version": "3.0.6",
+      "resolved": "http://registry.npm.taobao.org/nopt/download/nopt-3.0.6.tgz",
+      "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=",
+      "dev": true,
+      "requires": {
+        "abbrev": "1.1.1"
+      }
+    },
+    "normalize-package-data": {
+      "version": "2.4.0",
+      "resolved": "http://registry.npm.taobao.org/normalize-package-data/download/normalize-package-data-2.4.0.tgz",
+      "integrity": "sha1-EvlaMH1YNSB1oEkHuErIvpisAS8=",
+      "dev": true,
+      "requires": {
+        "hosted-git-info": "2.5.0",
+        "is-builtin-module": "1.0.0",
+        "semver": "4.3.6",
+        "validate-npm-package-license": "3.0.1"
+      }
+    },
+    "normalize-path": {
+      "version": "2.1.1",
+      "resolved": "http://registry.npm.taobao.org/normalize-path/download/normalize-path-2.1.1.tgz",
+      "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=",
+      "dev": true,
+      "requires": {
+        "remove-trailing-separator": "1.1.0"
+      }
+    },
+    "normalize-range": {
+      "version": "0.1.2",
+      "resolved": "http://registry.npm.taobao.org/normalize-range/download/normalize-range-0.1.2.tgz",
+      "integrity": "sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=",
+      "dev": true
+    },
+    "npm-install-package": {
+      "version": "1.1.0",
+      "resolved": "http://registry.npm.taobao.org/npm-install-package/download/npm-install-package-1.1.0.tgz",
+      "integrity": "sha1-+fwfhMLTH2EFYx4LX18oDRFR2X4=",
+      "dev": true,
+      "requires": {
+        "noop2": "2.0.0"
+      }
+    },
+    "npmlog": {
+      "version": "4.1.2",
+      "resolved": "http://registry.npm.taobao.org/npmlog/download/npmlog-4.1.2.tgz",
+      "integrity": "sha1-CKfyqL9zRgR3mp76StXMcXq7lUs=",
+      "dev": true,
+      "requires": {
+        "are-we-there-yet": "1.1.4",
+        "console-control-strings": "1.1.0",
+        "gauge": "2.7.4",
+        "set-blocking": "2.0.0"
+      }
+    },
+    "num2fraction": {
+      "version": "1.2.2",
+      "resolved": "http://registry.npm.taobao.org/num2fraction/download/num2fraction-1.2.2.tgz",
+      "integrity": "sha1-b2gragJ6Tp3fpFZM0lidHU5mnt4=",
+      "dev": true
+    },
+    "number-is-nan": {
+      "version": "1.0.1",
+      "resolved": "http://registry.npm.taobao.org/number-is-nan/download/number-is-nan-1.0.1.tgz",
+      "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0="
+    },
+    "oauth-sign": {
+      "version": "0.8.2",
+      "resolved": "http://registry.npm.taobao.org/oauth-sign/download/oauth-sign-0.8.2.tgz",
+      "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=",
+      "dev": true
+    },
+    "object-assign": {
+      "version": "4.1.1",
+      "resolved": "http://registry.npm.taobao.org/object-assign/download/object-assign-4.1.1.tgz",
+      "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM="
+    },
+    "object-keys": {
+      "version": "1.0.11",
+      "resolved": "http://registry.npm.taobao.org/object-keys/download/object-keys-1.0.11.tgz",
+      "integrity": "sha1-xUYBd4rVYPEULODgG8yotW0TQm0=",
+      "dev": true
+    },
+    "object.assign": {
+      "version": "4.1.0",
+      "resolved": "http://registry.npm.taobao.org/object.assign/download/object.assign-4.1.0.tgz",
+      "integrity": "sha1-lovxEA15Vrs8oIbwBvhGs7xACNo=",
+      "dev": true,
+      "requires": {
+        "define-properties": "1.1.2",
+        "function-bind": "1.1.1",
+        "has-symbols": "1.0.0",
+        "object-keys": "1.0.11"
+      }
+    },
+    "object.omit": {
+      "version": "2.0.1",
+      "resolved": "http://registry.npm.taobao.org/object.omit/download/object.omit-2.0.1.tgz",
+      "integrity": "sha1-Gpx0SCnznbuFjHbKNXmuKlTr0fo=",
+      "dev": true,
+      "requires": {
+        "for-own": "0.1.5",
+        "is-extendable": "0.1.1"
+      }
+    },
+    "on-finished": {
+      "version": "2.3.0",
+      "resolved": "http://registry.npm.taobao.org/on-finished/download/on-finished-2.3.0.tgz",
+      "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=",
+      "dev": true,
+      "requires": {
+        "ee-first": "1.1.1"
+      }
+    },
+    "once": {
+      "version": "1.4.0",
+      "resolved": "http://registry.npm.taobao.org/once/download/once-1.4.0.tgz",
+      "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
+      "dev": true,
+      "requires": {
+        "wrappy": "1.0.2"
+      }
+    },
+    "onetime": {
+      "version": "1.1.0",
+      "resolved": "http://registry.npm.taobao.org/onetime/download/onetime-1.1.0.tgz",
+      "integrity": "sha1-ofeDj4MUxRbwXs78vEzP4EtO14k=",
+      "dev": true
+    },
+    "optimist": {
+      "version": "0.6.1",
+      "resolved": "http://registry.npm.taobao.org/optimist/download/optimist-0.6.1.tgz",
+      "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=",
+      "dev": true,
+      "requires": {
+        "minimist": "0.0.8",
+        "wordwrap": "0.0.3"
+      },
+      "dependencies": {
+        "wordwrap": {
+          "version": "0.0.3",
+          "resolved": "http://registry.npm.taobao.org/wordwrap/download/wordwrap-0.0.3.tgz",
+          "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=",
+          "dev": true
+        }
+      }
+    },
+    "optionator": {
+      "version": "0.8.2",
+      "resolved": "http://registry.npm.taobao.org/optionator/download/optionator-0.8.2.tgz",
+      "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=",
+      "dev": true,
+      "requires": {
+        "deep-is": "0.1.3",
+        "fast-levenshtein": "2.0.6",
+        "levn": "0.3.0",
+        "prelude-ls": "1.1.2",
+        "type-check": "0.3.2",
+        "wordwrap": "1.0.0"
+      }
+    },
+    "os-browserify": {
+      "version": "0.2.1",
+      "resolved": "http://registry.npm.taobao.org/os-browserify/download/os-browserify-0.2.1.tgz",
+      "integrity": "sha1-Y/xMzuXS13Y9Jrv4YBB45sLgBE8=",
+      "dev": true
+    },
+    "os-homedir": {
+      "version": "1.0.2",
+      "resolved": "http://registry.npm.taobao.org/os-homedir/download/os-homedir-1.0.2.tgz",
+      "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M="
+    },
+    "os-locale": {
+      "version": "1.4.0",
+      "resolved": "http://registry.npm.taobao.org/os-locale/download/os-locale-1.4.0.tgz",
+      "integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=",
+      "dev": true,
+      "requires": {
+        "lcid": "1.0.0"
+      }
+    },
+    "os-shim": {
+      "version": "0.1.3",
+      "resolved": "http://registry.npm.taobao.org/os-shim/download/os-shim-0.1.3.tgz",
+      "integrity": "sha1-a2LDeRz3kJ6jXtRuF2WLtBfLORc=",
+      "dev": true
+    },
+    "os-tmpdir": {
+      "version": "1.0.2",
+      "resolved": "http://registry.npm.taobao.org/os-tmpdir/download/os-tmpdir-1.0.2.tgz",
+      "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ="
+    },
+    "osenv": {
+      "version": "0.1.4",
+      "resolved": "http://registry.npm.taobao.org/osenv/download/osenv-0.1.4.tgz",
+      "integrity": "sha1-Qv5tWVPfBsgGS+bxdsPQWqqjRkQ=",
+      "dev": true,
+      "requires": {
+        "os-homedir": "1.0.2",
+        "os-tmpdir": "1.0.2"
+      }
+    },
+    "output-file-sync": {
+      "version": "1.1.2",
+      "resolved": "http://registry.npm.taobao.org/output-file-sync/download/output-file-sync-1.1.2.tgz",
+      "integrity": "sha1-0KM+7+YaIF+suQCS6CZZjVJFznY=",
+      "dev": true,
+      "requires": {
+        "graceful-fs": "4.1.11",
+        "mkdirp": "0.5.1",
+        "object-assign": "4.1.1"
+      }
+    },
+    "pako": {
+      "version": "0.2.9",
+      "resolved": "http://registry.npm.taobao.org/pako/download/pako-0.2.9.tgz",
+      "integrity": "sha1-8/dSL073gjSNqBYbrZ7P1Rv4OnU=",
+      "dev": true
+    },
+    "parse-glob": {
+      "version": "3.0.4",
+      "resolved": "http://registry.npm.taobao.org/parse-glob/download/parse-glob-3.0.4.tgz",
+      "integrity": "sha1-ssN2z7EfNVE7rdFz7wu246OIORw=",
+      "dev": true,
+      "requires": {
+        "glob-base": "0.3.0",
+        "is-dotfile": "1.0.3",
+        "is-extglob": "1.0.0",
+        "is-glob": "2.0.1"
+      }
+    },
+    "parse-json": {
+      "version": "2.2.0",
+      "resolved": "http://registry.npm.taobao.org/parse-json/download/parse-json-2.2.0.tgz",
+      "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=",
+      "dev": true,
+      "requires": {
+        "error-ex": "1.3.1"
+      }
+    },
+    "parseurl": {
+      "version": "1.3.2",
+      "resolved": "http://registry.npm.taobao.org/parseurl/download/parseurl-1.3.2.tgz",
+      "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=",
+      "dev": true
+    },
+    "pascalcase": {
+      "version": "0.1.1",
+      "resolved": "http://registry.npm.taobao.org/pascalcase/download/pascalcase-0.1.1.tgz",
+      "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ="
+    },
+    "path-browserify": {
+      "version": "0.0.0",
+      "resolved": "http://registry.npm.taobao.org/path-browserify/download/path-browserify-0.0.0.tgz",
+      "integrity": "sha1-oLhwcpquIUAFt9UDLsLLuw+0RRo=",
+      "dev": true
+    },
+    "path-exists": {
+      "version": "1.0.0",
+      "resolved": "http://registry.npm.taobao.org/path-exists/download/path-exists-1.0.0.tgz",
+      "integrity": "sha1-1aiZjrce83p0w06w2eum6HjuoIE=",
+      "dev": true
+    },
+    "path-is-absolute": {
+      "version": "1.0.1",
+      "resolved": "http://registry.npm.taobao.org/path-is-absolute/download/path-is-absolute-1.0.1.tgz",
+      "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18="
+    },
+    "path-is-inside": {
+      "version": "1.0.2",
+      "resolved": "http://registry.npm.taobao.org/path-is-inside/download/path-is-inside-1.0.2.tgz",
+      "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=",
+      "dev": true
+    },
+    "path-parse": {
+      "version": "1.0.5",
+      "resolved": "http://registry.npm.taobao.org/path-parse/download/path-parse-1.0.5.tgz",
+      "integrity": "sha1-PBrfhx6pzWyUMbbqK9dKD/BVxME=",
+      "dev": true
+    },
+    "path-to-regexp": {
+      "version": "0.1.7",
+      "resolved": "http://registry.npm.taobao.org/path-to-regexp/download/path-to-regexp-0.1.7.tgz",
+      "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=",
+      "dev": true
+    },
+    "path-type": {
+      "version": "1.1.0",
+      "resolved": "http://registry.npm.taobao.org/path-type/download/path-type-1.1.0.tgz",
+      "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=",
+      "dev": true,
+      "requires": {
+        "graceful-fs": "4.1.11",
+        "pify": "2.3.0",
+        "pinkie-promise": "2.0.1"
+      }
+    },
+    "pbkdf2-compat": {
+      "version": "2.0.1",
+      "resolved": "http://registry.npm.taobao.org/pbkdf2-compat/download/pbkdf2-compat-2.0.1.tgz",
+      "integrity": "sha1-tuDI+plJTZTgURV1gCpZpcFC8og=",
+      "dev": true
+    },
+    "pend": {
+      "version": "1.2.0",
+      "resolved": "http://registry.npm.taobao.org/pend/download/pend-1.2.0.tgz",
+      "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=",
+      "dev": true
+    },
+    "performance-now": {
+      "version": "2.1.0",
+      "resolved": "http://registry.npm.taobao.org/performance-now/download/performance-now-2.1.0.tgz",
+      "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=",
+      "dev": true
+    },
+    "pify": {
+      "version": "2.3.0",
+      "resolved": "http://registry.npm.taobao.org/pify/download/pify-2.3.0.tgz",
+      "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=",
+      "dev": true
+    },
+    "pinkie": {
+      "version": "2.0.4",
+      "resolved": "http://registry.npm.taobao.org/pinkie/download/pinkie-2.0.4.tgz",
+      "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=",
+      "dev": true
+    },
+    "pinkie-promise": {
+      "version": "2.0.1",
+      "resolved": "http://registry.npm.taobao.org/pinkie-promise/download/pinkie-promise-2.0.1.tgz",
+      "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=",
+      "dev": true,
+      "requires": {
+        "pinkie": "2.0.4"
+      }
+    },
+    "pkg-dir": {
+      "version": "1.0.0",
+      "resolved": "http://registry.npm.taobao.org/pkg-dir/download/pkg-dir-1.0.0.tgz",
+      "integrity": "sha1-ektQio1bstYp1EcFb/TpyTFM89Q=",
+      "dev": true,
+      "requires": {
+        "find-up": "1.1.2"
+      }
+    },
+    "pluralize": {
+      "version": "3.0.0",
+      "resolved": "http://registry.npm.taobao.org/pluralize/download/pluralize-3.0.0.tgz",
+      "integrity": "sha1-cnmanvQaU/8MA95lIuKGtA1cky0="
+    },
+    "postcss": {
+      "version": "5.2.18",
+      "resolved": "http://registry.npm.taobao.org/postcss/download/postcss-5.2.18.tgz",
+      "integrity": "sha1-ut+hSX1GJE9jkPWLMZgw2RB4U8U=",
+      "dev": true,
+      "requires": {
+        "chalk": "1.1.3",
+        "js-base64": "2.4.0",
+        "source-map": "0.5.7",
+        "supports-color": "3.2.3"
+      }
+    },
+    "postcss-load-config": {
+      "version": "1.2.0",
+      "resolved": "http://registry.npm.taobao.org/postcss-load-config/download/postcss-load-config-1.2.0.tgz",
+      "integrity": "sha1-U56a/J3chiASHr+djDZz4M5Q0oo=",
+      "dev": true,
+      "requires": {
+        "cosmiconfig": "2.2.2",
+        "object-assign": "4.1.1",
+        "postcss-load-options": "1.2.0",
+        "postcss-load-plugins": "2.3.0"
+      }
+    },
+    "postcss-load-options": {
+      "version": "1.2.0",
+      "resolved": "http://registry.npm.taobao.org/postcss-load-options/download/postcss-load-options-1.2.0.tgz",
+      "integrity": "sha1-sJixVZ3awt8EvAuzdfmaXP4rbYw=",
+      "dev": true,
+      "requires": {
+        "cosmiconfig": "2.2.2",
+        "object-assign": "4.1.1"
+      }
+    },
+    "postcss-load-plugins": {
+      "version": "2.3.0",
+      "resolved": "http://registry.npm.taobao.org/postcss-load-plugins/download/postcss-load-plugins-2.3.0.tgz",
+      "integrity": "sha1-dFdoEWWZrKLwCfrUJrABdQSdjZI=",
+      "dev": true,
+      "requires": {
+        "cosmiconfig": "2.2.2",
+        "object-assign": "4.1.1"
+      }
+    },
+    "postcss-loader": {
+      "version": "1.1.1",
+      "resolved": "http://registry.npm.taobao.org/postcss-loader/download/postcss-loader-1.1.1.tgz",
+      "integrity": "sha1-y8TLdfvc+myfw8fsNCrgQGTxyPs=",
+      "dev": true,
+      "requires": {
+        "loader-utils": "0.2.17",
+        "object-assign": "4.1.1",
+        "postcss": "5.2.18",
+        "postcss-load-config": "1.2.0"
+      }
+    },
+    "postcss-value-parser": {
+      "version": "3.3.0",
+      "resolved": "http://registry.npm.taobao.org/postcss-value-parser/download/postcss-value-parser-3.3.0.tgz",
+      "integrity": "sha1-h/OPnxj3dKSrTIojL1xc6IcqnRU=",
+      "dev": true
+    },
+    "prelude-ls": {
+      "version": "1.1.2",
+      "resolved": "http://registry.npm.taobao.org/prelude-ls/download/prelude-ls-1.1.2.tgz",
+      "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=",
+      "dev": true
+    },
+    "preserve": {
+      "version": "0.2.0",
+      "resolved": "http://registry.npm.taobao.org/preserve/download/preserve-0.2.0.tgz",
+      "integrity": "sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks=",
+      "dev": true
+    },
+    "private": {
+      "version": "0.1.8",
+      "resolved": "http://registry.npm.taobao.org/private/download/private-0.1.8.tgz",
+      "integrity": "sha1-I4Hts2ifelPWUxkAYPz4ItLzaP8="
+    },
+    "process": {
+      "version": "0.5.2",
+      "resolved": "http://registry.npm.taobao.org/process/download/process-0.5.2.tgz",
+      "integrity": "sha1-FjjYqONML0QKkduVq5rrZ3/Bhc8=",
+      "dev": true
+    },
+    "process-nextick-args": {
+      "version": "1.0.7",
+      "resolved": "http://registry.npm.taobao.org/process-nextick-args/download/process-nextick-args-1.0.7.tgz",
+      "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=",
+      "dev": true
+    },
+    "progress": {
+      "version": "1.1.8",
+      "resolved": "http://registry.npm.taobao.org/progress/download/progress-1.1.8.tgz",
+      "integrity": "sha1-4mDHj2Fhzdmw5WzD4Khd4Xx6V74=",
+      "dev": true
+    },
+    "promise": {
+      "version": "7.3.1",
+      "resolved": "http://registry.npm.taobao.org/promise/download/promise-7.3.1.tgz",
+      "integrity": "sha1-BktyYCsY+Q8pGSuLG8QY/9Hr078=",
+      "requires": {
+        "asap": "2.0.6"
+      }
+    },
+    "prop-types": {
+      "version": "15.6.0",
+      "resolved": "http://registry.npm.taobao.org/prop-types/download/prop-types-15.6.0.tgz",
+      "integrity": "sha1-zq8IMCL8RrSjX2nhPvda7Q1jmFY=",
+      "requires": {
+        "fbjs": "0.8.16",
+        "loose-envify": "1.3.1",
+        "object-assign": "4.1.1"
+      }
+    },
+    "propagate": {
+      "version": "0.3.1",
+      "resolved": "http://registry.npm.taobao.org/propagate/download/propagate-0.3.1.tgz",
+      "integrity": "sha1-46hEBKfs6CDda76p9tkk4xNa4Jw=",
+      "dev": true
+    },
+    "proxy-addr": {
+      "version": "2.0.2",
+      "resolved": "http://registry.npm.taobao.org/proxy-addr/download/proxy-addr-2.0.2.tgz",
+      "integrity": "sha1-ZXFQT0e7mI7IGAJT+F3X4UlSvew=",
+      "dev": true,
+      "requires": {
+        "forwarded": "0.1.2",
+        "ipaddr.js": "1.5.2"
+      }
+    },
+    "prr": {
+      "version": "1.0.1",
+      "resolved": "http://registry.npm.taobao.org/prr/download/prr-1.0.1.tgz",
+      "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=",
+      "dev": true
+    },
+    "pseudomap": {
+      "version": "1.0.2",
+      "resolved": "http://registry.npm.taobao.org/pseudomap/download/pseudomap-1.0.2.tgz",
+      "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=",
+      "dev": true
+    },
+    "punycode": {
+      "version": "1.4.1",
+      "resolved": "http://registry.npm.taobao.org/punycode/download/punycode-1.4.1.tgz",
+      "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=",
+      "dev": true
+    },
+    "q": {
+      "version": "1.4.1",
+      "resolved": "http://registry.npm.taobao.org/q/download/q-1.4.1.tgz",
+      "integrity": "sha1-VXBbzZPF82c1MMLCy8DCs63cKG4=",
+      "dev": true
+    },
+    "qs": {
+      "version": "6.5.1",
+      "resolved": "http://registry.npm.taobao.org/qs/download/qs-6.5.1.tgz",
+      "integrity": "sha1-NJzfbu+J7EXBLX1es/wMhwNDptg=",
+      "dev": true
+    },
+    "query-string": {
+      "version": "3.0.3",
+      "resolved": "http://registry.npm.taobao.org/query-string/download/query-string-3.0.3.tgz",
+      "integrity": "sha1-ri4UtNBQcdTpuetIc8NbDc1C5jg=",
+      "requires": {
+        "strict-uri-encode": "1.1.0"
+      }
+    },
+    "querystring": {
+      "version": "0.2.0",
+      "resolved": "http://registry.npm.taobao.org/querystring/download/querystring-0.2.0.tgz",
+      "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=",
+      "dev": true
+    },
+    "querystring-es3": {
+      "version": "0.2.1",
+      "resolved": "http://registry.npm.taobao.org/querystring-es3/download/querystring-es3-0.2.1.tgz",
+      "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=",
+      "dev": true
+    },
+    "randomatic": {
+      "version": "1.1.7",
+      "resolved": "http://registry.npm.taobao.org/randomatic/download/randomatic-1.1.7.tgz",
+      "integrity": "sha1-x6vpzIuHwLqodrGf3oP9RkeX44w=",
+      "dev": true,
+      "requires": {
+        "is-number": "3.0.0",
+        "kind-of": "4.0.0"
+      },
+      "dependencies": {
+        "is-number": {
+          "version": "3.0.0",
+          "resolved": "http://registry.npm.taobao.org/is-number/download/is-number-3.0.0.tgz",
+          "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=",
+          "dev": true,
+          "requires": {
+            "kind-of": "3.2.2"
+          },
+          "dependencies": {
+            "kind-of": {
+              "version": "3.2.2",
+              "resolved": "http://registry.npm.taobao.org/kind-of/download/kind-of-3.2.2.tgz",
+              "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+              "dev": true,
+              "requires": {
+                "is-buffer": "1.1.6"
+              }
+            }
+          }
+        },
+        "kind-of": {
+          "version": "4.0.0",
+          "resolved": "http://registry.npm.taobao.org/kind-of/download/kind-of-4.0.0.tgz",
+          "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=",
+          "dev": true,
+          "requires": {
+            "is-buffer": "1.1.6"
+          }
+        }
+      }
+    },
+    "range-parser": {
+      "version": "1.2.0",
+      "resolved": "http://registry.npm.taobao.org/range-parser/download/range-parser-1.2.0.tgz",
+      "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=",
+      "dev": true
+    },
+    "raw-body": {
+      "version": "2.3.2",
+      "resolved": "http://registry.npm.taobao.org/raw-body/download/raw-body-2.3.2.tgz",
+      "integrity": "sha1-vNYMd9Prk83gBQKVw/N5OJvIj4k=",
+      "dev": true,
+      "requires": {
+        "bytes": "3.0.0",
+        "http-errors": "1.6.2",
+        "iconv-lite": "0.4.19",
+        "unpipe": "1.0.0"
+      }
+    },
+    "react": {
+      "version": "15.5.4",
+      "resolved": "http://registry.npm.taobao.org/react/download/react-15.5.4.tgz",
+      "integrity": "sha1-+oPrAVBqsjfNwcjDsc6o3gEr8Ec=",
+      "requires": {
+        "fbjs": "0.8.16",
+        "loose-envify": "1.3.1",
+        "object-assign": "4.1.1",
+        "prop-types": "15.6.0"
+      }
+    },
+    "react-ace": {
+      "version": "3.5.0",
+      "resolved": "http://registry.npm.taobao.org/react-ace/download/react-ace-3.5.0.tgz",
+      "integrity": "sha1-uFr198l9C4zfs4U3OyS35B/e9kQ=",
+      "requires": {
+        "brace": "0.8.0",
+        "lodash.isequal": "4.5.0"
+      }
+    },
+    "react-autosuggest": {
+      "version": "6.0.4",
+      "resolved": "http://registry.npm.taobao.org/react-autosuggest/download/react-autosuggest-6.0.4.tgz",
+      "integrity": "sha1-d3mMaSeWxJ6Mdb8/SyEnhAKNPrM=",
+      "requires": {
+        "react-autowhatever": "5.4.0",
+        "react-redux": "4.4.8",
+        "redux": "3.5.2",
+        "shallow-equal": "1.0.0"
+      }
+    },
+    "react-autowhatever": {
+      "version": "5.4.0",
+      "resolved": "http://registry.npm.taobao.org/react-autowhatever/download/react-autowhatever-5.4.0.tgz",
+      "integrity": "sha1-Ay6n8MWgVRY9FGGii6rgNn25J5o=",
+      "requires": {
+        "react-themeable": "1.1.0",
+        "section-iterator": "2.0.0"
+      }
+    },
+    "react-bootstrap": {
+      "version": "0.30.10",
+      "resolved": "http://registry.npm.taobao.org/react-bootstrap/download/react-bootstrap-0.30.10.tgz",
+      "integrity": "sha1-27ppCVlfKvTZGTfbD5bsjC3y0ag=",
+      "requires": {
+        "babel-runtime": "6.26.0",
+        "classnames": "2.2.5",
+        "dom-helpers": "3.3.1",
+        "invariant": "2.2.2",
+        "keycode": "2.1.9",
+        "prop-types": "15.6.0",
+        "react-overlays": "0.6.12",
+        "react-prop-types": "0.4.0",
+        "uncontrollable": "4.1.0",
+        "warning": "3.0.0"
+      }
+    },
+    "react-deep-force-update": {
+      "version": "1.1.1",
+      "resolved": "http://registry.npm.taobao.org/react-deep-force-update/download/react-deep-force-update-1.1.1.tgz",
+      "integrity": "sha1-vNMUeAJ7ZLMznxCJIatSC0MT3Cw=",
+      "dev": true
+    },
+    "react-dom": {
+      "version": "15.5.4",
+      "resolved": "http://registry.npm.taobao.org/react-dom/download/react-dom-15.5.4.tgz",
+      "integrity": "sha1-ugwoeG/VLtfk8hNf4CiNRirvk9o=",
+      "requires": {
+        "fbjs": "0.8.16",
+        "loose-envify": "1.3.1",
+        "object-assign": "4.1.1",
+        "prop-types": "15.5.10"
+      },
+      "dependencies": {
+        "prop-types": {
+          "version": "15.5.10",
+          "resolved": "http://registry.npm.taobao.org/prop-types/download/prop-types-15.5.10.tgz",
+          "integrity": "sha1-J5ffwxJhguOpXj37suiT3ddFYVQ=",
+          "requires": {
+            "fbjs": "0.8.16",
+            "loose-envify": "1.3.1"
+          }
+        }
+      }
+    },
+    "react-hot-api": {
+      "version": "0.4.7",
+      "resolved": "http://registry.npm.taobao.org/react-hot-api/download/react-hot-api-0.4.7.tgz",
+      "integrity": "sha1-p+IqVtJS4Rq9k2a2EmTPRJLFgXE=",
+      "dev": true
+    },
+    "react-hot-loader": {
+      "version": "1.3.1",
+      "resolved": "http://registry.npm.taobao.org/react-hot-loader/download/react-hot-loader-1.3.1.tgz",
+      "integrity": "sha1-yVZHrni3Pfzv9uxx/8sEGC/22vk=",
+      "dev": true,
+      "requires": {
+        "react-hot-api": "0.4.7",
+        "source-map": "0.4.4"
+      },
+      "dependencies": {
+        "source-map": {
+          "version": "0.4.4",
+          "resolved": "http://registry.npm.taobao.org/source-map/download/source-map-0.4.4.tgz",
+          "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=",
+          "dev": true,
+          "requires": {
+            "amdefine": "1.0.1"
+          }
+        }
+      }
+    },
+    "react-lazy-cache": {
+      "version": "3.0.1",
+      "resolved": "http://registry.npm.taobao.org/react-lazy-cache/download/react-lazy-cache-3.0.1.tgz",
+      "integrity": "sha1-DcZNON8XZ+93Z4xclBkAZMsRsM0=",
+      "requires": {
+        "deep-equal": "1.0.1"
+      }
+    },
+    "react-markdown": {
+      "version": "2.5.1",
+      "resolved": "http://registry.npm.taobao.org/react-markdown/download/react-markdown-2.5.1.tgz",
+      "integrity": "sha1-96bCajpfr11MIJgVXZd16Cb9Vu4=",
+      "requires": {
+        "commonmark": "0.24.0",
+        "commonmark-react-renderer": "4.3.4",
+        "prop-types": "15.6.0"
+      }
+    },
+    "react-overlays": {
+      "version": "0.6.12",
+      "resolved": "http://registry.npm.taobao.org/react-overlays/download/react-overlays-0.6.12.tgz",
+      "integrity": "sha1-oHnHUMxCnX20x0dKlbS1QDPiVcM=",
+      "requires": {
+        "classnames": "2.2.5",
+        "dom-helpers": "3.3.1",
+        "react-prop-types": "0.4.0",
+        "warning": "3.0.0"
+      }
+    },
+    "react-prop-types": {
+      "version": "0.4.0",
+      "resolved": "http://registry.npm.taobao.org/react-prop-types/download/react-prop-types-0.4.0.tgz",
+      "integrity": "sha1-+ZsL+0AGkpya8gUefBQUpcdbk9A=",
+      "requires": {
+        "warning": "3.0.0"
+      }
+    },
+    "react-proxy": {
+      "version": "1.1.8",
+      "resolved": "http://registry.npm.taobao.org/react-proxy/download/react-proxy-1.1.8.tgz",
+      "integrity": "sha1-nb/Z2SdSjDqp9ETkVYw3gwq4wmo=",
+      "dev": true,
+      "requires": {
+        "lodash": "4.17.4",
+        "react-deep-force-update": "1.1.1"
+      }
+    },
+    "react-redux": {
+      "version": "4.4.8",
+      "resolved": "http://registry.npm.taobao.org/react-redux/download/react-redux-4.4.8.tgz",
+      "integrity": "sha1-57wd0QDotk6WrIIS2xEyObni4I8=",
+      "requires": {
+        "create-react-class": "15.6.2",
+        "hoist-non-react-statics": "1.2.0",
+        "invariant": "2.2.2",
+        "lodash": "4.17.4",
+        "loose-envify": "1.3.1",
+        "prop-types": "15.6.0"
+      }
+    },
+    "react-router": {
+      "version": "2.6.1",
+      "resolved": "http://registry.npm.taobao.org/react-router/download/react-router-2.6.1.tgz",
+      "integrity": "sha1-4EVNZr1hsSPZTbco+O0z2ZCL4iY=",
+      "requires": {
+        "history": "2.1.2",
+        "hoist-non-react-statics": "1.2.0",
+        "invariant": "2.2.2",
+        "loose-envify": "1.3.1",
+        "warning": "3.0.0"
+      }
+    },
+    "react-router-redux": {
+      "version": "4.0.8",
+      "resolved": "http://registry.npm.taobao.org/react-router-redux/download/react-router-redux-4.0.8.tgz",
+      "integrity": "sha1-InQDWWtRUeGCN32rg1tdRfD4BU4="
+    },
+    "react-router-scroll": {
+      "version": "0.3.3",
+      "resolved": "http://registry.npm.taobao.org/react-router-scroll/download/react-router-scroll-0.3.3.tgz",
+      "integrity": "sha1-5XIA/YY+co/OjpC+kz9TFZkPwOY=",
+      "requires": {
+        "history": "2.1.2",
+        "scroll-behavior": "0.8.2",
+        "warning": "3.0.0"
+      }
+    },
+    "react-themeable": {
+      "version": "1.1.0",
+      "resolved": "http://registry.npm.taobao.org/react-themeable/download/react-themeable-1.1.0.tgz",
+      "integrity": "sha1-fURm3ZsrX6dQWHJ4JenxUro3mg4=",
+      "requires": {
+        "object-assign": "3.0.0"
+      },
+      "dependencies": {
+        "object-assign": {
+          "version": "3.0.0",
+          "resolved": "http://registry.npm.taobao.org/object-assign/download/object-assign-3.0.0.tgz",
+          "integrity": "sha1-m+3VygiXlJvKR+f/QIBi1Un1h/I="
+        }
+      }
+    },
+    "react-transform-catch-errors": {
+      "version": "1.0.2",
+      "resolved": "http://registry.npm.taobao.org/react-transform-catch-errors/download/react-transform-catch-errors-1.0.2.tgz",
+      "integrity": "sha1-G01KdulycYlvwW/jCGx5PsiKnus=",
+      "dev": true
+    },
+    "react-transform-hmr": {
+      "version": "1.0.4",
+      "resolved": "http://registry.npm.taobao.org/react-transform-hmr/download/react-transform-hmr-1.0.4.tgz",
+      "integrity": "sha1-4aQL0Krvxy6N/Xp82gmvhQZjl7s=",
+      "dev": true,
+      "requires": {
+        "global": "4.3.2",
+        "react-proxy": "1.1.8"
+      }
+    },
+    "read-pkg": {
+      "version": "1.1.0",
+      "resolved": "http://registry.npm.taobao.org/read-pkg/download/read-pkg-1.1.0.tgz",
+      "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=",
+      "dev": true,
+      "requires": {
+        "load-json-file": "1.1.0",
+        "normalize-package-data": "2.4.0",
+        "path-type": "1.1.0"
+      }
+    },
+    "read-pkg-up": {
+      "version": "1.0.1",
+      "resolved": "http://registry.npm.taobao.org/read-pkg-up/download/read-pkg-up-1.0.1.tgz",
+      "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=",
+      "dev": true,
+      "requires": {
+        "find-up": "1.1.2",
+        "read-pkg": "1.1.0"
+      }
+    },
+    "readable-stream": {
+      "version": "2.3.3",
+      "resolved": "http://registry.npm.taobao.org/readable-stream/download/readable-stream-2.3.3.tgz",
+      "integrity": "sha1-No8lEtefnUb9/HE0mueHi7weuVw=",
+      "dev": true,
+      "requires": {
+        "core-util-is": "1.0.2",
+        "inherits": "2.0.3",
+        "isarray": "1.0.0",
+        "process-nextick-args": "1.0.7",
+        "safe-buffer": "5.1.1",
+        "string_decoder": "1.0.3",
+        "util-deprecate": "1.0.2"
+      }
+    },
+    "readdirp": {
+      "version": "2.1.0",
+      "resolved": "http://registry.npm.taobao.org/readdirp/download/readdirp-2.1.0.tgz",
+      "integrity": "sha1-TtCtBg3zBzMAxIRANz9y0cxkLXg=",
+      "dev": true,
+      "requires": {
+        "graceful-fs": "4.1.11",
+        "minimatch": "3.0.4",
+        "readable-stream": "2.3.3",
+        "set-immediate-shim": "1.0.1"
+      }
+    },
+    "readline2": {
+      "version": "1.0.1",
+      "resolved": "http://registry.npm.taobao.org/readline2/download/readline2-1.0.1.tgz",
+      "integrity": "sha1-QQWWCP/BVHV7cV2ZidGZ/783LjU=",
+      "dev": true,
+      "requires": {
+        "code-point-at": "1.1.0",
+        "is-fullwidth-code-point": "1.0.0",
+        "mute-stream": "0.0.5"
+      }
+    },
+    "rechoir": {
+      "version": "0.6.2",
+      "resolved": "http://registry.npm.taobao.org/rechoir/download/rechoir-0.6.2.tgz",
+      "integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=",
+      "dev": true,
+      "requires": {
+        "resolve": "1.5.0"
+      }
+    },
+    "redbox-react": {
+      "version": "1.5.0",
+      "resolved": "http://registry.npm.taobao.org/redbox-react/download/redbox-react-1.5.0.tgz",
+      "integrity": "sha1-BNqxFVfSZlG/NWKmfCKs5WxdOWc=",
+      "dev": true,
+      "requires": {
+        "error-stack-parser": "1.3.6",
+        "object-assign": "4.1.1",
+        "prop-types": "15.6.0",
+        "sourcemapped-stacktrace": "1.1.8"
+      }
+    },
+    "redent": {
+      "version": "1.0.0",
+      "resolved": "http://registry.npm.taobao.org/redent/download/redent-1.0.0.tgz",
+      "integrity": "sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=",
+      "dev": true,
+      "requires": {
+        "indent-string": "2.1.0",
+        "strip-indent": "1.0.1"
+      }
+    },
+    "redux": {
+      "version": "3.5.2",
+      "resolved": "http://registry.npm.taobao.org/redux/download/redux-3.5.2.tgz",
+      "integrity": "sha1-RTN0XpcLZH7CYGaoOqMOnib6+EM=",
+      "requires": {
+        "lodash": "4.17.4",
+        "lodash-es": "4.17.4",
+        "loose-envify": "1.3.1",
+        "symbol-observable": "0.2.4"
+      }
+    },
+    "redux-form": {
+      "version": "5.3.6",
+      "resolved": "http://registry.npm.taobao.org/redux-form/download/redux-form-5.3.6.tgz",
+      "integrity": "sha1-93qB2/ONRNJupBEQCiPxninNGUY=",
+      "requires": {
+        "deep-equal": "1.0.1",
+        "hoist-non-react-statics": "1.2.0",
+        "invariant": "2.2.2",
+        "is-promise": "2.1.0",
+        "prop-types": "15.6.0",
+        "react-lazy-cache": "3.0.1"
+      }
+    },
+    "redux-thunk": {
+      "version": "2.1.2",
+      "resolved": "http://registry.npm.taobao.org/redux-thunk/download/redux-thunk-2.1.2.tgz",
+      "integrity": "sha1-xpjtc006dEjdDemGWg+0ExLC13k="
+    },
+    "regenerate": {
+      "version": "1.3.3",
+      "resolved": "http://registry.npm.taobao.org/regenerate/download/regenerate-1.3.3.tgz",
+      "integrity": "sha1-DDNtOYBVPXVcObWGrjsgqknIK38=",
+      "dev": true
+    },
+    "regenerator-runtime": {
+      "version": "0.9.6",
+      "resolved": "http://registry.npm.taobao.org/regenerator-runtime/download/regenerator-runtime-0.9.6.tgz",
+      "integrity": "sha1-0z65XQ0gAaS+OWWXB8UbDLcc4Ck="
+    },
+    "regenerator-transform": {
+      "version": "0.10.1",
+      "resolved": "http://registry.npm.taobao.org/regenerator-transform/download/regenerator-transform-0.10.1.tgz",
+      "integrity": "sha1-HkmWg3Ix2ot/PPQRTXG1aRoGgN0=",
+      "dev": true,
+      "requires": {
+        "babel-runtime": "6.26.0",
+        "babel-types": "6.26.0",
+        "private": "0.1.8"
+      }
+    },
+    "regex-cache": {
+      "version": "0.4.4",
+      "resolved": "http://registry.npm.taobao.org/regex-cache/download/regex-cache-0.4.4.tgz",
+      "integrity": "sha1-db3FiioUls7EihKDW8VMjVYjNt0=",
+      "dev": true,
+      "requires": {
+        "is-equal-shallow": "0.1.3"
+      }
+    },
+    "regexpu-core": {
+      "version": "2.0.0",
+      "resolved": "http://registry.npm.taobao.org/regexpu-core/download/regexpu-core-2.0.0.tgz",
+      "integrity": "sha1-SdA4g3uNz4v6W5pCE5k45uoq4kA=",
+      "dev": true,
+      "requires": {
+        "regenerate": "1.3.3",
+        "regjsgen": "0.2.0",
+        "regjsparser": "0.1.5"
+      }
+    },
+    "regjsgen": {
+      "version": "0.2.0",
+      "resolved": "http://registry.npm.taobao.org/regjsgen/download/regjsgen-0.2.0.tgz",
+      "integrity": "sha1-bAFq3qxVT3WCP+N6wFuS1aTtsfc=",
+      "dev": true
+    },
+    "regjsparser": {
+      "version": "0.1.5",
+      "resolved": "http://registry.npm.taobao.org/regjsparser/download/regjsparser-0.1.5.tgz",
+      "integrity": "sha1-fuj4Tcb6eS0/0K4ijSS9lJ6tIFw=",
+      "dev": true,
+      "requires": {
+        "jsesc": "0.5.0"
+      },
+      "dependencies": {
+        "jsesc": {
+          "version": "0.5.0",
+          "resolved": "http://registry.npm.taobao.org/jsesc/download/jsesc-0.5.0.tgz",
+          "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=",
+          "dev": true
+        }
+      }
+    },
+    "remove-trailing-separator": {
+      "version": "1.1.0",
+      "resolved": "http://registry.npm.taobao.org/remove-trailing-separator/download/remove-trailing-separator-1.1.0.tgz",
+      "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=",
+      "dev": true
+    },
+    "repeat-element": {
+      "version": "1.1.2",
+      "resolved": "http://registry.npm.taobao.org/repeat-element/download/repeat-element-1.1.2.tgz",
+      "integrity": "sha1-7wiaF40Ug7quTZPrmLT55OEdmQo=",
+      "dev": true
+    },
+    "repeat-string": {
+      "version": "1.6.1",
+      "resolved": "http://registry.npm.taobao.org/repeat-string/download/repeat-string-1.6.1.tgz",
+      "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=",
+      "dev": true
+    },
+    "repeating": {
+      "version": "2.0.1",
+      "resolved": "http://registry.npm.taobao.org/repeating/download/repeating-2.0.1.tgz",
+      "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=",
+      "requires": {
+        "is-finite": "1.0.2"
+      }
+    },
+    "request": {
+      "version": "2.83.0",
+      "resolved": "http://registry.npm.taobao.org/request/download/request-2.83.0.tgz",
+      "integrity": "sha1-ygtl2gLtYpNYh4COb1EDgQNOM1Y=",
+      "dev": true,
+      "requires": {
+        "aws-sign2": "0.7.0",
+        "aws4": "1.6.0",
+        "caseless": "0.12.0",
+        "combined-stream": "1.0.5",
+        "extend": "3.0.1",
+        "forever-agent": "0.6.1",
+        "form-data": "2.3.1",
+        "har-validator": "5.0.3",
+        "hawk": "6.0.2",
+        "http-signature": "1.2.0",
+        "is-typedarray": "1.0.0",
+        "isstream": "0.1.2",
+        "json-stringify-safe": "5.0.1",
+        "mime-types": "2.1.17",
+        "oauth-sign": "0.8.2",
+        "performance-now": "2.1.0",
+        "qs": "6.5.1",
+        "safe-buffer": "5.1.1",
+        "stringstream": "0.0.5",
+        "tough-cookie": "2.3.3",
+        "tunnel-agent": "0.6.0",
+        "uuid": "3.1.0"
+      },
+      "dependencies": {
+        "uuid": {
+          "version": "3.1.0",
+          "resolved": "http://registry.npm.taobao.org/uuid/download/uuid-3.1.0.tgz",
+          "integrity": "sha1-PdPT55Crwk17DToDT/q6vijrvAQ=",
+          "dev": true
+        }
+      }
+    },
+    "request-promise": {
+      "version": "4.2.2",
+      "resolved": "http://registry.npm.taobao.org/request-promise/download/request-promise-4.2.2.tgz",
+      "integrity": "sha1-0epG1lSm7k+O5qT+oQGMIpEZBLQ=",
+      "dev": true,
+      "requires": {
+        "bluebird": "3.5.1",
+        "request-promise-core": "1.1.1",
+        "stealthy-require": "1.1.1",
+        "tough-cookie": "2.3.3"
+      }
+    },
+    "request-promise-core": {
+      "version": "1.1.1",
+      "resolved": "http://registry.npm.taobao.org/request-promise-core/download/request-promise-core-1.1.1.tgz",
+      "integrity": "sha1-Pu4AssWqgyOc+wTFcA2jb4HNCLY=",
+      "dev": true,
+      "requires": {
+        "lodash": "4.17.4"
+      }
+    },
+    "require-directory": {
+      "version": "2.1.1",
+      "resolved": "http://registry.npm.taobao.org/require-directory/download/require-directory-2.1.1.tgz",
+      "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=",
+      "dev": true
+    },
+    "require-from-string": {
+      "version": "1.2.1",
+      "resolved": "http://registry.npm.taobao.org/require-from-string/download/require-from-string-1.2.1.tgz",
+      "integrity": "sha1-UpyczvJzgK3+yaL5ZbZJu+5jZBg=",
+      "dev": true
+    },
+    "require-main-filename": {
+      "version": "1.0.1",
+      "resolved": "http://registry.npm.taobao.org/require-main-filename/download/require-main-filename-1.0.1.tgz",
+      "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=",
+      "dev": true
+    },
+    "require-uncached": {
+      "version": "1.0.3",
+      "resolved": "http://registry.npm.taobao.org/require-uncached/download/require-uncached-1.0.3.tgz",
+      "integrity": "sha1-Tg1W1slmL9MeQwEcS5WqSZVUIdM=",
+      "dev": true,
+      "requires": {
+        "caller-path": "0.1.0",
+        "resolve-from": "1.0.1"
+      }
+    },
+    "requires-port": {
+      "version": "1.0.0",
+      "resolved": "http://registry.npm.taobao.org/requires-port/download/requires-port-1.0.0.tgz",
+      "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=",
+      "dev": true
+    },
+    "reselect": {
+      "version": "3.0.1",
+      "resolved": "http://registry.npm.taobao.org/reselect/download/reselect-3.0.1.tgz",
+      "integrity": "sha1-79qpjqdFEyTQkrKyFjpqHXqaIUc="
+    },
+    "resolve": {
+      "version": "1.5.0",
+      "resolved": "http://registry.npm.taobao.org/resolve/download/resolve-1.5.0.tgz",
+      "integrity": "sha1-HwmsznlsmnYlefMbLBzEw83fnzY=",
+      "dev": true,
+      "requires": {
+        "path-parse": "1.0.5"
+      }
+    },
+    "resolve-from": {
+      "version": "1.0.1",
+      "resolved": "http://registry.npm.taobao.org/resolve-from/download/resolve-from-1.0.1.tgz",
+      "integrity": "sha1-Jsv+k10a7uq7Kbw/5a6wHpPUQiY=",
+      "dev": true
+    },
+    "resolve-url": {
+      "version": "0.2.1",
+      "resolved": "http://registry.npm.taobao.org/resolve-url/download/resolve-url-0.2.1.tgz",
+      "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=",
+      "dev": true
+    },
+    "resolve-url-loader": {
+      "version": "1.6.1",
+      "resolved": "http://registry.npm.taobao.org/resolve-url-loader/download/resolve-url-loader-1.6.1.tgz",
+      "integrity": "sha1-Sm4Dx03TjV393w9AS0ddbpACVjU=",
+      "dev": true,
+      "requires": {
+        "camelcase": "1.2.1",
+        "convert-source-map": "1.5.1",
+        "loader-utils": "0.2.17",
+        "lodash.defaults": "3.1.2",
+        "rework": "1.0.1",
+        "rework-visit": "1.0.0",
+        "source-map": "0.1.43",
+        "urix": "0.1.0"
+      },
+      "dependencies": {
+        "camelcase": {
+          "version": "1.2.1",
+          "resolved": "http://registry.npm.taobao.org/camelcase/download/camelcase-1.2.1.tgz",
+          "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=",
+          "dev": true
+        },
+        "lodash.assign": {
+          "version": "3.2.0",
+          "resolved": "http://registry.npm.taobao.org/lodash.assign/download/lodash.assign-3.2.0.tgz",
+          "integrity": "sha1-POnwI0tLIiPilrj6CsH+6OvKZPo=",
+          "dev": true,
+          "requires": {
+            "lodash._baseassign": "3.2.0",
+            "lodash._createassigner": "3.1.1",
+            "lodash.keys": "3.1.2"
+          }
+        },
+        "lodash.defaults": {
+          "version": "3.1.2",
+          "resolved": "http://registry.npm.taobao.org/lodash.defaults/download/lodash.defaults-3.1.2.tgz",
+          "integrity": "sha1-xzCLGNv4vJNy1wGnNJPGEZK9Liw=",
+          "dev": true,
+          "requires": {
+            "lodash.assign": "3.2.0",
+            "lodash.restparam": "3.6.1"
+          }
+        },
+        "source-map": {
+          "version": "0.1.43",
+          "resolved": "http://registry.npm.taobao.org/source-map/download/source-map-0.1.43.tgz",
+          "integrity": "sha1-wkvBRspRfBRx9drL4lcbK3+eM0Y=",
+          "dev": true,
+          "requires": {
+            "amdefine": "1.0.1"
+          }
+        }
+      }
+    },
+    "restore-cursor": {
+      "version": "1.0.1",
+      "resolved": "http://registry.npm.taobao.org/restore-cursor/download/restore-cursor-1.0.1.tgz",
+      "integrity": "sha1-NGYfRohjJ/7SmRR5FSJS35LapUE=",
+      "dev": true,
+      "requires": {
+        "exit-hook": "1.1.1",
+        "onetime": "1.1.0"
+      }
+    },
+    "rework": {
+      "version": "1.0.1",
+      "resolved": "http://registry.npm.taobao.org/rework/download/rework-1.0.1.tgz",
+      "integrity": "sha1-MIBqhBNCtUUQqkEQhQzUhTQUSqc=",
+      "dev": true,
+      "requires": {
+        "convert-source-map": "0.3.5",
+        "css": "2.2.1"
+      },
+      "dependencies": {
+        "convert-source-map": {
+          "version": "0.3.5",
+          "resolved": "http://registry.npm.taobao.org/convert-source-map/download/convert-source-map-0.3.5.tgz",
+          "integrity": "sha1-8dgClQr33SYxof6+BZZVDIarMZA=",
+          "dev": true
+        }
+      }
+    },
+    "rework-visit": {
+      "version": "1.0.0",
+      "resolved": "http://registry.npm.taobao.org/rework-visit/download/rework-visit-1.0.0.tgz",
+      "integrity": "sha1-mUWygD8hni96ygCtuLyfZA+ELJo=",
+      "dev": true
+    },
+    "rgb2hex": {
+      "version": "0.1.0",
+      "resolved": "http://registry.npm.taobao.org/rgb2hex/download/rgb2hex-0.1.0.tgz",
+      "integrity": "sha1-zNVfhgrgxcTqN1BLlY5ELY0SMls=",
+      "dev": true
+    },
+    "right-align": {
+      "version": "0.1.3",
+      "resolved": "http://registry.npm.taobao.org/right-align/download/right-align-0.1.3.tgz",
+      "integrity": "sha1-YTObci/mo1FWiSENJOFMlhSGE+8=",
+      "dev": true,
+      "requires": {
+        "align-text": "0.1.4"
+      }
+    },
+    "rimraf": {
+      "version": "2.6.2",
+      "resolved": "http://registry.npm.taobao.org/rimraf/download/rimraf-2.6.2.tgz",
+      "integrity": "sha1-LtgVDSShbqhlHm1u8PR8QVjOejY=",
+      "dev": true,
+      "requires": {
+        "glob": "7.1.2"
+      },
+      "dependencies": {
+        "glob": {
+          "version": "7.1.2",
+          "resolved": "http://registry.npm.taobao.org/glob/download/glob-7.1.2.tgz",
+          "integrity": "sha1-wZyd+aAocC1nhhI4SmVSQExjbRU=",
+          "dev": true,
+          "requires": {
+            "fs.realpath": "1.0.0",
+            "inflight": "1.0.6",
+            "inherits": "2.0.3",
+            "minimatch": "3.0.4",
+            "once": "1.4.0",
+            "path-is-absolute": "1.0.1"
+          }
+        }
+      }
+    },
+    "ripemd160": {
+      "version": "0.2.0",
+      "resolved": "http://registry.npm.taobao.org/ripemd160/download/ripemd160-0.2.0.tgz",
+      "integrity": "sha1-K/GYveFnys+lHAqSjoS2i74XH84=",
+      "dev": true
+    },
+    "run-async": {
+      "version": "0.1.0",
+      "resolved": "http://registry.npm.taobao.org/run-async/download/run-async-0.1.0.tgz",
+      "integrity": "sha1-yK1KXhEGYeQCp9IbUw4AnyX444k=",
+      "dev": true,
+      "requires": {
+        "once": "1.4.0"
+      }
+    },
+    "rx": {
+      "version": "4.1.0",
+      "resolved": "http://registry.npm.taobao.org/rx/download/rx-4.1.0.tgz",
+      "integrity": "sha1-pfE/957zt0D+MKqAP7CfmIBdR4I=",
+      "dev": true
+    },
+    "rx-lite": {
+      "version": "3.1.2",
+      "resolved": "http://registry.npm.taobao.org/rx-lite/download/rx-lite-3.1.2.tgz",
+      "integrity": "sha1-Gc5QLKVyZl87ZHsQk5+X/RYV8QI=",
+      "dev": true
+    },
+    "safe-buffer": {
+      "version": "5.1.1",
+      "resolved": "http://registry.npm.taobao.org/safe-buffer/download/safe-buffer-5.1.1.tgz",
+      "integrity": "sha1-iTMSr2myEj3vcfV4iQAWce6yyFM="
+    },
+    "samsam": {
+      "version": "1.1.2",
+      "resolved": "http://registry.npm.taobao.org/samsam/download/samsam-1.1.2.tgz",
+      "integrity": "sha1-vsEf3IOp/aBjQBIQ5AF2wwJNFWc=",
+      "dev": true
+    },
+    "sass-graph": {
+      "version": "2.2.4",
+      "resolved": "http://registry.npm.taobao.org/sass-graph/download/sass-graph-2.2.4.tgz",
+      "integrity": "sha1-E/vWPNHK8JCLn9k0dq1DpR0eC0k=",
+      "dev": true,
+      "requires": {
+        "glob": "7.1.2",
+        "lodash": "4.17.4",
+        "scss-tokenizer": "0.2.3",
+        "yargs": "7.1.0"
+      },
+      "dependencies": {
+        "camelcase": {
+          "version": "3.0.0",
+          "resolved": "http://registry.npm.taobao.org/camelcase/download/camelcase-3.0.0.tgz",
+          "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=",
+          "dev": true
+        },
+        "cliui": {
+          "version": "3.2.0",
+          "resolved": "http://registry.npm.taobao.org/cliui/download/cliui-3.2.0.tgz",
+          "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=",
+          "dev": true,
+          "requires": {
+            "string-width": "1.0.2",
+            "strip-ansi": "3.0.1",
+            "wrap-ansi": "2.1.0"
+          }
+        },
+        "glob": {
+          "version": "7.1.2",
+          "resolved": "http://registry.npm.taobao.org/glob/download/glob-7.1.2.tgz",
+          "integrity": "sha1-wZyd+aAocC1nhhI4SmVSQExjbRU=",
+          "dev": true,
+          "requires": {
+            "fs.realpath": "1.0.0",
+            "inflight": "1.0.6",
+            "inherits": "2.0.3",
+            "minimatch": "3.0.4",
+            "once": "1.4.0",
+            "path-is-absolute": "1.0.1"
+          }
+        },
+        "yargs": {
+          "version": "7.1.0",
+          "resolved": "http://registry.npm.taobao.org/yargs/download/yargs-7.1.0.tgz",
+          "integrity": "sha1-a6MY6xaWFyf10oT46gA+jWFU0Mg=",
+          "dev": true,
+          "requires": {
+            "camelcase": "3.0.0",
+            "cliui": "3.2.0",
+            "decamelize": "1.2.0",
+            "get-caller-file": "1.0.2",
+            "os-locale": "1.4.0",
+            "read-pkg-up": "1.0.1",
+            "require-directory": "2.1.1",
+            "require-main-filename": "1.0.1",
+            "set-blocking": "2.0.0",
+            "string-width": "1.0.2",
+            "which-module": "1.0.0",
+            "y18n": "3.2.1",
+            "yargs-parser": "5.0.0"
+          }
+        }
+      }
+    },
+    "sass-loader": {
+      "version": "4.0.2",
+      "resolved": "http://registry.npm.taobao.org/sass-loader/download/sass-loader-4.0.2.tgz",
+      "integrity": "sha1-phbrdwNmVD5k9UfIYw85xNp18V0=",
+      "dev": true,
+      "requires": {
+        "async": "2.6.0",
+        "loader-utils": "0.2.17",
+        "object-assign": "4.1.1"
+      },
+      "dependencies": {
+        "async": {
+          "version": "2.6.0",
+          "resolved": "http://registry.npm.taobao.org/async/download/async-2.6.0.tgz",
+          "integrity": "sha1-YaKau2/MAm/qd+VtHG7FOnlZUfQ=",
+          "dev": true,
+          "requires": {
+            "lodash": "4.17.4"
+          }
+        }
+      }
+    },
+    "sass-resources-loader": {
+      "version": "1.1.0",
+      "resolved": "http://registry.npm.taobao.org/sass-resources-loader/download/sass-resources-loader-1.1.0.tgz",
+      "integrity": "sha1-D9nm4cy89+ibbRx00Ykg4HsQIL8=",
+      "dev": true,
+      "requires": {
+        "async": "1.5.2",
+        "chalk": "1.1.3",
+        "glob": "7.1.2"
+      },
+      "dependencies": {
+        "glob": {
+          "version": "7.1.2",
+          "resolved": "http://registry.npm.taobao.org/glob/download/glob-7.1.2.tgz",
+          "integrity": "sha1-wZyd+aAocC1nhhI4SmVSQExjbRU=",
+          "dev": true,
+          "requires": {
+            "fs.realpath": "1.0.0",
+            "inflight": "1.0.6",
+            "inherits": "2.0.3",
+            "minimatch": "3.0.4",
+            "once": "1.4.0",
+            "path-is-absolute": "1.0.1"
+          }
+        }
+      }
+    },
+    "scroll-behavior": {
+      "version": "0.8.2",
+      "resolved": "http://registry.npm.taobao.org/scroll-behavior/download/scroll-behavior-0.8.2.tgz",
+      "integrity": "sha1-rOE+QLAB2NTQB67A5/tmjPkENUY=",
+      "requires": {
+        "dom-helpers": "2.4.0",
+        "invariant": "2.2.2"
+      },
+      "dependencies": {
+        "dom-helpers": {
+          "version": "2.4.0",
+          "resolved": "http://registry.npm.taobao.org/dom-helpers/download/dom-helpers-2.4.0.tgz",
+          "integrity": "sha1-m7SyRfY3NnsfpnAnQnKqKP4Gw2c="
+        }
+      }
+    },
+    "scss-tokenizer": {
+      "version": "0.2.3",
+      "resolved": "http://registry.npm.taobao.org/scss-tokenizer/download/scss-tokenizer-0.2.3.tgz",
+      "integrity": "sha1-jrBtualyMzOCTT9VMGQRSYR85dE=",
+      "dev": true,
+      "requires": {
+        "js-base64": "2.4.0",
+        "source-map": "0.4.4"
+      },
+      "dependencies": {
+        "source-map": {
+          "version": "0.4.4",
+          "resolved": "http://registry.npm.taobao.org/source-map/download/source-map-0.4.4.tgz",
+          "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=",
+          "dev": true,
+          "requires": {
+            "amdefine": "1.0.1"
+          }
+        }
+      }
+    },
+    "section-iterator": {
+      "version": "2.0.0",
+      "resolved": "http://registry.npm.taobao.org/section-iterator/download/section-iterator-2.0.0.tgz",
+      "integrity": "sha1-v0RNev7rlK1Dw5rS+yYVFifMuio="
+    },
+    "selenium-standalone": {
+      "version": "5.11.2",
+      "resolved": "http://registry.npm.taobao.org/selenium-standalone/download/selenium-standalone-5.11.2.tgz",
+      "integrity": "sha1-ckzKpy+ybzcR4OIJieR4xBM9+EQ=",
+      "dev": true,
+      "requires": {
+        "async": "1.2.1",
+        "commander": "2.6.0",
+        "lodash": "3.9.3",
+        "minimist": "1.1.0",
+        "mkdirp": "0.5.0",
+        "progress": "1.1.8",
+        "request": "2.79.0",
+        "tar-stream": "1.5.2",
+        "urijs": "1.16.1",
+        "which": "1.1.1",
+        "yauzl": "2.9.1"
+      },
+      "dependencies": {
+        "assert-plus": {
+          "version": "0.2.0",
+          "resolved": "http://registry.npm.taobao.org/assert-plus/download/assert-plus-0.2.0.tgz",
+          "integrity": "sha1-104bh+ev/A24qttwIfP+SBAasjQ=",
+          "dev": true
+        },
+        "async": {
+          "version": "1.2.1",
+          "resolved": "http://registry.npm.taobao.org/async/download/async-1.2.1.tgz",
+          "integrity": "sha1-pIFqF81f9RbfosdpikUzabl5DeA=",
+          "dev": true
+        },
+        "aws-sign2": {
+          "version": "0.6.0",
+          "resolved": "http://registry.npm.taobao.org/aws-sign2/download/aws-sign2-0.6.0.tgz",
+          "integrity": "sha1-FDQt0428yU0OW4fXY81jYSwOeU8=",
+          "dev": true
+        },
+        "boom": {
+          "version": "2.10.1",
+          "resolved": "http://registry.npm.taobao.org/boom/download/boom-2.10.1.tgz",
+          "integrity": "sha1-OciRjO/1eZ+D+UkqhI9iWt0Mdm8=",
+          "dev": true,
+          "requires": {
+            "hoek": "2.16.3"
+          }
+        },
+        "caseless": {
+          "version": "0.11.0",
+          "resolved": "http://registry.npm.taobao.org/caseless/download/caseless-0.11.0.tgz",
+          "integrity": "sha1-cVuW6phBWTzDMGeSP17GDr2k99c=",
+          "dev": true
+        },
+        "commander": {
+          "version": "2.6.0",
+          "resolved": "http://registry.npm.taobao.org/commander/download/commander-2.6.0.tgz",
+          "integrity": "sha1-nfflL7Kgyw+4kFjugMMQQiXzfh0=",
+          "dev": true
+        },
+        "cryptiles": {
+          "version": "2.0.5",
+          "resolved": "http://registry.npm.taobao.org/cryptiles/download/cryptiles-2.0.5.tgz",
+          "integrity": "sha1-O9/s3GCBR8HGcgL6KR59ylnqo7g=",
+          "dev": true,
+          "requires": {
+            "boom": "2.10.1"
+          }
+        },
+        "form-data": {
+          "version": "2.1.4",
+          "resolved": "http://registry.npm.taobao.org/form-data/download/form-data-2.1.4.tgz",
+          "integrity": "sha1-M8GDrPGTJ27KqYFDpp6Uv+4XUNE=",
+          "dev": true,
+          "requires": {
+            "asynckit": "0.4.0",
+            "combined-stream": "1.0.5",
+            "mime-types": "2.1.17"
+          }
+        },
+        "har-validator": {
+          "version": "2.0.6",
+          "resolved": "http://registry.npm.taobao.org/har-validator/download/har-validator-2.0.6.tgz",
+          "integrity": "sha1-zcvAgYgmWtEZtqWnyKtw7s+10n0=",
+          "dev": true,
+          "requires": {
+            "chalk": "1.1.3",
+            "commander": "2.12.2",
+            "is-my-json-valid": "2.17.1",
+            "pinkie-promise": "2.0.1"
+          },
+          "dependencies": {
+            "commander": {
+              "version": "2.12.2",
+              "resolved": "http://registry.npm.taobao.org/commander/download/commander-2.12.2.tgz",
+              "integrity": "sha1-D1lGxCftnsDZGka7ne9T5UZQ5VU=",
+              "dev": true
+            }
+          }
+        },
+        "hawk": {
+          "version": "3.1.3",
+          "resolved": "http://registry.npm.taobao.org/hawk/download/hawk-3.1.3.tgz",
+          "integrity": "sha1-B4REvXwWQLD+VA0sm3PVlnjo4cQ=",
+          "dev": true,
+          "requires": {
+            "boom": "2.10.1",
+            "cryptiles": "2.0.5",
+            "hoek": "2.16.3",
+            "sntp": "1.0.9"
+          }
+        },
+        "hoek": {
+          "version": "2.16.3",
+          "resolved": "http://registry.npm.taobao.org/hoek/download/hoek-2.16.3.tgz",
+          "integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=",
+          "dev": true
+        },
+        "http-signature": {
+          "version": "1.1.1",
+          "resolved": "http://registry.npm.taobao.org/http-signature/download/http-signature-1.1.1.tgz",
+          "integrity": "sha1-33LiZwZs0Kxn+3at+OE0qPvPkb8=",
+          "dev": true,
+          "requires": {
+            "assert-plus": "0.2.0",
+            "jsprim": "1.4.1",
+            "sshpk": "1.13.1"
+          }
+        },
+        "lodash": {
+          "version": "3.9.3",
+          "resolved": "http://registry.npm.taobao.org/lodash/download/lodash-3.9.3.tgz",
+          "integrity": "sha1-AVnoaDL+/8bWHYUrEqlTuZSWvTI=",
+          "dev": true
+        },
+        "minimist": {
+          "version": "1.1.0",
+          "resolved": "http://registry.npm.taobao.org/minimist/download/minimist-1.1.0.tgz",
+          "integrity": "sha1-zfIl6ImPhAolje1E/JF3Z3Cv3JM=",
+          "dev": true
+        },
+        "mkdirp": {
+          "version": "0.5.0",
+          "resolved": "http://registry.npm.taobao.org/mkdirp/download/mkdirp-0.5.0.tgz",
+          "integrity": "sha1-HXMHam35hs2TROFecfzAWkyavxI=",
+          "dev": true,
+          "requires": {
+            "minimist": "0.0.8"
+          },
+          "dependencies": {
+            "minimist": {
+              "version": "0.0.8",
+              "resolved": "http://registry.npm.taobao.org/minimist/download/minimist-0.0.8.tgz",
+              "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=",
+              "dev": true
+            }
+          }
+        },
+        "qs": {
+          "version": "6.3.2",
+          "resolved": "http://registry.npm.taobao.org/qs/download/qs-6.3.2.tgz",
+          "integrity": "sha1-51vV9uJoEioqDgvaYwslUMFmUCw=",
+          "dev": true
+        },
+        "request": {
+          "version": "2.79.0",
+          "resolved": "http://registry.npm.taobao.org/request/download/request-2.79.0.tgz",
+          "integrity": "sha1-Tf5b9r6LjNw3/Pk+BLZVd3InEN4=",
+          "dev": true,
+          "requires": {
+            "aws-sign2": "0.6.0",
+            "aws4": "1.6.0",
+            "caseless": "0.11.0",
+            "combined-stream": "1.0.5",
+            "extend": "3.0.1",
+            "forever-agent": "0.6.1",
+            "form-data": "2.1.4",
+            "har-validator": "2.0.6",
+            "hawk": "3.1.3",
+            "http-signature": "1.1.1",
+            "is-typedarray": "1.0.0",
+            "isstream": "0.1.2",
+            "json-stringify-safe": "5.0.1",
+            "mime-types": "2.1.17",
+            "oauth-sign": "0.8.2",
+            "qs": "6.3.2",
+            "stringstream": "0.0.5",
+            "tough-cookie": "2.3.3",
+            "tunnel-agent": "0.4.3",
+            "uuid": "3.1.0"
+          }
+        },
+        "sntp": {
+          "version": "1.0.9",
+          "resolved": "http://registry.npm.taobao.org/sntp/download/sntp-1.0.9.tgz",
+          "integrity": "sha1-ZUEYTMkK7qbG57NeJlkIJEPGYZg=",
+          "dev": true,
+          "requires": {
+            "hoek": "2.16.3"
+          }
+        },
+        "tunnel-agent": {
+          "version": "0.4.3",
+          "resolved": "http://registry.npm.taobao.org/tunnel-agent/download/tunnel-agent-0.4.3.tgz",
+          "integrity": "sha1-Y3PbdpCf5XDgjXNYM2Xtgop07us=",
+          "dev": true
+        },
+        "uuid": {
+          "version": "3.1.0",
+          "resolved": "http://registry.npm.taobao.org/uuid/download/uuid-3.1.0.tgz",
+          "integrity": "sha1-PdPT55Crwk17DToDT/q6vijrvAQ=",
+          "dev": true
+        },
+        "which": {
+          "version": "1.1.1",
+          "resolved": "http://registry.npm.taobao.org/which/download/which-1.1.1.tgz",
+          "integrity": "sha1-nOUSRZlGFm4SwIPwjsBzOA/Iy7s=",
+          "dev": true,
+          "requires": {
+            "is-absolute": "0.1.7"
+          }
+        }
+      }
+    },
+    "semver": {
+      "version": "4.3.6",
+      "resolved": "http://registry.npm.taobao.org/semver/download/semver-4.3.6.tgz",
+      "integrity": "sha1-MAvG4OhjdPe6YQaLWx7NV/xlMto=",
+      "dev": true
+    },
+    "semver-regex": {
+      "version": "1.0.0",
+      "resolved": "http://registry.npm.taobao.org/semver-regex/download/semver-regex-1.0.0.tgz",
+      "integrity": "sha1-kqSWkGX5xwxpR1PVUkj8aPj2Usk=",
+      "dev": true
+    },
+    "semver-truncate": {
+      "version": "1.1.2",
+      "resolved": "http://registry.npm.taobao.org/semver-truncate/download/semver-truncate-1.1.2.tgz",
+      "integrity": "sha1-V/Qd5pcHpicJp+AQS6IRcQnqR+g=",
+      "dev": true,
+      "requires": {
+        "semver": "5.4.1"
+      },
+      "dependencies": {
+        "semver": {
+          "version": "5.4.1",
+          "resolved": "http://registry.npm.taobao.org/semver/download/semver-5.4.1.tgz",
+          "integrity": "sha1-4FnAnYVx8FQII3M0M1BdOi8AsY4=",
+          "dev": true
+        }
+      }
+    },
+    "send": {
+      "version": "0.16.1",
+      "resolved": "http://registry.npm.taobao.org/send/download/send-0.16.1.tgz",
+      "integrity": "sha1-pw4coh0TgsEdDZ9iMd6ygQgNerM=",
+      "dev": true,
+      "requires": {
+        "debug": "2.6.9",
+        "depd": "1.1.1",
+        "destroy": "1.0.4",
+        "encodeurl": "1.0.1",
+        "escape-html": "1.0.3",
+        "etag": "1.8.1",
+        "fresh": "0.5.2",
+        "http-errors": "1.6.2",
+        "mime": "1.4.1",
+        "ms": "2.0.0",
+        "on-finished": "2.3.0",
+        "range-parser": "1.2.0",
+        "statuses": "1.3.1"
+      }
+    },
+    "serve-static": {
+      "version": "1.13.1",
+      "resolved": "http://registry.npm.taobao.org/serve-static/download/serve-static-1.13.1.tgz",
+      "integrity": "sha1-TFfVNASnYdjy58HooYpH2/J4pxk=",
+      "dev": true,
+      "requires": {
+        "encodeurl": "1.0.1",
+        "escape-html": "1.0.3",
+        "parseurl": "1.3.2",
+        "send": "0.16.1"
+      }
+    },
+    "set-blocking": {
+      "version": "2.0.0",
+      "resolved": "http://registry.npm.taobao.org/set-blocking/download/set-blocking-2.0.0.tgz",
+      "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=",
+      "dev": true
+    },
+    "set-immediate-shim": {
+      "version": "1.0.1",
+      "resolved": "http://registry.npm.taobao.org/set-immediate-shim/download/set-immediate-shim-1.0.1.tgz",
+      "integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=",
+      "dev": true
+    },
+    "setimmediate": {
+      "version": "1.0.5",
+      "resolved": "http://registry.npm.taobao.org/setimmediate/download/setimmediate-1.0.5.tgz",
+      "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU="
+    },
+    "setprototypeof": {
+      "version": "1.1.0",
+      "resolved": "http://registry.npm.taobao.org/setprototypeof/download/setprototypeof-1.1.0.tgz",
+      "integrity": "sha1-0L2FU2iHtv58DYGMuWLZ2RxU5lY=",
+      "dev": true
+    },
+    "sha.js": {
+      "version": "2.4.9",
+      "resolved": "http://registry.npm.taobao.org/sha.js/download/sha.js-2.4.9.tgz",
+      "integrity": "sha1-mPZIgEdLdPSji42p08Dy0QRjPn0=",
+      "requires": {
+        "inherits": "2.0.3",
+        "safe-buffer": "5.1.1"
+      }
+    },
+    "shallow-equal": {
+      "version": "1.0.0",
+      "resolved": "http://registry.npm.taobao.org/shallow-equal/download/shallow-equal-1.0.0.tgz",
+      "integrity": "sha1-UI0YOLPeWQq4dXsBGyXkMJAJRfc="
+    },
+    "shebang-regex": {
+      "version": "1.0.0",
+      "resolved": "http://registry.npm.taobao.org/shebang-regex/download/shebang-regex-1.0.0.tgz",
+      "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=",
+      "dev": true
+    },
+    "shelljs": {
+      "version": "0.7.8",
+      "resolved": "http://registry.npm.taobao.org/shelljs/download/shelljs-0.7.8.tgz",
+      "integrity": "sha1-3svPh0sNHl+3LhSxZKloMEjprLM=",
+      "dev": true,
+      "requires": {
+        "glob": "7.1.2",
+        "interpret": "1.1.0",
+        "rechoir": "0.6.2"
+      },
+      "dependencies": {
+        "glob": {
+          "version": "7.1.2",
+          "resolved": "http://registry.npm.taobao.org/glob/download/glob-7.1.2.tgz",
+          "integrity": "sha1-wZyd+aAocC1nhhI4SmVSQExjbRU=",
+          "dev": true,
+          "requires": {
+            "fs.realpath": "1.0.0",
+            "inflight": "1.0.6",
+            "inherits": "2.0.3",
+            "minimatch": "3.0.4",
+            "once": "1.4.0",
+            "path-is-absolute": "1.0.1"
+          }
+        },
+        "interpret": {
+          "version": "1.1.0",
+          "resolved": "http://registry.npm.taobao.org/interpret/download/interpret-1.1.0.tgz",
+          "integrity": "sha1-ftGxQQxqDg94z5XTuEQMY/eLhhQ=",
+          "dev": true
+        }
+      }
+    },
+    "sigmund": {
+      "version": "1.0.1",
+      "resolved": "http://registry.npm.taobao.org/sigmund/download/sigmund-1.0.1.tgz",
+      "integrity": "sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA=",
+      "dev": true
+    },
+    "signal-exit": {
+      "version": "3.0.2",
+      "resolved": "http://registry.npm.taobao.org/signal-exit/download/signal-exit-3.0.2.tgz",
+      "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=",
+      "dev": true
+    },
+    "sinon": {
+      "version": "1.17.7",
+      "resolved": "http://registry.npm.taobao.org/sinon/download/sinon-1.17.7.tgz",
+      "integrity": "sha1-RUKk9JugxFwF6y6d2dID4rjv4L8=",
+      "dev": true,
+      "requires": {
+        "formatio": "1.1.1",
+        "lolex": "1.3.2",
+        "samsam": "1.1.2",
+        "util": "0.10.3"
+      }
+    },
+    "slash": {
+      "version": "1.0.0",
+      "resolved": "http://registry.npm.taobao.org/slash/download/slash-1.0.0.tgz",
+      "integrity": "sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU="
+    },
+    "slice-ansi": {
+      "version": "0.0.4",
+      "resolved": "http://registry.npm.taobao.org/slice-ansi/download/slice-ansi-0.0.4.tgz",
+      "integrity": "sha1-7b+JA/ZvfOL46v1s7tZeJkyDGzU=",
+      "dev": true
+    },
+    "sntp": {
+      "version": "2.1.0",
+      "resolved": "http://registry.npm.taobao.org/sntp/download/sntp-2.1.0.tgz",
+      "integrity": "sha1-LGzsFP7cIiJznK+bXD2F0cxaLMg=",
+      "dev": true,
+      "requires": {
+        "hoek": "4.2.0"
+      }
+    },
+    "source-list-map": {
+      "version": "0.1.8",
+      "resolved": "http://registry.npm.taobao.org/source-list-map/download/source-list-map-0.1.8.tgz",
+      "integrity": "sha1-xVCyq1Qn9rPyH1r+rYjE9Vh7IQY=",
+      "dev": true
+    },
+    "source-map": {
+      "version": "0.5.7",
+      "resolved": "http://registry.npm.taobao.org/source-map/download/source-map-0.5.7.tgz",
+      "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w="
+    },
+    "source-map-resolve": {
+      "version": "0.3.1",
+      "resolved": "http://registry.npm.taobao.org/source-map-resolve/download/source-map-resolve-0.3.1.tgz",
+      "integrity": "sha1-YQ9hIqRFuN1RU1oqcbeD38Ekh2E=",
+      "dev": true,
+      "requires": {
+        "atob": "1.1.3",
+        "resolve-url": "0.2.1",
+        "source-map-url": "0.3.0",
+        "urix": "0.1.0"
+      }
+    },
+    "source-map-support": {
+      "version": "0.4.18",
+      "resolved": "http://registry.npm.taobao.org/source-map-support/download/source-map-support-0.4.18.tgz",
+      "integrity": "sha1-Aoam3ovkJkEzhZTpfM6nXwosWF8=",
+      "requires": {
+        "source-map": "0.5.7"
+      }
+    },
+    "source-map-url": {
+      "version": "0.3.0",
+      "resolved": "http://registry.npm.taobao.org/source-map-url/download/source-map-url-0.3.0.tgz",
+      "integrity": "sha1-fsrxO1e80J2opAxdJp2zN5nUqvk=",
+      "dev": true
+    },
+    "sourcemapped-stacktrace": {
+      "version": "1.1.8",
+      "resolved": "http://registry.npm.taobao.org/sourcemapped-stacktrace/download/sourcemapped-stacktrace-1.1.8.tgz",
+      "integrity": "sha1-a3o/Gm+xX21A5wHiPOQEVTSA1og=",
+      "dev": true,
+      "requires": {
+        "source-map": "0.5.6"
+      },
+      "dependencies": {
+        "source-map": {
+          "version": "0.5.6",
+          "resolved": "http://registry.npm.taobao.org/source-map/download/source-map-0.5.6.tgz",
+          "integrity": "sha1-dc449SvwczxafwwRjYEzSiu19BI=",
+          "dev": true
+        }
+      }
+    },
+    "spawn-sync": {
+      "version": "1.0.15",
+      "resolved": "http://registry.npm.taobao.org/spawn-sync/download/spawn-sync-1.0.15.tgz",
+      "integrity": "sha1-sAeZVX63+wyDdsKdROih6mfldHY=",
+      "dev": true,
+      "requires": {
+        "concat-stream": "1.6.0",
+        "os-shim": "0.1.3"
+      }
+    },
+    "spdx-correct": {
+      "version": "1.0.2",
+      "resolved": "http://registry.npm.taobao.org/spdx-correct/download/spdx-correct-1.0.2.tgz",
+      "integrity": "sha1-SzBz2TP/UfORLwOsVRlJikFQ20A=",
+      "dev": true,
+      "requires": {
+        "spdx-license-ids": "1.2.2"
+      }
+    },
+    "spdx-expression-parse": {
+      "version": "1.0.4",
+      "resolved": "http://registry.npm.taobao.org/spdx-expression-parse/download/spdx-expression-parse-1.0.4.tgz",
+      "integrity": "sha1-m98vIOH0DtRH++JzJmGR/O1RYmw=",
+      "dev": true
+    },
+    "spdx-license-ids": {
+      "version": "1.2.2",
+      "resolved": "http://registry.npm.taobao.org/spdx-license-ids/download/spdx-license-ids-1.2.2.tgz",
+      "integrity": "sha1-yd96NCRZSt5r0RkA1ZZpbcBrrFc=",
+      "dev": true
+    },
+    "sprintf-js": {
+      "version": "1.0.3",
+      "resolved": "http://registry.npm.taobao.org/sprintf-js/download/sprintf-js-1.0.3.tgz",
+      "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=",
+      "dev": true
+    },
+    "sshpk": {
+      "version": "1.13.1",
+      "resolved": "http://registry.npm.taobao.org/sshpk/download/sshpk-1.13.1.tgz",
+      "integrity": "sha1-US322mKHFEMW3EwY/hzx2UBzm+M=",
+      "dev": true,
+      "requires": {
+        "asn1": "0.2.3",
+        "assert-plus": "1.0.0",
+        "bcrypt-pbkdf": "1.0.1",
+        "dashdash": "1.14.1",
+        "ecc-jsbn": "0.1.1",
+        "getpass": "0.1.7",
+        "jsbn": "0.1.1",
+        "tweetnacl": "0.14.5"
+      }
+    },
+    "stackframe": {
+      "version": "0.3.1",
+      "resolved": "http://registry.npm.taobao.org/stackframe/download/stackframe-0.3.1.tgz",
+      "integrity": "sha1-M6qE8Rd6VUjIk1Uzy/6zQgl19aQ=",
+      "dev": true
+    },
+    "statuses": {
+      "version": "1.3.1",
+      "resolved": "http://registry.npm.taobao.org/statuses/download/statuses-1.3.1.tgz",
+      "integrity": "sha1-+vUbnrdKrvOzrPStX2Gr8ky3uT4=",
+      "dev": true
+    },
+    "stealthy-require": {
+      "version": "1.1.1",
+      "resolved": "http://registry.npm.taobao.org/stealthy-require/download/stealthy-require-1.1.1.tgz",
+      "integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=",
+      "dev": true
+    },
+    "stream-browserify": {
+      "version": "2.0.1",
+      "resolved": "http://registry.npm.taobao.org/stream-browserify/download/stream-browserify-2.0.1.tgz",
+      "integrity": "sha1-ZiZu5fm9uZQKTkUUyvtDu3Hlyds=",
+      "dev": true,
+      "requires": {
+        "inherits": "2.0.3",
+        "readable-stream": "2.3.3"
+      }
+    },
+    "stream-http": {
+      "version": "2.7.2",
+      "resolved": "http://registry.npm.taobao.org/stream-http/download/stream-http-2.7.2.tgz",
+      "integrity": "sha1-QKBQ7I3DtTsz2ZCUFcAsC/Gr+60=",
+      "dev": true,
+      "requires": {
+        "builtin-status-codes": "3.0.0",
+        "inherits": "2.0.3",
+        "readable-stream": "2.3.3",
+        "to-arraybuffer": "1.0.1",
+        "xtend": "4.0.1"
+      }
+    },
+    "strict-uri-encode": {
+      "version": "1.1.0",
+      "resolved": "http://registry.npm.taobao.org/strict-uri-encode/download/strict-uri-encode-1.1.0.tgz",
+      "integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM="
+    },
+    "string-width": {
+      "version": "1.0.2",
+      "resolved": "http://registry.npm.taobao.org/string-width/download/string-width-1.0.2.tgz",
+      "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
+      "dev": true,
+      "requires": {
+        "code-point-at": "1.1.0",
+        "is-fullwidth-code-point": "1.0.0",
+        "strip-ansi": "3.0.1"
+      }
+    },
+    "string.prototype.repeat": {
+      "version": "0.2.0",
+      "resolved": "http://registry.npm.taobao.org/string.prototype.repeat/download/string.prototype.repeat-0.2.0.tgz",
+      "integrity": "sha1-q6Nt4I3O5qWjN9SbLqHaGyj8Ds8="
+    },
+    "string_decoder": {
+      "version": "1.0.3",
+      "resolved": "http://registry.npm.taobao.org/string_decoder/download/string_decoder-1.0.3.tgz",
+      "integrity": "sha1-D8Z9fBQYJd6UKC3VNr7GubzoYKs=",
+      "dev": true,
+      "requires": {
+        "safe-buffer": "5.1.1"
+      }
+    },
+    "stringstream": {
+      "version": "0.0.5",
+      "resolved": "http://registry.npm.taobao.org/stringstream/download/stringstream-0.0.5.tgz",
+      "integrity": "sha1-TkhM1N5aC7vuGORjB3EKioFiGHg=",
+      "dev": true
+    },
+    "strip-ansi": {
+      "version": "3.0.1",
+      "resolved": "http://registry.npm.taobao.org/strip-ansi/download/strip-ansi-3.0.1.tgz",
+      "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
+      "requires": {
+        "ansi-regex": "2.1.1"
+      }
+    },
+    "strip-bom": {
+      "version": "2.0.0",
+      "resolved": "http://registry.npm.taobao.org/strip-bom/download/strip-bom-2.0.0.tgz",
+      "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=",
+      "dev": true,
+      "requires": {
+        "is-utf8": "0.2.1"
+      }
+    },
+    "strip-indent": {
+      "version": "1.0.1",
+      "resolved": "http://registry.npm.taobao.org/strip-indent/download/strip-indent-1.0.1.tgz",
+      "integrity": "sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI=",
+      "dev": true,
+      "requires": {
+        "get-stdin": "4.0.1"
+      }
+    },
+    "strip-json-comments": {
+      "version": "1.0.4",
+      "resolved": "http://registry.npm.taobao.org/strip-json-comments/download/strip-json-comments-1.0.4.tgz",
+      "integrity": "sha1-HhX7ysl9Pumb8tc7TGVrCCu6+5E=",
+      "dev": true
+    },
+    "style-loader": {
+      "version": "0.13.2",
+      "resolved": "http://registry.npm.taobao.org/style-loader/download/style-loader-0.13.2.tgz",
+      "integrity": "sha1-dFMzhM9pjHEEx5URULSXF63C87s=",
+      "dev": true,
+      "requires": {
+        "loader-utils": "1.1.0"
+      },
+      "dependencies": {
+        "loader-utils": {
+          "version": "1.1.0",
+          "resolved": "http://registry.npm.taobao.org/loader-utils/download/loader-utils-1.1.0.tgz",
+          "integrity": "sha1-yYrvSIvM7aL/teLeZG1qdUQp9c0=",
+          "dev": true,
+          "requires": {
+            "big.js": "3.2.0",
+            "emojis-list": "2.1.0",
+            "json5": "0.5.1"
+          }
+        }
+      }
+    },
+    "supports-color": {
+      "version": "3.2.3",
+      "resolved": "http://registry.npm.taobao.org/supports-color/download/supports-color-3.2.3.tgz",
+      "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=",
+      "dev": true,
+      "requires": {
+        "has-flag": "1.0.0"
+      }
+    },
+    "symbol-observable": {
+      "version": "0.2.4",
+      "resolved": "http://registry.npm.taobao.org/symbol-observable/download/symbol-observable-0.2.4.tgz",
+      "integrity": "sha1-lag9smGG1q9+ehjb2XYKL4bQj0A="
+    },
+    "table": {
+      "version": "3.8.3",
+      "resolved": "http://registry.npm.taobao.org/table/download/table-3.8.3.tgz",
+      "integrity": "sha1-K7xULw/amGGnVdOUf+/Ys/UThV8=",
+      "dev": true,
+      "requires": {
+        "ajv": "4.11.8",
+        "ajv-keywords": "1.5.1",
+        "chalk": "1.1.3",
+        "lodash": "4.17.4",
+        "slice-ansi": "0.0.4",
+        "string-width": "2.1.1"
+      },
+      "dependencies": {
+        "ajv": {
+          "version": "4.11.8",
+          "resolved": "http://registry.npm.taobao.org/ajv/download/ajv-4.11.8.tgz",
+          "integrity": "sha1-gv+wKynmYq5TvcIK8VlHcGc5xTY=",
+          "dev": true,
+          "requires": {
+            "co": "4.6.0",
+            "json-stable-stringify": "1.0.1"
+          }
+        },
+        "ansi-regex": {
+          "version": "3.0.0",
+          "resolved": "http://registry.npm.taobao.org/ansi-regex/download/ansi-regex-3.0.0.tgz",
+          "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=",
+          "dev": true
+        },
+        "is-fullwidth-code-point": {
+          "version": "2.0.0",
+          "resolved": "http://registry.npm.taobao.org/is-fullwidth-code-point/download/is-fullwidth-code-point-2.0.0.tgz",
+          "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=",
+          "dev": true
+        },
+        "string-width": {
+          "version": "2.1.1",
+          "resolved": "http://registry.npm.taobao.org/string-width/download/string-width-2.1.1.tgz",
+          "integrity": "sha1-q5Pyeo3BPSjKyBXEYhQ6bZASrp4=",
+          "dev": true,
+          "requires": {
+            "is-fullwidth-code-point": "2.0.0",
+            "strip-ansi": "4.0.0"
+          }
+        },
+        "strip-ansi": {
+          "version": "4.0.0",
+          "resolved": "http://registry.npm.taobao.org/strip-ansi/download/strip-ansi-4.0.0.tgz",
+          "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
+          "dev": true,
+          "requires": {
+            "ansi-regex": "3.0.0"
+          }
+        }
+      }
+    },
+    "tapable": {
+      "version": "0.1.10",
+      "resolved": "http://registry.npm.taobao.org/tapable/download/tapable-0.1.10.tgz",
+      "integrity": "sha1-KcNXB8K3DlDQdIK10gLo7URtr9Q=",
+      "dev": true
+    },
+    "tar": {
+      "version": "2.2.1",
+      "resolved": "http://registry.npm.taobao.org/tar/download/tar-2.2.1.tgz",
+      "integrity": "sha1-jk0qJWwOIYXGsYrWlK7JaLg8sdE=",
+      "dev": true,
+      "requires": {
+        "block-stream": "0.0.9",
+        "fstream": "1.0.11",
+        "inherits": "2.0.3"
+      }
+    },
+    "tar-stream": {
+      "version": "1.5.2",
+      "resolved": "http://registry.npm.taobao.org/tar-stream/download/tar-stream-1.5.2.tgz",
+      "integrity": "sha1-+8bG6DwaGdTLSMfZYXH8JI7/x78=",
+      "dev": true,
+      "requires": {
+        "bl": "1.2.1",
+        "end-of-stream": "1.4.0",
+        "readable-stream": "2.3.3",
+        "xtend": "4.0.1"
+      }
+    },
+    "temp-fs": {
+      "version": "0.9.9",
+      "resolved": "http://registry.npm.taobao.org/temp-fs/download/temp-fs-0.9.9.tgz",
+      "integrity": "sha1-gHFzBDeHByDpQxUy/igUNk+IA9c=",
+      "dev": true,
+      "requires": {
+        "rimraf": "2.5.4"
+      },
+      "dependencies": {
+        "glob": {
+          "version": "7.1.2",
+          "resolved": "http://registry.npm.taobao.org/glob/download/glob-7.1.2.tgz",
+          "integrity": "sha1-wZyd+aAocC1nhhI4SmVSQExjbRU=",
+          "dev": true,
+          "requires": {
+            "fs.realpath": "1.0.0",
+            "inflight": "1.0.6",
+            "inherits": "2.0.3",
+            "minimatch": "3.0.4",
+            "once": "1.4.0",
+            "path-is-absolute": "1.0.1"
+          }
+        },
+        "rimraf": {
+          "version": "2.5.4",
+          "resolved": "http://registry.npm.taobao.org/rimraf/download/rimraf-2.5.4.tgz",
+          "integrity": "sha1-loAAk8vxoMhr2VtGJUZ1NcKd+gQ=",
+          "dev": true,
+          "requires": {
+            "glob": "7.1.2"
+          }
+        }
+      }
+    },
+    "test-value": {
+      "version": "2.1.0",
+      "resolved": "http://registry.npm.taobao.org/test-value/download/test-value-2.1.0.tgz",
+      "integrity": "sha1-Edpv9nDzRxpztiXKTz/c97t0gpE=",
+      "dev": true,
+      "requires": {
+        "array-back": "1.0.4",
+        "typical": "2.6.1"
+      }
+    },
+    "text-table": {
+      "version": "0.2.0",
+      "resolved": "http://registry.npm.taobao.org/text-table/download/text-table-0.2.0.tgz",
+      "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=",
+      "dev": true
+    },
+    "through": {
+      "version": "2.3.8",
+      "resolved": "http://registry.npm.taobao.org/through/download/through-2.3.8.tgz",
+      "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=",
+      "dev": true
+    },
+    "time-stamp": {
+      "version": "2.0.0",
+      "resolved": "http://registry.npm.taobao.org/time-stamp/download/time-stamp-2.0.0.tgz",
+      "integrity": "sha1-lcakRTDhW6jW9KPsuMOj+sRto1c=",
+      "dev": true
+    },
+    "timers-browserify": {
+      "version": "2.0.4",
+      "resolved": "http://registry.npm.taobao.org/timers-browserify/download/timers-browserify-2.0.4.tgz",
+      "integrity": "sha1-lspT9LeUpefA4b18yIo3Ipj6AeY=",
+      "dev": true,
+      "requires": {
+        "setimmediate": "1.0.5"
+      }
+    },
+    "tmp": {
+      "version": "0.0.29",
+      "resolved": "http://registry.npm.taobao.org/tmp/download/tmp-0.0.29.tgz",
+      "integrity": "sha1-8lEl/w3Z2jzLDC3Tce4SiLuRKMA=",
+      "dev": true,
+      "requires": {
+        "os-tmpdir": "1.0.2"
+      }
+    },
+    "to-arraybuffer": {
+      "version": "1.0.1",
+      "resolved": "http://registry.npm.taobao.org/to-arraybuffer/download/to-arraybuffer-1.0.1.tgz",
+      "integrity": "sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M=",
+      "dev": true
+    },
+    "to-fast-properties": {
+      "version": "1.0.3",
+      "resolved": "http://registry.npm.taobao.org/to-fast-properties/download/to-fast-properties-1.0.3.tgz",
+      "integrity": "sha1-uDVx+k2MJbguIxsG46MFXeTKGkc="
+    },
+    "tough-cookie": {
+      "version": "2.3.3",
+      "resolved": "http://registry.npm.taobao.org/tough-cookie/download/tough-cookie-2.3.3.tgz",
+      "integrity": "sha1-C2GKVWW23qkL80JdBNVe3EdadWE=",
+      "dev": true,
+      "requires": {
+        "punycode": "1.4.1"
+      }
+    },
+    "trim-newlines": {
+      "version": "1.0.0",
+      "resolved": "http://registry.npm.taobao.org/trim-newlines/download/trim-newlines-1.0.0.tgz",
+      "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=",
+      "dev": true
+    },
+    "trim-right": {
+      "version": "1.0.1",
+      "resolved": "http://registry.npm.taobao.org/trim-right/download/trim-right-1.0.1.tgz",
+      "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM="
+    },
+    "tty-browserify": {
+      "version": "0.0.0",
+      "resolved": "http://registry.npm.taobao.org/tty-browserify/download/tty-browserify-0.0.0.tgz",
+      "integrity": "sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY=",
+      "dev": true
+    },
+    "tunnel-agent": {
+      "version": "0.6.0",
+      "resolved": "http://registry.npm.taobao.org/tunnel-agent/download/tunnel-agent-0.6.0.tgz",
+      "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=",
+      "dev": true,
+      "requires": {
+        "safe-buffer": "5.1.1"
+      }
+    },
+    "tweetnacl": {
+      "version": "0.14.5",
+      "resolved": "http://registry.npm.taobao.org/tweetnacl/download/tweetnacl-0.14.5.tgz",
+      "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=",
+      "dev": true,
+      "optional": true
+    },
+    "type-check": {
+      "version": "0.3.2",
+      "resolved": "http://registry.npm.taobao.org/type-check/download/type-check-0.3.2.tgz",
+      "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=",
+      "dev": true,
+      "requires": {
+        "prelude-ls": "1.1.2"
+      }
+    },
+    "type-detect": {
+      "version": "1.0.0",
+      "resolved": "http://registry.npm.taobao.org/type-detect/download/type-detect-1.0.0.tgz",
+      "integrity": "sha1-diIXzAbbJY7EiQihKY6LlRIejqI=",
+      "dev": true
+    },
+    "type-is": {
+      "version": "1.6.15",
+      "resolved": "http://registry.npm.taobao.org/type-is/download/type-is-1.6.15.tgz",
+      "integrity": "sha1-yrEPtJCeRByChC6v4a1kbIGARBA=",
+      "dev": true,
+      "requires": {
+        "media-typer": "0.3.0",
+        "mime-types": "2.1.17"
+      }
+    },
+    "typedarray": {
+      "version": "0.0.6",
+      "resolved": "http://registry.npm.taobao.org/typedarray/download/typedarray-0.0.6.tgz",
+      "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=",
+      "dev": true
+    },
+    "typical": {
+      "version": "2.6.1",
+      "resolved": "http://registry.npm.taobao.org/typical/download/typical-2.6.1.tgz",
+      "integrity": "sha1-XAgOXWYcu+OCWdLnCjxyU+hziB0=",
+      "dev": true
+    },
+    "ua-parser-js": {
+      "version": "0.7.17",
+      "resolved": "http://registry.npm.taobao.org/ua-parser-js/download/ua-parser-js-0.7.17.tgz",
+      "integrity": "sha1-6exflJi57JEOeuOsYmqAXE0J7Kw="
+    },
+    "uglify-js": {
+      "version": "2.7.5",
+      "resolved": "http://registry.npm.taobao.org/uglify-js/download/uglify-js-2.7.5.tgz",
+      "integrity": "sha1-RhLAx7qu4rp8SH3kkErhIgefLKg=",
+      "dev": true,
+      "requires": {
+        "async": "0.2.10",
+        "source-map": "0.5.7",
+        "uglify-to-browserify": "1.0.2",
+        "yargs": "3.10.0"
+      },
+      "dependencies": {
+        "async": {
+          "version": "0.2.10",
+          "resolved": "http://registry.npm.taobao.org/async/download/async-0.2.10.tgz",
+          "integrity": "sha1-trvgsGdLnXGXCMo43owjfLUmw9E=",
+          "dev": true
+        }
+      }
+    },
+    "uglify-to-browserify": {
+      "version": "1.0.2",
+      "resolved": "http://registry.npm.taobao.org/uglify-to-browserify/download/uglify-to-browserify-1.0.2.tgz",
+      "integrity": "sha1-bgkk1r2mta/jSeOabWMoUKD4grc=",
+      "dev": true
+    },
+    "uncontrollable": {
+      "version": "4.1.0",
+      "resolved": "http://registry.npm.taobao.org/uncontrollable/download/uncontrollable-4.1.0.tgz",
+      "integrity": "sha1-4DWCkSUuGGUiLZCTmxny9J+Bwak=",
+      "requires": {
+        "invariant": "2.2.2"
+      }
+    },
+    "unpipe": {
+      "version": "1.0.0",
+      "resolved": "http://registry.npm.taobao.org/unpipe/download/unpipe-1.0.0.tgz",
+      "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=",
+      "dev": true
+    },
+    "urijs": {
+      "version": "1.16.1",
+      "resolved": "http://registry.npm.taobao.org/urijs/download/urijs-1.16.1.tgz",
+      "integrity": "sha1-hZrTGJD1+VKHJ76J8ZMslPtHMeI=",
+      "dev": true
+    },
+    "urix": {
+      "version": "0.1.0",
+      "resolved": "http://registry.npm.taobao.org/urix/download/urix-0.1.0.tgz",
+      "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=",
+      "dev": true
+    },
+    "url": {
+      "version": "0.11.0",
+      "resolved": "http://registry.npm.taobao.org/url/download/url-0.11.0.tgz",
+      "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=",
+      "dev": true,
+      "requires": {
+        "punycode": "1.3.2",
+        "querystring": "0.2.0"
+      },
+      "dependencies": {
+        "punycode": {
+          "version": "1.3.2",
+          "resolved": "http://registry.npm.taobao.org/punycode/download/punycode-1.3.2.tgz",
+          "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=",
+          "dev": true
+        }
+      }
+    },
+    "url-loader": {
+      "version": "0.5.9",
+      "resolved": "http://registry.npm.taobao.org/url-loader/download/url-loader-0.5.9.tgz",
+      "integrity": "sha1-zI/qgse5Bud3cBklCGnlaemVwpU=",
+      "dev": true,
+      "requires": {
+        "loader-utils": "1.1.0",
+        "mime": "1.3.6"
+      },
+      "dependencies": {
+        "loader-utils": {
+          "version": "1.1.0",
+          "resolved": "http://registry.npm.taobao.org/loader-utils/download/loader-utils-1.1.0.tgz",
+          "integrity": "sha1-yYrvSIvM7aL/teLeZG1qdUQp9c0=",
+          "dev": true,
+          "requires": {
+            "big.js": "3.2.0",
+            "emojis-list": "2.1.0",
+            "json5": "0.5.1"
+          }
+        },
+        "mime": {
+          "version": "1.3.6",
+          "resolved": "http://registry.npm.taobao.org/mime/download/mime-1.3.6.tgz",
+          "integrity": "sha1-WR2E02U6awtKO5343lqoEI5y5eA=",
+          "dev": true
+        }
+      }
+    },
+    "user-home": {
+      "version": "1.1.1",
+      "resolved": "http://registry.npm.taobao.org/user-home/download/user-home-1.1.1.tgz",
+      "integrity": "sha1-K1viOjK2Onyd640PKNSFcko98ZA=",
+      "dev": true
+    },
+    "util": {
+      "version": "0.10.3",
+      "resolved": "http://registry.npm.taobao.org/util/download/util-0.10.3.tgz",
+      "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=",
+      "dev": true,
+      "requires": {
+        "inherits": "2.0.1"
+      },
+      "dependencies": {
+        "inherits": {
+          "version": "2.0.1",
+          "resolved": "http://registry.npm.taobao.org/inherits/download/inherits-2.0.1.tgz",
+          "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=",
+          "dev": true
+        }
+      }
+    },
+    "util-deprecate": {
+      "version": "1.0.2",
+      "resolved": "http://registry.npm.taobao.org/util-deprecate/download/util-deprecate-1.0.2.tgz",
+      "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=",
+      "dev": true
+    },
+    "utils-merge": {
+      "version": "1.0.1",
+      "resolved": "http://registry.npm.taobao.org/utils-merge/download/utils-merge-1.0.1.tgz",
+      "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=",
+      "dev": true
+    },
+    "uuid": {
+      "version": "2.0.3",
+      "resolved": "http://registry.npm.taobao.org/uuid/download/uuid-2.0.3.tgz",
+      "integrity": "sha1-Z+LoY3lyFVMN/zGOW/nc6/1Hsho="
+    },
+    "v8flags": {
+      "version": "2.1.1",
+      "resolved": "http://registry.npm.taobao.org/v8flags/download/v8flags-2.1.1.tgz",
+      "integrity": "sha1-qrGh+jDUX4jdMhFIh1rALAtV5bQ=",
+      "dev": true,
+      "requires": {
+        "user-home": "1.1.1"
+      }
+    },
+    "validate-npm-package-license": {
+      "version": "3.0.1",
+      "resolved": "http://registry.npm.taobao.org/validate-npm-package-license/download/validate-npm-package-license-3.0.1.tgz",
+      "integrity": "sha1-KAS6vnEq0zeUWaz74kdGqywwP7w=",
+      "dev": true,
+      "requires": {
+        "spdx-correct": "1.0.2",
+        "spdx-expression-parse": "1.0.4"
+      }
+    },
+    "validator": {
+      "version": "5.7.0",
+      "resolved": "http://registry.npm.taobao.org/validator/download/validator-5.7.0.tgz",
+      "integrity": "sha1-eoelgUa2laxIYHEUHAxJ1n2gXlw=",
+      "dev": true
+    },
+    "vary": {
+      "version": "1.1.2",
+      "resolved": "http://registry.npm.taobao.org/vary/download/vary-1.1.2.tgz",
+      "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=",
+      "dev": true
+    },
+    "verror": {
+      "version": "1.10.0",
+      "resolved": "http://registry.npm.taobao.org/verror/download/verror-1.10.0.tgz",
+      "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=",
+      "dev": true,
+      "requires": {
+        "assert-plus": "1.0.0",
+        "core-util-is": "1.0.2",
+        "extsprintf": "1.3.0"
+      }
+    },
+    "vm-browserify": {
+      "version": "0.0.4",
+      "resolved": "http://registry.npm.taobao.org/vm-browserify/download/vm-browserify-0.0.4.tgz",
+      "integrity": "sha1-XX6kW7755Kb/ZflUOOCofDV9WnM=",
+      "dev": true,
+      "requires": {
+        "indexof": "0.0.1"
+      }
+    },
+    "w3c-blob": {
+      "version": "0.0.1",
+      "resolved": "http://registry.npm.taobao.org/w3c-blob/download/w3c-blob-0.0.1.tgz",
+      "integrity": "sha1-sM01KhpQ9RVWNCD/1YYflQ8dhbg="
+    },
+    "warning": {
+      "version": "3.0.0",
+      "resolved": "http://registry.npm.taobao.org/warning/download/warning-3.0.0.tgz",
+      "integrity": "sha1-MuU3fLVy3kqwR1O9+IIcAe1gW3w=",
+      "requires": {
+        "loose-envify": "1.3.1"
+      }
+    },
+    "watchpack": {
+      "version": "0.2.9",
+      "resolved": "http://registry.npm.taobao.org/watchpack/download/watchpack-0.2.9.tgz",
+      "integrity": "sha1-Yuqkq15bo1/fwBgnVibjwPXj+ws=",
+      "dev": true,
+      "requires": {
+        "async": "0.9.2",
+        "chokidar": "1.7.0",
+        "graceful-fs": "4.1.11"
+      },
+      "dependencies": {
+        "async": {
+          "version": "0.9.2",
+          "resolved": "http://registry.npm.taobao.org/async/download/async-0.9.2.tgz",
+          "integrity": "sha1-rqdNXmHB+JlhO/ZL2mbUx48v0X0=",
+          "dev": true
+        }
+      }
+    },
+    "wdio-browserstack-service": {
+      "version": "0.1.13",
+      "resolved": "http://registry.npm.taobao.org/wdio-browserstack-service/download/wdio-browserstack-service-0.1.13.tgz",
+      "integrity": "sha1-5Ww2mu0nzciXq17Ur/3VJZmTPpo=",
+      "dev": true,
+      "requires": {
+        "browserstack-local": "1.3.0",
+        "request": "2.83.0",
+        "request-promise": "4.2.2"
+      }
+    },
+    "wdio-dot-reporter": {
+      "version": "0.0.6",
+      "resolved": "http://registry.npm.taobao.org/wdio-dot-reporter/download/wdio-dot-reporter-0.0.6.tgz",
+      "integrity": "sha1-FTs+HF12d3GQ2JNID/pvN4M3QPE=",
+      "dev": true,
+      "requires": {
+        "babel-runtime": "5.8.38"
+      },
+      "dependencies": {
+        "babel-runtime": {
+          "version": "5.8.38",
+          "resolved": "http://registry.npm.taobao.org/babel-runtime/download/babel-runtime-5.8.38.tgz",
+          "integrity": "sha1-HAsC62MxL18If/IEUIJ7QlydTBk=",
+          "dev": true,
+          "requires": {
+            "core-js": "1.2.7"
+          }
+        },
+        "core-js": {
+          "version": "1.2.7",
+          "resolved": "http://registry.npm.taobao.org/core-js/download/core-js-1.2.7.tgz",
+          "integrity": "sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY=",
+          "dev": true
+        }
+      }
+    },
+    "wdio-mocha-framework": {
+      "version": "0.5.12",
+      "resolved": "http://registry.npm.taobao.org/wdio-mocha-framework/download/wdio-mocha-framework-0.5.12.tgz",
+      "integrity": "sha1-hGM8aXJ1iWtG95Is17TLDNWQScw=",
+      "dev": true,
+      "requires": {
+        "babel-runtime": "6.26.0",
+        "mocha": "4.0.1",
+        "wdio-sync": "0.7.1"
+      },
+      "dependencies": {
+        "commander": {
+          "version": "2.11.0",
+          "resolved": "http://registry.npm.taobao.org/commander/download/commander-2.11.0.tgz",
+          "integrity": "sha1-FXFS/R56bI2YpbcVzzdt+SgARWM=",
+          "dev": true
+        },
+        "debug": {
+          "version": "3.1.0",
+          "resolved": "http://registry.npm.taobao.org/debug/download/debug-3.1.0.tgz",
+          "integrity": "sha1-W7WgZyYotkFJVmuhaBnmFRjGcmE=",
+          "dev": true,
+          "requires": {
+            "ms": "2.0.0"
+          }
+        },
+        "diff": {
+          "version": "3.3.1",
+          "resolved": "http://registry.npm.taobao.org/diff/download/diff-3.3.1.tgz",
+          "integrity": "sha1-qoVnpu7QPFMfyJ0/cRzQ5SWd7HU=",
+          "dev": true
+        },
+        "glob": {
+          "version": "7.1.2",
+          "resolved": "http://registry.npm.taobao.org/glob/download/glob-7.1.2.tgz",
+          "integrity": "sha1-wZyd+aAocC1nhhI4SmVSQExjbRU=",
+          "dev": true,
+          "requires": {
+            "fs.realpath": "1.0.0",
+            "inflight": "1.0.6",
+            "inherits": "2.0.3",
+            "minimatch": "3.0.4",
+            "once": "1.4.0",
+            "path-is-absolute": "1.0.1"
+          }
+        },
+        "growl": {
+          "version": "1.10.3",
+          "resolved": "http://registry.npm.taobao.org/growl/download/growl-1.10.3.tgz",
+          "integrity": "sha1-GSa6kM8+3+KttJJ/WIC8IsZseQ8=",
+          "dev": true
+        },
+        "has-flag": {
+          "version": "2.0.0",
+          "resolved": "http://registry.npm.taobao.org/has-flag/download/has-flag-2.0.0.tgz",
+          "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=",
+          "dev": true
+        },
+        "mocha": {
+          "version": "4.0.1",
+          "resolved": "http://registry.npm.taobao.org/mocha/download/mocha-4.0.1.tgz",
+          "integrity": "sha1-Cu5alc9ppGGIIPXlH6MXFxF9rxs=",
+          "dev": true,
+          "requires": {
+            "browser-stdout": "1.3.0",
+            "commander": "2.11.0",
+            "debug": "3.1.0",
+            "diff": "3.3.1",
+            "escape-string-regexp": "1.0.5",
+            "glob": "7.1.2",
+            "growl": "1.10.3",
+            "he": "1.1.1",
+            "mkdirp": "0.5.1",
+            "supports-color": "4.4.0"
+          }
+        },
+        "supports-color": {
+          "version": "4.4.0",
+          "resolved": "http://registry.npm.taobao.org/supports-color/download/supports-color-4.4.0.tgz",
+          "integrity": "sha1-iD992rwWUUKyphQn8zUt7RldGj4=",
+          "dev": true,
+          "requires": {
+            "has-flag": "2.0.0"
+          }
+        }
+      }
+    },
+    "wdio-selenium-standalone-service": {
+      "version": "0.0.7",
+      "resolved": "http://registry.npm.taobao.org/wdio-selenium-standalone-service/download/wdio-selenium-standalone-service-0.0.7.tgz",
+      "integrity": "sha1-eMAOmW0SQihbYqpqu9XDVEses+E=",
+      "dev": true,
+      "requires": {
+        "fs-extra": "0.30.0",
+        "selenium-standalone": "5.11.2"
+      }
+    },
+    "wdio-spec-reporter": {
+      "version": "0.0.5",
+      "resolved": "http://registry.npm.taobao.org/wdio-spec-reporter/download/wdio-spec-reporter-0.0.5.tgz",
+      "integrity": "sha1-0PuP0UrxU/4BAFG7dAqjCrMQY/U=",
+      "dev": true,
+      "requires": {
+        "babel-runtime": "5.8.38",
+        "humanize-duration": "3.12.0"
+      },
+      "dependencies": {
+        "babel-runtime": {
+          "version": "5.8.38",
+          "resolved": "http://registry.npm.taobao.org/babel-runtime/download/babel-runtime-5.8.38.tgz",
+          "integrity": "sha1-HAsC62MxL18If/IEUIJ7QlydTBk=",
+          "dev": true,
+          "requires": {
+            "core-js": "1.2.7"
+          }
+        },
+        "core-js": {
+          "version": "1.2.7",
+          "resolved": "http://registry.npm.taobao.org/core-js/download/core-js-1.2.7.tgz",
+          "integrity": "sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY=",
+          "dev": true
+        }
+      }
+    },
+    "wdio-sync": {
+      "version": "0.7.1",
+      "resolved": "http://registry.npm.taobao.org/wdio-sync/download/wdio-sync-0.7.1.tgz",
+      "integrity": "sha1-AIR/u84WgmwyJWGPQlnSi2CkJIM=",
+      "dev": true,
+      "requires": {
+        "babel-runtime": "6.26.0",
+        "fibers": "2.0.0",
+        "object.assign": "4.1.0"
+      }
+    },
+    "webdriverio": {
+      "version": "4.6.2",
+      "resolved": "http://registry.npm.taobao.org/webdriverio/download/webdriverio-4.6.2.tgz",
+      "integrity": "sha1-3Qle5hiJaiHI8bnUJ4c22Fpkyg8=",
+      "dev": true,
+      "requires": {
+        "archiver": "1.0.0",
+        "babel-runtime": "6.26.0",
+        "css-parse": "2.0.0",
+        "css-value": "0.0.1",
+        "deepmerge": "0.2.10",
+        "ejs": "2.5.7",
+        "gaze": "1.1.2",
+        "glob": "7.1.2",
+        "inquirer": "1.2.3",
+        "json-stringify-safe": "5.0.1",
+        "mkdirp": "0.5.1",
+        "npm-install-package": "1.1.0",
+        "optimist": "0.6.1",
+        "q": "1.4.1",
+        "request": "2.79.0",
+        "rgb2hex": "0.1.0",
+        "supports-color": "3.2.3",
+        "url": "0.11.0",
+        "validator": "5.7.0",
+        "wdio-dot-reporter": "0.0.6",
+        "wgxpath": "1.0.0"
+      },
+      "dependencies": {
+        "assert-plus": {
+          "version": "0.2.0",
+          "resolved": "http://registry.npm.taobao.org/assert-plus/download/assert-plus-0.2.0.tgz",
+          "integrity": "sha1-104bh+ev/A24qttwIfP+SBAasjQ=",
+          "dev": true
+        },
+        "aws-sign2": {
+          "version": "0.6.0",
+          "resolved": "http://registry.npm.taobao.org/aws-sign2/download/aws-sign2-0.6.0.tgz",
+          "integrity": "sha1-FDQt0428yU0OW4fXY81jYSwOeU8=",
+          "dev": true
+        },
+        "boom": {
+          "version": "2.10.1",
+          "resolved": "http://registry.npm.taobao.org/boom/download/boom-2.10.1.tgz",
+          "integrity": "sha1-OciRjO/1eZ+D+UkqhI9iWt0Mdm8=",
+          "dev": true,
+          "requires": {
+            "hoek": "2.16.3"
+          }
+        },
+        "caseless": {
+          "version": "0.11.0",
+          "resolved": "http://registry.npm.taobao.org/caseless/download/caseless-0.11.0.tgz",
+          "integrity": "sha1-cVuW6phBWTzDMGeSP17GDr2k99c=",
+          "dev": true
+        },
+        "cryptiles": {
+          "version": "2.0.5",
+          "resolved": "http://registry.npm.taobao.org/cryptiles/download/cryptiles-2.0.5.tgz",
+          "integrity": "sha1-O9/s3GCBR8HGcgL6KR59ylnqo7g=",
+          "dev": true,
+          "requires": {
+            "boom": "2.10.1"
+          }
+        },
+        "form-data": {
+          "version": "2.1.4",
+          "resolved": "http://registry.npm.taobao.org/form-data/download/form-data-2.1.4.tgz",
+          "integrity": "sha1-M8GDrPGTJ27KqYFDpp6Uv+4XUNE=",
+          "dev": true,
+          "requires": {
+            "asynckit": "0.4.0",
+            "combined-stream": "1.0.5",
+            "mime-types": "2.1.17"
+          }
+        },
+        "glob": {
+          "version": "7.1.2",
+          "resolved": "http://registry.npm.taobao.org/glob/download/glob-7.1.2.tgz",
+          "integrity": "sha1-wZyd+aAocC1nhhI4SmVSQExjbRU=",
+          "dev": true,
+          "requires": {
+            "fs.realpath": "1.0.0",
+            "inflight": "1.0.6",
+            "inherits": "2.0.3",
+            "minimatch": "3.0.4",
+            "once": "1.4.0",
+            "path-is-absolute": "1.0.1"
+          }
+        },
+        "har-validator": {
+          "version": "2.0.6",
+          "resolved": "http://registry.npm.taobao.org/har-validator/download/har-validator-2.0.6.tgz",
+          "integrity": "sha1-zcvAgYgmWtEZtqWnyKtw7s+10n0=",
+          "dev": true,
+          "requires": {
+            "chalk": "1.1.3",
+            "commander": "2.12.2",
+            "is-my-json-valid": "2.17.1",
+            "pinkie-promise": "2.0.1"
+          }
+        },
+        "hawk": {
+          "version": "3.1.3",
+          "resolved": "http://registry.npm.taobao.org/hawk/download/hawk-3.1.3.tgz",
+          "integrity": "sha1-B4REvXwWQLD+VA0sm3PVlnjo4cQ=",
+          "dev": true,
+          "requires": {
+            "boom": "2.10.1",
+            "cryptiles": "2.0.5",
+            "hoek": "2.16.3",
+            "sntp": "1.0.9"
+          }
+        },
+        "hoek": {
+          "version": "2.16.3",
+          "resolved": "http://registry.npm.taobao.org/hoek/download/hoek-2.16.3.tgz",
+          "integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=",
+          "dev": true
+        },
+        "http-signature": {
+          "version": "1.1.1",
+          "resolved": "http://registry.npm.taobao.org/http-signature/download/http-signature-1.1.1.tgz",
+          "integrity": "sha1-33LiZwZs0Kxn+3at+OE0qPvPkb8=",
+          "dev": true,
+          "requires": {
+            "assert-plus": "0.2.0",
+            "jsprim": "1.4.1",
+            "sshpk": "1.13.1"
+          }
+        },
+        "inquirer": {
+          "version": "1.2.3",
+          "resolved": "http://registry.npm.taobao.org/inquirer/download/inquirer-1.2.3.tgz",
+          "integrity": "sha1-TexvMvN+97sLLtPx0aXD9UUHSRg=",
+          "dev": true,
+          "requires": {
+            "ansi-escapes": "1.4.0",
+            "chalk": "1.1.3",
+            "cli-cursor": "1.0.2",
+            "cli-width": "2.2.0",
+            "external-editor": "1.1.1",
+            "figures": "1.7.0",
+            "lodash": "4.17.4",
+            "mute-stream": "0.0.6",
+            "pinkie-promise": "2.0.1",
+            "run-async": "2.3.0",
+            "rx": "4.1.0",
+            "string-width": "1.0.2",
+            "strip-ansi": "3.0.1",
+            "through": "2.3.8"
+          }
+        },
+        "mute-stream": {
+          "version": "0.0.6",
+          "resolved": "http://registry.npm.taobao.org/mute-stream/download/mute-stream-0.0.6.tgz",
+          "integrity": "sha1-SJYrGeFp/R38JAs/HnMXYnu8R9s=",
+          "dev": true
+        },
+        "qs": {
+          "version": "6.3.2",
+          "resolved": "http://registry.npm.taobao.org/qs/download/qs-6.3.2.tgz",
+          "integrity": "sha1-51vV9uJoEioqDgvaYwslUMFmUCw=",
+          "dev": true
+        },
+        "request": {
+          "version": "2.79.0",
+          "resolved": "http://registry.npm.taobao.org/request/download/request-2.79.0.tgz",
+          "integrity": "sha1-Tf5b9r6LjNw3/Pk+BLZVd3InEN4=",
+          "dev": true,
+          "requires": {
+            "aws-sign2": "0.6.0",
+            "aws4": "1.6.0",
+            "caseless": "0.11.0",
+            "combined-stream": "1.0.5",
+            "extend": "3.0.1",
+            "forever-agent": "0.6.1",
+            "form-data": "2.1.4",
+            "har-validator": "2.0.6",
+            "hawk": "3.1.3",
+            "http-signature": "1.1.1",
+            "is-typedarray": "1.0.0",
+            "isstream": "0.1.2",
+            "json-stringify-safe": "5.0.1",
+            "mime-types": "2.1.17",
+            "oauth-sign": "0.8.2",
+            "qs": "6.3.2",
+            "stringstream": "0.0.5",
+            "tough-cookie": "2.3.3",
+            "tunnel-agent": "0.4.3",
+            "uuid": "3.1.0"
+          }
+        },
+        "run-async": {
+          "version": "2.3.0",
+          "resolved": "http://registry.npm.taobao.org/run-async/download/run-async-2.3.0.tgz",
+          "integrity": "sha1-A3GrSuC91yDUFm19/aZP96RFpsA=",
+          "dev": true,
+          "requires": {
+            "is-promise": "2.1.0"
+          }
+        },
+        "sntp": {
+          "version": "1.0.9",
+          "resolved": "http://registry.npm.taobao.org/sntp/download/sntp-1.0.9.tgz",
+          "integrity": "sha1-ZUEYTMkK7qbG57NeJlkIJEPGYZg=",
+          "dev": true,
+          "requires": {
+            "hoek": "2.16.3"
+          }
+        },
+        "tunnel-agent": {
+          "version": "0.4.3",
+          "resolved": "http://registry.npm.taobao.org/tunnel-agent/download/tunnel-agent-0.4.3.tgz",
+          "integrity": "sha1-Y3PbdpCf5XDgjXNYM2Xtgop07us=",
+          "dev": true
+        },
+        "uuid": {
+          "version": "3.1.0",
+          "resolved": "http://registry.npm.taobao.org/uuid/download/uuid-3.1.0.tgz",
+          "integrity": "sha1-PdPT55Crwk17DToDT/q6vijrvAQ=",
+          "dev": true
+        }
+      }
+    },
+    "webpack": {
+      "version": "1.15.0",
+      "resolved": "http://registry.npm.taobao.org/webpack/download/webpack-1.15.0.tgz",
+      "integrity": "sha1-T/MfU9sDM55VFkqdRo7gMklo/pg=",
+      "dev": true,
+      "requires": {
+        "acorn": "3.3.0",
+        "async": "1.5.2",
+        "clone": "1.0.3",
+        "enhanced-resolve": "0.9.1",
+        "interpret": "0.6.6",
+        "loader-utils": "0.2.17",
+        "memory-fs": "0.3.0",
+        "mkdirp": "0.5.1",
+        "node-libs-browser": "0.7.0",
+        "optimist": "0.6.1",
+        "supports-color": "3.2.3",
+        "tapable": "0.1.10",
+        "uglify-js": "2.7.5",
+        "watchpack": "0.2.9",
+        "webpack-core": "0.6.9"
+      },
+      "dependencies": {
+        "acorn": {
+          "version": "3.3.0",
+          "resolved": "http://registry.npm.taobao.org/acorn/download/acorn-3.3.0.tgz",
+          "integrity": "sha1-ReN/s56No/JbruP/U2niu18iAXo=",
+          "dev": true
+        }
+      }
+    },
+    "webpack-core": {
+      "version": "0.6.9",
+      "resolved": "http://registry.npm.taobao.org/webpack-core/download/webpack-core-0.6.9.tgz",
+      "integrity": "sha1-/FcViMhVjad76e+23r3Fo7FyvcI=",
+      "dev": true,
+      "requires": {
+        "source-list-map": "0.1.8",
+        "source-map": "0.4.4"
+      },
+      "dependencies": {
+        "source-map": {
+          "version": "0.4.4",
+          "resolved": "http://registry.npm.taobao.org/source-map/download/source-map-0.4.4.tgz",
+          "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=",
+          "dev": true,
+          "requires": {
+            "amdefine": "1.0.1"
+          }
+        }
+      }
+    },
+    "webpack-dev-middleware": {
+      "version": "1.12.2",
+      "resolved": "http://registry.npm.taobao.org/webpack-dev-middleware/download/webpack-dev-middleware-1.12.2.tgz",
+      "integrity": "sha1-+PwRIM47T8VoDO7LQ9d3lmshEF4=",
+      "dev": true,
+      "requires": {
+        "memory-fs": "0.4.1",
+        "mime": "1.6.0",
+        "path-is-absolute": "1.0.1",
+        "range-parser": "1.2.0",
+        "time-stamp": "2.0.0"
+      },
+      "dependencies": {
+        "memory-fs": {
+          "version": "0.4.1",
+          "resolved": "http://registry.npm.taobao.org/memory-fs/download/memory-fs-0.4.1.tgz",
+          "integrity": "sha1-OpoguEYlI+RHz7x+i7gO1me/xVI=",
+          "dev": true,
+          "requires": {
+            "errno": "0.1.6",
+            "readable-stream": "2.3.3"
+          }
+        },
+        "mime": {
+          "version": "1.6.0",
+          "resolved": "http://registry.npm.taobao.org/mime/download/mime-1.6.0.tgz",
+          "integrity": "sha1-Ms2eXGRVO9WNGaVor0Uqz/BJgbE=",
+          "dev": true
+        }
+      }
+    },
+    "webpack-hot-middleware": {
+      "version": "2.21.0",
+      "resolved": "http://registry.npm.taobao.org/webpack-hot-middleware/download/webpack-hot-middleware-2.21.0.tgz",
+      "integrity": "sha1-ezwROnpLMByR4HSVc8eqsotBS1I=",
+      "dev": true,
+      "requires": {
+        "ansi-html": "0.0.7",
+        "html-entities": "1.2.1",
+        "querystring": "0.2.0",
+        "strip-ansi": "3.0.1"
+      }
+    },
+    "webpack-sources": {
+      "version": "0.1.5",
+      "resolved": "http://registry.npm.taobao.org/webpack-sources/download/webpack-sources-0.1.5.tgz",
+      "integrity": "sha1-qh86vw8NdNtxEcQOUAuE+WZkB1A=",
+      "dev": true,
+      "requires": {
+        "source-list-map": "0.1.8",
+        "source-map": "0.5.7"
+      }
+    },
+    "wgxpath": {
+      "version": "1.0.0",
+      "resolved": "http://registry.npm.taobao.org/wgxpath/download/wgxpath-1.0.0.tgz",
+      "integrity": "sha1-7vikudVYzEla06mit1FZfs2a9pA=",
+      "dev": true
+    },
+    "whatwg-fetch": {
+      "version": "2.0.3",
+      "resolved": "http://registry.npm.taobao.org/whatwg-fetch/download/whatwg-fetch-2.0.3.tgz",
+      "integrity": "sha1-nITsLc9oGH/wC8ZOEnS0QhduHIQ="
+    },
+    "which": {
+      "version": "1.3.0",
+      "resolved": "http://registry.npm.taobao.org/which/download/which-1.3.0.tgz",
+      "integrity": "sha1-/wS9/AEO5UfXgL7DjhrBwnd9JTo=",
+      "dev": true,
+      "requires": {
+        "isexe": "2.0.0"
+      }
+    },
+    "which-module": {
+      "version": "1.0.0",
+      "resolved": "http://registry.npm.taobao.org/which-module/download/which-module-1.0.0.tgz",
+      "integrity": "sha1-u6Y8qGGUiZT/MHc2CJ47lgJsKk8=",
+      "dev": true
+    },
+    "wide-align": {
+      "version": "1.1.2",
+      "resolved": "http://registry.npm.taobao.org/wide-align/download/wide-align-1.1.2.tgz",
+      "integrity": "sha1-Vx4PGwYEY268DfwhsDObvjE0FxA=",
+      "dev": true,
+      "requires": {
+        "string-width": "1.0.2"
+      }
+    },
+    "window-size": {
+      "version": "0.1.0",
+      "resolved": "http://registry.npm.taobao.org/window-size/download/window-size-0.1.0.tgz",
+      "integrity": "sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0=",
+      "dev": true
+    },
+    "wordwrap": {
+      "version": "1.0.0",
+      "resolved": "http://registry.npm.taobao.org/wordwrap/download/wordwrap-1.0.0.tgz",
+      "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=",
+      "dev": true
+    },
+    "wrap-ansi": {
+      "version": "2.1.0",
+      "resolved": "http://registry.npm.taobao.org/wrap-ansi/download/wrap-ansi-2.1.0.tgz",
+      "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=",
+      "dev": true,
+      "requires": {
+        "string-width": "1.0.2",
+        "strip-ansi": "3.0.1"
+      }
+    },
+    "wrappy": {
+      "version": "1.0.2",
+      "resolved": "http://registry.npm.taobao.org/wrappy/download/wrappy-1.0.2.tgz",
+      "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
+      "dev": true
+    },
+    "write": {
+      "version": "0.2.1",
+      "resolved": "http://registry.npm.taobao.org/write/download/write-0.2.1.tgz",
+      "integrity": "sha1-X8A4KOJkzqP+kUVUdvejxWbLB1c=",
+      "dev": true,
+      "requires": {
+        "mkdirp": "0.5.1"
+      }
+    },
+    "xss-filters": {
+      "version": "1.2.7",
+      "resolved": "http://registry.npm.taobao.org/xss-filters/download/xss-filters-1.2.7.tgz",
+      "integrity": "sha1-Wfod4gHzby80cNysX1jMwoMLCpo="
+    },
+    "xtend": {
+      "version": "4.0.1",
+      "resolved": "http://registry.npm.taobao.org/xtend/download/xtend-4.0.1.tgz",
+      "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=",
+      "dev": true
+    },
+    "y18n": {
+      "version": "3.2.1",
+      "resolved": "http://registry.npm.taobao.org/y18n/download/y18n-3.2.1.tgz",
+      "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=",
+      "dev": true
+    },
+    "yallist": {
+      "version": "2.1.2",
+      "resolved": "http://registry.npm.taobao.org/yallist/download/yallist-2.1.2.tgz",
+      "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=",
+      "dev": true
+    },
+    "yargs": {
+      "version": "3.10.0",
+      "resolved": "http://registry.npm.taobao.org/yargs/download/yargs-3.10.0.tgz",
+      "integrity": "sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=",
+      "dev": true,
+      "requires": {
+        "camelcase": "1.2.1",
+        "cliui": "2.1.0",
+        "decamelize": "1.2.0",
+        "window-size": "0.1.0"
+      },
+      "dependencies": {
+        "camelcase": {
+          "version": "1.2.1",
+          "resolved": "http://registry.npm.taobao.org/camelcase/download/camelcase-1.2.1.tgz",
+          "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=",
+          "dev": true
+        }
+      }
+    },
+    "yargs-parser": {
+      "version": "5.0.0",
+      "resolved": "http://registry.npm.taobao.org/yargs-parser/download/yargs-parser-5.0.0.tgz",
+      "integrity": "sha1-J17PDX/+Bcd+ZOfIbkzZS/DhIoo=",
+      "dev": true,
+      "requires": {
+        "camelcase": "3.0.0"
+      },
+      "dependencies": {
+        "camelcase": {
+          "version": "3.0.0",
+          "resolved": "http://registry.npm.taobao.org/camelcase/download/camelcase-3.0.0.tgz",
+          "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=",
+          "dev": true
+        }
+      }
+    },
+    "yauzl": {
+      "version": "2.9.1",
+      "resolved": "http://registry.npm.taobao.org/yauzl/download/yauzl-2.9.1.tgz",
+      "integrity": "sha1-qBmB6nCleUYTOIPwKcWCGok1mn8=",
+      "dev": true,
+      "requires": {
+        "buffer-crc32": "0.2.13",
+        "fd-slicer": "1.0.1"
+      }
+    },
+    "zip-stream": {
+      "version": "1.2.0",
+      "resolved": "http://registry.npm.taobao.org/zip-stream/download/zip-stream-1.2.0.tgz",
+      "integrity": "sha1-qLxF9MG0lpnGuQGYuqyqzbzUugQ=",
+      "dev": true,
+      "requires": {
+        "archiver-utils": "1.3.0",
+        "compress-commons": "1.2.2",
+        "lodash": "4.17.4",
+        "readable-stream": "2.3.3"
+      }
+    }
+  }
+}
diff --git a/package.json b/package.json
new file mode 100644 (file)
index 0000000..583c361
--- /dev/null
@@ -0,0 +1,89 @@
+{
+  "name": "dashboard",
+  "version": "1.0.0",
+  "description": "",
+  "scripts": {
+    "build:dll": "node ./bin/dependencies.js",
+    "postinstall": "npm run build:dll",
+    "build": "NODE_ENV=production webpack --config webpack/webpack.app.js",
+    "lint": "eslint \"src/**\"",
+    "start": "hjs-dev-server webpack/webpack.app.js",
+    "test": "wdio test/conf/wdio.local.js --suite base",
+    "testExtended": "EXTENDED=1 wdio test/conf/wdio.local.js",
+    "generate-component": "babel-node bin/generate.js component"
+  },
+  "author": "Chain",
+  "license": "",
+  "browserslist": [
+    "> 1%",
+    "last 3 versions",
+    "Safari >= 8"
+  ],
+  "dependencies": {
+    "babel-polyfill": "~6.16.0",
+    "bootstrap-sass": "~3.3.7",
+    "chain-sdk": "~1.2.1",
+    "classnames": "~2.2.5",
+    "fetch-ponyfill": "~3.0.2",
+    "lodash": "~4.17.4",
+    "moment": "~2.14.1",
+    "moment-timezone": "~0.5.5",
+    "pluralize": "~3.0.0",
+    "prop-types": "^15.5.8",
+    "react": "~15.5.0",
+    "react-ace": "~3.5.0",
+    "react-autosuggest": "~6.0.4",
+    "react-bootstrap": "~0.30.3",
+    "react-dom": "~15.5.0",
+    "react-markdown": "^2.5.0",
+    "react-redux": "~4.4.5",
+    "react-router": "~2.6.0",
+    "react-router-redux": "~4.0.5",
+    "react-router-scroll": "~0.3.2",
+    "redux": "~3.5.2",
+    "redux-form": "~5.3.2",
+    "redux-thunk": "~2.1.0",
+    "reselect": "^3.0.0",
+    "sha.js": "^2.4.8",
+    "uuid": "~2.0.2"
+  },
+  "devDependencies": {
+    "autoprefixer": "~6.7.7",
+    "babel-cli": "~6.14.0",
+    "babel-core": "~6.11.4",
+    "babel-eslint": "~7.0.0",
+    "babel-loader": "~6.2.4",
+    "babel-plugin-transform-object-rest-spread": "~6.8.0",
+    "babel-preset-es2015": "~6.9.0",
+    "babel-preset-react": "~6.5.0",
+    "babel-preset-react-hmre": "~1.1.1",
+    "babel-register": "~6.22.0",
+    "bootstrap-loader": "~1.0.10",
+    "chai": "~3.5.0",
+    "chai-as-promised": "~6.0.0",
+    "command-line-args": "~3.0.1",
+    "command-line-commands": "~1.0.4",
+    "css-loader": "0.14.5",
+    "eslint": "~3.7.0",
+    "eslint-plugin-react": "~6.4.0",
+    "file-loader": "~0.9.0",
+    "hjs-webpack": "~8.3.0",
+    "json-loader": "~0.5.4",
+    "mocha": "~2.2.0",
+    "nock": "~4.0.0",
+    "node-sass": "~3.8.0",
+    "postcss-loader": "~1.1.0",
+    "react-hot-loader": "~1.3.0",
+    "resolve-url-loader": "~1.6.0",
+    "sass-loader": "~4.0.0",
+    "sass-resources-loader": "~1.1.0",
+    "shelljs": "~0.7.4",
+    "style-loader": "~0.13.1",
+    "url-loader": "~0.5.7",
+    "wdio-browserstack-service": "~0.1.4",
+    "wdio-mocha-framework": "~0.5.8",
+    "wdio-selenium-standalone-service": "0.0.7",
+    "wdio-spec-reporter": "0.0.5",
+    "webdriverio": "~4.6.2"
+  }
+}
diff --git a/src/Root.jsx b/src/Root.jsx
new file mode 100644 (file)
index 0000000..9d1d543
--- /dev/null
@@ -0,0 +1,28 @@
+import React from 'react'
+import { Provider } from 'react-redux'
+import { applyRouterMiddleware, Router } from 'react-router'
+import { history } from 'utility/environment'
+import { syncHistoryWithStore } from 'react-router-redux'
+import useScroll from 'react-router-scroll/lib/useScroll'
+
+import makeRoutes from './routes'
+
+export default class Root extends React.Component {
+  componentWillMount() {
+    document.title = 'Bytom Dashboard'
+  }
+
+  render() {
+    const store = this.props.store
+    const syncedHistory = syncHistoryWithStore(history, store)
+    return (
+      <Provider store={store}>
+        <Router
+          history={syncedHistory}
+          routes={makeRoutes(store)}
+          render={applyRouterMiddleware(useScroll())}
+        />
+      </Provider>
+    )
+  }
+}
diff --git a/src/actions.js b/src/actions.js
new file mode 100644 (file)
index 0000000..18d494e
--- /dev/null
@@ -0,0 +1,31 @@
+import accessControl from 'features/accessControl/actions'
+import { actions as account } from 'features/accounts'
+import { actions as app } from 'features/app'
+import { actions as asset } from 'features/assets'
+import { actions as balance } from 'features/balances'
+import { actions as configuration } from 'features/configuration'
+import { actions as core } from 'features/core'
+import { actions as mockhsm } from 'features/mockhsm'
+import { actions as testnet } from 'features/testnet'
+import { actions as transaction } from 'features/transactions'
+import { actions as transactionFeed } from 'features/transactionFeeds'
+import { actions as tutorial } from 'features/tutorial'
+import { actions as unspent } from 'features/unspents'
+
+const actions = {
+  accessControl,
+  account,
+  app,
+  asset,
+  balance,
+  configuration,
+  core,
+  mockhsm,
+  testnet,
+  transaction,
+  transactionFeed,
+  tutorial,
+  unspent,
+}
+
+export default actions
diff --git a/src/app.js b/src/app.js
new file mode 100644 (file)
index 0000000..841a4ab
--- /dev/null
@@ -0,0 +1,22 @@
+/*eslint-env node*/
+
+import 'bootstrap-loader'
+import React from 'react'
+import { render } from 'react-dom'
+import Root from 'Root'
+import configureStore from 'configureStore'
+
+// Set favicon
+let faviconPath = require('!!file?name=favicon.ico!../static/images/favicon.png')
+let favicon = document.createElement('link')
+favicon.type = 'image/png'
+favicon.rel = 'shortcut icon'
+favicon.href = faviconPath
+document.getElementsByTagName('head')[0].appendChild(favicon)
+
+// Start app
+export const store = configureStore()
+render(
+       <Root store={store}/>,
+       document.getElementById('root')
+)
diff --git a/src/configureStore.js b/src/configureStore.js
new file mode 100644 (file)
index 0000000..d3b80e6
--- /dev/null
@@ -0,0 +1,37 @@
+/*eslint-env node*/
+
+import { createStore, applyMiddleware, compose } from 'redux'
+import thunkMiddleware from 'redux-thunk'
+import { routerMiddleware as createRouterMiddleware } from 'react-router-redux'
+import { history } from 'utility/environment'
+import { exportState, importState } from 'utility/localStorage'
+
+import makeRootReducer from 'reducers'
+
+const routerMiddleware = createRouterMiddleware(history)
+
+export default function() {
+  const store = createStore(
+    makeRootReducer(),
+    importState(),
+    compose(
+      applyMiddleware(
+        thunkMiddleware,
+        routerMiddleware
+      ),
+      window.devToolsExtension ? window.devToolsExtension() : f => f
+    )
+  )
+
+  if (module.hot) {
+    // Enable Webpack hot module replacement for reducers
+    module.hot.accept('reducers', () => {
+      const newRootReducer = require('reducers').default
+      store.replaceReducer(newRootReducer())
+    })
+  }
+
+  store.subscribe(exportState(store))
+
+  return store
+}
diff --git a/src/features/accessControl/actions.js b/src/features/accessControl/actions.js
new file mode 100644 (file)
index 0000000..04cd179
--- /dev/null
@@ -0,0 +1,157 @@
+import React from 'react'
+import { chainClient } from 'utility/environment'
+import { actions as appActions } from 'features/app'
+import { push } from 'react-router-redux'
+import { policyOptions, subjectFieldOptions } from './constants'
+import { hasProtectedGrant } from './selectors'
+import TokenCreateModal from './components/TokenCreateModal'
+
+// Given a list of policies, create a grant for
+// all policies that are truthy, and delete any
+// outstanding grants for policies that are not.
+const setPolicies = (body, policies) => {
+  const promises = []
+
+  for (let policy in policies) {
+    if (policyOptions.find(opt => opt.value == policy).hidden) continue
+    if (hasProtectedGrant(body.grants || [], policy)) continue
+
+    const grant = {
+      guardData: body.guardData,
+      guardType: body.guardType,
+      policy
+    }
+
+    try {
+      promises.push(policies[policy] ?
+        chainClient().authorizationGrants.create(grant) :
+        chainClient().authorizationGrants.delete(grant)
+      )
+    } catch (err) {
+      promises.push(Promise.reject(err))
+    }
+  }
+
+  return Promise.all(promises)
+}
+
+export default {
+  fetchItems: () => {
+    return (dispatch) => {
+      const tokens = []
+
+      return Promise.all([
+        chainClient().authorizationGrants.list(),
+        chainClient().accessTokens.queryAll({}, (token, next) => {
+          tokens.push(token)
+          next()
+        })
+      ]).then(result => {
+        const grants = result[0].items
+        return dispatch({ type: 'RECEIVED_ACCESS_GRANTS', grants, tokens })
+      })
+    }
+  },
+
+  submitTokenForm: data => {
+    const body = {
+      guardType: 'access_token',
+      guardData: data.guardData
+    }
+
+    return dispatch => {
+      return chainClient().accessTokens.create({
+        id: body.guardData.id,
+        type: 'client', // TODO: remove me when deprecated!
+      }).then(tokenResp =>
+        setPolicies(body, data.policies).then(grantResp => {
+          dispatch(appActions.showModal(
+            <TokenCreateModal token={tokenResp.token}/>,
+            appActions.hideModal
+          ))
+
+          dispatch({ type: 'CREATED_TOKEN_WITH_GRANT', grantResp })
+
+          dispatch(push({
+            pathname: '/access-control',
+            search: '?type=token',
+            state: {preserveFlash: true},
+          }))
+        })
+      ).catch(err => { throw {_error: err} })
+    }
+  },
+
+  submitCertificateForm: data => {
+    const fieldInfo = {}
+    for (let index in subjectFieldOptions) {
+      const option = subjectFieldOptions[index]
+      fieldInfo[option.value] = option
+    }
+
+    const body = {
+      guardType: 'x509',
+      guardData: {subject: {}},
+    }
+
+    for (let index in data.subject) {
+      const field = data.subject[index]
+
+      if (field.key && fieldInfo[field.key].array) {
+        const values = body.guardData[field.key] || []
+        values.push(field.value)
+        body.guardData.subject[field.key] = values
+      } else {
+        body.guardData.subject[field.key] = field.value
+      }
+    }
+
+    return dispatch => {
+      if (!Object.values(data.policies).some(policy => policy == true)) {
+        return Promise.reject({_error: 'You must specify one or more policies'})
+      }
+
+      return setPolicies(body, data.policies).then(resp => {
+        dispatch({ type: 'CREATED_X509_GRANT', resp })
+        dispatch(push({
+          pathname: '/access-control',
+          search: '?type=certificate',
+          state: {preserveFlash: true},
+        }))
+      }, err => { throw {_error: err} })
+    }
+  },
+
+  beginEditing: id => ({
+    type: 'BEGIN_POLICY_EDITING',
+    id: id
+  }),
+
+  editPolicies: data => {
+    const body = data.grant
+    const policies = data.policies
+
+    return dispatch =>
+      setPolicies(body, policies).then(() => {
+        dispatch({ type: 'END_POLICY_EDITING', id: data.grant.id, policies })
+      }, err => { throw {_error: err} })
+  },
+
+  deleteToken: grant => {
+    const id = grant.guardData.id
+    if (!window.confirm(`Really delete access token "${id}"?`)) {
+      return
+    }
+
+    return dispatch => chainClient().accessTokens.delete(id)
+      .then(() => {
+        dispatch({
+          type: 'DELETE_ACCESS_TOKEN',
+          id: grant.id,
+          message: 'Token deleted.'
+        })
+      }).catch(err => dispatch({
+        type: 'ERROR', payload: err
+      }))
+  }
+}
diff --git a/src/features/accessControl/components/AccessControlList.jsx b/src/features/accessControl/components/AccessControlList.jsx
new file mode 100644 (file)
index 0000000..22809be
--- /dev/null
@@ -0,0 +1,92 @@
+import React from 'react'
+import GrantListItem from './GrantListItem'
+import { connect } from 'react-redux'
+import { TableList, PageTitle, PageContent } from 'features/shared/components'
+import { push, replace } from 'react-router-redux'
+import actions from 'features/accessControl/actions'
+import styles from './AccessControlList.scss'
+
+class AccessControlList extends React.Component {
+  render() {
+    const itemProps = {
+      beginEditing: this.props.beginEditing,
+      delete: this.props.delete,
+    }
+    const tokenList = <TableList titles={['Token Name', 'Policies']}>
+      {this.props.tokens.map(item => <GrantListItem key={item.id} item={item} {...itemProps} />)}
+    </TableList>
+
+    const certList = <TableList titles={['Certificate', 'Policies']}>
+      {this.props.certs.map(item => <GrantListItem key={item.id} item={item} {...itemProps} />)}
+    </TableList>
+
+    return (<div>
+      <PageTitle title='Access control' />
+
+      <PageContent>
+        <div className={`btn-group ${styles.btnGroup}`} role='group'>
+          <button
+            className={`btn btn-default ${styles.btn} ${this.props.tokensButtonStyle}`}
+            onClick={this.props.showTokens}>
+              Tokens
+          </button>
+
+          <button
+            className={`btn btn-default ${styles.btn} ${this.props.certificatesButtonStyle}`}
+            onClick={this.props.showCertificates}>
+              Certificates
+          </button>
+        </div>
+
+        {this.props.tokensSelected && <div>
+          <button
+            className={`btn btn-primary ${styles.newBtn}`}
+            onClick={this.props.showTokenCreate}>
+              + New token
+          </button>
+
+          {tokenList}
+        </div>}
+
+        {this.props.certificatesSelected && <div>
+          <button
+            className={`btn btn-primary ${styles.newBtn}`}
+            onClick={this.props.showAddCertificate}>
+              + Register certificate
+          </button>
+
+          {certList}
+        </div>}
+      </PageContent>
+    </div>)
+  }
+}
+
+const mapStateToProps = (state, ownProps) => {
+  const items = state.accessControl.ids.map(id => state.accessControl.items[id])
+  const tokensSelected = ownProps.location.query.type == 'token'
+  const certificatesSelected = ownProps.location.query.type != 'token'
+
+  return {
+    tokens: items.filter(item => item.guardType == 'access_token'),
+    certs: items.filter(item => item.guardType == 'x509'),
+    tokensSelected,
+    certificatesSelected,
+    tokensButtonStyle: tokensSelected && styles.active,
+    certificatesButtonStyle: certificatesSelected && styles.active,
+  }
+}
+
+const mapDispatchToProps = (dispatch) => ({
+  delete: (grant) => dispatch(actions.deleteToken(grant)),
+  showTokens: () => dispatch(replace('/access-control?type=token')),
+  showCertificates: () => dispatch(replace('/access-control?type=certificate')),
+  showTokenCreate: () => dispatch(push('/access-control/create-token')),
+  showAddCertificate: () => dispatch(push('/access-control/add-certificate')),
+  beginEditing: (id) => dispatch(actions.beginEditing(id)),
+})
+
+export default connect(
+  mapStateToProps,
+  mapDispatchToProps
+)(AccessControlList)
diff --git a/src/features/accessControl/components/AccessControlList.scss b/src/features/accessControl/components/AccessControlList.scss
new file mode 100644 (file)
index 0000000..26a921e
--- /dev/null
@@ -0,0 +1,36 @@
+.newBtn {
+  position: absolute;
+  right: $gutter-size;
+  top: $gutter-size;
+  vertical-align: baseline;
+}
+
+.btnGroup {
+  margin-bottom: $gutter-size;
+}
+
+.btn {
+  color: $highlight-default;
+
+  &:hover {
+    text-decoration: underline;
+  }
+
+  &:hover, &:active, &:hover:active, &:active:focus, &:focus {
+    color: $highlight-default;
+    background: $background-color;
+  }
+
+  &:focus {
+    background: white;
+  }
+}
+
+.active, .active:focus, .active:hover, .active:active:focus {
+  box-shadow: inset 0 2px 3px rgba(0, 0, 0, 0.15);
+  border-color: $border-strong-color;
+  background: $background-emphasis-color;
+  color: $text-color;
+  cursor: default;
+  text-decoration: none;
+}
diff --git a/src/features/accessControl/components/EditPolicies.jsx b/src/features/accessControl/components/EditPolicies.jsx
new file mode 100644 (file)
index 0000000..b792707
--- /dev/null
@@ -0,0 +1,66 @@
+import React from 'react'
+import { BaseNew, CheckboxField } from 'features/shared/components'
+import { policyOptions } from 'features/accessControl/constants'
+import { hasProtectedGrant } from 'features/accessControl/selectors'
+import { reduxForm } from 'redux-form'
+import actions from 'features/accessControl/actions'
+import styles from './EditPolicies.scss'
+
+class EditPolicies extends React.Component {
+  render() {
+    const {
+      fields: { policies },
+      handleSubmit,
+    } = this.props
+
+    return(
+      <div className={styles.main}>
+        {policyOptions.map(option => {
+          if (option.hidden) return
+
+          const isProtected = hasProtectedGrant(this.props.item.grants, option.value)
+          return <CheckboxField key={option.label}
+            title={option.label}
+            hint={option.hint}
+            fieldProps={{
+              ...policies[option.value],
+              disabled: isProtected,
+            }} />
+        })}
+
+        <button className='btn btn-primary' onClick={handleSubmit(this.props.submitForm)}>Save</button>
+      </div>
+    )
+  }
+}
+
+const mapDispatchToProps = (dispatch) => ({
+  submitForm: (data) => dispatch(actions.editPolicies(data))
+})
+
+const initialValues = (state, ownProps) => {
+  const item = ownProps.item
+  if (!item) { return {} }
+
+  const fields = {
+    initialValues: {
+      grant: item,
+      policies: policyOptions.reduce((memo, p) => {
+        const policyIndex = item.grants.findIndex(grant => grant.policy == p.value)
+        memo[p.value] = policyIndex >= 0
+        return memo
+      }, {}),
+    }
+  }
+
+  return fields
+}
+
+export default BaseNew.connect(
+  () => ({}),
+  mapDispatchToProps,
+  reduxForm({
+    form: 'editPoliciesForm',
+    fields: ['grant'].concat(policyOptions.map(p => `policies.${p.value}`)),
+  }, initialValues)(EditPolicies)
+)
diff --git a/src/features/accessControl/components/EditPolicies.scss b/src/features/accessControl/components/EditPolicies.scss
new file mode 100644 (file)
index 0000000..5945a8a
--- /dev/null
@@ -0,0 +1,10 @@
+.main {
+  position: relative;
+  text-align: left;
+
+  button {
+    position: absolute;
+    right: 0;
+    top: 0;
+  }
+}
diff --git a/src/features/accessControl/components/GrantListItem.jsx b/src/features/accessControl/components/GrantListItem.jsx
new file mode 100644 (file)
index 0000000..7d0b61a
--- /dev/null
@@ -0,0 +1,58 @@
+import React from 'react'
+import PropTypes from 'prop-types'
+import { isAccessToken, getPolicyNames } from 'features/accessControl/selectors'
+import EditPolicies from './EditPolicies'
+import { isArray } from 'lodash'
+
+class GrantListItem extends React.Component {
+  render() {
+    const item = this.props.item
+
+    let desc
+    if (isAccessToken(item)) {
+      desc = item.guardData.id
+    } else { // x509
+      const subject = item.guardData.subject
+      desc = <div>
+        {Object.keys(subject).map(field =>
+          <div key={`${item.id}-${field}`}>
+            {field.toUpperCase()}:
+            {' '}
+            {isArray(subject[field])
+              ? subject[field].join(', ')
+              : subject[field]}
+          </div>
+        )}
+      </div>
+    }
+    return(
+      <tr>
+        <td>{desc}</td>
+        {!item.isEditing && <td>
+          {getPolicyNames(item).map(name =>
+            <span key={`${item.id}-${name}`}>{name}<br /></span>
+          )}
+        </td>}
+        {!item.isEditing && <td>
+          <button className='btn btn-link' onClick={this.props.beginEditing.bind(this, item.id)}>
+            Edit
+          </button>
+
+          {isAccessToken(item) && <button className='btn btn-link' onClick={this.props.delete.bind(this, item)}>
+            Delete
+          </button>}
+        </td>}
+        {item.isEditing && <td colSpan='2'>
+          <EditPolicies item={item}/>
+        </td>}
+      </tr>
+    )
+  }
+}
+
+GrantListItem.propTypes = {
+  item: PropTypes.object,
+  delete: PropTypes.func,
+}
+
+export default GrantListItem
diff --git a/src/features/accessControl/components/NewCertificate.jsx b/src/features/accessControl/components/NewCertificate.jsx
new file mode 100644 (file)
index 0000000..b3aa637
--- /dev/null
@@ -0,0 +1,84 @@
+import React from 'react'
+import { BaseNew, FormContainer, FormSection, SelectField, TextField, CheckboxField } from 'features/shared/components'
+import { policyOptions, subjectFieldOptions } from 'features/accessControl/constants'
+import { reduxForm } from 'redux-form'
+import actions from 'features/accessControl/actions'
+import styles from './NewCertificate.scss'
+
+class NewCertificate extends React.Component {
+  constructor(props) {
+    super(props)
+    this.props.fields.subject.addField()
+  }
+
+  render() {
+    const {
+      fields: { subject, policies },
+      error,
+      handleSubmit,
+      submitting
+    } = this.props
+
+    return(
+      <FormContainer
+        error={error}
+        label='Add certificate grant'
+        onSubmit={handleSubmit(this.props.submitForm)}
+        submitting={submitting} >
+
+        <FormSection title='Certificate subject'>
+          {subject.map((line, index) =>
+            <div key={index} className={styles.subjectField}>
+              <SelectField title='Field Name' options={subjectFieldOptions} fieldProps={line.key} />
+              <TextField title='Field Value' fieldProps={line.value} />
+              <button
+                className='btn btn-danger btn-xs'
+                tabIndex='-1'
+                type='button'
+                onClick={() => subject.removeField(index)}
+              >
+                Remove
+              </button>
+            </div>
+          )}
+          <button
+            type='button'
+            className='btn btn-default'
+            onClick={() => subject.addField()}
+          >
+            Add Field
+          </button>
+        </FormSection>
+        <FormSection title='Policy'>
+          {policyOptions.map(option => {
+            if (option.hidden) return
+
+            return <CheckboxField key={option.label}
+              title={option.label}
+              hint={option.hint}
+              fieldProps={policies[option.value]} />
+          })}
+        </FormSection>
+      </FormContainer>
+    )
+  }
+}
+
+const mapDispatchToProps = (dispatch) => ({
+  submitForm: (data) => dispatch(actions.submitCertificateForm(data))
+})
+
+export default BaseNew.connect(
+  BaseNew.mapStateToProps('accessControl'),
+  mapDispatchToProps,
+  reduxForm({
+    form: 'newAccessGrantForm',
+    fields: [
+      'guardType',
+      'subject[].key',
+      'subject[].value'
+    ].concat(
+      policyOptions.map(p => `policies.${p.value}`)
+    ),
+  })(NewCertificate)
+)
diff --git a/src/features/accessControl/components/NewCertificate.scss b/src/features/accessControl/components/NewCertificate.scss
new file mode 100644 (file)
index 0000000..1d27c82
--- /dev/null
@@ -0,0 +1,11 @@
+.subjectField {
+  display: flex;
+  align-items: center;
+
+  :global {
+    .form-group {
+      flex: 1;
+      margin-right: $gutter-size;
+    }
+  }
+}
diff --git a/src/features/accessControl/components/NewToken.jsx b/src/features/accessControl/components/NewToken.jsx
new file mode 100644 (file)
index 0000000..d308c57
--- /dev/null
@@ -0,0 +1,67 @@
+import React from 'react'
+import { BaseNew, FormContainer, FormSection, TextField, CheckboxField } from 'features/shared/components'
+import { policyOptions } from 'features/accessControl/constants'
+import { reduxForm } from 'redux-form'
+import actions from 'features/accessControl/actions'
+
+class NewToken extends React.Component {
+  render() {
+    const {
+      fields: { guardData, policies },
+      error,
+      handleSubmit,
+      submitting
+    } = this.props
+
+    return(
+      <FormContainer
+        error={error}
+        label='New access token'
+        onSubmit={handleSubmit(this.props.submitForm)}
+        submitting={submitting} >
+
+        <FormSection title='Token information'>
+          <TextField title='Token Name' fieldProps={guardData.id} autoFocus={true} />
+        </FormSection>
+        <FormSection title='Policy'>
+          {policyOptions.map(option => {
+            if (option.hidden) return
+
+            return <CheckboxField key={option.label}
+              title={option.label}
+              hint={option.hint}
+              fieldProps={policies[option.value]} />
+          })}
+        </FormSection>
+
+      </FormContainer>
+    )
+  }
+}
+
+const mapDispatchToProps = (dispatch) => ({
+  submitForm: (data) => dispatch(actions.submitTokenForm(data))
+})
+
+export default BaseNew.connect(
+  BaseNew.mapStateToProps('accessControl'),
+  mapDispatchToProps,
+  reduxForm({
+    form: 'newAccessGrantForm',
+    fields: [
+      'guardType',
+      'guardData.id',
+    ].concat(
+      policyOptions.map(p => `policies.${p.value}`)
+    ),
+    validate: values => {
+      const errors = {}
+
+      if (!values.guardData.id) {
+        errors.guardData = {id: 'Token name is required'}
+      }
+
+      return errors
+    }
+  })(NewToken)
+)
diff --git a/src/features/accessControl/components/TokenCreateModal.jsx b/src/features/accessControl/components/TokenCreateModal.jsx
new file mode 100644 (file)
index 0000000..789b1ba
--- /dev/null
@@ -0,0 +1,13 @@
+import React from 'react'
+import { CopyableBlock } from 'features/shared/components'
+
+export default class CreateModal extends React.Component {
+  render() {
+    return <div>
+      <h4>Created new access token</h4>
+      <p>Please store this token carefully. This is the last time it will be displayed.</p>
+
+      <CopyableBlock value={this.props.token} />
+    </div>
+  }
+}
diff --git a/src/features/accessControl/constants.js b/src/features/accessControl/constants.js
new file mode 100644 (file)
index 0000000..a700c7b
--- /dev/null
@@ -0,0 +1,44 @@
+export const policyOptions = [
+  {
+    label: 'Client read/write',
+    value: 'client-readwrite',
+    hint: 'Full access to the Client API'
+  },
+  {
+    label: 'Client read-only',
+    value: 'client-readonly',
+    hint: 'Access to read-only Client endpoints'
+  },
+  {
+    label: 'Monitoring',
+    value: 'monitoring',
+    hint: 'Access to monitoring-specific endpoints'
+  },
+  {
+    label: 'Cross-core',
+    value: 'crosscore',
+    hint: 'Access to the cross-core API, not including block-signing. Necessary for connecting to the generator'
+  },
+  {
+    label: 'Cross-core block signing',
+    value: 'crosscore-signblock',
+    hint: 'Access to the cross-core API\'s block-signing functionality'
+  },
+  {
+    label: 'Internal',
+    value: 'internal',
+    hidden: true,
+  },
+]
+
+export const subjectFieldOptions = [
+  {label: 'CommonName', value: 'cn'},
+  {label: 'Country', value: 'c', array: true},
+  {label: 'Organization', value: 'o', array: true},
+  {label: 'OrganizationalUnit', value: 'ou', array: true},
+  {label: 'Locality', value: 'l', array: true},
+  {label: 'Province', value: 'st', array: true},
+  {label: 'StreetAddress', value: 'street', array: true},
+  {label: 'PostalCode', value: 'postalcode', array: true},
+  {label: 'SerialNumber', value: 'serialnumber'},
+]
diff --git a/src/features/accessControl/reducers.js b/src/features/accessControl/reducers.js
new file mode 100644 (file)
index 0000000..148e2cd
--- /dev/null
@@ -0,0 +1,106 @@
+import createHash from 'sha.js'
+
+export default (state = {ids: [], items: {}}, action) => {
+  // Grant list is always complete, so we rebuild state from scratch
+  if (action.type == 'RECEIVED_ACCESS_GRANTS') {
+    const newObjects = {}
+
+    action.tokens.forEach(token => {
+      const tokenGuard = {
+        id: token.id
+      }
+      const id = createHash('sha256').update(JSON.stringify(tokenGuard), 'utf8').digest('hex')
+      newObjects[id] = {
+        id: id,
+        name: token.id,
+        guardType: 'access_token',
+        guardData: tokenGuard,
+        grants: [],
+        createdAt: token.createdAt
+      }
+    })
+
+    action.grants.forEach(grant => {
+      const id = createHash('sha256').update(JSON.stringify(grant.guardData), 'utf8').digest('hex')
+
+      if (newObjects[id]) {
+        const existingIndex = newObjects[id].grants.findIndex(g => g.policy == grant.policy)
+        if (existingIndex >= 0) {
+          const existing = newObjects[id].grants[existingIndex]
+          if (existing.protected) { return }
+          if (grant.protected) { newObjects[id].grants.splice(existingIndex, 1) }
+        }
+
+        newObjects[id].grants.push(grant)
+        if (newObjects[id].createdAt.localeCompare(grant.createdAt) > 0) {
+          newObjects[id].createdAt = grant.createdAt
+        }
+      } else {
+        newObjects[id] = {
+          id: id,
+          guardType: grant.guardType,
+          guardData: grant.guardData,
+          grants: [grant],
+          createdAt: grant.createdAt
+        }
+      }
+    })
+
+    const newIds = Object.values(newObjects)
+      .sort((a, b) => b.createdAt.localeCompare(a.createdAt))
+      .map(object => object.id)
+
+    return {
+      ids: newIds,
+      items: newObjects
+    }
+  } else if (action.type == 'BEGIN_POLICY_EDITING') {
+    const id = action.id
+    const item = {...state.items[id]}
+    item.isEditing = true
+
+    return {
+      ids: state.ids,
+      items: {
+        ...state.items,
+        [id]: item
+      }
+    }
+  } else if (action.type == 'END_POLICY_EDITING') {
+    const id = action.id
+    const item = {...state.items[id]}
+    item.isEditing = false
+    if (action.policies) {
+      item.grants = Object.keys(action.policies)
+        .filter(policy => action.policies[policy])
+        .map(policy => ({
+          ...item.grants.find(grant => grant.policy == policy),
+          policy: policy
+        }))
+    }
+
+    return {
+      ids: state.ids,
+      items: {
+        ...state.items,
+        [id]: item
+      }
+    }
+  } else if (action.type == 'DELETE_ACCESS_TOKEN') {
+    const ids = [...state.ids]
+    const items = {...state.items}
+
+    const idToRemove = action.id
+    const deleteIndex = ids.indexOf(idToRemove)
+    ids.splice(deleteIndex, 1)
+
+    delete items[idToRemove]
+
+    return {
+      ids,
+      items
+    }
+  }
+
+  return state
+}
diff --git a/src/features/accessControl/routes.js b/src/features/accessControl/routes.js
new file mode 100644 (file)
index 0000000..8bad06c
--- /dev/null
@@ -0,0 +1,46 @@
+import AccessControlList from './components/AccessControlList'
+import NewToken from './components/NewToken'
+import NewCertificate from './components/NewCertificate'
+import { makeRoutes } from 'features/shared'
+import actions from './actions'
+
+const checkParams = (nextState, replace) => {
+  if (!['token', 'certificate'].includes(nextState.location.query.type)) {
+    replace({
+      pathname: '/access-control',
+      search: '?type=token',
+      state: {preserveFlash: true}
+    })
+    return false
+  }
+  return true
+}
+
+export default (store) => {
+  const loadGrants = () => store.dispatch(actions.fetchItems())
+
+  const routes = makeRoutes(store, 'accessControl', AccessControlList, null, null, null, {
+    path: 'access-control',
+    name: 'Access control'
+  })
+
+  routes.indexRoute.onEnter = (nextState, replace) => {
+    if (checkParams(nextState, replace)) { loadGrants() }
+  }
+
+  routes.indexRoute.onChange = (_, nextState, replace) => {
+    checkParams(nextState, replace)
+  }
+
+  routes.childRoutes.push({
+    path: 'create-token',
+    component: NewToken
+  })
+
+  routes.childRoutes.push({
+    path: 'add-certificate',
+    component: NewCertificate
+  })
+
+  return routes
+}
diff --git a/src/features/accessControl/selectors.js b/src/features/accessControl/selectors.js
new file mode 100644 (file)
index 0000000..ac84d8f
--- /dev/null
@@ -0,0 +1,29 @@
+import { createSelector } from 'reselect'
+import { policyOptions } from './constants'
+
+export const getPolicyNames = createSelector(
+  item => item.grants,
+  grants => grants.map(
+    grant => {
+      const isProtected = grant.protected
+      const policy = grant.policy
+
+      const found = policyOptions.find(elem => elem.value == policy)
+      let label = found ? found.label : policy
+      if (isProtected) {
+        label = label + ' (Protected)'
+      }
+      return label
+    }
+  )
+)
+
+export const guardType = (item) => item.guardType
+
+export const isAccessToken = createSelector(
+  guardType,
+  type => type == 'access_token'
+)
+
+export const hasProtectedGrant = (grants, policy) =>
+  grants.find(grant => grant.protected && grant.policy == policy) != undefined
diff --git a/src/features/accounts/actions.js b/src/features/accounts/actions.js
new file mode 100644 (file)
index 0000000..f7a4b2d
--- /dev/null
@@ -0,0 +1,25 @@
+import { chainClient } from 'utility/environment'
+import { baseCreateActions, baseUpdateActions, baseListActions } from 'features/shared/actions'
+
+const type = 'account'
+
+const list = baseListActions(type, { defaultKey: 'alias' })
+const create = baseCreateActions(type, {
+  jsonFields: ['tags'],
+  intFields: ['quorum'],
+  redirectToShow: true,
+})
+const update = baseUpdateActions(type, {
+  jsonFields: ['tags']
+})
+
+let actions = {
+  ...list,
+  ...create,
+  ...update,
+  createReceiver: (data) => () => {
+    return chainClient().accounts.createReceiver(data)
+  }
+}
+
+export default actions
diff --git a/src/features/accounts/components/AccountShow.jsx b/src/features/accounts/components/AccountShow.jsx
new file mode 100644 (file)
index 0000000..8d2a472
--- /dev/null
@@ -0,0 +1,122 @@
+import React from 'react'
+import {
+  BaseShow,
+  CopyableBlock,
+  KeyValueTable,
+  PageContent,
+  PageTitle,
+  RawJsonButton,
+} from 'features/shared/components'
+import componentClassNames from 'utility/componentClassNames'
+
+class AccountShow extends BaseShow {
+  constructor(props) {
+    super(props)
+
+    this.createReceiver = this.createReceiver.bind(this)
+  }
+
+  createReceiver() {
+    this.props.createReceiver({
+      accountId: this.props.item.id
+    }).then((receiver) => this.props.showReceiver(<div>
+      <p>Copy this one-time use receiver to use in a transaction:</p>
+      <CopyableBlock value={JSON.stringify(receiver, null, 1)} />
+    </div>))
+  }
+
+  render() {
+    const item = this.props.item
+
+    let view
+    if (item) {
+      const title = <span>
+        {'Account '}
+        <code>{item.alias ? item.alias : item.id}</code>
+      </span>
+
+      view = <div className={componentClassNames(this)}>
+        <PageTitle
+          title={title}
+          actions={[
+            <button className='btn btn-link' onClick={this.createReceiver}>
+              Create receiver
+            </button>
+          ]}
+        />
+
+        <PageContent>
+          <KeyValueTable
+            id={item.id}
+            object='account'
+            title='Details'
+            actions={[
+              <button key='show-txs' className='btn btn-link' onClick={this.props.showTransactions.bind(this, item)}>Transactions</button>,
+              <button key='show-balances' className='btn btn-link' onClick={this.props.showBalances.bind(this, item)}>Balances</button>,
+              <RawJsonButton key='raw-json' item={item} />
+            ]}
+            items={[
+              {label: 'ID', value: item.id},
+              {label: 'Alias', value: item.alias},
+              {label: 'Tags', value: item.tags, editUrl: `/accounts/${item.id}/tags`},
+              {label: 'Keys', value: item.keys.length},
+              {label: 'Quorum', value: item.quorum},
+            ]}
+          />
+
+          {item.keys.map((key, index) =>
+            <KeyValueTable
+              key={index}
+              title={`Key ${index + 1}`}
+              items={[
+                {label: 'Root Xpub', value: key.rootXpub},
+                {label: 'Account Xpub', value: key.accountXpub},
+                {label: 'Account Derivation Path', value: key.accountDerivationPath},
+              ]}
+            />
+          )}
+        </PageContent>
+      </div>
+    }
+    return this.renderIfFound(view)
+  }
+}
+
+// Container
+
+import { connect } from 'react-redux'
+import actions from 'actions'
+
+const mapStateToProps = (state, ownProps) => ({
+  item: state.account.items[ownProps.params.id]
+})
+
+const mapDispatchToProps = ( dispatch ) => ({
+  fetchItem: (id) => dispatch(actions.account.fetchItems({filter: `id='${id}'`})),
+  showTransactions: (item) => {
+    let filter = `inputs(account_id='${item.id}') OR outputs(account_id='${item.id}')`
+    if (item.alias) {
+      filter = `inputs(account_alias='${item.alias}') OR outputs(account_alias='${item.alias}')`
+    }
+
+    dispatch(actions.transaction.pushList({ filter }))
+  },
+  showBalances: (item) => {
+    let filter = `account_id='${item.id}'`
+    if (item.alias) {
+      filter = `account_alias='${item.alias}'`
+    }
+
+    dispatch(actions.balance.pushList({ filter }))
+  },
+  createReceiver: (data) => dispatch(actions.account.createReceiver(data)),
+  showReceiver: (body) => dispatch(actions.app.showModal(
+    body,
+    actions.app.hideModal
+  ))
+})
+
+export default connect(
+  mapStateToProps,
+  mapDispatchToProps
+)(AccountShow)
diff --git a/src/features/accounts/components/AccountUpdate.jsx b/src/features/accounts/components/AccountUpdate.jsx
new file mode 100644 (file)
index 0000000..5e6256f
--- /dev/null
@@ -0,0 +1,120 @@
+import React from 'react'
+import { BaseUpdate, FormContainer, FormSection, JsonField, NotFound } from 'features/shared/components'
+import { reduxForm } from 'redux-form'
+import { docsRoot } from 'utility/environment'
+
+class Form extends React.Component {
+  constructor(props) {
+    super(props)
+
+    this.submitWithErrors = this.submitWithErrors.bind(this)
+
+    this.state = {}
+  }
+
+  submitWithErrors(data) {
+    return this.props.submitForm(data, this.props.item.id).catch(err => {
+      throw {_error: err}
+    })
+  }
+
+  componentDidMount() {
+    this.props.fetchItem(this.props.params.id).then(resp => {
+      if (resp.items.length == 0) {
+        this.setState({notFound: true})
+      }
+    })
+  }
+
+  render() {
+    if (this.state.notFound) {
+      return <NotFound />
+    }
+    const item = this.props.item
+
+    if (!item) {
+      return <div>Loading...</div>
+    }
+
+    const {
+      fields: { tags },
+      error,
+      handleSubmit,
+      submitting
+    } = this.props
+
+    const title = <span>
+      {'Edit account tags '}
+      <code>{item.alias ? item.alias :item.id}</code>
+    </span>
+
+    const tagsString = Object.keys(item.tags).length === 0 ? '{\n\t\n}' : JSON.stringify(item.tags, null, 1)
+    const tagLines = tagsString.split(/\r\n|\r|\n/).length
+    let JsonFieldHeight
+
+    if (tagLines < 5) {
+      JsonFieldHeight = '80px'
+    } else if (tagLines < 20) {
+      JsonFieldHeight = `${tagLines * 17}px`
+    } else {
+      JsonFieldHeight = '340px'
+    }
+
+    return <FormContainer
+      error={error}
+      label={title}
+      onSubmit={handleSubmit(this.submitWithErrors)}
+      submitting={submitting} >
+
+      <FormSection title='Account Tags'>
+        <JsonField
+          height={JsonFieldHeight}
+          fieldProps={tags} />
+
+        <p>
+          Note: Account tags can be used for querying transactions, unspent outputs, and balances. Queries reflect the account tags that are present when transactions are submitted. Only new transaction activity will reflect the updated tags. <a href={`${docsRoot}/core/build-applications/accounts#update-tags-on-existing-accounts`} target='_blank' style={{whiteSpace: 'nowrap'}}>
+            Learn more →</a>
+        </p>
+      </FormSection>
+    </FormContainer>
+  }
+}
+
+const mapStateToProps = (state, ownProps) => ({
+  item: state.account.items[ownProps.params.id]
+})
+
+const initialValues = (state, ownProps) => {
+  const item = state.account.items[ownProps.params.id]
+  if (item) {
+    const tags = Object.keys(item.tags).length === 0 ? '{\n\t\n}' : JSON.stringify(item.tags, null, 1)
+    return {
+      initialValues: {
+        tags: tags
+      }
+    }
+  }
+  return {}
+}
+
+const updateForm = reduxForm({
+  form: 'updateAccountForm',
+  fields: ['tags'],
+  validate: values => {
+    const errors = {}
+
+    const jsonFields = ['tags']
+    jsonFields.forEach(key => {
+      const fieldError = JsonField.validator(values[key])
+      if (fieldError) { errors[key] = fieldError }
+    })
+
+    return errors
+  }
+}, initialValues)(Form)
+
+export default BaseUpdate.connect(
+  mapStateToProps,
+  BaseUpdate.mapDispatchToProps('account'),
+  updateForm
+)
diff --git a/src/features/accounts/components/List.jsx b/src/features/accounts/components/List.jsx
new file mode 100644 (file)
index 0000000..c605708
--- /dev/null
@@ -0,0 +1,15 @@
+import React from 'react'
+import { BaseList, TableList } from 'features/shared/components'
+import ListItem from './ListItem'
+
+const type = 'account'
+
+export default BaseList.connect(
+  BaseList.mapStateToProps(type, ListItem, {
+    wrapperComponent: TableList,
+    wrapperProps: {
+      titles: ['Account Alias', 'Account ID']
+    }
+  }),
+  BaseList.mapDispatchToProps(type)
+)
diff --git a/src/features/accounts/components/ListItem.jsx b/src/features/accounts/components/ListItem.jsx
new file mode 100644 (file)
index 0000000..a774735
--- /dev/null
@@ -0,0 +1,22 @@
+import React from 'react'
+import { Link } from 'react-router'
+
+class ListItem extends React.Component {
+  render() {
+    const item = this.props.item
+
+    return(
+      <tr>
+        <td>{item.alias || '-'}</td>
+        <td><code>{item.id}</code></td>
+        <td>
+          <Link to={`/accounts/${item.id}`}>
+            View details →
+          </Link>
+        </td>
+      </tr>
+    )
+  }
+}
+
+export default ListItem
diff --git a/src/features/accounts/components/New.jsx b/src/features/accounts/components/New.jsx
new file mode 100644 (file)
index 0000000..157c148
--- /dev/null
@@ -0,0 +1,79 @@
+import React from 'react'
+import { BaseNew, FormContainer, FormSection, JsonField, KeyConfiguration, TextField } from 'features/shared/components'
+import { reduxForm } from 'redux-form'
+
+class Form extends React.Component {
+  constructor(props) {
+    super(props)
+
+    this.submitWithErrors = this.submitWithErrors.bind(this)
+  }
+
+  submitWithErrors(data) {
+    return new Promise((resolve, reject) => {
+      this.props.submitForm(data)
+        .catch((err) => reject({_error: err}))
+    })
+  }
+
+  render() {
+    const {
+      fields: { alias, tags, xpubs, quorum },
+      error,
+      handleSubmit,
+      submitting
+    } = this.props
+
+    return(
+      <FormContainer
+        error={error}
+        label='New account'
+        onSubmit={handleSubmit(this.submitWithErrors)}
+        submitting={submitting} >
+
+        <FormSection title='Account Information'>
+          <TextField title='Alias' placeholder='Alias' fieldProps={alias} autoFocus={true} />
+          <JsonField title='Tags' fieldProps={tags} />
+        </FormSection>
+
+        <FormSection title='Keys and Signing'>
+          <KeyConfiguration
+            xpubs={xpubs}
+            quorum={quorum}
+            quorumHint='Number of keys required for transfer' />
+        </FormSection>
+      </FormContainer>
+    )
+  }
+}
+
+const validate = values => {
+  const errors = {}
+
+  const tagError = JsonField.validator(values.tags)
+  if (tagError) { errors.tags = tagError }
+
+  return errors
+}
+
+const fields = [
+  'alias',
+  'tags',
+  'xpubs[].value',
+  'xpubs[].type',
+  'quorum'
+]
+
+export default BaseNew.connect(
+  BaseNew.mapStateToProps('account'),
+  BaseNew.mapDispatchToProps('account'),
+  reduxForm({
+    form: 'newAccountForm',
+    fields,
+    validate,
+    initialValues: {
+      tags: '{\n\t\n}',
+      quorum: 1,
+    }
+  })(Form)
+)
diff --git a/src/features/accounts/components/index.js b/src/features/accounts/components/index.js
new file mode 100644 (file)
index 0000000..3038b08
--- /dev/null
@@ -0,0 +1,11 @@
+import List from './List'
+import New from './New'
+import AccountShow from './AccountShow'
+import AccountUpdate from './AccountUpdate'
+
+export {
+  List,
+  New,
+  AccountShow,
+  AccountUpdate
+}
diff --git a/src/features/accounts/index.js b/src/features/accounts/index.js
new file mode 100644 (file)
index 0000000..f4fbe4d
--- /dev/null
@@ -0,0 +1,9 @@
+import actions from './actions'
+import reducers from './reducers'
+import routes from './routes'
+
+export {
+  actions,
+  reducers,
+  routes
+}
diff --git a/src/features/accounts/reducers.js b/src/features/accounts/reducers.js
new file mode 100644 (file)
index 0000000..2f1cb94
--- /dev/null
@@ -0,0 +1,10 @@
+import { reducers } from 'features/shared'
+import { combineReducers } from 'redux'
+
+const type = 'account'
+
+export default combineReducers({
+  items: reducers.itemsReducer(type),
+  queries: reducers.queriesReducer(type),
+  autocompleteIsLoaded: reducers.autocompleteIsLoadedReducer(type),
+})
diff --git a/src/features/accounts/routes.js b/src/features/accounts/routes.js
new file mode 100644 (file)
index 0000000..6574a3c
--- /dev/null
@@ -0,0 +1,4 @@
+import { List, New, AccountShow, AccountUpdate } from './components'
+import { makeRoutes } from 'features/shared'
+
+export default (store) => makeRoutes(store, 'account', List, New, AccountShow, AccountUpdate)
diff --git a/src/features/app/actions.js b/src/features/app/actions.js
new file mode 100644 (file)
index 0000000..f87ff7e
--- /dev/null
@@ -0,0 +1,28 @@
+import { push } from 'react-router-redux'
+
+const actions = {
+  dismissFlash: (param) => ({type: 'DISMISS_FLASH', param}),
+  displayedFlash: (param) => ({type: 'DISPLAYED_FLASH', param}),
+  showModal: (body, accept, cancel, options = {}) => ({type: 'SHOW_MODAL', payload: { body, accept, cancel, options }}),
+  hideModal: { type: 'HIDE_MODAL' },
+  showRoot: push('/transactions'),
+  toggleDropdown: { type: 'TOGGLE_DROPDOWN' },
+  closeDropdown: (dispatch, getState) => {
+    if (getState().app.dropdownState === 'open') {
+      dispatch({ type: 'CLOSE_DROPDOWN' })
+    }
+  },
+  showConfiguration: () => {
+    return (dispatch, getState) => {
+      // Need a default here, since locationBeforeTransitions gets cleared
+      // during logout.
+      let pathname = (getState().routing.locationBeforeTransitions || {}).pathname
+
+      if (pathname !== 'configuration') {
+        dispatch(push('/configuration'))
+      }
+    }
+  }
+}
+
+export default actions
diff --git a/src/features/app/components/Config/Config.jsx b/src/features/app/components/Config/Config.jsx
new file mode 100644 (file)
index 0000000..854c9a0
--- /dev/null
@@ -0,0 +1,15 @@
+import React from 'react'
+
+class Config extends React.Component {
+  render() {
+    return (
+      <div>
+        <div className='container'>
+          {this.props.children}
+        </div>
+      </div>
+    )
+  }
+}
+
+export default Config
diff --git a/src/features/app/components/Container.jsx b/src/features/app/components/Container.jsx
new file mode 100644 (file)
index 0000000..150b03d
--- /dev/null
@@ -0,0 +1,101 @@
+import React from 'react'
+import { connect } from 'react-redux'
+import actions from 'actions'
+import { Main, Config, Login, Loading, Modal } from './'
+
+const CORE_POLLING_TIME = 2 * 1000
+const TESTNET_INFO_POLLING_TIME = 30 * 1000
+
+class Container extends React.Component {
+  constructor(props) {
+    super(props)
+    this.redirectRoot = this.redirectRoot.bind(this)
+  }
+
+  redirectRoot(props) {
+    const {
+      authOk,
+      configKnown,
+      configured,
+      location
+    } = props
+
+    if (!authOk || !configKnown) {
+      return
+    }
+
+    if (configured) {
+      if (location.pathname === '/' ||
+          location.pathname.indexOf('configuration') >= 0) {
+        this.props.showRoot()
+      }
+    } else {
+      this.props.showConfiguration()
+    }
+  }
+
+  componentWillMount() {
+    const checkTestnet = () => {
+      if (this.props.onTestnet) this.props.fetchTestnetInfo()
+    }
+
+    this.props.fetchInfo().then(() => {
+      checkTestnet()
+      this.redirectRoot(this.props)
+    })
+
+    setInterval(() => this.props.fetchInfo(), CORE_POLLING_TIME)
+    setInterval(() => checkTestnet(), TESTNET_INFO_POLLING_TIME)
+  }
+
+  componentWillReceiveProps(nextProps) {
+    if (nextProps.authOk != this.props.authOk ||
+        nextProps.configKnown != this.props.configKnown ||
+        nextProps.configured != this.props.configured ||
+        nextProps.location.pathname != this.props.location.pathname) {
+      this.redirectRoot(nextProps)
+    }
+  }
+
+  render() {
+    let layout
+
+    if (!this.props.authOk) {
+      layout = <Login />
+    } else if (!this.props.configKnown) {
+      return <Loading>Connecting to Chain Core...</Loading>
+    } else if (!this.props.configured) {
+      layout = <Config>{this.props.children}</Config>
+    } else {
+      layout = <Main>{this.props.children}</Main>
+    }
+
+    return <div>
+      {layout}
+      <Modal />
+
+      {/* For copyToClipboard(). TODO: move this some place cleaner. */}
+      <input
+        id='_copyInput'
+        onChange={() => 'do nothing'}
+        value='dummy'
+        style={{display: 'none'}}
+      />
+    </div>
+  }
+}
+
+export default connect(
+  (state) => ({
+    authOk: !state.core.requireClientToken || state.core.validToken,
+    configKnown: state.core.configKnown,
+    configured: state.core.configured,
+    onTestnet: state.core.onTestnet,
+  }),
+  (dispatch) => ({
+    fetchInfo: options => dispatch(actions.core.fetchCoreInfo(options)),
+    fetchTestnetInfo: () => dispatch(actions.testnet.fetchTestnetInfo()),
+    showRoot: () => dispatch(actions.app.showRoot),
+    showConfiguration: () => dispatch(actions.app.showConfiguration()),
+  })
+)(Container)
diff --git a/src/features/app/components/Loading/Loading.jsx b/src/features/app/components/Loading/Loading.jsx
new file mode 100644 (file)
index 0000000..2893201
--- /dev/null
@@ -0,0 +1,16 @@
+import React from 'react'
+import styles from './Loading.scss'
+import componentClassNames from 'utility/componentClassNames'
+
+class Loading extends React.Component {
+  render() {
+    return (
+      <div className={componentClassNames(this, styles.main)}>
+        <img src={require('images/logo-shadowed.png')} className={styles.logo} />
+        {this.props.children}
+      </div>
+    )
+  }
+}
+
+export default Loading
diff --git a/src/features/app/components/Loading/Loading.scss b/src/features/app/components/Loading/Loading.scss
new file mode 100644 (file)
index 0000000..8fb499c
--- /dev/null
@@ -0,0 +1,21 @@
+@include keyframes(pulsing) {
+  0%   { opacity: 0.6; }
+  50%  { opacity: 1.0; }
+  100% { opacity: 0.6; }
+}
+
+.main {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  color: $text-color;
+  font-size: 24px;
+  margin-top: -75px;
+}
+
+.logo {
+  width: 175px;
+  padding-bottom: $gutter-size;
+  @include animation(pulsing 2s infinite);
+  @include animation-timing-function(ease-in-out);
+}
diff --git a/src/features/app/components/Login/Login.jsx b/src/features/app/components/Login/Login.jsx
new file mode 100644 (file)
index 0000000..f2ecf80
--- /dev/null
@@ -0,0 +1,65 @@
+import React from 'react'
+import { connect } from 'react-redux'
+import { ErrorBanner, TextField } from 'features/shared/components'
+import actions from 'actions'
+import styles from './Login.scss'
+import { reduxForm } from 'redux-form'
+
+class Login extends React.Component {
+  constructor(props) {
+    super(props)
+
+    this.submitWithErrors = this.submitWithErrors.bind(this)
+  }
+
+  submitWithErrors(data) {
+    return new Promise((resolve, reject) => {
+      this.props.logIn(data.token)
+        .catch((err) => reject({_error: err.message}))
+    })
+  }
+
+  render() {
+    let logo = require('images/logo-white.png')
+
+    const {
+      fields: { token },
+      error,
+      handleSubmit,
+      submitting
+    } = this.props
+
+    return (
+      <div className={styles.main}>
+        <img className={styles.image} src={logo} />
+        <div className={styles.form}>
+          <form onSubmit={handleSubmit(this.submitWithErrors)}>
+            <TextField
+              placeholder='Enter client token (tokenname:xyz...)'
+              fieldProps={token}
+              autoFocus={true} />
+
+            {error &&
+              <ErrorBanner
+                title='Error logging in'
+                error={error} />}
+
+            <button type='submit' className='btn btn-primary' disabled={submitting}>
+              Log In
+            </button>
+          </form>
+        </div>
+      </div>
+    )
+  }
+}
+
+export default connect(
+  () => ({}),
+  (dispatch) => ({
+    logIn: (token) => dispatch(actions.core.logIn(token))
+  })
+)(reduxForm({
+  form: 'login',
+  fields: ['token'],
+})(Login))
diff --git a/src/features/app/components/Login/Login.scss b/src/features/app/components/Login/Login.scss
new file mode 100644 (file)
index 0000000..ba33af7
--- /dev/null
@@ -0,0 +1,25 @@
+.main {
+  background: $background-inverse-color;
+  position: absolute;
+  top: 0;
+  bottom: 0;
+  left: 0;
+  right: 0;
+}
+
+.image {
+  width: 150px;
+  position: absolute;
+  top: calc(100px);
+  left: calc(50% - 75px);
+}
+
+.form {
+  background: $background-color;
+  border-radius: $border-radius-standard;
+  position: absolute;
+  left: calc(50% - 250px);
+  top: calc(200px);
+  width: 500px;
+  padding: 30px;
+}
diff --git a/src/features/app/components/Main/Main.jsx b/src/features/app/components/Main/Main.jsx
new file mode 100644 (file)
index 0000000..422b5a3
--- /dev/null
@@ -0,0 +1,60 @@
+import React from 'react'
+import styles from './Main.scss'
+import { Link } from 'react-router'
+import { connect } from 'react-redux'
+import actions from 'actions'
+import { Navigation } from '../'
+
+class Main extends React.Component {
+
+  constructor(props) {
+    super(props)
+
+    this.toggleDropdown = this.toggleDropdown.bind(this)
+  }
+
+  toggleDropdown(event) {
+    event.stopPropagation()
+    this.props.toggleDropdown()
+  }
+
+  render() {
+    let logo = require('images/logo-bytom-white.png')
+
+    return (
+      <div className={styles.main}
+           onClick={this.props.closeDropdown} >
+        <div className={styles.sidebar}>
+          <div className={styles.sidebarContent}>
+            <div className={styles.logo}>
+              <Link to={'/'}>
+                <img src={logo} className={styles.brand_image} />
+              </Link>
+            </div>
+
+            <Navigation />
+          </div>
+        </div>
+
+        <div className={`${styles.content} flex-container`}>
+          {!this.props.connected && <div className={styles.connectionIssue}>
+            There was an issue connecting to Chain Core. Please check your connection while dashboard attempts to reconnect.
+          </div>}
+          {this.props.children}
+        </div>
+      </div>
+    )
+  }
+}
+
+export default connect(
+  (state) => ({
+    canLogOut: state.core.requireClientToken,
+    connected: state.core.connected,
+    showDropwdown: state.app.dropdownState == 'open',
+  }),
+  (dispatch) => ({
+    toggleDropdown: () => dispatch(actions.app.toggleDropdown),
+    closeDropdown: () => dispatch(actions.app.closeDropdown),
+  })
+)(Main)
diff --git a/src/features/app/components/Main/Main.scss b/src/features/app/components/Main/Main.scss
new file mode 100644 (file)
index 0000000..0fe1e3d
--- /dev/null
@@ -0,0 +1,63 @@
+.main {
+  display: flex;
+  flex-direction: column;
+  flex: 1;
+}
+
+.sidebar {
+  width: $sidebar-width;
+  position: fixed;
+}
+
+.content {
+  width: 100%;
+  background: $background-content-color;
+  padding-left: $sidebar-width;
+}
+
+.sidebarContent {
+  background-color: $background-inverse-color;
+  position: fixed;
+  width: $sidebar-width;
+  height: 100%;
+  padding-bottom: 30px;
+
+  overflow-y: scroll;
+  -ms-overflow-style: none; // MS Edge fix
+
+  z-index: 10;
+}
+
+.logo {
+  border-bottom: 1px solid $border-inverse-color;
+  height: $title-height;
+  padding-left: $gutter-size;
+  display: flex;
+  align-items: center;
+
+  img {
+    width: 100px;
+  }
+}
+
+.settings {
+  cursor: pointer;
+  line-height: $title-height;
+  padding: 0 20px;
+  position: absolute;
+  right: 0;
+  top: 0;
+
+  img {
+    width: 28px;
+    height: 28px;
+  }
+}
+
+.connectionIssue {
+  padding: $gutter-size/2;
+  text-align: center;
+  background: $highlight-danger-background;
+  color: $highlight-danger;
+  border-bottom: 1px solid $highlight-danger;
+}
diff --git a/src/features/app/components/Modal/Modal.jsx b/src/features/app/components/Modal/Modal.jsx
new file mode 100644 (file)
index 0000000..6f93beb
--- /dev/null
@@ -0,0 +1,49 @@
+import React from 'react'
+import { connect } from 'react-redux'
+import styles from './Modal.scss'
+import actions from 'actions'
+
+class Modal extends React.Component {
+  render() {
+    let {
+      dispatch,
+      isShowing,
+      body,
+      acceptAction,
+      cancelAction
+    } = this.props
+
+    if (!isShowing) return null
+
+    const accept = () => {
+      dispatch(acceptAction)
+      dispatch(actions.app.hideModal)
+    }
+    const cancel = cancelAction ? () => dispatch(cancelAction) : null
+    const backdropAction = cancel || accept
+
+    return(
+      <div className={styles.main}>
+        <div className={styles.backdrop} onClick={backdropAction}></div>
+        <div className={`${this.props.options.wide && styles.wide} ${styles.content}`}>
+          {body}
+
+          <button className={`btn btn-${this.props.options.danger ? 'danger' : 'primary'} ${styles.accept}`} onClick={accept}>OK</button>
+          {cancel && <button className={`btn btn-link ${styles.cancel}`} onClick={cancel}>Cancel</button>}
+        </div>
+      </div>
+    )
+  }
+}
+
+const mapStateToProps = (state) => ({
+  isShowing: state.app.modal.isShowing,
+  body: state.app.modal.body,
+  acceptAction: state.app.modal.accept,
+  cancelAction: state.app.modal.cancel,
+  options: state.app.modal.options,
+})
+
+// NOTE: ommitting a function for `mapDispatchToProps` passes `dispatch` as a
+// prop to the component
+export default connect(mapStateToProps)(Modal)
diff --git a/src/features/app/components/Modal/Modal.scss b/src/features/app/components/Modal/Modal.scss
new file mode 100644 (file)
index 0000000..d6f1e48
--- /dev/null
@@ -0,0 +1,42 @@
+.main {
+  position: fixed;
+  top: 0;
+  right: 0;
+  left: 0;
+  bottom: 0;
+  z-index: 100;
+}
+.backdrop {
+  background: transparentize(black, 0.2);
+  width: 100%;
+  height: 100%;
+}
+
+.content {
+  background: $background-color;
+  padding: 30px;
+  position: absolute;
+  top: 10%;
+  left: calc(50% - 250px);
+  width: 500px;
+  max-height: 80%;
+  overflow: scroll;
+
+  pre {
+    white-space: pre-wrap;
+    background: $background-content-color;
+    display: inline-block;
+    padding: 8px 12px;
+    color: $text-color;
+    line-height: 1.4;
+  }
+}
+
+.wide {
+  left: calc(50% - 425px);
+  width: 850px;
+}
+
+.cancel {
+  color: $text-color;
+}
diff --git a/src/features/app/components/Navigation/Navigation.jsx b/src/features/app/components/Navigation/Navigation.jsx
new file mode 100644 (file)
index 0000000..dfd668f
--- /dev/null
@@ -0,0 +1,78 @@
+import React from 'react'
+import { connect } from 'react-redux'
+import { Link } from 'react-router'
+import styles from './Navigation.scss'
+import { navIcon } from '../../utils'
+
+class Navigation extends React.Component {
+  constructor(props) {
+    super(props)
+
+    this.openTutorial = this.openTutorial.bind(this)
+  }
+
+  openTutorial(event) {
+    event.preventDefault()
+    this.props.openTutorial()
+  }
+
+  render() {
+    return (
+      <div className={styles.main}>
+        <ul className={styles.navigation}>
+          <li>
+            <Link to='/transactions' activeClassName={styles.active}>
+              {navIcon('transaction', styles)}
+              交易
+            </Link>
+          </li>
+          <li>
+            <Link to='/accounts' activeClassName={styles.active}>
+              {navIcon('account', styles)}
+              账户
+            </Link>
+          </li>
+          <li>
+            <Link to='/assets' activeClassName={styles.active}>
+              {navIcon('asset', styles)}
+              资产
+            </Link>
+          </li>
+          <li>
+            <Link to='/balances' activeClassName={styles.active}>
+              {navIcon('balance', styles)}
+              账单
+            </Link>
+          </li>
+          <li>
+            <Link to='/unspents' activeClassName={styles.active}>
+              {navIcon('unspent', styles)}
+              UTXO
+            </Link>
+          </li>
+        </ul>
+      </div>
+    )
+  }
+}
+
+export default connect(
+  state => {
+    let docVersion = ''
+
+    const versionComponents = state.core.version.match('^([0-9]\\.[0-9])\\.')
+    if (versionComponents != null) {
+      docVersion = versionComponents[1]
+    }
+
+    return {
+      routing: state.routing, // required for <Link>s to update active state on navigation
+      showSync: state.core.configured && !state.core.generator,
+      mockhsm: state.core.mockhsm,
+      docVersion
+    }
+  },
+  (dispatch) => ({
+    openTutorial: () => dispatch({ type: 'OPEN_TUTORIAL' })
+  })
+)(Navigation)
diff --git a/src/features/app/components/Navigation/Navigation.scss b/src/features/app/components/Navigation/Navigation.scss
new file mode 100644 (file)
index 0000000..0c769a3
--- /dev/null
@@ -0,0 +1,55 @@
+.main {
+  a {
+    display: block;
+    padding: 1px 0;
+    color: $highlight-inverse-color;
+    font-size: $font-size-nav;
+  }
+
+  .active,
+  .active:hover,
+  .active:focus {
+    color: $highlight-default;
+  }
+
+  a:hover,
+  a:focus {
+    text-decoration: none;
+    color: lighten($highlight-inverse-color, 15%);
+  }
+}
+
+.navigation {
+  padding: 0;
+  list-style-type: none;
+  margin-top: $gutter-size/2;
+  margin-bottom: 0;
+  padding: 0 $gutter-size;
+}
+
+.navigationTitle {
+  color: $highlight-inverse-color;
+  font-size: $font-size-caps;
+  text-transform: uppercase;
+  padding: 3px 0;
+}
+
+.activeIcon {
+  display: none;
+}
+
+.active .activeIcon {
+  display: inline;
+}
+
+.active .icon {
+  display: none;
+}
+
+.iconWrapper {
+  img {
+    width: 28px;
+    height: 28px;
+    margin-top: -2px;
+  }
+}
diff --git a/src/features/app/components/SecondaryNavigation/SecondaryNavigation.jsx b/src/features/app/components/SecondaryNavigation/SecondaryNavigation.jsx
new file mode 100644 (file)
index 0000000..8218e3d
--- /dev/null
@@ -0,0 +1,58 @@
+import React from 'react'
+import { Link } from 'react-router'
+import { connect } from 'react-redux'
+import actions from 'actions'
+import { navIcon } from '../../utils'
+import styles from './SecondaryNavigation.scss'
+
+class SecondaryNavigation extends React.Component {
+  constructor(props) {
+    super(props)
+
+    this.logOut = this.logOut.bind(this)
+  }
+
+  logOut(event) {
+    event.preventDefault()
+    this.props.logOut()
+  }
+
+  render() {
+    return (
+      <div className={styles.main}>
+        <ul className={styles.navigation}>
+          <li className={styles.navigationTitle}>settings</li>
+
+          <li>
+            <Link to='/core' activeClassName={styles.active}>
+              {navIcon('core', styles)}
+              Core status
+            </Link>
+          </li>
+          <li>
+            <Link to='/access-control' activeClassName={styles.active}>
+              {navIcon('network', styles)}
+              Access Control
+            </Link>
+          </li>
+
+          {this.props.canLogOut && <li className={styles.logOut}>
+            <a href='#' onClick={this.logOut}>
+              {navIcon('logout', styles)}
+              Log Out
+            </a>
+          </li>}
+        </ul>
+      </div>
+    )
+  }
+}
+
+export default connect(
+  (state) => ({
+    canLogOut: state.core.requireClientToken,
+  }),
+  (dispatch) => ({
+    logOut: () => dispatch(actions.core.clearSession)
+  })
+)(SecondaryNavigation)
diff --git a/src/features/app/components/SecondaryNavigation/SecondaryNavigation.scss b/src/features/app/components/SecondaryNavigation/SecondaryNavigation.scss
new file mode 100644 (file)
index 0000000..d11b614
--- /dev/null
@@ -0,0 +1,66 @@
+.main {
+  background: $background-color;
+  border-radius: $border-radius-standard;
+  position: absolute;
+  top: $title-height - 8px;
+  right: $gutter-size/2;
+  width: $sidebar-width - $gutter-size;
+  z-index: 11;
+
+  a {
+    display: block;
+    padding: 1px 0;
+    color: $text-color;
+  }
+
+  .active {
+    color: $highlight-default;
+  }
+}
+
+.navigation {
+  padding: 0;
+  list-style-type: none;
+  margin-top: 8px;
+  margin-bottom: 8px;
+
+  li {
+    padding: 0 20px;
+  }
+}
+
+.navigationTitle {
+  font-size: $font-size-caps;
+  text-transform: uppercase;
+  padding: 3px 20px;
+}
+
+.logOut {
+  margin-top: 5px;
+  border-top: 1px solid $border-color;
+  margin-bottom: -3px;
+
+  a {
+    padding-top: 5px;
+  }
+}
+
+.activeIcon {
+  display: none;
+}
+
+.active .activeIcon {
+  display: inline;
+}
+
+.active .icon {
+  display: none;
+}
+
+.iconWrapper {
+  img {
+    width: 28px;
+    height: 28px;
+    margin-top: -2px;
+  }
+}
diff --git a/src/features/app/components/Sync/Sync.jsx b/src/features/app/components/Sync/Sync.jsx
new file mode 100644 (file)
index 0000000..e11cefd
--- /dev/null
@@ -0,0 +1,87 @@
+import React from 'react'
+import moment from 'moment'
+import { connect } from 'react-redux'
+import { Link } from 'react-router'
+import { humanizeDuration } from 'utility/time'
+import testnetUtils from 'features/testnet/utils'
+import { navIcon } from '../../utils'
+import navStyles from '../Navigation/Navigation.scss'
+import styles from './Sync.scss'
+
+class Sync extends React.Component {
+  render() {
+    const {
+      onTestnet,
+      replicationLag,
+      snapshot,
+      syncEstimates,
+      testnetError,
+      testnetNextReset
+    } = this.props
+
+    if (snapshot && snapshot.inProgress) { // Currently downloading the snapshot.
+      const downloaded = (snapshot.downloaded / snapshot.size) * 100
+
+      return <ul className={`${navStyles.navigation} ${styles.main}`}>
+        {onTestnet &&
+          <li className={navStyles.navigationTitle}>chain testnet snapshot</li>
+        }
+        {!onTestnet &&
+          <li className={navStyles.navigationTitle}>snapshot sync</li>
+        }
+
+        <li>{snapshot.height} blocks</li>
+        {!!downloaded && <li>{Math.round(downloaded)}% downloaded</li>}
+        {!!syncEstimates.snapshot && <li>Time remaining: {humanizeDuration(syncEstimates.snapshot)}</li>}
+      </ul>
+    }
+
+    const elems = []
+
+    if (onTestnet) {
+      elems.push(<li key='sync-title' className={navStyles.navigationTitle}>chain testnet sync</li>)
+    } else {
+      elems.push(<li key='sync-title' className={navStyles.navigationTitle}>generator sync</li>)
+    }
+
+    if (onTestnet && !testnetError && testnetNextReset) {
+      const diff = testnetNextReset.diff(moment(), 'seconds')
+      if (diff < 2 * 24 * 60 * 60) {
+        elems.push(<li key='sync-reset-warning'><span className={styles.testnetReset}>Next reset: {humanizeDuration(diff)}</span></li>)
+      }
+    }
+
+    if (onTestnet && testnetError) {
+      elems.push(<li key='sync-error'>
+        <Link to='/core'>
+          {navIcon('error', navStyles)}
+          <span className={styles.testnetError}>Chain Testnet error</span>
+        </Link>
+      </li>)
+    } else {
+      if (replicationLag === null || replicationLag >= 2) {
+        elems.push(<li key='sync-lag'>Blocks behind: {replicationLag === null ? '-' : replicationLag}</li>)
+
+        if (syncEstimates.replicationLag) {
+          elems.push(<li key='sync-time'>Time remaining: {humanizeDuration(syncEstimates.replicationLag)}</li>)
+        }
+      } else {
+        elems.push(<li key='sync-done'>Local core fully synced.</li>)
+      }
+    }
+
+    return <ul className={`${navStyles.navigation} ${styles.main}`}>{elems}</ul>
+  }
+}
+
+export default connect(
+  (state) => ({
+    onTestnet: state.core.onTestnet,
+    routing: state.routing, // required for <Link>s to update active state on navigation
+    replicationLag: state.core.replicationLag,
+    snapshot: state.core.snapshot,
+    syncEstimates: state.core.syncEstimates,
+    testnetError: testnetUtils.isBlockchainMismatch(state) || testnetUtils.isCrosscoreRpcMismatch(state),
+    testnetNextReset: state.testnet.nextReset,
+  })
+)(Sync)
diff --git a/src/features/app/components/Sync/Sync.scss b/src/features/app/components/Sync/Sync.scss
new file mode 100644 (file)
index 0000000..f24ba10
--- /dev/null
@@ -0,0 +1,13 @@
+.main {
+  padding-top: $gutter-size;
+  border-top: 1px solid $border-inverse-color;
+  margin-top: $gutter-size;
+}
+
+.testnetError {
+  color: $brand-danger;
+}
+
+.testnetReset {
+  color: $brand-warning;
+}
diff --git a/src/features/app/components/index.js b/src/features/app/components/index.js
new file mode 100644 (file)
index 0000000..39dbc68
--- /dev/null
@@ -0,0 +1,19 @@
+import Container from './Container'
+import Main from './Main/Main'
+import Config from './Config/Config'
+import Login from './Login/Login'
+import Loading from './Loading/Loading'
+import Modal from './Modal/Modal'
+import Navigation from './Navigation/Navigation'
+import SecondaryNavigation from './SecondaryNavigation/SecondaryNavigation'
+
+export {
+  Container,
+  Main,
+  Config,
+  Login,
+  Loading,
+  Modal,
+  Navigation,
+  SecondaryNavigation,
+}
diff --git a/src/features/app/index.js b/src/features/app/index.js
new file mode 100644 (file)
index 0000000..fa284b5
--- /dev/null
@@ -0,0 +1,7 @@
+import actions from './actions'
+import reducers from './reducers'
+
+export {
+  actions,
+  reducers,
+}
diff --git a/src/features/app/reducers.js b/src/features/app/reducers.js
new file mode 100644 (file)
index 0000000..f5bd9a5
--- /dev/null
@@ -0,0 +1,136 @@
+import React from 'react'
+import { Link } from 'react-router'
+import { combineReducers } from 'redux'
+import uuid from 'uuid'
+
+const flash = (message, title, type) => ({ message, title, type, displayed: false })
+const newFlash = (state, f) => ({...state, [uuid.v4()]: f})
+const newSuccess = (state, message, title) => ({...state, [uuid.v4()]: flash(message, title, 'success')})
+const newError = (state, message, title) => ({...state, [uuid.v4()]: flash(message, title, 'danger')})
+
+export const flashMessages = (state = {}, action) => {
+  switch (action.type) {
+    case '@@router/LOCATION_CHANGE': {
+      if (action.payload.state && action.payload.state.preserveFlash) {
+        return state
+      } else {
+        Object.keys(state).forEach(key => {
+          const item = state[key]
+          if (item.displayed) {
+            delete state[key]
+          }
+        })
+        return {...state}
+      }
+    }
+
+    case 'CREATED_ACCOUNT': {
+      return newSuccess(state, <p>
+          Created account. <Link to='accounts/create'>Create another?</Link>
+        </p>)
+    }
+
+    case 'CREATED_ASSET': {
+      return newSuccess(state, <p>
+        Created asset. <Link to='assets/create'>Create another?</Link>
+      </p>)
+    }
+
+    case 'CREATED_TRANSACTION': {
+      return newSuccess(state, <p>
+        Submitted transaction. <Link to='transactions/create'>Create another?</Link>
+      </p>)
+    }
+
+    case 'CREATED_MOCKHSM': {
+      return newSuccess(state, <p>
+        Created key. <Link to='mockhsms/create'>Create another?</Link>
+      </p>)
+    }
+
+    case 'CREATED_TRANSACTIONFEED': {
+      return newSuccess(state, <p>
+        Created transaction feed. <Link to='transaction-feeds/create'>Create another?</Link>
+      </p>)
+    }
+
+    case 'CREATED_TOKEN_WITH_GRANT': {
+      return newSuccess(state, <p>
+        Created access token. <Link to='access-control/create-token'>Create another?</Link>
+      </p>)
+    }
+
+    case 'CREATED_X509_GRANT': {
+      return newSuccess(state, <p>
+        Granted policy to X509 certificate. <Link to='access-control/add-certificate'>Create another?</Link>
+      </p>)
+    }
+
+    case 'DELETE_ACCESS_TOKEN':
+    case 'DELETE_TRANSACTIONFEED': {
+      return newFlash(state, flash(action.message, null, 'info'))
+    }
+
+    case 'DISMISS_FLASH': {
+      delete state[action.param]
+      return {...state}
+    }
+
+    case 'DISPLAYED_FLASH': {
+      const existing = state[action.param]
+      if (existing && !existing.displayed) {
+        const newState = {...state}
+        existing.displayed = true
+        newState[action.param] = existing
+        return newState
+      }
+      return state
+    }
+
+    case 'UPDATED_ACCOUNT': {
+      return newSuccess(state, <p>
+          Updated account tags.
+        </p>)
+    }
+
+    case 'UPDATED_ASSET': {
+      return newSuccess(state, <p>
+          Updated asset tags.
+        </p>)
+    }
+
+    case 'ERROR': {
+      return newError(state, action.payload.message)
+    }
+
+    case 'USER_LOG_IN': {
+      return {}
+    }
+
+    default: {
+      return state
+    }
+  }
+}
+
+export const modal = (state = { isShowing: false }, action) => {
+  if      (action.type == 'SHOW_MODAL') return { isShowing: true, ...action.payload }
+  else if (action.type == 'HIDE_MODAL') return { isShowing: false }
+  return state
+}
+
+export const dropdownState = (state = '', action) => {
+  if (action.type == 'TOGGLE_DROPDOWN') {
+    return state === '' ? 'open' : ''
+  } else if (action.type == 'CLOSE_DROPDOWN') {
+    return ''
+  }
+
+  return state
+}
+
+export default combineReducers({
+  flashMessages,
+  modal,
+  dropdownState,
+})
diff --git a/src/features/app/utils.jsx b/src/features/app/utils.jsx
new file mode 100644 (file)
index 0000000..a20d67e
--- /dev/null
@@ -0,0 +1,16 @@
+import React from 'react'
+
+export const navIcon = (name, styles) => {
+  let active = false
+  const icon = require(`images/navigation/${name}.png`)
+
+  try {
+    active = require(`images/navigation/${name}-active.png`)
+  } catch (err) { /* do nothing */ }
+  return (
+    <span className={styles.iconWrapper}>
+      <img className={styles.icon} src={icon}/>
+      {active && <img className={styles.activeIcon} src={active}/>}
+    </span>
+  )
+}
diff --git a/src/features/assets/actions.js b/src/features/assets/actions.js
new file mode 100644 (file)
index 0000000..c3c3b56
--- /dev/null
@@ -0,0 +1,20 @@
+import { baseCreateActions, baseUpdateActions, baseListActions } from 'features/shared/actions'
+
+const type = 'asset'
+
+const list = baseListActions(type, { defaultKey: 'alias' })
+const create = baseCreateActions(type, {
+  jsonFields: ['tags', 'definition'],
+  intFields: ['quorum'],
+  redirectToShow: true,
+})
+const update = baseUpdateActions(type, {
+  jsonFields: ['tags']
+})
+
+const actions = {
+  ...list,
+  ...create,
+  ...update,
+}
+export default actions
diff --git a/src/features/assets/components/AssetShow.jsx b/src/features/assets/components/AssetShow.jsx
new file mode 100644 (file)
index 0000000..e95e5b0
--- /dev/null
@@ -0,0 +1,90 @@
+import React from 'react'
+import {
+  BaseShow,
+  PageContent,
+  PageTitle,
+  KeyValueTable,
+  RawJsonButton,
+} from 'features/shared/components'
+import componentClassNames from 'utility/componentClassNames'
+
+class AssetShow extends BaseShow {
+  render() {
+    const item = this.props.item
+
+    let view
+    if (item) {
+      const title = <span>
+        {'Asset '}
+        <code>{item.alias ? item.alias :item.id}</code>
+      </span>
+
+      view = <div className={componentClassNames(this)}>
+        <PageTitle title={title} />
+
+        <PageContent>
+          <KeyValueTable
+            id={item.id}
+            object='asset'
+            title='Details'
+            actions={[
+              <button key='show-circulation' className='btn btn-link' onClick={this.props.showCirculation.bind(this, item)}>
+                Circulation
+              </button>,
+              <RawJsonButton key='raw-json' item={item} />
+            ]}
+            items={[
+              {label: 'ID', value: item.id},
+              {label: 'Alias', value: item.alias},
+              {label: 'Tags', value: item.tags, editUrl: `/assets/${item.id}/tags`},
+              {label: 'Definition', value: item.definition},
+              {label: 'Keys', value: item.keys.length},
+              {label: 'Quorum', value: item.quorum},
+            ]}
+          />
+
+          {item.keys.map((key, index) =>
+            <KeyValueTable
+              key={index}
+              title={`Key ${index + 1}`}
+              items={[
+                {label: 'Index', value: index},
+                {label: 'Root Xpub', value: key.rootXpub},
+                {label: 'Asset Pubkey', value: key.assetPubkey},
+                {label: 'Asset Derivation Path', value: key.assetDerivationPath},
+              ]}
+            />
+          )}
+        </PageContent>
+      </div>
+    }
+    return this.renderIfFound(view)
+  }
+}
+
+// Container
+
+import { connect } from 'react-redux'
+import actions from 'actions'
+
+const mapStateToProps = (state, ownProps) => ({
+  item: state.asset.items[ownProps.params.id]
+})
+
+const mapDispatchToProps = ( dispatch ) => ({
+  fetchItem: (id) => dispatch(actions.asset.fetchItems({filter: `id='${id}'`})),
+  showCirculation: (item) => {
+    let filter = `asset_id='${item.id}'`
+    if (item.alias) {
+      filter = `asset_alias='${item.alias}'`
+    }
+
+    dispatch(actions.balance.pushList({ filter }))
+  },
+})
+
+
+export default connect(
+  mapStateToProps,
+  mapDispatchToProps
+)(AssetShow)
diff --git a/src/features/assets/components/AssetUpdate.jsx b/src/features/assets/components/AssetUpdate.jsx
new file mode 100644 (file)
index 0000000..cf652c7
--- /dev/null
@@ -0,0 +1,120 @@
+import React from 'react'
+import { BaseUpdate, FormContainer, FormSection, JsonField, NotFound } from 'features/shared/components'
+import { reduxForm } from 'redux-form'
+import { docsRoot } from 'utility/environment'
+
+class Form extends React.Component {
+  constructor(props) {
+    super(props)
+
+    this.submitWithErrors = this.submitWithErrors.bind(this)
+
+    this.state = {}
+  }
+
+  submitWithErrors(data) {
+    return this.props.submitForm(data, this.props.item.id).catch(err => {
+      throw {_error: err}
+    })
+  }
+
+  componentDidMount() {
+    this.props.fetchItem(this.props.params.id).then(resp => {
+      if (resp.items.length == 0) {
+        this.setState({notFound: true})
+      }
+    })
+  }
+
+  render() {
+    if (this.state.notFound) {
+      return <NotFound />
+    }
+    const item = this.props.item
+
+    if (!item) {
+      return <div>Loading...</div>
+    }
+
+    const {
+      fields: { tags },
+      error,
+      handleSubmit,
+      submitting
+    } = this.props
+
+    const title = <span>
+      {'Edit asset tags '}
+      <code>{item.alias ? item.alias :item.id}</code>
+    </span>
+
+    const tagsString = Object.keys(item.tags).length === 0 ? '{\n\t\n}' : JSON.stringify(item.tags, null, 1)
+    const tagLines = tagsString.split(/\r\n|\r|\n/).length
+    let JsonFieldHeight
+
+    if (tagLines < 5) {
+      JsonFieldHeight = '80px'
+    } else if (tagLines < 20) {
+      JsonFieldHeight = `${tagLines * 17}px`
+    } else {
+      JsonFieldHeight = '340px'
+    }
+
+    return <FormContainer
+      error={error}
+      label={title}
+      onSubmit={handleSubmit(this.submitWithErrors)}
+      submitting={submitting} >
+
+      <FormSection title='Asset Tags'>
+        <JsonField
+          height={JsonFieldHeight}
+          fieldProps={tags} />
+
+        <p>
+          Note: Asset tags can be used for querying transactions, unspent outputs, and balances. Queries reflect the account tags that are present when transactions are submitted. Only new transaction activity will reflect the updated tags. <a href={`${docsRoot}/core/build-applications/assets#update-tags-on-existing-assets`} target='_blank' style={{whiteSpace: 'nowrap'}}>
+            Learn more →</a>
+        </p>
+      </FormSection>
+    </FormContainer>
+  }
+}
+
+const mapStateToProps = (state, ownProps) => ({
+  item: state.asset.items[ownProps.params.id]
+})
+
+const initialValues = (state, ownProps) => {
+  const item = state.asset.items[ownProps.params.id]
+  if (item) {
+    const tags = Object.keys(item.tags).length === 0 ? '{\n\t\n}' : JSON.stringify(item.tags, null, 1)
+    return {
+      initialValues: {
+        tags: tags
+      }
+    }
+  }
+  return {}
+}
+
+const updateForm = reduxForm({
+  form: 'updateAssetForm',
+  fields: ['tags'],
+  validate: values => {
+    const errors = {}
+
+    const jsonFields = ['tags']
+    jsonFields.forEach(key => {
+      const fieldError = JsonField.validator(values[key])
+      if (fieldError) { errors[key] = fieldError }
+    })
+
+    return errors
+  }
+}, initialValues)(Form)
+
+export default BaseUpdate.connect(
+  mapStateToProps,
+  BaseUpdate.mapDispatchToProps('asset'),
+  updateForm
+)
diff --git a/src/features/assets/components/List.jsx b/src/features/assets/components/List.jsx
new file mode 100644 (file)
index 0000000..bd09046
--- /dev/null
@@ -0,0 +1,15 @@
+import React from 'react'
+import { BaseList, TableList } from 'features/shared/components'
+import ListItem from './ListItem'
+
+const type = 'asset'
+
+export default BaseList.connect(
+  BaseList.mapStateToProps(type, ListItem, {
+    wrapperComponent: TableList,
+    wrapperProps: {
+      titles: ['Asset Alias', 'Asset ID']
+    }
+  }),
+  BaseList.mapDispatchToProps(type)
+)
diff --git a/src/features/assets/components/ListItem.jsx b/src/features/assets/components/ListItem.jsx
new file mode 100644 (file)
index 0000000..0b737b3
--- /dev/null
@@ -0,0 +1,22 @@
+import React from 'react'
+import { Link } from 'react-router'
+
+class ListItem extends React.Component {
+  render() {
+    const item = this.props.item
+
+    return(
+      <tr>
+        <td>{item.alias || '-'}</td>
+        <td><code>{item.id}</code></td>
+        <td>
+          <Link to={`/assets/${item.id}`}>
+            View details →
+          </Link>
+        </td>
+      </tr>
+    )
+  }
+}
+
+export default ListItem
diff --git a/src/features/assets/components/New.jsx b/src/features/assets/components/New.jsx
new file mode 100644 (file)
index 0000000..0da7861
--- /dev/null
@@ -0,0 +1,85 @@
+import React from 'react'
+import { BaseNew, FormContainer, FormSection, JsonField, KeyConfiguration, TextField } from 'features/shared/components'
+import { reduxForm } from 'redux-form'
+
+class Form extends React.Component {
+  constructor(props) {
+    super(props)
+
+    this.submitWithErrors = this.submitWithErrors.bind(this)
+  }
+
+  submitWithErrors(data) {
+    return new Promise((resolve, reject) => {
+      this.props.submitForm(data)
+        .catch((err) => reject({_error: err}))
+    })
+  }
+
+  render() {
+    const {
+      fields: { alias, tags, definition, xpubs, quorum },
+      error,
+      handleSubmit,
+      submitting
+    } = this.props
+
+    return(
+      <FormContainer
+        error={error}
+        label='New asset'
+        onSubmit={handleSubmit(this.submitWithErrors)}
+        submitting={submitting} >
+
+        <FormSection title='Asset Information'>
+          <TextField title='Alias' placeholder='Alias' fieldProps={alias} autoFocus={true} />
+          <JsonField title='Tags' fieldProps={tags} />
+          <JsonField title='Definition' fieldProps={definition} />
+        </FormSection>
+
+        <FormSection title='Keys and Signing'>
+          <KeyConfiguration
+            xpubs={xpubs}
+            quorum={quorum}
+            quorumHint='Number of signatures required to issue' />
+        </FormSection>
+
+      </FormContainer>
+    )
+  }
+}
+
+const validate = values => {
+  const errors = {}
+
+  const jsonFields = ['tags', 'definition']
+  jsonFields.forEach(key => {
+    const fieldError = JsonField.validator(values[key])
+    if (fieldError) { errors[key] = fieldError }
+  })
+
+  return errors
+}
+
+const fields = [
+  'alias',
+  'tags',
+  'definition',
+  'xpubs[].value',
+  'xpubs[].type',
+  'quorum'
+]
+export default BaseNew.connect(
+  BaseNew.mapStateToProps('asset'),
+  BaseNew.mapDispatchToProps('asset'),
+  reduxForm({
+    form: 'newAssetForm',
+    fields,
+    validate,
+    initialValues: {
+      tags: '{\n\t\n}',
+      definition: '{\n\t\n}',
+      quorum: 1,
+    }
+  })(Form)
+)
diff --git a/src/features/assets/components/index.js b/src/features/assets/components/index.js
new file mode 100644 (file)
index 0000000..f9ca785
--- /dev/null
@@ -0,0 +1,11 @@
+import List from './List'
+import New from './New'
+import AssetShow from './AssetShow'
+import AssetUpdate from './AssetUpdate'
+
+export {
+  List,
+  New,
+  AssetShow,
+  AssetUpdate
+}
diff --git a/src/features/assets/index.js b/src/features/assets/index.js
new file mode 100644 (file)
index 0000000..f4fbe4d
--- /dev/null
@@ -0,0 +1,9 @@
+import actions from './actions'
+import reducers from './reducers'
+import routes from './routes'
+
+export {
+  actions,
+  reducers,
+  routes
+}
diff --git a/src/features/assets/reducers.js b/src/features/assets/reducers.js
new file mode 100644 (file)
index 0000000..89813e9
--- /dev/null
@@ -0,0 +1,10 @@
+import { reducers } from 'features/shared'
+import { combineReducers } from 'redux'
+
+const type = 'asset'
+
+export default combineReducers({
+  items: reducers.itemsReducer(type),
+  queries: reducers.queriesReducer(type),
+  autocompleteIsLoaded: reducers.autocompleteIsLoadedReducer(type),
+})
diff --git a/src/features/assets/routes.js b/src/features/assets/routes.js
new file mode 100644 (file)
index 0000000..05c2419
--- /dev/null
@@ -0,0 +1,4 @@
+import { List, New, AssetShow, AssetUpdate } from './components'
+import { makeRoutes } from 'features/shared'
+
+export default (store) => makeRoutes(store, 'asset', List, New, AssetShow, AssetUpdate)
diff --git a/src/features/balances/actions.js b/src/features/balances/actions.js
new file mode 100644 (file)
index 0000000..f5dee48
--- /dev/null
@@ -0,0 +1,2 @@
+import { baseListActions } from 'features/shared/actions'
+export default baseListActions('balance')
diff --git a/src/features/balances/components/List.jsx b/src/features/balances/components/List.jsx
new file mode 100644 (file)
index 0000000..7cb1ae3
--- /dev/null
@@ -0,0 +1,20 @@
+import { BaseList } from 'features/shared/components'
+import ListItem from './ListItem'
+
+const type = 'balance'
+
+const newStateToProps = (state, ownProps) => {
+  const props =  {
+    ...BaseList.mapStateToProps(type, ListItem)(state, ownProps),
+    skipCreate: true,
+    defaultFilter: "is_local='yes'"
+  }
+
+  props.searchState.sumBy = ownProps.location.query.sumBy || ''
+  return props
+}
+
+export default BaseList.connect(
+  newStateToProps,
+  BaseList.mapDispatchToProps(type)
+)
diff --git a/src/features/balances/components/ListItem.jsx b/src/features/balances/components/ListItem.jsx
new file mode 100644 (file)
index 0000000..22c8e98
--- /dev/null
@@ -0,0 +1,11 @@
+import React from 'react'
+import { KeyValueTable } from 'features/shared/components'
+import { buildBalanceDisplay } from 'utility/buildInOutDisplay'
+
+class ListItem extends React.Component {
+  render() {
+    return <KeyValueTable items={buildBalanceDisplay(this.props.item)} />
+  }
+}
+
+export default ListItem
diff --git a/src/features/balances/components/index.js b/src/features/balances/components/index.js
new file mode 100644 (file)
index 0000000..ac005ec
--- /dev/null
@@ -0,0 +1,5 @@
+import List from './List'
+
+export {
+  List,
+}
diff --git a/src/features/balances/index.js b/src/features/balances/index.js
new file mode 100644 (file)
index 0000000..f4fbe4d
--- /dev/null
@@ -0,0 +1,9 @@
+import actions from './actions'
+import reducers from './reducers'
+import routes from './routes'
+
+export {
+  actions,
+  reducers,
+  routes
+}
diff --git a/src/features/balances/reducers.js b/src/features/balances/reducers.js
new file mode 100644 (file)
index 0000000..fb8a32a
--- /dev/null
@@ -0,0 +1,42 @@
+import { combineReducers } from 'redux'
+import { reducers } from 'features/shared'
+
+const type = 'balance'
+const idFunc = (item, index) => index
+
+const itemsReducer = (state = {}, action) => {
+  if (action.type == 'APPEND_BALANCE_PAGE') {
+    const newState = {}
+    action.param.items.forEach((item, index) => {
+      item.id = `balance-${index}`
+      newState[index] = item
+    })
+    return newState
+  }
+  return state
+}
+
+const listViewReducer = combineReducers({
+  itemIds: reducers.queryItemsReducer(type, idFunc),
+  cursor: reducers.queryCursorReducer(type),
+  queryTime: reducers.queryTimeReducer(type),
+})
+
+const queriesReducer = (state = {}, action) => {
+  if (action.type == 'APPEND_BALANCE_PAGE') {
+    const query = action.param.next.filter || ''
+    const list = state[query] || {}
+
+    return {
+      [`${query}`]: listViewReducer(list, action)
+    }
+  }
+
+  return state
+}
+
+
+export default combineReducers({
+  items: itemsReducer,
+  queries: queriesReducer
+})
diff --git a/src/features/balances/routes.js b/src/features/balances/routes.js
new file mode 100644 (file)
index 0000000..bfc16d8
--- /dev/null
@@ -0,0 +1,6 @@
+import { List } from './components'
+import { makeRoutes } from 'features/shared'
+
+export default (store) => makeRoutes(store, 'balance', List, null, null, null, {
+  defaultFilter: "is_local='yes'"
+})
diff --git a/src/features/configuration/actions.js b/src/features/configuration/actions.js
new file mode 100644 (file)
index 0000000..6c07ef3
--- /dev/null
@@ -0,0 +1,45 @@
+import { chainClient } from 'utility/environment'
+import { actions as coreActions } from 'features/core'
+import { fetchTestnetInfo } from 'features/testnet/actions'
+
+const retry = (dispatch, promise, count = 10) => {
+  return dispatch(promise).catch((err) => {
+    var currentTime = new Date().getTime()
+    while (currentTime + 200 >= new Date().getTime()) { /* wait for retry */ }
+
+    if (count >= 1) {
+      retry(dispatch, promise, count - 1)
+    } else {
+      throw(err)
+    }
+  })
+}
+
+let actions = {
+  submitConfiguration: (data) => {
+    const configureWithRetry = (dispatch, config) => {
+      return chainClient().config.configure(config)
+        .then(() => retry(dispatch, coreActions.fetchCoreInfo({throw: true})))
+    }
+
+    return (dispatch) => {
+      if (data.type == 'testnet') {
+        return dispatch(fetchTestnetInfo()).then(testnet =>
+          configureWithRetry(dispatch, testnet))
+      } else {
+        if (data.type == 'new') {
+          data = {
+            isGenerator: true,
+            isSigner: true,
+            quorum: 1,
+          }
+        }
+
+        delete data.type
+        return configureWithRetry(dispatch, data)
+      }
+    }
+  }
+}
+
+export default actions
diff --git a/src/features/configuration/components/Index/Index.jsx b/src/features/configuration/components/Index/Index.jsx
new file mode 100644 (file)
index 0000000..f499472
--- /dev/null
@@ -0,0 +1,210 @@
+import { reduxForm } from 'redux-form'
+import { ErrorBanner, SubmitIndicator, TextField } from 'features/shared/components'
+import pick from 'lodash/pick'
+import actions from 'actions'
+import React from 'react'
+import styles from './Index.scss'
+import { docsRoot } from 'utility/environment'
+
+class Index extends React.Component {
+  constructor(props) {
+    super(props)
+
+    this.submitWithValidation = this.submitWithValidation.bind(this)
+  }
+
+  showNewFields() {
+    return this.props.fields.type.value === 'new'
+  }
+
+  showJoinFields() {
+    return this.props.fields.type.value === 'join'
+  }
+
+  showTestnetFields() {
+    return this.props.fields.type.value === 'testnet'
+  }
+
+  submitWithValidation(data) {
+    if (data.generatorUrl && !data.blockchainId) {
+      return new Promise((_, reject) => reject({
+        _error: 'You must specify a blockchain ID to connect to a network'
+      }))
+    }
+
+    return new Promise((resolve, reject) => {
+      this.props.submitForm(data)
+        .catch((err) => reject({type: err}))
+    })
+  }
+
+  render() {
+    const {
+      fields: {
+        type,
+        generatorUrl,
+        generatorAccessToken,
+        blockchainId
+      },
+      handleSubmit,
+      submitting,
+    } = this.props
+
+    const typeChange = (event) => {
+      const value = type.onChange(event).value
+
+      if (value != 'join') {
+        generatorUrl.onChange('')
+        generatorAccessToken.onChange('')
+        blockchainId.onChange('')
+      }
+    }
+
+    const typeProps = {
+      ...pick(type, ['name', 'value', 'checked', 'onBlur', 'onFocus']),
+      onChange: typeChange
+    }
+
+    let configSubmit = [
+      (type.error && <ErrorBanner
+        key='configError'
+        title='There was a problem configuring your core'
+        error={type.error}
+      />),
+      <button
+        key='configSubmit'
+        type='submit'
+        className={`btn btn-primary btn-lg ${styles.submit}`}
+        disabled={submitting}>
+          &nbsp;{this.showNewFields() ? 'Create' : 'Join'} network
+      </button>
+    ]
+
+    if (submitting) {
+      configSubmit.push(<SubmitIndicator
+        text={this.showNewFields() ? 'Creating network...' : 'Joining network...'}
+      />)
+    }
+
+    return (
+      <form onSubmit={handleSubmit(this.submitWithValidation)}>
+        <h2 className={styles.title}>Configure Chain Core</h2>
+
+        <div className={styles.choices}>
+          <div className={styles.choice_wrapper}>
+            <label>
+              <input className={styles.choice_radio_button}
+                    type='radio'
+                    {...typeProps}
+                    value='new'
+                    disabled={!this.props.mockhsm} />
+              <div className={`${styles.choice} ${styles.new} ` + (this.props.mockhsm ? '' : styles.disabled)}>
+                <span className={styles.choice_title}>Create new blockchain network</span>
+
+                {this.props.mockhsm &&
+                  <p>Start a new blockchain network with this Chain Core as the block generator.</p>
+                }
+                {!this.props.mockhsm &&
+                  <p>This core is compiled without a MockHSM. Use <code>corectl</code> to configure as a generator.</p>
+                }
+              </div>
+            </label>
+          </div>
+
+          <div className={styles.choice_wrapper}>
+            <label>
+              <input className={styles.choice_radio_button}
+                    type='radio'
+                    {...typeProps}
+                    value='join' />
+              <div className={`${styles.choice} ${styles.join}`}>
+                <span className={styles.choice_title}>Join existing blockchain network</span>
+
+                <p>
+                  Connect this Chain Core to an existing blockchain network.
+                </p>
+              </div>
+            </label>
+          </div>
+
+          <div className={styles.choice_wrapper}>
+            <label>
+              <input className={styles.choice_radio_button}
+                    type='radio'
+                    {...typeProps}
+                    value='testnet' />
+              <div className={`${styles.choice} ${styles.testnet}`}>
+                  <span className={styles.choice_title}>Join the Chain Testnet</span>
+
+                  <p>
+                    Connect this Chain Core to the Chain Testnet. <strong>Data will be reset every week.</strong>
+                  </p>
+              </div>
+            </label>
+          </div>
+        </div>
+
+        <div className={styles.choices}>
+          <div>
+            {this.showNewFields() && <span className={styles.submitWrapper}>{configSubmit}</span>}
+          </div>
+
+          <div>
+            {this.showJoinFields() && <div className={styles.joinFields}>
+              <TextField
+                title='Block Generator URL'
+                placeholder='https://<block-generator-host>'
+                fieldProps={generatorUrl} />
+              <TextField
+                title='Blockchain ID'
+                placeholder='896a800000000000000'
+                fieldProps={blockchainId} />
+              <TextField
+                title={[
+                  'Cross-core Access Token',
+                  <a href={`${docsRoot}/core/learn-more/authentication-and-authorization`} target='_blank'>
+                    <small className={styles.infoLink}>
+                      <span className='glyphicon glyphicon-info-sign'></span>
+                    </small>
+                  </a>]}
+                placeholder='token-id:9e5f139755366add8c76'
+                fieldProps={generatorAccessToken} />
+
+              {configSubmit}
+            </div>}
+          </div>
+
+          <div>
+            {this.showTestnetFields() &&
+              <span className={styles.submitWrapper}>{configSubmit}</span>
+            }
+          </div>
+        </div>
+      </form>
+    )
+  }
+}
+
+const mapStateToProps = state => ({
+  mockhsm: state.core.mockhsm,
+})
+
+const mapDispatchToProps = (dispatch) => ({
+  submitForm: (data) => dispatch(actions.configuration.submitConfiguration(data))
+})
+
+const config = {
+  form: 'coreConfigurationForm',
+  fields: [
+    'type',
+    'generatorUrl',
+    'generatorAccessToken',
+    'blockchainId'
+  ]
+}
+
+export default reduxForm(
+  config,
+  mapStateToProps,
+  mapDispatchToProps
+)(Index)
diff --git a/src/features/configuration/components/Index/Index.scss b/src/features/configuration/components/Index/Index.scss
new file mode 100644 (file)
index 0000000..ea917a0
--- /dev/null
@@ -0,0 +1,162 @@
+.title {
+  font-size: $font-size-h2;
+  font-weight: normal;
+  margin-top: $gutter-size;
+  margin-bottom: $gutter-size;
+  text-align: center;
+}
+
+.choices {
+  display: flex;
+  flex-wrap: wrap;
+  justify-content: space-between;
+
+  > div {
+    width: 30%;
+    min-height: 100%;
+  }
+}
+
+.choice_wrapper {
+  display: flex;
+  min-height: 100%;
+
+  > label {
+    display: flex;
+    min-height:100%;
+    font-weight: normal;
+  }
+}
+
+.choice_radio_button {
+  position: absolute;
+  visibility: hidden;
+}
+
+.choice {
+  border: 1px solid $table-border-color;
+  border-radius: $border-radius-base;
+  cursor: pointer;
+  min-height: 100%;
+  padding: 20px $grid-gutter-width;
+  padding-top: 110px;
+  text-align: center;
+
+  background-repeat: no-repeat;
+  background-position: center 25px;
+  background-size: 90px 90px;
+
+  &:hover {
+    background-color: $background-emphasis-color;
+  }
+
+  &.disabled {
+    cursor: default;
+    background-color: $background-emphasis-color;
+    opacity: 0.75;
+
+    .choice_title {
+      color: $text-light-color;
+    }
+  }
+
+  p {
+    line-height: 1.4;
+  }
+
+  svg {
+    display: block;
+    margin: 0 auto;
+    width: 80px;
+    height: 80px;
+  }
+
+  .choice_title{
+    display: block;
+    font-size: $font-size-section-title;
+    margin: 12px 0;
+    color: $text-strong-color;
+    font-weight: bold;
+  }
+}
+
+.new {
+  background-image: url('images/config/new.png')
+}
+
+.join {
+  background-image: url('images/config/join.png')
+}
+
+.testnet {
+  background-image: url('images/config/testnet.png')
+}
+
+input[type=radio]:checked ~ .choice {
+  strong {
+    color: $text-danger;
+  }
+
+  &:hover {
+    background-color: transparent;
+  }
+
+  &.new {
+    background-image: url('images/config/new-active.png')
+  }
+
+  &.join {
+    background-image: url('images/config/join-active.png')
+  }
+
+  &.testnet {
+    background-image: url('images/config/testnet-active.png')
+  }
+
+
+  .choice_title {
+    color: $brand-primary;
+  }
+
+  svg {
+    polygon {
+      stroke: $brand-primary;
+      fill: transparentize($brand-primary, 0.85);
+    }
+
+    rect, path {
+      fill: $brand-primary
+    }
+  }
+}
+
+.joinFields {
+  margin-top: 20px; // hack for 13" displays
+  margin-bottom: $gutter-size;
+  width: 100%;
+
+  // HACK for 13" displays
+  :global {
+    .form-group {
+      margin-bottom: 20px;
+    }
+  }
+}
+
+.submitWrapper {
+  display: block;
+  margin-top: 30px;
+}
+
+.submit {
+  display: block;
+  width: 100%;
+  margin-top: $gutter-size;
+}
+
+.infoLink {
+  position: relative;
+  left: 4px;
+  top: 1px;
+  color:$text-light-color;
+}
diff --git a/src/features/configuration/components/index.js b/src/features/configuration/components/index.js
new file mode 100644 (file)
index 0000000..f09f6fe
--- /dev/null
@@ -0,0 +1,5 @@
+import Index from './Index/Index'
+
+export {
+  Index
+}
diff --git a/src/features/configuration/index.js b/src/features/configuration/index.js
new file mode 100644 (file)
index 0000000..b133bca
--- /dev/null
@@ -0,0 +1,7 @@
+import actions from './actions'
+import routes from './routes'
+
+export {
+  actions,
+  routes,
+}
diff --git a/src/features/configuration/routes.js b/src/features/configuration/routes.js
new file mode 100644 (file)
index 0000000..6107302
--- /dev/null
@@ -0,0 +1,8 @@
+import { RoutingContainer } from 'features/shared/components'
+import { Index } from './components'
+
+export default {
+  path: 'configuration',
+  component: RoutingContainer,
+  indexRoute: { component: Index }
+}
diff --git a/src/features/core/actions.js b/src/features/core/actions.js
new file mode 100644 (file)
index 0000000..944a1f2
--- /dev/null
@@ -0,0 +1,38 @@
+import { chainClient } from 'utility/environment'
+
+const updateInfo = (param) => ({type: 'UPDATE_CORE_INFO', param})
+const setClientToken = (token) => ({type: 'SET_CLIENT_TOKEN', token})
+const clearSession = ({ type: 'USER_LOG_OUT' })
+
+const fetchCoreInfo = (options = {}) => {
+  return (dispatch) => {
+    return chainClient().config.info()
+      .then((info) => dispatch(updateInfo(info)))
+      .catch((err) => {
+        if (options.throw || !err.status) {
+          throw err
+        } else {
+          if (err.status == 401) {
+            dispatch({type: 'ERROR', payload: err})
+          } else {
+            dispatch({type: 'CORE_DISCONNECT'})
+          }
+        }
+      })
+  }
+}
+
+let actions = {
+  setClientToken,
+  updateInfo,
+  fetchCoreInfo,
+  clearSession,
+  logIn: (token) => (dispatch) => {
+    dispatch(setClientToken(token))
+    return dispatch(fetchCoreInfo({throw: true}))
+      .then(() => dispatch({type: 'USER_LOG_IN'})
+    )
+  }
+}
+
+export default actions
diff --git a/src/features/core/components/CoreIndex/CoreIndex.jsx b/src/features/core/components/CoreIndex/CoreIndex.jsx
new file mode 100644 (file)
index 0000000..4753cf1
--- /dev/null
@@ -0,0 +1,222 @@
+import { chainClient } from 'utility/environment'
+import { connect } from 'react-redux'
+import componentClassNames from 'utility/componentClassNames'
+import { PageContent, ErrorBanner, PageTitle } from 'features/shared/components'
+import React from 'react'
+import styles from './CoreIndex.scss'
+import testnetUtils from 'features/testnet/utils'
+import { docsRoot } from 'utility/environment'
+
+class CoreIndex extends React.Component {
+  constructor(props) {
+    super(props)
+    this.state = {}
+    this.deleteClick = this.deleteClick.bind(this)
+  }
+
+  deleteClick() {
+    if (!window.confirm('Are you sure you want to delete all data on this core?')) {
+      return
+    }
+
+    this.setState({deleteDisabled: true})
+
+    chainClient().config.reset(true).then(() => {
+      // TODO: Use Redux state reset and nav action instead of window.location.
+      // Also, move confirmation message to a bonafide flash div. alert() in a
+      // browser microtask is going away. cf https://www.chromestatus.com/features/5647113010544640
+      setTimeout(function(){
+        window.location.href = '/'
+      }, 500)
+    }).catch((err) => {
+      this.setState({
+        deleteError: err,
+        deleteDisabled: false,
+      })
+    })
+  }
+
+  render() {
+    const {
+      onTestnet,
+      testnetBlockchainMismatch,
+      testnetNetworkMismatch,
+      testnetNextReset,
+    } = this.props
+
+    let generatorUrl
+    if (this.props.core.generator) {
+      generatorUrl = window.location.origin
+    } else if (onTestnet) {
+      generatorUrl = <span>
+        {this.props.core.generatorUrl}
+        &nbsp;
+        <span className='label label-primary'>Chain Testnet</span>
+      </span>
+    } else {
+      generatorUrl = this.props.core.generatorUrl
+    }
+
+    let configBlock = (
+      <div className={[styles.left, styles.col].join(' ')}>
+        <div>
+          <h4>Configuration</h4>
+          <table className={styles.table}>
+            <tbody>
+              <tr>
+                <td className={styles.row_label}>Core type:</td>
+                <td>{this.props.core.coreType}</td>
+              </tr>
+              <tr>
+                <td className={styles.row_label}>Setup time:</td>
+                <td>{this.props.core.configuredAt}</td>
+              </tr>
+              <tr>
+                <td className={styles.row_label}>Version:</td>
+                <td><code>{this.props.core.version}</code></td>
+              </tr>
+              <tr>
+                <td className={styles.row_label}>MockHSM enabled:</td>
+                <td><code>{this.props.core.mockhsm.toString()}</code></td>
+              </tr>
+              <tr>
+                <td className={styles.row_label}>Localhost auth:</td>
+                <td><code>{this.props.core.localhostAuth.toString()}</code></td>
+              </tr>
+              <tr>
+                <td className={styles.row_label}>Reset enabled:</td>
+                <td><code>{this.props.core.reset.toString()}</code></td>
+              </tr>
+              <tr>
+                <td className={styles.row_label}>Non-TLS HTTP requests enabled:</td>
+                <td><code>{this.props.core.httpOk.toString()}</code></td>
+              </tr>
+              <tr>
+                <td colSpan={2}><hr /></td>
+              </tr>
+              <tr>
+                <td className={styles.row_label}>Generator URL:</td>
+                <td>{generatorUrl}</td>
+              </tr>
+              {onTestnet && !!testnetNextReset &&
+                <tr>
+                  <td className={styles.row_label}>Next Chain Testnet data reset:</td>
+                  <td>{testnetNextReset.toString()}</td>
+                </tr>}
+              {!this.props.core.generator &&
+                <tr>
+                  <td className={styles.row_label}>Generator Access Token:</td>
+                  <td><code>{this.props.core.generatorAccessToken}</code></td>
+                </tr>}
+              <tr>
+                <td className={styles.row_label}>Blockchain ID:</td>
+                <td><code className={styles.block_hash}>{this.props.core.blockchainId}</code></td>
+              </tr>
+            </tbody>
+          </table>
+        </div>
+      </div>
+    )
+
+    let testnetErr
+    if (onTestnet) {
+      if (testnetBlockchainMismatch) {
+        testnetErr = 'Chain Testnet has been reset. Please reset your core below.'
+      } else if (testnetNetworkMismatch) {
+        testnetErr = {message: <span>This core is no longer compatible with Chain Testnet. <a href={`${docsRoot}/core/get-started/install`} target='_blank'>Please upgrade Chain Core</a>.</span>}
+      }
+    }
+
+    let networkStatusBlock = (
+      <div className={[styles.right, styles.col].join(' ')}>
+        <div>
+          <h4>Network status</h4>
+
+          <table className={styles.table}>
+            <tbody>
+              <tr>
+                <td className={styles.row_label}>Generator block:</td>
+                <td className={styles.row_value}>{this.props.core.generatorBlockHeight}</td>
+              </tr>
+              <tr>
+                <td className={styles.row_label}>Local block:</td>
+                <td className={styles.row_value}>{this.props.core.blockHeight}</td>
+              </tr>
+              <tr>
+                <td className={styles.row_label}>Replication lag:</td>
+                <td className={`${styles.replication_lag} ${styles[this.props.core.replicationLagClass]}`}>
+                  {this.props.core.replicationLag === null ? '???' : this.props.core.replicationLag}
+                </td>
+              </tr>
+            </tbody>
+          </table>
+
+          {testnetErr && <ErrorBanner title='Chain Testnet error' error={testnetErr} />}
+        </div>
+      </div>
+    )
+
+    let resetDataBlock = (
+      <div className='row'>
+        <div className='col-sm-6'>
+          <h4>Reset data</h4>
+
+          {this.props.core.reset ?
+            <div>
+              <p>
+                This will permanently delete all data stored in this core,
+                including blockchain data, accounts, assets, indexes,
+                and MockHSM keys.
+              </p>
+
+              {this.state.deleteError && <ErrorBanner
+                title='Error resetting data'
+                message={this.state.deleteError.toString()}
+              />}
+
+              <button
+                className='btn btn-danger'
+                onClick={this.deleteClick}
+                disabled={this.state.deleteDisabled}
+              >
+                Delete all data
+              </button>
+            </div> :
+            <p>
+              This core is not configured with reset capabilities.
+            </p>}
+        </div>
+      </div>
+    )
+
+    return (
+      <div className={componentClassNames(this, 'flex-container', styles.mainContainer)}>
+        <PageTitle title='Core' />
+
+        <PageContent>
+          <div className={`${styles.top} ${styles.flex}`}>
+            {configBlock}
+            {networkStatusBlock}
+          </div>
+
+          {resetDataBlock}
+        </PageContent>
+      </div>
+    )
+  }
+}
+
+const mapStateToProps = (state) => ({
+  core: state.core,
+  onTestnet: state.core.onTestnet,
+  testnetBlockchainMismatch: testnetUtils.isBlockchainMismatch(state),
+  testnetNetworkMismatch: testnetUtils.isCrosscoreRpcMismatch(state),
+  testnetNextReset: state.testnet.nextReset,
+})
+
+const mapDispatchToProps = () => ({})
+
+export default connect(
+  mapStateToProps,
+  mapDispatchToProps
+)(CoreIndex)
diff --git a/src/features/core/components/CoreIndex/CoreIndex.scss b/src/features/core/components/CoreIndex/CoreIndex.scss
new file mode 100644 (file)
index 0000000..53cde20
--- /dev/null
@@ -0,0 +1,91 @@
+.page_header h1 {
+  margin-bottom: 0;
+}
+
+.table {
+  margin-bottom: $grid-gutter-width;
+
+  td {
+    vertical-align: top;
+  }
+}
+
+.row_label {
+  padding-right: $grid-gutter-width;
+  font-weight: bold;
+  white-space: pre;
+}
+
+.row_value {
+  white-space: pre;
+  text-align: right;
+}
+
+.block_hash {
+  display: block;
+  word-wrap: break-word;
+  word-break: break-all;
+}
+
+.flex {
+  display: flex;
+  overflow:hidden;
+}
+
+.col {
+  display: flex;
+  width: 50%;
+  padding: $grid-gutter-width;
+  h4 {
+    margin-top: 0;
+  }
+}
+
+h4 {
+  font-weight: bold;
+}
+
+.top {
+  border-bottom: 1px solid $border-color;
+}
+
+.left {
+  padding-left: 0;
+  width: 67%;
+}
+
+.right {
+  border-left: 1px solid $border-color;
+  width: 33%;
+}
+
+.replication_lag {
+  display: inline-block;
+  float: right;
+  border-radius: $border-radius-base;
+  color: white;
+  padding: 0 8px;
+  line-height: 1.5;
+  margin-top: 2px;
+  margin-left: -8px;
+}
+
+.green {
+  background: $highlight-secondary;
+}
+
+.yellow {
+  background: $brand-warning;
+}
+
+.red {
+  background: darken($highlight-danger-background, 20%);
+}
+
+.mainContainer {
+  background-color: $background-color;
+}
+
+code {
+  padding-left: 0;
+}
diff --git a/src/features/core/components/index.js b/src/features/core/components/index.js
new file mode 100644 (file)
index 0000000..d0fd0f5
--- /dev/null
@@ -0,0 +1,5 @@
+import CoreIndex from './CoreIndex/CoreIndex'
+
+export {
+  CoreIndex,
+}
diff --git a/src/features/core/index.js b/src/features/core/index.js
new file mode 100644 (file)
index 0000000..bf77b37
--- /dev/null
@@ -0,0 +1,9 @@
+import actions from './actions'
+import reducers from './reducers'
+import routes from './routes'
+
+export {
+  actions,
+  reducers,
+  routes,
+}
diff --git a/src/features/core/reducers.js b/src/features/core/reducers.js
new file mode 100644 (file)
index 0000000..aec428b
--- /dev/null
@@ -0,0 +1,236 @@
+import { combineReducers } from 'redux'
+import { testnetUrl } from 'utility/environment'
+import moment from 'moment'
+import { DeltaSampler } from 'utility/time'
+
+const LONG_TIME_FORMAT = 'YYYY-MM-DD, h:mm:ss a'
+
+const coreConfigReducer = (key, state, defaultState, action) => {
+  if (action.type == 'UPDATE_CORE_INFO') {
+    return action.param[key] || defaultState
+  }
+
+  return state || defaultState
+}
+
+const buildConfigReducer = (key, state, defaultState, action) => {
+  if (action.type == 'UPDATE_CORE_INFO') {
+       return action.param.buildConfig[key] || defaultState
+  }
+
+  return state || defaultState
+}
+
+const configKnown = (state = false, action) => {
+  if (action.type == 'UPDATE_CORE_INFO') {
+    return true
+  }
+  return state
+}
+
+export const configured = (state, action) =>
+  coreConfigReducer('isConfigured', state, false, action)
+export const configuredAt = (state, action) => {
+  let value = coreConfigReducer('configuredAt', state, '', action)
+  if (action.type == 'UPDATE_CORE_INFO' && value != '') {
+    value = moment(value).format(LONG_TIME_FORMAT)
+  }
+  return value
+}
+
+export const mockhsm = (state, action) =>
+  buildConfigReducer('isMockhsm', state, false, action)
+export const localhostAuth = (state, action) =>
+  buildConfigReducer('isLocalhostAuth', state, false, action)
+export const reset = (state, action) =>
+  buildConfigReducer('isReset', state, false, action)
+export const httpOk = (state, action) =>
+  buildConfigReducer('isHttpOk', state, false, action)
+export const blockHeight = (state, action) =>
+  coreConfigReducer('blockHeight', state, 0, action)
+export const generatorBlockHeight = (state, action) => {
+  if (action.type == 'UPDATE_CORE_INFO') {
+    if (action.param.generatorBlockHeight == 0) return '???'
+  }
+
+  return coreConfigReducer('generatorBlockHeight', state, 0, action)
+}
+export const signer = (state, action) =>
+  coreConfigReducer('isSigner', state, false, action)
+export const generator = (state, action) =>
+  coreConfigReducer('isGenerator', state, false, action)
+export const generatorUrl = (state, action) =>
+  coreConfigReducer('generatorUrl', state, false, action)
+export const generatorAccessToken = (state, action) =>
+  coreConfigReducer('generatorAccessToken', state, false, action)
+export const blockchainId = (state, action) =>
+  coreConfigReducer('blockchainId', state, 0, action)
+export const crosscoreRpcVersion = (state, action) =>
+  coreConfigReducer('crosscoreRpcVersion', state, 0, action)
+
+export const coreType = (state = '', action) => {
+  if (action.type == 'UPDATE_CORE_INFO') {
+    if (action.param.isGenerator) return 'Generator'
+    if (action.param.isSigner) return 'Signer'
+    return 'Participant'
+  }
+  return state
+}
+
+export const replicationLag = (state = null, action) => {
+  if (action.type == 'UPDATE_CORE_INFO') {
+    if (action.param.generatorBlockHeight == 0) {
+      return null
+    }
+    return action.param.generatorBlockHeight - action.param.blockHeight
+  }
+
+  return state
+}
+
+let syncSamplers = null
+const resetSyncSamplers = () => {
+  syncSamplers = {
+    snapshot: new DeltaSampler({sampleTtl: 10 * 1000}),
+    replicationLag: new DeltaSampler({sampleTtl: 10 * 1000}),
+  }
+}
+
+export const syncEstimates = (state = {}, action) => {
+  switch (action.type) {
+    case 'UPDATE_CORE_INFO': {
+      if (!syncSamplers) {
+        resetSyncSamplers()
+      }
+
+      const {
+        snapshot,
+        generatorBlockHeight,
+        blockHeight,
+      } = action.param
+
+      const estimates = {}
+
+      if (snapshot && snapshot.inProgress) {
+        const speed = syncSamplers.snapshot.sample(snapshot.downloaded)
+
+        if (speed != 0) {
+          estimates.snapshot = (snapshot.size - snapshot.downloaded) / speed
+        }
+      } else if (generatorBlockHeight > 0) {
+        const replicationLag = generatorBlockHeight - blockHeight
+        const speed = syncSamplers.replicationLag.sample(replicationLag)
+        if (speed != 0) {
+          const duration = -1 * replicationLag / speed
+          if (duration > 0) {
+            estimates.replicationLag = duration
+          }
+        }
+      }
+
+      return estimates
+    }
+
+    case 'CORE_DISCONNECT':
+      resetSyncSamplers()
+      return {}
+
+    default:
+      return state
+  }
+}
+
+export const replicationLagClass = (state = null, action) => {
+  if (action.type == 'UPDATE_CORE_INFO') {
+    if (action.param.generatorBlockHeight == 0) {
+      return 'red'
+    } else {
+      let lag = action.param.generatorBlockHeight - action.param.blockHeight
+      if (lag < 5) {
+        return 'green'
+      } else if (lag < 10) {
+        return 'yellow'
+      } else {
+        return 'red'
+      }
+    }
+  }
+
+  return state
+}
+
+export const onTestnet = (state = false, action) => {
+  if (action.type == 'UPDATE_CORE_INFO') {
+    return (action.param.generatorUrl || '').indexOf(testnetUrl) >= 0
+  }
+
+  return state
+}
+
+export const requireClientToken = (state = false, action) => {
+  if (action.type == 'ERROR' && action.payload.status == 401) return true
+
+  return state
+}
+
+export const clientToken = (state = '', action) => {
+  if      (action.type == 'SET_CLIENT_TOKEN') return action.token
+  else if (action.type == 'ERROR' &&
+           action.payload.status == 401)      return ''
+
+  return state
+}
+
+export const validToken = (state = false, action) => {
+  if      (action.type == 'SET_CLIENT_TOKEN') return false
+  else if (action.type == 'USER_LOG_IN')      return true
+  else if (action.type == 'ERROR' &&
+           action.payload.status == 401)      return false
+
+  return state
+}
+
+export const connected = (state = true, action) => {
+  if      (action.type == 'UPDATE_CORE_INFO') return true
+  else if (action.type == 'CORE_DISCONNECT')  return false
+
+  return state
+}
+
+const snapshot = (state = null, action) => {
+  if (action.type == 'UPDATE_CORE_INFO') {
+    return action.param.snapshot || null // snapshot may be undefined, which Redux doesn't like.
+  }
+  return state
+}
+
+const version = (state, action) => coreConfigReducer('version', state, 'N/A', action)
+
+export default combineReducers({
+  blockchainId,
+  blockHeight,
+  connected,
+  clientToken,
+  configKnown,
+  configured,
+  configuredAt,
+  coreType,
+  generator,
+  generatorAccessToken,
+  generatorBlockHeight,
+  generatorUrl,
+  localhostAuth,
+  mockhsm,
+  crosscoreRpcVersion,
+  onTestnet,
+  httpOk,
+  replicationLag,
+  replicationLagClass,
+  requireClientToken,
+  reset,
+  signer,
+  snapshot,
+  syncEstimates,
+  validToken,
+  version,
+})
diff --git a/src/features/core/routes.js b/src/features/core/routes.js
new file mode 100644 (file)
index 0000000..86981d9
--- /dev/null
@@ -0,0 +1,8 @@
+import { RoutingContainer } from 'features/shared/components'
+import { CoreIndex } from './components'
+
+export default {
+  path: 'core',
+  component: RoutingContainer,
+  indexRoute: { component: CoreIndex }
+}
diff --git a/src/features/mockhsm/actions.js b/src/features/mockhsm/actions.js
new file mode 100644 (file)
index 0000000..29860a2
--- /dev/null
@@ -0,0 +1,16 @@
+import { baseListActions, baseCreateActions } from 'features/shared/actions'
+import { chainClient } from 'utility/environment'
+
+const type = 'mockhsm'
+const clientApi = () => chainClient().mockHsm.keys
+
+export default {
+  ...baseCreateActions(type, {
+    className: 'MockHsm',
+    clientApi,
+  }),
+  ...baseListActions(type, {
+    className: 'MockHsm',
+    clientApi,
+  }),
+}
diff --git a/src/features/mockhsm/components/List.jsx b/src/features/mockhsm/components/List.jsx
new file mode 100644 (file)
index 0000000..98f355a
--- /dev/null
@@ -0,0 +1,17 @@
+import React from 'react'
+import { BaseList, TableList } from 'features/shared/components'
+import ListItem from './ListItem'
+
+const type = 'mockhsm'
+
+export default BaseList.connect(
+  BaseList.mapStateToProps(type, ListItem, {
+    skipQuery: true,
+    label: 'MockHSM keys',
+    wrapperComponent: TableList,
+    wrapperProps: {
+      titles: ['Alias', 'xpub']
+    }
+  }),
+  BaseList.mapDispatchToProps(type)
+)
diff --git a/src/features/mockhsm/components/ListItem.jsx b/src/features/mockhsm/components/ListItem.jsx
new file mode 100644 (file)
index 0000000..5ae787e
--- /dev/null
@@ -0,0 +1,17 @@
+import React from 'react'
+
+class ListItem extends React.Component {
+  render() {
+    const item = this.props.item
+
+    return(
+      <tr>
+        <td>{item.alias}</td>
+        <td><code>{item.xpub}</code></td>
+        <td></td>
+      </tr>
+    )
+  }
+}
+
+export default ListItem
diff --git a/src/features/mockhsm/components/New.jsx b/src/features/mockhsm/components/New.jsx
new file mode 100644 (file)
index 0000000..7408fa7
--- /dev/null
@@ -0,0 +1,50 @@
+import React from 'react'
+import { BaseNew, FormContainer, FormSection, TextField } from 'features/shared/components'
+import { reduxForm } from 'redux-form'
+
+class New extends React.Component {
+  constructor(props) {
+    super(props)
+
+    this.submitWithErrors = this.submitWithErrors.bind(this)
+  }
+
+  submitWithErrors(data) {
+    return new Promise((resolve, reject) => {
+      this.props.submitForm(data)
+        .catch((err) => reject({_error: err}))
+    })
+  }
+
+  render() {
+    const {
+      fields: { alias },
+      error,
+      handleSubmit,
+      submitting
+    } = this.props
+
+    return(
+      <FormContainer
+        error={error}
+        label='New MockHSM key'
+        onSubmit={handleSubmit(this.submitWithErrors)}
+        submitting={submitting} >
+
+        <FormSection title='Key Information'>
+          <TextField title='Alias' placeholder='Alias' fieldProps={alias} autoFocus={true} />
+        </FormSection>
+      </FormContainer>
+    )
+  }
+}
+
+const fields = [ 'alias' ]
+export default BaseNew.connect(
+  BaseNew.mapStateToProps('mockhsm'),
+  BaseNew.mapDispatchToProps('mockhsm'),
+  reduxForm({
+    form: 'newMockHsmKey',
+    fields,
+  })(New)
+)
diff --git a/src/features/mockhsm/components/index.js b/src/features/mockhsm/components/index.js
new file mode 100644 (file)
index 0000000..d57a1f8
--- /dev/null
@@ -0,0 +1,7 @@
+import List from './List'
+import New from './New'
+
+export {
+  List,
+  New,
+}
diff --git a/src/features/mockhsm/index.js b/src/features/mockhsm/index.js
new file mode 100644 (file)
index 0000000..bf77b37
--- /dev/null
@@ -0,0 +1,9 @@
+import actions from './actions'
+import reducers from './reducers'
+import routes from './routes'
+
+export {
+  actions,
+  reducers,
+  routes,
+}
diff --git a/src/features/mockhsm/reducers.js b/src/features/mockhsm/reducers.js
new file mode 100644 (file)
index 0000000..dd813d7
--- /dev/null
@@ -0,0 +1,11 @@
+import { reducers } from 'features/shared'
+import { combineReducers } from 'redux'
+
+const type = 'mockhsm'
+const idFunc = item => item.xpub
+
+export default combineReducers({
+  items: reducers.itemsReducer(type, idFunc),
+  queries: reducers.queriesReducer(type, idFunc),
+  autocompleteIsLoaded: reducers.autocompleteIsLoadedReducer(type),
+})
diff --git a/src/features/mockhsm/routes.js b/src/features/mockhsm/routes.js
new file mode 100644 (file)
index 0000000..a740543
--- /dev/null
@@ -0,0 +1,4 @@
+import { List, New } from './components'
+import { makeRoutes } from 'features/shared'
+
+export default (store) => makeRoutes(store, 'mockhsm', List, New, null, null, { skipFilter: true, name: 'MockHSM keys' })
diff --git a/src/features/shared/actions/create.js b/src/features/shared/actions/create.js
new file mode 100644 (file)
index 0000000..f3f66ac
--- /dev/null
@@ -0,0 +1,80 @@
+import { chainClient } from 'utility/environment'
+import { parseNonblankJSON } from 'utility/string'
+import { push } from 'react-router-redux'
+import actions from 'actions'
+import uuid from 'uuid'
+
+export default function(type, options = {}) {
+  const listPath = options.listPath || `/${type}s`
+  const createPath = options.createPath || `${listPath}/create`
+  const created = (param) => ({ type: `CREATED_${type.toUpperCase()}`, param })
+
+  return {
+    showCreate: push(createPath),
+    created,
+    submitForm: (data) => {
+      const clientApi = options.clientApi ? options.clientApi() : chainClient()[`${type}s`]
+      let promise = Promise.resolve()
+
+      if (typeof data.id == 'string')     data.id = data.id.trim()
+      if (typeof data.alias == 'string')  data.alias = data.alias.trim()
+
+      const jsonFields = options.jsonFields || []
+      jsonFields.map(fieldName => {
+        data[fieldName] = parseNonblankJSON(data[fieldName])
+      })
+
+      const intFields = options.intFields || []
+      intFields.map(fieldName => {
+        data[fieldName] = parseInt(data[fieldName])
+      })
+
+      if (data.xpubs) {
+        data.rootXpubs = []
+        data.xpubs.forEach(key => {
+          if (key.type == 'generate') {
+            promise = promise
+              .then(() => {
+                const alias = (key.value || '').trim()
+                  ? key.value.trim()
+                  : (data.alias || 'generated') + '-' + uuid.v4()
+
+                return chainClient().mockHsm.keys.create({alias})
+              }).then(newKey => {
+                data.rootXpubs.push(newKey.xpub)
+              })
+          } else if (key.value) {
+            data.rootXpubs.push(key.value)
+          }
+        })
+        delete data.xpubs
+      }
+
+      return function(dispatch) {
+        return promise.then(() => clientApi.create(data)
+          .then((resp) => {
+            dispatch(created(resp))
+
+            if (options.createModal) {
+              dispatch(actions.app.showModal(
+                options.createModal(resp),
+                actions.app.hideModal
+              ))
+            }
+
+            let postCreatePath = listPath
+            if (options.redirectToShow) {
+              postCreatePath = `${postCreatePath}/${resp.id}`
+            }
+
+            dispatch(push({
+              pathname: postCreatePath,
+              state: {
+                preserveFlash: true
+              }
+            }))
+          }))
+      }
+    }
+  }
+}
diff --git a/src/features/shared/actions/index.js b/src/features/shared/actions/index.js
new file mode 100644 (file)
index 0000000..a31244c
--- /dev/null
@@ -0,0 +1,9 @@
+import baseCreateActions from './create'
+import baseUpdateActions from './update'
+import baseListActions from './list'
+
+export {
+  baseCreateActions,
+  baseUpdateActions,
+  baseListActions,
+}
diff --git a/src/features/shared/actions/list.js b/src/features/shared/actions/list.js
new file mode 100644 (file)
index 0000000..7014580
--- /dev/null
@@ -0,0 +1,170 @@
+import { chainClient } from 'utility/environment'
+import { pageSize } from 'utility/environment'
+import { push, replace } from 'react-router-redux'
+
+export default function(type, options = {}) {
+  const className = options.className || type.charAt(0).toUpperCase() + type.slice(1)
+  const listPath  = options.listPath || `/${type}s`
+  const clientApi = () => options.clientApi ? options.clientApi() : chainClient()[`${type}s`]
+
+  const receive = (param) => ({
+    type: `RECEIVED_${type.toUpperCase()}_ITEMS`,
+    param,
+  })
+
+  // Dispatch a single request for the specified query, and persist the
+  // results to the default item store
+  const fetchItems = (params) => {
+    const requiredParams = options.requiredParams || {}
+
+    params = { ...params, ...requiredParams }
+
+    return (dispatch) => {
+      const promise = clientApi().query(params)
+
+      promise.then(
+        (resp) => dispatch(receive(resp))
+      )
+
+      return promise
+    }
+  }
+
+  // Fetch all items up to the specified page, and persist the results to
+  // the filter-specific store
+  const fetchPage = (query, pageNumber = 1, options = {}) => {
+    const getPageSlice = (list, page) => {
+      const pageStart = page * pageSize
+      return (list.itemIds || []).slice(pageStart, pageStart + pageSize)
+    }
+
+    const listId =  query.filter || ''
+    pageNumber = parseInt(pageNumber || 1)
+
+    return (dispatch, getState) => {
+      const getFilterStore = () => getState()[type].queries[listId] || {}
+
+      const fullPage = () => {
+        // Return early to load all pages if -1 is passed
+        if (pageNumber == -1) return
+
+        const list = getFilterStore()
+        const currentPage = getPageSlice(list, pageNumber)
+        return currentPage.length == pageSize
+      }
+
+      if (!options.refresh && fullPage()) return Promise.resolve({})
+
+      const fetchNextPage = () =>
+        dispatch(_load(query, getFilterStore(), options)).then((resp) => {
+          if (!resp || resp.type == 'ERROR') return
+
+          if (resp && resp.last) {
+            return Promise.resolve(resp)
+          } else if (!fullPage()) {
+            options.refresh = false
+            return dispatch(fetchNextPage)
+          }
+        })
+
+      return dispatch(fetchNextPage)
+    }
+  }
+
+  // Fetch and persist all records of the current object type
+  const fetchAll = () => {
+    return fetchPage('', -1)
+  }
+
+  const _load = function(query = {}, list = {}, requestOptions) {
+    return function(dispatch) {
+      const latestResponse = list.cursor || null
+      const refresh = requestOptions.refresh || false
+
+      if (!refresh && latestResponse && latestResponse.lastPage) {
+        return Promise.resolve({last: true})
+      }
+
+      let promise
+      const filter = query.filter || ''
+
+      if (!refresh && latestResponse) {
+        let responsePage
+        promise = latestResponse.nextPage()
+          .then(resp => {
+            responsePage = resp
+            return dispatch(receive(responsePage))
+          }).then(() =>
+            responsePage
+          )
+      } else {
+        const params = {}
+        if (query.filter) params.filter = filter
+        if (query.sumBy) params.sumBy = query.sumBy.split(',')
+
+        promise = dispatch(fetchItems(params))
+      }
+
+      return promise.then((response) => {
+        return dispatch({
+          type: `APPEND_${type.toUpperCase()}_PAGE`,
+          param: response,
+          refresh: refresh,
+        })
+      }).catch(err => {
+        if (options.defaultKey && filter.indexOf('\'') < 0 && filter.indexOf('=') < 0) {
+          dispatch(pushList({
+            filter: `${options.defaultKey}='${query.filter}'`
+          }, null, {replace: true}))
+        } else {
+          return dispatch({type: 'ERROR', payload: err})
+        }
+      })
+    }
+  }
+
+  const deleteItem = (id, confirmMessage, deleteMessage) => {
+    return (dispatch) => {
+      if (!window.confirm(confirmMessage)) {
+        return
+      }
+
+      clientApi().delete(id)
+        .then(() => dispatch({
+          type: `DELETE_${type.toUpperCase()}`,
+          id: id,
+          message: deleteMessage,
+        })).catch(err => dispatch({
+          type: 'ERROR', payload: err
+        }))
+    }
+  }
+
+  const pushList = (query = {}, pageNumber, options = {}) => {
+    if (pageNumber) {
+      query = {
+        ...query,
+        page: pageNumber,
+      }
+    }
+
+    const location = {
+      pathname: listPath,
+      query
+    }
+
+    if (options.replace) return replace(location)
+    return push(location)
+  }
+
+  return {
+    fetchItems,
+    fetchPage,
+    fetchAll,
+    deleteItem,
+    pushList,
+    didLoadAutocomplete: {
+      type: `DID_LOAD_${type.toUpperCase()}_AUTOCOMPLETE`
+    },
+  }
+}
diff --git a/src/features/shared/actions/update.js b/src/features/shared/actions/update.js
new file mode 100644 (file)
index 0000000..3b0b458
--- /dev/null
@@ -0,0 +1,30 @@
+import { chainClient } from 'utility/environment'
+import { push } from 'react-router-redux'
+
+export default function(type, options = {}) {
+  const updated = (param) => ({ type: `UPDATED_${type.toUpperCase()}`, param })
+
+  return {
+    updated,
+    submitUpdateForm: (data, id) => {
+      const clientApi = options.clientApi ? options.clientApi() : chainClient()[`${type}s`]
+      let promise = Promise.resolve()
+
+      return function(dispatch) {
+        return promise.then(() => clientApi.updateTags({
+          id: id,
+          tags: JSON.parse(data.tags),
+        }).then((resp) => {
+          dispatch(updated(resp))
+
+          dispatch(push({
+            pathname: `/${type}s/${id}`,
+            state: {
+              preserveFlash: true
+            }
+          }))
+        }))
+      }
+    }
+  }
+}
diff --git a/src/features/shared/components/Autocomplete/AccountAlias.jsx b/src/features/shared/components/Autocomplete/AccountAlias.jsx
new file mode 100644 (file)
index 0000000..ed966c9
--- /dev/null
@@ -0,0 +1,9 @@
+import { connect } from 'react-redux'
+import AutocompleteField, {mapStateToProps, mapDispatchToProps} from './AutocompleteField'
+
+const type = 'account'
+
+export default connect(
+  mapStateToProps(type),
+  mapDispatchToProps(type)
+)(AutocompleteField)
diff --git a/src/features/shared/components/Autocomplete/AssetAlias.jsx b/src/features/shared/components/Autocomplete/AssetAlias.jsx
new file mode 100644 (file)
index 0000000..2582095
--- /dev/null
@@ -0,0 +1,9 @@
+import { connect } from 'react-redux'
+import AutocompleteField, {mapStateToProps, mapDispatchToProps} from './AutocompleteField'
+
+const type = 'asset'
+
+export default connect(
+  mapStateToProps(type),
+  mapDispatchToProps(type)
+)(AutocompleteField)
diff --git a/src/features/shared/components/Autocomplete/AutocompleteField.jsx b/src/features/shared/components/Autocomplete/AutocompleteField.jsx
new file mode 100644 (file)
index 0000000..f6e6209
--- /dev/null
@@ -0,0 +1,116 @@
+import React from 'react'
+import styles from './AutocompleteField.scss'
+import Autosuggest from 'react-autosuggest'
+import actions from 'actions'
+
+class AutocompleteField extends React.Component {
+  constructor() {
+    super()
+
+    this.state = {
+      suggestions: []
+    }
+
+    this.getSuggestionValue = this.getSuggestionValue.bind(this)
+    this.renderSuggestion = this.renderSuggestion.bind(this)
+    this.onSuggestionsFetchRequested = this.onSuggestionsFetchRequested.bind(this)
+    this.onSuggestionsClearRequested = this.onSuggestionsClearRequested.bind(this)
+  }
+
+  getSuggestions(value) {
+    const escapedValue = (value.trim()).replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
+
+    if (escapedValue === '') {
+      return []
+    }
+
+    const regex = new RegExp('^' + escapedValue, 'i')
+
+    const suggestions = this.props.items.filter(item => regex.test(item.alias))
+    suggestions.sort((a,b) => a.alias.localeCompare(b.alias))
+
+    return suggestions
+  }
+
+  getSuggestionValue(suggestion) {
+    return suggestion.alias
+  }
+
+  renderSuggestion(suggestion) {
+    return (
+      <div onMouseOver={() => this.props.fieldProps.onChange(suggestion.alias)}>
+        <span>{suggestion.alias}</span>
+      </div>
+    )
+  }
+
+  onSuggestionsFetchRequested({ value }) {
+    if (this.props.autocompleteIsLoaded) {
+      this.setState({suggestions: this.getSuggestions(value)})
+    } else {
+      this.props.fetchAll().then(() => {
+        this.setState({suggestions: this.getSuggestions(value)})
+        this.props.didLoadAutocomplete()
+      })
+    }
+  }
+
+  onSuggestionsClearRequested() {
+    this.setState({
+      suggestions: []
+    })
+  }
+
+  keyCheck(event) {
+    // Fills input with top suggestion if suggestions are present and key
+    // pressed was either tab (keyCode 9), or enter/return (keyCode 13)
+    const suggestions = this.state.suggestions
+    if (suggestions.length > 0 && (event.keyCode == 9 || event.keyCode == 13)) {
+
+      // Prevent form submission if key pressed was enter/return
+      event.keyCode == 13 && event.preventDefault()
+
+      const suggestion = suggestions[0]['alias']
+      const input = this.props.fieldProps.value.toLowerCase()
+      if (suggestion.toLowerCase().startsWith(input)) {
+        this.props.fieldProps.onChange(suggestion)
+      }
+    }
+  }
+
+  render() {
+    const { suggestions } = this.state
+    const { fieldProps } = this.props
+
+    return (
+      <Autosuggest
+        theme={styles}
+        suggestions={suggestions}
+        onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
+        onSuggestionsClearRequested={this.onSuggestionsClearRequested}
+        onSuggestionSelected={(event) => event.preventDefault()}
+        getSuggestionValue={this.getSuggestionValue}
+        renderSuggestion={this.renderSuggestion}
+        focusFirstSuggestion={true}
+        inputProps={{
+          className: `form-control ${this.props.className}`,
+          value: fieldProps.value,
+          placeholder: this.props.placeholder,
+          onChange: (event, { newValue }) => fieldProps.onChange(newValue),
+          onKeyDown: (event) => this.keyCheck(event)}}
+      />
+    )
+  }
+}
+
+export default AutocompleteField
+
+export const mapStateToProps = (type) => (state) => ({
+  autocompleteIsLoaded: state[type].autocompleteIsLoaded,
+  items: Object.keys(state[type].items).map(k => state[type].items[k])
+})
+
+export const mapDispatchToProps = (type) => (dispatch) => ({
+  didLoadAutocomplete: () => dispatch(actions[type].didLoadAutocomplete),
+  fetchAll: () => dispatch(actions[type].fetchAll())
+})
diff --git a/src/features/shared/components/Autocomplete/AutocompleteField.scss b/src/features/shared/components/Autocomplete/AutocompleteField.scss
new file mode 100644 (file)
index 0000000..00b7484
--- /dev/null
@@ -0,0 +1,42 @@
+.suggestionsContainer {
+  position: relative;
+}
+
+.suggestionsList {
+  background: $body-bg;
+  border: 1px solid $input-border;
+  border-radius: $input-border-radius-large;
+  box-shadow: 0px 0px 4px transparentize($text-color, 0.5);
+  list-style-type: none;
+  margin-top: 6px;
+  max-height: 200px;
+  overflow-y: scroll;
+  padding: 0;
+  position: absolute;
+  top: 40px;
+  width: 100%;
+  z-index: 10;
+}
+
+.suggestion {
+  border-bottom: 1px solid $input-border;
+
+  &:last-child {
+    border: none;
+  }
+
+  div {
+    padding: 0;
+  }
+
+  span {
+    padding: 12px;
+    display: inline-block;
+    width: 100%;
+  }
+}
+
+.suggestionFocused {
+  color: $brand-primary;
+  background: #e6e6e6;
+}
diff --git a/src/features/shared/components/Autocomplete/index.js b/src/features/shared/components/Autocomplete/index.js
new file mode 100644 (file)
index 0000000..936c3a5
--- /dev/null
@@ -0,0 +1,7 @@
+import AccountAlias from './AccountAlias'
+import AssetAlias from './AssetAlias'
+
+export default {
+  AccountAlias,
+  AssetAlias
+}
diff --git a/src/features/shared/components/BaseList/BaseList.jsx b/src/features/shared/components/BaseList/BaseList.jsx
new file mode 100644 (file)
index 0000000..5469fa3
--- /dev/null
@@ -0,0 +1,144 @@
+import React from 'react'
+import actions from 'actions'
+import { connect as reduxConnect } from 'react-redux'
+import { pluralize, capitalize, humanize } from 'utility/string'
+import componentClassNames from 'utility/componentClassNames'
+import { PageContent, PageTitle, Pagination, SearchBar } from '../'
+import EmptyList from './EmptyList'
+import { pageSize } from 'utility/environment'
+
+class ItemList extends React.Component {
+  render() {
+    const label = this.props.label || pluralize(humanize(this.props.type))
+    const objectName = label.slice(0,-1)
+    const actions = [...(this.props.actions || [])]
+
+    const labelTitleMap = {
+      transactions: '交易',
+      accounts: '账户',
+      assets: '资产',
+      balances: '账单',
+      'unspent outputs': 'UTXO'
+    }
+    const title = labelTitleMap[label] || capitalize(label)
+
+    const newButton = <button key='showCreate' className='btn btn-primary' onClick={this.props.showCreate}>
+      + {'新建'}
+    </button>
+    if (!this.props.skipCreate) {
+      actions.push(newButton)
+    }
+
+    let header = <div>
+      <PageTitle
+        title={title}
+        actions={actions}
+      />
+
+      {!this.props.skipQuery && !this.props.showFirstTimeFlow &&
+        <SearchBar key='search-bar'
+          {...this.props.searchState}
+          pushList={this.props.pushList}
+          currentFilter={this.props.currentFilter}
+          defaultFilter={this.props.defaultFilter}
+        />}
+    </div>
+
+    const rootClassNames = componentClassNames(this, 'flex-container')
+
+    if (this.props.noResults) {
+      return(
+        <div className={rootClassNames}>
+          {header}
+
+          <EmptyList
+            firstTimeContent={this.props.firstTimeContent}
+            type={this.props.type}
+            objectName={objectName}
+            newButton={newButton}
+            showFirstTimeFlow={this.props.showFirstTimeFlow}
+            skipCreate={this.props.skipCreate}
+            loadedOnce={this.props.loadedOnce}
+            currentFilter={this.props.currentFilter} />
+
+        </div>
+      )
+    } else {
+      let pagination = <Pagination
+          currentPage={this.props.currentPage}
+          currentFilter={this.props.currentFilter}
+          isLastPage={this.props.isLastPage}
+          pushList={this.props.pushList} />
+
+      const items = this.props.items.map((item) =>
+        <this.props.listItemComponent key={item.id} item={item} {...this.props.itemActions}/>)
+      const Wrapper = this.props.wrapperComponent
+
+      return(
+        <div className={rootClassNames}>
+          {header}
+
+          <PageContent>
+            {Wrapper ? <Wrapper {...this.props.wrapperProps}>{items}</Wrapper> : items}
+
+            {pagination}
+          </PageContent>
+        </div>
+      )
+    }
+  }
+}
+
+export const mapStateToProps = (type, itemComponent, additionalProps = {}) => (state, ownProps) => {
+  const currentPage = Math.max(parseInt(ownProps.location.query.page) || 1, 1)
+  // TODO: this should be renamed `currentQuery`; we should
+  // do some renaminng in here
+  const currentFilter = ownProps.location.query || {}
+  const currentQueryString = currentFilter.filter || ''
+  const currentQuery = state[type].queries[currentQueryString] || {}
+  const currentIds = currentQuery.itemIds || []
+  const cursor = currentQuery.cursor || {}
+
+  const lastPageIndex = Math.ceil(currentIds.length/pageSize) - 1
+  const isLastPage = ((currentPage - 1) == lastPageIndex) && cursor && cursor.lastPage
+  const startIndex = (currentPage - 1) * pageSize
+  const items = currentIds.slice(startIndex, startIndex + pageSize).map(
+    id => state[type].items[id]
+  ).filter(item => item != undefined)
+
+  return {
+    currentPage: currentPage,
+    currentFilter: currentFilter,
+    items: items,
+    isLastPage: isLastPage,
+
+    loadedOnce: Object.keys(state[type].queries).length > 0,
+    type: type,
+    listItemComponent: itemComponent,
+    searchState: { queryTime: currentQuery.queryTime },
+
+    noResults: items.length == 0,
+    showFirstTimeFlow: items.length == 0 && currentQueryString == '',
+
+    ...additionalProps
+  }
+}
+
+export const mapDispatchToProps = (type) => (dispatch) => {
+  return {
+    pushList: (query, pageNumber) => dispatch(actions[type].pushList(query, pageNumber)),
+    showCreate: () => dispatch(actions[type].showCreate),
+  }
+}
+
+export const connect = (state, dispatch, component = ItemList) => reduxConnect(
+  state,
+  dispatch
+)(component)
+
+export default {
+  mapStateToProps,
+  mapDispatchToProps,
+  connect,
+  ItemList,
+}
diff --git a/src/features/shared/components/BaseList/EmptyList.jsx b/src/features/shared/components/BaseList/EmptyList.jsx
new file mode 100644 (file)
index 0000000..6ebfd51
--- /dev/null
@@ -0,0 +1,58 @@
+import React from 'react'
+import styles from './EmptyList.scss'
+import componentClassNames from 'utility/componentClassNames'
+import { docsRoot } from 'utility/environment'
+
+class EmptyList extends React.Component {
+  render() {
+    let emptyImage
+
+    try {
+      emptyImage = require(`images/empty/${this.props.type}.svg`)
+    } catch (err) { /* do nothing */ }
+
+    let emptyBlock
+    if (!this.props.loadedOnce) {
+      emptyBlock = <span>LOADING…</span>
+    } else if (this.props.showFirstTimeFlow) {
+      emptyBlock = <div>
+        <span className={`${styles.emptyLabel} ${styles.noResultsLabel}`}>
+          There are no {this.props.objectName}s
+        </span>
+        {this.props.firstTimeContent}
+      </div>
+    } else if (!this.props.showFirstTimeFlow) {
+      emptyBlock = <div className={styles.emptyContainer}>
+        <span className={`${styles.emptyLabel} ${styles.noResultsLabel}`}>No results for query:</span>
+        <code className={styles.code}>{this.props.currentFilter.filter}</code>
+      </div>
+    }
+
+    const classNames = [
+      'flex-container',
+      styles.empty,
+      {[styles.noResults]: !this.props.showFirstTimeFlow}
+    ]
+
+    return (
+      <div className={componentClassNames(this, ...classNames)}>
+        {emptyImage && <img className={styles.image} src={emptyImage} />}
+        {emptyBlock}
+      </div>
+    )
+  }
+}
+
+EmptyList.propTypes = {
+  type: React.PropTypes.string,
+  objectName: React.PropTypes.string,
+  newButton: React.PropTypes.object,
+  noRecords: React.PropTypes.bool,
+  skipCreate: React.PropTypes.bool,
+  loadedOnce: React.PropTypes.bool,
+  currentFilter: React.PropTypes.object,
+  showFirstTimeFlow: React.PropTypes.bool,
+  firstTimeContent: React.PropTypes.object
+}
+
+export default EmptyList
diff --git a/src/features/shared/components/BaseList/EmptyList.scss b/src/features/shared/components/BaseList/EmptyList.scss
new file mode 100644 (file)
index 0000000..7bcc829
--- /dev/null
@@ -0,0 +1,59 @@
+.empty {
+  text-align: center;
+  display: flex;
+  flex-direction: column;
+  justify-content: flex-start;
+  padding-top: 100px;
+}
+
+.emptyContainer {
+  margin: 0 auto;
+  width: 350px;
+
+  .emptyContent {
+    margin-top: $gutter-size;
+    border: 1px solid $border-color;
+    padding: $gutter-size/2;
+    text-align: left;
+    background: $background-color;
+  }
+
+  ol {
+    padding-left: $gutter-size/2;
+    margin-bottom: 0px;
+    padding-top: 10px;
+    padding-bottom: 10px;
+  }
+
+  li {
+    padding-left: 6px;
+  }
+}
+
+.emptyLabel {
+  color: $text-strong-color;
+  display: block;
+  font-size: $font-size-page-title;
+  padding-bottom: $gutter-size;
+  width: 250px;
+  margin: 0 auto;
+  line-height: 26px;
+}
+
+.noResults {
+  background: transparent;
+}
+
+.noResultsLabel {
+  padding-bottom: 0;
+}
+
+.image {
+  height: 70px;
+  margin-bottom: $gutter-size;
+}
+
+.code {
+  padding: 2px 6px;
+  font-size: $font-size-section-title;
+}
diff --git a/src/features/shared/components/BaseNew.jsx b/src/features/shared/components/BaseNew.jsx
new file mode 100644 (file)
index 0000000..57a9629
--- /dev/null
@@ -0,0 +1,26 @@
+import { connect as reduxConnect } from 'react-redux'
+import actions from 'actions'
+
+export const mapStateToProps = ( type ) => ( /* state */ ) => ({
+  type: type,
+})
+
+export const mapDispatchToProps = (type) => (dispatch) => ({
+  submitForm: (data) => {
+    return dispatch(actions[type].submitForm(data)).then((resp) => {
+      dispatch(actions.tutorial.submitTutorialForm(data, type))
+      return resp
+    })
+  }
+})
+
+export const connect = (state, dispatch, element) => reduxConnect(
+  state,
+  dispatch
+)(element)
+
+export default {
+  mapStateToProps,
+  mapDispatchToProps,
+  connect,
+}
diff --git a/src/features/shared/components/BaseShow.jsx b/src/features/shared/components/BaseShow.jsx
new file mode 100644 (file)
index 0000000..4411c9c
--- /dev/null
@@ -0,0 +1,28 @@
+import React from 'react'
+import NotFound from './NotFound'
+
+export default class BaseShow extends React.Component {
+  constructor(props) {
+    super(props)
+
+    this.state = {}
+  }
+
+  componentDidMount() {
+    this.props.fetchItem(this.props.params.id).then(resp => {
+      if (resp.items.length == 0) {
+        this.setState({notFound: true})
+      }
+    })
+  }
+
+  renderIfFound(view) {
+    if (this.state.notFound) {
+      return(<NotFound />)
+    } else if (view) {
+      return(view)
+    } else {
+      return(<div>Loading...</div>)
+    }
+  }
+}
diff --git a/src/features/shared/components/BaseUpdate.jsx b/src/features/shared/components/BaseUpdate.jsx
new file mode 100644 (file)
index 0000000..a1e0ab0
--- /dev/null
@@ -0,0 +1,24 @@
+import { connect as reduxConnect } from 'react-redux'
+import actions from 'actions'
+
+export const mapStateToProps = ( type ) => ( /* state */ ) => ({
+  type: type,
+})
+
+export const mapDispatchToProps = (type) => (dispatch) => ({
+  fetchItem: (id) => dispatch(actions[type].fetchItems({filter: `id='${id}'`})).then((resp) => {
+    return resp
+  }),
+  submitForm: (data, id) => dispatch(actions[type].submitUpdateForm(data, id))
+})
+
+export const connect = (state, dispatch, element) => reduxConnect(
+  state,
+  dispatch
+)(element)
+
+export default {
+  mapStateToProps,
+  mapDispatchToProps,
+  connect,
+}
diff --git a/src/features/shared/components/CheckboxField/CheckboxField.jsx b/src/features/shared/components/CheckboxField/CheckboxField.jsx
new file mode 100644 (file)
index 0000000..833cb28
--- /dev/null
@@ -0,0 +1,32 @@
+import React from 'react'
+import pick from 'lodash/pick'
+import styles from './CheckboxField.scss'
+
+const CHECKBOX_FIELD_PROPS = [
+  'value',
+  'onBlur',
+  'onChange',
+  'onFocus',
+  'name',
+  'checked',
+  'disabled'
+]
+
+class CheckboxField extends React.Component {
+  render() {
+    const fieldProps = pick(this.props.fieldProps, CHECKBOX_FIELD_PROPS)
+
+    return (
+      <div>
+        <label className={styles.label}>
+          <input type='checkbox' {...fieldProps} />
+          <span className={styles.title}>{this.props.title}</span>
+
+          {this.props.hint && <div className={styles.hint}>{this.props.hint}</div>}
+        </label>
+      </div>
+    )
+  }
+}
+
+export default CheckboxField
diff --git a/src/features/shared/components/CheckboxField/CheckboxField.scss b/src/features/shared/components/CheckboxField/CheckboxField.scss
new file mode 100644 (file)
index 0000000..6a93eeb
--- /dev/null
@@ -0,0 +1,15 @@
+.label {
+  position: relative;
+
+  .title {
+    position: absolute;
+    left: 20px;
+  }
+
+  user-select: none;
+}
+
+.hint {
+  color: $text-light-color;
+  margin-left: 20px;
+}
diff --git a/src/features/shared/components/CopyableBlock/CopyableBlock.jsx b/src/features/shared/components/CopyableBlock/CopyableBlock.jsx
new file mode 100644 (file)
index 0000000..a71fad7
--- /dev/null
@@ -0,0 +1,22 @@
+import React from 'react'
+import { copyToClipboard } from 'utility/clipboard'
+import styles from './CopyableBlock.scss'
+
+class CopyableBlock extends React.Component {
+  copyClick() {
+    copyToClipboard(this.props.value)
+  }
+
+  render() {
+    return (
+      <div className={styles.main}>
+        <pre className={styles.pre}>{this.props.value}</pre>
+        <div className={styles.copyButton}>
+          <button className='btn btn-default btn-sm' onClick={this.copyClick.bind(this)}>Copy to clipboard</button>
+        </div>
+      </div>
+    )
+  }
+}
+
+export default CopyableBlock
diff --git a/src/features/shared/components/CopyableBlock/CopyableBlock.scss b/src/features/shared/components/CopyableBlock/CopyableBlock.scss
new file mode 100644 (file)
index 0000000..77d8d45
--- /dev/null
@@ -0,0 +1,16 @@
+.main {
+  margin: $gutter-size auto;
+  border-radius: $border-radius-standard;
+  background-color: $background-content-color;
+  padding: $gutter-size/2;
+}
+
+.pre {
+  padding: 0px !important;
+  background-color: transparent !important;
+  margin-bottom: 10px;
+}
+
+.copyButton {
+  width: 100%;
+}
diff --git a/src/features/shared/components/EmptyContent/EmptyContent.jsx b/src/features/shared/components/EmptyContent/EmptyContent.jsx
new file mode 100644 (file)
index 0000000..4583e0e
--- /dev/null
@@ -0,0 +1,21 @@
+import React from 'react'
+import styles from './EmptyContent.scss'
+
+class EmptyContent extends React.Component {
+  render() {
+
+    return (
+      <div className={styles.emptyContainer}>
+        {this.props.children && <div className={styles.emptyContent}>
+          {this.props.children}
+        </div>}
+      </div>
+    )
+  }
+}
+
+EmptyContent.propTypes = {
+  title: React.PropTypes.string
+}
+
+export default EmptyContent
diff --git a/src/features/shared/components/EmptyContent/EmptyContent.scss b/src/features/shared/components/EmptyContent/EmptyContent.scss
new file mode 100644 (file)
index 0000000..16dc047
--- /dev/null
@@ -0,0 +1,22 @@
+.emptyContainer {
+  margin: $gutter-size auto;
+  width: 350px;
+
+  .emptyContent {
+    border: 1px solid $border-color;
+    padding: $gutter-size/2;
+    text-align: left;
+    background: $background-color;
+  }
+
+  ol {
+    padding-left: $gutter-size/2;
+    margin-bottom: 0px;
+    padding-top: 10px;
+    padding-bottom: 10px;
+  }
+
+  li {
+    padding-left: 6px;
+  }
+}
diff --git a/src/features/shared/components/ErrorBanner/ErrorBanner.jsx b/src/features/shared/components/ErrorBanner/ErrorBanner.jsx
new file mode 100644 (file)
index 0000000..b84a5b2
--- /dev/null
@@ -0,0 +1,28 @@
+import React from 'react'
+import styles from './ErrorBanner.scss'
+
+class ErrorBanner extends React.Component {
+  render() {
+    const error = this.props.error || ''
+    const message = error.chainMessage || error.message || error
+
+    return (
+      <div className={styles.main}>
+        {this.props.title && <strong>{this.props.title}<br/></strong>}
+
+        {message &&
+          <div className={(error.code || error.requestId) ? styles.message : ''}>
+            {message}{error.detail ? `: ${error.detail}` : ''}
+          </div>}
+
+        {error.code &&
+          <div className={styles.extra}>Error Code: <strong>{error.code}</strong></div>}
+
+        {error.requestId &&
+          <div className={styles.extra}>Request ID: <strong>{error.requestId}</strong></div>}
+      </div>
+    )
+  }
+}
+
+export default ErrorBanner
diff --git a/src/features/shared/components/ErrorBanner/ErrorBanner.scss b/src/features/shared/components/ErrorBanner/ErrorBanner.scss
new file mode 100644 (file)
index 0000000..25bc3e7
--- /dev/null
@@ -0,0 +1,22 @@
+.main {
+  background: $highlight-danger-background;
+  color: $highlight-danger;
+  border: 1px solid $highlight-danger-border;
+  border-radius: $border-radius-standard;
+  padding: 20px;
+  margin-bottom: 20px;
+  word-wrap: break-word;
+
+  a {
+    color: $highlight-danger;
+    text-decoration: underline;
+  }
+}
+
+.message {
+  margin-bottom: 15px;
+}
+
+.extra {
+  line-height: 1.3;
+}
diff --git a/src/features/shared/components/FieldLabel/FieldLabel.jsx b/src/features/shared/components/FieldLabel/FieldLabel.jsx
new file mode 100644 (file)
index 0000000..0b9df6d
--- /dev/null
@@ -0,0 +1,10 @@
+import React from 'react'
+import styles from './FieldLabel.scss'
+
+class FieldLabel extends React.Component {
+  render() {
+    return (<label className={styles.main}>{this.props.children}</label>)
+  }
+}
+
+export default FieldLabel
diff --git a/src/features/shared/components/FieldLabel/FieldLabel.scss b/src/features/shared/components/FieldLabel/FieldLabel.scss
new file mode 100644 (file)
index 0000000..8178ea8
--- /dev/null
@@ -0,0 +1,4 @@
+.main {
+  font-size: $font-size-caps;
+  text-transform: uppercase;
+}
diff --git a/src/features/shared/components/Flash/Flash.jsx b/src/features/shared/components/Flash/Flash.jsx
new file mode 100644 (file)
index 0000000..be47540
--- /dev/null
@@ -0,0 +1,55 @@
+import React from 'react'
+import styles from './Flash.scss'
+
+class Flash extends React.Component {
+  componentWillReceiveProps(nextProps) {
+    Object.keys(nextProps.messages).forEach(key => {
+      const item = nextProps.messages[key]
+      if (!item.displayed) {
+        this.props.markFlashDisplayed(key)
+      }
+    })
+  }
+
+  render() {
+    if (!this.props.messages || this.props.hideFlash) {
+      return null
+    }
+
+    const messages = []
+    // Flash messages are stored in an objecty key with a random UUID. If
+    // multiple messages are displayed, we rely on the browser maintaining
+    // object inerstion order of keys to display messages in the order they
+    // were created.
+    Object.keys(this.props.messages).forEach(key => {
+      const item = this.props.messages[key]
+      messages.push(
+        <div className={`${styles.alert} ${styles[item.type]} ${styles.main}`} key={key}>
+          <div className={styles.content}>
+            {item.title && <div><strong>{item.title}</strong></div>}
+            {item.message}
+          </div>
+
+          <button type='button' className='close' onClick={() => this.props.dismissFlash(key)}>
+            <span>&times;</span>
+          </button>
+        </div>)
+    })
+
+    return (
+      <div>
+        {messages}
+      </div>
+    )
+  }
+}
+
+import { connect } from 'react-redux'
+
+const mapStateToProps = (state) => ({
+  hideFlash: state.tutorial.isShowing && state.routing.locationBeforeTransitions.pathname.includes(state.tutorial.route)
+})
+
+export default connect(
+  mapStateToProps
+)(Flash)
diff --git a/src/features/shared/components/Flash/Flash.scss b/src/features/shared/components/Flash/Flash.scss
new file mode 100644 (file)
index 0000000..1f7aab4
--- /dev/null
@@ -0,0 +1,35 @@
+.main {
+  display: flex;
+}
+
+.alert {
+  padding: 15px;
+  border-bottom: 1px solid transparent;
+}
+
+.info {
+  background-color: $alert-info-bg;
+  border-color: $alert-info-border;
+  color: $alert-info-text;
+}
+
+.success {
+  background-color: $success-background;
+  border-color: $success-border;
+  color: $success;
+}
+
+.danger {
+  background-color: $highlight-danger-background;
+  border-color: $highlight-danger-border;
+  color: $highlight-danger;
+}
+
+.content {
+  width: 100%;
+  flex: 1;
+
+  p {
+    margin: 0;
+  }
+}
diff --git a/src/features/shared/components/FormContainer/FormContainer.jsx b/src/features/shared/components/FormContainer/FormContainer.jsx
new file mode 100644 (file)
index 0000000..f688b9b
--- /dev/null
@@ -0,0 +1,45 @@
+import React from 'react'
+import { ErrorBanner, PageTitle, FormSection, SubmitIndicator } from 'features/shared/components'
+import componentClassNames from 'utility/componentClassNames'
+import disableAutocomplete from 'utility/disableAutocomplete'
+
+import styles from './FormContainer.scss'
+import Tutorial from 'features/tutorial/components/Tutorial'
+
+class FormContainer extends React.Component {
+  render() {
+    return(
+      <div className={componentClassNames(this, 'flex-container')}>
+        <PageTitle title={this.props.label} />
+
+        <div className={`${styles.main} flex-container`}>
+          <div className={styles.content}>
+            <form onSubmit={this.props.onSubmit} {...disableAutocomplete}>
+              {this.props.children}
+
+              <FormSection className={styles.submitSection}>
+                {this.props.error &&
+                  <ErrorBanner
+                    title='Error submitting form'
+                    error={this.props.error} />}
+
+                <div className={styles.submit}>
+                  <button type='submit' className='btn btn-primary' disabled={this.props.submitting || this.props.disabled}>
+                    {this.props.submitLabel || 'Submit'}
+                  </button>
+
+                  {this.props.showSubmitIndicator && this.props.submitting &&
+                    <SubmitIndicator />
+                  }
+                </div>
+              </FormSection>
+            </form>
+          </div>
+          <Tutorial types={['TutorialForm']} />
+        </div>
+      </div>
+    )
+  }
+}
+
+export default FormContainer
diff --git a/src/features/shared/components/FormContainer/FormContainer.scss b/src/features/shared/components/FormContainer/FormContainer.scss
new file mode 100644 (file)
index 0000000..f39d8bf
--- /dev/null
@@ -0,0 +1,17 @@
+.main {
+  background: $background-color;
+  padding: 0;
+  display: flex;
+  flex-direction: row;
+  padding: 0 $gutter-size;
+}
+
+.content {
+  min-width: 400px;
+  width: 55%;
+  margin: 0 auto;
+}
+
+.submit {
+  text-align: right;
+}
diff --git a/src/features/shared/components/FormSection/FormSection.jsx b/src/features/shared/components/FormSection/FormSection.jsx
new file mode 100644 (file)
index 0000000..222a4ea
--- /dev/null
@@ -0,0 +1,18 @@
+import React from 'react'
+import styles from './FormSection.scss'
+
+class FormSection extends React.Component {
+  render() {
+    return (
+      <div className={`${styles.main} ${this.props.className || ''}`}>
+        <div className={styles.title}>{this.props.title}</div>
+
+        <div className={styles.content}>
+          {this.props.children}
+        </div>
+      </div>
+    )
+  }
+}
+
+export default FormSection
diff --git a/src/features/shared/components/FormSection/FormSection.scss b/src/features/shared/components/FormSection/FormSection.scss
new file mode 100644 (file)
index 0000000..f8ea919
--- /dev/null
@@ -0,0 +1,24 @@
+.main {
+  padding-bottom: $gutter-size;
+  border-bottom: 1px solid $border-color;
+  margin: $gutter-size 0;
+
+  &:first-child {
+    margin-top: 0;
+    padding-top: $gutter-size;
+  }
+
+  &:last-child {
+    border: 0;
+    margin-bottom: 0;
+    padding-bottom: $gutter-size*2;
+  }
+}
+
+.title {
+  color: $text-strong-color;
+  font-weight: bold;
+  font-size: $font-size-section-title;
+  text-transform: uppercase;
+  margin-bottom: $gutter-size;
+}
diff --git a/src/features/shared/components/HiddenField.jsx b/src/features/shared/components/HiddenField.jsx
new file mode 100644 (file)
index 0000000..8452532
--- /dev/null
@@ -0,0 +1,15 @@
+import React from 'react'
+
+class HiddenField extends React.Component {
+  render() {
+    return(
+      <input className='form-control'
+        type='hidden'
+        onChange={this.props.fieldProps.onChange}
+        value={this.props.fieldProps.value} />
+    )
+  }
+
+}
+
+export default HiddenField
diff --git a/src/features/shared/components/JsonField/JsonField.jsx b/src/features/shared/components/JsonField/JsonField.jsx
new file mode 100644 (file)
index 0000000..b0fc4f2
--- /dev/null
@@ -0,0 +1,84 @@
+import React from 'react'
+import styles from './JsonField.scss'
+import AceEditor from 'react-ace'
+import { parseNonblankJSON } from 'utility/string'
+import { FieldLabel } from 'features/shared/components'
+
+import 'brace/mode/json'
+import 'brace/theme/github'
+
+class JsonField extends React.Component {
+  constructor(props) {
+    super(props)
+    this.state = {syntaxError: {}}
+  }
+
+  render() {
+    const hint = this.props.hint || 'Contents must be represented as a JSON object'
+    const fieldProps = this.props.fieldProps
+    const displayProps = {
+      mode: 'json',
+      theme: 'github',
+      height: this.props.height || '80px',
+      width: '100%',
+      tabSize: 2,
+      showGutter: false,
+      highlightActiveLine: false,
+      showPrintMargin: false,
+      editorProps: {$blockScrolling: true}
+    }
+
+    const onLoad = (editor) => {
+      const self = this
+
+      editor.navigateFileStart()
+      editor.navigateDown()
+      editor.navigateRight(1)
+
+      // Restore default browser tab-focusing behavior
+      editor.commands.bindKey('Tab', null)
+      editor.commands.bindKey('Shift-Tab', null)
+
+      editor.getSession().on('changeAnnotation', function() {
+        self.setState({syntaxError: editor.getSession().getAnnotations()[0]})
+      })
+    }
+
+    const showError = fieldProps.touched && !fieldProps.active && fieldProps.error
+    const syntaxError = this.state.syntaxError
+
+    const editorStyles = [styles.editorWrapper]
+    if (showError) { editorStyles.push(styles.editorError) }
+
+    return (
+      <div className='form-group'>
+        {this.props.title && <FieldLabel>{this.props.title}</FieldLabel>}
+        <div className={editorStyles.join(' ')}>
+          <AceEditor
+            {...fieldProps}
+            {...displayProps}
+            onLoad={onLoad}
+          />
+        </div>
+
+        {showError &&
+          <span className={`help-block ${styles.errorBlock}`}>
+            {fieldProps.error}:
+            {syntaxError && ` ${syntaxError.text} on row ${syntaxError.row + 1}`}
+          </span>}
+        {!showError && <span className='help-block'>{hint}</span>}
+      </div>
+    )
+  }
+}
+
+JsonField.validator = value => {
+  try {
+    parseNonblankJSON(value)
+  } catch (err) {
+    return 'Error parsing JSON'
+  }
+  return null
+}
+
+export default JsonField
diff --git a/src/features/shared/components/JsonField/JsonField.scss b/src/features/shared/components/JsonField/JsonField.scss
new file mode 100644 (file)
index 0000000..a39b9ee
--- /dev/null
@@ -0,0 +1,13 @@
+.editorWrapper {
+  border: 1px solid $input-border;
+  border-radius: $input-border-radius-large;
+  padding: 12px 8px; // ace-text-layer gets 4px default padding
+}
+
+.editorError {
+  border-color: $brand-danger;
+}
+
+.errorBlock {
+  color: $brand-danger;
+}
diff --git a/src/features/shared/components/KeyConfiguration.jsx b/src/features/shared/components/KeyConfiguration.jsx
new file mode 100644 (file)
index 0000000..ee52c8d
--- /dev/null
@@ -0,0 +1,85 @@
+import React from 'react'
+import { SelectField, XpubField } from 'features/shared/components'
+
+const rangeOptions = [1,2,3,4,5,6].map(val => ({label: val, value: val}))
+
+class KeyConfiguration extends React.Component {
+  componentWillMount() {
+    this.setState({ keys: 1 })
+    this.props.xpubs.addField()
+  }
+
+  render() {
+    const {
+      quorum,
+      quorumHint,
+      xpubs
+    } = this.props
+
+    // Override onChange here rather than in a redux-form normalizer because
+    // we're using component state (keys) to determine the max value
+    const quorumChange = (event, maxKeys) => {
+      let quorum = parseInt(typeof(event) == 'object' ? event.target.value : event)
+      if (isNaN(quorum)) { return }
+
+      if (maxKeys == undefined) {
+        maxKeys = parseInt(this.state.keys || 0)
+      }
+
+      if (quorum > maxKeys) { quorum = maxKeys }
+
+      this.props.quorum.onChange(quorum)
+    }
+
+    const keyCountChange = event => {
+      let maxKeys = parseInt(event.target.value) || 0
+      let existing = this.state.keys || 0
+
+      if (maxKeys > existing) {
+        for (let i = 0; i < maxKeys - existing; i++) {
+          this.props.xpubs.addField()
+        }
+      } else if (maxKeys < existing) {
+        for (let i = 0; i < existing - maxKeys; i++) {
+          this.props.xpubs.removeField()
+        }
+      }
+
+      this.setState({ keys: maxKeys })
+      quorumChange(this.props.quorum.value, maxKeys)
+    }
+
+    const quorumOptions = rangeOptions.slice(0, this.state.keys)
+
+    return(
+      <div>
+        <SelectField options={rangeOptions}
+          title='Keys'
+          skipEmpty={true}
+          fieldProps={{
+            value: this.state.keys,
+            onChange: keyCountChange,
+          }} />
+
+        <SelectField options={quorumOptions}
+          title='Quorum'
+          skipEmpty={true}
+          hint={quorumHint}
+          fieldProps={{
+            ...quorum,
+            onChange: quorumChange,
+          }} />
+
+        {xpubs.map((xpub, index) =>
+          <XpubField
+            key={`xpub-${index}`}
+            index={index}
+            typeProps={xpub.type}
+            valueProps={xpub.value}
+          />)}
+      </div>
+    )
+  }
+}
+
+export default KeyConfiguration
diff --git a/src/features/shared/components/KeyValueTable/KeyValueTable.jsx b/src/features/shared/components/KeyValueTable/KeyValueTable.jsx
new file mode 100644 (file)
index 0000000..3d3a3b5
--- /dev/null
@@ -0,0 +1,71 @@
+import React from 'react'
+import styles from './KeyValueTable.scss'
+import { Section } from 'features/shared/components'
+import { Link } from 'react-router'
+import { size, sample, isArray, isObject, toPairs } from 'lodash'
+
+class KeyValueTable extends React.Component {
+  shouldUsePre(item) {
+    if (item.pre) return true
+
+    return item.value != null && (typeof item.value == 'object')
+  }
+
+  stringify(value) {
+    if (isObject(value) && size(value) == 1) {
+      // Random sample will always be the lone value here
+      let sampled = sample(value)
+
+      if (!isObject(sampled)) {
+        if (isArray(value)) return JSON.stringify(value)
+
+        // Manually construct single-key object stringify for better formatting
+        const pair = toPairs(value)[0]
+        return `{${JSON.stringify(pair[0])}: ${JSON.stringify(pair[1])}}`
+      }
+    }
+
+    return JSON.stringify(value, null, '  ')
+  }
+
+  renderValue(item) {
+    let value = item.value
+    if (this.shouldUsePre(item)) {
+      value = <pre className={styles.pre}>{this.stringify(item.value)}</pre>
+    }
+    if (item.link) {
+      value = <Link to={item.link}>{value}</Link>
+    }
+
+    if (value === undefined || value === null || value === '') {
+      value = '-'
+    }
+
+    return value
+  }
+
+  render() {
+    return(
+      <Section
+        title={this.props.title}
+        actions={this.props.actions} >
+        <table className={styles.table}>
+          <tbody>
+            {this.props.items.map((item) => {
+              return <tr key={item.label}>
+                <td className={styles.label}>{item.label}</td>
+                <td className={styles.value}>{this.renderValue(item)}
+                  {item.editUrl && <Link to={item.editUrl} className={styles.edit}>
+                    <span className={`${styles.pencil} glyphicon glyphicon-pencil`}></span>Edit
+                  </Link>}
+                </td>
+              </tr>
+            })}
+          </tbody>
+        </table>
+      </Section>
+    )
+  }
+}
+
+export default KeyValueTable
diff --git a/src/features/shared/components/KeyValueTable/KeyValueTable.scss b/src/features/shared/components/KeyValueTable/KeyValueTable.scss
new file mode 100644 (file)
index 0000000..6a1eff6
--- /dev/null
@@ -0,0 +1,73 @@
+.table {
+  background: $background-color;
+  table-layout: fixed;
+  width: 100%;
+
+  td {
+    border-bottom: 1px solid $border-color;
+    vertical-align: top;
+    padding: 13px $gutter-size;
+    line-height: 15px;
+  }
+
+  a .pre {
+    color: $highlight-default;
+  }
+
+  tr:first-child td {
+    padding-top: 20px;
+  }
+
+  tr:last-child td {
+    border-bottom: none;
+    padding-bottom: 20px;
+  }
+}
+
+.edit {
+  float: right;
+  font-family: Nitti Grotesk;
+  font-size: $font-size-base;
+}
+
+.pencil {
+  padding-right: 5px;
+  font-size: 10px;
+  font-weight: bold;
+}
+
+.label {
+  background: $background-color;
+  border-right: 1px solid $border-color;
+  color: $text-strong-color;
+  font-weight: bold;
+  width: 200px;
+  font-size: $font-size-caps;
+  font-family: Nitti Grotesk;
+  text-align: right;
+  letter-spacing: 0.2px;
+  text-transform: uppercase;
+}
+
+.value, .pre {
+  font-size: $font-size-code;
+  color: $text-table-value-color;
+}
+
+.value {
+  word-wrap: break-word;
+  font-family: Nitti;
+}
+
+.pre {
+  display: inline-block;
+  padding: 0;
+  margin: 0;
+
+  word-break: break-all;
+  word-wrap: break-word;
+  white-space: pre-wrap;
+
+  border: none;
+  line-height: 1.4;
+}
diff --git a/src/features/shared/components/NotFound.jsx b/src/features/shared/components/NotFound.jsx
new file mode 100644 (file)
index 0000000..ec92d97
--- /dev/null
@@ -0,0 +1,14 @@
+import React from 'react'
+
+class NotFound extends React.Component {
+  render() {
+    return (
+      <div className='jumbotron text-center'>
+        <h3>404 Not Found</h3>
+        <p>We couldn't find the page you were looking for.</p>
+      </div>
+    )
+  }
+}
+
+export default NotFound
diff --git a/src/features/shared/components/ObjectSelectorField/ObjectSelectorField.jsx b/src/features/shared/components/ObjectSelectorField/ObjectSelectorField.jsx
new file mode 100644 (file)
index 0000000..97f6916
--- /dev/null
@@ -0,0 +1,72 @@
+import React from 'react'
+import styles from './ObjectSelectorField.scss'
+import { DropdownButton, MenuItem } from 'react-bootstrap'
+import { FieldLabel } from 'features/shared/components'
+
+const ALIAS_SELECTED = 'Alias'
+const ID_SELECTED = 'ID'
+
+class ObjectSelectorField extends React.Component {
+  constructor(props) {
+    super(props)
+
+    this.state = {
+      showDropdown: false,
+      selected: ALIAS_SELECTED
+    }
+
+    this.select = this.select.bind(this)
+    this.toggleDropwdown = this.toggleDropwdown.bind(this)
+    this.closeDropdown = this.closeDropdown.bind(this)
+  }
+
+  toggleDropwdown() {
+    this.setState({ showDropdown: !this.state.showDropdown })
+  }
+
+  closeDropdown() {
+    this.setState({ showDropdown: false })
+  }
+
+  select(value) {
+    this.setState({ selected: value })
+    this.closeDropdown()
+  }
+
+  render() {
+    return(
+      <div className='form-group'>
+        {this.props.title && <FieldLabel>{this.props.title}</FieldLabel>}
+        <div className='input-group'>
+          <div className={`input-group-btn ${this.state.showDropdown && 'open'}`}>
+            <DropdownButton
+              className={styles.dropdownButton}
+              id='input-dropdown-addon'
+              title={this.state.selected}
+              onSelect={this.select}
+            >
+              <MenuItem eventKey={ALIAS_SELECTED}>Alias</MenuItem>
+              <MenuItem eventKey={ID_SELECTED}>ID</MenuItem>
+            </DropdownButton>
+          </div>
+
+          {this.state.selected == ID_SELECTED &&
+            <input className='form-control'
+              type={this.state.type}
+              placeholder={`${this.props.title} ID`}
+              {...this.props.fieldProps.id} />}
+
+          {this.state.selected == ALIAS_SELECTED &&
+            <this.props.aliasField
+              className={styles.aliasFieldGroupItem}
+              placeholder={`Start typing ${this.props.title.toLowerCase()} alias...`}
+              fieldProps={this.props.fieldProps.alias} />}
+
+        </div>
+        {this.props.hint && <span className='help-block'>{this.props.hint}</span>}
+      </div>
+    )
+  }
+}
+
+export default ObjectSelectorField
diff --git a/src/features/shared/components/ObjectSelectorField/ObjectSelectorField.scss b/src/features/shared/components/ObjectSelectorField/ObjectSelectorField.scss
new file mode 100644 (file)
index 0000000..edac7d0
--- /dev/null
@@ -0,0 +1,11 @@
+.dropdownButton {
+  text-align: left;
+  width: 90px;
+}
+
+.aliasFieldGroupItem {
+  border-top-left-radius: 0 !important;
+  border-bottom-left-radius: 0 !important;
+  border-top-right-radius: $input-border-radius-large !important;
+  border-bottom-right-radius: $input-border-radius-large !important;
+}
diff --git a/src/features/shared/components/PageContent/PageContent.jsx b/src/features/shared/components/PageContent/PageContent.jsx
new file mode 100644 (file)
index 0000000..a1a3178
--- /dev/null
@@ -0,0 +1,14 @@
+import React from 'react'
+import styles from './PageContent.scss'
+
+class PageContent extends React.Component {
+  render() {
+    return (
+      <div className={styles.main}>
+        {this.props.children}
+      </div>
+    )
+  }
+}
+
+export default PageContent
diff --git a/src/features/shared/components/PageContent/PageContent.scss b/src/features/shared/components/PageContent/PageContent.scss
new file mode 100644 (file)
index 0000000..1b1da43
--- /dev/null
@@ -0,0 +1,4 @@
+.main {
+  padding: $gutter-size;
+  position: relative;
+}
diff --git a/src/features/shared/components/PageTitle/PageTitle.jsx b/src/features/shared/components/PageTitle/PageTitle.jsx
new file mode 100644 (file)
index 0000000..0b38d7e
--- /dev/null
@@ -0,0 +1,88 @@
+import React from 'react'
+import { connect } from 'react-redux'
+import { Flash } from 'features/shared/components'
+import { Link } from 'react-router'
+import { humanize, capitalize } from 'utility/string'
+import makeRoutes from 'routes'
+import actions from 'actions'
+import styles from './PageTitle.scss'
+import componentClassNames from 'utility/componentClassNames'
+
+class PageTitle extends React.Component {
+  render() {
+    const chevron = require('images/chevron.png')
+
+    return(
+      <div className={componentClassNames(this)}>
+        <div className={styles.main}>
+          <div className={styles.navigation}>
+            <ul className={styles.crumbs}>
+              {this.props.breadcrumbs.map(crumb =>
+                <li className={styles.crumb} key={crumb.name}>
+                  {!crumb.last && <Link to={crumb.path}>
+                    {capitalize(crumb.name)}
+                    <img src={chevron} className={styles.chevron} />
+                  </Link>}
+
+                  {crumb.last && <span className={styles.title}>
+                    {this.props.title || crumb.name}
+                  </span>}
+                </li>
+              )}
+            </ul>
+          </div>
+
+          {Array.isArray(this.props.actions) && <ul className={styles.actions}>
+            {this.props.actions.map(item => <li key={item.key}>{item}</li>)}
+          </ul>}
+        </div>
+
+        <Flash messages={this.props.flashMessages}
+          markFlashDisplayed={this.props.markFlashDisplayed}
+          dismissFlash={this.props.dismissFlash}
+        />
+      </div>
+    )
+  }
+}
+
+const mapStateToProps = (state) => {
+  const routes = makeRoutes()
+  const pathname = state.routing.locationBeforeTransitions.pathname
+  const breadcrumbs = []
+
+  let currentRoutes = routes.childRoutes
+  let currentPath = []
+  pathname.split('/').forEach(component => {
+    let match = currentRoutes.find(route => {
+      return route.path == component || route.path.indexOf(':') >= 0
+    })
+
+    if (match) {
+      currentRoutes = match.childRoutes || []
+      currentPath.push(component)
+
+      if (!match.skipBreadcrumb) {
+        breadcrumbs.push({
+          name: match.name || humanize(component),
+          path: currentPath.join('/')
+        })
+      }
+    }
+  })
+
+  breadcrumbs[breadcrumbs.length - 1].last = true
+
+  return {
+    breadcrumbs,
+    flashMessages: state.app.flashMessages,
+  }
+}
+
+export default connect(
+  mapStateToProps,
+  (dispatch) => ({
+    markFlashDisplayed: (key) => dispatch(actions.app.displayedFlash(key)),
+    dismissFlash: (key) => dispatch(actions.app.dismissFlash(key)),
+  })
+)(PageTitle)
diff --git a/src/features/shared/components/PageTitle/PageTitle.scss b/src/features/shared/components/PageTitle/PageTitle.scss
new file mode 100644 (file)
index 0000000..1c670cc
--- /dev/null
@@ -0,0 +1,59 @@
+.main {
+  background: $background-color;
+  padding: $gutter-size;
+  border-bottom: 1px solid $border-color;
+  height: $title-height;
+  display: flex;
+  align-items: center;
+  code {
+    display: inline-block;
+    font-size: $font-size-code;
+    font-weight: normal;
+    margin-left: 3px;
+    vertical-align: middle;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+    max-width: 300px;
+    padding: 0 6px;
+    background: $background-emphasis-color;
+    border: 1px solid transparentize($border-color, 0.5);
+    line-height: 1.4;
+  }
+}
+.title {
+  color: $text-strong-color;
+}
+
+.navigation {
+  flex-grow: 1;
+}
+
+.crumbs {
+  display: flex;
+  font-size: $font-size-page-title;
+  list-style-type: none;
+  margin: 0;
+  padding: 0;
+}
+
+.crumb a {
+  position: relative;
+  padding-right: $gutter-size/2;
+  margin-right: $gutter-size/2;
+
+  .chevron {
+    width: 5px;
+    height: 10px;
+    position: absolute;
+    right: -2px;
+    top: calc(50% - 5px;)
+  }
+}
+
+.actions {
+  list-style-type: none;
+  display: flex;
+  margin: 0;
+  padding: 0;
+}
diff --git a/src/features/shared/components/Pagination/Pagination.jsx b/src/features/shared/components/Pagination/Pagination.jsx
new file mode 100644 (file)
index 0000000..53b4c19
--- /dev/null
@@ -0,0 +1,36 @@
+import React from 'react'
+import styles from './Pagination.scss'
+
+class Pagination extends React.Component {
+  render() {
+    const prevClass = `${styles.button} ${this.props.currentPage > 1 ? '' : styles.disabled}`
+    const nextClass = `${styles.button} ${this.props.isLastPage ? styles.disabled : ''}`
+    const nextPage = () => this.props.pushList(this.props.currentFilter, this.props.currentPage + 1)
+    const prevPage = () => this.props.pushList(this.props.currentFilter, this.props.currentPage - 1)
+
+    return (
+      <ul className={styles.main}>
+        <li>
+          <a className={prevClass} onClick={prevPage}>
+            &larr;
+          </a>
+        </li>
+        <li className={styles.label}>Page {this.props.currentPage}</li>
+        <li>
+          <a className={nextClass} onClick={nextPage}>
+            &rarr;
+          </a>
+        </li>
+      </ul>
+    )
+  }
+}
+
+Pagination.propTypes = {
+  currentPage: React.PropTypes.number,
+  isLastPage: React.PropTypes.bool,
+  pushList: React.PropTypes.func,
+  currentFilter: React.PropTypes.object,
+}
+
+export default Pagination
diff --git a/src/features/shared/components/Pagination/Pagination.scss b/src/features/shared/components/Pagination/Pagination.scss
new file mode 100644 (file)
index 0000000..1c95285
--- /dev/null
@@ -0,0 +1,36 @@
+.main {
+  text-align: center;
+  list-style-type: none;
+  padding: 0;
+  user-select: none;
+  margin-top: $gutter-size;
+
+  li {
+    display: inline-block;
+    margin: 0 $gutter-size;
+  }
+}
+
+
+.button {
+  border: 2px solid $highlight-default;
+  color: $highlight-default;
+  cursor: pointer;
+  display: block;
+  line-height: 26px;
+  font-size: $font-size-btn-lg;
+  height: 30px;
+  width: 30px;
+  border-radius: 15px;
+
+  &:hover {
+    border-color: $highlight-secondary;
+  }
+}
+
+.disabled {
+  border-color: $text-color;
+  color: $text-color;
+  opacity: 0;
+  pointer-events: none;
+}
diff --git a/src/features/shared/components/RawJsonButton.jsx b/src/features/shared/components/RawJsonButton.jsx
new file mode 100644 (file)
index 0000000..f61c045
--- /dev/null
@@ -0,0 +1,34 @@
+import React from 'react'
+import { Connection } from 'chain-sdk'
+
+class RawJsonButton extends React.Component {
+  showRawJson(item){
+    const snakeCased = Connection.snakeize(item)
+    this.props.showJsonModal(<pre>{JSON.stringify(snakeCased, null, 2)}</pre>)
+  }
+
+  render() {
+    return (
+        <button className='btn btn-link' onClick={this.showRawJson.bind(this, this.props.item)}>
+          Raw JSON
+        </button>
+    )
+  }
+}
+
+import { connect } from 'react-redux'
+import actions from 'actions'
+
+const mapDispatchToProps = ( dispatch ) => ({
+  showJsonModal: (body) => dispatch(actions.app.showModal(
+    body,
+    actions.app.hideModal,
+    null,
+    { wide: true }
+  )),
+})
+
+export default connect(
+  () => ({}),
+  mapDispatchToProps
+)(RawJsonButton)
diff --git a/src/features/shared/components/RelativeTime.jsx b/src/features/shared/components/RelativeTime.jsx
new file mode 100644 (file)
index 0000000..f866289
--- /dev/null
@@ -0,0 +1,20 @@
+import React from 'react'
+import moment from 'moment'
+import { humanizeDuration } from 'utility/time'
+
+class RelativeTime extends React.Component {
+  render() {
+    let timestamp = moment(this.props.timestamp).fromNow()
+
+    const diff = moment(this.props.timestamp).diff(moment())
+    if (diff > 0) {
+      timestamp = humanizeDuration(diff/1000) + ' ahead of local time'
+    }
+
+    return(
+      <span title={this.props.timestamp}>{timestamp}</span>
+    )
+  }
+}
+
+export default RelativeTime
diff --git a/src/features/shared/components/RoutingContainer.jsx b/src/features/shared/components/RoutingContainer.jsx
new file mode 100644 (file)
index 0000000..f799732
--- /dev/null
@@ -0,0 +1,11 @@
+import React from 'react'
+
+export default class RoutingContainer extends React.Component {
+  render() {
+    return (
+      <div className='flex-container'>
+        {this.props.children}
+      </div>
+    )
+  }
+}
diff --git a/src/features/shared/components/SearchBar/SearchBar.jsx b/src/features/shared/components/SearchBar/SearchBar.jsx
new file mode 100644 (file)
index 0000000..8893962
--- /dev/null
@@ -0,0 +1,176 @@
+import React from 'react'
+import styles from './SearchBar.scss'
+import disableAutocomplete from 'utility/disableAutocomplete'
+
+class SearchBar extends React.Component {
+  constructor(props) {
+    super(props)
+
+    // TODO: examine renaming and refactoring for clarity. Consider moving
+    // away from local state if possible.
+    this.state = {
+      query: this.props.currentFilter.filter || '',
+      sumBy: this.props.currentFilter.sumBy || '',
+      sumByVisible: false,
+    }
+    this.state.showClear = (this.state.query != (this.props.defaultFilter || '')) || this.state.sumBy != ''
+    this.state.sumByVisible = this.state.sumBy != ''
+
+    this.filterKeydown = this.filterKeydown.bind(this)
+    this.filterOnChange = this.filterOnChange.bind(this)
+    this.sumByOnChange = this.sumByOnChange.bind(this)
+    this.showSumBy = this.showSumBy.bind(this)
+    this.handleSubmit = this.handleSubmit.bind(this)
+    this.clearQuery = this.clearQuery.bind(this)
+  }
+
+  componentWillReceiveProps(nextProps) {
+    // Override text field with default query when provided
+    if (nextProps.currentFilter.filter != this.props.currentFilter.filter) {
+      this.setState({query: nextProps.currentFilter.filter})
+    }
+  }
+
+  filterKeydown(event) {
+    this.setState({lastKeypress: event.key})
+  }
+
+  filterOnChange(event) {
+    const input = event.target
+    const key = this.state.lastKeypress
+    let value = event.target.value
+    let cursorPosition = input.selectionStart
+
+    switch (key) {
+      case '"':
+        value = value.substr(0, value.length - 1) + "'"
+        break
+      case "'":
+        if (value[cursorPosition] == "'" &&
+            value[cursorPosition - 1] == "'") {
+          value = value.substr(0, cursorPosition-1)
+            + value.substr(cursorPosition)
+        }
+        break
+      case '(':
+        value = value.substr(0, cursorPosition)
+          + ')'
+          + value.substr(cursorPosition)
+
+        break
+      case ')':
+        if (value[cursorPosition] == ')' &&
+            value[cursorPosition - 1] == ')') {
+          value = value.substr(0, cursorPosition-1)
+            + value.substr(cursorPosition)
+        }
+        break
+    }
+
+    this.setState({query: value})
+
+    // Setting selection range only works after the onChange
+    // handler has completed
+    setTimeout(() => {
+      input.setSelectionRange(cursorPosition, cursorPosition)
+    }, 0)
+  }
+
+  showSumBy() {
+    this.setState({sumByVisible: true})
+  }
+
+  sumByOnChange(event) {
+    this.setState({sumBy: event.target.value})
+  }
+
+  handleSubmit(event) {
+    event.preventDefault()
+
+    const query = {}
+    const state = {
+      showClear: (this.state.query && (this.state.query != this.props.defaultFilter)) || this.state.sumBy
+    }
+
+    if (this.state.query) {
+      query.filter = this.state.query
+    } else if (this.props.defaultFilter) {
+      state.query = this.props.defaultFilter
+      query.filter = this.props.defaultFilter
+    }
+    if (this.state.sumBy) query.sumBy = this.state.sumBy
+
+    this.setState(state)
+    this.props.pushList(query)
+  }
+
+  clearQuery() {
+    const newState = { query: (this.props.defaultFilter || ''), sumBy: '', showClear: false}
+    this.setState(newState)
+
+    const query = {}
+    if (newState.query) { query.filter = newState.query }
+    this.props.pushList(query)
+  }
+
+  render() {
+    let usesSumBy = false
+    let searchFieldClass = styles.search_field_full
+
+    if (this.props.sumBy !== undefined) usesSumBy = true
+    if (this.state.sumByVisible) searchFieldClass = styles.search_field_half
+
+    return (
+      <div className={styles.main}>
+        <form onSubmit={this.handleSubmit} {...disableAutocomplete}>
+          <span className={`${styles.searchField} ${searchFieldClass}`}>
+            <input
+              value={this.state.query || ''}
+              onKeyDown={this.filterKeydown}
+              onChange={this.filterOnChange}
+              className={`form-control ${styles.search_input}`}
+              type='search'
+              autoFocus='autofocus'
+              placeholder='Enter filter...' />
+
+            {usesSumBy && !this.state.sumByVisible &&
+              <span onClick={this.showSumBy} className={styles.showSumBy}>set sum_by</span>}
+          </span>
+
+          {usesSumBy && this.state.sumByVisible &&
+            <span className={styles.sum_by_field}>
+              <input
+                value={this.state.sumBy}
+                onChange={this.sumByOnChange}
+                className={`form-control ${styles.search_input} ${styles.sum_by_input}`}
+                type='search'
+                autoFocus='autofocus'
+                placeholder='Enter sum_by...' />
+            </span>}
+
+            {/* This is required for form submission */}
+            <input type='submit' className={styles.submit} tabIndex='-1' />
+        </form>
+
+        <span className={styles.queryTime}>
+          {/* TODO: in the future there may be objects with default filters that
+              do not require a filter; this is a stopgap measure for balances. */}
+          {this.props.defaultFilter && !this.state.query.trim() && 'Filter is required • '}
+
+          Queried at {this.props.queryTime}
+
+          {this.state.showClear && <span>
+            {' • '}
+            <span type='button'
+              className={styles.clearSearch}
+              onClick={this.clearQuery}>
+                {this.props.defaultFilter ? 'Restore default filter' : 'Clear filter'}
+            </span>
+          </span>}
+        </span>
+      </div>
+    )
+  }
+}
+
+export default SearchBar
diff --git a/src/features/shared/components/SearchBar/SearchBar.scss b/src/features/shared/components/SearchBar/SearchBar.scss
new file mode 100644 (file)
index 0000000..ba5d075
--- /dev/null
@@ -0,0 +1,85 @@
+.main {
+  padding: $gutter-size/2 $gutter-size;
+}
+
+.search_field_full, .search_field_half, .sum_by_field {
+  display: inline-block;
+  position: relative;
+}
+
+.searchField {
+  position: relative;
+}
+
+.search_field_full {
+  width: 100%;
+}
+
+.search_field_half {
+  width: 55%;
+  padding-right: $gutter-size/2;
+}
+
+.sum_by_field {
+  width: 45%;
+}
+
+.label {
+  padding-left: 20px;
+}
+
+.search_input {
+  height: 50px;
+  padding: 10px 20px 10px 45px;
+  border: 1px solid $border-color;
+  height: 44px;
+  color: $text-strong-color;
+  border-radius: 25px;
+  background-image: url('images/search.png');
+  background-repeat: no-repeat;
+  background-size: 15px 15px;
+  background-position: 20px center;
+}
+
+.sum_by_input {
+  background-image: url('images/sum.png');
+}
+
+.showSumBy {
+  border: 1px solid $border-color;
+  border-radius: 25px;
+  color: $text-light-color;
+  padding: 0px 10px;
+  position: absolute;
+  right: $gutter-size/2;
+  bottom: 8px;
+
+  &:hover {
+    cursor: pointer;
+    background-color: $background-emphasis-color;
+  }
+}
+
+.search_input::-webkit-input-placeholder {
+  color: #b6bcc5;
+}
+
+.submit {
+  position: absolute;
+  left: -9999px;
+}
+
+.clearSearch {
+  display: inline-block;
+  text-decoration: underline;
+
+  &:hover {
+    cursor: pointer;
+  }
+}
+
+.queryTime {
+  display: inline-block;
+  padding: 4px 20px;
+  opacity: 0.66
+}
diff --git a/src/features/shared/components/Section/Section.jsx b/src/features/shared/components/Section/Section.jsx
new file mode 100644 (file)
index 0000000..6c8c640
--- /dev/null
@@ -0,0 +1,24 @@
+import React from 'react'
+import styles from './Section.scss'
+
+class Section extends React.Component {
+  render() {
+    return (
+      <div className={styles.main}>
+        {this.props.title && <div className={styles.title}>
+          <h5>
+            {this.props.title}
+          </h5>
+
+          {this.props.actions && <div>{this.props.actions}</div>}
+        </div>}
+
+        <div className={styles.children}>
+          {this.props.children}
+        </div>
+      </div>
+    )
+  }
+}
+
+export default Section
diff --git a/src/features/shared/components/Section/Section.scss b/src/features/shared/components/Section/Section.scss
new file mode 100644 (file)
index 0000000..27003c5
--- /dev/null
@@ -0,0 +1,33 @@
+.main {
+  margin: 0 0 $gutter-size;
+}
+
+.title {
+  display: flex;
+
+  h5 {
+    flex-grow: 1;
+    font-size: $font-size-section-title;
+    font-weight: bold;
+  }
+
+  code {
+    display: inline-block;
+    font-size: $font-size-code;
+    font-weight: normal;
+    margin-left: 3px;
+    vertical-align: middle;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+    max-width: 200px;
+    padding: 0 6px;
+    background: $background-emphasis-color;
+    border: 1px solid transparentize($border-color, 0.5);
+    line-height: 1.5;
+  }
+}
+
+.children {
+  border: 1px solid $border-color;
+}
diff --git a/src/features/shared/components/SelectField.jsx b/src/features/shared/components/SelectField.jsx
new file mode 100644 (file)
index 0000000..47569ed
--- /dev/null
@@ -0,0 +1,44 @@
+import React from 'react'
+import FieldLabel from './FieldLabel/FieldLabel'
+import pick from 'lodash/pick'
+import ReactMarkdown from 'react-markdown'
+
+const SELECT_FIELD_PROPS = [
+  'value',
+  'onBlur',
+  'onChange',
+  'onFocus',
+]
+
+class SelectField extends React.Component {
+  render() {
+    const options = this.props.options
+    const emptyLabel = this.props.emptyLabel || 'Select one...'
+    const valueKey = this.props.valueKey || 'value'
+    const labelKey = this.props.labelKey || 'label'
+
+    const fieldProps = pick(this.props.fieldProps, SELECT_FIELD_PROPS)
+    const {touched, error} = this.props.fieldProps
+
+    return(
+      <div className='form-group'>
+        {this.props.title && <FieldLabel>{this.props.title}</FieldLabel>}
+        <select
+          className='form-control' {...fieldProps}
+          autoFocus={!!this.props.autoFocus}>
+          {!this.props.skipEmpty && <option value=''>{emptyLabel}</option>}
+
+          {options.map((option) =>
+            <option value={option[valueKey]} key={option[valueKey]}>
+              {option[labelKey]}
+            </option>)}
+        </select>
+
+        {touched && error && <span className='text-danger'><strong>{error}</strong></span>}
+        {this.props.hint && <span className='help-block'><ReactMarkdown source={this.props.hint} /></span>}
+      </div>
+    )
+  }
+}
+
+export default SelectField
diff --git a/src/features/shared/components/SubmitIndicator/SubmitIndicator.jsx b/src/features/shared/components/SubmitIndicator/SubmitIndicator.jsx
new file mode 100644 (file)
index 0000000..f46d948
--- /dev/null
@@ -0,0 +1,9 @@
+import React from 'react'
+import styles from './SubmitIndicator.scss'
+
+export default class SubmitIndicator extends React.Component {
+  render() {
+    const text = this.props.text || 'Submitting...'
+    return <div className={styles.activeSubmit}>{text}</div>
+  }
+}
diff --git a/src/features/shared/components/SubmitIndicator/SubmitIndicator.scss b/src/features/shared/components/SubmitIndicator/SubmitIndicator.scss
new file mode 100644 (file)
index 0000000..5b8797b
--- /dev/null
@@ -0,0 +1,15 @@
+.activeSubmit {
+  animation: pulse 2s infinite;
+}
+
+@keyframes pulse {
+  0% {
+    opacity: 1.0;
+  }
+  50% {
+    opacity: 0.0;
+  }
+  100% {
+    opacity: 1.0;
+  }
+}
diff --git a/src/features/shared/components/TableList/TableList.jsx b/src/features/shared/components/TableList/TableList.jsx
new file mode 100644 (file)
index 0000000..2e44ba0
--- /dev/null
@@ -0,0 +1,22 @@
+import React from 'react'
+import styles from './TableList.scss'
+
+class TableList extends React.Component {
+  render() {
+    return (
+      <table className={styles.main}>
+        <thead>
+          <tr>
+            {this.props.titles.map(title => <th key={title}>{title}</th>)}
+            <th></th>
+          </tr>
+        </thead>
+        <tbody>
+          {this.props.children}
+        </tbody>
+      </table>
+    )
+  }
+}
+
+export default TableList
diff --git a/src/features/shared/components/TableList/TableList.scss b/src/features/shared/components/TableList/TableList.scss
new file mode 100644 (file)
index 0000000..25a17de
--- /dev/null
@@ -0,0 +1,51 @@
+.main {
+  background: $background-color;
+  border: 1px solid $border-color !important;
+  color: $text-strong-color;
+  width: 100%;
+  table-layout: fixed;
+  margin-bottom: $gutter-size;
+
+  code {
+    padding: 0;
+    font-size: $font-size-code;
+  }
+
+  td {
+    border-top: 1px solid $border-light-color;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    color: $text-color;
+    line-height: 20px;
+    vertical-align: top;
+  }
+
+  th {
+    color: $text-strong-color;
+    font-size: $font-size-caps;
+    text-transform: uppercase;
+    font-weight: bold;
+  }
+
+  td, th  {
+    padding: 13px $gutter-size 13px 0;
+  }
+  th, td {
+
+    &:first-child {
+      padding-left: $gutter-size * 2;
+    }
+
+    &:last-child {
+      text-align: right;
+    }
+  }
+
+  :global {
+    .btn-link {
+      padding-top: 0;
+      padding-bottom: 0;
+      line-height: 1;
+    }
+  }
+}
diff --git a/src/features/shared/components/TextField.jsx b/src/features/shared/components/TextField.jsx
new file mode 100644 (file)
index 0000000..8389e8f
--- /dev/null
@@ -0,0 +1,41 @@
+import React from 'react'
+import pick from 'lodash/pick'
+import { FieldLabel } from 'features/shared/components'
+import disableAutocomplete from 'utility/disableAutocomplete'
+
+const TEXT_FIELD_PROPS = [
+  'value',
+  'onBlur',
+  'onChange',
+  'onFocus',
+  'name'
+]
+
+class TextField extends React.Component {
+  constructor(props) {
+    super(props)
+    this.state = {type: 'text'}
+  }
+
+  render() {
+    const fieldProps = pick(this.props.fieldProps, TEXT_FIELD_PROPS)
+    const {touched, error} = this.props.fieldProps
+
+    return(
+      <div className='form-group'>
+        {this.props.title && <FieldLabel>{this.props.title}</FieldLabel>}
+        <input className='form-control'
+          type={this.state.type}
+          placeholder={this.props.placeholder}
+          autoFocus={!!this.props.autoFocus}
+          {...disableAutocomplete}
+          {...fieldProps} />
+
+        {touched && error && <span className='text-danger'><strong>{error}</strong></span>}
+        {this.props.hint && <span className='help-block'>{this.props.hint}</span>}
+      </div>
+    )
+  }
+}
+
+export default TextField
diff --git a/src/features/shared/components/XpubField/XpubField.jsx b/src/features/shared/components/XpubField/XpubField.jsx
new file mode 100644 (file)
index 0000000..0989f95
--- /dev/null
@@ -0,0 +1,132 @@
+import React from 'react'
+import styles from './XpubField.scss'
+import { SelectField, FieldLabel, TextField } from '../'
+import { connect } from 'react-redux'
+import actions from 'features/mockhsm/actions'
+
+const methodOptions = {
+  mockhsm: 'Use existing MockHSM key',
+  generate: 'Generate new MockHSM key',
+  provide: 'Provide existing xpub',
+}
+
+class XpubField extends React.Component {
+  constructor(props) {
+    super(props)
+
+    this.state = {
+      generate: '',
+      mockhsm: '',
+      provide: '',
+      autofocusInput: false
+    }
+  }
+
+  componentDidMount() {
+    if (!this.props.autocompleteIsLoaded) {
+      this.props.fetchAll().then(() => {
+        this.props.didLoadAutocomplete()
+      })
+    }
+
+    this.props.typeProps.onChange(Object.keys(methodOptions)[0])
+  }
+
+  render() {
+    const {
+      typeProps,
+      valueProps,
+      mockhsmKeys,
+    } = this.props
+
+    const typeOnChange = event => {
+      const value = typeProps.onChange(event).value
+      valueProps.onChange(this.state[value] || '')
+      this.setState({ autofocusInput: true })
+    }
+
+    const valueOnChange = event => {
+      const value = valueProps.onChange(event).value
+      this.setState({ [typeProps.value]: value })
+    }
+
+    const fields = {
+      'mockhsm': <SelectField options={mockhsmKeys}
+        autoFocus={this.state.autofocusInput}
+        valueKey='xpub'
+        labelKey='label'
+        fieldProps={{...valueProps, onChange: valueOnChange}} />,
+      'provide': <TextField
+        autoFocus={this.state.autofocusInput}
+        fieldProps={{...valueProps, onChange: valueOnChange}}
+        placeholder='Enter xpub' />,
+      'generate': <TextField
+        autoFocus={this.state.autofocusInput}
+        fieldProps={{...valueProps, onChange: valueOnChange}}
+        placeholder='Alias for generated key (leave blank for automatic value)' />,
+    }
+
+    return (
+      <div className={styles.main}>
+        <FieldLabel>Key {this.props.index + 1}</FieldLabel>
+
+        <table className={styles.options}>
+          <tbody>
+            {Object.keys(methodOptions).map((key) =>
+              <tr key={`key-${this.props.index}-option-${key}`}>
+                <td className={styles.label}>
+                  <label>
+                    <input type='radio'
+                      className={styles.radio}
+                      name={`keys-${this.props.index}`}
+                      onChange={typeOnChange}
+                      checked={key == typeProps.value}
+                      value={key}
+                    />
+                    {methodOptions[key]}
+                  </label>
+                </td>
+
+                <td className={styles.field}>
+                  {typeProps.value == key && fields[key]}
+                </td>
+              </tr>
+            )}
+          </tbody>
+        </table>
+      </div>
+    )
+  }
+}
+
+XpubField.propTypes = {
+  index: React.PropTypes.number,
+  typeProps: React.PropTypes.object,
+  valueProps: React.PropTypes.object,
+  mockhsmKeys: React.PropTypes.array,
+  autocompleteIsLoaded: React.PropTypes.bool,
+  fetchAll: React.PropTypes.func,
+  didLoadAutocomplete: React.PropTypes.func,
+}
+
+export default connect(
+  (state) => {
+    let keys = []
+    for (var key in state.mockhsm.items) {
+      const item = state.mockhsm.items[key]
+      keys.push({
+        ...item,
+        label: item.alias ? item.alias : item.id.slice(0, 32) + '...'
+      })
+    }
+
+    return {
+      autocompleteIsLoaded: state.mockhsm.autocompleteIsLoaded,
+      mockhsmKeys: keys,
+    }
+  },
+  (dispatch) => ({
+    didLoadAutocomplete: () => dispatch(actions.didLoadAutocomplete),
+    fetchAll: (cb) => dispatch(actions.fetchAll(cb)),
+  })
+)(XpubField)
diff --git a/src/features/shared/components/XpubField/XpubField.scss b/src/features/shared/components/XpubField/XpubField.scss
new file mode 100644 (file)
index 0000000..a08cef4
--- /dev/null
@@ -0,0 +1,38 @@
+.main {
+  border: 1px solid $border-color;
+  margin-top: $grid-gutter-width/2;
+  padding: $grid-gutter-width;
+  margin-bottom: $grid-gutter-width;
+
+  > div {
+    margin-bottom: $gutter-size/2;
+  }
+}
+
+.options {
+  width: 100%;
+
+  td { border: none; }
+}
+
+.options .radio {
+  margin-right: 8px;
+}
+
+
+.label {
+  width: 220px;
+
+  label {
+    font-weight: normal;
+    line-height: 36px;
+  }
+}
+
+.field {
+  :global {
+    .form-group {
+      margin: 0;
+    }
+  }
+}
diff --git a/src/features/shared/components/index.js b/src/features/shared/components/index.js
new file mode 100644 (file)
index 0000000..1ae791e
--- /dev/null
@@ -0,0 +1,67 @@
+import CheckboxField from './CheckboxField/CheckboxField'
+import Autocomplete from './Autocomplete'
+import BaseUpdate from './BaseUpdate'
+import BaseList from './BaseList/BaseList'
+import BaseNew from './BaseNew'
+import BaseShow from './BaseShow'
+import CopyableBlock from './CopyableBlock/CopyableBlock'
+import EmptyContent from './EmptyContent/EmptyContent'
+import ErrorBanner from './ErrorBanner/ErrorBanner'
+import FieldLabel from './FieldLabel/FieldLabel'
+import Flash from './Flash/Flash'
+import FormContainer from './FormContainer/FormContainer'
+import FormSection from './FormSection/FormSection'
+import HiddenField from './HiddenField'
+import JsonField from './JsonField/JsonField'
+import KeyConfiguration from './KeyConfiguration'
+import KeyValueTable from './KeyValueTable/KeyValueTable'
+import NotFound from './NotFound'
+import ObjectSelectorField from './ObjectSelectorField/ObjectSelectorField'
+import PageContent from './PageContent/PageContent'
+import PageTitle from './PageTitle/PageTitle'
+import Pagination from './Pagination/Pagination'
+import RawJsonButton from './RawJsonButton'
+import RelativeTime from './RelativeTime'
+import RoutingContainer from './RoutingContainer'
+import SearchBar from './SearchBar/SearchBar'
+import Section from './Section/Section'
+import SelectField from './SelectField'
+import SubmitIndicator from './SubmitIndicator/SubmitIndicator'
+import TableList from './TableList/TableList'
+import TextField from './TextField'
+import XpubField from './XpubField/XpubField'
+
+export {
+  CheckboxField,
+  Autocomplete,
+  BaseUpdate,
+  BaseList,
+  BaseNew,
+  BaseShow,
+  CopyableBlock,
+  EmptyContent,
+  ErrorBanner,
+  FieldLabel,
+  Flash,
+  FormContainer,
+  FormSection,
+  HiddenField,
+  JsonField,
+  KeyConfiguration,
+  KeyValueTable,
+  NotFound,
+  ObjectSelectorField,
+  PageContent,
+  PageTitle,
+  Pagination,
+  RawJsonButton,
+  RelativeTime,
+  RoutingContainer,
+  SearchBar,
+  Section,
+  SelectField,
+  SubmitIndicator,
+  TableList,
+  TextField,
+  XpubField,
+}
diff --git a/src/features/shared/index.js b/src/features/shared/index.js
new file mode 100644 (file)
index 0000000..986df58
--- /dev/null
@@ -0,0 +1,9 @@
+import * as actions from './actions'
+import makeRoutes from './routes'
+import * as reducers from './reducers'
+
+export {
+  actions,
+  reducers,
+  makeRoutes
+}
diff --git a/src/features/shared/reducers.js b/src/features/shared/reducers.js
new file mode 100644 (file)
index 0000000..ce86640
--- /dev/null
@@ -0,0 +1,80 @@
+import { combineReducers } from 'redux'
+import moment from 'moment'
+import uniq from 'lodash/uniq'
+
+const defaultIdFunc = (item) => item.id
+
+export const itemsReducer = (type, idFunc = defaultIdFunc) => (state = {}, action) => {
+  if (action.type == `RECEIVED_${type.toUpperCase()}_ITEMS`) {
+    const newObjects = {}
+    action.param.items.forEach(item => {
+      if (!item.id) { item.id = idFunc(item) }
+      newObjects[idFunc(item)] = item
+    })
+    return {...state, ...newObjects}
+  } else if (action.type == `DELETE_${type.toUpperCase()}`) {
+    delete state[action.id]
+    return {...state}
+  }
+  return state
+}
+
+export const queryItemsReducer = (type, idFunc = defaultIdFunc) => (state = [], action) => {
+  if (action.type == `APPEND_${type.toUpperCase()}_PAGE`) {
+    let newItemIds = action.param.items.map((item, index) => idFunc(item, index))
+
+    if (action.refresh) return newItemIds
+    else {
+      return uniq([...state, ...newItemIds])
+    }
+  } else if (action.type == `DELETE_${type.toUpperCase()}`) {
+    const index = state.indexOf(action.id)
+    if (index >= 0) {
+      state.splice(index, 1)
+      return [...state]
+    }
+  }
+  return state
+}
+
+export const queryCursorReducer = (type) => (state = {}, action) => {
+  if (action.type == `APPEND_${type.toUpperCase()}_PAGE`) {
+    return action.param
+  }
+  return state
+}
+
+export const queryTimeReducer = (type) => (state = '', action) => {
+  if (action.type == `APPEND_${type.toUpperCase()}_PAGE`) {
+    return moment().format('h:mm:ss a')
+  }
+  return state
+}
+
+export const autocompleteIsLoadedReducer = (type) => (state = false, action) => {
+  if (action.type == `DID_LOAD_${type.toUpperCase()}_AUTOCOMPLETE`) {
+    return true
+  }
+
+  return state
+}
+
+export const listViewReducer = (type, idFunc = defaultIdFunc) => combineReducers({
+  itemIds: queryItemsReducer(type, idFunc),
+  cursor: queryCursorReducer(type),
+  queryTime: queryTimeReducer(type)
+})
+
+export const queriesReducer = (type, idFunc = defaultIdFunc) => (state = {}, action) => {
+  if (action.type == `APPEND_${type.toUpperCase()}_PAGE`) {
+    const query = action.param.next.filter || ''
+    const list = state[query] || {}
+
+    return {
+      ...state,
+      [query]: listViewReducer(type, idFunc)(list, action)
+    }
+  }
+
+  return state
+}
diff --git a/src/features/shared/routes.js b/src/features/shared/routes.js
new file mode 100644 (file)
index 0000000..02c991c
--- /dev/null
@@ -0,0 +1,64 @@
+import { RoutingContainer } from 'features/shared/components'
+import { humanize } from 'utility/string'
+import actions from 'actions'
+
+const makeRoutes = (store, type, List, New, Show, Update, options = {}) => {
+  const loadPage = (state, replace) => {
+    const query = state.location.query
+    if (query.filter && options.skipFilter) {
+      replace(state.location.pathname)
+      return
+    } else if (query.filter === undefined && options.defaultFilter) {
+      replace(`${state.location.pathname}?filter=${options.defaultFilter}`)
+      return
+    }
+
+    const pageNumber = parseInt(state.location.query.page || 1)
+    if (pageNumber == 1) {
+      store.dispatch(actions[type].fetchPage(query, pageNumber, { refresh: true }))
+    } else {
+      store.dispatch(actions[type].fetchPage(query, pageNumber))
+    }
+  }
+
+  const childRoutes = []
+
+  if (New) {
+    childRoutes.push({
+      path: 'create',
+      component: New
+    })
+  }
+
+  if (options.childRoutes) {
+    childRoutes.push(...options.childRoutes)
+  }
+
+  if (Show) {
+    childRoutes.push({
+      path: ':id',
+      component: Show
+    })
+  }
+
+  if (Update) {
+    childRoutes.push({
+      path: ':id/tags',
+      component: Update
+    })
+  }
+
+  return {
+    path: options.path || type + 's',
+    component: RoutingContainer,
+    name: options.name || humanize(type + 's'),
+    indexRoute: {
+      component: List,
+      onEnter: (nextState, replace) => { loadPage(nextState, replace) },
+      onChange: (_, nextState, replace) => { loadPage(nextState, replace) }
+    },
+    childRoutes: childRoutes
+  }
+}
+
+export default makeRoutes
diff --git a/src/features/testnet/actions.js b/src/features/testnet/actions.js
new file mode 100644 (file)
index 0000000..162729f
--- /dev/null
@@ -0,0 +1,21 @@
+// FIXME: Microsoft Edge has issues returning errors for responses
+// with a 401 status. We should add browser detection to only
+// use the ponyfill for unsupported browsers.
+const { fetch } = require('fetch-ponyfill')()
+import { testnetInfoUrl } from 'utility/environment'
+
+export const fetchTestnetInfo = () => {
+  return (dispatch) =>
+    fetch(testnetInfoUrl)
+      .then(resp => resp.json())
+      .then(json => {
+        dispatch({type: 'TESTNET_CONFIG', data: json})
+        return json
+      })
+}
+
+const actions = {
+  fetchTestnetInfo,
+}
+
+export default actions
diff --git a/src/features/testnet/index.js b/src/features/testnet/index.js
new file mode 100644 (file)
index 0000000..e6ab350
--- /dev/null
@@ -0,0 +1,9 @@
+import actions from './actions'
+import reducers from './reducers'
+import utils from './utils'
+
+export {
+  actions,
+  reducers,
+  utils,
+}
diff --git a/src/features/testnet/reducers.js b/src/features/testnet/reducers.js
new file mode 100644 (file)
index 0000000..e9af860
--- /dev/null
@@ -0,0 +1,42 @@
+import { combineReducers } from 'redux'
+import moment from 'moment-timezone'
+
+export const nextReset = (state = '', action) => {
+  if (action.type == 'TESTNET_CONFIG') {
+    if (action.data.next_reset) {
+      return moment(action.data.next_reset)
+    } else {
+      // Default reset time is the upcoming Sunday 00:00:00 Pacific.
+      return moment().tz('America/Los_Angeles').day(7).startOf('day')
+    }
+  }
+  return state
+}
+
+export const blockchainId = (state = 0, action) => {
+  if (action.type == 'TESTNET_CONFIG') {
+    return action.data.blockchain_id
+  }
+  return state
+}
+
+export const crosscoreRpcVersion = (state = 0, action) => {
+  if (action.type == 'TESTNET_CONFIG') {
+    return action.data.crosscore_rpc_version || action.data.network_rpc_version
+  }
+  return state
+}
+
+export const testnetInfo = (state = { loading: true }, action) => {
+  if (action.type == 'TESTNET_CONFIG') {
+    state = {...action.data}
+  }
+  return state
+}
+
+export default combineReducers({
+  blockchainId,
+  nextReset,
+  crosscoreRpcVersion,
+  testnetInfo,
+})
diff --git a/src/features/testnet/utils.js b/src/features/testnet/utils.js
new file mode 100644 (file)
index 0000000..7c24217
--- /dev/null
@@ -0,0 +1,22 @@
+const isBlockchainMismatch = (state) => {
+  if (!state.core.onTestnet) {
+    return false
+  }
+
+  return !!state.core.blockchainId && !!state.testnet.blockchainId &&
+    state.core.blockchainId != state.testnet.blockchainId
+}
+
+const isCrosscoreRpcMismatch = (state) => {
+  if (!state.core.onTestnet) {
+    return false
+  }
+
+  return !!state.core.crosscoreRpcVersion && !!state.testnet.crosscoreRpcVersion &&
+    state.core.crosscoreRpcVersion != state.testnet.crosscoreRpcVersion
+}
+
+export default {
+  isBlockchainMismatch,
+  isCrosscoreRpcMismatch,
+}
diff --git a/src/features/transactionFeeds/actions.js b/src/features/transactionFeeds/actions.js
new file mode 100644 (file)
index 0000000..4907247
--- /dev/null
@@ -0,0 +1,14 @@
+import { baseListActions, baseCreateActions } from 'features/shared/actions'
+
+const type = 'transactionFeed'
+
+export default {
+  ...baseCreateActions(type, {
+    listPath: 'transaction-feeds',
+    className: 'TransactionFeed',
+  }),
+  ...baseListActions(type, {
+    listPath: 'transaction-feeds',
+    className: 'TransactionFeed',
+  }),
+}
diff --git a/src/features/transactionFeeds/components/List.jsx b/src/features/transactionFeeds/components/List.jsx
new file mode 100644 (file)
index 0000000..213a1e2
--- /dev/null
@@ -0,0 +1,43 @@
+import React from 'react'
+import { BaseList, EmptyContent } from 'features/shared/components'
+import ListItem from './ListItem'
+import { actions } from 'features/transactionFeeds'
+import { docsRoot } from 'utility/environment'
+
+const type = 'transactionFeed'
+
+const firstTimeContent = <EmptyContent>
+  <p>
+    Transaction feeds enable real-time processing of transactions as they arrive on the blockchain.
+  </p>
+  <a href={`${docsRoot}/core/build-applications/real-time-transaction-processing`} target='_blank'>
+    Learn more
+  </a> about how to use transaction feeds.
+</EmptyContent>
+
+const dispatch = (dispatch) => ({
+  ...BaseList.mapDispatchToProps(type)(dispatch),
+  itemActions: {
+    delete: (feed) => {
+      let label = `ID ${feed.id}`
+      if (!!feed.alias && feed.alias.length > 0) {
+        label = `"${feed.alias}"`
+      }
+
+      dispatch(actions.deleteItem(
+        feed.id,
+        `Really delete transaction feed ${label}?`,
+        `Deleted transaction feed ${label}.`
+      ))
+    }
+  },
+})
+
+export default BaseList.connect(
+  BaseList.mapStateToProps(type, ListItem, {
+    skipQuery: true,
+    label: 'transaction feeds',
+    firstTimeContent
+  }),
+  dispatch
+)
diff --git a/src/features/transactionFeeds/components/ListItem.jsx b/src/features/transactionFeeds/components/ListItem.jsx
new file mode 100644 (file)
index 0000000..7d8692c
--- /dev/null
@@ -0,0 +1,35 @@
+import React from 'react'
+import { KeyValueTable } from 'features/shared/components'
+
+class ListItem extends React.Component {
+  render() {
+    const item = {...this.props.item}
+
+    const after = item.after.split('-')[0].split(':')
+    const blockHeight = after[0]
+    const blockPosition = after[1]
+
+    // int max (2147483647) is used to indicate that a feed
+    // hasn't yet been read from.
+    const hasStarted = blockPosition != '2147483647'
+
+    const options = [
+      {label: 'ID', value: item.id}
+    ]
+
+    if (item.alias) options.push({label: 'Alias', value: item.alias})
+    options.push({label: 'Filter', value: item.filter, link: `/transactions?filter=${item.filter}`, pre: true})
+
+    if (hasStarted) {
+      options.push({label: 'Last Acknowledged', value: {blockHeight, blockPosition}})
+    } else {
+      options.push({label: 'Last Acknowledged', value: 'None'})
+    }
+
+    return(
+      <KeyValueTable items={options} />
+    )
+  }
+}
+
+export default ListItem
diff --git a/src/features/transactionFeeds/components/New.jsx b/src/features/transactionFeeds/components/New.jsx
new file mode 100644 (file)
index 0000000..d7c183a
--- /dev/null
@@ -0,0 +1,51 @@
+import React from 'react'
+import { BaseNew, FormContainer, FormSection, TextField } from 'features/shared/components'
+import { reduxForm } from 'redux-form'
+
+class New extends React.Component {
+  constructor(props) {
+    super(props)
+
+    this.submitWithErrors = this.submitWithErrors.bind(this)
+  }
+
+  submitWithErrors(data) {
+    return new Promise((resolve, reject) => {
+      this.props.submitForm(data)
+        .catch((err) => reject({_error: err}))
+    })
+  }
+
+  render() {
+    const {
+      fields: { alias, filter },
+      error,
+      handleSubmit,
+      submitting
+    } = this.props
+
+    return(
+      <FormContainer
+        error={error}
+        label='New transaction feed'
+        onSubmit={handleSubmit(this.submitWithErrors)}
+        submitting={submitting} >
+
+        <FormSection title='Feed Information'>
+          <TextField title='Alias' placeholder='Alias' fieldProps={alias} autoFocus={true} />
+          <TextField title='Filter' placeholder='Filter' fieldProps={filter} />
+        </FormSection>
+      </FormContainer>
+    )
+  }
+}
+
+const fields = [ 'alias', 'filter' ]
+export default BaseNew.connect(
+  BaseNew.mapStateToProps('transactionFeed'),
+  BaseNew.mapDispatchToProps('transactionFeed'),
+  reduxForm({
+    form: 'newTxFeed',
+    fields,
+  })(New)
+)
diff --git a/src/features/transactionFeeds/components/index.js b/src/features/transactionFeeds/components/index.js
new file mode 100644 (file)
index 0000000..d57a1f8
--- /dev/null
@@ -0,0 +1,7 @@
+import List from './List'
+import New from './New'
+
+export {
+  List,
+  New,
+}
diff --git a/src/features/transactionFeeds/index.js b/src/features/transactionFeeds/index.js
new file mode 100644 (file)
index 0000000..bf77b37
--- /dev/null
@@ -0,0 +1,9 @@
+import actions from './actions'
+import reducers from './reducers'
+import routes from './routes'
+
+export {
+  actions,
+  reducers,
+  routes,
+}
diff --git a/src/features/transactionFeeds/reducers.js b/src/features/transactionFeeds/reducers.js
new file mode 100644 (file)
index 0000000..428bcc8
--- /dev/null
@@ -0,0 +1,9 @@
+import { reducers } from 'features/shared'
+import { combineReducers } from 'redux'
+
+const type = 'transactionFeed'
+
+export default combineReducers({
+  items: reducers.itemsReducer(type),
+  queries: reducers.queriesReducer(type),
+})
diff --git a/src/features/transactionFeeds/routes.js b/src/features/transactionFeeds/routes.js
new file mode 100644 (file)
index 0000000..2dac71d
--- /dev/null
@@ -0,0 +1,6 @@
+import { List, New } from './components'
+import { makeRoutes } from 'features/shared'
+
+export default (store) => makeRoutes(
+  store, 'transactionFeed', List, New, null, null, { path: 'transaction-feeds', name: 'Transaction feeds'}
+)
diff --git a/src/features/transactions/actions.js b/src/features/transactions/actions.js
new file mode 100644 (file)
index 0000000..1c69abc
--- /dev/null
@@ -0,0 +1,131 @@
+import uuid from 'uuid'
+import { chainClient, chainSigner } from 'utility/environment'
+import { parseNonblankJSON } from 'utility/string'
+import { push } from 'react-router-redux'
+import { baseCreateActions, baseListActions } from 'features/shared/actions'
+
+const type = 'transaction'
+
+const list = baseListActions(type, {
+  defaultKey: 'id'
+})
+const form = baseCreateActions(type)
+
+function preprocessTransaction(formParams) {
+  const copy = JSON.parse(JSON.stringify(formParams))
+  const builder = {
+    baseTransaction: copy.baseTransaction,
+    actions: copy.actions,
+  }
+
+  if (builder.baseTransaction == '') {
+    delete builder.baseTransaction
+  }
+
+  if (formParams.submitAction == 'generate') {
+    builder.ttl = '1h' // 1 hour
+  }
+
+  for (let i in builder.actions) {
+    let a = builder.actions[i]
+
+    const intFields = ['amount', 'position']
+    intFields.forEach(key => {
+      const value = a[key]
+      if (value) {
+        if ((parseInt(value)+'') == value) {
+          a[key] = parseInt(value)
+        } else {
+          throw new Error(`Action ${parseInt(i)+1} ${key} must be an integer.`)
+        }
+      }
+    })
+
+    try {
+      a.referenceData = parseNonblankJSON(a.referenceData)
+    } catch (err) {
+      throw new Error(`Action ${parseInt(i)+1} reference data should be valid JSON, or blank.`)
+    }
+
+    try {
+      a.receiver = parseNonblankJSON(a.receiver)
+    } catch (err) {
+      throw new Error(`Action ${parseInt(i)+1} receiver should be valid JSON.`)
+    }
+  }
+
+  return builder
+}
+
+function getTemplateXpubs(tpl) {
+  const xpubs = []
+  tpl.signingInstructions.forEach((instruction) => {
+    instruction.witnessComponents.forEach((component) => {
+      component.keys.forEach((key) => {
+        xpubs.push(key.xpub)
+      })
+    })
+  })
+  return xpubs
+}
+
+form.submitForm = (formParams) => function(dispatch) {
+  const buildPromise = chainClient().transactions.build(builder => {
+    const processed = preprocessTransaction(formParams)
+
+    builder.actions = processed.actions
+    if (processed.baseTransaction) {
+      builder.baseTransaction = processed.baseTransaction
+    }
+  })
+
+  if (formParams.submitAction == 'submit') {
+    return buildPromise
+      .then(tpl => {
+        const signer = chainSigner()
+
+        getTemplateXpubs(tpl).forEach(key => {
+          signer.addKey(key, chainClient().mockHsm.signerConnection)
+        })
+
+        return signer.sign(tpl)
+      }).then(signed => chainClient().transactions.submit(signed))
+      .then(resp => {
+        dispatch(form.created())
+        dispatch(push({
+          pathname: `/transactions/${resp.id}`,
+          state: {
+            preserveFlash: true
+          }
+        }))
+      })
+  }
+
+  // submitAction == 'generate'
+  return buildPromise
+    .then(tpl => {
+      const signer = chainSigner()
+
+      getTemplateXpubs(tpl).forEach(key => {
+        signer.addKey(key, chainClient().mockHsm.signerConnection)
+      })
+
+      return signer.sign({...tpl, allowAdditionalActions: true})
+    })
+    .then(signed => {
+      const id = uuid.v4()
+      dispatch({
+        type: 'GENERATED_TX_HEX',
+        generated: {
+          id: id,
+          hex: signed.rawTransaction,
+        },
+      })
+      dispatch(push(`/transactions/generated/${id}`))
+    })
+}
+
+export default {
+  ...list,
+  ...form,
+}
diff --git a/src/features/transactions/components/GeneratedTxHex/GeneratedTxHex.jsx b/src/features/transactions/components/GeneratedTxHex/GeneratedTxHex.jsx
new file mode 100644 (file)
index 0000000..7b684c5
--- /dev/null
@@ -0,0 +1,42 @@
+import React from 'react'
+import { connect } from 'react-redux'
+import { NotFound, PageContent, PageTitle } from 'features/shared/components'
+import styles from './GeneratedTxHex.scss'
+import { copyToClipboard } from 'utility/clipboard'
+
+class Generated extends React.Component {
+  render() {
+    if (!this.props.hex) return <NotFound />
+
+    return (
+      <div>
+        <PageTitle title='Generated Transaction' />
+
+        <PageContent>
+          <div className={styles.main}>
+            <p>Use the following hex string as the base transaction for a future transaction:</p>
+
+            <button
+              className='btn btn-primary'
+              onClick={() => copyToClipboard(this.props.hex)}
+            >
+              Copy to clipboard
+            </button>
+
+            <pre className={styles.hex}>{this.props.hex}</pre>
+          </div>
+        </PageContent>
+      </div>
+    )
+  }
+}
+
+export default connect(
+  // mapStateToProps
+  (state, ownProps) => {
+    const generated = (state.transaction || {}).generated || []
+    const found = generated.find(i => i.id == ownProps.params.id)
+    if (found) return {hex: found.hex}
+    return {}
+  }
+)(Generated)
diff --git a/src/features/transactions/components/GeneratedTxHex/GeneratedTxHex.scss b/src/features/transactions/components/GeneratedTxHex/GeneratedTxHex.scss
new file mode 100644 (file)
index 0000000..1d04a98
--- /dev/null
@@ -0,0 +1,18 @@
+.main {
+  background: $background-color;
+  max-width: 50%;
+  padding: $gutter-size;
+  margin: 0 auto;
+}
+
+.hex {
+  word-break: break-all;
+  word-wrap: break-word;
+  white-space: pre-wrap;
+
+  margin-top: 5px;
+  margin-left: auto;
+  margin-right: auto;
+  padding: 10px;
+  background-color: #EEE;
+}
diff --git a/src/features/transactions/components/List.jsx b/src/features/transactions/components/List.jsx
new file mode 100644 (file)
index 0000000..dc3483e
--- /dev/null
@@ -0,0 +1,33 @@
+import React from 'react'
+import { BaseList } from 'features/shared/components'
+import ListItem from './ListItem/ListItem'
+import actions from 'actions'
+
+const type = 'transaction'
+
+class List extends React.Component {
+  componentWillReceiveProps(nextProps) {
+    if (nextProps.blockHeight != this.props.blockHeight) {
+      if (nextProps.currentPage == 1) {
+        this.props.getLatest(nextProps.currentFilter)
+      }
+    }
+  }
+
+  render() {
+    const ItemList = BaseList.ItemList
+    return (<ItemList {...this.props} />)
+  }
+}
+
+export default BaseList.connect(
+  (state, ownProps) => ({
+    ...BaseList.mapStateToProps(type, ListItem)(state, ownProps),
+    blockHeight: state.core.blockHeight
+  }),
+  (dispatch) => ({
+    ...BaseList.mapDispatchToProps(type)(dispatch),
+    getLatest: (query) => dispatch(actions.transaction.fetchPage(query, 1, { refresh: true })),
+  }),
+  List
+)
diff --git a/src/features/transactions/components/ListItem/ListItem.jsx b/src/features/transactions/components/ListItem/ListItem.jsx
new file mode 100644 (file)
index 0000000..6acc35f
--- /dev/null
@@ -0,0 +1,33 @@
+import React from 'react'
+import { Link } from 'react-router'
+import { Summary } from 'features/transactions/components'
+import { RelativeTime } from 'features/shared/components'
+import styles from './ListItem.scss'
+
+class ListItem extends React.Component {
+  render() {
+    const item = this.props.item
+
+    return(
+      <div className={styles.main}>
+        <div className={styles.titleBar}>
+          <div className={styles.title}>
+            <label>Transaction ID:</label>
+            &nbsp;<code>{item.id}</code>&nbsp;
+
+            <span className={styles.timestamp}>
+              <RelativeTime timestamp={item.timestamp} />
+            </span>
+          </div>
+          <Link className={styles.viewLink} to={`/transactions/${item.id}`}>
+            View details
+          </Link>
+        </div>
+
+        <Summary transaction={item} />
+      </div>
+    )
+  }
+}
+
+export default ListItem
diff --git a/src/features/transactions/components/ListItem/ListItem.scss b/src/features/transactions/components/ListItem/ListItem.scss
new file mode 100644 (file)
index 0000000..9774c86
--- /dev/null
@@ -0,0 +1,52 @@
+.main {
+  border: 1px solid $border-color;
+  margin-bottom: 30px;
+}
+
+.titleBar {
+  background: $background-color;
+  border-bottom: 1px solid $border-color;
+  display: flex;
+  align-items: center;
+  padding: 20px 25px;
+
+  code {
+    display: inline-block;
+    font-size: $font-size-code;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    vertical-align: middle;
+    width: 200px;
+    padding: 0 6px;
+    background: $background-emphasis-color;
+    border: 1px solid transparentize($border-color, 0.5);
+    line-height: 1.4;
+  }
+}
+
+.title {
+  flex-grow: 1;
+
+  label {
+    color: $text-strong-color;
+    font-size: $font-size-caps;
+    text-transform: uppercase;
+    font-weight: bold;
+    margin: 0 8px 0 0;
+  }
+}
+
+.timestamp {
+  margin-left: $gutter-size;
+}
+
+.viewLink {
+  margin: -15px 0;
+  padding: 15px;
+  position: relative;
+
+  background: url('images/chevron-green.png');
+  background-repeat: no-repeat;
+  background-position: right center;
+  background-size: 5px 9px;
+}
diff --git a/src/features/transactions/components/New/FormActionItem.jsx b/src/features/transactions/components/New/FormActionItem.jsx
new file mode 100644 (file)
index 0000000..b37539e
--- /dev/null
@@ -0,0 +1,125 @@
+import React from 'react'
+import { ErrorBanner, HiddenField, Autocomplete, JsonField, TextField, ObjectSelectorField } from 'features/shared/components'
+import styles from './FormActionItem.scss'
+
+const ISSUE_KEY = 'issue'
+const SPEND_ACCOUNT_KEY = 'spend_account'
+const SPEND_UNSPENT_KEY = 'spend_account_unspent_output'
+const CONTROL_ACCOUNT_KEY = 'control_account'
+const CONTROL_RECEIVER_KEY = 'control_receiver'
+const RETIRE_ASSET_KEY = 'retire'
+const TRANSACTION_REFERENCE_DATA = 'set_transaction_reference_data'
+
+const actionLabels = {
+  [ISSUE_KEY]: 'Issue',
+  [SPEND_ACCOUNT_KEY]: 'Spend from account',
+  [SPEND_UNSPENT_KEY]: 'Spend unspent output',
+  [CONTROL_ACCOUNT_KEY]: 'Control with account',
+  [CONTROL_RECEIVER_KEY]: 'Control with receiver',
+  [RETIRE_ASSET_KEY]: 'Retire',
+  [TRANSACTION_REFERENCE_DATA]: 'Set transaction reference data',
+}
+
+const visibleFields = {
+  [ISSUE_KEY]: {asset: true, amount: true},
+  [SPEND_ACCOUNT_KEY]: {asset: true, account: true, amount: true},
+  [SPEND_UNSPENT_KEY]: {outputId: true},
+  [CONTROL_ACCOUNT_KEY]: {asset: true, account: true, amount: true},
+  [CONTROL_RECEIVER_KEY]: {asset: true, receiver: true, amount: true},
+  [RETIRE_ASSET_KEY]: {asset: true, amount: true},
+  [TRANSACTION_REFERENCE_DATA]: {},
+}
+
+export default class ActionItem extends React.Component {
+  constructor(props) {
+    super(props)
+    this.state = {
+      referenceDataOpen: props.fieldProps.type.value == TRANSACTION_REFERENCE_DATA
+    }
+    this.openReferenceData = this.openReferenceData.bind(this)
+  }
+
+  openReferenceData() {
+    this.setState({referenceDataOpen: true})
+  }
+
+  componentDidMount() {
+    window.scroll(
+      window.scrollX,
+      window.scrollY + this.scrollRef.getBoundingClientRect().top - 10
+    )
+  }
+
+  render() {
+    const {
+      outputId,
+      type,
+      accountId,
+      accountAlias,
+      receiver,
+      assetId,
+      assetAlias,
+      amount,
+      referenceData } = this.props.fieldProps
+
+    const visible = visibleFields[type.value] || {}
+    const remove = (event) => {
+      event.preventDefault()
+      this.props.remove(this.props.index)
+    }
+
+    const classNames = [styles.main]
+    if (type.error) classNames.push(styles.error)
+
+    return (
+      <div className={classNames.join(' ')} ref={ref => this.scrollRef = ref}>
+        <HiddenField fieldProps={type} />
+
+        <div className={styles.header}>
+          <label className={styles.title}>{actionLabels[type.value]}</label>
+          <a href='#' className='btn btn-sm btn-danger' onClick={remove}>Remove</a>
+        </div>
+
+        {type.error && <ErrorBanner message={type.error} />}
+
+        {visible.account &&
+          <ObjectSelectorField
+            title='Account'
+            aliasField={Autocomplete.AccountAlias}
+            fieldProps={{
+              id: accountId,
+              alias: accountAlias
+            }}
+          />}
+
+        {visible.receiver &&
+          <JsonField title='Receiver' fieldProps={receiver} />}
+
+        {visible.outputId &&
+          <TextField title='Output ID' fieldProps={outputId} />}
+
+        {visible.asset &&
+          <ObjectSelectorField
+            title='Asset'
+            aliasField={Autocomplete.AssetAlias}
+            fieldProps={{
+              id: assetId,
+              alias: assetAlias
+            }}
+          />}
+
+        {visible.amount &&
+          <TextField title='Amount' fieldProps={amount} />}
+
+        {this.state.referenceDataOpen &&
+          <JsonField title='Reference data' fieldProps={referenceData} />
+        }
+        {!this.state.referenceDataOpen &&
+          <button type='button' className='btn btn-link' onClick={this.openReferenceData}>
+            Add reference data
+          </button>
+        }
+      </div>
+    )
+  }
+}
diff --git a/src/features/transactions/components/New/FormActionItem.scss b/src/features/transactions/components/New/FormActionItem.scss
new file mode 100644 (file)
index 0000000..b8a7625
--- /dev/null
@@ -0,0 +1,21 @@
+.main {
+  border: 1px solid $border-color;
+  padding: $gutter-size;
+  margin-bottom: $gutter-size;
+}
+
+.error {
+  border-color: $highlight-danger-border;
+}
+
+.header {
+  display: flex;
+  justify-content: space-between;
+  margin-bottom: $gutter-size;
+}
+
+.title {
+  color: $text-strong-color;
+  font-size: $font-size-form-section-title;
+  font-weight: bold;
+}
diff --git a/src/features/transactions/components/New/New.jsx b/src/features/transactions/components/New/New.jsx
new file mode 100644 (file)
index 0000000..632b521
--- /dev/null
@@ -0,0 +1,231 @@
+import { BaseNew, FormContainer, FormSection, FieldLabel, JsonField, TextField } from 'features/shared/components'
+import { DropdownButton, MenuItem } from 'react-bootstrap'
+import { reduxForm } from 'redux-form'
+import ActionItem from './FormActionItem'
+import React from 'react'
+import styles from './New.scss'
+
+class Form extends React.Component {
+  constructor(props) {
+    super(props)
+    this.state = {
+      showDropdown: false
+    }
+
+    this.submitWithValidation = this.submitWithValidation.bind(this)
+    this.addActionItem = this.addActionItem.bind(this)
+    this.removeActionItem = this.removeActionItem.bind(this)
+    this.toggleDropwdown = this.toggleDropwdown.bind(this)
+    this.closeDropdown = this.closeDropdown.bind(this)
+    this.disableSubmit = this.disableSubmit.bind(this)
+  }
+
+  toggleDropwdown() {
+    this.setState({ showDropdown: !this.state.showDropdown })
+  }
+
+  closeDropdown() {
+    this.setState({ showDropdown: false })
+  }
+
+  addActionItem(type) {
+    this.props.fields.actions.addField({
+      type: type,
+      referenceData: '{\n\t\n}'
+    })
+    this.closeDropdown()
+  }
+
+  disableSubmit(actions) {
+    return actions.length == 0 & !this.state.showAdvanced
+  }
+
+  removeActionItem(index) {
+    this.props.fields.actions.removeField(index)
+  }
+
+  submitWithValidation(data) {
+    const lagThreshold = 5
+    if (this.props.replicationLag === null || this.props.replicationLag >= lagThreshold) {
+      return Promise.reject({
+        _error: `Replication lag must be less than ${lagThreshold} to submit transactions via the dashboard. Please wait for the local core to catch up to the generator.`
+      })
+    }
+
+    return new Promise((resolve, reject) => {
+      this.props.submitForm(data)
+        .catch((err) => {
+          const response = {}
+
+          if (err.data) {
+            response.actions = []
+
+            err.data.forEach((error) => {
+              response.actions[error.data.actionIndex] = {type: error}
+            })
+          }
+
+          response['_error'] = err
+          return reject(response)
+        })
+    })
+  }
+
+  render() {
+    const {
+      fields: { baseTransaction, actions, submitAction },
+      error,
+      handleSubmit,
+      submitting
+    } = this.props
+
+    let submitLabel = 'Submit transaction'
+    if (submitAction.value == 'generate') {
+      submitLabel = 'Generate transaction hex'
+    }
+
+    return(
+      <FormContainer
+        error={error}
+        label='New transaction'
+        submitLabel={submitLabel}
+        onSubmit={handleSubmit(this.submitWithValidation)}
+        showSubmitIndicator={true}
+        submitting={submitting}
+        disabled={this.disableSubmit(actions)} >
+
+        <FormSection title='Actions'>
+          {actions.map((action, index) =>
+            <ActionItem
+              key={index}
+              index={index}
+              fieldProps={action}
+              accounts={this.props.accounts}
+              assets={this.props.assets}
+              remove={this.removeActionItem}
+            />)}
+
+            <div className={`btn-group ${styles.addActionContainer} ${this.state.showDropdown && 'open'}`}>
+              <DropdownButton
+                className={`btn btn-default ${styles.addAction}`}
+                id='input-dropdown-addon'
+                title='+ Add action'
+                onSelect={this.addActionItem}
+              >
+                <MenuItem eventKey='issue'>Issue</MenuItem>
+                <MenuItem eventKey='spend_account'>Spend from account</MenuItem>
+                <MenuItem eventKey='spend_account_unspent_output'>Spend unspent output</MenuItem>
+                <MenuItem eventKey='control_account'>Control with account</MenuItem>
+                <MenuItem eventKey='control_receiver'>Control with receiver</MenuItem>
+                <MenuItem eventKey='retire'>Retire</MenuItem>
+                <MenuItem eventKey='set_transaction_reference_data'>Set transaction reference data</MenuItem>
+              </DropdownButton>
+            </div>
+        </FormSection>
+
+        {!this.state.showAdvanced &&
+          <FormSection>
+            <a href='#'
+              className={styles.showAdvanced}
+              onClick={(e) => {
+                e.preventDefault()
+                this.setState({showAdvanced: true})
+              }}
+            >
+              Show advanced options
+            </a>
+          </FormSection>
+        }
+
+        {this.state.showAdvanced && <FormSection title='Advanced Options'>
+          <div>
+            <TextField
+              title='Base transaction'
+              placeholder='Paste transaction hex here...'
+              fieldProps={baseTransaction}
+              autoFocus={true} />
+
+            <FieldLabel>Transaction Build Type</FieldLabel>
+            <table className={styles.submitTable}>
+              <tbody>
+                <tr>
+                  <td><input id='submit_action_submit' type='radio' {...submitAction} value='submit' checked={submitAction.value == 'submit'} /></td>
+                  <td>
+                    <label htmlFor='submit_action_submit'>Submit transaction to blockchain</label>
+                    <br />
+                    <label htmlFor='submit_action_submit' className={styles.submitDescription}>
+                      This transaction will be signed by the MockHSM and submitted to the blockchain.
+                    </label>
+                  </td>
+                </tr>
+                <tr>
+                  <td><input id='submit_action_generate' type='radio' {...submitAction} value='generate' checked={submitAction.value == 'generate'} /></td>
+                  <td>
+                    <label htmlFor='submit_action_generate'>Allow additional actions</label>
+                    <br />
+                    <label htmlFor='submit_action_generate' className={styles.submitDescription}>
+                      These actions will be signed by the MockHSM and returned as a
+                      transaction hex string, which should be used as the base
+                      transaction in a multi-party swap. This transaction will be
+                      valid for one hour.
+                    </label>
+                  </td>
+                </tr>
+              </tbody>
+            </table>
+          </div>
+        </FormSection>}
+      </FormContainer>
+    )
+  }
+}
+
+const validate = values => {
+  const errors = {actions: {}}
+
+  // Base transaction
+  let baseTx = values.baseTransaction || ''
+  if (baseTx.trim().match(/[^0-9a-fA-F]/)) {
+    errors.baseTransaction = 'Base transaction must be a hex string.'
+  }
+
+  // Actions
+  let fieldError
+  values.actions.forEach((action, index) => {
+    fieldError = JsonField.validator(values.actions[index].referenceData)
+    if (fieldError) {
+      errors.actions[index] = {...errors.actions[index], referenceData: fieldError}
+    }
+  })
+
+  return errors
+}
+
+export default BaseNew.connect(
+  state => ({
+    ...BaseNew.mapStateToProps('transaction')(state),
+    replicationLag: state.core.replicationLag,
+  }),
+  BaseNew.mapDispatchToProps('transaction'),
+  reduxForm({
+    form: 'NewTransactionForm',
+    fields: [
+      'baseTransaction',
+      'actions[].accountId',
+      'actions[].accountAlias',
+      'actions[].assetId',
+      'actions[].assetAlias',
+      'actions[].amount',
+      'actions[].receiver',
+      'actions[].outputId',
+      'actions[].referenceData',
+      'actions[].type',
+      'submitAction',
+    ],
+    validate,
+    initialValues: {
+      submitAction: 'submit',
+    },
+  }
+  )(Form)
+)
diff --git a/src/features/transactions/components/New/New.scss b/src/features/transactions/components/New/New.scss
new file mode 100644 (file)
index 0000000..c9c0229
--- /dev/null
@@ -0,0 +1,31 @@
+.submitTable {
+  border-spacing: 5px;
+
+  tr:first-child td {
+    padding-bottom: $gutter-size/2;
+  }
+
+  td input {
+    margin-right: 7px;
+  }
+
+  td {
+    padding: 0;
+    vertical-align: top;
+  }
+
+  label {
+    margin-bottom: 0;
+    color: $text-strong-color;
+  }
+
+  .submitDescription {
+    font-weight: normal;
+    color: $text-color;
+    line-height: 1.4;
+  }
+}
+
+.actionInfo {
+  margin-bottom: $gutter-size;
+}
diff --git a/src/features/transactions/components/Show.jsx b/src/features/transactions/components/Show.jsx
new file mode 100644 (file)
index 0000000..7472660
--- /dev/null
@@ -0,0 +1,91 @@
+import React from 'react'
+import {
+  BaseShow,
+  PageTitle,
+  PageContent,
+  KeyValueTable,
+  Section,
+  RawJsonButton,
+} from 'features/shared/components'
+
+import { Summary } from './'
+import { buildTxInputDisplay, buildTxOutputDisplay } from 'utility/buildInOutDisplay'
+
+class Show extends BaseShow {
+
+
+  render() {
+    const item = this.props.item
+
+    let view
+    if (item) {
+      const title = <span>
+        {'Transaction '}
+        &nbsp;<code>{item.id}</code>
+      </span>
+
+      view = <div>
+        <PageTitle title={title} />
+
+        <PageContent>
+          <Section
+            title='Summary'
+            actions={[
+              <RawJsonButton key='raw-json' item={item} />
+            ]}>
+            <Summary transaction={item} />
+          </Section>
+
+          <KeyValueTable
+            title='Details'
+            items={[
+              {label: 'ID', value: item.id},
+              {label: 'Timestamp', value: item.timestamp},
+              {label: 'Block ID', value: item.blockId},
+              {label: 'Block Height', value: item.blockHeight},
+              {label: 'Position', value: item.position},
+              {label: 'Local?', value: item.isLocal},
+              {label: 'Reference Data', value: item.referenceData},
+            ]}
+          />
+
+          {item.inputs.map((input, index) =>
+            <KeyValueTable
+              key={index}
+              title={index == 0 ? 'Inputs' : ''}
+              items={buildTxInputDisplay(input)}
+            />
+          )}
+
+          {item.outputs.map((output, index) =>
+            <KeyValueTable
+              key={index}
+              title={index == 0 ? 'Outputs' : ''}
+              items={buildTxOutputDisplay(output)}
+            />
+          )}
+        </PageContent>
+      </div>
+    }
+
+    return this.renderIfFound(view)
+  }
+}
+
+// Container
+
+import { actions } from 'features/transactions'
+import { connect } from 'react-redux'
+
+const mapStateToProps = (state, ownProps) => ({
+  item: state.transaction.items[ownProps.params.id]
+})
+
+const mapDispatchToProps = ( dispatch ) => ({
+  fetchItem: (id) => dispatch(actions.fetchItems({filter: `id='${id}'`}))
+})
+
+export default connect(
+  mapStateToProps,
+  mapDispatchToProps
+)(Show)
diff --git a/src/features/transactions/components/Summary/Summary.jsx b/src/features/transactions/components/Summary/Summary.jsx
new file mode 100644 (file)
index 0000000..05062b6
--- /dev/null
@@ -0,0 +1,132 @@
+import React from 'react'
+import { Link } from 'react-router'
+import styles from './Summary.scss'
+
+const INOUT_TYPES = {
+  issue: 'Issue',
+  spend: 'Spend',
+  control: 'Control',
+  retire: 'Retire',
+}
+
+class Summary extends React.Component {
+  normalizeInouts(inouts) {
+    const normalized = {}
+
+    inouts.forEach(inout => {
+      let asset = normalized[inout.assetId]
+      if (!asset) asset = normalized[inout.assetId] = {
+        alias: inout.assetAlias,
+        issue: 0,
+        retire: 0
+      }
+
+      if (['issue', 'retire'].includes(inout.type)) {
+        asset[inout.type] += inout.amount
+      } else {
+        let accountKey = inout.accountId || 'external'
+        let account = asset[accountKey]
+        if (!account) account = asset[accountKey] = {
+          alias: inout.accountAlias,
+          spend: 0,
+          control: 0
+        }
+
+        if (inout.type == 'spend') {
+          account.spend += inout.amount
+        } else if (inout.type == 'control' && inout.purpose == 'change') {
+          account.spend -= inout.amount
+        } else if (inout.type == 'control') {
+          account.control += inout.amount
+        }
+      }
+    })
+
+    return normalized
+  }
+
+  render() {
+    const inouts = this.props.transaction.inputs.concat(this.props.transaction.outputs)
+    const summary = this.normalizeInouts(inouts)
+    const items = []
+
+    Object.keys(summary).forEach((assetId) => {
+      const asset = summary[assetId]
+      const nonAccountTypes = ['issue','retire']
+
+      nonAccountTypes.forEach((type) => {
+        if (asset[type] > 0) {
+          items.push({
+            type: INOUT_TYPES[type],
+            rawAction: type,
+            amount: asset[type],
+            asset: asset.alias ? asset.alias : <code className={styles.rawId}>{assetId}</code>,
+            assetId: assetId,
+          })
+        }
+      })
+
+
+      Object.keys(asset).forEach((accountId) => {
+        if (nonAccountTypes.includes(accountId)) return
+        const account = asset[accountId]
+        if (!account) return
+
+        if (accountId == 'external') {
+          account.alias = 'external'
+          accountId = null
+        }
+
+        const accountTypes = ['spend', 'control']
+        accountTypes.forEach((type) => {
+          if (account[type] > 0) {
+            items.push({
+              type: INOUT_TYPES[type],
+              rawAction: type,
+              amount: account[type],
+              asset: asset.alias ? asset.alias : <code className={styles.rawId}>{assetId}</code>,
+              assetId: assetId,
+              direction: type == 'spend' ? 'from' : 'to',
+              account: account.alias ? account.alias : <code className={styles.rawId}>{accountId}</code>,
+              accountId: accountId,
+            })
+          }
+        })
+      })
+    })
+
+    const ordering = ['issue', 'spend', 'control', 'retire']
+    items.sort((a,b) => {
+      return ordering.indexOf(a.rawAction) - ordering.indexOf(b.rawAction)
+    })
+
+    return(<table className={styles.main}>
+      <tbody>
+        {items.map((item, index) =>
+          <tr key={index}>
+            <td className={styles.colAction}>{item.type}</td>
+            <td className={styles.colLabel}>amount</td>
+            <td className={styles.colAmount}>
+              <code className={styles.amount}>{item.amount}</code>
+            </td>
+            <td className={styles.colLabel}>asset</td>
+            <td className={styles.colAccount}>
+              <Link to={`/assets/${item.assetId}`}>
+                {item.asset}
+              </Link>
+            </td>
+            <td className={styles.colLabel}>{item.account && 'account'}</td>
+            <td className={styles.colAccount}>
+              {item.accountId && <Link to={`/accounts/${item.accountId}`}>
+                {item.account}
+              </Link>}
+              {!item.accountId && item.account}
+            </td>
+          </tr>
+        )}
+      </tbody>
+    </table>)
+  }
+}
+
+export default Summary
diff --git a/src/features/transactions/components/Summary/Summary.scss b/src/features/transactions/components/Summary/Summary.scss
new file mode 100644 (file)
index 0000000..0f318ed
--- /dev/null
@@ -0,0 +1,59 @@
+.main {
+  background: $background-color;
+  width: 100%;
+
+  thead {
+    border-bottom: 1px solid $border-color;
+  }
+
+  td, th {
+    padding-top: 6px;
+    padding-bottom: 6px;
+    padding-right: 10px;
+  }
+
+  td {
+    border-bottom: 1px solid $border-light-color;
+  }
+  tr:last-of-type td {
+    border-bottom: none;
+  }
+
+  a {
+    .rawId {
+      color: $highlight-default;
+    }
+
+    &:hover .rawId {
+      text-decoration: underline;
+    }
+  }
+}
+
+.colAction {
+  padding-left: $gutter-size * 1.5;
+}
+
+.colAction, .colAmount, .colAsset, .colAccount {
+  color: $text-strong-color;
+  width: 20%;
+}
+
+.amount {
+  color: $text-strong-color;
+  background: none;
+}
+
+.colLabel {
+  color: $text-light-color;
+  text-align: right;
+  width: 5%;
+}
+
+.rawId {
+  display: inline-block;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  max-width: 150px;
+  vertical-align: middle;
+}
diff --git a/src/features/transactions/components/index.js b/src/features/transactions/components/index.js
new file mode 100644 (file)
index 0000000..f3cb9e6
--- /dev/null
@@ -0,0 +1,13 @@
+import List from './List'
+import Summary from './Summary/Summary'
+import New from './New/New'
+import Show from './Show'
+import GeneratedTxHex from './GeneratedTxHex/GeneratedTxHex'
+
+export {
+  List,
+  Summary,
+  New,
+  Show,
+  GeneratedTxHex,
+}
diff --git a/src/features/transactions/index.js b/src/features/transactions/index.js
new file mode 100644 (file)
index 0000000..bf77b37
--- /dev/null
@@ -0,0 +1,9 @@
+import actions from './actions'
+import reducers from './reducers'
+import routes from './routes'
+
+export {
+  actions,
+  reducers,
+  routes,
+}
diff --git a/src/features/transactions/reducers.js b/src/features/transactions/reducers.js
new file mode 100644 (file)
index 0000000..9af7f6c
--- /dev/null
@@ -0,0 +1,16 @@
+import { reducers } from 'features/shared'
+import { combineReducers } from 'redux'
+
+const type = 'transaction'
+const maxGeneratedHistory = 50
+
+export default combineReducers({
+  items: reducers.itemsReducer(type),
+  queries: reducers.queriesReducer(type),
+  generated: (state = [], action) => {
+    if (action.type == 'GENERATED_TX_HEX') {
+      return [action.generated, ...state].slice(0, maxGeneratedHistory)
+    }
+    return state
+  },
+})
diff --git a/src/features/transactions/routes.js b/src/features/transactions/routes.js
new file mode 100644 (file)
index 0000000..b0a07c0
--- /dev/null
@@ -0,0 +1,21 @@
+import { List, New, Show, GeneratedTxHex } from './components'
+import { makeRoutes } from 'features/shared'
+
+export default (store) => {
+  return makeRoutes(
+    store,
+    'transaction',
+    List,
+    New,
+    Show,
+    null,
+    {
+      childRoutes: [
+        {
+          path: 'generated/:id',
+          component: GeneratedTxHex,
+        },
+      ]
+    }
+  )
+}
diff --git a/src/features/tutorial/actions.js b/src/features/tutorial/actions.js
new file mode 100644 (file)
index 0000000..e2a547d
--- /dev/null
@@ -0,0 +1,13 @@
+function tutorialNextStep(route){
+  return { type: 'TUTORIAL_NEXT_STEP', route }
+}
+function submitTutorialForm(data, object){
+  return { type: 'UPDATE_TUTORIAL', object, data }
+}
+
+let actions = {
+  tutorialNextStep,
+  submitTutorialForm
+}
+
+export default actions
diff --git a/src/features/tutorial/components/Tutorial.jsx b/src/features/tutorial/components/Tutorial.jsx
new file mode 100644 (file)
index 0000000..ee78cef
--- /dev/null
@@ -0,0 +1,47 @@
+import React from 'react'
+import TutorialInfo from './TutorialInfo/TutorialInfo'
+import TutorialForm from './TutorialForm/TutorialForm'
+import TutorialModal from './TutorialModal/TutorialModal'
+
+const components = {
+  TutorialInfo,
+  TutorialForm,
+  TutorialModal
+}
+
+class Tutorial extends React.Component {
+  render() {
+    const userInput = this.props.tutorial.userInputs
+    const tutorialOpen = this.props.tutorial.isShowing
+    const tutorialRoute = this.props.currentStep['route']
+    const tutorialTypes = this.props.types
+    const TutorialComponent = components[this.props.currentStep['component']]
+
+    return (
+      <div>
+      {tutorialOpen && (tutorialTypes.includes(this.props.currentStep['component'])) &&
+        <TutorialComponent
+          userInput={userInput}
+          {...this.props.currentStep}
+          handleNext={() => this.props.showNextStep(tutorialRoute)}/>}
+      </div>
+    )
+  }
+}
+
+import { actions } from 'features/tutorial'
+import { connect } from 'react-redux'
+
+const mapStateToProps = (state) => ({
+  currentStep: state.tutorial.currentStep,
+  tutorial: state.tutorial
+})
+
+const mapDispatchToProps = ( dispatch ) => ({
+  showNextStep: (route) => dispatch(actions.tutorialNextStep(route))
+})
+
+export default connect(
+  mapStateToProps,
+  mapDispatchToProps
+)(Tutorial)
diff --git a/src/features/tutorial/components/TutorialForm/TutorialForm.jsx b/src/features/tutorial/components/TutorialForm/TutorialForm.jsx
new file mode 100644 (file)
index 0000000..7021c88
--- /dev/null
@@ -0,0 +1,98 @@
+import React from 'react'
+import styles from './TutorialForm.scss'
+
+class TutorialForm extends React.Component {
+  constructor() {
+    super()
+
+    this.state = { showFixed: false }
+
+    // We must bind in the constructor so that we have a reference to the bound
+    // function to remove from the window event listener later.
+    this.handleScroll = this.handleScroll.bind(this)
+  }
+
+  handleScroll(event) {
+    const scrollTop = event.srcElement.scrollingElement.scrollTop
+
+    // Hardcoding visual distance between top of screen and top of TutorialForm
+    // component to create smooth scrolling effect.
+    this.setState({showFixed: scrollTop > 140})
+  }
+
+  componentDidMount() {
+    window.addEventListener('scroll', this.handleScroll)
+  }
+
+  componentWillUnmount() {
+    window.removeEventListener('scroll', this.handleScroll)
+  }
+
+  render() {
+    const userInput = this.props.userInput
+
+    return (
+      <div className={styles.container}>
+        <div className={`${styles.tutorialContainer} ${this.state.showFixed && styles.fixedTutorial}`}>
+          <div className={styles.header}>
+            {this.props.content['header']}
+          </div>
+          <div className={styles.list}>
+            <table className={styles.listItemContainer}>
+              {this.props.content['steps'].map(function (contentLine, i){
+                let title = contentLine['title']
+                if (contentLine['type']) {
+                  let replacement = userInput[contentLine['type']]
+                  if ('index' in contentLine){
+                    replacement = replacement[contentLine['index']]
+                  }
+                  title = contentLine['title'].replace('STRING', replacement['alias'])
+                }
+                let rows = [
+                  <tr key={`item-title-${i}`}>
+                    <td className={styles.listBullet}>{i+1}</td>
+                    <td>{title}</td>
+                  </tr>
+                ]
+                if (contentLine['description']) {
+                  let descriptionResult = []
+                  contentLine['description'].forEach( (descriptionLine, j) => {
+                    let description = descriptionLine
+                    if (description['line']) { description = description['line'] }
+
+                    if (descriptionLine['type']) {
+                      let replacement = userInput[descriptionLine['type']] || descriptionLine['type']
+                      if ('index' in descriptionLine){
+                        replacement = replacement[descriptionLine['index']]
+                      }
+
+                      if (replacement.hasOwnProperty('alias')) {
+                        replacement = replacement['alias'] || ''
+                      }
+
+                      description.split('STRING').forEach( (item, k, arr) => {
+                        descriptionResult.push(item)
+                        let replacementText = k < arr.length - 1 && <span key={`item-input-${j}-${k}`} className={styles.userInputData}>"{replacement}"</span>
+                        descriptionResult.push(replacementText)
+                      })
+                    } else {
+                      descriptionResult.push(description)
+                    }
+                  })
+                  rows.push(<tr key={`item-description-${i}`} className={styles.listItemDescription}>
+                    <td></td>
+                    <td>{descriptionResult}</td>
+                  </tr>)
+                }
+
+                return <tbody key={`item-${i}`} className={styles.listItemGroup}>{rows}</tbody>
+              })}
+            </table>
+          </div>
+        </div>
+      </div>
+    )
+  }
+}
+
+export default TutorialForm
diff --git a/src/features/tutorial/components/TutorialForm/TutorialForm.scss b/src/features/tutorial/components/TutorialForm/TutorialForm.scss
new file mode 100644 (file)
index 0000000..56e6085
--- /dev/null
@@ -0,0 +1,75 @@
+.container {
+  width: 300px;
+  margin: $gutter-size $gutter-size*2;
+
+  .tutorialContainer {
+    position: absolute;
+    width: 300px;
+    min-height: 100px;
+    border: solid $highlight-tutorial 1px;
+    background-color: $background-emphasis-color;
+
+    &.fixedTutorial {
+      position: fixed;
+      top: $gutter-size;
+    }
+
+    .header {
+      background-color: $highlight-tutorial;
+      box-sizing: border-box;
+      color: $text-inverse-color;
+      padding: 0 $gutter-size;
+      height: $title-height;
+      display: flex;
+      align-items: center;
+      font-size: 16px;
+      font-weight: bold;
+      width: 100%;
+    }
+
+    .list {
+      margin: 25px;
+
+      .listItemContainer {
+        margin: 0;
+
+        .listBullet {
+          margin-right: 10px;
+          padding: 2px;
+          display: inline-block;
+          border-radius: 50%;
+          background: $highlight-tutorial;
+          width: 18px;
+          height: 18px;
+          font-size: 12px;
+          font-weight: bold;
+          text-align: center;
+          line-height: 14px;
+          color: #FFFFFF;
+        }
+
+        .listItemGroup {
+          display: inline-block;
+          margin-bottom: 10px;
+          font-size: 15px;
+          font-weight: 500;
+          line-height: 18px;
+          color: #20252D;
+
+          td {
+            padding-bottom: 5px;
+          }
+        }
+
+        .listItemDescription {
+          color: #A3A9B8;
+
+          .userInputData {
+            color: $highlight-tutorial;
+            font-weight: bolder;
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/src/features/tutorial/components/TutorialHeader/TutorialHeader.jsx b/src/features/tutorial/components/TutorialHeader/TutorialHeader.jsx
new file mode 100644 (file)
index 0000000..ef7a9c7
--- /dev/null
@@ -0,0 +1,49 @@
+import React from 'react'
+import { Link } from 'react-router'
+import styles from './TutorialHeader.scss'
+
+class TutorialHeader extends React.Component {
+
+  render() {
+    if(!this.props.tutorial.isShowing || this.props.currentStep.component == 'TutorialModal'){
+      return (
+        <div>
+          {this.props.children}
+        </div>
+      )
+    } else {
+      return (
+        <div className={`${styles.main} ${this.props.showTutorial && styles.collapsed}`}>
+          <div className={styles.header}>
+            {this.props.currentStep.title}
+            <div className={styles.skip}>
+              {!this.props.showTutorial && <Link to={this.props.tutorial.route}>
+                Resume tutorial
+              </Link>}
+              {this.props.showTutorial &&
+              <a onClick={this.props.dismissTutorial}>{this.props.currentStep.dismiss || 'End tutorial'}</a>}
+            </div>
+          </div>
+          {this.props.showTutorial && this.props.children}
+        </div>
+      )
+    }
+  }
+}
+
+import { connect } from 'react-redux'
+
+const mapStateToProps = (state) => ({
+  tutorial: state.tutorial,
+  currentStep: state.tutorial.currentStep,
+  showTutorial: state.routing.locationBeforeTransitions.pathname.includes(state.tutorial.route)
+})
+
+const mapDispatchToProps = ( dispatch ) => ({
+  dismissTutorial: () => dispatch({ type: 'DISMISS_TUTORIAL' })
+})
+
+export default connect(
+  mapStateToProps,
+  mapDispatchToProps
+)(TutorialHeader)
diff --git a/src/features/tutorial/components/TutorialHeader/TutorialHeader.scss b/src/features/tutorial/components/TutorialHeader/TutorialHeader.scss
new file mode 100644 (file)
index 0000000..8ee7dc8
--- /dev/null
@@ -0,0 +1,37 @@
+.main {
+  box-shadow: 0 1px 2px 0 rgba(0,0,0,0.25);
+  margin-bottom: $gutter-size/2;
+  border-bottom: solid $highlight-tutorial 1px;
+}
+
+.collapsed {
+  border-bottom: 1px darken($highlight-tutorial, 15%) solid;
+  box-shadow: none;
+}
+
+.header {
+  background-color: $highlight-tutorial;
+  box-sizing: border-box;
+  color: $text-inverse-color;
+  padding: 0 $gutter-size;
+  height: $title-height;
+  display: flex;
+  align-items: center;
+  font-size: 16px;
+  font-weight: bold;
+  width: 100%;
+}
+
+.skip {
+  flex: 1;
+  text-align: right;
+
+  a {
+    color: $text-inverse-color;
+    font-size: 15px;
+    font-weight: 500;
+    line-height: 18px;
+    text-decoration: underline;
+    cursor: pointer;
+  }
+}
diff --git a/src/features/tutorial/components/TutorialInfo/TutorialInfo.jsx b/src/features/tutorial/components/TutorialInfo/TutorialInfo.jsx
new file mode 100644 (file)
index 0000000..bb4ee3e
--- /dev/null
@@ -0,0 +1,59 @@
+import React from 'react'
+import styles from './TutorialInfo.scss'
+import { Link } from 'react-router'
+
+class TutorialInfo extends React.Component {
+
+  render() {
+    let objectImage
+    try {
+      objectImage = require(`images/empty/${this.props.image}.svg`)
+    } catch (err) { /* do nothing */ }
+
+    const userInput = this.props.userInput
+    const nextButton = <Link to={this.props.route} className={styles.nextWrapper}>
+        <button key='showNext' className={`btn ${styles.next}`} onClick={this.props.handleNext}>
+          Next: {this.props.button}
+        </button>
+      </Link>
+
+    return (
+      <div>
+        <div className={styles.container}>
+          {this.props.image && <img className={styles.image} src={objectImage} />}
+          <div className={styles.text}>
+            {this.props.content.map(function (contentLine, i){
+              let str = contentLine
+              if (contentLine['line']) { str = contentLine['line'] }
+              if(contentLine['list']){
+                let list = []
+                contentLine['list'].forEach(function(listItem, j){
+                  list.push(<tr key={j} className={styles.listItemGroup}>
+                    <td className={styles.listBullet}>{j+1}</td>
+                    <td>{listItem}</td>
+                  </tr>)
+                })
+                return <table key={i} className={styles.listItemContainer}>
+                  <tbody>{list}</tbody>
+                </table>
+              }
+              if (contentLine['type']){
+                let replacement = userInput[contentLine['type']]
+                if ('index' in contentLine){
+                  replacement = replacement[contentLine['index']]
+                }
+                str = str.replace('STRING', replacement['alias'])
+              }
+
+              return <p key={i}>{str}</p>
+            })}
+          </div>
+
+          {nextButton}
+        </div>
+    </div>
+    )
+  }
+}
+
+export default TutorialInfo
diff --git a/src/features/tutorial/components/TutorialInfo/TutorialInfo.scss b/src/features/tutorial/components/TutorialInfo/TutorialInfo.scss
new file mode 100644 (file)
index 0000000..d260908
--- /dev/null
@@ -0,0 +1,77 @@
+.container {
+  width: 100%;
+  background-color: $background-emphasis-color;
+  padding: 25px $gutter-size;
+  display: flex;
+  align-items: center;
+
+  .image {
+    margin-right: 20px;
+    margin-top: 5px;
+    height: 30px;
+    width: 30px;
+    align-self: flex-start;
+  }
+
+  .text {
+    flex: 10;
+    font-size: 15px;
+    line-height: 1.3;
+    max-width: 600px;
+    padding-right: $gutter-size;
+
+    p:last-child {
+      margin: 0;
+    }
+  }
+
+  .listItemContainer {
+    margin: 15px 0;
+
+    .listBullet {
+      margin-right: 10px;
+      padding: 2px;
+      display: inline-block;
+      border-radius: 50%;
+      background: $highlight-tutorial;
+      width: 18px;
+      height: 18px;
+      font-size: 12px;
+      font-weight: bold;
+      text-align: center;
+      line-height: 14px;
+      color: #FFFFFF;
+    }
+
+    .listItemGroup {
+      margin-bottom: 10px;
+      font-size: 15px;
+      font-weight: 500;
+      line-height: 1.4;
+    }
+  }
+
+  .nextWrapper {
+    margin-left: $gutter-size;
+    text-align: right;
+    flex: 1;
+  }
+
+  .next {
+    background-color: $background-color;
+    background-image: url('images/chevron-blue.png');
+    background-repeat: no-repeat;
+    background-position: right 8px center;
+    background-size: 5px 9px;
+    border: 1px solid $highlight-tutorial;
+    border-radius: 2px;
+    color: $highlight-tutorial;
+    padding-right: 20px;
+  }
+
+  button:active:focus {
+    background-color: $highlight-tutorial;
+    border-color: $highlight-tutorial;
+    color: $background-color;
+  }
+}
diff --git a/src/features/tutorial/components/TutorialModal/TutorialModal.jsx b/src/features/tutorial/components/TutorialModal/TutorialModal.jsx
new file mode 100644 (file)
index 0000000..87f1098
--- /dev/null
@@ -0,0 +1,64 @@
+import React from 'react'
+import styles from './TutorialModal.scss'
+import { Link } from 'react-router'
+
+class TutorialModal extends React.Component {
+
+  render() {
+    return (
+      <div className={styles.main}>
+        <div className={styles.backdrop} onClick={this.props.dismissTutorial}></div>
+          <div className={styles.content}>
+            <div className={styles.header}>
+              {this.props.title}
+            </div>
+            <div className={styles.text}>
+              {this.props.content.map(function (contentLine, i){
+                if(contentLine['list']){
+                  let list = []
+                  contentLine['list'].forEach(function(listItem, j){
+                    list.push(<tr key={j} className={styles.listItemGroup}>
+                      <td className={styles.listBullet}>{j+1}</td>
+                      <td>{listItem}</td>
+                    </tr>)
+                  })
+                  return <table key={i} className={styles.listItemContainer}>
+                    <tbody>{list}</tbody>
+                  </table>
+                } else {
+                  let value = contentLine
+                  if (typeof(value) === 'object') { value = value['line'] }
+                  return <p key={i}>{value}</p>
+                }
+              })}
+            </div>
+            <div className={styles.footer}>
+              <button onClick={this.props.dismissTutorial} className={`btn btn-primary ${styles.dismiss}`}>{this.props.dismiss}</button>
+              {this.props.button && <Link to={this.props.route}>
+                  <button key='showNext' className={`btn btn-primary ${styles.next}`} onClick={this.props.handleNext}>
+                    {this.props.button}
+                  </button>
+                </Link>}
+            </div>
+          </div>
+      </div>
+    )
+  }
+}
+
+import { connect } from 'react-redux'
+
+const mapStateToProps = (state) => ({
+  tutorialRoute: state.tutorial.route,
+  currentStep: state.tutorial.currentStep,
+  showTutorial: state.routing.locationBeforeTransitions.pathname.includes(state.tutorial.route)
+})
+
+const mapDispatchToProps = ( dispatch ) => ({
+  dismissTutorial: () => dispatch({ type: 'DISMISS_TUTORIAL' })
+})
+
+export default connect(
+  mapStateToProps,
+  mapDispatchToProps
+)(TutorialModal)
diff --git a/src/features/tutorial/components/TutorialModal/TutorialModal.scss b/src/features/tutorial/components/TutorialModal/TutorialModal.scss
new file mode 100644 (file)
index 0000000..8217c4c
--- /dev/null
@@ -0,0 +1,97 @@
+.main {
+  position: fixed;
+  top: 0;
+  right: 0;
+  left: 0;
+  bottom: 0;
+  z-index: 100;
+}
+.backdrop {
+  background: transparentize(black, 0.2);
+  width: 100%;
+  height: 100%;
+}
+
+.content {
+  border: solid $highlight-tutorial 1px;
+  background: $background-emphasis-color;
+  position: absolute;
+  top: 10%;
+  left: calc(50% - 250px);
+  width: 500px;
+  max-height: 80%;
+  overflow: scroll;
+
+  .header {
+    background-color: $highlight-tutorial;
+    box-sizing: border-box;
+    color: $text-inverse-color;
+    padding: 0 $gutter-size;
+    height: $title-height;
+    display: flex;
+    align-items: center;
+    font-size: 16px;
+    font-weight: bold;
+    width: 100%;
+  }
+
+  .text {
+    padding: $gutter-size/2 $gutter-size;
+    font-size: 15px;
+    font-weight: 500;
+    line-height: 18px;
+    color: #747C89;
+    border-bottom: 1px solid rgba(182,188,197,0.27);
+
+    .listItemContainer {
+      margin: 15px 0;
+
+      .listBullet {
+        margin-right: 10px;
+        padding: 2px;
+        display: inline-block;
+        border-radius: 50%;
+        background: $highlight-tutorial;
+        width: 18px;
+        height: 18px;
+        font-size: 12px;
+        font-weight: bold;
+        text-align: center;
+        line-height: 14px;
+        color: #FFFFFF;
+      }
+
+      .listItemGroup {
+        margin-bottom: 10px;
+        font-size: 15px;
+        font-weight: 500;
+        line-height: 1.4;
+      }
+    }
+
+    p {
+      margin: $gutter-size/2 0;
+      display: block;
+      padding: 0;
+    }
+  }
+
+  .footer {
+    margin: $gutter-size;
+    text-align: right;
+
+    .dismiss {
+      text-align: right;
+      margin-right: $gutter-size/2;
+      background-color: $background-color;
+      border: 1px solid $highlight-tutorial;
+      border-radius: 2px;
+      color: $highlight-tutorial;
+    }
+
+    .next {
+      background: $highlight-tutorial;
+      border-color: $highlight-tutorial;
+    }
+  }
+}
diff --git a/src/features/tutorial/index.js b/src/features/tutorial/index.js
new file mode 100644 (file)
index 0000000..fa284b5
--- /dev/null
@@ -0,0 +1,7 @@
+import actions from './actions'
+import reducers from './reducers'
+
+export {
+  actions,
+  reducers,
+}
diff --git a/src/features/tutorial/reducers.js b/src/features/tutorial/reducers.js
new file mode 100644 (file)
index 0000000..65fc733
--- /dev/null
@@ -0,0 +1,51 @@
+import steps from './steps.json'
+
+export const step = (state = 0, action) => {
+  if (action.type == 'TUTORIAL_NEXT_STEP') return state + 1
+  if (action.type == 'UPDATE_TUTORIAL' && steps[state].objectType == action.object) {
+    return state + 1
+  }
+  if (action.type == 'DISMISS_TUTORIAL') return 0
+  return state
+}
+
+export const isShowing = (state = true, action) => {
+  if (action.type == 'DISMISS_TUTORIAL') return false
+  if (action.type == 'OPEN_TUTORIAL') return true
+  return state
+}
+
+export const route = (currentStep) => (state = 'transactions', action) => {
+  if (action.type == 'TUTORIAL_NEXT_STEP') return action.route
+  if (action.type == 'UPDATE_TUTORIAL' && currentStep.objectType == action.object) {
+    return action.object + 's'
+  }
+  if (action.type == 'DISMISS_TUTORIAL') return 'transactions'
+  return state
+}
+
+export const userInputs = (currentStep) => (state = { accounts: [] }, action) => {
+  if (action.type == 'UPDATE_TUTORIAL' && currentStep.objectType == action.object) {
+    if (action.object == 'mockhsm') return { ...state, mockhsm: action.data }
+    if (action.object == 'asset') return { ...state, asset: action.data }
+    if (action.object == 'account') {
+      return { ...state, accounts: [...state.accounts, action.data] }
+    }
+    return state
+  }
+  if (action.type == 'DISMISS_TUTORIAL') return { accounts: [] }
+  return state
+}
+
+export default (state = {}, action) => {
+  const newState = {
+    step: step(state.step, action),
+    isShowing: isShowing(state.isShowing, action)
+  }
+
+  newState.currentStep = steps[newState.step]
+  newState.userInputs = userInputs(newState.currentStep)(state.userInputs, action)
+  newState.route = route(newState.currentStep)(state.route, action)
+
+  return newState
+}
diff --git a/src/features/tutorial/steps.json b/src/features/tutorial/steps.json
new file mode 100644 (file)
index 0000000..d63efd9
--- /dev/null
@@ -0,0 +1,449 @@
+[
+  {
+    "component": "TutorialModal",
+    "title": "Welcome to Chain Core!",
+    "button": "Start 5-minute tutorial",
+    "dismiss": "Skip",
+    "route": "mockhsms",
+    "content": [
+      "Would you like a brief tutorial? This guide will walk you through the basic functions available in the Chain Core Dashboard."
+    ]
+  },
+  {
+    "component": "TutorialInfo",
+    "title": "Tutorial - Creating keys - Step 1 of 7",
+    "button": "Create MockHSM key",
+    "route": "mockhsms/create",
+    "image": "mockhsm",
+    "content": [
+      "Cryptographic private keys are the primary authorization mechanism on a blockchain. They control both the issuance and transfer of asset units. An asset or account will define one or more keys required for issuance or transfers.",
+      "For development environments, Chain Core provides a convenient MockHSM that simulates production behaviors. Let's begin by creating a new key in our HSM."
+    ]
+  },
+  {
+    "component": "TutorialForm",
+    "objectType": "mockhsm",
+    "title": "Tutorial - Creating keys - Step 1 of 7",
+    "content": {
+      "header": "Create MockHSM key",
+      "steps": [
+        {
+          "title": "Enter an alias for your key (ex. \"goldkey\")",
+          "description": [
+            "An alias is a friendly label used to distinguish between keys. We will use this alias in the next step."
+          ]
+        },
+        {
+          "title": "Click the \"Submit\" button"
+        }
+      ]
+    }
+  },
+  {
+    "component": "TutorialInfo",
+    "objectType": "mockhsm",
+    "title": "Tutorial - Creating keys - Step 1 of 7",
+    "button": "Learn about assets",
+    "route": "assets",
+    "image": "mockhsm",
+    "content": [
+      {
+        "type": "mockhsm",
+        "line": "Great work! You created a new key in the MockHSM named \"STRING\". Next up, we'll learn about assets and how to create them."
+      }
+
+    ]
+  },
+  {
+    "component": "TutorialInfo",
+    "title": "Tutorial - Creating assets - Step 2 of 7",
+    "button": "Create asset",
+    "route": "assets/create",
+    "image": "asset",
+    "content": [
+      "An asset is a type of value that can be issued on a blockchain. All units of an asset are fungible and can be transacted directly between parties without the involvement of the issuer.",
+      "Let's create a new asset now."
+    ]
+  },
+  {
+    "component": "TutorialForm",
+    "objectType": "asset",
+    "title": "Tutorial - Creating assets - Step 2 of 7",
+    "content": {
+      "header": "Create asset",
+      "steps": [
+        {
+          "title": "Enter an alias for your asset (ex. \"gold\")",
+          "description": [
+            "An alias is a friendly label used to distinguish between assets."
+          ]
+        },
+        {
+          "title": "Select \"Use existing MockHSM key\" under \"Keys and Signing\"",
+          "description": [
+            {
+              "type": "mockhsm",
+              "line": "Choose STRING key from the dropdown"
+            }
+          ]
+        },
+        {
+          "title": "Click the \"Submit\" button"
+        }
+      ]
+    }
+  },
+  {
+    "component": "TutorialInfo",
+    "objectType": "asset",
+    "title": "Tutorial - Creating assets - Step 2 of 7",
+    "button": "Learn about accounts",
+    "route": "accounts",
+    "image": "asset",
+    "content": [
+      {
+        "type": "asset",
+        "line": "Great! You created a new asset type named \"STRING\". Next up, we'll learn about accounts and create two of them for parties who want to trade assets."
+      }
+    ]
+  },
+  {
+    "component": "TutorialInfo",
+    "title": "Tutorial - Creating accounts - Step 3 of 7",
+    "button": "Create account",
+    "route": "accounts/create",
+    "image": "account",
+    "content": [
+      "An account is an object in Chain Core that tracks ownership of assets on a blockchain by creating and tracking control programs. The account object does not exist on the blockchain - it is local to this Chain Core. Only the control programs created in the account are visible on the blockchain.",
+      "We'll be moving assets from one account to another; let's create the first one now."
+    ]
+  },
+  {
+    "component": "TutorialForm",
+    "objectType": "account",
+    "title": "Tutorial - Creating accounts - Step 3 of 7",
+    "content": {
+      "header": "Create account",
+      "steps": [
+        {
+          "title": "Type name for account alias (ex. \"alice\")",
+          "description": [
+            "An alias is a friendly  label used to distinguish between accounts."
+          ]
+        },
+        {
+          "title": "Select \"Generate new MockHSM key\" under \"Keys and Signing\"",
+          "description": [
+            "You can create a new MockHSM key here as well. Enter a descriptive alias for your new key (ex. 'aliceKey')."
+          ]
+        },
+        {
+          "title": "Click the \"Submit\" button"
+        }
+      ]
+    }
+  },
+  {
+    "component": "TutorialInfo",
+    "objectType": "account",
+    "title": "Tutorial - Accounts - Step 3 of 7",
+    "button": "Create another account",
+    "route": "accounts/create",
+    "image": "account",
+    "content": [
+      {
+        "type": "accounts",
+        "index": 0,
+        "line": "Great! You created a new account named \"STRING\". In order to do some trading, let's create our second account."
+      }
+    ]
+  },
+  {
+    "component": "TutorialForm",
+    "objectType": "account",
+    "title": "Tutorial - Accounts - Step 3 of 7",
+    "button": null,
+    "route": null,
+    "content": {
+      "header": "Create 2nd account",
+      "steps": [
+        {
+          "title": "Type name for account alias (ex. \"bob\")",
+          "description": [
+            "An alias is a friendly  label used to distinguish between accounts."
+          ]
+        },
+        {
+          "title": "Select \"Generate new MockHSM key\" under \"Keys and Signing\"",
+          "description": [
+            "You can leave the name field blank, and Chain Core will generate a key with a unique name based on your alias."
+          ]
+        },
+        {
+          "title": "Click the \"Submit\" button"
+        }
+      ]
+    }
+  },
+  {
+    "component": "TutorialInfo",
+    "objectType": "account",
+    "title": "Tutorial - Accounts - Step 4 of 7",
+    "button": "Learn about transactions",
+    "route": "transactions",
+    "image": "account",
+    "content": [
+      {
+        "type": "accounts",
+        "index": 1,
+        "line": "Great! Now we've got an account for \"STRING\" too. Let's start to learn about transactions and how to issue units of your asset."
+      }
+    ]
+  },
+  {
+    "component": "TutorialInfo",
+    "title": "Tutorial - Issuing - Step 5 of 7",
+    "button": "Create issuance transaction",
+    "route": "transactions/create",
+    "image": "transaction",
+    "content": [
+      {
+        "type": "asset",
+        "line": "Chain Core allows you to build transactions using a predefined set of actions to issue, spend, control, or retire asset units. We'll start by creating a transaction that issues units of your \"STRING\" asset."
+      }
+    ]
+  },
+  {
+    "component": "TutorialForm",
+    "objectType": "transaction",
+    "title": "Tutorial - Issuing - Step 5 of 7",
+    "button": null,
+    "route": null,
+    "content": {
+      "header": "Issue asset units",
+      "steps": [
+        {
+          "title": "Click the \"Add action\" button",
+          "description": [
+            "Select \"Issue\" to add a new action to your transaction."
+          ]
+        },
+        {
+          "title": "Fill in the issue action",
+          "description": [
+            {
+              "type": "asset",
+              "line": "Enter STRING for the asset alias "
+            },
+            {
+              "type": 100,
+              "line": "and STRING for the amount."
+            }
+          ]
+        },
+        {
+          "title": "Click the \"Add action\" button",
+          "description": [
+            "Select \"Control with account\"."
+          ]
+        },
+        {
+          "title": "Fill in the control action",
+          "description": [
+            {
+              "type": "accounts",
+              "index": 0,
+              "line": "Enter STRING for the account alias, "
+            },
+            {
+              "type": "asset",
+              "line": " STRING for the asset alias"
+            },
+            {
+              "type": 100,
+              "line": " and STRING for the amount."
+            }
+          ]
+        },
+        {
+          "title": "Click the \"Submit transaction\" button"
+        }
+      ]
+    }
+  },
+  {
+    "component": "TutorialInfo",
+    "objectType": "transaction",
+    "title": "Tutorial - Issuing - Step 5 of 7",
+    "button": "Spend asset units",
+    "route": "transactions/create",
+    "image": "transaction",
+    "content": [
+      {
+        "type": "asset",
+        "line": "You just issued your first units of \"STRING\" onto the blockchain. Nice work!"
+      },
+      "When you clicked \"Submit transaction\", Chain Core Dashboard built your blockchain transaction in three steps:",
+      {
+        "list": [
+          "your raw actions were sent to Chain Core and an unsigned transaction was returned.",
+          "the Dashboard submitted the unsigned transaction to the MockHSM for signature.",
+          "the signed transaction was submitted to the blockchain (you can see the committed result below)."
+        ]
+      },
+      {
+        "type": "accounts",
+        "index": 1,
+        "line": "Now let's transfer some of the newly issued asset units to \"STRING\"."
+      }
+    ]
+  },
+  {
+    "component": "TutorialForm",
+    "objectType": "transaction",
+    "title": "Tutorial - Transferring - Step 6 of 7",
+    "button": null,
+    "route": null,
+    "content": {
+      "header": "Transfer asset units",
+      "steps": [
+        {
+          "title": "Add a \"Spend from account\" action",
+          "description": [
+            {
+              "type": "accounts",
+              "index": 0,
+              "line": "Enter STRING for the account alias, "
+            },
+            {
+              "type": "asset",
+              "line": "STRING for the asset alias "
+            },
+            {
+              "type": 10,
+              "line": "and STRING for the amount."
+            }
+          ]
+        },
+        {
+          "title": "Add a \"Control with account\" action",
+          "description": [
+            {
+              "type": "accounts",
+              "index": 1,
+              "line": "Enter STRING for the account alias, "
+            },
+            {
+              "type": "asset",
+              "line": "STRING for the asset alias "
+            },
+            {
+              "type": 10,
+              "line": "and STRING for the amount."
+            }
+          ]
+        },
+        {
+          "title": "Click the \"Submit transaction\" button"
+        }
+      ]
+    }
+  },
+  {
+    "component": "TutorialInfo",
+    "objectType": "transaction",
+    "title": "Tutorial - Transferring - Step 6 of 7",
+    "button": "Retire asset units",
+    "route": "transactions/create",
+    "image": "transaction",
+    "content": [
+      {
+        "type": "accounts",
+        "index": 1,
+        "line": "Rad! You transferred 10 units of your asset to \"STRING\"."
+      },
+      "In your transaction, the total amount of assets spent was equal to the total amount of assets controlled (received). Any combination of actions can be added to a single transaction, as long as what goes in equals what comes out.",
+      {
+        "type": "asset",
+        "line": "Finally, let's retire some units of \"STRING\" from circulation."
+      }
+    ]
+  },
+  {
+    "component": "TutorialForm",
+    "objectType": "transaction",
+    "title": "Tutorial - Retiring - Step 7 of 7",
+    "content": {
+      "header": "Retire asset units",
+      "steps": [
+        {
+          "title": "Add a \"Spend from account\" action",
+          "description": [
+            {
+              "type": "accounts",
+              "index": 1,
+              "line": "Enter STRING for the account alias, "
+            },
+            {
+              "type": "asset",
+              "line": "STRING for the asset alias "
+            },
+            {
+              "type": 5,
+              "line": "and STRING for the amount."
+            }
+          ]
+        },
+        {
+          "title": "Add a \"Retire\" action",
+          "description": [
+            {
+              "type": "asset",
+              "line": "Enter STRING for the asset alias "
+            },
+            {
+              "type": 5,
+              "line": "and STRING for the amount."
+            }
+          ]
+        },
+        {
+          "title": "Click the \"Submit transaction\" button"
+        }
+      ]
+    }
+  },
+  {
+    "component": "TutorialInfo",
+    "objectType": "transaction",
+    "title": "Tutorial - Retiring - Step 7 of 7",
+    "button": "Finish tutorial",
+    "dismiss": " ",
+    "route": "transactions",
+    "image": "transaction",
+    "content": [
+      {
+        "type": "asset",
+        "line": "That's it - your 5 units of \"STRING\" have been removed from circulation. These units can no longer be spent, but their history is a permanent part of the blockchain."
+      },
+      "Let's wrap up this tutorial with a quick recap of what we've learned."
+    ]
+  },
+  {
+    "component": "TutorialModal",
+    "title": "Tutorial - Complete",
+    "dismiss": "Close",
+    "content": [
+      "Congratulations! You now know the basics of:",
+      {
+        "list": [
+          "creating cryptographic keys",
+          "using keys to create assets and accounts",
+          "building transactions to issue, spend, and retire"
+        ]
+      },
+      "To dig deeper, check out the \"Documentation\" link in the sidebar for information about building applications, using our SDKs, and how the blockchain works.",
+      "You can revisit this tutorial at any time by clicking \"Tutorial\" in the sidebar."
+    ]
+  }
+]
diff --git a/src/features/unspents/actions.js b/src/features/unspents/actions.js
new file mode 100644 (file)
index 0000000..3227b54
--- /dev/null
@@ -0,0 +1,6 @@
+import { baseListActions } from 'features/shared/actions'
+import { chainClient } from 'utility/environment'
+
+export default baseListActions('unspent', {
+  clientApi: () => chainClient().unspentOutputs
+})
diff --git a/src/features/unspents/components/List.jsx b/src/features/unspents/components/List.jsx
new file mode 100644 (file)
index 0000000..50ea34d
--- /dev/null
@@ -0,0 +1,16 @@
+import React from 'react'
+import { BaseList } from 'features/shared/components'
+import ListItem from './ListItem'
+
+const type = 'unspent'
+
+const newStateToProps = (state, ownProps) => ({
+  ...BaseList.mapStateToProps(type, ListItem)(state, ownProps),
+  skipCreate: true,
+  label: 'unspent outputs'
+})
+
+export default BaseList.connect(
+  newStateToProps,
+  BaseList.mapDispatchToProps(type)
+)
diff --git a/src/features/unspents/components/ListItem.jsx b/src/features/unspents/components/ListItem.jsx
new file mode 100644 (file)
index 0000000..41bc6e0
--- /dev/null
@@ -0,0 +1,20 @@
+import React from 'react'
+import { KeyValueTable, RawJsonButton, } from 'features/shared/components'
+import { buildUnspentDisplay } from 'utility/buildInOutDisplay'
+
+class ListItem extends React.Component {
+  render() {
+    return(<KeyValueTable
+            title={
+              <span>
+                ID <code>{this.props.item.id}</code>
+              </span>
+             }
+            actions={[
+              <RawJsonButton key='raw-json' item={this.props.item} />
+            ]}
+            items={buildUnspentDisplay(this.props.item)} />)
+  }
+}
+
+export default ListItem
diff --git a/src/features/unspents/components/index.js b/src/features/unspents/components/index.js
new file mode 100644 (file)
index 0000000..ac005ec
--- /dev/null
@@ -0,0 +1,5 @@
+import List from './List'
+
+export {
+  List,
+}
diff --git a/src/features/unspents/index.js b/src/features/unspents/index.js
new file mode 100644 (file)
index 0000000..bf77b37
--- /dev/null
@@ -0,0 +1,9 @@
+import actions from './actions'
+import reducers from './reducers'
+import routes from './routes'
+
+export {
+  actions,
+  reducers,
+  routes,
+}
diff --git a/src/features/unspents/reducers.js b/src/features/unspents/reducers.js
new file mode 100644 (file)
index 0000000..e37cbb4
--- /dev/null
@@ -0,0 +1,10 @@
+import { reducers } from 'features/shared'
+import { combineReducers } from 'redux'
+
+const type = 'unspent'
+const idFunc = item => `${item.id}`
+
+export default combineReducers({
+  items: reducers.itemsReducer(type, idFunc),
+  queries: reducers.queriesReducer(type, idFunc)
+})
diff --git a/src/features/unspents/routes.js b/src/features/unspents/routes.js
new file mode 100644 (file)
index 0000000..e398a16
--- /dev/null
@@ -0,0 +1,4 @@
+import { List } from './components'
+import { makeRoutes } from 'features/shared'
+
+export default (store) => makeRoutes(store, 'unspent', List)
diff --git a/src/reducers.js b/src/reducers.js
new file mode 100644 (file)
index 0000000..8016a56
--- /dev/null
@@ -0,0 +1,63 @@
+import { combineReducers } from 'redux'
+import { routerReducer as routing} from 'react-router-redux'
+import { reducer as form } from 'redux-form'
+import accessControl from 'features/accessControl/reducers'
+import { reducers as account } from 'features/accounts'
+import { reducers as app } from 'features/app'
+import { reducers as asset } from 'features/assets'
+import { reducers as balance } from 'features/balances'
+import { reducers as core } from 'features/core'
+import { reducers as mockhsm } from 'features/mockhsm'
+import { reducers as testnet } from 'features/testnet'
+import { reducers as transaction } from 'features/transactions'
+import { reducers as transactionFeed } from 'features/transactionFeeds'
+import { reducers as tutorial } from 'features/tutorial'
+import { reducers as unspent } from 'features/unspents'
+import { clear as clearStorage } from 'utility/localStorage'
+
+const makeRootReducer = () => (state, action) => {
+  if (action.type == 'UPDATE_CORE_INFO' &&
+      !action.param.isConfigured) {
+    const newState = {
+      form: state.form,
+      routing: state.routing,
+    }
+
+    if (state.core.blockchainId == (action.param.blockchainId || 0)) {
+      newState.core = state.core
+    }
+
+    state = newState
+  } else if (action.type == 'USER_LOG_OUT') {
+    // TODO: see if we can't move this outside of a reducer..
+
+    // Actions still may fire after the location redirect, so make sure they
+    // fire against blank state, and the local storage listener doesn't
+    // persist state.
+    state = undefined
+
+    // Clear tokens and other state from local storage.
+    clearStorage()
+
+    // Finally, reboot the entire dashboard app via a hard redirect.
+    window.location.href = '/'
+  }
+
+  return combineReducers({
+    accessControl,
+    account,
+    app,
+    asset,
+    balance,
+    core,
+    form,
+    mockhsm,
+    routing,
+    testnet,
+    transaction,
+    transactionFeed,
+    tutorial,
+    unspent,
+  })(state, action)
+}
+export default makeRootReducer
diff --git a/src/routes.js b/src/routes.js
new file mode 100644 (file)
index 0000000..4d5da8d
--- /dev/null
@@ -0,0 +1,35 @@
+import { Container } from 'features/app/components'
+import { NotFound } from 'features/shared/components'
+import accessControl from 'features/accessControl/routes'
+import { routes as accounts } from 'features/accounts'
+import { routes as assets } from 'features/assets'
+import { routes as balances } from 'features/balances'
+import { routes as configuration } from 'features/configuration'
+import { routes as core } from 'features/core'
+import { routes as transactions } from 'features/transactions'
+import { routes as transactionFeeds } from 'features/transactionFeeds'
+import { routes as unspents } from 'features/unspents'
+import { routes as mockhsm } from 'features/mockhsm'
+
+const makeRoutes = (store) => ({
+  path: '/',
+  component: Container,
+  childRoutes: [
+    accessControl(store),
+    accounts(store),
+    assets(store),
+    balances(store),
+    configuration,
+    core,
+    transactions(store),
+    transactionFeeds(store),
+    unspents(store),
+    mockhsm(store),
+    {
+      path: '*',
+      component: NotFound
+    }
+  ]
+})
+
+export default makeRoutes
diff --git a/src/utility/buildInOutDisplay.js b/src/utility/buildInOutDisplay.js
new file mode 100644 (file)
index 0000000..93f8c9e
--- /dev/null
@@ -0,0 +1,104 @@
+const mappings = {
+  id: 'ID',
+  type: 'Type',
+  purpose: 'Purpose',
+  transactionId: 'Transaction ID',
+  position: 'Position',
+  assetId: 'Asset ID',
+  assetAlias: 'Asset Alias',
+  assetDefinition: 'Asset Definition',
+  assetTags: 'Asset Tags',
+  assetIsLocal: 'Asset Is Local?',
+  amount: 'Amount',
+  accountId: 'Account ID',
+  accountAlias: 'Account Alias',
+  accountTags: 'Account Tags',
+  controlProgram: 'Control Program',
+  spentOutputId: 'Spent Output ID',
+  issuanceProgram: 'Issuance Program',
+  isLocal: 'Local?',
+  referenceData: 'Reference Data',
+}
+
+const txInputFields = [
+  'type',
+  'assetId',
+  'assetAlias',
+  'assetDefinition',
+  'assetTags',
+  'assetIsLocal',
+  'amount',
+  'accountId',
+  'accountAlias',
+  'accountTags',
+  'issuanceProgram',
+  'spentOutputId',
+  'isLocal',
+  'referenceData',
+]
+
+const txOutputFields = [
+  'type',
+  'purpose',
+  'id',
+  'position',
+  'assetId',
+  'assetAlias',
+  'assetDefinition',
+  'assetTags',
+  'assetIsLocal',
+  'amount',
+  'accountId',
+  'accountAlias',
+  'accountTags',
+  'controlProgram',
+  'isLocal',
+  'referenceData',
+]
+
+const unspentFields = [
+  'type',
+  'purpose',
+  'transactionId',
+  'position',
+  'assetId',
+  'assetAlias',
+  'assetDefinition',
+  'assetTags',
+  'assetIsLocal',
+  'amount',
+  'accountId',
+  'accountAlias',
+  'accountTags',
+  'controlProgram',
+  'isLocal',
+  'referenceData',
+]
+
+const balanceFields = Object.keys(mappings)
+
+const buildDisplay = (item, fields) => {
+  const details = []
+  fields.forEach(key => {
+    if (item.hasOwnProperty(key)) {
+      details.push({label: mappings[key], value: item[key]})
+    }
+  })
+  return details
+}
+
+export function buildTxInputDisplay(input) {
+  return buildDisplay(input, txInputFields)
+}
+
+export function buildTxOutputDisplay(output) {
+  return buildDisplay(output, txOutputFields)
+}
+
+export function buildUnspentDisplay(output) {
+  return buildDisplay(output, unspentFields)
+}
+
+export function buildBalanceDisplay(balance) {
+  return buildDisplay({amount: balance.amount, ...balance.sumBy}, balanceFields)
+}
diff --git a/src/utility/clipboard.js b/src/utility/clipboard.js
new file mode 100644 (file)
index 0000000..f70732e
--- /dev/null
@@ -0,0 +1,14 @@
+// Assumes the presence of an input element with ID #_copyInput
+export const copyToClipboard = value => {
+  const listener = e => {
+    e.clipboardData.setData('text/plain', value)
+    e.preventDefault()
+    document.removeEventListener('copy', listener)
+  }
+
+  // Required for Safari. Contents of selection are not used.
+  document.getElementById('_copyInput').select()
+
+  document.addEventListener('copy', listener)
+  document.execCommand('copy')
+}
diff --git a/src/utility/componentClassNames.js b/src/utility/componentClassNames.js
new file mode 100644 (file)
index 0000000..cde90ae
--- /dev/null
@@ -0,0 +1,12 @@
+import React from 'react'
+import classNames from 'classnames'
+
+const componentClassNames = (owner, ...args) => {
+  if (!React.Component.prototype.isPrototypeOf(owner)) {
+    throw new Error('Component class must descend from React.Component')
+  }
+
+  return classNames(owner.constructor.name, args)
+}
+
+export default componentClassNames
diff --git a/src/utility/disableAutocomplete.js b/src/utility/disableAutocomplete.js
new file mode 100644 (file)
index 0000000..5ffbbd3
--- /dev/null
@@ -0,0 +1,6 @@
+export default {
+  autoComplete: 'off',
+  autoCorrect: 'off',
+  autoCapitalize: 'none',
+  spellCheck: 'false'
+}
diff --git a/src/utility/environment.js b/src/utility/environment.js
new file mode 100644 (file)
index 0000000..6108c17
--- /dev/null
@@ -0,0 +1,34 @@
+/* global process */
+
+import chainSdk from 'chain-sdk'
+import { store } from 'app'
+
+import { useRouterHistory } from 'react-router'
+import { createHistory } from 'history'
+
+let apiHost, basename
+if (process.env.NODE_ENV === 'production') {
+  apiHost = window.location.origin
+  basename = '/dashboard'
+} else {
+  apiHost = process.env.API_URL || 'http://localhost:3000/api'
+  basename = ''
+}
+
+export const chainClient = () => new chainSdk.Client({
+  url: apiHost,
+  accessToken: store.getState().core.clientToken
+})
+
+export const chainSigner = () => new chainSdk.HsmSigner()
+
+// react-router history object
+export const history = useRouterHistory(createHistory)({
+  basename: basename
+})
+
+export const pageSize = 25
+
+export const testnetInfoUrl = process.env.TESTNET_INFO_URL || 'https://testnet-info.chain.com'
+export const testnetUrl = process.env.TESTNET_GENERATOR_URL || 'https://testnet.chain.com'
+export const docsRoot = 'https://chain.com/docs/1.2'
diff --git a/src/utility/localStorage.js b/src/utility/localStorage.js
new file mode 100644 (file)
index 0000000..86f7051
--- /dev/null
@@ -0,0 +1,44 @@
+export const clear = () => {
+  try {
+    localStorage.clear()
+  } catch (err) {
+    // Local storage is not available.
+  }
+}
+
+export const exportState = (store) => () => {
+  const state = store.getState()
+  const exportable = {
+    core: {
+      clientToken: (state.core || {}).clientToken,
+
+      // TODO: If the dashboard has a way of probing the core for a token
+      // requirement, we won't need to store these anymore.
+      requireClientToken: (state.core || {}).requireClientToken,
+      validToken: (state.core || {}).validToken,
+    },
+    transaction: {
+      generated: (state.transaction || {}).generated,
+    },
+    tutorial: state.tutorial
+  }
+
+  try {
+    localStorage.setItem('reduxState', JSON.stringify(exportable))
+  } catch (err) { /* localstorage not available */ }
+}
+
+export const importState = () => {
+  let state
+  try {
+    state = localStorage.getItem('reduxState')
+  } catch (err) { /* localstorage not available */ }
+
+  if (!state) return {}
+
+  try {
+    return JSON.parse(state)
+  } catch (_) {
+    return {}
+  }
+}
diff --git a/src/utility/string.js b/src/utility/string.js
new file mode 100644 (file)
index 0000000..56f3459
--- /dev/null
@@ -0,0 +1,24 @@
+import _pluralize from 'pluralize'
+import { snakeCase } from 'lodash'
+
+export const pluralize = _pluralize
+
+export const capitalize = (string) => {
+  return string.charAt(0).toUpperCase() + string.slice(1)
+}
+
+export const humanize = (string) => {
+  return snakeCase(string)
+    .replace(/_/g, ' ')
+}
+
+export const parseNonblankJSON = (json) => {
+  json = json || ''
+  json = json.trim()
+
+  if (json == '') {
+    return null
+  }
+
+  return JSON.parse(json)
+}
diff --git a/src/utility/time.js b/src/utility/time.js
new file mode 100644 (file)
index 0000000..8266886
--- /dev/null
@@ -0,0 +1,100 @@
+/**
+ * Calculates the average change per second of a variable sampled at various times.
+ */
+export class DeltaSampler {
+  constructor({sampleTtl = 60*1000, maxSamples = 30} = {}) {
+    this.sampleTtl = sampleTtl
+    this.maxSamples = maxSamples
+    this.samples = []
+  }
+
+  sample(value) {
+    this.samples.push({
+      value,
+      time: Date.now(),
+    })
+
+    if (this.samples.length > this.maxSamples) {
+      this.samples.shift()
+    }
+
+    return this.average()
+  }
+
+  /**
+   * Returns the average growth of the value per second.
+   * Algorithm: sum the changes
+   */
+  average() {
+    const cutoff = Date.now() - this.sampleTtl
+
+    let earliest = null
+    let latest = null
+
+    for (let i = 0; i < this.samples.length; i++) {
+      const s = this.samples[i]
+      if (s.time < cutoff) continue
+      if (earliest === null) earliest = s
+      latest = s
+    }
+
+    if (earliest === latest) {
+      return NaN
+    }
+
+    return 1000 * (latest.value - earliest.value) / (latest.time - earliest.time)
+  }
+}
+
+export const humanizeDuration = seconds => {
+  if (seconds == 0) {
+    return '0s'
+  }
+
+  const sec = 1
+  const min = 60 * sec
+  const hr = 60 * min
+  const day = 24 * hr
+
+  let bigUnit, littleUnit, bigLabel, littleLabel
+
+  if (seconds >= day) {
+    bigUnit = day
+    littleUnit = hr
+    bigLabel = 'd'
+    littleLabel = 'h'
+  } else if (seconds >= hr) {
+    bigUnit = hr
+    littleUnit = min
+    bigLabel = 'h'
+    littleLabel = 'm'
+  } else {
+    bigUnit = min
+    littleUnit = sec
+    bigLabel = 'm'
+    littleLabel = 's'
+  }
+
+  const bigVal = Math.floor(seconds / bigUnit)
+  const littleVal = Math.round((seconds % bigUnit) / littleUnit)
+
+  // Rounding may give us little-unit vals of 60s, 24h, etc.
+  if (littleVal == bigUnit / littleUnit) {
+    return `${bigVal + 1}${bigLabel}`
+  }
+
+  const big = `${bigVal}${bigLabel}`
+  const little = `${littleVal}${littleLabel}`
+
+  // Don't show little unit if the big unit is in double digits
+  if (bigVal > 9 || littleVal == 0) {
+    return big
+  }
+
+  // For values that round to under 60 seconds
+  if (bigVal == 0) {
+    return little
+  }
+
+  return `${big} ${little}`
+}
diff --git a/static/.DS_Store b/static/.DS_Store
new file mode 100644 (file)
index 0000000..7be6f57
Binary files /dev/null and b/static/.DS_Store differ
diff --git a/static/fonts/nitti-normal.woff b/static/fonts/nitti-normal.woff
new file mode 100644 (file)
index 0000000..5ea48ee
Binary files /dev/null and b/static/fonts/nitti-normal.woff differ
diff --git a/static/fonts/nittigrotesk-medium.woff b/static/fonts/nittigrotesk-medium.woff
new file mode 100644 (file)
index 0000000..1230bee
Binary files /dev/null and b/static/fonts/nittigrotesk-medium.woff differ
diff --git a/static/fonts/nittigrotesk-normal.woff b/static/fonts/nittigrotesk-normal.woff
new file mode 100644 (file)
index 0000000..aacee1d
Binary files /dev/null and b/static/fonts/nittigrotesk-normal.woff differ
diff --git a/static/images/chain-favicon.png b/static/images/chain-favicon.png
new file mode 100644 (file)
index 0000000..8df0d4f
Binary files /dev/null and b/static/images/chain-favicon.png differ
diff --git a/static/images/chevron-blue.png b/static/images/chevron-blue.png
new file mode 100644 (file)
index 0000000..fdbd762
Binary files /dev/null and b/static/images/chevron-blue.png differ
diff --git a/static/images/chevron-green.png b/static/images/chevron-green.png
new file mode 100644 (file)
index 0000000..0fee6c4
Binary files /dev/null and b/static/images/chevron-green.png differ
diff --git a/static/images/chevron.png b/static/images/chevron.png
new file mode 100644 (file)
index 0000000..19f388d
Binary files /dev/null and b/static/images/chevron.png differ
diff --git a/static/images/config/join-active.png b/static/images/config/join-active.png
new file mode 100644 (file)
index 0000000..fb3827b
Binary files /dev/null and b/static/images/config/join-active.png differ
diff --git a/static/images/config/join.png b/static/images/config/join.png
new file mode 100644 (file)
index 0000000..4609499
Binary files /dev/null and b/static/images/config/join.png differ
diff --git a/static/images/config/new-active.png b/static/images/config/new-active.png
new file mode 100644 (file)
index 0000000..45b551f
Binary files /dev/null and b/static/images/config/new-active.png differ
diff --git a/static/images/config/new.png b/static/images/config/new.png
new file mode 100644 (file)
index 0000000..9b2ca15
Binary files /dev/null and b/static/images/config/new.png differ
diff --git a/static/images/config/testnet-active.png b/static/images/config/testnet-active.png
new file mode 100644 (file)
index 0000000..aa3a61e
Binary files /dev/null and b/static/images/config/testnet-active.png differ
diff --git a/static/images/config/testnet.png b/static/images/config/testnet.png
new file mode 100644 (file)
index 0000000..9a3520b
Binary files /dev/null and b/static/images/config/testnet.png differ
diff --git a/static/images/empty/account.svg b/static/images/empty/account.svg
new file mode 100644 (file)
index 0000000..b3d1228
--- /dev/null
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg width="70px" height="70px" viewBox="0 0 70 70" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <!-- Generator: sketchtool 40.2 (33826) - http://www.bohemiancoding.com/sketch -->
+    <title>B5411677-47F3-4349-940F-8B25C024DA78</title>
+    <desc>Created with sketchtool.</desc>
+    <defs></defs>
+    <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="account:-empty-state" transform="translate(-796.000000, -246.000000)" fill="#A3A9B8">
+            <path d="M849.62069,265.612069 C849.62069,254.75 841.229885,246 830.885057,246 C820.54023,246 812.149425,254.810345 812.149425,265.612069 C812.149425,276.413793 820.54023,285.284483 830.885057,285.284483 C841.229885,285.284483 849.62069,276.474138 849.62069,265.612069 Z M844.678161,265.612069 C844.678161,273.577586 838.471264,280.094828 830.885057,280.094828 C823.298851,280.094828 817.091954,273.577586 817.091954,265.612069 C817.091954,257.646552 823.298851,251.12931 830.885057,251.12931 C838.471264,251.12931 844.678161,257.646552 844.678161,265.612069 Z M863.528736,316 C864.908046,316 866,314.853448 866,313.405172 C866,299.827586 855.482759,288.724138 842.494253,288.724138 L819.505747,288.724138 C806.574713,288.724138 796,299.767241 796,313.405172 C796,314.853448 797.091954,316 798.471264,316 L863.528736,316 Z M842.494253,293.913793 C851.91954,293.913793 859.678161,301.275862 860.885057,310.810345 L801.114943,310.810345 C802.321839,301.336207 810.08046,293.913793 819.505747,293.913793 L842.494253,293.913793 Z" id="account-icon"></path>
+        </g>
+    </g>
+</svg>
\ No newline at end of file
diff --git a/static/images/empty/asset.svg b/static/images/empty/asset.svg
new file mode 100644 (file)
index 0000000..6766fc1
--- /dev/null
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg width="76px" height="76px" viewBox="0 0 76 76" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <!-- Generator: Sketch 40.3 (33839) - http://www.bohemiancoding.com/sketch -->
+    <title>asset-empty-state</title>
+    <desc>Created with Sketch.</desc>
+    <defs></defs>
+    <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="asset-empty-state" transform="translate(3.000000, 3.000000)" stroke="#A3A9B8" stroke-width="5">
+            <path d="M35,69.5772217 C54.3299662,69.5772217 70,54.0018301 70,34.7886109 C70,18.4236743 58.6316272,4.69792672 43.3163395,0.988002827 C40.650746,0.342298404 37.8655899,0 35,0 C15.6700338,0 0,15.5753916 0,34.7886109 C0,54.0018301 15.6700338,69.5772217 35,69.5772217 Z" id="Oval-3"></path>
+            <ellipse id="Oval-4" stroke-linecap="square" stroke-dasharray="1,11" cx="35" cy="34.7886109" rx="23.3333333" ry="23.1924072"></ellipse>
+        </g>
+    </g>
+</svg>
\ No newline at end of file
diff --git a/static/images/empty/balance.svg b/static/images/empty/balance.svg
new file mode 100644 (file)
index 0000000..9217b7f
--- /dev/null
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg width="66px" height="70px" viewBox="0 0 66 70" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <!-- Generator: sketchtool 40.2 (33826) - http://www.bohemiancoding.com/sketch -->
+    <title>A16A9544-D417-4F5C-9987-9DFE3173D967</title>
+    <desc>Created with sketchtool.</desc>
+    <defs></defs>
+    <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="balances:-empty-state" transform="translate(-798.000000, -246.000000)" fill="#A3A9B8">
+            <path d="M802.639767,311.055986 L802.639767,248.472007 C802.639767,247.085271 801.621281,246 800.319883,246 C799.018485,246 798,247.085271 798,248.472007 L798,313.527993 C798,314.914729 799.018485,316 800.319883,316 L861.372424,316 C862.673822,316 863.692308,314.914729 863.692308,313.527993 C863.692308,312.141258 862.673822,311.055986 861.372424,311.055986 L802.639767,311.055986 Z M825.103028,265.956934 C825.103028,264.570198 824.084543,263.484927 822.783145,263.484927 C821.481747,263.484927 820.463261,264.570198 820.463261,265.956934 L820.463261,301.529716 C820.463261,302.916451 821.481747,304.001723 822.783145,304.001723 C824.084543,304.001723 825.103028,302.916451 825.103028,301.529716 L825.103028,265.956934 Z M813.899689,265.956934 C813.899689,264.570198 812.881203,263.484927 811.579805,263.484927 C810.278407,263.484927 809.259922,264.570198 809.259922,265.956934 L809.259922,301.529716 C809.259922,302.916451 810.278407,304.001723 811.579805,304.001723 C812.881203,304.001723 813.899689,302.916451 813.899689,301.529716 L813.899689,265.956934 Z M847.509706,265.956934 C847.509706,264.570198 846.491221,263.484927 845.189823,263.484927 C843.888425,263.484927 842.86994,264.570198 842.86994,265.956934 L842.86994,301.529716 C842.86994,302.916451 843.888425,304.001723 845.189823,304.001723 C846.491221,304.001723 847.509706,302.916451 847.509706,301.529716 L847.509706,265.956934 Z M858.713046,283.743325 C858.713046,282.356589 857.69456,281.271318 856.393162,281.271318 C855.091764,281.271318 854.073279,282.356589 854.073279,283.743325 L854.073279,301.529716 C854.073279,302.916451 855.091764,304.001723 856.393162,304.001723 C857.69456,304.001723 858.713046,302.916451 858.713046,301.529716 L858.713046,283.743325 Z M836.306367,283.743325 C836.306367,282.356589 835.287882,281.271318 833.986484,281.271318 C832.685086,281.271318 831.6666,282.356589 831.6666,283.743325 L831.6666,301.529716 C831.6666,302.916451 832.685086,304.001723 833.986484,304.001723 C835.287882,304.001723 836.306367,302.916451 836.306367,301.529716 L836.306367,283.743325 Z" id="Balances-icon"></path>
+        </g>
+    </g>
+</svg>
\ No newline at end of file
diff --git a/static/images/empty/client_access_token.svg b/static/images/empty/client_access_token.svg
new file mode 100644 (file)
index 0000000..7ad3b4d
--- /dev/null
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="12px" height="12px" viewBox="0 0 12 12" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <!-- Generator: Sketch 41.2 (35397) - http://www.bohemiancoding.com/sketch -->
+    <title>client</title>
+    <desc>Created with Sketch.</desc>
+    <defs>
+        <rect id="path-1" x="0" y="0" width="11.0769231" height="3.37236534" rx="1.68618267"></rect>
+        <mask id="mask-2" maskContentUnits="userSpaceOnUse" maskUnits="objectBoundingBox" x="0" y="0" width="11.0769231" height="3.37236534" fill="white">
+            <use xlink:href="#path-1"></use>
+        </mask>
+        <rect id="path-3" x="0" y="4.21545667" width="11.0769231" height="3.37236534" rx="1.68618267"></rect>
+        <mask id="mask-4" maskContentUnits="userSpaceOnUse" maskUnits="objectBoundingBox" x="0" y="0" width="11.0769231" height="3.37236534" fill="white">
+            <use xlink:href="#path-3"></use>
+        </mask>
+        <rect id="path-5" x="0" y="8.43091335" width="11.0769231" height="3.37236534" rx="1.68618267"></rect>
+        <mask id="mask-6" maskContentUnits="userSpaceOnUse" maskUnits="objectBoundingBox" x="0" y="0" width="11.0769231" height="3.37236534" fill="white">
+            <use xlink:href="#path-5"></use>
+        </mask>
+    </defs>
+    <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="icon-set" transform="translate(-92.000000, -8.000000)">
+            <g id="icons">
+                <g id="client" transform="translate(84.000000, 0.000000)">
+                    <g id="server" transform="translate(8.000000, 8.000000)">
+                        <use id="Rectangle" stroke="#A3A9B8" mask="url(#mask-2)" stroke-width="1.4" xlink:href="#path-1"></use>
+                        <use id="Rectangle" stroke="#A3A9B8" mask="url(#mask-4)" stroke-width="1.4" xlink:href="#path-3"></use>
+                        <ellipse id="Oval" fill="#A3A9B8" cx="8.72307691" cy="1.70725994" rx="0.415384604" ry="0.442622939"></ellipse>
+                        <ellipse id="Oval" fill="#A3A9B8" cx="8.72307691" cy="5.92271662" rx="0.415384604" ry="0.442622939"></ellipse>
+                        <use id="Rectangle" stroke="#A3A9B8" mask="url(#mask-6)" stroke-width="1.4" xlink:href="#path-5"></use>
+                        <ellipse id="Oval" fill="#A3A9B8" cx="8.72307691" cy="10.1381733" rx="0.415384604" ry="0.442622939"></ellipse>
+                        <ellipse id="Oval" fill="#A3A9B8" cx="6.87692307" cy="1.70725994" rx="0.415384604" ry="0.442622939"></ellipse>
+                        <ellipse id="Oval" fill="#A3A9B8" cx="6.87692307" cy="5.92271662" rx="0.415384604" ry="0.442622939"></ellipse>
+                        <ellipse id="Oval" fill="#A3A9B8" cx="6.87692307" cy="10.1381733" rx="0.415384604" ry="0.442622939"></ellipse>
+                    </g>
+                </g>
+            </g>
+        </g>
+    </g>
+</svg>
\ No newline at end of file
diff --git a/static/images/empty/mockhsm.svg b/static/images/empty/mockhsm.svg
new file mode 100644 (file)
index 0000000..969158f
--- /dev/null
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg width="54px" height="69px" viewBox="0 0 54 69" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <!-- Generator: sketchtool 40.2 (33826) - http://www.bohemiancoding.com/sketch -->
+    <title>616B4CA3-2F97-4EB6-BAF7-F57108445F83</title>
+    <desc>Created with sketchtool.</desc>
+    <defs>
+        <rect id="path-1" x="20.5949247" y="7.32712226" width="34.8784722" height="34.8784722" rx="17.4392361"></rect>
+        <mask id="mask-2" maskContentUnits="userSpaceOnUse" maskUnits="objectBoundingBox" x="0" y="0" width="34.8784722" height="34.8784722" fill="white">
+            <use xlink:href="#path-1"></use>
+        </mask>
+    </defs>
+    <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="mock-keys:-empty-state" transform="translate(-801.000000, -249.000000)">
+            <g id="Group-4" transform="translate(799.000000, 242.000000)">
+                <rect id="Rectangle" fill="#A3A9B8" transform="translate(15.170233, 67.954291) rotate(42.000000) translate(-15.170233, -67.954291) " x="5.58523331" y="65.8684677" width="19.1700001" height="4.17164664" rx="2.08582332"></rect>
+                <rect id="Rectangle" fill="#A3A9B8" transform="translate(20.361299, 61.466907) rotate(42.000000) translate(-20.361299, -61.466907) " x="12.4712989" y="59.4198433" width="15.7799997" height="4.09412769" rx="2.04706385"></rect>
+                <path d="M16.7098563,33.1768654 L18.4076844,71.7950862" id="Line" stroke="#A3A9B8" stroke-width="5" stroke-linecap="round" transform="translate(17.558770, 52.485976) rotate(42.000000) translate(-17.558770, -52.485976) "></path>
+                <use id="Rectangle-2" stroke="#A3A9B8" mask="url(#mask-2)" stroke-width="10" transform="translate(38.034161, 24.766358) rotate(42.000000) translate(-38.034161, -24.766358) " xlink:href="#path-1"></use>
+            </g>
+        </g>
+    </g>
+</svg>
\ No newline at end of file
diff --git a/static/images/empty/network_access_token.svg b/static/images/empty/network_access_token.svg
new file mode 100644 (file)
index 0000000..fe899a8
--- /dev/null
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="12px" height="12px" viewBox="0 0 12 12" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <!-- Generator: Sketch 41.2 (35397) - http://www.bohemiancoding.com/sketch -->
+    <title>network</title>
+    <desc>Created with Sketch.</desc>
+    <defs>
+        <path d="M6,0 C2.6916,0 0,2.6916 0,6 C0,9.30828 2.6916,12 6,12 C9.30828,12 12,9.30828 12,6 C12,2.6916 9.30828,0 6,0 L6,0 Z M3.96468,1.13628 C3.69036,1.48356 3.44748,1.89996 3.24468,2.37324 C2.98692,2.28492 2.7324,2.18736 2.4846,2.07456 C2.91924,1.68468 3.41844,1.36572 3.96468,1.13628 L3.96468,1.13628 Z M1.95108,2.6268 C2.28852,2.79216 2.63616,2.9364 2.9916,3.05892 C2.74968,3.82524 2.60088,4.6986 2.57016,5.6364 L0.74112,5.6364 C0.81924,4.49616 1.26048,3.45444 1.95108,2.6268 L1.95108,2.6268 Z M0.74112,6.3636 L2.57028,6.3636 C2.601,7.3014 2.7498,8.17476 2.99172,8.94108 C2.6364,9.0636 2.28864,9.20784 1.9512,9.37332 C1.26048,8.54568 0.81924,7.50384 0.74112,6.3636 L0.74112,6.3636 Z M2.48448,9.92544 C2.73228,9.81264 2.98692,9.71508 3.24456,9.62688 C3.44748,10.10016 3.69024,10.51656 3.96456,10.86384 C3.41844,10.6344 2.91924,10.3152 2.48448,9.92544 L2.48448,9.92544 Z M5.6364,11.22168 C4.974,11.03976 4.37976,10.37124 3.95508,9.41352 C4.50456,9.27744 5.06856,9.19908 5.6364,9.17472 L5.6364,11.22168 L5.6364,11.22168 Z M5.6364,8.44416 C4.98036,8.47032 4.32912,8.56656 3.69612,8.72988 C3.47172,8.02668 3.33012,7.21944 3.29844,6.3636 L5.6364,6.3636 L5.6364,8.44416 L5.6364,8.44416 Z M5.6364,5.6364 L3.29844,5.6364 C3.33012,4.78044 3.47172,3.97332 3.69612,3.27024 C4.32924,3.43344 4.98036,3.52968 5.6364,3.55572 L5.6364,5.6364 L5.6364,5.6364 Z M5.6364,2.82552 C5.06856,2.80104 4.50444,2.72268 3.95496,2.5866 C4.37976,1.629 4.974,0.96036 5.6364,0.77844 L5.6364,2.82552 L5.6364,2.82552 Z M11.25888,5.6364 L9.42984,5.6364 C9.39912,4.6986 9.25032,3.82524 9.0084,3.05892 C9.36372,2.9364 9.71148,2.79216 10.04892,2.6268 C10.73964,3.45444 11.18076,4.49628 11.25888,5.6364 L11.25888,5.6364 Z M9.51564,2.07456 C9.26784,2.18748 9.0132,2.28492 8.75544,2.37324 C8.55264,1.89996 8.30976,1.48356 8.03544,1.13628 C8.58156,1.36572 9.08088,1.68468 9.51564,2.07456 L9.51564,2.07456 Z M6.3636,0.77832 C7.02588,0.96024 7.62012,1.62888 8.04492,2.58648 C7.49556,2.72256 6.93144,2.80092 6.3636,2.8254 L6.3636,0.77832 L6.3636,0.77832 Z M6.3636,3.55572 C7.01964,3.52968 7.67088,3.43332 8.30388,3.27024 C8.52828,3.97332 8.66988,4.78044 8.70156,5.6364 L6.3636,5.6364 L6.3636,3.55572 L6.3636,3.55572 Z M6.3636,6.3636 L8.70156,6.3636 C8.66976,7.21944 8.52828,8.02656 8.30388,8.72988 C7.67076,8.56656 7.01964,8.47032 6.3636,8.44416 L6.3636,6.3636 L6.3636,6.3636 Z M6.3636,11.22168 L6.3636,9.1746 C6.93132,9.19896 7.49544,9.27732 8.04492,9.41352 C7.62012,10.37124 7.02588,11.03976 6.3636,11.22168 L6.3636,11.22168 Z M8.03544,10.86372 C8.30976,10.51644 8.55264,10.10016 8.75544,9.62676 C9.0132,9.7152 9.26784,9.81264 9.51564,9.92556 C9.08088,10.31532 8.58156,10.6344 8.03544,10.86372 L8.03544,10.86372 Z M10.04892,9.37308 C9.71136,9.20772 9.36372,9.0636 9.0084,8.94096 C9.25032,8.17464 9.39912,7.3014 9.42984,6.36348 L11.25888,6.36348 C11.18076,7.50384 10.73964,8.54556 10.04892,9.37308 L10.04892,9.37308 Z" id="path-1"></path>
+    </defs>
+    <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="icon-set" transform="translate(-204.000000, -8.000000)">
+            <g id="icons">
+                <g id="network" transform="translate(196.000000, 0.000000)">
+                    <g id="Group-13" transform="translate(8.000000, 8.000000)">
+                        <g id="Group-32">
+                            <g id="Oval-5">
+                                <mask id="mask-2" fill="white">
+                                    <use xlink:href="#path-1"></use>
+                                </mask>
+                                <use id="Mask" fill="#A3A9B8" xlink:href="#path-1"></use>
+                            </g>
+                        </g>
+                    </g>
+                </g>
+            </g>
+        </g>
+    </g>
+</svg>
\ No newline at end of file
diff --git a/static/images/empty/transaction.svg b/static/images/empty/transaction.svg
new file mode 100644 (file)
index 0000000..f41c804
--- /dev/null
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg width="77px" height="76px" viewBox="0 0 77 76" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <!-- Generator: sketchtool 40.2 (33826) - http://www.bohemiancoding.com/sketch -->
+    <title>76C7D533-A83B-4327-8A43-6BDFAE71A2AA</title>
+    <desc>Created with sketchtool.</desc>
+    <defs></defs>
+    <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">
+        <g id="TX:-empty-state" transform="translate(-793.000000, -243.000000)" stroke-width="5" stroke="#A3A9B8">
+            <g id="Group-4" transform="translate(796.000000, 246.000000)">
+                <g id="Group-8">
+                    <g id="Group-12">
+                        <polyline id="Stroke-220" transform="translate(45.728192, 24.521891) rotate(90.000000) translate(-45.728192, -24.521891) " points="37.4297534 49.5491458 37.256645 -0.505362849 54.1997388 14.8008778"></polyline>
+                        <path d="M69.6902611,-0.0638225939 L54.6161174,15.1555785" id="Stroke-222" transform="translate(62.153189, 7.545878) rotate(90.000000) translate(-62.153189, -7.545878) "></path>
+                        <polyline id="Stroke-220" transform="translate(25.217754, 61.317914) scale(1, -1) rotate(-90.000000) translate(-25.217754, -61.317914) " points="16.9193156 86.3451685 16.7462072 36.2906599 33.689301 51.5969006"></polyline>
+                        <path d="M16.3298285,36.7322001 L1.25568488,51.9516012" id="Stroke-222" transform="translate(8.792757, 44.341901) scale(1, -1) rotate(-90.000000) translate(-8.792757, -44.341901) "></path>
+                    </g>
+                </g>
+            </g>
+        </g>
+    </g>
+</svg>
\ No newline at end of file
diff --git a/static/images/empty/transactionFeed.svg b/static/images/empty/transactionFeed.svg
new file mode 100644 (file)
index 0000000..bb1fd70
--- /dev/null
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg width="96px" height="70px" viewBox="0 0 96 70" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <!-- Generator: sketchtool 40.2 (33826) - http://www.bohemiancoding.com/sketch -->
+    <title>D7A07DBD-1127-48B0-B12A-E3891989A374</title>
+    <desc>Created with sketchtool.</desc>
+    <defs></defs>
+    <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="Feeds:-empty-state" transform="translate(-780.000000, -244.000000)" stroke="#A3A9B8">
+            <g id="Group-21" transform="translate(781.000000, 245.000000)">
+                <g id="Group-19">
+                    <ellipse id="Oval-6" fill="#A3A9B8" cx="3.73333339" cy="3.97814214" rx="3.73333339" ry="3.97814214"></ellipse>
+                    <path d="M24.8888889,3.5 L92.0281844,3.5" id="Line" stroke-width="5" stroke-linecap="round" stroke-linejoin="bevel"></path>
+                </g>
+                <g id="Group-17" transform="translate(0.000000, 20.000000)">
+                    <ellipse id="Oval-6" fill="#A3A9B8" cx="3.73333339" cy="4.20036436" rx="3.73333339" ry="3.97814214"></ellipse>
+                    <path d="M24.8888889,3.5 L92.0281844,3.5" id="Line" stroke-width="5" stroke-linecap="round" stroke-linejoin="bevel"></path>
+                </g>
+                <g id="Group-16" transform="translate(0.000000, 40.000000)">
+                    <ellipse id="Oval-6" fill="#A3A9B8" cx="3.3504274" cy="4.014572" rx="3.3504274" ry="3.57012756"></ellipse>
+                    <path d="M24.8888889,3.5 L92.0281844,3.5" id="Line" stroke-width="5" stroke-linecap="round" stroke-linejoin="bevel"></path>
+                </g>
+                <g id="Group-13" transform="translate(0.000000, 60.000000)">
+                    <ellipse id="Oval-6" fill="#A3A9B8" cx="3.3504274" cy="4.23679422" rx="3.3504274" ry="3.57012756"></ellipse>
+                    <path d="M24.8888889,3.5 L92.0281844,3.5" id="Line" stroke-width="5" stroke-linecap="round" stroke-linejoin="bevel"></path>
+                </g>
+            </g>
+        </g>
+    </g>
+</svg>
\ No newline at end of file
diff --git a/static/images/empty/unspent.svg b/static/images/empty/unspent.svg
new file mode 100644 (file)
index 0000000..6e6c60e
--- /dev/null
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg width="66px" height="70px" viewBox="0 0 66 70" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <!-- Generator: sketchtool 40.2 (33826) - http://www.bohemiancoding.com/sketch -->
+    <title>D8315B36-6F39-44FD-A5AF-2904533DC5C3</title>
+    <desc>Created with sketchtool.</desc>
+    <defs></defs>
+    <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="unspent-outputs:-empty-state" transform="translate(-800.000000, -246.000000)">
+            <g id="Group-3" transform="translate(800.000000, 246.000000)">
+                <path d="M57.0795476,16.1449029 C56.8638017,12.2243038 46.1102509,9.19036774 33.0477414,9.36841704 C19.970786,9.54646633 9.06720532,12.8690773 8.70699059,16.7896764 C8.34677586,20.7102755 19.1332414,23.7442116 32.7861575,23.5661623 C46.4246276,23.388113 57.2952935,20.065502 57.0795476,16.1449029 Z" id="Oval-4" stroke="#A3A9B8" stroke-width="3" stroke-dasharray="6,5,6,7"></path>
+                <path d="M32.6517979,0.253952857 C14.3465996,0.253952857 0,7.44989902 0,16.6197332 L0,53.4739713 C0,62.6687914 14.3465996,69.8397517 32.6517979,69.8397517 C50.9569962,69.8397517 65.3035958,62.6687914 65.3035958,53.4989573 L65.3035958,16.6447191 C65.3035958,7.44989902 50.9569962,0.253952857 32.6517979,0.253952857 L32.6517979,0.253952857 Z M60.9464804,53.4989573 C60.9464804,59.4206213 49.5754718,65.792032 32.6517979,65.792032 C15.728124,65.792032 4.35711542,59.4456072 4.35711542,53.4989573 L4.35711542,49.4762235 C9.93634859,54.3734646 20.4040527,57.5966488 32.6517979,57.5966488 C44.8995431,57.5966488 55.3672472,54.3734646 60.9464804,49.4762235 L60.9464804,53.4989573 L60.9464804,53.4989573 Z M60.9464804,41.2058826 C60.9464804,47.1275466 49.5754718,53.4989573 32.6517979,53.4989573 C15.728124,53.4989573 4.35711542,47.1525325 4.35711542,41.2058826 L4.35711542,37.1831488 C9.93634859,42.0803899 20.4040527,45.3035741 32.6517979,45.3035741 C44.8995431,45.3035741 55.3672472,42.0803899 60.9464804,37.1831488 L60.9464804,41.2058826 L60.9464804,41.2058826 Z M60.9464804,28.9128079 C60.9464804,34.8344719 49.5754718,41.2058826 32.6517979,41.2058826 C15.728124,41.2058826 4.35711542,34.8594578 4.35711542,28.9128079 L4.35711542,24.8900741 C9.93634859,29.7873152 20.4040527,33.0104994 32.6517979,33.0104994 C44.8995431,33.0104994 55.3672472,29.7873152 60.9464804,24.8900741 L60.9464804,28.9128079 L60.9464804,28.9128079 Z M32.6517979,28.9128079 C15.728124,28.9128079 4.35711542,22.5663831 4.35711542,16.6197332 C4.35711542,10.6980692 15.728124,4.3266585 32.6517979,4.3266585 C49.5754718,4.3266585 60.9464804,10.6730832 60.9464804,16.6197332 C60.9464804,22.5663831 49.5754718,28.9128079 32.6517979,28.9128079 L32.6517979,28.9128079 Z" id="Shape" fill="#A3A9B8"></path>
+            </g>
+        </g>
+    </g>
+</svg>
\ No newline at end of file
diff --git a/static/images/favicon.png b/static/images/favicon.png
new file mode 100644 (file)
index 0000000..3339a40
Binary files /dev/null and b/static/images/favicon.png differ
diff --git a/static/images/logo-bytom-white.png b/static/images/logo-bytom-white.png
new file mode 100644 (file)
index 0000000..4327e44
Binary files /dev/null and b/static/images/logo-bytom-white.png differ
diff --git a/static/images/logo-shadowed.png b/static/images/logo-shadowed.png
new file mode 100644 (file)
index 0000000..e801c49
Binary files /dev/null and b/static/images/logo-shadowed.png differ
diff --git a/static/images/logo-white.png b/static/images/logo-white.png
new file mode 100644 (file)
index 0000000..cb5688e
Binary files /dev/null and b/static/images/logo-white.png differ
diff --git a/static/images/navigation/account-active.png b/static/images/navigation/account-active.png
new file mode 100644 (file)
index 0000000..fbcb834
Binary files /dev/null and b/static/images/navigation/account-active.png differ
diff --git a/static/images/navigation/account.png b/static/images/navigation/account.png
new file mode 100644 (file)
index 0000000..1ced99b
Binary files /dev/null and b/static/images/navigation/account.png differ
diff --git a/static/images/navigation/asset-active.png b/static/images/navigation/asset-active.png
new file mode 100644 (file)
index 0000000..eda9701
Binary files /dev/null and b/static/images/navigation/asset-active.png differ
diff --git a/static/images/navigation/asset.png b/static/images/navigation/asset.png
new file mode 100644 (file)
index 0000000..9ea5ead
Binary files /dev/null and b/static/images/navigation/asset.png differ
diff --git a/static/images/navigation/balance-active.png b/static/images/navigation/balance-active.png
new file mode 100644 (file)
index 0000000..774c530
Binary files /dev/null and b/static/images/navigation/balance-active.png differ
diff --git a/static/images/navigation/balance.png b/static/images/navigation/balance.png
new file mode 100644 (file)
index 0000000..3467e9a
Binary files /dev/null and b/static/images/navigation/balance.png differ
diff --git a/static/images/navigation/client-active.png b/static/images/navigation/client-active.png
new file mode 100644 (file)
index 0000000..0f8f1e6
Binary files /dev/null and b/static/images/navigation/client-active.png differ
diff --git a/static/images/navigation/client.png b/static/images/navigation/client.png
new file mode 100644 (file)
index 0000000..44ce692
Binary files /dev/null and b/static/images/navigation/client.png differ
diff --git a/static/images/navigation/core-active.png b/static/images/navigation/core-active.png
new file mode 100644 (file)
index 0000000..77f9da5
Binary files /dev/null and b/static/images/navigation/core-active.png differ
diff --git a/static/images/navigation/core.png b/static/images/navigation/core.png
new file mode 100644 (file)
index 0000000..efecedc
Binary files /dev/null and b/static/images/navigation/core.png differ
diff --git a/static/images/navigation/docs.png b/static/images/navigation/docs.png
new file mode 100644 (file)
index 0000000..2591ff1
Binary files /dev/null and b/static/images/navigation/docs.png differ
diff --git a/static/images/navigation/error.png b/static/images/navigation/error.png
new file mode 100644 (file)
index 0000000..47d4fc8
Binary files /dev/null and b/static/images/navigation/error.png differ
diff --git a/static/images/navigation/feed-active.png b/static/images/navigation/feed-active.png
new file mode 100644 (file)
index 0000000..d34cb4c
Binary files /dev/null and b/static/images/navigation/feed-active.png differ
diff --git a/static/images/navigation/feed.png b/static/images/navigation/feed.png
new file mode 100644 (file)
index 0000000..bddc49a
Binary files /dev/null and b/static/images/navigation/feed.png differ
diff --git a/static/images/navigation/help.png b/static/images/navigation/help.png
new file mode 100644 (file)
index 0000000..558fedc
Binary files /dev/null and b/static/images/navigation/help.png differ
diff --git a/static/images/navigation/logout.png b/static/images/navigation/logout.png
new file mode 100644 (file)
index 0000000..390907f
Binary files /dev/null and b/static/images/navigation/logout.png differ
diff --git a/static/images/navigation/mockhsm-active.png b/static/images/navigation/mockhsm-active.png
new file mode 100644 (file)
index 0000000..eda3b6e
Binary files /dev/null and b/static/images/navigation/mockhsm-active.png differ
diff --git a/static/images/navigation/mockhsm.png b/static/images/navigation/mockhsm.png
new file mode 100644 (file)
index 0000000..1c93239
Binary files /dev/null and b/static/images/navigation/mockhsm.png differ
diff --git a/static/images/navigation/network-active.png b/static/images/navigation/network-active.png
new file mode 100644 (file)
index 0000000..f3a0015
Binary files /dev/null and b/static/images/navigation/network-active.png differ
diff --git a/static/images/navigation/network.png b/static/images/navigation/network.png
new file mode 100644 (file)
index 0000000..0d4adab
Binary files /dev/null and b/static/images/navigation/network.png differ
diff --git a/static/images/navigation/settings.png b/static/images/navigation/settings.png
new file mode 100644 (file)
index 0000000..fe6b075
Binary files /dev/null and b/static/images/navigation/settings.png differ
diff --git a/static/images/navigation/transaction-active.png b/static/images/navigation/transaction-active.png
new file mode 100644 (file)
index 0000000..732b0ab
Binary files /dev/null and b/static/images/navigation/transaction-active.png differ
diff --git a/static/images/navigation/transaction.png b/static/images/navigation/transaction.png
new file mode 100644 (file)
index 0000000..9032cf8
Binary files /dev/null and b/static/images/navigation/transaction.png differ
diff --git a/static/images/navigation/tutorial-active.png b/static/images/navigation/tutorial-active.png
new file mode 100644 (file)
index 0000000..f0434e6
Binary files /dev/null and b/static/images/navigation/tutorial-active.png differ
diff --git a/static/images/navigation/tutorial.png b/static/images/navigation/tutorial.png
new file mode 100644 (file)
index 0000000..160e91b
Binary files /dev/null and b/static/images/navigation/tutorial.png differ
diff --git a/static/images/navigation/unspent-active.png b/static/images/navigation/unspent-active.png
new file mode 100644 (file)
index 0000000..736a90c
Binary files /dev/null and b/static/images/navigation/unspent-active.png differ
diff --git a/static/images/navigation/unspent.png b/static/images/navigation/unspent.png
new file mode 100644 (file)
index 0000000..99df1f7
Binary files /dev/null and b/static/images/navigation/unspent.png differ
diff --git a/static/images/search.png b/static/images/search.png
new file mode 100644 (file)
index 0000000..7d91cd0
Binary files /dev/null and b/static/images/search.png differ
diff --git a/static/images/sum.png b/static/images/sum.png
new file mode 100644 (file)
index 0000000..529dc47
Binary files /dev/null and b/static/images/sum.png differ
diff --git a/static/styles/_body.scss b/static/styles/_body.scss
new file mode 100644 (file)
index 0000000..4260b7a
--- /dev/null
@@ -0,0 +1,147 @@
+@font-face {
+  font-family: "Nitti";
+  src: url('fonts/nitti-normal.woff');
+}
+
+@font-face {
+  font-family: "Nitti Grotesk";
+  src: url('fonts/nittigrotesk-normal.woff');
+}
+
+@font-face {
+  font-family: "Nitti Grotesk";
+  font-weight: bold;
+  src: url('fonts/nittigrotesk-medium.woff');
+}
+
+html {
+  height: 100%;
+  box-sizing: border-box;
+}
+
+*,
+*:before,
+*:after {
+  box-sizing: inherit;
+}
+
+body {
+  display: flex;
+  flex-direction: column;
+  position: relative;
+  margin: 0 auto;
+  min-height: 100%;
+  min-width: $minWidth;
+  color: $text-color;
+}
+
+#root {
+  display: flex;
+  flex-direction: column;
+  flex: 1;
+
+  > div {
+    display: flex;
+    flex-direction: column;
+    flex: 1;
+  }
+}
+
+:focus, .focus {
+  outline: none !important;
+}
+
+.jumbotron p {
+  margin-bottom: 0;
+  color: $gray-light;
+}
+
+p {
+  line-height: 1.5;
+  margin-bottom: $grid-gutter-width/2;
+}
+
+.flex-container {
+  display: flex;
+  flex-direction: column;
+  flex: 1;
+}
+
+.btn-primary,
+.btn-default {
+  height: 36px;
+  padding: 0 20px;
+  border-radius: $border-radius-standard;
+}
+
+.btn-primary {
+  border-color: $highlight-default;
+  color: $text-inverse-color;
+  background-color: $highlight-default;
+}
+
+.btn-primary:hover,
+.btn-primary:active,
+.btn-primary:focus,
+.btn-primary:active:hover,
+.btn-primary:focus,
+.btn-primary:active:focus,
+.btn-primary:focus, .btn-primary.focus
+.btn-primary:active.focus, .btn-primary.active:hover,
+.btn-primary.active:focus, .btn-primary.active.focus {
+  text-decoration: none;
+  color: $text-inverse-color;
+  background: $highlight-secondary;
+  border-color: $highlight-secondary;
+}
+
+.btn-default {
+  background-color: $background-color;
+  border-color: $border-strong-color;
+}
+.btn-default:active:hover,
+.btn-default:hover,
+.btn-default:focus,
+.btn-default:active:focus,
+.btn-default:focus, .btn-default.focus
+.btn-default:active.focus, .btn-default.active:hover,
+.btn-default.active:focus, .btn-default.active.focus,
+.open > .btn-default.dropdown-toggle:hover,
+.open > .btn-default.dropdown-toggle:focus, .open > .btn-default.dropdown-toggle.focus {
+  color: $text-strong-color;
+  background-color: $background-content-color;
+  border-color: $border-strong-color;
+}
+
+a:hover,
+a:focus {
+  color: $highlight-secondary;
+  text-decoration: none;
+}
+
+.form-control {
+  height: 36px;
+  &:focus {
+    box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 2px rgba(0, 217, 194, 0.6);
+  }
+}
+.help-block {
+  color: $text-light-color;
+}
+.jumbotron {
+  background: transparent;
+}
+.btn-lg {
+  height: 44px;
+  font-size: $font-size-btn-lg;
+}
+.label-primary {
+  color: $text-color;
+  background: $background-emphasis-color;
+  border: 1px solid transparentize($border-color, 0.5);
+}
+.close {
+  text-shadow: none;
+  font-size: $font-size-close;
+  font-weight: normal;
+}
diff --git a/static/styles/_bootstrap-overrides.scss b/static/styles/_bootstrap-overrides.scss
new file mode 100644 (file)
index 0000000..301d103
--- /dev/null
@@ -0,0 +1,52 @@
+$grid-gutter-width: 30px;
+
+$font-size-caps: 13px;
+$font-size-code: $font-size-caps;
+$font-size-base: 15px;
+$font-size-nav: 16px;
+$font-size-form-section-title: $font-size-nav;
+$font-size-section-title: 18px;
+$font-size-btn-lg: $font-size-section-title;
+$font-size-page-title: 22px;
+$font-size-close: 26px;
+$font-size-h2: 28px;
+$line-height-base: 1.7;
+$font-family-sans-serif: "Nitti Grotesk", "Helvetica Neue", Helvetica, Arial, sans-serif;
+$font-family-monospace: "Nitti", Menlo, Monaco, Consolas, "Courier New", monospace;
+
+// Disable responsive styles below 900px wide
+$screen-xs: 1px;
+$screen-sm: 2px;
+$container-sm: 900px;
+$container-desktop: 1040px + $grid-gutter-width;
+$screen-md: $container-desktop;
+
+$brand-primary: #00bfaa;
+$brand-danger: #EF5354;
+
+$text-color: #8c8e94;
+$headings-color: #222834;
+
+$navbar-default-bg: #20252D;
+
+$link-color: $brand-primary;
+$navbar-default-link-color: transparentize(white, 0.1);
+$navbar-default-link-active-color: white;
+$navbar-default-link-hover-color: white;
+
+$input-border-focus: $brand-primary;
+
+$page-header-border-color: #e3e3e3;
+$pre-bg: #fff;
+$panel-footer-bg: #fff;
+$pre-border-color: transparent;
+
+$code-color: #747c89;
+$code-bg: none;
+
+$border-radius-base: 3px;
+$border-radius-large: 3px;
+
+$form-group-margin-bottom: 30px;
+
+$input-border-focus: #00D9C2;
diff --git a/static/styles/app.scss b/static/styles/app.scss
new file mode 100644 (file)
index 0000000..f13f85c
--- /dev/null
@@ -0,0 +1,2 @@
+@import 'resources';
+@import 'body';
diff --git a/static/styles/resources.scss b/static/styles/resources.scss
new file mode 100644 (file)
index 0000000..0d2eb71
--- /dev/null
@@ -0,0 +1,65 @@
+// Contents of this file are included in every
+// CSS module.
+//
+// Do NOT put styles in here that generate output. Only include
+// variables, mixins and other functions describing common
+// functionality.
+
+@import 'static/styles/bootstrap-overrides';
+@import 'node_modules/bootstrap-sass/assets/stylesheets/bootstrap/_variables';
+@import 'node_modules/bootstrap-sass/assets/stylesheets/bootstrap/_mixins';
+
+$background-color: #ffffff;
+$background-content-color: #f2f2f2;
+$background-emphasis-color: #fafafa;
+
+$border-color: #dadee2;
+$border-strong-color: #cccccc;
+$border-light-color: $background-content-color;
+$text-color: #747c89;
+$text-light-color: #bac0c7;
+$text-inverse-color: #ffffff;
+$text-strong-color: #20252d;
+$text-table-value-color: #747c89;
+$text-table-label-color: #9ea8b6;
+$text-danger: #d44c4c;
+
+$highlight-default: #00bfaa;
+$highlight-secondary: #00D9C2;
+$highlight-danger: #be4343;
+$highlight-danger-background: #f8cece;
+$highlight-danger-border: #f3aaaa;
+$highlight-tutorial: #69A7E4;
+
+$success-background: #dff0d3;
+$success-border: #cbe6b7;
+$success: #227855;
+$background-inverse-color: #20252D;
+$border-inverse-color: #303638;
+$highlight-inverse-color: #d4d7de;
+
+$border-radius-standard: 3px;
+
+$title-height: 60px;
+$gutter-size: 30px;
+
+$minWidth: 900px;
+$maxWidth: 1240px;
+$sidebar-width: 220px;
+
+$box-shadow: 0 3px 6px -2px rgba(0,0,0,0.15);
+
+@mixin keyframes($name) {
+  @-webkit-keyframes #{$name} {
+    @content;
+  }
+  @-moz-keyframes #{$name} {
+    @content;
+  }
+  @-ms-keyframes #{$name} {
+    @content;
+  }
+  @keyframes #{$name} {
+    @content;
+  }
+}
diff --git a/test/.eslintrc.json b/test/.eslintrc.json
new file mode 100644 (file)
index 0000000..636e15f
--- /dev/null
@@ -0,0 +1,10 @@
+{
+  "env": {
+    "mocha": true
+  },
+  "globals": {
+    "browser": true,
+    "expect": true,
+    "testHelpers": true
+  }
+}
diff --git a/test/conf/wdio.browserstack.js b/test/conf/wdio.browserstack.js
new file mode 100644 (file)
index 0000000..b53061b
--- /dev/null
@@ -0,0 +1,214 @@
+/* eslint-env node */
+
+exports.config = {
+
+  //
+  // ==================
+  // Specify Test Files
+  // ==================
+  // Define which test specs should run. The pattern is relative to the directory
+  // from which `wdio` was called. Notice that, if you are calling `wdio` from an
+  // NPM script (see https://docs.npmjs.com/cli/run-script) then the current working
+  // directory is where your package.json resides, so `wdio` will be called from there.
+  //
+  specs: [
+    './test/specs/**/*.js'
+  ],
+  // Patterns to exclude.
+  exclude: [
+      // 'path/to/excluded/files'
+  ],
+  //
+  // ============
+  // Capabilities
+  // ============
+  // Define your capabilities here. WebdriverIO can run multiple capabilities at the same
+  // time. Depending on the number of capabilities, WebdriverIO launches several test
+  // sessions. Within your capabilities you can overwrite the spec and exclude options in
+  // order to group specific specs to a specific capability.
+  //
+  // First, you can define how many instances should be started at the same time. Let's
+  // say you have 3 different capabilities (Chrome, Firefox, and Safari) and you have
+  // set maxInstances to 1; wdio will spawn 3 processes. Therefore, if you have 10 spec
+  // files and you set maxInstances to 10, all spec files will get tested at the same time
+  // and 30 processes will get spawned. The property handles how many capabilities
+  // from the same test should run tests.
+  //
+  maxInstances: 10,
+  //
+  // Browser Stack configuration
+  user: process.env.BROWSER_STACK_USER,
+  key: process.env.BROWSER_STACK_KEY,
+  browserstackLocal: true,
+  //
+  // If you have trouble getting all important capabilities together, check out the
+  // Sauce Labs platform configurator - a great tool to configure your capabilities:
+  // https://docs.saucelabs.com/reference/platforms-configurator
+  //
+  capabilities: [{
+    // maxInstances can get overwritten per capability. So if you have an in-house Selenium
+    // grid with only 5 firefox instances available you can make sure that not more than
+    // 5 instances get started at a time.
+    //
+    browser: 'chrome',
+    'browserstack.local': true,
+  }],
+  //
+  // ===================
+  // Test Configurations
+  // ===================
+  // Define all options that are relevant for the WebdriverIO instance here
+  //
+  // By default WebdriverIO commands are executed in a synchronous way using
+  // the wdio-sync package. If you still want to run your tests in an async way
+  // e.g. using promises you can set the sync option to false.
+  sync: true,
+  //
+  // Level of logging verbosity: silent | verbose | command | data | result | error
+  logLevel: 'error',
+  //
+  // Enables colors for log output.
+  coloredLogs: true,
+  //
+  // If you only want to run your tests until a specific amount of tests have failed use
+  // bail (default is 0 - don't bail, run all tests).
+  bail: 0,
+  //
+  // Saves a screenshot to a given path if a command fails.
+  screenshotPath: './errorShots/',
+  //
+  // Set a base URL in order to shorten url command calls. If your url parameter starts
+  // with "/", then the base url gets prepended.
+  baseUrl: 'http://localhost:3000',
+  //
+  // Default timeout for all waitFor* commands.
+  waitforTimeout: 20000,
+  //
+  // Default timeout in milliseconds for request
+  // if Selenium Grid doesn't send response
+  connectionRetryTimeout: 90000,
+  //
+  // Default request retries count
+  connectionRetryCount: 3,
+  //
+  // Initialize the browser instance with a WebdriverIO plugin. The object should have the
+  // plugin name as key and the desired plugin options as properties. Make sure you have
+  // the plugin installed before running any tests. The following plugins are currently
+  // available:
+  // WebdriverCSS: https://github.com/webdriverio/webdrivercss
+  // WebdriverRTC: https://github.com/webdriverio/webdriverrtc
+  // Browserevent: https://github.com/webdriverio/browserevent
+  // plugins: {
+  //     webdrivercss: {
+  //         screenshotRoot: 'my-shots',
+  //         failedComparisonsRoot: 'diffs',
+  //         misMatchTolerance: 0.05,
+  //         screenWidth: [320,480,640,1024]
+  //     },
+  //     webdriverrtc: {},
+  //     browserevent: {}
+  // },
+  //
+  // Test runner services
+  // Services take over a specific job you don't want to take care of. They enhance
+  // your test setup with almost no effort. Unlike plugins, they don't add new
+  // commands. Instead, they hook themselves up into the test process.
+  services: ['browserstack'],
+  //
+  // Framework you want to run your specs with.
+  // The following are supported: Mocha, Jasmine, and Cucumber
+  // see also: http://webdriver.io/guide/testrunner/frameworks.html
+  //
+  // Make sure you have the wdio adapter package for the specific framework installed
+  // before running any tests.
+  framework: 'mocha',
+  //
+  // Test reporter for stdout.
+  // The only one supported by default is 'dot'
+  // see also: http://webdriver.io/guide/testrunner/reporters.html
+  reporters: ['spec'],
+
+  //
+  // Options to be passed to Mocha.
+  // See the full list at http://mochajs.org/
+  mochaOpts: {
+    ui: 'bdd',
+    timeout: 30000
+  },
+  //
+  // =====
+  // Hooks
+  // =====
+  // WebdriverIO provides several hooks you can use to interfere with the test process in order to enhance
+  // it and to build services around it. You can either apply a single function or an array of
+  // methods to it. If one of them returns with a promise, WebdriverIO will wait until that promise got
+  // resolved to continue.
+  //
+  // Gets executed once before all workers get launched.
+  // onPrepare: function (config, capabilities) {
+  // },
+  //
+  // Gets executed just before initialising the webdriver session and test framework. It allows you
+  // to manipulate configurations depending on the capability or spec.
+  // beforeSession: function (config, capabilities, specs) {
+  // },
+  //
+  // Gets executed before test execution begins. At this point you can access all global
+  // variables, such as `browser`. It is the perfect place to define custom commands.
+  before: function () {
+    const chai = require('chai')
+    const chaiAsPromised = require('chai-as-promised')
+    chai.use(chaiAsPromised)
+
+    global.expect = chai.expect
+    chai.Should()
+  },
+  //
+  // Hook that gets executed before the suite starts
+  // beforeSuite: function (suite) {
+  // },
+  //
+  // Hook that gets executed _before_ a hook within the suite starts (e.g. runs before calling
+  // beforeEach in Mocha)
+  // beforeHook: function () {
+  // },
+  //
+  // Hook that gets executed _after_ a hook within the suite starts (e.g. runs after calling
+  // afterEach in Mocha)
+  // afterHook: function () {
+  // },
+  //
+  // Function to be executed before a test (in Mocha/Jasmine) or a step (in Cucumber) starts.
+  // beforeTest: function (test) {
+  // },
+  //
+  // Runs before a WebdriverIO command gets executed.
+  // beforeCommand: function (commandName, args) {
+  // },
+  //
+  // Runs after a WebdriverIO command gets executed
+  // afterCommand: function (commandName, args, result, error) {
+  // },
+  //
+  // Function to be executed after a test (in Mocha/Jasmine) or a step (in Cucumber) starts.
+  // afterTest: function (test) {
+  // },
+  //
+  // Hook that gets executed after the suite has ended
+  // afterSuite: function (suite) {
+  // },
+  //
+  // Gets executed after all tests are done. You still have access to all global variables from
+  // the test.
+  // after: function (result, capabilities, specs) {
+  // },
+  //
+  // Gets executed right after terminating the webdriver session.
+  // afterSession: function (config, capabilities, specs) {
+  // },
+  //
+  // Gets executed after all workers got shut down and the process is about to exit. It is not
+  // possible to defer the end of the process using a promise.
+  // onComplete: function(exitCode) {
+  // }
+}
diff --git a/test/conf/wdio.local.js b/test/conf/wdio.local.js
new file mode 100644 (file)
index 0000000..c004d9e
--- /dev/null
@@ -0,0 +1,219 @@
+const extended = process.env.EXTENDED
+
+exports.config = {
+  //
+  // ==================
+  // Specify Test Files
+  // ==================
+  // Define which test specs should run. The pattern is relative to the directory
+  // from which `wdio` was called. Notice that, if you are calling `wdio` from an
+  // NPM script (see https://docs.npmjs.com/cli/run-script) then the current working
+  // directory is where your package.json resides, so `wdio` will be called from there.
+  //
+  specs: [
+    './test/specs/**/*.js',
+  ],
+  // Patterns to exclude.
+  exclude: [
+      // 'path/to/excluded/files'
+  ],
+  //
+  // ============
+  // Suites
+  // ============
+  // Define specific test suites
+  suites: {
+    base: [
+      './test/specs/*.js',
+    ],
+  },
+  //
+  // ============
+  // Capabilities
+  // ============
+  // Define your capabilities here. WebdriverIO can run multiple capabilities at the same
+  // time. Depending on the number of capabilities, WebdriverIO launches several test
+  // sessions. Within your capabilities you can overwrite the spec and exclude options in
+  // order to group specific specs to a specific capability.
+  //
+  // First, you can define how many instances should be started at the same time. Let's
+  // say you have 3 different capabilities (Chrome, Firefox, and Safari) and you have
+  // set maxInstances to 1; wdio will spawn 3 processes. Therefore, if you have 10 spec
+  // files and you set maxInstances to 10, all spec files will get tested at the same time
+  // and 30 processes will get spawned. The property handles how many capabilities
+  // from the same test should run tests.
+  //
+  maxInstances: extended ? 1 : 10,
+  //
+  // If you have trouble getting all important capabilities together, check out the
+  // Sauce Labs platform configurator - a great tool to configure your capabilities:
+  // https://docs.saucelabs.com/reference/platforms-configurator
+  //
+  capabilities: [{
+    // maxInstances can get overwritten per capability. So if you have an in-house Selenium
+    // grid with only 5 firefox instances available you can make sure that not more than
+    // 5 instances get started at a time.
+    //
+    browserName: 'chrome',
+  }],
+  //
+  // ===================
+  // Test Configurations
+  // ===================
+  // Define all options that are relevant for the WebdriverIO instance here
+  //
+  // By default WebdriverIO commands are executed in a synchronous way using
+  // the wdio-sync package. If you still want to run your tests in an async way
+  // e.g. using promises you can set the sync option to false.
+  sync: true,
+  //
+  // Level of logging verbosity: silent | verbose | command | data | result | error
+  logLevel: 'error',
+  //
+  // Enables colors for log output.
+  coloredLogs: true,
+  //
+  // If you only want to run your tests until a specific amount of tests have failed use
+  // bail (default is 0 - don't bail, run all tests).
+  bail: 5,
+  //
+  // Saves a screenshot to a given path if a command fails.
+  screenshotPath: './errorShots/',
+  //
+  // Set a base URL in order to shorten url command calls. If your url parameter starts
+  // with "/", then the base url gets prepended.
+  baseUrl: 'http://localhost:3000',
+  //
+  // Default timeout for all waitFor* commands.
+  waitforTimeout: 20000,
+  //
+  // Default timeout in milliseconds for request
+  // if Selenium Grid doesn't send response
+  connectionRetryTimeout: 90000,
+  //
+  // Default request retries count
+  connectionRetryCount: 3,
+  //
+  // Initialize the browser instance with a WebdriverIO plugin. The object should have the
+  // plugin name as key and the desired plugin options as properties. Make sure you have
+  // the plugin installed before running any tests. The following plugins are currently
+  // available:
+  // WebdriverCSS: https://github.com/webdriverio/webdrivercss
+  // WebdriverRTC: https://github.com/webdriverio/webdriverrtc
+  // Browserevent: https://github.com/webdriverio/browserevent
+  // plugins: {
+  //     webdrivercss: {
+  //         screenshotRoot: 'my-shots',
+  //         failedComparisonsRoot: 'diffs',
+  //         misMatchTolerance: 0.05,
+  //         screenWidth: [320,480,640,1024]
+  //     },
+  //     webdriverrtc: {},
+  //     browserevent: {}
+  // },
+  //
+  // Test runner services
+  // Services take over a specific job you don't want to take care of. They enhance
+  // your test setup with almost no effort. Unlike plugins, they don't add new
+  // commands. Instead, they hook themselves up into the test process.
+  services: ['selenium-standalone'],
+  //
+  // Framework you want to run your specs with.
+  // The following are supported: Mocha, Jasmine, and Cucumber
+  // see also: http://webdriver.io/guide/testrunner/frameworks.html
+  //
+  // Make sure you have the wdio adapter package for the specific framework installed
+  // before running any tests.
+  framework: 'mocha',
+  //
+  // Test reporter for stdout.
+  // The only one supported by default is 'dot'
+  // see also: http://webdriver.io/guide/testrunner/reporters.html
+  reporters: ['spec'],
+
+  //
+  // Options to be passed to Mocha.
+  // See the full list at http://mochajs.org/
+  mochaOpts: {
+    ui: 'bdd',
+    compilers: ['js:babel-register'],
+    require: ['./test/helpers.js'],
+    timeout: 30000,
+  },
+  //
+  // =====
+  // Hooks
+  // =====
+  // WebdriverIO provides several hooks you can use to interfere with the test process in order to enhance
+  // it and to build services around it. You can either apply a single function or an array of
+  // methods to it. If one of them returns with a promise, WebdriverIO will wait until that promise got
+  // resolved to continue.
+  //
+  // Gets executed once before all workers get launched.
+  // onPrepare: function (config, capabilities) {
+  // },
+  //
+  // Gets executed just before initialising the webdriver session and test framework. It allows you
+  // to manipulate configurations depending on the capability or spec.
+  // beforeSession: function (config, capabilities, specs) {
+  // },
+  //
+  // Gets executed before test execution begins. At this point you can access all global
+  // variables, such as `browser`. It is the perfect place to define custom commands.
+  before: function () {
+    const chai = require('chai')
+    const chaiAsPromised = require('chai-as-promised')
+    chai.use(chaiAsPromised)
+
+    global.expect = chai.expect
+    chai.Should()
+  },
+  //
+  // Hook that gets executed before the suite starts
+  // beforeSuite: function (suite) {
+  // },
+  //
+  // Hook that gets executed _before_ a hook within the suite starts (e.g. runs before calling
+  // beforeEach in Mocha)
+  // beforeHook: function () {
+  // },
+  //
+  // Hook that gets executed _after_ a hook within the suite starts (e.g. runs after calling
+  // afterEach in Mocha)
+  // afterHook: function () {
+  // },
+  //
+  // Function to be executed before a test (in Mocha/Jasmine) or a step (in Cucumber) starts.
+  // beforeTest: function (test) {
+  // },
+  //
+  // Runs before a WebdriverIO command gets executed.
+  // beforeCommand: function (commandName, args) {
+  // },
+  //
+  // Runs after a WebdriverIO command gets executed
+  // afterCommand: function (commandName, args, result, error) {
+  // },
+  //
+  // Function to be executed after a test (in Mocha/Jasmine) or a step (in Cucumber) starts.
+  // afterTest: function (test) {
+  // },
+  //
+  // Hook that gets executed after the suite has ended
+  // afterSuite: function (suite) {
+  // },
+  //
+  // Gets executed after all tests are done. You still have access to all global variables from
+  // the test.
+  // after: function (result, capabilities, specs) {
+  // },
+  //
+  // Gets executed right after terminating the webdriver session.
+  // afterSession: function (config, capabilities, specs) {
+  // },
+  //
+  // Gets executed after all workers got shut down and the process is about to exit. It is not
+  // possible to defer the end of the process using a promise.
+  // onComplete: function(exitCode) {
+  // }
+}
diff --git a/test/helpers.js b/test/helpers.js
new file mode 100644 (file)
index 0000000..7c1afd4
--- /dev/null
@@ -0,0 +1,84 @@
+const chain = require('chain-sdk')
+const client = new chain.Client()
+
+const sleep = (ms) => new Promise(resolve => {
+  setTimeout(() => resolve(), ms)
+})
+
+const resetCore = () => expect(
+  client.config.reset()
+    .then(() => ensureConfigured())
+).to.be.fulfilled
+
+const ensureConfigured = () => {
+  const doConfig = () => client.config.info()
+    .then((info) => {
+      if (info.isConfigured) {
+        return
+      } else {
+        return client.config.configure({ isGenerator: true })
+          .then(() => sleep(1000))
+      }
+    })
+    .catch(() => sleep(100).then(() => doConfig()))
+
+  return expect(doConfig()).to.be.fulfilled
+}
+
+const setUpObjects = (signer) => {
+  let keyResults, assetResults, accountResults
+  let key
+
+  return expect(Promise.all([
+    client.mockHsm.keys.query({aliases: ['testkey']}),
+    client.assets.query({filter: "alias='gold'"}),
+    client.accounts.query({filter: "alias='alice'"}),
+  ])).to.be.fulfilled
+  .then((results) => {
+    keyResults = results[0]
+    assetResults = results[1]
+    accountResults = results[2]
+
+    let keyPromise = Promise.resolve()
+
+    key = keyResults.items[0]
+    if (!key) {
+      keyPromise = keyPromise.then(() => expect(client.mockHsm.keys.create({alias: 'testkey'})
+      .then((keyResp) => {
+        key = keyResp
+      }, (err) => {
+        if (err.code == 'CH050') {
+          return client.mockHsm.keys.query({aliases: ['testkey']}).then(resp => resp.items[0])
+        }
+        throw err
+      })).to.be.fulfilled)
+    }
+
+    return keyPromise
+  }).then(() => signer.addKey(key, client.mockHsm.signerConnection))
+  .then(() => {
+    const createPromises = []
+
+    if (!assetResults.items[0]) createPromises.push(client.assets.create({alias: 'gold', rootXpubs: [key.xpub], quorum: 1}))
+    if (!accountResults.items[0]) createPromises.push(client.accounts.create({alias: 'alice', rootXpubs: [key.xpub], quorum: 1}))
+
+    return expect(Promise.all(createPromises)).to.be.fulfilled
+  })
+}
+
+const issueTransaction = (signer) => expect(
+  client.transactions.build((builder) => {
+    builder.issue({ assetAlias: 'gold', amount: 100 })
+    builder.controlWithAccount({ accountAlias: 'alice', assetAlias: 'gold', amount: 100 })
+  })
+  .then(tpl => signer.sign(tpl))
+  .then(tpl => client.transactions.submit(tpl))
+).to.be.fulfilled
+
+global.testHelpers = {
+  sleep,
+  resetCore,
+  ensureConfigured,
+  setUpObjects,
+  issueTransaction,
+}
diff --git a/test/specs/accounts.js b/test/specs/accounts.js
new file mode 100644 (file)
index 0000000..ac65e8a
--- /dev/null
@@ -0,0 +1,47 @@
+const chain = require('chain-sdk')
+const uuid = require('uuid')
+
+let signer
+
+describe('accounts', () => {
+  describe('list view', () => {
+    before(() => {
+      signer = new chain.HsmSigner()
+
+      return expect(testHelpers.ensureConfigured()).to.be.fulfilled
+        .then(() => expect(testHelpers.setUpObjects(signer)).to.be.fulfilled)
+        .then(() => browser.url('/accounts'))
+    })
+
+    it('does not display a welcome message', () => {
+      browser.isExisting('.EmptyList').should.equal(false)
+    })
+
+    it('lists all accounts on the core', () => {
+      browser.getText('.ItemList').should.contain('ACCOUNT ALIAS')
+      browser.getText('.ItemList').should.contain('alice')
+      browser.getText('.ItemList').should.contain('View details')
+    })
+
+    it('displays the correct page title', () => {
+      browser.getText('.PageTitle').should.contain('Accounts')
+      browser.getText('.PageTitle').should.contain('New account')
+    })
+  })
+
+  describe('creating accounts', () => {
+    before(() => expect(testHelpers.ensureConfigured()).to.be.fulfilled
+      .then(() => browser.url('/accounts'))
+    )
+
+    it('can create a new account', () => {
+      const alias = 'test-account-' + uuid.v4()
+      browser.click('.ItemList button')
+      browser.setValue('input[name=alias]', alias)
+      browser.click('.FormContainer button')
+      browser.waitForVisible('.AccountShow')
+      browser.getText('.AccountShow').should.contain('Created account. Create another?')
+      browser.getText('.AccountShow').should.contain(alias)
+    })
+  })
+})
diff --git a/test/specs/app.js b/test/specs/app.js
new file mode 100644 (file)
index 0000000..d572cc5
--- /dev/null
@@ -0,0 +1,16 @@
+describe('dashboard', () => {
+  describe('homepage', () => {
+    before(() =>
+      expect(testHelpers.ensureConfigured()).to.be.fulfilled
+        .then(() => browser.url('/'))
+    )
+
+    it('should load the page', function() {
+      browser.getTitle().should.equal('Chain Core Dashboard')
+    })
+
+    it('should redirect to /transactions', function() {
+      browser.getUrl().should.contain('/transactions')
+    })
+  })
+})
diff --git a/test/specs/assets.js b/test/specs/assets.js
new file mode 100644 (file)
index 0000000..97c998c
--- /dev/null
@@ -0,0 +1,48 @@
+const chain = require('chain-sdk')
+const uuid = require('uuid')
+
+let signer
+
+describe('assets', () => {
+  describe('list view', () => {
+    before(() => {
+      signer = new chain.HsmSigner()
+
+      return expect(testHelpers.ensureConfigured()).to.be.fulfilled
+        .then(() => expect(testHelpers.setUpObjects(signer)).to.be.fulfilled)
+        .then(() => browser.url('/assets'))
+    })
+
+    it('does not display a welcome message', () => {
+      browser.isExisting('.EmptyList').should.equal(false)
+    })
+
+    it('lists all assets on the core', () => {
+      browser.getText('.ItemList').should.contain('ASSET ALIAS')
+      browser.getText('.ItemList').should.contain('gold')
+      browser.getText('.ItemList').should.contain('View details')
+    })
+
+    it('displays the correct page title', () => {
+      browser.getText('.PageTitle').should.contain('Assets')
+      browser.getText('.PageTitle').should.contain('New asset')
+    })
+  })
+
+  describe('creating assets', () => {
+    before(() => expect(testHelpers.ensureConfigured()).to.be.fulfilled
+      .then(() => browser.url('/assets'))
+    )
+
+    it('can create a new asset', () => {
+      const alias = 'test-asset-' + uuid.v4()
+      browser.click('.ItemList button')
+      browser.setValue('input[name=alias]', alias)
+      browser.click('.FormContainer button')
+      browser.waitForVisible('.AssetShow')
+      browser.getText('.AssetShow').should.contain('Created asset. Create another?')
+      browser.getText('.AssetShow').should.contain(alias)
+    })
+  })
+
+})
diff --git a/test/specs/extended/empty.js b/test/specs/extended/empty.js
new file mode 100644 (file)
index 0000000..dfb20ef
--- /dev/null
@@ -0,0 +1,27 @@
+describe('empty states', () => {
+  before(() => expect(testHelpers.resetCore()).to.be.fulfilled)
+
+  describe('transactions', () => {
+    it('displays a welcome message', () => {
+      browser.url('/transactions')
+      browser.getText('.EmptyList').should.contain('Welcome to Chain Core')
+      browser.getText('.EmptyList').should.contain('New transaction')
+    })
+  })
+
+  describe('assets', () => {
+    it('displays documentation links', () => {
+      browser.url('/assets')
+      browser.getText('.EmptyList').should.contain('Learn more about how to use assets.')
+      browser.getText('.EmptyList').should.contain('New asset')
+    })
+  })
+
+  describe('accounts', () => {
+    it('displays documentation links', () => {
+      browser.url('/accounts')
+      browser.getText('.EmptyList').should.contain('Learn more about how to use accounts.')
+      browser.getText('.EmptyList').should.contain('New account')
+    })
+  })
+})
diff --git a/test/specs/keys.js b/test/specs/keys.js
new file mode 100644 (file)
index 0000000..13d4215
--- /dev/null
@@ -0,0 +1,48 @@
+const chain = require('chain-sdk')
+const uuid = require('uuid')
+
+let signer
+
+describe('mock hsm keys', () => {
+  describe('list view', () => {
+    before(() => {
+      signer = new chain.HsmSigner()
+
+      return expect(testHelpers.ensureConfigured()).to.be.fulfilled
+        .then(() => expect(testHelpers.setUpObjects(signer)).to.be.fulfilled)
+        .then(() => browser.url('/mockhsms'))
+    })
+
+    it('does not display a welcome message', () => {
+      browser.isExisting('.EmptyList').should.equal(false)
+    })
+
+    it('lists all keys on the core', () => {
+      browser.getText('.ItemList').should.contain('ALIAS')
+      browser.getText('.ItemList').should.contain('XPUB')
+      browser.getText('.ItemList').should.contain('testkey')
+    })
+
+    it('displays the correct page title', () => {
+      browser.getText('.PageTitle').should.contain('MockHSM keys')
+      browser.getText('.PageTitle').should.contain('New MockHSM key')
+    })
+  })
+
+  describe('creating keys', () => {
+    before(() => expect(testHelpers.ensureConfigured()).to.be.fulfilled
+      .then(() => browser.url('/mockhsms'))
+    )
+
+    it('can create a new key', () => {
+      const alias = 'test-key-' + uuid.v4()
+      browser.click('.ItemList button')
+      browser.setValue('input[name=alias]', alias)
+      browser.click('.FormContainer button')
+      browser.waitForVisible('.ItemList')
+      browser.getText('.ItemList').should.contain('Created key. Create another?')
+      browser.getText('.ItemList').should.contain(alias)
+    })
+  })
+
+})
diff --git a/test/specs/transactions.js b/test/specs/transactions.js
new file mode 100644 (file)
index 0000000..9f3cab7
--- /dev/null
@@ -0,0 +1,31 @@
+const chain = require('chain-sdk')
+
+let signer
+
+describe('tranasctions', () => {
+  describe('list view', () => {
+    before(() => {
+      signer = new chain.HsmSigner()
+
+      return expect(testHelpers.ensureConfigured()).to.be.fulfilled
+        .then(() => expect(testHelpers.setUpObjects(signer)).to.be.fulfilled)
+        .then(() => expect(testHelpers.issueTransaction(signer)).to.be.fulfilled)
+        .then(() => browser.url('/transactions'))
+    })
+
+    it('does not display a welcome message', () => {
+      browser.isExisting('.EmptyList').should.equal(false)
+    })
+
+    it('lists all blockchain transactions', () => {
+      browser.getText('.ItemList').should.contain('alice')
+      browser.getText('.ItemList').should.contain('gold')
+      browser.getText('.ItemList').should.contain('100')
+    })
+
+    it('displays the correct page title', () => {
+      browser.getText('.PageTitle').should.contain('Transactions')
+      browser.getText('.PageTitle').should.contain('New transaction')
+    })
+  })
+})
diff --git a/webpack/webpack.app.js b/webpack/webpack.app.js
new file mode 100644 (file)
index 0000000..c2085bd
--- /dev/null
@@ -0,0 +1,135 @@
+/*eslint-env node*/
+
+// TODO: this should be broken up into `dev` and `prod`
+// configuration variants
+
+var webpack = require('webpack')
+var getConfig = require('hjs-webpack')
+var path = require('path')
+
+// Set base path to JS and CSS files when
+// required by other files
+let publicPath = '/'
+let outPath = 'public'
+if (process.env.NODE_ENV === 'production') {
+  publicPath = '/dashboard/'
+} else {
+  outPath = 'node_modules/dashboard-dlls'
+}
+
+// Creates a webpack config object. The
+// object can be extended by accessing
+// its properties.
+var config = getConfig({
+  // entry point for the app
+  in: 'src/app.js',
+
+  // Name or full path of output directory
+  // commonly named `www` or `public`. This
+  // is where your fully static site should
+  // end up for simple deployment.
+  out: outPath,
+
+  output: {
+    hash: true
+  },
+
+  // This will destroy and re-create your
+  // `out` folder before building so you always
+  // get a fresh folder. Usually you want this
+  // but since it's destructive we make it
+  // false by default
+  clearBeforeBuild: true,
+
+  html: function (context) {
+    return {
+      'index.html': context.defaultTemplate({
+        publicPath: publicPath,
+        head: process.env.NODE_ENV !== 'production' ? '<script data-dll="true" src="/dependencies.dll.js"></script>' : '',
+      })
+    }
+  },
+
+  // Proxy API requests to local core server
+  devServer: {
+    proxy: {
+      context: '/api',
+      options: {
+        target: process.env.PROXY_API_HOST || 'http://localhost:1999',
+        pathRewrite: {
+          '^/api': ''
+        }
+      }
+    }
+  }
+})
+
+// Customize loader configuration
+let loaders = config.module.loaders
+
+for (let item of loaders) {
+  // Enable CSS module support
+  if (item.loader && item.loader.indexOf('css-loader') > 0) {
+    item.loader = item.loader.replace('css-loader','css-loader?module&importLoaders=1&localIdentName=[name]__[local]__[hash:base64:5]')
+  }
+  if ('.scss'.match(item.test) != null) {
+    item.loader = item.loader.replace('sass-loader','sass-loader!sass-resources-loader')
+  }
+
+  // Enable babel-loader caching
+  if (item.loader == 'babel-loader') {
+    item.loader = 'babel-loader?cacheDirectory'
+  }
+}
+
+config.module.loaders = loaders
+config.sassResources = './static/styles/resources.scss'
+
+// Configure node modules which may or
+// may not be present in the browser.
+config.node = {
+  console: true,
+  fs: 'empty',
+  net: 'empty',
+  tls: 'empty'
+}
+
+config.resolve = {
+  root: [
+    path.resolve('./src'),
+    path.resolve('./static'),
+  ],
+  extensions: [ '', '.js', '.jsx' ]
+}
+
+// module.noParse disables parsing for
+// matched files. Used here to bypass
+// issues with an AMD configured module.
+config.module.noParse = /node_modules\/json-schema\/lib\/validate\.js/
+
+// Import specified env vars in packaged source
+config.plugins.push(new webpack.DefinePlugin({
+  'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'),
+  'process.env.API_URL': JSON.stringify(process.env.API_URL),
+  'process.env.PROXY_API_HOST': JSON.stringify(process.env.PROXY_API_HOST),
+  'process.env.TESTNET_INFO_URL': JSON.stringify(process.env.TESTNET_INFO_URL),
+  'process.env.TESTNET_GENERATOR_URL': JSON.stringify(process.env.TESTNET_GENERATOR_URL),
+}))
+
+// Enable babel-polyfill
+config.entry.push('babel-polyfill')
+
+config.output.publicPath = publicPath
+
+if (process.env.NODE_ENV !== 'production') {
+  // Support source maps for Babel
+  config.devtool = 'eval-cheap-module-source-map'
+
+  // Use DLL
+  config.plugins.push(new webpack.DllReferencePlugin({
+    context: process.cwd(),
+    manifest: require(path.resolve(process.cwd(), 'node_modules/dashboard-dlls/manifest.json')),
+  }))
+}
+
+module.exports = config
diff --git a/webpack/webpack.base.js b/webpack/webpack.base.js
new file mode 100644 (file)
index 0000000..3e662ac
--- /dev/null
@@ -0,0 +1,85 @@
+// Original from https://github.com/mxstbr/react-boilerplate/
+
+/*eslint-env node*/
+
+/**
+ * COMMON WEBPACK CONFIGURATION
+ */
+
+const path = require('path')
+const webpack = require('webpack')
+
+module.exports = (options) => ({
+  entry: options.entry,
+  output: Object.assign({ // Compile into js/build.js
+    path: path.resolve(process.cwd(), 'public'),
+    publicPath: '/',
+  }, options.output), // Merge with env dependent settings
+  module: {
+    loaders: [{
+      test: /\.js$/, // Transform all .js files required somewhere with Babel
+      loader: 'babel',
+      exclude: /node_modules/,
+      query: options.babelQuery,
+    }, {
+      // Do not transform vendor's CSS with CSS-modules
+      // The point is that they remain in global scope.
+      // Since we require these CSS files in our JS or CSS files,
+      // they will be a part of our compilation either way.
+      // So, no need for ExtractTextPlugin here.
+      test: /\.css$/,
+      include: /node_modules/,
+      loaders: ['style-loader', 'css-loader'],
+    }, {
+      test: /\.(eot|svg|ttf|woff|woff2)$/,
+      loader: 'file-loader',
+    }, {
+      test: /\.(jpg|png|gif)$/,
+      loaders: [
+        'file-loader',
+        'image-webpack?{progressive:true, optimizationLevel: 7, interlaced: false, pngquant:{quality: "65-90", speed: 4}}',
+      ],
+    }, {
+      test: /\.html$/,
+      loader: 'html-loader',
+    }, {
+      test: /\.json$/,
+      loader: 'json-loader',
+    }, {
+      test: /\.(mp4|webm)$/,
+      loader: 'url-loader?limit=10000',
+    }],
+  },
+  plugins: options.plugins.concat([
+    new webpack.ProvidePlugin({
+      // make fetch available
+      fetch: 'exports?self.fetch!whatwg-fetch',
+    }),
+
+    // Always expose NODE_ENV to webpack, in order to use `process.env.NODE_ENV`
+    // inside your code for any environment checks; UglifyJS will automatically
+    // drop any unreachable code.
+    new webpack.DefinePlugin({
+      'process.env': {
+        NODE_ENV: JSON.stringify(process.env.NODE_ENV),
+      },
+    }),
+    new webpack.NamedModulesPlugin(),
+  ]),
+  resolve: {
+    modules: ['src', 'node_modules'],
+    extensions: [
+      '',
+      '.js',
+      '.jsx',
+      '.react.js',
+    ],
+    mainFields: [
+      'browser',
+      'jsnext:main',
+      'main',
+    ],
+  },
+  devtool: options.devtool,
+  target: 'web', // Make web variables accessible to webpack, e.g. window
+})
diff --git a/webpack/webpack.dll.js b/webpack/webpack.dll.js
new file mode 100644 (file)
index 0000000..ef55227
--- /dev/null
@@ -0,0 +1,38 @@
+// Original from https://github.com/mxstbr/react-boilerplate/
+
+/*eslint-env node*/
+
+/**
+ * WEBPACK DLL GENERATOR
+ *
+ * This profile is used to cache webpack's module
+ * contexts for external library and framework type
+ * dependencies which will usually not change often enough
+ * to warrant building them from scratch every time we use
+ * the webpack process.
+ */
+
+const { join } = require('path')
+const webpack = require('webpack')
+const pkg = require(join(process.cwd(), 'package.json'))
+
+const outputPath = join(process.cwd(), 'node_modules/dashboard-dlls')
+
+const config = require('./webpack.base')({
+  context: process.cwd(),
+  entry: {dependencies: Object.keys(pkg.dependencies)},
+  devtool: 'eval',
+  output: {
+    filename: '[name].dll.js',
+    path: outputPath,
+    library: '[name]',
+  },
+  plugins: [
+    new webpack.DllPlugin({
+      name: '[name]',
+      path: join(outputPath, 'manifest.json')
+    }),
+  ],
+})
+
+module.exports = config