feat(1646): add positions table sorting and filtering (#1920)

* feat(1646): add positions table sorting and filtering

* feat(1646): add positions table sorting and filtering - fixes
This commit is contained in:
Bartłomiej Głownia 2022-11-02 08:01:40 +01:00 committed by GitHub
parent 186bcfa95a
commit 3415c8d86c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 343 additions and 270 deletions

View File

@ -95,8 +95,7 @@ describe('Portfolio page', { tags: '@smoke' }, () => {
}); });
it('data should be properly rendered', () => { it('data should be properly rendered', () => {
cy.getByTestId('positions-asset-tDAI').should('exist'); cy.get('.ag-center-cols-container .ag-row').should('have.length', 2);
cy.getByTestId('positions-asset-tEURO').should('exist');
}); });
}); });

View File

@ -1,58 +0,0 @@
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
import { useRef } from 'react';
import type { AgGridReact } from 'ag-grid-react';
import type { Position } from '@vegaprotocol/positions';
import { PriceFlashCell, t } from '@vegaprotocol/react-helpers';
import { AssetBalance } from '@vegaprotocol/accounts';
import { usePositionsData } from '@vegaprotocol/positions';
import { ConsoleLiteGrid } from '../../console-lite-grid';
import useColumnDefinitions from './use-column-definitions';
interface Props {
partyId: string;
assetSymbol: string;
}
const getRowId = ({ data }: { data: Position }) => data.marketId;
const PositionsAsset = ({ partyId, assetSymbol }: Props) => {
const gridRef = useRef<AgGridReact | null>(null);
const { data, error, loading, getRows } = usePositionsData(
partyId,
gridRef,
assetSymbol
);
const { columnDefs, defaultColDef } = useColumnDefinitions();
return (
<AsyncRenderer loading={loading} error={error} data={data}>
<div
data-testid={`positions-asset-${assetSymbol}`}
className="flex justify-between items-center px-4 pt-3 pb-1"
>
<h4>
{assetSymbol} {t('markets')}
</h4>
<div className="text-sm text-neutral-500 dark:text-neutral-300">
{assetSymbol} {t('balance')}:
<span data-testid="balance" className="pl-1 font-mono">
<AssetBalance partyId={partyId} assetSymbol={assetSymbol} />
</span>
</div>
</div>
<ConsoleLiteGrid<Position & { id: undefined }>
ref={gridRef}
domLayout="autoHeight"
classNamesParam="h-auto"
columnDefs={columnDefs}
defaultColDef={defaultColDef}
getRowId={getRowId}
rowModelType={data?.length ? 'infinite' : 'clientSide'}
rowData={data?.length ? undefined : []}
datasource={{ getRows }}
components={{ PriceFlashCell }}
/>
</AsyncRenderer>
);
};
export default PositionsAsset;

View File

@ -1,26 +1,37 @@
import { useOutletContext } from 'react-router-dom'; import { useOutletContext } from 'react-router-dom';
import { t } from '@vegaprotocol/react-helpers'; import { PriceFlashCell } from '@vegaprotocol/react-helpers';
import { usePositionsAssets } from '@vegaprotocol/positions'; import { usePositionsData, getRowId } from '@vegaprotocol/positions';
import { AsyncRenderer, Splash } from '@vegaprotocol/ui-toolkit'; import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
import PositionsAsset from './positions-asset'; import { ConsoleLiteGrid } from '../../console-lite-grid';
import { useRef } from 'react';
import type { AgGridReact } from 'ag-grid-react';
import type { Position } from '@vegaprotocol/positions';
import { NO_DATA_MESSAGE } from '../../../constants';
import useColumnDefinitions from './use-column-definitions';
const Positions = () => { const Positions = () => {
const gridRef = useRef<AgGridReact | null>(null);
const { partyId } = useOutletContext<{ partyId: string }>(); const { partyId } = useOutletContext<{ partyId: string }>();
const { data, error, loading, assetSymbols } = usePositionsAssets(partyId); const { data, error, loading } = usePositionsData(partyId, gridRef);
const { columnDefs, defaultColDef } = useColumnDefinitions();
return ( return (
<AsyncRenderer loading={loading} error={error} data={data}> <AsyncRenderer
{assetSymbols && assetSymbols.length > 0 && ( loading={loading}
<div className="w-full, h-max"> error={error}
{assetSymbols?.map((assetSymbol) => ( data={data?.length ? data : null}
<PositionsAsset noDataMessage={NO_DATA_MESSAGE}
key={assetSymbol} >
partyId={partyId} <ConsoleLiteGrid<Position>
assetSymbol={assetSymbol} ref={gridRef}
/> domLayout="autoHeight"
))} classNamesParam="h-auto"
</div> columnDefs={columnDefs}
)} defaultColDef={defaultColDef}
{assetSymbols?.length === 0 && <Splash>{t('No data to display')}</Splash>} getRowId={getRowId}
rowData={data || undefined}
components={{ PriceFlashCell }}
/>
</AsyncRenderer> </AsyncRenderer>
); );
}; };

View File

