feat(sql-editor): consolidate controls into a single top bar (#47792)

## I have read the
[CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md)
file.

YES

## What kind of change does this PR introduce?

Refactor / UI improvement

## What is the current behavior?

SQL editor controls are split across two rows: a bar between the editor
and results (tabs, download,
run/save/format/db-selector/role/favorite/intellisense) and a separate
footer at the very bottom (row count + limit dropdown).

## What is the new behavior?

All controls are consolidated into a single top bar above the editor (⋮
overflow menu, database selector, role impersonation, limit, save, run).
The bar between the editor and results now holds only result-relevant
information: the Results/Explain/Chart tabs, the row-count summary, and
the Export button. The limit dropdown is now always visible as a
persistent query setting.
This commit is contained in:
Charis authored and GitHub committed 2026-07-10 10:27:43 -04:00
1 parent 93ebd8adf1
commit 3461d60aba
3 files changed
+82 -100

No files matched your search

@@ -9,32 +9,17 @@ import {
import { wrapWithRollback } from '@supabase/pg-meta/src/query'
import { useQueryClient } from '@tanstack/react-query'
import { IS_PLATFORM, LOCAL_STORAGE_KEYS, useFlag, useParams } from 'common'
import { ChevronUp, Loader2 } from 'lucide-react'
import { Loader2 } from 'lucide-react'
import dynamic from 'next/dynamic'
import { useRouter } from 'next/router'
import { useCallback, useEffect, useEffectEvent, useMemo, useRef, useState } from 'react'
import { toast } from 'sonner'
import {
Button,
cn,
DropdownMenu,
DropdownMenuContent,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuTrigger,
ResizableHandle,
ResizablePanel,
ResizablePanelGroup,
Tooltip,
TooltipContent,
TooltipTrigger,
} from 'ui'
import { cn, ResizableHandle, ResizablePanel, ResizablePanelGroup } from 'ui'
import { useSqlEditorDiff, useSqlEditorPrompt } from './hooks'
import { RunQueryWarningModal } from './RunQueryWarningModal'
import {
generateSnippetTitle,
ROWS_PER_PAGE_OPTIONS,
sqlAiDisclaimerComment,
untitledSnippetTitle,
} from './SQLEditor.constants'
@@ -57,6 +42,7 @@ import {
suffixWithLimit,
} from './SQLEditor.utils'
import { useAddDefinitions } from './useAddDefinitions'
import { UtilityActions } from './UtilityPanel/UtilityActions'
import { UtilityPanel } from './UtilityPanel/UtilityPanel'
import {
isExplainQuery,
@@ -65,7 +51,6 @@ import {
} from '@/components/interfaces/ExplainVisualizer/ExplainVisualizer.utils'
import { SIDEBAR_KEYS } from '@/components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider'
import ResizableAIWidget from '@/components/ui/AIEditor/ResizableAIWidget'
import { GridFooter } from '@/components/ui/GridFooter'
import { useSqlTitleGenerateMutation } from '@/data/ai/sql-title-mutation'
import { useDatabaseEventTriggersQuery } from '@/data/database-event-triggers/database-event-triggers-query'
import { constructHeaders, isValidConnString } from '@/data/fetchers'
@@ -207,7 +192,6 @@ export const SQLEditor = () => {
const id = !urlId || urlId === 'new' ? generatedId : urlId
const limit = sessionSnap.limit
const results = sessionSnap.results[id]?.[0]
const snippetIsLoading = !(
id in snapV2.snippets && snapV2.snippets[id].snippet.content !== undefined
)
@@ -916,9 +900,18 @@ export const SQLEditor = () => {
}}
/>
<div className="flex h-full">
<div className="flex flex-col h-full">
<UtilityActions
id={id}
isExecuting={isExecuting}
isDisabled={isDiffOpen}
hasSelection={hasSelection}
prettifyQuery={prettifyQuery}
executeQuery={executeQueryFromButton}
className="px-4 min-h-[42px] border-b shrink-0"
/>
<ResizablePanelGroup
className="relative"
className="relative flex-1 min-h-0"
orientation="vertical"
autoSaveId={LOCAL_STORAGE_KEYS.SQL_EDITOR_SPLIT_SIZE}
>
@@ -1047,9 +1040,6 @@ export const SQLEditor = () => {
isExecuting={isExecuting}
isExplainExecuting={isExplainExecuting}
isDisabled={isDiffOpen}
hasSelection={hasSelection}
prettifyQuery={prettifyQuery}
executeQuery={executeQueryFromButton}
executeExplainQuery={executeExplainQuery}
showExplainTab={!disablePrettyExplain}
onDebug={onDebug}
@@ -1059,63 +1049,6 @@ export const SQLEditor = () => {
/>
)}
</ResizablePanel>
<div className="h-9">
{results?.rows !== undefined && !isExecuting && (
<GridFooter className="flex items-center justify-between gap-2">
<Tooltip>
<TooltipTrigger>
<p className="text-xs">
<span className="text-foreground">
{results.rows.length} row{results.rows.length > 1 ? 's' : ''}
</span>
<span className="text-foreground-lighter ml-1">
{results.autoLimit !== undefined &&
` (Limited to only ${results.autoLimit} rows)`}
</span>
</p>
</TooltipTrigger>
<TooltipContent className="max-w-xs">
<p className="flex flex-col gap-y-1">
<span>
Results are automatically limited to preserve browser performance, in
particular if your query returns an exceptionally large number of rows.
</span>
<span className="text-foreground-light">
You may change or remove this limit from the dropdown on the right
</span>
</p>
</TooltipContent>
</Tooltip>
{results.autoLimit !== undefined && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="default" iconRight={<ChevronUp size={14} />}>
Limit results to:{' '}
{
ROWS_PER_PAGE_OPTIONS.find((opt) => opt.value === sessionSnap.limit)
?.label
}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-40" align="end">
<DropdownMenuRadioGroup
value={sessionSnap.limit.toString()}
onValueChange={(val) => sessionSnap.setLimit(Number(val))}
>
{ROWS_PER_PAGE_OPTIONS.map((option) => (
<DropdownMenuRadioItem key={option.label} value={option.value.toString()}>
{option.label}
</DropdownMenuRadioItem>
))}
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
)}
</GridFooter>
)}
</div>
</ResizablePanelGroup>
</div>
</>
@@ -1,6 +1,6 @@
import { Hotkey } from '@tanstack/react-hotkeys'
import { LOCAL_STORAGE_KEYS, useParams } from 'common'
import { AlignLeft, Check, Heart, Keyboard, MoreVertical } from 'lucide-react'
import { AlignLeft, Check, ChevronDown, Heart, Keyboard, MoreVertical } from 'lucide-react'
import { toast } from 'sonner'
import {
Button,
@@ -8,6 +8,8 @@ import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
KeyboardShortcut,
@@ -16,6 +18,7 @@ import {
TooltipTrigger,
} from 'ui'
import { ROWS_PER_PAGE_OPTIONS } from '../SQLEditor.constants'
import { AutosaveStatus } from './AutosaveStatus'
import { SqlRunButton } from './RunButton'
import { SqlSaveButton } from './SaveButton'
@@ -37,6 +40,7 @@ export type UtilityActionsProps = {
hasSelection?: boolean
prettifyQuery: () => void
executeQuery: () => void
className?: string
}
export const UtilityActions = ({
@@ -46,6 +50,7 @@ export const UtilityActions = ({
hasSelection = false,
prettifyQuery,
executeQuery,
className,
}: UtilityActionsProps) => {
const { ref } = useParams()
const snapV2 = useSqlEditorV2StateSnapshot()
@@ -86,7 +91,7 @@ export const UtilityActions = ({
}
return (
<div className="inline-flex items-center justify-end gap-x-2">
<div className={cn('flex items-center justify-end gap-x-2', className)}>
<AutosaveStatus id={id} />
{/* SavingIndicator reports auto-save progress (spinner/checkmark). In manual
mode AutosaveStatus + the Save button own the status, so hide it there. */}
@@ -233,6 +238,30 @@ export const UtilityActions = ({
/>
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="default"
iconRight={<ChevronDown size={14} className="text-foreground-light" />}
>
<span className="text-foreground-light">Limit</span>{' '}
{ROWS_PER_PAGE_OPTIONS.find((opt) => opt.value === sessionSnap.limit)?.label}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-40" align="end">
<DropdownMenuRadioGroup
value={sessionSnap.limit.toString()}
onValueChange={(val) => sessionSnap.setLimit(Number(val))}
>
{ROWS_PER_PAGE_OPTIONS.map((option) => (
<DropdownMenuRadioItem key={option.label} value={option.value.toString()}>
{option.label}
</DropdownMenuRadioItem>
))}
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
<div className="flex items-center">
{isManualSaveEnabled && <SqlSaveButton id={id} className="rounded-r-none" />}
<SqlRunButton
@@ -1,9 +1,16 @@
import { useParams } from 'common'
import { toast } from 'sonner'
import { Tabs, TabsContent, TabsList, TabsTrigger } from 'ui'
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
Tooltip,
TooltipContent,
TooltipTrigger,
} from 'ui'
import { ChartConfig } from './ChartConfig'
import { UtilityActions } from './UtilityActions'
import { UtilityTabExplain } from './UtilityTabExplain'
import { UtilityTabResults } from './UtilityTabResults'
import { DownloadResultsButton } from '@/components/ui/DownloadResultsButton'
@@ -19,9 +26,6 @@ export type UtilityPanelProps = {
isExplainExecuting?: boolean
isDebugging?: boolean
isDisabled?: boolean
hasSelection: boolean
prettifyQuery: () => void
executeQuery: () => void
executeExplainQuery: () => void
showExplainTab?: boolean
onDebug: () => void
@@ -45,9 +49,6 @@ export const UtilityPanel = ({
isExplainExecuting,
isDebugging,
isDisabled,
hasSelection,
prettifyQuery,
executeQuery,
executeExplainQuery,
showExplainTab = true,
onDebug,
@@ -144,6 +145,34 @@ export const UtilityPanel = ({
<TabsTrigger className="py-3 text-xs" value="chart">
<span className="translate-y-px">Chart</span>
</TabsTrigger>
</div>
<div className="flex items-center gap-4">
{result?.rows !== undefined && !isExecuting && (
<Tooltip>
<TooltipTrigger>
<p className="text-xs">
<span className="text-foreground">
{result.rows.length} row{result.rows.length === 1 ? '' : 's'}
</span>
<span className="text-foreground-lighter ml-1">
{result.autoLimit !== undefined && `(Limited to only ${result.autoLimit} rows)`}
</span>
</p>
</TooltipTrigger>
<TooltipContent className="max-w-xs">
<p className="flex flex-col gap-y-1">
<span>
Results are automatically limited to preserve browser performance, in particular
if your query returns an exceptionally large number of rows.
</span>
<span className="text-foreground-light">
You may change or remove this limit from the toolbar above.
</span>
</p>
</TooltipContent>
</Tooltip>
)}
{result?.rows && (
<DownloadResultsButton
@@ -157,15 +186,6 @@ export const UtilityPanel = ({
/>
)}
</div>
<UtilityActions
id={id}
isExecuting={isExecuting}
isDisabled={isDisabled}
hasSelection={hasSelection}
prettifyQuery={prettifyQuery}
executeQuery={executeQuery}
/>
</TabsList>
<TabsContent asChild value="results" className="mt-0 grow">