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,