fix(accounts): avoid last row re-rendering (#3950)

This commit is contained in:
Bartłomiej Głownia 2023-06-05 12:40:34 +02:00 committed by GitHub
parent 18c034b910
commit d6e2432955
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 379 additions and 269 deletions

View File

@ -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<AccountFieldsFragment, 'market' | 'asset'> & {
market?: Market | null;
export type Account = Omit<AccountFieldsFragment, 'asset' | 'market'> & {
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<string, Asset>)[account.asset.id];
const market =
account.market?.id &&
(markets as Record<string, Asset>)[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
);

View File

@ -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 (
<div
className="h-[35vh] w-full m-auto flex flex-col"
data-testid="usage-breakdown"
>
<h1 className="text-xl mb-4">
{data?.asset?.symbol} {t('usage breakdown')}
</h1>
{data && (
<p className="mb-2 text-sm">
{t('You have %s %s in total.', [
addDecimalsFormatNumber(data.total, data.asset.decimals),
data.asset.symbol,
])}
</p>
)}
<BreakdownTable data={data?.breakdown || null} domLayout="autoHeight" />
</div>
);
};
interface AccountManagerProps {
partyId: string;
@ -30,16 +71,59 @@ export const AccountManager = ({
storeKey,
}: AccountManagerProps) => {
const gridRef = useRef<AgGridReact | null>(null);
const variables = useMemo(() => ({ partyId }), [partyId]);
const [hasData, setHasData] = useState(Boolean(pinnedAsset));
const [breakdownAssetId, setBreakdownAssetId] = useState<string>();
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 (
<div className="relative h-full">
<AccountTable
@ -48,6 +132,8 @@ export const AccountManager = ({
onClickAsset={onClickAsset}
onClickDeposit={onClickDeposit}
onClickWithdraw={onClickWithdraw}
onClickBreakdown={setBreakdownAssetId}
onRowDataUpdated={onRowDataUpdated}
isReadOnly={isReadOnly}
suppressLoadingOverlay
suppressNoRowsOverlay
@ -58,13 +144,26 @@ export const AccountManager = ({
<div className="pointer-events-none absolute inset-0">
<AsyncRenderer
data={data}
noDataCondition={(data) => !(data && data.length)}
noDataCondition={() => !hasData}
error={error}
loading={loading}
noDataMessage={pinnedAsset ? ' ' : t('No accounts')}
reload={reload}
/>
</div>
<Dialog
size="medium"
open={Boolean(breakdownAssetId)}
onChange={(isOpen) => {
if (!isOpen) {
setBreakdownAssetId(undefined);
}
}}
>
{breakdownAssetId && (
<AccountBreakdown assetId={breakdownAssetId} partyId={partyId} />
)}
</Dialog>
</div>
);
};

View File

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

View File

@ -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,23 +93,32 @@ 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<AgGridReact, AccountTableProps>(
({ onClickAsset, onClickWithdraw, onClickDeposit, ...props }, ref) => {
const [openBreakdown, setOpenBreakdown] = useState(false);
const [row, setRow] = useState<AccountFields>();
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',
@ -121,9 +127,8 @@ export const AccountTable = forwardRef<AgGridReact, AccountTableProps>(
balance: '0',
};
}
}
return currentPinnedAssetRow;
}, [pinnedAssetId, props.pinnedAsset, props.rowData]);
}, [props.pinnedAsset, rowData]);
const { getRowHeight } = props;
@ -131,25 +136,19 @@ export const AccountTable = forwardRef<AgGridReact, AccountTableProps>(
(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 (
<>
<AgGrid
{...props}
style={{ width: '100%', height: '100%' }}
@ -161,8 +160,8 @@ export const AccountTable = forwardRef<AgGridReact, AccountTableProps>(
}) => (data.isLastPlaceholder && data.id ? data.id : data.asset.id)}
ref={ref}
tooltipShowDelay={500}
rowData={props.rowData?.filter(
(data) => data.asset.id !== pinnedAssetId
rowData={rowData?.filter(
(data) => data.asset.id !== props.pinnedAsset?.id
)}
defaultColDef={{
resizable: true,
@ -217,8 +216,7 @@ export const AccountTable = forwardRef<AgGridReact, AccountTableProps>(
<ButtonLink
data-testid="breakdown"
onClick={() => {
setOpenBreakdown(!openBreakdown);
setRow(data);
onClickBreakdown && onClickBreakdown(data.asset.id);
}}
>
<span>{valueFormatted}</span>
@ -277,15 +275,16 @@ export const AccountTable = forwardRef<AgGridReact, AccountTableProps>(
/>
<AgGridColumn
colId="accounts-actions"
field="asset.id"
{...COL_DEFS.actions}
minWidth={showDepositButton ? 130 : COL_DEFS.actions.minWidth}
maxWidth={showDepositButton ? 130 : COL_DEFS.actions.maxWidth}
cellRenderer={({
data,
}: VegaICellRendererParams<AccountFields>) => {
if (!data) return null;
else {
if (showDepositButton && data.asset.id === pinnedAssetId) {
value: assetId,
node,
}: VegaICellRendererParams<AccountFields, 'asset.id'>) => {
if (!assetId) return null;
if (node.rowPinned && node.data?.balance === '0') {
return (
<CenteredGridCellWrapper className="h-[30px] justify-end py-1">
<Button
@ -293,7 +292,7 @@ export const AccountTable = forwardRef<AgGridReact, AccountTableProps>(
variant="primary"
data-testid="deposit"
onClick={() => {
onClickDeposit && onClickDeposit(data.asset.id);
onClickDeposit && onClickDeposit(assetId);
}}
>
<VegaIcon name={VegaIconNames.DEPOSIT} /> {t('Deposit')}
@ -301,55 +300,28 @@ export const AccountTable = forwardRef<AgGridReact, AccountTableProps>(
</CenteredGridCellWrapper>
);
}
return (
!props.isReadOnly && (
return props.isReadOnly ? null : (
<AccountsActionsDropdown
assetId={data.asset.id}
assetId={assetId}
assetContractAddress={
data.asset.source?.__typename === 'ERC20'
? data.asset.source.contractAddress
node.data?.asset.source?.__typename === 'ERC20'
? node.data.asset.source.contractAddress
: undefined
}
onClickDeposit={() => {
onClickDeposit && onClickDeposit(data.asset.id);
onClickDeposit && onClickDeposit(assetId);
}}
onClickWithdraw={() => {
onClickWithdraw && onClickWithdraw(data.asset.id);
onClickWithdraw && onClickWithdraw(assetId);
}}
onClickBreakdown={() => {
setOpenBreakdown(!openBreakdown);
setRow(data);
onClickBreakdown && onClickBreakdown(assetId);
}}
/>
)
);
}
}}
/>
</AgGrid>
<Dialog size="medium" open={openBreakdown} onChange={setOpenBreakdown}>
<div
className="h-[35vh] w-full m-auto flex flex-col"
data-testid="usage-breakdown"
>
<h1 className="text-xl mb-4">
{row?.asset?.symbol} {t('usage breakdown')}
</h1>
{row && (
<p className="mb-2 text-sm">
{t('You have %s %s in total.', [
addDecimalsFormatNumber(row.total, row.asset.decimals),
row.asset.symbol,
])}
</p>
)}
<BreakdownTable
data={row?.breakdown || null}
domLayout="autoHeight"
/>
</div>
</Dialog>
</>
);
}
);

View File

@ -31,6 +31,29 @@ export const assetsProvider = makeDataProvider<
getData,
});
export const assetsMapProvider = makeDerivedDataProvider<
Record<string, Asset>,
never,
undefined
>(
[(callback, client) => assetsProvider(callback, client, undefined)],
([assets]) => {
return ((assets as ReturnType<typeof getData>) || []).reduce(
(assets, asset) => {
assets[asset.id] = asset;
return assets;
},
{} as Record<string, Asset>
);
}
);
export const useAssetsMapProvider = () =>
useDataProvider({
dataProvider: assetsMapProvider,
variables: undefined,
});
export const enabledAssetsProvider = makeDerivedDataProvider<
ReturnType<typeof getData>,
never

View File

@ -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<TObj, TRow, TField extends Field> = Omit<
TObj,
'data' | 'value'
'data' | 'value' | 'node'
> & {
data?: TRow;
value?: Get<TRow, TField>;
node: (Omit<RowNode, 'data'> & { data?: TRow }) | null;
};
export type VegaValueFormatterParams<TRow, TField extends Field> = RowHelper<
@ -29,10 +30,10 @@ export type VegaValueGetterParams<TRow, TField extends Field> = RowHelper<
TField
>;
export type VegaICellRendererParams<
TRow,
TField extends Field = string
> = RowHelper<ICellRendererParams, TRow, TField>;
export type VegaICellRendererParams<TRow, TField extends Field = string> = Omit<
RowHelper<ICellRendererParams, TRow, TField>,
'node'
> & { node: NonNullable<RowHelper<ICellRendererParams, TRow, TField>['node']> };
export interface GetRowsParams<T> extends IGetRowsParams {
successCallback(rowsThisBlock: T[], lastRow?: number): void;

View File

@ -41,6 +41,29 @@ export const marketsProvider = makeDataProvider<
fetchPolicy: 'cache-first',
});
export const marketsMapProvider = makeDerivedDataProvider<
Record<string, Market>,
never,
undefined
>(
[(callback, client) => marketsProvider(callback, client, undefined)],
([markets]) => {
return ((markets as ReturnType<typeof getData>) || []).reduce(
(markets, market) => {
markets[market.id] = market;
return markets;
},
{} as Record<string, Market>
);
}
);
export const useMarketsMapProvider = () =>
useDataProvider({
dataProvider: marketsMapProvider,
variables: undefined,
});
export const marketProvider = makeDerivedDataProvider<
Market,
never,

View File

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