Compare commits

..
27 changed files with 625 additions and 366 deletions
+29
View File
@@ -0,0 +1,29 @@
---
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
+9 -3
View File
@@ -29,6 +29,12 @@ jobs:
echo IS_TESTNET_RELEASE=false >> $GITHUB_ENV echo IS_TESTNET_RELEASE=false >> $GITHUB_ENV
echo IS_IPFS_RELEASE=false >> $GITHUB_ENV echo IS_IPFS_RELEASE=false >> $GITHUB_ENV
echo IS_S3_RELEASE=false >> $GITHUB_ENV echo IS_S3_RELEASE=false >> $GITHUB_ENV
echo IS_DEV_IMAGE=false >> $GITHUB_ENV
- name: Is dev image
if: ${{ contains(github.ref, 'develop') && github.event_name == 'push' && matrix.app == 'trading' }}
run: |
echo IS_DEV_IMAGE=true >> $GITHUB_ENV
- name: Is PR - name: Is PR
if: ${{ github.event_name == 'pull_request' }} if: ${{ github.event_name == 'pull_request' }}
@@ -75,7 +81,7 @@ jobs:
- name: Log in to the Container registry (docker hub) - name: Log in to the Container registry (docker hub)
uses: docker/login-action@v2 uses: docker/login-action@v2
if: ${{ env.IS_IPFS_RELEASE == 'true' }} if: ${{ env.IS_IPFS_RELEASE == 'true' || env.IS_DEV_IMAGE == 'true' }}
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
@@ -173,7 +179,7 @@ jobs:
uses: docker/build-push-action@v3 uses: docker/build-push-action@v3
continue-on-error: true continue-on-error: true
id: dockerhub-push id: dockerhub-push
if: ${{ env.IS_IPFS_RELEASE == 'true' }} if: ${{ env.IS_IPFS_RELEASE == 'true' || env.IS_DEV_IMAGE == 'true' }}
with: with:
context: . context: .
file: docker/node-outside-docker.Dockerfile file: docker/node-outside-docker.Dockerfile
@@ -183,7 +189,7 @@ jobs:
ENV_NAME=${{ env.ENV_NAME }} ENV_NAME=${{ env.ENV_NAME }}
tags: | tags: |
vegaprotocol/${{ matrix.app }}:${{ github.sha }} vegaprotocol/${{ matrix.app }}:${{ github.sha }}
vegaprotocol/${{ matrix.app }}:${{ env.IS_MAINNET_RELEASE == 'true' && 'mainnet' || env.IS_TESTNET_RELEASE == 'true' && 'testnet' || '' }} vegaprotocol/${{ matrix.app }}:${{ env.IS_MAINNET_RELEASE == 'true' && 'mainnet' || env.IS_TESTNET_RELEASE == 'true' && 'testnet' || env.IS_DEV_IMAGE == 'true' && 'develop' || '' }}
- name: Publish dist as docker image (ghcr - retry) - name: Publish dist as docker image (ghcr - retry)
uses: docker/build-push-action@v3 uses: docker/build-push-action@v3
@@ -10,7 +10,6 @@ import {
DropdownMenuTrigger, DropdownMenuTrigger,
DropdownMenuSubContent, DropdownMenuSubContent,
Icon, Icon,
Button,
} from '@vegaprotocol/ui-toolkit'; } from '@vegaprotocol/ui-toolkit';
import type { Dispatch, SetStateAction } from 'react'; import type { Dispatch, SetStateAction } from 'react';
import { FilterLabel } from './tx-filter-label'; import { FilterLabel } from './tx-filter-label';
@@ -100,15 +99,13 @@ export const TxsFilter = ({ filters, setFilters }: TxFilterProps) => {
<DropdownMenu <DropdownMenu
modal={false} modal={false}
trigger={ trigger={
<DropdownMenuTrigger className="ml-0"> <DropdownMenuTrigger>
<Button size="xs" data-testid="filter-trigger"> <FilterLabel filters={filters} />
<FilterLabel filters={filters} />
</Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
} }
> >
<DropdownMenuContent> <DropdownMenuContent>
{filters.size > 0 ? null : ( {filters.size > 1 ? null : (
<> <>
<DropdownMenuCheckboxItem <DropdownMenuCheckboxItem
onCheckedChange={() => setFilters(new Set(AllFilterOptions))} onCheckedChange={() => setFilters(new Set(AllFilterOptions))}
@@ -44,7 +44,7 @@ export const TxsListNavigation = ({
</Button> </Button>
<Button <Button
size="xs" size="xs"
disabled={!hasMoreTxs || loading} disabled={!hasMoreTxs}
onClick={() => { onClick={() => {
nextPage(); nextPage();
}} }}
@@ -86,14 +86,18 @@ describe('Txs infinite list item', () => {
render( render(
<MockedProvider> <MockedProvider>
<MemoryRouter> <MemoryRouter>
<TxsInfiniteListItem <table>
type="testType" <tbody>
submitter="testPubKey" <TxsInfiniteListItem
hash="testTxHash" type="testType"
block="1" submitter="testPubKey"
code={0} hash="testTxHash"
command={{}} block="1"
/> code={0}
command={{}}
/>
</tbody>
</table>
</MemoryRouter> </MemoryRouter>
</MockedProvider> </MockedProvider>
); );
@@ -14,9 +14,9 @@ const DEFAULT_TRUNCATE_LENGTH = 7;
export function getIdTruncateLength(screen: Screen): number { export function getIdTruncateLength(screen: Screen): number {
if (['xxxl', 'xxl'].includes(screen)) { if (['xxxl', 'xxl'].includes(screen)) {
return 64;
} else if (['xl', 'lg', 'md'].includes(screen)) {
return 32; return 32;
} else if (['xl', 'lg', 'md'].includes(screen)) {
return 16;
} }
return DEFAULT_TRUNCATE_LENGTH; return DEFAULT_TRUNCATE_LENGTH;
} }
@@ -5,11 +5,17 @@ import type { BlockExplorerTransactionResult } from '../../routes/types/block-ex
import { Side } from '@vegaprotocol/types'; import { Side } from '@vegaprotocol/types';
import { MockedProvider } from '@apollo/client/testing'; 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[] => { const generateTxs = (number: number): BlockExplorerTransactionResult[] => {
return Array.from(Array(number)).map((_) => ({ return Array.from(Array(number)).map((_) => ({
block: '87901', block: '87901',
index: 2, index: 2,
hash: '0F8B98DA0923A50786B852D9CA11E051CACC4C733E1DB93D535C7D81DBD10F6F', hash: generateHash(),
submitter: submitter:
'4b782482f587d291e8614219eb9a5ee9280fa2c58982dee71d976782a9be1964', '4b782482f587d291e8614219eb9a5ee9280fa2c58982dee71d976782a9be1964',
type: 'Submit Order', type: 'Submit Order',
@@ -2,7 +2,7 @@ import { Table, TableRow } from '../table';
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/i18n';
import { useFetch } from '@vegaprotocol/react-helpers'; import { useFetch } from '@vegaprotocol/react-helpers';
import type { BlockExplorerTransactions } from '../../routes/types/block-explorer-response'; 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 { AsyncRenderer, Loader } from '@vegaprotocol/ui-toolkit';
import EmptyList from '../empty-list/empty-list'; import EmptyList from '../empty-list/empty-list';
import { TxsInfiniteListItem } from './txs-infinite-list-item'; import { TxsInfiniteListItem } from './txs-infinite-list-item';
@@ -14,7 +14,7 @@ interface TxsPerBlockProps {
export const TxsPerBlock = ({ blockHeight, txCount }: TxsPerBlockProps) => { export const TxsPerBlock = ({ blockHeight, txCount }: TxsPerBlockProps) => {
const filters = `filters[block.height]=${blockHeight}`; const filters = `filters[block.height]=${blockHeight}`;
const url = getTxsDataUrl({ limit: txCount.toString(), filters }); const url = getTxsDataUrl({ filters, count: txCount });
const { const {
state: { data, loading, error }, state: { data, loading, error },
} = useFetch<BlockExplorerTransactions>(url); } = 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 { useFetch } from '@vegaprotocol/react-helpers';
import type { import type {
BlockExplorerTransactionResult, BlockExplorerTransactionResult,
BlockExplorerTransactions, BlockExplorerTransactions,
} from '../routes/types/block-explorer-response'; } from '../routes/types/block-explorer-response';
import { DATA_SOURCES } from '../config';
import isNumber from 'lodash/isNumber'; 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 { export function getTypeFilters(filters?: Set<FilterOption>) {
txsData: BlockExplorerTransactionResult[]; if (!filters) {
hasMoreTxs: boolean; return '';
cursor: string; } else if (filters.size > 1) {
previousCursors: string[]; return '';
hasPreviousPage: boolean; }
const forcedSingleFilter = Array.from(filters)[0];
return `filters[cmd.type]=${forcedSingleFilter}`;
} }
export interface IUseTxsData { export interface IUseTxsData {
limit: number; count?: number;
filters?: string; before?: string;
after?: string;
party?: string;
filters?: Set<FilterOption>;
} }
interface IGetTxsDataUrl { export const useTxsData = ({
limit: string; count = 25,
filters?: string; before,
} after,
filters,
party,
}: IUseTxsData) => {
const [, setSearchParams] = useSearchParams();
let hasMoreTxs = true;
let txsData: BlockExplorerTransactionResult[] = [];
export const getTxsDataUrl = ({ limit, filters }: IGetTxsDataUrl) => { const url = getTxsDataUrl({
const url = new URL(`${DATA_SOURCES.blockExplorerUrl}/transactions`); filters: getTypeFilters(filters),
count,
if (limit) { before,
url.searchParams.append('limit', limit); after,
} party,
// 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({ limit: limit.toString(), filters });
const { const {
state: { data, error, loading }, state: { data, error, loading },
refetch, refetch,
} = useFetch<BlockExplorerTransactions>(url, {}, true); } = useFetch<BlockExplorerTransactions>(url, {}, true);
useEffect(() => { if (!loading && data && isNumber(data.transactions.length)) {
if (!loading && data && isNumber(data.transactions.length)) { hasMoreTxs = data.transactions.length >= count;
setTxsState((prev) => { txsData = data.transactions;
return { }
...prev,
txsData: data.transactions,
hasMoreTxs: data.transactions.length >= limit,
cursor: data?.transactions.at(-1)?.cursor || '',
};
});
}
}, [loading, setTxsState, data, limit]);
const nextPage = useCallback(() => { const nextPage = useCallback(() => {
const c = data?.transactions.at(0)?.cursor; const after = data?.transactions.at(-1)?.cursor || '';
const newPreviousCursors = c ? [...previousCursors, c] : previousCursors; const params: URLSearchParamsInit = { after };
if (filters) {
setTxsState((prev) => ({ params.filters = Array.from(filters).join(',');
...prev, }
hasPreviousPage: true, setSearchParams(params);
previousCursors: newPreviousCursors, }, [filters, data, setSearchParams]);
}));
return refetch({
limit,
before: cursor,
});
}, [data, previousCursors, cursor, limit, refetch]);
const previousPage = useCallback(() => { const previousPage = useCallback(() => {
const previousCursor = [...previousCursors].pop(); const before = data?.transactions[0]?.cursor || '';
const newPreviousCursors = previousCursors.slice(0, -1); const params: URLSearchParamsInit = { before };
setTxsState((prev) => ({ if (filters && filters.size > 0 && filters.size === 1) {
...prev, params.filters = Array.from(filters)[0];
hasPreviousPage: newPreviousCursors.length > 0, }
previousCursors: newPreviousCursors, setSearchParams(params);
})); }, [filters, data, setSearchParams]);
return refetch({
limit,
before: previousCursor,
});
}, [previousCursors, limit, refetch]);
const refreshTxs = useCallback(async () => { const refreshTxs = useCallback(async () => {
setTxsState(() => ({ const params: URLSearchParamsInit = {};
txsData: [], if (filters && filters.size > 0 && filters.size === 1) {
cursor: '', params.filters = Array.from(filters)[0];
previousCursors: [], }
hasMoreTxs: false, setSearchParams(params);
hasPreviousPage: false,
}));
refetch({ limit }); refetch({ count: BE_TXS_PER_REQUEST });
}, [setTxsState, limit, refetch, filters]); // eslint-disable-line react-hooks/exhaustive-deps }, [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 { return {
updateFilters,
txsData, txsData,
loading, loading,
error, error,
hasMoreTxs, hasMoreTxs,
hasPreviousPage,
previousCursors,
cursor,
refreshTxs, refreshTxs,
nextPage, nextPage,
previousPage, 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({ (useFetch as jest.Mock).mockReturnValue({
state: { data: createBlockResponse(1), loading: false, error: null }, state: { data: createBlockResponse(1), loading: false, error: null },
}); });
render(renderComponent(1)); render(renderComponent(1));
await waitFor(() => screen.getByTestId('block-header')); await waitFor(() => screen.getByTestId('block-header'));
expect(screen.getByTestId('previous-block-button')).toHaveAttribute( 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 { isValidPartyId } from './components/party-id-error';
import { useDataProvider } from '@vegaprotocol/data-provider'; import { useDataProvider } from '@vegaprotocol/data-provider';
import { TxsListNavigation } from '../../../components/txs/tx-list-navigation'; 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 { AllFilterOptions, TxsFilter } from '../../../components/txs/tx-filter';
import { useSearchParams } from 'react-router-dom';
const Party = () => { const Party = () => {
const [params] = useSearchParams();
const [filters, setFilters] = useState(new Set(AllFilterOptions)); const [filters, setFilters] = useState(new Set(AllFilterOptions));
const { party } = useParams<{ party: string }>(); const { party } = useParams<{ party: string }>();
@@ -27,24 +31,21 @@ const Party = () => {
const partyId = toNonHex(party ? party : ''); const partyId = toNonHex(party ? party : '');
const { isMobile } = useScreenDimensions(); const { isMobile } = useScreenDimensions();
const visibleChars = useMemo(() => (isMobile ? 10 : 14), [isMobile]); 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 { const {
hasMoreTxs,
nextPage, nextPage,
refreshTxs,
previousPage, previousPage,
error, error,
refreshTxs,
loading, loading,
txsData, txsData,
hasPreviousPage, hasMoreTxs,
updateFilters,
} = useTxsData({ } = useTxsData({
limit: 25, filters: filters.size === 1 ? filters : undefined,
filters: f, before: params.get('before') || undefined,
after: !params.get('before') ? params.get('after') || undefined : undefined,
party: partyId,
}); });
const variables = useMemo(() => ({ partyId }), [partyId]); const variables = useMemo(() => ({ partyId }), [partyId]);
@@ -102,15 +103,21 @@ const Party = () => {
refreshTxs={refreshTxs} refreshTxs={refreshTxs}
nextPage={nextPage} nextPage={nextPage}
previousPage={previousPage} previousPage={previousPage}
hasPreviousPage={hasPreviousPage} hasPreviousPage={true}
loading={loading} loading={loading}
hasMoreTxs={hasMoreTxs} hasMoreTxs={hasMoreTxs}
> >
<TxsFilter filters={filters} setFilters={setFilters} /> <TxsFilter
filters={filters}
setFilters={(f) => {
setFilters(f);
updateFilters(f as Set<FilterOption>);
}}
/>
</TxsListNavigation> </TxsListNavigation>
{!error && txsData ? ( {!error && txsData ? (
<TxsInfiniteList <TxsInfiniteList
hasMoreTxs={hasMoreTxs} hasMoreTxs={true}
areTxsLoading={loading} areTxsLoading={loading}
txs={txsData} txs={txsData}
loadMoreTxs={nextPage} loadMoreTxs={nextPage}
+19 -16
View File
@@ -1,14 +1,14 @@
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/i18n';
import { RouteTitle } from '../../../components/route-title'; import { RouteTitle } from '../../../components/route-title';
import { TxsInfiniteList } from '../../../components/txs'; 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 { useDocumentTitle } from '../../../hooks/use-document-title';
import { useState } from 'react'; 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'; import { TxsListNavigation } from '../../../components/txs/tx-list-navigation';
import { useSearchParams } from 'react-router-dom';
const BE_TXS_PER_REQUEST = 25;
export const TxsList = () => { export const TxsList = () => {
useDocumentTitle(['Transactions']); useDocumentTitle(['Transactions']);
@@ -27,25 +27,22 @@ export const TxsList = () => {
* @returns {JSX.Element} Transaction List and controls * @returns {JSX.Element} Transaction List and controls
*/ */
export const TxsListFiltered = () => { export const TxsListFiltered = () => {
const [filters, setFilters] = useState(new Set(AllFilterOptions)); const [params] = useSearchParams();
const [filters, setFilters] = useState(getInitialFilters(params));
const f =
filters && filters.size === 1
? `filters[cmd.type]=${Array.from(filters)[0]}`
: '';
const { const {
hasMoreTxs,
nextPage, nextPage,
previousPage, previousPage,
error, error,
refreshTxs, refreshTxs,
loading, loading,
txsData, txsData,
hasPreviousPage, hasMoreTxs,
updateFilters,
} = useTxsData({ } = useTxsData({
limit: BE_TXS_PER_REQUEST, filters: filters.size === 1 ? filters : undefined,
filters: f, before: params.get('before') || undefined,
after: !params.get('before') ? params.get('after') || undefined : undefined,
}); });
return ( return (
@@ -54,11 +51,17 @@ export const TxsListFiltered = () => {
refreshTxs={refreshTxs} refreshTxs={refreshTxs}
nextPage={nextPage} nextPage={nextPage}
previousPage={previousPage} previousPage={previousPage}
hasPreviousPage={hasPreviousPage} hasPreviousPage={true}
loading={loading} loading={loading}
hasMoreTxs={hasMoreTxs} hasMoreTxs={hasMoreTxs}
> >
<TxsFilter filters={filters} setFilters={setFilters} /> <TxsFilter
filters={filters}
setFilters={(f) => {
setFilters(f);
updateFilters(f as Set<FilterOption>);
}}
/>
</TxsListNavigation> </TxsListNavigation>
<TxsInfiniteList <TxsInfiniteList
hasFilters={filters.size > 0} hasFilters={filters.size > 0}
+1 -1
View File
@@ -1,4 +1,4 @@
{ {
"name": "@vegaprotocol/announcements", "name": "@vegaprotocol/announcements",
"version": "0.0.1" "version": "0.0.2"
} }
+1 -1
View File
@@ -1,5 +1,5 @@
{ {
"name": "@vegaprotocol/i18n", "name": "@vegaprotocol/i18n",
"version": "0.0.3", "version": "0.0.4",
"type": "commonjs" "type": "commonjs"
} }
+116 -114
View File
@@ -7,123 +7,125 @@ import {
} from './utils'; } from './utils';
import * as Types from '@vegaprotocol/types'; import * as Types from '@vegaprotocol/types';
describe('getOrderToastTitle', () => { describe('WithdrawalsTable', () => {
it('should return the correct title', () => { describe('getOrderToastTitle', () => {
expect(getOrderToastTitle(Types.OrderStatus.STATUS_ACTIVE)).toBe( it('should return the correct title', () => {
'Order submitted' expect(getOrderToastTitle(Types.OrderStatus.STATUS_ACTIVE)).toBe(
); 'Order submitted'
expect(getOrderToastTitle(Types.OrderStatus.STATUS_FILLED)).toBe( );
'Order filled' expect(getOrderToastTitle(Types.OrderStatus.STATUS_FILLED)).toBe(
); 'Order filled'
expect(getOrderToastTitle(Types.OrderStatus.STATUS_PARTIALLY_FILLED)).toBe( );
'Order partially filled' expect(
); getOrderToastTitle(Types.OrderStatus.STATUS_PARTIALLY_FILLED)
expect(getOrderToastTitle(Types.OrderStatus.STATUS_PARKED)).toBe( ).toBe('Order partially filled');
'Order parked' expect(getOrderToastTitle(Types.OrderStatus.STATUS_PARKED)).toBe(
); 'Order parked'
expect(getOrderToastTitle(Types.OrderStatus.STATUS_STOPPED)).toBe( );
'Order stopped' expect(getOrderToastTitle(Types.OrderStatus.STATUS_STOPPED)).toBe(
); 'Order stopped'
expect(getOrderToastTitle(Types.OrderStatus.STATUS_CANCELLED)).toBe( );
'Order cancelled' expect(getOrderToastTitle(Types.OrderStatus.STATUS_CANCELLED)).toBe(
); 'Order cancelled'
expect(getOrderToastTitle(Types.OrderStatus.STATUS_EXPIRED)).toBe( );
'Order expired' expect(getOrderToastTitle(Types.OrderStatus.STATUS_EXPIRED)).toBe(
); 'Order expired'
expect(getOrderToastTitle(Types.OrderStatus.STATUS_REJECTED)).toBe( );
'Order rejected' expect(getOrderToastTitle(Types.OrderStatus.STATUS_REJECTED)).toBe(
); 'Order rejected'
expect(getOrderToastTitle(undefined)).toBe(undefined); );
}); expect(getOrderToastTitle(undefined)).toBe(undefined);
}); });
describe('getOrderToastIntent', () => {
it('should return the correct intent', () => {
expect(getOrderToastIntent(Types.OrderStatus.STATUS_PARKED)).toBe(
Intent.Warning
);
expect(getOrderToastIntent(Types.OrderStatus.STATUS_EXPIRED)).toBe(
Intent.Warning
);
expect(getOrderToastIntent(Types.OrderStatus.STATUS_PARTIALLY_FILLED)).toBe(
Intent.Warning
);
expect(getOrderToastIntent(Types.OrderStatus.STATUS_REJECTED)).toBe(
Intent.Danger
);
expect(getOrderToastIntent(Types.OrderStatus.STATUS_STOPPED)).toBe(
Intent.Danger
);
expect(getOrderToastIntent(Types.OrderStatus.STATUS_FILLED)).toBe(
Intent.Success
);
expect(getOrderToastIntent(Types.OrderStatus.STATUS_ACTIVE)).toBe(
Intent.Success
);
expect(getOrderToastIntent(Types.OrderStatus.STATUS_CANCELLED)).toBe(
Intent.Success
);
expect(getOrderToastIntent(undefined)).toBe(undefined);
});
});
describe('getRejectionReason', () => {
it('should return the correct rejection reason for insufficient asset balance', () => {
expect(
getRejectionReason({
rejectionReason:
Types.OrderRejectionReason.ORDER_ERROR_INSUFFICIENT_ASSET_BALANCE,
status: Types.OrderStatus.STATUS_REJECTED,
id: '',
createdAt: undefined,
size: '',
price: '',
timeInForce: Types.OrderTimeInForce.TIME_IN_FORCE_FOK,
side: Types.Side.SIDE_BUY,
marketId: '',
})
).toBe('Insufficient asset balance');
}); });
it('should return the correct rejection reason when order is stopped', () => { describe('getOrderToastIntent', () => {
expect( it('should return the correct intent', () => {
getRejectionReason({ expect(getOrderToastIntent(Types.OrderStatus.STATUS_PARKED)).toBe(
rejectionReason: null, Intent.Warning
status: Types.OrderStatus.STATUS_STOPPED, );
id: '', expect(getOrderToastIntent(Types.OrderStatus.STATUS_EXPIRED)).toBe(
createdAt: undefined, Intent.Warning
size: '', );
price: '', expect(
timeInForce: Types.OrderTimeInForce.TIME_IN_FORCE_FOK, getOrderToastIntent(Types.OrderStatus.STATUS_PARTIALLY_FILLED)
side: Types.Side.SIDE_BUY, ).toBe(Intent.Warning);
marketId: '', expect(getOrderToastIntent(Types.OrderStatus.STATUS_REJECTED)).toBe(
}) Intent.Danger
).toBe( );
'Your Fill or Kill (FOK) order was not filled and it has been stopped' expect(getOrderToastIntent(Types.OrderStatus.STATUS_STOPPED)).toBe(
); Intent.Warning
);
expect(getOrderToastIntent(Types.OrderStatus.STATUS_FILLED)).toBe(
Intent.Success
);
expect(getOrderToastIntent(Types.OrderStatus.STATUS_ACTIVE)).toBe(
Intent.Success
);
expect(getOrderToastIntent(Types.OrderStatus.STATUS_CANCELLED)).toBe(
Intent.Success
);
expect(getOrderToastIntent(undefined)).toBe(undefined);
});
}); });
});
describe('timeInForceLabel', () => { describe('getRejectionReason', () => {
it('should return the correct label for time in force', () => { it('should return the correct rejection reason for insufficient asset balance', () => {
expect(timeInForceLabel(Types.OrderTimeInForce.TIME_IN_FORCE_FOK)).toBe( expect(
`Fill or Kill (FOK)` getRejectionReason({
); rejectionReason:
expect(timeInForceLabel(Types.OrderTimeInForce.TIME_IN_FORCE_GTC)).toBe( Types.OrderRejectionReason.ORDER_ERROR_INSUFFICIENT_ASSET_BALANCE,
`Good 'til Cancelled (GTC)` status: Types.OrderStatus.STATUS_REJECTED,
); id: '',
expect(timeInForceLabel(Types.OrderTimeInForce.TIME_IN_FORCE_IOC)).toBe( createdAt: undefined,
`Immediate or Cancel (IOC)` size: '',
); price: '',
expect(timeInForceLabel(Types.OrderTimeInForce.TIME_IN_FORCE_GTT)).toBe( timeInForce: Types.OrderTimeInForce.TIME_IN_FORCE_FOK,
`Good 'til Time (GTT)` side: Types.Side.SIDE_BUY,
); marketId: '',
expect(timeInForceLabel(Types.OrderTimeInForce.TIME_IN_FORCE_GFA)).toBe( })
`Good for Auction (GFA)` ).toBe('Insufficient asset balance');
); });
expect(timeInForceLabel(Types.OrderTimeInForce.TIME_IN_FORCE_GFN)).toBe(
`Good for Normal (GFN)` it('should return the correct rejection reason when order is stopped', () => {
); expect(
expect(timeInForceLabel('')).toBe(''); getRejectionReason({
rejectionReason: null,
status: Types.OrderStatus.STATUS_STOPPED,
id: '',
createdAt: undefined,
size: '',
price: '',
timeInForce: Types.OrderTimeInForce.TIME_IN_FORCE_FOK,
side: Types.Side.SIDE_BUY,
marketId: '',
})
).toBe(
'Your Fill or Kill (FOK) order was not filled and it has been stopped'
);
});
});
describe('timeInForceLabel', () => {
it('should return the correct label for time in force', () => {
expect(timeInForceLabel(Types.OrderTimeInForce.TIME_IN_FORCE_FOK)).toBe(
`Fill or Kill (FOK)`
);
expect(timeInForceLabel(Types.OrderTimeInForce.TIME_IN_FORCE_GTC)).toBe(
`Good 'til Cancelled (GTC)`
);
expect(timeInForceLabel(Types.OrderTimeInForce.TIME_IN_FORCE_IOC)).toBe(
`Immediate or Cancel (IOC)`
);
expect(timeInForceLabel(Types.OrderTimeInForce.TIME_IN_FORCE_GTT)).toBe(
`Good 'til Time (GTT)`
);
expect(timeInForceLabel(Types.OrderTimeInForce.TIME_IN_FORCE_GFA)).toBe(
`Good for Auction (GFA)`
);
expect(timeInForceLabel(Types.OrderTimeInForce.TIME_IN_FORCE_GFN)).toBe(
`Good for Normal (GFN)`
);
expect(timeInForceLabel('')).toBe('');
});
}); });
}); });
+2 -2
View File
@@ -36,7 +36,7 @@ export const getRejectionReason = (
default: default:
return order.rejectionReason return order.rejectionReason
? t(Schema.OrderRejectionReasonMapping[order.rejectionReason]) ? t(Schema.OrderRejectionReasonMapping[order.rejectionReason])
: null; : '';
} }
}; };
@@ -79,9 +79,9 @@ export const getOrderToastIntent = (
case Schema.OrderStatus.STATUS_PARKED: case Schema.OrderStatus.STATUS_PARKED:
case Schema.OrderStatus.STATUS_EXPIRED: case Schema.OrderStatus.STATUS_EXPIRED:
case Schema.OrderStatus.STATUS_PARTIALLY_FILLED: case Schema.OrderStatus.STATUS_PARTIALLY_FILLED:
case Schema.OrderStatus.STATUS_STOPPED:
return Intent.Warning; return Intent.Warning;
case Schema.OrderStatus.STATUS_REJECTED: case Schema.OrderStatus.STATUS_REJECTED:
case Schema.OrderStatus.STATUS_STOPPED:
return Intent.Danger; return Intent.Danger;
case Schema.OrderStatus.STATUS_FILLED: case Schema.OrderStatus.STATUS_FILLED:
case Schema.OrderStatus.STATUS_ACTIVE: case Schema.OrderStatus.STATUS_ACTIVE:
+1 -1
View File
@@ -1,4 +1,4 @@
{ {
"name": "@vegaprotocol/react-helpers", "name": "@vegaprotocol/react-helpers",
"version": "0.2.4" "version": "0.2.5"
} }
+1 -1
View File
@@ -1,4 +1,4 @@
{ {
"name": "@vegaprotocol/tailwindcss-config", "name": "@vegaprotocol/tailwindcss-config",
"version": "0.0.3" "version": "0.0.4"
} }
@@ -51,6 +51,12 @@ const vegaCustomClasses = plugin(function ({ addUtilities }) {
'.rtl-dir': { '.rtl-dir': {
direction: 'rtl', 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", "name": "@vegaprotocol/types",
"version": "0.0.3" "version": "0.0.4"
} }
+1 -1
View File
@@ -1,4 +1,4 @@
{ {
"name": "@vegaprotocol/ui-toolkit", "name": "@vegaprotocol/ui-toolkit",
"version": "0.12.6" "version": "0.12.7"
} }
@@ -43,7 +43,55 @@ const Target = ({
> >
<div <div
className={classNames( 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-6': !isLarge,
'h-12': isLarge, 'h-12': isLarge,
@@ -133,16 +181,21 @@ export const HealthBar = ({
levels, levels,
size = 'small', size = 'small',
intent, intent,
triggerRatio,
}: { }: {
target: string; target: string;
decimals: number; decimals: number;
levels: Levels[]; levels: Levels[];
size?: 'small' | 'large'; size?: 'small' | 'large';
intent: Intent; intent: Intent;
triggerRatio?: string;
}) => { }) => {
const targetNumber = parseInt(target, 10); const targetNumber = parseInt(target, 10);
const rangeLimit = targetNumber * 2; const rangeLimit = targetNumber * 2;
const triggerRatioNumber = triggerRatio ? parseFloat(triggerRatio) : 0;
const auctionTrigger = targetNumber * triggerRatioNumber;
let lastVisibleLevel = 0; let lastVisibleLevel = 0;
const committedNumber = levels const committedNumber = levels
.reduce((total, current, index) => { .reduce((total, current, index) => {
@@ -174,7 +227,10 @@ export const HealthBar = ({
> >
<Full /> <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) => { {levels.map((p, index) => {
const { commitmentAmount, fee } = p; const { commitmentAmount, fee } = p;
const prevLevel = levels[index - 1]?.commitmentAmount; const prevLevel = levels[index - 1]?.commitmentAmount;
@@ -206,6 +262,15 @@ export const HealthBar = ({
)} )}
</div> </div>
</div> </div>
{triggerRatio && (
<AuctionTarget
isLarge={isLarge}
trigger={auctionTrigger}
rangeLimit={rangeLimit}
decimals={decimals}
/>
)}
<Target isLarge={isLarge} target={target} decimals={decimals} /> <Target isLarge={isLarge} target={target} decimals={decimals} />
</div> </div>
</div> </div>
+1 -1
View File
@@ -1,5 +1,5 @@
{ {
"name": "@vegaprotocol/utils", "name": "@vegaprotocol/utils",
"version": "0.0.6", "version": "0.0.7",
"type": "commonjs" "type": "commonjs"
} }
@@ -52,6 +52,7 @@ import type { Side } from '@vegaprotocol/types';
import { OrderStatusMapping } from '@vegaprotocol/types'; import { OrderStatusMapping } from '@vegaprotocol/types';
import { Size } from '@vegaprotocol/datagrid'; import { Size } from '@vegaprotocol/datagrid';
import { useWithdrawalApprovalDialog } from './withdrawal-approval-dialog'; import { useWithdrawalApprovalDialog } from './withdrawal-approval-dialog';
import * as Schema from '@vegaprotocol/types';
const intentMap: { [s in VegaTxStatus]: Intent } = { const intentMap: { [s in VegaTxStatus]: Intent } = {
Default: Intent.Primary, Default: Intent.Primary,
@@ -508,17 +509,27 @@ const VegaTxCompleteToastsContent = ({ tx }: VegaTxToastContentProps) => {
} }
if (tx.order && tx.order.rejectionReason) { if (tx.order && tx.order.rejectionReason) {
const rejectionReason = const rejectionReason = getRejectionReason(tx.order);
getRejectionReason(tx.order) || tx.order.rejectionReason || '';
return ( return (
<> <>
<ToastHeading>{getOrderToastTitle(tx.order.status)}</ToastHeading> <ToastHeading>{getOrderToastTitle(tx.order.status)}</ToastHeading>
{rejectionReason ? ( {rejectionReason ? (
<p> <p>
{t('Your order has been rejected because: %s', [rejectionReason])} {t('Your order has been %s because: %s', [
tx.order.status === Schema.OrderStatus.STATUS_STOPPED
? 'stopped'
: 'rejected',
rejectionReason,
])}
</p> </p>
) : ( ) : (
<p>{t('Your order has been rejected.')}</p> <p>
{t('Your order has been %s.', [
tx.order.status === Schema.OrderStatus.STATUS_STOPPED
? 'stopped'
: 'rejected',
])}
</p>
)} )}
{tx.txHash && ( {tx.txHash && (
<p className="break-all"> <p className="break-all">
@@ -18,109 +18,113 @@ const generateJsx = (props: TypedDataAgGrid<WithdrawalFieldsFragment>) => (
</MockedProvider> </MockedProvider>
); );
describe('renders the correct columns', () => { describe('Withdrawals', () => {
it('incomplete withdrawal', async () => { describe('renders the correct columns', () => {
const withdrawal = generateWithdrawal(); it('incomplete withdrawal', async () => {
await act(async () => { const withdrawal = generateWithdrawal();
render(generateJsx({ rowData: [withdrawal] })); await act(async () => {
render(generateJsx({ rowData: [withdrawal] }));
});
const headers = screen.getAllByRole('columnheader');
expect(headers).toHaveLength(7);
expect(headers.map((h) => h.textContent?.trim())).toEqual([
'Asset',
'Amount',
'Recipient',
'Created',
'Completed',
'Status',
'Transaction',
]);
const cells = screen.getAllByRole('gridcell');
const expectedValues = [
'asset-symbol',
'1.00',
'123456…123456',
getTimeFormat().format(new Date(withdrawal.createdTimestamp as string)),
'-',
'Pending',
'Complete withdrawal',
];
cells.forEach((cell, i) => {
expect(cell).toHaveTextContent(expectedValues[i]);
});
}); });
const headers = screen.getAllByRole('columnheader'); it('completed withdrawal', async () => {
expect(headers).toHaveLength(7); const withdrawal = generateWithdrawal({
expect(headers.map((h) => h.textContent?.trim())).toEqual([ txHash: '0x1234567891011121314',
'Asset', withdrawnTimestamp: '2022-04-21T00:00:00',
'Amount', status: Schema.WithdrawalStatus.STATUS_FINALIZED,
'Recipient', });
'Created',
'Completed',
'Status',
'Transaction',
]);
const cells = screen.getAllByRole('gridcell'); await act(async () => {
const expectedValues = [ render(generateJsx({ rowData: [withdrawal] }));
'asset-symbol', });
'1.00',
'123456…123456', const cells = screen.getAllByRole('gridcell');
getTimeFormat().format(new Date(withdrawal.createdTimestamp as string)), const expectedValues = [
'-', 'asset-symbol',
'Pending', '1.00',
'Complete withdrawal', '123456…123456',
]; getTimeFormat().format(new Date(withdrawal.createdTimestamp as string)),
cells.forEach((cell, i) => { getTimeFormat().format(
expect(cell).toHaveTextContent(expectedValues[i]); new Date(withdrawal.withdrawnTimestamp as string)
),
'Completed',
'0x1234…121314',
];
cells.forEach((cell, i) => {
expect(cell).toHaveTextContent(expectedValues[i]);
});
}); });
}); });
it('completed withdrawal', async () => { describe('StatusCell', () => {
const withdrawal = generateWithdrawal({ let props: { data: WithdrawalFieldsFragment };
txHash: '0x1234567891011121314', let withdrawal: WithdrawalFieldsFragment;
withdrawnTimestamp: '2022-04-21T00:00:00',
status: Schema.WithdrawalStatus.STATUS_FINALIZED, beforeEach(() => {
withdrawal = generateWithdrawal();
props = {
data: withdrawal,
};
}); });
await act(async () => { it('Open', () => {
render(generateJsx({ rowData: [withdrawal] })); props.data.pendingOnForeignChain = false;
props.data.txHash = null;
render(<StatusCell {...props} />);
expect(screen.getByText('Pending')).toBeInTheDocument();
}); });
const cells = screen.getAllByRole('gridcell'); it('Pending', () => {
const expectedValues = [ props.data.pendingOnForeignChain = true;
'asset-symbol', props.data.txHash = '0x123';
'1.00', render(<StatusCell {...props} />);
'123456…123456',
getTimeFormat().format(new Date(withdrawal.createdTimestamp as string)), expect(screen.getByText('Pending')).toBeInTheDocument();
getTimeFormat().format(new Date(withdrawal.withdrawnTimestamp as string)), });
'Completed',
'0x1234…121314', it('Completed', () => {
]; props.data.pendingOnForeignChain = false;
cells.forEach((cell, i) => { props.data.txHash = '0x123';
expect(cell).toHaveTextContent(expectedValues[i]); props.data.status = Schema.WithdrawalStatus.STATUS_FINALIZED;
render(<StatusCell {...props} />);
expect(screen.getByText('Completed')).toBeInTheDocument();
});
it('Rejected', () => {
props.data.pendingOnForeignChain = false;
props.data.txHash = '0x123';
props.data.status = Schema.WithdrawalStatus.STATUS_REJECTED;
render(<StatusCell {...props} />);
expect(screen.getByText('Rejected')).toBeInTheDocument();
}); });
}); });
}); });
describe('StatusCell', () => {
let props: { data: WithdrawalFieldsFragment };
let withdrawal: WithdrawalFieldsFragment;
beforeEach(() => {
withdrawal = generateWithdrawal();
props = {
data: withdrawal,
};
});
it('Open', () => {
props.data.pendingOnForeignChain = false;
props.data.txHash = null;
render(<StatusCell {...props} />);
expect(screen.getByText('Pending')).toBeInTheDocument();
});
it('Pending', () => {
props.data.pendingOnForeignChain = true;
props.data.txHash = '0x123';
render(<StatusCell {...props} />);
expect(screen.getByText('Pending')).toBeInTheDocument();
});
it('Completed', () => {
props.data.pendingOnForeignChain = false;
props.data.txHash = '0x123';
props.data.status = Schema.WithdrawalStatus.STATUS_FINALIZED;
render(<StatusCell {...props} />);
expect(screen.getByText('Completed')).toBeInTheDocument();
});
it('Rejected', () => {
props.data.pendingOnForeignChain = false;
props.data.txHash = '0x123';
props.data.status = Schema.WithdrawalStatus.STATUS_REJECTED;
render(<StatusCell {...props} />);
expect(screen.getByText('Rejected')).toBeInTheDocument();
});
});