Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
d44b822
refactor: use semantic notebook pane dimensions
emrberk Sep 1, 2026
832283b
refactor: single viewport-independent notebook cell pane preference
emrberk Sep 2, 2026
a94adff
refactor: derive cell panes synchronously and fix the markdown pane c…
emrberk Sep 2, 2026
057faf0
remove compact tier and simplify tools
emrberk Sep 3, 2026
43d2e16
Merge remote-tracking branch 'origin/main' into refactor/notebook-dim…
emrberk Sep 3, 2026
a877c47
reviews
emrberk Sep 4, 2026
88c0363
review
emrberk Sep 4, 2026
e3a08fe
address reviews
emrberk Sep 6, 2026
e05d6f9
reviews
emrberk Sep 6, 2026
6cb6be0
address reviews/race conditions
emrberk Sep 6, 2026
02aa5a4
fix agent path issues and run conflicts
emrberk Sep 7, 2026
739a698
resizer a11y, invalid mode values
emrberk Sep 7, 2026
c2f960a
cell stop button, cancel markers, deletion reason, snapshot cleanup a…
emrberk Sep 7, 2026
a36aa4e
drop statement identity caches, chart identity and per-statement sett…
emrberk Sep 8, 2026
61fd657
markdown topheight fix
emrberk Sep 8, 2026
2f665e9
submodule
emrberk Sep 8, 2026
c57de43
Merge remote-tracking branch 'origin/main' into refactor/notebook-dim…
emrberk Sep 8, 2026
1aa1eda
Merge remote-tracking branch 'origin/main' into refactor/notebook-dim…
emrberk Sep 10, 2026
8a8d3e2
fix type error
emrberk Sep 10, 2026
2d54e05
Merge remote-tracking branch 'origin/main' into refactor/notebook-dim…
emrberk Sep 23, 2026
5befdcf
arrange the toggle button with theme changes
emrberk Sep 23, 2026
8a84364
separate editor vs result height in user path
emrberk Sep 24, 2026
66e32bf
fix: statement identity, chart fetch times, legacy keys, split drag, …
emrberk Sep 25, 2026
f9dd569
fix: key statement identity with the QuestDB formatter
emrberk Sep 25, 2026
edbbd93
signal user edits on run and refresh
emrberk Sep 25, 2026
8c0ee67
fix: keep chart config on format edits, drop formatter from hot paths…
emrberk Sep 27, 2026
35d718d
submodule
emrberk Sep 27, 2026
4e226d0
fix: keep each result's fetch time with its rows, keep a stopped char…
emrberk Sep 27, 2026
745e168
fix: keep a stopped chart stopped while polling, date every result ta…
emrberk Sep 27, 2026
edd3fe1
fix: one fetch-time model for notebook results
emrberk Sep 27, 2026
ed61577
fix: read the split-drag callbacks through refs so every move sees th…
emrberk Sep 28, 2026
18b62a8
fix: keep keyboard focus on a busy view segment and hand it on from R…
emrberk Sep 28, 2026
2718924
fix: report cleared results, keep Auto on its floor, honor a stopped …
emrberk Sep 28, 2026
8bce8e6
cleanups, improve resize and zoom handling
emrberk Sep 28, 2026
ba668ba
Merge remote-tracking branch 'origin/main' into refactor/notebook-dim…
emrberk Sep 28, 2026
aece459
fix: address PR #606 review findings
emrberk Sep 28, 2026
eeaa665
fix: address PR #606 review findings, round two
emrberk Sep 28, 2026
6998bb7
fix: retext a run's results to the edited statements at commit
emrberk Sep 28, 2026
62a8fde
cleanups and formatter performance fixes
emrberk Sep 29, 2026
d42d67a
context -> describe
emrberk Sep 29, 2026
c00cf43
use previous trimming approach for query identity, resolve handful of…
emrberk Sep 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
175 changes: 175 additions & 0 deletions e2e/tests/console/notebookCellControls.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
/// <reference types="cypress" />

// Cell controls that only a real browser exercises: the keyboard pane resize
// and the Stop control of a first run or first chart fetch.

const HELD_QUERY = /\/exec\?.*long_sequence/
const CHART_SQL =
"select timestamp_sequence(0, 1000000) ts, x from long_sequence(50)"

const setCellSql = (sql) => {
cy.focusNotebookCell()
cy.withFocusedEditor((editor) => editor.setValue(sql))
}

const runFocusedCell = () =>
cy.withFocusedEditor((editor) => editor.getAction("notebook-run").run())

const editorSeparator = () =>
cy.get('[role="separator"][aria-label="Resize editor pane"]')

const valueNow = ($separator) => Number($separator.attr("aria-valuenow"))

