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