Annotations, click to error, UI improvements

Compiler annotations added to editor gutter for errors and warnings

Clicking an error now scrolls editor to offending line and scrolls page to the top of the editor

Added Compiling… loader.
This commit is contained in:
emizzle 2018-08-10 12:16:38 +10:00 committed by Pascal Precht
parent 70f5a09d47
commit 8caa478968
No known key found for this signature in database
GPG Key ID: 0EE28D8D6FD85D7D
12 changed files with 343 additions and 139 deletions

View File

@ -2039,11 +2039,47 @@
"resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-3.3.0.tgz",
"integrity": "sha512-MAAAIOdi2s4Gl6rZ76PNcUa9IOYB+5ICdT41o5uMRf09aEu/F9RK+qhe8RjXNPwcTjGV7KU7h2P/fljThFVqyQ=="
},
"component-clone": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/component-clone/-/component-clone-0.2.2.tgz",
"integrity": "sha1-x/WXmCKID62M+wliuikYbQYe4E8=",
"requires": {
"component-type": "*"
}
},
"component-emitter": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz",
"integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY="
},
"component-raf": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/component-raf/-/component-raf-1.2.0.tgz",
"integrity": "sha1-srxy1D8bAU/eeks8RHx2S8c8y6o="
},
"component-tween": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/component-tween/-/component-tween-1.2.0.tgz",
"integrity": "sha1-zDnOXbqwW1KCX0HRlHY4oLAbK4o=",
"requires": {
"component-clone": "0.2.2",
"component-emitter": "1.2.0",
"component-type": "1.1.0",
"ease-component": "1.0.0"
},
"dependencies": {
"component-emitter": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.0.tgz",
"integrity": "sha1-zNETqGOI0GSC0D3j/H35hSa6jv4="
}
}
},
"component-type": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/component-type/-/component-type-1.1.0.tgz",
"integrity": "sha1-lbZmqtU+XI0fK+E1xFtdSZGXwMU="
},
"compressible": {
"version": "2.0.14",
"resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.14.tgz",
@ -2835,6 +2871,11 @@
"resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz",
"integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI="
},
"ease-component": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/ease-component/-/ease-component-1.0.0.tgz",
"integrity": "sha1-s3VybbC1sEWVt3RAOW/sfapdd8k="
},
"ecc-jsbn": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz",
@ -9033,6 +9074,14 @@
}
}
},
"react-scroll-to-component": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/react-scroll-to-component/-/react-scroll-to-component-1.0.2.tgz",
"integrity": "sha1-8mDck2xipT53J4bXgy/giE4ZU1Q=",
"requires": {
"scroll-to": "0.0.2"
}
},
"react-text-mask": {
"version": "5.4.3",
"resolved": "https://registry.npmjs.org/react-text-mask/-/react-text-mask-5.4.3.tgz",
@ -9554,6 +9603,15 @@
"ajv": "^5.0.0"
}
},
"scroll-to": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/scroll-to/-/scroll-to-0.0.2.tgz",
"integrity": "sha1-k205ipEzZgokkhRcLACB38sHKPM=",
"requires": {
"component-raf": "1.2.0",
"component-tween": "1.2.0"
}
},
"select-hose": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz",

View File

@ -15,6 +15,7 @@
"react-redux": "^5.0.7",
"react-router-dom": "^4.3.1",
"react-scripts": "1.1.4",
"react-scroll-to-component": "^1.0.2",
"redux": "^4.0.0",
"redux-saga": "^0.16.0",
"tabler-react": "^1.18.0"

View File

@ -1,5 +0,0 @@
self.MonacoEnvironment = {
baseUrl: 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.13.1/min/'
};
importScripts('https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.13.1/min/vs/base/worker/workerMain.js'); // eslint-disable-line

View File