const readStoredEditorHeight = (win) =>
new Cypress.Promise((resolve, reject) => {
const openRequest = win.indexedDB.open("web-console")
openRequest.onerror = () => reject(openRequest.error)
openRequest.onsuccess = () => {
const database = openRequest.result
const fail = (error) => {
database.close()
reject(error)
}
const transaction = database.transaction(
["editor_settings", "buffers"],
"readonly",
)
const activeBufferRequest = transaction
.objectStore("editor_settings")
.index("key")
.get("activeBufferId")
activeBufferRequest.onerror = () => fail(activeBufferRequest.error)
activeBufferRequest.onsuccess = () => {
const bufferRequest = transaction
.objectStore("buffers")
.get(activeBufferRequest.result?.value)
bufferRequest.onerror = () => fail(bufferRequest.error)
bufferRequest.onsuccess = () => {
database.close()
resolve(bufferRequest.result?.notebookViewState?.cells[0]?.topHeight)
}
}
}
})

// The notebook writes a resize to IndexedDB after a debounce; polling the
// stored value is the only signal that the write landed.
const waitForStoredEditorHeight = (height) =>
cy.window().then((win) => {
const deadline = Date.now() + 8000
const poll = () =>
readStoredEditorHeight(win).then((stored) => {
if (stored === height) return
if (Date.now() > deadline) {
throw new Error(`stored editor height is ${stored}, not ${height}`)
}
return Cypress.Promise.delay(100).then(poll)
})
return poll()
})

// Holds the first matching request open until the test releases it, so the
// cell stays in its first run while the test drives the Stop control. The
// release must follow within Cypress's command timeout, or the held handler
// itself fails the test.
const holdFirstExec = () => {
const held = {}
cy.intercept(
{ url: HELD_QUERY, times: 1 },
() =>
new Cypress.Promise((resolve) => {
held.release = resolve
}),
)
return held
}

const awaitHeld = (held) =>
cy.wrap(null).should(() => {
expect(held.release, "the exec request is held").to.be.a("function")
})

const clickStop = (label) => {
cy.getByDataHook("cell-stop-button")
.should("have.attr", "aria-label", label)
.click()
}

describe("notebook cell controls", () => {
beforeEach(() => {
cy.loadConsoleWithAuth()
cy.createNotebook()
})

describe("keyboard resize", () => {
it("stores the height on key release and keeps it across a reload", () => {
// Given a cell whose result splits it into an editor and a result pane
setCellSql("select 1")
runFocusedCell()
editorSeparator().should("have.attr", "aria-valuenow")
let before
editorSeparator().then(($separator) => {
before = valueNow($separator)
})

// When the editor pane is grown by three keyboard steps
editorSeparator().focus()
cy.realPress("ArrowDown")
cy.realPress("ArrowDown")
cy.realPress("ArrowDown")

// Then the pane follows the keys and the release stores the height
editorSeparator().should(($grown) => {
expect(valueNow($grown)).to.eq(before + 30)
})
cy.then(() => waitForStoredEditorHeight(before + 30))

// And the stored height survives a reload
cy.reload()
editorSeparator().should(($restored) => {
expect(valueNow($restored)).to.eq(before + 30)
})
})
})

describe("Stop", () => {
it("cancels a first run and reports it in the result", () => {
// Given a cell whose first run the server has not answered yet
const held = holdFirstExec()
setCellSql("select x from long_sequence(100)")
runFocusedCell()
awaitHeld(held)

// When the run is stopped
clickStop("Stop run")
cy.then(() => held.release())

// Then the result reports the cancelled run and Stop is gone
cy.get("[data-notebook-cell]")
.contains("Cancelled by user")
.should("be.visible")
cy.getByDataHook("cell-stop-button").should("not.exist")
})

it("cancels a first chart fetch and retries it", () => {
// Given a draw cell whose first fetch the server has not answered yet
const held = holdFirstExec()
setCellSql(CHART_SQL)
cy.get("[data-notebook-cell] button[aria-label='Draw']").click()
awaitHeld(held)

// When the fetch is stopped
clickStop("Stop chart loading")
cy.then(() => held.release())

// Then the canvas offers a retry instead of a spinner
cy.getByDataHook("draw-canvas-cancelled").should("be.visible")
cy.getByDataHook("cell-stop-button").should("not.exist")

// When the fetch is retried, the chart loads from an unheld request
cy.getByDataHook("draw-canvas-retry").click()
cy.getByDataHook("draw-canvas-cancelled").should("not.exist")
cy.getByDataHook("draw-canvas").find("canvas").should("be.visible")
})
})
})
30 changes: 30 additions & 0 deletions e2e/tests/console/result_charts.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,34 @@ describe("questdb charts", () => {
.invoke("height")
.should("be.gt", 0)
})

