From d6e24329551e281eeccfa8ed1224129d105c49ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20G=C5=82ownia?= Date: Mon, 5 Jun 2023 12:40:34 +0200 Subject: [PATCH] fix(accounts): avoid last row re-rendering (#3950) --- .../src/lib/accounts-data-provider.ts | 50 ++- libs/accounts/src/lib/accounts-manager.tsx | 109 ++++- libs/accounts/src/lib/accounts-table.spec.tsx | 14 - libs/accounts/src/lib/accounts-table.tsx | 414 ++++++++---------- libs/assets/src/lib/assets-data-provider.ts | 23 + libs/datagrid/src/lib/type-helpers.ts | 13 +- libs/markets/src/lib/markets-provider.ts | 23 + .../src/lib/withdraw-form-container.spec.tsx | 2 +- 8 files changed, 379 insertions(+), 269 deletions(-) diff --git a/libs/accounts/src/lib/accounts-data-provider.ts b/libs/accounts/src/lib/accounts-data-provider.ts index c5bf61a64..3b6d1068e 100644 --- a/libs/accounts/src/lib/accounts-data-provider.ts +++ b/libs/accounts/src/lib/accounts-data-provider.ts @@ -1,11 +1,12 @@ -import { assetsProvider } from '@vegaprotocol/assets'; -import { marketsProvider } from '@vegaprotocol/markets'; +import { assetsMapProvider } from '@vegaprotocol/assets'; import { removePaginationWrapper } from '@vegaprotocol/utils'; +import { marketsMapProvider } from '@vegaprotocol/markets'; import { makeDataProvider, makeDerivedDataProvider, } from '@vegaprotocol/data-provider'; import * as Schema from '@vegaprotocol/types'; +import type { Market } from '@vegaprotocol/markets'; import produce from 'immer'; import { @@ -20,7 +21,6 @@ import type { AccountEventsSubscription, AccountsQueryVariables, } from './__generated__/Accounts'; -import type { Market } from '@vegaprotocol/markets'; import type { Asset } from '@vegaprotocol/assets'; const AccountType = Schema.AccountType; @@ -45,9 +45,9 @@ export const getId = ( ? `${account.type}-${account.asset.id}-${account.market?.id || 'null'}` : `${account.type}-${account.assetId}-${account.marketId || 'null'}`; -export type Account = Omit & { - market?: Market | null; +export type Account = Omit & { asset: Asset; + market?: Market | null; }; const update = ( @@ -170,31 +170,22 @@ export const accountsDataProvider = makeDerivedDataProvider< >( [ accountsOnlyDataProvider, - (callback, client) => marketsProvider(callback, client, undefined), - (callback, client) => assetsProvider(callback, client, undefined), + (callback, client) => marketsMapProvider(callback, client, undefined), + (callback, client) => assetsMapProvider(callback, client, undefined), ], ([accounts, markets, assets]): Account[] | null => { return accounts ? accounts .map((account: AccountFieldsFragment) => { - const market = markets.find( - (market: Market) => market.id === account.market?.id - ); - const asset = assets.find( - (asset: Asset) => asset.id === account.asset?.id - ); + const asset = (assets as Record)[account.asset.id]; + const market = + account.market?.id && + (markets as Record)[account.market?.id]; if (asset) { return { ...account, - partyId: account.party?.id, - asset: { - ...asset, - }, - market: market - ? { - ...market, - } - : null, + asset, + market, }; } return null; @@ -212,3 +203,18 @@ export const aggregatedAccountsDataProvider = makeDerivedDataProvider< [accountsDataProvider], (parts) => parts[0] && getAccountData(parts[0] as Account[]) ); + +export const aggregatedAccountDataProvider = makeDerivedDataProvider< + AccountFields, + never, + AccountsQueryVariables & { assetId: string } +>( + [ + (callback, client, { partyId }) => + aggregatedAccountsDataProvider(callback, client, { partyId }), + ], + (parts, { assetId }) => + (parts[0] as AccountFields[]).find( + (account) => account.asset.id === assetId + ) || null +); diff --git a/libs/accounts/src/lib/accounts-manager.tsx b/libs/accounts/src/lib/accounts-manager.tsx index f95c54f80..f8cf912df 100644 --- a/libs/accounts/src/lib/accounts-manager.tsx +++ b/libs/accounts/src/lib/accounts-manager.tsx @@ -1,12 +1,53 @@ -import { useRef, useMemo, memo } from 'react'; +import { useRef, memo, useCallback, useState, useEffect } from 'react'; +import { addDecimalsFormatNumber } from '@vegaprotocol/utils'; import { t } from '@vegaprotocol/i18n'; import { useBottomPlaceholder } from '@vegaprotocol/datagrid'; import { useDataProvider } from '@vegaprotocol/data-provider'; import { AsyncRenderer } from '@vegaprotocol/ui-toolkit'; import type { AgGridReact } from 'ag-grid-react'; -import { aggregatedAccountsDataProvider } from './accounts-data-provider'; +import type { RowDataUpdatedEvent } from 'ag-grid-community'; +import type { AccountFields } from './accounts-data-provider'; +import { + aggregatedAccountsDataProvider, + aggregatedAccountDataProvider, +} from './accounts-data-provider'; import type { PinnedAsset } from './accounts-table'; import { AccountTable } from './accounts-table'; +import isEqual from 'lodash/isEqual'; +import { Dialog } from '@vegaprotocol/ui-toolkit'; +import BreakdownTable from './breakdown-table'; + +const AccountBreakdown = ({ + assetId, + partyId, +}: { + assetId: string; + partyId: string; +}) => { + const { data } = useDataProvider({ + dataProvider: aggregatedAccountDataProvider, + variables: { partyId, assetId }, + }); + return ( +
+

+ {data?.asset?.symbol} {t('usage breakdown')} +

+ {data && ( +

+ {t('You have %s %s in total.', [ + addDecimalsFormatNumber(data.total, data.asset.decimals), + data.asset.symbol, + ])} +

+ )} + +
+ ); +}; interface AccountManagerProps { partyId: string; @@ -30,16 +71,59 @@ export const AccountManager = ({ storeKey, }: AccountManagerProps) => { const gridRef = useRef(null); - const variables = useMemo(() => ({ partyId }), [partyId]); + const [hasData, setHasData] = useState(Boolean(pinnedAsset)); + const [breakdownAssetId, setBreakdownAssetId] = useState(); + const update = useCallback( + ({ data }: { data: AccountFields[] | null }) => { + if (!data || !gridRef.current?.api) { + return false; + } + const pinnedAssetRowData = + pinnedAsset && data.find((d) => d.asset.id === pinnedAsset.id); + + if (pinnedAssetRowData) { + const pinnedTopRow = gridRef.current.api.getPinnedTopRow(0); + if ( + pinnedTopRow?.data?.balance === '0' && + pinnedAssetRowData.balance !== '0' + ) { + return false; + } + if (!isEqual(pinnedTopRow?.data, pinnedAssetRowData)) { + gridRef.current.api.setPinnedTopRowData([pinnedAssetRowData]); + } + } + gridRef.current.api.setRowData( + pinnedAssetRowData + ? data?.filter((d) => d !== pinnedAssetRowData) + : data + ); + return true; + }, + [gridRef, pinnedAsset] + ); const { data, loading, error, reload } = useDataProvider({ dataProvider: aggregatedAccountsDataProvider, - variables, + variables: { partyId }, + update, }); const bottomPlaceholderProps = useBottomPlaceholder({ gridRef, disabled: noBottomPlaceholder, }); + useEffect( + () => setHasData(Boolean(pinnedAsset || data?.length)), + [data, pinnedAsset] + ); + + const onRowDataUpdated = useCallback( + (event: RowDataUpdatedEvent) => { + setHasData(Boolean(pinnedAsset || event.api?.getModel().getRowCount())); + }, + [pinnedAsset] + ); + return (
!(data && data.length)} + noDataCondition={() => !hasData} error={error} loading={loading} noDataMessage={pinnedAsset ? ' ' : t('No accounts')} reload={reload} />
+ { + if (!isOpen) { + setBreakdownAssetId(undefined); + } + }} + > + {breakdownAssetId && ( + + )} + ); }; diff --git a/libs/accounts/src/lib/accounts-table.spec.tsx b/libs/accounts/src/lib/accounts-table.spec.tsx index cf3538a1b..00fcc457c 100644 --- a/libs/accounts/src/lib/accounts-table.spec.tsx +++ b/libs/accounts/src/lib/accounts-table.spec.tsx @@ -10,13 +10,6 @@ const singleRow = { balance: '125600000', market: { __typename: 'Market', - tradableInstrument: { - __typename: 'TradableInstrument', - instrument: { - __typename: 'Instrument', - name: 'BTCUSD Monthly (30 Jun 2022)', - }, - }, id: '10cd0a793ad2887b340940337fa6d97a212e0e517fe8e9eab2b5ef3a38633f35', }, asset: { @@ -136,13 +129,6 @@ describe('AccountsTable', () => { market: { __typename: 'Market', id: '10cd0a793ad2887b340940337fa6d97a212e0e517fe8e9eab2b5ef3a38633f35', - tradableInstrument: { - __typename: 'TradableInstrument', - instrument: { - __typename: 'Instrument', - name: 'BTCUSD Monthly (30 Jun 2022)', - }, - }, }, type: 'ACCOUNT_TYPE_MARGIN', used: '125600000', diff --git a/libs/accounts/src/lib/accounts-table.tsx b/libs/accounts/src/lib/accounts-table.tsx index d596885e9..b62ba37c8 100644 --- a/libs/accounts/src/lib/accounts-table.tsx +++ b/libs/accounts/src/lib/accounts-table.tsx @@ -1,4 +1,4 @@ -import { forwardRef, useCallback, useMemo, useState } from 'react'; +import { forwardRef, useMemo, useCallback } from 'react'; import { addDecimalsFormatNumber, isNumeric, @@ -11,28 +11,25 @@ import type { } from '@vegaprotocol/datagrid'; import { COL_DEFS } from '@vegaprotocol/datagrid'; import { - Button, ButtonLink, - Dialog, + Button, VegaIcon, VegaIconNames, } from '@vegaprotocol/ui-toolkit'; + import { TooltipCellComponent } from '@vegaprotocol/ui-toolkit'; -import { - AgGridLazy as AgGrid, - CenteredGridCellWrapper, -} from '@vegaprotocol/datagrid'; +import { AgGridLazy as AgGrid } from '@vegaprotocol/datagrid'; import { AgGridColumn } from 'ag-grid-react'; import type { IDatasource, IGetRowsParams, - RowHeightParams, RowNode, + RowHeightParams, } from 'ag-grid-community'; import type { AgGridReact, AgGridReactProps } from 'ag-grid-react'; -import BreakdownTable from './breakdown-table'; import type { AccountFields } from './accounts-data-provider'; import type { Asset } from '@vegaprotocol/types'; +import { CenteredGridCellWrapper } from '@vegaprotocol/datagrid'; import BigNumber from 'bignumber.js'; import classNames from 'classnames'; import { AccountsActionsDropdown } from './accounts-actions-dropdown'; @@ -96,34 +93,42 @@ export interface AccountTableProps extends AgGridReactProps { onClickAsset: (assetId: string) => void; onClickWithdraw?: (assetId: string) => void; onClickDeposit?: (assetId: string) => void; + onClickBreakdown?: (assetId: string) => void; isReadOnly: boolean; pinnedAsset?: PinnedAsset; storeKey?: string; } export const AccountTable = forwardRef( - ({ onClickAsset, onClickWithdraw, onClickDeposit, ...props }, ref) => { - const [openBreakdown, setOpenBreakdown] = useState(false); - const [row, setRow] = useState(); - const pinnedAssetId = props.pinnedAsset?.id; - + ( + { + onClickAsset, + onClickWithdraw, + onClickDeposit, + onClickBreakdown, + rowData, + ...props + }, + ref + ) => { const pinnedAsset = useMemo(() => { - const currentPinnedAssetRow = props.rowData?.find( - (row) => row.asset.id === pinnedAssetId + if (!props.pinnedAsset) { + return; + } + const currentPinnedAssetRow = rowData?.find( + (row) => row.asset.id === props.pinnedAsset?.id ); if (!currentPinnedAssetRow) { - if (props.pinnedAsset) { - return { - asset: props.pinnedAsset, - available: '0', - used: '0', - total: '0', - balance: '0', - }; - } + return { + asset: props.pinnedAsset, + available: '0', + used: '0', + total: '0', + balance: '0', + }; } return currentPinnedAssetRow; - }, [pinnedAssetId, props.pinnedAsset, props.rowData]); + }, [props.pinnedAsset, rowData]); const { getRowHeight } = props; @@ -131,225 +136,192 @@ export const AccountTable = forwardRef( (params: RowHeightParams) => { if ( params.node.rowPinned && - params.data.asset.id === pinnedAssetId && + params.data.asset.id === props.pinnedAsset?.id && new BigNumber(params.data.total).isLessThanOrEqualTo(0) ) { return 32; } return getRowHeight ? getRowHeight(params) : undefined; }, - [pinnedAssetId, getRowHeight] + [props.pinnedAsset?.id, getRowHeight] ); - const accountForPinnedAsset = props?.rowData?.find( - (a) => a.asset.id === pinnedAssetId - ); - const showDepositButton = accountForPinnedAsset - ? new BigNumber(accountForPinnedAsset.total).isLessThanOrEqualTo(0) - : true; + const showDepositButton = pinnedAsset?.balance === '0'; return ( - <> - (data.isLastPlaceholder && data.id ? data.id : data.asset.id)} - ref={ref} - tooltipShowDelay={500} - rowData={props.rowData?.filter( - (data) => data.asset.id !== pinnedAssetId + (data.isLastPlaceholder && data.id ? data.id : data.asset.id)} + ref={ref} + tooltipShowDelay={500} + rowData={rowData?.filter( + (data) => data.asset.id !== props.pinnedAsset?.id + )} + defaultColDef={{ + resizable: true, + tooltipComponent: TooltipCellComponent, + sortable: true, + comparator: accountValuesComparator, + }} + getRowHeight={getPinnedAssetRowHeight} + pinnedTopRowData={pinnedAsset ? [pinnedAsset] : undefined} + > + ) => { + return ( + { + if (data) { + onClickAsset(data.asset.id); + } + }} + > + {value} + + ); }} - getRowHeight={getPinnedAssetRowHeight} - pinnedTopRowData={pinnedAsset ? [pinnedAsset] : undefined} - > - ) => { - return ( + /> + ) => { + if (!data) return null; + const percentageUsed = percentageValue(value, data.total); + const valueFormatted = formatWithAssetDecimals(data, value); + + return data.breakdown ? ( + <> { - if (data) { - onClickAsset(data.asset.id); - } + onClickBreakdown && onClickBreakdown(data.asset.id); }} > - {value} + {valueFormatted} - ); - }} - /> - ) => { - if (!data) return null; - const percentageUsed = percentageValue(value, data.total); - const valueFormatted = formatWithAssetDecimals(data, value); + + {percentageUsed.toFixed(2)}% + + + ) : ( + <> + {valueFormatted} + + 0.00% + + + ); + }} + /> + ) => { + const percentageUsed = percentageValue(data?.used, data?.total); - return data.breakdown ? ( - <> - + {formatWithAssetDecimals(data, value)} + + ); + }} + /> + ) => + formatWithAssetDecimals(data, data?.total) + } + /> + ) => { + if (!assetId) return null; + if (node.rowPinned && node.data?.balance === '0') { + return ( + + + ); - }} - /> - ) => { - const percentageUsed = percentageValue(data?.used, data?.total); - - return ( - - {formatWithAssetDecimals(data, value)} - - ); - }} - /> - ) => - formatWithAssetDecimals(data, data?.total) } - /> - ) => { - if (!data) return null; - else { - if (showDepositButton && data.asset.id === pinnedAssetId) { - return ( - - - - ); + return props.isReadOnly ? null : ( + { - onClickDeposit && onClickDeposit(data.asset.id); - }} - onClickWithdraw={() => { - onClickWithdraw && onClickWithdraw(data.asset.id); - }} - onClickBreakdown={() => { - setOpenBreakdown(!openBreakdown); - setRow(data); - }} - /> - ) - ); - } - }} - /> - - -
-

