fix(trading): consolidate view as user mode (#2778)

This commit is contained in:
m.ray 2023-01-31 11:04:52 -05:00 committed by GitHub
parent 01f0934da3
commit b40358a723
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 237 additions and 121 deletions

View File

@ -5,7 +5,7 @@ import { t, useDataProvider } from '@vegaprotocol/react-helpers';
import { useVegaWallet } from '@vegaprotocol/wallet';
export const DepositsContainer = () => {
const { pubKey } = useVegaWallet();
const { pubKey, isReadOnly } = useVegaWallet();
const { data, loading, error } = useDataProvider({
dataProvider: depositsProvider,
variables: { partyId: pubKey || '' },
@ -30,15 +30,17 @@ export const DepositsContainer = () => {
/>
</div>
</div>
<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={() => openDepositDialog()}
data-testid="deposit-button"
>
{t('Deposit')}
</Button>
</div>
{!isReadOnly && (
<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={() => openDepositDialog()}
data-testid="deposit-button"
>
{t('Deposit')}
</Button>
</div>
)}
</div>
);
};

View File

@ -9,7 +9,7 @@ import { t, useDataProvider } from '@vegaprotocol/react-helpers';
import { VegaWalletContainer } from '../../components/vega-wallet-container';
export const WithdrawalsContainer = () => {
const { pubKey } = useVegaWallet();
const { pubKey, isReadOnly } = useVegaWallet();
const { data, loading, error } = useDataProvider({
dataProvider: withdrawalProvider,
variables: { partyId: pubKey || '' },
@ -36,15 +36,17 @@ export const WithdrawalsContainer = () => {
/>
</div>
</div>
<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={() => openWithdrawDialog()}
data-testid="withdraw-dialog-button"
>
{t('Make withdrawal')}
</Button>
</div>
{!isReadOnly && (
<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={() => openWithdrawDialog()}
data-testid="withdraw-dialog-button"
>
{t('Make withdrawal')}
</Button>
</div>
)}
</div>
</VegaWalletContainer>
);

View File

@ -9,7 +9,7 @@ import { AccountManager } from '@vegaprotocol/accounts';
import { useDepositDialog } from '@vegaprotocol/deposits';
export const AccountsContainer = () => {
const { pubKey } = useVegaWallet();
const { pubKey, isReadOnly } = useVegaWallet();
const { open: openAssetDetailsDialog } = useAssetDetailsDialogStore();
const openWithdrawalDialog = useWithdrawalDialog((store) => store.open);
const openDepositDialog = useDepositDialog((store) => store.open);
@ -37,13 +37,16 @@ export const AccountsContainer = () => {
onClickAsset={onClickAsset}
onClickWithdraw={openWithdrawalDialog}
onClickDeposit={openDepositDialog}
isReadOnly={isReadOnly}
/>
</div>
<div className="flex justify-end p-2 px-[11px]">
<Button size="sm" onClick={() => openDepositDialog()}>
{t('Deposit')}
</Button>
</div>
{!isReadOnly && (
<div className="flex justify-end p-2 px-[11px]">
<Button size="sm" onClick={() => openDepositDialog()}>
{t('Deposit')}
</Button>
</div>
)}
</div>
);
};

View File

@ -23,13 +23,23 @@ jest.mock('@vegaprotocol/react-helpers', () => ({
describe('AccountManager', () => {
it('change partyId should reload data provider', async () => {
const { rerender } = render(
<AccountManager partyId="partyOne" onClickAsset={jest.fn} />
<AccountManager
partyId="partyOne"
onClickAsset={jest.fn}
isReadOnly={false}
/>
);
expect(
(helpers.useDataProvider as jest.Mock).mock.calls[0][0].variables.partyId
).toEqual('partyOne');
await act(() => {
rerender(<AccountManager partyId="partyTwo" onClickAsset={jest.fn} />);
rerender(
<AccountManager
partyId="partyTwo"
onClickAsset={jest.fn}
isReadOnly={false}
/>
);
});
expect(
(helpers.useDataProvider as jest.Mock).mock.calls[1][0].variables.partyId
@ -38,7 +48,13 @@ describe('AccountManager', () => {
it('update method should return proper result', async () => {
await act(() => {
render(<AccountManager partyId="partyOne" onClickAsset={jest.fn} />);
render(
<AccountManager
partyId="partyOne"
onClickAsset={jest.fn}
isReadOnly={false}
/>
);
});
await waitFor(() => {
expect(screen.getByText('No accounts')).toBeInTheDocument();

View File

@ -16,6 +16,7 @@ interface AccountManagerProps {
onClickAsset: (assetId: string) => void;
onClickWithdraw?: (assetId?: string) => void;
onClickDeposit?: (assetId?: string) => void;
isReadOnly: boolean;
}
export const AccountManager = ({
@ -23,6 +24,7 @@ export const AccountManager = ({
onClickWithdraw,
onClickDeposit,
partyId,
isReadOnly,
}: AccountManagerProps) => {
const gridRef = useRef<AgGridReact | null>(null);
const dataRef = useRef<AccountFields[] | null>(null);
@ -58,6 +60,7 @@ export const AccountManager = ({
onClickAsset={onClickAsset}
onClickDeposit={onClickDeposit}
onClickWithdraw={onClickWithdraw}
isReadOnly={isReadOnly}
/>
<div className="pointer-events-none absolute inset-0">
<AsyncRenderer

View File

@ -35,7 +35,11 @@ describe('AccountsTable', () => {
it('should render correct columns', async () => {
await act(async () => {
render(
<AccountTable rowData={singleRowData} onClickAsset={() => null} />
<AccountTable
rowData={singleRowData}
onClickAsset={() => null}
isReadOnly={false}
/>
);
});
const expectedHeaders = ['Asset', 'Total', 'Used', 'Available', ''];
@ -49,7 +53,11 @@ describe('AccountsTable', () => {
it('should apply correct formatting', async () => {
await act(async () => {
render(
<AccountTable rowData={singleRowData} onClickAsset={() => null} />
<AccountTable
rowData={singleRowData}
onClickAsset={() => null}
isReadOnly={false}
/>
);
});
const cells = await screen.findAllByRole('gridcell');

View File

@ -29,6 +29,7 @@ export interface AccountTableProps extends AgGridReactProps {
onClickAsset: (assetId: string) => void;
onClickWithdraw?: (assetId: string) => void;
onClickDeposit?: (assetId: string) => void;
isReadOnly: boolean;
}
export const AccountTable = forwardRef<AgGridReact, AccountTableProps>(
@ -127,48 +128,50 @@ export const AccountTable = forwardRef<AgGridReact, AccountTableProps>(
}
maxWidth={300}
/>
<AgGridColumn
colId="breakdown"
headerName=""
sortable={false}
minWidth={200}
type="rightAligned"
cellRenderer={({
data,
}: VegaICellRendererParams<AccountFields>) => {
return data ? (
<>
<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')}
</ButtonLink>
<span className="mx-1" />
<ButtonLink
data-testid="withdraw"
onClick={() =>
onClickWithdraw && onClickWithdraw(data.asset.id)
}
>
{t('Withdraw')}
</ButtonLink>
</>
) : null;
}}
/>
{!props.isReadOnly && (
<AgGridColumn
colId="breakdown"
headerName=""
sortable={false}
minWidth={200}
type="rightAligned"
cellRenderer={({
data,
}: VegaICellRendererParams<AccountFields>) => {
return data ? (
<>
<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')}
</ButtonLink>
<span className="mx-1" />
<ButtonLink
data-testid="withdraw"
onClick={() =>
onClickWithdraw && onClickWithdraw(data.asset.id)
}
>
{t('Withdraw')}
</ButtonLink>
</>
) : null;
}}
/>
)}
</AgGrid>
<Dialog size="medium" open={openBreakdown} onChange={setOpenBreakdown}>
<div className="h-[35vh] w-full m-auto flex flex-col">

View File

@ -45,7 +45,7 @@ export type DealTicketFormFields = OrderSubmissionBody['orderSubmission'] & {
};
export const DealTicket = ({ market, submit }: DealTicketProps) => {
const { pubKey } = useVegaWallet();
const { pubKey, isReadOnly } = useVegaWallet();
const { getPersistedOrder, setPersistedOrder } = usePersistedOrderStore(
(store) => ({
getPersistedOrder: store.getOrder,
@ -158,7 +158,11 @@ export const DealTicket = ({ market, submit }: DealTicketProps) => {
);
return (
<form onSubmit={handleSubmit(onSubmit)} className="p-4" noValidate>
<form
onSubmit={isReadOnly ? () => null : handleSubmit(onSubmit)}
className="p-4"
noValidate
>
<Controller
name="type"
control={control}
@ -220,13 +224,14 @@ export const DealTicket = ({ market, submit }: DealTicketProps) => {
/>
)}
<DealTicketButton
disabled={Object.keys(errors).length >= 1}
disabled={Object.keys(errors).length >= 1 || isReadOnly}
variant={order.side === Schema.Side.SIDE_BUY ? 'ternary' : 'secondary'}
/>
<SummaryMessage
errorMessage={errors.summary?.message}
market={market}
order={order}
isReadOnly={isReadOnly}
/>
<DealTicketFeeDetails order={order} market={market} />
</form>
@ -241,9 +246,10 @@ interface SummaryMessageProps {
errorMessage?: string;
market: MarketDealTicket;
order: OrderSubmissionBody['orderSubmission'];
isReadOnly: boolean;
}
const SummaryMessage = memo(
({ errorMessage, market, order }: SummaryMessageProps) => {
({ errorMessage, market, order, isReadOnly }: SummaryMessageProps) => {
// Specific error UI for if balance is so we can
// render a deposit dialog
const asset = market.tradableInstrument.instrument.product.settlementAsset;
@ -251,6 +257,17 @@ const SummaryMessage = memo(
market,
order,
});
if (isReadOnly) {
return (
<div className="mb-4">
<InputError data-testid="dealticket-error-message-summary">
{
'You need to connect your own wallet to start trading on this market'
}
</InputError>
</div>
);
}
if (errorMessage === SummaryValidationType.NoCollateral) {
return (
<ZeroBalanceError

View File

@ -10,7 +10,7 @@ export const OrderListContainer = ({
marketId?: string;
onMarketClick?: (marketId: string) => void;
}) => {
const { pubKey } = useVegaWallet();
const { pubKey, isReadOnly } = useVegaWallet();
if (!pubKey) {
return <Splash>{t('Please connect Vega wallet')}</Splash>;
@ -21,6 +21,7 @@ export const OrderListContainer = ({
partyId={pubKey}
marketId={marketId}
onMarketClick={onMarketClick}
isReadOnly={isReadOnly}
/>
);
};

View File

@ -13,7 +13,7 @@ const generateJsx = () => {
return (
<MockedProvider>
<VegaWalletContext.Provider value={{ pubKey } as VegaWalletContextShape}>
<OrderListManager partyId={pubKey} />
<OrderListManager partyId={pubKey} isReadOnly={false} />
</VegaWalletContext.Provider>
</MockedProvider>
);

View File

@ -31,6 +31,7 @@ export interface OrderListManagerProps {
partyId: string;
marketId?: string;
onMarketClick?: (marketId: string) => void;
isReadOnly: boolean;
}
export const TransactionComplete = ({
@ -72,6 +73,7 @@ export const OrderListManager = ({
partyId,
marketId,
onMarketClick,
isReadOnly,
}: OrderListManagerProps) => {
const gridRef = useRef<AgGridReact | null>(null);
const scrolledToTop = useRef(true);
@ -146,6 +148,7 @@ export const OrderListManager = ({
}}
setEditOrder={setEditOrder}
onMarketClick={onMarketClick}
isReadOnly={isReadOnly}
/>
<div className="pointer-events-none absolute inset-0">
<AsyncRenderer

View File

@ -19,6 +19,7 @@ const defaultProps: OrderListTableProps = {
rowData: [],
setEditOrder: jest.fn(),
cancel: jest.fn(),
isReadOnly: false,
};
const generateJsx = (
@ -156,6 +157,29 @@ describe('OrderListTable', () => {
expect(mockCancel).toHaveBeenCalledWith(order);
});
it('does not allow cancelling and editing for permitted orders if read only', async () => {
const mockEdit = jest.fn();
const mockCancel = jest.fn();
const order = generateOrder({
type: Schema.OrderType.TYPE_LIMIT,
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GTC,
liquidityProvision: null,
peggedOrder: null,
});
await act(async () => {
render(
generateJsx({
rowData: [order],
setEditOrder: mockEdit,
cancel: mockCancel,
isReadOnly: true,
})
);
});
const amendCell = getAmendCell();
expect(amendCell.queryAllByRole('button')).toHaveLength(0);
});
it('shows if an order is a liquidity provision order and does not show order actions', async () => {
const order = generateOrder({
type: Schema.OrderType.TYPE_LIMIT,

View File

@ -22,6 +22,7 @@ const Template: Story = (args) => {
setEditOrder={() => {
return;
}}
isReadOnly={false}
/>
</div>
);
@ -48,6 +49,7 @@ const Template2: Story = (args) => {
rowData={args.data}
cancel={cancel}
setEditOrder={setEditOrder}
isReadOnly={false}
/>
</div>
<VegaTransactionDialog

View File

@ -32,6 +32,7 @@ export type OrderListTableProps = OrderListProps & {
cancel: (order: Order) => void;
setEditOrder: (order: Order) => void;
onMarketClick?: (marketId: string) => void;
isReadOnly: boolean;
};
export const OrderListTable = forwardRef<AgGridReact, OrderListTableProps>(
@ -248,7 +249,7 @@ export const OrderListTable = forwardRef<AgGridReact, OrderListTableProps>(
minWidth={100}
type="rightAligned"
cellRenderer={({ data, node }: VegaICellRendererParams<Order>) => {
return data && isOrderAmendable(data) ? (
return data && isOrderAmendable(data) && !props.isReadOnly ? (
<>
<ButtonLink
data-testid="edit"

View File

@ -8,7 +8,7 @@ export const PositionsContainer = ({
}: {
onMarketClick?: (marketId: string) => void;
}) => {
const { pubKey } = useVegaWallet();
const { pubKey, isReadOnly } = useVegaWallet();
if (!pubKey) {
return (
@ -17,5 +17,11 @@ export const PositionsContainer = ({
</Splash>
);
}
return <PositionsManager partyId={pubKey} onMarketClick={onMarketClick} />;
return (
<PositionsManager
partyId={pubKey}
onMarketClick={onMarketClick}
isReadOnly={isReadOnly}
/>
);
};

View File

@ -9,16 +9,45 @@ import { t } from '@vegaprotocol/react-helpers';
interface PositionsManagerProps {
partyId: string;
onMarketClick?: (marketId: string) => void;
isReadOnly: boolean;
}
export const PositionsManager = ({
partyId,
onMarketClick,
isReadOnly,
}: PositionsManagerProps) => {
const gridRef = useRef<AgGridReact | null>(null);
const { data, error, loading, getRows } = usePositionsData(partyId, gridRef);
const create = useVegaTransactionStore((store) => store.create);
const onClose = ({
marketId,
openVolume,
}: {
marketId: string;
openVolume: string;
}) =>
create({
batchMarketInstructions: {
cancellations: [
{
marketId,
orderId: '', // omit order id to cancel all active orders
},
],
submissions: [
{
marketId: marketId,
type: Schema.OrderType.TYPE_MARKET as const,
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_FOK as const,
side: openVolume.startsWith('-')
? Schema.Side.SIDE_BUY
: Schema.Side.SIDE_SELL,
size: openVolume.replace('-', ''),
},
],
},
});
return (
<div className="h-full relative">
<PositionsTable
@ -26,31 +55,9 @@ export const PositionsManager = ({
ref={gridRef}
datasource={{ getRows }}
onMarketClick={onMarketClick}
onClose={({ marketId, openVolume }) =>
create({
batchMarketInstructions: {
cancellations: [
{
marketId,
orderId: '', // omit order id to cancel all active orders
},
],
submissions: [
{
marketId: marketId,
type: Schema.OrderType.TYPE_MARKET as const,
timeInForce: Schema.OrderTimeInForce
.TIME_IN_FORCE_FOK as const,
side: openVolume.startsWith('-')
? Schema.Side.SIDE_BUY
: Schema.Side.SIDE_SELL,
size: openVolume.replace('-', ''),
},
],
},
})
}
onClose={onClose}
noRowsOverlayComponent={() => null}
isReadOnly={isReadOnly}
/>
<div className="pointer-events-none absolute inset-0">
<AsyncRenderer

View File

@ -32,14 +32,16 @@ const singleRowData = [singleRow];
it('should render successfully', async () => {
await act(async () => {
const { baseElement } = render(<PositionsTable rowData={[]} />);
const { baseElement } = render(
<PositionsTable rowData={[]} isReadOnly={false} />
);
expect(baseElement).toBeTruthy();
});
});
it('Render correct columns', async () => {
it('render correct columns', async () => {
await act(async () => {
render(<PositionsTable rowData={singleRowData} />);
render(<PositionsTable rowData={singleRowData} isReadOnly={true} />);
});
const headers = screen.getAllByRole('columnheader');
@ -64,7 +66,7 @@ it('Render correct columns', async () => {
it('renders market name', async () => {
await act(async () => {
render(<PositionsTable rowData={singleRowData} />);
render(<PositionsTable rowData={singleRowData} isReadOnly={false} />);
});
expect(screen.getByText('ETH/BTC (31 july 2022)')).toBeTruthy();
});
@ -75,7 +77,7 @@ it('Does not fail if the market name does not match the split pattern', async ()
Object.assign({}, singleRow, { marketName: breakingMarketName }),
];
await act(async () => {
render(<PositionsTable rowData={row} />);
render(<PositionsTable rowData={row} isReadOnly={false} />);
});
expect(screen.getByText(breakingMarketName)).toBeTruthy();
@ -84,7 +86,9 @@ it('Does not fail if the market name does not match the split pattern', async ()
it('add color and sign to amount, displays positive notional value', async () => {
let result: RenderResult;
await act(async () => {
result = render(<PositionsTable rowData={singleRowData} />);
result = render(
<PositionsTable rowData={singleRowData} isReadOnly={false} />
);
});
let cells = screen.getAllByRole('gridcell');
@ -94,7 +98,10 @@ it('add color and sign to amount, displays positive notional value', async () =>
expect(cells[1].textContent).toEqual('1,230.0');
await act(async () => {
result.rerender(
<PositionsTable rowData={[{ ...singleRow, openVolume: '-100' }]} />
<PositionsTable
rowData={[{ ...singleRow, openVolume: '-100' }]}
isReadOnly={false}
/>
);
});
cells = screen.getAllByRole('gridcell');
@ -107,7 +114,9 @@ it('add color and sign to amount, displays positive notional value', async () =>
it('displays mark price', async () => {
let result: RenderResult;
await act(async () => {
result = render(<PositionsTable rowData={singleRowData} />);
result = render(
<PositionsTable rowData={singleRowData} isReadOnly={false} />
);
});
let cells = screen.getAllByRole('gridcell');
@ -123,6 +132,7 @@ it('displays mark price', async () => {
Schema.MarketTradingMode.TRADING_MODE_OPENING_AUCTION,
},
]}
isReadOnly={false}
/>
);
});
@ -134,14 +144,19 @@ it('displays mark price', async () => {
it("displays properly entry, liquidation price and liquidation bar and it's intent", async () => {
let result: RenderResult;
await act(async () => {
result = render(<PositionsTable rowData={singleRowData} />);
result = render(
<PositionsTable rowData={singleRowData} isReadOnly={false} />
);
});
let cells = screen.getAllByRole('gridcell');
const entryPrice = cells[5].firstElementChild?.firstElementChild?.textContent;
expect(entryPrice).toEqual('13.3');
await act(async () => {
result.rerender(
<PositionsTable rowData={[{ ...singleRow, lowMarginLevel: true }]} />
<PositionsTable
rowData={[{ ...singleRow, lowMarginLevel: true }]}
isReadOnly={false}
/>
);
});
cells = screen.getAllByRole('gridcell');
@ -149,7 +164,7 @@ it("displays properly entry, liquidation price and liquidation bar and it's inte
it('displays leverage', async () => {
await act(async () => {
render(<PositionsTable rowData={singleRowData} />);
render(<PositionsTable rowData={singleRowData} isReadOnly={false} />);
});
const cells = screen.getAllByRole('gridcell');
expect(cells[7].textContent).toEqual('1.1');
@ -157,7 +172,7 @@ it('displays leverage', async () => {
it('displays allocated margin', async () => {
await act(async () => {
render(<PositionsTable rowData={singleRowData} />);
render(<PositionsTable rowData={singleRowData} isReadOnly={false} />);
});
const cells = screen.getAllByRole('gridcell');
const cell = cells[8];
@ -166,7 +181,7 @@ it('displays allocated margin', async () => {
it('displays realised and unrealised PNL', async () => {
await act(async () => {
render(<PositionsTable rowData={singleRowData} />);
render(<PositionsTable rowData={singleRowData} isReadOnly={false} />);
});
const cells = screen.getAllByRole('gridcell');
expect(cells[9].textContent).toEqual('1.23');
@ -181,6 +196,7 @@ it('displays close button', async () => {
onClose={() => {
return;
}}
isReadOnly={false}
/>
);
});
@ -196,6 +212,7 @@ it('do not display close button if openVolume is zero', async () => {
onClose={() => {
return;
}}
isReadOnly={false}
/>
);
});

View File

@ -33,6 +33,7 @@ interface Props extends TypedDataAgGrid<Position> {
onClose?: (data: Position) => void;
onMarketClick?: (id: string) => void;
style?: CSSProperties;
isReadOnly: boolean;
}
export interface AmountCellProps {
@ -378,7 +379,7 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
return getDateTimeFormat().format(new Date(value));
}}
/>
{onClose ? (
{onClose && !props.isReadOnly ? (
<AgGridColumn
type="rightAligned"
cellRenderer={({ data }: VegaICellRendererParams<Position>) =>