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 9dec3eb009a Fix log line-number links highlighting (#69293)
9dec3eb009a is described below

commit 9dec3eb009a03f1107146b0189e17eb884fdfbaf
Author: Guan-Ming Chiu <[email protected]>
AuthorDate: Fri Jul 10 00:01:18 2026 +0800

    Fix log line-number links highlighting (#69293)
    
    * Fix log line-number links highlighting
    
    * Add regression test for log line-number to visible index mapping
---
 .../src/pages/TaskInstance/Logs/TaskLogContent.tsx | 18 ++++----
 .../pages/TaskInstance/Logs/useLogGroups.test.tsx  | 50 ++++++++++++++++++++++
 .../src/pages/TaskInstance/Logs/useLogGroups.tsx   |  6 ++-
 .../ui/src/pages/TaskInstance/Logs/utils.test.ts   | 13 ++----
 .../ui/src/pages/TaskInstance/Logs/utils.ts        |  6 +--
 .../src/airflow/ui/src/queries/useLogs.tsx         | 37 +++++++++-------
 6 files changed, 91 insertions(+), 39 deletions(-)

diff --git 
a/airflow-core/src/airflow/ui/src/pages/TaskInstance/Logs/TaskLogContent.tsx 
b/airflow-core/src/airflow/ui/src/pages/TaskInstance/Logs/TaskLogContent.tsx
index 62fa6b9bfc2..8f57b096e3c 100644
--- a/airflow-core/src/airflow/ui/src/pages/TaskInstance/Logs/TaskLogContent.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/TaskInstance/Logs/TaskLogContent.tsx
@@ -62,13 +62,15 @@ export const TaskLogContent = ({
 
   const {
     expandedGroups,
-    originalToVisibleIndex,
+    lineNumberToVisibleIndex,
     toggleGroup,
     visibleCurrentMatchIndex,
     visibleItems,
     visibleSearchMatchIndices,
   } = useLogGroups({ currentMatchLineIndex, expanded, parsedLogs, 
searchMatchIndices });
 
+  const hashVisibleIndex = hash === "" ? undefined : 
lineNumberToVisibleIndex.get(Number(hash));
+
   const isAtBottomRef = useRef<boolean>(true);
   const prevVisibleCountRef = useRef<number>(0);
 
@@ -114,15 +116,11 @@ export const TaskLogContent = ({
   }, [visibleItems.length, rowVirtualizer]);
 
   useLayoutEffect(() => {
-    if (location.hash && !isLoading) {
-      const hashVisibleIndex = originalToVisibleIndex.get(Number(hash) - 1);
-
-      if (hashVisibleIndex !== undefined) {
-        rowVirtualizer.scrollToIndex(Math.min(hashVisibleIndex + 5, 
visibleItems.length - 1));
-      }
+    if (location.hash && !isLoading && hashVisibleIndex !== undefined) {
+      rowVirtualizer.scrollToIndex(Math.min(hashVisibleIndex + 5, 
visibleItems.length - 1));
     }
     // React Compiler auto-memoizes; safe to include in deps
-  }, [isLoading, rowVirtualizer, hash, visibleItems, originalToVisibleIndex]);
+  }, [isLoading, rowVirtualizer, visibleItems, hashVisibleIndex]);
 
   useLayoutEffect(() => {
     if (visibleCurrentMatchIndex !== undefined && !isLoading) {
@@ -209,7 +207,7 @@ export const TaskLogContent = ({
                     _rtl={{ left: "auto", right: 0 }}
                     bgColor={getHighlightColor({
                       currentMatchLineIndex: visibleCurrentMatchIndex,
-                      hash,
+                      hashIndex: hashVisibleIndex,
                       index: virtualRow.index,
                       searchMatchIndices: visibleSearchMatchIndices,
                     })}
@@ -257,7 +255,7 @@ export const TaskLogContent = ({
                   _rtl={{ left: "auto", right: 0 }}
                   bgColor={getHighlightColor({
                     currentMatchLineIndex: visibleCurrentMatchIndex,
-                    hash,
+                    hashIndex: hashVisibleIndex,
                     index: virtualRow.index,
                     searchMatchIndices: visibleSearchMatchIndices,
                   })}
diff --git 
a/airflow-core/src/airflow/ui/src/pages/TaskInstance/Logs/useLogGroups.test.tsx 
b/airflow-core/src/airflow/ui/src/pages/TaskInstance/Logs/useLogGroups.test.tsx
new file mode 100644
index 00000000000..3b5af89a118
--- /dev/null
+++ 
b/airflow-core/src/airflow/ui/src/pages/TaskInstance/Logs/useLogGroups.test.tsx
@@ -0,0 +1,50 @@
+/*!
+ * 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 { renderHook } from "@testing-library/react";
+import { describe, expect, it } from "vitest";
+
+import type { ParsedLogEntry } from "src/queries/useLogs";
+
+import { useLogGroups } from "./useLogGroups";
+
+const parsedLogs: Array<ParsedLogEntry> = [
+  { element: "task identity preamble" },
+  { element: "::group::setup", group: { id: 0, level: 0, type: "header" } },
+  { element: "line 1", group: { id: 0, level: 0, type: "line" }, lineNumber: 1 
},
+  { element: "line 2", group: { id: 0, level: 0, type: "line" }, lineNumber: 2 
},
+  { element: "line 3", lineNumber: 3 },
+];
+
+describe("useLogGroups", () => {
+  it("maps line numbers to visible indexes when groups are expanded", () => {
+    const { result } = renderHook(() => useLogGroups({ expanded: true, 
parsedLogs }));
+
+    expect([...result.current.lineNumberToVisibleIndex]).toStrictEqual([
+      [1, 2],
+      [2, 3],
+      [3, 4],
+    ]);
+  });
+
+  it("skips lines hidden inside a collapsed group", () => {
+    const { result } = renderHook(() => useLogGroups({ expanded: false, 
parsedLogs }));
+
+    expect([...result.current.lineNumberToVisibleIndex]).toStrictEqual([[3, 
2]]);
+  });
+});
diff --git 
a/airflow-core/src/airflow/ui/src/pages/TaskInstance/Logs/useLogGroups.tsx 
b/airflow-core/src/airflow/ui/src/pages/TaskInstance/Logs/useLogGroups.tsx
index 176f481cf55..12b290ffff9 100644
--- a/airflow-core/src/airflow/ui/src/pages/TaskInstance/Logs/useLogGroups.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/TaskInstance/Logs/useLogGroups.tsx
@@ -104,12 +104,16 @@ export const useLogGroups = ({
   // Build visible items list with index mapping
   const visibleItems: Array<VisibleItem> = [];
   const originalToVisibleIndex = new Map<number, number>();
+  const lineNumberToVisibleIndex = new Map<number, number>();
 
   for (let idx = 0; idx < parsedLogs.length; idx += 1) {
     const entry = parsedLogs[idx];
 
     if (entry && isEntryVisible(entry)) {
       originalToVisibleIndex.set(idx, visibleItems.length);
+      if (entry.lineNumber !== undefined) {
+        lineNumberToVisibleIndex.set(entry.lineNumber, visibleItems.length);
+      }
       visibleItems.push({ entry, originalIndex: idx });
     }
   }
@@ -162,7 +166,7 @@ export const useLogGroups = ({
 
   return {
     expandedGroups,
-    originalToVisibleIndex,
+    lineNumberToVisibleIndex,
     toggleGroup,
     visibleCurrentMatchIndex,
     visibleItems,
diff --git 
a/airflow-core/src/airflow/ui/src/pages/TaskInstance/Logs/utils.test.ts 
b/airflow-core/src/airflow/ui/src/pages/TaskInstance/Logs/utils.test.ts
index 4ca6a9f2530..004f9a2b6e0 100644
--- a/airflow-core/src/airflow/ui/src/pages/TaskInstance/Logs/utils.test.ts
+++ b/airflow-core/src/airflow/ui/src/pages/TaskInstance/Logs/utils.test.ts
@@ -110,7 +110,6 @@ describe("getHighlightColor", () => {
     expect(
       getHighlightColor({
         currentMatchLineIndex: 3,
-        hash: "",
         index: 3,
         searchMatchIndices: new Set([1, 3, 5]),
       }),
@@ -121,7 +120,6 @@ describe("getHighlightColor", () => {
     expect(
       getHighlightColor({
         currentMatchLineIndex: 1,
-        hash: "",
         index: 3,
         searchMatchIndices: new Set([1, 3, 5]),
       }),
@@ -131,8 +129,8 @@ describe("getHighlightColor", () => {
   it("returns brand.emphasized for the URL-hash-linked line when no search is 
active", () => {
     expect(
       getHighlightColor({
-        hash: "5",
-        index: 4, // hash "5" maps to index 4 (1-based to 0-based)
+        hashIndex: 4,
+        index: 4,
         searchMatchIndices: undefined,
       }),
     ).toBe("brand.emphasized");
@@ -141,7 +139,6 @@ describe("getHighlightColor", () => {
   it("returns transparent when no condition matches", () => {
     expect(
       getHighlightColor({
-        hash: "",
         index: 2,
         searchMatchIndices: undefined,
       }),
@@ -152,7 +149,6 @@ describe("getHighlightColor", () => {
     expect(
       getHighlightColor({
         currentMatchLineIndex: 0,
-        hash: "",
         index: 7,
         searchMatchIndices: new Set([0, 2]),
       }),
@@ -163,7 +159,7 @@ describe("getHighlightColor", () => {
     expect(
       getHighlightColor({
         currentMatchLineIndex: 4,
-        hash: "5",
+        hashIndex: 4,
         index: 4,
         searchMatchIndices: new Set([4]),
       }),
@@ -174,7 +170,7 @@ describe("getHighlightColor", () => {
     expect(
       getHighlightColor({
         currentMatchLineIndex: 0,
-        hash: "5",
+        hashIndex: 4,
         index: 4,
         searchMatchIndices: new Set([0, 4]),
       }),
@@ -185,7 +181,6 @@ describe("getHighlightColor", () => {
     expect(
       getHighlightColor({
         currentMatchLineIndex: undefined,
-        hash: "",
         index: 0,
         searchMatchIndices: new Set(),
       }),
diff --git a/airflow-core/src/airflow/ui/src/pages/TaskInstance/Logs/utils.ts 
b/airflow-core/src/airflow/ui/src/pages/TaskInstance/Logs/utils.ts
index 424006498ee..248a89de2a5 100644
--- a/airflow-core/src/airflow/ui/src/pages/TaskInstance/Logs/utils.ts
+++ b/airflow-core/src/airflow/ui/src/pages/TaskInstance/Logs/utils.ts
@@ -85,7 +85,7 @@ export const getDownloadText = ({
 
 export type HighlightOptions = {
   currentMatchLineIndex?: number;
-  hash: string;
+  hashIndex?: number;
   index: number;
   searchMatchIndices?: Set<number>;
 };
@@ -97,7 +97,7 @@ export type HighlightOptions = {
  */
 export const getHighlightColor = ({
   currentMatchLineIndex,
-  hash,
+  hashIndex,
   index,
   searchMatchIndices,
 }: HighlightOptions): string => {
@@ -107,7 +107,7 @@ export const getHighlightColor = ({
   if (searchMatchIndices?.has(index)) {
     return "yellow.subtle";
   }
-  if (Boolean(hash) && index === Number(hash) - 1) {
+  if (hashIndex !== undefined && index === hashIndex) {
     return "brand.emphasized";
   }
 
diff --git a/airflow-core/src/airflow/ui/src/queries/useLogs.tsx 
b/airflow-core/src/airflow/ui/src/queries/useLogs.tsx
index 3d0443c04cd..52f54e9263a 100644
--- a/airflow-core/src/airflow/ui/src/queries/useLogs.tsx
+++ b/airflow-core/src/airflow/ui/src/queries/useLogs.tsx
@@ -37,6 +37,7 @@ import { parseStreamingLogContent } from "src/utils/logs";
 export type ParsedLogEntry = {
   element: JSX.Element | string | undefined;
   group?: { id: number; level: number; parentId?: number; type: "header" | 
"line" };
+  lineNumber?: number;
 };
 
 type Props = {
@@ -103,19 +104,22 @@ const parseLogs = ({
           }
         }
 
-        return renderStructuredLog({
-          index: lineNumbers[index] ?? index,
-          logLevelFilters,
-          logLink,
-          logMessage: datum,
-          renderingMode: "jsx",
-          showSource,
-          showTimestamp,
-          sourceFilters,
-          translate,
-        });
+        return {
+          element: renderStructuredLog({
+            index: lineNumbers[index] ?? index,
+            logLevelFilters,
+            logLink,
+            logMessage: datum,
+            renderingMode: "jsx",
+            showSource,
+            showTimestamp,
+            sourceFilters,
+            translate,
+          }),
+          lineNumber: lineNumbers[index],
+        };
       })
-      .filter((parsedLine) => parsedLine !== "");
+      .filter(({ element }) => element !== "");
   } catch (error) {
     const errorMessage = error instanceof Error ? error.message : "An error 
occurred.";
 
@@ -132,8 +136,8 @@ const parseLogs = ({
     const result: Array<ParsedLogEntry> = [];
     let nextGroupId = 0;
 
-    parsedLines.forEach((line) => {
-      const text = innerText(line);
+    parsedLines.forEach(({ element, lineNumber }) => {
+      const text = innerText(element);
 
       if (text.includes("::group::")) {
         const groupName = text.split("::group::")[1] as string;
@@ -162,11 +166,12 @@ const parseLogs = ({
 
       if (groupStack.length > 0 && currentGroup) {
         result.push({
-          element: line,
+          element,
           group: { id: currentGroup.id, level: currentGroup.level, type: 
"line" },
+          lineNumber,
         });
       } else {
-        result.push({ element: line });
+        result.push({ element, lineNumber });
       }
     });
 

Reply via email to