feat(governance): new market details improvements (#4106)

This commit is contained in:
Sam Keen 2023-06-20 23:53:53 +01:00 committed by GitHub
parent 597e07608f
commit cabd99d3ef
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 315 additions and 63 deletions

View File

@ -596,6 +596,8 @@
"noPercentage": "No percentage",
"proposalJson": "Full proposal JSON",
"proposalDetails": "Proposal details",
"marketSpecification": "Market specification",
"viewMarketJson": "View market JSON",
"proposalDescription": "Description",
"currentlySetTo": "Currently expected to ",
"currently": "currently",

View File

@ -0,0 +1,6 @@
import classnames from 'classnames';
export const collapsibleToggleStyles = (toggleState: boolean) =>
classnames('mb-4 transition-transform ease-in-out duration-300', {
'rotate-180': toggleState,
});

View File

@ -1,9 +1,9 @@
import ReactMarkdown from 'react-markdown';
import classnames from 'classnames';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Icon, RoundedWrapper } from '@vegaprotocol/ui-toolkit';
import { SubHeading } from '../../../../components/heading';
import { collapsibleToggleStyles } from '../../../../lib/collapsible-toggle-styles';
export const ProposalDescription = ({
description,
@ -12,9 +12,6 @@ export const ProposalDescription = ({
}) => {
const { t } = useTranslation();
const [showDescription, setShowDescription] = useState(false);
const showDescriptionIconClasses = classnames('mb-4', {
'rotate-180': showDescription,
});
return (
<section data-testid="proposal-description">
@ -24,7 +21,7 @@ export const ProposalDescription = ({
>
<div className="flex items-center gap-3">
<SubHeading title={t('proposalDescription')} />
<div className={showDescriptionIconClasses}>
<div className={collapsibleToggleStyles(showDescription)}>
<Icon name="chevron-down" size={8} />
</div>
</div>

View File

@ -1,8 +1,8 @@
import classnames from 'classnames';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Icon, SyntaxHighlighter } from '@vegaprotocol/ui-toolkit';
import { SubHeading } from '../../../../components/heading';
import { collapsibleToggleStyles } from '../../../../lib/collapsible-toggle-styles';
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
@ -13,9 +13,6 @@ export const ProposalJson = ({
}) => {
const { t } = useTranslation();
const [showDetails, setShowDetails] = useState(false);
const showDetailsIconClasses = classnames('mb-4', {
'rotate-180': showDetails,
});
return (
<section data-testid="proposal-json">
@ -25,7 +22,7 @@ export const ProposalJson = ({
>
<div className="flex items-center gap-3">
<SubHeading title={t('proposalJson')} />
<div className={showDetailsIconClasses}>
<div className={collapsibleToggleStyles(showDetails)}>
<Icon name="chevron-down" size={8} />
</div>
</div>

View File

@ -0,0 +1 @@
export * from './proposal-market-data';

View File

@ -0,0 +1,222 @@
import isEqual from 'lodash/isEqual';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
InstrumentInfoPanel,
KeyDetailsInfoPanel,
LiquidityMonitoringParametersInfoPanel,
LiquidityPriceRangeInfoPanel,
MetadataInfoPanel,
OracleInfoPanel,
PriceMonitoringBoundsInfoPanel,
RiskFactorsInfoPanel,
RiskModelInfoPanel,
RiskParametersInfoPanel,
SettlementAssetInfoPanel,
} from '@vegaprotocol/markets';
import {
Accordion,
AccordionItem,
Button,
CopyWithTooltip,
Dialog,
Icon,
SyntaxHighlighter,
} from '@vegaprotocol/ui-toolkit';
import { SubHeading } from '../../../../components/heading';
import { collapsibleToggleStyles } from '../../../../lib/collapsible-toggle-styles';
import type { MarketInfoWithData } from '@vegaprotocol/markets';
import type { DataSourceDefinition } from '@vegaprotocol/types';
import { create } from 'zustand';
type MarketDataDialogState = {
isOpen: boolean;
open: () => void;
close: () => void;
};
export const useMarketDataDialogStore = create<MarketDataDialogState>(
(set) => ({
isOpen: false,
open: () => set({ isOpen: true }),
close: () => set({ isOpen: false }),
})
);
export const ProposalMarketData = ({
marketData,
}: {
marketData: MarketInfoWithData;
}) => {
const { t } = useTranslation();
const { isOpen, open, close } = useMarketDataDialogStore();
const [showDetails, setShowDetails] = useState(false);
if (!marketData) {
return null;
}
const settlementData = marketData.tradableInstrument.instrument.product
.dataSourceSpecForSettlementData.data as DataSourceDefinition;
const terminationData = marketData.tradableInstrument.instrument.product
.dataSourceSpecForTradingTermination.data as DataSourceDefinition;
const getSigners = (data: DataSourceDefinition) => {
if (data.sourceType.__typename === 'DataSourceDefinitionExternal') {
const signers = data.sourceType.sourceType.signers || [];
return signers.map(({ signer }) => {
return (
(signer.__typename === 'ETHAddress' && signer.address) ||
(signer.__typename === 'PubKey' && signer.key)
);
});
}
return [];
};
return (
<section className="relative" data-testid="proposal-market-data">
<button
onClick={() => setShowDetails(!showDetails)}
data-testid="proposal-market-data-toggle"
>
<div className="flex items-center gap-3">
<SubHeading title={t('marketSpecification')} />
<div className={collapsibleToggleStyles(showDetails)}>
<Icon name="chevron-down" size={8} />
</div>
</div>
</button>
{showDetails && (
<>
<div className="float-right">
<Button onClick={open} data-testid="view-market-json">
{t('viewMarketJson')}
</Button>
</div>
<div className="mb-10">
<Accordion>
<AccordionItem
itemId="key-details"
title={t('Key details')}
content={<KeyDetailsInfoPanel market={marketData} />}
/>
<AccordionItem
itemId="instrument"
title={t('Instrument')}
content={<InstrumentInfoPanel market={marketData} />}
/>
{isEqual(
getSigners(settlementData),
getSigners(terminationData)
) ? (
<AccordionItem
itemId="oracles"
title={t('Oracle')}
content={
<OracleInfoPanel
market={marketData}
type="settlementData"
/>
}
/>
) : (
<>
<AccordionItem
itemId="settlement-oracle"
title={t('Settlement Oracle')}
content={
<OracleInfoPanel
market={marketData}
type="settlementData"
/>
}
/>
<AccordionItem
itemId="termination-oracle"
title={t('Termination Oracle')}
content={
<OracleInfoPanel market={marketData} type="termination" />
}
/>
</>
)}
<AccordionItem
itemId="settlement-asset"
title={t('Settlement asset')}
content={<SettlementAssetInfoPanel market={marketData} />}
/>
<AccordionItem
itemId="metadata"
title={t('Metadata')}
content={<MetadataInfoPanel market={marketData} />}
/>
<AccordionItem
itemId="risk-model"
title={t('Risk model')}
content={<RiskModelInfoPanel market={marketData} />}
/>
<AccordionItem
itemId="risk-parameters"
title={t('Risk parameters')}
content={<RiskParametersInfoPanel market={marketData} />}
/>
<AccordionItem
itemId="risk-factors"
title={t('Risk factors')}
content={<RiskFactorsInfoPanel market={marketData} />}
/>
{(
marketData.priceMonitoringSettings?.parameters?.triggers || []
).map((_, triggerIndex) => (
<AccordionItem
itemId={`trigger-${triggerIndex}`}
title={t(`Price monitoring bounds ${triggerIndex + 1}`)}
content={
<PriceMonitoringBoundsInfoPanel
market={marketData}
triggerIndex={triggerIndex}
/>
}
/>
))}
<AccordionItem
itemId="liqudity-monitoring-parameters"
title={t('Liquidity monitoring parameters')}
content={
<LiquidityMonitoringParametersInfoPanel market={marketData} />
}
/>
<AccordionItem
itemId="liquidity-price-range"
title={t('Liquidity price range')}
content={<LiquidityPriceRangeInfoPanel market={marketData} />}
/>
</Accordion>
</div>
</>
)}
<Dialog
title={marketData.tradableInstrument.instrument.code}
open={isOpen}
onChange={(isOpen) => (isOpen ? open() : close())}
size="medium"
dataTestId="market-json-dialog"
>
<CopyWithTooltip text={JSON.stringify(marketData)}>
<button className="bg-vega-dark-100 rounded-sm py-2 px-3 mb-4 text-white">
<span>
<Icon name="duplicate" />
</span>
<span className="ml-2">Copy</span>
</button>
</CopyWithTooltip>
<SyntaxHighlighter data={marketData} />
</Dialog>
</section>
);
};

View File

@ -1,4 +1,3 @@
import classnames from 'classnames';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
@ -13,6 +12,7 @@ import { SubHeading } from '../../../../components/heading';
import { useVoteInformation } from '../../hooks';
import { useAppState } from '../../../../contexts/app-state/app-state-context';
import { ProposalType } from '../proposal/proposal';
import { collapsibleToggleStyles } from '../../../../lib/collapsible-toggle-styles';
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
@ -57,10 +57,6 @@ export const ProposalVotesTable = ({
? t('byTokenVote')
: t('byLiquidityVote');
const showDetailsIconClasses = classnames('mb-4', {
'rotate-180': showDetails,
});
return (
<>
<button
@ -69,7 +65,7 @@ export const ProposalVotesTable = ({
>
<div className="flex items-center gap-3">
<SubHeading title={t('voteBreakdown')} />
<div className={showDetailsIconClasses}>
<div className={collapsibleToggleStyles(showDetails)}>
<Icon name="chevron-down" size={8} />
</div>
</div>

View File

@ -29,9 +29,6 @@ jest.mock('../proposal-change-table', () => ({
jest.mock('../proposal-json', () => ({
ProposalJson: () => <div data-testid="proposal-json"></div>,
}));
jest.mock('../proposal-terms/proposal-terms', () => ({
ProposalTerms: () => <div data-testid="proposal-terms"></div>,
}));
jest.mock('../proposal-votes-table', () => ({
ProposalVotesTable: () => <div data-testid="proposal-votes-table"></div>,
}));
@ -74,7 +71,6 @@ it('renders each section', async () => {
expect(await screen.findByTestId('proposal-header')).toBeInTheDocument();
expect(screen.getByTestId('proposal-change-table')).toBeInTheDocument();
expect(screen.getByTestId('proposal-json')).toBeInTheDocument();
expect(screen.getByTestId('proposal-terms')).toBeInTheDocument();
expect(screen.getByTestId('proposal-votes-table')).toBeInTheDocument();
expect(screen.getByTestId('proposal-vote-details')).toBeInTheDocument();
expect(screen.queryByTestId('proposal-list-asset')).not.toBeInTheDocument();

View File

@ -1,22 +1,22 @@
import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
import {
NetworkParams,
useNetworkParams,
} from '@vegaprotocol/network-parameters';
import { AsyncRenderer, Icon, RoundedWrapper } from '@vegaprotocol/ui-toolkit';
import { ProposalHeader } from '../proposal-detail-header/proposal-header';
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import { ProposalDescription } from '../proposal-description';
import { ProposalChangeTable } from '../proposal-change-table';
import { ProposalJson } from '../proposal-json';
import { ProposalTerms } from '../proposal-terms';
import { ProposalVotesTable } from '../proposal-votes-table';
import { VoteDetails } from '../vote-details';
import { ListAsset } from '../list-asset';
import { Link } from 'react-router-dom';
import Routes from '../../../routes';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { ProposalMarketData } from '../proposal-market-data';
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import type { MarketInfoWithData } from '@vegaprotocol/markets';
export enum ProposalType {
PROPOSAL_NEW_MARKET = 'PROPOSAL_NEW_MARKET',
@ -28,11 +28,16 @@ export enum ProposalType {
}
export interface ProposalProps {
proposal: ProposalFieldsFragment | ProposalQuery['proposal'];
newMarketData?: MarketInfoWithData | null;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
restData: any;
}
export const Proposal = ({ proposal, restData }: ProposalProps) => {
export const Proposal = ({
proposal,
restData,
newMarketData,
}: ProposalProps) => {
const { t } = useTranslation();
const { params, loading, error } = useNetworkParams([
NetworkParams.governance_proposal_market_minVoterBalance,
@ -97,51 +102,58 @@ export const Proposal = ({ proposal, restData }: ProposalProps) => {
</div>
<ProposalHeader proposal={proposal} isListItem={false} />
<div className="my-10">
<ProposalChangeTable proposal={proposal} />
</div>
<div id="details">
<div className="my-10">
<ProposalChangeTable proposal={proposal} />
</div>
{proposal.terms.change.__typename === 'NewAsset' &&
proposal.terms.change.source.__typename === 'ERC20' &&
proposal.id ? (
<ListAsset
assetId={proposal.id}
withdrawalThreshold={proposal.terms.change.source.withdrawThreshold}
lifetimeLimit={proposal.terms.change.source.lifetimeLimit}
/>
) : null}
{proposal.terms.change.__typename === 'NewAsset' &&
proposal.terms.change.source.__typename === 'ERC20' &&
proposal.id ? (
<ListAsset
assetId={proposal.id}
withdrawalThreshold={
proposal.terms.change.source.withdrawThreshold
}
lifetimeLimit={proposal.terms.change.source.lifetimeLimit}
/>
) : null}
<div className="mb-4">
<ProposalDescription description={proposal.rationale.description} />
</div>
<div className="mb-4">
<ProposalDescription description={proposal.rationale.description} />
</div>
{proposal.terms.change.__typename !== 'NewMarket' &&
proposal.terms.change.__typename !== 'UpdateMarket' &&
proposal.terms.change.__typename !== 'NewFreeform' && (
{newMarketData && (
<div className="mb-4">
<ProposalTerms data={proposal.terms} />
<ProposalMarketData marketData={newMarketData} />
</div>
)}
<div className="mb-6">
<ProposalJson proposal={restData?.data?.proposal} />
<div className="mb-6">
<ProposalJson proposal={restData?.data?.proposal} />
</div>
</div>
<div className="mb-10">
<RoundedWrapper paddingBottom={true}>
<VoteDetails
<div id="voting">
<div className="mb-10">
<RoundedWrapper paddingBottom={true}>
<VoteDetails
proposal={proposal}
proposalType={proposalType}
minVoterBalance={minVoterBalance}
spamProtectionMinTokens={
params?.spam_protection_voting_min_tokens
}
/>
</RoundedWrapper>
</div>
<div className="mb-4">
<ProposalVotesTable
proposal={proposal}
proposalType={proposalType}
minVoterBalance={minVoterBalance}
spamProtectionMinTokens={
params?.spam_protection_voting_min_tokens
}
/>
</RoundedWrapper>
</div>
<div className="mb-4">
<ProposalVotesTable proposal={proposal} proposalType={proposalType} />
</div>
</div>
</section>
</AsyncRenderer>

View File

@ -7,6 +7,8 @@ import { ProposalNotFound } from '../components/proposal-not-found';
import { useProposalQuery } from './__generated__/Proposal';
import { useFetch } from '@vegaprotocol/react-helpers';
import { ENV } from '../../../config';
import { useDataProvider } from '@vegaprotocol/data-provider';
import { marketInfoWithDataProvider } from '@vegaprotocol/markets';
export const ProposalContainer = () => {
const params = useParams<{ proposalId: string }>();
@ -20,15 +22,36 @@ export const ProposalContainer = () => {
skip: !params.proposalId,
});
const {
data: newMarketData,
loading: newMarketLoading,
error: newMarketError,
} = useDataProvider({
dataProvider: marketInfoWithDataProvider,
skipUpdates: true,
variables: {
marketId: data?.proposal?.id || '',
skip: !data?.proposal?.id,
},
});
useEffect(() => {
const interval = setInterval(refetch, 1000);
const interval = setInterval(refetch, 2000);
return () => clearInterval(interval);
}, [refetch]);
return (
<AsyncRenderer loading={loading} error={error} data={data}>
<AsyncRenderer
loading={loading || newMarketLoading}
error={error || newMarketError}
data={newMarketData ? { newMarketData, data } : data}
>
{data?.proposal ? (
<Proposal proposal={data.proposal} restData={restData} />
<Proposal
proposal={data.proposal}
restData={restData}
newMarketData={newMarketData}
/>
) : (
<ProposalNotFound />
)}