fix(governance): batch proposal diff and proposal headers (#5825)

This commit is contained in:
Art 2024-02-20 15:04:11 +01:00 committed by GitHub
parent be6f395ce4
commit 48d6be0adf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 187 additions and 38 deletions

View File

@ -160,7 +160,7 @@ describe('Proposal header', () => {
screen.queryByTestId('proposal-description') screen.queryByTestId('proposal-description')
).not.toBeInTheDocument(); ).not.toBeInTheDocument();
expect(screen.getByTestId('proposal-details')).toHaveTextContent( expect(screen.getByTestId('proposal-details')).toHaveTextContent(
'Update to market ID: MarketId' 'Update to market: MarketId'
); );
}); });

View File

@ -36,6 +36,8 @@ import { type Proposal, type BatchProposal } from '../../types';
import { type ProposalTermsFieldsFragment } from '../../__generated__/Proposals'; import { type ProposalTermsFieldsFragment } from '../../__generated__/Proposals';
import { differenceInHours, format, formatDistanceToNowStrict } from 'date-fns'; import { differenceInHours, format, formatDistanceToNowStrict } from 'date-fns';
import { DATE_FORMAT_DETAILED } from '../../../../lib/date-formats'; import { DATE_FORMAT_DETAILED } from '../../../../lib/date-formats';
import { getIndicatorStyle } from '../proposal/colours';
import { MarketName } from '../proposal/market-name';
const ProposalTypeTags = ({ const ProposalTypeTags = ({
proposal, proposal,
@ -144,8 +146,42 @@ const ProposalDetails = ({
terms.change?.market?.id && terms.change?.market?.id &&
terms.change.updateType ? ( terms.change.updateType ? (
<> <>
{t(terms.change.updateType)}:{' '} <span>{t(terms.change.updateType)}: </span>
{truncateMiddle(terms.change.market.id)} <span className="inline-flex gap-2">
<span className="break-all">
<MarketName marketId={terms.change.market.id} />
</span>
<span className="inline-flex items-end gap-0">
<CopyWithTooltip
text={terms.change.market.id}
description={t('copyId')}
>
<button className="inline-block px-1">
<VegaIcon size={20} name={VegaIconNames.COPY} />
</button>
</CopyWithTooltip>
<Tooltip description={t('OpenInConsole')} align="center">
<button
className="inline-block px-1"
onClick={() => {
const marketPageLink = consoleLink(
CONSOLE_MARKET_PAGE.replace(
':marketId',
// @ts-ignore ts doesn't like this field even though its already a string above???
terms.change.market.id
)
);
window.open(marketPageLink, '_blank');
}}
>
<VegaIcon
size={20}
name={VegaIconNames.OPEN_EXTERNAL}
/>
</button>
</Tooltip>
</span>
</span>
</> </>
) : null} ) : null}
</span> </span>
@ -154,13 +190,15 @@ const ProposalDetails = ({
case 'UpdateMarket': { case 'UpdateMarket': {
return ( return (
<> <>
<span>{t('UpdateToMarket')}:</span>{' '} <span>{t('UpdateToMarket')}: </span>
<span className="inline-flex items-start gap-2"> <span className="inline-flex items-start gap-2">
<span className="break-all">{terms.change.marketId} </span> <span className="break-all">
<MarketName marketId={terms.change.marketId} />
</span>
<span className="inline-flex items-end gap-0"> <span className="inline-flex items-end gap-0">
<CopyWithTooltip <CopyWithTooltip
text={terms.change.marketId} text={terms.change.marketId}
description={t('copyToClipboard')} description={t('copyId')}
> >
<button className="inline-block px-1"> <button className="inline-block px-1">
<VegaIcon size={20} name={VegaIconNames.COPY} /> <VegaIcon size={20} name={VegaIconNames.COPY} />
@ -286,12 +324,15 @@ const ProposalDetails = ({
{proposal.subProposals.map((p, i) => { {proposal.subProposals.map((p, i) => {
if (!p?.terms) return null; if (!p?.terms) return null;
return ( return (
<li key={i}> <li key={i} className="flex gap-3">
<div>{renderDetails(p.terms)}</div> <span className={getIndicatorStyle(i + 1)}>{i + 1}</span>
<SubProposalStateText <span>
state={proposal.state} <div>{renderDetails(p.terms)}</div>
enactmentDatetime={p.terms.enactmentDatetime} <SubProposalStateText
/> state={proposal.state}
enactmentDatetime={p.terms.enactmentDatetime}
/>
</span>
</li> </li>
); );
})} })}

View File

@ -9,6 +9,7 @@ import { useState } from 'react';
import { CollapsibleToggle } from '../../../../components/collapsible-toggle'; import { CollapsibleToggle } from '../../../../components/collapsible-toggle';
import { SubHeading } from '../../../../components/heading'; import { SubHeading } from '../../../../components/heading';
import { type UpdateMarketStatesFragment } from '../../__generated__/Proposals'; import { type UpdateMarketStatesFragment } from '../../__generated__/Proposals';
import { MarketUpdateTypeMapping } from '@vegaprotocol/types';
interface ProposalUpdateMarketStateProps { interface ProposalUpdateMarketStateProps {
change: UpdateMarketStatesFragment | null; change: UpdateMarketStatesFragment | null;
@ -49,6 +50,12 @@ export const ProposalUpdateMarketState = ({
{t('marketId')} {t('marketId')}
{market?.id} {market?.id}
</KeyValueTableRow> </KeyValueTableRow>
<KeyValueTableRow>
{t('State')}
<span className="bg-vega-green-650 px-1">
{MarketUpdateTypeMapping[change.updateType]}
</span>
</KeyValueTableRow>
<KeyValueTableRow> <KeyValueTableRow>
{t('marketName')} {t('marketName')}
{market?.tradableInstrument?.instrument?.name} {market?.tradableInstrument?.instrument?.name}

View File

@ -0,0 +1,33 @@
import classNames from 'classnames';
// rainbow-ish order
const COLOURS = ['red', 'pink', 'orange', 'yellow', 'green', 'blue', 'purple'];
const getColour = (indicator: number, max = COLOURS.length) => {
const available =
max < COLOURS.length ? COLOURS.slice(COLOURS.length - max) : COLOURS;
const tiers = Object.keys(available).length;
let index = Math.abs(indicator - 1);
if (indicator >= tiers) {
index = index % tiers;
}
return available[index];
};
export const getStyle = (indicator: number, max = COLOURS.length) =>
classNames({
'bg-vega-yellow-400': 'yellow' === getColour(indicator, max),
'bg-vega-green-400': 'green' === getColour(indicator, max),
'bg-vega-blue-400': 'blue' === getColour(indicator, max),
'bg-vega-purple-400': 'purple' === getColour(indicator, max),
'bg-vega-pink-400': 'pink' === getColour(indicator, max),
'bg-vega-orange-400': 'orange' === getColour(indicator, max),
'bg-vega-red-400': 'red' === getColour(indicator, max),
'bg-vega-clight-600': 'none' === getColour(indicator, max),
});
export const getIndicatorStyle = (indicator: number) =>
classNames(
'rounded-sm text-black inline-block px-1 py-1 font-alpha calt h-8',
getStyle(indicator)
);

View File

@ -0,0 +1,14 @@
import { useMarketInfoQuery } from '@vegaprotocol/markets';
export const MarketName = ({ marketId }: { marketId?: string }) => {
const { data } = useMarketInfoQuery({
variables: {
marketId: marketId || '',
},
skip: !marketId,
});
return (
<span>{data?.market?.tradableInstrument.instrument.code || marketId}</span>
);
};

View File

@ -12,21 +12,26 @@ import {
import { ProposalUpdateBenefitTiers } from '../proposal-update-benefit-tiers'; import { ProposalUpdateBenefitTiers } from '../proposal-update-benefit-tiers';
import { ProposalUpdateMarketState } from '../proposal-update-market-state'; import { ProposalUpdateMarketState } from '../proposal-update-market-state';
import { ProposalVolumeDiscountProgramDetails } from '../proposal-volume-discount-program-details'; import { ProposalVolumeDiscountProgramDetails } from '../proposal-volume-discount-program-details';
import { getIndicatorStyle } from './colours';
export const ProposalChangeDetails = ({ export const ProposalChangeDetails = ({
proposal, proposal,
terms, terms,
restData, restData,
indicator,
}: { }: {
proposal: Proposal | BatchProposal; proposal: Proposal | BatchProposal;
terms: ProposalTermsFieldsFragment; terms: ProposalTermsFieldsFragment;
// eslint-disable-next-line // eslint-disable-next-line
restData: any; restData: any;
indicator?: number;
}) => { }) => {
let details = null;
switch (terms.change.__typename) { switch (terms.change.__typename) {
case 'NewAsset': { case 'NewAsset': {
if (proposal.id && terms.change.source.__typename === 'ERC20') { if (proposal.id && terms.change.source.__typename === 'ERC20') {
return ( details = (
<div> <div>
<ListAsset <ListAsset
assetId={proposal.id} assetId={proposal.id}
@ -37,62 +42,81 @@ export const ProposalChangeDetails = ({
</div> </div>
); );
} }
return null; break;
} }
case 'UpdateAsset': { case 'UpdateAsset': {
if (proposal.id) { if (proposal.id) {
return ( details = (
<ProposalAssetDetails <ProposalAssetDetails
change={terms.change} change={terms.change}
assetId={terms.change.assetId} assetId={terms.change.assetId}
/> />
); );
} }
return null; break;
} }
case 'NewMarket': { case 'NewMarket': {
if (proposal.id) { if (proposal.id) {
return <ProposalMarketData proposalId={proposal.id} />; details = <ProposalMarketData proposalId={proposal.id} />;
} }
return null; break;
} }
case 'UpdateMarket': { case 'UpdateMarket': {
if (proposal.id) { if (proposal.id) {
return ( const marketId = terms.change.marketId;
const proposalData = restData?.data?.proposal;
let updatedProposal = null;
// single proposal
if ('terms' in proposalData) {
updatedProposal = proposalData?.terms?.updateMarket?.changes;
}
// batch proposal - need to fish for the actual changes
if (
'batchTerms' in proposalData &&
Array.isArray(proposalData.batchTerms?.changes)
) {
updatedProposal = proposalData?.batchTerms?.changes.find(
(ch: { updateMarket?: { marketId: string } }) =>
ch?.updateMarket?.marketId === marketId
)?.updateMarket?.changes;
}
details = (
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<ProposalMarketData proposalId={proposal.id} /> <ProposalMarketData proposalId={proposal.id} />
<ProposalMarketChanges <ProposalMarketChanges
marketId={terms.change.marketId} marketId={terms.change.marketId}
updatedProposal={ updatedProposal={updatedProposal}
restData?.data?.proposal?.terms?.updateMarket?.changes
}
/> />
</div> </div>
); );
} }
return null; break;
} }
case 'NewTransfer': { case 'NewTransfer': {
if (proposal.id) { if (proposal.id) {
return <ProposalTransferDetails proposalId={proposal.id} />; details = <ProposalTransferDetails proposalId={proposal.id} />;
} }
return null; break;
} }
case 'CancelTransfer': { case 'CancelTransfer': {
if (proposal.id) { if (proposal.id) {
return <ProposalCancelTransferDetails proposalId={proposal.id} />; details = <ProposalCancelTransferDetails proposalId={proposal.id} />;
} }
return null; break;
} }
case 'UpdateMarketState': { case 'UpdateMarketState': {
return <ProposalUpdateMarketState change={terms.change} />; details = <ProposalUpdateMarketState change={terms.change} />;
break;
} }
case 'UpdateReferralProgram': { case 'UpdateReferralProgram': {
return <ProposalReferralProgramDetails change={terms.change} />; details = <ProposalReferralProgramDetails change={terms.change} />;
break;
} }
case 'UpdateVolumeDiscountProgram': { case 'UpdateVolumeDiscountProgram': {
return <ProposalVolumeDiscountProgramDetails change={terms.change} />; details = <ProposalVolumeDiscountProgramDetails change={terms.change} />;
break;
} }
case 'UpdateNetworkParameter': { case 'UpdateNetworkParameter': {
if ( if (
@ -100,18 +124,27 @@ export const ProposalChangeDetails = ({
terms.change.networkParameter.key === terms.change.networkParameter.key ===
'rewards.activityStreak.benefitTiers' 'rewards.activityStreak.benefitTiers'
) { ) {
return <ProposalUpdateBenefitTiers change={terms.change} />; details = <ProposalUpdateBenefitTiers change={terms.change} />;
} }
return null; break;
} }
case 'NewFreeform': case 'NewFreeform':
case 'NewSpotMarket': case 'NewSpotMarket':
case 'UpdateSpotMarket': { case 'UpdateSpotMarket':
return null;
}
default: { default: {
return null; break;
} }
} }
if (indicator != null) {
details = (
<div className="flex gap-3 mb-3">
<div className={getIndicatorStyle(indicator)}>{indicator}</div>
<div>{details}</div>
</div>
);
}
return details;
}; };

View File

@ -70,6 +70,7 @@ export const Proposal = ({ proposal, restData }: ProposalProps) => {
if (!p?.terms) return null; if (!p?.terms) return null;
return ( return (
<ProposalChangeDetails <ProposalChangeDetails
indicator={i + 1}
key={i} key={i}
proposal={proposal} proposal={proposal}
terms={p.terms} terms={p.terms}

View File

@ -15,6 +15,7 @@ import {
type VoteFieldsFragment, type VoteFieldsFragment,
} from '../../__generated__/Proposals'; } from '../../__generated__/Proposals';
import { useBatchVoteInformation } from '../../hooks/use-vote-information'; import { useBatchVoteInformation } from '../../hooks/use-vote-information';
import { getIndicatorStyle } from '../proposal/colours';
export const CompactVotes = ({ number }: { number: BigNumber }) => ( export const CompactVotes = ({ number }: { number: BigNumber }) => (
<CompactNumber <CompactNumber
@ -176,6 +177,7 @@ const VoteBreakdownBatch = ({ proposal }: { proposal: BatchProposal }) => {
if (!p?.terms) return null; if (!p?.terms) return null;
return ( return (
<VoteBreakdownBatchSubProposal <VoteBreakdownBatchSubProposal
indicator={i + 1}
key={i} key={i}
proposal={proposal} proposal={proposal}
votes={proposal.votes} votes={proposal.votes}
@ -255,10 +257,12 @@ const VoteBreakdownBatchSubProposal = ({
proposal, proposal,
votes, votes,
terms, terms,
indicator,
}: { }: {
proposal: BatchProposal; proposal: BatchProposal;
votes: VoteFieldsFragment; votes: VoteFieldsFragment;
terms: ProposalTermsFieldsFragment; terms: ProposalTermsFieldsFragment;
indicator?: number;
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const voteInfo = useVoteInformation({ const voteInfo = useVoteInformation({
@ -269,9 +273,16 @@ const VoteBreakdownBatchSubProposal = ({
const isProposalOpen = proposal?.state === ProposalState.STATE_OPEN; const isProposalOpen = proposal?.state === ProposalState.STATE_OPEN;
const isUpdateMarket = terms?.change?.__typename === 'UpdateMarket'; const isUpdateMarket = terms?.change?.__typename === 'UpdateMarket';
const indicatorElement = indicator && (
<span className={getIndicatorStyle(indicator)}>{indicator}</span>
);
return ( return (
<div> <div>
<h4>{t(terms.change.__typename)}</h4> <div className="flex items-baseline gap-3">
{indicatorElement}
<h4>{t(terms.change.__typename)}</h4>
</div>
<VoteBreakDownUI <VoteBreakDownUI
voteInfo={voteInfo} voteInfo={voteInfo}
isProposalOpen={isProposalOpen} isProposalOpen={isProposalOpen}

View File

@ -159,6 +159,7 @@
"ContinueSharingData": "Continue sharing data", "ContinueSharingData": "Continue sharing data",
"copied!": "Copied!", "copied!": "Copied!",
"copyToClipboard": "Copy to clipboard", "copyToClipboard": "Copy to clipboard",
"copyId": "Copy ID to clipboard",
"CouldNotInstantiateMarket": "Could not instantiate market", "CouldNotInstantiateMarket": "Could not instantiate market",
"created": "Created", "created": "Created",
"CreateProposalAndDownloadJSONToShare": "Create proposal and download JSON to share", "CreateProposalAndDownloadJSONToShare": "Create proposal and download JSON to share",
@ -807,7 +808,7 @@
"unsupportedVersion": "Looks like you're running an outdated version of GoWallet. You're running {{version}} but {{requiredVersion}} is required.", "unsupportedVersion": "Looks like you're running an outdated version of GoWallet. You're running {{version}} but {{requiredVersion}} is required.",
"UpdateAsset": "Update asset", "UpdateAsset": "Update asset",
"UpdateAssetProposal": "Update asset proposal", "UpdateAssetProposal": "Update asset proposal",
"UpdateToMarket": "Update to market ID", "UpdateToMarket": "Update to market",
"OpenInConsole": "Open in Console", "OpenInConsole": "Open in Console",
"UpdateMarket": "Update market", "UpdateMarket": "Update market",
"UpdateMarketProposal": "Update market proposal", "UpdateMarketProposal": "Update market proposal",

View File

@ -8,6 +8,7 @@ import {
type PeggedReference, type PeggedReference,
type ProposalChange, type ProposalChange,
type TransferStatus, type TransferStatus,
MarketUpdateType,
} from './__generated__/types'; } from './__generated__/types';
import type { AccountType } from './__generated__/types'; import type { AccountType } from './__generated__/types';
import type { import type {
@ -755,3 +756,10 @@ export const LiquidityFeeMethodMappingDescription: {
METHOD_UNSPECIFIED: 'Unspecified', METHOD_UNSPECIFIED: 'Unspecified',
METHOD_WEIGHTED_AVERAGE: `This liquidity fee is the weighted average of all liquidity providers' nominated fees, weighted by their commitment.`, METHOD_WEIGHTED_AVERAGE: `This liquidity fee is the weighted average of all liquidity providers' nominated fees, weighted by their commitment.`,
}; };
export const MarketUpdateTypeMapping = {
[MarketUpdateType.MARKET_STATE_UPDATE_TYPE_RESUME]: 'Resume',
[MarketUpdateType.MARKET_STATE_UPDATE_TYPE_SUSPEND]: 'Suspend',
[MarketUpdateType.MARKET_STATE_UPDATE_TYPE_TERMINATE]: 'Terminate',
[MarketUpdateType.MARKET_STATE_UPDATE_TYPE_UNSPECIFIED]: 'Unspecified',
};