diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 64553b175..f8f8a9a2a 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -3,6 +3,7 @@ "nrwl.angular-console", "esbenp.prettier-vscode", "firsttris.vscode-jest-runner", - "dbaeumer.vscode-eslint" + "dbaeumer.vscode-eslint", + "stevejpurves.cucumber" ] } diff --git a/apps/explorer-e2e/src/integration/network-page.feature b/apps/explorer-e2e/src/integration/network-page.feature index 466d9a568..52823f2cf 100644 --- a/apps/explorer-e2e/src/integration/network-page.feature +++ b/apps/explorer-e2e/src/integration/network-page.feature @@ -9,3 +9,15 @@ Feature: Network parameters Page Given I am on mobile and open the toggle menu When I navigate to network parameters page Then network parameters page is correctly displayed + + Scenario: Navigate to network parameters page and check each value is non-empty + Given I am on the homepage + When I navigate to network parameters page + Then network parameters page is correctly displayed + And each value is non-empty + + Scenario: Navigate to network parameters page and check each value using mobile + Given I am on mobile and open the toggle menu + When I navigate to network parameters page + Then network parameters page is correctly displayed + And each value is non-empty diff --git a/apps/explorer-e2e/src/support/pages/network-page.ts b/apps/explorer-e2e/src/support/pages/network-page.ts index 963e0e1ad..d1898b08b 100644 --- a/apps/explorer-e2e/src/support/pages/network-page.ts +++ b/apps/explorer-e2e/src/support/pages/network-page.ts @@ -11,4 +11,12 @@ export default class NetworkParametersPage extends BasePage { ); cy.getByTestId(this.parameters).should('not.be.empty'); } + + eachValueIsNonEmpty() { + cy.getByTestId(this.parameters).then(($parameters) => { + $parameters.each((_index, element) => { + cy.wrap(element).should('not.be.empty'); + }); + }); + } } diff --git a/apps/explorer-e2e/src/support/step_definitions/network-page.step.ts b/apps/explorer-e2e/src/support/step_definitions/network-page.step.ts index 8cfda1d37..c4e9f2114 100644 --- a/apps/explorer-e2e/src/support/step_definitions/network-page.step.ts +++ b/apps/explorer-e2e/src/support/step_definitions/network-page.step.ts @@ -9,3 +9,7 @@ When('I navigate to network parameters page', () => { Then('network parameters page is correctly displayed', () => { networkPage.verifyNetworkParametersDisplayed(); }); + +Then('each value is non-empty', () => { + networkPage.eachValueIsNonEmpty(); +}); diff --git a/apps/explorer/src/app/routes/network-parameters/index.tsx b/apps/explorer/src/app/routes/network-parameters/index.tsx index 6374cfaa4..90c3f470a 100644 --- a/apps/explorer/src/app/routes/network-parameters/index.tsx +++ b/apps/explorer/src/app/routes/network-parameters/index.tsx @@ -1,28 +1 @@ -import { gql, useQuery } from '@apollo/client'; -import { RouteTitle } from '../../components/route-title'; -import type { NetworkParametersQuery } from './__generated__/NetworkParametersQuery'; -import { SyntaxHighlighter } from '@vegaprotocol/ui-toolkit'; -import { t } from '@vegaprotocol/react-helpers'; - -export const NETWORK_PARAMETERS_QUERY = gql` - query NetworkParametersQuery { - networkParameters { - key - value - } - } -`; - -const NetworkParameters = () => { - const { data } = useQuery(NETWORK_PARAMETERS_QUERY); - return ( -
- - {t('Network Parameters')} - - {data ? : null} -
- ); -}; - -export default NetworkParameters; +export * from './network-parameters'; diff --git a/apps/explorer/src/app/routes/network-parameters/network-parameters.test.tsx b/apps/explorer/src/app/routes/network-parameters/network-parameters.test.tsx new file mode 100644 index 000000000..db966e9dc --- /dev/null +++ b/apps/explorer/src/app/routes/network-parameters/network-parameters.test.tsx @@ -0,0 +1,73 @@ +import { render, screen } from '@testing-library/react'; +import { NetworkParametersTable } from './network-parameters'; +import type { NetworkParametersQuery } from './__generated__/NetworkParametersQuery'; + +describe('NetworkParametersTable', () => { + it('renders correctly when it has network params', () => { + const data: NetworkParametersQuery = { + networkParameters: [ + { + __typename: 'NetworkParameter', + key: 'market.liquidityProvision.minLpStakeQuantumMultiple', + value: '1', + }, + { + __typename: 'NetworkParameter', + key: 'market.fee.factors.infrastructureFee', + value: '0.0005', + }, + ], + }; + render(); + expect(screen.getByTestId('network-param-header')).toHaveTextContent( + 'Network Parameters' + ); + const rows = screen.getAllByTestId('key-value-table-row'); + expect(rows[0].children[0]).toHaveTextContent( + 'market.fee.factors.infrastructureFee' + ); + expect(rows[1].children[0]).toHaveTextContent( + 'market.liquidityProvision.minLpStakeQuantumMultiple' + ); + expect(rows[0].children[1]).toHaveTextContent('0.0005'); + expect(rows[1].children[1]).toHaveTextContent('1'); + }); + + it('renders the rows in ascending order', () => { + const data: NetworkParametersQuery = { + networkParameters: [ + { + __typename: 'NetworkParameter', + key: 'market.fee.factors.infrastructureFee', + value: '0.0005', + }, + { + __typename: 'NetworkParameter', + key: 'market.liquidityProvision.minLpStakeQuantumMultiple', + value: '1', + }, + ], + }; + render(); + expect(screen.getByTestId('network-param-header')).toHaveTextContent( + 'Network Parameters' + ); + const rows = screen.getAllByTestId('key-value-table-row'); + expect(rows[0].children[0]).toHaveTextContent( + 'market.fee.factors.infrastructureFee' + ); + expect(rows[1].children[0]).toHaveTextContent( + 'market.liquidityProvision.minLpStakeQuantumMultiple' + ); + expect(rows[0].children[1]).toHaveTextContent('0.0005'); + expect(rows[1].children[1]).toHaveTextContent('1'); + }); + + it('does not render rows when is loading', () => { + render(); + expect(screen.getByTestId('network-param-header')).toHaveTextContent( + 'Network Parameters' + ); + expect(screen.queryByTestId('key-value-table-row')).not.toBeInTheDocument(); + }); +}); diff --git a/apps/explorer/src/app/routes/network-parameters/network-parameters.tsx b/apps/explorer/src/app/routes/network-parameters/network-parameters.tsx new file mode 100644 index 000000000..7fb9d95a7 --- /dev/null +++ b/apps/explorer/src/app/routes/network-parameters/network-parameters.tsx @@ -0,0 +1,118 @@ +import { gql, useQuery } from '@apollo/client'; +import { + AsyncRenderer, + KeyValueTable, + KeyValueTableRow, + SyntaxHighlighter, +} from '@vegaprotocol/ui-toolkit'; +import { + addDecimalsFormatNumber, + formatNumber, + t, +} from '@vegaprotocol/react-helpers'; +import { RouteTitle } from '../../components/route-title'; +import type { + NetworkParametersQuery, + NetworkParametersQuery_networkParameters, +} from './__generated__/NetworkParametersQuery'; +import orderBy from 'lodash/orderBy'; + +const BIG_NUMBER_PARAMS = [ + 'spam.protection.delegation.min.tokens', + 'validators.delegation.minAmount', + 'reward.staking.delegation.minimumValidatorStake', + 'reward.staking.delegation.maxPayoutPerParticipant', + 'reward.staking.delegation.maxPayoutPerEpoch', + 'spam.protection.voting.min.tokens', + 'governance.proposal.freeform.minProposerBalance', + 'governance.proposal.updateNetParam.minVoterBalance', + 'governance.proposal.updateMarket.minVoterBalance', + 'governance.proposal.asset.minVoterBalance', + 'governance.proposal.updateNetParam.minProposerBalance', + 'governance.proposal.freeform.minVoterBalance', + 'spam.protection.proposal.min.tokens', + 'governance.proposal.updateMarket.minProposerBalance', + 'governance.proposal.asset.minProposerBalance', +]; + +export const renderRow = ({ + key, + value, +}: NetworkParametersQuery_networkParameters) => { + const isSyntaxRow = isJsonObject(value); + return ( + + {key} + {isSyntaxRow ? ( + + ) : isNaN(Number(value)) ? ( + value + ) : BIG_NUMBER_PARAMS.includes(key) ? ( + addDecimalsFormatNumber(Number(value), 4) + ) : ( + formatNumber(Number(value), 4) + )} + + ); +}; + +export const isJsonObject = (str: string) => { + try { + return JSON.parse(str) && Object.keys(JSON.parse(str)).length > 0; + } catch (e) { + return false; + } +}; + +export const NETWORK_PARAMETERS_QUERY = gql` + query NetworkParametersQuery { + networkParameters { + key + value + } + } +`; + +export interface NetworkParametersTableProps + extends React.HTMLAttributes { + data?: NetworkParametersQuery; + error?: Error; + loading: boolean; +} + +export const NetworkParametersTable = ({ + data, + error, + loading, +}: NetworkParametersTableProps) => ( +
+ + {t('Network Parameters')} + + + { + const ascParams = orderBy( + data.networkParameters || [], + (param) => param.key, + 'asc' + ); + return ( + + {(ascParams || []).map((row) => renderRow(row))} + + ); + }} + /> +
+); + +export const NetworkParameters = () => { + const { data, loading, error } = useQuery( + NETWORK_PARAMETERS_QUERY + ); + return ; +}; diff --git a/apps/explorer/src/app/routes/router-config.tsx b/apps/explorer/src/app/routes/router-config.tsx index 60a3003a7..01393b0c9 100644 --- a/apps/explorer/src/app/routes/router-config.tsx +++ b/apps/explorer/src/app/routes/router-config.tsx @@ -9,7 +9,6 @@ import { Party as PartySingle } from './parties/id'; import Txs from './txs'; import Validators from './validators'; import Genesis from './genesis'; -import NetworkParameters from './network-parameters'; import { Block } from './blocks/id'; import { Blocks } from './blocks/home'; import { Tx } from './txs/id'; @@ -18,6 +17,7 @@ import { PendingTxs } from './pending'; import flags from '../lib/flags'; import { t } from '@vegaprotocol/react-helpers'; import { Routes } from './route-names'; +import { NetworkParameters } from './network-parameters'; const partiesRoutes = flags.parties ? [ diff --git a/libs/accounts/src/lib/accounts-table.tsx b/libs/accounts/src/lib/accounts-table.tsx index a4dcc6623..fd44f5746 100644 --- a/libs/accounts/src/lib/accounts-table.tsx +++ b/libs/accounts/src/lib/accounts-table.tsx @@ -2,7 +2,7 @@ import { forwardRef } from 'react'; import type { ColumnApi, ValueFormatterParams } from 'ag-grid-community'; import { PriceCell, - formatNumber, + addDecimalsFormatNumber, t, addSummaryRows, } from '@vegaprotocol/react-helpers'; @@ -136,7 +136,7 @@ export const AccountsTable = forwardRef( value, data, }: AccountsTableValueFormatterParams) => - formatNumber(value, data.asset.decimals) + addDecimalsFormatNumber(value, data.asset.decimals) } /> diff --git a/libs/deal-ticket/src/order-dialog.tsx b/libs/deal-ticket/src/order-dialog.tsx index 74492590c..d3bbe4926 100644 --- a/libs/deal-ticket/src/order-dialog.tsx +++ b/libs/deal-ticket/src/order-dialog.tsx @@ -1,7 +1,7 @@ import { Icon, Loader } from '@vegaprotocol/ui-toolkit'; import type { ReactNode } from 'react'; import type { OrderEvent_busEvents_event_Order } from './__generated__/OrderEvent'; -import { formatNumber, t } from '@vegaprotocol/react-helpers'; +import { addDecimalsFormatNumber, t } from '@vegaprotocol/react-helpers'; import type { VegaTxState } from '@vegaprotocol/wallet'; import { VegaTxStatus } from '@vegaprotocol/wallet'; @@ -76,7 +76,7 @@ export const OrderDialog = ({ {finalizedOrder.type === 'Limit' && finalizedOrder.market && (

{t( - `Price: ${formatNumber( + `Price: ${addDecimalsFormatNumber( finalizedOrder.price, finalizedOrder.market.decimalPlaces )}` diff --git a/libs/market-depth/src/lib/orderbook-row.tsx b/libs/market-depth/src/lib/orderbook-row.tsx index 119c13788..a0b4b4051 100644 --- a/libs/market-depth/src/lib/orderbook-row.tsx +++ b/libs/market-depth/src/lib/orderbook-row.tsx @@ -3,7 +3,7 @@ import { PriceCell, Vol, CumulativeVol, - formatNumber, + addDecimalsFormatNumber, } from '@vegaprotocol/react-helpers'; interface OrderbookRowProps { @@ -33,7 +33,7 @@ export const OrderbookRow = React.memo( ( type="rightAligned" cellRenderer="PriceFlashCell" valueFormatter={({ value, data }: ValueFormatterParams) => - formatNumber(value, data.decimalPlaces) + addDecimalsFormatNumber(value, data.decimalPlaces) } /> ( field="data.bestOfferPrice" type="rightAligned" valueFormatter={({ value, data }: ValueFormatterParams) => - formatNumber(value, data.decimalPlaces) + addDecimalsFormatNumber(value, data.decimalPlaces) } cellRenderer="PriceFlashCell" /> @@ -78,7 +82,7 @@ export const MarketListTable = forwardRef( type="rightAligned" cellRenderer="PriceFlashCell" valueFormatter={({ value, data }: ValueFormatterParams) => - formatNumber(value, data.decimalPlaces) + addDecimalsFormatNumber(value, data.decimalPlaces) } /> diff --git a/libs/network-stats/src/config/stats-fields.ts b/libs/network-stats/src/config/stats-fields.ts index a59542d88..5a1fa1d35 100644 --- a/libs/network-stats/src/config/stats-fields.ts +++ b/libs/network-stats/src/config/stats-fields.ts @@ -1,4 +1,4 @@ -import { formatNumber, t } from '@vegaprotocol/react-helpers'; +import { addDecimalsFormatNumber, t } from '@vegaprotocol/react-helpers'; import type { Stats as IStats, StatFields as IStatFields } from './types'; // Stats fields config. Keys will correspond to graphql queries when used, and values @@ -60,7 +60,7 @@ export const statsFields: { [key in keyof IStats]: IStatFields[] } = { { title: t('Total staked'), formatter: (total: string) => { - return formatNumber(total, 18, 2); + return addDecimalsFormatNumber(total, 18, 2); }, description: t('Sum of VEGA associated with a Vega key'), }, diff --git a/libs/positions/src/lib/positions-table.tsx b/libs/positions/src/lib/positions-table.tsx index fe3e7d05e..c5a420920 100644 --- a/libs/positions/src/lib/positions-table.tsx +++ b/libs/positions/src/lib/positions-table.tsx @@ -2,7 +2,7 @@ import { forwardRef } from 'react'; import type { ValueFormatterParams } from 'ag-grid-community'; import { PriceFlashCell, - formatNumber, + addDecimalsFormatNumber, volumePrefix, addDecimal, t, @@ -100,7 +100,7 @@ export const PositionsTable = forwardRef( value, data, }: PositionsTableValueFormatterParams) => - formatNumber(value, data.market.decimalPlaces) + addDecimalsFormatNumber(value, data.market.decimalPlaces) } /> { + return getNumberFormat(formatDecimals).format(Number(rawValue)); +}; + +export const addDecimalsFormatNumber = ( rawValue: string | number, decimalPlaces: number, formatDecimals: number = decimalPlaces ) => { const x = addDecimal(rawValue, decimalPlaces); - return getNumberFormat(formatDecimals).format(Number(x)); + return formatNumber(x, formatDecimals); }; diff --git a/libs/trades/src/lib/trades-table.tsx b/libs/trades/src/lib/trades-table.tsx index 395ad09ae..2e2aa577f 100644 --- a/libs/trades/src/lib/trades-table.tsx +++ b/libs/trades/src/lib/trades-table.tsx @@ -4,7 +4,7 @@ import { forwardRef, useMemo } from 'react'; import { AgGridDynamic as AgGrid } from '@vegaprotocol/ui-toolkit'; import type { TradeFields } from './__generated__/TradeFields'; import { - formatNumber, + addDecimalsFormatNumber, getDateTimeFormat, t, } from '@vegaprotocol/react-helpers'; @@ -67,7 +67,7 @@ export const TradesTable = forwardRef( field="price" cellClass={changeCellClass('price')} valueFormatter={({ value, data }: ValueFormatterParams) => { - return formatNumber(value, data.market.decimalPlaces); + return addDecimalsFormatNumber(value, data.market.decimalPlaces); }} /> { const { container } = render( - + My label My value @@ -38,10 +38,10 @@ it('Renders the correct elements', () => { expect(rows[1].children[1]).toHaveTextContent('My value 2'); }); -it('Applies numeric class if prop is passed', () => { +it('Applies numeric class if prop is passed row not inline', () => { render( - + My label My value @@ -51,12 +51,35 @@ it('Applies numeric class if prop is passed', () => { expect(screen.getByTestId('key-value-table')).toHaveClass( 'w-full border-collapse mb-8 [border-spacing:0] break-all' ); + + expect(screen.getByTestId('key-value-table-row')).toHaveClass( + ' flex gap-1 flex-wrap justify-between border-b first:border-t border-black dark:border-white flex-col items-start' + ); +}); + +it('Applies numeric class if prop is passed row inline', () => { + render( + + + My label + My value + + + ); + + expect(screen.getByTestId('key-value-table')).toHaveClass( + 'w-full border-collapse mb-8 [border-spacing:0] break-all' + ); + + expect(screen.getByTestId('key-value-table-row')).toHaveClass( + 'flex gap-1 flex-wrap justify-between border-b first:border-t border-black dark:border-white flex-row items-center' + ); }); it('Applies muted class if prop is passed', () => { render( - + My label My value diff --git a/libs/ui-toolkit/src/components/key-value-table/key-value-table.tsx b/libs/ui-toolkit/src/components/key-value-table/key-value-table.tsx index d1fb13f63..7df96b14f 100644 --- a/libs/ui-toolkit/src/components/key-value-table/key-value-table.tsx +++ b/libs/ui-toolkit/src/components/key-value-table/key-value-table.tsx @@ -62,6 +62,7 @@ export interface KeyValueTableRowProps className?: string; numerical?: boolean; // makes all values monospace muted?: boolean; + inline?: boolean; } export const KeyValueTableRow = ({ @@ -69,25 +70,28 @@ export const KeyValueTableRow = ({ className, muted, numerical, + inline = true, }: KeyValueTableRowProps) => { const dlClassName = classNames( - 'flex flex-wrap justify-between items-center border-b first:border-t border-black dark:border-white', + 'flex gap-1 flex-wrap justify-between border-b first:border-t border-black dark:border-white', + { 'flex-col items-start': !inline }, + { 'flex-row items-center': inline }, { 'border-black/60 dark:border-white/60 first:[border-top:none] last:[border-bottom:none]': muted, }, className ); - const dtClassName = `break-normal font-medium uppercase align-top p-4`; + const dtClassName = `break-words font-medium uppercase align-top p-4 capitalize`; const ddClassName = classNames( - 'align-top p-4 text-black/60 dark:text-white/60 break-normal', + 'align-top p-4 text-black/60 dark:text-white/60 break-words', { 'font-mono': numerical, } ); return ( -

+
{children[0]}
{children[1]}