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:
parent
186bcfa95a
commit
3415c8d86c
@ -95,8 +95,7 @@ describe('Portfolio page', { tags: '@smoke' }, () => {
|
||||
});
|
||||
|
||||
it('data should be properly rendered', () => {
|
||||
cy.getByTestId('positions-asset-tDAI').should('exist');
|
||||
cy.getByTestId('positions-asset-tEURO').should('exist');
|
||||
cy.get('.ag-center-cols-container .ag-row').should('have.length', 2);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -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;
|
@ -1,26 +1,37 @@
|
||||
import { useOutletContext } from 'react-router-dom';
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
import { usePositionsAssets } from '@vegaprotocol/positions';
|
||||
import { AsyncRenderer, Splash } from '@vegaprotocol/ui-toolkit';
|
||||
import PositionsAsset from './positions-asset';
|
||||
import { PriceFlashCell } from '@vegaprotocol/react-helpers';
|
||||
import { usePositionsData, getRowId } from '@vegaprotocol/positions';
|
||||
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
|
||||
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 gridRef = useRef<AgGridReact | null>(null);
|
||||
const { partyId } = useOutletContext<{ partyId: string }>();
|
||||
const { data, error, loading, assetSymbols } = usePositionsAssets(partyId);
|
||||
const { data, error, loading } = usePositionsData(partyId, gridRef);
|
||||
const { columnDefs, defaultColDef } = useColumnDefinitions();
|
||||
return (
|
||||
<AsyncRenderer loading={loading} error={error} data={data}>
|
||||
{assetSymbols && assetSymbols.length > 0 && (
|
||||
<div className="w-full, h-max">
|
||||
{assetSymbols?.map((assetSymbol) => (
|
||||
<PositionsAsset
|
||||
key={assetSymbol}
|
||||
partyId={partyId}
|
||||
assetSymbol={assetSymbol}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{assetSymbols?.length === 0 && <Splash>{t('No data to display')}</Splash>}
|
||||
<AsyncRenderer
|
||||
loading={loading}
|
||||
error={error}
|
||||
data={data?.length ? data : null}
|
||||
noDataMessage={NO_DATA_MESSAGE}
|
||||
>
|
||||
<ConsoleLiteGrid<Position>
|
||||
ref={gridRef}
|
||||
domLayout="autoHeight"
|
||||
classNamesParam="h-auto"
|
||||
columnDefs={columnDefs}
|
||||
defaultColDef={defaultColDef}
|
||||
getRowId={getRowId}
|
||||
rowData={data || undefined}
|
||||
components={{ PriceFlashCell }}
|
||||
/>
|
||||
</AsyncRenderer>
|
||||
);
|
||||
};
|
||||
|
@ -1,21 +1,19 @@
|
||||
import { forwardRef } from 'react';
|
||||
import type {
|
||||
GroupCellRendererParams,
|
||||
ValueFormatterParams,
|
||||
} from 'ag-grid-community';
|
||||
import {
|
||||
PriceFlashCell,
|
||||
addDecimalsFormatNumber,
|
||||
t,
|
||||
toBigNum,
|
||||
} from '@vegaprotocol/react-helpers';
|
||||
import type {
|
||||
VegaValueGetterParams,
|
||||
VegaValueFormatterParams,
|
||||
VegaICellRendererParams,
|
||||
TypedDataAgGrid,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { AgGridDynamic as AgGrid } from '@vegaprotocol/ui-toolkit';
|
||||
import { AgGridColumn } from 'ag-grid-react';
|
||||
import type {
|
||||
AgGridReact,
|
||||
AgGridReactProps,
|
||||
AgReactUiProps,
|
||||
} from 'ag-grid-react';
|
||||
import type { AgGridReact } from 'ag-grid-react';
|
||||
import {
|
||||
MarketTradingMode,
|
||||
AuctionTrigger,
|
||||
@ -25,18 +23,12 @@ import {
|
||||
import type { MarketWithData } from '../../';
|
||||
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 MarketListTable = forwardRef<AgGridReact, Props>((props, ref) => {
|
||||
export const MarketListTable = forwardRef<
|
||||
AgGridReact,
|
||||
TypedDataAgGrid<MarketWithData>
|
||||
>((props, ref) => {
|
||||
const { open: openAssetDetailsDialog } = useAssetDetailsDialogStore();
|
||||
return (
|
||||
<AgGrid
|
||||
@ -62,7 +54,12 @@ export const MarketListTable = forwardRef<AgGridReact, Props>((props, ref) => {
|
||||
<AgGridColumn
|
||||
headerName={t('Settlement asset')}
|
||||
field="tradableInstrument.instrument.product.settlementAsset.symbol"
|
||||
cellRenderer={({ value }: GroupCellRendererParams) =>
|
||||
cellRenderer={({
|
||||
value,
|
||||
}: VegaICellRendererParams<
|
||||
MarketWithData,
|
||||
'tradableInstrument.instrument.product.settlementAsset.symbol'
|
||||
>) =>
|
||||
value && value.length > 0 ? (
|
||||
<button
|
||||
className="hover:underline"
|
||||
@ -81,7 +78,9 @@ export const MarketListTable = forwardRef<AgGridReact, Props>((props, ref) => {
|
||||
headerName={t('Trading mode')}
|
||||
field="data"
|
||||
minWidth={170}
|
||||
valueGetter={({ data }: { data?: MarketWithData }) => {
|
||||
valueGetter={({
|
||||
data,
|
||||
}: VegaValueGetterParams<MarketWithData, 'data'>) => {
|
||||
if (!data?.data) return undefined;
|
||||
const { trigger } = data.data;
|
||||
const { tradingMode } = data;
|
||||
@ -100,12 +99,16 @@ export const MarketListTable = forwardRef<AgGridReact, Props>((props, ref) => {
|
||||
type="rightAligned"
|
||||
cellRenderer="PriceFlashCell"
|
||||
filter="agNumberColumnFilter"
|
||||
valueGetter={({ data }: { data?: MarketWithData }) => {
|
||||
valueGetter={({
|
||||
data,
|
||||
}: VegaValueGetterParams<MarketWithData, 'data.bestBidPrice'>) => {
|
||||
return data?.data?.bestBidPrice === undefined
|
||||
? undefined
|
||||
: toBigNum(data?.data?.bestBidPrice, data.decimalPlaces).toNumber();
|
||||
}}
|
||||
valueFormatter={({ data }: MarketListTableValueFormatterParams) =>
|
||||
valueFormatter={({
|
||||
data,
|
||||
}: VegaValueFormatterParams<MarketWithData, 'data.bestBidPrice'>) =>
|
||||
data?.data?.bestBidPrice === undefined
|
||||
? undefined
|
||||
: addDecimalsFormatNumber(
|
||||
@ -120,7 +123,9 @@ export const MarketListTable = forwardRef<AgGridReact, Props>((props, ref) => {
|
||||
type="rightAligned"
|
||||
cellRenderer="PriceFlashCell"
|
||||
filter="agNumberColumnFilter"
|
||||
valueGetter={({ data }: { data?: MarketWithData }) => {
|
||||
valueGetter={({
|
||||
data,
|
||||
}: VegaValueGetterParams<MarketWithData, 'data.bestOfferPrice'>) => {
|
||||
return data?.data?.bestOfferPrice === undefined
|
||||
? undefined
|
||||
: toBigNum(
|
||||
@ -128,7 +133,9 @@ export const MarketListTable = forwardRef<AgGridReact, Props>((props, ref) => {
|
||||
data.decimalPlaces
|
||||
).toNumber();
|
||||
}}
|
||||
valueFormatter={({ data }: MarketListTableValueFormatterParams) =>
|
||||
valueFormatter={({
|
||||
data,
|
||||
}: VegaValueFormatterParams<MarketWithData, 'data.bestOfferPrice'>) =>
|
||||
data?.data?.bestOfferPrice === undefined
|
||||
? undefined
|
||||
: addDecimalsFormatNumber(
|
||||
@ -143,12 +150,16 @@ export const MarketListTable = forwardRef<AgGridReact, Props>((props, ref) => {
|
||||
type="rightAligned"
|
||||
cellRenderer="PriceFlashCell"
|
||||
filter="agNumberColumnFilter"
|
||||
valueGetter={({ data }: { data?: MarketWithData }) => {
|
||||
valueGetter={({
|
||||
data,
|
||||
}: VegaValueGetterParams<MarketWithData, 'data.markPrice'>) => {
|
||||
return data?.data?.markPrice === undefined
|
||||
? undefined
|
||||
: toBigNum(data?.data?.markPrice, data.decimalPlaces).toNumber();
|
||||
}}
|
||||
valueFormatter={({ data }: MarketListTableValueFormatterParams) =>
|
||||
valueFormatter={({
|
||||
data,
|
||||
}: VegaValueFormatterParams<MarketWithData, 'data.markPrice'>) =>
|
||||
data?.data?.bestOfferPrice === undefined
|
||||
? undefined
|
||||
: addDecimalsFormatNumber(data.data.markPrice, data.decimalPlaces)
|
||||
|
@ -4,4 +4,3 @@ export * from './lib/positions-data-providers';
|
||||
export * from './lib/positions-table';
|
||||
export * from './lib/use-close-position';
|
||||
export * from './lib/use-positions-data';
|
||||
export * from './lib/use-positions-assets';
|
||||
|
@ -1,3 +1,4 @@
|
||||
import isEqual from 'lodash/isEqual';
|
||||
import produce from 'immer';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import sortBy from 'lodash/sortBy';
|
||||
@ -42,7 +43,7 @@ interface PositionRejoined {
|
||||
export interface Position {
|
||||
marketName: string;
|
||||
averageEntryPrice: string;
|
||||
marginAccountBalance: BigNumber;
|
||||
marginAccountBalance: string;
|
||||
capitalUtilisation: number;
|
||||
currentLeverage: number;
|
||||
decimals: number;
|
||||
@ -152,7 +153,7 @@ export const getMetrics = (
|
||||
metrics.push({
|
||||
marketName: market.tradableInstrument.instrument.name,
|
||||
averageEntryPrice: position.averageEntryPrice,
|
||||
marginAccountBalance,
|
||||
marginAccountBalance: marginAccount.balance,
|
||||
capitalUtilisation: Math.round(capitalUtilisation.toNumber()),
|
||||
currentLeverage: currentLeverage.toNumber(),
|
||||
marketDecimalPlaces,
|
||||
@ -277,9 +278,14 @@ export const rejoinPositionData = (
|
||||
return null;
|
||||
};
|
||||
|
||||
export const positionsMetricsDataProvider = makeDerivedDataProvider<
|
||||
export interface PositionsMetricsProviderVariables {
|
||||
partyId: string;
|
||||
}
|
||||
|
||||
export const positionsMetricsProvider = makeDerivedDataProvider<
|
||||
Position[],
|
||||
never
|
||||
Position[],
|
||||
PositionsMetricsProviderVariables
|
||||
>(
|
||||
[
|
||||
positionsDataProvider,
|
||||
@ -287,11 +293,21 @@ export const positionsMetricsDataProvider = makeDerivedDataProvider<
|
||||
marketsWithDataProvider,
|
||||
marginsDataProvider,
|
||||
],
|
||||
([positions, accounts, marketsData, margins]) => {
|
||||
([positions, accounts, marketsData, margins], variables) => {
|
||||
const positionsData = rejoinPositionData(positions, marketsData, margins);
|
||||
if (!variables) {
|
||||
return [];
|
||||
}
|
||||
return sortBy(
|
||||
getMetrics(positionsData, accounts as Account[] | null),
|
||||
'marketName'
|
||||
);
|
||||
}
|
||||
},
|
||||
(data, delta, previousData) =>
|
||||
data.filter((row) => {
|
||||
const previousRow = previousData?.find(
|
||||
(previousRow) => previousRow.marketId === row.marketId
|
||||
);
|
||||
return !(previousRow && isEqual(previousRow, row));
|
||||
})
|
||||
);
|
||||
|
@ -13,7 +13,7 @@ interface PositionsManagerProps {
|
||||
|
||||
export const PositionsManager = ({ partyId }: PositionsManagerProps) => {
|
||||
const gridRef = useRef<AgGridReact | null>(null);
|
||||
const { data, error, loading, getRows } = usePositionsData(partyId, gridRef);
|
||||
const { data, error, loading } = usePositionsData(partyId, gridRef);
|
||||
const {
|
||||
submit,
|
||||
closingOrder,
|
||||
@ -30,9 +30,7 @@ export const PositionsManager = ({ partyId }: PositionsManagerProps) => {
|
||||
domLayout="autoHeight"
|
||||
style={{ width: '100%' }}
|
||||
ref={gridRef}
|
||||
rowModelType={data?.length ? 'infinite' : 'clientSide'}
|
||||
rowData={data?.length ? undefined : []}
|
||||
datasource={{ getRows }}
|
||||
rowData={data}
|
||||
onClose={(position) => submit(position)}
|
||||
/>
|
||||
</AsyncRenderer>
|
||||
|
@ -3,8 +3,6 @@ import { act, render, screen } from '@testing-library/react';
|
||||
import PositionsTable from './positions-table';
|
||||
import type { Position } from './positions-data-providers';
|
||||
import { MarketTradingMode } from '@vegaprotocol/types';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import React from 'react';
|
||||
|
||||
const singleRow: Position = {
|
||||
marketName: 'ETH/BTC (31 july 2022)',
|
||||
@ -27,7 +25,7 @@ const singleRow: Position = {
|
||||
unrealisedPNL: '456',
|
||||
searchPrice: '0',
|
||||
updatedAt: '2022-07-27T15:02:58.400Z',
|
||||
marginAccountBalance: new BigNumber(123456),
|
||||
marginAccountBalance: '12345600',
|
||||
};
|
||||
|
||||
const singleRowData = [singleRow];
|
||||
|
@ -8,6 +8,8 @@ import type {
|
||||
import type {
|
||||
ValueProps as PriceCellProps,
|
||||
VegaValueFormatterParams,
|
||||
VegaValueGetterParams,
|
||||
TypedDataAgGrid,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { EmptyCell, ProgressBarCell } from '@vegaprotocol/ui-toolkit';
|
||||
import {
|
||||
@ -15,6 +17,7 @@ import {
|
||||
addDecimalsFormatNumber,
|
||||
volumePrefix,
|
||||
t,
|
||||
toBigNum,
|
||||
formatNumber,
|
||||
getDateTimeFormat,
|
||||
signedNumberCssClass,
|
||||
@ -22,24 +25,13 @@ import {
|
||||
} from '@vegaprotocol/react-helpers';
|
||||
import { AgGridDynamic as AgGrid } from '@vegaprotocol/ui-toolkit';
|
||||
import { AgGridColumn } from 'ag-grid-react';
|
||||
import type { AgGridReact, AgGridReactProps } from 'ag-grid-react';
|
||||
import type { IDatasource, IGetRowsParams } from 'ag-grid-community';
|
||||
import type { AgGridReact } from 'ag-grid-react';
|
||||
import type { Position } from './positions-data-providers';
|
||||
import { MarketTradingMode } from '@vegaprotocol/types';
|
||||
import { Intent, Button, TooltipCellComponent } from '@vegaprotocol/ui-toolkit';
|
||||
import { getRowId } from './use-positions-data';
|
||||
|
||||
export const getRowId = ({ data }: { data: Position }) => data.marketId;
|
||||
|
||||
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;
|
||||
interface Props extends TypedDataAgGrid<Position> {
|
||||
onClose?: (data: Position) => void;
|
||||
style?: CSSProperties;
|
||||
}
|
||||
@ -143,6 +135,8 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
|
||||
defaultColDef={{
|
||||
flex: 1,
|
||||
resizable: true,
|
||||
sortable: true,
|
||||
filter: true,
|
||||
tooltipComponent: TooltipCellComponent,
|
||||
}}
|
||||
components={{ AmountCell, PriceFlashCell, ProgressBarCell }}
|
||||
@ -171,14 +165,20 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
|
||||
field="notional"
|
||||
type="rightAligned"
|
||||
cellClass="font-mono text-right"
|
||||
valueFormatter={({
|
||||
value,
|
||||
filter="agNumberColumnFilter"
|
||||
valueGetter={({
|
||||
data,
|
||||
}: VegaValueFormatterParams<Position, 'notional'>): string => {
|
||||
if (!value || !data) {
|
||||
return '';
|
||||
}
|
||||
return addDecimalsFormatNumber(value, data.decimals);
|
||||
}: VegaValueGetterParams<Position, 'notional'>) => {
|
||||
return data?.notional === undefined
|
||||
? undefined
|
||||
: toBigNum(data?.notional, data.decimals).toNumber();
|
||||
}}
|
||||
valueFormatter={({
|
||||
data,
|
||||
}: VegaValueFormatterParams<Position, 'notional'>) => {
|
||||
return !data
|
||||
? undefined
|
||||
: addDecimalsFormatNumber(data.notional, data.decimals);
|
||||
}}
|
||||
/>
|
||||
<AgGridColumn
|
||||
@ -187,18 +187,27 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
|
||||
type="rightAligned"
|
||||
cellClass="font-mono text-right"
|
||||
cellClassRules={signedNumberCssClassRules}
|
||||
filter="agNumberColumnFilter"
|
||||
valueGetter={({
|
||||
data,
|
||||
}: VegaValueGetterParams<Position, 'openVolume'>) => {
|
||||
return data?.openVolume === undefined
|
||||
? undefined
|
||||
: toBigNum(data?.openVolume, data.decimals).toNumber();
|
||||
}}
|
||||
valueFormatter={({
|
||||
value,
|
||||
data,
|
||||
}: VegaValueFormatterParams<Position, 'openVolume'>):
|
||||
| string
|
||||
| undefined => {
|
||||
if (!value || !data) {
|
||||
return undefined;
|
||||
}
|
||||
return volumePrefix(
|
||||
addDecimalsFormatNumber(value, data.positionDecimalPlaces)
|
||||
);
|
||||
return !data
|
||||
? undefined
|
||||
: volumePrefix(
|
||||
addDecimalsFormatNumber(
|
||||
data.openVolume,
|
||||
data.positionDecimalPlaces
|
||||
)
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<AgGridColumn
|
||||
@ -212,12 +221,21 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
|
||||
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={({
|
||||
value,
|
||||
data,
|
||||
node,
|
||||
}: VegaValueFormatterParams<Position, 'markPrice'>) => {
|
||||
if (!data || !value || node?.rowPinned) {
|
||||
if (!data || node?.rowPinned) {
|
||||
return undefined;
|
||||
}
|
||||
if (
|
||||
@ -227,7 +245,7 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
|
||||
return '-';
|
||||
}
|
||||
return addDecimalsFormatNumber(
|
||||
value.toString(),
|
||||
data.markPrice,
|
||||
data.marketDecimalPlaces
|
||||
);
|
||||
}}
|
||||
@ -244,17 +262,30 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
|
||||
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={({
|
||||
data,
|
||||
value,
|
||||
node,
|
||||
}: VegaValueFormatterParams<Position, 'averageEntryPrice'>):
|
||||
| string
|
||||
| undefined => {
|
||||
if (!data || node?.rowPinned || !value) {
|
||||
if (!data || node?.rowPinned) {
|
||||
return undefined;
|
||||
}
|
||||
return addDecimalsFormatNumber(value, data.marketDecimalPlaces);
|
||||
return addDecimalsFormatNumber(
|
||||
data.averageEntryPrice,
|
||||
data.marketDecimalPlaces
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<AgGridColumn
|
||||
@ -264,6 +295,17 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
|
||||
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.'
|
||||
)}
|
||||
filter="agNumberColumnFilter"
|
||||
valueGetter={({
|
||||
data,
|
||||
}: VegaValueGetterParams<Position, 'liquidationPrice'>) => {
|
||||
return !data
|
||||
? undefined
|
||||
: toBigNum(
|
||||
data?.liquidationPrice,
|
||||
data.marketDecimalPlaces
|
||||
).toNumber();
|
||||
}}
|
||||
cellRendererSelector={(
|
||||
params: ICellRendererParams
|
||||
): CellRendererSelectorResult => {
|
||||
@ -277,6 +319,7 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
|
||||
headerName={t('Leverage')}
|
||||
field="currentLeverage"
|
||||
type="rightAligned"
|
||||
filter="agNumberColumnFilter"
|
||||
cellRendererSelector={(
|
||||
params: ICellRendererParams
|
||||
): CellRendererSelectorResult => {
|
||||
@ -294,6 +337,7 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
|
||||
headerName={t('Margin allocated')}
|
||||
field="marginAccountBalance"
|
||||
type="rightAligned"
|
||||
filter="agNumberColumnFilter"
|
||||
cellRendererSelector={(
|
||||
params: ICellRendererParams
|
||||
): CellRendererSelectorResult => {
|
||||
@ -301,17 +345,26 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
|
||||
component: params.node.rowPinned ? EmptyCell : PriceFlashCell,
|
||||
};
|
||||
}}
|
||||
valueGetter={({
|
||||
data,
|
||||
}: VegaValueGetterParams<Position, 'marginAccountBalance'>) => {
|
||||
return !data
|
||||
? undefined
|
||||
: toBigNum(data.marginAccountBalance, data.decimals).toNumber();
|
||||
}}
|
||||
valueFormatter={({
|
||||
data,
|
||||
value,
|
||||
node,
|
||||
}: VegaValueFormatterParams<Position, 'marginAccountBalance'>):
|
||||
| string
|
||||
| undefined => {
|
||||
if (!data || node?.rowPinned || !value) {
|
||||
if (!data || node?.rowPinned) {
|
||||
return undefined;
|
||||
}
|
||||
return formatNumber(value, data.decimals);
|
||||
return addDecimalsFormatNumber(
|
||||
data.marginAccountBalance,
|
||||
data.decimals
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<AgGridColumn
|
||||
@ -319,14 +372,21 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
|
||||
field="realisedPNL"
|
||||
type="rightAligned"
|
||||
cellClassRules={signedNumberCssClassRules}
|
||||
valueFormatter={({
|
||||
value,
|
||||
filter="agNumberColumnFilter"
|
||||
valueGetter={({
|
||||
data,
|
||||
}: VegaValueFormatterParams<Position, 'realisedPNL'>) =>
|
||||
value === undefined || data === undefined
|
||||
}: VegaValueGetterParams<Position, 'realisedPNL'>) => {
|
||||
return !data
|
||||
? 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"
|
||||
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.'
|
||||
@ -337,13 +397,20 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
|
||||
field="unrealisedPNL"
|
||||
type="rightAligned"
|
||||
cellClassRules={signedNumberCssClassRules}
|
||||
filter="agNumberColumnFilter"
|
||||
valueGetter={({
|
||||
data,
|
||||
}: VegaValueGetterParams<Position, 'unrealisedPNL'>) => {
|
||||
return !data
|
||||
? undefined
|
||||
: toBigNum(data.unrealisedPNL, data.decimals).toNumber();
|
||||
}}
|
||||
valueFormatter={({
|
||||
value,
|
||||
data,
|
||||
}: VegaValueFormatterParams<Position, 'unrealisedPNL'>) =>
|
||||
value === undefined || data === undefined
|
||||
!data
|
||||
? undefined
|
||||
: addDecimalsFormatNumber(value.toString(), data.decimals)
|
||||
: addDecimalsFormatNumber(data.unrealisedPNL, data.decimals)
|
||||
}
|
||||
cellRenderer="PriceFlashCell"
|
||||
headerTooltip={t(
|
||||
@ -354,6 +421,7 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
|
||||
headerName={t('Updated')}
|
||||
field="updatedAt"
|
||||
type="rightAligned"
|
||||
filter="agDateColumnFilter"
|
||||
valueFormatter={({
|
||||
value,
|
||||
}: VegaValueFormatterParams<Position, 'updatedAt'>) => {
|
||||
|
@ -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 };
|
||||
};
|
@ -2,12 +2,13 @@ import { useCallback, useMemo, useRef } from 'react';
|
||||
import type { RefObject } from 'react';
|
||||
import { BigNumber } from 'bignumber.js';
|
||||
import type { AgGridReact } from 'ag-grid-react';
|
||||
import type { GetRowsParams } from './positions-table';
|
||||
import type { Position } from './positions-data-providers';
|
||||
import { positionsMetricsDataProvider as dataProvider } from './positions-data-providers';
|
||||
import filter from 'lodash/filter';
|
||||
import { positionsMetricsProvider } from './positions-data-providers';
|
||||
import type { PositionsMetricsProviderVariables } from './positions-data-providers';
|
||||
import { t, toBigNum, useDataProvider } from '@vegaprotocol/react-helpers';
|
||||
|
||||
export const getRowId = ({ data }: { data: Position }) => data.marketId;
|
||||
|
||||
const getSummaryRowData = (positions: Position[]) => {
|
||||
const summaryRow = {
|
||||
notional: new BigNumber(0),
|
||||
@ -37,35 +38,44 @@ const getSummaryRowData = (positions: Position[]) => {
|
||||
|
||||
export const usePositionsData = (
|
||||
partyId: string,
|
||||
gridRef: RefObject<AgGridReact>,
|
||||
assetSymbol?: string
|
||||
gridRef: RefObject<AgGridReact>
|
||||
) => {
|
||||
const variables = useMemo(() => ({ partyId }), [partyId]);
|
||||
const variables = useMemo<PositionsMetricsProviderVariables>(
|
||||
() => ({ partyId }),
|
||||
[partyId]
|
||||
);
|
||||
const dataRef = useRef<Position[] | null>(null);
|
||||
const update = useCallback(
|
||||
({ data }: { data: Position[] | null }) => {
|
||||
dataRef.current = assetSymbol ? filter(data, { assetSymbol }) : data;
|
||||
gridRef.current?.api?.refreshInfiniteCache();
|
||||
return true;
|
||||
},
|
||||
[assetSymbol, gridRef]
|
||||
);
|
||||
const { data, error, loading } = useDataProvider<Position[], never>({
|
||||
dataProvider,
|
||||
update,
|
||||
variables,
|
||||
});
|
||||
if (!dataRef.current && data) {
|
||||
dataRef.current = assetSymbol ? filter(data, { assetSymbol }) : data;
|
||||
}
|
||||
const getRows = useCallback(
|
||||
async ({ successCallback, startRow, endRow }: GetRowsParams) => {
|
||||
const rowsThisBlock = dataRef.current
|
||||
? dataRef.current.slice(startRow, endRow)
|
||||
: [];
|
||||
const lastRow = dataRef.current?.length ?? -1;
|
||||
successCallback(rowsThisBlock, lastRow);
|
||||
if (gridRef.current?.api) {
|
||||
({
|
||||
data,
|
||||
delta,
|
||||
}: {
|
||||
data: Position[] | null;
|
||||
delta?: Position[] | null;
|
||||
}) => {
|
||||
dataRef.current = data;
|
||||
|
||||
const update: Position[] = [];
|
||||
const add: Position[] = [];
|
||||
if (!gridRef.current?.api) {
|
||||
return false;
|
||||
}
|
||||
(delta || []).forEach((position) => {
|
||||
const rowNode = gridRef.current?.api.getRowNode(
|
||||
getRowId({ data: position })
|
||||
);
|
||||
if (rowNode) {
|
||||
update.push(position);
|
||||
} else {
|
||||
add.push(position);
|
||||
}
|
||||
});
|
||||
if (update.length || add.length) {
|
||||
gridRef.current.api.applyTransactionAsync({
|
||||
update,
|
||||
add,
|
||||
addIndex: 0,
|
||||
});
|
||||
const summaryRowNode = gridRef.current.api.getPinnedBottomRow(0);
|
||||
if (summaryRowNode && dataRef.current) {
|
||||
summaryRowNode.data = getSummaryRowData(dataRef.current);
|
||||
@ -79,13 +89,18 @@ export const usePositionsData = (
|
||||
);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
},
|
||||
[gridRef]
|
||||
);
|
||||
const { data, error, loading } = useDataProvider({
|
||||
dataProvider: positionsMetricsProvider,
|
||||
update,
|
||||
variables,
|
||||
});
|
||||
return {
|
||||
data,
|
||||
error,
|
||||
loading,
|
||||
getRows,
|
||||
};
|
||||
};
|
||||
|
@ -15,8 +15,12 @@ function hasDelta<T>(
|
||||
return !!updateData.isUpdate;
|
||||
}
|
||||
|
||||
interface useDataProviderParams<Data, Delta> {
|
||||
dataProvider: Subscribe<Data, Delta>;
|
||||
interface useDataProviderParams<
|
||||
Data,
|
||||
Delta,
|
||||
Variables extends OperationVariables = OperationVariables
|
||||
> {
|
||||
dataProvider: Subscribe<Data, Delta, Variables>;
|
||||
update?: ({ delta, data }: { delta?: Delta; data: Data }) => boolean;
|
||||
insert?: ({
|
||||
insertionData,
|
||||
@ -27,7 +31,7 @@ interface useDataProviderParams<Data, Delta> {
|
||||
data: Data;
|
||||
totalCount?: number;
|
||||
}) => boolean;
|
||||
variables?: OperationVariables;
|
||||
variables?: Variables;
|
||||
updateOnInit?: boolean;
|
||||
noUpdate?: boolean;
|
||||
skip?: boolean;
|
||||
@ -40,7 +44,11 @@ interface useDataProviderParams<Data, Delta> {
|
||||
* @param variables optional
|
||||
* @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,
|
||||
update,
|
||||
insert,
|
||||
@ -48,7 +56,7 @@ export const useDataProvider = <Data, Delta>({
|
||||
updateOnInit,
|
||||
noUpdate,
|
||||
skip,
|
||||
}: useDataProviderParams<Data, Delta>) => {
|
||||
}: useDataProviderParams<Data, Delta, Variables>) => {
|
||||
const client = useApolloClient();
|
||||
const [data, setData] = useState<Data | null>(null);
|
||||
const [totalCount, setTotalCount] = useState<number>();
|
||||
|
@ -45,11 +45,15 @@ export interface PageInfo {
|
||||
hasNextPage?: boolean;
|
||||
hasPreviousPage?: boolean;
|
||||
}
|
||||
export interface Subscribe<Data, Delta> {
|
||||
export interface Subscribe<
|
||||
Data,
|
||||
Delta,
|
||||
Variables extends OperationVariables = OperationVariables
|
||||
> {
|
||||
(
|
||||
callback: UpdateCallback<Data, Delta>,
|
||||
client: ApolloClient<object>,
|
||||
variables?: OperationVariables
|
||||
variables?: Variables
|
||||
): {
|
||||
unsubscribe: () => void;
|
||||
reload: (forceReset?: boolean) => void;
|
||||
@ -166,7 +170,13 @@ interface DataProviderParams<QueryData, Data, SubscriptionData, Delta> {
|
||||
* @param fetchPolicy
|
||||
* @returns subscribe function
|
||||
*/
|
||||
function makeDataProviderInternal<QueryData, Data, SubscriptionData, Delta>({
|
||||
function makeDataProviderInternal<
|
||||
QueryData,
|
||||
Data,
|
||||
SubscriptionData,
|
||||
Delta,
|
||||
Variables extends OperationVariables = OperationVariables
|
||||
>({
|
||||
query,
|
||||
subscriptionQuery,
|
||||
update,
|
||||
@ -177,7 +187,8 @@ function makeDataProviderInternal<QueryData, Data, SubscriptionData, Delta>({
|
||||
resetDelay,
|
||||
}: DataProviderParams<QueryData, Data, SubscriptionData, Delta>): Subscribe<
|
||||
Data,
|
||||
Delta
|
||||
Delta,
|
||||
Variables
|
||||
> {
|
||||
// list of callbacks passed through subscribe call
|
||||
const callbacks: UpdateCallback<Data, Delta>[] = [];
|
||||
@ -439,14 +450,18 @@ function makeDataProviderInternal<QueryData, Data, SubscriptionData, Delta>({
|
||||
* @param fn
|
||||
* @returns subscibe function
|
||||
*/
|
||||
const memoize = <Data, Delta>(
|
||||
fn: (variables?: OperationVariables) => Subscribe<Data, Delta>
|
||||
const memoize = <
|
||||
Data,
|
||||
Delta,
|
||||
Variables extends OperationVariables = OperationVariables
|
||||
>(
|
||||
fn: (variables?: Variables) => Subscribe<Data, Delta, Variables>
|
||||
) => {
|
||||
const cache: {
|
||||
subscribe: Subscribe<Data, Delta>;
|
||||
variables?: OperationVariables;
|
||||
subscribe: Subscribe<Data, Delta, Variables>;
|
||||
variables?: Variables;
|
||||
}[] = [];
|
||||
return (variables?: OperationVariables) => {
|
||||
return (variables?: Variables) => {
|
||||
const cached = cache.find((c) => isEqual(c.variables, variables));
|
||||
if (cached) {
|
||||
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>
|
||||
): Subscribe<Data, Delta> {
|
||||
): Subscribe<Data, Delta, Variables> {
|
||||
const getInstance = memoize<Data, Delta>(() =>
|
||||
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 DependencyUpdateCallback = Parameters<DependencySubscribe>['0'];
|
||||
export type DerivedPart = Parameters<DependencyUpdateCallback>['0'];
|
||||
export type CombineDerivedData<Data> = (
|
||||
data: DerivedPart['data'][],
|
||||
variables?: OperationVariables
|
||||
) => Data | null;
|
||||
export type CombineDerivedData<
|
||||
Data,
|
||||
Variables extends OperationVariables = OperationVariables
|
||||
> = (data: DerivedPart['data'][], variables?: Variables) => Data | null;
|
||||
|
||||
export type CombineDerivedDelta<Data, Delta> = (
|
||||
export type CombineDerivedDelta<
|
||||
Data,
|
||||
Delta,
|
||||
Variables extends OperationVariables = OperationVariables
|
||||
> = (
|
||||
data: Data,
|
||||
parts: DerivedPart[],
|
||||
variables?: OperationVariables
|
||||
previousData: Data | null,
|
||||
variables?: Variables
|
||||
) => Delta | undefined;
|
||||
|
||||
export type CombineInsertionData<Data> = (
|
||||
export type CombineInsertionData<
|
||||
Data,
|
||||
Variables extends OperationVariables = OperationVariables
|
||||
> = (
|
||||
data: Data,
|
||||
parts: DerivedPart[],
|
||||
variables?: OperationVariables
|
||||
variables?: Variables
|
||||
) => Data | undefined;
|
||||
|
||||
function makeDerivedDataProviderInternal<Data, Delta>(
|
||||
function makeDerivedDataProviderInternal<
|
||||
Data,
|
||||
Delta,
|
||||
Variables extends OperationVariables = OperationVariables
|
||||
>(
|
||||
dependencies: DependencySubscribe[],
|
||||
combineData: CombineDerivedData<Data>,
|
||||
combineDelta?: CombineDerivedDelta<Data, Delta>,
|
||||
combineInsertionData?: CombineInsertionData<Data>
|
||||
): Subscribe<Data, Delta> {
|
||||
combineData: CombineDerivedData<Data, Variables>,
|
||||
combineDelta?: CombineDerivedDelta<Data, Delta, Variables>,
|
||||
combineInsertionData?: CombineInsertionData<Data, Variables>
|
||||
): Subscribe<Data, Delta, Variables> {
|
||||
let subscriptions: ReturnType<DependencySubscribe>[] | undefined;
|
||||
let client: ApolloClient<object>;
|
||||
const callbacks: UpdateCallback<Data, Delta>[] = [];
|
||||
let variables: OperationVariables | undefined;
|
||||
let variables: Variables | undefined;
|
||||
const parts: DerivedPart[] = [];
|
||||
let data: Data | null = null;
|
||||
let error: Error | undefined;
|
||||
@ -581,13 +614,14 @@ function makeDerivedDataProviderInternal<Data, Delta>(
|
||||
loading = newLoading;
|
||||
error = newError;
|
||||
loaded = newLoaded;
|
||||
const previousData = data;
|
||||
data = newData;
|
||||
if (newLoaded) {
|
||||
const updatedPart = parts[updatedPartIndex];
|
||||
if (updatedPart.isUpdate) {
|
||||
isUpdate = true;
|
||||
if (updatedPart.delta && combineDelta && data) {
|
||||
delta = combineDelta(data, parts, variables);
|
||||
delta = combineDelta(data, parts, previousData, variables);
|
||||
}
|
||||
delete updatedPart.isUpdate;
|
||||
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[],
|
||||
combineData: CombineDerivedData<Data>,
|
||||
combineDelta?: CombineDerivedDelta<Data, Delta>,
|
||||
combineInsertionData?: CombineInsertionData<Data>
|
||||
): Subscribe<Data, Delta> {
|
||||
const getInstance = memoize<Data, Delta>(() =>
|
||||
makeDerivedDataProviderInternal(
|
||||
combineData: CombineDerivedData<Data, Variables>,
|
||||
combineDelta?: CombineDerivedDelta<Data, Delta, Variables>,
|
||||
combineInsertionData?: CombineInsertionData<Data, Variables>
|
||||
): Subscribe<Data, Delta, Variables> {
|
||||
const getInstance = memoize<Data, Delta, Variables>(() =>
|
||||
makeDerivedDataProviderInternal<Data, Delta, Variables>(
|
||||
dependencies,
|
||||
combineData,
|
||||
combineDelta,
|
||||
|
@ -2,6 +2,7 @@ import type { Get } from 'type-fest';
|
||||
import type {
|
||||
ICellRendererParams,
|
||||
ValueFormatterParams,
|
||||
ValueGetterParams,
|
||||
} 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
|
||||
>;
|
||||
|
||||
export type VegaValueGetterParams<TRow, TField extends Field> = RowHelper<
|
||||
ValueGetterParams,
|
||||
TRow,
|
||||
TField
|
||||
>;
|
||||
|
||||
export type VegaICellRendererParams<
|
||||
TRow,
|
||||
TField extends Field = string
|
||||
|
Loading…
Reference in New Issue
Block a user