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

4326-Validators list - Page and widgets #4375

Merged
merged 47 commits into from
Jul 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
269099f
Add Validators Module to the side menu and add tabs to validators page
eshark9312 May 2, 2023
4bcb114
Add widgets
eshark9312 May 3, 2023
fe54303
fix widgets
eshark9312 May 4, 2023
3a2a812
display info modal on first open the page
eshark9312 May 4, 2023
6eefcff
get the staking informations using polkadot api
eshark9312 May 9, 2023
9a6a799
add validators list filter
eshark9312 May 12, 2023
fcd9a14
fix widgets component and hook, add last reward
eshark9312 May 23, 2023
2b87f8f
add validators' list- draft
eshark9312 May 23, 2023
796a230
add 'Total rewards', but not beyond history depth
eshark9312 May 24, 2023
15ce7ef
remove filter and search box
eshark9312 Jun 1, 2023
f972f42
fix validator page notification modal
eshark9312 Jun 13, 2023
08bb363
lint:fix
eshark9312 Jun 13, 2023
bec3a5c
Update packages/ui/src/validators/components/widgets/Era.tsx
eshark9312 Jun 14, 2023
ae82669
Update packages/ui/src/validators/components/widgets/Era.tsx
eshark9312 Jun 14, 2023
5880a41
Update packages/ui/src/validators/components/widgets/Rewards.tsx
eshark9312 Jun 14, 2023
c0c5e9a
Update packages/ui/src/validators/components/widgets/Rewards.tsx
eshark9312 Jun 15, 2023
f0374df
rename 'widgets' to 'statistics','validators.tsx' to 'validatorlist.tsx'
eshark9312 Jun 15, 2023
22f9602
add 'nominators' to the widget
eshark9312 Jun 16, 2023
b4c7c46
add staking percentage to the widget
eshark9312 Jun 16, 2023
e264e1e
add blocks and points to the widget
eshark9312 Jun 16, 2023
5d09ef6
fix validators page notification modal
eshark9312 Jun 16, 2023
d5f34f2
fix the counter for active nominators
eshark9312 Jun 19, 2023
e225bb6
fix the format of staking value
eshark9312 Jun 19, 2023
484a510
yarn lint:fix
eshark9312 Jun 19, 2023
480cd09
remove route, sidebar item, tab, dashboard, modal
eshark9312 Jun 22, 2023
94fd84f
add storybook for validator list page statistics
eshark9312 Jun 26, 2023
970824a
fix storybook, remove unused variables
eshark9312 Jun 27, 2023
44b3f4a
Update packages/ui/src/app/pages/Validators/ValidatorList.stories.tsx
eshark9312 Jun 28, 2023
97f5b58
Update packages/ui/src/app/pages/Validators/ValidatorList.stories.tsx
eshark9312 Jun 28, 2023
b027c79
Update packages/ui/src/app/pages/Validators/ValidatorList.stories.tsx
eshark9312 Jun 28, 2023
3f38195
Make story values more readable
thesan Jun 29, 2023
0b24a80
Add a special case for `unwrap` methods
thesan Jun 29, 2023
bde8bb7
Assign an object to unwrap
thesan Jun 29, 2023
384c159
Update packages/ui/src/common/components/charts/PercentageChart.tsx
eshark9312 Jun 30, 2023
889dd70
Update packages/ui/src/validators/constants/constant.ts
eshark9312 Jun 30, 2023
59de400
Update packages/ui/src/validators/hooks/useStakingStatistics.tsx
eshark9312 Jun 30, 2023
4f77503
Update packages/ui/src/validators/hooks/useStakingStatistics.tsx
eshark9312 Jun 30, 2023
18809bc
Update packages/ui/src/validators/hooks/useStakingStatistics.tsx
eshark9312 Jun 30, 2023
79214f2
fix rewardPoints
eshark9312 Jun 30, 2023
b757b1e
remove InfoModal
eshark9312 Jun 30, 2023
7cf3349
fix asChainData helper
eshark9312 Jul 21, 2023
e0d19fb
Simplify chain unwrapped mocks
thesan Jul 24, 2023
25ce568
move ERA_DRATION to common/constant/number
eshark9312 Jul 24, 2023
ab8a2dd
remove the default value for activeEra, assign undefined instead
eshark9312 Jul 24, 2023
afd3e5a
pass the statistic values as props
eshark9312 Jul 24, 2023
4a11d92
fix some code to make it clear
eshark9312 Jul 24, 2023
43c3824
fix some issue in Era widget
eshark9312 Jul 24, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 (
eshark9312 marked this conversation as resolved.
Show resolved Hide resolved
<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