Skip to content

Commit

Permalink
feat: warn about unsigned orders, and hide not relevant orders (#5214)
Browse files Browse the repository at this point in the history
* feat: create a warning banner for unsigned orders

* feat: add a toggle filter

* feat: filter in the client side for canceled/expired orders

* feat: show warning

* feat: style the no orders container

* fix: fix lint

* chore: delete debug line

* fix: show partially fillable orders

* chore: simplify

Co-authored-by: Leandro <[email protected]>

---------

Co-authored-by: Leandro <[email protected]>
  • Loading branch information
anxolin and alfetopito authored Dec 17, 2024
1 parent 83667f9 commit 0d19616
Show file tree
Hide file tree
Showing 4 changed files with 191 additions and 24 deletions.
23 changes: 22 additions & 1 deletion apps/explorer/src/components/orders/DetailsTable/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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.',
Expand Down Expand Up @@ -87,6 +88,8 @@ const tooltip = {
}

export const Wrapper = styled.div`
--cow-color-alert: ${({ theme }): string => theme.alert2};
display: flex;
flex-direction: row;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 (
<SimpleTable
columnViewMobile
body={
<>
{isSigning && (
<WarningRow>
<td colSpan={2}>
<UnsignedOrderWarning />
</td>
</WarningRow>
)}
<tr>
<td>
<span>
Expand All @@ -195,6 +210,12 @@ export function DetailsTable(props: Props): React.ReactNode | null {
</td>
<td>
<Wrapper>
{isSigning && (
<>
<Icon image="ALERT" color={UI.COLOR_ALERT} />
&nbsp;
</>
)}
<RowWithCopyButton
textToCopy={owner}
onCopy={(): void => onCopy('ownerAddress')}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<BadgeProps> = ({ checked, onChange, label, count }) => {
return (
<Wrapper checked={checked} onClick={onChange}>
<Label>{label}</Label>
<Count>{count}</Count>
</Wrapper>
)
}
139 changes: 116 additions & 23 deletions apps/explorer/src/components/orders/OrdersUserDetailsTable/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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.',
Expand All @@ -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<RowProps> = ({ 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<RowProps> = ({ order, isPriceInverted, showCanceledAndExpired, showPreSigning }) => {
const { creationDate, buyToken, buyAmount, sellToken, sellAmount, kind, partiallyFilled, uid, filledPercentage } =
order
const [_isPriceInverted, setIsPriceInverted] = useState(isPriceInverted)
Expand All @@ -67,6 +106,10 @@ const RowOrder: React.FC<RowProps> = ({ order, isPriceInverted }) => {
if (textValue === '-') return <Spinner spin size="1x" />
}

// 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 (
<tr key={uid}>
<td>
Expand Down Expand Up @@ -118,6 +161,14 @@ const RowOrder: React.FC<RowProps> = ({ order, isPriceInverted }) => {
const OrdersUserDetailsTable: React.FC<Props> = (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)
Expand All @@ -130,30 +181,72 @@ const OrdersUserDetailsTable: React.FC<Props> = (props) => {
return (
<SimpleTable
header={
<tr>
<th>
<span>
Order ID <HelpTooltip tooltip={tooltip.orderID} />
</span>
</th>
<th>Type</th>
<th>Sell amount</th>
<th>Buy amount</th>
<th>
<span>
Limit price <Icon icon={faExchangeAlt} onClick={invertLimitPrice} />
</span>
</th>
<th>Surplus</th>
<th>Created</th>
<th>Status</th>
</tr>
<>
{showFilter && (
<FilterRow>
<th colSpan={8}>
{canceledAndExpiredCount > 0 && (
<ToggleFilter
checked={showCanceledAndExpired}
onChange={() => setShowCanceledAndExpired((previousValue) => !previousValue)}
label={(showCanceledAndExpired ? 'Hide' : 'Show') + ' canceled/expired'}
count={canceledAndExpiredCount}
/>
)}
{preSigningCount > 0 && (
<>
<ToggleFilter
checked={showPreSigning}
onChange={() => setShowPreSigning((previousValue) => !previousValue)}
label={(showPreSigning ? 'Hide' : 'Show') + ' unsigned'}
count={preSigningCount}
/>
{showPreSigning && <UnsignedOrderWarning />}
</>
)}
</th>
</FilterRow>
)}
{!allOrdersAreHidden && (
<tr>
<th>
<span>
Order ID <HelpTooltip tooltip={tooltip.orderID} />
</span>
</th>
<th>Type</th>
<th>Sell amount</th>
<th>Buy amount</th>
<th>
<span>
Limit price <Icon icon={faExchangeAlt} onClick={invertLimitPrice} />
</span>
</th>
<th>Surplus</th>
<th>Created</th>
<th>Status</th>
</tr>
)}
</>
}
body={
<>
{orders.map((item) => (
<RowOrder key={item.uid} order={item} isPriceInverted={isPriceInverted} />
))}
{!allOrdersAreHidden ? (
orders.map((item) => (
<RowOrder
key={item.uid}
order={item}
isPriceInverted={isPriceInverted}
showCanceledAndExpired={showCanceledAndExpired}
showPreSigning={showPreSigning}
/>
))
) : (
<NoOrdersContainer>
<p>No orders found.</p>
<p>You can toggle the filters to show the {orders.length} hidden orders.</p>
</NoOrdersContainer>
)}
</>
}
/>
Expand Down
15 changes: 15 additions & 0 deletions apps/explorer/src/components/orders/UnsignedOrderWarning/index.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<StyledInlineBanner orientation={BannerOrientation.Horizontal} bannerType="danger" padding="0">
An unsigned order is not necessarily placed by the owner's account. Please be cautious.
</StyledInlineBanner>
)
}

0 comments on commit 0d19616

Please sign in to comment.