Minimise transactions progress (#4942)
* Watch the requests and display them throughout the app * Linting * Showing Requests * Fully working Transaction Requests Display * Add FormattedMessage to Requests * Clean-up the Transfer dialog * Update Validations * Cleanup Create Wallet * Clean Deploy Contract Dialog * Cleanup Contract Execution * Fix Requests * Cleanup Wallet Settings * Don't show stepper in Portal if less than 2 steps * WIP local storage requests * Caching requests and saving contract deployments * Add Historic prop to Requests MethodDecoding * Fix tests * Add Contract address to MethodDecoding * PR Grumbles - Part I * PR Grumbles - Part II * Use API Subscription methods * Linting * Move SavedRequests and add tests * Added tests for Requests Actions * Fixing tests * PR Grumbles + Playground fix * Revert Playground changes * PR Grumbles * Better showEth in MethodDecoding
This commit is contained in:
committed by
Jaco Greeff
parent
e28c477075
commit
a99721004b
17
js/src/views/Application/Requests/index.js
Normal file
17
js/src/views/Application/Requests/index.js
Normal file
@@ -0,0 +1,17 @@
|
||||
// Copyright 2015-2017 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
export default from './requests';
|
||||
127
js/src/views/Application/Requests/requests.css
Normal file
127
js/src/views/Application/Requests/requests.css
Normal file
@@ -0,0 +1,127 @@
|
||||
/* Copyright 2015-2017 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
$baseColor: 255;
|
||||
$baseOpacity: 0.95;
|
||||
|
||||
.requests {
|
||||
align-items: flex-end;
|
||||
bottom: 2em;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: fixed;
|
||||
right: 0.175em;
|
||||
z-index: 750;
|
||||
|
||||
* {
|
||||
font-size: 0.85rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
.request {
|
||||
animation-fill-mode: forwards;
|
||||
animation-timing-function: cubic-bezier(0.7, 0, 0.3, 1);
|
||||
background-color: rgba($baseColor, $baseColor, $baseColor, $baseOpacity);
|
||||
color: black;
|
||||
cursor: pointer;
|
||||
margin-top: 0.5em;
|
||||
opacity: 1;
|
||||
|
||||
&.hide {
|
||||
animation-duration: 0.5s;
|
||||
animation-name: fadeout;
|
||||
}
|
||||
|
||||
.status {
|
||||
padding: 0.5em;
|
||||
|
||||
&.error {
|
||||
background-color: rgba(200, 40, 40, 0.95);
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
padding: 1em;
|
||||
|
||||
* {
|
||||
color: black !important;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover .container {
|
||||
background-color: rgba($baseColor, $baseColor, $baseColor, 1);
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeout {
|
||||
from {
|
||||
display: block;
|
||||
height: inherit;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
49% {
|
||||
display: block;
|
||||
height: inherit;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
98% {
|
||||
display: block;
|
||||
height: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
display: none;
|
||||
height: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.identity {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
margin-right: 1em;
|
||||
|
||||
.icon {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.inline {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
.fill {
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
}
|
||||
|
||||
.hash {
|
||||
margin-left: 0.25em;
|
||||
}
|
||||
233
js/src/views/Application/Requests/requests.js
Normal file
233
js/src/views/Application/Requests/requests.js
Normal file
@@ -0,0 +1,233 @@
|
||||
// Copyright 2015-2017 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { LinearProgress } from 'material-ui';
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
|
||||
import { hideRequest } from '~/redux/providers/requestsActions';
|
||||
import { MethodDecoding, IdentityIcon, ScrollableText, ShortenedHash } from '~/ui';
|
||||
|
||||
import styles from './requests.css';
|
||||
|
||||
const ERROR_STATE = 'ERROR_STATE';
|
||||
const DONE_STATE = 'DONE_STATE';
|
||||
const WAITING_STATE = 'WAITING_STATE';
|
||||
|
||||
class Requests extends Component {
|
||||
static propTypes = {
|
||||
requests: PropTypes.object.isRequired,
|
||||
onHideRequest: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
state = {
|
||||
extras: {}
|
||||
};
|
||||
|
||||
render () {
|
||||
const { requests } = this.props;
|
||||
const { extras } = this.state;
|
||||
|
||||
return (
|
||||
<div className={ styles.requests }>
|
||||
{ Object.values(requests).map((request) => this.renderRequest(request, extras[request.requestId])) }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderRequest (request, extras = {}) {
|
||||
const { show, transaction } = request;
|
||||
const state = this.getTransactionState(request);
|
||||
const displayedTransaction = { ...transaction };
|
||||
|
||||
// Don't show gas and gasPrice
|
||||
delete displayedTransaction.gas;
|
||||
delete displayedTransaction.gasPrice;
|
||||
|
||||
const requestClasses = [ styles.request ];
|
||||
const statusClasses = [ styles.status ];
|
||||
const requestStyle = {};
|
||||
|
||||
const handleHideRequest = () => {
|
||||
this.handleHideRequest(request.requestId);
|
||||
};
|
||||
|
||||
if (state.type === ERROR_STATE) {
|
||||
statusClasses.push(styles.error);
|
||||
}
|
||||
|
||||
if (!show) {
|
||||
requestClasses.push(styles.hide);
|
||||
}
|
||||
|
||||
// Set the Request height (for animation) if found
|
||||
if (extras.height) {
|
||||
requestStyle.height = extras.height;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={ requestClasses.join(' ') }
|
||||
key={ request.requestId }
|
||||
ref={ `request_${request.requestId}` }
|
||||
onClick={ handleHideRequest }
|
||||
style={ requestStyle }
|
||||
>
|
||||
<div className={ statusClasses.join(' ') }>
|
||||
{ this.renderStatus(request) }
|
||||
</div>
|
||||
{
|
||||
state.type === ERROR_STATE
|
||||
? null
|
||||
: (
|
||||
<LinearProgress
|
||||
max={ 6 }
|
||||
mode={ state.type === WAITING_STATE ? 'indeterminate' : 'determinate' }
|
||||
value={ state.type === DONE_STATE ? request.blockHeight.toNumber() : 6 }
|
||||
/>
|
||||
)
|
||||
}
|
||||
<div className={ styles.container }>
|
||||
<div className={ styles.identity } title={ transaction.from }>
|
||||
<IdentityIcon
|
||||
address={ transaction.from }
|
||||
inline
|
||||
center
|
||||
className={ styles.icon }
|
||||
/>
|
||||
</div>
|
||||
<MethodDecoding
|
||||
address={ transaction.from }
|
||||
compact
|
||||
historic={ state.type === DONE_STATE }
|
||||
transaction={ displayedTransaction }
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderStatus (request) {
|
||||
const { error, transactionHash, transactionReceipt } = request;
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div
|
||||
className={ styles.inline }
|
||||
title={ error.message }
|
||||
>
|
||||
<FormattedMessage
|
||||
id='requests.status.error'
|
||||
defaultMessage='An error occured:'
|
||||
/>
|
||||
<div className={ styles.fill }>
|
||||
<ScrollableText
|
||||
text={ error.text || error.message }
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (transactionReceipt) {
|
||||
return (
|
||||
<FormattedMessage
|
||||
id='requests.status.transactionMined'
|
||||
defaultMessage='Transaction mined at block #{blockNumber} ({blockHeight} blocks ago)'
|
||||
values={ {
|
||||
blockHeight: request.blockHeight.toNumber(),
|
||||
blockNumber: transactionReceipt.blockNumber.toFormat()
|
||||
} }
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (transactionHash) {
|
||||
return (
|
||||
<div className={ styles.inline }>
|
||||
<FormattedMessage
|
||||
id='requests.status.transactionSent'
|
||||
defaultMessage='Transaction sent to network with hash'
|
||||
/>
|
||||
<div className={ [ styles.fill, styles.hash ].join(' ') }>
|
||||
<ShortenedHash data={ transactionHash } />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<FormattedMessage
|
||||
id='requests.status.waitingForSigner'
|
||||
defaultMessage='Waiting for authorization in the Parity Signer'
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
getTransactionState (request) {
|
||||
const { error, transactionReceipt } = request;
|
||||
|
||||
if (error) {
|
||||
return { type: ERROR_STATE };
|
||||
}
|
||||
|
||||
if (transactionReceipt) {
|
||||
return { type: DONE_STATE };
|
||||
}
|
||||
|
||||
return { type: WAITING_STATE };
|
||||
}
|
||||
|
||||
handleHideRequest = (requestId) => {
|
||||
const requestElement = ReactDOM.findDOMNode(this.refs[`request_${requestId}`]);
|
||||
|
||||
// Try to get the request element height, to have a nice transition effect
|
||||
if (requestElement) {
|
||||
const { height } = requestElement.getBoundingClientRect();
|
||||
const prevExtras = this.state.extras;
|
||||
const nextExtras = {
|
||||
...prevExtras,
|
||||
[ requestId ]: {
|
||||
...prevExtras[requestId],
|
||||
height
|
||||
}
|
||||
};
|
||||
|
||||
return this.setState({ extras: nextExtras }, () => {
|
||||
return this.props.onHideRequest(requestId);
|
||||
});
|
||||
}
|
||||
|
||||
return this.props.onHideRequest(requestId);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
const { requests } = state;
|
||||
|
||||
return { requests };
|
||||
};
|
||||
|
||||
function mapDispatchToProps (dispatch) {
|
||||
return bindActionCreators({
|
||||
onHideRequest: hideRequest
|
||||
}, dispatch);
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(Requests);
|
||||
89
js/src/views/Application/Requests/savedRequests.js
Normal file
89
js/src/views/Application/Requests/savedRequests.js
Normal file
@@ -0,0 +1,89 @@
|
||||
// Copyright 2015-2017 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
import store from 'store';
|
||||
|
||||
import { ERROR_CODES } from '~/api/transport/error';
|
||||
|
||||
export const LS_REQUESTS_KEY = '_parity::requests';
|
||||
|
||||
export default class SavedRequests {
|
||||
load (api) {
|
||||
const requests = this._get();
|
||||
|
||||
const promises = Object.values(requests).map((request) => {
|
||||
const { requestId, transactionHash } = request;
|
||||
|
||||
// The request hasn't been signed yet
|
||||
if (transactionHash) {
|
||||
return request;
|
||||
}
|
||||
|
||||
return this._requestExists(api, requestId)
|
||||
.then((exists) => {
|
||||
if (!exists) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return request;
|
||||
})
|
||||
.catch(() => {
|
||||
this.remove(requestId);
|
||||
});
|
||||
});
|
||||
|
||||
return Promise.all(promises).then((requests) => requests.filter((request) => request));
|
||||
}
|
||||
|
||||
save (requestId, requestData) {
|
||||
const requests = this._get();
|
||||
|
||||
requests[requestId] = {
|
||||
...(requests[requestId] || {}),
|
||||
...requestData
|
||||
};
|
||||
|
||||
this._set(requests);
|
||||
}
|
||||
|
||||
remove (requestId) {
|
||||
const requests = this._get();
|
||||
|
||||
delete requests[requestId];
|
||||
this._set(requests);
|
||||
}
|
||||
|
||||
_get () {
|
||||
return store.get(LS_REQUESTS_KEY) || {};
|
||||
}
|
||||
|
||||
_set (requests = {}) {
|
||||
return store.set(LS_REQUESTS_KEY, requests);
|
||||
}
|
||||
|
||||
_requestExists (api, requestId) {
|
||||
return api.parity
|
||||
.checkRequest(requestId)
|
||||
.then(() => true)
|
||||
.catch((error) => {
|
||||
if (error.code === ERROR_CODES.REQUEST_NOT_FOUND) {
|
||||
return false;
|
||||
}
|
||||
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
}
|
||||
88
js/src/views/Application/Requests/savedRequests.spec.js
Normal file
88
js/src/views/Application/Requests/savedRequests.spec.js
Normal file
@@ -0,0 +1,88 @@
|
||||
// Copyright 2015-2017 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
import sinon from 'sinon';
|
||||
import store from 'store';
|
||||
|
||||
import SavedRequests, { LS_REQUESTS_KEY } from './savedRequests';
|
||||
|
||||
const DEFAULT_REQUEST = {
|
||||
requestId: '0x1',
|
||||
transaction: {}
|
||||
};
|
||||
|
||||
const api = createApi();
|
||||
const savedRequests = new SavedRequests();
|
||||
|
||||
function createApi () {
|
||||
return {
|
||||
parity: {
|
||||
checkRequest: sinon.stub().resolves()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
describe('views/Application/Requests/savedRequests', () => {
|
||||
beforeEach(() => {
|
||||
store.set(LS_REQUESTS_KEY, {
|
||||
[DEFAULT_REQUEST.requestId]: DEFAULT_REQUEST
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
store.set(LS_REQUESTS_KEY, {});
|
||||
});
|
||||
|
||||
it('gets requests from local storage', () => {
|
||||
const requests = savedRequests._get();
|
||||
|
||||
expect(requests[DEFAULT_REQUEST.requestId]).to.deep.equal(DEFAULT_REQUEST);
|
||||
});
|
||||
|
||||
it('sets requests to local storage', () => {
|
||||
savedRequests._set({});
|
||||
|
||||
const requests = savedRequests._get();
|
||||
|
||||
expect(requests).to.deep.equal({});
|
||||
});
|
||||
|
||||
it('removes requests', () => {
|
||||
savedRequests.remove(DEFAULT_REQUEST.requestId);
|
||||
|
||||
const requests = savedRequests._get();
|
||||
|
||||
expect(requests).to.deep.equal({});
|
||||
});
|
||||
|
||||
it('saves new requests', () => {
|
||||
savedRequests.save(DEFAULT_REQUEST.requestId, { extraData: true });
|
||||
|
||||
const requests = savedRequests._get();
|
||||
|
||||
expect(requests[DEFAULT_REQUEST.requestId]).to.deep.equal({
|
||||
...DEFAULT_REQUEST,
|
||||
extraData: true
|
||||
});
|
||||
});
|
||||
|
||||
it('loads requests', () => {
|
||||
return savedRequests.load(api)
|
||||
.then((requests) => {
|
||||
expect(requests[0]).to.deep.equal(DEFAULT_REQUEST);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -31,6 +31,7 @@ import FrameError from './FrameError';
|
||||
import Status from './Status';
|
||||
import Store from './store';
|
||||
import TabBar from './TabBar';
|
||||
import Requests from './Requests';
|
||||
|
||||
import styles from './application.css';
|
||||
|
||||
@@ -103,6 +104,7 @@ class Application extends Component {
|
||||
}
|
||||
<Extension />
|
||||
<Snackbar />
|
||||
<Requests />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user