diff --git a/apps/explorer/src/components/orders/DetailsTable/index.tsx b/apps/explorer/src/components/orders/DetailsTable/index.tsx index 08756c8a4c..a52aaa8af1 100644 --- a/apps/explorer/src/components/orders/DetailsTable/index.tsx +++ b/apps/explorer/src/components/orders/DetailsTable/index.tsx @@ -3,7 +3,7 @@ import React from 'react' import { ExplorerDataType, getExplorerLink } from '@cowprotocol/common-utils' import { SupportedChainId } from '@cowprotocol/cow-sdk' import { Command } from '@cowprotocol/types' -import { Media } from '@cowprotocol/ui' +import { Icon, Media, UI } from '@cowprotocol/ui' import { TruncatedText } from '@cowprotocol/ui/pure/TruncatedText' import { faFill, faGroupArrowsRotate, faHistory, faProjectDiagram } from '@fortawesome/free-solid-svg-icons' @@ -31,6 +31,7 @@ import { Order } from 'api/operator' import { getUiOrderType } from 'utils/getUiOrderType' import { OrderHooksDetails } from '../OrderHooksDetails' +import { UnsignedOrderWarning } from '../UnsignedOrderWarning' const tooltip = { orderID: 'A unique identifier ID for this order.', @@ -87,6 +88,8 @@ const tooltip = { } export const Wrapper = styled.div` + --cow-color-alert: ${({ theme }): string => theme.alert2}; + display: flex; flex-direction: row; @@ -126,6 +129,10 @@ export const LinkButton = styled(LinkWithPrefixNetwork)` } ` +const WarningRow = styled.tr` + background-color: ${({ theme }): string => theme.background}; +` + export type Props = { chainId: SupportedChainId order: Order @@ -167,12 +174,20 @@ export function DetailsTable(props: Props): React.ReactNode | null { } const onCopy = (label: string): void => clickOnOrderDetails('Copy', label) + const isSigning = status === 'signing' return ( + {isSigning && ( + + + + + + )} @@ -195,6 +210,12 @@ export function DetailsTable(props: Props): React.ReactNode | null { + {isSigning && ( + <> + +   + + )} onCopy('ownerAddress')} diff --git a/apps/explorer/src/components/orders/OrdersUserDetailsTable/ToggleFilter.tsx b/apps/explorer/src/components/orders/OrdersUserDetailsTable/ToggleFilter.tsx new file mode 100644 index 0000000000..64c0691ead --- /dev/null +++ b/apps/explorer/src/components/orders/OrdersUserDetailsTable/ToggleFilter.tsx @@ -0,0 +1,38 @@ +import React from 'react' + +import styled from 'styled-components/macro' + +interface BadgeProps { + checked: boolean + onChange: () => void + label: string + count: number +} + +const Wrapper = styled.div<{ checked: boolean }>` + display: inline-block; + padding: 5px 10px; + border-radius: 20px; + background-color: ${({ checked }) => (checked ? '#007bff' : '#e0e0e0')}; + color: ${({ checked }) => (checked ? '#fff' : '#000')}; + cursor: pointer; + user-select: none; + font-size: 11px; +` + +const Label = styled.span` + margin-right: 10px; +` + +const Count = styled.span` + font-weight: bold; +` + +export const ToggleFilter: React.FC = ({ checked, onChange, label, count }) => { + return ( + + + {count} + + ) +} diff --git a/apps/explorer/src/components/orders/OrdersUserDetailsTable/index.tsx b/apps/explorer/src/components/orders/OrdersUserDetailsTable/index.tsx index 4677fa576f..935442b1c4 100644 --- a/apps/explorer/src/components/orders/OrdersUserDetailsTable/index.tsx +++ b/apps/explorer/src/components/orders/OrdersUserDetailsTable/index.tsx @@ -17,13 +17,26 @@ import { useNetworkId } from 'state/network' import styled from 'styled-components/macro' import { FormatAmountPrecision, formattedAmount } from 'utils' -import { Order } from 'api/operator' +import { Order, OrderStatus } from 'api/operator' import { getLimitPrice } from 'utils/getLimitPrice' import { OrderSurplusDisplayStyledByRow } from './OrderSurplusTooltipStyledByRow' +import { ToggleFilter } from './ToggleFilter' import { SimpleTable, SimpleTableProps } from '../../common/SimpleTable' import { StatusLabel } from '../StatusLabel' +import { UnsignedOrderWarning } from '../UnsignedOrderWarning' + +const EXPIRED_CANCELED_STATES: OrderStatus[] = ['cancelled', 'cancelling', 'expired'] + +function isExpiredOrCanceled(order: Order): boolean { + const { executedSellAmount, executedBuyAmount, status } = order + // We don't consider an order expired or canceled if it was partially or fully filled + if (!executedSellAmount.isZero() || !executedBuyAmount.isZero()) return false + + // Otherwise, return if the order is expired or canceled + return EXPIRED_CANCELED_STATES.includes(order.status) +} const tooltip = { orderID: 'A unique identifier ID for this order.', @@ -46,9 +59,35 @@ export type Props = SimpleTableProps & { interface RowProps { order: Order isPriceInverted: boolean + + // TODO: Filter by state using the API. Not available for now, so filtering in the client + showCanceledAndExpired: boolean + showPreSigning: boolean } -const RowOrder: React.FC = ({ order, isPriceInverted }) => { +const FilterRow = styled.tr` + background-color: ${({ theme }) => theme.background}; + + th { + text-align: right; + padding-right: 10px; + + & > * { + margin-left: 10px; + } + } +` + +const NoOrdersContainer = styled.div` + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: center; + flex-direction: column; + padding: 2rem; +` + +const RowOrder: React.FC = ({ order, isPriceInverted, showCanceledAndExpired, showPreSigning }) => { const { creationDate, buyToken, buyAmount, sellToken, sellAmount, kind, partiallyFilled, uid, filledPercentage } = order const [_isPriceInverted, setIsPriceInverted] = useState(isPriceInverted) @@ -67,6 +106,10 @@ const RowOrder: React.FC = ({ order, isPriceInverted }) => { if (textValue === '-') return } + // Hide the row if the order is canceled, expired or pre-signing + if (!showCanceledAndExpired && isExpiredOrCanceled(order)) return null + if (!showPreSigning && order.status === 'signing') return null + return ( @@ -118,6 +161,14 @@ const RowOrder: React.FC = ({ order, isPriceInverted }) => { const OrdersUserDetailsTable: React.FC = (props) => { const { orders, messageWhenEmpty } = props const [isPriceInverted, setIsPriceInverted] = useState(false) + const [showCanceledAndExpired, setShowCanceledAndExpired] = useState(false) + const [showPreSigning, setShowPreSigning] = useState(false) + + const canceledAndExpiredCount = orders?.filter(isExpiredOrCanceled).length || 0 + const preSigningCount = orders?.filter((order) => order.status === 'signing').length || 0 + const showFilter = canceledAndExpiredCount > 0 || preSigningCount > 0 + const allOrdersAreHidden = + orders?.length === (showPreSigning ? 0 : preSigningCount) + (showCanceledAndExpired ? 0 : canceledAndExpiredCount) const invertLimitPrice = (): void => { setIsPriceInverted((previousValue) => !previousValue) @@ -130,30 +181,72 @@ const OrdersUserDetailsTable: React.FC = (props) => { return ( - - - Order ID - - - Type - Sell amount - Buy amount - - - Limit price - - - Surplus - Created - Status - + <> + {showFilter && ( + + + {canceledAndExpiredCount > 0 && ( + setShowCanceledAndExpired((previousValue) => !previousValue)} + label={(showCanceledAndExpired ? 'Hide' : 'Show') + ' canceled/expired'} + count={canceledAndExpiredCount} + /> + )} + {preSigningCount > 0 && ( + <> + setShowPreSigning((previousValue) => !previousValue)} + label={(showPreSigning ? 'Hide' : 'Show') + ' unsigned'} + count={preSigningCount} + /> + {showPreSigning && } + + )} + + + )} + {!allOrdersAreHidden && ( + + + + Order ID + + + Type + Sell amount + Buy amount + + + Limit price + + + Surplus + Created + Status + + )} + } body={ <> - {orders.map((item) => ( - - ))} + {!allOrdersAreHidden ? ( + orders.map((item) => ( + + )) + ) : ( + +

No orders found.

+

You can toggle the filters to show the {orders.length} hidden orders.

+
+ )} } /> diff --git a/apps/explorer/src/components/orders/UnsignedOrderWarning/index.tsx b/apps/explorer/src/components/orders/UnsignedOrderWarning/index.tsx new file mode 100644 index 0000000000..06e2e5a566 --- /dev/null +++ b/apps/explorer/src/components/orders/UnsignedOrderWarning/index.tsx @@ -0,0 +1,15 @@ +import { BannerOrientation, InlineBanner } from '@cowprotocol/ui' + +import styled from 'styled-components/macro' + +const StyledInlineBanner = styled(InlineBanner)` + --cow-color-danger-text: ${({ theme }): string => theme.alert2}; +` + +export const UnsignedOrderWarning: React.FC = () => { + return ( + + An unsigned order is not necessarily placed by the owner's account. Please be cautious. + + ) +}