Skip to content

Commit

Permalink
Proposal votes: add search by validator name
Browse files Browse the repository at this point in the history
  • Loading branch information
csillag committed May 16, 2024
1 parent 29a439e commit dfeffc9
Show file tree
Hide file tree
Showing 9 changed files with 123 additions and 39 deletions.
1 change: 1 addition & 0 deletions .changelog/1357.trivial.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Network proposal votes: add search by validator name
4 changes: 2 additions & 2 deletions src/app/components/Proposals/VoteTypeFilter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export const VoteTypeFilter: FC<VoteTypeFilterProps> = ({ onSelect, value }) =>
]

return (
<>
<Box sx={{ display: 'inline-flex' }}>
{options.map(option => {
const selected = option.value === value
return (
Expand All @@ -59,6 +59,6 @@ export const VoteTypeFilter: FC<VoteTypeFilterProps> = ({ onSelect, value }) =>
/>
)
})}
</>
</Box>
)
}
1 change: 0 additions & 1 deletion src/app/components/Search/TableSearchBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import InputAdornment from '@mui/material/InputAdornment'
import { COLORS } from '../../../styles/theme/colors'
import IconButton from '@mui/material/IconButton'
import { useScreenSize } from '../../hooks/useScreensize'
import Box from '@mui/material/Box'
import WarningIcon from '@mui/icons-material/WarningAmber'
import { typingDelay } from '../../../styles/theme'
import Typography from '@mui/material/Typography'
Expand Down
12 changes: 10 additions & 2 deletions src/app/components/Validators/DeferredValidatorLink.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,20 @@ export const DeferredValidatorLink: FC<{
address: string
validator: Validator | undefined
isError: boolean
}> = ({ network, address, validator, isError }) => {
highlightedPart?: string | undefined
}> = ({ network, address, validator, isError, highlightedPart }) => {
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} />
return (
<ValidatorLink
address={address}
network={scope.network}
name={validator?.media?.name}
highlightedPart={highlightedPart}
/>
)
}
40 changes: 31 additions & 9 deletions src/app/components/Validators/ValidatorLink.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,37 @@ import { RouteUtils } from '../../utils/route-utils'
import Typography from '@mui/material/Typography'
import { COLORS } from '../../../styles/theme/colors'
import { Network } from '../../../types/network'
import { HighlightedText } from '../HighlightedText'

type ValidatorLinkProps = {
address: string
name?: string
network: Network
alwaysTrim?: boolean
highlightedPart?: string
}