@ -144,13 +144,13 @@ export function listenToContractLogs() {
}
// Fiddle
export const FETCH_COMPILE_CODE = 'FETCH_COMPILE_CODE';
export const RECEIVE_COMPILE_CODE = 'RECEIVE_COMPILE_CODE';
export const RECEIVE_COMPILE_CODE_ERROR = 'RECEIVE_COMPILE_CODE_ERROR';
export const COMPILE_CODE_REQUEST = 'COMPILE_CODE_REQUEST';
export const COMPILE_CODE_SUCCESS = 'COMPILE_CODE_SUCCESS';
export const COMPILE_CODE_FAILURE = 'COMPILE_CODE_FAILURE';
export function fetchCodeCompilation(codeToCompile){
return {
type: FETCH_COMPILE_CODE,
type: COMPILE_CODE_REQUEST,
codeToCompile
};
}
@ -158,14 +158,14 @@ export function fetchCodeCompilation(codeToCompile){
export function receiveCodeCompilation(compilationResult){
return {
type: RECEIVE_COMPILE_CODE,
type: COMPILE_CODE_SUCCESS,
compilationResult
};
}
export function receiveCodeCompilationError(){
return {
type: RECEIVE_COMPILE_CODE_ERROR
type: COMPILE_CODE_FAILURE
};
}

View File

@ -5,28 +5,49 @@ import 'brace/theme/tomorrow_night_blue';
import 'ace-mode-solidity/build/remix-ide/mode-solidity';
import PropTypes from 'prop-types';
const Fiddle = ({onCodeChange, value}) => {
class Fiddle extends React.Component {
constructor(props) {
super(props);
this.ace = null;
}
render() {
const {onCodeChange, value, errors, warnings} = this.props;
const annotations = errors.map((error) => { return error.annotation; }).concat(warnings.map(warning => { return warning.annotation; }));
return (
<React.Fragment>
<h1>Fiddle</h1>
<p>Play around with contract code and deploy against your running node.</p>
<AceEditor
mode="solidity"
theme="tomorrow_night_blue"
name="blah1"
name="fiddle"
height="60em"
width="100%"
onChange={onCodeChange}
value={value}
showGutter={true}
annotations={annotations}
ref={(ace) => { this.ace = ace; }}
setOptions={{
useWorker: false
}}
editorProps={{
$blockScrolling: Infinity,
enableLiveAutocompletion:true,
highlightSelectedWord: true
}}
/>
</React.Fragment>
);
};
}
}
Fiddle.propTypes = {
onCodeChange: PropTypes.func,
value: PropTypes.string
value: PropTypes.string,
errors: PropTypes.array,
warnings: PropTypes.array
};
export default Fiddle;

View File

@ -1,16 +1,10 @@
/* eslint {jsx-a11y/anchor-has-content:"off"} */
import React, {Component} from 'react';
import {Card, List, Badge} from 'tabler-react';
import PropTypes from 'prop-types';
class FiddleResults extends Component {
constructor(props){
super(props);
this.state = {
errors: props.compilationResult.errors
};
}
static _removeClass(elems, className) {
for (let elem of elems) {
elem.className = elem.className.replace(className, '').replace(' ', ' ');
@ -28,14 +22,14 @@ class FiddleResults extends Component{
}
}
toggleCollapse(e) {
_toggleCollapse(e) {
const collapsedClassName = 'card-collapsed';
const className = e.currentTarget.parentElement.className.replace('card-options', '').replace(' ', '');
const elems = document.getElementsByClassName(className + '-card');
FiddleResults._toggleClass(elems, collapsedClassName);
}
toggleFullscreen(e) {
_toggleFullscreen(e) {
const collapsedClassName = 'card-collapsed';
const fullscreenClassName = 'card-fullscreen';
const className = e.currentTarget.parentElement.className.replace('card-options', '').replace(' ', '');
@ -44,80 +38,46 @@ class FiddleResults extends Component{
FiddleResults._removeClass(elems, collapsedClassName);
}
_getFormatted(errors, errorType){
const color = (errorType === "error" ? "danger" : errorType);
return <Card
isCollapsible={true}
isFullscreenable={true}
statusColor={color}
statusSide="true"
className={errorType + "s-card"}
key={errorType + "s-card"}>
<Card.Header>
<Card.Title color={color}>{errorType + "s"} <Badge color={color}>{errors.length}</Badge></Card.Title>
<Card.Options className={errorType + "s"}>
<Card.OptionsItem key="0" type="collapse" icon="chevron-up" onClick={this._toggleCollapse} />
<Card.OptionsItem key="1" type="fullscreen" icon="maximize" onClick={this._toggleFullscreen} />
</Card.Options>
</Card.Header>
<Card.Body>
<List.Group>
{errors.map(error => { return error.node; })}
</List.Group>
</Card.Body>
</Card>;
}
render() {
const warningObjs = this.props.compilationResult.errors.filter(error => {
return error.severity === 'warning';
});
const errorObjs = this.props.compilationResult.errors.filter(error => {
return error.severity === 'error';
});
const warnings = warningObjs.map((warning, index) => {
return (
<List.GroupItem key={index} action>
<Badge color="warning" className="mr-1" key={index}>
Lines {warning.sourceLocation.start}-{warning.sourceLocation.end}
</Badge>
{warning.formattedMessage}
</List.GroupItem>
);
});
const errors = errorObjs.map((error, index) => {
return (
<List.GroupItem key={index} action>
<Badge color="danger" className="mr-1" key={index}>
Lines {error.sourceLocation.start}-{error.sourceLocation.end}
</Badge>
{error.formattedMessage}
</List.GroupItem>
);
});
const errorsCard = <Card
isCollapsible={true}
isFullscreenable={true}
statusColor="red"
statusSide="true"
className="errors-card"
key="errors">
<Card.Header>
<Card.Title color="red">Errors <Badge color="danger" className="mr-1">{errors.length}</Badge></Card.Title>
<Card.Options className="errors">
<Card.OptionsItem key="0" type="collapse" icon="chevron-up" onClick={this.toggleCollapse}/>
<Card.OptionsItem key="1" type="fullscreen" icon="maximize" onClick={this.toggleFullscreen}/>
</Card.Options>
</Card.Header>
<Card.Body>
<List.Group>
{errors}
</List.Group>
</Card.Body>
</Card>;
const warningsCard = <Card
isCollapsible={true}
isFullscreenable={true}
statusColor="warning"
statusSide="true"
className="warnings-card"
key="warnings">
<Card.Header>
<Card.Title color="warning">Warnings <Badge color="warning" className="mr-1">{warnings.length}</Badge></Card.Title>
<Card.Options className="warnings">
<Card.OptionsItem key="0" type="collapse" icon="chevron-up" onClick={this.toggleCollapse}/>
<Card.OptionsItem key="1" type="fullscreen" icon="maximize" onClick={this.toggleFullscreen}/>
</Card.Options>
</Card.Header>
<Card.Body>
<List.Group>
{warnings}
</List.Group>
</Card.Body>
</Card>;
const {warnings, errors} = this.props;
let renderings = [];
if(!this.state.errors){
return 'Compilation successful (add green tick mark)';
}
if(errors.length) renderings.push(errorsCard);
if(warnings.length) renderings.push(warningsCard);
if (errors.length) renderings.push(
<React.Fragment key="errors">
<a id="errors" aria-hidden="true"/>
{this._getFormatted(errors, "error")}
</React.Fragment>
);
if (warnings.length) renderings.push(
<React.Fragment key="warnings">
<a id="warnings" aria-hidden="true"/>
{this._getFormatted(warnings, "warning")}
</React.Fragment>
);
return (
<React.Fragment>
@ -128,7 +88,8 @@ class FiddleResults extends Component{
}
FiddleResults.propTypes = {
compilationResult: PropTypes.object
errors: PropTypes.array,
warnings: PropTypes.array
};
export default FiddleResults;

View File

@ -0,0 +1,45 @@
import React, {Component} from 'react';
import {Badge} from 'tabler-react';
import PropTypes from 'prop-types';
class FiddleResultsSummary extends Component{
render(){
const {warnings, errors, isFetching, hasResult} = this.props;
let renderings = [];
if(isFetching){
renderings.push(
<React.Fragment key="compiling"><div className="loader"></div><span className="loader-text">Compiling...</span></React.Fragment>
);
}
if(hasResult && !errors.length){
renderings.push(<Badge key="success" className="badge-link" color="success">Compiled</Badge>);
}
if(errors.length) renderings.push(
<React.Fragment key="errors">
<a className="badge-link" href="#errors"><Badge color="danger">{errors.length} error{errors.length > 1 ? "s" : ""}</Badge></a>
</React.Fragment>
);
if(warnings.length) renderings.push(
<React.Fragment key="warnings">
<a className="badge-link" href="#warnings"><Badge color="warning">{warnings.length} warning{warnings.length > 1 ? "s" : ""}</Badge></a>
</React.Fragment>
);
return (
<div className={"compilation-summary " + ((hasResult || isFetching) ? "visible" : "")}>
{renderings}
{!(hasResult || isFetching) ? "&nbsp;" : ""}
</div>
);
}
}
FiddleResultsSummary.propTypes = {
errors: PropTypes.array,
warnings: PropTypes.array,
isFetching: PropTypes.bool,
hasResult: PropTypes.bool
};
export default FiddleResultsSummary;

View File

@ -6,6 +6,9 @@ import PropTypes from 'prop-types';
import {fetchCodeCompilation} from '../actions';
import Fiddle from '../components/Fiddle';
import FiddleResults from '../components/FiddleResults';
import FiddleReultsSummary from '../components/FiddleResultsSummary';
import {Badge} from 'tabler-react';
import scrollToComponent from 'react-scroll-to-component';
class FiddleContainer extends Component {
@ -15,6 +18,8 @@ class FiddleContainer extends Component {
value: ''
};
this.compileTimeout = null;
this.ace = null;
this.editor = null;
}
componentDidMount() {
@ -23,7 +28,7 @@ class FiddleContainer extends Component {
}
}
onCodeChange(newValue) {
_onCodeChange(newValue) {
this.setState({value: newValue});
if (this.compileTimeout) clearTimeout(this.compileTimeout);
this.compileTimeout = setTimeout(() => {
@ -32,17 +37,98 @@ class FiddleContainer extends Component {
}
_getFormattedErrors(errors, errorType){
return errors.reduce(
(errors, error, index) => {
if (error.severity === errorType) {
const errorRowCol = this._getRowCol(error.formattedMessage);
const annotation = Object.assign({}, {
row: errorRowCol.row - 1, // must be 0 based
column: errorRowCol.col - 1, // must be 0 based
text: error.formattedMessage, // text to show in tooltip
type: error.severity // "error"|"warning"|"info"
});
errors.push({
solcError: error,
node:
<a
href="#editor"
className="list-group-item list-group-item-action"
onClick={(e) => { this._onErrorClick(e, annotation); }}
key={index}
//ref={(item) => { this.refCallback(item, annotation); }}
>
<Badge color={errorType === "error" ? "danger" : errorType} className="mr-1" key={index}>
Line {errorRowCol.row}
</Badge>
{error.formattedMessage}
</a>,
annotation: annotation
});
}
return errors;
}, []);
}
_getRowCol(errorMessage){
const errorSplit = errorMessage.split(':');
if(errorSplit.length >= 3){
return {row: errorSplit[1], col: errorSplit[2]};
}
return {row: 0, col: 0};
}
_onErrorClick(e, annotation){
e.preventDefault();
this.editor.gotoLine(annotation.row + 1);
scrollToComponent(this.ace);
}
render() {
const {fiddles} = this.props;
let renderings = [<Fiddle key="0" value={this.state.value} onCodeChange={(n) => this.onCodeChange(n)} />];
let renderings = [];
let warnings = [];
let errors = [];
if (fiddles.compilationResult) {
renderings.push(<FiddleResults key="1" compilationResult={fiddles.compilationResult}/>);
warnings = this._getFormattedErrors(fiddles.compilationResult.errors, "warning");
errors = this._getFormattedErrors(fiddles.compilationResult.errors, "error");
}
renderings.push(
<React.Fragment key="fiddle">
<FiddleReultsSummary
errors={errors}
warnings={warnings}
isFetching={fiddles.isFetching}
hasResult={Boolean(fiddles.compilationResult)}
/>
<Fiddle
value={this.state.value}
onCodeChange={(n) => this._onCodeChange(n)}
errors={errors}
warnings={warnings}
ref={(fiddle) => {
if(fiddle) {
this.editor = fiddle.ace.editor;
this.ace = fiddle.ace;
}
}}
/>
</React.Fragment>
);
if (fiddles.compilationResult) {
renderings.push(
<FiddleResults
key="results"
errors={errors}
warnings={warnings}
/>);
}
else renderings.push('Nothing to compile');
return (
<React.Fragment>
<h1>Fiddle</h1>
<p>Play around with contract code and deploy against your running node.</p>
{renderings}
</React.Fragment>
);

View File

@ -25,5 +25,43 @@
white-space: pre-line;
}
.card.card-fullscreen{
z-index:4;
z-index:6;
}
.card.warnings-card, .card.errors-card{
text-transform: capitalize;
}
.card.warnings-card .list-group-item, .card.errors-card .list-group-item{
white-space: pre-line;
}
.card.warnings-card .card-options a, .card.errors-card .card-options a{
cursor:pointer;
}
.compilation-summary {
float:right;
margin-bottom:3px;
line-height:30px;
visibility: hidden;
}
.compilation-summary.visible{
visibility: visible;
}
.compilation-summary .badge-link:not(:last-child){
margin-right:5px;
}
.ace_editor {
margin-bottom:24px;
}
.loader, .loader:before, .loader:after{
width:1.2rem;
height:1.2rem;
}
.loader:before, .loader:after{
margin:-0.6rem 0 0 -0.6rem;
}
.loader, .loader-text{
display:inline-block;
vertical-align: middle;
}
.loader {
margin-right:5px;
}

View File

@ -1,11 +1,13 @@
import {RECEIVE_COMPILE_CODE, RECEIVE_COMPILE_CODE_ERROR} from "../actions";
import {COMPILE_CODE_REQUEST, COMPILE_CODE_FAILURE, COMPILE_CODE_SUCCESS} from "../actions";
export default function processes(state = {}, action) {
switch (action.type) {
case RECEIVE_COMPILE_CODE:
return Object.assign({}, state, {compilationResult: action.compilationResult});
case RECEIVE_COMPILE_CODE_ERROR:
return Object.assign({}, state, {error: true});
case COMPILE_CODE_REQUEST:
return {...state, isFetching: true, compilationResult: action.compilationResult};
case COMPILE_CODE_SUCCESS:
return {...state, isFetching: false, compilationResult: action.compilationResult};
case COMPILE_CODE_FAILURE:
return {...state, isFetching: false, error: true};
default:
return state;
}

View File

@ -170,7 +170,7 @@ export function *fetchCodeCompilation(action) {
}
export function *watchFetchCodeCompilation() {
yield takeEvery(actions.FETCH_COMPILE_CODE, fetchCodeCompilation);
yield takeEvery(actions.COMPILE_CODE_REQUEST, fetchCodeCompilation);
}
export default function *root() {

View File

@ -25,10 +25,7 @@ class Solidity {
'post',
'/embark-api/contract/compile',
(req, res) => {
console.log('=====> POST contract/compile, req = ' + JSON.stringify(req.body));
this.events.request("contract:compile", req.body.code, (errors, compilationResult) => {
console.log('=====> POST contract/compile, errors = ' + JSON.stringify(errors));
console.log('=====> POST contract/compile, compilationResult = ' + JSON.stringify(compilationResult));
res.send({errors:errors, compilationResult: compilationResult});
});
}