Adding blocks explorer

This commit is contained in:
Anthony Laibe 2018-08-02 11:27:33 +01:00 committed by Pascal Precht
parent 27a237ec73
commit b00ce3c9fa
No known key found for this signature in database
GPG Key ID: 0EE28D8D6FD85D7D
17 changed files with 341 additions and 67 deletions

View File

@ -6,6 +6,7 @@
"axios": "^0.18.0", "axios": "^0.18.0",
"connected-react-router": "^4.3.0", "connected-react-router": "^4.3.0",
"history": "^4.7.2", "history": "^4.7.2",
"prop-types": "^15.6.2",
"react": "^16.4.1", "react": "^16.4.1",
"react-dom": "^16.4.1", "react-dom": "^16.4.1",
"react-redux": "^5.0.7", "react-redux": "^5.0.7",

View File

@ -6,6 +6,10 @@ export const RECEIVE_ACCOUNTS_ERROR = 'RECEIVE_ACCOUNTS_ERROR';
export const FETCH_PROCESSES = 'FETCH_PROCESSES'; export const FETCH_PROCESSES = 'FETCH_PROCESSES';
export const RECEIVE_PROCESSES = 'RECEIVE_PROCESSES'; export const RECEIVE_PROCESSES = 'RECEIVE_PROCESSES';
export const RECEIVE_PROCESSES_ERROR = 'RECEIVE_PROCESSES_ERROR'; export const RECEIVE_PROCESSES_ERROR = 'RECEIVE_PROCESSES_ERROR';
// Blocks
export const FETCH_BLOCKS = 'FETCH_BLOCKS';
export const RECEIVE_BLOCKS = 'RECEIVE_BLOCKS';
export const RECEIVE_BLOCKS_ERROR = 'RECEIVE_BLOCKS_ERROR';
export function fetchAccounts() { export function fetchAccounts() {
return { return {
@ -44,3 +48,22 @@ export function receiveProcessesError() {
type: RECEIVE_PROCESSES_ERROR type: RECEIVE_PROCESSES_ERROR
}; };
} }
export function fetchBlocks() {
return {
type: FETCH_BLOCKS
};
}
export function receiveBlocks(blocks) {
return {
type: RECEIVE_BLOCKS,
blocks
};
}
export function receiveBlocksError() {
return {
type: RECEIVE_BLOCKS_ERROR
};
}

View File

@ -1,9 +1,15 @@
import axios from "axios"; import axios from "axios";
const BASE_URL = 'http://localhost:8000/embark-api';
export function fetchAccounts() { export function fetchAccounts() {
return axios.get('http://localhost:8000/embark-api/blockchain/accounts'); return axios.get(`${BASE_URL}/blockchain/accounts`);
}
export function fetchBlocks() {
return axios.get(`${BASE_URL}/blockchain/blocks`);
} }
export function fetchProcesses() { export function fetchProcesses() {
return axios.get('http://localhost:8000/embark-api/processes'); return axios.get(`${BASE_URL}/processes`);
} }

View File

@ -1,31 +1,46 @@
import React from 'react'; import React from 'react';
import {
Page,
Grid,
Card,
Table
} from "tabler-react";
import PropTypes from 'prop-types';
const Accounts = ({accounts}) => ( const Accounts = ({accounts}) => (
<React.Fragment> <Page.Content title="Accounts">
<h1>Accounts</h1> <Grid.Row>
<table> <Grid.Col>
<thead> <Card>
<tr> <Table
<th>Address</th> responsive
<th>Balance</th> className="card-table table-vcenter text-nowrap"
<th>TX count</th> headerItems={[
<th>Index</th> {content: "Address"},
</tr> {content: "Balance"},
</thead> {content: "TX count"},
<tbody> {content: "Index"}
{accounts.map((account) => { ]}
return ( bodyItems={
<tr> accounts.map((account) => {
<td>{account.address}</td> return ([
<td>{account.balance} ETH</td> {content: account.address},
<td>{account.transactionCount}</td> {content: account.balance},
<td>{account.index}</td> {content: account.transactionCount},
</tr> {content: account.index}
) ]);
})} })
</tbody> }
</table> />
</React.Fragment> </Card>
</Grid.Col>
</Grid.Row>
</Page.Content>
); );
Accounts.propTypes = {
accounts: PropTypes.object
};
export default Accounts; export default Accounts;

View File

@ -0,0 +1,41 @@
import React from 'react';
import {
Page,
Grid,
Card,
Table
} from "tabler-react";
import PropTypes from 'prop-types';
const Blocks = ({blocks}) => (
<Page.Content title="Blocks">
<Grid.Row>
<Grid.Col>
<Card>
<Table
responsive
className="card-table table-vcenter text-nowrap"
headerItems={[{content: "Number"}, {content: "Mined On"}, {content: "Gas Used"}, {content: "TX Count"}]}
bodyItems={
blocks.map((block) => {
return ([
{content: block.number},
{content: new Date(block.timestamp * 1000).toLocaleString()},
{content: block.gasUsed},
{content: block.transactions.length}
]);
})
}
/>
</Card>
</Grid.Col>
</Grid.Row>
</Page.Content>
);
Blocks.propTypes = {
blocks: [PropTypes.object]
};
export default Blocks;

View File

@ -0,0 +1,46 @@
import React from 'react';
import {NavLink, Route, Switch, withRouter} from 'react-router-dom';
import {
Page,
Grid,
List,
} from "tabler-react";
import AccountsContainer from '../containers/AccountsContainer';
import BlocksContainer from '../containers/BlocksContainer';
const ExplorerLayout = () => (
<Grid.Row>
<Grid.Col md={3}>
<Page.Title className="my-5">Explorer</Page.Title>
<div>
<List.Group transparent={true}>
<List.GroupItem
className="d-flex align-items-center"
to="/embark/explorer/accounts"
icon="users"
RootComponent={withRouter(NavLink)}
>
Accounts
</List.GroupItem>
<List.GroupItem
className="d-flex align-items-center"
to="/embark/explorer/blocks"
icon="book-open"
RootComponent={withRouter(NavLink)}
>
Blocks
</List.GroupItem>
</List.Group>
</div>
</Grid.Col>
<Grid.Col md={9}>
<Switch>
<Route exact path="/embark/explorer/accounts" component={AccountsContainer} />
<Route exact path="/embark/explorer/blocks" component={BlocksContainer} />
</Switch>
</Grid.Col>
</Grid.Row>
);
export default ExplorerLayout;

View File

@ -0,0 +1,12 @@
import React from 'react';
import {Grid, Loader} from 'tabler-react';
const Loading = () => (
<Grid.Row className="align-items-center h-100 mt-5">
<Grid.Col>
<Loader className="mx-auto" />
</Grid.Col>
</Grid.Row>
);
export default Loading;

View File

@ -1,21 +1,20 @@
import React, { Component } from 'react'; import React, {Component} from 'react';
import { connect } from 'react-redux'; import {connect} from 'react-redux';
import { fetchAccounts } from '../actions'; import PropTypes from 'prop-types';
import {fetchAccounts} from '../actions';
import Accounts from '../components/Accounts'; import Accounts from '../components/Accounts';
import Loading from '../components/Loading';
class AccountsContainer extends Component { class AccountsContainer extends Component {
componentWillMount() { componentDidMount() {
this.props.fetchAccounts(); this.props.fetchAccounts();
} }
render() { render() {
const { accounts } = this.props; const {accounts} = this.props;
if (!accounts.data) { if (!accounts.data) {
return ( return <Loading />;
<h1>
<i>Loading accounts...</i>
</h1>
)
} }
if (accounts.error) { if (accounts.error) {
@ -23,22 +22,27 @@ class AccountsContainer extends Component {
<h1> <h1>
<i>Error API...</i> <i>Error API...</i>
</h1> </h1>
) );
} }
return ( return (
<Accounts accounts={accounts.data} /> <Accounts accounts={accounts.data} />
); );
} }
}; }
function mapStateToProps(state) { function mapStateToProps(state) {
return { accounts: state.accounts } return {accounts: state.accounts};
} }
AccountsContainer.propTypes = {
accounts: PropTypes.object,
fetchAccounts: PropTypes.func
};
export default connect( export default connect(
mapStateToProps, mapStateToProps,
{ {
fetchAccounts fetchAccounts
}, },
)(AccountsContainer) )(AccountsContainer);

