Skip to content

Commit

Permalink
feat: add indicator on proposals with remote commands awaiting execut…
Browse files Browse the repository at this point in the history
…ion (#3626)
  • Loading branch information
therealemjy authored Dec 12, 2024
1 parent 125e3df commit 2b8421b
Show file tree
Hide file tree
Showing 10 changed files with 84 additions and 48 deletions.
5 changes: 5 additions & 0 deletions .changeset/fifty-readers-type.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@venusprotocol/evm": minor
---

add indicator on proposals with remote commands awaiting execution
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { isBefore } from 'date-fns/isBefore';
import { useNow } from 'hooks/useNow';
import { useMemo } from 'react';
import { isProposalExecutable } from 'utilities/isProposalExecutable';

export type UseIsProposalExecutableProps = {
executionEtaDate?: Date;
Expand All @@ -14,7 +14,12 @@ export const useIsProposalExecutable = ({
const now = useNow();

const isExecutable = useMemo(
() => isQueued && !!(executionEtaDate && isBefore(executionEtaDate, now)),
() =>
isProposalExecutable({
executionEtaDate,
isQueued,
now,
}),
[executionEtaDate, isQueued, now],
);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,29 +1,49 @@
import { LabeledProgressCircle } from 'components';
import { Icon, LabeledProgressCircle } from 'components';
import { useIsFeatureEnabled } from 'hooks/useIsFeatureEnabled';
import { useNow } from 'hooks/useNow';
import { useTranslation } from 'libs/translations';
import { useMemo } from 'react';
import { ProposalState } from 'types';
import { ProposalState, type RemoteProposal, RemoteProposalState } from 'types';
import { cn, getProposalStateLabel } from 'utilities';
import { isProposalExecutable } from 'utilities/isProposalExecutable';
import { Indicator } from './Indicator';

export interface StatusProps extends React.HTMLAttributes<HTMLDivElement> {
state: ProposalState;
totalPayloadsCount: number;
executedPayloadsCount: number;
remoteProposals: RemoteProposal[];
}

export const Status: React.FC<StatusProps> = ({
state,
totalPayloadsCount,
executedPayloadsCount,
remoteProposals,
className,
...otherProps
}) => {
const { t } = useTranslation();
const now = useNow();

const isOmnichainGovernanceFeatureEnabled = useIsFeatureEnabled({
name: 'omnichainGovernance',
});

const totalPayloadsCount = 1 + remoteProposals.length; // BSC proposal + remote proposals
const executedPayloadsCount =
(state === ProposalState.Executed ? 1 : 0) +
remoteProposals.filter(remoteProposal => remoteProposal.state === RemoteProposalState.Executed)
.length;

const shouldShowAwaitingExecutionWarning =
state === ProposalState.Executed &&
remoteProposals.some(
remoteProposal =>
remoteProposal.state === RemoteProposalState.Queued &&
isProposalExecutable({
now,
isQueued: remoteProposal.state === RemoteProposalState.Queued,
executionEtaDate: remoteProposal.executionEtaDate,
}),
);

const isFullyExecuted = executedPayloadsCount === totalPayloadsCount;
const shouldShowExecutedPayloadsStatus =
isOmnichainGovernanceFeatureEnabled && state === ProposalState.Executed && !isFullyExecuted;
Expand All @@ -50,11 +70,19 @@ export const Status: React.FC<StatusProps> = ({
>
<div className="shrink-0">
{shouldShowExecutedPayloadsStatus ? (
<LabeledProgressCircle
total={totalPayloadsCount}
value={executedPayloadsCount}
className="mx-auto"
/>
<div className="relative">
<LabeledProgressCircle
total={totalPayloadsCount}
value={executedPayloadsCount}
className="mx-auto"
/>

{shouldShowAwaitingExecutionWarning && (
<div className="bg-orange w-4 h-4 rounded-full flex items-center justify-center absolute top-[-2px] right-[-2px] border-[2px] border-cards">
<Icon name="exclamation" className="w-2 h-2 text-offWhite" />
</div>
)}
</div>
) : (
<Indicator state={state} />
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,7 @@ import { useMemo } from 'react';
import { ActiveVotingProgress, Countdown, ProposalTypeChip } from 'components';
import { routes } from 'constants/routing';
import { useTranslation } from 'libs/translations';
import {
type Proposal,
ProposalState,
ProposalType,
RemoteProposalState,
type Token,
VoteSupport,
} from 'types';
import { type Proposal, ProposalState, ProposalType, type Token, VoteSupport } from 'types';

import { ProposalCard } from 'containers/ProposalCard';
import { useGetToken } from 'libs/tokens';
Expand Down Expand Up @@ -102,20 +95,7 @@ const GovernanceProposalUi: React.FC<GovernanceProposalProps> = ({
);
}

const totalPayloadsCount = 1 + remoteProposals.length; // BSC proposal + remote proposals
const executedPayloadsCount =
(state === ProposalState.Executed ? 1 : 0) +
remoteProposals.filter(
remoteProposal => remoteProposal.state === RemoteProposalState.Executed,
).length;

return (
<Status
state={state}
totalPayloadsCount={totalPayloadsCount}
executedPayloadsCount={executedPayloadsCount}
/>
);
return <Status state={state} remoteProposals={remoteProposals} />;
}, [state, remoteProposals, forVotesMantissa, againstVotesMantissa, abstainedVotesMantissa, xvs]);

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@ import { useCancelProposal, useExecuteProposal, useQueueProposal } from 'clients
import { Button } from 'components';
import type { ConnectWalletProps } from 'containers/ConnectWallet';
import { ConnectWallet } from 'containers/ConnectWallet';
import { useIsProposalExecutable } from 'hooks/useIsProposalExecutable';
import { handleError } from 'libs/errors';
import { useTranslation } from 'libs/translations';
import { governanceChain, useAccountAddress } from 'libs/wallet';
import { useMemo } from 'react';
import { ProposalState } from 'types';
import { cn } from 'utilities';
import { useIsProposalCancelableByUser } from '../../useIsProposalCancelableByUser';
import { useIsProposalExecutable } from '../../useIsProposalExecutable';

export interface ActionButtonProps extends Omit<ConnectWalletProps, 'onClick'> {
proposalId: number;
Expand Down
2 changes: 1 addition & 1 deletion apps/evm/src/pages/Proposal/Commands/BscCommand/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { chainMetadata } from '@venusprotocol/chains';
import { useIsProposalExecutable } from 'hooks/useIsProposalExecutable';
import { useTranslation } from 'libs/translations';
import { governanceChain, useChainId } from 'libs/wallet';
import { useAccountAddress } from 'libs/wallet';
Expand All @@ -7,7 +8,6 @@ import { type Proposal, ProposalState } from 'types';
import { Command } from '../Command';
import { Description } from '../Description';
import { useIsProposalCancelableByUser } from '../useIsProposalCancelableByUser';
import { useIsProposalExecutable } from '../useIsProposalExecutable';
import { ActionButton } from './ActionButton';
import { CurrentStep } from './CurrentStep';

Expand Down
9 changes: 3 additions & 6 deletions apps/evm/src/pages/Proposal/Commands/NonBscCommand/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,14 @@ import { chainMetadata } from '@venusprotocol/chains';
import { useExecuteProposal } from 'clients/api';
import { Button } from 'components';
import { ConnectWallet } from 'containers/ConnectWallet';
import { isAfter } from 'date-fns/isAfter';
import { useNow } from 'hooks/useNow';
import { useIsProposalExecutable } from 'hooks/useIsProposalExecutable';
import { VError, handleError } from 'libs/errors';
import { useTranslation } from 'libs/translations';
import { governanceChain, useChainId } from 'libs/wallet';
import { useMemo } from 'react';
import { type RemoteProposal, RemoteProposalState } from 'types';
import { Command } from '../Command';
import { Description } from '../Description';
import { useIsProposalExecutable } from '../useIsProposalExecutable';
import { CurrentStep } from './CurrentStep';

const governanceChainMetadata = chainMetadata[governanceChain.id];
Expand All @@ -27,7 +25,6 @@ export const NonBscCommand: React.FC<NonBscCommand> = ({
...otherProps
}) => {
const { t } = useTranslation();
const now = useNow();
const { chainId: currentChainId } = useChainId();

const chain = chainMetadata[remoteProposal.chainId];
Expand Down Expand Up @@ -67,7 +64,7 @@ export const NonBscCommand: React.FC<NonBscCommand> = ({
case RemoteProposalState.Canceled:
return t('voteProposalUi.command.description.canceled');
case RemoteProposalState.Queued:
if (!remoteProposal.executionEtaDate || isAfter(remoteProposal.executionEtaDate, now)) {
if (!isExecutable) {
return t('voteProposalUi.command.description.waitingToBeExecutable');
}

Expand All @@ -78,7 +75,7 @@ export const NonBscCommand: React.FC<NonBscCommand> = ({
}
break;
}
}, [t, remoteProposal.state, remoteProposal.executionEtaDate, now, chain, isOnWrongChain]);
}, [t, remoteProposal.state, chain, isOnWrongChain, isExecutable]);

return (
<Command
Expand Down
16 changes: 11 additions & 5 deletions apps/evm/src/pages/Proposal/ProposalSummary/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
/** @jsxImportSource @emotion/react */
import { Typography } from '@mui/material';
import { isAfter } from 'date-fns/isAfter';
import { useMemo } from 'react';

import {
Expand All @@ -27,6 +26,7 @@ import { governanceChain, useAccountAddress } from 'libs/wallet';
import { type Proposal, ProposalState, ProposalType } from 'types';
import { areAddressesEqual } from 'utilities';

import { useIsProposalExecutable } from 'hooks/useIsProposalExecutable';
import Stepper from './Stepper';
import { useStyles } from './styles';
import TEST_IDS from './testIds';
Expand Down Expand Up @@ -62,6 +62,7 @@ export const ProposalSummaryUi: React.FC<ProposalSummaryUiProps & ProposalSummar
}) => {
const styles = useStyles();
const { t, Trans } = useTranslation();

const isVoteProposalFeatureEnabled = useIsFeatureEnabled({ name: 'voteProposal' });
const isOmnichainGovernanceFeatureEnabled = useIsFeatureEnabled({
name: 'omnichainGovernance',
Expand Down Expand Up @@ -111,7 +112,11 @@ export const ProposalSummaryUi: React.FC<ProposalSummaryUiProps & ProposalSummar

let updateProposalButton: JSX.Element | undefined;
let mainTransactionHash = createdTxHash;
const isExecuteEtaInFuture = !!proposalEta && isAfter(proposalEta, new Date());

const isProposalExecutable = useIsProposalExecutable({
executionEtaDate: proposalEta,
isQueued: state === ProposalState.Queued,
});

switch (state) {
case ProposalState.Active:
Expand Down Expand Up @@ -145,7 +150,7 @@ export const ProposalSummaryUi: React.FC<ProposalSummaryUiProps & ProposalSummar
);
break;
case ProposalState.Queued:
if (!isExecuteEtaInFuture) {
if (isProposalExecutable) {
updateProposalButton = (
<PrimaryButton
onClick={handleExecuteProposal}
Expand All @@ -157,6 +162,7 @@ export const ProposalSummaryUi: React.FC<ProposalSummaryUiProps & ProposalSummar
</PrimaryButton>
);
}

if (!isOmnichainGovernanceFeatureEnabled) {
mainTransactionHash = queuedTxHash;
}
Expand Down Expand Up @@ -188,15 +194,15 @@ export const ProposalSummaryUi: React.FC<ProposalSummaryUiProps & ProposalSummar
};
}

if (state === ProposalState.Queued && isExecuteEtaInFuture) {
if (state === ProposalState.Queued && proposalEta && !isProposalExecutable) {
return {
date: proposalEta,
// DO NOT REMOVE COMMENT: needed by i18next to extract translation key
// t('voteProposalUi.timeUntilExecutable')
i18nKey: 'voteProposalUi.timeUntilExecutable',
};
}
}, [state, endDate, startDate, proposalEta, isExecuteEtaInFuture]);
}, [state, endDate, startDate, proposalEta, isProposalExecutable]);

return (
<Card css={styles.root} className={className}>
Expand Down
1 change: 1 addition & 0 deletions apps/evm/src/utilities/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,4 @@ export * from './getProposalType';
export * from './getProposalState';
export * from './getUserVoteSupport';
export * from './getProposalStateLabel';
export * from './isProposalExecutable';
14 changes: 14 additions & 0 deletions apps/evm/src/utilities/isProposalExecutable/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { isBefore } from 'date-fns/isBefore';

export type IsProposalExecutableInput = {
now: Date;
isQueued: boolean;
executionEtaDate?: Date;
};

export const isProposalExecutable = ({
now,
isQueued,
executionEtaDate,
}: IsProposalExecutableInput) =>
isQueued && !!(executionEtaDate && isBefore(executionEtaDate, now));

0 comments on commit 2b8421b

Please sign in to comment.