chore(trading): grid cell fixes (#2986)

This commit is contained in:
Matthew Russell 2023-02-24 06:38:45 -08:00 committed by GitHub
parent 94509a29c5
commit 56b5214dbf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 163 additions and 118 deletions

View File

@ -70,7 +70,6 @@ export const OrderbookRow = React.memo(
relativeAsk={cumulativeRelativeAsk} relativeAsk={cumulativeRelativeAsk}
relativeBid={cumulativeRelativeBid} relativeBid={cumulativeRelativeBid}
indicativeVolume={indicativeVolume} indicativeVolume={indicativeVolume}
className="pr-4 text-black dark:text-white"
/> />
</> </>
); );

View File

@ -201,26 +201,8 @@ const OrderbookDebugInfo = ({
midPrice?: string; midPrice?: string;
}) => ( }) => (
<Fragment> <Fragment>
<div <div className="absolute top-1/2 left-0 border-t border-t-black w-full" />
style={{ <div className="text-xs p-2 bg-black/80 text-white absolute left-0 bottom-6 font-mono">
position: 'absolute',
top: '50%',
left: '0',
borderTop: '1px solid rgba(255,0,0,0.5)',
background: 'black',
width: '100%',
transform: 'translateY(-50%)',
}}
></div>
<div
className="absolute left-0 bottom-0 font-mono"
style={{
fontSize: '10px',
color: '#FFF',
background: '#000',
padding: '2px',
}}
>
<pre> <pre>
{JSON.stringify( {JSON.stringify(
{ {
@ -493,12 +475,7 @@ export const Orderbook = ({
const tableBody = const tableBody =
data && data.length !== 0 ? ( data && data.length !== 0 ? (
<div <div className="grid grid-cols-4 gap-1 text-right auto-rows-[17px]">
className="grid grid-cols-4 gap-[0.3125rem] text-right"
style={{
gridAutoRows: '17px',
}}
>
{data.map((data, i) => ( {data.map((data, i) => (
<OrderbookRow <OrderbookRow
key={data.price} key={data.price}
@ -559,14 +536,13 @@ export const Orderbook = ({
onDoubleClick={() => setDebug(!debug)} onDoubleClick={() => setDebug(!debug)}
> >
<div <div
className="absolute top-0 grid grid-cols-4 gap-2 text-right border-b pt-2 bg-white dark:bg-black z-10 border-default w-full" className="absolute top-0 grid grid-cols-4 auto-rows-[17px] gap-2 text-right border-b pt-2 bg-white dark:bg-black z-10 border-default w-full"
style={{ gridAutoRows: '17px' }}
ref={headerElement} ref={headerElement}
> >
<div>{t('Bid vol')}</div> <div>{t('Bid vol')}</div>
<div>{t('Ask vol')}</div> <div>{t('Ask vol')}</div>
<div>{t('Price')}</div> <div>{t('Price')}</div>
<div className="pr-[2px] whitespace-nowrap overflow-hidden text-ellipsis"> <div className="pr-1 whitespace-nowrap overflow-hidden text-ellipsis">
{t('Cumulative vol')} {t('Cumulative vol')}
</div> </div>
</div> </div>
@ -605,8 +581,7 @@ export const Orderbook = ({
)} )}
</div> </div>
<div <div
className="absolute bottom-0 grid grid-cols-4 gap-2 border-t-[1px] border-default mt-2 z-10 bg-white dark:bg-black w-full" className="absolute bottom-0 grid grid-cols-4 gap-2 border-t border-default mt-2 z-10 bg-white dark:bg-black w-full"
style={{ gridAutoRows: '20px' }}
ref={footerElement} ref={footerElement}
> >
<div className="col-span-2"> <div className="col-span-2">

View File

@ -323,6 +323,7 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
field="realisedPNL" field="realisedPNL"
type="rightAligned" type="rightAligned"
cellClassRules={signedNumberCssClassRules} cellClassRules={signedNumberCssClassRules}
cellClass="text-right font-mono"
filter="agNumberColumnFilter" filter="agNumberColumnFilter"
valueGetter={({ valueGetter={({
data, data,
@ -347,6 +348,7 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
field="unrealisedPNL" field="unrealisedPNL"
type="rightAligned" type="rightAligned"
cellClassRules={signedNumberCssClassRules} cellClassRules={signedNumberCssClassRules}
cellClass="text-right font-mono"
filter="agNumberColumnFilter" filter="agNumberColumnFilter"
valueGetter={({ valueGetter={({
data, data,

View File

@ -1,7 +1,6 @@
import React from 'react'; import { memo } from 'react';
import type { ICellRendererParams } from 'ag-grid-community';
import classNames from 'classnames';
import { BID_COLOR, ASK_COLOR } from './vol-cell'; import { BID_COLOR, ASK_COLOR } from './vol-cell';
import { NumericCell } from './numeric-cell';
import { addDecimalsFormatNumber } from '../format'; import { addDecimalsFormatNumber } from '../format';
export interface CumulativeVolProps { export interface CumulativeVolProps {
@ -15,11 +14,7 @@ export interface CumulativeVolProps {
positionDecimalPlaces: number; positionDecimalPlaces: number;
} }
export interface ICumulativeVolCellProps extends ICellRendererParams { export const CumulativeVol = memo(
value: CumulativeVolProps;
}
export const CumulativeVol = React.memo(
({ ({
relativeAsk, relativeAsk,
relativeBid, relativeBid,
@ -27,7 +22,6 @@ export const CumulativeVol = React.memo(
bid, bid,
indicativeVolume, indicativeVolume,
testId, testId,
className,
positionDecimalPlaces, positionDecimalPlaces,
}: CumulativeVolProps) => { }: CumulativeVolProps) => {
const askBar = relativeAsk ? ( const askBar = relativeAsk ? (
@ -40,7 +34,7 @@ export const CumulativeVol = React.memo(
backgroundColor: ASK_COLOR, backgroundColor: ASK_COLOR,
opacity: 0.6, opacity: 0.6,
}} }}
></div> />
) : null; ) : null;
const bidBar = relativeBid ? ( const bidBar = relativeBid ? (
<div <div
@ -53,25 +47,48 @@ export const CumulativeVol = React.memo(
backgroundColor: BID_COLOR, backgroundColor: BID_COLOR,
opacity: 0.6, opacity: 0.6,
}} }}
></div> />
) : null; ) : null;
const volume = indicativeVolume ? ( const volume = indicativeVolume ? (
<span className="relative"> <span>
({addDecimalsFormatNumber(indicativeVolume, positionDecimalPlaces ?? 0)} (
<NumericCell
value={Number(indicativeVolume)}
valueFormatted={addDecimalsFormatNumber(
indicativeVolume,
positionDecimalPlaces ?? 0
)}
/>
) )
</span> </span>
) : ( ) : (
<span className="relative"> <span>
{ask ? addDecimalsFormatNumber(ask, positionDecimalPlaces ?? 0) : null} {ask ? (
<NumericCell
value={ask}
valueFormatted={addDecimalsFormatNumber(
ask,
positionDecimalPlaces ?? 0
)}
/>
) : null}
{ask && bid ? '/' : null} {ask && bid ? '/' : null}
{bid ? addDecimalsFormatNumber(bid, positionDecimalPlaces ?? 0) : null} {bid ? (
<NumericCell
value={ask}
valueFormatted={addDecimalsFormatNumber(
bid,
positionDecimalPlaces ?? 0
)}
/>
) : null}
</span> </span>
); );
return ( return (
<div <div
className={classNames('h-full relative', className)} className="relative font-mono pr-1"
data-testid={testId || 'cumulative-vol'} data-testid={testId || 'cumulative-vol'}
> >
{askBar} {askBar}
@ -83,9 +100,3 @@ export const CumulativeVol = React.memo(
); );
CumulativeVol.displayName = 'CumulativeVol'; CumulativeVol.displayName = 'CumulativeVol';
export const CumulativeVolCell = ({ value }: ICumulativeVolCellProps) => (
<CumulativeVol {...value} />
);
CumulativeVolCell.displayName = 'CumulativeVolCell';

View File

@ -8,3 +8,4 @@ export * from './summary-rows';
export * from './vol-cell'; export * from './vol-cell';
export * from './set-filter'; export * from './set-filter';
export * from './date-range-filter'; export * from './date-range-filter';
export * from './numeric-cell';

View File

@ -0,0 +1,31 @@
import { render, screen } from '@testing-library/react';
import '@testing-library/jest-dom/extend-expect';
import { NumericCell } from './numeric-cell';
describe('NumericCell', () => {
const testId = 'cell';
it('Displays formatted value', () => {
const valueFormatted = '111.00';
render(
<NumericCell
value={111}
valueFormatted={valueFormatted}
testId={testId}
/>
);
expect(screen.getByTestId(testId)).toHaveTextContent(valueFormatted);
expect(screen.getByText('00')).toBeInTheDocument();
expect(screen.getByText('00')).toHaveClass('opacity-60');
});
it('Displays 0', () => {
render(<NumericCell value={0} valueFormatted="0.00" testId={testId} />);
expect(screen.getByTestId(testId)).toHaveTextContent('0.00');
});
it('Displays - if value is not a number', () => {
render(<NumericCell value={null} valueFormatted="" testId={testId} />);
expect(screen.getByTestId(testId)).toHaveTextContent('-');
});
});

View File

@ -0,0 +1,44 @@
import { forwardRef } from 'react';
import { getDecimalSeparator, isNumeric } from '../format';
interface NumericCellProps {
value: number | bigint | null | undefined;
valueFormatted: string;
testId?: string;
}
/**
* Renders a numeric value in a consistent way for data grid
* use, right aligned, monospace and decimals deemphasised
*/
export const NumericCell = forwardRef<HTMLSpanElement, NumericCellProps>(
({ value, valueFormatted, testId }, ref) => {
if (!isNumeric(value)) {
return (
<span ref={ref} data-testid={testId}>
-
</span>
);
}
const decimalSeparator = getDecimalSeparator();
const valueSplit: string[] = decimalSeparator
? valueFormatted.split(decimalSeparator).map((v) => `${v}`)
: [`${value}`];
return (
<span
ref={ref}
className="font-mono relative text-black dark:text-white whitespace-nowrap overflow-hidden text-ellipsis text-right rtl-dir"
data-testid={testId}
title={valueFormatted}
>
{valueSplit[0]}
{valueSplit[1] ? decimalSeparator : null}
{valueSplit[1] ? (
<span className="opacity-60">{valueSplit[1]}</span>
) : null}
</span>
);
}
);

View File

@ -1,15 +1,15 @@
import { render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import '@testing-library/jest-dom/extend-expect'; import userEvent from '@testing-library/user-event';
import * as React from 'react';
import { PriceCell } from './price-cell'; import { PriceCell } from './price-cell';
describe('<PriceCell />', () => { describe('PriceCell', () => {
it('Displays formatted value', () => { it('Displays formatted value', () => {
render(<PriceCell value={100} valueFormatted="100.00" />); render(<PriceCell value={100} valueFormatted="100.00" />);
expect(screen.getByTestId('price')).toHaveTextContent('100.00'); expect(screen.getByTestId('price')).toHaveTextContent('100.00');
expect(screen.getByTestId('price')).toHaveAttribute('title', '100.00'); expect(screen.getByTestId('price')).toHaveAttribute('title', '100.00');
}); });
it('Displays 0', () => { it('Displays 0', () => {
render(<PriceCell value={0} valueFormatted="0.00" />); render(<PriceCell value={0} valueFormatted="0.00" />);
expect(screen.getByTestId('price')).toHaveTextContent('0.00'); expect(screen.getByTestId('price')).toHaveTextContent('0.00');
@ -19,4 +19,14 @@ describe('<PriceCell />', () => {
render(<PriceCell value={null} valueFormatted="" />); render(<PriceCell value={null} valueFormatted="" />);
expect(screen.getByTestId('price')).toHaveTextContent('-'); expect(screen.getByTestId('price')).toHaveTextContent('-');
}); });
it('can be clicked if given an onClick handler', async () => {
const mockOnClick = jest.fn();
const value = 100;
render(
<PriceCell value={value} valueFormatted="100.00" onClick={mockOnClick} />
);
await userEvent.click(screen.getByTestId('price'));
expect(mockOnClick).toHaveBeenCalledWith(value);
});
}); });

View File

@ -1,5 +1,6 @@
import { memo, forwardRef } from 'react'; import { memo, forwardRef } from 'react';
import { getDecimalSeparator, isNumeric } from '../format'; import { isNumeric } from '../format';
import { NumericCell } from './numeric-cell';
export interface IPriceCellProps { export interface IPriceCellProps {
value: number | bigint | null | undefined; value: number | bigint | null | undefined;
valueFormatted: string; valueFormatted: string;
@ -17,41 +18,23 @@ export const PriceCell = memo(
</span> </span>
); );
} }
const decimalSeparator = getDecimalSeparator();
const valueSplit: string[] = decimalSeparator
? valueFormatted.split(decimalSeparator).map((v) => `${v}`)
: [`${value}`];
return onClick ? ( return onClick ? (
<button <button
onClick={() => onClick(value)} onClick={() => onClick(value)}
className="hover:dark:bg-neutral-800 hover:bg-neutral-200" className="hover:dark:bg-neutral-800 hover:bg-neutral-200 text-right"
> >
<span <NumericCell
ref={ref} value={value}
className="font-mono relative text-black dark:text-white whitespace-nowrap overflow-hidden text-ellipsis text-right rtl-dir" valueFormatted={valueFormatted}
data-testid={testId || 'price'} testId={testId || 'price'}
title={valueFormatted} />
>
{valueSplit[0]}
{valueSplit[1] ? decimalSeparator : null}
{valueSplit[1] ? (
<span className="opacity-60">{valueSplit[1]}</span>
) : null}
</span>
</button> </button>
) : ( ) : (
<span <NumericCell
ref={ref} value={value}
className="font-mono relative text-black dark:text-white whitespace-nowrap overflow-hidden text-ellipsis text-right rtl-dir" valueFormatted={valueFormatted}
data-testid={testId || 'price'} testId={testId || 'price'}
title={valueFormatted} />
>
{valueSplit[0]}
{valueSplit[1] ? decimalSeparator : null}
{valueSplit[1] ? (
<span className="opacity-60">{valueSplit[1]}</span>
) : null}
</span>
); );
} }
) )

View File

@ -1,8 +1,8 @@
import React from 'react'; import React from 'react';
import type { ICellRendererParams } from 'ag-grid-community'; import type { ICellRendererParams } from 'ag-grid-community';
import { PriceCell } from './price-cell';
import classNames from 'classnames'; import classNames from 'classnames';
import * as tailwind from '@vegaprotocol/tailwindcss-config'; import * as tailwind from '@vegaprotocol/tailwindcss-config';
import { NumericCell } from './numeric-cell';
export enum VolumeType { export enum VolumeType {
bid, bid,
@ -43,17 +43,11 @@ export const Vol = React.memo(
backgroundColor: type === VolumeType.bid ? BID_COLOR : ASK_COLOR, backgroundColor: type === VolumeType.bid ? BID_COLOR : ASK_COLOR,
opacity: type === VolumeType.bid ? 0.6 : 0.6, opacity: type === VolumeType.bid ? 0.6 : 0.6,
}} }}
></div> />
<PriceCell value={value} valueFormatted={valueFormatted} /> <NumericCell value={value} valueFormatted={valueFormatted} />
</div> </div>
); );
} }
); );
Vol.displayName = 'Vol'; Vol.displayName = 'Vol';
export const VolCell = ({ value, valueFormatted }: IVolCellProps) => (
<Vol value={value} {...valueFormatted} />
);
VolCell.displayName = 'VolCell';

View File

@ -1,16 +1,20 @@
import type { AgGridReact } from 'ag-grid-react'; import type { AgGridReact } from 'ag-grid-react';
import { AgGridColumn } from 'ag-grid-react'; import { AgGridColumn } from 'ag-grid-react';
import { forwardRef } from 'react'; import { forwardRef } from 'react';
import type { VegaICellRendererParams } from '@vegaprotocol/ui-toolkit'; import type {
VegaICellRendererParams,
VegaValueFormatterParams,
} from '@vegaprotocol/ui-toolkit';
import { AgGridDynamic as AgGrid } from '@vegaprotocol/ui-toolkit'; import { AgGridDynamic as AgGrid } from '@vegaprotocol/ui-toolkit';
import { import {
addDecimal, addDecimal,
addDecimalsFormatNumber, addDecimalsFormatNumber,
getDateTimeFormat, getDateTimeFormat,
NumericCell,
t, t,
} from '@vegaprotocol/react-helpers'; } from '@vegaprotocol/react-helpers';
import type { IDatasource, IGetRowsParams } from 'ag-grid-community'; import type { IDatasource, IGetRowsParams } from 'ag-grid-community';
import type { CellClassParams, ValueFormatterParams } from 'ag-grid-community'; import type { CellClassParams } from 'ag-grid-community';
import type { AgGridReactProps } from 'ag-grid-react'; import type { AgGridReactProps } from 'ag-grid-react';
import type { Trade } from './trades-data-provider'; import type { Trade } from './trades-data-provider';
import { Side } from '@vegaprotocol/types'; import { Side } from '@vegaprotocol/types';
@ -44,21 +48,15 @@ interface Props extends AgGridReactProps {
onClick?: (price?: string) => void; onClick?: (price?: string) => void;
} }
type TradesTableValueFormatterParams = Omit<
ValueFormatterParams,
'data' | 'value'
> & {
data: Trade | null;
};
export const TradesTable = forwardRef<AgGridReact, Props>((props, ref) => { export const TradesTable = forwardRef<AgGridReact, Props>((props, ref) => {
return ( return (
<AgGrid <AgGrid
style={{ width: '100%', height: '100%' }} style={{ width: '100%', height: '100%', background: 'red' }}
overlayNoRowsTemplate={t('No trades')} overlayNoRowsTemplate={t('No trades')}
getRowId={({ data }) => data.id} getRowId={({ data }) => data.id}
ref={ref} ref={ref}
defaultColDef={{ defaultColDef={{
flex: 1,
resizable: true, resizable: true,
}} }}
{...props} {...props}
@ -72,10 +70,8 @@ export const TradesTable = forwardRef<AgGridReact, Props>((props, ref) => {
valueFormatter={({ valueFormatter={({
value, value,
data, data,
}: TradesTableValueFormatterParams & { }: VegaValueFormatterParams<Trade, 'price'>) => {
value: Trade['price']; if (!value || !data?.market) {
}) => {
if (!data?.market) {
return null; return null;
} }
return addDecimalsFormatNumber(value, data.market.decimalPlaces); return addDecimalsFormatNumber(value, data.market.decimalPlaces);
@ -110,10 +106,8 @@ export const TradesTable = forwardRef<AgGridReact, Props>((props, ref) => {
valueFormatter={({ valueFormatter={({
value, value,
data, data,
}: TradesTableValueFormatterParams & { }: VegaValueFormatterParams<Trade, 'size'>) => {
value: Trade['size']; if (!value || !data?.market) {
}) => {
if (!data?.market) {
return null; return null;
} }
return addDecimalsFormatNumber( return addDecimalsFormatNumber(
@ -121,16 +115,17 @@ export const TradesTable = forwardRef<AgGridReact, Props>((props, ref) => {
data.market.positionDecimalPlaces data.market.positionDecimalPlaces
); );
}} }}
cellRenderer={NumericCell}
/> />
<AgGridColumn <AgGridColumn
headerName={t('Created at')} headerName={t('Created at')}
field="createdAt" field="createdAt"
type="rightAligned"
width={170} width={170}
cellClass="text-right"
valueFormatter={({ valueFormatter={({
value, value,
}: TradesTableValueFormatterParams & { }: VegaValueFormatterParams<Trade, 'createdAt'>) => {
value: Trade['createdAt'];
}) => {
return value && getDateTimeFormat().format(new Date(value)); return value && getDateTimeFormat().format(new Date(value));
}} }}
/> />