feat(explorer): add headings and anchors to net params page (#4691)
This commit is contained in:
parent
4f610bbd1b
commit
2a0811863f
@ -1 +1,2 @@
|
|||||||
export * from './network-parameters';
|
export * from './network-parameters';
|
||||||
|
export * from './structure-network-params';
|
||||||
|
@ -1,82 +1,105 @@
|
|||||||
import { render, screen } from '@testing-library/react';
|
import { render, screen } from '@testing-library/react';
|
||||||
import type { NetworkParamsQuery } from '@vegaprotocol/network-parameters';
|
import type { NetworkParamsQuery } from '@vegaprotocol/network-parameters';
|
||||||
import { NetworkParametersTable } from './network-parameters';
|
import { NetworkParametersTable } from './network-parameters';
|
||||||
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
|
|
||||||
|
const renderComponent = (data: NetworkParamsQuery | undefined) => {
|
||||||
|
return render(
|
||||||
|
<MemoryRouter>
|
||||||
|
<NetworkParametersTable data={data} loading={false} />
|
||||||
|
</MemoryRouter>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockData = {
|
||||||
|
networkParametersConnection: {
|
||||||
|
edges: [
|
||||||
|
{
|
||||||
|
node: {
|
||||||
|
key: 'spam.protection.delegation.min.tokens',
|
||||||
|
value: '3',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
node: {
|
||||||
|
key: 'spam.protection.voting.min.tokens',
|
||||||
|
value: '1',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
node: {
|
||||||
|
key: 'reward.staking.delegation.minimumValidatorStake',
|
||||||
|
value: '2',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
node: {
|
||||||
|
key: 'reward.asset',
|
||||||
|
value:
|
||||||
|
'fc7fd956078fb1fc9db5c19b88f0874c4299b2a7639ad05a47a28c0aef291b55',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
describe('NetworkParametersTable', () => {
|
describe('NetworkParametersTable', () => {
|
||||||
it('renders correctly when it has network params', () => {
|
it('renders headers correctly', () => {
|
||||||
const data: NetworkParamsQuery = {
|
renderComponent(mockData);
|
||||||
networkParametersConnection: {
|
|
||||||
edges: [
|
const allHeadings = screen.getAllByRole('heading');
|
||||||
{
|
expect(
|
||||||
node: {
|
allHeadings.map((h) => {
|
||||||
__typename: 'NetworkParameter',
|
return {
|
||||||
key: 'market.liquidityProvision.minLpStakeQuantumMultiple',
|
text: h.textContent,
|
||||||
value: '1',
|
level: h.tagName,
|
||||||
},
|
testId: h.getAttribute('data-testid'),
|
||||||
},
|
|
||||||
{
|
|
||||||
node: {
|
|
||||||
__typename: 'NetworkParameter',
|
|
||||||
key: 'market.fee.factors.infrastructureFee',
|
|
||||||
value: '0.0005',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
render(<NetworkParametersTable data={data} loading={false} />);
|
})
|
||||||
expect(screen.getByTestId('network-param-header')).toHaveTextContent(
|
).toEqual([
|
||||||
'Network Parameters'
|
{
|
||||||
);
|
text: 'Network Parameters',
|
||||||
const rows = screen.getAllByTestId('key-value-table-row');
|
level: 'H1',
|
||||||
expect(rows[0].children[0]).toHaveTextContent(
|
testId: 'network-param-header',
|
||||||
'market.fee.factors.infrastructureFee'
|
},
|
||||||
);
|
{ text: 'Spam', level: 'H1', testId: 'spam' },
|
||||||
expect(rows[1].children[0]).toHaveTextContent(
|
{ text: 'Protection', level: 'H2', testId: 'spam-protection' },
|
||||||
'market.liquidityProvision.minLpStakeQuantumMultiple'
|
{ text: 'Delegation', level: 'H3', testId: 'spam-protection-delegation' },
|
||||||
);
|
{ text: 'Min', level: 'H4', testId: 'spam-protection-delegation-min' },
|
||||||
expect(rows[0].children[1]).toHaveTextContent('0.0005');
|
{ text: 'Voting', level: 'H3', testId: 'spam-protection-voting' },
|
||||||
expect(rows[1].children[1]).toHaveTextContent('1');
|
{ text: 'Min', level: 'H4', testId: 'spam-protection-voting-min' },
|
||||||
|
{ text: 'Reward', level: 'H1', testId: 'reward' },
|
||||||
|
{ text: 'Staking', level: 'H2', testId: 'reward-staking' },
|
||||||
|
{ text: 'Delegation', level: 'H3', testId: 'reward-staking-delegation' },
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders the rows in ascending order', () => {
|
it('renders network params correctly', () => {
|
||||||
const data: NetworkParamsQuery = {
|
renderComponent(mockData);
|
||||||
networkParametersConnection: {
|
|
||||||
edges: [
|
const delegationMinTokensRow = screen.getByTestId(
|
||||||
{
|
'spam-protection-delegation-min-tokens'
|
||||||
node: {
|
|
||||||
__typename: 'NetworkParameter',
|
|
||||||
key: 'market.fee.factors.infrastructureFee',
|
|
||||||
value: '0.0005',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
node: {
|
|
||||||
__typename: 'NetworkParameter',
|
|
||||||
key: 'market.liquidityProvision.minLpStakeQuantumMultiple',
|
|
||||||
value: '1',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
render(<NetworkParametersTable data={data} loading={false} />);
|
|
||||||
expect(screen.getByTestId('network-param-header')).toHaveTextContent(
|
|
||||||
'Network Parameters'
|
|
||||||
);
|
);
|
||||||
const rows = screen.getAllByTestId('key-value-table-row');
|
expect(delegationMinTokensRow).toHaveTextContent('0.000000000000000003');
|
||||||
expect(rows[0].children[0]).toHaveTextContent(
|
|
||||||
'market.fee.factors.infrastructureFee'
|
const votingMinTokensRow = screen.getByTestId(
|
||||||
|
'spam-protection-voting-min-tokens'
|
||||||
);
|
);
|
||||||
expect(rows[1].children[0]).toHaveTextContent(
|
expect(votingMinTokensRow).toHaveTextContent('0.000000000000000001');
|
||||||
'market.liquidityProvision.minLpStakeQuantumMultiple'
|
|
||||||
|
const minimumValidatorStakeRow = screen.getByTestId(
|
||||||
|
'reward-staking-delegation-minimumValidatorStake'
|
||||||
|
);
|
||||||
|
expect(minimumValidatorStakeRow).toHaveTextContent('2');
|
||||||
|
|
||||||
|
const assetRow = screen.getByTestId('reward-asset');
|
||||||
|
expect(assetRow).toHaveTextContent(
|
||||||
|
'fc7fd956078fb1fc9db5c19b88f0874c4299b2a7639ad05a47a28c0aef291b55'
|
||||||
);
|
);
|
||||||
expect(rows[0].children[1]).toHaveTextContent('0.0005');
|
|
||||||
expect(rows[1].children[1]).toHaveTextContent('1');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not render rows when is loading', () => {
|
it('does not render rows when is loading', () => {
|
||||||
render(<NetworkParametersTable data={undefined} loading={true} />);
|
renderComponent(undefined);
|
||||||
expect(screen.getByTestId('network-param-header')).toHaveTextContent(
|
expect(screen.getByTestId('network-param-header')).toHaveTextContent(
|
||||||
'Network Parameters'
|
'Network Parameters'
|
||||||
);
|
);
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
|
import startCase from 'lodash/startCase';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
AsyncRenderer,
|
AsyncRenderer,
|
||||||
KeyValueTable,
|
|
||||||
KeyValueTableRow,
|
KeyValueTableRow,
|
||||||
SyntaxHighlighter,
|
SyntaxHighlighter,
|
||||||
} from '@vegaprotocol/ui-toolkit';
|
} from '@vegaprotocol/ui-toolkit';
|
||||||
@ -12,11 +14,12 @@ import {
|
|||||||
} from '@vegaprotocol/utils';
|
} from '@vegaprotocol/utils';
|
||||||
import { t } from '@vegaprotocol/i18n';
|
import { t } from '@vegaprotocol/i18n';
|
||||||
import { RouteTitle } from '../../components/route-title';
|
import { RouteTitle } from '../../components/route-title';
|
||||||
import orderBy from 'lodash/orderBy';
|
|
||||||
import { useNetworkParamsQuery } from '@vegaprotocol/network-parameters';
|
import { useNetworkParamsQuery } from '@vegaprotocol/network-parameters';
|
||||||
import type { NetworkParamsQuery } from '@vegaprotocol/network-parameters';
|
|
||||||
import { useScrollToLocation } from '../../hooks/scroll-to-location';
|
import { useScrollToLocation } from '../../hooks/scroll-to-location';
|
||||||
import { useDocumentTitle } from '../../hooks/use-document-title';
|
import { useDocumentTitle } from '../../hooks/use-document-title';
|
||||||
|
import { structureNetworkParams } from './structure-network-params';
|
||||||
|
import type { NetworkParamsQuery } from '@vegaprotocol/network-parameters';
|
||||||
|
import type { GroupedParams } from './structure-network-params';
|
||||||
|
|
||||||
const PERCENTAGE_PARAMS = [
|
const PERCENTAGE_PARAMS = [
|
||||||
'governance.proposal.asset.requiredMajority',
|
'governance.proposal.asset.requiredMajority',
|
||||||
@ -58,6 +61,54 @@ const BIG_NUMBER_PARAMS = [
|
|||||||
'governance.proposal.updateAsset.minVoterBalance',
|
'governance.proposal.updateAsset.minVoterBalance',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const renderGroupedParams = (
|
||||||
|
group: GroupedParams,
|
||||||
|
level: number,
|
||||||
|
parentKeys: string[] = []
|
||||||
|
) => {
|
||||||
|
const Header = `h${level}` as keyof JSX.IntrinsicElements;
|
||||||
|
const headerStyles = classNames('uppercase font-semibold', {
|
||||||
|
'pt-6 text-3xl underline': level === 1,
|
||||||
|
'pt-3 text-2xl': level === 2,
|
||||||
|
'pt-2 text-lg': level === 3,
|
||||||
|
'pt-2 text-default': level === 4,
|
||||||
|
});
|
||||||
|
|
||||||
|
return Object.entries(group).map(([key, value]) => {
|
||||||
|
const fullPath = [...parentKeys, key].join('.');
|
||||||
|
const isLeafNode = typeof value !== 'object';
|
||||||
|
const id = parentKeys.concat([key]).join('-');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={key}>
|
||||||
|
{!isLeafNode && (
|
||||||
|
<div id={id}>
|
||||||
|
<Link to={`#${id}`}>
|
||||||
|
<Header className={headerStyles} data-testid={id}>
|
||||||
|
{startCase(key)}
|
||||||
|
</Header>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{isLeafNode ? (
|
||||||
|
typeof value === 'string' ? (
|
||||||
|
<div data-testid={id}>
|
||||||
|
<NetworkParameterRow
|
||||||
|
key={fullPath}
|
||||||
|
row={{ key: fullPath, value: value }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : null
|
||||||
|
) : (
|
||||||
|
<div className="pb-1">
|
||||||
|
{renderGroupedParams(value, level + 1, [...parentKeys, key])}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export const NetworkParameterRow = ({
|
export const NetworkParameterRow = ({
|
||||||
row: { key, value },
|
row: { key, value },
|
||||||
}: {
|
}: {
|
||||||
@ -77,7 +128,9 @@ export const NetworkParameterRow = ({
|
|||||||
>
|
>
|
||||||
{key}
|
{key}
|
||||||
{isSyntaxRow ? (
|
{isSyntaxRow ? (
|
||||||
|
<div className="pb-2">
|
||||||
<SyntaxHighlighter data={JSON.parse(value)} />
|
<SyntaxHighlighter data={JSON.parse(value)} />
|
||||||
|
</div>
|
||||||
) : isNaN(Number(value)) ? (
|
) : isNaN(Number(value)) ? (
|
||||||
value
|
value
|
||||||
) : BIG_NUMBER_PARAMS.includes(key) ? (
|
) : BIG_NUMBER_PARAMS.includes(key) ? (
|
||||||
@ -113,17 +166,12 @@ export const NetworkParametersTable = ({
|
|||||||
loading={loading}
|
loading={loading}
|
||||||
error={error}
|
error={error}
|
||||||
render={(data) => {
|
render={(data) => {
|
||||||
const ascParams = orderBy(
|
const flatParams = removePaginationWrapper(
|
||||||
removePaginationWrapper(data.networkParametersConnection.edges),
|
data.networkParametersConnection.edges
|
||||||
(param) => param.key,
|
|
||||||
'asc'
|
|
||||||
);
|
);
|
||||||
|
const groupedParams = structureNetworkParams(flatParams);
|
||||||
return (
|
return (
|
||||||
<KeyValueTable data-testid="parameters">
|
<div className="-mt-6">{renderGroupedParams(groupedParams, 1)}</div>
|
||||||
{(ascParams || []).map((row) => (
|
|
||||||
<NetworkParameterRow key={row.key} row={row} />
|
|
||||||
))}
|
|
||||||
</KeyValueTable>
|
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
@ -0,0 +1,109 @@
|
|||||||
|
import type { GroupedParams } from './structure-network-params';
|
||||||
|
import {
|
||||||
|
structureParams,
|
||||||
|
sortGroupedParams,
|
||||||
|
structureNetworkParams,
|
||||||
|
} from './structure-network-params';
|
||||||
|
|
||||||
|
describe('structureParams', () => {
|
||||||
|
it('should correctly structure params', () => {
|
||||||
|
const input = [
|
||||||
|
{ key: 'spam.protection.delegation.min.tokens', value: '10' },
|
||||||
|
{ key: 'spam.protection.voting.min.tokens', value: '5' },
|
||||||
|
];
|
||||||
|
const output: GroupedParams = {
|
||||||
|
spam: {
|
||||||
|
protection: {
|
||||||
|
delegation: {
|
||||||
|
min: {
|
||||||
|
tokens: '10',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
voting: {
|
||||||
|
min: {
|
||||||
|
tokens: '5',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
expect(structureParams(input)).toEqual(output);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle top-level keys correctly', () => {
|
||||||
|
const input = [{ key: 'levelOne', value: '10' }];
|
||||||
|
const output = {
|
||||||
|
levelOne: '10',
|
||||||
|
};
|
||||||
|
expect(structureParams(input)).toEqual(output);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('sortGroupedParams', () => {
|
||||||
|
it('should correctly sort grouped params', () => {
|
||||||
|
const input: GroupedParams = {
|
||||||
|
spam: {
|
||||||
|
protection: {
|
||||||
|
delegation: {
|
||||||
|
min: {
|
||||||
|
tokens: '10',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
reward: '50',
|
||||||
|
};
|
||||||
|
const output: GroupedParams = {
|
||||||
|
reward: '50',
|
||||||
|
spam: {
|
||||||
|
protection: {
|
||||||
|
delegation: {
|
||||||
|
min: {
|
||||||
|
tokens: '10',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
expect(sortGroupedParams(input)).toEqual(output);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle already sorted keys', () => {
|
||||||
|
const input = {
|
||||||
|
a: '10',
|
||||||
|
b: {
|
||||||
|
c: '5',
|
||||||
|
d: '6',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
expect(sortGroupedParams(input)).toEqual(input);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('structureNetworkParams', () => {
|
||||||
|
it('should structure and sort network params correctly', () => {
|
||||||
|
const input = [
|
||||||
|
{ key: 'spam.protection.delegation.min.tokens', value: '10' },
|
||||||
|
{ key: 'reward.asset', value: '50' },
|
||||||
|
];
|
||||||
|
const output: GroupedParams = {
|
||||||
|
reward: {
|
||||||
|
asset: '50',
|
||||||
|
},
|
||||||
|
spam: {
|
||||||
|
protection: {
|
||||||
|
delegation: {
|
||||||
|
min: {
|
||||||
|
tokens: '10',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
expect(structureNetworkParams(input)).toEqual(output);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return an empty object if no params are provided', () => {
|
||||||
|
expect(structureNetworkParams([])).toEqual({});
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,107 @@
|
|||||||
|
/**
|
||||||
|
* Categorizes and sorts an array of key-value pairs of network params into a nested object structure.
|
||||||
|
*
|
||||||
|
* The function takes an array of network params where keys are dot-delimited
|
||||||
|
* strings representing nested categories (e.g., 'spam.protection.delegation.min.tokens').
|
||||||
|
*
|
||||||
|
* Why this is necessary:
|
||||||
|
* A flat key-value structure wouldn't provide the hierarchical information needed.
|
||||||
|
* Organizing network parameters like this allows the rendering of nested headers
|
||||||
|
* and their corresponding network params.
|
||||||
|
*
|
||||||
|
* It also ensures that items with the minimum amount of nesting are ordered first. This
|
||||||
|
* allows us to render these items first, before more deeply nested items are rendered with
|
||||||
|
* subheaders. This creates a more intuitive UI.
|
||||||
|
*
|
||||||
|
* For example, given the input:
|
||||||
|
* [
|
||||||
|
* { key: 'spam.protection.delegation.min.tokens', value: '10' },
|
||||||
|
* { key: 'spam.protection.voting.min.tokens', value: '5' },
|
||||||
|
* { key: 'reward.staking.delegation.minimumValidatorStake', value: '2' }
|
||||||
|
* { key: 'reward.asset', value: 'fc7fd956078fb1fc9db5c19b88f0874c4299b2a7639ad05a47a28c0aef291b55' }
|
||||||
|
* ]
|
||||||
|
*
|
||||||
|
* The output will be:
|
||||||
|
* {
|
||||||
|
* spam: {
|
||||||
|
* protection: {
|
||||||
|
* delegation: {
|
||||||
|
* min: {
|
||||||
|
* tokens: '10'
|
||||||
|
* }
|
||||||
|
* },
|
||||||
|
* voting: {
|
||||||
|
* min: {
|
||||||
|
* tokens: '5'
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* },
|
||||||
|
* reward: {
|
||||||
|
* asset: 'fc7fd956078fb1fc9db5c19b88f0874c4299b2a7639ad05a47a28c0aef291b55',
|
||||||
|
* staking: {
|
||||||
|
* delegation: {
|
||||||
|
* minimumValidatorStake: '2'
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* @param {Array} params - An array of key-value pairs to categorize and sort.
|
||||||
|
* @returns {GroupedParams} - A nested object that groups the key-value pairs.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type GroupedParams = {
|
||||||
|
[key: string]: string | GroupedParams;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const structureParams = (
|
||||||
|
params: { key: string; value: string }[]
|
||||||
|
): GroupedParams => {
|
||||||
|
const grouped: GroupedParams = {};
|
||||||
|
|
||||||
|
params.forEach(({ key, value }) => {
|
||||||
|
const parts = key.split('.');
|
||||||
|
let node: GroupedParams = grouped;
|
||||||
|
|
||||||
|
parts.forEach((part, i) => {
|
||||||
|
if (typeof node[part] === 'undefined') {
|
||||||
|
node[part] = i === parts.length - 1 ? value : {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof node[part] === 'object') {
|
||||||
|
node = node[part] as GroupedParams;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return grouped;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const sortGroupedParams = (
|
||||||
|
groupedParams: GroupedParams
|
||||||
|
): GroupedParams => {
|
||||||
|
const sorted: GroupedParams = {};
|
||||||
|
|
||||||
|
// Sort top-level keys first
|
||||||
|
Object.entries(groupedParams).forEach(([key, value]) => {
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
sorted[key] = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.entries(groupedParams).forEach(([key, value]) => {
|
||||||
|
if (typeof value === 'object') {
|
||||||
|
sorted[key] = sortGroupedParams(value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return sorted;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const structureNetworkParams = (
|
||||||
|
params: { key: string; value: string }[]
|
||||||
|
) => {
|
||||||
|
const grouped = structureParams(params);
|
||||||
|
return sortGroupedParams(grouped);
|
||||||
|
};
|
Loading…
Reference in New Issue
Block a user