Skip to content

Commit

Permalink
feat: add list pagination to the nano contract history table (#292)
Browse files Browse the repository at this point in the history
  • Loading branch information
pedroferreira1 authored Aug 20, 2024
1 parent b61769d commit a431bb5
Show file tree
Hide file tree
Showing 4 changed files with 290 additions and 64 deletions.
14 changes: 13 additions & 1 deletion src/api/nanoApi.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,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;
Expand Down
267 changes: 267 additions & 0 deletions src/components/nano/NanoContractHistory.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,267 @@
/**
* 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, { useCallback, useEffect, useMemo, useState } from 'react';
import { Link, useLocation } from 'react-router-dom';
import hathorLib from '@hathor/wallet-lib';
import { reverse } from 'lodash';
import Loading from '../Loading';
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';

/**
* 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 }) {
// We must use memo here because we were creating a new pagination
// object in every new render, so the useEffect was being called forever
const pagination = useMemo(
() =>
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);

/**
* useCallback is important here to update this method with new history state
* otherwise it would be fixed the moment the event listener is started in the useEffect
* with the history as an empty array
*
* @param {Transaction} tx Transaction object that arrived from the websocket
*/
const updateListWs = useCallback(
tx => {
// We only add to the list if it's the first page and it's a new tx from this nano
if (hasBefore) {
return;
}

if (tx.version !== hathorLib.constants.NANO_CONTRACTS_VERSION || tx.nc_id !== ncId) {
return;
}

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);
},
[history, hasAfter, hasBefore, ncId]
);

/**
* useCallback is needed here because this method is used as a dependency in the useEffect
*
* @param {string | null} after Hash to use for pagination when user clicks to fetch the next page
* @param {string | null} before Hash to use for pagination when user clicks to fetch the previous page
*/
const loadData = useCallback(
async (after, before) => {
try {
const data = await nanoApi.getHistory(ncId, NANO_CONTRACT_TX_HISTORY_COUNT, after, before);
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 (!after && !before) {
// This is the first load without query params, so if has_more === true
// we must enable next button
setHasAfter(data.has_more);
setHasBefore(false);
return;
}

if (after) {
// We clicked the next button, so we have before page
// and we will have the next page if has_more === true
setHasAfter(data.has_more);
setHasBefore(true);
return;
}

if (before) {
// We clicked the previous button, so we have next page
// and we will have the previous page if has_more === true
setHasAfter(true);
setHasBefore(data.has_more);
if (!data.has_more) {
// We are in the first page and clicked the Previous button
// so we must clear the query params
pagination.clearOptionalQueryParams();
}
return;
}
} catch (e) {
// Error in request
setErrorMessage('Error getting nano contract history.');
} finally {
setLoading(false);
}
},
[ncId, pagination]
);

/**
* useCallback is needed here because this method is used as a dependency in the useEffect
*
* Method to handle websocket messages that arrive in the network scope
* This method will discard any messages that are not new transactions
*
* wsData {Object} Data send in the websocket message
*/
const handleWebsocket = useCallback(
wsData => {
if (wsData.type === 'network:new_tx_accepted') {
updateListWs(wsData);
}
},
[updateListWs]
);

useEffect(() => {
// Handle load history depending on the query params in the URL
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);
}, [location, loadData, pagination]);

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

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

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 => {
// For some reason this API returns tx.hash instead of tx.tx_id like the others
const rowTx = { ...tx };
rowTx.tx_id = rowTx.hash;
return <TxRow key={rowTx.tx_id} tx={rowTx} />;
});
};

const loadPagination = () => {
if (history.length === 0) {
return null;
}
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 @@ -107,3 +107,6 @@ export const UNLEASH_TIME_SERIES_FEATURE_FLAG = `explorer-timeseries-${REACT_APP
export const { REACT_APP_TIMESERIES_DASHBOARD_ID } = process.env;
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;
Loading

0 comments on commit a431bb5

Please sign in to comment.