From 4dcbf06aee442ada0b1ce8bbf0ce3865486d7654 Mon Sep 17 00:00:00 2001 From: shaishab316 Date: Sat, 4 Apr 2026 16:30:13 +0600 Subject: [PATCH 1/2] fix(sql): keep SQL editor scrollable so the header stays visible Constrain the SQL panel with flex + overflow so CodeMirror scrolls internally instead of growing the view. Fixes prisma/studio#1480. --- ui/studio/views/sql/SqlView.test.tsx | 17 ++++ ui/studio/views/sql/SqlView.tsx | 117 +++++++++++++++------------ 2 files changed, 81 insertions(+), 53 deletions(-) diff --git a/ui/studio/views/sql/SqlView.test.tsx b/ui/studio/views/sql/SqlView.test.tsx index c18a3884..827945a8 100644 --- a/ui/studio/views/sql/SqlView.test.tsx +++ b/ui/studio/views/sql/SqlView.test.tsx @@ -1259,6 +1259,23 @@ describe("SqlView", () => { harness.cleanup(); }); + it("keeps the SQL editor in a bounded scroll region for long scripts", () => { + const { adapter } = createAdapterMock(); + const studio = createStudioMock(adapter); + useStudioMock.mockReturnValue(studio); + + const harness = renderSqlView(); + + const scrollRegion = harness.container.querySelector( + '[data-testid="sql-editor-scroll-container"]', + ); + expect(scrollRegion).toBeTruthy(); + expect(scrollRegion?.className).toContain("min-h-0"); + expect(scrollRegion?.className).toContain("overflow-hidden"); + + harness.cleanup(); + }); + it("supports cancelling a running query", async () => { const raw: Adapter["raw"] = async (_details, options) => { return await new Promise((resolve) => { diff --git a/ui/studio/views/sql/SqlView.tsx b/ui/studio/views/sql/SqlView.tsx index 904ec5ac..9c2cd640 100644 --- a/ui/studio/views/sql/SqlView.tsx +++ b/ui/studio/views/sql/SqlView.tsx @@ -24,6 +24,7 @@ import { createSqlEditorSchemaFromIntrospection } from "../../../../data/sql-edi import { getTopLevelSqlStatementAtCursor } from "../../../../data/sql-statements"; import { Button } from "../../../components/ui/button"; import { Input } from "../../../components/ui/input"; +import { cn } from "../../../lib/utils"; import { TableHead, TableRow } from "../../../components/ui/table"; import { useColumnPinning } from "../../../hooks/use-column-pinning"; import { useIntrospection } from "../../../hooks/use-introspection"; @@ -875,41 +876,47 @@ export function SqlView(_props: ViewProps) { ) : null} -
-
- { - editorViewRef.current = view; - const cursorIndex = view.state.doc.length; - view.dispatch({ - selection: { - anchor: cursorIndex, - head: cursorIndex, - }, - }); - view.focus(); - }} - onChange={(value) => { - hasUserEditedEditorValueRef.current = true; - latestEditorValueRef.current = value; - setEditorValue(value); - }} - placeholder="Write SQL..." - theme={isDarkMode ? "dark" : "light"} - value={editorValue} - /> -
+
+
+
+ { + editorViewRef.current = view; + const cursorIndex = view.state.doc.length; + view.dispatch({ + selection: { + anchor: cursorIndex, + head: cursorIndex, + }, + }); + view.focus(); + }} + onChange={(value) => { + hasUserEditedEditorValueRef.current = true; + latestEditorValueRef.current = value; + setEditorValue(value); + }} + placeholder="Write SQL..." + theme={isDarkMode ? "dark" : "light"} + value={editorValue} + /> +
{aiGenerationErrorMessage ? (
AI SQL generation error: {aiGenerationErrorMessage} @@ -969,25 +976,29 @@ export function SqlView(_props: ViewProps) { ) : null}
) : null} -
+
-
- {result == null ? null : ( - - )} +
+ {result == null ? null : ( + + )} +
); From a28e5f5eba7d67ffa713fa515fd6647f4b10b7b0 Mon Sep 17 00:00:00 2001 From: shaishab316 Date: Sun, 19 Jul 2026 11:37:06 +0600 Subject: [PATCH 2/2] fix(sql): cap editor height with maxHeight instead of splitting view Address review feedback: instead of evenly splitting the SQL panel between editor and result grid, keep the editor auto-sized to content (capped at 40vh) so short queries don't leave a mostly empty editor pane above the results. --- .changeset/bright-bears-run.md | 5 + Architecture/sql-view.md | 2 + FEATURES.md | 1 + ui/studio/views/sql/SqlView.test.tsx | 65 +++++++++++++ ui/studio/views/sql/SqlView.tsx | 133 +++++++++++++++------------ 5 files changed, 145 insertions(+), 61 deletions(-) create mode 100644 .changeset/bright-bears-run.md diff --git a/.changeset/bright-bears-run.md b/.changeset/bright-bears-run.md new file mode 100644 index 00000000..8eed28ac --- /dev/null +++ b/.changeset/bright-bears-run.md @@ -0,0 +1,5 @@ +--- +'@prisma/studio-core': patch +--- + +Keep the SQL editor content-sized until results are shown, then cap its height so long scripts scroll internally and the result grid can use the remaining space. \ No newline at end of file diff --git a/Architecture/sql-view.md b/Architecture/sql-view.md index 8766de9d..5554ddb3 100644 --- a/Architecture/sql-view.md +++ b/Architecture/sql-view.md @@ -47,6 +47,7 @@ SQL result visualization is governed by: - SQL result grid MUST keep column pinning enabled and URL-backed using existing `pin` navigation state. - SQL execution MUST remain cancellable via `AbortController`. - SQL editor lines MUST soft-wrap within the available editor width instead of forcing page-level horizontal overflow while typing long queries. +- When SQL results are visible, the editor pane MUST stay content-sized up to a bounded height and then scroll internally, rather than splitting the viewport evenly with the result grid. ## Result Rendering Contract @@ -97,3 +98,4 @@ Changes to SQL view MUST include tests for: - read-only grid rendering (pin control present, sorting controls disabled) - absence of history UI - absence of pagination controls in SQL result grid +- bounded editor height when results are visible diff --git a/FEATURES.md b/FEATURES.md index 75d25a96..8e79a773 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -122,6 +122,7 @@ The same lint transport also validates saved table-level SQL filter pills in the Keyboard execution supports `Cmd/Ctrl+Enter`, and in multi-statement scripts it runs only the top-level statement at the current cursor. Large SQL result sets stay responsive while you keep editing the query because result-grid rendering is isolated from editor keystrokes unless the executed result itself changes. Long SQL lines wrap inside the editor instead of stretching the overall page wider, so writing large queries stays readable on narrow viewports. +When a query returns results, the editor keeps its content-sized height until it reaches a viewport cap, then scrolls internally so the result grid can use the remaining space. ## AI SQL Generation diff --git a/ui/studio/views/sql/SqlView.test.tsx b/ui/studio/views/sql/SqlView.test.tsx index 827945a8..1f95634f 100644 --- a/ui/studio/views/sql/SqlView.test.tsx +++ b/ui/studio/views/sql/SqlView.test.tsx @@ -17,6 +17,9 @@ const setPinnedColumnIdsMock = vi.fn(); let mockEditorCursorHead = 0; let mockEditorDocLength = 0; let mockEditorDocText = ""; +let mockCodeMirrorProps: + | { height?: string; maxHeight?: string; minHeight?: string } + | undefined; let mockCodeMirrorOnChange: ((value: string) => void) | undefined; let mockCodeMirrorExtensions: unknown[] = []; const mockEditorDispatch = vi.fn((transaction: unknown) => { @@ -106,7 +109,10 @@ vi.mock("../../../hooks/use-navigation", () => ({ vi.mock("@uiw/react-codemirror", () => ({ default: (props: { "aria-label"?: string; + height?: string; extensions?: unknown[]; + maxHeight?: string; + minHeight?: string; onCreateEditor?: (view: { dispatch: (transaction: unknown) => void; focus: () => void; @@ -118,6 +124,11 @@ vi.mock("@uiw/react-codemirror", () => ({ onChange?: (value: string) => void; value?: string; }) => { + mockCodeMirrorProps = { + height: props.height, + maxHeight: props.maxHeight, + minHeight: props.minHeight, + }; mockCodeMirrorOnChange = props.onChange; mockCodeMirrorExtensions = props.extensions ?? []; mockEditorDocLength = (props.value ?? "").length; @@ -398,6 +409,7 @@ afterEach(() => { mockEditorDocText = ""; mockCodeMirrorOnChange = undefined; mockCodeMirrorExtensions = []; + mockCodeMirrorProps = undefined; }); beforeEach(() => { @@ -1276,6 +1288,59 @@ describe("SqlView", () => { harness.cleanup(); }); + it("keeps the editor content-sized until results are shown, then caps it", async () => { + const { adapter } = createAdapterMock(); + const studio = createStudioMock(adapter); + useStudioMock.mockReturnValue(studio); + + const harness = renderSqlView(); + + expect( + harness.container + .querySelector('[data-testid="sql-editor-scroll-container"]') + ?.className, + ).toContain("flex-1"); + expect(mockCodeMirrorProps).toMatchObject({ + height: "100%", + maxHeight: undefined, + }); + + const runButton = [...harness.container.querySelectorAll("button")].find( + (button) => button.textContent?.includes("Run SQL"), + ); + + if (!runButton) { + throw new Error("SQL view run control not rendered"); + } + + act(() => { + runButton.dispatchEvent(new MouseEvent("click", { bubbles: true })); + }); + + await waitFor(() => { + return ( + harness.container.textContent?.includes("1 row(s) returned") ?? false + ); + }); + + expect( + harness.container + .querySelector('[data-testid="sql-editor-scroll-container"]') + ?.className, + ).toContain("flex-none"); + expect( + harness.container + .querySelector('[data-testid="sql-result-grid-container"]') + ?.className, + ).toContain("flex-1"); + expect(mockCodeMirrorProps).toMatchObject({ + height: undefined, + maxHeight: "40vh", + }); + + harness.cleanup(); + }); + it("supports cancelling a running query", async () => { const raw: Adapter["raw"] = async (_details, options) => { return await new Promise((resolve) => { diff --git a/ui/studio/views/sql/SqlView.tsx b/ui/studio/views/sql/SqlView.tsx index 9c2cd640..6f3951b1 100644 --- a/ui/studio/views/sql/SqlView.tsx +++ b/ui/studio/views/sql/SqlView.tsx @@ -24,11 +24,11 @@ import { createSqlEditorSchemaFromIntrospection } from "../../../../data/sql-edi import { getTopLevelSqlStatementAtCursor } from "../../../../data/sql-statements"; import { Button } from "../../../components/ui/button"; import { Input } from "../../../components/ui/input"; -import { cn } from "../../../lib/utils"; import { TableHead, TableRow } from "../../../components/ui/table"; import { useColumnPinning } from "../../../hooks/use-column-pinning"; import { useIntrospection } from "../../../hooks/use-introspection"; import { useNavigation } from "../../../hooks/use-navigation"; +import { cn } from "../../../lib/utils"; import type { CellProps } from "../../cell/Cell"; import { Cell } from "../../cell/Cell"; import { getCell } from "../../cell/get-cell"; @@ -557,6 +557,8 @@ export function SqlView(_props: ViewProps) { resetKey: visualizationResetKey, rows: result?.rows ?? EMPTY_SQL_RESULT_ROWS, }); + const hasResult = result != null; + const editorMaxHeight = hasResult ? "40vh" : undefined; async function runSqlRequest(args: { aiQueryRequest?: string | null; @@ -877,9 +879,17 @@ export function SqlView(_props: ViewProps) {
-
+
{ editorViewRef.current = view; const cursorIndex = view.state.doc.length; @@ -917,65 +928,65 @@ export function SqlView(_props: ViewProps) { value={editorValue} />
- {aiGenerationErrorMessage ? ( -
- AI SQL generation error: {aiGenerationErrorMessage} -
- ) : null} - {aiGenerationRationale ? ( -
- AI rationale: {aiGenerationRationale} -
- ) : null} - {errorMessage ? ( -
- Query error: {errorMessage} -
- ) : null} - {result ? ( -
-
- {result.rowCount} row(s) returned in {result.durationMs}ms + {aiGenerationErrorMessage ? ( +
+ AI SQL generation error: {aiGenerationErrorMessage}
- {hasAiSql ? ( -
- {visualization.state.status === "idle" && - visualization.canGenerate ? ( - - ) : null} - {visualization.state.status === "loading" ? ( -
- - Generating graph... -
- ) : null} - {visualization.state.status === "error" ? ( -
- {visualization.state.message} -
- ) : null} + ) : null} + {aiGenerationRationale ? ( +
+ AI rationale: {aiGenerationRationale} +
+ ) : null} + {errorMessage ? ( +
+ Query error: {errorMessage} +
+ ) : null} + {result ? ( +
+
+ {result.rowCount} row(s) returned in {result.durationMs}ms
- ) : null} -
- ) : null} + {hasAiSql ? ( +
+ {visualization.state.status === "idle" && + visualization.canGenerate ? ( + + ) : null} + {visualization.state.status === "loading" ? ( +
+ + Generating graph... +
+ ) : null} + {visualization.state.status === "error" ? ( +
+ {visualization.state.message} +
+ ) : null} +
+ ) : null} +
+ ) : null}