From 5edb33c3cd2b904910e37e4ff75fcd0c5b4700ac Mon Sep 17 00:00:00 2001 From: Madalina Raicu Date: Thu, 22 Sep 2022 14:22:02 +0100 Subject: [PATCH] fix: container moved, progress bar in helpers --- apps/trading/pages/markets/trade-grid.tsx | 2 +- .../pages/portfolio/accounts-container.tsx | 89 ++++++ apps/trading/pages/portfolio/index.page.tsx | 2 +- .../pages/portfolio/withdrawals-container.tsx | 1 - .../src/lib/__generated__/Accounts.ts | 111 +++++++ libs/accounts/src/lib/__generated__/index.ts | 1 + libs/accounts/src/lib/__generated___/index.ts | 1 + libs/accounts/src/lib/account-deposit.tsx | 142 --------- libs/accounts/src/lib/accounts-container.tsx | 18 -- .../src/lib/accounts-data-provider.ts | 137 ++++----- libs/accounts/src/lib/accounts-manager.tsx | 194 ------------- libs/accounts/src/lib/accounts-table.spec.tsx | 73 ----- libs/accounts/src/lib/accounts-table.tsx | 271 ++++++++++-------- .../accounts/src/lib/breakdown-table.spec.tsx | 133 +++++++++ libs/accounts/src/lib/breakdown-table.tsx | 71 +++++ libs/accounts/src/lib/index.ts | 6 +- .../order-data-provider.ts | 2 +- libs/positions/src/lib/positions-table.tsx | 77 ++--- .../src/lib/grid/grid-progress-bar.tsx | 58 ++++ libs/react-helpers/src/lib/grid/index.ts | 1 + 20 files changed, 713 insertions(+), 677 deletions(-) create mode 100644 apps/trading/pages/portfolio/accounts-container.tsx create mode 100644 libs/accounts/src/lib/__generated__/Accounts.ts create mode 100644 libs/accounts/src/lib/__generated__/index.ts create mode 100644 libs/accounts/src/lib/__generated___/index.ts delete mode 100644 libs/accounts/src/lib/account-deposit.tsx delete mode 100644 libs/accounts/src/lib/accounts-container.tsx delete mode 100644 libs/accounts/src/lib/accounts-manager.tsx delete mode 100644 libs/accounts/src/lib/accounts-table.spec.tsx create mode 100644 libs/accounts/src/lib/breakdown-table.spec.tsx create mode 100644 libs/accounts/src/lib/breakdown-table.tsx create mode 100644 libs/react-helpers/src/lib/grid/grid-progress-bar.tsx diff --git a/apps/trading/pages/markets/trade-grid.tsx b/apps/trading/pages/markets/trade-grid.tsx index 8d4aca2ce..25790ab1e 100644 --- a/apps/trading/pages/markets/trade-grid.tsx +++ b/apps/trading/pages/markets/trade-grid.tsx @@ -11,7 +11,6 @@ import AutoSizer from 'react-virtualized-auto-sizer'; import { useState } from 'react'; import type { ReactNode } from 'react'; import type { Market_market } from './__generated__/Market'; -import { AccountsContainer } from '@vegaprotocol/accounts'; import { DepthChartContainer } from '@vegaprotocol/market-depth'; import { CandlesChartContainer } from '@vegaprotocol/candles-chart'; import { @@ -41,6 +40,7 @@ import { import { TradingModeTooltip } from '../../components/trading-mode-tooltip'; import { useRouter } from 'next/router'; import { Header, HeaderStat } from '../../components/header'; +import { AccountsContainer } from '../portfolio/accounts-container'; const TradingViews = { Candles: CandlesChartContainer, diff --git a/apps/trading/pages/portfolio/accounts-container.tsx b/apps/trading/pages/portfolio/accounts-container.tsx new file mode 100644 index 000000000..df3bf6f2b --- /dev/null +++ b/apps/trading/pages/portfolio/accounts-container.tsx @@ -0,0 +1,89 @@ +import { useState } from 'react'; +import { Button, Dialog } from '@vegaprotocol/ui-toolkit'; +import { t } from '@vegaprotocol/react-helpers'; +import { WithdrawalDialogs } from '@vegaprotocol/withdraws'; +import { Web3Container } from '@vegaprotocol/web3'; +import { DepositContainer } from '@vegaprotocol/deposits'; +import { useAssetDetailsDialogStore } from '@vegaprotocol/assets'; +import { Splash } from '@vegaprotocol/ui-toolkit'; +import { useVegaWallet } from '@vegaprotocol/wallet'; +import { AccountsTable } from '@vegaprotocol/accounts'; + +export const AccountsContainer = () => { + const { keypair } = useVegaWallet(); + const [depositDialog, setDepositDialog] = useState(false); + + if (!keypair) { + return ( + +

{t('Please connect Vega wallet')}

+
+ ); + } + + return ( + +
+ +
+ +
+ +
+
+ ); +}; + +export const AssetAccountTable = ({ partyId }: { partyId: string }) => { + const [withdrawDialog, setWithdrawDialog] = useState(false); + const [depositDialog, setDepositDialog] = useState(false); + const { setAssetDetailsDialogOpen, setAssetDetailsDialogSymbol } = + useAssetDetailsDialogStore(); + return ( + <> + { + if (value) { + setAssetDetailsDialogOpen(true); + setAssetDetailsDialogSymbol(value); + } + }} + onClickWithdraw={() => setWithdrawDialog(true)} + onClickDeposit={() => setDepositDialog(true)} + /> + + + + ); +}; + +export interface DepositDialogProps { + assetId?: string; + depositDialog: boolean; + setDepositDialog: (open: boolean) => void; +} + +export const DepositDialog = ({ + assetId, + depositDialog, + setDepositDialog, +}: DepositDialogProps) => { + return ( + +

{t('Deposit')}

+ +
+ ); +}; diff --git a/apps/trading/pages/portfolio/index.page.tsx b/apps/trading/pages/portfolio/index.page.tsx index 63139b4d4..477a1b036 100644 --- a/apps/trading/pages/portfolio/index.page.tsx +++ b/apps/trading/pages/portfolio/index.page.tsx @@ -1,7 +1,6 @@ import { t } from '@vegaprotocol/react-helpers'; import { PositionsContainer } from '@vegaprotocol/positions'; import { OrderListContainer } from '@vegaprotocol/orders'; -import { AccountsContainer } from '@vegaprotocol/accounts'; import { ResizableGridPanel, Tab, Tabs } from '@vegaprotocol/ui-toolkit'; import { WithdrawalsContainer } from './withdrawals-container'; import { FillsContainer } from '@vegaprotocol/fills'; @@ -10,6 +9,7 @@ import { VegaWalletContainer } from '../../components/vega-wallet-container'; import { DepositsContainer } from './deposits-container'; import { ResizableGrid } from '@vegaprotocol/ui-toolkit'; import { LayoutPriority } from 'allotment'; +import { AccountsContainer } from './accounts-container'; const Portfolio = () => { const wrapperClasses = 'h-full max-h-full flex flex-col'; diff --git a/apps/trading/pages/portfolio/withdrawals-container.tsx b/apps/trading/pages/portfolio/withdrawals-container.tsx index 61d4a16f0..6299a6677 100644 --- a/apps/trading/pages/portfolio/withdrawals-container.tsx +++ b/apps/trading/pages/portfolio/withdrawals-container.tsx @@ -13,7 +13,6 @@ export const WithdrawalsContainer = () => { const { withdrawals, loading, error } = useWithdrawals(); const [withdrawDialog, setWithdrawDialog] = useState(false); - console.log('render'); return ( diff --git a/libs/accounts/src/lib/__generated__/Accounts.ts b/libs/accounts/src/lib/__generated__/Accounts.ts new file mode 100644 index 000000000..6448e214b --- /dev/null +++ b/libs/accounts/src/lib/__generated__/Accounts.ts @@ -0,0 +1,111 @@ +import { Schema as Types } from '@vegaprotocol/types'; + +import { gql } from '@apollo/client'; +import * as Apollo from '@apollo/client'; +const defaultOptions = {} as const; +export type AccountFieldsFragment = { __typename?: 'Account', type: Types.AccountType, balance: string, market?: { __typename?: 'Market', id: string, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', name: string } } } | null, asset: { __typename?: 'Asset', id: string, symbol: string, decimals: number } }; + +export type AccountsQueryVariables = Types.Exact<{ + partyId: Types.Scalars['ID']; +}>; + + +export type AccountsQuery = { __typename?: 'Query', party?: { __typename?: 'Party', id: string, accounts?: Array<{ __typename?: 'Account', type: Types.AccountType, balance: string, market?: { __typename?: 'Market', id: string, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', name: string } } } | null, asset: { __typename?: 'Asset', id: string, symbol: string, decimals: number } }> | null } | null }; + +export type AccountEventsSubscriptionVariables = Types.Exact<{ + partyId: Types.Scalars['ID']; +}>; + + +export type AccountEventsSubscription = { __typename?: 'Subscription', accounts: Array<{ __typename?: 'AccountUpdate', type: Types.AccountType, balance: string, assetId: string, marketId?: string | null }> }; + +export const AccountFieldsFragmentDoc = gql` + fragment AccountFields on Account { + type + balance + market { + id + tradableInstrument { + instrument { + name + } + } + } + asset { + id + symbol + decimals + } +} + `; +export const AccountsDocument = gql` + query Accounts($partyId: ID!) { + party(id: $partyId) { + id + accounts { + ...AccountFields + } + } +} + ${AccountFieldsFragmentDoc}`; + +/** + * __useAccountsQuery__ + * + * To run a query within a React component, call `useAccountsQuery` and pass it any options that fit your needs. + * When your component renders, `useAccountsQuery` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = useAccountsQuery({ + * variables: { + * partyId: // value for 'partyId' + * }, + * }); + */ +export function useAccountsQuery(baseOptions: Apollo.QueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useQuery(AccountsDocument, options); + } +export function useAccountsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useLazyQuery(AccountsDocument, options); + } +export type AccountsQueryHookResult = ReturnType; +export type AccountsLazyQueryHookResult = ReturnType; +export type AccountsQueryResult = Apollo.QueryResult; +export const AccountEventsDocument = gql` + subscription AccountEvents($partyId: ID!) { + accounts(partyId: $partyId) { + type + balance + assetId + marketId + } +} + `; + +/** + * __useAccountEventsSubscription__ + * + * To run a query within a React component, call `useAccountEventsSubscription` and pass it any options that fit your needs. + * When your component renders, `useAccountEventsSubscription` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the subscription, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = useAccountEventsSubscription({ + * variables: { + * partyId: // value for 'partyId' + * }, + * }); + */ +export function useAccountEventsSubscription(baseOptions: Apollo.SubscriptionHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useSubscription(AccountEventsDocument, options); + } +export type AccountEventsSubscriptionHookResult = ReturnType; +export type AccountEventsSubscriptionResult = Apollo.SubscriptionResult; diff --git a/libs/accounts/src/lib/__generated__/index.ts b/libs/accounts/src/lib/__generated__/index.ts new file mode 100644 index 000000000..99ab81bfb --- /dev/null +++ b/libs/accounts/src/lib/__generated__/index.ts @@ -0,0 +1 @@ +export * from './Accounts'; diff --git a/libs/accounts/src/lib/__generated___/index.ts b/libs/accounts/src/lib/__generated___/index.ts new file mode 100644 index 000000000..99ab81bfb --- /dev/null +++ b/libs/accounts/src/lib/__generated___/index.ts @@ -0,0 +1 @@ +export * from './Accounts'; diff --git a/libs/accounts/src/lib/account-deposit.tsx b/libs/accounts/src/lib/account-deposit.tsx deleted file mode 100644 index 35ef1f720..000000000 --- a/libs/accounts/src/lib/account-deposit.tsx +++ /dev/null @@ -1,142 +0,0 @@ -import { forwardRef } from 'react'; -import type { - GroupCellRendererParams, - ValueFormatterParams, -} from 'ag-grid-community'; -import { addDecimalsFormatNumber, t } from '@vegaprotocol/react-helpers'; -import { Button, Icon } from '@vegaprotocol/ui-toolkit'; -import { TooltipCellComponent } from '@vegaprotocol/ui-toolkit'; -import { AgGridDynamic as AgGrid } from '@vegaprotocol/ui-toolkit'; -import { AgGridColumn } from 'ag-grid-react'; -import type { AgGridReact, AgGridReactProps } from 'ag-grid-react'; -import type { AccountFieldsFragment } from './__generated__/Accounts'; -import { getId } from './accounts-data-provider'; -import type { AccountFields } from './accounts-manager'; -import { - progressBarCellRendererSelector, - progressBarHeaderComponentParams, - progressBarValueFormatter, -} from './accounts-table'; - -interface AccountsTableProps extends AgGridReactProps { - data: AccountFields[] | null; - onClickAsset: () => void; - onClickWithdraw: () => void; - onClickDeposit: () => void; - expanded: boolean; - showRows: boolean; - hideHeader?: boolean; -} - -interface AccountsTableValueFormatterParams extends ValueFormatterParams { - data: AccountFieldsFragment; -} - -export const assetDecimalsFormatter = ({ - value, - data, -}: AccountsTableValueFormatterParams) => - addDecimalsFormatNumber(value, data.asset.decimals); - -export const AccountDeposit = forwardRef( - ( - { - data, - onClickAsset, - expanded, - showRows, - hideHeader, - onClickWithdraw, - onClickDeposit, - }, - ref - ) => { - const openAssetAccountCellRenderer = ({ value }: GroupCellRendererParams) => - showRows ? ( - - ) : ( - - ); - return ( - getId(data)} - ref={ref} - rowHeight={34} - tooltipShowDelay={500} - headerHeight={hideHeader ? 0 : undefined} - defaultColDef={{ - flex: 1, - resizable: true, - tooltipComponent: TooltipCellComponent, - }} - > - - - - { - return ( - - ); - }} - /> - { - return ( - - ); - }} - /> - - ); - } -); - -export default AccountDeposit; diff --git a/libs/accounts/src/lib/accounts-container.tsx b/libs/accounts/src/lib/accounts-container.tsx deleted file mode 100644 index 8d6b27809..000000000 --- a/libs/accounts/src/lib/accounts-container.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { t } from '@vegaprotocol/react-helpers'; -import { Splash } from '@vegaprotocol/ui-toolkit'; -import { useVegaWallet } from '@vegaprotocol/wallet'; -import { AccountsManager } from './accounts-manager'; - -export const AccountsContainer = () => { - const { keypair } = useVegaWallet(); - - if (!keypair) { - return ( - -

{t('Please connect Vega wallet')}

-
- ); - } - - return ; -}; diff --git a/libs/accounts/src/lib/accounts-data-provider.ts b/libs/accounts/src/lib/accounts-data-provider.ts index d03eb33cb..ee6d3a000 100644 --- a/libs/accounts/src/lib/accounts-data-provider.ts +++ b/libs/accounts/src/lib/accounts-data-provider.ts @@ -9,11 +9,16 @@ import type { AccountsQuery, AccountEventsSubscription, } from './__generated___/Accounts'; -import type { SummaryRow } from '@vegaprotocol/react-helpers'; import { makeDataProvider } from '@vegaprotocol/react-helpers'; import { AccountType } from '@vegaprotocol/types'; -import type { ColumnApi } from 'ag-grid-community'; -import type { AccountFields } from './accounts-manager'; + +export interface AccountFields extends AccountFieldsFragment { + available: string; + used: string; + deposited: string; + balance: string; + breakdown?: AccountFields[]; +} function isAccount( account: @@ -32,49 +37,6 @@ export const getId = ( ? `${account.type}-${account.asset.id}-${account.market?.id ?? 'null'}` : `${account.type}-${account.assetId}-${account.marketId}`; -export const getGroupId = ( - data: AccountFields & SummaryRow, - columnApi: ColumnApi -) => { - if (data.__summaryRow) { - return null; - } - const sortColumnId = columnApi.getColumnState().find((c) => c.sort)?.colId; - switch (sortColumnId) { - case 'asset.symbol': - return data.asset.id; - } - return undefined; -}; - -export const getGroupSummaryRow = ( - data: AccountFields[], - columnApi: ColumnApi -): Partial | null => { - if (!data.length) { - return null; - } - // TODO to be updated for sorting or summary - return null; -}; - -const update = ( - data: AccountFieldsFragment[], - deltas: AccountEventsSubscription['accounts'] -) => { - return produce(data, (draft) => { - deltas.forEach((delta) => { - const id = getId(delta); - const index = draft.findIndex((a) => getId(a) === id); - if (index !== -1) { - draft[index].balance = delta.balance; - } else { - // #TODO handle new account - } - }); - }); -}; - const INCOMING_ACCOUNT_TYPES = [ AccountType.ACCOUNT_TYPE_GENERAL, AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES, @@ -102,6 +64,24 @@ const getDelta = ( subscriptionData: AccountEventsSubscription ): AccountEventsSubscription['accounts'] => subscriptionData.accounts; +const update = ( + data: AccountFieldsFragment[], + deltas: AccountEventsSubscription['accounts'] +) => { + return produce(data, (draft) => { + deltas.forEach((delta) => { + const id = getId(delta); + const index = draft.findIndex((a) => getId(a) === id); + if (index !== -1) { + draft[index].balance = delta.balance; + } else { + // #TODO handle new account + // draft.push(delta); + } + }); + }); +}; + export const accountsDataProvider = makeDataProvider< AccountsQuery, AccountFieldsFragment[], @@ -115,41 +95,46 @@ export const accountsDataProvider = makeDataProvider< getDelta, }); +const getSymbols = (data: AccountFieldsFragment[]) => + Array.from(new Set(data.map((a) => a.asset.symbol))).sort(); + +// TODO add test for this export const getAccountData = ( - data: AccountFieldsFragment[], - assetSymbol: string -) => { - const assetData = data.filter((a) => a.asset.symbol === assetSymbol); + data: AccountFieldsFragment[] +): AccountFields[] => { + const collateralData = getSymbols(data).map((assetSymbol) => { + const assetData = data.filter((a) => a.asset.symbol === assetSymbol); - const deposited = assetData - .filter((a) => [AccountType.ACCOUNT_TYPE_GENERAL].includes(a.type)) - .reduce((acc, a) => acc + BigInt(a.balance), BigInt(0)); + const deposited = assetData + .filter((a) => [AccountType.ACCOUNT_TYPE_GENERAL].includes(a.type)) + .reduce((acc, a) => acc + BigInt(a.balance), BigInt(0)); - const incoming = assetData - .filter((a) => INCOMING_ACCOUNT_TYPES.includes(a.type)) - .reduce((acc, a) => acc + BigInt(a.balance), BigInt(0)); + const incoming = assetData + .filter((a) => INCOMING_ACCOUNT_TYPES.includes(a.type)) + .reduce((acc, a) => acc + BigInt(a.balance), BigInt(0)); - const used = assetData - .filter((a) => OUTCOMING_ACCOUNT_TYPES.includes(a.type)) - .reduce((acc, a) => acc + BigInt(a.balance), BigInt(0)); + const used = assetData + .filter((a) => OUTCOMING_ACCOUNT_TYPES.includes(a.type)) + .reduce((acc, a) => acc + BigInt(a.balance), BigInt(0)); - const depositRow = { - ...assetData[0], - available: (incoming - used).toString(), - deposited: deposited.toString(), - used: used.toString(), - }; - const accountRows = assetData - .filter((a) => !INCOMING_ACCOUNT_TYPES.includes(a.type)) - .map((a) => ({ - ...a, - available: (incoming - BigInt(a.balance)).toString(), + const depositRow: AccountFields = { + ...assetData[0], + available: (incoming - used).toString(), deposited: deposited.toString(), - used: a.balance.toString(), - })); + used: used.toString(), + }; - return { - accountRows, - depositRow, - }; + const accountRows = assetData + .filter((a) => !INCOMING_ACCOUNT_TYPES.includes(a.type)) + .map((a) => ({ + ...a, + available: (incoming - BigInt(a.balance)).toString(), + deposited: deposited.toString(), + used: a.balance.toString(), + })); + + return { ...depositRow, breakdown: accountRows }; + }); + + return collateralData; }; diff --git a/libs/accounts/src/lib/accounts-manager.tsx b/libs/accounts/src/lib/accounts-manager.tsx deleted file mode 100644 index 4299abcb1..000000000 --- a/libs/accounts/src/lib/accounts-manager.tsx +++ /dev/null @@ -1,194 +0,0 @@ -import { forwardRef, useCallback, useMemo, useRef, useState } from 'react'; -import { AsyncRenderer, Button, Dialog } from '@vegaprotocol/ui-toolkit'; -import { - addSummaryRows, - t, - useDataProvider, -} from '@vegaprotocol/react-helpers'; -import type { - AccountFieldsFragment, - AccountEventsSubscription, -} from './__generated___/Accounts'; -import { - accountsDataProvider, - getAccountData, - getGroupId, - getGroupSummaryRow, - getId, -} from './accounts-data-provider'; -import AccountsTable from './accounts-table'; -import type { AgGridReact } from 'ag-grid-react'; -import { AccountDeposit } from './account-deposit'; -import { WithdrawalDialogs } from '@vegaprotocol/withdraws'; -import { Web3Container } from '@vegaprotocol/web3'; -import { DepositContainer } from '@vegaprotocol/deposits'; -import produce from 'immer'; -import merge from 'lodash/merge'; -import classNames from 'classnames'; - -export interface AccountFields extends AccountFieldsFragment { - available: string; - used: string; - deposited: string; - balance: string; -} - -const getSymbols = (account: AccountFieldsFragment[]) => - Array.from(new Set(account.map((a) => a.asset.symbol))).sort(); - -interface AccountsManagerProps { - partyId: string; -} - -export const AccountsManager = ({ partyId }: AccountsManagerProps) => { - const variables = useMemo(() => ({ partyId }), [partyId]); - const assetSymbols = useRef(); - const gridRef = useRef(null); - const update = useCallback( - ({ delta: deltas }: { delta: AccountEventsSubscription['accounts'] }) => { - const update: AccountFieldsFragment[] = []; - const add: AccountFieldsFragment[] = []; - if (!gridRef.current?.api) { - return false; - } - const api = gridRef.current.api; - deltas.forEach((delta) => { - const rowNode = api.getRowNode(getId(delta)); - if (rowNode) { - const updatedData = produce( - rowNode.data, - (draft: AccountFieldsFragment) => { - merge(draft, delta); - } - ); - if (updatedData !== rowNode.data) { - update.push(updatedData); - } - } else { - // #TODO handle new account (or leave it to data provider to handle it) - } - }); - if (update.length || add.length) { - gridRef.current.api.applyTransactionAsync({ - update, - add, - addIndex: 0, - }); - } - if (add.length) { - addSummaryRows( - gridRef.current.api, - gridRef.current.columnApi, - getGroupId, - getGroupSummaryRow - ); - } - return true; - }, - [gridRef] - ); - - const { data, error, loading } = useDataProvider< - AccountFieldsFragment[], - AccountEventsSubscription['accounts'] - >({ dataProvider: accountsDataProvider, update, variables }); - const [depositDialog, setDepositDialog] = useState(false); - - const symbols = data && getSymbols(data); - return ( - - - {symbols && - symbols.map((assetSymbol, i) => ( - - ))} - -
- -
- -
- ); -}; - -export interface AssetAccountTableProps { - assetSymbol: string; - data: AccountFieldsFragment[]; - hideHeader?: boolean; -} - -export const AssetAccountTable = forwardRef< - AgGridReact, - AssetAccountTableProps ->(({ data, assetSymbol, hideHeader }, ref) => { - const { accountRows, depositRow } = getAccountData(data, assetSymbol); - const [open, setOpen] = useState(false); - const [withdrawDialog, setWithdrawDialog] = useState(false); - const [depositDialog, setDepositDialog] = useState(false); - return ( - <> -
- 0} - hideHeader={hideHeader} - onClickAsset={() => setOpen(!open)} - onClickWithdraw={() => setWithdrawDialog(true)} - onClickDeposit={() => setDepositDialog(true)} - /> -
- {open && accountRows.length > 0 && ( -
- -
- )} - - - - ); -}); - -export interface DepositDialogProps { - assetId?: string; - depositDialog: boolean; - setDepositDialog: (open: boolean) => void; -} - -export const DepositDialog = ({ - assetId, - depositDialog, - setDepositDialog, -}: DepositDialogProps) => { - return ( - -

{t('Deposit')}

- -
- ); -}; diff --git a/libs/accounts/src/lib/accounts-table.spec.tsx b/libs/accounts/src/lib/accounts-table.spec.tsx deleted file mode 100644 index e72828281..000000000 --- a/libs/accounts/src/lib/accounts-table.spec.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import AccountsTable from './accounts-table'; -import { act, render, screen, waitFor } from '@testing-library/react'; -import type { AccountFields } from './accounts-manager'; -import { Schema as Types } from '@vegaprotocol/types'; - -const singleRow: AccountFields = { - __typename: 'Account', - type: Types.AccountType.ACCOUNT_TYPE_MARGIN, - balance: '125600000', - market: { - __typename: 'Market', - tradableInstrument: { - __typename: 'TradableInstrument', - instrument: { - __typename: 'Instrument', - name: 'BTCUSD Monthly (30 Jun 2022)', - }, - }, - id: '10cd0a793ad2887b340940337fa6d97a212e0e517fe8e9eab2b5ef3a38633f35', - }, - asset: { - __typename: 'Asset', - id: '5cfa87844724df6069b94e4c8a6f03af21907d7bc251593d08e4251043ee9f7c', - symbol: 'tBTC', - decimals: 5, - }, - available: '125600000', - used: '125600000', - deposited: '125600000', -}; -const singleRowData = [singleRow]; - -describe('AccountsTable', () => { - it('should render successfully', async () => { - await act(async () => { - const { baseElement } = render(); - expect(baseElement).toBeTruthy(); - }); - }); - - it('should render correct columns', async () => { - act(async () => { - render(); - await waitFor(async () => { - const headers = await screen.getAllByRole('columnheader'); - expect(headers).toHaveLength(4); - expect( - headers.map((h) => - h.querySelector('[ref="eText"]')?.textContent?.trim() - ) - ).toEqual(['Asset', 'Type', 'Market', 'Balance']); - }); - }); - }); - - it('should apply correct formatting', async () => { - act(async () => { - render(); - await waitFor(async () => { - const cells = await screen.getAllByRole('gridcell'); - const expectedValues = [ - 'tBTC', - singleRow.type, - 'BTCUSD Monthly (30 Jun 2022)', - '1,256.00000', - ]; - cells.forEach((cell, i) => { - expect(cell).toHaveTextContent(expectedValues[i]); - }); - }); - }); - }); -}); diff --git a/libs/accounts/src/lib/accounts-table.tsx b/libs/accounts/src/lib/accounts-table.tsx index 0c7f675ab..900c87f0e 100644 --- a/libs/accounts/src/lib/accounts-table.tsx +++ b/libs/accounts/src/lib/accounts-table.tsx @@ -1,59 +1,41 @@ -import type { CSSProperties } from 'react'; -import { forwardRef } from 'react'; +import { useCallback, useRef, useState } from 'react'; import type { - CellRendererSelectorResult, GroupCellRendererParams, - ICellRendererParams, ValueFormatterParams, } from 'ag-grid-community'; +import type { Asset, ValueProps } from '@vegaprotocol/react-helpers'; import { - PriceCell, - addDecimalsFormatNumber, - t, - formatNumberPercentage, + calculateLowHighRange, + progressBarCellRendererSelector, } from '@vegaprotocol/react-helpers'; -import { Button, Intent } from '@vegaprotocol/ui-toolkit'; -import { AgGridDynamic as AgGrid, ProgressBar } from '@vegaprotocol/ui-toolkit'; +import { useDataProvider } from '@vegaprotocol/react-helpers'; +import { addDecimalsFormatNumber, t } from '@vegaprotocol/react-helpers'; +import { Button, ButtonLink, Dialog, Intent } from '@vegaprotocol/ui-toolkit'; +import { TooltipCellComponent } from '@vegaprotocol/ui-toolkit'; +import { AgGridDynamic as AgGrid } from '@vegaprotocol/ui-toolkit'; import { AgGridColumn } from 'ag-grid-react'; import type { AgGridReact, AgGridReactProps } from 'ag-grid-react'; -import type { AccountFieldsFragment } from './__generated___/Accounts'; -import { getId } from './accounts-data-provider'; -import { useAssetDetailsDialogStore } from '@vegaprotocol/assets'; -import type { AccountFields } from './accounts-manager'; -import BigNumber from 'bignumber.js'; -import { AccountTypeMapping } from '@vegaprotocol/types'; -import type { AccountType } from '@vegaprotocol/types'; +import { + accountsDataProvider, + getAccountData, + getId, +} from './accounts-data-provider'; +import type { AccountFields } from './accounts-data-provider'; +import type { + AccountEventsSubscription, + AccountFieldsFragment, +} from './__generated___/Accounts'; +import BreakdownTable from './breakdown-table'; +import produce from 'immer'; +import merge from 'lodash/merge'; -export interface ValueProps { - valueFormatted?: { - low: string; - high: string; - percentage: string; - value: number; - intent?: Intent; - }; +interface AccountsTableProps extends AgGridReactProps { + partyId: string; + onClickAsset: (value?: string | Asset) => void; + onClickWithdraw?: () => void; + onClickDeposit?: () => void; } -export const EmptyCell = () => ''; - -export const ProgressBarCell = ({ valueFormatted }: ValueProps) => { - return valueFormatted ? ( - <> -
-
{valueFormatted.low}
-
- {valueFormatted.high} ({valueFormatted.percentage}) -
-
- - - ) : null; -}; - export const progressBarValueFormatter = ({ data, node, @@ -63,17 +45,10 @@ export const progressBarValueFormatter = ({ } const min = BigInt(data.used); const max = BigInt(data.deposited); - const range = max; - const value = range ? Number((min * BigInt(100)) / range) : 0; - return { - low: addDecimalsFormatNumber(min.toString(), data.asset.decimals), - high: addDecimalsFormatNumber((max - min).toString(), data.asset.decimals), - value: value, - intent: Intent.None, - percentage: value - ? formatNumberPercentage(new BigNumber(value), 2) - : '0.00%', - }; + const mid = max > min ? max - min : max; + const intent = Intent.None; + const decimals = data.asset.decimals; + return calculateLowHighRange(max, min, decimals, mid, intent); }; export const progressBarHeaderComponentParams = { @@ -84,19 +59,6 @@ export const progressBarHeaderComponentParams = { '', }; -export const progressBarCellRendererSelector = ( - params: ICellRendererParams -): CellRendererSelectorResult => { - return { - component: params.node.rowPinned ? EmptyCell : ProgressBarCell, - }; -}; - -interface AccountsTableProps extends AgGridReactProps { - data: AccountFields[] | null; - style?: CSSProperties; -} - interface AccountsTableValueFormatterParams extends ValueFormatterParams { data: AccountFieldsFragment; } @@ -107,46 +69,90 @@ export const assetDecimalsFormatter = ({ }: AccountsTableValueFormatterParams) => addDecimalsFormatNumber(value, data.asset.decimals); -export const AccountsTable = forwardRef( - ({ data }, ref) => { - const { setAssetDetailsDialogOpen, setAssetDetailsDialogSymbol } = - useAssetDetailsDialogStore(); - const assetDialogCellRenderer = ({ value }: GroupCellRendererParams) => { - if (!value || value.length <= 0) return '-'; - return ( - - ); - }; +export const AccountsTable = ({ + onClickAsset, + onClickWithdraw, + onClickDeposit, + partyId, +}: AccountsTableProps) => { + const gridRef = useRef(null); + const update = useCallback( + ({ delta: deltas }: { delta: AccountEventsSubscription['accounts'] }) => { + const update: AccountFieldsFragment[] = []; + const add: AccountFieldsFragment[] = []; + if (!gridRef.current?.api) { + return false; + } + const api = gridRef.current.api; + deltas.forEach((delta) => { + const rowNode = api.getRowNode(getId(delta)); + if (rowNode) { + const updatedData = produce( + rowNode.data, + (draft: AccountFieldsFragment) => { + merge(draft, delta); + } + ); + if (updatedData !== rowNode.data) { + update.push(updatedData); + } + } else { + // #TODO handle new account (or leave it to data provider to handle it) + } + }); + if (update.length || add.length) { + gridRef.current.api.applyTransactionAsync({ + update, + add, + addIndex: 0, + }); + } + return true; + }, + [gridRef] + ); - return ( + const { data: collateralData } = useDataProvider< + AccountFieldsFragment[], + AccountEventsSubscription['accounts'] + >({ dataProvider: accountsDataProvider, update, variables: { partyId } }); + const data = collateralData && getAccountData(collateralData); + const [openBreakdown, setOpenBreakdown] = useState(false); + const [breakdown, setBreakdown] = useState(null); + return ( + <> getId(data)} - ref={ref} + ref={gridRef} rowHeight={34} - headerHeight={0} - components={{ PriceCell }} tooltipShowDelay={500} defaultColDef={{ flex: 1, resizable: true, + tooltipComponent: TooltipCellComponent, + sortable: true, }} > { + return ( + { + onClickAsset(value); + }} + > + {value} + + ); + }} maxWidth={300} /> ( valueFormatter={progressBarValueFormatter} /> - AccountTypeMapping[value as AccountType] - } /> { + return ( + + ); + }} + /> + { + return ( + + ); + }} + /> + { + return ( + + ); + }} /> - ); - } -); + +
+

{'Breakdown'}

+ +
+
+ + ); +}; export default AccountsTable; diff --git a/libs/accounts/src/lib/breakdown-table.spec.tsx b/libs/accounts/src/lib/breakdown-table.spec.tsx new file mode 100644 index 000000000..b36cd3660 --- /dev/null +++ b/libs/accounts/src/lib/breakdown-table.spec.tsx @@ -0,0 +1,133 @@ +import BreakdownTable from './breakdown-table'; +import { act, render, screen, waitFor } from '@testing-library/react'; +import { Schema as Types } from '@vegaprotocol/types'; +import type { AccountFields } from './accounts-data-provider'; +import { getAccountData } from './accounts-data-provider'; + +const singleRow: AccountFields = { + __typename: 'Account', + type: Types.AccountType.ACCOUNT_TYPE_MARGIN, + balance: '125600000', + market: { + __typename: 'Market', + tradableInstrument: { + __typename: 'TradableInstrument', + instrument: { + __typename: 'Instrument', + name: 'BTCUSD Monthly (30 Jun 2022)', + }, + }, + id: '10cd0a793ad2887b340940337fa6d97a212e0e517fe8e9eab2b5ef3a38633f35', + }, + asset: { + __typename: 'Asset', + id: '5cfa87844724df6069b94e4c8a6f03af21907d7bc251593d08e4251043ee9f7c', + symbol: 'tBTC', + decimals: 5, + }, + available: '125600000', + used: '125600000', + deposited: '125600000', +}; +const singleRowData = [singleRow]; + +describe('AccountsTable', () => { + it('should render successfully', async () => { + await act(async () => { + const { baseElement } = render(); + expect(baseElement).toBeTruthy(); + }); + }); + + it('should render correct columns', async () => { + act(async () => { + render(); + await waitFor(async () => { + const headers = await screen.getAllByRole('columnheader'); + expect(headers).toHaveLength(4); + expect( + headers.map((h) => + h.querySelector('[ref="eText"]')?.textContent?.trim() + ) + ).toEqual(['Asset', 'Type', 'Market', 'Balance']); + }); + }); + }); + + it('should apply correct formatting', async () => { + act(async () => { + render(); + await waitFor(async () => { + const cells = await screen.getAllByRole('gridcell'); + const expectedValues = [ + 'tBTC', + singleRow.type, + 'BTCUSD Monthly (30 Jun 2022)', + '1,256.00000', + ]; + cells.forEach((cell, i) => { + expect(cell).toHaveTextContent(expectedValues[i]); + }); + }); + }); + }); + + it('should get correct account data', () => { + const result = getAccountData([singleRow]); + const expected = [ + { + __typename: 'Account', + asset: { + __typename: 'Asset', + decimals: 5, + id: '5cfa87844724df6069b94e4c8a6f03af21907d7bc251593d08e4251043ee9f7c', + symbol: 'tBTC', + }, + available: '-125600000', + balance: '125600000', + breakdown: [ + { + __typename: 'Account', + asset: { + __typename: 'Asset', + decimals: 5, + id: '5cfa87844724df6069b94e4c8a6f03af21907d7bc251593d08e4251043ee9f7c', + symbol: 'tBTC', + }, + available: '-125600000', + balance: '125600000', + deposited: '0', + market: { + __typename: 'Market', + id: '10cd0a793ad2887b340940337fa6d97a212e0e517fe8e9eab2b5ef3a38633f35', + tradableInstrument: { + __typename: 'TradableInstrument', + instrument: { + __typename: 'Instrument', + name: 'BTCUSD Monthly (30 Jun 2022)', + }, + }, + }, + type: 'ACCOUNT_TYPE_MARGIN', + used: '125600000', + }, + ], + deposited: '0', + market: { + __typename: 'Market', + id: '10cd0a793ad2887b340940337fa6d97a212e0e517fe8e9eab2b5ef3a38633f35', + tradableInstrument: { + __typename: 'TradableInstrument', + instrument: { + __typename: 'Instrument', + name: 'BTCUSD Monthly (30 Jun 2022)', + }, + }, + }, + type: 'ACCOUNT_TYPE_MARGIN', + used: '125600000', + }, + ]; + expect(result).toEqual(expected); + }); +}); diff --git a/libs/accounts/src/lib/breakdown-table.tsx b/libs/accounts/src/lib/breakdown-table.tsx new file mode 100644 index 000000000..d08ec56c1 --- /dev/null +++ b/libs/accounts/src/lib/breakdown-table.tsx @@ -0,0 +1,71 @@ +import type { CSSProperties } from 'react'; +import { forwardRef } from 'react'; +import type { ValueFormatterParams } from 'ag-grid-community'; +import { + PriceCell, + progressBarCellRendererSelector, + t, +} 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 { AccountFields } from './accounts-data-provider'; +import { getId } from './accounts-data-provider'; +import { AccountTypeMapping } from '@vegaprotocol/types'; +import type { AccountType } from '@vegaprotocol/types'; +import { + progressBarHeaderComponentParams, + progressBarValueFormatter, +} from './accounts-table'; + +interface BreakdownTableProps extends AgGridReactProps { + data: AccountFields[] | null; + style?: CSSProperties; +} + +export const BreakdownTable = forwardRef( + ({ data }, ref) => { + return ( + getId(data)} + ref={ref} + rowHeight={34} + components={{ PriceCell }} + tooltipShowDelay={500} + defaultColDef={{ + flex: 1, + resizable: true, + }} + > + + + + AccountTypeMapping[value as AccountType] + } + /> + + ); + } +); + +export default BreakdownTable; diff --git a/libs/accounts/src/lib/index.ts b/libs/accounts/src/lib/index.ts index 6bdb90b6b..b75f7997f 100644 --- a/libs/accounts/src/lib/index.ts +++ b/libs/accounts/src/lib/index.ts @@ -1,6 +1,6 @@ -export * from './__generated___/Accounts'; -export * from './accounts-container'; +export * from './__generated__'; +// export * from './__generated___'; export * from './accounts-data-provider'; -export * from './accounts-manager'; export * from './accounts-table'; export * from './asset-balance'; +export * from './breakdown-table'; diff --git a/libs/orders/src/lib/components/order-data-provider/order-data-provider.ts b/libs/orders/src/lib/components/order-data-provider/order-data-provider.ts index 0accf076b..67645d6c6 100644 --- a/libs/orders/src/lib/components/order-data-provider/order-data-provider.ts +++ b/libs/orders/src/lib/components/order-data-provider/order-data-provider.ts @@ -153,7 +153,7 @@ export const ordersWithMarketProvider = makeDerivedDataProvider< >( [ordersProvider, marketsProvider], (partsData): OrderWithMarketEdge[] => - (partsData[0] as Parameters['0']).map((edge) => ({ + ((partsData[0] as Parameters['0']) || []).map((edge) => ({ cursor: edge.cursor, node: { ...edge.node, diff --git a/libs/positions/src/lib/positions-table.tsx b/libs/positions/src/lib/positions-table.tsx index c6d081bd3..711ec8bae 100644 --- a/libs/positions/src/lib/positions-table.tsx +++ b/libs/positions/src/lib/positions-table.tsx @@ -7,6 +7,7 @@ import type { ICellRendererParams, CellRendererSelectorResult, } from 'ag-grid-community'; +import type { ValueProps as PriceCellProps } from '@vegaprotocol/react-helpers'; import { PriceFlashCell, addDecimalsFormatNumber, @@ -16,8 +17,11 @@ import { getDateTimeFormat, signedNumberCssClass, signedNumberCssClassRules, + calculateLowHighRange, + EmptyCell, + ProgressBarCell, } from '@vegaprotocol/react-helpers'; -import { AgGridDynamic as AgGrid, ProgressBar } from '@vegaprotocol/ui-toolkit'; +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'; @@ -64,33 +68,6 @@ export const MarketNameCell = ({ valueFormatted }: MarketNameCellProps) => { return (valueFormatted && valueFormatted[0]) || undefined; }; -export interface PriceCellProps { - valueFormatted?: { - low: string; - high: string; - value: number; - intent?: Intent; - }; -} - -export const ProgressBarCell = ({ valueFormatted }: PriceCellProps) => { - return valueFormatted ? ( - <> -
-
{valueFormatted.low}
-
{valueFormatted.high}
-
- - - ) : null; -}; - -ProgressBarCell.displayName = 'PriceFlashCell'; - export interface AmountCellProps { valueFormatted?: Pick< Position, @@ -140,7 +117,22 @@ const ButtonCell = ({ ); }; -const EmptyCell = () => ''; +const progressBarValueFormatter = ({ + data, + node, +}: PositionsTableValueFormatterParams): + | PriceCellProps['valueFormatted'] + | undefined => { + if (!data || node?.rowPinned) { + return undefined; + } + const min = BigInt(data.averageEntryPrice); + const max = BigInt(data.liquidationPrice); + const mid = BigInt(data.markPrice); + const decimals = data.marketDecimalPlaces; + const intent = data.lowMarginLevel ? Intent.Warning : Intent.None; + return calculateLowHighRange(max, min, decimals, mid, intent); +}; export const PositionsTable = forwardRef( ({ onClose, ...props }, ref) => { @@ -264,32 +256,7 @@ export const PositionsTable = forwardRef( component: params.node.rowPinned ? EmptyCell : ProgressBarCell, }; }} - valueFormatter={({ - data, - node, - }: PositionsTableValueFormatterParams): - | PriceCellProps['valueFormatted'] - | undefined => { - if (!data || node?.rowPinned) { - return undefined; - } - const min = BigInt(data.averageEntryPrice); - const max = BigInt(data.liquidationPrice); - const mid = BigInt(data.markPrice); - const range = max - min; - return { - low: addDecimalsFormatNumber( - min.toString(), - data.marketDecimalPlaces - ), - high: addDecimalsFormatNumber( - max.toString(), - data.marketDecimalPlaces - ), - value: range ? Number(((mid - min) * BigInt(100)) / range) : 0, - intent: data.lowMarginLevel ? Intent.Warning : undefined, - }; - }} + valueFormatter={progressBarValueFormatter} /> ''; + +export const ProgressBarCell = ({ valueFormatted }: ValueProps) => { + return valueFormatted ? ( + <> +
+
{valueFormatted.low}
+
{valueFormatted.high}
+
+ + + ) : null; +}; + +export const progressBarCellRendererSelector = ( + params: ICellRendererParams +): CellRendererSelectorResult => { + return { + component: params.node.rowPinned ? EmptyCell : ProgressBarCell, + }; +}; + +export const calculateLowHighRange = ( + max: bigint, + min: bigint, + decimals: number, + mid: bigint, + intent: Intent +) => { + const range = max > min ? max - min : max; // coloured range should stay between min max bounds. + return { + low: addDecimalsFormatNumber(min.toString(), decimals), + high: addDecimalsFormatNumber(max.toString(), decimals), + value: range ? Number(((mid - min) * BigInt(100)) / range) : 0, + intent, + }; +}; diff --git a/libs/react-helpers/src/lib/grid/index.ts b/libs/react-helpers/src/lib/grid/index.ts index c71250dbb..a1e2342c5 100644 --- a/libs/react-helpers/src/lib/grid/index.ts +++ b/libs/react-helpers/src/lib/grid/index.ts @@ -6,3 +6,4 @@ export * from './price-flash-cell'; export * from './size'; export * from './summary-rows'; export * from './vol-cell'; +export * from './grid-progress-bar';