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', () => {
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);
});
});

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 { 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>
);
};

View File

@ -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)

View File

@ -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';

View File

@ -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));
})
);

View File

@ -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>

View File

@ -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];

View File

@ -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'>) => {

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 { 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,
};
};

View File

@ -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>();

View File

@ -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,

View File

@ -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