Merge pull request #3498 from ethcore/jg-ghh-info-display

Better GHH event display & tracking
This commit is contained in:
Gav Wood 2016-11-19 02:49:56 +08:00 committed by GitHub
commit a97e08fbc8
5 changed files with 258 additions and 43 deletions

View File

@ -15,12 +15,17 @@
/* along with Parity. If not, see <http://www.gnu.org/licenses/>. /* along with Parity. If not, see <http://www.gnu.org/licenses/>.
*/ */
.container { .body {
text-align: center;
background: #333; background: #333;
color: #fff;
}
.container {
font-family: 'Roboto'; font-family: 'Roboto';
vertical-align: middle; vertical-align: middle;
padding: 4em 0; padding: 4em 0;
text-align: center; margin: 0 0 2em 0;
} }
.form { .form {
@ -98,7 +103,7 @@
color: #333; color: #333;
background: #eee; background: #eee;
border: none; border: none;
border-radius: 5px; border-radius: 0.5em;
width: 100%; width: 100%;
font-size: 1em; font-size: 1em;
text-align: center; text-align: center;
@ -113,20 +118,29 @@
} }
.hashError, .hashWarning, .hashOk { .hashError, .hashWarning, .hashOk {
padding-top: 0.5em; margin: 0.5em 0;
text-align: center; text-align: center;
padding: 1em 0;
border: 0.25em solid #333;
border-radius: 0.5em;
} }
.hashError { .hashError {
border-color: #f66;
color: #f66; color: #f66;
background: rgba(255, 102, 102, 0.25);
} }
.hashWarning { .hashWarning {
border-color: #f80;
color: #f80; color: #f80;
background: rgba(255, 236, 0, 0.25);
} }
.hashOk { .hashOk {
opacity: 0.5; border-color: #6f6;
color: #6f6;
background: rgba(102, 255, 102, 0.25);
} }
.typeButtons { .typeButtons {

View File

@ -19,6 +19,7 @@ import React, { Component } from 'react';
import { api } from '../parity'; import { api } from '../parity';
import { attachInterface } from '../services'; import { attachInterface } from '../services';
import Button from '../Button'; import Button from '../Button';
import Events from '../Events';
import IdentityIcon from '../IdentityIcon'; import IdentityIcon from '../IdentityIcon';
import Loading from '../Loading'; import Loading from '../Loading';
@ -27,6 +28,8 @@ import styles from './application.css';
const INVALID_URL_HASH = '0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470'; const INVALID_URL_HASH = '0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470';
const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'; const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';
let nextEventId = 0;
export default class Application extends Component { export default class Application extends Component {
state = { state = {
fromAddress: null, fromAddress: null,
@ -43,7 +46,9 @@ export default class Application extends Component {
registerState: '', registerState: '',
registerType: 'file', registerType: 'file',
repo: '', repo: '',
repoError: null repoError: null,
events: {},
eventIds: []
} }
componentDidMount () { componentDidMount () {
@ -75,7 +80,7 @@ export default class Application extends Component {
let hashClass = null; let hashClass = null;
if (contentHashError) { if (contentHashError) {
hashClass = contentHashOwner !== fromAddress ? styles.hashError : styles.hashWarning; hashClass = contentHashOwner !== fromAddress ? styles.hashError : styles.hashWarning;
} else { } else if (contentHash) {
hashClass = styles.hashOk; hashClass = styles.hashOk;
} }
@ -116,29 +121,34 @@ export default class Application extends Component {
} }
return ( return (
<div className={ styles.container }> <div className={ styles.body }>
<div className={ styles.form }> <div className={ styles.container }>
<div className={ styles.typeButtons }> <div className={ styles.form }>
<Button <div className={ styles.typeButtons }>
disabled={ registerBusy } <Button
invert={ registerType !== 'file' } disabled={ registerBusy }
onClick={ this.onClickTypeNormal }>File Link</Button> invert={ registerType !== 'file' }
<Button onClick={ this.onClickTypeNormal }>File Link</Button>
disabled={ registerBusy } <Button
invert={ registerType !== 'content' } disabled={ registerBusy }
onClick={ this.onClickTypeContent }>Content Bundle</Button> invert={ registerType !== 'content' }
</div> onClick={ this.onClickTypeContent }>Content Bundle</Button>
<div className={ styles.box }>
<div className={ styles.description }>
Provide a valid URL to register. The content information can be used in other contracts that allows for reverse lookups, e.g. image registries, dapp registries, etc.
</div> </div>
{ valueInputs } <div className={ styles.box }>
<div className={ hashClass }> <div className={ styles.description }>
{ contentHashError || contentHash } Provide a valid URL to register. The content information can be used in other contracts that allows for reverse lookups, e.g. image registries, dapp registries, etc.
</div>
{ valueInputs }
<div className={ hashClass }>
{ contentHashError || contentHash }
</div>
{ registerBusy ? this.renderProgress() : this.renderButtons() }
</div> </div>
{ registerBusy ? this.renderProgress() : this.renderButtons() }
</div> </div>
</div> </div>
<Events
eventIds={ this.state.eventIds }
events={ this.state.events } />
</div> </div>
); );
} }
@ -285,15 +295,29 @@ export default class Application extends Component {
} }
} }
trackRequest (promise) { trackRequest (eventId, promise) {
return promise return promise
.then((signerRequestId) => { .then((signerRequestId) => {
this.setState({ signerRequestId, registerState: 'Transaction posted, Waiting for transaction authorization' }); this.setState({
events: Object.assign({}, this.state.events, {
[eventId]: Object.assign({}, this.state.events[eventId], {
signerRequestId,
registerState: 'Transaction posted, Waiting for transaction authorization'
})
})
});
return api.pollMethod('parity_checkRequest', signerRequestId); return api.pollMethod('parity_checkRequest', signerRequestId);
}) })
.then((txHash) => { .then((txHash) => {
this.setState({ txHash, registerState: 'Transaction authorized, Waiting for network confirmations' }); this.setState({
events: Object.assign({}, this.state.events, {
[eventId]: Object.assign({}, this.state.events[eventId], {
txHash,
registerState: 'Transaction authorized, Waiting for network confirmations'
})
})
});
return api.pollMethod('eth_getTransactionReceipt', txHash, (receipt) => { return api.pollMethod('eth_getTransactionReceipt', txHash, (receipt) => {
if (!receipt || !receipt.blockNumber || receipt.blockNumber.eq(0)) { if (!receipt || !receipt.blockNumber || receipt.blockNumber.eq(0)) {
@ -304,27 +328,72 @@ export default class Application extends Component {
}); });
}) })
.then((txReceipt) => { .then((txReceipt) => {
this.setState({ txReceipt, registerBusy: false, registerState: 'Network confirmed, Received transaction receipt', url: '', commit: '', repo: '', commitError: null, contentHash: '', contentHashOwner: null, contentHashError: null }); this.setState({
events: Object.assign({}, this.state.events, {
[eventId]: Object.assign({}, this.state.events[eventId], {
txReceipt,
registerBusy: false,
registerState: 'Network confirmed, Received transaction receipt'
})
})
});
}) })
.catch((error) => { .catch((error) => {
console.error('onSend', error); console.error('onSend', error);
this.setState({ registerError: error.message });
this.setState({
events: Object.assign({}, this.state.events, {
[eventId]: Object.assign({}, this.state.events[eventId], {
registerState: error.message,
registerError: true,
registerBusy: false
})
})
});
}); });
} }
registerContent (repo, commit) { registerContent (contentRepo, contentCommit) {
const { contentHash, fromAddress, instance } = this.state; const { contentHash, fromAddress, instance } = this.state;
contentCommit = contentCommit.substr(0, 2) === '0x' ? contentCommit : `0x${contentCommit}`;
this.setState({ registerBusy: true, registerState: 'Estimating gas for the transaction' }); const eventId = nextEventId++;
const values = [contentHash, contentRepo, contentCommit];
const values = [contentHash, repo, commit.substr(0, 2) === '0x' ? commit : `0x${commit}`];
const options = { from: fromAddress }; const options = { from: fromAddress };
this.setState({
eventIds: [eventId].concat(this.state.eventIds),
events: Object.assign({}, this.state.events, {
[eventId]: {
contentHash,
contentRepo,
contentCommit,
fromAddress,
registerBusy: true,
registerState: 'Estimating gas for the transaction',
timestamp: new Date()
}
}),
url: '',
commit: '',
repo: '',
commitError: null,
contentHash: '',
contentHashOwner: null,
contentHashError: null
});
this.trackRequest( this.trackRequest(
instance eventId, instance
.hint.estimateGas(options, values) .hint.estimateGas(options, values)
.then((gas) => { .then((gas) => {
this.setState({ registerState: 'Gas estimated, Posting transaction to the network' }); this.setState({
events: Object.assign({}, this.state.events, {
[eventId]: Object.assign({}, this.state.events[eventId], {
registerState: 'Gas estimated, Posting transaction to the network'
})
})
});
const gasPassed = gas.mul(1.2); const gasPassed = gas.mul(1.2);
options.gas = gasPassed.toFixed(0); options.gas = gasPassed.toFixed(0);
@ -335,19 +404,45 @@ export default class Application extends Component {
); );
} }
registerUrl (url) { registerUrl (contentUrl) {
const { contentHash, fromAddress, instance } = this.state; const { contentHash, fromAddress, instance } = this.state;
this.setState({ registerBusy: true, registerState: 'Estimating gas for the transaction' }); const eventId = nextEventId++;
const values = [contentHash, contentUrl];
const values = [contentHash, url];
const options = { from: fromAddress }; const options = { from: fromAddress };
this.setState({
eventIds: [eventId].concat(this.state.eventIds),
events: Object.assign({}, this.state.events, {
[eventId]: {
contentHash,
contentUrl,
fromAddress,
registerBusy: true,
registerState: 'Estimating gas for the transaction',
timestamp: new Date()
}
}),
url: '',
commit: '',
repo: '',
commitError: null,
contentHash: '',
contentHashOwner: null,
contentHashError: null
});
this.trackRequest( this.trackRequest(
instance eventId, instance
.hintURL.estimateGas(options, values) .hintURL.estimateGas(options, values)
.then((gas) => { .then((gas) => {
this.setState({ registerState: 'Gas estimated, Posting transaction to the network' }); this.setState({
events: Object.assign({}, this.state.events, {
[eventId]: Object.assign({}, this.state.events[eventId], {
registerState: 'Gas estimated, Posting transaction to the network'
})
})
});
const gasPassed = gas.mul(1.2); const gasPassed = gas.mul(1.2);
options.gas = gasPassed.toFixed(0); options.gas = gasPassed.toFixed(0);

View File

@ -0,0 +1,37 @@
/* 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 <http://www.gnu.org/licenses/>.
*/
.list {
border: none;
margin: 0 auto;
text-align: left;
vertical-align: top;
tr {
&[data-busy="true"] {
opacity: 0.5;
}
&[data-error="true"] {
color: #f66;
}
}
td {
padding: 0.5em;
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
import React, { Component, PropTypes } from 'react';
import moment from 'moment';
import styles from './events.css';
export default class Events extends Component {
static propTypes = {
eventIds: PropTypes.array.isRequired,
events: PropTypes.array.isRequired
}
render () {
return (
<table className={ styles.list }>
<tbody>
{ this.props.eventIds.map((id) => this.renderEvent(id, this.props.events[id])) }
</tbody>
</table>
);
}
renderEvent = (eventId, event) => {
return (
<tr key={ `event_${eventId}` } data-busy={ event.registerBusy } data-error={ event.registerError }>
<td>
<div>{ moment(event.timestamp).fromNow() }</div>
<div>{ event.registerState }</div>
</td>
<td>
<div>{ event.contentUrl || `${event.contentRepo}/${event.contentCommit}` }</div>
<div>{ event.contentHash }</div>
</td>
</tr>
);
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
export default from './events';