@ -1,21 +1,19 @@
import { forwardRef } from 'react'; import { forwardRef } from 'react';
import type {
GroupCellRendererParams,
ValueFormatterParams,
} from 'ag-grid-community';
import { import {
PriceFlashCell, PriceFlashCell,
addDecimalsFormatNumber, addDecimalsFormatNumber,
t, t,
toBigNum, toBigNum,
} from '@vegaprotocol/react-helpers'; } from '@vegaprotocol/react-helpers';
import type {
VegaValueGetterParams,
VegaValueFormatterParams,
VegaICellRendererParams,
TypedDataAgGrid,
} from '@vegaprotocol/ui-toolkit';
import { AgGridDynamic as AgGrid } from '@vegaprotocol/ui-toolkit'; import { AgGridDynamic as AgGrid } from '@vegaprotocol/ui-toolkit';
import { AgGridColumn } from 'ag-grid-react'; import { AgGridColumn } from 'ag-grid-react';
import type { import type { AgGridReact } from 'ag-grid-react';
AgGridReact,
AgGridReactProps,
AgReactUiProps,
} from 'ag-grid-react';
import { import {
MarketTradingMode, MarketTradingMode,
AuctionTrigger, AuctionTrigger,
@ -25,18 +23,12 @@ import {
import type { MarketWithData } from '../../'; import type { MarketWithData } from '../../';
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets'; import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
type Props = AgGridReactProps | AgReactUiProps;
type MarketListTableValueFormatterParams = Omit<
ValueFormatterParams,
'data' | 'value'
> & {
data: MarketWithData;
};
export const getRowId = ({ data }: { data: { id: string } }) => data.id; export const getRowId = ({ data }: { data: { id: string } }) => data.id;
export const MarketListTable = forwardRef<AgGridReact, Props>((props, ref) => { export const MarketListTable = forwardRef<
AgGridReact,
TypedDataAgGrid<MarketWithData>
>((props, ref) => {
const { open: openAssetDetailsDialog } = useAssetDetailsDialogStore(); const { open: openAssetDetailsDialog } = useAssetDetailsDialogStore();
return ( return (
<AgGrid <AgGrid
@ -62,7 +54,12 @@ export const MarketListTable = forwardRef<AgGridReact, Props>((props, ref) => {
<AgGridColumn <AgGridColumn
headerName={t('Settlement asset')} headerName={t('Settlement asset')}
field="tradableInstrument.instrument.product.settlementAsset.symbol" field="tradableInstrument.instrument.product.settlementAsset.symbol"
cellRenderer={({ value }: GroupCellRendererParams) => cellRenderer={({
value,
}: VegaICellRendererParams<
MarketWithData,
'tradableInstrument.instrument.product.settlementAsset.symbol'
>) =>
value && value.length > 0 ? ( value && value.length > 0 ? (
<button <button
className="hover:underline" className="hover:underline"
@ -81,7 +78,9 @@ export const MarketListTable = forwardRef<AgGridReact, Props>((props, ref) => {
headerName={t('Trading mode')} headerName={t('Trading mode')}
field="data" field="data"
minWidth={170} minWidth={170}
valueGetter={({ data }: { data?: MarketWithData }) => { valueGetter={({
data,
}: VegaValueGetterParams<MarketWithData, 'data'>) => {
if (!data?.data) return undefined; if (!data?.data) return undefined;
const { trigger } = data.data; const { trigger } = data.data;
const { tradingMode } = data; const { tradingMode } = data;
@ -100,12 +99,16 @@ export const MarketListTable = forwardRef<AgGridReact, Props>((props, ref) => {
type="rightAligned" type="rightAligned"
cellRenderer="PriceFlashCell" cellRenderer="PriceFlashCell"
filter="agNumberColumnFilter" filter="agNumberColumnFilter"
valueGetter={({ data }: { data?: MarketWithData }) => { valueGetter={({
data,
}: VegaValueGetterParams<MarketWithData, 'data.bestBidPrice'>) => {
return data?.data?.bestBidPrice === undefined return data?.data?.bestBidPrice === undefined
? undefined ? undefined
: toBigNum(data?.data?.bestBidPrice, data.decimalPlaces).toNumber(); : toBigNum(data?.data?.bestBidPrice, data.decimalPlaces).toNumber();
}} }}
valueFormatter={({ data }: MarketListTableValueFormatterParams) => valueFormatter={({
data,
}: VegaValueFormatterParams<MarketWithData, 'data.bestBidPrice'>) =>
data?.data?.bestBidPrice === undefined data?.data?.bestBidPrice === undefined
? undefined ? undefined
: addDecimalsFormatNumber( : addDecimalsFormatNumber(
@ -120,7 +123,9 @@ export const MarketListTable = forwardRef<AgGridReact, Props>((props, ref) => {
type="rightAligned" type="rightAligned"
cellRenderer="PriceFlashCell" cellRenderer="PriceFlashCell"
filter="agNumberColumnFilter" filter="agNumberColumnFilter"
valueGetter={({ data }: { data?: MarketWithData }) => { valueGetter={({
data,
}: VegaValueGetterParams<MarketWithData, 'data.bestOfferPrice'>) => {
return data?.data?.bestOfferPrice === undefined return data?.data?.bestOfferPrice === undefined
? undefined ? undefined
: toBigNum( : toBigNum(
@ -128,7 +133,9 @@ export const MarketListTable = forwardRef<AgGridReact, Props>((props, ref) => {
data.decimalPlaces data.decimalPlaces
).toNumber(); ).toNumber();
}} }}
valueFormatter={({ data }: MarketListTableValueFormatterParams) => valueFormatter={({
data,
}: VegaValueFormatterParams<MarketWithData, 'data.bestOfferPrice'>) =>
data?.data?.bestOfferPrice === undefined data?.data?.bestOfferPrice === undefined
? undefined ? undefined
: addDecimalsFormatNumber( : addDecimalsFormatNumber(
@ -143,12 +150,16 @@ export const MarketListTable = forwardRef<AgGridReact, Props>((props, ref) => {
type="rightAligned" type="rightAligned"
cellRenderer="PriceFlashCell" cellRenderer="PriceFlashCell"
filter="agNumberColumnFilter" filter="agNumberColumnFilter"
valueGetter={({ data }: { data?: MarketWithData }) => { valueGetter={({
data,
}: VegaValueGetterParams<MarketWithData, 'data.markPrice'>) => {
return data?.data?.markPrice === undefined return data?.data?.markPrice === undefined
? undefined ? undefined
: toBigNum(data?.data?.markPrice, data.decimalPlaces).toNumber(); : toBigNum(data?.data?.markPrice, data.decimalPlaces).toNumber();
}} }}
valueFormatter={({ data }: MarketListTableValueFormatterParams) => valueFormatter={({
data,
}: VegaValueFormatterParams<MarketWithData, 'data.markPrice'>) =>
data?.data?.bestOfferPrice === undefined data?.data?.bestOfferPrice === undefined
? undefined ? undefined
: addDecimalsFormatNumber(data.data.markPrice, data.decimalPlaces) : addDecimalsFormatNumber(data.data.markPrice, data.decimalPlaces)

View File

@ -4,4 +4,3 @@ export * from './lib/positions-data-providers';
export * from './lib/positions-table'; export * from './lib/positions-table';
export * from './lib/use-close-position'; export * from './lib/use-close-position';
export * from './lib/use-positions-data'; export * from './lib/use-positions-data';
export * from './lib/use-positions-assets';

View File

@ -1,3 +1,4 @@
import isEqual from 'lodash/isEqual';
import produce from 'immer'; import produce from 'immer';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import sortBy from 'lodash/sortBy'; import sortBy from 'lodash/sortBy';
@ -42,7 +43,7 @@ interface PositionRejoined {
export interface Position { export interface Position {
marketName: string; marketName: string;
averageEntryPrice: string; averageEntryPrice: string;
marginAccountBalance: BigNumber; marginAccountBalance: string;
capitalUtilisation: number; capitalUtilisation: number;
currentLeverage: number; currentLeverage: number;
decimals: number; decimals: number;
@ -152,7 +153,7 @@ export const getMetrics = (
metrics.push({ metrics.push({
marketName: market.tradableInstrument.instrument.name, marketName: market.tradableInstrument.instrument.name,
averageEntryPrice: position.averageEntryPrice, averageEntryPrice: position.averageEntryPrice,
marginAccountBalance, marginAccountBalance: marginAccount.balance,
capitalUtilisation: Math.round(capitalUtilisation.toNumber()), capitalUtilisation: Math.round(capitalUtilisation.toNumber()),
currentLeverage: currentLeverage.toNumber(), currentLeverage: currentLeverage.toNumber(),
marketDecimalPlaces, marketDecimalPlaces,
@ -277,9 +278,14 @@ export const rejoinPositionData = (
return null; return null;
}; };
export const positionsMetricsDataProvider = makeDerivedDataProvider< export interface PositionsMetricsProviderVariables {
partyId: string;
}
export const positionsMetricsProvider = makeDerivedDataProvider<
Position[], Position[],
never Position[],
PositionsMetricsProviderVariables
>( >(
[ [
positionsDataProvider, positionsDataProvider,
@ -287,11 +293,21 @@ export const positionsMetricsDataProvider = makeDerivedDataProvider<
marketsWithDataProvider, marketsWithDataProvider,
marginsDataProvider, marginsDataProvider,
], ],
([positions, accounts, marketsData, margins]) => { ([positions, accounts, marketsData, margins], variables) => {
const positionsData = rejoinPositionData(positions, marketsData, margins); const positionsData = rejoinPositionData(positions, marketsData, margins);
if (!variables) {
return [];
}
return sortBy( return sortBy(
getMetrics(positionsData, accounts as Account[] | null), getMetrics(positionsData, accounts as Account[] | null),
'marketName' 'marketName'
); );
} },
(data, delta, previousData) =>
data.filter((row) => {
const previousRow = previousData?.find(
(previousRow) => previousRow.marketId === row.marketId
);
return !(previousRow && isEqual(previousRow, row));
})
); );

View File

@ -13,7 +13,7 @@ interface PositionsManagerProps {
export const PositionsManager = ({ partyId }: PositionsManagerProps) => { export const PositionsManager = ({ partyId }: PositionsManagerProps) => {
const gridRef = useRef<AgGridReact | null>(null); const gridRef = useRef<AgGridReact | null>(null);
const { data, error, loading, getRows } = usePositionsData(partyId, gridRef); const { data, error, loading } = usePositionsData(partyId, gridRef);
const { const {
submit, submit,
closingOrder, closingOrder,
@ -30,9 +30,7 @@ export const PositionsManager = ({ partyId }: PositionsManagerProps) => {
domLayout="autoHeight" domLayout="autoHeight"
style={{ width: '100%' }} style={{ width: '100%' }}
ref={gridRef} ref={gridRef}
rowModelType={data?.length ? 'infinite' : 'clientSide'} rowData={data}
rowData={data?.length ? undefined : []}
datasource={{ getRows }}
onClose={(position) => submit(position)} onClose={(position) => submit(position)}
/> />
</AsyncRenderer> </AsyncRenderer>

View File

@ -3,8 +3,6 @@ import { act, render, screen } from '@testing-library/react';
import PositionsTable from './positions-table'; import PositionsTable from './positions-table';
import type { Position } from './positions-data-providers'; import type { Position } from './positions-data-providers';
import { MarketTradingMode } from '@vegaprotocol/types'; import { MarketTradingMode } from '@vegaprotocol/types';
import BigNumber from 'bignumber.js';
import React from 'react';
const singleRow: Position = { const singleRow: Position = {
marketName: 'ETH/BTC (31 july 2022)', marketName: 'ETH/BTC (31 july 2022)',
@ -27,7 +25,7 @@ const singleRow: Position = {
unrealisedPNL: '456', unrealisedPNL: '456',
searchPrice: '0', searchPrice: '0',
updatedAt: '2022-07-27T15:02:58.400Z', updatedAt: '2022-07-27T15:02:58.400Z',
marginAccountBalance: new BigNumber(123456), marginAccountBalance: '12345600',
}; };
const singleRowData = [singleRow]; const singleRowData = [singleRow];

View File

@ -8,6 +8,8 @@ import type {
import type { import type {
ValueProps as PriceCellProps, ValueProps as PriceCellProps,
VegaValueFormatterParams, VegaValueFormatterParams,
VegaValueGetterParams,
TypedDataAgGrid,
} from '@vegaprotocol/ui-toolkit'; } from '@vegaprotocol/ui-toolkit';
import { EmptyCell, ProgressBarCell } from '@vegaprotocol/ui-toolkit'; import { EmptyCell, ProgressBarCell } from '@vegaprotocol/ui-toolkit';
import { import {
@ -15,6 +17,7 @@ import {
addDecimalsFormatNumber, addDecimalsFormatNumber,
volumePrefix, volumePrefix,
t, t,
toBigNum,
formatNumber, formatNumber,
getDateTimeFormat, getDateTimeFormat,
signedNumberCssClass, signedNumberCssClass,
@ -22,24 +25,13 @@ import {
} from '@vegaprotocol/react-helpers'; } from '@vegaprotocol/react-helpers';
import { AgGridDynamic as AgGrid } from '@vegaprotocol/ui-toolkit'; import { AgGridDynamic as AgGrid } from '@vegaprotocol/ui-toolkit';
import { AgGridColumn } from 'ag-grid-react'; import { AgGridColumn } from 'ag-grid-react';
import type { AgGridReact, AgGridReactProps } from 'ag-grid-react'; import type { AgGridReact } from 'ag-grid-react';
import type { IDatasource, IGetRowsParams } from 'ag-grid-community';
import type { Position } from './positions-data-providers'; import type { Position } from './positions-data-providers';
import { MarketTradingMode } from '@vegaprotocol/types'; import { MarketTradingMode } from '@vegaprotocol/types';
import { Intent, Button, TooltipCellComponent } from '@vegaprotocol/ui-toolkit'; import { Intent, Button, TooltipCellComponent } from '@vegaprotocol/ui-toolkit';
import { getRowId } from './use-positions-data';
export const getRowId = ({ data }: { data: Position }) => data.marketId; interface Props extends TypedDataAgGrid<Position> {
export interface GetRowsParams extends Omit<IGetRowsParams, 'successCallback'> {
successCallback(rowsThisBlock: Position[], lastRow?: number): void;
}
export interface Datasource extends IDatasource {
getRows(params: GetRowsParams): void;
}
interface Props extends AgGridReactProps {
rowData?: Position[] | null;
datasource?: Datasource;
onClose?: (data: Position) => void; onClose?: (data: Position) => void;
style?: CSSProperties; style?: CSSProperties;
} }
@ -143,6 +135,8 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
defaultColDef={{ defaultColDef={{
flex: 1, flex: 1,
resizable: true, resizable: true,
sortable: true,
filter: true,
tooltipComponent: TooltipCellComponent, tooltipComponent: TooltipCellComponent,
}} }}
components={{ AmountCell, PriceFlashCell, ProgressBarCell }} components={{ AmountCell, PriceFlashCell, ProgressBarCell }}
@ -171,14 +165,20 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
field="notional" field="notional"
type="rightAligned" type="rightAligned"
cellClass="font-mono text-right" cellClass="font-mono text-right"
valueFormatter={({ filter="agNumberColumnFilter"
value, valueGetter={({
data, data,
}: VegaValueFormatterParams<Position, 'notional'>): string => { }: VegaValueGetterParams<Position, 'notional'>) => {
if (!value || !data) { return data?.notional === undefined
return ''; ? undefined
} : toBigNum(data?.notional, data.decimals).toNumber();
return addDecimalsFormatNumber(value, data.decimals); }}
valueFormatter={({
data,
}: VegaValueFormatterParams<Position, 'notional'>) => {
return !data
? undefined
: addDecimalsFormatNumber(data.notional, data.decimals);
}} }}
/> />
<AgGridColumn <AgGridColumn
@ -187,18 +187,27 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
type="rightAligned" type="rightAligned"
cellClass="font-mono text-right" cellClass="font-mono text-right"
cellClassRules={signedNumberCssClassRules} cellClassRules={signedNumberCssClassRules}
filter="agNumberColumnFilter"
valueGetter={({
data,
}: VegaValueGetterParams<Position, 'openVolume'>) => {
return data?.openVolume === undefined
? undefined
: toBigNum(data?.openVolume, data.decimals).toNumber();
}}
valueFormatter={({ valueFormatter={({
value,
data, data,
}: VegaValueFormatterParams<Position, 'openVolume'>): }: VegaValueFormatterParams<Position, 'openVolume'>):
| string | string
| undefined => { | undefined => {
if (!value || !data) { return !data
return undefined; ? undefined
} : volumePrefix(
return volumePrefix( addDecimalsFormatNumber(
addDecimalsFormatNumber(value, data.positionDecimalPlaces) data.openVolume,
); data.positionDecimalPlaces
)
);
}} }}
/> />
<AgGridColumn <AgGridColumn
@ -212,12 +221,21 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
component: params.node.rowPinned ? EmptyCell : PriceFlashCell, component: params.node.rowPinned ? EmptyCell : PriceFlashCell,
}; };
}} }}
filter="agNumberColumnFilter"
valueGetter={({
data,
}: VegaValueGetterParams<Position, 'markPrice'>) => {
return !data ||
data.marketTradingMode ===
MarketTradingMode.TRADING_MODE_OPENING_AUCTION
? undefined
: toBigNum(data.markPrice, data.marketDecimalPlaces).toNumber();
}}
valueFormatter={({ valueFormatter={({
value,
data, data,
node, node,
}: VegaValueFormatterParams<Position, 'markPrice'>) => { }: VegaValueFormatterParams<Position, 'markPrice'>) => {
if (!data || !value || node?.rowPinned) { if (!data || node?.rowPinned) {
return undefined; return undefined;
} }
if ( if (
@ -227,7 +245,7 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
return '-'; return '-';
} }
return addDecimalsFormatNumber( return addDecimalsFormatNumber(
value.toString(), data.markPrice,
data.marketDecimalPlaces data.marketDecimalPlaces
); );
}} }}
@ -244,17 +262,30 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
component: params.node.rowPinned ? EmptyCell : PriceFlashCell, component: params.node.rowPinned ? EmptyCell : PriceFlashCell,
}; };
}} }}
filter="agNumberColumnFilter"
valueGetter={({
data,
}: VegaValueGetterParams<Position, 'averageEntryPrice'>) => {
return data?.markPrice === undefined || !data
? undefined
: toBigNum(
data.averageEntryPrice,
data.marketDecimalPlaces
).toNumber();
}}
valueFormatter={({ valueFormatter={({
data, data,
value,
node, node,
}: VegaValueFormatterParams<Position, 'averageEntryPrice'>): }: VegaValueFormatterParams<Position, 'averageEntryPrice'>):
| string | string
| undefined => { | undefined => {
if (!data || node?.rowPinned || !value) { if (!data || node?.rowPinned) {
return undefined; return undefined;
} }
return addDecimalsFormatNumber(value, data.marketDecimalPlaces); return addDecimalsFormatNumber(
data.averageEntryPrice,
data.marketDecimalPlaces
);
}} }}
/> />
<AgGridColumn <AgGridColumn
@ -264,6 +295,17 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
headerTooltip={t( headerTooltip={t(
'Liquidation prices are based on the amount of collateral you have available, the risk of your position and the liquidity on the order book. They can change rapidly based on the profit and loss of your positions and any changes to collateral from opening/closing other positions and making deposits/withdrawals.' 'Liquidation prices are based on the amount of collateral you have available, the risk of your position and the liquidity on the order book. They can change rapidly based on the profit and loss of your positions and any changes to collateral from opening/closing other positions and making deposits/withdrawals.'
)} )}
filter="agNumberColumnFilter"
valueGetter={({
data,
}: VegaValueGetterParams<Position, 'liquidationPrice'>) => {
return !data
? undefined
: toBigNum(
data?.liquidationPrice,
data.marketDecimalPlaces
).toNumber();
}}
cellRendererSelector={( cellRendererSelector={(
params: ICellRendererParams params: ICellRendererParams
): CellRendererSelectorResult => { ): CellRendererSelectorResult => {
@ -277,6 +319,7 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
headerName={t('Leverage')} headerName={t('Leverage')}
field="currentLeverage" field="currentLeverage"
type="rightAligned" type="rightAligned"
filter="agNumberColumnFilter"
cellRendererSelector={( cellRendererSelector={(
params: ICellRendererParams params: ICellRendererParams
): CellRendererSelectorResult => { ): CellRendererSelectorResult => {
@ -294,6 +337,7 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
headerName={t('Margin allocated')} headerName={t('Margin allocated')}
field="marginAccountBalance" field="marginAccountBalance"
type="rightAligned" type="rightAligned"
filter="agNumberColumnFilter"
cellRendererSelector={( cellRendererSelector={(
params: ICellRendererParams params: ICellRendererParams
): CellRendererSelectorResult => { ): CellRendererSelectorResult => {
@ -301,17 +345,26 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
component: params.node.rowPinned ? EmptyCell : PriceFlashCell, component: params.node.rowPinned ? EmptyCell : PriceFlashCell,
}; };
}} }}
valueGetter={({
data,
}: VegaValueGetterParams<Position, 'marginAccountBalance'>) => {
return !data
? undefined
: toBigNum(data.marginAccountBalance, data.decimals).toNumber();
}}
valueFormatter={({ valueFormatter={({
data, data,
value,
node, node,
}: VegaValueFormatterParams<Position, 'marginAccountBalance'>): }: VegaValueFormatterParams<Position, 'marginAccountBalance'>):
| string | string
| undefined => { | undefined => {
if (!data || node?.rowPinned || !value) { if (!data || node?.rowPinned) {
return undefined; return undefined;
} }
return formatNumber(value, data.decimals); return addDecimalsFormatNumber(
data.marginAccountBalance,
data.decimals
);
}} }}
/> />
<AgGridColumn <AgGridColumn
@ -319,14 +372,21 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
field="realisedPNL" field="realisedPNL"
type="rightAligned" type="rightAligned"
cellClassRules={signedNumberCssClassRules} cellClassRules={signedNumberCssClassRules}
valueFormatter={({ filter="agNumberColumnFilter"
value, valueGetter={({
data, data,
}: VegaValueFormatterParams<Position, 'realisedPNL'>) => }: VegaValueGetterParams<Position, 'realisedPNL'>) => {
value === undefined || data === undefined return !data
? undefined ? undefined
: addDecimalsFormatNumber(value.toString(), data.decimals) : toBigNum(data.realisedPNL, data.decimals).toNumber();
} }}
valueFormatter={({
data,
}: VegaValueFormatterParams<Position, 'realisedPNL'>) => {
return !data
? undefined
: addDecimalsFormatNumber(data.realisedPNL, data.decimals);
}}
cellRenderer="PriceFlashCell" cellRenderer="PriceFlashCell"
headerTooltip={t( headerTooltip={t(
'Profit or loss is realised whenever your position is reduced to zero and the margin is released back to your collateral balance. P&L excludes any fees paid.' 'Profit or loss is realised whenever your position is reduced to zero and the margin is released back to your collateral balance. P&L excludes any fees paid.'
@ -337,13 +397,20 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
field="unrealisedPNL" field="unrealisedPNL"
type="rightAligned" type="rightAligned"
cellClassRules={signedNumberCssClassRules} cellClassRules={signedNumberCssClassRules}
filter="agNumberColumnFilter"
valueGetter={({
data,
}: VegaValueGetterParams<Position, 'unrealisedPNL'>) => {
return !data
? undefined
: toBigNum(data.unrealisedPNL, data.decimals).toNumber();
}}
valueFormatter={({ valueFormatter={({
value,
data, data,
}: VegaValueFormatterParams<Position, 'unrealisedPNL'>) => }: VegaValueFormatterParams<Position, 'unrealisedPNL'>) =>
value === undefined || data === undefined !data
? undefined ? undefined
: addDecimalsFormatNumber(value.toString(), data.decimals) : addDecimalsFormatNumber(data.unrealisedPNL, data.decimals)
} }
cellRenderer="PriceFlashCell" cellRenderer="PriceFlashCell"
headerTooltip={t( headerTooltip={t(
@ -354,6 +421,7 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
headerName={t('Updated')} headerName={t('Updated')}
field="updatedAt" field="updatedAt"
type="rightAligned" type="rightAligned"
filter="agDateColumnFilter"
valueFormatter={({ valueFormatter={({
value, value,
}: VegaValueFormatterParams<Position, 'updatedAt'>) => { }: VegaValueFormatterParams<Position, 'updatedAt'>) => {

View File

@ -1,37 +0,0 @@
import { useCallback, useMemo, useRef } from 'react';
import { useDataProvider } from '@vegaprotocol/react-helpers';
import type { Position } from './positions-data-providers';
import { positionsMetricsDataProvider as dataProvider } from './positions-data-providers';
const getSymbols = (positions: Position[]) =>
Array.from(new Set(positions.map((position) => position.assetSymbol))).sort();
export const usePositionsAssets = (partyId: string) => {
const variables = useMemo(() => ({ partyId }), [partyId]);
const assetSymbols = useRef<string[] | undefined>();
const update = useCallback(({ data }: { data: Position[] | null }) => {
if (data?.length) {
const newAssetSymbols = getSymbols(data);
if (
!newAssetSymbols.every(
(symbol) =>
assetSymbols.current && assetSymbols.current.includes(symbol)
)
) {
assetSymbols.current = newAssetSymbols;
return false;
}
}
return true;
}, []);
const { data, error, loading } = useDataProvider<Position[], never>({
dataProvider,
update,
variables,
});
if (!assetSymbols.current && data) {
assetSymbols.current = getSymbols(data);
}
return { data, error, loading, assetSymbols: assetSymbols.current };
};

View File

@ -2,12 +2,13 @@ import { useCallback, useMemo, useRef } from 'react';
import type { RefObject } from 'react'; import type { RefObject } from 'react';
import { BigNumber } from 'bignumber.js'; import { BigNumber } from 'bignumber.js';
import type { AgGridReact } from 'ag-grid-react'; import type { AgGridReact } from 'ag-grid-react';
import type { GetRowsParams } from './positions-table';
import type { Position } from './positions-data-providers'; import type { Position } from './positions-data-providers';
import { positionsMetricsDataProvider as dataProvider } from './positions-data-providers'; import { positionsMetricsProvider } from './positions-data-providers';
import filter from 'lodash/filter'; import type { PositionsMetricsProviderVariables } from './positions-data-providers';
import { t, toBigNum, useDataProvider } from '@vegaprotocol/react-helpers'; import { t, toBigNum, useDataProvider } from '@vegaprotocol/react-helpers';
export const getRowId = ({ data }: { data: Position }) => data.marketId;
const getSummaryRowData = (positions: Position[]) => { const getSummaryRowData = (positions: Position[]) => {
const summaryRow = { const summaryRow = {
notional: new BigNumber(0), notional: new BigNumber(0),
@ -37,35 +38,44 @@ const getSummaryRowData = (positions: Position[]) => {
export const usePositionsData = ( export const usePositionsData = (
partyId: string, partyId: string,
gridRef: RefObject<AgGridReact>, gridRef: RefObject<AgGridReact>
assetSymbol?: string
) => { ) => {
const variables = useMemo(() => ({ partyId }), [partyId]); const variables = useMemo<PositionsMetricsProviderVariables>(
() => ({ partyId }),
[partyId]
);
const dataRef = useRef<Position[] | null>(null); const dataRef = useRef<Position[] | null>(null);
const update = useCallback( const update = useCallback(
({ data }: { data: Position[] | null }) => { ({
dataRef.current = assetSymbol ? filter(data, { assetSymbol }) : data; data,
gridRef.current?.api?.refreshInfiniteCache(); delta,
return true; }: {
}, data: Position[] | null;
[assetSymbol, gridRef] delta?: Position[] | null;
); }) => {
const { data, error, loading } = useDataProvider<Position[], never>({ dataRef.current = data;
dataProvider,
update, const update: Position[] = [];
variables, const add: Position[] = [];
}); if (!gridRef.current?.api) {
if (!dataRef.current && data) { return false;
dataRef.current = assetSymbol ? filter(data, { assetSymbol }) : data; }
} (delta || []).forEach((position) => {
const getRows = useCallback( const rowNode = gridRef.current?.api.getRowNode(
async ({ successCallback, startRow, endRow }: GetRowsParams) => { getRowId({ data: position })
const rowsThisBlock = dataRef.current );
? dataRef.current.slice(startRow, endRow) if (rowNode) {
: []; update.push(position);
const lastRow = dataRef.current?.length ?? -1; } else {
successCallback(rowsThisBlock, lastRow); add.push(position);
if (gridRef.current?.api) { }
});
if (update.length || add.length) {
gridRef.current.api.applyTransactionAsync({
update,
add,
addIndex: 0,
});
const summaryRowNode = gridRef.current.api.getPinnedBottomRow(0); const summaryRowNode = gridRef.current.api.getPinnedBottomRow(0);
if (summaryRowNode && dataRef.current) { if (summaryRowNode && dataRef.current) {
summaryRowNode.data = getSummaryRowData(dataRef.current); summaryRowNode.data = getSummaryRowData(dataRef.current);
@ -79,13 +89,18 @@ export const usePositionsData = (
); );
} }
} }
return true;
}, },
[gridRef] [gridRef]
); );
const { data, error, loading } = useDataProvider({
dataProvider: positionsMetricsProvider,
update,
variables,
});
return { return {
data, data,
error, error,
loading, loading,
getRows,
}; };
}; };

View File

@ -15,8 +15,12 @@ function hasDelta<T>(
return !!updateData.isUpdate; return !!updateData.isUpdate;
} }
interface useDataProviderParams<Data, Delta> { interface useDataProviderParams<
dataProvider: Subscribe<Data, Delta>; Data,
Delta,
Variables extends OperationVariables = OperationVariables
> {
dataProvider: Subscribe<Data, Delta, Variables>;
update?: ({ delta, data }: { delta?: Delta; data: Data }) => boolean; update?: ({ delta, data }: { delta?: Delta; data: Data }) => boolean;
insert?: ({ insert?: ({
insertionData, insertionData,
@ -27,7 +31,7 @@ interface useDataProviderParams<Data, Delta> {
data: Data; data: Data;
totalCount?: number; totalCount?: number;
}) => boolean; }) => boolean;
variables?: OperationVariables; variables?: Variables;
updateOnInit?: boolean; updateOnInit?: boolean;
noUpdate?: boolean; noUpdate?: boolean;
skip?: boolean; skip?: boolean;
@ -40,7 +44,11 @@ interface useDataProviderParams<Data, Delta> {
* @param variables optional * @param variables optional
* @returns state: data, loading, error, methods: flush (pass updated data to update function without delta), restart: () => void}}; * @returns state: data, loading, error, methods: flush (pass updated data to update function without delta), restart: () => void}};
*/ */
export const useDataProvider = <Data, Delta>({ export const useDataProvider = <
Data,
Delta,
Variables extends OperationVariables = OperationVariables
>({
dataProvider, dataProvider,
update, update,
insert, insert,
@ -48,7 +56,7 @@ export const useDataProvider = <Data, Delta>({
updateOnInit, updateOnInit,
noUpdate, noUpdate,
skip, skip,
}: useDataProviderParams<Data, Delta>) => { }: useDataProviderParams<Data, Delta, Variables>) => {
const client = useApolloClient(); const client = useApolloClient();
const [data, setData] = useState<Data | null>(null); const [data, setData] = useState<Data | null>(null);
const [totalCount, setTotalCount] = useState<number>(); const [totalCount, setTotalCount] = useState<number>();

View File

@ -45,11 +45,15 @@ export interface PageInfo {
hasNextPage?: boolean; hasNextPage?: boolean;
hasPreviousPage?: boolean; hasPreviousPage?: boolean;
} }
export interface Subscribe<Data, Delta> { export interface Subscribe<
Data,
Delta,
Variables extends OperationVariables = OperationVariables
> {
( (
callback: UpdateCallback<Data, Delta>, callback: UpdateCallback<Data, Delta>,
client: ApolloClient<object>, client: ApolloClient<object>,
variables?: OperationVariables variables?: Variables
): { ): {
unsubscribe: () => void; unsubscribe: () => void;
reload: (forceReset?: boolean) => void; reload: (forceReset?: boolean) => void;
@ -166,7 +170,13 @@ interface DataProviderParams<QueryData, Data, SubscriptionData, Delta> {
* @param fetchPolicy * @param fetchPolicy
* @returns subscribe function * @returns subscribe function
*/ */
function makeDataProviderInternal<QueryData, Data, SubscriptionData, Delta>({ function makeDataProviderInternal<
QueryData,
Data,
SubscriptionData,
Delta,
Variables extends OperationVariables = OperationVariables
>({
query, query,
subscriptionQuery, subscriptionQuery,
update, update,
@ -177,7 +187,8 @@ function makeDataProviderInternal<QueryData, Data, SubscriptionData, Delta>({
resetDelay, resetDelay,
}: DataProviderParams<QueryData, Data, SubscriptionData, Delta>): Subscribe< }: DataProviderParams<QueryData, Data, SubscriptionData, Delta>): Subscribe<
Data, Data,
Delta Delta,
Variables
> { > {
// list of callbacks passed through subscribe call // list of callbacks passed through subscribe call
const callbacks: UpdateCallback<Data, Delta>[] = []; const callbacks: UpdateCallback<Data, Delta>[] = [];
@ -439,14 +450,18 @@ function makeDataProviderInternal<QueryData, Data, SubscriptionData, Delta>({
* @param fn * @param fn
* @returns subscibe function * @returns subscibe function
*/ */
const memoize = <Data, Delta>( const memoize = <
fn: (variables?: OperationVariables) => Subscribe<Data, Delta> Data,
Delta,
Variables extends OperationVariables = OperationVariables
>(
fn: (variables?: Variables) => Subscribe<Data, Delta, Variables>
) => { ) => {
const cache: { const cache: {
subscribe: Subscribe<Data, Delta>; subscribe: Subscribe<Data, Delta, Variables>;
variables?: OperationVariables; variables?: Variables;
}[] = []; }[] = [];
return (variables?: OperationVariables) => { return (variables?: Variables) => {
const cached = cache.find((c) => isEqual(c.variables, variables)); const cached = cache.find((c) => isEqual(c.variables, variables));
if (cached) { if (cached) {
return cached.subscribe; return cached.subscribe;
@ -481,9 +496,15 @@ const memoize = <Data, Delta>(
* ) * )
* *
*/ */
export function makeDataProvider<QueryData, Data, SubscriptionData, Delta>( export function makeDataProvider<
QueryData,
Data,
SubscriptionData,
Delta,
Variables extends OperationVariables = OperationVariables
>(
params: DataProviderParams<QueryData, Data, SubscriptionData, Delta> params: DataProviderParams<QueryData, Data, SubscriptionData, Delta>
): Subscribe<Data, Delta> { ): Subscribe<Data, Delta, Variables> {
const getInstance = memoize<Data, Delta>(() => const getInstance = memoize<Data, Delta>(() =>
makeDataProviderInternal(params) makeDataProviderInternal(params)
); );
@ -498,33 +519,45 @@ export function makeDataProvider<QueryData, Data, SubscriptionData, Delta>(
type DependencySubscribe = Subscribe<any, any>; // eslint-disable-line @typescript-eslint/no-explicit-any type DependencySubscribe = Subscribe<any, any>; // eslint-disable-line @typescript-eslint/no-explicit-any
type DependencyUpdateCallback = Parameters<DependencySubscribe>['0']; type DependencyUpdateCallback = Parameters<DependencySubscribe>['0'];
export type DerivedPart = Parameters<DependencyUpdateCallback>['0']; export type DerivedPart = Parameters<DependencyUpdateCallback>['0'];
export type CombineDerivedData<Data> = ( export type CombineDerivedData<
data: DerivedPart['data'][], Data,
variables?: OperationVariables Variables extends OperationVariables = OperationVariables
) => Data | null; > = (data: DerivedPart['data'][], variables?: Variables) => Data | null;
export type CombineDerivedDelta<Data, Delta> = ( export type CombineDerivedDelta<
Data,
Delta,
Variables extends OperationVariables = OperationVariables
> = (
data: Data, data: Data,
parts: DerivedPart[], parts: DerivedPart[],
variables?: OperationVariables previousData: Data | null,
variables?: Variables
) => Delta | undefined; ) => Delta | undefined;
export type CombineInsertionData<Data> = ( export type CombineInsertionData<
Data,
Variables extends OperationVariables = OperationVariables
> = (
data: Data, data: Data,
parts: DerivedPart[], parts: DerivedPart[],
variables?: OperationVariables variables?: Variables
) => Data | undefined; ) => Data | undefined;
function makeDerivedDataProviderInternal<Data, Delta>( function makeDerivedDataProviderInternal<
Data,
Delta,
Variables extends OperationVariables = OperationVariables
>(
dependencies: DependencySubscribe[], dependencies: DependencySubscribe[],
combineData: CombineDerivedData<Data>, combineData: CombineDerivedData<Data, Variables>,
combineDelta?: CombineDerivedDelta<Data, Delta>, combineDelta?: CombineDerivedDelta<Data, Delta, Variables>,
combineInsertionData?: CombineInsertionData<Data> combineInsertionData?: CombineInsertionData<Data, Variables>
): Subscribe<Data, Delta> { ): Subscribe<Data, Delta, Variables> {
let subscriptions: ReturnType<DependencySubscribe>[] | undefined; let subscriptions: ReturnType<DependencySubscribe>[] | undefined;
let client: ApolloClient<object>; let client: ApolloClient<object>;
const callbacks: UpdateCallback<Data, Delta>[] = []; const callbacks: UpdateCallback<Data, Delta>[] = [];
let variables: OperationVariables | undefined; let variables: Variables | undefined;
const parts: DerivedPart[] = []; const parts: DerivedPart[] = [];
let data: Data | null = null; let data: Data | null = null;
let error: Error | undefined; let error: Error | undefined;
@ -581,13 +614,14 @@ function makeDerivedDataProviderInternal<Data, Delta>(
loading = newLoading; loading = newLoading;
error = newError; error = newError;
loaded = newLoaded; loaded = newLoaded;
const previousData = data;
data = newData; data = newData;
if (newLoaded) { if (newLoaded) {
const updatedPart = parts[updatedPartIndex]; const updatedPart = parts[updatedPartIndex];
if (updatedPart.isUpdate) { if (updatedPart.isUpdate) {
isUpdate = true; isUpdate = true;
if (updatedPart.delta && combineDelta && data) { if (updatedPart.delta && combineDelta && data) {
delta = combineDelta(data, parts, variables); delta = combineDelta(data, parts, previousData, variables);
} }
delete updatedPart.isUpdate; delete updatedPart.isUpdate;
delete updatedPart.delta; delete updatedPart.delta;
@ -661,14 +695,18 @@ function makeDerivedDataProviderInternal<Data, Delta>(
}; };
} }
export function makeDerivedDataProvider<Data, Delta>( export function makeDerivedDataProvider<
Data,
Delta,
Variables extends OperationVariables = OperationVariables
>(
dependencies: DependencySubscribe[], dependencies: DependencySubscribe[],
combineData: CombineDerivedData<Data>, combineData: CombineDerivedData<Data, Variables>,
combineDelta?: CombineDerivedDelta<Data, Delta>, combineDelta?: CombineDerivedDelta<Data, Delta, Variables>,
combineInsertionData?: CombineInsertionData<Data> combineInsertionData?: CombineInsertionData<Data, Variables>
): Subscribe<Data, Delta> { ): Subscribe<Data, Delta, Variables> {
const getInstance = memoize<Data, Delta>(() => const getInstance = memoize<Data, Delta, Variables>(() =>
makeDerivedDataProviderInternal( makeDerivedDataProviderInternal<Data, Delta, Variables>(
dependencies, dependencies,
combineData, combineData,
combineDelta, combineDelta,

View File

@ -2,6 +2,7 @@ import type { Get } from 'type-fest';
import type { import type {
ICellRendererParams, ICellRendererParams,
ValueFormatterParams, ValueFormatterParams,
ValueGetterParams,
} from 'ag-grid-community'; } from 'ag-grid-community';
import type { IDatasource, IGetRowsParams } from 'ag-grid-community'; import type { IDatasource, IGetRowsParams } from 'ag-grid-community';
@ -26,6 +27,12 @@ export type VegaValueFormatterParams<TRow, TField extends Field> = RowHelper<
TField TField
>; >;
export type VegaValueGetterParams<TRow, TField extends Field> = RowHelper<
ValueGetterParams,
TRow,
TField
>;
export type VegaICellRendererParams< export type VegaICellRendererParams<
TRow, TRow,
TField extends Field = string TField extends Field = string