diff --git a/apps/simple-trading-app/setup-tests.ts b/apps/simple-trading-app/setup-tests.ts index dabdc544a..adff1f883 100644 --- a/apps/simple-trading-app/setup-tests.ts +++ b/apps/simple-trading-app/setup-tests.ts @@ -1,4 +1,7 @@ import '@testing-library/jest-dom'; +import { defaultFallbackInView } from 'react-intersection-observer'; + +defaultFallbackInView(true); Object.defineProperty(window, 'matchMedia', { writable: true, diff --git a/apps/simple-trading-app/src/app/app.tsx b/apps/simple-trading-app/src/app/app.tsx index aeb99149f..f106bddbb 100644 --- a/apps/simple-trading-app/src/app/app.tsx +++ b/apps/simple-trading-app/src/app/app.tsx @@ -43,7 +43,7 @@ function App() { -
+
( ); export const DrawerWrapper = ({ children }: Props) => ( -
{children}
+
{children}
); diff --git a/apps/simple-trading-app/src/app/components/simple-market-list/__generated__/CandleLive.ts b/apps/simple-trading-app/src/app/components/simple-market-list/__generated__/CandleLive.ts new file mode 100644 index 000000000..6c390a93d --- /dev/null +++ b/apps/simple-trading-app/src/app/components/simple-market-list/__generated__/CandleLive.ts @@ -0,0 +1,27 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL subscription operation: CandleLive +// ==================================================== + +export interface CandleLive_candles { + __typename: "Candle"; + /** + * Close price (uint64) + */ + close: string; +} + +export interface CandleLive { + /** + * Subscribe to the candles updates + */ + candles: CandleLive_candles; +} + +export interface CandleLiveVariables { + marketId: string; +} diff --git a/apps/simple-trading-app/src/app/components/simple-market-list/__generated__/SimpleMarkets.ts b/apps/simple-trading-app/src/app/components/simple-market-list/__generated__/SimpleMarkets.ts index b84ffddc6..9e9ce7943 100644 --- a/apps/simple-trading-app/src/app/components/simple-market-list/__generated__/SimpleMarkets.ts +++ b/apps/simple-trading-app/src/app/components/simple-market-list/__generated__/SimpleMarkets.ts @@ -3,7 +3,7 @@ // @generated // This file was automatically generated and should not be edited. -import { Interval, MarketState } from "@vegaprotocol/types"; +import { MarketState } from "@vegaprotocol/types"; // ==================================================== // GraphQL query operation: SimpleMarkets @@ -117,6 +117,5 @@ export interface SimpleMarkets { } export interface SimpleMarketsVariables { - CandleInterval: Interval; CandleSince: string; } diff --git a/apps/simple-trading-app/src/app/components/simple-market-list/data-provider.ts b/apps/simple-trading-app/src/app/components/simple-market-list/data-provider.ts index 04e9f35ce..1560020c9 100644 --- a/apps/simple-trading-app/src/app/components/simple-market-list/data-provider.ts +++ b/apps/simple-trading-app/src/app/components/simple-market-list/data-provider.ts @@ -18,9 +18,9 @@ const MARKET_DATA_FRAGMENT = gql` } `; -const MARKETS_QUERY = gql` +export const MARKETS_QUERY = gql` ${MARKET_DATA_FRAGMENT} - query SimpleMarkets($CandleInterval: Interval!, $CandleSince: String!) { + query SimpleMarkets($CandleSince: String!) { markets { id name @@ -41,7 +41,7 @@ const MARKETS_QUERY = gql` } } } - candles(interval: $CandleInterval, since: $CandleSince) { + candles(interval: I1H, since: $CandleSince) { open close } @@ -58,6 +58,14 @@ const MARKET_DATA_SUB = gql` } `; +export const CANDLE_SUB = gql` + subscription CandleLive($marketId: ID!) { + candles(marketId: $marketId, interval: I1H) { + close + } + } +`; + const update = ( draft: SimpleMarkets_markets[], delta: SimpleMarketDataSub_marketData diff --git a/apps/simple-trading-app/src/app/components/simple-market-list/simple-market-list.spec.tsx b/apps/simple-trading-app/src/app/components/simple-market-list/simple-market-list.spec.tsx index 850f718a1..be70d03bf 100644 --- a/apps/simple-trading-app/src/app/components/simple-market-list/simple-market-list.spec.tsx +++ b/apps/simple-trading-app/src/app/components/simple-market-list/simple-market-list.spec.tsx @@ -1,9 +1,12 @@ import React from 'react'; import { render, screen } from '@testing-library/react'; -import { useDataProvider } from '@vegaprotocol/react-helpers'; +import { MockedProvider } from '@apollo/client/testing'; +import type { MockedResponse } from '@apollo/client/testing'; import { MarketState } from '@vegaprotocol/types'; import SimpleMarketList from './simple-market-list'; +import { MARKETS_QUERY } from './data-provider'; import type { SimpleMarkets_markets } from './__generated__/SimpleMarkets'; +import type { SimpleMarkets } from './__generated__/SimpleMarkets'; const mockedNavigate = jest.fn(); @@ -12,11 +15,8 @@ jest.mock('react-router-dom', () => ({ useNavigate: () => mockedNavigate, })); -jest.mock('./data-provider', () => jest.fn()); - -jest.mock('@vegaprotocol/react-helpers', () => ({ - useDataProvider: jest.fn(), - t: (txt: string) => txt, +jest.mock('date-fns', () => ({ + subDays: () => new Date('2022-06-02T11:11:21.721Z'), })); describe('SimpleMarketList', () => { @@ -24,16 +24,31 @@ describe('SimpleMarketList', () => { jest.clearAllMocks(); }); - it('should be properly renderer as empty', () => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (useDataProvider as unknown as jest.SpyInstance).mockImplementation( - () => ({ data: [], error: false, loading: false }) + it('should be properly renderer as empty', async () => { + const mocks: MockedResponse = { + request: { + query: MARKETS_QUERY, + variables: { + CandleSince: '2022-06-02T11:11:21.721Z', + }, + }, + result: { + data: { markets: [] }, + }, + }; + + render( + + + ); - render(); + + await new Promise((resolve) => setTimeout(resolve, 0)); + expect(screen.getByText('No data to display')).toBeInTheDocument(); }); - it('should be properly rendered with some data', () => { + it('should be properly rendered with some data', async () => { const data = [ { id: '1', @@ -76,11 +91,26 @@ describe('SimpleMarketList', () => { }, }, ] as unknown as SimpleMarkets_markets[]; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (useDataProvider as unknown as jest.SpyInstance).mockImplementation( - () => ({ data, error: false, loading: false }) + + const mocks: MockedResponse = { + request: { + query: MARKETS_QUERY, + variables: { + CandleSince: '2022-06-02T11:11:21.721Z', + }, + }, + result: { + data: { markets: data }, + }, + }; + render( + + + ); - render(); + + await new Promise((resolve) => setTimeout(resolve, 0)); + expect(screen.getByRole('list')).toBeInTheDocument(); expect(screen.getAllByRole('listitem')).toHaveLength(2); }); diff --git a/apps/simple-trading-app/src/app/components/simple-market-list/simple-market-list.tsx b/apps/simple-trading-app/src/app/components/simple-market-list/simple-market-list.tsx index 8077c99c8..7ff7006c2 100644 --- a/apps/simple-trading-app/src/app/components/simple-market-list/simple-market-list.tsx +++ b/apps/simple-trading-app/src/app/components/simple-market-list/simple-market-list.tsx @@ -1,9 +1,11 @@ -import React, { useCallback, useMemo } from 'react'; +import React, { useCallback, useEffect, useMemo, useRef } from 'react'; import { useNavigate } from 'react-router-dom'; +import { subDays } from 'date-fns'; import { useDataProvider } from '@vegaprotocol/react-helpers'; import { t } from '@vegaprotocol/react-helpers'; import { AsyncRenderer, Lozenge, Splash } from '@vegaprotocol/ui-toolkit'; import { Button } from '@vegaprotocol/ui-toolkit'; +import type { MarketState } from '@vegaprotocol/types'; import SimpleMarketPercentChange from './simple-market-percent-change'; import SimpleMarketExpires from './simple-market-expires'; import DataProvider from './data-provider'; @@ -11,18 +13,36 @@ import { MARKET_STATUS } from './constants'; const SimpleMarketList = () => { const navigate = useNavigate(); + const statusesRef = useRef>({}); const variables = useMemo( () => ({ - CandleInterval: 'I1H', - CandleSince: new Date(Date.now() - 24 * 60 * 60 * 1000).toJSON(), + CandleSince: subDays(Date.now(), 1).toJSON(), }), [] ); + const update = useCallback( + (delta) => { + if (statusesRef.current[delta.market.id] !== delta.market.state) { + return false; + } + return true; + }, + [statusesRef] + ); + const { data, error, loading } = useDataProvider( DataProvider, - undefined, // @TODO - if we need a live update in the future + update, variables ); + useEffect(() => { + const statuses: Record = {}; + data?.forEach((market) => { + statuses[market.id] = market.data?.market.state || ''; + }); + statusesRef.current = statuses; + }, [data]); + const onClick = useCallback( (marketId) => { navigate(`/trading/${marketId}`); @@ -52,7 +72,10 @@ const SimpleMarketList = () => {
- +
{ let candles: (SimpleMarkets_markets_candles | null)[] | null; it('empty array', () => { candles = []; - render(); + render( + + + + ); expect(screen.getByText('-')).toBeInTheDocument(); }); it('null', () => { candles = null; - render(); + render( + + + + ); expect(screen.getByText('-')).toBeInTheDocument(); }); it('an appreciated one', () => { @@ -22,7 +31,11 @@ describe('SimpleMarketPercentChange should parse proper change', () => { { close: '100' } as SimpleMarkets_markets_candles, null, ]; - render(); + render( + + + + ); expect(screen.getByText('100.000%')).toBeInTheDocument(); expect(screen.getByText('100.000%')).toHaveStyle( `color: ${theme.colors.vega.green}` @@ -34,7 +47,11 @@ describe('SimpleMarketPercentChange should parse proper change', () => { { close: '50' } as SimpleMarkets_markets_candles, null, ]; - render(); + render( + + + + ); expect(screen.getByText('-50.000%')).toBeInTheDocument(); expect(screen.getByText('-50.000%')).toHaveStyle( `color: ${theme.colors.vega.pink}` diff --git a/apps/simple-trading-app/src/app/components/simple-market-list/simple-market-percent-change.tsx b/apps/simple-trading-app/src/app/components/simple-market-list/simple-market-percent-change.tsx index 58aac8cc7..9d1cd2086 100644 --- a/apps/simple-trading-app/src/app/components/simple-market-list/simple-market-percent-change.tsx +++ b/apps/simple-trading-app/src/app/components/simple-market-list/simple-market-percent-change.tsx @@ -1,22 +1,34 @@ -import React from 'react'; +import React, { useState } from 'react'; +import { InView } from 'react-intersection-observer'; +import { useSubscription } from '@apollo/client'; import { theme } from '@vegaprotocol/tailwindcss-config'; import type { SimpleMarkets_markets_candles } from './__generated__/SimpleMarkets'; +import type { + CandleLive, + CandleLiveVariables, +} from './__generated__/CandleLive'; +import { CANDLE_SUB } from './data-provider'; interface Props { candles: (SimpleMarkets_markets_candles | null)[] | null; + marketId: string; } const getChange = ( - candles: (SimpleMarkets_markets_candles | null)[] | null + candles: (SimpleMarkets_markets_candles | null)[] | null, + lastClose?: string ) => { if (candles) { const first = parseInt(candles.find((item) => item?.open)?.open || '-1'); - const last = candles.reduceRight((aggr, item) => { - if (aggr === -1 && item?.close) { - aggr = parseInt(item.close); - } - return aggr; - }, -1); + const last = + typeof lastClose === 'undefined' + ? candles.reduceRight((aggr, item) => { + if (aggr === -1 && item?.close) { + aggr = parseInt(item.close); + } + return aggr; + }, -1) + : parseInt(lastClose); if (first !== -1 && last !== -1) { return Number(((last - first) / first) * 100).toFixed(3) + '%'; } @@ -34,10 +46,25 @@ const getColor = (change: number | string) => { return theme.colors.intent.highlight; }; -const SimpleMarketPercentChange = ({ candles }: Props) => { - const change = getChange(candles); +const SimpleMarketPercentChangeWrapper = (props: Props) => { + const [inView, setInView] = useState(false); + + return ( + // @ts-ignore falsy wrong type? + + {inView && } + + ); +}; + +const SimpleMarketPercentChange = ({ candles, marketId }: Props) => { + const { data: { candles: { close = undefined } = {} } = {} } = + useSubscription(CANDLE_SUB, { + variables: { marketId }, + }); + const change = getChange(candles, close); const color = getColor(change); return

{change}

; }; -export default SimpleMarketPercentChange; +export default SimpleMarketPercentChangeWrapper; diff --git a/apps/simple-trading-app/src/app/lib/apollo-client.tsx b/apps/simple-trading-app/src/app/lib/apollo-client.tsx index 2b2ed69bd..a687d6cbe 100644 --- a/apps/simple-trading-app/src/app/lib/apollo-client.tsx +++ b/apps/simple-trading-app/src/app/lib/apollo-client.tsx @@ -1,6 +1,15 @@ -import { ApolloClient, from, HttpLink, InMemoryCache } from '@apollo/client'; +import { + ApolloClient, + from, + HttpLink, + InMemoryCache, + split, +} from '@apollo/client'; import { onError } from '@apollo/client/link/error'; import { RetryLink } from '@apollo/client/link/retry'; +import { GraphQLWsLink } from '@apollo/client/link/subscriptions'; +import { createClient as createWSClient } from 'graphql-ws'; +import { getMainDefinition } from '@apollo/client/utilities'; export function createClient(base?: string) { if (!base) { @@ -40,6 +49,24 @@ export function createClient(base?: string) { credentials: 'same-origin', }); + const wsLink = new GraphQLWsLink( + createWSClient({ + url: urlWS.href, + }) + ); + + const splitLink = split( + ({ query }) => { + const definition = getMainDefinition(query); + return ( + definition.kind === 'OperationDefinition' && + definition.operation === 'subscription' + ); + }, + wsLink, + httpLink + ); + const errorLink = onError(({ graphQLErrors, networkError }) => { console.log(graphQLErrors); console.log(networkError); @@ -47,7 +74,7 @@ export function createClient(base?: string) { return new ApolloClient({ connectToDevTools: process.env['NODE_ENV'] === 'development', - link: from([errorLink, retryLink, httpLink]), + link: from([errorLink, retryLink, splitLink]), cache, }); } diff --git a/package.json b/package.json index 6357ac198..8791933f7 100644 --- a/package.json +++ b/package.json @@ -60,6 +60,7 @@ "react-dom": "17.0.2", "react-hook-form": "^7.27.0", "react-i18next": "^11.11.4", + "react-intersection-observer": "^9.2.2", "react-router-dom": "^6.3.0", "react-syntax-highlighter": "^15.4.5", "react-use-websocket": "^3.0.0", diff --git a/yarn.lock b/yarn.lock index 51089bb63..c2da3727e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -18049,6 +18049,11 @@ react-inspector@^5.1.0: is-dom "^1.0.0" prop-types "^15.0.0" +react-intersection-observer@^9.2.2: + version "9.2.2" + resolved "https://registry.yarnpkg.com/react-intersection-observer/-/react-intersection-observer-9.2.2.tgz#4f72487e2e9c62b6a6f55f38c5b0b6b7dc6c196d" + integrity sha512-xrFOWo5DbDzayAuDn6WpLrXfw73mvfx4WjuAjjy7dY7jWeTTle5+18nGtvMlR7q0npJanmmh6/CaSaSg4JxTJQ== + react-is@17.0.2, react-is@^17.0.1, react-is@^17.0.2: version "17.0.2" resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"