Compare commits

..
Author SHA1 Message Date
JonRay15 f2eedc434d Update feature-epic.md 2023-08-02 12:02:21 +01:00
Matthew Russell 4eacbbba1c chore: add default title 2023-08-02 11:47:59 +01:00
JonRay15 e9188344cf Update feature-epic.md 2023-08-01 23:24:24 +01:00
JonRay15 16c18b972d Create feature-epic.md 2023-08-01 23:11:15 +01:00
Edd d4a1a9b193 fix(explorer): add tests and fix transactions url generation (#4454) 2023-08-01 16:35:40 +01:00
Edd 376c5241a8 feat(explorer): improve transaction pagination (#4435) 2023-08-01 14:41:16 +00:00
Ciaran McGhie 8e6d8517cf feat(ui-toolkit,tailwindcss-config): healthbar auction level indicator (#4406) 2023-08-01 09:44:03 +01:00
Matthew Russell 5f9ec222c1 feat(trading): navigation (#4375) 2023-07-31 17:08:55 +01:00
Edd 8954c41c0a fix(governance): remove incorrect individual reward percentage (#4443) 2023-07-31 15:23:52 +00:00
Mikołaj Młodzikowski 4414ab4f47 feat(ci): wait for publish job to complete via github API 2023-07-31 15:04:34 +02:00
Mikołaj Młodzikowski d1d4bacc68 feat(ci): patch mainnet release 2023-07-31 13:09:00 +02:00
Joe Tsang f476688c7f chore(governance): fix failing proposal test (#4434) 2023-07-31 09:58:52 +01:00
Sam Keen 8845222700 feat(governance): remove max limit on associate function (#4427) 2023-07-28 11:12:29 +00:00
Radosław Szpiech e64c464091 chore(trading): move cumulative vol testid in orderbook (#4420) 2023-07-28 11:15:40 +02:00
Joe Tsang b2c2d0d7d6 test(governance): add e2e test and acs for proposal enhancements (#4421) 2023-07-28 09:34:39 +01:00
Joe Tsang 1fb61d313c chore(governance): add acs for network upgrade banner and estimate (#4402) 2023-07-28 09:34:30 +01:00
Mikołaj Młodzikowski ccf5ff632c feat(ci): setup new tests for console (#4400) 2023-07-28 07:32:07 +02:00
Maciek 54d6aaf56f feat(trading): add market successor proposal banner (#4401) 2023-07-27 14:54:00 +00:00
Mikołaj Młodzikowski 736b262947 fix(ci): publish of static dist (#4414) 2023-07-27 13:52:00 +02:00
Sam KeenandMatthew Russell 6a07127185 feat(governance): diff highlighter for update market proposals (#4260)
Co-authored-by: Matthew Russell <mattrussell36@gmail.com>
2023-07-27 11:26:32 +01:00
Sam Keen 45f0ba7c0e fix(explorer): explorer validator page filtering (#4354) 2023-07-27 09:04:54 +00:00
119 changed files with 2566 additions and 964 deletions
+2 -1
View File
@@ -51,7 +51,8 @@
"ul": ["list"],
"ol": ["list"]
}
]
],
"no-console": ["error", { "allow": ["warn", "error"] }]
}
},
{
+28
View File
@@ -0,0 +1,28 @@
---
name: Feature Epic
about: A template to capture and scope user requirements, high level process, and basic mockups for an upcoming feature as part of the initial core spec review process.
title: 'Epic: '
labels: feature-epic
---
## Core Feature
<Name>
## Tasks
- [ ] Define high level requirements
- [ ] Create basic mockups
- [ ] Update "API Requirements" in core spec
- [ ] Update "User-Interface Spec" in relevant front end repo
- [ ] Create detailed user stories using normal template
## High Level Requirements
## Basic Mockups
## Link to API Requirements in Core spec
## Link to User Interface Specs
## Linked User Stories
@@ -7,7 +7,7 @@ on:
jobs:
after-release:
runs-on: ubuntu-22.04
timeout-minutes: 30
timeout-minutes: 45
steps:
- name: Checkout
uses: actions/checkout@v3
@@ -30,18 +30,20 @@ jobs:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Wait for publish to complete
uses: lewagon/wait-on-check-action@v1.3.1
with:
ref: ${{ github.event.release.tag_name }}
check-name: '(CD) publish dist / trading'
repo-token: ${{ secrets.GITHUB_TOKEN }}
wait-interval: 10
- name: resolve ipfs hashes for release
run: |
echo "Name: ${{ github.event.release.name }}"
echo "Description: ${{ github.event.release.body }}"
echo "Tag: ${{ github.event.release.tag_name }}"
commit="$(git rev-list -n 1 ${{ github.event.release.tag_name }})"
echo "Commit: $commit"
until docker pull vegaprotocol/trading:$commit; do
echo "Image not pushed yet, waiting 60 seconds"
sleep 60
done
docker run --rm vegaprotocol/trading:$commit cat /ipfs-hash > ipfs-hash
docker run --rm vegaprotocol/trading:mainnet cat /ipfs-hash > ipfs-hash
curl -L https://dist.ipfs.tech/kubo/v0.20.0/kubo_v0.20.0_linux-amd64.tar.gz -o kubo.tgz
tar -xzf kubo.tgz
export PATH="$PATH:$PWD/kubo"
+9
View File
@@ -120,6 +120,15 @@ jobs:
preview_explorer: ${{ env.PREVIEW_EXPLORER }}
preview_tools: ${{ env.PREVIEW_TOOLS }}
console-e2e:
needs: lint-test-build
name: '(CI) console python'
uses: ./.github/workflows/console-test-run.yml
secrets: inherit
if: ${{ contains(fromJSON(needs.lint-test-build.outputs.projects), 'trading') && github.event_name == 'pull_request' }}
with:
github-sha: ${{ github.event.pull_request.head.sha || github.sha }}
cypress:
needs: lint-test-build
name: '(CI) cypress'
+110 -27
View File
@@ -1,55 +1,138 @@
name: console-test-run
name: (CI) Console tests
on:
workflow_call:
inputs:
github-sha:
required: true
type: string
jobs:
console-test:
timeout-minutes: 5
runs-on: self-hosted-runner
run-tests:
name: run-tests
runs-on: ubuntu-22.04
timeout-minutes: 25
steps:
#----------------------------------------------
# check-out frontend-monorepo
#----------------------------------------------
- name: Checkout console test repo
uses: actions/checkout@v3
with:
ref: ${{ inputs.github-sha }}
#----------------------------------------------
# cache node modules
#----------------------------------------------
- name: Cache node modules
id: cache
uses: actions/cache@v3
with:
path: node_modules
key: ${{ runner.os }}-cache-node-modules-${{ hashFiles('yarn.lock') }}
# comment out "restore-keys" if you need to rebuild yarn from 0
restore-keys: |
${{ runner.os }}-cache-node-modules-
#----------------------------------------------
# setup node
#----------------------------------------------
- name: Setup node
uses: actions/setup-node@v3
with:
node-version-file: '.nvmrc'
# https://stackoverflow.com/questions/61010294/how-to-cache-yarn-packages-in-github-actions
cache: yarn
#----------------------------------------------
# install deps if cache missing
#----------------------------------------------
- name: yarn install
if: steps.cache.outputs.cache-hit != 'true'
run: yarn install --pure-lockfile
#----------------------------------------------
# build trading
#----------------------------------------------
- name: Build affected spec
run: |
yarn env-cmd -f ./apps/trading/.env.stagnet1 yarn nx export trading
#----------------------------------------------
# run trading server
#----------------------------------------------
- name: Run trading server
run: |
docker run -d -p 80:4200 -v $PWD/docker/nginx.conf:/etc/nginx/conf.d/default.conf -v $PWD/dist/apps/trading/exported:/usr/share/nginx/html nginx:1.23-alpine@sha256:6318314189b40e73145a48060bff4783a116c34cc7241532d0d94198fb2c9629
sleep 5
docker ps
#----------------------------------------------
# check if container persists between runs
#----------------------------------------------
- name: Check server
run: |
docker ps
#----------------------------------------------
# check-out tests repo
#----------------------------------------------
- name: Checkout console test repo
uses: actions/checkout@v3
with:
repository: vegaprotocol/console-test
path: './console-test'
- name: Set up Python
#----------------------------------------------
# set-up python
#----------------------------------------------
- name: Set up python
id: setup-python
uses: actions/setup-python@v4
with:
python-version: '3.10.11'
#----------------------------------------------
# ----- install & configure poetry -----
#----------------------------------------------
- name: Install Poetry
uses: snok/install-poetry@v1
with:
virtualenvs-create: true
virtualenvs-in-project: true
virtualenvs-path: console-test/.venv
#----------------------------------------------
# load cached venv if cache exists
#----------------------------------------------
- name: Load cached venv
id: cached-poetry-dependencies
uses: actions/cache@v3
with:
path: console-test/.venv
key: venv-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('**/poetry.lock') }}
#----------------------------------------------
# install dependencies if cache does not exist
#----------------------------------------------
- name: Install dependencies
run: |
poetry install --no-root
working-directory: ./console-test
- name: load Binaries
run: |
poetry run python -m vega_sim.tools.load_binaries
if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true'
run: poetry install --no-interaction --no-root
#----------------------------------------------
# install vega binaries
#----------------------------------------------
- name: Install vega binaries
working-directory: ./console-test
- name: pull console
run: |
poetry run docker pull ghcr.io/vegaprotocol/frontend/trading:${{ inputs.github-sha }}
- name: Update container_name in config.py
run: |
sed -i "s/container_name = \".*\"/container_name = \"vegaprotocol\/frontend\/trading:${{ inputs.github-sha }}\"/g" config.py
run: poetry run python -m vega_sim.tools.load_binaries --force
#----------------------------------------------
# install playwright
#----------------------------------------------
- name: install playwright
run: poetry run playwright install
working-directory: ./console-test
- name: run tests
run: poetry run pytest --numprocesses auto
#----------------------------------------------
# run tests
#----------------------------------------------
- name: Run tests
working-directory: ./console-test
run: poetry run pytest --numprocesses auto
- name: Check files
run: |
ls -al .
ls -al console-test
#----------------------------------------------
# upload traces
#----------------------------------------------
- name: Upload Playwright Trace
uses: actions/upload-artifact@v3
if: always()
+5 -1
View File
@@ -36,7 +36,7 @@ jobs:
echo IS_PR=true >> $GITHUB_ENV
- name: Is mainnet release
if: ${{ contains(github.ref, 'release/mainnnet') && !contains(github.ref, 'mirror') }}
if: ${{ contains(github.ref, 'release/mainnet') && !contains(github.ref, 'mirror') }}
run: |
echo IS_MAINNET_RELEASE=true >> $GITHUB_ENV
@@ -118,8 +118,12 @@ jobs:
elif [ "${{ matrix.app }}" = "ui-toolkit" ]; then
NODE_ENV=production yarn nx run ui-toolkit:build-storybook
DIST_LOCATION=dist/storybook/ui-toolkit
elif [ "${{ matrix.app }}" = "static" ]; then
yarn nx build static || (yarn install && yarn nx build static)
else
$envCmd yarn nx build ${{ matrix.app }} || (yarn install && $envCmd yarn nx build ${{ matrix.app }})
fi
if [[ -z "$DIST_LOCATION" ]]; then
DIST_LOCATION=dist/apps/${{ matrix.app }}
fi
mv $DIST_LOCATION dist-result
@@ -7,7 +7,11 @@ context('Validator page', { tags: '@smoke' }, function () {
it('should be able to see validator tiles', function () {
cy.getNodes().then((nodes) => {
nodes.forEach((node) => {
cy.get(`[validator-id="${node.id}"]`).should('be.visible');
if (node.rankingScore.performanceScore > 0) {
cy.get(`[validator-id="${node.id}"]`).should('be.visible');
} else {
cy.get(`[validator-id="${node.id}"]`).should('not.exist');
}
});
});
});
@@ -10,7 +10,6 @@ import {
DropdownMenuTrigger,
DropdownMenuSubContent,
Icon,
Button,
} from '@vegaprotocol/ui-toolkit';
import type { Dispatch, SetStateAction } from 'react';
import { FilterLabel } from './tx-filter-label';
@@ -100,15 +99,13 @@ export const TxsFilter = ({ filters, setFilters }: TxFilterProps) => {
<DropdownMenu
modal={false}
trigger={
<DropdownMenuTrigger className="ml-0">
<Button size="xs" data-testid="filter-trigger">
<FilterLabel filters={filters} />
</Button>
<DropdownMenuTrigger>
<FilterLabel filters={filters} />
</DropdownMenuTrigger>
}
>
<DropdownMenuContent>
{filters.size > 0 ? null : (
{filters.size > 1 ? null : (
<>
<DropdownMenuCheckboxItem
onCheckedChange={() => setFilters(new Set(AllFilterOptions))}
@@ -44,7 +44,7 @@ export const TxsListNavigation = ({
</Button>
<Button
size="xs"
disabled={!hasMoreTxs || loading}
disabled={!hasMoreTxs}
onClick={() => {
nextPage();
}}
@@ -86,14 +86,18 @@ describe('Txs infinite list item', () => {
render(
<MockedProvider>
<MemoryRouter>
<TxsInfiniteListItem
type="testType"
submitter="testPubKey"
hash="testTxHash"
block="1"
code={0}
command={{}}
/>
<table>
<tbody>
<TxsInfiniteListItem
type="testType"
submitter="testPubKey"
hash="testTxHash"
block="1"
code={0}
command={{}}
/>
</tbody>
</table>
</MemoryRouter>
</MockedProvider>
);
@@ -14,9 +14,9 @@ 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;
} else if (['xl', 'lg', 'md'].includes(screen)) {
return 16;
}
return DEFAULT_TRUNCATE_LENGTH;
}
@@ -5,11 +5,17 @@ import type { BlockExplorerTransactionResult } from '../../routes/types/block-ex
import { Side } from '@vegaprotocol/types';
import { MockedProvider } from '@apollo/client/testing';
const generateHash = (): string =>
Array.from(
{ length: 64 },
() => '0123456789ABCDEF'[Math.floor(Math.random() * 16)]
).join('');
const generateTxs = (number: number): BlockExplorerTransactionResult[] => {
return Array.from(Array(number)).map((_) => ({
block: '87901',
index: 2,
hash: '0F8B98DA0923A50786B852D9CA11E051CACC4C733E1DB93D535C7D81DBD10F6F',
hash: generateHash(),
submitter:
'4b782482f587d291e8614219eb9a5ee9280fa2c58982dee71d976782a9be1964',
type: 'Submit Order',
@@ -2,7 +2,7 @@ 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 { getTxsDataUrl } from '../../hooks/use-txs-data';
import { getTxsDataUrl } from '../../hooks/get-txs-data-url';
import { AsyncRenderer, Loader } from '@vegaprotocol/ui-toolkit';
import EmptyList from '../empty-list/empty-list';
import { TxsInfiniteListItem } from './txs-infinite-list-item';
@@ -14,7 +14,7 @@ interface TxsPerBlockProps {
export const TxsPerBlock = ({ blockHeight, txCount }: TxsPerBlockProps) => {
const filters = `filters[block.height]=${blockHeight}`;
const url = getTxsDataUrl({ limit: txCount.toString(), filters });
const url = getTxsDataUrl({ filters, count: txCount });
const {
state: { data, loading, error },
} = useFetch<BlockExplorerTransactions>(url);
@@ -0,0 +1,59 @@
import { DATA_SOURCES } from '../config';
type IGetTxsDataFirstPage = {
baseUrl?: string;
count?: number;
party?: string;
filters?: string;
};
interface IGetTxsDataPrevious extends IGetTxsDataFirstPage {
before: string;
}
interface IGetTxsDataNext extends IGetTxsDataFirstPage {
after: string;
}
type IGetTxsDataUrl =
| IGetTxsDataPrevious
| IGetTxsDataNext
| IGetTxsDataFirstPage;
export const BE_TXS_PER_REQUEST = 25;
/**
* Properly encodes the filters and parameters for a request to the block explorer
* API for transactions. As the API uses a slightly less common format for encoding
* filters, some of it is more manual than you might expect.
*
* @param params An object containing the pagination and filters
* @returns string URL to call
*/
export const getTxsDataUrl = (params: IGetTxsDataUrl) => {
const baseUrl =
params.baseUrl || `${DATA_SOURCES.blockExplorerUrl}/transactions`;
const url = new URL(baseUrl);
const count = `${params.count || BE_TXS_PER_REQUEST}`;
if ('before' in params && params.before?.length > 0) {
url.searchParams.append('last', count);
url.searchParams.append('before', params.before);
} else if ('after' in params && params.after?.length > 0) {
url.searchParams.append('first', count);
url.searchParams.append('after', params.after);
} else {
url.searchParams.append('first', count);
}
// Hacky fix for param as array
let urlAsString = url.toString();
if (params.filters && params.filters?.length > 0) {
urlAsString += '&' + params.filters.replaceAll(' ', '%20');
}
if (params.party && params.party?.length > 0) {
urlAsString += `&filters[tx.submitter]=${params.party}`;
}
return urlAsString;
};
@@ -0,0 +1,48 @@
import { getTxsDataUrl } from './get-txs-data-url'; // import the function to be tested
describe('getTxsDataUrl', () => {
it('should return the correct URL without filters and party', () => {
const params = {
count: 10,
baseUrl: 'https://example.com/transactions',
};
const expectedUrl = 'https://example.com/transactions?first=10';
expect(getTxsDataUrl(params)).toEqual(expectedUrl);
});
it('should return the correct URL with "before" in params', () => {
const params = {
count: 5,
before: '100.1',
baseUrl: 'https://example.com/transactions',
};
const expectedUrl = 'https://example.com/transactions?last=5&before=100.1';
expect(getTxsDataUrl(params)).toEqual(expectedUrl);
});
it('should return the correct URL with "after" in params', () => {
const params = {
count: 5,
after: '222.1',
baseUrl: 'https://example.com/transactions',
};
const expectedUrl = 'https://example.com/transactions?first=5&after=222.1';
expect(getTxsDataUrl(params)).toEqual(expectedUrl);
});
it('should return the correct URL with filters and party', () => {
const params = {
count: 10,
filters: 'filters[cmd.type]=Made Up Transaction',
party: '1234',
baseUrl: 'https://example.com/transactions',
};
const expectedUrl =
'https://example.com/transactions?first=10&filters[cmd.type]=Made%20Up%20Transaction&filters[tx.submitter]=1234';
expect(getTxsDataUrl(params)).toEqual(expectedUrl);
});
});
+103 -92
View File
@@ -1,130 +1,141 @@
import { useCallback, useEffect, useState } from 'react';
import { useSearchParams } from 'react-router-dom';
import type { URLSearchParamsInit } from 'react-router-dom';
import { useCallback } from 'react';
import { useFetch } from '@vegaprotocol/react-helpers';
import type {
BlockExplorerTransactionResult,
BlockExplorerTransactions,
} from '../routes/types/block-explorer-response';
import { DATA_SOURCES } from '../config';
import isNumber from 'lodash/isNumber';
import { AllFilterOptions } from '../components/txs/tx-filter';
import type { FilterOption } from '../components/txs/tx-filter';
import { BE_TXS_PER_REQUEST, getTxsDataUrl } from './get-txs-data-url';
export interface TxsStateProps {
txsData: BlockExplorerTransactionResult[];
hasMoreTxs: boolean;
cursor: string;
previousCursors: string[];
hasPreviousPage: boolean;
export function getTypeFilters(filters?: Set<FilterOption>) {
if (!filters) {
return '';
} else if (filters.size > 1) {
return '';
}
const forcedSingleFilter = Array.from(filters)[0];
return `filters[cmd.type]=${forcedSingleFilter}`;
}
export interface IUseTxsData {
limit: number;
filters?: string;
count?: number;
before?: string;
after?: string;
party?: string;
filters?: Set<FilterOption>;
}
interface IGetTxsDataUrl {
limit: string;
filters?: string;
}
export const useTxsData = ({
count = 25,
before,
after,
filters,
party,
}: IUseTxsData) => {
const [, setSearchParams] = useSearchParams();
let hasMoreTxs = true;
let txsData: BlockExplorerTransactionResult[] = [];
export const getTxsDataUrl = ({ limit, filters }: IGetTxsDataUrl) => {
const url = new URL(`${DATA_SOURCES.blockExplorerUrl}/transactions`);
if (limit) {
url.searchParams.append('limit', limit);
}
// Hacky fix for param as array
let urlAsString = url.toString();
if (filters) {
urlAsString += '&' + filters.replace(' ', '%20');
}
return urlAsString;
};
export const useTxsData = ({ limit, filters }: IUseTxsData) => {
const [
{ txsData, hasMoreTxs, cursor, previousCursors, hasPreviousPage },
setTxsState,
] = useState<TxsStateProps>({
txsData: [],
hasMoreTxs: false,
previousCursors: [],
cursor: '',
hasPreviousPage: false,
const url = getTxsDataUrl({
filters: getTypeFilters(filters),
count,
before,
after,
party,
});
const url = getTxsDataUrl({ limit: limit.toString(), filters });
const {
state: { data, error, loading },
refetch,
} = useFetch<BlockExplorerTransactions>(url, {}, true);
useEffect(() => {
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 || '',
};
});
}
}, [loading, setTxsState, data, limit]);
if (!loading && data && isNumber(data.transactions.length)) {
hasMoreTxs = data.transactions.length >= count;
txsData = data.transactions;
}
const nextPage = useCallback(() => {
const c = data?.transactions.at(0)?.cursor;
const newPreviousCursors = c ? [...previousCursors, c] : previousCursors;
setTxsState((prev) => ({
...prev,
hasPreviousPage: true,
previousCursors: newPreviousCursors,
}));
return refetch({
limit,
before: cursor,
});
}, [data, previousCursors, cursor, limit, refetch]);
const after = data?.transactions.at(-1)?.cursor || '';
const params: URLSearchParamsInit = { after };
if (filters) {
params.filters = Array.from(filters).join(',');
}
setSearchParams(params);
}, [filters, data, setSearchParams]);
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 before = data?.transactions[0]?.cursor || '';
const params: URLSearchParamsInit = { before };
if (filters && filters.size > 0 && filters.size === 1) {
params.filters = Array.from(filters)[0];
}
setSearchParams(params);
}, [filters, data, setSearchParams]);
const refreshTxs = useCallback(async () => {
setTxsState(() => ({
txsData: [],
cursor: '',
previousCursors: [],
hasMoreTxs: false,
hasPreviousPage: false,
}));
const params: URLSearchParamsInit = {};
if (filters && filters.size > 0 && filters.size === 1) {
params.filters = Array.from(filters)[0];
}
setSearchParams(params);
refetch({ limit });
}, [setTxsState, limit, refetch, filters]); // eslint-disable-line react-hooks/exhaustive-deps
refetch({ count: BE_TXS_PER_REQUEST });
}, [setSearchParams, refetch, filters]);
const updateFilters = useCallback(
(newFilters: Set<FilterOption>) => {
const params: URLSearchParamsInit = {};
if (newFilters && newFilters.size === 1) {
params.filters = Array.from(newFilters)[0];
}
setSearchParams(params);
},
[setSearchParams]
);
return {
updateFilters,
txsData,
loading,
error,
hasMoreTxs,
hasPreviousPage,
previousCursors,
cursor,
refreshTxs,
nextPage,
previousPage,
};
};
/**
* Returns a Set of filters based on the URLSearchParams, or
* defaults to all.
* @param params
* @returns Set
*/
export function getInitialFilters(params: URLSearchParams): Set<FilterOption> {
const defaultFilters = new Set(AllFilterOptions);
const p = params.get('filters');
if (!p) {
return defaultFilters;
}
const filters = new Set<FilterOption>();
p.split(',').forEach((f) => {
if (AllFilterOptions.includes(f as FilterOption)) {
filters.add(f as FilterOption);
}
});
if (filters.size === 0) {
return defaultFilters;
}
return filters;
}
@@ -204,6 +204,7 @@ describe('Block', () => {
(useFetch as jest.Mock).mockReturnValue({
state: { data: createBlockResponse(1), loading: false, error: null },
});
render(renderComponent(1));
await waitFor(() => screen.getByTestId('block-header'));
expect(screen.getByTestId('previous-block-button')).toHaveAttribute(
@@ -15,9 +15,13 @@ 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 type { FilterOption } from '../../../components/txs/tx-filter';
import { AllFilterOptions, TxsFilter } from '../../../components/txs/tx-filter';
import { useSearchParams } from 'react-router-dom';
const Party = () => {
const [params] = useSearchParams();
const [filters, setFilters] = useState(new Set(AllFilterOptions));
const { party } = useParams<{ party: string }>();
@@ -27,24 +31,21 @@ const Party = () => {
const partyId = toNonHex(party ? party : '');
const { isMobile } = useScreenDimensions();
const visibleChars = useMemo(() => (isMobile ? 10 : 14), [isMobile]);
const baseFilters = `filters[tx.submitter]=${partyId}`;
const f =
filters && filters.size === 1
? `${baseFilters}&filters[cmd.type]=${Array.from(filters)[0]}`
: baseFilters;
const {
hasMoreTxs,
nextPage,
refreshTxs,
previousPage,
error,
refreshTxs,
loading,
txsData,
hasPreviousPage,
hasMoreTxs,
updateFilters,
} = useTxsData({
limit: 25,
filters: f,
filters: filters.size === 1 ? filters : undefined,
before: params.get('before') || undefined,
after: !params.get('before') ? params.get('after') || undefined : undefined,
party: partyId,
});
const variables = useMemo(() => ({ partyId }), [partyId]);
@@ -102,15 +103,21 @@ const Party = () => {
refreshTxs={refreshTxs}
nextPage={nextPage}
previousPage={previousPage}
hasPreviousPage={hasPreviousPage}
hasPreviousPage={true}
loading={loading}
hasMoreTxs={hasMoreTxs}
>
<TxsFilter filters={filters} setFilters={setFilters} />
<TxsFilter
filters={filters}
setFilters={(f) => {
setFilters(f);
updateFilters(f as Set<FilterOption>);
}}
/>
</TxsListNavigation>
{!error && txsData ? (
<TxsInfiniteList
hasMoreTxs={hasMoreTxs}
hasMoreTxs={true}
areTxsLoading={loading}
txs={txsData}
loadMoreTxs={nextPage}
+19 -16
View File
@@ -1,14 +1,14 @@
import { t } from '@vegaprotocol/i18n';
import { RouteTitle } from '../../../components/route-title';
import { TxsInfiniteList } from '../../../components/txs';
import { useTxsData } from '../../../hooks/use-txs-data';
import { useTxsData, getInitialFilters } 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 { TxsFilter } from '../../../components/txs/tx-filter';
import type { FilterOption } from '../../../components/txs/tx-filter';
import { TxsListNavigation } from '../../../components/txs/tx-list-navigation';
const BE_TXS_PER_REQUEST = 25;
import { useSearchParams } from 'react-router-dom';
export const TxsList = () => {
useDocumentTitle(['Transactions']);
@@ -27,25 +27,22 @@ export const TxsList = () => {
* @returns {JSX.Element} Transaction List and controls
*/
export const TxsListFiltered = () => {
const [filters, setFilters] = useState(new Set(AllFilterOptions));
const f =
filters && filters.size === 1
? `filters[cmd.type]=${Array.from(filters)[0]}`
: '';
const [params] = useSearchParams();
const [filters, setFilters] = useState(getInitialFilters(params));
const {
hasMoreTxs,
nextPage,
previousPage,
error,
refreshTxs,
loading,
txsData,
hasPreviousPage,
hasMoreTxs,
updateFilters,
} = useTxsData({
limit: BE_TXS_PER_REQUEST,
filters: f,
filters: filters.size === 1 ? filters : undefined,
before: params.get('before') || undefined,
after: !params.get('before') ? params.get('after') || undefined : undefined,
});
return (
@@ -54,11 +51,17 @@ export const TxsListFiltered = () => {
refreshTxs={refreshTxs}
nextPage={nextPage}
previousPage={previousPage}
hasPreviousPage={hasPreviousPage}
hasPreviousPage={true}
loading={loading}
hasMoreTxs={hasMoreTxs}
>
<TxsFilter filters={filters} setFilters={setFilters} />
<TxsFilter
filters={filters}
setFilters={(f) => {
setFilters(f);
updateFilters(f as Set<FilterOption>);
}}
/>
</TxsListNavigation>
<TxsInfiniteList
hasFilters={filters.size > 0}
@@ -15,6 +15,9 @@ query ExplorerNodes {
stakedByDelegates
stakedTotal
pendingStake
rankingScore {
performanceScore
}
epochData {
total
offline
@@ -6,7 +6,7 @@ const defaultOptions = {} as const;
export type ExplorerNodesQueryVariables = Types.Exact<{ [key: string]: never; }>;
export type ExplorerNodesQuery = { __typename?: 'Query', nodesConnection: { __typename?: 'NodesConnection', edges?: Array<{ __typename?: 'NodeEdge', node: { __typename?: 'Node', id: string, name: string, infoUrl: string, avatarUrl?: string | null, pubkey: string, tmPubkey: string, ethereumAddress: string, location: string, status: Types.NodeStatus, stakedByOperator: string, stakedByDelegates: string, stakedTotal: string, pendingStake: string, epochData?: { __typename?: 'EpochData', total: number, offline: number, online: number } | null } } | null> | null } };
export type ExplorerNodesQuery = { __typename?: 'Query', nodesConnection: { __typename?: 'NodesConnection', edges?: Array<{ __typename?: 'NodeEdge', node: { __typename?: 'Node', id: string, name: string, infoUrl: string, avatarUrl?: string | null, pubkey: string, tmPubkey: string, ethereumAddress: string, location: string, status: Types.NodeStatus, stakedByOperator: string, stakedByDelegates: string, stakedTotal: string, pendingStake: string, rankingScore: { __typename?: 'RankingScore', performanceScore: string }, epochData?: { __typename?: 'EpochData', total: number, offline: number, online: number } | null } } | null> | null } };
export const ExplorerNodesDocument = gql`
@@ -27,6 +27,9 @@ export const ExplorerNodesDocument = gql`
stakedByDelegates
stakedTotal
pendingStake
rankingScore {
performanceScore
}
epochData {
total
offline
@@ -91,7 +91,15 @@ export const ValidatorsPage = () => {
const { data: tmData } = useTendermintValidators(5000);
const { data, loading, error, refetch } = useExplorerNodesQuery();
const validators = compact(data?.nodesConnection.edges?.map((e) => e?.node));
const validators = compact(
data?.nodesConnection.edges
?.map((e) => e?.node)
.filter(
(node) =>
node?.rankingScore?.performanceScore &&
new BigNumber(node.rankingScore.performanceScore).isGreaterThan(0)
)
);
// voting power
const powers = compact(tmData?.result.validators).map(
@@ -218,17 +218,33 @@ describe(
getProposalInformationFromTable('Tokens for proposal')
.should('have.text', (1).toFixed(2))
.and('be.visible');
navigateTo(navigation.proposals);
cy.get<testFreeformProposal>('@rawProposal').then((rawProposal) => {
getProposalFromTitle(rawProposal.rationale.title).within(() => {
// 3002-PROP-021
cy.getByTestId('user-voted-yes').should('exist');
cy.getByTestId(viewProposalButton).click();
});
});
cy.getByTestId(changeVoteButton).should('be.visible').click();
voteForProposal('against');
cy.getByTestId(proposalVoteProgressAgainstPercentage)
.contains('100.00%')
.and('be.visible');
cy.getByTestId(voteBreakdownToggle).click();
getProposalInformationFromTable('Tokens against proposal')
.should('have.text', (1).toFixed(2))
.and('be.visible');
getProposalInformationFromTable('Number of voting parties')
.should('have.text', '1')
.and('be.visible');
navigateTo(navigation.proposals);
cy.get<testFreeformProposal>('@rawProposal').then((rawProposal) => {
getProposalFromTitle(rawProposal.rationale.title).within(() => {
cy.getByTestId('user-voted-no').should('exist');
cy.getByTestId(viewProposalButton).click();
});
});
});
// 3001-VOTE-042, 3001-VOTE-057, 3001-VOTE-058, 3001-VOTE-059, 3001-VOTE-060
@@ -26,6 +26,8 @@ const votesTable = 'votes-table';
const openProposals = 'open-proposals';
const proposalVoteProgressForPercentage =
'vote-progress-indicator-percentage-for';
const majorityVoteReached = 'majority-reached';
const minParticipationReached = 'participation-reached';
const proposalTimeout = { timeout: 8000 };
context(
@@ -72,7 +74,7 @@ context(
});
});
// 3001-VOTE-046 3001-VOTE-044 3001-VOTE-074 3001-VOTE-074
// 3001-VOTE-020 3001-VOTE-021 3001-VOTE-046 3001-VOTE-044 3001-VOTE-074 3001-VOTE-074
it('Able to enact proposal by voting', function () {
const proposalTitle = 'Add New proposal with short enactment';
const proposalTx = createUpdateNetworkProposalTxBody();
@@ -85,10 +87,18 @@ context(
cy.contains(proposalTitle)
.parentsUntil(proposalListItem)
.last()
.within(() => cy.getByTestId(viewProposalButton).click());
.within(() => {
// 3001-VOTE-019 time to vote is highlighted red
cy.getByTestId('vote-details')
.find('span')
.should('have.class', 'text-vega-pink');
cy.getByTestId(viewProposalButton).click();
});
});
cy.getByTestId(proposalStatus).should('have.text', 'Open');
voteForProposal('for');
cy.getByTestId(majorityVoteReached).should('exist');
cy.getByTestId(minParticipationReached).should('exist');
cy.getByTestId(proposalStatus, proposalTimeout)
.should('have.text', 'Passed')
.then(() => {
@@ -104,6 +114,14 @@ context(
cy.getByTestId(proposalVoteProgressForPercentage)
.contains('100.00%')
.and('be.visible');
navigateTo(navigation.proposals);
cy.contains(proposalTitle)
.parentsUntil(proposalListItem)
.last()
.within(() => {
cy.getByTestId(majorityVoteReached).should('exist');
cy.getByTestId(minParticipationReached).should('exist');
});
});
// 3001-VOTE-047
@@ -49,8 +49,8 @@ describe('Governance flow for proposal list', { tags: '@slow' }, function () {
navigateTo(navigation.proposals);
});
// 3001-VOTE-018
it('Newly created proposals list - proposals closest to closing date appear higher in list', function () {
// 3001-VOTE-005
const proposalDays = [364, 50, 2];
for (let index = 0; index < proposalDays.length; index++) {
goToMakeNewProposal(governanceProposalType.RAW);
@@ -120,16 +120,24 @@ describe('Governance flow for proposal list', { tags: '@slow' }, function () {
submitUniqueRawProposal({ proposalTitle: proposalTitle });
getProposalFromTitle(proposalTitle).within(() => {
// 3001-VOTE-039
cy.getByTestId(voteStatus).should(
cy.getByTestId('participation-not-reached').should(
'have.text',
'Participation not reached'
'Min. participation not reached'
);
cy.getByTestId(viewProposalButton).click();
});
voteForProposal('for');
navigateTo(navigation.proposals);
getProposalFromTitle(proposalTitle).within(() => {
cy.getByTestId(voteStatus).should('have.text', 'Set to pass');
cy.getByTestId(voteStatus).should(
'have.text',
'Currently expected to pass'
);
cy.getByTestId('user-voted-yes').should('exist');
cy.getByTestId('participation-reached').should(
'have.text',
'Min. participation reached'
);
cy.getByTestId(viewProposalButton).click();
});
cy.getByTestId(voteBreakDownToggle).click();
@@ -351,6 +351,12 @@ context(
);
stakingPageDisassociateAllTokens();
});
it('Able to associate over 1 million tokens', function () {
cy.get(ethWalletAssociateButton).click();
stakingPageAssociateTokens('2000000', { approve: true });
stakingPageDisassociateAllTokens();
});
});
}
);
@@ -17,6 +17,7 @@ const governanceDocsUrl = 'https://vega.xyz/governance';
const networkUpgradeProposalListItem = 'protocol-upgrade-proposals-list-item';
const closedProposals = 'closed-proposals';
const closedProposalToggle = 'closed-proposals-toggle-networkUpgrades';
const protocolUpgradeTime = 'protocol-upgrade-time';
context(
'Governance Page - verify elements on page',
@@ -174,6 +175,7 @@ context(
});
});
// 3009-NTWU-003 3009-NTWU-004 3009-NTWU-007
it('should see details of network upgrade proposal', function () {
mockNetworkUpgradeProposal();
navigateTo(navigation.proposals);
@@ -228,6 +230,7 @@ context(
cy.getByTestId(closedProposalToggle).should('not.exist');
});
// 3009-NTWU-001 3009-NTWU-002 3009-NTWU-006 3009-NTWU-009
it('should display network upgrade banner with estimate', function () {
mockNetworkUpgradeProposal();
cy.visit('/');
@@ -259,10 +262,18 @@ context(
.as('displayedEstimate');
cy.get('@displayedEstimate').then((estimateText) => {
// Estimated time should automatically update every second
cy.getByTestId('protocol-upgrade-time')
cy.getByTestId(protocolUpgradeTime)
.invoke('text')
.should('not.eq', estimateText);
});
// time estimate on proposal detail
cy.getByTestId('protocol-upgrade-proposal').within(() => {
cy.get('@displayedEstimate').then((estimateText) => {
cy.getByTestId(protocolUpgradeTime)
.invoke('text')
.should('not.eq', estimateText);
});
});
});
}
);
@@ -0,0 +1 @@
export * from './json-diff';
@@ -0,0 +1,30 @@
import { render, screen, cleanup } from '@testing-library/react';
import { JsonDiff } from './json-diff';
describe('JsonDiff', () => {
afterEach(cleanup);
it('renders without crashing', () => {
render(<JsonDiff left={{}} right={{}} />);
expect(screen.getByTestId('json-diff')).toBeInTheDocument();
});
it('shows the correct message when both objects are identical', () => {
render(<JsonDiff left={{}} right={{}} />);
expect(screen.getByText('Data is identical')).toBeInTheDocument();
});
it('does not show the "identical" message when both objects are not identical', () => {
render(
<JsonDiff
left={{
name: 'test',
}}
right={{
name: 'test2',
}}
/>
);
expect(screen.queryByText('Data is identical')).not.toBeInTheDocument();
});
});
@@ -0,0 +1,46 @@
import { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { formatters, create } from 'jsondiffpatch';
import 'jsondiffpatch/dist/formatters-styles/html.css';
import 'jsondiffpatch/dist/formatters-styles/annotated.css';
export type JsonValue =
| string
| number
| boolean
| null
| JsonValue[]
| { [key: string]: JsonValue };
interface JsonDiffProps {
left: JsonValue;
right: JsonValue;
objectHash?: (obj: unknown) => string | undefined;
}
export const JsonDiff = ({ right, left, objectHash }: JsonDiffProps) => {
const { t } = useTranslation();
const [html, setHtml] = useState<string | undefined>();
useEffect(() => {
const delta = create({
objectHash,
}).diff(left, right);
if (delta) {
const deltaHtml = formatters.html.format(delta, left);
formatters.html.hideUnchanged();
setHtml(deltaHtml);
} else {
setHtml(undefined);
}
}, [right, left, objectHash]);
return html ? (
<div data-testid="json-diff" dangerouslySetInnerHTML={{ __html: html }} />
) : (
<p data-testid="json-diff">{t('dataIsIdentical')}</p>
);
};
+2 -7
View File
@@ -18,10 +18,7 @@ import { VegaWallet } from '../vega-wallet';
import { useLocation, useMatch } from 'react-router-dom';
import { useEffect } from 'react';
import { useTelemetryDialog } from '../telemetry-dialog/telemetry-dialog';
import {
ProtocolUpgradeCountdown,
ProtocolUpgradeCountdownMode,
} from '@vegaprotocol/proposals';
import { ProtocolUpgradeCountdown } from '@vegaprotocol/proposals';
export const SettingsLink = () => {
const { open, isOpen, close } = useTelemetryDialog();
@@ -68,9 +65,7 @@ export const Nav = ({ theme }: Pick<NavigationProps, 'theme'>) => {
actions={
<>
<SettingsLink />
<ProtocolUpgradeCountdown
mode={ProtocolUpgradeCountdownMode.IN_ESTIMATED_TIME_REMAINING}
/>
<ProtocolUpgradeCountdown />
</>
}
>
@@ -853,5 +853,7 @@
"consensusNodes": "consensus nodes",
"activeNodes": "active nodes",
"Estimated time to upgrade": "Estimated time to upgrade",
"Upgraded at": "Upgraded at"
"Upgraded at": "Upgraded at",
"dataIsIdentical": "Data is identical",
"updatesToMarket": "Updates to market"
}
+1 -1
View File
@@ -20,6 +20,6 @@ export const downloadJson = (jsonString: string, proposalTitle: string) => {
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
} catch (error) {
console.log(error);
console.error(error);
}
};
@@ -0,0 +1 @@
export * from './proposal-market-changes';
@@ -0,0 +1,91 @@
import { render, fireEvent } from '@testing-library/react';
import {
ProposalMarketChanges,
applyImmutableKeysFromEarlierVersion,
} from './proposal-market-changes';
import type { JsonValue } from '../../../../components/json-diff';
describe('applyImmutableKeysFromEarlierVersion', () => {
it('returns an empty object if any argument is not an object or null', () => {
const earlierVersion: JsonValue = null;
const updatedVersion: JsonValue = null;
expect(
applyImmutableKeysFromEarlierVersion(earlierVersion, updatedVersion)
).toEqual({});
});
it('overrides updatedVersion with values from earlierVersion for immutable keys', () => {
const earlierVersion: JsonValue = {
decimalPlaces: 2,
positionDecimalPlaces: 3,
instrument: {
name: 'Instrument1',
future: {
settlementAsset: 'Asset1',
},
},
};
const updatedVersion: JsonValue = {
decimalPlaces: 3, // should be overridden by 2
positionDecimalPlaces: 4, // should be overridden by 3
instrument: {
name: 'Instrument2', // should be overridden by 'Instrument1'
future: {
settlementAsset: 'Asset2', // should be overridden by 'Asset1'
},
},
};
const expected: JsonValue = {
decimalPlaces: 2,
positionDecimalPlaces: 3,
instrument: {
name: 'Instrument1',
future: {
settlementAsset: 'Asset1',
},
},
};
expect(
applyImmutableKeysFromEarlierVersion(earlierVersion, updatedVersion)
).toEqual(expected);
});
});
describe('ProposalMarketChanges', () => {
it('renders correctly', () => {
const { getByTestId } = render(
<ProposalMarketChanges
originalProposal={{}}
latestEnactedProposal={{}}
updatedProposal={{}}
/>
);
expect(getByTestId('proposal-market-changes')).toBeInTheDocument();
});
it('JsonDiff is not visible when showChanges is false', () => {
const { queryByTestId } = render(
<ProposalMarketChanges
originalProposal={{}}
latestEnactedProposal={{}}
updatedProposal={{}}
/>
);
expect(queryByTestId('json-diff')).not.toBeInTheDocument();
});
it('JsonDiff is visible when showChanges is true', async () => {
const { getByTestId } = render(
<ProposalMarketChanges
originalProposal={{}}
latestEnactedProposal={{}}
updatedProposal={{}}
/>
);
fireEvent.click(getByTestId('proposal-market-changes-toggle'));
expect(getByTestId('json-diff')).toBeInTheDocument();
});
});
@@ -0,0 +1,86 @@
import cloneDeep from 'lodash/cloneDeep';
import set from 'lodash/set';
import get from 'lodash/get';
import { JsonDiff } from '../../../../components/json-diff';
import { useTranslation } from 'react-i18next';
import { useState } from 'react';
import { CollapsibleToggle } from '../../../../components/collapsible-toggle';
import { SubHeading } from '../../../../components/heading';
import type { JsonValue } from '../../../../components/json-diff';
const immutableKeys = [
'decimalPlaces',
'positionDecimalPlaces',
'instrument.name',
'instrument.future.settlementAsset',
];
export const applyImmutableKeysFromEarlierVersion = (
earlierVersion: JsonValue,
updatedVersion: JsonValue
) => {
if (
typeof earlierVersion !== 'object' ||
earlierVersion === null ||
typeof updatedVersion !== 'object' ||
updatedVersion === null
) {
// If either version is not an object or is null, return null or throw an error
return {};
}
const updatedVersionCopy = cloneDeep(updatedVersion);
// Overwrite the immutable keys in the updatedVersionCopy with the earlier values
immutableKeys.forEach((key) => {
set(updatedVersionCopy, key, get(earlierVersion, key));
});
return updatedVersionCopy;
};
interface ProposalMarketChangesProps {
originalProposal: JsonValue;
latestEnactedProposal: JsonValue | undefined;
updatedProposal: JsonValue;
}
export const ProposalMarketChanges = ({
originalProposal,
latestEnactedProposal,
updatedProposal,
}: ProposalMarketChangesProps) => {
const { t } = useTranslation();
const [showChanges, setShowChanges] = useState(false);
return (
<section data-testid="proposal-market-changes">
<CollapsibleToggle
toggleState={showChanges}
setToggleState={setShowChanges}
dataTestId={'proposal-market-changes-toggle'}
>
<SubHeading title={t('updatesToMarket')} />
</CollapsibleToggle>
{showChanges && (
<div className="mb-6">
<JsonDiff
left={latestEnactedProposal || originalProposal}
right={
latestEnactedProposal
? applyImmutableKeysFromEarlierVersion(
latestEnactedProposal,
updatedProposal
)
: applyImmutableKeysFromEarlierVersion(
originalProposal,
updatedProposal
)
}
/>
</div>
)}
</section>
);
};
@@ -17,6 +17,7 @@ import type { MarketInfoWithData } from '@vegaprotocol/markets';
import type { AssetQuery } from '@vegaprotocol/assets';
import { removePaginationWrapper } from '@vegaprotocol/utils';
import { ProposalState } from '@vegaprotocol/types';
import { ProposalMarketChanges } from '../proposal-market-changes';
import type { NetworkParamsResult } from '@vegaprotocol/network-parameters';
export enum ProposalType {
@@ -34,6 +35,10 @@ export interface ProposalProps {
assetData?: AssetQuery | null;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
restData: any;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
originalMarketProposalRestData?: any;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
mostRecentlyEnactedAssociatedMarketProposal?: any;
}
export const Proposal = ({
@@ -42,6 +47,8 @@ export const Proposal = ({
restData,
newMarketData,
assetData,
originalMarketProposalRestData,
mostRecentlyEnactedAssociatedMarketProposal,
}: ProposalProps) => {
const { t } = useTranslation();
@@ -154,6 +161,24 @@ export const Proposal = ({
</div>
)}
{proposal.terms.change.__typename === 'UpdateMarket' && (
<div className="mb-4">
<ProposalMarketChanges
originalProposal={
originalMarketProposalRestData?.data?.proposal?.terms?.newMarket
?.changes || {}
}
latestEnactedProposal={
mostRecentlyEnactedAssociatedMarketProposal?.node?.proposal
?.terms?.updateMarket?.changes || {}
}
updatedProposal={
restData?.data?.proposal?.terms?.updateMarket?.changes || {}
}
/>
</div>
)}
{(proposal.terms.change.__typename === 'NewAsset' ||
proposal.terms.change.__typename === 'UpdateAsset') &&
asset && (
@@ -1,5 +1,5 @@
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
import { useEffect } from 'react';
import { useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
import { Proposal } from '../components/proposal';
@@ -16,7 +16,12 @@ import {
} from '@vegaprotocol/network-parameters';
export const ProposalContainer = () => {
const [
mostRecentlyEnactedAssociatedMarketProposal,
setMostRecentlyEnactedAssociatedMarketProposal,
] = useState(undefined);
const params = useParams<{ proposalId: string }>();
const {
params: networkParams,
loading: networkParamsLoading,
@@ -37,9 +42,11 @@ export const ProposalContainer = () => {
NetworkParams.governance_proposal_updateNetParam_requiredMajority,
NetworkParams.governance_proposal_freeform_requiredMajority,
]);
const {
state: { data: restData },
state: { data: restData, loading: restLoading, error: restError },
} = useFetch(`${ENV.rest}governance?proposalId=${params.proposalId}`);
const { data, loading, error, refetch } = useProposalQuery({
fetchPolicy: 'network-only',
errorPolicy: 'ignore',
@@ -47,6 +54,35 @@ export const ProposalContainer = () => {
skip: !params.proposalId,
});
const {
state: {
data: originalMarketProposalRestData,
loading: originalMarketProposalRestLoading,
error: originalMarketProposalRestError,
},
} = useFetch(
`${ENV.rest}governance?proposalId=${
data?.proposal?.terms.change.__typename === 'UpdateMarket' &&
data?.proposal.terms.change.marketId
}`,
undefined,
true,
data?.proposal?.terms.change.__typename !== 'UpdateMarket'
);
const {
state: {
data: previouslyEnactedMarketProposalsRestData,
loading: previouslyEnactedMarketProposalsRestLoading,
error: previouslyEnactedMarketProposalsRestError,
},
} = useFetch(
`${ENV.rest}governances?proposalState=STATE_ENACTED&proposalType=TYPE_UPDATE_MARKET`,
undefined,
true,
data?.proposal?.terms.change.__typename !== 'UpdateMarket'
);
const {
data: newMarketData,
loading: newMarketLoading,
@@ -79,6 +115,39 @@ export const ProposalContainer = () => {
),
});
useEffect(() => {
if (
previouslyEnactedMarketProposalsRestData &&
data?.proposal?.terms.change.__typename === 'UpdateMarket'
) {
const change = data?.proposal?.terms?.change as { marketId: string };
const filteredProposals =
// @ts-ignore rest data is not typed
previouslyEnactedMarketProposalsRestData.connection.edges.filter(
// @ts-ignore rest data is not typed
({ node }) =>
node?.proposal?.terms?.updateMarket?.marketId === change.marketId
);
const sortedProposals = filteredProposals.sort(
// @ts-ignore rest data is not typed
(a, b) =>
new Date(a?.node?.terms?.enactmentTimestamp).getTime() -
new Date(b?.node?.terms?.enactmentTimestamp).getTime()
);
setMostRecentlyEnactedAssociatedMarketProposal(
sortedProposals[sortedProposals.length - 1]
);
}
}, [
previouslyEnactedMarketProposalsRestData,
params.proposalId,
data?.proposal?.terms.change.__typename,
data?.proposal?.terms.change,
]);
useEffect(() => {
const interval = setInterval(refetch, 2000);
return () => clearInterval(interval);
@@ -87,14 +156,39 @@ export const ProposalContainer = () => {
return (
<AsyncRenderer
loading={
loading || newMarketLoading || assetLoading || networkParamsLoading
loading ||
newMarketLoading ||
assetLoading ||
networkParamsLoading ||
(restLoading ? (restLoading as boolean) : false) ||
(originalMarketProposalRestLoading
? (originalMarketProposalRestLoading as boolean)
: false) ||
(previouslyEnactedMarketProposalsRestLoading
? (previouslyEnactedMarketProposalsRestLoading as boolean)
: false)
}
error={
error ||
newMarketError ||
assetError ||
restError ||
originalMarketProposalRestError ||
previouslyEnactedMarketProposalsRestError ||
networkParamsError
}
error={error || newMarketError || assetError || networkParamsError}
data={{
...data,
...networkParams,
...(newMarketData ? { newMarketData } : {}),
...(assetData ? { assetData } : {}),
...(restData ? { restData } : {}),
...(originalMarketProposalRestData
? { originalMarketProposalRestData }
: {}),
...(previouslyEnactedMarketProposalsRestData
? { previouslyEnactedMarketProposalsRestData }
: {}),
}}
>
{data?.proposal ? (
@@ -104,6 +198,10 @@ export const ProposalContainer = () => {
restData={restData}
newMarketData={newMarketData}
assetData={assetData}
originalMarketProposalRestData={originalMarketProposalRestData}
mostRecentlyEnactedAssociatedMarketProposal={
mostRecentlyEnactedAssociatedMarketProposal
}
/>
) : (
<ProposalNotFound />
@@ -5,7 +5,6 @@ import {
RewardsTable,
} from '../shared-rewards-table-assets/shared-rewards-table-assets';
import type { EpochIndividualReward } from './generate-epoch-individual-rewards-list';
import { useTranslation } from 'react-i18next';
interface EpochIndividualRewardsGridProps {
data: EpochIndividualReward;
@@ -22,14 +21,11 @@ interface RewardItemProps {
const DisplayReward = ({
reward,
decimals,
percentageOfTotal,
}: {
reward: string;
decimals: number;
percentageOfTotal?: string;
}) => {
const { t } = useTranslation();
if (Number(reward) === 0) {
return <span className="text-vega-dark-300">-</span>;
}
@@ -39,23 +35,12 @@ const DisplayReward = ({
description={
<div className="flex flex-col items-start">
<span>{formatNumber(toBigNum(reward, decimals), decimals)}</span>
{percentageOfTotal && (
<span className="text-vega-dark-300">
({percentageOfTotal}% {t('ofTotalDistributed')})
</span>
)}
</div>
}
>
<button>
<div className="flex flex-col items-start">
<span>{formatNumber(toBigNum(reward, decimals), 4)}</span>
{percentageOfTotal && (
<span className="text-vega-dark-300">
({formatNumber(percentageOfTotal, 4).toString()}
%)
</span>
)}
</div>
</button>
</Tooltip>
@@ -168,17 +168,6 @@ describe('generateEpochIndividualRewardsList', () => {
expect(result2[1].epoch).toEqual(1);
});
it('correctly calculates the total value of rewards for an asset', () => {
const rewards = [reward1, reward4];
const result = generateEpochIndividualRewardsList({
rewards,
epochId: 1,
epochRewardSummaries: [],
});
expect(result[0].rewards[0].totalAmount).toEqual('200');
});
it('returns data in the expected shape', () => {
// Just sanity checking the whole structure here
const rewards = [reward1, reward2, reward3, reward4];
@@ -458,69 +447,4 @@ describe('generateEpochIndividualRewardsList', () => {
},
]);
});
it('correctly calculates the percentage of two or more rewards by referencing the total rewards amount', () => {
const result = generateEpochIndividualRewardsList({
rewards: [
// reward1 is 100 usd, which is 10% of the total rewards amount
reward1,
{
rewardType: AccountType.ACCOUNT_TYPE_GLOBAL_REWARD,
amount: '200',
percentageOfTotal: '0.2',
receivedAt: new Date(),
asset: { id: 'usd', symbol: 'USD', name: 'USD', decimals: 6 },
party: { id: 'blah' },
epoch: { id: '1' },
},
],
epochId: 1,
epochRewardSummaries: [
{
__typename: 'EpochRewardSummary',
epoch: 1,
assetId: 'usd',
amount: '1000',
rewardType: AccountType.ACCOUNT_TYPE_GLOBAL_REWARD,
},
],
});
expect(result[0]).toEqual({
epoch: 1,
rewards: [
{
asset: 'USD',
decimals: 6,
totalAmount: '300',
rewardTypes: {
[AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_GLOBAL_REWARD]: {
amount: '300',
percentageOfTotal: '30',
},
[AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS]: {
amount: '0',
percentageOfTotal: '0',
},
},
},
],
});
});
});
@@ -59,7 +59,6 @@ export const generateEpochIndividualRewardsList = ({
const epochIndividualRewards = rewards.reduce((acc, reward) => {
const epochId = reward.epoch.id;
const assetName = reward.asset.name;
const assetId = reward.asset.id;
const assetDecimals = reward.asset.decimals;
const rewardType = reward.rewardType;
const amount = reward.amount;
@@ -77,13 +76,6 @@ export const generateEpochIndividualRewardsList = ({
const epoch = acc.get(epochId);
// matchingTotalReward is the total awarded for all users for the reward type in the epoch of the asset
const matchingTotalRewardAmount = epochRewardSummaries.find(
(summary) =>
summary.epoch === Number(epochId) &&
summary.assetId === assetId &&
summary.rewardType === rewardType
)?.amount;
let asset = epoch?.rewards.find((r) => r.asset === assetName);
if (!asset) {
@@ -106,14 +98,7 @@ export const generateEpochIndividualRewardsList = ({
asset.rewardTypes[rewardType] = {
amount: newAmount,
percentageOfTotal: matchingTotalRewardAmount
? new BigNumber(newAmount)
.dividedBy(matchingTotalRewardAmount)
.multipliedBy(100)
.toString()
: // this should never be reached, if there's an individual reward there should
// always be a reward total from the api too, but set it as a fallback just in case
percentageOfTotal,
percentageOfTotal: percentageOfTotal,
};
}
@@ -61,8 +61,6 @@ export const RewardsPage = () => {
error: paramsError,
} = useNetworkParams([NetworkParams.reward_staking_delegation_payoutDelay]);
console.log('params', params);
const payoutDuration = useMemo(() => {
if (!params) {
return 0;
@@ -8,9 +8,10 @@ import { TxState } from '../../../hooks/transaction-reducer';
import { useTransaction } from '../../../hooks/use-transaction';
import { BigNumber } from '../../../lib/bignumber';
import { AssociateInfo } from './associate-info';
import { removeDecimal, toBigNum } from '@vegaprotocol/utils';
import { toBigNum } from '@vegaprotocol/utils';
import type { EthereumConfig } from '@vegaprotocol/web3';
import { useBalances } from '../../../lib/balances/balances-store';
import { MaxUint256 } from '@ethersproject/constants';
export const WalletAssociate = ({
perform,
@@ -42,7 +43,7 @@ export const WalletAssociate = ({
} = useTransaction(() => {
return token.approve(
ethereumConfig.staking_bridge_contract.address,
removeDecimal('1000000', decimals).toString()
MaxUint256.toString()
);
});
+31 -1
View File
@@ -62,7 +62,7 @@
cursor: pointer;
}
/* Styles required to (effectively) un-override the
/* Styles required to (effectively) un-override the
* reset styles so that the Proposal description fields
* render as you'd expect them to.
*
@@ -97,3 +97,33 @@
.react-markdown-container ul li {
list-style: circle;
}
.jsondiffpatch-delta,
.jsondiffpatch-delta pre {
font-family: 'Roboto Mono', monospace !important;
}
.jsondiffpatch-delta pre {
padding: 0 0.25em !important;
}
.jsondiffpatch-added .jsondiffpatch-property-name,
.jsondiffpatch-added .jsondiffpatch-value pre,
.jsondiffpatch-modified .jsondiffpatch-right-value pre,
.jsondiffpatch-textdiff-added {
background: theme(colors.vega.green[650]) !important;
color: theme(colors.white) !important;
}
.jsondiffpatch-deleted .jsondiffpatch-property-name,
.jsondiffpatch-deleted pre,
.jsondiffpatch-modified .jsondiffpatch-left-value pre,
.jsondiffpatch-textdiff-deleted {
background: theme(colors.vega.pink[650]) !important;
color: theme(colors.white) !important;
}
.jsondiffpatch-moved .jsondiffpatch-moved-destination {
background: theme(colors.vega.yellow[350]) !important;
color: theme(colors.vega.dark[200]) !important;
}
@@ -535,6 +535,7 @@ function checkIfDataAndTimeOfCreationAndUpdateIsEqual(date: string) {
// unexpected latency
const minBefore = subSeconds(new Date(), 5);
const maxAfter = addSeconds(new Date(), 5);
// eslint-disable-next-line no-console
console.log(maxAfter);
const date = new Date($dateTime.toString());
expect(isAfter(date, minBefore) && isBefore(date, maxAfter)).to.equal(
@@ -68,22 +68,4 @@ describe('home', { tags: '@regression' }, () => {
cy.getByTestId('connect').click();
});
});
describe('Network switcher', () => {
before(() => {
cy.mockTradingPage();
cy.mockSubscription();
cy.visit('/');
});
it('switch to fairground network and check status & incidents link', () => {
// 0006-NETW-002
// 0006-NETW-003
cy.getByTestId('navigation')
.find('[data-testid="network-switcher"]')
.should('have.text', 'Custom')
.click();
cy.getByTestId('network-item').contains('Fairground testnet');
});
});
});
@@ -84,7 +84,7 @@ describe('markets proposed table', { tags: '@smoke' }, () => {
.find('button')
.click();
const dropdownContent = '[data-testid="market-actions-content"]';
const dropdownContent = '[data-testid="proposal-actions-content"]';
const dropdownContentItem = '[role="menuitem"]';
// 6001-MARK-059
@@ -100,7 +100,7 @@ describe('markets proposed table', { tags: '@smoke' }, () => {
'VEGA_TOKEN_URL'
)}/proposals/e9ec6d5c46a7e7bcabf9ba7a893fa5a5eeeec08b731f06f7a6eb7bf0e605b829`
);
cy.getByTestId('market-actions-content').click();
cy.getByTestId('proposal-actions-content').click();
});
// 6001-MARK-060
@@ -1,114 +0,0 @@
import { mockConnectWallet } from '@vegaprotocol/cypress';
describe('Navbar', { tags: '@smoke' }, () => {
beforeEach(() => {
cy.clearAllLocalStorage();
cy.mockTradingPage();
cy.mockSubscription();
cy.visit('/');
cy.wait('@Markets');
cy.wait('@MarketsData');
});
const pages = [
{ name: 'Markets', link: '#/markets/all' },
{ name: 'Trading', link: '#/markets' },
{ name: 'Portfolio', link: '#/portfolio' },
];
describe('desktop view', () => {
pages.forEach(({ name, link }) => {
it(`${name} should be correctly rendered`, () => {
cy.get('nav')
.find(`a[data-testid=${name}]:visible`)
.then((element) => {
cy.wrap(element).click();
cy.location('hash').should('contain', link);
});
});
});
it('Resources dropdown should be correctly rendered', () => {
const resourceSelector = 'ul li:contains(Resources)';
['Docs', 'Give Feedback'].forEach((text, index) => {
cy.get('nav').find(resourceSelector).contains('Resources').click();
cy.get('nav')
.find(resourceSelector)
.find('.navigation-content li')
.eq(index)
.find('a')
.then((element) => {
expect(element.attr('target')).to.eq('_blank');
expect(element.attr('href')).to.not.be.empty;
expect(element.text()).to.eq(text);
});
});
});
it('Disclaimer should be presented after choosing from menu', () => {
cy.get('nav')
.find('ul li:contains(Resources)')
.contains('Resources')
.click();
cy.getByTestId('Disclaimer').eq(0).click();
cy.location('hash').should('equal', '#/disclaimer');
cy.get('p').contains(
'Vega is a decentralised peer-to-peer protocol that can be used to trade derivatives with cryptoassets.'
);
});
});
describe('mobile view', () => {
const viewportHeight = Cypress.config('viewportHeight');
const viewportWidth = Cypress.config('viewportWidth');
before(() => {
// a little hack to keep the viewport size between tests (cypress bug)
Cypress.config({
viewportWidth: 560,
viewportHeight: 890,
});
cy.viewport(560, 890);
});
describe('wallet drawer', () => {
it('wallet drawer should be correctly rendered', () => {
mockConnectWallet();
cy.connectVegaWallet(true);
cy.getByTestId('connect-vega-wallet-mobile').click();
cy.getByTestId('wallets-drawer').should('be.visible');
cy.getByTestId('wallets-drawer').within((el) => {
cy.wrap(el).get('button').contains('Disconnect').click();
});
cy.getByTestId('wallets-drawer').should('not.be.visible');
});
});
describe('menu drawer', () => {
pages.forEach(({ name, link }) => {
it(`${name} should be correctly rendered`, () => {
cy.getByTestId('button-menu-drawer').click();
cy.getByTestId('menu-drawer').should('be.visible');
cy.getByTestId('menu-drawer').within((el) => {
cy.wrap(el).getByTestId(name).click();
cy.location('hash').should('contain', link);
});
});
});
it('Menu drawer should not be visible until opened', () => {
cy.getByTestId('menu-drawer').should('not.be.visible');
cy.getByTestId('button-menu-drawer').click();
cy.getByTestId('menu-drawer').should('be.visible');
cy.getByTestId('button-menu-drawer').click();
cy.getByTestId('menu-drawer').should('not.be.visible');
});
});
after(() => {
// a little hack to keep the viewport size between tests (cypress bug)
Cypress.config({
viewportWidth,
viewportHeight,
});
});
});
});
@@ -171,6 +171,7 @@ describe(
.invoke('text')
.then((text) => {
const actualDate = text.slice(0, -67);
// eslint-disable-next-line no-console
console.log(actualDate);
const actualOhlc = text.slice(-67);
assert.isTrue(expectedDateRegex.test(actualDate));
@@ -7,7 +7,7 @@ const closePosition = 'close-position';
const dialogCloseX = 'dialog-close';
const dialogContent = 'dialog-content';
const dropDownMenu = 'dropdown-menu';
const marketActionsContent = 'market-actions-content';
const marketActionsContent = 'position-actions-content';
const positions = 'Positions';
const tabPositions = 'tab-positions';
const toastContent = 'toast-content';
@@ -17,7 +17,10 @@ import {
usePaneLayout,
} from '../../components/resizable-grid';
import { TradingViews } from './trade-views';
import { MarketSuccessorBanner } from '../../components/market-banner';
import {
MarketSuccessorBanner,
MarketSuccessorProposalBanner,
} from '../../components/market-banner';
import { FLAGS } from '@vegaprotocol/environment';
interface TradeGridProps {
@@ -162,7 +165,12 @@ export const TradeGrid = ({ market, pinnedAsset }: TradeGridProps) => {
return (
<div className={wrapperClasses}>
<div>
{FLAGS.SUCCESSOR_MARKETS && <MarketSuccessorBanner market={market} />}
{FLAGS.SUCCESSOR_MARKETS && (
<>
<MarketSuccessorBanner market={market} />
<MarketSuccessorProposalBanner marketId={market?.id} />
</>
)}
<OracleBanner marketId={market?.id || ''} />
</div>
<div className="min-h-0 p-0.5">
@@ -12,7 +12,10 @@ import { Splash } from '@vegaprotocol/ui-toolkit';
import { NO_MARKET } from './constants';
import AutoSizer from 'react-virtualized-auto-sizer';
import classNames from 'classnames';
import { MarketSuccessorBanner } from '../../components/market-banner';
import {
MarketSuccessorBanner,
MarketSuccessorProposalBanner,
} from '../../components/market-banner';
import { FLAGS } from '@vegaprotocol/environment';
interface TradePanelsProps {
@@ -65,7 +68,12 @@ export const TradePanels = ({
return (
<div className="h-full grid grid-rows-[min-content_1fr_min-content]">
<div>
{FLAGS.SUCCESSOR_MARKETS && <MarketSuccessorBanner market={market} />}
{FLAGS.SUCCESSOR_MARKETS && (
<>
<MarketSuccessorBanner market={market} />
<MarketSuccessorProposalBanner marketId={market?.id} />
</>
)}
<OracleBanner marketId={market?.id || ''} />
</div>
<div className="h-full">
@@ -80,9 +88,12 @@ export const TradePanels = ({
<div className="flex flex-nowrap overflow-x-auto max-w-full border-t border-default">
{Object.keys(TradingViews).map((key) => {
const isActive = view === key;
const className = classNames('p-4 min-w-[100px] capitalize', {
'bg-vega-clight-500 dark:bg-vega-cdark-500': isActive,
});
const className = classNames(
'py-2 px-4 min-w-[100px] capitalize text-sm',
{
'bg-vega-clight-500 dark:bg-vega-cdark-500': isActive,
}
);
return (
<button
data-testid={key}
+1 -1
View File
@@ -17,7 +17,7 @@ export const Header = ({ title, children }: TradeMarketHeaderProps) => {
);
return (
<header className={headerClasses}>
<div className="flex flex-col justify-center items-start pl-3 lg:pl-4 pt-2 xl:pb-2 pb-0">
<div className="hidden lg:flex flex-col justify-center items-start pl-3 lg:pl-4 pt-2 xl:pb-2 pb-0">
{title}
</div>
<div data-testid="header-summary" className="min-w-0">
+3 -1
View File
@@ -1,10 +1,12 @@
import classNames from 'classnames';
export const WalletIcon = ({ className }: { className?: string }) => {
return (
<svg
width="26"
height="18"
viewBox="0 0 26 18"
className={className}
className={classNames('fill-current', className)}
data-testid="wallet-icon"
>
<path d="M4.77437 17.7499H4.74987C3.6504 17.7368 2.77439 16.8489 2.77439 15.7772V12.8495V12.6343L2.5615 12.6023C1.59672 12.4575 0.849609 11.6116 0.849609 10.6266V7.40064C0.849609 6.39018 1.59509 5.56985 2.56147 5.4249L2.77439 5.39297V5.17767V2.24998C2.77439 1.14102 3.66537 0.25 4.77437 0.25H23.7501C24.8591 0.25 25.7501 1.14098 25.7501 2.24998V15.7499C25.7501 16.8588 24.8591 17.7499 23.7501 17.7499H4.77437ZM4.44917 12.5992H4.19917L4.77441 16.075V16.325H4.77466H23.7502C24.0778 16.325 24.3254 16.0777 24.3254 15.7497V2.24984C24.3254 1.9222 24.0782 1.6746 23.7502 1.6746H4.77441C4.44677 1.6746 4.19917 1.92182 4.19917 2.24984V5.12306V5.37306H4.44917H7.0244C8.51139 5.37306 9.67508 6.56094 9.67508 8.02374V9.94852C9.67508 11.4355 8.4872 12.5992 7.0244 12.5992H4.44917ZM2.84963 6.8253C2.52199 6.8253 2.27439 7.07253 2.27439 7.40054V10.6264C2.27439 10.9541 2.52161 11.2017 2.84962 11.2017L7.02419 11.2019C7.73619 11.2019 8.25009 10.6515 8.25009 9.97598V8.0512C8.25009 7.3392 7.69976 6.8253 7.0242 6.8253H2.84963Z" />
@@ -11,9 +11,9 @@ export const LayoutWithSidebar = () => {
const gridClasses = classNames(
'h-full relative z-0 grid',
'grid-rows-[min-content_1fr]',
'grid-cols-[1fr_45px]',
'lg:grid-cols-[1fr_350px_45px]'
'grid-rows-[min-content_1fr_40px]',
'lg:grid-rows-[min-content_1fr]',
'lg:grid-cols-[1fr_350px_40px]'
);
return (
@@ -40,7 +40,14 @@ export const LayoutWithSidebar = () => {
>
<SidebarContent />
</aside>
<div className="col-start-2 lg:col-start-3 bg-vega-clight-800 dark:bg-vega-cdark-800 border-l border-default">
<div
className={classNames(
'bg-vega-clight-800 dark:bg-vega-cdark-800',
'border-t lg:border-l lg:border-t-0 border-default',
'row-start-3 col-start-1 cols-span-full',
'lg:row-start-2 lg:row-span-full lg:col-start-3'
)}
>
<Sidebar />
</div>
</div>
@@ -42,8 +42,6 @@ export const LiquidityHeader = () => {
triggeringRatio,
});
console.log(market);
return (
<Header
title={
@@ -1 +1,2 @@
export * from './market-successor-banner';
export * from './market-successor-proposal-banner';
@@ -0,0 +1,155 @@
import { act, render, screen, waitFor } from '@testing-library/react';
import type { SingleExecutionResult } from '@apollo/client';
import type { MockedResponse } from '@apollo/react-testing';
import { MockedProvider } from '@apollo/react-testing';
import { MarketSuccessorProposalBanner } from './market-successor-proposal-banner';
import type { SuccessorProposalsListQuery } from '@vegaprotocol/proposals';
import { SuccessorProposalsListDocument } from '@vegaprotocol/proposals';
const marketProposalMock: MockedResponse<SuccessorProposalsListQuery> = {
request: {
query: SuccessorProposalsListDocument,
},
result: {
data: {
proposalsConnection: {
edges: [
{
node: {
__typename: 'Proposal',
id: 'proposal-1',
terms: {
__typename: 'ProposalTerms',
change: {
__typename: 'NewMarket',
instrument: {
name: 'New proposal of the market successor',
},
successorConfiguration: {
parentMarketId: 'marketId',
},
},
},
},
},
],
},
},
},
};
describe('MarketSuccessorProposalBanner', () => {
it('should display single proposal', async () => {
render(
<MockedProvider mocks={[marketProposalMock]}>
<MarketSuccessorProposalBanner marketId="marketId" />
</MockedProvider>
);
await waitFor(() => {
expect(
screen.getByText('A successors to this market has been proposed')
).toBeInTheDocument();
});
expect(
screen
.getByRole('link')
.getAttribute('href')
?.endsWith('/proposals/proposal-1') ?? false
).toBe(true);
});
it('should display plural proposals', async () => {
const dualProposalMock = {
...marketProposalMock,
result: {
...marketProposalMock.result,
data: {
proposalsConnection: {
edges: [
...((
marketProposalMock?.result as SingleExecutionResult<SuccessorProposalsListQuery>
)?.data?.proposalsConnection?.edges ?? []),
{
node: {
__typename: 'Proposal',
id: 'proposal-2',
terms: {
__typename: 'ProposalTerms',
change: {
__typename: 'NewMarket',
instrument: {
name: 'New second proposal of the market successor',
},
successorConfiguration: {
parentMarketId: 'marketId',
},
},
},
},
},
],
},
},
},
};
render(
<MockedProvider mocks={[dualProposalMock]}>
<MarketSuccessorProposalBanner marketId="marketId" />
</MockedProvider>
);
await waitFor(() => {
expect(
screen.getByText('Successors to this market have been proposed')
).toBeInTheDocument();
});
expect(
screen
.getAllByRole('link')[0]
.getAttribute('href')
?.endsWith('/proposals/proposal-1') ?? false
).toBe(true);
expect(
screen
.getAllByRole('link')[1]
.getAttribute('href')
?.endsWith('/proposals/proposal-2') ?? false
).toBe(true);
});
it('banner should be hidden because no proposals', () => {
const { container } = render(
<MockedProvider>
<MarketSuccessorProposalBanner marketId="marketId" />
</MockedProvider>
);
expect(container).toBeEmptyDOMElement();
});
it('banner should be hidden because no proposals for the market', () => {
const { container } = render(
<MockedProvider mocks={[marketProposalMock]}>
<MarketSuccessorProposalBanner marketId="otherMarketId" />
</MockedProvider>
);
expect(container).toBeEmptyDOMElement();
});
it('banner should be hidden after user close click', async () => {
const { container } = render(
<MockedProvider mocks={[marketProposalMock]}>
<MarketSuccessorProposalBanner marketId="marketId" />
</MockedProvider>
);
await waitFor(() => {
expect(
screen.getByText('A successors to this market has been proposed')
).toBeInTheDocument();
});
await act(() => {
screen.getByTestId('notification-banner-close').click();
});
await waitFor(() => {
expect(container).toBeEmptyDOMElement();
});
});
});
@@ -0,0 +1,71 @@
import { useState } from 'react';
import type {
SuccessorProposalListFieldsFragment,
NewMarketSuccessorFieldsFragment,
} from '@vegaprotocol/proposals';
import { useSuccessorProposalsListQuery } from '@vegaprotocol/proposals';
import {
ExternalLink,
Intent,
NotificationBanner,
} from '@vegaprotocol/ui-toolkit';
import { t } from '@vegaprotocol/i18n';
import { DApp, TOKEN_PROPOSAL, useLinks } from '@vegaprotocol/environment';
export const MarketSuccessorProposalBanner = ({
marketId,
}: {
marketId?: string;
}) => {
const { data: proposals } = useSuccessorProposalsListQuery({
skip: !marketId,
});
const successors =
proposals?.proposalsConnection?.edges
?.map((item) => item?.node as SuccessorProposalListFieldsFragment)
.filter(
(item: SuccessorProposalListFieldsFragment) =>
(item.terms?.change as NewMarketSuccessorFieldsFragment)
?.successorConfiguration?.parentMarketId === marketId
) ?? [];
const [visible, setVisible] = useState(true);
const tokenLink = useLinks(DApp.Token);
if (visible && successors.length) {
return (
<NotificationBanner
intent={Intent.Primary}
onClose={() => {
setVisible(false);
}}
>
<div className="uppercase mb-1">
{successors.length === 1
? t('A successors to this market has been proposed')
: t('Successors to this market have been proposed')}
</div>
<div>
{successors.length === 1
? t('Check out the terms of the proposal and vote:')
: t('Check out the terms of the proposals and vote:')}{' '}
{successors.map((item, i) => {
const externalLink = tokenLink(
TOKEN_PROPOSAL.replace(':id', item.id || '')
);
return (
<>
<ExternalLink href={externalLink} key={i}>
{
(item.terms?.change as NewMarketSuccessorFieldsFragment)
?.instrument.name
}
</ExternalLink>
{i < successors.length - 1 && ', '}
</>
);
})}
</div>
</NotificationBanner>
);
}
return null;
};
@@ -4,10 +4,12 @@ import { useParams } from 'react-router-dom';
import { MarketSelector } from '../../components/market-selector/market-selector';
import { MarketHeaderStats } from '../../client-pages/market/market-header-stats';
import { useMarket } from '@vegaprotocol/markets';
import { useState } from 'react';
export const MarketHeader = () => {
const { marketId } = useParams();
const { data } = useMarket(marketId);
const [open, setOpen] = useState(false);
if (!data) return null;
@@ -15,6 +17,8 @@ export const MarketHeader = () => {
<Header
title={
<Popover
open={open}
onChange={setOpen}
trigger={
<HeaderTitle>
{data.tradableInstrument.instrument.code}
@@ -23,7 +27,10 @@ export const MarketHeader = () => {
}
alignOffset={-10}
>
<MarketSelector currentMarketId={marketId} />
<MarketSelector
currentMarketId={marketId}
onSelect={() => setOpen(false)}
/>
</Popover>
}
>
@@ -98,6 +98,7 @@ describe('MarketSelectorItem', () => {
market={market}
currentMarketId={market.id}
style={{}}
onSelect={jest.fn()}
/>
</MockedProvider>
</MemoryRouter>
@@ -17,10 +17,12 @@ export const MarketSelectorItem = ({
market,
style,
currentMarketId,
onSelect,
}: {
market: MarketMaybeWithDataAndCandles;
style: CSSProperties;
currentMarketId?: string;
onSelect: (marketId: string) => void;
}) => {
return (
<div style={style} role="row">
@@ -32,6 +34,7 @@ export const MarketSelectorItem = ({
'bg-vega-clight-600 dark:bg-vega-cdark-600':
market.id === currentMarketId,
})}
onClick={() => onSelect(market.id)}
>
<MarketData market={market} />
</Link>
@@ -80,7 +83,7 @@ const MarketData = ({ market }: { market: MarketMaybeWithDataAndCandles }) => {
return (
<>
<div className="w-2/5" role="gridcell">
<h3 className="text-ellipsis whitespace-nowrap overflow-hidden">
<h3 className="text-ellipsis text-sm lg:text-base whitespace-nowrap overflow-hidden">
{market.tradableInstrument.instrument.code}
</h3>
{mode && (
@@ -90,7 +93,7 @@ const MarketData = ({ market }: { market: MarketMaybeWithDataAndCandles }) => {
)}
</div>
<div
className="w-1/5 text-sm whitespace-nowrap text-ellipsis overflow-hidden"
className="w-1/5 text-xs lg:text-sm whitespace-nowrap text-ellipsis overflow-hidden"
title={instrument.product.settlementAsset.symbol}
data-testid="market-selector-price"
role="gridcell"
@@ -98,7 +101,7 @@ const MarketData = ({ market }: { market: MarketMaybeWithDataAndCandles }) => {
{price} {instrument.product.settlementAsset.symbol}
</div>
<div
className="w-1/5 text-sm text-right whitespace-nowrap text-ellipsis overflow-hidden"
className="w-1/5 text-xs lg:text-sm text-right whitespace-nowrap text-ellipsis overflow-hidden"
title={t('24h vol')}
data-testid="market-selector-volume"
role="gridcell"
@@ -137,7 +137,7 @@ describe('MarketSelector', () => {
it('renders only active markets', () => {
render(
<MemoryRouter>
<MarketSelector currentMarketId="market-0" />
<MarketSelector currentMarketId="market-0" onSelect={jest.fn()} />
</MemoryRouter>
);
expect(screen.getAllByTestId(/market-\d/)).toHaveLength(
@@ -148,7 +148,7 @@ describe('MarketSelector', () => {
it('filters by product type', async () => {
render(
<MemoryRouter>
<MarketSelector currentMarketId="market-0" />
<MarketSelector currentMarketId="market-0" onSelect={jest.fn()} />
</MemoryRouter>
);
@@ -174,7 +174,7 @@ describe('MarketSelector', () => {
it('filters by search term', async () => {
render(
<MemoryRouter>
<MarketSelector currentMarketId="market-0" />
<MarketSelector currentMarketId="market-0" onSelect={jest.fn()} />
</MemoryRouter>
);
@@ -202,7 +202,7 @@ describe('MarketSelector', () => {
it('filters by asset', async () => {
render(
<MemoryRouter>
<MarketSelector currentMarketId="market-0" />
<MarketSelector currentMarketId="market-0" onSelect={jest.fn()} />
</MemoryRouter>
);
@@ -234,7 +234,7 @@ describe('MarketSelector', () => {
it('sorts by gained', async () => {
render(
<MemoryRouter>
<MarketSelector currentMarketId="market-0" />
<MarketSelector currentMarketId="market-0" onSelect={jest.fn()} />
</MemoryRouter>
);
@@ -256,7 +256,7 @@ describe('MarketSelector', () => {
it('sorts by lost', async () => {
render(
<MemoryRouter>
<MarketSelector currentMarketId="market-0" />
<MarketSelector currentMarketId="market-0" onSelect={jest.fn()} />
</MemoryRouter>
);
@@ -272,7 +272,7 @@ describe('MarketSelector', () => {
it('sorts by new', async () => {
render(
<MemoryRouter>
<MarketSelector currentMarketId="market-0" />
<MarketSelector currentMarketId="market-0" onSelect={jest.fn()} />
</MemoryRouter>
);
@@ -35,7 +35,7 @@ export const MarketSelector = ({
onSelect,
}: {
currentMarketId?: string;
onSelect?: (marketId: string) => void;
onSelect: (marketId: string) => void;
}) => {
const [filter, setFilter] = useState<Filter>({
searchTerm: '',
@@ -48,7 +48,7 @@ export const MarketSelector = ({
return (
<div data-testid="market-selector">
<div className="pt-2 px-2 mb-2 w-[320px] lg:w-[584px]">
<div className="pt-2 px-2 mb-2">
<ProductSelector
product={filter.product}
onSelect={(product) => {
@@ -147,16 +147,17 @@ const MarketList = ({
loading: boolean;
searchTerm: string;
currentMarketId?: string;
onSelect?: (marketId: string) => void;
onSelect: (marketId: string) => void;
noItems: string;
}) => {
const itemSize = 45;
const listRef = useRef<HTMLDivElement | null>(null);
const rect = listRef.current?.getBoundingClientRect();
// allow virtualized list to grow until it runs out of space
const height = rect
const computedHeight = rect
? Math.min(data.length * itemSize, window.innerHeight - rect.y)
: 400;
const height = Math.max(computedHeight, 45);
if (error) {
return <div>{error.message}</div>;
@@ -199,7 +200,7 @@ const MarketList = ({
interface ListItemData {
data: MarketMaybeWithDataAndCandles[];
onSelect?: (marketId: string) => void;
onSelect: (marketId: string) => void;
currentMarketId?: string;
}
@@ -216,6 +217,7 @@ const ListItem = ({
market={data.data[index]}
currentMarketId={data.currentMarketId}
style={style}
onSelect={data.onSelect}
/>
);
@@ -252,7 +254,11 @@ const List = ({
if (!data.length) {
return (
<div style={{ height }} data-testid="no-items">
<div
style={{ height }}
className="flex items-center"
data-testid="no-items"
>
<div className="mx-4 my-2 text-sm">{noItems}</div>
</div>
);
+1
View File
@@ -1 +1,2 @@
export * from './navbar';
export * from './nav-header';
@@ -0,0 +1,66 @@
import { VegaIcon, VegaIconNames } from '@vegaprotocol/ui-toolkit';
import { MarketSelector } from '../market-selector';
import { useMarket } from '@vegaprotocol/markets';
import { t } from '@vegaprotocol/i18n';
import { useParams } from 'react-router-dom';
import * as PopoverPrimitive from '@radix-ui/react-popover';
import { useState } from 'react';
/**
* This is only rendered for the mobile navigation
*/
export const NavHeader = () => {
const { marketId } = useParams();
const { data } = useMarket(marketId);
const [open, setOpen] = useState(false);
if (!marketId) return null;
return (
<FullScreenPopover
open={open}
onOpenChange={(x) => {
setOpen(x);
}}
trigger={
<h1 className="flex gap-1 sm:gap-2 md:gap-4 items-center text-default text-lg whitespace-nowrap xl:pr-4 xl:border-r border-default">
{data ? data.tradableInstrument.instrument.code : t('Select market')}
<VegaIcon name={VegaIconNames.CHEVRON_DOWN} size={20} />
</h1>
}
>
<MarketSelector
currentMarketId={marketId}
onSelect={() => setOpen(false)}
/>
</FullScreenPopover>
);
};
export interface PopoverProps extends PopoverPrimitive.PopoverProps {
trigger: React.ReactNode | string;
}
export const FullScreenPopover = ({
trigger,
children,
open,
onOpenChange,
}: PopoverProps) => {
return (
<PopoverPrimitive.Root open={open} onOpenChange={onOpenChange}>
<PopoverPrimitive.Trigger data-testid="popover-trigger">
{trigger}
</PopoverPrimitive.Trigger>
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
data-testid="popover-content"
className="w-screen bg-vega-clight-800 dark:bg-vega-cdark-800 text-default border border-default"
sideOffset={5}
>
{children}
</PopoverPrimitive.Content>
</PopoverPrimitive.Portal>
</PopoverPrimitive.Root>
);
};
+145 -29
View File
@@ -1,42 +1,158 @@
import { render, screen } from '@testing-library/react';
import { render, screen, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { MemoryRouter } from 'react-router-dom';
import { MockedProvider } from '@apollo/client/testing';
import type { VegaWalletContextShape } from '@vegaprotocol/wallet';
import { VegaWalletContext } from '@vegaprotocol/wallet';
import { Navbar } from './navbar';
import { useGlobalStore } from '../../stores';
jest.mock('@vegaprotocol/proposals', () => ({
ProtocolUpgradeCountdown: () => null,
}));
describe('Navbar', () => {
const pubKey = 'pubKey';
it('should be properly rendered', () => {
render(
<MockedProvider>
<MemoryRouter>
<VegaWalletContext.Provider
value={{ pubKey } as VegaWalletContextShape}
>
<Navbar theme="dark" />
</VegaWalletContext.Provider>
</MemoryRouter>
</MockedProvider>
const pubKey = '000';
const pubKeys = [
{
publicKey: pubKey,
name: 'Pub key 0',
},
{
publicKey: '111',
name: 'Pub key 1',
},
];
const marketId = 'abc';
const navbarContent = 'navbar-menu-content';
const renderComponent = (
initialEntries?: string[],
walletContext?: Partial<VegaWalletContextShape>
) => {
const context = {
pubKey,
pubKeys,
selectPubKey: jest.fn(),
disconnect: jest.fn(),
...walletContext,
} as VegaWalletContextShape;
return render(
<MemoryRouter initialEntries={initialEntries}>
<VegaWalletContext.Provider value={context}>
<Navbar />
</VegaWalletContext.Provider>
</MemoryRouter>
);
expect(screen.getByTestId('Markets')).toBeInTheDocument();
expect(screen.getByTestId('Trading')).toBeInTheDocument();
expect(screen.getByTestId('Portfolio')).toBeInTheDocument();
};
beforeAll(() => {
useGlobalStore.setState({ marketId });
});
it('should be properly rendered', () => {
renderComponent();
const expectedLinks = [
['/', ''],
['/markets/all', 'Markets'],
[`/markets/${marketId}`, 'Trading'],
['/portfolio', 'Portfolio'],
];
const links = screen.getAllByRole('link');
links.forEach((link, i) => {
const [href, text] = expectedLinks[i];
expect(link).toHaveAttribute('href', href);
expect(link).toHaveTextContent(text);
});
});
it('Markets page route should not match empty market page', () => {
render(
<MockedProvider>
<MemoryRouter initialEntries={['/markets/all']}>
<VegaWalletContext.Provider
value={{ pubKey } as VegaWalletContextShape}
>
<Navbar theme="dark" />
</VegaWalletContext.Provider>
</MemoryRouter>
</MockedProvider>
renderComponent(['/markets/all']);
expect(screen.getByRole('link', { name: 'Markets' })).toHaveClass('active');
expect(screen.getByRole('link', { name: 'Trading' })).not.toHaveClass(
'active'
);
expect(screen.getByTestId('Markets')).toHaveClass('active');
expect(screen.getByTestId('Trading')).not.toHaveClass('active');
});
it('can open menu and navigate on small screens', async () => {
renderComponent();
await userEvent.click(screen.getByRole('button', { name: 'Menu' }));
const menuEl = screen.getByTestId(navbarContent);
expect(menuEl).toBeInTheDocument();
const menu = within(menuEl);
const expectedLinks = [
['/markets/all', 'Markets'],
[`/markets/${marketId}`, 'Trading'],
['/portfolio', 'Portfolio'],
];
const links = menu.getAllByRole('link');
links.forEach((link, i) => {
const [href, text] = expectedLinks[i];
expect(link).toHaveAttribute('href', href);
expect(link).toHaveTextContent(text);
});
await userEvent.click(screen.getByRole('button', { name: 'Close menu' }));
expect(screen.queryByTestId(navbarContent)).not.toBeInTheDocument();
});
it('can close menu by clicking overlay', async () => {
renderComponent();
await userEvent.click(screen.getByRole('button', { name: 'Menu' }));
expect(screen.getByTestId(navbarContent)).toBeInTheDocument();
await userEvent.click(screen.getByTestId('navbar-menu-overlay'));
expect(screen.queryByTestId(navbarContent)).not.toBeInTheDocument();
});
it('can open wallet menu on small screens and change pubkey', async () => {
const mockSelectPubKey = jest.fn();
renderComponent(undefined, { selectPubKey: mockSelectPubKey });
await userEvent.click(screen.getByRole('button', { name: 'Wallet' }));
const menuEl = screen.getByTestId(navbarContent);
expect(menuEl).toBeInTheDocument();
const menu = within(menuEl);
expect(menu.getAllByTestId(/key-\d+-mobile/)).toHaveLength(pubKeys.length);
const activeKey = within(menu.getByTestId('key-000-mobile'));
expect(activeKey.getByText(pubKeys[0].name)).toBeInTheDocument();
expect(activeKey.getByTestId('icon-tick')).toBeInTheDocument();
const inactiveKey = within(menu.getByTestId('key-111-mobile'));
await userEvent.click(inactiveKey.getByText(pubKeys[1].name));
expect(mockSelectPubKey).toHaveBeenCalledWith(pubKeys[1].publicKey);
});
it('can transfer and close menu', async () => {
renderComponent();
await userEvent.click(screen.getByRole('button', { name: 'Wallet' }));
const menuEl = screen.getByTestId(navbarContent);
expect(menuEl).toBeInTheDocument();
const menu = within(menuEl);
await userEvent.click(menu.getByText('Transfer'));
expect(screen.queryByTestId(navbarContent)).not.toBeInTheDocument();
});
it('can disconnect and close menu', async () => {
const mockDisconnect = jest.fn();
renderComponent(undefined, { disconnect: mockDisconnect });
await userEvent.click(screen.getByRole('button', { name: 'Wallet' }));
const menuEl = screen.getByTestId(navbarContent);
expect(menuEl).toBeInTheDocument();
const menu = within(menuEl);
await userEvent.click(menu.getByText('Disconnect'));
expect(mockDisconnect).toHaveBeenCalled();
expect(screen.queryByTestId(navbarContent)).not.toBeInTheDocument();
});
});
+369 -109
View File
@@ -1,140 +1,400 @@
import type { ComponentProps, ReactNode } from 'react';
import {
DApp,
NetworkSwitcher,
TOKEN_GOVERNANCE,
useEnvironment,
useLinks,
DocsLinks,
} from '@vegaprotocol/environment';
import type { ButtonHTMLAttributes, LiHTMLAttributes, ReactNode } from 'react';
import { useState } from 'react';
import { useEnvironment, DocsLinks, Networks } from '@vegaprotocol/environment';
import { t } from '@vegaprotocol/i18n';
import { useGlobalStore } from '../../stores';
import { VegaWalletConnectButton } from '../vega-wallet-connect-button';
import {
Navigation,
NavigationList,
NavigationItem,
NavigationLink,
ExternalLink,
NavigationBreakpoint,
NavigationTrigger,
NavigationContent,
VegaIconNames,
VegaIcon,
} from '@vegaprotocol/ui-toolkit';
import { VegaIconNames, VegaIcon, VLogo } from '@vegaprotocol/ui-toolkit';
import * as N from '@radix-ui/react-navigation-menu';
import * as D from '@radix-ui/react-dialog';
import { NavLink } from 'react-router-dom';
import { Links, Routes } from '../../pages/client-router';
import {
ProtocolUpgradeCountdown,
ProtocolUpgradeCountdownMode,
} from '@vegaprotocol/proposals';
import classNames from 'classnames';
import { VegaWalletMenu } from '../vega-wallet';
import { useVegaWallet, useVegaWalletDialogStore } from '@vegaprotocol/wallet';
import { WalletIcon } from '../icons/wallet';
import { ProtocolUpgradeCountdown } from '@vegaprotocol/proposals';
type MenuState = 'wallet' | 'nav' | null;
type Theme = 'system' | 'yellow';
export const Navbar = ({
children,
theme = 'system',
}: {
theme: ComponentProps<typeof Navigation>['theme'];
children?: ReactNode;
theme?: Theme;
}) => {
const { GITHUB_FEEDBACK_URL } = useEnvironment();
const tokenLink = useLinks(DApp.Token);
// menu state for small screens
const [menu, setMenu] = useState<MenuState>(null);
const { pubKey } = useVegaWallet();
const openVegaWalletDialog = useVegaWalletDialogStore(
(store) => store.openVegaWalletDialog
);
const isConnected = pubKey !== null;
const navTextClasses = 'text-vega-clight-200 dark:text-vega-cdark-200';
const rootClasses = classNames(
navTextClasses,
'flex gap-3 h-10 pr-1',
'border-b border-default',
'bg-vega-clight-800 dark:bg-vega-cdark-800'
);
return (
<N.Root className={rootClasses}>
<NavLink
to="/"
className={classNames('flex items-center px-3', {
'bg-vega-yellow text-vega-clight-50': theme === 'yellow',
'text-default': theme === 'system',
})}
>
<VLogo className="w-4" />
</NavLink>
{/* Left section */}
<div className="lg:hidden flex items-center">{children}</div>
{/* Used to show header in nav on mobile */}
<div className="hidden lg:block">
<NavbarMenu onClick={() => setMenu(null)} />
</div>
{/* Right section */}
<div className="ml-auto flex justify-end items-center gap-2">
<ProtocolUpgradeCountdown />
<NavbarMobileButton
onClick={() => {
if (isConnected) {
setMenu((x) => (x === 'wallet' ? null : 'wallet'));
} else {
openVegaWalletDialog();
}
}}
data-testid="navbar-mobile-wallet"
>
<span className="sr-only">{t('Wallet')}</span>
<WalletIcon className="w-6" />
</NavbarMobileButton>
<NavbarMobileButton
onClick={() => {
setMenu((x) => (x === 'nav' ? null : 'nav'));
}}
data-testid="navbar-mobile-burger"
>
<span className="sr-only">{t('Menu')}</span>
<BurgerIcon />
</NavbarMobileButton>
<div className="hidden lg:block">
<VegaWalletConnectButton />
</div>
</div>
{menu !== null && (
<D.Root
open={menu !== null}
onOpenChange={(open) => setMenu((x) => (open ? x : null))}
>
<D.Overlay
className="lg:hidden fixed inset-0 dark:bg-black/80 bg-black/50 z-20"
data-testid="navbar-menu-overlay"
/>
<D.Content
className={classNames(
'lg:hidden',
'fixed top-0 right-0 z-20 w-3/4 h-screen border-l border-default bg-vega-clight-700 dark:bg-vega-cdark-700',
navTextClasses
)}
data-testid="navbar-menu-content"
>
<div className="flex justify-end items-center h-10 p-1">
<NavbarMobileButton onClick={() => setMenu(null)}>
<span className="sr-only">{t('Close menu')}</span>
<VegaIcon name={VegaIconNames.CROSS} size={24} />
</NavbarMobileButton>
</div>
{menu === 'nav' && <NavbarMenu onClick={() => setMenu(null)} />}
{menu === 'wallet' && <VegaWalletMenu setMenu={setMenu} />}
</D.Content>
</D.Root>
)}
</N.Root>
);
};
/**
* List of links or dropdown triggers to show in the main section
* of the navigation
*/
const NavbarMenu = ({ onClick }: { onClick: () => void }) => {
const { VEGA_ENV, VEGA_NETWORKS, GITHUB_FEEDBACK_URL } = useEnvironment();
const marketId = useGlobalStore((store) => store.marketId);
// If we have a stored marketId make Trade link go to that market
// otherwise always go to /markets/all
const tradingPath = marketId
? Links[Routes.MARKET](marketId)
: Links[Routes.MARKET]();
: Links[Routes.MARKET]('');
return (
<Navigation
appName="console"
theme={theme}
actions={
<>
<ProtocolUpgradeCountdown
mode={ProtocolUpgradeCountdownMode.IN_ESTIMATED_TIME_REMAINING}
/>
<VegaWalletConnectButton />
</>
}
breakpoints={[521, 1122]}
>
<NavigationList
className="[.drawer-content_&]:border-b [.drawer-content_&]:border-b-vega-light-200 dark:[.drawer-content_&]:border-b-vega-dark-200 [.drawer-content_&]:pb-8 [.drawer-content_&]:mb-2"
hide={[NavigationBreakpoint.Small]}
>
<NavigationItem className="[.drawer-content_&]:w-full">
<NetworkSwitcher className="[.drawer-content_&]:w-full" />
</NavigationItem>
</NavigationList>
<NavigationList
hide={[NavigationBreakpoint.Narrow, NavigationBreakpoint.Small]}
>
<NavigationItem>
<NavigationLink data-testid="Markets" to={Links[Routes.MARKETS]()}>
<div className="lg:flex lg:h-full gap-3">
<NavbarList>
<NavbarItem>
<NavbarTrigger data-testid="navbar-network-switcher-trigger">
{envNameMapping[VEGA_ENV]}
</NavbarTrigger>
<NavbarContent data-testid="navbar-content-network-switcher">
<ul className="lg:p-4">
{[Networks.MAINNET, Networks.TESTNET].map((n) => {
const url = VEGA_NETWORKS[n];
if (!url) return;
return (
<NavbarSubItem key={n}>
<NavbarLink to={url}>{envNameMapping[n]}</NavbarLink>
</NavbarSubItem>
);
})}
</ul>
</NavbarContent>
</NavbarItem>
</NavbarList>
<NavbarListDivider />
<NavbarList>
<NavbarItem>
<NavbarLink to={Links[Routes.MARKETS]()} onClick={onClick}>
{t('Markets')}
</NavigationLink>
</NavigationItem>
<NavigationItem>
<NavigationLink data-testid="Trading" to={tradingPath} end>
</NavbarLink>
</NavbarItem>
<NavbarItem>
<NavbarLink to={tradingPath} onClick={onClick}>
{t('Trading')}
</NavigationLink>
</NavigationItem>
<NavigationItem>
<NavigationLink
data-testid="Portfolio"
to={Links[Routes.PORTFOLIO]()}
>
</NavbarLink>
</NavbarItem>
<NavbarItem>
<NavbarLink to={Links[Routes.PORTFOLIO]()} onClick={onClick}>
{t('Portfolio')}
</NavigationLink>
</NavigationItem>
<NavigationItem>
<NavExternalLink href={tokenLink(TOKEN_GOVERNANCE)}>
{t('Governance')}
</NavExternalLink>
</NavigationItem>
{DocsLinks?.NEW_TO_VEGA && GITHUB_FEEDBACK_URL && (
<NavigationItem>
<NavigationTrigger>{t('Resources')}</NavigationTrigger>
<NavigationContent>
<NavigationList>
<NavigationItem>
<NavExternalLink href={DocsLinks.NEW_TO_VEGA}>
</NavbarLink>
</NavbarItem>
<NavbarItem>
<NavbarTrigger>{t('Resources')}</NavbarTrigger>
<NavbarContent data-testid="navbar-content-resources">
<ul className="lg:p-4">
{DocsLinks?.NEW_TO_VEGA && (
<NavbarSubItem>
<NavbarLinkExternal to={DocsLinks?.NEW_TO_VEGA}>
{t('Docs')}
</NavExternalLink>
</NavigationItem>
<NavigationItem>
<NavExternalLink href={GITHUB_FEEDBACK_URL}>
</NavbarLinkExternal>
</NavbarSubItem>
)}
{GITHUB_FEEDBACK_URL && (
<NavbarSubItem>
<NavbarLinkExternal to={GITHUB_FEEDBACK_URL}>
{t('Give Feedback')}
</NavExternalLink>
</NavigationItem>
<NavigationItem>
<NavigationLink
data-testid="Disclaimer"
to={Links[Routes.DISCLAIMER]()}
>
{t('Disclaimer')}
</NavigationLink>
</NavigationItem>
</NavigationList>
</NavigationContent>
</NavigationItem>
)}
</NavigationList>
</Navigation>
</NavbarLinkExternal>
</NavbarSubItem>
)}
<NavbarSubItem>
<NavbarLink to={Links[Routes.DISCLAIMER]()} onClick={onClick}>
{t('Disclaimer')}
</NavbarLink>
</NavbarSubItem>
</ul>
</NavbarContent>
</NavbarItem>
</NavbarList>
</div>
);
};
const NavExternalLink = ({
/**
* Wrapper for radix-ux Trigger for consistent styles
*/
const NavbarTrigger = ({
children,
href,
...props
}: N.NavigationMenuTriggerProps) => {
return (
<N.Trigger
{...props}
onPointerMove={preventHover}
onPointerLeave={preventHover}
className={classNames(
'w-full lg:w-auto lg:h-full',
'flex items-center justify-between lg:justify-center gap-2 px-6 py-2 lg:p-0',
'text-lg lg:text-sm',
'hover:text-vega-clight-100 dark:hover:text-vega-cdark-100'
)}
>
{children}
<VegaIcon name={VegaIconNames.CHEVRON_DOWN} size={14} />
</N.Trigger>
);
};
/**
* Wrapper for react-router-dom NavLink for consistent styles
*/
const NavbarLink = ({
children,
to,
onClick,
}: {
children: ReactNode;
href: string;
to: string;
onClick?: () => void;
}) => {
return (
<ExternalLink href={href}>
<span className="flex items-center gap-2">
<span>{children}</span>
<VegaIcon name={VegaIconNames.OPEN_EXTERNAL} />
</span>
</ExternalLink>
<N.Link asChild={true}>
<NavLink
to={to}
className={classNames(
'block lg:flex lg:h-full flex-col justify-center',
'px-6 py-2 lg:p-0 text-lg lg:text-sm',
'hover:text-vega-clight-100 dark:hover:text-vega-cdark-100'
)}
onClick={onClick}
>
{({ isActive }) => {
const borderClasses = {
'border-b-2': true,
'border-transparent': !isActive,
'border-vega-yellow lg:group-[.navbar-content]:border-transparent':
isActive,
};
return (
<>
<span
className={classNames('lg:border-0', borderClasses, {
'text-vega-clight-50 dark:text-vega-cdark-50': isActive,
})}
>
{children}
</span>
<span
className={classNames(
'hidden lg:block absolute left-0 bottom-0 w-full h-0',
borderClasses
)}
/>
</>
);
}}
</NavLink>
</N.Link>
);
};
const NavbarItem = (props: N.NavigationMenuItemProps) => {
return <N.Item {...props} className="relative" />;
};
const NavbarSubItem = (props: LiHTMLAttributes<HTMLElement>) => {
return <li {...props} className="lg:mb-4 lg:last:mb-0" />;
};
const NavbarList = (props: N.NavigationMenuListProps) => {
return <N.List {...props} className="lg:flex lg:h-full gap-6" />;
};
/**
* Content that gets rendered when a sub section of the navbar is shown
*/
const NavbarContent = (props: N.NavigationMenuContentProps) => {
return (
<N.Content
{...props}
className={classNames(
'group navbar-content',
'lg:absolute lg:mt-2 pl-2 lg:pl-0 z-20 lg:min-w-[290px]',
'lg:bg-vega-clight-700 lg:dark:bg-vega-cdark-700',
'lg:border border-vega-clight-500 dark:border-vega-cdark-500 lg:rounded'
)}
onPointerEnter={preventHover}
onPointerLeave={preventHover}
/>
);
};
/**
* NavbarLink with OPEN_EXTERNAL icon
*/
const NavbarLinkExternal = ({
children,
to,
onClick,
}: {
children: ReactNode;
to: string;
onClick?: () => void;
}) => {
return (
<N.Link asChild={true}>
<NavLink
to={to}
className={classNames(
'flex lg:inline-flex gap-2 justify-between items-center relative',
'px-6 py-2 lg:p-0 text-lg lg:text-sm',
'hover:text-vega-clight-100 dark:hover:text-vega-cdark-100'
)}
onClick={onClick}
target="_blank"
>
<span>{children}</span>
<VegaIcon name={VegaIconNames.OPEN_EXTERNAL} />
</NavLink>
</N.Link>
);
};
const BurgerIcon = () => (
<svg
width="20"
height="20"
viewBox="0 0 16 16"
className="w-full stroke-current"
>
<line x1={0.5} x2={15.5} y1={3.5} y2={3.5} />
<line x1={0.5} x2={15.5} y1={11.5} y2={11.5} />
</svg>
);
const NavbarListDivider = () => {
return (
<div className="py-2 px-6 lg:px-0" role="separator">
<div className="h-px lg:h-full w-full lg:w-px bg-vega-clight-500 dark:bg-vega-cdark-500" />
</div>
);
};
/**
* Button component to avoid repeating styles for buttons shown on small screens
*/
const NavbarMobileButton = (props: ButtonHTMLAttributes<HTMLButtonElement>) => {
return (
<button
{...props}
className={classNames(
'w-8 h-8 lg:hidden flex items-center p-1 rounded ',
'hover:bg-vega-clight-500 dark:hover:bg-vega-cdark-500',
'hover:text-vega-clight-50 dark:hover:text-vega-cdark-50'
)}
/>
);
};
const envNameMapping: Record<Networks, string> = {
[Networks.VALIDATOR_TESTNET]: t('VALIDATOR_TESTNET'),
[Networks.CUSTOM]: t('Custom'),
[Networks.DEVNET]: t('Devnet'),
[Networks.STAGNET1]: t('Stagnet'),
[Networks.TESTNET]: t('Fairground testnet'),
[Networks.MAINNET_MIRROR]: t('Mirror'),
[Networks.MAINNET]: t('Mainnet'),
};
// https://github.com/radix-ui/primitives/issues/1630
// eslint-disable-next-line
const preventHover = (e: any) => {
e.preventDefault();
};
+5 -4
View File
@@ -51,9 +51,10 @@ type SidebarView =
};
export const Sidebar = () => {
const navClasses = 'flex lg:flex-col items-center gap-2 lg:gap-4 p-1';
return (
<div className="flex flex-col gap-2 h-full py-1" data-testid="sidebar">
<nav className="flex flex-col items-center gap-4 p-1">
<div className="flex lg:flex-col gap-2 h-full p-1" data-testid="sidebar">
<nav className={navClasses}>
{/* sidebar options that always show */}
<SidebarButton
view={ViewType.Deposit}
@@ -102,7 +103,7 @@ export const Sidebar = () => {
/>
</Routes>
</nav>
<nav className="mt-auto flex flex-col items-center gap-4 p-1">
<nav className={classNames(navClasses, 'ml-auto lg:mt-auto lg:ml-0')}>
<SidebarButton
view={ViewType.Settings}
icon={VegaIconNames.COG}
@@ -161,7 +162,7 @@ const SidebarButton = ({
const SidebarDivider = () => {
return (
<div
className="bg-vega-clight-600 dark:bg-vega-cdark-600 w-4 h-px"
className="bg-vega-clight-600 dark:bg-vega-cdark-600 w-px h-4 lg:w-4 lg:h-px"
role="separator"
/>
);
@@ -28,7 +28,7 @@ describe('VegaWalletConnectButton', () => {
render(generateJsx({ pubKey: null } as VegaWalletContextShape));
const button = screen.getByTestId('connect-vega-wallet');
expect(button).toHaveTextContent('Connect Vega wallet');
expect(button).toHaveTextContent('Connect');
fireEvent.click(button);
expect(mockUpdateDialogOpen).toHaveBeenCalled();
});
@@ -1,148 +1,26 @@
import { useCallback, useMemo, useState } from 'react';
import { useMemo, useState } from 'react';
import CopyToClipboard from 'react-copy-to-clipboard';
import classNames from 'classnames';
import { truncateByChars } from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import {
Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuItemIndicator,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuTrigger,
Drawer,
DropdownMenuSeparator,
VegaIcon,
VegaIconNames,
TradingButton as Button,
Intent,
TradingDropdown,
TradingDropdownTrigger,
TradingDropdownContent,
TradingDropdownRadioGroup,
TradingDropdownSeparator,
TradingDropdownItem,
TradingDropdownRadioItem,
TradingDropdownItemIndicator,
} from '@vegaprotocol/ui-toolkit';
import type { PubKey } from '@vegaprotocol/wallet';
import { useVegaWallet, useVegaWalletDialogStore } from '@vegaprotocol/wallet';
import { Networks, useEnvironment } from '@vegaprotocol/environment';
import { WalletIcon } from '../icons/wallet';
import { useCopyTimeout } from '@vegaprotocol/react-helpers';
import { ViewType, useSidebar } from '../sidebar';
const MobileWalletButton = ({
isConnected,
activeKey,
}: {
isConnected?: boolean;
activeKey?: PubKey;
}) => {
const { pubKeys, selectPubKey, disconnect, fetchPubKeys } = useVegaWallet();
const openVegaWalletDialog = useVegaWalletDialogStore(
(store) => store.openVegaWalletDialog
);
const setView = useSidebar((store) => store.setView);
const { VEGA_ENV } = useEnvironment();
const isYellow = VEGA_ENV === Networks.TESTNET;
const [drawerOpen, setDrawerOpen] = useState(false);
const mobileDisconnect = useCallback(() => {
setDrawerOpen(false);
disconnect();
}, [disconnect]);
const openDrawer = useCallback(() => {
if (!isConnected) {
openVegaWalletDialog();
setDrawerOpen(false);
} else {
if (fetchPubKeys) {
fetchPubKeys();
}
setDrawerOpen(!drawerOpen);
}
}, [drawerOpen, fetchPubKeys, isConnected, openVegaWalletDialog]);
const iconClass = drawerOpen
? 'hidden'
: isYellow
? 'fill-black'
: 'fill-white';
const [container, setContainer] = useState<HTMLElement | null>(null);
const walletButton = (
<button
className="my-2 transition-all flex flex-col justify-around gap-3 p-2 relative h-[34px]"
onClick={openDrawer}
data-testid="connect-vega-wallet-mobile"
>
<WalletIcon className={iconClass} />
</button>
);
const onSelectItem = useCallback(
(pubkey: string) => {
setDrawerOpen(false);
selectPubKey(pubkey);
},
[selectPubKey]
);
return (
<div className="lg:hidden overflow-hidden flex" ref={setContainer}>
<Drawer
dataTestId="wallets-drawer"
open={drawerOpen}
onChange={setDrawerOpen}
container={container}
trigger={walletButton}
>
<div className="border-l border-default p-2 gap-4 flex flex-col w-full h-full bg-white dark:bg-black dark:text-white justify-between">
<div className="flex h-5 justify-end">
<button
className="transition-all flex flex-col justify-around gap-3 p-2 relative h-[34px]"
onClick={() => setDrawerOpen(false)}
data-testid="connect-vega-wallet-mobile-close"
>
<>
<div
className={classNames(
'w-[26px] h-[2px] bg-black dark:bg-white transition-all translate-y-[7.5px] rotate-45',
{
hidden: !drawerOpen,
}
)}
/>
<div
className={classNames(
'w-[26px] h-[2px] bg-black dark:bg-white transition-all -translate-y-[7.5px] -rotate-45',
{
hidden: !drawerOpen,
}
)}
/>
</>
</button>
</div>
<div className="grow my-4" role="list">
{(pubKeys || []).map((pk) => (
<KeypairListItem
key={pk.publicKey}
pk={pk}
isActive={activeKey?.publicKey === pk.publicKey}
onSelectItem={onSelectItem}
/>
))}
</div>
<div className="flex flex-col gap-2 m-4">
<Button
onClick={() => {
setDrawerOpen(false);
setView({ type: ViewType.Transfer });
}}
fill
>
{t('Transfer')}
</Button>
<Button onClick={mobileDisconnect} fill>
{t('Disconnect')}
</Button>
</div>
</div>
</Drawer>
</div>
);
};
import classNames from 'classnames';
export const VegaWalletConnectButton = () => {
const [dropdownOpen, setDropdownOpen] = useState(false);
@@ -166,96 +44,101 @@ export const VegaWalletConnectButton = () => {
if (isConnected && pubKeys) {
return (
<>
<div className="hidden lg:block">
<DropdownMenu
open={dropdownOpen}
trigger={
<DropdownMenuTrigger
data-testid="manage-vega-wallet"
<TradingDropdown
open={dropdownOpen}
trigger={
<TradingDropdownTrigger
data-testid="manage-vega-wallet"
onClick={() => {
if (fetchPubKeys) {
fetchPubKeys();
}
setDropdownOpen(!dropdownOpen);
}}
>
<Button
size="small"
icon={<VegaIcon name={VegaIconNames.CHEVRON_DOWN} size={14} />}
>
{activeKey && <span className="uppercase">{activeKey.name}</span>}
{' | '}
{truncateByChars(pubKey)}
</Button>
</TradingDropdownTrigger>
}
>
<TradingDropdownContent
onInteractOutside={() => setDropdownOpen(false)}
sideOffset={12}
side="bottom"
align="end"
onEscapeKeyDown={() => setDropdownOpen(false)}
>
<div className="min-w-[340px]" data-testid="keypair-list">
<TradingDropdownRadioGroup
value={pubKey}
onValueChange={(value) => {
selectPubKey(value);
}}
>
{pubKeys.map((pk) => (
<KeypairItem
key={pk.publicKey}
pk={pk}
active={pk.publicKey === pubKey}
/>
))}
</TradingDropdownRadioGroup>
<TradingDropdownSeparator />
{!isReadOnly && (
<TradingDropdownItem
data-testid="wallet-transfer"
onClick={() => {
if (fetchPubKeys) {
fetchPubKeys();
}
setDropdownOpen(!dropdownOpen);
setView({ type: ViewType.Transfer });
setDropdownOpen(false);
}}
>
{activeKey && (
<span className="uppercase">{activeKey.name}</span>
)}
{': '}
{truncateByChars(pubKey)}
</DropdownMenuTrigger>
}
>
<DropdownMenuContent
onInteractOutside={() => setDropdownOpen(false)}
sideOffset={17}
side="bottom"
align="end"
onEscapeKeyDown={() => setDropdownOpen(false)}
>
<div className="min-w-[340px]" data-testid="keypair-list">
<DropdownMenuRadioGroup
value={pubKey}
onValueChange={(value) => {
selectPubKey(value);
}}
>
{pubKeys.map((pk) => (
<KeypairItem key={pk.publicKey} pk={pk} />
))}
</DropdownMenuRadioGroup>
<DropdownMenuSeparator />
{!isReadOnly && (
<DropdownMenuItem
data-testid="wallet-transfer"
onClick={() => {
setView({ type: ViewType.Transfer });
setDropdownOpen(false);
}}
>
{t('Transfer')}
</DropdownMenuItem>
)}
<DropdownMenuItem data-testid="disconnect" onClick={disconnect}>
{t('Disconnect')}
</DropdownMenuItem>
</div>
</DropdownMenuContent>
</DropdownMenu>
</div>
<MobileWalletButton isConnected activeKey={activeKey} />
</>
{t('Transfer')}
</TradingDropdownItem>
)}
<TradingDropdownItem data-testid="disconnect" onClick={disconnect}>
{t('Disconnect')}
</TradingDropdownItem>
</div>
</TradingDropdownContent>
</TradingDropdown>
);
}
return (
<>
<Button
data-testid="connect-vega-wallet"
onClick={openVegaWalletDialog}
size="sm"
className="hidden lg:block"
>
<span className="whitespace-nowrap">{t('Connect Vega wallet')}</span>
</Button>
<MobileWalletButton />
</>
<Button
data-testid="connect-vega-wallet"
onClick={openVegaWalletDialog}
size="small"
intent={Intent.None}
icon={<VegaIcon name={VegaIconNames.ARROW_RIGHT} size={14} />}
>
<span className="whitespace-nowrap uppercase">{t('Connect')}</span>
</Button>
);
};
const KeypairItem = ({ pk }: { pk: PubKey }) => {
const KeypairItem = ({ pk, active }: { pk: PubKey; active: boolean }) => {
const [copied, setCopied] = useCopyTimeout();
return (
<DropdownMenuRadioItem value={pk.publicKey}>
<div className="flex-1 mr-2" data-testid={`key-${pk.publicKey}`}>
<span className="mr-2">
<span>
<span className="uppercase">{pk.name}</span>:{' '}
{truncateByChars(pk.publicKey)}
</span>
<TradingDropdownRadioItem value={pk.publicKey}>
<div
className={classNames('flex-1 mr-2', {
'text-default': active,
'text-muted': !active,
})}
data-testid={`key-${pk.publicKey}`}
>
<span className={classNames('mr-2 uppercase')}>
{pk.name}
{' | '}
{truncateByChars(pk.publicKey)}
</span>
<span className="inline-flex items-center gap-1">
<CopyToClipboard text={pk.publicKey} onCopy={() => setCopied(true)}>
@@ -270,46 +153,7 @@ const KeypairItem = ({ pk }: { pk: PubKey }) => {
{copied && <span className="text-xs">{t('Copied')}</span>}
</span>
</div>
<DropdownMenuItemIndicator />
</DropdownMenuRadioItem>
);
};
const KeypairListItem = ({
pk,
isActive,
onSelectItem,
}: {
pk: PubKey;
isActive: boolean;
onSelectItem: (pk: string) => void;
}) => {
const [copied, setCopied] = useCopyTimeout();
return (
<div
className="flex flex-col w-full ml-4 mr-2 mb-4"
data-testid={`key-${pk.publicKey}-mobile`}
>
<span className="flex gap-2 items-center mr-2">
<button onClick={() => onSelectItem(pk.publicKey)}>
<span className="uppercase">{pk.name}</span>
</button>
{isActive && <VegaIcon name={VegaIconNames.TICK} />}
</span>
<span className="flex gap-2 items-center">
{truncateByChars(pk.publicKey)}{' '}
<CopyToClipboard text={pk.publicKey} onCopy={() => setCopied(true)}>
<button
data-testid="copy-vega-public-key"
onClick={(e) => e.stopPropagation()}
>
<span className="sr-only">{t('Copy')}</span>
<VegaIcon name={VegaIconNames.COPY} />
</button>
</CopyToClipboard>
{copied && <span className="text-xs">{t('Copied')}</span>}
</span>
</div>
<TradingDropdownItemIndicator />
</TradingDropdownRadioItem>
);
};
@@ -0,0 +1 @@
export { VegaWalletMenu } from './vega-wallet-menu';
@@ -0,0 +1,106 @@
import { t } from '@vegaprotocol/i18n';
import { useCopyTimeout } from '@vegaprotocol/react-helpers';
import {
TradingButton as Button,
VegaIcon,
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
import { truncateByChars } from '@vegaprotocol/utils';
import { useVegaWallet, type PubKey } from '@vegaprotocol/wallet';
import { useCallback, useMemo } from 'react';
import CopyToClipboard from 'react-copy-to-clipboard';
import { ViewType, useSidebar } from '../sidebar';
export const VegaWalletMenu = ({
setMenu,
}: {
setMenu: (open: 'nav' | 'wallet' | null) => void;
}) => {
const { pubKey, pubKeys, selectPubKey, disconnect } = useVegaWallet();
const setView = useSidebar((store) => store.setView);
const activeKey = useMemo(() => {
return pubKeys?.find((pk) => pk.publicKey === pubKey);
}, [pubKey, pubKeys]);
const onSelectItem = useCallback(
(pubkey: string) => {
selectPubKey(pubkey);
},
[selectPubKey]
);
return (
<div>
<div className="grow my-4" role="list">
{(pubKeys || []).map((pk) => (
<KeypairListItem
key={pk.publicKey}
pk={pk}
isActive={activeKey?.publicKey === pk.publicKey}
onSelectItem={onSelectItem}
/>
))}
</div>
<div className="flex flex-col gap-2 m-4">
<Button
onClick={() => {
setView({ type: ViewType.Transfer });
setMenu(null);
}}
>
{t('Transfer')}
</Button>
<Button
onClick={async () => {
await disconnect();
setMenu(null);
}}
>
{t('Disconnect')}
</Button>
</div>
</div>
);
};
const KeypairListItem = ({
pk,
isActive,
onSelectItem,
}: {
pk: PubKey;
isActive: boolean;
onSelectItem: (pk: string) => void;
}) => {
const [copied, setCopied] = useCopyTimeout();
return (
<div
className="flex flex-col w-full ml-4 mr-2 mb-4"
data-testid={`key-${pk.publicKey}-mobile`}
>
<span className="flex gap-2 items-center mr-2">
<button type="button" onClick={() => onSelectItem(pk.publicKey)}>
<span className="uppercase">{pk.name}</span>
</button>
{isActive && <VegaIcon name={VegaIconNames.TICK} />}
</span>
<span className="flex gap-2 items-center">
{truncateByChars(pk.publicKey)}{' '}
<CopyToClipboard text={pk.publicKey} onCopy={() => setCopied(true)}>
<button
type="button"
data-testid="copy-vega-public-key"
onClick={(e) => e.stopPropagation()}
>
<span className="sr-only">{t('Copy')}</span>
<VegaIcon name={VegaIconNames.COPY} />
</button>
</CopyToClipboard>
{copied && <span className="text-xs">{t('Copied')}</span>}
</span>
</div>
);
};
+21 -2
View File
@@ -16,6 +16,7 @@ import {
} from '@vegaprotocol/web3';
import {
envTriggerMapping,
Networks,
NodeSwitcherDialog,
useEnvironment,
useInitializeEnv,
@@ -25,7 +26,13 @@ import './styles.css';
import { usePageTitleStore } from '../stores';
import DialogsContainer from './dialogs-container';
import ToastsManager from './toasts-manager';
import { HashRouter, useLocation, useSearchParams } from 'react-router-dom';
import {
HashRouter,
useLocation,
Route,
Routes,
useSearchParams,
} from 'react-router-dom';
import { Connectors } from '../lib/vega-connectors';
import { AppLoader, DynamicLoader } from '../components/app-loader';
import { useDataProvider } from '@vegaprotocol/data-provider';
@@ -39,6 +46,8 @@ import {
ProtocolUpgradeProposalNotification,
} from '@vegaprotocol/proposals';
import { ViewingBanner } from '../components/viewing-banner';
import { NavHeader } from '../components/navbar/nav-header';
import { Routes as AppRoutes } from './client-router';
const DEFAULT_TITLE = t('Welcome to Vega trading!');
@@ -74,6 +83,7 @@ const InitializeHandlers = () => {
function AppBody({ Component }: AppProps) {
const location = useLocation();
const { VEGA_ENV } = useEnvironment();
const gridClasses = classNames(
'h-full relative z-0 grid',
'grid-rows-[repeat(3,min-content),minmax(0,1fr)]'
@@ -87,7 +97,16 @@ function AppBody({ Component }: AppProps) {
<Title />
<div className={gridClasses}>
<AnnouncementBanner />
<Navbar theme="system" />
<Navbar theme={VEGA_ENV === Networks.TESTNET ? 'yellow' : 'system'}>
<Routes>
<Route
path={AppRoutes.MARKETS}
// render nothing for markets/all, otherwise markets/:marketId will match with markets/all
element={null}
/>
<Route path={AppRoutes.MARKET} element={<NavHeader />} />
</Routes>
</Navbar>
<div data-testid="banners">
<ProtocolUpgradeProposalNotification
mode={ProtocolUpgradeCountdownMode.IN_ESTIMATED_TIME_REMAINING}
+1
View File
@@ -1,5 +1,6 @@
export function createLog(name: string) {
return (message: string) => {
// eslint-disable-next-line no-console
console.log(`[${name}]: ${message}`);
};
}
@@ -160,7 +160,7 @@ export function waitForProposal(id: string): Promise<{ id: string }> {
resolve(res.proposal);
}
} catch (err) {
console.log(err);
console.error(err);
}
tick++;
@@ -29,6 +29,9 @@ export function addGetNodes() {
location
name
pendingStake
rankingScore {
performanceScore
}
pubkey
stakedByDelegates
stakedByOperator
@@ -15,6 +15,7 @@ export const addImportNodeWallets = () => {
.its('stdout')
.then((result) => {
const obj = JSON.parse(result);
// eslint-disable-next-line no-console
console.log(obj);
cy.writeFile(
'./src/fixtures/wallet/node0RecoveryPhrase',
@@ -30,6 +30,7 @@ export const addValidatorsSelfDelegate = () => {
.its('stdout')
.then((result) => {
const obj = JSON.parse(result);
// eslint-disable-next-line no-console
console.log(obj);
cy.writeFile(
'./src/fixtures/wallet/node0RecoveryPhrase',
@@ -30,8 +30,10 @@ export function addVegaWalletTopUpRewardsPool() {
transferStartEpoch = Number(epochText.replace('Epoch', '')) + 5;
transferEndEpoch = transferStartEpoch + 100;
/* eslint-disable no-console */
console.log(transferStartEpoch);
console.log(transferEndEpoch);
/* eslint-enable */
});
})
.then(() => {
+4
View File
@@ -9,12 +9,14 @@ export class CustomizedBridge extends Eip1193Bridge {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
async sendAsync(...args: any) {
// eslint-disable-next-line no-console
console.debug('sendAsync called', ...args);
return this.send(...args);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
override async send(...args: any) {
// eslint-disable-next-line no-console
console.debug('send called', ...args);
const isCallbackForm =
typeof args[0] === 'object' && typeof args[1] === 'function';
@@ -89,6 +91,7 @@ export class CustomizedBridge extends Eip1193Bridge {
// All other transactions the base class works for
result = await super.send(method, params);
}
// eslint-disable-next-line no-console
console.debug('result received', method, params, result);
if (isCallbackForm) {
callback(null, { result });
@@ -96,6 +99,7 @@ export class CustomizedBridge extends Eip1193Bridge {
return result;
}
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
if (isCallbackForm) {
callback(error, null);
@@ -11,6 +11,6 @@ export const CenteredGridCellWrapper = ({
<div
className={classNames('flex h-[20px] p-0 justify-items-center', className)}
>
<div className="self-center">{children}</div>
<div className="w-full self-center">{children}</div>
</div>
);
+8 -5
View File
@@ -1,6 +1,6 @@
import {
ActionsDropdown,
DropdownMenuCopyItem,
TradingDropdownCopyItem,
} from '@vegaprotocol/ui-toolkit';
import { t } from '@vegaprotocol/i18n';
@@ -14,10 +14,13 @@ export const FillActionsDropdown = ({
sellOrderId: string;
}) => {
return (
<ActionsDropdown data-testid="market-actions-content">
<DropdownMenuCopyItem value={tradeId} text={t('Copy trade ID')} />
<DropdownMenuCopyItem value={buyOrderId} text={t('Copy buy order ID')} />
<DropdownMenuCopyItem
<ActionsDropdown data-testid="fill-actions-content">
<TradingDropdownCopyItem value={tradeId} text={t('Copy trade ID')} />
<TradingDropdownCopyItem
value={buyOrderId}
text={t('Copy buy order ID')}
/>
<TradingDropdownCopyItem
value={sellOrderId}
text={t('Copy sell order ID')}
/>
+5
View File
@@ -48,6 +48,7 @@ describe('LocalLogger', () => {
const consoleMethod = methodToConsoleMethod[i];
jest.spyOn(console, consoleMethod).mockImplementation();
logger[method]('test', 'test2');
// eslint-disable-next-line no-console
expect(console[consoleMethod]).toHaveBeenCalledWith(
`trading:${methodToLevel[i]}: `,
'test',
@@ -100,10 +101,14 @@ describe('LocalLogger', () => {
const logger = localLoggerFactory({ logLevel: 'info' });
jest.spyOn(console, 'debug').mockImplementation();
logger.debug('test', 'test1');
// eslint-disable-next-line no-console
expect(console.debug).not.toHaveBeenCalled();
logger.setLogLevel('debug');
logger.debug('test', 'test1');
// eslint-disable-next-line no-console
expect(console.debug).toHaveBeenCalledWith(
'trading:debug: ',
'test',
+1
View File
@@ -99,6 +99,7 @@ export class LocalLogger {
this.numberLogLevel <= LocalLogger.levelLogMap[level] // &&
//!global.__LOGGER_SILENT_MODE__
) {
// eslint-disable-next-line no-console
console[logMethod].apply(console, [
`${this._application}:${level}: `,
...args,
+1 -1
View File
@@ -59,6 +59,7 @@ const CumulativeVol = memo(
}) => {
const volume = cumulativeValue ? (
<NumericCell
testId={testId}
value={cumulativeValue}
valueFormatted={addDecimalsFixedFormatNumber(
cumulativeValue,
@@ -69,7 +70,6 @@ const CumulativeVol = memo(
return onClick && volume ? (
<button
data-testid={testId}
onClick={() => onClick(cumulativeValue)}
className="hover:dark:bg-neutral-800 hover:bg-neutral-200 text-right pr-1"
>
+1 -1
View File
@@ -183,4 +183,4 @@ export function useSuccessorMarketLazyQuery(baseOptions?: Apollo.LazyQueryHookOp
}
export type SuccessorMarketQueryHookResult = ReturnType<typeof useSuccessorMarketQuery>;
export type SuccessorMarketLazyQueryHookResult = ReturnType<typeof useSuccessorMarketLazyQuery>;
export type SuccessorMarketQueryResult = Apollo.QueryResult<SuccessorMarketQuery, SuccessorMarketQueryVariables>;
export type SuccessorMarketQueryResult = Apollo.QueryResult<SuccessorMarketQuery, SuccessorMarketQueryVariables>;
@@ -1,7 +1,7 @@
import { t } from '@vegaprotocol/i18n';
import {
DropdownMenuItem,
DropdownMenuCopyItem,
TradingDropdownItem,
TradingDropdownCopyItem,
Link,
VegaIcon,
VegaIconNames,
@@ -22,8 +22,8 @@ export const MarketActionsDropdown = ({
return (
<ActionsDropdown data-testid="market-actions-content">
<DropdownMenuCopyItem value={marketId} text={t('Copy Market ID')} />
<DropdownMenuItem>
<TradingDropdownCopyItem value={marketId} text={t('Copy Market ID')} />
<TradingDropdownItem>
<Link
href={linkCreator(EXPLORER_MARKET.replace(':id', marketId))}
target="_blank"
@@ -33,15 +33,15 @@ export const MarketActionsDropdown = ({
{t('View on Explorer')}
</span>
</Link>
</DropdownMenuItem>
<DropdownMenuItem
</TradingDropdownItem>
<TradingDropdownItem
onClick={(e) => {
open(assetId, e.target as HTMLElement);
}}
>
<VegaIcon name={VegaIconNames.INFO} size={16} />
{t('View settlement asset details')}
</DropdownMenuItem>
</TradingDropdownItem>
</ActionsDropdown>
);
};
@@ -50,6 +50,7 @@ export const MarketsContainer = ({
'tradableInstrument.instrument.code',
'tradableInstrument.instrument.product.settlementAsset',
'tradableInstrument.instrument.product.settlementAsset.symbol',
'market-actions',
].includes(colId)
) {
return;
-1
View File
@@ -83,7 +83,6 @@ export const marketProvider = makeDerivedDataProvider<
);
export const useMarket = (marketId?: string) => {
console.log(marketId);
const variables = useMemo(() => ({ marketId: marketId || '' }), [marketId]);
return useDataProvider({
dataProvider: marketProvider,
@@ -1,13 +1,13 @@
import {
ActionsDropdown,
DropdownMenuCopyItem,
TradingDropdownCopyItem,
} from '@vegaprotocol/ui-toolkit';
import { t } from '@vegaprotocol/i18n';
export const OrderActionsDropdown = ({ id }: { id: string }) => {
return (
<ActionsDropdown data-testid="market-actions-content">
<DropdownMenuCopyItem value={id} text={t('Copy order ID')} />
<ActionsDropdown data-testid="order-actions-content">
<TradingDropdownCopyItem value={id} text={t('Copy order ID')} />
</ActionsDropdown>
);
};
@@ -1,7 +1,7 @@
import { t } from '@vegaprotocol/i18n';
import {
ActionsDropdown,
DropdownMenuItem,
TradingDropdownItem,
VegaIcon,
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
@@ -11,15 +11,15 @@ export const PositionActionsDropdown = ({ assetId }: { assetId: string }) => {
const open = useAssetDetailsDialogStore((store) => store.open);
return (
<ActionsDropdown data-testid="market-actions-content">
<DropdownMenuItem
<ActionsDropdown data-testid="position-actions-content">
<TradingDropdownItem
onClick={(e) => {
open(assetId, e.target as HTMLElement);
}}
>
<VegaIcon name={VegaIconNames.INFO} size={16} />
{t('View settlement asset details')}
</DropdownMenuItem>
</TradingDropdownItem>
</ActionsDropdown>
);
};
@@ -1,5 +1,5 @@
import {
DropdownMenuItem,
TradingDropdownItem,
VegaIcon,
VegaIconNames,
Link,
@@ -12,8 +12,8 @@ export const ProposalActionsDropdown = ({ id }: { id: string }) => {
const linkCreator = useLinks(DApp.Token);
return (
<ActionsDropdown data-testid="market-actions-content">
<DropdownMenuItem>
<ActionsDropdown data-testid="proposal-actions-content">
<TradingDropdownItem>
<Link
href={linkCreator(TOKEN_PROPOSAL.replace(':id', id))}
target="_blank"
@@ -21,7 +21,7 @@ export const ProposalActionsDropdown = ({ id }: { id: string }) => {
<VegaIcon name={VegaIconNames.OPEN_EXTERNAL} size={16} />
{t('View proposal')}
</Link>
</DropdownMenuItem>
</TradingDropdownItem>
</ActionsDropdown>
);
};
@@ -1,7 +1,12 @@
import { useMemo } from 'react';
import BigNumber from 'bignumber.js';
import type { ColDef } from 'ag-grid-community';
import { COL_DEFS, DateRangeFilter, SetFilter } from '@vegaprotocol/datagrid';
import {
CenteredGridCellWrapper,
COL_DEFS,
DateRangeFilter,
SetFilter,
} from '@vegaprotocol/datagrid';
import compact from 'lodash/compact';
import { useEnvironment, FLAGS } from '@vegaprotocol/environment';
import { getDateTimeFormat } from '@vegaprotocol/utils';
@@ -31,7 +36,6 @@ export const useColumnDefs = () => {
return new BigNumber(requiredMajority).times(100);
}, [params?.governance_proposal_market_requiredMajority]);
const cellCss = 'grid h-full items-center';
const columnDefs: ColDef[] = useMemo(() => {
return compact([
{
@@ -94,7 +98,6 @@ export const useColumnDefs = () => {
{
colId: 'voting',
headerName: t('Voting'),
cellClass: 'flex justify-between leading-tight font-mono',
cellRenderer: ({
data,
}: VegaICellRendererParams<ProposalListFieldsFragment>) => {
@@ -106,12 +109,12 @@ export const useColumnDefs = () => {
? new BigNumber(0)
: yesTokens.multipliedBy(100).dividedBy(totalTokensVoted);
return (
<div className="uppercase flex h-full items-center justify-center">
<CenteredGridCellWrapper>
<VoteProgress
threshold={requiredMajorityPercentage}
progress={yesPercentage}
/>
</div>
</CenteredGridCellWrapper>
);
}
return '-';
@@ -160,7 +163,6 @@ export const useColumnDefs = () => {
const defaultColDef: ColDef = useMemo(() => {
return {
sortable: true,
cellClass: cellCss,
resizable: true,
filter: true,
filterParams: { buttons: ['reset'] },
@@ -1,20 +1,25 @@
import { t } from '@vegaprotocol/i18n';
import { useNextProtocolUpgradeProposal, useTimeToUpgrade } from '../lib';
import { convertToCountdownString } from '@vegaprotocol/utils';
import { IconNames } from '@blueprintjs/icons';
import classNames from 'classnames';
import { Icon, NavigationContext } from '@vegaprotocol/ui-toolkit';
import {
NavigationContext,
VegaIcon,
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
import { useProtocolUpgradeProposalLink } from '@vegaprotocol/environment';
import { useContext } from 'react';
export enum ProtocolUpgradeCountdownMode {
IN_BLOCKS,
IN_ESTIMATED_TIME_REMAINING,
}
type ProtocolUpgradeCountdownProps = {
mode?: ProtocolUpgradeCountdownMode;
};
export const ProtocolUpgradeCountdown = ({
mode = ProtocolUpgradeCountdownMode.IN_BLOCKS,
mode = ProtocolUpgradeCountdownMode.IN_ESTIMATED_TIME_REMAINING,
}: ProtocolUpgradeCountdownProps) => {
const { theme } = useContext(NavigationContext);
const { data, lastBlockHeight } = useNextProtocolUpgradeProposal();
@@ -75,20 +80,17 @@ export const ProtocolUpgradeCountdown = ({
<div
data-testid="protocol-upgrade-counter"
className={classNames(
'flex flex-nowrap items-center text-xs py-2 px-4',
'flex flex-nowrap gap-1 items-center text-xs py-1 px-2 lg:px-4 h-8',
'border rounded',
'border-vega-orange-500 dark:border-vega-orange-500',
'bg-vega-orange-300 dark:bg-vega-orange-700',
'text-default',
{
'!bg-transparent !border-black': theme === 'yellow',
}
)}
>
<Icon
name={IconNames.WARNING_SIGN}
size={3}
className={classNames('mr-2', emphasis)}
/>{' '}
<VegaIcon name={VegaIconNames.EXCLAIMATION_MARK} size={12} />{' '}
<span className="flex gap-1 flex-nowrap whitespace-nowrap">
<span>{t('Network upgrade in')} </span>
{countdown}
@@ -327,3 +327,33 @@ query ProposalsList($proposalType: ProposalType, $inState: ProposalState) {
}
}
}
fragment NewMarketSuccessorFields on NewMarket {
instrument {
name
}
successorConfiguration {
parentMarketId
}
}
fragment SuccessorProposalListFields on Proposal {
id
terms {
change {
... on NewMarket {
...NewMarketSuccessorFields
}
}
}
}
query SuccessorProposalsList {
proposalsConnection(proposalType: TYPE_NEW_MARKET, inState: STATE_OPEN) {
edges {
node {
...SuccessorProposalListFields
}
}
}
}
File diff suppressed because one or more lines are too long
+4 -3
View File
@@ -21,7 +21,8 @@ type Action<T> =
export const useFetch = <T>(
url: string,
options?: RequestInit,
initialFetch = true
initialFetch = true,
skip?: boolean
): {
state: State<T>;
refetch: (
@@ -105,10 +106,10 @@ export const useFetch = <T>(
useEffect(() => {
cancelRequest.current = false;
if (initialFetch) {
if (initialFetch && !skip) {
fetchCallback();
}
}, [fetchCallback, initialFetch, url]);
}, [fetchCallback, initialFetch, skip, url]);
useEffect(() => {
// Use the cleanup function for avoiding a possibly...
+1 -1
View File
@@ -152,7 +152,7 @@ module.exports = {
200: '#7C7E83',
300: '#626469',
400: '#44464B',
500: '#323339', // surface-container-highest
500: '#323339', // surface-container-highest, outline-surface-default
600: '#292B30',
700: '#202227',
800: '#17191E', // surface-container
@@ -1,26 +0,0 @@
import { VegaIcon, VegaIconNames } from '../icon';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
} from './dropdown-menu';
export const ActionsDropdownTrigger = () => {
return (
<DropdownMenuTrigger
className='hover:bg-vega-light-200 dark:hover:bg-vega-dark-200 [&[aria-expanded="true"]]:bg-vega-light-200 dark:[&[aria-expanded="true"]]:bg-vega-dark-200 p-0.5 rounded-full'
data-testid="dropdown-menu"
>
<VegaIcon name={VegaIconNames.KEBAB} />
</DropdownMenuTrigger>
);
};
type ActionMenuContentProps = React.ComponentProps<typeof DropdownMenuContent>;
export const ActionsDropdown = (props: ActionMenuContentProps) => {
return (
<DropdownMenu trigger={<ActionsDropdownTrigger />}>
<DropdownMenuContent {...props}></DropdownMenuContent>
</DropdownMenu>
);
};

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