[FEATURE] Create Toggle component, Share code between components

This commit is contained in:
Daniel Kmak 2018-04-09 05:06:29 +02:00 committed by Joseph Bagaric
parent 3e10970f83
commit 8c30e36fe8
14 changed files with 220 additions and 284 deletions

View File

@ -110,7 +110,6 @@ class TXMetaDataPanel extends React.Component<Props, State> {
gasPrice={gasPrice}
inputGasPrice={this.handleGasPriceInput}
setGasPrice={this.props.inputGasPrice}
scheduling={scheduling}
/>
)}

View File

@ -1,6 +1,6 @@
import React from 'react';
import { translateRaw } from 'translations';
import FeeSummary from './FeeSummary';
import FeeSummary, { RenderData } from './FeeSummary';
import './AdvancedGas.scss';
import { TToggleAutoGasLimit, toggleAutoGasLimit } from 'actions/config';
import { AppState } from 'reducers';
@ -11,7 +11,6 @@ import { getAutoGasLimitEnabled } from 'selectors/config';
import { isValidGasPrice } from 'selectors/transaction';
import { sanitizeNumericalInput } from 'libs/values';
import { Input, UnitDisplay } from 'components/ui';
import SchedulingFeeSummary from './SchedulingFeeSummary';
import { EAC_SCHEDULING_CONFIG } from 'libs/scheduling';
import { getScheduleGasPrice, getTimeBounty } from 'selectors/schedule';
@ -126,56 +125,62 @@ class AdvancedGas extends React.Component<Props, State> {
}
private renderFee() {
const { gasPrice, scheduling, scheduleGasPrice, timeBounty } = this.props;
const { gasPrice, scheduleGasPrice } = this.props;
const { feeSummary } = this.state.options;
if (!feeSummary) {
return;
}
if (scheduling) {
return (
<div className="AdvancedGas-fee-summary">
<SchedulingFeeSummary
gasPrice={gasPrice}
scheduleGasPrice={scheduleGasPrice}
render={({ gasPriceWei, scheduleGasLimit, fee, usd }) => (
<div>
<span>
<UnitDisplay
value={EAC_SCHEDULING_CONFIG.FEE.mul(EAC_SCHEDULING_CONFIG.FEE_MULTIPLIER)}
unit={'ether'}
displayShortBalance={true}
checkOffline={true}
symbol="ETH"
/>{' '}
+ {timeBounty && timeBounty.value && timeBounty.value.toString()} + {gasPriceWei}{' '}
* {EAC_SCHEDULING_CONFIG.SCHEDULING_GAS_LIMIT.toString()} +{' '}
{scheduleGasPrice && scheduleGasPrice.value && scheduleGasPrice.value.toString()}{' '}
* ({EAC_SCHEDULING_CONFIG.FUTURE_EXECUTION_COST.toString()} + {scheduleGasLimit})
=&nbsp;{fee}&nbsp;{usd && <span>~=&nbsp;{usd}&nbsp;USD</span>}
</span>
</div>
)}
/>
</div>
);
}
return (
<div className="AdvancedGas-fee-summary">
<FeeSummary
gasPrice={gasPrice}
render={({ gasPriceWei, gasLimit, fee, usd }) => (
<span>
{gasPriceWei} * {gasLimit} = {fee} {usd && <span>~= {usd} USD</span>}
</span>
)}
scheduleGasPrice={scheduleGasPrice}
render={(data: RenderData) => this.printFeeFormula(data)}
/>
</div>
);
}
private printFeeFormula(data: RenderData) {
if (this.props.scheduling) {
return this.getScheduleFeeFormula(data);
}
return this.getStandardFeeFormula(data);
}
private getStandardFeeFormula({ gasPriceWei, gasLimit, fee, usd }: RenderData) {
return (
<span>
{gasPriceWei} * {gasLimit} = {fee} {usd && <span>~= ${usd} USD</span>}
</span>
);
}
private getScheduleFeeFormula({ gasPriceWei, scheduleGasLimit, fee, usd }: RenderData) {
const { scheduleGasPrice, timeBounty } = this.props;
return (
<div>
<span>
<UnitDisplay
value={EAC_SCHEDULING_CONFIG.FEE.mul(EAC_SCHEDULING_CONFIG.FEE_MULTIPLIER)}
unit={'ether'}
displayShortBalance={true}
checkOffline={true}
symbol="ETH"
/>{' '}
+ {timeBounty && timeBounty.value && timeBounty.value.toString()} + {gasPriceWei} *{' '}
{EAC_SCHEDULING_CONFIG.SCHEDULING_GAS_LIMIT.toString()} +{' '}
{scheduleGasPrice && scheduleGasPrice.value && scheduleGasPrice.value.toString()} * ({EAC_SCHEDULING_CONFIG.FUTURE_EXECUTION_COST.toString()}{' '}
+ {scheduleGasLimit}) =&nbsp;{fee}&nbsp;{usd && <span>~=&nbsp;${usd}&nbsp;USD</span>}
</span>
</div>
);
}
private handleGasPriceChange = (ev: React.FormEvent<HTMLInputElement>) => {
const { value } = ev.currentTarget;
this.props.inputGasPrice(sanitizeNumericalInput(value));

View File

@ -8,11 +8,14 @@ import { getGasLimit } from 'selectors/transaction';
import { UnitDisplay, Spinner } from 'components/ui';
import { NetworkConfig } from 'types/network';
import './FeeSummary.scss';
import { getScheduleGasLimit, getTimeBounty, getSchedulingToggle } from 'selectors/schedule';
import { calcEACTotalCost } from 'libs/scheduling';
interface RenderData {
export interface RenderData {
gasPriceWei: string;
gasPriceGwei: string;
gasLimit: string;
scheduleGasLimit: string;
fee: React.ReactElement<string>;
usd: React.ReactElement<string> | null;
}
@ -23,10 +26,15 @@ interface ReduxStateProps {
network: NetworkConfig;
isOffline: AppState['config']['meta']['offline'];
isGasEstimating: AppState['gas']['isEstimating'];
scheduleGasLimit: AppState['schedule']['scheduleGasLimit'];
timeBounty: AppState['schedule']['timeBounty'];
scheduling: AppState['schedule']['schedulingToggle']['value'];
}
interface OwnProps {
gasPrice: AppState['transaction']['fields']['gasPrice'];
scheduleGasPrice: AppState['schedule']['scheduleGasPrice'];
render(data: RenderData): React.ReactElement<string> | string;
}
@ -34,7 +42,16 @@ type Props = OwnProps & ReduxStateProps;
class FeeSummary extends React.Component<Props> {
public render() {
const { gasPrice, gasLimit, rates, network, isOffline, isGasEstimating } = this.props;
const {
gasPrice,
gasLimit,
rates,
network,
isOffline,
isGasEstimating,
scheduling,
scheduleGasLimit
} = this.props;
if (isGasEstimating) {
return (
@ -44,7 +61,7 @@ class FeeSummary extends React.Component<Props> {
);
}
const feeBig = gasPrice.value && gasLimit.value && gasPrice.value.mul(gasLimit.value);
const feeBig = this.getFee();
const fee = (
<UnitDisplay
value={feeBig}
@ -69,17 +86,50 @@ class FeeSummary extends React.Component<Props> {
);
return (
<div className="FeeSummary">
<div className={`FeeSummary ${scheduling && 'SchedulingFeeSummary'}`}>
{this.props.render({
gasPriceWei: gasPrice.value.toString(),
gasPriceGwei: gasPrice.raw,
fee,
usd,
gasLimit: gasLimit.raw
gasLimit: gasLimit.raw,
scheduleGasLimit: scheduleGasLimit.raw
})}
</div>
);
}
private getFee() {
const { scheduling } = this.props;
if (scheduling) {
return this.calculateSchedulingFee();
}
return this.calculateStandardFee();
}
private calculateStandardFee() {
const { gasPrice, gasLimit } = this.props;
return gasPrice.value && gasLimit.value && gasPrice.value.mul(gasLimit.value);
}
private calculateSchedulingFee() {
const { gasPrice, scheduleGasLimit, scheduleGasPrice, timeBounty } = this.props;
return (
gasPrice.value &&
scheduleGasLimit.value &&
timeBounty.value &&
calcEACTotalCost(
scheduleGasLimit.value,
gasPrice.value,
scheduleGasPrice.value,
timeBounty.value
)
);
}
}
function mapStateToProps(state: AppState): ReduxStateProps {
@ -88,7 +138,10 @@ function mapStateToProps(state: AppState): ReduxStateProps {
rates: state.rates.rates,
network: getNetworkConfig(state),
isOffline: getOffline(state),
isGasEstimating: getIsEstimating(state)
isGasEstimating: getIsEstimating(state),
scheduling: getSchedulingToggle(state).value,
scheduleGasLimit: getScheduleGasLimit(state),
timeBounty: getTimeBounty(state)
};
}

View File

@ -1,120 +0,0 @@
import React from 'react';
import BN from 'bn.js';
import { connect } from 'react-redux';
import { AppState } from 'reducers';
import { getNetworkConfig, getOffline } from 'selectors/config';
import { getIsEstimating } from 'selectors/gas';
import { getTimeBounty, getScheduleGasLimit } from 'selectors/schedule';
import { UnitDisplay, Spinner } from 'components/ui';
import { NetworkConfig } from 'types/network';
import './FeeSummary.scss';
import { calcEACTotalCost, EAC_SCHEDULING_CONFIG } from 'libs/scheduling';
import { gasPriceToBase } from 'libs/units';
interface RenderData {
gasPriceWei: string;
gasPriceGwei: string;
scheduleGasLimit: string;
fee: React.ReactElement<string>;
usd: React.ReactElement<string> | null;
}
interface ReduxStateProps {
scheduleGasLimit: AppState['schedule']['scheduleGasLimit'];
rates: AppState['rates']['rates'];
network: NetworkConfig;
isOffline: AppState['config']['meta']['offline'];
isGasEstimating: AppState['gas']['isEstimating'];
timeBounty: AppState['schedule']['timeBounty'];
}
interface OwnProps {
gasPrice: AppState['transaction']['fields']['gasPrice'];
scheduleGasPrice: AppState['schedule']['scheduleGasPrice'];
render(data: RenderData): React.ReactElement<string> | string;
}
type Props = OwnProps & ReduxStateProps;
class SchedulingFeeSummary extends React.Component<Props> {
public render() {
const {
gasPrice,
scheduleGasLimit,
rates,
network,
isOffline,
isGasEstimating,
scheduleGasPrice,
timeBounty
} = this.props;
if (isGasEstimating || !scheduleGasPrice) {
return (
<div className="FeeSummary is-loading">
<Spinner />
</div>
);
}
const feeBig =
gasPrice.value &&
scheduleGasLimit.value &&
timeBounty.value &&
calcEACTotalCost(
scheduleGasLimit.value,
gasPrice.value,
scheduleGasPrice.value || gasPriceToBase(EAC_SCHEDULING_CONFIG.SCHEDULE_GAS_PRICE_FALLBACK),
timeBounty.value
);
const fee = (
<UnitDisplay
value={feeBig}
unit="ether"
symbol={network.unit}
displayShortBalance={6}
checkOffline={false}
/>
);
const usdBig = network.isTestnet
? new BN(0)
: feeBig && rates[network.unit] && feeBig.muln(rates[network.unit].USD);
const usd = isOffline ? null : (
<UnitDisplay
value={usdBig}
unit="ether"
displayShortBalance={2}
displayTrailingZeroes={true}
checkOffline={true}
/>
);
return (
<div className="FeeSummary SchedulingFeeSummary">
{this.props.render({
gasPriceWei: gasPrice.value.toString(),
gasPriceGwei: gasPrice.raw,
fee,
usd,
scheduleGasLimit: scheduleGasLimit.raw
})}
</div>
);
}
}
function mapStateToProps(state: AppState): ReduxStateProps {
return {
scheduleGasLimit: getScheduleGasLimit(state),
rates: state.rates.rates,
network: getNetworkConfig(state),
isOffline: getOffline(state),
isGasEstimating: getIsEstimating(state),
timeBounty: getTimeBounty(state)
};
}
export default connect(mapStateToProps)(SchedulingFeeSummary);

View File

@ -17,7 +17,6 @@ import { gasPriceDefaults } from 'config';
import { InlineSpinner } from 'components/ui/InlineSpinner';
import { TInputGasPrice } from 'actions/transaction';
import FeeSummary from './FeeSummary';
import SchedulingFeeSummary from './SchedulingFeeSummary';
import { getScheduleGasPrice } from 'selectors/schedule';
const SliderWithTooltip = createSliderWithTooltip(Slider);
@ -25,7 +24,6 @@ const SliderWithTooltip = createSliderWithTooltip(Slider);
interface OwnProps {
gasPrice: AppState['transaction']['fields']['gasPrice'];
setGasPrice: TInputGasPrice;
scheduling?: boolean;
inputGasPrice(rawGas: string): void;
}
@ -74,7 +72,8 @@ class SimpleGas extends React.Component<Props> {
gasLimitEstimationTimedOut,
isWeb3Node,
noncePending,
gasLimitPending
gasLimitPending,
scheduleGasPrice
} = this.props;
const bounds = {
@ -118,41 +117,20 @@ class SimpleGas extends React.Component<Props> {
<span>{translate('TX_FEE_SCALE_RIGHT')}</span>
</div>
</div>
{this.renderFee()}
<FeeSummary
gasPrice={gasPrice}
scheduleGasPrice={scheduleGasPrice}
render={({ fee, usd }) => (
<span>
{fee} {usd && <span>/ ${usd}</span>}
</span>
)}
/>
</div>
</div>
);
}
private renderFee() {
const { gasPrice, scheduling, scheduleGasPrice } = this.props;
if (scheduling) {
return (
<SchedulingFeeSummary
gasPrice={gasPrice}
scheduleGasPrice={scheduleGasPrice}
render={({ fee, usd }) => (
<span>
{fee}&nbsp;{usd && <span>/&nbsp;{usd}</span>}
</span>
)}
/>
);
}
return (
<FeeSummary
gasPrice={gasPrice}
render={({ fee, usd }) => (
<span>
{fee} {usd && <span>/ {usd}</span>}
</span>
)}
/>
);
}
private handleSlider = (gasGwei: number) => {
this.props.inputGasPrice(gasGwei.toString());
};

View File

@ -0,0 +1,64 @@
$slider-radius: 26px;
$transition-time: .4s;
$toggle-color: #0C6482;
$travel-distance: 38px;
.Toggle {
$root: &;
position: relative;
display: inline-block;
width: 72px;
height: 34px;
margin: 5px 0;
&-input {
display: none;
&:checked + #{$root}-slider {
background-color: $toggle-color;
&::before {
-webkit-transform: translateX($travel-distance);
-ms-transform: translateX($travel-distance);
transform: translateX($travel-distance);
}
}
&:focus + #{$root}-slider {
box-shadow: 0 0 1px $toggle-color;
}
}
&-slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
-webkit-transition: $transition-time;
transition: $transition-time;
&.round {
border-radius: 34px;
&::before {
border-radius: 50%;
}
}
&::before {
position: absolute;
content: "";
height: $slider-radius;
width: $slider-radius;
left: 4px;
bottom: 4px;
background-color: white;
-webkit-transition: $transition-time;
transition: $transition-time;
}
}
}

View File

@ -0,0 +1,16 @@
import React, { ChangeEvent } from 'react';
import './Toggle.scss';
interface Props {
checked: boolean;
onChangeHandler(event: ChangeEvent<HTMLInputElement>): any;
}
const Toggle: React.SFC<Props> = ({ checked, onChangeHandler }) => (
<label className="Toggle checkbox">
<input className="Toggle-input" type="checkbox" checked={checked} onChange={onChangeHandler} />
<span className="Toggle-slider round" />
</label>
);
export default Toggle;

View File

@ -17,6 +17,7 @@ export { default as Input } from './Input';
export { default as TextArea } from './TextArea';
export { default as Address } from './Address';
export { default as CodeBlock } from './CodeBlock';
export { default as Toggle } from './Toggle';
export * from './ConditionalInput';
export * from './Expandable';
export * from './InlineSpinner';

View File

@ -35,7 +35,7 @@ class ScheduleDepositFieldClass extends Component<Props> {
<Input
className={!!scheduleDeposit.raw && !validScheduleDeposit ? 'invalid' : ''}
type="number"
placeholder={translateRaw('SCHEDULE_DEPOSIT_PLACEHOLDER')}
placeholder="0.00001"
value={scheduleDeposit.raw}
onChange={this.handleDepositChange}
/>

View File

@ -4,6 +4,7 @@ import { connect } from 'react-redux';
import translate from 'translations';
import { getCurrentSchedulingToggle, ICurrentSchedulingToggle } from 'selectors/schedule/fields';
import { AppState } from 'reducers';
import { Toggle } from 'components/ui';
interface DispatchProps {
setSchedulingToggle: TSetSchedulingToggle;
@ -22,14 +23,7 @@ class SchedulingToggleClass extends Component<Props> {
return (
<div className="input-group-wrapper">
<span className="input-group-header">{translate('SCHEDULING_TOGGLE')}</span>
<label className="switch checkbox">
<input
type="checkbox"
checked={currentSchedulingToggle.value}
onChange={this.handleOnChange}
/>
<span className="slider round" />
</label>
<Toggle checked={currentSchedulingToggle.value} onChangeHandler={this.handleOnChange} />
</div>
);
}

View File

@ -67,9 +67,13 @@ export const calcEACEndowment = (
export const calcEACTotalCost = (
callGas: BN,
gasPrice: BN,
callGasPrice: BN,
callGasPrice: BN | null,
timeBounty: BN | null
) => {
if (!callGasPrice) {
callGasPrice = gasPriceToBase(EAC_SCHEDULING_CONFIG.SCHEDULE_GAS_PRICE_FALLBACK);
}
const deployCost = gasPrice.mul(EAC_SCHEDULING_CONFIG.SCHEDULING_GAS_LIMIT);
const futureExecutionCost = calcEACFutureExecutionCost(callGas, callGasPrice, timeBounty);
@ -123,20 +127,30 @@ export const getScheduleData = (
]);
};
enum SchedulingParamsError {
InsufficientEndowment,
ReservedWindowBiggerThanExecutionWindow,
InvalidTemporalUnit,
ExecutionWindowTooSoon,
CallGasTooHigh,
EmptyToAddress
}
export const parseSchedulingParametersValidity = (isValid: boolean[]) => {
const Errors = [
'InsufficientEndowment',
'ReservedWindowBiggerThanExecutionWindow',
'InvalidTemporalUnit',
'ExecutionWindowTooSoon',
'CallGasTooHigh',
'EmptyToAddress'
const errorsIndexMapping = [
SchedulingParamsError.InsufficientEndowment,
SchedulingParamsError.ReservedWindowBiggerThanExecutionWindow,
SchedulingParamsError.InvalidTemporalUnit,
SchedulingParamsError.ExecutionWindowTooSoon,
SchedulingParamsError.CallGasTooHigh,
SchedulingParamsError.EmptyToAddress
];
const errors: string[] = [];
const errors: SchedulingParamsError[] = [];
isValid.forEach((boolIsTrue, index) => {
if (!boolIsTrue) {
errors.push(Errors[index]);
errors.push(errorsIndexMapping[index]);
}
});

View File

@ -37,7 +37,6 @@
@import './styles/helpers';
@import './styles/datetimepicker';
@import './styles/code';
@import './styles/btn-toggle';
@import './fonts';
[data-whatintent='mouse'] *:focus {

View File

@ -1,66 +0,0 @@
/* TOGGLE BUTTON */
$slider-radius: 26px;
$transition-time: .4s;
$toggle-color: #0C6482;
$travel-distance: 38px;
.switch {
position: relative;
display: inline-block;
width: 72px;
height: 34px;
margin: 5px 0;
& input {
display: none;
}
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
-webkit-transition: $transition-time;
transition: $transition-time;
&:before {
position: absolute;
content: "";
height: $slider-radius;
width: $slider-radius;
left: 4px;
bottom: 4px;
background-color: white;
-webkit-transition: $transition-time;
transition: $transition-time;
}
}
input {
&:checked + .slider {
background-color: $toggle-color;
}
&:focus + .slider {
box-shadow: 0 0 1px $toggle-color;
}
&:checked + .slider:before {
-webkit-transform: translateX($travel-distance);
-ms-transform: translateX($travel-distance);
transform: translateX($travel-distance);
}
}
/* Rounded sliders */
.slider.round {
border-radius: 34px;
&:before {
border-radius: 50%;
}
}

View File

@ -111,7 +111,6 @@
"SCHEDULE_BLOCK": "Block Number ",
"SCHEDULE_BLOCK_PLACEHOLDER": "Enter Block Number ",
"SCHEDULE_DEPOSIT": "Require Deposit (optional) ",
"SCHEDULE_DEPOSIT_PLACEHOLDER": "0.00001 ",
"SCHEDULE_DEPOSIT_TOOLTIP": "Require TimeNode to deposit a given amount of ETH in order to gain an exclusive time window for execution.",
"SCHEDULE_TIMESTAMP": "Date & Time ",
"SCHEDULE_TIMEZONE": "Timezone ",