Merge remote-tracking branch 'origin/master' into feat/127-generalised-stats-page

# Conflicts:
#	apps/stats/jest.config.js
#	libs/network-stats/src/config/types.ts
#	tsconfig.base.json
#	workspace.json
This commit is contained in:
sam-keen 2022-03-24 12:21:44 +00:00
commit af5832fa9e
50 changed files with 1374 additions and 102 deletions

View File

@ -1,4 +1,23 @@
Feature: Home page
Scenario: Visit Home page
Scenario Outline: Succesfful search for specific id by <IdType>
Given I am on the homepage
When I search for '<Id>'
Then I am redirected to page containing id '<Id>'
Examples:
| IdType | Id |
| Block Id | 973624 |
| Tx Hash | 9ED3718AA8308E7E08EC588EE7AADAF49711D2138860D8914B4D81A2054D9FB8 |
| Tx Id | 0x61DCCEBB955087F50D0B85382DAE138EDA9631BF1A4F92E563D528904AA38898 |
Scenario Outline: Error message displayed when invalid search by <invalidType>
Given I am on the homepage
When I search for '<Id>'
Then search error message "<errorMessage>" is displayed
Examples:
| invalidType | Id | errorMessage |
| wrong string length | 9ED3718AA8308E7E08EC588EE7AADAF497D2138860D8914B4D81A2054D9FB8 | Something doesn't look right |
| invalid hash | 9ED3718AA8308E7E08ECht8EE753DAF49711D2138860D8914B4D81A2054D9FB8 | Transaction is not hexadecimal |
| empty search | | Search required |

View File

