chore(trading): replace successor cols with market code cell (#4722)
This commit is contained in:
parent
ccbe34e172
commit
7f5e8ebb15
@ -1,4 +1,4 @@
|
||||
import { act, render, screen, within, waitFor } from '@testing-library/react';
|
||||
import { act, render, screen, within } from '@testing-library/react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { Closed } from './closed';
|
||||
import { MarketStateMapping, PropertyKeyType } from '@vegaprotocol/types';
|
||||
@ -10,55 +10,21 @@ import type {
|
||||
OracleSpecDataConnectionQuery,
|
||||
MarketsDataQuery,
|
||||
MarketsQuery,
|
||||
SuccessorMarketIdsQuery,
|
||||
} from '@vegaprotocol/markets';
|
||||
import {
|
||||
OracleSpecDataConnectionDocument,
|
||||
MarketsDataDocument,
|
||||
MarketsDocument,
|
||||
SuccessorMarketIdsDocument,
|
||||
} from '@vegaprotocol/markets';
|
||||
import type { VegaWalletContextShape } from '@vegaprotocol/wallet';
|
||||
import { VegaWalletContext } from '@vegaprotocol/wallet';
|
||||
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
|
||||
import { FLAGS } from '@vegaprotocol/environment';
|
||||
import {
|
||||
createMarketFragment,
|
||||
marketsQuery,
|
||||
marketsDataQuery,
|
||||
createMarketsDataFragment,
|
||||
} from '@vegaprotocol/mock';
|
||||
import type { FeatureFlags } from '@vegaprotocol/environment';
|
||||
|
||||
jest.mock('@vegaprotocol/markets', () => ({
|
||||
...jest.requireActual('@vegaprotocol/markets'),
|
||||
useSuccessorMarket: (marketId: string) =>
|
||||
marketId === 'include-0'
|
||||
? {
|
||||
data: {
|
||||
id: 'successorMarketID',
|
||||
state: 'STATE_ACTIVE',
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
name: 'Successor Market Name',
|
||||
code: 'SuccessorCode',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
: { data: undefined },
|
||||
}));
|
||||
|
||||
jest.mock('@vegaprotocol/environment', () => {
|
||||
const actual = jest.requireActual('@vegaprotocol/environment');
|
||||
return {
|
||||
...actual,
|
||||
FLAGS: {
|
||||
...actual.FLAGS,
|
||||
SUCCESSOR_MARKETS: true,
|
||||
} as FeatureFlags,
|
||||
};
|
||||
});
|
||||
|
||||
describe('Closed', () => {
|
||||
let originalNow: typeof Date.now;
|
||||
@ -218,10 +184,8 @@ describe('Closed', () => {
|
||||
const headers = screen.getAllByRole('columnheader');
|
||||
const expectedHeaders = [
|
||||
'Market',
|
||||
'Description',
|
||||
'Status',
|
||||
'Settlement date',
|
||||
'Successor market',
|
||||
'Best bid',
|
||||
'Best offer',
|
||||
'Mark price',
|
||||
@ -235,10 +199,8 @@ describe('Closed', () => {
|
||||
const cells = screen.getAllByRole('gridcell');
|
||||
const expectedValues = [
|
||||
market.tradableInstrument.instrument.code,
|
||||
market.tradableInstrument.instrument.name,
|
||||
MarketStateMapping[market.state],
|
||||
'3 days ago',
|
||||
'-',
|
||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
addDecimalsFormatNumber(marketsData.bestBidPrice, market.decimalPlaces),
|
||||
addDecimalsFormatNumber(
|
||||
@ -340,43 +302,26 @@ describe('Closed', () => {
|
||||
.getAllByRole('gridcell')
|
||||
.filter((cell) => cell.getAttribute('col-id') === 'code')
|
||||
.map((cell) => {
|
||||
const marketId = within(cell)
|
||||
.getByTestId('market-code')
|
||||
.getAttribute('data-market-id');
|
||||
return marketId;
|
||||
const marketCode = within(cell).getByTestId('stack-cell-primary');
|
||||
return marketCode.textContent;
|
||||
});
|
||||
expect(cells).toEqual(expectedRows.map((m) => m.node.id));
|
||||
expect(cells).toEqual(
|
||||
expectedRows.map((m) => m.node.tradableInstrument.instrument.code)
|
||||
);
|
||||
});
|
||||
|
||||
it('successor marked should be visible', async () => {
|
||||
const mixedMarkets = [
|
||||
const marketsWithSuccessorID = [
|
||||
{
|
||||
__typename: 'MarketEdge' as const,
|
||||
node: createMarketFragment({
|
||||
id: 'include-0',
|
||||
state: MarketState.STATE_SETTLED,
|
||||
successorMarketID: 'successor',
|
||||
}),
|
||||
},
|
||||
{
|
||||
__typename: 'MarketEdge' as const,
|
||||
node: {
|
||||
...createMarketFragment({
|
||||
id: 'successorMarketID',
|
||||
state: MarketState.STATE_ACTIVE,
|
||||
}),
|
||||
tradableInstrument: {
|
||||
...createMarketFragment().tradableInstrument,
|
||||
instrument: {
|
||||
...createMarketFragment().tradableInstrument.instrument,
|
||||
id: 'successorAssset',
|
||||
name: 'Successor Market Name',
|
||||
code: 'SuccessorCode',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
const mixedMarketsMock: MockedResponse<MarketsQuery> = {
|
||||
const mockWithSuccessors: MockedResponse<MarketsQuery> = {
|
||||
request: {
|
||||
query: MarketsDocument,
|
||||
},
|
||||
@ -384,42 +329,17 @@ describe('Closed', () => {
|
||||
data: {
|
||||
marketsConnection: {
|
||||
__typename: 'MarketConnection',
|
||||
edges: mixedMarkets,
|
||||
edges: marketsWithSuccessorID,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const successorMarketsMock: MockedResponse<SuccessorMarketIdsQuery> = {
|
||||
request: {
|
||||
query: SuccessorMarketIdsDocument,
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
marketsConnection: {
|
||||
__typename: 'MarketConnection',
|
||||
edges: [
|
||||
{
|
||||
node: {
|
||||
id: 'include-0',
|
||||
successorMarketID: 'successorMarketID',
|
||||
parentMarketID: '',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
await act(() => {
|
||||
|
||||
await act(async () => {
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<MockedProvider
|
||||
mocks={[
|
||||
mixedMarketsMock,
|
||||
marketsDataMock,
|
||||
oracleDataMock,
|
||||
successorMarketsMock,
|
||||
]}
|
||||
mocks={[mockWithSuccessors, marketsDataMock, oracleDataMock]}
|
||||
>
|
||||
<VegaWalletContext.Provider
|
||||
value={{ pubKey } as VegaWalletContextShape}
|
||||
@ -431,122 +351,15 @@ describe('Closed', () => {
|
||||
);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByRole('button', { name: /^SuccessorCode/ })
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
expect(
|
||||
screen.getByRole('columnheader', {
|
||||
name: (_name, element) =>
|
||||
element.getAttribute('col-id') === 'successorMarket',
|
||||
})
|
||||
).toBeInTheDocument();
|
||||
screen
|
||||
.getAllByRole('gridcell', {
|
||||
name: (_name, element) =>
|
||||
element.getAttribute('col-id') === 'successorMarket',
|
||||
})
|
||||
.forEach((element) => {
|
||||
expect(element.querySelector('[title="Future"]')?.textContent).toEqual(
|
||||
'Futr'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('feature flag should hide successors', async () => {
|
||||
const mockedFlags = jest.mocked(FLAGS);
|
||||
mockedFlags.SUCCESSOR_MARKETS = false;
|
||||
|
||||
const mixedMarkets = [
|
||||
{
|
||||
__typename: 'MarketEdge' as const,
|
||||
node: createMarketFragment({
|
||||
id: 'include-0',
|
||||
state: MarketState.STATE_SETTLED,
|
||||
}),
|
||||
},
|
||||
{
|
||||
__typename: 'MarketEdge' as const,
|
||||
node: {
|
||||
...createMarketFragment({
|
||||
id: 'successorMarketID',
|
||||
state: MarketState.STATE_ACTIVE,
|
||||
}),
|
||||
tradableInstrument: {
|
||||
...createMarketFragment().tradableInstrument,
|
||||
instrument: {
|
||||
...createMarketFragment().tradableInstrument.instrument,
|
||||
id: 'successorAssset',
|
||||
name: 'Successor Market Name',
|
||||
code: 'SuccessorCode',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
const mixedMarketsMock: MockedResponse<MarketsQuery> = {
|
||||
request: {
|
||||
query: MarketsDocument,
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
marketsConnection: {
|
||||
__typename: 'MarketConnection',
|
||||
edges: mixedMarkets,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const successorMarketsMock: MockedResponse<SuccessorMarketIdsQuery> = {
|
||||
request: {
|
||||
query: SuccessorMarketIdsDocument,
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
marketsConnection: {
|
||||
__typename: 'MarketConnection',
|
||||
edges: [
|
||||
{
|
||||
node: {
|
||||
id: 'include-0',
|
||||
successorMarketID: 'successorMarketID',
|
||||
parentMarketID: '',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<MockedProvider
|
||||
mocks={[
|
||||
mixedMarketsMock,
|
||||
marketsDataMock,
|
||||
oracleDataMock,
|
||||
successorMarketsMock,
|
||||
]}
|
||||
>
|
||||
<VegaWalletContext.Provider
|
||||
value={{ pubKey } as VegaWalletContextShape}
|
||||
>
|
||||
<Closed />
|
||||
</VegaWalletContext.Provider>
|
||||
</MockedProvider>
|
||||
</MemoryRouter>
|
||||
const container = within(
|
||||
document.querySelector('.ag-center-cols-container') as HTMLElement
|
||||
);
|
||||
const cell = container.getAllByRole('gridcell', {
|
||||
name: (_name, element) => element.getAttribute('col-id') === 'code',
|
||||
})[0];
|
||||
|
||||
expect(within(cell).getByTestId('stack-cell-secondary')).toHaveTextContent(
|
||||
'PRNT'
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByRole('columnheader', {
|
||||
name: (_name, element) =>
|
||||
element.getAttribute('col-id') === 'settlementDate',
|
||||
})
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
screen.getAllByRole('columnheader').forEach((element) => {
|
||||
expect(element.getAttribute('col-id')).not.toEqual('successorMarket');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -4,13 +4,10 @@ import type {
|
||||
VegaICellRendererParams,
|
||||
VegaValueFormatterParams,
|
||||
} from '@vegaprotocol/datagrid';
|
||||
import {
|
||||
AgGridLazy as AgGrid,
|
||||
COL_DEFS,
|
||||
MarketNameCell,
|
||||
} from '@vegaprotocol/datagrid';
|
||||
import { AgGridLazy as AgGrid, COL_DEFS } from '@vegaprotocol/datagrid';
|
||||
import { useMemo } from 'react';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import type { ProductType } from '@vegaprotocol/types';
|
||||
import { MarketState, MarketStateMapping } from '@vegaprotocol/types';
|
||||
import {
|
||||
addDecimalsFormatNumber,
|
||||
@ -20,17 +17,13 @@ import type {
|
||||
DataSourceFilterFragment,
|
||||
MarketMaybeWithData,
|
||||
} from '@vegaprotocol/markets';
|
||||
import {
|
||||
MarketActionsDropdown,
|
||||
closedMarketsWithDataProvider,
|
||||
} from '@vegaprotocol/markets';
|
||||
import { closedMarketsWithDataProvider } from '@vegaprotocol/markets';
|
||||
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
|
||||
import type { ColDef } from 'ag-grid-community';
|
||||
import { FLAGS } from '@vegaprotocol/environment';
|
||||
import { SettlementDateCell } from './settlement-date-cell';
|
||||
import { SettlementPriceCell } from './settlement-price-cell';
|
||||
import { useDataProvider } from '@vegaprotocol/data-provider';
|
||||
import { SuccessorMarketRenderer } from './successor-market-cell';
|
||||
import { MarketActionsDropdown } from './market-table-actions';
|
||||
import { MarketCodeCell } from './market-code-cell';
|
||||
|
||||
type SettlementAsset =
|
||||
MarketMaybeWithData['tradableInstrument']['instrument']['product']['settlementAsset'];
|
||||
@ -51,7 +44,9 @@ interface Row {
|
||||
setlementDataSourceFilter: DataSourceFilterFragment | undefined;
|
||||
tradingTerminationOracleId: string;
|
||||
settlementAsset: SettlementAsset;
|
||||
productType: string;
|
||||
productType: ProductType | undefined;
|
||||
successorMarketID: string | null | undefined;
|
||||
parentMarketID: string | null | undefined;
|
||||
}
|
||||
|
||||
export const Closed = () => {
|
||||
@ -95,16 +90,19 @@ export const Closed = () => {
|
||||
tradingTerminationOracleId:
|
||||
instrument.product.dataSourceSpecForTradingTermination.id,
|
||||
settlementAsset: instrument.product.settlementAsset,
|
||||
productType: instrument.product.__typename || '',
|
||||
productType: instrument.product.__typename,
|
||||
successorMarketID: market.successorMarketID,
|
||||
parentMarketID: market.parentMarketID,
|
||||
};
|
||||
|
||||
return row;
|
||||
});
|
||||
return (
|
||||
<div className="h-full relative">
|
||||
<ClosedMarketsDataGrid rowData={rowData} error={error} />
|
||||
</div>
|
||||
);
|
||||
|
||||
return <ClosedMarketsDataGrid rowData={rowData} error={error} />;
|
||||
};
|
||||
|
||||
const components = {
|
||||
MarketCodeCell,
|
||||
};
|
||||
|
||||
const ClosedMarketsDataGrid = ({
|
||||
@ -117,15 +115,11 @@ const ClosedMarketsDataGrid = ({
|
||||
const openAssetDialog = useAssetDetailsDialogStore((store) => store.open);
|
||||
|
||||
const colDefs = useMemo(() => {
|
||||
const cols: ColDef[] = compact([
|
||||
return [
|
||||
{
|
||||
headerName: t('Market'),
|
||||
field: 'code',
|
||||
cellRenderer: 'MarketNameCell',
|
||||
},
|
||||
{
|
||||
headerName: t('Description'),
|
||||
field: 'name',
|
||||
cellRenderer: 'MarketCodeCell',
|
||||
},
|
||||
{
|
||||
headerName: t('Status'),
|
||||
@ -176,12 +170,6 @@ const ClosedMarketsDataGrid = ({
|
||||
},
|
||||
},
|
||||
},
|
||||
FLAGS.SUCCESSOR_MARKETS && {
|
||||
headerName: t('Successor market'),
|
||||
field: 'id',
|
||||
colId: 'successorMarket',
|
||||
cellRenderer: 'SuccessorMarketRenderer',
|
||||
},
|
||||
{
|
||||
headerName: t('Best bid'),
|
||||
field: 'bestBidPrice',
|
||||
@ -263,12 +251,13 @@ const ClosedMarketsDataGrid = ({
|
||||
<MarketActionsDropdown
|
||||
marketId={data.id}
|
||||
assetId={data.settlementAsset.id}
|
||||
successorMarketID={data.successorMarketID}
|
||||
parentMarketID={data.parentMarketID}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
]);
|
||||
return cols;
|
||||
];
|
||||
}, [openAssetDialog]);
|
||||
|
||||
return (
|
||||
@ -276,8 +265,8 @@ const ClosedMarketsDataGrid = ({
|
||||
rowData={rowData}
|
||||
columnDefs={colDefs}
|
||||
getRowId={({ data }) => data.id}
|
||||
components={{ SuccessorMarketRenderer, MarketNameCell }}
|
||||
overlayNoRowsTemplate={error ? error.message : t('No markets')}
|
||||
components={components}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
82
apps/trading/client-pages/markets/market-code-cell.spec.tsx
Normal file
82
apps/trading/client-pages/markets/market-code-cell.spec.tsx
Normal file
@ -0,0 +1,82 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { ProductTypeShortName } from '@vegaprotocol/types';
|
||||
import type { MarketCodeCellProps } from './market-code-cell';
|
||||
import { MarketCodeCell } from './market-code-cell';
|
||||
|
||||
describe('MarketCodeCell', () => {
|
||||
const renderComponent = (props: MarketCodeCellProps) => {
|
||||
return render(<MarketCodeCell {...props} />);
|
||||
};
|
||||
|
||||
it('renders SCCR if the market is a successor', () => {
|
||||
const productType = 'Future';
|
||||
const code = 'code';
|
||||
const props = {
|
||||
value: 'code',
|
||||
data: {
|
||||
productType,
|
||||
parentMarketID: 'foo',
|
||||
successorMarketID: undefined,
|
||||
},
|
||||
} as const;
|
||||
renderComponent(props);
|
||||
expect(screen.getByTestId('stack-cell-primary')).toHaveTextContent(code);
|
||||
expect(screen.getByTestId('stack-cell-secondary')).toHaveTextContent(
|
||||
ProductTypeShortName[productType]
|
||||
);
|
||||
expect(screen.getByTestId('stack-cell-secondary')).toHaveTextContent(
|
||||
'SCCR'
|
||||
);
|
||||
expect(screen.getByTestId('stack-cell-secondary')).not.toHaveTextContent(
|
||||
'PRNT'
|
||||
);
|
||||
});
|
||||
|
||||
it('renders PRNT if the market is a parent', () => {
|
||||
const productType = 'Future';
|
||||
const code = 'code';
|
||||
const props = {
|
||||
value: 'code',
|
||||
data: {
|
||||
productType,
|
||||
parentMarketID: undefined,
|
||||
successorMarketID: 'foo',
|
||||
},
|
||||
} as const;
|
||||
renderComponent(props);
|
||||
expect(screen.getByTestId('stack-cell-primary')).toHaveTextContent(code);
|
||||
expect(screen.getByTestId('stack-cell-secondary')).toHaveTextContent(
|
||||
ProductTypeShortName[productType]
|
||||
);
|
||||
expect(screen.getByTestId('stack-cell-secondary')).toHaveTextContent(
|
||||
'PRNT'
|
||||
);
|
||||
expect(screen.getByTestId('stack-cell-secondary')).not.toHaveTextContent(
|
||||
'SCCR'
|
||||
);
|
||||
});
|
||||
|
||||
it('renders both SCCR and PRNT if the market is both a parent and a successor', () => {
|
||||
const productType = 'Future';
|
||||
const code = 'code';
|
||||
const props = {
|
||||
value: 'code',
|
||||
data: {
|
||||
productType,
|
||||
parentMarketID: 'foo',
|
||||
successorMarketID: 'bar',
|
||||
},
|
||||
} as const;
|
||||
renderComponent(props);
|
||||
expect(screen.getByTestId('stack-cell-primary')).toHaveTextContent(code);
|
||||
expect(screen.getByTestId('stack-cell-secondary')).toHaveTextContent(
|
||||
ProductTypeShortName[productType]
|
||||
);
|
||||
expect(screen.getByTestId('stack-cell-secondary')).toHaveTextContent(
|
||||
'PRNT'
|
||||
);
|
||||
expect(screen.getByTestId('stack-cell-secondary')).toHaveTextContent(
|
||||
'SCCR'
|
||||
);
|
||||
});
|
||||
});
|
51
apps/trading/client-pages/markets/market-code-cell.tsx
Normal file
51
apps/trading/client-pages/markets/market-code-cell.tsx
Normal file
@ -0,0 +1,51 @@
|
||||
import compact from 'lodash/compact';
|
||||
import type { ProductType } from '@vegaprotocol/types';
|
||||
import { ProductTypeMapping, ProductTypeShortName } from '@vegaprotocol/types';
|
||||
import { StackedCell } from '@vegaprotocol/datagrid';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
|
||||
export interface MarketCodeCellProps {
|
||||
value: string | undefined; // market code
|
||||
data: {
|
||||
productType: ProductType | undefined;
|
||||
parentMarketID: string | null | undefined;
|
||||
successorMarketID: string | null | undefined;
|
||||
};
|
||||
}
|
||||
|
||||
export const MarketCodeCell = ({ value, data }: MarketCodeCellProps) => {
|
||||
if (!value || !data || !data.productType) return null;
|
||||
|
||||
const infoSpanClasses =
|
||||
'mr-1 pr-1 uppercase border-r last:pr-0 last:mr-0 last:border-r-0 border-vega-clight-200 dark:border-vega-cdark-200';
|
||||
|
||||
const info = compact([
|
||||
<span
|
||||
className={infoSpanClasses}
|
||||
key="productType"
|
||||
title={ProductTypeMapping[data.productType]}
|
||||
>
|
||||
{ProductTypeShortName[data.productType]}
|
||||
</span>,
|
||||
data.parentMarketID && (
|
||||
<span
|
||||
className={infoSpanClasses}
|
||||
key="successor"
|
||||
title={t('Successor of a market')}
|
||||
>
|
||||
{t('SCCR')}
|
||||
</span>
|
||||
),
|
||||
data.successorMarketID && (
|
||||
<span
|
||||
className={infoSpanClasses}
|
||||
key="parent"
|
||||
title={t('Parent of a market')}
|
||||
>
|
||||
{t('PRNT')}
|
||||
</span>
|
||||
),
|
||||
]);
|
||||
|
||||
return <StackedCell primary={value} secondary={info} />;
|
||||
};
|
35
apps/trading/client-pages/markets/market-list-table.tsx
Normal file
35
apps/trading/client-pages/markets/market-list-table.tsx
Normal file
@ -0,0 +1,35 @@
|
||||
import type { TypedDataAgGrid } from '@vegaprotocol/datagrid';
|
||||
import { AgGridLazy as AgGrid, PriceFlashCell } from '@vegaprotocol/datagrid';
|
||||
import type { MarketMaybeWithData } from '@vegaprotocol/markets';
|
||||
import { useColumnDefs } from './use-column-defs';
|
||||
|
||||
export const getRowId = ({ data }: { data: { id: string } }) => data.id;
|
||||
|
||||
const defaultColDef = {
|
||||
sortable: true,
|
||||
filter: true,
|
||||
filterParams: { buttons: ['reset'] },
|
||||
};
|
||||
|
||||
const components = {
|
||||
PriceFlashCell,
|
||||
};
|
||||
|
||||
type Props = TypedDataAgGrid<MarketMaybeWithData>;
|
||||
|
||||
export const MarketListTable = (props: Props) => {
|
||||
const columnDefs = useColumnDefs();
|
||||
|
||||
return (
|
||||
<AgGrid
|
||||
getRowId={getRowId}
|
||||
defaultColDef={defaultColDef}
|
||||
columnDefs={columnDefs}
|
||||
components={components}
|
||||
rowHeight={45}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default MarketListTable;
|
@ -9,14 +9,21 @@ import {
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { DApp, EXPLORER_MARKET, useLinks } from '@vegaprotocol/environment';
|
||||
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Links, Routes } from '../../pages/client-router';
|
||||
|
||||
export const MarketActionsDropdown = ({
|
||||
marketId,
|
||||
assetId,
|
||||
successorMarketID,
|
||||
parentMarketID,
|
||||
}: {
|
||||
marketId: string;
|
||||
assetId: string;
|
||||
successorMarketID: string | null | undefined;
|
||||
parentMarketID: string | null | undefined;
|
||||
}) => {
|
||||
const navigate = useNavigate();
|
||||
const open = useAssetDetailsDialogStore((store) => store.open);
|
||||
const linkCreator = useLinks(DApp.Explorer);
|
||||
|
||||
@ -42,6 +49,26 @@ export const MarketActionsDropdown = ({
|
||||
<VegaIcon name={VegaIconNames.INFO} size={16} />
|
||||
{t('View settlement asset details')}
|
||||
</TradingDropdownItem>
|
||||
{parentMarketID && (
|
||||
<TradingDropdownItem
|
||||
onClick={() => {
|
||||
navigate(Links[Routes.MARKET](parentMarketID));
|
||||
}}
|
||||
>
|
||||
<VegaIcon name={VegaIconNames.EYE} size={16} />
|
||||
{t('View parent market')}
|
||||
</TradingDropdownItem>
|
||||
)}
|
||||
{successorMarketID && (
|
||||
<TradingDropdownItem
|
||||
onClick={() => {
|
||||
navigate(Links[Routes.MARKET](successorMarketID));
|
||||
}}
|
||||
>
|
||||
<VegaIcon name={VegaIconNames.EYE} size={16} />
|
||||
{t('View successor market')}
|
||||
</TradingDropdownItem>
|
||||
)}
|
||||
</ActionsDropdown>
|
||||
);
|
||||
};
|
@ -6,7 +6,7 @@ import {
|
||||
Tab,
|
||||
TradingAnchorButton,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { Markets } from './markets';
|
||||
import { OpenMarkets } from './open-markets';
|
||||
import { Proposed } from './proposed';
|
||||
import { usePageTitleStore } from '../../stores';
|
||||
import { Closed } from './closed';
|
||||
@ -33,7 +33,7 @@ export const MarketsPage = () => {
|
||||
<div className="h-full my-1 border rounded-sm border-default">
|
||||
<Tabs storageKey="console-markets">
|
||||
<Tab id="open-markets" name={t('Open markets')}>
|
||||
<Markets />
|
||||
<OpenMarkets />
|
||||
</Tab>
|
||||
<Tab
|
||||
id="proposed-markets"
|
||||
|
@ -1,13 +0,0 @@
|
||||
import { MarketsContainer } from '@vegaprotocol/markets';
|
||||
import { useMarketClickHandler } from '../../lib/hooks/use-market-click-handler';
|
||||
import { SuccessorMarketRenderer } from './successor-market-cell';
|
||||
|
||||
export const Markets = () => {
|
||||
const handleOnSelect = useMarketClickHandler();
|
||||
return (
|
||||
<MarketsContainer
|
||||
onSelect={handleOnSelect}
|
||||
SuccessorMarketRenderer={SuccessorMarketRenderer}
|
||||
/>
|
||||
);
|
||||
};
|
63
apps/trading/client-pages/markets/open-markets.tsx
Normal file
63
apps/trading/client-pages/markets/open-markets.tsx
Normal file
@ -0,0 +1,63 @@
|
||||
import { useDataProvider } from '@vegaprotocol/data-provider';
|
||||
import type { MarketMaybeWithData } from '@vegaprotocol/markets';
|
||||
import { marketListProvider } from '@vegaprotocol/markets';
|
||||
import { useEffect } from 'react';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import type { CellClickedEvent } from 'ag-grid-community';
|
||||
import MarketListTable from './market-list-table';
|
||||
import { useMarketClickHandler } from '../../lib/hooks/use-market-click-handler';
|
||||
import { Interval } from '@vegaprotocol/types';
|
||||
import { useYesterday } from '@vegaprotocol/react-helpers';
|
||||
|
||||
const POLLING_TIME = 2000;
|
||||
|
||||
export const OpenMarkets = () => {
|
||||
const handleOnSelect = useMarketClickHandler();
|
||||
const yesterday = useYesterday();
|
||||
const { data, error, reload } = useDataProvider({
|
||||
dataProvider: marketListProvider,
|
||||
variables: {
|
||||
since: new Date(yesterday).toISOString(),
|
||||
interval: Interval.INTERVAL_I1H,
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
reload();
|
||||
}, POLLING_TIME);
|
||||
return () => {
|
||||
clearInterval(interval);
|
||||
};
|
||||
}, [reload]);
|
||||
|
||||
return (
|
||||
<MarketListTable
|
||||
rowData={data}
|
||||
onCellClicked={({
|
||||
data,
|
||||
column,
|
||||
event,
|
||||
}: CellClickedEvent<MarketMaybeWithData>) => {
|
||||
if (!data) return;
|
||||
|
||||
// prevent navigating to the market page if any of the below cells are clicked
|
||||
// event.preventDefault or event.stopPropagation dont seem to apply for aggird
|
||||
const colId = column.getColId();
|
||||
|
||||
if (
|
||||
[
|
||||
'tradableInstrument.instrument.product.settlementAsset.symbol',
|
||||
'market-actions',
|
||||
].includes(colId)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// @ts-ignore metaKey exists
|
||||
handleOnSelect(data.id, event ? event.metaKey : false);
|
||||
}}
|
||||
overlayNoRowsTemplate={error ? error.message : t('No markets')}
|
||||
/>
|
||||
);
|
||||
};
|
46
apps/trading/client-pages/markets/oracle-status.tsx
Normal file
46
apps/trading/client-pages/markets/oracle-status.tsx
Normal file
@ -0,0 +1,46 @@
|
||||
import { useEnvironment } from '@vegaprotocol/environment';
|
||||
import { Icon } from '@vegaprotocol/ui-toolkit';
|
||||
import type { IconName } from '@blueprintjs/icons';
|
||||
import type { Market } from '@vegaprotocol/markets';
|
||||
import {
|
||||
getMatchingOracleProvider,
|
||||
getVerifiedStatusIcon,
|
||||
useOracleProofs,
|
||||
} from '@vegaprotocol/markets';
|
||||
|
||||
export const OracleStatus = ({
|
||||
dataSourceSpecForSettlementData,
|
||||
dataSourceSpecForTradingTermination,
|
||||
}: Pick<
|
||||
Market['tradableInstrument']['instrument']['product'],
|
||||
'dataSourceSpecForSettlementData' | 'dataSourceSpecForTradingTermination'
|
||||
>) => {
|
||||
const { ORACLE_PROOFS_URL } = useEnvironment();
|
||||
const { data: providers } = useOracleProofs(ORACLE_PROOFS_URL);
|
||||
|
||||
if (providers) {
|
||||
const settlementDataProvider = getMatchingOracleProvider(
|
||||
dataSourceSpecForSettlementData.data,
|
||||
providers
|
||||
);
|
||||
const tradingTerminationDataProvider = getMatchingOracleProvider(
|
||||
dataSourceSpecForTradingTermination.data,
|
||||
providers
|
||||
);
|
||||
let maliciousOracleProvider = null;
|
||||
|
||||
if (settlementDataProvider?.oracle.status !== 'GOOD') {
|
||||
maliciousOracleProvider = settlementDataProvider;
|
||||
} else if (tradingTerminationDataProvider?.oracle.status !== 'GOOD') {
|
||||
maliciousOracleProvider = tradingTerminationDataProvider;
|
||||
}
|
||||
|
||||
if (!maliciousOracleProvider) return null;
|
||||
|
||||
const { icon } = getVerifiedStatusIcon(maliciousOracleProvider);
|
||||
|
||||
return <Icon size={3} name={icon as IconName} className="ml-1" />;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
15
apps/trading/client-pages/markets/parent-market-cell.tsx
Normal file
15
apps/trading/client-pages/markets/parent-market-cell.tsx
Normal file
@ -0,0 +1,15 @@
|
||||
import { useMarketsMapProvider } from '@vegaprotocol/markets';
|
||||
|
||||
export const ParentMarketCell = ({
|
||||
value,
|
||||
}: {
|
||||
value: string; // parentMarketId
|
||||
}) => {
|
||||
const { data, loading } = useMarketsMapProvider();
|
||||
|
||||
if (loading) return null;
|
||||
|
||||
if (!data || !data[value]) return <span>-</span>;
|
||||
|
||||
return <div>{data[value].tradableInstrument.instrument.code}</div>;
|
||||
};
|
@ -1,6 +1,10 @@
|
||||
import { ProposalsList } from '@vegaprotocol/proposals';
|
||||
import { SuccessorMarketRenderer } from './successor-market-cell';
|
||||
import { ParentMarketCell } from './parent-market-cell';
|
||||
|
||||
const cellRenderers = {
|
||||
ParentMarketCell,
|
||||
};
|
||||
|
||||
export const Proposed = () => {
|
||||
return <ProposalsList SuccessorMarketRenderer={SuccessorMarketRenderer} />;
|
||||
return <ProposalsList cellRenderers={cellRenderers} />;
|
||||
};
|
||||
|
@ -1,190 +0,0 @@
|
||||
import { render, screen, act, waitFor } from '@testing-library/react';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import type { Market } from '@vegaprotocol/markets';
|
||||
import { SuccessorMarketRenderer } from './successor-market-cell';
|
||||
import {
|
||||
MarketsDocument,
|
||||
SuccessorMarketIdsDocument,
|
||||
} from '@vegaprotocol/markets';
|
||||
import { createMarketFragment } from '@vegaprotocol/mock';
|
||||
|
||||
const mockSuccessorsQuery = [
|
||||
{
|
||||
id: 'market1',
|
||||
parentMarketID: 'parentMarket1',
|
||||
successorMarketID: 'successorMarket1',
|
||||
},
|
||||
{ id: 'market2', parentMarketID: 'parentMarket2' },
|
||||
{ id: 'market3', successorMarketID: 'successorMarket3' },
|
||||
];
|
||||
const parentMarket1 = {
|
||||
id: 'parentMarket1',
|
||||
tradableInstrument: {
|
||||
instrument: { code: 'code parent 1', id: '1' },
|
||||
},
|
||||
} as unknown as Market;
|
||||
const successorMarket1 = {
|
||||
id: 'successorMarket1',
|
||||
tradableInstrument: {
|
||||
instrument: { code: 'code successor 1', id: '2' },
|
||||
},
|
||||
} as unknown as Market;
|
||||
const parentMarket2 = {
|
||||
id: 'parentMarket2',
|
||||
tradableInstrument: {
|
||||
instrument: { code: 'code parent 2', id: '3' },
|
||||
},
|
||||
} as unknown as Market;
|
||||
const successorMarket3 = {
|
||||
id: 'successorMarket3',
|
||||
tradableInstrument: {
|
||||
instrument: { code: 'code successor 3', id: '4' },
|
||||
},
|
||||
} as unknown as Market;
|
||||
|
||||
const mockMarkets = [
|
||||
parentMarket1,
|
||||
successorMarket1,
|
||||
parentMarket2,
|
||||
successorMarket3,
|
||||
];
|
||||
|
||||
const mockClickHandler = jest.fn();
|
||||
jest.mock('../../lib/hooks/use-market-click-handler', () => ({
|
||||
useMarketClickHandler: jest.fn().mockImplementation(() => mockClickHandler),
|
||||
}));
|
||||
|
||||
const marketMock = {
|
||||
request: {
|
||||
query: MarketsDocument,
|
||||
variables: undefined,
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
marketsConnection: {
|
||||
edges: mockMarkets.map((item) => ({
|
||||
node: {
|
||||
...createMarketFragment(item),
|
||||
},
|
||||
})),
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const successorMock = {
|
||||
request: {
|
||||
query: SuccessorMarketIdsDocument,
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
marketsConnection: {
|
||||
edges: mockSuccessorsQuery.map((item) => ({
|
||||
node: {
|
||||
...item,
|
||||
},
|
||||
})),
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const mocks = [marketMock, successorMock];
|
||||
|
||||
describe('SuccessorMarketRenderer', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
it('should properly rendered successor market', async () => {
|
||||
const successorValue = 'market1';
|
||||
render(
|
||||
<MockedProvider mocks={[...mocks]}>
|
||||
<SuccessorMarketRenderer value={successorValue} />
|
||||
</MockedProvider>
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('market-code')).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByText('code successor 1')).toBeInTheDocument();
|
||||
expect(screen.getByText('Futr')).toBeInTheDocument();
|
||||
|
||||
await userEvent.click(screen.getByRole('button'));
|
||||
await waitFor(() => {
|
||||
expect(mockClickHandler).toHaveBeenCalledWith('successorMarket1', false);
|
||||
});
|
||||
});
|
||||
it('should properly rendered parent market', async () => {
|
||||
const successorValue = 'market1';
|
||||
render(
|
||||
<MockedProvider mocks={[...mocks]}>
|
||||
<SuccessorMarketRenderer value={successorValue} parent />
|
||||
</MockedProvider>
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('market-code')).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByText('code parent 1')).toBeInTheDocument();
|
||||
expect(screen.getByText('Futr')).toBeInTheDocument();
|
||||
|
||||
await userEvent.click(screen.getByRole('button'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockClickHandler).toHaveBeenCalledWith('parentMarket1', false);
|
||||
});
|
||||
});
|
||||
it('should properly rendered only parent market', async () => {
|
||||
const successorValue = 'market2';
|
||||
const { rerender } = render(
|
||||
<MockedProvider mocks={[...mocks]}>
|
||||
<SuccessorMarketRenderer value={successorValue} parent />
|
||||
</MockedProvider>
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('market-code')).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByText('code parent 2')).toBeInTheDocument();
|
||||
expect(screen.getByText('Futr')).toBeInTheDocument();
|
||||
await userEvent.click(screen.getByRole('button'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockClickHandler).toHaveBeenCalledWith('parentMarket2', false);
|
||||
});
|
||||
|
||||
rerender(
|
||||
<MockedProvider mocks={[...mocks]}>
|
||||
<SuccessorMarketRenderer value={successorValue} />
|
||||
</MockedProvider>
|
||||
);
|
||||
|
||||
expect(screen.getByText('-')).toBeInTheDocument();
|
||||
});
|
||||
it('should properly rendered only successor market', async () => {
|
||||
const successorValue = 'market3';
|
||||
const { rerender } = render(
|
||||
<MockedProvider mocks={[...mocks]}>
|
||||
<SuccessorMarketRenderer value={successorValue} />
|
||||
</MockedProvider>
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('market-code')).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByText('code successor 3')).toBeInTheDocument();
|
||||
expect(screen.getByText('Futr')).toBeInTheDocument();
|
||||
|
||||
await userEvent.click(screen.getByRole('button'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockClickHandler).toHaveBeenCalledWith('successorMarket3', false);
|
||||
});
|
||||
|
||||
await act(() => {
|
||||
rerender(
|
||||
<MockedProvider mocks={[...mocks]}>
|
||||
<SuccessorMarketRenderer value={successorValue} parent />
|
||||
</MockedProvider>
|
||||
);
|
||||
});
|
||||
expect(screen.getByText('-')).toBeInTheDocument();
|
||||
});
|
||||
});
|
@ -1,41 +0,0 @@
|
||||
import React from 'react';
|
||||
import { MarketNameCell } from '@vegaprotocol/datagrid';
|
||||
import { useDataProvider } from '@vegaprotocol/data-provider';
|
||||
import { marketProvider, useSuccessorMarketIds } from '@vegaprotocol/markets';
|
||||
import { useMarketClickHandler } from '../../lib/hooks/use-market-click-handler';
|
||||
|
||||
export const SuccessorMarketRenderer = ({
|
||||
value,
|
||||
parent,
|
||||
}: {
|
||||
value: string;
|
||||
parent?: boolean;
|
||||
}) => {
|
||||
const successors = useSuccessorMarketIds(value);
|
||||
const onMarketClick = useMarketClickHandler();
|
||||
|
||||
const lookupValue = successors
|
||||
? parent
|
||||
? successors.parentMarketID
|
||||
: successors.successorMarketID
|
||||
: '';
|
||||
|
||||
const { data } = useDataProvider({
|
||||
dataProvider: marketProvider,
|
||||
variables: {
|
||||
marketId: lookupValue || '',
|
||||
},
|
||||
skip: !lookupValue,
|
||||
});
|
||||
|
||||
return data ? (
|
||||
<MarketNameCell
|
||||
value={data.tradableInstrument.instrument.code}
|
||||
data={data}
|
||||
onMarketClick={onMarketClick}
|
||||
productType={data.tradableInstrument.instrument?.product.__typename}
|
||||
/>
|
||||
) : (
|
||||
'-'
|
||||
);
|
||||
};
|
226
apps/trading/client-pages/markets/use-column-defs.tsx
Normal file
226
apps/trading/client-pages/markets/use-column-defs.tsx
Normal file
@ -0,0 +1,226 @@
|
||||
import { useMemo } from 'react';
|
||||
import type { ColDef, ValueFormatterParams } from 'ag-grid-community';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import type {
|
||||
VegaICellRendererParams,
|
||||
VegaValueFormatterParams,
|
||||
VegaValueGetterParams,
|
||||
} from '@vegaprotocol/datagrid';
|
||||
import { COL_DEFS, SetFilter } from '@vegaprotocol/datagrid';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import { addDecimalsFormatNumber, toBigNum } from '@vegaprotocol/utils';
|
||||
import { ButtonLink, Tooltip } from '@vegaprotocol/ui-toolkit';
|
||||
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
|
||||
import type {
|
||||
MarketMaybeWithData,
|
||||
MarketMaybeWithDataAndCandles,
|
||||
} from '@vegaprotocol/markets';
|
||||
import { MarketActionsDropdown } from './market-table-actions';
|
||||
import { calcCandleVolume } from '@vegaprotocol/markets';
|
||||
import { MarketCodeCell } from './market-code-cell';
|
||||
|
||||
const { MarketTradingMode, AuctionTrigger } = Schema;
|
||||
|
||||
export const useColumnDefs = () => {
|
||||
const { open: openAssetDetailsDialog } = useAssetDetailsDialogStore();
|
||||
return useMemo<ColDef[]>(
|
||||
() => [
|
||||
{
|
||||
headerName: t('Market'),
|
||||
field: 'tradableInstrument.instrument.code',
|
||||
flex: 2,
|
||||
cellRenderer: ({
|
||||
value,
|
||||
data,
|
||||
}: VegaICellRendererParams<
|
||||
MarketMaybeWithData,
|
||||
'tradableInstrument.instrument.code'
|
||||
>) => (
|
||||
<MarketCodeCell
|
||||
value={value}
|
||||
data={{
|
||||
productType:
|
||||
data?.tradableInstrument.instrument.product.__typename,
|
||||
successorMarketID: data?.successorMarketID,
|
||||
parentMarketID: data?.parentMarketID,
|
||||
}}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
headerName: t('Description'),
|
||||
field: 'tradableInstrument.instrument.name',
|
||||
flex: 2,
|
||||
},
|
||||
{
|
||||
headerName: t('Trading mode'),
|
||||
field: 'tradingMode',
|
||||
cellRenderer: ({
|
||||
data,
|
||||
}: VegaICellRendererParams<MarketMaybeWithData, 'data'>) => {
|
||||
if (!data?.data) return '-';
|
||||
const { trigger, marketTradingMode } = data.data;
|
||||
|
||||
const withTriggerInfo =
|
||||
marketTradingMode ===
|
||||
MarketTradingMode.TRADING_MODE_MONITORING_AUCTION &&
|
||||
trigger &&
|
||||
trigger !== AuctionTrigger.AUCTION_TRIGGER_UNSPECIFIED;
|
||||
|
||||
if (withTriggerInfo) {
|
||||
return (
|
||||
<Tooltip
|
||||
description={`${Schema.MarketTradingModeMapping[marketTradingMode]}
|
||||
- ${Schema.AuctionTriggerMapping[trigger]}`}
|
||||
>
|
||||
<span>
|
||||
{Schema.MarketTradingModeMapping[marketTradingMode]}
|
||||
</span>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
return Schema.MarketTradingModeMapping[marketTradingMode];
|
||||
},
|
||||
filter: SetFilter,
|
||||
filterParams: {
|
||||
set: Schema.MarketTradingModeMapping,
|
||||
},
|
||||
},
|
||||
{
|
||||
headerName: t('Status'),
|
||||
field: 'state',
|
||||
valueFormatter: ({
|
||||
data,
|
||||
}: VegaValueFormatterParams<MarketMaybeWithData, 'state'>) => {
|
||||
return data?.state ? Schema.MarketStateMapping[data.state] : '-';
|
||||
},
|
||||
filter: SetFilter,
|
||||
filterParams: {
|
||||
set: Schema.MarketStateMapping,
|
||||
},
|
||||
},
|
||||
{
|
||||
headerName: t('Mark price'),
|
||||
field: 'data.markPrice',
|
||||
type: 'rightAligned',
|
||||
cellRenderer: 'PriceFlashCell',
|
||||
filter: 'agNumberColumnFilter',
|
||||
valueGetter: ({ data }: VegaValueGetterParams<MarketMaybeWithData>) => {
|
||||
return data?.data?.markPrice === undefined
|
||||
? undefined
|
||||
: toBigNum(data?.data?.markPrice, data.decimalPlaces).toNumber();
|
||||
},
|
||||
valueFormatter: ({
|
||||
data,
|
||||
}: VegaValueFormatterParams<MarketMaybeWithData, 'data.markPrice'>) =>
|
||||
data?.data?.bestOfferPrice === undefined
|
||||
? '-'
|
||||
: addDecimalsFormatNumber(data.data.markPrice, data.decimalPlaces),
|
||||
},
|
||||
{
|
||||
headerName: t('24h volume'),
|
||||
type: 'rightAligned',
|
||||
field: 'data.candles',
|
||||
valueGetter: ({
|
||||
data,
|
||||
}: VegaValueGetterParams<MarketMaybeWithDataAndCandles>) => {
|
||||
if (!data) return 0;
|
||||
const candles = data?.candles;
|
||||
const vol = candles ? calcCandleVolume(candles) : '0';
|
||||
return Number(vol);
|
||||
},
|
||||
valueFormatter: ({
|
||||
data,
|
||||
}: ValueFormatterParams<MarketMaybeWithDataAndCandles, 'candles'>) => {
|
||||
const candles = data?.candles;
|
||||
const vol = candles ? calcCandleVolume(candles) : '0';
|
||||
const volume =
|
||||
data && vol && vol !== '0'
|
||||
? addDecimalsFormatNumber(vol, data.positionDecimalPlaces)
|
||||
: '0.00';
|
||||
return volume;
|
||||
},
|
||||
},
|
||||
{
|
||||
headerName: t('Settlement asset'),
|
||||
field: 'tradableInstrument.instrument.product.settlementAsset.symbol',
|
||||
cellRenderer: ({
|
||||
data,
|
||||
}: VegaICellRendererParams<
|
||||
MarketMaybeWithData,
|
||||
'tradableInstrument.instrument.product.settlementAsset.symbol'
|
||||
>) => {
|
||||
const value =
|
||||
data?.tradableInstrument.instrument.product.settlementAsset;
|
||||
return value ? (
|
||||
<ButtonLink
|
||||
onClick={(e) => {
|
||||
openAssetDetailsDialog(value.id, e.target as HTMLElement);
|
||||
}}
|
||||
>
|
||||
{value.symbol}
|
||||
</ButtonLink>
|
||||
) : (
|
||||
''
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
headerName: t('Spread'),
|
||||
field: 'data.bestBidPrice',
|
||||
type: 'rightAligned',
|
||||
filter: 'agNumberColumnFilter',
|
||||
cellRenderer: 'PriceFlashCell',
|
||||
valueGetter: ({ data }: VegaValueGetterParams<MarketMaybeWithData>) => {
|
||||
if (!data || !data.data?.bestOfferPrice || !data.data?.bestBidPrice) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const offer = toBigNum(data.data.bestOfferPrice, data.decimalPlaces);
|
||||
const bid = toBigNum(data.data.bestBidPrice, data.decimalPlaces);
|
||||
|
||||
const spread = offer.minus(bid).toNumber();
|
||||
|
||||
// The calculation above can result in '-0' being rendered after formatting
|
||||
// so return Math.abs to remove it and just render '0'
|
||||
if (spread === 0) {
|
||||
return Math.abs(spread);
|
||||
}
|
||||
|
||||
return spread;
|
||||
},
|
||||
valueFormatter: ({
|
||||
value,
|
||||
}: VegaValueFormatterParams<
|
||||
MarketMaybeWithData,
|
||||
'data.bestBidPrice'
|
||||
>) => {
|
||||
if (!value) return '-';
|
||||
return value.toString();
|
||||
},
|
||||
},
|
||||
{
|
||||
colId: 'market-actions',
|
||||
field: 'id',
|
||||
...COL_DEFS.actions,
|
||||
cellRenderer: ({
|
||||
data,
|
||||
}: VegaICellRendererParams<MarketMaybeWithData>) => {
|
||||
if (!data) return null;
|
||||
return (
|
||||
<MarketActionsDropdown
|
||||
marketId={data.id}
|
||||
assetId={
|
||||
data.tradableInstrument.instrument.product.settlementAsset.id
|
||||
}
|
||||
successorMarketID={data.successorMarketID}
|
||||
parentMarketID={data.parentMarketID}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
],
|
||||
[openAssetDetailsDialog]
|
||||
);
|
||||
};
|
@ -1,24 +1,18 @@
|
||||
import { useNavigate, useParams, useLocation } from 'react-router-dom';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useCallback } from 'react';
|
||||
import { Links, Routes } from '../../pages/client-router';
|
||||
|
||||
export const useMarketClickHandler = (replace = false) => {
|
||||
const navigate = useNavigate();
|
||||
const { marketId } = useParams();
|
||||
const { pathname } = useLocation();
|
||||
const isMarketPage = pathname.match(/^\/markets\/(.+)/);
|
||||
|
||||
return useCallback(
|
||||
(selectedId: string, metaKey?: boolean) => {
|
||||
const link = Links[Routes.MARKET](selectedId);
|
||||
if (metaKey) {
|
||||
window.open(`/#${link}`, '_blank');
|
||||
} else if (selectedId !== marketId || !isMarketPage) {
|
||||
navigate(link, { replace });
|
||||
}
|
||||
},
|
||||
[navigate, marketId, replace, isMarketPage]
|
||||
);
|
||||
return (selectedId: string, metaKey?: boolean) => {
|
||||
const link = Links[Routes.MARKET](selectedId);
|
||||
if (metaKey) {
|
||||
window.open(`/#${link}`, '_blank');
|
||||
} else {
|
||||
navigate(link, { replace });
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export const useMarketLiquidityClickHandler = () => {
|
||||
|
@ -12,6 +12,7 @@ export * from './lib/cells/centered-grid-cell';
|
||||
export * from './lib/cells/market-name-cell';
|
||||
export * from './lib/cells/order-type-cell';
|
||||
export * from './lib/cells/size';
|
||||
export * from './lib/cells/stacked-cell';
|
||||
|
||||
export * from './lib/filters/date-range-filter';
|
||||
export * from './lib/filters/set-filter';
|
||||
|
6
libs/markets/src/lib/__generated__/markets.ts
generated
6
libs/markets/src/lib/__generated__/markets.ts
generated
@ -7,12 +7,12 @@ export type DataSourceFilterFragment = { __typename?: 'Filter', key: { __typenam
|
||||
|
||||
export type DataSourceSpecFragment = { __typename?: 'DataSourceDefinition', sourceType: { __typename?: 'DataSourceDefinitionExternal', sourceType: { __typename?: 'DataSourceSpecConfiguration', signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, filters?: Array<{ __typename?: 'Filter', key: { __typename?: 'PropertyKey', name?: string | null, type: Types.PropertyKeyType, numberDecimalPlaces?: number | null } }> | null } } | { __typename?: 'DataSourceDefinitionInternal' } };
|
||||
|
||||
export type MarketFieldsFragment = { __typename?: 'Market', id: string, decimalPlaces: number, positionDecimalPlaces: number, state: Types.MarketState, tradingMode: Types.MarketTradingMode, fees: { __typename?: 'Fees', factors: { __typename?: 'FeeFactors', makerFee: string, infrastructureFee: string, liquidityFee: string } }, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', id: string, name: string, code: string, metadata: { __typename?: 'InstrumentMetadata', tags?: Array<string> | null }, product: { __typename?: 'Future', quoteName: string, settlementAsset: { __typename?: 'Asset', id: string, symbol: string, name: string, decimals: number, quantum: string }, dataSourceSpecForTradingTermination: { __typename?: 'DataSourceSpec', id: string, data: { __typename?: 'DataSourceDefinition', sourceType: { __typename?: 'DataSourceDefinitionExternal', sourceType: { __typename?: 'DataSourceSpecConfiguration', signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, filters?: Array<{ __typename?: 'Filter', key: { __typename?: 'PropertyKey', name?: string | null, type: Types.PropertyKeyType, numberDecimalPlaces?: number | null } }> | null } } | { __typename?: 'DataSourceDefinitionInternal' } } }, dataSourceSpecForSettlementData: { __typename?: 'DataSourceSpec', id: string, data: { __typename?: 'DataSourceDefinition', sourceType: { __typename?: 'DataSourceDefinitionExternal', sourceType: { __typename?: 'DataSourceSpecConfiguration', signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, filters?: Array<{ __typename?: 'Filter', key: { __typename?: 'PropertyKey', name?: string | null, type: Types.PropertyKeyType, numberDecimalPlaces?: number | null } }> | null } } | { __typename?: 'DataSourceDefinitionInternal' } } }, dataSourceSpecBinding: { __typename?: 'DataSourceSpecToFutureBinding', settlementDataProperty: string, tradingTerminationProperty: string } } } }, marketTimestamps: { __typename?: 'MarketTimestamps', open: any, close: any } };
|
||||
export type MarketFieldsFragment = { __typename?: 'Market', id: string, decimalPlaces: number, positionDecimalPlaces: number, state: Types.MarketState, tradingMode: Types.MarketTradingMode, parentMarketID?: string | null, successorMarketID?: string | null, fees: { __typename?: 'Fees', factors: { __typename?: 'FeeFactors', makerFee: string, infrastructureFee: string, liquidityFee: string } }, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', id: string, name: string, code: string, metadata: { __typename?: 'InstrumentMetadata', tags?: Array<string> | null }, product: { __typename?: 'Future', quoteName: string, settlementAsset: { __typename?: 'Asset', id: string, symbol: string, name: string, decimals: number, quantum: string }, dataSourceSpecForTradingTermination: { __typename?: 'DataSourceSpec', id: string, data: { __typename?: 'DataSourceDefinition', sourceType: { __typename?: 'DataSourceDefinitionExternal', sourceType: { __typename?: 'DataSourceSpecConfiguration', signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, filters?: Array<{ __typename?: 'Filter', key: { __typename?: 'PropertyKey', name?: string | null, type: Types.PropertyKeyType, numberDecimalPlaces?: number | null } }> | null } } | { __typename?: 'DataSourceDefinitionInternal' } } }, dataSourceSpecForSettlementData: { __typename?: 'DataSourceSpec', id: string, data: { __typename?: 'DataSourceDefinition', sourceType: { __typename?: 'DataSourceDefinitionExternal', sourceType: { __typename?: 'DataSourceSpecConfiguration', signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, filters?: Array<{ __typename?: 'Filter', key: { __typename?: 'PropertyKey', name?: string | null, type: Types.PropertyKeyType, numberDecimalPlaces?: number | null } }> | null } } | { __typename?: 'DataSourceDefinitionInternal' } } }, dataSourceSpecBinding: { __typename?: 'DataSourceSpecToFutureBinding', settlementDataProperty: string, tradingTerminationProperty: string } } } }, marketTimestamps: { __typename?: 'MarketTimestamps', open: any, close: any } };
|
||||
|
||||
export type MarketsQueryVariables = Types.Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
export type MarketsQuery = { __typename?: 'Query', marketsConnection?: { __typename?: 'MarketConnection', edges: Array<{ __typename?: 'MarketEdge', node: { __typename?: 'Market', id: string, decimalPlaces: number, positionDecimalPlaces: number, state: Types.MarketState, tradingMode: Types.MarketTradingMode, fees: { __typename?: 'Fees', factors: { __typename?: 'FeeFactors', makerFee: string, infrastructureFee: string, liquidityFee: string } }, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', id: string, name: string, code: string, metadata: { __typename?: 'InstrumentMetadata', tags?: Array<string> | null }, product: { __typename?: 'Future', quoteName: string, settlementAsset: { __typename?: 'Asset', id: string, symbol: string, name: string, decimals: number, quantum: string }, dataSourceSpecForTradingTermination: { __typename?: 'DataSourceSpec', id: string, data: { __typename?: 'DataSourceDefinition', sourceType: { __typename?: 'DataSourceDefinitionExternal', sourceType: { __typename?: 'DataSourceSpecConfiguration', signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, filters?: Array<{ __typename?: 'Filter', key: { __typename?: 'PropertyKey', name?: string | null, type: Types.PropertyKeyType, numberDecimalPlaces?: number | null } }> | null } } | { __typename?: 'DataSourceDefinitionInternal' } } }, dataSourceSpecForSettlementData: { __typename?: 'DataSourceSpec', id: string, data: { __typename?: 'DataSourceDefinition', sourceType: { __typename?: 'DataSourceDefinitionExternal', sourceType: { __typename?: 'DataSourceSpecConfiguration', signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, filters?: Array<{ __typename?: 'Filter', key: { __typename?: 'PropertyKey', name?: string | null, type: Types.PropertyKeyType, numberDecimalPlaces?: number | null } }> | null } } | { __typename?: 'DataSourceDefinitionInternal' } } }, dataSourceSpecBinding: { __typename?: 'DataSourceSpecToFutureBinding', settlementDataProperty: string, tradingTerminationProperty: string } } } }, marketTimestamps: { __typename?: 'MarketTimestamps', open: any, close: any } } }> } | null };
|
||||
export type MarketsQuery = { __typename?: 'Query', marketsConnection?: { __typename?: 'MarketConnection', edges: Array<{ __typename?: 'MarketEdge', node: { __typename?: 'Market', id: string, decimalPlaces: number, positionDecimalPlaces: number, state: Types.MarketState, tradingMode: Types.MarketTradingMode, parentMarketID?: string | null, successorMarketID?: string | null, fees: { __typename?: 'Fees', factors: { __typename?: 'FeeFactors', makerFee: string, infrastructureFee: string, liquidityFee: string } }, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', id: string, name: string, code: string, metadata: { __typename?: 'InstrumentMetadata', tags?: Array<string> | null }, product: { __typename?: 'Future', quoteName: string, settlementAsset: { __typename?: 'Asset', id: string, symbol: string, name: string, decimals: number, quantum: string }, dataSourceSpecForTradingTermination: { __typename?: 'DataSourceSpec', id: string, data: { __typename?: 'DataSourceDefinition', sourceType: { __typename?: 'DataSourceDefinitionExternal', sourceType: { __typename?: 'DataSourceSpecConfiguration', signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, filters?: Array<{ __typename?: 'Filter', key: { __typename?: 'PropertyKey', name?: string | null, type: Types.PropertyKeyType, numberDecimalPlaces?: number | null } }> | null } } | { __typename?: 'DataSourceDefinitionInternal' } } }, dataSourceSpecForSettlementData: { __typename?: 'DataSourceSpec', id: string, data: { __typename?: 'DataSourceDefinition', sourceType: { __typename?: 'DataSourceDefinitionExternal', sourceType: { __typename?: 'DataSourceSpecConfiguration', signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, filters?: Array<{ __typename?: 'Filter', key: { __typename?: 'PropertyKey', name?: string | null, type: Types.PropertyKeyType, numberDecimalPlaces?: number | null } }> | null } } | { __typename?: 'DataSourceDefinitionInternal' } } }, dataSourceSpecBinding: { __typename?: 'DataSourceSpecToFutureBinding', settlementDataProperty: string, tradingTerminationProperty: string } } } }, marketTimestamps: { __typename?: 'MarketTimestamps', open: any, close: any } } }> } | null };
|
||||
|
||||
export const DataSourceFilterFragmentDoc = gql`
|
||||
fragment DataSourceFilter on Filter {
|
||||
@ -55,6 +55,8 @@ export const MarketFieldsFragmentDoc = gql`
|
||||
positionDecimalPlaces
|
||||
state
|
||||
tradingMode
|
||||
parentMarketID
|
||||
successorMarketID
|
||||
fees {
|
||||
factors {
|
||||
makerFee
|
||||
|
@ -2,7 +2,6 @@ export * from './fees-breakdown';
|
||||
export * from './last-24h-price-change';
|
||||
export * from './last-24h-volume';
|
||||
export * from './market-info';
|
||||
export * from './markets-container';
|
||||
export * from './oracle-banner';
|
||||
export * from './oracle-basic-profile';
|
||||
export * from './oracle-full-profile';
|
||||
|
@ -1,2 +0,0 @@
|
||||
export * from './markets-container';
|
||||
export * from './market-table-actions';
|
@ -1,69 +0,0 @@
|
||||
import type { TypedDataAgGrid } from '@vegaprotocol/datagrid';
|
||||
import {
|
||||
AgGridLazy as AgGrid,
|
||||
PriceFlashCell,
|
||||
MarketNameCell,
|
||||
} from '@vegaprotocol/datagrid';
|
||||
import type { MarketMaybeWithData } from '../../markets-provider';
|
||||
import { OracleStatus } from './oracle-status';
|
||||
import { useColumnDefs } from './use-column-defs';
|
||||
|
||||
export const getRowId = ({ data }: { data: { id: string } }) => data.id;
|
||||
|
||||
interface MarketNameCellProps {
|
||||
value?: string;
|
||||
data?: MarketMaybeWithData;
|
||||
onMarketClick?: (marketId: string, metaKey?: boolean) => void;
|
||||
}
|
||||
|
||||
const MarketName = (props: MarketNameCellProps) => (
|
||||
<>
|
||||
<MarketNameCell {...props} />
|
||||
{props.data ? (
|
||||
<OracleStatus
|
||||
dataSourceSpecForSettlementData={
|
||||
props.data.tradableInstrument.instrument.product
|
||||
.dataSourceSpecForSettlementData
|
||||
}
|
||||
dataSourceSpecForTradingTermination={
|
||||
props.data.tradableInstrument.instrument.product
|
||||
.dataSourceSpecForTradingTermination
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
|
||||
const defaultColDef = {
|
||||
sortable: true,
|
||||
filter: true,
|
||||
filterParams: { buttons: ['reset'] },
|
||||
};
|
||||
type Props = TypedDataAgGrid<MarketMaybeWithData> & {
|
||||
onMarketClick: (marketId: string, metaKey?: boolean) => void;
|
||||
SuccessorMarketRenderer?: React.FC<{ value: string }>;
|
||||
};
|
||||
export const MarketListTable = ({
|
||||
onMarketClick,
|
||||
SuccessorMarketRenderer,
|
||||
...props
|
||||
}: Props) => {
|
||||
const columnDefs = useColumnDefs({ onMarketClick });
|
||||
const components = {
|
||||
PriceFlashCell,
|
||||
MarketName,
|
||||
...(SuccessorMarketRenderer ? { SuccessorMarketRenderer } : null),
|
||||
};
|
||||
return (
|
||||
<AgGrid
|
||||
getRowId={getRowId}
|
||||
defaultColDef={defaultColDef}
|
||||
columnDefs={columnDefs}
|
||||
suppressCellFocus
|
||||
components={components}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default MarketListTable;
|
@ -1,150 +0,0 @@
|
||||
import { render, screen, act, within } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import * as DataProviders from '@vegaprotocol/data-provider';
|
||||
import { MockedProvider } from '@apollo/react-testing';
|
||||
import type { MarketMaybeWithData } from '../../markets-provider';
|
||||
import { MarketsContainer } from './markets-container';
|
||||
import { FLAGS } from '@vegaprotocol/environment';
|
||||
|
||||
jest.mock('@vegaprotocol/environment', () => {
|
||||
const actual = jest.requireActual('@vegaprotocol/environment');
|
||||
return {
|
||||
...actual,
|
||||
FLAGS: {
|
||||
...actual.FLAGS,
|
||||
SUCCESSOR_MARKETS: true,
|
||||
},
|
||||
};
|
||||
});
|
||||
const SuccessorMarketRenderer = ({ value }: { value: string }) => {
|
||||
return '-';
|
||||
};
|
||||
|
||||
const market = {
|
||||
id: 'id-1',
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
product: { settlementAsset: { id: 'assetId-1' } },
|
||||
},
|
||||
},
|
||||
decimalPlaces: 1,
|
||||
positionDecimalPlaces: 1,
|
||||
state: 'STATE_ACTIVE',
|
||||
tradingMode: 'TRADING_MODE_OPENING_AUCTION',
|
||||
data: {
|
||||
bestBidPrice: 100,
|
||||
},
|
||||
} as unknown as MarketMaybeWithData;
|
||||
|
||||
describe('MarketsContainer', () => {
|
||||
const spyOnSelect = jest.fn();
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
jest
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
.spyOn<typeof DataProviders, any>(DataProviders, 'useDataProvider')
|
||||
.mockImplementation(() => {
|
||||
return {
|
||||
error: null,
|
||||
reload: jest.fn(),
|
||||
data: [market],
|
||||
};
|
||||
});
|
||||
});
|
||||
it('context menu should stay open', async () => {
|
||||
let rerenderRef: (ui: React.ReactElement) => void;
|
||||
await act(async () => {
|
||||
const { rerender } = render(
|
||||
<MockedProvider>
|
||||
<MarketsContainer
|
||||
onSelect={spyOnSelect}
|
||||
SuccessorMarketRenderer={SuccessorMarketRenderer}
|
||||
/>
|
||||
</MockedProvider>
|
||||
);
|
||||
rerenderRef = rerender;
|
||||
});
|
||||
|
||||
// make sure ag grid is finished initializaing
|
||||
const rowContainer = await screen.findByRole('rowgroup', {
|
||||
name: (_name, element) =>
|
||||
element.classList.contains('ag-center-cols-container'),
|
||||
});
|
||||
expect(within(rowContainer).getAllByRole('row')).toHaveLength(1);
|
||||
expect(
|
||||
screen.getByRole('rowgroup', {
|
||||
name: (_name, element) =>
|
||||
element.classList.contains('ag-pinned-right-cols-container'),
|
||||
})
|
||||
).toBeInTheDocument();
|
||||
|
||||
// open the dropdown
|
||||
await userEvent.click(
|
||||
screen.getByRole('button', {
|
||||
name: (_name, element) =>
|
||||
(element.parentNode as Element)
|
||||
?.getAttribute('id')
|
||||
?.startsWith('cell-market-actions-') || false,
|
||||
})
|
||||
);
|
||||
|
||||
await checkDropdown();
|
||||
|
||||
// reset the mock and rerender so the component
|
||||
// updates with new data
|
||||
jest
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
.spyOn<typeof DataProviders, any>(DataProviders, 'useDataProvider')
|
||||
.mockImplementation(() => {
|
||||
return {
|
||||
error: null,
|
||||
reload: jest.fn(),
|
||||
data: [{ ...market, state: 'STATE_PENDING' }],
|
||||
};
|
||||
});
|
||||
|
||||
// @ts-ignore we await the act above so rerenderRef is definitely defined
|
||||
rerenderRef(
|
||||
<MockedProvider mocks={[]}>
|
||||
<MarketsContainer onSelect={spyOnSelect} />
|
||||
</MockedProvider>
|
||||
);
|
||||
|
||||
// make sure dropdown is still open
|
||||
await checkDropdown();
|
||||
|
||||
async function checkDropdown() {
|
||||
const dropdownContent = await screen.findByTestId(
|
||||
'market-actions-content'
|
||||
);
|
||||
expect(dropdownContent).toBeInTheDocument();
|
||||
expect(
|
||||
within(dropdownContent).getByRole('menuitem', {
|
||||
name: 'Copy Market ID',
|
||||
})
|
||||
).toBeInTheDocument();
|
||||
}
|
||||
});
|
||||
|
||||
it('feature flag should hide successorMarketID column', async () => {
|
||||
const mockedFlags = jest.mocked(FLAGS);
|
||||
mockedFlags.SUCCESSOR_MARKETS = false;
|
||||
|
||||
const spySuccessorMarketRenderer = jest.fn();
|
||||
|
||||
render(
|
||||
<MockedProvider>
|
||||
<MarketsContainer
|
||||
onSelect={spyOnSelect}
|
||||
SuccessorMarketRenderer={spySuccessorMarketRenderer}
|
||||
/>
|
||||
</MockedProvider>
|
||||
);
|
||||
|
||||
expect(spySuccessorMarketRenderer).not.toHaveBeenCalled();
|
||||
screen.getAllByRole('columnheader').forEach((element) => {
|
||||
expect(element.getAttribute('col-id')).not.toEqual('successorMarketID');
|
||||
});
|
||||
});
|
||||
});
|
@ -1,72 +0,0 @@
|
||||
import type { MouseEvent } from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import type { CellClickedEvent } from 'ag-grid-community';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { MarketListTable } from './market-list-table';
|
||||
import { useDataProvider } from '@vegaprotocol/data-provider';
|
||||
import { marketListProvider as dataProvider } from '../../markets-provider';
|
||||
import type { MarketMaybeWithData } from '../../markets-provider';
|
||||
import { useYesterday } from '@vegaprotocol/react-helpers';
|
||||
import { Interval } from '@vegaprotocol/types';
|
||||
|
||||
const POLLING_TIME = 2000;
|
||||
interface MarketsContainerProps {
|
||||
onSelect: (marketId: string, metaKey?: boolean) => void;
|
||||
SuccessorMarketRenderer?: React.FC<{ value: string }>;
|
||||
}
|
||||
|
||||
export const MarketsContainer = ({
|
||||
onSelect,
|
||||
SuccessorMarketRenderer,
|
||||
}: MarketsContainerProps) => {
|
||||
const yesterday = useYesterday();
|
||||
const { data, error, reload } = useDataProvider({
|
||||
dataProvider,
|
||||
variables: {
|
||||
since: new Date(yesterday).toISOString(),
|
||||
interval: Interval.INTERVAL_I1H,
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
reload();
|
||||
}, POLLING_TIME);
|
||||
return () => {
|
||||
clearInterval(interval);
|
||||
};
|
||||
}, [reload]);
|
||||
|
||||
return (
|
||||
<div className="h-full relative">
|
||||
<MarketListTable
|
||||
rowData={data}
|
||||
onCellClicked={(cellEvent: CellClickedEvent) => {
|
||||
const { data, column, event } = cellEvent;
|
||||
// prevent navigating to the market page if any of the below cells are clicked
|
||||
// event.preventDefault or event.stopPropagation dont seem to apply for aggird
|
||||
const colId = column.getColId();
|
||||
if (
|
||||
[
|
||||
'id',
|
||||
'tradableInstrument.instrument.code',
|
||||
'tradableInstrument.instrument.product.settlementAsset',
|
||||
'tradableInstrument.instrument.product.settlementAsset.symbol',
|
||||
'market-actions',
|
||||
].includes(colId)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
onSelect(
|
||||
(data as MarketMaybeWithData).id,
|
||||
(event as unknown as MouseEvent)?.metaKey ||
|
||||
(event as unknown as MouseEvent)?.ctrlKey
|
||||
);
|
||||
}}
|
||||
onMarketClick={onSelect}
|
||||
overlayNoRowsTemplate={error ? error.message : t('No markets')}
|
||||
SuccessorMarketRenderer={SuccessorMarketRenderer}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -1,44 +0,0 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useEnvironment } from '@vegaprotocol/environment';
|
||||
import { Icon } from '@vegaprotocol/ui-toolkit';
|
||||
import type { IconName } from '@blueprintjs/icons';
|
||||
import { getMatchingOracleProvider, useOracleProofs } from '../../hooks';
|
||||
import type { Market } from '../../markets-provider';
|
||||
import { getVerifiedStatusIcon } from '../oracle-basic-profile';
|
||||
|
||||
export const OracleStatus = ({
|
||||
dataSourceSpecForSettlementData,
|
||||
dataSourceSpecForTradingTermination,
|
||||
}: Pick<
|
||||
Market['tradableInstrument']['instrument']['product'],
|
||||
'dataSourceSpecForSettlementData' | 'dataSourceSpecForTradingTermination'
|
||||
>) => {
|
||||
const { ORACLE_PROOFS_URL } = useEnvironment();
|
||||
const { data: providers } = useOracleProofs(ORACLE_PROOFS_URL);
|
||||
return useMemo(() => {
|
||||
if (providers) {
|
||||
const settlementDataProvider = getMatchingOracleProvider(
|
||||
dataSourceSpecForSettlementData.data,
|
||||
providers
|
||||
);
|
||||
const tradingTerminationDataProvider = getMatchingOracleProvider(
|
||||
dataSourceSpecForTradingTermination.data,
|
||||
providers
|
||||
);
|
||||
let maliciousOracleProvider = null;
|
||||
if (settlementDataProvider?.oracle.status !== 'GOOD') {
|
||||
maliciousOracleProvider = settlementDataProvider;
|
||||
} else if (tradingTerminationDataProvider?.oracle.status !== 'GOOD') {
|
||||
maliciousOracleProvider = tradingTerminationDataProvider;
|
||||
}
|
||||
if (!maliciousOracleProvider) return null;
|
||||
const { icon } = getVerifiedStatusIcon(maliciousOracleProvider);
|
||||
return <Icon size={3} name={icon as IconName} className="ml-1" />;
|
||||
}
|
||||
return null;
|
||||
}, [
|
||||
providers,
|
||||
dataSourceSpecForSettlementData,
|
||||
dataSourceSpecForTradingTermination,
|
||||
]);
|
||||
};
|
@ -1,29 +0,0 @@
|
||||
import * as React from 'react';
|
||||
|
||||
/*
|
||||
import { useMarketOverview } from '../../../../hooks/use-market-overview'
|
||||
import { colorByMarketMovement } from '../../../../lib/vega-colours'
|
||||
import { Sparkline } from '../../components/sparkline'
|
||||
import { VEGA_TABLE_CLASSES } from '../../components/vega-table'
|
||||
*/
|
||||
|
||||
export interface SummaryCellProps {
|
||||
value: string; // marketId
|
||||
}
|
||||
|
||||
export const SummaryCellView = ({ value }: SummaryCellProps) => {
|
||||
// const { sparkline, change, bullish } = useMarketOverview(value)
|
||||
// const color = colorByMarketMovement(bullish)
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* <Sparkline data={sparkline} style={{ marginRight: 4 }} />*/}
|
||||
<span>{'change'}</span>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
SummaryCellView.displayName = 'SummaryCellView';
|
||||
|
||||
export const SummaryCell = React.memo(SummaryCellView);
|
||||
SummaryCell.displayName = 'SummaryCell';
|
@ -1,231 +0,0 @@
|
||||
import { useMemo } from 'react';
|
||||
import type { ColDef, ValueFormatterParams } from 'ag-grid-community';
|
||||
import compact from 'lodash/compact';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import type {
|
||||
VegaICellRendererParams,
|
||||
VegaValueFormatterParams,
|
||||
VegaValueGetterParams,
|
||||
} from '@vegaprotocol/datagrid';
|
||||
import { COL_DEFS, SetFilter } from '@vegaprotocol/datagrid';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import { addDecimalsFormatNumber, toBigNum } from '@vegaprotocol/utils';
|
||||
import { ButtonLink, Tooltip } from '@vegaprotocol/ui-toolkit';
|
||||
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
|
||||
import type {
|
||||
MarketMaybeWithData,
|
||||
MarketMaybeWithDataAndCandles,
|
||||
} from '../../markets-provider';
|
||||
import { MarketActionsDropdown } from './market-table-actions';
|
||||
import { calcCandleVolume } from '../../market-utils';
|
||||
|
||||
interface Props {
|
||||
onMarketClick: (marketId: string, metaKey?: boolean) => void;
|
||||
}
|
||||
|
||||
const { MarketTradingMode, AuctionTrigger } = Schema;
|
||||
|
||||
export const useColumnDefs = ({ onMarketClick }: Props) => {
|
||||
const { open: openAssetDetailsDialog } = useAssetDetailsDialogStore();
|
||||
return useMemo<ColDef[]>(
|
||||
() =>
|
||||
compact([
|
||||
{
|
||||
headerName: t('Market'),
|
||||
field: 'tradableInstrument.instrument.code',
|
||||
cellRenderer: 'MarketName',
|
||||
cellRendererParams: { onMarketClick },
|
||||
flex: 2,
|
||||
},
|
||||
{
|
||||
headerName: t('Description'),
|
||||
field: 'tradableInstrument.instrument.name',
|
||||
flex: 2,
|
||||
},
|
||||
{
|
||||
headerName: t('Trading mode'),
|
||||
field: 'tradingMode',
|
||||
cellRenderer: ({
|
||||
data,
|
||||
}: VegaICellRendererParams<MarketMaybeWithData, 'data'>) => {
|
||||
if (!data?.data) return '-';
|
||||
const { trigger, marketTradingMode } = data.data;
|
||||
|
||||
const withTriggerInfo =
|
||||
marketTradingMode ===
|
||||
MarketTradingMode.TRADING_MODE_MONITORING_AUCTION &&
|
||||
trigger &&
|
||||
trigger !== AuctionTrigger.AUCTION_TRIGGER_UNSPECIFIED;
|
||||
|
||||
if (withTriggerInfo) {
|
||||
return (
|
||||
<Tooltip
|
||||
description={`${Schema.MarketTradingModeMapping[marketTradingMode]}
|
||||
- ${Schema.AuctionTriggerMapping[trigger]}`}
|
||||
>
|
||||
<span>
|
||||
{Schema.MarketTradingModeMapping[marketTradingMode]}
|
||||
</span>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
return Schema.MarketTradingModeMapping[marketTradingMode];
|
||||
},
|
||||
filter: SetFilter,
|
||||
filterParams: {
|
||||
set: Schema.MarketTradingModeMapping,
|
||||
},
|
||||
},
|
||||
{
|
||||
headerName: t('Status'),
|
||||
field: 'state',
|
||||
valueFormatter: ({
|
||||
data,
|
||||
}: VegaValueFormatterParams<MarketMaybeWithData, 'state'>) => {
|
||||
return data?.state ? Schema.MarketStateMapping[data.state] : '-';
|
||||
},
|
||||
filter: SetFilter,
|
||||
filterParams: {
|
||||
set: Schema.MarketStateMapping,
|
||||
},
|
||||
},
|
||||
{
|
||||
headerName: t('Mark price'),
|
||||
field: 'data.markPrice',
|
||||
type: 'rightAligned',
|
||||
cellRenderer: 'PriceFlashCell',
|
||||
filter: 'agNumberColumnFilter',
|
||||
valueGetter: ({
|
||||
data,
|
||||
}: VegaValueGetterParams<MarketMaybeWithData>) => {
|
||||
return data?.data?.markPrice === undefined
|
||||
? undefined
|
||||
: toBigNum(data?.data?.markPrice, data.decimalPlaces).toNumber();
|
||||
},
|
||||
valueFormatter: ({
|
||||
data,
|
||||
}: VegaValueFormatterParams<MarketMaybeWithData, 'data.markPrice'>) =>
|
||||
data?.data?.bestOfferPrice === undefined
|
||||
? '-'
|
||||
: addDecimalsFormatNumber(
|
||||
data.data.markPrice,
|
||||
data.decimalPlaces
|
||||
),
|
||||
},
|
||||
{
|
||||
headerName: t('24h volume'),
|
||||
type: 'rightAligned',
|
||||
field: 'data.candles',
|
||||
valueGetter: ({
|
||||
data,
|
||||
}: VegaValueGetterParams<MarketMaybeWithDataAndCandles>) => {
|
||||
if (!data) return 0;
|
||||
const candles = data?.candles;
|
||||
const vol = candles ? calcCandleVolume(candles) : '0';
|
||||
return Number(vol);
|
||||
},
|
||||
valueFormatter: ({
|
||||
data,
|
||||
}: ValueFormatterParams<
|
||||
MarketMaybeWithDataAndCandles,
|
||||
'candles'
|
||||
>) => {
|
||||
const candles = data?.candles;
|
||||
const vol = candles ? calcCandleVolume(candles) : '0';
|
||||
const volume =
|
||||
data && vol && vol !== '0'
|
||||
? addDecimalsFormatNumber(vol, data.positionDecimalPlaces)
|
||||
: '0.00';
|
||||
return volume;
|
||||
},
|
||||
},
|
||||
{
|
||||
headerName: t('Settlement asset'),
|
||||
field: 'tradableInstrument.instrument.product.settlementAsset.symbol',
|
||||
cellRenderer: ({
|
||||
data,
|
||||
}: VegaICellRendererParams<
|
||||
MarketMaybeWithData,
|
||||
'tradableInstrument.instrument.product.settlementAsset.symbol'
|
||||
>) => {
|
||||
const value =
|
||||
data?.tradableInstrument.instrument.product.settlementAsset;
|
||||
return value ? (
|
||||
<ButtonLink
|
||||
onClick={(e) => {
|
||||
openAssetDetailsDialog(value.id, e.target as HTMLElement);
|
||||
}}
|
||||
>
|
||||
{value.symbol}
|
||||
</ButtonLink>
|
||||
) : (
|
||||
''
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
headerName: t('Spread'),
|
||||
field: 'data.bestBidPrice',
|
||||
type: 'rightAligned',
|
||||
filter: 'agNumberColumnFilter',
|
||||
cellRenderer: 'PriceFlashCell',
|
||||
valueGetter: ({
|
||||
data,
|
||||
}: VegaValueGetterParams<MarketMaybeWithData>) => {
|
||||
if (
|
||||
!data ||
|
||||
!data.data?.bestOfferPrice ||
|
||||
!data.data?.bestBidPrice
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const offer = toBigNum(
|
||||
data.data.bestOfferPrice,
|
||||
data.decimalPlaces
|
||||
);
|
||||
const bid = toBigNum(data.data.bestBidPrice, data.decimalPlaces);
|
||||
|
||||
const spread = offer.minus(bid).toNumber();
|
||||
|
||||
// The calculation above can result in '-0' being rendered after formatting
|
||||
// so return Math.abs to remove it and just render '0'
|
||||
if (spread === 0) {
|
||||
return Math.abs(spread);
|
||||
}
|
||||
|
||||
return spread;
|
||||
},
|
||||
valueFormatter: ({
|
||||
value,
|
||||
}: VegaValueFormatterParams<
|
||||
MarketMaybeWithData,
|
||||
'data.bestBidPrice'
|
||||
>) => {
|
||||
if (!value) return '-';
|
||||
return value.toString();
|
||||
},
|
||||
},
|
||||
{
|
||||
colId: 'market-actions',
|
||||
field: 'id',
|
||||
...COL_DEFS.actions,
|
||||
cellRenderer: ({
|
||||
data,
|
||||
}: VegaICellRendererParams<MarketMaybeWithData>) => {
|
||||
if (!data) return null;
|
||||
return (
|
||||
<MarketActionsDropdown
|
||||
marketId={data.id}
|
||||
assetId={
|
||||
data.tradableInstrument.instrument.product.settlementAsset.id
|
||||
}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
]),
|
||||
[onMarketClick, openAssetDetailsDialog]
|
||||
);
|
||||
};
|
@ -43,6 +43,7 @@ export const getVerifiedStatusIcon = (provider: Provider) => {
|
||||
const lastVerified = provider.oracle.last_verified
|
||||
? new Date(provider.oracle.last_verified)
|
||||
: new Date(provider.oracle.first_verified);
|
||||
|
||||
return {
|
||||
...getIconIntent(),
|
||||
message: t(
|
||||
@ -112,13 +113,13 @@ export const OracleBasicProfile = ({
|
||||
<Icon size={3} name={icon as IconName} />
|
||||
</span>
|
||||
</span>
|
||||
<p className="text-sm dark:text-vega-light-300 text-vega-dark-300 mb-2">
|
||||
<p className="mb-2 text-sm dark:text-vega-light-300 text-vega-dark-300">
|
||||
{message}
|
||||
</p>
|
||||
{oracleMarkets && (
|
||||
<p
|
||||
data-testid="signed-proofs"
|
||||
className="text-sm dark:text-vega-light-300 text-vega-dark-300 mb-2"
|
||||
className="mb-2 text-sm dark:text-vega-light-300 text-vega-dark-300"
|
||||
>
|
||||
{t('Involved in %s %s', [
|
||||
oracleMarkets.length.toString(),
|
||||
@ -130,9 +131,9 @@ export const OracleBasicProfile = ({
|
||||
<div className="flex flex-row gap-3">
|
||||
{links.map((link) => (
|
||||
<ExternalLink key={link.url} href={link.url} data-testid={link.url}>
|
||||
<span className="flex gap-1 items-center">
|
||||
<span className="flex items-center gap-1">
|
||||
<VegaIcon name={getLinkIcon(link.type)} />
|
||||
<span className="capitalize underline">{link.type}</span>
|
||||
<span className="underline capitalize">{link.type}</span>
|
||||
<VegaIcon name={VegaIconNames.OPEN_EXTERNAL} size={13} />
|
||||
</span>
|
||||
</ExternalLink>
|
||||
|
@ -28,8 +28,6 @@ import {
|
||||
} from './market-utils';
|
||||
import { MarketsDocument } from './__generated__/markets';
|
||||
import type { Candle } from './market-candles-provider';
|
||||
import type { SuccessorMarketIdsQuery } from './__generated__/SuccessorMarket';
|
||||
import { SuccessorMarketIdsDocument } from './__generated__';
|
||||
|
||||
export type Market = MarketFieldsFragment;
|
||||
|
||||
@ -240,34 +238,3 @@ export const useMarketList = () => {
|
||||
reload,
|
||||
};
|
||||
};
|
||||
|
||||
export type MarketSuccessors = {
|
||||
__typename?: 'Market';
|
||||
id: string;
|
||||
successorMarketID?: string | null;
|
||||
parentMarketID?: string | null;
|
||||
};
|
||||
const getMarketSuccessorData = (
|
||||
responseData: SuccessorMarketIdsQuery | null
|
||||
): MarketSuccessors[] | null =>
|
||||
responseData?.marketsConnection?.edges.map((edge) => edge.node) || null;
|
||||
|
||||
export const marketSuccessorProvider = makeDataProvider<
|
||||
SuccessorMarketIdsQuery,
|
||||
MarketSuccessors[],
|
||||
never,
|
||||
never
|
||||
>({
|
||||
query: SuccessorMarketIdsDocument,
|
||||
getData: getMarketSuccessorData,
|
||||
fetchPolicy: 'no-cache',
|
||||
});
|
||||
|
||||
export const useSuccessorMarketIds = (marketId: string) => {
|
||||
const { data } = useDataProvider({
|
||||
dataProvider: marketSuccessorProvider,
|
||||
variables: undefined,
|
||||
skip: !marketId,
|
||||
});
|
||||
return data?.find((item) => item.id === marketId) ?? null;
|
||||
};
|
||||
|
@ -36,6 +36,8 @@ fragment MarketFields on Market {
|
||||
positionDecimalPlaces
|
||||
state
|
||||
tradingMode
|
||||
parentMarketID
|
||||
successorMarketID
|
||||
fees {
|
||||
factors {
|
||||
makerFee
|
||||
|
@ -42,6 +42,8 @@ export const createMarketFragment = (
|
||||
close: null,
|
||||
open: null,
|
||||
},
|
||||
successorMarketID: null,
|
||||
parentMarketID: null,
|
||||
fees: {
|
||||
__typename: 'Fees',
|
||||
factors: {
|
||||
|
@ -15,6 +15,7 @@ import {
|
||||
MarketNameCell,
|
||||
ProgressBarCell,
|
||||
MarketProductPill,
|
||||
StackedCell,
|
||||
} from '@vegaprotocol/datagrid';
|
||||
import {
|
||||
ButtonLink,
|
||||
@ -40,7 +41,6 @@ import {
|
||||
import { DocsLinks } from '@vegaprotocol/environment';
|
||||
import { PositionActionsDropdown } from './position-actions-dropdown';
|
||||
import { LiquidationPrice } from './liquidation-price';
|
||||
import { StackedCell } from './stacked-cell';
|
||||
|
||||
interface Props extends TypedDataAgGrid<Position> {
|
||||
onClose?: (data: Position) => void;
|
||||
|
@ -1,10 +1,4 @@
|
||||
import {
|
||||
render,
|
||||
screen,
|
||||
act,
|
||||
waitFor,
|
||||
getAllByRole,
|
||||
} from '@testing-library/react';
|
||||
import { render, screen, waitFor, within } from '@testing-library/react';
|
||||
import merge from 'lodash/merge';
|
||||
import type { MockedResponse } from '@apollo/client/testing';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
@ -14,25 +8,13 @@ import { createProposalListFieldsFragment } from '../../lib/proposals-data-provi
|
||||
import type { ProposalsListQuery } from '../../lib';
|
||||
import { ProposalsListDocument } from '../../lib';
|
||||
import type { PartialDeep } from 'type-fest';
|
||||
import { FLAGS } from '@vegaprotocol/environment';
|
||||
|
||||
jest.mock('@vegaprotocol/environment', () => {
|
||||
const actual = jest.requireActual('@vegaprotocol/environment');
|
||||
return {
|
||||
...actual,
|
||||
FLAGS: {
|
||||
...actual.FLAGS,
|
||||
SUCCESSOR_MARKETS: true,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const successorMarketName = 'Successor Market Name';
|
||||
const spySuccessorMarketRenderer = jest
|
||||
.fn()
|
||||
.mockReturnValue(successorMarketName);
|
||||
const parentMarketName = 'Parent Market Name';
|
||||
const ParentMarketCell = () => <span>{parentMarketName}</span>;
|
||||
|
||||
describe('ProposalsList', () => {
|
||||
const rowContainerSelector = '.ag-center-cols-container';
|
||||
|
||||
const createProposalsMock = (override?: PartialDeep<ProposalsListQuery>) => {
|
||||
const defaultProposalEdges = [
|
||||
{
|
||||
@ -81,75 +63,55 @@ describe('ProposalsList', () => {
|
||||
|
||||
return mock;
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should be properly rendered', async () => {
|
||||
const mock = createProposalsMock();
|
||||
await act(() => {
|
||||
render(
|
||||
<MockedProvider mocks={[mock]}>
|
||||
<ProposalsList SuccessorMarketRenderer={spySuccessorMarketRenderer} />
|
||||
</MockedProvider>
|
||||
);
|
||||
});
|
||||
const container = document.querySelector('.ag-center-cols-container');
|
||||
render(
|
||||
<MockedProvider mocks={[mock]}>
|
||||
<ProposalsList cellRenderers={{ ParentMarketCell }} />
|
||||
</MockedProvider>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(container).toBeInTheDocument();
|
||||
});
|
||||
expect(getAllByRole(container as HTMLDivElement, 'row')).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('some of states should be filtered out', async () => {
|
||||
const proposalNode = createProposalListFieldsFragment({
|
||||
id: 'id-1',
|
||||
state: Types.ProposalState.STATE_ENACTED,
|
||||
expect(document.querySelector(rowContainerSelector)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const mock = createProposalsMock({
|
||||
proposalsConnection: {
|
||||
edges: [
|
||||
{
|
||||
__typename: 'ProposalEdge',
|
||||
node: {
|
||||
...proposalNode,
|
||||
terms: {
|
||||
...proposalNode.terms,
|
||||
change: {
|
||||
...proposalNode.terms.change,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
} as PartialDeep<ProposalsListQuery>);
|
||||
await act(() => {
|
||||
render(
|
||||
<MockedProvider mocks={[mock]}>
|
||||
<ProposalsList SuccessorMarketRenderer={spySuccessorMarketRenderer} />
|
||||
</MockedProvider>
|
||||
);
|
||||
});
|
||||
const container = document.querySelector('.ag-center-cols-container');
|
||||
await waitFor(() => {
|
||||
expect(container).toBeInTheDocument();
|
||||
expect(getAllByRole(container as HTMLDivElement, 'row')).toHaveLength(2);
|
||||
});
|
||||
const expectedHeaders = [
|
||||
'Market',
|
||||
'Settlement asset',
|
||||
'State',
|
||||
'Parent market',
|
||||
'Voting',
|
||||
'Closing date',
|
||||
'Enactment date',
|
||||
'', // actions col
|
||||
];
|
||||
|
||||
expect(spySuccessorMarketRenderer).toHaveBeenCalled();
|
||||
const headers = screen.getAllByRole('columnheader');
|
||||
expect(headers).toHaveLength(expectedHeaders.length);
|
||||
expect(
|
||||
screen.getByRole('columnheader', {
|
||||
name: (_name, element) =>
|
||||
element.getAttribute('col-id') === 'parentMarket',
|
||||
})
|
||||
).toBeInTheDocument();
|
||||
headers.map((h) => h.querySelector('[ref="eText"]')?.textContent?.trim())
|
||||
).toEqual(expectedHeaders);
|
||||
|
||||
const container = within(
|
||||
document.querySelector(rowContainerSelector) as HTMLElement
|
||||
);
|
||||
expect(container.getAllByRole('row')).toHaveLength(
|
||||
// @ts-ignore data is mocked
|
||||
mock?.result?.data.proposalsConnection.edges.length
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.getAllByRole('gridcell', {
|
||||
name: (name, element) =>
|
||||
element.getAttribute('col-id') === 'parentMarket',
|
||||
container.getAllByRole('gridcell', {
|
||||
name: (_, element) =>
|
||||
element.getAttribute('col-id') ===
|
||||
'terms.change.successorConfiguration.parentMarketId',
|
||||
})[0]
|
||||
).toHaveTextContent(successorMarketName);
|
||||
).toHaveTextContent(parentMarketName);
|
||||
});
|
||||
|
||||
it('empty response should causes no data message display', async () => {
|
||||
@ -169,58 +131,11 @@ describe('ProposalsList', () => {
|
||||
},
|
||||
},
|
||||
};
|
||||
await act(() => {
|
||||
render(
|
||||
<MockedProvider mocks={[mock]}>
|
||||
<ProposalsList SuccessorMarketRenderer={spySuccessorMarketRenderer} />
|
||||
</MockedProvider>
|
||||
);
|
||||
});
|
||||
expect(await screen.findByText('No markets')).toBeInTheDocument();
|
||||
|
||||
expect(
|
||||
screen.getByRole('columnheader', {
|
||||
name: (_name, element) =>
|
||||
element.getAttribute('col-id') === 'parentMarket',
|
||||
})
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('feature flag should hide parent marketcolumn', async () => {
|
||||
const mockedFlags = jest.mocked(FLAGS);
|
||||
mockedFlags.SUCCESSOR_MARKETS = false;
|
||||
const mock: MockedResponse<ProposalsListQuery> = {
|
||||
request: {
|
||||
query: ProposalsListDocument,
|
||||
variables: {
|
||||
proposalType: Types.ProposalType.TYPE_NEW_MARKET,
|
||||
},
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
proposalsConnection: {
|
||||
__typename: 'ProposalsConnection',
|
||||
edges: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
await act(() => {
|
||||
render(
|
||||
<MockedProvider mocks={[mock]}>
|
||||
<ProposalsList SuccessorMarketRenderer={spySuccessorMarketRenderer} />
|
||||
</MockedProvider>
|
||||
);
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByRole('columnheader', {
|
||||
name: (_name, element) => element.getAttribute('col-id') === 'market',
|
||||
})
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
screen.getAllByRole('columnheader').forEach((element) => {
|
||||
expect(element.getAttribute('col-id')).not.toEqual('parentMarket');
|
||||
});
|
||||
render(
|
||||
<MockedProvider mocks={[mock]}>
|
||||
<ProposalsList cellRenderers={{ ParentMarketCell }} />
|
||||
</MockedProvider>
|
||||
);
|
||||
expect(await screen.findByText('No proposed markets')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
@ -1,11 +1,11 @@
|
||||
import React from 'react';
|
||||
import type { FC } from 'react';
|
||||
import { AgGridLazy as AgGrid } from '@vegaprotocol/datagrid';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import * as Types from '@vegaprotocol/types';
|
||||
import { MarketNameProposalCell, useColumnDefs } from './use-column-defs';
|
||||
import { removePaginationWrapper } from '@vegaprotocol/utils';
|
||||
import type { ProposalListFieldsFragment } from '../../lib/proposals-data-provider/__generated__/Proposals';
|
||||
import { useProposalsListQuery } from '../../lib/proposals-data-provider/__generated__/Proposals';
|
||||
import { removePaginationWrapper } from '@vegaprotocol/utils';
|
||||
import { useColumnDefs } from './use-column-defs';
|
||||
|
||||
export const getNewMarketProposals = (data: ProposalListFieldsFragment[]) =>
|
||||
data.filter((proposal) =>
|
||||
@ -16,13 +16,19 @@ export const getNewMarketProposals = (data: ProposalListFieldsFragment[]) =>
|
||||
].includes(proposal.state)
|
||||
);
|
||||
|
||||
const defaultColDef = {
|
||||
sortable: true,
|
||||
filter: true,
|
||||
filterParams: { buttons: ['reset'] },
|
||||
};
|
||||
|
||||
interface ProposalListProps {
|
||||
SuccessorMarketRenderer: React.FC<{ value: string }>;
|
||||
cellRenderers: {
|
||||
[name: string]: FC<{ value: string; data: ProposalListFieldsFragment }>;
|
||||
};
|
||||
}
|
||||
|
||||
export const ProposalsList = ({
|
||||
SuccessorMarketRenderer,
|
||||
}: ProposalListProps) => {
|
||||
export const ProposalsList = ({ cellRenderers }: ProposalListProps) => {
|
||||
const { data } = useProposalsListQuery({
|
||||
variables: {
|
||||
proposalType: Types.ProposalType.TYPE_NEW_MARKET,
|
||||
@ -32,7 +38,7 @@ export const ProposalsList = ({
|
||||
const filteredData = getNewMarketProposals(
|
||||
removePaginationWrapper(data?.proposalsConnection?.edges)
|
||||
);
|
||||
const { columnDefs, defaultColDef } = useColumnDefs();
|
||||
const columnDefs = useColumnDefs();
|
||||
|
||||
return (
|
||||
<AgGrid
|
||||
@ -40,8 +46,9 @@ export const ProposalsList = ({
|
||||
rowData={filteredData}
|
||||
defaultColDef={defaultColDef}
|
||||
getRowId={({ data }) => data.id}
|
||||
overlayNoRowsTemplate={t('No markets')}
|
||||
components={{ SuccessorMarketRenderer, MarketNameProposalCell }}
|
||||
overlayNoRowsTemplate={t('No proposed markets')}
|
||||
components={cellRenderers}
|
||||
rowHeight={45}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -6,9 +6,9 @@ import {
|
||||
COL_DEFS,
|
||||
DateRangeFilter,
|
||||
SetFilter,
|
||||
StackedCell,
|
||||
} from '@vegaprotocol/datagrid';
|
||||
import compact from 'lodash/compact';
|
||||
import { useEnvironment, FLAGS } from '@vegaprotocol/environment';
|
||||
import { getDateTimeFormat } from '@vegaprotocol/utils';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import {
|
||||
@ -19,50 +19,15 @@ import type {
|
||||
VegaICellRendererParams,
|
||||
VegaValueFormatterParams,
|
||||
} from '@vegaprotocol/datagrid';
|
||||
import { ExternalLink, Pill } from '@vegaprotocol/ui-toolkit';
|
||||
import {
|
||||
ProposalProductTypeMapping,
|
||||
ProposalProductTypeShortName,
|
||||
ProductTypeMapping,
|
||||
ProductTypeShortName,
|
||||
ProposalStateMapping,
|
||||
} from '@vegaprotocol/types';
|
||||
import type { ProposalListFieldsFragment } from '../../lib/proposals-data-provider/__generated__/Proposals';
|
||||
import { VoteProgress } from '../voting-progress';
|
||||
import { ProposalActionsDropdown } from '../proposal-actions-dropdown';
|
||||
|
||||
export const MarketNameProposalCell = ({
|
||||
value,
|
||||
data,
|
||||
}: VegaICellRendererParams<
|
||||
ProposalListFieldsFragment,
|
||||
'terms.change.instrument.code'
|
||||
>) => {
|
||||
const { VEGA_TOKEN_URL } = useEnvironment();
|
||||
const { change } = data?.terms || {};
|
||||
if (change?.__typename === 'NewMarket' && VEGA_TOKEN_URL) {
|
||||
const type = change.instrument.futureProduct?.__typename;
|
||||
const content = (
|
||||
<>
|
||||
<span data-testid="market-code">{value as string}</span>
|
||||
{type && (
|
||||
<Pill
|
||||
size="xxs"
|
||||
className="uppercase ml-0.5"
|
||||
title={ProposalProductTypeMapping[type]}
|
||||
>
|
||||
{ProposalProductTypeShortName[type]}
|
||||
</Pill>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
if (data?.id) {
|
||||
const link = `${VEGA_TOKEN_URL}/proposals/${data.id}`;
|
||||
return <ExternalLink href={link}>{content}</ExternalLink>;
|
||||
}
|
||||
return content;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export const useColumnDefs = () => {
|
||||
const { params } = useNetworkParams([
|
||||
NetworkParams.governance_proposal_market_requiredMajority,
|
||||
@ -80,17 +45,36 @@ export const useColumnDefs = () => {
|
||||
headerName: t('Market'),
|
||||
field: 'terms.change.instrument.code',
|
||||
cellStyle: { lineHeight: '14px' },
|
||||
cellRenderer: 'MarketNameProposalCell',
|
||||
},
|
||||
{
|
||||
colId: 'description',
|
||||
headerName: t('Description'),
|
||||
field: 'terms.change.instrument.name',
|
||||
cellRenderer: ({
|
||||
value,
|
||||
data,
|
||||
}: {
|
||||
value: string;
|
||||
data: ProposalListFieldsFragment;
|
||||
}) => {
|
||||
if (!value || !data) return '-';
|
||||
|
||||
// TODO: update when we switch to ProductConfiguration
|
||||
const productType = 'Future';
|
||||
return (
|
||||
<StackedCell
|
||||
primary={value}
|
||||
secondary={
|
||||
<span
|
||||
title={ProductTypeMapping[productType]}
|
||||
className="uppercase"
|
||||
>
|
||||
{ProductTypeShortName[productType]}
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
colId: 'asset',
|
||||
headerName: t('Settlement asset'),
|
||||
field: 'terms.change.instrument.futureProduct.settlementAsset.name',
|
||||
field: 'terms.change.instrument.futureProduct.settlementAsset.symbol',
|
||||
},
|
||||
{
|
||||
colId: 'state',
|
||||
@ -105,12 +89,10 @@ export const useColumnDefs = () => {
|
||||
set: ProposalStateMapping,
|
||||
},
|
||||
},
|
||||
FLAGS.SUCCESSOR_MARKETS && {
|
||||
{
|
||||
headerName: t('Parent market'),
|
||||
field: 'id',
|
||||
colId: 'parentMarket',
|
||||
cellRenderer: 'SuccessorMarketRenderer',
|
||||
cellRendererParams: { parent: true },
|
||||
field: 'terms.change.successorConfiguration.parentMarketId',
|
||||
cellRenderer: 'ParentMarketCell',
|
||||
},
|
||||
{
|
||||
colId: 'voting',
|
||||
@ -169,25 +151,12 @@ export const useColumnDefs = () => {
|
||||
data,
|
||||
}: VegaICellRendererParams<ProposalListFieldsFragment>) => {
|
||||
if (!data?.id) return null;
|
||||
|
||||
return <ProposalActionsDropdown id={data.id} />;
|
||||
},
|
||||
},
|
||||
]);
|
||||
}, [requiredMajorityPercentage]);
|
||||
|
||||
const defaultColDef: ColDef = useMemo(() => {
|
||||
return {
|
||||
sortable: true,
|
||||
filter: true,
|
||||
filterParams: { buttons: ['reset'] },
|
||||
};
|
||||
}, []);
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
columnDefs,
|
||||
defaultColDef,
|
||||
}),
|
||||
[columnDefs, defaultColDef]
|
||||
);
|
||||
return columnDefs;
|
||||
};
|
||||
|
@ -114,6 +114,9 @@ fragment NewMarketFields on NewMarket {
|
||||
lpPriceRange
|
||||
# linearSlippageFactor
|
||||
# quadraticSlippageFactor
|
||||
successorConfiguration {
|
||||
parentMarketId
|
||||
}
|
||||
}
|
||||
|
||||
fragment UpdateMarketFields on UpdateMarket {
|
||||
|
File diff suppressed because one or more lines are too long
@ -109,6 +109,58 @@ export const marketUpdateProposal: ProposalListFieldsFragment = {
|
||||
export const createProposalListFieldsFragment = (
|
||||
override?: PartialDeep<ProposalListFieldsFragment>
|
||||
): ProposalListFieldsFragment => {
|
||||
const newMarket = {
|
||||
decimalPlaces: 1,
|
||||
lpPriceRange: '',
|
||||
riskParameters: {
|
||||
__typename: 'SimpleRiskModel',
|
||||
params: {
|
||||
__typename: 'SimpleRiskModelParams',
|
||||
factorLong: 0,
|
||||
factorShort: 1,
|
||||
},
|
||||
},
|
||||
metadata: undefined,
|
||||
successorConfiguration: {
|
||||
__typename: 'SuccessorConfiguration',
|
||||
parentMarketId: 'xyz',
|
||||
},
|
||||
instrument: {
|
||||
code: 'ETHUSD',
|
||||
name: 'ETHUSD',
|
||||
futureProduct: {
|
||||
settlementAsset: {
|
||||
id: 'b340c130096819428a62e5df407fd6abe66e444b89ad64f670beb98621c9c663',
|
||||
name: 'tDAI TEST',
|
||||
symbol: 'tDAI',
|
||||
decimals: 1,
|
||||
quantum: '1',
|
||||
__typename: 'Asset',
|
||||
},
|
||||
quoteName: '',
|
||||
dataSourceSpecBinding: {
|
||||
__typename: 'DataSourceSpecToFutureBinding',
|
||||
settlementDataProperty: '',
|
||||
tradingTerminationProperty: '',
|
||||
},
|
||||
dataSourceSpecForSettlementData: {
|
||||
__typename: 'DataSourceDefinition',
|
||||
sourceType: {
|
||||
__typename: 'DataSourceDefinitionInternal',
|
||||
},
|
||||
},
|
||||
dataSourceSpecForTradingTermination: {
|
||||
__typename: 'DataSourceDefinition',
|
||||
sourceType: {
|
||||
__typename: 'DataSourceDefinitionInternal',
|
||||
},
|
||||
},
|
||||
__typename: 'FutureProduct',
|
||||
},
|
||||
__typename: 'InstrumentConfiguration',
|
||||
},
|
||||
__typename: 'NewMarket',
|
||||
} as const;
|
||||
const defaultProposal: ProposalListFieldsFragment = {
|
||||
id: 'e9ec6d5c46a7e7bcabf9ba7a893fa5a5eeeec08b731f06f7a6eb7bf0e605b829',
|
||||
reference: 'injected_at_runtime',
|
||||
@ -147,54 +199,7 @@ export const createProposalListFieldsFragment = (
|
||||
terms: {
|
||||
closingDatetime: '2022-11-15T12:44:34Z',
|
||||
enactmentDatetime: '2022-11-15T12:44:54Z',
|
||||
change: {
|
||||
decimalPlaces: 1,
|
||||
lpPriceRange: '',
|
||||
riskParameters: {
|
||||
__typename: 'SimpleRiskModel',
|
||||
params: {
|
||||
__typename: 'SimpleRiskModelParams',
|
||||
factorLong: 0,
|
||||
factorShort: 1,
|
||||
},
|
||||
},
|
||||
metadata: [],
|
||||
instrument: {
|
||||
code: 'ETHUSD',
|
||||
name: 'ETHUSD',
|
||||
futureProduct: {
|
||||
settlementAsset: {
|
||||
id: 'b340c130096819428a62e5df407fd6abe66e444b89ad64f670beb98621c9c663',
|
||||
name: 'tDAI TEST',
|
||||
symbol: 'tDAI',
|
||||
decimals: 1,
|
||||
quantum: '1',
|
||||
__typename: 'Asset',
|
||||
},
|
||||
quoteName: '',
|
||||
dataSourceSpecBinding: {
|
||||
__typename: 'DataSourceSpecToFutureBinding',
|
||||
settlementDataProperty: '',
|
||||
tradingTerminationProperty: '',
|
||||
},
|
||||
dataSourceSpecForSettlementData: {
|
||||
__typename: 'DataSourceDefinition',
|
||||
sourceType: {
|
||||
__typename: 'DataSourceDefinitionInternal',
|
||||
},
|
||||
},
|
||||
dataSourceSpecForTradingTermination: {
|
||||
__typename: 'DataSourceDefinition',
|
||||
sourceType: {
|
||||
__typename: 'DataSourceDefinitionInternal',
|
||||
},
|
||||
},
|
||||
__typename: 'FutureProduct',
|
||||
},
|
||||
__typename: 'InstrumentConfiguration',
|
||||
},
|
||||
__typename: 'NewMarket',
|
||||
},
|
||||
change: newMarket,
|
||||
__typename: 'ProposalTerms',
|
||||
},
|
||||
__typename: 'Proposal',
|
||||
|
Loading…
Reference in New Issue
Block a user