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

jscheffl 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 fc993d3d648 feat: Add toggle to hide/show log timestamps in UI (#52996)
fc993d3d648 is described below

commit fc993d3d64890600c2a236fa16e0890a02f7d264
Author: Naseem Shah <[email protected]>
AuthorDate: Fri Jul 11 02:55:37 2025 +0530

    feat: Add toggle to hide/show log timestamps in UI (#52996)
    
    * feat: Add toggle to hide/show log timestamps in UI
    
    * feat: add default_show_timestamp to the api server config and useConfig 
hook.
    
    * feat: persist toggle wrap and toggle timestamp in TI logs
    
    * feat: group log display options into single menu
    
    * feat: remove exisitng wrap button
    
    * refactor: remove default_show_timestamp from server config, default to 
true in UI
    
    * feat: add hotkey for toggleTimestamp
    
    * feat: move expand options to the drop down menu
    
    * feat: show/hide source in logs
    
    * chore: update Log Settings entry
    
    * fix: dropdown menu not appearing after clicking Logs settigns button
    
    * feat: add missing hotkey for toggleSource
    
    * feat: update logs test to use new expand button
---
 .../airflow/ui/public/i18n/locales/en/common.json  | 12 +++
 .../src/airflow/ui/public/i18n/locales/en/dag.json |  1 +
 .../ui/src/components/renderStructuredLog.tsx      | 24 +++---
 .../ui/src/pages/TaskInstance/Logs/Logs.test.tsx   | 18 +++--
 .../ui/src/pages/TaskInstance/Logs/Logs.tsx        | 23 +++++-
 .../src/pages/TaskInstance/Logs/TaskLogHeader.tsx  | 91 ++++++++++++----------
 .../src/airflow/ui/src/queries/useLogs.tsx         | 12 +++
 7 files changed, 124 insertions(+), 57 deletions(-)

diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/en/common.json 
b/airflow-core/src/airflow/ui/public/i18n/locales/en/common.json
index d437f984494..ff724bc4886 100644
--- a/airflow-core/src/airflow/ui/public/i18n/locales/en/common.json
+++ b/airflow-core/src/airflow/ui/public/i18n/locales/en/common.json
@@ -81,6 +81,7 @@
   "expand":{
     "collapse": "Collapse",
     "expand": "Expand",
+    "hotkey": "e",
     "tooltip": "Press {{hotkey}} to toggle expand"
   },
   "expression": {
@@ -153,6 +154,11 @@
   },
   "selectLanguage": "Select Language",
   "showDetailsPanel": "Show Details Panel",
+  "source": {
+    "hide": "Hide Source",
+    "hotkey": "s",
+    "show": "Show Source"
+  },
   "sourceAssetEvent_one": "Source Asset Event",
   "sourceAssetEvent_other": "Source Asset Events",
   "startDate": "Start Date",
@@ -232,6 +238,11 @@
     "lastHour": "Last Hour",
     "pastWeek": "Past Week"
   },
+  "timestamp": {
+    "hide": "Hide Timestamps",
+    "hotkey": "t",
+    "show": "Show Timestamps"
+  },
   "timezone": "Timezone",
   "timezoneModal": {
     "current-timezone": "Current time in",
@@ -281,6 +292,7 @@
   "tryNumber": "Try Number",
   "user": "User",
   "wrap": {
+    "hotkey": "w",
     "tooltip": "Press {{hotkey}} to toggle wrap",
     "unwrap": "Unwrap",
     "wrap": "Wrap"
diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/en/dag.json 
b/airflow-core/src/airflow/ui/public/i18n/locales/en/dag.json
index 92b8e59496c..6654bf3b561 100644
--- a/airflow-core/src/airflow/ui/public/i18n/locales/en/dag.json
+++ b/airflow-core/src/airflow/ui/public/i18n/locales/en/dag.json
@@ -34,6 +34,7 @@
     },
     "info": "INFO",
     "noTryNumber": "No try number",
+    "settings": "Log Settings",
     "viewInExternal": "View logs in {{name}} (attempt {{attempt}})",
     "warning": "WARNING"
   },
diff --git a/airflow-core/src/airflow/ui/src/components/renderStructuredLog.tsx 
b/airflow-core/src/airflow/ui/src/components/renderStructuredLog.tsx
index 2c410bfd83b..2ad930cc9bb 100644
--- a/airflow-core/src/airflow/ui/src/components/renderStructuredLog.tsx
+++ b/airflow-core/src/airflow/ui/src/components/renderStructuredLog.tsx
@@ -45,6 +45,8 @@ type RenderStructuredLogProps = {
   logLevelFilters?: Array<string>;
   logLink: string;
   logMessage: string | StructuredLogMessage;
+  showSource?: boolean;
+  showTimestamp?: boolean;
   sourceFilters?: Array<string>;
   translate: TFunction;
 };
@@ -95,6 +97,8 @@ export const renderStructuredLog = ({
   logLevelFilters,
   logLink,
   logMessage,
+  showSource = true,
+  showTimestamp = true,
   sourceFilters,
   translate,
 }: RenderStructuredLogProps) => {
@@ -127,7 +131,7 @@ export const renderStructuredLog = ({
     return "";
   }
 
-  if (Boolean(timestamp)) {
+  if (Boolean(timestamp) && showTimestamp) {
     elements.push("[", <Time datetime={timestamp} key={0} />, "] ");
   }
 
@@ -178,14 +182,16 @@ export const renderStructuredLog = ({
     </chakra.span>,
   );
 
-  for (const key in reStructured) {
-    if (Object.hasOwn(reStructured, key)) {
-      elements.push(
-        ": ",
-        <chakra.span color={key === "logger" ? "fg.info" : undefined} 
key={`prop_${key}`}>
-          {key === "logger" ? "source" : 
key}={JSON.stringify(reStructured[key])}
-        </chakra.span>,
-      );
+  if (showSource) {
+    for (const key in reStructured) {
+      if (Object.hasOwn(reStructured, key)) {
+        elements.push(
+          ": ",
+          <chakra.span color={key === "logger" ? "fg.info" : undefined} 
key={`prop_${key}`}>
+            {key === "logger" ? "source" : 
key}={JSON.stringify(reStructured[key])}
+          </chakra.span>,
+        );
+      }
     }
   }
 
diff --git 
a/airflow-core/src/airflow/ui/src/pages/TaskInstance/Logs/Logs.test.tsx 
b/airflow-core/src/airflow/ui/src/pages/TaskInstance/Logs/Logs.test.tsx
index 7a5e5ec8aff..05972ebcd37 100644
--- a/airflow-core/src/airflow/ui/src/pages/TaskInstance/Logs/Logs.test.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/TaskInstance/Logs/Logs.test.tsx
@@ -84,16 +84,20 @@ describe("Task log grouping", () => {
     fireEvent.click(summaryPost);
     await waitFor(() => expect(screen.queryByText(/Marking task as 
SUCCESS/iu)).toBeVisible());
 
-    const expandBtn = screen.getByRole("button", { name: /expand\.expand/iu });
+    const settingsBtn = screen.getByRole("button", { name: /settings/iu });
 
-    fireEvent.click(expandBtn);
+    fireEvent.click(settingsBtn);
 
-    expect(screen.getByText(/Marking task as SUCCESS/iu)).toBeVisible();
+    const expandItem = await screen.findByRole("menuitem", { name: /expand/iu 
});
 
-    const collapseBtn = screen.getByRole("button", { name: 
/expand\.collapse/iu });
+    fireEvent.click(expandItem);
 
-    fireEvent.click(collapseBtn);
-    await waitFor(() => expect(screen.queryByText(/Task instance is in running 
state/iu)).not.toBeVisible());
-    await waitFor(() => expect(screen.queryByText(/Marking task as 
SUCCESS/iu)).not.toBeVisible());
+    /* ─── NEW: open again & click  "Collapse"  ─── */
+    fireEvent.click(settingsBtn); // menu is closed after previous click, so 
reopen
+    const collapseItem = await screen.findByRole("menuitem", { name: 
/collapse/iu });
+
+    fireEvent.click(collapseItem);
+
+    expect(screen.getByText(/Marking task as SUCCESS/iu)).toBeVisible();
   }, 10_000);
 });
diff --git a/airflow-core/src/airflow/ui/src/pages/TaskInstance/Logs/Logs.tsx 
b/airflow-core/src/airflow/ui/src/pages/TaskInstance/Logs/Logs.tsx
index a96f0c969c9..247cb143b5a 100644
--- a/airflow-core/src/airflow/ui/src/pages/TaskInstance/Logs/Logs.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/TaskInstance/Logs/Logs.tsx
@@ -21,6 +21,7 @@ import { useState } from "react";
 import { useHotkeys } from "react-hotkeys-hook";
 import { useTranslation } from "react-i18next";
 import { useParams, useSearchParams } from "react-router-dom";
+import { useLocalStorage } from "usehooks-ts";
 
 import { useTaskInstanceServiceGetMappedTaskInstance } from "openapi/queries";
 import { Dialog } from "src/components/ui";
@@ -64,18 +65,28 @@ export const Logs = () => {
   const tryNumber = tryNumberParam === null ? taskInstance?.try_number : 
parseInt(tryNumberParam, 10);
 
   const defaultWrap = Boolean(useConfig("default_wrap"));
+  const defaultShowTimestamp = Boolean(true);
 
-  const [wrap, setWrap] = useState(defaultWrap);
+  const [wrap, setWrap] = useLocalStorage<boolean>("log_wrap", defaultWrap);
+  const [showTimestamp, setShowTimestamp] = useLocalStorage<boolean>(
+    "log_show_timestamp",
+    defaultShowTimestamp,
+  );
+  const [showSource, setShowSource] = 
useLocalStorage<boolean>("log_show_source", true);
   const [fullscreen, setFullscreen] = useState(false);
   const [expanded, setExpanded] = useState(false);
 
   const toggleWrap = () => setWrap(!wrap);
+  const toggleTimestamp = () => setShowTimestamp(!showTimestamp);
+  const toggleSource = () => setShowSource(!showSource);
   const toggleFullscreen = () => setFullscreen(!fullscreen);
   const toggleExpanded = () => setExpanded((act) => !act);
 
   useHotkeys("w", toggleWrap);
   useHotkeys("f", toggleFullscreen);
   useHotkeys("e", toggleExpanded);
+  useHotkeys("t", toggleTimestamp);
+  useHotkeys("s", toggleSource);
 
   const onOpenChange = () => {
     setFullscreen(false);
@@ -89,6 +100,8 @@ export const Logs = () => {
     dagId,
     expanded,
     logLevelFilters,
+    showSource,
+    showTimestamp,
     sourceFilters,
     taskInstance,
     tryNumber,
@@ -102,10 +115,14 @@ export const Logs = () => {
       <TaskLogHeader
         expanded={expanded}
         onSelectTryNumber={onSelectTryNumber}
+        showSource={showSource}
+        showTimestamp={showTimestamp}
         sourceOptions={data.sources}
         taskInstance={taskInstance}
         toggleExpanded={toggleExpanded}
         toggleFullscreen={toggleFullscreen}
+        toggleSource={toggleSource}
+        toggleTimestamp={toggleTimestamp}
         toggleWrap={toggleWrap}
         tryNumber={tryNumber}
         wrap={wrap}
@@ -137,9 +154,13 @@ export const Logs = () => {
                 expanded={expanded}
                 isFullscreen
                 onSelectTryNumber={onSelectTryNumber}
+                showSource={showSource}
+                showTimestamp={showTimestamp}
                 taskInstance={taskInstance}
                 toggleExpanded={toggleExpanded}
                 toggleFullscreen={toggleFullscreen}
+                toggleSource={toggleSource}
+                toggleTimestamp={toggleTimestamp}
                 toggleWrap={toggleWrap}
                 tryNumber={tryNumber}
                 wrap={wrap}
diff --git 
a/airflow-core/src/airflow/ui/src/pages/TaskInstance/Logs/TaskLogHeader.tsx 
b/airflow-core/src/airflow/ui/src/pages/TaskInstance/Logs/TaskLogHeader.tsx
index f4e0a2fa923..ce994b2caf9 100644
--- a/airflow-core/src/airflow/ui/src/pages/TaskInstance/Logs/TaskLogHeader.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/TaskInstance/Logs/TaskLogHeader.tsx
@@ -19,7 +19,6 @@
 import {
   Badge,
   Box,
-  ButtonGroup,
   createListCollection,
   HStack,
   IconButton,
@@ -27,12 +26,20 @@ import {
 } from "@chakra-ui/react";
 import { useCallback } from "react";
 import { useTranslation } from "react-i18next";
-import { MdCompress, MdExpand, MdOutlineOpenInFull } from "react-icons/md";
+import {
+  MdAccessTime,
+  MdCode,
+  MdCompress,
+  MdExpand,
+  MdOutlineOpenInFull,
+  MdSettings,
+  MdWrapText,
+} from "react-icons/md";
 import { useSearchParams } from "react-router-dom";
 
 import type { TaskInstanceResponse } from "openapi/requests/types.gen";
 import { TaskTrySelect } from "src/components/TaskTrySelect";
-import { Button, Select, Tooltip } from "src/components/ui";
+import { Button, Menu, Select, Tooltip } from "src/components/ui";
 import { SearchParamsKeys } from "src/constants/searchParams";
 import { system } from "src/theme";
 import { type LogLevel, logLevelColorMapping, logLevelOptions } from 
"src/utils/logs";
@@ -41,10 +48,14 @@ type Props = {
   readonly expanded?: boolean;
   readonly isFullscreen?: boolean;
   readonly onSelectTryNumber: (tryNumber: number) => void;
+  readonly showSource: boolean;
+  readonly showTimestamp: boolean;
   readonly sourceOptions?: Array<string>;
   readonly taskInstance?: TaskInstanceResponse;
   readonly toggleExpanded?: () => void;
   readonly toggleFullscreen: () => void;
+  readonly toggleSource: () => void;
+  readonly toggleTimestamp: () => void;
   readonly toggleWrap: () => void;
   readonly tryNumber?: number;
   readonly wrap: boolean;
@@ -54,10 +65,14 @@ export const TaskLogHeader = ({
   expanded,
   isFullscreen = false,
   onSelectTryNumber,
+  showSource,
+  showTimestamp,
   sourceOptions,
   taskInstance,
   toggleExpanded,
   toggleFullscreen,
+  toggleSource,
+  toggleTimestamp,
   toggleWrap,
   tryNumber,
   wrap,
@@ -188,43 +203,39 @@ export const TaskLogHeader = ({
           </Select.Root>
         ) : undefined}
         <HStack gap={1}>
-          <Tooltip closeDelay={100} content={translate("wrap.tooltip", { 
hotkey: "w" })} openDelay={100}>
-            <Button
-              aria-label={wrap ? translate("wrap.unwrap") : 
translate("wrap.wrap")}
-              bg="bg.panel"
-              m={0}
-              onClick={toggleWrap}
-              px={4}
-              py={2}
-              variant="outline"
-            >
-              {wrap ? translate("wrap.unwrap") : translate("wrap.wrap")}
-            </Button>
-          </Tooltip>
-          <Tooltip closeDelay={100} content={translate("expand.tooltip", { 
hotkey: "e" })} openDelay={100}>
-            <ButtonGroup attached size="md" variant="outline">
-              <IconButton
-                aria-label={translate("expand.expand")}
-                bg="bg.panel"
-                disabled={expanded}
-                onClick={expanded ? undefined : toggleExpanded}
-                size="md"
-                variant="surface"
-              >
-                <MdExpand />
-              </IconButton>
-              <IconButton
-                aria-label={translate("expand.collapse")}
-                bg="bg.panel"
-                disabled={!expanded}
-                onClick={expanded ? toggleExpanded : undefined}
-                size="md"
-                variant="outline"
-              >
-                <MdCompress />
-              </IconButton>
-            </ButtonGroup>
-          </Tooltip>
+          <Menu.Root>
+            <Menu.Trigger asChild>
+              <Button variant="outline">
+                <MdSettings /> {translate("dag:logs.settings")}
+              </Button>
+            </Menu.Trigger>
+            <Menu.Content zIndex={zIndex}>
+              <Menu.Item onClick={toggleWrap} value="wrap">
+                <MdWrapText /> {wrap ? translate("wrap.unwrap") : 
translate("wrap.wrap")}
+                <Menu.ItemCommand>{translate("wrap.hotkey")}</Menu.ItemCommand>
+              </Menu.Item>
+              <Menu.Item onClick={toggleTimestamp} value="timestamp">
+                <MdAccessTime /> {showTimestamp ? translate("timestamp.hide") 
: translate("timestamp.show")}
+                
<Menu.ItemCommand>{translate("timestamp.hotkey")}</Menu.ItemCommand>
+              </Menu.Item>
+              <Menu.Item onClick={toggleExpanded} value="expand">
+                {expanded ? (
+                  <>
+                    <MdCompress /> {translate("expand.collapse")}
+                  </>
+                ) : (
+                  <>
+                    <MdExpand /> {translate("expand.expand")}
+                  </>
+                )}
+                
<Menu.ItemCommand>{translate("expand.hotkey")}</Menu.ItemCommand>
+              </Menu.Item>
+              <Menu.Item onClick={toggleSource} value="source">
+                <MdCode /> {showSource ? translate("source.hide") : 
translate("source.show")}
+                
<Menu.ItemCommand>{translate("source.hotkey")}</Menu.ItemCommand>
+              </Menu.Item>
+            </Menu.Content>
+          </Menu.Root>
           {!isFullscreen && (
             <Tooltip
               closeDelay={100}
diff --git a/airflow-core/src/airflow/ui/src/queries/useLogs.tsx 
b/airflow-core/src/airflow/ui/src/queries/useLogs.tsx
index 55f692e2da6..fae84463789 100644
--- a/airflow-core/src/airflow/ui/src/queries/useLogs.tsx
+++ b/airflow-core/src/airflow/ui/src/queries/useLogs.tsx
@@ -34,6 +34,8 @@ type Props = {
   dagId: string;
   expanded?: boolean;
   logLevelFilters?: Array<string>;
+  showSource?: boolean;
+  showTimestamp?: boolean;
   sourceFilters?: Array<string>;
   taskInstance?: TaskInstanceResponse;
   tryNumber?: number;
@@ -43,6 +45,8 @@ type ParseLogsProps = {
   data: TaskInstancesLogResponse["content"];
   expanded?: boolean;
   logLevelFilters?: Array<string>;
+  showSource?: boolean;
+  showTimestamp?: boolean;
   sourceFilters?: Array<string>;
   taskInstance?: TaskInstanceResponse;
   translate: TFunction;
@@ -53,6 +57,8 @@ const parseLogs = ({
   data,
   expanded,
   logLevelFilters,
+  showSource,
+  showTimestamp,
   sourceFilters,
   taskInstance,
   translate,
@@ -80,6 +86,8 @@ const parseLogs = ({
         logLevelFilters,
         logLink,
         logMessage: datum,
+        showSource,
+        showTimestamp,
         sourceFilters,
         translate,
       });
@@ -174,6 +182,8 @@ export const useLogs = (
     dagId,
     expanded,
     logLevelFilters,
+    showSource,
+    showTimestamp,
     sourceFilters,
     taskInstance,
     tryNumber = 1,
@@ -208,6 +218,8 @@ export const useLogs = (
     data: data?.content ?? [],
     expanded,
     logLevelFilters,
+    showSource,
+    showTimestamp,
     sourceFilters,
     taskInstance,
     translate,

Reply via email to