From 61eef903a66aaa597ba8052bd4a0f206bf0db4fb Mon Sep 17 00:00:00 2001 From: soaresa <10797037+soaresa@users.noreply.github.com> Date: Sat, 1 Jun 2024 13:33:22 -0300 Subject: [PATCH] feat: add sortable functionality to validators table --- .../core/routes/Validators/Validators.tsx | 118 +------- .../Validators/components/ValidatorsStats.tsx | 32 +++ .../Validators/components/ValidatorsTable.tsx | 262 ++++++++++++++++++ .../modules/interface/Validator.interface.ts | 23 ++ 4 files changed, 321 insertions(+), 114 deletions(-) create mode 100644 web-app/src/modules/core/routes/Validators/components/ValidatorsStats.tsx create mode 100644 web-app/src/modules/core/routes/Validators/components/ValidatorsTable.tsx create mode 100644 web-app/src/modules/interface/Validator.interface.ts diff --git a/web-app/src/modules/core/routes/Validators/Validators.tsx b/web-app/src/modules/core/routes/Validators/Validators.tsx index 2bed3bb..cf1b550 100644 --- a/web-app/src/modules/core/routes/Validators/Validators.tsx +++ b/web-app/src/modules/core/routes/Validators/Validators.tsx @@ -1,10 +1,8 @@ import { FC } from 'react'; import { gql, useQuery } from '@apollo/client'; import Page from '../../../ui/Page'; -import AccountAddress from '../../../ui/AccountAddress'; -import Money from '../../../ui/Money'; -import clsx from 'clsx'; -import { CheckIcon, XMarkIcon } from '@heroicons/react/20/solid'; +import ValidatorsTable from './components/ValidatorsTable'; +import ValidatorsStats from './components/ValidatorsStats'; const GET_VALIDATORS = gql` query GetValidators { @@ -70,119 +68,11 @@ const Validators: FC = () => { } if (data) { - const validatorSet = data.validators.filter((it) => it.inSet); - const eligible = data.validators.length - validatorSet.length; - return (
-
-
-
-
Validator Set
-
- {validatorSet.length} -
-
-
-
Eligible
-
- {eligible} -
-
-
-
- -
-
-
- - - - - - - - - - - - - - - {data.validators.map((validator) => ( - - - - - - - - - - - ))} - -
- Address - - In Set - - Voting Power - - Grade - - Active Vouches - - Current Bid (Expiration Epoch) - - Balance - - Unlocked -
- - - {validator.inSet ? ( - - ) : ( - - )} - - {Number(validator.votingPower).toLocaleString()} - - {validator.grade.compliant ? ( - - ) : ( - - )} - {`${validator.grade.proposedBlocks.toLocaleString()} / ${validator.grade.failedBlocks.toLocaleString()}`} - - {validator.vouches.length.toLocaleString()} - - {`${validator.currentBid.currentBid.toLocaleString()} (${validator.currentBid.expirationEpoch.toLocaleString()})`} - - {Number(validator.account.balance)} - - {validator.account.slowWallet ? ( - {Number(validator.account.slowWallet.unlocked)} - ) : ( - '' - )} -
-
-
-
+ +
); diff --git a/web-app/src/modules/core/routes/Validators/components/ValidatorsStats.tsx b/web-app/src/modules/core/routes/Validators/components/ValidatorsStats.tsx new file mode 100644 index 0000000..282d59f --- /dev/null +++ b/web-app/src/modules/core/routes/Validators/components/ValidatorsStats.tsx @@ -0,0 +1,32 @@ +import { FC } from 'react'; +import { IValidator } from '../../../../interface/Validator.interface'; + +interface ValidatorsStatsProps { + validators: IValidator[]; +} + +const ValidatorsStats: FC = ({ validators }) => { + const validatorSet = validators.filter((it) => it.inSet); + const eligible = validators.length - validatorSet.length; + + return ( +
+
+
+
Validator Set
+
+ {validatorSet.length} +
+
+
+
Eligible
+
+ {eligible} +
+
+
+
+ ); +}; + +export default ValidatorsStats; diff --git a/web-app/src/modules/core/routes/Validators/components/ValidatorsTable.tsx b/web-app/src/modules/core/routes/Validators/components/ValidatorsTable.tsx new file mode 100644 index 0000000..517c997 --- /dev/null +++ b/web-app/src/modules/core/routes/Validators/components/ValidatorsTable.tsx @@ -0,0 +1,262 @@ +import { FC, useState } from 'react'; +import clsx from 'clsx'; +import { CheckIcon, XMarkIcon, ChevronUpIcon, ChevronDownIcon } from '@heroicons/react/20/solid'; +import AccountAddress from '../../../../ui/AccountAddress'; +import Money from '../../../../ui/Money'; +import { IValidator } from '../../../../interface/Validator.interface'; + +interface ValidatorsTableProps { + validators: IValidator[]; +} + +type SortOrder = 'asc' | 'desc'; + +const ValidatorsTable: FC = ({ validators }) => { + const [sortColumn, setSortColumn] = useState('currentBid'); + const [sortOrder, setSortOrder] = useState('asc'); + + const handleSort = (column: string) => { + if (sortColumn === column) { + setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc'); + } else { + setSortColumn(column); + setSortOrder('asc'); + } + }; + + const getSortedValidators = () => { + const sortedValidators = [...validators].sort((a, b) => { + let aValue: any; + let bValue: any; + + switch (sortColumn) { + case 'address': + aValue = a.address; + bValue = b.address; + break; + case 'inSet': + aValue = a.inSet; + bValue = b.inSet; + break; + case 'votingPower': + aValue = a.votingPower; + bValue = b.votingPower; + break; + case 'grade': + aValue = a.grade.compliant ? 1 : 0; + bValue = b.grade.compliant ? 1 : 0; + if (aValue === bValue) { + aValue = a.grade.proposedBlocks - a.grade.failedBlocks; + bValue = b.grade.proposedBlocks - b.grade.failedBlocks; + } + break; + case 'vouches': + aValue = a.vouches.length; + bValue = b.vouches.length; + break; + case 'currentBid': + aValue = a.currentBid.currentBid; + bValue = b.currentBid.currentBid; + break; + case 'balance': + aValue = Number(a.account.balance); + bValue = Number(b.account.balance); + break; + case 'unlocked': + aValue = a.account.slowWallet ? Number(a.account.slowWallet.unlocked) : 0; + bValue = b.account.slowWallet ? Number(b.account.slowWallet.unlocked) : 0; + break; + default: + aValue = a.address; + bValue = b.address; + } + + if (aValue === bValue) { + return a.address.localeCompare(b.address); + } + + return aValue < bValue ? -1 : 1; + }); + + if (sortOrder === 'asc') { + sortedValidators.reverse(); + } + + return sortedValidators; + }; + + const sortedValidators = getSortedValidators(); + + return ( +
+
+
+ + + + + + + + + + + + + + + {sortedValidators.map((validator) => ( + + + + + + + + + + + ))} + +
handleSort('address')} + > + Address + {sortColumn === 'address' && + (sortOrder === 'asc' ? ( + + ) : ( + + ))} + handleSort('inSet')} + > + In Set + {sortColumn === 'inSet' && + (sortOrder === 'asc' ? ( + + ) : ( + + ))} + handleSort('votingPower')} + > + Voting Power + {sortColumn === 'votingPower' && + (sortOrder === 'asc' ? ( + + ) : ( + + ))} + handleSort('grade')} + > + Grade + {sortColumn === 'grade' && + (sortOrder === 'asc' ? ( + + ) : ( + + ))} + handleSort('vouches')} + > + Active Vouches + {sortColumn === 'vouches' && + (sortOrder === 'asc' ? ( + + ) : ( + + ))} + handleSort('currentBid')} + > + Current Bid (Expiration Epoch) + {sortColumn === 'currentBid' && + (sortOrder === 'asc' ? ( + + ) : ( + + ))} + handleSort('balance')} + > + Balance + {sortColumn === 'balance' && + (sortOrder === 'asc' ? ( + + ) : ( + + ))} + handleSort('unlocked')} + > + Unlocked + {sortColumn === 'unlocked' && + (sortOrder === 'asc' ? ( + + ) : ( + + ))} +
+ + + {validator.inSet ? ( + + ) : ( + + )} + {Number(validator.votingPower).toLocaleString()} + {validator.grade.compliant ? ( + + ) : ( + + )} + {`${validator.grade.proposedBlocks.toLocaleString()} / ${validator.grade.failedBlocks.toLocaleString()}`} + + {validator.vouches.length.toLocaleString()} + + {`${validator.currentBid.currentBid.toLocaleString()} (${validator.currentBid.expirationEpoch.toLocaleString()})`} + + {Number(validator.account.balance)} + + {validator.account.slowWallet ? ( + {Number(validator.account.slowWallet.unlocked)} + ) : ( + '' + )} +
+
+
+
+ ); +}; + +export default ValidatorsTable; diff --git a/web-app/src/modules/interface/Validator.interface.ts b/web-app/src/modules/interface/Validator.interface.ts new file mode 100644 index 0000000..e8bdef4 --- /dev/null +++ b/web-app/src/modules/interface/Validator.interface.ts @@ -0,0 +1,23 @@ +export interface IValidator { + address: string; + inSet: boolean; + votingPower: number; + account: { + balance: number; + slowWallet: { + unlocked: number; + } | null; + }; + vouches: { + epoch: number; + }[]; + grade: { + compliant: boolean; + failedBlocks: number; + proposedBlocks: number; + }; + currentBid: { + currentBid: number; + expirationEpoch: number; + }; +}