Compare commits

...
Author SHA1 Message Date
Matthew Russell 5f23a09810 fix: navbar tests 2023-09-13 11:37:24 -07:00
Madalina Raicu 13b1269a58 chore: update navbar spec 2023-09-13 18:51:39 +01:00
Madalina Raicu 398cd9e639 Merge branch 'feat/add-governance-link-back' of github.com:vegaprotocol/frontend-monorepo into feat/add-governance-link-back 2023-09-13 18:49:11 +01:00
Matthew Russell d675410a78 chore: adjust NavbarLinkExternal to be able to be used at top level 2023-09-13 10:24:56 -07:00
Madalina Raicu afe85ae622 chore(trading): add governance link back 2023-09-13 12:35:26 +01:00
Matthew Russell c6df34fe95 chore(trading): bump app version and enable snaps 2023-09-12 13:40:08 -07:00
Matthew Russell c7d0803164 chore(trading): make closed market table rows the correct height 2023-09-12 12:47:47 -07:00
m.ray 250a654544 feat(trading): margin required progressive disclosure (#4755) 2023-09-12 12:06:47 -07:00
Matthew Russell 7f5e8ebb15 chore(trading): replace successor cols with market code cell (#4722) 2023-09-12 11:52:03 -07:00
Art ccbe34e172 chore(trading): make deal ticket open on market change (#4749) 2023-09-12 11:26:54 -07:00
Maciek 27e8ce88bf chore(trading): adjust onboarding experience (#4754) 2023-09-12 11:10:17 -07:00
ArtandMatthew Russell 41804d9869 chore(trading): market list tweaks (#4756)
Co-authored-by: Matthew Russell <mattrussell36@gmail.com>
2023-09-12 10:41:45 -07:00
Maciek 293288286b chore(trading): adjust gap between candles (#4753) 2023-09-12 09:59:13 -07:00
Matthew Russell a77765b1e9 feat(trading): store study sizes, reduce candles gap (#4708) 2023-09-12 09:56:43 -07:00
MaciekandMatthew Russell 2453d7841a chore(trading): open connect dialog from onboarding dialog (#4744)
Co-authored-by: Matthew Russell <mattrussell36@gmail.com>
2023-09-12 08:07:37 -07:00
69 changed files with 1152 additions and 1653 deletions
+12 -11
View File
@@ -1,27 +1,28 @@
NX_ETHEREUM_PROVIDER_URL=https://sepolia.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
NX_HOSTED_WALLET_URL=https://wallet.testnet.vega.xyz
NX_ORACLE_PROOFS_URL=https://raw.githubusercontent.com/vegaprotocol/well-known/main/__generated__/oracle-proofs.json
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/stagnet1/vegawallet-stagnet1.toml
NX_VEGA_ENV=STAGNET1
NX_VEGA_EXPLORER_URL=https://explorer.stagnet1.vega.rocks
NX_SENTRY_DSN=https://2ffce43721964aafa78277c50654ece4@o286262.ingest.sentry.io/6300613
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/fairground/vegawallet-fairground.toml
NX_VEGA_ENV=TESTNET
NX_VEGA_EXPLORER_URL=https://explorer.fairground.wtf
NX_VEGA_NETWORKS={\"MAINNET\":\"https://console.vega.xyz\",\"TESTNET\":\"https://console.fairground.wtf\",\"STAGNET1\":\"https://trading.stagnet1.vega.rocks\"}
NX_VEGA_TOKEN_URL=https://governance.fairground.wtf
NX_VEGA_WALLET_URL=http://localhost:1789
NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet
NX_VEGA_REPO_URL=https://github.com/vegaprotocol/vega/releases
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/main/announcements.json
NX_WALLETCONNECT_PROJECT_ID=fe8091dc35738863e509fc4947525c72
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json
NX_VEGA_INCIDENT_URL=https://blog.vega.xyz/tagged/vega-incident-reports
NX_VEGA_CONSOLE_URL=https://console.fairground.wtf
NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-fairground
# Cosmic elevator flags
NX_SUCCESSOR_MARKETS=true
NX_STOP_ORDERS=true
# NX_ICEBERG_ORDERS
NX_ICEBERG_ORDERS=true
# NX_PRODUCT_PERPETUALS
NX_METAMASK_SNAPS=true
NX_METAMASK_SNAPS=false
NX_TENDERMINT_URL=https://tm.n01.stagnet1.vega.rocks
NX_TENDERMINT_WEBSOCKET_URL=wss://tm.n01.stagnet1.vega.xyz/websocket
NX_TENDERMINT_URL=https://tm.be.testnet.vega.xyz
NX_TENDERMINT_WEBSOCKET_URL=wss://be.testnet.vega.xyz/websocket
+2 -2
View File
@@ -16,14 +16,14 @@ NX_VEGA_CONSOLE_URL=https://console.vega.xyz
NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-mainnet/codfcglpplgmmlokgilfkpcjnmkbfiel
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-mainnet
# TAG name of the current app version - TODO: bump to the latest upon release
NX_APP_VERSION=v0.21.0-core-0.72.14
NX_APP_VERSION=v0.21.1-core-0.72.14
# Cosmic elevator flags
NX_SUCCESSOR_MARKETS=true
NX_STOP_ORDERS=true
NX_ICEBERG_ORDERS=true
# NX_PRODUCT_PERPETUALS
NX_METAMASK_SNAPS=false
NX_METAMASK_SNAPS=true
NX_TENDERMINT_URL=https://be.vega.community
NX_TENDERMINT_WEBSOCKET_URL=wss://be.vega.community/websocket
+1 -1
View File
@@ -22,7 +22,7 @@ NX_SUCCESSOR_MARKETS=true
NX_STOP_ORDERS=true
NX_ICEBERG_ORDERS=true
# NX_PRODUCT_PERPETUALS
NX_METAMASK_SNAPS=false
NX_METAMASK_SNAPS=true
NX_TENDERMINT_URL=https://tm.be.testnet.vega.xyz
NX_TENDERMINT_WEBSOCKET_URL=wss://be.testnet.vega.xyz/websocket
+7 -7
View File
@@ -69,15 +69,15 @@ export const MarketPage = () => {
useEffect(() => {
if (data?.id && data.id !== lastMarketId) {
update({ marketId: data.id });
}
}, [update, lastMarketId, data?.id]);
// Make sidebar open on deal ticket by default
useEffect(() => {
if (init && view === null) {
// make sidebar open on market id change
setView({ type: ViewType.Order });
}
}, [init, view, setView]);
// make sidebar open on deal ticket by default
if (view === null) {
setView({ type: ViewType.Order });
}
}, [update, lastMarketId, data?.id, setView, init, view]);
const tradeView = useMemo(() => {
if (largeScreen) {
+21 -208
View File
@@ -1,4 +1,4 @@
import { act, render, screen, within, waitFor } from '@testing-library/react';
import { act, render, screen, within } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import { Closed } from './closed';
import { MarketStateMapping, PropertyKeyType } from '@vegaprotocol/types';
@@ -10,55 +10,21 @@ import type {
OracleSpecDataConnectionQuery,
MarketsDataQuery,
MarketsQuery,
SuccessorMarketIdsQuery,
} from '@vegaprotocol/markets';
import {
OracleSpecDataConnectionDocument,
MarketsDataDocument,
MarketsDocument,
SuccessorMarketIdsDocument,
} from '@vegaprotocol/markets';
import type { VegaWalletContextShape } from '@vegaprotocol/wallet';
import { VegaWalletContext } from '@vegaprotocol/wallet';
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
import { FLAGS } from '@vegaprotocol/environment';
import {
createMarketFragment,
marketsQuery,
marketsDataQuery,
createMarketsDataFragment,
} from '@vegaprotocol/mock';
import type { FeatureFlags } from '@vegaprotocol/environment';
jest.mock('@vegaprotocol/markets', () => ({
...jest.requireActual('@vegaprotocol/markets'),
useSuccessorMarket: (marketId: string) =>
marketId === 'include-0'
? {
data: {
id: 'successorMarketID',
state: 'STATE_ACTIVE',
tradableInstrument: {
instrument: {
name: 'Successor Market Name',
code: 'SuccessorCode',
},
},
},
}
: { data: undefined },
}));
jest.mock('@vegaprotocol/environment', () => {
const actual = jest.requireActual('@vegaprotocol/environment');
return {
...actual,
FLAGS: {
...actual.FLAGS,
SUCCESSOR_MARKETS: true,
} as FeatureFlags,
};
});
describe('Closed', () => {
let originalNow: typeof Date.now;
@@ -218,10 +184,8 @@ describe('Closed', () => {
const headers = screen.getAllByRole('columnheader');
const expectedHeaders = [
'Market',
'Description',
'Status',
'Settlement date',
'Successor market',
'Best bid',
'Best offer',
'Mark price',
@@ -235,10 +199,8 @@ describe('Closed', () => {
const cells = screen.getAllByRole('gridcell');
const expectedValues = [
market.tradableInstrument.instrument.code,
market.tradableInstrument.instrument.name,
MarketStateMapping[market.state],
'3 days ago',
'-',
/* eslint-disable @typescript-eslint/no-non-null-assertion */
addDecimalsFormatNumber(marketsData.bestBidPrice, market.decimalPlaces),
addDecimalsFormatNumber(
@@ -340,43 +302,26 @@ describe('Closed', () => {
.getAllByRole('gridcell')
.filter((cell) => cell.getAttribute('col-id') === 'code')
.map((cell) => {
const marketId = within(cell)
.getByTestId('market-code')
.getAttribute('data-market-id');
return marketId;
const marketCode = within(cell).getByTestId('stack-cell-primary');
return marketCode.textContent;
});
expect(cells).toEqual(expectedRows.map((m) => m.node.id));
expect(cells).toEqual(
expectedRows.map((m) => m.node.tradableInstrument.instrument.code)
);
});
it('successor marked should be visible', async () => {
const mixedMarkets = [
const marketsWithSuccessorID = [
{
__typename: 'MarketEdge' as const,
node: createMarketFragment({
id: 'include-0',
state: MarketState.STATE_SETTLED,
successorMarketID: 'successor',
}),
},
{
__typename: 'MarketEdge' as const,
node: {
...createMarketFragment({
id: 'successorMarketID',
state: MarketState.STATE_ACTIVE,
}),
tradableInstrument: {
...createMarketFragment().tradableInstrument,
instrument: {
...createMarketFragment().tradableInstrument.instrument,
id: 'successorAssset',
name: 'Successor Market Name',
code: 'SuccessorCode',
},
},
},
},
];
const mixedMarketsMock: MockedResponse<MarketsQuery> = {
const mockWithSuccessors: MockedResponse<MarketsQuery> = {
request: {
query: MarketsDocument,
},
@@ -384,42 +329,17 @@ describe('Closed', () => {
data: {
marketsConnection: {
__typename: 'MarketConnection',
edges: mixedMarkets,
edges: marketsWithSuccessorID,
},
},
},
};
const successorMarketsMock: MockedResponse<SuccessorMarketIdsQuery> = {
request: {
query: SuccessorMarketIdsDocument,
},
result: {
data: {
marketsConnection: {
__typename: 'MarketConnection',
edges: [
{
node: {
id: 'include-0',
successorMarketID: 'successorMarketID',
parentMarketID: '',
},
},
],
},
},
},
};
await act(() => {
await act(async () => {
render(
<MemoryRouter>
<MockedProvider
mocks={[
mixedMarketsMock,
marketsDataMock,
oracleDataMock,
successorMarketsMock,
]}
mocks={[mockWithSuccessors, marketsDataMock, oracleDataMock]}
>
<VegaWalletContext.Provider
value={{ pubKey } as VegaWalletContextShape}
@@ -431,122 +351,15 @@ describe('Closed', () => {
);
});
await waitFor(() => {
expect(
screen.getByRole('button', { name: /^SuccessorCode/ })
).toBeInTheDocument();
});
expect(
screen.getByRole('columnheader', {
name: (_name, element) =>
element.getAttribute('col-id') === 'successorMarket',
})
).toBeInTheDocument();
screen
.getAllByRole('gridcell', {
name: (_name, element) =>
element.getAttribute('col-id') === 'successorMarket',
})
.forEach((element) => {
expect(element.querySelector('[title="Future"]')?.textContent).toEqual(
'Futr'
);
});
});
const container = within(
document.querySelector('.ag-center-cols-container') as HTMLElement
);
const cell = container.getAllByRole('gridcell', {
name: (_name, element) => element.getAttribute('col-id') === 'code',
})[0];
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>
expect(within(cell).getByTestId('stack-cell-secondary')).toHaveTextContent(
'PRNT'
);
await waitFor(() => {
expect(
screen.getByRole('columnheader', {
name: (_name, element) =>
element.getAttribute('col-id') === 'settlementDate',
})
).toBeInTheDocument();
});
screen.getAllByRole('columnheader').forEach((element) => {
expect(element.getAttribute('col-id')).not.toEqual('successorMarket');
});
});
});
+24 -34
View File
@@ -4,13 +4,10 @@ import type {
VegaICellRendererParams,
VegaValueFormatterParams,
} from '@vegaprotocol/datagrid';
import {
AgGridLazy as AgGrid,
COL_DEFS,
MarketNameCell,
} from '@vegaprotocol/datagrid';
import { AgGridLazy as AgGrid, COL_DEFS } from '@vegaprotocol/datagrid';
import { useMemo } from 'react';
import { t } from '@vegaprotocol/i18n';
import type { ProductType } from '@vegaprotocol/types';
import { MarketState, MarketStateMapping } from '@vegaprotocol/types';
import {
addDecimalsFormatNumber,
@@ -20,17 +17,13 @@ import type {
DataSourceFilterFragment,
MarketMaybeWithData,
} from '@vegaprotocol/markets';
import {
MarketActionsDropdown,
closedMarketsWithDataProvider,
} from '@vegaprotocol/markets';
import { closedMarketsWithDataProvider } from '@vegaprotocol/markets';
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
import type { ColDef } from 'ag-grid-community';
import { FLAGS } from '@vegaprotocol/environment';
import { SettlementDateCell } from './settlement-date-cell';
import { SettlementPriceCell } from './settlement-price-cell';
import { useDataProvider } from '@vegaprotocol/data-provider';
import { SuccessorMarketRenderer } from './successor-market-cell';
import { MarketActionsDropdown } from './market-table-actions';
import { MarketCodeCell } from './market-code-cell';
type SettlementAsset =
MarketMaybeWithData['tradableInstrument']['instrument']['product']['settlementAsset'];
@@ -51,7 +44,9 @@ interface Row {
setlementDataSourceFilter: DataSourceFilterFragment | undefined;
tradingTerminationOracleId: string;
settlementAsset: SettlementAsset;
productType: string;
productType: ProductType | undefined;
successorMarketID: string | null | undefined;
parentMarketID: string | null | undefined;
}
export const Closed = () => {
@@ -95,16 +90,19 @@ export const Closed = () => {
tradingTerminationOracleId:
instrument.product.dataSourceSpecForTradingTermination.id,
settlementAsset: instrument.product.settlementAsset,
productType: instrument.product.__typename || '',
productType: instrument.product.__typename,
successorMarketID: market.successorMarketID,
parentMarketID: market.parentMarketID,
};
return row;
});
return (
<div className="h-full relative">
<ClosedMarketsDataGrid rowData={rowData} error={error} />
</div>
);
return <ClosedMarketsDataGrid rowData={rowData} error={error} />;
};
const components = {
MarketCodeCell,
};
const ClosedMarketsDataGrid = ({
@@ -117,15 +115,11 @@ const ClosedMarketsDataGrid = ({
const openAssetDialog = useAssetDetailsDialogStore((store) => store.open);
const colDefs = useMemo(() => {
const cols: ColDef[] = compact([
return [
{
headerName: t('Market'),
field: 'code',
cellRenderer: 'MarketNameCell',
},
{
headerName: t('Description'),
field: 'name',
cellRenderer: 'MarketCodeCell',
},
{
headerName: t('Status'),
@@ -176,12 +170,6 @@ const ClosedMarketsDataGrid = ({
},
},
},
FLAGS.SUCCESSOR_MARKETS && {
headerName: t('Successor market'),
field: 'id',
colId: 'successorMarket',
cellRenderer: 'SuccessorMarketRenderer',
},
{
headerName: t('Best bid'),
field: 'bestBidPrice',
@@ -263,12 +251,13 @@ const ClosedMarketsDataGrid = ({
<MarketActionsDropdown
marketId={data.id}
assetId={data.settlementAsset.id}
successorMarketID={data.successorMarketID}
parentMarketID={data.parentMarketID}
/>
);
},
},
]);
return cols;
];
}, [openAssetDialog]);
return (
@@ -276,8 +265,9 @@ const ClosedMarketsDataGrid = ({
rowData={rowData}
columnDefs={colDefs}
getRowId={({ data }) => data.id}
components={{ SuccessorMarketRenderer, MarketNameCell }}
overlayNoRowsTemplate={error ? error.message : t('No markets')}
components={components}
rowHeight={45}
/>
);
};
@@ -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'
);
});
});
@@ -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} />;
};
@@ -0,0 +1,35 @@
import type { TypedDataAgGrid } from '@vegaprotocol/datagrid';
import { AgGridLazy as AgGrid, PriceFlashCell } from '@vegaprotocol/datagrid';
import type { MarketMaybeWithData } from '@vegaprotocol/markets';
import { useColumnDefs } from './use-column-defs';
export const getRowId = ({ data }: { data: { id: string } }) => data.id;
const defaultColDef = {
sortable: true,
filter: true,
filterParams: { buttons: ['reset'] },
};
const components = {
PriceFlashCell,
};
type Props = TypedDataAgGrid<MarketMaybeWithData>;
export const MarketListTable = (props: Props) => {
const columnDefs = useColumnDefs();
return (
<AgGrid
getRowId={getRowId}
defaultColDef={defaultColDef}
columnDefs={columnDefs}
components={components}
rowHeight={45}
{...props}
/>
);
};
export default MarketListTable;
@@ -9,14 +9,21 @@ import {
} from '@vegaprotocol/ui-toolkit';
import { DApp, EXPLORER_MARKET, useLinks } from '@vegaprotocol/environment';
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
import { useNavigate } from 'react-router-dom';
import { Links, Routes } from '../../pages/client-router';
export const MarketActionsDropdown = ({
marketId,
assetId,
successorMarketID,
parentMarketID,
}: {
marketId: string;
assetId: string;
successorMarketID: string | null | undefined;
parentMarketID: string | null | undefined;
}) => {
const navigate = useNavigate();
const open = useAssetDetailsDialogStore((store) => store.open);
const linkCreator = useLinks(DApp.Explorer);
@@ -42,6 +49,26 @@ export const MarketActionsDropdown = ({
<VegaIcon name={VegaIconNames.INFO} size={16} />
{t('View settlement asset details')}
</TradingDropdownItem>
{parentMarketID && (
<TradingDropdownItem
onClick={() => {
navigate(Links[Routes.MARKET](parentMarketID));
}}
>
<VegaIcon name={VegaIconNames.EYE} size={16} />
{t('View parent market')}
</TradingDropdownItem>
)}
{successorMarketID && (
<TradingDropdownItem
onClick={() => {
navigate(Links[Routes.MARKET](successorMarketID));
}}
>
<VegaIcon name={VegaIconNames.EYE} size={16} />
{t('View successor market')}
</TradingDropdownItem>
)}
</ActionsDropdown>
);
};
@@ -6,7 +6,7 @@ import {
Tab,
TradingAnchorButton,
} from '@vegaprotocol/ui-toolkit';
import { Markets } from './markets';
import { OpenMarkets } from './open-markets';
import { Proposed } from './proposed';
import { usePageTitleStore } from '../../stores';
import { Closed } from './closed';
@@ -33,7 +33,7 @@ export const MarketsPage = () => {
<div className="h-full my-1 border rounded-sm border-default">
<Tabs storageKey="console-markets">
<Tab id="open-markets" name={t('Open markets')}>
<Markets />
<OpenMarkets />
</Tab>
<Tab
id="proposed-markets"
@@ -1,13 +0,0 @@
import { MarketsContainer } from '@vegaprotocol/markets';
import { useMarketClickHandler } from '../../lib/hooks/use-market-click-handler';
import { SuccessorMarketRenderer } from './successor-market-cell';
export const Markets = () => {
const handleOnSelect = useMarketClickHandler();
return (
<MarketsContainer
onSelect={handleOnSelect}
SuccessorMarketRenderer={SuccessorMarketRenderer}
/>
);
};
@@ -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')}
/>
);
};
@@ -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;
};
@@ -0,0 +1,15 @@
import { useMarketsMapProvider } from '@vegaprotocol/markets';
export const ParentMarketCell = ({
value,
}: {
value: string; // parentMarketId
}) => {
const { data, loading } = useMarketsMapProvider();
if (loading) return null;
if (!data || !data[value]) return <span>-</span>;
return <div>{data[value].tradableInstrument.instrument.code}</div>;
};
@@ -1,6 +1,10 @@
import { ProposalsList } from '@vegaprotocol/proposals';
import { SuccessorMarketRenderer } from './successor-market-cell';
import { ParentMarketCell } from './parent-market-cell';
const cellRenderers = {
ParentMarketCell,
};
export const Proposed = () => {
return <ProposalsList SuccessorMarketRenderer={SuccessorMarketRenderer} />;
return <ProposalsList cellRenderers={cellRenderers} />;
};
@@ -1,190 +0,0 @@
import { render, screen, act, waitFor } from '@testing-library/react';
import { MockedProvider } from '@apollo/client/testing';
import userEvent from '@testing-library/user-event';
import type { Market } from '@vegaprotocol/markets';
import { SuccessorMarketRenderer } from './successor-market-cell';
import {
MarketsDocument,
SuccessorMarketIdsDocument,
} from '@vegaprotocol/markets';
import { createMarketFragment } from '@vegaprotocol/mock';
const mockSuccessorsQuery = [
{
id: 'market1',
parentMarketID: 'parentMarket1',
successorMarketID: 'successorMarket1',
},
{ id: 'market2', parentMarketID: 'parentMarket2' },
{ id: 'market3', successorMarketID: 'successorMarket3' },
];
const parentMarket1 = {
id: 'parentMarket1',
tradableInstrument: {
instrument: { code: 'code parent 1', id: '1' },
},
} as unknown as Market;
const successorMarket1 = {
id: 'successorMarket1',
tradableInstrument: {
instrument: { code: 'code successor 1', id: '2' },
},
} as unknown as Market;
const parentMarket2 = {
id: 'parentMarket2',
tradableInstrument: {
instrument: { code: 'code parent 2', id: '3' },
},
} as unknown as Market;
const successorMarket3 = {
id: 'successorMarket3',
tradableInstrument: {
instrument: { code: 'code successor 3', id: '4' },
},
} as unknown as Market;
const mockMarkets = [
parentMarket1,
successorMarket1,
parentMarket2,
successorMarket3,
];
const mockClickHandler = jest.fn();
jest.mock('../../lib/hooks/use-market-click-handler', () => ({
useMarketClickHandler: jest.fn().mockImplementation(() => mockClickHandler),
}));
const marketMock = {
request: {
query: MarketsDocument,
variables: undefined,
},
result: {
data: {
marketsConnection: {
edges: mockMarkets.map((item) => ({
node: {
...createMarketFragment(item),
},
})),
},
},
},
};
const successorMock = {
request: {
query: SuccessorMarketIdsDocument,
},
result: {
data: {
marketsConnection: {
edges: mockSuccessorsQuery.map((item) => ({
node: {
...item,
},
})),
},
},
},
};
const mocks = [marketMock, successorMock];
describe('SuccessorMarketRenderer', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('should properly rendered successor market', async () => {
const successorValue = 'market1';
render(
<MockedProvider mocks={[...mocks]}>
<SuccessorMarketRenderer value={successorValue} />
</MockedProvider>
);
await waitFor(() => {
expect(screen.getByTestId('market-code')).toBeInTheDocument();
});
expect(screen.getByText('code successor 1')).toBeInTheDocument();
expect(screen.getByText('Futr')).toBeInTheDocument();
await userEvent.click(screen.getByRole('button'));
await waitFor(() => {
expect(mockClickHandler).toHaveBeenCalledWith('successorMarket1', false);
});
});
it('should properly rendered parent market', async () => {
const successorValue = 'market1';
render(
<MockedProvider mocks={[...mocks]}>
<SuccessorMarketRenderer value={successorValue} parent />
</MockedProvider>
);
await waitFor(() => {
expect(screen.getByTestId('market-code')).toBeInTheDocument();
});
expect(screen.getByText('code parent 1')).toBeInTheDocument();
expect(screen.getByText('Futr')).toBeInTheDocument();
await userEvent.click(screen.getByRole('button'));
await waitFor(() => {
expect(mockClickHandler).toHaveBeenCalledWith('parentMarket1', false);
});
});
it('should properly rendered only parent market', async () => {
const successorValue = 'market2';
const { rerender } = render(
<MockedProvider mocks={[...mocks]}>
<SuccessorMarketRenderer value={successorValue} parent />
</MockedProvider>
);
await waitFor(() => {
expect(screen.getByTestId('market-code')).toBeInTheDocument();
});
expect(screen.getByText('code parent 2')).toBeInTheDocument();
expect(screen.getByText('Futr')).toBeInTheDocument();
await userEvent.click(screen.getByRole('button'));
await waitFor(() => {
expect(mockClickHandler).toHaveBeenCalledWith('parentMarket2', false);
});
rerender(
<MockedProvider mocks={[...mocks]}>
<SuccessorMarketRenderer value={successorValue} />
</MockedProvider>
);
expect(screen.getByText('-')).toBeInTheDocument();
});
it('should properly rendered only successor market', async () => {
const successorValue = 'market3';
const { rerender } = render(
<MockedProvider mocks={[...mocks]}>
<SuccessorMarketRenderer value={successorValue} />
</MockedProvider>
);
await waitFor(() => {
expect(screen.getByTestId('market-code')).toBeInTheDocument();
});
expect(screen.getByText('code successor 3')).toBeInTheDocument();
expect(screen.getByText('Futr')).toBeInTheDocument();
await userEvent.click(screen.getByRole('button'));
await waitFor(() => {
expect(mockClickHandler).toHaveBeenCalledWith('successorMarket3', false);
});
await act(() => {
rerender(
<MockedProvider mocks={[...mocks]}>
<SuccessorMarketRenderer value={successorValue} parent />
</MockedProvider>
);
});
expect(screen.getByText('-')).toBeInTheDocument();
});
});
@@ -1,41 +0,0 @@
import React from 'react';
import { MarketNameCell } from '@vegaprotocol/datagrid';
import { useDataProvider } from '@vegaprotocol/data-provider';
import { marketProvider, useSuccessorMarketIds } from '@vegaprotocol/markets';
import { useMarketClickHandler } from '../../lib/hooks/use-market-click-handler';
export const SuccessorMarketRenderer = ({
value,
parent,
}: {
value: string;
parent?: boolean;
}) => {
const successors = useSuccessorMarketIds(value);
const onMarketClick = useMarketClickHandler();
const lookupValue = successors
? parent
? successors.parentMarketID
: successors.successorMarketID
: '';
const { data } = useDataProvider({
dataProvider: marketProvider,
variables: {
marketId: lookupValue || '',
},
skip: !lookupValue,
});
return data ? (
<MarketNameCell
value={data.tradableInstrument.instrument.code}
data={data}
onMarketClick={onMarketClick}
productType={data.tradableInstrument.instrument?.product.__typename}
/>
) : (
'-'
);
};
@@ -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]
);
};
@@ -10,7 +10,7 @@ import type { SortType } from './sort-dropdown';
import { SortTypeMapping } from './sort-dropdown';
import { Sort } from './sort-dropdown';
import { subDays } from 'date-fns';
import { isMarketActive } from './use-market-selector-list';
import { isMarketActive } from '../../lib/utils';
jest.mock('@vegaprotocol/markets');
const mockUseMarketList = useMarketList as jest.Mock;
@@ -1,15 +1,13 @@
import merge from 'lodash/merge';
import { renderHook } from '@testing-library/react';
import {
isMarketActive,
useMarketSelectorList,
} from './use-market-selector-list';
import { Product } from '../../components/market-selector/product-selector';
import { useMarketSelectorList } from './use-market-selector-list';
import { isMarketActive } from '../../lib/utils';
import { Product } from './product-selector';
import { Sort } from './sort-dropdown';
import { createMarketFragment } from '@vegaprotocol/mock';
import { MarketState } from '@vegaprotocol/types';
import { useMarketList } from '@vegaprotocol/markets';
import type { Filter } from '../../components/market-selector';
import type { Filter } from './market-selector';
import { subDays } from 'date-fns';
jest.mock('@vegaprotocol/markets', () => ({
@@ -1,18 +1,11 @@
import { useMemo } from 'react';
import orderBy from 'lodash/orderBy';
import { MarketState } from '@vegaprotocol/types';
import { calcTradedFactor, useMarketList } from '@vegaprotocol/markets';
import { priceChangePercentage } from '@vegaprotocol/utils';
import type { Filter } from '../../components/market-selector/market-selector';
import { Sort } from './sort-dropdown';
import { Product } from './product-selector';
// Used for sort order and filter
const MARKET_TEMPLATE = [
MarketState.STATE_ACTIVE,
MarketState.STATE_SUSPENDED,
MarketState.STATE_PENDING,
];
import { isMarketActive } from '../../lib/utils';
export const useMarketSelectorList = ({
product,
@@ -87,7 +80,3 @@ export const useMarketSelectorList = ({
return { markets, data, loading, error, reload };
};
export const isMarketActive = (state: MarketState) => {
return MARKET_TEMPLATE.includes(state);
};
@@ -57,6 +57,7 @@ describe('Navbar', () => {
['/markets/all', 'Markets'],
[`/markets/${marketId}`, 'Trading'],
['/portfolio', 'Portfolio'],
[expect.stringContaining('governance'), 'Governance'],
];
const links = screen.getAllByRole('link');
@@ -88,6 +89,7 @@ describe('Navbar', () => {
['/markets/all', 'Markets'],
[`/markets/${marketId}`, 'Trading'],
['/portfolio', 'Portfolio'],
[expect.stringContaining('governance'), 'Governance'],
];
const links = menu.getAllByRole('link');
links.forEach((link, i) => {
+20 -9
View File
@@ -1,6 +1,12 @@
import type { ButtonHTMLAttributes, LiHTMLAttributes, ReactNode } from 'react';
import { useState } from 'react';
import { useEnvironment, DocsLinks, Networks } from '@vegaprotocol/environment';
import {
useEnvironment,
DocsLinks,
Networks,
DApp,
useLinks,
} from '@vegaprotocol/environment';
import { t } from '@vegaprotocol/i18n';
import { useGlobalStore } from '../../stores';
import { VegaWalletConnectButton } from '../vega-wallet-connect-button';
@@ -59,14 +65,14 @@ export const Navbar = ({
<VLogo className="w-4" />
</NavLink>
{/* Left section */}
<div className="lg:hidden flex items-center">{children}</div>
<div className="flex items-center lg:hidden">{children}</div>
{/* Used to show header in nav on mobile */}
<div className="hidden lg:block">
<NavbarMenu onClick={() => setMenu(null)} />
</div>
{/* Right section */}
<div className="ml-auto flex justify-end items-center gap-2">
<div className="flex items-center justify-end ml-auto gap-2">
<ProtocolUpgradeCountdown />
<NavbarMobileButton
onClick={() => {
@@ -100,7 +106,7 @@ export const Navbar = ({
onOpenChange={(open) => setMenu((x) => (open ? x : null))}
>
<D.Overlay
className="lg:hidden fixed inset-0 dark:bg-black/80 bg-black/50 z-20"
className="fixed inset-0 z-20 lg:hidden dark:bg-black/80 bg-black/50"
data-testid="navbar-menu-overlay"
/>
<D.Content
@@ -111,7 +117,7 @@ export const Navbar = ({
)}
data-testid="navbar-menu-content"
>
<div className="flex justify-end items-center h-10 p-1">
<div className="flex items-center justify-end h-10 p-1">
<NavbarMobileButton onClick={() => setMenu(null)}>
<span className="sr-only">{t('Close menu')}</span>
<VegaIcon name={VegaIconNames.CROSS} size={24} />
@@ -179,6 +185,11 @@ const NavbarMenu = ({ onClick }: { onClick: () => void }) => {
{t('Portfolio')}
</NavbarLink>
</NavbarItem>
<NavbarItem>
<NavbarLinkExternal to={useLinks(DApp.Token)()}>
{t('Governance')}
</NavbarLinkExternal>
</NavbarItem>
<NavbarItem>
<NavbarTrigger>{t('Resources')}</NavbarTrigger>
<NavbarContent data-testid="navbar-content-resources">
@@ -336,7 +347,7 @@ const NavbarLinkExternal = ({
<NavLink
to={to}
className={classNames(
'flex lg:inline-flex gap-2 justify-between items-center relative',
'flex gap-2 lg:h-full items-center',
'px-6 py-2 lg:p-0 text-lg lg:text-sm',
'hover:text-vega-clight-100 dark:hover:text-vega-cdark-100'
)}
@@ -344,7 +355,7 @@ const NavbarLinkExternal = ({
target="_blank"
>
<span>{children}</span>
<VegaIcon name={VegaIconNames.OPEN_EXTERNAL} />
<VegaIcon name={VegaIconNames.OPEN_EXTERNAL} size={14} />
</NavLink>
</N.Link>
);
@@ -364,8 +375,8 @@ const BurgerIcon = () => (
const NavbarListDivider = () => {
return (
<div className="py-2 px-6 lg:px-0" role="separator">
<div className="h-px lg:h-full w-full lg:w-px bg-vega-clight-500 dark:bg-vega-cdark-500" />
<div className="px-6 py-2 lg:px-0" role="separator">
<div className="w-full h-px lg:h-full lg:w-px bg-vega-clight-500 dark:bg-vega-cdark-500" />
</div>
);
};
@@ -22,7 +22,7 @@ describe('GetStarted', () => {
};
const checkTicks = (elements: Element[]) => {
elements.forEach((item, i) => {
if (i + 1 < mockStep) {
if (i + 1 < mockStep - 1) {
expect(item.querySelector('[data-testid="icon-tick"]')).toBeTruthy();
}
});
@@ -57,19 +57,9 @@ describe('GetStarted', () => {
'get'
);
navigatorGetter.mockReturnValue('Chrome');
mockStep = 1;
const { rerender, container } = renderComponent();
expect(screen.queryByTestId('icon-tick')).not.toBeInTheDocument();
expect(screen.getByTestId('get-wallet-button')).toBeInTheDocument();
mockStep = 2;
rerender(
<MemoryRouter>
<VegaWalletContext.Provider value={{} as VegaWalletContextShape}>
<GetStarted />
</VegaWalletContext.Provider>
</MemoryRouter>
);
const { rerender, container } = renderComponent();
checkTicks(screen.getAllByRole('listitem'));
expect(screen.getByRole('button', { name: 'Connect' })).toBeInTheDocument();
@@ -93,7 +83,9 @@ describe('GetStarted', () => {
</MemoryRouter>
);
checkTicks(screen.getAllByRole('listitem'));
expect(screen.getByRole('button', { name: 'Dismiss' })).toBeInTheDocument();
expect(
screen.getByRole('button', { name: 'Ready to trade' })
).toBeInTheDocument();
mockStep = 5;
rerender(
@@ -7,11 +7,7 @@ import {
VegaIcon,
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
import {
GetWalletButton,
useVegaWallet,
useVegaWalletDialogStore,
} from '@vegaprotocol/wallet';
import { useVegaWallet, useVegaWalletDialogStore } from '@vegaprotocol/wallet';
import { Networks, useEnvironment } from '@vegaprotocol/environment';
import { useLocalStorage } from '@vegaprotocol/react-helpers';
import { useNavigate } from 'react-router-dom';
@@ -30,7 +26,6 @@ interface Props {
}
const GetStartedButton = ({ step }: { step: OnboardingStep }) => {
const { CHROME_EXTENSION_URL, MOZILLA_EXTENSION_URL } = useEnvironment();
const navigate = useNavigate();
const [, setOnboardingViewed] = useLocalStorage(
constants.ONBOARDING_VIEWED_KEY
@@ -47,15 +42,7 @@ const GetStartedButton = ({ step }: { step: OnboardingStep }) => {
let onClickHandle = () => {
openVegaWalletDialog();
};
if (step === OnboardingStep.ONBOARDING_WALLET_STEP) {
return (
<GetWalletButton
className="justify-between"
chromeExtensionUrl={CHROME_EXTENSION_URL}
mozillaExtensionUrl={MOZILLA_EXTENSION_URL}
/>
);
} else if (step === OnboardingStep.ONBOARDING_CONNECT_STEP) {
if (step <= OnboardingStep.ONBOARDING_CONNECT_STEP) {
buttonText = t('Connect');
} else if (step === OnboardingStep.ONBOARDING_DEPOSIT_STEP) {
buttonText = t('Deposit');
@@ -65,7 +52,7 @@ const GetStartedButton = ({ step }: { step: OnboardingStep }) => {
dismiss();
};
} else if (step === OnboardingStep.ONBOARDING_ORDER_STEP) {
buttonText = t('Dismiss');
buttonText = t('Ready to trade');
onClickHandle = () => {
navigate(link);
setView({ type: ViewType.Order });
@@ -116,23 +103,18 @@ export const GetStarted = ({ lead }: Props) => {
<ul className="list-none">
<Step
step={1}
text={t('Get a Vega wallet')}
complete={currentStep > OnboardingStep.ONBOARDING_WALLET_STEP}
/>
<Step
step={2}
text={t('Connect')}
complete={Boolean(
currentStep > OnboardingStep.ONBOARDING_CONNECT_STEP || pubKey
)}
/>
<Step
step={3}
step={2}
text={t('Deposit funds')}
complete={currentStep > OnboardingStep.ONBOARDING_DEPOSIT_STEP}
/>
<Step
step={4}
step={3}
text={t('Open a position')}
complete={currentStep > OnboardingStep.ONBOARDING_ORDER_STEP}
/>
@@ -7,8 +7,6 @@ import {
import type { VegaWalletContextShape } from '@vegaprotocol/wallet';
import { VegaWalletContext } from '@vegaprotocol/wallet';
import { useDataProvider } from '@vegaprotocol/data-provider';
import { depositsProvider } from '@vegaprotocol/deposits';
import { aggregatedAccountsDataProvider } from '@vegaprotocol/accounts';
import { ordersWithMarketProvider } from '@vegaprotocol/orders';
import { positionsDataProvider } from '@vegaprotocol/positions';
@@ -23,7 +21,7 @@ let mockContext: Partial<VegaWalletContextShape> = { pubKey: 'test-pubkey' };
describe('useGetOnboardingStep', () => {
beforeEach(() => {
jest.clearAllMocks();
mockData = [{ id: 'item-id' }];
mockData = [];
mockContext = { pubKey: 'test-pubkey' };
globalThis.window.vega = {} as Vega;
});
@@ -42,13 +40,6 @@ describe('useGetOnboardingStep', () => {
expect(result.current).toEqual(OnboardingStep.ONBOARDING_UNKNOWN_STEP);
});
it('should return properly ONBOARDING_WALLET_STEP', () => {
// @ts-ignore test only purpose
globalThis.window.vega = undefined;
const { result } = renderHook(() => useGetOnboardingStep(), { wrapper });
expect(result.current).toEqual(OnboardingStep.ONBOARDING_WALLET_STEP);
});
it('should return properly ONBOARDING_CONNECT_STEP', () => {
mockContext = { pubKey: null };
const { result } = renderHook(() => useGetOnboardingStep(), { wrapper });
@@ -56,15 +47,6 @@ describe('useGetOnboardingStep', () => {
});
it('should return properly ONBOARDING_DEPOSIT_STEP', async () => {
(useDataProvider as jest.Mock).mockImplementation((args) => {
if (
args.dataProvider === depositsProvider ||
args.dataProvider === aggregatedAccountsDataProvider
) {
return { data: [] };
}
return { data: mockData };
});
const { result } = renderHook(() => useGetOnboardingStep(), { wrapper });
await expect(result.current).toEqual(
OnboardingStep.ONBOARDING_DEPOSIT_STEP
@@ -72,6 +54,7 @@ describe('useGetOnboardingStep', () => {
});
it('should return properly ONBOARDING_ORDER_STEP', async () => {
mockData = [{ id: 'item-id' }];
(useDataProvider as jest.Mock).mockImplementation((args) => {
if (
args.dataProvider === ordersWithMarketProvider ||
@@ -86,6 +69,7 @@ describe('useGetOnboardingStep', () => {
});
it('should return properly ONBOARDING_COMPLETE_STEP', async () => {
mockData = [{ id: 'item-id' }];
(useDataProvider as jest.Mock).mockImplementation(() => {
return { data: mockData };
});
@@ -1,4 +1,4 @@
import { isBrowserWalletInstalled, useVegaWallet } from '@vegaprotocol/wallet';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { depositsProvider } from '@vegaprotocol/deposits';
import { useDataProvider } from '@vegaprotocol/data-provider';
import { ordersWithMarketProvider } from '@vegaprotocol/orders';
@@ -63,20 +63,21 @@ export const useGetOnboardingStep = () => {
collateralData === null ||
positionsData === null)
);
let step = OnboardingStep.ONBOARDING_UNKNOWN_STEP;
if (isLoading) {
return OnboardingStep.ONBOARDING_UNKNOWN_STEP;
}
if (!isBrowserWalletInstalled()) {
return OnboardingStep.ONBOARDING_WALLET_STEP;
return step;
}
if (!pubKey) {
return OnboardingStep.ONBOARDING_CONNECT_STEP;
step = OnboardingStep.ONBOARDING_CONNECT_STEP;
}
if (!deposits && !collaterals) {
return OnboardingStep.ONBOARDING_DEPOSIT_STEP;
if (pubKey) {
step = OnboardingStep.ONBOARDING_DEPOSIT_STEP;
}
if (!orders && !positions) {
return OnboardingStep.ONBOARDING_ORDER_STEP;
if (pubKey && (deposits || collaterals)) {
step = OnboardingStep.ONBOARDING_ORDER_STEP;
}
return OnboardingStep.ONBOARDING_COMPLETE_STEP;
if (pubKey && (orders || positions)) {
step = OnboardingStep.ONBOARDING_COMPLETE_STEP;
}
return step;
};
@@ -6,14 +6,32 @@ import { Links, Routes } from '../../pages/client-router';
import { Networks, useEnvironment } from '@vegaprotocol/environment';
import type { ReactNode } from 'react';
import { useOnboardingStore } from './welcome-dialog';
import { useMarketList } from '@vegaprotocol/markets';
import { isMarketActive } from '../../lib/utils';
import orderBy from 'lodash/orderBy';
import { priceChangePercentage } from '@vegaprotocol/utils';
export const WelcomeDialogContent = () => {
const { VEGA_ENV } = useEnvironment();
const dismiss = useOnboardingStore((store) => store.dismiss);
const navigate = useNavigate();
const browseMarkets = () => {
const link = Links[Routes.MARKETS]();
const { data } = useMarketList();
const markets = orderBy(
data?.filter((m) => isMarketActive(m.state)) || [],
[
(m) => {
if (!m.candles?.length) return 0;
return Number(priceChangePercentage(m.candles.map((c) => c.close)));
},
],
['desc']
);
const explore = () => {
const marketId = markets?.[0].id ?? '';
const link = marketId
? Links[Routes.MARKET](marketId)
: Links[Routes.MARKETS]();
navigate(link);
dismiss();
};
@@ -48,11 +66,11 @@ export const WelcomeDialogContent = () => {
/>
</ul>
<TradingButton
onClick={browseMarkets}
onClick={explore}
className="block w-full"
data-testid="browse-markets-button"
>
{t('Browse the markets')}
{t('Explore')}
</TradingButton>
</div>
<div className="sm:w-1/2 flex grow">
@@ -1,24 +1,18 @@
import { useNavigate, useParams, useLocation } from 'react-router-dom';
import { useNavigate } from 'react-router-dom';
import { useCallback } from 'react';
import { Links, Routes } from '../../pages/client-router';
export const useMarketClickHandler = (replace = false) => {
const navigate = useNavigate();
const { marketId } = useParams();
const { pathname } = useLocation();
const isMarketPage = pathname.match(/^\/markets\/(.+)/);
return useCallback(
(selectedId: string, metaKey?: boolean) => {
const link = Links[Routes.MARKET](selectedId);
if (metaKey) {
window.open(`/#${link}`, '_blank');
} else if (selectedId !== marketId || !isMarketPage) {
navigate(link, { replace });
}
},
[navigate, marketId, replace, isMarketPage]
);
return (selectedId: string, metaKey?: boolean) => {
const link = Links[Routes.MARKET](selectedId);
if (metaKey) {
window.open(`/#${link}`, '_blank');
} else {
navigate(link, { replace });
}
};
};
export const useMarketLiquidityClickHandler = () => {
+12
View File
@@ -0,0 +1,12 @@
import { MarketState } from '@vegaprotocol/types';
// Used for sort order and filter
const MARKET_TEMPLATE = [
MarketState.STATE_ACTIVE,
MarketState.STATE_SUSPENDED,
MarketState.STATE_PENDING,
];
export const isMarketActive = (state: MarketState) => {
return MARKET_TEMPLATE.includes(state);
};
+3 -1
View File
@@ -82,7 +82,9 @@ html [data-theme='light'] {
--pennant-color-volume-sell: theme(colors.market.red.DEFAULT);
--pennant-candlestick-inner-padding: 0.1;
/* reduce space between candles */
--pennant-candlestick-inner-padding: 0.175;
--pennant-candlestick-stroke-width: 0.5;
}
html [data-theme='light'] {
+25 -6
View File
@@ -3,6 +3,7 @@ import { CandlestickChart } from 'pennant';
import { VegaDataSource } from './data-source';
import { useApolloClient } from '@apollo/client';
import { useMemo } from 'react';
import debounce from 'lodash/debounce';
import AutoSizer from 'react-virtualized-auto-sizer';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { useThemeSwitcher } from '@vegaprotocol/react-helpers';
@@ -22,8 +23,25 @@ export const CandlesChartContainer = ({
const { pubKey } = useVegaWallet();
const { theme } = useThemeSwitcher();
const { interval, chartType, overlays, studies, merge } =
useCandlesChartSettings();
const {
interval,
chartType,
overlays,
studies,
studySizes,
setStudies,
setStudySizes,
setOverlays,
} = useCandlesChartSettings();
const handlePaneChange = useMemo(
() =>
debounce((sizes: number[]) => {
// first number is main pain, which is greedy so we don't store it
setStudySizes(sizes.filter((_, i) => i !== 0));
}, 300),
[setStudySizes]
);
const dataSource = useMemo(() => {
return new VegaDataSource(client, marketId, pubKey);
@@ -45,15 +63,16 @@ export const CandlesChartContainer = ({
initialNumCandlesToDisplay: Math.floor(
width * CANDLES_TO_WIDTH_FACTOR
),
studySize: 150, // default size
studySizes,
}}
interval={interval}
theme={theme}
onOptionsChanged={(options) => {
merge({
overlays: options.overlays,
studies: options.studies,
});
setStudies(options.studies);
setOverlays(options.overlays);
}}
onPaneChanged={handlePaneChange}
/>
</div>
)}
@@ -5,36 +5,45 @@ import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import { immer } from 'zustand/middleware/immer';
type StudySizes = { [S in Study]?: number };
interface StoredSettings {
interval: Interval;
type: ChartType;
overlays: Overlay[];
studies: Study[];
studySizes: StudySizes;
}
export const STUDY_SIZE = 100;
const STUDY_ORDER: Study[] = [
Study.FORCE_INDEX,
Study.RELATIVE_STRENGTH_INDEX,
Study.ELDAR_RAY,
Study.MACD,
Study.VOLUME,
];
const DEFAULT_CHART_SETTINGS = {
interval: Interval.I15M,
type: ChartType.CANDLE,
overlays: [Overlay.MOVING_AVERAGE],
studies: [Study.MACD, Study.VOLUME],
studySizes: {},
};
export const useCandlesChartSettingsStore = create<
StoredSettings & {
merge: (settings: Partial<StoredSettings>) => void;
setType: (type: ChartType) => void;
setInterval: (interval: Interval) => void;
setOverlays: (overlays: Overlay[]) => void;
setStudies: (studies: Study[]) => void;
setOverlays: (overlays?: Overlay[]) => void;
setStudies: (studies?: Study[]) => void;
setStudySizes: (sizes: number[]) => void;
}
>()(
persist(
immer((set) => ({
...DEFAULT_CHART_SETTINGS,
merge: (settings: Partial<StoredSettings>) =>
set((state) => {
Object.assign(state, settings);
}),
setType: (type) =>
set((state) => {
state.type = type;
@@ -43,14 +52,35 @@ export const useCandlesChartSettingsStore = create<
set((state) => {
state.interval = interval;
}),
setOverlays: (overlays) =>
setOverlays: (overlays) => {
if (!overlays) return;
set((state) => {
state.overlays = overlays;
}),
setStudies: (studies) =>
});
},
setStudies: (studies) => {
if (!studies) return;
// Make sure studies are always returned in the same order
studies.sort((a, b) => {
return STUDY_ORDER.indexOf(a) - STUDY_ORDER.indexOf(b);
});
set((state) => {
state.studies = studies;
}),
});
},
setStudySizes: (sizes) => {
set((state) => {
// for every study find the corresonding size and update
// the size record for that study
state.studies.forEach((s, i) => {
const size = sizes[i];
state.studySizes[s] = size;
});
});
},
})),
{
name: 'vega_candles_chart_store',
@@ -85,15 +115,22 @@ export const useCandlesChartSettings = () => {
[Study.VOLUME]
);
// find the study size
const studySizes = studies.map((s) => {
const size = settings.studySizes[s] || STUDY_SIZE;
return size;
});
return {
interval,
chartType,
overlays,
studies,
studySizes,
setInterval: settings.setInterval,
setType: settings.setType,
setStudies: settings.setStudies,
setOverlays: settings.setOverlays,
merge: settings.merge,
setStudySizes: settings.setStudySizes,
};
};
+1
View File
@@ -12,6 +12,7 @@ export * from './lib/cells/centered-grid-cell';
export * from './lib/cells/market-name-cell';
export * from './lib/cells/order-type-cell';
export * from './lib/cells/size';
export * from './lib/cells/stacked-cell';
export * from './lib/filters/date-range-filter';
export * from './lib/filters/set-filter';
@@ -59,7 +59,7 @@ export const MarketNameCell = ({
(data as { market: Market })?.market?.tradableInstrument.instrument.product
.__typename;
if (!value) return;
if (!value) return null;
const content = (
<>
<span data-testid="market-code" data-market-id={id}>
@@ -72,7 +72,7 @@ export const MarketNameCell = ({
<button
onClick={handleOnClick}
tabIndex={0}
className="block text-left text-ellipsis overflow-hidden whitespace-nowrap w-full"
className="block w-full overflow-hidden text-left text-ellipsis whitespace-nowrap"
>
{content}
</button>
@@ -11,8 +11,7 @@ import { AccountBreakdownDialog } from '@vegaprotocol/accounts';
import { formatRange, formatValue } from '@vegaprotocol/utils';
import { marketMarginDataProvider } from '@vegaprotocol/accounts';
import { useDataProvider } from '@vegaprotocol/data-provider';
import * as Accordion from '@radix-ui/react-accordion';
import * as AccordionPrimitive from '@radix-ui/react-accordion';
import {
MARGIN_DIFF_TOOLTIP_TEXT,
@@ -24,7 +23,13 @@ import {
} from '../../constants';
import { useEstimateFees } from '../../hooks';
import { KeyValue } from './key-value';
import { TOOLTIP_TRIGGER_CLASS_NAME } from '@vegaprotocol/ui-toolkit';
import {
Accordion,
AccordionChevron,
AccordionPanel,
Tooltip,
} from '@vegaprotocol/ui-toolkit';
import classNames from 'classnames';
const emptyValue = '-';
@@ -246,31 +251,56 @@ export const DealTicketMarginDetails = ({
const quoteName = market.tradableInstrument.instrument.product.quoteName;
return (
<>
<Accordion.Root type="single" collapsible>
<Accordion.Item value="margin">
<KeyValue
id="margin-required"
label={
<Accordion.Trigger className={TOOLTIP_TRIGGER_CLASS_NAME}>
{t('Margin required')}
</Accordion.Trigger>
}
value={formatRange(
marginRequiredBestCase,
marginRequiredWorstCase,
assetDecimals
)}
formattedValue={formatRange(
marginRequiredBestCase,
marginRequiredWorstCase,
assetDecimals,
quantum
)}
labelDescription={MARGIN_DIFF_TOOLTIP_TEXT(assetSymbol)}
symbol={assetSymbol}
/>
<Accordion.Content>
<div className="flex flex-col gap-2 w-full">
<Accordion>
<AccordionPanel
itemId="margin"
trigger={
<AccordionPrimitive.Trigger
data-testid="accordion-toggle"
className={classNames(
'w-full pt-2',
'flex items-center gap-2 text-xs',
'group'
)}
>
<div
data-testid={`deal-ticket-fee-margin-required`}
key={'value-dropdown'}
className="flex items-center gap-2 justify-between w-full"
>
<div className="flex items-center gap-1">
<Tooltip description={MARGIN_DIFF_TOOLTIP_TEXT(assetSymbol)}>
<span className="text-muted">{t('Margin required')}</span>
</Tooltip>
<AccordionChevron size={10} />
</div>
<Tooltip
description={
formatRange(
marginRequiredBestCase,
marginRequiredWorstCase,
assetDecimals
) ?? '-'
}
noUnderline
>
<div className="font-mono text-right">
{formatRange(
marginRequiredBestCase,
marginRequiredWorstCase,
assetDecimals,
quantum
)}{' '}
{assetSymbol || ''}
</div>
</Tooltip>
</div>
</AccordionPrimitive.Trigger>
}
>
<div className="flex flex-col gap-2 w-full">
<KeyValue
label={t('Total margin available')}
indent
@@ -310,12 +340,12 @@ export const DealTicketMarginDetails = ({
quantum
)}
/>
</Accordion.Content>
</Accordion.Item>
</Accordion.Root>
</div>
</AccordionPanel>
</Accordion>
{projectedMargin}
<KeyValue
label={t('Liquidation price estimate')}
label={t('Liquidation')}
value={liquidationPriceEstimate}
formattedValue={liquidationPriceEstimate}
symbol={quoteName}
@@ -329,6 +359,6 @@ export const DealTicketMarginDetails = ({
onClose={onAccountBreakdownDialogClose}
/>
)}
</>
</div>
);
};
@@ -538,7 +538,7 @@ const NotionalAndFees = ({
market.positionDecimalPlaces
);
return (
<div className="mb-4">
<div className="mb-4 flex flex-col gap-2 w-full">
<KeyValue
label={t('Notional')}
value={formatValue(notionalSize, market.decimalPlaces)}
@@ -464,7 +464,7 @@ export const DealTicket = ({
)}
/>
)}
<div className="mb-4">
<div className="mb-4 flex flex-col gap-2 w-full">
<KeyValue
label={t('Notional')}
value={formatValue(notionalSize, market.decimalPlaces)}
@@ -25,11 +25,11 @@ export const KeyValue = ({
}: KeyValuePros) => {
const displayValue = `${formattedValue ?? '-'} ${symbol || ''}`;
const valueElement = onClick ? (
<button onClick={onClick} className="text-muted">
<button onClick={onClick} className="font-mono">
{displayValue}
</button>
) : (
<div className="text-muted">{displayValue}</div>
<div className="font-mono">{displayValue}</div>
);
return (
<div
@@ -40,14 +40,14 @@ export const KeyValue = ({
}`}
key={typeof label === 'string' ? label : 'value-dropdown'}
className={classnames(
'text-xs mt-2 flex justify-between items-center gap-4 flex-wrap',
'text-xs flex justify-between items-center gap-4 flex-wrap',
{ 'ml-2': indent }
)}
>
<Tooltip description={labelDescription}>
<div>{label}</div>
<div className="text-muted">{label}</div>
</Tooltip>
<Tooltip description={`${value ?? '-'} ${symbol || ''}`}>
<Tooltip description={`${value ?? '-'} ${symbol || ''}`} noUnderline>
{valueElement}
</Tooltip>
</div>
+2 -2
View File
@@ -8,7 +8,7 @@ export const EST_MARGIN_TOOLTIP_TEXT = (settlementAsset: string) =>
[settlementAsset]
);
export const EST_TOTAL_MARGIN_TOOLTIP_TEXT = t(
'Estimated total margin that will cover open position, active orders and this order.'
'Estimated total margin that will cover open positions, active orders and this order.'
);
export const MARGIN_ACCOUNT_TOOLTIP_TEXT = t('Margin account balance.');
export const MARGIN_DIFF_TOOLTIP_TEXT = (settlementAsset: string) =>
@@ -60,7 +60,7 @@ export const EST_FEES_TOOLTIP_TEXT = t(
);
export const LIQUIDATION_PRICE_ESTIMATE_TOOLTIP_TEXT = t(
'This is a approximation to the liquidation price for that particular contract position, assuming nothing else changes, which may affect your margin and collateral balances.'
'This is an approximation (or a range) for the liquidation price for that particular contract position, assuming nothing else changes, which may affect your margin and collateral balances.'
);
export const EST_SLIPPAGE = t(
+4 -2
View File
@@ -7,12 +7,12 @@ export type DataSourceFilterFragment = { __typename?: 'Filter', key: { __typenam
export type DataSourceSpecFragment = { __typename?: 'DataSourceDefinition', sourceType: { __typename?: 'DataSourceDefinitionExternal', sourceType: { __typename?: 'DataSourceSpecConfiguration', signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, filters?: Array<{ __typename?: 'Filter', key: { __typename?: 'PropertyKey', name?: string | null, type: Types.PropertyKeyType, numberDecimalPlaces?: number | null } }> | null } } | { __typename?: 'DataSourceDefinitionInternal' } };
export type MarketFieldsFragment = { __typename?: 'Market', id: string, decimalPlaces: number, positionDecimalPlaces: number, state: Types.MarketState, tradingMode: Types.MarketTradingMode, fees: { __typename?: 'Fees', factors: { __typename?: 'FeeFactors', makerFee: string, infrastructureFee: string, liquidityFee: string } }, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', id: string, name: string, code: string, metadata: { __typename?: 'InstrumentMetadata', tags?: Array<string> | null }, product: { __typename?: 'Future', quoteName: string, settlementAsset: { __typename?: 'Asset', id: string, symbol: string, name: string, decimals: number, quantum: string }, dataSourceSpecForTradingTermination: { __typename?: 'DataSourceSpec', id: string, data: { __typename?: 'DataSourceDefinition', sourceType: { __typename?: 'DataSourceDefinitionExternal', sourceType: { __typename?: 'DataSourceSpecConfiguration', signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, filters?: Array<{ __typename?: 'Filter', key: { __typename?: 'PropertyKey', name?: string | null, type: Types.PropertyKeyType, numberDecimalPlaces?: number | null } }> | null } } | { __typename?: 'DataSourceDefinitionInternal' } } }, dataSourceSpecForSettlementData: { __typename?: 'DataSourceSpec', id: string, data: { __typename?: 'DataSourceDefinition', sourceType: { __typename?: 'DataSourceDefinitionExternal', sourceType: { __typename?: 'DataSourceSpecConfiguration', signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, filters?: Array<{ __typename?: 'Filter', key: { __typename?: 'PropertyKey', name?: string | null, type: Types.PropertyKeyType, numberDecimalPlaces?: number | null } }> | null } } | { __typename?: 'DataSourceDefinitionInternal' } } }, dataSourceSpecBinding: { __typename?: 'DataSourceSpecToFutureBinding', settlementDataProperty: string, tradingTerminationProperty: string } } } }, marketTimestamps: { __typename?: 'MarketTimestamps', open: any, close: any } };
export type MarketFieldsFragment = { __typename?: 'Market', id: string, decimalPlaces: number, positionDecimalPlaces: number, state: Types.MarketState, tradingMode: Types.MarketTradingMode, parentMarketID?: string | null, successorMarketID?: string | null, fees: { __typename?: 'Fees', factors: { __typename?: 'FeeFactors', makerFee: string, infrastructureFee: string, liquidityFee: string } }, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', id: string, name: string, code: string, metadata: { __typename?: 'InstrumentMetadata', tags?: Array<string> | null }, product: { __typename?: 'Future', quoteName: string, settlementAsset: { __typename?: 'Asset', id: string, symbol: string, name: string, decimals: number, quantum: string }, dataSourceSpecForTradingTermination: { __typename?: 'DataSourceSpec', id: string, data: { __typename?: 'DataSourceDefinition', sourceType: { __typename?: 'DataSourceDefinitionExternal', sourceType: { __typename?: 'DataSourceSpecConfiguration', signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, filters?: Array<{ __typename?: 'Filter', key: { __typename?: 'PropertyKey', name?: string | null, type: Types.PropertyKeyType, numberDecimalPlaces?: number | null } }> | null } } | { __typename?: 'DataSourceDefinitionInternal' } } }, dataSourceSpecForSettlementData: { __typename?: 'DataSourceSpec', id: string, data: { __typename?: 'DataSourceDefinition', sourceType: { __typename?: 'DataSourceDefinitionExternal', sourceType: { __typename?: 'DataSourceSpecConfiguration', signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, filters?: Array<{ __typename?: 'Filter', key: { __typename?: 'PropertyKey', name?: string | null, type: Types.PropertyKeyType, numberDecimalPlaces?: number | null } }> | null } } | { __typename?: 'DataSourceDefinitionInternal' } } }, dataSourceSpecBinding: { __typename?: 'DataSourceSpecToFutureBinding', settlementDataProperty: string, tradingTerminationProperty: string } } } }, marketTimestamps: { __typename?: 'MarketTimestamps', open: any, close: any } };
export type MarketsQueryVariables = Types.Exact<{ [key: string]: never; }>;
export type MarketsQuery = { __typename?: 'Query', marketsConnection?: { __typename?: 'MarketConnection', edges: Array<{ __typename?: 'MarketEdge', node: { __typename?: 'Market', id: string, decimalPlaces: number, positionDecimalPlaces: number, state: Types.MarketState, tradingMode: Types.MarketTradingMode, fees: { __typename?: 'Fees', factors: { __typename?: 'FeeFactors', makerFee: string, infrastructureFee: string, liquidityFee: string } }, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', id: string, name: string, code: string, metadata: { __typename?: 'InstrumentMetadata', tags?: Array<string> | null }, product: { __typename?: 'Future', quoteName: string, settlementAsset: { __typename?: 'Asset', id: string, symbol: string, name: string, decimals: number, quantum: string }, dataSourceSpecForTradingTermination: { __typename?: 'DataSourceSpec', id: string, data: { __typename?: 'DataSourceDefinition', sourceType: { __typename?: 'DataSourceDefinitionExternal', sourceType: { __typename?: 'DataSourceSpecConfiguration', signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, filters?: Array<{ __typename?: 'Filter', key: { __typename?: 'PropertyKey', name?: string | null, type: Types.PropertyKeyType, numberDecimalPlaces?: number | null } }> | null } } | { __typename?: 'DataSourceDefinitionInternal' } } }, dataSourceSpecForSettlementData: { __typename?: 'DataSourceSpec', id: string, data: { __typename?: 'DataSourceDefinition', sourceType: { __typename?: 'DataSourceDefinitionExternal', sourceType: { __typename?: 'DataSourceSpecConfiguration', signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, filters?: Array<{ __typename?: 'Filter', key: { __typename?: 'PropertyKey', name?: string | null, type: Types.PropertyKeyType, numberDecimalPlaces?: number | null } }> | null } } | { __typename?: 'DataSourceDefinitionInternal' } } }, dataSourceSpecBinding: { __typename?: 'DataSourceSpecToFutureBinding', settlementDataProperty: string, tradingTerminationProperty: string } } } }, marketTimestamps: { __typename?: 'MarketTimestamps', open: any, close: any } } }> } | null };
export type MarketsQuery = { __typename?: 'Query', marketsConnection?: { __typename?: 'MarketConnection', edges: Array<{ __typename?: 'MarketEdge', node: { __typename?: 'Market', id: string, decimalPlaces: number, positionDecimalPlaces: number, state: Types.MarketState, tradingMode: Types.MarketTradingMode, parentMarketID?: string | null, successorMarketID?: string | null, fees: { __typename?: 'Fees', factors: { __typename?: 'FeeFactors', makerFee: string, infrastructureFee: string, liquidityFee: string } }, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', id: string, name: string, code: string, metadata: { __typename?: 'InstrumentMetadata', tags?: Array<string> | null }, product: { __typename?: 'Future', quoteName: string, settlementAsset: { __typename?: 'Asset', id: string, symbol: string, name: string, decimals: number, quantum: string }, dataSourceSpecForTradingTermination: { __typename?: 'DataSourceSpec', id: string, data: { __typename?: 'DataSourceDefinition', sourceType: { __typename?: 'DataSourceDefinitionExternal', sourceType: { __typename?: 'DataSourceSpecConfiguration', signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, filters?: Array<{ __typename?: 'Filter', key: { __typename?: 'PropertyKey', name?: string | null, type: Types.PropertyKeyType, numberDecimalPlaces?: number | null } }> | null } } | { __typename?: 'DataSourceDefinitionInternal' } } }, dataSourceSpecForSettlementData: { __typename?: 'DataSourceSpec', id: string, data: { __typename?: 'DataSourceDefinition', sourceType: { __typename?: 'DataSourceDefinitionExternal', sourceType: { __typename?: 'DataSourceSpecConfiguration', signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, filters?: Array<{ __typename?: 'Filter', key: { __typename?: 'PropertyKey', name?: string | null, type: Types.PropertyKeyType, numberDecimalPlaces?: number | null } }> | null } } | { __typename?: 'DataSourceDefinitionInternal' } } }, dataSourceSpecBinding: { __typename?: 'DataSourceSpecToFutureBinding', settlementDataProperty: string, tradingTerminationProperty: string } } } }, marketTimestamps: { __typename?: 'MarketTimestamps', open: any, close: any } } }> } | null };
export const DataSourceFilterFragmentDoc = gql`
fragment DataSourceFilter on Filter {
@@ -55,6 +55,8 @@ export const MarketFieldsFragmentDoc = gql`
positionDecimalPlaces
state
tradingMode
parentMarketID
successorMarketID
fees {
factors {
makerFee
-1
View File
@@ -2,7 +2,6 @@ export * from './fees-breakdown';
export * from './last-24h-price-change';
export * from './last-24h-volume';
export * from './market-info';
export * from './markets-container';
export * from './oracle-banner';
export * from './oracle-basic-profile';
export * from './oracle-full-profile';
@@ -1,2 +0,0 @@
export * from './markets-container';
export * from './market-table-actions';
@@ -1,69 +0,0 @@
import type { TypedDataAgGrid } from '@vegaprotocol/datagrid';
import {
AgGridLazy as AgGrid,
PriceFlashCell,
MarketNameCell,
} from '@vegaprotocol/datagrid';
import type { MarketMaybeWithData } from '../../markets-provider';
import { OracleStatus } from './oracle-status';
import { useColumnDefs } from './use-column-defs';
export const getRowId = ({ data }: { data: { id: string } }) => data.id;
interface MarketNameCellProps {
value?: string;
data?: MarketMaybeWithData;
onMarketClick?: (marketId: string, metaKey?: boolean) => void;
}
const MarketName = (props: MarketNameCellProps) => (
<>
<MarketNameCell {...props} />
{props.data ? (
<OracleStatus
dataSourceSpecForSettlementData={
props.data.tradableInstrument.instrument.product
.dataSourceSpecForSettlementData
}
dataSourceSpecForTradingTermination={
props.data.tradableInstrument.instrument.product
.dataSourceSpecForTradingTermination
}
/>
) : null}
</>
);
const defaultColDef = {
sortable: true,
filter: true,
filterParams: { buttons: ['reset'] },
};
type Props = TypedDataAgGrid<MarketMaybeWithData> & {
onMarketClick: (marketId: string, metaKey?: boolean) => void;
SuccessorMarketRenderer?: React.FC<{ value: string }>;
};
export const MarketListTable = ({
onMarketClick,
SuccessorMarketRenderer,
...props
}: Props) => {
const columnDefs = useColumnDefs({ onMarketClick });
const components = {
PriceFlashCell,
MarketName,
...(SuccessorMarketRenderer ? { SuccessorMarketRenderer } : null),
};
return (
<AgGrid
getRowId={getRowId}
defaultColDef={defaultColDef}
columnDefs={columnDefs}
suppressCellFocus
components={components}
{...props}
/>
);
};
export default MarketListTable;
@@ -1,179 +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') ===
'cell-market-actions-9',
})
);
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('SuccessorMarketRenderer should be rendered', async () => {
const successorMarketName = 'Successor Market Name';
const spySuccessorMarketRenderer = jest
.fn()
.mockReturnValue(successorMarketName);
render(
<MockedProvider>
<MarketsContainer
onSelect={spyOnSelect}
SuccessorMarketRenderer={spySuccessorMarketRenderer}
/>
</MockedProvider>
);
expect(spySuccessorMarketRenderer).toHaveBeenCalled();
expect(
screen.getByRole('columnheader', {
name: (_name, element) =>
element.getAttribute('col-id') === 'successorMarketID',
})
).toBeInTheDocument();
expect(
screen.getByRole('presentation', {
name: (_name, element) =>
element.getAttribute('id') === 'cell-successorMarketID-14',
})
).toHaveTextContent(successorMarketName);
});
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,66 +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 { marketsWithDataProvider as dataProvider } from '../../markets-provider';
import type { MarketMaybeWithData } from '../../markets-provider';
const POLLING_TIME = 2000;
interface MarketsContainerProps {
onSelect: (marketId: string, metaKey?: boolean) => void;
SuccessorMarketRenderer?: React.FC<{ value: string }>;
}
export const MarketsContainer = ({
onSelect,
SuccessorMarketRenderer,
}: MarketsContainerProps) => {
const { data, error, reload } = useDataProvider({
dataProvider,
variables: undefined,
});
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,205 +0,0 @@
import { useMemo } from 'react';
import type { ColDef } 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 } from '@vegaprotocol/ui-toolkit';
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
import { FLAGS } from '@vegaprotocol/environment';
import type { MarketMaybeWithData } from '../../markets-provider';
import { MarketActionsDropdown } from './market-table-actions';
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 },
},
{
headerName: t('Description'),
field: 'tradableInstrument.instrument.name',
},
{
headerName: t('Trading mode'),
field: 'tradingMode',
valueFormatter: ({
data,
}: VegaValueFormatterParams<MarketMaybeWithData, 'data'>) => {
if (!data?.data) return '-';
const { trigger, marketTradingMode } = data.data;
return marketTradingMode ===
MarketTradingMode.TRADING_MODE_MONITORING_AUCTION &&
trigger &&
trigger !== AuctionTrigger.AUCTION_TRIGGER_UNSPECIFIED
? `${Schema.MarketTradingModeMapping[marketTradingMode]}
- ${Schema.AuctionTriggerMapping[trigger]}`
: 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,
},
},
FLAGS.SUCCESSOR_MARKETS && {
headerName: t('Successor market'),
field: 'successorMarketID',
cellRenderer: 'SuccessorMarketRenderer',
},
{
headerName: t('Best bid'),
field: 'data.bestBidPrice',
type: 'rightAligned',
cellRenderer: 'PriceFlashCell',
filter: 'agNumberColumnFilter',
valueGetter: ({
data,
}: VegaValueGetterParams<MarketMaybeWithData>) => {
return data?.data?.bestBidPrice === undefined
? undefined
: toBigNum(
data?.data?.bestBidPrice,
data.decimalPlaces
).toNumber();
},
valueFormatter: ({
data,
}: VegaValueFormatterParams<
MarketMaybeWithData,
'data.bestBidPrice'
>) =>
data?.data?.bestBidPrice === undefined
? '-'
: addDecimalsFormatNumber(
data.data.bestBidPrice,
data.decimalPlaces
),
},
{
headerName: t('Best offer'),
field: 'data.bestOfferPrice',
type: 'rightAligned',
cellRenderer: 'PriceFlashCell',
filter: 'agNumberColumnFilter',
valueGetter: ({
data,
}: VegaValueGetterParams<MarketMaybeWithData>) => {
return data?.data?.bestOfferPrice === undefined
? undefined
: toBigNum(
data?.data?.bestOfferPrice,
data.decimalPlaces
).toNumber();
},
valueFormatter: ({
data,
}: VegaValueFormatterParams<
MarketMaybeWithData,
'data.bestOfferPrice'
>) =>
data?.data?.bestOfferPrice === undefined
? '-'
: addDecimalsFormatNumber(
data.data.bestOfferPrice,
data.decimalPlaces
),
},
{
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('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>
) : (
''
);
},
},
{
colId: 'market-actions',
field: 'id',
...COL_DEFS.actions,
cellRenderer: ({
data,
}: VegaICellRendererParams<MarketMaybeWithData>) => {
if (!data) return null;
return (
<MarketActionsDropdown
marketId={data.id}
assetId={
data.tradableInstrument.instrument.product.settlementAsset.id
}
/>
);
},
},
]),
[onMarketClick, openAssetDetailsDialog]
);
};
@@ -43,6 +43,7 @@ export const getVerifiedStatusIcon = (provider: Provider) => {
const lastVerified = provider.oracle.last_verified
? new Date(provider.oracle.last_verified)
: new Date(provider.oracle.first_verified);
return {
...getIconIntent(),
message: t(
@@ -112,13 +113,13 @@ export const OracleBasicProfile = ({
<Icon size={3} name={icon as IconName} />
</span>
</span>
<p className="text-sm dark:text-vega-light-300 text-vega-dark-300 mb-2">
<p className="mb-2 text-sm dark:text-vega-light-300 text-vega-dark-300">
{message}
</p>
{oracleMarkets && (
<p
data-testid="signed-proofs"
className="text-sm dark:text-vega-light-300 text-vega-dark-300 mb-2"
className="mb-2 text-sm dark:text-vega-light-300 text-vega-dark-300"
>
{t('Involved in %s %s', [
oracleMarkets.length.toString(),
@@ -130,9 +131,9 @@ export const OracleBasicProfile = ({
<div className="flex flex-row gap-3">
{links.map((link) => (
<ExternalLink key={link.url} href={link.url} data-testid={link.url}>
<span className="flex gap-1 items-center">
<span className="flex items-center gap-1">
<VegaIcon name={getLinkIcon(link.type)} />
<span className="capitalize underline">{link.type}</span>
<span className="underline capitalize">{link.type}</span>
<VegaIcon name={VegaIconNames.OPEN_EXTERNAL} size={13} />
</span>
</ExternalLink>
-33
View File
@@ -28,8 +28,6 @@ import {
} from './market-utils';
import { MarketsDocument } from './__generated__/markets';
import type { Candle } from './market-candles-provider';
import type { SuccessorMarketIdsQuery } from './__generated__/SuccessorMarket';
import { SuccessorMarketIdsDocument } from './__generated__';
export type Market = MarketFieldsFragment;
@@ -240,34 +238,3 @@ export const useMarketList = () => {
reload,
};
};
export type MarketSuccessors = {
__typename?: 'Market';
id: string;
successorMarketID?: string | null;
parentMarketID?: string | null;
};
const getMarketSuccessorData = (
responseData: SuccessorMarketIdsQuery | null
): MarketSuccessors[] | null =>
responseData?.marketsConnection?.edges.map((edge) => edge.node) || null;
export const marketSuccessorProvider = makeDataProvider<
SuccessorMarketIdsQuery,
MarketSuccessors[],
never,
never
>({
query: SuccessorMarketIdsDocument,
getData: getMarketSuccessorData,
fetchPolicy: 'no-cache',
});
export const useSuccessorMarketIds = (marketId: string) => {
const { data } = useDataProvider({
dataProvider: marketSuccessorProvider,
variables: undefined,
skip: !marketId,
});
return data?.find((item) => item.id === marketId) ?? null;
};
+2
View File
@@ -36,6 +36,8 @@ fragment MarketFields on Market {
positionDecimalPlaces
state
tradingMode
parentMarketID
successorMarketID
fees {
factors {
makerFee
+2
View File
@@ -42,6 +42,8 @@ export const createMarketFragment = (
close: null,
open: null,
},
successorMarketID: null,
parentMarketID: null,
fees: {
__typename: 'Fees',
factors: {
+1 -1
View File
@@ -15,6 +15,7 @@ import {
MarketNameCell,
ProgressBarCell,
MarketProductPill,
StackedCell,
} from '@vegaprotocol/datagrid';
import {
ButtonLink,
@@ -40,7 +41,6 @@ import {
import { DocsLinks } from '@vegaprotocol/environment';
import { PositionActionsDropdown } from './position-actions-dropdown';
import { LiquidationPrice } from './liquidation-price';
import { StackedCell } from './stacked-cell';
interface Props extends TypedDataAgGrid<Position> {
onClose?: (data: Position) => void;
@@ -1,10 +1,4 @@
import {
render,
screen,
act,
waitFor,
getAllByRole,
} from '@testing-library/react';
import { render, screen, waitFor, within } from '@testing-library/react';
import merge from 'lodash/merge';
import type { MockedResponse } from '@apollo/client/testing';
import { MockedProvider } from '@apollo/client/testing';
@@ -14,25 +8,13 @@ import { createProposalListFieldsFragment } from '../../lib/proposals-data-provi
import type { ProposalsListQuery } from '../../lib';
import { ProposalsListDocument } from '../../lib';
import type { PartialDeep } from 'type-fest';
import { FLAGS } from '@vegaprotocol/environment';
jest.mock('@vegaprotocol/environment', () => {
const actual = jest.requireActual('@vegaprotocol/environment');
return {
...actual,
FLAGS: {
...actual.FLAGS,
SUCCESSOR_MARKETS: true,
},
};
});
const successorMarketName = 'Successor Market Name';
const spySuccessorMarketRenderer = jest
.fn()
.mockReturnValue(successorMarketName);
const parentMarketName = 'Parent Market Name';
const ParentMarketCell = () => <span>{parentMarketName}</span>;
describe('ProposalsList', () => {
const rowContainerSelector = '.ag-center-cols-container';
const createProposalsMock = (override?: PartialDeep<ProposalsListQuery>) => {
const defaultProposalEdges = [
{
@@ -81,75 +63,55 @@ describe('ProposalsList', () => {
return mock;
};
beforeEach(() => {
jest.clearAllMocks();
});
it('should be properly rendered', async () => {
const mock = createProposalsMock();
await act(() => {
render(
<MockedProvider mocks={[mock]}>
<ProposalsList SuccessorMarketRenderer={spySuccessorMarketRenderer} />
</MockedProvider>
);
});
const container = document.querySelector('.ag-center-cols-container');
render(
<MockedProvider mocks={[mock]}>
<ProposalsList cellRenderers={{ ParentMarketCell }} />
</MockedProvider>
);
await waitFor(() => {
expect(container).toBeInTheDocument();
});
expect(getAllByRole(container as HTMLDivElement, 'row')).toHaveLength(3);
});
it('some of states should be filtered out', async () => {
const proposalNode = createProposalListFieldsFragment({
id: 'id-1',
state: Types.ProposalState.STATE_ENACTED,
expect(document.querySelector(rowContainerSelector)).toBeInTheDocument();
});
const mock = createProposalsMock({
proposalsConnection: {
edges: [
{
__typename: 'ProposalEdge',
node: {
...proposalNode,
terms: {
...proposalNode.terms,
change: {
...proposalNode.terms.change,
},
},
},
},
],
},
} as PartialDeep<ProposalsListQuery>);
await act(() => {
render(
<MockedProvider mocks={[mock]}>
<ProposalsList SuccessorMarketRenderer={spySuccessorMarketRenderer} />
</MockedProvider>
);
});
const container = document.querySelector('.ag-center-cols-container');
await waitFor(() => {
expect(container).toBeInTheDocument();
expect(getAllByRole(container as HTMLDivElement, 'row')).toHaveLength(2);
});
const expectedHeaders = [
'Market',
'Settlement asset',
'State',
'Parent market',
'Voting',
'Closing date',
'Enactment date',
'', // actions col
];
expect(spySuccessorMarketRenderer).toHaveBeenCalled();
const headers = screen.getAllByRole('columnheader');
expect(headers).toHaveLength(expectedHeaders.length);
expect(
screen.getByRole('columnheader', {
name: (_name, element) =>
element.getAttribute('col-id') === 'parentMarket',
})
).toBeInTheDocument();
headers.map((h) => h.querySelector('[ref="eText"]')?.textContent?.trim())
).toEqual(expectedHeaders);
const container = within(
document.querySelector(rowContainerSelector) as HTMLElement
);
expect(container.getAllByRole('row')).toHaveLength(
// @ts-ignore data is mocked
mock?.result?.data.proposalsConnection.edges.length
);
expect(
screen.getAllByRole('gridcell', {
name: (name, element) =>
element.getAttribute('col-id') === 'parentMarket',
container.getAllByRole('gridcell', {
name: (_, element) =>
element.getAttribute('col-id') ===
'terms.change.successorConfiguration.parentMarketId',
})[0]
).toHaveTextContent(successorMarketName);
).toHaveTextContent(parentMarketName);
});
it('empty response should causes no data message display', async () => {
@@ -169,58 +131,11 @@ describe('ProposalsList', () => {
},
},
};
await act(() => {
render(
<MockedProvider mocks={[mock]}>
<ProposalsList SuccessorMarketRenderer={spySuccessorMarketRenderer} />
</MockedProvider>
);
});
expect(await screen.findByText('No markets')).toBeInTheDocument();
expect(
screen.getByRole('columnheader', {
name: (_name, element) =>
element.getAttribute('col-id') === 'parentMarket',
})
).toBeInTheDocument();
});
it('feature flag should hide parent marketcolumn', async () => {
const mockedFlags = jest.mocked(FLAGS);
mockedFlags.SUCCESSOR_MARKETS = false;
const mock: MockedResponse<ProposalsListQuery> = {
request: {
query: ProposalsListDocument,
variables: {
proposalType: Types.ProposalType.TYPE_NEW_MARKET,
},
},
result: {
data: {
proposalsConnection: {
__typename: 'ProposalsConnection',
edges: [],
},
},
},
};
await act(() => {
render(
<MockedProvider mocks={[mock]}>
<ProposalsList SuccessorMarketRenderer={spySuccessorMarketRenderer} />
</MockedProvider>
);
});
await waitFor(() => {
expect(
screen.getByRole('columnheader', {
name: (_name, element) => element.getAttribute('col-id') === 'market',
})
).toBeInTheDocument();
});
screen.getAllByRole('columnheader').forEach((element) => {
expect(element.getAttribute('col-id')).not.toEqual('parentMarket');
});
render(
<MockedProvider mocks={[mock]}>
<ProposalsList cellRenderers={{ ParentMarketCell }} />
</MockedProvider>
);
expect(await screen.findByText('No proposed markets')).toBeInTheDocument();
});
});
@@ -1,11 +1,11 @@
import React from 'react';
import type { FC } from 'react';
import { AgGridLazy as AgGrid } from '@vegaprotocol/datagrid';
import { t } from '@vegaprotocol/i18n';
import * as Types from '@vegaprotocol/types';
import { MarketNameProposalCell, useColumnDefs } from './use-column-defs';
import { removePaginationWrapper } from '@vegaprotocol/utils';
import type { ProposalListFieldsFragment } from '../../lib/proposals-data-provider/__generated__/Proposals';
import { useProposalsListQuery } from '../../lib/proposals-data-provider/__generated__/Proposals';
import { removePaginationWrapper } from '@vegaprotocol/utils';
import { useColumnDefs } from './use-column-defs';
export const getNewMarketProposals = (data: ProposalListFieldsFragment[]) =>
data.filter((proposal) =>
@@ -16,13 +16,19 @@ export const getNewMarketProposals = (data: ProposalListFieldsFragment[]) =>
].includes(proposal.state)
);
const defaultColDef = {
sortable: true,
filter: true,
filterParams: { buttons: ['reset'] },
};
interface ProposalListProps {
SuccessorMarketRenderer: React.FC<{ value: string }>;
cellRenderers: {
[name: string]: FC<{ value: string; data: ProposalListFieldsFragment }>;
};
}
export const ProposalsList = ({
SuccessorMarketRenderer,
}: ProposalListProps) => {
export const ProposalsList = ({ cellRenderers }: ProposalListProps) => {
const { data } = useProposalsListQuery({
variables: {
proposalType: Types.ProposalType.TYPE_NEW_MARKET,
@@ -32,7 +38,7 @@ export const ProposalsList = ({
const filteredData = getNewMarketProposals(
removePaginationWrapper(data?.proposalsConnection?.edges)
);
const { columnDefs, defaultColDef } = useColumnDefs();
const columnDefs = useColumnDefs();
return (
<AgGrid
@@ -40,8 +46,9 @@ export const ProposalsList = ({
rowData={filteredData}
defaultColDef={defaultColDef}
getRowId={({ data }) => data.id}
overlayNoRowsTemplate={t('No markets')}
components={{ SuccessorMarketRenderer, MarketNameProposalCell }}
overlayNoRowsTemplate={t('No proposed markets')}
components={cellRenderers}
rowHeight={45}
/>
);
};
@@ -6,9 +6,9 @@ import {
COL_DEFS,
DateRangeFilter,
SetFilter,
StackedCell,
} from '@vegaprotocol/datagrid';
import compact from 'lodash/compact';
import { useEnvironment, FLAGS } from '@vegaprotocol/environment';
import { getDateTimeFormat } from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import {
@@ -19,50 +19,15 @@ import type {
VegaICellRendererParams,
VegaValueFormatterParams,
} from '@vegaprotocol/datagrid';
import { ExternalLink, Pill } from '@vegaprotocol/ui-toolkit';
import {
ProposalProductTypeMapping,
ProposalProductTypeShortName,
ProductTypeMapping,
ProductTypeShortName,
ProposalStateMapping,
} from '@vegaprotocol/types';
import type { ProposalListFieldsFragment } from '../../lib/proposals-data-provider/__generated__/Proposals';
import { VoteProgress } from '../voting-progress';
import { ProposalActionsDropdown } from '../proposal-actions-dropdown';
export const MarketNameProposalCell = ({
value,
data,
}: VegaICellRendererParams<
ProposalListFieldsFragment,
'terms.change.instrument.code'
>) => {
const { VEGA_TOKEN_URL } = useEnvironment();
const { change } = data?.terms || {};
if (change?.__typename === 'NewMarket' && VEGA_TOKEN_URL) {
const type = change.instrument.futureProduct?.__typename;
const content = (
<>
<span data-testid="market-code">{value as string}</span>
{type && (
<Pill
size="xxs"
className="uppercase ml-0.5"
title={ProposalProductTypeMapping[type]}
>
{ProposalProductTypeShortName[type]}
</Pill>
)}
</>
);
if (data?.id) {
const link = `${VEGA_TOKEN_URL}/proposals/${data.id}`;
return <ExternalLink href={link}>{content}</ExternalLink>;
}
return content;
}
return null;
};
export const useColumnDefs = () => {
const { params } = useNetworkParams([
NetworkParams.governance_proposal_market_requiredMajority,
@@ -80,17 +45,36 @@ export const useColumnDefs = () => {
headerName: t('Market'),
field: 'terms.change.instrument.code',
cellStyle: { lineHeight: '14px' },
cellRenderer: 'MarketNameProposalCell',
},
{
colId: 'description',
headerName: t('Description'),
field: 'terms.change.instrument.name',
cellRenderer: ({
value,
data,
}: {
value: string;
data: ProposalListFieldsFragment;
}) => {
if (!value || !data) return '-';
// TODO: update when we switch to ProductConfiguration
const productType = 'Future';
return (
<StackedCell
primary={value}
secondary={
<span
title={ProductTypeMapping[productType]}
className="uppercase"
>
{ProductTypeShortName[productType]}
</span>
}
/>
);
},
},
{
colId: 'asset',
headerName: t('Settlement asset'),
field: 'terms.change.instrument.futureProduct.settlementAsset.name',
field: 'terms.change.instrument.futureProduct.settlementAsset.symbol',
},
{
colId: 'state',
@@ -105,12 +89,10 @@ export const useColumnDefs = () => {
set: ProposalStateMapping,
},
},
FLAGS.SUCCESSOR_MARKETS && {
{
headerName: t('Parent market'),
field: 'id',
colId: 'parentMarket',
cellRenderer: 'SuccessorMarketRenderer',
cellRendererParams: { parent: true },
field: 'terms.change.successorConfiguration.parentMarketId',
cellRenderer: 'ParentMarketCell',
},
{
colId: 'voting',
@@ -169,25 +151,12 @@ export const useColumnDefs = () => {
data,
}: VegaICellRendererParams<ProposalListFieldsFragment>) => {
if (!data?.id) return null;
return <ProposalActionsDropdown id={data.id} />;
},
},
]);
}, [requiredMajorityPercentage]);
const defaultColDef: ColDef = useMemo(() => {
return {
sortable: true,
filter: true,
filterParams: { buttons: ['reset'] },
};
}, []);
return useMemo(
() => ({
columnDefs,
defaultColDef,
}),
[columnDefs, defaultColDef]
);
return columnDefs;
};
@@ -114,6 +114,9 @@ fragment NewMarketFields on NewMarket {
lpPriceRange
# linearSlippageFactor
# quadraticSlippageFactor
successorConfiguration {
parentMarketId
}
}
fragment UpdateMarketFields on UpdateMarket {
File diff suppressed because one or more lines are too long
@@ -109,6 +109,58 @@ export const marketUpdateProposal: ProposalListFieldsFragment = {
export const createProposalListFieldsFragment = (
override?: PartialDeep<ProposalListFieldsFragment>
): ProposalListFieldsFragment => {
const newMarket = {
decimalPlaces: 1,
lpPriceRange: '',
riskParameters: {
__typename: 'SimpleRiskModel',
params: {
__typename: 'SimpleRiskModelParams',
factorLong: 0,
factorShort: 1,
},
},
metadata: undefined,
successorConfiguration: {
__typename: 'SuccessorConfiguration',
parentMarketId: 'xyz',
},
instrument: {
code: 'ETHUSD',
name: 'ETHUSD',
futureProduct: {
settlementAsset: {
id: 'b340c130096819428a62e5df407fd6abe66e444b89ad64f670beb98621c9c663',
name: 'tDAI TEST',
symbol: 'tDAI',
decimals: 1,
quantum: '1',
__typename: 'Asset',
},
quoteName: '',
dataSourceSpecBinding: {
__typename: 'DataSourceSpecToFutureBinding',
settlementDataProperty: '',
tradingTerminationProperty: '',
},
dataSourceSpecForSettlementData: {
__typename: 'DataSourceDefinition',
sourceType: {
__typename: 'DataSourceDefinitionInternal',
},
},
dataSourceSpecForTradingTermination: {
__typename: 'DataSourceDefinition',
sourceType: {
__typename: 'DataSourceDefinitionInternal',
},
},
__typename: 'FutureProduct',
},
__typename: 'InstrumentConfiguration',
},
__typename: 'NewMarket',
} as const;
const defaultProposal: ProposalListFieldsFragment = {
id: 'e9ec6d5c46a7e7bcabf9ba7a893fa5a5eeeec08b731f06f7a6eb7bf0e605b829',
reference: 'injected_at_runtime',
@@ -147,54 +199,7 @@ export const createProposalListFieldsFragment = (
terms: {
closingDatetime: '2022-11-15T12:44:34Z',
enactmentDatetime: '2022-11-15T12:44:54Z',
change: {
decimalPlaces: 1,
lpPriceRange: '',
riskParameters: {
__typename: 'SimpleRiskModel',
params: {
__typename: 'SimpleRiskModelParams',
factorLong: 0,
factorShort: 1,
},
},
metadata: [],
instrument: {
code: 'ETHUSD',
name: 'ETHUSD',
futureProduct: {
settlementAsset: {
id: 'b340c130096819428a62e5df407fd6abe66e444b89ad64f670beb98621c9c663',
name: 'tDAI TEST',
symbol: 'tDAI',
decimals: 1,
quantum: '1',
__typename: 'Asset',
},
quoteName: '',
dataSourceSpecBinding: {
__typename: 'DataSourceSpecToFutureBinding',
settlementDataProperty: '',
tradingTerminationProperty: '',
},
dataSourceSpecForSettlementData: {
__typename: 'DataSourceDefinition',
sourceType: {
__typename: 'DataSourceDefinitionInternal',
},
},
dataSourceSpecForTradingTermination: {
__typename: 'DataSourceDefinition',
sourceType: {
__typename: 'DataSourceDefinitionInternal',
},
},
__typename: 'FutureProduct',
},
__typename: 'InstrumentConfiguration',
},
__typename: 'NewMarket',
},
change: newMarket,
__typename: 'ProposalTerms',
},
__typename: 'Proposal',
@@ -1,5 +1,6 @@
import * as AccordionPrimitive from '@radix-ui/react-accordion';
import classNames from 'classnames';
import type { VegaIconProps } from '../icon';
import { VegaIcon, VegaIconNames } from '../icon';
export interface AccordionItemProps {
@@ -24,6 +25,28 @@ export const Accordion = ({ panels, children }: AccordionProps) => {
);
};
export const AccordionPanel = ({
trigger,
children,
itemId,
}: {
trigger: React.ReactNode;
children: React.ReactNode;
itemId: string;
}) => {
return (
<AccordionPrimitive.Item value={itemId}>
<AccordionPrimitive.Header>{trigger}</AccordionPrimitive.Header>
<AccordionPrimitive.Content
className="py-3 text-sm"
data-testid="accordion-content"
>
{children}
</AccordionPrimitive.Content>
</AccordionPrimitive.Item>
);
};
export const AccordionItem = ({
title,
content,
@@ -31,7 +54,7 @@ export const AccordionItem = ({
}: AccordionPanelProps) => {
const triggerClassNames = classNames(
'w-full py-2',
'flex items-center justify-between border-b border-vega-light-200 dark:border-vega-dark-200 text-sm',
'flex items-center justify-between gap-2 border-b border-vega-light-200 dark:border-vega-dark-200 text-sm',
'group'
);
return (
@@ -41,7 +64,9 @@ export const AccordionItem = ({
data-testid="accordion-toggle"
className={triggerClassNames}
>
<span data-testid="accordion-title">{title}</span>
<span data-testid="accordion-title" className="flex-1 text-left">
{title}
</span>
<AccordionChevron aria-hidden />
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
@@ -55,15 +80,17 @@ export const AccordionItem = ({
);
};
export const AccordionChevron = () => {
export const AccordionChevron = ({
size = 16,
}: Pick<VegaIconProps, 'size'>) => {
return (
<span
className={classNames(
'transform transition ease-in-out duration-300',
'flex transform transition ease-in-out duration-300',
'group-data-[state=open]:rotate-180'
)}
>
<VegaIcon name={VegaIconNames.CHEVRON_DOWN} aria-hidden />
<VegaIcon name={VegaIconNames.CHEVRON_DOWN} aria-hidden size={size} />
</span>
);
};
@@ -8,6 +8,7 @@ import {
Portal,
} from '@radix-ui/react-tooltip';
import type { ITooltipParams } from 'ag-grid-community';
import classNames from 'classnames';
const tooltipContentClasses =
'max-w-sm bg-vega-light-100 dark:bg-vega-dark-100 border border-vega-light-200 dark:border-vega-dark-200 px-2 py-1 z-20 rounded text-xs text-black dark:text-white break-word';
@@ -18,10 +19,14 @@ export interface TooltipProps {
align?: 'start' | 'center' | 'end';
side?: 'top' | 'right' | 'bottom' | 'left';
sideOffset?: number;
noUnderline?: boolean;
}
export const TOOLTIP_TRIGGER_CLASS_NAME =
'underline underline-offset-2 decoration-neutral-400 dark:decoration-neutral-400 decoration-dashed';
export const TOOLTIP_TRIGGER_CLASS_NAME = (noUnderline?: boolean) =>
classNames(
{ 'underline underline-offset-2': !noUnderline },
'decoration-neutral-400 dark:decoration-neutral-400 decoration-dashed'
);
// Conditionally rendered tooltip if description content is provided.
export const Tooltip = ({
@@ -31,11 +36,12 @@ export const Tooltip = ({
sideOffset,
align = 'start',
side = 'bottom',
noUnderline,
}: TooltipProps) =>
description ? (
<Provider delayDuration={200} skipDelayDuration={100}>
<Root open={open}>
<Trigger asChild className={TOOLTIP_TRIGGER_CLASS_NAME}>
<Trigger asChild className={TOOLTIP_TRIGGER_CLASS_NAME(noUnderline)}>
{children}
</Trigger>
{description && (
@@ -39,17 +39,17 @@ const getClassName = (
},
// colours
{
'bg-vega-yellow hover:bg-vega-yellow-550 dark:bg-vega-yellow dark:hover:bg-vega-yellow-450':
'bg-vega-yellow enabled:hover:bg-vega-yellow-550 dark:bg-vega-yellow dark:enabled:hover:bg-vega-yellow-450':
intent === Intent.Primary,
'bg-vega-clight-500 hover:bg-vega-clight-400 dark:bg-vega-cdark-500 dark:hover:bg-vega-cdark-400':
'bg-vega-clight-500 enabled:hover:bg-vega-clight-400 dark:bg-vega-cdark-500 dark:enabled:hover:bg-vega-cdark-400':
intent === Intent.None,
'bg-vega-blue-350 hover:bg-vega-blue-400 dark:bg-vega-blue-650 dark:hover:bg-vega-blue-600':
'bg-vega-blue-350 enabled:hover:bg-vega-blue-400 dark:bg-vega-blue-650 dark:enabled:hover:bg-vega-blue-600':
intent === Intent.Info,
'bg-vega-orange-350 hover:bg-vega-orange-400 dark:bg-vega-orange-650 dark:hover:bg-vega-orange-600':
'bg-vega-orange-350 enabled:hover:bg-vega-orange-400 dark:bg-vega-orange-650 dark:enabled:hover:bg-vega-orange-600':
intent === Intent.Warning,
'bg-vega-red-350 hover:bg-vega-red-400 dark:bg-vega-red-650 dark:hover:bg-vega-red-600':
'bg-vega-red-350 enabled:hover:bg-vega-red-400 dark:bg-vega-red-650 dark:enabled:hover:bg-vega-red-600':
intent === Intent.Danger,
'bg-vega-green-350 hover:bg-vega-green-400 dark:bg-vega-green-650 dark:hover:bg-vega-green-600':
'bg-vega-green-350 enabled:hover:bg-vega-green-400 dark:bg-vega-green-650 dark:enabled:hover:bg-vega-green-600':
intent === Intent.Success,
'text-vega-clight-50 dark:text-vega-cdark-50': intent !== Intent.Primary,
'text-vega-clight-900 dark:text-vega-cdark-900':
@@ -73,7 +73,7 @@ const Content = ({
children,
}: Pick<TradingButtonProps, 'icon' | 'subLabel' | 'children'>) => (
<>
<span data-label className="font-alpha leading-none" key="children">
<span data-label className="leading-none font-alpha" key="children">
{children}
</span>
{icon}
@@ -241,51 +241,49 @@ const ConnectorList = ({
'Connect securely, deposit funds and approve or reject transactions with the Vega wallet'
)}
</p>
<div data-testid="connectors-list" className="flex flex-col mt-4 gap-4">
<div>
{isBrowserWalletInstalled() ? (
<ConnectionOptionWithDescription
type="injected"
text={extendedText}
onClick={() => onSelect('injected')}
title={
<>
<span>{t('Vega Wallet')}</span>
{' '}
<span className="text-xs">{t('full featured')}</span>
</>
}
description={t(
`Connect Vega Wallet extension
<div data-testid="connectors-list" className="flex flex-col mt-4 gap-2">
{isBrowserWalletInstalled() ? (
<ConnectionOptionWithDescription
type="injected"
text={extendedText}
onClick={() => onSelect('injected')}
title={
<>
<span>{t('Vega Wallet')}</span>
{' '}
<span className="text-xs">{t('full featured')}</span>
</>
}
description={t(
`Connect Vega Wallet extension
for %s to access all features including key
management and detailed transaction views from your
browser.`,
[browserName]
)}
/>
) : (
<div>
<h1 className="mb-1 text-lg">
<span>{t('Vega Wallet')}</span>
{' '}
<span className="text-xs"> {t('full featured')}</span>
</h1>
<p className="mb-2 text-sm">
{t(
`Install Vega Wallet extension
for %s to access all features including key
management and detailed transaction views from your
browser.`,
[browserName]
)}
</p>
<GetWalletButton
chromeExtensionUrl={links.chromeExtensionUrl}
mozillaExtensionUrl={links.mozillaExtensionUrl}
/>
) : (
<div className="py-2">
<h1 className="px-2 text-lg">
<span>{t('Vega Wallet')}</span>
{' '}
<span className="text-xs"> {t('full featured')}</span>
</h1>
<p className="p-2 text-sm">
{t(
`Install Vega Wallet extension
for %s to access all features including key
management and detailed transaction views from your
browser.`,
[browserName]
)}
</p>
<GetWalletButton
chromeExtensionUrl={links.chromeExtensionUrl}
mozillaExtensionUrl={links.mozillaExtensionUrl}
/>
</div>
)}
</div>
</div>
)}
{connectors['snap'] !== undefined ? (
<div>
{snapStatus === SnapStatus.INSTALLED ? (
@@ -335,7 +333,7 @@ const ConnectorList = ({
}}
/>
{snapStatus === SnapStatus.NOT_SUPPORTED ? (
<p className="pt-1 text-xs leading-tight text-default">
<p className="pt-1 text-xs leading-tight text-muted">
{t('No MetaMask version that supports snaps detected.')}{' '}
{t('Learn more about')}{' '}
<ExternalLink href="https://metamask.io/snaps/">
@@ -348,7 +346,7 @@ const ConnectorList = ({
</div>
) : null}
<div>
<h1 className="my-1 text-md">{t('Advanced / Other options...')}</h1>
<h1 className="mb-1 text-md">{t('Advanced / Other options...')}</h1>
<ConnectionOption
type="view"
text={t('View as party')}
@@ -520,7 +518,7 @@ const ConnectionOptionWithDescription = ({
return (
<div>
<h1 className="text-md">{title}</h1>
<p className="pb-2 text-sm text-gray-60 text-muted">{description}</p>
<p className="mb-2 text-sm text-gray-60 text-muted">{description}</p>
<ConnectionOption
disabled={disabled}
type={type}
@@ -579,10 +577,11 @@ const CustomUrlInput = ({
<div className="flex justify-between mb-1.5">
<p className="text-sm text-secondary">{t('Custom wallet location')}</p>
<button
className="text-sm underline"
className="text-sm text-muted"
onClick={() => setUrlInputExpanded(false)}
>
<VegaIcon name={VegaIconNames.ARROW_LEFT} /> {t('Go back')}
<VegaIcon name={VegaIconNames.ARROW_LEFT} />{' '}
<span className="underline underline-offset-4">{t('Go back')}</span>
</button>
</div>
<TradingFormGroup
@@ -612,18 +611,20 @@ const CustomUrlInput = ({
onClick={() => onSelect('jsonRpc')}
/>
{isDesktopWalletRunning !== null && (
<p className="pt-1 mb-2 text-sm leading-tight">
<div className="pt-1 mb-2 text-sm">
{isDesktopWalletRunning ? (
<button
className="underline text-default"
className="text-muted"
onClick={() => setUrlInputExpanded(true)}
disabled={Boolean(pubKey)}
>
{t('Enter a custom wallet location')}{' '}
<span className="underline underline-offset-4">
{t('Enter a custom wallet location')}
</span>{' '}
<VegaIcon name={VegaIconNames.ARROW_RIGHT} />
</button>
) : (
<>
<p className="leading-tight text-muted">
<span className="text-xs">
{t(
'No running Desktop App/CLI detected. Open your app now to connect or enter a'
@@ -636,9 +637,9 @@ const CustomUrlInput = ({
>
{t('custom wallet location')}
</button>
</>
</p>
)}
</p>
</div>
)}
</>
);
@@ -88,10 +88,13 @@ export function ViewConnectorForm({
<div className="flex justify-end">
<button
onClick={reset}
className="p-2 text-sm underline"
className="p-2 text-sm"
data-testid="back-button"
>
<VegaIcon name={VegaIconNames.ARROW_LEFT} /> {t('Go back')}
<VegaIcon name={VegaIconNames.ARROW_LEFT} />{' '}
<span className="underline underline-offset-4">
{t('Go back')}
</span>
</button>
</div>
)}
+1 -1
View File
@@ -70,7 +70,7 @@
"jsondiffpatch": "^0.4.1",
"lodash": "^4.17.21",
"next": "13.3.0",
"pennant": "1.11.1",
"pennant": "1.12.0",
"react": "18.2.0",
"react-copy-to-clipboard": "^5.0.4",
"react-dom": "18.2.0",
+4 -4
View File
@@ -20253,10 +20253,10 @@ pend@~1.2.0:
resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50"
integrity sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==
pennant@1.11.1:
version "1.11.1"
resolved "https://registry.yarnpkg.com/pennant/-/pennant-1.11.1.tgz#f47bceaade01db215eeba666cd755840d16fe9ed"
integrity sha512-U26OxjxETWLJAvCFj20oH0y5gzyBfJ5oD4O3dYg1aWnH4giUYDPWuUDFnzlMJ+yXolyYECccXm6S1BkoVcVzDg==
pennant@1.12.0:
version "1.12.0"
resolved "https://registry.yarnpkg.com/pennant/-/pennant-1.12.0.tgz#e12707d5f1aac554d81bad060637e608335e0b50"
integrity sha512-xosg5erRf+Ke9iORdqyv+SOGcD3uJX1dgf990q1DvHuzz36w2txCZsfnvcXhRO++HYVKS9sU/YLPRLJgolFGtA==
dependencies:
"@babel/runtime" "^7.13.10"
"@d3fc/d3fc-technical-indicator" "^8.0.1"