Add nonce loading indicator & refresh button (#1021)

* Add InlineSpinner component

* Add 'what-input' module

* Add input style overrides

* Add new refresh icon

* Update footer styles

* Add nonce refresh button & loading indicator

* Center InlineSpinner

* Add types

* Lock version

* prettify package.json

* prettify package.json
This commit is contained in:
James Prado 2018-02-08 12:51:15 -05:00 committed by Daniel Ternyak
parent 7ac546acaf
commit 0ab226ca16
19 changed files with 236 additions and 138 deletions

View File

@ -17,6 +17,7 @@ import { Store } from 'redux';
import { pollOfflineStatus } from 'actions/config';
import { AppState } from 'reducers';
import { RouteNotFound } from 'components/RouteNotFound';
import 'what-input';
interface Props {
store: Store<AppState>;

View File

@ -146,8 +146,8 @@
margin: 0 0 $space-md 0;
}
li,
p {
> li,
> p {
font-size: 0.8rem;
margin: $space-sm 0;
}

View File

@ -0,0 +1,11 @@
@import 'common/sass/variables';
.gaslimit {
&-label-wrapper {
align-items: center;
margin-bottom: $space-xs;
> label {
margin-bottom: 0;
}
}
}

View File

@ -1,66 +1,34 @@
import React from 'react';
import { GasLimitFieldFactory } from './GasLimitFieldFactory';
import translate from 'translations';
import { CSSTransition } from 'react-transition-group';
import { Spinner } from 'components/ui';
import { gasLimitValidator } from 'libs/validators';
import { InlineSpinner } from 'components/ui/InlineSpinner';
import './GasLimitField.scss';
interface Props {
includeLabel: boolean;
onlyIncludeLoader: boolean;
customLabel?: string;
disabled?: boolean;
}
export const GaslimitLoading: React.SFC<{
gasEstimationPending: boolean;
onlyIncludeLoader?: boolean;
}> = ({ gasEstimationPending, onlyIncludeLoader }) => (
<CSSTransition in={gasEstimationPending} timeout={300} classNames="fade">
<div className={`Calculating-limit small ${gasEstimationPending ? 'active' : ''}`}>
{onlyIncludeLoader ? 'Calculating gas limit' : 'Calculating'}
<Spinner />
</div>
</CSSTransition>
);
export const GasLimitField: React.SFC<Props> = ({
includeLabel,
onlyIncludeLoader,
customLabel,
disabled
}) => (
<React.Fragment>
<GasLimitFieldFactory
withProps={({ gasLimit: { raw }, onChange, readOnly, gasEstimationPending }) => (
<React.Fragment>
<div className="flex-wrapper">
{includeLabel ? (
customLabel ? (
<label>{customLabel} </label>
) : (
<label>{translate('TRANS_gas')} </label>
)
) : null}
<div className="flex-spacer" />
<GaslimitLoading
gasEstimationPending={gasEstimationPending}
onlyIncludeLoader={onlyIncludeLoader}
/>
</div>
{onlyIncludeLoader ? null : (
<input
className={`form-control ${gasLimitValidator(raw) ? 'is-valid' : 'is-invalid'}`}
type="number"
placeholder="e.g. 21000"
readOnly={!!readOnly}
value={raw}
onChange={onChange}
disabled={disabled}
/>
)}
</React.Fragment>
)}
/>
</React.Fragment>
export const GasLimitField: React.SFC<Props> = ({ customLabel, disabled }) => (
<GasLimitFieldFactory
withProps={({ gasLimit: { raw }, onChange, readOnly, gasEstimationPending }) => (
<React.Fragment>
<div className="gaslimit-label-wrapper flex-wrapper">
{customLabel ? <label>{customLabel} </label> : <label>{translate('TRANS_gas')} </label>}
<div className="flex-spacer" />
<InlineSpinner active={gasEstimationPending} text="Calculating" />
</div>
<input
className={`form-control ${gasLimitValidator(raw) ? 'is-valid' : 'is-invalid'}`}
type="number"
placeholder="e.g. 21000"
readOnly={!!readOnly}
value={raw}
onChange={onChange}
disabled={disabled}
/>
</React.Fragment>
)}
/>
);

View File

@ -0,0 +1,36 @@
@import 'common/sass/variables';
.nonce {
&-label-wrapper {
align-items: center;
margin-bottom: $space-xs;
> label {
margin-bottom: 0;
}
}
&-input-wrapper {
position: relative;
}
&-refresh {
position: absolute;
right: 0;
top: 0;
border: none;
background: transparent;
padding: 0;
margin: 0 1rem;
height: 2.55rem;
opacity: 0.3;
transition: opacity 300ms;
> img {
height: 1.4rem;
}
&:hover {
opacity: 0.54;
}
&:active {
transition: opacity 120ms;
opacity: 1;
}
}
}

View File

@ -1,38 +1,72 @@
import React from 'react';
import { NonceFieldFactory } from 'components/NonceFieldFactory';
import Help from 'components/ui/Help';
import RefreshIcon from 'assets/images/refresh.svg';
import './NonceField.scss';
import { InlineSpinner } from 'components/ui/InlineSpinner';
import { connect } from 'react-redux';
import { getNonceRequested, TGetNonceRequested } from 'actions/transaction';
import { nonceRequestPending } from 'selectors/transaction';
import { AppState } from 'reducers';
interface Props {
interface OwnProps {
alwaysDisplay: boolean;
}
const nonceHelp = (
<Help
size={'x1'}
link={'https://myetherwallet.github.io/knowledge-base/transactions/what-is-nonce.html'}
/>
);
interface StateProps {
nonePending: boolean;
}
export const NonceField: React.SFC<Props> = ({ alwaysDisplay }) => (
<NonceFieldFactory
withProps={({ nonce: { raw, value }, onChange, readOnly, shouldDisplay }) => {
const content = (
<div>
<label>Nonce</label>
{nonceHelp}
interface DispatchProps {
requestNonce: TGetNonceRequested;
}
<input
className={`form-control ${!!value ? 'is-valid' : 'is-invalid'}`}
type="number"
placeholder="e.g. 7"
value={raw}
readOnly={readOnly}
onChange={onChange}
/>
</div>
);
type Props = OwnProps & DispatchProps & StateProps;
return alwaysDisplay || shouldDisplay ? content : null;
}}
/>
);
class NonceField extends React.Component<Props> {
public render() {
const { alwaysDisplay, requestNonce, nonePending } = this.props;
return (
<NonceFieldFactory
withProps={({ nonce: { raw, value }, onChange, readOnly, shouldDisplay }) => {
return alwaysDisplay || shouldDisplay ? (
<React.Fragment>
<div className="nonce-label-wrapper flex-wrapper">
<label className="nonce-label">Nonce</label>
<Help
size={'x1'}
link={
'https://myetherwallet.github.io/knowledge-base/transactions/what-is-nonce.html'
}
/>
<div className="flex-spacer" />
<InlineSpinner active={nonePending} text="Calculating" />
</div>
<div className="nonce-input-wrapper">
<input
className={`form-control nonce-input ${!!value ? 'is-valid' : 'is-invalid'}`}
type="number"
placeholder="e.g. 7"
value={raw}
readOnly={readOnly}
onChange={onChange}
/>
<button className="nonce-refresh" onClick={requestNonce}>
<img src={RefreshIcon} alt="refresh" />
</button>
</div>
</React.Fragment>
) : null;
}}
/>
);
}
}
const mapStateToProps = (state: AppState) => {
return {
nonePending: nonceRequestPending(state)
};
};
export default connect(mapStateToProps, { requestNonce: getNonceRequested })(NonceField);

View File

@ -12,7 +12,7 @@
.Calculating-limit {
color: rgba(51, 51, 51, 0.7);
display: flex;
align-items: baseline;
align-items: center;
font-weight: 400;
opacity: 0;
pointer-events: none;

View File

@ -40,10 +40,4 @@
width: 100%;
}
}
&-data {
}
&-fee-summary {
}
}

View File

@ -87,11 +87,7 @@ class AdvancedGas extends React.Component<Props, State> {
{gasLimitField && (
<div className="AdvancedGas-gas-limit">
<GasLimitField
includeLabel={true}
customLabel={translateRaw('OFFLINE_Step2_Label_4')}
onlyIncludeLoader={false}
/>
<GasLimitField customLabel={translateRaw('OFFLINE_Step2_Label_4')} />
</div>
)}
{nonceField && (

View File

@ -53,26 +53,3 @@
}
}
}
.fade {
&-enter,
&-exit {
transition: opacity 300ms;
}
&-enter {
opacity: 0;
&-active {
opacity: 1;
}
}
&-exit {
opacity: 1;
&-active {
opacity: 0;
}
}
}

View File

@ -5,15 +5,21 @@ import { gasPriceDefaults } from 'config';
import FeeSummary from './FeeSummary';
import './SimpleGas.scss';
import { AppState } from 'reducers';
import { getGasLimitEstimationTimedOut } from 'selectors/transaction';
import {
getGasLimitEstimationTimedOut,
getGasEstimationPending,
nonceRequestPending
} from 'selectors/transaction';
import { connect } from 'react-redux';
import { GasLimitField } from 'components/GasLimitField';
import { getIsWeb3Node } from 'selectors/config';
import { Wei, fromWei } from 'libs/units';
import { InlineSpinner } from 'components/ui/InlineSpinner';
const SliderWithTooltip = Slider.createSliderWithTooltip(Slider);
interface OwnProps {
gasPrice: AppState['transaction']['fields']['gasPrice'];
noncePending: boolean;
gasLimitPending: boolean;
inputGasPrice(rawGas: string);
setGasPrice(rawGas: string);
}
@ -31,16 +37,22 @@ class SimpleGas extends React.Component<Props> {
}
public render() {
const { gasPrice, gasLimitEstimationTimedOut, isWeb3Node } = this.props;
const {
gasPrice,
gasLimitEstimationTimedOut,
isWeb3Node,
noncePending,
gasLimitPending
} = this.props;
return (
<div className="SimpleGas row form-group">
<div className="SimpleGas-title">
<GasLimitField
includeLabel={true}
customLabel={translateRaw('Transaction Fee')}
onlyIncludeLoader={true}
/>
<div className="flex-wrapper">
<label>{translateRaw('Transaction Fee')} </label>
<div className="flex-spacer" />
<InlineSpinner active={noncePending || gasLimitPending} text="Calculating" />
</div>
</div>
{gasLimitEstimationTimedOut && (
@ -101,6 +113,8 @@ class SimpleGas extends React.Component<Props> {
}
export default connect((state: AppState) => ({
noncePending: nonceRequestPending(state),
gasLimitPending: getGasEstimationPending(state),
gasLimitEstimationTimedOut: getGasLimitEstimationTimedOut(state),
isWeb3Node: getIsWeb3Node(state)
}))(SimpleGas);

View File

@ -1,7 +1,6 @@
export * from './AddressField';
export * from './DataField';
export * from './GasLimitField';
export * from './NonceField';
export * from './AmountField';
export * from './SendEverything';
export * from './UnitDropDown';
@ -9,6 +8,7 @@ export * from './CurrentCustomMessage';
export * from './GenerateTransaction';
export * from './SendButton';
export * from './SigningStatus';
export { default as NonceField } from './NonceField';
export { default as Header } from './Header';
export { default as Footer } from './Footer';
export { default as BalanceSidebar } from './BalanceSidebar';

View File

@ -0,0 +1,24 @@
.inline-spinner {
&--fade {
&-enter,
&-exit {
transition: opacity 300ms;
}
&-enter {
opacity: 0;
&-active {
opacity: 1;
}
}
&-exit {
opacity: 1;
&-active {
opacity: 0;
}
}
}
}

View File

@ -0,0 +1,17 @@
import React from 'react';
import { CSSTransition } from 'react-transition-group';
import { Spinner } from 'components/ui';
import './InlineSpinner.scss';
export const InlineSpinner: React.SFC<{
active: boolean;
text?: string;
}> = ({ active, text }) => (
<CSSTransition in={active} timeout={300} classNames="inline-spinner--fade">
{/* TODO: when react-transition-group v2.3 releases, use '-done' classes instead of conditional 'active' class https://github.com/reactjs/react-transition-group/issues/274 */}
<div className={`Calculating-limit small ${active ? 'active' : ''}`}>
{text}
<Spinner />
</div>
</CSSTransition>
);

View File

@ -35,3 +35,7 @@
@import './styles/tab';
@import './styles/flexbox';
@import './fonts';
[data-whatintent='mouse'] *:focus {
outline: none;
}

View File

@ -9,6 +9,7 @@
@import './overrides/input-groups';
@import './overrides/type';
@import './overrides/tables';
@import './overrides/inputs';
// Other overrides
@import './overrides/react-select';

View File

@ -0,0 +1,9 @@
input[type='number']::-webkit-inner-spin-button,
input[type='number']::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
}
input[type='number'] {
-moz-appearance: textfield;
}

View File

@ -4,6 +4,9 @@ import { RequestStatus } from 'reducers/transaction/network';
export const getNetworkStatus = (state: AppState) => getTransactionState(state).network;
export const nonceRequestPending = (state: AppState) =>
getNetworkStatus(state).getNonceStatus === RequestStatus.REQUESTED;
export const nonceRequestFailed = (state: AppState) =>
getNetworkStatus(state).getNonceStatus === RequestStatus.FAILED;

View File

@ -130,7 +130,8 @@
"webpack-hot-middleware": "2.21.0",
"webpack-sources": "1.0.1",
"webpack-subresource-integrity": "1.0.3",
"worker-loader": "1.1.0"
"worker-loader": "1.1.0",
"what-input": "5.0.5"
},
"scripts": {
"freezer": "webpack --config=./webpack_config/webpack.freezer.js && node ./dist/freezer.js",
@ -140,10 +141,14 @@
"prebuild": "check-node-version --package",
"build:downloadable": "webpack --config webpack_config/webpack.html.js",
"prebuild:downloadable": "check-node-version --package",
"build:electron": "webpack --config webpack_config/webpack.electron-prod.js && node webpack_config/buildElectron.js",
"build:electron:osx": "webpack --config webpack_config/webpack.electron-prod.js && ELECTRON_OS=osx node webpack_config/buildElectron.js",
"build:electron:windows": "webpack --config webpack_config/webpack.electron-prod.js && ELECTRON_OS=windows node webpack_config/buildElectron.js",
"build:electron:linux": "webpack --config webpack_config/webpack.electron-prod.js && ELECTRON_OS=linux node webpack_config/buildElectron.js",
"build:electron":
"webpack --config webpack_config/webpack.electron-prod.js && node webpack_config/buildElectron.js",
"build:electron:osx":
"webpack --config webpack_config/webpack.electron-prod.js && ELECTRON_OS=osx node webpack_config/buildElectron.js",
"build:electron:windows":
"webpack --config webpack_config/webpack.electron-prod.js && ELECTRON_OS=windows node webpack_config/buildElectron.js",
"build:electron:linux":
"webpack --config webpack_config/webpack.electron-prod.js && ELECTRON_OS=linux node webpack_config/buildElectron.js",
"prebuild:electron": "check-node-version --package",
"test:coverage": "jest --config=jest_config/jest.config.json --coverage",
"test": "jest --config=jest_config/jest.config.json",
@ -155,14 +160,18 @@
"predev": "check-node-version --package",
"dev:https": "HTTPS=true node webpack_config/devServer.js",
"predev:https": "check-node-version --package",
"dev:electron": "concurrently --kill-others --names 'webpack,electron' 'BUILD_ELECTRON=true node webpack_config/devServer.js' 'webpack --config webpack_config/webpack.electron-dev.js && electron dist/electron-js/main.js'",
"dev:electron:https": "concurrently --kill-others --names 'webpack,electron' 'BUILD_ELECTRON=true HTTPS=true node webpack_config/devServer.js' 'HTTPS=true webpack --config webpack_config/webpack.electron-dev.js && electron dist/electron-js/main.js'",
"dev:electron":
"concurrently --kill-others --names 'webpack,electron' 'BUILD_ELECTRON=true node webpack_config/devServer.js' 'webpack --config webpack_config/webpack.electron-dev.js && electron dist/electron-js/main.js'",
"dev:electron:https":
"concurrently --kill-others --names 'webpack,electron' 'BUILD_ELECTRON=true HTTPS=true node webpack_config/devServer.js' 'HTTPS=true webpack --config webpack_config/webpack.electron-dev.js && electron dist/electron-js/main.js'",
"tslint": "tslint --project . --exclude common/vendor/**/*",
"tscheck": "tsc --noEmit",
"start": "npm run dev",
"precommit": "lint-staged",
"formatAll": "find ./common/ -name '*.ts*' | xargs prettier --write --config ./.prettierrc --config-precedence file-override",
"prettier:diff": "prettier --write --config ./.prettierrc --list-different \"common/**/*.ts\" \"common/**/*.tsx\"",
"formatAll":
"find ./common/ -name '*.ts*' | xargs prettier --write --config ./.prettierrc --config-precedence file-override",
"prettier:diff":
"prettier --write --config ./.prettierrc --list-different \"common/**/*.ts\" \"common/**/*.tsx\"",
"prepush": "npm run tslint && npm run tscheck"
},
"lint-staged": {