Compare commits

..
35 changed files with 1395 additions and 1194 deletions
+1
View File
@@ -151,6 +151,7 @@ jobs:
fi fi
projects_e2e=${projects_e2e%?} projects_e2e=${projects_e2e%?}
projects_e2e=[${projects_e2e// /,}] projects_e2e=[${projects_e2e// /,}]
projects=${projects%?}
projects=[${projects// /,}] projects=[${projects// /,}]
echo PROJECTS_E2E=$projects_e2e >> $GITHUB_ENV echo PROJECTS_E2E=$projects_e2e >> $GITHUB_ENV
echo PROJECTS=$projects >> $GITHUB_ENV echo PROJECTS=$projects >> $GITHUB_ENV
@@ -63,7 +63,7 @@ context(
vegaWalletSetSpecifiedApprovalAmount('1000'); vegaWalletSetSpecifiedApprovalAmount('1000');
}); });
describe.skip('Eth wallet - contains VEGA tokens', function () { describe('Eth wallet - contains VEGA tokens', function () {
beforeEach( beforeEach(
'teardown wallet & drill into a specific validator', 'teardown wallet & drill into a specific validator',
function () { function () {
@@ -102,8 +102,7 @@ export const Proposal = ({ proposal, restData }: ProposalProps) => {
</div> </div>
{proposal.terms.change.__typename !== 'NewMarket' && {proposal.terms.change.__typename !== 'NewMarket' &&
proposal.terms.change.__typename !== 'UpdateMarket' && proposal.terms.change.__typename !== 'UpdateMarket' && (
proposal.terms.change.__typename !== 'NewFreeform' && (
<div className="mb-4"> <div className="mb-4">
<ProposalTerms data={proposal.terms} /> <ProposalTerms data={proposal.terms} />
</div> </div>
@@ -3,18 +3,6 @@ import { aliasGQLQuery } from '@vegaprotocol/cypress';
import { marketsDataQuery } from '@vegaprotocol/mock'; import { marketsDataQuery } from '@vegaprotocol/mock';
import { positionsQuery } from '@vegaprotocol/mock'; import { positionsQuery } from '@vegaprotocol/mock';
// #region consts
const closePosition = 'close-position';
const dialogCloseX = 'dialog-close';
const dialogContent = 'dialog-content';
const dropDownMenu = 'dropdown-menu';
const marketActionsContent = 'market-actions-content';
const positions = 'Positions';
const tabPositions = 'tab-positions';
const toastContent = 'toast-content';
const tooltipContent = 'tooltip-content';
// #endregion
beforeEach(() => { beforeEach(() => {
cy.mockTradingPage(); cy.mockTradingPage();
cy.mockSubscription(); cy.mockSubscription();
@@ -23,9 +11,8 @@ beforeEach(() => {
describe('positions', { tags: '@smoke', testIsolation: true }, () => { describe('positions', { tags: '@smoke', testIsolation: true }, () => {
it('renders positions on trading page', () => { it('renders positions on trading page', () => {
visitAndClickPositions(); cy.visit('/#/markets/market-0');
// 7004-POSI-001 cy.getByTestId('Positions').click();
// 7004-POSI-002
validatePositionsDisplayed(); validatePositionsDisplayed();
}); });
@@ -48,313 +35,174 @@ describe('positions', { tags: '@smoke', testIsolation: true }, () => {
} }
aliasGQLQuery(req, 'Positions', positions); aliasGQLQuery(req, 'Positions', positions);
}); });
visitAndClickPositions(); cy.visit('/#/portfolio');
// 7004-POSI-001 cy.getByTestId('Positions').click();
// 7004-POSI-002
validatePositionsDisplayed(true); validatePositionsDisplayed(true);
}); });
describe('renders position among some graphql errors', () => {
it('Close my position', () => { it('rows should be displayed despite errors', () => {
visitAndClickPositions(); const errors = [
cy.getByTestId(closePosition).first().click(); {
// 7004-POSI-010 message:
cy.getByTestId(toastContent).should( 'no market data for market: 9c55fb644c6f7de5422d40d691a62bffd5898384c70135bab29ba1e3e2e5280a',
'contain.text', path: ['marketsConnection', 'edges'],
'Awaiting confirmation' extensions: {
); code: 13,
}); type: 'Internal',
}); },
describe('positions', { tags: '@regression', testIsolation: true }, () => {
it('rows should be displayed despite errors', () => {
const errors = [
{
message:
'no market data for market: 9c55fb644c6f7de5422d40d691a62bffd5898384c70135bab29ba1e3e2e5280a',
path: ['marketsConnection', 'edges'],
extensions: {
code: 13,
type: 'Internal',
}, },
}, ];
]; const marketData = marketsDataQuery();
const marketData = marketsDataQuery(); const edges = marketData.marketsConnection?.edges.map((market) => {
const edges = marketData.marketsConnection?.edges.map((market) => { const replace =
const replace = market.node.data?.market.id === 'market-2' ? null : market.node.data;
market.node.data?.market.id === 'market-2' ? null : market.node.data; return { ...market, node: { ...market.node, data: replace } };
return { ...market, node: { ...market.node, data: replace } }; });
}); const overrides = {
const overrides = { ...marketData,
...marketData, marketsConnection: { ...marketData.marketsConnection, edges },
marketsConnection: { ...marketData.marketsConnection, edges }, };
}; cy.mockGQL((req) => {
cy.mockGQL((req) => { aliasGQLQuery(req, 'MarketsData', overrides, errors);
aliasGQLQuery(req, 'MarketsData', overrides, errors); });
}); cy.visit('/#/markets/market-0');
cy.visit('/#/markets/market-0'); const emptyCells = [
const emptyCells = [ 'notional',
'notional', 'markPrice',
'markPrice', 'currentLeverage',
'currentLeverage', 'averageEntryPrice',
'averageEntryPrice', ];
]; cy.getByTestId('tab-positions')
cy.getByTestId(tabPositions) .first()
.first() .within(() => {
.within(() => { cy.get(
cy.get( '[row-id="02eceaba4df2bef76ea10caf728d8a099a2aa846cced25737cccaa9812342f65-market-2"]'
'[row-id="02eceaba4df2bef76ea10caf728d8a099a2aa846cced25737cccaa9812342f65-market-2"]' )
) .eq(1)
.eq(1) .within(() => {
.within(() => { emptyCells.forEach((cell) => {
emptyCells.forEach((cell) => { cy.get(`[col-id="${cell}"]`).should('contain.text', '-');
cy.get(`[col-id="${cell}"]`).should('contain.text', '-'); });
}); });
}); });
}); });
}); it('error message should be displayed', () => {
const errors = [
it('error message should be displayed', () => { {
const errors = [ message:
{ 'no market data for asset: 9c55fb644c6f7de5422d40d691a62bffd5898384c70135bab29ba1e3e2e5280a',
message: path: ['assets', 'edges'],
'no market data for asset: 9c55fb644c6f7de5422d40d691a62bffd5898384c70135bab29ba1e3e2e5280a', extensions: {
path: ['assets', 'edges'], code: 13,
extensions: { type: 'Internal',
code: 13, },
type: 'Internal',
}, },
}, ];
]; const overrides = {
const overrides = { marketsConnection: { edges: [] },
marketsConnection: { edges: [] }, };
}; cy.mockGQL((req) => {
cy.mockGQL((req) => { aliasGQLQuery(req, 'MarketsData', overrides, errors);
aliasGQLQuery(req, 'MarketsData', overrides, errors);
});
cy.visit('/#/markets/market-0');
cy.getByTestId(tabPositions).contains('no market data');
});
it('sorting by Market', () => {
visitAndClickPositions();
const marketsSortedDefault = [
'ACTIVE MARKET',
'Apple Monthly (30 Jun 2022)',
'ETHBTC Quarterly (30 Jun 2022)',
'SUSPENDED MARKET',
];
const marketsSortedAsc = [
'ACTIVE MARKET',
'Apple Monthly (30 Jun 2022)',
'ETHBTC Quarterly (30 Jun 2022)',
'SUSPENDED MARKET',
];
const marketsSortedDesc = [
'SUSPENDED MARKET',
'ETHBTC Quarterly (30 Jun 2022)',
'Apple Monthly (30 Jun 2022)',
'ACTIVE MARKET',
];
cy.getByTestId(positions).click();
// 7004-POSI-003
checkSorting(
'marketName',
marketsSortedDefault,
marketsSortedAsc,
marketsSortedDesc
);
});
it('Resize column', () => {
let elementWidth: number;
visitAndClickPositions();
cy.get('.ag-overlay-loading-wrapper').should('not.be.visible');
cy.get('.ag-header-container').within(() => {
cy.get(`[col-id="marketName"]`)
.find('.ag-header-cell-resize')
.realMouseDown()
.realMouseMove(250, 0)
.realMouseUp();
});
// 7004-POSI-006
cy.get(`[col-id="marketName"]`)
.invoke('width')
.should('be.greaterThan', 250);
cy.get(`[col-id="marketName"]`)
.invoke('width')
.then((width) => {
elementWidth = width as number;
})
.then(() => {
let localStorageCopy: Record<string, string>;
cy.window().then((win) => {
localStorageCopy = { ...win.localStorage };
});
cy.reload();
cy.window().then((win) => {
Object.keys(localStorageCopy).forEach((key) => {
win.localStorage.setItem(key, localStorageCopy[key]);
});
});
// 7004-POSI-012
cy.get('[col-id="marketName"]')
.invoke('width')
.should('equal', elementWidth);
}); });
}); cy.visit('/#/markets/market-0');
cy.get('[data-testid="tab-positions"]').contains('no market data');
it('Scroll horizontally', () => {
visitAndClickPositions();
cy.get('.ag-header-container').within(() => {
cy.get(`[col-id="marketName"]`)
.find('.ag-header-cell-resize')
.realMouseDown()
.realMouseMove(400, 0)
.realMouseUp();
});
cy.get('[col-id="marketName"]').should('be.visible');
cy.get('.ag-body-horizontal-scroll-viewport').realMouseWheel({
deltaX: 500,
});
// 7004-POSI-004
cy.get('[col-id="updatedAt"]').should('be.visible');
});
it('Drag and drop columns', () => {
visitAndClickPositions();
cy.get('.ag-overlay-loading-wrapper').should('not.be.visible');
cy.get('[col-id="marketName"]')
.realMouseDown()
.realMouseMove(700, 15)
.realMouseUp();
// 7004-POSI-005
cy.get('[col-id="marketName"]').should(($element) => {
const attributeValue = $element.attr('aria-colindex');
expect(attributeValue).not.to.equal('1');
}); });
}); });
it('I can see warnings', () => { describe('sorting by ag-grid columns should work well', () => {
visitAndClickPositions(); it('sorting by Market', () => {
cy.visit('/#/markets/market-0');
const marketsSortedDefault = [
'ACTIVE MARKET',
'Apple Monthly (30 Jun 2022)',
'SUSPENDED MARKET',
];
const marketsSortedAsc = ['ACTIVE MARKET', 'Apple Monthly (30 Jun 2022)'];
const marketsSortedDesc = [
'SUSPENDED MARKET',
'Apple Monthly (30 Jun 2022)',
'ACTIVE MARKET',
];
cy.getByTestId('Positions').click();
checkSorting(
'marketName',
marketsSortedDefault,
marketsSortedAsc,
marketsSortedDesc
);
});
it('sorting by notional', () => {
cy.visit('/#/markets/market-0');
const marketsSortedDefault = [
'276,761.40348',
'46,126.90058',
'1,688.20',
];
const marketsSortedAsc = ['1,688.20', '46,126.90058', '276,761.40348'];
const marketsSortedDesc = ['276,761.40348', '46,126.90058', '1,688.20'];
cy.getByTestId('Positions').click();
checkSorting(
'notional',
marketsSortedDefault,
marketsSortedAsc,
marketsSortedDesc
);
});
it('sorting by unrealisedPNL', () => {
cy.visit('/#/markets/market-0');
const marketsSortedDefault = ['8.95', '-0.22519', '8.95'];
const marketsSortedAsc = ['-0.22519', '8.95', '8.95'];
const marketsSortedDesc = ['8.95', '8.95', '-0.22519'];
cy.getByTestId('Positions').click();
checkSorting(
'unrealisedPNL',
marketsSortedDefault,
marketsSortedAsc,
marketsSortedDesc
);
});
});
cy.get('[col-id="openVolume"]').within(() => { function validatePositionsDisplayed(multiKey = false) {
cy.get('[aria-label="warning-sign icon"]') cy.getByTestId('tab-positions').should('be.visible');
cy.getByTestId('tab-positions').within(() => {
cy.get('[col-id="marketName"]')
.should('be.visible') .should('be.visible')
.realHover(); .each(($marketSymbol) => {
}); cy.wrap($marketSymbol).invoke('text').should('not.be.empty');
// 7004-POSI-011 });
cy.getByTestId(tooltipContent).should('be.visible');
});
it('Positive and Negative color change', () => { cy.get('.ag-center-cols-container [col-id="openVolume"]').each(
cy.visit('/#/markets/market-0'); ($openVolume) => {
cy.getByTestId(positions).click(); cy.wrap($openVolume).invoke('text').should('not.be.empty');
// 7004-POSI-007 }
cy.get('.ag-center-cols-container').within(() => {
assertPNLColor(
'[col-id="realisedPNL"]',
'text-vega-green',
'text-vega-pink'
); );
});
cy.get('.ag-center-cols-container').within(() => {
assertPNLColor(
'[col-id="unrealisedPNL"]',
'text-vega-green',
'text-vega-pink'
);
});
cy.get('.ag-center-cols-container').within(() => {
assertPNLColor(
'[col-id="openVolume"]',
'text-vega-green',
'text-vega-pink'
);
});
});
it('View settlement asset', () => { // includes average entry price, mark price, realised PNL & leverage
visitAndClickPositions(); cy.getByTestId('flash-cell').each(($prices) => {
cy.get('[col-id="asset"]').within(() => { cy.wrap($prices).invoke('text').should('not.be.empty');
cy.get('button[type="button"]').first().click();
});
// 7004-POSI-008
cy.getByTestId(dialogContent).should('be.visible');
cy.getByTestId(dialogCloseX).click();
cy.getByTestId(dropDownMenu).first().click();
cy.getByTestId(marketActionsContent).click();
// 7004-POSI-009
cy.getByTestId(dialogContent).should('be.visible');
});
});
function validatePositionsDisplayed(multiKey = false) {
cy.getByTestId('tab-positions').should('be.visible');
cy.getByTestId('tab-positions').within(() => {
cy.get('[col-id="marketName"]')
.should('be.visible')
.each(($marketSymbol) => {
cy.wrap($marketSymbol).invoke('text').should('not.be.empty');
}); });
cy.get('.ag-center-cols-container [col-id="openVolume"]').each( if (!multiKey) {
($openVolume) => { cy.get('[col-id="currentLeverage"]').should('contain.text', '2.846.1');
cy.wrap($openVolume).invoke('text').should('not.be.empty'); cy.get('[col-id="marginAccountBalance"]') // margin allocated
.should('contain.text', '0.01');
} }
);
// includes average entry price, mark price, realised PNL & leverage cy.get('[col-id="unrealisedPNL"]').each(($unrealisedPnl) => {
cy.getByTestId('flash-cell').each(($prices) => { cy.wrap($unrealisedPnl).invoke('text').should('not.be.empty');
cy.wrap($prices).invoke('text').should('not.be.empty'); });
cy.get('[col-id="notional"]').should('contain.text', '276,761.40348'); // Total tDAI position
cy.get('[col-id="realisedPNL"]').should('contain.text', '2.30'); // Total Realised PNL
cy.get('[col-id="unrealisedPNL"]').should('contain.text', '8.95'); // Total Unrealised PNL
cy.get('.ag-header-row [col-id="notional"]')
.should('contain.text', 'Notional')
.realHover();
cy.get('.ag-popup').should('contain.text', 'Mark price x open volume');
}); });
if (!multiKey) { cy.getByTestId('close-position').should('be.visible').and('have.length', 3);
cy.get('[col-id="currentLeverage"]').should('contain.text', '2.846.1'); }
cy.get('[col-id="marginAccountBalance"]') // margin allocated });
.should('contain.text', '0.01');
}
cy.get('[col-id="unrealisedPNL"]').each(($unrealisedPnl) => {
cy.wrap($unrealisedPnl).invoke('text').should('not.be.empty');
});
cy.get('[col-id="notional"]').should('contain.text', '276,761.40348'); // Total tDAI position
cy.get('[col-id="realisedPNL"]').should('contain.text', '2.30'); // Total Realised PNL
cy.get('[col-id="unrealisedPNL"]').should('contain.text', '8.95'); // Total Unrealised PNL
cy.get('.ag-header-row [col-id="notional"]')
.should('contain.text', 'Notional')
.realHover();
cy.get('.ag-popup').should('contain.text', 'Mark price x open volume');
});
cy.getByTestId('close-position').should('be.visible').and('have.length', 3);
}
function assertPNLColor(
pnlSelector: string,
positiveClass: string,
negativeClass: string
) {
cy.get(pnlSelector).each(($el) => {
const value = parseFloat($el.text());
if (value > 0) {
cy.wrap($el).invoke('attr', 'class').should('contain', positiveClass);
} else if (value < 0) {
cy.wrap($el).invoke('attr', 'class').should('contain', negativeClass);
} else if (value == 0) {
cy.wrap($el)
.invoke('attr', 'class')
.should('not.contain', negativeClass, positiveClass);
} else {
throw new Error('Unexpected value');
}
});
}
function visitAndClickPositions() {
cy.visit('/#/markets/market-0');
cy.getByTestId(positions).click();
}
+1 -1
View File
@@ -14,4 +14,4 @@ NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/annou
NX_VEGA_INCIDENT_URL=https://blog.vega.xyz/tagged/vega-incident-reports NX_VEGA_INCIDENT_URL=https://blog.vega.xyz/tagged/vega-incident-reports
NX_VEGA_CONSOLE_URL=https://console.vega.xyz NX_VEGA_CONSOLE_URL=https://console.vega.xyz
# TAG name of the current app version - TODO: bump to the latest upon release # TAG name of the current app version - TODO: bump to the latest upon release
NX_APP_VERSION=v0.20.18-core-0.71.8 NX_APP_VERSION=v0.20.16-core-0.71.5
@@ -38,6 +38,7 @@ type Data = Item[];
type QueryData = { type QueryData = {
data: Data; data: Data;
pageInfo?: PageInfo; pageInfo?: PageInfo;
totalCount?: number;
}; };
type CombinedData = { type CombinedData = {
@@ -114,6 +115,7 @@ const paginatedSubscribe = makeDataProvider<
first, first,
append: defaultAppend, append: defaultAppend,
getPageInfo: (r) => r?.pageInfo ?? null, getPageInfo: (r) => r?.pageInfo ?? null,
getTotalCount: (r) => r?.totalCount,
}, },
}); });
@@ -371,10 +373,32 @@ describe('data provider', () => {
subscription.unsubscribe(); subscription.unsubscribe();
}); });
it('loads requested data blocks', async () => { it('fills data with nulls if pagination is enabled', async () => {
const totalCount = 1000;
const data: Item[] = new Array(first).fill(null).map((v, i) => ({
cursor: i.toString(),
node: {
id: i.toString(),
},
}));
const subscription = paginatedSubscribe(callback, client, variables);
await resolveQuery({
data,
totalCount,
pageInfo: {
hasNextPage: true,
},
});
expect(callback.mock.calls[1][0].data?.length).toBe(totalCount);
subscription.unsubscribe();
});
it('loads requested data blocks and inserts data with total count', async () => {
const totalCount = 1000;
const subscription = paginatedSubscribe(callback, client, variables); const subscription = paginatedSubscribe(callback, client, variables);
await resolveQuery({ await resolveQuery({
data: generateData(), data: generateData(),
totalCount,
pageInfo: { pageInfo: {
hasNextPage: true, hasNextPage: true,
endCursor: '100', endCursor: '100',
@@ -383,25 +407,168 @@ describe('data provider', () => {
// load next page // load next page
subscription.load && subscription.load(); subscription.load && subscription.load();
const lastQueryArgs = let lastQueryArgs =
clientQuery.mock.calls[clientQuery.mock.calls.length - 1][0]; clientQuery.mock.calls[clientQuery.mock.calls.length - 1][0];
expect(lastQueryArgs?.variables?.['pagination']).toEqual({ expect(lastQueryArgs?.variables?.pagination).toEqual({
after: '100', after: '100',
first, first,
}); });
await resolveQuery({ await resolveQuery({
data: generateData(100), data: generateData(100),
pageInfo: { pageInfo: {
hasNextPage: false, hasNextPage: true,
endCursor: '200', endCursor: '200',
}, },
}); });
// load page with skip
subscription.load && subscription.load(500, 600);
lastQueryArgs =
clientQuery.mock.calls[clientQuery.mock.calls.length - 1][0];
expect(lastQueryArgs?.variables?.pagination).toEqual({
after: '200',
first,
skip: 300,
});
await resolveQuery({
data: generateData(500),
pageInfo: {
hasNextPage: true,
endCursor: '600',
},
});
// load in the gap
subscription.load && subscription.load(400, 500);
lastQueryArgs =
clientQuery.mock.calls[clientQuery.mock.calls.length - 1][0];
expect(lastQueryArgs?.variables?.pagination).toEqual({
after: '200',
first,
skip: 200,
});
await resolveQuery({
data: generateData(400),
pageInfo: {
hasNextPage: true,
endCursor: '500',
},
});
// load page after last block
subscription.load && subscription.load(700, 800);
lastQueryArgs =
clientQuery.mock.calls[clientQuery.mock.calls.length - 1][0];
expect(lastQueryArgs?.variables?.pagination).toEqual({
after: '600',
first,
skip: 100,
});
await resolveQuery({
data: generateData(700),
pageInfo: {
hasNextPage: true,
endCursor: '800',
},
});
// load last page shorter than expected
subscription.load && subscription.load(950, 1050);
lastQueryArgs =
clientQuery.mock.calls[clientQuery.mock.calls.length - 1][0];
expect(lastQueryArgs?.variables?.pagination).toEqual({
after: '800',
first,
skip: 150,
});
await resolveQuery({
data: generateData(950, 20),
pageInfo: {
hasNextPage: false,
endCursor: '970',
},
});
let lastCallbackArgs = callback.mock.calls[callback.mock.calls.length - 1];
expect(lastCallbackArgs[0].totalCount).toBe(970);
// load next page when pageInfo.hasNextPage === false // load next page when pageInfo.hasNextPage === false
const clientQueryCallsLength = clientQuery.mock.calls.length; const clientQueryCallsLength = clientQuery.mock.calls.length;
subscription.load && subscription.load(); subscription.load && subscription.load();
expect(clientQuery.mock.calls.length).toBe(clientQueryCallsLength); expect(clientQuery.mock.calls.length).toBe(clientQueryCallsLength);
// load last page longer than expected
subscription.load && subscription.load(960, 1000);
lastQueryArgs =
clientQuery.mock.calls[clientQuery.mock.calls.length - 1][0];
expect(lastQueryArgs?.variables?.pagination).toEqual({
after: '960',
first,
});
await resolveQuery({
data: generateData(960, 40),
pageInfo: {
hasNextPage: true,
endCursor: '1000',
},
});
lastCallbackArgs = callback.mock.calls[callback.mock.calls.length - 1];
expect(lastCallbackArgs[0].totalCount).toBe(1000);
subscription.unsubscribe();
});
it('loads requested data blocks and inserts data without totalCount', async () => {
const totalCount = undefined;
const subscription = paginatedSubscribe(callback, client, variables);
await resolveQuery({
data: generateData(),
totalCount,
pageInfo: {
hasNextPage: true,
endCursor: '100',
},
});
let lastCallbackArgs = callback.mock.calls[callback.mock.calls.length - 1];
expect(lastCallbackArgs[0].totalCount).toBe(undefined);
// load next page
subscription.load && subscription.load();
await resolveQuery({
data: generateData(100),
pageInfo: {
hasNextPage: true,
endCursor: '200',
},
});
lastCallbackArgs = callback.mock.calls[callback.mock.calls.length - 1];
expect(lastCallbackArgs[0].totalCount).toBe(undefined);
// load last page
subscription.load && subscription.load();
await resolveQuery({
data: generateData(200, 50),
pageInfo: {
hasNextPage: false,
endCursor: '250',
},
});
lastCallbackArgs = callback.mock.calls[callback.mock.calls.length - 1];
expect(lastCallbackArgs[0].totalCount).toBe(250);
subscription.unsubscribe();
});
it('sets total count when first page has no next page', async () => {
const subscription = paginatedSubscribe(callback, client, variables);
await resolveQuery({
data: generateData(),
pageInfo: {
hasNextPage: false,
endCursor: '100',
},
});
const lastCallbackArgs =
callback.mock.calls[callback.mock.calls.length - 1];
expect(lastCallbackArgs[0].totalCount).toBe(100);
subscription.unsubscribe(); subscription.unsubscribe();
}); });
@@ -585,7 +752,7 @@ describe('derived data provider', () => {
subscription.load && subscription.load(); subscription.load && subscription.load();
const lastQueryArgs = const lastQueryArgs =
clientQuery.mock.calls[clientQuery.mock.calls.length - 1][0]; clientQuery.mock.calls[clientQuery.mock.calls.length - 1][0];
expect(lastQueryArgs?.variables?.['pagination']).toEqual({ expect(lastQueryArgs?.variables?.pagination).toEqual({
after: '100', after: '100',
first, first,
}); });
+85 -28
View File
@@ -27,6 +27,7 @@ export interface UpdateCallback<Data, Delta> {
loading: boolean; loading: boolean;
loaded: boolean; loaded: boolean;
pageInfo: PageInfo | null; pageInfo: PageInfo | null;
totalCount?: number;
} }
): void; ): void;
} }
@@ -39,7 +40,9 @@ export interface Reload {
(forceReset?: boolean): void; (forceReset?: boolean): void;
} }
type Pagination = Schema.Pagination; type Pagination = Schema.Pagination & {
skip?: number;
};
export interface PageInfo { export interface PageInfo {
startCursor?: string; startCursor?: string;
@@ -80,8 +83,12 @@ export interface Append<Data> {
data: Data | null, data: Data | null,
insertionData: Data | null, insertionData: Data | null,
insertionPageInfo: PageInfo | null, insertionPageInfo: PageInfo | null,
pagination?: Pagination pagination?: Pagination,
): Data | null; totalCount?: number
): {
data: Data | null;
totalCount?: number;
};
} }
interface GetData<QueryData, Data, Variables> { interface GetData<QueryData, Data, Variables> {
@@ -92,6 +99,10 @@ interface GetPageInfo<QueryData> {
(queryData: QueryData): PageInfo | null; (queryData: QueryData): PageInfo | null;
} }
interface GetTotalCount<QueryData> {
(queryData: QueryData): number | undefined;
}
interface GetDelta<SubscriptionData, Delta, Variables> { interface GetDelta<SubscriptionData, Delta, Variables> {
( (
subscriptionData: SubscriptionData, subscriptionData: SubscriptionData,
@@ -108,32 +119,44 @@ export interface Edge<T extends Node> extends Cursor {
node: T; node: T;
} }
export function defaultAppend<T extends Cursor>( export function defaultAppend<Data>(
data: T[] | null, data: Data | null,
insertionData: T[] | null, insertionData: Data | null,
insertionPageInfo: PageInfo | null, insertionPageInfo: PageInfo | null,
pagination?: Pagination pagination?: Pagination,
totalCount?: number
) { ) {
if (data && insertionData && insertionPageInfo) { if (data && insertionData && insertionPageInfo) {
if (!(data instanceof Array) || !(insertionData instanceof Array)) { if (!(data instanceof Array) || !(insertionData instanceof Array)) {
throw new Error( throw new Error(
'data needs to be instance of Array[] when using pagination' 'data needs to be instance of Edge[] when using pagination'
); );
} }
if (pagination?.after) { if (pagination?.after) {
if (data[data.length - 1].cursor === pagination?.after) {
return [...data, ...insertionData];
}
const cursors = data.map((item) => item && item.cursor); const cursors = data.map((item) => item && item.cursor);
const startIndex = cursors.lastIndexOf(pagination.after); const startIndex = cursors.lastIndexOf(pagination.after);
if (startIndex !== -1) { if (startIndex !== -1) {
const start = startIndex + 1; const start = startIndex + 1 + (pagination.skip ?? 0);
const updatedData = [...data.slice(0, start), ...insertionData]; const end = start + insertionData.length;
return updatedData; let updatedData = [
...data.slice(0, start),
...insertionData,
...data.slice(end),
];
if (!insertionPageInfo.hasNextPage && end !== (totalCount ?? 0)) {
// adjust totalCount if last page is shorter or longer than expected
totalCount = end;
updatedData = updatedData.slice(0, end);
}
return {
data: updatedData,
// increase totalCount if last page is longer than expected
totalCount: totalCount && Math.max(updatedData.length, totalCount),
};
} }
} }
} }
return data; return { data, totalCount };
} }
interface DataProviderParams< interface DataProviderParams<
@@ -152,6 +175,7 @@ interface DataProviderParams<
getDelta?: GetDelta<SubscriptionData, Delta, Variables>; getDelta?: GetDelta<SubscriptionData, Delta, Variables>;
pagination?: { pagination?: {
getPageInfo: GetPageInfo<QueryData>; getPageInfo: GetPageInfo<QueryData>;
getTotalCount?: GetTotalCount<QueryData>;
append: Append<Data>; append: Append<Data>;
first: number; first: number;
}; };
@@ -221,6 +245,7 @@ function makeDataProviderInternal<
let client: ApolloClient<object>; let client: ApolloClient<object>;
let subscription: Subscription[] | undefined; let subscription: Subscription[] | undefined;
let pageInfo: PageInfo | null = null; let pageInfo: PageInfo | null = null;
let totalCount: number | undefined;
// notify single callback about current state, delta is passes optionally only if notify was invoked onNext // notify single callback about current state, delta is passes optionally only if notify was invoked onNext
const notify = ( const notify = (
@@ -233,6 +258,7 @@ function makeDataProviderInternal<
loading, loading,
loaded, loaded,
pageInfo, pageInfo,
totalCount,
...updateData, ...updateData,
}); });
}; };
@@ -275,41 +301,59 @@ function makeDataProviderInternal<
} }
}); });
const load = async () => { const load = async (start?: number) => {
if (!pagination) { if (!pagination) {
return Promise.reject(); return Promise.reject();
} }
if (!pageInfo?.hasNextPage) {
return null;
}
const paginationVariables: Pagination = { const paginationVariables: Pagination = {
first: pagination.first, first: pagination.first,
after: pageInfo?.endCursor,
}; };
if (data) { if (start !== undefined && data instanceof Array) {
const endCursor = (data as Cursor[])[(data as Cursor[]).length - 1] if (!start) {
.cursor; paginationVariables.after = undefined;
if (endCursor) { } else if (data && data[start - 1]) {
paginationVariables.after = endCursor; paginationVariables.after = (data[start - 1] as Cursor).cursor;
} else {
let skip = 1;
while (!data[start - 1 - skip] && skip <= start) {
skip += 1;
}
paginationVariables.skip = skip;
if (skip === start) {
paginationVariables.after = undefined;
} else {
paginationVariables.after = (data[start - 1 - skip] as Cursor).cursor;
}
} }
} else if (!pageInfo?.hasNextPage) {
return null;
} }
const res = await call(paginationVariables); const res = await call(paginationVariables);
const insertionData = getData(res.data, variables); const insertionData = getData(res.data, variables);
const insertionPageInfo = pagination.getPageInfo(res.data); const insertionPageInfo = pagination.getPageInfo(res.data);
data = pagination.append( ({ data, totalCount } = pagination.append(
data, data,
insertionData, insertionData,
insertionPageInfo, insertionPageInfo,
paginationVariables paginationVariables,
); totalCount
));
pageInfo = insertionPageInfo; pageInfo = insertionPageInfo;
totalCount =
(pagination.getTotalCount && pagination.getTotalCount(res.data)) ??
totalCount;
notifyAll({ insertionData, isInsert: true }); notifyAll({ insertionData, isInsert: true });
return insertionData; return insertionData;
}; };
const setData = (updatedData: Data | null) => { const setData = (updatedData: Data | null) => {
data = updatedData; data = updatedData;
if (totalCount !== undefined && data instanceof Array) {
totalCount = data.length;
}
}; };
const subscriptionSubscribe = () => { const subscriptionSubscribe = () => {
@@ -356,6 +400,16 @@ function makeDataProviderInternal<
); );
} }
pageInfo = pagination.getPageInfo(res.data); pageInfo = pagination.getPageInfo(res.data);
if (pageInfo && !pageInfo.hasNextPage) {
totalCount = data.length;
} else {
totalCount =
pagination.getTotalCount && pagination.getTotalCount(res.data);
}
if (data && totalCount && data.length < totalCount) {
data.push(...new Array(totalCount - data.length).fill(null));
}
} }
// if there was some updates received from subscription during initial query loading apply them on just received data // if there was some updates received from subscription during initial query loading apply them on just received data
if (update && data && updateQueue && updateQueue.length > 0) { if (update && data && updateQueue && updateQueue.length > 0) {
@@ -363,6 +417,9 @@ function makeDataProviderInternal<
const delta = updateQueue.shift(); const delta = updateQueue.shift();
if (delta) { if (delta) {
setData(update(data, delta, reload, variables)); setData(update(data, delta, reload, variables));
if (totalCount !== undefined && data instanceof Array) {
totalCount = data.length;
}
} }
} }
} }
@@ -533,7 +590,7 @@ const memoize = <
* @param update Update<Data, Delta> function that will be executed on each onNext, it should update data base on delta, it can reload data provider * @param update Update<Data, Delta> function that will be executed on each onNext, it should update data base on delta, it can reload data provider
* @param getData transforms received query data to format that will be stored in data provider * @param getData transforms received query data to format that will be stored in data provider
* @param getDelta transforms delta data to format that will be stored in data provider * @param getDelta transforms delta data to format that will be stored in data provider
* @param pagination pagination related functions { getPageInfo, append, first } * @param pagination pagination related functions { getPageInfo, getTotalCount, append, first }
* @returns Subscribe<Data, Delta> subscribe function * @returns Subscribe<Data, Delta> subscribe function
* @example * @example
* const marketMidPriceProvider = makeDataProvider<QueryData, Data, SubscriptionData, Delta>({ * const marketMidPriceProvider = makeDataProvider<QueryData, Data, SubscriptionData, Delta>({
+18 -3
View File
@@ -12,13 +12,23 @@ export interface useDataProviderParams<
Variables extends OperationVariables | undefined = undefined Variables extends OperationVariables | undefined = undefined
> { > {
dataProvider: Subscribe<Data, Delta, Variables>; dataProvider: Subscribe<Data, Delta, Variables>;
update?: ({ delta, data }: { delta?: Delta; data: Data | null }) => boolean; update?: ({
delta,
data,
totalCount,
}: {
delta?: Delta;
data: Data | null;
totalCount?: number;
}) => boolean;
insert?: ({ insert?: ({
insertionData, insertionData,
data, data,
totalCount,
}: { }: {
insertionData?: Data | null; insertionData?: Data | null;
data: Data | null; data: Data | null;
totalCount?: number;
}) => boolean; }) => boolean;
variables: Variables; variables: Variables;
skipUpdates?: boolean; skipUpdates?: boolean;
@@ -46,6 +56,7 @@ export const useDataProvider = <
}: useDataProviderParams<Data, Delta, Variables>) => { }: useDataProviderParams<Data, Delta, Variables>) => {
const client = useApolloClient(); const client = useApolloClient();
const [data, setData] = useState<Data | null>(null); const [data, setData] = useState<Data | null>(null);
const [totalCount, setTotalCount] = useState<number>();
const [loading, setLoading] = useState<boolean>(!skip); const [loading, setLoading] = useState<boolean>(!skip);
const [error, setError] = useState<Error | undefined>(undefined); const [error, setError] = useState<Error | undefined>(undefined);
const flushRef = useRef<(() => void) | undefined>(undefined); const flushRef = useRef<(() => void) | undefined>(undefined);
@@ -90,6 +101,7 @@ export const useDataProvider = <
error, error,
loading, loading,
insertionData, insertionData,
totalCount,
isInsert, isInsert,
isUpdate, isUpdate,
loaded, loaded,
@@ -104,18 +116,19 @@ export const useDataProvider = <
(skipUpdatesRef.current || (skipUpdatesRef.current ||
(!skipUpdatesRef.current && (!skipUpdatesRef.current &&
updateRef.current && updateRef.current &&
updateRef.current({ delta, data }))) updateRef.current({ delta, data, totalCount })))
) { ) {
return; return;
} }
if ( if (
isInsert && isInsert &&
insertRef.current && insertRef.current &&
insertRef.current({ insertionData, data }) insertRef.current({ insertionData, data, totalCount })
) { ) {
return; return;
} }
} }
setTotalCount(totalCount);
setData(data); setData(data);
if (!loading && !isUpdate && updateRef.current) { if (!loading && !isUpdate && updateRef.current) {
updateRef.current({ data }); updateRef.current({ data });
@@ -137,6 +150,7 @@ export const useDataProvider = <
useEffect(() => { useEffect(() => {
setData(null); setData(null);
setError(undefined); setError(undefined);
setTotalCount(undefined);
if (updateRef.current) { if (updateRef.current) {
updateRef.current({ data: null }); updateRef.current({ data: null });
} }
@@ -170,6 +184,7 @@ export const useDataProvider = <
flush, flush,
reload, reload,
load, load,
totalCount,
}; };
}; };
@@ -146,7 +146,7 @@ export const DealTicket = ({
}); });
const openVolume = useOpenVolume(pubKey, market.id) ?? '0'; const openVolume = useOpenVolume(pubKey, market.id) ?? '0';
const orders = activeOrders const orders = activeOrders
? activeOrders.map<OrderInfo>((order) => ({ ? activeOrders.map<OrderInfo>(({ node: order }) => ({
isMarketOrder: order.type === OrderType.TYPE_MARKET, isMarketOrder: order.type === OrderType.TYPE_MARKET,
price: order.price, price: order.price,
remaining: order.remaining, remaining: order.remaining,
+1
View File
@@ -1,3 +1,4 @@
export * from './lib/fills-container'; export * from './lib/fills-container';
export * from './lib/use-fills-list';
export * from './lib/fills-data-provider'; export * from './lib/fills-data-provider';
export * from './lib/__generated__/Fills'; export * from './lib/__generated__/Fills';
+21 -25
View File
@@ -48,32 +48,28 @@ query Fills($filter: TradesFilter, $pagination: Pagination) {
} }
} }
fragment FillUpdateFields on TradeUpdate {
id
marketId
buyOrder
sellOrder
buyerId
sellerId
aggressor
price
size
createdAt
type
buyerFee {
makerFee
infrastructureFee
liquidityFee
}
sellerFee {
makerFee
infrastructureFee
liquidityFee
}
}
subscription FillsEvent($filter: TradesSubscriptionFilter!) { subscription FillsEvent($filter: TradesSubscriptionFilter!) {
tradesStream(filter: $filter) { tradesStream(filter: $filter) {
...FillUpdateFields id
marketId
buyOrder
sellOrder
buyerId
sellerId
aggressor
price
size
createdAt
type
buyerFee {
makerFee
infrastructureFee
liquidityFee
}
sellerFee {
makerFee
infrastructureFee
liquidityFee
}
} }
} }
+22 -29
View File
@@ -15,8 +15,6 @@ export type FillsQueryVariables = Types.Exact<{
export type FillsQuery = { __typename?: 'Query', trades?: { __typename?: 'TradeConnection', edges: Array<{ __typename?: 'TradeEdge', cursor: string, node: { __typename?: 'Trade', id: string, createdAt: any, price: string, size: string, buyOrder: string, sellOrder: string, aggressor: Types.Side, market: { __typename?: 'Market', id: string }, buyer: { __typename?: 'Party', id: string }, seller: { __typename?: 'Party', id: string }, buyerFee: { __typename?: 'TradeFee', makerFee: string, infrastructureFee: string, liquidityFee: string }, sellerFee: { __typename?: 'TradeFee', makerFee: string, infrastructureFee: string, liquidityFee: string } } }>, pageInfo: { __typename?: 'PageInfo', startCursor: string, endCursor: string, hasNextPage: boolean, hasPreviousPage: boolean } } | null }; export type FillsQuery = { __typename?: 'Query', trades?: { __typename?: 'TradeConnection', edges: Array<{ __typename?: 'TradeEdge', cursor: string, node: { __typename?: 'Trade', id: string, createdAt: any, price: string, size: string, buyOrder: string, sellOrder: string, aggressor: Types.Side, market: { __typename?: 'Market', id: string }, buyer: { __typename?: 'Party', id: string }, seller: { __typename?: 'Party', id: string }, buyerFee: { __typename?: 'TradeFee', makerFee: string, infrastructureFee: string, liquidityFee: string }, sellerFee: { __typename?: 'TradeFee', makerFee: string, infrastructureFee: string, liquidityFee: string } } }>, pageInfo: { __typename?: 'PageInfo', startCursor: string, endCursor: string, hasNextPage: boolean, hasPreviousPage: boolean } } | null };
export type FillUpdateFieldsFragment = { __typename?: 'TradeUpdate', id: string, marketId: string, buyOrder: string, sellOrder: string, buyerId: string, sellerId: string, aggressor: Types.Side, price: string, size: string, createdAt: any, type: Types.TradeType, buyerFee: { __typename?: 'TradeFee', makerFee: string, infrastructureFee: string, liquidityFee: string }, sellerFee: { __typename?: 'TradeFee', makerFee: string, infrastructureFee: string, liquidityFee: string } };
export type FillsEventSubscriptionVariables = Types.Exact<{ export type FillsEventSubscriptionVariables = Types.Exact<{
filter: Types.TradesSubscriptionFilter; filter: Types.TradesSubscriptionFilter;
}>; }>;
@@ -62,31 +60,6 @@ export const FillEdgeFragmentDoc = gql`
cursor cursor
} }
${FillFieldsFragmentDoc}`; ${FillFieldsFragmentDoc}`;
export const FillUpdateFieldsFragmentDoc = gql`
fragment FillUpdateFields on TradeUpdate {
id
marketId
buyOrder
sellOrder
buyerId
sellerId
aggressor
price
size
createdAt
type
buyerFee {
makerFee
infrastructureFee
liquidityFee
}
sellerFee {
makerFee
infrastructureFee
liquidityFee
}
}
`;
export const FillsDocument = gql` export const FillsDocument = gql`
query Fills($filter: TradesFilter, $pagination: Pagination) { query Fills($filter: TradesFilter, $pagination: Pagination) {
trades(filter: $filter, pagination: $pagination) { trades(filter: $filter, pagination: $pagination) {
@@ -134,10 +107,30 @@ export type FillsQueryResult = Apollo.QueryResult<FillsQuery, FillsQueryVariable
export const FillsEventDocument = gql` export const FillsEventDocument = gql`
subscription FillsEvent($filter: TradesSubscriptionFilter!) { subscription FillsEvent($filter: TradesSubscriptionFilter!) {
tradesStream(filter: $filter) { tradesStream(filter: $filter) {
...FillUpdateFields id
marketId
buyOrder
sellOrder
buyerId
sellerId
aggressor
price
size
createdAt
type
buyerFee {
makerFee
infrastructureFee
liquidityFee
}
sellerFee {
makerFee
infrastructureFee
liquidityFee
}
} }
} }
${FillUpdateFieldsFragmentDoc}`; `;
/** /**
* __useFillsEventSubscription__ * __useFillsEventSubscription__
+70 -90
View File
@@ -1,34 +1,74 @@
import produce from 'immer';
import orderBy from 'lodash/orderBy'; import orderBy from 'lodash/orderBy';
import type { PageInfo, Cursor } from '@vegaprotocol/data-provider'; import {} from '@vegaprotocol/utils';
import type { PageInfo, Edge } from '@vegaprotocol/data-provider';
import { import {
makeDataProvider, makeDataProvider,
makeDerivedDataProvider, makeDerivedDataProvider,
defaultAppend as append, defaultAppend as append,
paginatedCombineDelta as combineDelta,
paginatedCombineInsertionData as combineInsertionData,
} from '@vegaprotocol/data-provider'; } from '@vegaprotocol/data-provider';
import type { Market } from '@vegaprotocol/markets'; import type { Market } from '@vegaprotocol/markets';
import { marketsMapProvider } from '@vegaprotocol/markets'; import { marketsProvider } from '@vegaprotocol/markets';
import { FillsDocument, FillsEventDocument } from './__generated__/Fills'; import { FillsDocument, FillsEventDocument } from './__generated__/Fills';
import type { import type {
FillsQuery, FillsQuery,
FillsQueryVariables, FillsQueryVariables,
FillFieldsFragment, FillFieldsFragment,
FillEdgeFragment,
FillsEventSubscription, FillsEventSubscription,
FillUpdateFieldsFragment,
FillsEventSubscriptionVariables,
} from './__generated__/Fills'; } from './__generated__/Fills';
const update = (
data: FillEdgeFragment[] | null,
delta: FillsEventSubscription['tradesStream']
) => {
return produce(data, (draft) => {
orderBy(delta, 'createdAt').forEach((node) => {
if (draft === null) {
return;
}
const index = draft.findIndex((edge) => edge?.node.id === node.id);
if (index !== -1) {
if (draft[index]?.node) {
Object.assign(draft[index]?.node as FillFieldsFragment, node);
}
} else {
const firstNode = draft[0]?.node;
if (
(firstNode && node.createdAt >= firstNode.createdAt) ||
!firstNode
) {
const { buyerId, sellerId, marketId, ...trade } = node;
draft.unshift({
node: {
...trade,
__typename: 'Trade',
market: {
__typename: 'Market',
id: marketId,
},
buyer: { id: buyerId, __typename: 'Party' },
seller: { id: buyerId, __typename: 'Party' },
},
cursor: '',
__typename: 'TradeEdge',
});
}
}
});
});
};
export type Trade = Omit<FillFieldsFragment, 'market'> & { export type Trade = Omit<FillFieldsFragment, 'market'> & {
market?: Market; market?: Market;
isLastPlaceholder?: boolean; isLastPlaceholder?: boolean;
}; };
export type TradeEdge = Edge<Trade>;
const getData = ( const getData = (responseData: FillsQuery | null): FillEdgeFragment[] =>
responseData: FillsQuery | null responseData?.trades?.edges || [];
): (FillFieldsFragment & Cursor)[] =>
responseData?.trades?.edges.map<FillFieldsFragment & Cursor>((edge) => ({
...edge.node,
cursor: edge.cursor,
})) || [];
const getPageInfo = (responseData: FillsQuery | null): PageInfo | null => const getPageInfo = (responseData: FillsQuery | null): PageInfo | null =>
responseData?.trades?.pageInfo || null; responseData?.trades?.pageInfo || null;
@@ -36,65 +76,16 @@ const getPageInfo = (responseData: FillsQuery | null): PageInfo | null =>
const getDelta = (subscriptionData: FillsEventSubscription) => const getDelta = (subscriptionData: FillsEventSubscription) =>
subscriptionData.tradesStream || []; subscriptionData.tradesStream || [];
const mapFillUpdateToFill = (
fillUpdate: FillUpdateFieldsFragment
): FillFieldsFragment => {
const { buyerId, sellerId, marketId, ...fill } = fillUpdate;
return {
...fill,
__typename: 'Trade',
market: {
__typename: 'Market',
id: marketId,
},
buyer: { id: buyerId, __typename: 'Party' },
seller: { id: buyerId, __typename: 'Party' },
};
};
const mapFillUpdateToFillWithMarket =
(markets: Record<string, Market>) =>
(fillUpdate: FillUpdateFieldsFragment): Trade => {
const { market, ...fill } = mapFillUpdateToFill(fillUpdate);
return {
...fill,
market: markets[market.id],
};
};
const update = <T extends Omit<FillFieldsFragment, 'market'> & Cursor>(
data: T[] | null,
delta: ReturnType<typeof getDelta>,
variables: FillsQueryVariables,
mapDeltaToData: (delta: FillUpdateFieldsFragment) => T
): T[] => {
const updatedData = data ? [...data] : ([] as T[]);
orderBy(delta, 'createdAt', 'desc').forEach((fillUpdate) => {
const index = data?.findIndex((fill) => fill.id === fillUpdate.id) ?? -1;
if (index !== -1) {
updatedData[index] = {
...updatedData[index],
...mapDeltaToData(fillUpdate),
};
} else if (!data?.length || fillUpdate.createdAt >= data[0].createdAt) {
updatedData.unshift(mapDeltaToData(fillUpdate));
}
});
return updatedData;
};
export const fillsProvider = makeDataProvider< export const fillsProvider = makeDataProvider<
Parameters<typeof getData>['0'], Parameters<typeof getData>['0'],
ReturnType<typeof getData>, ReturnType<typeof getData>,
Parameters<typeof getDelta>['0'], Parameters<typeof getDelta>['0'],
ReturnType<typeof getDelta>, ReturnType<typeof getDelta>,
FillsQueryVariables, FillsQueryVariables
FillsEventSubscriptionVariables
>({ >({
query: FillsDocument, query: FillsDocument,
subscriptionQuery: FillsEventDocument, subscriptionQuery: FillsEventDocument,
update: (data, delta, reload, variables) => update,
update(data, delta, variables, mapFillUpdateToFill),
getData, getData,
getDelta, getDelta,
pagination: { pagination: {
@@ -102,41 +93,30 @@ export const fillsProvider = makeDataProvider<
append, append,
first: 100, first: 100,
}, },
getSubscriptionVariables: ({ filter }) => {
const variables: FillsEventSubscriptionVariables = { filter: {} };
if (filter) {
variables.filter = {
partyIds: filter.partyIds,
marketIds: filter.marketIds,
};
}
return variables;
},
}); });
export const fillsWithMarketProvider = makeDerivedDataProvider< export const fillsWithMarketProvider = makeDerivedDataProvider<
(TradeEdge | null)[],
Trade[], Trade[],
never,
FillsQueryVariables FillsQueryVariables
>( >(
[ [
fillsProvider, fillsProvider,
(callback, client) => marketsMapProvider(callback, client, undefined), (callback, client) => marketsProvider(callback, client, undefined),
], ],
(partsData, variables, prevData, parts): Trade[] | null => { (partsData): (TradeEdge | null)[] =>
if (prevData && parts[0].isUpdate) { (partsData[0] as ReturnType<typeof getData>)?.map(
return update( (edge) =>
prevData, edge && {
parts[0].delta as ReturnType<typeof getDelta>, cursor: edge.cursor,
variables, node: {
mapFillUpdateToFillWithMarket(partsData[1] as Record<string, Market>) ...edge.node,
); market: (partsData[1] as Market[]).find(
} (market) => market.id === edge.node.market.id
return ((partsData[0] as ReturnType<typeof getData>) || []).map( ),
(trade) => ({ },
...trade, }
market: (partsData[1] as Record<string, Market>)[trade.market.id], ) || null,
}) combineDelta<Trade, ReturnType<typeof getDelta>['0']>,
); combineInsertionData<Trade>
}
); );
+22 -28
View File
@@ -1,11 +1,10 @@
import compact from 'lodash/compact';
import type { AgGridReact } from 'ag-grid-react'; import type { AgGridReact } from 'ag-grid-react';
import { useRef } from 'react'; import { useRef } from 'react';
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/i18n';
import { FillsTable } from './fills-table'; import { FillsTable } from './fills-table';
import { useFillsList } from './use-fills-list';
import { useBottomPlaceholder } from '@vegaprotocol/datagrid'; import { useBottomPlaceholder } from '@vegaprotocol/datagrid';
import { useDataProvider } from '@vegaprotocol/data-provider';
import type * as Schema from '@vegaprotocol/types';
import { fillsWithMarketProvider } from './fills-data-provider';
interface FillsManagerProps { interface FillsManagerProps {
partyId: string; partyId: string;
@@ -21,36 +20,31 @@ export const FillsManager = ({
storeKey, storeKey,
}: FillsManagerProps) => { }: FillsManagerProps) => {
const gridRef = useRef<AgGridReact | null>(null); const gridRef = useRef<AgGridReact | null>(null);
const filter: Schema.TradesFilter | Schema.TradesSubscriptionFilter = { const scrolledToTop = useRef(true);
partyIds: [partyId], const { data, error } = useFillsList({
}; partyId,
if (marketId) { marketId,
filter.marketIds = [marketId]; gridRef,
} scrolledToTop,
const { data, error } = useDataProvider({
dataProvider: fillsWithMarketProvider,
update: ({ data }) => {
if (data?.length && gridRef.current?.api) {
gridRef.current?.api.setRowData(data);
return true;
}
return false;
},
variables: { filter },
}); });
const bottomPlaceholderProps = useBottomPlaceholder({ const bottomPlaceholderProps = useBottomPlaceholder({
gridRef, gridRef,
}); });
const fills = compact(data).map((e) => e.node);
return ( return (
<FillsTable <div className="h-full relative">
ref={gridRef} <FillsTable
rowData={data} ref={gridRef}
partyId={partyId} rowData={fills}
onMarketClick={onMarketClick} partyId={partyId}
storeKey={storeKey} onMarketClick={onMarketClick}
{...bottomPlaceholderProps} storeKey={storeKey}
overlayNoRowsTemplate={error ? error.message : t('No fills')} {...bottomPlaceholderProps}
/> overlayNoRowsTemplate={error ? error.message : t('No fills')}
/>
</div>
); );
}; };
+95
View File
@@ -0,0 +1,95 @@
import type { AgGridReact } from 'ag-grid-react';
import { MockedProvider } from '@apollo/client/testing';
import { renderHook } from '@testing-library/react';
import { useFillsList } from './use-fills-list';
import type { TradeEdge } from './fills-data-provider';
let mockData = null;
let mockDataProviderData = {
data: mockData as (TradeEdge | null)[] | null,
error: undefined,
loading: true,
};
let updateMock: jest.Mock;
const mockDataProvider = jest.fn((args) => {
updateMock = args.update;
return mockDataProviderData;
});
jest.mock('@vegaprotocol/data-provider', () => ({
...jest.requireActual('@vegaprotocol/data-provider'),
useDataProvider: jest.fn((args) => mockDataProvider(args)),
}));
describe('useFillsList Hook', () => {
const mockRefreshAgGridApi = jest.fn();
const partyId = 'partyId';
const gridRef = {
current: {
api: {
refreshInfiniteCache: mockRefreshAgGridApi,
getModel: () => ({ getType: () => 'infinite' }),
},
} as unknown as AgGridReact,
};
const scrolledToTop = {
current: false,
};
afterEach(() => {
jest.clearAllMocks();
});
it('should return proper dataProvider results', () => {
const { result } = renderHook(
() => useFillsList({ partyId, gridRef, scrolledToTop }),
{
wrapper: MockedProvider,
}
);
expect(result.current).toMatchObject({
data: null,
error: undefined,
loading: true,
addNewRows: expect.any(Function),
getRows: expect.any(Function),
});
});
it('return proper mocked results', () => {
mockData = [
{
node: {
id: 'data_id_1',
},
} as unknown as TradeEdge,
{
node: {
id: 'data_id_2',
},
} as unknown as TradeEdge,
];
mockDataProviderData = {
...mockDataProviderData,
data: mockData,
loading: false,
};
const { result } = renderHook(
() => useFillsList({ partyId, gridRef, scrolledToTop }),
{
wrapper: MockedProvider,
}
);
expect(result.current).toMatchObject({
data: mockData,
error: undefined,
loading: false,
addNewRows: expect.any(Function),
getRows: expect.any(Function),
});
updateMock({ data: mockData });
expect(mockRefreshAgGridApi).not.toHaveBeenCalled();
updateMock({ data: mockData });
expect(mockRefreshAgGridApi).toHaveBeenCalled();
});
});
+123
View File
@@ -0,0 +1,123 @@
import type { RefObject } from 'react';
import type { AgGridReact } from 'ag-grid-react';
import { useCallback, useRef } from 'react';
import { makeInfiniteScrollGetRows } from '@vegaprotocol/data-provider';
import type * as Types from '@vegaprotocol/types';
import { updateGridData } from '@vegaprotocol/datagrid';
import { useDataProvider } from '@vegaprotocol/data-provider';
import type { Trade, TradeEdge } from './fills-data-provider';
import { fillsWithMarketProvider } from './fills-data-provider';
interface Props {
partyId: string;
marketId?: string;
gridRef: RefObject<AgGridReact>;
scrolledToTop: RefObject<boolean>;
}
export const useFillsList = ({
partyId,
marketId,
gridRef,
scrolledToTop,
}: Props) => {
const dataRef = useRef<(TradeEdge | null)[] | null>(null);
const totalCountRef = useRef<number | undefined>(undefined);
const newRows = useRef(0);
const placeholderAdded = useRef(-1);
const makeBottomPlaceholders = useCallback((trade?: Trade) => {
if (!trade) {
if (placeholderAdded.current >= 0) {
dataRef.current?.splice(placeholderAdded.current, 1);
}
placeholderAdded.current = -1;
} else if (placeholderAdded.current === -1) {
dataRef.current?.push({
node: { ...trade, id: `${trade?.id}-1`, isLastPlaceholder: true },
});
placeholderAdded.current = (dataRef.current?.length || 0) - 1;
}
}, []);
const addNewRows = useCallback(() => {
if (newRows.current === 0) {
return;
}
if (totalCountRef.current !== undefined) {
totalCountRef.current += newRows.current;
}
newRows.current = 0;
gridRef.current?.api?.refreshInfiniteCache();
}, [gridRef]);
const update = useCallback(
({
data,
delta,
}: {
data: (TradeEdge | null)[] | null;
delta?: Trade[];
}) => {
if (dataRef.current?.length) {
if (!scrolledToTop.current) {
const createdAt = dataRef.current?.[0]?.node.createdAt;
if (createdAt) {
newRows.current += (delta || []).filter(
(trade) => trade.createdAt > createdAt
).length;
}
}
return updateGridData(dataRef, data, gridRef);
}
dataRef.current = data;
return false;
},
[gridRef, scrolledToTop]
);
const insert = useCallback(
({
data,
totalCount,
}: {
data: (TradeEdge | null)[] | null;
totalCount?: number;
}) => {
totalCountRef.current = totalCount;
return updateGridData(dataRef, data, gridRef);
},
[gridRef]
);
const filter: Types.TradesFilter & Types.TradesSubscriptionFilter = {
partyIds: [partyId],
};
if (marketId) {
filter.marketIds = [marketId];
}
const { data, error, loading, load, totalCount, reload } = useDataProvider({
dataProvider: fillsWithMarketProvider,
update,
insert,
variables: { filter },
});
totalCountRef.current = totalCount;
const getRows = makeInfiniteScrollGetRows<TradeEdge>(
dataRef,
totalCountRef,
load,
newRows
);
return {
data,
error,
loading,
addNewRows,
getRows,
reload,
makeBottomPlaceholders,
};
};
@@ -1,12 +1,22 @@
import type { Asset } from '@vegaprotocol/assets'; import type { Asset } from '@vegaprotocol/assets';
import { assetsMapProvider } from '@vegaprotocol/assets'; import { assetsProvider } from '@vegaprotocol/assets';
import type { Market } from '@vegaprotocol/markets'; import type { Market } from '@vegaprotocol/markets';
import { marketsMapProvider } from '@vegaprotocol/markets'; import { marketsProvider } from '@vegaprotocol/markets';
import { makeInfiniteScrollGetRows } from '@vegaprotocol/data-provider';
import { updateGridData } from '@vegaprotocol/datagrid';
import { import {
makeDataProvider, makeDataProvider,
makeDerivedDataProvider, makeDerivedDataProvider,
useDataProvider,
} from '@vegaprotocol/data-provider'; } from '@vegaprotocol/data-provider';
import type * as Schema from '@vegaprotocol/types';
import type { AgGridReact } from 'ag-grid-react';
import produce from 'immer';
import orderBy from 'lodash/orderBy';
import uniqBy from 'lodash/uniqBy';
import type { RefObject } from 'react';
import { useCallback, useMemo, useRef } from 'react';
import type { Filter } from './ledger-manager';
import type { import type {
LedgerEntriesQuery, LedgerEntriesQuery,
LedgerEntriesQueryVariables, LedgerEntriesQueryVariables,
@@ -20,58 +30,171 @@ export type LedgerEntry = LedgerEntryFragment & {
marketReceiver: Market | null | undefined; marketReceiver: Market | null | undefined;
}; };
type Edge = LedgerEntriesQuery['ledgerEntries']['edges'][number]; export type AggregatedLedgerEntriesEdge = Schema.AggregatedLedgerEntriesEdge;
export type AggregatedLedgerEntriesNode = Omit<
const isLedgerEntryEdge = (entry: Edge): entry is NonNullable<Edge> => AggregatedLedgerEntriesEdge,
entry !== null; 'node'
> & {
const getData = (responseData: LedgerEntriesQuery | null) => { node: LedgerEntry;
return (
responseData?.ledgerEntries?.edges
.filter(isLedgerEntryEdge)
.map((edge) => edge.node) || []
);
}; };
const ledgerEntriesOnlyProvider = makeDataProvider< const getData = (responseData: LedgerEntriesQuery | null) => {
LedgerEntriesQuery, return responseData?.ledgerEntries?.edges || [];
ReturnType<typeof getData>, };
never,
never, export const update = (
LedgerEntriesQueryVariables data: ReturnType<typeof getData> | null,
>({ delta: ReturnType<typeof getData>,
reload: () => void,
variables: LedgerEntriesQueryVariables
) => {
if (!data) {
return data;
}
return produce(data, (draft) => {
// A single update can contain the same order with multiple updates, so we need to find
// the latest version of the order and only update using that
const incoming = uniqBy(
orderBy(delta, (entry) => entry?.node.vegaTime, 'desc'),
'id'
);
// Add or update incoming orders
incoming.reverse().forEach((node) => {
const index = draft.findIndex(
(edge) => edge?.node.vegaTime === node?.node.vegaTime
);
const newer =
draft.length === 0 || node?.node.vegaTime >= draft[0]?.node.vegaTime;
let doesFilterPass = true;
if (
doesFilterPass &&
variables?.dateRange?.start &&
new Date(node?.node.vegaTime) <= new Date(variables?.dateRange?.start)
) {
doesFilterPass = false;
}
if (
doesFilterPass &&
variables?.dateRange?.end &&
new Date(node?.node.vegaTime) >= new Date(variables?.dateRange?.end)
) {
doesFilterPass = false;
}
if (index !== -1) {
if (doesFilterPass) {
// Object.assign(draft[index]?.node, node?.node);
if (newer) {
draft.unshift(...draft.splice(index, 1));
}
} else {
draft.splice(index, 1);
}
} else if (newer && doesFilterPass) {
draft.unshift(node);
}
});
});
};
const ledgerEntriesOnlyProvider = makeDataProvider({
query: LedgerEntriesDocument, query: LedgerEntriesDocument,
getData, getData,
getDelta: getData,
update,
additionalContext: { additionalContext: {
isEnlargedTimeout: true, isEnlargedTimeout: true,
}, },
}); });
export const ledgerEntriesProvider = makeDerivedDataProvider< export const ledgerEntriesProvider = makeDerivedDataProvider<
LedgerEntry[], AggregatedLedgerEntriesNode[],
never, AggregatedLedgerEntriesNode[],
LedgerEntriesQueryVariables LedgerEntriesQueryVariables
>( >(
[ [
ledgerEntriesOnlyProvider, ledgerEntriesOnlyProvider,
(callback, client) => assetsMapProvider(callback, client, undefined), (callback, client) => assetsProvider(callback, client, undefined),
(callback, client) => marketsMapProvider(callback, client, undefined), (callback, client) => marketsProvider(callback, client, undefined),
], ],
(partsData) => { ([entries, assets, markets]) => {
const entries = partsData[0] as ReturnType<typeof getData>; return entries.map((edge: AggregatedLedgerEntriesEdge) => {
const assets = partsData[1] as Record<string, Asset>; const entry = edge.node;
const markets = partsData[1] as Record<string, Market>; const asset = assets.find((asset: Asset) => asset.id === entry.assetId);
return entries.map((entry) => { const marketSender = markets.find(
const asset = entry.assetId (market: Market) => market.id === entry.fromAccountMarketId
? (assets as Record<string, Asset>)[entry.assetId] );
: null; const marketReceiver = markets.find(
const marketSender = entry.fromAccountMarketId (market: Market) => market.id === entry.toAccountMarketId
? markets[entry.fromAccountMarketId] );
: null; const cursor = edge?.cursor;
const marketReceiver = entry.toAccountMarketId return {
? markets[entry.toAccountMarketId] node: { ...entry, asset, marketSender, marketReceiver },
: null; cursor,
return { ...entry, asset, marketSender, marketReceiver }; };
}); });
} }
); );
interface Props {
partyId: string;
filter?: Filter;
gridRef: RefObject<AgGridReact>;
}
export const useLedgerEntriesDataProvider = ({
partyId,
filter,
gridRef,
}: Props) => {
const dataRef = useRef<AggregatedLedgerEntriesEdge[] | null>(null);
const totalCountRef = useRef<number>();
const variables = useMemo<LedgerEntriesQueryVariables>(
() => ({
partyId,
dateRange: filter?.vegaTime?.value,
pagination: {
first: 5000,
},
}),
[partyId, filter?.vegaTime?.value]
);
const update = useCallback(
({ data }: { data: AggregatedLedgerEntriesEdge[] | null }) => {
return updateGridData(dataRef, data, gridRef);
},
[gridRef]
);
const insert = useCallback(
({
data,
totalCount,
}: {
data: AggregatedLedgerEntriesEdge[] | null;
totalCount?: number;
}) => {
totalCountRef.current = totalCount;
return updateGridData(dataRef, data, gridRef);
},
[gridRef]
);
const { data, error, loading, load, totalCount, reload } = useDataProvider({
dataProvider: ledgerEntriesProvider,
update,
insert,
variables,
skip: !variables.partyId,
});
totalCountRef.current = totalCount;
const getRows = makeInfiniteScrollGetRows<AggregatedLedgerEntriesEdge>(
dataRef,
totalCountRef,
load
);
return { loading, error, data, getRows, reload };
};
+13 -21
View File
@@ -2,12 +2,10 @@ import { t } from '@vegaprotocol/i18n';
import type * as Schema from '@vegaprotocol/types'; import type * as Schema from '@vegaprotocol/types';
import type { FilterChangedEvent } from 'ag-grid-community'; import type { FilterChangedEvent } from 'ag-grid-community';
import type { AgGridReact } from 'ag-grid-react'; import type { AgGridReact } from 'ag-grid-react';
import { useCallback, useRef, useState, useMemo } from 'react'; import { useCallback, useRef, useState } from 'react';
import { subDays, formatRFC3339 } from 'date-fns'; import { subDays, formatRFC3339 } from 'date-fns';
import { ledgerEntriesProvider } from './ledger-entries-data-provider'; import { useLedgerEntriesDataProvider } from './ledger-entries-data-provider';
import type { LedgerEntriesQueryVariables } from './__generated__/LedgerEntries';
import { LedgerTable } from './ledger-table'; import { LedgerTable } from './ledger-table';
import { useDataProvider } from '@vegaprotocol/data-provider';
import type * as Types from '@vegaprotocol/types'; import type * as Types from '@vegaprotocol/types';
import { LedgerExportLink } from './ledger-export-link'; import { LedgerExportLink } from './ledger-export-link';
@@ -28,21 +26,10 @@ export const LedgerManager = ({ partyId }: { partyId: string }) => {
const gridRef = useRef<AgGridReact | null>(null); const gridRef = useRef<AgGridReact | null>(null);
const [filter, setFilter] = useState<Filter>(defaultFilter); const [filter, setFilter] = useState<Filter>(defaultFilter);
const variables = useMemo<LedgerEntriesQueryVariables>( const { data, error } = useLedgerEntriesDataProvider({
() => ({ partyId,
partyId, filter,
dateRange: filter?.vegaTime?.value, gridRef,
pagination: {
first: 5000,
},
}),
[partyId, filter?.vegaTime?.value]
);
const { data, error } = useDataProvider({
dataProvider: ledgerEntriesProvider,
variables,
skip: !variables.partyId,
}); });
const onFilterChanged = useCallback((event: FilterChangedEvent) => { const onFilterChanged = useCallback((event: FilterChangedEvent) => {
@@ -50,15 +37,20 @@ export const LedgerManager = ({ partyId }: { partyId: string }) => {
setFilter(updatedFilter); setFilter(updatedFilter);
}, []); }, []);
// allow passing undefined to grid so that loading state is shown
const extractedData = data?.map((item) => item.node);
return ( return (
<div className="h-full relative"> <div className="h-full relative">
<LedgerTable <LedgerTable
ref={gridRef} ref={gridRef}
rowData={data} rowData={extractedData}
onFilterChanged={onFilterChanged} onFilterChanged={onFilterChanged}
overlayNoRowsTemplate={error ? error.message : t('No entries')} overlayNoRowsTemplate={error ? error.message : t('No entries')}
/> />
{data && <LedgerExportLink entries={data} partyId={partyId} />} {extractedData && (
<LedgerExportLink entries={extractedData} partyId={partyId} />
)}
</div> </div>
); );
}; };
+6
View File
@@ -0,0 +1,6 @@
function ReactMarkdown({ children }) {
// eslint-disable-next-line react/jsx-no-useless-fragment
return <>{children}</>;
}
export default ReactMarkdown;
@@ -1,14 +1,29 @@
import { forwardRef } from 'react'; import { forwardRef } from 'react';
import type { TypedDataAgGrid } from '@vegaprotocol/datagrid'; import { addDecimalsFormatNumber, toBigNum } from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import type {
VegaValueGetterParams,
VegaValueFormatterParams,
VegaICellRendererParams,
TypedDataAgGrid,
} from '@vegaprotocol/datagrid';
import { COL_DEFS } from '@vegaprotocol/datagrid';
import { import {
AgGridLazy as AgGrid, AgGridLazy as AgGrid,
PriceFlashCell, PriceFlashCell,
MarketNameCell, MarketNameCell,
SetFilter,
} from '@vegaprotocol/datagrid'; } from '@vegaprotocol/datagrid';
import { ButtonLink } from '@vegaprotocol/ui-toolkit';
import { AgGridColumn } from 'ag-grid-react';
import type { AgGridReact } from 'ag-grid-react'; import type { AgGridReact } from 'ag-grid-react';
import * as Schema from '@vegaprotocol/types';
import type { MarketMaybeWithData } from '../../markets-provider'; import type { MarketMaybeWithData } from '../../markets-provider';
import { MarketTableActions } from './market-table-actions';
import { OracleStatus } from './oracle-status'; import { OracleStatus } from './oracle-status';
import { useColumnDefs } from './use-column-defs'; import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
const { MarketTradingMode, AuctionTrigger } = Schema;
export const getRowId = ({ data }: { data: { id: string } }) => data.id; export const getRowId = ({ data }: { data: { id: string } }) => data.id;
@@ -36,34 +51,199 @@ const MarketName = (props: MarketNameCellProps) => (
</> </>
); );
const defaultColDef = {
resizable: true,
sortable: true,
filter: true,
filterParams: { buttons: ['reset'] },
minWidth: 100,
};
export const MarketListTable = forwardRef< export const MarketListTable = forwardRef<
AgGridReact, AgGridReact,
TypedDataAgGrid<MarketMaybeWithData> & { TypedDataAgGrid<MarketMaybeWithData> & {
onMarketClick: (marketId: string, metaKey?: boolean) => void; onMarketClick: (marketId: string, metaKey?: boolean) => void;
} }
>(({ onMarketClick, ...props }, ref) => { >(({ onMarketClick, ...props }, ref) => {
const columnDefs = useColumnDefs({ onMarketClick }); const { open: openAssetDetailsDialog } = useAssetDetailsDialogStore();
return ( return (
<AgGrid <AgGrid
style={{ width: '100%', height: '100%' }} style={{ width: '100%', height: '100%' }}
getRowId={getRowId} getRowId={getRowId}
ref={ref} ref={ref}
defaultColDef={defaultColDef} defaultColDef={{
columnDefs={columnDefs} resizable: true,
sortable: true,
filter: true,
filterParams: { buttons: ['reset'] },
minWidth: 100,
}}
suppressCellFocus suppressCellFocus
components={{ PriceFlashCell, MarketName }} components={{ PriceFlashCell, MarketName }}
storeKey="allMarkets" storeKey="allMarkets"
{...props} {...props}
/> >
<AgGridColumn
headerName={t('Market')}
field="tradableInstrument.instrument.code"
cellRenderer="MarketName"
cellRendererParams={{ onMarketClick }}
/>
<AgGridColumn
headerName={t('Description')}
field="tradableInstrument.instrument.name"
/>
<AgGridColumn
headerName={t('Trading mode')}
field="tradingMode"
minWidth={170}
valueFormatter={({
data,
}: VegaValueFormatterParams<MarketMaybeWithData, 'data'>) => {
if (!data?.data) return undefined;
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,
}}
/>
<AgGridColumn
headerName={t('Status')}
field="state"
valueFormatter={({
data,
}: VegaValueFormatterParams<MarketMaybeWithData, 'state'>) => {
return data?.state ? Schema.MarketStateMapping[data.state] : '-';
}}
filter={SetFilter}
filterParams={{
set: Schema.MarketStateMapping,
}}
/>
<AgGridColumn
headerName={t('Best bid')}
field="data.bestBidPrice"
type="rightAligned"
cellRenderer="PriceFlashCell"
filter="agNumberColumnFilter"
valueGetter={({
data,
}: VegaValueGetterParams<MarketMaybeWithData, 'data.bestBidPrice'>) => {
return data?.data?.bestBidPrice === undefined
? undefined
: toBigNum(data?.data?.bestBidPrice, data.decimalPlaces).toNumber();
}}
valueFormatter={({
data,
}: VegaValueFormatterParams<
MarketMaybeWithData,
'data.bestBidPrice'
>) =>
data?.data?.bestBidPrice === undefined
? undefined
: addDecimalsFormatNumber(
data.data.bestBidPrice,
data.decimalPlaces
)
}
/>
<AgGridColumn
headerName={t('Best offer')}
field="data.bestOfferPrice"
type="rightAligned"
cellRenderer="PriceFlashCell"
filter="agNumberColumnFilter"
valueGetter={({
data,
}: VegaValueGetterParams<
MarketMaybeWithData,
'data.bestOfferPrice'
>) => {
return data?.data?.bestOfferPrice === undefined
? undefined
: toBigNum(
data?.data?.bestOfferPrice,
data.decimalPlaces
).toNumber();
}}
valueFormatter={({
data,
}: VegaValueFormatterParams<
MarketMaybeWithData,
'data.bestOfferPrice'
>) =>
data?.data?.bestOfferPrice === undefined
? undefined
: addDecimalsFormatNumber(
data.data.bestOfferPrice,
data.decimalPlaces
)
}
/>
<AgGridColumn
headerName={t('Mark price')}
field="data.markPrice"
type="rightAligned"
cellRenderer="PriceFlashCell"
filter="agNumberColumnFilter"
valueGetter={({
data,
}: VegaValueGetterParams<MarketMaybeWithData, 'data.markPrice'>) => {
return data?.data?.markPrice === undefined
? undefined
: toBigNum(data?.data?.markPrice, data.decimalPlaces).toNumber();
}}
valueFormatter={({
data,
}: VegaValueFormatterParams<MarketMaybeWithData, 'data.markPrice'>) =>
data?.data?.bestOfferPrice === undefined
? undefined
: addDecimalsFormatNumber(data.data.markPrice, data.decimalPlaces)
}
/>
<AgGridColumn
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>
) : (
''
);
}}
/>
<AgGridColumn
colId="market-actions"
field="id"
{...COL_DEFS.actions}
cellRenderer={({
data,
}: VegaICellRendererParams<MarketMaybeWithData>) => {
if (!data) return null;
return (
<MarketTableActions
marketId={data.id}
assetId={
data.tradableInstrument.instrument.product.settlementAsset.id
}
/>
);
}}
/>
</AgGrid>
); );
}); });
@@ -1,107 +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';
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', () => {
it('context menu should stay open', async () => {
const spyOnSelect = jest.fn();
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],
};
});
let rerenderRef: (ui: React.ReactElement) => void;
await act(async () => {
const { rerender } = render(
<MockedProvider>
<MarketsContainer onSelect={spyOnSelect} />
</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-8',
})
);
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();
}
});
});
@@ -1,196 +0,0 @@
import { useMemo } from 'react';
import type { ColDef } 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 } from '@vegaprotocol/ui-toolkit';
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
import type { MarketMaybeWithData } from '../../markets-provider';
import { MarketTableActions } 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[]>(
() => [
{
headerName: t('Market'),
field: 'tradableInstrument.instrument.code',
cellRenderer: 'MarketName',
cellRendererParams: { onMarketClick },
},
{
headerName: t('Description'),
field: 'tradableInstrument.instrument.name',
},
{
headerName: t('Trading mode'),
field: 'tradingMode',
minWidth: 170,
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,
},
},
{
headerName: t('Best bid'),
field: 'data.bestBidPrice',
type: 'rightAligned',
cellRenderer: 'PriceFlashCell',
filter: 'agNumberColumnFilter',
valueGetter: ({
data,
}: VegaValueGetterParams<MarketMaybeWithData, 'data.bestBidPrice'>) => {
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,
'data.bestOfferPrice'
>) => {
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, 'data.markPrice'>) => {
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 (
<MarketTableActions
marketId={data.id}
assetId={
data.tradableInstrument.instrument.product.settlementAsset.id
}
/>
);
},
},
],
[onMarketClick, openAssetDetailsDialog]
);
};
@@ -1,23 +1,23 @@
import { import { update } from './order-data-provider';
update,
mapOrderUpdateToOrder,
filterOrderUpdates,
} from './order-data-provider';
import type { OrderUpdateFieldsFragment, OrderFieldsFragment } from '../'; import type { OrderUpdateFieldsFragment, OrderFieldsFragment } from '../';
import type { Edge } from '@vegaprotocol/data-provider';
describe('order data provider', () => { describe('order data provider', () => {
it('puts incoming data in proper place', () => { it('puts incoming data in proper place', () => {
const data = [ const data = [
{ {
id: '2', node: {
createdAt: new Date('2022-01-29').toISOString(), id: '2',
createdAt: new Date('2022-01-29').toISOString(),
},
}, },
{ {
id: '1', node: {
createdAt: new Date('2022-01-28').toISOString(), id: '1',
createdAt: new Date('2022-01-28').toISOString(),
},
}, },
] as OrderFieldsFragment[]; ] as Edge<OrderFieldsFragment>[];
const delta = [ const delta = [
// this one should be dropped because id don't exits and it's older than newest // this one should be dropped because id don't exits and it's older than newest
@@ -52,41 +52,39 @@ describe('order data provider', () => {
createdAt: new Date('2022-02-05').toISOString(), createdAt: new Date('2022-02-05').toISOString(),
}, },
] as OrderUpdateFieldsFragment[]; ] as OrderUpdateFieldsFragment[];
const updatedData = update( const updatedData = update(data, delta, () => null, { partyId: '0x123' });
data,
filterOrderUpdates(delta),
{ partyId: '0x123' },
mapOrderUpdateToOrder
);
expect(updatedData?.findIndex((node) => node.id === delta[0].id)).toEqual(
-1
);
expect(updatedData && updatedData[3].id).toEqual(delta[2].id);
expect(updatedData && updatedData[3].updatedAt).toEqual(delta[2].updatedAt);
expect(updatedData && updatedData[0].id).toEqual(delta[5].id);
expect(updatedData && updatedData[1].id).toEqual(delta[3].id);
expect(updatedData && updatedData[2].id).toEqual(delta[4].id);
expect(updatedData && updatedData[2].updatedAt).toEqual(delta[4].updatedAt);
expect( expect(
update( updatedData?.findIndex((edge) => edge.node.id === delta[0].id)
[], ).toEqual(-1);
filterOrderUpdates(delta), expect(updatedData && updatedData[3].node.id).toEqual(delta[2].id);
{ partyId: '0x123' }, expect(updatedData && updatedData[3].node.updatedAt).toEqual(
mapOrderUpdateToOrder delta[2].updatedAt
)?.length );
).toEqual(5); expect(updatedData && updatedData[0].node.id).toEqual(delta[5].id);
expect(updatedData && updatedData[1].node.id).toEqual(delta[3].id);
expect(updatedData && updatedData[2].node.id).toEqual(delta[4].id);
expect(updatedData && updatedData[2].node.updatedAt).toEqual(
delta[4].updatedAt
);
expect(update([], delta, () => null, { partyId: '0x123' })?.length).toEqual(
5
);
}); });
it('add only data matching date range filter', () => { it('add only data matching date range filter', () => {
const data = [ const data = [
{ {
id: '1', node: {
createdAt: new Date('2022-01-29').toISOString(), id: '1',
createdAt: new Date('2022-01-29').toISOString(),
},
}, },
{ {
id: '2', node: {
createdAt: new Date('2022-01-30').toISOString(), id: '2',
createdAt: new Date('2022-01-30').toISOString(),
},
}, },
] as OrderFieldsFragment[]; ] as Edge<OrderFieldsFragment>[];
const delta = [ const delta = [
// this one should be ignored because it does not match date range // this one should be ignored because it does not match date range
@@ -107,23 +105,22 @@ describe('order data provider', () => {
}, },
] as OrderUpdateFieldsFragment[]; ] as OrderUpdateFieldsFragment[];
const updatedData = update( const updatedData = update(data, delta, () => null, {
data, partyId: '0x123',
filterOrderUpdates(delta), filter: {
{ dateRange: { end: new Date('2022-02-01').toISOString() },
partyId: '0x123',
filter: {
dateRange: { end: new Date('2022-02-01').toISOString() },
},
}, },
mapOrderUpdateToOrder });
expect(
updatedData?.findIndex((edge) => edge.node.id === delta[0].id)
).toEqual(-1);
expect(updatedData && updatedData[0].node.id).toEqual(delta[2].id);
expect(updatedData && updatedData[0].node.updatedAt).toEqual(
delta[2].updatedAt
); );
expect(updatedData?.findIndex((node) => node.id === delta[0].id)).toEqual( expect(updatedData && updatedData[2].node.id).toEqual(delta[1].id);
-1 expect(updatedData && updatedData[2].node.updatedAt).toEqual(
delta[1].updatedAt
); );
expect(updatedData && updatedData[0].id).toEqual(delta[2].id);
expect(updatedData && updatedData[0].updatedAt).toEqual(delta[2].updatedAt);
expect(updatedData && updatedData[2].id).toEqual(delta[1].id);
expect(updatedData && updatedData[2].updatedAt).toEqual(delta[1].updatedAt);
}); });
}); });
@@ -6,8 +6,8 @@ import {
defaultAppend as append, defaultAppend as append,
} from '@vegaprotocol/data-provider'; } from '@vegaprotocol/data-provider';
import type { Market } from '@vegaprotocol/markets'; import type { Market } from '@vegaprotocol/markets';
import { marketsMapProvider } from '@vegaprotocol/markets'; import { marketsProvider } from '@vegaprotocol/markets';
import type { PageInfo, Edge, Cursor } from '@vegaprotocol/data-provider'; import type { PageInfo, Edge } from '@vegaprotocol/data-provider';
import { OrderStatus } from '@vegaprotocol/types'; import { OrderStatus } from '@vegaprotocol/types';
import type { import type {
OrderFieldsFragment, OrderFieldsFragment,
@@ -15,7 +15,6 @@ import type {
OrdersQuery, OrdersQuery,
OrdersUpdateSubscription, OrdersUpdateSubscription,
OrdersQueryVariables, OrdersQueryVariables,
OrdersUpdateSubscriptionVariables,
} from './__generated__/Orders'; } from './__generated__/Orders';
import { OrdersDocument, OrdersUpdateDocument } from './__generated__/Orders'; import { OrdersDocument, OrdersUpdateDocument } from './__generated__/Orders';
import type { ApolloClient } from '@apollo/client'; import type { ApolloClient } from '@apollo/client';
@@ -38,21 +37,18 @@ const orderMatchFilters = (
if (!order) { if (!order) {
return true; return true;
} }
if ( if (
variables?.filter?.status && variables?.filter?.status &&
!(order.status && variables.filter.status.includes(order.status)) !(order.status && variables.filter.status.includes(order.status))
) { ) {
return false; return false;
} }
if ( if (
variables?.filter?.liveOnly && variables?.filter?.liveOnly &&
!(order.status && liveOnlyOrderStatuses.includes(order.status)) !(order.status && liveOnlyOrderStatuses.includes(order.status))
) { ) {
return false; return false;
} }
if ( if (
variables?.filter?.types && variables?.filter?.types &&
!(order.type && variables.filter.types.includes(order.type)) !(order.type && variables.filter.types.includes(order.type))
@@ -80,11 +76,10 @@ const orderMatchFilters = (
) { ) {
return false; return false;
} }
return true; return true;
}; };
export const mapOrderUpdateToOrder = ( const mapOrderUpdateToOrder = (
orderUpdate: OrderUpdateFieldsFragment orderUpdate: OrderUpdateFieldsFragment
): OrderFieldsFragment => { ): OrderFieldsFragment => {
const { marketId, liquidityProvisionId, ...order } = orderUpdate; const { marketId, liquidityProvisionId, ...order } = orderUpdate;
@@ -106,36 +101,10 @@ export const mapOrderUpdateToOrder = (
}; };
}; };
const mapOrderUpdateToOrderWithMarket =
(markets: Record<string, Market>) =>
(orderUpdate: OrderUpdateFieldsFragment): Order => {
const { market, ...order } = mapOrderUpdateToOrder(orderUpdate);
return {
...order,
market: markets[market.id],
};
};
const getData = ( const getData = (
responseData: OrdersQuery | null responseData: OrdersQuery | null
): (OrderFieldsFragment & Cursor)[] => ): Edge<OrderFieldsFragment>[] =>
responseData?.party?.ordersConnection?.edges?.map< responseData?.party?.ordersConnection?.edges || [];
OrderFieldsFragment & Cursor
>((edge) => ({ ...edge.node, cursor: edge.cursor })) || [];
export const filterOrderUpdates = (
orders: OrdersUpdateSubscription['orders']
) => {
// A single update can contain the same order with multiple updates, so we need to find
// the latest version of the order and only update using that
return orderBy(
uniqBy(
orderBy(orders, (order) => order.updatedAt || order.createdAt, 'desc'),
'id'
),
'createdAt'
);
};
const getDelta = ( const getDelta = (
subscriptionData: OrdersUpdateSubscription, subscriptionData: OrdersUpdateSubscription,
@@ -145,32 +114,49 @@ const getDelta = (
if (!subscriptionData.orders) { if (!subscriptionData.orders) {
return []; return [];
} }
return filterOrderUpdates(subscriptionData.orders); return subscriptionData.orders;
}; };
export const update = <T extends Omit<OrderFieldsFragment, 'market'> & Cursor>( export const update = (
data: T[] | null, data: ReturnType<typeof getData> | null,
delta: ReturnType<typeof getDelta>, delta: ReturnType<typeof getDelta>,
variables: OrdersQueryVariables, reload: () => void,
mapDeltaToData: (delta: OrderUpdateFieldsFragment) => T variables?: OrdersQueryVariables
): T[] => { ) => {
const updatedData = data ? [...data] : ([] as T[]); if (!data) {
delta.forEach((orderUpdate) => { return data;
const index = data?.findIndex((order) => order.id === orderUpdate.id) ?? -1; }
const newer = !data?.length || orderUpdate.createdAt >= data[0].createdAt; // A single update can contain the same order with multiple updates, so we need to find
// the latest version of the order and only update using that
const incoming = orderBy(
uniqBy(
orderBy(delta, (order) => order.updatedAt || order.createdAt, 'desc'),
'id'
),
'createdAt'
);
const updatedData = [...data];
incoming.forEach((orderUpdate) => {
const index = data.findIndex((edge) => edge.node.id === orderUpdate.id);
const newer =
data.length === 0 || orderUpdate.createdAt >= data[0].node.createdAt;
const doesFilterPass = const doesFilterPass =
!variables || orderMatchFilters(orderUpdate, variables); !variables || orderMatchFilters(orderUpdate, variables);
if (index !== -1) { if (index !== -1) {
if (doesFilterPass) { if (doesFilterPass) {
updatedData[index] = { updatedData[index] = {
...updatedData[index], ...updatedData[index],
...mapDeltaToData(orderUpdate), node: mapOrderUpdateToOrder(orderUpdate),
}; };
} else { } else {
updatedData.splice(index, 1); updatedData.splice(index, 1);
} }
} else if (newer && doesFilterPass) { } else if (newer && doesFilterPass) {
updatedData.unshift(mapDeltaToData(orderUpdate)); updatedData.unshift({
node: mapOrderUpdateToOrder(orderUpdate),
cursor: '',
});
} }
}); });
return updatedData; return updatedData;
@@ -184,13 +170,11 @@ export const ordersProvider = makeDataProvider<
ReturnType<typeof getData>, ReturnType<typeof getData>,
OrdersUpdateSubscription, OrdersUpdateSubscription,
ReturnType<typeof getDelta>, ReturnType<typeof getDelta>,
OrdersQueryVariables, OrdersQueryVariables
OrdersUpdateSubscriptionVariables
>({ >({
query: OrdersDocument, query: OrdersDocument,
subscriptionQuery: OrdersUpdateDocument, subscriptionQuery: OrdersUpdateDocument,
update: (data, delta, reload, variables) => update,
update(data, delta, variables, mapOrderUpdateToOrder),
getData, getData,
getDelta, getDelta,
pagination: { pagination: {
@@ -201,10 +185,6 @@ export const ordersProvider = makeDataProvider<
resetDelay: 1000, resetDelay: 1000,
additionalContext: { isEnlargedTimeout: true }, additionalContext: { isEnlargedTimeout: true },
fetchPolicy: 'no-cache', fetchPolicy: 'no-cache',
getSubscriptionVariables: ({ partyId, marketIds }) => ({
partyId,
marketIds,
}),
}); });
export const activeOrdersProvider = makeDerivedDataProvider< export const activeOrdersProvider = makeDerivedDataProvider<
@@ -228,36 +208,27 @@ export const activeOrdersProvider = makeDerivedDataProvider<
} }
const orders = partsData[0] as ReturnType<typeof getData>; const orders = partsData[0] as ReturnType<typeof getData>;
return variables.marketId return variables.marketId
? orders.filter((order) => variables.marketId === order.market.id) ? orders.filter((edge) => variables.marketId === edge.node.market.id)
: orders; : orders;
} }
); );
export const ordersWithMarketProvider = makeDerivedDataProvider< export const ordersWithMarketProvider = makeDerivedDataProvider<
(Order & Cursor)[], (Order | null)[],
never, Order[],
OrdersQueryVariables OrdersQueryVariables
>( >(
[ [
ordersProvider, ordersProvider,
(callback, client) => marketsMapProvider(callback, client, undefined), (callback, client) => marketsProvider(callback, client, undefined),
], ],
(partsData, variables, prevData, parts): Order[] => { (partsData): Order[] =>
if (prevData && parts[0].isUpdate) { ((partsData[0] as ReturnType<typeof getData>) || []).map((edge) => ({
return update( ...edge.node,
prevData, market: (partsData[1] as Market[]).find(
parts[0].delta, (market) => market.id === edge.node.market.id
variables, ),
mapOrderUpdateToOrderWithMarket(partsData[1] as Record<string, Market>) }))
);
}
return ((partsData[0] as ReturnType<typeof getData>) || []).map(
(order) => ({
...order,
market: (partsData[1] as Record<string, Market>)[order.market.id],
})
);
}
); );
export const hasActiveOrderProvider = makeDerivedDataProvider< export const hasActiveOrderProvider = makeDerivedDataProvider<
@@ -273,7 +244,7 @@ export const hasAmendableOrderProvider = makeDerivedDataProvider<
>([activeOrdersProvider], (parts) => { >([activeOrdersProvider], (parts) => {
const activeOrders = parts[0] as ReturnType<typeof getData>; const activeOrders = parts[0] as ReturnType<typeof getData>;
const hasAmendableOrder = activeOrders.some( const hasAmendableOrder = activeOrders.some(
(order) => !(order.liquidityProvision || order.peggedOrder) (edge) => !(edge.node.liquidityProvision || edge.node.peggedOrder)
); );
return hasAmendableOrder; return hasAmendableOrder;
}); });
@@ -32,6 +32,7 @@ describe('OrderListManager', () => {
flush: jest.fn(), flush: jest.fn(),
reload: jest.fn(), reload: jest.fn(),
load: jest.fn(), load: jest.fn(),
totalCount: undefined,
}); });
await act(async () => { await act(async () => {
render(generateJsx()); render(generateJsx());
@@ -87,7 +87,7 @@ export const OrderListManager = ({
gridRef.current.api.setRowData(data); gridRef.current.api.setRowData(data);
return true; return true;
} }
return false; return true;
}, },
}); });
+2 -20
View File
@@ -47,7 +47,7 @@ const positionFields: PositionFieldsFragment[] = [
{ {
__typename: 'Position', __typename: 'Position',
realisedPNL: '230000', realisedPNL: '230000',
openVolume: '-6', openVolume: '6',
unrealisedPNL: '895000', unrealisedPNL: '895000',
averageEntryPrice: '1129935', averageEntryPrice: '1129935',
updatedAt: '2022-07-28T15:09:34.441143Z', updatedAt: '2022-07-28T15:09:34.441143Z',
@@ -82,7 +82,7 @@ const positionFields: PositionFieldsFragment[] = [
}, },
{ {
__typename: 'Position', __typename: 'Position',
realisedPNL: '-230000', realisedPNL: '230000',
openVolume: '1', openVolume: '1',
unrealisedPNL: '-22519', unrealisedPNL: '-22519',
averageEntryPrice: '84400088', averageEntryPrice: '84400088',
@@ -98,24 +98,6 @@ const positionFields: PositionFieldsFragment[] = [
lossSocializationAmount: '0', lossSocializationAmount: '0',
positionStatus: PositionStatus.POSITION_STATUS_UNSPECIFIED, positionStatus: PositionStatus.POSITION_STATUS_UNSPECIFIED,
}, },
{
__typename: 'Position',
realisedPNL: '-303295252',
openVolume: '0',
unrealisedPNL: '0',
averageEntryPrice: '6126312',
updatedAt: '2022-07-28T14:53:54.725477Z',
positionStatus: PositionStatus.POSITION_STATUS_CLOSED_OUT,
lossSocializationAmount: '261',
market: {
id: 'market-3',
__typename: 'Market',
},
party: {
id: '02eceaba4df2bef76ea10caf728d8a099a2aa846cced25737cccaa9812342f65',
__typename: 'Party',
},
},
]; ];
const marginsFields: MarginFieldsFragment[] = [ const marginsFields: MarginFieldsFragment[] = [
+21 -22
View File
@@ -10,33 +10,32 @@ fragment TradeFields on Trade {
} }
query Trades($marketId: ID!, $pagination: Pagination) { query Trades($marketId: ID!, $pagination: Pagination) {
trades(filter: { marketIds: [$marketId] }, pagination: $pagination) { market(id: $marketId) {
edges { id
node { tradesConnection(pagination: $pagination) {
...TradeFields edges {
node {
...TradeFields
}
cursor
}
pageInfo {
startCursor
endCursor
hasNextPage
hasPreviousPage
} }
cursor
}
pageInfo {
startCursor
endCursor
hasNextPage
hasPreviousPage
} }
} }
} }
fragment TradeUpdateFields on TradeUpdate {
id
price
size
createdAt
marketId
aggressor
}
subscription TradesUpdate($marketId: ID!) { subscription TradesUpdate($marketId: ID!) {
tradesStream(filter: { marketIds: [$marketId] }) { trades(marketId: $marketId) {
...TradeUpdateFields id
price
size
createdAt
marketId
aggressor
} }
} }
+24 -28
View File
@@ -11,16 +11,14 @@ export type TradesQueryVariables = Types.Exact<{
}>; }>;
export type TradesQuery = { __typename?: 'Query', trades?: { __typename?: 'TradeConnection', edges: Array<{ __typename?: 'TradeEdge', cursor: string, node: { __typename?: 'Trade', id: string, price: string, size: string, createdAt: any, aggressor: Types.Side, market: { __typename?: 'Market', id: string } } }>, pageInfo: { __typename?: 'PageInfo', startCursor: string, endCursor: string, hasNextPage: boolean, hasPreviousPage: boolean } } | null }; export type TradesQuery = { __typename?: 'Query', market?: { __typename?: 'Market', id: string, tradesConnection?: { __typename?: 'TradeConnection', edges: Array<{ __typename?: 'TradeEdge', cursor: string, node: { __typename?: 'Trade', id: string, price: string, size: string, createdAt: any, aggressor: Types.Side, market: { __typename?: 'Market', id: string } } }>, pageInfo: { __typename?: 'PageInfo', startCursor: string, endCursor: string, hasNextPage: boolean, hasPreviousPage: boolean } } | null } | null };
export type TradeUpdateFieldsFragment = { __typename?: 'TradeUpdate', id: string, price: string, size: string, createdAt: any, marketId: string, aggressor: Types.Side };
export type TradesUpdateSubscriptionVariables = Types.Exact<{ export type TradesUpdateSubscriptionVariables = Types.Exact<{
marketId: Types.Scalars['ID']; marketId: Types.Scalars['ID'];
}>; }>;
export type TradesUpdateSubscription = { __typename?: 'Subscription', tradesStream?: Array<{ __typename?: 'TradeUpdate', id: string, price: string, size: string, createdAt: any, marketId: string, aggressor: Types.Side }> | null }; export type TradesUpdateSubscription = { __typename?: 'Subscription', trades?: Array<{ __typename?: 'TradeUpdate', id: string, price: string, size: string, createdAt: any, marketId: string, aggressor: Types.Side }> | null };
export const TradeFieldsFragmentDoc = gql` export const TradeFieldsFragmentDoc = gql`
fragment TradeFields on Trade { fragment TradeFields on Trade {
@@ -34,30 +32,23 @@ export const TradeFieldsFragmentDoc = gql`
} }
} }
`; `;
export const TradeUpdateFieldsFragmentDoc = gql`
fragment TradeUpdateFields on TradeUpdate {
id
price
size
createdAt
marketId
aggressor
}
`;
export const TradesDocument = gql` export const TradesDocument = gql`
query Trades($marketId: ID!, $pagination: Pagination) { query Trades($marketId: ID!, $pagination: Pagination) {
trades(filter: {marketIds: [$marketId]}, pagination: $pagination) { market(id: $marketId) {
edges { id
node { tradesConnection(pagination: $pagination) {
...TradeFields edges {
node {
...TradeFields
}
cursor
}
pageInfo {
startCursor
endCursor
hasNextPage
hasPreviousPage
} }
cursor
}
pageInfo {
startCursor
endCursor
hasNextPage
hasPreviousPage
} }
} }
} }
@@ -93,11 +84,16 @@ export type TradesLazyQueryHookResult = ReturnType<typeof useTradesLazyQuery>;
export type TradesQueryResult = Apollo.QueryResult<TradesQuery, TradesQueryVariables>; export type TradesQueryResult = Apollo.QueryResult<TradesQuery, TradesQueryVariables>;
export const TradesUpdateDocument = gql` export const TradesUpdateDocument = gql`
subscription TradesUpdate($marketId: ID!) { subscription TradesUpdate($marketId: ID!) {
tradesStream(filter: {marketIds: [$marketId]}) { trades(marketId: $marketId) {
...TradeUpdateFields id
price
size
createdAt
marketId
aggressor
} }
} }
${TradeUpdateFieldsFragmentDoc}`; `;
/** /**
* __useTradesUpdateSubscription__ * __useTradesUpdateSubscription__
+3 -1
View File
@@ -1,3 +1,4 @@
import compact from 'lodash/compact';
import { useDataProvider } from '@vegaprotocol/data-provider'; import { useDataProvider } from '@vegaprotocol/data-provider';
import type { AgGridReact } from 'ag-grid-react'; import type { AgGridReact } from 'ag-grid-react';
import { useRef } from 'react'; import { useRef } from 'react';
@@ -18,11 +19,12 @@ export const TradesContainer = ({ marketId }: TradesContainerProps) => {
dataProvider: tradesWithMarketProvider, dataProvider: tradesWithMarketProvider,
variables: { marketId }, variables: { marketId },
}); });
const trades = compact(data).map((d) => d.node);
return ( return (
<TradesTable <TradesTable
ref={gridRef} ref={gridRef}
rowData={data} rowData={trades}
onClick={(price?: string) => { onClick={(price?: string) => {
if (price) { if (price) {
updateOrder(marketId, { price }); updateOrder(marketId, { price });
+57 -75
View File
@@ -3,95 +3,75 @@ import {
makeDerivedDataProvider, makeDerivedDataProvider,
defaultAppend as append, defaultAppend as append,
} from '@vegaprotocol/data-provider'; } from '@vegaprotocol/data-provider';
import type { PageInfo, Cursor } from '@vegaprotocol/data-provider'; import type { PageInfo, Edge } from '@vegaprotocol/data-provider';
import type { Market } from '@vegaprotocol/markets'; import type { Market } from '@vegaprotocol/markets';
import { marketsMapProvider } from '@vegaprotocol/markets'; import { marketsProvider } from '@vegaprotocol/markets';
import type { import type {
TradesQuery, TradesQuery,
TradesQueryVariables, TradesQueryVariables,
TradeFieldsFragment, TradeFieldsFragment,
TradesUpdateSubscription, TradesUpdateSubscription,
TradeUpdateFieldsFragment,
TradesUpdateSubscriptionVariables,
} from './__generated__/Trades'; } from './__generated__/Trades';
import { TradesDocument, TradesUpdateDocument } from './__generated__/Trades'; import { TradesDocument, TradesUpdateDocument } from './__generated__/Trades';
import orderBy from 'lodash/orderBy'; import orderBy from 'lodash/orderBy';
import produce from 'immer';
export const MAX_TRADES = 500; export const MAX_TRADES = 500;
const getData = ( const getData = (
responseData: TradesQuery | null responseData: TradesQuery | null
): (TradeFieldsFragment & Cursor)[] => ): ({
responseData?.trades?.edges.map<TradeFieldsFragment & Cursor>((edge) => ({ cursor: string;
...edge.node, node: TradeFieldsFragment;
cursor: edge.cursor, } | null)[] => responseData?.market?.tradesConnection?.edges || [];
})) || [];
const getDelta = (subscriptionData: TradesUpdateSubscription) => const getDelta = (subscriptionData: TradesUpdateSubscription) =>
subscriptionData?.tradesStream || []; subscriptionData?.trades || [];
const mapTradeUpdateToTrade = ( const update = (
tradeUpdate: TradeUpdateFieldsFragment data: ReturnType<typeof getData> | null,
): TradeFieldsFragment => { delta: ReturnType<typeof getDelta>
const { marketId, ...trade } = tradeUpdate; ) => {
return { if (!data) return data;
...trade, return produce(data, (draft) => {
__typename: 'Trade', // for each incoming trade add it to the beginning and remove oldest trade
market: { orderBy(delta, 'createdAt', 'desc').forEach((node) => {
__typename: 'Market', const { marketId, ...nodeData } = node;
id: marketId, draft.unshift({
}, node: {
}; ...nodeData,
}; __typename: 'Trade',
market: {
__typename: 'Market',
id: marketId,
},
},
cursor: '',
});
const mapTradeUpdateToTradeWithMarket = if (draft.length > MAX_TRADES) {
(markets: Record<string, Market>) => draft.pop();
(tradeUpdate: TradeUpdateFieldsFragment): Trade => { }
const { market, ...trade } = mapTradeUpdateToTrade(tradeUpdate); });
return {
...trade,
market: markets[market.id],
};
};
const update = <T extends Omit<TradeFieldsFragment, 'market'> & Cursor>(
data: T[] | null,
delta: ReturnType<typeof getDelta>,
variables: TradesQueryVariables,
mapDeltaToData: (delta: TradeUpdateFieldsFragment) => T
): T[] => {
const updatedData = data ? [...data] : ([] as T[]);
orderBy(delta, 'createdAt', 'desc').forEach((tradeUpdate) => {
const index = data?.findIndex((trade) => trade.id === tradeUpdate.id) ?? -1;
if (index !== -1) {
updatedData[index] = {
...updatedData[index],
...mapDeltaToData(tradeUpdate),
};
} else if (!data?.length || tradeUpdate.createdAt >= data[0].createdAt) {
updatedData.unshift(mapDeltaToData(tradeUpdate));
}
}); });
return updatedData.slice(0, MAX_TRADES);
}; };
export type Trade = Omit<TradeFieldsFragment, 'market'> & { market?: Market }; export type Trade = Omit<TradeFieldsFragment, 'market'> & { market?: Market };
export type TradeEdge = Edge<Trade>;
const getPageInfo = (responseData: TradesQuery | null): PageInfo | null => const getPageInfo = (responseData: TradesQuery | null): PageInfo | null =>
responseData?.trades?.pageInfo || null; responseData?.market?.tradesConnection?.pageInfo || null;
export const tradesProvider = makeDataProvider< export const tradesProvider = makeDataProvider<
Parameters<typeof getData>['0'], Parameters<typeof getData>['0'],
ReturnType<typeof getData>, ReturnType<typeof getData>,
Parameters<typeof getDelta>['0'], Parameters<typeof getDelta>['0'],
ReturnType<typeof getDelta>, ReturnType<typeof getDelta>,
TradesQueryVariables, TradesQueryVariables
TradesUpdateSubscriptionVariables
>({ >({
query: TradesDocument, query: TradesDocument,
subscriptionQuery: TradesUpdateDocument, subscriptionQuery: TradesUpdateDocument,
update: (data, delta, reload, variables) => update,
update(data, delta, variables, mapTradeUpdateToTrade),
getData, getData,
getDelta, getDelta,
pagination: { pagination: {
@@ -99,32 +79,34 @@ export const tradesProvider = makeDataProvider<
append, append,
first: MAX_TRADES, first: MAX_TRADES,
}, },
getSubscriptionVariables: ({ marketId }) => ({ marketId }),
}); });
export const tradesWithMarketProvider = makeDerivedDataProvider< export const tradesWithMarketProvider = makeDerivedDataProvider<
(Trade & Cursor)[], (TradeEdge | null)[],
never, Trade[],
TradesQueryVariables TradesQueryVariables
>( >(
[ [
tradesProvider, tradesProvider,
(callback, client) => marketsMapProvider(callback, client, undefined), (callback, client) => marketsProvider(callback, client, undefined),
], ],
(partsData, variables, prevData, parts): Trade[] | null => { (partsData): (TradeEdge | null)[] | null => {
if (prevData && parts[0].isUpdate) { const edges = partsData[0] as ReturnType<typeof getData>;
return update( return edges.map((edge) => {
prevData, if (edge === null) {
parts[0].delta as ReturnType<typeof getDelta>, return null;
variables, }
mapTradeUpdateToTradeWithMarket(partsData[1] as Record<string, Market>) const node = {
); ...edge.node,
} market: (partsData[1] as Market[]).find(
return ((partsData[0] as ReturnType<typeof getData>) || []).map( (market) => market.id === edge.node.market.id
(trade) => ({ ),
...trade, };
market: (partsData[1] as Record<string, Market>)[trade.market.id], const cursor = edge?.cursor || '';
}) return {
); cursor,
node,
};
});
} }
); );
+18 -14
View File
@@ -11,20 +11,24 @@ export const tradesQuery = (
override?: PartialDeep<TradesQuery> override?: PartialDeep<TradesQuery>
): TradesQuery => { ): TradesQuery => {
const defaultResult: TradesQuery = { const defaultResult: TradesQuery = {
trades: { market: {
__typename: 'TradeConnection', id: 'market-0',
edges: trades.map((node, i) => ({ tradesConnection: {
__typename: 'TradeEdge', __typename: 'TradeConnection',
node, edges: trades.map((node, i) => ({
cursor: (i + 1).toString(), __typename: 'TradeEdge',
})), node,
pageInfo: { cursor: (i + 1).toString(),
__typename: 'PageInfo', })),
startCursor: '0', pageInfo: {
endCursor: trades.length.toString(), __typename: 'PageInfo',
hasNextPage: false, startCursor: '0',
hasPreviousPage: false, endCursor: trades.length.toString(),
hasNextPage: false,
hasPreviousPage: false,
},
}, },
__typename: 'Market',
}, },
}; };
@@ -36,7 +40,7 @@ export const tradesUpdateSubscription = (
): TradesUpdateSubscription => { ): TradesUpdateSubscription => {
const defaultResult: TradesUpdateSubscription = { const defaultResult: TradesUpdateSubscription = {
__typename: 'Subscription', __typename: 'Subscription',
tradesStream: [ trades: [
{ {
__typename: 'TradeUpdate', __typename: 'TradeUpdate',
id: '1234567890', id: '1234567890',
+1 -1
View File
@@ -12,7 +12,7 @@
}, },
"tasksRunnerOptions": { "tasksRunnerOptions": {
"default": { "default": {
"runner": "nx/tasks-runners/default", "runner": "@nrwl/nx-cloud",
"options": { "options": {
"cacheableOperations": ["build", "lint", "test", "e2e"], "cacheableOperations": ["build", "lint", "test", "e2e"],
"accessToken": "OTY4ZjdlZTItNGIwNy00NDcyLTllZjctOWIzYTg1NWE0Yzg1fHJlYWQtd3JpdGU=", "accessToken": "OTY4ZjdlZTItNGIwNy00NDcyLTllZjctOWIzYTg1NWE0Yzg1fHJlYWQtd3JpdGU=",
+1 -1
View File
@@ -161,7 +161,7 @@
"babel-loader": "8.1.0", "babel-loader": "8.1.0",
"cypress": "^12.9.0", "cypress": "^12.9.0",
"cypress-mochawesome-reporter": "^3.3.0", "cypress-mochawesome-reporter": "^3.3.0",
"cypress-real-events": "^1.8.1", "cypress-real-events": "^1.7.6",
"dotenv": "^16.0.1", "dotenv": "^16.0.1",
"eslint": "8.15.0", "eslint": "8.15.0",
"eslint-config-next": "12.2.3", "eslint-config-next": "12.2.3",
+4 -4
View File
@@ -11882,10 +11882,10 @@ cypress-mochawesome-reporter@^3.3.0:
mochawesome-merge "^4.2.1" mochawesome-merge "^4.2.1"
mochawesome-report-generator "^6.2.0" mochawesome-report-generator "^6.2.0"
cypress-real-events@^1.8.1: cypress-real-events@^1.7.6:
version "1.8.1" version "1.7.6"
resolved "https://registry.yarnpkg.com/cypress-real-events/-/cypress-real-events-1.8.1.tgz#d00c7fe93124bbe7c0f27296684838614d24a840" resolved "https://registry.yarnpkg.com/cypress-real-events/-/cypress-real-events-1.7.6.tgz#6f17e0b2ceea1d6dc60f6737d8f84cc517bbbb4c"
integrity sha512-8fFnA8EzS3EVbAmpSEUf3A8yZCmfU3IPOSGUDVFCdE1ke1gYL1A+gvXXV6HKUbTPRuvKKt2vpaMbUwYLpDRswQ== integrity sha512-yP6GnRrbm6HK5q4DH6Nnupz37nOfZu/xn1xFYqsE2o4G73giPWQOdu6375QYpwfU1cvHNCgyD2bQ2hPH9D7NMw==
cypress@^12.9.0: cypress@^12.9.0:
version "12.9.0" version "12.9.0"