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,

Reply via email to