feat(@cockpit): implement pagination for contracts

Display five contracts per page in the dashboard. Display ten contracts per
page in the contracts explorer and deployment page.

Sort contracts by name. In the future we can implement an option to sort by
block number and index within a block by calculating and including that
information as part of the server-side api response (based on a contract's
txhash).

Remove unnecessary contract filtering in the components since the containers
take care of it.

Make use of `listenToContracts` / `stopContracts` in DeploymentContainer.
This commit is contained in:
Michael Bradley, Jr 2019-04-04 12:44:55 -05:00 committed by Michael Bradley
parent 07b2ecc448
commit d71352b781
7 changed files with 310 additions and 149 deletions

View File

@ -1,12 +1,13 @@
import PropTypes from "prop-types";
import React from 'react'; import React from 'react';
import {Row, Col, Card, CardHeader, CardBody} from "reactstrap"; import {Row, Col, Card, CardHeader, CardBody} from "reactstrap";
import {Link} from 'react-router-dom'; import {Link} from 'react-router-dom';
import PropTypes from "prop-types";
import Pagination from './Pagination';
import {formatContractForDisplay} from '../utils/presentation'; import {formatContractForDisplay} from '../utils/presentation';
import CardTitleIdenticon from './CardTitleIdenticon'; import CardTitleIdenticon from './CardTitleIdenticon';
const Contracts = ({contracts, title = "Contracts"}) => ( const Contracts = ({contracts, changePage, currentPage, numberOfPages, title = "Contracts"}) => (
<Row> <Row>
<Col> <Col>
<Card> <Card>
@ -14,36 +15,37 @@ const Contracts = ({contracts, title = "Contracts"}) => (
<h2>{title}</h2> <h2>{title}</h2>
</CardHeader> </CardHeader>
<CardBody> <CardBody>
{ {!contracts.length && "No contracts to display"}
contracts {contracts
.filter(contract => !contract.silent) .map((contract, key) => {
.map((contract, key) => { const contractDisplay = formatContractForDisplay(contract);
const contractDisplay = formatContractForDisplay(contract); if (!contractDisplay) {
if (!contractDisplay) { return '';
return ''; }
}
return ( return (
<div className="explorer-row border-top" key={`contract-${key}`}> <div className="explorer-row border-top" key={`contract-${key}`}>
<CardTitleIdenticon id={contract.className}> <CardTitleIdenticon id={contract.className}>
<Link to={`/explorer/contracts/${contract.className}`}>{contract.className}</Link> <Link to={`/explorer/contracts/${contract.className}`}>
</CardTitleIdenticon> {contract.className}
<Row> </Link>
<Col> </CardTitleIdenticon>
<strong>Address</strong> <Row>
<div>{contract.address}</div> <Col>
</Col> <strong>Address</strong>
<Col> <div>{contract.address}</div>
<strong>State</strong> </Col>
<div className={contractDisplay.stateColor}> <Col>
{contractDisplay.state} <strong>State</strong>
</div> <div className={contractDisplay.stateColor}>
</Col> {contractDisplay.state}
</Row> </div>
</div> </Col>
) </Row>
}) </div>
} )
})}
{numberOfPages > 1 && <Pagination changePage={changePage} currentPage={currentPage} numberOfPages={numberOfPages}/>}
</CardBody> </CardBody>
</Card> </Card>
</Col> </Col>
@ -52,6 +54,9 @@ const Contracts = ({contracts, title = "Contracts"}) => (
Contracts.propTypes = { Contracts.propTypes = {
contracts: PropTypes.array, contracts: PropTypes.array,
changePage: PropTypes.func,
currentPage: PropTypes.number,
numberOfPages: PropTypes.number,
title: PropTypes.string title: PropTypes.string
}; };

View File

@ -1,19 +1,18 @@
import PropTypes from "prop-types";
import React from 'react'; import React from 'react';
import FontAwesomeIcon from 'react-fontawesome'; import FontAwesomeIcon from 'react-fontawesome';
import { import {Row,
Row, Col,
Col, FormGroup,
FormGroup, Input,
Input, Label,
Label, UncontrolledTooltip,
UncontrolledTooltip, Button,
Button, Card,
Card, CardHeader,
CardHeader, CardTitle,
CardTitle, CardBody} from 'reactstrap';
CardBody import PropTypes from "prop-types";
} from 'reactstrap'; import Pagination from './Pagination';
import classNames from 'classnames'; import classNames from 'classnames';
import {DEPLOYMENT_PIPELINES} from '../constants'; import {DEPLOYMENT_PIPELINES} from '../constants';
import Description from './Description'; import Description from './Description';
@ -326,39 +325,49 @@ class ContractsDeployment extends React.Component {
} }
render() { render() {
const props = this.props;
const {changePage, currentPage, numberOfPages} = props;
return ( return (
<Row> <React.Fragment>
<Col> <Row>
<ContractsHeader deploymentPipeline={this.props.deploymentPipeline} <Col>
updateDeploymentPipeline={this.props.updateDeploymentPipeline}/> <ContractsHeader
{this.props.contracts.filter(contract => (contract.code || contract.deploy) && !contract.silent) deploymentPipeline={props.deploymentPipeline}
.sort((a, b) => a.index - b.index).map((contract, index) => { updateDeploymentPipeline={props.updateDeploymentPipeline} />
{!props.contracts.length && "No contracts to display"}
{props.contracts
.map((contract, index) => {
contract.deployIndex = index; contract.deployIndex = index;
return (<Contract key={contract.deployIndex} return (
contract={contract} <Contract key={contract.deployIndex}
toggleContractOverview={(contract) => this.toggleContractOverview(contract)} contract={contract}
{...this.props} />); toggleContractOverview={(contract) => this.toggleContractOverview(contract)}
} {...props} />
)} );
</Col> })}
{this.isContractOverviewOpen() &&
<Col xs={6} md={3}>
<Card>
<CardBody>
<h2>{this.state.currentContractOverview.className} - Overview</h2>
<ContractOverviewContainer contract={this.state.currentContractOverview} />
</CardBody>
</Card>
</Col> </Col>
} {this.isContractOverviewOpen() &&
</Row> <Col xs={6} md={3}>
<Card>
<CardBody>
<h2>{this.state.currentContractOverview.className} - Overview</h2>
<ContractOverviewContainer contract={this.state.currentContractOverview} />
</CardBody>
</Card>
</Col>
}
</Row>
{numberOfPages > 1 && <Pagination changePage={changePage} currentPage={currentPage} numberOfPages={numberOfPages}/>}
</React.Fragment>
) )
} }
} }
ContractsDeployment.propTypes = { ContractsDeployment.propTypes = {
contracts: PropTypes.array, contracts: PropTypes.array,
changePage: PropTypes.func,
currentPage: PropTypes.number,
numberOfPages: PropTypes.number,
deploymentPipeline: PropTypes.oneOfType([ deploymentPipeline: PropTypes.oneOfType([
PropTypes.object, PropTypes.object,
PropTypes.string PropTypes.string
@ -373,4 +382,3 @@ ContractsDeployment.propTypes = {
}; };
export default ContractsDeployment; export default ContractsDeployment;

View File

@ -1,42 +1,49 @@
import PropTypes from "prop-types";
import React from 'react'; import React from 'react';
import {Table} from "reactstrap"; import {Table} from "reactstrap";
import {Link} from 'react-router-dom'; import {Link} from 'react-router-dom';
import PropTypes from "prop-types";
import Pagination from './Pagination';
import {formatContractForDisplay} from '../utils/presentation'; import {formatContractForDisplay} from '../utils/presentation';
const ContractsList = ({contracts}) => ( const ContractsList = ({contracts, changePage, currentPage, numberOfPages}) => (
<Table hover responsive className="table-outline mb-0 d-none d-sm-table text-nowrap"> <React.Fragment>
<thead className="thead-light"> {!contracts.length && "No contracts to display"}
<tr> <Table hover responsive className="table-outline mb-0 d-none d-sm-table text-nowrap">
<th>Name</th> <thead className="thead-light">
<th>Address</th> <tr>
<th>State</th> <th>Name</th>
</tr> <th>Address</th>
</thead> <th>State</th>
<tbody> </tr>
{ </thead>
contracts <tbody>
.filter(contract => !contract.silent) {
.map((contract) => { contracts
const contractDisplay = formatContractForDisplay(contract); .map((contract) => {
if (!contractDisplay) { const contractDisplay = formatContractForDisplay(contract);
return null; if (!contractDisplay) {
} return null;
return ( }
<tr key={contract.className} className={contractDisplay.stateColor}> return (
<td><Link to={`/explorer/contracts/${contract.className}`}>{contract.className}</Link></td> <tr key={contract.className} className={contractDisplay.stateColor}>
<td>{contractDisplay.address}</td> <td><Link to={`/explorer/contracts/${contract.className}`}>{contract.className}</Link></td>
<td>{contractDisplay.state}</td> <td>{contractDisplay.address}</td>
</tr> <td>{contractDisplay.state}</td>
); </tr>
}) );
} })
</tbody> }
</Table> </tbody>
</Table>
{numberOfPages > 1 && <Pagination changePage={changePage} currentPage={currentPage} numberOfPages={numberOfPages}/>}
</React.Fragment>
); );
ContractsList.propTypes = { ContractsList.propTypes = {
contracts: PropTypes.array, contracts: PropTypes.array,
changePage: PropTypes.func,
currentPage: PropTypes.number,
numberOfPages: PropTypes.number
}; };
export default ContractsList; export default ContractsList;

View File

@ -1,19 +1,24 @@
import React, {Component} from 'react'; import React, {Component} from 'react';
import {connect} from 'react-redux'; import {connect} from 'react-redux';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { import {contracts as contractsAction,
listenToContracts as listenToContractsAction, listenToContracts,
stopContracts as stopContractsAction, stopContracts} from "../actions";
contracts as contractsAction
} from "../actions";
import Contracts from '../components/Contracts'; import Contracts from '../components/Contracts';
import ContractsList from '../components/ContractsList'; import ContractsList from '../components/ContractsList';
import DataWrapper from "../components/DataWrapper";
import PageHead from "../components/PageHead";
import {getContracts} from "../reducers/selectors"; import {getContracts} from "../reducers/selectors";
import PageHead from "../components/PageHead";
const MAX_CONTRACTS = 10;
class ContractsContainer extends Component { class ContractsContainer extends Component {
constructor(props) {
super(props);
this.numContractsToDisplay = this.props.numContractsToDisplay || MAX_CONTRACTS;
this.state = {currentPage: 1};
}
componentDidMount() { componentDidMount() {
this.props.fetchContracts(); this.props.fetchContracts();
this.props.listenToContracts(); this.props.listenToContracts();
@ -23,14 +28,72 @@ class ContractsContainer extends Component {
this.props.stopContracts(); this.props.stopContracts();
} }
get numberOfContracts() {
if (this._numberOfContracts === undefined) {
this._numberOfContracts = this.props.contracts
.filter(contract => !contract.silent)
.length;
}
return this._numberOfContracts;
}
get numberOfPages() {
if (this._numberOfPages === undefined) {
this._numberOfPages = Math.ceil(
this.numberOfContracts / this.numContractsToDisplay
);
}
return this._numberOfPages;
}
resetNums() {
this._numberOfContracts = undefined;
this._numberOfPages = undefined;
}
changePage(newPage) {
if (newPage <= 0) {
newPage = 1;
} else if (newPage > this.numberOfPages) {
newPage = this.numberOfPages;
}
this.setState({ currentPage: newPage });
this.props.fetchContracts();
}
get currentContracts() {
let offset = 0;
return this.props.contracts
.filter((contract, arrIndex) => {
if (contract.silent) {
offset++;
return false
};
const index = (
(arrIndex + 1 - offset) -
(this.numContractsToDisplay * (this.state.currentPage - 1))
);
return index <= this.numContractsToDisplay && index > 0;
});
}
render() { render() {
this.resetNums();
let ContractsComp;
if (this.props.mode === "detail") {
ContractsComp = Contracts
} else if (this.props.mode === "list") {
ContractsComp = ContractsList
}
return ( return (
<React.Fragment> <React.Fragment>
{this.props.updatePageHeader && <PageHead title="Contracts" description="Summary of all deployed contracts" />} {this.props.updatePageHeader &&
<DataWrapper shouldRender={this.props.contracts.length > 0} {...this.props} render={({contracts}) => { <PageHead title="Contracts"
if (this.props.mode === "list") return <ContractsList contracts={contracts} />; description="Summary of all deployed contracts" />}
if (this.props.mode === "detail") return <Contracts contracts={contracts} />; <ContractsComp contracts={this.currentContracts}
}} /> numberOfPages={this.numberOfPages}
changePage={(newPage) => this.changePage(newPage)}
currentPage={this.state.currentPage} />
</React.Fragment> </React.Fragment>
); );
} }
@ -40,15 +103,17 @@ function mapStateToProps(state) {
return { return {
contracts: getContracts(state), contracts: getContracts(state),
error: state.errorMessage, error: state.errorMessage,
loading: state.loading}; loading: state.loading
};
} }
ContractsContainer.propTypes = { ContractsContainer.propTypes = {
contracts: PropTypes.array,
fetchContracts: PropTypes.func,
numContractsToDisplay: PropTypes.number,
listenToContracts: PropTypes.func, listenToContracts: PropTypes.func,
stopContracts: PropTypes.func, stopContracts: PropTypes.func,
contracts: PropTypes.array,
fiddleContracts: PropTypes.array, fiddleContracts: PropTypes.array,
fetchContracts: PropTypes.func,
mode: PropTypes.string, mode: PropTypes.string,
updatePageHeader: PropTypes.bool updatePageHeader: PropTypes.bool
}; };
@ -59,9 +124,9 @@ ContractsContainer.defaultProps = {
} }
export default connect( export default connect(
mapStateToProps,{ mapStateToProps, {
listenToContracts: listenToContractsAction, fetchContracts: contractsAction.request,
stopContracts: stopContractsAction, listenToContracts,
fetchContracts: contractsAction.request stopContracts,
} }
)(ContractsContainer); )(ContractsContainer);

View File

@ -1,44 +1,107 @@
import React, {Component} from 'react'; import React, {Component} from 'react';
import {connect} from 'react-redux'; import {connect} from 'react-redux';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { import {contracts as contractsAction,
contracts as contractsAction, listenToContracts,
web3Deploy as web3DeployAction, stopContracts,
web3EstimateGas as web3EstimateGasAction, web3Deploy as web3DeployAction,
updateDeploymentPipeline} from "../actions"; web3EstimateGas as web3EstimateGasAction,
updateDeploymentPipeline} from "../actions";
import ContractsDeployment from '../components/ContractsDeployment'; import ContractsDeployment from '../components/ContractsDeployment';
import DataWrapper from "../components/DataWrapper"; import {getContracts,
getDeploymentPipeline,
getWeb3,
getWeb3GasEstimates,
getWeb3Deployments,
getWeb3ContractsDeployed} from "../reducers/selectors";
import PageHead from '../components/PageHead'; import PageHead from '../components/PageHead';
import {
getContracts, const MAX_CONTRACTS = 10;
getDeploymentPipeline,
getWeb3,
getWeb3GasEstimates,
getWeb3Deployments,
getWeb3ContractsDeployed
} from "../reducers/selectors";
class DeploymentContainer extends Component { class DeploymentContainer extends Component {
constructor(props) {
super(props);
this.numContractsToDisplay = this.props.numContractsToDisplay || MAX_CONTRACTS;
this.state = {currentPage: 1};
}
componentDidMount() { componentDidMount() {
this.props.fetchContracts(); this.props.fetchContracts();
this.props.listenToContracts();
}
componentWillUnmount() {
this.props.stopContracts();
}
get numberOfContracts() {
if (this._numberOfContracts === undefined) {
this._numberOfContracts = this.props.contracts
.filter(contract => (contract.code || contract.deploy) && !contract.silent)
.length;
}
return this._numberOfContracts;
}
get numberOfPages() {
if (this._numberOfPages === undefined) {
this._numberOfPages = Math.ceil(
this.numberOfContracts / this.numContractsToDisplay
);
}
return this._numberOfPages;
}
resetNums() {
this._numberOfContracts = undefined;
this._numberOfPages = undefined;
}
changePage(newPage) {
if (newPage <= 0) {
newPage = 1;
} else if (newPage > this.numberOfPages) {
newPage = this.numberOfPages;
}
this.setState({ currentPage: newPage });
this.props.fetchContracts();
}
get currentContracts() {
let offset = 0;
return this.props.contracts
.filter((contract, arrIndex) => {
if (!(contract.code || contract.deploy) || contract.silent) {
offset++;
return false
};
const index = (
(arrIndex + 1 - offset) -
(this.numContractsToDisplay * (this.state.currentPage - 1))
);
return index <= this.numContractsToDisplay && index > 0;
});
} }
render() { render() {
this.resetNums();
return ( return (
<React.Fragment> <React.Fragment>
<PageHead title="Deployment" description="Deploy your contracts using Embark or a web3-enabled browser such as Mist or MetaMask." /> <PageHead title="Deployment"
<DataWrapper shouldRender={this.props.contracts.length > 0} {...this.props} render={() => ( description="Deploy your contracts using Embark or a web3-enabled browser such as Mist or MetaMask." />
<ContractsDeployment contracts={this.props.contracts} <ContractsDeployment contracts={this.currentContracts}
deploymentPipeline={this.props.deploymentPipeline} deploymentPipeline={this.props.deploymentPipeline}
web3={this.props.web3} web3={this.props.web3}
web3Deploy={this.props.web3Deploy} web3Deploy={this.props.web3Deploy}
web3EstimateGas={this.props.web3EstimateGas} web3EstimateGas={this.props.web3EstimateGas}
web3Deployments={this.props.web3Deployments} web3Deployments={this.props.web3Deployments}
web3GasEstimates={this.props.web3GasEstimates} web3GasEstimates={this.props.web3GasEstimates}
web3ContractsDeployed={this.props.web3ContractsDeployed} web3ContractsDeployed={this.props.web3ContractsDeployed}
updateDeploymentPipeline={this.props.updateDeploymentPipeline} /> updateDeploymentPipeline={this.props.updateDeploymentPipeline}
)} /> numberOfPages={this.numberOfPages}
changePage={(newPage) => this.changePage(newPage)}
currentPage={this.state.currentPage} />
</React.Fragment> </React.Fragment>
); );
} }
@ -59,11 +122,14 @@ function mapStateToProps(state) {
DeploymentContainer.propTypes = { DeploymentContainer.propTypes = {
contracts: PropTypes.array, contracts: PropTypes.array,
fetchContracts: PropTypes.func,
numContractsToDisplay: PropTypes.number,
listenToContracts: PropTypes.func,
stopContracts: PropTypes.func,
deploymentPipeline: PropTypes.oneOfType([ deploymentPipeline: PropTypes.oneOfType([
PropTypes.object, PropTypes.object,
PropTypes.string PropTypes.string
]), ]),
fetchContracts: PropTypes.func,
updateDeploymentPipeline: PropTypes.func, updateDeploymentPipeline: PropTypes.func,
web3: PropTypes.object, web3: PropTypes.object,
web3Deploy: PropTypes.func, web3Deploy: PropTypes.func,
@ -75,6 +141,8 @@ DeploymentContainer.propTypes = {
export default connect( export default connect(
mapStateToProps, { mapStateToProps, {
fetchContracts: contractsAction.request, fetchContracts: contractsAction.request,
listenToContracts,
stopContracts,
web3Deploy: web3DeployAction.request, web3Deploy: web3DeployAction.request,
web3EstimateGas: web3EstimateGasAction.request, web3EstimateGas: web3EstimateGasAction.request,
updateDeploymentPipeline: updateDeploymentPipeline updateDeploymentPipeline: updateDeploymentPipeline

View File

@ -76,7 +76,7 @@ class HomeContainer extends Component {
<CardBody> <CardBody>
<CardTitle>Deployed Contracts</CardTitle> <CardTitle>Deployed Contracts</CardTitle>
<div style={{marginBottom: '1.5rem', overflow: 'auto'}}> <div style={{marginBottom: '1.5rem', overflow: 'auto'}}>
<ContractsContainer contracts={contracts} mode="list" updatePageHeader={false} /> <ContractsContainer contracts={contracts} mode="list" numContractsToDisplay={5} updatePageHeader={false} />
</div> </div>
</CardBody> </CardBody>
</Card> </Card>

View File

@ -50,6 +50,14 @@ const sorter = {
blocksFull: function(a, b) { blocksFull: function(a, b) {
return b.number - a.number; return b.number - a.number;
}, },
contracts: function (a, b) {
const aName = a.className || '';
const bName = b.className || '';
if (!(aName || bName)) return 0;
if (!aName) return 1;
if (!bName) return -1;
return aName < bName ? -1 : aName > bName ? 1 : 0
},
transactions: function(a, b) { transactions: function(a, b) {
return ((BN_FACTOR * b.blockNumber) + b.transactionIndex) - ((BN_FACTOR * a.blockNumber) + a.transactionIndex); return ((BN_FACTOR * b.blockNumber) + b.transactionIndex) - ((BN_FACTOR * a.blockNumber) + a.transactionIndex);
}, },