Skip to content

Commit f7ea722

Browse files
joshenlimdjhi
andauthored
Consolidate grid header actions in table editor into a single row (supabase#45504)
## Consolidate Table Editor grid header actions into a single row https://github.com/user-attachments/assets/1020c385-8fa9-4ef1-b5e7-03983111508b ## Changes involved - Index advisor, Realtime, and API docs are now behind a dropdown menu button (Treated as secondary actions) - Grid header actions shifted into the same row as filter bar (more space for data grid) - Header actions will hide while filter bar is in focus (remove distractions, more space for filter bar) ## Changes to filter bar - Filter bar will refocus when deleting a filter - Clicking on the search icon will focus on the free form input of the filter bar <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added a “More” dropdown in grid actions to access Realtime, API docs, and Index Advisor. * New dialogs for enabling Index Advisor and toggling Realtime are now consistently managed. * **Improvements** * Improved filter focus handling with auto-refocus when conditions change and responsive header behavior. * Adjusted popover alignment, separator visuals, header/footer/pagination layout and sizing. * Filter bar now supports programmatic focus; Connect button supports icon-only mode. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Gildas Garcia <1122076+djhi@users.noreply.github.com>
1 parent aee4c8f commit f7ea722

16 files changed

Lines changed: 325 additions & 133 deletions

File tree

apps/studio/components/grid/components/footer/Footer.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export const Footer: React.FC<FooterProps> = ({ enableForeignRowsQuery = true }:
3030
<GridFooter>
3131
{selectedView === 'data' && <Pagination enableForeignRowsQuery={enableForeignRowsQuery} />}
3232

33-
<div className="ml-auto flex items-center gap-x-2">
33+
<div className="flex items-center gap-x-2">
3434
{(isViewSelected || isTableSelected) && (
3535
<TwoOptionToggle
3636
width={75}

apps/studio/components/grid/components/footer/pagination/Pagination.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,7 @@ export const Pagination = ({ enableForeignRowsQuery = true }: PaginationProps) =
235235
}
236236

237237
return (
238-
<div className="flex items-center gap-x-4">
238+
<div className="flex items-center gap-x-4 min-w-fit">
239239
<div className="flex items-center gap-x-2">
240240
<Button
241241
aria-label="Previous page"

apps/studio/components/grid/components/header/Header.tsx

Lines changed: 70 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { keepPreviousData } from '@tanstack/react-query'
2-
import { useParams } from 'common'
2+
import { useBreakpoint, useParams } from 'common'
3+
import { AnimatePresence, motion } from 'framer-motion'
34
import { ChevronDown, Trash } from 'lucide-react'
4-
import { ReactNode, useState } from 'react'
5+
import { ReactNode, useEffect, useRef, useState } from 'react'
56
import { toast } from 'sonner'
67
import {
78
Button,
@@ -49,32 +50,78 @@ export type HeaderProps = {
4950

5051
export const Header = ({ customHeader, isRefetching, tableQueriesEnabled = true }: HeaderProps) => {
5152
useInitializeFiltersFromUrl()
52-
5353
useSyncFiltersToUrl()
5454

55+
const isMobile = useBreakpoint('md')
5556
const snap = useTableEditorTableStateSnapshot()
57+
const [isInputFocus, setIsInputFocus] = useState(false)
58+
const filterContainerRef = useRef<HTMLDivElement>(null)
59+
60+
useEffect(() => {
61+
if (!isInputFocus) return
62+
63+
const handleMouseDown = (e: MouseEvent) => {
64+
const target = e.target as Element
65+
const withinFilter = filterContainerRef.current?.contains(target)
66+
const withinPortal = target?.closest?.('[data-radix-popper-content-wrapper]')
67+
if (!withinFilter && !withinPortal) {
68+
setIsInputFocus(false)
69+
}
70+
}
71+
72+
document.addEventListener('mousedown', handleMouseDown)
73+
return () => document.removeEventListener('mousedown', handleMouseDown)
74+
}, [isInputFocus])
5675

5776
return (
58-
<div>
59-
<div className="flex flex-wrap min-h-10 items-center bg-dash-sidebar dark:bg-surface-100">
60-
{customHeader ? (
61-
<div className="flex-1 px-1.5">{customHeader}</div>
62-
) : snap.selectedRows.size > 0 ? (
63-
<div className="flex-1 px-1.5">
64-
<RowHeader tableQueriesEnabled={tableQueriesEnabled} />
65-
</div>
66-
) : (
67-
<div className="w-full flex items-center justify-between gap-2 pr-1.5 py-1.5 border-b border-border">
68-
<FilterPopoverNew isRefetching={isRefetching} />
69-
</div>
70-
)}
71-
<div className="flex items-center gap-2 overflow-x-auto px-1.5 py-1.5">
72-
{!customHeader && snap.selectedRows.size === 0 && (
73-
<SortPopover tableQueriesEnabled={tableQueriesEnabled} />
77+
<div className="flex flex-wrap md:min-h-10 items-center bg-dash-sidebar dark:bg-surface-100">
78+
{customHeader ? (
79+
<div className="flex-1 px-1.5">{customHeader}</div>
80+
) : snap.selectedRows.size > 0 ? (
81+
<div className="flex-1 px-1.5">
82+
<RowHeader tableQueriesEnabled={tableQueriesEnabled} />
83+
</div>
84+
) : (
85+
<div
86+
ref={filterContainerRef}
87+
className="w-full flex items-center justify-between gap-2 pr-1.5 border-b border-border md:border-none pt-1 md:pt-0"
88+
>
89+
<FilterPopoverNew
90+
isRefetching={isRefetching}
91+
onInputFocus={() => setIsInputFocus(true)}
92+
onInputBlur={() => setIsInputFocus(false)}
93+
/>
94+
95+
{!isMobile && (
96+
<AnimatePresence>
97+
{!isInputFocus && (
98+
<motion.div
99+
initial={{ opacity: 0, x: 20 }}
100+
animate={{ opacity: 1, x: 0 }}
101+
exit={{ opacity: 0, x: 20 }}
102+
transition={{
103+
type: 'spring',
104+
stiffness: 420,
105+
damping: 30,
106+
mass: 0.4,
107+
}}
108+
className="hidden md:flex items-center gap-2 overflow-x-auto"
109+
>
110+
<SortPopover tableQueriesEnabled={tableQueriesEnabled} />
111+
<GridHeaderActions table={snap.originalTable} isRefetching={isRefetching} />
112+
</motion.div>
113+
)}
114+
</AnimatePresence>
74115
)}
116+
</div>
117+
)}
118+
119+
{isMobile && (
120+
<div className="flex items-center gap-2 overflow-x-auto px-1.5 py-1.5">
121+
<SortPopover tableQueriesEnabled={tableQueriesEnabled} />
75122
<GridHeaderActions table={snap.originalTable} isRefetching={isRefetching} />
76123
</div>
77-
</div>
124+
)}
78125
</div>
79126
)
80127
}
@@ -270,6 +317,7 @@ const RowHeader = ({ tableQueriesEnabled = true }: RowHeaderProps) => {
270317
<Shortcut
271318
id={SHORTCUT_IDS.TABLE_EDITOR_DELETE_SELECTED_ROWS}
272319
onTrigger={onRowsDelete}
320+
side="bottom"
273321
options={{
274322
registerInCommandMenu: true,
275323
enabled: !(snap.allRowsSelected && isImpersonatingRole),
@@ -366,12 +414,13 @@ const RowHeader = ({ tableQueriesEnabled = true }: RowHeaderProps) => {
366414
{snap.selectedRows.size > 0 && totalRows > allRows.length && (
367415
<>
368416
<div className="h-6 ml-0.5">
369-
<Separator orientation="vertical" />
417+
<Separator orientation="vertical" className="bg-border" />
370418
</div>
371419
<Shortcut
372420
id={SHORTCUT_IDS.TABLE_EDITOR_SELECT_ALL_IN_TABLE}
373421
onTrigger={onToggleSelectAllInTable}
374422
options={{ registerInCommandMenu: true }}
423+
side="bottom"
375424
>
376425
<Button type="text" onClick={onToggleSelectAllInTable}>
377426
{snap.allRowsSelected ? 'Deselect all rows in table' : 'Select all rows in table'}

apps/studio/components/grid/components/header/filter/FilterPopoverNew.tsx

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import { format } from 'date-fns'
22
import { Loader2 } from 'lucide-react'
3-
import { useCallback, useMemo, useState } from 'react'
3+
import { useCallback, useMemo, useRef, useState } from 'react'
44
import { AiIconAnimation, Button, Calendar } from 'ui'
55
import {
66
CustomOptionProps,
77
FilterBar,
8+
FilterBarHandle,
89
FilterGroup,
910
FilterOption,
1011
FilterProperty,
@@ -21,6 +22,8 @@ import { useTableEditorTableStateSnapshot } from '@/state/table-editor-table'
2122

2223
export interface FilterPopoverProps {
2324
isRefetching?: boolean
25+
onInputFocus?: () => void
26+
onInputBlur?: () => void
2427
}
2528

2629
// Convert Filter[] to FilterGroup
@@ -118,9 +121,14 @@ function serializeFilterProperties(
118121
}))
119122
}
120123

121-
export const FilterPopoverNew = ({ isRefetching = false }: FilterPopoverProps) => {
124+
export const FilterPopoverNew = ({
125+
isRefetching = false,
126+
onInputFocus,
127+
onInputBlur,
128+
}: FilterPopoverProps) => {
122129
const { filters, setFilters } = useTableFilter()
123130
const snap = useTableEditorTableStateSnapshot()
131+
const filterBarRef = useRef<FilterBarHandle>(null)
124132

125133
const [freeformText, setFreeformText] = useState('')
126134
const { mutateAsync: generateFilters, isPending: isGenerating } = useSqlFilterGenerateMutation()
@@ -160,9 +168,14 @@ export const FilterPopoverNew = ({ isRefetching = false }: FilterPopoverProps) =
160168
const handleFilterChange = useCallback(
161169
(newFilterGroup: FilterGroup) => {
162170
const newFilters = filterGroupToFilters(newFilterGroup)
171+
const conditionRemoved = newFilters.length < filters.length
163172
setFilters(newFilters)
173+
174+
if (conditionRemoved) {
175+
setTimeout(() => filterBarRef.current?.focus(), 0)
176+
}
164177
},
165-
[setFilters]
178+
[filters.length, setFilters]
166179
)
167180

168181
const actions = useMemo(
@@ -199,8 +212,9 @@ export const FilterPopoverNew = ({ isRefetching = false }: FilterPopoverProps) =
199212
) : null
200213

201214
return (
202-
<div className="flex-1 min-w-0">
215+
<div className="flex-1 min-w-0" onFocus={() => onInputFocus?.()} onBlur={() => onInputBlur?.()}>
203216
<FilterBar
217+
ref={filterBarRef}
204218
filterProperties={filterProperties}
205219
filters={filterGroup}
206220
onFilterChange={handleFilterChange}

apps/studio/components/grid/components/header/sort/SortPopoverPrimitive.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,7 @@ export const SortPopoverPrimitive = ({
243243
{displayButtonText}
244244
</Button>
245245
</PopoverTrigger_Shadcn_>
246-
<PopoverContent_Shadcn_ className="p-0 w-96" side="bottom" align="start">
246+
<PopoverContent_Shadcn_ className="p-0 w-96" side="bottom" align="center">
247247
<div className="space-y-2 py-2">
248248
<DndContext
249249
sensors={sensors}

apps/studio/components/interfaces/ConnectButton/ConnectButton.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,14 @@ import { useAppStateSnapshot } from '@/state/app-state'
1212
interface ConnectButtonProps {
1313
buttonType?: ComponentProps<typeof Button>['type']
1414
className?: string
15+
iconOnly?: boolean
1516
}
1617

17-
export const ConnectButton = ({ buttonType = 'default', className }: ConnectButtonProps) => {
18+
export const ConnectButton = ({
19+
buttonType = 'default',
20+
className,
21+
iconOnly = false,
22+
}: ConnectButtonProps) => {
1823
const { data: selectedProject } = useSelectedProjectQuery()
1924
const { setConnectSheetSource } = useAppStateSnapshot()
2025
const isActiveHealthy = selectedProject?.status === PROJECT_STATUS.ACTIVE_HEALTHY
@@ -42,7 +47,7 @@ export const ConnectButton = ({ buttonType = 'default', className }: ConnectButt
4247
},
4348
}}
4449
>
45-
<span>Connect</span>
50+
<span className={cn({ 'sr-only': !iconOnly })}>Connect</span>
4651
</ButtonTooltip>
4752
)
4853
}

apps/studio/components/interfaces/QueryPerformance/IndexAdvisor/EnableIndexAdvisorButton.tsx

Lines changed: 39 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import {
99
AlertDialogFooter,
1010
AlertDialogHeader,
1111
AlertDialogTitle,
12-
AlertDialogTrigger,
1312
Button,
1413
} from 'ui'
1514

@@ -21,10 +20,34 @@ import { useTrack } from '@/lib/telemetry/track'
2120

2221
export const EnableIndexAdvisorButton = () => {
2322
const track = useTrack()
24-
const { data: project } = useSelectedProjectQuery()
25-
2623
const [isDialogOpen, setIsDialogOpen] = useState(false)
2724

25+
return (
26+
<>
27+
<Button
28+
type="primary"
29+
onClick={() => {
30+
setIsDialogOpen(true)
31+
track('index_advisor_banner_enable_button_clicked')
32+
}}
33+
>
34+
Enable
35+
</Button>
36+
<EnableIndexAdvisorDialog open={isDialogOpen} setOpen={setIsDialogOpen} />
37+
</>
38+
)
39+
}
40+
41+
export const EnableIndexAdvisorDialog = ({
42+
open,
43+
setOpen,
44+
}: {
45+
open: boolean
46+
setOpen: (value: boolean) => void
47+
}) => {
48+
const track = useTrack()
49+
const { data: project } = useSelectedProjectQuery()
50+
2851
const { data: extensions } = useDatabaseExtensionsQuery({
2952
projectRef: project?.ref,
3053
connectionString: project?.connectionString,
@@ -60,27 +83,27 @@ export const EnableIndexAdvisorButton = () => {
6083
})
6184
}
6285
toast.success('Successfully enabled Index Advisor!')
63-
setIsDialogOpen(false)
86+
setOpen(false)
6487
} catch (error: any) {
6588
toast.error(`Failed to enable Index Advisor: ${error.message}`)
6689
}
6790
}
6891

6992
return (
70-
<AlertDialog open={isDialogOpen} onOpenChange={() => setIsDialogOpen(!isDialogOpen)}>
71-
<AlertDialogTrigger asChild>
72-
<Button type="primary" onClick={() => track('index_advisor_banner_enable_button_clicked')}>
73-
Enable
74-
</Button>
75-
</AlertDialogTrigger>
76-
77-
<AlertDialogContent>
93+
<AlertDialog open={open} onOpenChange={() => setOpen(!open)}>
94+
<AlertDialogContent size="medium">
7895
<AlertDialogHeader>
7996
<AlertDialogTitle>Enable Index Advisor</AlertDialogTitle>
80-
<AlertDialogDescription>
81-
This will enable the <code className="text-code-inline">index_advisor</code> and{' '}
82-
<code className="text-code-inline">hypopg</code> Postgres extensions so Index Advisor
83-
can analyse queries and suggest performance-improving indexes.
97+
<AlertDialogDescription className="flex flex-col gap-y-2">
98+
<p>
99+
The Index Advisor recommends indexes to improve query performance on your tables based
100+
on your actual query patterns.
101+
</p>
102+
<p>
103+
Enable this will install the <code className="text-code-inline">index_advisor</code>{' '}
104+
and <code className="text-code-inline">hypopg</code> Postgres extensions so Index
105+
Advisor can analyse queries and suggest performance-improving indexes.
106+
</p>
84107
</AlertDialogDescription>
85108
</AlertDialogHeader>
86109
<AlertDialogFooter>

0 commit comments

Comments
 (0)