From 9afdbd9848333b04847e2fe0f2f6f6dcd2741e53 Mon Sep 17 00:00:00 2001 From: "Michael Bradley, Jr" Date: Tue, 5 Mar 2019 16:45:18 -0600 Subject: [PATCH] refactor(embark-ui): improve cockpit's contracts explorer Rename contract "Transactions" tab to "Log". Display and allow filtering of all contract methods. Disable debug button for pure/view functions and the constructor. Revise the filtering logic so that filters are combined together. Make the status filter a drop down menu like the others. Revise styling for consistent row height, alignment of text, and button sizes; use a monospaced font in some cases to achieve the effect. Handle enter/return correctly in forms within a contract's Interact tab. Remove event rows from a contract's Interact tab. Track pure/view calls in the blockchain proxy so they can be logged server-side by console listener and reported in Cockpit within a contract's Log tab. Eliminate double logging in the contracts manager. Ensure contracts deployed on a fresh `embark run` have an `address` / `deployedAddress` property. --- .../demo/contracts/simple_storage.sol | 2 +- .../src/components/ContractDetail.js | 1 - .../src/components/ContractLayout.js | 8 +- .../embark-ui/src/components/ContractLog.js | 173 ++++++++++++++++++ .../embark-ui/src/components/ContractLog.scss | 6 + .../src/components/ContractOverview.css | 16 -- .../src/components/ContractOverview.js | 124 +++++++++---- .../src/components/ContractOverview.scss | 67 +++++++ .../src/components/ContractTransactions.js | 151 --------------- .../embark-ui/src/components/DebugButton.js | 15 +- .../src/components/TextEditorToolbar.js | 2 +- ...nsContainer.js => ContractLogContainer.js} | 14 +- .../src/containers/EditorContainer.js | 1 - .../containers/TextEditorAsideContainer.js | 8 +- packages/embark/src/lib/constants.json | 1 + .../lib/modules/blockchain_process/proxy.js | 46 +++-- .../src/lib/modules/console_listener/index.js | 16 +- .../lib/modules/contracts_manager/index.js | 28 ++- 18 files changed, 422 insertions(+), 257 deletions(-) create mode 100644 packages/embark-ui/src/components/ContractLog.js create mode 100644 packages/embark-ui/src/components/ContractLog.scss delete mode 100644 packages/embark-ui/src/components/ContractOverview.css create mode 100644 packages/embark-ui/src/components/ContractOverview.scss delete mode 100644 packages/embark-ui/src/components/ContractTransactions.js rename packages/embark-ui/src/containers/{ContractTransactionsContainer.js => ContractLogContainer.js} (79%) diff --git a/dapps/templates/demo/contracts/simple_storage.sol b/dapps/templates/demo/contracts/simple_storage.sol index fee1cb245..a845284a2 100644 --- a/dapps/templates/demo/contracts/simple_storage.sol +++ b/dapps/templates/demo/contracts/simple_storage.sol @@ -14,4 +14,4 @@ contract SimpleStorage { function get() public view returns (uint retVal) { return storedData; } -} \ No newline at end of file +} diff --git a/packages/embark-ui/src/components/ContractDetail.js b/packages/embark-ui/src/components/ContractDetail.js index da37a73ad..1772ee532 100644 --- a/packages/embark-ui/src/components/ContractDetail.js +++ b/packages/embark-ui/src/components/ContractDetail.js @@ -31,4 +31,3 @@ ContractDetail.propTypes = { }; export default ContractDetail; - diff --git a/packages/embark-ui/src/components/ContractLayout.js b/packages/embark-ui/src/components/ContractLayout.js index c8e75fae6..96c626518 100644 --- a/packages/embark-ui/src/components/ContractLayout.js +++ b/packages/embark-ui/src/components/ContractLayout.js @@ -5,7 +5,7 @@ import { TabContent, TabPane, Nav, NavItem, NavLink, Card, CardBody, CardTitle } import classnames from 'classnames'; import ContractDetail from '../components/ContractDetail'; -import ContractTransactionsContainer from '../containers/ContractTransactionsContainer'; +import ContractLogContainer from '../containers/ContractLogContainer'; import ContractOverviewContainer from '../containers/ContractOverviewContainer'; class ContractLayout extends React.Component { @@ -53,7 +53,7 @@ class ContractLayout extends React.Component { className={classnames({ active: this.state.activeTab === '3' })} onClick={() => { this.toggle('3'); }} > - Transactions + Log @@ -65,13 +65,13 @@ class ContractLayout extends React.Component { - + - ) + ); } } diff --git a/packages/embark-ui/src/components/ContractLog.js b/packages/embark-ui/src/components/ContractLog.js new file mode 100644 index 000000000..5cb614fa3 --- /dev/null +++ b/packages/embark-ui/src/components/ContractLog.js @@ -0,0 +1,173 @@ +import PropTypes from "prop-types"; +import React from 'react'; +import {Row, Col, Table, FormGroup, Label, Input, Form} from 'reactstrap'; +import DebugButton from './DebugButton'; + +import "./ContractLog.scss"; + +const TX_STATES = {Any: '', Success: '0x1', Fail: '0x0'}; +const EVENT = 'event'; +const FUNCTION = 'function'; + +class ContractLog extends React.Component { + constructor(props) { + super(props); + this.state = {method: '', event: '', status: TX_STATES['Any']}; + } + + getMethods() { + if (!this.props.contract.abiDefinition) { + return []; + } + + return this.props.contract.abiDefinition.filter(method => method.type === FUNCTION); + } + + getEvents() { + if (!this.props.contract.abiDefinition) { + return []; + } + + return this.props.contract.abiDefinition.filter(method => method.type === EVENT); + } + + updateState(key, value) { + this.setState({[key]: value}); + } + + dataToDisplay() { + return this.props.contractLogs.map(contractLog => { + const events = this.props.contractEvents + .filter(contractEvent => contractEvent.transactionHash === contractLog.transactionHash) + .map(contractEvent => contractEvent.event); + contractLog.events = events; + return contractLog; + }).filter(contractLog => { + if (this.state.status && contractLog.status !== this.state.status) { + return false; + } + + if (this.state.method && this.state.event) { + return this.state.method === contractLog.functionName && + contractLog.events.includes(this.state.event); + } + + if (this.state.method) { + return this.state.method === contractLog.functionName; + } + + if (this.state.event) { + return contractLog.events.includes(this.state.event); + } + + return true; + }); + } + + render() { + return ( + +
+ + + + + this.updateState('method', event.target.value)} + value={this.state.method}> + + {this.getMethods().map((method, index) => ( + ))} + + + + + + + + this.updateState('event', event.target.value)} + value={this.state.event}> + + {this.getEvents().map((event, index) => ( + ))} + + + + + + + this.updateState('status', event.target.value)} + value={this.state.status}> + {Object.keys(TX_STATES).map((key, index) => ( + + ))} + + + + +
+ + + + + + + + + + + + + + + { + this.dataToDisplay().map((log, index) => { + return ( + + + + + + + + + + ); + }) + } + +
+ InvocationEventsGasBlockStatusTransaction
{`${log.name}.${log.functionName}(${log.paramString})`}{log.events.join(', ')}{log.gasUsed}{log.blockNumber}{log.status}{log.transactionHash}
+ +
+
+ ); + } +} + +ContractLog.propTypes = { + contractLogs: PropTypes.array, + contractEvents: PropTypes.array, + contract: PropTypes.object.isRequired +}; + +export default ContractLog; diff --git a/packages/embark-ui/src/components/ContractLog.scss b/packages/embark-ui/src/components/ContractLog.scss new file mode 100644 index 000000000..7dd87840d --- /dev/null +++ b/packages/embark-ui/src/components/ContractLog.scss @@ -0,0 +1,6 @@ +table.contract-log { + td, th { + height: 4.2em; + vertical-align: middle !important; + } +} diff --git a/packages/embark-ui/src/components/ContractOverview.css b/packages/embark-ui/src/components/ContractOverview.css deleted file mode 100644 index 223847dbc..000000000 --- a/packages/embark-ui/src/components/ContractOverview.css +++ /dev/null @@ -1,16 +0,0 @@ -.contract-function-container .gas-price-form #gasPrice { - border-top-right-radius: 0; - border-bottom-right-radius: 0; -} - -.contract-function-container .gas-price-form button { - border-top-left-radius: 0; - border-bottom-left-radius: 0; - border: 0; - box-shadow: none !important; -} - -.contract-function-container .card-header.closed { - border-bottom: none; - border-radius: 0.25rem; -} diff --git a/packages/embark-ui/src/components/ContractOverview.js b/packages/embark-ui/src/components/ContractOverview.js index 46a97e6d0..d192853b1 100644 --- a/packages/embark-ui/src/components/ContractOverview.js +++ b/packages/embark-ui/src/components/ContractOverview.js @@ -23,7 +23,7 @@ import {formatContractForDisplay} from '../utils/presentation'; import FontAwesome from 'react-fontawesome'; import classnames from 'classnames'; -import "./ContractOverview.css"; +import "./ContractOverview.scss"; class ContractFunction extends Component { constructor(props) { @@ -45,7 +45,7 @@ class ContractFunction extends Component { return 'Deploy'; } - return ContractFunction.isPureCall(method) ? 'Call' : 'Send'; + return ContractFunction.isPureCall(method) ? 'call' : 'send'; } inputsAsArray() { @@ -70,7 +70,22 @@ class ContractFunction extends Component { handleCall(e) { e.preventDefault(); - this.props.postContractFunction(this.props.contractName, this.props.method.name, this.inputsAsArray(), this.state.inputs.gasPrice * 1000000000); + this.props.postContractFunction( + this.props.contractName, + this.props.method.name, + this.inputsAsArray(), + this.state.inputs.gasPrice * 1000000000 + ); + } + + handleKeyPress(e) { + if (e.key === 'Enter') { + if (this.callDisabled()) { + e.preventDefault(); + } else { + this.handleCall(e); + } + } } callDisabled() { @@ -95,38 +110,62 @@ class ContractFunction extends Component { }); } + makeBadge(color, codeColor, text) { + const badgeDark = this.state.functionCollapse; + const _codeColor = badgeDark ? 'white' : codeColor; + return ( + + + {text} + + + ); + } + render() { + if (ContractFunction.isEvent(this.props.method)) { + return ; + } return ( this.toggleFunction()}> - {ContractFunction.isPureCall(this.props.method) && Boolean(this.props.method.inputs.length) && - call - } - {ContractFunction.isPureCall(this.props.method) && !this.props.method.inputs.length && - - } - {ContractFunction.isEvent(this.props.method) && - event - } - {this.props.method.name}({this.props.method.inputs.map(input => input.name).join(', ')}) + + {`${this.props.method.name}` + + `(${this.props.method.inputs.map(i => i.name).join(', ')})`} + +
+ {(ContractFunction.isPureCall(this.props.method) && + this.makeBadge('success', 'white', 'call')) || + this.makeBadge('warning', 'black', 'send')} +
- {!ContractFunction.isEvent(this.props.method) && -
+ {this.props.method.inputs.map(input => ( - - this.handleChange(e, input.name)}/> + + this.handleChange(e, input.name)} + onKeyPress={(e) => this.handleKeyPress(e)}/> ))}
@@ -140,12 +179,15 @@ class ContractFunction extends Component { -
+ - - Gas Price (in GWei) (optional) + this.handleChange(e, 'gasPrice')}/> + onChange={(e) => this.handleChange(e, 'gasPrice')} + onKeyPress={(e) => this.handleKeyPress(e)}/>
{this.props.contractFunctions && this.props.contractFunctions.length > 0 && - {this.props.contractFunctions.map(contractFunction => ( - - {contractFunction.inputs.length > 0 &&

Input(s): {contractFunction.inputs.join(', ')}

} - Result: {JSON.stringify(contractFunction.result)} + {this.props.contractFunctions.map((contractFunction, idx) => ( + + {contractFunction.inputs.length > 0 && +

Input(s):   + + {contractFunction.inputs.join(', ')} + +

} + Result:   + + + {JSON.stringify(contractFunction.result).slice(1, -1)} + +
))}
} - } - + ); } @@ -217,7 +275,8 @@ const ContractOverview = (props) => { .filter((method) => { return props.onlyConstructor ? method.type === 'constructor' : method.type !== 'constructor'; }) - .map(method => )} @@ -237,4 +296,3 @@ ContractOverview.defaultProps = { }; export default ContractOverview; - diff --git a/packages/embark-ui/src/components/ContractOverview.scss b/packages/embark-ui/src/components/ContractOverview.scss new file mode 100644 index 000000000..2a6d6925c --- /dev/null +++ b/packages/embark-ui/src/components/ContractOverview.scss @@ -0,0 +1,67 @@ +.contract-function-badge { + cursor: default; + + code { + font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace; + font-size: 13.1333px !important; + font-weight: 700 !important; + } + + .code-black { + color: black !important; + } + + .code-white { + color: white !important; + } +} + +.contract-function-button { + font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace; + font-size: 13.1333px !important; + font-weight: 700 !important; +} + +.contract-function-button-with-margin-top { + margin-top: 8px; +} + +.contract-function-container { + .card-header.closed { + border-bottom: none; + border-radius: 0.25rem; + } + + .card-title { + align-items: center; + display: flex; + margin-bottom: auto; + } + + .card-title div { + margin-left: auto; + } + + .gas-price-form { + #gasPrice { + border-top-right-radius: 0; + border-bottom-right-radius: 0; + } + + button { + border-top-left-radius: 0; + border-bottom-left-radius: 0; + border: 0; + box-shadow: none !important; + } + } +} + +.contract-function-input { + font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace; + margin: 0 5px; +} + +.contract-function-input-values, .contract-function-result, .contract-function-signature { + font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace; +} diff --git a/packages/embark-ui/src/components/ContractTransactions.js b/packages/embark-ui/src/components/ContractTransactions.js deleted file mode 100644 index 5590ea311..000000000 --- a/packages/embark-ui/src/components/ContractTransactions.js +++ /dev/null @@ -1,151 +0,0 @@ -import PropTypes from "prop-types"; -import React from 'react'; -import {Row, Col, Table, FormGroup, Label, Input, Form} from 'reactstrap'; - -import DebugButton from './DebugButton' - -const TX_STATES = {Success: '0x1', Fail: '0x0', Any: ''}; -const EVENT = 'event'; -const FUNCTION = 'function'; -const CONSTRUCTOR = 'constructor'; -const PURE = 'pure'; -const VIEW = 'view'; - -class ContractTransactions extends React.Component { - constructor(props) { - super(props); - this.state = {method: '', event: '', status: TX_STATES['Any']}; - } - - getMethods() { - if (!this.props.contract.abiDefinition) { - return []; - } - - return this.props.contract.abiDefinition.filter(method => method.mutability !== VIEW && method.mutability !== PURE && method.constant !== true && (method.type === FUNCTION || method.type === CONSTRUCTOR)); - } - - getEvents() { - if (!this.props.contract.abiDefinition) { - return []; - } - return this.props.contract.abiDefinition.filter(method => method.type === EVENT); - } - - updateState(key, value) { - this.setState({[key]: value}); - } - - dataToDisplay() { - return this.props.contractLogs.map(contractLog => { - const events = this.props.contractEvents - .filter(contractEvent => contractEvent.transactionHash === contractLog.transactionHash) - .map(contractEvent => contractEvent.event); - contractLog.events = events; - return contractLog; - }).filter(contractLog => { - if (this.state.status && contractLog.status !== this.state.status) { - return false; - } - - if (this.state.method || this.state.event) { - return this.state.method === contractLog.functionName || contractLog.events.includes(this.state.event); - } - - return true; - }); - } - - render() { - return ( - - - - - - - this.updateState('method', event.target.value)} value={this.state.method}> - )} - - - - - - - this.updateState('event', event.target.value)} value={this.state.event}> - )} - - - - - - - - - - {Object.keys(TX_STATES).map(key => ( - - this.updateState('status', event.target.value)} - checked={TX_STATES[key] === this.state.status} /> - - - ))} - - - - - - - - - - - - - - - - - - - - { - this.dataToDisplay().map((log, index) => { - return ( - - - - - - - - - - ); - }) - } - -
- CallEventsGas UsedBlock numberStatusTransaction hash
{`${log.name}.${log.functionName}(${log.paramString})`}{log.events.join(', ')}{log.gasUsed}{log.blockNumber}{log.status}{log.transactionHash}
- -
-
- ); - } -} - -ContractTransactions.propTypes = { - contractLogs: PropTypes.array, - contractEvents: PropTypes.array, - contract: PropTypes.object.isRequired -}; - -export default ContractTransactions; - diff --git a/packages/embark-ui/src/components/DebugButton.js b/packages/embark-ui/src/components/DebugButton.js index a44fc0c1b..7a45e482d 100644 --- a/packages/embark-ui/src/components/DebugButton.js +++ b/packages/embark-ui/src/components/DebugButton.js @@ -1,4 +1,3 @@ - import React from 'react'; import PropTypes from 'prop-types'; import {Button} from "reactstrap"; @@ -13,12 +12,20 @@ class DebugButton extends React.Component { isDebuggable() { return this.props.forceDebuggable || - (this.props.contracts && this.props.contracts.find(contract => contract.address === this.props.transaction.to)); + (!this.props.transaction.isCall && + !this.props.transaction.isConstructor && + this.props.contracts && + this.props.contracts.find(contract => { + const address = this.props.transaction.to || this.props.transaction.address; + return contract.address && + address && + (contract.address.toLowerCase() === address.toLowerCase()); + })); } render() { if (!this.isDebuggable()) { - return + return ; } return (