Feature/303 orderbook improvements (#312)
* [#151] market-depth code cleanup
* [#303] Make ask and bid relative volume bars relative to maximum bid or ask volume
* [#151] align cmulative vol bars to left
* [#151] replace orderbook zoom in zoom out buttons with dropdown
* [#151] fill gaps in orderbook data
* Order book mocks added
* [#151] mark mid price in orderbook
* [303] Show number in orderbook cumulative volume column
* [#808] show indicative uncrossing volume instead of volume if market is in auction mode
* Method for asserting order book style
* [#303] Add test id attributes to orderbook cells
* Cleanup steps after merge
* Order book test passing
* Change method name
* Revert "[#151] fill gaps in orderbook data"
This reverts commit 90ea4e4ab3
.
* [#303] Orderbook rows render optimization
* test: update feature with @todo tests
Same tests can be found in Notion
* [#303] Orderbook scroll to mid price
* [#303] orderbook scroll to row pixel perfect alignment
* [#303] Bring back best offer horizontal lines
* [#303] Preserve center price level on row number change, adjust indicativePrice to resoluton
* feat(orderbook): add storybook
Refs: #303
* feat(orderbook): fix no rows handling
Refs: #303
* feat(orderbook): add orderbook stories for auction and continous market
Refs: #303
* feat(orderbook): add stories for empty orderbook
Refs: #303
* feat(orderbook): fix footer position when there is no data
Refs: #303
* feat(orderbook): seperate number of rows for buy and sell in storybook
Refs: #303
* feat(orderbook): keep mid price in middle until user will scroll
Refs: #303
* feat(orderbook): style scrollbar
* feat(orderbook): style scrollbar
* feat(orderbook): adjust gaps
* feat(orderbook): adjust gaps
* test: addition for autofilled order and mid price lines
* fix: lint
* feat(orderbook): make it posiible to write RTL tests
* feat(orderbook): fix price focus, add unit tests
* feat(orderbook): fix price scroll to mid proce, add unit tests
* feat(orderbook): improvements
- fix scrollbar colors in firefox
- bring back resolution dropdown chevron
- hide go to mid button when locked on mid price
- right align ask vol bar
- change grid gap to 5px
- add vertical lines between columns
- display "No data" if theis no orderbook data
- align header labels to right
* feat(orderbook): fix formatting
* feat(orderbook): add 5px gap
* feat(orderbook): improvements after code review
* feat(orderbook): display full height vertical lines
* fix: change in mid position
* feat(orderbook): fix number cannot be converted to BigInt because it is not integer
* feat(orderbook): fix TS2307 in trading-e2e caused by .module.scss import
Co-authored-by: Joe <joe@vega.xyz>
This commit is contained in:
parent
71226e3d75
commit
d0452aeb81
1
apps/trading-e2e/declaration.d.ts
vendored
Normal file
1
apps/trading-e2e/declaration.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
declare module '*.scss';
|
@ -76,15 +76,6 @@ Feature: Trading page
|
||||
And place a buy 'FOK' market order with amount of 0
|
||||
Then Order rejected by wallet error shown containing text "must be positive"
|
||||
|
||||
@manual
|
||||
Scenario: Deal ticket: GTT order failed because invalid date
|
||||
|
||||
@manual
|
||||
Scenario: Deal ticket: GTT order failed because date in the past
|
||||
|
||||
@manual
|
||||
Scenario: Deal ticket: GTT order failed because date over allowed period
|
||||
|
||||
Scenario: Positions: Displayed when connected to wallet
|
||||
Given I am on the trading page for an active market
|
||||
And I connect to Vega Wallet
|
||||
@ -97,9 +88,40 @@ Feature: Trading page
|
||||
When I click on accounts tab
|
||||
Then accounts are displayed
|
||||
And I can see account for tEURO
|
||||
|
||||
|
||||
Scenario: Orders: Placed orders displayed
|
||||
Given I am on the trading page for an active market
|
||||
And I connect to Vega Wallet
|
||||
When I click on orders tab
|
||||
Then placed orders are displayed
|
||||
Then placed orders are displayed
|
||||
|
||||
Scenario: Orderbook displayed
|
||||
Given I am on the trading page for an active market
|
||||
When I click on order book tab
|
||||
Then orderbook is displayed with expected orders
|
||||
And orderbook can be reduced and expanded
|
||||
|
||||
@todo
|
||||
Scenario: Orderbook paginated with over 100 orders
|
||||
Given I am on the trading page for an active market
|
||||
When I click on order book tab
|
||||
And a large amount is orders are received
|
||||
Then a certain amount of orders are displayed
|
||||
|
||||
@todo
|
||||
Scenario: Orderbook uses non-static prices for market in auction
|
||||
Given I am on the trading page for a market in auction
|
||||
When I click on order book tab
|
||||
Then order book is rendered using non-static offers
|
||||
|
||||
@todo
|
||||
Scenario: Orderbook updated when large order is made
|
||||
Given I am on the trading page for an active market
|
||||
When I place a large order
|
||||
Then I should see my order have an effect on the order book
|
||||
|
||||
@todo
|
||||
Scenario: Able to place order by clicking on order from orderbook
|
||||
Given I am on the trading page for an active market
|
||||
When I place a large order
|
||||
Then I should see my order have an effect on the order book
|
||||
|
125
apps/trading-e2e/src/support/mocks/generate-order-book.ts
Normal file
125
apps/trading-e2e/src/support/mocks/generate-order-book.ts
Normal file
@ -0,0 +1,125 @@
|
||||
import merge from 'lodash/merge';
|
||||
import type { PartialDeep } from 'type-fest';
|
||||
import type {
|
||||
MarketDepth,
|
||||
MarketDepth_market,
|
||||
} from '@vegaprotocol/market-depth';
|
||||
import { MarketTradingMode } from '@vegaprotocol/types';
|
||||
|
||||
export const generateOrderBook = (
|
||||
override?: PartialDeep<MarketDepth>
|
||||
): MarketDepth => {
|
||||
const marketDepth: MarketDepth_market = {
|
||||
id: 'b2426f67b085ba8fb429f1b529d49372b2d096c6fb6f509f76c5863abb6d969e',
|
||||
decimalPlaces: 5,
|
||||
data: {
|
||||
staticMidPrice: '826337',
|
||||
marketTradingMode: MarketTradingMode.Continuous,
|
||||
indicativeVolume: '0',
|
||||
indicativePrice: '0',
|
||||
bestStaticBidPrice: '826336',
|
||||
bestStaticOfferPrice: '826338',
|
||||
market: {
|
||||
id: 'b2426f67b085ba8fb429f1b529d49372b2d096c6fb6f509f76c5863abb6d969e',
|
||||
__typename: 'Market',
|
||||
},
|
||||
__typename: 'MarketData',
|
||||
},
|
||||
depth: {
|
||||
lastTrade: {
|
||||
price: '826338',
|
||||
__typename: 'Trade',
|
||||
},
|
||||
sell: [
|
||||
{
|
||||
price: '826338',
|
||||
volume: '303',
|
||||
numberOfOrders: '8',
|
||||
__typename: 'PriceLevel',
|
||||
},
|
||||
{
|
||||
price: '826339',
|
||||
volume: '193',
|
||||
numberOfOrders: '4',
|
||||
__typename: 'PriceLevel',
|
||||
},
|
||||
{
|
||||
price: '826340',
|
||||
volume: '316',
|
||||
numberOfOrders: '7',
|
||||
__typename: 'PriceLevel',
|
||||
},
|
||||
{
|
||||
price: '826341',
|
||||
volume: '412',
|
||||
numberOfOrders: '9',
|
||||
__typename: 'PriceLevel',
|
||||
},
|
||||
{
|
||||
price: '826342',
|
||||
volume: '264',
|
||||
numberOfOrders: '6',
|
||||
__typename: 'PriceLevel',
|
||||
},
|
||||
],
|
||||
buy: [
|
||||
{
|
||||
price: '826339',
|
||||
volume: '200',
|
||||
numberOfOrders: '5',
|
||||
__typename: 'PriceLevel',
|
||||
},
|
||||
{
|
||||
price: '826336',
|
||||
volume: '1475',
|
||||
numberOfOrders: '28',
|
||||
__typename: 'PriceLevel',
|
||||
},
|
||||
{
|
||||
price: '826335',
|
||||
volume: '193',
|
||||
numberOfOrders: '3',
|
||||
__typename: 'PriceLevel',
|
||||
},
|
||||
{
|
||||
price: '826334',
|
||||
volume: '425',
|
||||
numberOfOrders: '8',
|
||||
__typename: 'PriceLevel',
|
||||
},
|
||||
{
|
||||
price: '826333',
|
||||
volume: '845',
|
||||
numberOfOrders: '17',
|
||||
__typename: 'PriceLevel',
|
||||
},
|
||||
{
|
||||
price: '826332',
|
||||
volume: '248',
|
||||
numberOfOrders: '4',
|
||||
__typename: 'PriceLevel',
|
||||
},
|
||||
{
|
||||
price: '826331',
|
||||
volume: '162',
|
||||
numberOfOrders: '3',
|
||||
__typename: 'PriceLevel',
|
||||
},
|
||||
{
|
||||
price: '826328',
|
||||
volume: '20',
|
||||
numberOfOrders: '2',
|
||||
__typename: 'PriceLevel',
|
||||
},
|
||||
],
|
||||
sequenceNumber: '36109974',
|
||||
__typename: 'MarketDepth',
|
||||
},
|
||||
__typename: 'Market',
|
||||
};
|
||||
const defaultResult = {
|
||||
market: marketDepth,
|
||||
};
|
||||
|
||||
return merge(defaultResult, override);
|
||||
};
|
@ -10,7 +10,6 @@ export default class TradingPage extends BasePage {
|
||||
collateralTab = 'Collateral';
|
||||
tradesTab = 'Trades';
|
||||
completedTrades = 'Market-trades';
|
||||
orderBookTab = 'Prderbook';
|
||||
|
||||
clickOnOrdersTab() {
|
||||
cy.getByTestId(this.ordersTab).click();
|
||||
@ -37,6 +36,6 @@ export default class TradingPage extends BasePage {
|
||||
}
|
||||
|
||||
clickOrderBookTab() {
|
||||
cy.getByTestId(this.orderBookTab).click();
|
||||
cy.getByTestId(this.orderbookTab).click();
|
||||
}
|
||||
}
|
||||
|
@ -8,20 +8,23 @@ import { generateDealTicketQuery } from '../mocks/generate-deal-ticket-query';
|
||||
import { generateMarket } from '../mocks/generate-market';
|
||||
import { generateOrders } from '../mocks/generate-orders';
|
||||
import { generatePositions } from '../mocks/generate-positions';
|
||||
import { generateOrderBook } from '../mocks/generate-order-book';
|
||||
import { generateAccounts } from '../mocks/generate-accounts';
|
||||
import PositionsList from '../trading-windows/positions-list';
|
||||
import AccountsList from '../trading-windows/accounts-list';
|
||||
import TradesList from '../trading-windows/trades-list';
|
||||
import TradingPage from '../pages/trading-page';
|
||||
import OrdersList from '../trading-windows/orders-list';
|
||||
import OrderBookList from '../trading-windows/orderbook-list';
|
||||
import MarketPage from '../pages/markets-page';
|
||||
|
||||
const tradesList = new TradesList();
|
||||
const tradingPage = new TradingPage();
|
||||
const marketPage = new MarketPage();
|
||||
const positionsList = new PositionsList();
|
||||
const accountList = new AccountsList();
|
||||
const ordersList = new OrdersList();
|
||||
const orderBookList = new OrderBookList();
|
||||
const marketPage = new MarketPage();
|
||||
|
||||
const mockMarket = (state: MarketState) => {
|
||||
cy.mockGQL('Market', (req) => {
|
||||
@ -75,6 +78,12 @@ const mockMarket = (state: MarketState) => {
|
||||
});
|
||||
}
|
||||
|
||||
if (hasOperationName(req, 'MarketDepth')) {
|
||||
req.reply({
|
||||
body: { data: generateOrderBook() },
|
||||
});
|
||||
}
|
||||
|
||||
if (hasOperationName(req, 'Candles')) {
|
||||
req.reply({
|
||||
body: { data: generateCandles() },
|
||||
@ -159,3 +168,78 @@ When('I click on positions tab', () => {
|
||||
Then('positions are displayed', () => {
|
||||
positionsList.verifyPositionsDisplayed();
|
||||
});
|
||||
|
||||
When('I click on order book tab', () => {
|
||||
tradingPage.clickOrderBookTab();
|
||||
});
|
||||
|
||||
Then('orderbook is displayed with expected orders', () => {
|
||||
orderBookList.verifyOrderBookRow('826342', '0', '8.26342', '264', '1488');
|
||||
orderBookList.verifyOrderBookRow('826336', '1475', '8.26336', '0', '1675');
|
||||
orderBookList.verifyDisplayedVolume(
|
||||
'826342',
|
||||
false,
|
||||
'18%',
|
||||
orderBookList.testingVolume.AskVolume
|
||||
);
|
||||
orderBookList.verifyDisplayedVolume(
|
||||
'826331',
|
||||
true,
|
||||
'100%',
|
||||
orderBookList.testingVolume.CumulativeVolume
|
||||
);
|
||||
// mid level price
|
||||
orderBookList.verifyOrderBookRow('826337', '0', '8.26337', '0', '200');
|
||||
orderBookList.verifyDisplayedVolume(
|
||||
'826337',
|
||||
true,
|
||||
'6%',
|
||||
orderBookList.testingVolume.CumulativeVolume
|
||||
);
|
||||
orderBookList.verifyTopMidPricePosition('129');
|
||||
orderBookList.verifyBottomMidPricePosition('151');
|
||||
|
||||
// autofilled order
|
||||
orderBookList.verifyOrderBookRow('826330', '0', '8.26330', '0', '3548');
|
||||
orderBookList.verifyDisplayedVolume(
|
||||
'826330',
|
||||
true,
|
||||
'0%',
|
||||
orderBookList.testingVolume.BidVolume
|
||||
);
|
||||
orderBookList.verifyDisplayedVolume(
|
||||
'826330',
|
||||
true,
|
||||
'100%',
|
||||
orderBookList.testingVolume.CumulativeVolume
|
||||
);
|
||||
});
|
||||
|
||||
Then('orderbook can be reduced and expanded', () => {
|
||||
orderBookList.changePrecision('10');
|
||||
orderBookList.verifyOrderBookRow(
|
||||
'82634',
|
||||
'1868',
|
||||
'8.2634',
|
||||
'1488',
|
||||
'1488/1868'
|
||||
);
|
||||
orderBookList.verifyCumulativeAskBarPercentage('42%');
|
||||
orderBookList.verifyCumulativeBidBarPercentage('53%');
|
||||
orderBookList.changePrecision('100');
|
||||
orderBookList.verifyOrderBookRow('8263', '3568', '8.263', '1488', '');
|
||||
orderBookList.verifyDisplayedVolume(
|
||||
'8263',
|
||||
true,
|
||||
'100%',
|
||||
orderBookList.testingVolume.BidVolume
|
||||
);
|
||||
orderBookList.verifyDisplayedVolume(
|
||||
'8263',
|
||||
false,
|
||||
'42%',
|
||||
orderBookList.testingVolume.AskVolume
|
||||
);
|
||||
orderBookList.changePrecision('1');
|
||||
orderBookList.verifyOrderBookRow('826342', '0', '8.26342', '264', '1488');
|
||||
});
|
||||
|
129
apps/trading-e2e/src/support/trading-windows/orderbook-list.ts
Normal file
129
apps/trading-e2e/src/support/trading-windows/orderbook-list.ts
Normal file
@ -0,0 +1,129 @@
|
||||
export default class OrderBookList {
|
||||
cumulativeVolBidBar = 'bid-bar';
|
||||
cumulativeVolAskBar = 'ask-bar';
|
||||
precisionChange = 'resolution';
|
||||
bidColour = 'darkgreen';
|
||||
askColour = 'maroon';
|
||||
testingVolume = TestingVolumeType;
|
||||
topMidPriceLine = 'best-static-offer-price';
|
||||
bottomMidPriceLine = 'best-static-bid-price';
|
||||
|
||||
bidVolTestId(price: string) {
|
||||
return `bid-vol-${price}`;
|
||||
}
|
||||
priceTestId(price: string) {
|
||||
return `price-${price}`;
|
||||
}
|
||||
askVolTestId(price: string) {
|
||||
return `ask-vol-${price}`;
|
||||
}
|
||||
cumulativeVolTestId(price: string) {
|
||||
return `cumulative-vol-${price}`;
|
||||
}
|
||||
|
||||
verifyOrderBookDisplayed(price: string) {
|
||||
cy.getByTestId(this.bidVolTestId(price)).should('not.be.empty');
|
||||
cy.getByTestId(this.priceTestId(price))
|
||||
.invoke('text')
|
||||
.then(($priceText) => {
|
||||
$priceText = $priceText.replace('.', '');
|
||||
expect($priceText).to.equal(price);
|
||||
});
|
||||
cy.getByTestId(this.askVolTestId(price)).should('not.be.empty');
|
||||
cy.getByTestId(this.cumulativeVolTestId(price)).should('not.be.empty');
|
||||
}
|
||||
|
||||
verifyOrderBookRow(
|
||||
price: string,
|
||||
expectedBidVol: string,
|
||||
expectedPrice: string,
|
||||
expectedAskVol: string,
|
||||
expectedCumulativeVol: string
|
||||
) {
|
||||
cy.getByTestId(this.bidVolTestId(price)).should(
|
||||
'have.text',
|
||||
expectedBidVol
|
||||
);
|
||||
cy.getByTestId(this.priceTestId(price)).should('have.text', expectedPrice);
|
||||
cy.getByTestId(this.askVolTestId(price)).should(
|
||||
'have.text',
|
||||
expectedAskVol
|
||||
);
|
||||
cy.getByTestId(this.cumulativeVolTestId(price)).should(
|
||||
'have.text',
|
||||
expectedCumulativeVol
|
||||
);
|
||||
}
|
||||
|
||||
// Value should be 1, 10 or 100
|
||||
changePrecision(precisionValue: string) {
|
||||
cy.getByTestId(this.precisionChange).select(precisionValue);
|
||||
}
|
||||
|
||||
verifyDisplayedVolume(
|
||||
price: string,
|
||||
isBuy: boolean,
|
||||
expectedPercentage: string,
|
||||
volumeType: TestingVolumeType
|
||||
) {
|
||||
let expectedColour = '';
|
||||
let testId = '';
|
||||
|
||||
if (isBuy == true) {
|
||||
expectedColour = this.bidColour;
|
||||
} else {
|
||||
expectedColour = this.askColour;
|
||||
}
|
||||
|
||||
switch (volumeType) {
|
||||
case TestingVolumeType.BidVolume:
|
||||
testId = `[data-testid=${this.bidVolTestId(price)}]`;
|
||||
break;
|
||||
|
||||
case TestingVolumeType.AskVolume:
|
||||
testId = `[data-testid=${this.askVolTestId(price)}]`;
|
||||
break;
|
||||
|
||||
case TestingVolumeType.CumulativeVolume:
|
||||
testId = `[data-testid=${this.cumulativeVolTestId(price)}]`;
|
||||
break;
|
||||
}
|
||||
|
||||
cy.get(`${testId} > div`)
|
||||
.invoke('attr', 'style')
|
||||
.should('contain', `width: ${expectedPercentage}`)
|
||||
.should('contain', `background-color: ${expectedColour}`);
|
||||
}
|
||||
|
||||
verifyCumulativeAskBarPercentage(expectedPercentage: string) {
|
||||
cy.getByTestId(this.cumulativeVolAskBar)
|
||||
.invoke('attr', 'style')
|
||||
.should('contain', `width: ${expectedPercentage}`)
|
||||
.should('contain', `background-color: ${this.askColour}`);
|
||||
}
|
||||
|
||||
verifyCumulativeBidBarPercentage(expectedPercentage: string) {
|
||||
cy.getByTestId(this.cumulativeVolBidBar)
|
||||
.invoke('attr', 'style')
|
||||
.should('contain', `width: ${expectedPercentage}`)
|
||||
.should('contain', `background-color: ${this.bidColour}`);
|
||||
}
|
||||
|
||||
verifyTopMidPricePosition(expectedPosition: string) {
|
||||
cy.getByTestId(this.topMidPriceLine)
|
||||
.invoke('attr', 'style')
|
||||
.should('contain', `top: ${expectedPosition}px`);
|
||||
}
|
||||
|
||||
verifyBottomMidPricePosition(expectedPosition: string) {
|
||||
cy.getByTestId(this.bottomMidPriceLine)
|
||||
.invoke('attr', 'style')
|
||||
.should('contain', `top: ${expectedPosition}px`);
|
||||
}
|
||||
}
|
||||
|
||||
enum TestingVolumeType {
|
||||
BidVolume = 'BidVolume',
|
||||
AskVolume = 'AskVolume',
|
||||
CumulativeVolume = 'CumulativeVolume',
|
||||
}
|
@ -15,5 +15,5 @@
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.js"]
|
||||
"include": ["src/**/*.ts", "src/**/*.js", "./declaration.d.ts"]
|
||||
}
|
||||
|
28
libs/market-depth/.storybook/main.js
Normal file
28
libs/market-depth/.storybook/main.js
Normal file
@ -0,0 +1,28 @@
|
||||
const rootMain = require('../../../.storybook/main');
|
||||
|
||||
module.exports = {
|
||||
...rootMain,
|
||||
|
||||
core: { ...rootMain.core, builder: 'webpack5' },
|
||||
|
||||
stories: [
|
||||
...rootMain.stories,
|
||||
'../src/lib/**/*.stories.mdx',
|
||||
'../src/lib/**/*.stories.@(js|jsx|ts|tsx)',
|
||||
],
|
||||
addons: [
|
||||
...rootMain.addons,
|
||||
'@nrwl/react/plugins/storybook',
|
||||
'storybook-addon-themes',
|
||||
],
|
||||
webpackFinal: async (config, { configType }) => {
|
||||
// apply any global webpack configs that might have been specified in .storybook/main.js
|
||||
if (rootMain.webpackFinal) {
|
||||
config = await rootMain.webpackFinal(config, { configType });
|
||||
}
|
||||
|
||||
// add your own webpack tweaks if needed
|
||||
|
||||
return config;
|
||||
},
|
||||
};
|
1
libs/market-depth/.storybook/preview-head.html
Normal file
1
libs/market-depth/.storybook/preview-head.html
Normal file
@ -0,0 +1 @@
|
||||
<link rel="stylesheet" href="https://static.vega.xyz/fonts.css" />
|
12
libs/market-depth/.storybook/preview.js
Normal file
12
libs/market-depth/.storybook/preview.js
Normal file
@ -0,0 +1,12 @@
|
||||
import '../src/styles.scss';
|
||||
export const parameters = {
|
||||
actions: { argTypesRegex: '^on[A-Z].*' },
|
||||
backgrounds: { disable: true },
|
||||
themes: {
|
||||
default: 'dark',
|
||||
list: [
|
||||
{ name: 'dark', class: ['dark', 'bg-black'], color: '#000' },
|
||||
{ name: 'light', class: '', color: '#FFF' },
|
||||
],
|
||||
},
|
||||
};
|
19
libs/market-depth/.storybook/tsconfig.json
Normal file
19
libs/market-depth/.storybook/tsconfig.json
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"emitDecoratorMetadata": true,
|
||||
"outDir": ""
|
||||
},
|
||||
"files": [
|
||||
"../../../node_modules/@nrwl/react/typings/styled-jsx.d.ts",
|
||||
"../../../node_modules/@nrwl/react/typings/cssmodule.d.ts",
|
||||
"../../../node_modules/@nrwl/react/typings/image.d.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"../**/*.spec.ts",
|
||||
"../**/*.spec.js",
|
||||
"../**/*.spec.tsx",
|
||||
"../**/*.spec.jsx"
|
||||
],
|
||||
"include": ["../src/**/*", "*.js"]
|
||||
}
|
@ -4,4 +4,4 @@ This library was generated with [Nx](https://nx.dev).
|
||||
|
||||
## Running unit tests
|
||||
|
||||
Run `nx test orderbook` to execute the unit tests via [Jest](https://jestjs.io).
|
||||
Run `nx test market-depth` to execute the unit tests via [Jest](https://jestjs.io).
|
||||
|
@ -1,13 +1,8 @@
|
||||
module.exports = {
|
||||
displayName: 'orderbook',
|
||||
displayName: 'market-depth',
|
||||
preset: '../../jest.preset.js',
|
||||
globals: {
|
||||
'ts-jest': {
|
||||
tsconfig: '<rootDir>/tsconfig.spec.json',
|
||||
},
|
||||
},
|
||||
transform: {
|
||||
'^.+\\.[tj]sx?$': 'ts-jest',
|
||||
'^.+\\.[tj]sx?$': 'babel-jest',
|
||||
},
|
||||
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
|
||||
coverageDirectory: '../../coverage/libs/market-depth',
|
||||
|
10
libs/market-depth/postcss.config.js
Normal file
10
libs/market-depth/postcss.config.js
Normal file
@ -0,0 +1,10 @@
|
||||
const { join } = require('path');
|
||||
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {
|
||||
config: join(__dirname, 'tailwind.config.js'),
|
||||
},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
@ -38,6 +38,37 @@
|
||||
"jestConfig": "libs/market-depth/jest.config.js",
|
||||
"passWithNoTests": true
|
||||
}
|
||||
},
|
||||
"storybook": {
|
||||
"executor": "@nrwl/storybook:storybook",
|
||||
"options": {
|
||||
"uiFramework": "@storybook/react",
|
||||
"port": 4400,
|
||||
"config": {
|
||||
"configFolder": "libs/market-depth/.storybook"
|
||||
}
|
||||
},
|
||||
"configurations": {
|
||||
"ci": {
|
||||
"quiet": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"build-storybook": {
|
||||
"executor": "@nrwl/storybook:build",
|
||||
"outputs": ["{options.outputPath}"],
|
||||
"options": {
|
||||
"uiFramework": "@storybook/react",
|
||||
"outputPath": "dist/storybook/market-depth",
|
||||
"config": {
|
||||
"configFolder": "libs/market-depth/.storybook"
|
||||
}
|
||||
},
|
||||
"configurations": {
|
||||
"ci": {
|
||||
"quiet": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,2 +1,3 @@
|
||||
export * from './lib/depth-chart';
|
||||
export * from './lib/orderbook-container';
|
||||
export * from './lib/__generated__/MarketDepth';
|
||||
|
@ -3,6 +3,8 @@
|
||||
// @generated
|
||||
// This file was automatically generated and should not be edited.
|
||||
|
||||
import { MarketTradingMode } from "@vegaprotocol/types";
|
||||
|
||||
// ====================================================
|
||||
// GraphQL query operation: MarketDepth
|
||||
// ====================================================
|
||||
@ -18,9 +20,29 @@ export interface MarketDepth_market_data_market {
|
||||
export interface MarketDepth_market_data {
|
||||
__typename: "MarketData";
|
||||
/**
|
||||
* the arithmetic average of the best bid price and best offer price.
|
||||
* the arithmetic average of the best static bid price and best static offer price
|
||||
*/
|
||||
midPrice: string;
|
||||
staticMidPrice: string;
|
||||
/**
|
||||
* what state the market is in (auction, continuous etc)
|
||||
*/
|
||||
marketTradingMode: MarketTradingMode;
|
||||
/**
|
||||
* indicative volume if the auction ended now, 0 if not in auction mode
|
||||
*/
|
||||
indicativeVolume: string;
|
||||
/**
|
||||
* indicative price if the auction ended now, 0 if not in auction mode
|
||||
*/
|
||||
indicativePrice: string;
|
||||
/**
|
||||
* the highest price level on an order book for buy orders not including pegged orders.
|
||||
*/
|
||||
bestStaticBidPrice: string;
|
||||
/**
|
||||
* the lowest price level on an order book for offer orders not including pegged orders.
|
||||
*/
|
||||
bestStaticOfferPrice: string;
|
||||
/**
|
||||
* market id of the associated mark price
|
||||
*/
|
||||
|
@ -3,6 +3,8 @@
|
||||
// @generated
|
||||
// This file was automatically generated and should not be edited.
|
||||
|
||||
import { MarketTradingMode } from "@vegaprotocol/types";
|
||||
|
||||
// ====================================================
|
||||
// GraphQL subscription operation: MarketDepthSubscription
|
||||
// ====================================================
|
||||
@ -18,9 +20,29 @@ export interface MarketDepthSubscription_marketDepthUpdate_market_data_market {
|
||||
export interface MarketDepthSubscription_marketDepthUpdate_market_data {
|
||||
__typename: "MarketData";
|
||||
/**
|
||||
* the arithmetic average of the best bid price and best offer price.
|
||||
* the arithmetic average of the best static bid price and best static offer price
|
||||
*/
|
||||
midPrice: string;
|
||||
staticMidPrice: string;
|
||||
/**
|
||||
* what state the market is in (auction, continuous etc)
|
||||
*/
|
||||
marketTradingMode: MarketTradingMode;
|
||||
/**
|
||||
* indicative volume if the auction ended now, 0 if not in auction mode
|
||||
*/
|
||||
indicativeVolume: string;
|
||||
/**
|
||||
* indicative price if the auction ended now, 0 if not in auction mode
|
||||
*/
|
||||
indicativePrice: string;
|
||||
/**
|
||||
* the highest price level on an order book for buy orders not including pegged orders.
|
||||
*/
|
||||
bestStaticBidPrice: string;
|
||||
/**
|
||||
* the lowest price level on an order book for offer orders not including pegged orders.
|
||||
*/
|
||||
bestStaticOfferPrice: string;
|
||||
/**
|
||||
* market id of the associated mark price
|
||||
*/
|
||||
|
@ -107,9 +107,9 @@ export const DepthChartContainer = ({ marketId }: DepthChartManagerProps) => {
|
||||
decimalPlacesRef.current
|
||||
);
|
||||
}
|
||||
draft.midPrice = delta.market.data?.midPrice
|
||||
draft.midPrice = delta.market.data?.staticMidPrice
|
||||
? formatMidPrice(
|
||||
delta.market.data?.midPrice,
|
||||
delta.market.data?.staticMidPrice,
|
||||
decimalPlacesRef.current
|
||||
)
|
||||
: undefined;
|
||||
@ -133,8 +133,8 @@ export const DepthChartContainer = ({ marketId }: DepthChartManagerProps) => {
|
||||
return;
|
||||
}
|
||||
dataRef.current = {
|
||||
midPrice: data.data?.midPrice
|
||||
? formatMidPrice(data.data?.midPrice, data.decimalPlaces)
|
||||
midPrice: data.data?.staticMidPrice
|
||||
? formatMidPrice(data.data?.staticMidPrice, data.decimalPlaces)
|
||||
: undefined,
|
||||
data: {
|
||||
buy:
|
||||
|
@ -17,7 +17,12 @@ const MARKET_DEPTH_QUERY = gql`
|
||||
id
|
||||
decimalPlaces
|
||||
data {
|
||||
midPrice
|
||||
staticMidPrice
|
||||
marketTradingMode
|
||||
indicativeVolume
|
||||
indicativePrice
|
||||
bestStaticBidPrice
|
||||
bestStaticOfferPrice
|
||||
market {
|
||||
id
|
||||
}
|
||||
@ -48,7 +53,12 @@ export const MARKET_DEPTH_SUBSCRIPTION_QUERY = gql`
|
||||
market {
|
||||
id
|
||||
data {
|
||||
midPrice
|
||||
staticMidPrice
|
||||
marketTradingMode
|
||||
indicativeVolume
|
||||
indicativePrice
|
||||
bestStaticBidPrice
|
||||
bestStaticOfferPrice
|
||||
market {
|
||||
id
|
||||
}
|
||||
@ -74,7 +84,7 @@ const sequenceNumbers: Record<string, number> = {};
|
||||
const update: Update<
|
||||
MarketDepth_market,
|
||||
MarketDepthSubscription_marketDepthUpdate
|
||||
> = (draft, delta, restart) => {
|
||||
> = (draft, delta, reload) => {
|
||||
if (delta.market.id !== draft.id) {
|
||||
return;
|
||||
}
|
||||
@ -84,10 +94,11 @@ const update: Update<
|
||||
}
|
||||
if (sequenceNumber - 1 !== sequenceNumbers[delta.market.id]) {
|
||||
sequenceNumbers[delta.market.id] = 0;
|
||||
restart(true);
|
||||
reload();
|
||||
return;
|
||||
}
|
||||
sequenceNumbers[delta.market.id] = sequenceNumber;
|
||||
Object.assign(draft.data, delta.market.data);
|
||||
if (delta.buy) {
|
||||
draft.depth.buy = updateLevels(draft.depth.buy ?? [], delta.buy);
|
||||
}
|
||||
|
@ -1,57 +0,0 @@
|
||||
import type {
|
||||
MarketDepth_market,
|
||||
MarketDepth_market_depth_sell,
|
||||
MarketDepth_market_depth_buy,
|
||||
} from './__generated__/MarketDepth';
|
||||
|
||||
const depthRow = (
|
||||
price: number
|
||||
): MarketDepth_market_depth_sell | MarketDepth_market_depth_buy => {
|
||||
return {
|
||||
__typename: 'PriceLevel',
|
||||
price: price.toString(),
|
||||
volume: Math.round(Math.random() * 100).toString(),
|
||||
numberOfOrders: Math.round(Math.random() * 20).toString(),
|
||||
};
|
||||
};
|
||||
|
||||
const sell = (
|
||||
price: number,
|
||||
numberOfRecords: number
|
||||
): MarketDepth_market_depth_sell[] => {
|
||||
const distance = Math.random() * price * 0.1;
|
||||
return new Array(numberOfRecords)
|
||||
.fill(null)
|
||||
.map(() => depthRow(price + Math.round(Math.random() * distance)));
|
||||
};
|
||||
|
||||
const buy = (
|
||||
price: number,
|
||||
numberOfRecords: number
|
||||
): MarketDepth_market_depth_buy[] => {
|
||||
const distance = Math.random() * price * 0.1;
|
||||
return new Array(numberOfRecords)
|
||||
.fill(null)
|
||||
.map(() => depthRow(price - Math.round(Math.random() * distance)));
|
||||
};
|
||||
|
||||
export const getMockedData = (id?: string): MarketDepth_market => ({
|
||||
__typename: 'Market',
|
||||
id: id || '',
|
||||
decimalPlaces: 2,
|
||||
// "positionDecimalPlaces": 0,
|
||||
data: {
|
||||
__typename: 'MarketData',
|
||||
midPrice: '0',
|
||||
},
|
||||
depth: {
|
||||
__typename: 'MarketDepth',
|
||||
lastTrade: {
|
||||
__typename: 'Trade',
|
||||
price: '12350',
|
||||
},
|
||||
sell: sell(12350 * 0.99, 100),
|
||||
buy: buy(12350, 100),
|
||||
sequenceNumber: '118118448',
|
||||
},
|
||||
});
|
@ -1,31 +1,5 @@
|
||||
import { useState } from 'react';
|
||||
import { OrderbookManager } from './orderbook-manager';
|
||||
import { Button } from '@vegaprotocol/ui-toolkit';
|
||||
|
||||
export const OrderbookContainer = ({ marketId }: { marketId: string }) => {
|
||||
const [resolution, setResolution] = useState(1);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex gap-8">
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => setResolution(resolution * 10)}
|
||||
appendIconName="minus"
|
||||
className="flex-1"
|
||||
>
|
||||
Zoom out
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => setResolution(Math.max(resolution / 10, 1))}
|
||||
appendIconName="plus"
|
||||
className="flex-1"
|
||||
>
|
||||
Zoom in
|
||||
</Button>
|
||||
</div>
|
||||
<OrderbookManager resolution={resolution} marketId={marketId} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
export const OrderbookContainer = ({ marketId }: { marketId: string }) => (
|
||||
<OrderbookManager marketId={marketId} />
|
||||
);
|
||||
|
@ -1,16 +1,16 @@
|
||||
import {
|
||||
compactData,
|
||||
compactRows,
|
||||
updateLevels,
|
||||
updateCompactedData,
|
||||
updateCompactedRows,
|
||||
} from './orderbook-data';
|
||||
import type { OrderbookData } from './orderbook-data';
|
||||
import type { OrderbookRowData } from './orderbook-data';
|
||||
import type { MarketDepth_market_depth_sell } from './__generated__/MarketDepth';
|
||||
import type {
|
||||
MarketDepthSubscription_marketDepthUpdate_sell,
|
||||
MarketDepthSubscription_marketDepthUpdate_buy,
|
||||
} from './__generated__/MarketDepthSubscription';
|
||||
|
||||
describe('compactData', () => {
|
||||
describe('compactRows', () => {
|
||||
const numberOfRows = 100;
|
||||
const middle = 1000;
|
||||
const sell: MarketDepth_market_depth_sell[] = new Array(numberOfRows)
|
||||
@ -30,26 +30,26 @@ describe('compactData', () => {
|
||||
numberOfOrders: (numberOfRows - i).toString(),
|
||||
}));
|
||||
it('groups data by price and resolution', () => {
|
||||
expect(compactData(sell, buy, 1).length).toEqual(200);
|
||||
expect(compactData(sell, buy, 5).length).toEqual(41);
|
||||
expect(compactData(sell, buy, 10).length).toEqual(21);
|
||||
expect(compactRows(sell, buy, 1).length).toEqual(200);
|
||||
expect(compactRows(sell, buy, 5).length).toEqual(41);
|
||||
expect(compactRows(sell, buy, 10).length).toEqual(21);
|
||||
});
|
||||
it('counts cumulative vol', () => {
|
||||
const orderbookData = compactData(sell, buy, 10);
|
||||
expect(orderbookData[0].cumulativeVol.ask).toEqual(4950);
|
||||
expect(orderbookData[0].cumulativeVol.bid).toEqual(0);
|
||||
expect(orderbookData[10].cumulativeVol.ask).toEqual(390);
|
||||
expect(orderbookData[10].cumulativeVol.bid).toEqual(579);
|
||||
expect(orderbookData[orderbookData.length - 1].cumulativeVol.bid).toEqual(
|
||||
const orderbookRows = compactRows(sell, buy, 10);
|
||||
expect(orderbookRows[0].cumulativeVol.ask).toEqual(4950);
|
||||
expect(orderbookRows[0].cumulativeVol.bid).toEqual(0);
|
||||
expect(orderbookRows[10].cumulativeVol.ask).toEqual(390);
|
||||
expect(orderbookRows[10].cumulativeVol.bid).toEqual(579);
|
||||
expect(orderbookRows[orderbookRows.length - 1].cumulativeVol.bid).toEqual(
|
||||
4950
|
||||
);
|
||||
expect(orderbookData[orderbookData.length - 1].cumulativeVol.ask).toEqual(
|
||||
expect(orderbookRows[orderbookRows.length - 1].cumulativeVol.ask).toEqual(
|
||||
0
|
||||
);
|
||||
});
|
||||
it('stores volume by level', () => {
|
||||
const orderbookData = compactData(sell, buy, 10);
|
||||
expect(orderbookData[0].askByLevel).toEqual({
|
||||
const orderbookRows = compactRows(sell, buy, 10);
|
||||
expect(orderbookRows[0].askByLevel).toEqual({
|
||||
'1095': 5,
|
||||
'1096': 4,
|
||||
'1097': 3,
|
||||
@ -57,7 +57,7 @@ describe('compactData', () => {
|
||||
'1099': 1,
|
||||
'1100': 0,
|
||||
});
|
||||
expect(orderbookData[orderbookData.length - 1].bidByLevel).toEqual({
|
||||
expect(orderbookRows[orderbookRows.length - 1].bidByLevel).toEqual({
|
||||
'901': 0,
|
||||
'902': 1,
|
||||
'903': 2,
|
||||
@ -66,17 +66,17 @@ describe('compactData', () => {
|
||||
});
|
||||
|
||||
it('updates relative data', () => {
|
||||
const orderbookData = compactData(sell, buy, 10);
|
||||
expect(orderbookData[0].cumulativeVol.relativeAsk).toEqual(100);
|
||||
expect(orderbookData[0].cumulativeVol.relativeBid).toEqual(0);
|
||||
expect(orderbookData[0].relativeAskVol).toEqual(2);
|
||||
expect(orderbookData[0].relativeBidVol).toEqual(0);
|
||||
expect(orderbookData[10].cumulativeVol.relativeAsk).toEqual(8);
|
||||
expect(orderbookData[10].cumulativeVol.relativeBid).toEqual(12);
|
||||
expect(orderbookData[10].relativeAskVol).toEqual(44);
|
||||
expect(orderbookData[10].relativeBidVol).toEqual(66);
|
||||
expect(orderbookData[orderbookData.length - 1].relativeAskVol).toEqual(0);
|
||||
expect(orderbookData[orderbookData.length - 1].relativeBidVol).toEqual(1);
|
||||
const orderbookRows = compactRows(sell, buy, 10);
|
||||
expect(orderbookRows[0].cumulativeVol.relativeAsk).toEqual(100);
|
||||
expect(orderbookRows[0].cumulativeVol.relativeBid).toEqual(0);
|
||||
expect(orderbookRows[0].relativeAsk).toEqual(2);
|
||||
expect(orderbookRows[0].relativeBid).toEqual(0);
|
||||
expect(orderbookRows[10].cumulativeVol.relativeAsk).toEqual(8);
|
||||
expect(orderbookRows[10].cumulativeVol.relativeBid).toEqual(12);
|
||||
expect(orderbookRows[10].relativeAsk).toEqual(44);
|
||||
expect(orderbookRows[10].relativeBid).toEqual(64);
|
||||
expect(orderbookRows[orderbookRows.length - 1].relativeAsk).toEqual(0);
|
||||
expect(orderbookRows[orderbookRows.length - 1].relativeBid).toEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
@ -137,8 +137,8 @@ describe('updateLevels', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateCompactedData', () => {
|
||||
const orderbookData: OrderbookData[] = [
|
||||
describe('updateCompactedRows', () => {
|
||||
const orderbookRows: OrderbookRowData[] = [
|
||||
{
|
||||
price: '120',
|
||||
cumulativeVol: {
|
||||
@ -153,8 +153,8 @@ describe('updateCompactedData', () => {
|
||||
bidByLevel: {},
|
||||
ask: 10,
|
||||
bid: 0,
|
||||
relativeAskVol: 25,
|
||||
relativeBidVol: 0,
|
||||
relativeAsk: 25,
|
||||
relativeBid: 0,
|
||||
},
|
||||
{
|
||||
price: '100',
|
||||
@ -174,8 +174,8 @@ describe('updateCompactedData', () => {
|
||||
},
|
||||
ask: 40,
|
||||
bid: 40,
|
||||
relativeAskVol: 100,
|
||||
relativeBidVol: 100,
|
||||
relativeAsk: 100,
|
||||
relativeBid: 100,
|
||||
},
|
||||
{
|
||||
price: '80',
|
||||
@ -191,8 +191,8 @@ describe('updateCompactedData', () => {
|
||||
},
|
||||
ask: 0,
|
||||
bid: 10,
|
||||
relativeAskVol: 0,
|
||||
relativeBidVol: 25,
|
||||
relativeAsk: 0,
|
||||
relativeBid: 25,
|
||||
},
|
||||
];
|
||||
const resolution = 10;
|
||||
@ -210,18 +210,18 @@ describe('updateCompactedData', () => {
|
||||
volume: '10',
|
||||
numberOfOrders: '10',
|
||||
};
|
||||
const updatedData = updateCompactedData(
|
||||
orderbookData,
|
||||
const updatedRows = updateCompactedRows(
|
||||
orderbookRows,
|
||||
[sell],
|
||||
[buy],
|
||||
resolution
|
||||
);
|
||||
expect(updatedData[0].ask).toEqual(20);
|
||||
expect(updatedData[0].askByLevel?.[120]).toEqual(10);
|
||||
expect(updatedData[0].cumulativeVol.ask).toEqual(60);
|
||||
expect(updatedData[2].bid).toEqual(20);
|
||||
expect(updatedData[2].bidByLevel?.[80]).toEqual(10);
|
||||
expect(updatedData[2].cumulativeVol.bid).toEqual(60);
|
||||
expect(updatedRows[0].ask).toEqual(20);
|
||||
expect(updatedRows[0].askByLevel?.[120]).toEqual(10);
|
||||
expect(updatedRows[0].cumulativeVol.ask).toEqual(60);
|
||||
expect(updatedRows[2].bid).toEqual(20);
|
||||
expect(updatedRows[2].bidByLevel?.[80]).toEqual(10);
|
||||
expect(updatedRows[2].cumulativeVol.bid).toEqual(60);
|
||||
});
|
||||
|
||||
it('remove row', () => {
|
||||
@ -237,13 +237,13 @@ describe('updateCompactedData', () => {
|
||||
volume: '0',
|
||||
numberOfOrders: '0',
|
||||
};
|
||||
const updatedData = updateCompactedData(
|
||||
orderbookData,
|
||||
const updatedRows = updateCompactedRows(
|
||||
orderbookRows,
|
||||
[sell],
|
||||
[buy],
|
||||
resolution
|
||||
);
|
||||
expect(updatedData.length).toEqual(1);
|
||||
expect(updatedRows.length).toEqual(1);
|
||||
});
|
||||
|
||||
it('add new row at the end', () => {
|
||||
@ -259,17 +259,17 @@ describe('updateCompactedData', () => {
|
||||
volume: '5',
|
||||
numberOfOrders: '5',
|
||||
};
|
||||
const updatedData = updateCompactedData(
|
||||
orderbookData,
|
||||
const updatedRows = updateCompactedRows(
|
||||
orderbookRows,
|
||||
[sell],
|
||||
[buy],
|
||||
resolution
|
||||
);
|
||||
expect(updatedData.length).toEqual(5);
|
||||
expect(updatedData[0].price).toEqual('130');
|
||||
expect(updatedData[0].cumulativeVol.ask).toEqual(55);
|
||||
expect(updatedData[4].price).toEqual('60');
|
||||
expect(updatedData[4].cumulativeVol.bid).toEqual(55);
|
||||
expect(updatedRows.length).toEqual(5);
|
||||
expect(updatedRows[0].price).toEqual('130');
|
||||
expect(updatedRows[0].cumulativeVol.ask).toEqual(55);
|
||||
expect(updatedRows[4].price).toEqual('60');
|
||||
expect(updatedRows[4].cumulativeVol.bid).toEqual(55);
|
||||
});
|
||||
|
||||
it('add new row in the middle', () => {
|
||||
@ -285,18 +285,18 @@ describe('updateCompactedData', () => {
|
||||
volume: '5',
|
||||
numberOfOrders: '5',
|
||||
};
|
||||
const updatedData = updateCompactedData(
|
||||
orderbookData,
|
||||
const updatedRows = updateCompactedRows(
|
||||
orderbookRows,
|
||||
[sell],
|
||||
[buy],
|
||||
resolution
|
||||
);
|
||||
expect(updatedData.length).toEqual(5);
|
||||
expect(updatedData[1].price).toEqual('110');
|
||||
expect(updatedData[1].cumulativeVol.ask).toEqual(45);
|
||||
expect(updatedData[0].cumulativeVol.ask).toEqual(55);
|
||||
expect(updatedData[3].price).toEqual('90');
|
||||
expect(updatedData[3].cumulativeVol.bid).toEqual(45);
|
||||
expect(updatedData[4].cumulativeVol.bid).toEqual(55);
|
||||
expect(updatedRows.length).toEqual(5);
|
||||
expect(updatedRows[1].price).toEqual('110');
|
||||
expect(updatedRows[1].cumulativeVol.ask).toEqual(45);
|
||||
expect(updatedRows[0].cumulativeVol.ask).toEqual(55);
|
||||
expect(updatedRows[3].price).toEqual('90');
|
||||
expect(updatedRows[3].cumulativeVol.bid).toEqual(45);
|
||||
expect(updatedRows[4].cumulativeVol.bid).toEqual(55);
|
||||
});
|
||||
});
|
||||
|
@ -1,12 +1,16 @@
|
||||
import produce from 'immer';
|
||||
import groupBy from 'lodash/groupBy';
|
||||
import { VolumeType } from '@vegaprotocol/react-helpers';
|
||||
import { MarketTradingMode } from '@vegaprotocol/types';
|
||||
import type {
|
||||
MarketDepth_market_depth_sell,
|
||||
MarketDepth_market_depth_buy,
|
||||
MarketDepth_market_data,
|
||||
} from './__generated__/MarketDepth';
|
||||
import type {
|
||||
MarketDepthSubscription_marketDepthUpdate_sell,
|
||||
MarketDepthSubscription_marketDepthUpdate_buy,
|
||||
MarketDepthSubscription_marketDepthUpdate_market_data,
|
||||
} from './__generated__/MarketDepthSubscription';
|
||||
|
||||
export interface CumulativeVol {
|
||||
@ -16,28 +20,32 @@ export interface CumulativeVol {
|
||||
relativeAsk?: number;
|
||||
}
|
||||
|
||||
export interface OrderbookData {
|
||||
export interface OrderbookRowData {
|
||||
price: string;
|
||||
bid: number;
|
||||
bidByLevel: Record<string, number>;
|
||||
relativeBidVol?: number;
|
||||
relativeBid?: number;
|
||||
ask: number;
|
||||
askByLevel: Record<string, number>;
|
||||
relativeAskVol?: number;
|
||||
relativeAsk?: number;
|
||||
cumulativeVol: CumulativeVol;
|
||||
}
|
||||
|
||||
const getGroupPrice = (price: string, resolution: number) => {
|
||||
export type OrderbookData = Partial<
|
||||
Omit<MarketDepth_market_data, '__typename' | 'market'>
|
||||
> & { rows: OrderbookRowData[] | null };
|
||||
|
||||
export const getPriceLevel = (price: string | bigint, resolution: number) => {
|
||||
const p = BigInt(price);
|
||||
const r = BigInt(resolution);
|
||||
let groupPrice = (p / r) * r;
|
||||
if (p - groupPrice >= resolution / 2) {
|
||||
groupPrice += BigInt(resolution);
|
||||
let priceLevel = (p / r) * r;
|
||||
if (p - priceLevel >= resolution / 2) {
|
||||
priceLevel += BigInt(resolution);
|
||||
}
|
||||
return groupPrice.toString();
|
||||
return priceLevel.toString();
|
||||
};
|
||||
|
||||
const getMaxVolumes = (orderbookData: OrderbookData[]) => ({
|
||||
const getMaxVolumes = (orderbookData: OrderbookRowData[]) => ({
|
||||
bid: Math.max(...orderbookData.map((data) => data.bid)),
|
||||
ask: Math.max(...orderbookData.map((data) => data.ask)),
|
||||
cumulativeVol: Math.max(
|
||||
@ -50,13 +58,14 @@ const getMaxVolumes = (orderbookData: OrderbookData[]) => ({
|
||||
const toPercentValue = (value?: number) => Math.ceil((value ?? 0) * 100);
|
||||
|
||||
/**
|
||||
* @summary Updates relativeAskVol, relativeBidVol, cumulativeVol.relativeAsk, cumulativeVol.relativeBid
|
||||
* @summary Updates relativeAsk, relativeBid, cumulativeVol.relativeAsk, cumulativeVol.relativeBid
|
||||
*/
|
||||
const updateRelativeData = (data: OrderbookData[]) => {
|
||||
const updateRelativeData = (data: OrderbookRowData[]) => {
|
||||
const { bid, ask, cumulativeVol } = getMaxVolumes(data);
|
||||
const maxBidAsk = Math.max(bid, ask);
|
||||
data.forEach((data) => {
|
||||
data.relativeAskVol = toPercentValue(data.ask / ask);
|
||||
data.relativeBidVol = toPercentValue(data.bid / bid);
|
||||
data.relativeAsk = toPercentValue(data.ask / maxBidAsk);
|
||||
data.relativeBid = toPercentValue(data.bid / maxBidAsk);
|
||||
data.cumulativeVol.relativeAsk = toPercentValue(
|
||||
data.cumulativeVol.ask / cumulativeVol
|
||||
);
|
||||
@ -66,37 +75,37 @@ const updateRelativeData = (data: OrderbookData[]) => {
|
||||
});
|
||||
};
|
||||
|
||||
const createData = (
|
||||
export const createRow = (
|
||||
price: string,
|
||||
volume = 0,
|
||||
dataType?: 'sell' | 'buy'
|
||||
): OrderbookData => ({
|
||||
dataType?: VolumeType
|
||||
): OrderbookRowData => ({
|
||||
price,
|
||||
ask: dataType === 'sell' ? volume : 0,
|
||||
bid: dataType === 'buy' ? volume : 0,
|
||||
ask: dataType === VolumeType.ask ? volume : 0,
|
||||
bid: dataType === VolumeType.bid ? volume : 0,
|
||||
cumulativeVol: {
|
||||
ask: dataType === 'sell' ? volume : 0,
|
||||
bid: dataType === 'buy' ? volume : 0,
|
||||
ask: dataType === VolumeType.ask ? volume : 0,
|
||||
bid: dataType === VolumeType.bid ? volume : 0,
|
||||
},
|
||||
askByLevel: dataType === 'sell' ? { [price]: volume } : {},
|
||||
bidByLevel: dataType === 'buy' ? { [price]: volume } : {},
|
||||
askByLevel: dataType === VolumeType.ask ? { [price]: volume } : {},
|
||||
bidByLevel: dataType === VolumeType.bid ? { [price]: volume } : {},
|
||||
});
|
||||
|
||||
const mapRawData =
|
||||
(dataType: 'sell' | 'buy') =>
|
||||
(dataType: VolumeType.ask | VolumeType.bid) =>
|
||||
(
|
||||
data:
|
||||
| MarketDepth_market_depth_sell
|
||||
| MarketDepthSubscription_marketDepthUpdate_sell
|
||||
| MarketDepth_market_depth_buy
|
||||
| MarketDepthSubscription_marketDepthUpdate_buy
|
||||
): OrderbookData =>
|
||||
createData(data.price, Number(data.volume), dataType);
|
||||
): OrderbookRowData =>
|
||||
createRow(data.price, Number(data.volume), dataType);
|
||||
|
||||
/**
|
||||
* @summary merges sell amd buy data, orders by price desc, group by price level, counts cumulative and relative values
|
||||
*/
|
||||
export const compactData = (
|
||||
export const compactRows = (
|
||||
sell:
|
||||
| (
|
||||
| MarketDepth_market_depth_sell
|
||||
@ -112,25 +121,25 @@ export const compactData = (
|
||||
resolution: number
|
||||
) => {
|
||||
// map raw sell data to OrderbookData
|
||||
const askOrderbookData = [...(sell ?? [])].map<OrderbookData>(
|
||||
mapRawData('sell')
|
||||
const askOrderbookData = [...(sell ?? [])].map<OrderbookRowData>(
|
||||
mapRawData(VolumeType.ask)
|
||||
);
|
||||
// map raw buy data to OrderbookData
|
||||
const bidOrderbookData = [...(buy ?? [])].map<OrderbookData>(
|
||||
mapRawData('buy')
|
||||
const bidOrderbookData = [...(buy ?? [])].map<OrderbookRowData>(
|
||||
mapRawData(VolumeType.bid)
|
||||
);
|
||||
|
||||
// group by price level
|
||||
const groupedByLevel = groupBy<OrderbookData>(
|
||||
const groupedByLevel = groupBy<OrderbookRowData>(
|
||||
[...askOrderbookData, ...bidOrderbookData],
|
||||
(row) => getGroupPrice(row.price, resolution)
|
||||
(row) => getPriceLevel(row.price, resolution)
|
||||
);
|
||||
|
||||
// create single OrderbookData from grouped OrderbookData[], sum volumes and atore volume by level
|
||||
const orderbookData = Object.keys(groupedByLevel).reduce<OrderbookData[]>(
|
||||
const orderbookData = Object.keys(groupedByLevel).reduce<OrderbookRowData[]>(
|
||||
(rows, price) =>
|
||||
rows.concat(
|
||||
groupedByLevel[price].reduce<OrderbookData>(
|
||||
groupedByLevel[price].reduce<OrderbookRowData>(
|
||||
(a, c) => ({
|
||||
...a,
|
||||
ask: a.ask + c.ask,
|
||||
@ -138,7 +147,7 @@ export const compactData = (
|
||||
bid: (a.bid ?? 0) + (c.bid ?? 0),
|
||||
bidByLevel: Object.assign(a.bidByLevel, c.bidByLevel),
|
||||
}),
|
||||
createData(price)
|
||||
createRow(price)
|
||||
)
|
||||
),
|
||||
[]
|
||||
@ -175,9 +184,9 @@ export const compactData = (
|
||||
* @param modifiedIndex
|
||||
* @returns max (sell) or min (buy) modified index in draft data, mutates draft
|
||||
*/
|
||||
const partiallyUpdateCompactedData = (
|
||||
dataType: 'sell' | 'buy',
|
||||
draft: OrderbookData[],
|
||||
const partiallyUpdateCompactedRows = (
|
||||
dataType: VolumeType,
|
||||
draft: OrderbookRowData[],
|
||||
delta:
|
||||
| MarketDepthSubscription_marketDepthUpdate_sell
|
||||
| MarketDepthSubscription_marketDepthUpdate_buy,
|
||||
@ -186,26 +195,27 @@ const partiallyUpdateCompactedData = (
|
||||
) => {
|
||||
const { price } = delta;
|
||||
const volume = Number(delta.volume);
|
||||
const groupPrice = getGroupPrice(price, resolution);
|
||||
const volKey = dataType === 'sell' ? 'ask' : 'bid';
|
||||
const oppositeVolKey = dataType === 'sell' ? 'bid' : 'ask';
|
||||
const volByLevelKey = dataType === 'sell' ? 'askByLevel' : 'bidByLevel';
|
||||
const resolveModifiedIndex = dataType === 'sell' ? Math.max : Math.min;
|
||||
let index = draft.findIndex((data) => data.price === groupPrice);
|
||||
const priceLevel = getPriceLevel(price, resolution);
|
||||
const isAskDataType = dataType === VolumeType.ask;
|
||||
const volKey = isAskDataType ? 'ask' : 'bid';
|
||||
const oppositeVolKey = isAskDataType ? 'bid' : 'ask';
|
||||
const volByLevelKey = isAskDataType ? 'askByLevel' : 'bidByLevel';
|
||||
const resolveModifiedIndex = isAskDataType ? Math.max : Math.min;
|
||||
let index = draft.findIndex((data) => data.price === priceLevel);
|
||||
if (index !== -1) {
|
||||
modifiedIndex = resolveModifiedIndex(modifiedIndex, index);
|
||||
draft[index][volKey] =
|
||||
draft[index][volKey] - (draft[index][volByLevelKey][price] || 0) + volume;
|
||||
draft[index][volByLevelKey][price] = volume;
|
||||
} else {
|
||||
const newData: OrderbookData = createData(groupPrice, volume, dataType);
|
||||
index = draft.findIndex((data) => BigInt(data.price) < BigInt(groupPrice));
|
||||
const newData: OrderbookRowData = createRow(priceLevel, volume, dataType);
|
||||
index = draft.findIndex((data) => BigInt(data.price) < BigInt(priceLevel));
|
||||
if (index !== -1) {
|
||||
draft.splice(index, 0, newData);
|
||||
newData.cumulativeVol[oppositeVolKey] =
|
||||
draft[index + (groupPrice === 'sell' ? -1 : 1)].cumulativeVol[
|
||||
draft[index + (isAskDataType ? -1 : 1)]?.cumulativeVol[
|
||||
oppositeVolKey
|
||||
];
|
||||
] ?? 0;
|
||||
modifiedIndex = resolveModifiedIndex(modifiedIndex, index);
|
||||
} else {
|
||||
draft.push(newData);
|
||||
@ -218,35 +228,35 @@ const partiallyUpdateCompactedData = (
|
||||
/**
|
||||
* Updates OrderbookData[] with new data received from subscription - mutates input
|
||||
*
|
||||
* @param orderbookData
|
||||
* @param rows
|
||||
* @param sell
|
||||
* @param buy
|
||||
* @param resolution
|
||||
* @returns void
|
||||
*/
|
||||
export const updateCompactedData = (
|
||||
orderbookData: OrderbookData[],
|
||||
export const updateCompactedRows = (
|
||||
rows: OrderbookRowData[],
|
||||
sell: MarketDepthSubscription_marketDepthUpdate_sell[] | null,
|
||||
buy: MarketDepthSubscription_marketDepthUpdate_buy[] | null,
|
||||
resolution: number
|
||||
) =>
|
||||
produce(orderbookData, (draft) => {
|
||||
produce(rows, (draft) => {
|
||||
let sellModifiedIndex = -1;
|
||||
sell?.forEach((buy) => {
|
||||
sellModifiedIndex = partiallyUpdateCompactedData(
|
||||
'sell',
|
||||
sell?.forEach((delta) => {
|
||||
sellModifiedIndex = partiallyUpdateCompactedRows(
|
||||
VolumeType.ask,
|
||||
draft,
|
||||
buy,
|
||||
delta,
|
||||
resolution,
|
||||
sellModifiedIndex
|
||||
);
|
||||
});
|
||||
let buyModifiedIndex = draft.length;
|
||||
buy?.forEach((sell) => {
|
||||
buyModifiedIndex = partiallyUpdateCompactedData(
|
||||
'buy',
|
||||
buy?.forEach((delta) => {
|
||||
buyModifiedIndex = partiallyUpdateCompactedRows(
|
||||
VolumeType.bid,
|
||||
draft,
|
||||
sell,
|
||||
delta,
|
||||
resolution,
|
||||
buyModifiedIndex
|
||||
);
|
||||
@ -283,6 +293,25 @@ export const updateCompactedData = (
|
||||
updateRelativeData(draft);
|
||||
});
|
||||
|
||||
export const mapMarketData = (
|
||||
data:
|
||||
| MarketDepth_market_data
|
||||
| MarketDepthSubscription_marketDepthUpdate_market_data
|
||||
| null,
|
||||
resolution: number
|
||||
) => ({
|
||||
staticMidPrice:
|
||||
data?.staticMidPrice && getPriceLevel(data?.staticMidPrice, resolution),
|
||||
bestStaticBidPrice:
|
||||
data?.bestStaticBidPrice &&
|
||||
getPriceLevel(data?.bestStaticBidPrice, resolution),
|
||||
bestStaticOfferPrice:
|
||||
data?.bestStaticOfferPrice &&
|
||||
getPriceLevel(data?.bestStaticOfferPrice, resolution),
|
||||
indicativePrice:
|
||||
data?.indicativePrice && getPriceLevel(data?.indicativePrice, resolution),
|
||||
});
|
||||
|
||||
/**
|
||||
* Updates raw data with new data received from subscription - mutates input
|
||||
* @param levels
|
||||
@ -317,3 +346,69 @@ export const updateLevels = (
|
||||
});
|
||||
return levels;
|
||||
};
|
||||
|
||||
export interface MockDataGeneratorParams {
|
||||
numberOfSellRows: number;
|
||||
numberOfBuyRows: number;
|
||||
overlap: number;
|
||||
midPrice: number;
|
||||
bestStaticBidPrice: number;
|
||||
bestStaticOfferPrice: number;
|
||||
indicativePrice?: number;
|
||||
indicativeVolume?: number;
|
||||
resolution: number;
|
||||
}
|
||||
|
||||
export const generateMockData = ({
|
||||
numberOfSellRows,
|
||||
numberOfBuyRows,
|
||||
midPrice,
|
||||
overlap,
|
||||
bestStaticBidPrice,
|
||||
bestStaticOfferPrice,
|
||||
indicativePrice,
|
||||
indicativeVolume,
|
||||
resolution,
|
||||
}: MockDataGeneratorParams) => {
|
||||
let matrix = new Array(numberOfSellRows).fill(undefined);
|
||||
let price = midPrice + (numberOfSellRows - Math.ceil(overlap / 2) + 1);
|
||||
const sell: MarketDepth_market_depth_sell[] = matrix.map((row, i) => ({
|
||||
__typename: 'PriceLevel',
|
||||
price: (price -= 1).toString(),
|
||||
volume: (numberOfSellRows - i + 1).toString(),
|
||||
numberOfOrders: '',
|
||||
}));
|
||||
price += overlap;
|
||||
matrix = new Array(numberOfBuyRows).fill(undefined);
|
||||
const buy: MarketDepth_market_depth_buy[] = matrix.map((row, i) => ({
|
||||
__typename: 'PriceLevel',
|
||||
price: (price -= 1).toString(),
|
||||
volume: (i + 2).toString(),
|
||||
numberOfOrders: '',
|
||||
}));
|
||||
const rows = compactRows(sell, buy, resolution);
|
||||
return {
|
||||
rows,
|
||||
resolution,
|
||||
indicativeVolume: indicativeVolume?.toString(),
|
||||
...mapMarketData(
|
||||
{
|
||||
__typename: 'MarketData',
|
||||
staticMidPrice: '',
|
||||
marketTradingMode:
|
||||
overlap > 0
|
||||
? MarketTradingMode.BatchAuction
|
||||
: MarketTradingMode.Continuous,
|
||||
bestStaticBidPrice: bestStaticBidPrice.toString(),
|
||||
bestStaticOfferPrice: bestStaticOfferPrice.toString(),
|
||||
indicativePrice: indicativePrice?.toString() ?? '',
|
||||
indicativeVolume: indicativeVolume?.toString() ?? '',
|
||||
market: {
|
||||
__typename: 'Market',
|
||||
id: '',
|
||||
},
|
||||
},
|
||||
resolution
|
||||
),
|
||||
};
|
||||
};
|
||||
|
@ -1,44 +1,54 @@
|
||||
import throttle from 'lodash/throttle';
|
||||
import produce from 'immer';
|
||||
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
|
||||
import { Orderbook } from './orderbook';
|
||||
import { useDataProvider } from '@vegaprotocol/react-helpers';
|
||||
import { marketDepthDataProvider } from './market-depth-data-provider';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import type { MarketDepthSubscription_marketDepthUpdate } from './__generated__/MarketDepthSubscription';
|
||||
import { compactData, updateCompactedData } from './orderbook-data';
|
||||
import {
|
||||
compactRows,
|
||||
updateCompactedRows,
|
||||
mapMarketData,
|
||||
} from './orderbook-data';
|
||||
import type { OrderbookData } from './orderbook-data';
|
||||
|
||||
interface OrderbookManagerProps {
|
||||
marketId: string;
|
||||
resolution: number;
|
||||
}
|
||||
|
||||
export const OrderbookManager = ({
|
||||
marketId,
|
||||
resolution,
|
||||
}: OrderbookManagerProps) => {
|
||||
export const OrderbookManager = ({ marketId }: OrderbookManagerProps) => {
|
||||
const [resolution, setResolution] = useState(1);
|
||||
const variables = useMemo(() => ({ marketId }), [marketId]);
|
||||
const resolutionRef = useRef(resolution);
|
||||
const [orderbookData, setOrderbookData] = useState<OrderbookData[] | null>(
|
||||
null
|
||||
);
|
||||
const dataRef = useRef<OrderbookData[] | null>(null);
|
||||
const [orderbookData, setOrderbookData] = useState<OrderbookData>({
|
||||
rows: null,
|
||||
});
|
||||
const dataRef = useRef<OrderbookData>({ rows: null });
|
||||
const setOrderbookDataThrottled = useRef(throttle(setOrderbookData, 1000));
|
||||
|
||||
const update = useCallback(
|
||||
(delta: MarketDepthSubscription_marketDepthUpdate) => {
|
||||
if (!dataRef.current) {
|
||||
if (!dataRef.current.rows) {
|
||||
return false;
|
||||
}
|
||||
dataRef.current = updateCompactedData(
|
||||
dataRef.current,
|
||||
delta.sell,
|
||||
delta.buy,
|
||||
resolutionRef.current
|
||||
);
|
||||
dataRef.current = produce(dataRef.current, (draft) => {
|
||||
Object.assign(draft, delta.market.data);
|
||||
draft.rows = updateCompactedRows(
|
||||
draft.rows ?? [],
|
||||
delta.sell,
|
||||
delta.buy,
|
||||
resolutionRef.current
|
||||
);
|
||||
Object.assign(
|
||||
draft,
|
||||
mapMarketData(delta.market.data, resolutionRef.current)
|
||||
);
|
||||
});
|
||||
setOrderbookDataThrottled.current(dataRef.current);
|
||||
return true;
|
||||
},
|
||||
// using resolutionRef.current to avoid using resolution as a dependency - it will cause data provider restart on resolution change
|
||||
[]
|
||||
);
|
||||
|
||||
@ -50,11 +60,15 @@ export const OrderbookManager = ({
|
||||
|
||||
useEffect(() => {
|
||||
if (!data) {
|
||||
dataRef.current = null;
|
||||
dataRef.current = { rows: null };
|
||||
setOrderbookData(dataRef.current);
|
||||
return;
|
||||
}
|
||||
dataRef.current = compactData(data.depth.sell, data.depth.buy, resolution);
|
||||
dataRef.current = {
|
||||
...data.data,
|
||||
rows: compactRows(data.depth.sell, data.depth.buy, resolution),
|
||||
...mapMarketData(data.data, resolution),
|
||||
};
|
||||
setOrderbookData(dataRef.current);
|
||||
}, [data, resolution]);
|
||||
|
||||
@ -66,8 +80,10 @@ export const OrderbookManager = ({
|
||||
return (
|
||||
<AsyncRenderer loading={loading} error={error} data={data}>
|
||||
<Orderbook
|
||||
data={orderbookData}
|
||||
{...orderbookData}
|
||||
decimalPlaces={data?.decimalPlaces ?? 0}
|
||||
resolution={resolution}
|
||||
onResolutionChange={(resolution: number) => setResolution(resolution)}
|
||||
/>
|
||||
</AsyncRenderer>
|
||||
);
|
||||
|
@ -4,41 +4,64 @@ import {
|
||||
Vol,
|
||||
CumulativeVol,
|
||||
addDecimalsFormatNumber,
|
||||
VolumeType,
|
||||
} from '@vegaprotocol/react-helpers';
|
||||
|
||||
interface OrderbookRowProps {
|
||||
bid: number;
|
||||
relativeBidVol?: number;
|
||||
price: string;
|
||||
ask: number;
|
||||
relativeAskVol?: number;
|
||||
bid: number;
|
||||
cumulativeAsk?: number;
|
||||
cumulativeBid?: number;
|
||||
cumulativeRelativeAsk?: number;
|
||||
cumulativeRelativeBid?: number;
|
||||
decimalPlaces: number;
|
||||
indicativeVolume?: string;
|
||||
price: string;
|
||||
relativeAsk?: number;
|
||||
relativeBid?: number;
|
||||
}
|
||||
|
||||
export const OrderbookRow = React.memo(
|
||||
({
|
||||
bid,
|
||||
relativeBidVol,
|
||||
price,
|
||||
ask,
|
||||
relativeAskVol,
|
||||
decimalPlaces,
|
||||
bid,
|
||||
cumulativeAsk,
|
||||
cumulativeBid,
|
||||
cumulativeRelativeAsk,
|
||||
cumulativeRelativeBid,
|
||||
decimalPlaces,
|
||||
indicativeVolume,
|
||||
price,
|
||||
relativeAsk,
|
||||
relativeBid,
|
||||
}: OrderbookRowProps) => {
|
||||
return (
|
||||
<>
|
||||
<Vol value={bid} relativeValue={relativeBidVol} type="bid" />
|
||||
<Vol
|
||||
testId={`bid-vol-${price}`}
|
||||
value={bid}
|
||||
relativeValue={relativeBid}
|
||||
type={VolumeType.bid}
|
||||
/>
|
||||
<PriceCell
|
||||
testId={`price-${price}`}
|
||||
value={BigInt(price)}
|
||||
valueFormatted={addDecimalsFormatNumber(price, decimalPlaces)}
|
||||
/>
|
||||
<Vol value={ask} relativeValue={relativeAskVol} type="ask" />
|
||||
<Vol
|
||||
testId={`ask-vol-${price}`}
|
||||
value={ask}
|
||||
relativeValue={relativeAsk}
|
||||
type={VolumeType.ask}
|
||||
/>
|
||||
<CumulativeVol
|
||||
testId={`cumulative-vol-${price}`}
|
||||
bid={cumulativeBid}
|
||||
ask={cumulativeAsk}
|
||||
relativeAsk={cumulativeRelativeAsk}
|
||||
relativeBid={cumulativeRelativeBid}
|
||||
indicativeVolume={indicativeVolume}
|
||||
className="pr-4"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
22
libs/market-depth/src/lib/orderbook.module.scss
Normal file
22
libs/market-depth/src/lib/orderbook.module.scss
Normal file
@ -0,0 +1,22 @@
|
||||
$scrollbar-width: 6px;
|
||||
|
||||
/* Works on Firefox */
|
||||
.scroll {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: #999 #333;
|
||||
}
|
||||
|
||||
/* Works on Chrome, Edge, and Safari */
|
||||
.scroll::-webkit-scrollbar {
|
||||
width: $scrollbar-width;
|
||||
background-color: #999;
|
||||
}
|
||||
|
||||
.scroll::-webkit-scrollbar-thumb {
|
||||
width: $scrollbar-width;
|
||||
background-color: #333;
|
||||
}
|
||||
.scroll::-webkit-scrollbar-track {
|
||||
box-shadow: inset 0 0 #{$scrollbar-width} rgb(0 0 0 / 30%);
|
||||
background-color: #999;
|
||||
}
|
@ -1,10 +1,165 @@
|
||||
import { render } from '@testing-library/react';
|
||||
|
||||
import Orderbook from './orderbook';
|
||||
import { render, fireEvent, waitFor, screen } from '@testing-library/react';
|
||||
import { generateMockData } from './orderbook-data';
|
||||
import { Orderbook, rowHeight } from './orderbook';
|
||||
|
||||
describe('Orderbook', () => {
|
||||
it('should render successfully', () => {
|
||||
const { baseElement } = render(<Orderbook data={null} decimalPlaces={4} />);
|
||||
expect(baseElement).toBeTruthy();
|
||||
const params = {
|
||||
numberOfSellRows: 100,
|
||||
numberOfBuyRows: 100,
|
||||
midPrice: 122900,
|
||||
bestStaticBidPrice: 122905,
|
||||
bestStaticOfferPrice: 122895,
|
||||
decimalPlaces: 3,
|
||||
overlap: 10,
|
||||
indicativePrice: 122900,
|
||||
indicativeVolume: 11,
|
||||
resolution: 1,
|
||||
};
|
||||
const onResolutionChange = jest.fn();
|
||||
const decimalPlaces = 3;
|
||||
it('should scroll to mid price on init', async () => {
|
||||
window.innerHeight = 11 * rowHeight;
|
||||
const result = render(
|
||||
<Orderbook
|
||||
decimalPlaces={decimalPlaces}
|
||||
{...generateMockData(params)}
|
||||
onResolutionChange={onResolutionChange}
|
||||
/>
|
||||
);
|
||||
await waitFor(() => screen.getByTestId(`bid-vol-${params.midPrice}`));
|
||||
expect(result.getByTestId('scroll').scrollTop).toBe(91 * rowHeight);
|
||||
});
|
||||
|
||||
it('should keep mid price row in the middle', async () => {
|
||||
window.innerHeight = 11 * rowHeight;
|
||||
const result = render(
|
||||
<Orderbook
|
||||
decimalPlaces={decimalPlaces}
|
||||
{...generateMockData(params)}
|
||||
onResolutionChange={onResolutionChange}
|
||||
/>
|
||||
);
|
||||
await waitFor(() => screen.getByTestId(`bid-vol-${params.midPrice}`));
|
||||
expect(result.getByTestId('scroll').scrollTop).toBe(91 * rowHeight);
|
||||
result.rerender(
|
||||
<Orderbook
|
||||
decimalPlaces={decimalPlaces}
|
||||
{...generateMockData({
|
||||
...params,
|
||||
numberOfSellRows: params.numberOfSellRows - 1,
|
||||
})}
|
||||
onResolutionChange={onResolutionChange}
|
||||
/>
|
||||
);
|
||||
await waitFor(() => screen.getByTestId(`bid-vol-${params.midPrice}`));
|
||||
expect(result.getByTestId('scroll').scrollTop).toBe(90 * rowHeight);
|
||||
});
|
||||
|
||||
it('should scroll to mid price when it will change', async () => {
|
||||
window.innerHeight = 11 * rowHeight;
|
||||
const result = render(
|
||||
<Orderbook
|
||||
decimalPlaces={decimalPlaces}
|
||||
{...generateMockData(params)}
|
||||
onResolutionChange={onResolutionChange}
|
||||
/>
|
||||
);
|
||||
await waitFor(() => screen.getByTestId(`bid-vol-${params.midPrice}`));
|
||||
expect(result.getByTestId('scroll').scrollTop).toBe(91 * rowHeight);
|
||||
result.rerender(
|
||||
<Orderbook
|
||||
decimalPlaces={decimalPlaces}
|
||||
{...generateMockData({
|
||||
...params,
|
||||
bestStaticBidPrice: params.bestStaticBidPrice + 1,
|
||||
bestStaticOfferPrice: params.bestStaticOfferPrice + 1,
|
||||
})}
|
||||
onResolutionChange={onResolutionChange}
|
||||
/>
|
||||
);
|
||||
await waitFor(() => screen.getByTestId(`bid-vol-${params.midPrice}`));
|
||||
expect(result.getByTestId('scroll').scrollTop).toBe(90 * rowHeight);
|
||||
});
|
||||
|
||||
it('should should keep price it the middle', async () => {
|
||||
window.innerHeight = 11 * rowHeight;
|
||||
const result = render(
|
||||
<Orderbook
|
||||
decimalPlaces={decimalPlaces}
|
||||
{...generateMockData(params)}
|
||||
onResolutionChange={onResolutionChange}
|
||||
/>
|
||||
);
|
||||
await waitFor(() => screen.getByTestId(`bid-vol-${params.midPrice}`));
|
||||
const scrollElement = result.getByTestId('scroll');
|
||||
expect(scrollElement.scrollTop).toBe(91 * rowHeight);
|
||||
scrollElement.scrollTop = 92 * rowHeight;
|
||||
fireEvent.scroll(scrollElement);
|
||||
result.rerender(
|
||||
<Orderbook
|
||||
decimalPlaces={decimalPlaces}
|
||||
{...generateMockData({
|
||||
...params,
|
||||
numberOfSellRows: params.numberOfSellRows - 1,
|
||||
})}
|
||||
onResolutionChange={onResolutionChange}
|
||||
/>
|
||||
);
|
||||
await waitFor(() => screen.getByTestId(`bid-vol-${params.midPrice}`));
|
||||
expect(result.getByTestId('scroll').scrollTop).toBe(91 * rowHeight);
|
||||
});
|
||||
|
||||
it('should should get back to mid price on click', async () => {
|
||||
window.innerHeight = 11 * rowHeight;
|
||||
const result = render(
|
||||
<Orderbook
|
||||
decimalPlaces={decimalPlaces}
|
||||
{...generateMockData(params)}
|
||||
onResolutionChange={onResolutionChange}
|
||||
/>
|
||||
);
|
||||
await waitFor(() => screen.getByTestId(`bid-vol-${params.midPrice}`));
|
||||
const scrollElement = result.getByTestId('scroll');
|
||||
expect(scrollElement.scrollTop).toBe(91 * rowHeight);
|
||||
scrollElement.scrollTop = 0;
|
||||
fireEvent.scroll(scrollElement);
|
||||
expect(result.getByTestId('scroll').scrollTop).toBe(0);
|
||||
const scrollToMidPriceButton = result.getByTestId('scroll-to-midprice');
|
||||
fireEvent.click(scrollToMidPriceButton);
|
||||
expect(result.getByTestId('scroll').scrollTop).toBe(91 * rowHeight);
|
||||
});
|
||||
|
||||
it('should should get back to mid price on resolution change', async () => {
|
||||
window.innerHeight = 11 * rowHeight;
|
||||
const result = render(
|
||||
<Orderbook
|
||||
decimalPlaces={decimalPlaces}
|
||||
{...generateMockData(params)}
|
||||
onResolutionChange={onResolutionChange}
|
||||
/>
|
||||
);
|
||||
await waitFor(() => screen.getByTestId(`bid-vol-${params.midPrice}`));
|
||||
const scrollElement = result.getByTestId('scroll');
|
||||
expect(scrollElement.scrollTop).toBe(91 * rowHeight);
|
||||
scrollElement.scrollTop = 0;
|
||||
fireEvent.scroll(scrollElement);
|
||||
expect(result.getByTestId('scroll').scrollTop).toBe(0);
|
||||
const resolutionSelect = result.getByTestId(
|
||||
'resolution'
|
||||
) as HTMLSelectElement;
|
||||
fireEvent.change(resolutionSelect, { target: { value: '10' } });
|
||||
expect(onResolutionChange.mock.calls.length).toBe(1);
|
||||
expect(onResolutionChange.mock.calls[0][0]).toBe(10);
|
||||
result.rerender(
|
||||
<Orderbook
|
||||
decimalPlaces={decimalPlaces}
|
||||
{...generateMockData({
|
||||
...params,
|
||||
resolution: 10,
|
||||
})}
|
||||
onResolutionChange={onResolutionChange}
|
||||
/>
|
||||
);
|
||||
expect(result.getByTestId('scroll').scrollTop).toBe(5 * rowHeight);
|
||||
});
|
||||
});
|
||||
|
71
libs/market-depth/src/lib/orderbook.stories.tsx
Normal file
71
libs/market-depth/src/lib/orderbook.stories.tsx
Normal file
@ -0,0 +1,71 @@
|
||||
import type { Story, Meta } from '@storybook/react';
|
||||
import { generateMockData } from './orderbook-data';
|
||||
import type { MockDataGeneratorParams } from './orderbook-data';
|
||||
import { Orderbook } from './orderbook';
|
||||
import { useState } from 'react';
|
||||
|
||||
type Props = Omit<MockDataGeneratorParams, 'resolution'> & {
|
||||
decimalPlaces: number;
|
||||
};
|
||||
|
||||
const OrderbokMockDataProvider = ({ decimalPlaces, ...props }: Props) => {
|
||||
const [resolution, setResolution] = useState(1);
|
||||
return (
|
||||
<div className="absolute inset-0 dark:bg-black dark:text-white-60 bg-white text-black-60">
|
||||
<div
|
||||
className="absolute left-0 top-0 bottom-0"
|
||||
style={{ width: '400px' }}
|
||||
>
|
||||
<Orderbook
|
||||
onResolutionChange={setResolution}
|
||||
decimalPlaces={decimalPlaces}
|
||||
{...generateMockData({ ...props, resolution })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default {
|
||||
component: OrderbokMockDataProvider,
|
||||
title: 'Orderbook',
|
||||
} as Meta;
|
||||
|
||||
const Template: Story<Props> = (args) => <OrderbokMockDataProvider {...args} />;
|
||||
|
||||
export const Continuous = Template.bind({});
|
||||
Continuous.args = {
|
||||
numberOfSellRows: 100,
|
||||
numberOfBuyRows: 100,
|
||||
midPrice: 1000,
|
||||
bestStaticBidPrice: 1000,
|
||||
bestStaticOfferPrice: 1000,
|
||||
decimalPlaces: 3,
|
||||
overlap: -1,
|
||||
};
|
||||
|
||||
export const Auction = Template.bind({});
|
||||
Auction.args = {
|
||||
numberOfSellRows: 100,
|
||||
numberOfBuyRows: 100,
|
||||
midPrice: 122900,
|
||||
bestStaticBidPrice: 122905,
|
||||
bestStaticOfferPrice: 122895,
|
||||
decimalPlaces: 3,
|
||||
overlap: 10,
|
||||
indicativePrice: 122900,
|
||||
indicativeVolume: 11,
|
||||
};
|
||||
|
||||
export const Empty = Template.bind({});
|
||||
Empty.args = {
|
||||
numberOfSellRows: 0,
|
||||
numberOfBuyRows: 0,
|
||||
midPrice: 0,
|
||||
bestStaticBidPrice: 0,
|
||||
bestStaticOfferPrice: 0,
|
||||
decimalPlaces: 3,
|
||||
overlap: 0,
|
||||
indicativePrice: 0,
|
||||
indicativeVolume: 0,
|
||||
};
|
@ -1,36 +1,395 @@
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
import { OrderbookRow } from './orderbook-row';
|
||||
import type { OrderbookData } from './orderbook-data';
|
||||
import styles from './orderbook.module.scss';
|
||||
|
||||
interface OrderbookProps {
|
||||
data: OrderbookData[] | null;
|
||||
import {
|
||||
Fragment,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useRef,
|
||||
useState,
|
||||
useMemo,
|
||||
useCallback,
|
||||
} from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { formatNumber, t } from '@vegaprotocol/react-helpers';
|
||||
import { MarketTradingMode } from '@vegaprotocol/types';
|
||||
import { OrderbookRow } from './orderbook-row';
|
||||
import { createRow, getPriceLevel } from './orderbook-data';
|
||||
import { Icon, Splash } from '@vegaprotocol/ui-toolkit';
|
||||
import type { OrderbookData, OrderbookRowData } from './orderbook-data';
|
||||
interface OrderbookProps extends OrderbookData {
|
||||
decimalPlaces: number;
|
||||
resolution: number;
|
||||
onResolutionChange: (resolution: number) => void;
|
||||
}
|
||||
|
||||
export const Orderbook = ({ data, decimalPlaces }: OrderbookProps) => (
|
||||
<>
|
||||
<div className="grid grid-cols-4 gap-4 border-b-1 text-ui-small mb-2 pb-2">
|
||||
<div>{t('Bid Vol')}</div>
|
||||
<div>{t('Price')}</div>
|
||||
<div>{t('Ask Vol')}</div>
|
||||
<div>{t('Cumulative Vol')}</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 gap-4 text-right text-ui-small">
|
||||
{data?.map((data) => (
|
||||
<OrderbookRow
|
||||
key={data.price}
|
||||
price={data.price}
|
||||
decimalPlaces={decimalPlaces}
|
||||
bid={data.bid}
|
||||
relativeBidVol={data.relativeBidVol}
|
||||
cumulativeRelativeBid={data.cumulativeVol.relativeBid}
|
||||
ask={data.ask}
|
||||
relativeAskVol={data.relativeAskVol}
|
||||
cumulativeRelativeAsk={data.cumulativeVol.relativeAsk}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
const horizontalLine = (top: string, testId: string) => (
|
||||
<div
|
||||
className="border-b-1 absolute inset-x-0"
|
||||
style={{ top }}
|
||||
data-testid={testId}
|
||||
></div>
|
||||
);
|
||||
|
||||
const getNumberOfRows = (
|
||||
rows: OrderbookRowData[] | null,
|
||||
resolution: number
|
||||
) => {
|
||||
if (!rows || !rows.length) {
|
||||
return 0;
|
||||
}
|
||||
if (rows.length === 1) {
|
||||
return 1;
|
||||
}
|
||||
return (
|
||||
Number(BigInt(rows[0].price) - BigInt(rows[rows.length - 1].price)) /
|
||||
resolution +
|
||||
1
|
||||
);
|
||||
};
|
||||
|
||||
const getRowsToRender = (
|
||||
rows: OrderbookRowData[] | null,
|
||||
resolution: number,
|
||||
offset: number,
|
||||
limit: number
|
||||
): OrderbookRowData[] | null => {
|
||||
if (!rows || !rows.length) {
|
||||
return rows;
|
||||
}
|
||||
if (rows.length === 1) {
|
||||
return rows;
|
||||
}
|
||||
const selectedRows: OrderbookRowData[] = [];
|
||||
let price = BigInt(rows[0].price) - BigInt(offset * resolution);
|
||||
let index = Math.max(
|
||||
rows.findIndex((row) => BigInt(row.price) <= price) - 1,
|
||||
-1
|
||||
);
|
||||
while (selectedRows.length < limit && index + 1 < rows.length) {
|
||||
if (rows[index + 1].price === price.toString()) {
|
||||
selectedRows.push(rows[index + 1]);
|
||||
index += 1;
|
||||
} else {
|
||||
const row = createRow(price.toString());
|
||||
row.cumulativeVol = {
|
||||
bid: rows[index].cumulativeVol.bid,
|
||||
relativeBid: rows[index].cumulativeVol.relativeBid,
|
||||
ask: rows[index + 1].cumulativeVol.ask,
|
||||
relativeAsk: rows[index + 1].cumulativeVol.relativeAsk,
|
||||
};
|
||||
selectedRows.push(row);
|
||||
}
|
||||
price -= BigInt(resolution);
|
||||
}
|
||||
return selectedRows;
|
||||
};
|
||||
|
||||
// 17px of row height plus 5px gap
|
||||
export const rowHeight = 22;
|
||||
// buffer size in rows
|
||||
const bufferSize = 30;
|
||||
// margin size in px, when reached scrollOffset will be updated
|
||||
const marginSize = bufferSize * 0.9 * rowHeight;
|
||||
|
||||
export const Orderbook = ({
|
||||
rows,
|
||||
bestStaticBidPrice,
|
||||
bestStaticOfferPrice,
|
||||
marketTradingMode,
|
||||
indicativeVolume,
|
||||
indicativePrice,
|
||||
decimalPlaces,
|
||||
resolution,
|
||||
onResolutionChange,
|
||||
}: OrderbookProps) => {
|
||||
const scrollElement = useRef<HTMLDivElement>(null);
|
||||
// scroll offset for which rendered rows are selected, will change after user will scroll to margin of rendered data
|
||||
const [scrollOffset, setScrollOffset] = useState(0);
|
||||
// actual scrollTop of scrollElement current element
|
||||
const scrollTopRef = useRef(0);
|
||||
// price level which is rendered in center of viewport, need to preserve price level when rows will be added or removed
|
||||
// if undefined then we render mid price in center
|
||||
const priceInCenter = useRef<string>();
|
||||
const [lockOnMidPrice, setLockOnMidPrice] = useState(true);
|
||||
const resolutionRef = useRef(resolution);
|
||||
// stores rows[0].price value
|
||||
const [maxPriceLevel, setMaxPriceLevel] = useState('');
|
||||
const [viewportHeight, setViewportHeight] = useState(window.innerHeight);
|
||||
const numberOfRows = useMemo(
|
||||
() => getNumberOfRows(rows, resolution),
|
||||
[rows, resolution]
|
||||
);
|
||||
|
||||
const updateScrollOffset = useCallback(
|
||||
(scrollTop: number) => {
|
||||
if (Math.abs(scrollOffset - scrollTop) > marginSize) {
|
||||
setScrollOffset(scrollTop);
|
||||
}
|
||||
},
|
||||
[scrollOffset]
|
||||
);
|
||||
|
||||
const onScroll = useCallback(
|
||||
(event: React.UIEvent<HTMLDivElement>) => {
|
||||
const { scrollTop } = event.currentTarget;
|
||||
updateScrollOffset(scrollTop);
|
||||
if (scrollTop === scrollTopRef.current) {
|
||||
return;
|
||||
}
|
||||
priceInCenter.current = (
|
||||
BigInt(resolution) + // extra row on very top - sticky header
|
||||
BigInt(maxPriceLevel) -
|
||||
BigInt(
|
||||
Math.floor((scrollTop + Math.floor(viewportHeight / 2)) / rowHeight)
|
||||
) *
|
||||
BigInt(resolution)
|
||||
).toString();
|
||||
if (lockOnMidPrice) {
|
||||
setLockOnMidPrice(false);
|
||||
}
|
||||
scrollTopRef.current = scrollTop;
|
||||
},
|
||||
[
|
||||
resolution,
|
||||
lockOnMidPrice,
|
||||
maxPriceLevel,
|
||||
viewportHeight,
|
||||
updateScrollOffset,
|
||||
]
|
||||
);
|
||||
|
||||
const scrollToPrice = useCallback(
|
||||
(price: string) => {
|
||||
if (scrollElement.current && maxPriceLevel) {
|
||||
let scrollTop =
|
||||
// distance in rows between midPrice and price from first row * row Height
|
||||
(Number(
|
||||
(BigInt(maxPriceLevel) - BigInt(price)) / BigInt(resolution)
|
||||
) +
|
||||
1) * // add one row for sticky header
|
||||
rowHeight;
|
||||
// minus half height of viewport plus half of row
|
||||
scrollTop -= Math.ceil((viewportHeight - rowHeight) / 2);
|
||||
// adjust to current rows position
|
||||
scrollTop +=
|
||||
(scrollTopRef.current % rowHeight) - (scrollTop % rowHeight);
|
||||
const priceCenterScrollOffset = Math.max(0, Math.min(scrollTop));
|
||||
if (scrollTopRef.current !== priceCenterScrollOffset) {
|
||||
updateScrollOffset(priceCenterScrollOffset);
|
||||
scrollTopRef.current = priceCenterScrollOffset;
|
||||
scrollElement.current.scrollTop = priceCenterScrollOffset;
|
||||
}
|
||||
}
|
||||
},
|
||||
[maxPriceLevel, resolution, viewportHeight, updateScrollOffset]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const newMaxPriceLevel = rows?.[0]?.price ?? '';
|
||||
if (newMaxPriceLevel !== maxPriceLevel) {
|
||||
setMaxPriceLevel(newMaxPriceLevel);
|
||||
}
|
||||
}, [rows, maxPriceLevel]);
|
||||
|
||||
const scrollToMidPrice = useCallback(() => {
|
||||
if (!bestStaticOfferPrice || !bestStaticBidPrice) {
|
||||
return;
|
||||
}
|
||||
priceInCenter.current = undefined;
|
||||
setLockOnMidPrice(true);
|
||||
scrollToPrice(
|
||||
getPriceLevel(
|
||||
BigInt(bestStaticOfferPrice) +
|
||||
(BigInt(bestStaticBidPrice) - BigInt(bestStaticOfferPrice)) /
|
||||
BigInt(2),
|
||||
resolution
|
||||
)
|
||||
);
|
||||
}, [bestStaticOfferPrice, bestStaticBidPrice, scrollToPrice, resolution]);
|
||||
|
||||
// adjust scroll position to keep selected price in center
|
||||
useLayoutEffect(() => {
|
||||
if (resolutionRef.current !== resolution) {
|
||||
priceInCenter.current = undefined;
|
||||
resolutionRef.current = resolution;
|
||||
setLockOnMidPrice(true);
|
||||
}
|
||||
if (priceInCenter.current) {
|
||||
scrollToPrice(priceInCenter.current);
|
||||
} else {
|
||||
scrollToMidPrice();
|
||||
}
|
||||
}, [scrollToMidPrice, scrollToPrice, resolution]);
|
||||
|
||||
// handles viewport resize
|
||||
useEffect(() => {
|
||||
function handleResize() {
|
||||
if (scrollElement.current) {
|
||||
setViewportHeight(
|
||||
scrollElement.current.clientHeight || window.innerHeight
|
||||
);
|
||||
}
|
||||
}
|
||||
window.addEventListener('resize', handleResize);
|
||||
handleResize();
|
||||
return () => window.removeEventListener('resize', handleResize);
|
||||
}, []);
|
||||
|
||||
const renderedRows = useMemo(() => {
|
||||
let offset = Math.max(0, Math.round(scrollOffset / rowHeight));
|
||||
const prependingBufferSize = Math.min(bufferSize, offset);
|
||||
offset -= prependingBufferSize;
|
||||
const viewportSize = Math.round(viewportHeight / rowHeight);
|
||||
const limit = Math.min(
|
||||
prependingBufferSize + viewportSize + bufferSize,
|
||||
numberOfRows - offset
|
||||
);
|
||||
return {
|
||||
offset,
|
||||
limit,
|
||||
data: getRowsToRender(rows, resolution, offset, limit),
|
||||
};
|
||||
}, [rows, scrollOffset, resolution, viewportHeight, numberOfRows]);
|
||||
|
||||
const paddingTop = renderedRows.offset * rowHeight;
|
||||
const paddingBottom =
|
||||
(numberOfRows - renderedRows.offset - renderedRows.limit) * rowHeight;
|
||||
const minPriceLevel =
|
||||
BigInt(maxPriceLevel) - BigInt(Math.floor(numberOfRows * resolution));
|
||||
const hasData = renderedRows.data && renderedRows.data.length !== 0;
|
||||
return (
|
||||
<div
|
||||
className={`h-full overflow-auto relative ${styles['scroll']}`}
|
||||
onScroll={onScroll}
|
||||
ref={scrollElement}
|
||||
data-testid="scroll"
|
||||
>
|
||||
<div
|
||||
className="sticky top-0 grid grid-cols-4 gap-5 text-right border-b-1 text-ui-small mb-2 pb-2 bg-white dark:bg-black z-10"
|
||||
style={{ gridAutoRows: '17px' }}
|
||||
>
|
||||
<div>{t('Bid Vol')}</div>
|
||||
<div>{t('Price')}</div>
|
||||
<div>{t('Ask Vol')}</div>
|
||||
<div className="pr-4">{t('Cumulative Vol')}</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
paddingTop: `${paddingTop}px`,
|
||||
paddingBottom: `${paddingBottom}px`,
|
||||
minHeight: `calc(100% - ${2 * rowHeight}px)`,
|
||||
background: hasData
|
||||
? 'linear-gradient(#999,#999) 24.6% 0/1px 100% no-repeat, linear-gradient(#999,#999) 50% 0/1px 100% no-repeat, linear-gradient(#999,#999) 75.2% 0/1px 100% no-repeat'
|
||||
: 'none',
|
||||
}}
|
||||
>
|
||||
{hasData ? (
|
||||
<div
|
||||
className="grid grid-cols-4 gap-5 text-right text-ui-small"
|
||||
style={{
|
||||
gridAutoRows: '17px',
|
||||
}}
|
||||
>
|
||||
{renderedRows.data?.map((data) => {
|
||||
return (
|
||||
<Fragment key={data.price}>
|
||||
<OrderbookRow
|
||||
price={(BigInt(data.price) / BigInt(resolution)).toString()}
|
||||
decimalPlaces={decimalPlaces - Math.log10(resolution)}
|
||||
bid={data.bid}
|
||||
relativeBid={data.relativeBid}
|
||||
cumulativeBid={data.cumulativeVol.bid}
|
||||
cumulativeRelativeBid={data.cumulativeVol.relativeBid}
|
||||
ask={data.ask}
|
||||
relativeAsk={data.relativeAsk}
|
||||
cumulativeAsk={data.cumulativeVol.ask}
|
||||
cumulativeRelativeAsk={data.cumulativeVol.relativeAsk}
|
||||
indicativeVolume={
|
||||
marketTradingMode !== MarketTradingMode.Continuous &&
|
||||
indicativePrice === data.price
|
||||
? indicativeVolume
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="inset-0 absolute">
|
||||
<Splash>{t('No data')}</Splash>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className="sticky bottom-0 grid grid-cols-4 gap-5 border-t-1 text-ui-small mt-2 pb-2 bg-white dark:bg-black z-10"
|
||||
style={{ gridAutoRows: '17px' }}
|
||||
>
|
||||
<div className="text-ui-small col-start-2">
|
||||
<select
|
||||
onChange={(e) => onResolutionChange(Number(e.currentTarget.value))}
|
||||
value={resolution}
|
||||
className="block bg-black-25 dark:bg-white-25 text-black dark:text-white focus-visible:shadow-focus dark:focus-visible:shadow-focus-dark focus-visible:outline-0 font-mono w-100 text-right w-full h-full"
|
||||
data-testid="resolution"
|
||||
>
|
||||
{new Array(3)
|
||||
.fill(null)
|
||||
.map((v, i) => Math.pow(10, i))
|
||||
.map((r) => (
|
||||
<option key={r} value={r}>
|
||||
{formatNumber(0, decimalPlaces - Math.log10(r))}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="text-ui-small col-start-4">
|
||||
<button
|
||||
onClick={scrollToMidPrice}
|
||||
className={classNames('w-full h-full', {
|
||||
hidden: lockOnMidPrice,
|
||||
block: !lockOnMidPrice,
|
||||
})}
|
||||
data-testid="scroll-to-midprice"
|
||||
>
|
||||
Go to mid
|
||||
<span className="ml-4">
|
||||
<Icon name="th-derived" />
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{maxPriceLevel &&
|
||||
bestStaticBidPrice &&
|
||||
BigInt(bestStaticBidPrice) < BigInt(maxPriceLevel) &&
|
||||
BigInt(bestStaticBidPrice) > minPriceLevel &&
|
||||
horizontalLine(
|
||||
`${(
|
||||
((BigInt(maxPriceLevel) - BigInt(bestStaticBidPrice)) /
|
||||
BigInt(resolution) +
|
||||
BigInt(1)) *
|
||||
BigInt(rowHeight) -
|
||||
BigInt(3)
|
||||
).toString()}px`,
|
||||
'best-static-bid-price'
|
||||
)}
|
||||
{maxPriceLevel &&
|
||||
bestStaticOfferPrice &&
|
||||
BigInt(bestStaticOfferPrice) <= BigInt(maxPriceLevel) &&
|
||||
BigInt(bestStaticOfferPrice) > minPriceLevel &&
|
||||
horizontalLine(
|
||||
`${(
|
||||
((BigInt(maxPriceLevel) - BigInt(bestStaticOfferPrice)) /
|
||||
BigInt(resolution) +
|
||||
BigInt(2)) *
|
||||
BigInt(rowHeight) -
|
||||
BigInt(3)
|
||||
).toString()}px`,
|
||||
'best-static-offer-price'
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Orderbook;
|
||||
|
@ -1 +1,2 @@
|
||||
import '@testing-library/jest-dom';
|
||||
import 'jest-canvas-mock';
|
||||
|
3
libs/market-depth/src/styles.scss
Normal file
3
libs/market-depth/src/styles.scss
Normal file
@ -0,0 +1,3 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
17
libs/market-depth/tailwind.config.js
Normal file
17
libs/market-depth/tailwind.config.js
Normal file
@ -0,0 +1,17 @@
|
||||
const { join } = require('path');
|
||||
const { createGlobPatternsForDependencies } = require('@nrwl/next/tailwind');
|
||||
const theme = require('../tailwindcss-config/src/theme');
|
||||
const vegaCustomClasses = require('../tailwindcss-config/src/vega-custom-classes');
|
||||
|
||||
module.exports = {
|
||||
content: [
|
||||
join(__dirname, 'src/**/*.{ts,tsx,html,mdx}'),
|
||||
join(__dirname, '.storybook/preview.js'),
|
||||
...createGlobPatternsForDependencies(__dirname),
|
||||
],
|
||||
darkMode: 'class',
|
||||
theme: {
|
||||
extend: theme,
|
||||
},
|
||||
plugins: [vegaCustomClasses],
|
||||
};
|
@ -20,6 +20,9 @@
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.spec.json"
|
||||
},
|
||||
{
|
||||
"path": "./.storybook/tsconfig.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -16,7 +16,11 @@
|
||||
"**/*.spec.js",
|
||||
"**/*.test.js",
|
||||
"**/*.spec.jsx",
|
||||
"**/*.test.jsx"
|
||||
"**/*.test.jsx",
|
||||
"**/*.stories.ts",
|
||||
"**/*.stories.js",
|
||||
"**/*.stories.jsx",
|
||||
"**/*.stories.tsx"
|
||||
],
|
||||
"include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"]
|
||||
}
|
||||
|
@ -3,7 +3,7 @@
|
||||
"compilerOptions": {
|
||||
"outDir": "../../dist/out-tsc",
|
||||
"module": "commonjs",
|
||||
"types": ["jest", "node"]
|
||||
"types": ["jest", "node", "@testing-library/jest-dom"]
|
||||
},
|
||||
"include": [
|
||||
"**/*.test.ts",
|
||||
|
@ -20,16 +20,16 @@ export function useDataProvider<Data, Delta>(
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [error, setError] = useState<Error | undefined>(undefined);
|
||||
const flushRef = useRef<(() => void) | undefined>(undefined);
|
||||
const restartRef = useRef<((force?: boolean) => void) | undefined>(undefined);
|
||||
const reloadRef = useRef<((force?: boolean) => void) | undefined>(undefined);
|
||||
const initialized = useRef<boolean>(false);
|
||||
const flush = useCallback(() => {
|
||||
if (flushRef.current) {
|
||||
flushRef.current();
|
||||
}
|
||||
}, []);
|
||||
const restart = useCallback((force = false) => {
|
||||
if (restartRef.current) {
|
||||
restartRef.current(force);
|
||||
const reload = useCallback((force = false) => {
|
||||
if (reloadRef.current) {
|
||||
reloadRef.current(force);
|
||||
}
|
||||
}, []);
|
||||
const callback = useCallback(
|
||||
@ -48,14 +48,14 @@ export function useDataProvider<Data, Delta>(
|
||||
[update]
|
||||
);
|
||||
useEffect(() => {
|
||||
const { unsubscribe, flush, restart } = dataProvider(
|
||||
const { unsubscribe, flush, reload } = dataProvider(
|
||||
callback,
|
||||
client,
|
||||
variables
|
||||
);
|
||||
flushRef.current = flush;
|
||||
restartRef.current = restart;
|
||||
reloadRef.current = reload;
|
||||
return unsubscribe;
|
||||
}, [client, initialized, dataProvider, callback, variables]);
|
||||
return { data, loading, error, flush, restart };
|
||||
return { data, loading, error, flush, reload };
|
||||
}
|
||||
|
@ -25,7 +25,7 @@ export interface Subscribe<Data, Delta> {
|
||||
variables?: OperationVariables
|
||||
): {
|
||||
unsubscribe: () => void;
|
||||
restart: (force?: boolean) => void;
|
||||
reload: (forceReset?: boolean) => void;
|
||||
flush: () => void;
|
||||
};
|
||||
}
|
||||
@ -34,7 +34,11 @@ export interface Subscribe<Data, Delta> {
|
||||
type Query<Result> = DocumentNode | TypedDocumentNode<Result, any>;
|
||||
|
||||
export interface Update<Data, Delta> {
|
||||
(draft: Draft<Data>, delta: Delta, restart: (force?: boolean) => void): void;
|
||||
(
|
||||
draft: Draft<Data>,
|
||||
delta: Delta,
|
||||
reload: (forceReset?: boolean) => void
|
||||
): void;
|
||||
}
|
||||
|
||||
interface GetData<QueryData, Data> {
|
||||
@ -47,7 +51,7 @@ interface GetDelta<SubscriptionData, Delta> {
|
||||
|
||||
/**
|
||||
* @param subscriptionQuery query that will be used for subscription
|
||||
* @param update function that will be executed on each onNext, it should update data base on delta, it can restart data provider
|
||||
* @param update function that will be execued 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 getDelta transforms delta data to format that will be stored in data provider
|
||||
* @param fetchPolicy
|
||||
@ -105,7 +109,7 @@ function makeDataProviderInternal<QueryData, Data, SubscriptionData, Delta>(
|
||||
while (updateQueue.length) {
|
||||
const delta = updateQueue.shift();
|
||||
if (delta) {
|
||||
update(draft, delta, restart);
|
||||
update(draft, delta, reload);
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -123,13 +127,13 @@ function makeDataProviderInternal<QueryData, Data, SubscriptionData, Delta>(
|
||||
}
|
||||
};
|
||||
|
||||
// restart function is passed to update and as a returned by subscribe function
|
||||
const restart = (hard = false) => {
|
||||
// reload function is passed to update and as a returned by subscribe function
|
||||
const reload = (forceReset = false) => {
|
||||
if (loading) {
|
||||
return;
|
||||
}
|
||||
// hard reset on demand or when there is no apollo subscription yet
|
||||
if (hard || !subscription) {
|
||||
if (forceReset || !subscription) {
|
||||
reset();
|
||||
initialize();
|
||||
} else {
|
||||
@ -165,7 +169,7 @@ function makeDataProviderInternal<QueryData, Data, SubscriptionData, Delta>(
|
||||
updateQueue.push(delta);
|
||||
} else {
|
||||
const newData = produce(data, (draft) => {
|
||||
update(draft, delta, restart);
|
||||
update(draft, delta, reload);
|
||||
});
|
||||
if (newData === data) {
|
||||
return;
|
||||
@ -174,12 +178,12 @@ function makeDataProviderInternal<QueryData, Data, SubscriptionData, Delta>(
|
||||
notifyAll(delta);
|
||||
}
|
||||
},
|
||||
() => restart()
|
||||
() => reload()
|
||||
);
|
||||
await initialFetch();
|
||||
};
|
||||
|
||||
const reset = () => {
|
||||
const reset = (clean = true) => {
|
||||
if (subscription) {
|
||||
subscription.unsubscribe();
|
||||
subscription = undefined;
|
||||
@ -198,7 +202,6 @@ function makeDataProviderInternal<QueryData, Data, SubscriptionData, Delta>(
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
return (callback, c, v) => {
|
||||
callbacks.push(callback);
|
||||
if (callbacks.length === 1) {
|
||||
@ -210,7 +213,7 @@ function makeDataProviderInternal<QueryData, Data, SubscriptionData, Delta>(
|
||||
}
|
||||
return {
|
||||
unsubscribe: () => unsubscribe(callback),
|
||||
restart,
|
||||
reload,
|
||||
flush: () => notify(callback),
|
||||
};
|
||||
};
|
||||
@ -243,7 +246,7 @@ const memoize = <Data, Delta>(
|
||||
/**
|
||||
* @param query Query<QueryData>
|
||||
* @param subscriptionQuery Query<SubscriptionData> query that will be used for subscription
|
||||
* @param update Update<Data, Delta> function that will be executed on each onNext, it should update data base on delta, it can restart 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 getDelta transforms delta data to format that will be stored in data provider
|
||||
* @param fetchPolicy
|
||||
@ -252,12 +255,12 @@ const memoize = <Data, Delta>(
|
||||
* const marketMidPriceProvider = makeDataProvider<QueryData, Data, SubscriptionData, Delta>(
|
||||
* gql`query MarketMidPrice($marketId: ID!) { market(id: $marketId) { data { midPrice } } }`,
|
||||
* gql`subscription MarketMidPriceSubscription($marketId: ID!) { marketDepthUpdate(marketId: $marketId) { market { data { midPrice } } } }`,
|
||||
* (draft: Draft<Data>, delta: Delta, restart: (force?: boolean) => void) => { draft.midPrice = delta.midPrice }
|
||||
* (draft: Draft<Data>, delta: Delta, reload: (forceReset?: boolean) => void) => { draft.midPrice = delta.midPrice }
|
||||
* (data:QueryData) => data.market.data.midPrice
|
||||
* (delta:SubscriptionData) => delta.marketData.market
|
||||
* )
|
||||
*
|
||||
* const { unsubscribe, flush, restart } = marketMidPriceProvider(
|
||||
* const { unsubscribe, flush, reload } = marketMidPriceProvider(
|
||||
* ({ data, error, loading, delta }) => { ... },
|
||||
* apolloClient,
|
||||
* { id: '1fd726454fa1220038acbf6ff9ac701d8b8bf3f2d77c93a4998544471dc58747' }
|
||||
|
@ -1,13 +1,16 @@
|
||||
import React from 'react';
|
||||
import type { ICellRendererParams } from 'ag-grid-community';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { BID_COLOR, ASK_COLOR } from './vol-cell';
|
||||
|
||||
const INTERSECT_COLOR = 'darkgray';
|
||||
|
||||
export interface CumulativeVolProps {
|
||||
ask?: number;
|
||||
bid?: number;
|
||||
relativeAsk?: number;
|
||||
relativeBid?: number;
|
||||
indicativeVolume?: string;
|
||||
testId?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export interface ICumulativeVolCellProps extends ICellRendererParams {
|
||||
@ -15,44 +18,57 @@ export interface ICumulativeVolCellProps extends ICellRendererParams {
|
||||
}
|
||||
|
||||
export const CumulativeVol = React.memo(
|
||||
({ relativeAsk, relativeBid }: CumulativeVolProps) => {
|
||||
const bid = relativeBid ? (
|
||||
({
|
||||
relativeAsk,
|
||||
relativeBid,
|
||||
ask,
|
||||
bid,
|
||||
indicativeVolume,
|
||||
testId,
|
||||
className,
|
||||
}: CumulativeVolProps) => {
|
||||
const askBar = relativeAsk ? (
|
||||
<div
|
||||
className="h-full absolute top-0 right-0"
|
||||
style={{
|
||||
width: `${relativeBid}%`,
|
||||
backgroundColor:
|
||||
relativeAsk && relativeAsk > relativeBid
|
||||
? INTERSECT_COLOR
|
||||
: BID_COLOR,
|
||||
}}
|
||||
></div>
|
||||
) : null;
|
||||
const ask = relativeAsk ? (
|
||||
<div
|
||||
className="h-full absolute top-0 left-0"
|
||||
data-testid="ask-bar"
|
||||
className="absolute left-0 top-0"
|
||||
style={{
|
||||
height: relativeBid && relativeAsk ? '50%' : '100%',
|
||||
width: `${relativeAsk}%`,
|
||||
backgroundColor:
|
||||
relativeBid && relativeBid > relativeAsk
|
||||
? INTERSECT_COLOR
|
||||
: ASK_COLOR,
|
||||
backgroundColor: ASK_COLOR,
|
||||
}}
|
||||
></div>
|
||||
) : null;
|
||||
const bidBar = relativeBid ? (
|
||||
<div
|
||||
data-testid="bid-bar"
|
||||
className="absolute top-0 left-0"
|
||||
style={{
|
||||
height: relativeBid && relativeAsk ? '50%' : '100%',
|
||||
top: relativeBid && relativeAsk ? '50%' : '0',
|
||||
width: `${relativeBid}%`,
|
||||
backgroundColor: BID_COLOR,
|
||||
}}
|
||||
></div>
|
||||
) : null;
|
||||
|
||||
const volume = indicativeVolume ? (
|
||||
<span className="relative">({indicativeVolume})</span>
|
||||
) : (
|
||||
<span className="relative">
|
||||
{ask ? ask : null}
|
||||
{ask && bid ? '/' : null}
|
||||
{bid ? bid : null}
|
||||
</span>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="h-full relative" data-testid="vol">
|
||||
{relativeBid && relativeAsk && relativeBid > relativeAsk ? (
|
||||
<>
|
||||
{ask}
|
||||
{bid}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{bid}
|
||||
{ask}
|
||||
</>
|
||||
)}
|
||||
<div
|
||||
className={classNames('h-full relative', className)}
|
||||
data-testid={testId || 'cummulative-vol'}
|
||||
>
|
||||
{askBar}
|
||||
{bidBar}
|
||||
{volume}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -2,10 +2,11 @@ import React from 'react';
|
||||
export interface IPriceCellProps {
|
||||
value: number | bigint | null | undefined;
|
||||
valueFormatted: string;
|
||||
testId?: string;
|
||||
}
|
||||
|
||||
export const PriceCell = React.memo(
|
||||
({ value, valueFormatted }: IPriceCellProps) => {
|
||||
({ value, valueFormatted, testId }: IPriceCellProps) => {
|
||||
if (
|
||||
(!value && value !== 0) ||
|
||||
(typeof value === 'number' && isNaN(Number(value)))
|
||||
@ -13,7 +14,10 @@ export const PriceCell = React.memo(
|
||||
return <span data-testid="price">-</span>;
|
||||
}
|
||||
return (
|
||||
<span className="font-mono relative text-ui-small" data-testid="price">
|
||||
<span
|
||||
className="font-mono relative text-ui-small"
|
||||
data-testid={testId || 'price'}
|
||||
>
|
||||
{valueFormatted}
|
||||
</span>
|
||||
);
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom/extend-expect';
|
||||
import * as React from 'react';
|
||||
|
||||
import { PriceFlashCell } from './price-flash-cell';
|
||||
|
||||
|
@ -1,11 +1,17 @@
|
||||
import React from 'react';
|
||||
import type { ICellRendererParams } from 'ag-grid-community';
|
||||
import { PriceCell } from './price-cell';
|
||||
import classNames from 'classnames';
|
||||
|
||||
export enum VolumeType {
|
||||
bid,
|
||||
ask,
|
||||
}
|
||||
export interface VolProps {
|
||||
value: number | bigint | null | undefined;
|
||||
relativeValue?: number;
|
||||
type: 'bid' | 'ask';
|
||||
type: VolumeType;
|
||||
testId?: string;
|
||||
}
|
||||
export interface IVolCellProps extends ICellRendererParams {
|
||||
value: number | bigint | null | undefined;
|
||||
@ -15,23 +21,28 @@ export interface IVolCellProps extends ICellRendererParams {
|
||||
export const BID_COLOR = 'darkgreen';
|
||||
export const ASK_COLOR = 'maroon';
|
||||
|
||||
export const Vol = React.memo(({ value, relativeValue, type }: VolProps) => {
|
||||
if ((!value && value !== 0) || isNaN(Number(value))) {
|
||||
return <div data-testid="vol">-</div>;
|
||||
export const Vol = React.memo(
|
||||
({ value, relativeValue, type, testId }: VolProps) => {
|
||||
if ((!value && value !== 0) || isNaN(Number(value))) {
|
||||
return <div data-testid="vol">-</div>;
|
||||
}
|
||||
return (
|
||||
<div className="relative" data-testid={testId || 'vol'}>
|
||||
<div
|
||||
className={classNames('h-full absolute top-0', {
|
||||
'left-0': type === VolumeType.bid,
|
||||
'right-0': type === VolumeType.ask,
|
||||
})}
|
||||
style={{
|
||||
width: relativeValue ? `${relativeValue}%` : '0%',
|
||||
backgroundColor: type === VolumeType.bid ? BID_COLOR : ASK_COLOR,
|
||||
}}
|
||||
></div>
|
||||
<PriceCell value={value} valueFormatted={value.toString()} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="relative" data-testid="vol">
|
||||
<div
|
||||
className="h-full absolute top-0 left-0"
|
||||
style={{
|
||||
width: relativeValue ? `${relativeValue}%` : '0%',
|
||||
backgroundColor: type === 'bid' ? BID_COLOR : ASK_COLOR,
|
||||
}}
|
||||
></div>
|
||||
<PriceCell value={value} valueFormatted={value.toString()} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
);
|
||||
|
||||
Vol.displayName = 'Vol';
|
||||
|
||||
|
@ -79,6 +79,7 @@ module.exports = {
|
||||
0: '0px',
|
||||
2: '0.125rem',
|
||||
4: '0.25rem',
|
||||
5: '0.3125rem',
|
||||
8: '0.5rem',
|
||||
12: '0.75rem',
|
||||
16: '1rem',
|
||||
|
@ -1,6 +1,7 @@
|
||||
import '../src/styles.scss';
|
||||
export const parameters = {
|
||||
actions: { argTypesRegex: '^on[A-Z].*' },
|
||||
backgrounds: { disable: true },
|
||||
/*themes: {
|
||||
default: 'dark',
|
||||
list: [
|
||||
|
@ -97,6 +97,7 @@
|
||||
"@storybook/addon-a11y": "^6.4.19",
|
||||
"@storybook/addon-essentials": "~6.4.12",
|
||||
"@storybook/builder-webpack5": "~6.4.12",
|
||||
"@storybook/core-server": "~6.4.12",
|
||||
"@storybook/manager-webpack5": "~6.4.12",
|
||||
"@storybook/react": "~6.4.12",
|
||||
"@svgr/webpack": "^5.4.0",
|
||||
|
536
yarn.lock
536
yarn.lock
@ -4158,6 +4158,23 @@
|
||||
global "^4.4.0"
|
||||
regenerator-runtime "^0.13.7"
|
||||
|
||||
"@storybook/addons@6.4.22":
|
||||
version "6.4.22"
|
||||
resolved "https://registry.yarnpkg.com/@storybook/addons/-/addons-6.4.22.tgz#e165407ca132c2182de2d466b7ff7c5644b6ad7b"
|
||||
integrity sha512-P/R+Jsxh7pawKLYo8MtE3QU/ilRFKbtCewV/T1o5U/gm8v7hKQdFz3YdRMAra4QuCY8bQIp7MKd2HrB5aH5a1A==
|
||||
dependencies:
|
||||
"@storybook/api" "6.4.22"
|
||||
"@storybook/channels" "6.4.22"
|
||||
"@storybook/client-logger" "6.4.22"
|
||||
"@storybook/core-events" "6.4.22"
|
||||
"@storybook/csf" "0.0.2--canary.87bc651.0"
|
||||
"@storybook/router" "6.4.22"
|
||||
"@storybook/theming" "6.4.22"
|
||||
"@types/webpack-env" "^1.16.0"
|
||||
core-js "^3.8.2"
|
||||
global "^4.4.0"
|
||||
regenerator-runtime "^0.13.7"
|
||||
|
||||
"@storybook/api@6.4.21", "@storybook/api@^6.0.0":
|
||||
version "6.4.21"
|
||||
resolved "https://registry.yarnpkg.com/@storybook/api/-/api-6.4.21.tgz#efee41ae7bde37f6fe43ee960fef1a261b1b1dd6"
|
||||
@ -4181,6 +4198,29 @@
|
||||
ts-dedent "^2.0.0"
|
||||
util-deprecate "^1.0.2"
|
||||
|
||||
"@storybook/api@6.4.22":
|
||||
version "6.4.22"
|
||||
resolved "https://registry.yarnpkg.com/@storybook/api/-/api-6.4.22.tgz#d63f7ad3ffdd74af01ae35099bff4c39702cf793"
|
||||
integrity sha512-lAVI3o2hKupYHXFTt+1nqFct942up5dHH6YD7SZZJGyW21dwKC3HK1IzCsTawq3fZAKkgWFgmOO649hKk60yKg==
|
||||
dependencies:
|
||||
"@storybook/channels" "6.4.22"
|
||||
"@storybook/client-logger" "6.4.22"
|
||||
"@storybook/core-events" "6.4.22"
|
||||
"@storybook/csf" "0.0.2--canary.87bc651.0"
|
||||
"@storybook/router" "6.4.22"
|
||||
"@storybook/semver" "^7.3.2"
|
||||
"@storybook/theming" "6.4.22"
|
||||
core-js "^3.8.2"
|
||||
fast-deep-equal "^3.1.3"
|
||||
global "^4.4.0"
|
||||
lodash "^4.17.21"
|
||||
memoizerific "^1.11.3"
|
||||
regenerator-runtime "^0.13.7"
|
||||
store2 "^2.12.0"
|
||||
telejson "^5.3.2"
|
||||
ts-dedent "^2.0.0"
|
||||
util-deprecate "^1.0.2"
|
||||
|
||||
"@storybook/builder-webpack4@6.4.21":
|
||||
version "6.4.21"
|
||||
resolved "https://registry.yarnpkg.com/@storybook/builder-webpack4/-/builder-webpack4-6.4.21.tgz#5355ab1bfe7ee153e907d8e64c6088fdb7a95676"
|
||||
@ -4256,6 +4296,81 @@
|
||||
webpack-hot-middleware "^2.25.1"
|
||||
webpack-virtual-modules "^0.2.2"
|
||||
|
||||
"@storybook/builder-webpack4@6.4.22":
|
||||
version "6.4.22"
|
||||
resolved "https://registry.yarnpkg.com/@storybook/builder-webpack4/-/builder-webpack4-6.4.22.tgz#d3384b146e97a2b3a6357c6eb8279ff0f1c7f8f5"
|
||||
integrity sha512-A+GgGtKGnBneRFSFkDarUIgUTI8pYFdLmUVKEAGdh2hL+vLXAz9A46sEY7C8LQ85XWa8TKy3OTDxqR4+4iWj3A==
|
||||
dependencies:
|
||||
"@babel/core" "^7.12.10"
|
||||
"@babel/plugin-proposal-class-properties" "^7.12.1"
|
||||
"@babel/plugin-proposal-decorators" "^7.12.12"
|
||||
"@babel/plugin-proposal-export-default-from" "^7.12.1"
|
||||
"@babel/plugin-proposal-nullish-coalescing-operator" "^7.12.1"
|
||||
"@babel/plugin-proposal-object-rest-spread" "^7.12.1"
|
||||
"@babel/plugin-proposal-optional-chaining" "^7.12.7"
|
||||
"@babel/plugin-proposal-private-methods" "^7.12.1"
|
||||
"@babel/plugin-syntax-dynamic-import" "^7.8.3"
|
||||
"@babel/plugin-transform-arrow-functions" "^7.12.1"
|
||||
"@babel/plugin-transform-block-scoping" "^7.12.12"
|
||||
"@babel/plugin-transform-classes" "^7.12.1"
|
||||
"@babel/plugin-transform-destructuring" "^7.12.1"
|
||||
"@babel/plugin-transform-for-of" "^7.12.1"
|
||||
"@babel/plugin-transform-parameters" "^7.12.1"
|
||||
"@babel/plugin-transform-shorthand-properties" "^7.12.1"
|
||||
"@babel/plugin-transform-spread" "^7.12.1"
|
||||
"@babel/plugin-transform-template-literals" "^7.12.1"
|
||||
"@babel/preset-env" "^7.12.11"
|
||||
"@babel/preset-react" "^7.12.10"
|
||||
"@babel/preset-typescript" "^7.12.7"
|
||||
"@storybook/addons" "6.4.22"
|
||||
"@storybook/api" "6.4.22"
|
||||
"@storybook/channel-postmessage" "6.4.22"
|
||||
"@storybook/channels" "6.4.22"
|
||||
"@storybook/client-api" "6.4.22"
|
||||
"@storybook/client-logger" "6.4.22"
|
||||
"@storybook/components" "6.4.22"
|
||||
"@storybook/core-common" "6.4.22"
|
||||
"@storybook/core-events" "6.4.22"
|
||||
"@storybook/node-logger" "6.4.22"
|
||||
"@storybook/preview-web" "6.4.22"
|
||||
"@storybook/router" "6.4.22"
|
||||
"@storybook/semver" "^7.3.2"
|
||||
"@storybook/store" "6.4.22"
|
||||
"@storybook/theming" "6.4.22"
|
||||
"@storybook/ui" "6.4.22"
|
||||
"@types/node" "^14.0.10"
|
||||
"@types/webpack" "^4.41.26"
|
||||
autoprefixer "^9.8.6"
|
||||
babel-loader "^8.0.0"
|
||||
babel-plugin-macros "^2.8.0"
|
||||
babel-plugin-polyfill-corejs3 "^0.1.0"
|
||||
case-sensitive-paths-webpack-plugin "^2.3.0"
|
||||
core-js "^3.8.2"
|
||||
css-loader "^3.6.0"
|
||||
file-loader "^6.2.0"
|
||||
find-up "^5.0.0"
|
||||
fork-ts-checker-webpack-plugin "^4.1.6"
|
||||
glob "^7.1.6"
|
||||
glob-promise "^3.4.0"
|
||||
global "^4.4.0"
|
||||
html-webpack-plugin "^4.0.0"
|
||||
pnp-webpack-plugin "1.6.4"
|
||||
postcss "^7.0.36"
|
||||
postcss-flexbugs-fixes "^4.2.1"
|
||||
postcss-loader "^4.2.0"
|
||||
raw-loader "^4.0.2"
|
||||
stable "^0.1.8"
|
||||
style-loader "^1.3.0"
|
||||
terser-webpack-plugin "^4.2.3"
|
||||
ts-dedent "^2.0.0"
|
||||
url-loader "^4.1.1"
|
||||
util-deprecate "^1.0.2"
|
||||
webpack "4"
|
||||
webpack-dev-middleware "^3.7.3"
|
||||
webpack-filter-warnings-plugin "^1.2.1"
|
||||
webpack-hot-middleware "^2.25.1"
|
||||
webpack-virtual-modules "^0.2.2"
|
||||
|
||||
"@storybook/builder-webpack5@~6.4.12":
|
||||
version "6.4.21"
|
||||
resolved "https://registry.yarnpkg.com/@storybook/builder-webpack5/-/builder-webpack5-6.4.21.tgz#d601676083a263a1f03847b12fe2ad1ecd3865bb"
|
||||
@ -4332,6 +4447,19 @@
|
||||
qs "^6.10.0"
|
||||
telejson "^5.3.2"
|
||||
|
||||
"@storybook/channel-postmessage@6.4.22":
|
||||
version "6.4.22"
|
||||
resolved "https://registry.yarnpkg.com/@storybook/channel-postmessage/-/channel-postmessage-6.4.22.tgz#8be0be1ea1e667a49fb0f09cdfdeeb4a45829637"
|
||||
integrity sha512-gt+0VZLszt2XZyQMh8E94TqjHZ8ZFXZ+Lv/Mmzl0Yogsc2H+6VzTTQO4sv0IIx6xLbpgG72g5cr8VHsxW5kuDQ==
|
||||
dependencies:
|
||||
"@storybook/channels" "6.4.22"
|
||||
"@storybook/client-logger" "6.4.22"
|
||||
"@storybook/core-events" "6.4.22"
|
||||
core-js "^3.8.2"
|
||||
global "^4.4.0"
|
||||
qs "^6.10.0"
|
||||
telejson "^5.3.2"
|
||||
|
||||
"@storybook/channel-websocket@6.4.21":
|
||||
version "6.4.21"
|
||||
resolved "https://registry.yarnpkg.com/@storybook/channel-websocket/-/channel-websocket-6.4.21.tgz#46db7dbfb9a37907ab12ba2632c46070557b5a97"
|
||||
@ -4343,6 +4471,17 @@
|
||||
global "^4.4.0"
|
||||
telejson "^5.3.2"
|
||||
|
||||
"@storybook/channel-websocket@6.4.22":
|
||||
version "6.4.22"
|
||||
resolved "https://registry.yarnpkg.com/@storybook/channel-websocket/-/channel-websocket-6.4.22.tgz#d541f69125873123c453757e2b879a75a9266c65"
|
||||
integrity sha512-Bm/FcZ4Su4SAK5DmhyKKfHkr7HiHBui6PNutmFkASJInrL9wBduBfN8YQYaV7ztr8ezoHqnYRx8sj28jpwa6NA==
|
||||
dependencies:
|
||||
"@storybook/channels" "6.4.22"
|
||||
"@storybook/client-logger" "6.4.22"
|
||||
core-js "^3.8.2"
|
||||
global "^4.4.0"
|
||||
telejson "^5.3.2"
|
||||
|
||||
"@storybook/channels@6.4.21":
|
||||
version "6.4.21"
|
||||
resolved "https://registry.yarnpkg.com/@storybook/channels/-/channels-6.4.21.tgz#0f1924963f77ec0c3d82aa643a246824ca9f5fca"
|
||||
@ -4352,6 +4491,15 @@
|
||||
ts-dedent "^2.0.0"
|
||||
util-deprecate "^1.0.2"
|
||||
|
||||
"@storybook/channels@6.4.22":
|
||||
version "6.4.22"
|
||||
resolved "https://registry.yarnpkg.com/@storybook/channels/-/channels-6.4.22.tgz#710f732763d63f063f615898ab1afbe74e309596"
|
||||
integrity sha512-cfR74tu7MLah1A8Rru5sak71I+kH2e/sY6gkpVmlvBj4hEmdZp4Puj9PTeaKcMXh9DgIDPNA5mb8yvQH6VcyxQ==
|
||||
dependencies:
|
||||
core-js "^3.8.2"
|
||||
ts-dedent "^2.0.0"
|
||||
util-deprecate "^1.0.2"
|
||||
|
||||
"@storybook/client-api@6.4.21":
|
||||
version "6.4.21"
|
||||
resolved "https://registry.yarnpkg.com/@storybook/client-api/-/client-api-6.4.21.tgz#6dcf41a9e55b5e38638cd4d032f1ceaec305e0eb"
|
||||
@ -4378,6 +4526,32 @@
|
||||
ts-dedent "^2.0.0"
|
||||
util-deprecate "^1.0.2"
|
||||
|
||||
"@storybook/client-api@6.4.22":
|
||||
version "6.4.22"
|
||||
resolved "https://registry.yarnpkg.com/@storybook/client-api/-/client-api-6.4.22.tgz#df14f85e7900b94354c26c584bab53a67c47eae9"
|
||||
integrity sha512-sO6HJNtrrdit7dNXQcZMdlmmZG1k6TswH3gAyP/DoYajycrTwSJ6ovkarzkO+0QcJ+etgra4TEdTIXiGHBMe/A==
|
||||
dependencies:
|
||||
"@storybook/addons" "6.4.22"
|
||||
"@storybook/channel-postmessage" "6.4.22"
|
||||
"@storybook/channels" "6.4.22"
|
||||
"@storybook/client-logger" "6.4.22"
|
||||
"@storybook/core-events" "6.4.22"
|
||||
"@storybook/csf" "0.0.2--canary.87bc651.0"
|
||||
"@storybook/store" "6.4.22"
|
||||
"@types/qs" "^6.9.5"
|
||||
"@types/webpack-env" "^1.16.0"
|
||||
core-js "^3.8.2"
|
||||
fast-deep-equal "^3.1.3"
|
||||
global "^4.4.0"
|
||||
lodash "^4.17.21"
|
||||
memoizerific "^1.11.3"
|
||||
qs "^6.10.0"
|
||||
regenerator-runtime "^0.13.7"
|
||||
store2 "^2.12.0"
|
||||
synchronous-promise "^2.0.15"
|
||||
ts-dedent "^2.0.0"
|
||||
util-deprecate "^1.0.2"
|
||||
|
||||
"@storybook/client-logger@6.4.21":
|
||||
version "6.4.21"
|
||||
resolved "https://registry.yarnpkg.com/@storybook/client-logger/-/client-logger-6.4.21.tgz#7df21cec4d5426669e828af59232ec44ea19c81a"
|
||||
@ -4386,6 +4560,14 @@
|
||||
core-js "^3.8.2"
|
||||
global "^4.4.0"
|
||||
|
||||
"@storybook/client-logger@6.4.22":
|
||||
version "6.4.22"
|
||||
resolved "https://registry.yarnpkg.com/@storybook/client-logger/-/client-logger-6.4.22.tgz#51abedb7d3c9bc21921aeb153ac8a19abc625cd6"
|
||||
integrity sha512-LXhxh/lcDsdGnK8kimqfhu3C0+D2ylCSPPQNbU0IsLRmTfbpQYMdyl0XBjPdHiRVwlL7Gkw5OMjYemQgJ02zlw==
|
||||
dependencies:
|
||||
core-js "^3.8.2"
|
||||
global "^4.4.0"
|
||||
|
||||
"@storybook/components@6.4.21", "@storybook/components@^6.0.0":
|
||||
version "6.4.21"
|
||||
resolved "https://registry.yarnpkg.com/@storybook/components/-/components-6.4.21.tgz#77483ef429f96d94cf7d2d8c1af8441ef855a77d"
|
||||
@ -4416,6 +4598,36 @@
|
||||
ts-dedent "^2.0.0"
|
||||
util-deprecate "^1.0.2"
|
||||
|
||||
"@storybook/components@6.4.22":
|
||||
version "6.4.22"
|
||||
resolved "https://registry.yarnpkg.com/@storybook/components/-/components-6.4.22.tgz#4d425280240702883225b6a1f1abde7dc1a0e945"
|
||||
integrity sha512-dCbXIJF9orMvH72VtAfCQsYbe57OP7fAADtR6YTwfCw9Sm1jFuZr8JbblQ1HcrXEoJG21nOyad3Hm5EYVb/sBw==
|
||||
dependencies:
|
||||
"@popperjs/core" "^2.6.0"
|
||||
"@storybook/client-logger" "6.4.22"
|
||||
"@storybook/csf" "0.0.2--canary.87bc651.0"
|
||||
"@storybook/theming" "6.4.22"
|
||||
"@types/color-convert" "^2.0.0"
|
||||
"@types/overlayscrollbars" "^1.12.0"
|
||||
"@types/react-syntax-highlighter" "11.0.5"
|
||||
color-convert "^2.0.1"
|
||||
core-js "^3.8.2"
|
||||
fast-deep-equal "^3.1.3"
|
||||
global "^4.4.0"
|
||||
lodash "^4.17.21"
|
||||
markdown-to-jsx "^7.1.3"
|
||||
memoizerific "^1.11.3"
|
||||
overlayscrollbars "^1.13.1"
|
||||
polished "^4.0.5"
|
||||
prop-types "^15.7.2"
|
||||
react-colorful "^5.1.2"
|
||||
react-popper-tooltip "^3.1.1"
|
||||
react-syntax-highlighter "^13.5.3"
|
||||
react-textarea-autosize "^8.3.0"
|
||||
regenerator-runtime "^0.13.7"
|
||||
ts-dedent "^2.0.0"
|
||||
util-deprecate "^1.0.2"
|
||||
|
||||
"@storybook/core-client@6.4.21":
|
||||
version "6.4.21"
|
||||
resolved "https://registry.yarnpkg.com/@storybook/core-client/-/core-client-6.4.21.tgz#4882092315c884dca6118202c83a5e6758b7de57"
|
||||
@ -4442,6 +4654,32 @@
|
||||
unfetch "^4.2.0"
|
||||
util-deprecate "^1.0.2"
|
||||
|
||||
"@storybook/core-client@6.4.22":
|
||||
version "6.4.22"
|
||||
resolved "https://registry.yarnpkg.com/@storybook/core-client/-/core-client-6.4.22.tgz#9079eda8a9c8e6ba24b84962a749b1c99668cb2a"
|
||||
integrity sha512-uHg4yfCBeM6eASSVxStWRVTZrAnb4FT6X6v/xDqr4uXCpCttZLlBzrSDwPBLNNLtCa7ntRicHM8eGKIOD5lMYQ==
|
||||
dependencies:
|
||||
"@storybook/addons" "6.4.22"
|
||||
"@storybook/channel-postmessage" "6.4.22"
|
||||
"@storybook/channel-websocket" "6.4.22"
|
||||
"@storybook/client-api" "6.4.22"
|
||||
"@storybook/client-logger" "6.4.22"
|
||||
"@storybook/core-events" "6.4.22"
|
||||
"@storybook/csf" "0.0.2--canary.87bc651.0"
|
||||
"@storybook/preview-web" "6.4.22"
|
||||
"@storybook/store" "6.4.22"
|
||||
"@storybook/ui" "6.4.22"
|
||||
airbnb-js-shims "^2.2.1"
|
||||
ansi-to-html "^0.6.11"
|
||||
core-js "^3.8.2"
|
||||
global "^4.4.0"
|
||||
lodash "^4.17.21"
|
||||
qs "^6.10.0"
|
||||
regenerator-runtime "^0.13.7"
|
||||
ts-dedent "^2.0.0"
|
||||
unfetch "^4.2.0"
|
||||
util-deprecate "^1.0.2"
|
||||
|
||||
"@storybook/core-common@6.4.21":
|
||||
version "6.4.21"
|
||||
resolved "https://registry.yarnpkg.com/@storybook/core-common/-/core-common-6.4.21.tgz#7151eeb5f628bec1dc1461df2de4c51fec15ac4c"
|
||||
@ -4497,6 +4735,61 @@
|
||||
util-deprecate "^1.0.2"
|
||||
webpack "4"
|
||||
|
||||
"@storybook/core-common@6.4.22":
|
||||
version "6.4.22"
|
||||
resolved "https://registry.yarnpkg.com/@storybook/core-common/-/core-common-6.4.22.tgz#b00fa3c0625e074222a50be3196cb8052dd7f3bf"
|
||||
integrity sha512-PD3N/FJXPNRHeQS2zdgzYFtqPLdi3MLwAicbnw+U3SokcsspfsAuyYHZOYZgwO8IAEKy6iCc7TpBdiSJZ/vAKQ==
|
||||
dependencies:
|
||||
"@babel/core" "^7.12.10"
|
||||
"@babel/plugin-proposal-class-properties" "^7.12.1"
|
||||
"@babel/plugin-proposal-decorators" "^7.12.12"
|
||||
"@babel/plugin-proposal-export-default-from" "^7.12.1"
|
||||
"@babel/plugin-proposal-nullish-coalescing-operator" "^7.12.1"
|
||||
"@babel/plugin-proposal-object-rest-spread" "^7.12.1"
|
||||
"@babel/plugin-proposal-optional-chaining" "^7.12.7"
|
||||
"@babel/plugin-proposal-private-methods" "^7.12.1"
|
||||
"@babel/plugin-syntax-dynamic-import" "^7.8.3"
|
||||
"@babel/plugin-transform-arrow-functions" "^7.12.1"
|
||||
"@babel/plugin-transform-block-scoping" "^7.12.12"
|
||||
"@babel/plugin-transform-classes" "^7.12.1"
|
||||
"@babel/plugin-transform-destructuring" "^7.12.1"
|
||||
"@babel/plugin-transform-for-of" "^7.12.1"
|
||||
"@babel/plugin-transform-parameters" "^7.12.1"
|
||||
"@babel/plugin-transform-shorthand-properties" "^7.12.1"
|
||||
"@babel/plugin-transform-spread" "^7.12.1"
|
||||
"@babel/preset-env" "^7.12.11"
|
||||
"@babel/preset-react" "^7.12.10"
|
||||
"@babel/preset-typescript" "^7.12.7"
|
||||
"@babel/register" "^7.12.1"
|
||||
"@storybook/node-logger" "6.4.22"
|
||||
"@storybook/semver" "^7.3.2"
|
||||
"@types/node" "^14.0.10"
|
||||
"@types/pretty-hrtime" "^1.0.0"
|
||||
babel-loader "^8.0.0"
|
||||
babel-plugin-macros "^3.0.1"
|
||||
babel-plugin-polyfill-corejs3 "^0.1.0"
|
||||
chalk "^4.1.0"
|
||||
core-js "^3.8.2"
|
||||
express "^4.17.1"
|
||||
file-system-cache "^1.0.5"
|
||||
find-up "^5.0.0"
|
||||
fork-ts-checker-webpack-plugin "^6.0.4"
|
||||
fs-extra "^9.0.1"
|
||||
glob "^7.1.6"
|
||||
handlebars "^4.7.7"
|
||||
interpret "^2.2.0"
|
||||
json5 "^2.1.3"
|
||||
lazy-universal-dotenv "^3.0.1"
|
||||
picomatch "^2.3.0"
|
||||
pkg-dir "^5.0.0"
|
||||
pretty-hrtime "^1.0.3"
|
||||
resolve-from "^5.0.0"
|
||||
slash "^3.0.0"
|
||||
telejson "^5.3.2"
|
||||
ts-dedent "^2.0.0"
|
||||
util-deprecate "^1.0.2"
|
||||
webpack "4"
|
||||
|
||||
"@storybook/core-events@6.4.21", "@storybook/core-events@^6.0.0":
|
||||
version "6.4.21"
|
||||
resolved "https://registry.yarnpkg.com/@storybook/core-events/-/core-events-6.4.21.tgz#28fff8b10c0d564259edf4439ff8677615ce59c0"
|
||||
@ -4504,6 +4797,13 @@
|
||||
dependencies:
|
||||
core-js "^3.8.2"
|
||||
|
||||
"@storybook/core-events@6.4.22":
|
||||
version "6.4.22"
|
||||
resolved "https://registry.yarnpkg.com/@storybook/core-events/-/core-events-6.4.22.tgz#c09b0571951affd4254028b8958a4d8652700989"
|
||||
integrity sha512-5GYY5+1gd58Gxjqex27RVaX6qbfIQmJxcbzbNpXGNSqwqAuIIepcV1rdCVm6I4C3Yb7/AQ3cN5dVbf33QxRIwA==
|
||||
dependencies:
|
||||
core-js "^3.8.2"
|
||||
|
||||
"@storybook/core-server@6.4.21":
|
||||
version "6.4.21"
|
||||
resolved "https://registry.yarnpkg.com/@storybook/core-server/-/core-server-6.4.21.tgz#3f60c68bb21fd1b07113b2bbaefd6e0498bdbd68"
|
||||
@ -4552,6 +4852,54 @@
|
||||
webpack "4"
|
||||
ws "^8.2.3"
|
||||
|
||||
"@storybook/core-server@~6.4.12":
|
||||
version "6.4.22"
|
||||
resolved "https://registry.yarnpkg.com/@storybook/core-server/-/core-server-6.4.22.tgz#254409ec2ba49a78b23f5e4a4c0faea5a570a32b"
|
||||
integrity sha512-wFh3e2fa0un1d4+BJP+nd3FVWUO7uHTqv3OGBfOmzQMKp4NU1zaBNdSQG7Hz6mw0fYPBPZgBjPfsJRwIYLLZyw==
|
||||
dependencies:
|
||||
"@discoveryjs/json-ext" "^0.5.3"
|
||||
"@storybook/builder-webpack4" "6.4.22"
|
||||
"@storybook/core-client" "6.4.22"
|
||||
"@storybook/core-common" "6.4.22"
|
||||
"@storybook/core-events" "6.4.22"
|
||||
"@storybook/csf" "0.0.2--canary.87bc651.0"
|
||||
"@storybook/csf-tools" "6.4.22"
|
||||
"@storybook/manager-webpack4" "6.4.22"
|
||||
"@storybook/node-logger" "6.4.22"
|
||||
"@storybook/semver" "^7.3.2"
|
||||
"@storybook/store" "6.4.22"
|
||||
"@types/node" "^14.0.10"
|
||||
"@types/node-fetch" "^2.5.7"
|
||||
"@types/pretty-hrtime" "^1.0.0"
|
||||
"@types/webpack" "^4.41.26"
|
||||
better-opn "^2.1.1"
|
||||
boxen "^5.1.2"
|
||||
chalk "^4.1.0"
|
||||
cli-table3 "^0.6.1"
|
||||
commander "^6.2.1"
|
||||
compression "^1.7.4"
|
||||
core-js "^3.8.2"
|
||||
cpy "^8.1.2"
|
||||
detect-port "^1.3.0"
|
||||
express "^4.17.1"
|
||||
file-system-cache "^1.0.5"
|
||||
fs-extra "^9.0.1"
|
||||
globby "^11.0.2"
|
||||
ip "^1.1.5"
|
||||
lodash "^4.17.21"
|
||||
node-fetch "^2.6.1"
|
||||
pretty-hrtime "^1.0.3"
|
||||
prompts "^2.4.0"
|
||||
regenerator-runtime "^0.13.7"
|
||||
serve-favicon "^2.5.0"
|
||||
slash "^3.0.0"
|
||||
telejson "^5.3.3"
|
||||
ts-dedent "^2.0.0"
|
||||
util-deprecate "^1.0.2"
|
||||
watchpack "^2.2.0"
|
||||
webpack "4"
|
||||
ws "^8.2.3"
|
||||
|
||||
"@storybook/core@6.4.21":
|
||||
version "6.4.21"
|
||||
resolved "https://registry.yarnpkg.com/@storybook/core/-/core-6.4.21.tgz#d92a60a6014df5f88902edfe4fadf1cbdd9ba238"
|
||||
@ -4583,6 +4931,29 @@
|
||||
regenerator-runtime "^0.13.7"
|
||||
ts-dedent "^2.0.0"
|
||||
|
||||
"@storybook/csf-tools@6.4.22":
|
||||
version "6.4.22"
|
||||
resolved "https://registry.yarnpkg.com/@storybook/csf-tools/-/csf-tools-6.4.22.tgz#f6d64bcea1b36114555972acae66a1dbe9e34b5c"
|
||||
integrity sha512-LMu8MZAiQspJAtMBLU2zitsIkqQv7jOwX7ih5JrXlyaDticH7l2j6Q+1mCZNWUOiMTizj0ivulmUsSaYbpToSw==
|
||||
dependencies:
|
||||
"@babel/core" "^7.12.10"
|
||||
"@babel/generator" "^7.12.11"
|
||||
"@babel/parser" "^7.12.11"
|
||||
"@babel/plugin-transform-react-jsx" "^7.12.12"
|
||||
"@babel/preset-env" "^7.12.11"
|
||||
"@babel/traverse" "^7.12.11"
|
||||
"@babel/types" "^7.12.11"
|
||||
"@mdx-js/mdx" "^1.6.22"
|
||||
"@storybook/csf" "0.0.2--canary.87bc651.0"
|
||||
core-js "^3.8.2"
|
||||
fs-extra "^9.0.1"
|
||||
global "^4.4.0"
|
||||
js-string-escape "^1.0.1"
|
||||
lodash "^4.17.21"
|
||||
prettier ">=2.2.1 <=2.3.0"
|
||||
regenerator-runtime "^0.13.7"
|
||||
ts-dedent "^2.0.0"
|
||||
|
||||
"@storybook/csf@0.0.2--canary.87bc651.0":
|
||||
version "0.0.2--canary.87bc651.0"
|
||||
resolved "https://registry.yarnpkg.com/@storybook/csf/-/csf-0.0.2--canary.87bc651.0.tgz#c7b99b3a344117ef67b10137b6477a3d2750cf44"
|
||||
@ -4632,6 +5003,48 @@
|
||||
webpack-dev-middleware "^3.7.3"
|
||||
webpack-virtual-modules "^0.2.2"
|
||||
|
||||
"@storybook/manager-webpack4@6.4.22":
|
||||
version "6.4.22"
|
||||
resolved "https://registry.yarnpkg.com/@storybook/manager-webpack4/-/manager-webpack4-6.4.22.tgz#eabd674beee901c7f755d9b679e9f969cbab636d"
|
||||
integrity sha512-nzhDMJYg0vXdcG0ctwE6YFZBX71+5NYaTGkxg3xT7gbgnP1YFXn9gVODvgq3tPb3gcRapjyOIxUa20rV+r8edA==
|
||||
dependencies:
|
||||
"@babel/core" "^7.12.10"
|
||||
"@babel/plugin-transform-template-literals" "^7.12.1"
|
||||
"@babel/preset-react" "^7.12.10"
|
||||
"@storybook/addons" "6.4.22"
|
||||
"@storybook/core-client" "6.4.22"
|
||||
"@storybook/core-common" "6.4.22"
|
||||
"@storybook/node-logger" "6.4.22"
|
||||
"@storybook/theming" "6.4.22"
|
||||
"@storybook/ui" "6.4.22"
|
||||
"@types/node" "^14.0.10"
|
||||
"@types/webpack" "^4.41.26"
|
||||
babel-loader "^8.0.0"
|
||||
case-sensitive-paths-webpack-plugin "^2.3.0"
|
||||
chalk "^4.1.0"
|
||||
core-js "^3.8.2"
|
||||
css-loader "^3.6.0"
|
||||
express "^4.17.1"
|
||||
file-loader "^6.2.0"
|
||||
file-system-cache "^1.0.5"
|
||||
find-up "^5.0.0"
|
||||
fs-extra "^9.0.1"
|
||||
html-webpack-plugin "^4.0.0"
|
||||
node-fetch "^2.6.1"
|
||||
pnp-webpack-plugin "1.6.4"
|
||||
read-pkg-up "^7.0.1"
|
||||
regenerator-runtime "^0.13.7"
|
||||
resolve-from "^5.0.0"
|
||||
style-loader "^1.3.0"
|
||||
telejson "^5.3.2"
|
||||
terser-webpack-plugin "^4.2.3"
|
||||
ts-dedent "^2.0.0"
|
||||
url-loader "^4.1.1"
|
||||
util-deprecate "^1.0.2"
|
||||
webpack "4"
|
||||
webpack-dev-middleware "^3.7.3"
|
||||
webpack-virtual-modules "^0.2.2"
|
||||
|
||||
"@storybook/manager-webpack5@~6.4.12":
|
||||
version "6.4.21"
|
||||
resolved "https://registry.yarnpkg.com/@storybook/manager-webpack5/-/manager-webpack5-6.4.21.tgz#f8f20c03bed8c3911a3678e637feef1d36bb45f5"
|
||||
@ -4693,6 +5106,17 @@
|
||||
npmlog "^5.0.1"
|
||||
pretty-hrtime "^1.0.3"
|
||||
|
||||
"@storybook/node-logger@6.4.22":
|
||||
version "6.4.22"
|
||||
resolved "https://registry.yarnpkg.com/@storybook/node-logger/-/node-logger-6.4.22.tgz#c4ec00f8714505f44eda7671bc88bb44abf7ae59"
|
||||
integrity sha512-sUXYFqPxiqM7gGH7gBXvO89YEO42nA4gBicJKZjj9e+W4QQLrftjF9l+mAw2K0mVE10Bn7r4pfs5oEZ0aruyyA==
|
||||
dependencies:
|
||||
"@types/npmlog" "^4.1.2"
|
||||
chalk "^4.1.0"
|
||||
core-js "^3.8.2"
|
||||
npmlog "^5.0.1"
|
||||
pretty-hrtime "^1.0.3"
|
||||
|
||||
"@storybook/postinstall@6.4.21":
|
||||
version "6.4.21"
|
||||
resolved "https://registry.yarnpkg.com/@storybook/postinstall/-/postinstall-6.4.21.tgz#1a0dc4ae0c8bf73fcda3d2abf6f22477dce0a908"
|
||||
@ -4722,6 +5146,28 @@
|
||||
unfetch "^4.2.0"
|
||||
util-deprecate "^1.0.2"
|
||||
|
||||
"@storybook/preview-web@6.4.22":
|
||||
version "6.4.22"
|
||||
resolved "https://registry.yarnpkg.com/@storybook/preview-web/-/preview-web-6.4.22.tgz#58bfc6492503ff4265b50f42a27ea8b0bfcf738a"
|
||||
integrity sha512-sWS+sgvwSvcNY83hDtWUUL75O2l2LY/GTAS0Zp2dh3WkObhtuJ/UehftzPZlZmmv7PCwhb4Q3+tZDKzMlFxnKQ==
|
||||
dependencies:
|
||||
"@storybook/addons" "6.4.22"
|
||||
"@storybook/channel-postmessage" "6.4.22"
|
||||
"@storybook/client-logger" "6.4.22"
|
||||
"@storybook/core-events" "6.4.22"
|
||||
"@storybook/csf" "0.0.2--canary.87bc651.0"
|
||||
"@storybook/store" "6.4.22"
|
||||
ansi-to-html "^0.6.11"
|
||||
core-js "^3.8.2"
|
||||
global "^4.4.0"
|
||||
lodash "^4.17.21"
|
||||
qs "^6.10.0"
|
||||
regenerator-runtime "^0.13.7"
|
||||
synchronous-promise "^2.0.15"
|
||||
ts-dedent "^2.0.0"
|
||||
unfetch "^4.2.0"
|
||||
util-deprecate "^1.0.2"
|
||||
|
||||
"@storybook/react-docgen-typescript-plugin@1.0.2-canary.253f8c1.0":
|
||||
version "1.0.2-canary.253f8c1.0"
|
||||
resolved "https://registry.yarnpkg.com/@storybook/react-docgen-typescript-plugin/-/react-docgen-typescript-plugin-1.0.2-canary.253f8c1.0.tgz#f2da40e6aae4aa586c2fb284a4a1744602c3c7fa"
|
||||
@ -4782,6 +5228,23 @@
|
||||
react-router-dom "^6.0.0"
|
||||
ts-dedent "^2.0.0"
|
||||
|
||||
"@storybook/router@6.4.22":
|
||||
version "6.4.22"
|
||||
resolved "https://registry.yarnpkg.com/@storybook/router/-/router-6.4.22.tgz#e3cc5cd8595668a367e971efb9695bbc122ed95e"
|
||||
integrity sha512-zeuE8ZgFhNerQX8sICQYNYL65QEi3okyzw7ynF58Ud6nRw4fMxSOHcj2T+nZCIU5ufozRL4QWD/Rg9P2s/HtLw==
|
||||
dependencies:
|
||||
"@storybook/client-logger" "6.4.22"
|
||||
core-js "^3.8.2"
|
||||
fast-deep-equal "^3.1.3"
|
||||
global "^4.4.0"
|
||||
history "5.0.0"
|
||||
lodash "^4.17.21"
|
||||
memoizerific "^1.11.3"
|
||||
qs "^6.10.0"
|
||||
react-router "^6.0.0"
|
||||
react-router-dom "^6.0.0"
|
||||
ts-dedent "^2.0.0"
|
||||
|
||||
"@storybook/semver@^7.3.2":
|
||||
version "7.3.2"
|
||||
resolved "https://registry.yarnpkg.com/@storybook/semver/-/semver-7.3.2.tgz#f3b9c44a1c9a0b933c04e66d0048fcf2fa10dac0"
|
||||
@ -4827,6 +5290,27 @@
|
||||
ts-dedent "^2.0.0"
|
||||
util-deprecate "^1.0.2"
|
||||
|
||||
"@storybook/store@6.4.22":
|
||||
version "6.4.22"
|
||||
resolved "https://registry.yarnpkg.com/@storybook/store/-/store-6.4.22.tgz#f291fbe3639f14d25f875cac86abb209a97d4e2a"
|
||||
integrity sha512-lrmcZtYJLc2emO+1l6AG4Txm9445K6Pyv9cGAuhOJ9Kks0aYe0YtvMkZVVry0RNNAIv6Ypz72zyKc/QK+tZLAQ==
|
||||
dependencies:
|
||||
"@storybook/addons" "6.4.22"
|
||||
"@storybook/client-logger" "6.4.22"
|
||||
"@storybook/core-events" "6.4.22"
|
||||
"@storybook/csf" "0.0.2--canary.87bc651.0"
|
||||
core-js "^3.8.2"
|
||||
fast-deep-equal "^3.1.3"
|
||||
global "^4.4.0"
|
||||
lodash "^4.17.21"
|
||||
memoizerific "^1.11.3"
|
||||
regenerator-runtime "^0.13.7"
|
||||
slash "^3.0.0"
|
||||
stable "^0.1.8"
|
||||
synchronous-promise "^2.0.15"
|
||||
ts-dedent "^2.0.0"
|
||||
util-deprecate "^1.0.2"
|
||||
|
||||
"@storybook/theming@6.4.21", "@storybook/theming@^6.0.0":
|
||||
version "6.4.21"
|
||||
resolved "https://registry.yarnpkg.com/@storybook/theming/-/theming-6.4.21.tgz#ea1a33be70c654cb31e5b38fae93f72171e88ef8"
|
||||
@ -4845,6 +5329,24 @@
|
||||
resolve-from "^5.0.0"
|
||||
ts-dedent "^2.0.0"
|
||||
|
||||
"@storybook/theming@6.4.22":
|
||||
version "6.4.22"
|
||||
resolved "https://registry.yarnpkg.com/@storybook/theming/-/theming-6.4.22.tgz#19097eec0366447ddd0d6917b0e0f81d0ec5e51e"
|
||||
integrity sha512-NVMKH/jxSPtnMTO4VCN1k47uztq+u9fWv4GSnzq/eezxdGg9ceGL4/lCrNGoNajht9xbrsZ4QvsJ/V2sVGM8wA==
|
||||
dependencies:
|
||||
"@emotion/core" "^10.1.1"
|
||||
"@emotion/is-prop-valid" "^0.8.6"
|
||||
"@emotion/styled" "^10.0.27"
|
||||
"@storybook/client-logger" "6.4.22"
|
||||
core-js "^3.8.2"
|
||||
deep-object-diff "^1.1.0"
|
||||
emotion-theming "^10.0.27"
|
||||
global "^4.4.0"
|
||||
memoizerific "^1.11.3"
|
||||
polished "^4.0.5"
|
||||
resolve-from "^5.0.0"
|
||||
ts-dedent "^2.0.0"
|
||||
|
||||
"@storybook/ui@6.4.21":
|
||||
version "6.4.21"
|
||||
resolved "https://registry.yarnpkg.com/@storybook/ui/-/ui-6.4.21.tgz#03b0ba66663f70b706ca29481bedf08a468dad3d"
|
||||
@ -4879,6 +5381,40 @@
|
||||
resolve-from "^5.0.0"
|
||||
store2 "^2.12.0"
|
||||
|
||||
"@storybook/ui@6.4.22":
|
||||
version "6.4.22"
|
||||
resolved "https://registry.yarnpkg.com/@storybook/ui/-/ui-6.4.22.tgz#49badd7994465d78d984ca4c42533c1c22201c46"
|
||||
integrity sha512-UVjMoyVsqPr+mkS1L7m30O/xrdIEgZ5SCWsvqhmyMUok3F3tRB+6M+OA5Yy+cIVfvObpA7MhxirUT1elCGXsWQ==
|
||||
dependencies:
|
||||
"@emotion/core" "^10.1.1"
|
||||
"@storybook/addons" "6.4.22"
|
||||
"@storybook/api" "6.4.22"
|
||||
"@storybook/channels" "6.4.22"
|
||||
"@storybook/client-logger" "6.4.22"
|
||||
"@storybook/components" "6.4.22"
|
||||
"@storybook/core-events" "6.4.22"
|
||||
"@storybook/router" "6.4.22"
|
||||
"@storybook/semver" "^7.3.2"
|
||||
"@storybook/theming" "6.4.22"
|
||||
copy-to-clipboard "^3.3.1"
|
||||
core-js "^3.8.2"
|
||||
core-js-pure "^3.8.2"
|
||||
downshift "^6.0.15"
|
||||
emotion-theming "^10.0.27"
|
||||
fuse.js "^3.6.1"
|
||||
global "^4.4.0"
|
||||
lodash "^4.17.21"
|
||||
markdown-to-jsx "^7.1.3"
|
||||
memoizerific "^1.11.3"
|
||||
polished "^4.0.5"
|
||||
qs "^6.10.0"
|
||||
react-draggable "^4.4.3"
|
||||
react-helmet-async "^1.0.7"
|
||||
react-sizeme "^3.0.1"
|
||||
regenerator-runtime "^0.13.7"
|
||||
resolve-from "^5.0.0"
|
||||
store2 "^2.12.0"
|
||||
|
||||
"@svgr/babel-plugin-add-jsx-attribute@^5.4.0":
|
||||
version "5.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-5.4.0.tgz#81ef61947bb268eb9d50523446f9c638fb355906"
|
||||
|
Loading…
Reference in New Issue
Block a user