chore(trading): split bottom panel into two parts (#3205)

This commit is contained in:
Maciek 2023-03-17 10:50:43 +01:00 committed by GitHub
parent 300019f108
commit 84795b2d6a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 323 additions and 187 deletions

View File

@ -10,7 +10,7 @@ export const Footer = () => {
const [nodeSwitcherOpen, setNodeSwitcherOpen] = useState(false);
const { screenSize } = useScreenDimensions();
const showFullFeedbackLabel = useMemo(
() => ['lg', 'xl'].includes(screenSize),
() => ['lg', 'xl', 'xxl', 'xxxl'].includes(screenSize),
[screenSize]
);

View File

@ -216,6 +216,60 @@ describe('Market trading page', () => {
});
});
});
describe('market bottom panel', { tags: '@smoke' }, () => {
it('on xxl screen should be splitted out into two tables', () => {
cy.getByTestId('tab-positions').should(
'have.attr',
'data-state',
'active'
);
cy.getByTestId('tab-orders').should(
'have.attr',
'data-state',
'inactive'
);
cy.getByTestId('tab-fills').should('have.attr', 'data-state', 'inactive');
cy.getByTestId('tab-accounts').should(
'have.attr',
'data-state',
'inactive'
);
cy.viewport(1801, 1000);
cy.getByTestId('tab-positions').should(
'have.attr',
'data-state',
'active'
);
cy.getByTestId('tab-orders').should('have.attr', 'data-state', 'active');
cy.getByTestId('tab-fills').should('have.attr', 'data-state', 'inactive');
cy.getByTestId('tab-accounts').should(
'have.attr',
'data-state',
'inactive'
);
cy.getByTestId('Fills').click();
cy.getByTestId('Collateral').click();
cy.getByTestId('tab-positions').should(
'have.attr',
'data-state',
'inactive'
);
cy.getByTestId('tab-orders').should(
'have.attr',
'data-state',
'inactive'
);
cy.getByTestId('tab-fills').should('have.attr', 'data-state', 'active');
cy.getByTestId('tab-accounts').should(
'have.attr',
'data-state',
'active'
);
});
});
});
describe('market states not accepting orders', { tags: '@smoke' }, function () {

View File

@ -1,9 +1,9 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import debounce from 'lodash/debounce';
import React, { useCallback, useEffect, useMemo } from 'react';
import { addDecimalsFormatNumber, titlefy } from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import {
useDataProvider,
useScreenDimensions,
useThrottledDataProvider,
} from '@vegaprotocol/react-helpers';
import { AsyncRenderer, ExternalLink, Splash } from '@vegaprotocol/ui-toolkit';
@ -61,7 +61,8 @@ export const MarketPage = () => {
const { marketId } = useParams();
const navigate = useNavigate();
const { w } = useWindowSize();
const { screenSize } = useScreenDimensions();
const largeScreen = ['lg', 'xl', 'xxl', 'xxxl'].includes(screenSize);
const update = useGlobalStore((store) => store.update);
const lastMarketId = useGlobalStore((store) => store.marketId);
@ -87,7 +88,7 @@ export const MarketPage = () => {
}, [update, lastMarketId, data?.id]);
const tradeView = useMemo(() => {
if (w > 960) {
if (largeScreen) {
return (
<TradeGrid
market={data}
@ -105,7 +106,7 @@ export const MarketPage = () => {
onClickCollateral={() => navigate('/portfolio')}
/>
);
}, [w, data, onSelect, navigate]);
}, [largeScreen, data, onSelect, navigate]);
if (!data && marketId) {
return (
<Splash>
@ -140,37 +141,3 @@ export const MarketPage = () => {
</AsyncRenderer>
);
};
const useWindowSize = () => {
const [windowSize, setWindowSize] = useState(() => {
if (typeof window !== 'undefined') {
return {
w: window.innerWidth,
h: window.innerHeight,
};
}
// Something sensible for server rendered page
return {
w: 1200,
h: 900,
};
});
useEffect(() => {
const handleResize = debounce(({ target }) => {
setWindowSize({
w: target.innerWidth,
h: target.innerHeight,
});
}, 300);
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);
return windowSize;
};

View File

@ -8,7 +8,7 @@ import { TradesContainer } from '@vegaprotocol/trades';
import { LayoutPriority } from 'allotment';
import classNames from 'classnames';
import AutoSizer from 'react-virtualized-auto-sizer';
import { memo, useState } from 'react';
import { memo, useCallback, useState } from 'react';
import type { ReactNode, ComponentProps } from 'react';
import { DepthChartContainer } from '@vegaprotocol/market-depth';
import { CandlesChartContainer } from '@vegaprotocol/candles-chart';
@ -29,6 +29,7 @@ import { LiquidityContainer } from '../liquidity/liquidity';
import { useNavigate } from 'react-router-dom';
import { Links, Routes } from '../../pages/client-router';
import type { PinnedAsset } from '@vegaprotocol/accounts';
import { useScreenDimensions } from '@vegaprotocol/react-helpers';
type MarketDependantView =
| typeof CandlesChartContainer
@ -69,129 +70,204 @@ interface TradeGridProps {
pinnedAsset?: PinnedAsset;
}
const MainGrid = ({
marketId,
onSelect,
pinnedAsset,
}: {
interface BottomPanelProps {
marketId: string;
onSelect?: (marketId: string) => void;
pinnedAsset?: PinnedAsset;
}) => {
const navigate = useNavigate();
const onMarketClick = (marketId: string) => {
navigate(Links[Routes.MARKET](marketId), {
replace: true,
});
};
return (
<ResizableGrid vertical>
<ResizableGridPanel minSize={75} priority={LayoutPriority.High}>
<ResizableGrid proportionalLayout={false} minSize={200}>
<ResizableGridPanel
priority={LayoutPriority.High}
minSize={200}
preferredSize="50%"
>
<TradeGridChild>
<Tabs>
<Tab id="chart" name={t('Chart')}>
<TradingViews.Candles marketId={marketId} />
</Tab>
<Tab id="depth" name={t('Depth')}>
<TradingViews.Depth marketId={marketId} />
</Tab>
<Tab id="liquidity" name={t('Liquidity')}>
<TradingViews.Liquidity marketId={marketId} />
</Tab>
</Tabs>
</TradeGridChild>
</ResizableGridPanel>
<ResizableGridPanel
priority={LayoutPriority.Low}
preferredSize={330}
minSize={300}
>
<TradeGridChild>
<Tabs>
<Tab id="ticket" name={t('Ticket')}>
<TradingViews.Ticket
}
const MarketBottomPanel = memo(
({ marketId, pinnedAsset }: BottomPanelProps) => {
const { screenSize } = useScreenDimensions();
const navigate = useNavigate();
const onMarketClick = useCallback(
(marketId: string) => {
navigate(Links[Routes.MARKET](marketId), {
replace: true,
});
},
[navigate]
);
return 'xxxl' === screenSize ? (
<ResizableGrid proportionalLayout minSize={200}>
<ResizableGridPanel
priority={LayoutPriority.Low}
preferredSize="50%"
minSize={50}
>
<TradeGridChild>
<Tabs>
<Tab id="orders" name={t('Orders')}>
<VegaWalletContainer>
<TradingViews.Orders
marketId={marketId}
onClickCollateral={() => navigate('/portfolio')}
onMarketClick={onMarketClick}
/>
</Tab>
<Tab id="info" name={t('Info')}>
<TradingViews.Info
</VegaWalletContainer>
</Tab>
<Tab id="fills" name={t('Fills')}>
<VegaWalletContainer>
<TradingViews.Fills
marketId={marketId}
onSelect={(id: string) => {
onSelect?.(id);
}}
onMarketClick={onMarketClick}
/>
</Tab>
</Tabs>
</TradeGridChild>
</ResizableGridPanel>
<ResizableGridPanel
priority={LayoutPriority.Low}
preferredSize={430}
minSize={200}
>
<TradeGridChild>
<Tabs>
<Tab id="orderbook" name={t('Orderbook')}>
<TradingViews.Orderbook marketId={marketId} />
</Tab>
<Tab id="trades" name={t('Trades')}>
<TradingViews.Trades marketId={marketId} />
</Tab>
</Tabs>
</TradeGridChild>
</ResizableGridPanel>
</ResizableGrid>
</ResizableGridPanel>
<ResizableGridPanel
priority={LayoutPriority.Low}
preferredSize="25%"
minSize={50}
>
<TradeGridChild>
<Tabs>
<Tab id="positions" name={t('Positions')}>
<VegaWalletContainer>
<TradingViews.Positions onMarketClick={onMarketClick} />
</VegaWalletContainer>
</Tab>
<Tab id="orders" name={t('Orders')}>
<VegaWalletContainer>
<TradingViews.Orders
marketId={marketId}
onMarketClick={onMarketClick}
/>
</VegaWalletContainer>
</Tab>
<Tab id="fills" name={t('Fills')}>
<VegaWalletContainer>
<TradingViews.Fills
marketId={marketId}
onMarketClick={onMarketClick}
/>
</VegaWalletContainer>
</Tab>
<Tab id="accounts" name={t('Collateral')}>
<VegaWalletContainer>
<TradingViews.Collateral
pinnedAsset={pinnedAsset}
hideButtons
/>
</VegaWalletContainer>
</Tab>
</Tabs>
</TradeGridChild>
</ResizableGridPanel>
</ResizableGrid>
);
};
const MainGridWrapped = memo(MainGrid);
</VegaWalletContainer>
</Tab>
</Tabs>
</TradeGridChild>
</ResizableGridPanel>
<ResizableGridPanel
priority={LayoutPriority.Low}
preferredSize="50%"
minSize={50}
>
<TradeGridChild>
<Tabs>
<Tab id="positions" name={t('Positions')}>
<VegaWalletContainer>
<TradingViews.Positions
onMarketClick={onMarketClick}
noBottomPlaceholder
/>
</VegaWalletContainer>
</Tab>
<Tab id="accounts" name={t('Collateral')}>
<VegaWalletContainer>
<TradingViews.Collateral
pinnedAsset={pinnedAsset}
noBottomPlaceholder
hideButtons
/>
</VegaWalletContainer>
</Tab>
</Tabs>
</TradeGridChild>
</ResizableGridPanel>
</ResizableGrid>
) : (
<TradeGridChild>
<Tabs>
<Tab id="positions" name={t('Positions')}>
<VegaWalletContainer>
<TradingViews.Positions onMarketClick={onMarketClick} />
</VegaWalletContainer>
</Tab>
<Tab id="orders" name={t('Orders')}>
<VegaWalletContainer>
<TradingViews.Orders
marketId={marketId}
onMarketClick={onMarketClick}
/>
</VegaWalletContainer>
</Tab>
<Tab id="fills" name={t('Fills')}>
<VegaWalletContainer>
<TradingViews.Fills
marketId={marketId}
onMarketClick={onMarketClick}
/>
</VegaWalletContainer>
</Tab>
<Tab id="accounts" name={t('Collateral')}>
<VegaWalletContainer>
<TradingViews.Collateral pinnedAsset={pinnedAsset} hideButtons />
</VegaWalletContainer>
</Tab>
</Tabs>
</TradeGridChild>
);
}
);
MarketBottomPanel.displayName = 'MarketBottomPanel';
const MainGrid = memo(
({
marketId,
onSelect,
pinnedAsset,
}: {
marketId: string;
onSelect?: (marketId: string) => void;
pinnedAsset?: PinnedAsset;
}) => {
const navigate = useNavigate();
return (
<ResizableGrid vertical>
<ResizableGridPanel minSize={75} priority={LayoutPriority.High}>
<ResizableGrid proportionalLayout={false} minSize={200}>
<ResizableGridPanel
priority={LayoutPriority.High}
minSize={200}
preferredSize="50%"
>
<TradeGridChild>
<Tabs>
<Tab id="chart" name={t('Chart')}>
<TradingViews.Candles marketId={marketId} />
</Tab>
<Tab id="depth" name={t('Depth')}>
<TradingViews.Depth marketId={marketId} />
</Tab>
<Tab id="liquidity" name={t('Liquidity')}>
<TradingViews.Liquidity marketId={marketId} />
</Tab>
</Tabs>
</TradeGridChild>
</ResizableGridPanel>
<ResizableGridPanel
priority={LayoutPriority.Low}
preferredSize={330}
minSize={300}
>
<TradeGridChild>
<Tabs>
<Tab id="ticket" name={t('Ticket')}>
<TradingViews.Ticket
marketId={marketId}
onClickCollateral={() => navigate('/portfolio')}
/>
</Tab>
<Tab id="info" name={t('Info')}>
<TradingViews.Info
marketId={marketId}
onSelect={(id: string) => {
onSelect?.(id);
}}
/>
</Tab>
</Tabs>
</TradeGridChild>
</ResizableGridPanel>
<ResizableGridPanel
priority={LayoutPriority.Low}
preferredSize={430}
minSize={200}
>
<TradeGridChild>
<Tabs>
<Tab id="orderbook" name={t('Orderbook')}>
<TradingViews.Orderbook marketId={marketId} />
</Tab>
<Tab id="trades" name={t('Trades')}>
<TradingViews.Trades marketId={marketId} />
</Tab>
</Tabs>
</TradeGridChild>
</ResizableGridPanel>
</ResizableGrid>
</ResizableGridPanel>
<ResizableGridPanel
priority={LayoutPriority.Low}
preferredSize="25%"
minSize={50}
>
<MarketBottomPanel marketId={marketId} pinnedAsset={pinnedAsset} />
</ResizableGridPanel>
</ResizableGrid>
);
}
);
MainGrid.displayName = 'MainGrid';
export const TradeGrid = ({
market,
@ -201,7 +277,7 @@ export const TradeGrid = ({
return (
<div className="h-full grid grid-rows-[min-content_1fr]">
<TradeMarketHeader market={market} onSelect={onSelect} />
<MainGridWrapped
<MainGrid
marketId={market?.id || ''}
onSelect={onSelect}
pinnedAsset={pinnedAsset}

View File

@ -12,9 +12,11 @@ import { useDepositDialog } from '@vegaprotocol/deposits';
export const AccountsContainer = ({
pinnedAsset,
hideButtons,
noBottomPlaceholder,
}: {
pinnedAsset?: PinnedAsset;
hideButtons?: boolean;
noBottomPlaceholder?: boolean;
}) => {
const { pubKey, isReadOnly } = useVegaWallet();
const { open: openAssetDetailsDialog } = useAssetDetailsDialogStore();
@ -46,6 +48,7 @@ export const AccountsContainer = ({
onClickDeposit={openDepositDialog}
isReadOnly={isReadOnly}
pinnedAsset={pinnedAsset}
noBottomPlaceholder={noBottomPlaceholder}
/>
{!isReadOnly && !hideButtons && (
<div className="flex gap-2 justify-end p-2 px-[11px] absolute lg:fixed bottom-0 right-3 dark:bg-black/75 bg-white/75 rounded">

View File

@ -19,6 +19,7 @@ interface AccountManagerProps {
onClickDeposit?: (assetId?: string) => void;
isReadOnly: boolean;
pinnedAsset?: PinnedAsset;
noBottomPlaceholder?: boolean;
}
export const AccountManager = ({
@ -28,6 +29,7 @@ export const AccountManager = ({
partyId,
isReadOnly,
pinnedAsset,
noBottomPlaceholder,
}: AccountManagerProps) => {
const gridRef = useRef<AgGridReact | null>(null);
const variables = useMemo(() => ({ partyId }), [partyId]);
@ -45,6 +47,7 @@ export const AccountManager = ({
const bottomPlaceholderProps = useBottomPlaceholder<AccountFields>({
gridRef,
setId,
disabled: noBottomPlaceholder,
});
const getRowHeight = useCallback(

View File

@ -68,10 +68,11 @@ export const PositionsManager = ({
const bottomPlaceholderProps = useBottomPlaceholder<Position>({
gridRef,
setId,
disabled: noBottomPlaceholder,
});
useEffect(() => {
setDataCount(gridRef.current?.api?.getModel().getRowCount() ?? 0);
}, [data]);
}, [data?.length]);
const onFilterChanged = useCallback((event: FilterChangedEvent) => {
setDataCount(gridRef.current?.api?.getModel().getRowCount() ?? 0);
}, []);
@ -86,7 +87,7 @@ export const PositionsManager = ({
suppressNoRowsOverlay
isReadOnly={isReadOnly}
onFilterChanged={onFilterChanged}
{...(noBottomPlaceholder ? null : bottomPlaceholderProps)}
{...bottomPlaceholderProps}
/>
<div className="pointer-events-none absolute inset-0">
<AsyncRenderer

View File

@ -116,6 +116,7 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
value
)
}
minWidth={190}
/>
<AgGridColumn
headerName={t('Notional')}
@ -141,6 +142,7 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
data.marketDecimalPlaces
);
}}
minWidth={80}
/>
<AgGridColumn
headerName={t('Open volume')}
@ -174,6 +176,7 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
);
}}
cellRenderer={OpenVolumeCell}
minWidth={100}
/>
<AgGridColumn
headerName={t('Mark price')}
@ -213,8 +216,13 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
data.marketDecimalPlaces
);
}}
minWidth={100}
/>
<AgGridColumn
headerName={t('Settlement asset')}
field="assetSymbol"
minWidth={100}
/>
<AgGridColumn headerName={t('Settlement asset')} field="assetSymbol" />
<AgGridColumn
headerName={t('Entry price')}
field="averageEntryPrice"
@ -248,6 +256,7 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
data.marketDecimalPlaces
);
}}
minWidth={100}
/>
<AgGridColumn
headerName={t('Leverage')}
@ -264,6 +273,7 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
}: VegaValueFormatterParams<Position, 'currentLeverage'>) =>
value === undefined ? undefined : formatNumber(value.toString(), 1)
}
minWidth={100}
/>
<AgGridColumn
headerName={t('Margin allocated')}
@ -295,6 +305,7 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
data.decimals
);
}}
minWidth={100}
/>
<AgGridColumn
headerName={t('Realised PNL')}
@ -321,6 +332,7 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
'Profit or loss is realised whenever your position is reduced to zero and the margin is released back to your collateral balance. P&L excludes any fees paid.'
)}
cellRenderer={PNLCell}
minWidth={100}
/>
<AgGridColumn
headerName={t('Unrealised PNL')}
@ -347,6 +359,7 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
'Unrealised profit is the current profit on your open position. Margin is still allocated to your position.'
)}
cellRenderer={PNLCell}
minWidth={100}
/>
<AgGridColumn
headerName={t('Updated')}
@ -361,6 +374,7 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
}
return getDateTimeFormat().format(new Date(value));
}}
minWidth={150}
/>
{onClose && !props.isReadOnly ? (
<AgGridColumn
@ -375,6 +389,7 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
</ButtonLink>
) : null
}
minWidth={80}
/>
) : null}
</AgGrid>

