Compare commits

..
20 Commits
Author SHA1 Message Date
Bartłomiej Głownia 8c9d9fd078 chore: ignore parse source map warnings, fix trading readme 2023-07-04 16:30:21 +02:00
Joe Tsang ce3da1762b chore(governance): add e2e test for proposal filter changes (#4218) 2023-07-04 13:27:12 +01:00
John Walley 5040fbfd07 chore(trading): upgrade pennant to v1.9.0 (#4235) 2023-07-04 12:10:38 +00:00
Edd 74f2cfa4a5 feat(explorer): remove infinite tx loader (#4217) 2023-07-04 12:51:28 +01:00
Art 8c8fe6878a chore(wallet): remove links from the risk disclaimer (#4240) 2023-07-04 12:12:38 +01:00
Bartłomiej Głownia 6e0577aee4 chore(trading): migrate nx to latest version (#4196) 2023-07-04 13:01:25 +02:00
Edd 87b41a30d8 feat(explorer): add pegged order details (#4228) 2023-07-04 11:18:44 +01:00
Edd 0850f31855 fix(governance): reinstate deleted mock (#4232) 2023-07-03 15:27:40 +01:00
Bartłomiej Głownia 43aff8e359 chore(trading): ag-grid upgrade (#4187) 2023-07-01 13:02:23 +02:00
Bartłomiej Głownia 6e9e7c2a5c fix(positions): use market decimal places to format realized and unrealized PnL (#4225) 2023-06-30 14:58:01 +02:00
Mikołaj Młodzikowski f054f4c516 fix(ci): invalidation id without quotes 2023-06-30 11:24:16 +02:00
Mikołaj Młodzikowski f382078ee6 feat(ci): create cloudfront invalidation after deployment (#4223) 2023-06-30 10:46:25 +02:00
Matthew Russell ebc058bcbe chore(trading): vega icons for withrawals table and wallet dropdown (#4203) 2023-06-29 16:48:53 -07:00
Madalina Raicu d3df339696 chore(trading,governance,explorer): release update to vv0.20.19-core-0.71.6 2023-06-29 16:51:57 +03:00
Sam Keen a31008ea26 feat(governance): improve asset proposal details view (#4216) 2023-06-29 14:41:25 +01:00
Edd 5e93e98f07 fix(explorer): fix order amend tx view (#4197) 2023-06-29 12:25:42 +01:00
Ciaran McGhie bf3ff8fb6f fix(ui-toolkit): healthbar tooltips text colour (#4199) 2023-06-29 12:25:14 +01:00
Bartłomiej Głownia 16538ca3a3 feat(trading): show fills across all markets (#4210) 2023-06-29 12:24:51 +01:00
Edd 2fa00dacaa test(governance,markets): tidy up minor test quibbles (#4213) 2023-06-29 11:20:53 +00:00
Ben 45b7c2ad4d test(trading): 6004-CHAR-chart e2e tests (#4179) 2023-06-29 09:14:54 +01:00
387 changed files with 10486 additions and 10487 deletions
+1
View File
@@ -0,0 +1 @@
node_modules
+11 -6
View File
@@ -1,7 +1,7 @@
{
"root": true,
"ignorePatterns": ["**/*"],
"plugins": ["@nrwl/nx", "eslint-plugin-unicorn", "jsx-a11y", "jest"],
"plugins": ["@nx", "eslint-plugin-unicorn", "jsx-a11y", "jest"],
"settings": {
"jsx-a11y": {
"components": {
@@ -18,7 +18,7 @@
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"extends": ["plugin:jsx-a11y/strict"],
"rules": {
"@nrwl/nx/enforce-module-boundaries": [
"@nx/enforce-module-boundaries": [
"error",
{
"enforceBuildableLibDependency": true,
@@ -56,7 +56,7 @@
},
{
"files": ["*.ts", "*.tsx"],
"extends": ["plugin:@nrwl/nx/typescript"],
"extends": ["plugin:@nx/typescript"],
"rules": {
"@typescript-eslint/ban-ts-comment": [
"error",
@@ -80,14 +80,19 @@
},
{
"files": ["*.spec.ts", "*.spec.tsx"],
"extends": ["plugin:@nrwl/nx/typescript", "plugin:jest/recommended"],
"extends": ["plugin:@nx/typescript", "plugin:jest/recommended"],
"rules": {
"jest/consistent-test-it": ["error", { "fn": "it" }]
"jest/consistent-test-it": [
"error",
{
"fn": "it"
}
]
}
},
{
"files": ["*.js", "*.jsx"],
"extends": ["plugin:@nrwl/nx/javascript"],
"extends": ["plugin:@nx/javascript"],
"rules": {}
}
]
+20 -4
View File
@@ -113,19 +113,19 @@ jobs:
- name: Build local dist
run: |
flags=""
envCmd=""
if [[ ! -z "${{ env.ENV_NAME }}" ]]; then
flags="--env=${{ env.ENV_NAME }}"
envCmd="yarn env-cmd -f ./apps/${{ matrix.app }}/.env.${{ env.ENV_NAME }}"
fi
if [ "${{ matrix.app }}" = "trading" ]; then
yarn nx export trading $flags || (yarn install && yarn nx export trading $flags)
$envCmd yarn nx export trading || (yarn install && $envCmd yarn nx export trading)
DIST_LOCATION=dist/apps/trading/exported
elif [ "${{ matrix.app }}" = "ui-toolkit" ]; then
NODE_ENV=production yarn nx run ui-toolkit:build-storybook
DIST_LOCATION=dist/storybook/ui-toolkit
else
yarn nx build ${{ matrix.app }} $flags || (yarn install && yarn nx build ${{ matrix.app }} $flags)
$envCmd yarn nx build ${{ matrix.app }} || (yarn install && $envCmd yarn nx build ${{ matrix.app }})
DIST_LOCATION=dist/apps/${{ matrix.app }}
fi
mv $DIST_LOCATION dist-result
@@ -228,6 +228,22 @@ jobs:
AWS_REGION: 'eu-west-1'
SOURCE_DIR: 'dist-result'
- name: Install aws CLI
if: ${{ github.event_name == 'push' && ( matrix.app != 'trading' || (matrix.app == 'trading' && !( endsWith(github.ref, 'main') || endsWith(github.ref, 'release/testnet') ) ) ) }}
uses: unfor19/install-aws-cli-action@master
- name: Perform cache invalidation
if: ${{ github.event_name == 'push' && ( matrix.app != 'trading' || (matrix.app == 'trading' && !( endsWith(github.ref, 'main') || endsWith(github.ref, 'release/testnet') ) ) ) }}
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_REGION: 'eu-west-1'
run: |
echo "Looking for distribution for bucket: ${{ env.BUCKET_NAME }}"
id=$(aws cloudfront list-distributions | jq -Mrc '.DistributionList.Items | .[] | select(.DefaultCacheBehavior.TargetOriginId == "${{ env.BUCKET_NAME }}") | .Id')
echo "Found id is: ${id}"
aws cloudfront create-invalidation --distribution-id $id --paths "/*"
- name: Add preview label
uses: actions-ecosystem/action-add-labels@v1
if: ${{ github.event_name == 'pull_request' || github.event_name == 'pull_request_target' }}
-29
View File
@@ -1,29 +0,0 @@
module.exports = {
stories: [],
addons: [
'@storybook/addon-actions',
'@storybook/addon-viewport',
{
name: '@storybook/addon-docs',
options: {
configureJSX: true,
babelOptions: {},
sourceLoaderOptions: null,
transcludeMarkdown: true,
},
},
'@storybook/addon-controls',
'@storybook/addon-backgrounds',
'@storybook/addon-toolbars',
'@storybook/addon-measure',
'@storybook/addon-outline',
'@storybook/addon-a11y',
],
// uncomment the property below if you want to apply some webpack config globally
// webpackFinal: async (config, { configType }) => {
// // Make whatever fine-grained changes you need that should apply to all storybook configs
// // Return the altered config
// return config;
// },
};
-14
View File
@@ -1,14 +0,0 @@
{
"extends": "../tsconfig.base.json",
"exclude": [
"../**/*.spec.js",
"../**/*.test.js",
"../**/*.spec.ts",
"../**/*.test.ts",
"../**/*.spec.tsx",
"../**/*.test.tsx",
"../**/*.spec.jsx",
"../**/*.test.jsx"
],
"include": ["../**/*"]
}
+4 -3
View File
@@ -1,10 +1,11 @@
{
"name": "explorer-e2e",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "apps/explorer-e2e/src",
"projectType": "application",
"targets": {
"e2e": {
"executor": "@nrwl/cypress:cypress",
"executor": "@nx/cypress:cypress",
"options": {
"cypressConfig": "apps/explorer-e2e/cypress.config.js",
"devServerTarget": "explorer:serve"
@@ -16,14 +17,14 @@
}
},
"lint": {
"executor": "@nrwl/linter:eslint",
"executor": "@nx/linter:eslint",
"outputs": ["{options.outputFile}"],
"options": {
"lintFilePatterns": ["apps/explorer-e2e/**/*.{js,ts}"]
}
},
"build": {
"executor": "@nrwl/workspace:run-commands",
"executor": "nx:run-commands",
"outputs": [],
"options": {
"command": "yarn tsc --project ./apps/explorer-e2e/"
@@ -27,6 +27,11 @@ context('Home Page', function () {
16: 'Chain ID',
};
/**
* TODO(@nx/cypress): Nesting Cypress commands in a should assertion now throws.
* You should use .then() to chain commands instead.
* More Info: https://docs.cypress.io/guides/references/migration-guide#-should
**/
cy.get('[data-testid="stats-title"]')
.each(($list, index) => {
cy.wrap($list).should('contain.text', statTitles[index]);
@@ -34,6 +34,11 @@ context('Network parameters page', { tags: '@smoke' }, function () {
const parameterName = network_parameter[0];
const parameterValue = network_parameter[1];
if (this.networkParameterFormat.json.includes(parameterName)) {
/**
* TODO(@nx/cypress): Nesting Cypress commands in a should assertion now throws.
* You should use .then() to chain commands instead.
* More Info: https://docs.cypress.io/guides/references/migration-guide#-should
**/
cy.get(tableRows)
.contains(parameterName)
.should('be.visible')
@@ -65,6 +70,11 @@ context('Network parameters page', { tags: '@smoke' }, function () {
if (this.networkParameterFormat.percentage.includes(parameterName)) {
const formattedPercentageParameter =
(parseFloat(parameterValue) * 100).toFixed(0) + '%';
/**
* TODO(@nx/cypress): Nesting Cypress commands in a should assertion now throws.
* You should use .then() to chain commands instead.
* More Info: https://docs.cypress.io/guides/references/migration-guide#-should
**/
cy.get(tableRows)
.contains(parameterName)
.should('be.visible')
@@ -148,6 +158,11 @@ context('Network parameters page', { tags: '@smoke' }, function () {
cy.convert_number_to_max_four_decimal(parameterValue)
.add_commas_to_number_if_large_enough()
.then((parameterValueFormatted) => {
/**
* TODO(@nx/cypress): Nesting Cypress commands in a should assertion now throws.
* You should use .then() to chain commands instead.
* More Info: https://docs.cypress.io/guides/references/migration-guide#-should
**/
cy.get(tableRows)
.contains(parameterName)
.should('be.visible')
@@ -179,6 +194,11 @@ context('Network parameters page', { tags: '@smoke' }, function () {
cy.convert_number_to_max_eighteen_decimal(parameterValue)
.add_commas_to_number_if_large_enough()
.then((parameterValueFormatted) => {
/**
* TODO(@nx/cypress): Nesting Cypress commands in a should assertion now throws.
* You should use .then() to chain commands instead.
* More Info: https://docs.cypress.io/guides/references/migration-guide#-should
**/
cy.get(tableRows)
.contains(parameterName)
.should('be.visible')
@@ -170,6 +170,11 @@ context.skip('Parties page', { tags: '@regression' }, function () {
const sideMenuBackground = '.absolute';
// Engage dark mode if not allready set
/**
* TODO(@nx/cypress): Nesting Cypress commands in a should assertion now throws.
* You should use .then() to chain commands instead.
* More Info: https://docs.cypress.io/guides/references/migration-guide#-should
**/
cy.get(sideMenuBackground)
.should('have.css', 'background-color')
.then((background_color) => {
@@ -60,16 +60,31 @@ context.skip('Transactions page', function () {
});
cy.get('block').should('not.be.empty');
cy.get('encoded-tnx').should('not.be.empty');
/**
* TODO(@nx/cypress): Nesting Cypress commands in a should assertion now throws.
* You should use .then() to chain commands instead.
* More Info: https://docs.cypress.io/guides/references/migration-guide#-should
**/
cy.get('tx-type')
.should('not.be.empty')
.invoke('text')
.then((txTypeTxt) => {
if (txTypeTxt == 'Order Submission') {
/**
* TODO(@nx/cypress): Nesting Cypress commands in a should assertion now throws.
* You should use .then() to chain commands instead.
* More Info: https://docs.cypress.io/guides/references/migration-guide#-should
**/
cy.get('.hljs-attr')
.should('have.length.at.least', 8)
.each(($propertyName) => {
cy.wrap($propertyName).should('not.be.empty');
});
/**
* TODO(@nx/cypress): Nesting Cypress commands in a should assertion now throws.
* You should use .then() to chain commands instead.
* More Info: https://docs.cypress.io/guides/references/migration-guide#-should
**/
cy.get('.hljs-string')
.should('have.length.at.least', 8)
.each(($propertyValue) => {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"presets": [
[
"@nrwl/react/babel",
"@nx/react/babel",
{
"runtime": "automatic"
}
+3 -3
View File
@@ -1,14 +1,14 @@
NX_ETHEREUM_PROVIDER_URL=https://sepolia.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
NX_HOSTED_WALLET_URL=https://wallet.testnet.vega.xyz
NX_HOSTED_WALLET_URL=https://wallet.testnet.vega.rocks
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/stagnet1/vegawallet-stagnet1.toml
NX_VEGA_ENV=STAGNET1
NX_VEGA_EXPLORER_URL=https://explorer.stagnet1.vega.rocks
NX_VEGA_TOKEN_URL=https://governance.stagnet1.vega.rocks
NX_VEGA_WALLET_URL=http://localhost:1789
NX_TENDERMINT_URL=https://tm.n01.stagnet1.vega.xyz
NX_TENDERMINT_URL=https://tm.n01.stagnet1.vega.rocks
NX_TENDERMINT_WEBSOCKET_URL=wss://tm.n01.stagnet1.vega.xyz/websocket
NX_BLOCK_EXPLORER=https://be.stagnet1.vega.xyz/rest
NX_BLOCK_EXPLORER=https://be.stagnet1.vega.rocks/rest
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
NX_ORACLE_PROOFS_URL=https://raw.githubusercontent.com/vegaprotocol/well-known/main/__generated__/oracle-proofs.json
NX_VEGA_GOVERNANCE_URL=https://governance.stagnet1.vega.rocks
+2 -2
View File
@@ -1,6 +1,6 @@
{
"extends": ["plugin:@nrwl/nx/react", "../../.eslintrc.json"],
"ignorePatterns": ["!**/*", "__generated__", "__generated___"],
"extends": ["plugin:@nx/react", "../../.eslintrc.json"],
"ignorePatterns": ["!**/*", "__generated__"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
+1 -1
View File
@@ -39,7 +39,7 @@ Example configurations are provided here:
For convenience, you can boot the app injecting one of the configurations above by running:
```bash
yarn nx run explorer:serve --env={env} # e.g. stagnet1
yarn env-cmd -f .\apps\explorer\.env.{env} yarn nx run explorer:serve # e.g. stagnet1
```
There are a few different configuration options offered for this app:
+2 -2
View File
@@ -4,8 +4,8 @@ export default {
displayName: 'explorer',
preset: '../../jest.preset.js',
transform: {
'^(?!.*\\.(js|jsx|ts|tsx|css|json)$)': '@nrwl/react/plugins/jest',
'^.+\\.[tj]sx?$': 'babel-jest',
'^(?!.*\\.(js|jsx|ts|tsx|css|json)$)': '@nx/react/plugins/jest',
'^.+\\.[tj]sx?$': ['babel-jest', { presets: ['@nx/react/babel'] }],
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
coverageDirectory: '../../coverage/apps/explorer',
+15 -8
View File
@@ -1,10 +1,11 @@
{
"name": "explorer",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "apps/explorer/src",
"projectType": "application",
"targets": {
"build": {
"executor": "./tools/executors/webpack:build",
"executor": "@nx/webpack:webpack",
"outputs": ["{options.outputPath}"],
"defaultConfiguration": "production",
"options": {
@@ -38,7 +39,7 @@
}
},
"serve": {
"executor": "./tools/executors/webpack:serve",
"executor": "@nx/webpack:dev-server",
"options": {
"port": 3000,
"buildTarget": "explorer:build:development",
@@ -52,22 +53,28 @@
}
},
"lint": {
"executor": "@nrwl/linter:eslint",
"executor": "@nx/linter:eslint",
"outputs": ["{options.outputFile}"],
"options": {
"lintFilePatterns": ["apps/explorer/**/*.{ts,tsx,js,jsx}"]
}
},
"test": {
"executor": "@nrwl/jest:jest",
"outputs": ["coverage/apps/explorer"],
"executor": "@nx/jest:jest",
"outputs": ["{workspaceRoot}/coverage/apps/explorer"],
"options": {
"jestConfig": "apps/explorer/jest.config.ts",
"passWithNoTests": true
},
"configurations": {
"ci": {
"ci": true,
"codeCoverage": true
}
}
},
"generate-types": {
"executor": "@nrwl/workspace:run-commands",
"executor": "nx:run-commands",
"options": {
"commands": [
"npx openapi-typescript https://raw.githubusercontent.com/vegaprotocol/documentation/main/specs/v0.71.4/blockexplorer.openapi.json --output apps/explorer/src/types/explorer.d.ts --immutable-types"
@@ -75,7 +82,7 @@
}
},
"build-netlify": {
"executor": "@nrwl/workspace:run-commands",
"executor": "nx:run-commands",
"options": {
"commands": [
"cp apps/explorer/netlify.toml netlify.toml",
@@ -84,7 +91,7 @@
}
},
"build-spec": {
"executor": "@nrwl/workspace:run-commands",
"executor": "nx:run-commands",
"outputs": [],
"options": {
"command": "yarn tsc --project ./apps/explorer/tsconfig.spec.json"
@@ -1,15 +1,15 @@
import { useMemo } from 'react';
import type { AssetFieldsFragment } from '@vegaprotocol/assets';
import { AssetTypeMapping, AssetStatusMapping } from '@vegaprotocol/assets';
import { t } from '@vegaprotocol/i18n';
import { ButtonLink } from '@vegaprotocol/ui-toolkit';
import type { AgGridReact } from 'ag-grid-react';
import { AgGridColumn } from 'ag-grid-react';
import { AgGridLazy as AgGrid } from '@vegaprotocol/datagrid';
import type { VegaICellRendererParams } from '@vegaprotocol/datagrid';
import { useRef, useLayoutEffect } from 'react';
import { BREAKPOINT_MD } from '../../config/breakpoints';
import { useNavigate } from 'react-router-dom';
import type { RowClickedEvent } from 'ag-grid-community';
import type { RowClickedEvent, ColDef } from 'ag-grid-community';
type AssetsTableProps = {
data: AssetFieldsFragment[] | null;
@@ -31,6 +31,58 @@ export const AssetsTable = ({ data }: AssetsTableProps) => {
};
}, []);
const columnDefs = useMemo<ColDef[]>(
() => [
{ headerName: t('Symbol'), field: 'symbol' },
{ headerName: t('Name'), field: 'name' },
{
flex: 2,
headerName: t('ID'),
field: 'id',
hide: window.innerWidth < BREAKPOINT_MD,
},
{
colId: 'type',
headerName: t('Type'),
field: 'source.__typename',
hide: window.innerWidth < BREAKPOINT_MD,
valueFormatter: ({ value }: { value?: string }) =>
value ? AssetTypeMapping[value].value : '',
},
{
headerName: t('Status'),
field: 'status',
hide: window.innerWidth < BREAKPOINT_MD,
valueFormatter: ({ value }: { value?: string }) =>
value ? AssetStatusMapping[value].value : '',
},
{
colId: 'actions',
headerName: '',
sortable: false,
filter: false,
resizable: false,
wrapText: true,
field: 'id',
cellRenderer: ({
value,
}: VegaICellRendererParams<AssetFieldsFragment, 'id'>) =>
value ? (
<ButtonLink
onClick={(e) => {
navigate(value);
}}
>
{t('View details')}
</ButtonLink>
) : (
''
),
},
],
[navigate]
);
return (
<AgGrid
ref={ref}
@@ -46,60 +98,11 @@ export const AssetsTable = ({ data }: AssetsTableProps) => {
filterParams: { buttons: ['reset'] },
autoHeight: true,
}}
columnDefs={columnDefs}
suppressCellFocus={true}
onRowClicked={({ data }: RowClickedEvent) => {
navigate(data.id);
}}
>
<AgGridColumn headerName={t('Symbol')} field="symbol" />
<AgGridColumn headerName={t('Name')} field="name" />
<AgGridColumn
flex="2"
headerName={t('ID')}
field="id"
hide={window.innerWidth < BREAKPOINT_MD}
/>
<AgGridColumn
colId="type"
headerName={t('Type')}
field="source.__typename"
hide={window.innerWidth < BREAKPOINT_MD}
valueFormatter={({ value }: { value?: string }) =>
value && AssetTypeMapping[value].value
}
/>
<AgGridColumn
headerName={t('Status')}
field="status"
hide={window.innerWidth < BREAKPOINT_MD}
valueFormatter={({ value }: { value?: string }) =>
value && AssetStatusMapping[value].value
}
/>
<AgGridColumn
colId="actions"
headerName=""
sortable={false}
filter={false}
resizable={false}
wrapText={true}
field="id"
cellRenderer={({
value,
}: VegaICellRendererParams<AssetFieldsFragment, 'id'>) =>
value ? (
<ButtonLink
onClick={(e) => {
navigate(value);
}}
>
{t('View details')}
</ButtonLink>
) : (
''
)
}
/>
</AgGrid>
/>
);
};
@@ -1,5 +1,4 @@
import { Icon, Tooltip } from '@vegaprotocol/ui-toolkit';
import React from 'react';
export interface InfoBlockProps {
title: string;
@@ -1,33 +1,98 @@
import { render } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import PartyLink from './party-link';
import { MockedProvider } from '@apollo/client/testing';
import { ExplorerNodeNamesDocument } from '../../../routes/validators/__generated__/NodeNames';
import { act } from 'react-dom/test-utils';
const zeroes =
'0000000000000000000000000000000000000000000000000000000000000000';
const mocks = [
{
request: {
query: ExplorerNodeNamesDocument,
},
result: {
data: {
nodesConnection: {
edges: [
{
node: {
id: '1',
name: 'Validator Node',
pubkey:
'13464e35bcb8e8a2900ca0f87acaf252d50cf2ab2fc73694845a16b7c8a0dc6e',
tmPubkey: 'tmPubkey1',
ethereumAddress: '0x123456789',
},
},
{
node: {
id: '2',
name: 'Node 2',
pubkey: 'pubkey2',
tmPubkey: 'tmPubkey2',
ethereumAddress: '0xabcdef123',
},
},
],
},
},
},
},
];
describe('PartyLink', () => {
it('renders Network for 000.000 party', () => {
const zeroes =
'0000000000000000000000000000000000000000000000000000000000000000';
const screen = render(<PartyLink id={zeroes} />);
const screen = render(
<MockedProvider>
<PartyLink id={zeroes} />
</MockedProvider>
);
expect(screen.getByText('Network')).toBeInTheDocument();
});
it('renders Network for network party', () => {
const screen = render(<PartyLink id="network" />);
const screen = render(
<MockedProvider>
<PartyLink id="network" />
</MockedProvider>
);
expect(screen.getByText('Network')).toBeInTheDocument();
});
it('renders ID with no link for invalid party', () => {
const screen = render(<PartyLink id="this-party-is-not-valid" />);
const screen = render(
<MockedProvider>
<PartyLink id="this-party-is-not-valid" />
</MockedProvider>
);
expect(screen.getByTestId('invalid-party')).toBeInTheDocument();
});
it('if the key is a validator, render their name instead', async () => {
const screen = render(
<MockedProvider mocks={mocks}>
<MemoryRouter>
<PartyLink id="13464e35bcb8e8a2900ca0f87acaf252d50cf2ab2fc73694845a16b7c8a0dc6e" />
</MemoryRouter>
</MockedProvider>
);
// Wait for hook to update with mock data
await act(() => new Promise((resolve) => setTimeout(resolve, 0)));
await expect(screen.getByText('Validator Node')).toBeInTheDocument();
});
it('links a valid party to the party page', () => {
const aValidParty =
'13464e35bcb8e8a2900ca0f87acaf252d50cf2ab2fc73694845a16b7c8a0dc6e';
const screen = render(
<MemoryRouter>
<PartyLink id={aValidParty} />
</MemoryRouter>
<MockedProvider>
<MemoryRouter>
<PartyLink id={aValidParty} />
</MemoryRouter>
</MockedProvider>
);
const el = screen.getByText(aValidParty);
@@ -1,22 +1,44 @@
import { Routes } from '../../../routes/route-names';
import { Link } from 'react-router-dom';
import type { ComponentProps } from 'react';
import { useMemo, type ComponentProps } from 'react';
import Hash from '../hash';
import { t } from '@vegaprotocol/i18n';
import { isValidPartyId } from '../../../routes/parties/id/components/party-id-error';
import { truncateMiddle } from '@vegaprotocol/ui-toolkit';
import { Icon, truncateMiddle } from '@vegaprotocol/ui-toolkit';
import { useExplorerNodeNamesQuery } from '../../../routes/validators/__generated__/NodeNames';
import type { ExplorerNodeNamesQuery } from '../../../routes/validators/__generated__/NodeNames';
export const SPECIAL_CASE_NETWORK_ID =
'0000000000000000000000000000000000000000000000000000000000000000';
export const SPECIAL_CASE_NETWORK = 'network';
export function getNameForParty(id: string, data?: ExplorerNodeNamesQuery) {
if (!data || data?.nodesConnection?.edges?.length === 0) {
return id;
}
const validator = data.nodesConnection.edges?.find((e) => {
return e?.node.pubkey === id;
});
if (validator) {
return validator.node.name;
}
return id;
}
export type PartyLinkProps = Partial<ComponentProps<typeof Link>> & {
id: string;
truncate?: boolean;
};
const PartyLink = ({ id, truncate = false, ...props }: PartyLinkProps) => {
const { data } = useExplorerNodeNamesQuery();
const name = useMemo(() => getNameForParty(id, data), [data, id]);
const useName = name !== id;
// Some transactions will involve the 'network' party, which is alias for '000...000'
// The party page does not handle this nicely, so in this case we render the word 'Network'
if (id === SPECIAL_CASE_NETWORK || id === SPECIAL_CASE_NETWORK_ID) {
@@ -38,13 +60,20 @@ const PartyLink = ({ id, truncate = false, ...props }: PartyLinkProps) => {
}
return (
<Link
className="underline font-mono"
{...props}
to={`/${Routes.PARTIES}/${id}`}
>
<Hash text={truncate ? truncateMiddle(id) : id} />
</Link>
<span className="whitespace-nowrap">
{useName && <Icon size={4} name="cube" className="mr-2" />}
<Link
className="underline font-mono"
{...props}
to={`/${Routes.PARTIES}/${id}`}
>
{useName ? (
name
) : (
<Hash text={truncate ? truncateMiddle(id, 4, 4) : id} />
)}
</Link>
</span>
);
};
@@ -1,8 +1,9 @@
import { useMemo } from 'react';
import type { MarketFieldsFragment } from '@vegaprotocol/markets';
import { t } from '@vegaprotocol/i18n';
import { ButtonLink } from '@vegaprotocol/ui-toolkit';
import type { AgGridReact } from 'ag-grid-react';
import { AgGridColumn } from 'ag-grid-react';
import type { ColDef } from 'ag-grid-community';
import { AgGridLazy as AgGrid } from '@vegaprotocol/datagrid';
import type {
VegaICellRendererParams,
@@ -39,54 +40,34 @@ export const MarketsTable = ({ data }: MarketsTableProps) => {
};
}, []);
return (
<AgGrid
ref={gridRef}
rowData={data}
getRowId={({ data }: { data: MarketFieldsFragment }) => data.id}
overlayNoRowsTemplate={t('This chain has no markets')}
domLayout="autoHeight"
defaultColDef={{
flex: 1,
resizable: true,
sortable: true,
filter: true,
filterParams: { buttons: ['reset'] },
autoHeight: true,
}}
suppressCellFocus={true}
onRowClicked={({ data, event }: RowClickedEvent) => {
if ((event?.target as HTMLElement).tagName.toUpperCase() !== 'BUTTON') {
navigate(data.id);
}
}}
>
<AgGridColumn
colId="code"
headerName={t('Code')}
field="tradableInstrument.instrument.code"
/>
<AgGridColumn
colId="name"
headerName={t('Name')}
field="tradableInstrument.instrument.name"
/>
<AgGridColumn
headerName={t('Status')}
field="state"
hide={window.innerWidth <= BREAKPOINT_MD}
valueGetter={({
const columnDefs = useMemo<ColDef[]>(
() => [
{
colId: 'code',
headerName: t('Code'),
field: 'tradableInstrument.instrument.code',
},
{
colId: 'name',
headerName: t('Name'),
field: 'tradableInstrument.instrument.name',
},
{
headerName: t('Status'),
field: 'state',
hide: window.innerWidth <= BREAKPOINT_MD,
valueGetter: ({
data,
}: VegaValueGetterParams<MarketFieldsFragment>) => {
return data?.state ? MarketStateMapping[data?.state] : '-';
}}
/>
<AgGridColumn
colId="asset"
headerName={t('Settlement asset')}
field="tradableInstrument.instrument.product.settlementAsset.symbol"
hide={window.innerWidth <= BREAKPOINT_MD}
cellRenderer={({
},
},
{
colId: 'asset',
headerName: t('Settlement asset'),
field: 'tradableInstrument.instrument.product.settlementAsset.symbol',
hide: window.innerWidth <= BREAKPOINT_MD,
cellRenderer: ({
data,
}: VegaICellRendererParams<
MarketFieldsFragment,
@@ -105,19 +86,19 @@ export const MarketsTable = ({ data }: MarketsTableProps) => {
) : (
''
);
}}
/>
<AgGridColumn
flex={2}
headerName={t('Market ID')}
field="id"
hide={window.innerWidth <= BREAKPOINT_MD}
/>
<AgGridColumn
colId="actions"
headerName=""
field="id"
cellRenderer={({
},
},
{
flex: 2,
headerName: t('Market ID'),
field: 'id',
hide: window.innerWidth <= BREAKPOINT_MD,
},
{
colId: 'actions',
headerName: '',
field: 'id',
cellRenderer: ({
value,
}: VegaICellRendererParams<MarketFieldsFragment, 'id'>) =>
value ? (
@@ -126,9 +107,34 @@ export const MarketsTable = ({ data }: MarketsTableProps) => {
</Link>
) : (
''
)
),
},
],
[openAssetDetailsDialog]
);
return (
<AgGrid
ref={gridRef}
rowData={data}
getRowId={({ data }: { data: MarketFieldsFragment }) => data.id}
overlayNoRowsTemplate={t('This chain has no markets')}
domLayout="autoHeight"
defaultColDef={{
flex: 1,
resizable: true,
sortable: true,
filter: true,
filterParams: { buttons: ['reset'] },
autoHeight: true,
}}
columnDefs={columnDefs}
suppressCellFocus={true}
onRowClicked={({ data, event }: RowClickedEvent) => {
if ((event?.target as HTMLElement).tagName.toUpperCase() !== 'BUTTON') {
navigate(data.id);
}
/>
</AgGrid>
}}
/>
);
};
@@ -13,6 +13,10 @@ fragment ExplorerDeterministicOrderFields on Order {
remaining
size
rejectionReason
peggedOrder {
reference
offset
}
party {
id
}
@@ -3,7 +3,7 @@ import * as Types from '@vegaprotocol/types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
const defaultOptions = {} as const;
export type ExplorerDeterministicOrderFieldsFragment = { __typename?: 'Order', id: string, type?: Types.OrderType | null, reference: string, status: Types.OrderStatus, version: string, createdAt: any, updatedAt?: any | null, expiresAt?: any | null, timeInForce: Types.OrderTimeInForce, price: string, side: Types.Side, remaining: string, size: string, rejectionReason?: Types.OrderRejectionReason | null, party: { __typename?: 'Party', id: string }, market: { __typename?: 'Market', id: string, decimalPlaces: number, positionDecimalPlaces: number, state: Types.MarketState, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', name: string, product: { __typename?: 'Future', quoteName: string } } } } };
export type ExplorerDeterministicOrderFieldsFragment = { __typename?: 'Order', id: string, type?: Types.OrderType | null, reference: string, status: Types.OrderStatus, version: string, createdAt: any, updatedAt?: any | null, expiresAt?: any | null, timeInForce: Types.OrderTimeInForce, price: string, side: Types.Side, remaining: string, size: string, rejectionReason?: Types.OrderRejectionReason | null, peggedOrder?: { __typename?: 'PeggedOrder', reference: Types.PeggedReference, offset: string } | null, party: { __typename?: 'Party', id: string }, market: { __typename?: 'Market', id: string, decimalPlaces: number, positionDecimalPlaces: number, state: Types.MarketState, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', name: string, product: { __typename?: 'Future', quoteName: string } } } } };
export type ExplorerDeterministicOrderQueryVariables = Types.Exact<{
orderId: Types.Scalars['ID'];
@@ -11,7 +11,7 @@ export type ExplorerDeterministicOrderQueryVariables = Types.Exact<{
}>;
export type ExplorerDeterministicOrderQuery = { __typename?: 'Query', orderByID: { __typename?: 'Order', id: string, type?: Types.OrderType | null, reference: string, status: Types.OrderStatus, version: string, createdAt: any, updatedAt?: any | null, expiresAt?: any | null, timeInForce: Types.OrderTimeInForce, price: string, side: Types.Side, remaining: string, size: string, rejectionReason?: Types.OrderRejectionReason | null, party: { __typename?: 'Party', id: string }, market: { __typename?: 'Market', id: string, decimalPlaces: number, positionDecimalPlaces: number, state: Types.MarketState, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', name: string, product: { __typename?: 'Future', quoteName: string } } } } } };
export type ExplorerDeterministicOrderQuery = { __typename?: 'Query', orderByID: { __typename?: 'Order', id: string, type?: Types.OrderType | null, reference: string, status: Types.OrderStatus, version: string, createdAt: any, updatedAt?: any | null, expiresAt?: any | null, timeInForce: Types.OrderTimeInForce, price: string, side: Types.Side, remaining: string, size: string, rejectionReason?: Types.OrderRejectionReason | null, peggedOrder?: { __typename?: 'PeggedOrder', reference: Types.PeggedReference, offset: string } | null, party: { __typename?: 'Party', id: string }, market: { __typename?: 'Market', id: string, decimalPlaces: number, positionDecimalPlaces: number, state: Types.MarketState, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', name: string, product: { __typename?: 'Future', quoteName: string } } } } } };
export const ExplorerDeterministicOrderFieldsFragmentDoc = gql`
fragment ExplorerDeterministicOrderFields on Order {
@@ -29,6 +29,10 @@ export const ExplorerDeterministicOrderFieldsFragmentDoc = gql`
remaining
size
rejectionReason
peggedOrder {
reference
offset
}
party {
id
}
@@ -12,7 +12,7 @@ type Amend = components['schemas']['v1OrderAmendment'];
function renderAmendOrderDetails(
id: string,
version: number,
version: number | undefined,
amend: Amend,
mocks: MockedResponse[]
) {
@@ -25,7 +25,11 @@ function renderAmendOrderDetails(
);
}
function renderExistingAmend(id: string, version: number, amend: Amend) {
function renderExistingAmend(
id: string,
version: number | undefined,
amend: Amend
) {
const mocks = [
{
request: {
@@ -49,6 +53,7 @@ function renderExistingAmend(id: string, version: number, amend: Amend) {
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GTC,
price: '200',
side: 'BUY',
peggedOrder: null,
remaining: '99',
rejectionReason: 'rejection',
reference: '123',
@@ -77,6 +82,56 @@ function renderExistingAmend(id: string, version: number, amend: Amend) {
},
},
},
{
request: {
query: ExplorerDeterministicOrderDocument,
variables: {
orderId: '123',
},
},
result: {
data: {
orderByID: {
__typename: 'Order',
id: '123',
type: 'GTT',
status: Schema.OrderStatus.STATUS_ACTIVE,
version: 100,
createdAt: '123',
updatedAt: '456',
expiresAt: '789',
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GTC,
peggedOrder: null,
price: '200',
side: 'BUY',
remaining: '99',
rejectionReason: 'rejection',
reference: '123',
size: '200',
party: {
__typename: 'Party',
id: '234',
},
market: {
__typename: 'Market',
id: 'amend-to-order-latest-version',
state: 'STATUS_ACTIVE',
positionDecimalPlaces: 2,
decimalPlaces: '5',
tradableInstrument: {
instrument: {
name: 'amend-to-order-latest-version-test',
product: {
__typename: 'Future',
quoteName: '123',
},
},
},
},
},
},
},
},
{
request: {
query: ExplorerMarketDocument,
@@ -157,4 +212,15 @@ describe('Amend order details', () => {
expect(await res.findByText('New price')).toBeInTheDocument();
expect(await res.findByText('-7879')).toBeInTheDocument();
});
it('Fetches latest version when version is not specified', async () => {
const amend: Amend = {
price: '-7879',
};
const res = renderExistingAmend('123', undefined, amend);
expect(
await res.findByText('amend-to-order-latest-version')
).toBeInTheDocument();
});
});
@@ -12,7 +12,7 @@ import { wrapperClasses } from './deterministic-order-details';
export interface AmendOrderDetailsProps {
id: string;
amend: components['schemas']['v1OrderAmendment'];
// Version to fetch, with 0 being 'latest' and 1 being 'first'. Defaults to 0
// Version to fetch. Latest is provided by default
version?: number;
}
@@ -34,13 +34,11 @@ export function getSideDeltaColour(delta: string): string {
* @param param0
* @returns
*/
const AmendOrderDetails = ({
id,
version = 0,
amend,
}: AmendOrderDetailsProps) => {
const AmendOrderDetails = ({ id, version, amend }: AmendOrderDetailsProps) => {
const variables = version ? { orderId: id, version } : { orderId: id };
const { data, error } = useExplorerDeterministicOrderQuery({
variables: { orderId: id, version },
variables,
});
if (error || (data && !data.orderByID)) {
@@ -5,6 +5,7 @@ import PriceInMarket from '../price-in-market/price-in-market';
import { Time } from '../time';
import { sideText, statusText, tifFull, tifShort } from './lib/order-labels';
import SizeInMarket from '../size-in-market/size-in-market';
import { TxOrderPeggedReference } from '../txs/details/order/tx-order-peg';
export interface DeterministicOrderDetailsProps {
id: string;
@@ -68,25 +69,35 @@ const DeterministicOrderDetails = ({
<span className="mx-5 text-base">@</span>
<PriceInMarket price={o.price} marketId={o.market.id} />
</h2>
<p className="text-gray-500 mb-4">
<p className="text-gray-200">
In <MarketLink id={o.market.id} /> at <Time date={o.createdAt} />.
</p>
{o.peggedOrder ? (
<p className="text-gray-200">
{t('Price peg')}:{' '}
<TxOrderPeggedReference
side={o.side}
reference={o.peggedOrder.reference}
offset={o.peggedOrder.offset}
marketId={o.market.id}
/>
</p>
) : null}
{o.reference ? (
<p className="text-gray-500 mb-4">
<p className="text-gray-500 mt-4">
<span>{t('Reference')}</span>: {o.reference}
</p>
) : null}
<div className="grid md:grid-cols-4 gap-x-6">
{version !== 0 ? null : (
<div className="mb-12 md:mb-0">
<h2 className="text-2xl font-bold text-dark mb-4">
{t('Status')}
</h2>
<h5 className="text-lg font-medium text-gray-500 mb-0 capitalize">
{statusText[o.status]}
</h5>
</div>
)}
<div className="grid md:grid-cols-4 gap-x-6 mt-4">
<div className="mb-12 md:mb-0">
<h2 className="text-2xl font-bold text-dark mb-4">
{t('Status')}
</h2>
<h5 className="text-lg font-medium text-gray-500 mb-0 capitalize">
{statusText[o.status]}
</h5>
</div>
<div className="mb-12 md:mb-0">
<h2 className="text-2xl font-bold text-dark mb-4">{t('Size')}</h2>
@@ -95,17 +106,6 @@ const DeterministicOrderDetails = ({
</h5>
</div>
{version !== 0 ? null : (
<div className="">
<h2 className="text-2xl font-bold text-dark mb-4">
{t('Remaining')}
</h2>
<h5 className="text-lg font-medium text-gray-500 mb-0">
<SizeInMarket size={o.remaining} marketId={o.market.id} />
</h5>
</div>
)}
<div className="">
<h2 className="text-2xl font-bold text-dark mb-4">
{t('Version')}
@@ -31,6 +31,7 @@ const mock = {
side: 'SIDE_BUY',
remaining: '100',
size: '100',
peggedOrder: null,
party: {
id: '456',
},
@@ -1,7 +1,6 @@
import type { ProposalListFieldsFragment } from '@vegaprotocol/proposals';
import { VoteProgress } from '@vegaprotocol/proposals';
import type { AgGridReact } from 'ag-grid-react';
import { AgGridColumn } from 'ag-grid-react';
import { ExternalLink } from '@vegaprotocol/ui-toolkit';
import { AgGridLazy as AgGrid } from '@vegaprotocol/datagrid';
import type {
@@ -9,7 +8,7 @@ import type {
VegaValueFormatterParams,
} from '@vegaprotocol/datagrid';
import { useLayoutEffect, useMemo, useRef, useState } from 'react';
import type { RowClickedEvent } from 'ag-grid-community';
import type { RowClickedEvent, ColDef } from 'ag-grid-community';
import { getDateTimeFormat } from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import {
@@ -64,7 +63,128 @@ export const ProposalsTable = ({ data }: ProposalsTableProps) => {
title: '',
content: null,
});
const columnDefs = useMemo<ColDef[]>(
() => [
{
colId: 'title',
headerName: t('Title'),
field: 'rationale.title',
flex: 2,
wrapText: true,
},
{
colId: 'type',
maxWidth: 180,
hide: window.innerWidth <= BREAKPOINT_MD,
headerName: t('Type'),
field: 'terms.change.__typename',
},
{
maxWidth: 100,
headerName: t('State'),
field: 'state',
valueFormatter: ({
value,
}: VegaValueFormatterParams<ProposalListFieldsFragment, 'state'>) => {
return value ? ProposalStateMapping[value] : '-';
},
},
{
colId: 'voting',
maxWidth: 100,
hide: window.innerWidth <= BREAKPOINT_MD,
headerName: t('Voting'),
cellRenderer: ({
data,
}: VegaICellRendererParams<ProposalListFieldsFragment>) => {
if (data) {
const yesTokens = new BigNumber(data.votes.yes.totalTokens);
const noTokens = new BigNumber(data.votes.no.totalTokens);
const totalTokensVoted = yesTokens.plus(noTokens);
const yesPercentage = totalTokensVoted.isZero()
? new BigNumber(0)
: yesTokens.multipliedBy(100).dividedBy(totalTokensVoted);
return (
<div className="uppercase flex h-full items-center justify-center pt-2">
<VoteProgress
threshold={requiredMajorityPercentage}
progress={yesPercentage}
/>
</div>
);
}
return '-';
},
},
{
colId: 'cDate',
maxWidth: 150,
hide: window.innerWidth <= BREAKPOINT_MD,
headerName: t('Closing date'),
field: 'terms.closingDatetime',
valueFormatter: ({
value,
}: VegaValueFormatterParams<
ProposalListFieldsFragment,
'terms.closingDatetime'
>) => {
return value ? getDateTimeFormat().format(new Date(value)) : '-';
},
},
{
colId: 'eDate',
maxWidth: 150,
hide: window.innerWidth <= BREAKPOINT_MD,
headerName: t('Enactment date'),
field: 'terms.enactmentDatetime',
valueFormatte: ({
value,
}: VegaValueFormatterParams<
ProposalListFieldsFragment,
'terms.enactmentDatetime'
>) => {
return value ? getDateTimeFormat().format(new Date(value)) : '-';
},
},
{
colId: 'actions',
minWidth: window.innerWidth > BREAKPOINT_MD ? 221 : 80,
maxWidth: 221,
sortable: false,
filter: false,
resizable: false,
cellRenderer: ({
data,
}: VegaICellRendererParams<ProposalListFieldsFragment>) => {
const proposalPage = tokenLink(
TOKEN_PROPOSAL.replace(':id', data?.id || '')
);
const openDialog = () => {
if (!data) return;
setDialog({
open: true,
title: data.rationale.title,
content: data.terms,
});
};
return (
<div className="pb-1">
<button className="underline max-md:hidden" onClick={openDialog}>
{t('View terms')}
</button>{' '}
<ExternalLink className="max-md:hidden" href={proposalPage}>
{t('Open in Governance')}
</ExternalLink>
<ExternalLink className="md:hidden" href={proposalPage}>
{t('Open')}
</ExternalLink>
</div>
);
},
},
],
[requiredMajorityPercentage, tokenLink]
);
return (
<>
<AgGrid
@@ -83,6 +203,7 @@ export const ProposalsTable = ({ data }: ProposalsTableProps) => {
filterParams: { buttons: ['reset'] },
autoHeight: true,
}}
columnDefs={columnDefs}
suppressCellFocus={true}
onRowClicked={({ data, event }: RowClickedEvent) => {
if (
@@ -94,128 +215,7 @@ export const ProposalsTable = ({ data }: ProposalsTableProps) => {
window.open(proposalPage, '_blank');
}
}}
>
<AgGridColumn
colId="title"
headerName={t('Title')}
field="rationale.title"
flex={2}
wrapText={true}
/>
<AgGridColumn
colId="type"
maxWidth={180}
hide={window.innerWidth <= BREAKPOINT_MD}
headerName={t('Type')}
field="terms.change.__typename"
/>
<AgGridColumn
maxWidth={100}
headerName={t('State')}
field="state"
valueFormatter={({
value,
}: VegaValueFormatterParams<ProposalListFieldsFragment, 'state'>) => {
return value ? ProposalStateMapping[value] : '-';
}}
/>
<AgGridColumn
colId="voting"
maxWidth={100}
hide={window.innerWidth <= BREAKPOINT_MD}
headerName={t('Voting')}
cellRenderer={({
data,
}: VegaICellRendererParams<ProposalListFieldsFragment>) => {
if (data) {
const yesTokens = new BigNumber(data.votes.yes.totalTokens);
const noTokens = new BigNumber(data.votes.no.totalTokens);
const totalTokensVoted = yesTokens.plus(noTokens);
const yesPercentage = totalTokensVoted.isZero()
? new BigNumber(0)
: yesTokens.multipliedBy(100).dividedBy(totalTokensVoted);
return (
<div className="uppercase flex h-full items-center justify-center pt-2">
<VoteProgress
threshold={requiredMajorityPercentage}
progress={yesPercentage}
/>
</div>
);
}
return '-';
}}
/>
<AgGridColumn
colId="cDate"
maxWidth={150}
hide={window.innerWidth <= BREAKPOINT_MD}
headerName={t('Closing date')}
field="terms.closingDatetime"
valueFormatter={({
value,
}: VegaValueFormatterParams<
ProposalListFieldsFragment,
'terms.closingDatetime'
>) => {
return value ? getDateTimeFormat().format(new Date(value)) : '-';
}}
/>
<AgGridColumn
colId="eDate"
maxWidth={150}
hide={window.innerWidth <= BREAKPOINT_MD}
headerName={t('Enactment date')}
field="terms.enactmentDatetime"
valueFormatter={({
value,
}: VegaValueFormatterParams<
ProposalListFieldsFragment,
'terms.enactmentDatetime'
>) => {
return value ? getDateTimeFormat().format(new Date(value)) : '-';
}}
/>
<AgGridColumn
colId="actions"
minWidth={window.innerWidth > BREAKPOINT_MD ? 221 : 80}
maxWidth={221}
sortable={false}
filter={false}
resizable={false}
cellRenderer={({
data,
}: VegaICellRendererParams<ProposalListFieldsFragment>) => {
const proposalPage = tokenLink(
TOKEN_PROPOSAL.replace(':id', data?.id || '')
);
const openDialog = () => {
if (!data) return;
setDialog({
open: true,
title: data.rationale.title,
content: data.terms,
});
};
return (
<div className="pb-1">
<button
className="underline max-md:hidden"
onClick={openDialog}
>
{t('View terms')}
</button>{' '}
<ExternalLink className="max-md:hidden" href={proposalPage}>
{t('Open in Governance')}
</ExternalLink>
<ExternalLink className="md:hidden" href={proposalPage}>
{t('Open')}
</ExternalLink>
</div>
);
}}
/>
</AgGrid>
/>
<JsonViewerDialog
open={dialog.open}
onChange={(isOpen) => setDialog({ ...dialog, open: isOpen })}
@@ -1,4 +1,4 @@
import { Icon } from '@vegaprotocol/ui-toolkit';
import { Icon, Tooltip } from '@vegaprotocol/ui-toolkit';
// https://github.com/vegaprotocol/vega/blob/develop/core/blockchain/response.go
export const ErrorCodes = new Map([
@@ -17,6 +17,8 @@ interface ChainResponseCodeProps {
code: number;
hideLabel?: boolean;
error?: string;
hideIfOk?: boolean;
small?: boolean;
}
/**
@@ -28,14 +30,21 @@ export const ChainResponseCode = ({
code,
hideLabel = false,
error,
hideIfOk = false,
small = false,
}: ChainResponseCodeProps) => {
if (hideIfOk && code === 0) {
return null;
}
const isSuccess = successCodes.has(code);
const size = small ? 3 : 4;
const successColour =
code === 71 ? 'fill-vega-orange' : 'fill-vega-green-600';
code === 71 ? '!fill-vega-orange' : '!fill-vega-green-600';
const icon = isSuccess ? (
<Icon name="tick-circle" className={successColour} />
<Icon size={size} name="tick-circle" className={`${successColour}`} />
) : (
<Icon name="cross" className="fill-vega-pink-600" />
<Icon size={size} name="cross" className="!fill-vega-pink-500" />
);
const label = ErrorCodes.get(code) || 'Unknown response code';
@@ -44,18 +53,28 @@ export const ChainResponseCode = ({
error && error.length > 100 ? error.replace(/,/g, ',\r\n') : error;
return (
<div title={`Response code: ${code} - ${label}`} className=" inline-block">
<span
className="mr-2"
aria-label={isSuccess ? 'Success' : 'Warning'}
role="img"
>
{icon}
</span>
{hideLabel ? null : <span>{label}</span>}
{!hideLabel && !!displayError ? (
<span className="ml-1 whitespace-pre">&mdash;&nbsp;{displayError}</span>
) : null}
</div>
<Tooltip
description={
<span>
Response code: {code} - {label}
</span>
}
>
<div className="mt-[-1px] inline-block">
<span
className="mr-2"
aria-label={isSuccess ? 'Success' : 'Warning'}
role="img"
>
{icon}
</span>
{hideLabel ? null : <span>{label}</span>}
{!hideLabel && !!displayError ? (
<span className="ml-1 whitespace-pre">
&mdash;&nbsp;{displayError}
</span>
) : null}
</div>
</Tooltip>
);
};
@@ -0,0 +1,113 @@
import { render } from '@testing-library/react';
import type { TxDetailsOrderProps } from './tx-order-peg';
import { TxOrderPeggedReference, getMarketDecimals } from './tx-order-peg';
import { useExplorerMarketQuery } from '../../../links/market-link/__generated__/Market';
import type { ExplorerMarketQuery } from '../../../links/market-link/__generated__/Market';
import { PeggedReference, Side } from '@vegaprotocol/types';
// Mock the useExplorerMarketQuery hook
jest.mock('../../../links/market-link/__generated__/Market', () => ({
useExplorerMarketQuery: jest.fn().mockReturnValue({
data: {
market: { decimalPlaces: 0 },
},
loading: false,
}),
}));
describe('getSettlementAsset', () => {
it('should return the decimal places if data is defined', () => {
const data = {
market: {
__typename: 'Market',
id: '123',
decimalPlaces: 8,
},
};
const result = getMarketDecimals(data as Partial<ExplorerMarketQuery>);
expect(result).toEqual(8);
});
it('should return 0 if data is undefined', () => {
const result = getMarketDecimals(undefined);
expect(result).toEqual(0);
});
});
describe('TxOrderPeggedReference', () => {
beforeEach(() => {
// Mock the useExplorerMarketQuery hook return value
(useExplorerMarketQuery as jest.Mock).mockReturnValue({
data: {
settlementAsset: 'some-settlement-asset',
},
loading: false,
});
});
afterEach(() => {
jest.resetAllMocks();
});
it('should render the offset and reference correctly', () => {
const props: TxDetailsOrderProps = {
side: Side.SIDE_BUY,
offset: '10',
reference: PeggedReference.PEGGED_REFERENCE_MID,
marketId: 'some-market-id',
};
const { getByTestId } = render(<TxOrderPeggedReference {...props} />);
expect(getByTestId('pegged-reference')).toHaveTextContent('Mid + 10');
});
it('should return null if the reference is "PEGGED_REFERENCE_UNSPECIFIED"', () => {
const props: TxDetailsOrderProps = {
side: Side.SIDE_BUY,
offset: '10',
reference: 'PEGGED_REFERENCE_UNSPECIFIED',
marketId: 'some-market-id',
};
const { container } = render(<TxOrderPeggedReference {...props} />);
expect(container.firstChild).toBeNull();
});
it('should render the offset without formatting initially, then render the formatted version', () => {
const props: TxDetailsOrderProps = {
side: Side.SIDE_BUY,
offset: '10',
reference: PeggedReference.PEGGED_REFERENCE_BEST_ASK,
marketId: 'some-market-id',
};
(useExplorerMarketQuery as jest.Mock).mockReturnValue({
data: null,
loading: true,
});
const screen = render(<TxOrderPeggedReference {...props} />);
expect(screen.getByTestId('pegged-reference')).toHaveTextContent(
'Ask + 10'
);
(useExplorerMarketQuery as jest.Mock).mockReturnValue({
data: {
market: {
decimalPlaces: 10,
},
},
loading: false,
});
screen.rerender(<TxOrderPeggedReference {...props} />);
expect(screen.getByTestId('pegged-reference')).toHaveTextContent(
'Ask + 0.000000001'
);
});
});
@@ -0,0 +1,72 @@
import { t } from '@vegaprotocol/i18n';
import { TableCell, TableRow } from '../../../table';
import type { VegaPeggedReference } from '../liquidity-provision/liquidity-provision-details';
import { Side, PeggedReferenceMapping } from '@vegaprotocol/types';
import { useExplorerMarketQuery } from '../../../links/market-link/__generated__/Market';
import type { ExplorerMarketQuery } from '../../../links/market-link/__generated__/Market';
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
export interface TxDetailsOrderProps {
offset: string;
reference: VegaPeggedReference;
marketId: string;
side: Side;
}
export function getMarketDecimals(
data: ExplorerMarketQuery | undefined
): number {
return data?.market?.decimalPlaces || 0;
}
/**
* Summarises an order's peg
*/
export const TxOrderPeggedReferenceRow = ({
offset,
reference,
marketId,
side,
}: TxDetailsOrderProps) => {
return (
<TableRow modifier="bordered">
<TableCell>{t('Pegged order')}</TableCell>
<TableCell>
<TxOrderPeggedReference
side={side}
offset={offset}
reference={reference}
marketId={marketId}
/>
</TableCell>
</TableRow>
);
};
export const TxOrderPeggedReference = ({
offset,
reference,
marketId,
side,
}: TxDetailsOrderProps) => {
const { data, loading } = useExplorerMarketQuery({
variables: { id: marketId },
});
const direction = side === Side.SIDE_BUY ? '+' : '-';
const decimalPlaces = getMarketDecimals(data);
if (reference === 'PEGGED_REFERENCE_UNSPECIFIED') {
return null;
}
return (
<span data-testid="pegged-reference">
{PeggedReferenceMapping[reference]}&nbsp;
{direction}&nbsp;
{!loading && data
? addDecimalsFormatNumber(offset, decimalPlaces)
: offset}
</span>
);
};
@@ -7,6 +7,7 @@ import { TableCell, TableRow, TableWithTbody } from '../../table';
import { txSignatureToDeterministicId } from '../lib/deterministic-ids';
import DeterministicOrderDetails from '../../order-details/deterministic-order-details';
import Hash from '../../links/hash';
import { TxOrderPeggedReferenceRow } from './order/tx-order-peg';
interface TxDetailsOrderProps {
txData: BlockExplorerTransactionResult | undefined;
@@ -29,6 +30,8 @@ export const TxDetailsOrder = ({
return <>{t('Awaiting Block Explorer transaction details')}</>;
}
const marketId = txData.command.orderSubmission.marketId || '-';
const reference = txData.command.orderSubmission.peggedOrder;
const side = txData.command.orderSubmission.side;
let deterministicId = '';
@@ -63,6 +66,14 @@ export const TxDetailsOrder = ({
<MarketLink id={marketId} />
</TableCell>
</TableRow>
{reference ? (
<TxOrderPeggedReferenceRow
side={side}
offset={reference.offset}
reference={reference.reference}
marketId={marketId}
/>
) : null}
</TableWithTbody>
{deterministicId.length > 0 ? (
@@ -10,13 +10,17 @@ export interface FilterLabelProps {
*/
export function FilterLabel({ filters }: FilterLabelProps) {
if (!filters || filters.size !== 1) {
return <span className="uppercase">{t('Filter')}</span>;
return (
<span data-testid="filter-empty" className="uppercase">
{t('Filter')}
</span>
);
}
return (
<div>
<div data-testid="filter-selected">
<span className="uppercase">{t('Filters')}:</span>&nbsp;
<code className="bg-vega-light-150 px-2 rounded-md capitalize">
<code className="bg-vega-light-150 dark:bg-vega-light-300 px-2 rounded-md capitalize dark:text-black">
{Array.from(filters)[0]}
</code>
</div>
@@ -0,0 +1,21 @@
import { render, screen } from '@testing-library/react';
import { TxsFilter } from './tx-filter';
import type { FilterOption } from './tx-filter';
describe('TxsFilter', () => {
it('renders holding text when nothing is selected', () => {
const filters: Set<FilterOption> = new Set([]);
const setFilters = jest.fn();
render(<TxsFilter filters={filters} setFilters={setFilters} />);
expect(screen.getByTestId('filter-empty')).toBeInTheDocument();
expect(screen.getByText('Filter')).toBeInTheDocument();
});
it('renders the submit order filter as selected', () => {
const filters: Set<FilterOption> = new Set(['Submit Order']);
const setFilters = jest.fn();
render(<TxsFilter filters={filters} setFilters={setFilters} />);
expect(screen.getByTestId('filter-selected')).toBeInTheDocument();
expect(screen.getByText('Submit Order')).toBeInTheDocument();
});
});
@@ -91,7 +91,7 @@ export interface TxFilterProps {
* types. It allows a user to select one transaction type to view. Later
* it will support multiple selection, but until the API supports that it is
* one or all.
* @param filters null or Set of tranaction types
* @param filters null or Set of transaction types
* @param setFilters A function to update the filters prop
* @returns
*/
@@ -100,15 +100,15 @@ export const TxsFilter = ({ filters, setFilters }: TxFilterProps) => {
<DropdownMenu
modal={false}
trigger={
<DropdownMenuTrigger className="ml-2">
<Button size="xs">
<DropdownMenuTrigger className="ml-0">
<Button size="xs" data-testid="filter-trigger">
<FilterLabel filters={filters} />
</Button>
</DropdownMenuTrigger>
}
>
<DropdownMenuContent>
{filters.size > 1 ? null : (
{filters.size > 0 ? null : (
<>
<DropdownMenuCheckboxItem
onCheckedChange={() => setFilters(new Set(AllFilterOptions))}
@@ -0,0 +1,113 @@
import { render, screen, fireEvent } from '@testing-library/react';
import { TxsListNavigation } from './tx-list-navigation';
const NOOP = () => {
return;
};
describe('TxsListNavigation', () => {
it('renders transaction list navigation', () => {
render(
<TxsListNavigation
refreshTxs={NOOP}
nextPage={NOOP}
previousPage={NOOP}
hasMoreTxs={true}
hasPreviousPage={true}
>
<span></span>
</TxsListNavigation>
);
expect(screen.getByText('Newer')).toBeInTheDocument();
expect(screen.getByText('Older')).toBeInTheDocument();
});
it('calls previousPage when "Newer" button is clicked', () => {
const previousPageMock = jest.fn();
render(
<TxsListNavigation
refreshTxs={NOOP}
nextPage={NOOP}
previousPage={previousPageMock}
hasMoreTxs={true}
hasPreviousPage={true}
>
<span></span>
</TxsListNavigation>
);
fireEvent.click(screen.getByText('Newer'));
expect(previousPageMock).toHaveBeenCalledTimes(1);
});
it('calls nextPage when "Older" button is clicked', () => {
const nextPageMock = jest.fn();
render(
<TxsListNavigation
refreshTxs={NOOP}
nextPage={nextPageMock}
previousPage={NOOP}
hasMoreTxs={true}
hasPreviousPage={true}
>
<span></span>
</TxsListNavigation>
);
fireEvent.click(screen.getByText('Older'));
expect(nextPageMock).toHaveBeenCalledTimes(1);
});
it('disables "Older" button if hasMoreTxs is false', () => {
render(
<TxsListNavigation
refreshTxs={NOOP}
nextPage={NOOP}
previousPage={NOOP}
hasMoreTxs={false}
hasPreviousPage={false}
>
<span></span>
</TxsListNavigation>
);
expect(screen.getByText('Older')).toBeDisabled();
});
it('disables "Newer" button if hasPreviousPage is false', () => {
render(
<TxsListNavigation
refreshTxs={NOOP}
nextPage={NOOP}
previousPage={NOOP}
hasMoreTxs={true}
hasPreviousPage={false}
>
<span></span>
</TxsListNavigation>
);
expect(screen.getByText('Newer')).toBeDisabled();
});
it('disables both buttons when more and previous are false', () => {
render(
<TxsListNavigation
refreshTxs={NOOP}
nextPage={NOOP}
previousPage={NOOP}
hasMoreTxs={false}
hasPreviousPage={false}
>
<span></span>
</TxsListNavigation>
);
expect(screen.getByText('Newer')).toBeDisabled();
expect(screen.getByText('Older')).toBeDisabled();
});
});
@@ -0,0 +1,63 @@
import { t } from '@vegaprotocol/i18n';
import { BlocksRefetch } from '../blocks';
import { Button } from '@vegaprotocol/ui-toolkit';
export interface TxListNavigationProps {
refreshTxs: () => void;
nextPage: () => void;
previousPage: () => void;
loading?: boolean;
hasPreviousPage: boolean;
hasMoreTxs: boolean;
children: React.ReactNode;
}
/**
* Displays a list of transactions with filters and controls to navigate through the list.
*
* @returns {JSX.Element} Transaction List and controls
*/
export const TxsListNavigation = ({
refreshTxs,
nextPage,
previousPage,
hasMoreTxs,
hasPreviousPage,
children,
loading = false,
}: TxListNavigationProps) => {
return (
<>
<menu className="mb-2 w-full ">{children}</menu>
<menu className="mb-2 w-full">
<BlocksRefetch refetch={refreshTxs} />
<div className="float-right">
<Button
className="mr-2"
size="xs"
disabled={!hasPreviousPage || loading}
onClick={() => {
previousPage();
}}
>
{t('Newer')}
</Button>
<Button
size="xs"
disabled={!hasMoreTxs || loading}
onClick={() => {
nextPage();
}}
>
{t('Older')}
</Button>
</div>
<div className="float-right mr-2">
{loading ? (
<span className="text-vega-light-300">{t('Loading...')}</span>
) : null}
</div>
</menu>
</>
);
};
@@ -24,6 +24,7 @@ const displayString: StringMap = {
LiquidityProvisionSubmission: 'LP order',
'Liquidity Provision Order': 'LP order',
LiquidityProvisionCancellation: 'LP cancel',
'Cancel LiquidityProvision Order': 'LP cancel',
LiquidityProvisionAmendment: 'LP update',
'Amend LiquidityProvision Order': 'Amend LP',
ProposalSubmission: 'Governance Proposal',
@@ -36,9 +37,12 @@ const displayString: StringMap = {
UndelegateSubmission: 'Undelegation',
KeyRotateSubmission: 'Key Rotation',
StateVariableProposal: 'State Variable',
'State Variable Proposal': 'State Variable',
Transfer: 'Transfer',
CancelTransfer: 'Cancel Transfer',
'Cancel Transfer Funds': 'Cancel Transfer',
ValidatorHeartbeat: 'Heartbeat',
'Validator Heartbeat': 'Heartbeat',
'Batch Market Instructions': 'Batch',
};
@@ -172,7 +176,7 @@ export const TxOrderType = ({ orderType, command }: TxOrderTypeProps) => {
return (
<div
data-testid="tx-type"
className={`text-sm rounded-md leading-none px-2 py-2 inline-block ${colours}`}
className={`text-sm rounded-md leading-tight px-2 inline-block whitespace-nowrap ${colours}`}
>
{type}
</div>
@@ -1,3 +1,4 @@
import { MockedProvider } from '@apollo/client/testing';
import { TxsInfiniteListItem } from './txs-infinite-list-item';
import { render, screen } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
@@ -83,21 +84,22 @@ describe('Txs infinite list item', () => {
it('renders data correctly', () => {
render(
<MemoryRouter>
<TxsInfiniteListItem
type="testType"
submitter="testPubKey"
hash="testTxHash"
block="1"
code={0}
command={{}}
/>
</MemoryRouter>
<MockedProvider>
<MemoryRouter>
<TxsInfiniteListItem
type="testType"
submitter="testPubKey"
hash="testTxHash"
block="1"
code={0}
command={{}}
/>
</MemoryRouter>
</MockedProvider>
);
expect(screen.getByTestId('tx-hash')).toHaveTextContent('testTxHash');
expect(screen.getByTestId('pub-key')).toHaveTextContent('testPubKey');
expect(screen.getByTestId('tx-type')).toHaveTextContent('testType');
expect(screen.getByTestId('tx-block')).toHaveTextContent('1');
expect(screen.getByTestId('tx-success')).toHaveTextContent('Success');
});
});
@@ -1,4 +1,3 @@
import React from 'react';
import { TruncatedLink } from '../truncate/truncated-link';
import { Routes } from '../../routes/route-names';
import { TxOrderType } from './tx-order-type';
@@ -6,8 +5,25 @@ import type { BlockExplorerTransactionResult } from '../../routes/types/block-ex
import { toHex } from '../search/detect-search';
import { ChainResponseCode } from './details/chain-response-code/chain-reponse.code';
import isNumber from 'lodash/isNumber';
import { PartyLink } from '../links';
import { useScreenDimensions } from '@vegaprotocol/react-helpers';
import type { Screen } from '@vegaprotocol/react-helpers';
import { useMemo } from 'react';
const TRUNCATE_LENGTH = 10;
const DEFAULT_TRUNCATE_LENGTH = 7;
export function getIdTruncateLength(screen: Screen): number {
if (['xxxl', 'xxl'].includes(screen)) {
return 64;
} else if (['xl', 'lg', 'md'].includes(screen)) {
return 32;
}
return DEFAULT_TRUNCATE_LENGTH;
}
export function shouldTruncateParty(screen: Screen): boolean {
return !['xxxl', 'xxl', 'xl'].includes(screen);
}
export const TxsInfiniteListItem = ({
hash,
@@ -17,6 +33,12 @@ export const TxsInfiniteListItem = ({
block,
command,
}: Partial<BlockExplorerTransactionResult>) => {
const { screenSize } = useScreenDimensions();
const idTruncateLength = useMemo(
() => getIdTruncateLength(screenSize),
[screenSize]
);
if (
!hash ||
!submitter ||
@@ -29,68 +51,40 @@ export const TxsInfiniteListItem = ({
}
return (
<div
<tr
data-testid="transaction-row"
className="flex items-center h-full border-t border-neutral-600 dark:border-neutral-800 txs-infinite-list-item grid grid-cols-10"
className="transaction-row text-left items-center h-full border-t border-neutral-600 dark:border-neutral-800 txs-infinite-list-item py-[2px]"
>
<div
className="text-sm col-span-10 md:col-span-3 leading-none"
<td
className="text-sm leading-none whitespace-nowrap font-mono"
data-testid="tx-hash"
>
<span className="md:hidden uppercase text-vega-dark-300">
ID:&nbsp;
</span>
<TruncatedLink
to={`/${Routes.TX}/${toHex(hash)}`}
text={hash}
startChars={TRUNCATE_LENGTH}
endChars={TRUNCATE_LENGTH}
/>
</div>
<div
className="text-sm col-span-10 md:col-span-3 leading-none"
data-testid="pub-key"
>
<span className="md:hidden uppercase text-vega-dark-300">
By:&nbsp;
</span>
<TruncatedLink
to={`/${Routes.PARTIES}/${submitter}`}
text={submitter}
startChars={TRUNCATE_LENGTH}
endChars={TRUNCATE_LENGTH}
/>
</div>
<div className="text-sm col-span-5 md:col-span-2 leading-none flex items-center">
<TxOrderType orderType={type} command={command} />
</div>
<div
className="text-sm col-span-3 md:col-span-1 leading-none flex items-center"
data-testid="tx-block"
>
<span className="md:hidden uppercase text-vega-dark-300">
Block:&nbsp;
</span>
<TruncatedLink
to={`/${Routes.BLOCKS}/${block}`}
text={block}
startChars={TRUNCATE_LENGTH}
endChars={TRUNCATE_LENGTH}
/>
</div>
<div
className="text-sm col-span-2 md:col-span-1 leading-none flex items-center"
data-testid="tx-success"
>
<span className="md:hidden uppercase text-vega-dark-300">
Success&nbsp;
</span>
{isNumber(code) ? (
<ChainResponseCode code={code} hideLabel={true} />
<ChainResponseCode code={code} hideLabel={true} hideIfOk={true} />
) : (
code
)}
</div>
</div>
<TruncatedLink
to={`/${Routes.TX}/${toHex(hash)}`}
text={hash}
startChars={idTruncateLength}
endChars={0}
/>
</td>
<td className="text-sm leading-none">
<TxOrderType orderType={type} command={command} />
</td>
<td className="text-sm leading-none" data-testid="pub-key">
<PartyLink truncate={shouldTruncateParty(screenSize)} id={submitter} />
</td>
<td className="text-sm items-center font-mono" data-testid="tx-block">
<TruncatedLink
to={`/${Routes.BLOCKS}/${block}`}
text={block}
startChars={5}
endChars={5}
/>
</td>
</tr>
);
};
@@ -1,8 +1,9 @@
import { TxsInfiniteList } from './txs-infinite-list';
import { render, screen, fireEvent, act } from '@testing-library/react';
import { render, screen } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import type { BlockExplorerTransactionResult } from '../../routes/types/block-explorer-response';
import { Side } from '@vegaprotocol/types';
import { MockedProvider } from '@apollo/client/testing';
const generateTxs = (number: number): BlockExplorerTransactionResult[] => {
return Array.from(Array(number)).map((_) => ({
@@ -40,7 +41,7 @@ describe('Txs infinite list', () => {
it('should display a "no items" message when no items provided', () => {
render(
<TxsInfiniteList
txs={undefined}
txs={undefined as unknown as BlockExplorerTransactionResult[]}
areTxsLoading={false}
hasMoreTxs={false}
loadMoreTxs={() => null}
@@ -48,23 +49,7 @@ describe('Txs infinite list', () => {
/>
);
expect(screen.getByTestId('emptylist')).toBeInTheDocument();
expect(
screen.getByText('This chain has 0 transactions')
).toBeInTheDocument();
});
it('error is displayed at item level', () => {
const txs = generateTxs(1);
render(
<TxsInfiniteList
txs={txs}
areTxsLoading={false}
hasMoreTxs={false}
loadMoreTxs={() => null}
error={Error('test error!')}
/>
);
expect(screen.getByText('Cannot fetch transaction')).toBeInTheDocument();
expect(screen.getByText('No transactions found')).toBeInTheDocument();
});
it('item renders data of n length into list of n length', () => {
@@ -73,85 +58,22 @@ describe('Txs infinite list', () => {
const txs = generateTxs(7);
render(
<MemoryRouter>
<TxsInfiniteList
txs={txs}
areTxsLoading={false}
hasMoreTxs={false}
loadMoreTxs={() => null}
error={undefined}
/>
<MockedProvider>
<TxsInfiniteList
txs={txs}
areTxsLoading={false}
hasMoreTxs={false}
loadMoreTxs={() => null}
error={undefined}
/>
</MockedProvider>
</MemoryRouter>
);
expect(
screen
.getByTestId('infinite-scroll-wrapper')
.querySelectorAll('.txs-infinite-list-item')
.getByTestId('transactions-list')
.querySelectorAll('.transaction-row')
).toHaveLength(7);
});
it('tries to load more items when required to initially fill the list', () => {
// For example, if initially rendering 15, the bottom of the list is
// in view of the viewport, and the callback should be executed
const txs = generateTxs(15);
const callback = jest.fn();
render(
<MemoryRouter>
<TxsInfiniteList
txs={txs}
areTxsLoading={false}
hasMoreTxs={true}
loadMoreTxs={callback}
error={undefined}
/>
</MemoryRouter>
);
expect(callback.mock.calls.length).toEqual(1);
});
it('does not try to load more items if there are no more', () => {
const txs = generateTxs(3);
const callback = jest.fn();
render(
<MemoryRouter>
<TxsInfiniteList
txs={txs}
areTxsLoading={false}
hasMoreTxs={false}
loadMoreTxs={callback}
error={undefined}
/>
</MemoryRouter>
);
expect(callback.mock.calls.length).toEqual(0);
});
it('loads more items is called when scrolled', () => {
const txs = generateTxs(14);
const callback = jest.fn();
render(
<MemoryRouter>
<TxsInfiniteList
txs={txs}
areTxsLoading={false}
hasMoreTxs={true}
loadMoreTxs={callback}
error={undefined}
/>
</MemoryRouter>
);
act(() => {
fireEvent.scroll(screen.getByTestId('infinite-scroll-wrapper'), {
target: { scrollY: 2000 },
});
});
expect(callback.mock.calls.length).toEqual(1);
});
});
@@ -1,8 +1,4 @@
import React, { useEffect, useRef } from 'react';
import { FixedSizeList as List } from 'react-window';
import InfiniteLoader from 'react-window-infinite-loader';
import { t } from '@vegaprotocol/i18n';
import { useScreenDimensions } from '@vegaprotocol/react-helpers';
import { TxsInfiniteListItem } from './txs-infinite-list-item';
import type { BlockExplorerTransactionResult } from '../../routes/types/block-explorer-response';
import EmptyList from '../empty-list/empty-list';
@@ -11,82 +7,46 @@ import { Loader } from '@vegaprotocol/ui-toolkit';
interface TxsInfiniteListProps {
hasMoreTxs: boolean;
areTxsLoading: boolean | undefined;
txs: BlockExplorerTransactionResult[] | undefined;
txs: BlockExplorerTransactionResult[];
loadMoreTxs: () => void;
error: Error | undefined;
className?: string;
hasFilters?: boolean;
}
interface ItemProps {
index: BlockExplorerTransactionResult;
style: React.CSSProperties;
isLoading: boolean;
error: Error | undefined;
tx: BlockExplorerTransactionResult;
}
// eslint-disable-next-line @typescript-eslint/no-empty-function
const NOOP = () => {};
const Item = ({ index, style, isLoading, error }: ItemProps) => {
let content;
if (error) {
content = t(`Cannot fetch transaction`);
} else if (isLoading) {
content = <Loader />;
} else {
const {
hash,
submitter,
type,
command,
block,
code,
index: blockIndex,
} = index;
content = (
<TxsInfiniteListItem
type={type}
code={code}
command={command}
submitter={submitter}
hash={hash}
block={block}
index={blockIndex}
/>
);
}
return <div style={style}>{content}</div>;
const Item = ({ tx }: ItemProps) => {
const { hash, submitter, type, command, block, code, index: blockIndex } = tx;
return (
<TxsInfiniteListItem
type={type}
code={code}
command={command}
submitter={submitter}
hash={hash}
block={block}
index={blockIndex}
/>
);
};
export const TxsInfiniteList = ({
hasMoreTxs,
areTxsLoading,
txs,
loadMoreTxs,
error,
className,
hasFilters = false,
}: TxsInfiniteListProps) => {
const { screenSize } = useScreenDimensions();
const isStacked = ['xs', 'sm'].includes(screenSize);
const infiniteLoaderRef = useRef<InfiniteLoader>(null);
const hasMountedRef = useRef(false);
useEffect(() => {
if (hasMountedRef.current) {
if (infiniteLoaderRef.current) {
infiniteLoaderRef.current.resetloadMoreItemsCache(true);
}
}
hasMountedRef.current = true;
}, [loadMoreTxs]);
if (!txs) {
if (!txs || txs.length === 0) {
if (!areTxsLoading) {
return (
<EmptyList
heading={t('This chain has 0 transactions')}
label={t('Check back soon')}
heading={t('No transactions found')}
label={
hasFilters ? t('Try a different filter') : t('Check back soon')
}
/>
);
} else {
@@ -94,57 +54,26 @@ export const TxsInfiniteList = ({
}
}
// If there are more items to be loaded then add an extra row to hold a loading indicator.
const itemCount = hasMoreTxs ? txs.length + 1 : txs.length;
// Pass an empty callback to InfiniteLoader in case it asks us to load more than once.
// eslint-disable-next-line @typescript-eslint/no-empty-function
const loadMoreItems = areTxsLoading ? NOOP : loadMoreTxs;
// Every row is loaded except for our loading indicator row.
const isItemLoaded = (index: number) => !hasMoreTxs || index < txs.length;
return (
<div className={className} data-testid="transactions-list">
<div className="lg:grid grid-cols-10 w-full mb-3 hidden text-vega-dark-300 uppercase">
<div className="col-span-3">
<span className="hidden xl:inline">{t('Transaction')} &nbsp;</span>
<span>ID</span>
</div>
<div className="col-span-3">{t('Submitted By')}</div>
<div className="col-span-2">{t('Type')}</div>
<div className="col-span-1">{t('Block')}</div>
<div className="col-span-1">{t('Success')}</div>
</div>
<div data-testid="infinite-scroll-wrapper">
<InfiniteLoader
isItemLoaded={isItemLoaded}
itemCount={itemCount}
loadMoreItems={loadMoreItems}
ref={infiniteLoaderRef}
>
{({ onItemsRendered, ref }) => (
<List
className="List"
height={995}
itemCount={itemCount}
itemSize={isStacked ? 134 : 50}
onItemsRendered={onItemsRendered}
ref={ref}
width={'100%'}
>
{({ index, style }) => (
<Item
index={txs[index]}
style={style}
isLoading={!isItemLoaded(index)}
error={error}
/>
)}
</List>
)}
</InfiniteLoader>
</div>
<div className="overflow-scroll">
<table className={className} data-testid="transactions-list">
<thead>
<tr className="w-full mb-3 text-vega-dark-300 uppercase text-left">
<th>
<span className="hidden xl:inline">{t('Txn')} &nbsp;</span>
<span>ID</span>
</th>
<th>{t('Type')}</th>
<th className="text-left">{t('From')}</th>
<th>{t('Block')}</th>
</tr>
</thead>
<tbody>
{txs.map((t) => (
<Item key={t.hash} tx={t} />
))}
</tbody>
</table>
</div>
);
};
@@ -1,23 +1,17 @@
import { Routes } from '../../routes/route-names';
import { TruncatedLink } from '../truncate/truncated-link';
import { TxOrderType } from './tx-order-type';
import { Table, TableRow, TableCell } from '../table';
import { Table, TableRow } from '../table';
import { t } from '@vegaprotocol/i18n';
import { useFetch } from '@vegaprotocol/react-helpers';
import type { BlockExplorerTransactions } from '../../routes/types/block-explorer-response';
import isNumber from 'lodash/isNumber';
import { ChainResponseCode } from './details/chain-response-code/chain-reponse.code';
import { getTxsDataUrl } from '../../hooks/use-txs-data';
import { AsyncRenderer, Loader } from '@vegaprotocol/ui-toolkit';
import EmptyList from '../empty-list/empty-list';
import { TxsInfiniteListItem } from './txs-infinite-list-item';
interface TxsPerBlockProps {
blockHeight: string;
txCount: number;
}
const truncateLength = 5;
export const TxsPerBlock = ({ blockHeight, txCount }: TxsPerBlockProps) => {
const filters = `filters[block.height]=${blockHeight}`;
const url = getTxsDataUrl({ limit: txCount.toString(), filters });
@@ -33,53 +27,23 @@ export const TxsPerBlock = ({ blockHeight, txCount }: TxsPerBlockProps) => {
<thead>
<TableRow modifier="bordered" className="font-mono">
<td>{t('Transaction')}</td>
<td>{t('From')}</td>
<td>{t('Type')}</td>
<td>{t('Status')}</td>
<td>{t('From')}</td>
<td>{t('Block')}</td>
</TableRow>
</thead>
<tbody>
{data.transactions.map(
({ hash, submitter, type, command, code }) => {
({ hash, submitter, type, command, code, block }) => {
return (
<TableRow
modifier="bordered"
key={hash}
data-testid="transaction-row"
>
<TableCell
modifier="bordered"
className="pr-12 font-mono"
>
<TruncatedLink
to={`/${Routes.TX}/${hash}`}
text={hash}
startChars={truncateLength}
endChars={truncateLength}
/>
</TableCell>
<TableCell
modifier="bordered"
className="pr-12 font-mono"
>
<TruncatedLink
to={`/${Routes.PARTIES}/${submitter}`}
text={submitter}
startChars={truncateLength}
endChars={truncateLength}
/>
</TableCell>
<TableCell modifier="bordered">
<TxOrderType orderType={type} command={command} />
</TableCell>
<TableCell modifier="bordered" className="text">
{isNumber(code) ? (
<ChainResponseCode code={code} hideLabel={true} />
) : (
code
)}
</TableCell>
</TableRow>
<TxsInfiniteListItem
block={block}
hash={hash}
submitter={submitter}
type={type}
command={command}
code={code}
/>
);
}
)}
@@ -56,10 +56,14 @@ export function VoteIcon({
return (
<div
className={`voteicon inline-block my-1 py-1 px-2 py rounded-md text-white leading-one sm align-top ${bg}`}
className={`voteicon inline-block py-0 px-2 py rounded-md text-white whitespace-nowrap leading-tight sm align-top ${bg}`}
>
<Icon name={icon} size={3} className={`mr-2 p-0 fill-${fill}`} />
<span className={`text-base text-${text}`} data-testid="label">
<Icon
name={icon}
size={3}
className={`mr-2 p-0 mb-[-1px] fill-${fill}`}
/>
<span className={`text-${text}`} data-testid="label">
{label}
</span>
</div>
+70 -42
View File
@@ -10,16 +10,18 @@ import isNumber from 'lodash/isNumber';
export interface TxsStateProps {
txsData: BlockExplorerTransactionResult[];
hasMoreTxs: boolean;
lastCursor: string;
cursor: string;
previousCursors: string[];
hasPreviousPage: boolean;
}
export interface IUseTxsData {
limit?: number;
limit: number;
filters?: string;
}
interface IGetTxsDataUrl {
limit?: string;
limit: string;
filters?: string;
}
@@ -40,63 +42,89 @@ export const getTxsDataUrl = ({ limit, filters }: IGetTxsDataUrl) => {
};
export const useTxsData = ({ limit, filters }: IUseTxsData) => {
const [{ txsData, hasMoreTxs, lastCursor }, setTxsState] =
useState<TxsStateProps>({
txsData: [],
hasMoreTxs: true,
lastCursor: '',
});
const [
{ txsData, hasMoreTxs, cursor, previousCursors, hasPreviousPage },
setTxsState,
] = useState<TxsStateProps>({
txsData: [],
hasMoreTxs: false,
previousCursors: [],
cursor: '',
hasPreviousPage: false,
});
const url = getTxsDataUrl({ limit: limit?.toString(), filters });
const url = getTxsDataUrl({ limit: limit.toString(), filters });
const {
state: { data, error, loading },
refetch,
} = useFetch<BlockExplorerTransactions>(url, {}, false);
} = useFetch<BlockExplorerTransactions>(url, {}, true);
useEffect(() => {
if (data && isNumber(data?.transactions?.length)) {
setTxsState((prev) => ({
txsData: [...prev.txsData, ...data.transactions],
hasMoreTxs: data.transactions.length > 0,
lastCursor:
data.transactions[data.transactions.length - 1]?.cursor || '',
}));
if (!loading && data && isNumber(data.transactions.length)) {
setTxsState((prev) => {
return {
...prev,
txsData: data.transactions,
hasMoreTxs: data.transactions.length >= limit,
cursor: data?.transactions.at(-1)?.cursor || '',
};
});
}
}, [setTxsState, data]);
}, [loading, setTxsState, data, limit]);
useEffect(() => {
setTxsState((prev) => ({
txsData: [],
hasMoreTxs: true,
lastCursor: '',
}));
}, [filters]);
const nextPage = useCallback(() => {
const c = data?.transactions.at(0)?.cursor;
const newPreviousCursors = c ? [...previousCursors, c] : previousCursors;
const loadTxs = useCallback(() => {
return refetch({
limit: limit,
before: lastCursor,
});
}, [lastCursor, limit, refetch]);
const refreshTxs = useCallback(async () => {
setTxsState((prev) => ({
...prev,
lastCursor: '',
hasMoreTxs: true,
txsData: [],
hasPreviousPage: true,
previousCursors: newPreviousCursors,
}));
}, [setTxsState]);
return refetch({
limit,
before: cursor,
});
}, [data, previousCursors, cursor, limit, refetch]);
const previousPage = useCallback(() => {
const previousCursor = [...previousCursors].pop();
const newPreviousCursors = previousCursors.slice(0, -1);
setTxsState((prev) => ({
...prev,
hasPreviousPage: newPreviousCursors.length > 0,
previousCursors: newPreviousCursors,
}));
return refetch({
limit,
before: previousCursor,
});
}, [previousCursors, limit, refetch]);
const refreshTxs = useCallback(async () => {
setTxsState(() => ({
txsData: [],
cursor: '',
previousCursors: [],
hasMoreTxs: false,
hasPreviousPage: false,
}));
refetch({ limit });
}, [setTxsState, limit, refetch, filters]); // eslint-disable-line react-hooks/exhaustive-deps
return {
data,
txsData,
loading,
error,
txsData,
hasMoreTxs,
lastCursor,
hasPreviousPage,
previousCursors,
cursor,
refreshTxs,
loadTxs,
nextPage,
previousPage,
};
};
+3 -1
View File
@@ -21,6 +21,7 @@ import {
import { Footer } from '../components/footer/footer';
import { Header } from '../components/header';
import { Routes } from './route-names';
import { useExplorerNodeNamesLazyQuery } from './validators/__generated__/NodeNames';
const DialogsContainer = () => {
const { isOpen, id, trigger, asJson, setOpen } = useAssetDetailsDialogStore();
@@ -39,6 +40,7 @@ export const Layout = () => {
const isHome = Boolean(useMatch(Routes.HOME));
const { ANNOUNCEMENTS_CONFIG_URL } = useEnvironment();
const fixedWidthClasses = 'w-full max-w-[1500px] mx-auto';
useExplorerNodeNamesLazyQuery();
return (
<>
@@ -49,7 +51,7 @@ export const Layout = () => {
'grid grid-rows-[auto_1fr_auto] grid-cols-1',
'border-vega-light-200 dark:border-vega-dark-200',
'antialiased text-black dark:text-white',
'overflow-hidden relative'
'relative'
)}
>
<div>
@@ -2,12 +2,15 @@ import { render } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import type { SourceType } from './oracle';
import { OracleSigners } from './oracle-signers';
import { MockedProvider } from '@apollo/client/testing';
function renderComponent(sourceType: SourceType) {
return (
<MemoryRouter>
<OracleSigners sourceType={sourceType} />
</MemoryRouter>
<MockedProvider>
<MemoryRouter>
<OracleSigners sourceType={sourceType} />
</MemoryRouter>
</MockedProvider>
);
}
@@ -1,6 +1,6 @@
import { t } from '@vegaprotocol/i18n';
import { useScreenDimensions } from '@vegaprotocol/react-helpers';
import { useMemo } from 'react';
import { useMemo, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { SubHeading } from '../../../components/sub-heading';
import { toNonHex } from '../../../components/search/detect-search';
@@ -14,8 +14,11 @@ import { PartyBlockStake } from './components/party-block-stake';
import { PartyBlockAccounts } from './components/party-block-accounts';
import { isValidPartyId } from './components/party-id-error';
import { useDataProvider } from '@vegaprotocol/data-provider';
import { TxsListNavigation } from '../../../components/txs/tx-list-navigation';
import { AllFilterOptions, TxsFilter } from '../../../components/txs/tx-filter';
const Party = () => {
const [filters, setFilters] = useState(new Set(AllFilterOptions));
const { party } = useParams<{ party: string }>();
useDocumentTitle(['Public keys', party || '-']);
@@ -24,10 +27,24 @@ const Party = () => {
const partyId = toNonHex(party ? party : '');
const { isMobile } = useScreenDimensions();
const visibleChars = useMemo(() => (isMobile ? 10 : 14), [isMobile]);
const filters = `filters[tx.submitter]=${partyId}`;
const { hasMoreTxs, loadTxs, error, txsData, loading } = useTxsData({
limit: 10,
filters,
const baseFilters = `filters[tx.submitter]=${partyId}`;
const f =
filters && filters.size === 1
? `${baseFilters}&filters[cmd.type]=${Array.from(filters)[0]}`
: baseFilters;
const {
hasMoreTxs,
nextPage,
previousPage,
error,
refreshTxs,
loading,
txsData,
hasPreviousPage,
} = useTxsData({
limit: 25,
filters: f,
});
const variables = useMemo(() => ({ partyId }), [partyId]);
@@ -81,14 +98,24 @@ const Party = () => {
</div>
<SubHeading>{t('Transactions')}</SubHeading>
<TxsListNavigation
refreshTxs={refreshTxs}
nextPage={nextPage}
previousPage={previousPage}
hasPreviousPage={hasPreviousPage}
loading={loading}
hasMoreTxs={hasMoreTxs}
>
<TxsFilter filters={filters} setFilters={setFilters} />
</TxsListNavigation>
{!error && txsData ? (
<TxsInfiniteList
hasMoreTxs={hasMoreTxs}
areTxsLoading={loading}
txs={txsData}
loadMoreTxs={loadTxs}
loadMoreTxs={nextPage}
error={error}
className="mb-28"
className="mb-28 w-full"
/>
) : (
<Splash>
+32 -13
View File
@@ -1,14 +1,14 @@
import { t } from '@vegaprotocol/i18n';
import { RouteTitle } from '../../../components/route-title';
import { BlocksRefetch } from '../../../components/blocks';
import { TxsInfiniteList } from '../../../components/txs';
import { useTxsData } from '../../../hooks/use-txs-data';
import { useDocumentTitle } from '../../../hooks/use-document-title';
import { useState } from 'react';
import { AllFilterOptions, TxsFilter } from '../../../components/txs/tx-filter';
import { TxsListNavigation } from '../../../components/txs/tx-list-navigation';
const BE_TXS_PER_REQUEST = 15;
const BE_TXS_PER_REQUEST = 25;
export const TxsList = () => {
useDocumentTitle(['Transactions']);
@@ -21,6 +21,11 @@ export const TxsList = () => {
);
};
/**
* Displays a list of transactions with filters and controls to navigate through the list.
*
* @returns {JSX.Element} Transaction List and controls
*/
export const TxsListFiltered = () => {
const [filters, setFilters] = useState(new Set(AllFilterOptions));
@@ -29,26 +34,40 @@ export const TxsListFiltered = () => {
? `filters[cmd.type]=${Array.from(filters)[0]}`
: '';
const { hasMoreTxs, loadTxs, error, txsData, refreshTxs, loading } =
useTxsData({
limit: BE_TXS_PER_REQUEST,
filters: f,
});
const {
hasMoreTxs,
nextPage,
previousPage,
error,
refreshTxs,
loading,
txsData,
hasPreviousPage,
} = useTxsData({
limit: BE_TXS_PER_REQUEST,
filters: f,
});
return (
<>
<menu className="mb-2">
<BlocksRefetch refetch={refreshTxs} />
<TxsListNavigation
refreshTxs={refreshTxs}
nextPage={nextPage}
previousPage={previousPage}
hasPreviousPage={hasPreviousPage}
loading={loading}
hasMoreTxs={hasMoreTxs}
>
<TxsFilter filters={filters} setFilters={setFilters} />
</menu>
</TxsListNavigation>
<TxsInfiniteList
hasFilters={filters.size > 0}
hasMoreTxs={hasMoreTxs}
areTxsLoading={loading}
txs={txsData}
loadMoreTxs={loadTxs}
loadMoreTxs={nextPage}
error={error}
className="mb-28"
className="mb-28 w-full min-w-[400px]"
/>
</>
);
@@ -1,10 +1,11 @@
import { BrowserRouter as Router } from 'react-router-dom';
import { render, screen } from '@testing-library/react';
import { TxDetails } from './tx-details';
import type {
BlockExplorerTransactionResult,
ValidatorHeartbeat,
} from '../../../routes/types/block-explorer-response';
import { MemoryRouter } from 'react-router-dom';
import { MockedProvider } from '@apollo/client/testing';
// Note: Long enough that there is a truncated output and a full output
const pubKey =
@@ -27,9 +28,11 @@ const txData: BlockExplorerTransactionResult = {
};
const renderComponent = (txData: BlockExplorerTransactionResult) => (
<Router>
<TxDetails txData={txData} pubKey={pubKey} />
</Router>
<MemoryRouter>
<MockedProvider>
<TxDetails txData={txData} pubKey={pubKey} />
</MockedProvider>
</MemoryRouter>
);
describe('Transaction details', () => {
@@ -0,0 +1,13 @@
query ExplorerNodeNames {
nodesConnection {
edges {
node {
id
name
pubkey
tmPubkey
ethereumAddress
}
}
}
}
@@ -0,0 +1,53 @@
import * as Types from '@vegaprotocol/types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
const defaultOptions = {} as const;
export type ExplorerNodeNamesQueryVariables = Types.Exact<{ [key: string]: never; }>;
export type ExplorerNodeNamesQuery = { __typename?: 'Query', nodesConnection: { __typename?: 'NodesConnection', edges?: Array<{ __typename?: 'NodeEdge', node: { __typename?: 'Node', id: string, name: string, pubkey: string, tmPubkey: string, ethereumAddress: string } } | null> | null } };
export const ExplorerNodeNamesDocument = gql`
query ExplorerNodeNames {
nodesConnection {
edges {
node {
id
name
pubkey
tmPubkey
ethereumAddress
}
}
}
}
`;
/**
* __useExplorerNodeNamesQuery__
*
* To run a query within a React component, call `useExplorerNodeNamesQuery` and pass it any options that fit your needs.
* When your component renders, `useExplorerNodeNamesQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useExplorerNodeNamesQuery({
* variables: {
* },
* });
*/
export function useExplorerNodeNamesQuery(baseOptions?: Apollo.QueryHookOptions<ExplorerNodeNamesQuery, ExplorerNodeNamesQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<ExplorerNodeNamesQuery, ExplorerNodeNamesQueryVariables>(ExplorerNodeNamesDocument, options);
}
export function useExplorerNodeNamesLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<ExplorerNodeNamesQuery, ExplorerNodeNamesQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<ExplorerNodeNamesQuery, ExplorerNodeNamesQueryVariables>(ExplorerNodeNamesDocument, options);
}
export type ExplorerNodeNamesQueryHookResult = ReturnType<typeof useExplorerNodeNamesQuery>;
export type ExplorerNodeNamesLazyQueryHookResult = ReturnType<typeof useExplorerNodeNamesLazyQuery>;
export type ExplorerNodeNamesQueryResult = Apollo.QueryResult<ExplorerNodeNamesQuery, ExplorerNodeNamesQueryVariables>;
+2 -3
View File
@@ -1,6 +1,5 @@
@import 'ag-grid-community/dist/styles/ag-grid.css';
@import 'ag-grid-community/dist/styles/ag-theme-balham.css';
@import 'ag-grid-community/dist/styles/ag-theme-balham-dark.css';
@import 'ag-grid-community/styles/ag-grid.css';
@import 'ag-grid-community/styles/ag-theme-balham.css';
/* You can add global styles to this file, and also import other style files */
@tailwind base;
+1 -1
View File
@@ -1,5 +1,5 @@
const { join } = require('path');
const { createGlobPatternsForDependencies } = require('@nrwl/next/tailwind');
const { createGlobPatternsForDependencies } = require('@nx/react/tailwind');
const theme = require('../../libs/tailwindcss-config/src/theme');
const vegaCustomClasses = require('../../libs/tailwindcss-config/src/vega-custom-classes');
+2 -2
View File
@@ -5,8 +5,8 @@
"types": ["node"]
},
"files": [
"../../node_modules/@nrwl/react/typings/cssmodule.d.ts",
"../../node_modules/@nrwl/react/typings/image.d.ts"
"../../node_modules/@nx/react/typings/cssmodule.d.ts",
"../../node_modules/@nx/react/typings/image.d.ts"
],
"exclude": [
"**/*.spec.ts",
+2 -2
View File
@@ -18,7 +18,7 @@
"jest.config.ts"
],
"files": [
"../../node_modules/@nrwl/react/typings/cssmodule.d.ts",
"../../node_modules/@nrwl/react/typings/image.d.ts"
"../../node_modules/@nx/react/typings/cssmodule.d.ts",
"../../node_modules/@nx/react/typings/image.d.ts"
]
}
+5 -2
View File
@@ -1,6 +1,8 @@
const { composePlugins, withNx } = require('@nx/webpack');
const { withReact } = require('@nx/react');
const SentryPlugin = require('@sentry/webpack-plugin');
module.exports = (config, context) => {
module.exports = composePlugins(withNx(), withReact(), (config) => {
const additionalPlugins = process.env.SENTRY_AUTH_TOKEN
? [
new SentryPlugin({
@@ -13,5 +15,6 @@ module.exports = (config, context) => {
return {
...config,
plugins: [...additionalPlugins, ...config.plugins],
ignoreWarnings: [/Failed to parse source map/],
};
};
});
+4 -3
View File
@@ -1,10 +1,11 @@
{
"name": "governance-e2e",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "apps/governance-e2e/src",
"projectType": "application",
"targets": {
"e2e": {
"executor": "@nrwl/cypress:cypress",
"executor": "@nx/cypress:cypress",
"options": {
"cypressConfig": "apps/governance-e2e/cypress.config.js",
"devServerTarget": "governance:serve"
@@ -16,14 +17,14 @@
}
},
"lint": {
"executor": "@nrwl/linter:eslint",
"executor": "@nx/linter:eslint",
"outputs": ["{options.outputFile}"],
"options": {
"lintFilePatterns": ["apps/governance-e2e/**/*.{js,ts}"]
}
},
"build": {
"executor": "@nrwl/workspace:run-commands",
"executor": "nx:run-commands",
"outputs": [],
"options": {
"command": "yarn tsc --project ./apps/governance-e2e/"
@@ -98,6 +98,11 @@ describe(
.and('have.length', 64);
cy.getByTestId(proposalTermsToggle).click();
// 3001-VOTE-052 3001-VOTE-010
/**
* TODO(@nx/cypress): Nesting Cypress commands in a should assertion now throws.
* You should use .then() to chain commands instead.
* More Info: https://docs.cypress.io/guides/references/migration-guide#-should
**/
cy.get('code.language-json')
.should('exist')
.within(() => {
@@ -117,6 +117,11 @@ context(
cy.getByTestId(submitWithdrawalButton).click();
});
// assert withdrawal request
/**
* TODO(@nx/cypress): Nesting Cypress commands in a should assertion now throws.
* You should use .then() to chain commands instead.
* More Info: https://docs.cypress.io/guides/references/migration-guide#-should
**/
cy.getByTestId(toast)
.first(txTimeout)
.should('contain.text', 'Funds unlocked')
@@ -130,6 +135,11 @@ context(
cy.getByTestId(toastClose).click();
});
// withdrawal complete
/**
* TODO(@nx/cypress): Nesting Cypress commands in a should assertion now throws.
* You should use .then() to chain commands instead.
* More Info: https://docs.cypress.io/guides/references/migration-guide#-should
**/
cy.getByTestId(toast)
.first(txTimeout)
.should('contain.text', 'The withdrawal has been approved.')
@@ -139,6 +149,11 @@ context(
'Withdraw 120.00 tUSDC'
);
});
/**
* TODO(@nx/cypress): Nesting Cypress commands in a should assertion now throws.
* You should use .then() to chain commands instead.
* More Info: https://docs.cypress.io/guides/references/migration-guide#-should
**/
cy.getByTestId(toast)
.last(txTimeout)
.should('contain.text', 'Transaction confirmed')
@@ -146,6 +161,11 @@ context(
cy.getByTestId('external-link').should('exist');
});
// withdrawal history for complete withdrawal displayed
/**
* TODO(@nx/cypress): Nesting Cypress commands in a should assertion now throws.
* You should use .then() to chain commands instead.
* More Info: https://docs.cypress.io/guides/references/migration-guide#-should
**/
cy.get(tableWithdrawnStatus)
.eq(1, txTimeout)
.should('have.text', 'Completed')
@@ -187,6 +207,11 @@ context(
cy.getByTestId(submitWithdrawalButton).click();
});
/**
* TODO(@nx/cypress): Nesting Cypress commands in a should assertion now throws.
* You should use .then() to chain commands instead.
* More Info: https://docs.cypress.io/guides/references/migration-guide#-should
**/
cy.getByTestId(toast)
.first(txTimeout)
.should('contain.text', 'Funds unlocked')
@@ -198,6 +223,11 @@ context(
);
cy.getByTestId(toastClose).click();
});
/**
* TODO(@nx/cypress): Nesting Cypress commands in a should assertion now throws.
* You should use .then() to chain commands instead.
* More Info: https://docs.cypress.io/guides/references/migration-guide#-should
**/
cy.get(tableTxHash)
.eq(1)
.should('have.text', 'Complete withdrawal')
@@ -213,18 +243,33 @@ context(
});
ethereumWalletConnect();
cy.getByTestId(completeWithdrawalButton).first().click();
/**
* TODO(@nx/cypress): Nesting Cypress commands in a should assertion now throws.
* You should use .then() to chain commands instead.
* More Info: https://docs.cypress.io/guides/references/migration-guide#-should
**/
cy.getByTestId(toast)
.last(txTimeout)
.should('contain.text', 'Awaiting confirmation')
.within(() => {
cy.getByTestId('external-link').should('exist');
});
/**
* TODO(@nx/cypress): Nesting Cypress commands in a should assertion now throws.
* You should use .then() to chain commands instead.
* More Info: https://docs.cypress.io/guides/references/migration-guide#-should
**/
cy.getByTestId(toast)
.first(txTimeout)
.should('contain.text', 'The withdrawal has been approved.')
.within(() => {
cy.getByTestId(toastPanel).should('contain.text', '110.00', 'tUSDC');
});
/**
* TODO(@nx/cypress): Nesting Cypress commands in a should assertion now throws.
* You should use .then() to chain commands instead.
* More Info: https://docs.cypress.io/guides/references/migration-guide#-should
**/
cy.getByTestId(toast)
.last(txTimeout)
.should('contain.text', 'Transaction confirmed')
@@ -248,6 +293,11 @@ context(
cy.getByTestId(amountInput).click().type('50');
cy.getByTestId(submitWithdrawalButton).click();
});
/**
* TODO(@nx/cypress): Nesting Cypress commands in a should assertion now throws.
* You should use .then() to chain commands instead.
* More Info: https://docs.cypress.io/guides/references/migration-guide#-should
**/
cy.getByTestId(toast)
.first(txTimeout)
.should('contain.text', 'Funds unlocked')
@@ -16,6 +16,11 @@ context('Home Page - verify elements on page', { tags: '@smoke' }, function () {
});
it('should display announcement banner', function () {
/**
* TODO(@nx/cypress): Nesting Cypress commands in a should assertion now throws.
* You should use .then() to chain commands instead.
* More Info: https://docs.cypress.io/guides/references/migration-guide#-should
**/
cy.getByTestId('app-announcement')
.should('contain.text', 'TEST ANNOUNCEMENT!')
.within(() => {
@@ -35,6 +40,11 @@ context('Home Page - verify elements on page', { tags: '@smoke' }, function () {
waitForSpinner();
}
});
/**
* TODO(@nx/cypress): Nesting Cypress commands in a should assertion now throws.
* You should use .then() to chain commands instead.
* More Info: https://docs.cypress.io/guides/references/migration-guide#-should
**/
cy.getByTestId('proposals-list-item')
.should('have.length.at.least', 1)
.first()
@@ -94,6 +104,11 @@ context('Home Page - verify elements on page', { tags: '@smoke' }, function () {
});
it('should contain link to specific validators', function () {
/**
* TODO(@nx/cypress): Nesting Cypress commands in a should assertion now throws.
* You should use .then() to chain commands instead.
* More Info: https://docs.cypress.io/guides/references/migration-guide#-should
**/
cy.getByTestId('validators')
.should('have.length', '2')
.each(($validator) => {
@@ -120,6 +135,11 @@ context('Home Page - verify elements on page', { tags: '@smoke' }, function () {
});
it('should display network data', function () {
/**
* TODO(@nx/cypress): Nesting Cypress commands in a should assertion now throws.
* You should use .then() to chain commands instead.
* More Info: https://docs.cypress.io/guides/references/migration-guide#-should
**/
cy.getByTestId('git-network-data')
.should('contain.text', 'Reading network data from')
.within(() => {
@@ -131,6 +151,11 @@ context('Home Page - verify elements on page', { tags: '@smoke' }, function () {
});
it('should display eth data', function () {
/**
* TODO(@nx/cypress): Nesting Cypress commands in a should assertion now throws.
* You should use .then() to chain commands instead.
* More Info: https://docs.cypress.io/guides/references/migration-guide#-should
**/
cy.getByTestId('git-eth-data')
.should('contain.text', 'Reading Ethereum data from')
.within(() => {
@@ -14,6 +14,9 @@ const proposalDocsLink = 'proposal-docs-link';
const proposalDocumentationLink = 'proposal-documentation-link';
const connectToVegaWalletButton = 'connect-to-vega-wallet-btn';
const governanceDocsUrl = 'https://vega.xyz/governance';
const networkUpgradeProposalListItem = 'protocol-upgrade-proposals-list-item';
const closedProposals = 'closed-proposals';
const closedProposalToggle = 'closed-proposals-toggle-networkUpgrades';
context(
'Governance Page - verify elements on page',
@@ -127,7 +130,7 @@ context(
mockNetworkUpgradeProposal();
cy.visit('/');
cy.getByTestId('home-proposal-list').within(() => {
cy.getByTestId('protocol-upgrade-proposals-list-item').should('exist');
cy.getByTestId(networkUpgradeProposalListItem).should('exist');
cy.getByTestId('protocol-upgrade-proposal-title').should(
'have.text',
'Vega release v1'
@@ -139,13 +142,14 @@ context(
mockNetworkUpgradeProposal();
navigateTo(navigation.proposals);
cy.getByTestId('open-proposals').within(() => {
/**
* TODO(@nx/cypress): Nesting Cypress commands in a should assertion now throws.
* You should use .then() to chain commands instead.
* More Info: https://docs.cypress.io/guides/references/migration-guide#-should
**/
cy.get('li')
.eq(0)
.should(
'have.attr',
'data-testid',
'protocol-upgrade-proposals-list-item'
)
.should('have.attr', 'data-testid', networkUpgradeProposalListItem)
.within(() => {
cy.get('h2').should('have.text', 'Vega release v1');
cy.getByTestId('protocol-upgrade-proposal-type').should(
@@ -166,19 +170,19 @@ context(
);
});
});
cy.get('[data-testid="closed-proposals-toggle-networkUpgrades"]').click();
cy.getByTestId('closed-proposals').within(() => {
cy.getByTestId('protocol-upgrade-proposals-list-item').should(
'have.length',
1
);
cy.getByTestId(closedProposals).within(() => {
cy.getByTestId(networkUpgradeProposalListItem).should('not.exist');
});
cy.getByTestId(closedProposalToggle).click();
cy.getByTestId(closedProposals).within(() => {
cy.getByTestId(networkUpgradeProposalListItem).should('have.length', 1);
});
});
it('should see details of network upgrade proposal', function () {
mockNetworkUpgradeProposal();
navigateTo(navigation.proposals);
cy.getByTestId('protocol-upgrade-proposals-list-item')
cy.getByTestId(networkUpgradeProposalListItem)
.first()
.find('[data-testid="view-proposal-btn"]')
.click();
@@ -201,6 +205,11 @@ context(
.should('contain.text', '99.98% approval (% validator voting power)')
.and('contain.text', '(67% voting power required)');
cy.get('h2').should('contain.text', 'Approvers (4/4 validators)');
/**
* TODO(@nx/cypress): Nesting Cypress commands in a should assertion now throws.
* You should use .then() to chain commands instead.
* More Info: https://docs.cypress.io/guides/references/migration-guide#-should
**/
cy.getByTestId('validator-name')
.should('have.length', 4)
.each(($validator) => {
@@ -215,5 +224,18 @@ context(
);
});
});
it('filtering proposal should not display any network upgrade proposals', function () {
const proposalId =
'd848fc7881f13d366df5f61ab139d5fcfa72bf838151bb51b54381870e357931';
mockNetworkUpgradeProposal();
navigateTo(navigation.proposals);
cy.get('[data-testid="proposal-filter-toggle"]').click();
cy.get('[data-testid="filter-input"]').type(proposalId);
cy.getByTestId(closedProposals).should('have.length', 1);
cy.getByTestId(networkUpgradeProposalListItem).should('not.exist');
cy.getByTestId(closedProposalToggle).should('not.exist');
});
}
);
@@ -42,6 +42,11 @@ context(
// Skipping due to bug #3471 causing flaky failuress
it.skip('should have option to view go to next and previous page', function () {
waitForBeginningOfEpoch();
/**
* TODO(@nx/cypress): Nesting Cypress commands in a should assertion now throws.
* You should use .then() to chain commands instead.
* More Info: https://docs.cypress.io/guides/references/migration-guide#-should
**/
cy.getByTestId('page-info')
.should('contain.text', 'Page ')
.invoke('text')
@@ -21,6 +21,11 @@ context(
// 1005-VEST-001
// 1005-VEST-002
it('Able to view tranches', function () {
/**
* TODO(@nx/cypress): Nesting Cypress commands in a should assertion now throws.
* You should use .then() to chain commands instead.
* More Info: https://docs.cypress.io/guides/references/migration-guide#-should
**/
cy.getByTestId('tranche-item')
.should('have.length', 2)
.first()
@@ -51,6 +56,11 @@ context(
cy.get('span').eq(1).should('have.text', 0);
});
cy.getByTestId('key-value-table').within(() => {
/**
* TODO(@nx/cypress): Nesting Cypress commands in a should assertion now throws.
* You should use .then() to chain commands instead.
* More Info: https://docs.cypress.io/guides/references/migration-guide#-should
**/
cy.getByTestId('link')
.should('have.length', 8)
.each((ethLink) => {
@@ -58,6 +68,11 @@ context(
.should('have.attr', 'href')
.and('contain', 'https://sepolia.etherscan.io/address/');
});
/**
* TODO(@nx/cypress): Nesting Cypress commands in a should assertion now throws.
* You should use .then() to chain commands instead.
* More Info: https://docs.cypress.io/guides/references/migration-guide#-should
**/
cy.getByTestId('redeem-link')
.should('have.length', 8)
.each((redeemLink) => {
@@ -71,6 +86,11 @@ context(
it('Able to view tranches with less than 10 vega', function () {
navigateTo(navigation.supply);
cy.getByTestId('show-all-tranches').click();
/**
* TODO(@nx/cypress): Nesting Cypress commands in a should assertion now throws.
* You should use .then() to chain commands instead.
* More Info: https://docs.cypress.io/guides/references/migration-guide#-should
**/
cy.getByTestId('tranche-item')
.should('have.length', 8)
.first()
@@ -74,6 +74,11 @@ context('Validators Page - verify elements on page', function () {
function () {
// 1002-STKE-050
it('Should be able to see validator names', function () {
/**
* TODO(@nx/cypress): Nesting Cypress commands in a should assertion now throws.
* You should use .then() to chain commands instead.
* More Info: https://docs.cypress.io/guides/references/migration-guide#-should
**/
cy.get('[col-id="validator"] > div > span')
.should('have.length.at.least', 1)
.each(($name) => {
@@ -82,6 +87,11 @@ context('Validators Page - verify elements on page', function () {
});
it('Should be able to see validator stake', function () {
/**
* TODO(@nx/cypress): Nesting Cypress commands in a should assertion now throws.
* You should use .then() to chain commands instead.
* More Info: https://docs.cypress.io/guides/references/migration-guide#-should
**/
cy.getByTestId('total-stake')
.should('have.length.at.least', 1)
.each(($stake) => {
@@ -105,6 +115,11 @@ context('Validators Page - verify elements on page', function () {
});
it('Should be able to see validator normalised voting power', function () {
/**
* TODO(@nx/cypress): Nesting Cypress commands in a should assertion now throws.
* You should use .then() to chain commands instead.
* More Info: https://docs.cypress.io/guides/references/migration-guide#-should
**/
cy.getByTestId('normalised-voting-power')
.should('have.length.at.least', 1)
.each(($vPower) => {
@@ -126,6 +141,11 @@ context('Validators Page - verify elements on page', function () {
// 2002-SINC-018
it('Should be able to see validator total penalties', function () {
/**
* TODO(@nx/cypress): Nesting Cypress commands in a should assertion now throws.
* You should use .then() to chain commands instead.
* More Info: https://docs.cypress.io/guides/references/migration-guide#-should
**/
cy.getByTestId('total-penalty')
.should('have.length.at.least', 1)
.each(($penalties) => {
@@ -146,6 +166,11 @@ context('Validators Page - verify elements on page', function () {
});
it('Should be able to see validator pending stake', function () {
/**
* TODO(@nx/cypress): Nesting Cypress commands in a should assertion now throws.
* You should use .then() to chain commands instead.
* More Info: https://docs.cypress.io/guides/references/migration-guide#-should
**/
cy.getByTestId('total-pending-stake')
.should('have.length.at.least', 1)
.each(($pendingStake) => {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"presets": [
[
"@nrwl/react/babel",
"@nx/react/babel",
{
"runtime": "automatic"
}
+2 -2
View File
@@ -1,6 +1,6 @@
{
"extends": ["plugin:@nrwl/nx/react", "../../.eslintrc.json"],
"ignorePatterns": ["!**/*", "__generated__", "__generated___"],
"extends": ["plugin:@nx/react", "../../.eslintrc.json"],
"ignorePatterns": ["!**/*", "__generated__"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
+1 -1
View File
@@ -31,7 +31,7 @@ Example configurations are provided here:
For convenience, you can boot the app injecting one of the configurations above by running:
```bash
yarn nx run governance:serve --env={env} # e.g. stagnet1
yarn env-cmd -f .\apps\governance\.env.{env} yarn nx run governance:serve # e.g. stagnet1
```
There are a few different configuration options offered for this app:
-6
View File
@@ -1,6 +0,0 @@
function ReactMarkdown({ children }) {
// eslint-disable-next-line react/jsx-no-useless-fragment
return <>{children}</>;
}
export default ReactMarkdown;
+2 -3
View File
@@ -3,8 +3,8 @@ export default {
displayName: 'governance',
preset: '../../jest.preset.js',
transform: {
'^(?!.*\\.(js|jsx|ts|tsx|css|json)$)': '@nrwl/react/plugins/jest',
'^.+\\.[tj]sx?$': 'babel-jest',
'^(?!.*\\.(js|jsx|ts|tsx|css|json)$)': '@nx/react/plugins/jest',
'^.+\\.[tj]sx?$': ['babel-jest', { presets: ['@nx/react/babel'] }],
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
coverageDirectory: '../../coverage/apps/governance',
@@ -16,6 +16,5 @@ export default {
'**/*.{ts,tsx}',
'!**/node_modules/**',
'!**/__generated__/**',
'!**/__generated___/**',
],
};
+14 -7
View File
@@ -1,10 +1,11 @@
{
"name": "governance",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "apps/governance/src",
"projectType": "application",
"targets": {
"build": {
"executor": "./tools/executors/webpack:build",
"executor": "@nx/webpack:webpack",
"outputs": ["{options.outputPath}"],
"defaultConfiguration": "production",
"options": {
@@ -41,7 +42,7 @@
}
},
"serve": {
"executor": "./tools/executors/webpack:serve",
"executor": "@nx/webpack:dev-server",
"options": {
"port": 4210,
"buildTarget": "governance:build:development",
@@ -55,22 +56,28 @@
}
},
"lint": {
"executor": "@nrwl/linter:eslint",
"executor": "@nx/linter:eslint",
"outputs": ["{options.outputFile}"],
"options": {
"lintFilePatterns": ["apps/governance/**/*.{ts,tsx,js,jsx}"]
}
},
"test": {
"executor": "@nrwl/jest:jest",
"outputs": ["coverage/apps/governance"],
"executor": "@nx/jest:jest",
"outputs": ["{workspaceRoot}/coverage/apps/governance"],
"options": {
"jestConfig": "apps/governance/jest.config.ts",
"passWithNoTests": true
},
"configurations": {
"ci": {
"ci": true,
"codeCoverage": true
}
}
},
"build-netlify": {
"executor": "@nrwl/workspace:run-commands",
"executor": "nx:run-commands",
"options": {
"commands": [
"cp apps/governance/netlify.toml netlify.toml",
@@ -79,7 +86,7 @@
}
},
"build-spec": {
"executor": "@nrwl/workspace:run-commands",
"executor": "nx:run-commands",
"outputs": [],
"options": {
"command": "yarn tsc --project ./apps/governance/tsconfig.spec.json"
@@ -836,5 +836,6 @@
"AllProposals": "All proposals",
"RejectedProposals": "Rejected proposals",
"networkGovernance": "Network governance",
"networkUpgrades": "Network upgrades"
"networkUpgrades": "Network upgrades",
"assetSpecification": "Asset specification"
}
@@ -1,159 +0,0 @@
import * as Types from '@vegaprotocol/types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
const defaultOptions = {} as const;
export type ProposalAssetQueryVariables = Types.Exact<{
assetId: Types.Scalars['ID'];
}>;
export type ProposalAssetQuery = {
__typename?: 'Query';
asset?: {
__typename?: 'Asset';
status: Types.AssetStatus;
source:
| { __typename?: 'BuiltinAsset' }
| { __typename?: 'ERC20'; contractAddress: string };
} | null;
};
export type AssetListBundleQueryVariables = Types.Exact<{
assetId: Types.Scalars['ID'];
}>;
export type AssetListBundleQuery = {
__typename?: 'Query';
erc20ListAssetBundle?: {
__typename?: 'Erc20ListAssetBundle';
assetSource: string;
vegaAssetId: string;
nonce: string;
signatures: string;
} | null;
};
export const ProposalAssetDocument = gql`
query ProposalAsset($assetId: ID!) {
asset(id: $assetId) {
status
source {
... on ERC20 {
contractAddress
}
}
}
}
`;
/**
* __useProposalAssetQuery__
*
* To run a query within a React component, call `useProposalAssetQuery` and pass it any options that fit your needs.
* When your component renders, `useProposalAssetQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useProposalAssetQuery({
* variables: {
* assetId: // value for 'assetId'
* },
* });
*/
export function useProposalAssetQuery(
baseOptions: Apollo.QueryHookOptions<
ProposalAssetQuery,
ProposalAssetQueryVariables
>
) {
const options = { ...defaultOptions, ...baseOptions };
return Apollo.useQuery<ProposalAssetQuery, ProposalAssetQueryVariables>(
ProposalAssetDocument,
options
);
}
export function useProposalAssetLazyQuery(
baseOptions?: Apollo.LazyQueryHookOptions<
ProposalAssetQuery,
ProposalAssetQueryVariables
>
) {
const options = { ...defaultOptions, ...baseOptions };
return Apollo.useLazyQuery<ProposalAssetQuery, ProposalAssetQueryVariables>(
ProposalAssetDocument,
options
);
}
export type ProposalAssetQueryHookResult = ReturnType<
typeof useProposalAssetQuery
>;
export type ProposalAssetLazyQueryHookResult = ReturnType<
typeof useProposalAssetLazyQuery
>;
export type ProposalAssetQueryResult = Apollo.QueryResult<
ProposalAssetQuery,
ProposalAssetQueryVariables
>;
export const AssetListBundleDocument = gql`
query AssetListBundle($assetId: ID!) {
erc20ListAssetBundle(assetId: $assetId) {
assetSource
vegaAssetId
nonce
signatures
}
}
`;
/**
* __useAssetListBundleQuery__
*
* To run a query within a React component, call `useAssetListBundleQuery` and pass it any options that fit your needs.
* When your component renders, `useAssetListBundleQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useAssetListBundleQuery({
* variables: {
* assetId: // value for 'assetId'
* },
* });
*/
export function useAssetListBundleQuery(
baseOptions: Apollo.QueryHookOptions<
AssetListBundleQuery,
AssetListBundleQueryVariables
>
) {
const options = { ...defaultOptions, ...baseOptions };
return Apollo.useQuery<AssetListBundleQuery, AssetListBundleQueryVariables>(
AssetListBundleDocument,
options
);
}
export function useAssetListBundleLazyQuery(
baseOptions?: Apollo.LazyQueryHookOptions<
AssetListBundleQuery,
AssetListBundleQueryVariables
>
) {
const options = { ...defaultOptions, ...baseOptions };
return Apollo.useLazyQuery<
AssetListBundleQuery,
AssetListBundleQueryVariables
>(AssetListBundleDocument, options);
}
export type AssetListBundleQueryHookResult = ReturnType<
typeof useAssetListBundleQuery
>;
export type AssetListBundleLazyQueryHookResult = ReturnType<
typeof useAssetListBundleLazyQuery
>;
export type AssetListBundleQueryResult = Apollo.QueryResult<
AssetListBundleQuery,
AssetListBundleQueryVariables
>;
@@ -5,13 +5,11 @@ import { MockedProvider } from '@apollo/client/testing';
import type {
AssetListBundleQuery,
ProposalAssetQuery,
} from './__generated___/Asset';
import { AssetListBundleDocument } from './__generated___/Asset';
import { ProposalAssetDocument } from './__generated___/Asset';
} from './__generated__/Asset';
import { AssetListBundleDocument } from './__generated__/Asset';
import { ProposalAssetDocument } from './__generated__/Asset';
import * as Schema from '@vegaprotocol/types';
import type { useWeb3React } from '@web3-react/core';
import BigNumber from 'bignumber.js';
import type { AppState } from '../../../../contexts/app-state/app-state-context';
const mockUseEthTx = {
perform: jest.fn(),
@@ -47,23 +45,6 @@ jest.mock('@web3-react/core', () => {
};
});
const mockAppState: AppState = {
totalAssociated: new BigNumber('50063005'),
decimals: 18,
totalSupply: new BigNumber(65000000),
vegaWalletOverlay: false,
vegaWalletManageOverlay: false,
transactionOverlay: false,
bannerMessage: '',
disconnectNotice: false,
};
jest.mock('../../../contexts/app-state/app-state-context', () => ({
useAppState: () => ({
appState: mockAppState,
}),
}));
const ASSET_ID = 'foo';
const DEFAULT__ASSET: ProposalAssetQuery = {
@@ -6,7 +6,7 @@ import { useTranslation } from 'react-i18next';
import {
useAssetListBundleQuery,
useProposalAssetQuery,
} from './__generated___/Asset';
} from './__generated__/Asset';
import { EthWalletContainer } from '../../../../components/eth-wallet-container';
const useListAsset = (assetId: string) => {
@@ -0,0 +1 @@
export * from './proposal-asset-details';
@@ -0,0 +1,48 @@
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { SubHeading } from '../../../../components/heading';
import { CollapsibleToggle } from '../../../../components/collapsible-toggle';
import { AssetDetail, AssetDetailsTable } from '@vegaprotocol/assets';
import type { AssetFieldsFragment } from '@vegaprotocol/assets';
export const ProposalAssetDetails = ({
asset,
}: {
asset: AssetFieldsFragment;
}) => {
const { t } = useTranslation();
const [showAssetDetails, setShowAssetDetails] = useState(false);
return (
<section data-testid="proposal-asset-details">
<CollapsibleToggle
toggleState={showAssetDetails}
setToggleState={setShowAssetDetails}
dataTestId={'proposal-asset-details-toggle'}
>
<SubHeading title={t('assetSpecification')} />
</CollapsibleToggle>
{showAssetDetails && (
<div className="mb-10 pb-4">
<AssetDetailsTable
asset={asset}
omitRows={[
AssetDetail.STATUS,
AssetDetail.INFRASTRUCTURE_FEE_ACCOUNT_BALANCE,
AssetDetail.GLOBAL_REWARD_POOL_ACCOUNT_BALANCE,
AssetDetail.MAKER_PAID_FEES_ACCOUNT_BALANCE,
AssetDetail.MAKER_RECEIVED_FEES_ACCOUNT_BALANCE,
AssetDetail.LP_FEE_REWARD_ACCOUNT_BALANCE,
AssetDetail.MARKET_PROPOSER_REWARD_ACCOUNT_BALANCE,
]}
inline={true}
noBorder={true}
dtClassName="text-black dark:text-white text-ui !px-0 !font-normal"
ddClassName="text-black dark:text-white text-ui !px-0 !font-normal max-w-full"
/>
</div>
)}
</section>
);
};
@@ -10,6 +10,7 @@ import { ProposalDescription } from '../proposal-description';
import { ProposalChangeTable } from '../proposal-change-table';
import { ProposalJson } from '../proposal-json';
import { ProposalVotesTable } from '../proposal-votes-table';
import { ProposalAssetDetails } from '../proposal-asset-details';
import { VoteDetails } from '../vote-details';
import { ListAsset } from '../list-asset';
import Routes from '../../../routes';
@@ -17,6 +18,8 @@ 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';
import type { AssetQuery } from '@vegaprotocol/assets';
import { removePaginationWrapper } from '@vegaprotocol/utils';
import { ProposalState } from '@vegaprotocol/types';
export enum ProposalType {
@@ -30,6 +33,7 @@ export enum ProposalType {
export interface ProposalProps {
proposal: ProposalFieldsFragment | ProposalQuery['proposal'];
newMarketData?: MarketInfoWithData | null;
assetData?: AssetQuery | null;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
restData: any;
}
@@ -38,6 +42,7 @@ export const Proposal = ({
proposal,
restData,
newMarketData,
assetData,
}: ProposalProps) => {
const { t } = useTranslation();
const { params, loading, error } = useNetworkParams([
@@ -54,6 +59,23 @@ export const Proposal = ({
return null;
}
let asset = assetData
? removePaginationWrapper(assetData.assetsConnection?.edges)[0]
: undefined;
if (proposal.terms.change.__typename === 'UpdateAsset' && asset) {
asset = {
...asset,
quantum: proposal.terms.change.quantum,
};
if (asset.source.__typename === 'ERC20') {
asset.source.lifetimeLimit = proposal.terms.change.source.lifetimeLimit;
asset.source.withdrawThreshold =
proposal.terms.change.source.withdrawThreshold;
}
}
let minVoterBalance = null;
let proposalType = null;
@@ -138,6 +160,14 @@ export const Proposal = ({
</div>
)}
{(proposal.terms.change.__typename === 'NewAsset' ||
proposal.terms.change.__typename === 'UpdateAsset') &&
asset && (
<div className="mb-4">
<ProposalAssetDetails asset={asset} />
</div>
)}
<div className="mb-6">
<ProposalJson proposal={restData?.data?.proposal} />
</div>
@@ -191,12 +191,13 @@ export const ProposalsList = ({
{sortedProposals.open.length > 0 ||
sortedProtocolUpgradeProposals.open.length > 0 ? (
<ul data-testid="open-proposals">
{sortedProtocolUpgradeProposals.open.map((proposal) => (
<ProtocolUpgradeProposalsListItem
key={proposal.upgradeBlockHeight}
proposal={proposal}
/>
))}
{filterString.length < 1 &&
sortedProtocolUpgradeProposals.open.map((proposal) => (
<ProtocolUpgradeProposalsListItem
key={proposal.upgradeBlockHeight}
proposal={proposal}
/>
))}
{sortedProposals.open.filter(filterPredicate).map((proposal) => (
<ProposalsListItem key={proposal?.id} proposal={proposal} />
@@ -9,6 +9,7 @@ import { useFetch } from '@vegaprotocol/react-helpers';
import { ENV } from '../../../config';
import { useDataProvider } from '@vegaprotocol/data-provider';
import { marketInfoWithDataProvider } from '@vegaprotocol/markets';
import { useAssetQuery } from '@vegaprotocol/assets';
export const ProposalContainer = () => {
const params = useParams<{ proposalId: string }>();
@@ -35,6 +36,25 @@ export const ProposalContainer = () => {
},
});
const {
data: assetData,
loading: assetLoading,
error: assetError,
} = useAssetQuery({
fetchPolicy: 'network-only',
variables: {
assetId:
(data?.proposal?.terms.change.__typename === 'NewAsset' &&
data?.proposal?.id) ||
(data?.proposal?.terms.change.__typename === 'UpdateAsset' &&
data.proposal.terms.change.assetId) ||
'',
},
skip: !['NewAsset', 'UpdateAsset'].includes(
data?.proposal?.terms?.change?.__typename || ''
),
});
useEffect(() => {
const interval = setInterval(refetch, 2000);
return () => clearInterval(interval);
@@ -42,15 +62,20 @@ export const ProposalContainer = () => {
return (
<AsyncRenderer
loading={loading || newMarketLoading}
error={error || newMarketError}
data={newMarketData ? { newMarketData, data } : data}
loading={loading || newMarketLoading || assetLoading}
error={error || newMarketError || assetError}
data={{
...data,
...(newMarketData ? { newMarketData } : {}),
...(assetData ? { assetData } : {}),
}}
>
{data?.proposal ? (
<Proposal
proposal={data.proposal}
restData={restData}
newMarketData={newMarketData}
assetData={assetData}
/>
) : (
<ProposalNotFound />
@@ -6,7 +6,7 @@ jest.mock('../../../../../components/connect-to-vega', () => ({
ConnectToVega: () => <div data-testid="connect-to-vega" />,
}));
jest.mock('../../../../components/eth-connect-prompt', () => ({
jest.mock('../../../../../components/eth-connect-prompt', () => ({
EthConnectPrompt: () => <div data-testid="eth-connect-prompt" />,
}));
@@ -1,64 +0,0 @@
import * as Types from '@vegaprotocol/types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
const defaultOptions = {} as const;
export type NodeDataQueryVariables = Types.Exact<{ [key: string]: never }>;
export type NodeDataQuery = {
__typename?: 'Query';
nodeData?: { __typename?: 'NodeData'; stakedTotal: string } | null;
};
export const NodeDataDocument = gql`
query NodeData {
nodeData {
stakedTotal
}
}
`;
/**
* __useNodeDataQuery__
*
* To run a query within a React component, call `useNodeDataQuery` and pass it any options that fit your needs.
* When your component renders, `useNodeDataQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useNodeDataQuery({
* variables: {
* },
* });
*/
export function useNodeDataQuery(
baseOptions?: Apollo.QueryHookOptions<NodeDataQuery, NodeDataQueryVariables>
) {
const options = { ...defaultOptions, ...baseOptions };
return Apollo.useQuery<NodeDataQuery, NodeDataQueryVariables>(
NodeDataDocument,
options
);
}
export function useNodeDataLazyQuery(
baseOptions?: Apollo.LazyQueryHookOptions<
NodeDataQuery,
NodeDataQueryVariables
>
) {
const options = { ...defaultOptions, ...baseOptions };
return Apollo.useLazyQuery<NodeDataQuery, NodeDataQueryVariables>(
NodeDataDocument,
options
);
}
export type NodeDataQueryHookResult = ReturnType<typeof useNodeDataQuery>;
export type NodeDataLazyQueryHookResult = ReturnType<
typeof useNodeDataLazyQuery
>;
export type NodeDataQueryResult = Apollo.QueryResult<
NodeDataQuery,
NodeDataQueryVariables
>;
+1 -1
View File
@@ -11,7 +11,7 @@ import type { RouteChildProps } from '..';
import Routes from '../routes';
import { TokenDetails } from './token-details';
import { Button } from '@vegaprotocol/ui-toolkit';
import { useNodeDataQuery } from './__generated___/NodeData';
import { useNodeDataQuery } from './__generated__/NodeData';
const Home = ({ name }: RouteChildProps) => {
useDocumentTitle(name);
+2 -3
View File
@@ -1,6 +1,5 @@
@import 'ag-grid-community/dist/styles/ag-grid.css';
@import 'ag-grid-community/dist/styles/ag-theme-balham.css';
@import 'ag-grid-community/dist/styles/ag-theme-balham-dark.css';
@import 'ag-grid-community/styles/ag-grid.css';
@import 'ag-grid-community/styles/ag-theme-balham.css';
@tailwind base;
@tailwind components;
+1 -1
View File
@@ -1,5 +1,5 @@
const { join } = require('path');
const { createGlobPatternsForDependencies } = require('@nrwl/next/tailwind');
const { createGlobPatternsForDependencies } = require('@nx/react/tailwind');
const theme = require('../../libs/tailwindcss-config/src/theme');
const vegaCustomClasses = require('../../libs/tailwindcss-config/src/vega-custom-classes');
+2 -2
View File
@@ -5,8 +5,8 @@
"types": ["node"]
},
"files": [
"../../node_modules/@nrwl/react/typings/cssmodule.d.ts",
"../../node_modules/@nrwl/react/typings/image.d.ts"
"../../node_modules/@nx/react/typings/cssmodule.d.ts",
"../../node_modules/@nx/react/typings/image.d.ts"
],
"exclude": [
"**/*.spec.ts",
+2 -2
View File
@@ -18,7 +18,7 @@
"jest.config.ts"
],
"files": [
"../../node_modules/@nrwl/react/typings/cssmodule.d.ts",
"../../node_modules/@nrwl/react/typings/image.d.ts"
"../../node_modules/@nx/react/typings/cssmodule.d.ts",
"../../node_modules/@nx/react/typings/image.d.ts"
]
}
+5 -2
View File
@@ -1,6 +1,8 @@
const { composePlugins, withNx } = require('@nx/webpack');
const { withReact } = require('@nx/react');
const SentryPlugin = require('@sentry/webpack-plugin');
module.exports = (config, context) => {
module.exports = composePlugins(withNx(), withReact(), (config, context) => {
const additionalPlugins = process.env.SENTRY_AUTH_TOKEN
? [
new SentryPlugin({
@@ -12,5 +14,6 @@ module.exports = (config, context) => {
return {
...config,
plugins: [...additionalPlugins, ...config.plugins],
ignoreWarnings: [/Failed to parse source map/],
};
};
});
+1 -1
View File
@@ -1,7 +1,7 @@
{
"presets": [
[
"@nrwl/react/babel",
"@nx/react/babel",
{
"runtime": "automatic"
}
@@ -1,6 +1,6 @@
{
"extends": ["plugin:@nrwl/nx/react", "../../.eslintrc.json"],
"ignorePatterns": ["!**/*", "__generated__", "__generated___"],
"extends": ["plugin:@nx/react", "../../.eslintrc.json"],
"ignorePatterns": ["!**/*", "__generated__"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
@@ -3,8 +3,8 @@ export default {
displayName: 'liquidity-provision-dashboard',
preset: '../../jest.preset.js',
transform: {
'^(?!.*\\.(js|jsx|ts|tsx|css|json)$)': '@nrwl/react/plugins/jest',
'^.+\\.[tj]sx?$': ['babel-jest', { presets: ['@nrwl/next/babel'] }],
'^(?!.*\\.(js|jsx|ts|tsx|css|json)$)': '@nx/react/plugins/jest',
'^.+\\.[tj]sx?$': ['babel-jest', { presets: ['@nx/next/babel'] }],
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
coverageDirectory: '../../coverage/apps/liquidity-provision-dashboard',
@@ -1,10 +1,11 @@
{
"name": "liquidity-provision-dashboard",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "apps/liquidity-provision-dashboard/src",
"projectType": "application",
"targets": {
"build": {
"executor": "@nrwl/web:webpack",
"executor": "@nx/webpack:webpack",
"outputs": ["{options.outputPath}"],
"defaultConfiguration": "production",
"options": {
@@ -21,7 +22,7 @@
],
"styles": ["apps/liquidity-provision-dashboard/src/styles.scss"],
"scripts": [],
"webpackConfig": "@nrwl/react/plugins/webpack"
"webpackConfig": "@nx/react/plugins/webpack"
},
"configurations": {
"development": {
@@ -47,7 +48,7 @@
}
},
"serve": {
"executor": "./tools/executors/webpack:serve",
"executor": "@nx/webpack:dev-server",
"options": {
"buildTarget": "liquidity-provision-dashboard:build",
"hmr": true,
@@ -64,7 +65,7 @@
}
},
"lint": {
"executor": "@nrwl/linter:eslint",
"executor": "@nx/linter:eslint",
"outputs": ["{options.outputFile}"],
"options": {
"lintFilePatterns": [
@@ -73,15 +74,23 @@
}
},
"test": {
"executor": "@nrwl/jest:jest",
"outputs": ["coverage/apps/liquidity-provision-dashboard"],
"executor": "@nx/jest:jest",
"outputs": [
"{workspaceRoot}/coverage/apps/liquidity-provision-dashboard"
],
"options": {
"jestConfig": "apps/liquidity-provision-dashboard/jest.config.ts",
"passWithNoTests": true
},
"configurations": {
"ci": {
"ci": true,
"codeCoverage": true
}
}
},
"build-netlify": {
"executor": "@nrwl/workspace:run-commands",
"executor": "nx:run-commands",
"options": {
"commands": [
"cp apps/liquidity-provision-dashboard/netlify.toml netlify.toml",
@@ -90,7 +99,7 @@
}
},
"build-spec": {
"executor": "@nrwl/workspace:run-commands",
"executor": "nx:run-commands",
"outputs": [],
"options": {
"command": "yarn tsc --project ./apps/liquidity-provision-dashboard/tsconfig.spec.json"
@@ -21,11 +21,14 @@ import {
HealthBar,
TooltipCellComponent,
} from '@vegaprotocol/ui-toolkit';
import type { GetRowIdParams, RowClickedEvent } from 'ag-grid-community';
import 'ag-grid-community/dist/styles/ag-grid.css';
import 'ag-grid-community/dist/styles/ag-theme-alpine.css';
import { AgGridColumn } from 'ag-grid-react';
import { useCallback, useState } from 'react';
import type {
GetRowIdParams,
RowClickedEvent,
ColDef,
} from 'ag-grid-community';
import 'ag-grid-community/styles/ag-grid.css';
import 'ag-grid-community/styles/ag-theme-alpine.css';
import { useCallback, useState, useMemo } from 'react';
import { Grid } from '../../grid';
import { HealthDialog } from '../../health-dialog';
@@ -39,6 +42,234 @@ export const MarketList = () => {
const consoleLink = useLinks(DApp.Console);
const getRowId = useCallback(({ data }: GetRowIdParams) => data.id, []);
const columnDefs = useMemo<ColDef[]>(
() => [
{
headerName: t('Market (futures)'),
field: 'tradableInstrument.instrument.name',
cellRenderer: ({ value, data }: { value: string; data: Market }) => {
return (
<>
<span className="leading-3">{value}</span>
<span className="leading-3">
{
data?.tradableInstrument?.instrument?.product?.settlementAsset
?.symbol
}
</span>
</>
);
},
minWidth: 100,
flex: 1,
headerTooltip: t('The market name and settlement asset'),
},
{
headerName: t('Market Code'),
headerTooltip: t(
'The market code is a unique identifier for this market'
),
field: 'tradableInstrument.instrument.code',
},
{
headerName: t('Type'),
headerTooltip: t('Type'),
field: 'tradableInstrument.instrument.product.__typename',
},
{
headerName: t('Last Price'),
headerTooltip: t('Latest price for this market'),
field: 'data.markPrice',
valueFormatter: ({
value,
data,
}: VegaValueFormatterParams<Market, 'data.markPrice'>) =>
value && data
? formatWithAsset(
value,
data.tradableInstrument.instrument.product.settlementAsset
)
: '-',
},
{
headerName: t('Change (24h)'),
headerTooltip: t('Change in price over the last 24h'),
cellRenderer: ({
data,
}: VegaValueFormatterParams<Market, 'data.candles'>) => {
if (data && data.candles) {
const prices = data.candles.map((candle) => candle.close);
return (
<PriceChangeCell
candles={prices}
decimalPlaces={data?.decimalPlaces}
/>
);
} else return <div>{t('-')}</div>;
},
},
{
headerName: t('Volume (24h)'),
field: 'dayVolume',
valueFormatter: ({
value,
data,
}: VegaValueFormatterParams<Market, 'dayVolume'>) =>
value && data
? `${addDecimalsFormatNumber(
value,
data.tradableInstrument.instrument.product.settlementAsset
.decimals
)} (${displayChange(data.volumeChange)})`
: '-',
headerTooltip: t('The trade volume over the last 24h'),
},
{
headerName: t('Total staked by LPs'),
field: 'liquidityCommitted',
valueFormatter: ({
value,
data,
}: VegaValueFormatterParams<Market, 'liquidityCommitted'>) =>
data && value
? formatWithAsset(
value.toString(),
data.tradableInstrument.instrument.product.settlementAsset
)
: '-',
headerTooltip: t('The amount of funds allocated to provide liquidity'),
},
{
headerName: t('Target stake'),
field: 'target',
valueFormatter: ({
value,
data,
}: VegaValueFormatterParams<Market, 'target'>) =>
data && value
? formatWithAsset(
value,
data.tradableInstrument.instrument.product.settlementAsset
)
: '-',
headerTooltip: t(
'The ideal committed liquidity to operate the market. If total commitment currently below this level then LPs can set the fee level with new commitment.'
),
},
{
headerName: t('% Target stake met'),
valueFormatter: ({ data }: VegaValueFormatterParams<Market, ''>) => {
if (data) {
const roundedPercentage =
parseInt(
(data.liquidityCommitted / parseFloat(data.target)).toFixed(0)
) * 100;
const display = Number.isNaN(roundedPercentage)
? 'N/A'
: formatNumberPercentage(toBigNum(roundedPercentage, 0), 0);
return display;
} else return '-';
},
headerTooltip: t('% Target stake met'),
},
{
headerName: t('Fee levels'),
field: 'fees',
valueFormatter: ({ value }: VegaValueFormatterParams<Market, 'fees'>) =>
value ? `${value.factors.liquidityFee}%` : '-',
headerTooltip: t('Fee level for this market'),
},
{
headerName: t('Status'),
field: 'tradingMode',
cellRenderer: ({
value,
data,
}: {
value: Schema.MarketTradingMode;
data: Market;
}) => {
return <Status trigger={data.data?.trigger} tradingMode={value} />;
},
headerTooltip: t(
'The current market status - those below the target stake mark are most in need of liquidity'
),
},
{
headerComponent: () => {
return (
<div>
<span>{t('Health')}</span>{' '}
<button
onClick={() => setIsHealthDialogOpen(true)}
aria-label={t('open tooltip')}
>
<Icon name="info-sign" />
</button>
</div>
);
},
field: 'tradingMode',
cellRenderer: ({
value,
data,
}: {
value: Schema.MarketTradingMode;
data: Market;
}) => (
<HealthBar
target={data.target}
decimals={
data.tradableInstrument.instrument.product.settlementAsset
.decimals
}
levels={data.feeLevels}
intent={intentForStatus(value)}
/>
),
sortable: false,
cellStyle: { overflow: 'unset' },
},
{
headerName: t('Age'),
field: 'marketTimestamps.open',
headerTooltip: t('Age of the market'),
valueFormatter: ({
value,
}: VegaValueFormatterParams<Market, 'marketTimestamps.open'>) => {
return value ? formatDistanceToNow(new Date(value)) : '-';
},
},
{
headerName: t('Closing Time'),
field: 'tradableInstrument.instrument.metadata.tags',
headerTooltip: t('Closing time of the market'),
valueFormatter: ({ data }: VegaValueFormatterParams<Market, ''>) => {
let expiry;
if (data?.tradableInstrument.instrument.metadata.tags) {
expiry = getExpiryDate(
data?.tradableInstrument.instrument.metadata.tags,
data?.marketTimestamps.close,
data?.state
);
}
return expiry ? expiry : '-';
},
},
],
[]
);
return (
<AsyncRenderer loading={loading} error={error} data={data}>
@@ -64,258 +295,11 @@ export const MarketList = () => {
cellClass: ['flex', 'flex-col', 'justify-center'],
tooltipComponent: TooltipCellComponent,
}}
columnDefs={columnDefs}
getRowId={getRowId}
isRowClickable
tooltipShowDelay={500}
>
<AgGridColumn
headerName={t('Market (futures)')}
field="tradableInstrument.instrument.name"
cellRenderer={({
value,
data,
}: {
value: string;
data: Market;
}) => {
return (
<>
<span className="leading-3">{value}</span>
<span className="leading-3">
{
data?.tradableInstrument?.instrument?.product
?.settlementAsset?.symbol
}
</span>
</>
);
}}
minWidth={100}
flex="1"
headerTooltip={t('The market name and settlement asset')}
/>
<AgGridColumn
headerName={t('Market Code')}
headerTooltip={t(
'The market code is a unique identifier for this market'
)}
field="tradableInstrument.instrument.code"
/>
<AgGridColumn
headerName={t('Type')}
headerTooltip={t('Type')}
field="tradableInstrument.instrument.product.__typename"
/>
<AgGridColumn
headerName={t('Last Price')}
headerTooltip={t('Latest price for this market')}
field="data.markPrice"
valueFormatter={({
value,
data,
}: VegaValueFormatterParams<Market, 'data.markPrice'>) =>
value && data
? formatWithAsset(
value,
data.tradableInstrument.instrument.product.settlementAsset
)
: '-'
}
/>
<AgGridColumn
headerName={t('Change (24h)')}
headerTooltip={t('Change in price over the last 24h')}
cellRenderer={({
data,
}: VegaValueFormatterParams<Market, 'data.candles'>) => {
if (data && data.candles) {
const prices = data.candles.map((candle) => candle.close);
return (
<PriceChangeCell
candles={prices}
decimalPlaces={data?.decimalPlaces}
/>
);
} else return <div>{t('-')}</div>;
}}
/>
<AgGridColumn
headerName={t('Volume (24h)')}
field="dayVolume"
valueFormatter={({
value,
data,
}: VegaValueFormatterParams<Market, 'dayVolume'>) =>
value && data
? `${addDecimalsFormatNumber(
value,
data.tradableInstrument.instrument.product.settlementAsset
.decimals
)} (${displayChange(data.volumeChange)})`
: '-'
}
headerTooltip={t('The trade volume over the last 24h')}
/>
<AgGridColumn
headerName={t('Total staked by LPs')}
field="liquidityCommitted"
valueFormatter={({
value,
data,
}: VegaValueFormatterParams<Market, 'liquidityCommitted'>) =>
data && value
? formatWithAsset(
value.toString(),
data.tradableInstrument.instrument.product.settlementAsset
)
: '-'
}
headerTooltip={t(
'The amount of funds allocated to provide liquidity'
)}
/>
<AgGridColumn
headerName={t('Target stake')}
field="target"
valueFormatter={({
value,
data,
}: VegaValueFormatterParams<Market, 'target'>) =>
data && value
? formatWithAsset(
value,
data.tradableInstrument.instrument.product.settlementAsset
)
: '-'
}
headerTooltip={t(
'The ideal committed liquidity to operate the market. If total commitment currently below this level then LPs can set the fee level with new commitment.'
)}
/>
<AgGridColumn
headerName={t('% Target stake met')}
valueFormatter={({
data,
}: VegaValueFormatterParams<Market, ''>) => {
if (data) {
const roundedPercentage =
parseInt(
(data.liquidityCommitted / parseFloat(data.target)).toFixed(
0
)
) * 100;
const display = Number.isNaN(roundedPercentage)
? 'N/A'
: formatNumberPercentage(toBigNum(roundedPercentage, 0), 0);
return display;
} else return '-';
}}
headerTooltip={t('% Target stake met')}
/>
<AgGridColumn
headerName={t('Fee levels')}
field="fees"
valueFormatter={({
value,
}: VegaValueFormatterParams<Market, 'fees'>) =>
value ? `${value.factors.liquidityFee}%` : '-'
}
headerTooltip={t('Fee level for this market')}
/>
<AgGridColumn
headerName={t('Status')}
field="tradingMode"
cellRenderer={({
value,
data,
}: {
value: Schema.MarketTradingMode;
data: Market;
}) => {
return (
<Status trigger={data.data?.trigger} tradingMode={value} />
);
}}
headerTooltip={t(
'The current market status - those below the target stake mark are most in need of liquidity'
)}
/>
<AgGridColumn
headerComponent={() => {
return (
<div>
<span>{t('Health')}</span>{' '}
<button
onClick={() => setIsHealthDialogOpen(true)}
aria-label={t('open tooltip')}
>
<Icon name="info-sign" />
</button>
</div>
);
}}
field="tradingMode"
cellRenderer={({
value,
data,
}: {
value: Schema.MarketTradingMode;
data: Market;
}) => (
<HealthBar
target={data.target}
decimals={
data.tradableInstrument.instrument.product.settlementAsset
.decimals
}
levels={data.feeLevels}
intent={intentForStatus(value)}
/>
)}
sortable={false}
cellStyle={{ overflow: 'unset' }}
/>
<AgGridColumn
headerName={t('Age')}
field="marketTimestamps.open"
headerTooltip={t('Age of the market')}
valueFormatter={({
value,
}: VegaValueFormatterParams<Market, 'marketTimestamps.open'>) => {
return value ? formatDistanceToNow(new Date(value)) : '-';
}}
/>
<AgGridColumn
headerName={t('Closing Time')}
field="tradableInstrument.instrument.metadata.tags"
headerTooltip={t('Closing time of the market')}
valueFormatter={({
data,
}: VegaValueFormatterParams<Market, ''>) => {
let expiry;
if (data?.tradableInstrument.instrument.metadata.tags) {
expiry = getExpiryDate(
data?.tradableInstrument.instrument.metadata.tags,
data?.marketTimestamps.close,
data?.state
);
}
return expiry ? expiry : '-';
}}
/>
</Grid>
/>
<HealthDialog
isOpen={isHealthDialogOpen}
onChange={() => {
@@ -1,7 +1,6 @@
import { useCallback } from 'react';
import { AgGridColumn } from 'ag-grid-react';
import { useCallback, useMemo } from 'react';
import type { GetRowIdParams } from 'ag-grid-community';
import type { GetRowIdParams, ColDef } from 'ag-grid-community';
import { t } from '@vegaprotocol/i18n';
import type {
@@ -36,6 +35,75 @@ export const LPProvidersGrid = ({
};
}) => {
const getRowId = useCallback(({ data }: GetRowIdParams) => data.party.id, []);
const columnDefs = useMemo<ColDef[]>(
() => [
{
headerName: t('LPs'),
field: 'party.id',
flex: 1,
minWidth: 100,
headerTooltip: t('Liquidity providers'),
},
{
headerName: t('Duration'),
valueFormatter: formatToHours,
field: 'createdAt',
headerTooltip: t('Time in market'),
},
{
headerName: t('Equity-like share'),
field: 'equityLikeShare',
valueFormatter: ({ value }: { value?: string | null }) => {
return value
? `${parseFloat(parseFloat(value).toFixed(2)) * 100}%`
: '';
},
headerTooltip: t(
'The share of the markets liquidity held - the earlier you commit liquidity the greater % fees you earn'
),
minWidth: 140,
},
{
headerName: t('committed bond'),
field: 'commitmentAmount',
valueFormatter: ({ value }: { value?: string | null }) =>
value ? formatWithAsset(value, settlementAsset) : '0',
headerTooltip: t('The amount of funds allocated to provide liquidity'),
minWidth: 140,
},
{
headerName: t('Margin Req.'),
field: 'margin',
headerTooltip: t(
'Margin required for arising positions based on liquidity commitment'
),
},
{
headerName: t('24h Fees'),
field: 'fees',
headerTooltip: t(
'Total fees earned by the liquidity provider in the last 24 hours'
),
},
{
headerName: t('Fee level'),
valueFormatter: ({ value }: { value?: string | null }) => `${value}%`,
field: 'fee',
headerTooltip: t(
"The market's liquidity fee, or the percentage of a trade's value which is collected from the price taker for every trade"
),
},
{
headerName: t('APY'),
field: 'apy',
headerTooltip: t(
'An annualised estimate based on the total liquidity provision fees and maker fees collected by liquidity providers, the maximum margin needed and maximum commitment (bond) over the course of 7 epochs'
),
},
],
[settlementAsset]
);
return (
<Grid
@@ -49,74 +117,9 @@ export const LPProvidersGrid = ({
tooltipComponent: TooltipCellComponent,
minWidth: 100,
}}
columnDefs={columnDefs}
getRowId={getRowId}
rowHeight={92}
>
<AgGridColumn
headerName={t('LPs')}
field="party.id"
flex="1"
minWidth={100}
headerTooltip={t('Liquidity providers')}
/>
<AgGridColumn
headerName={t('Duration')}
valueFormatter={formatToHours}
field="createdAt"
headerTooltip={t('Time in market')}
/>
<AgGridColumn
headerName={t('Equity-like share')}
field="equityLikeShare"
valueFormatter={({ value }: { value?: string | null }) => {
return value
? `${parseFloat(parseFloat(value).toFixed(2)) * 100}%`
: '';
}}
headerTooltip={t(
'The share of the markets liquidity held - the earlier you commit liquidity the greater % fees you earn'
)}
minWidth={140}
/>
<AgGridColumn
headerName={t('committed bond')}
field="commitmentAmount"
valueFormatter={({ value }: { value?: string | null }) =>
value ? formatWithAsset(value, settlementAsset) : '0'
}
headerTooltip={t('The amount of funds allocated to provide liquidity')}
minWidth={140}
/>
<AgGridColumn
headerName={t('Margin Req.')}
field="margin"
headerTooltip={t(
'Margin required for arising positions based on liquidity commitment'
)}
/>
<AgGridColumn
headerName={t('24h Fees')}
field="fees"
headerTooltip={t(
'Total fees earned by the liquidity provider in the last 24 hours'
)}
/>
<AgGridColumn
headerName={t('Fee level')}
valueFormatter={({ value }: { value?: string | null }) => `${value}%`}
field="fee"
headerTooltip={t(
"The market's liquidity fee, or the percentage of a trade's value which is collected from the price taker for every trade"
)}
/>
<AgGridColumn
headerName={t('APY')}
field="apy"
headerTooltip={t(
'An annualised estimate based on the total liquidity provision fees and maker fees collected by liquidity providers, the maximum margin needed and maximum commitment (bond) over the course of 7 epochs'
)}
/>
</Grid>
/>
);
};
@@ -1,5 +1,4 @@
import { useRef, useCallback, useEffect } from 'react';
import type { ReactNode } from 'react';
import { AgGridReact } from 'ag-grid-react';
import type {
AgGridReactProps,
@@ -7,18 +6,17 @@ import type {
AgGridReact as AgGridReactType,
} from 'ag-grid-react';
import classNames from 'classnames';
import 'ag-grid-community/dist/styles/ag-grid.css';
import 'ag-grid-community/dist/styles/ag-theme-alpine.css';
import 'ag-grid-community/styles/ag-grid.css';
import 'ag-grid-community/styles/ag-theme-alpine.css';
import './grid.scss';
type Props = (AgGridReactProps | AgReactUiProps) & {
isRowClickable?: boolean;
style?: React.CSSProperties;
children: ReactNode;
};
export const Grid = ({ isRowClickable, children, ...props }: Props) => {
export const Grid = ({ isRowClickable, ...props }: Props) => {
const gridRef = useRef<AgGridReactType | null>(null);
const resizeGrid = useCallback(() => {
@@ -44,8 +42,6 @@ export const Grid = ({ isRowClickable, children, ...props }: Props) => {
onGridReady={handleOnGridReady}
suppressRowClickSelection
{...props}
>
{children}
</AgGridReact>
/>
);
};
@@ -1,5 +1,5 @@
const { join } = require('path');
const { createGlobPatternsForDependencies } = require('@nrwl/next/tailwind');
const { createGlobPatternsForDependencies } = require('@nx/react/tailwind');
const theme = require('../../libs/tailwindcss-config/src/theme-lite');
const vegaCustomClasses = require('../../libs/tailwindcss-config/src/vega-custom-classes');
const vegaCustomClassesLite = require('../../libs/tailwindcss-config/src/vega-custom-classes-lite');
@@ -5,8 +5,8 @@
"types": ["node"]
},
"files": [
"../../node_modules/@nrwl/react/typings/cssmodule.d.ts",
"../../node_modules/@nrwl/react/typings/image.d.ts"
"../../node_modules/@nx/react/typings/cssmodule.d.ts",
"../../node_modules/@nx/react/typings/image.d.ts"
],
"exclude": [
"jest.config.ts",
@@ -21,7 +21,7 @@
"**/*.d.ts"
],
"files": [
"../../node_modules/@nrwl/react/typings/cssmodule.d.ts",
"../../node_modules/@nrwl/react/typings/image.d.ts"
"../../node_modules/@nx/react/typings/cssmodule.d.ts",
"../../node_modules/@nx/react/typings/image.d.ts"
]
}
+1 -1
View File
@@ -1,7 +1,7 @@
{
"presets": [
[
"@nrwl/react/babel",
"@nx/react/babel",
{
"runtime": "automatic"
}
+2 -2
View File
@@ -1,6 +1,6 @@
{
"extends": ["plugin:@nrwl/nx/react", "../../.eslintrc.json"],
"ignorePatterns": ["!**/*", "__generated__", "__generated___"],
"extends": ["plugin:@nx/react", "../../.eslintrc.json"],
"ignorePatterns": ["!**/*", "__generated__"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],

Some files were not shown because too many files have changed in this diff Show More