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} ); };