Align contract event log l&f with transactions (#2812)

* Event into own component, align with transactions

* Pass value & type through from event log params

* Reformat display columns
This commit is contained in:
Jaco Greeff 2016-10-25 16:02:12 +02:00 committed by GitHub
parent 8d0cff3599
commit 7eacf07629
12 changed files with 272 additions and 109 deletions

View File

@ -150,7 +150,9 @@ export default class Contract {
log.event = event.name; log.event = event.name;
decoded.params.forEach((param) => { decoded.params.forEach((param) => {
log.params[param.name] = param.token.value; const { type, value } = param.token;
log.params[param.name] = { type, value };
}); });
return log; return log;

View File

@ -110,6 +110,10 @@ export default class Events extends Component {
logToEvent = (log) => { logToEvent = (log) => {
log.key = api.util.sha3(JSON.stringify(log)); log.key = api.util.sha3(JSON.stringify(log));
log.params = Object.keys(log.params).reduce((params, name) => {
params[name] = log.params[name].value;
return params;
}, {});
return log; return log;
} }

View File

@ -114,6 +114,10 @@ export default class Events extends Component {
logToEvent = (log) => { logToEvent = (log) => {
log.key = api.util.sha3(JSON.stringify(log)); log.key = api.util.sha3(JSON.stringify(log));
log.params = Object.keys(log.params).reduce((params, name) => {
params[name] = log.params[name].value;
return params;
}, {});
return log; return log;
} }
@ -139,7 +143,6 @@ export default class Events extends Component {
.concat(pendingEvents) .concat(pendingEvents)
.filter((log) => !minedNew.find((event) => event.transactionHash === log.transactionHash)); .filter((log) => !minedNew.find((event) => event.transactionHash === log.transactionHash));
const events = [].concat(pendingNew).concat(minedNew); const events = [].concat(pendingNew).concat(minedNew);
console.log('*** events', events.map((event) => event.address));
this.setState({ loading: false, events, minedEvents: minedNew, pendingEvents: pendingNew }); this.setState({ loading: false, events, minedEvents: minedNew, pendingEvents: pendingNew });
} }
} }

View File

@ -106,7 +106,10 @@ export default class Events extends Component {
logIndex, logIndex,
transactionHash, transactionHash,
transactionIndex, transactionIndex,
params, params: Object.keys(params).reduce((data, name) => {
data[name] = params[name].value;
return data;
}, {}),
key key
}; };
}; };

View File

