diff --git a/Cargo.lock b/Cargo.lock index 821128afa..ab6b28a8b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1294,7 +1294,7 @@ dependencies = [ [[package]] name = "parity-ui-precompiled" version = "1.4.0" -source = "git+https://github.com/ethcore/js-precompiled.git#3e04f32403aab917a149d54f4191263f1c79f5ce" +source = "git+https://github.com/ethcore/js-precompiled.git#6430c6ef5d00f3a411546021ae2915740abce561" dependencies = [ "parity-dapps-glue 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", ] diff --git a/js/.babelrc b/js/.babelrc index 9f44b6bd2..a4f2a2006 100644 --- a/js/.babelrc +++ b/js/.babelrc @@ -13,18 +13,19 @@ "retainLines": true, "env": { "production": { - "plugins": ["transform-react-remove-prop-types"] + "plugins": [ + "transform-react-remove-prop-types" + ] }, "development": { - "plugins": ["react-hot-loader/babel"] + "plugins": [ + "react-hot-loader/babel", + ["react-intl", { "messagesDir": "./.build/i18n/" }] + ] }, "test": { "plugins": [ - [ - "babel-plugin-webpack-alias", { - "config": "webpack/test.js" - } - ] + ["babel-plugin-webpack-alias", { "config": "webpack/test.js" }] ] } } diff --git a/js/package.json b/js/package.json index 577ec6f14..b4ea41a61 100644 --- a/js/package.json +++ b/js/package.json @@ -1,6 +1,6 @@ { "name": "parity.js", - "version": "0.2.111", + "version": "0.2.113", "main": "release/index.js", "jsnext:main": "src/index.js", "author": "Parity Team ", @@ -52,6 +52,7 @@ "babel-eslint": "7.1.1", "babel-loader": "6.2.8", "babel-plugin-lodash": "3.2.10", + "babel-plugin-react-intl": "2.2.0", "babel-plugin-transform-class-properties": "6.18.0", "babel-plugin-transform-decorators-legacy": "1.3.4", "babel-plugin-transform-object-rest-spread": "6.20.2", @@ -111,6 +112,7 @@ "react-addons-perf": "15.4.1", "react-addons-test-utils": "15.4.1", "react-hot-loader": "3.0.0-beta.6", + "react-intl-aggregate-webpack-plugin": "0.0.1", "rucksack-css": "0.9.1", "sinon": "1.17.6", "sinon-as-promised": "4.0.2", @@ -133,6 +135,7 @@ "ethereumjs-tx": "1.1.4", "eventemitter3": "2.0.2", "file-saver": "1.3.3", + "flat": "2.0.1", "format-json": "1.0.3", "format-number": "2.0.1", "geopattern": "1.2.3", @@ -155,6 +158,7 @@ "react-copy-to-clipboard": "4.2.3", "react-dom": "15.4.1", "react-dropzone": "3.7.3", + "react-intl": "2.1.5", "react-redux": "4.4.6", "react-router": "3.0.0", "react-router-redux": "4.0.7", diff --git a/js/src/api/rpc/signer/signer.js b/js/src/api/rpc/signer/signer.js index 6783e660a..403da6c0a 100644 --- a/js/src/api/rpc/signer/signer.js +++ b/js/src/api/rpc/signer/signer.js @@ -14,7 +14,7 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . -import { inNumber16, inData } from '../../format/input'; +import { inData, inNumber16, inOptions } from '../../format/input'; import { outSignerRequest } from '../../format/output'; export default class Signer { @@ -24,7 +24,7 @@ export default class Signer { confirmRequest (requestId, options, password) { return this._transport - .execute('signer_confirmRequest', inNumber16(requestId), options, password); + .execute('signer_confirmRequest', inNumber16(requestId), inOptions(options), password); } confirmRequestRaw (requestId, data) { diff --git a/js/src/i18n/de/index.js b/js/src/i18n/de/index.js new file mode 100644 index 000000000..d8c0f9b4b --- /dev/null +++ b/js/src/i18n/de/index.js @@ -0,0 +1,21 @@ +// Copyright 2015, 2016 Ethcore (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +import settings from './settings'; + +export default { + settings +}; diff --git a/js/src/i18n/de/settings.js b/js/src/i18n/de/settings.js new file mode 100644 index 000000000..9142ff9c7 --- /dev/null +++ b/js/src/i18n/de/settings.js @@ -0,0 +1,96 @@ +// Copyright 2015, 2016 Ethcore (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +export default { + label: 'Einstellungen', + + background: { + label: 'Hintergrund', + + overview_0: 'Dein Hintergrundmuster ist einzigartig und beruht auf deiner Parity-Installation. Es ändert sich immer dann, wenn du ein neues Signer-Token erstellst. So können dezentrale Applicationen keine Vertrauenswürdigkeit vortäuschen.', + overview_1: 'Such dir ein Muster aus und merke es dir. Dieses Muster wird dir von nun an immer angezeigt, es sei denn du löschst deinen Browser-Cache oder benutzt ein neues Signer-Token.', + + button_more: 'weitere generieren' + }, + + parity: { + label: 'Parity', + + overview_0: 'Diese Einstellungen verändern das Verhalten deines Parity-Knotens.', + + languages: { + label: 'Anzeigesprache', + hint: 'Die Sprache, in der dir diese Obefläche angezeigt wird' + }, + + modes: { + label: 'Betriebsmodus', + hint: 'Der Synchronisations-Modus deines Parity-Knotens', + + mode_active: 'Parity synchronisiert kontinuierlich die Blockchain', + mode_passive: 'Parity synchronisiert zu Beginn, schläft dann und wacht regelmäßig zum Synchronisieren auf', + mode_dark: 'Parity synchronisiert nur falls erforderlich, etwa wenn es aus der Ferne aufgerufen wird', + mode_offline: 'Parity synchronisiert nicht' + } + }, + + proxy: { + label: 'Proxy', + + overview_0: 'Die Proxy-Einstellungen ermöglichen dir einen einfachen Zugriff über einprägsame Adressen, auf die Oberfläche mit all ihren dezentralen Anwendungen.', + + details_0: 'Anstelle des Zugriffs über IP-Adresse und Port wirst du über die .parity-Subdomain auf die Parity-Oberfläche zugreifen können, indem du {homeProxy} besuchst. Dafür musst du folgenden Eintrag in den Proxy-Einstellungen deines Browsers hinzufügen:', + details_1: 'Hier findest du Anleitungen zum Anpassen der Proxy-Einstellungen in {windowsLink}, {macOSLink} oder {ubuntuLink}.' + }, + + views: { + label: 'Ansicht', + + overview_0: 'Hier kannst du entscheiden, welche Teile der Parity-Oberfläche dir angezeigt werden sollen.', + overview_1: 'Benutzt du Parity ganz normal? Die Standardeinstellungen sind gleichermaßen für Einsteigende als auch für Fortgeschrittene gedacht.', + overview_2: 'Entwickelst du mit Parity? Füge zum Beispiel den Reiter "Contracts" zu deiner Ansicht hinzu.', + overview_3: 'Bist du Miner oder betreibst du einen großen Knoten? Füge den Reiter "Status" hinzu, um alle Information über den Betrieb deines Knotens im Blick zu halten.', + + accounts: { + label: 'Konten', + description: 'Eine Liste aller Konten, die mit dieser Parity-Installation verbunden sind. Sende Transaktionen, empfange eingehende Beträge, verwalte deine Kontostände oder lade deine Konten auf.' + }, + addresses: { + label: 'Adressbuch', + description: 'Eine Liste all deiner Kontakte und der Adressen, die von dieser Parity-Installation verwaltet werden. Überwache Konten und gelange mit nur einem Klick zu Details deiner Transaktionen.' + }, + apps: { + label: 'Anwendungen', + description: 'Dezentrale Anwendungen, die mit dem Netzwerk interagieren. Füge Anwendungen hinzu, entferne oder öffne Anwendungen.' + }, + contracts: { + label: 'Contracts', + description: 'Interagiere mit Smart Contracts im Netzwerk. Diese Umgebung ist auf Fortgeschrittene mit gutem Verständnis der Fuktionsweise von Smart Contracts zugeschnitten.' + }, + status: { + label: 'Status', + description: 'Schau dir an, wie sich dein Parity-Knoten schlägt. Hier findest du zum Beispiel die Anzahl der aktuellen Verbindungen zum Netzwerk, Logs deiner laufenden Instanz und (sofern konfiguriert) Details zum Mining.' + }, + signer: { + label: 'Signer', + description: 'In diesem sicheren Bereich kannst du Transaktionen, die von dir oder von dezentralen Anwendungen erstellt wurden, prüfen und dann genehmigen oder ablehnen.' + }, + settings: { + label: 'Einstellungen', + description: 'Diese Seite. Pass die Parity-Oberfläche nach deinen Wünschen an.' + } + } +}; diff --git a/js/src/i18n/en/index.js b/js/src/i18n/en/index.js new file mode 100644 index 000000000..d8c0f9b4b --- /dev/null +++ b/js/src/i18n/en/index.js @@ -0,0 +1,21 @@ +// Copyright 2015, 2016 Ethcore (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +import settings from './settings'; + +export default { + settings +}; diff --git a/js/src/i18n/en/settings.js b/js/src/i18n/en/settings.js new file mode 100644 index 000000000..a0e7c7f1f --- /dev/null +++ b/js/src/i18n/en/settings.js @@ -0,0 +1,63 @@ +// Copyright 2015, 2016 Ethcore (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +export default { + label: 'settings', + + background: { + label: 'background' + }, + + parity: { + label: 'parity' + }, + + proxy: { + label: 'proxy' + }, + + views: { + label: 'views', + + accounts: { + label: 'Accounts' + }, + + addresses: { + label: 'Addressbook' + }, + + apps: { + label: 'Applications' + }, + + contracts: { + label: 'Contracts' + }, + + status: { + label: 'Status' + }, + + signer: { + label: 'Signer' + }, + + settings: { + label: 'Settings' + } + } +}; diff --git a/js/src/i18n/index.js b/js/src/i18n/index.js new file mode 100644 index 000000000..4ed119c90 --- /dev/null +++ b/js/src/i18n/index.js @@ -0,0 +1,21 @@ +// Copyright 2015, 2016 Ethcore (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +import LocaleStore from './store'; + +export { + LocaleStore +}; diff --git a/js/src/i18n/languages.js b/js/src/i18n/languages.js new file mode 100644 index 000000000..f4950e401 --- /dev/null +++ b/js/src/i18n/languages.js @@ -0,0 +1,20 @@ +// Copyright 2015, 2016 Ethcore (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +export default { + de: 'Deutsch', + en: 'English' +}; diff --git a/js/src/i18n/store.js b/js/src/i18n/store.js new file mode 100644 index 000000000..f171497e4 --- /dev/null +++ b/js/src/i18n/store.js @@ -0,0 +1,62 @@ +// Copyright 2015, 2016 Ethcore (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +import flatten from 'flat'; +import { action, observable, transaction } from 'mobx'; +import { addLocaleData } from 'react-intl'; +import de from 'react-intl/locale-data/de'; +import en from 'react-intl/locale-data/en'; + +import languages from './languages'; +import deMessages from './de'; +import enMessages from './en'; + +let instance = null; +const isProduction = process.env.NODE_ENV === 'production'; + +const DEFAULT = 'en'; +const LANGUAGES = flatten({ languages }); +const MESSAGES = { + de: Object.assign(flatten(deMessages), LANGUAGES), + en: Object.assign(flatten(enMessages), LANGUAGES) +}; +const LOCALES = isProduction + ? ['en'] + : ['en', 'de']; + +addLocaleData([...de, ...en]); + +export default class Store { + @observable locale = DEFAULT; + @observable locales = LOCALES; + @observable messages = MESSAGES[DEFAULT]; + @observable isDevelopment = !isProduction; + + @action setLocale (locale) { + transaction(() => { + this.locale = locale; + this.messages = MESSAGES[locale]; + }); + } + + static get () { + if (!instance) { + instance = new Store(); + } + + return instance; + } +} diff --git a/js/src/modals/ExecuteContract/executeContract.js b/js/src/modals/ExecuteContract/executeContract.js index 90b161321..afcc826b7 100644 --- a/js/src/modals/ExecuteContract/executeContract.js +++ b/js/src/modals/ExecuteContract/executeContract.js @@ -66,7 +66,7 @@ class ExecuteContract extends Component { onFromAddressChange: PropTypes.func.isRequired } - gasStore = new GasPriceEditor.Store(this.context.api, this.props.gasLimit); + gasStore = new GasPriceEditor.Store(this.context.api, { gasLimit: this.props.gasLimit }); state = { amount: '0', diff --git a/js/src/modals/Transfer/store.js b/js/src/modals/Transfer/store.js index a175c4ea1..679e03609 100644 --- a/js/src/modals/Transfer/store.js +++ b/js/src/modals/Transfer/store.js @@ -112,7 +112,7 @@ export default class TransferStore { this.isWallet = account && account.wallet; this.newError = newError; - this.gasStore = new GasPriceStore(api, gasLimit); + this.gasStore = new GasPriceStore(api, { gasLimit }); if (this.isWallet) { this.wallet = props.wallet; diff --git a/js/src/modals/Transfer/transfer.js b/js/src/modals/Transfer/transfer.js index b43ae0de4..19c337e5a 100644 --- a/js/src/modals/Transfer/transfer.js +++ b/js/src/modals/Transfer/transfer.js @@ -80,8 +80,9 @@ class Transfer extends Component {
+ address={ account.address } + center + inline />
@@ -165,18 +166,18 @@ class Transfer extends Component { balance={ balance } extras={ extras } images={ images } - senders={ senders } + onChange={ this.store.onUpdateDetails } recipient={ recipient } recipientError={ recipientError } sender={ sender } senderError={ senderError } + senders={ senders } sendersBalances={ sendersBalances } tag={ tag } total={ total } totalError={ totalError } value={ value } valueError={ valueError } - onChange={ this.store.onUpdateDetails } wallet={ account.wallet && this.props.wallet } /> ); diff --git a/js/src/redux/providers/signerMiddleware.js b/js/src/redux/providers/signerMiddleware.js index 2e99bbc28..018e01e59 100644 --- a/js/src/redux/providers/signerMiddleware.js +++ b/js/src/redux/providers/signerMiddleware.js @@ -52,9 +52,9 @@ export default class SignerMiddleware { } onConfirmStart = (store, action) => { - const { id, password, wallet, payload } = action.payload; + const { gas, gasPrice, id, password, payload, wallet } = action.payload; - const handlePromise = promise => { + const handlePromise = (promise) => { promise .then((txHash) => { console.log('confirmRequest', id, txHash); @@ -102,7 +102,7 @@ export default class SignerMiddleware { return; } - handlePromise(this._api.signer.confirmRequest(id, {}, password)); + handlePromise(this._api.signer.confirmRequest(id, { gas, gasPrice }, password)); } onRejectStart = (store, action) => { diff --git a/js/src/ui/ContextProvider/contextProvider.js b/js/src/ui/ContextProvider/contextProvider.js index c1efbeb96..c38bda143 100644 --- a/js/src/ui/ContextProvider/contextProvider.js +++ b/js/src/ui/ContextProvider/contextProvider.js @@ -14,8 +14,13 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . -import { Component, PropTypes } from 'react'; +import React, { Component, PropTypes } from 'react'; +import { IntlProvider } from 'react-intl'; +import { observer } from 'mobx-react'; +import { LocaleStore } from '../../i18n'; + +@observer export default class ContextProvider extends Component { static propTypes = { api: PropTypes.object.isRequired, @@ -30,10 +35,17 @@ export default class ContextProvider extends Component { store: PropTypes.object } + localeStore = LocaleStore.get(); + render () { const { children } = this.props; + const { locale, messages } = this.localeStore; - return children; + return ( + + { children } + + ); } getChildContext () { diff --git a/js/src/ui/Editor/mode-solidity.js b/js/src/ui/Editor/mode-solidity.js index 2ad4e9a76..5a74ecb11 100644 --- a/js/src/ui/Editor/mode-solidity.js +++ b/js/src/ui/Editor/mode-solidity.js @@ -22,6 +22,12 @@ /* eslint-disable */ var ace = window.ace; +if (process.env.NODE_ENV === 'test') { + if (!ace.define) { + ace.define = () => {}; + } +} + ace.define("ace/mode/doc_comment_highlight_rules",["require","exports","module","ace/lib/oop","ace/mode/text_highlight_rules"], function(acequire, exports, module) { "use strict"; diff --git a/js/src/ui/Form/Select/select.js b/js/src/ui/Form/Select/select.js index adc4a9bd5..c6dae8b61 100644 --- a/js/src/ui/Form/Select/select.js +++ b/js/src/ui/Form/Select/select.js @@ -17,6 +17,8 @@ import React, { Component, PropTypes } from 'react'; import { SelectField } from 'material-ui'; +import { nodeOrStringProptype } from '~/util/proptypes'; + // TODO: duplicated in Input const UNDERLINE_DISABLED = { borderColor: 'rgba(255, 255, 255, 0.298039)' // 'transparent' // 'rgba(255, 255, 255, 0.298039)' @@ -33,9 +35,9 @@ export default class Select extends Component { children: PropTypes.node, className: PropTypes.string, disabled: PropTypes.bool, - error: PropTypes.string, - hint: PropTypes.string, - label: PropTypes.string, + error: nodeOrStringProptype(), + hint: nodeOrStringProptype(), + label: nodeOrStringProptype(), onBlur: PropTypes.func, onChange: PropTypes.func, onKeyDown: PropTypes.func, diff --git a/js/src/ui/GasPriceEditor/GasPriceSelector/gasPriceSelector.js b/js/src/ui/GasPriceEditor/GasPriceSelector/gasPriceSelector.js deleted file mode 100644 index 375d4962d..000000000 --- a/js/src/ui/GasPriceEditor/GasPriceSelector/gasPriceSelector.js +++ /dev/null @@ -1,556 +0,0 @@ -// Copyright 2015, 2016 Parity Technologies (UK) Ltd. -// This file is part of Parity. - -// Parity is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. - -// Parity is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. - -// You should have received a copy of the GNU General Public License -// along with Parity. If not, see . - -import React, { Component, PropTypes } from 'react'; - -import { - Bar, BarChart, - Rectangle, - Scatter, ScatterChart, - Tooltip, - XAxis, YAxis, - Dot, - ResponsiveContainer -} from 'recharts'; - -import Slider from 'material-ui/Slider'; -import BigNumber from 'bignumber.js'; - -import styles from './gasPriceSelector.css'; - -const COLORS = { - default: 'rgba(255, 99, 132, 0.2)', - selected: 'rgba(255, 99, 132, 0.5)', - hover: 'rgba(255, 99, 132, 0.15)', - grid: 'rgba(255, 99, 132, 0.5)', - line: 'rgb(255, 99, 132)', - intersection: '#fff' -}; - -const countModifier = (count) => { - const val = count.toNumber ? count.toNumber() : count; - return Math.log10(val + 1) + 0.1; -}; - -class CustomCursor extends Component { - static propTypes = { - x: PropTypes.number, - y: PropTypes.number, - width: PropTypes.number, - height: PropTypes.number, - onClick: PropTypes.func, - getIndex: PropTypes.func, - counts: PropTypes.array, - yDomain: PropTypes.array - } - - render () { - const { x, y, width, height, getIndex, counts, yDomain } = this.props; - - const index = getIndex(); - - if (index === -1) { - return null; - } - - const count = countModifier(counts[index]); - const barHeight = (count / yDomain[1]) * (y + height); - - return ( - - - - - ); - } - - onClick = () => { - const { onClick, getIndex } = this.props; - const index = getIndex(); - onClick({ index }); - } -} - -class CustomBar extends Component { - static propTypes = { - selected: PropTypes.number, - x: PropTypes.number, - y: PropTypes.number, - width: PropTypes.number, - height: PropTypes.number, - index: PropTypes.number, - onClick: PropTypes.func - } - - render () { - const { x, y, selected, index, width, height, onClick } = this.props; - - const fill = selected === index - ? COLORS.selected - : COLORS.default; - - const borderWidth = 0.5; - const borderColor = 'rgba(255, 255, 255, 0.5)'; - - return ( - - - - - - - ); - } -} - -class CustomizedShape extends Component { - static propTypes = { - showValue: PropTypes.number.isRequired, - cx: PropTypes.number, - cy: PropTypes.number, - payload: PropTypes.object - } - - render () { - const { cx, cy, showValue, payload } = this.props; - - if (showValue !== payload.y) { - return null; - } - - return ( - - - - - ); - } -} - -class CustomTooltip extends Component { - static propTypes = { - gasPriceHistogram: PropTypes.object.isRequired, - type: PropTypes.string, - payload: PropTypes.array, - label: PropTypes.number, - active: PropTypes.bool - } - - render () { - const { active, label, gasPriceHistogram } = this.props; - - if (!active) { - return null; - } - - const index = label; - - const count = gasPriceHistogram.counts[index]; - const minGasPrice = gasPriceHistogram.bucketBounds[index]; - const maxGasPrice = gasPriceHistogram.bucketBounds[index + 1]; - - return ( -
-

- { count.toNumber() } transactions - with gas price set from - { minGasPrice.toFormat(0) } - to - { maxGasPrice.toFormat(0) } -

-
- ); - } -} - -const TOOL_STYLE = { - color: 'rgba(255,255,255,0.5)', - backgroundColor: 'rgba(0, 0, 0, 0.75)', - padding: '0 0.5em', - fontSize: '0.75em' -}; - -export default class GasPriceSelector extends Component { - static propTypes = { - gasPriceHistogram: PropTypes.object.isRequired, - onChange: PropTypes.func.isRequired, - - gasPrice: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.object - ]) - } - - state = { - gasPrice: null, - sliderValue: 0.5, - selectedIndex: 0, - - chartData: { - values: [], - xDomain: [], - yDomain: [], - N: 0 - } - } - - componentWillMount () { - this.computeCharts(); - this.setGasPrice(); - } - - componentWillReceiveProps (nextProps) { - if (nextProps.gasPrice !== this.props.gasPrice) { - this.setGasPrice(nextProps); - } - } - - componentWillUpdate (nextProps, nextState) { - if (Math.floor(nextState.sliderValue) !== Math.floor(this.state.sliderValue)) { - this.updateSelectedBarChart(nextState); - } - } - - render () { - return ( -
- { this.renderChart() } - { this.renderSlider() } -
- ); - } - - renderSlider () { - const { sliderValue } = this.state; - - return ( -
- -
- ); - } - - renderChart () { - const { gasPriceHistogram } = this.props; - const { chartData, sliderValue, selectedIndex } = this.state; - - if (chartData.values.length === 0) { - return null; - } - - const height = 300; - const countIndex = Math.max(0, Math.min(selectedIndex, gasPriceHistogram.counts.length - 1)); - const selectedCount = countModifier(gasPriceHistogram.counts[countIndex]); - - return ( -
-
-
- - - } - line - isAnimationActive={ false } - /> - - - - - -
- -
- - - } - /> - - } - /> - - - - - -
-
-
- ); - } - - renderCustomCursor = () => { - const { gasPriceHistogram } = this.props; - const { chartData } = this.state; - - return ( - - ); - } - - getBarHoverIndex = () => { - const { barChart } = this.refs; - - if (!barChart || !barChart.state) { - return -1; - } - - return barChart.state.activeTooltipIndex; - } - - computeChartsData () { - const { gasPriceChartData } = this.state; - const { gasPriceHistogram } = this.props; - - const values = gasPriceChartData - .map((value, index) => ({ value, index })); - - const N = values.length - 1; - const maxGasCounts = countModifier( - gasPriceHistogram - .counts - .reduce((max, count) => count.greaterThan(max) ? count : max, 0) - ); - - const xDomain = [0, N]; - const yDomain = [0, maxGasCounts * 1.1]; - - const chartData = { - values, N, - xDomain, yDomain - }; - - this.setState({ chartData }, () => { - this.updateSelectedBarChart(); - }); - } - - computeCharts (props = this.props) { - const { gasPriceHistogram } = props; - - const gasPriceChartData = gasPriceHistogram - .counts - .map(count => countModifier(count)); - - this.setState( - { gasPriceChartData }, - () => this.computeChartsData() - ); - } - - updateSelectedBarChart (state = this.state) { - } - - setGasPrice (props = this.props) { - const { gasPrice, gasPriceHistogram } = props; - - // If no gas price yet... - if (!gasPrice) { - return this.setSliderValue(0.5); - } - - const bnGasPrice = (typeof gasPrice === 'string') - ? new BigNumber(gasPrice) - : gasPrice; - - // If gas price hasn't changed - if (this.state.gasPrice && bnGasPrice.equals(this.state.gasPrice)) { - return; - } - - const gasPrices = gasPriceHistogram.bucketBounds; - const startIndex = gasPrices - .findIndex(price => price.greaterThan(bnGasPrice)) - 1; - - // gasPrice Lower than the max in histogram - if (startIndex === -1) { - return this.setSliderValue(0, bnGasPrice); - } - - // gasPrice Greater than the max in histogram - if (startIndex === -2) { - return this.setSliderValue(1, bnGasPrice); - } - - const priceA = gasPrices[startIndex]; - const priceB = gasPrices[startIndex + 1]; - - const sliderValueDec = bnGasPrice - .minus(priceA) - .dividedBy(priceB.minus(priceA)) - .toNumber(); - - const sliderValue = (startIndex + sliderValueDec) / (gasPrices.length - 1); - this.setSliderValue(sliderValue, bnGasPrice); - } - - setSliderValue (value, gasPrice = this.state.gasPrice) { - const { gasPriceHistogram } = this.props; - - const N = gasPriceHistogram.bucketBounds.length - 1; - - const sliderValue = Math.max(0, Math.min(value, 1)); - const selectedIndex = Math.floor(sliderValue * N); - - this.setState({ sliderValue, gasPrice, selectedIndex }); - } - - onBarChartMouseUp = (event) => { - console.log(event); - } - - onClickGasPrice = (bar) => { - const { index } = bar; - - const ratio = (index + 0.5) / (this.state.chartData.N + 1); - - this.onEditGasPriceSlider(null, ratio); - } - - onEditGasPriceSlider = (event, sliderValue) => { - const { gasPriceHistogram } = this.props; - - const gasPrices = gasPriceHistogram.bucketBounds; - const N = gasPrices.length - 1; - const gasPriceAIdx = Math.floor(sliderValue * N); - const gasPriceBIdx = gasPriceAIdx + 1; - - if (gasPriceBIdx === N + 1) { - const gasPrice = gasPrices[gasPriceAIdx].round(); - this.props.onChange(event, gasPrice); - return; - } - - const gasPriceA = gasPrices[gasPriceAIdx]; - const gasPriceB = gasPrices[gasPriceBIdx]; - - const mult = Math.round((sliderValue % 1) * 100) / 100; - const gasPrice = gasPriceA - .plus(gasPriceB.minus(gasPriceA).times(mult)) - .round(); - - this.setSliderValue(sliderValue, gasPrice); - this.props.onChange(event, gasPrice); - } -} diff --git a/js/src/ui/GasPriceEditor/gasPriceEditor.css b/js/src/ui/GasPriceEditor/gasPriceEditor.css index cf1fff81c..bcb99a63b 100644 --- a/js/src/ui/GasPriceEditor/gasPriceEditor.css +++ b/js/src/ui/GasPriceEditor/gasPriceEditor.css @@ -15,16 +15,12 @@ /* along with Parity. If not, see . */ -.columns { +.container { display: flex; flex-wrap: wrap; position: relative; } -.graphColumn { - flex: 65; -} - .editColumn { flex: 35; padding-left: 1em; @@ -41,6 +37,10 @@ opacity: 0.5; } +.graphColumn { + flex: 65; +} + .row { display: flex; flex-wrap: wrap; diff --git a/js/src/ui/GasPriceEditor/gasPriceEditor.js b/js/src/ui/GasPriceEditor/gasPriceEditor.js index 323f189e4..e98707410 100644 --- a/js/src/ui/GasPriceEditor/gasPriceEditor.js +++ b/js/src/ui/GasPriceEditor/gasPriceEditor.js @@ -15,11 +15,11 @@ // along with Parity. If not, see . import BigNumber from 'bignumber.js'; -import React, { Component, PropTypes } from 'react'; import { observer } from 'mobx-react'; +import React, { Component, PropTypes } from 'react'; import Input from '../Form/Input'; -import GasPriceSelector from './GasPriceSelector'; +import GasPriceSelector from '../GasPriceSelector'; import Store from './store'; import styles from './gasPriceEditor.css'; @@ -31,62 +31,60 @@ export default class GasPriceEditor extends Component { }; static propTypes = { - store: PropTypes.object.isRequired, - onChange: PropTypes.func + children: PropTypes.node, + onChange: PropTypes.func, + store: PropTypes.object.isRequired } static Store = Store; render () { const { api } = this.context; - const { store } = this.props; - const { estimated, priceDefault, price, gas, histogram, errorGas, errorPrice, errorTotal, totalValue } = store; + const { children, store } = this.props; + const { errorGas, errorPrice, errorTotal, estimated, gas, histogram, price, priceDefault, totalValue } = store; const eth = api.util.fromWei(totalValue).toFormat(); const gasLabel = `gas (estimated: ${new BigNumber(estimated).toFormat()})`; const priceLabel = `price (current: ${new BigNumber(priceDefault).toFormat()})`; return ( -
+
+ histogram={ histogram } + onChange={ this.onEditGasPrice } + price={ price } />
- You can choose the gas price based on the - distribution of recent included transaction gas prices. - The lower the gas price is, the cheaper the transaction will - be. The higher the gas price is, the faster it should - get mined by the network. + You can choose the gas price based on the distribution of recent included transaction gas prices. The lower the gas price is, the cheaper the transaction will be. The higher the gas price is, the faster it should get mined by the network.
- + hint='the amount of gas to use for the transaction' + label={ gasLabel } + onChange={ this.onEditGas } + value={ gas } /> + hint='the price of gas to use for the transaction' + label={ priceLabel } + onChange={ this.onEditGasPrice } + value={ price } />
-
+
+ { children } +
); diff --git a/js/src/ui/GasPriceEditor/gasPriceEditor.spec.js b/js/src/ui/GasPriceEditor/gasPriceEditor.spec.js new file mode 100644 index 000000000..4197bc876 --- /dev/null +++ b/js/src/ui/GasPriceEditor/gasPriceEditor.spec.js @@ -0,0 +1,45 @@ +// Copyright 2015, 2016 Ethcore (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +import BigNumber from 'bignumber.js'; +import { shallow } from 'enzyme'; +import React from 'react'; +import sinon from 'sinon'; + +import GasPriceEditor from './'; + +const api = { + util: { + fromWei: (value) => new BigNumber(value) + } +}; + +const store = { + estimated: '123', + priceDefault: '456', + totalValue: '789', + setGas: sinon.stub(), + setPrice: sinon.stub() +}; + +describe('ui/GasPriceEditor', () => { + it('renders', () => { + expect(shallow( + , + { context: { api } } + )).to.be.ok; + }); +}); diff --git a/js/src/ui/GasPriceEditor/store.js b/js/src/ui/GasPriceEditor/store.js index 9c403acb2..22867fdd0 100644 --- a/js/src/ui/GasPriceEditor/store.js +++ b/js/src/ui/GasPriceEditor/store.js @@ -26,18 +26,22 @@ export default class GasPriceEditor { @observable errorPrice = null; @observable errorTotal = null; @observable estimated = DEFAULT_GAS; + @observable gas; + @observable gasLimit; @observable histogram = null; - @observable price = DEFAULT_GASPRICE; - @observable priceDefault = DEFAULT_GASPRICE; - @observable gas = DEFAULT_GAS; - @observable gasLimit = 0; + @observable isEditing = false; + @observable price; + @observable priceDefault; @observable weiValue = '0'; - constructor (api, gasLimit, loadDefaults = true) { + constructor (api, { gas, gasLimit, gasPrice }) { this._api = api; - this.gasLimit = gasLimit; - if (loadDefaults) { + this.gas = gas; + this.gasLimit = gasLimit; + this.price = gasPrice; + + if (api) { this.loadDefaults(); } } @@ -50,6 +54,10 @@ export default class GasPriceEditor { } } + @action setEditing = (isEditing) => { + this.isEditing = isEditing; + } + @action setErrorTotal = (errorTotal) => { this.errorTotal = errorTotal; } @@ -74,6 +82,30 @@ export default class GasPriceEditor { this.weiValue = weiValue; } + @action setGas = (gas) => { + transaction(() => { + const { numberError } = validatePositiveNumber(gas); + + this.gas = gas; + + if (numberError) { + this.errorGas = numberError; + } else { + const bn = new BigNumber(gas); + + if (bn.gte(this.gasLimit)) { + this.errorGas = ERRORS.gasBlockLimit; + } else { + this.errorGas = null; + } + } + }); + } + + @action setGasLimit = (gasLimit) => { + this.gasLimit = gasLimit; + } + @action setHistogram = (gasHistogram) => { this.histogram = gasHistogram; } @@ -85,39 +117,37 @@ export default class GasPriceEditor { }); } - @action setGas = (gas) => { - transaction(() => { - const { numberError } = validatePositiveNumber(gas); - const bn = new BigNumber(gas); - - this.gas = gas; - - if (numberError) { - this.errorGas = numberError; - } else if (bn.gte(this.gasLimit)) { - this.errorGas = ERRORS.gasBlockLimit; - } else { - this.errorGas = null; - } - }); - } - @action loadDefaults () { Promise .all([ this._api.parity.gasPriceHistogram(), this._api.eth.gasPrice() ]) - .then(([gasPriceHistogram, gasPrice]) => { + .then(([histogram, _price]) => { transaction(() => { - this.setPrice(gasPrice.toFixed(0)); - this.setHistogram(gasPriceHistogram); + const price = _price.toFixed(0); - this.priceDefault = gasPrice.toFixed(); + if (!this.price) { + this.setPrice(price); + } + this.setHistogram(histogram); + + this.priceDefault = price; }); }) .catch((error) => { console.warn('getDefaults', error); }); } + + overrideTransaction = (transaction) => { + if (this.errorGas || this.errorPrice) { + return transaction; + } + + return Object.assign({}, transaction, { + gas: new BigNumber(this.gas || DEFAULT_GAS), + gasPrice: new BigNumber(this.price || DEFAULT_GASPRICE) + }); + } } diff --git a/js/src/ui/GasPriceEditor/store.spec.js b/js/src/ui/GasPriceEditor/store.spec.js new file mode 100644 index 000000000..bbee1b8c4 --- /dev/null +++ b/js/src/ui/GasPriceEditor/store.spec.js @@ -0,0 +1,197 @@ +// Copyright 2015, 2016 Ethcore (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +import BigNumber from 'bignumber.js'; +import sinon from 'sinon'; + +import { DEFAULT_GAS, DEFAULT_GASPRICE, MAX_GAS_ESTIMATION } from '~/util/constants'; +import { ERRORS } from '~/util/validation'; + +import GasPriceEditor from './gasPriceEditor'; + +const { Store } = GasPriceEditor; + +const GASPRICE = new BigNumber(123456); +const GASLIMIT = 100000; +const HISTOGRAM = { + bucketBounds: [1, 2], + counts: [3, 4] +}; + +const api = { + eth: { + gasPrice: sinon.stub().resolves(GASPRICE) + }, + parity: { + gasPriceHistogram: sinon.stub().resolves(HISTOGRAM) + } +}; + +describe('ui/GasPriceEditor/store', () => { + let store = null; + + it('is available via GasPriceEditor.Store', () => { + expect(new Store(null, {})).to.be.ok; + }); + + describe('constructor (defaults)', () => { + beforeEach(() => { + store = new Store(api, { gasLimit: GASLIMIT }); + }); + + it('retrieves the histogram and gasPrice', () => { + expect(api.eth.gasPrice).to.have.been.called; + expect(api.parity.gasPriceHistogram).to.have.been.called; + }); + + it('sets the gasLimit as passed', () => { + expect(store.gasLimit).to.equal(GASLIMIT); + }); + }); + + describe('setters', () => { + beforeEach(() => { + store = new Store(null, { gasLimit: GASLIMIT }); + }); + + describe('setEditing', () => { + it('sets the value', () => { + expect(store.isEditing).to.be.false; + store.setEditing(true); + expect(store.isEditing).to.be.true; + }); + }); + + describe('setErrorTotal', () => { + it('sets the value', () => { + store.setErrorTotal('errorTotal'); + expect(store.errorTotal).to.equal('errorTotal'); + }); + }); + + describe('setEstimated', () => { + it('sets the value', () => { + store.setEstimated('789'); + expect(store.estimated).to.equal('789'); + }); + + it('sets error when above exception max', () => { + store.setEstimated(MAX_GAS_ESTIMATION); + expect(store.errorEstimated).to.equal(ERRORS.gasException); + }); + + it('sets error when above gaslimit', () => { + store.setEstimated(GASLIMIT); + expect(store.errorEstimated).to.equal(ERRORS.gasBlockLimit); + }); + }); + + describe('setEthValue', () => { + it('sets the value', () => { + store.setEthValue('123'); + expect(store.weiValue).to.equal('123'); + }); + }); + + describe('setGas', () => { + it('sets the value', () => { + store.setGas('123'); + expect(store.gas).to.equal('123'); + expect(store.errorGas).to.be.null; + }); + + it('sets error on negative numbers', () => { + store.setGas(-123); + expect(store.errorGas).not.to.be.null; + }); + + it('sets error when above block limit', () => { + store.setGas(GASLIMIT); + expect(store.errorGas).to.equal(ERRORS.gasBlockLimit); + }); + }); + + describe('setGasLimit', () => { + it('sets the value', () => { + store.setGasLimit('123'); + expect(store.gasLimit).to.equal('123'); + }); + }); + + describe('setHistogram', () => { + it('sets the value', () => { + store.setHistogram(HISTOGRAM); + expect(store.histogram).to.deep.equal(HISTOGRAM); + }); + }); + + describe('setPrice', () => { + it('sets the value', () => { + store.setPrice('123'); + expect(store.price).to.equal('123'); + expect(store.errorPrice).to.be.null; + }); + + it('sets error on negative numbers', () => { + store.setPrice(-123); + expect(store.errorPrice).not.to.be.null; + }); + }); + }); + + describe('computed', () => { + beforeEach(() => { + store = new Store(null, { gasLimit: GASLIMIT }); + }); + + describe('totalValue', () => { + it('holds the total including eth, price & gas', () => { + store.setPrice('123'); + store.setGas('123'); + store.setEthValue('123'); + expect(store.totalValue).to.deep.equal(new BigNumber(123 + 123 * 123)); + }); + }); + }); + + describe('methods', () => { + beforeEach(() => { + store = new Store(null, { gasLimit: GASLIMIT }); + }); + + describe('overrideTransaction', () => { + const TRANSACTION = { gas: '123', gasPrice: '456' }; + + it('overrides gas & gasPrice with values', () => { + store.setGas(DEFAULT_GAS); + const transaction = store.overrideTransaction(TRANSACTION); + + expect(transaction.gas).to.deep.equal(new BigNumber(DEFAULT_GAS)); + expect(transaction.gasPrice).to.deep.equal(new BigNumber(DEFAULT_GASPRICE)); + }); + + it('does not override with invalid gas', () => { + store.setGas(-123); + expect(store.overrideTransaction(TRANSACTION)).to.deep.equal(TRANSACTION); + }); + + it('does not override with invalid price', () => { + store.setPrice(-123); + expect(store.overrideTransaction(TRANSACTION)).to.deep.equal(TRANSACTION); + }); + }); + }); +}); diff --git a/js/src/ui/GasPriceSelector/CustomBar/customBar.js b/js/src/ui/GasPriceSelector/CustomBar/customBar.js new file mode 100644 index 000000000..60f066f97 --- /dev/null +++ b/js/src/ui/GasPriceSelector/CustomBar/customBar.js @@ -0,0 +1,77 @@ +// Copyright 2015, 2016 Ethcore (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +import React, { Component, PropTypes } from 'react'; +import { Rectangle } from 'recharts'; + +import { COLORS } from '../util'; + +export default class CustomBar extends Component { + static propTypes = { + selected: PropTypes.number, + x: PropTypes.number, + y: PropTypes.number, + width: PropTypes.number, + height: PropTypes.number, + index: PropTypes.number, + onClick: PropTypes.func + } + + render () { + const { x, y, selected, index, width, height, onClick } = this.props; + + const fill = selected === index + ? COLORS.selected + : COLORS.default; + + const borderWidth = 0.5; + const borderColor = 'rgba(255, 255, 255, 0.5)'; + + return ( + + + + + + + ); + } +} diff --git a/js/src/ui/GasPriceSelector/CustomBar/index.js b/js/src/ui/GasPriceSelector/CustomBar/index.js new file mode 100644 index 000000000..fa4008e42 --- /dev/null +++ b/js/src/ui/GasPriceSelector/CustomBar/index.js @@ -0,0 +1,17 @@ +// Copyright 2015, 2016 Ethcore (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +export default from './customBar'; diff --git a/js/src/ui/GasPriceSelector/CustomCursor/customCursor.js b/js/src/ui/GasPriceSelector/CustomCursor/customCursor.js new file mode 100644 index 000000000..8ce9a0e42 --- /dev/null +++ b/js/src/ui/GasPriceSelector/CustomCursor/customCursor.js @@ -0,0 +1,73 @@ +// Copyright 2015, 2016 Ethcore (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +import React, { Component, PropTypes } from 'react'; +import { Rectangle } from 'recharts'; + +import { COLORS, countModifier } from '../util'; + +export default class CustomCursor extends Component { + static propTypes = { + x: PropTypes.number, + y: PropTypes.number, + width: PropTypes.number, + height: PropTypes.number, + onClick: PropTypes.func, + getIndex: PropTypes.func, + counts: PropTypes.object, + yDomain: PropTypes.array + } + + render () { + const { x, y, width, height, getIndex, counts, yDomain } = this.props; + + const index = getIndex(); + + if (index === -1) { + return null; + } + + const count = countModifier(counts[index]); + const barHeight = (count / yDomain[1]) * (y + height); + + return ( + + + + + ); + } + + onClick = () => { + const { onClick, getIndex } = this.props; + const index = getIndex(); + onClick({ index }); + } +} diff --git a/js/src/ui/GasPriceSelector/CustomCursor/index.js b/js/src/ui/GasPriceSelector/CustomCursor/index.js new file mode 100644 index 000000000..8129d97f0 --- /dev/null +++ b/js/src/ui/GasPriceSelector/CustomCursor/index.js @@ -0,0 +1,17 @@ +// Copyright 2015, 2016 Ethcore (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +export default from './customCursor'; diff --git a/js/src/ui/GasPriceSelector/CustomShape/customShape.js b/js/src/ui/GasPriceSelector/CustomShape/customShape.js new file mode 100644 index 000000000..1c721ab35 --- /dev/null +++ b/js/src/ui/GasPriceSelector/CustomShape/customShape.js @@ -0,0 +1,52 @@ +// Copyright 2015, 2016 Ethcore (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +import React, { Component, PropTypes } from 'react'; +import { Dot } from 'recharts'; + +export default class CustomShape extends Component { + static propTypes = { + showValue: PropTypes.number.isRequired, + cx: PropTypes.number, + cy: PropTypes.number, + payload: PropTypes.object + } + + render () { + const { cx, cy, showValue, payload } = this.props; + + if (showValue !== payload.y) { + return null; + } + + return ( + + + + + ); + } +} diff --git a/js/src/ui/GasPriceSelector/CustomShape/index.js b/js/src/ui/GasPriceSelector/CustomShape/index.js new file mode 100644 index 000000000..663c6b91b --- /dev/null +++ b/js/src/ui/GasPriceSelector/CustomShape/index.js @@ -0,0 +1,17 @@ +// Copyright 2015, 2016 Ethcore (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +export default from './customShape'; diff --git a/js/src/ui/GasPriceSelector/CustomTooltip/customTooltip.js b/js/src/ui/GasPriceSelector/CustomTooltip/customTooltip.js new file mode 100644 index 000000000..4fd165cb0 --- /dev/null +++ b/js/src/ui/GasPriceSelector/CustomTooltip/customTooltip.js @@ -0,0 +1,53 @@ +// Copyright 2015, 2016 Ethcore (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +import React, { Component, PropTypes } from 'react'; + +export default class CustomTooltip extends Component { + static propTypes = { + active: PropTypes.bool, + histogram: PropTypes.object.isRequired, + label: PropTypes.number, + payload: PropTypes.array, + type: PropTypes.string + } + + render () { + const { active, label, histogram } = this.props; + + if (!active) { + return null; + } + + const index = label; + + const count = histogram.counts[index]; + const minprice = histogram.bucketBounds[index]; + const maxprice = histogram.bucketBounds[index + 1]; + + return ( +
+

+ { count.toNumber() } transactions + with gas price set from + { minprice.toFormat(0) } + to + { maxprice.toFormat(0) } +

+
+ ); + } +} diff --git a/js/src/ui/GasPriceSelector/CustomTooltip/index.js b/js/src/ui/GasPriceSelector/CustomTooltip/index.js new file mode 100644 index 000000000..a16f9d2f9 --- /dev/null +++ b/js/src/ui/GasPriceSelector/CustomTooltip/index.js @@ -0,0 +1,17 @@ +// Copyright 2015, 2016 Ethcore (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +export default from './customTooltip'; diff --git a/js/src/ui/GasPriceEditor/GasPriceSelector/gasPriceSelector.css b/js/src/ui/GasPriceSelector/gasPriceSelector.css similarity index 92% rename from js/src/ui/GasPriceEditor/GasPriceSelector/gasPriceSelector.css rename to js/src/ui/GasPriceSelector/gasPriceSelector.css index 247211c50..ce5bd55e8 100644 --- a/js/src/ui/GasPriceEditor/GasPriceSelector/gasPriceSelector.css +++ b/js/src/ui/GasPriceSelector/gasPriceSelector.css @@ -20,8 +20,12 @@ width: 100%; } -.columns { +.chartRow, .sliderRow { display: flex; flex-wrap: wrap; position: relative; } + +.chartRow { + margin-bottom: -24px; +} diff --git a/js/src/ui/GasPriceSelector/gasPriceSelector.js b/js/src/ui/GasPriceSelector/gasPriceSelector.js new file mode 100644 index 000000000..e37dc8b39 --- /dev/null +++ b/js/src/ui/GasPriceSelector/gasPriceSelector.js @@ -0,0 +1,341 @@ +// Copyright 2015, 2016 Ethcore (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +import BigNumber from 'bignumber.js'; +import { Slider } from 'material-ui'; +import React, { Component, PropTypes } from 'react'; +import { Bar, BarChart, ResponsiveContainer, Scatter, ScatterChart, Tooltip, XAxis, YAxis } from 'recharts'; + +import CustomBar from './CustomBar'; +import CustomCursor from './CustomCursor'; +import CustomShape from './CustomShape'; +import CustomTooltip from './CustomTooltip'; + +import { COLORS, countModifier } from './util'; + +import styles from './gasPriceSelector.css'; + +const TOOL_STYLE = { + color: 'rgba(255,255,255,0.5)', + backgroundColor: 'rgba(0, 0, 0, 0.75)', + padding: '0 0.5em', + fontSize: '0.75em' +}; + +export default class GasPriceSelector extends Component { + static propTypes = { + histogram: PropTypes.object.isRequired, + onChange: PropTypes.func.isRequired, + price: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.object + ]) + } + + state = { + price: null, + sliderValue: 0.5, + selectedIndex: 0, + + chartData: { + values: [], + xDomain: [], + yDomain: [], + N: 0 + } + } + + componentWillMount () { + this.computeCharts(); + this.setprice(); + } + + componentWillReceiveProps (nextProps) { + if (nextProps.price !== this.props.price) { + this.setprice(nextProps); + } + } + + componentWillUpdate (nextProps, nextState) { + if (Math.floor(nextState.sliderValue) !== Math.floor(this.state.sliderValue)) { + this.updateSelectedBarChart(nextState); + } + } + + render () { + return ( +
+ { this.renderChart() } + { this.renderSlider() } +
+ ); + } + + renderChart () { + const { histogram } = this.props; + const { chartData, sliderValue, selectedIndex } = this.state; + + if (chartData.values.length === 0) { + return null; + } + + const height = 196; + const countIndex = Math.max(0, Math.min(selectedIndex, histogram.counts.length - 1)); + const selectedCount = countModifier(histogram.counts[countIndex]); + + return ( +
+
+ +
+ + + } /> + + + + +
+ +
+ + + }stroke={ COLORS.line } /> + } + cursor={ this.renderCustomCursor() } + wrapperStyle={ TOOL_STYLE } /> + + + + +
+
+
+ ); + } + + renderSlider () { + const { sliderValue } = this.state; + + return ( +
+ +
+ ); + } + + renderCustomCursor = () => { + const { histogram } = this.props; + const { chartData } = this.state; + + return ( + + ); + } + + getBarHoverIndex = () => { + const { barChart } = this.refs; + + if (!barChart || !barChart.state) { + return -1; + } + + return barChart.state.activeTooltipIndex; + } + + computeChartsData () { + const { priceChartData } = this.state; + const { histogram } = this.props; + + const values = priceChartData + .map((value, index) => ({ value, index })); + + const N = values.length - 1; + const maxGasCounts = countModifier( + histogram + .counts + .reduce((max, count) => count.greaterThan(max) ? count : max, 0) + ); + + const xDomain = [0, N]; + const yDomain = [0, maxGasCounts * 1.1]; + + const chartData = { + values, N, + xDomain, yDomain + }; + + this.setState({ chartData }, () => { + this.updateSelectedBarChart(); + }); + } + + computeCharts (props = this.props) { + const { histogram } = props; + + const priceChartData = histogram + .counts + .map(count => countModifier(count)); + + this.setState( + { priceChartData }, + () => this.computeChartsData() + ); + } + + updateSelectedBarChart (state = this.state) { + } + + setprice (props = this.props) { + const { price, histogram } = props; + + // If no gas price yet... + if (!price) { + return this.setSliderValue(0.5); + } + + const bnprice = (typeof price === 'string') + ? new BigNumber(price) + : price; + + // If gas price hasn't changed + if (this.state.price && bnprice.equals(this.state.price)) { + return; + } + + const prices = histogram.bucketBounds; + const startIndex = prices + .findIndex(price => price.greaterThan(bnprice)) - 1; + + // price Lower than the max in histogram + if (startIndex === -1) { + return this.setSliderValue(0, bnprice); + } + + // price Greater than the max in histogram + if (startIndex === -2) { + return this.setSliderValue(1, bnprice); + } + + const priceA = prices[startIndex]; + const priceB = prices[startIndex + 1]; + + const sliderValueDec = bnprice + .minus(priceA) + .dividedBy(priceB.minus(priceA)) + .toNumber(); + + const sliderValue = (startIndex + sliderValueDec) / (prices.length - 1); + this.setSliderValue(sliderValue, bnprice); + } + + setSliderValue (value, price = this.state.price) { + const { histogram } = this.props; + + const N = histogram.bucketBounds.length - 1; + + const sliderValue = Math.max(0, Math.min(value, 1)); + const selectedIndex = Math.floor(sliderValue * N); + + this.setState({ sliderValue, price, selectedIndex }); + } + + onBarChartMouseUp = (event) => { + console.log(event); + } + + onClickprice = (bar) => { + const { index } = bar; + + const ratio = (index + 0.5) / (this.state.chartData.N + 1); + + this.onEditpriceSlider(null, ratio); + } + + onEditpriceSlider = (event, sliderValue) => { + const { histogram } = this.props; + + const prices = histogram.bucketBounds; + const N = prices.length - 1; + const priceAIdx = Math.floor(sliderValue * N); + const priceBIdx = priceAIdx + 1; + + if (priceBIdx === N + 1) { + const price = prices[priceAIdx].round(); + this.props.onChange(event, price); + return; + } + + const priceA = prices[priceAIdx]; + const priceB = prices[priceBIdx]; + + const mult = Math.round((sliderValue % 1) * 100) / 100; + const price = priceA + .plus(priceB.minus(priceA).times(mult)) + .round(); + + this.setSliderValue(sliderValue, price); + this.props.onChange(event, price.toFixed()); + } +} diff --git a/js/src/ui/GasPriceEditor/GasPriceSelector/index.js b/js/src/ui/GasPriceSelector/index.js similarity index 100% rename from js/src/ui/GasPriceEditor/GasPriceSelector/index.js rename to js/src/ui/GasPriceSelector/index.js diff --git a/js/src/ui/GasPriceSelector/util.js b/js/src/ui/GasPriceSelector/util.js new file mode 100644 index 000000000..63c3e89b0 --- /dev/null +++ b/js/src/ui/GasPriceSelector/util.js @@ -0,0 +1,34 @@ +// Copyright 2015, 2016 Ethcore (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +const COLORS = { + default: 'rgba(255, 99, 132, 0.2)', + selected: 'rgba(255, 99, 132, 0.5)', + hover: 'rgba(255, 99, 132, 0.15)', + grid: 'rgba(255, 99, 132, 0.5)', + line: 'rgb(255, 99, 132)', + intersection: '#fff' +}; + +const countModifier = (count) => { + const val = count.toNumber ? count.toNumber() : count; + return Math.log10(val + 1) + 0.1; +}; + +export { + COLORS, + countModifier +}; diff --git a/js/src/ui/LanguageSelector/index.js b/js/src/ui/LanguageSelector/index.js new file mode 100644 index 000000000..772167d14 --- /dev/null +++ b/js/src/ui/LanguageSelector/index.js @@ -0,0 +1,17 @@ +// Copyright 2015, 2016 Ethcore (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +export default from './languageSelector'; diff --git a/js/src/ui/LanguageSelector/languageSelector.js b/js/src/ui/LanguageSelector/languageSelector.js new file mode 100644 index 000000000..db8949510 --- /dev/null +++ b/js/src/ui/LanguageSelector/languageSelector.js @@ -0,0 +1,71 @@ +// Copyright 2015, 2016 Ethcore (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +import React, { Component } from 'react'; +import { FormattedMessage } from 'react-intl'; +import { MenuItem } from 'material-ui'; +import { observer } from 'mobx-react'; + +import Select from '../Form/Select'; +import { LocaleStore } from '../../i18n'; + +@observer +export default class LanguageSelector extends Component { + store = LocaleStore.get(); + + render () { + if (!this.store.isDevelopment) { + return null; + } + + return ( + + ); + } + + renderOptions () { + return this.store.locales.map((locale) => { + const label = ; + + return ( + + { label } + + ); + }); + } + + onChange = (event, index, locale) => { + this.store.setLocale(locale); + } +} diff --git a/js/src/ui/index.js b/js/src/ui/index.js index 785f33850..e1ebb16a6 100644 --- a/js/src/ui/index.js +++ b/js/src/ui/index.js @@ -32,8 +32,10 @@ import Editor from './Editor'; import Errors from './Errors'; import Form, { AddressSelect, FormWrap, TypedInput, Input, InputAddress, InputAddressSelect, InputChip, InputInline, Select, RadioButtons } from './Form'; import GasPriceEditor from './GasPriceEditor'; +import GasPriceSelector from './GasPriceSelector'; import IdentityIcon from './IdentityIcon'; import IdentityName from './IdentityName'; +import LanguageSelector from './LanguageSelector'; import Loading from './Loading'; import MethodDecoding from './MethodDecoding'; import Modal, { Busy as BusyStep, Completed as CompletedStep } from './Modal'; @@ -69,15 +71,16 @@ export { Form, FormWrap, GasPriceEditor, + GasPriceSelector, Input, InputAddress, InputAddressSelect, InputChip, InputInline, - Loading, - Select, IdentityIcon, IdentityName, + LanguageSelector, + Loading, MethodDecoding, Modal, BusyStep, @@ -87,6 +90,7 @@ export { ParityBackground, RadioButtons, ShortenedHash, + Select, SignerIcon, Tags, Tooltip, diff --git a/js/src/views/Application/TabBar/tabBar.js b/js/src/views/Application/TabBar/tabBar.js index 95db7a1f7..81be1d294 100644 --- a/js/src/views/Application/TabBar/tabBar.js +++ b/js/src/views/Application/TabBar/tabBar.js @@ -15,6 +15,7 @@ // along with Parity. If not, see . import React, { Component, PropTypes } from 'react'; +import { FormattedMessage } from 'react-intl'; import { connect } from 'react-redux'; import { Link } from 'react-router'; import { Toolbar, ToolbarGroup } from 'material-ui/Toolbar'; @@ -23,85 +24,57 @@ import { isEqual } from 'lodash'; import { Badge, Tooltip } from '~/ui'; +import imagesEthcoreBlock from '~/../assets/images/parity-logo-white-no-text.svg'; + import styles from './tabBar.css'; -import imagesEthcoreBlock from '../../../../assets/images/parity-logo-white-no-text.svg'; class Tab extends Component { static propTypes = { - view: PropTypes.object, children: PropTypes.node, - pendings: PropTypes.number + pendings: PropTypes.number, + view: PropTypes.object }; - shouldComponentUpdate (nextProps) { - return (nextProps.view.id === 'signer' && nextProps.pendings !== this.props.pendings); - } - render () { const { view, children } = this.props; - const label = this.getLabel(view); - return ( + label={ + view.id === 'signer' + ? this.renderSignerLabel(view.id) + : this.renderLabel(view.id) + }> { children } ); } - getLabel (view) { - const { label } = view; - - if (view.id === 'signer') { - return this.renderSignerLabel(label); - } - - if (view.id === 'status') { - return this.renderStatusLabel(label); - } - - return this.renderLabel(label); - } - - renderLabel (name, bubble) { + renderLabel (id, bubble) { return (
- { name } + { bubble }
); } - renderSignerLabel (label) { + renderSignerLabel (id) { const { pendings } = this.props; + let bubble; if (pendings) { - const bubble = ( + bubble = ( ); - - return this.renderLabel(label, bubble); } - return this.renderLabel(label); - } - - renderStatusLabel (label) { - // const { isTest, netChain } = this.props; - // const bubble = ( - // - // ); - - return this.renderLabel(label, null); + return this.renderLabel(id, bubble); } } @@ -111,28 +84,19 @@ class TabBar extends Component { }; static propTypes = { - views: PropTypes.array.isRequired, - pending: PropTypes.array, isTest: PropTypes.bool, - netChain: PropTypes.string + netChain: PropTypes.string, + pending: PropTypes.array, + views: PropTypes.array.isRequired }; static defaultProps = { pending: [] }; - shouldComponentUpdate (nextProps, nextState) { - const prevViews = this.props.views.map((v) => v.id).sort(); - const nextViews = nextProps.views.map((v) => v.id).sort(); - - return (nextProps.pending.length !== this.props.pending.length) || - (!isEqual(prevViews, nextViews)); - } - render () { return ( - + { this.renderLogo() } { this.renderTabs() } { this.renderLast() } @@ -167,21 +131,20 @@ class TabBar extends Component { .map((view, index) => { const body = (view.id === 'accounts') ? ( - + ) : null; return ( + className={ styles.tabLink }key={ view.id } + to={ view.route }> + view={ view }> { body } @@ -203,11 +166,10 @@ function mapStateToProps (initState) { .keys(views) .filter((id) => views[id].fixed || views[id].active); - let filteredViews = filteredViewIds - .map((id) => ({ - ...views[id], - id - })); + let filteredViews = filteredViewIds.map((id) => ({ + ...views[id], + id + })); return (state) => { const { views } = state.settings; @@ -221,11 +183,10 @@ function mapStateToProps (initState) { } filteredViewIds = viewIds; - filteredViews = viewIds - .map((id) => ({ - ...views[id], - id - })); + filteredViews = viewIds.map((id) => ({ + ...views[id], + id + })); return { views: filteredViews }; }; diff --git a/js/src/views/Application/application.css b/js/src/views/Application/application.css index 4ce748a30..e6f97f105 100644 --- a/js/src/views/Application/application.css +++ b/js/src/views/Application/application.css @@ -19,5 +19,8 @@ display: flex; flex-direction: column; min-height: 100vh; +} + +.content { padding-bottom: 1em; } diff --git a/js/src/views/Application/application.js b/js/src/views/Application/application.js index 1c86dcbd2..c536f10df 100644 --- a/js/src/views/Application/application.js +++ b/js/src/views/Application/application.js @@ -30,6 +30,8 @@ import Status from './Status'; import Store from './store'; import TabBar from './TabBar'; +import styles from './application.css'; + const inFrame = window.parent !== window && window.parent.frames.length !== 0; @observer @@ -79,7 +81,9 @@ class Application extends Component { netChain={ netChain } isTest={ isTest } pending={ pending } /> - { children } +
+ { children } +
{ blockNumber ? () : null } diff --git a/js/src/views/Dapps/AddDapps/AddDapps.js b/js/src/views/Dapps/AddDapps/AddDapps.js index 84e06b9dd..cc2b60552 100644 --- a/js/src/views/Dapps/AddDapps/AddDapps.js +++ b/js/src/views/Dapps/AddDapps/AddDapps.js @@ -15,6 +15,7 @@ // along with Parity. If not, see . import React, { Component, PropTypes } from 'react'; +import { FormattedMessage } from 'react-intl'; import { observer } from 'mobx-react'; import DoneIcon from 'material-ui/svg-icons/action/done'; import { List, ListItem } from 'material-ui/List'; @@ -39,22 +40,56 @@ export default class AddDapps extends Component { return ( + } actions={ [
diff --git a/js/src/views/Settings/Parity/parity.js b/js/src/views/Settings/Parity/parity.js index 5acf4bfb1..978ef296f 100644 --- a/js/src/views/Settings/Parity/parity.js +++ b/js/src/views/Settings/Parity/parity.js @@ -15,19 +15,13 @@ // along with Parity. If not, see . import React, { Component, PropTypes } from 'react'; +import { FormattedMessage } from 'react-intl'; import { MenuItem } from 'material-ui'; -import { Select, Container } from '~/ui'; +import { Select, Container, LanguageSelector } from '~/ui'; import layout from '../layout.css'; -const MODES = { - 'active': 'Parity continuously syncs the chain', - 'passive': 'Parity syncs initially, then sleeps and wakes regularly to resync', - 'dark': 'Parity syncs only when the RPC is active', - 'offline': 'Parity doesn\'t sync' -}; - export default class Parity extends Component { static contextTypes = { api: PropTypes.object.isRequired @@ -43,12 +37,19 @@ export default class Parity extends Component { render () { return ( - + + }>
-
Control the Parity node settings and mode of operation via this interface.
+
+ +
+ { this.renderModes() }
@@ -57,30 +58,53 @@ export default class Parity extends Component { } renderModes () { - const modes = Object - .keys(MODES) - .map((mode) => { - const description = MODES[mode]; - - return ( - - { description } - - ); - }); - const { mode } = this.state; + const renderItem = (mode, label) => { + return ( + + { label } + + ); + }; + return ( ); } diff --git a/js/src/views/Settings/Proxy/proxy.js b/js/src/views/Settings/Proxy/proxy.js index f4dcd97b1..dba95d458 100644 --- a/js/src/views/Settings/Proxy/proxy.js +++ b/js/src/views/Settings/Proxy/proxy.js @@ -15,6 +15,7 @@ // along with Parity. If not, see . import React, { Component, PropTypes } from 'react'; +import { FormattedMessage } from 'react-intl'; import { Container } from '~/ui'; @@ -31,16 +32,40 @@ export default class Proxy extends Component { const proxyurl = `${dappsUrl}/proxy/proxy.pac`; return ( - + + }>
-
The proxy setup allows you to access Parity and all associated decentralized applications via memorable addresses.
+
+ +
-
Instead of accessing Parity via the IP address and port, you will be able to access it via the .parity subdomain, by visiting http://home.parity/. To setup subdomain-based routing, you need to add the relevant proxy entries to your browser,
- -
To learn how to configure the proxy, instructions are provided for Windows, Max OS X or Ubuntu.
+
+ http://home.parity/ + } } /> +
+ +
+ , + macOSLink: , + ubuntuLink: + } } /> +
diff --git a/js/src/views/Settings/Views/defaults.js b/js/src/views/Settings/Views/defaults.js index 7fe6ccc80..bdd6ae29c 100644 --- a/js/src/views/Settings/Views/defaults.js +++ b/js/src/views/Settings/Views/defaults.js @@ -28,66 +28,52 @@ const defaultViews = { active: true, fixed: true, icon: , - label: 'Accounts', route: '/accounts', - value: 'account', - description: 'A list of all the accounts associated to and imported into this Parity instance. Send transactions, receive incoming values, manage your balances and fund your accounts.' + value: 'account' }, addresses: { active: true, icon: , - label: 'Addressbook', route: '/addresses', - value: 'address', - description: 'A list of all contacts and address book entries that is managed by this Parity instance. Watch accounts and have the details available at the click of a button when transacting.' + value: 'address' }, apps: { active: true, icon: , - label: 'Applications', route: '/apps', - value: 'app', - description: 'Distributed applications that interact with the underlying network. Add applications, manage you application portfolio and interact with application from around the network.' + value: 'app' }, contracts: { active: false, icon: , - label: 'Contracts', route: '/contracts', - value: 'contract', - description: 'Watch and interact with specific contracts that have been deployed on the network. This is a more technically-focused environment, specifically for advanced users that understand the inner working of certain contracts.' + value: 'contract' }, status: { active: false, icon: , - label: 'Status', route: '/status', - value: 'status', - description: 'See how the Parity node is performing in terms of connections to the network, logs from the actual running instance and details of mining (if enabled and configured).' + value: 'status' }, signer: { active: true, fixed: true, icon: , - label: 'Signer', route: '/signer', - value: 'signer', - description: 'The secure transaction management area of the application where you can approve any outgoing transactions made from the application as well as those placed into the queue by distributed applications.' + value: 'signer' }, settings: { active: true, fixed: true, icon: , - label: 'Settings', route: '/settings', - value: 'settings', - description: 'This view. Allows you to customize the application in term of options, operation and look and feel.' + value: 'settings' } }; diff --git a/js/src/views/Settings/Views/views.js b/js/src/views/Settings/Views/views.js index 5bd6154d8..85b811876 100644 --- a/js/src/views/Settings/Views/views.js +++ b/js/src/views/Settings/Views/views.js @@ -15,6 +15,7 @@ // along with Parity. If not, see . import React, { Component, PropTypes } from 'react'; +import { FormattedMessage } from 'react-intl'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; import { Checkbox } from 'material-ui'; @@ -34,16 +35,96 @@ class Views extends Component { render () { return ( - + + }>
-
Manage the available application views, using only the parts of the application that is applicable to you.
-
Are you an end-user? The defaults are setups for both beginner and advanced users alike.
-
Are you a developer? Add some features to manage contracts are interact with application deployments.
-
Are you a miner or run a large-scale node? Add the features to give you all the information needed to watch the node operation.
+
+ +
+
+ +
+
+ +
+
+ +
- { this.renderViews() } + { + this.renderView('accounts', + , + + ) + } + { + this.renderView('addresses', + , + + ) + } + { + this.renderView('apps', + , + + ) + } + { + this.renderView('contracts', + , + + ) + } + { + this.renderView('status', + , + + ) + } + { + this.renderView('signer', + , + + ) + } + { + this.renderView('settings', + , + + ) + }
@@ -51,37 +132,45 @@ class Views extends Component { } renderViews () { - const { settings, toggleView } = this.props; + const { settings } = this.props; return Object.keys(settings.views).map((id) => { - const toggle = () => toggleView(id); - const view = settings.views[id]; - const label = ( -
-
- { view.icon } -
-
- { view.label } -
-
- ); + const description = ; + const label = ; - return ( -
- -
- { view.description } -
-
- ); + this.renderView(id, label, description); }); } + + renderView = (id, label, description) => { + const { settings, toggleView } = this.props; + + const toggle = () => toggleView(id); + const view = settings.views[id]; + + return ( +
+ +
+ { view.icon } +
+
+ { label } +
+
+ } + onCheck={ toggle } + checked={ view.active } + value={ view.active } /> +
+ { description } +
+
+ ); + } } function mapStateToProps (state) { diff --git a/js/src/views/Settings/settings.js b/js/src/views/Settings/settings.js index bc1230b9c..dbf0043e4 100644 --- a/js/src/views/Settings/settings.js +++ b/js/src/views/Settings/settings.js @@ -15,6 +15,7 @@ // along with Parity. If not, see . import React, { Component, PropTypes } from 'react'; +import { FormattedMessage } from 'react-intl'; import { Tab, Tabs } from 'material-ui'; import ActionSettingsEthernet from 'material-ui/svg-icons/action/settings-ethernet'; import ImageBlurOn from 'material-ui/svg-icons/image/blur-on'; @@ -46,7 +47,11 @@ export default class Settings extends Component { return (
- + + }> { this.renderTab(hash, 'views', ) } { this.renderTab(hash, 'background', ) } @@ -64,12 +69,20 @@ export default class Settings extends Component { renderTab (hash, section, icon) { return ( { section }
} - onActive={ this.onActivate(section) } /> + key={ section } + label={ +
+ +
+ } + onActive={ this.onActivate(section) } + value={ section } /> ); } diff --git a/js/src/views/Signer/components/Account/AccountLink/AccountLink.css b/js/src/views/Signer/components/Account/AccountLink/accountLink.css similarity index 100% rename from js/src/views/Signer/components/Account/AccountLink/AccountLink.css rename to js/src/views/Signer/components/Account/AccountLink/accountLink.css diff --git a/js/src/views/Signer/components/Account/AccountLink/AccountLink.js b/js/src/views/Signer/components/Account/AccountLink/accountLink.js similarity index 97% rename from js/src/views/Signer/components/Account/AccountLink/AccountLink.js rename to js/src/views/Signer/components/Account/AccountLink/accountLink.js index 871baa5da..c4bc362c1 100644 --- a/js/src/views/Signer/components/Account/AccountLink/AccountLink.js +++ b/js/src/views/Signer/components/Account/AccountLink/accountLink.js @@ -17,7 +17,7 @@ import React, { Component, PropTypes } from 'react'; import { addressLink } from '~/3rdparty/etherscan/links'; -import styles from './AccountLink.css'; +import styles from './accountLink.css'; export default class AccountLink extends Component { static propTypes = { diff --git a/js/src/views/Signer/components/Account/AccountLink/index.js b/js/src/views/Signer/components/Account/AccountLink/index.js index 7ca280a6a..7828ec929 100644 --- a/js/src/views/Signer/components/Account/AccountLink/index.js +++ b/js/src/views/Signer/components/Account/AccountLink/index.js @@ -14,4 +14,4 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . -export default from './AccountLink'; +export default from './accountLink'; diff --git a/js/src/views/Signer/components/Account/Account.css b/js/src/views/Signer/components/Account/account.css similarity index 100% rename from js/src/views/Signer/components/Account/Account.css rename to js/src/views/Signer/components/Account/account.css diff --git a/js/src/views/Signer/components/Account/Account.js b/js/src/views/Signer/components/Account/account.js similarity index 98% rename from js/src/views/Signer/components/Account/Account.js rename to js/src/views/Signer/components/Account/account.js index 787fadc17..dbe950b75 100644 --- a/js/src/views/Signer/components/Account/Account.js +++ b/js/src/views/Signer/components/Account/account.js @@ -19,7 +19,7 @@ import React, { Component, PropTypes } from 'react'; import { IdentityIcon, IdentityName } from '~/ui'; import AccountLink from './AccountLink'; -import styles from './Account.css'; +import styles from './account.css'; export default class Account extends Component { static propTypes = { diff --git a/js/src/views/Signer/components/Account/index.js b/js/src/views/Signer/components/Account/index.js index 1bfc3515f..75c7cc401 100644 --- a/js/src/views/Signer/components/Account/index.js +++ b/js/src/views/Signer/components/Account/index.js @@ -14,4 +14,4 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . -export default from './Account'; +export default from './account'; diff --git a/js/src/views/Signer/components/RequestPending/requestPending.js b/js/src/views/Signer/components/RequestPending/requestPending.js index fc5b04d96..84783a614 100644 --- a/js/src/views/Signer/components/RequestPending/requestPending.js +++ b/js/src/views/Signer/components/RequestPending/requestPending.js @@ -21,21 +21,26 @@ import SignRequest from '../SignRequest'; export default class RequestPending extends Component { static propTypes = { + className: PropTypes.string, + date: PropTypes.instanceOf(Date).isRequired, + gasLimit: PropTypes.object.isRequired, id: PropTypes.object.isRequired, + isSending: PropTypes.bool.isRequired, + isTest: PropTypes.bool.isRequired, onConfirm: PropTypes.func.isRequired, onReject: PropTypes.func.isRequired, - isSending: PropTypes.bool.isRequired, - date: PropTypes.instanceOf(Date).isRequired, payload: PropTypes.oneOfType([ - PropTypes.shape({ signTransaction: PropTypes.object.isRequired }), PropTypes.shape({ sendTransaction: PropTypes.object.isRequired }), - PropTypes.shape({ sign: PropTypes.object.isRequired }) + PropTypes.shape({ sign: PropTypes.object.isRequired }), + PropTypes.shape({ signTransaction: PropTypes.object.isRequired }) ]).isRequired, - className: PropTypes.string, - isTest: PropTypes.bool.isRequired, store: PropTypes.object.isRequired }; + static defaultProps = { + isSending: false + }; + onConfirm = data => { const { onConfirm, payload } = this.props; @@ -44,24 +49,23 @@ export default class RequestPending extends Component { }; render () { - const { payload, id, className, isSending, date, onReject, isTest, store } = this.props; + const { className, date, gasLimit, id, isSending, isTest, onReject, payload, store } = this.props; if (payload.sign) { const { sign } = payload; return ( + store={ store } /> ); } @@ -70,19 +74,19 @@ export default class RequestPending extends Component { return ( + transaction={ transaction } /> ); } - // Unknown payload + console.error('RequestPending: Unknown payload', payload); return null; } } diff --git a/js/src/views/Signer/components/SignRequest/index.js b/js/src/views/Signer/components/SignRequest/index.js index b9c2cc8cd..8485f6109 100644 --- a/js/src/views/Signer/components/SignRequest/index.js +++ b/js/src/views/Signer/components/SignRequest/index.js @@ -14,4 +14,4 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . -export default from './SignRequest'; +export default from './signRequest'; diff --git a/js/src/views/Signer/components/SignRequest/SignRequest.css b/js/src/views/Signer/components/SignRequest/signRequest.css similarity index 100% rename from js/src/views/Signer/components/SignRequest/SignRequest.css rename to js/src/views/Signer/components/SignRequest/signRequest.css diff --git a/js/src/views/Signer/components/SignRequest/SignRequest.js b/js/src/views/Signer/components/SignRequest/signRequest.js similarity index 97% rename from js/src/views/Signer/components/SignRequest/SignRequest.js rename to js/src/views/Signer/components/SignRequest/signRequest.js index 9363e11ee..783e5b9f6 100644 --- a/js/src/views/Signer/components/SignRequest/SignRequest.js +++ b/js/src/views/Signer/components/SignRequest/signRequest.js @@ -21,7 +21,7 @@ import Account from '../Account'; import TransactionPendingForm from '../TransactionPendingForm'; import TxHashLink from '../TxHashLink'; -import styles from './SignRequest.css'; +import styles from './signRequest.css'; @observer export default class SignRequest extends Component { @@ -49,7 +49,7 @@ export default class SignRequest extends Component { const { className } = this.props; return ( -
+
{ this.renderDetails() } { this.renderActions() }
diff --git a/js/src/views/Signer/components/SignRequest/signRequest.spec.js b/js/src/views/Signer/components/SignRequest/signRequest.spec.js new file mode 100644 index 000000000..53099bb9c --- /dev/null +++ b/js/src/views/Signer/components/SignRequest/signRequest.spec.js @@ -0,0 +1,34 @@ +// Copyright 2015, 2016 Ethcore (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +import { shallow } from 'enzyme'; +import React from 'react'; +import sinon from 'sinon'; + +import SignRequest from './'; + +const store = { + balances: {}, + fetchBalance: sinon.stub() +}; + +describe('views/Signer/components/SignRequest', () => { + it('renders', () => { + expect(shallow( + , + )).to.be.ok; + }); +}); diff --git a/js/src/views/Signer/components/TransactionMainDetails/index.js b/js/src/views/Signer/components/TransactionMainDetails/index.js index ad9b01a92..2c4ec0e77 100644 --- a/js/src/views/Signer/components/TransactionMainDetails/index.js +++ b/js/src/views/Signer/components/TransactionMainDetails/index.js @@ -14,4 +14,4 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . -export default from './TransactionMainDetails'; +export default from './transactionMainDetails'; diff --git a/js/src/views/Signer/components/TransactionMainDetails/TransactionMainDetails.css b/js/src/views/Signer/components/TransactionMainDetails/transactionMainDetails.css similarity index 95% rename from js/src/views/Signer/components/TransactionMainDetails/TransactionMainDetails.css rename to js/src/views/Signer/components/TransactionMainDetails/transactionMainDetails.css index 92ed968d1..2a42789ea 100644 --- a/js/src/views/Signer/components/TransactionMainDetails/TransactionMainDetails.css +++ b/js/src/views/Signer/components/TransactionMainDetails/transactionMainDetails.css @@ -17,20 +17,25 @@ @import '../../_layout.css'; -.transaction { - flex: 1; - overflow: auto; -} - -.transaction > * { - display: inline-block; -} - .account { text-align: center; } +.contractIcon { + background: #eee; + width: 50px !important; + height: 50px !important; + box-sizing: border-box; + border-radius: 50%; + padding: 13px; +} + +.editButtonRow { + text-align: right; +} + .from { + display: inline-block; width: 40%; vertical-align: top; @@ -47,6 +52,7 @@ } .method { + display: inline-block; width: 60%; vertical-align: top; line-height: 1em; @@ -66,11 +72,7 @@ opacity: .5; } -.contractIcon { - background: #eee; - width: 50px !important; - height: 50px !important; - box-sizing: border-box; - border-radius: 50%; - padding: 13px; +.transaction { + flex: 1; + overflow: auto; } diff --git a/js/src/views/Signer/components/TransactionMainDetails/TransactionMainDetails.js b/js/src/views/Signer/components/TransactionMainDetails/transactionMainDetails.js similarity index 65% rename from js/src/views/Signer/components/TransactionMainDetails/TransactionMainDetails.js rename to js/src/views/Signer/components/TransactionMainDetails/transactionMainDetails.js index fa52730f8..319c5ae98 100644 --- a/js/src/views/Signer/components/TransactionMainDetails/TransactionMainDetails.js +++ b/js/src/views/Signer/components/TransactionMainDetails/transactionMainDetails.js @@ -14,52 +14,43 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . +import MapsLocalGasStation from 'material-ui/svg-icons/maps/local-gas-station'; import React, { Component, PropTypes } from 'react'; - import ReactTooltip from 'react-tooltip'; -import { MethodDecoding } from '~/ui'; +import { Button, MethodDecoding } from '~/ui'; import * as tUtil from '../util/transaction'; import Account from '../Account'; -import styles from './TransactionMainDetails.css'; +import styles from './transactionMainDetails.css'; export default class TransactionMainDetails extends Component { static propTypes = { - id: PropTypes.object.isRequired, + children: PropTypes.node, from: PropTypes.string.isRequired, - fromBalance: PropTypes.object, // eth BigNumber, not required since it might take time to fetch - value: PropTypes.object.isRequired, // wei hex - totalValue: PropTypes.object.isRequired, // wei BigNumber + fromBalance: PropTypes.object, + gasStore: PropTypes.object, + id: PropTypes.object.isRequired, isTest: PropTypes.bool.isRequired, + totalValue: PropTypes.object.isRequired, transaction: PropTypes.object.isRequired, - children: PropTypes.node + value: PropTypes.object.isRequired }; componentWillMount () { - const { value, totalValue } = this.props; + const { totalValue, value } = this.props; this.updateDisplayValues(value, totalValue); } componentWillReceiveProps (nextProps) { - const { value, totalValue } = nextProps; + const { totalValue, value } = nextProps; this.updateDisplayValues(value, totalValue); } - updateDisplayValues (value, totalValue) { - this.setState({ - feeEth: tUtil.calcFeeInEth(totalValue, value), - valueDisplay: tUtil.getValueDisplay(value), - valueDisplayWei: tUtil.getValueDisplayWei(value), - totalValueDisplay: tUtil.getTotalValueDisplay(totalValue), - totalValueDisplayWei: tUtil.getTotalValueDisplayWei(totalValue) - }); - } - render () { - const { children, from, fromBalance, transaction, isTest } = this.props; + const { children, from, fromBalance, gasStore, isTest, transaction } = this.props; return (
@@ -74,29 +65,74 @@ export default class TransactionMainDetails extends Component {
+ historic={ false } + transaction={ + gasStore + ? gasStore.overrideTransaction(transaction) + : transaction + } /> + { this.renderEditGas() }
{ children }
); } + renderEditGas () { + const { gasStore } = this.props; + + if (!gasStore) { + return null; + } + + return ( +
+
+ ); + } + + renderTotalValue () { + const { id } = this.props; + const { feeEth, totalValueDisplay, totalValueDisplayWei } = this.state; + const labelId = `totalValue${id}`; + + return ( +
+
+ { totalValueDisplay } ETH +
+ + The value of the transaction including the mining fee is { totalValueDisplayWei } WEI.
+ (This includes a mining fee of { feeEth } ETH) +
+
+ ); + } + renderValue () { const { id } = this.props; const { valueDisplay, valueDisplayWei } = this.state; + const labelId = `value${id}`; return (
+ data-for={ labelId } + data-tip> { valueDisplay } ETH
- + The value of the transaction.
{ valueDisplayWei } WEI
@@ -104,25 +140,17 @@ export default class TransactionMainDetails extends Component { ); } - renderTotalValue () { - const { id } = this.props; - const { totalValueDisplay, totalValueDisplayWei, feeEth } = this.state; + updateDisplayValues (value, totalValue) { + this.setState({ + feeEth: tUtil.calcFeeInEth(totalValue, value), + totalValueDisplay: tUtil.getTotalValueDisplay(totalValue), + totalValueDisplayWei: tUtil.getTotalValueDisplayWei(totalValue), + valueDisplay: tUtil.getValueDisplay(value), + valueDisplayWei: tUtil.getValueDisplayWei(value) + }); + } - return ( -
-
- { totalValueDisplay } ETH -
- - The value of the transaction including the mining fee is { totalValueDisplayWei } WEI.
- (This includes a mining fee of { feeEth } ETH) -
-
- ); + toggleGasEditor = () => { + this.props.gasStore.setEditing(true); } } diff --git a/js/src/views/Signer/components/TransactionPending/index.js b/js/src/views/Signer/components/TransactionPending/index.js index 5a945e1ea..3524dffb4 100644 --- a/js/src/views/Signer/components/TransactionPending/index.js +++ b/js/src/views/Signer/components/TransactionPending/index.js @@ -14,4 +14,4 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . -export default from './TransactionPending'; +export default from './transactionPending'; diff --git a/js/src/views/Signer/components/TransactionPending/TransactionPending.css b/js/src/views/Signer/components/TransactionPending/transactionPending.css similarity index 92% rename from js/src/views/Signer/components/TransactionPending/TransactionPending.css rename to js/src/views/Signer/components/TransactionPending/transactionPending.css index 877066e57..dbda27135 100644 --- a/js/src/views/Signer/components/TransactionPending/TransactionPending.css +++ b/js/src/views/Signer/components/TransactionPending/transactionPending.css @@ -19,13 +19,9 @@ .container { display: flex; - padding: 1em 0 1em; + padding: 1.5em 1em 1.5em 0; & > * { vertical-align: middle; } } - -.container+.container { - padding-top: 2em; -} diff --git a/js/src/views/Signer/components/TransactionPending/TransactionPending.js b/js/src/views/Signer/components/TransactionPending/transactionPending.js similarity index 57% rename from js/src/views/Signer/components/TransactionPending/TransactionPending.js rename to js/src/views/Signer/components/TransactionPending/transactionPending.js index 7d3c04744..dc07de99a 100644 --- a/js/src/views/Signer/components/TransactionPending/TransactionPending.js +++ b/js/src/views/Signer/components/TransactionPending/transactionPending.js @@ -17,89 +17,130 @@ import React, { Component, PropTypes } from 'react'; import { observer } from 'mobx-react'; +import { Button, GasPriceEditor } from '~/ui'; + import TransactionMainDetails from '../TransactionMainDetails'; import TransactionPendingForm from '../TransactionPendingForm'; -import styles from './TransactionPending.css'; +import styles from './transactionPending.css'; import * as tUtil from '../util/transaction'; @observer export default class TransactionPending extends Component { + static contextTypes = { + api: PropTypes.object.isRequired + }; + static propTypes = { - id: PropTypes.object.isRequired, - transaction: PropTypes.shape({ - from: PropTypes.string.isRequired, - value: PropTypes.object.isRequired, // wei hex - gasPrice: PropTypes.object.isRequired, // wei hex - gas: PropTypes.object.isRequired, // hex - data: PropTypes.string, // hex - to: PropTypes.string // undefined if it's a contract - }).isRequired, + className: PropTypes.string, date: PropTypes.instanceOf(Date).isRequired, + gasLimit: PropTypes.object, + id: PropTypes.object.isRequired, + isSending: PropTypes.bool.isRequired, + isTest: PropTypes.bool.isRequired, nonce: PropTypes.number, onConfirm: PropTypes.func.isRequired, onReject: PropTypes.func.isRequired, - isSending: PropTypes.bool.isRequired, - className: PropTypes.string, - isTest: PropTypes.bool.isRequired, - store: PropTypes.object.isRequired + store: PropTypes.object.isRequired, + transaction: PropTypes.shape({ + data: PropTypes.string, + from: PropTypes.string.isRequired, + gas: PropTypes.object.isRequired, + gasPrice: PropTypes.object.isRequired, + to: PropTypes.string, + value: PropTypes.object.isRequired + }).isRequired }; - static defaultProps = { - isSending: false - }; + gasStore = new GasPriceEditor.Store(this.context.api, { + gas: this.props.transaction.gas.toFixed(), + gasLimit: this.props.gasLimit, + gasPrice: this.props.transaction.gasPrice.toFixed() + }); componentWillMount () { - const { transaction, store } = this.props; - const { gas, gasPrice, value, from, to } = transaction; + const { store, transaction } = this.props; + const { from, gas, gasPrice, to, value } = transaction; const fee = tUtil.getFee(gas, gasPrice); // BigNumber object - const totalValue = tUtil.getTotalValue(fee, value); const gasPriceEthmDisplay = tUtil.getEthmFromWeiDisplay(gasPrice); const gasToDisplay = tUtil.getGasDisplay(gas); + const totalValue = tUtil.getTotalValue(fee, value); this.setState({ gasPriceEthmDisplay, totalValue, gasToDisplay }); + this.gasStore.setEthValue(value); store.fetchBalances([from, to]); } render () { - const { className, id, transaction, store, isTest } = this.props; - const { from, value } = transaction; + return this.gasStore.isEditing + ? this.renderGasEditor() + : this.renderTransaction(); + } + + renderTransaction () { + const { className, id, isSending, isTest, store, transaction } = this.props; const { totalValue } = this.state; + const { from, value } = transaction; const fromBalance = store.balances[from]; return ( -
+
+ value={ value } /> + onReject={ this.onReject } />
); } - onConfirm = data => { - const { id, transaction } = this.props; - const { gasPrice } = transaction; - const { password, wallet } = data; + renderGasEditor () { + const { className } = this.props; - this.props.onConfirm({ id, password, wallet, gasPrice }); + return ( +
+ +
+ ); + } + + onConfirm = (data) => { + const { id, transaction } = this.props; + const { password, wallet } = data; + const { gas, gasPrice } = this.gasStore.overrideTransaction(transaction); + + this.props.onConfirm({ + gas, + gasPrice, + id, + password, + wallet + }); } onReject = () => { this.props.onReject(this.props.id); } + + toggleGasEditor = () => { + this.gasStore.setEditing(false); + } } diff --git a/js/src/views/Signer/components/TransactionPendingForm/TransactionPendingFormConfirm/index.js b/js/src/views/Signer/components/TransactionPendingForm/TransactionPendingFormConfirm/index.js index d12b85006..733e1ebc0 100644 --- a/js/src/views/Signer/components/TransactionPendingForm/TransactionPendingFormConfirm/index.js +++ b/js/src/views/Signer/components/TransactionPendingForm/TransactionPendingFormConfirm/index.js @@ -14,4 +14,4 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . -export default from './TransactionPendingFormConfirm'; +export default from './transactionPendingFormConfirm'; diff --git a/js/src/views/Signer/components/TransactionPendingForm/TransactionPendingFormConfirm/TransactionPendingFormConfirm.css b/js/src/views/Signer/components/TransactionPendingForm/TransactionPendingFormConfirm/transactionPendingFormConfirm.css similarity index 100% rename from js/src/views/Signer/components/TransactionPendingForm/TransactionPendingFormConfirm/TransactionPendingFormConfirm.css rename to js/src/views/Signer/components/TransactionPendingForm/TransactionPendingFormConfirm/transactionPendingFormConfirm.css diff --git a/js/src/views/Signer/components/TransactionPendingForm/TransactionPendingFormConfirm/TransactionPendingFormConfirm.js b/js/src/views/Signer/components/TransactionPendingForm/TransactionPendingFormConfirm/transactionPendingFormConfirm.js similarity index 71% rename from js/src/views/Signer/components/TransactionPendingForm/TransactionPendingFormConfirm/TransactionPendingFormConfirm.js rename to js/src/views/Signer/components/TransactionPendingForm/TransactionPendingFormConfirm/transactionPendingFormConfirm.js index 0b24925ff..360125d9f 100644 --- a/js/src/views/Signer/components/TransactionPendingForm/TransactionPendingFormConfirm/TransactionPendingFormConfirm.js +++ b/js/src/views/Signer/components/TransactionPendingForm/TransactionPendingFormConfirm/transactionPendingFormConfirm.js @@ -22,7 +22,7 @@ import ReactTooltip from 'react-tooltip'; import { Form, Input, IdentityIcon } from '~/ui'; -import styles from './TransactionPendingFormConfirm.css'; +import styles from './transactionPendingFormConfirm.css'; class TransactionPendingFormConfirm extends Component { static propTypes = { @@ -35,14 +35,14 @@ class TransactionPendingFormConfirm extends Component { id = Math.random(); // for tooltip state = { - walletError: null, + password: '', wallet: null, - password: '' + walletError: null } render () { const { accounts, address, isSending } = this.props; - const { password, walletError, wallet } = this.state; + const { password, wallet, walletError } = this.state; const account = accounts[address] || {}; const isExternal = !account.uuid; @@ -51,37 +51,54 @@ class TransactionPendingFormConfirm extends Component { : null; const isWalletOk = !isExternal || (walletError === null && wallet !== null); - const keyInput = isExternal ? this.renderKeyInput() : null; + const keyInput = isExternal + ? this.renderKeyInput() + : null; return (
{ keyInput }
{ passwordHint }
+ data-for={ `transactionConfirmForm${this.id}` } + data-place='bottom' + data-tip> } - label={ isSending ? 'Confirming...' : 'Confirm Transaction' } - /> + fullWidth + icon={ + + } + label={ + isSending + ? 'Confirming...' + : 'Confirm Transaction' + } + onTouchTap={ this.onConfirm } + primary />
{ this.renderTooltip() }
@@ -95,11 +112,10 @@ class TransactionPendingFormConfirm extends Component { return ( + onChange={ this.onKeySelect } + type='file' /> ); } @@ -109,34 +125,37 @@ class TransactionPendingFormConfirm extends Component { } return ( - + Please provide a password for this account ); } - onKeySelect = evt => { + onKeySelect = (event) => { const fileReader = new FileReader(); - fileReader.onload = e => { + + fileReader.onload = (e) => { try { const wallet = JSON.parse(e.target.result); + this.setState({ - walletError: null, - wallet: wallet + wallet, + walletError: null }); - } catch (e) { + } catch (error) { this.setState({ - walletError: 'Given wallet file is invalid.', - wallet: null + wallet: null, + walletError: 'Given wallet file is invalid.' }); } }; - fileReader.readAsText(evt.target.files[0]); + fileReader.readAsText(event.target.files[0]); } - onModifyPassword = evt => { - const password = evt.target.value; + onModifyPassword = (event) => { + const password = event.target.value; + this.setState({ password }); @@ -150,8 +169,8 @@ class TransactionPendingFormConfirm extends Component { }); } - onKeyDown = evt => { - if (evt.which !== 13) { + onKeyDown = (event) => { + if (event.which !== 13) { return; } diff --git a/js/src/views/Signer/components/TransactionPendingForm/TransactionPendingFormReject/index.js b/js/src/views/Signer/components/TransactionPendingForm/TransactionPendingFormReject/index.js index 15d219f5c..82c9aa359 100644 --- a/js/src/views/Signer/components/TransactionPendingForm/TransactionPendingFormReject/index.js +++ b/js/src/views/Signer/components/TransactionPendingForm/TransactionPendingFormReject/index.js @@ -14,4 +14,4 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . -export default from './TransactionPendingFormReject'; +export default from './transactionPendingFormReject'; diff --git a/js/src/views/Signer/components/TransactionPendingForm/TransactionPendingFormReject/TransactionPendingFormReject.css b/js/src/views/Signer/components/TransactionPendingForm/TransactionPendingFormReject/transactionPendingFormReject.css similarity index 100% rename from js/src/views/Signer/components/TransactionPendingForm/TransactionPendingFormReject/TransactionPendingFormReject.css rename to js/src/views/Signer/components/TransactionPendingForm/TransactionPendingFormReject/transactionPendingFormReject.css diff --git a/js/src/views/Signer/components/TransactionPendingForm/TransactionPendingFormReject/TransactionPendingFormReject.js b/js/src/views/Signer/components/TransactionPendingForm/TransactionPendingFormReject/transactionPendingFormReject.js similarity index 96% rename from js/src/views/Signer/components/TransactionPendingForm/TransactionPendingFormReject/TransactionPendingFormReject.js rename to js/src/views/Signer/components/TransactionPendingForm/TransactionPendingFormReject/transactionPendingFormReject.js index 438da642d..def8aae08 100644 --- a/js/src/views/Signer/components/TransactionPendingForm/TransactionPendingFormReject/TransactionPendingFormReject.js +++ b/js/src/views/Signer/components/TransactionPendingForm/TransactionPendingFormReject/transactionPendingFormReject.js @@ -18,7 +18,7 @@ import React, { Component, PropTypes } from 'react'; import RaisedButton from 'material-ui/RaisedButton'; -import styles from './TransactionPendingFormReject.css'; +import styles from './transactionPendingFormReject.css'; export default class TransactionPendingFormReject extends Component { diff --git a/js/src/views/Signer/components/TransactionPendingForm/index.js b/js/src/views/Signer/components/TransactionPendingForm/index.js index 54d777edd..2b58e2148 100644 --- a/js/src/views/Signer/components/TransactionPendingForm/index.js +++ b/js/src/views/Signer/components/TransactionPendingForm/index.js @@ -14,4 +14,4 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . -export default from './TransactionPendingForm'; +export default from './transactionPendingForm'; diff --git a/js/src/views/Signer/components/TransactionPendingForm/TransactionPendingForm.css b/js/src/views/Signer/components/TransactionPendingForm/transactionPendingForm.css similarity index 100% rename from js/src/views/Signer/components/TransactionPendingForm/TransactionPendingForm.css rename to js/src/views/Signer/components/TransactionPendingForm/transactionPendingForm.css diff --git a/js/src/views/Signer/components/TransactionPendingForm/TransactionPendingForm.js b/js/src/views/Signer/components/TransactionPendingForm/transactionPendingForm.js similarity index 88% rename from js/src/views/Signer/components/TransactionPendingForm/TransactionPendingForm.js rename to js/src/views/Signer/components/TransactionPendingForm/transactionPendingForm.js index 99c24a591..f6e92761f 100644 --- a/js/src/views/Signer/components/TransactionPendingForm/TransactionPendingForm.js +++ b/js/src/views/Signer/components/TransactionPendingForm/transactionPendingForm.js @@ -20,10 +20,9 @@ import BackIcon from 'material-ui/svg-icons/navigation/arrow-back'; import TransactionPendingFormConfirm from './TransactionPendingFormConfirm'; import TransactionPendingFormReject from './TransactionPendingFormReject'; -import styles from './TransactionPendingForm.css'; +import styles from './transactionPendingForm.css'; export default class TransactionPendingForm extends Component { - static propTypes = { address: PropTypes.string.isRequired, isSending: PropTypes.bool.isRequired, @@ -60,8 +59,8 @@ export default class TransactionPendingForm extends Component { return ( + isSending={ isSending } + onConfirm={ onConfirm } /> ); } @@ -72,14 +71,13 @@ export default class TransactionPendingForm extends Component { if (!isRejectOpen) { html = reject transaction; } else { - html = I've changed my mind; + html = { "I've changed my mind" }; } return ( + onClick={ this.onToggleReject }> { html } ); @@ -87,7 +85,9 @@ export default class TransactionPendingForm extends Component { onToggleReject = () => { const { isRejectOpen } = this.state; - this.setState({ isRejectOpen: !isRejectOpen }); - } + this.setState({ + isRejectOpen: !isRejectOpen + }); + } } diff --git a/js/src/views/Signer/components/TxHashLink/index.js b/js/src/views/Signer/components/TxHashLink/index.js index 5f1b8391b..43a7a7c55 100644 --- a/js/src/views/Signer/components/TxHashLink/index.js +++ b/js/src/views/Signer/components/TxHashLink/index.js @@ -14,4 +14,4 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . -export default from './TxHashLink'; +export default from './txHashLink'; diff --git a/js/src/views/Signer/components/TxHashLink/TxHashLink.js b/js/src/views/Signer/components/TxHashLink/txHashLink.js similarity index 84% rename from js/src/views/Signer/components/TxHashLink/TxHashLink.js rename to js/src/views/Signer/components/TxHashLink/txHashLink.js index 17ccb4f9b..be92828c1 100644 --- a/js/src/views/Signer/components/TxHashLink/TxHashLink.js +++ b/js/src/views/Signer/components/TxHashLink/txHashLink.js @@ -19,22 +19,21 @@ import React, { Component, PropTypes } from 'react'; import { txLink } from '~/3rdparty/etherscan/links'; export default class TxHashLink extends Component { - static propTypes = { - txHash: PropTypes.string.isRequired, - isTest: PropTypes.bool.isRequired, children: PropTypes.node, - className: PropTypes.string + className: PropTypes.string, + isTest: PropTypes.bool.isRequired, + txHash: PropTypes.string.isRequired } render () { - const { children, txHash, className, isTest } = this.props; + const { children, className, isTest, txHash } = this.props; return ( + target='_blank'> { children || txHash } ); diff --git a/js/src/views/Signer/containers/Embedded/embedded.css b/js/src/views/Signer/containers/Embedded/embedded.css index ffc6a209f..fcd49a851 100644 --- a/js/src/views/Signer/containers/Embedded/embedded.css +++ b/js/src/views/Signer/containers/Embedded/embedded.css @@ -17,16 +17,22 @@ @import '../../_layout.css'; -.signer { - box-sizing: border-box; - padding: 0; - width: $embedWidth; +.info { + padding: 1em 0; } .none { color: #aaa; } -.info { - padding: 1em 0; +.request { + &:nth-child(even) { + background: rgba(255, 255, 255, 0.04); + } +} + +.signer { + box-sizing: border-box; + padding: 0; + width: $embedWidth; } diff --git a/js/src/views/Signer/containers/Embedded/embedded.js b/js/src/views/Signer/containers/Embedded/embedded.js index 28e323127..3fa450473 100644 --- a/js/src/views/Signer/containers/Embedded/embedded.js +++ b/js/src/views/Signer/containers/Embedded/embedded.js @@ -33,15 +33,16 @@ class Embedded extends Component { }; static propTypes = { - signer: PropTypes.shape({ - pending: PropTypes.array.isRequired, - finished: PropTypes.array.isRequired - }).isRequired, actions: PropTypes.shape({ startConfirmRequest: PropTypes.func.isRequired, startRejectRequest: PropTypes.func.isRequired }).isRequired, - isTest: PropTypes.bool.isRequired + gasLimit: PropTypes.object.isRequired, + isTest: PropTypes.bool.isRequired, + signer: PropTypes.shape({ + finished: PropTypes.array.isRequired, + pending: PropTypes.array.isRequired + }).isRequired }; store = new Store(this.context.api); @@ -78,20 +79,21 @@ class Embedded extends Component { } renderPending = (data) => { - const { actions, isTest } = this.props; - const { payload, id, isSending, date } = data; + const { actions, gasLimit, isTest } = this.props; + const { date, id, isSending, payload } = data; return ( ); @@ -103,13 +105,14 @@ class Embedded extends Component { } function mapStateToProps (state) { - const { isTest } = state.nodeStatus; + const { gasLimit, isTest } = state.nodeStatus; const { actions, signer } = state; return { actions, - signer, - isTest + gasLimit, + isTest, + signer }; } diff --git a/js/src/views/Signer/containers/RequestsPage/index.js b/js/src/views/Signer/containers/RequestsPage/index.js index b561ddcf9..212fef231 100644 --- a/js/src/views/Signer/containers/RequestsPage/index.js +++ b/js/src/views/Signer/containers/RequestsPage/index.js @@ -14,4 +14,4 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . -export default from './RequestsPage'; +export default from './requestsPage'; diff --git a/js/src/views/Signer/containers/RequestsPage/RequestsPage.css b/js/src/views/Signer/containers/RequestsPage/requestsPage.css similarity index 90% rename from js/src/views/Signer/containers/RequestsPage/RequestsPage.css rename to js/src/views/Signer/containers/RequestsPage/requestsPage.css index 662c45817..f26f66e86 100644 --- a/js/src/views/Signer/containers/RequestsPage/RequestsPage.css +++ b/js/src/views/Signer/containers/RequestsPage/requestsPage.css @@ -18,3 +18,9 @@ .noRequestsMsg { color: #aaa; } + +.request { + &:nth-child(odd) { + background: rgba(255, 255, 255, 0.04); + } +} diff --git a/js/src/views/Signer/containers/RequestsPage/RequestsPage.js b/js/src/views/Signer/containers/RequestsPage/requestsPage.js similarity index 88% rename from js/src/views/Signer/containers/RequestsPage/RequestsPage.js rename to js/src/views/Signer/containers/RequestsPage/requestsPage.js index a8ad6e6ce..8f2da6208 100644 --- a/js/src/views/Signer/containers/RequestsPage/RequestsPage.js +++ b/js/src/views/Signer/containers/RequestsPage/requestsPage.js @@ -26,7 +26,7 @@ import { Container, Page, TxList } from '~/ui'; import RequestPending from '../../components/RequestPending'; -import styles from './RequestsPage.css'; +import styles from './requestsPage.css'; @observer class RequestsPage extends Component { @@ -35,15 +35,16 @@ class RequestsPage extends Component { }; static propTypes = { - signer: PropTypes.shape({ - pending: PropTypes.array.isRequired, - finished: PropTypes.array.isRequired - }).isRequired, actions: PropTypes.shape({ startConfirmRequest: PropTypes.func.isRequired, startRejectRequest: PropTypes.func.isRequired }).isRequired, - isTest: PropTypes.bool.isRequired + gasLimit: PropTypes.object.isRequired, + isTest: PropTypes.bool.isRequired, + signer: PropTypes.shape({ + pending: PropTypes.array.isRequired, + finished: PropTypes.array.isRequired + }).isRequired }; store = new Store(this.context.api, true); @@ -104,19 +105,21 @@ class RequestsPage extends Component { } renderPending = (data) => { - const { actions, isTest } = this.props; - const { payload, id, isSending, date } = data; + const { actions, gasLimit, isTest } = this.props; + const { date, id, isSending, payload } = data; return ( ); @@ -124,13 +127,14 @@ class RequestsPage extends Component { } function mapStateToProps (state) { - const { isTest } = state.nodeStatus; + const { gasLimit, isTest } = state.nodeStatus; const { actions, signer } = state; return { actions, - signer, - isTest + gasLimit, + isTest, + signer }; } diff --git a/js/webpack/app.js b/js/webpack/app.js index 1cb8c0eaf..cf38ec99c 100644 --- a/js/webpack/app.js +++ b/js/webpack/app.js @@ -17,6 +17,7 @@ const webpack = require('webpack'); const path = require('path'); +const ReactIntlAggregatePlugin = require('react-intl-aggregate-webpack-plugin'); const WebpackErrorNotificationPlugin = require('webpack-error-notification'); const CopyWebpackPlugin = require('copy-webpack-plugin'); const HtmlWebpackPlugin = require('html-webpack-plugin'); @@ -163,7 +164,15 @@ module.exports = { })); if (!isProd) { + const DEST_I18N = path.join(__dirname, '..', DEST, 'i18n'); + plugins.push( + new ReactIntlAggregatePlugin({ + messagesPattern: DEST_I18N + '/src/**/*.json', + aggregateOutputDir: DEST_I18N + '/i18n/', + aggregateFilename: 'en' + }), + new webpack.optimize.CommonsChunkPlugin({ filename: 'commons.[hash:10].js', name: 'commons',