Skip to content

Commit 1e169bd

Browse files
guibescoscprussin
andauthored
feat: filter for voted props (#2173)
* use getSignaturesForAddress * feat(xc-admin-frontend): add a filter to show voted proposals * fix rebase --------- Co-authored-by: Connor Prussin <[email protected]>
1 parent 7bf23f2 commit 1e169bd

File tree

10 files changed

+1167
-414
lines changed

10 files changed

+1167
-414
lines changed

governance/xc_admin/packages/xc_admin_frontend/components/ProposalStatusFilter.tsx

Lines changed: 0 additions & 89 deletions
This file was deleted.
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import {
2+
Field,
3+
Label,
4+
Listbox,
5+
ListboxButton,
6+
ListboxOptions,
7+
ListboxOption,
8+
Transition,
9+
} from '@headlessui/react'
10+
import type { ComponentProps } from 'react'
11+
import Arrow from '@images/icons/down.inline.svg'
12+
import { Fragment } from 'react'
13+
14+
type OwnProps<T> = {
15+
label: string
16+
options: readonly T[]
17+
value: T
18+
onChange: (newValue: T) => void
19+
}
20+
21+
type Props<T> = Omit<ComponentProps<typeof Listbox>, keyof OwnProps<T>> &
22+
OwnProps<T>
23+
24+
export const Select = <T extends string>({
25+
options,
26+
label,
27+
...props
28+
}: Props<T>) => (
29+
<Field className="flex flex-col gap-1">
30+
<Label>{label}</Label>
31+
<Listbox as="div" className="relative block w-[180px] text-left" {...props}>
32+
{({ open }) => (
33+
<>
34+
<ListboxButton className="inline-flex w-full items-center justify-between bg-darkGray2 py-3 px-6 text-sm outline-0">
35+
<span className="mr-3">{props.value}</span>
36+
<Arrow className={`${open && 'rotate-180'}`} />
37+
</ListboxButton>
38+
<Transition
39+
as={Fragment}
40+
enter="transition ease-out duration-100"
41+
enterFrom="transform opacity-0 scale-95"
42+
enterTo="transform opacity-100 scale-100"
43+
leave="transition ease-in duration-75"
44+
leaveFrom="transform opacity-100 scale-100"
45+
leaveTo="transform opacity-0 scale-95"
46+
>
47+
<ListboxOptions className="absolute right-0 mt-2 w-full origin-top-right z-10">
48+
{options.map((option) => (
49+
<ListboxOption
50+
key={option}
51+
value={option}
52+
className="block w-full bg-darkGray py-3 px-6 text-left text-sm hover:bg-darkGray2 cursor-pointer"
53+
>
54+
{option}
55+
</ListboxOption>
56+
))}
57+
</ListboxOptions>
58+
</Transition>
59+
</>
60+
)}
61+
</Listbox>
62+
</Field>
63+
)

governance/xc_admin/packages/xc_admin_frontend/components/tabs/Proposals/ProposalRow.tsx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { StatusTag } from './StatusTag'
99
import { getInstructionsSummary, getProposalStatus } from './utils'
1010

1111
import NodeWallet from '@coral-xyz/anchor/dist/cjs/nodewallet'
12+
import { useWallet } from '@solana/wallet-adapter-react'
1213
import { AccountMeta, Keypair } from '@solana/web3.js'
1314
import {
1415
MultisigParser,
@@ -30,6 +31,7 @@ export const ProposalRow = ({
3031
const { isLoading: isMultisigLoading, connection } = useMultisigContext()
3132
const router = useRouter()
3233
const elementRef = useRef(null)
34+
const { publicKey: walletPublicKey } = useWallet()
3335
const formattedTime = time?.toLocaleString(undefined, {
3436
year: 'numeric',
3537
month: 'short',
@@ -191,6 +193,24 @@ export const ProposalRow = ({
191193
/>
192194
</div>
193195
)}
196+
{walletPublicKey &&
197+
proposal.approved.some((vote) => vote.equals(walletPublicKey)) && (
198+
<div>
199+
<StatusTag proposalStatus="executed" text="You approved" />
200+
</div>
201+
)}
202+
{walletPublicKey &&
203+
proposal.rejected.some((vote) => vote.equals(walletPublicKey)) && (
204+
<div>
205+
<StatusTag proposalStatus="rejected" text="You rejected" />
206+
</div>
207+
)}
208+
{walletPublicKey &&
209+
proposal.cancelled.some((vote) => vote.equals(walletPublicKey)) && (
210+
<div>
211+
<StatusTag proposalStatus="cancelled" text="You cancelled" />
212+
</div>
213+
)}
194214
<div>
195215
<StatusTag proposalStatus={status} />
196216
</div>

governance/xc_admin/packages/xc_admin_frontend/components/tabs/Proposals/Proposals.tsx

Lines changed: 98 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,50 @@
11
import { TransactionAccount } from '@sqds/mesh/lib/types'
22
import { useRouter } from 'next/router'
3-
import { useCallback, useContext, useEffect, useState } from 'react'
3+
import { useCallback, useContext, useEffect, useState, useMemo } from 'react'
44
import { ClusterContext } from '../../../contexts/ClusterContext'
55
import { useMultisigContext } from '../../../contexts/MultisigContext'
6-
import { StatusFilterContext } from '../../../contexts/StatusFilterContext'
6+
import { PROPOSAL_STATUSES } from './utils'
77
import ClusterSwitch from '../../ClusterSwitch'
8-
import ProposalStatusFilter from '../../ProposalStatusFilter'
98
import Loadbar from '../../loaders/Loadbar'
9+
import { Select } from '../../Select'
10+
import { useQueryState, parseAsStringLiteral } from 'nuqs'
1011

1112
import { ProposalRow } from './ProposalRow'
1213
import { getProposalStatus } from './utils'
1314
import { Proposal } from './Proposal'
15+
import { useWallet } from '@solana/wallet-adapter-react'
1416

1517
type ProposalType = 'priceFeed' | 'governance'
1618

19+
const VOTE_STATUSES = [
20+
'any',
21+
'voted',
22+
'approved',
23+
'rejected',
24+
'cancelled',
25+
'notVoted',
26+
] as const
27+
const DEFAULT_VOTE_STATUS = 'any'
28+
29+
const PROPOSAL_STATUS_FILTERS = ['all', ...PROPOSAL_STATUSES] as const
30+
const DEFAULT_PROPOSAL_STATUS_FILTER = 'all'
31+
1732
const Proposals = () => {
1833
const router = useRouter()
1934
const [currentProposal, setCurrentProposal] = useState<TransactionAccount>()
2035
const [currentProposalPubkey, setCurrentProposalPubkey] = useState<string>()
36+
const [statusFilter, setStatusFilter] = useQueryState(
37+
'status',
38+
parseAsStringLiteral(PROPOSAL_STATUS_FILTERS).withDefault(
39+
DEFAULT_PROPOSAL_STATUS_FILTER
40+
)
41+
)
42+
const [voteStatus, setVoteStatus] = useQueryState(
43+
'voteStatus',
44+
parseAsStringLiteral(VOTE_STATUSES).withDefault(DEFAULT_VOTE_STATUS)
45+
)
2146
const { cluster } = useContext(ClusterContext)
22-
const { statusFilter } = useContext(StatusFilterContext)
47+
const { publicKey: walletPublicKey } = useWallet()
2348

2449
const {
2550
upgradeMultisigAccount,
@@ -40,9 +65,6 @@ const Proposals = () => {
4065
proposalType === 'priceFeed'
4166
? priceFeedMultisigProposals
4267
: upgradeMultisigProposals
43-
const [filteredProposals, setFilteredProposals] = useState<
44-
TransactionAccount[]
45-
>([])
4668

4769
const handleClickBackToProposals = () => {
4870
delete router.query.proposal
@@ -103,19 +125,60 @@ const Proposals = () => {
103125
cluster,
104126
])
105127

106-
useEffect(() => {
107-
// filter price feed multisig proposals by status
108-
if (statusFilter === 'all') {
109-
setFilteredProposals(multisigProposals)
128+
const proposalsFilteredByStatus = useMemo(
129+
() =>
130+
statusFilter === 'all'
131+
? multisigProposals
132+
: multisigProposals.filter(
133+
(proposal) =>
134+
getProposalStatus(proposal, multisigAccount) === statusFilter
135+
),
136+
[statusFilter, multisigAccount, multisigProposals]
137+
)
138+
139+
const filteredProposals = useMemo(() => {
140+
if (walletPublicKey) {
141+
switch (voteStatus) {
142+
case 'any':
143+
return proposalsFilteredByStatus
144+
case 'voted': {
145+
return proposalsFilteredByStatus.filter((proposal) =>
146+
[
147+
...proposal.approved,
148+
...proposal.rejected,
149+
...proposal.cancelled,
150+
].some((vote) => vote.equals(walletPublicKey))
151+
)
152+
}
153+
case 'approved': {
154+
return proposalsFilteredByStatus.filter((proposal) =>
155+
proposal.approved.some((vote) => vote.equals(walletPublicKey))
156+
)
157+
}
158+
case 'rejected': {
159+
return proposalsFilteredByStatus.filter((proposal) =>
160+
proposal.rejected.some((vote) => vote.equals(walletPublicKey))
161+
)
162+
}
163+
case 'cancelled': {
164+
return proposalsFilteredByStatus.filter((proposal) =>
165+
proposal.cancelled.some((vote) => vote.equals(walletPublicKey))
166+
)
167+
}
168+
case 'notVoted': {
169+
return proposalsFilteredByStatus.filter((proposal) =>
170+
[
171+
...proposal.approved,
172+
...proposal.rejected,
173+
...proposal.cancelled,
174+
].every((vote) => !vote.equals(walletPublicKey))
175+
)
176+
}
177+
}
110178
} else {
111-
setFilteredProposals(
112-
multisigProposals.filter(
113-
(proposal) =>
114-
getProposalStatus(proposal, multisigAccount) === statusFilter
115-
)
116-
)
179+
return proposalsFilteredByStatus
117180
}
118-
}, [statusFilter, multisigAccount, multisigProposals])
181+
}, [proposalsFilteredByStatus, walletPublicKey, voteStatus])
119182

120183
return (
121184
<div className="relative">
@@ -167,11 +230,26 @@ const Proposals = () => {
167230
</div>
168231
) : (
169232
<>
170-
<div className="flex items-center justify-between pb-4">
233+
<div className="flex items-end md:flex-row-reverse justify-between pb-4">
234+
<div className="flex flex-col md:flex-row md:items-center gap-4 text-sm">
235+
{walletPublicKey && (
236+
<Select
237+
label="Your Vote"
238+
value={voteStatus}
239+
options={VOTE_STATUSES}
240+
onChange={setVoteStatus}
241+
/>
242+
)}
243+
<Select
244+
label="Proposal Status"
245+
value={statusFilter}
246+
options={PROPOSAL_STATUS_FILTERS}
247+
onChange={setStatusFilter}
248+
/>
249+
</div>
171250
<h4 className="h4">
172251
Total Proposals: {filteredProposals.length}
173252
</h4>
174-
<ProposalStatusFilter />
175253
</div>
176254
{filteredProposals.length > 0 ? (
177255
<div className="flex flex-col">

governance/xc_admin/packages/xc_admin_frontend/components/tabs/Proposals/utils.ts

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,17 @@ import {
1010
WormholeMultisigInstruction,
1111
} from '@pythnetwork/xc-admin-common'
1212

13-
export type ProposalStatus =
14-
| 'active'
15-
| 'executed'
16-
| 'cancelled'
17-
| 'rejected'
18-
| 'expired'
19-
| 'executeReady'
20-
| 'draft'
21-
| 'unkwown'
13+
export const PROPOSAL_STATUSES = [
14+
'active',
15+
'executed',
16+
'cancelled',
17+
'rejected',
18+
'expired',
19+
'executeReady',
20+
'draft',
21+
'unkwown',
22+
] as const
23+
export type ProposalStatus = typeof PROPOSAL_STATUSES[number]
2224

2325
export const getProposalStatus = (
2426
proposal: TransactionAccount | undefined,

0 commit comments

Comments
 (0)