feat: enhance polling functionality with voting transparency on the governance page#683
feat: enhance polling functionality with voting transparency on the governance page#683ibsule wants to merge 3 commits into
Conversation
…overnance page - Updated GraphQL queries to include additional fields for votes, such as choiceID, voter, voteStake, and nonVoteStake. - Introduced a new `parsePollText` function to handle proposal parsing from IPFS. - Enhanced the PollVotingWidget component to display a link to view votes and improved the layout for better user experience. - Added a new PollVotingTable component for displaying detailed vote information. - Updated the voting page to support tab navigation between overview and votes, improving accessibility to voting data. These changes aim to provide a more comprehensive voting experience and better data handling for polls.
…te components - Removed duplicate imports and organized import statements for clarity in DesktopVoteTable, index, MobileVoteCards, and MobileVoteView components. - Ensured consistent formatting and spacing in various files to enhance readability. - Updated GraphQL queries to maintain structure and improve maintainability. - Minor adjustments to the PollVotingWidget and PollVotePopover components for better integration.
- Updated the PollVote components to include vote stake information, allowing for better transparency in voting data. - Modified the `onSelect` function to pass `voteStake` along with the voter's address and ENS name. - Introduced a `formatVoteStake` function to format and display vote stakes consistently across DesktopVoteTable, MobileVoteCards, and MobileVoteView components. - Adjusted GraphQL queries and state management to accommodate the new vote stake data, improving overall functionality and user experience.
|
@ibsule is attempting to deploy a commit to the Livepeer Foundation Team on Vercel. A member of the Team first needs to authorize it. |
|
Caution Review failedFailed to post review comments 📝 WalkthroughWalkthroughThis PR extends voting transparency to the Governance page, bringing vote visibility (who voted, how, and for what stake) to LIP polls. It adds GraphQL queries for vote events, builds responsive vote display components for desktop and mobile, and integrates tab navigation between poll overview and vote details on the voting page. ChangesGovernance Voting Transparency
Sequence DiagramsequenceDiagram
participant User
participant Page as voting/[poll].tsx
participant Widget as PollVotingWidget
participant Table as PollVotingTable
participant Apollo as Apollo Query
participant Popover as PollVotePopover
User->>Page: Load governance poll page
Page->>Table: Render vote table (view=votes)
Table->>Apollo: useQuery(poll votes + voteEvents)
Apollo-->>Table: votes with ENS, tx hash, timestamp
Table->>Page: Render desktop table or mobile cards
User->>Table: Click voter row/card
Table->>Popover: Show voter's vote history
Popover->>Apollo: useVoteEventsQuery(voter filter)
Apollo-->>Popover: Sorted vote events
Popover->>Page: Render PollVoteDetail list
User->>Page: Click "View votes" link in widget
Widget->>Page: Navigate to view=votes
🎯 3 (Moderate) | ⏱️ ~25 minutes 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 9
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@components/PollVote/index.tsx`:
- Around line 114-118: The Index component currently ignores the error returned
by useVotes, causing request failures to be rendered as an empty “No votes
found” state; update Index to consume the error value returned from useVotes
(the error/pollError or pollVoteEventsError aggregated into error) and render an
explicit error state (message and optional retry UI) when error is truthy before
the empty-state logic, ensuring you still respect votesLoading/loading to show
spinners while loading and only show the empty-state when there is no error and
no votes.
In `@components/PollVote/MobileVoteView.tsx`:
- Around line 75-77: In MobileVoteView.tsx there are external anchor elements
that set target="_blank" (the one rendering
href={`https://explorer.livepeer.org/accounts/${vote.voter}/delegating`} and the
other at the later occurrence) but do not include rel protection; update both
anchor elements (in the MobileVoteView component) to add rel="noopener
noreferrer" alongside target="_blank" to prevent reverse-tabnabbing and other
security issues.
In `@components/PollVote/PollVoteDetail.tsx`:
- Around line 80-82: Add rel="noopener noreferrer" to any anchor that opens in a
new tab to prevent reverse-tabnabbing; specifically update the anchor rendering
href={`/voting/${vote.poll.id}`} (inside the PollVoteDetail component) and the
other anchor instance around lines ~185-187 to include rel="noopener noreferrer"
alongside target="_blank" so both links use target="_blank" rel="noopener
noreferrer".
In `@components/PollVote/PollVotePopover.tsx`:
- Around line 41-43: PollVotePopover is currently counting votes by checking
choiceID === "0" and "1" which undercounts when events use enum keys like
PollChoice.Yes/PollChoice.No; update the counting to normalize each
voteEvent.choiceID via the existing POLL_VOTES/VOTING_SUPPORT_MAP semantics (or
a small helper normalizeChoiceID) and then compute totals by testing the
normalized support value (e.g., SUPPORT_FOR vs SUPPORT_AGAINST) rather than raw
"0"/"1" strings so voteEvents.map/ filter logic uses the canonical support
mapping for for/against/abstain.
- Around line 60-62: In PollVotePopover (the JSX anchor that renders
href={`https://explorer.livepeer.org/accounts/${voter}/delegating`} and uses
target="_blank"), add the rel="noopener noreferrer" attribute to the anchor
element to prevent reverse-tabnabbing; update the anchor in the PollVotePopover
component where the external account link is created to include rel="noopener
noreferrer".
In `@lib/api/polls.ts`:
- Around line 64-69: parsePollText currently calls catIpfsJson(proposal) and
parsePollIpfs without handling IPFS/network errors, so wrap the fetch and parse
in a try/catch inside parsePollText (the function returning Promise<Fm | null)
and return null on any failure; ensure you call catIpfsJson<IpfsPoll>(proposal)
and then parsePollIpfs(ipfsObject) inside the try, and in the catch optionally
log the error (e.g., via console.error or existing logger) before returning null
to avoid unhandled promise rejections.
In `@pages/voting/`[poll].tsx:
- Around line 314-316: The current implementation hides tab panes with CSS
(using view) but keeps both component trees mounted so voteContent() still
executes; change to conditional JSX rendering so only the active pane mounts by
using a state/derived variable (e.g., safeView or view) to choose between
rendering the Overview component and the Votes component (replace the Box
wrappers that use display toggles with conditional expressions like safeView ===
"overview" ? <Overview .../> : <Votes .../>), and ensure voteContent() is only
called/used inside the Votes branch so expensive work runs only when Votes is
active.
- Line 55: The query-derived view value is not validated: normalize it by
creating a safeView (e.g., const safeView = view === "votes" ? "votes" :
"overview") and use safeView everywhere the component reads tab state or
conditionally renders (replace uses of view in tab state initialization and the
conditional rendering blocks around the overview/votes content, e.g., where view
is referenced at the tab state and the render checks currently at lines ~315 and
~469); this ensures any unsupported query value falls back to "overview" and
prevents empty main content.
In `@queries/voteEvents.graphql`:
- Around line 1-6: The voteEvents query call in
components/PollVote/PollVotePopover.tsx is missing a bounded `first` variable
and can return unbounded results; update the useVoteEventsQuery invocation in
PollVotePopover (the hook call) to pass a safe limit (e.g., `first: 200`)
consistent with components/PollVote/index.tsx, or alternatively set a default in
the GraphQL definition by changing `$first: Int` to `$first: Int = 200` in
queries/voteEvents.graphql so the query is always paginated.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 346f92d3-4693-46b7-a689-465fd40a8b1e
📒 Files selected for processing (13)
apollo/subgraph.tscomponents/PollVote/DesktopVoteTable.tsxcomponents/PollVote/MobileVoteCards.tsxcomponents/PollVote/MobileVoteView.tsxcomponents/PollVote/PollVoteDetail.tsxcomponents/PollVote/PollVotePopover.tsxcomponents/PollVote/index.tsxcomponents/PollVotingWidget/index.tsxlib/api/polls.tslib/api/types/votes.tspages/voting/[poll].tsxqueries/poll.graphqlqueries/voteEvents.graphql
| return { | ||
| votes, | ||
| loading: loading || votesLoading, | ||
| error: pollError || pollVoteEventsError, | ||
| }; |
There was a problem hiding this comment.
Query errors are dropped and rendered as empty results
- Problem:
useVotesexposeserror, butIndexignores it (Line 133) and can render “No votes found” on request failures. - Why it matters: Users see incorrect state, and operational issues are hidden.
- Suggested fix: Consume
errorinIndexand render a dedicated error state/message (or retry UI) before empty-state logic.
Also applies to: 133-177
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@components/PollVote/index.tsx` around lines 114 - 118, The Index component
currently ignores the error returned by useVotes, causing request failures to be
rendered as an empty “No votes found” state; update Index to consume the error
value returned from useVotes (the error/pollError or pollVoteEventsError
aggregated into error) and render an explicit error state (message and optional
retry UI) when error is truthy before the empty-state logic, ensuring you still
respect votesLoading/loading to show spinners while loading and only show the
empty-state when there is no error and no votes.
| href={`https://explorer.livepeer.org/accounts/${vote.voter}/delegating`} | ||
| target="_blank" | ||
| css={{ |
There was a problem hiding this comment.
target="_blank" links missing rel protection
- Problem: External links opened in a new tab (Line 76, Line 146) omit
rel="noopener noreferrer". - Why it matters: This allows reverse-tabnabbing and weakens security posture.
- Suggested fix: Add
rel="noopener noreferrer"to both links.
Also applies to: 145-147
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@components/PollVote/MobileVoteView.tsx` around lines 75 - 77, In
MobileVoteView.tsx there are external anchor elements that set target="_blank"
(the one rendering
href={`https://explorer.livepeer.org/accounts/${vote.voter}/delegating`} and the
other at the later occurrence) but do not include rel protection; update both
anchor elements (in the MobileVoteView component) to add rel="noopener
noreferrer" alongside target="_blank" to prevent reverse-tabnabbing and other
security issues.
| href={`/voting/${vote.poll.id}`} | ||
| target="_blank" | ||
| css={{ |
There was a problem hiding this comment.
New-tab links need rel="noopener noreferrer"
- Problem: Links with
target="_blank"(Line 81, Line 186) do not setrel. - Why it matters: Creates reverse-tabnabbing exposure.
- Suggested fix: Add
rel="noopener noreferrer"on both link instances.
Also applies to: 185-187
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@components/PollVote/PollVoteDetail.tsx` around lines 80 - 82, Add
rel="noopener noreferrer" to any anchor that opens in a new tab to prevent
reverse-tabnabbing; specifically update the anchor rendering
href={`/voting/${vote.poll.id}`} (inside the PollVoteDetail component) and the
other anchor instance around lines ~185-187 to include rel="noopener noreferrer"
alongside target="_blank" so both links use target="_blank" rel="noopener
noreferrer".
| for: voteEvents.filter((v) => v.choiceID === "0").length, | ||
| against: voteEvents.filter((v) => v.choiceID === "1").length, | ||
| }; |
There was a problem hiding this comment.
Vote summary can undercount due to hardcoded choice keys
- Problem: Stats only count
choiceID === "0"and"1"(Line 41-42). - Why it matters: If vote events come through
PollChoice.Yes/No, totals for “For/Against” show incorrect values. - Suggested fix: Normalize with
POLL_VOTES/VOTING_SUPPORT_MAPsemantics (handle both"0"/"1"andYes/No) before counting.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@components/PollVote/PollVotePopover.tsx` around lines 41 - 43,
PollVotePopover is currently counting votes by checking choiceID === "0" and "1"
which undercounts when events use enum keys like PollChoice.Yes/PollChoice.No;
update the counting to normalize each voteEvent.choiceID via the existing
POLL_VOTES/VOTING_SUPPORT_MAP semantics (or a small helper normalizeChoiceID)
and then compute totals by testing the normalized support value (e.g.,
SUPPORT_FOR vs SUPPORT_AGAINST) rather than raw "0"/"1" strings so
voteEvents.map/ filter logic uses the canonical support mapping for
for/against/abstain.
| href={`https://explorer.livepeer.org/accounts/${voter}/delegating`} | ||
| target="_blank" | ||
| css={{ |
There was a problem hiding this comment.
Missing rel on external new-tab account link
- Problem: Link opens with
target="_blank"(Line 61) withoutrel="noopener noreferrer". - Why it matters: Reverse-tabnabbing risk.
- Suggested fix: Add
rel="noopener noreferrer".
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@components/PollVote/PollVotePopover.tsx` around lines 60 - 62, In
PollVotePopover (the JSX anchor that renders
href={`https://explorer.livepeer.org/accounts/${voter}/delegating`} and uses
target="_blank"), add the rel="noopener noreferrer" attribute to the anchor
element to prevent reverse-tabnabbing; update the anchor in the PollVotePopover
component where the external account link is created to include rel="noopener
noreferrer".
| export const parsePollText = async (proposal: string): Promise<Fm | null> => { | ||
| const ipfsObject = await catIpfsJson<IpfsPoll>(proposal); | ||
| const attributes = parsePollIpfs(ipfsObject); | ||
|
|
||
| return attributes; | ||
| }; |
There was a problem hiding this comment.
Unhandled IPFS fetch failures in parsePollText
- Problem:
catIpfsJsonerrors are not caught inparsePollText(Line 65), so failed IPFS reads reject the promise. - Why it matters: Consumers can hit unhandled async failures and lose vote-title rendering, especially on intermittent IPFS/network issues.
- Suggested fix: Wrap the fetch/parse in
try/catchand returnnullon failure.
Proposed fix
export const parsePollText = async (proposal: string): Promise<Fm | null> => {
- const ipfsObject = await catIpfsJson<IpfsPoll>(proposal);
- const attributes = parsePollIpfs(ipfsObject);
-
- return attributes;
+ try {
+ const ipfsObject = await catIpfsJson<IpfsPoll>(proposal);
+ return parsePollIpfs(ipfsObject);
+ } catch {
+ return null;
+ }
};📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export const parsePollText = async (proposal: string): Promise<Fm | null> => { | |
| const ipfsObject = await catIpfsJson<IpfsPoll>(proposal); | |
| const attributes = parsePollIpfs(ipfsObject); | |
| return attributes; | |
| }; | |
| export const parsePollText = async (proposal: string): Promise<Fm | null> => { | |
| try { | |
| const ipfsObject = await catIpfsJson<IpfsPoll>(proposal); | |
| return parsePollIpfs(ipfsObject); | |
| } catch { | |
| return null; | |
| } | |
| }; |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@lib/api/polls.ts` around lines 64 - 69, parsePollText currently calls
catIpfsJson(proposal) and parsePollIpfs without handling IPFS/network errors, so
wrap the fetch and parse in a try/catch inside parsePollText (the function
returning Promise<Fm | null) and return null on any failure; ensure you call
catIpfsJson<IpfsPoll>(proposal) and then parsePollIpfs(ipfsObject) inside the
try, and in the catch optionally log the error (e.g., via console.error or
existing logger) before returning null to avoid unhandled promise rejections.
|
|
||
| const [pollData, setPollData] = useState<PollExtended | null>(null); | ||
| const { query } = router; | ||
| const view = query?.view?.toString().toLowerCase() || "overview"; |
There was a problem hiding this comment.
Normalize view to supported values before rendering.
- Problem: Line 55 accepts any
viewquery value, but Line 315 and Line 469 only render content for"overview"or"votes". - Why it matters: Invalid/stale URLs can render an empty main content area.
- Suggested fix: Derive a safe value, e.g.
const safeView = view === "votes" ? "votes" : "overview";, and usesafeViewfor tab state and conditional rendering.
Also applies to: 315-316, 469-470
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@pages/voting/`[poll].tsx at line 55, The query-derived view value is not
validated: normalize it by creating a safeView (e.g., const safeView = view ===
"votes" ? "votes" : "overview") and use safeView everywhere the component reads
tab state or conditionally renders (replace uses of view in tab state
initialization and the conditional rendering blocks around the overview/votes
content, e.g., where view is referenced at the tab state and the render checks
currently at lines ~315 and ~469); this ensures any unsupported query value
falls back to "overview" and prevents empty main content.
| <Box css={{ marginTop: "$4" }}> | ||
| <Box css={{ display: view === "overview" ? "block" : "none" }}> | ||
| <Box |
There was a problem hiding this comment.
Avoid CSS-only tab hiding for expensive content.
- Problem: Line 314–316 and Line 469–470 hide panes with
display, but still mount both trees;voteContent()at Line 470 runs regardless of active tab. - Why it matters: The votes table path can do unnecessary render/data work when users stay on Overview.
- Suggested fix: Render conditionally in JSX (
safeView === "overview" ? <Overview/> : <Votes/>) so only the active pane mounts.
Also applies to: 469-470
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@pages/voting/`[poll].tsx around lines 314 - 316, The current implementation
hides tab panes with CSS (using view) but keeps both component trees mounted so
voteContent() still executes; change to conditional JSX rendering so only the
active pane mounts by using a state/derived variable (e.g., safeView or view) to
choose between rendering the Overview component and the Votes component (replace
the Box wrappers that use display toggles with conditional expressions like
safeView === "overview" ? <Overview .../> : <Votes .../>), and ensure
voteContent() is only called/used inside the Votes branch so expensive work runs
only when Votes is active.
| query voteEvents($first: Int, $where: VoteEvent_filter) { | ||
| voteEvents( | ||
| orderBy: timestamp | ||
| orderDirection: desc | ||
| first: $first | ||
| where: $where |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify that all usages of the voteEvents query provide a 'first' limit
rg -nP --type=ts --type=tsx -A5 'useVoteEventsQuery|voteEvents' -g '!queries/' -g '!apollo/'Repository: livepeer/explorer
Length of output: 89
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# 1) Find all JS/TS/TSX call sites that mention voteEvents usage/hooks.
rg -n --glob '*.{ts,tsx,js,jsx}' -S 'voteEvents' .
# 2) Narrow: find GraphQL variable usage patterns for `first` alongside the voteEvents query.
rg -n --glob '*.{ts,tsx,js,jsx}' -S '(first\s*:|first\s*=|variables\s*:\s*\{[^}]*first|first\s*\?)' .
# 3) Specifically look for a hook named like useVoteEventsQuery and inspect nearby code.
rg -n --glob '*.{ts,tsx}' -S 'useVoteEventsQuery' . -A30 -B10
# 4) If the query is imported as voteEvents.gql/graphQL, locate the import and inspect usage.
rg -n --glob '*.{ts,tsx,js,jsx}' -S 'from\s+[\'"].*voteEvents\.graphql[\'"]|voteEvents\.graphql' . -A20 -B5Repository: livepeer/explorer
Length of output: 20603
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# List all call sites of useVoteEventsQuery and inspect variables for `first`
rg -n --glob '*.{ts,tsx}' 'useVoteEventsQuery\s*\(' . -S | cut -d: -f1-2 | sort -u | while read -r file; do
echo "==== $file ===="
rg -n --glob "$file" 'useVoteEventsQuery\s*\(' . -S -A25 -B5
doneRepository: livepeer/explorer
Length of output: 98
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Show voteEvents usages around useVoteEventsQuery(
rg -n --glob 'components/**/*.{ts,tsx,js,jsx}' -S 'useVoteEventsQuery\s*\(' components/PollVote -A25 -B5 || true
# Inspect the full variables object for PollVotePopover usage
sed -n '1,120p' components/PollVote/PollVotePopover.tsx
# Inspect generated hook defaults in apollo/subgraph.ts around useVoteEventsQuery
rg -n 'export function useVoteEventsQuery' apollo/subgraph.ts
sed -n '11180,11230p' apollo/subgraph.tsRepository: livepeer/explorer
Length of output: 9372
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n --glob '*.{ts,tsx,js,jsx}' 'useVoteEventsQuery\s*\(\s*\{' . -SRepository: livepeer/explorer
Length of output: 327
Add pagination (first) to the voteEvents query in PollVotePopover
- Problem:
queries/voteEvents.graphqlleaves$firstoptional, andcomponents/PollVote/PollVotePopover.tsxcallsuseVoteEventsQuerywithout providingfirst(whilecomponents/PollVote/index.tsxcorrectly setsfirst: 200). - Why it matters: voters with large vote histories can trigger large/unbounded responses, increasing latency and risking timeouts/perf issues.
- Suggested fix: pass a bounded
firstincomponents/PollVote/PollVotePopover.tsx(e.g.,first: 200, matching the other usage) or add a safe default for$firstinqueries/voteEvents.graphql(e.g.,$first: Int = 200).
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@queries/voteEvents.graphql` around lines 1 - 6, The voteEvents query call in
components/PollVote/PollVotePopover.tsx is missing a bounded `first` variable
and can return unbounded results; update the useVoteEventsQuery invocation in
PollVotePopover (the hook call) to pass a safe limit (e.g., `first: 200`)
consistent with components/PollVote/index.tsx, or alternatively set a default in
the GraphQL definition by changing `$first: Int` to `$first: Int = 200` in
queries/voteEvents.graphql so the query is always paginated.
Description
This PR introduces a new poll vote transparency experience on the governance voting page through the following:
Why
Governance users need transparent, in-context access to vote details (who voted, vote stake, and related metadata) to better understand outcomes and build trust in the polling process without leaving the poll flow.
Type of Change
Related Issue(s)
Related: #310
Closes: #482
Changes Made
PollVoteDetail,DesktopVoteTable,MobileVoteView,MobileVoteCards,PollVotePopover, andcomponents/PollVote/index.tsx.apollo/subgraph.ts,lib/api/polls.ts,queries/poll.graphql, newqueries/voteEvents.graphql) to fetch richer vote details.pages/voting/[poll].tsxandcomponents/PollVotingWidget/index.tsxto support improved vote UX/navigation.Testing
Impact / Risk
Risk level: Low
Impacted areas: UI
Rollback plan: PR revert
Screenshots / Recordings (if applicable)
Before
After
Summary by CodeRabbit
Release Notes