Skip to content

Commit

Permalink
feat: add list pagination to the nano contract history table
Browse files Browse the repository at this point in the history
  • Loading branch information
pedroferreira1 committed Aug 16, 2024
1 parent 9f8f228 commit 945f773
Show file tree
Hide file tree
Showing 4 changed files with 254 additions and 61 deletions.
14 changes: 13 additions & 1 deletion src/api/nanoApi.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,23 @@ const nanoApi = {
* Get the history of transactions of a nano contract
*
* @param {string} id Nano contract id
* @param {number | null} count Number of elements to get the history
* @param {string | null} after Hash of the tx to get as reference for after pagination
* @param {string | null} before Hash of the tx to get as reference for before pagination
*
* For more details, see full node api docs
*/
getHistory(id) {
getHistory(id, count, after, before) {
const data = { id };
if (count) {
data['count'] = count;
}
if (after) {
data['after'] = after;
}
if (before) {
data['before'] = before;
}
return requestExplorerServiceV1.get(`node_api/nc_history`, {params: data}).then((res) => {
return res.data
}, (res) => {
Expand Down
234 changes: 234 additions & 0 deletions src/components/nano/NanoContractHistory.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
/**
* 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, { useEffect, useState } from 'react';
import Loading from '../../components/Loading';
import { Link, useLocation } from 'react-router-dom';
import { NANO_CONTRACT_TX_HISTORY_COUNT } from '../../constants';
import TxRow from '../tx/TxRow';
import helpers from '../../utils/helpers';
import nanoApi from '../../api/nanoApi';
import WebSocketHandler from '../../WebSocketHandler';
import PaginationURL from '../../utils/pagination';
import hathorLib from '@hathor/wallet-lib';
import { reverse } from 'lodash';


/**
* Displays nano tx history in a table with pagination buttons. As the user navigates through the history,
* the URL parameters 'hash' and 'page' are updated.
*
* Either all URL parameters are set or they are all missing.
*
* Example 1:
* hash = "00000000001b328fafb336b4515bb9557733fe93cf685dfd0c77cae3131f3fff"
* page = "previous"
*
* Example 2:
* hash = "00000000001b328fafb336b4515bb9557733fe93cf685dfd0c77cae3131f3fff"
* page = "next"
*/
function NanoContractHistory({ ncId }) {
const pagination = new PaginationURL({
'hash': {'required': false},
'page': {'required': false}
});

const location = useLocation();

// loading {boolean} Bool to show/hide loading element
const [loading, setLoading] = useState(true);
// history {Array} Nano contract history
const [history, setHistory] = useState([]);
// errorMessage {string} Message to show when error happens on history load
const [errorMessage, setErrorMessage] = useState('');
// hasBefore {boolean} If 'Previous' button should be enabled
const [hasBefore, setHasBefore] = useState(false);
// hasAfter {boolean} If 'Next' button should be enabled
const [hasAfter, setHasAfter] = useState(false);

useEffect(() => {
const queryParams = pagination.obtainQueryParams();
let after = null;
let before = null;
if (queryParams.hash) {
if (queryParams.page === 'previous') {
before = queryParams.hash;
} else if (queryParams.page === 'next') {
after = queryParams.hash;
} else {
// Params are wrong
pagination.clearOptionalQueryParams();
}
}

loadData(after, before);

// Handle new txs in the network to update the list in real time
WebSocketHandler.on('network', handleWebsocket);

return () => {
WebSocketHandler.removeListener('network', handleWebsocket);
};
}, [location]);

const loadData = async (after, before) => {
try {
const data = await nanoApi.getHistory(ncId, NANO_CONTRACT_TX_HISTORY_COUNT, after, before);
if (data.history.length === 0) {
// XXX
// The hathor-core API does not return if it has more or not, so if the last page
// has exactly the number of elements of the list, we need to fetch another page
// to understand that the previous was the final one. In that case, we just
// return without updating the state
if (before) {
// It means we reached the first page going back, then we clear the query params
pagination.clearOptionalQueryParams();
setHasBefore(false);
}
if (after) {
// It means we reached the last page going forward
setHasAfter(false);
}
return;
}
if (before) {
// When we are querying the previous set of transactions
// the API return the oldest first, so we need to revert the history
reverse(data.history);
}
setHistory(data.history);

if (data.history.length < NANO_CONTRACT_TX_HISTORY_COUNT) {
// This was the last page
if (!after && !before) {
// This is the first load without query params, so we do nothing because
// previous and next are already disabled
return;
}

if (after) {
setHasAfter(false);
setHasBefore(true);
return;
}

if (before) {
setHasAfter(true);
setHasBefore(false);
return;
}
} else {
// This is not the last page
if (!after && !before) {
// This is the first load without query params, so we need to
// enable only the next button
setHasAfter(true);
setHasBefore(false);
return;
}

// In all other cases, we must enable both buttons
// because this is not the last page
setHasAfter(true);
setHasBefore(true);
}
} catch (e) {
// Error in request
setErrorMessage('Error getting nano contract history.');
} finally {
setLoading(false);
}
}

const handleWebsocket = (wsData) => {
if (wsData.type === 'network:new_tx_accepted') {
updateListWs(wsData);
}
}

const updateListWs = (tx) => {
// We only add to the list if it's the first page and it's a new tx from this nano
if (!hasBefore) {
if (tx.version === hathorLib.constants.NANO_CONTRACTS_VERSION && tx.nc_id === ncId) {
let nanoHistory = [...history];
const willHaveAfter = (hasAfter || nanoHistory.length === NANO_CONTRACT_TX_HISTORY_COUNT)
// This updates the list with the new element at first
nanoHistory = helpers.updateListWs(nanoHistory, tx, NANO_CONTRACT_TX_HISTORY_COUNT);

// Now update the history
setHistory(nanoHistory);
setHasAfter(willHaveAfter);
}
}
}

if (errorMessage) {
return <p className='text-danger mb-4'>{errorMessage}</p>;
}

if (loading) {
return <Loading />;
}

const loadTable = () => {
return (
<div className="table-responsive mt-5">
<table className="table table-striped" id="tx-table">
<thead>
<tr>
<th className="d-none d-lg-table-cell">Hash</th>
<th className="d-none d-lg-table-cell">Timestamp</th>
<th className="d-table-cell d-lg-none" colSpan="2">Hash<br/>Timestamp</th>
</tr>
</thead>
<tbody>
{loadTableBody()}
</tbody>
</table>
</div>
);
}

const loadTableBody = () => {
return history.map((tx, idx) => {
// For some reason this API returns tx.hash instead of tx.tx_id like the others
tx.tx_id = tx.hash;
return (
<TxRow key={tx.tx_id} tx={tx} />
);
});
}

const loadPagination = () => {
if (history.length === 0) {
return null;
} else {
return (
<nav aria-label="nano history tx pagination" className="d-flex justify-content-center">
<ul className="pagination">
<li className={(!hasBefore || history.length === 0) ? "page-item mr-3 disabled" : "page-item mr-3"}>
<Link className="page-link" to={pagination.setURLParameters({hash: history[0].hash, page: 'previous'})}>Previous</Link>
</li>
<li className={(!hasAfter || history.length === 0) ? "page-item disabled" : "page-item"}>
<Link className="page-link" to={pagination.setURLParameters({hash: history.slice(-1).pop().hash, page: 'next'})}>Next</Link>
</li>
</ul>
</nav>
);
}
}

return (
<div className="w-100">
{loadTable()}
{loadPagination()}
</div>
);
}

export default NanoContractHistory;
3 changes: 3 additions & 0 deletions src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,3 +101,6 @@ export const UNLEASH_TIME_SERIES_FEATURE_FLAG = `explorer-timeseries-${REACT_AP
export const REACT_APP_TIMESERIES_DASHBOARD_ID = process.env.REACT_APP_TIMESERIES_DASHBOARD_ID;
export const TIMESERIES_DASHBOARD_URL = `https://hathor-explorer-75a9f9.kb.eu-central-1.aws.cloud.es.io:9243/s/anonymous-user/app/dashboards?auth_provider_hint=anonymous1#/view/${REACT_APP_TIMESERIES_DASHBOARD_ID}?embed=true&_g=(filters%3A!()%2CrefreshInterval%3A(pause%3A!t%2Cvalue%3A0)%2Ctime%3A(from%3Anow-1w%2Cto%3Anow))&show-time-filter=true&hide-filter-bar=true`;
export const SCREEN_STATUS_LOOP_INTERVAL_IN_SECONDS = 60 // This is the interval that ElasticSearch takes to ingest data from blocks

// Number of elements in the nano contract transaction history table
export const NANO_CONTRACT_TX_HISTORY_COUNT = 5;
64 changes: 4 additions & 60 deletions src/screens/nano/NanoContractDetail.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import React, { useEffect, useState } from 'react';
import Loading from '../../components/Loading';
import TxRow from '../../components/tx/TxRow';
import NanoContractHistory from '../../components/nano/NanoContractHistory';
import hathorLib from '@hathor/wallet-lib';
import nanoApi from '../../api/nanoApi';
import txApi from '../../api/txApi';
Expand All @@ -27,14 +27,10 @@ function NanoContractDetail(props) {
const [ncState, setNcState] = useState(null);
// blueprintInformation {Object | null} Blueprint Information from API
const [blueprintInformation, setBlueprintInformation] = useState(null);
// history {Array | null} Nano contract history
const [history, setHistory] = useState(null);
// txData {Object | null} Nano contract transaction data
const [txData, setTxData] = useState(null);
// loadingDetail {boolean} Bool to show/hide loading when getting transaction detail
const [loadingDetail, setLoadingDetail] = useState(true);
// loadingHistory {boolean} Bool to show/hide loading when getting nano history
const [loadingHistory, setLoadingHistory] = useState(true);
// errorMessage {string | null} Error message in case a request to get nano contract data fails
const [errorMessage, setErrorMessage] = useState(null);

Expand All @@ -54,7 +50,7 @@ function NanoContractDetail(props) {
const transactionData = await txApi.getTransaction(ncId);
if (transactionData.tx.version !== hathorLib.constants.NANO_CONTRACTS_VERSION) {
if (ignore) {
// This is to prevent setting a state after the componenet has been already cleaned
// This is to prevent setting a state after the component has been already cleaned
return;
}
setErrorMessage('Transaction is not a nano contract.');
Expand All @@ -81,30 +77,7 @@ function NanoContractDetail(props) {
}
}

async function loadNCHistory() {
setLoadingHistory(true);
setHistory(null);

try {
const data = await nanoApi.getHistory(ncId);
if (ignore) {
// This is to prevent setting a state after the componenet has been already cleaned
return;
}
setHistory(data.history);
setLoadingHistory(false);
} catch (e) {
if (ignore) {
// This is to prevent setting a state after the componenet has been already cleaned
return;
}
// Error in request
setErrorMessage('Error getting nano contract history.');
setLoadingHistory(false);
}
}
loadBlueprintInformation();
loadNCHistory();

return () => {
ignore = true;
Expand All @@ -116,39 +89,10 @@ function NanoContractDetail(props) {
return <p className='text-danger mb-4'>{errorMessage}</p>;
}

if (loadingHistory || loadingDetail) {
if (loadingDetail) {
return <Loading />;
}

const loadTable = () => {
return (
<div className="table-responsive mt-5">
<table className="table table-striped" id="tx-table">
<thead>
<tr>
<th className="d-none d-lg-table-cell">Hash</th>
<th className="d-none d-lg-table-cell">Timestamp</th>
<th className="d-table-cell d-lg-none" colSpan="2">Hash<br/>Timestamp</th>
</tr>
</thead>
<tbody>
{loadTableBody()}
</tbody>
</table>
</div>
);
}

const loadTableBody = () => {
return history.map((tx, idx) => {
// For some reason this API returns tx.hash instead of tx.tx_id like the others
tx.tx_id = tx.hash;
return (
<TxRow key={tx.tx_id} tx={tx} />
);
});
}

const renderBalances = () => {
return Object.entries(ncState.balances).map(([tokenUid, data]) => (
<tr key={tokenUid}>
Expand Down Expand Up @@ -225,7 +169,7 @@ function NanoContractDetail(props) {
{ renderNCBalances() }
<hr />
<h3 className="mt-4">History</h3>
{history && loadTable()}
<NanoContractHistory ncId={ncId} />
</div>
</div>
);
Expand Down

0 comments on commit 945f773

Please sign in to comment.