Skip to content

Commit

Permalink
4326-Validators list - Page and widgets (#4375)
Browse files Browse the repository at this point in the history
* Add Validators Module to the side menu and add tabs to validators page

* Add widgets

* fix widgets

* display info modal on first open the page

* get the staking informations using polkadot api

* add validators list filter

* fix widgets component and hook, add last reward

* add validators' list- draft

* add 'Total rewards', but not beyond history depth

* remove filter and search box

* fix validator page notification modal

* lint:fix

* Update packages/ui/src/validators/components/widgets/Era.tsx

Co-authored-by: l1.media <31551045+traumschule@users.noreply.github.com>

* Update packages/ui/src/validators/components/widgets/Era.tsx

Co-authored-by: l1.media <31551045+traumschule@users.noreply.github.com>

* Update packages/ui/src/validators/components/widgets/Rewards.tsx

Co-authored-by: l1.media <31551045+traumschule@users.noreply.github.com>

* Update packages/ui/src/validators/components/widgets/Rewards.tsx

Co-authored-by: l1.media <31551045+traumschule@users.noreply.github.com>

* rename 'widgets' to 'statistics','validators.tsx' to 'validatorlist.tsx'

* add 'nominators' to the widget

* add staking percentage to the widget

* add blocks and points to the widget

* fix validators page notification modal

* fix the counter for active nominators

* fix the format of staking value

* yarn lint:fix

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

* add storybook for validator list page statistics

* fix storybook, remove unused variables

* Update packages/ui/src/app/pages/Validators/ValidatorList.stories.tsx

Co-authored-by: Theophile Sandoz <theophile.sandoz@gmail.com>

* Update packages/ui/src/app/pages/Validators/ValidatorList.stories.tsx

Co-authored-by: Theophile Sandoz <theophile.sandoz@gmail.com>

* Update packages/ui/src/app/pages/Validators/ValidatorList.stories.tsx

Co-authored-by: Theophile Sandoz <theophile.sandoz@gmail.com>

* Make story values more readable

* Add a special case for `unwrap` methods

* Assign an object to unwrap

* Update packages/ui/src/common/components/charts/PercentageChart.tsx

Co-authored-by: Theophile Sandoz <theophile.sandoz@gmail.com>

* Update packages/ui/src/validators/constants/constant.ts

Co-authored-by: Theophile Sandoz <theophile.sandoz@gmail.com>

* Update packages/ui/src/validators/hooks/useStakingStatistics.tsx

Co-authored-by: Theophile Sandoz <theophile.sandoz@gmail.com>

* Update packages/ui/src/validators/hooks/useStakingStatistics.tsx

Co-authored-by: Theophile Sandoz <theophile.sandoz@gmail.com>

* Update packages/ui/src/validators/hooks/useStakingStatistics.tsx

Co-authored-by: Theophile Sandoz <theophile.sandoz@gmail.com>

* fix rewardPoints

* remove InfoModal

* fix asChainData helper

* Simplify chain unwrapped mocks

* move ERA_DRATION to common/constant/number

* remove the default value for activeEra, assign undefined instead

* pass the statistic values as props

* fix some code to make it clear

* fix some issue in Era widget

---------

Co-authored-by: l1.media <31551045+traumschule@users.noreply.github.com>
Co-authored-by: Theophile Sandoz <theophile.sandoz@gmail.com>
  • Loading branch information
3 people authored Jul 25, 2023
1 parent a4f23b7 commit 9f9514b
Show file tree
Hide file tree
Showing 13 changed files with 447 additions and 17 deletions.
116 changes: 116 additions & 0 deletions packages/ui/src/app/pages/Validators/ValidatorList.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { Meta, StoryObj } from '@storybook/react'

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

import { ValidatorList } from './ValidatorList'

type Args = object

export default {
title: 'Pages/Validators/ValidatorList',
component: ValidatorList,

parameters: {
mocks: (): MocksParameters => {
return {
chain: {
derive: {
staking: {
erasRewards: [
{ era: 688, eraReward: joy(0.123456) },
{ era: 689, eraReward: joy(0.123456) },
{ era: 690, eraReward: joy(0.123456) },
{ era: 691, eraReward: joy(0.123456) },
{ era: 692, eraReward: joy(0.123456) },
{ era: 693, eraReward: joy(0.123456) },
{ era: 694, eraReward: joy(0.123456) },
{ era: 695, eraReward: joy(0.123456) },
{ era: 696, eraReward: joy(0.123456) },
{ era: 697, eraReward: joy(0.123456) },
{ era: 698, eraReward: joy(0.123456) },
{ era: 699, eraReward: joy(0.123456) },
{ era: 700, eraReward: joy(0.123456) },
],
},
},
query: {
balances: {
totalIssuance: joy(1000000),
},
timestamp: { now: Date.now() },
session: {
validators: [
'j4RLnWh3DWgc9u4CMprqxfBhq3kthXhvZDmnpjEtETFVm446D',
'j4RbTjvPyaufVVoxVGk5vEKHma1k7j5ZAQCaAL9qMKQWKAswW',
'j4Rc8VUXGYAx7FNbVZBFU72rQw3GaCuG2AkrUQWnWTh5SpemP',
'j4Rh1cHtZFAQYGh7Y8RZwoXbkAPtZN46FmuYpKNiR3P2Dc2oz',
'j4RjraznxDKae1aGL2L2xzXPSf8qCjFbjuw9sPWkoiy1UqWCa',
'j4RuqkJ2Xqf3NTVRYBUqgbatKVZ31mbK59fWnq4ZzfZvhbhbN',
'j4RxTMa1QVucodYPfQGA2JrHxZP944dfJ8qdDDYKU4QbJCWNP',
'j4Rxkb1w9yB6WXroB2npKjRJJxwxbD8JjSQwMZFB31cf5aZAJ',
'j4RyLBbSUBvipuQLkjLyUGeFWEzmrnfYdpteDa2gYNoM13qEg',
'j4S998Thq5kQHyurofh8QfHrcFN2c1T19gTdMGUVVx5EHKgky',
],
},
staking: {
activeEra: {
index: 700,
start: Date.now() - 5400000,
},
counterForValidators: 12,
counterForNominators: 20,
erasRewardPoints: {
total: 18000,
individuals: {
j4RLnWh3DWgc9u4CMprqxfBhq3kthXhvZDmnpjEtETFVm446D: 180,
j4RbTjvPyaufVVoxVGk5vEKHma1k7j5ZAQCaAL9qMKQWKAswW: 200,
j4Rc8VUXGYAx7FNbVZBFU72rQw3GaCuG2AkrUQWnWTh5SpemP: 280,
j4Rh1cHtZFAQYGh7Y8RZwoXbkAPtZN46FmuYpKNiR3P2Dc2oz: 200,
j4RjraznxDKae1aGL2L2xzXPSf8qCjFbjuw9sPWkoiy1UqWCa: 160,
j4RuqkJ2Xqf3NTVRYBUqgbatKVZ31mbK59fWnq4ZzfZvhbhbN: 180,
j4RxTMa1QVucodYPfQGA2JrHxZP944dfJ8qdDDYKU4QbJCWNP: 140,
j4Rxkb1w9yB6WXroB2npKjRJJxwxbD8JjSQwMZFB31cf5aZAJ: 160,
j4RyLBbSUBvipuQLkjLyUGeFWEzmrnfYdpteDa2gYNoM13qEg: 160,
j4S998Thq5kQHyurofh8QfHrcFN2c1T19gTdMGUVVx5EHKgky: 220,
},
},
erasValidatorReward: joy(0.123456),
erasStakers: {
total: joy(0.1),
own: joy(0.0001),
others: [
{ who: 'j4WGdFxqTkyAgzJiTbEBeRseP12dPEvJgf2Wy9qkPa68XSP55', value: joy(0.2) },
{ who: 'j4UQEfPFnKwGuHytxs9YEouLnhnSNkPDgNm9tKeB7an3dRaiy', value: joy(0.2) },
{ who: 'j4WwTZ3fnkoXJw3D1vGVyymjaiLxM78TGyAAX41JRH8Kx6T2u', value: joy(0.2) },
{ who: 'j4WqZwj6KjB4DbxknxyJB1ZkeVrPRGmg6DUGw2YkuAy7jUERg', value: joy(0.2) },
{ who: 'j4WwTZ3fnkoXJw3D1vGVyymjaiLxM78TGyAAX41JRH8Kx6T2u', value: joy(0.2) },
{ who: 'j4Wo9377XBAvhmB35J4TkpJUHnUKmyccXhGtHCVvi6pPr9so8', value: joy(0.2) },
{ who: 'j4WwTZ3fnkoXJw3D1vGVyymjaiLxM78TGyAAX41JRH8Kx6T2u', value: joy(0.2) },
{ who: 'j4WfB3TD4tFgrJpCmUi8P3wPp3EocyC5At9ZM2YUpmKGJ1FWM', value: joy(0.2) },
{ who: 'j4WwTZ3fnkoXJw3D1vGVyymjaiLxM78TGyAAX41JRH8Kx6T2u', value: joy(0.2) },
{ who: 'j4T3XgRMUaZZL6GsMk6RXfBcjuMWxfSLnoATYkBTHh7xyjmoH', value: joy(0.2) },
{ who: 'j4W2bw7ggG69e9TZ77RP9mjem1GrbPwpbKYK7WdZiym77yzMJ', value: joy(0.2) },
{ who: 'j4UzoJUhDGpnsCWrmx9ojofwaT8KHz3azp8C1S49MSN6rYjim', value: joy(0.2) },
{ who: 'j4ShWRXxTG4K5Q5H7KXmdWN8HnaaLwppqM7GdiSwAy3eTLsJt', value: joy(0.2) },
{ who: 'j4SgrgDrzzGyfrxPe4ZgaKfByKyLo5SdsUXNfHzZJPh5R6f8q', value: joy(0.2) },
{ who: 'j4RLnWh3DWgc9u4CMprqxfBhq3kthXhvZDmnpjEtETFVm446D', value: joy(0.2) },
{ who: 'j4SgrgDrzzGyfrxPe4ZgaKfByKyLo5SdsUXNfHzZJPh5R6f8q', value: joy(0.2) },
{ 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),
},
},
},
}
},
},
} satisfies Meta<Args>

type Story = StoryObj<typeof ValidatorList>

export const Statistics: Story = {}
53 changes: 53 additions & 0 deletions packages/ui/src/app/pages/Validators/ValidatorList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import React from 'react'

import { PageLayout } from '@/app/components/PageLayout'
import { RowGapBlock } from '@/common/components/page/PageContent'
import { Statistics } from '@/common/components/statistics'
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 { useStakingStatistics } from '@/validators/hooks/useStakingStatistics'

export const ValidatorList = () => {
const {
eraStartedOn,
eraDuration,
now,
eraRewardPoints,
totalRewards,
lastRewards,
idealStaking,
currentStaking,
stakingPercentage,
activeValidatorsCount,
allValidatorsCount,
acitveNominatorsCount,
allNominatorsCount,
} = useStakingStatistics()

return (
<PageLayout
header={
<RowGapBlock gap={24}>
<Statistics>
<ValidatorsState
activeValidatorsCount={activeValidatorsCount}
allValidatorsCount={allValidatorsCount}
acitveNominatorsCount={acitveNominatorsCount}
allNominatorsCount={allNominatorsCount}
/>
<Staking
idealStaking={idealStaking}
currentStaking={currentStaking}
stakingPercentage={stakingPercentage}
/>
<Era eraStartedOn={eraStartedOn} eraDuration={eraDuration} now={now} eraRewardPoints={eraRewardPoints} />
<Rewards totalRewards={totalRewards} lastRewards={lastRewards} />
</Statistics>
</RowGapBlock>
}
main={<></>}
/>
)
}
16 changes: 9 additions & 7 deletions packages/ui/src/common/components/charts/PercentageChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,15 @@ export interface PercentageChartProps {
percentage: number
isOnBlack?: boolean
className?: string
small?: boolean
}

export const PercentageChart = ({ percentage, className, isOnBlack }: PercentageChartProps) => {
export const PercentageChart = ({ percentage, className, isOnBlack, small }: PercentageChartProps) => {
const innerPercentage = percentage <= 0 ? 0 : percentage
return (
<PercentageChartContainer className={className} isOnBlack={isOnBlack}>
<PercentageChartContainer className={className} isOnBlack={isOnBlack} small={small}>
<PercentageChartBorder>
<PercentageChartText>{Math.min(innerPercentage, 100)}%</PercentageChartText>
<PercentageChartText small={small}>{Math.min(innerPercentage, 100)}%</PercentageChartText>
<PercentageChartSvg viewBox="0 0 34 34" fill="none" color="currentColor">
<PercentageChartCircle cx="17" cy="17" r="16" percentage={Math.min(innerPercentage, 100)} />
</PercentageChartSvg>
Expand All @@ -24,13 +25,13 @@ export const PercentageChart = ({ percentage, className, isOnBlack }: Percentage
)
}

const PercentageChartContainer = styled.div<{ isOnBlack?: boolean }>`
const PercentageChartContainer = styled.div<{ isOnBlack?: boolean; small?: boolean }>`
display: flex;
position: relative;
justify-content: center;
align-items: center;
width: 44px;
height: 44px;
width: ${({ small }) => (small ? '24px' : '44px')};
height: ${({ small }) => (small ? '24px' : '44px')};
padding: 1px;
color: ${({ isOnBlack }) => (isOnBlack ? Colors.White : Colors.Black[900])};
overflow: hidden;
Expand All @@ -46,7 +47,8 @@ const PercentageChartBorder = styled.div`
border-radius: ${BorderRad.round};
`

const PercentageChartText = styled(TextInlineSmall)`
const PercentageChartText = styled(TextInlineSmall)<{ small?: boolean }>`
${({ small }) => (small ? 'font-size:9px;' : '')}
color: inherit;
font-weight: 700;
text-align: center;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export interface StatisticHeaderProps {
TooltipIcon?: React.ElementType
counter?: number
dotElement?: React.ReactNode
actionElement?: React.ReactNode
}

export const StatisticHeader = ({
Expand All @@ -24,6 +25,7 @@ export const StatisticHeader = ({
tooltipLinkURL,
counter,
dotElement,
actionElement,
TooltipIcon = TooltipDefault,
}: StatisticHeaderProps) => (
<StatsHeader>
Expand All @@ -43,6 +45,7 @@ export const StatisticHeader = ({
)}
{counter && <Counter>{counter}</Counter>}
</StatsInfo>
{actionElement ?? null}
</StatsHeader>
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ interface StatisticsLayoutProps {

export const Statistics = styled.div<StatisticsLayoutProps>`
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: ${({ gapSize }) => (gapSize === 's' ? '16px' : '24px')};
width: 100%;
max-width: 100%;
Expand Down
20 changes: 16 additions & 4 deletions packages/ui/src/common/components/typography/TokenValue.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,10 @@ interface ValueProps extends ValueSizingProps {
value?: BN | null
className?: string
isLoading?: boolean
mjoy?: boolean
}

export const TokenValue = React.memo(({ className, value, size, isLoading }: ValueProps) => {
export const TokenValue = React.memo(({ className, value, size, isLoading, mjoy }: ValueProps) => {
if (isLoading) {
return <Skeleton id="tokenValueSkeleton" variant="rect" height="32px" width="50%" />
}
Expand All @@ -29,9 +30,15 @@ export const TokenValue = React.memo(({ className, value, size, isLoading }: Val
}
return (
<Tooltip tooltipText={<JOYSuffix>{formatJoyValue(value)}</JOYSuffix>}>
<ValueInJoys className={className} size={size}>
{formatJoyValue(value, { precision: 2 })}
</ValueInJoys>
{mjoy ? (
<ValueInMJoys className={className} size={size}>
{formatJoyValue(value.divn(Math.pow(10, 6)), { precision: 2 })}
</ValueInMJoys>
) : (
<ValueInJoys className={className} size={size}>
{formatJoyValue(value, { precision: 2 })}
</ValueInJoys>
)}
</Tooltip>
)
})
Expand Down Expand Up @@ -89,3 +96,8 @@ export const ValueInJoys = styled(JOYSuffix)<ValueSizingProps>`
}
}}
`
export const ValueInMJoys = styled(ValueInJoys)`
&:after {
content: 'M${CurrencyName.integerValue}';
}
`
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 @@ -5,3 +5,4 @@ export const JOY_DECIMAL_PLACES = 10
export const ED = new BN(10)
export const BN_ZERO = new BN(0)
export const SECONDS_PER_BLOCK = 6
export const ERA_DURATION = 21600000
9 changes: 4 additions & 5 deletions packages/ui/src/mocks/helpers/asChainData.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
import { createType } from '@joystream/types'
import { mapValues } from 'lodash'

import { isDefined } from '@/common/utils'

export const asChainData = (data: any): any => {
const type = isDefined(data) ? Object.getPrototypeOf(data).constructor.name : typeof data
switch (type) {
switch (Object.getPrototypeOf(data).constructor.name) {
case 'Object':
return mapValues(data, asChainData)
return withUnwrap(mapValues(data, asChainData))

case 'Array':
return data.map(asChainData)
Expand All @@ -22,3 +19,5 @@ export const asChainData = (data: any): any => {
return data
}
}

const withUnwrap = (data: Record<any, any>) => Object.defineProperty(data, 'unwrap', { value: () => data })
62 changes: 62 additions & 0 deletions packages/ui/src/validators/components/statistics/Era.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { Option, u64 } from '@polkadot/types'
import { PalletStakingEraRewardPoints } from '@polkadot/types/lookup'
import React, { useMemo } from 'react'

import { PercentageChart } from '@/common/components/charts/PercentageChart'
import { BlockIcon } from '@/common/components/icons'
import {
NumericValue,
StatisticItem,
StatisticItemSpacedContent,
StatisticLabel,
formatDurationDate,
} from '@/common/components/statistics'
import { DurationValue } from '@/common/components/typography/DurationValue'

interface EraProps {
eraStartedOn: Option<u64> | undefined
eraDuration: number
now: u64 | undefined
eraRewardPoints: PalletStakingEraRewardPoints | undefined
}

export const Era = ({ eraStartedOn, eraDuration, now, eraRewardPoints }: EraProps) => {
const { nextReward, percentage } = useMemo(() => {
const nextReward = now && eraStartedOn && eraDuration - (Number(now) - Number(eraStartedOn))
const totalDuration = Number(eraDuration)
const percentage = nextReward ? Math.ceil(100 - (nextReward / totalDuration) * 100) : 0
return {
nextReward: formatDurationDate(nextReward ?? 0),
totalDuration: formatDurationDate(totalDuration ?? 0),
percentage,
}
}, [eraStartedOn, eraDuration, now])
return (
<StatisticItem
title="era"
tooltipText="One era consists of 6 epochs with 1 hour duration each."
tooltipTitle="Era"
tooltipLinkText="What is an era"
tooltipLinkURL="TBD"
actionElement={<PercentageChart percentage={percentage} small />}
>
<StatisticItemSpacedContent>
<StatisticLabel>Next Reward</StatisticLabel>
<div>
<DurationValue value={nextReward} />
</div>
</StatisticItemSpacedContent>
<StatisticItemSpacedContent>
<StatisticLabel>Blocks / Points</StatisticLabel>
<div>
{eraRewardPoints && (
<NumericValue>
<BlockIcon />
{eraRewardPoints.total.toNumber() / 20} / {eraRewardPoints?.total.toNumber()}
</NumericValue>
)}
</div>
</StatisticItemSpacedContent>
</StatisticItem>
)
}
Loading

2 comments on commit 9f9514b

@vercel
Copy link

@vercel vercel bot commented on 9f9514b Jul 25, 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 9f9514b Jul 25, 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-joystream.vercel.app
pioneer-2.vercel.app
pioneer-2-git-dev-joystream.vercel.app

Please sign in to comment.