diff --git a/src/api/operator/operatorApi.ts b/src/api/operator/operatorApi.ts index 44acf5aa0..d902b8141 100644 --- a/src/api/operator/operatorApi.ts +++ b/src/api/operator/operatorApi.ts @@ -193,16 +193,24 @@ export async function getOrder(params: GetOrderParams): Promise * - owner: address * - sellToken: address * - buyToken: address + * - minValidTo: number */ export async function getOrders(params: GetOrdersParams): Promise { const { networkId, ...searchParams } = params - const { owner, sellToken, buyToken } = searchParams + const { owner, sellToken, buyToken, minValidTo } = searchParams + const defaultValues = { + includeFullyExecuted: 'true', + includeInvalidated: 'true', + includeInsufficientBalance: 'true', + includePresignaturePending: 'true', + includeUnsupportedTokens: 'true', + } console.log( `[getOrders] Fetching orders on network ${networkId} with filters: owner=${owner} sellToken=${sellToken} buyToken=${buyToken}`, ) - const searchString = buildSearchString({ ...searchParams }) + const searchString = buildSearchString({ ...searchParams, ...defaultValues, minValidTo: String(minValidTo) }) const queryString = '/orders/' + searchString diff --git a/src/api/operator/types.ts b/src/api/operator/types.ts index eb7ca6914..b72fbb359 100644 --- a/src/api/operator/types.ts +++ b/src/api/operator/types.ts @@ -119,7 +119,8 @@ export type GetOrderParams = WithNetworkId & { } export type GetOrdersParams = WithNetworkId & { - owner?: string + owner: string + minValidTo: number sellToken?: string buyToken?: string } diff --git a/src/apps/explorer/components/OrdersTableWidget/OrdersTableWithData.tsx b/src/apps/explorer/components/OrdersTableWidget/OrdersTableWithData.tsx new file mode 100644 index 000000000..9821994ea --- /dev/null +++ b/src/apps/explorer/components/OrdersTableWidget/OrdersTableWithData.tsx @@ -0,0 +1,19 @@ +import React, { useContext } from 'react' +import { faSpinner } from '@fortawesome/free-solid-svg-icons' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' + +import OrdersTable from 'components/orders/OrdersUserDetailsTable' +import { EmptyItemWrapper } from 'components/common/StyledUserDetailsTable' +import { OrdersTableContext, OrdersTableState } from './context/OrdersTableContext' + +export const OrdersTableWithData: React.FC = () => { + const { orders, kind } = useContext(OrdersTableContext) + + return kind === OrdersTableState.Loading ? ( + + + + ) : ( + + ) +} diff --git a/src/apps/explorer/components/OrdersTableWidget/context/OrdersTableContext.tsx b/src/apps/explorer/components/OrdersTableWidget/context/OrdersTableContext.tsx new file mode 100644 index 000000000..ab2452f8b --- /dev/null +++ b/src/apps/explorer/components/OrdersTableWidget/context/OrdersTableContext.tsx @@ -0,0 +1,17 @@ +import React from 'react' + +import { Order } from 'api/operator' + +export enum OrdersTableState { + Loading, + Loaded, + Error, +} + +interface CommonState { + orders: Order[] + error: string + kind: OrdersTableState +} + +export const OrdersTableContext = React.createContext({} as CommonState) diff --git a/src/apps/explorer/components/OrdersTableWidget/index.tsx b/src/apps/explorer/components/OrdersTableWidget/index.tsx new file mode 100644 index 000000000..cd06583ae --- /dev/null +++ b/src/apps/explorer/components/OrdersTableWidget/index.tsx @@ -0,0 +1,85 @@ +import React, { useMemo } from 'react' +import styled from 'styled-components' +import { useParams } from 'react-router' +import { faSpinner } from '@fortawesome/free-solid-svg-icons' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' + +import { media } from 'theme/styles/media' + +import ExplorerTabs from 'apps/explorer/components/common/ExplorerTabs/ExplorerTab' +import { OrdersTableWithData } from './OrdersTableWithData' +import { OrdersTableContext, OrdersTableState } from './context/OrdersTableContext' +import { useGetOrders } from './useGetOrders' +import { TabItemInterface } from 'components/common/Tabs/Tabs' + +const Wrapper = styled.div` + padding: 1.6rem; + margin: 0 auto; + width: 100%; + max-width: 140rem; + + ${media.mediumDown} { + max-width: 94rem; + } + + ${media.mobile} { + max-width: 100%; + } +` + +const StyledTabLoader = styled.span` + padding-left: 4px; +` + +const tabItems = (ordersTableState: OrdersTableState): TabItemInterface[] => { + return [ + { + id: 1, + tab: ( + <> + Orders + + {ordersTableState === OrdersTableState.Loading && } + + + ), + content: , + }, + { + id: 2, + tab: 'Trades', + content: ( + <> +

Trades Content

+ + ), + }, + ] +} + +function useAddressParam(): string { + const { address } = useParams<{ address: string }>() + + return address +} + +const OrdersTableWidget: React.FC = () => { + const ownerAddress = useAddressParam() + const { orders, isLoading, error } = useGetOrders(ownerAddress) + const ordersTableState = useMemo(() => { + if (isLoading && orders.length === 0) return OrdersTableState.Loading + else if (error) return OrdersTableState.Error + + return OrdersTableState.Loaded + }, [isLoading, orders.length, error]) + + return ( + + + + + + ) +} + +export default OrdersTableWidget diff --git a/src/apps/explorer/components/OrdersTableWidget/useGetOrders.tsx b/src/apps/explorer/components/OrdersTableWidget/useGetOrders.tsx new file mode 100644 index 000000000..96a80763c --- /dev/null +++ b/src/apps/explorer/components/OrdersTableWidget/useGetOrders.tsx @@ -0,0 +1,128 @@ +import { useState, useCallback, useEffect } from 'react' +import { subMinutes, getTime } from 'date-fns' + +import { Network } from 'types' +import { useMultipleErc20 } from 'hooks/useErc20' +import { getOrders, Order, RawOrder } from 'api/operator' +import { useNetworkId } from 'state/network' +import { transformOrder } from 'utils' +import { ORDERS_HISTORY_MINUTES_AGO, ORDERS_QUERY_INTERVAL } from 'apps/explorer/const' + +/** + * + * Merge new RawOrders consulted, that may have changed status + * + * @param previousOrders List of orders + * @param newOrdersFetched List of fetched block order that could have changed + */ +export function mergeNewOrders(previousOrders: Order[], newOrdersFetched: RawOrder[]): Order[] { + if (newOrdersFetched.length === 0) return previousOrders + + // find the order up to which it is to be replaced + const lastOrder = newOrdersFetched[newOrdersFetched.length - 1] + const positionLastOrder = previousOrders.map((o) => o.uid).indexOf(lastOrder.uid) + if (positionLastOrder === -1) { + return newOrdersFetched.map((order) => transformOrder(order)).concat(previousOrders) + } + + const slicedOrders: Order[] = previousOrders.slice(positionLastOrder + 1) + return newOrdersFetched.map((order) => transformOrder(order)).concat(slicedOrders) +} + +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[] + error: string + isLoading: boolean +} + +export function useGetOrders(ownerAddress: string): 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 fetchOrders = useCallback( + async (network: Network, owner: string, minTimeHistoryTimeStamp = 0): Promise => { + setIsLoading(true) + setError('') + + try { + const ordersFetched = await getOrders({ networkId: network, owner, minValidTo: minTimeHistoryTimeStamp }) + 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) + // For the moment it is neccesary to sort by date + ordersFetched.sort((a, b) => +new Date(b.creationDate) - +new Date(a.creationDate)) + + setOrders((previousOrders) => mergeNewOrders(previousOrders, ordersFetched)) + setMountNewOrders(true) + } catch (e) { + const msg = `Failed to fetch orders` + console.error(msg, e) + setError(msg) + } finally { + setIsLoading(false) + } + }, + [], + ) + + useEffect(() => { + if (!networkId) { + return + } + const getOrUpdateOrders = (minHistoryTime?: number): Promise => + fetchOrders(networkId, ownerAddress, minHistoryTime) + + getOrUpdateOrders() + + const intervalId: NodeJS.Timeout = setInterval(() => { + const minutesAgoTimestamp = getTime(subMinutes(new Date(), ORDERS_HISTORY_MINUTES_AGO)) + getOrUpdateOrders(Math.floor(minutesAgoTimestamp / 1000)) + }, ORDERS_QUERY_INTERVAL) + + return (): void => { + clearInterval(intervalId) + } + }, [fetchOrders, networkId, ownerAddress]) + + useEffect(() => { + if (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) + }, [valueErc20s, networkId, areErc20Loading, mountNewOrders, orders]) + + return { orders, error, isLoading } +} diff --git a/src/apps/explorer/const.ts b/src/apps/explorer/const.ts index b173428bf..c53b75dfc 100644 --- a/src/apps/explorer/const.ts +++ b/src/apps/explorer/const.ts @@ -2,6 +2,8 @@ import { AnalyticsDimension } from 'types' /** Explorer app constants */ export const ORDER_QUERY_INTERVAL = 10000 // in ms +export const ORDERS_QUERY_INTERVAL = 10000 // in ms +export const ORDERS_HISTORY_MINUTES_AGO = 10 // in minutes export const DISPLAY_TEXT_COPIED_CHECK = 1000 // in ms diff --git a/src/apps/explorer/pages/UserDetails.tsx b/src/apps/explorer/pages/UserDetails.tsx index 3372574ce..88dab84ae 100644 --- a/src/apps/explorer/pages/UserDetails.tsx +++ b/src/apps/explorer/pages/UserDetails.tsx @@ -1,5 +1,7 @@ import React from 'react' -const UserDetails: React.FC = () =>

Placeholder Page

+import OrdersTableWidget from '../components/OrdersTableWidget' + +const UserDetails: React.FC = () => export default UserDetails diff --git a/src/components/common/StyledUserDetailsTable.tsx b/src/components/common/StyledUserDetailsTable.tsx index 7b6d51688..188f53a8f 100644 --- a/src/components/common/StyledUserDetailsTable.tsx +++ b/src/components/common/StyledUserDetailsTable.tsx @@ -2,7 +2,7 @@ import styled from 'styled-components' import { SimpleTable, Props as SimpleTableProps } from 'components/common/SimpleTable' -interface Props { +export interface Props { showBorderTable?: boolean } @@ -11,6 +11,7 @@ export type StyledUserDetailsTableProps = SimpleTableProps & Props const StyledUserDetailsTable = styled(SimpleTable)` border: ${({ theme, showBorderTable }): string => (showBorderTable ? `0.1rem solid ${theme.borderPrimary}` : 'none')}; border-radius: 0.4rem; + margin-top: 0; tr td { &:not(:first-of-type) { @@ -38,6 +39,9 @@ const StyledUserDetailsTable = styled(SimpleTable)` gap: 6px; } + thead { + position: inherit; + } thead tr { width: 100%; } @@ -53,6 +57,10 @@ const StyledUserDetailsTable = styled(SimpleTable)` span.wrap-datedisplay > span:last-of-type { display: flex; } + + tbody tr td.row-td-empty { + grid-column: 1 / span all; + } ` export const EmptyItemWrapper = styled.div` @@ -62,6 +70,8 @@ export const EmptyItemWrapper = styled.div` align-items: center; justify-content: center; display: flex; + width: 100%; + font-size: ${({ theme }): string => theme.fontSizeDefault}; ` export default StyledUserDetailsTable diff --git a/src/components/orders/OrdersUserDetailsTable/index.tsx b/src/components/orders/OrdersUserDetailsTable/index.tsx index 3da0174cd..4c80227a8 100644 --- a/src/components/orders/OrdersUserDetailsTable/index.tsx +++ b/src/components/orders/OrdersUserDetailsTable/index.tsx @@ -11,7 +11,7 @@ import { getOrderLimitPrice, formatCalculatedPriceToDisplay, formattedAmount } f import { StatusLabel } from '../StatusLabel' import { HelpTooltip } from 'components/Tooltip' import StyledUserDetailsTable, { - StyledUserDetailsTableProps, + Props as StyledUserDetailsTableProps, EmptyItemWrapper, } from '../../common/StyledUserDetailsTable' import Icon from 'components/Icon' @@ -23,7 +23,6 @@ const Wrapper = styled(StyledUserDetailsTable)` grid-template-columns: 12rem 7rem repeat(2, 16rem) repeat(2, minmax(18rem, 24rem)) 1fr; } ` - function getLimitPrice(order: Order, isPriceInverted: boolean): string { if (!order.buyToken || !order.sellToken) return '-' @@ -59,9 +58,13 @@ const RowOrder: React.FC = ({ order, isPriceInverted }) => { { {shortId}} + contentsToDisplay={ + + {shortId} + + } /> } @@ -94,7 +97,14 @@ const OrdersUserDetailsTable: React.FC = (props) => { } const orderItems = (items: Order[]): JSX.Element => { - if (items.length === 0) return No Orders. + if (items.length === 0) + return ( + + + No Orders. + + + ) return ( <> diff --git a/src/hooks/useOperatorOrder.ts b/src/hooks/useOperatorOrder.ts index 8991f7a0b..5f671f2e3 100644 --- a/src/hooks/useOperatorOrder.ts +++ b/src/hooks/useOperatorOrder.ts @@ -60,7 +60,7 @@ async function _getOrder( } } -export function useOrderByNetwork(orderId: string, updateInterval = 0, networkId: Network | null): UseOrderResult { +export function useOrderByNetwork(orderId: string, networkId: Network | null, updateInterval = 0): UseOrderResult { const [isLoading, setIsLoading] = useState(false) const [error, setError] = useState('') const [order, setOrder] = useState(null) @@ -121,7 +121,7 @@ export function useOrderByNetwork(orderId: string, updateInterval = 0, networkId export function useOrder(orderId: string, updateInterval?: number): UseOrderResult { const networkId = useNetworkId() - return useOrderByNetwork(orderId, updateInterval, networkId) + return useOrderByNetwork(orderId, networkId, updateInterval) } type UseOrderAndErc20sResult = { diff --git a/src/utils/operator.ts b/src/utils/operator.ts index 226bf274b..c12fa4218 100644 --- a/src/utils/operator.ts +++ b/src/utils/operator.ts @@ -238,7 +238,7 @@ function isZeroAddress(address: string): boolean { } export function isTokenErc20(token: TokenErc20 | null | undefined): token is TokenErc20 { - return (token as TokenErc20).address !== undefined + return (token as TokenErc20)?.address !== undefined } export function formattedAmount(erc20: TokenErc20 | null | undefined, amount: BigNumber): string {