Improve click targets on property actions (#45936)

## Context

When opening the logs detail panel, some of the fields can be clicked to
add them as a filter. However the existing UX is that the clickable part
is just the text which makes it target small
<img width="233" height="127" alt="image"
src="https://github.com/user-attachments/assets/1d876bcc-05cf-464c-bdbe-907229be0586"
/>

Am opting the following:
- Make the whole row clickable
- Make all rows clickable with the main action being "Copy {column}"
- Only filterable columns will have the option to "Add as filter"

### After
<img width="483" height="153" alt="image"
src="https://github.com/user-attachments/assets/9d6e5479-fdbb-4609-839c-2bb7ad571b57"
/>
<img width="473" height="152" alt="image"
src="https://github.com/user-attachments/assets/f22197df-fa59-4e01-be00-2557260374f8"
/>


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Row actions now consistently wrap rows and show a filter icon next to
the label when a resolved filter is available; copy menu displays "Copy
{label}".

* **Style**
* Standardized icon sizes and adjusted dropdown/row spacing; simplified
text wrap/truncate behavior for field values; minor status text color
refinement.

* **Bug Fixes**
* Dropdown row-action rendering made more robust to ensure menu wrappers
render reliably.

<!-- review_stack_entry_start -->

[![Review Change
Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/supabase/supabase/pull/45936)

<!-- review_stack_entry_end -->
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Joshen Lim
2026-05-14 22:31:40 +08:00
committed by GitHub
parent ec21e68eee
commit ebfec0df36
6 changed files with 48 additions and 69 deletions

View File

@@ -1,4 +1,5 @@
import { Table } from '@tanstack/react-table'
import { Filter } from 'lucide-react'
import { ReactNode } from 'react'
import { cn, Skeleton } from 'ui'
@@ -11,7 +12,7 @@ interface DetailRowProps {
filterId?: string
filterValue?: string | number
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- matches ServiceFlow types convention
filterFields?: DataTableFilterField<any>[]
filterFields: DataTableFilterField<any>[]
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- matches ServiceFlow types convention
table?: Table<any>
isLoading?: boolean
@@ -61,58 +62,31 @@ export const DetailRow = ({
<Skeleton className="h-4 w-24" />
) : isEmpty ? (
<span className="font-mono text-xs text-foreground-muted"></span>
) : typeof value === 'string' || typeof value === 'number' ? (
<span
className={cn(
'font-mono text-xs text-foreground',
wrap ? 'break-all text-right max-w-[calc(100%-12rem)]' : 'truncate text-right',
isFilterable && 'group-hover:underline'
)}
>
{value}
</span>
) : (
value
)
const rowClass = cn(
'flex items-start justify-between gap-3 px-4',
'flex items-start justify-between gap-x-10 px-4',
wrap ? 'min-h-9 py-2' : 'h-9 items-center'
)
const isStringValue = typeof value === 'string' || typeof value === 'number'
if (isFilterable && resolvedFilterValue !== undefined && isStringValue) {
return (
<DataTableSheetRowAction
fieldValue={filterId!}
filterFields={filterFields!}
value={resolvedFilterValue}
table={table!}
className={cn(rowClass, 'group w-full cursor-pointer hover:bg-surface-200/50')}
>
{labelEl}
{valueEl}
</DataTableSheetRowAction>
)
}
return (
<div className={rowClass}>
{labelEl}
{isFilterable && resolvedFilterValue !== undefined && !isStringValue ? (
<DataTableSheetRowAction
fieldValue={filterId!}
filterFields={filterFields!}
value={resolvedFilterValue}
table={table!}
className="group"
>
<span className="[&_div]:group-hover:text-foreground">{valueEl}</span>
</DataTableSheetRowAction>
) : (
valueEl
)}
</div>
<DataTableSheetRowAction
fieldValue={filterId}
filterFields={filterFields}
value={resolvedFilterValue ?? ''}
table={table!}
label={label}
className={cn(rowClass, 'rounded-none group w-full cursor-pointer hover:bg-surface-100!')}
>
<div className="flex items-center gap-x-2">
{labelEl}
{isFilterable && resolvedFilterValue !== undefined && (
<Filter size={12} className="text-foreground-lighter" />
)}
</div>
{valueEl}
</DataTableSheetRowAction>
)
}

View File

@@ -29,8 +29,7 @@ export const FieldValue = ({ config, value, wrap }: FieldValueProps): ReactNode
<span
className={cn(
'font-mono text-xs text-foreground',
wrap ? 'break-all text-right max-w-[calc(100%-12rem)]' : 'truncate text-right',
'group-hover:underline'
wrap ? 'break-all text-right' : 'truncate text-right'
)}
>
{value}

View File

@@ -85,6 +85,7 @@ export function DataTableSheetContent<TData, TMeta>({
'flex gap-4 my-1 py-1 text-sm justify-between items-center w-full',
field.className
)}
label={field.label}
>
<dt className="shrink-0 text-muted-foreground">{field.label}</dt>
<dd className="font-mono w-full text-right truncate">

View File

@@ -99,7 +99,7 @@ export function getStatusColor(value?: number | string): Record<'text' | 'bg' |
case '2':
case 'success':
return {
text: 'text-foreground-lighter',
text: 'text-foreground',
bg: '',
border: 'border-green-200 dark:border-green-800',
}
@@ -120,7 +120,7 @@ export function getStatusColor(value?: number | string): Record<'text' | 'bg' |
}
default:
return {
text: 'text-foreground-lighter',
text: 'text-foreground',
bg: '',
border: '',
}

View File

@@ -21,7 +21,6 @@ export const DataTableColumnStatusCode = ({
<div className={cn('flex items-center relative', className)}>
<div
className={cn(
'px-1 py-[0.03rem] rounded-md',
'flex items-center justify-center relative font-mono',
colors.text,
colors.bg,

View File

@@ -8,7 +8,7 @@ import {
ChevronRight,
Copy,
Equal,
Search,
Filter,
} from 'lucide-react'
import { ComponentPropsWithRef } from 'react'
import {
@@ -28,10 +28,11 @@ interface DataTableSheetRowActionProps<
TData,
TFields extends DataTableFilterField<TData>,
> extends ComponentPropsWithRef<typeof DropdownMenuTrigger> {
fieldValue: TFields['value']
fieldValue?: TFields['value']
filterFields: TFields[]
value: string | number
table: Table<TData>
label?: string
}
export function DataTableSheetRowAction<TData, TFields extends DataTableFilterField<TData>>({
@@ -41,14 +42,13 @@ export function DataTableSheetRowAction<TData, TFields extends DataTableFilterFi
children,
className,
table,
label,
onKeyDown,
...props
}: DataTableSheetRowActionProps<TData, TFields>) {
const { copy, isCopied } = useCopyToClipboard()
const field = filterFields.find((field) => field.value === fieldValue)
const column = table.getColumn(fieldValue.toString())
if (!field || !column) return null
const field = !!fieldValue ? filterFields.find((f) => f.value === fieldValue) : undefined
const column = !!fieldValue ? table.getColumn(fieldValue.toString()) : undefined
function renderOptions() {
if (!field) return null
@@ -66,7 +66,7 @@ export function DataTableSheetRowAction<TData, TFields extends DataTableFilterFi
}}
className="flex items-center gap-2"
>
<Search size={14} />
<Filter size={12} />
Add as filter for {column?.id}
</DropdownMenuItem>
)
@@ -76,7 +76,7 @@ export function DataTableSheetRowAction<TData, TFields extends DataTableFilterFi
onClick={() => column?.setFilterValue(value)}
className="flex items-center gap-2"
>
<Search size={14} />
<Filter size={12} />
Add as filter for {column?.id}
</DropdownMenuItem>
)
@@ -88,7 +88,7 @@ export function DataTableSheetRowAction<TData, TFields extends DataTableFilterFi
className="flex items-center gap-2"
>
{/* FIXME: change icon as it is not clear */}
<ChevronLeft size={16} />
<ChevronLeft size={12} />
Less or equal than
</DropdownMenuItem>
<DropdownMenuItem
@@ -96,14 +96,14 @@ export function DataTableSheetRowAction<TData, TFields extends DataTableFilterFi
className="flex items-center gap-2"
>
{/* FIXME: change icon as it is not clear */}
<ChevronRight size={16} />
<ChevronRight size={12} />
Greater or equal than
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => column?.setFilterValue([value])}
className="flex items-center gap-2"
>
<Equal size={16} />
<Equal size={12} />
Equal to
</DropdownMenuItem>
</DropdownMenuGroup>
@@ -116,7 +116,7 @@ export function DataTableSheetRowAction<TData, TFields extends DataTableFilterFi
onClick={() => column?.setFilterValue([date])}
className="flex items-center gap-2"
>
<CalendarSearch size={16} />
<CalendarSearch size={12} />
Exact timestamp
</DropdownMenuItem>
<DropdownMenuItem
@@ -127,7 +127,7 @@ export function DataTableSheetRowAction<TData, TFields extends DataTableFilterFi
}}
className="flex items-center gap-2"
>
<CalendarClock size={16} />
<CalendarClock size={12} />
Same hour
</DropdownMenuItem>
<DropdownMenuItem
@@ -138,7 +138,7 @@ export function DataTableSheetRowAction<TData, TFields extends DataTableFilterFi
}}
className="flex items-center gap-2"
>
<CalendarDays size={16} />
<CalendarDays size={12} />
Same day
</DropdownMenuItem>
</DropdownMenuGroup>
@@ -174,15 +174,21 @@ export function DataTableSheetRowAction<TData, TFields extends DataTableFilterFi
</div>
) : null}
</DropdownMenuTrigger>
<DropdownMenuContent align="end" side="bottom" className="w-48">
{renderOptions()}
<DropdownMenuSeparator />
<DropdownMenuContent align="end" side="bottom" className="w-48 -translate-x-4">
{!!field && !!column && (
<>
{renderOptions()}
<DropdownMenuSeparator />
</>
)}
<DropdownMenuItem
onClick={() => copy(String(value), { timeout: 1000 })}
className="flex items-center gap-2"
>
<Copy size={16} />
Copy value
<Copy size={12} />
Copy {label}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>