export const ValidatorLink: FC<ValidatorLinkProps> = ({ address, name, network, alwaysTrim }) => {
export const ValidatorLink: FC<ValidatorLinkProps> = ({
address,
name,
network,
alwaysTrim,
highlightedPart,
}) => {
const { isTablet } = useScreenSize()
const to = RouteUtils.getValidatorRoute(network, address)
return (
<Typography variant="mono" component="span" sx={{ color: COLORS.brandDark, fontWeight: 700 }}>
{isTablet ? (
<TabletValidatorLink address={address} name={name} to={to} />
<TabletValidatorLink address={address} name={name} to={to} highlightedPart={highlightedPart} />
) : (
<DesktopValidatorLink address={address} alwaysTrim={alwaysTrim} name={name} to={to} />
<DesktopValidatorLink
address={address}
alwaysTrim={alwaysTrim}
name={name}
to={to}
highlightedPart={highlightedPart}
/>
)}
</Typography>
)
Expand All @@ -32,21 +46,23 @@ export const ValidatorLink: FC<ValidatorLinkProps> = ({ address, name, network,
type TrimValidatorEndLinkLabelProps = {
name: string
to: string
highlightedPart?: string
}

const TrimValidatorEndLinkLabel: FC<TrimValidatorEndLinkLabelProps> = ({ name, to }) => (
<TrimEndLinkLabel label={name} to={to} trimStart={14} />
const TrimValidatorEndLinkLabel: FC<TrimValidatorEndLinkLabelProps> = ({ name, to, highlightedPart }) => (
<TrimEndLinkLabel label={name} to={to} trimStart={14} highlightedPart={highlightedPart} />
)

type TabletValidatorLinkProps = {
address: string
name?: string
to: string
highlightedPart?: string
}

const TabletValidatorLink: FC<TabletValidatorLinkProps> = ({ address, name, to }) => {
const TabletValidatorLink: FC<TabletValidatorLinkProps> = ({ address, name, to, highlightedPart }) => {
if (name) {
return <TrimValidatorEndLinkLabel name={name} to={to} />
return <TrimValidatorEndLinkLabel name={name} to={to} highlightedPart={highlightedPart} />
}
return <TrimLinkLabel label={address} to={to} />
}
Expand All @@ -55,13 +71,19 @@ type DesktopValidatorLinkProps = TabletValidatorLinkProps & {
alwaysTrim?: boolean
}

const DesktopValidatorLink: FC<DesktopValidatorLinkProps> = ({ address, name, to, alwaysTrim }) => {
const DesktopValidatorLink: FC<DesktopValidatorLinkProps> = ({
address,
name,
to,
alwaysTrim,
highlightedPart,
}) => {
if (alwaysTrim) {
return <TrimLinkLabel label={address} to={to} />
}
return (
<Link component={RouterLink} to={to}>
{name ?? address}
{name ? <HighlightedText text={name} pattern={highlightedPart} /> : address}
</Link>
)
}
46 changes: 35 additions & 11 deletions src/app/pages/ProposalDetailsPage/ProposalVotesCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ 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 { useVotes, useVoteFiltering } 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'
import Box from '@mui/material/Box'
import { NoMatchingDataMaybeClearFilters, TableSearchBar } from '../../components/Search/TableSearchBar'

type ProposalVotesProps = {
isLoading: boolean
Expand All @@ -25,6 +27,8 @@ const ProposalVotes: FC<ProposalVotesProps> = ({ isLoading, votes, rowsNumber, p
const { t } = useTranslation()
const scope = useRequiredScopeParam()

const { wantedNamePattern } = useVoteFiltering(t)

const tableColumns: TableColProps[] = [
{ key: 'index', content: <></>, width: '50px' },
{ key: 'voter', content: t('common.voter'), align: TableCellAlign.Left },
Expand All @@ -46,6 +50,7 @@ const ProposalVotes: FC<ProposalVotesProps> = ({ isLoading, votes, rowsNumber, p
address={vote.address}
isError={vote.haveValidatorsFailed}
validator={vote.validator}
highlightedPart={wantedNamePattern}
/>
),
},
Expand Down Expand Up @@ -74,41 +79,60 @@ const ProposalVotes: FC<ProposalVotesProps> = ({ isLoading, votes, rowsNumber, p
}

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

const { isLoading } = useAllVotes(network, proposalId)
const displayedVotes = useDisplayedVotes(network, proposalId)
const { clearFilters } = useVoteFiltering(t)
const { results, isLoading, isOnNonexistentPage, hasNoResultsBecauseOfFilters } = useVotes(
network,
proposalId,
t,
)

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

if (hasNoResultsBecauseOfFilters) {
return <NoMatchingDataMaybeClearFilters clearFilters={clearFilters} />
}

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

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

const [wantedVoteType, setWantedVoteType] = useWantedVoteType()
const { wantedType, setWantedType, wantedNameInput, setWantedNameInput, nameError } = useVoteFiltering(t)

return (
<SubPageCard>
<CardHeaderWithResponsiveActions
action={<VoteTypeFilter onSelect={setWantedVoteType} value={wantedVoteType} />}
action={
<>
<TableSearchBar
value={wantedNameInput}
onChange={setWantedNameInput}
placeholder={t('networkProposal.searchForVoter')}
warning={nameError}
/>
<VoteTypeFilter onSelect={setWantedType} value={wantedType} />
</>
}
disableTypography
component="h3"
title={t('common.votes')}
/>
<ErrorBoundary light={true}>
<ProposalVotesView />
<Box sx={{ height: '704px' }}>
<ProposalVotesView />
</Box>
</ErrorBoundary>
</SubPageCard>
)
Expand Down
51 changes: 38 additions & 13 deletions src/app/pages/ProposalDetailsPage/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@ import {
useGetConsensusValidators,
} from '../../../oasis-nexus/api'
import { Network } from '../../../types/network'
import { ExtendedVote, ProposalVoteValue, VoteFilter, VoteType } from '../../../types/vote'
import { getFilterForVoteType, getRandomVote } from '../../utils/vote'
import { ExtendedVote, ProposalVoteValue, VoteType } from '../../../types/vote'
import { getFilterForVoterNameFragment, getFilterForVoteType, getRandomVote } from '../../utils/vote'
import { useClientSidePagination } from '../../components/Table/useClientSidePagination'
import { NUMBER_OF_ITEMS_ON_SEPARATE_PAGE } from '../../config'
import { useTypedSearchParam } from '../../hooks/useTypedSearchParam'
import { TFunction } from 'i18next'

export type AllVotesData = List & {
isLoading: boolean
Expand Down Expand Up @@ -116,28 +117,52 @@ export const useVoteStats = (network: Network, proposalId: number): VoteStats =>
}
}

export const useWantedVoteType = () =>
useTypedSearchParam<VoteType>('vote', 'any', {
export const useVoteFiltering = (t: TFunction) => {
const [wantedType, setWantedType] = useTypedSearchParam<VoteType>('vote', 'any', {
deleteParams: ['page'],
})
const [wantedNameInput, setWantedNameInput] = useTypedSearchParam('voter', '', { deleteParams: ['page'] })
const wantedNamePattern = wantedNameInput.length < 3 ? undefined : wantedNameInput
const nameError = !!wantedNameInput && !wantedNamePattern ? t('tableSearch.error.tooShort') : undefined
const hasFilters = wantedType !== 'any' || !!wantedNamePattern
const clearFilters = () => {
setWantedType('any', { deleteParams: ['voter', 'page'] })
}
return {
wantedType,
setWantedType,
wantedNameInput,
setWantedNameInput,
wantedNamePattern,
nameError,
hasFilters,
clearFilters,
}
}

const useWantedVoteFilter = (): VoteFilter => getFilterForVoteType(useWantedVoteType()[0])

export const PAGE_SIZE = NUMBER_OF_ITEMS_ON_SEPARATE_PAGE

export const useDisplayedVotes = (network: Network, proposalId: number) => {
const filter = useWantedVoteFilter()
export const useVotes = (network: Network, proposalId: number, t: TFunction) => {
const { hasFilters, wantedType, wantedNamePattern } = useVoteFiltering(t)
const typeFilter = getFilterForVoteType(wantedType)
const nameFilter = getFilterForVoterNameFragment(wantedNamePattern)

const pagination = useClientSidePagination<ExtendedVote, AllVotesData>({
paramName: 'page',
clientPageSize: PAGE_SIZE,
clientPageSize: NUMBER_OF_ITEMS_ON_SEPARATE_PAGE,
serverPageSize: 1000,
filter,
filter: (vote: ExtendedVote) => typeFilter(vote) && nameFilter(vote),
})

// Get all the votes
const allVotes = useAllVotes(network, proposalId)

// Get the section of the votes that we should display in the table
return pagination.getResults(allVotes)
const results = pagination.getResults(allVotes)

const { isLoading } = allVotes
const isOnFirstPage = results.tablePaginationProps.selectedPage === 1
const hasData = !!results.data?.length
const isOnNonexistentPage = !isLoading && !isOnFirstPage && !hasData
const hasNoResultsBecauseOfFilters = !isLoading && !hasData && isOnFirstPage && hasFilters

return { results, isLoading, isOnNonexistentPage, hasNoResultsBecauseOfFilters }
}
6 changes: 5 additions & 1 deletion src/app/utils/vote.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ProposalVoteValue, VoteFilter, VoteType } from '../../types/vote'
import { ExtendedVote, ProposalVoteValue, VoteFilter, VoteType } from '../../types/vote'
import { hasTextMatch } from '../components/HighlightedText/text-matching'

export const getRandomVote = (): ProposalVoteValue =>
[ProposalVoteValue.yes, ProposalVoteValue.no, ProposalVoteValue.abstain][Math.floor(Math.random() * 3)]
Expand All @@ -11,3 +12,6 @@ const voteFilters: Record<VoteType, VoteFilter> = {
}

export const getFilterForVoteType = (voteType: VoteType): VoteFilter => voteFilters[voteType]

export const getFilterForVoterNameFragment = (fragment: string | undefined) =>
fragment ? (vote: ExtendedVote) => hasTextMatch(vote.validator?.media?.name, [fragment]) : () => true
1 change: 1 addition & 0 deletions src/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,7 @@
"passed": "Passed",
"rejected": "Rejected"
},
"searchForVoter": "Search for voter",
"type": {
"upgrade": "Upgrade",
"parameterUpgrade": "Parameter upgrade",
Expand Down

0 comments on commit dfeffc9

Please sign in to comment.