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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -50,6 +50,14 @@ const sorter = {
blocksFull: function(a, b) {
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) {
return ((BN_FACTOR * b.blockNumber) + b.transactionIndex) - ((BN_FACTOR * a.blockNumber) + a.transactionIndex);
},