mirror of
https://github.com/supabase/supabase.git
synced 2026-10-06 01:45:10 +03:00
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:
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'
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
))
|
||||
|
||||
Reference in new issue
Block a user