mirror of
https://github.com/supabase/supabase.git
synced 2026-10-09 11:25:06 +03:00
Five layout fixes across Explorer chat, notebooks, and the sidebar. ### Chat - **Conversation fade overlapped the scrollbar.** The top and bottom gradients are positioned against the conversation's padding box, which includes the scroll container's scrollbar gutter, so `inset-x-0` painted them over the scrollbar. They now stop at the conversation's content gutter, which `Conversation` owns for both the content and the fades. - **Composer background bled past the input's radius.** The form paints the surface behind the textarea but had no radius of its own, so its square corners showed outside the `rounded-lg` input. It now shares the radius. - **Message parts used two different widths.** Wide parts come down to `max-w-3xl` so every part shares a column, matching `AssistantQueryCell` and `AssistantNotebookPreview`. `isWide` / `isWideMessagePart` stay in place with both widths equal, so a part can diverge again later without rebuilding the mechanism. ### Notebooks - **Cell controls sat at the container edge.** Each cell centred itself at its own max width while the grip and add-cell button stayed at the far left of the full-width row, leaving a large gap. `SortableSection` takes a `sectionWidth` and carries its control gutter twice — once as the controls, once as padding on the other side — so the section stays centred with its controls immediately beside it. Cell widths are unchanged (prose `48rem`, query `72rem`); set them equal and the two cell types' controls line up on their own. The controls stay in flow rather than floating in an outside gutter, so on a viewport narrower than the cap the row just fills the space instead of clipping the controls into the padding. ### Sidebar - **Search icon didn't line up with the menu row icons.** The row box already sits flush with the search input's box, so rows moved from `pl-3` to `pl-2` to put their icons on the same 8px offset the search icon uses. Spacing between the input and the list now matches the 12px side padding. ### Testing `pnpm --filter studio run typecheck`, Prettier, and 378 tests across `Explorer`, `ProjectHome`, `AIAssistantPanel`, and `ExplorerLayout` pass. ESLint warning counts are unchanged from master. These were reasoned from layout rather than checked in a browser, so they're worth a look on a preview before merge. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **UI Improvements** * Updated Explorer layouts with flexible, configurable widths for notebook and query sections. * Refined navigation spacing and padding across Explorer views. * Centered and standardized AI Assistant preview, query, and message content widths. * Improved chat form styling with rounded corners. * Adjusted conversation spacing and fade overlays to avoid overlapping the scrollbar. * Preserved full-width behavior where appropriate while keeping controls aligned. * **Tests** * Updated layout tests to reflect revised width and alignment behavior. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
153 lines
5.5 KiB
TypeScript
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,
|
|
shouldInvalidateResultOnSourceChange,
|
|
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 run against the old source (backend or time range) does not.
|
|
if (shouldInvalidateResultOnSourceChange(cell, source)) 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}
|
|
sectionWidth="72rem"
|
|
actions={<AddCellDropdown cellId={cell._id} />}
|
|
gripDropdownContent={<MoveCellDropdownContent cellId={cell._id} />}
|
|
gripClassName="mt-2 sm:opacity-0 group-hover:opacity-100 has-[[data-state=open]]:opacity-100 transition"
|
|
>
|
|
<QueryEditor
|
|
ref={ref}
|
|
id={cell._id}
|
|
variant="embedded"
|
|
className="min-h-0"
|
|
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>
|
|
)
|
|
})
|