View File

@ -0,0 +1,48 @@
import React, {Component} from 'react';
import {connect} from 'react-redux';
import PropTypes from 'prop-types';
import {fetchBlocks} from '../actions';
import Blocks from '../components/Blocks';
import Loading from '../components/Loading';
class BlocksContainer extends Component {
componentDidMount() {
this.props.fetchBlocks();
}
render() {
const {blocks} = this.props;
if (!blocks.data) {
return <Loading />;
}
if (blocks.error) {
return (
<h1>
<i>Error API...</i>
</h1>
);
}
return (
<Blocks blocks={blocks.data} />
);
}
}
function mapStateToProps(state) {
return {blocks: state.blocks};
}
BlocksContainer.propTypes = {
blocks: PropTypes.object,
fetchBlocks: PropTypes.func
};
export default connect(
mapStateToProps,
{
fetchBlocks
},
)(BlocksContainer);

View File

@ -1,9 +1,11 @@
import React, {Component} from 'react'; import React, {Component} from 'react';
import {connect} from 'react-redux'; import {connect} from 'react-redux';
import {fetchProcesses} from '../actions';
import {Tabs, Tab} from 'tabler-react'; import {Tabs, Tab} from 'tabler-react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import {fetchProcesses} from '../actions';
import Loading from '../components/Loading';
import "./css/processContainer.css"; import "./css/processContainer.css";
class ProcessesContainer extends Component { class ProcessesContainer extends Component {
@ -14,11 +16,7 @@ class ProcessesContainer extends Component {
render() { render() {
const {processes} = this.props; const {processes} = this.props;
if (!processes.data) { if (!processes.data) {
return ( return <Loading />;
<h1>
<i>Loading processes...</i>
</h1>
);
} }
if (processes.error) { if (processes.error) {

View File

@ -0,0 +1,12 @@
import {RECEIVE_ACCOUNTS, RECEIVE_ACCOUNTS_ERROR} from "../actions";
export default function accounts(state = {}, action) {
switch (action.type) {
case RECEIVE_ACCOUNTS:
return Object.assign({}, state, {data: action.accounts.data});
case RECEIVE_ACCOUNTS_ERROR:
return Object.assign({}, state, {error: true});
default:
return state;
}
}

View File

@ -0,0 +1,12 @@
import {RECEIVE_BLOCKS, RECEIVE_BLOCKS_ERROR} from "../actions";
export default function accounts(state = {}, action) {
switch (action.type) {
case RECEIVE_BLOCKS:
return Object.assign({}, state, {data: action.blocks.data});
case RECEIVE_BLOCKS_ERROR:
return Object.assign({}, state, {error: true});
default:
return state;
}
}

View File

@ -1,17 +1,12 @@
import {combineReducers} from 'redux'; import {combineReducers} from 'redux';
import {RECEIVE_ACCOUNTS, RECEIVE_ACCOUNTS_ERROR} from "../actions";
import processesReducer from './processesReducer'; import processesReducer from './processesReducer';
import accountsReducer from './accountsReducer';
import blocksReducer from './blocksReducer';
function accounts(state = {}, action) { const rootReducer = combineReducers({
switch (action.type) { accounts: accountsReducer,
case RECEIVE_ACCOUNTS: processes: processesReducer,
return Object.assign({}, state, {data: action.accounts.data}); blocks: blocksReducer
case RECEIVE_ACCOUNTS_ERROR: });
return Object.assign({}, state, {error: true});
default:
return state;
}
}
const rootReducer = combineReducers({accounts, processes: processesReducer});
export default rootReducer; export default rootReducer;

View File

@ -1,16 +1,18 @@
import React from 'react'; import React from 'react';
import {Route, Switch} from 'react-router'; import {Route, Switch} from 'react-router-dom';
import Home from './components/Home'; import Home from './components/Home';
import AccountsContainer from './containers/AccountsContainer';
import ProcessesContainer from './containers/ProcessesContainer';
import NoMatch from './components/NoMatch'; import NoMatch from './components/NoMatch';
import ExplorerLayout from './components/ExplorerLayout';
import ProcessesContainer from './containers/ProcessesContainer';
const routes = ( const routes = (
<React.Fragment> <React.Fragment>
<Switch> <Switch>
<Route exact path="/embark" component={Home} /> <Route exact path="/embark/" component={Home} />
<Route path="/embark/explorer/accounts" component={AccountsContainer} /> <Route path="/embark/explorer/" component={ExplorerLayout} />
<Route path="/embark/processes" component={ProcessesContainer} /> <Route path="/embark/processes/" component={ProcessesContainer} />
<Route component={NoMatch} /> <Route component={NoMatch} />
</Switch> </Switch>
</React.Fragment> </React.Fragment>

View File

@ -2,6 +2,19 @@ import * as actions from '../actions';
import * as api from '../api'; import * as api from '../api';
import {all, call, fork, put, takeEvery} from 'redux-saga/effects'; import {all, call, fork, put, takeEvery} from 'redux-saga/effects';
export function *fetchBlocks() {
try {
const blocks = yield call(api.fetchBlocks);
yield put(actions.receiveBlocks(blocks));
} catch (e) {
yield put(actions.receiveBlocksError());
}
}
export function *watchFetchBlocks() {
yield takeEvery(actions.FETCH_BLOCKS, fetchBlocks);
}
export function *fetchAccounts() { export function *fetchAccounts() {
try { try {
const accounts = yield call(api.fetchAccounts); const accounts = yield call(api.fetchAccounts);
@ -29,5 +42,5 @@ export function *watchFetchProcesses() {
} }
export default function *root() { export default function *root() {
yield all([fork(watchFetchAccounts), fork(watchFetchProcesses)]); yield all([fork(watchFetchAccounts), fork(watchFetchProcesses), fork(watchFetchBlocks)]);
} }

View File

@ -6,12 +6,14 @@ import history from '../history';
import rootReducer from '../reducers'; import rootReducer from '../reducers';
import saga from '../sagas'; import saga from '../sagas';
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
export default function configureStore() { export default function configureStore() {
const sagaMiddleware = createSagaMiddleware(); const sagaMiddleware = createSagaMiddleware();
const store = createStore( const store = createStore(
connectRouter(history)(rootReducer), connectRouter(history)(rootReducer),
compose( composeEnhancers(
applyMiddleware( applyMiddleware(
routerMiddleware(history), routerMiddleware(history),
sagaMiddleware sagaMiddleware

View File

@ -327,6 +327,46 @@ class BlockchainConnector {
}); });
} }
); );
plugin.registerAPICall(
'get',
'/embark-api/blockchain/blocks',
(req, res) => {
let from = req.query.from;
let limit = req.query.limit || 10;
let results = [];
async.waterfall([
function(callback) {
if (from) {
return callback();
}
self.getBlockNumber((err, blockNumber) => {
if (err) {
from = 0;
} else {
from = blockNumber;
}
callback();
});
},
function(callback) {
async.times(limit, function(n, next) {
self.web3.eth.getBlock(from - n, function(err, block) {
if (err){
return next();
}
results.push(block);
next();
});
}, function() {
callback();
});
}
], function () {
res.send(results);
});
}
);
} }
@ -334,6 +374,10 @@ class BlockchainConnector {
return this.web3.eth.defaultAccount; return this.web3.eth.defaultAccount;
} }
getBlockNumber(cb) {
return this.web3.eth.getBlockNumber(cb);
}
setDefaultAccount(account) { setDefaultAccount(account) {
this.web3.eth.defaultAccount = account; this.web3.eth.defaultAccount = account;
} }