This is an automated email from the ASF dual-hosted git repository.

potiuk pushed a commit to branch v3-3-test
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/v3-3-test by this push:
     new 84779bacd89 [v3-3-test] UI: Fix copied SQL from Rendered Templates 
breaking at every token (#73633) (#73640)
84779bacd89 is described below

commit 84779bacd895edf1e1bcff2697f34d9954433763
Author: github-actions[bot] 
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Mon Sep 28 16:58:13 2026 +0200

    [v3-3-test] UI: Fix copied SQL from Rendered Templates breaking at every 
token (#73633) (#73640)
    
    * [v3-3-test] 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.
    (cherry picked from commit ffa19c31a67f86dadf23709eb10cfaacd2669e63)
    
    Co-authored-by: Brent Bovenzi <[email protected]>
    Closes: #73537
    
    * Apply v3-3-test UI import grouping to the RenderedTemplates test
    
    Generated-by: Claude Opus 5
    
    ---------
    
    Co-authored-by: Brent Bovenzi <[email protected]>
    Co-authored-by: Jarek Potiuk <[email protected]>
---
 .../pages/TaskInstance/RenderedTemplates.test.tsx  | 83 ++++++++++++++++++++++
 .../src/pages/TaskInstance/RenderedTemplates.tsx   | 17 ++++-
 2 files changed, 97 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..3bb6b141d25
--- /dev/null
+++ 
b/airflow-core/src/airflow/ui/src/pages/TaskInstance/RenderedTemplates.test.tsx
@@ -0,0 +1,83 @@
+/*!
+ * 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 de2c6313699..d9a6720e495 100644
--- a/airflow-core/src/airflow/ui/src/pages/TaskInstance/RenderedTemplates.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/TaskInstance/RenderedTemplates.tsx
@@ -61,10 +61,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}
@@ -75,11 +87,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