@ -48,9 +48,9 @@ const renderEvent = (classNames, verb) => (e, accounts, contacts) => {
return ( return (
<tr key={ e.key } className={ classes }> <tr key={ e.key } className={ classes }>
<td>{ renderAddress(e.parameters.owner, accounts, contacts) }</td> <td>{ renderAddress(e.parameters.owner.value, accounts, contacts) }</td>
<td><abbr title={ e.transaction }>{ verb }</abbr></td> <td><abbr title={ e.transaction }>{ verb }</abbr></td>
<td><code>{ renderHash(bytesToHex(e.parameters.name)) }</code></td> <td><code>{ renderHash(bytesToHex(e.parameters.name.value)) }</code></td>
<td>{ renderStatus(e.timestamp, e.state === 'pending') }</td> <td>{ renderStatus(e.timestamp, e.state === 'pending') }</td>
</tr> </tr>
); );
@ -64,10 +64,10 @@ const renderDataChanged = (e, accounts, contacts) => {
return ( return (
<tr key={ e.key } className={ classNames }> <tr key={ e.key } className={ classNames }>
<td>{ renderAddress(e.parameters.owner, accounts, contacts) }</td> <td>{ renderAddress(e.parameters.owner.value, accounts, contacts) }</td>
<td><abbr title={ e.transaction }>updated</abbr></td> <td><abbr title={ e.transaction }>updated</abbr></td>
<td> <td>
key <code>{ new Buffer(e.parameters.plainKey).toString('utf8') }</code> of <code>{ renderHash(bytesToHex(e.parameters.name)) }</code> key <code>{ new Buffer(e.parameters.plainKey.value).toString('utf8') }</code> of <code>{ renderHash(bytesToHex(e.parameters.name.value)) }</code>
</td> </td>
<td>{ renderStatus(e.timestamp, e.state === 'pending') }</td> <td>{ renderStatus(e.timestamp, e.state === 'pending') }</td>
</tr> </tr>

View File

@ -30,7 +30,10 @@ const logToEvent = (log) => {
logIndex, logIndex,
transactionHash, transactionHash,
transactionIndex, transactionIndex,
params, params: Object.keys(params).reduce((data, name) => {
data[name] = params[name].value;
return data;
}, {}),
key key
}; };
}; };

View File

@ -148,27 +148,27 @@ export const subscribeEvents = () => (dispatch, getState) => {
return dispatch(setTokenData(params.id.toNumber(), { return dispatch(setTokenData(params.id.toNumber(), {
tla: '...', tla: '...',
base: -1, base: -1,
address: params.addr, address: params.addr.value,
name: params.name, name: params.name.value,
isPending: true isPending: true
})); }));
} }
if (event === 'Registered' && type === 'mined') { if (event === 'Registered' && type === 'mined') {
return dispatch(loadToken(params.id.toNumber())); return dispatch(loadToken(params.id.value.toNumber()));
} }
if (event === 'Unregistered' && type === 'pending') { if (event === 'Unregistered' && type === 'pending') {
return dispatch(setTokenPending(params.id.toNumber(), true)); return dispatch(setTokenPending(params.id.value.toNumber(), true));
} }
if (event === 'Unregistered' && type === 'mined') { if (event === 'Unregistered' && type === 'mined') {
return dispatch(deleteToken(params.id.toNumber())); return dispatch(deleteToken(params.id.value.toNumber()));
} }
if (event === 'MetaChanged' && type === 'pending') { if (event === 'MetaChanged' && type === 'pending') {
return dispatch(setTokenData( return dispatch(setTokenData(
params.id.toNumber(), params.id.value.toNumber(),
{ metaPending: true, metaMined: false } { metaPending: true, metaMined: false }
)); ));
} }
@ -176,13 +176,13 @@ export const subscribeEvents = () => (dispatch, getState) => {
if (event === 'MetaChanged' && type === 'mined') { if (event === 'MetaChanged' && type === 'mined') {
setTimeout(() => { setTimeout(() => {
dispatch(setTokenData( dispatch(setTokenData(
params.id.toNumber(), params.id.value.toNumber(),
{ metaPending: false, metaMined: false } { metaPending: false, metaMined: false }
)); ));
}, 5000); }, 5000);
return dispatch(setTokenData( return dispatch(setTokenData(
params.id.toNumber(), params.id.value.toNumber(),
{ metaPending: false, metaMined: true } { metaPending: false, metaMined: true }
)); ));
} }

View File

@ -0,0 +1,183 @@
// 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 BigNumber from 'bignumber.js';
import moment from 'moment';
import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { fetchBlock, fetchTransaction } from '../../../../redux/providers/blockchainActions';
import { IdentityIcon, IdentityName, Input, InputAddress } from '../../../../ui';
import styles from '../../contract.css';
class Event extends Component {
static contextTypes = {
api: PropTypes.object.isRequired
}
static propTypes = {
event: PropTypes.object.isRequired,
blocks: PropTypes.object,
transactions: PropTypes.object,
isTest: PropTypes.bool,
fetchBlock: PropTypes.func.isRequired,
fetchTransaction: PropTypes.func.isRequired
}
componentDidMount () {
this.retrieveTransaction();
}
render () {
const { event, blocks, transactions, isTest } = this.props;
const block = blocks[event.blockNumber.toString()];
const transaction = transactions[event.transactionHash] || {};
const classes = `${styles.event} ${styles[event.state]}`;
const url = `https://${isTest ? 'testnet.' : ''}etherscan.io/tx/${event.transactionHash}`;
const keys = Object.keys(event.params).join(', ');
const values = Object.keys(event.params).map((name, index) => {
const param = event.params[name];
return (
<div className={ styles.eventValue } key={ `${event.key}_val_${index}` }>
{ this.renderParam(name, param) }
</div>
);
});
return (
<tr className={ classes }>
<td className={ styles.timestamp }>
<div>{ event.state === 'pending' ? 'pending' : this.formatBlockTimestamp(block) }</div>
<div>{ this.formatNumber(transaction.blockNumber) }</div>
</td>
<td className={ styles.txhash }>
{ this.renderAddressName(transaction.from) }
</td>
<td className={ styles.txhash }>
<div className={ styles.eventType }>
{ event.type }({ keys })
</div>
<a href={ url } target='_blank'>{ this.formatHash(event.transactionHash) }</a>
</td>
<td className={ styles.eventDetails }>
<div className={ styles.eventParams }>
{ values }
</div>
</td>
</tr>
);
}
formatHash (hash) {
if (!hash || hash.length <= 16) {
return hash;
}
return `${hash.substr(2, 6)}...${hash.slice(-6)}`;
}
renderAddressName (address, withName = true) {
return (
<span className={ styles.eventAddress }>
<IdentityIcon center inline address={ address } className={ styles.eventIdentityicon } />
{ withName ? <IdentityName address={ address } /> : address }
</span>
);
}
renderParam (name, param) {
const { api } = this.context;
switch (param.type) {
case 'address':
return (
<InputAddress
disabled
text
className={ styles.input }
value={ param.value }
label={ name } />
);
default:
let value;
if (api.util.isInstanceOf(param.value, BigNumber)) {
value = param.value.toFormat(0);
} else if (api.util.isArray(param.value)) {
value = api.util.bytesToHex(param.value);
} else {
value = param.value.toString();
}
return (
<Input
disabled
className={ styles.input }
value={ value }
label={ name } />
);
}
}
formatBlockTimestamp (block) {
if (!block) {
return null;
}
return moment(block.timestamp).fromNow();
}
formatNumber (number) {
if (!number) {
return null;
}
return new BigNumber(number).toFormat();
}
retrieveTransaction () {
const { event, fetchBlock, fetchTransaction } = this.props;
fetchBlock(event.blockNumber);
fetchTransaction(event.transactionHash);
}
}
function mapStateToProps (state) {
const { isTest } = state.nodeStatus;
const { blocks, transactions } = state.blockchain;
return {
isTest,
blocks,
transactions
};
}
function mapDispatchToProps (dispatch) {
return bindActionCreators({
fetchBlock, fetchTransaction
}, dispatch);
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(Event);

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 './event';

View File

@ -14,11 +14,11 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
import BigNumber from 'bignumber.js';
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import { Container, ContainerTitle } from '../../../ui'; import { Container, ContainerTitle } from '../../../ui';
import Event from './Event';
import styles from '../contract.css'; import styles from '../contract.css';
export default class Events extends Component { export default class Events extends Component {
@ -27,106 +27,23 @@ export default class Events extends Component {
} }
static propTypes = { static propTypes = {
events: PropTypes.array, events: PropTypes.array
isTest: PropTypes.bool
}
state = {
transactions: {}
}
componentDidMount () {
this.componentWillReceiveProps(this.props);
}
componentWillReceiveProps (newProps) {
this.retrieveTransactions(newProps.events);
} }
render () { render () {
const { events, isTest } = this.props; const { events } = this.props;
const { transactions } = this.state;
if (!events || !events.length) { if (!events || !events.length) {
return null; return null;
} }
const rows = events.map((event) => {
const transaction = transactions[event.transactionHash] || {};
const classes = `${styles.event} ${styles[event.state]}`;
const url = `https://${isTest ? 'testnet.' : ''}etherscan.io/tx/${event.transactionHash}`;
const keys = Object.keys(event.params).map((key, index) => {
return <div className={ styles.key } key={ `${event.key}_key_${index}` }>{ key }</div>;
});
const values = Object.values(event.params).map((value, index) => {
return (
<div className={ styles.value } key={ `${event.key}_val_${index}` }>
{ this.renderValue(value) }
</div>
);
});
return (
<tr className={ classes } key={ event.key }>
<td>{ event.state === 'pending' ? 'pending' : event.blockNumber.toFormat(0) }</td>
<td className={ styles.txhash }>
<div>{ transaction.from }</div>
<a href={ url } target='_blank'>{ event.transactionHash }</a>
</td>
<td>
<div>{ event.type } =></div>
{ keys }
</td>
<td>
<div>&nbsp;</div>
{ values }
</td>
</tr>
);
});
return ( return (
<Container> <Container>
<ContainerTitle title='events' /> <ContainerTitle title='events' />
<table className={ styles.events }> <table className={ styles.events }>
<tbody>{ rows }</tbody> <tbody>{ events.map((event) => <Event event={ event } key={ event.key } />) }</tbody>
</table> </table>
</Container> </Container>
); );
} }
renderValue (value) {
const { api } = this.context;
if (api.util.isInstanceOf(value, BigNumber)) {
return value.toFormat(0);
} else if (api.util.isArray(value)) {
return api.util.bytesToHex(value);
}
return value.toString();
}
retrieveTransactions (events) {
const { api } = this.context;
const { transactions } = this.state;
const hashes = {};
events.forEach((event) => {
if (!hashes[event.transactionHash] && !transactions[event.transactionHash]) {
hashes[event.transactionHash] = true;
}
});
Promise
.all(Object.keys(hashes).map((hash) => api.eth.getTransactionByHash(hash)))
.then((newTransactions) => {
this.setState({
transactions: newTransactions.reduce((store, transaction) => {
transactions[transaction.hash] = transaction;
return transactions;
}, transactions)
});
});
}
} }

View File

@ -24,9 +24,12 @@
border-spacing: 0; border-spacing: 0;
} }
.event { .events tr {
line-height: 32px;
vertical-align: top; vertical-align: top;
line-height: 26px; }
.event {
} }
.event td { .event td {
@ -43,9 +46,6 @@
color: #aaa; color: #aaa;
} }
.value {
}
.event td div { .event td div {
white-space: nowrap; white-space: nowrap;
} }
@ -56,3 +56,35 @@
.pending { .pending {
opacity: 0.5; opacity: 0.5;
} }
.timestamp {
padding-top: 1.5em;
text-align: right;
line-height: 1.5em;
opacity: 0.5;
white-space: nowrap;
}
.eventDetails {
}
.eventType {
}
.eventParams {
padding-left: 2em;
}
.eventValue {
margin-top: -16px;
}
.eventAddress {
display: inline-block;
position: relative;
}
.eventIdentityicon {
margin-bottom: -10px;
margin-right: 0.5em;
}

View File

@ -116,7 +116,6 @@ class Contract extends Component {
contract={ contract } contract={ contract }
values={ queryValues } /> values={ queryValues } />
<Events <Events
isTest={ isTest }
events={ allEvents } /> events={ allEvents } />
</Page> </Page>
</div> </div>