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

jedcunningham pushed a commit to branch ui-hide-empty-log-groups
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/ui-hide-empty-log-groups by 
this push:
     new 793758c6851 Hide empty log groups in the task log view
793758c6851 is described below

commit 793758c68514af6c5ea1e49cd4c23374fb38d5d3
Author: Jed Cunningham <[email protected]>
AuthorDate: Mon Oct 26 12:11:08 2026 -0600

    Hide empty log groups in the task log view
    
    The task runner always opens a "Post Execute" log group after execute()
    returns, but for a plain task nothing is logged at INFO before it is
    closed, so the UI showed a section header that expanded to nothing.
    
    When parsing logs, drop a group header if the group closes with no
    entries in it. A parent containing only empty groups is dropped too.
    Unclosed groups are kept so a running task still shows its current
    section.
---
 .../src/airflow/ui/src/queries/useLogs.test.ts     | 38 ++++++++++++++++++++++
 .../src/airflow/ui/src/queries/useLogs.tsx         | 11 +++++--
 2 files changed, 46 insertions(+), 3 deletions(-)

diff --git a/airflow-core/src/airflow/ui/src/queries/useLogs.test.ts 
b/airflow-core/src/airflow/ui/src/queries/useLogs.test.ts
index 958bb86d515..68d824b02be 100644
--- a/airflow-core/src/airflow/ui/src/queries/useLogs.test.ts
+++ b/airflow-core/src/airflow/ui/src/queries/useLogs.test.ts
@@ -31,6 +31,13 @@ import { getLogLineText, parseLogs } from "./useLogs";
 
 const translate = ((key: string) => key) as unknown as TFunction;
 
+const msg = (event: string) =>
+  ({ event, level: "info", timestamp: "2026-01-01T00:00:00Z" }) as 
StructuredLogMessage;
+const headers = (data: Array<StructuredLogMessage>) =>
+  (parseLogs({ data, translate, tryNumber: 1 }).parsedLogs ?? [])
+    .filter((entry) => entry.group?.type === "header")
+    .map((entry) => entry.element);
+
 describe("getLogLineText", () => {
   it("renders a structured line with timestamp and level, stripping ANSI 
codes", () => {
     const logMessage = {
@@ -108,3 +115,34 @@ describe("parseLogs line links", () => {
     expect(screen.getByRole("link", { name: "0" })).toHaveAttribute("href", 
href);
   });
 });
+
+describe("parseLogs log groups", () => {
+  it("drops a closed group with no entries", () => {
+    expect(
+      headers([
+        msg("::group::Pre Execute"),
+        msg("starting"),
+        msg("::endgroup::"),
+        msg("Done."),
+        msg("::group::Post Execute"),
+        msg("::endgroup::"),
+      ]),
+    ).toEqual(["Pre Execute"]);
+  });
+
+  it("keeps a closed group that has entries", () => {
+    expect(headers([msg("::group::Post Execute"), msg("post hook ran"), 
msg("::endgroup::")])).toEqual([
+      "Post Execute",
+    ]);
+  });
+
+  it("drops a parent group that only contains empty groups", () => {
+    expect(
+      headers([msg("::group::Outer"), msg("::group::Inner"), 
msg("::endgroup::"), msg("::endgroup::")]),
+    ).toEqual([]);
+  });
+
+  it("keeps an unclosed empty group so a running task still shows its current 
section", () => {
+    expect(headers([msg("::group::Post Execute")])).toEqual(["Post Execute"]);
+  });
+});
diff --git a/airflow-core/src/airflow/ui/src/queries/useLogs.tsx 
b/airflow-core/src/airflow/ui/src/queries/useLogs.tsx
index 759fa1a636b..ba8a7969da8 100644
--- a/airflow-core/src/airflow/ui/src/queries/useLogs.tsx
+++ b/airflow-core/src/airflow/ui/src/queries/useLogs.tsx
@@ -195,7 +195,7 @@ export const parseLogs = ({
   }
 
   const flatEntries: Array<ParsedLogEntry> = (() => {
-    type Group = { id: number; level: number; name: string };
+    type Group = { headerIndex: number; id: number; level: number; name: 
string };
     const groupStack: Array<Group> = [];
     const result: Array<ParsedLogEntry> = [];
     let nextGroupId = 0;
@@ -221,7 +221,7 @@ export const parseLogs = ({
         const level = groupStack.length;
         const parentGroup = groupStack[groupStack.length - 1];
 
-        groupStack.push({ id, level, name: groupName });
+        groupStack.push({ headerIndex: result.length, id, level, name: 
groupName });
         result.push({
           element: groupName,
           group: { id, level, parentId: parentGroup?.id, type: "header" },
@@ -231,7 +231,12 @@ export const parseLogs = ({
       }
 
       if (text.includes("::endgroup::")) {
-        groupStack.pop();
+        const closedGroup = groupStack.pop();
+
+        // Hide groups that closed with nothing in them.
+        if (closedGroup !== undefined && result.length === 
closedGroup.headerIndex + 1) {
+          result.pop();
+        }
 
         return;
       }

Reply via email to