diff --git a/.mergify.yml b/.mergify.yml index 3c9624d9c..aed23a60b 100644 --- a/.mergify.yml +++ b/.mergify.yml @@ -1,3 +1,8 @@ +queue_rules: + - name: default + conditions: + - check-success=Deploy + pull_request_rules: - name: Merge approved and green PRs when tagged with 'Auto-merge' conditions: @@ -5,9 +10,9 @@ pull_request_rules: - label="Auto-merge" - check-success=Deploy actions: - merge: + queue: method: squash - strict: smart+fasttrack + name: default commit_message: title+body - name: automatic merge for Dependabot pull requests conditions: @@ -17,8 +22,8 @@ pull_request_rules: - check-success=Deploy - base=develop actions: - merge: + queue: method: squash - strict: smart+fasttrack + name: default commit_message: title+body \ No newline at end of file diff --git a/package.json b/package.json index 0175e365b..03173acd0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@gnosis.pm/gp-v1", - "version": "2.5.1", + "version": "2.6.0-rc.1", "description": "", "main": "src/index.js", "sideEffects": false, @@ -54,7 +54,7 @@ "@apollo/client": "^3.1.5", "@fortawesome/fontawesome-svg-core": "^1.2.26", "@fortawesome/free-regular-svg-icons": "^5.12.0", - "@fortawesome/free-solid-svg-icons": "^5.12.0", + "@fortawesome/free-solid-svg-icons": "^5.15.4", "@fortawesome/react-fontawesome": "^0.1.8", "@gnosis.pm/dex-js": "^0.10.0", "@gnosis.pm/gp-v2-contracts": "^1.0.2", diff --git a/src/api/operator/index.ts b/src/api/operator/index.ts index 6a59dbbb9..15307bd44 100644 --- a/src/api/operator/index.ts +++ b/src/api/operator/index.ts @@ -13,6 +13,7 @@ export const { getOrder, getOrders, getAccountOrders, + getTxOrders, getTrades, // functions that do not have a mock getOrderLink = realApi.getOrderLink, diff --git a/src/api/operator/operatorApi.ts b/src/api/operator/operatorApi.ts index 74e96fc2e..9bb22e654 100644 --- a/src/api/operator/operatorApi.ts +++ b/src/api/operator/operatorApi.ts @@ -14,6 +14,7 @@ import { OrderPostError, RawOrder, RawTrade, + GetTxOrdersParams, } from './types' function getOperatorUrl(): Partial> { @@ -240,6 +241,19 @@ export async function getAccountOrders(params: GetAccountOrdersParams): Promise< return _fetchQuery(networkId, queryString) } +/** + * Gets a order list within Tx + */ +export async function getTxOrders(params: GetTxOrdersParams): Promise { + const { networkId, txHash } = params + + console.log(`[getTxOrders] Fetching tx orders on network ${networkId}`) + + const queryString = `/transactions/${txHash}/orders` + + return _fetchQuery(networkId, queryString) +} + /** * Gets a list of trades * diff --git a/src/api/operator/operatorMock.ts b/src/api/operator/operatorMock.ts index 7f97670bf..c29cc6915 100644 --- a/src/api/operator/operatorMock.ts +++ b/src/api/operator/operatorMock.ts @@ -1,4 +1,12 @@ -import { GetOrderParams, GetOrdersParams, GetAccountOrdersParams, GetTradesParams, RawOrder, RawTrade } from './types' +import { + GetOrderParams, + GetOrdersParams, + GetAccountOrdersParams, + GetTxOrdersParams, + GetTradesParams, + RawOrder, + RawTrade, +} from './types' import { RAW_ORDER, RAW_TRADE } from '../../../test/data' @@ -31,6 +39,14 @@ export async function getAccountOrders(params: GetAccountOrdersParams): Promise< return [order] } +export async function getTxOrders(params: GetTxOrdersParams): Promise { + const { networkId } = params + + const order = await getOrder({ networkId, orderId: 'whatever' }) + + return [order] +} + export async function getTrades(params: GetTradesParams): Promise { const { owner, orderId } = params diff --git a/src/api/operator/types.ts b/src/api/operator/types.ts index 6a1d8b049..20d48943e 100644 --- a/src/api/operator/types.ts +++ b/src/api/operator/types.ts @@ -5,6 +5,7 @@ import { TokenErc20 } from '@gnosis.pm/dex-js' import { Network } from 'types' export type OrderID = string +export type TxHash = string export interface OrderPostError { errorType: 'MissingOrderData' | 'InvalidSignature' | 'DuplicateOrder' | 'InsufficientFunds' @@ -114,7 +115,7 @@ export type Trade = Pick + import( + /* webpackChunkName: "TransactionDetails_chunk"*/ + './pages/TransactionDetails' + ), +) + /** * Update the global state */ @@ -117,12 +124,13 @@ const AppContent = (): JSX.Element => { } /> + @@ -131,20 +139,6 @@ const AppContent = (): JSX.Element => { ) } -const Wrapper = styled.div` - max-width: 118rem; - margin: 0 auto; - - ${media.mediumDown} { - max-width: 94rem; - flex-flow: column wrap; - } - - ${media.mobile} { - max-width: 100%; - } -` - /** * Render Explorer App */ @@ -153,17 +147,20 @@ export const ExplorerApp: React.FC = () => { useNetworkCheck() return ( - - - - - - - - - - {process.env.NODE_ENV === 'development' && } - + <> + + + + + + + + + + + {process.env.NODE_ENV === 'development' && } + + ) } diff --git a/src/apps/explorer/components/OrdersTableWidget/index.tsx b/src/apps/explorer/components/OrdersTableWidget/index.tsx index dc2e89979..bdbe8d41e 100644 --- a/src/apps/explorer/components/OrdersTableWidget/index.tsx +++ b/src/apps/explorer/components/OrdersTableWidget/index.tsx @@ -2,12 +2,12 @@ import React from 'react' import styled from 'styled-components' import ExplorerTabs from 'apps/explorer/components/common/ExplorerTabs/ExplorerTab' -import { useGetOrders } from './useGetOrders' import { TabItemInterface } from 'components/common/Tabs/Tabs' import { useTable } from './useTable' import { OrdersTableWithData } from './OrdersTableWithData' import { OrdersTableContext, BlockchainNetwork } from './context/OrdersTableContext' import PaginationOrdersTable from './PaginationOrdersTable' +import { useGetAccountOrders } from 'hooks/useGetOrders' import Spinner from 'components/common/Spinner' const StyledTabLoader = styled.span` @@ -58,7 +58,7 @@ const OrdersTableWidget: React.FC = ({ ownerAddress, networkId }) => { isLoading: isOrdersLoading, error, isThereNext: isThereNextOrder, - } = useGetOrders(ownerAddress, tableState.pageSize, tableState.pageOffset, tableState.pageIndex) + } = useGetAccountOrders(ownerAddress, tableState.pageSize, tableState.pageOffset, tableState.pageIndex) tableState['hasNextPage'] = isThereNextOrder const addressAccountParams = { ownerAddress, networkId } diff --git a/src/apps/explorer/components/OrdersTableWidget/useGetOrders.tsx b/src/apps/explorer/components/OrdersTableWidget/useGetOrders.tsx deleted file mode 100644 index feffe4fd1..000000000 --- a/src/apps/explorer/components/OrdersTableWidget/useGetOrders.tsx +++ /dev/null @@ -1,117 +0,0 @@ -import { useState, useCallback, useEffect } from 'react' - -import { Network } from 'types' -import { useMultipleErc20 } from 'hooks/useErc20' -import { getAccountOrders, Order } from 'api/operator' -import { useNetworkId } from 'state/network' -import { transformOrder } from 'utils' -import { ORDERS_QUERY_INTERVAL } from 'apps/explorer/const' - -function isObjectEmpty(object: Record): boolean { - // eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars - for (const key in object) { - if (key) return false - } - - return true -} - -type Result = { - orders: Order[] | undefined - error: string - isLoading: boolean - isThereNext: boolean -} - -export function useGetOrders(ownerAddress: string, limit = 1000, offset = 0, pageIndex?: number): Result { - const [isLoading, setIsLoading] = useState(false) - const [error, setError] = useState('') - const [orders, setOrders] = useState() - const networkId = useNetworkId() || undefined - const [erc20Addresses, setErc20Addresses] = useState([]) - const { value: valueErc20s, isLoading: areErc20Loading } = useMultipleErc20({ networkId, addresses: erc20Addresses }) - const [mountNewOrders, setMountNewOrders] = useState(false) - const [isThereNext, setIsThereNext] = useState(false) - - useEffect(() => { - setOrders(undefined) - setMountNewOrders(false) - }, [networkId]) - - const fetchOrders = useCallback( - async (network: Network, owner: string): Promise => { - setIsLoading(true) - setError('') - const limitPlusOne = limit + 1 - - try { - const ordersFetched = await getAccountOrders({ networkId: network, owner, offset, limit: limitPlusOne }) - if (ordersFetched.length === limitPlusOne) { - setIsThereNext(true) - ordersFetched.pop() - } - const newErc20Addresses = ordersFetched.reduce((accumulator: string[], element) => { - const updateAccumulator = (tokenAddress: string): void => { - if (accumulator.indexOf(tokenAddress) === -1) { - accumulator.push(tokenAddress) - } - } - updateAccumulator(element.buyToken) - updateAccumulator(element.sellToken) - - return accumulator - }, []) - - setErc20Addresses(newErc20Addresses) - - setOrders(ordersFetched.map((order) => transformOrder(order))) - setMountNewOrders(true) - } catch (e) { - const msg = `Failed to fetch orders` - console.error(msg, e) - setError(msg) - } finally { - setIsLoading(false) - } - }, - [limit, offset], - ) - - useEffect(() => { - if (!networkId) { - return - } - - setIsThereNext(false) - fetchOrders(networkId, ownerAddress) - - if (pageIndex && pageIndex > 1) return - - const intervalId: NodeJS.Timeout = setInterval(() => { - fetchOrders(networkId, ownerAddress) - }, ORDERS_QUERY_INTERVAL) - - return (): void => { - clearInterval(intervalId) - } - }, [fetchOrders, networkId, ownerAddress, pageIndex]) - - useEffect(() => { - if (!orders || areErc20Loading || isObjectEmpty(valueErc20s) || !mountNewOrders) { - return - } - - const newOrders = orders.map((order) => { - order.buyToken = valueErc20s[order.buyTokenAddress] || order.buyToken - order.sellToken = valueErc20s[order.sellTokenAddress] || order.sellToken - - return order - }) - - setOrders(newOrders) - setMountNewOrders(false) - setErc20Addresses([]) - }, [valueErc20s, networkId, areErc20Loading, mountNewOrders, orders]) - - return { orders, error, isLoading, isThereNext } -} diff --git a/src/apps/explorer/components/TransactionsTableWidget/TransactionsTableWithData.tsx b/src/apps/explorer/components/TransactionsTableWidget/TransactionsTableWithData.tsx new file mode 100644 index 000000000..9caa3518c --- /dev/null +++ b/src/apps/explorer/components/TransactionsTableWidget/TransactionsTableWithData.tsx @@ -0,0 +1,44 @@ +import React, { useContext, useState, useEffect } from 'react' + +import { EmptyItemWrapper } from 'components/common/StyledUserDetailsTable' +import useFirstRender from 'hooks/useFirstRender' +import Spinner from 'components/common/Spinner' +import { TransactionsTableContext } from 'apps/explorer/components/TransactionsTableWidget/context/TransactionsTableContext' +import TransactionTable from 'components/transaction/TransactionTable' +import { DEFAULT_TIMEOUT } from 'const' + +export const TransactionsTableWithData: React.FC = () => { + const { + orders, + txHashParams: { networkId }, + } = useContext(TransactionsTableContext) + const isFirstRender = useFirstRender() + const [isFirstLoading, setIsFirstLoading] = useState(true) + + useEffect(() => { + setIsFirstLoading(true) + }, [networkId]) + + useEffect(() => { + let timeOutMs = 0 + if (!orders) { + timeOutMs = DEFAULT_TIMEOUT + } + + const timeOutId: NodeJS.Timeout = setTimeout(() => { + setIsFirstLoading(false) + }, timeOutMs) + + return (): void => { + clearTimeout(timeOutId) + } + }, [orders, orders?.length]) + + return isFirstRender || isFirstLoading ? ( + + + + ) : ( + + ) +} diff --git a/src/apps/explorer/components/TransactionsTableWidget/context/TransactionsTableContext.tsx b/src/apps/explorer/components/TransactionsTableWidget/context/TransactionsTableContext.tsx new file mode 100644 index 000000000..c45d8fe60 --- /dev/null +++ b/src/apps/explorer/components/TransactionsTableWidget/context/TransactionsTableContext.tsx @@ -0,0 +1,15 @@ +import React from 'react' + +import { Network } from 'types' +import { Order } from 'api/operator' + +export type BlockchainNetwork = Network | undefined + +type CommonState = { + txHashParams: { networkId: BlockchainNetwork; txHash: string } + error: string + orders: Order[] | undefined + isTxLoading: boolean +} + +export const TransactionsTableContext = React.createContext({} as CommonState) diff --git a/src/apps/explorer/components/TransactionsTableWidget/index.tsx b/src/apps/explorer/components/TransactionsTableWidget/index.tsx new file mode 100644 index 000000000..00aeb8801 --- /dev/null +++ b/src/apps/explorer/components/TransactionsTableWidget/index.tsx @@ -0,0 +1,89 @@ +import React, { useState, useEffect } from 'react' + +import { BlockchainNetwork, TransactionsTableContext } from './context/TransactionsTableContext' +import { useGetTxOrders } from 'hooks/useGetOrders' +import RedirectToSearch from 'components/RedirectToSearch' +import Spinner from 'components/common/Spinner' +import { RedirectToNetwork, useNetworkId } from 'state/network' +import { Order } from 'api/operator' +import { TransactionsTableWithData } from 'apps/explorer/components/TransactionsTableWidget/TransactionsTableWithData' +import { TabItemInterface } from 'components/common/Tabs/Tabs' +import ExplorerTabs from '../common/ExplorerTabs/ExplorerTab' +import styled from 'styled-components' +import { TitleAddress } from 'apps/explorer/pages/styled' +import { BlockExplorerLink } from 'components/common/BlockExplorerLink' + +interface Props { + txHash: string + networkId: BlockchainNetwork + transactions?: Order[] +} + +const StyledTabLoader = styled.span` + padding-left: 4px; +` + +const tabItems = (isLoadingOrders: boolean): TabItemInterface[] => { + return [ + { + id: 1, + tab: ( + <> + Transactions + {isLoadingOrders && } + + ), + content: , + }, + ] +} + +export const TransactionsTableWidget: React.FC = ({ txHash }) => { + const { orders, isLoading: isTxLoading, errorTxPresentInNetworkId, error } = useGetTxOrders(txHash) + const networkId = useNetworkId() || undefined + const [redirectTo, setRedirectTo] = useState(false) + const txHashParams = { networkId, txHash } + // Avoid redirecting until another network is searched again + useEffect(() => { + if (orders?.length || isTxLoading) return + + const timer = setTimeout(() => { + setRedirectTo(true) + }, 500) + + return (): void => clearTimeout(timer) + }) + + if (errorTxPresentInNetworkId && networkId != errorTxPresentInNetworkId) { + return + } + if (redirectTo) { + return + } + + if (!orders?.length) { + return + } + + return ( + <> +

