[#128] add <PositionsTable/> component

This commit is contained in:
Bartłomiej Głownia 2022-03-25 13:32:14 +01:00
parent 936ed3f9e7
commit 7f1632d44d
11 changed files with 171 additions and 41 deletions

View File

@ -10,7 +10,7 @@ import type { Subscription } from 'zen-observable-ts';
export function makeDataProvider<QueryData, Data, SubscriptionData, Delta>( export function makeDataProvider<QueryData, Data, SubscriptionData, Delta>(
query: DocumentNode | TypedDocumentNode<QueryData, any>, // eslint-disable-line @typescript-eslint/no-explicit-any query: DocumentNode | TypedDocumentNode<QueryData, any>, // eslint-disable-line @typescript-eslint/no-explicit-any
subscriptionQuery: DocumentNode | TypedDocumentNode<SubscriptionData, any>, // eslint-disable-line @typescript-eslint/no-explicit-any subscriptionQuery: DocumentNode | TypedDocumentNode<SubscriptionData, any>, // eslint-disable-line @typescript-eslint/no-explicit-any
update: (draft: Draft<Data>[] | null, delta: Delta) => void, update: (draft: Draft<Data>[], delta: Delta) => void,
getData: (subscriptionData: QueryData) => Data[] | null, getData: (subscriptionData: QueryData) => Data[] | null,
getDelta: (subscriptionData: SubscriptionData) => Delta getDelta: (subscriptionData: SubscriptionData) => Delta
) { ) {
@ -61,7 +61,7 @@ export function makeDataProvider<QueryData, Data, SubscriptionData, Delta>(
return; return;
} }
const delta = getDelta(subscriptionData); const delta = getDelta(subscriptionData);
if (loading) { if (loading || !data) {
updateQueue.push(delta); updateQueue.push(delta);
} else { } else {
const newData = produce(data, (draft) => { const newData = produce(data, (draft) => {
@ -77,7 +77,7 @@ export function makeDataProvider<QueryData, Data, SubscriptionData, Delta>(
try { try {
const res = await client.query<QueryData>({ query }); const res = await client.query<QueryData>({ query });
data = getData(res.data); data = getData(res.data);
if (updateQueue && updateQueue.length > 0) { if (data && updateQueue && updateQueue.length > 0) {
data = produce(data, (draft) => { data = produce(data, (draft) => {
while (updateQueue.length) { while (updateQueue.length) {
const delta = updateQueue.shift(); const delta = updateQueue.shift();

View File

@ -63,10 +63,7 @@ export const marketsDataProvider = makeDataProvider<
>( >(
MARKETS_QUERY, MARKETS_QUERY,
MARKET_DATA_SUB, MARKET_DATA_SUB,
(draft: Markets_markets[] | null, delta: MarketDataSub_marketData) => { (draft: Markets_markets[], delta: MarketDataSub_marketData) => {
if (!draft) {
return;
}
const index = draft.findIndex((m) => m.id === delta.market.id); const index = draft.findIndex((m) => m.id === delta.market.id);
if (index !== -1) { if (index !== -1) {
draft[index].data = delta; draft[index].data = delta;

View File

@ -77,13 +77,7 @@ export const positionsDataProvider = makeDataProvider<
>( >(
POSITION_QUERY, POSITION_QUERY,
POSITIONS_SUB, POSITIONS_SUB,
( (draft: positions_party_positions[], delta: positionSubscribe_positions) => {
draft: positions_party_positions[] | null,
delta: positionSubscribe_positions
) => {
if (!draft) {
return;
}
const index = draft.findIndex((m) => m.market.id === delta.market.id); const index = draft.findIndex((m) => m.market.id === delta.market.id);
if (index !== -1) { if (index !== -1) {
draft[index] = delta; draft[index] = delta;

View File

@ -2,10 +2,10 @@ export * from './__generated__/AssetsQuery';
export * from './__generated__/globalTypes'; export * from './__generated__/globalTypes';
export * from './__generated__/Guess'; export * from './__generated__/Guess';
export * from './__generated__/Market'; export * from './__generated__/Market';
export * from './__generated__/MarketDataFields';
export * from './__generated__/MarketDataSub';
export * from './__generated__/Markets'; export * from './__generated__/Markets';
export * from './__generated__/MarketsQuery'; export * from './__generated__/MarketsQuery';
export * from './__generated__/MarketDataSub';
export * from './__generated__/MarketDataFields';
export * from './__generated__/NetworkParametersQuery'; export * from './__generated__/NetworkParametersQuery';
export * from './__generated__/NodesQuery'; export * from './__generated__/NodesQuery';
export * from './__generated__/OrderEvent'; export * from './__generated__/OrderEvent';
@ -13,6 +13,9 @@ export * from './__generated__/OrderFields';
export * from './__generated__/Orders'; export * from './__generated__/Orders';
export * from './__generated__/OrderSub'; export * from './__generated__/OrderSub';
export * from './__generated__/PartyAssetsQuery'; export * from './__generated__/PartyAssetsQuery';
export * from './__generated__/PositionDetails';
export * from './__generated__/positions';
export * from './__generated__/positionSubscribe';
export * from './__generated__/ProposalsQuery'; export * from './__generated__/ProposalsQuery';
export * from './data-providers'; export * from './data-providers';

View File

@ -1 +1 @@
export * from './lib/positions'; export * from './lib/positions-table';

View File

@ -0,0 +1,22 @@
import { render } from '@testing-library/react';
import { act } from 'react-dom/test-utils';
import { MockedProvider } from '@apollo/react-testing';
import PositionsTable from './positions-table';
describe('PositionsTable', () => {
it('should render successfully', async () => {
await act(async () => {
const { baseElement } = render(
<MockedProvider>
<PositionsTable
data={[]}
onRowClicked={(marketId) => {
console.log(marketId);
}}
/>
</MockedProvider>
);
expect(baseElement).toBeTruthy();
});
});
});

View File

@ -0,0 +1,125 @@
import { forwardRef, useMemo } from 'react';
import type { ValueFormatterParams } from 'ag-grid-community';
import {
PriceCell /*, MarginCell*/,
formatNumber,
volumePrefix,
addDecimal,
} from '@vegaprotocol/react-helpers';
import { AgGridDynamic as AgGrid } from '@vegaprotocol/ui-toolkit';
import { AgGridColumn } from 'ag-grid-react';
import type { AgGridReact } from 'ag-grid-react';
import compact from 'lodash/compact';
import {
positions_party_positions,
MarketTradingMode,
} from '@vegaprotocol/graphql';
interface PositionsTableProps {
data: positions_party_positions[];
onRowClicked: (marketId: string) => void;
}
export const getRowNodeId = (data: { market: { id: string } }) =>
data.market.id;
const sortByName = (
a: positions_party_positions,
b: positions_party_positions
) => {
if (
a.market.tradableInstrument.instrument.name <
b.market.tradableInstrument.instrument.name
) {
return -1;
}
if (
a.market.tradableInstrument.instrument.name >
b.market.tradableInstrument.instrument.name
) {
return 1;
}
return 0;
};
export const PositionsTable = forwardRef<AgGridReact, PositionsTableProps>(
({ data, onRowClicked }, ref) => {
const sortedData = useMemo(() => {
return compact(data).sort(sortByName);
}, [data]);
return (
<AgGrid
style={{ width: '100%', height: '100%' }}
overlayNoRowsTemplate="No positions"
rowData={sortedData}
getRowNodeId={getRowNodeId}
ref={ref}
defaultColDef={{
flex: 1,
resizable: true,
}}
onRowClicked={({ data }: { data: positions_party_positions }) =>
onRowClicked(getRowNodeId(data))
}
components={{ PriceCell }}
>
<AgGridColumn
headerName="Market"
field="tradableInstrument.instrument.code"
/>
<AgGridColumn
headerName="Amount"
field="openVolume"
valueFormatter={({ value, data }: ValueFormatterParams) =>
volumePrefix(value)
}
/>
<AgGridColumn
headerName="Average Entry Price"
field="averageEntryPrice"
cellRenderer="PriceCell"
valueFormatter={({ value, data }: ValueFormatterParams) =>
formatNumber(value, data.market.decimalPlaces)
}
/>
<AgGridColumn
headerName="Mark Price"
field="market.data.markPrice"
type="rightAligned"
cellRenderer="PriceCell"
valueFormatter={({ value, data }: ValueFormatterParams) => {
if (
data.market.data.marketTradingMode ===
MarketTradingMode.OpeningAuction
) {
return '-';
}
return addDecimal(value, data.market.decimalPlaces);
}}
/>
<AgGridColumn
headerName="Realised PNL"
field="realisedPNL"
type="rightAligned"
cellClassRules={{
'color-vega-green': ({ value }: { value: string }) =>
Number(value) > 0,
'color-vega-red': ({ value }: { value: string }) =>
Number(value) < 0,
}}
valueFormatter={({ value }: ValueFormatterParams) => {
if (Number(value) > 0) {
return '+' + value;
}
return value;
}}
cellRenderer="PriceCell"
/>
</AgGrid>
);
}
);
export default PositionsTable;

View File

@ -1,10 +0,0 @@
import { render } from '@testing-library/react';
import Positions from './positions';
describe('Positions', () => {
it('should render successfully', () => {
const { baseElement } = render(<Positions />);
expect(baseElement).toBeTruthy();
});
});

View File

@ -1,14 +0,0 @@
import './positions.module.scss';
/* eslint-disable-next-line */
export interface PositionsProps {}
export function Positions(props: PositionsProps) {
return (
<div>
<h1>Welcome to Positions!</h1>
</div>
);
}
export default Positions;

View File

@ -7,6 +7,19 @@ const getUserLocale = () => 'default';
export const splitAt = (index: number) => (x: string) => export const splitAt = (index: number) => (x: string) =>
[x.slice(0, index), x.slice(index)]; [x.slice(0, index), x.slice(index)];
/**
* Returns a number prefixed with either a '-' or a '+'. The open volume field
* already comes with a '-' if negative so we only need to actually prefix if
* its a positive value
*/
export function volumePrefix(value: string): string {
if (value === '0' || value.startsWith('-')) {
return value;
}
return '+' + value;
}
export const getTimeFormat = once( export const getTimeFormat = once(
() => () =>
new Intl.DateTimeFormat(getUserLocale(), { new Intl.DateTimeFormat(getUserLocale(), {