This is an automated email from the ASF dual-hosted git repository.
kaxilnaik pushed a commit to branch v3-0-test
in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/v3-0-test by this push:
new 1ef6852cc37 [v3-0-test] Update `TaskLogContent` to support virtualized
rendering (#50746) (#51202)
1ef6852cc37 is described below
commit 1ef6852cc3772490f063aab4207639bfd81b659c
Author: github-actions[bot]
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Tue Jun 3 16:27:22 2025 +0530
[v3-0-test] Update `TaskLogContent` to support virtualized rendering
(#50746) (#51202)
* Fix OpenAPI schema for `get_log` API (#50547)
* Fix openapi schema for get_log API
* Fix test_log
(cherry picked from commit 08cc57d5bba8a045d34bce39b28ac21af691c3a9)
* [v3-0-test] Update `TaskLogContent` to support virtualized rendering
(#50746)
* Update TaskLogContent to support virtualized rendering
* Update TaskLogPreview and Logs to handle undefined parsedLogs
(cherry picked from commit 813f3e3f15985c017b615b5e4a33ed9c4496fdbe)
Co-authored-by: Guan Ming(Wesley) Chiu
<[email protected]>
---------
Co-authored-by: LIU ZHE YOU <[email protected]>
Co-authored-by: Guan Ming(Wesley) Chiu
<[email protected]>
---
airflow-core/src/airflow/ui/package.json | 1 +
airflow-core/src/airflow/ui/pnpm-lock.yaml | 20 ++++++++++++
.../ui/src/pages/Dag/Overview/TaskLogPreview.tsx | 2 +-
.../ui/src/pages/TaskInstance/Logs/Logs.test.tsx | 21 +++++++++---
.../ui/src/pages/TaskInstance/Logs/Logs.tsx | 8 ++---
.../src/pages/TaskInstance/Logs/TaskLogContent.tsx | 38 ++++++++++++++++++----
.../src/airflow/ui/src/queries/useLogs.tsx | 4 ++-
7 files changed, 77 insertions(+), 17 deletions(-)
diff --git a/airflow-core/src/airflow/ui/package.json
b/airflow-core/src/airflow/ui/package.json
index d1a7b9773a2..83d4eb143b5 100644
--- a/airflow-core/src/airflow/ui/package.json
+++ b/airflow-core/src/airflow/ui/package.json
@@ -22,6 +22,7 @@
"@emotion/react": "^11.14.0",
"@tanstack/react-query": "^5.75.1",
"@tanstack/react-table": "^8.21.3",
+ "@tanstack/react-virtual": "^3.13.8",
"@types/debounce-promise": "^3.1.9",
"@uiw/codemirror-themes-all": "^4.23.12",
"@uiw/react-codemirror": "^4.23.12",
diff --git a/airflow-core/src/airflow/ui/pnpm-lock.yaml
b/airflow-core/src/airflow/ui/pnpm-lock.yaml
index 0bbcce382b0..5909fe6a143 100644
--- a/airflow-core/src/airflow/ui/pnpm-lock.yaml
+++ b/airflow-core/src/airflow/ui/pnpm-lock.yaml
@@ -26,6 +26,9 @@ importers:
'@tanstack/react-table':
specifier: ^8.21.3
version: 8.21.3([email protected]([email protected]))([email protected])
+ '@tanstack/react-virtual':
+ specifier: ^3.13.8
+ version: 3.13.8([email protected]([email protected]))([email protected])
'@types/debounce-promise':
specifier: ^3.1.9
version: 3.1.9
@@ -986,10 +989,19 @@ packages:
react: '>=16.8'
react-dom: '>=16.8'
+ '@tanstack/[email protected]':
+ resolution: {integrity:
sha512-meS2AanUg50f3FBSNoAdBSRAh8uS0ue01qm7zrw65KGJtiXB9QXfybqZwkh4uFpRv2iX/eu5tjcH5wqUpwYLPg==}
+ peerDependencies:
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+ react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+
'@tanstack/[email protected]':
resolution: {integrity:
sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==}
engines: {node: '>=12'}
+ '@tanstack/[email protected]':
+ resolution: {integrity:
sha512-BT6w89Hqy7YKaWewYzmecXQzcJh6HTBbKYJIIkMaNU49DZ06LoTV3z32DWWEdUsgW6n1xTmwTLs4GtWrZC261w==}
+
'@testing-library/[email protected]':
resolution: {integrity:
sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==}
engines: {node: '>=18'}
@@ -5236,8 +5248,16 @@ snapshots:
react: 18.3.1
react-dom: 18.3.1([email protected])
+
'@tanstack/[email protected]([email protected]([email protected]))([email protected])':
+ dependencies:
+ '@tanstack/virtual-core': 3.13.8
+ react: 18.3.1
+ react-dom: 18.3.1([email protected])
+
'@tanstack/[email protected]': {}
+ '@tanstack/[email protected]': {}
+
'@testing-library/[email protected]':
dependencies:
'@babel/code-frame': 7.27.1
diff --git
a/airflow-core/src/airflow/ui/src/pages/Dag/Overview/TaskLogPreview.tsx
b/airflow-core/src/airflow/ui/src/pages/Dag/Overview/TaskLogPreview.tsx
index 979f6bcfddf..a4b82e16803 100644
--- a/airflow-core/src/airflow/ui/src/pages/Dag/Overview/TaskLogPreview.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Dag/Overview/TaskLogPreview.tsx
@@ -67,7 +67,7 @@ export const TaskLogPreview = ({
error={error}
isLoading={isLoading}
logError={error}
- parsedLogs={data.parsedLogs}
+ parsedLogs={data.parsedLogs ?? []}
wrap={wrap}
/>
</Box>
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 e39b98cbaf9..af613d9dc61 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
@@ -17,7 +17,7 @@
* under the License.
*/
import "@testing-library/jest-dom";
-import { render, screen, waitFor } from "@testing-library/react";
+import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { setupServer, type SetupServerApi } from "msw/node";
import { afterEach, describe, it, expect, beforeAll, afterAll } from "vitest";
@@ -25,10 +25,17 @@ import { handlers } from "src/mocks/handlers";
import { AppWrapper } from "src/utils/AppWrapper";
let server: SetupServerApi;
+const ITEM_HEIGHT = 20;
beforeAll(() => {
server = setupServer(...handlers);
server.listen({ onUnhandledRequest: "bypass" });
+ Object.defineProperty(HTMLElement.prototype, "offsetHeight", {
+ value: ITEM_HEIGHT,
+ });
+ Object.defineProperty(HTMLElement.prototype, "offsetWidth", {
+ value: 800,
+ });
});
afterEach(() => server.resetHandlers());
@@ -39,14 +46,18 @@ describe("Task log grouping", () => {
render(
<AppWrapper
initialEntries={["/dags/log_grouping/runs/manual__2025-02-18T12:19/tasks/generate"]}
/>,
);
+ await waitFor(() =>
expect(screen.queryByTestId("virtualized-list")).toBeInTheDocument());
+ await waitFor(() =>
expect(screen.queryByTestId("virtualized-item-0")).toBeInTheDocument());
+ await waitFor(() =>
expect(screen.queryByTestId("virtualized-item-10")).toBeInTheDocument());
- await waitFor(() => expect(screen.queryByTestId("summary-Pre task
execution logs")).toBeInTheDocument(), {
- timeout: 10_000,
- });
+ fireEvent.scroll(screen.getByTestId("virtualized-list"), { target: {
scrollTop: ITEM_HEIGHT * 6 } });
+ await waitFor(() =>
expect(screen.queryByTestId("virtualized-item-16")).toBeInTheDocument());
+
+ await waitFor(() => expect(screen.queryByTestId("summary-Pre task
execution logs")).toBeInTheDocument());
await waitFor(() => expect(screen.getByTestId("summary-Pre task execution
logs")).toBeVisible());
await waitFor(() => expect(screen.queryByText(/Task instance is in running
state/iu)).not.toBeVisible());
await waitFor(() => screen.getByTestId("summary-Pre task execution
logs").click());
await waitFor(() => expect(screen.queryByText(/Task instance is in running
state/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 4e2c7af02d1..3b9864a6db3 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
@@ -88,7 +88,7 @@ export const Logs = () => {
});
return (
- <Box p={2}>
+ <Box display="flex" flexDirection="column" h="100%" p={2}>
<TaskLogHeader
onSelectTryNumber={onSelectTryNumber}
sourceOptions={data.sources}
@@ -102,7 +102,7 @@ export const Logs = () => {
error={error}
isLoading={isLoading || isLoadingLogs}
logError={logError}
- parsedLogs={data.parsedLogs}
+ parsedLogs={data.parsedLogs ?? []}
wrap={wrap}
/>
<Dialog.Root onOpenChange={onOpenChange} open={fullscreen}
scrollBehavior="inside" size="full">
@@ -124,12 +124,12 @@ export const Logs = () => {
<Dialog.CloseTrigger />
- <Dialog.Body>
+ <Dialog.Body display="flex" flexDirection="column">
<TaskLogContent
error={error}
isLoading={isLoading || isLoadingLogs}
logError={logError}
- parsedLogs={data.parsedLogs}
+ parsedLogs={data.parsedLogs ?? []}
wrap={wrap}
/>
</Dialog.Body>
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 db62882ad08..e2c5109ab41 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
@@ -17,8 +17,8 @@
* under the License.
*/
import { Box, Code, VStack, useToken } from "@chakra-ui/react";
-import type { ReactNode } from "react";
-import { useLayoutEffect } from "react";
+import { useVirtualizer } from "@tanstack/react-virtual";
+import { useLayoutEffect, useRef } from "react";
import { ErrorAlert } from "src/components/ErrorAlert";
import { ProgressBar } from "src/components/ui";
@@ -27,12 +27,19 @@ type Props = {
readonly error: unknown;
readonly isLoading: boolean;
readonly logError: unknown;
- readonly parsedLogs: ReactNode;
+ readonly parsedLogs: Array<JSX.Element | string | undefined>;
readonly wrap: boolean;
};
export const TaskLogContent = ({ error, isLoading, logError, parsedLogs, wrap
}: Props) => {
const [bgLine] = useToken("colors", ["blue.emphasized"]);
+ const parentRef = useRef(null);
+ const rowVirtualizer = useVirtualizer({
+ count: parsedLogs.length,
+ estimateSize: () => 20,
+ getScrollElement: () => parentRef.current,
+ overscan: 10,
+ });
useLayoutEffect(() => {
if (location.hash) {
@@ -53,7 +60,7 @@ export const TaskLogContent = ({ error, isLoading, logError,
parsedLogs, wrap }:
}, [isLoading, bgLine]);
return (
- <Box>
+ <Box display="flex" flexDirection="column" flexGrow={1} h="100%"
minHeight={0}>
<ErrorAlert error={error ?? logError} />
<ProgressBar size="xs" visibility={isLoading ? "visible" : "hidden"} />
<Code
@@ -62,13 +69,32 @@ export const TaskLogContent = ({ error, isLoading,
logError, parsedLogs, wrap }:
bg: "blue.subtle",
},
}}
+ data-testid="virtualized-list"
+ flexGrow={1}
+ h="auto"
overflow="auto"
+ position="relative"
py={3}
+ ref={parentRef}
textWrap={wrap ? "pre" : "nowrap"}
width="100%"
>
- <VStack alignItems="flex-start" gap={0}>
- {parsedLogs}
+ <VStack alignItems="flex-start" gap={0}
h={`${rowVirtualizer.getTotalSize()}px`}>
+ {rowVirtualizer.getVirtualItems().map((virtualRow) => (
+ <Box
+ data-index={virtualRow.index}
+ data-testid={`virtualized-item-${virtualRow.index}`}
+ key={virtualRow.key}
+ left={0}
+ position="absolute"
+ ref={rowVirtualizer.measureElement}
+ top={0}
+ transform={`translateY(${virtualRow.start}px)`}
+ width={wrap ? "100%" : "max-content"}
+ >
+ {parsedLogs[virtualRow.index] ?? undefined}
+ </Box>
+ ))}
</VStack>
</Code>
</Box>
diff --git a/airflow-core/src/airflow/ui/src/queries/useLogs.tsx
b/airflow-core/src/airflow/ui/src/queries/useLogs.tsx
index 3007deee70d..01b476f93c9 100644
--- a/airflow-core/src/airflow/ui/src/queries/useLogs.tsx
+++ b/airflow-core/src/airflow/ui/src/queries/useLogs.tsx
@@ -28,6 +28,7 @@ import { isStatePending, useAutoRefresh } from "src/utils";
import { getTaskInstanceLink } from "src/utils/links";
type Props = {
+ accept?: "*/*" | "application/json" | "application/x-ndjson";
dagId: string;
logLevelFilters?: Array<string>;
sourceFilters?: Array<string>;
@@ -120,13 +121,14 @@ const parseLogs = ({ data, logLevelFilters,
sourceFilters, taskInstance, tryNumb
};
export const useLogs = (
- { dagId, logLevelFilters, sourceFilters, taskInstance, tryNumber = 1 }:
Props,
+ { accept = "application/json", dagId, logLevelFilters, sourceFilters,
taskInstance, tryNumber = 1 }: Props,
options?: Omit<UseQueryOptions<TaskInstancesLogResponse>, "queryFn" |
"queryKey">,
) => {
const refetchInterval = useAutoRefresh({ dagId });
const { data, ...rest } = useTaskInstanceServiceGetLog(
{
+ accept,
dagId,
dagRunId: taskInstance?.dag_run_id ?? "",
mapIndex: taskInstance?.map_index ?? -1,