Skip to content

Commit 7f0fee6

Browse files
feat(ui): add shadow to tables on inner scroll (supabase#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 <[email protected]>
1 parent edc8b05 commit 7f0fee6

File tree

9 files changed

+221
-81
lines changed

9 files changed

+221
-81
lines changed

apps/studio/components/interfaces/Account/AccessTokens/AccessTokenList.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -152,12 +152,12 @@ export const AccessTokenList = ({ searchString = '', onDeleteSuccess }: AccessTo
152152
{filteredTokens?.map((x) => {
153153
return (
154154
<TableRow key={x.token_alias}>
155-
<TableCell className="w-36 max-w-36">
155+
<TableCell className="max-w-32 lg:max-w-40">
156156
<p className="truncate" title={x.name}>
157157
{x.name}
158158
</p>
159159
</TableCell>
160-
<TableCell className="max-w-96">
160+
<TableCell className="max-w-36 lg:max-w-80">
161161
<p className="font-mono text-foreground-light truncate">{x.token_alias}</p>
162162
</TableCell>
163163
<TableCell className="min-w-32">

apps/studio/components/interfaces/Database/Extensions/ExtensionRow.tsx

Lines changed: 60 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { DatabaseExtension } from 'data/database-extensions/database-extensions-
1010
import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions'
1111
import { useIsOrioleDb, useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
1212
import { extensions } from 'shared-data'
13-
import { Button, Switch, Tooltip, TooltipContent, TooltipTrigger, TableRow, TableCell } from 'ui'
13+
import { Button, Switch, TableCell, TableRow, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
1414
import { Admonition } from 'ui-patterns'
1515
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
1616
import EnableExtensionModal from './EnableExtensionModal'
@@ -20,7 +20,7 @@ interface ExtensionRowProps {
2020
extension: DatabaseExtension
2121
}
2222

23-
const ExtensionRow = ({ extension }: ExtensionRowProps) => {
23+
export const ExtensionRow = ({ extension }: ExtensionRowProps) => {
2424
const { data: project } = useSelectedProjectQuery()
2525
const isOn = extension.installed_version !== null
2626
const isOrioleDb = useIsOrioleDb()
@@ -118,58 +118,66 @@ const ExtensionRow = ({ extension }: ExtensionRowProps) => {
118118
)}
119119
</TableCell>
120120

121-
<TableCell className="flex gap-2 items-center">
122-
{extensionMeta?.github_url && (
123-
<Button asChild type="default" icon={<Github />} className="rounded-full">
124-
<a
125-
target="_blank"
126-
rel="noreferrer"
127-
href={extensionMeta.github_url}
128-
className="font-mono tracking-tighter"
129-
>
130-
{extensionMeta.github_url.split('/').slice(-2).join('/')}
131-
</a>
132-
</Button>
133-
)}
134-
{docsUrl !== undefined && (
135-
<Button asChild type="default" icon={<Book />} className="rounded-full">
136-
<a
137-
target="_blank"
138-
rel="noreferrer"
139-
className="font-mono tracking-tighter"
140-
href={docsUrl}
141-
>
142-
Docs
143-
</a>
144-
</Button>
145-
)}
121+
<TableCell>
122+
<div className="flex gap-2 items-center">
123+
{extensionMeta?.github_url && (
124+
<Button asChild type="default" icon={<Github />} className="rounded-full">
125+
<a
126+
target="_blank"
127+
rel="noreferrer"
128+
href={extensionMeta.github_url}
129+
className="font-mono tracking-tighter"
130+
>
131+
{extensionMeta.github_url.split('/').slice(-2).join('/')}
132+
</a>
133+
</Button>
134+
)}
135+
{docsUrl !== undefined && (
136+
<Button asChild type="default" icon={<Book />} className="rounded-full">
137+
<a
138+
target="_blank"
139+
rel="noreferrer"
140+
className="font-mono tracking-tighter"
141+
href={docsUrl}
142+
>
143+
Docs
144+
</a>
145+
</Button>
146+
)}
147+
</div>
146148
</TableCell>
147149

148-
<TableCell className="w-20 sticky bg-surface-100 border-l right-0">
149-
{isDisabling ? (
150-
<Loader2 className="animate-spin" size={16} />
151-
) : (
152-
<Tooltip>
153-
<TooltipTrigger>
154-
<Switch
155-
disabled={disabled}
156-
checked={isOn}
157-
onCheckedChange={() =>
158-
isOn ? setIsDisableModalOpen(true) : setShowConfirmEnableModal(true)
159-
}
160-
/>
161-
</TooltipTrigger>
162-
{disabled && (
163-
<TooltipContent side="bottom">
164-
{!canUpdateExtensions
165-
? 'You need additional permissions to toggle extensions'
166-
: orioleDbCheck
167-
? 'Project is using OrioleDB and cannot be disabled'
168-
: null}
169-
</TooltipContent>
170-
)}
171-
</Tooltip>
172-
)}
150+
{/*
151+
[Joshen] The div child here and all these classes is to properly add a left border
152+
to make the sticky column more distinct
153+
*/}
154+
<TableCell className="w-20 sticky bg-surface-100 right-0 relative">
155+
<div className="absolute top-0 right-0 left-0 bottom-0 flex items-center justify-center border-l">
156+
{isDisabling ? (
157+
<Loader2 className="animate-spin" size={16} />
158+
) : (
159+
<Tooltip>
160+
<TooltipTrigger>
161+
<Switch
162+
disabled={disabled}
163+
checked={isOn}
164+
onCheckedChange={() =>
165+
isOn ? setIsDisableModalOpen(true) : setShowConfirmEnableModal(true)
166+
}
167+
/>
168+
</TooltipTrigger>
169+
{disabled && (
170+
<TooltipContent side="bottom">
171+
{!canUpdateExtensions
172+
? 'You need additional permissions to toggle extensions'
173+
: orioleDbCheck
174+
? 'Project is using OrioleDB and cannot be disabled'
175+
: null}
176+
</TooltipContent>
177+
)}
178+
</Tooltip>
179+
)}
180+
</div>
173181
</TableCell>
174182
</TableRow>
175183

@@ -202,5 +210,3 @@ const ExtensionRow = ({ extension }: ExtensionRowProps) => {
202210
</>
203211
)
204212
}
205-
206-
export default ExtensionRow

apps/studio/components/interfaces/Database/Extensions/Extensions.tsx

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,21 @@ import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader'
1111
import { useDatabaseExtensionsQuery } from 'data/database-extensions/database-extensions-query'
1212
import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions'
1313
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
14-
import { Card, Input, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from 'ui'
15-
import ExtensionRow from './ExtensionRow'
14+
import {
15+
Card,
16+
Input,
17+
ShadowScrollArea,
18+
Table,
19+
TableBody,
20+
TableCell,
21+
TableHead,
22+
TableHeader,
23+
TableRow,
24+
} from 'ui'
25+
import { ExtensionRow } from './ExtensionRow'
1626
import { HIDDEN_EXTENSIONS, SEARCH_TERMS } from './Extensions.constants'
1727

18-
const Extensions = () => {
28+
export const Extensions = () => {
1929
const { filter } = useParams()
2030
const { data: project } = useSelectedProjectQuery()
2131
const [filterString, setFilterString] = useState<string>('')
@@ -74,8 +84,8 @@ const Extensions = () => {
7484
{isLoading ? (
7585
<GenericSkeletonLoader />
7686
) : (
77-
<div className="w-full overflow-hidden overflow-x-auto">
78-
<Card>
87+
<Card>
88+
<ShadowScrollArea stickyLastColumn>
7989
<Table>
8090
<TableHeader>
8191
<TableRow>
@@ -85,11 +95,16 @@ const Extensions = () => {
8595
<TableHead key="description">Description</TableHead>
8696
<TableHead key="used-by">Used by</TableHead>
8797
<TableHead key="links">Links</TableHead>
88-
<TableHead
89-
key="enabled"
90-
className="w-20 bg-background-200 border-l sticky right-0"
91-
>
92-
Enabled
98+
{/*
99+
[Joshen] All these classes are just to make the last column sticky
100+
I reckon we can pull these out into the Table component where we can declare
101+
sticky columns via props, but we can do that if we start to have more tables
102+
in the dashboard with sticky columns
103+
*/}
104+
<TableHead key="enabled" className="px-0">
105+
<div className="!bg-200 px-4 w-full h-full flex items-center border-l">
106+
Enabled
107+
</div>
93108
</TableHead>
94109
</TableRow>
95110
</TableHeader>
@@ -110,11 +125,9 @@ const Extensions = () => {
110125
)}
111126
</TableBody>
112127
</Table>
113-
</Card>
114-
</div>
128+
</ShadowScrollArea>
129+
</Card>
115130
)}
116131
</>
117132
)
118133
}
119-
120-
export default Extensions

apps/studio/components/interfaces/Database/index.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
export { default as RolesList } from './Roles/RolesList'
22

3-
export { default as Extensions } from './Extensions/Extensions'
4-
53
export { default as BackupsList } from './Backups/BackupsList'
64

75
export { default as CreateFunction } from './Functions/CreateFunction'

apps/studio/pages/project/[ref]/database/extensions.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import { PermissionAction } from '@supabase/shared-types/out/constants'
22

3-
import { Extensions } from 'components/interfaces/Database'
3+
import { Extensions } from 'components/interfaces/Database/Extensions/Extensions'
44
import DatabaseLayout from 'components/layouts/DatabaseLayout/DatabaseLayout'
55
import DefaultLayout from 'components/layouts/DefaultLayout'
6-
import { ScaffoldContainer, ScaffoldSection } from 'components/layouts/Scaffold'
76
import { PageLayout } from 'components/layouts/PageLayout/PageLayout'
7+
import { ScaffoldContainer, ScaffoldSection } from 'components/layouts/Scaffold'
88
import NoPermission from 'components/ui/NoPermission'
99
import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions'
1010
import type { NextPageWithLayout } from 'types'

packages/ui/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,7 @@ export * from './src/components/shadcn/ui/hover-card'
204204
export * from './src/components/shadcn/ui/aspect-ratio'
205205

206206
export * from './src/components/shadcn/ui/table'
207+
export * from './src/components/ShadowScrollArea'
207208

208209
export {
209210
Collapsible as Collapsible_Shadcn_,
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import * as React from 'react'
2+
import { cn } from '../../lib/utils/cn'
3+
import { useHorizontalScroll } from '../hooks/use-horizontal-scroll'
4+
5+
interface ShadowScrollAreaProps extends React.HTMLAttributes<HTMLDivElement> {
6+
children: React.ReactNode
7+
stickyLastColumn?: boolean
8+
}
9+
10+
/**
11+
* [Joshen] Leaving feedback here to address in the future
12+
We should pull out all the sticky column logic, shift them to Table component where we can declare
13+
sticky columns via a prop. ShadowScrollArea here should purely handle the shadow styling
14+
*/
15+
16+
const ShadowScrollArea = React.forwardRef<HTMLDivElement, ShadowScrollAreaProps>(
17+
({ className, children, stickyLastColumn, ...props }, ref) => {
18+
const containerRef = React.useRef<HTMLDivElement>(null)
19+
const { hasHorizontalScroll, canScrollLeft, canScrollRight } = useHorizontalScroll(containerRef)
20+
21+
const stickyColumnShadow = cn(
22+
'[&_td:last-child]:before:absolute [&_td:last-child]:before:top-0 [&_td:last-child]:before:-left-6',
23+
'[&_td:last-child]:before:bottom-0 [&_td:last-child]:before:w-6 [&_td:last-child]:before:bg-gradient-to-l',
24+
'[&_td:last-child]:before:from-black/5 dark:[&_td:last-child]:before:from-black/20 [&_td:last-child]:before:to-transparent',
25+
'[&_td:last-child]:before:opacity-0 [&_td:last-child]:before:transition-all [&_td:last-child]:before:duration-400',
26+
'[&_td:last-child]:before:easing-[0.24, 0.25, 0.05, 1] [&_td:last-child]:before:z-[60]',
27+
'[&_th:last-child]:before:absolute [&_th:last-child]:before:top-0 [&_th:last-child]:before:-left-6',
28+
'[&_th:last-child]:before:bottom-0 [&_th:last-child]:before:w-6 [&_th:last-child]:before:bg-gradient-to-l',
29+
'[&_th:last-child]:before:from-black/5 dark:[&_th:last-child]:before:from-black/20 [&_th:last-child]:before:to-transparent',
30+
'[&_th:last-child]:before:opacity-0 [&_th:last-child]:before:transition-all [&_th:last-child]:before:duration-400',
31+
'[&_th:last-child]:before:easing-[0.24, 0.25, 0.05, 1] [&_th:last-child]:before:z-[60]'
32+
)
33+
34+
return (
35+
<div className="relative">
36+
<div
37+
className={cn(
38+
'absolute inset-0 pointer-events-none z-50',
39+
'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]',
40+
'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]',
41+
hasHorizontalScroll && 'hover:before:opacity-100 hover:after:opacity-100',
42+
canScrollRight && 'before:opacity-100',
43+
canScrollLeft && 'after:opacity-100'
44+
)}
45+
/>
46+
<div
47+
ref={containerRef}
48+
className={cn(
49+
'w-full overflow-auto',
50+
stickyLastColumn && [
51+
'[&_tr>*:last-child]:sticky [&_tr>*:last-child]:z-50 [&_tr>*:last-child]:right-0',
52+
'[&_tr:hover>*:last-child]:bg-transparent',
53+
'[&_th>*:last-child]:bg-surface-100',
54+
stickyColumnShadow,
55+
],
56+
hasHorizontalScroll && '[&_tr:hover>td:last-child]:!bg-surface-200',
57+
canScrollRight &&
58+
'[&_td]:before:opacity-100 [&_tr>*:last-child]:before:opacity-100 [&_th:last-child]:before:opacity-100',
59+
className
60+
)}
61+
{...props}
62+
>
63+
{children}
64+
</div>
65+
</div>
66+
)
67+
}
68+
)
69+
70+
ShadowScrollArea.displayName = 'ShadowScrollArea'
71+
72+
export { ShadowScrollArea }
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import * as React from 'react'
2+
3+
export const useHorizontalScroll = (ref: React.RefObject<HTMLDivElement>) => {
4+
const [hasHorizontalScroll, setHasHorizontalScroll] = React.useState(false)
5+
const [canScrollLeft, setCanScrollLeft] = React.useState(false)
6+
const [canScrollRight, setCanScrollRight] = React.useState(false)
7+
8+
React.useEffect(() => {
9+
const element = ref.current
10+
if (!element) return
11+
12+
const checkScroll = () => {
13+
const hasScroll = element.scrollWidth > element.clientWidth
14+
setHasHorizontalScroll(hasScroll)
15+
16+
if (hasScroll) {
17+
const canScrollLeft = element.scrollLeft > 0
18+
const canScrollRight = element.scrollLeft < element.scrollWidth - element.clientWidth
19+
setCanScrollLeft(canScrollLeft)
20+
setCanScrollRight(canScrollRight)
21+
} else {
22+
setCanScrollLeft(false)
23+
setCanScrollRight(false)
24+
}
25+
}
26+
27+
const handleScroll = () => {
28+
if (hasHorizontalScroll) {
29+
const canScrollLeft = element.scrollLeft > 0
30+
const canScrollRight = element.scrollLeft < element.scrollWidth - element.clientWidth
31+
setCanScrollLeft(canScrollLeft)
32+
setCanScrollRight(canScrollRight)
33+
}
34+
}
35+
36+
checkScroll()
37+
element.addEventListener('scroll', handleScroll)
38+
window.addEventListener('resize', checkScroll)
39+
40+
return () => {
41+
element.removeEventListener('scroll', handleScroll)
42+
window.removeEventListener('resize', checkScroll)
43+
}
44+
}, [ref, hasHorizontalScroll])
45+
46+
return { hasHorizontalScroll, canScrollLeft, canScrollRight }
47+
}

0 commit comments

Comments
 (0)