Skip to content

Commit

Permalink
Display the votes on the network proposal details page
Browse files Browse the repository at this point in the history
  • Loading branch information
csillag committed May 9, 2024
1 parent d0abbf9 commit a8531a9
Show file tree
Hide file tree
Showing 13 changed files with 535 additions and 6 deletions.
1 change: 1 addition & 0 deletions .changelog/1356.feature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Display the votes on the network proposal details page
1 change: 1 addition & 0 deletions src/app/components/ErrorDisplay/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ const errorMap: Record<AppErrors, (t: TFunction, error: ErrorPayload) => Formatt
title: t('errors.error'),
message: t('errors.storage'),
}),
[AppErrors.InvalidVote]: t => ({ title: t('errors.invalidVote'), message: null }),
}

export const errorFormatter = (t: TFunction, error: ErrorPayload) => {
Expand Down
78 changes: 78 additions & 0 deletions src/app/components/Proposals/ProposalVoteIndicator.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { FC } from 'react'
import { TFunction } from 'i18next'
import { useTranslation } from 'react-i18next'
import Box from '@mui/material/Box'
import CheckCircleIcon from '@mui/icons-material/CheckCircle'
import CancelIcon from '@mui/icons-material/Cancel'
import RemoveCircleIcon from '@mui/icons-material/RemoveCircle'
import { styled } from '@mui/material/styles'
import { COLORS } from '../../../styles/theme/colors'
import { ProposalVoteValue } from '../../../types/vote'
import { AppErrors } from '../../../types/errors'

const StyledBox = styled(Box)(({ theme }) => ({
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'space-between',
gap: 3,
flex: 1,
borderRadius: 10,
padding: theme.spacing(2, 2, 2, '10px'),
fontSize: '12px',
minWidth: '85px',
}))

const StyledIcon = styled(Box)({
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
fontSize: '18px',
})

const getStatuses = (t: TFunction) => ({
[ProposalVoteValue.abstain]: {
backgroundColor: COLORS.lightSilver,
icon: RemoveCircleIcon,
iconColor: COLORS.grayMedium,
label: t('networkProposal.vote.abstain'),
textColor: COLORS.grayExtraDark,
},
[ProposalVoteValue.yes]: {
backgroundColor: COLORS.honeydew,
icon: CheckCircleIcon,
iconColor: COLORS.eucalyptus,
label: t('networkProposal.vote.yes'),
textColor: COLORS.grayExtraDark,
},
[ProposalVoteValue.no]: {
backgroundColor: COLORS.linen,
icon: CancelIcon,
iconColor: COLORS.errorIndicatorBackground,
label: t('networkProposal.vote.no'),
textColor: COLORS.grayExtraDark,
},
})

type ProposalVoteIndicatorProps = {
vote: ProposalVoteValue
}

export const ProposalVoteIndicator: FC<ProposalVoteIndicatorProps> = ({ vote }) => {
const { t } = useTranslation()

if (!ProposalVoteValue[vote]) {
throw AppErrors.InvalidVote
}

const statusConfig = getStatuses(t)[vote]
const IconComponent = statusConfig.icon

return (
<StyledBox sx={{ backgroundColor: statusConfig.backgroundColor, color: statusConfig.textColor }}>
{statusConfig.label}
<StyledIcon sx={{ color: statusConfig.iconColor }}>
<IconComponent color="inherit" fontSize="inherit" />
</StyledIcon>
</StyledBox>
)
}
64 changes: 64 additions & 0 deletions src/app/components/Proposals/VoteTypeFilter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { FC } from 'react'
import { useTranslation } from 'react-i18next'
import Box from '@mui/material/Box'
import Chip from '@mui/material/Chip'
import Typography from '@mui/material/Typography'
import { COLORS } from '../../../styles/theme/colors'
import { ProposalVoteValue, VoteType } from '../../../types/vote'

type VoteTypeFilterProps = {
onSelect: (voteType: VoteType) => void
value?: VoteType
}

export const VoteTypeFilter: FC<VoteTypeFilterProps> = ({ onSelect, value }) => {
const { t } = useTranslation()
const options: { label: string; value: VoteType }[] = [
{
label: t('networkProposal.vote.all'),
value: 'any',
},
{
label: t('networkProposal.vote.yes'),
value: ProposalVoteValue.yes,
},
{
label: t('networkProposal.vote.abstain'),
value: ProposalVoteValue.abstain,
},
{
label: t('networkProposal.vote.no'),
value: ProposalVoteValue.no,
},
]

return (
<>
{options.map(option => {
const selected = option.value === value
return (
<Chip
key={option.value}
onClick={() => onSelect(option.value)}
clickable
color="secondary"
label={
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<Typography component="span" sx={{ fontSize: 16 }}>
{option.label}
</Typography>
</Box>
}
sx={{
mr: 3,
borderColor: COLORS.brandMedium,
backgroundColor: selected ? COLORS.brandMedium : COLORS.brandMedium15,
color: selected ? COLORS.white : COLORS.grayExtraDark,
}}
variant={selected ? 'outlined-selected' : 'outlined'}
/>
)
})}
</>
)
}
20 changes: 20 additions & 0 deletions src/app/components/Validators/DeferredValidatorLink.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { FC } from 'react'
import { Network } from '../../../types/network'
import { Layer, Validator } from '../../../oasis-nexus/api'
import { SearchScope } from '../../../types/searchScope'
import { ValidatorLink } from './ValidatorLink'

export const DeferredValidatorLink: FC<{
network: Network
address: string
validator: Validator | undefined
isError: boolean
}> = ({ network, address, validator, isError }) => {
const scope: SearchScope = { network, layer: Layer.consensus }

if (isError) {
console.log('Warning: failed to look up validators!')
}

return <ValidatorLink address={address} network={scope.network} name={validator?.media?.name} />
}
2 changes: 1 addition & 1 deletion src/app/components/Validators/ValidatorLink.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ const DesktopValidatorLink: FC<DesktopValidatorLinkProps> = ({ address, name, to
}
return (
<Link component={RouterLink} to={to}>
{name || address}
{name ?? address}
</Link>
)
}
115 changes: 115 additions & 0 deletions src/app/pages/ProposalDetailsPage/ProposalVotesCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { FC } from 'react'
import { useParams } from 'react-router-dom'
import { useRequiredScopeParam } from '../../hooks/useScopeParam'
import { SubPageCard } from '../../components/SubPageCard'
import { TablePaginationProps } from '../../components/Table/TablePagination'
import { useTranslation } from 'react-i18next'
import { Table, TableCellAlign, TableColProps } from '../../components/Table'
import { ExtendedVote, ProposalVoteValue } from '../../../types/vote'
import { PAGE_SIZE, useAllVotes, useDisplayedVotes, useWantedVoteType } from './hooks'
import { ProposalVoteIndicator } from '../../components/Proposals/ProposalVoteIndicator'
import { DeferredValidatorLink } from '../../components/Validators/DeferredValidatorLink'
import { CardHeaderWithResponsiveActions } from '../../components/CardHeaderWithResponsiveActions'
import { VoteTypeFilter } from '../../components/Proposals/VoteTypeFilter'
import { AppErrors } from '../../../types/errors'
import { ErrorBoundary } from '../../components/ErrorBoundary'

type ProposalVotesProps = {
isLoading: boolean
votes: ExtendedVote[] | undefined
rowsNumber: number
pagination: TablePaginationProps
}

const ProposalVotes: FC<ProposalVotesProps> = ({ isLoading, votes, rowsNumber, pagination }) => {
const { t } = useTranslation()
const scope = useRequiredScopeParam()

const tableColumns: TableColProps[] = [
{ key: 'index', content: <></>, width: '50px' },
{ key: 'voter', content: t('common.voter'), align: TableCellAlign.Left },
{ key: 'vote', content: t('common.vote'), align: TableCellAlign.Right },
]
const tableRows = votes?.map(vote => {
return {
key: `vote-${vote.index}`,
data: [
{
key: 'index',
content: `#${vote.index + 1}`,
},
{
key: 'voter',
content: (
<DeferredValidatorLink
network={scope.network}
address={vote.address}
isError={vote.haveValidatorsFailed}
validator={vote.validator}
/>
),
},
{
key: 'vote',
content: (
<ErrorBoundary light={true}>
<ProposalVoteIndicator vote={vote.vote as ProposalVoteValue} />
</ErrorBoundary>
),
align: TableCellAlign.Right,
},
],
}
})
return (
<Table
name={t('common.votes')}
columns={tableColumns}
rows={tableRows}
rowsNumber={rowsNumber}
isLoading={isLoading}
pagination={pagination}
/>
)
}

export const ProposalVotesView: FC = () => {
const { network } = useRequiredScopeParam()
const proposalId = parseInt(useParams().proposalId!, 10)

const { isLoading } = useAllVotes(network, proposalId)
const displayedVotes = useDisplayedVotes(network, proposalId)

if (!isLoading && displayedVotes.tablePaginationProps.selectedPage > 1 && !displayedVotes.data?.length) {
throw AppErrors.PageDoesNotExist
}

return (
<ProposalVotes
isLoading={isLoading}
votes={displayedVotes.data}
rowsNumber={PAGE_SIZE}
pagination={displayedVotes.tablePaginationProps}
/>
)
}

export const ProposalVotesCard: FC = () => {
const { t } = useTranslation()

const [wantedVoteType, setWantedVoteType] = useWantedVoteType()

return (
<SubPageCard>
<CardHeaderWithResponsiveActions
action={<VoteTypeFilter onSelect={setWantedVoteType} value={wantedVoteType} />}
disableTypography
component="h3"
title={t('common.votes')}
/>
<ErrorBoundary light={true}>
<ProposalVotesView />
</ErrorBoundary>
</SubPageCard>
)
}
Loading

0 comments on commit a8531a9

Please sign in to comment.