fix: container moved, progress bar in helpers

This commit is contained in:
Madalina Raicu
2022-09-22 14:22:02 +01:00
parent d9032d3171
commit 5edb33c3cd
20 changed files with 713 additions and 677 deletions
+1 -1
View File
@@ -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,
@@ -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 (
<Splash>
<p>{t('Please connect Vega wallet')}</p>
</Splash>
);
}
return (
<Web3Container>
<div className="h-full">
<AssetAccountTable partyId={keypair.pub} />
<div className="m-auto ml-4">
<Button size="sm" onClick={() => setDepositDialog(true)}>
{t('Deposit new asset')}
</Button>
</div>
<DepositDialog
depositDialog={depositDialog}
setDepositDialog={setDepositDialog}
/>
</div>
</Web3Container>
);
};
export const AssetAccountTable = ({ partyId }: { partyId: string }) => {
const [withdrawDialog, setWithdrawDialog] = useState(false);
const [depositDialog, setDepositDialog] = useState(false);
const { setAssetDetailsDialogOpen, setAssetDetailsDialogSymbol } =
useAssetDetailsDialogStore();
return (
<>
<AccountsTable
partyId={partyId}
onClickAsset={(value) => {
if (value) {
setAssetDetailsDialogOpen(true);
setAssetDetailsDialogSymbol(value);
}
}}
onClickWithdraw={() => setWithdrawDialog(true)}
onClickDeposit={() => setDepositDialog(true)}
/>
<WithdrawalDialogs
withdrawDialog={withdrawDialog}
setWithdrawDialog={setWithdrawDialog}
/>
<DepositDialog
depositDialog={depositDialog}
setDepositDialog={setDepositDialog}
/>
</>
);
};
export interface DepositDialogProps {
assetId?: string;
depositDialog: boolean;
setDepositDialog: (open: boolean) => void;
}
export const DepositDialog = ({
assetId,
depositDialog,
setDepositDialog,
}: DepositDialogProps) => {
return (
<Dialog open={depositDialog} onChange={setDepositDialog}>
<h1 className="text-2xl mb-4">{t('Deposit')}</h1>
<DepositContainer assetId={assetId} />
</Dialog>
);
};
+1 -1
View File
@@ -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';
@@ -13,7 +13,6 @@ export const WithdrawalsContainer = () => {
const { withdrawals, loading, error } = useWithdrawals();
const [withdrawDialog, setWithdrawDialog] = useState(false);
console.log('render');
return (
<Web3Container>
<VegaWalletContainer>
+111
View File
@@ -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<AccountsQuery, AccountsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<AccountsQuery, AccountsQueryVariables>(AccountsDocument, options);
}
export function useAccountsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<AccountsQuery, AccountsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<AccountsQuery, AccountsQueryVariables>(AccountsDocument, options);
}
export type AccountsQueryHookResult = ReturnType<typeof useAccountsQuery>;
export type AccountsLazyQueryHookResult = ReturnType<typeof useAccountsLazyQuery>;
export type AccountsQueryResult = Apollo.QueryResult<AccountsQuery, AccountsQueryVariables>;
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<AccountEventsSubscription, AccountEventsSubscriptionVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useSubscription<AccountEventsSubscription, AccountEventsSubscriptionVariables>(AccountEventsDocument, options);
}
export type AccountEventsSubscriptionHookResult = ReturnType<typeof useAccountEventsSubscription>;
export type AccountEventsSubscriptionResult = Apollo.SubscriptionResult<AccountEventsSubscription>;
+1
View File
@@ -0,0 +1 @@
export * from './Accounts';
@@ -0,0 +1 @@
export * from './Accounts';
-142
View File
@@ -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<AgGridReact, AccountsTableProps>(
(
{
data,
onClickAsset,
expanded,
showRows,
hideHeader,
onClickWithdraw,
onClickDeposit,
},
ref
) => {
const openAssetAccountCellRenderer = ({ value }: GroupCellRendererParams) =>
showRows ? (
<button onClick={onClickAsset}>
<span className="p-2">
<Icon
name="chevron-down"
className={expanded ? 'rotate-180' : ''}
size={3}
/>
</span>
{value}
</button>
) : (
<button className="pl-4">{value}</button>
);
return (
<AgGrid
style={{ width: '100%', height: '100%' }}
rowData={data}
getRowId={({ data }) => getId(data)}
ref={ref}
rowHeight={34}
tooltipShowDelay={500}
headerHeight={hideHeader ? 0 : undefined}
defaultColDef={{
flex: 1,
resizable: true,
tooltipComponent: TooltipCellComponent,
}}
>
<AgGridColumn
headerName={t('Asset')}
colId="asset.symbol"
field="asset.symbol"
headerTooltip={t(
'Asset is the collateral that is deposited into the Vega protocol.'
)}
cellRenderer={openAssetAccountCellRenderer}
maxWidth={300}
/>
<AgGridColumn
headerName={t('Used')}
field="used"
flex={2}
maxWidth={500}
headerComponentParams={progressBarHeaderComponentParams}
cellRendererSelector={progressBarCellRendererSelector}
valueFormatter={progressBarValueFormatter}
/>
<AgGridColumn
headerName={t('Deposited')}
field="deposited"
valueFormatter={assetDecimalsFormatter}
maxWidth={300}
/>
<AgGridColumn
headerName=""
field="deposit"
maxWidth={300}
cellRenderer={() => {
return (
<Button size="xs" data-testid="deposit" onClick={onClickDeposit}>
{t('Deposit')}
</Button>
);
}}
/>
<AgGridColumn
headerName=""
field="withdraw"
maxWidth={300}
cellRenderer={() => {
return (
<Button
size="xs"
data-testid="withdraw"
onClick={onClickWithdraw}
>
{t('Withdraw')}
</Button>
);
}}
/>
</AgGrid>
);
}
);
export default AccountDeposit;
@@ -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 (
<Splash>
<p>{t('Please connect Vega wallet')}</p>
</Splash>
);
}
return <AccountsManager partyId={keypair.pub} />;
};
+61 -76
View File
@@ -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<AccountFields & SummaryRow> | 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;
};
-194
View File
@@ -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<string[] | undefined>();
const gridRef = useRef<AgGridReact | null>(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<AccountFieldsFragment>(
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 (
<Web3Container>
<AsyncRenderer loading={loading} error={error} data={assetSymbols}>
{symbols &&
symbols.map((assetSymbol, i) => (
<AssetAccountTable
ref={gridRef}
key={assetSymbol}
assetSymbol={assetSymbol}
data={data}
hideHeader={i !== 0}
/>
))}
</AsyncRenderer>
<div className="m-auto ml-4">
<Button size="sm" onClick={() => setDepositDialog(true)}>
{t('Deposit new asset')}
</Button>
</div>
<DepositDialog
depositDialog={depositDialog}
setDepositDialog={setDepositDialog}
/>
</Web3Container>
);
};
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 (
<>
<div
className={classNames({
'h-[50px]': hideHeader,
'h-[85px]': !hideHeader,
})}
>
<AccountDeposit
ref={ref}
data={[depositRow]}
expanded={open}
showRows={accountRows?.length > 0}
hideHeader={hideHeader}
onClickAsset={() => setOpen(!open)}
onClickWithdraw={() => setWithdrawDialog(true)}
onClickDeposit={() => setDepositDialog(true)}
/>
</div>
{open && accountRows.length > 0 && (
<div className="h-[15vh]">
<AccountsTable ref={ref} data={accountRows} domLayout="autoHeight" />
</div>
)}
<WithdrawalDialogs
assetId={depositRow.asset.id}
withdrawDialog={withdrawDialog}
setWithdrawDialog={setWithdrawDialog}
/>
<DepositDialog
assetId={depositRow.asset.id}
depositDialog={depositDialog}
setDepositDialog={setDepositDialog}
/>
</>
);
});
export interface DepositDialogProps {
assetId?: string;
depositDialog: boolean;
setDepositDialog: (open: boolean) => void;
}
export const DepositDialog = ({
assetId,
depositDialog,
setDepositDialog,
}: DepositDialogProps) => {
return (
<Dialog open={depositDialog} onChange={setDepositDialog}>
<h1 className="text-2xl mb-4">{t('Deposit')}</h1>
<DepositContainer assetId={assetId} />
</Dialog>
);
};
@@ -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(<AccountsTable data={[]} />);
expect(baseElement).toBeTruthy();
});
});
it('should render correct columns', async () => {
act(async () => {
render(<AccountsTable data={singleRowData} />);
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(<AccountsTable data={singleRowData} />);
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]);
});
});
});
});
});
+159 -112
View File
@@ -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 ? (
<>
<div className="flex justify-between leading-tight font-mono">
<div>{valueFormatted.low}</div>
<div>
{valueFormatted.high} ({valueFormatted.percentage})
</div>
</div>
<ProgressBar
value={valueFormatted.value}
intent={valueFormatted.intent}
className="mt-2"
/>
</>
) : 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 = {
'</div>',
};
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<AgGridReact, AccountsTableProps>(
({ data }, ref) => {
const { setAssetDetailsDialogOpen, setAssetDetailsDialogSymbol } =
useAssetDetailsDialogStore();
const assetDialogCellRenderer = ({ value }: GroupCellRendererParams) => {
if (!value || value.length <= 0) return '-';
return (
<Button
size="xs"
className="hover:underline"
onClick={() => {
setAssetDetailsDialogOpen(true);
setAssetDetailsDialogSymbol(value);
}}
>
{t('Asset details')}
</Button>
);
};
export const AccountsTable = ({
onClickAsset,
onClickWithdraw,
onClickDeposit,
partyId,
}: AccountsTableProps) => {
const gridRef = useRef<AgGridReact | null>(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<AccountFieldsFragment>(
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<AccountFields[] | null>(null);
return (
<>
<AgGrid
style={{ width: '100%', height: '100%' }}
overlayNoRowsTemplate={t('No accounts')}
rowData={data}
getRowId={({ data }) => getId(data)}
ref={ref}
ref={gridRef}
rowHeight={34}
headerHeight={0}
components={{ PriceCell }}
tooltipShowDelay={500}
defaultColDef={{
flex: 1,
resizable: true,
tooltipComponent: TooltipCellComponent,
sortable: true,
}}
>
<AgGridColumn
headerName={t('Market')}
field="market.tradableInstrument.instrument.name"
valueFormatter="value || '—'"
headerName={t('Asset')}
field="asset.symbol"
headerTooltip={t(
'Asset is the collateral that is deposited into the Vega protocol.'
)}
cellRenderer={({ value }: ValueFormatterParams) => {
return (
<ButtonLink
data-testid="deposit"
onClick={() => {
onClickAsset(value);
}}
>
{value}
</ButtonLink>
);
}}
maxWidth={300}
/>
<AgGridColumn
@@ -159,26 +165,67 @@ export const AccountsTable = forwardRef<AgGridReact, AccountsTableProps>(
valueFormatter={progressBarValueFormatter}
/>
<AgGridColumn
headerName={t('Type')}
field="type"
headerName={t('Deposited')}
field="deposited"
valueFormatter={assetDecimalsFormatter}
maxWidth={300}
valueFormatter={({ value }: ValueFormatterParams) =>
AccountTypeMapping[value as AccountType]
}
/>
<AgGridColumn
headerName={t('Asset')}
colId="asset.symbol"
field="asset.symbol"
maxWidth={300}
headerTooltip={t(
'Asset is the collateral that is deposited into the Vega protocol.'
)}
cellRenderer={assetDialogCellRenderer}
headerName=""
field="breakdown"
minWidth={250}
cellRenderer={({ value }: GroupCellRendererParams) => {
return (
<Button
size="xs"
data-testid="breakdown"
onClick={() => {
setOpenBreakdown(!openBreakdown);
setBreakdown(value);
}}
>
{t('Collateral breakdown')}
</Button>
);
}}
/>
<AgGridColumn
headerName=""
field="deposit"
maxWidth={200}
cellRenderer={() => {
return (
<Button size="xs" data-testid="deposit" onClick={onClickDeposit}>
{t('Deposit')}
</Button>
);
}}
/>
<AgGridColumn
headerName=""
field="withdraw"
maxWidth={200}
cellRenderer={() => {
return (
<Button
size="xs"
data-testid="withdraw"
onClick={onClickWithdraw}
>
{t('Withdraw')}
</Button>
);
}}
/>
</AgGrid>
);
}
);
<Dialog size="medium" open={openBreakdown} onChange={setOpenBreakdown}>
<div className="h-[35vh] w-full m-auto flex flex-col">
<h1 className="text-xl mb-4">{'Breakdown'}</h1>
<BreakdownTable data={breakdown} domLayout="autoHeight" />
</div>
</Dialog>
</>
);
};
export default AccountsTable;
@@ -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(<BreakdownTable data={[]} />);
expect(baseElement).toBeTruthy();
});
});
it('should render correct columns', async () => {
act(async () => {
render(<BreakdownTable data={singleRowData} />);
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(<BreakdownTable data={singleRowData} />);
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);
});
});
+71
View File
@@ -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<AgGridReact, BreakdownTableProps>(
({ data }, ref) => {
return (
<AgGrid
style={{ width: '100%', height: '100%' }}
overlayNoRowsTemplate={t('No collateral breakdown')}
rowData={data}
getRowId={({ data }) => getId(data)}
ref={ref}
rowHeight={34}
components={{ PriceCell }}
tooltipShowDelay={500}
defaultColDef={{
flex: 1,
resizable: true,
}}
>
<AgGridColumn
headerName={t('Market')}
field="market.tradableInstrument.instrument.name"
valueFormatter="value || '—'"
maxWidth={300}
/>
<AgGridColumn
headerName={t('Used')}
field="used"
flex={2}
maxWidth={500}
headerComponentParams={progressBarHeaderComponentParams}
cellRendererSelector={progressBarCellRendererSelector}
valueFormatter={progressBarValueFormatter}
/>
<AgGridColumn
headerName={t('Type')}
field="type"
maxWidth={300}
valueFormatter={({ value }: ValueFormatterParams) =>
AccountTypeMapping[value as AccountType]
}
/>
</AgGrid>
);
}
);
export default BreakdownTable;
+3 -3
View File
@@ -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';
@@ -153,7 +153,7 @@ export const ordersWithMarketProvider = makeDerivedDataProvider<
>(
[ordersProvider, marketsProvider],
(partsData): OrderWithMarketEdge[] =>
(partsData[0] as Parameters<typeof update>['0']).map((edge) => ({
((partsData[0] as Parameters<typeof update>['0']) || []).map((edge) => ({
cursor: edge.cursor,
node: {
...edge.node,
+22 -55
View File
@@ -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 ? (
<>
<div className="flex justify-between leading-tight font-mono">
<div>{valueFormatted.low}</div>
<div>{valueFormatted.high}</div>
</div>
<ProgressBar
value={valueFormatted.value}
intent={valueFormatted.intent}
className="mt-2"
/>
</>
) : 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<AgGridReact, Props>(
({ onClose, ...props }, ref) => {
@@ -264,32 +256,7 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
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}
/>
<AgGridColumn
headerName={t('Leverage')}
@@ -0,0 +1,58 @@
import type {
CellRendererSelectorResult,
ICellRendererParams,
} from 'ag-grid-community';
import { addDecimalsFormatNumber } from '@vegaprotocol/react-helpers';
import type { Intent } from '@vegaprotocol/ui-toolkit';
import { ProgressBar } from '@vegaprotocol/ui-toolkit';
export interface ValueProps {
valueFormatted?: {
low: string;
high: string;
value: number;
intent?: Intent;
};
}
export const EmptyCell = () => '';
export const ProgressBarCell = ({ valueFormatted }: ValueProps) => {
return valueFormatted ? (
<>
<div className="flex justify-between leading-tight font-mono">
<div>{valueFormatted.low}</div>
<div>{valueFormatted.high}</div>
</div>
<ProgressBar
value={valueFormatted.value}
intent={valueFormatted.intent}
className="mt-2"
/>
</>
) : 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,
};
};
+1
View File
@@ -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';