feat(ui): add shadow to tables on inner scroll (#38094)

* feat: add shadow to tables on inner scroll

This adds a shadow to indicate the tables still have more content to scroll.

* chore: remove comments from file

* fix: update easing and duration of shadow transitions

* fix: some more column size fixes

* feat: generic ShadowScrollArea component

This creates a generic ShadowScrollArea component we can reuse eleswhere

* feat: working sticky last col and shadow

* chore: run prettier

* fix: bg hover of sticky column

* Fix hover states for table header and table row, and if last column is sticky

---------

Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
This commit is contained in:
kemal.earthandJoshen Lim authored and GitHub committed 2025-08-26 18:05:53 +01:00
1 parent edc8b05f86
commit 7f0fee6541
9 files changed
+221 -81

No files matched your search

@@ -152,12 +152,12 @@ export const AccessTokenList = ({ searchString = '', onDeleteSuccess }: AccessTo
{filteredTokens?.map((x) => {
return (
<TableRow key={x.token_alias}>
<TableCell className="w-36 max-w-36">
<TableCell className="max-w-32 lg:max-w-40">
<p className="truncate" title={x.name}>
{x.name}
</p>
</TableCell>
<TableCell className="max-w-96">
<TableCell className="max-w-36 lg:max-w-80">
<p className="font-mono text-foreground-light truncate">{x.token_alias}</p>
</TableCell>
<TableCell className="min-w-32">
@@ -10,7 +10,7 @@ import { DatabaseExtension } from 'data/database-extensions/database-extensions-
import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions'
import { useIsOrioleDb, useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { extensions } from 'shared-data'
import { Button, Switch, Tooltip, TooltipContent, TooltipTrigger, TableRow, TableCell } from 'ui'
import { Button, Switch, TableCell, TableRow, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
import { Admonition } from 'ui-patterns'
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
import EnableExtensionModal from './EnableExtensionModal'
@@ -20,7 +20,7 @@ interface ExtensionRowProps {
extension: DatabaseExtension
}
const ExtensionRow = ({ extension }: ExtensionRowProps) => {
export const ExtensionRow = ({ extension }: ExtensionRowProps) => {
const { data: project } = useSelectedProjectQuery()
const isOn = extension.installed_version !== null
const isOrioleDb = useIsOrioleDb()
@@ -118,58 +118,66 @@ const ExtensionRow = ({ extension }: ExtensionRowProps) => {
)}
</TableCell>
<TableCell className="flex gap-2 items-center">
{extensionMeta?.github_url && (
<Button asChild type="default" icon={<Github />} className="rounded-full">
<a
target="_blank"
rel="noreferrer"
href={extensionMeta.github_url}
className="font-mono tracking-tighter"
>
{extensionMeta.github_url.split('/').slice(-2).join('/')}
</a>
</Button>
)}
{docsUrl !== undefined && (
<Button asChild type="default" icon={<Book />} className="rounded-full">
<a
target="_blank"
rel="noreferrer"
className="font-mono tracking-tighter"
href={docsUrl}
>
Docs
</a>
</Button>
)}
<TableCell>
<div className="flex gap-2 items-center">
{extensionMeta?.github_url && (
<Button asChild type="default" icon={<Github />} className="rounded-full">
<a
target="_blank"
rel="noreferrer"
href={extensionMeta.github_url}
className="font-mono tracking-tighter"
>
{extensionMeta.github_url.split('/').slice(-2).join('/')}
</a>
</Button>
)}
{docsUrl !== undefined && (
<Button asChild type="default" icon={<Book />} className="rounded-full">
<a
target="_blank"
rel="noreferrer"
className="font-mono tracking-tighter"
href={docsUrl}
>
Docs
</a>
</Button>
)}
</div>
</TableCell>
<TableCell className="w-20 sticky bg-surface-100 border-l right-0">
{isDisabling ? (
<Loader2 className="animate-spin" size={16} />
) : (
<Tooltip>
<TooltipTrigger>
<Switch
disabled={disabled}
checked={isOn}
onCheckedChange={() =>
isOn ? setIsDisableModalOpen(true) : setShowConfirmEnableModal(true)
}
/>
</TooltipTrigger>
{disabled && (
<TooltipContent side="bottom">
{!canUpdateExtensions
? 'You need additional permissions to toggle extensions'
: orioleDbCheck
? 'Project is using OrioleDB and cannot be disabled'
: null}
</TooltipContent>
)}
</Tooltip>
)}
{/*
[Joshen] The div child here and all these classes is to properly add a left border
to make the sticky column more distinct
*/}
<TableCell className="w-20 sticky bg-surface-100 right-0 relative">
<div className="absolute top-0 right-0 left-0 bottom-0 flex items-center justify-center border-l">
{isDisabling ? (
<Loader2 className="animate-spin" size={16} />
) : (
<Tooltip>
<TooltipTrigger>
<Switch
disabled={disabled}
checked={isOn}
onCheckedChange={() =>
isOn ? setIsDisableModalOpen(true) : setShowConfirmEnableModal(true)
}
/>
</TooltipTrigger>
{disabled && (
<TooltipContent side="bottom">
{!canUpdateExtensions
? 'You need additional permissions to toggle extensions'
: orioleDbCheck
? 'Project is using OrioleDB and cannot be disabled'
: null}
</TooltipContent>
)}
</Tooltip>
)}
</div>
</TableCell>
</TableRow>
@@ -202,5 +210,3 @@ const ExtensionRow = ({ extension }: ExtensionRowProps) => {
</>
)
}
export default ExtensionRow
@@ -11,11 +11,21 @@ import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader'
import { useDatabaseExtensionsQuery } from 'data/database-extensions/database-extensions-query'
import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { Card, Input, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from 'ui'
import ExtensionRow from './ExtensionRow'
import {
Card,
Input,
ShadowScrollArea,
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from 'ui'
import { ExtensionRow } from './ExtensionRow'
import { HIDDEN_EXTENSIONS, SEARCH_TERMS } from './Extensions.constants'
const Extensions = () => {
export const Extensions = () => {
const { filter } = useParams()
const { data: project } = useSelectedProjectQuery()
const [filterString, setFilterString] = useState<string>('')
@@ -74,8 +84,8 @@ const Extensions = () => {
{isLoading ? (
<GenericSkeletonLoader />
) : (
<div className="w-full overflow-hidden overflow-x-auto">
<Card>
<Card>
<ShadowScrollArea stickyLastColumn>
<Table>
<TableHeader>
<TableRow>
@@ -85,11 +95,16 @@ const Extensions = () => {
<TableHead key="description">Description</TableHead>
<TableHead key="used-by">Used by</TableHead>
<TableHead key="links">Links</TableHead>
<TableHead
key="enabled"
className="w-20 bg-background-200 border-l sticky right-0"
>
Enabled
{/*
[Joshen] All these classes are just to make the last column sticky
I reckon we can pull these out into the Table component where we can declare
sticky columns via props, but we can do that if we start to have more tables
in the dashboard with sticky columns
*/}
<TableHead key="enabled" className="px-0">
<div className="!bg-200 px-4 w-full h-full flex items-center border-l">
Enabled
</div>
</TableHead>
</TableRow>
</TableHeader>
@@ -110,11 +125,9 @@ const Extensions = () => {
)}
</TableBody>
</Table>
</Card>
</div>
</ShadowScrollArea>
</Card>
)}
</>
)
}
export default Extensions
@@ -1,7 +1,5 @@
export { default as RolesList } from './Roles/RolesList'
export { default as Extensions } from './Extensions/Extensions'
export { default as BackupsList } from './Backups/BackupsList'
export { default as CreateFunction } from './Functions/CreateFunction'
@@ -1,10 +1,10 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { Extensions } from 'components/interfaces/Database'
import { Extensions } from 'components/interfaces/Database/Extensions/Extensions'
import DatabaseLayout from 'components/layouts/DatabaseLayout/DatabaseLayout'
import DefaultLayout from 'components/layouts/DefaultLayout'
import { ScaffoldContainer, ScaffoldSection } from 'components/layouts/Scaffold'
import { PageLayout } from 'components/layouts/PageLayout/PageLayout'
import { ScaffoldContainer, ScaffoldSection } from 'components/layouts/Scaffold'
import NoPermission from 'components/ui/NoPermission'
import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions'
import type { NextPageWithLayout } from 'types'
+1
View File
@@ -204,6 +204,7 @@ export * from './src/components/shadcn/ui/hover-card'
export * from './src/components/shadcn/ui/aspect-ratio'
export * from './src/components/shadcn/ui/table'
export * from './src/components/ShadowScrollArea'
export {
Collapsible as Collapsible_Shadcn_,
@@ -0,0 +1,72 @@
import * as React from 'react'
import { cn } from '../../lib/utils/cn'
import { useHorizontalScroll } from '../hooks/use-horizontal-scroll'
interface ShadowScrollAreaProps extends React.HTMLAttributes<HTMLDivElement> {
children: React.ReactNode
stickyLastColumn?: boolean
}
/**
* [Joshen] Leaving feedback here to address in the future
We should pull out all the sticky column logic, shift them to Table component where we can declare
sticky columns via a prop. ShadowScrollArea here should purely handle the shadow styling
*/
const ShadowScrollArea = React.forwardRef<HTMLDivElement, ShadowScrollAreaProps>(
({ className, children, stickyLastColumn, ...props }, ref) => {
const containerRef = React.useRef<HTMLDivElement>(null)
const { hasHorizontalScroll, canScrollLeft, canScrollRight } = useHorizontalScroll(containerRef)
const stickyColumnShadow = cn(
'[&_td:last-child]:before:absolute [&_td:last-child]:before:top-0 [&_td:last-child]:before:-left-6',
'[&_td:last-child]:before:bottom-0 [&_td:last-child]:before:w-6 [&_td:last-child]:before:bg-gradient-to-l',
'[&_td:last-child]:before:from-black/5 dark:[&_td:last-child]:before:from-black/20 [&_td:last-child]:before:to-transparent',
'[&_td:last-child]:before:opacity-0 [&_td:last-child]:before:transition-all [&_td:last-child]:before:duration-400',
'[&_td:last-child]:before:easing-[0.24, 0.25, 0.05, 1] [&_td:last-child]:before:z-[60]',
'[&_th:last-child]:before:absolute [&_th:last-child]:before:top-0 [&_th:last-child]:before:-left-6',
'[&_th:last-child]:before:bottom-0 [&_th:last-child]:before:w-6 [&_th:last-child]:before:bg-gradient-to-l',
'[&_th:last-child]:before:from-black/5 dark:[&_th:last-child]:before:from-black/20 [&_th:last-child]:before:to-transparent',
'[&_th:last-child]:before:opacity-0 [&_th:last-child]:before:transition-all [&_th:last-child]:before:duration-400',
'[&_th:last-child]:before:easing-[0.24, 0.25, 0.05, 1] [&_th:last-child]:before:z-[60]'
)
return (
<div className="relative">
<div
className={cn(
'absolute inset-0 pointer-events-none z-50',
'before:absolute before:top-0 before:right-0 before:bottom-0 before:w-6 before:bg-gradient-to-l before:from-black/5 dark:before:from-black/20 before:to-transparent before:opacity-0 before:transition-all before:duration-400 before:easing-[0.24, 0.25, 0.05, 1]',
'after:absolute after:top-0 after:left-0 after:bottom-0 after:w-6 after:bg-gradient-to-r after:from-black/5 dark:after:from-black/20 after:to-transparent after:opacity-0 after:transition-all after:duration-400 after:easing-[0.24, 0.25, 0.05, 1]',
hasHorizontalScroll && 'hover:before:opacity-100 hover:after:opacity-100',
canScrollRight && 'before:opacity-100',
canScrollLeft && 'after:opacity-100'
)}
/>
<div
ref={containerRef}
className={cn(
'w-full overflow-auto',
stickyLastColumn && [
'[&_tr>*:last-child]:sticky [&_tr>*:last-child]:z-50 [&_tr>*:last-child]:right-0',
'[&_tr:hover>*:last-child]:bg-transparent',
'[&_th>*:last-child]:bg-surface-100',
stickyColumnShadow,
],
hasHorizontalScroll && '[&_tr:hover>td:last-child]:!bg-surface-200',
canScrollRight &&
'[&_td]:before:opacity-100 [&_tr>*:last-child]:before:opacity-100 [&_th:last-child]:before:opacity-100',
className
)}
{...props}
>
{children}
</div>
</div>
)
}
)
ShadowScrollArea.displayName = 'ShadowScrollArea'
export { ShadowScrollArea }
@@ -0,0 +1,47 @@
import * as React from 'react'
export const useHorizontalScroll = (ref: React.RefObject<HTMLDivElement>) => {
const [hasHorizontalScroll, setHasHorizontalScroll] = React.useState(false)
const [canScrollLeft, setCanScrollLeft] = React.useState(false)
const [canScrollRight, setCanScrollRight] = React.useState(false)
React.useEffect(() => {
const element = ref.current
if (!element) return
const checkScroll = () => {
const hasScroll = element.scrollWidth > element.clientWidth
setHasHorizontalScroll(hasScroll)
if (hasScroll) {
const canScrollLeft = element.scrollLeft > 0
const canScrollRight = element.scrollLeft < element.scrollWidth - element.clientWidth
setCanScrollLeft(canScrollLeft)
setCanScrollRight(canScrollRight)
} else {
setCanScrollLeft(false)
setCanScrollRight(false)
}
}
const handleScroll = () => {
if (hasHorizontalScroll) {
const canScrollLeft = element.scrollLeft > 0
const canScrollRight = element.scrollLeft < element.scrollWidth - element.clientWidth
setCanScrollLeft(canScrollLeft)
setCanScrollRight(canScrollRight)
}
}
checkScroll()
element.addEventListener('scroll', handleScroll)
window.addEventListener('resize', checkScroll)
return () => {
element.removeEventListener('scroll', handleScroll)
window.removeEventListener('resize', checkScroll)
}
}, [ref, hasHorizontalScroll])
return { hasHorizontalScroll, canScrollLeft, canScrollRight }
}
+10 -7
View File
@@ -1,17 +1,20 @@
import * as React from 'react'
import { cn } from '../../../lib/utils/cn'
import { ShadowScrollArea } from '../../ShadowScrollArea'
interface TableProps extends React.HTMLAttributes<HTMLTableElement> {
containerProps?: React.HTMLAttributes<HTMLDivElement>
}
const Table = React.forwardRef<HTMLTableElement, TableProps>(
({ className, containerProps, ...props }, ref) => (
<div className={cn('w-full overflow-auto')} {...containerProps}>
<table ref={ref} className={cn('w-full caption-bottom text-sm', className)} {...props} />
</div>
)
({ className, containerProps, ...props }, ref) => {
return (
<ShadowScrollArea {...containerProps}>
<table ref={ref} className={cn('w-full caption-bottom text-sm', className)} {...props} />
</ShadowScrollArea>
)
}
)
Table.displayName = 'Table'
@@ -48,7 +51,7 @@ const TableRow = React.forwardRef<HTMLTableRowElement, React.HTMLAttributes<HTML
<tr
ref={ref}
className={cn(
'border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted',
'border-b [&>td]:hover:bg-surface-200 data-[state=selected]:bg-muted',
className
)}
{...props}
@@ -78,7 +81,7 @@ const TableCell = React.forwardRef<
>(({ className, ...props }, ref) => (
<td
ref={ref}
className={cn('p-4 align-middle [&:has([role=checkbox])]:pr-0', className)}
className={cn('transition-colors p-4 align-middle [&:has([role=checkbox])]:pr-0', className)}
{...props}
/>
))