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 6cebff264bdd7..cb83efcedb9d1 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
@@ -128,6 +128,13 @@
"manual": "Manual",
"scheduled": "Scheduled"
},
+ "scroll": {
+ "direction": {
+ "bottom": "bottom",
+ "top": "top"
+ },
+ "tooltip": "Press {{hotkey}} to scroll to {{direction}}"
+ },
"seconds": "{{count}}s",
"security": {
"actions": "Actions",
diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/zh-TW/common.json b/airflow-core/src/airflow/ui/public/i18n/locales/zh-TW/common.json
index 1b94813361e1a..7df78a954dd47 100644
--- a/airflow-core/src/airflow/ui/public/i18n/locales/zh-TW/common.json
+++ b/airflow-core/src/airflow/ui/public/i18n/locales/zh-TW/common.json
@@ -128,6 +128,13 @@
"manual": "手動觸發",
"scheduled": "已排程"
},
+ "scroll": {
+ "direction": {
+ "bottom": "最下方",
+ "top": "最上方"
+ },
+ "tooltip": "按 {{hotkey}} 捲動到{{direction}}"
+ },
"seconds": "{{count}} 秒",
"security": {
"actions": "操作",
@@ -223,7 +230,7 @@
"title": "選擇時區",
"utc": "UTC"
},
- "toaster": {
+ "toaster": {
"bulkDelete": {
"error": "批次刪除 {{resourceName}} 請求失敗",
"success": {
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 c6a1e5a793213..c70ed7bc7c7ec 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
@@ -16,12 +16,16 @@
* specific language governing permissions and limitations
* under the License.
*/
-import { Box, Code, VStack, useToken } from "@chakra-ui/react";
+import { Box, Code, VStack, IconButton } from "@chakra-ui/react";
import { useVirtualizer } from "@tanstack/react-virtual";
-import { useLayoutEffect, useRef } from "react";
+import { useLayoutEffect, useMemo, useRef } from "react";
+import { useHotkeys } from "react-hotkeys-hook";
+import { useTranslation } from "react-i18next";
+import { FiChevronDown, FiChevronUp } from "react-icons/fi";
import { ErrorAlert } from "src/components/ErrorAlert";
-import { ProgressBar } from "src/components/ui";
+import { ProgressBar, Tooltip } from "src/components/ui";
+import { getMetaKey } from "src/utils";
type Props = {
readonly error: unknown;
@@ -31,8 +35,50 @@ type Props = {
readonly wrap: boolean;
};
+const ScrollToButton = ({
+ direction,
+ onClick,
+}: {
+ readonly direction: "bottom" | "top";
+ readonly onClick: () => void;
+}) => {
+ const { t: translate } = useTranslation("common");
+
+ return (
+
+
+ {direction === "bottom" ? : }
+
+
+ );
+};
+
export const TaskLogContent = ({ error, isLoading, logError, parsedLogs, wrap }: Props) => {
- const [bgLine] = useToken("colors", ["blue.emphasized"]);
+ const hash = location.hash.replace("#", "");
const parentRef = useRef(null);
const rowVirtualizer = useVirtualizer({
count: parsedLogs.length,
@@ -41,26 +87,30 @@ export const TaskLogContent = ({ error, isLoading, logError, parsedLogs, wrap }:
overscan: 10,
});
- useLayoutEffect(() => {
- if (location.hash) {
- const hash = location.hash.replace("#", "");
+ const showScrollButtons = useMemo(() => {
+ const contentHeight = rowVirtualizer.getTotalSize();
+ const containerHeight = rowVirtualizer.scrollElement?.clientHeight ?? 0;
- setTimeout(() => {
- const element = document.querySelector(`[id='${hash}']`);
+ return parsedLogs.length > 0 && contentHeight > containerHeight;
+ }, [rowVirtualizer, parsedLogs]);
- if (element !== null) {
- element.style.background = bgLine as string;
- }
- element?.scrollIntoView({
- behavior: "smooth",
- block: "center",
- });
- }, 100);
+ useLayoutEffect(() => {
+ if (location.hash && !isLoading) {
+ rowVirtualizer.scrollToIndex(Math.min(Number(hash) + 5, parsedLogs.length - 1));
}
- }, [isLoading, bgLine]);
+ }, [isLoading, rowVirtualizer, hash, parsedLogs]);
+
+ const handleScrollTo = (to: "bottom" | "top") => {
+ if (parsedLogs.length > 0) {
+ rowVirtualizer.scrollToIndex(to === "bottom" ? parsedLogs.length - 1 : 0);
+ }
+ };
+
+ useHotkeys("mod+ArrowDown", () => handleScrollTo("bottom"), { enabled: !isLoading });
+ useHotkeys("mod+ArrowUp", () => handleScrollTo("top"), { enabled: !isLoading });
return (
-
+
+
+ {showScrollButtons ? (
+ <>
+ handleScrollTo("top")} />
+ handleScrollTo("bottom")} />
+ >
+ ) : undefined}
);
};