feat(2408): trading data grid snags (#2513)

* feat: trading data grid snags

* feat: fix e2e tests, fix use order list data avoid rerender condition
This commit is contained in:
Bartłomiej Głownia 2023-01-11 15:43:52 +01:00 committed by GitHub
parent 1f96ccea68
commit 4608683bde
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 548 additions and 816 deletions

View File

@ -66,7 +66,7 @@ describe('Portfolio page tabs', { tags: '@smoke' }, () => {
it('data should be properly rendered', () => {
cy.get('.ag-center-cols-container .ag-row').should('have.length', 5);
cy.get('[title="tEURO"] button').click();
cy.contains('.ag-center-cols-container button', 'tEURO').click();
cy.getByTestId('dialog-title').should(
'have.text',
'Asset details - tEURO'

View File

@ -41,7 +41,7 @@ const AccountsManager = () => {
const rowsThisBlock = dataRef.current
? dataRef.current.slice(startRow, endRow)
: [];
const lastRow = dataRef.current?.length ?? -1;
const lastRow = dataRef.current ? dataRef.current.length : 0;
successCallback(rowsThisBlock, lastRow);
};
const { columnDefs, defaultColDef } = useAccountColumnDefinitions();

View File

@ -5,7 +5,6 @@ import { Heading } from '../../components/heading';
import { SplashLoader } from '../../components/splash-loader';
import { VegaWalletContainer } from '../../components/vega-wallet-container';
import {
PendingWithdrawalsTable,
useWithdrawals,
useWithdrawalDialog,
WithdrawalsTable,
@ -30,7 +29,7 @@ const Withdrawals = ({ name }: RouteChildProps) => {
const WithdrawPendingContainer = () => {
const openWithdrawalDialog = useWithdrawalDialog((state) => state.open);
const { t } = useTranslation();
const { pending, completed, loading, error } = useWithdrawals();
const { data, loading, error } = useWithdrawals();
if (error) {
return (
@ -60,11 +59,7 @@ const WithdrawPendingContainer = () => {
<p>{t('withdrawalsText')}</p>
<p className="mb-8">{t('withdrawalsPreparedWarningText')}</p>
<div className="w-full h-[500px]">
{pending && pending.length > 0 && (
<PendingWithdrawalsTable rowData={pending} />
)}
<h4 className="pt-3 pb-1">{t('Withdrawal history')}</h4>
<WithdrawalsTable rowData={completed} />
<WithdrawalsTable rowData={data} />
</div>
</>
);

View File

@ -20,9 +20,19 @@ describe('accounts', { tags: '@smoke' }, () => {
cy.getByTestId('tab-accounts')
.get(tradingAccountRowId)
.find('[col-id="breakdown"]')
.find('[col-id="breakdown"] [data-testid="breakdown"]')
.should('have.text', 'Breakdown');
cy.getByTestId('tab-accounts')
.get(tradingAccountRowId)
.find('[col-id="breakdown"] [data-testid="deposit"]')
.should('have.text', 'Deposit');
cy.getByTestId('tab-accounts')
.get(tradingAccountRowId)
.find('[col-id="breakdown"] [data-testid="withdraw"]')
.should('have.text', 'Withdraw');
cy.getByTestId('tab-accounts')
.get(tradingAccountRowId)
.find('[col-id="deposited"]')

View File

@ -16,7 +16,11 @@ describe('Portfolio page', { tags: '@smoke' }, () => {
cy.getByTestId('"Ledger entries"').click();
const headers = [
'Sender',
'Account type',
'Market',
'Receiver',
'Account type',
'Market',
'Transfer Type',
'Quantity',
'Asset',

View File

@ -1,6 +1,5 @@
import { AsyncRenderer, Button } from '@vegaprotocol/ui-toolkit';
import {
PendingWithdrawalsTable,
useWithdrawals,
useWithdrawalDialog,
WithdrawalsTable,
@ -9,35 +8,27 @@ import { t } from '@vegaprotocol/react-helpers';
import { VegaWalletContainer } from '../../components/vega-wallet-container';
export const WithdrawalsContainer = () => {
const { pending, completed, loading, error } = useWithdrawals();
const { data, loading, error } = useWithdrawals();
const openWithdrawDialog = useWithdrawalDialog((state) => state.open);
return (
<VegaWalletContainer>
<div className="h-full relative grid grid-rows-[1fr,min-content]">
<div className="h-full">
<AsyncRenderer
data={{ pending, completed }}
loading={loading}
error={error}
render={({ pending, completed }) => (
<>
{pending && pending.length > 0 && (
<>
<h4 className="pt-3 pb-1">{t('Pending withdrawals')}</h4>
<PendingWithdrawalsTable rowData={pending} />
</>
)}
{completed && completed.length > 0 && (
<h4 className="pt-3 pb-1">{t('Withdrawal history')}</h4>
)}
<WithdrawalsTable
data-testid="withdrawals-history"
rowData={completed}
/>
</>
)}
<div className="h-full relative">
<WithdrawalsTable
data-testid="withdrawals-history"
rowData={data}
noRowsOverlayComponent={() => null}
/>
<div className="pointer-events-none absolute inset-0">
<AsyncRenderer
data={data}
loading={loading}
error={error}
noDataCondition={(data) => !(data && data.length)}
noDataMessage={t('No withdrawals')}
/>
</div>
</div>
<div className="w-full dark:bg-black bg-white absolute bottom-0 h-auto flex justify-end px-[11px] py-2">
<Button

View File

@ -43,7 +43,7 @@ export const AccountManager = ({
const rowsThisBlock = dataRef.current
? dataRef.current.slice(startRow, endRow)
: [];
const lastRow = dataRef.current?.length ?? undefined;
const lastRow = dataRef.current ? dataRef.current.length : 0;
successCallback(rowsThisBlock, lastRow);
},
[]
@ -58,9 +58,10 @@ export const AccountManager = ({
onClickDeposit={onClickDeposit}
onClickWithdraw={onClickWithdraw}
/>
<div className="pointer-events-none absolute inset-0 top-5">
<div className="pointer-events-none absolute inset-0">
<AsyncRenderer
data={data?.length ? data : null}
data={data}
noDataCondition={(data) => !(data && data.length)}
error={error}
loading={loading}
noDataMessage={t('No accounts')}

View File

@ -38,7 +38,7 @@ describe('AccountsTable', () => {
<AccountTable rowData={singleRowData} onClickAsset={() => null} />
);
});
const expectedHeaders = ['Asset', 'Total', 'Used', 'Available', '', ''];
const expectedHeaders = ['Asset', 'Total', 'Used', 'Available', ''];
const headers = await screen.findAllByRole('columnheader');
expect(headers).toHaveLength(expectedHeaders.length);
expect(

View File

@ -5,7 +5,7 @@ import {
t,
} from '@vegaprotocol/react-helpers';
import type { VegaICellRendererParams } from '@vegaprotocol/ui-toolkit';
import { Button, ButtonLink, Dialog } from '@vegaprotocol/ui-toolkit';
import { ButtonLink, Dialog } from '@vegaprotocol/ui-toolkit';
import { TooltipCellComponent } from '@vegaprotocol/ui-toolkit';
import { AgGridDynamic as AgGrid } from '@vegaprotocol/ui-toolkit';
import { AgGridColumn } from 'ag-grid-react';
@ -42,7 +42,6 @@ export const AccountTable = forwardRef<AgGridReact, AccountTableProps>(
overlayNoRowsTemplate={t('No accounts')}
getRowId={({ data }: { data: AccountFields }) => data.asset.id}
ref={ref}
rowHeight={34}
tooltipShowDelay={500}
defaultColDef={{
flex: 1,
@ -64,7 +63,7 @@ export const AccountTable = forwardRef<AgGridReact, AccountTableProps>(
}: VegaICellRendererParams<AccountFields, 'asset.symbol'>) => {
return value ? (
<ButtonLink
data-testid="deposit"
data-testid="asset"
onClick={() => {
if (data) {
onClickAsset(data.asset.id);
@ -129,55 +128,44 @@ export const AccountTable = forwardRef<AgGridReact, AccountTableProps>(
maxWidth={300}
/>
<AgGridColumn
headerName=""
field="breakdown"
minWidth={150}
cellRenderer={({
value,
}: VegaICellRendererParams<AccountFields, 'breakdown'>) => {
return (
<ButtonLink
data-testid="breakdown"
onClick={() => {
setOpenBreakdown(!openBreakdown);
setBreakdown(value || null);
}}
>
{t('Breakdown')}
</ButtonLink>
);
}}
/>
<AgGridColumn
colId="transact"
colId="breakdown"
headerName=""
sortable={false}
minWidth={250}
minWidth={200}
type="rightAligned"
cellRenderer={({
data,
}: VegaICellRendererParams<AccountFields>) => {
return data ? (
<div className="flex gap-2 justify-end">
<Button
size="xs"
<>
<ButtonLink
data-testid="breakdown"
onClick={() => {
setOpenBreakdown(!openBreakdown);
setBreakdown(data.breakdown || null);
}}
>
{t('Breakdown')}
</ButtonLink>
<span className="mx-1" />
<ButtonLink
data-testid="deposit"
onClick={() => {
onClickDeposit && onClickDeposit(data.asset.id);
}}
>
{t('Deposit')}
</Button>
<Button
size="xs"
</ButtonLink>
<span className="mx-1" />
<ButtonLink
data-testid="withdraw"
onClick={() =>
onClickWithdraw && onClickWithdraw(data.asset.id)
}
>
{t('Withdraw')}
</Button>
</div>
</ButtonLink>
</>
) : null;
}}
/>

View File

@ -28,7 +28,7 @@ export const accountsQuery = (
return merge(defaultAccounts, override);
};
const accountFields: AccountFieldsFragment[] = [
export const accountFields: AccountFieldsFragment[] = [
{
__typename: 'AccountBalance',
type: Schema.AccountType.ACCOUNT_TYPE_GENERAL,

View File

@ -1,6 +1,7 @@
import type { AgGridReact } from 'ag-grid-react';
import { useRef } from 'react';
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
import { t } from '@vegaprotocol/react-helpers';
import { FillsTable } from './fills-table';
import type { BodyScrollEvent, BodyScrollEndEvent } from 'ag-grid-community';
import { useFillsList } from './use-fills-list';
@ -31,21 +32,25 @@ export const FillsManager = ({ partyId, marketId }: FillsManagerProps) => {
};
return (
<AsyncRenderer
loading={loading}
error={error}
data={data}
noDataCondition={() => false}
>
<div className="h-full relative">
<FillsTable
ref={gridRef}
partyId={partyId}
rowModelType={data?.length ? 'infinite' : 'clientSide'}
rowData={data?.length ? undefined : []}
rowModelType="infinite"
datasource={{ getRows }}
onBodyScrollEnd={onBodyScrollEnd}
onBodyScroll={onBodyScroll}
noRowsOverlayComponent={() => null}
/>
</AsyncRenderer>
<div className="pointer-events-none absolute inset-0">
<AsyncRenderer
loading={loading}
error={error}
data={data}
noDataMessage={t('No fills')}
noDataCondition={(data) => !(data && data.length)}
/>
</div>
</div>
);
};

View File

@ -54,8 +54,12 @@ export const useFillsList = ({
}
}
dataRef.current = data;
const avoidRerender = !!(
(dataRef.current?.length && data?.length) ||
(!dataRef.current?.length && !data?.length)
);
gridRef.current?.api?.refreshInfiniteCache();
return true;
return avoidRerender;
}
dataRef.current = data;
return false;

View File

@ -34,14 +34,14 @@ export const LedgerManager = ({ partyId }: LedgerManagerProps) => {
};
return (
<>
<div className="h-full relative">
<LedgerTable
ref={gridRef}
rowModelType="infinite"
datasource={{ getRows }}
onFilterChanged={onFilterChanged}
/>
<div className="pointer-events-none absolute inset-0 top-5">
<div className="pointer-events-none absolute inset-0">
<AsyncRenderer
loading={loading}
error={error}
@ -50,6 +50,6 @@ export const LedgerManager = ({ partyId }: LedgerManagerProps) => {
noDataCondition={(data) => !(data && data.length)}
/>
</div>
</>
</div>
);
};

View File

@ -8,7 +8,6 @@ import {
} from '@vegaprotocol/react-helpers';
import type {
VegaValueFormatterParams,
VegaICellRendererParams,
TypedDataAgGrid,
} from '@vegaprotocol/ui-toolkit';
import { AgGridDynamic as AgGrid } from '@vegaprotocol/ui-toolkit';
@ -35,58 +34,6 @@ export const TransferTooltipCellComponent = ({
);
};
type LedgerCellRendererProps = {
accountType?: Types.AccountType | null;
partyId?: string | null;
marketName?: string;
};
const LedgerCellRenderer = ({
accountType,
partyId,
marketName,
}: LedgerCellRendererProps) => {
return (
<div className="flex flex-col justify-around leading-5 h-full">
<div
className="flex"
title={`${t('ID')}: ${truncateByChars(partyId || '-')}`}
>
{truncateByChars(partyId || '')}
</div>
<div
className="flex"
title={`${t('Account type')}: ${
accountType ? AccountTypeMapping[accountType] : '-'
}`}
>
{accountType && AccountTypeMapping[accountType]}
</div>
<div className="flex" title={`${t('Market')}: ${marketName || '-'}`}>
{marketName}
</div>
</div>
);
};
const SenderCellRenderer = ({ data }: VegaICellRendererParams<LedgerEntry>) => {
const props = {
accountType: data?.senderAccountType,
partyId: data?.senderPartyId,
marketName: data?.marketSender?.tradableInstrument?.instrument?.code,
};
return <LedgerCellRenderer {...props} />;
};
const ReceiverCellRenderer = ({
data,
}: VegaICellRendererParams<LedgerEntry>) => {
const props = {
accountType: data?.receiverAccountType,
partyId: data?.receiverPartyId,
marketName: data?.marketReceiver?.tradableInstrument?.instrument?.code,
};
return <LedgerCellRenderer {...props} />;
};
type LedgerEntryProps = TypedDataAgGrid<LedgerEntry>;
export const LedgerTable = forwardRef<AgGridReact, LedgerEntryProps>(
@ -95,7 +42,6 @@ export const LedgerTable = forwardRef<AgGridReact, LedgerEntryProps>(
<AgGrid
style={{ width: '100%', height: '100%' }}
overlayNoRowsTemplate={t('No entries')}
rowHeight={70}
ref={ref}
getRowId={({ data }) => data.id}
tooltipShowDelay={500}
@ -109,13 +55,59 @@ export const LedgerTable = forwardRef<AgGridReact, LedgerEntryProps>(
>
<AgGridColumn
headerName={t('Sender')}
field="senderPartyId"
cellRenderer={({
value,
}: VegaValueFormatterParams<LedgerEntry, 'senderPartyId'>) =>
truncateByChars(value || '')
}
/>
<AgGridColumn
headerName={t('Account type')}
field="senderAccountType"
cellRenderer={SenderCellRenderer}
cellRenderer={({
value,
}: VegaValueFormatterParams<LedgerEntry, 'senderAccountType'>) =>
value ? AccountTypeMapping[value] : '-'
}
/>
<AgGridColumn
headerName={t('Market')}
field="marketSender.tradableInstrument.instrument.code"
cellRenderer={({
value,
}: VegaValueFormatterParams<
LedgerEntry,
'marketSender.tradableInstrument.instrument.code'
>) => value || '-'}
/>
<AgGridColumn
headerName={t('Receiver')}
field="receiverPartyId"
cellRenderer={({
value,
}: VegaValueFormatterParams<LedgerEntry, 'receiverPartyId'>) =>
truncateByChars(value || '')
}
/>
<AgGridColumn
headerName={t('Account type')}
field="receiverAccountType"
cellRenderer={ReceiverCellRenderer}
cellRenderer={({
value,
}: VegaValueFormatterParams<LedgerEntry, 'receiverAccountType'>) =>
value ? AccountTypeMapping[value] : '-'
}
/>
<AgGridColumn
headerName={t('Market')}
field="marketReceiver.tradableInstrument.instrument.code"
cellRenderer={({
value,
}: VegaValueFormatterParams<
LedgerEntry,
'marketReceiver.tradableInstrument.instrument.code'
>) => value || '-'}
/>
<AgGridColumn
headerName={t('Transfer Type')}

View File

@ -57,7 +57,6 @@ export const LiquidityTable = forwardRef<AgGridReact, LiquidityTableProps>(
style={{ width: '100%', height: '100%' }}
overlayNoRowsTemplate={t('No liquidity provisions')}
getRowId={({ data }) => getId(data)}
rowHeight={34}
ref={ref}
tooltipShowDelay={500}
defaultColDef={{

View File

@ -11,7 +11,7 @@ import type {
VegaICellRendererParams,
TypedDataAgGrid,
} from '@vegaprotocol/ui-toolkit';
import { AgGridDynamic as AgGrid } from '@vegaprotocol/ui-toolkit';
import { AgGridDynamic as AgGrid, ButtonLink } from '@vegaprotocol/ui-toolkit';
import { AgGridColumn } from 'ag-grid-react';
import type { AgGridReact } from 'ag-grid-react';
import * as Schema from '@vegaprotocol/types';
@ -58,14 +58,13 @@ export const MarketListTable = forwardRef<
'tradableInstrument.instrument.product.settlementAsset'
>) =>
value ? (
<button
className="hover:underline"
<ButtonLink
onClick={(e) => {
openAssetDetailsDialog(value.id, e.target as HTMLElement);
}}
>
{value.symbol}
</button>
</ButtonLink>
) : (
''
)

View File

@ -53,7 +53,7 @@ it('Renders an error state', () => {
it('Renders the order list if orders provided', async () => {
// @ts-ignore Orderlist is read only but we need to override with the forwardref to
// avoid warnings about padding refs
orderListMock.OrderList = forwardRef(() => <div>OrderList</div>);
orderListMock.OrderListTable = forwardRef(() => <div>OrderList</div>);
jest.spyOn(useDataProviderHook, 'useDataProvider').mockReturnValue({
data: [{ id: '1' } as OrderFieldsFragment],
loading: false,

View File

@ -1,5 +1,5 @@
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
import { t } from '@vegaprotocol/react-helpers';
import { t, truncateByChars } from '@vegaprotocol/react-helpers';
import { useRef, useState } from 'react';
import type {
BodyScrollEvent,
@ -7,17 +7,66 @@ import type {
FilterChangedEvent,
SortChangedEvent,
} from 'ag-grid-community';
import { Button, Intent } from '@vegaprotocol/ui-toolkit';
import type { AgGridReact } from 'ag-grid-react';
import { OrderList } from '../order-list/order-list';
import { OrderListTable } from '../order-list/order-list';
import { useOrderListData } from './use-order-list-data';
import { useHasActiveOrder } from '../../order-hooks/use-has-active-order';
import type { Filter, Sort } from './use-order-list-data';
import { useEnvironment } from '@vegaprotocol/environment';
import { Link } from '@vegaprotocol/ui-toolkit';
import type { TransactionResult } from '@vegaprotocol/wallet';
import type { VegaTxState } from '@vegaprotocol/wallet';
import { useOrderCancel } from '../../order-hooks/use-order-cancel';
import { useOrderEdit } from '../../order-hooks/use-order-edit';
import { OrderFeedback } from '../order-feedback';
import { OrderEditDialog } from '../order-list/order-edit-dialog';
import type { OrderEventFieldsFragment } from '../../order-hooks';
import * as Schema from '@vegaprotocol/types';
import type { Order } from '../order-data-provider';
export interface OrderListManagerProps {
partyId: string;
marketId?: string;
}
export const TransactionComplete = ({
transaction,
transactionResult,
}: {
transaction: VegaTxState;
transactionResult?: TransactionResult;
}) => {
const { VEGA_EXPLORER_URL } = useEnvironment();
if (!transactionResult) return null;
return (
<>
{transactionResult.status ? (
<p>{t('Transaction successful')}</p>
) : (
<p className="text-vega-pink">
{t('Transaction failed')}: {transactionResult.error}
</p>
)}
{transaction.txHash && (
<>
<p className="font-semibold mt-4">{t('Transaction')}</p>
<p>
<Link
href={`${VEGA_EXPLORER_URL}/txs/${transaction.txHash}`}
target="_blank"
>
{truncateByChars(transaction.txHash)}
</Link>
</p>
</>
)}
</>
);
};
export const OrderListManager = ({
partyId,
marketId,
@ -26,6 +75,10 @@ export const OrderListManager = ({
const scrolledToTop = useRef(true);
const [sort, setSort] = useState<Sort[] | undefined>();
const [filter, setFilter] = useState<Filter | undefined>();
const [editOrder, setEditOrder] = useState<Order | null>(null);
const orderCancel = useOrderCancel();
const orderEdit = useOrderEdit(editOrder);
const hasActiveOrder = useHasActiveOrder(marketId);
const { data, error, loading, addNewRows, getRows } = useOrderListData({
partyId,
@ -71,25 +124,158 @@ export const OrderListManager = ({
return (
<>
<OrderList
ref={gridRef}
rowModelType="infinite"
datasource={{ getRows }}
onBodyScrollEnd={onBodyScrollEnd}
onBodyScroll={onBodyScroll}
onFilterChanged={onFilterChanged}
onSortChanged={onSortChange}
marketId={marketId}
/>
<div className="pointer-events-none absolute inset-0 top-5">
<AsyncRenderer
loading={loading}
error={error}
data={data}
noDataMessage={t('No orders')}
noDataCondition={(data) => !(data && data.length)}
/>
<div className="h-full relative grid grid-rows-[1fr,min-content]">
<div className="h-full relative">
<OrderListTable
ref={gridRef}
rowModelType="infinite"
datasource={{ getRows }}
onBodyScrollEnd={onBodyScrollEnd}
onBodyScroll={onBodyScroll}
onFilterChanged={onFilterChanged}
onSortChanged={onSortChange}
cancel={(order: Order) => {
if (!order.market) return;
orderCancel.cancel({
orderId: order.id,
marketId: order.market.id,
});
}}
setEditOrder={setEditOrder}
/>
<div className="pointer-events-none absolute inset-0">
<AsyncRenderer
loading={loading}
error={error}
data={data}
noDataMessage={t('No orders')}
noDataCondition={(data) => !(data && data.length)}
/>
</div>
</div>
{hasActiveOrder && (
<div className="w-full dark:bg-black bg-white absolute bottom-0 h-auto flex justify-end px-[11px] py-2">
<Button
size="sm"
onClick={() => orderCancel.cancel({ marketId })}
data-testid="cancelAll"
>
{t('Cancel all')}
</Button>
</div>
)}
</div>
<orderCancel.Dialog
title={getCancelDialogTitle(orderCancel)}
intent={getCancelDialogIntent(orderCancel)}
content={{
Complete: orderCancel.cancelledOrder ? (
<OrderFeedback
transaction={orderCancel.transaction}
order={orderCancel.cancelledOrder}
/>
) : (
<TransactionComplete {...orderCancel} />
),
}}
/>
<orderEdit.Dialog
title={getEditDialogTitle(orderEdit.updatedOrder?.status)}
content={{
Complete: (
<OrderFeedback
transaction={orderEdit.transaction}
order={orderEdit.updatedOrder}
/>
),
}}
/>
{editOrder && (
<OrderEditDialog
isOpen={Boolean(editOrder)}
onChange={(isOpen) => {
if (!isOpen) setEditOrder(null);
}}
order={editOrder}
onSubmit={(fields) => {
setEditOrder(null);
orderEdit.edit({ price: fields.limitPrice, size: fields.size });
}}
/>
)}
</>
);
};
export const getEditDialogTitle = (
status?: Schema.OrderStatus
): string | undefined => {
if (!status) {
return;
}
switch (status) {
case Schema.OrderStatus.STATUS_ACTIVE:
return t('Order updated');
case Schema.OrderStatus.STATUS_FILLED:
return t('Order filled');
case Schema.OrderStatus.STATUS_PARTIALLY_FILLED:
return t('Order partially filled');
case Schema.OrderStatus.STATUS_PARKED:
return t('Order parked');
case Schema.OrderStatus.STATUS_STOPPED:
return t('Order stopped');
case Schema.OrderStatus.STATUS_EXPIRED:
return t('Order expired');
case Schema.OrderStatus.STATUS_CANCELLED:
return t('Order cancelled');
case Schema.OrderStatus.STATUS_REJECTED:
return t('Order rejected');
default:
return t('Order amendment failed');
}
};
export const getCancelDialogIntent = ({
cancelledOrder,
transactionResult,
}: {
cancelledOrder: OrderEventFieldsFragment | null;
transactionResult?: TransactionResult;
}): Intent | undefined => {
if (cancelledOrder) {
if (cancelledOrder.status === Schema.OrderStatus.STATUS_CANCELLED) {
return Intent.Success;
}
return Intent.Danger;
}
if (transactionResult) {
if ('error' in transactionResult && transactionResult.error) {
return Intent.Danger;
}
return Intent.Success;
}
return;
};
export const getCancelDialogTitle = ({
cancelledOrder,
transactionResult,
}: {
cancelledOrder: OrderEventFieldsFragment | null;
transactionResult?: TransactionResult;
}): string | undefined => {
if (cancelledOrder) {
if (cancelledOrder.status === Schema.OrderStatus.STATUS_CANCELLED) {
return t('Order cancelled');
}
return t('Order cancellation failed');
}
if (transactionResult) {
if (transactionResult.status) {
return t('Orders cancelled');
}
return t('Orders not cancelled');
}
return;
};

View File

@ -155,10 +155,11 @@ export const useOrderListData = ({
).length;
}
}
dataRef.current = filterOrders(data, variables);
const filteredData = filterOrders(data, variables);
dataRef.current = filteredData;
const avoidRerender = !!(
(dataRef.current?.length && data?.length) ||
(!dataRef.current?.length && !data?.length)
(dataRef.current?.length && filteredData?.length) ||
(!dataRef.current?.length && !filteredData?.length)
);
gridRef.current?.api?.refreshInfiniteCache();
return avoidRerender;

View File

@ -16,11 +16,9 @@ import {
} from '../mocks/generate-orders';
const defaultProps: OrderListTableProps = {
hasActiveOrder: true,
rowData: [],
setEditOrder: jest.fn(),
cancel: jest.fn(),
cancelAll: jest.fn(),
};
const generateJsx = (

View File

@ -1,5 +1,5 @@
import type { Story, Meta } from '@storybook/react';
import { OrderList, OrderListTable } from './order-list';
import { OrderListTable } from './order-list';
import { useState } from 'react';
import type { VegaTxState } from '@vegaprotocol/wallet';
import { VegaTransactionDialog, VegaTxStatus } from '@vegaprotocol/wallet';
@ -8,8 +8,8 @@ import { OrderEditDialog } from './order-edit-dialog';
import type { Order } from '../order-data-provider';
export default {
component: OrderList,
title: 'OrderList',
component: OrderListTable,
title: 'OrderListTable',
} as Meta;
const Template: Story = (args) => {
@ -17,10 +17,8 @@ const Template: Story = (args) => {
return (
<div style={{ height: 1000 }}>
<OrderListTable
hasActiveOrder
rowData={args.data}
cancel={cancel}
cancelAll={cancel}
setEditOrder={() => {
return;
}}
@ -47,10 +45,8 @@ const Template2: Story = (args) => {
<>
<div style={{ height: 1000 }}>
<OrderListTable
hasActiveOrder
rowData={args.data}
cancel={cancel}
cancelAll={cancel}
setEditOrder={setEditOrder}
/>
</div>

View File

@ -1,4 +1,3 @@
import { useEnvironment } from '@vegaprotocol/environment';
import {
addDecimalsFormatNumber,
getDateTimeFormat,
@ -6,28 +5,19 @@ import {
negativeClassNames,
positiveClassNames,
t,
truncateByChars,
SetFilter,
DateRangeFilter,
} from '@vegaprotocol/react-helpers';
import * as Schema from '@vegaprotocol/types';
import {
AgGridDynamic as AgGrid,
Button,
Intent,
ButtonLink,
Link,
} from '@vegaprotocol/ui-toolkit';
import type { TransactionResult } from '@vegaprotocol/wallet';
import type { VegaTxState } from '@vegaprotocol/wallet';
import { AgGridColumn } from 'ag-grid-react';
import BigNumber from 'bignumber.js';
import { forwardRef, useState } from 'react';
import { forwardRef } from 'react';
import type { TypedDataAgGrid } from '@vegaprotocol/ui-toolkit';
import { useOrderCancel } from '../../order-hooks/use-order-cancel';
import { useHasActiveOrder } from '../../order-hooks/use-has-active-order';
import { useOrderEdit } from '../../order-hooks/use-order-edit';
import { OrderFeedback } from '../order-feedback';
import { OrderEditDialog } from './order-edit-dialog';
import type {
VegaICellRendererParams,
@ -35,124 +25,16 @@ import type {
} from '@vegaprotocol/ui-toolkit';
import type { AgGridReact } from 'ag-grid-react';
import type { Order } from '../order-data-provider';
import type { OrderEventFieldsFragment } from '../../order-hooks';
type OrderListProps = TypedDataAgGrid<Order> & { marketId?: string };
export const TransactionComplete = ({
transaction,
transactionResult,
}: {
transaction: VegaTxState;
transactionResult?: TransactionResult;
}) => {
const { VEGA_EXPLORER_URL } = useEnvironment();
if (!transactionResult) return null;
return (
<>
{transactionResult.status ? (
<p>{t('Transaction successful')}</p>
) : (
<p className="text-vega-pink">
{t('Transaction failed')}: {transactionResult.error}
</p>
)}
{transaction.txHash && (
<>
<p className="font-semibold mt-4">{t('Transaction')}</p>
<p>
<Link
href={`${VEGA_EXPLORER_URL}/txs/${transaction.txHash}`}
target="_blank"
>
{truncateByChars(transaction.txHash)}
</Link>
</p>
</>
)}
</>
);
};
export const OrderList = forwardRef<AgGridReact, OrderListProps>(
(props, ref) => {
const [editOrder, setEditOrder] = useState<Order | null>(null);
const orderCancel = useOrderCancel();
const orderEdit = useOrderEdit(editOrder);
const hasActiveOrder = useHasActiveOrder(props.marketId);
return (
<>
<OrderListTable
{...props}
hasActiveOrder={hasActiveOrder}
cancelAll={() => {
orderCancel.cancel({
marketId: props.marketId,
});
}}
cancel={(order: Order) => {
if (!order.market) return;
orderCancel.cancel({
orderId: order.id,
marketId: order.market.id,
});
}}
ref={ref}
setEditOrder={setEditOrder}
/>
<orderCancel.Dialog
title={getCancelDialogTitle(orderCancel)}
intent={getCancelDialogIntent(orderCancel)}
content={{
Complete: orderCancel.cancelledOrder ? (
<OrderFeedback
transaction={orderCancel.transaction}
order={orderCancel.cancelledOrder}
/>
) : (
<TransactionComplete {...orderCancel} />
),
}}
/>
<orderEdit.Dialog
title={getEditDialogTitle(orderEdit.updatedOrder?.status)}
content={{
Complete: (
<OrderFeedback
transaction={orderEdit.transaction}
order={orderEdit.updatedOrder}
/>
),
}}
/>
{editOrder && (
<OrderEditDialog
isOpen={Boolean(editOrder)}
onChange={(isOpen) => {
if (!isOpen) setEditOrder(null);
}}
order={editOrder}
onSubmit={(fields) => {
setEditOrder(null);
orderEdit.edit({ price: fields.limitPrice, size: fields.size });
}}
/>
)}
</>
);
}
);
export type OrderListTableProps = OrderListProps & {
cancel: (order: Order) => void;
cancelAll: () => void;
hasActiveOrder: boolean;
setEditOrder: (order: Order) => void;
};
export const OrderListTable = forwardRef<AgGridReact, OrderListTableProps>(
({ cancel, cancelAll, setEditOrder, hasActiveOrder, ...props }, ref) => {
({ cancel, setEditOrder, ...props }, ref) => {
return (
<AgGrid
ref={ref}
@ -164,8 +46,6 @@ export const OrderListTable = forwardRef<AgGridReact, OrderListTableProps>(
}}
style={{ width: '100%', height: '100%' }}
getRowId={({ data }) => data.id}
rowHeight={34}
pinnedBottomRowData={[{}]}
{...props}
>
<AgGridColumn
@ -202,9 +82,6 @@ export const OrderListTable = forwardRef<AgGridReact, OrderListTableProps>(
data,
node,
}: VegaValueFormatterParams<Order, 'size'>) => {
if (node?.rowPinned) {
return '';
}
if (!data?.market || !isNumeric(value)) {
return '-';
}
@ -230,9 +107,6 @@ export const OrderListTable = forwardRef<AgGridReact, OrderListTableProps>(
value,
node,
}: VegaValueFormatterParams<Order, 'type'>) => {
if (node?.rowPinned) {
return '';
}
if (!value) return '-';
if (order?.peggedOrder) return t('Pegged');
if (order?.liquidityProvision) return t('Liquidity provision');
@ -279,9 +153,6 @@ export const OrderListTable = forwardRef<AgGridReact, OrderListTableProps>(
value,
node,
}: VegaValueFormatterParams<Order, 'remaining'>) => {
if (node?.rowPinned) {
return '';
}
if (!data?.market || !isNumeric(value) || !isNumeric(data.size)) {
return '-';
}
@ -304,9 +175,6 @@ export const OrderListTable = forwardRef<AgGridReact, OrderListTableProps>(
data,
node,
}: VegaValueFormatterParams<Order, 'price'>) => {
if (node?.rowPinned) {
return '';
}
if (
!data?.market ||
data.type === Schema.OrderType.TYPE_MARKET ||
@ -355,9 +223,6 @@ export const OrderListTable = forwardRef<AgGridReact, OrderListTableProps>(
value,
node,
}: VegaValueFormatterParams<Order, 'updatedAt'>) => {
if (node?.rowPinned) {
return '';
}
return value ? getDateTimeFormat().format(new Date(value)) : '-';
}}
/>
@ -366,44 +231,22 @@ export const OrderListTable = forwardRef<AgGridReact, OrderListTableProps>(
headerName=""
field="status"
minWidth={150}
type="rightAligned"
cellRenderer={({ data, node }: VegaICellRendererParams<Order>) => {
if (node?.rowPinned) {
return (
hasActiveOrder && (
<div className="flex gap-2 items-center h-full justify-end">
<Button
size="xs"
data-testid="cancelAll"
onClick={() => cancelAll()}
>
{t('Cancel all')}
</Button>
</div>
)
);
}
if (isOrderAmendable(data)) {
return data ? (
<div className="flex gap-2 items-center h-full justify-end">
<Button
data-testid="edit"
onClick={() => setEditOrder(data)}
size="xs"
>
{t('Edit')}
</Button>
<Button
size="xs"
data-testid="cancel"
onClick={() => cancel(data)}
>
{t('Cancel')}
</Button>
</div>
) : null;
}
return null;
return data && isOrderAmendable(data) ? (
<>
<ButtonLink
data-testid="edit"
onClick={() => setEditOrder(data)}
>
{t('Edit')}
</ButtonLink>
<span className="mx-1" />
<ButtonLink data-testid="cancel" onClick={() => cancel(data)}>
{t('Cancel')}
</ButtonLink>
</>
) : null;
}}
/>
</AgGrid>
@ -436,76 +279,3 @@ export const isOrderAmendable = (order: Order | undefined) => {
return false;
};
export const getEditDialogTitle = (
status?: Schema.OrderStatus
): string | undefined => {
if (!status) {
return;
}
switch (status) {
case Schema.OrderStatus.STATUS_ACTIVE:
return t('Order updated');
case Schema.OrderStatus.STATUS_FILLED:
return t('Order filled');
case Schema.OrderStatus.STATUS_PARTIALLY_FILLED:
return t('Order partially filled');
case Schema.OrderStatus.STATUS_PARKED:
return t('Order parked');
case Schema.OrderStatus.STATUS_STOPPED:
return t('Order stopped');
case Schema.OrderStatus.STATUS_EXPIRED:
return t('Order expired');
case Schema.OrderStatus.STATUS_CANCELLED:
return t('Order cancelled');
case Schema.OrderStatus.STATUS_REJECTED:
return t('Order rejected');
default:
return t('Order amendment failed');
}
};
export const getCancelDialogIntent = ({
cancelledOrder,
transactionResult,
}: {
cancelledOrder: OrderEventFieldsFragment | null;
transactionResult?: TransactionResult;
}): Intent | undefined => {
if (cancelledOrder) {
if (cancelledOrder.status === Schema.OrderStatus.STATUS_CANCELLED) {
return Intent.Success;
}
return Intent.Danger;
}
if (transactionResult) {
if ('error' in transactionResult && transactionResult.error) {
return Intent.Danger;
}
return Intent.Success;
}
return;
};
export const getCancelDialogTitle = ({
cancelledOrder,
transactionResult,
}: {
cancelledOrder: OrderEventFieldsFragment | null;
transactionResult?: TransactionResult;
}): string | undefined => {
if (cancelledOrder) {
if (cancelledOrder.status === Schema.OrderStatus.STATUS_CANCELLED) {
return t('Order cancelled');
}
return t('Order cancellation failed');
}
if (transactionResult) {
if (transactionResult.status) {
return t('Orders cancelled');
}
return t('Orders not cancelled');
}
return;
};

View File

@ -24,20 +24,22 @@ export const PositionsManager = ({ partyId }: PositionsManagerProps) => {
} = useClosePosition();
return (
<>
<AsyncRenderer
loading={loading}
error={error}
data={data}
noDataMessage={t('No positions')}
noDataCondition={(data) => !(data && data.length)}
>
<PositionsTable
ref={gridRef}
rowData={data}
onClose={(position) => submit(position)}
<div className="h-full relative">
<PositionsTable
ref={gridRef}
rowData={data}
onClose={(position) => submit(position)}
noRowsOverlayComponent={() => null}
/>
<div className="pointer-events-none absolute inset-0">
<AsyncRenderer
loading={loading}
error={error}
data={data}
noDataMessage={t('No positions')}
noDataCondition={(data) => !(data && data.length)}
/>
</AsyncRenderer>
</div>
<Dialog
intent={getDialogIntent(transactionResult)}
icon={getDialogIcon(transactionResult)}
@ -55,7 +57,7 @@ export const PositionsManager = ({ partyId }: PositionsManagerProps) => {
),
}}
/>
</>
</div>
);
};

View File

@ -62,12 +62,11 @@ it('Render correct columns', async () => {
]);
});
it('Splits market name', async () => {
it('renders market name', async () => {
await act(async () => {
render(<PositionsTable rowData={singleRowData} />);
});
expect(screen.getByText('ETH/BTC')).toBeTruthy();
expect(screen.getByText('31 july 2022')).toBeTruthy();
expect(screen.getByText('ETH/BTC (31 july 2022)')).toBeTruthy();
});
it('Does not fail if the market name does not match the split pattern', async () => {

View File

@ -25,30 +25,15 @@ import { AgGridColumn } from 'ag-grid-react';
import type { AgGridReact } from 'ag-grid-react';
import type { Position } from './positions-data-providers';
import * as Schema from '@vegaprotocol/types';
import { Button, TooltipCellComponent } from '@vegaprotocol/ui-toolkit';
import { ButtonLink, TooltipCellComponent } from '@vegaprotocol/ui-toolkit';
import { getRowId } from './use-positions-data';
import type { VegaICellRendererParams } from '@vegaprotocol/ui-toolkit';
interface Props extends TypedDataAgGrid<Position> {
onClose?: (data: Position) => void;
style?: CSSProperties;
}
export interface MarketNameCellProps {
valueFormatted?: [string, string];
}
export const MarketNameCell = ({ valueFormatted }: MarketNameCellProps) => {
if (valueFormatted && valueFormatted[1]) {
return (
<div className="leading-tight">
<div>{valueFormatted[0]}</div>
<div>{valueFormatted[1]}</div>
</div>
);
}
return (valueFormatted && valueFormatted[0]) || undefined;
};
export interface AmountCellProps {
valueFormatted?: Pick<
Position,
@ -80,24 +65,6 @@ export const AmountCell = ({ valueFormatted }: AmountCellProps) => {
AmountCell.displayName = 'AmountCell';
const ButtonCell = ({
onClick,
data,
}: {
onClick: (position: Position) => void;
data: Position;
}) => {
return (
<Button
data-testid="close-position"
onClick={() => onClick(data)}
size="xs"
>
{t('Close')}
</Button>
);
};
export const PositionsTable = forwardRef<AgGridReact, Props>(
({ onClose, ...props }, ref) => {
return (
@ -105,7 +72,6 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
style={{ width: '100%', height: '100%' }}
overlayNoRowsTemplate={t('No positions')}
getRowId={getRowId}
rowHeight={34}
ref={ref}
tooltipShowDelay={500}
defaultColDef={{
@ -119,24 +85,7 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
components={{ AmountCell, PriceFlashCell, ProgressBarCell }}
{...props}
>
<AgGridColumn
headerName={t('Market')}
field="marketName"
cellRenderer={MarketNameCell}
valueFormatter={({
value,
}: VegaValueFormatterParams<Position, 'marketName'>) => {
if (!value) {
return undefined;
}
// split market name into two parts, 'Part1 (Part2)' or 'Part1 - Part2'
const matches = value.match(/^(.*)(\((.*)\)| - (.*))\s*$/);
if (matches && matches[1] && matches[3]) {
return [matches[1].trim(), matches[3].trim()];
}
return [value];
}}
/>
<AgGridColumn headerName={t('Market')} field="marketName" />
<AgGridColumn
headerName={t('Notional')}
headerTooltip={t('Mark price x open volume.')}
@ -413,12 +362,15 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
/>
{onClose ? (
<AgGridColumn
cellRendererSelector={(): CellRendererSelectorResult => {
return {
component: ButtonCell,
};
}}
cellRendererParams={{ onClick: onClose }}
type="rightAligned"
cellRenderer={({ data }: VegaICellRendererParams<Position>) => (
<ButtonLink
data-testid="close-position"
onClick={() => data && onClose(data)}
>
{t('Close')}
</ButtonLink>
)}
/>
) : null}
</AgGrid>

View File

@ -20,13 +20,17 @@ const agGridDarkVariables = `
.ag-theme-balham-dark .ag-row {
border-width: 1px 0;
border-bottom: solid transparent;
border-bottom: 1px solid transparent;
}
.ag-theme-balham-dark .ag-react-container {
overflow: hidden;
text-overflow: ellipsis;
}
.ag-theme-balham-dark .ag-cell, .ag-theme-balham-dark .ag-full-width-row .ag-cell-wrapper.ag-row-group {
line-height: calc(min(var(--ag-line-height, 26px), 26px) - 4px);
}
`;
export const AgGrid = ({

View File

@ -20,13 +20,17 @@ const agGridLightVariables = `
.ag-theme-balham .ag-row {
border-width: 1px 0;
border-bottom: solid transparent;
border-bottom: 1px solid transparent;
}
.ag-theme-balham .ag-react-container {
overflow: hidden;
text-overflow: ellipsis;
}
.ag-theme-balham .ag-cell, .ag-theme-balham .ag-full-width-row .ag-cell-wrapper.ag-row-group {
line-height: calc(min(var(--ag-line-height, 26px), 26px) - 4px);
}
`;
export const AgGrid = ({

View File

@ -4,7 +4,6 @@ export * from './lib/withdraw-form';
export * from './lib/withdraw-form-container';
export * from './lib/withdraw-manager';
export * from './lib/withdrawals-table';
export * from './lib/pending-withdrawals-table';
export * from './lib/withdrawal-feedback';
export * from './lib/use-complete-withdraw';
export * from './lib/use-create-withdraw';

View File

@ -1,65 +0,0 @@
import { MockedProvider } from '@apollo/client/testing';
import { act, fireEvent, render, screen } from '@testing-library/react';
import { generateWithdrawal } from './test-helpers';
import { CompleteCell } from './pending-withdrawals-table';
import { PendingWithdrawalsTable } from './pending-withdrawals-table';
import { getTimeFormat } from '@vegaprotocol/react-helpers';
import type { TypedDataAgGrid } from '@vegaprotocol/ui-toolkit';
import type { WithdrawalFieldsFragment } from './__generated__/Withdrawal';
jest.mock('@web3-react/core', () => ({
useWeb3React: () => ({ provider: undefined }),
}));
const generateTable = (props: TypedDataAgGrid<WithdrawalFieldsFragment>) => (
<MockedProvider>
<PendingWithdrawalsTable {...props} />
</MockedProvider>
);
describe('PendingWithdrawalsTable', () => {
it('displays correct columns', async () => {
const withdrawal = generateWithdrawal();
await act(async () => {
render(generateTable({ rowData: [withdrawal] }));
});
const headers = screen.getAllByRole('columnheader');
expect(headers).toHaveLength(5);
expect(headers.map((h) => h.textContent?.trim())).toEqual([
'Asset',
'Amount',
'Recipient',
'Created',
'',
]);
});
it('displays given withdrawals', async () => {
const withdrawal = generateWithdrawal();
await act(async () => {
render(generateTable({ rowData: [withdrawal] }));
});
const cells = screen.getAllByRole('gridcell');
const expectedValues = [
'asset-symbol',
'1.00',
'123456…123456',
getTimeFormat().format(new Date(withdrawal.createdTimestamp)),
'Complete withdrawal',
];
cells.forEach((cell, i) => {
expect(cell).toHaveTextContent(expectedValues[i]);
});
});
});
describe('CompleteCell', () => {
const mockComplete = jest.fn();
const data = generateWithdrawal();
it('opens the dialog', () => {
render(<CompleteCell complete={mockComplete} data={data} />);
fireEvent.click(
screen.getByText('Complete withdrawal', { selector: 'button' })
);
expect(mockComplete).toBeCalled();
});
});

View File

@ -1,177 +0,0 @@
import { AgGridColumn } from 'ag-grid-react';
import {
getDateTimeFormat,
t,
truncateByChars,
addDecimalsFormatNumber,
isNumeric,
} from '@vegaprotocol/react-helpers';
import type {
TypedDataAgGrid,
VegaICellRendererParams,
VegaValueFormatterParams,
} from '@vegaprotocol/ui-toolkit';
import { Button } from '@vegaprotocol/ui-toolkit';
import {
Link,
AgGridDynamic as AgGrid,
Intent,
Loader,
Icon,
} from '@vegaprotocol/ui-toolkit';
import { useEnvironment } from '@vegaprotocol/environment';
import { useEthWithdrawApprovalsStore } from '@vegaprotocol/web3';
import type { WithdrawalFieldsFragment } from './__generated__/Withdrawal';
import type { VerifyState } from './use-verify-withdrawal';
import { ApprovalStatus } from './use-verify-withdrawal';
export const PendingWithdrawalsTable = (
props: TypedDataAgGrid<WithdrawalFieldsFragment>
) => {
const { ETHERSCAN_URL } = useEnvironment();
const createWithdrawApproval = useEthWithdrawApprovalsStore(
(store) => store.create
);
return (
<AgGrid
overlayNoRowsTemplate={t('No withdrawals')}
defaultColDef={{ flex: 1, resizable: true }}
style={{ width: '100%' }}
components={{ CompleteCell }}
suppressCellFocus={true}
domLayout="autoHeight"
rowHeight={30}
{...props}
>
<AgGridColumn headerName="Asset" field="asset.symbol" />
<AgGridColumn
headerName={t('Amount')}
field="amount"
valueFormatter={({
value,
data,
}: VegaValueFormatterParams<WithdrawalFieldsFragment, 'amount'>) => {
return isNumeric(value) && data?.asset
? addDecimalsFormatNumber(value, data.asset.decimals)
: null;
}}
/>
<AgGridColumn
headerName={t('Recipient')}
field="details.receiverAddress"
cellRenderer={({
ethUrl,
value,
valueFormatted,
}: VegaICellRendererParams<
WithdrawalFieldsFragment,
'details.receiverAddress'
> & {
ethUrl: string;
}) => (
<Link
title={t('View on Etherscan (opens in a new tab)')}
href={`${ethUrl}/address/${value}`}
data-testid="etherscan-link"
target="_blank"
>
{valueFormatted}
</Link>
)}
cellRendererParams={{ ethUrl: ETHERSCAN_URL }}
valueFormatter={({
value,
}: VegaValueFormatterParams<
WithdrawalFieldsFragment,
'details.receiverAddress'
>) => {
if (!value) return '-';
return truncateByChars(value);
}}
/>
<AgGridColumn
headerName={t('Created')}
field="createdTimestamp"
valueFormatter={({
value,
}: VegaValueFormatterParams<
WithdrawalFieldsFragment,
'createdTimestamp'
>) => {
return value ? getDateTimeFormat().format(new Date(value)) : '';
}}
/>
<AgGridColumn
headerName=""
field="status"
flex={2}
cellRendererParams={{
complete: (withdrawal: WithdrawalFieldsFragment) => {
createWithdrawApproval(withdrawal);
},
}}
cellRenderer="CompleteCell"
/>
</AgGrid>
);
};
export type CompleteCellProps = {
data: WithdrawalFieldsFragment;
complete: (withdrawal: WithdrawalFieldsFragment) => void;
};
export const CompleteCell = ({ data, complete }: CompleteCellProps) => (
<Button
data-testid="complete-withdrawal"
size="xs"
onClick={() => complete(data)}
>
{t('Complete withdrawal')}
</Button>
);
export const getVerifyDialogProps = (status: ApprovalStatus) => {
if (status === ApprovalStatus.Error) {
return {
intent: Intent.Danger,
icon: <Icon name="warning-sign" />,
};
}
if (status === ApprovalStatus.Pending) {
return { intent: Intent.None, icon: <Loader size="small" /> };
}
if (status === ApprovalStatus.Delayed) {
return { intent: Intent.Warning, icon: <Icon name="time" /> };
}
return { intent: Intent.None };
};
export const VerificationStatus = ({ state }: { state: VerifyState }) => {
if (state.status === ApprovalStatus.Error) {
return <p>{t('Something went wrong')}</p>;
}
if (state.status === ApprovalStatus.Pending) {
return <p>{t('Verifying...')}</p>;
}
if (state.status === ApprovalStatus.Delayed && state.completeTimestamp) {
const formattedTime = getDateTimeFormat().format(
new Date(state.completeTimestamp)
);
return (
<>
<p className="mb-2">
{t("The amount you're withdrawing has triggered a time delay")}
</p>
<p>{t(`Cannot be completed until ${formattedTime}`)}</p>
</>
);
}
return null;
};

View File

@ -1,8 +1,7 @@
import orderBy from 'lodash/orderBy';
import type { UpdateQueryFn } from '@apollo/client/core/watchQueryOptions';
import { useVegaWallet } from '@vegaprotocol/wallet';
import uniqBy from 'lodash/uniqBy';
import { useEffect, useMemo } from 'react';
import { useEffect } from 'react';
import {
useWithdrawalsQuery,
WithdrawalEventDocument,
@ -27,7 +26,7 @@ export const useWithdrawals = () => {
useEffect(() => {
if (!pubKey) return;
const unsub = subscribeToMore<
const unsubscribe = subscribeToMore<
WithdrawalEventSubscription,
WithdrawalEventSubscriptionVariables
>({
@ -37,49 +36,23 @@ export const useWithdrawals = () => {
});
return () => {
unsub();
unsubscribe();
};
}, [pubKey, subscribeToMore]);
const withdrawals = useMemo(() => {
if (!data?.party?.withdrawalsConnection?.edges) {
return [];
}
return orderBy(
removePaginationWrapper(data.party.withdrawalsConnection.edges),
'createdTimestamp',
'desc'
);
}, [data]);
/**
* withdrawals that have to be completed by a user.
*/
const pending = useMemo(() => {
return withdrawals.filter((w) => !w.txHash);
}, [withdrawals]);
/**
* withdrawals that are completed or being completed
*/
const completed = useMemo(() => {
return withdrawals
.filter((w) => w.txHash)
.sort((a, b) =>
(b.withdrawnTimestamp || b.createdTimestamp).localeCompare(
a.withdrawnTimestamp || a.createdTimestamp
)
);
}, [withdrawals]);
return {
data,
data: removePaginationWrapper(
data?.party?.withdrawalsConnection?.edges ?? []
).sort((a, b) => {
if (!b.txHash !== !a.txHash) {
return b.txHash ? -1 : 1;
}
return (
b.txHash ? b.withdrawnTimestamp : b.createdTimestamp
).localeCompare(a.txHash ? a.withdrawnTimestamp : a.createdTimestamp);
}),
loading,
error,
withdrawals,
pending,
completed,
};
};

View File

@ -26,11 +26,12 @@ describe('renders the correct columns', () => {
});
const headers = screen.getAllByRole('columnheader');
expect(headers).toHaveLength(6);
expect(headers).toHaveLength(7);
expect(headers.map((h) => h.textContent?.trim())).toEqual([
'Asset',
'Amount',
'Recipient',
'Created',
'Completed',
'Status',
'Transaction',
@ -41,9 +42,10 @@ describe('renders the correct columns', () => {
'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]);
@ -66,6 +68,7 @@ describe('renders the correct columns', () => {
'asset-symbol',
'1.00',
'123456…123456',
getTimeFormat().format(new Date(withdrawal.createdTimestamp as string)),
getTimeFormat().format(new Date(withdrawal.withdrawnTimestamp as string)),
'Completed',
'0x1234…121314',

View File

@ -11,25 +11,42 @@ import type {
VegaICellRendererParams,
VegaValueFormatterParams,
} from '@vegaprotocol/ui-toolkit';
import { Link, AgGridDynamic as AgGrid } from '@vegaprotocol/ui-toolkit';
import {
Link,
ButtonLink,
AgGridDynamic as AgGrid,
Intent,
Icon,
Loader,
} from '@vegaprotocol/ui-toolkit';
import { useEnvironment } from '@vegaprotocol/environment';
import type { WithdrawalFieldsFragment } from './__generated__/Withdrawal';
import { useEthWithdrawApprovalsStore } from '@vegaprotocol/web3';
import * as Schema from '@vegaprotocol/types';
import type { VerifyState } from './use-verify-withdrawal';
import { ApprovalStatus } from './use-verify-withdrawal';
export const WithdrawalsTable = (
props: TypedDataAgGrid<WithdrawalFieldsFragment>
) => {
const { ETHERSCAN_URL } = useEnvironment();
const createWithdrawApproval = useEthWithdrawApprovalsStore(
(store) => store.create
);
return (
<AgGrid
overlayNoRowsTemplate={t('No withdrawals')}
defaultColDef={{ flex: 1, resizable: true }}
style={{ width: '100%' }}
components={{ RecipientCell, StatusCell }}
style={{ width: '100%', height: '100%' }}
components={{
RecipientCell,
StatusCell,
EtherscanLinkCell,
CompleteCell,
}}
suppressCellFocus={true}
domLayout="autoHeight"
rowHeight={30}
{...props}
>
<AgGridColumn headerName="Asset" field="asset.symbol" />
@ -60,19 +77,25 @@ export const WithdrawalsTable = (
return truncateByChars(value);
}}
/>
<AgGridColumn
headerName={t('Created')}
field="createdTimestamp"
valueFormatter={({
value,
}: VegaValueFormatterParams<
WithdrawalFieldsFragment,
'createdTimestamp'
>) => (value ? getDateTimeFormat().format(new Date(value)) : '-')}
/>
<AgGridColumn
headerName={t('Completed')}
field="withdrawnTimestamp"
valueFormatter={({
data,
value,
}: VegaValueFormatterParams<
WithdrawalFieldsFragment,
'withdrawnTimestamp'
>) => {
const ts = data?.withdrawnTimestamp;
if (!ts) return '-';
return getDateTimeFormat().format(new Date(ts));
}}
>) => (value ? getDateTimeFormat().format(new Date(value)) : '-')}
/>
<AgGridColumn
headerName={t('Status')}
@ -82,26 +105,57 @@ export const WithdrawalsTable = (
<AgGridColumn
headerName={t('Transaction')}
field="txHash"
cellRenderer={({
value,
}: VegaValueFormatterParams<WithdrawalFieldsFragment, 'txHash'>) => {
if (!value) return '-';
return (
<Link
title={t('View transaction on Etherscan')}
href={`${ETHERSCAN_URL}/tx/${value}`}
data-testid="etherscan-link"
target="_blank"
>
{truncateByChars(value)}
</Link>
);
flex={2}
cellRendererParams={{
complete: (withdrawal: WithdrawalFieldsFragment) => {
createWithdrawApproval(withdrawal);
},
}}
cellRendererSelector={({
data,
}: VegaICellRendererParams<WithdrawalFieldsFragment>) => ({
component: data?.txHash ? 'EtherscanLinkCell' : 'CompleteCell',
})}
/>
</AgGrid>
);
};
export type CompleteCellProps = {
data: WithdrawalFieldsFragment;
complete: (withdrawal: WithdrawalFieldsFragment) => void;
};
export const CompleteCell = ({ data, complete }: CompleteCellProps) =>
data.pendingOnForeignChain ? (
'-'
) : (
<ButtonLink
data-testid="complete-withdrawal"
onClick={() => complete(data)}
>
{t('Complete withdrawal')}
</ButtonLink>
);
export const EtherscanLinkCell = ({
value,
ethUrl,
}: VegaValueFormatterParams<WithdrawalFieldsFragment, 'txHash'> & {
ethUrl: string;
}) => {
if (!value) return '-';
return (
<Link
title={t('View transaction on Etherscan')}
href={`${ethUrl}/tx/${value}`}
data-testid="etherscan-link"
target="_blank"
>
{truncateByChars(value)}
</Link>
);
};
export const StatusCell = ({ data }: { data: WithdrawalFieldsFragment }) => {
if (data.pendingOnForeignChain || !data.txHash) {
return <span>{t('Pending')}</span>;
@ -139,3 +193,48 @@ const RecipientCell = ({
</Link>
);
};
export const getVerifyDialogProps = (status: ApprovalStatus) => {
if (status === ApprovalStatus.Error) {
return {
intent: Intent.Danger,
icon: <Icon name="warning-sign" />,
};
}
if (status === ApprovalStatus.Pending) {
return { intent: Intent.None, icon: <Loader size="small" /> };
}
if (status === ApprovalStatus.Delayed) {
return { intent: Intent.Warning, icon: <Icon name="time" /> };
}
return { intent: Intent.None };
};
export const VerificationStatus = ({ state }: { state: VerifyState }) => {
if (state.status === ApprovalStatus.Error) {
return <p>{t('Something went wrong')}</p>;
}
if (state.status === ApprovalStatus.Pending) {
return <p>{t('Verifying...')}</p>;
}
if (state.status === ApprovalStatus.Delayed && state.completeTimestamp) {
const formattedTime = getDateTimeFormat().format(
new Date(state.completeTimestamp)
);
return (
<>
<p className="mb-2">
{t("The amount you're withdrawing has triggered a time delay")}
</p>
<p>{t(`Cannot be completed until ${formattedTime}`)}</p>
</>
);
}
return null;
};