This is an automated email from the ASF dual-hosted git repository.
bbovenzi pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/main by this push:
new ffa19c31a67 UI: Fix copied SQL from Rendered Templates breaking at
every token (#73633)
ffa19c31a67 is described below
commit ffa19c31a67f86dadf23709eb10cfaacd2669e63
Author: Brent Bovenzi <[email protected]>
AuthorDate: Wed Sep 23 16:33:37 2026 -0400
UI: Fix copied SQL from Rendered Templates breaking at every token (#73633)
Combining `wrapLongLines` with `showLineNumbers` makes
react-syntax-highlighter
lay each line out as a flex container, so every highlighted token becomes a
block-level box and the browser emits a newline after each one when it
serializes a selection. Selecting a rendered SQL field and copying it
therefore
produced something that no longer parsed. The copy button kept working
because
it copies the raw string rather than the DOM, which is why this went
unnoticed.
The prism theme also hard-codes `white-space: pre` on the code tag and the
library merges that over its own `pre-wrap`, so `wrapLongLines` has never
actually wrapped anything.
The copy button was pulled back over the code block with a fixed negative
margin, so where it landed depended on the block's height.
Closes: #73537
---
.../pages/TaskInstance/RenderedTemplates.test.tsx | 84 ++++++++++++++++++++++
.../src/pages/TaskInstance/RenderedTemplates.tsx | 17 ++++-
2 files changed, 98 insertions(+), 3 deletions(-)
diff --git
a/airflow-core/src/airflow/ui/src/pages/TaskInstance/RenderedTemplates.test.tsx
b/airflow-core/src/airflow/ui/src/pages/TaskInstance/RenderedTemplates.test.tsx
new file mode 100644
index 00000000000..f9cbb9c2b3e
--- /dev/null
+++
b/airflow-core/src/airflow/ui/src/pages/TaskInstance/RenderedTemplates.test.tsx
@@ -0,0 +1,84 @@
+/*!
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import "@testing-library/jest-dom";
+import { render, screen } from "@testing-library/react";
+import { useParams } from "react-router-dom";
+import { beforeEach, describe, expect, it, vi } from "vitest";
+
+import * as queries from "openapi/queries";
+
+import { Wrapper } from "src/utils/Wrapper";
+
+import { RenderedTemplates } from "./RenderedTemplates";
+
+vi.mock("openapi/queries");
+vi.mock("react-router-dom", async () => {
+ const actual = await vi.importActual("react-router-dom");
+
+ return { ...actual, useParams: vi.fn() };
+});
+
+const SQL = `select
+ count(*) as total_rows,
+ nvl(count_if(m.a is null), 0) as nulls_count
+from my_table as m`;
+
+describe("RenderedTemplates", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ vi.mocked(useParams).mockReturnValue({
+ dagId: "test-dag",
+ mapIndex: "-1",
+ runId: "test-run",
+ taskId: "test-task",
+ });
+
vi.mocked(queries.useTaskInstanceServiceGetMappedTaskInstance).mockReturnValue({
+ data: { rendered_fields: { sql: SQL } },
+ } as unknown as ReturnType<typeof
queries.useTaskInstanceServiceGetMappedTaskInstance>);
+ });
+
+ it("lays out highlighted lines as blocks so copied text keeps its tokens on
one line", async () => {
+ const { container } = render(
+ <Wrapper>
+ <RenderedTemplates />
+ </Wrapper>,
+ );
+
+ await screen.findByText("sql");
+
+ const lines = container.querySelectorAll("code > span");
+
+ expect(lines.length).toBeGreaterThan(0);
+ lines.forEach((line) => {
+ expect(line).toHaveStyle({ display: "block" });
+ });
+ });
+
+ it("overrides the theme's white-space so long lines actually wrap", async ()
=> {
+ const { container } = render(
+ <Wrapper>
+ <RenderedTemplates />
+ </Wrapper>,
+ );
+
+ await screen.findByText("sql");
+
+ expect(container.querySelector("code")).toHaveStyle({ whiteSpace:
"pre-wrap" });
+ });
+});
diff --git
a/airflow-core/src/airflow/ui/src/pages/TaskInstance/RenderedTemplates.tsx
b/airflow-core/src/airflow/ui/src/pages/TaskInstance/RenderedTemplates.tsx
index c41a80bc92c..892609fa2db 100644
--- a/airflow-core/src/airflow/ui/src/pages/TaskInstance/RenderedTemplates.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/TaskInstance/RenderedTemplates.tsx
@@ -64,10 +64,22 @@ const RenderedTemplatesContent = () => {
opacity: 1,
},
}}
+ position="relative"
>
<Box borderRadius="md" fontSize="sm" m={0}
overflowX="auto" p={2}>
<SyntaxHighlighter
+ // The prism theme hard-codes `white-space: pre` on
the code tag, which
+ // silently defeats `wrapLongLines` unless we
override it here.
+ codeTagProps={{ style: { whiteSpace: "pre-wrap",
wordBreak: "break-word" } }}
language={language}
+ lineNumberStyle={{ minWidth: "2.5em" }}
+ // Combining `wrapLongLines` with `showLineNumbers`
makes the library lay
+ // each line out as a flex container, so every
highlighted token becomes a
+ // block-level flex item and copied text breaks
after each one. The hanging
+ // indent keeps wrapped lines clear of the gutter
(2.5em number + 1em pad).
+ lineProps={{
+ style: { display: "block", paddingLeft: "3.5em",
textIndent: "-3.5em" },
+ }}
PreTag="pre"
showLineNumbers
style={style}
@@ -78,11 +90,10 @@ const RenderedTemplatesContent = () => {
</Box>
<ClipboardRoot
className="copy-button"
- float="right"
- marginTop="-3.5rem"
opacity={0}
- position="sticky"
+ position="absolute"
right={4}
+ top={6}
transition="opacity 0.2s ease-in-out"
value={renderedValue}
>