it("keeps reset zoom available after the viewport widens", () => {
// Given a narrow viewport with a dense chart that shows the zoom slider
cy.viewport(760, 900)
cy.typeQueryDirectly(
"SELECT rnd_timestamp(to_timestamp('2024-07-19:00:00:00.000000', 'yyyy-MM-dd:HH:mm:ss.SSSUUU'), to_timestamp('2024-07-20:00:00:00.000000', 'yyyy-MM-dd:HH:mm:ss.SSSUUU'), 0), x FROM long_sequence(500);",
)
cy.clickRunIconInLine(1)
cy.getByDataHook("chart-panel-button").should("be.visible").click()

// When the user drags the slider to zoom in
cy.getByDataHook("result-chart")
.find("canvas")
.should("be.visible")
.then(($canvas) => {
const canvas = $canvas[0]
const sliderY = canvas.getBoundingClientRect().height - 44

cy.wrap(canvas).realMouseDown({ x: 54, y: sliderY })
cy.wrap(canvas).realMouseMove(130, sliderY)
cy.wrap(canvas).realMouseUp({ x: 130, y: sliderY })
})

// Then reset zoom appears and survives the viewport widening
cy.getByDataHook("result-chart-reset-zoom").should("be.visible")

cy.viewport(1280, 900)
cy.getByDataHook("result-chart-reset-zoom").should("be.visible").click()
cy.getByDataHook("result-chart-reset-zoom").should("not.exist")
})
})
40 changes: 39 additions & 1 deletion src/components/DropdownMenu/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from "react"
import * as RadixDropdownMenu from "@radix-ui/react-dropdown-menu"
import { CaretRightIcon } from "@phosphor-icons/react"
import { CaretRightIcon, CheckIcon } from "@phosphor-icons/react"
import styled, { css } from "styled-components"
import {
menuContainerStyles,
Expand Down Expand Up @@ -76,6 +76,42 @@ const Item = React.forwardRef<

Item.displayName = "DropdownMenuItem"

const StyledCheckboxItem = styled(RadixDropdownMenu.CheckboxItem)`
${menuItemStyles}
`

const CheckSlot = styled.span`
display: inline-flex;
width: 1.6rem;
margin-left: auto;
align-items: center;
justify-content: center;
color: ${({ theme }) => theme.color.contentAccent};
`

type CheckboxItemProps = React.ComponentPropsWithoutRef<
typeof RadixDropdownMenu.CheckboxItem
> & {
icon?: React.ReactNode
}

const CheckboxItem = React.forwardRef<
React.ElementRef<typeof RadixDropdownMenu.CheckboxItem>,
CheckboxItemProps
>(({ icon, children, ...props }, ref) => (
<StyledCheckboxItem ref={ref} {...props}>
{icon != null && <MenuItemIcon>{icon}</MenuItemIcon>}
{children}
<CheckSlot>
<RadixDropdownMenu.ItemIndicator asChild>
<CheckIcon size={16} weight="bold" />
</RadixDropdownMenu.ItemIndicator>
</CheckSlot>
</StyledCheckboxItem>
))

CheckboxItem.displayName = "DropdownMenuCheckboxItem"

const StyledSubTrigger = styled(RadixDropdownMenu.SubTrigger)`
${menuItemStyles}
justify-content: space-between;
Expand Down Expand Up @@ -120,6 +156,8 @@ export const DropdownMenu = {

Item,

CheckboxItem,

Sub: RadixDropdownMenu.Sub,

SubTrigger,
Expand Down
25 changes: 14 additions & 11 deletions src/components/SegmentedControl/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,7 @@ const SegmentedControlRoot = styled.div`

/* The liquid track clips content at its rounded edge, so the standard
outside button outline is not visible here. Draw the keyboard ring inside
every direct action instead, including auxiliary actions such as
maximize/reset that live beside the segments. */
each segment instead. */
&& > button:focus-visible,
&& > button[aria-pressed="true"]:focus-visible {
outline: none;
Expand All @@ -69,15 +68,7 @@ const FilterDefinitions = styled.svg`
pointer-events: none;
`

const GlassSelection = styled.div`
position: absolute;
top: 0;
left: 0;
width: 0;
height: 0;
z-index: 2;
opacity: 0;
pointer-events: none;
export const glassLens = css`
background: ${({ theme }) => theme.color.glassSurface};
border: 1px solid ${({ theme }) => theme.color.glassBorder};
border-bottom-width: 2px;
Expand All @@ -89,6 +80,18 @@ const GlassSelection = styled.div`
: `0 3px 9px ${theme.color.shadowSoft}`};
backdrop-filter: blur(6px) saturate(145%);
-webkit-backdrop-filter: blur(5px) saturate(150%);
`

const GlassSelection = styled.div`
position: absolute;
top: 0;
left: 0;
width: 0;
height: 0;
z-index: 2;
opacity: 0;
pointer-events: none;
${glassLens}
transition: opacity 100ms ease;
will-change: transform, width;
`
Expand Down
8 changes: 7 additions & 1 deletion src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,13 @@ export * from "./PopperHover"
export * from "./PopperToggle"
export * from "./Popover"
export * from "./SelectMenu"
export * from "./SegmentedControl"
export {
SegmentedControl,
SegmentedControlButton,
type SegmentedControlActiveTone,
type SegmentedControlSize,
type SegmentedControlTone,
} from "./SegmentedControl"
export * from "./SelectableCardButton"
export * from "./SetupAIAssistant"
export * from "./Spinner"
Expand Down
Loading
Loading