View File

@ -11,11 +11,13 @@ const isFullWidthRow = (params: IsFullWidthRowParams) =>
interface Props<T> {
gridRef: RefObject<AgGridReact>;
setId?: (data: T) => T;
disabled?: boolean;
}
// eslint-disable-next-line @typescript-eslint/ban-types
export const useBottomPlaceholder = <T extends {}>({
gridRef,
setId,
disabled,
}: Props<T>) => {
const onBodyScrollEnd = useCallback(() => {
const rowCont = gridRef.current?.api.getModel().getRowCount() ?? 0;
@ -58,14 +60,17 @@ export const useBottomPlaceholder = <T extends {}>({
}, [gridRef, onBodyScrollEnd]);
return useMemo(
() => ({
onBodyScrollEnd,
rowClassRules: NO_HOVER_CSS_RULE,
isFullWidthRow,
fullWidthCellRenderer,
onSortChanged: onRowsChanged,
onFilterChange: onRowsChanged,
}),
[onBodyScrollEnd, onRowsChanged]
() =>
!disabled
? {
onBodyScrollEnd,
rowClassRules: NO_HOVER_CSS_RULE,
isFullWidthRow,
fullWidthCellRenderer,
onSortChanged: onRowsChanged,
onFilterChange: onRowsChanged,
}
: {},
[onBodyScrollEnd, onRowsChanged, disabled]
);
};

View File

@ -1,10 +1,19 @@
import { useRef, useEffect, useState } from 'react';
const SERVER_SIDE_DIMENSIONS = {
width: 1200,
height: 900,
};
export const useResize = () => {
const [windowSize, setWindowSize] = useState({
width: window.innerWidth,
height: window.innerHeight,
});
const [windowSize, setWindowSize] = useState(
typeof window !== undefined
? {
width: window.innerWidth,
height: window.innerHeight,
}
: { ...SERVER_SIDE_DIMENSIONS }
);
const timeout = useRef(0);

View File

@ -1,29 +1,31 @@
import { useMemo } from 'react';
// @ts-ignore avoid adding declaration file
import { theme } from '@vegaprotocol/tailwindcss-config';
import { useResize } from './use-resize';
type Screen = keyof typeof theme.screens;
export type Screen = keyof typeof theme.screens;
interface Props {
isMobile: boolean;
screenSize: Screen;
width: number;
}
export const useScreenDimensions = (): Props => {
const { width } = useResize();
const isMobile = width < parseInt(theme.screens.md);
const screenSize = Object.entries(theme.screens).reduce(
(agg: Screen, entry) => {
if (width > parseInt(entry[1])) {
agg = entry[0] as Screen;
}
return agg;
},
'xs'
);
return useMemo(
() => ({
width,
isMobile: width < parseInt(theme.screens.md),
screenSize: Object.entries(theme.screens).reduce((agg: Screen, entry) => {
if (width > parseInt(entry[1])) {
agg = entry[0] as Screen;
}
return agg;
}, 'xs'),
isMobile,
screenSize,
}),
[width]
[isMobile, screenSize]
);
};

View File

@ -6,6 +6,7 @@ module.exports = {
lg: '960px',
xl: '1280px',
xxl: '1536px',
xxxl: '1800px',
},
colors: {
transparent: 'transparent',