@ -9,7 +9,9 @@ export default class BasePage {
networkParametersUrl = '/network-parameters';
validatorsUrl = '/validators';
blockExplorerHeader = 'explorer-header';
searchField = 'search-input';
searchField = 'search';
searchButton = 'search-button';
searchError = 'search-error';
navigateToTxs() {
cy.get(`a[href='${this.transactionsUrl}']`).click();
@ -48,8 +50,18 @@ export default class BasePage {
}
search(searchText) {
if (searchText) {
cy.getByTestId(this.searchField).type(searchText);
}
}
clickSearch() {
cy.getByTestId(this.searchButton).click();
}
validateUrl(expectedUrl) {
cy.url().should('include', expectedUrl);
}
validateSearchDisplayed() {
cy.getByTestId(this.blockExplorerHeader).should(
@ -59,6 +71,10 @@ export default class BasePage {
cy.getByTestId(this.searchField).should('be.visible');
}
validateSearchErrorDisplayed(errorMessage) {
cy.getByTestId(this.searchError).should('have.text', errorMessage);
}
validateBlockDataDisplayed(headerTestId) {
cy.getByTestId(headerTestId).then(($assetHeaders) => {
const headersAmount = parseInt($assetHeaders.length);
@ -68,7 +84,7 @@ export default class BasePage {
});
cy.get('.language-json')
.each(($asset, index, $list) => {
.each(($asset) => {
expect($asset).to.not.be.empty;
})
.then(($list) => {

View File

@ -1,5 +1,20 @@
import { Given, Then, When } from 'cypress-cucumber-preprocessor/steps';
import BasePage from '../pages/base-page';
const basePage = new BasePage();
Given('I am on the homepage', () => {
cy.visit('/');
});
When('I search for {string}', (searchText) => {
basePage.search(searchText);
basePage.clickSearch();
});
Then('I am redirected to page containing id {string}', (expectedUrl) => {
basePage.validateUrl(expectedUrl);
});
Then('search error message {string} is displayed', (expectedErrorMsg) => {
basePage.validateSearchErrorDisplayed(expectedErrorMsg);
});

View File

@ -1,4 +1,4 @@
import { Given, Then, When } from 'cypress-cucumber-preprocessor/steps';
import { Then, When } from 'cypress-cucumber-preprocessor/steps';
import NetworkPage from '../pages/network-page';
const networkPage = new NetworkPage();

View File

@ -7,4 +7,5 @@ module.exports = {
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
coverageDirectory: '../../coverage/apps/stats',
setupFilesAfterEnv: ['./src/setup-tests.ts'],
};

View File

@ -0,0 +1,27 @@
import { Splash } from '@vegaprotocol/ui-toolkit';
import { ReactNode } from 'react';
interface AsyncRendererProps<T> {
loading: boolean;
error: Error | undefined;
data: T;
children: (data: T) => ReactNode;
}
// eslint-disable-next-line
export function AsyncRenderer<T = any>({
loading,
error,
data,
children,
}: AsyncRendererProps<T>) {
if (error) {
return <Splash>Something went wrong: {error.message}</Splash>;
}
if (loading) {
return <Splash>Loading...</Splash>;
}
return <>{children(data)}</>;
}

View File

@ -0,0 +1 @@
export * from './async-renderer';

View File

@ -4,10 +4,7 @@ import {
TransactionState,
VegaTxStatus,
} from '../../hooks/use-vega-transaction';
import {
OrderEvent_busEvents_event_Order,
OrderStatus,
} from '@vegaprotocol/graphql';
import { OrderEvent_busEvents_event_Order } from '@vegaprotocol/graphql';
import { formatNumber } from '@vegaprotocol/react-helpers';
interface OrderDialogProps {

View File

@ -0,0 +1 @@
export * from './order-list-container';

View File

@ -0,0 +1,41 @@
import { OrderListContainer } from './order-list-container';
import * as useOrdersHook from '../../hooks/use-orders';
import { render, screen } from '@testing-library/react';
import { Orders_party_orders } from '@vegaprotocol/graphql';
jest.mock('@vegaprotocol/order-list', () => ({
OrderList: () => <div>OrderList</div>,
}));
test('Renders a loading state while awaiting orders', () => {
jest.spyOn(useOrdersHook, 'useOrders').mockReturnValue({
orders: [],
loading: true,
error: null,
});
render(<OrderListContainer />);
expect(screen.getByText('Loading...')).toBeInTheDocument();
});
test('Renders an error state', () => {
const errorMsg = 'Oops! An Error';
jest.spyOn(useOrdersHook, 'useOrders').mockReturnValue({
orders: [],
loading: false,
error: new Error(errorMsg),
});
render(<OrderListContainer />);
expect(
screen.getByText(`Something went wrong: ${errorMsg}`)
).toBeInTheDocument();
});
test('Renders the order list if orders provided', () => {
jest.spyOn(useOrdersHook, 'useOrders').mockReturnValue({
orders: [{ id: '1' } as Orders_party_orders],
loading: false,
error: null,
});
render(<OrderListContainer />);
expect(screen.getByText('OrderList')).toBeInTheDocument();
});

View File

@ -0,0 +1,14 @@
import { useOrders } from '../../hooks/use-orders';
import { OrderList } from '@vegaprotocol/order-list';
import { AsyncRenderer } from '../async-renderer';
import { OrderFields } from '@vegaprotocol/graphql';
export const OrderListContainer = () => {
const { orders, loading, error } = useOrders();
return (
<AsyncRenderer<OrderFields[]> loading={loading} error={error} data={orders}>
{(data) => <OrderList orders={data} />}
</AsyncRenderer>
);
};

View File

@ -1,7 +1,7 @@
import { OperationVariables, QueryHookOptions, useQuery } from '@apollo/client';
import { DocumentNode } from 'graphql';
import { ReactNode } from 'react';
import { Splash } from '@vegaprotocol/ui-toolkit';
import { AsyncRenderer } from '../async-renderer';
interface PageQueryContainerProps<TData, TVariables> {
query: DocumentNode;
@ -16,13 +16,13 @@ export const PageQueryContainer = <TData, TVariables = OperationVariables>({
}: PageQueryContainerProps<TData, TVariables>) => {
const { data, loading, error } = useQuery<TData, TVariables>(query, options);
if (loading || !data) {
return <Splash>Loading...</Splash>;
}
if (error) {
return <Splash>Something went wrong: {error.message}</Splash>;
}
return <>{children(data)}</>;
return (
<AsyncRenderer<TData>
loading={loading || Boolean(!data)}
error={error}
data={data}
>
{(data) => children(data)}
</AsyncRenderer>
);
};

View File

@ -1,5 +1,4 @@
import { gql, useApolloClient } from '@apollo/client';
import { singletonHook } from 'react-singleton-hook';
import {
Markets,
Markets_markets,
@ -56,7 +55,13 @@ const MARKET_DATA_SUB = gql`
}
`;
export const useMarketsImpl = () => {
interface UseMarkets {
markets: Markets_markets[];
error: Error | null;
loading: boolean;
}
export const useMarkets = (): UseMarkets => {
const client = useApolloClient();
const [markets, setMarkets] = useState<Markets_markets[]>([]);
const [error, setError] = useState<Error | null>(null);
@ -121,11 +126,3 @@ export const useMarketsImpl = () => {
return { markets, error, loading };
};
const initial = {
markets: [],
error: null,
loading: false,
};
export const useMarkets = singletonHook(initial, useMarketsImpl);

View File

@ -0,0 +1,165 @@
import { MockedProvider, MockedResponse } from '@apollo/client/testing';
import { renderHook } from '@testing-library/react-hooks';
import {
OrderFields,
Orders,
OrderStatus,
OrderSub,
OrderTimeInForce,
OrderType,
Side,
} from '@vegaprotocol/graphql';
import {
VegaKeyExtended,
VegaWalletContext,
VegaWalletContextShape,
} from '@vegaprotocol/wallet';
import { ReactNode } from 'react';
import { ORDERS_QUERY, ORDERS_SUB, useOrders } from './use-orders';
const keypair = { pub: '0x123' } as VegaKeyExtended;
const defaultWalletContext = {
keypair,
keypairs: [keypair],
sendTx: jest.fn().mockReturnValue(Promise.resolve(null)),
connect: jest.fn(),
disconnect: jest.fn(),
selectPublicKey: jest.fn(),
connector: null,
};
function generateOrder(order?: Partial<OrderFields>): OrderFields {
return {
__typename: 'Order',
id: '1',
market: {
__typename: 'Market',
id: 'market-id',
name: 'market-name',
decimalPlaces: 0,
tradableInstrument: {
__typename: 'TradableInstrument',
instrument: {
__typename: 'Instrument',
code: 'instrument-code',
},
},
},
type: OrderType.Market,
side: Side.Buy,
size: '10',
status: OrderStatus.Active,
rejectionReason: null,
price: '',
timeInForce: OrderTimeInForce.GTC,
remaining: '10',
createdAt: '2022-01-01T00:00:00',
updatedAt: null,
expiresAt: null,
...order,
};
}
function setup(
context?: Partial<VegaWalletContextShape>,
mocks: MockedResponse[] = []
) {
const wrapper = ({ children }: { children: ReactNode }) => (
<MockedProvider mocks={mocks}>
<VegaWalletContext.Provider
value={{ ...defaultWalletContext, ...context }}
>
{children}
</VegaWalletContext.Provider>
</MockedProvider>
);
return renderHook(() => useOrders(), { wrapper });
}
test('Fetches and subscribes to orders and merges appropriately', async () => {
const order = generateOrder();
const mockOrderQuery: MockedResponse<Orders> = {
request: {
query: ORDERS_QUERY,
variables: { partyId: keypair.pub },
},
result: {
data: {
party: {
__typename: 'Party',
id: keypair.pub,
orders: [order],
},
},
},
};
const updatedOrder = generateOrder({
id: '1',
remaining: '5',
updatedAt: '2022-01-01T00:01:00',
});
const newOrder = generateOrder({
id: '2',
createdAt: '2022-01-01T01:00:00',
});
const mockOrderSub: MockedResponse<OrderSub> = {
request: {
query: ORDERS_SUB,
variables: { partyId: keypair.pub },
},
result: {
data: {
orders: [updatedOrder, newOrder],
},
},
delay: 100,
};
const { result, waitForNextUpdate } = setup(defaultWalletContext, [
mockOrderQuery,
mockOrderSub,
]);
expect(result.current.loading).toBe(true);
expect(result.current.error).toBe(null);
await waitForNextUpdate();
expect(result.current.orders).toEqual([order]);
expect(result.current.loading).toBe(false);
await waitForNextUpdate();
expect(result.current.orders).toEqual([newOrder, updatedOrder]);
});
test('Returns an error if fetch fails', async () => {
const error = new Error('Something failed');
const mockFailedOrderQuery: MockedResponse<Orders> = {
request: {
query: ORDERS_QUERY,
variables: { partyId: keypair.pub },
},
error,
};
const { result, waitForNextUpdate } = setup(defaultWalletContext, [
mockFailedOrderQuery,
]);
expect(result.current.loading).toBe(true);
expect(result.current.error).toBe(null);
await waitForNextUpdate();
expect(result.current.error).toEqual(error);
expect(result.current.loading).toBe(false);
});
test('No queries are made if no pubkey provided', () => {
const mockQuery: MockedResponse<Orders> = {
request: {
query: ORDERS_QUERY,
variables: { partyId: keypair.pub },
},
newData: jest.fn(),
};
const { result } = setup(
{ ...defaultWalletContext, keypair: null, keypairs: [] },
[mockQuery]
);
expect(mockQuery.newData).not.toBeCalled();
expect(result.current.loading).toBe(false);
expect(result.current.error).toBe(null);
});

View File

@ -0,0 +1,140 @@
import { gql, useApolloClient } from '@apollo/client';
import { useCallback, useEffect, useState } from 'react';
import {
OrderSub,
OrderSubVariables,
Orders,
OrdersVariables,
OrderFields,
} from '@vegaprotocol/graphql';
import uniqBy from 'lodash/uniqBy';
import orderBy from 'lodash/orderBy';
import { useVegaWallet } from '@vegaprotocol/wallet';
const ORDER_FRAGMENT = gql`
fragment OrderFields on Order {
id
market {
id
name
decimalPlaces
tradableInstrument {
instrument {
code
}
}
}
type
side
size
status
rejectionReason
price
timeInForce
remaining
expiresAt
createdAt
updatedAt
}
`;
export const ORDERS_QUERY = gql`
${ORDER_FRAGMENT}
query Orders($partyId: ID!) {
party(id: $partyId) {
id
orders {
...OrderFields
}
}
}
`;
export const ORDERS_SUB = gql`
${ORDER_FRAGMENT}
subscription OrderSub($partyId: ID!) {
orders(partyId: $partyId) {
...OrderFields
}
}
`;
interface UseOrders {
orders: OrderFields[];
error: Error | null;
loading: boolean;
}
export const useOrders = (): UseOrders => {
const client = useApolloClient();
const { keypair } = useVegaWallet();
const [orders, setOrders] = useState<OrderFields[]>([]);
const [error, setError] = useState<Error | null>(null);
const [loading, setLoading] = useState(false);
const mergeOrders = useCallback((update: OrderFields[]) => {
// A subscription payload can contain multiple updates for a single order so we need to first
// sort them by updatedAt (or createdAt if the order hasn't been updated) with the newest first,
// then use uniqBy, which selects the first occuring order for an id to ensure we only get the latest order
setOrders((curr) => {
const sorted = orderBy(
[...curr, ...update],
(o) => {
if (!o.updatedAt) return new Date(o.createdAt).getTime();
return new Date(o.updatedAt).getTime();
},
'desc'
);
const uniq = uniqBy(sorted, 'id');
return uniq;
});
}, []);
// Make initial fetch
useEffect(() => {
const fetchOrders = async () => {
if (!keypair?.pub) return;
setLoading(true);
try {
const res = await client.query<Orders, OrdersVariables>({
query: ORDERS_QUERY,
variables: { partyId: keypair.pub },
});
if (!res.data.party?.orders.length) return;
mergeOrders(res.data.party.orders);
} catch (err) {
setError(err);
} finally {
setLoading(false);
}
};
fetchOrders();
}, [mergeOrders, keypair, client]);
// Start subscription
useEffect(() => {
if (!keypair?.pub) return;
const sub = client
.subscribe<OrderSub, OrderSubVariables>({
query: ORDERS_SUB,
variables: { partyId: keypair.pub },
})
.subscribe(({ data }) => {
mergeOrders(data.orders);
});
return () => {
if (sub) {
sub.unsubscribe();
}
};
}, [client, keypair, mergeOrders]);
return { orders, error, loading };
};

View File

@ -7,4 +7,5 @@ module.exports = {
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
coverageDirectory: '../../coverage/apps/trading',
setupFilesAfterEnv: ['./setup-tests.ts'],
};

View File

@ -5,7 +5,6 @@ import { ThemeContext } from '@vegaprotocol/react-helpers';
import { VegaConnectDialog, VegaWalletProvider } from '@vegaprotocol/wallet';
import { Connectors } from '../lib/connectors';
import { useCallback, useMemo, useState } from 'react';
import { SingletonHooksContainer } from 'react-singleton-hook';
import { createClient } from '../lib/apollo-client';
import { ThemeSwitcher } from '@vegaprotocol/ui-toolkit';
import { ApolloProvider } from '@apollo/client';
@ -35,7 +34,6 @@ function VegaTradingApp({ Component, pageProps }: AppProps) {
<ThemeContext.Provider value={theme}>
<ApolloProvider client={client}>
<VegaWalletProvider>
<SingletonHooksContainer />
<AppLoader>
<Head>
<title>Welcome to trading!</title>

View File

@ -3,7 +3,7 @@ import { Market, MarketVariables } from '@vegaprotocol/graphql';
import { Splash } from '@vegaprotocol/ui-toolkit';
import { useRouter } from 'next/router';
import React, { useEffect, useState } from 'react';
import debounce from 'lodash.debounce';
import debounce from 'lodash/debounce';
import { PageQueryContainer } from '../../components/page-query-container';
import { TradeGrid, TradePanels } from './trade-grid';

View File

@ -67,7 +67,7 @@ export const GridTabs = ({ children, group }: GridTabsProps) => {
{Children.map(children, (child) => {
if (!isValidElement(child)) return null;
return (
<Tabs.Content value={child.props.name}>
<Tabs.Content value={child.props.name} className="h-full">
{child.props.children}
</Tabs.Content>
);

View File

@ -2,27 +2,23 @@ import { Markets } from '@vegaprotocol/graphql';
import { useRouter } from 'next/router';
import { MarketListTable } from '@vegaprotocol/market-list';
import { useMarkets } from '../../hooks/use-markets';
import { Splash } from '@vegaprotocol/ui-toolkit';
import { AsyncRenderer } from '../../components/async-renderer';
const Markets = () => {
const { pathname, push } = useRouter();
const { markets, error, loading } = useMarkets();
if (error) {
return <Splash>Something went wrong: {error.message}</Splash>;
}
if (loading) {
return <Splash>Loading...</Splash>;
}
return (
<AsyncRenderer loading={loading} error={error} data={markets}>
{(data) => (
<MarketListTable
markets={markets}
markets={data}
onRowClicked={(id) =>
push(`${pathname}/${id}?portfolio=orders&trade=orderbook`)
}
/>
)}
</AsyncRenderer>
);
};

View File

@ -4,10 +4,10 @@ import AutoSizer from 'react-virtualized-auto-sizer';
import { useState, ReactNode } from 'react';
import { GridTab, GridTabs } from './grid-tabs';
import { DealTicketContainer } from '../../components/deal-ticket-container';
import { OrderListContainer } from '../..//components/order-list-container';
const Chart = () => <div>TODO: Chart</div>;
const Orderbook = () => <div>TODO: Orderbook</div>;
const Orders = () => <div>TODO: Orders</div>;
const Positions = () => <div>TODO: Positions</div>;
const Collateral = () => <div>TODO: Collateral</div>;
@ -17,7 +17,7 @@ const TradingViews = {
chart: Chart,
ticket: DealTicketContainer,
orderbook: Orderbook,
orders: Orders,
orders: OrderListContainer,
positions: Positions,
collateral: Collateral,
};

View File

@ -0,0 +1 @@
import '@testing-library/jest-dom';

View File

@ -1,26 +1,17 @@
import React from 'react';
import { render } from '@testing-library/react';
import Index from '../pages/index.page';
import { VegaWalletContext } from '@vegaprotocol/wallet';
jest.mock('@vegaprotocol/ui-toolkit', () => {
const original = jest.requireActual('@vegaprotocol/ui-toolkit');
return {
...original,
AgGridDynamic: () => <div>AgGrid</div>,
};
});
describe('Index', () => {
it('should render successfully', () => {
const { baseElement } = render(
<VegaWalletContext.Provider
value={{
keypair: null,
keypairs: null,
connect: jest.fn(),
disconnect: jest.fn(),
selectPublicKey: jest.fn(),
connector: null,
sendTx: jest.fn(),
}}
>
<Index />
</VegaWalletContext.Provider>
);
expect(baseElement).toBeTruthy();
render(<Index />);
});
});

View File

@ -12,9 +12,6 @@ const DEFAULT_ORDER: Order = {
timeInForce: OrderTimeInForce.IOC,
};
// TODO: Consider using a generated type when we have a better solution for
// sharing the types from GQL
export type TransactionStatus = 'default' | 'pending';
export interface DealTicketProps {

View File

@ -0,0 +1,115 @@
/* tslint:disable */
/* eslint-disable */
// @generated
// This file was automatically generated and should not be edited.
import { OrderType, Side, OrderStatus, OrderRejectionReason, OrderTimeInForce } from "./globalTypes";
// ====================================================
// GraphQL fragment: OrderFields
// ====================================================
export interface OrderFields_market_tradableInstrument_instrument {
__typename: "Instrument";
/**
* A short non necessarily unique code used to easily describe the instrument (e.g: FX:BTCUSD/DEC18) (string)
*/
code: string;
}
export interface OrderFields_market_tradableInstrument {
__typename: "TradableInstrument";
/**
* An instance of or reference to a fully specified instrument.
*/
instrument: OrderFields_market_tradableInstrument_instrument;
}
export interface OrderFields_market {
__typename: "Market";
/**
* Market ID
*/
id: string;
/**
* Market full name
*/
name: string;
/**
* decimalPlaces indicates the number of decimal places that an integer must be shifted by in order to get a correct
* number denominated in the currency of the Market. (uint64)
*
* Examples:
* Currency Balance decimalPlaces Real Balance
* GBP 100 0 GBP 100
* GBP 100 2 GBP 1.00
* GBP 100 4 GBP 0.01
* GBP 1 4 GBP 0.0001 ( 0.01p )
*
* GBX (pence) 100 0 GBP 1.00 (100p )
* GBX (pence) 100 2 GBP 0.01 ( 1p )
* GBX (pence) 100 4 GBP 0.0001 ( 0.01p )
* GBX (pence) 1 4 GBP 0.000001 ( 0.0001p)
*/
decimalPlaces: number;
/**
* An instance of or reference to a tradable instrument.
*/
tradableInstrument: OrderFields_market_tradableInstrument;
}
export interface OrderFields {
__typename: "Order";
/**
* Hash of the order data
*/
id: string;
/**
* The market the order is trading on (probably stored internally as a hash of the market details)
*/
market: OrderFields_market | null;
/**
* Type the order type (defaults to PARTY)
*/
type: OrderType | null;
/**
* Whether the order is to buy or sell
*/
side: Side;
/**
* Total number of contracts that may be bought or sold (immutable) (uint64)
*/
size: string;
/**
* The status of an order, for example 'Active'
*/
status: OrderStatus;
/**
* Reason for the order to be rejected
*/
rejectionReason: OrderRejectionReason | null;
/**
* The worst price the order will trade at (e.g. buy for price or less, sell for price or more) (uint64)
*/
price: string;
/**
* The timeInForce of order (determines how and if it executes, and whether it persists on the book)
*/
timeInForce: OrderTimeInForce;
/**
* Number of contracts remaining of the total that have not yet been bought or sold (uint64)
*/
remaining: string;
/**
* Expiration time of this order (ISO-8601 RFC3339+Nano formatted date)
*/
expiresAt: string | null;
/**
* RFC3339Nano formatted date and time for when the order was created (timestamp)
*/
createdAt: string;
/**
* RFC3339Nano time the order was altered
*/
updatedAt: string | null;
}

126
libs/graphql/src/__generated__/OrderSub.ts generated Normal file
View File

@ -0,0 +1,126 @@
/* tslint:disable */
/* eslint-disable */
// @generated
// This file was automatically generated and should not be edited.
import { OrderType, Side, OrderStatus, OrderRejectionReason, OrderTimeInForce } from "./globalTypes";
// ====================================================
// GraphQL subscription operation: OrderSub
// ====================================================
export interface OrderSub_orders_market_tradableInstrument_instrument {
__typename: "Instrument";
/**
* A short non necessarily unique code used to easily describe the instrument (e.g: FX:BTCUSD/DEC18) (string)
*/
code: string;
}
export interface OrderSub_orders_market_tradableInstrument {
__typename: "TradableInstrument";
/**
* An instance of or reference to a fully specified instrument.
*/
instrument: OrderSub_orders_market_tradableInstrument_instrument;
}
export interface OrderSub_orders_market {
__typename: "Market";
/**
* Market ID
*/
id: string;
/**
* Market full name
*/
name: string;
/**
* decimalPlaces indicates the number of decimal places that an integer must be shifted by in order to get a correct
* number denominated in the currency of the Market. (uint64)
*
* Examples:
* Currency Balance decimalPlaces Real Balance
* GBP 100 0 GBP 100
* GBP 100 2 GBP 1.00
* GBP 100 4 GBP 0.01
* GBP 1 4 GBP 0.0001 ( 0.01p )
*
* GBX (pence) 100 0 GBP 1.00 (100p )
* GBX (pence) 100 2 GBP 0.01 ( 1p )
* GBX (pence) 100 4 GBP 0.0001 ( 0.01p )
* GBX (pence) 1 4 GBP 0.000001 ( 0.0001p)
*/
decimalPlaces: number;
/**
* An instance of or reference to a tradable instrument.
*/
tradableInstrument: OrderSub_orders_market_tradableInstrument;
}
export interface OrderSub_orders {
__typename: "Order";
/**
* Hash of the order data
*/
id: string;
/**
* The market the order is trading on (probably stored internally as a hash of the market details)
*/
market: OrderSub_orders_market | null;
/**
* Type the order type (defaults to PARTY)
*/
type: OrderType | null;
/**
* Whether the order is to buy or sell
*/
side: Side;
/**
* Total number of contracts that may be bought or sold (immutable) (uint64)
*/
size: string;
/**
* The status of an order, for example 'Active'
*/
status: OrderStatus;
/**
* Reason for the order to be rejected
*/
rejectionReason: OrderRejectionReason | null;
/**
* The worst price the order will trade at (e.g. buy for price or less, sell for price or more) (uint64)
*/
price: string;
/**
* The timeInForce of order (determines how and if it executes, and whether it persists on the book)
*/
timeInForce: OrderTimeInForce;
/**
* Number of contracts remaining of the total that have not yet been bought or sold (uint64)
*/
remaining: string;
/**
* Expiration time of this order (ISO-8601 RFC3339+Nano formatted date)
*/
expiresAt: string | null;
/**
* RFC3339Nano formatted date and time for when the order was created (timestamp)
*/
createdAt: string;
/**
* RFC3339Nano time the order was altered
*/
updatedAt: string | null;
}
export interface OrderSub {
/**
* Subscribe to orders updates
*/
orders: OrderSub_orders[] | null;
}
export interface OrderSubVariables {
partyId: string;
}

138
libs/graphql/src/__generated__/Orders.ts generated Normal file
View File

@ -0,0 +1,138 @@
/* tslint:disable */
/* eslint-disable */
// @generated
// This file was automatically generated and should not be edited.
import { OrderType, Side, OrderStatus, OrderRejectionReason, OrderTimeInForce } from "./globalTypes";
// ====================================================
// GraphQL query operation: Orders
// ====================================================
export interface Orders_party_orders_market_tradableInstrument_instrument {
__typename: "Instrument";
/**
* A short non necessarily unique code used to easily describe the instrument (e.g: FX:BTCUSD/DEC18) (string)
*/
code: string;
}
export interface Orders_party_orders_market_tradableInstrument {
__typename: "TradableInstrument";
/**
* An instance of or reference to a fully specified instrument.
*/
instrument: Orders_party_orders_market_tradableInstrument_instrument;
}
export interface Orders_party_orders_market {
__typename: "Market";
/**
* Market ID
*/
id: string;
/**
* Market full name
*/
name: string;
/**
* decimalPlaces indicates the number of decimal places that an integer must be shifted by in order to get a correct
* number denominated in the currency of the Market. (uint64)
*
* Examples:
* Currency Balance decimalPlaces Real Balance
* GBP 100 0 GBP 100
* GBP 100 2 GBP 1.00
* GBP 100 4 GBP 0.01
* GBP 1 4 GBP 0.0001 ( 0.01p )
*
* GBX (pence) 100 0 GBP 1.00 (100p )
* GBX (pence) 100 2 GBP 0.01 ( 1p )
* GBX (pence) 100 4 GBP 0.0001 ( 0.01p )
* GBX (pence) 1 4 GBP 0.000001 ( 0.0001p)
*/
decimalPlaces: number;
/**
* An instance of or reference to a tradable instrument.
*/
tradableInstrument: Orders_party_orders_market_tradableInstrument;
}
export interface Orders_party_orders {
__typename: "Order";
/**
* Hash of the order data
*/
id: string;
/**
* The market the order is trading on (probably stored internally as a hash of the market details)
*/
market: Orders_party_orders_market | null;
/**
* Type the order type (defaults to PARTY)
*/
type: OrderType | null;
/**
* Whether the order is to buy or sell
*/
side: Side;
/**
* Total number of contracts that may be bought or sold (immutable) (uint64)
*/
size: string;
/**
* The status of an order, for example 'Active'
*/
status: OrderStatus;
/**
* Reason for the order to be rejected
*/
rejectionReason: OrderRejectionReason | null;
/**
* The worst price the order will trade at (e.g. buy for price or less, sell for price or more) (uint64)
*/
price: string;
/**
* The timeInForce of order (determines how and if it executes, and whether it persists on the book)
*/
timeInForce: OrderTimeInForce;
/**
* Number of contracts remaining of the total that have not yet been bought or sold (uint64)
*/
remaining: string;
/**
* Expiration time of this order (ISO-8601 RFC3339+Nano formatted date)
*/
expiresAt: string | null;
/**
* RFC3339Nano formatted date and time for when the order was created (timestamp)
*/
createdAt: string;
/**
* RFC3339Nano time the order was altered
*/
updatedAt: string | null;
}
export interface Orders_party {
__typename: "Party";
/**
* Party identifier
*/
id: string;
/**
* Orders relating to a party
*/
orders: Orders_party_orders[] | null;
}
export interface Orders {
/**
* An entity that is trading on the VEGA network
*/
party: Orders_party | null;
}
export interface OrdersVariables {
partyId: string;
}

View File

@ -162,6 +162,18 @@ export enum OrderStatus {
Stopped = "Stopped",
}
/**
* Valid order types, these determine what happens when an order is added to the book
*/
export enum OrderTimeInForce {
FOK = "FOK",
GFA = "GFA",
GFN = "GFN",
GTC = "GTC",
GTT = "GTT",
IOC = "IOC",
}
export enum OrderType {
Limit = "Limit",
Market = "Market",
@ -224,6 +236,14 @@ export enum ProposalState {
WaitingForNodeVote = "WaitingForNodeVote",
}
/**
* Whether the placer of an order is aiming to buy or sell on the market
*/
export enum Side {
Buy = "Buy",
Sell = "Sell",
}
export enum VoteValue {
No = "No",
Yes = "Yes",

View File

@ -9,5 +9,8 @@ export * from './__generated__/MarketDataFields';
export * from './__generated__/NetworkParametersQuery';
export * from './__generated__/NodesQuery';
export * from './__generated__/OrderEvent';
export * from './__generated__/OrderFields';
export * from './__generated__/Orders';
export * from './__generated__/OrderSub';
export * from './__generated__/PartyAssetsQuery';
export * from './__generated__/ProposalsQuery';

View File

@ -20,7 +20,7 @@ export const MarketListTable = ({
}: MarketListTableProps) => {
const [initialMarkets] = useState(markets);
const gridApi = useRef<GridApi | null>(null);
useApplyGridTransaction(markets, gridApi.current);
useApplyGridTransaction<Markets_markets>(markets, gridApi.current);
return (
<AgGrid
@ -28,6 +28,7 @@ export const MarketListTable = ({
overlayNoRowsTemplate="No markets"
rowData={initialMarkets}
getRowNodeId={(data) => data.id}
suppressCellFocus={true}
defaultColDef={{
flex: 1,
resizable: true,

View File

@ -18,13 +18,14 @@ export interface Stats {
chainId: string;
}
// @ts-ignore investigate why series of 'ors' didn't work instead of this 'any'
// eslint-disable-next-line
export type value = any;
export type goodThreshold = (...args: value[]) => boolean;
export interface StatFields {
title: string;
goodThreshold?: goodThreshold;
// eslint-disable-next-line
formatter?: (arg0: value) => any;
promoted?: boolean;
value?: value;

12
libs/order-list/.babelrc Normal file
View File

@ -0,0 +1,12 @@
{
"presets": [
[
"@nrwl/react/babel",
{
"runtime": "automatic",
"useBuiltIns": "usage"
}
]
],
"plugins": []
}

View File

@ -0,0 +1,18 @@
{
"extends": ["plugin:@nrwl/nx/react", "../../.eslintrc.json"],
"ignorePatterns": ["!**/*"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"rules": {}
},
{
"files": ["*.ts", "*.tsx"],
"rules": {}
},
{
"files": ["*.js", "*.jsx"],
"rules": {}
}
]
}

View File

@ -0,0 +1,7 @@
# order-list
This library was generated with [Nx](https://nx.dev).
## Running unit tests
Run `nx test order-list` to execute the unit tests via [Jest](https://jestjs.io).

View File

@ -0,0 +1,10 @@
module.exports = {
displayName: 'order-list',
preset: '../../jest.preset.js',
transform: {
'^.+\\.[tj]sx?$': 'babel-jest',
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
coverageDirectory: '../../coverage/libs/order-list',
setupFilesAfterEnv: ['./src/setup-tests.ts'],
};

View File

@ -0,0 +1,23 @@
{
"root": "libs/order-list",
"sourceRoot": "libs/order-list/src",
"projectType": "library",
"tags": [],
"targets": {
"lint": {
"executor": "@nrwl/linter:eslint",
"outputs": ["{options.outputFile}"],
"options": {
"lintFilePatterns": ["libs/order-list/**/*.{ts,tsx,js,jsx}"]
}
},
"test": {
"executor": "@nrwl/jest:jest",
"outputs": ["coverage/libs/order-list"],
"options": {
"jestConfig": "libs/order-list/jest.config.js",
"passWithNoTests": true
}
}
}
}

View File

@ -0,0 +1 @@
export * from './order-list';

View File

@ -0,0 +1,153 @@
import { act, render, screen } from '@testing-library/react';
import { formatNumber, getDateTimeFormat } from '@vegaprotocol/react-helpers';
import { Orders_party_orders } from '@vegaprotocol/graphql';
import {
OrderStatus,
OrderTimeInForce,
OrderType,
Side,
OrderRejectionReason,
} from '@vegaprotocol/graphql';
import { OrderList } from './order-list';
test('No orders message shown', async () => {
await act(async () => {
render(<OrderList orders={[]} />);
});
expect(screen.getByText('No orders')).toBeInTheDocument();
});
const marketOrder: Orders_party_orders = {
__typename: 'Order',
id: 'order-id',
market: {
__typename: 'Market',
id: 'market-id',
name: 'market-name',
decimalPlaces: 2,
tradableInstrument: {
__typename: 'TradableInstrument',
instrument: {
__typename: 'Instrument',
code: 'instrument-code',
},
},
},
size: '10',
type: OrderType.Market,
status: OrderStatus.Active,
side: Side.Buy,
remaining: '5',
price: '',
timeInForce: OrderTimeInForce.IOC,
createdAt: new Date().toISOString(),
updatedAt: null,
expiresAt: null,
rejectionReason: null,
};
const limitOrder: Orders_party_orders = {
__typename: 'Order',
id: 'order-id',
market: {
__typename: 'Market',
id: 'market-id',
name: 'market-name',
decimalPlaces: 2,
tradableInstrument: {
__typename: 'TradableInstrument',
instrument: {
__typename: 'Instrument',
code: 'instrument-code',
},
},
},
size: '10',
type: OrderType.Limit,
status: OrderStatus.Active,
side: Side.Sell,
remaining: '5',
price: '12345',
timeInForce: OrderTimeInForce.GTT,
createdAt: new Date('2022-3-3').toISOString(),
expiresAt: new Date('2022-3-5').toISOString(),
updatedAt: null,
rejectionReason: null,
};
test('Correct columns are rendered', async () => {
await act(async () => {
render(<OrderList orders={[marketOrder]} />);
});
const headers = screen.getAllByRole('columnheader');
expect(headers).toHaveLength(8);
expect(headers.map((h) => h.textContent?.trim())).toEqual([
'Market',
'Amount',
'Type',
'Status',
'Filled',
'Price',
'Time In Force',
'Created At',
]);
});
test('Correct formatting applied for market order', async () => {
await act(async () => {
render(<OrderList orders={[marketOrder]} />);
});
const cells = screen.getAllByRole('gridcell');
const expectedValues = [
marketOrder.market?.tradableInstrument.instrument.code,
'+10',
marketOrder.type,
marketOrder.status,
'5',
'-',
marketOrder.timeInForce,
getDateTimeFormat().format(new Date(marketOrder.createdAt)),
];
cells.forEach((cell, i) => {
expect(cell).toHaveTextContent(expectedValues[i]);
});
});
test('Correct formatting applied for GTT limit order', async () => {
await act(async () => {
render(<OrderList orders={[limitOrder]} />);
});
const cells = screen.getAllByRole('gridcell');
const expectedValues = [
limitOrder.market?.tradableInstrument.instrument.code,
'-10',
limitOrder.type,
limitOrder.status,
'5',
formatNumber(limitOrder.price, limitOrder.market?.decimalPlaces ?? 0),
`${limitOrder.timeInForce}: ${getDateTimeFormat().format(
new Date(limitOrder.expiresAt ?? '')
)}`,
getDateTimeFormat().format(new Date(limitOrder.createdAt)),
];
cells.forEach((cell, i) => {
expect(cell).toHaveTextContent(expectedValues[i]);
});
});
test('Correct formatting applied for a rejected order', async () => {
const rejectedOrder = {
...marketOrder,
status: OrderStatus.Rejected,
rejectionReason: OrderRejectionReason.InsufficientAssetBalance,
};
await act(async () => {
render(<OrderList orders={[rejectedOrder]} />);
});
const cells = screen.getAllByRole('gridcell');
expect(cells[3]).toHaveTextContent(
`${rejectedOrder.status}: ${rejectedOrder.rejectionReason}`
);
});

View File

@ -0,0 +1,97 @@
import {
Orders_party_orders,
OrderTimeInForce,
OrderStatus,
Side,
} from '@vegaprotocol/graphql';
import {
formatNumber,
getDateTimeFormat,
useApplyGridTransaction,
} from '@vegaprotocol/react-helpers';
import { AgGridDynamic as AgGrid } from '@vegaprotocol/ui-toolkit';
import { GridApi, ValueFormatterParams } from 'ag-grid-community';
import { AgGridColumn } from 'ag-grid-react';
import { useRef, useState } from 'react';
interface OrderListProps {
orders: Orders_party_orders[];
}
export const OrderList = ({ orders }: OrderListProps) => {
// Store initial orders for initial table data set, further updates
// are handled by the effect below
const [initialOrders] = useState(orders);
const gridApi = useRef<GridApi | null>(null);
useApplyGridTransaction<Orders_party_orders>(orders, gridApi.current);
return (
<AgGrid
rowData={initialOrders}
overlayNoRowsTemplate="No orders"
defaultColDef={{ flex: 1, resizable: true }}
style={{ width: '100%', height: '100%' }}
onGridReady={(params) => {
gridApi.current = params.api;
}}
getRowNodeId={(data) => data.id}
>
<AgGridColumn
headerName="Market"
field="market.tradableInstrument.instrument.code"
/>
<AgGridColumn
headerName="Amount"
field="size"
valueFormatter={({ value, data }: ValueFormatterParams) => {
const prefix = data.side === Side.Buy ? '+' : '-';
return prefix + value;
}}
/>
<AgGridColumn field="type" />
<AgGridColumn
field="status"
valueFormatter={({ value, data }: ValueFormatterParams) => {
if (value === OrderStatus.Rejected) {
return `${value}: ${data.rejectionReason}`;
}
return value;
}}
/>
<AgGridColumn
headerName="Filled"
field="remaining"
valueFormatter={({ data }: ValueFormatterParams) => {
return `${Number(data.size) - Number(data.remaining)}/${data.size}`;
}}
/>
<AgGridColumn
field="price"
valueFormatter={({ value, data }: ValueFormatterParams) => {
if (data.type === 'Market') {
return '-';
}
return formatNumber(value, data.market.decimalPlaces);
}}
/>
<AgGridColumn
field="timeInForce"
valueFormatter={({ value, data }: ValueFormatterParams) => {
if (value === OrderTimeInForce.GTT && data.expiresAt) {
const expiry = getDateTimeFormat().format(new Date(data.expiresAt));
return `${value}: ${expiry}`;
}
return value;
}}
/>
<AgGridColumn
field="createdAt"
valueFormatter={({ value }: ValueFormatterParams) => {
return getDateTimeFormat().format(new Date(value));
}}
/>
</AgGrid>
);
};

View File

@ -0,0 +1 @@
import '@testing-library/jest-dom';

View File

@ -0,0 +1,25 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"jsx": "react-jsx",
"allowJs": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true
},
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
},
{
"path": "./tsconfig.spec.json"
}
]
}

View File

@ -0,0 +1,22 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"types": ["node"]
},
"files": [
"../../node_modules/@nrwl/react/typings/cssmodule.d.ts",
"../../node_modules/@nrwl/react/typings/image.d.ts"
],
"exclude": [
"**/*.spec.ts",
"**/*.test.ts",
"**/*.spec.tsx",
"**/*.test.tsx",
"**/*.spec.js",
"**/*.test.js",
"**/*.spec.jsx",
"**/*.test.jsx"
],
"include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"]
}

View File

@ -0,0 +1,19 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"module": "commonjs",
"types": ["jest", "node"]
},
"include": [
"**/*.test.ts",
"**/*.spec.ts",
"**/*.test.tsx",
"**/*.spec.tsx",
"**/*.test.js",
"**/*.spec.js",
"**/*.test.jsx",
"**/*.spec.jsx",
"**/*.d.ts"
]
}

View File

@ -0,0 +1,74 @@
import { useApplyGridTransaction } from './use-apply-grid-transaction';
import { renderHook } from '@testing-library/react-hooks';
import { GridApi } from 'ag-grid-community';
type Items = Array<{ id: string; value: number }>;
const item = {
id: '1',
value: 1,
};
const item2 = {
id: '2',
value: 2,
};
const items = [item, item2];
function setup(items: Items, rowNodes: Items) {
const gridApiMock = {
applyTransaction: jest.fn(),
getRowNode: (id: string) => {
const node = rowNodes.find((i) => i.id === id);
if (node) {
return { data: node };
}
return undefined;
},
};
renderHook(() => useApplyGridTransaction(items, gridApiMock as any));
return gridApiMock;
}
test('Adds items', () => {
const gridApiMock = setup(items, []);
expect(gridApiMock.applyTransaction).toHaveBeenCalledWith({
update: [],
add: items,
addIndex: 0,
});
});
test('Doesnt update rows without changes', () => {
const rowNodes: Array<{ id: string; value: number }> = [...items];
const gridApiMock = setup(items, rowNodes);
expect(gridApiMock.applyTransaction).toHaveBeenCalledWith({
update: [],
add: [],
addIndex: 0,
});
});
test('Update rows with changes', () => {
const rowNodes = [...items];
const updatedItems = [
{ id: '1', value: 10 },
{ id: '2', value: 20 },
];
const gridApiMock = setup(updatedItems, rowNodes);
expect(gridApiMock.applyTransaction).toHaveBeenCalledWith({
update: updatedItems,
add: [],
addIndex: 0,
});
});
test('Updates and adds at the same time', () => {
const newItem = { id: '3', value: 3 };
const updatedItem = { id: '2', value: 20 };
const gridApiMock = setup([newItem, updatedItem], [...items]);
expect(gridApiMock.applyTransaction).toHaveBeenCalledWith({
update: [updatedItem],
add: [newItem],
addIndex: 0,
});
});

View File

@ -1,5 +1,6 @@
import { GridApi } from 'ag-grid-community';
import { useEffect } from 'react';
import isEqual from 'lodash/isEqual';
export const useApplyGridTransaction = <T extends { id: string }>(
data: T[],
@ -18,7 +19,9 @@ export const useApplyGridTransaction = <T extends { id: string }>(
const rowNode = gridApi.getRowNode(d.id);
if (rowNode) {
if (!isEqual(rowNode.data, d)) {
update.push(d);
}
} else {
add.push(d);
}

View File

@ -1,5 +1,5 @@
import once from 'lodash.once';
import memoize from 'lodash.memoize';
import once from 'lodash/once';
import memoize from 'lodash/memoize';
import { addDecimal } from '../decimals';
const getUserLocale = () => 'default';

View File

@ -22,6 +22,7 @@
"@radix-ui/react-tooltip": "^0.1.7",
"@sentry/react": "^6.18.1",
"@sentry/tracing": "^6.18.1",
"@types/lodash": "^4.14.180",
"@types/uuid": "^8.3.4",
"@vegaprotocol/vegawallet-service-api-client": "^0.4.6",
"ag-grid-community": "^27.0.1",
@ -31,20 +32,18 @@
"bignumber.js": "^9.0.2",
"classnames": "^2.3.1",
"core-js": "^3.6.5",
"date-fns": "^2.28.0",
"env-cmd": "^10.1.0",
"ethers": "^5.6.0",
"graphql": "^15.7.2",
"graphql-ws": "^5.6.3",
"lodash.debounce": "^4.0.8",
"lodash.memoize": "^4.1.2",
"lodash.once": "^4.1.1",
"lodash": "^4.17.21",
"next": "12.0.7",
"nx": "^13.8.3",
"postcss": "^8.4.6",
"react": "17.0.2",
"react-dom": "17.0.2",
"react-hook-form": "^7.27.0",
"react-singleton-hook": "^3.2.3",
"react-syntax-highlighter": "^15.4.5",
"react-use-websocket": "^3.0.0",
"react-virtualized-auto-sizer": "^1.0.6",
@ -80,8 +79,6 @@
"@testing-library/react-hooks": "7.0.2",
"@types/classnames": "^2.3.1",
"@types/jest": "27.0.2",
"@types/lodash.memoize": "^4.1.6",
"@types/lodash.once": "^4.1.6",
"@types/node": "16.11.7",
"@types/prismjs": "^1.26.0",
"@types/react": "17.0.30",

View File

@ -19,6 +19,7 @@
"@vegaprotocol/graphql": ["libs/graphql/src/index.ts"],
"@vegaprotocol/market-list": ["libs/market-list/src/index.ts"],
"@vegaprotocol/network-stats": ["libs/network-stats/src/index.ts"],
"@vegaprotocol/order-list": ["libs/order-list/src/index.ts"],
"@vegaprotocol/react-helpers": ["libs/react-helpers/src/index.ts"],
"@vegaprotocol/tailwindcss-config": [
"libs/tailwindcss-config/src/index.js"

View File

@ -7,6 +7,7 @@
"graphql": "libs/graphql",
"market-list": "libs/market-list",
"network-stats": "libs/network-stats",
"order-list": "libs/order-list",
"react-helpers": "libs/react-helpers",
"stats": "apps/stats",
"stats-e2e": "apps/stats-e2e",

View File

@ -5201,21 +5201,7 @@
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4=
"@types/lodash.memoize@^4.1.6":
version "4.1.6"
resolved "https://registry.yarnpkg.com/@types/lodash.memoize/-/lodash.memoize-4.1.6.tgz#3221f981790a415cab1a239f25c17efd8b604c23"
integrity sha512-mYxjKiKzRadRJVClLKxS4wb3Iy9kzwJ1CkbyKiadVxejnswnRByyofmPMscFKscmYpl36BEEhCMPuWhA1R/1ZQ==
dependencies:
"@types/lodash" "*"
"@types/lodash.once@^4.1.6":
version "4.1.6"
resolved "https://registry.yarnpkg.com/@types/lodash.once/-/lodash.once-4.1.6.tgz#f6ea46e3426bc3494b72a45e65efed440103b967"
integrity sha512-kawTe2cBNZ5OI4CvTJT9cs8wacGZK4BoakKAGASl/jH3LxflMTuy82wN2U5klYsxYjrABkNWmzgO33volt7urQ==
dependencies:
"@types/lodash" "*"
"@types/lodash@*":
"@types/lodash@^4.14.180":
version "4.14.180"
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.180.tgz#4ab7c9ddfc92ec4a887886483bc14c79fb380670"
integrity sha512-XOKXa1KIxtNXgASAnwj7cnttJxS4fksBRywK/9LzRV5YxrF80BXZIGeQSuoESQ/VkUj30Ae0+YcuHc15wJCB2g==
@ -9230,6 +9216,11 @@ date-fns@^1.27.2:
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-1.30.1.tgz#2e71bf0b119153dbb4cc4e88d9ea5acfb50dc05c"
integrity sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw==
date-fns@^2.28.0:
version "2.28.0"
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.28.0.tgz#9570d656f5fc13143e50c975a3b6bbeb46cd08b2"
integrity sha512-8d35hViGYx/QH0icHYCeLmsLmMUheMmTyV9Fcm6gvNwdw31yXXH+O85sOBJ+OLnLQMKZowvpKb6FgMIQjcpvQw==
dayjs@^1.10.4:
version "1.10.8"
resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.10.8.tgz#267df4bc6276fcb33c04a6735287e3f429abec41"
@ -16577,11 +16568,6 @@ react-shallow-renderer@^16.13.1:
object-assign "^4.1.1"
react-is "^16.12.0 || ^17.0.0"
react-singleton-hook@^3.2.3:
version "3.2.3"
resolved "https://registry.yarnpkg.com/react-singleton-hook/-/react-singleton-hook-3.2.3.tgz#1765e67b1910fd163cdcd186a840af82b60ca17e"
integrity sha512-DYJ70V8IVUZznygmRYYF+hxwLkGPLrCCTAD1Je7sZYXquE9HpAJqBVeEgb2XIZwxWUw97PN2ho5J/+63YwdKaA==
react-sizeme@^3.0.1:
version "3.0.2"
resolved "https://registry.yarnpkg.com/react-sizeme/-/react-sizeme-3.0.2.tgz#4a2f167905ba8f8b8d932a9e35164e459f9020e4"