From 436273770602e6760883c4a491ab25f73fd7c1cc Mon Sep 17 00:00:00 2001 From: songwongtp <16089160+songwongtp@users.noreply.github.com> Date: Wed, 13 Mar 2024 17:29:53 +0700 Subject: [PATCH 1/7] feat: validator list --- CHANGELOG.md | 1 + src/lib/amplitude/types.ts | 1 + .../abi/args-form/field/ArgFieldWidget.tsx | 6 +- .../json-schema/form/widgets/SelectWidget.tsx | 6 +- src/lib/components/pagination/usePaginator.ts | 6 +- src/lib/layout/SubHeader.tsx | 27 ++-- src/lib/layout/mobile/NavDrawer.tsx | 23 +-- .../validators/components/ActiveFilter.tsx | 72 +++++++++ .../validators/components/OrderSelect.tsx | 95 +++++++++++ src/lib/pages/validators/components/index.ts | 2 + .../ValidatorsPercentDivider.tsx | 41 +++++ .../validators-table/ValidatorsTableBody.tsx | 94 +++++++++++ .../ValidatorsTableHeader.tsx | 114 +++++++++++++ .../ValidatorsTableMobileCard.tsx | 121 ++++++++++++++ .../validators-table/ValidatorsTableRow.tsx | 119 ++++++++++++++ .../components/validators-table/index.tsx | 71 +++++++++ src/lib/pages/validators/index.tsx | 150 ++++++++++++++++++ src/lib/pages/validators/types.ts | 6 + src/lib/services/validator.ts | 9 +- src/lib/services/validatorService.ts | 1 + src/lib/types/validator.ts | 1 - src/pages/[network]/validators/index.tsx | 3 + src/pages/validators/index.tsx | 3 + 23 files changed, 940 insertions(+), 32 deletions(-) create mode 100644 src/lib/pages/validators/components/ActiveFilter.tsx create mode 100644 src/lib/pages/validators/components/OrderSelect.tsx create mode 100644 src/lib/pages/validators/components/index.ts create mode 100644 src/lib/pages/validators/components/validators-table/ValidatorsPercentDivider.tsx create mode 100644 src/lib/pages/validators/components/validators-table/ValidatorsTableBody.tsx create mode 100644 src/lib/pages/validators/components/validators-table/ValidatorsTableHeader.tsx create mode 100644 src/lib/pages/validators/components/validators-table/ValidatorsTableMobileCard.tsx create mode 100644 src/lib/pages/validators/components/validators-table/ValidatorsTableRow.tsx create mode 100644 src/lib/pages/validators/components/validators-table/index.tsx create mode 100644 src/lib/pages/validators/index.tsx create mode 100644 src/lib/pages/validators/types.ts create mode 100644 src/pages/[network]/validators/index.tsx create mode 100644 src/pages/validators/index.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index a33ffbcef..e8e3b110b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Features +- [#822](https://github.com/alleslabs/celatone-frontend/pull/822) Add validator list page - [#801](https://github.com/alleslabs/celatone-frontend/pull/801) Add validator detail ui structure - [#817](https://github.com/alleslabs/celatone-frontend/pull/817) api v1 - validator voted proposals - [#816](https://github.com/alleslabs/celatone-frontend/pull/816) api v1 - validator staking provisions diff --git a/src/lib/amplitude/types.ts b/src/lib/amplitude/types.ts index 38b50cffd..4953a2706 100644 --- a/src/lib/amplitude/types.ts +++ b/src/lib/amplitude/types.ts @@ -63,6 +63,7 @@ export enum AmpEvent { TO_PROPOSAL_DETAILS = "To Proposal Detail", TO_PROPOSAL_TO_STORE_CODE = "To Proposal To Store Code", TO_PROPOSAL_TO_WHITELIST = "To Proposal To Whitelist", + TO_VALIDATORS = "To Validators", TO_VALIDATOR_DETAILS = "To Validator Detail", TO_MODULE_DETAILS = "To Module Detail", TO_MODULE_INTERACTION = "To Module Interaction", diff --git a/src/lib/components/abi/args-form/field/ArgFieldWidget.tsx b/src/lib/components/abi/args-form/field/ArgFieldWidget.tsx index 340b1b646..fedc9f05b 100644 --- a/src/lib/components/abi/args-form/field/ArgFieldWidget.tsx +++ b/src/lib/components/abi/args-form/field/ArgFieldWidget.tsx @@ -108,13 +108,15 @@ export const ArgFieldWidget = ({ ...provided, color: state.isDisabled ? "gray.700" : undefined, }), - option: (provided, state) => ({ + option: (provided) => ({ ...provided, color: "text.main", - bg: state.isSelected ? "gray.800" : "gray.900", _hover: { bg: "gray.700", }, + _selected: { + bg: "gray.800", + }, }), }} /> diff --git a/src/lib/components/json-schema/form/widgets/SelectWidget.tsx b/src/lib/components/json-schema/form/widgets/SelectWidget.tsx index db0b43338..70a2277c0 100644 --- a/src/lib/components/json-schema/form/widgets/SelectWidget.tsx +++ b/src/lib/components/json-schema/form/widgets/SelectWidget.tsx @@ -159,13 +159,15 @@ const SelectWidget = (props: WidgetProps) => { ...provided, color: state.isDisabled ? "gray.700" : undefined, }), - option: (provided, state) => ({ + option: (provided) => ({ ...provided, color: "text.main", - bg: state.isSelected ? "gray.800" : "gray.900", _hover: { bg: "gray.700", }, + _selected: { + bg: "gray.800", + }, }), }} /> diff --git a/src/lib/components/pagination/usePaginator.ts b/src/lib/components/pagination/usePaginator.ts index 192524165..937f8918b 100644 --- a/src/lib/components/pagination/usePaginator.ts +++ b/src/lib/components/pagination/usePaginator.ts @@ -31,7 +31,7 @@ export const usePaginator = ({ const { currentChainId } = useCelatoneApp(); const [totalData, setTotalData] = useState(total ?? 0); - const [pageSize, setPageSize] = useState(initialState.pageSize ?? 0); + const [pageSize, setPageSize] = useState(initialState.pageSize ?? 10); const [currentPage, setCurrentPage] = useState( initialState.currentPage ); @@ -56,9 +56,9 @@ export const usePaginator = ({ }, [totalData, pageSize]); useEffect(() => { - setPageSize(10); + setPageSize(initialState.pageSize ?? 10); setCurrentPage(1); - }, [currentChainId]); + }, [currentChainId, initialState.pageSize]); useEffect(() => { setTotalData(total ?? 0); diff --git a/src/lib/layout/SubHeader.tsx b/src/lib/layout/SubHeader.tsx index 1c57ec617..4bb32ae22 100644 --- a/src/lib/layout/SubHeader.tsx +++ b/src/lib/layout/SubHeader.tsx @@ -32,29 +32,19 @@ const SubHeader = () => { { name: "Blocks", slug: "/blocks", icon: "block" }, ]; - if (moveConfig.enabled) - subHeaderMenu.push({ - name: "Modules", - slug: "/modules", - icon: "contract-address", - }); - if (wasmConfig.enabled) subHeaderMenu.push( { name: "Codes", slug: "/codes", icon: "code" }, { name: "Contracts", slug: "/contracts", icon: "contract-address" } ); - if (govConfig.enabled) + if (moveConfig.enabled) subHeaderMenu.push({ - name: "Proposals", - slug: "/proposals", - icon: "proposal", + name: "Modules", + slug: "/modules", + icon: "contract-address", }); - if (poolConfig.enabled) - subHeaderMenu.push({ name: "Osmosis Pools", slug: "/pools", icon: "pool" }); - if (nftConfig.enabled) subHeaderMenu.push({ name: "NFTs", @@ -62,6 +52,15 @@ const SubHeader = () => { icon: "group", }); + if (govConfig.enabled) + subHeaderMenu.push( + { name: "Proposals", slug: "/proposals", icon: "proposal" }, + { name: "Validators", slug: "/validators", icon: "admin" } + ); + + if (poolConfig.enabled) + subHeaderMenu.push({ name: "Osmosis Pools", slug: "/pools", icon: "pool" }); + const isCurrentPage = useIsCurrentPage(); const activeColor = "primary.light"; diff --git a/src/lib/layout/mobile/NavDrawer.tsx b/src/lib/layout/mobile/NavDrawer.tsx index 72567c685..548e23dee 100644 --- a/src/lib/layout/mobile/NavDrawer.tsx +++ b/src/lib/layout/mobile/NavDrawer.tsx @@ -74,15 +74,6 @@ export const NavDrawer = () => { }, ] : []), - ...(govConfig.enabled - ? [ - { - name: "Proposals", - slug: "/proposals", - icon: "proposal" as IconKeys, - }, - ] - : []), ...(moveConfig.enabled ? [ { @@ -106,6 +97,20 @@ export const NavDrawer = () => { }, ] : []), + ...(govConfig.enabled + ? [ + { + name: "Proposals", + slug: "/proposals", + icon: "proposal" as IconKeys, + }, + { + name: "Validators", + slug: "/validators", + icon: "admin" as IconKeys, + }, + ] + : []), ], }, ]; diff --git a/src/lib/pages/validators/components/ActiveFilter.tsx b/src/lib/pages/validators/components/ActiveFilter.tsx new file mode 100644 index 000000000..f8441eca3 --- /dev/null +++ b/src/lib/pages/validators/components/ActiveFilter.tsx @@ -0,0 +1,72 @@ +import { Flex, Text } from "@chakra-ui/react"; +import { Select } from "chakra-react-select"; +import { useMemo } from "react"; + +import type { Option } from "lib/types"; + +interface ActiveFilterProps { + isActive: boolean; + setIsActive: (value: boolean) => void; + activeCount: Option; + inactiveCount: Option; +} + +const getOptionLabel = (label: string, count: Option) => + label + (count ? ` (${count})` : ""); + +export const ActiveFilter = ({ + isActive, + setIsActive, + activeCount, + inactiveCount, +}: ActiveFilterProps) => { + const activeOptions = useMemo( + () => [ + { label: getOptionLabel("Active validators", activeCount), value: true }, + { + label: getOptionLabel("Inactive validators", inactiveCount), + value: false, + }, + ], + [activeCount, inactiveCount] + ); + + return ( + + + Show only + + value.order === order && value.isDesc === isDesc + )} + onChange={(selectedOption) => { + if (selectedOption) { + setOrder(selectedOption.value.order); + setIsDesc(selectedOption.value.isDesc); + } + }} + chakraStyles={{ + valueContainer: (provided) => ({ + ...provided, + pl: 3, + pr: 0, + }), + dropdownIndicator: (provided) => ({ + ...provided, + px: 2, + }), + option: (provided) => ({ + ...provided, + color: "text.main", + fontSize: "16px", + _hover: { + bg: "gray.700", + }, + _selected: { + bg: "gray.800", + }, + }), + }} + isSearchable={false} + /> + +); diff --git a/src/lib/pages/validators/components/index.ts b/src/lib/pages/validators/components/index.ts new file mode 100644 index 000000000..98f388349 --- /dev/null +++ b/src/lib/pages/validators/components/index.ts @@ -0,0 +1,2 @@ +export * from "./ActiveFilter"; +export * from "./OrderSelect"; diff --git a/src/lib/pages/validators/components/validators-table/ValidatorsPercentDivider.tsx b/src/lib/pages/validators/components/validators-table/ValidatorsPercentDivider.tsx new file mode 100644 index 000000000..df9fd7888 --- /dev/null +++ b/src/lib/pages/validators/components/validators-table/ValidatorsPercentDivider.tsx @@ -0,0 +1,41 @@ +import { Flex, Text } from "@chakra-ui/react"; + +import { Tooltip } from "lib/components/Tooltip"; + +interface ValidatorsPercentDividerProps { + rank: number; + label: string; +} + +export const ValidatorsPercentDivider = ({ + rank, + label, +}: ValidatorsPercentDividerProps) => ( + +
+ + Cumulative voting power from 1-{rank} ranked validators are {">"} + {label} + + } + > + + {">"} + {label} + + +
+
+); diff --git a/src/lib/pages/validators/components/validators-table/ValidatorsTableBody.tsx b/src/lib/pages/validators/components/validators-table/ValidatorsTableBody.tsx new file mode 100644 index 000000000..c1898d444 --- /dev/null +++ b/src/lib/pages/validators/components/validators-table/ValidatorsTableBody.tsx @@ -0,0 +1,94 @@ +import type { GridProps } from "@chakra-ui/react"; + +import { ValidatorOrder } from "../../types"; +import { useCelatoneApp, useMobile } from "lib/app-provider"; +import { Loading } from "lib/components/Loading"; +import { EmptyState, ErrorFetching } from "lib/components/state"; +import { useAssetInfos } from "lib/services/assetService"; +import type { ValidatorsResponse } from "lib/services/validator"; +import type { Option } from "lib/types"; +import { coinToTokenWithValue } from "lib/utils"; + +import { ValidatorsPercentDivider } from "./ValidatorsPercentDivider"; +import { ValidatorsTableMobileCard } from "./ValidatorsTableMobileCard"; +import { ValidatorsTableRow } from "./ValidatorsTableRow"; + +interface ValidatorsTableBodyProps { + templateColumns: GridProps["templateColumns"]; + data: Option; + isLoading: boolean; + isActive: boolean; + order: ValidatorOrder; + isDesc: boolean; +} + +export const ValidatorsTableBody = ({ + templateColumns, + data, + isLoading, + isActive, + order, + isDesc, +}: ValidatorsTableBodyProps) => { + const isMobile = useMobile(); + const { + chainConfig: { + extra: { singleStakingDenom }, + }, + } = useCelatoneApp(); + const { data: assetInfos } = useAssetInfos({ withPrices: false }); + + if (isLoading) return ; + if (!data) return ; + if (!data.total) + return ( + + ); + + const displayDividers = order === ValidatorOrder.VotingPower && isDesc; + const denomToken = singleStakingDenom + ? coinToTokenWithValue(singleStakingDenom, "0", assetInfos) + : undefined; + return isMobile ? ( + <> + {data.items.map((validator) => ( + + ))} + + ) : ( + <> + {data.items.map((validator) => ( + <> + + {displayDividers && + data.metadata.percent33Rank === validator.rank && ( + + )} + {displayDividers && + data.metadata.percent66Rank === validator.rank && ( + + )} + + ))} + + ); +}; diff --git a/src/lib/pages/validators/components/validators-table/ValidatorsTableHeader.tsx b/src/lib/pages/validators/components/validators-table/ValidatorsTableHeader.tsx new file mode 100644 index 000000000..1fad67710 --- /dev/null +++ b/src/lib/pages/validators/components/validators-table/ValidatorsTableHeader.tsx @@ -0,0 +1,114 @@ +import type { GridProps } from "@chakra-ui/react"; +import { Grid } from "@chakra-ui/react"; +import { useCallback } from "react"; + +import { ValidatorOrder } from "../../types"; +import { CustomIcon } from "lib/components/icon"; +import { TableHeader } from "lib/components/table"; + +interface ValidatorsTableHeaderProps { + templateColumns: GridProps["templateColumns"]; + scrollComponentId: string; + isActive: boolean; + order: ValidatorOrder; + setOrder: (value: ValidatorOrder) => void; + isDesc: boolean; + setIsDesc: (value: boolean) => void; +} + +const SortIcon = ({ + column, + order, + isDesc, +}: { + column: ValidatorOrder; + order: ValidatorOrder; + isDesc: boolean; +}) => { + if (column !== order) return null; + return ( + + ); +}; + +export const ValidatorsTableHeader = ({ + templateColumns, + scrollComponentId, + isActive, + order, + setOrder, + isDesc, + setIsDesc, +}: ValidatorsTableHeaderProps) => { + const handleOrderChange = useCallback( + (column: ValidatorOrder) => () => { + if (order === column) setIsDesc(!isDesc); + else { + setOrder(column); + setIsDesc(column !== ValidatorOrder.Moniker); + } + }, + [isDesc, order, setIsDesc, setOrder] + ); + + return ( + + {isActive && Rank} + + Validator + + + + Voting Power + + + + Uptime (100B) + + + + Commission + + + + ); +}; diff --git a/src/lib/pages/validators/components/validators-table/ValidatorsTableMobileCard.tsx b/src/lib/pages/validators/components/validators-table/ValidatorsTableMobileCard.tsx new file mode 100644 index 000000000..17730fd65 --- /dev/null +++ b/src/lib/pages/validators/components/validators-table/ValidatorsTableMobileCard.tsx @@ -0,0 +1,121 @@ +import { Flex, Text } from "@chakra-ui/react"; +import type Big from "big.js"; + +import { useInternalNavigate } from "lib/app-provider"; +import { MobileCardTemplate, MobileLabel } from "lib/components/table"; +import { ValidatorBadge } from "lib/components/ValidatorBadge"; +import type { + Option, + Token, + TokenWithValue, + U, + ValidatorData, +} from "lib/types"; +import { + formatPrettyPercent, + formatUTokenWithPrecision, + getTokenLabel, +} from "lib/utils"; + +interface ValidatorsTableMobileCardProps { + validator: ValidatorData; + isActive: boolean; + totalVotingPower: Big; + minCommissionRate: number; + denomToken: Option; +} +export const ValidatorsTableMobileCard = ({ + validator, + isActive, + totalVotingPower, + minCommissionRate, + denomToken, +}: ValidatorsTableMobileCardProps) => { + const navigate = useInternalNavigate(); + + const isZeroUptime = !validator.uptime; + const isMinCommissionRate = minCommissionRate === validator.commissionRate; + return ( + + navigate({ + pathname: "/blocks/[validatorAddress]", + query: { validatorAddress: validator.validatorAddress }, + }) + } + topContent={ + + {isActive && ( + + + + {validator.rank} + + + )} + + + + + + } + middleContent={ + + + + + + {formatPrettyPercent( + validator.votingPower.div(totalVotingPower).toNumber(), + 2, + true + )} + + + ( + {formatUTokenWithPrecision( + validator.votingPower as U>, + denomToken?.precision ?? 0, + false, + 2 + )} + {denomToken + ? ` ${getTokenLabel(denomToken.denom, denomToken.symbol)}` + : undefined} + ) + + + + + + {formatPrettyPercent((validator.uptime ?? 0) / 100, 0, true)} + + + + + + + {formatPrettyPercent(validator.commissionRate, 2, true)} + + + + } + /> + ); +}; diff --git a/src/lib/pages/validators/components/validators-table/ValidatorsTableRow.tsx b/src/lib/pages/validators/components/validators-table/ValidatorsTableRow.tsx new file mode 100644 index 000000000..49120c54a --- /dev/null +++ b/src/lib/pages/validators/components/validators-table/ValidatorsTableRow.tsx @@ -0,0 +1,119 @@ +import type { GridProps } from "@chakra-ui/react"; +import { Grid, Text } from "@chakra-ui/react"; +import type Big from "big.js"; + +import { useInternalNavigate } from "lib/app-provider"; +import { TableRow } from "lib/components/table"; +import { ValidatorBadge } from "lib/components/ValidatorBadge"; +import type { + Option, + Token, + TokenWithValue, + U, + ValidatorAddr, + ValidatorData, +} from "lib/types"; +import { + formatPrettyPercent, + formatUTokenWithPrecision, + getTokenLabel, +} from "lib/utils"; + +interface ValidatorsTableRowProps { + templateColumns: GridProps["templateColumns"]; + isActive: boolean; + validator: ValidatorData; + totalVotingPower: Big; + minCommissionRate: number; + denomToken: Option; +} + +export const ValidatorsTableRow = ({ + templateColumns, + isActive, + validator, + totalVotingPower, + minCommissionRate, + denomToken, +}: ValidatorsTableRowProps) => { + const navigate = useInternalNavigate(); + + const onRowSelect = (validatorAddress: ValidatorAddr) => + navigate({ + pathname: "/validators/[validatorAddress]", + query: { validatorAddress }, + }); + + const isZeroUptime = !validator.uptime; + const isMinCommissionRate = minCommissionRate === validator.commissionRate; + return ( + onRowSelect(validator.validatorAddress)} + _hover={{ bg: "gray.900" }} + transition="all 0.25s ease-in-out" + cursor="pointer" + > + {isActive && ( + + + {validator.rank} + + + )} + + + + +
+ + {formatPrettyPercent( + validator.votingPower.div(totalVotingPower).toNumber(), + 2, + true + )} + + + ( + {formatUTokenWithPrecision( + validator.votingPower as U>, + denomToken?.precision ?? 0, + false, + 2 + )} + {denomToken + ? ` ${getTokenLabel(denomToken.denom, denomToken.symbol)}` + : undefined} + ) + +
+
+ + + {formatPrettyPercent((validator.uptime ?? 0) / 100, 0, true)} + + + + + {formatPrettyPercent(validator.commissionRate, 2, true)} + + +
+ ); +}; diff --git a/src/lib/pages/validators/components/validators-table/index.tsx b/src/lib/pages/validators/components/validators-table/index.tsx new file mode 100644 index 000000000..fc7393401 --- /dev/null +++ b/src/lib/pages/validators/components/validators-table/index.tsx @@ -0,0 +1,71 @@ +import { TableContainer } from "@chakra-ui/react"; + +import type { ValidatorOrder } from "../../types"; +import { useMobile } from "lib/app-provider"; +import { MobileTableContainer } from "lib/components/table"; +import type { ValidatorsResponse } from "lib/services/validator"; +import type { Option } from "lib/types"; + +import { ValidatorsTableBody } from "./ValidatorsTableBody"; +import { ValidatorsTableHeader } from "./ValidatorsTableHeader"; + +interface ValidatorsTableProps { + data: Option; + isLoading: boolean; + isActive: boolean; + order: ValidatorOrder; + setOrder: (value: ValidatorOrder) => void; + isDesc: boolean; + setIsDesc: (value: boolean) => void; + scrollComponentId: string; +} + +export const ValidatorsTable = ({ + data, + isLoading, + isActive, + order, + setOrder, + isDesc, + setIsDesc, + scrollComponentId, +}: ValidatorsTableProps) => { + const isMobile = useMobile(); + const templateColumns = `${isActive ? "64px " : ""}3fr 2fr 110px 110px`; + return ( + <> + {isMobile ? ( + + + + ) : ( + + + + + )} + + ); +}; diff --git a/src/lib/pages/validators/index.tsx b/src/lib/pages/validators/index.tsx new file mode 100644 index 000000000..b9ec7e0a0 --- /dev/null +++ b/src/lib/pages/validators/index.tsx @@ -0,0 +1,150 @@ +import { Flex, Heading, Text } from "@chakra-ui/react"; +import { useRouter } from "next/router"; +import { useEffect, useState } from "react"; + +import { AmpEvent, track } from "lib/amplitude"; +import { useMobile } from "lib/app-provider"; +import InputWithIcon from "lib/components/InputWithIcon"; +import PageContainer from "lib/components/PageContainer"; +import { Pagination } from "lib/components/pagination"; +import { usePaginator } from "lib/components/pagination/usePaginator"; +import { useDebounce } from "lib/hooks"; +import { useValidators } from "lib/services/validatorService"; + +import { ActiveFilter, OrderSelect } from "./components"; +import { ValidatorsTable } from "./components/validators-table"; +import { ValidatorOrder } from "./types"; + +const Validators = () => { + const router = useRouter(); + const isMobile = useMobile(); + + const [isActive, setIsActive] = useState(true); + const [order, setOrder] = useState(ValidatorOrder.VotingPower); + const [isDesc, setIsDesc] = useState(true); + const [search, setSearch] = useState(""); + const debouncedSearch = useDebounce(search); + + const { + pagesQuantity, + setTotalData, + currentPage, + setCurrentPage, + pageSize, + setPageSize, + offset, + } = usePaginator({ + initialState: { + pageSize: 100, + currentPage: 1, + isDisabled: false, + }, + }); + const { data, isFetching: isLoading } = useValidators( + pageSize, + offset, + isActive, + order, + isDesc, + debouncedSearch, + { + onSuccess: ({ total }) => setTotalData(total), + } + ); + + useEffect(() => { + if (router.isReady) track(AmpEvent.TO_VALIDATORS); + }, [router.isReady]); + + useEffect(() => { + setCurrentPage(1); + setPageSize(100); + }, [isActive, order, isDesc, debouncedSearch, setCurrentPage, setPageSize]); + + const scrollComponentId = "validator-table-header"; + return ( +
+ + + + Validators + + + This page displays all validators on this network + + + + + {isMobile && ( + + )} + { + const newValue = e.target.value; + setSearch(newValue); + }} + amptrackSection="validator-list-search" + /> + + + + + {data && data.total > 10 && ( + { + const size = Number(e.target.value); + setPageSize(size); + setCurrentPage(1); + }} + /> + )} + +
+ ); +}; + +export default Validators; diff --git a/src/lib/pages/validators/types.ts b/src/lib/pages/validators/types.ts new file mode 100644 index 000000000..4e7a9dfd6 --- /dev/null +++ b/src/lib/pages/validators/types.ts @@ -0,0 +1,6 @@ +export enum ValidatorOrder { + Moniker = "moniker", + VotingPower = "voting_power", + Uptime = "uptime", + Commission = "commission", +} diff --git a/src/lib/services/validator.ts b/src/lib/services/validator.ts index 6ee04b7a2..e0f725b8f 100644 --- a/src/lib/services/validator.ts +++ b/src/lib/services/validator.ts @@ -115,7 +115,14 @@ const zValidatorsResponse = z .object({ items: z.array(zValidatorData), total: z.number().nonnegative(), - total_voting_power: zBig, + metadata: z.object({ + total_voting_power: zBig, + active_count: z.number().nonnegative(), + inactive_count: z.number().nonnegative(), + percent_33_rank: z.number().positive(), + percent_66_rank: z.number().positive(), + min_commission_rate: z.coerce.number(), + }), }) .transform(snakeToCamel); export type ValidatorsResponse = z.infer; diff --git a/src/lib/services/validatorService.ts b/src/lib/services/validatorService.ts index 6783bfcde..74745d151 100644 --- a/src/lib/services/validatorService.ts +++ b/src/lib/services/validatorService.ts @@ -121,6 +121,7 @@ export const useValidators = ( getValidators(endpoint, limit, offset, isActive, sortBy, isDesc, search), { retry: 1, + keepPreviousData: true, ...options, } ); diff --git a/src/lib/types/validator.ts b/src/lib/types/validator.ts index f7a93b7da..862220cd5 100644 --- a/src/lib/types/validator.ts +++ b/src/lib/types/validator.ts @@ -22,7 +22,6 @@ export type Validator = z.infer; export const zValidatorData = z .object({ rank: z.number().nullable(), - rank_cummulative_voting_power: zBig, validator_address: zValidatorAddr, account_address: zBechAddr20, identity: z.string(), diff --git a/src/pages/[network]/validators/index.tsx b/src/pages/[network]/validators/index.tsx new file mode 100644 index 000000000..4fce8e40e --- /dev/null +++ b/src/pages/[network]/validators/index.tsx @@ -0,0 +1,3 @@ +import Validators from "lib/pages/validators"; + +export default Validators; diff --git a/src/pages/validators/index.tsx b/src/pages/validators/index.tsx new file mode 100644 index 000000000..4fce8e40e --- /dev/null +++ b/src/pages/validators/index.tsx @@ -0,0 +1,3 @@ +import Validators from "lib/pages/validators"; + +export default Validators; From 1dc4fc734df14a7ae1144615381e2a793b872899 Mon Sep 17 00:00:00 2001 From: songwongtp <16089160+songwongtp@users.noreply.github.com> Date: Wed, 13 Mar 2024 17:32:49 +0700 Subject: [PATCH 2/7] fix: changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 95e7c126e..19dfa0798 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,7 +40,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Features - [#822](https://github.com/alleslabs/celatone-frontend/pull/822) Add validator list page -- [#818](https://github.com/alleslabs/celatone-frontend/pull/818) bonded tokens voting powers replacement with real data from api v1 +- [#818](https://github.com/alleslabs/celatone-frontend/pull/818) Replace bonded tokens voting powers with real data from api v1 - [#801](https://github.com/alleslabs/celatone-frontend/pull/801) Add validator detail ui structure - [#817](https://github.com/alleslabs/celatone-frontend/pull/817) api v1 - validator voted proposals - [#816](https://github.com/alleslabs/celatone-frontend/pull/816) api v1 - validator staking provisions From 2e916a3a2326e889338bef367708b39b1372a65c Mon Sep 17 00:00:00 2001 From: songwongtp <16089160+songwongtp@users.noreply.github.com> Date: Wed, 13 Mar 2024 17:41:40 +0700 Subject: [PATCH 3/7] fix: order option label --- src/lib/pages/validators/components/OrderSelect.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/pages/validators/components/OrderSelect.tsx b/src/lib/pages/validators/components/OrderSelect.tsx index c109a7b30..2eefb8ef5 100644 --- a/src/lib/pages/validators/components/OrderSelect.tsx +++ b/src/lib/pages/validators/components/OrderSelect.tsx @@ -5,11 +5,11 @@ import { ValidatorOrder } from "../types"; const ORDER_OPTIONS = [ { - label: "Name (A to Z)", + label: "Validator Name (A to Z)", value: { order: ValidatorOrder.Moniker, isDesc: false }, }, { - label: "Name (Z to A)", + label: "Validator Name (Z to A)", value: { order: ValidatorOrder.Moniker, isDesc: true }, }, { From cf8108f52c108bf118b88bf85d210cd200adadaa Mon Sep 17 00:00:00 2001 From: songwongtp <16089160+songwongtp@users.noreply.github.com> Date: Wed, 13 Mar 2024 18:14:03 +0700 Subject: [PATCH 4/7] fix: jennie --- .../validators-table/ValidatorsTableMobileCard.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/lib/pages/validators/components/validators-table/ValidatorsTableMobileCard.tsx b/src/lib/pages/validators/components/validators-table/ValidatorsTableMobileCard.tsx index 17730fd65..e808880d9 100644 --- a/src/lib/pages/validators/components/validators-table/ValidatorsTableMobileCard.tsx +++ b/src/lib/pages/validators/components/validators-table/ValidatorsTableMobileCard.tsx @@ -46,11 +46,13 @@ export const ValidatorsTableMobileCard = ({ topContent={ {isActive && ( - + - - {validator.rank} - + + + {validator.rank} + + )} From 9b6eaf9bfef4b7801bd958de2d31ca47f69ba138 Mon Sep 17 00:00:00 2001 From: songwongtp <16089160+songwongtp@users.noreply.github.com> Date: Thu, 14 Mar 2024 10:07:59 +0700 Subject: [PATCH 5/7] fix: wording --- .../components/validators-table/ValidatorsPercentDivider.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/pages/validators/components/validators-table/ValidatorsPercentDivider.tsx b/src/lib/pages/validators/components/validators-table/ValidatorsPercentDivider.tsx index df9fd7888..f3ac42acc 100644 --- a/src/lib/pages/validators/components/validators-table/ValidatorsPercentDivider.tsx +++ b/src/lib/pages/validators/components/validators-table/ValidatorsPercentDivider.tsx @@ -17,7 +17,7 @@ export const ValidatorsPercentDivider = ({ placement="right" label={ - Cumulative voting power from 1-{rank} ranked validators are {">"} + Cumulative voting power from 1-{rank} ranked validators is {">"} {label} } From 558d705d801bf8bdfc708d022edba08bfaaa4a75 Mon Sep 17 00:00:00 2001 From: songwongtp <16089160+songwongtp@users.noreply.github.com> Date: Thu, 14 Mar 2024 10:41:13 +0700 Subject: [PATCH 6/7] feat: add validator custom icon --- src/lib/components/icon/CustomIcon.tsx | 23 +++++++++++++++++++++++ src/lib/layout/SubHeader.tsx | 2 +- src/lib/layout/mobile/NavDrawer.tsx | 2 +- 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/lib/components/icon/CustomIcon.tsx b/src/lib/components/icon/CustomIcon.tsx index 9a1ba1cda..6d17494e2 100644 --- a/src/lib/components/icon/CustomIcon.tsx +++ b/src/lib/components/icon/CustomIcon.tsx @@ -1404,6 +1404,29 @@ export const ICONS = { ), viewBox: viewboxDefault, }, + validator: { + svg: ( + <> + + + + + ), + viewBox: "5 3 17 16", + }, view: { svg: ( { if (govConfig.enabled) subHeaderMenu.push( { name: "Proposals", slug: "/proposals", icon: "proposal" }, - { name: "Validators", slug: "/validators", icon: "admin" } + { name: "Validators", slug: "/validators", icon: "validator" } ); if (poolConfig.enabled) diff --git a/src/lib/layout/mobile/NavDrawer.tsx b/src/lib/layout/mobile/NavDrawer.tsx index 548e23dee..1ded5ea9f 100644 --- a/src/lib/layout/mobile/NavDrawer.tsx +++ b/src/lib/layout/mobile/NavDrawer.tsx @@ -107,7 +107,7 @@ export const NavDrawer = () => { { name: "Validators", slug: "/validators", - icon: "admin" as IconKeys, + icon: "validator" as IconKeys, }, ] : []), From bd8c82e76857a2e2be9cee09f0cf775f1beb5f3b Mon Sep 17 00:00:00 2001 From: songwongtp <16089160+songwongtp@users.noreply.github.com> Date: Thu, 14 Mar 2024 17:24:52 +0700 Subject: [PATCH 7/7] fix: comment --- .../components/ErrorFetchingProposalInfos.tsx | 2 +- .../components/validators-table/ValidatorsTableBody.tsx | 6 +++--- src/lib/pages/validators/index.tsx | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/lib/pages/proposal-details/components/ErrorFetchingProposalInfos.tsx b/src/lib/pages/proposal-details/components/ErrorFetchingProposalInfos.tsx index 5cbe9531b..cfca205da 100644 --- a/src/lib/pages/proposal-details/components/ErrorFetchingProposalInfos.tsx +++ b/src/lib/pages/proposal-details/components/ErrorFetchingProposalInfos.tsx @@ -8,6 +8,6 @@ export const ErrorFetchingProposalInfos = ({ isParamsOnly = false, }: ErrorFetchingProposalInfosProps) => ( ); diff --git a/src/lib/pages/validators/components/validators-table/ValidatorsTableBody.tsx b/src/lib/pages/validators/components/validators-table/ValidatorsTableBody.tsx index c1898d444..dc415ad6e 100644 --- a/src/lib/pages/validators/components/validators-table/ValidatorsTableBody.tsx +++ b/src/lib/pages/validators/components/validators-table/ValidatorsTableBody.tsx @@ -1,4 +1,5 @@ import type { GridProps } from "@chakra-ui/react"; +import React from "react"; import { ValidatorOrder } from "../../types"; import { useCelatoneApp, useMobile } from "lib/app-provider"; @@ -69,9 +70,8 @@ export const ValidatorsTableBody = ({ ) : ( <> {data.items.map((validator) => ( - <> + )} - + ))} ); diff --git a/src/lib/pages/validators/index.tsx b/src/lib/pages/validators/index.tsx index b9ec7e0a0..734495059 100644 --- a/src/lib/pages/validators/index.tsx +++ b/src/lib/pages/validators/index.tsx @@ -63,7 +63,7 @@ const Validators = () => { const scrollComponentId = "validator-table-header"; return ( -
+ <> { /> )} -
+ ); };