Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions src/components/ResultGrid/ResultGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,14 @@ import {
} from "@tanstack/react-table"

import type { ColumnDefinition } from "../../utils/questdb/types"
import { unescapeHtml } from "../../utils/escapeHtml"
import type { CellValue, ResultGridDataSource, ResultGridRow } from "./types"
import {
clampColumnWidths,
sampleColumnWidths,
isLeftAligned,
formatCellValue,
formatColumnType,
toSingleLineDisplay,
} from "./inlineGridUtils"
import { useGridKeyboardNav } from "./useGridKeyboardNav"
import {
Expand Down Expand Up @@ -107,7 +107,7 @@ const GridCell = React.memo(function GridCell({
const colType = col?.type ?? ""
const align = isLeftAligned(colType) ? "left" : "right"
const displayValue = loaded
? unescapeHtml(formatCellValue(rawValue, col, colWidth))
? toSingleLineDisplay(formatCellValue(rawValue, col, colWidth))
: ""
return (
<Cell
Expand Down
38 changes: 33 additions & 5 deletions src/components/ResultGrid/inlineGridUtils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
formatCellValueForCopy,
formatColumnType,
isLeftAligned,
toSingleLineDisplay,
} from "./inlineGridUtils"
import type { ColumnDefinition } from "../../utils/questdb/types"

Expand Down Expand Up @@ -185,14 +186,41 @@ describe("formatCellValueForCopy", () => {
expect(out).toBe("ARRAY[1.0,2.0]")
})

it("unescapes HTML entities so copy matches the displayed text", () => {
// Given a string value carrying HTML entities, as the grid displays it
// unescaped
it("preserves HTML entity-looking text", () => {
// Given a string value containing entity-looking text
// When formatting it for copy
const out = formatCellValueForCopy("a&amp;b&lt;c&gt;d", col("x", "VARCHAR"))

// Then the copied text is unescaped, matching the cell
expect(out).toBe("a&b<c>d")
// Then the copied text is preserved exactly as returned by the server
expect(out).toBe("a&amp;b&lt;c&gt;d")
})

it("preserves embedded line breaks so copy keeps the raw value", () => {
const out = formatCellValueForCopy(
"line1\nline2\r\nline3",
col("x", "VARCHAR"),
)

expect(out).toBe("line1\nline2\r\nline3")
})
})

describe("toSingleLineDisplay", () => {
it("collapses line breaks to a single space so cells render on one line", () => {
expect(toSingleLineDisplay("line1\nline2")).toBe("line1 line2")
expect(toSingleLineDisplay("line1\r\nline2")).toBe("line1 line2")
expect(toSingleLineDisplay("line1\rline2")).toBe("line1 line2")
expect(toSingleLineDisplay("a\n\n\nb")).toBe("a b")
})

it("preserves leading and interior spaces (rendered via white-space: pre)", () => {
expect(toSingleLineDisplay(" indented")).toBe(" indented")
expect(toSingleLineDisplay("a b")).toBe("a b")
})

it("leaves values without line breaks untouched", () => {
expect(toSingleLineDisplay("a&amp;b<c>d")).toBe("a&amp;b<c>d")
expect(toSingleLineDisplay("")).toBe("")
})
})

Expand Down
8 changes: 5 additions & 3 deletions src/components/ResultGrid/inlineGridUtils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import type { ColumnDefinition } from "../../utils/questdb/types"
import { unescapeHtml } from "../../utils/escapeHtml"
import type { CellValue, ResultGridRow } from "./types"

const CELL_WIDTH_MULTIPLIER = 9.6
Expand Down Expand Up @@ -170,8 +169,11 @@ export const formatCellValueForCopy = (
if (value === null) return "null"

if (col && isArrayColumn(col)) {
return unescapeHtml(formatArrayFull(value, col))
return formatArrayFull(value, col)
}

return unescapeHtml(formatCellValue(value, col))
return formatCellValue(value, col)
}

export const toSingleLineDisplay = (text: string): string =>
text.replace(/[\r\n]+/g, " ")
27 changes: 19 additions & 8 deletions src/components/ResultGrid/resultPageMarkdown.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,16 +44,27 @@ describe("buildResultPageMarkdown", () => {
})

it("renders a QUERY PLAN result as a fenced code block", () => {
// Given a single QUERY PLAN column with two plan lines
// Given a query plan containing structural indentation, raw operators, and
// entity-looking SQL literals
const columns = [col("QUERY PLAN", "STRING")]
const rows = [["Async JIT Filter"], [" Row forward scan"]]
const rows = [
["Async JIT Filter"],
[" filter: x < y"],
[" functions: ['&nbsp;','&lt;','&gt;']"],
]

// When building the markdown
const md = buildResultPageMarkdown(columns, rows)

// Then it is a fenced block, one line per row, no table pipes
// Then every plan line is preserved verbatim in the fenced block
expect(md).toBe(
["```", "Async JIT Filter", " Row forward scan", "```"].join("\n"),
[
"```",
"Async JIT Filter",
" filter: x < y",
" functions: ['&nbsp;','&lt;','&gt;']",
"```",
].join("\n"),
)
})

Expand All @@ -69,17 +80,17 @@ describe("buildResultPageMarkdown", () => {
expect(md).toBe(["| a | b |", "| - | - |"].join("\n"))
})

it("unescapes HTML entities so exported text matches the grid", () => {
// Given a string column whose value carries HTML entities
it("preserves HTML entity-looking text", () => {
// Given a string column whose value contains entity-looking text
const columns = [col("note", "VARCHAR")]
const rows = [["price&gt;100"]]

// When building the markdown
const md = buildResultPageMarkdown(columns, rows)

// Then the cell is unescaped, matching what the grid displays
// Then the cell is preserved exactly as returned by the server
expect(md).toBe(
["| note |", "| --------- |", "| price>100 |"].join("\n"),
["| note |", "| ------------ |", "| price&gt;100 |"].join("\n"),
)
})

Expand Down
3 changes: 1 addition & 2 deletions src/components/ResultGrid/resultPageMarkdown.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import type { ColumnDefinition } from "../../utils/questdb/types"
import { unescapeHtml } from "../../utils/escapeHtml"
import type { ResultGridRow } from "./types"
import { formatCellValueForCopy } from "./inlineGridUtils"

Expand All @@ -11,7 +10,7 @@ const buildQueryPlanMarkdown = (rows: ResultGridRow[]): string => {
for (const row of rows) {
const cell = row[0]
if (cell === null || cell === undefined) continue
lines.push(unescapeHtml(String(cell)))
lines.push(String(cell))
}
lines.push("```")
return lines.join("\n")
Expand Down
2 changes: 1 addition & 1 deletion src/components/ResultGrid/styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ export const CellText = styled.div`
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
white-space: pre;
`

export const FrozenShadow = styled.div`
Expand Down
4 changes: 2 additions & 2 deletions src/js/console/grid.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@
*
******************************************************************************/
import { copyToClipboard } from "../../utils/copyToClipboard"
import { unescapeHtml } from "../../utils/escapeHtml"
import { toast } from "../../components"
import { trackEvent } from "../../modules/ConsoleEventTracker"
import { ConsoleEvent } from "../../modules/ConsoleEventTracker/events"
import { buildResultPageMarkdown } from "../../components/ResultGrid/resultPageMarkdown"
import { toSingleLineDisplay } from "../../components/ResultGrid/inlineGridUtils"

const hashString = (str) => {
let hash = 0
Expand Down Expand Up @@ -1107,7 +1107,7 @@ export function grid(rootElement, _paginationFn, id) {
if (cellData !== null) {
const layoutEntry = getLayoutEntry()
const columnWidth = layoutEntry.deviants[column.name] ?? null
cell.textContent = unescapeHtml(
cell.textContent = toSingleLineDisplay(
getDisplayedCellValue(column, cellData, columnWidth),
)

Expand Down
4 changes: 2 additions & 2 deletions src/styles/_grid.scss
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,12 @@ $col-resize-ghost-z-index: ($panel-left-z-index + 1);
color: #939393;
}

.qg-header-row,
.qg-r {
.qg-header-row {
white-space: nowrap;
}

.qg-r {
white-space: pre;
display: flex;
border-bottom: 1px solid #44475a;
position: absolute;
Expand Down
13 changes: 0 additions & 13 deletions src/utils/escapeHtml.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,3 @@ export const escapeHtml = (text: string): string => {

return text.replace(/[&<>"']/g, (m) => map[m])
}

export const unescapeHtml = (text: string): string => {
const map: Record<string, string> = {
"&amp;": "&",
"&lt;": "<",
"&gt;": ">",
"&quot;": '"',
"&#039;": "'",
"&nbsp;": "\u00A0",
}

return text.replace(/&(amp|lt|gt|quot|#039|nbsp);/g, (m) => map[m])
}
Loading