mirror of
https://github.com/supabase/supabase.git
synced 2026-06-01 02:14:43 +08:00
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 --> [](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:
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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: '',
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user