Files
supabase/apps/studio/components/interfaces/Explorer/QueryCell/index.tsx
Saxon Fletcher 4ee43f3585 chore(studio): refine Explorer query UI (#49895)
## 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?

UI refinements for Explorer query surfaces.

## What is the current behavior?

- The assistant chat textarea uses a tighter radius than the Run SQL /
Create a notebook cards on Explorer home.
- Chart results sit unevenly in the results pane because axis gutters
stack on card padding, and long Y labels can clip.
- Selecting SQL in the editor changes the primary Run button to Run
selected, which is easy to trigger by accident.
- The Prettify SQL icon in notebook query cells uses Lucide's default
size, so it doesn't match other toolbar actions.

## What is the new behavior?

- Assistant chat form uses `rounded-lg` so it matches the home action
cards everywhere the form is used.
- Query result charts collapse unused axis space, add a little padding
when labels are on, and size the Y axis from formatted ticks so longer
labels fit.
- Run is a default split button that always executes the full query. Run
selected is a secondary menu item, disabled until SQL is selected.
- Notebook cell Prettify SQL icons use `size={16}` and `strokeWidth={2}`
like the rest of the Explorer toolbar.

## Additional context

Cmd+Enter in the editor still runs the current selection when there is
one.

## Test plan

- [ ] Open Explorer home and confirm the assistant chat radius matches
the Run SQL and Create a notebook cards.
- [ ] Run a query, switch to chart view, and check spacing with labels
off and on, including large Y values.
- [ ] With no selection, click Run and confirm the full query runs. Open
the split menu and confirm Run selected is disabled.
- [ ] Select SQL, click Run, and confirm the full query still runs. Use
Run selected from the menu to run only the selection.
- [ ] In a notebook query cell, confirm Prettify SQL matches the size
and stroke of nearby toolbar icons.


Made with [Cursor](https://cursor.com)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- **New Features**
- Added a split Run control in the query editor, with separate actions
for running all content or only selected text.
  - Added support for customizing chart X-axis display settings.
- Improved chart Y-axis sizing, scaling, and tick formatting for clearer
results.

- **Bug Fixes**
  - The “Run selected” action is unavailable when no text is selected.

- **Style**
- Updated toolbar icon sizing and added rounded corners to the assistant
chat input.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-09-03 09:44:45 +10:00

153 lines
5.5 KiB
TypeScript

import { AlignLeft } from 'lucide-react'
import { forwardRef, useState } from 'react'
import { KeyboardShortcut } from 'ui'
import { type Snapshot } from 'valtio'
import { AddCellDropdown } from '../AddCellDropdown'
import { ExplorerToolbarAction } from '../ExplorerToolbar'
import { MoveCellDropdownContent } from '../MoveCellDropdownContent'
import { QueryEditor, type QueryEditorHandle } from '../QueryEditor'
import { type QueryDisplay, type QueryResult } from '../types'
import {
changeCellSource,
cloneChartConfig,
cloneQueryCell,
getCellDisplay,
setCellRowLimit,
setCellSql,
toQueryModel,
} from './QueryCell.utils'
import { SortableSection } from '@/components/ui/SortableSection'
import {
isQueryCell,
type QueryCell as QueryCellSchema,
} from '@/data/content/notebooks/notebook-schema'
import { type QuerySourceBinding } from '@/data/query-sources/query-source-registry'
import { useCurrentNotebook, useNotebooksStateSnapshot } from '@/state/notebooks/notebooks-state'
import { useLocalRoleImpersonationState } from '@/state/role-impersonation-state'
import { hotkeyToKeys } from '@/state/shortcuts/formatShortcut'
import { SHORTCUT_DEFINITIONS, SHORTCUT_IDS } from '@/state/shortcuts/registry'
const PRETTIFY_SHORTCUT_KEYS = hotkeyToKeys(
SHORTCUT_DEFINITIONS[SHORTCUT_IDS.SQL_EDITOR_FORMAT].sequence[0]
)
interface QueryCellProps {
cell: Snapshot<QueryCellSchema>
onEdit?: () => void
onPrettifyQuery?: () => void
}
/** Notebook adapter around the shared QueryEditor. */
export const QueryCell = forwardRef<QueryEditorHandle, QueryCellProps>(function QueryCell(
{ cell, onEdit, onPrettifyQuery },
ref
) {
const snap = useNotebooksStateSnapshot()
const currentNotebook = useCurrentNotebook()
const [sql, setSql] = useState<string>(cell.unchecked_sql)
const [result, setResult] = useState<QueryResult>()
const roleImpersonationState = useLocalRoleImpersonationState()
const title = cell.title ?? 'Untitled query'
const showQuery =
snap.cellLocalState.get(cell._id)?.showQuery ?? currentNotebook?.status === 'new'
/**
* Applies an update to this cell. The updater runs against the cell as the store holds
* it rather than the snapshot this component rendered with, so a concurrent edit isn't
* clobbered; `isQueryCell` keeps the per-backend helpers off a markdown cell that
* somehow shares the id.
*/
const updateQueryCell = (updater: (candidate: Snapshot<QueryCellSchema>) => QueryCellSchema) => {
const notebookId = currentNotebook?.notebook.id
if (!notebookId) return
onEdit?.()
snap.updateCell({
id: notebookId,
cellId: cell._id,
updater: (candidate) => {
if (!isQueryCell(candidate)) return candidate
return updater(candidate)
},
})
}
const handleSourceChange = (source: QuerySourceBinding) => {
// The query text carries over (see `changeCellSource`), so the editor's buffer stays
// valid — but a result the old backend produced does not, since another engine
// returns unrelated columns.
const isBackendChange = (source._tag === 'logs') !== (cell._tag === 'log_cell')
if (isBackendChange) setResult(undefined)
updateQueryCell((candidate) => changeCellSource(candidate, source))
}
const handleTitleChange = (value: string) => {
const nextTitle = value.trim()
if (!nextTitle) return
updateQueryCell((candidate) => ({ ...cloneQueryCell(candidate), title: nextTitle }))
}
// Running a cell re-commits its current SQL (see QueryEditor's handleRunQuery) even when
// nothing changed — skip the store write so that doesn't spuriously mark the notebook
// unsaved.
const handleSqlCommit = (value: string) => {
if (value === cell.unchecked_sql) return
updateQueryCell((candidate) => setCellSql(candidate, value))
}
const handleDisplayChange = (display: QueryDisplay) =>
updateQueryCell((candidate) => ({
...cloneQueryCell(candidate),
view: display.view,
chart: cloneChartConfig(display.chart),
}))
const handleRowLimitChange = (rowLimit: number) =>
updateQueryCell((candidate) => setCellRowLimit(candidate, rowLimit))
return (
<SortableSection
id={cell._id}
actions={<AddCellDropdown cellId={cell._id} />}
gripDropdownContent={<MoveCellDropdownContent cellId={cell._id} />}
gripClassName="mt-2 opacity-0 group-hover:opacity-100 has-[[data-state=open]]:opacity-100 transition"
>
<QueryEditor
ref={ref}
id={cell._id}
variant="embedded"
title={title}
query={toQueryModel(cell, sql)}
result={result}
showQuery={showQuery}
onShowQueryChange={(showQuery) => snap.setQueryVisibility({ cellId: cell._id, showQuery })}
roleImpersonationState={roleImpersonationState}
display={getCellDisplay(cell)}
onTitleChange={handleTitleChange}
onSqlChange={setSql}
onSqlCommit={handleSqlCommit}
onSourceChange={handleSourceChange}
onResultChange={setResult}
onRowLimitChange={handleRowLimitChange}
onDisplayChange={handleDisplayChange}
toolbarActions={
<ExplorerToolbarAction
icon={<AlignLeft size={16} strokeWidth={2} />}
tooltip={
<div className="flex items-center gap-2.5">
<span>Prettify SQL</span>
<KeyboardShortcut keys={PRETTIFY_SHORTCUT_KEYS} />
</div>
}
onClick={onPrettifyQuery}
/>
}
/>
</SortableSection>
)
})