Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update validator list columns #4643

Merged
merged 13 commits into from
Dec 7, 2023
165 changes: 126 additions & 39 deletions packages/ui/src/app/pages/Validators/ValidatorList.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { expect } from '@storybook/jest'
import { Meta, StoryObj } from '@storybook/react'
import { userEvent, waitFor, within } from '@storybook/testing-library'

import { Address } from '@/common/types'
import { GetMembersWithDetailsDocument } from '@/memberships/queries'
import { member } from '@/mocks/data/members'
import { joy, selectFromDropdown } from '@/mocks/helpers'
Expand Down Expand Up @@ -36,20 +37,55 @@ export default {
{ era: 699, eraReward: joy(0.123456) },
{ era: 700, eraReward: joy(0.123456) },
],
stakerRewards: [
{
eraReward: joy(0.7),
},
{
eraReward: joy(0.79),
},
{
eraReward: joy(0.3),
},
{
eraReward: joy(0.8),
},
],
stakerRewards: (address: Address) => {
const validatorRewards = [
{
address: 'j4RLnWh3DWgc9u4CMprqxfBhq3kthXhvZDmnpjEtETFVm446D',
rewards: [{ eraReward: joy(0.5) }],
},
{
address: 'j4RbTjvPyaufVVoxVGk5vEKHma1k7j5ZAQCaAL9qMKQWKAswW',
rewards: [{ eraReward: joy(0.5) }],
},
{
address: 'j4Rc8VUXGYAx7FNbVZBFU72rQw3GaCuG2AkrUQWnWTh5SpemP',
rewards: [{ eraReward: joy(0.9) }],
},
{
address: 'j4Rh1cHtZFAQYGh7Y8RZwoXbkAPtZN46FmuYpKNiR3P2Dc2oz',
rewards: [{ eraReward: joy(0.1) }],
},
{
address: 'j4RjraznxDKae1aGL2L2xzXPSf8qCjFbjuw9sPWkoiy1UqWCa',
rewards: [{ eraReward: joy(0.1) }],
},
{
address: 'j4RuqkJ2Xqf3NTVRYBUqgbatKVZ31mbK59fWnq4ZzfZvhbhbN',
rewards: [{ eraReward: joy(0.8) }],
},
{
address: 'j4RxTMa1QVucodYPfQGA2JrHxZP944dfJ8qdDDYKU4QbJCWNP',
rewards: [{ eraReward: joy(0.4) }],
},
{
address: 'j4Rxkb1w9yB6WXroB2npKjRJJxwxbD8JjSQwMZFB31cf5aZAJ',
rewards: [{ eraReward: joy(0.6) }],
},
{
address: 'j4RyLBbSUBvipuQLkjLyUGeFWEzmrnfYdpteDa2gYNoM13qEg',
rewards: [{ eraReward: joy(0.7) }],
},
{
address: 'j4ShWRXxTG4K5Q5H7KXmdWN8HnaaLwppqM7GdiSwAy3eTLsJt',
rewards: [{ eraReward: joy(0.7) }],
},
{
address: 'j4WfB3TD4tFgrJpCmUi8P3wPp3EocyC5At9ZM2YUpmKGJ1FWM',
rewards: [{ eraReward: joy(0.7) }],
},
]
return validatorRewards.find((validatorReward) => validatorReward.address === address)?.rewards ?? []
},
},
},
query: {
Expand Down Expand Up @@ -135,20 +171,51 @@ export default {
erasTotalStake: joy(130_000),
validators: {
entries: [
['j4RLnWh3DWgc9u4CMprqxfBhq3kthXhvZDmnpjEtETFVm446D'],
['j4RbTjvPyaufVVoxVGk5vEKHma1k7j5ZAQCaAL9qMKQWKAswW'],
['j4Rc8VUXGYAx7FNbVZBFU72rQw3GaCuG2AkrUQWnWTh5SpemP'],
['j4Rh1cHtZFAQYGh7Y8RZwoXbkAPtZN46FmuYpKNiR3P2Dc2oz'],
['j4RjraznxDKae1aGL2L2xzXPSf8qCjFbjuw9sPWkoiy1UqWCa'],
['j4RuqkJ2Xqf3NTVRYBUqgbatKVZ31mbK59fWnq4ZzfZvhbhbN'],
['j4RxTMa1QVucodYPfQGA2JrHxZP944dfJ8qdDDYKU4QbJCWNP'],
['j4Rxkb1w9yB6WXroB2npKjRJJxwxbD8JjSQwMZFB31cf5aZAJ'],
['j4RyLBbSUBvipuQLkjLyUGeFWEzmrnfYdpteDa2gYNoM13qEg'],
['j4ShWRXxTG4K5Q5H7KXmdWN8HnaaLwppqM7GdiSwAy3eTLsJt'],
['j4WfB3TD4tFgrJpCmUi8P3wPp3EocyC5At9ZM2YUpmKGJ1FWM'],
[
{ args: ['j4RLnWh3DWgc9u4CMprqxfBhq3kthXhvZDmnpjEtETFVm446D'] },
{ commission: 0.05 * 10 ** 9, blocked: false },
],
[
{ args: ['j4RbTjvPyaufVVoxVGk5vEKHma1k7j5ZAQCaAL9qMKQWKAswW'] },
{ commission: 0.1 * 10 ** 9, blocked: false },
],
[
{ args: ['j4Rc8VUXGYAx7FNbVZBFU72rQw3GaCuG2AkrUQWnWTh5SpemP'] },
{ commission: 0.05 * 10 ** 9, blocked: false },
],
[
{ args: ['j4Rh1cHtZFAQYGh7Y8RZwoXbkAPtZN46FmuYpKNiR3P2Dc2oz'] },
{ commission: 0.15 * 10 ** 9, blocked: false },
],
[
{ args: ['j4RjraznxDKae1aGL2L2xzXPSf8qCjFbjuw9sPWkoiy1UqWCa'] },
{ commission: 0.2 * 10 ** 9, blocked: false },
],
[
{ args: ['j4RuqkJ2Xqf3NTVRYBUqgbatKVZ31mbK59fWnq4ZzfZvhbhbN'] },
{ commission: 0.01 * 10 ** 9, blocked: false },
],
[
{ args: ['j4RxTMa1QVucodYPfQGA2JrHxZP944dfJ8qdDDYKU4QbJCWNP'] },
{ commission: 0.03 * 10 ** 9, blocked: false },
],
[
{ args: ['j4Rxkb1w9yB6WXroB2npKjRJJxwxbD8JjSQwMZFB31cf5aZAJ'] },
{ commission: 0.05 * 10 ** 9, blocked: false },
],
[
{ args: ['j4RyLBbSUBvipuQLkjLyUGeFWEzmrnfYdpteDa2gYNoM13qEg'] },
{ commission: 0.05 * 10 ** 9, blocked: false },
],
[
{ args: ['j4ShWRXxTG4K5Q5H7KXmdWN8HnaaLwppqM7GdiSwAy3eTLsJt'] },
{ commission: 0.05 * 10 ** 9, blocked: false },
],
[
{ args: ['j4WfB3TD4tFgrJpCmUi8P3wPp3EocyC5At9ZM2YUpmKGJ1FWM'] },
{ commission: 0.05 * 10 ** 9, blocked: false },
],
],
commission: 0.05 * 10 ** 9,
blocked: false,
},
},
},
Expand Down Expand Up @@ -180,24 +247,24 @@ export const TestsFilters: Story = {

await step('Verifcation Filter', async () => {
await selectFromDropdown(screen, verificationFilter, 'verified')
await waitFor(() => expect(screen.queryAllByRole('button', { name: 'Nominate' })).toHaveLength(3))
expect(screen.queryByText('unverifed')).toBeNull()
expect(screen.queryAllByRole('button', { name: 'Nominate' })).toHaveLength(3)
expect(screen.getByText('alice'))
expect(screen.queryByText('bob')).toBeNull()
await selectFromDropdown(screen, verificationFilter, 'unverified')
await waitFor(() => expect(screen.queryAllByRole('button', { name: 'Nominate' })).toHaveLength(8))
expect(screen.queryByText('verifed')).toBeNull()
expect(screen.queryAllByRole('button', { name: 'Nominate' })).toHaveLength(8)
expect(screen.queryByText('alice')).toBeNull()
expect(screen.getByText('bob'))
await selectFromDropdown(screen, verificationFilter, 'All')
})
await step('State Filter', async () => {
await selectFromDropdown(screen, stateFilter, 'active')
await waitFor(() => expect(screen.queryAllByRole('button', { name: 'Nominate' })).toHaveLength(9))
expect(screen.queryByText('waiting')).toBeNull()
expect(screen.queryAllByRole('button', { name: 'Nominate' })).toHaveLength(9)
await selectFromDropdown(screen, stateFilter, 'waiting')
await waitFor(() => expect(screen.queryAllByRole('button', { name: 'Nominate' })).toHaveLength(2))
expect(screen.queryByText('active')).toBeNull()
expect(screen.queryAllByRole('button', { name: 'Nominate' })).toHaveLength(2)
await selectFromDropdown(screen, stateFilter, 'All')
})
await step('Search', async () => {
Expand All @@ -221,17 +288,37 @@ export const TestsFilters: Story = {
await selectFromDropdown(screen, verificationFilter, 'verified')
expect(screen.queryByText('Clear all filters'))
await selectFromDropdown(screen, stateFilter, 'active')
expect(screen.queryAllByRole('button', { name: 'Nominate' })).toHaveLength(3)
await waitFor(() => expect(screen.queryAllByRole('button', { name: 'Nominate' })).toHaveLength(3))
await userEvent.click(screen.getByText('Clear all filters'))
expect(screen.queryAllByRole('button', { name: 'Nominate' })).toHaveLength(11)
await userEvent.type(searchElement, 'j4R')
await waitFor(async () => {
await userEvent.type(searchElement, '{enter}')
expect(screen.queryAllByRole('button', { name: 'Nominate' })).toHaveLength(9)
})
await waitFor(() => expect(screen.queryAllByRole('button', { name: 'Nominate' })).toHaveLength(11))
await userEvent.type(searchElement, 'j4R{enter}')
await waitFor(() => expect(screen.queryAllByRole('button', { name: 'Nominate' })).toHaveLength(9))
expect(screen.queryByText('Clear all filters'))
await userEvent.click(screen.getByText('Clear all filters'))
expect(screen.queryAllByRole('button', { name: 'Nominate' })).toHaveLength(11)
await waitFor(() => expect(screen.queryAllByRole('button', { name: 'Nominate' })).toHaveLength(11))
})

await step('Sort', async () => {
await userEvent.click(screen.getByText('Expected Nom APR'))
expect(
screen.queryAllByRole('button', { name: 'Nominate' })[0].parentElement?.querySelectorAll('p')[0].innerText ===
'18%'
).toBeTruthy()
await userEvent.click(screen.getByText('Expected Nom APR'))
expect(
screen.queryAllByRole('button', { name: 'Nominate' })[0].parentElement?.querySelectorAll('p')[0].innerText ===
'2%'
).toBeTruthy()
await userEvent.click(screen.getByText('Commission'))
expect(
screen.queryAllByRole('button', { name: 'Nominate' })[0].parentElement?.querySelectorAll('p')[1].innerText ===
'20%'
).toBeTruthy()
await userEvent.click(screen.getByText('Commission'))
expect(
screen.queryAllByRole('button', { name: 'Nominate' })[0].parentElement?.querySelectorAll('p')[1].innerText ===
'1%'
).toBeTruthy()
})
},
}
3 changes: 3 additions & 0 deletions packages/ui/src/common/utils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { BN } from '@polkadot/util'
export * from './utils/bn'

