Compare commits

..
94 changed files with 1654 additions and 913 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"
+1 -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
@@ -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}
@@ -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();
});
});
}
);
+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 />
</>
}
>
+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);
}
};
@@ -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;
@@ -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';
@@ -88,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={
@@ -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 -1
View File
@@ -1,4 +1,4 @@
{
"name": "@vegaprotocol/announcements",
"version": "0.0.1"
"version": "0.0.2"
}
+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++;
@@ -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);
+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')}
/>
+1 -1
View File
@@ -1,5 +1,5 @@
{
"name": "@vegaprotocol/i18n",
"version": "0.0.3",
"version": "0.0.4",
"type": "commonjs"
}
+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,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,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}
+1 -1
View File
@@ -1,4 +1,4 @@
{
"name": "@vegaprotocol/react-helpers",
"version": "0.2.4"
"version": "0.2.5"
}
+1 -1
View File
@@ -1,4 +1,4 @@
{
"name": "@vegaprotocol/tailwindcss-config",
"version": "0.0.3"
"version": "0.0.4"
}
+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
@@ -51,6 +51,12 @@ const vegaCustomClasses = plugin(function ({ addUtilities }) {
'.rtl-dir': {
direction: 'rtl',
},
'.dashed-background': {
background: `repeating-linear-gradient(0deg, transparent, transparent 2px, ${colors.neutral[900]} 2px, ${colors.neutral[900]} 4px)`,
},
'.dark .dashed-background': {
background: `repeating-linear-gradient(0deg, transparent, transparent 2px, ${colors.neutral[200]} 2px, ${colors.neutral[200]} 4px)`,
},
});
});
+1 -1
View File
@@ -1,4 +1,4 @@
{
"name": "@vegaprotocol/types",
"version": "0.0.3"
"version": "0.0.4"
}
+1 -1
View File
@@ -1,4 +1,4 @@
{
"name": "@vegaprotocol/ui-toolkit",
"version": "0.12.6"
"version": "0.12.7"
}
@@ -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>
);
};
@@ -26,7 +26,6 @@ export const CheckboxItems = () => {
{ label: 'Moving average', state: useState(false) },
{ label: 'Price monitoring bands', state: useState(false) },
];
console.log(checkboxItems);
return (
<DropdownMenu
@@ -68,6 +67,7 @@ export const RadioItems = () => {
}
>
<DropdownMenuContent>
{/* eslint-disable no-console */}
<DropdownMenuItem onSelect={() => console.log('minimize')}>
Minimize window
</DropdownMenuItem>
@@ -77,6 +77,7 @@ export const RadioItems = () => {
<DropdownMenuItem onSelect={() => console.log('smaller')}>
Smaller
</DropdownMenuItem>
{/* eslint-enable */}
<DropdownMenuSeparator />
<DropdownMenuRadioGroup value={selected} onValueChange={setSelected}>
{files.map((file) => (
@@ -1,2 +1 @@
export * from './dropdown-menu';
export * from './actions-dropdown';
@@ -43,7 +43,55 @@ const Target = ({
>
<div
className={classNames(
'health-target w-0.5 bg-black group-hover:scale-x-150 group-hover:scale-y-108',
'health-target w-0.5 bg-vega-dark-100 dark:bg-vega-light-100 group-hover:scale-x-150 group-hover:scale-y-108',
{
'h-6': !isLarge,
'h-12': isLarge,
}
)}
/>
</div>
</Tooltip>
);
};
const AuctionTarget = ({
trigger,
isLarge,
rangeLimit,
decimals,
}: {
isLarge: boolean;
trigger: number;
rangeLimit: number;
decimals: number;
}) => {
const leftPosition = new BigNumber(trigger).div(rangeLimit).multipliedBy(100);
return (
<Tooltip
description={
<div className="text-vega-dark-100 dark:text-vega-light-200">
<div className="mt-1.5 inline-flex">
<Indicator variant={Intent.None} />
</div>
<span>
{t('Auction Trigger stake')}{' '}
{addDecimalsFormatNumber(trigger, decimals)}
</span>
</div>
}
>
<div
className={classNames(
'absolute top-1/2 left-1/2 -translate-x-2/4 -translate-y-1/2 px-1.5 group'
)}
style={{
left: `${leftPosition}%`,
}}
>
<div
className={classNames(
'health-target w-0.5 group-hover:scale-x-150 group-hover:scale-y-108 dashed-background',
{
'h-6': !isLarge,
'h-12': isLarge,
@@ -133,16 +181,21 @@ export const HealthBar = ({
levels,
size = 'small',
intent,
triggerRatio,
}: {
target: string;
decimals: number;
levels: Levels[];
size?: 'small' | 'large';
intent: Intent;
triggerRatio?: string;
}) => {
const targetNumber = parseInt(target, 10);
const rangeLimit = targetNumber * 2;
const triggerRatioNumber = triggerRatio ? parseFloat(triggerRatio) : 0;
const auctionTrigger = targetNumber * triggerRatioNumber;
let lastVisibleLevel = 0;
const committedNumber = levels
.reduce((total, current, index) => {
@@ -174,7 +227,10 @@ export const HealthBar = ({
>
<Full />
<div className="health-bars h-[inherit] flex w-full gap-0.5">
<div
className="health-bars h-[inherit] flex w-full
gap-0.5 outline outline-vega-light-200 dark:outline-vega-dark-200"
>
{levels.map((p, index) => {
const { commitmentAmount, fee } = p;
const prevLevel = levels[index - 1]?.commitmentAmount;
@@ -206,6 +262,15 @@ export const HealthBar = ({
)}
</div>
</div>
{triggerRatio && (
<AuctionTarget
isLarge={isLarge}
trigger={auctionTrigger}
rangeLimit={rangeLimit}
decimals={decimals}
/>
)}
<Target isLarge={isLarge} target={target} decimals={decimals} />
</div>
</div>
@@ -0,0 +1,7 @@
export const IconExclaimationMark = ({ size = 16 }: { size: number }) => {
return (
<svg width={size} height={size} viewBox="0 0 16 16">
<path d="M8 0.879997L7.57 1.63L0.130005 14.5H15.87L8 0.879997ZM8.75 12H7.25V10.5H8.75V12ZM7.25 9.5V6H8.75V9.5H7.25Z" />
</svg>
);
};
@@ -11,6 +11,7 @@ import { IconCopy } from './svg-icons/icon-copy';
import { IconCross } from './svg-icons/icon-cross';
import { IconDeposit } from './svg-icons/icon-deposit';
import { IconEdit } from './svg-icons/icon-edit';
import { IconExclaimationMark } from './svg-icons/icon-exclaimation-mark';
import { IconForum } from './svg-icons/icon-forum';
import { IconGlobe } from './svg-icons/icon-globe';
import { IconInfo } from './svg-icons/icon-info';
@@ -46,6 +47,7 @@ export enum VegaIconNames {
CROSS = 'cross',
DEPOSIT = 'deposit',
EDIT = 'edit',
EXCLAIMATION_MARK = 'exclaimation-mark',
FORUM = 'forum',
GLOBE = 'globe',
INFO = 'info',
@@ -85,6 +87,7 @@ export const VegaIconNameMap: Record<
cross: IconCross,
deposit: IconDeposit,
edit: IconEdit,
'exclaimation-mark': IconExclaimationMark,
forum: IconForum,
globe: IconGlobe,
info: IconInfo,
@@ -16,7 +16,7 @@ export const VegaIcon = ({ size = 16, name }: VegaIconProps) => {
);
const Element = VegaIconNameMap[name];
return (
<span className={effectiveClassName}>
<span className={effectiveClassName} data-testid={`icon-${name}`}>
<Element size={size} />
</span>
);
+1
View File
@@ -49,6 +49,7 @@ export * from './toast';
export * from './toggle';
export * from './tooltip';
export * from './trading-button';
export * from './trading-dropdown';
export * from './traffic-light';
export * from './vega-icons';
export * from './vega-logo';
@@ -19,6 +19,7 @@ export const RadioItems = () => {
<span>Open</span>
</NavDropdownMenuTrigger>
<NavDropdownMenuContent>
{/* eslint-disable no-console */}
<NavDropdownMenuItem onSelect={() => console.log('minimize')}>
Minimize window
</NavDropdownMenuItem>
@@ -28,6 +29,7 @@ export const RadioItems = () => {
<NavDropdownMenuItem onSelect={() => console.log('smaller')}>
Smaller
</NavDropdownMenuItem>
{/* eslint-enable */}
</NavDropdownMenuContent>
</NavDropdownMenu>
</div>
@@ -46,6 +46,7 @@ RichDefaultSelect.args = {
name: 'rich',
placeholder: 'Select an option',
onValueChange: (v: string) => {
// eslint-disable-next-line no-console
console.log(v);
},
children: (
@@ -8,7 +8,7 @@ import type {
import { Intent } from '../../utils/intent';
type TradingButtonProps = {
size: 'large' | 'medium' | 'small';
size?: 'large' | 'medium' | 'small';
intent?: Intent;
children?: ReactNode;
icon?: ReactNode;
@@ -24,7 +24,7 @@ const getClassName = (
className?: string
) =>
classNames(
'flex items-center justify-center rounded',
'flex gap-2 items-center justify-center rounded',
// size
{
'h-12': !subLabel && size === 'large',
@@ -0,0 +1,31 @@
import { VegaIcon, VegaIconNames } from '../icon';
import {
TradingDropdown,
TradingDropdownContent,
TradingDropdownTrigger,
} from './trading-dropdown';
export const ActionsDropdownTrigger = () => {
return (
<TradingDropdownTrigger
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"
>
<button type="button">
<VegaIcon name={VegaIconNames.KEBAB} />
</button>
</TradingDropdownTrigger>
);
};
type ActionMenuContentProps = React.ComponentProps<
typeof TradingDropdownContent
>;
export const ActionsDropdown = (props: ActionMenuContentProps) => {
return (
<TradingDropdown trigger={<ActionsDropdownTrigger />}>
<TradingDropdownContent {...props} side="bottom" align="end" />
</TradingDropdown>
);
};
@@ -0,0 +1,2 @@
export * from './trading-dropdown';
export * from './actions-dropdown';
@@ -0,0 +1,38 @@
import {
TradingDropdown,
TradingDropdownContent,
TradingDropdownTrigger,
} from './trading-dropdown';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
describe('DropdownMenu', () => {
const text = 'Dropdown menu content';
// Upgrade from @radix-ui/react-dropdown-menu 0.1.6 to 2.0.2 renders
// dropdowns inline (rather than portals). Currently not using a portal
// will break the UI due to z-index issues
it('renders using a portal', async () => {
render(
<div className="test-wrapper">
<TradingDropdown
trigger={
<TradingDropdownTrigger>
<button>Trigger</button>
</TradingDropdownTrigger>
}
>
<TradingDropdownContent>
<p>{text}</p>
</TradingDropdownContent>
</TradingDropdown>
</div>
);
userEvent.click(screen.getByText(/trigger/i));
const contentElement = await screen.findByText(text);
expect(contentElement).toBeInTheDocument();
// if content is within .test-wrapper then its not been rendered in a portal
expect(contentElement.closest('.test-wrapper')).toBe(null);
});
});
@@ -0,0 +1,209 @@
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
import classNames from 'classnames';
import type { ComponentProps, ReactNode } from 'react';
import { forwardRef } from 'react';
import { VegaIcon, VegaIconNames } from '../icon';
import { useCopyTimeout } from '@vegaprotocol/react-helpers';
import CopyToClipboard from 'react-copy-to-clipboard';
import { t } from '@vegaprotocol/i18n';
const itemClass = classNames(
'relative flex gap-2 items-center rounded-sm p-2 text-sm',
'cursor-default hover:cursor-pointer',
'hover:bg-vega-clight-400 dark:hover:bg-vega-cdark-400',
'focus:bg-vega-clight-400 dark:focus:bg-vega-cdark-400',
'select-none',
'whitespace-nowrap'
);
type TradingDropdownProps = DropdownMenuPrimitive.DropdownMenuProps & {
trigger: ReactNode;
};
/**
* Contains all the parts of a dropdown menu.
*/
export const TradingDropdown = ({
children,
trigger,
...props
}: TradingDropdownProps) => {
return (
<DropdownMenuPrimitive.Root {...props}>
{trigger}
<DropdownMenuPrimitive.Portal>{children}</DropdownMenuPrimitive.Portal>
</DropdownMenuPrimitive.Root>
);
};
/**
* The button that toggles the dropdown menu.
* By default, the {@link TradingDropdownContent} will position itself against the trigger.
*/
export const TradingDropdownTrigger = forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Trigger>,
DropdownMenuPrimitive.DropdownMenuTriggerProps
>(({ className, children, ...props }, forwardedRef) => {
return (
<DropdownMenuPrimitive.Trigger
asChild={true}
ref={forwardedRef}
className={className}
{...props}
>
{children}
</DropdownMenuPrimitive.Trigger>
);
});
TradingDropdownTrigger.displayName = 'DropdownMenuTrigger';
/**
* Used to group multiple {@link TradingDropdownRadioItem}s.
*/
export const TradingDropdownRadioGroup = DropdownMenuPrimitive.RadioGroup;
/**
* The component that pops out when the dropdown menu is open.
*/
export const TradingDropdownContent = forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
React.ComponentProps<typeof DropdownMenuPrimitive.Content>
>(
(
{ className, align = 'start', side, sideOffset = 10, ...contentProps },
forwardedRef
) => (
<DropdownMenuPrimitive.Content
ref={forwardedRef}
className={classNames(
'min-w-[290px] bg-vega-clight-700 dark:bg-vega-cdark-700',
'border border-vega-clight-500 dark:border-vega-cdark-500',
'p-2 rounded z-20 text-default'
)}
align={align}
sideOffset={sideOffset}
side={side}
{...contentProps}
/>
)
);
TradingDropdownContent.displayName = 'DropdownMenuContent';
/**
* The component that contains the dropdown menu items.
*/
export const TradingDropdownItem = forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentProps<typeof DropdownMenuPrimitive.Item>
>(({ className, ...itemProps }, forwardedRef) => (
<DropdownMenuPrimitive.Item
{...itemProps}
ref={forwardedRef}
className={classNames(itemClass, className)}
/>
));
TradingDropdownItem.displayName = 'DropdownMenuItem';
/**
* An item that can be controlled and rendered like a checkbox.
*/
export const TradingDropdownCheckboxItem = forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>
>(({ className, ...checkboxItemProps }, forwardedRef) => (
<DropdownMenuPrimitive.CheckboxItem
{...checkboxItemProps}
ref={forwardedRef}
className={classNames(itemClass, 'justify-between', className)}
/>
));
TradingDropdownCheckboxItem.displayName = 'DropdownMenuCheckboxItem';
/**
* An item that can be controlled and rendered like a radio.
*/
export const TradingDropdownRadioItem = forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem> & {
inset?: boolean;
}
>(({ className, inset = false, ...radioItemProps }, forwardedRef) => (
<DropdownMenuPrimitive.RadioItem
{...radioItemProps}
ref={forwardedRef}
className={classNames(itemClass, 'justify-between', className)}
/>
));
TradingDropdownRadioItem.displayName = 'DropdownMenuRadioItem';
/**
* Renders when the parent {@link TradingDropdownCheckboxItem} or {@link TradingDropdownRadioItem} is checked.
* You can style this element directly, or you can use it as a wrapper to put an icon into, or both.
*/
export const TradingDropdownItemIndicator = forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.ItemIndicator>,
React.ComponentProps<typeof DropdownMenuPrimitive.ItemIndicator>
>(({ ...itemIndicatorProps }, forwardedRef) => (
<DropdownMenuPrimitive.ItemIndicator
{...itemIndicatorProps}
ref={forwardedRef}
className="flex-end text-vega-green"
>
<VegaIcon name={VegaIconNames.TICK} />
</DropdownMenuPrimitive.ItemIndicator>
));
TradingDropdownItemIndicator.displayName = 'DropdownMenuItemIndicator';
/**
* Used to visually separate items in the dropdown menu.
*/
export const TradingDropdownSeparator = forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
React.ComponentProps<typeof DropdownMenuPrimitive.Separator>
>(({ className, ...separatorProps }, forwardedRef) => (
<DropdownMenuPrimitive.Separator
{...separatorProps}
ref={forwardedRef}
className={classNames(
'h-px my-1 mx-2 bg-vega-clight-500 dark:bg-vega-cdark-500',
className
)}
/>
));
TradingDropdownSeparator.displayName = 'DropdownMenuSeparator';
/**
* Portal to ensure menu portions are rendered outwith where they appear in the
* DOM.
*/
export const TradingDropdownPortal = (
portalProps: ComponentProps<typeof DropdownMenuPrimitive.Portal>
) => <DropdownMenuPrimitive.Portal {...portalProps} />;
/**
* Wraps a regular DropdownMenuItem with copy to clip board functionality
*/
export const TradingDropdownCopyItem = ({
value,
text,
}: {
value: string;
text: string;
}) => {
const [copied, setCopied] = useCopyTimeout();
return (
<CopyToClipboard text={value} onCopy={() => setCopied(true)}>
<TradingDropdownItem
onClick={(e) => {
e.preventDefault();
}}
>
<VegaIcon name={VegaIconNames.COPY} size={16} />
{text}
{copied && (
<span className="text-xs text-neutral-500">{t('Copied')}</span>
)}
</TradingDropdownItem>
</CopyToClipboard>
);
};
+1 -1
View File
@@ -1,5 +1,5 @@
{
"name": "@vegaprotocol/utils",
"version": "0.0.6",
"version": "0.0.7",
"type": "commonjs"
}
@@ -4,12 +4,14 @@ import { ethers } from 'ethers';
export class Eip1193CustomBridge 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';
@@ -68,6 +70,7 @@ export class Eip1193CustomBridge 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 });
+1 -1
View File
@@ -89,7 +89,7 @@ const ConnectButton = ({
setEagerConnector(info.name);
onClick?.();
} catch (err) {
console.log('could not connect to the wallet', info.name, err);
console.warn('could not connect to the wallet', info.name, err);
// NOOP - cancelled wallet connector
}
}}
+3 -3
View File
@@ -26,7 +26,7 @@ export const initializeCoinbaseConnector = (providerUrl: string) =>
url: providerUrl,
},
onError: (error) => {
console.log('ERR_COINBASE_WALLET', error);
console.warn('ERR_COINBASE_WALLET', error);
useWeb3ConnectStore.setState({ error });
},
})
@@ -82,7 +82,7 @@ export const initializeWalletConnector = (
},
},
onError: (error) => {
console.log('ERR_WALLET_CONNECT', error.message);
console.warn('ERR_WALLET_CONNECT', error.message);
useWeb3ConnectStore.setState({ error });
},
})
@@ -98,7 +98,7 @@ export const initializeMetaMaskConnector = () =>
mustBeMetaMask: false,
},
onError: (error) => {
console.log('ERR_META_MASK', error.message);
console.warn('ERR_META_MASK', error.message);
useWeb3ConnectStore.setState({ error });
},
})
+3 -3
View File
@@ -12,7 +12,7 @@ import { t } from '@vegaprotocol/i18n';
import {
ActionsDropdown,
ButtonLink,
DropdownMenuItem,
TradingDropdownItem,
VegaIcon,
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
@@ -175,7 +175,7 @@ export const CompleteCell = ({ data, complete }: CompleteCellProps) => {
</ButtonLink>
<ActionsDropdown>
<DropdownMenuItem
<TradingDropdownItem
key={'withdrawal-approval'}
data-testid="withdrawal-approval"
onClick={() => {
@@ -186,7 +186,7 @@ export const CompleteCell = ({ data, complete }: CompleteCellProps) => {
>
<VegaIcon name={VegaIconNames.BREAKDOWN} size={16} />
{t('View withdrawal details')}
</DropdownMenuItem>
</TradingDropdownItem>
</ActionsDropdown>
</div>
);