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 { MemoryRouter } from 'react-router-dom';
|
||||||
import { Closed } from './closed';
|
import { Closed } from './closed';
|
||||||
import { MarketStateMapping, PropertyKeyType } from '@vegaprotocol/types';
|
import { MarketStateMapping, PropertyKeyType } from '@vegaprotocol/types';
|
||||||
@ -10,55 +10,21 @@ import type {
|
|||||||
OracleSpecDataConnectionQuery,
|
OracleSpecDataConnectionQuery,
|
||||||
MarketsDataQuery,
|
MarketsDataQuery,
|
||||||
MarketsQuery,
|
MarketsQuery,
|
||||||
SuccessorMarketIdsQuery,
|
|
||||||
} from '@vegaprotocol/markets';
|
} from '@vegaprotocol/markets';
|
||||||
import {
|
import {
|
||||||
OracleSpecDataConnectionDocument,
|
OracleSpecDataConnectionDocument,
|
||||||
MarketsDataDocument,
|
MarketsDataDocument,
|
||||||
MarketsDocument,
|
MarketsDocument,
|
||||||
SuccessorMarketIdsDocument,
|
|
||||||
} from '@vegaprotocol/markets';
|
} from '@vegaprotocol/markets';
|
||||||
import type { VegaWalletContextShape } from '@vegaprotocol/wallet';
|
import type { VegaWalletContextShape } from '@vegaprotocol/wallet';
|
||||||
import { VegaWalletContext } from '@vegaprotocol/wallet';
|
import { VegaWalletContext } from '@vegaprotocol/wallet';
|
||||||
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
|
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
|
||||||
import { FLAGS } from '@vegaprotocol/environment';
|
|
||||||
import {
|
import {
|
||||||
createMarketFragment,
|
createMarketFragment,
|
||||||
marketsQuery,
|
marketsQuery,
|
||||||
marketsDataQuery,
|
marketsDataQuery,
|
||||||
createMarketsDataFragment,
|
createMarketsDataFragment,
|
||||||
} from '@vegaprotocol/mock';
|
} 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', () => {
|
describe('Closed', () => {
|
||||||
let originalNow: typeof Date.now;
|
let originalNow: typeof Date.now;
|
||||||
@ -218,10 +184,8 @@ describe('Closed', () => {
|
|||||||
const headers = screen.getAllByRole('columnheader');
|
const headers = screen.getAllByRole('columnheader');
|
||||||
const expectedHeaders = [
|
const expectedHeaders = [
|
||||||
'Market',
|
'Market',
|
||||||
'Description',
|
|
||||||
'Status',
|
'Status',
|
||||||
'Settlement date',
|
'Settlement date',
|
||||||
'Successor market',
|
|
||||||
'Best bid',
|
'Best bid',
|
||||||
'Best offer',
|
'Best offer',
|
||||||
'Mark price',
|
'Mark price',
|
||||||
@ -235,10 +199,8 @@ describe('Closed', () => {
|
|||||||
const cells = screen.getAllByRole('gridcell');
|
const cells = screen.getAllByRole('gridcell');
|
||||||
const expectedValues = [
|
const expectedValues = [
|
||||||
market.tradableInstrument.instrument.code,
|
market.tradableInstrument.instrument.code,
|
||||||
market.tradableInstrument.instrument.name,
|
|
||||||
MarketStateMapping[market.state],
|
MarketStateMapping[market.state],
|
||||||
'3 days ago',
|
'3 days ago',
|
||||||
'-',
|
|
||||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||||
addDecimalsFormatNumber(marketsData.bestBidPrice, market.decimalPlaces),
|
addDecimalsFormatNumber(marketsData.bestBidPrice, market.decimalPlaces),
|
||||||
addDecimalsFormatNumber(
|
addDecimalsFormatNumber(
|
||||||
@ -340,43 +302,26 @@ describe('Closed', () => {
|
|||||||
.getAllByRole('gridcell')
|
.getAllByRole('gridcell')
|
||||||
.filter((cell) => cell.getAttribute('col-id') === 'code')
|
.filter((cell) => cell.getAttribute('col-id') === 'code')
|
||||||
.map((cell) => {
|
.map((cell) => {
|
||||||
const marketId = within(cell)
|
const marketCode = within(cell).getByTestId('stack-cell-primary');
|
||||||
.getByTestId('market-code')
|
return marketCode.textContent;
|
||||||
.getAttribute('data-market-id');
|
|
||||||
return marketId;
|
|
||||||
});
|
});
|
||||||
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 () => {
|
it('successor marked should be visible', async () => {
|
||||||
const mixedMarkets = [
|
const marketsWithSuccessorID = [
|
||||||
{
|
{
|
||||||
__typename: 'MarketEdge' as const,
|
__typename: 'MarketEdge' as const,
|
||||||
node: createMarketFragment({
|
node: createMarketFragment({
|
||||||
id: 'include-0',
|
id: 'include-0',
|
||||||
state: MarketState.STATE_SETTLED,
|
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: {
|
request: {
|
||||||
query: MarketsDocument,
|
query: MarketsDocument,
|
||||||
},
|
},
|
||||||
@ -384,42 +329,17 @@ describe('Closed', () => {
|
|||||||
data: {
|
data: {
|
||||||
marketsConnection: {
|
marketsConnection: {
|
||||||
__typename: 'MarketConnection',
|
__typename: 'MarketConnection',
|
||||||
edges: mixedMarkets,
|
edges: marketsWithSuccessorID,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const successorMarketsMock: MockedResponse<SuccessorMarketIdsQuery> = {
|
|
||||||
request: {
|
await act(async () => {
|
||||||
query: SuccessorMarketIdsDocument,
|
|
||||||
},
|
|
||||||
result: {
|
|
||||||
data: {
|
|
||||||
marketsConnection: {
|
|
||||||
__typename: 'MarketConnection',
|
|
||||||
edges: [
|
|
||||||
{
|
|
||||||
node: {
|
|
||||||
id: 'include-0',
|
|
||||||
successorMarketID: 'successorMarketID',
|
|
||||||
parentMarketID: '',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
await act(() => {
|
|
||||||
render(
|
render(
|
||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
<MockedProvider
|
<MockedProvider
|
||||||
mocks={[
|
mocks={[mockWithSuccessors, marketsDataMock, oracleDataMock]}
|
||||||
mixedMarketsMock,
|
|
||||||
marketsDataMock,
|
|
||||||
oracleDataMock,
|
|
||||||
successorMarketsMock,
|
|
||||||
]}
|
|
||||||
>
|
>
|
||||||
<VegaWalletContext.Provider
|
<VegaWalletContext.Provider
|
||||||
value={{ pubKey } as VegaWalletContextShape}
|
value={{ pubKey } as VegaWalletContextShape}
|
||||||
@ -431,122 +351,15 @@ describe('Closed', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
await waitFor(() => {
|
const container = within(
|
||||||
expect(
|
document.querySelector('.ag-center-cols-container') as HTMLElement
|
||||||
screen.getByRole('button', { name: /^SuccessorCode/ })
|
);
|
||||||
).toBeInTheDocument();
|
const cell = container.getAllByRole('gridcell', {
|
||||||
});
|
name: (_name, element) => element.getAttribute('col-id') === 'code',
|
||||||
expect(
|
})[0];
|
||||||
screen.getByRole('columnheader', {
|
|
||||||
name: (_name, element) =>
|
expect(within(cell).getByTestId('stack-cell-secondary')).toHaveTextContent(
|
||||||
element.getAttribute('col-id') === 'successorMarket',
|
'PRNT'
|
||||||
})
|
|
||||||
).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>
|
|
||||||
);
|
);
|
||||||
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,
|
VegaICellRendererParams,
|
||||||
VegaValueFormatterParams,
|
VegaValueFormatterParams,
|
||||||
} from '@vegaprotocol/datagrid';
|
} from '@vegaprotocol/datagrid';
|
||||||
import {
|
import { AgGridLazy as AgGrid, COL_DEFS } from '@vegaprotocol/datagrid';
|
||||||
AgGridLazy as AgGrid,
|
|
||||||
COL_DEFS,
|
|
||||||
MarketNameCell,
|
|
||||||
} from '@vegaprotocol/datagrid';
|
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { t } from '@vegaprotocol/i18n';
|
import { t } from '@vegaprotocol/i18n';
|
||||||
|
import type { ProductType } from '@vegaprotocol/types';
|
||||||
import { MarketState, MarketStateMapping } from '@vegaprotocol/types';
|
import { MarketState, MarketStateMapping } from '@vegaprotocol/types';
|
||||||
import {
|
import {
|
||||||
addDecimalsFormatNumber,
|
addDecimalsFormatNumber,
|
||||||
@ -20,17 +17,13 @@ import type {
|
|||||||
DataSourceFilterFragment,
|
DataSourceFilterFragment,
|
||||||
MarketMaybeWithData,
|
MarketMaybeWithData,
|
||||||
} from '@vegaprotocol/markets';
|
} from '@vegaprotocol/markets';
|
||||||
import {
|
import { closedMarketsWithDataProvider } from '@vegaprotocol/markets';
|
||||||
MarketActionsDropdown,
|
|
||||||
closedMarketsWithDataProvider,
|
|
||||||
} from '@vegaprotocol/markets';
|
|
||||||
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
|
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
|
||||||
import type { ColDef } from 'ag-grid-community';
|
|
||||||
import { FLAGS } from '@vegaprotocol/environment';
|
|
||||||
import { SettlementDateCell } from './settlement-date-cell';
|
import { SettlementDateCell } from './settlement-date-cell';
|
||||||
import { SettlementPriceCell } from './settlement-price-cell';
|
import { SettlementPriceCell } from './settlement-price-cell';
|
||||||
import { useDataProvider } from '@vegaprotocol/data-provider';
|
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 =
|
type SettlementAsset =
|
||||||
MarketMaybeWithData['tradableInstrument']['instrument']['product']['settlementAsset'];
|
MarketMaybeWithData['tradableInstrument']['instrument']['product']['settlementAsset'];
|
||||||
@ -51,7 +44,9 @@ interface Row {
|
|||||||
setlementDataSourceFilter: DataSourceFilterFragment | undefined;
|
setlementDataSourceFilter: DataSourceFilterFragment | undefined;
|
||||||
tradingTerminationOracleId: string;
|
tradingTerminationOracleId: string;
|
||||||
settlementAsset: SettlementAsset;
|
settlementAsset: SettlementAsset;
|
||||||
productType: string;
|
productType: ProductType | undefined;
|
||||||
|
successorMarketID: string | null | undefined;
|
||||||
|
parentMarketID: string | null | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Closed = () => {
|
export const Closed = () => {
|
||||||
@ -95,16 +90,19 @@ export const Closed = () => {
|
|||||||
tradingTerminationOracleId:
|
tradingTerminationOracleId:
|
||||||
instrument.product.dataSourceSpecForTradingTermination.id,
|
instrument.product.dataSourceSpecForTradingTermination.id,
|
||||||
settlementAsset: instrument.product.settlementAsset,
|
settlementAsset: instrument.product.settlementAsset,
|
||||||
productType: instrument.product.__typename || '',
|
productType: instrument.product.__typename,
|
||||||
|
successorMarketID: market.successorMarketID,
|
||||||
|
parentMarketID: market.parentMarketID,
|
||||||
};
|
};
|
||||||
|
|
||||||
return row;
|
return row;
|
||||||
});
|
});
|
||||||
return (
|
|
||||||
<div className="h-full relative">
|
return <ClosedMarketsDataGrid rowData={rowData} error={error} />;
|
||||||
<ClosedMarketsDataGrid rowData={rowData} error={error} />
|
};
|
||||||
</div>
|
|
||||||
);
|
const components = {
|
||||||
|
MarketCodeCell,
|
||||||
};
|
};
|
||||||
|
|
||||||
const ClosedMarketsDataGrid = ({
|
const ClosedMarketsDataGrid = ({
|
||||||
@ -117,15 +115,11 @@ const ClosedMarketsDataGrid = ({
|
|||||||
const openAssetDialog = useAssetDetailsDialogStore((store) => store.open);
|
const openAssetDialog = useAssetDetailsDialogStore((store) => store.open);
|
||||||
|
|
||||||
const colDefs = useMemo(() => {
|
const colDefs = useMemo(() => {
|
||||||
const cols: ColDef[] = compact([
|
return [
|
||||||
{
|
{
|
||||||
headerName: t('Market'),
|
headerName: t('Market'),
|
||||||
field: 'code',
|
field: 'code',
|
||||||
cellRenderer: 'MarketNameCell',
|
cellRenderer: 'MarketCodeCell',
|
||||||
},
|
|
||||||
{
|
|
||||||
headerName: t('Description'),
|
|
||||||
field: 'name',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
headerName: t('Status'),
|
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'),
|
headerName: t('Best bid'),
|
||||||
field: 'bestBidPrice',
|
field: 'bestBidPrice',
|
||||||
@ -263,12 +251,13 @@ const ClosedMarketsDataGrid = ({
|
|||||||
<MarketActionsDropdown
|
<MarketActionsDropdown
|
||||||
marketId={data.id}
|
marketId={data.id}
|
||||||
assetId={data.settlementAsset.id}
|
assetId={data.settlementAsset.id}
|
||||||
|
successorMarketID={data.successorMarketID}
|
||||||
|
parentMarketID={data.parentMarketID}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]);
|
];
|
||||||
return cols;
|
|
||||||
}, [openAssetDialog]);
|
}, [openAssetDialog]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -276,8 +265,8 @@ const ClosedMarketsDataGrid = ({
|
|||||||
rowData={rowData}
|
rowData={rowData}
|
||||||
columnDefs={colDefs}
|
columnDefs={colDefs}
|
||||||
getRowId={({ data }) => data.id}
|
getRowId={({ data }) => data.id}
|
||||||
components={{ SuccessorMarketRenderer, MarketNameCell }}
|
|
||||||
overlayNoRowsTemplate={error ? error.message : t('No markets')}
|
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';
|
} from '@vegaprotocol/ui-toolkit';
|
||||||
import { DApp, EXPLORER_MARKET, useLinks } from '@vegaprotocol/environment';
|
import { DApp, EXPLORER_MARKET, useLinks } from '@vegaprotocol/environment';
|
||||||
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
|
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { Links, Routes } from '../../pages/client-router';
|
||||||
|
|
||||||
export const MarketActionsDropdown = ({
|
export const MarketActionsDropdown = ({
|
||||||
marketId,
|
marketId,
|
||||||
assetId,
|
assetId,
|
||||||
|
successorMarketID,
|
||||||
|
parentMarketID,
|
||||||
}: {
|
}: {
|
||||||
marketId: string;
|
marketId: string;
|
||||||
assetId: string;
|
assetId: string;
|
||||||
|
successorMarketID: string | null | undefined;
|
||||||
|
parentMarketID: string | null | undefined;
|
||||||
}) => {
|
}) => {
|
||||||
|
const navigate = useNavigate();
|
||||||
const open = useAssetDetailsDialogStore((store) => store.open);
|
const open = useAssetDetailsDialogStore((store) => store.open);
|
||||||
const linkCreator = useLinks(DApp.Explorer);
|
const linkCreator = useLinks(DApp.Explorer);
|
||||||
|
|
||||||
@ -42,6 +49,26 @@ export const MarketActionsDropdown = ({
|
|||||||
<VegaIcon name={VegaIconNames.INFO} size={16} />
|
<VegaIcon name={VegaIconNames.INFO} size={16} />
|
||||||
{t('View settlement asset details')}
|
{t('View settlement asset details')}
|
||||||
</TradingDropdownItem>
|
</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>
|
</ActionsDropdown>
|
||||||
);
|
);
|
||||||
};
|
};
|
@ -6,7 +6,7 @@ import {
|
|||||||
Tab,
|
Tab,
|
||||||
TradingAnchorButton,
|
TradingAnchorButton,
|
||||||
} from '@vegaprotocol/ui-toolkit';
|
} from '@vegaprotocol/ui-toolkit';
|
||||||
import { Markets } from './markets';
|
import { OpenMarkets } from './open-markets';
|
||||||
import { Proposed } from './proposed';
|
import { Proposed } from './proposed';
|
||||||
import { usePageTitleStore } from '../../stores';
|
import { usePageTitleStore } from '../../stores';
|
||||||
import { Closed } from './closed';
|
import { Closed } from './closed';
|
||||||
@ -33,7 +33,7 @@ export const MarketsPage = () => {
|
|||||||
<div className="h-full my-1 border rounded-sm border-default">
|
<div className="h-full my-1 border rounded-sm border-default">
|
||||||
<Tabs storageKey="console-markets">
|
<Tabs storageKey="console-markets">
|
||||||
<Tab id="open-markets" name={t('Open markets')}>
|
<Tab id="open-markets" name={t('Open markets')}>
|
||||||
<Markets />
|
<OpenMarkets />
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab
|
<Tab
|
||||||
id="proposed-markets"
|
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 { ProposalsList } from '@vegaprotocol/proposals';
|
||||||
import { SuccessorMarketRenderer } from './successor-market-cell';
|
import { ParentMarketCell } from './parent-market-cell';
|
||||||
|
|
||||||
|
const cellRenderers = {
|
||||||
|
ParentMarketCell,
|
||||||
|
};
|
||||||
|
|
||||||
export const Proposed = () => {
|
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 { useCallback } from 'react';
|
||||||
import { Links, Routes } from '../../pages/client-router';
|
import { Links, Routes } from '../../pages/client-router';
|
||||||
|
|
||||||
export const useMarketClickHandler = (replace = false) => {
|
export const useMarketClickHandler = (replace = false) => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { marketId } = useParams();
|
|
||||||
const { pathname } = useLocation();
|
|
||||||
const isMarketPage = pathname.match(/^\/markets\/(.+)/);
|
|
||||||
|
|
||||||
return useCallback(
|
return (selectedId: string, metaKey?: boolean) => {
|
||||||
(selectedId: string, metaKey?: boolean) => {
|
const link = Links[Routes.MARKET](selectedId);
|
||||||
const link = Links[Routes.MARKET](selectedId);
|
if (metaKey) {
|
||||||
if (metaKey) {
|
window.open(`/#${link}`, '_blank');
|
||||||
window.open(`/#${link}`, '_blank');
|
} else {
|
||||||
} else if (selectedId !== marketId || !isMarketPage) {
|
navigate(link, { replace });
|
||||||
navigate(link, { replace });
|
}
|
||||||
}
|
};
|
||||||
},
|
|
||||||
[navigate, marketId, replace, isMarketPage]
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useMarketLiquidityClickHandler = () => {
|
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/market-name-cell';
|
||||||
export * from './lib/cells/order-type-cell';
|
export * from './lib/cells/order-type-cell';
|
||||||
export * from './lib/cells/size';
|
export * from './lib/cells/size';
|
||||||
|
export * from './lib/cells/stacked-cell';
|
||||||
|
|
||||||
export * from './lib/filters/date-range-filter';
|
export * from './lib/filters/date-range-filter';
|
||||||
export * from './lib/filters/set-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 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 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`
|
export const DataSourceFilterFragmentDoc = gql`
|
||||||
fragment DataSourceFilter on Filter {
|
fragment DataSourceFilter on Filter {
|
||||||
@ -55,6 +55,8 @@ export const MarketFieldsFragmentDoc = gql`
|
|||||||
positionDecimalPlaces
|
positionDecimalPlaces
|
||||||
state
|
state
|
||||||
tradingMode
|
tradingMode
|
||||||
|
parentMarketID
|
||||||
|
successorMarketID
|
||||||
fees {
|
fees {
|
||||||
factors {
|
factors {
|
||||||
makerFee
|
makerFee
|
||||||
|
@ -2,7 +2,6 @@ export * from './fees-breakdown';
|
|||||||
export * from './last-24h-price-change';
|
export * from './last-24h-price-change';
|
||||||
export * from './last-24h-volume';
|
export * from './last-24h-volume';
|
||||||
export * from './market-info';
|
export * from './market-info';
|
||||||
export * from './markets-container';
|
|
||||||
export * from './oracle-banner';
|
export * from './oracle-banner';
|
||||||
export * from './oracle-basic-profile';
|
export * from './oracle-basic-profile';
|
||||||
export * from './oracle-full-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
|
const lastVerified = provider.oracle.last_verified
|
||||||
? new Date(provider.oracle.last_verified)
|
? new Date(provider.oracle.last_verified)
|
||||||
: new Date(provider.oracle.first_verified);
|
: new Date(provider.oracle.first_verified);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...getIconIntent(),
|
...getIconIntent(),
|
||||||
message: t(
|
message: t(
|
||||||
@ -112,13 +113,13 @@ export const OracleBasicProfile = ({
|
|||||||
<Icon size={3} name={icon as IconName} />
|
<Icon size={3} name={icon as IconName} />
|
||||||
</span>
|
</span>
|
||||||
</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}
|
{message}
|
||||||
</p>
|
</p>
|
||||||
{oracleMarkets && (
|
{oracleMarkets && (
|
||||||
<p
|
<p
|
||||||
data-testid="signed-proofs"
|
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', [
|
{t('Involved in %s %s', [
|
||||||
oracleMarkets.length.toString(),
|
oracleMarkets.length.toString(),
|
||||||
@ -130,9 +131,9 @@ export const OracleBasicProfile = ({
|
|||||||
<div className="flex flex-row gap-3">
|
<div className="flex flex-row gap-3">
|
||||||
{links.map((link) => (
|
{links.map((link) => (
|
||||||
<ExternalLink key={link.url} href={link.url} data-testid={link.url}>
|
<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)} />
|
<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} />
|
<VegaIcon name={VegaIconNames.OPEN_EXTERNAL} size={13} />
|
||||||
</span>
|
</span>
|
||||||
</ExternalLink>
|
</ExternalLink>
|
||||||
|
@ -28,8 +28,6 @@ import {
|
|||||||
} from './market-utils';
|
} from './market-utils';
|
||||||
import { MarketsDocument } from './__generated__/markets';
|
import { MarketsDocument } from './__generated__/markets';
|
||||||
import type { Candle } from './market-candles-provider';
|
import type { Candle } from './market-candles-provider';
|
||||||
import type { SuccessorMarketIdsQuery } from './__generated__/SuccessorMarket';
|
|
||||||
import { SuccessorMarketIdsDocument } from './__generated__';
|
|
||||||
|
|
||||||
export type Market = MarketFieldsFragment;
|
export type Market = MarketFieldsFragment;
|
||||||
|
|
||||||
@ -240,34 +238,3 @@ export const useMarketList = () => {
|
|||||||
reload,
|
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
|
positionDecimalPlaces
|
||||||
state
|
state
|
||||||
tradingMode
|
tradingMode
|
||||||
|
parentMarketID
|
||||||
|
successorMarketID
|
||||||
fees {
|
fees {
|
||||||
factors {
|
factors {
|
||||||
makerFee
|
makerFee
|
||||||
|
@ -42,6 +42,8 @@ export const createMarketFragment = (
|
|||||||
close: null,
|
close: null,
|
||||||
open: null,
|
open: null,
|
||||||
},
|
},
|
||||||
|
successorMarketID: null,
|
||||||
|
parentMarketID: null,
|
||||||
fees: {
|
fees: {
|
||||||
__typename: 'Fees',
|
__typename: 'Fees',
|
||||||
factors: {
|
factors: {
|
||||||
|
@ -15,6 +15,7 @@ import {
|
|||||||
MarketNameCell,
|
MarketNameCell,
|
||||||
ProgressBarCell,
|
ProgressBarCell,
|
||||||
MarketProductPill,
|
MarketProductPill,
|
||||||
|
StackedCell,
|
||||||
} from '@vegaprotocol/datagrid';
|
} from '@vegaprotocol/datagrid';
|
||||||
import {
|
import {
|
||||||
ButtonLink,
|
ButtonLink,
|
||||||
@ -40,7 +41,6 @@ import {
|
|||||||
import { DocsLinks } from '@vegaprotocol/environment';
|
import { DocsLinks } from '@vegaprotocol/environment';
|
||||||
import { PositionActionsDropdown } from './position-actions-dropdown';
|
import { PositionActionsDropdown } from './position-actions-dropdown';
|
||||||
import { LiquidationPrice } from './liquidation-price';
|
import { LiquidationPrice } from './liquidation-price';
|
||||||
import { StackedCell } from './stacked-cell';
|
|
||||||
|
|
||||||
interface Props extends TypedDataAgGrid<Position> {
|
interface Props extends TypedDataAgGrid<Position> {
|
||||||
onClose?: (data: Position) => void;
|
onClose?: (data: Position) => void;
|
||||||
|
@ -1,10 +1,4 @@
|
|||||||
import {
|
import { render, screen, waitFor, within } from '@testing-library/react';
|
||||||
render,
|
|
||||||
screen,
|
|
||||||
act,
|
|
||||||
waitFor,
|
|
||||||
getAllByRole,
|
|
||||||
} from '@testing-library/react';
|
|
||||||
import merge from 'lodash/merge';
|
import merge from 'lodash/merge';
|
||||||
import type { MockedResponse } from '@apollo/client/testing';
|
import type { MockedResponse } from '@apollo/client/testing';
|
||||||
import { MockedProvider } 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 type { ProposalsListQuery } from '../../lib';
|
||||||
import { ProposalsListDocument } from '../../lib';
|
import { ProposalsListDocument } from '../../lib';
|
||||||
import type { PartialDeep } from 'type-fest';
|
import type { PartialDeep } from 'type-fest';
|
||||||
import { FLAGS } from '@vegaprotocol/environment';
|
|
||||||
|
|
||||||
jest.mock('@vegaprotocol/environment', () => {
|
const parentMarketName = 'Parent Market Name';
|
||||||
const actual = jest.requireActual('@vegaprotocol/environment');
|
const ParentMarketCell = () => <span>{parentMarketName}</span>;
|
||||||
return {
|
|
||||||
...actual,
|
|
||||||
FLAGS: {
|
|
||||||
...actual.FLAGS,
|
|
||||||
SUCCESSOR_MARKETS: true,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const successorMarketName = 'Successor Market Name';
|
|
||||||
const spySuccessorMarketRenderer = jest
|
|
||||||
.fn()
|
|
||||||
.mockReturnValue(successorMarketName);
|
|
||||||
|
|
||||||
describe('ProposalsList', () => {
|
describe('ProposalsList', () => {
|
||||||
|
const rowContainerSelector = '.ag-center-cols-container';
|
||||||
|
|
||||||
const createProposalsMock = (override?: PartialDeep<ProposalsListQuery>) => {
|
const createProposalsMock = (override?: PartialDeep<ProposalsListQuery>) => {
|
||||||
const defaultProposalEdges = [
|
const defaultProposalEdges = [
|
||||||
{
|
{
|
||||||
@ -81,75 +63,55 @@ describe('ProposalsList', () => {
|
|||||||
|
|
||||||
return mock;
|
return mock;
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be properly rendered', async () => {
|
it('should be properly rendered', async () => {
|
||||||
const mock = createProposalsMock();
|
const mock = createProposalsMock();
|
||||||
await act(() => {
|
render(
|
||||||
render(
|
<MockedProvider mocks={[mock]}>
|
||||||
<MockedProvider mocks={[mock]}>
|
<ProposalsList cellRenderers={{ ParentMarketCell }} />
|
||||||
<ProposalsList SuccessorMarketRenderer={spySuccessorMarketRenderer} />
|
</MockedProvider>
|
||||||
</MockedProvider>
|
);
|
||||||
);
|
|
||||||
});
|
|
||||||
const container = document.querySelector('.ag-center-cols-container');
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(container).toBeInTheDocument();
|
expect(document.querySelector(rowContainerSelector)).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,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const mock = createProposalsMock({
|
const expectedHeaders = [
|
||||||
proposalsConnection: {
|
'Market',
|
||||||
edges: [
|
'Settlement asset',
|
||||||
{
|
'State',
|
||||||
__typename: 'ProposalEdge',
|
'Parent market',
|
||||||
node: {
|
'Voting',
|
||||||
...proposalNode,
|
'Closing date',
|
||||||
terms: {
|
'Enactment date',
|
||||||
...proposalNode.terms,
|
'', // actions col
|
||||||
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);
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(spySuccessorMarketRenderer).toHaveBeenCalled();
|
const headers = screen.getAllByRole('columnheader');
|
||||||
|
expect(headers).toHaveLength(expectedHeaders.length);
|
||||||
expect(
|
expect(
|
||||||
screen.getByRole('columnheader', {
|
headers.map((h) => h.querySelector('[ref="eText"]')?.textContent?.trim())
|
||||||
name: (_name, element) =>
|
).toEqual(expectedHeaders);
|
||||||
element.getAttribute('col-id') === 'parentMarket',
|
|
||||||
})
|
const container = within(
|
||||||
).toBeInTheDocument();
|
document.querySelector(rowContainerSelector) as HTMLElement
|
||||||
|
);
|
||||||
|
expect(container.getAllByRole('row')).toHaveLength(
|
||||||
|
// @ts-ignore data is mocked
|
||||||
|
mock?.result?.data.proposalsConnection.edges.length
|
||||||
|
);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
screen.getAllByRole('gridcell', {
|
container.getAllByRole('gridcell', {
|
||||||
name: (name, element) =>
|
name: (_, element) =>
|
||||||
element.getAttribute('col-id') === 'parentMarket',
|
element.getAttribute('col-id') ===
|
||||||
|
'terms.change.successorConfiguration.parentMarketId',
|
||||||
})[0]
|
})[0]
|
||||||
).toHaveTextContent(successorMarketName);
|
).toHaveTextContent(parentMarketName);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('empty response should causes no data message display', async () => {
|
it('empty response should causes no data message display', async () => {
|
||||||
@ -169,58 +131,11 @@ describe('ProposalsList', () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
await act(() => {
|
render(
|
||||||
render(
|
<MockedProvider mocks={[mock]}>
|
||||||
<MockedProvider mocks={[mock]}>
|
<ProposalsList cellRenderers={{ ParentMarketCell }} />
|
||||||
<ProposalsList SuccessorMarketRenderer={spySuccessorMarketRenderer} />
|
</MockedProvider>
|
||||||
</MockedProvider>
|
);
|
||||||
);
|
expect(await screen.findByText('No proposed markets')).toBeInTheDocument();
|
||||||
});
|
|
||||||
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');
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import React from 'react';
|
import type { FC } from 'react';
|
||||||
import { AgGridLazy as AgGrid } from '@vegaprotocol/datagrid';
|
import { AgGridLazy as AgGrid } from '@vegaprotocol/datagrid';
|
||||||
import { t } from '@vegaprotocol/i18n';
|
import { t } from '@vegaprotocol/i18n';
|
||||||
import * as Types from '@vegaprotocol/types';
|
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 type { ProposalListFieldsFragment } from '../../lib/proposals-data-provider/__generated__/Proposals';
|
||||||
import { useProposalsListQuery } 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[]) =>
|
export const getNewMarketProposals = (data: ProposalListFieldsFragment[]) =>
|
||||||
data.filter((proposal) =>
|
data.filter((proposal) =>
|
||||||
@ -16,13 +16,19 @@ export const getNewMarketProposals = (data: ProposalListFieldsFragment[]) =>
|
|||||||
].includes(proposal.state)
|
].includes(proposal.state)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const defaultColDef = {
|
||||||
|
sortable: true,
|
||||||
|
filter: true,
|
||||||
|
filterParams: { buttons: ['reset'] },
|
||||||
|
};
|
||||||
|
|
||||||
interface ProposalListProps {
|
interface ProposalListProps {
|
||||||
SuccessorMarketRenderer: React.FC<{ value: string }>;
|
cellRenderers: {
|
||||||
|
[name: string]: FC<{ value: string; data: ProposalListFieldsFragment }>;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ProposalsList = ({
|
export const ProposalsList = ({ cellRenderers }: ProposalListProps) => {
|
||||||
SuccessorMarketRenderer,
|
|
||||||
}: ProposalListProps) => {
|
|
||||||
const { data } = useProposalsListQuery({
|
const { data } = useProposalsListQuery({
|
||||||
variables: {
|
variables: {
|
||||||
proposalType: Types.ProposalType.TYPE_NEW_MARKET,
|
proposalType: Types.ProposalType.TYPE_NEW_MARKET,
|
||||||
@ -32,7 +38,7 @@ export const ProposalsList = ({
|
|||||||
const filteredData = getNewMarketProposals(
|
const filteredData = getNewMarketProposals(
|
||||||
removePaginationWrapper(data?.proposalsConnection?.edges)
|
removePaginationWrapper(data?.proposalsConnection?.edges)
|
||||||
);
|
);
|
||||||
const { columnDefs, defaultColDef } = useColumnDefs();
|
const columnDefs = useColumnDefs();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AgGrid
|
<AgGrid
|
||||||
@ -40,8 +46,9 @@ export const ProposalsList = ({
|
|||||||
rowData={filteredData}
|
rowData={filteredData}
|
||||||
defaultColDef={defaultColDef}
|
defaultColDef={defaultColDef}
|
||||||
getRowId={({ data }) => data.id}
|
getRowId={({ data }) => data.id}
|
||||||
overlayNoRowsTemplate={t('No markets')}
|
overlayNoRowsTemplate={t('No proposed markets')}
|
||||||
components={{ SuccessorMarketRenderer, MarketNameProposalCell }}
|
components={cellRenderers}
|
||||||
|
rowHeight={45}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -6,9 +6,9 @@ import {
|
|||||||
COL_DEFS,
|
COL_DEFS,
|
||||||
DateRangeFilter,
|
DateRangeFilter,
|
||||||
SetFilter,
|
SetFilter,
|
||||||
|
StackedCell,
|
||||||
} from '@vegaprotocol/datagrid';
|
} from '@vegaprotocol/datagrid';
|
||||||
import compact from 'lodash/compact';
|
import compact from 'lodash/compact';
|
||||||
import { useEnvironment, FLAGS } from '@vegaprotocol/environment';
|
|
||||||
import { getDateTimeFormat } from '@vegaprotocol/utils';
|
import { getDateTimeFormat } from '@vegaprotocol/utils';
|
||||||
import { t } from '@vegaprotocol/i18n';
|
import { t } from '@vegaprotocol/i18n';
|
||||||
import {
|
import {
|
||||||
@ -19,50 +19,15 @@ import type {
|
|||||||
VegaICellRendererParams,
|
VegaICellRendererParams,
|
||||||
VegaValueFormatterParams,
|
VegaValueFormatterParams,
|
||||||
} from '@vegaprotocol/datagrid';
|
} from '@vegaprotocol/datagrid';
|
||||||
import { ExternalLink, Pill } from '@vegaprotocol/ui-toolkit';
|
|
||||||
import {
|
import {
|
||||||
ProposalProductTypeMapping,
|
ProductTypeMapping,
|
||||||
ProposalProductTypeShortName,
|
ProductTypeShortName,
|
||||||
ProposalStateMapping,
|
ProposalStateMapping,
|
||||||
} from '@vegaprotocol/types';
|
} from '@vegaprotocol/types';
|
||||||
import type { ProposalListFieldsFragment } from '../../lib/proposals-data-provider/__generated__/Proposals';
|
import type { ProposalListFieldsFragment } from '../../lib/proposals-data-provider/__generated__/Proposals';
|
||||||
import { VoteProgress } from '../voting-progress';
|
import { VoteProgress } from '../voting-progress';
|
||||||
import { ProposalActionsDropdown } from '../proposal-actions-dropdown';
|
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 = () => {
|
export const useColumnDefs = () => {
|
||||||
const { params } = useNetworkParams([
|
const { params } = useNetworkParams([
|
||||||
NetworkParams.governance_proposal_market_requiredMajority,
|
NetworkParams.governance_proposal_market_requiredMajority,
|
||||||
@ -80,17 +45,36 @@ export const useColumnDefs = () => {
|
|||||||
headerName: t('Market'),
|
headerName: t('Market'),
|
||||||
field: 'terms.change.instrument.code',
|
field: 'terms.change.instrument.code',
|
||||||
cellStyle: { lineHeight: '14px' },
|
cellStyle: { lineHeight: '14px' },
|
||||||
cellRenderer: 'MarketNameProposalCell',
|
cellRenderer: ({
|
||||||
},
|
value,
|
||||||
{
|
data,
|
||||||
colId: 'description',
|
}: {
|
||||||
headerName: t('Description'),
|
value: string;
|
||||||
field: 'terms.change.instrument.name',
|
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',
|
colId: 'asset',
|
||||||
headerName: t('Settlement asset'),
|
headerName: t('Settlement asset'),
|
||||||
field: 'terms.change.instrument.futureProduct.settlementAsset.name',
|
field: 'terms.change.instrument.futureProduct.settlementAsset.symbol',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
colId: 'state',
|
colId: 'state',
|
||||||
@ -105,12 +89,10 @@ export const useColumnDefs = () => {
|
|||||||
set: ProposalStateMapping,
|
set: ProposalStateMapping,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
FLAGS.SUCCESSOR_MARKETS && {
|
{
|
||||||
headerName: t('Parent market'),
|
headerName: t('Parent market'),
|
||||||
field: 'id',
|
field: 'terms.change.successorConfiguration.parentMarketId',
|
||||||
colId: 'parentMarket',
|
cellRenderer: 'ParentMarketCell',
|
||||||
cellRenderer: 'SuccessorMarketRenderer',
|
|
||||||
cellRendererParams: { parent: true },
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
colId: 'voting',
|
colId: 'voting',
|
||||||
@ -169,25 +151,12 @@ export const useColumnDefs = () => {
|
|||||||
data,
|
data,
|
||||||
}: VegaICellRendererParams<ProposalListFieldsFragment>) => {
|
}: VegaICellRendererParams<ProposalListFieldsFragment>) => {
|
||||||
if (!data?.id) return null;
|
if (!data?.id) return null;
|
||||||
|
|
||||||
return <ProposalActionsDropdown id={data.id} />;
|
return <ProposalActionsDropdown id={data.id} />;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
}, [requiredMajorityPercentage]);
|
}, [requiredMajorityPercentage]);
|
||||||
|
|
||||||
const defaultColDef: ColDef = useMemo(() => {
|
return columnDefs;
|
||||||
return {
|
|
||||||
sortable: true,
|
|
||||||
filter: true,
|
|
||||||
filterParams: { buttons: ['reset'] },
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return useMemo(
|
|
||||||
() => ({
|
|
||||||
columnDefs,
|
|
||||||
defaultColDef,
|
|
||||||
}),
|
|
||||||
[columnDefs, defaultColDef]
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
@ -114,6 +114,9 @@ fragment NewMarketFields on NewMarket {
|
|||||||
lpPriceRange
|
lpPriceRange
|
||||||
# linearSlippageFactor
|
# linearSlippageFactor
|
||||||
# quadraticSlippageFactor
|
# quadraticSlippageFactor
|
||||||
|
successorConfiguration {
|
||||||
|
parentMarketId
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fragment UpdateMarketFields on UpdateMarket {
|
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 = (
|
export const createProposalListFieldsFragment = (
|
||||||
override?: PartialDeep<ProposalListFieldsFragment>
|
override?: PartialDeep<ProposalListFieldsFragment>
|
||||||
): 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 = {
|
const defaultProposal: ProposalListFieldsFragment = {
|
||||||
id: 'e9ec6d5c46a7e7bcabf9ba7a893fa5a5eeeec08b731f06f7a6eb7bf0e605b829',
|
id: 'e9ec6d5c46a7e7bcabf9ba7a893fa5a5eeeec08b731f06f7a6eb7bf0e605b829',
|
||||||
reference: 'injected_at_runtime',
|
reference: 'injected_at_runtime',
|
||||||
@ -147,54 +199,7 @@ export const createProposalListFieldsFragment = (
|
|||||||
terms: {
|
terms: {
|
||||||
closingDatetime: '2022-11-15T12:44:34Z',
|
closingDatetime: '2022-11-15T12:44:34Z',
|
||||||
enactmentDatetime: '2022-11-15T12:44:54Z',
|
enactmentDatetime: '2022-11-15T12:44:54Z',
|
||||||
change: {
|
change: newMarket,
|
||||||
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',
|
|
||||||
},
|
|
||||||
__typename: 'ProposalTerms',
|
__typename: 'ProposalTerms',
|
||||||
},
|
},
|
||||||
__typename: 'Proposal',
|
__typename: 'Proposal',
|
||||||
|
Loading…
Reference in New Issue
Block a user