feat(governance): new market details improvements (#4106)
This commit is contained in:
parent
597e07608f
commit
cabd99d3ef
@ -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",
|
||||
|
6
apps/governance/src/lib/collapsible-toggle-styles.ts
Normal file
6
apps/governance/src/lib/collapsible-toggle-styles.ts
Normal 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,
|
||||
});
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -0,0 +1 @@
|
||||
export * from './proposal-market-data';
|
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
|
@ -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();
|
||||
|
@ -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>
|
||||
|
@ -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 />
|
||||
)}
|
||||
|
Loading…
Reference in New Issue
Block a user