Skip to content

Commit

Permalink
feat(feature-activation): implement features page
Browse files Browse the repository at this point in the history
  • Loading branch information
glevco committed Jul 26, 2023
1 parent 96a48d0 commit 536a9df
Show file tree
Hide file tree
Showing 8 changed files with 291 additions and 0 deletions.
2 changes: 2 additions & 0 deletions src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import AddressDetail from './screens/AddressDetail';
import DecodeTx from './screens/DecodeTx';
import PushTx from './screens/PushTx';
import TransactionList from './screens/TransactionList';
import FeatureList from './screens/FeatureList';
import TokenList from './screens/TokenList';
import TokenBalancesList from './screens/TokenBalances';
import BlockList from './screens/BlockList';
Expand Down Expand Up @@ -114,6 +115,7 @@ class Root extends React.Component {
<NavigationRoute exact path="/token_balances" component={TokenBalancesList} />
<NavigationRoute exact path="/blocks" component={BlockList} />
<NavigationRoute exact path="/dag" component={Dag} />
<NavigationRoute exact path="/features" component={FeatureList} />
<NavigationRoute exact path="/network/:peerId?" component={PeerAdmin} />
<NavigationRoute exact path="/statistics" component={Dashboard} />
<NavigationRoute exact path="/token_detail/:tokenUID" component={TokenDetail} />
Expand Down
22 changes: 22 additions & 0 deletions src/api/featureApi.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/**
* Copyright (c) Hathor Labs and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

import requestExplorerServiceV1 from './axiosInstance';

const featureApi = {

getFeatures() {
return requestExplorerServiceV1.get(`node_api/feature`).then(res => {
return res.data
}).catch(e => {
throw new Error(e);
});
},

};

export default featureApi;
1 change: 1 addition & 0 deletions src/components/Navigation.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ class Navigation extends React.Component {
<NavLink to="/decode-tx/" exact className="nav-link">Decode Tx</NavLink>
<NavLink to="/push-tx/" exact className="nav-link">Push Tx</NavLink>
<NavLink to="/dag/" exact className="nav-link">DAG</NavLink>
<NavLink to="/features/" exact className="nav-link">Features</NavLink>
</div>
</li>
</ul>
Expand Down
32 changes: 32 additions & 0 deletions src/components/feature_activation/FeatureRow.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/**
* Copyright (c) Hathor Labs and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

import React from 'react';


class FeatureRow extends React.Component {
render() {
const { acceptance } = this.props.feature
const acceptance_percentage = acceptance === null ? '-' : `${(acceptance * 100).toFixed(0)}%`

return (
<tr>
<td className="d-lg-table-cell pr-3">{this.props.feature.name}</td>
<td className="d-lg-table-cell pr-3">{this.props.feature.state}</td>
<td className="d-lg-table-cell pr-3">{acceptance_percentage}</td>
<td className="d-lg-table-cell pr-3">{(this.props.feature.threshold * 100).toFixed(0)}%</td>
<td className="d-lg-table-cell pr-3">{this.props.feature.start_height}</td>
<td className="d-lg-table-cell pr-3">{this.props.feature.minimum_activation_height}</td>
<td className="d-lg-table-cell pr-3">{this.props.feature.timeout_height}</td>
<td className="d-lg-table-cell pr-3">{this.props.feature.lock_in_on_timeout.toString()}</td>
<td className="d-lg-table-cell pr-3">{this.props.feature.version}</td>
</tr>
);
}
}

export default FeatureRow;
201 changes: 201 additions & 0 deletions src/components/feature_activation/Features.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
/**
* Copyright (c) Hathor Labs and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

import React from 'react';
import ReactLoading from 'react-loading';
import { Link } from 'react-router-dom';
import { chunk, orderBy } from 'lodash';
import { FEATURE_COUNT } from '../../constants';
import FeatureRow from './FeatureRow';
import colors from '../../index.scss';
import PaginationURL from '../../utils/pagination';

class Features extends React.Component {
constructor(props) {
super(props);

this.pagination = new PaginationURL({
page: { required: false }
});

this.state = {
block_hash: null,
block_height: null,
pages: [],
loaded: false,
page: 1,
queryParams: this.pagination.obtainQueryParams(),
showColumnDescriptions: false
}
}

componentDidMount() {
this.props.getFeatures().then(this.handleFeatures, e => console.error(e));
}

componentDidUpdate(prevProps, prevState) {
const { page = 1 } = this.pagination.obtainQueryParams();
const newPage = parseInt(page)

if (this.state.page === newPage) {
return;
}

this.setState({ page: newPage })

if (newPage === 1) {
this.pagination.clearOptionalQueryParams()
}
}

handleFeatures = (response) => {
const { features = [], block_height, block_hash } = response
const sortedFeatures = orderBy(features, 'start_height', 'desc');
const pages = chunk(sortedFeatures, FEATURE_COUNT);

this.setState({
pages,
block_hash,
block_height,
loaded: true
});
}

hasBefore = () => this.state.page > 1
hasAfter = () => this.state.page < this.state.pages.length

getPageFeatures = () => {
const { page, pages } = this.state
return pages[page - 1] || []
}

getColumnDescriptions = () => [
{
name: 'Name',
description: 'The feature name.',
},
{
name: 'State',
description: 'The state the feature is currently in.',
},
{
name: 'Acceptance',
description: 'The acceptance percentage of this feature in the last evaluation interval.',
},
{
name: 'Threshold',
description: 'The required acceptance percentage for this feature to become active.',
},
{
name: 'Start Height',
description: 'The block height at which this feature\'s activation process will start.',
},
{
name: 'Minimum Activation Height',
description: 'The minimum block height at which this feature can become active.',
},
{
name: 'Timeout Height',
description: 'The block height at which this feature\'s activation process ends.',
},
{
name: 'Lock-in on Timeout',
description: 'Whether this feature will be locked-in when its activation process times out.',
},
{
name: 'Since Version',
description: 'The hathor-core version at which this feature was introduced.',
},
]

toggleColumnDescriptions = (e) => {
e.preventDefault();
this.setState({ showColumnDescriptions: !this.state.showColumnDescriptions });
}

render() {
const loadPagination = () => {
if (this.state.pages.length === 0) {
return null;
}
return (
<nav aria-label="Feature pagination" className="d-flex justify-content-center">
<ul className="pagination">
<li ref="featurePrevious" className={!this.hasBefore() ? "page-item mr-3 disabled" : "page-item mr-3"}>
<Link className="page-link" to={this.pagination.setURLParameters({page: this.state.page - 1})}>Previous</Link>
</li>
<li ref="featureNext" className={!this.hasAfter() ? "page-item disabled" : "page-item"}>
<Link className="page-link" to={this.pagination.setURLParameters({ page: this.state.page + 1})}>Next</Link>
</li>
</ul>
</nav>
);
}

const loadTable = () => {
return (
<div className="table-responsive mt-5">
<table className="table table-striped" id="features-table">
<thead>
<tr>
<th className="d-lg-table-cell">Name</th>
<th className="d-lg-table-cell">State</th>
<th className="d-lg-table-cell">Acceptance</th>
<th className="d-lg-table-cell">Threshold</th>
<th className="d-lg-table-cell">Start Height</th>
<th className="d-lg-table-cell">Minimum Activation Height</th>
<th className="d-lg-table-cell">Timeout Height</th>
<th className="d-lg-table-cell">Lock-in on Timeout</th>
<th className="d-lg-table-cell">Since Version</th>
</tr>
</thead>
<tbody>
{loadTableBody()}
</tbody>
</table>
</div>
);
}

const loadTableBody = () => {
return this.getPageFeatures().map((feature) => {
return (
<FeatureRow key={feature.name} feature={feature} />
);
});
}

const loadColumnDescriptions = () => {
return this.getColumnDescriptions().map(({ name, description }) => {
return (
<div>
<label>{name}</label>
<p>{description}</p>
</div>
)
})
}

return (
<div className="w-100">
{this.props.title}
<div>Showing feature states for <Link to={`/transaction/${this.state.block_hash}`}>current best block</Link> at height {this.state.block_height}.</div>
{!this.state.loaded ? <ReactLoading type='spin' color={colors.purpleHathor} delay={500} /> : loadTable()}
{loadPagination()}
<div className="f-flex flex-column align-items-start common-div bordered-wrapper mt-3 mt-lg-0 w-100 feature-column-descriptions">
<div>
<label>Column descriptions: </label>
<a href="true" className="ml-1" onClick={(e) => this.toggleColumnDescriptions(e)}>{this.state.showColumnDescriptions ? 'Click to hide' : 'Click to show'}</a>
</div>
{this.state.showColumnDescriptions && loadColumnDescriptions()}
</div>
</div>
);
}
}

export default Features;
2 changes: 2 additions & 0 deletions src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ export const WS_URL = tmp_ws_url;

export const TX_COUNT = 10;

export const FEATURE_COUNT = 10;

// Maximum number of tokens to fetch when searching for tokens of an address
export const TOKEN_COUNT = 50;

Expand Down
4 changes: 4 additions & 0 deletions src/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,10 @@ a:hover {
font-weight: bold;
}

.feature-column-descriptions label {
font-weight: bold;
}

div.new-address-wrapper, button.send-tokens {
margin-bottom: 1rem;
margin-right: 1rem;
Expand Down
27 changes: 27 additions & 0 deletions src/screens/FeatureList.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/**
* Copyright (c) Hathor Labs and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

import React from 'react';
import Features from '../components/feature_activation/Features';
import featureApi from '../api/featureApi';


class FeatureList extends React.Component {
getFeatures = () => {
return featureApi.getFeatures();
}

render() {
return (
<div className="content-wrapper">
<Features title={<h1>Feature Activation</h1>} getFeatures={this.getFeatures} />
</div>
);
}
}

export default FeatureList;

0 comments on commit 536a9df

Please sign in to comment.