From 945f77399d23918368a154a7c61f2f3f9d65d7a2 Mon Sep 17 00:00:00 2001 From: Pedro Ferreira Date: Thu, 15 Aug 2024 09:19:47 -0300 Subject: [PATCH] feat: add list pagination to the nano contract history table --- src/api/nanoApi.js | 14 +- src/components/nano/NanoContractHistory.js | 234 +++++++++++++++++++++ src/constants.js | 3 + src/screens/nano/NanoContractDetail.js | 64 +----- 4 files changed, 254 insertions(+), 61 deletions(-) create mode 100644 src/components/nano/NanoContractHistory.js diff --git a/src/api/nanoApi.js b/src/api/nanoApi.js index ac47b34a..a2c1560b 100644 --- a/src/api/nanoApi.js +++ b/src/api/nanoApi.js @@ -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) => { diff --git a/src/components/nano/NanoContractHistory.js b/src/components/nano/NanoContractHistory.js new file mode 100644 index 00000000..4399b3df --- /dev/null +++ b/src/components/nano/NanoContractHistory.js @@ -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

{errorMessage}

; + } + + if (loading) { + return ; + } + + const loadTable = () => { + return ( +
+ + + + + + + + + + {loadTableBody()} + +
HashTimestampHash
Timestamp
+
+ ); + } + + 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 ( + + ); + }); + } + + const loadPagination = () => { + if (history.length === 0) { + return null; + } else { + return ( + + ); + } + } + + return ( +
+ {loadTable()} + {loadPagination()} +
+ ); +} + +export default NanoContractHistory; diff --git a/src/constants.js b/src/constants.js index 8d561265..bd0c3dfe 100644 --- a/src/constants.js +++ b/src/constants.js @@ -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; diff --git a/src/screens/nano/NanoContractDetail.js b/src/screens/nano/NanoContractDetail.js index 3401907c..782d6bca 100644 --- a/src/screens/nano/NanoContractDetail.js +++ b/src/screens/nano/NanoContractDetail.js @@ -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'; @@ -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); @@ -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.'); @@ -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; @@ -116,39 +89,10 @@ function NanoContractDetail(props) { return

{errorMessage}

; } - if (loadingHistory || loadingDetail) { + if (loadingDetail) { return ; } - const loadTable = () => { - return ( -
- - - - - - - - - - {loadTableBody()} - -
HashTimestampHash
Timestamp
-
- ); - } - - 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 ( - - ); - }); - } - const renderBalances = () => { return Object.entries(ncState.balances).map(([tokenUid, data]) => ( @@ -225,7 +169,7 @@ function NanoContractDetail(props) { { renderNCBalances() }

History

- {history && loadTable()} + );