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:
parent
7ac546acaf
commit
0ab226ca16
|
@ -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>;
|
||||
|
|
|
@ -146,8 +146,8 @@
|
|||
margin: 0 0 $space-md 0;
|
||||
}
|
||||
|
||||
li,
|
||||
p {
|
||||
> li,
|
||||
> p {
|
||||
font-size: 0.8rem;
|
||||
margin: $space-sm 0;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
@import 'common/sass/variables';
|
||||
|
||||
.gaslimit {
|
||||
&-label-wrapper {
|
||||
align-items: center;
|
||||
margin-bottom: $space-xs;
|
||||
> label {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -40,10 +40,4 @@
|
|||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&-data {
|
||||
}
|
||||
|
||||
&-fee-summary {
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 && (
|
||||
|
|
|
@ -53,26 +53,3 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fade {
|
||||
&-enter,
|
||||
&-exit {
|
||||
transition: opacity 300ms;
|
||||
}
|
||||
|
||||
&-enter {
|
||||
opacity: 0;
|
||||
|
||||
&-active {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&-exit {
|
||||
opacity: 1;
|
||||
|
||||
&-active {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
.inline-spinner {
|
||||
&--fade {
|
||||
&-enter,
|
||||
&-exit {
|
||||
transition: opacity 300ms;
|
||||
}
|
||||
|
||||
&-enter {
|
||||
opacity: 0;
|
||||
|
||||
&-active {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&-exit {
|
||||
opacity: 1;
|
||||
|
||||
&-active {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
);
|
|
@ -35,3 +35,7 @@
|
|||
@import './styles/tab';
|
||||
@import './styles/flexbox';
|
||||
@import './fonts';
|
||||
|
||||
[data-whatintent='mouse'] *:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
@import './overrides/input-groups';
|
||||
@import './overrides/type';
|
||||
@import './overrides/tables';
|
||||
@import './overrides/inputs';
|
||||
|
||||
// Other overrides
|
||||
@import './overrides/react-select';
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
|
||||
|
|
27
package.json
27
package.json
|
@ -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": {
|
||||
|
|
Loading…
Reference in New Issue