From a992108ee932cfe2699381c963c65db10c926df6 Mon Sep 17 00:00:00 2001 From: Kristof Csillag Date: Fri, 10 May 2024 02:45:31 +0200 Subject: [PATCH] Validators list: support search --- src/app/components/Validators/index.tsx | 16 +++++++---- src/app/pages/ValidatorsPage/hooks.ts | 37 ++++++++++++++++++------- src/app/pages/ValidatorsPage/index.tsx | 34 ++++++++++++++++------- src/locales/en/translation.json | 1 + src/oasis-nexus/api.ts | 4 ++- 5 files changed, 66 insertions(+), 26 deletions(-) diff --git a/src/app/components/Validators/index.tsx b/src/app/components/Validators/index.tsx index d63413eb7..cd0278fdf 100644 --- a/src/app/components/Validators/index.tsx +++ b/src/app/components/Validators/index.tsx @@ -17,9 +17,16 @@ type ValidatorsProps = { isLoading: boolean limit: number pagination: false | TablePaginationProps + highlightedPart?: string } -export const Validators: FC = ({ isLoading, limit, pagination, validators }) => { +export const Validators: FC = ({ + isLoading, + limit, + pagination, + validators, + highlightedPart, +}) => { const { t } = useTranslation() const { network } = useRequiredScopeParam() @@ -35,14 +42,12 @@ export const Validators: FC = ({ isLoading, limit, pagination, { key: 'status', content: t('common.status') }, { key: 'uptime', content: t('validator.uptime') }, ] - const offset = typeof pagination === 'boolean' ? 0 : (pagination.selectedPage - 1) * pagination.rowsPerPage - const tableRows = validators?.map((validator, index) => ({ + const tableRows = validators?.map(validator => ({ key: validator.entity_address, data: [ { align: TableCellAlign.Center, - // TODO: replace index when rank is implemented in the API - content:
{offset + index + 1}
, + content:
{validator.rank + 1}
, key: 'rank', }, { @@ -57,6 +62,7 @@ export const Validators: FC = ({ isLoading, limit, pagination, address={validator.entity_address} name={validator.media?.name} network={network} + highlightedPart={highlightedPart} /> ), diff --git a/src/app/pages/ValidatorsPage/hooks.ts b/src/app/pages/ValidatorsPage/hooks.ts index 5b6128562..310c9ba7d 100644 --- a/src/app/pages/ValidatorsPage/hooks.ts +++ b/src/app/pages/ValidatorsPage/hooks.ts @@ -3,23 +3,40 @@ import { useGetConsensusValidators, Validator, ValidatorList } from '../../../oa import { useClientSidePagination } from '../../components/Table/useClientSidePagination' import { NUMBER_OF_ITEMS_ON_SEPARATE_PAGE } from '../../config' import { Network } from '../../../types/network' +import { useTypedSearchParam } from '../../hooks/useTypedSearchParam' +import { hasTextMatch } from '../../components/HighlightedText/text-matching' +import { useTranslation } from 'react-i18next' -export const useLoadedValidators = (network: Network, tableView: TableLayout) => { - const pagination = useClientSidePagination({ +const useValidatorNameSearch = () => useTypedSearchParam('name', '', { deleteParams: ['page'] }) + +export const useValidatorFiltering = () => { + const { t } = useTranslation() + const [nameSearchInput, setNameSearchInput] = useValidatorNameSearch() + const namePattern = nameSearchInput.length < 3 ? undefined : nameSearchInput + const nameWarning = !!nameSearchInput && !namePattern ? t('tableSearch.error.tooShort') : undefined + return { nameSearchInput, setNameSearchInput, namePattern, nameWarning } +} + +export const useValidatorData = (network: Network, tableView: TableLayout) => { + const { namePattern } = useValidatorFiltering() + const nameFilter = namePattern + ? (validator: Validator) => hasTextMatch(validator.media?.name, [namePattern]) + : undefined + const pagination = useClientSidePagination({ paramName: 'page', serverPageSize: 1000, clientPageSize: NUMBER_OF_ITEMS_ON_SEPARATE_PAGE, + transform: (data, r) => [ + data, + [r.total_count, r.is_total_count_clipped], // Extract the real number of validators + ], + filter: nameFilter, }) - const offset = pagination.offsetForQuery + const { offset, limit } = pagination.paramsForServer const validatorsQuery = useGetConsensusValidators(network, { - limit: tableView === TableLayout.Vertical ? offset + pagination.limitForQuery : pagination.limitForQuery, + limit: tableView === TableLayout.Vertical ? offset + limit : limit, offset: tableView === TableLayout.Vertical ? 0 : offset, }) const { isLoading, isFetched, data } = validatorsQuery - const paginatedResults = pagination.getResults(data?.data) - return { - isLoading, - isFetched, - paginatedResults, - } + return pagination.getResults(isLoading, isFetched, data?.data) } diff --git a/src/app/pages/ValidatorsPage/index.tsx b/src/app/pages/ValidatorsPage/index.tsx index cdf001085..df93dd61f 100644 --- a/src/app/pages/ValidatorsPage/index.tsx +++ b/src/app/pages/ValidatorsPage/index.tsx @@ -7,13 +7,14 @@ import { SubPageCard } from '../../components/SubPageCard' import { AppErrors } from '../../../types/errors' import { TableLayout, TableLayoutButton } from '../../components/TableLayoutButton' -import { LoadMoreButton } from '../../components/LoadMoreButton' import { useRequiredScopeParam } from '../../hooks/useScopeParam' import { Validators } from '../../components/Validators' import { CardHeaderWithCounter } from '../../components/CardHeaderWithCounter' import { ValidatorDetailsView } from '../ValidatorDetailsPage' import { VerticalList } from '../../components/VerticalList' -import { useLoadedValidators } from './hooks' +import { useValidatorFiltering, useValidatorData } from './hooks' +import Box from '@mui/material/Box' +import { TableSearchBar } from '../../components/Search/TableSearchBar' export const ValidatorsPage: FC = () => { const [tableView, setTableView] = useState(TableLayout.Horizontal) @@ -22,6 +23,7 @@ export const ValidatorsPage: FC = () => { const scope = useRequiredScopeParam() const { network } = scope + const { nameSearchInput, setNameSearchInput, nameWarning, namePattern } = useValidatorFiltering() useEffect(() => { if (!isMobile) { @@ -31,15 +33,16 @@ export const ValidatorsPage: FC = () => { const { isLoading, - isFetched, - paginatedResults, - // pagination, pageSize, isLoading, isFetched, validatorsData - } = useLoadedValidators(network, tableView) + tablePaginationProps, + data: validators, + extractedData, + hasNoResultsOnSelectedPage, + } = useValidatorData(network, tableView) - const { tablePaginationProps, data: validators } = paginatedResults - const { selectedPage, totalCount, isTotalCountClipped, rowsPerPage } = tablePaginationProps + const { rowsPerPage } = tablePaginationProps + const [totalCount, isTotalCountClipped] = extractedData ?? [0, false] - if (isFetched && selectedPage > 1 && !validators?.length) { + if (hasNoResultsOnSelectedPage) { throw AppErrors.PageDoesNotExist } @@ -59,7 +62,17 @@ export const ValidatorsPage: FC = () => { isTotalCountClipped={isTotalCountClipped} /> } - action={isMobile && } + action={ + + + {isMobile && } + + } noPadding={tableView === TableLayout.Vertical} mainTitle > @@ -69,6 +82,7 @@ export const ValidatorsPage: FC = () => { isLoading={isLoading} limit={rowsPerPage} pagination={tablePaginationProps} + highlightedPart={namePattern} /> )} {tableView === TableLayout.Vertical && ( diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index 14ee0d887..c60fd0837 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -651,6 +651,7 @@ "signedBlocks": "Signed Blocks", "signedBlocksDescription": "Last 100 blocks", "proposedBlocks": "Proposed Blocks", + "search": "Search by name", "snapshot": "Validator Snapshot", "shares": "Shares", "sharesDocs": "How shares are calculated", diff --git a/src/oasis-nexus/api.ts b/src/oasis-nexus/api.ts index 48a26b745..fdc10bfed 100644 --- a/src/oasis-nexus/api.ts +++ b/src/oasis-nexus/api.ts @@ -89,6 +89,7 @@ declare module './generated/api' { export interface Validator { ticker: Ticker + rank: number } export interface Proposal { @@ -965,10 +966,11 @@ export const useGetConsensusValidators: typeof generated.useGetConsensusValidato ...arrayify(axios.defaults.transformResponse), (data: generated.ValidatorList, headers, status): ExtendedValidatorList => { if (status !== 200) return data - const validators = data.validators.map((validator): generated.Validator => { + const validators = data.validators.map((validator, index): generated.Validator => { return { ...validator, escrow: fromBaseUnits(validator.escrow, consensusDecimals), + rank: index + (params?.offset ?? 0), // TODO: remove this when rank is added to API ticker, } })