import { Reducer } from './types/helpers'
Expand Down Expand Up @@ -26,6 +27,8 @@ export const toNumber = (value: any): number => value?.toNumber?.() ?? (isNumber

export const clamp = (min: number, value: number, max: number) => Math.max(min, Math.min(max, value))

export const perbillToPercent = (perbill: BN) => perbill.toNumber() / 10 ** 7

// Objects:

interface EqualsOption {
Expand Down
7 changes: 4 additions & 3 deletions packages/ui/src/mocks/providers/api.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,9 @@ const asApiConst = (value: any) => {
}
const asApiMethod = (value: any) => {
if (isFunction(value)) {
return value
type ArgumentsType<T> = T extends (...args: infer A) => any ? A : never
type FunctionArgs = ArgumentsType<typeof value>
return (args: FunctionArgs) => of(asChainData(value(args)))
} else if (value instanceof Observable) {
return () => value
}
Expand All @@ -130,8 +132,7 @@ const asApiMethod = (value: any) => {
}

if (isObject(value) && 'entries' in value && isArray(value.entries)) {
const entries = value.entries.map((entry) => [{ args: [asChainData(entry)] }])
method.entries = () => of(entries)
method.entries = () => of(asChainData(value.entries))
}

if (isObject(value) && 'multi' in value && isArray(value.multi)) {
Expand Down
21 changes: 12 additions & 9 deletions packages/ui/src/validators/components/ValidatorItem.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,27 @@
import React from 'react'
import styled from 'styled-components'

import { encodeAddress } from '@/accounts/model/encodeAddress'
import { BadgeStatus } from '@/common/components/BadgeStatus'
import { ButtonPrimary } from '@/common/components/buttons'
import { TableListItemAsLinkHover } from '@/common/components/List'
import { Skeleton } from '@/common/components/Skeleton'
import { TextMedium, TokenValue } from '@/common/components/typography'
import { BorderRad, Colors, Sizes, Transitions } from '@/common/constants'

import { Validator } from '../types/Validator'
import { ValidatorWithDetails } from '../types/Validator'

import { ValidatorInfo } from './ValidatorInfo'
interface ValidatorItemProps {
validator: Validator
validator: ValidatorWithDetails
}
export const ValidatorItem = ({ validator }: ValidatorItemProps) => {
const { address, member, isVerified, isActive, totalRewards, APR } = validator
const { stashAccount, membership, isVerified, isActive, commission, APR, staking } = validator

return (
<ValidatorItemWrapper>
<ValidatorItemWrap>
<ValidatorInfo member={member} address={address} />
<ValidatorInfo member={membership} address={encodeAddress(stashAccount)} />
{isVerified ? (
<BadgeStatus inverted size="l">
verified
Expand All @@ -31,8 +32,10 @@ export const ValidatorItem = ({ validator }: ValidatorItemProps) => {
<BadgeStatus inverted size="l">
{isActive ? 'active' : 'waiting'}
</BadgeStatus>
<TokenValue size="xs" value={totalRewards} />
<TextMedium bold>{APR}</TextMedium>
<TokenValue size="xs" value={staking.own} />
<TokenValue size="xs" value={staking.total} />
<TextMedium bold>{APR}%</TextMedium>
<TextMedium bold>{commission}%</TextMedium>
<ButtonPrimary size="small">Nominate</ButtonPrimary>
</ValidatorItemWrap>
</ValidatorItemWrapper>
Expand All @@ -53,15 +56,15 @@ const ValidatorItemWrapper = styled.div`

export const ValidatorItemWrap = styled.div`
display: grid;
grid-template-columns: 250px 80px 80px 120px 80px 120px;
grid-template-columns: 250px 100px 80px 120px 120px 140px 100px 90px;
grid-template-rows: 1fr;
justify-content: space-between;
justify-items: end;
justify-items: start;
align-items: center;
width: 100%;
height: ${Sizes.accountHeight};
padding: 16px;
margin-left: -1px;
margin: -1px;

${Skeleton} {
min-width: 80%;
Expand Down
Loading