Feat/439 simple market list live update (#503)
* feat: [simple-app] - simple market list - improvements in apollo client * feat: [simple-app] - simple market list - add live update for state and price change * feat: [simple-app] - simple market list - add live update for state and price change * feat: [simple-app] - simple market list - add intersection observer, small improvements * feat: [simple-app] - simple market list - small improvements after review feedback * feat: [simple-app] - simple market list - small improvements after review feedback Co-authored-by: maciek <maciek@vegaprotocol.io>
This commit is contained in:
parent
3defb50739
commit
7b19d5e5e8
@ -1,4 +1,7 @@
|
||||
import '@testing-library/jest-dom';
|
||||
import { defaultFallbackInView } from 'react-intersection-observer';
|
||||
|
||||
defaultFallbackInView(true);
|
||||
|
||||
Object.defineProperty(window, 'matchMedia', {
|
||||
writable: true,
|
||||
|
@ -43,7 +43,7 @@ function App() {
|
||||
<ApolloProvider client={client}>
|
||||
<VegaWalletProvider>
|
||||
<AppLoader>
|
||||
<div className="h-full dark:bg-black dark:text-white-60 bg-white text-black-60 grid grid-rows-[min-content,1fr]">
|
||||
<div className="max-h-full min-h-full dark:bg-black dark:text-white-60 bg-white text-black-60 grid grid-rows-[min-content,1fr]">
|
||||
<div className="flex items-stretch border-b-[7px] border-vega-yellow">
|
||||
<DrawerToggle
|
||||
onToggle={onToggle}
|
||||
|
@ -10,5 +10,5 @@ export const DrawerContainer = ({ children }: Props) => (
|
||||
);
|
||||
|
||||
export const DrawerWrapper = ({ children }: Props) => (
|
||||
<div className="flex">{children}</div>
|
||||
<div className="flex dark:bg-black">{children}</div>
|
||||
);
|
||||
|
27
apps/simple-trading-app/src/app/components/simple-market-list/__generated__/CandleLive.ts
generated
Normal file
27
apps/simple-trading-app/src/app/components/simple-market-list/__generated__/CandleLive.ts
generated
Normal file
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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<any>).mockImplementation(
|
||||
() => ({ data: [], error: false, loading: false })
|
||||
it('should be properly renderer as empty', async () => {
|
||||
const mocks: MockedResponse<SimpleMarkets> = {
|
||||
request: {
|
||||
query: MARKETS_QUERY,
|
||||
variables: {
|
||||
CandleSince: '2022-06-02T11:11:21.721Z',
|
||||
},
|
||||
},
|
||||
result: {
|
||||
data: { markets: [] },
|
||||
},
|
||||
};
|
||||
|
||||
render(
|
||||
<MockedProvider mocks={[mocks]}>
|
||||
<SimpleMarketList />
|
||||
</MockedProvider>
|
||||
);
|
||||
render(<SimpleMarketList />);
|
||||
|
||||
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<any>).mockImplementation(
|
||||
() => ({ data, error: false, loading: false })
|
||||
|
||||
const mocks: MockedResponse<SimpleMarkets> = {
|
||||
request: {
|
||||
query: MARKETS_QUERY,
|
||||
variables: {
|
||||
CandleSince: '2022-06-02T11:11:21.721Z',
|
||||
},
|
||||
},
|
||||
result: {
|
||||
data: { markets: data },
|
||||
},
|
||||
};
|
||||
render(
|
||||
<MockedProvider mocks={[mocks]}>
|
||||
<SimpleMarketList />
|
||||
</MockedProvider>
|
||||
);
|
||||
render(<SimpleMarketList />);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
|
||||
expect(screen.getByRole('list')).toBeInTheDocument();
|
||||
expect(screen.getAllByRole('listitem')).toHaveLength(2);
|
||||
});
|
||||
|
@ -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<Record<string, MarketState | ''>>({});
|
||||
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<string, MarketState | ''> = {};
|
||||
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 = () => {
|
||||
</div>
|
||||
<div className="w-full grid sm:grid-rows-2">
|
||||
<div>
|
||||
<SimpleMarketPercentChange candles={market.candles} />
|
||||
<SimpleMarketPercentChange
|
||||
candles={market.candles}
|
||||
marketId={market.id}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Lozenge
|
||||
|
@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { theme } from '@vegaprotocol/tailwindcss-config';
|
||||
import { MockedProvider } from '@apollo/react-testing';
|
||||
import SimpleMarketPercentChange from './simple-market-percent-change';
|
||||
import type { SimpleMarkets_markets_candles } from './__generated__/SimpleMarkets';
|
||||
|
||||
@ -8,12 +9,20 @@ describe('SimpleMarketPercentChange should parse proper change', () => {
|
||||
let candles: (SimpleMarkets_markets_candles | null)[] | null;
|
||||
it('empty array', () => {
|
||||
candles = [];
|
||||
render(<SimpleMarketPercentChange candles={candles} />);
|
||||
render(
|
||||
<MockedProvider>
|
||||
<SimpleMarketPercentChange candles={candles} marketId={'1'} />
|
||||
</MockedProvider>
|
||||
);
|
||||
expect(screen.getByText('-')).toBeInTheDocument();
|
||||
});
|
||||
it('null', () => {
|
||||
candles = null;
|
||||
render(<SimpleMarketPercentChange candles={candles} />);
|
||||
render(
|
||||
<MockedProvider>
|
||||
<SimpleMarketPercentChange candles={candles} marketId={'1'} />
|
||||
</MockedProvider>
|
||||
);
|
||||
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(<SimpleMarketPercentChange candles={candles} />);
|
||||
render(
|
||||
<MockedProvider>
|
||||
<SimpleMarketPercentChange candles={candles} marketId={'1'} />
|
||||
</MockedProvider>
|
||||
);
|
||||
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(<SimpleMarketPercentChange candles={candles} />);
|
||||
render(
|
||||
<MockedProvider>
|
||||
<SimpleMarketPercentChange candles={candles} marketId={'1'} />
|
||||
</MockedProvider>
|
||||
);
|
||||
expect(screen.getByText('-50.000%')).toBeInTheDocument();
|
||||
expect(screen.getByText('-50.000%')).toHaveStyle(
|
||||
`color: ${theme.colors.vega.pink}`
|
||||
|
@ -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) => {
|
||||
const last =
|
||||
typeof lastClose === 'undefined'
|
||||
? candles.reduceRight((aggr, item) => {
|
||||
if (aggr === -1 && item?.close) {
|
||||
aggr = parseInt(item.close);
|
||||
}
|
||||
return aggr;
|
||||
}, -1);
|
||||
}, -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 onChange={setInView}>
|
||||
{inView && <SimpleMarketPercentChange {...props} />}
|
||||
</InView>
|
||||
);
|
||||
};
|
||||
|
||||
const SimpleMarketPercentChange = ({ candles, marketId }: Props) => {
|
||||
const { data: { candles: { close = undefined } = {} } = {} } =
|
||||
useSubscription<CandleLive, CandleLiveVariables>(CANDLE_SUB, {
|
||||
variables: { marketId },
|
||||
});
|
||||
const change = getChange(candles, close);
|
||||
const color = getColor(change);
|
||||
return <p style={{ color }}>{change}</p>;
|
||||
};
|
||||
|
||||
export default SimpleMarketPercentChange;
|
||||
export default SimpleMarketPercentChangeWrapper;
|
||||
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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"
|
||||
|
Loading…
Reference in New Issue
Block a user