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}
                       >

Reply via email to