Skip to content

Commit

Permalink
Validators list filter search (#4404)
Browse files Browse the repository at this point in the history
* add types for Validator

* add ValidatorItem component for validator table

* fix tokenValue

* add validatorLists and filter component

* complete ValidatorLists and filter

* add mocking data

* fix useValidator hook to check the state of validator

* add entries method to array for api mocking

* add waiting validators to mocking data

* remove duplicate accounts from mocking data

* mock verification state randomly in hook for test

* add filter and search  tests

* fix storybook test

* add social media to validator info

* Revert "remove route, sidebar item, tab, dashboard, modal"

This reverts commit 480cd09.

* fix the case of zero totalstaking

* Revert "Revert "remove route, sidebar item, tab, dashboard, modal""

This reverts commit 02594e1.

* fix optionVariables type mocking qn

* fix type Validator, ValidatorInfo component

* mocking validator membership in hook (temp)

* fix components according to the review

* fix storybook test, add the expectations more

* fix interaction-test

* Fix the search test

* make the process calculating apr more readable

* fix storybook interaction test according to the review

* remove startedOn

* lint fix

* fix useValidatorsList when totalStaking is zero

---------

Co-authored-by: Theophile Sandoz <[email protected]>
  • Loading branch information
eshark9312 and thesan authored Aug 7, 2023
1 parent 3691ce7 commit 3d2099e
Show file tree
Hide file tree
Showing 13 changed files with 666 additions and 10 deletions.
105 changes: 99 additions & 6 deletions packages/ui/src/app/pages/Validators/ValidatorList.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { expect } from '@storybook/jest'
import { Meta, StoryObj } from '@storybook/react'
import { userEvent, waitFor, within } from '@storybook/testing-library'

import { joy } from '@/mocks/helpers'
import { joy, selectFromDropdown } from '@/mocks/helpers'
import { MocksParameters } from '@/mocks/providers'

import { ValidatorList } from './ValidatorList'
Expand Down Expand Up @@ -32,6 +34,20 @@ 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),
},
],
},
},
query: {
Expand All @@ -50,7 +66,6 @@ export default {
'j4RxTMa1QVucodYPfQGA2JrHxZP944dfJ8qdDDYKU4QbJCWNP',
'j4Rxkb1w9yB6WXroB2npKjRJJxwxbD8JjSQwMZFB31cf5aZAJ',
'j4RyLBbSUBvipuQLkjLyUGeFWEzmrnfYdpteDa2gYNoM13qEg',
'j4S998Thq5kQHyurofh8QfHrcFN2c1T19gTdMGUVVx5EHKgky',
],
},
staking: {
Expand All @@ -72,12 +87,11 @@ export default {
j4RxTMa1QVucodYPfQGA2JrHxZP944dfJ8qdDDYKU4QbJCWNP: 140,
j4Rxkb1w9yB6WXroB2npKjRJJxwxbD8JjSQwMZFB31cf5aZAJ: 160,
j4RyLBbSUBvipuQLkjLyUGeFWEzmrnfYdpteDa2gYNoM13qEg: 160,
j4S998Thq5kQHyurofh8QfHrcFN2c1T19gTdMGUVVx5EHKgky: 220,
},
},
erasValidatorReward: joy(0.123456),
erasStakers: {
total: joy(0.1),
total: joy(400),
own: joy(0.0001),
others: [
{ who: 'j4WGdFxqTkyAgzJiTbEBeRseP12dPEvJgf2Wy9qkPa68XSP55', value: joy(0.2) },
Expand All @@ -99,10 +113,26 @@ export default {
{ who: 'j4RxTMa1QVucodYPfQGA2JrHxZP944dfJ8qdDDYKU4QbJCWNP', value: joy(0.2) },
{ who: 'j4Rxkb1w9yB6WXroB2npKjRJJxwxbD8JjSQwMZFB31cf5aZAJ', value: joy(0.2) },
{ who: 'j4RyLBbSUBvipuQLkjLyUGeFWEzmrnfYdpteDa2gYNoM13qEg', value: joy(0.2) },
{ who: 'j4S998Thq5kQHyurofh8QfHrcFN2c1T19gTdMGUVVx5EHKgky', value: joy(0.2) },
],
},
erasTotalStake: joy(130_000),
validators: {
entries: [
['j4RLnWh3DWgc9u4CMprqxfBhq3kthXhvZDmnpjEtETFVm446D'],
['j4RbTjvPyaufVVoxVGk5vEKHma1k7j5ZAQCaAL9qMKQWKAswW'],
['j4Rc8VUXGYAx7FNbVZBFU72rQw3GaCuG2AkrUQWnWTh5SpemP'],
['j4Rh1cHtZFAQYGh7Y8RZwoXbkAPtZN46FmuYpKNiR3P2Dc2oz'],
['j4RjraznxDKae1aGL2L2xzXPSf8qCjFbjuw9sPWkoiy1UqWCa'],
['j4RuqkJ2Xqf3NTVRYBUqgbatKVZ31mbK59fWnq4ZzfZvhbhbN'],
['j4RxTMa1QVucodYPfQGA2JrHxZP944dfJ8qdDDYKU4QbJCWNP'],
['j4Rxkb1w9yB6WXroB2npKjRJJxwxbD8JjSQwMZFB31cf5aZAJ'],
['j4RyLBbSUBvipuQLkjLyUGeFWEzmrnfYdpteDa2gYNoM13qEg'],
['j4ShWRXxTG4K5Q5H7KXmdWN8HnaaLwppqM7GdiSwAy3eTLsJt'],
['j4WfB3TD4tFgrJpCmUi8P3wPp3EocyC5At9ZM2YUpmKGJ1FWM'],
],
commission: 0.05 * 10 ** 9,
blocked: false,
},
},
},
},
Expand All @@ -113,4 +143,67 @@ export default {

type Story = StoryObj<typeof ValidatorList>

export const Statistics: Story = {}
export const StatisticsAndLists: Story = {}

export const TestsFilters: Story = {
play: async ({ canvasElement, step }) => {
const screen = within(canvasElement)

const searchElement = screen.getByPlaceholderText('Search')
const verificationFilter = screen.getAllByText('Verification')[0]
const stateFilter = screen.getAllByText('State')[0]

await step('Verifcation Filter', async () => {
await selectFromDropdown(screen, verificationFilter, 'verified')
expect(screen.queryByText('unverifed')).toBeNull()
expect(screen.queryAllByRole('button', { name: 'Nominate' })).toHaveLength(4)
expect(screen.getByText('alice'))
expect(screen.getByText('bob'))
await selectFromDropdown(screen, verificationFilter, 'unverified')
expect(screen.queryByText('verifed')).toBeNull()
expect(screen.queryAllByRole('button', { name: 'Nominate' })).toHaveLength(7)
expect(screen.queryByText('alice')).toBeNull()
expect(screen.queryByText('bob')).toBeNull()
await selectFromDropdown(screen, verificationFilter, 'All')
})
await step('State Filter', async () => {
await selectFromDropdown(screen, stateFilter, 'active')
expect(screen.queryByText('waiting')).toBeNull()
expect(screen.queryAllByRole('button', { name: 'Nominate' })).toHaveLength(9)
await selectFromDropdown(screen, stateFilter, 'waiting')
expect(screen.queryByText('active')).toBeNull()
expect(screen.queryAllByRole('button', { name: 'Nominate' })).toHaveLength(2)
await selectFromDropdown(screen, stateFilter, 'All')
})
await step('Search', async () => {
await userEvent.type(searchElement, 'j4Rh1c')
await waitFor(async () => {
await userEvent.type(searchElement, '{enter}')
expect(screen.queryAllByRole('button', { name: 'Nominate' })).toHaveLength(1)
})
expect(screen.queryByText('alice'))
await userEvent.clear(searchElement)
await userEvent.type(searchElement, 'j4R')
await waitFor(async () => {
await userEvent.type(searchElement, '{enter}')
expect(screen.queryAllByRole('button', { name: 'Nominate' })).toHaveLength(9)
})
expect(screen.queryByText('alice'))
expect(screen.queryByText('bob'))
})

await step('Clear Filter', async () => {
await selectFromDropdown(screen, verificationFilter, 'verified')
expect(screen.queryByText('Clear all filters'))
await selectFromDropdown(screen, stateFilter, 'active')
expect(screen.queryAllByRole('button', { name: 'Nominate' })).toHaveLength(4)
await userEvent.click(screen.getByText('Clear all filters'))
expect(screen.queryAllByRole('button', { name: 'Nominate' })).toHaveLength(11)
await userEvent.type(searchElement, 'j4R{enter}')
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)
})
},
}
7 changes: 6 additions & 1 deletion packages/ui/src/app/pages/Validators/ValidatorList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ import { Era } from '@/validators/components/statistics/Era'
import { Rewards } from '@/validators/components/statistics/Rewards'
import { Staking } from '@/validators/components/statistics/Staking'
import { ValidatorsState } from '@/validators/components/statistics/ValidatorsState'
import { ValidatorsFilter } from '@/validators/components/ValidatorsFilter'
import { ValidatorsList } from '@/validators/components/ValidatorsList'
import { useStakingStatistics } from '@/validators/hooks/useStakingStatistics'
import { useValidatorsList } from '@/validators/hooks/useValidatorsList'

