From cc1fab4e3f857e9e06792ba7c39f9c0f1ad74892 Mon Sep 17 00:00:00 2001 From: emrberk Date: Tue, 24 Mar 2026 13:38:13 +0300 Subject: [PATCH 1/2] fix: improve grid UX, context menu clipping, and toolbar polish - Replace header click-to-insert with per-column copy button - Track grid selection state to disable "move column to front" when nothing is selected - Fix Monaco context menu being clipped by overflow containers - Consolidate DDL queries to strip redundant blank lines - Add truncation for long DDL in table details drawer - Rebind format shortcuts to Alt+Shift+F / Alt+F - Tighten toolbar and result bar spacing - Switch LiteEditor icon based on diff vs regular mode Co-Authored-By: Claude Opus 4.6 --- e2e/questdb | 2 +- src/components/CopyButton/index.tsx | 1 + src/components/LiteEditor/index.tsx | 15 ++- src/components/LiteEditor/utils.ts | 23 ++++ src/components/TopBar/toolbar.tsx | 8 +- src/js/console/grid.js | 46 ++++++-- src/modules/EventBus/types.ts | 1 - src/scenes/Editor/ButtonBar/index.tsx | 4 +- src/scenes/Editor/Monaco/editor-addons.ts | 15 +++ src/scenes/Editor/Monaco/index.tsx | 38 ++++++- src/scenes/Editor/Monaco/legacy-event-bus.ts | 9 -- src/scenes/Result/index.tsx | 102 ++++++++++-------- .../Schema/TableDetailsDrawer/index.tsx | 9 +- src/scenes/Schema/VirtualTables/index.tsx | 2 +- src/styles/_editor.scss | 1 - src/styles/_grid.scss | 40 ++++++- src/theme/global-styles/docsearch.ts | 1 + src/utils/questdb/client.ts | 19 +++- 18 files changed, 248 insertions(+), 88 deletions(-) diff --git a/e2e/questdb b/e2e/questdb index 8bb20f7ce..5d939dbe9 160000 --- a/e2e/questdb +++ b/e2e/questdb @@ -1 +1 @@ -Subproject commit 8bb20f7ce06a65a7c846816cb75458929ad7cdc2 +Subproject commit 5d939dbe926c6b9573d33ed45551e619cc32f742 diff --git a/src/components/CopyButton/index.tsx b/src/components/CopyButton/index.tsx index 4d8ee9b3f..0544948bf 100644 --- a/src/components/CopyButton/index.tsx +++ b/src/components/CopyButton/index.tsx @@ -7,6 +7,7 @@ import { copyToClipboard } from "../../utils/copyToClipboard" const StyledButton = styled(Button)` padding: 1.2rem 0.6rem; + position: relative; ` const StyledCheckboxCircle = styled(CheckboxCircle)` diff --git a/src/components/LiteEditor/index.tsx b/src/components/LiteEditor/index.tsx index d2342401c..b32599dcc 100644 --- a/src/components/LiteEditor/index.tsx +++ b/src/components/LiteEditor/index.tsx @@ -4,6 +4,7 @@ import type { editor } from "monaco-editor" import { QuestDBLanguageName } from "../../scenes/Editor/Monaco/utils" import styled, { useTheme } from "styled-components" import { Button } from "../Button" +import { CornersOutIcon } from "@phosphor-icons/react" import { FileCopy } from "@styled-icons/remix-line" import { CheckboxCircle } from "@styled-icons/remix-fill" import { copyToClipboard } from "../../utils/copyToClipboard" @@ -39,6 +40,10 @@ const EditorWrapper = styled.div<{ $noBorder?: boolean }>` pointer-events: none; } + .view-overlays > * { + overflow-x: hidden; + } + .current-line { background: transparent !important; border: 0 !important; @@ -141,14 +146,17 @@ const LiteEditorToolbar = ({ onOpenInEditor, onCopy, copied, + diffEditor, compact = false, }: { + diffEditor: boolean onOpenInEditor: () => void onCopy: () => void copied: boolean compact?: boolean }) => { const appTheme = useTheme() + const Icon = diffEditor ? SquareSplitHorizontalIcon : CornersOutIcon return ( {!compact && "Open in editor"} - + {!compact && ( = ({ }} > handleCopy(props.modified)} copied={copied} @@ -415,6 +421,7 @@ export const LiteEditor: React.FC = ({ {showToolbarForRegularEditor || compactToolbar ? ( handleCopy(props.value ?? "")} copied={copied} diff --git a/src/components/LiteEditor/utils.ts b/src/components/LiteEditor/utils.ts index e578928ca..a466b6818 100644 --- a/src/components/LiteEditor/utils.ts +++ b/src/components/LiteEditor/utils.ts @@ -1,5 +1,28 @@ import type { Column } from "../../utils/questdb/types" +export const truncateLongDDL = ( + ddl: string, + maxLines: number = 10, +): { text: string; grayedOutLines: [number, number] | null } => { + const lines = ddl.split("\n") + if (lines.length <= maxLines) { + return { text: ddl, grayedOutLines: null } + } + + const keepTop = 5 + const keepBottom = 3 + const topLines = lines.slice(0, keepTop) + const bottomLines = lines.slice(-keepBottom) + + const indent = lines[keepTop]?.match(/^\s*/)?.[0] ?? " " + const text = [...topLines, indent + "...", ...bottomLines].join("\n") + + const grayStartLine = keepTop - 1 + const grayEndLine = keepTop + 1 + + return { text, grayedOutLines: [grayStartLine, grayEndLine] } +} + export const hideColumnsFromTableDDL = ( ddl: string, columns: Column[], diff --git a/src/components/TopBar/toolbar.tsx b/src/components/TopBar/toolbar.tsx index 0557a5322..e0f71b350 100644 --- a/src/components/TopBar/toolbar.tsx +++ b/src/components/TopBar/toolbar.tsx @@ -36,7 +36,7 @@ const EnvIconWrapper = styled.div<{ $background?: string }>` ` const Root = styled(Box).attrs({ align: "center" })` - gap: 1.5rem; + gap: 0.8rem; padding-left: 1.5rem; white-space: nowrap; display: flex; @@ -554,7 +554,9 @@ export const Toolbar = () => { return ( - Web Console + + Web Console + {settings["release.type"] === "EE" && ( EE} @@ -631,7 +633,7 @@ export const Toolbar = () => { )} )} - + {settings["acl.enabled"] && currentUser && ( diff --git a/src/js/console/grid.js b/src/js/console/grid.js index 7283ce8f4..4a86a6092 100644 --- a/src/js/console/grid.js +++ b/src/js/console/grid.js @@ -35,6 +35,16 @@ const hashString = (str) => { return new Uint32Array([hash])[0].toString(36) } +const COPY_ICON_SVG = + '' + + '' + + "" + +const CHECK_ICON_SVG = + '' + + '' + + "" + export function grid(rootElement, _paginationFn, id) { const defaults = { gridID: "qdb-grid", @@ -519,13 +529,18 @@ export function grid(rootElement, _paginationFn, id) { } } - function triggerHeaderClick(e) { - // avoid broadcasting fat finger clicks - if (colResizeColIndex === undefined) { - triggerEvent("header.click", { - columnName: e.currentTarget.getAttribute("data-column-name"), - }) - } + function headerCopyClick(e) { + e.stopPropagation() + const copyBtn = e.currentTarget + const headerEl = copyBtn.closest(".qg-header") + const columnName = headerEl.getAttribute("data-column-name") + copyToClipboard(columnName).then(undefined) + copyBtn.innerHTML = COPY_ICON_SVG + CHECK_ICON_SVG + addClass(copyBtn, "qg-header-copy-active") + setTimeout(() => { + copyBtn.innerHTML = COPY_ICON_SVG + removeClass(copyBtn, "qg-header-copy-active") + }, 2000) } function colResizeClearTimer() { @@ -984,9 +999,19 @@ export function grid(rootElement, _paginationFn, id) { const hBorderSpan = document.createElement("span") addClass(hBorderSpan, "qg-header-border") + + const copyBtn = document.createElement("div") + addClass(copyBtn, "qg-header-copy") + copyBtn.innerHTML = COPY_ICON_SVG + copyBtn.onclick = headerCopyClick + + const hNameRow = document.createElement("div") + addClass(hNameRow, "qg-header-name-row") + hNameRow.append(hName, copyBtn) + h.append(hysteresis, hBorderSpan) - h.append(hName, hType) - h.onclick = triggerHeaderClick + h.append(hNameRow, hType) + header.append(h) } @@ -1093,6 +1118,7 @@ export function grid(rootElement, _paginationFn, id) { focusedCell = cell focusedColumnIndex = cell.columnIndex renderFocusedCell() + triggerEvent("selection.change", { hasSelection: true }) } } @@ -2230,6 +2256,7 @@ export function grid(rootElement, _paginationFn, id) { function setData(_data) { initialFocusSkipped = false + triggerEvent("selection.change", { hasSelection: false }) setTimeout(() => { setDataPart1(_data) // This part of the update sequence requires layoutStore access. @@ -2287,6 +2314,7 @@ export function grid(rootElement, _paginationFn, id) { setBothRowsInactive() focusedRowIndex = -1 focusedColumnIndex = -1 + triggerEvent("selection.change", { hasSelection: false }) } }) diff --git a/src/modules/EventBus/types.ts b/src/modules/EventBus/types.ts index 0407161a3..6d98a5fbb 100644 --- a/src/modules/EventBus/types.ts +++ b/src/modules/EventBus/types.ts @@ -1,7 +1,6 @@ export enum EventType { MSG_ACTIVE_SIDEBAR = "active.panel", MSG_EDITOR_FOCUS = "editor.focus", - MSG_EDITOR_INSERT_COLUMN = "editor.insert.column", GRID_FOCUS = "grid.focus", MSG_CHART_DRAW = "chart.draw", MSG_QUERY_CANCEL = "query.in.cancel", diff --git a/src/scenes/Editor/ButtonBar/index.tsx b/src/scenes/Editor/ButtonBar/index.tsx index c3d6ac393..e1f0ef6c2 100644 --- a/src/scenes/Editor/ButtonBar/index.tsx +++ b/src/scenes/Editor/ButtonBar/index.tsx @@ -20,9 +20,9 @@ const ButtonBarWrapper = styled.div<{ ${({ $searchWidgetType }) => css` position: absolute; top: ${$searchWidgetType === "replace" - ? "8.2rem" + ? "calc(8.2rem + 8px)" : $searchWidgetType === "find" - ? "5.3rem" + ? "calc(5.3rem + 8px)" : "1rem"}; right: 2.4rem; z-index: 1; diff --git a/src/scenes/Editor/Monaco/editor-addons.ts b/src/scenes/Editor/Monaco/editor-addons.ts index e0bd385dc..c7ba43522 100644 --- a/src/scenes/Editor/Monaco/editor-addons.ts +++ b/src/scenes/Editor/Monaco/editor-addons.ts @@ -163,6 +163,21 @@ export const registerEditorActions = ({ } export const registerLanguageAddons = (monaco: Monaco) => { + monaco.editor.addKeybindingRules([ + { keybinding: 0, command: "-editor.action.formatDocument" }, + { keybinding: 0, command: "-editor.action.formatSelection" }, + { + keybinding: monaco.KeyMod.Alt | monaco.KeyMod.Shift | monaco.KeyCode.KeyF, + command: "editor.action.formatDocument", + when: "editorTextFocus", + }, + { + keybinding: monaco.KeyMod.Alt | monaco.KeyCode.KeyF, + command: "editor.action.formatSelection", + when: "editorTextFocus", + }, + ]) + monaco.languages.register({ id: QuestDBLanguageName }) monaco.languages.setMonarchTokensProvider( diff --git a/src/scenes/Editor/Monaco/index.tsx b/src/scenes/Editor/Monaco/index.tsx index 91fa96b61..4ba62bd6d 100644 --- a/src/scenes/Editor/Monaco/index.tsx +++ b/src/scenes/Editor/Monaco/index.tsx @@ -262,7 +262,6 @@ const MonacoEditor = ({ hidden = false }: { hidden?: boolean }) => { setTabsDisabled, editorRef, monacoRef, - insertTextAtCursor, activeBuffer, updateBuffer, editorReadyTrigger, @@ -854,7 +853,6 @@ const MonacoEditor = ({ hidden = false }: { hidden?: boolean }) => { cleanupActionsRef.current.push( registerLegacyEventBusEvents({ editor, - insertTextAtCursor, toggleRunning, }), ) @@ -877,6 +875,42 @@ const MonacoEditor = ({ hidden = false }: { hidden?: boolean }) => { }), ) + // Prevent context menu from being clipped + const containerDomNode = editor.getContainerDomNode() + const contextMenuObserver = new MutationObserver((mutations) => { + for (const mutation of mutations) { + for (const node of Array.from(mutation.addedNodes)) { + if ( + node instanceof HTMLElement && + node.classList.contains("context-view") && + node.classList.contains("monaco-menu-container") + ) { + const rect = node.getBoundingClientRect() + node.style.position = "fixed" + node.style.left = `${rect.left}px` + node.style.top = `${rect.top}px` + + // Monaco reuses the node on subsequent opens, resetting styles. + const styleObserver = new MutationObserver(() => { + if (node.style.position !== "fixed") { + const rect = node.getBoundingClientRect() + node.style.position = "fixed" + node.style.left = `${rect.left}px` + node.style.top = `${rect.top}px` + } + }) + styleObserver.observe(node, { + attributes: true, + attributeFilter: ["style"], + }) + cleanupActionsRef.current.push(() => styleObserver.disconnect()) + } + } + } + }) + contextMenuObserver.observe(containerDomNode, { childList: true }) + cleanupActionsRef.current.push(() => contextMenuObserver.disconnect()) + editor.onDidChangeCursorPosition((e) => { // To ensure the fixed position of the "run query" glyph we adjust the width of the line count element. // This width is represented in char numbers. diff --git a/src/scenes/Editor/Monaco/legacy-event-bus.ts b/src/scenes/Editor/Monaco/legacy-event-bus.ts index a316a4f96..f0ae046bc 100644 --- a/src/scenes/Editor/Monaco/legacy-event-bus.ts +++ b/src/scenes/Editor/Monaco/legacy-event-bus.ts @@ -29,19 +29,11 @@ import { RunningType } from "../../../store/Query/types" export const registerLegacyEventBusEvents = ({ editor, - insertTextAtCursor, toggleRunning, }: { editor: editor.IStandaloneCodeEditor - insertTextAtCursor: (text: string) => void toggleRunning: (runningType?: RunningType) => void }) => { - eventBus.subscribe(EventType.MSG_EDITOR_INSERT_COLUMN, (column) => { - if (column) { - insertTextAtCursor(column) - } - }) - eventBus.subscribe<{ query: string; options?: AppendQueryOptions }>( EventType.MSG_QUERY_FIND_N_EXEC, (payload) => { @@ -69,7 +61,6 @@ export const registerLegacyEventBusEvents = ({ }) return () => { - eventBus.unsubscribe(EventType.MSG_EDITOR_INSERT_COLUMN) eventBus.unsubscribe(EventType.MSG_QUERY_FIND_N_EXEC) eventBus.unsubscribe(EventType.MSG_QUERY_EXEC) eventBus.unsubscribe(EventType.MSG_EDITOR_FOCUS) diff --git a/src/scenes/Result/index.tsx b/src/scenes/Result/index.tsx index 19fa7cccf..e6d7e2c6d 100644 --- a/src/scenes/Result/index.tsx +++ b/src/scenes/Result/index.tsx @@ -84,7 +84,7 @@ const Actions = styled.div` display: grid; grid-auto-flow: column; grid-auto-columns: max-content; - gap: 0; + gap: 0.5rem; align-items: center; justify-content: flex-end; padding: 0 1rem; @@ -98,6 +98,12 @@ const TableFreezeColumnIcon = styled(TableFreezeColumn)` transform: scaleX(-1); ` +const StyledPrimaryToggleButton = styled(PrimaryToggleButton)` + padding: 0 1rem; + height: 3rem; + width: 4rem; +` + const RowCount = styled(Text)` margin-right: 1rem; line-height: 1.285; @@ -142,6 +148,7 @@ const Result = ({ viewMode }: { viewMode: ResultViewMode }) => { const activeSidebar = useSelector(selectors.console.getActiveSidebar) const gridRef = useRef() const [gridFreezeLeftState, setGridFreezeLeftState] = useState(0) + const [gridHasSelection, setGridHasSelection] = useState(false) const [downloadMenuActive, setDownloadMenuActive] = useState(false) const dispatch = useDispatch() @@ -180,12 +187,9 @@ const Result = ({ viewMode }: { viewMode: ResultViewMode }) => { ) _grid.addEventListener( - "header.click", - function (event: CustomEvent<{ columnName: string }>) { - eventBus.publish( - EventType.MSG_EDITOR_INSERT_COLUMN, - event.detail.columnName, - ) + "selection.change", + function (event: CustomEvent<{ hasSelection: boolean }>) { + setGridHasSelection(event.detail.hasSelection) }, ) @@ -235,7 +239,8 @@ const Result = ({ viewMode }: { viewMode: ResultViewMode }) => { { tooltipText: "Copy result to Markdown", trigger: ( - { void copyToClipboard( gridRef?.current?.getResultAsMarkdown() as string, @@ -246,13 +251,13 @@ const Result = ({ viewMode }: { viewMode: ResultViewMode }) => { }} > {isCopied ? : } - + ), }, { tooltipText: "Freeze left column", trigger: ( - { gridRef?.current?.toggleFreezeLeft() gridRef?.current?.focus() @@ -260,7 +265,7 @@ const Result = ({ viewMode }: { viewMode: ResultViewMode }) => { selected={gridFreezeLeftState > 0} > - + ), }, { @@ -268,6 +273,7 @@ const Result = ({ viewMode }: { viewMode: ResultViewMode }) => { trigger: (