- {row?.asset?.symbol} {t('usage breakdown')} -

- {row && ( -

- {t('You have %s %s in total.', [ - addDecimalsFormatNumber(row.total, row.asset.decimals), - row.asset.symbol, - ])} -

- )} - -
-
- + onClickDeposit={() => { + onClickDeposit && onClickDeposit(assetId); + }} + onClickWithdraw={() => { + onClickWithdraw && onClickWithdraw(assetId); + }} + onClickBreakdown={() => { + onClickBreakdown && onClickBreakdown(assetId); + }} + /> + ); + }} + /> +
); } ); diff --git a/libs/assets/src/lib/assets-data-provider.ts b/libs/assets/src/lib/assets-data-provider.ts index 760b5b567..3a7084ca7 100644 --- a/libs/assets/src/lib/assets-data-provider.ts +++ b/libs/assets/src/lib/assets-data-provider.ts @@ -31,6 +31,29 @@ export const assetsProvider = makeDataProvider< getData, }); +export const assetsMapProvider = makeDerivedDataProvider< + Record, + never, + undefined +>( + [(callback, client) => assetsProvider(callback, client, undefined)], + ([assets]) => { + return ((assets as ReturnType) || []).reduce( + (assets, asset) => { + assets[asset.id] = asset; + return assets; + }, + {} as Record + ); + } +); + +export const useAssetsMapProvider = () => + useDataProvider({ + dataProvider: assetsMapProvider, + variables: undefined, + }); + export const enabledAssetsProvider = makeDerivedDataProvider< ReturnType, never diff --git a/libs/datagrid/src/lib/type-helpers.ts b/libs/datagrid/src/lib/type-helpers.ts index f4becc173..494c9b81c 100644 --- a/libs/datagrid/src/lib/type-helpers.ts +++ b/libs/datagrid/src/lib/type-helpers.ts @@ -4,17 +4,18 @@ import type { ValueFormatterParams, ValueGetterParams, } from 'ag-grid-community'; -import type { IDatasource, IGetRowsParams } from 'ag-grid-community'; +import type { IDatasource, IGetRowsParams, RowNode } from 'ag-grid-community'; import type { AgGridReactProps } from 'ag-grid-react'; type Field = string | readonly string[]; type RowHelper = Omit< TObj, - 'data' | 'value' + 'data' | 'value' | 'node' > & { data?: TRow; value?: Get; + node: (Omit & { data?: TRow }) | null; }; export type VegaValueFormatterParams = RowHelper< @@ -29,10 +30,10 @@ export type VegaValueGetterParams = RowHelper< TField >; -export type VegaICellRendererParams< - TRow, - TField extends Field = string -> = RowHelper; +export type VegaICellRendererParams = Omit< + RowHelper, + 'node' +> & { node: NonNullable['node']> }; export interface GetRowsParams extends IGetRowsParams { successCallback(rowsThisBlock: T[], lastRow?: number): void; diff --git a/libs/markets/src/lib/markets-provider.ts b/libs/markets/src/lib/markets-provider.ts index 529907346..b1cb11235 100644 --- a/libs/markets/src/lib/markets-provider.ts +++ b/libs/markets/src/lib/markets-provider.ts @@ -41,6 +41,29 @@ export const marketsProvider = makeDataProvider< fetchPolicy: 'cache-first', }); +export const marketsMapProvider = makeDerivedDataProvider< + Record, + never, + undefined +>( + [(callback, client) => marketsProvider(callback, client, undefined)], + ([markets]) => { + return ((markets as ReturnType) || []).reduce( + (markets, market) => { + markets[market.id] = market; + return markets; + }, + {} as Record + ); + } +); + +export const useMarketsMapProvider = () => + useDataProvider({ + dataProvider: marketsMapProvider, + variables: undefined, + }); + export const marketProvider = makeDerivedDataProvider< Market, never, diff --git a/libs/withdraws/src/lib/withdraw-form-container.spec.tsx b/libs/withdraws/src/lib/withdraw-form-container.spec.tsx index 3ada398e7..47f910080 100644 --- a/libs/withdraws/src/lib/withdraw-form-container.spec.tsx +++ b/libs/withdraws/src/lib/withdraw-form-container.spec.tsx @@ -27,7 +27,7 @@ describe('WithdrawFormContainer', () => { const account1: Account = { type: Types.AccountType.ACCOUNT_TYPE_GENERAL, balance: '200099689', - market: null, + market: undefined, asset: { id: 'assetId-1', name: 'tBTC TEST',