export const ValidatorList = () => {
const {
Expand All @@ -25,6 +28,7 @@ export const ValidatorList = () => {
acitveNominatorsCount,
allNominatorsCount,
} = useStakingStatistics()
const { visibleValidators, filter } = useValidatorsList()

return (
<PageLayout
Expand All @@ -45,9 +49,10 @@ export const ValidatorList = () => {
<Era eraStartedOn={eraStartedOn} eraDuration={eraDuration} now={now} eraRewardPoints={eraRewardPoints} />
<Rewards totalRewards={totalRewards} lastRewards={lastRewards} />
</Statistics>
<ValidatorsFilter filter={filter} />
</RowGapBlock>
}
main={<></>}
main={<ValidatorsList validators={visibleValidators} />}
/>
)
}
10 changes: 9 additions & 1 deletion packages/ui/src/common/components/typography/TokenValue.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { formatJoyValue } from '../../model/formatters'
import { Tooltip } from '../Tooltip'

interface ValueSizingProps {
size?: 's' | 'm' | 'l'
size?: 'xs' | 's' | 'm' | 'l'
}

interface ValueProps extends ValueSizingProps {
Expand Down Expand Up @@ -67,6 +67,14 @@ export const ValueInJoys = styled(JOYSuffix)<ValueSizingProps>`
${({ size }) => {
switch (size) {
case 'xs': {
return css`
font-size: 14px;
:after {
font-size: 14px;
}
`
}
case 's': {
return css`
font-size: 16px;
Expand Down
1 change: 1 addition & 0 deletions packages/ui/src/common/constants/numbers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ export const ED = new BN(10)
export const BN_ZERO = new BN(0)
export const SECONDS_PER_BLOCK = 6
export const ERA_DURATION = 21600000
export const ERAS_PER_YEAR = 1460
7 changes: 6 additions & 1 deletion packages/ui/src/mocks/providers/api.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { AugmentedConsts, AugmentedQueries, AugmentedSubmittables } from '@polkadot/api/types'
import { RpcInterface } from '@polkadot/rpc-core/types'
import { Codec } from '@polkadot/types/types'
import { isFunction, isObject, mapValues, merge } from 'lodash'
import { isArray, isFunction, isObject, mapValues, merge } from 'lodash'
import React, { FC, useEffect, useMemo, useState } from 'react'
import { Observable, of } from 'rxjs'

Expand Down Expand Up @@ -126,5 +126,10 @@ const asApiMethod = (value: any) => {
method.size = () => of(asChainData(value.size))
}

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

return method
}
2 changes: 1 addition & 1 deletion packages/ui/src/mocks/providers/query-node.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { BLOCK_HEAD } from './api'
export { ApolloClient, gql, HttpLink, InMemoryCache } from '@apollo/client/core'
export { ApolloProvider } from '@apollo/client/react'

type OptionVariables = { where?: Record<'string', any>; orderBy?: string | string[]; limit?: number; offset?: number }
type OptionVariables = { where?: Record<string, any>; orderBy?: string | string[]; limit?: number; offset?: number }
type Options = { variables?: OptionVariables; skip?: boolean }
type Result = { loading: boolean; data: any }
type Resolver = (options?: Options) => Result
Expand Down
137 changes: 137 additions & 0 deletions packages/ui/src/validators/components/ValidatorInfo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import { Identicon } from '@polkadot/react-identicon'
import React from 'react'
import styled from 'styled-components'

import { CopyComponent } from '@/common/components/CopyComponent'
import { DiscordIcon, TelegramIcon, TwitterIcon } from '@/common/components/icons/socials'
import { DefaultTooltip, Tooltip } from '@/common/components/Tooltip'
import { BorderRad, Colors, Transitions } from '@/common/constants'
import { shortenAddress } from '@/common/model/formatters'
import { Address } from '@/common/types'
import { MemberIcons } from '@/memberships/components'
import { Avatar } from '@/memberships/components/Avatar'
import { MemberWithDetails } from '@/memberships/types'

interface ValidatorInfoProps {
address: Address
member?: MemberWithDetails
isOnDark?: boolean
}

export const ValidatorInfo = React.memo(({ address, member }: ValidatorInfoProps) => {
const twitter = member?.externalResources?.find(({ source }) => source === 'TWITTER')
const telegram = member?.externalResources?.find(({ source }) => source === 'TELEGRAM')
const discord = member?.externalResources?.find(({ source }) => source === 'DISCORD')

return (
<ValidatorInfoWrap>
<PhotoWrapper>
<AccountPhoto>
{member ? <Avatar avatarUri={member.avatar} /> : <Identicon size={40} theme={'beachball'} value={address} />}
</AccountPhoto>
</PhotoWrapper>
<ValidatorHandle className="accountName">
{member?.handle ?? 'Unknown'}
{(twitter || telegram || discord) && (
<MemberIcons>
{twitter && (
<Tooltip tooltipText={twitter.value}>
<SocialTooltip>
<TwitterIcon />
</SocialTooltip>
</Tooltip>
)}
{telegram && (
<Tooltip tooltipText={telegram.value}>
<SocialTooltip>
<TelegramIcon />
</SocialTooltip>
</Tooltip>
)}
{discord && (
<Tooltip tooltipText={discord.value}>
<SocialTooltip>
<DiscordIcon />
</SocialTooltip>
</Tooltip>
)}
</MemberIcons>
)}
</ValidatorHandle>
<AccountCopyAddress altText={shortenAddress(address)} copyText={address} />
</ValidatorInfoWrap>
)
})

const ValidatorInfoWrap = styled.div`
display: grid;
grid-template-columns: 40px 1fr;
grid-template-rows: min-content 24px 18px;
grid-column-gap: 12px;
grid-template-areas:
'accountphoto accounttype'
'accountphoto accountname'
'accountphoto accountaddress';
align-items: center;
width: 100%;
justify-self: start;
`

const AccountPhoto = styled.div`
display: flex;
justify-content: flex-end;
align-items: center;
align-content: center;
align-self: center;
height: 40px;
width: 40px;
border-radius: ${BorderRad.full};
overflow: hidden;
`

const PhotoWrapper = styled.div`
grid-area: accountphoto;
position: relative;
`

const ValidatorHandle = styled.h5`
grid-area: accountname;
max-width: 100%;
margin: 0;
padding: 0;
font-size: 16px;
line-height: 24px;
font-weight: 700;
color: ${Colors.Black[900]};
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
text-align: left;
transition: ${Transitions.all};
display: grid;
grid-auto-flow: column;
grid-column-gap: 4px;
align-items: center;
width: fit-content;
`

const AccountCopyAddress = styled(CopyComponent)`
grid-area: accountaddress;
`
const SocialTooltip = styled(DefaultTooltip)`
> svg {
width: 8px;
height; 8px;
cursor: pointer;
path {
fill: ${Colors.Black[900]};
}
}
&:hover {
border-color: ${Colors.Blue[300]} !important;
background: transparent !important;
path {
fill: ${Colors.Blue[500]};
}
}
`
Loading

2 comments on commit 3d2099e

@vercel
Copy link

@vercel vercel bot commented on 3d2099e Aug 7, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vercel
Copy link

@vercel vercel bot commented on 3d2099e Aug 7, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

pioneer-2 – ./

pioneer-2.vercel.app
pioneer-2-joystream.vercel.app
pioneer-2-git-dev-joystream.vercel.app

Please sign in to comment.