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

pierrejeambrun 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 4766f9c8685 Fix slow log scrolling for large task logs (#60806)
4766f9c8685 is described below

commit 4766f9c8685e547b44762f2f5c485489bddae12d
Author: Guan-Ming (Wesley) Chiu <[email protected]>
AuthorDate: Wed Jan 21 21:59:32 2026 +0800

    Fix slow log scrolling for large task logs (#60806)
    
    * Fix slow log scrolling for large task logs
    
    * Trim to focus on scroll top/bottom
---
 .../src/pages/TaskInstance/Logs/TaskLogContent.tsx | 97 +++++++++++++---------
 .../ui/src/pages/TaskInstance/Logs/utils.ts        | 51 ++++++++++++
 2 files changed, 109 insertions(+), 39 deletions(-)

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 7b79cd89c60..68ae198b2a9 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
@@ -27,6 +27,8 @@ import { ErrorAlert } from "src/components/ErrorAlert";
 import { ProgressBar, Tooltip } from "src/components/ui";
 import { getMetaKey } from "src/utils";
 
+import { scrollToBottom, scrollToTop } from "./utils";
+
 type Props = {
   readonly error: unknown;
   readonly isLoading: boolean;
@@ -79,7 +81,8 @@ const ScrollToButton = ({
 
 export const TaskLogContent = ({ error, isLoading, logError, parsedLogs, wrap 
}: Props) => {
   const hash = location.hash.replace("#", "");
-  const parentRef = useRef(null);
+  const parentRef = useRef<HTMLDivElement | null>(null);
+
   const rowVirtualizer = useVirtualizer({
     count: parsedLogs.length,
     estimateSize: () => 20,
@@ -98,8 +101,20 @@ export const TaskLogContent = ({ error, isLoading, 
logError, parsedLogs, wrap }:
   }, [isLoading, rowVirtualizer, hash, parsedLogs]);
 
   const handleScrollTo = (to: "bottom" | "top") => {
-    if (parsedLogs.length > 0) {
-      rowVirtualizer.scrollToIndex(to === "bottom" ? parsedLogs.length - 1 : 
0);
+    if (parsedLogs.length === 0) {
+      return;
+    }
+
+    const el = rowVirtualizer.scrollElement ?? parentRef.current;
+
+    if (!el) {
+      return;
+    }
+
+    if (to === "top") {
+      scrollToTop({ element: el, virtualizer: rowVirtualizer });
+    } else {
+      scrollToBottom({ element: el, virtualizer: rowVirtualizer });
     }
   };
 
@@ -110,49 +125,53 @@ export const TaskLogContent = ({ error, isLoading, 
logError, parsedLogs, wrap }:
     <Box display="flex" flexDirection="column" flexGrow={1} h="100%" 
minHeight={0} position="relative">
       <ErrorAlert error={error ?? logError} />
       <ProgressBar size="xs" visibility={isLoading ? "visible" : "hidden"} />
-      <Code
-        css={{
-          "& *::selection": {
-            bg: "blue.emphasized",
-          },
-        }}
-        data-testid="virtualized-list"
+      <Box
+        data-testid="virtual-scroll-container"
         flexGrow={1}
-        h="auto"
+        minHeight={0}
         overflow="auto"
         position="relative"
         py={3}
         ref={parentRef}
-        textWrap={wrap ? "pre" : "nowrap"}
         width="100%"
       >
-        <VStack alignItems="flex-start" gap={0} 
h={`${rowVirtualizer.getTotalSize()}px`}>
-          {rowVirtualizer.getVirtualItems().map((virtualRow) => (
-            <Box
-              _ltr={{
-                left: 0,
-                right: "auto",
-              }}
-              _rtl={{
-                left: "auto",
-                right: 0,
-              }}
-              bgColor={
-                Boolean(hash) && virtualRow.index === Number(hash) - 1 ? 
"brand.emphasized" : "transparent"
-              }
-              data-index={virtualRow.index}
-              data-testid={`virtualized-item-${virtualRow.index}`}
-              key={virtualRow.key}
-              position="absolute"
-              ref={rowVirtualizer.measureElement}
-              top={`${virtualRow.start}px`}
-              width={wrap ? "100%" : "max-content"}
-            >
-              {parsedLogs[virtualRow.index] ?? undefined}
-            </Box>
-          ))}
-        </VStack>
-      </Code>
+        <Code
+          css={{
+            "& *::selection": { bg: "blue.emphasized" },
+          }}
+          data-testid="virtualized-list"
+          display="block"
+          textWrap={wrap ? "pre" : "nowrap"}
+          width="100%"
+        >
+          <VStack
+            alignItems="flex-start"
+            gap={0}
+            h={`${rowVirtualizer.getTotalSize()}px`}
+            position="relative"
+          >
+            {rowVirtualizer.getVirtualItems().map((virtualRow) => (
+              <Box
+                _ltr={{ left: 0, right: "auto" }}
+                _rtl={{ left: "auto", right: 0 }}
+                bgColor={
+                  Boolean(hash) && virtualRow.index === Number(hash) - 1 ? 
"brand.emphasized" : "transparent"
+                }
+                data-index={virtualRow.index}
+                data-testid={`virtualized-item-${virtualRow.index}`}
+                key={virtualRow.key}
+                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>
 
       {showScrollButtons ? (
         <>
diff --git a/airflow-core/src/airflow/ui/src/pages/TaskInstance/Logs/utils.ts 
b/airflow-core/src/airflow/ui/src/pages/TaskInstance/Logs/utils.ts
new file mode 100644
index 00000000000..44fce3cc06c
--- /dev/null
+++ b/airflow-core/src/airflow/ui/src/pages/TaskInstance/Logs/utils.ts
@@ -0,0 +1,51 @@
+/*!
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import type { Virtualizer } from "@tanstack/react-virtual";
+
+type VirtualizerInstance = Virtualizer<HTMLDivElement, Element>;
+
+type ScrollToTopOptions = {
+  element: HTMLElement;
+  virtualizer: VirtualizerInstance;
+};
+
+type ScrollToBottomOptions = {
+  element: HTMLElement;
+  virtualizer: VirtualizerInstance;
+};
+
+/**
+ * Scroll to the top of the list
+ */
+export const scrollToTop = ({ element, virtualizer }: ScrollToTopOptions): 
void => {
+  virtualizer.scrollToOffset(0);
+  element.scrollTop = 0;
+};
+
+/**
+ * Scroll to the bottom of the list
+ */
+export const scrollToBottom = ({ element, virtualizer }: 
ScrollToBottomOptions): void => {
+  const totalSize = virtualizer.getTotalSize();
+  const clientHeight = element.clientHeight || 0;
+  const offset = Math.max(0, totalSize - clientHeight);
+
+  virtualizer.scrollToOffset(offset);
+  element.scrollTop = offset;
+};

Reply via email to