diff --git a/apps/token/src/routes/governance/components/proposals-list/proposals-list.tsx b/apps/token/src/routes/governance/components/proposals-list/proposals-list.tsx index ab99ec9d4..63d5c05c2 100644 --- a/apps/token/src/routes/governance/components/proposals-list/proposals-list.tsx +++ b/apps/token/src/routes/governance/components/proposals-list/proposals-list.tsx @@ -9,6 +9,7 @@ import Routes from '../../../routes'; import { Button } from '@vegaprotocol/ui-toolkit'; import { Link } from 'react-router-dom'; import { Links } from '../../../../config'; +import { ExternalLink } from '@vegaprotocol/ui-toolkit'; interface ProposalsListProps { proposals: Proposals_proposals[]; @@ -65,14 +66,9 @@ export const ProposalsList = ({ proposals }: ProposalsListProps) => { {t( `The Vega network is governed by the community. View active proposals, vote on them or propose changes to the network.` )}{' '} - + {t(`Find out more about Vega governance`)} - +

{proposals.length > 0 && ( diff --git a/apps/trading-e2e/src/support/mocks/generate-market-info-query.ts b/apps/trading-e2e/src/support/mocks/generate-market-info-query.ts index 926fc4120..39238e0b0 100644 --- a/apps/trading-e2e/src/support/mocks/generate-market-info-query.ts +++ b/apps/trading-e2e/src/support/mocks/generate-market-info-query.ts @@ -20,6 +20,15 @@ export const generateMarketInfoQuery = ( positionDecimalPlaces: 0, state: MarketState.STATE_ACTIVE, tradingMode: MarketTradingMode.TRADING_MODE_CONTINUOUS, + proposal: { + __typename: 'Proposal', + id: 'market-0', + rationale: { + __typename: 'ProposalRationale', + title: 'ETHBTC', + description: '', + }, + }, accounts: [ { type: AccountType.ACCOUNT_TYPE_INSURANCE, diff --git a/apps/trading/.env b/apps/trading/.env index 005b9b8e6..5e9905ad6 100644 --- a/apps/trading/.env +++ b/apps/trading/.env @@ -6,4 +6,5 @@ NX_ETHERSCAN_URL=https://ropsten.etherscan.io NX_VEGA_NETWORKS={\"MAINNET\":\"https://alpha.console.vega.xyz\"} NX_USE_ENV_OVERRIDES=1 NX_VEGA_EXPLORER_URL=https://explorer.fairground.wtf -NX_VEGA_WALLET_URL=http://localhost:1789/api/v1 +NX_VEGA_TOKEN_URL=https://token.fairground.wtf +NX_VEGA_WALLET_URL=http://localhost:1789/api/v1 \ No newline at end of file diff --git a/apps/trading/.env.devnet b/apps/trading/.env.devnet index de04803c7..b8d601848 100644 --- a/apps/trading/.env.devnet +++ b/apps/trading/.env.devnet @@ -6,3 +6,4 @@ NX_VEGA_NETWORKS={\"MAINNET\":\"https://alpha.console.vega.xyz\"} NX_ETHEREUM_PROVIDER_URL=https://ropsten.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8 NX_ETHERSCAN_URL=https://ropsten.etherscan.io NX_VEGA_EXPLORER_URL=https://dev.explorer.vega.xyz +NX_VEGA_TOKEN_URL=https://token.fairground.wtf \ No newline at end of file diff --git a/apps/trading/.env.mainnet b/apps/trading/.env.mainnet index 9aa4b5d17..c23385bad 100644 --- a/apps/trading/.env.mainnet +++ b/apps/trading/.env.mainnet @@ -6,3 +6,4 @@ NX_VEGA_NETWORKS='{\"MAINNET\":\"https://alpha.console.vega.xyz\"}' NX_ETHEREUM_PROVIDER_URL=https://mainnet.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8 NX_ETHERSCAN_URL=https://etherscan.io NX_VEGA_EXPLORER_URL=https://explorer.vega.xyz +NX_VEGA_TOKEN_URL=https://token.vega.xyz \ No newline at end of file diff --git a/apps/trading/.env.stagnet3 b/apps/trading/.env.stagnet3 index c2d7fe4de..353c8bfd9 100644 --- a/apps/trading/.env.stagnet3 +++ b/apps/trading/.env.stagnet3 @@ -6,3 +6,4 @@ NX_VEGA_NETWORKS='{\"MAINNET\":\"https://alpha.console.vega.xyz\"}' NX_ETHEREUM_PROVIDER_URL=https://ropsten.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8 NX_ETHERSCAN_URL=https://ropsten.etherscan.io NX_VEGA_EXPLORER_URL=https://staging3.explorer.vega.xyz +NX_VEGA_TOKEN_URL=https://token.fairground.wtf \ No newline at end of file diff --git a/apps/trading/.env.testnet b/apps/trading/.env.testnet index 502a98025..5bd96ac0e 100644 --- a/apps/trading/.env.testnet +++ b/apps/trading/.env.testnet @@ -5,3 +5,4 @@ NX_VEGA_NETWORKS='{\"MAINNET\":\"https://alpha.console.vega.xyz\"}' NX_ETHEREUM_PROVIDER_URL=https://ropsten.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8 NX_ETHERSCAN_URL=https://ropsten.etherscan.io NX_VEGA_EXPLORER_URL=https://explorer.fairground.wtf +NX_VEGA_TOKEN_URL=https://token.fairground.wtf \ No newline at end of file diff --git a/libs/deal-ticket/src/components/__generated__/MarketInfoQuery.ts b/libs/deal-ticket/src/components/__generated__/MarketInfoQuery.ts index 010e84997..0193664e7 100644 --- a/libs/deal-ticket/src/components/__generated__/MarketInfoQuery.ts +++ b/libs/deal-ticket/src/components/__generated__/MarketInfoQuery.ts @@ -9,6 +9,34 @@ import { Interval, MarketState, AccountType, MarketTradingMode, AuctionTrigger } // GraphQL query operation: MarketInfoQuery // ==================================================== +export interface MarketInfoQuery_market_proposal_rationale { + __typename: "ProposalRationale"; + /** + * Title to be used to give a short description of the proposal in lists. + * This is to be between 0 and 100 unicode characters. + * This is mandatory for all proposals. + */ + title: string; + /** + * Description to show a short title / something in case the link goes offline. + * This is to be between 0 and 20k unicode characters. + * This is mandatory for all proposals. + */ + description: string; +} + +export interface MarketInfoQuery_market_proposal { + __typename: "Proposal"; + /** + * Proposal ID that is filled by Vega once proposal reaches the network + */ + id: string | null; + /** + * Rationale behind the proposal + */ + rationale: MarketInfoQuery_market_proposal_rationale; +} + export interface MarketInfoQuery_market_accounts_asset { __typename: "Asset"; /** @@ -451,6 +479,10 @@ export interface MarketInfoQuery_market { * Current state of the market */ state: MarketState; + /** + * The proposal that initiated this market + */ + proposal: MarketInfoQuery_market_proposal | null; /** * Get account for a party or market */ diff --git a/libs/deal-ticket/src/components/info-market-query.ts b/libs/deal-ticket/src/components/info-market-query.ts index 9ec53cf21..6c197b93a 100644 --- a/libs/deal-ticket/src/components/info-market-query.ts +++ b/libs/deal-ticket/src/components/info-market-query.ts @@ -8,6 +8,13 @@ export const MARKET_INFO_QUERY = gql` decimalPlaces positionDecimalPlaces state + proposal { + id + rationale { + title + description + } + } accounts { type asset { @@ -16,13 +23,6 @@ export const MARKET_INFO_QUERY = gql` balance } tradingMode - accounts { - type - asset { - id - } - balance - } fees { factors { makerFee @@ -44,13 +44,6 @@ export const MARKET_INFO_QUERY = gql` short long } - accounts { - type - asset { - id - } - balance - } data { market { id diff --git a/libs/deal-ticket/src/components/info-market.tsx b/libs/deal-ticket/src/components/info-market.tsx index 685c9272b..e7fb234e6 100644 --- a/libs/deal-ticket/src/components/info-market.tsx +++ b/libs/deal-ticket/src/components/info-market.tsx @@ -26,6 +26,14 @@ import { useQuery } from '@apollo/client'; import { totalFees } from '@vegaprotocol/market-list'; import { AccountType, Interval } from '@vegaprotocol/types'; import { MARKET_INFO_QUERY } from './info-market-query'; +import { ExternalLink } from '@vegaprotocol/ui-toolkit'; + +import { generatePath } from 'react-router-dom'; +import { useEnvironment } from '@vegaprotocol/environment'; + +const Links = { + PROPOSAL_PAGE: ':tokenUrl/governance/:proposalId', +}; export interface InfoProps { market: MarketInfoQuery_market; @@ -68,7 +76,8 @@ export const MarketInfoContainer = ({ marketId }: MarketInfoContainerProps) => { }; export const Info = ({ market }: InfoProps) => { - const headerClassName = 'uppercase text-lg mb-4'; + const { VEGA_TOKEN_URL } = useEnvironment(); + const headerClassName = 'uppercase text-lg'; const dayVolume = calcCandleVolume(market); const marketDataPanels = [ { @@ -272,16 +281,45 @@ export const Info = ({ market }: InfoProps) => { }, ]; + const marketGovPanels = [ + { + title: t('Proposal'), + content: ( +

+ + {t('View governance proposal')} + +

+ ), + }, + ]; + return (

{t('Market data')}

-
+

{t('Market specification')}

+ {VEGA_TOKEN_URL && market.proposal?.id && ( +
+

{t('Market governance')}

+ +
+ )}
); }; diff --git a/libs/environment/src/utils/compile-environment.ts b/libs/environment/src/utils/compile-environment.ts index 3b4de999a..1b61c3564 100644 --- a/libs/environment/src/utils/compile-environment.ts +++ b/libs/environment/src/utils/compile-environment.ts @@ -70,6 +70,8 @@ const getBundledEnvironmentValue = (key: EnvKey) => { return process.env['NX_VEGA_EXPLORER_URL']; case 'VEGA_WALLET_URL': return process.env['NX_VEGA_WALLET_URL']; + case 'VEGA_TOKEN_URL': + return process.env['NX_VEGA_TOKEN_URL']; } }; diff --git a/libs/environment/src/utils/validate-environment.ts b/libs/environment/src/utils/validate-environment.ts index 1488da896..260e8b275 100644 --- a/libs/environment/src/utils/validate-environment.ts +++ b/libs/environment/src/utils/validate-environment.ts @@ -21,6 +21,7 @@ const schemaObject = { GITHUB_FEEDBACK_URL: z.optional(z.string()), VEGA_ENV: z.nativeEnum(Networks), VEGA_EXPLORER_URL: z.optional(z.string()), + VEGA_TOKEN_URL: z.optional(z.string()), VEGA_NETWORKS: z .object( Object.keys(Networks).reduce( diff --git a/libs/ui-toolkit/src/components/icon/icon.tsx b/libs/ui-toolkit/src/components/icon/icon.tsx index 202bea14a..d9ea92b16 100644 --- a/libs/ui-toolkit/src/components/icon/icon.tsx +++ b/libs/ui-toolkit/src/components/icon/icon.tsx @@ -7,7 +7,7 @@ export type { IconName } from '@blueprintjs/icons'; export interface IconProps { name: IconName; className?: string; - size?: 4 | 6 | 8 | 10 | 12 | 14 | 16; + size?: 2 | 3 | 4 | 6 | 8 | 10 | 12 | 14 | 16; ariaLabel?: string; } @@ -21,6 +21,8 @@ export const Icon = ({ size = 4, name, className, ariaLabel }: IconProps) => { // Cant just concatenate as TW wont pick up that the class is being used // so below syntax is required { + 'w-2 h-2': size === 2, + 'w-3 h-3': size === 3, 'w-4 h-4': size === 4, 'w-6 h-6': size === 6, 'w-8 h-8': size === 8, diff --git a/libs/ui-toolkit/src/components/link/index.ts b/libs/ui-toolkit/src/components/link/index.ts index 63b88256e..e33728e03 100644 --- a/libs/ui-toolkit/src/components/link/index.ts +++ b/libs/ui-toolkit/src/components/link/index.ts @@ -1 +1 @@ -export { Link } from './link'; +export * from './link'; diff --git a/libs/ui-toolkit/src/components/link/link.spec.tsx b/libs/ui-toolkit/src/components/link/link.spec.tsx index 37f8621e2..389105dbf 100644 --- a/libs/ui-toolkit/src/components/link/link.spec.tsx +++ b/libs/ui-toolkit/src/components/link/link.spec.tsx @@ -1,34 +1,62 @@ import { render, screen } from '@testing-library/react'; -import { Link } from '.'; +import { ExternalLink, Link } from '.'; -it('renders a link with a text', () => { - render( - - Link text - - ); - const link = screen.getByRole('link', { name: 'Link title' }); - expect(link).toBeInTheDocument(); - expect(link).toHaveAttribute('data-testid', 'link'); - expect(link).toHaveAttribute('referrerPolicy', 'strict-origin'); - expect(link).toHaveAttribute('href', '127.0.0.1'); - expect(link).toHaveAttribute('title', 'Link title'); - expect(link).toHaveClass('cursor-pointer'); - expect(link).toHaveClass('underline'); +describe('Link', () => { + it('renders a link with a text', () => { + render( + + Link text + + ); + const link = screen.getByRole('link', { name: 'Link title' }); + expect(link).toBeInTheDocument(); + expect(link).toHaveAttribute('data-testid', 'link'); + expect(link).toHaveAttribute('referrerPolicy', 'strict-origin'); + expect(link).toHaveAttribute('href', '127.0.0.1'); + expect(link).toHaveAttribute('title', 'Link title'); + expect(link).toHaveClass('cursor-pointer'); + expect(link).toHaveClass('underline'); + }); + + it('renders a link with children elements', () => { + render( + + Link text + + ); + const link = screen.getByRole('link', { name: 'Link title' }); + expect(link).toBeInTheDocument(); + expect(link).toHaveAttribute('data-testid', 'link'); + expect(link).toHaveAttribute('referrerPolicy', 'strict-origin'); + expect(link).toHaveAttribute('href', '127.0.0.1'); + expect(link).toHaveAttribute('title', 'Link title'); + expect(link).toHaveClass('cursor-pointer'); + expect(link).not.toHaveClass('underline'); + }); }); -it('renders a link with children elements', () => { - render( - - Link text - - ); - const link = screen.getByRole('link', { name: 'Link title' }); - expect(link).toBeInTheDocument(); - expect(link).toHaveAttribute('data-testid', 'link'); - expect(link).toHaveAttribute('referrerPolicy', 'strict-origin'); - expect(link).toHaveAttribute('href', '127.0.0.1'); - expect(link).toHaveAttribute('title', 'Link title'); - expect(link).toHaveClass('cursor-pointer'); - expect(link).not.toHaveClass('underline'); +describe('ExternalLink', () => { + it('should have an icon indicating that it is an external link', () => { + render(Go to Vega); + const link = screen.getByTestId('external-link'); + expect(link.children.length).toEqual(2); + expect(link.children[1].tagName.toUpperCase()).toEqual('SVG'); + }); + + it('should have an underlined text part', () => { + render(Go to Vega); + const link = screen.getByTestId('external-link'); + expect(link.children[0]).toHaveClass('underline'); + }); + + it('should not have an icon or underlined text if wrapping an element', () => { + render( + +
inner element
+
+ ); + const link = screen.getByTestId('external-link'); + expect(link.children.length).toEqual(1); + expect(link.children[0]).toHaveClass('inner'); + }); }); diff --git a/libs/ui-toolkit/src/components/link/link.tsx b/libs/ui-toolkit/src/components/link/link.tsx index 8d973f12b..b729017c5 100644 --- a/libs/ui-toolkit/src/components/link/link.tsx +++ b/libs/ui-toolkit/src/components/link/link.tsx @@ -1,5 +1,7 @@ +import { IconNames } from '@blueprintjs/icons'; import classNames from 'classnames'; import type { ReactNode, AnchorHTMLAttributes } from 'react'; +import { Icon } from '../icon'; type LinkProps = AnchorHTMLAttributes & { children?: ReactNode; @@ -27,5 +29,27 @@ export const Link = ({ className, children, ...props }: LinkProps) => { ); }; - Link.displayName = 'Link'; + +export const ExternalLink = ({ children, className, ...props }: LinkProps) => ( + + {typeof children === 'string' ? ( + <> + + {children} + + + + ) : ( + children + )} + +); +ExternalLink.displayName = 'ExternalLink';