+ Transaction details + } + /> +

+ + + + + ) +} diff --git a/src/apps/explorer/components/common/Search/index.tsx b/src/apps/explorer/components/common/Search/index.tsx index 59a23c425..c74f6df06 100644 --- a/src/apps/explorer/components/common/Search/index.tsx +++ b/src/apps/explorer/components/common/Search/index.tsx @@ -15,6 +15,7 @@ export const Search: React.FC & SearchProps const [query, setQuery] = useState('') const [showPlaceholder, setShowPlaceholder] = useState(true) const handleSubmit = useSearchSubmit() + const placeHolderText = 'Order ID / ETH Address / ENS Address / Tx Hash' useEffect(() => { if (searchString && submitSearchImmediatly) { @@ -39,10 +40,10 @@ export const Search: React.FC & SearchProps name="query" value={query} onChange={(e): void => setQuery(e.target.value.trim())} - placeholder="Order ID / ETH Address / ENS Address" + placeholder={placeHolderText} aria-label="Search the GP explorer for orders, batches and transactions" /> - Order ID / ETH Address / ENS Address + {placeHolderText} ) } diff --git a/src/apps/explorer/layout/Header.tsx b/src/apps/explorer/layout/Header.tsx index 5ba411be4..9f1273e98 100644 --- a/src/apps/explorer/layout/Header.tsx +++ b/src/apps/explorer/layout/Header.tsx @@ -1,11 +1,29 @@ -import React from 'react' +import React, { useEffect, useRef, useState } from 'react' -import { Navigation } from 'components/layout/GenericLayout/Navigation' +import { MenuBarToggle, Navigation } from 'components/layout/GenericLayout/Navigation' import { Header as GenericHeader } from 'components/layout/GenericLayout/Header' import { NetworkSelector } from 'components/NetworkSelector' import { PREFIX_BY_NETWORK_ID, useNetworkId } from 'state/network' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { faEllipsisH, faTimes } from '@fortawesome/free-solid-svg-icons' +import { FlexWrap } from 'apps/explorer/pages/styled' +import { ExternalLink } from 'components/analytics/ExternalLink' +import { useHistory } from 'react-router' export const Header: React.FC = () => { + const ref = useRef(null) + const history = useHistory() + const [isBarActive, setBarActive] = useState(false) + + useEffect(() => { + const isClickedOutside = (e: any): void => { + isBarActive && ref.current && !ref.current.contains(e.target) && setBarActive(false) + } + document.addEventListener('mousedown', isClickedOutside) + return (): void => { + document.removeEventListener('mousedown', isClickedOutside) + } + }, [isBarActive]) const networkId = useNetworkId() if (!networkId) { return null @@ -13,22 +31,40 @@ export const Header: React.FC = () => { const prefixNetwork = PREFIX_BY_NETWORK_ID.get(networkId) + const handleNavigate = (e: any): void => { + e.preventDefault() + setBarActive(false) + history.push('/') + } + return ( - - - {/* -
  • - Batches -
  • -
  • - Trades -
  • -
  • - Markets -
  • - */} -
    + + + setBarActive(!isBarActive)}> + + + +
  • + handleNavigate(e)}>Home +
  • +
  • + + CoW Protocol + +
  • +
  • + + Documentation + +
  • +
  • + + Community + +
  • +
    +
    ) } diff --git a/src/apps/explorer/pages/Home/index.tsx b/src/apps/explorer/pages/Home/index.tsx index 9aa95fce2..3cd30e10f 100644 --- a/src/apps/explorer/pages/Home/index.tsx +++ b/src/apps/explorer/pages/Home/index.tsx @@ -1,34 +1,21 @@ import React from 'react' -import styled from 'styled-components' import { Search } from 'apps/explorer/components/common/Search' -import { media } from 'theme/styles/media' +import { Wrapper as WrapperMod } from 'apps/explorer/pages/styled' +import styled from 'styled-components' -const Wrapper = styled.div` - display: flex; - justify-content: center; +const Wrapper = styled(WrapperMod)` + max-width: 140rem; flex-flow: column wrap; - height: calc(100vh - 15rem); - padding: 1.6rem; - margin: 0 auto; - width: 100%; + justify-content: center; + display: flex; > h1 { - text-align: center; + justify-content: center; padding: 2.4rem 0 0.75rem; - font-weight: ${({ theme }): string => theme.fontBold}; - width: 100%; margin: 0 0 2.4rem; - font-size: 2rem; + font-size: 2.4rem; line-height: 1; } - - ${media.mobile} { - > h1 { - line-height: 1.2; - margin-bottom: 1rem; - font-size: 1.7rem; - } - } ` export const Home: React.FC = () => { diff --git a/src/apps/explorer/pages/NotFound.tsx b/src/apps/explorer/pages/NotFound.tsx index cbcfb076e..ae266bf4e 100644 --- a/src/apps/explorer/pages/NotFound.tsx +++ b/src/apps/explorer/pages/NotFound.tsx @@ -1,63 +1,19 @@ import React from 'react' -import { Link } from 'react-router-dom' import styled from 'styled-components' -import { media } from 'theme/styles/media' +import { ContentCard as Content, StyledLink, Title, Wrapper as WrapperTemplate } from 'apps/explorer/pages/styled' import { getNetworkFromId } from '@gnosis.pm/dex-js' import { useNetworkId } from 'state/network' +import { media } from 'theme/styles/media' -const Wrapper = styled.div` +const Wrapper = styled(WrapperTemplate)` max-width: 118rem; - margin: 0 auto; - padding: 1.6rem; ${media.mediumDown} { - max-width: 94rem; flex-flow: column wrap; } - - ${media.mobile} { - max-width: 100%; - } ` -const Title = styled.h1` - margin: 3rem 0 2.95rem; - font-weight: ${({ theme }): string => theme.fontBold}; -` - -const Content = styled.div` - font-size: 1.6rem; - border: 0.1rem solid ${({ theme }): string => theme.borderPrimary}; - padding: 20px; - border-radius: 0.4rem; - min-height: 23rem; - display: flex; - flex-direction: column; - justify-content: space-between; - - p { - line-height: ${({ theme }): string => theme.fontLineHeight}; - overflow-wrap: break-word; - } -` - -const StyledLink = styled(Link)` - height: 5rem; - border: 0.1rem solid ${({ theme }): string => theme.borderPrimary}; - border-radius: 0.6rem; - width: 16rem; - display: flex; - align-items: center; - justify-content: center; - cursor: pointer; - color: ${({ theme }): string => theme.white} !important; - - :hover { - background-color: ${({ theme }): string => theme.greyOpacity}; - text-decoration: none; - } -` const NotFoundRequestPage: React.FC = () => { const networkId = useNetworkId() || 1 const network = networkId !== 1 ? getNetworkFromId(networkId).toLowerCase() : '' diff --git a/src/apps/explorer/pages/Order.tsx b/src/apps/explorer/pages/Order.tsx index 35fc6e404..f1fab59ce 100644 --- a/src/apps/explorer/pages/Order.tsx +++ b/src/apps/explorer/pages/Order.tsx @@ -5,7 +5,16 @@ import { isAnOrderId } from 'utils' import RedirectToSearch from 'components/RedirectToSearch' import { OrderWidget } from 'apps/explorer/components/OrderWidget' -import { WrapperPage } from './styled' +import { Wrapper as WrapperMod } from './styled' +import styled from 'styled-components' + +const Wrapper = styled(WrapperMod)` + max-width: 140rem; + + > h1 { + padding: 2.4rem 0 0.75rem; + } +` const Order: React.FC = () => { const orderId = useOrderIdParam() @@ -15,9 +24,9 @@ const Order: React.FC = () => { } return ( - + - + ) } diff --git a/src/apps/explorer/pages/SearchNotFound.tsx b/src/apps/explorer/pages/SearchNotFound.tsx index 8e6f61345..e6ed4b046 100644 --- a/src/apps/explorer/pages/SearchNotFound.tsx +++ b/src/apps/explorer/pages/SearchNotFound.tsx @@ -1,13 +1,13 @@ import React from 'react' import { OrderAddressNotFound } from 'components/orders/OrderNotFound' -import { WrapperPage } from './styled' +import { Wrapper } from './styled' const SearchNotFound: React.FC = () => { return ( - + - + ) } diff --git a/src/apps/explorer/pages/TransactionDetails.tsx b/src/apps/explorer/pages/TransactionDetails.tsx new file mode 100644 index 000000000..98042ba48 --- /dev/null +++ b/src/apps/explorer/pages/TransactionDetails.tsx @@ -0,0 +1,25 @@ +import React from 'react' +import { useParams } from 'react-router' + +import { isATxHash } from 'utils' +import RedirectToSearch from 'components/RedirectToSearch' +import { Wrapper } from 'apps/explorer/pages/styled' +import { useNetworkId } from 'state/network' +import { TransactionsTableWidget } from 'apps/explorer/components/TransactionsTableWidget' + +const TransactionDetails: React.FC = () => { + const { txHash } = useParams<{ txHash: string }>() + const networkId = useNetworkId() || undefined + + if (!isATxHash(txHash)) { + return + } + + return ( + + + + ) +} + +export default TransactionDetails diff --git a/src/apps/explorer/pages/UserDetails.tsx b/src/apps/explorer/pages/UserDetails.tsx index 2ba86a055..7c8c41469 100644 --- a/src/apps/explorer/pages/UserDetails.tsx +++ b/src/apps/explorer/pages/UserDetails.tsx @@ -1,42 +1,14 @@ import React from 'react' import { useParams } from 'react-router' -import styled from 'styled-components' -import { media } from 'theme/styles/media' import OrdersTableWidget from '../components/OrdersTableWidget' import { useNetworkId } from 'state/network' import { BlockExplorerLink } from 'components/common/BlockExplorerLink' -import { RowWithCopyButton } from 'components/common/RowWithCopyButton' import RedirectToSearch from 'components/RedirectToSearch' import { useResolveEns } from 'hooks/useResolveEns' import Spinner from 'components/common/Spinner' +import { TitleAddress, Wrapper } from 'apps/explorer/pages/styled' -const Wrapper = styled.div` - padding: 1.6rem; - margin: 0 auto; - width: 100%; - - ${media.mediumDown} { - max-width: 94rem; - } - - ${media.mobile} { - max-width: 100%; - } - > h1 { - display: flex; - padding: 2.4rem 0 2.35rem; - align-items: center; - font-weight: ${({ theme }): string => theme.fontBold}; - } -` -const TitleAddress = styled(RowWithCopyButton)` - font-size: ${({ theme }): string => theme.fontSizeDefault}; - font-weight: ${({ theme }): string => theme.fontNormal}; - margin: 0 0 0 1.5rem; - display: flex; - align-items: center; -` const UserDetails: React.FC = () => { const { address } = useParams<{ address: string }>() const networkId = useNetworkId() || undefined diff --git a/src/apps/explorer/pages/styled.tsx b/src/apps/explorer/pages/styled.tsx index 65629d4d4..a859e17cc 100644 --- a/src/apps/explorer/pages/styled.tsx +++ b/src/apps/explorer/pages/styled.tsx @@ -1,11 +1,14 @@ import styled from 'styled-components' import { media } from 'theme/styles/media' +import { RowWithCopyButton } from 'components/common/RowWithCopyButton' +import * as CSS from 'csstype' +import { Link } from 'react-router-dom' -export const WrapperPage = styled.div` +export const Wrapper = styled.div` padding: 1.6rem; margin: 0 auto; width: 100%; - max-width: 140rem; + flex-grow: 1; ${media.mediumDown} { max-width: 94rem; @@ -17,8 +20,64 @@ export const WrapperPage = styled.div` > h1 { display: flex; - padding: 2.4rem 0 0.75rem; + padding: 2.4rem 0 2.35rem; align-items: center; font-weight: ${({ theme }): string => theme.fontBold}; + margin: 0; + } +` + +export const TitleAddress = styled(RowWithCopyButton)` + font-size: ${({ theme }): string => theme.fontSizeDefault}; + font-weight: ${({ theme }): string => theme.fontNormal}; + margin: 0 0 0 1.5rem; + display: flex; + align-items: center; + ${media.tinyDown} { + font-size: 1.2rem; + } +` + +export const FlexWrap = styled.div>` + display: flex; + align-items: center; + flex-grow: ${({ grow }): string => `${grow}` || '0'}; +` + +export const StyledLink = styled(Link)` + height: 5rem; + border: 0.1rem solid ${({ theme }): string => theme.borderPrimary}; + border-radius: 0.6rem; + width: 16rem; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + color: ${({ theme }): string => theme.white} !important; + + :hover { + background-color: ${({ theme }): string => theme.greyOpacity}; + text-decoration: none; + } +` + +export const Title = styled.h1` + margin: 3rem 0 2.95rem; + font-weight: ${({ theme }): string => theme.fontBold}; +` + +export const ContentCard = styled.div` + font-size: 1.6rem; + border: 0.1rem solid ${({ theme }): string => theme.borderPrimary}; + padding: 20px; + border-radius: 0.4rem; + min-height: 23rem; + display: flex; + flex-direction: column; + justify-content: space-between; + + p { + line-height: ${({ theme }): string => theme.fontLineHeight}; + overflow-wrap: break-word; } ` diff --git a/src/apps/explorer/styled.ts b/src/apps/explorer/styled.ts new file mode 100644 index 000000000..7840a3b1f --- /dev/null +++ b/src/apps/explorer/styled.ts @@ -0,0 +1,58 @@ +import styled from 'styled-components' +import { media } from 'theme/styles/media' +import { createGlobalStyle } from 'styled-components' + +export const GlobalStyle = createGlobalStyle` + html { + height: 100%; + } + html, + body, + #root { + display: flex; + flex-direction: column; + flex-grow: 1; + } +` + +export const MainWrapper = styled.div` + max-width: 118rem; + width: 100%; + margin: 0 auto; + display: flex; + flex-direction: column; + flex-grow: 1; + + > div { + display: flex; + flex-direction: column; + flex-grow: 1; + } + footer { + flex-direction: row; + flex-wrap: wrap; + flex-grow: 0; + } + header { + margin-left: 0; + margin-right: 0; + } + + ${media.mediumDown} { + max-width: 94rem; + flex-flow: column wrap; + } + + ${media.xSmallDown} { + max-width: 100%; + flex-grow: 1; + header { + margin-left: auto; + margin-right: auto; + } + footer { + flex-direction: column; + flex-wrap: nowrap; + } + } +` diff --git a/src/assets/img/tokens/0x44fa8e6f47987339850636f88629646662444217.png b/src/assets/img/tokens/0x44fa8e6f47987339850636f88629646662444217.png new file mode 100644 index 000000000..836fe7876 Binary files /dev/null and b/src/assets/img/tokens/0x44fa8e6f47987339850636f88629646662444217.png differ diff --git a/src/assets/img/tokens/0x9C58BAcC331c9aa871AFD802DB6379a98e80CEdb.png b/src/assets/img/tokens/0x9C58BAcC331c9aa871AFD802DB6379a98e80CEdb.png new file mode 100644 index 000000000..6c9641c86 Binary files /dev/null and b/src/assets/img/tokens/0x9C58BAcC331c9aa871AFD802DB6379a98e80CEdb.png differ diff --git a/src/assets/img/tokens/0xe91D153E0b41518A2Ce8Dd3D7944Fa863463a97d.png b/src/assets/img/tokens/0xe91D153E0b41518A2Ce8Dd3D7944Fa863463a97d.png index 836fe7876..c37d75f76 100644 Binary files a/src/assets/img/tokens/0xe91D153E0b41518A2Ce8Dd3D7944Fa863463a97d.png and b/src/assets/img/tokens/0xe91D153E0b41518A2Ce8Dd3D7944Fa863463a97d.png differ diff --git a/src/assets/img/tokens/xdai.png b/src/assets/img/tokens/xdai.png index 503994caf..a24b7c0e4 100644 Binary files a/src/assets/img/tokens/xdai.png and b/src/assets/img/tokens/xdai.png differ diff --git a/src/components/NetworkSelector/NetworkSelector.styled.ts b/src/components/NetworkSelector/NetworkSelector.styled.ts index cac87df8a..6d10fb0db 100644 --- a/src/components/NetworkSelector/NetworkSelector.styled.ts +++ b/src/components/NetworkSelector/NetworkSelector.styled.ts @@ -2,7 +2,7 @@ import styled from 'styled-components' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { COLOURS } from 'styles' -const { fadedGreyishWhiteOpacity, white, hippieBlue } = COLOURS +const { fadedGreyishWhiteOpacity, white, gnosisChainColor } = COLOURS export const SelectorContainer = styled.div` display: flex; @@ -14,10 +14,10 @@ export const SelectorContainer = styled.div` border-left: 5px solid transparent; border-right: 5px solid transparent; border-bottom: ${({ theme }): string => `5px solid ${theme.grey}`}; - transform: rotate(0deg); + transform: rotate(180deg); transition: transform 0.1s linear; &.open { - transform: rotate(180deg); + transform: rotate(0deg); transition: transform 0.1s linear; } } @@ -30,7 +30,7 @@ export const OptionsContainer = styled.div<{ width: number }>` width: ${(props: { width: number }): string => `${184 + props.width}px`}; height: 128px; left: 15px; - top: 50px; + top: 54px; background: ${({ theme }): string => theme.bg1}; border: ${(): string => `1px solid ${fadedGreyishWhiteOpacity}`}; box-sizing: border-box; @@ -68,7 +68,7 @@ export const Option = styled.div` background: ${({ theme }): string => theme.yellow4}; } &.gnosischain { - background: ${(): string => hippieBlue}; + background: ${(): string => gnosisChainColor}; } &.ethereum { background: ${({ theme }): string => theme.blue4}; @@ -98,8 +98,8 @@ export const NetworkLabel = styled.span` } &.gnosischain { - background: ${(): string => `rgb(72 169 166 / 25%);`}; - color: ${(): string => hippieBlue}; + background: ${(): string => `rgb(4 121 91 / 15%);`}; + color: ${(): string => gnosisChainColor}; } ` diff --git a/src/components/common/DateDisplay/index.tsx b/src/components/common/DateDisplay/index.tsx index fa71bd52f..362c8fe89 100644 --- a/src/components/common/DateDisplay/index.tsx +++ b/src/components/common/DateDisplay/index.tsx @@ -45,12 +45,14 @@ export function DateDisplay({ date, showIcon, tooltipPlacement = 'top' }: DateDi {distance} - {fullLocaleBased} - {showIcon && ( + {showIcon ? ( + {previewDate} - )}{' '} - {!showIcon ? {previewDate} : {previewDate}} + ) : ( + {previewDate} + )} ) diff --git a/src/components/common/SimpleTable/index.tsx b/src/components/common/SimpleTable/index.tsx index f8d676b06..8e3577482 100644 --- a/src/components/common/SimpleTable/index.tsx +++ b/src/components/common/SimpleTable/index.tsx @@ -6,7 +6,7 @@ const Wrapper = styled.table<{ $numColumns?: number }>` font-size: ${({ theme }): string => theme.fontSizeDefault}; background-color: transparent; color: ${({ theme }): string => theme.textPrimary1}; - height: 100%; + height: auto; width: 100%; margin: 1.6rem auto 0; padding: 0; @@ -113,7 +113,7 @@ const Wrapper = styled.table<{ $numColumns?: number }>` export type Props = { header?: JSX.Element - body: JSX.Element + body?: JSX.Element className?: string numColumns?: number } diff --git a/src/components/common/TokenImg.tsx b/src/components/common/TokenImg.tsx index d15697cdf..de9ef3266 100644 --- a/src/components/common/TokenImg.tsx +++ b/src/components/common/TokenImg.tsx @@ -10,7 +10,6 @@ const Wrapper = styled.img` border-radius: 3.6rem; object-fit: contain; background-color: white; - padding: 2px; opacity: ${(props): number => (props.faded ? 0.4 : 1)}; ` diff --git a/src/components/layout/GenericLayout/Footer/index.tsx b/src/components/layout/GenericLayout/Footer/index.tsx index 4ba638b65..321746bdb 100644 --- a/src/components/layout/GenericLayout/Footer/index.tsx +++ b/src/components/layout/GenericLayout/Footer/index.tsx @@ -7,10 +7,11 @@ import { getGpV2ContractAddress } from 'utils/contract' import { BlockExplorerLink } from 'apps/gp-v1/components/common/BlockExplorerLink' // Hooks -import { useWalletConnection } from 'hooks/useWalletConnection' +import { useNetworkId } from 'state/network' // Config import { footerConfig } from '../Footer/config' +import { Network } from 'types' const FooterStyled = styled.footer` display: flex; @@ -99,10 +100,9 @@ export interface FooterType { export const Footer: React.FC = (props) => { const { isBeta = footerConfig.isBeta, url = footerConfig.url } = props - const { networkIdOrDefault: networkId } = useWalletConnection() + const networkId = useNetworkId() || Network.Mainnet const settlementContractAddress = getGpV2ContractAddress(networkId, 'GPv2Settlement') const vaultRelayerContractAddress = getGpV2ContractAddress(networkId, 'GPv2VaultRelayer') - return ( {isBeta && 'This project is in beta. Use at your own risk.'} diff --git a/src/components/layout/GenericLayout/Navigation/index.tsx b/src/components/layout/GenericLayout/Navigation/index.tsx index 35aef1393..ef1231b33 100644 --- a/src/components/layout/GenericLayout/Navigation/index.tsx +++ b/src/components/layout/GenericLayout/Navigation/index.tsx @@ -1,15 +1,45 @@ import styled from 'styled-components' -import { applyMediaStyles } from 'theme' +import * as CSS from 'csstype' +import { media } from 'theme/styles/media' +import { BASE_COLOURS } from 'theme' -export const Navigation = styled.ol` +export const Navigation = styled.ol>` list-style: none; display: flex; padding: 0; + flex-grow: 1; + justify-content: flex-end; - /* ${applyMediaStyles('upToMedium')` + ${media.mediumDownMd} { + flex-direction: column; + flex-wrap: nowrap; + justify-content: flex-start; margin: 0 0 0 auto; - `} - */ + position: absolute; + width: 100%; + top: 100%; + max-width: 260px; + border: 1px solid var(--color-border); + border-radius: 0.4rem; + transition: right 0.07s ease-in-out; + left: auto; + right: 15px; + background-color: var(--color-primary); + opacity: ${({ isActive }): string => (isActive ? '1' : '0')}; + z-index: ${({ isActive }): string => (isActive ? '99' : '-1')}; + } + + ${media.xSmallDown} { + border-right: 1px solid var(--color-border); + max-width: 100%; + top: 0; + bottom: 0; + position: fixed; + transition: left 0.15s ease-in-out; + padding-top: 20%; + right: auto; + left: ${({ isActive }): string => (isActive ? '0' : '-100%')}; + } > li { font-size: var(--font-size-larger); @@ -20,11 +50,40 @@ export const Navigation = styled.ol` position: relative; flex-flow: row; display: flex; + padding: 0 15px; + &:not(:last-child):after { + content: ''; + display: block; + position: absolute; + right: 0; + top: 0; + bottom: 0; + height: 100%; + width: 1px; + background-color: var(--color-text-secondary2); + opacity: 0.6; + } + + ${media.mediumDownMd} { + padding: 10px 15px; + text-align: center; + &:not(:last-child):after { + right: auto; + left: 50%; + top: auto; + bottom: 0; + width: calc(100% - 30px); + transform: translateX(-50%); + height: 1px; + background-color: var(--color-text-secondary2); + opacity: 0.2; + } + } } > li.active, > li:hover { - background-color: var(--color-gradient-2); + background-color: transparent; color: var(--color-text-primary); font-weight: var(--font-weight-medium); } @@ -35,7 +94,7 @@ export const Navigation = styled.ol` > li > div > a, > li > a { - font-weight: var(--font-weight-normal); + font-weight: var(--font-weight-bold); font-size: inherit; color: inherit; text-align: center; @@ -45,8 +104,8 @@ export const Navigation = styled.ol` display: flex; align-items: center; position: relative; - font-family: inherit; margin: 0; + flex-grow: 1; border-radius: 0.6rem; } @@ -55,3 +114,31 @@ export const Navigation = styled.ol` transition: width 0.3s ease-in-out, background 0.3s ease-in-out; } ` + +export const MenuBarToggle = styled.button>` + color: ${({ isActive }): string => (isActive ? 'var(--color-text-secondary1)' : 'var(--color-text-secondary2)')}; + font-size: 17px; + padding: 5px 10px; + border: 1px solid var(--color-border); + display: none; + width: 40px; + height: 40px; + justify-content: center; + align-items: center; + background-color: var(--color-primary); + background-image: none; + border-radius: 0.4rem; + margin-left: auto; + cursor: pointer; + transition: 0.1s ease-in-out; + ${media.mediumDownMd} { + display: flex; + z-index: 100; + position: relative; + } + + &:hover, + &:focus { + border: 1px solid ${BASE_COLOURS.blue4}; + } +` diff --git a/src/components/orders/DetailsTable/index.tsx b/src/components/orders/DetailsTable/index.tsx index b32321bc9..58c98809f 100644 --- a/src/components/orders/DetailsTable/index.tsx +++ b/src/components/orders/DetailsTable/index.tsx @@ -6,7 +6,6 @@ import { Order } from 'api/operator' import { capitalize } from 'utils' -import { BlockExplorerLink } from 'apps/explorer/components/common/BlockExplorerLink' import { HelpTooltip } from 'components/Tooltip' import { SimpleTable } from 'components/common/SimpleTable' @@ -174,7 +173,7 @@ export function DetailsTable(props: Props): JSX.Element | null { onCopy('settlementTx')} - contentsToDisplay={} + contentsToDisplay={{txHash}} /> ) : ( '-' diff --git a/src/components/orders/OrderNotFound/index.tsx b/src/components/orders/OrderNotFound/index.tsx index 00f5f4f07..a38749338 100644 --- a/src/components/orders/OrderNotFound/index.tsx +++ b/src/components/orders/OrderNotFound/index.tsx @@ -95,7 +95,7 @@ export const OrderAddressNotFound: React.FC = (): JSX.Element => { return ( <> - Order or Address not found + No results found {searchString ? ( <> diff --git a/src/components/transaction/TransactionTable/index.stories.tsx b/src/components/transaction/TransactionTable/index.stories.tsx new file mode 100644 index 000000000..e1ad231c2 --- /dev/null +++ b/src/components/transaction/TransactionTable/index.stories.tsx @@ -0,0 +1,51 @@ +import React from 'react' +// also exported from '@storybook/react' if you can deal with breaking changes in 6.1 +import { Story, Meta } from '@storybook/react/types-6-0' +import TransactionTable, { Props as TransactionTableProps } from '.' +import BigNumber from 'bignumber.js' +import { GlobalStyles, ThemeToggler, Router, NetworkDecorator } from 'storybook/decorators' + +import { Order } from 'api/operator' +import { RICH_ORDER, TUSD, WETH } from '../../../../test/data' + +export default { + title: 'transaction/TransactionTable', + decorators: [Router, GlobalStyles, NetworkDecorator, ThemeToggler], + component: TransactionTable, +} as Meta + +const transactionExBuy: Order = { + ...RICH_ORDER, + kind: 'buy', + buyToken: WETH, + sellToken: TUSD, + buyAmount: new BigNumber('1500000000000000000'), // 1.5WETH + sellAmount: new BigNumber('7500000000000000000000'), // 7500 TUSD + expirationDate: new Date(), + txHash: '0x489d8fd1efd43394c7c2b26216f36f1ab49b8d67623047e0fcb60efa2a2c420b', + partiallyFilled: true, + shortId: '0x489d8fd1ef', + status: 'filled', +} + +const transactionExSell: Order = { + ...RICH_ORDER, + kind: 'sell', + buyToken: WETH, + sellToken: TUSD, + buyAmount: new BigNumber('1500000000000000000'), // 1.5WETH + sellAmount: new BigNumber('7500000000000000000000'), // 7500 TUSD + expirationDate: new Date(), + txHash: '0x489d8fd1efd43394c7c2b26216f36f1ab49b8d67623047e0fcb60efa2a2c420b', + partiallyFilled: false, + shortId: '0x489d8fd1ef', + status: 'open', +} + +const Template: Story = (args) => + +export const Default = Template.bind({}) +Default.args = { orders: [transactionExBuy, transactionExSell], showBorderTable: true } + +export const TxDetailsError = Template.bind({}) +TxDetailsError.args = { orders: [], showBorderTable: true } diff --git a/src/components/transaction/TransactionTable/index.tsx b/src/components/transaction/TransactionTable/index.tsx new file mode 100644 index 000000000..34353e4e7 --- /dev/null +++ b/src/components/transaction/TransactionTable/index.tsx @@ -0,0 +1,294 @@ +import React, { useEffect, useState } from 'react' +import styled from 'styled-components' +import { faExchangeAlt, faSpinner } from '@fortawesome/free-solid-svg-icons' + +import { Order } from 'api/operator' + +import { DateDisplay } from 'components/common/DateDisplay' +import { RowWithCopyButton } from 'components/common/RowWithCopyButton' +import { getOrderLimitPrice, formatCalculatedPriceToDisplay, formattedAmount, FormatAmountPrecision } from 'utils' +import { getShortOrderId } from 'utils/operator' +import { HelpTooltip } from 'components/Tooltip' +import StyledUserDetailsTable, { + StyledUserDetailsTableProps, + EmptyItemWrapper, +} from '../../common/StyledUserDetailsTable' +import Icon from 'components/Icon' +import TradeOrderType from 'components/common/TradeOrderType' +import { LinkWithPrefixNetwork } from 'components/common/LinkWithPrefixNetwork' +import { StatusLabel } from 'components/orders/StatusLabel' +import { media } from 'theme/styles/media' +import { TextWithTooltip } from 'apps/explorer/components/common/TextWithTooltip' +import { TokenDisplay } from 'components/common/TokenDisplay' +import { useNetworkId } from 'state/network' +import { safeTokenName } from '@gnosis.pm/dex-js' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' + +const Wrapper = styled(StyledUserDetailsTable)` + > thead > tr, + > tbody > tr { + grid-template-columns: 12rem 7rem repeat(2, minmax(16rem, 1.5fr)) repeat(2, minmax(18rem, 2fr)) 1fr; + } + tr > td { + span.span-inside-tooltip { + display: flex; + flex-direction: row; + flex-wrap: wrap; + img { + padding: 0; + } + } + } + ${media.desktopMediumDown} { + > thead > tr { + display: none; + } + > tbody > tr { + grid-template-columns: none; + border: 0.1rem solid ${({ theme }): string => theme.tableRowBorder}; + box-shadow: 0px 4px 12px ${({ theme }): string => theme.boxShadow}; + border-radius: 6px; + margin-top: 16px; + padding: 12px; + &:hover { + background: none; + backdrop-filter: none; + } + } + tr > td { + display: flex; + flex: 1; + width: 100%; + justify-content: space-between; + margin: 0; + margin-bottom: 18px; + min-height: 32px; + span.span-inside-tooltip { + align-items: flex-end; + flex-direction: column; + img { + margin-left: 0; + } + } + } + .header-value { + flex-wrap: wrap; + text-align: end; + } + .span-copybtn-wrap { + display: flex; + flex-wrap: nowrap; + span { + display: flex; + align-items: center; + } + .copy-text { + margin-left: 5px; + } + } + } + overflow: auto; +` + +const HeaderTitle = styled.span` + display: none; + ${media.desktopMediumDown} { + font-weight: 600; + align-items: center; + display: flex; + margin-right: 3rem; + svg { + margin-left: 5px; + } + } +` +const HeaderValue = styled.span` + ${media.desktopMediumDown} { + flex-wrap: wrap; + text-align: end; + } +` + +function getLimitPrice(order: Order, isPriceInverted: boolean): string { + if (!order.buyToken || !order.sellToken) return '-' + + const calculatedPrice = getOrderLimitPrice({ + buyAmount: order.buyAmount, + sellAmount: order.sellAmount, + buyTokenDecimals: order.buyToken.decimals, + sellTokenDecimals: order.sellToken.decimals, + inverted: isPriceInverted, + }) + + return formatCalculatedPriceToDisplay(calculatedPrice, order.buyToken, order.sellToken, isPriceInverted) +} + +const tooltip = { + orderID: 'A unique identifier ID for this order.', +} + +export type Props = StyledUserDetailsTableProps & { + orders: Order[] | undefined +} + +interface RowProps { + order: Order + isPriceInverted: boolean + invertLimitPrice: () => void +} + +const RowTransaction: React.FC = ({ order, isPriceInverted, invertLimitPrice }) => { + const { + buyToken, + buyAmount, + expirationDate, + partiallyFilled = false, + sellToken, + sellAmount, + kind, + txHash, + shortId, + uid, + } = order + const network = useNetworkId() + const buyTokenSymbol = buyToken ? safeTokenName(buyToken) : '' + const sellTokenSymbol = sellToken ? safeTokenName(sellToken) : '' + const sellFormattedAmount = formattedAmount(sellToken, sellAmount) + const buyFormattedAmount = formattedAmount(buyToken, buyAmount) + const renderSpinnerWhenNoValue = (textValue: string): JSX.Element | void => { + if (textValue === '-') return + } + const limitPriceSettled = getLimitPrice(order, isPriceInverted) + + return ( + + + + Order ID + + + + {getShortOrderId(shortId)} + + } + /> + + + + Type + + + + + + Sell Amount + + {renderSpinnerWhenNoValue(sellFormattedAmount) || ( + + {formattedAmount(sellToken, sellAmount, FormatAmountPrecision.highPrecision)}{' '} + {sellToken && network && } + + )} + + + + Buy amount + + {renderSpinnerWhenNoValue(buyFormattedAmount) || ( + + {formattedAmount(buyToken, buyAmount, FormatAmountPrecision.highPrecision)}{' '} + {buyToken && network && } + + )} + + + + + Limit price + + + {renderSpinnerWhenNoValue(limitPriceSettled) || limitPriceSettled} + + + Created + + + + + + Status + + + + + + ) +} + +const TransactionTable: React.FC = (props) => { + const { orders, showBorderTable = false } = props + const [isPriceInverted, setIsPriceInverted] = useState(false) + useEffect(() => { + setIsPriceInverted(isPriceInverted) + }, [isPriceInverted]) + const invertLimitPrice = (): void => { + setIsPriceInverted((previousValue) => !previousValue) + } + + const orderItems = (items: Order[] | undefined): JSX.Element => { + let tableContent + if (!items || items.length === 0) { + tableContent = ( + + + + Can't load details
    Please try again +
    + + + ) + } else { + tableContent = ( + <> + {items.map((item, i) => ( + + ))} + + ) + } + return tableContent + } + + return ( + + + Order ID + + Type + Sell Amount + Buy Amount + + Limit price + + Created + Status + + } + body={orderItems(orders)} + /> + ) +} + +export default TransactionTable diff --git a/src/hooks/useGetOrders.tsx b/src/hooks/useGetOrders.tsx new file mode 100644 index 000000000..c295aab67 --- /dev/null +++ b/src/hooks/useGetOrders.tsx @@ -0,0 +1,212 @@ +import { useState, useEffect, useCallback } from 'react' + +import { Network } from 'types' +import { useMultipleErc20 } from 'hooks/useErc20' +import { getAccountOrders, getTxOrders, Order } from 'api/operator' +import { GetTxOrdersParams, RawOrder } from 'api/operator/types' +import { useNetworkId } from 'state/network' +import { transformOrder } from 'utils' +import { ORDERS_QUERY_INTERVAL } from 'apps/explorer/const' +import { + GetOrderResult, + MultipleOrders, + GetOrderApi, + tryGetOrderOnAllNetworks, +} from 'services/helpers/tryGetOrderOnAllNetworks' + +function isObjectEmpty(object: Record): boolean { + // eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars + for (const key in object) { + if (key) return false + } + + return true +} + +function filterDuplicateErc20Addresses(ordersFetched: RawOrder[]): string[] { + return ordersFetched.reduce((accumulator: string[], element) => { + const updateAccumulator = (tokenAddress: string): void => { + if (accumulator.indexOf(tokenAddress) === -1) { + accumulator.push(tokenAddress) + } + } + updateAccumulator(element.buyToken) + updateAccumulator(element.sellToken) + + return accumulator + }, []) +} + +type Result = { + orders: Order[] | undefined + error: string + isLoading: boolean +} + +type GetAccountOrdersResult = Result & { + isThereNext: boolean +} + +type GetTxOrdersResult = Result & { + errorTxPresentInNetworkId: Network | null +} + +interface UseOrdersWithTokenInfo { + orders: Order[] | undefined + areErc20Loading: boolean + setOrders: (value: Order[] | undefined) => void + setMountNewOrders: (value: boolean) => void + setErc20Addresses: (value: string[]) => void +} + +export function getTxOrderOnEveryNetwork(networkId: Network, txHash: string): Promise> { + const defaultParams: GetTxOrdersParams = { networkId, txHash } + const getOrderApi: GetOrderApi = { + api: (_defaultParams) => getTxOrders(_defaultParams).then((orders) => (orders.length ? orders : null)), + defaultParams, + } + + return tryGetOrderOnAllNetworks(networkId, getOrderApi) +} + +function useOrdersWithTokenInfo(networkId: Network | undefined): UseOrdersWithTokenInfo { + const [orders, setOrders] = useState() + const [erc20Addresses, setErc20Addresses] = useState([]) + const { value: valueErc20s, isLoading: areErc20Loading } = useMultipleErc20({ networkId, addresses: erc20Addresses }) + const [mountNewOrders, setMountNewOrders] = useState(false) + + useEffect(() => { + setOrders(undefined) + setMountNewOrders(false) + }, [networkId]) + + useEffect(() => { + if (!orders || areErc20Loading || isObjectEmpty(valueErc20s) || !mountNewOrders) { + return + } + + const newOrders = orders.map((order) => { + order.buyToken = valueErc20s[order.buyTokenAddress] || order.buyToken + order.sellToken = valueErc20s[order.sellTokenAddress] || order.sellToken + + return order + }) + + setOrders(newOrders) + setMountNewOrders(false) + setErc20Addresses([]) + }, [valueErc20s, networkId, areErc20Loading, mountNewOrders, orders]) + + return { orders, areErc20Loading, setOrders, setMountNewOrders, setErc20Addresses } +} + +export function useGetTxOrders(txHash: string): GetTxOrdersResult { + const networkId = useNetworkId() || undefined + const [isLoading, setIsLoading] = useState(false) + const [error, setError] = useState('') + const { orders, areErc20Loading, setOrders, setMountNewOrders, setErc20Addresses } = useOrdersWithTokenInfo(networkId) + const [errorTxPresentInNetworkId, setErrorTxPresentInNetworkId] = useState(null) + + const fetchOrders = useCallback( + async (network: Network, _txHash: string): Promise => { + setIsLoading(true) + setError('') + + try { + const { order: _orders, errorOrderPresentInNetworkId: errorTxPresentInNetworkIdRaw } = + await getTxOrderOnEveryNetwork(network, _txHash) + const ordersFetched = _orders || [] + const newErc20Addresses = filterDuplicateErc20Addresses(ordersFetched) + + setErc20Addresses(newErc20Addresses) + + setOrders(ordersFetched.map((order) => transformOrder(order))) + setMountNewOrders(true) + + if (errorTxPresentInNetworkIdRaw) { + console.log({ _orders, errorTxPresentInNetworkIdRaw }) + setErrorTxPresentInNetworkId(errorTxPresentInNetworkIdRaw) + } + } catch (e) { + const msg = `Failed to fetch tx orders` + console.error(msg, e) + setError(msg) + } finally { + setIsLoading(false) + } + }, + [setErc20Addresses, setMountNewOrders, setOrders], + ) + + useEffect(() => { + if (!networkId) { + return + } + + fetchOrders(networkId, txHash) + }, [fetchOrders, networkId, txHash]) + + return { orders, error, isLoading: isLoading || areErc20Loading, errorTxPresentInNetworkId } +} + +export function useGetAccountOrders( + ownerAddress: string, + limit = 1000, + offset = 0, + pageIndex?: number, +): GetAccountOrdersResult { + const networkId = useNetworkId() || undefined + const [isLoading, setIsLoading] = useState(false) + const [error, setError] = useState('') + const { orders, setOrders, setMountNewOrders, setErc20Addresses } = useOrdersWithTokenInfo(networkId) + const [isThereNext, setIsThereNext] = useState(false) + + const fetchOrders = useCallback( + async (network: Network, owner: string): Promise => { + setIsLoading(true) + setError('') + const limitPlusOne = limit + 1 + + try { + const ordersFetched = await getAccountOrders({ networkId: network, owner, offset, limit: limitPlusOne }) + if (ordersFetched.length === limitPlusOne) { + setIsThereNext(true) + ordersFetched.pop() + } + const newErc20Addresses = filterDuplicateErc20Addresses(ordersFetched) + setErc20Addresses(newErc20Addresses) + + setOrders(ordersFetched.map((order) => transformOrder(order))) + setMountNewOrders(true) + } catch (e) { + const msg = `Failed to fetch orders` + console.error(msg, e) + setError(msg) + } finally { + setIsLoading(false) + } + }, + [limit, offset, setErc20Addresses, setMountNewOrders, setOrders], + ) + + useEffect(() => { + if (!networkId) { + return + } + + setIsThereNext(false) + fetchOrders(networkId, ownerAddress) + + if (pageIndex && pageIndex > 1) return + + const intervalId: NodeJS.Timeout = setInterval(() => { + fetchOrders(networkId, ownerAddress) + }, ORDERS_QUERY_INTERVAL) + + return (): void => { + clearInterval(intervalId) + } + }, [fetchOrders, networkId, ownerAddress, pageIndex]) + + return { orders, error, isLoading, isThereNext } +} diff --git a/src/hooks/useOperatorOrder.ts b/src/hooks/useOperatorOrder.ts index 9c5cabc29..6c74286e0 100644 --- a/src/hooks/useOperatorOrder.ts +++ b/src/hooks/useOperatorOrder.ts @@ -1,6 +1,6 @@ import { useCallback, useEffect, useState } from 'react' -import { Order, getOrder, RawOrder } from 'api/operator' +import { Order, getOrder, GetOrderParams } from 'api/operator' import { transformOrder } from 'utils' @@ -8,55 +8,29 @@ import { useNetworkId } from 'state/network' import { useMultipleErc20 } from './useErc20' import { Network } from 'types' -import { NETWORK_ID_SEARCH_LIST } from 'apps/explorer/const' +import { + GetOrderApi, + GetOrderResult, + SingleOrder, + tryGetOrderOnAllNetworks, +} from 'services/helpers/tryGetOrderOnAllNetworks' type UseOrderResult = { order: Order | null error?: string isLoading: boolean errorOrderPresentInNetworkId: Network | null - forceUpdate: () => void + forceUpdate?: () => void } -interface GetOrderResult { - order: RawOrder | null - errorOrderPresentInNetworkId?: Network -} - -async function _getOrder( - networkId: Network, - orderId: string, - networkIdSearchListRemaining: Network[] = NETWORK_ID_SEARCH_LIST, -): Promise { - // Get order - const order = await getOrder({ networkId, orderId }) - - if (order || networkIdSearchListRemaining.length === 0) { - // We found the order in the right network - // ...or we have no more networks in which to continue looking - // so we return the "order" (can be null if it wasn't found in any network) - return { order } +function _getOrder(networkId: Network, orderId: string): Promise> { + const defaultParams: GetOrderParams = { networkId, orderId } + const getOrderApi: GetOrderApi = { + api: (_defaultParams) => getOrder(_defaultParams), + defaultParams, } - // If we didn't find the order in the current network, we look in different networks - const [nextNetworkId, ...remainingNetworkIds] = networkIdSearchListRemaining.filter((network) => network != networkId) - - // Try to get the oder in another network (to see if the ID is OK, but the network not) - const isOrderInDifferentNetwork = await getOrder({ networkId: nextNetworkId, orderId }).then( - (order) => order !== null, - ) - - console.log('is in different network', isOrderInDifferentNetwork) - if (isOrderInDifferentNetwork) { - // If the order exist in the other network - return { - order: null, - errorOrderPresentInNetworkId: nextNetworkId, - } - } else { - // Keep looking in other networks - return _getOrder(nextNetworkId, orderId, remainingNetworkIds) - } + return tryGetOrderOnAllNetworks(networkId, getOrderApi) } export function useOrderByNetwork(orderId: string, networkId: Network | null, updateInterval = 0): UseOrderResult { diff --git a/src/hooks/useSearchSubmit.ts b/src/hooks/useSearchSubmit.ts index 2efdd6e0f..e3f4f40af 100644 --- a/src/hooks/useSearchSubmit.ts +++ b/src/hooks/useSearchSubmit.ts @@ -1,6 +1,6 @@ import { useCallback } from 'react' import { useHistory } from 'react-router-dom' -import { isAnAddressAccount, isAnOrderId, isEns } from 'utils' +import { isAnAddressAccount, isAnOrderId, isEns, isATxHash } from 'utils' import { usePathPrefix } from 'state/network' import { web3 } from 'apps/explorer/api' @@ -10,6 +10,8 @@ export function pathAccordingTo(query: string): string { path = 'address' } else if (isAnOrderId(query)) { path = 'orders' + } else if (isATxHash(query)) { + path = 'tx' } return path diff --git a/src/services/helpers/tryGetOrderOnAllNetworks.ts b/src/services/helpers/tryGetOrderOnAllNetworks.ts new file mode 100644 index 000000000..c11f8fae6 --- /dev/null +++ b/src/services/helpers/tryGetOrderOnAllNetworks.ts @@ -0,0 +1,61 @@ +import { GetOrderParams, GetTxOrdersParams, RawOrder } from 'api/operator' +import { NETWORK_ID_SEARCH_LIST } from 'apps/explorer/const' +import { Network } from 'types' + +export type SingleOrder = RawOrder | null +export type MultipleOrders = RawOrder[] | null + +export interface GetOrderResult { + order: R | null + errorOrderPresentInNetworkId?: Network +} + +type GetOrderParamsApi = { + [K in keyof T]: T[K] +} + +interface GetOrderApiFn { + (params: GetOrderParamsApi): Promise +} + +export type GetOrderApi = { + api: GetOrderApiFn + defaultParams: GetOrderParamsApi +} + +type TypeOrderApiParams = GetOrderParams | GetTxOrdersParams + +export async function tryGetOrderOnAllNetworks( + networkId: Network, + getOrderApi: GetOrderApi, + networkIdSearchListRemaining: Network[] = NETWORK_ID_SEARCH_LIST, +): Promise> { + // Get order + const order = await getOrderApi.api({ ...getOrderApi.defaultParams, networkId }) + + if (order || networkIdSearchListRemaining.length === 0) { + // We found the order in the right network + // ...or we have no more networks in which to continue looking + // so we return the "order" (can be null if it wasn't found in any network) + return { order } + } + + // If we didn't find the order in the current network, we look in different networks + const [nextNetworkId, ...remainingNetworkIds] = networkIdSearchListRemaining.filter((network) => network != networkId) + + // Try to get the oder in another network (to see if the ID is OK, but the network not) + const isOrderInDifferentNetwork = await getOrderApi + .api({ ...getOrderApi.defaultParams, networkId: nextNetworkId }) + .then((_order) => _order !== null) + + if (isOrderInDifferentNetwork) { + // If the order exist in the other network + return { + order: null, + errorOrderPresentInNetworkId: nextNetworkId, + } + } else { + // Keep looking in other networks + return tryGetOrderOnAllNetworks(nextNetworkId, getOrderApi, remainingNetworkIds) + } +} diff --git a/src/styles/colours.ts b/src/styles/colours.ts index f85aba455..d3dbd05c3 100644 --- a/src/styles/colours.ts +++ b/src/styles/colours.ts @@ -3,7 +3,7 @@ export const COLOURS = { whiteDark: '#e9e9f0', blue: '#3F77FF', blueDark: '#185afb', - hippieBlue: '#48A9A6', + gnosisChainColor: '#04795b', purple: '#8958FF', bgLight: '#edf2f7', bgDark: 'linear-gradient(0deg, #21222E 0.05%, #2C2D3F 100%)', diff --git a/src/theme/styles/media.ts b/src/theme/styles/media.ts index 7159143ba..27c4134c9 100644 --- a/src/theme/styles/media.ts +++ b/src/theme/styles/media.ts @@ -5,8 +5,10 @@ export const media = { smallScreen: '736px', smallScreenUp: '737px', mediumScreenSmall: '850px', + mediumScreenMd: '960px', mediumEnd: '1024px', desktopScreen: '1025px', + desktopScreenMedium: '1180px', desktopScreenLarge: '1366px', get tinyDown(): string { return `@media only screen and (max-width : ${this.tinyScreen})` @@ -23,12 +25,18 @@ export const media = { get mediumDown(): string { return `@media only screen and (max-width : ${this.mediumEnd})` }, + get mediumDownMd(): string { + return `@media (max-width: ${this.mediumScreenMd})` + }, get mediumOnly(): string { return `@media only screen and (min-width : ${this.smallScreenUp}) and (max-width : ${this.mediumEnd})` }, get desktop(): string { return `@media only screen and (min-width : ${this.desktopScreen})` }, + get desktopMediumDown(): string { + return `@media only screen and (max-width : ${this.desktopScreenMedium})` + }, get desktopLarge(): string { return `@media only screen and (min-width: ${this.desktopScreenLarge})` }, diff --git a/src/utils/miscellaneous.ts b/src/utils/miscellaneous.ts index 27959a5db..ab445f800 100644 --- a/src/utils/miscellaneous.ts +++ b/src/utils/miscellaneous.ts @@ -281,6 +281,14 @@ export const isAnAddressAccount = (text: string): boolean => { */ export const isEns = (text: string): boolean => text.match(/[a-zA-Z0-9]+\.[a-zA-Z]+$/)?.input !== undefined +/** + * Check if a string is a valid Tx Hash against regex + * + * @param text Possible TxHash string to check + * @returns true if valid or false if not + */ +export const isATxHash = (text: string): boolean => text.match(/^(0x)?([a-fA-F0-9]{64})$/)?.input !== undefined + /** Convert string to lowercase and remove whitespace */ export function cleanNetworkName(networkName: string | undefined): string { if (!networkName) return '' diff --git a/test/services/tryGetOrderOnAllNetworks.test.ts b/test/services/tryGetOrderOnAllNetworks.test.ts new file mode 100644 index 000000000..5a0064004 --- /dev/null +++ b/test/services/tryGetOrderOnAllNetworks.test.ts @@ -0,0 +1,40 @@ +import { GetTxOrdersParams } from 'api/operator/types' +import { GetOrderApi, MultipleOrders, tryGetOrderOnAllNetworks } from 'services/helpers/tryGetOrderOnAllNetworks' +import { RAW_ORDER } from '../data' +import { Network } from 'types' + +const networkIdSearchListRemaining = [Network.Mainnet, Network.Rinkeby] + +describe('tryGetOrderOnAllNetworks', () => { + test('Should consult other networks when the order is empty', async () => { + const network = Network.Rinkeby + const txHash = '0xTest_txHash' + const defaultParams: GetTxOrdersParams = { networkId: network, txHash } + const mockedApi = jest.fn().mockImplementation(() => Promise.resolve(null)) + + const getOrderApi: GetOrderApi = { + api: mockedApi, + defaultParams, + } + const result = await tryGetOrderOnAllNetworks(network, getOrderApi, networkIdSearchListRemaining) + + expect(mockedApi).toHaveBeenLastCalledWith({ networkId: Network.Mainnet, txHash }) + expect(result).toEqual({ order: null }) + }) + test('Should return and not call other networks when encountered', async () => { + const network = Network.Rinkeby + const txHash = '0xTest_txHash' + const ordersResult = [RAW_ORDER] + const defaultParams: GetTxOrdersParams = { networkId: network, txHash } + const mockedApi = jest.fn().mockImplementation(() => Promise.resolve(ordersResult)) + + const getOrderApi: GetOrderApi = { + api: mockedApi, + defaultParams, + } + const result = await tryGetOrderOnAllNetworks(network, getOrderApi, networkIdSearchListRemaining) + + expect(mockedApi).not.toHaveBeenCalledWith({ networkId: Network.Mainnet, txHash }) + expect(result).toEqual({ order: ordersResult }) + }) +}) diff --git a/test/utils/miscellaneous.spec.ts b/test/utils/miscellaneous.spec.ts index 2e45f418e..a69e99083 100644 --- a/test/utils/miscellaneous.spec.ts +++ b/test/utils/miscellaneous.spec.ts @@ -1,5 +1,5 @@ import { tokenList } from '../data' -import { cleanNetworkName, getToken, isAnAddressAccount, isAnOrderId, isEns } from 'utils' +import { cleanNetworkName, getToken, isAnAddressAccount, isAnOrderId, isEns, isATxHash } from 'utils' import BN from 'bn.js' import { pathAccordingTo } from 'hooks/useSearchSubmit' @@ -157,6 +157,30 @@ describe('isEns', () => { }) }) +describe('isATxHash', () => { + it('should return true for valid Tx Hash', () => { + const text = '0x218ef93e287740cb029f5ca178a9cd21b5fe85a5f9f06795e08ed79b6668a9a0' + + const result = isATxHash(text) + + expect(result).toBe(true) + }) + it('should return true for valid Tx Hash without 0x', () => { + const text = '218ef93e287740cb029f5ca178a9cd21b5fe85a5f9f06795e08ed79b6668a9a0' + + const result = isATxHash(text) + + expect(result).toBe(true) + }) + it('should return false for invalid tx Hash', () => { + const text = 'invalidTxHash' + + const result = isATxHash(text) + + expect(result).toBe(false) + }) +}) + describe('cleanNetworkName', () => { it('should return empty string for undefined input', () => { const text = undefined diff --git a/yarn.lock b/yarn.lock index 5cf66111f..ea26eca70 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1826,7 +1826,7 @@ dependencies: "@fortawesome/fontawesome-common-types" "^0.2.36" -"@fortawesome/free-solid-svg-icons@^5.12.0": +"@fortawesome/free-solid-svg-icons@^5.15.4": version "5.15.4" resolved "https://registry.yarnpkg.com/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-5.15.4.tgz#2a68f3fc3ddda12e52645654142b9e4e8fbb6cc5" integrity sha512-JLmQfz6tdtwxoihXLg6lT78BorrFyCf59SAwBM6qV/0zXyVeDygJVb3fk+j5Qat+Yvcxp1buLTY5iDh1ZSAQ8w== @@ -3982,9 +3982,9 @@ integrity sha512-niAuZrwrjKck4+XhoCw6AAVQBENHftpXw9F4ryk66fTgYaKQ53R4FI7c9vUGGw5vQis1HKBHDR1gcYI/Bq1xvw== "@types/node@^14.0.10", "@types/node@^14.0.14": - version "14.17.34" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.17.34.tgz#fe4b38b3f07617c0fa31ae923fca9249641038f0" - integrity sha512-USUftMYpmuMzeWobskoPfzDi+vkpe0dvcOBRNOscFrGxVp4jomnRxWuVohgqBow2xyIPC0S3gjxV/5079jhmDg== + version "14.18.1" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.1.tgz#459886b51f52aa923dc06b9ea81cb8b1d733e9d3" + integrity sha512-fTFWOFrgAkj737w1o0HLTIgisgYHnsZfeiqhG1Ltrf/iJjudEbUwetQAsfrtVE49JGwvpEzQR+EbMkIqG4227g== "@types/normalize-package-data@^2.4.0": version "2.4.1" @@ -4183,9 +4183,9 @@ integrity sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw== "@types/styled-components@*", "@types/styled-components@^5.0.1": - version "5.1.15" - resolved "https://registry.yarnpkg.com/@types/styled-components/-/styled-components-5.1.15.tgz#30855b40aa80b3b4e4c0e43a4af366e7c246d148" - integrity sha512-4evch8BRI3AKgb0GAZ/sn+mSeB+Dq7meYtMi7J/0Mg98Dt1+r8fySOek7Sjw1W+Wskyjc93565o5xWAT/FdY0Q== + version "5.1.19" + resolved "https://registry.yarnpkg.com/@types/styled-components/-/styled-components-5.1.19.tgz#d76f431ee49d0a222ab4e758dcd540b01987652d" + integrity sha512-hNj14Oamk7Jhb/fMMQG7TUkd3e8uMMgxsCTH+ueJNGdFo/PuhlGDQTPChqyilpZP0WttgBHkc2YyT5UG+yc6Yw== dependencies: "@types/hoist-non-react-statics" "*" "@types/react" "*" @@ -9990,9 +9990,9 @@ flush-write-stream@^1.0.0: readable-stream "^2.3.6" follow-redirects@^1.0.0, follow-redirects@^1.14.0: - version "1.14.5" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.5.tgz#f09a5848981d3c772b5392309778523f8d85c381" - integrity sha512-wtphSXy7d4/OR+MvIFbCVBDzZ5520qV8XfPklSN5QtxuMUJZ+b0Wnst1e1lCDocfzuCkHqj8k0FpZqO+UIaKNA== + version "1.14.7" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.7.tgz#2004c02eb9436eee9a21446a6477debf17e81685" + integrity sha512-+hbxoLbFMbRKDwohX8GkTataGqO6Jb7jGwpAlwgy2bIz25XtRm7KEzJM76R1WiNT5SwZkX4Y75SwBolkpmE7iQ== fontkit@^1.8.1: version "1.8.1" @@ -15489,10 +15489,10 @@ playwright-cli@^0.152.0: highlight.js "^10.1.2" playwright "=1.5.2" -playwright-core@=1.16.3: - version "1.16.3" - resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.16.3.tgz#f466be9acaffb698654adfb0a17a4906ba936895" - integrity sha512-16hF27IvQheJee+DbhC941AUZLjbJgfZFWi9YPS4LKEk/lKFhZI+9TiFD0sboYqb9eaEWvul47uR5xxTVbE4iw== +playwright-core@=1.17.2: + version "1.17.2" + resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.17.2.tgz#916254fa8fb3eb76c160b5c2e06bc979d6ec2cf8" + integrity sha512-TCYIt2UNHvqGxvD79bBjBv9osDLAH1gn7AZD5kRpMNQJG6BAmJt8B4Ek8fzdKmCQOnHf9ASJmcYRszoIZxcdVA== dependencies: commander "^8.2.0" debug "^4.1.1" @@ -15529,11 +15529,11 @@ playwright@=1.5.2: ws "^7.3.1" playwright@^1.5.2: - version "1.16.3" - resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.16.3.tgz#27a292d9fa54fbac923998d3af58cd2b691f5ebe" - integrity sha512-nfJx/OpIb/8OexL3rYGxNN687hGyaM3XNpfuMzoPlrekURItyuiHHsNhC9oQCx3JDmCn5O3EyyyFCnrZjH6MpA== + version "1.17.2" + resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.17.2.tgz#918b9a7e43ac8640fa3e2162ce0cb8b395c55fb7" + integrity sha512-u1HZmVoeLCLptNcpuOyp5KfBzsdsLxE9CReK82i/p8j5i7EPqtY3fX77SMHqDGeO7tLBSYk2a6eFDVlQfSSANg== dependencies: - playwright-core "=1.16.3" + playwright-core "=1.17.2" please-upgrade-node@^3.2.0: version "3.2.0"