Refactor logic to FiddleContainer

Refactored fiddle logic to be contained in the `FiddleContainer` and the components as purely presentational.

Added scroll from summary to errors/warnings/fatal/deployed cards.

Added fatal error support (ie network error in api)

Removed `lodash`
This commit is contained in:
emizzle 2018-08-30 21:12:39 +10:00 committed by Pascal Precht
parent 05b324dffe
commit 6207023bec
No known key found for this signature in database
GPG Key ID: 0EE28D8D6FD85D7D
10 changed files with 299 additions and 277 deletions

View File

@ -8,7 +8,6 @@
"classnames": "^2.2.6",
"connected-react-router": "^4.3.0",
"history": "^4.7.2",
"lodash": "^4.17.10",
"prop-types": "^15.6.2",
"react": "^16.4.1",
"react-ace": "^6.1.4",

View File

@ -14,7 +14,7 @@ import ContractDeploymentContainer from '../containers/ContractDeploymentContain
import ContractProfileContainer from '../containers/ContractProfileContainer';
import ContractSourceContainer from '../containers/ContractSourceContainer';
const ContractLayout = ({match, contract}) => (
const ContractLayout = ({match, contractIsFiddle = false}) => (
<Grid.Row>
<Grid.Col md={3}>
<Page.Title className="my-5">&nbsp;</Page.Title>
@ -28,7 +28,7 @@ const ContractLayout = ({match, contract}) => (
>
Overview
</List.GroupItem>
{!contract.isFiddle ?
{!contractIsFiddle &&
<List.GroupItem
className="d-flex align-items-center"
to={`/embark/contracts/${match.params.contractName}/deployment`}
@ -37,8 +37,6 @@ const ContractLayout = ({match, contract}) => (
>
Deployment / Utils
</List.GroupItem>
:
''
}
<List.GroupItem
className="d-flex align-items-center"
@ -89,7 +87,8 @@ const ContractLayout = ({match, contract}) => (
);
ContractLayout.propTypes = {
match: PropTypes.object
match: PropTypes.object,
contractIsFiddle: PropTypes.bool
};
export default withRouter(ContractLayout);

View File

@ -1,159 +1,25 @@
/* eslint {jsx-a11y/anchor-has-content:"off"} */
import React, {Component} from 'react';
import {Card, List, Badge, Icon, Dimmer, Button} from 'tabler-react';
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import {NavLink} from 'react-router-dom';
class FiddleResults extends Component {
constructor(props){
super(props);
this.state = {
errorsCollapsed: false,
warningsCollapsed: false,
errorsFullscreen: false,
warningsFullscreen: false
};
}
_toggle(e, type){
const className = e.currentTarget.parentElement.className.replace('card-options', '').replace(' ', '');
const updatedState = {};
updatedState[className + type] = !(this.state[className + type]);
this.setState(updatedState);
}
_getFormatted(errors, errorType, loading){
const color = (errorType === "error" ? "danger" : errorType);
const isFullscreen = Boolean(this.state[errorType + 'sFullscreen']);
const classes = classNames({
'card-fullscreen': Boolean(this.state[errorType + 'sFullscreen']),
'card-collapsed': Boolean(this.state[errorType + 'sCollapsed']) && !isFullscreen
});
return <Card
isCollapsible={true}
isFullscreenable={true}
statusColor={color}
statusSide="true"
className={errorType + "s-card " + classes}
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={(e) => this._toggle(e, 'Collapsed')}/>
<Card.OptionsItem key="1" type="fullscreen" icon="maximize" onClick={(e) => this._toggle(e, 'Fullscreen')} />
</Card.Options>
</Card.Header>
<Card.Body>
<Dimmer active={loading ? "active" : ""} loader>
<List.Group>
{errors.map(error => { return error.node; })}
</List.Group>
</Dimmer>
</Card.Body>
</Card>;
}
render() {
const {warnings, errors, fatalFiddle, fatalFiddleDeploy, isLoading, deployedContracts} = this.props;
const hasFatal = fatalFiddle || fatalFiddleDeploy;
let renderings = [];
if(hasFatal){
if(fatalFiddle){
renderings.push(
<React.Fragment key="fatal-compile">
<a id="fatal-compile" aria-hidden="true"/>
<Card
statusColor="danger"
statusSide="true"
className="fatal-card">
<Card.Header>
<Card.Title color="danger"><Icon name="slash"/> Failed to compile</Card.Title>
</Card.Header>
<Card.Body>
<Dimmer active={isLoading ? "active" : ""} loader>
{fatalFiddle}
</Dimmer>
</Card.Body>
</Card>
</React.Fragment>
const FiddleResults = ({warningsCard, errorsCard, fatalFiddleCard, fatalFiddleDeployCard, deployedContractsCard, fatalErrorCard, forwardedRef}) => (
<div ref={forwardedRef}>
{fatalErrorCard}
{fatalFiddleCard}
{fatalFiddleDeployCard}
{deployedContractsCard}
{errorsCard}
{warningsCard}
</div>
);
}
if(fatalFiddleDeploy){
renderings.push(
<React.Fragment key="fatal-deploy">
<a id="fatal-deploy" aria-hidden="true"/>
<Card
statusColor="danger"
statusSide="true"
className="fatal-card">
<Card.Header>
<Card.Title color="danger"><Icon name="slash"/> Failed to deploy</Card.Title>
</Card.Header>
<Card.Body>
<Dimmer active={isLoading ? "active" : ""} loader>
{fatalFiddleDeploy}
</Dimmer>
</Card.Body>
</Card>
</React.Fragment>
);
}
}
else if (deployedContracts){
renderings.push(
<Card
statusColor="success"
statusSide="true"
className="success-card"
key="success-card">
<Card.Header>
<Card.Title color="success"><Icon name="check"/> Contract(s) deployed!</Card.Title>
</Card.Header>
<Card.Body>
<Dimmer active={isLoading ? "active" : ""} loader>
<Button
to={`/embark/contracts/${deployedContracts}/overview`}
RootComponent={NavLink}
>Play with my contract(s)</Button>
</Dimmer>
</Card.Body>
</Card>
);
}
else{
if (errors.length) renderings.push(
<React.Fragment key="errors">
<a id="errors" aria-hidden="true"/>
{this._getFormatted(errors, "error", isLoading)}
</React.Fragment>
);
if (warnings.length) renderings.push(
<React.Fragment key="warnings">
<a id="warnings" aria-hidden="true"/>
{this._getFormatted(warnings, "warning", isLoading)}
</React.Fragment>
);
}
return (
<React.Fragment>
{renderings}
</React.Fragment>
);
}
}
FiddleResults.propTypes = {
errors: PropTypes.array,
warnings: PropTypes.array,
fatalFiddle: PropTypes.string,
fatalFiddleDeploy: PropTypes.string,
isLoading: PropTypes.bool,
deployedContracts: PropTypes.string
errorsCard: PropTypes.node,
warningsCard: PropTypes.node,
fatalFiddleCard: PropTypes.node,
fatalFiddleDeployCard: PropTypes.node,
deployedContractsCard: PropTypes.node,
fatalErrorCard: PropTypes.node,
forwardedRef: PropTypes.any
};
export default FiddleResults;

View File

@ -1,71 +1,69 @@
import React, {Component} from 'react';
import {Badge, Icon} from 'tabler-react';
import {Badge, Icon, Loader} from 'tabler-react';
import PropTypes from 'prop-types';
import FiddleDeployButton from './FiddleDeployButton';
import classNames from 'classnames';
class FiddleResultsSummary extends Component {
_renderFatal(fatalType, title) {
return <a className="badge-link" href={`#fatal-${fatalType}`} onClick={(e) => this.props.onFatalClick(e)}><Badge color="danger"><Icon name="slash" className="mr-1" />{title}</Badge></a>;
}
_renderError(errorType, numErrors) {
const color = errorType === 'error' ? 'danger' : 'warning';
const clickAction = errorType === 'error' ? this.props.onWarningsClick : this.props.onErrorsClick;
return <a className="badge-link" href={`#${errorType}`} onClick={(e) => clickAction(e)}><Badge color={color}>{numErrors} {errorType}{numErrors > 1 ? "s" : ""}</Badge></a>;
}
render() {
const {warnings, errors, isLoading, loadingMessage, hasResult, fatalFiddle, fatalFiddleDeploy} = this.props;
let renderings = [];
if(isLoading){
renderings.push(
<React.Fragment key="loading"><div className="loader"></div><span className="loader-text">{loadingMessage}</span></React.Fragment>
);
}
if(fatalFiddle) {
renderings.push(
<React.Fragment key="errors">
<a className="badge-link" href="#fatal-compile"><Badge color="danger"><Icon name="slash"/> Compilation</Badge></a>
</React.Fragment>
);
}
const {numWarnings, numErrors, isLoading, loadingMessage, isVisible, showDeploy, showFatalFiddle, showFatalFiddleDeploy, showFatalError} = this.props;
const classes = classNames("compilation-summary", {
'visible': isVisible
});
if(fatalFiddleDeploy) {
renderings.push(
<React.Fragment key="errors">
<a className="badge-link" href="#fatal-deploy"><Badge color="danger"><Icon name="slash"/> Deployment</Badge></a>
</React.Fragment>
);
}
return (
<div className={classes}>
{isLoading &&
<Loader className="mr-1">
<span className="loader-text">{loadingMessage}</span>
</Loader>}
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>
);
if(hasResult && !errors.length){
renderings.push(
{showFatalError && this._renderFatal("error", "Error")}
{showFatalFiddle && this._renderFatal("compile", "Compilation")}
{showFatalFiddleDeploy && this._renderFatal("deploy", "Deployment")}
{numErrors > 0 && this._renderError("error", numErrors)}
{numWarnings > 0 && this._renderError("warning", numWarnings)}
{showDeploy &&
<React.Fragment key="success">
<Badge className="badge-link" color="success">Compiled</Badge>
<FiddleDeployButton onDeployClick={(e) => this.props.onDeployClick(e)} />
</React.Fragment>
);
}
return (
<div className={"compilation-summary " + ((hasResult || isLoading) ? "visible" : "")}>
{renderings}
{!(hasResult || isLoading) ? "&nbsp;" : ""}
</div>
);
}
}
FiddleResultsSummary.propTypes = {
errors: PropTypes.array,
warnings: PropTypes.array,
isLoading: PropTypes.bool,
loadingMessage: PropTypes.string,
hasResult: PropTypes.bool,
fatalFiddle: PropTypes.string,
fatalFiddleDeploy: PropTypes.string,
onDeployClick: PropTypes.func
isVisible: PropTypes.bool,
showDeploy: PropTypes.bool,
showFatalError: PropTypes.bool,
showFatalFiddle: PropTypes.bool,
showFatalFiddleDeploy: PropTypes.bool,
onDeployClick: PropTypes.func,
numErrors: PropTypes.number,
numWarnings: PropTypes.number,
onWarningsClick: PropTypes.func.isRequired,
onErrorsClick: PropTypes.func.isRequired,
onFatalClick: PropTypes.func.isRequired
};
export default FiddleResultsSummary;

View File

@ -0,0 +1,80 @@
/* eslint {jsx-a11y/anchor-has-content:"off"} */
import React from 'react';
import PropTypes from 'prop-types';
import {Card, Icon, Dimmer} from 'tabler-react';
import classNames from 'classnames';
class LoadingCardWithIcon extends React.Component {
constructor(props) {
super(props);
this.state = {
errorsCollapsed: false,
warningsCollapsed: false,
errorsFullscreen: false,
warningsFullscreen: false
};
}
_onToggle(e, type) {
const className = e.currentTarget.parentElement.className.replace('card-options', '').replace(' ', '');
const updatedState = {};
updatedState[className + type] = !(this.state[className + type]);
this.setState(updatedState);
}
render() {
const {
color,
className,
iconName,
headerTitle,
isLoading,
body,
showCardOptions = true,
cardOptionsClassName} = this.props;
const isFullscreen = Boolean(this.state[cardOptionsClassName + 'Fullscreen']);
const classes = classNames(className, {
'card-fullscreen': showCardOptions && Boolean(this.state[cardOptionsClassName + 'Fullscreen']),
'card-collapsed': showCardOptions && Boolean(this.state[cardOptionsClassName + 'Collapsed']) && !isFullscreen
});
return (
<Card
statusColor={color}
statusSide="true"
className={classes}
isCollapsible={showCardOptions}
isFullscreenable={showCardOptions}>
<Card.Header>
<Card.Title color={color}>{iconName && <Icon name={iconName} className="mr-1" />}{headerTitle}</Card.Title>
{showCardOptions &&
<Card.Options className={cardOptionsClassName}>
<Card.OptionsItem key="0" type="collapse" icon="chevron-up" onClick={(e) => this._onToggle(e, 'Collapsed')} />
<Card.OptionsItem key="1" type="fullscreen" icon="maximize" onClick={(e) => this._onToggle(e, 'Fullscreen')} />
</Card.Options>
}
</Card.Header>
<Card.Body>
<Dimmer active={isLoading ? "active" : ""} loader>
{body}
</Dimmer>
</Card.Body>
</Card>
);
}
}
LoadingCardWithIcon.propTypes = {
color: PropTypes.string.isRequired,
className: PropTypes.string.isRequired,
iconName: PropTypes.string,
headerTitle: PropTypes.any,
isLoading: PropTypes.bool.isRequired,
body: PropTypes.node,
showCardOptions: PropTypes.bool,
onOptionToggle: PropTypes.func,
cardOptionsClassName: PropTypes.string
};
export default LoadingCardWithIcon;

View File

@ -14,7 +14,7 @@ class ContractLayoutContainer extends Component {
render() {
if (this.props.contract){
return <ContractLayout contract={this.props.contract} />;
return <ContractLayout contractIsFiddle={this.props.contract.isFiddle} />;
} else {
return <React.Fragment />;
}

View File

@ -14,6 +14,10 @@ import FiddleResultsSummary from '../components/FiddleResultsSummary';
import scrollToComponent from 'react-scroll-to-component';
import {getFiddle, getFiddleDeploy} from "../reducers/selectors";
import CompilerError from "../components/CompilerError";
import {List, Badge, Button} from 'tabler-react';
import {NavLink} from 'react-router-dom';
import LoadingCardWithIcon from '../components/LoadingCardWithIcon';
import {hashCode} from '../utils/utils';
class FiddleContainer extends Component {
@ -22,12 +26,16 @@ class FiddleContainer extends Component {
this.state = {
value: undefined,
loadingMessage: 'Loading...',
readOnly: true
};
this.compileTimeout = null;
this.ace = null;
this.editor = null;
this.warningsCardRef = null;
this.errorsCardRef = null;
this.fatalCardRef = null;
this.deployedCardRef = null;
this.fiddleResultsRef = React.createRef();
}
componentDidMount() {
@ -43,6 +51,14 @@ class FiddleContainer extends Component {
}
}
_getRowCol(errorMessage) {
const errorSplit = errorMessage.split(':');
if (errorSplit.length >= 3) {
return {row: errorSplit[1], col: errorSplit[2]};
}
return {row: 0, col: 0};
}
_onCodeChange(newValue, immediate = false) {
this.setState({readOnly: false, value: newValue});
if (this.compileTimeout) clearTimeout(this.compileTimeout);
@ -50,10 +66,25 @@ class FiddleContainer extends Component {
this.setState({loadingMessage: 'Compiling...'});
this.props.postFiddle(newValue, Date.now());
}, immediate ? 0 : 1000);
}
_getFormattedErrors(errors, errorType){
_onErrorClick(e, annotation) {
e.preventDefault();
this.editor.gotoLine(annotation.row + 1);
scrollToComponent(this.ace);
}
_onErrorSummaryClick(e, refName) {
scrollToComponent(this[refName]);
}
_onDeployClick(_e) {
this.setState({loadingMessage: 'Deploying...'});
this.props.postFiddleDeploy(this.props.fiddle.compilationResult);
scrollToComponent(this.deployedCardRef || this.fiddleResultsRef.current); // deployedCardRef null on first Deploy click
}
_renderErrors(errors, errorType) {
return errors.reduce(
(errors, error, index) => {
if (error.severity === errorType) {
@ -81,46 +112,85 @@ class FiddleContainer extends Component {
}, []);
}
_getRowCol(errorMessage){
const errorSplit = errorMessage.split(':');
if(errorSplit.length >= 3){
return {row: errorSplit[1], col: errorSplit[2]};
_renderErrorsCard(errors, errorType) {
const color = (errorType === "error" ? "danger" : errorType);
return (Boolean(errors.length) && <LoadingCardWithIcon
anchorId={errorType + "s"}
color={color}
className={errorType + "s-card "}
key={errorType + "s-card"}
showCardOptions={true}
isLoading={this.props.loading}
cardOptionsClassName={errorType + "s"}
body={
<List.Group>
{errors.map(error => { return error.node; })}
</List.Group>
}
return {row: 0, col: 0};
headerTitle={
<React.Fragment>
<span className="mr-1">{errorType + "s"}</span><Badge color={color}>{errors.length}</Badge>
</React.Fragment>
}
ref={cardRef => { this[errorType + "sCardRef"] = cardRef; }}
/>);
}
_onErrorClick(e, annotation){
e.preventDefault();
this.editor.gotoLine(annotation.row + 1);
scrollToComponent(this.ace);
_renderSuccessCard(title, body) {
return this._renderLoadingCard("success", "success-card", "check", title, body, (cardRef) => {
this.deployedCardRef = cardRef;
});
}
_onDeployClick(_e){
this.setState({loadingMessage: 'Deploying...'});
this.props.postFiddleDeploy(this.props.fiddle.compilationResult);
_renderFatalCard(title, body) {
return body && this._renderLoadingCard("danger", "fatal-card", "slash", title, body, (cardRef) => {
this.fatalCardRef = cardRef;
});
}
_renderLoadingCard(color, className, iconName, headerTitle, body, refCb) {
return (<LoadingCardWithIcon
color={color}
className={className}
iconName={iconName}
showCardOptions={false}
isLoading={this.props.loading}
body={body}
headerTitle={headerTitle}
key={hashCode([className, iconName, headerTitle].join(''))}
ref={refCb}
/>);
}
render() {
const {fiddle, loading, fiddleError, fiddleDeployError, deployedContracts} = this.props;
const {fiddle, loading, fiddleError, fiddleDeployError, deployedContracts, fatalError} = this.props;
const {loadingMessage, value, readOnly} = this.state;
let renderings = [];
let warnings = [];
let errors = [];
if (fiddle && fiddle.errors) {
warnings = this._getFormattedErrors(fiddle.errors, "warning");
errors = this._getFormattedErrors(fiddle.errors, "error");
warnings = this._renderErrors(fiddle.errors, "warning");
errors = this._renderErrors(fiddle.errors, "error");
}
renderings.push(
<React.Fragment key="fiddle">
const hasResult = Boolean(fiddle);
return (
<React.Fragment>
<h1 className="page-title">Fiddle</h1>
<p>Play around with contract code and deploy against your running node.</p>
<FiddleResultsSummary
errors={errors}
warnings={warnings}
numErrors={errors.length}
numWarnings={warnings.length}
isLoading={loading}
loadingMessage={loadingMessage}
hasResult={Boolean(fiddle)}
fatalFiddle={fiddleError}
fatalFiddleDeploy={fiddleDeployError}
showFatalError={Boolean(fatalError)}
showFatalFiddle={Boolean(fiddleError)}
showFatalFiddleDeploy={Boolean(fiddleDeployError)}
onDeployClick={(e) => this._onDeployClick(e)}
isVisible={Boolean(fatalError || hasResult || loading)}
showDeploy={hasResult && !errors.length}
onWarningsClick={(e) => this._onErrorSummaryClick(e, "errorsCardRef")}
onErrorsClick={(e) => this._onErrorSummaryClick(e, "warningsCardRef")}
onFatalClick={(e) => this._onErrorSummaryClick(e, "fatalCardRef")}
/>
<Fiddle
value={value}
@ -135,26 +205,21 @@ class FiddleContainer extends Component {
}
}}
/>
</React.Fragment>
);
if (fiddle || (this.state.value && (fiddleError || fiddleDeployError))) {
renderings.push(
<FiddleResults
key="results"
errors={errors}
warnings={warnings}
fatalFiddle={fiddleError}
fatalFiddleDeploy={fiddleDeployError}
isLoading={loading}
deployedContracts={deployedContracts}
/>);
}
return (
<React.Fragment>
<h1 className="page-title">Fiddle</h1>
<p>Play around with contract code and deploy against your running node.</p>
{renderings}
errorsCard={this._renderErrorsCard(errors, "error")}
warningsCard={this._renderErrorsCard(warnings, "warning")}
fatalErrorCard={this._renderFatalCard("Fatal error", fatalError)}
fatalFiddleCard={this._renderFatalCard("Failed to compile", fiddleError)}
fatalFiddleDeployCard={this._renderFatalCard("Failed to deploy", fiddleDeployError)}
deployedContractsCard={deployedContracts && this._renderSuccessCard("Contract(s) deployed!",
<Button
to={`/embark/contracts/${deployedContracts}/overview`}
RootComponent={NavLink}
>Play with my contract(s)</Button>
)}
forwardedRef={this.fiddleResultsRef}
/>
</React.Fragment>
);
}
@ -168,7 +233,8 @@ function mapStateToProps(state) {
fiddleError: fiddle.error,
fiddleDeployError: deployedFiddle.error,
loading: state.loading,
lastFiddle: fiddle.data ? fiddle.data.codeToCompile : undefined
lastFiddle: fiddle.data ? fiddle.data.codeToCompile : undefined,
fatalError: state.errorMessage
};
}
@ -181,7 +247,8 @@ FiddleContainer.propTypes = {
postFiddleDeploy: PropTypes.func,
deployedContracts: PropTypes.string,
fetchLastFiddle: PropTypes.func,
lastFiddle: PropTypes.any
lastFiddle: PropTypes.any,
fatalError: PropTypes.string
};
export default connect(

View File

@ -37,7 +37,6 @@
.compilation-summary {
float: right;
margin-bottom: 3px;
line-height: 30px;
visibility: hidden;
}
.compilation-summary.visible{
@ -55,11 +54,11 @@
}
.loader:before, .loader:after{
margin: -0.6rem 0 0 -0.6rem;
left: 0;
}
.loader, .loader-text{
display: inline-block;
vertical-align: middle;
}
.loader {
margin-right: 5px;
vertical-align: top;
padding-left: 0.6rem;
width: auto;
}

View File

@ -1,4 +1,4 @@
import _ from 'lodash';
import {last} from '../utils/utils';
export function getAccounts(state) {
return state.entities.accounts;
@ -109,7 +109,7 @@ export function getMessages(state) {
}
export function getFiddle(state) {
const fiddleCompilation = _.last(state.entities.fiddles.sort((a, b) => { return (a.timestamp || 0) - (b.timestamp || 0); }));
const fiddleCompilation = last(state.entities.fiddles.sort((a, b) => { return (a.timestamp || 0) - (b.timestamp || 0); }));
const isNoTempFileError = Boolean(fiddleCompilation && fiddleCompilation.codeToCompile && fiddleCompilation.codeToCompile.error && fiddleCompilation.codeToCompile.error.indexOf('ENOENT') > -1);
return {
data: fiddleCompilation,
@ -119,7 +119,7 @@ export function getFiddle(state) {
export function getFiddleDeploy(state) {
return {
data: _.last(state.entities.fiddleDeploys),
data: last(state.entities.fiddleDeploys),
error: state.errorEntities.fiddleDeploys
};
}

View File

@ -0,0 +1,14 @@
export function last(array) {
return array && array.length ? array[array.length - 1] : undefined;
}
/* eslint no-bitwise: "off" */
export function hashCode(str) {
let hash = 0;
if (str.length === 0) return hash;
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash; // Convert to 32bit integer
}
return hash;
}