Prechádzať zdrojové kódy

[xc-admin] improve approve/reject UX (#608)

* add spinner while approving/rejecting and update result in real time

* fix confirmed/rejected/cancelled section

* remove console.log

* update previous list of proposals to the most updated one after approving/rejecting

* fix precommit

* fix bug

* refactor
Daniel Chew 2 rokov pred
rodič
commit
4b008810e2

+ 1 - 0
governance/xc_admin/packages/xc_admin_frontend/components/tabs/General.tsx

@@ -415,6 +415,7 @@ const General = () => {
         )
         toast.success(`Proposal sent! 🚀 Proposal Pubkey: ${proposalPubkey}`)
         setIsSendProposalButtonLoading(false)
+        closeModal()
       } catch (e: any) {
         toast.error(capitalizeFirstLetter(e.message))
         setIsSendProposalButtonLoading(false)

+ 137 - 12
governance/xc_admin/packages/xc_admin_frontend/components/tabs/Proposals.tsx

@@ -13,6 +13,8 @@ import {
 import toast from 'react-hot-toast'
 import {
   ExecutePostedVaa,
+  getMultisigCluster,
+  getProposals,
   getRemoteCluster,
   MultisigInstruction,
   MultisigParser,
@@ -23,10 +25,12 @@ import {
 import { ClusterContext } from '../../contexts/ClusterContext'
 import { useMultisigContext } from '../../contexts/MultisigContext'
 import { usePythContext } from '../../contexts/PythContext'
+import { PRICE_FEED_MULTISIG } from '../../hooks/useMultisig'
 import VerifiedIcon from '../../images/icons/verified.inline.svg'
 import { capitalizeFirstLetter } from '../../utils/capitalizeFirstLetter'
 import ClusterSwitch from '../ClusterSwitch'
 import CopyPubkey from '../common/CopyPubkey'
+import Spinner from '../common/Spinner'
 import Loadbar from '../loaders/Loadbar'
 
 // check if a string is a pubkey
@@ -170,15 +174,19 @@ const ParsedAccountPubkeyRow = ({
 
 const Proposal = ({
   proposal,
+  proposalIndex,
   instructions,
   verified,
   multisig,
 }: {
   proposal: TransactionAccount | undefined
+  proposalIndex: number
   instructions: MultisigInstruction[]
   verified: boolean
   multisig: MultisigAccount | undefined
 }) => {
+  const [currentProposal, setCurrentProposal] = useState<TransactionAccount>()
+  const [isTransactionLoading, setIsTransactionLoading] = useState(false)
   const [
     productAccountKeyToSymbolMapping,
     setProductAccountKeyToSymbolMapping,
@@ -186,9 +194,17 @@ const Proposal = ({
   const [priceAccountKeyToSymbolMapping, setPriceAccountKeyToSymbolMapping] =
     useState<{ [key: string]: string }>({})
   const { cluster } = useContext(ClusterContext)
-  const { squads, isLoading: isMultisigLoading } = useMultisigContext()
+  const {
+    squads,
+    isLoading: isMultisigLoading,
+    setpriceFeedMultisigProposals,
+  } = useMultisigContext()
   const { rawConfig, dataIsLoading, connection } = usePythContext()
 
+  useEffect(() => {
+    setCurrentProposal(proposal)
+  }, [proposal])
+
   useEffect(() => {
     if (!dataIsLoading) {
       const productAccountMapping: { [key: string]: string } = {}
@@ -207,12 +223,36 @@ const Proposal = ({
 
   const proposalStatus = proposal ? Object.keys(proposal.status)[0] : 'unknown'
 
+  useEffect(() => {
+    // update the priceFeedMultisigProposals with previous value but replace the current proposal with the updated one at the specific index
+    if (currentProposal) {
+      setpriceFeedMultisigProposals((prevProposals: TransactionAccount[]) => {
+        prevProposals.splice(proposalIndex, 1, currentProposal)
+        return [...prevProposals]
+      })
+    }
+  }, [currentProposal, setpriceFeedMultisigProposals, proposalIndex])
+
   const handleClickApprove = async () => {
     if (proposal && squads) {
       try {
+        setIsTransactionLoading(true)
         await squads.approveTransaction(proposal.publicKey)
+        const proposals = await getProposals(
+          squads,
+          PRICE_FEED_MULTISIG[getMultisigCluster(cluster)]
+        )
+        setCurrentProposal(
+          proposals.find(
+            (proposal) =>
+              proposal.publicKey.toBase58() ===
+              currentProposal?.publicKey.toBase58()
+          )
+        )
         toast.success(`Approved proposal ${proposal.publicKey.toBase58()}`)
+        setIsTransactionLoading(false)
       } catch (e: any) {
+        setIsTransactionLoading(false)
         toast.error(capitalizeFirstLetter(e.message))
       }
     }
@@ -221,9 +261,23 @@ const Proposal = ({
   const handleClickReject = async () => {
     if (proposal && squads) {
       try {
+        setIsTransactionLoading(true)
         await squads.rejectTransaction(proposal.publicKey)
+        const proposals = await getProposals(
+          squads,
+          PRICE_FEED_MULTISIG[getMultisigCluster(cluster)]
+        )
+        setCurrentProposal(
+          proposals.find(
+            (proposal) =>
+              proposal.publicKey.toBase58() ===
+              currentProposal?.publicKey.toBase58()
+          )
+        )
         toast.success(`Rejected proposal ${proposal.publicKey.toBase58()}`)
+        setIsTransactionLoading(false)
       } catch (e: any) {
+        setIsTransactionLoading(false)
         toast.error(capitalizeFirstLetter(e.message))
       }
     }
@@ -232,9 +286,23 @@ const Proposal = ({
   const handleClickExecute = async () => {
     if (proposal && squads) {
       try {
+        setIsTransactionLoading(true)
         await squads.executeTransaction(proposal.publicKey)
+        const proposals = await getProposals(
+          squads,
+          PRICE_FEED_MULTISIG[getMultisigCluster(cluster)]
+        )
+        setCurrentProposal(
+          proposals.find(
+            (proposal) =>
+              proposal.publicKey.toBase58() ===
+              currentProposal?.publicKey.toBase58()
+          )
+        )
         toast.success(`Executed proposal ${proposal.publicKey.toBase58()}`)
+        setIsTransactionLoading(false)
       } catch (e: any) {
+        setIsTransactionLoading(false)
         toast.error(capitalizeFirstLetter(e.message))
       }
     }
@@ -243,15 +311,29 @@ const Proposal = ({
   const handleClickCancel = async () => {
     if (proposal && squads) {
       try {
+        setIsTransactionLoading(true)
         await squads.cancelTransaction(proposal.publicKey)
+        const proposals = await getProposals(
+          squads,
+          PRICE_FEED_MULTISIG[getMultisigCluster(cluster)]
+        )
+        setCurrentProposal(
+          proposals.find(
+            (proposal) =>
+              proposal.publicKey.toBase58() ===
+              currentProposal?.publicKey.toBase58()
+          )
+        )
         toast.success(`Cancelled proposal ${proposal.publicKey.toBase58()}`)
+        setIsTransactionLoading(false)
       } catch (e: any) {
+        setIsTransactionLoading(false)
         toast.error(capitalizeFirstLetter(e.message))
       }
     }
   }
 
-  return proposal !== undefined &&
+  return currentProposal !== undefined &&
     multisig !== undefined &&
     !isMultisigLoading ? (
     <div className="grid grid-cols-3 gap-4">
@@ -267,15 +349,15 @@ const Proposal = ({
         </div>
         <div className="flex justify-between">
           <div>Proposal</div>
-          <CopyPubkey pubkey={proposal.publicKey.toBase58()} />
+          <CopyPubkey pubkey={currentProposal.publicKey.toBase58()} />
         </div>
         <div className="flex justify-between">
           <div>Creator</div>
-          <CopyPubkey pubkey={proposal.creator.toBase58()} />
+          <CopyPubkey pubkey={currentProposal.creator.toBase58()} />
         </div>
         <div className="flex justify-between">
           <div>Multisig</div>
-          <CopyPubkey pubkey={proposal.ms.toBase58()} />
+          <CopyPubkey pubkey={currentProposal.ms.toBase58()} />
         </div>
       </div>
       <div className="col-span-3 my-2 space-y-4 bg-[#1E1B2F] p-4 lg:col-span-1">
@@ -284,17 +366,17 @@ const Proposal = ({
         <div className="grid grid-cols-3 justify-center gap-4 text-center align-middle">
           <div>
             <div className="font-bold">Confirmed</div>
-            <div className="text-lg">{proposal.approved.length}</div>
+            <div className="text-lg">{currentProposal.approved.length}</div>
           </div>
           {proposalStatus === 'active' || proposalStatus === 'rejected' ? (
             <div>
               <div className="font-bold">Rejected</div>
-              <div className="text-lg">{proposal.rejected.length}</div>
+              <div className="text-lg">{currentProposal.rejected.length}</div>
             </div>
           ) : (
             <div>
               <div className="font-bold">Cancelled</div>
-              <div className="text-lg">{proposal.cancelled.length}</div>
+              <div className="text-lg">{currentProposal.cancelled.length}</div>
             </div>
           )}
           <div>
@@ -310,13 +392,13 @@ const Proposal = ({
               className="action-btn text-base"
               onClick={handleClickApprove}
             >
-              Approve
+              {isTransactionLoading ? <Spinner /> : 'Approve'}
             </button>
             <button
               className="sub-action-btn text-base"
               onClick={handleClickReject}
             >
-              Reject
+              {isTransactionLoading ? <Spinner /> : 'Reject'}
             </button>
           </div>
         ) : proposalStatus === 'executeReady' ? (
@@ -325,17 +407,59 @@ const Proposal = ({
               className="action-btn text-base"
               onClick={handleClickExecute}
             >
-              Execute
+              {isTransactionLoading ? <Spinner /> : 'Execute'}
             </button>
             <button
               className="sub-action-btn text-base"
               onClick={handleClickCancel}
             >
-              Cancel
+              {isTransactionLoading ? <Spinner /> : 'Cancel'}
             </button>
           </div>
         ) : null}
       </div>
+      {currentProposal.approved.length > 0 ? (
+        <div className="col-span-3 my-2 space-y-4 bg-[#1E1B2F] p-4">
+          <h4 className="h4 font-semibold">
+            Confirmed: {currentProposal.approved.length}
+          </h4>
+          <hr className="border-gray-700" />
+          {currentProposal.approved.map((pubkey, idx) => (
+            <div className="flex justify-between" key={pubkey.toBase58()}>
+              <div>Key {idx + 1}</div>
+              <CopyPubkey pubkey={pubkey.toBase58()} />
+            </div>
+          ))}
+        </div>
+      ) : null}
+      {currentProposal.rejected.length > 0 ? (
+        <div className="col-span-3 my-2 space-y-4 bg-[#1E1B2F] p-4">
+          <h4 className="h4 font-semibold">
+            Rejected: {currentProposal.rejected.length}
+          </h4>
+          <hr className="border-gray-700" />
+          {currentProposal.rejected.map((pubkey, idx) => (
+            <div className="flex justify-between" key={pubkey.toBase58()}>
+              <div>Key {idx + 1}</div>
+              <CopyPubkey pubkey={pubkey.toBase58()} />
+            </div>
+          ))}
+        </div>
+      ) : null}
+      {currentProposal.cancelled.length > 0 ? (
+        <div className="col-span-3 my-2 space-y-4 bg-[#1E1B2F] p-4">
+          <h4 className="h4 font-semibold">
+            Cancelled: {currentProposal.cancelled.length}
+          </h4>
+          <hr className="border-gray-700" />
+          {currentProposal.cancelled.map((pubkey, idx) => (
+            <div className="flex justify-between" key={pubkey.toBase58()}>
+              <div>Key {idx + 1}</div>
+              <CopyPubkey pubkey={pubkey.toBase58()} />
+            </div>
+          ))}
+        </div>
+      ) : null}
       <div className="col-span-3 my-2 space-y-4 bg-[#1E1B2F] p-4">
         <h4 className="h4 font-semibold">
           Total Instructions: {instructions.length}
@@ -989,6 +1113,7 @@ const Proposals = () => {
             <div className="relative mt-6">
               <Proposal
                 proposal={currentProposal}
+                proposalIndex={currentProposalIndex}
                 instructions={allProposalsIxsParsed[currentProposalIndex]}
                 verified={allProposalsVerifiedArr[currentProposalIndex]}
                 multisig={priceFeedMultisigAccount}

+ 5 - 0
governance/xc_admin/packages/xc_admin_frontend/contexts/MultisigContext.tsx

@@ -16,6 +16,7 @@ interface MultisigContextProps {
   upgradeMultisigProposals: TransactionAccount[]
   priceFeedMultisigProposals: TransactionAccount[]
   allProposalsIxsParsed: MultisigInstruction[][]
+  setpriceFeedMultisigProposals: any
 }
 
 const MultisigContext = createContext<MultisigContextProps>({
@@ -27,6 +28,7 @@ const MultisigContext = createContext<MultisigContextProps>({
   isLoading: true,
   error: null,
   squads: undefined,
+  setpriceFeedMultisigProposals: () => {},
 })
 
 export const useMultisigContext = () => useContext(MultisigContext)
@@ -48,6 +50,7 @@ export const MultisigContextProvider: React.FC<
     upgradeMultisigProposals,
     priceFeedMultisigProposals,
     allProposalsIxsParsed,
+    setpriceFeedMultisigProposals,
   } = useMultisig(anchorWallet as Wallet)
 
   const value = useMemo(
@@ -57,6 +60,7 @@ export const MultisigContextProvider: React.FC<
       upgradeMultisigProposals,
       priceFeedMultisigProposals,
       allProposalsIxsParsed,
+      setpriceFeedMultisigProposals,
       isLoading,
       error,
       squads,
@@ -70,6 +74,7 @@ export const MultisigContextProvider: React.FC<
       upgradeMultisigProposals,
       priceFeedMultisigProposals,
       allProposalsIxsParsed,
+      setpriceFeedMultisigProposals,
     ]
   )
 

+ 4 - 0
governance/xc_admin/packages/xc_admin_frontend/hooks/useMultisig.ts

@@ -46,6 +46,9 @@ interface MultisigHookData {
   upgradeMultisigProposals: TransactionAccount[]
   priceFeedMultisigProposals: TransactionAccount[]
   allProposalsIxsParsed: MultisigInstruction[][]
+  setpriceFeedMultisigProposals: React.Dispatch<
+    React.SetStateAction<TransactionAccount[]>
+  >
 }
 
 const getSortedProposals = async (
@@ -229,5 +232,6 @@ export const useMultisig = (wallet: Wallet): MultisigHookData => {
     upgradeMultisigProposals,
     priceFeedMultisigProposals,
     allProposalsIxsParsed,
+    setpriceFeedMultisigProposals,
   }
 }