Skip to content

Commit 2579bbf

Browse files
authored
Add scrollTo buttons in TaskLogContent (#51943)
* Add scrollTo buttons in TaskLogContent * Update visibility logic, keyboard shortcuts and position issues * Add parsedLogs in useMemo dep list * Update hotkey display with meta key
1 parent 86ee9ca commit 2579bbf

File tree

3 files changed

+92
-20
lines changed

3 files changed

+92
-20
lines changed

airflow-core/src/airflow/ui/public/i18n/locales/en/common.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,13 @@
128128
"manual": "Manual",
129129
"scheduled": "Scheduled"
130130
},
131+
"scroll": {
132+
"direction": {
133+
"bottom": "bottom",
134+
"top": "top"
135+
},
136+
"tooltip": "Press {{hotkey}} to scroll to {{direction}}"
137+
},
131138
"seconds": "{{count}}s",
132139
"security": {
133140
"actions": "Actions",

airflow-core/src/airflow/ui/public/i18n/locales/zh-TW/common.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,13 @@
128128
"manual": "手動觸發",
129129
"scheduled": "已排程"
130130
},
131+
"scroll": {
132+
"direction": {
133+
"bottom": "最下方",
134+
"top": "最上方"
135+
},
136+
"tooltip": "按 {{hotkey}} 捲動到{{direction}}"
137+
},
131138
"seconds": "{{count}} 秒",
132139
"security": {
133140
"actions": "操作",
@@ -223,7 +230,7 @@
223230
"title": "選擇時區",
224231
"utc": "UTC"
225232
},
226-
"toaster": {
233+
"toaster": {
227234
"bulkDelete": {
228235
"error": "批次刪除 {{resourceName}} 請求失敗",
229236
"success": {

airflow-core/src/airflow/ui/src/pages/TaskInstance/Logs/TaskLogContent.tsx

Lines changed: 77 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,16 @@
1616
* specific language governing permissions and limitations
1717
* under the License.
1818
*/
19-
import { Box, Code, VStack, useToken } from "@chakra-ui/react";
19+
import { Box, Code, VStack, IconButton } from "@chakra-ui/react";
2020
import { useVirtualizer } from "@tanstack/react-virtual";
21-
import { useLayoutEffect, useRef } from "react";
21+
import { useLayoutEffect, useMemo, useRef } from "react";
22+
import { useHotkeys } from "react-hotkeys-hook";
23+
import { useTranslation } from "react-i18next";
24+
import { FiChevronDown, FiChevronUp } from "react-icons/fi";
2225

2326
import { ErrorAlert } from "src/components/ErrorAlert";
24-
import { ProgressBar } from "src/components/ui";
27+
import { ProgressBar, Tooltip } from "src/components/ui";
28+
import { getMetaKey } from "src/utils";
2529

2630
type Props = {
2731
readonly error: unknown;
@@ -31,8 +35,50 @@ type Props = {
3135
readonly wrap: boolean;
3236
};
3337

38+
const ScrollToButton = ({
39+
direction,
40+
onClick,
41+
}: {
42+
readonly direction: "bottom" | "top";
43+
readonly onClick: () => void;
44+
}) => {
45+
const { t: translate } = useTranslation("common");
46+
47+
return (
48+
<Tooltip
49+
closeDelay={100}
50+
content={translate("scroll.tooltip", {
51+
direction: translate(`scroll.direction.${direction}`),
52+
hotkey: `${getMetaKey()}+${direction === "bottom" ? "↓" : "↑"}`,
53+
})}
54+
openDelay={100}
55+
>
56+
<IconButton
57+
_ltr={{
58+
left: "auto",
59+
right: 4,
60+
}}
61+
_rtl={{
62+
left: 4,
63+
right: "auto",
64+
}}
65+
aria-label={translate(`scroll.direction.${direction}`)}
66+
bg="bg.panel"
67+
bottom={direction === "bottom" ? 4 : 14}
68+
onClick={onClick}
69+
position="absolute"
70+
rounded="full"
71+
size="xs"
72+
variant="outline"
73+
>
74+
{direction === "bottom" ? <FiChevronDown /> : <FiChevronUp />}
75+
</IconButton>
76+
</Tooltip>
77+
);
78+
};
79+
3480
export const TaskLogContent = ({ error, isLoading, logError, parsedLogs, wrap }: Props) => {
35-
const [bgLine] = useToken("colors", ["blue.emphasized"]);
81+
const hash = location.hash.replace("#", "");
3682
const parentRef = useRef(null);
3783
const rowVirtualizer = useVirtualizer({
3884
count: parsedLogs.length,
@@ -41,26 +87,30 @@ export const TaskLogContent = ({ error, isLoading, logError, parsedLogs, wrap }:
4187
overscan: 10,
4288
});
4389

44-
useLayoutEffect(() => {
45-
if (location.hash) {
46-
const hash = location.hash.replace("#", "");
90+
const showScrollButtons = useMemo(() => {
91+
const contentHeight = rowVirtualizer.getTotalSize();
92+
const containerHeight = rowVirtualizer.scrollElement?.clientHeight ?? 0;
4793

48-
setTimeout(() => {
49-
const element = document.querySelector<HTMLElement>(`[id='${hash}']`);
94+
return parsedLogs.length > 0 && contentHeight > containerHeight;
95+
}, [rowVirtualizer, parsedLogs]);
5096

51-
if (element !== null) {
52-
element.style.background = bgLine as string;
53-
}
54-
element?.scrollIntoView({
55-
behavior: "smooth",
56-
block: "center",
57-
});
58-
}, 100);
97+
useLayoutEffect(() => {
98+
if (location.hash && !isLoading) {
99+
rowVirtualizer.scrollToIndex(Math.min(Number(hash) + 5, parsedLogs.length - 1));
59100
}
60-
}, [isLoading, bgLine]);
101+
}, [isLoading, rowVirtualizer, hash, parsedLogs]);
102+
103+
const handleScrollTo = (to: "bottom" | "top") => {
104+
if (parsedLogs.length > 0) {
105+
rowVirtualizer.scrollToIndex(to === "bottom" ? parsedLogs.length - 1 : 0);
106+
}
107+
};
108+
109+
useHotkeys("mod+ArrowDown", () => handleScrollTo("bottom"), { enabled: !isLoading });
110+
useHotkeys("mod+ArrowUp", () => handleScrollTo("top"), { enabled: !isLoading });
61111

62112
return (
63-
<Box display="flex" flexDirection="column" flexGrow={1} h="100%" minHeight={0}>
113+
<Box display="flex" flexDirection="column" flexGrow={1} h="100%" minHeight={0} position="relative">
64114
<ErrorAlert error={error ?? logError} />
65115
<ProgressBar size="xs" visibility={isLoading ? "visible" : "hidden"} />
66116
<Code
@@ -90,6 +140,7 @@ export const TaskLogContent = ({ error, isLoading, logError, parsedLogs, wrap }:
90140
left: "auto",
91141
right: 0,
92142
}}
143+
bgColor={virtualRow.index === Number(hash) ? "blue.emphasized" : "transparent"}
93144
data-index={virtualRow.index}
94145
data-testid={`virtualized-item-${virtualRow.index}`}
95146
key={virtualRow.key}
@@ -104,6 +155,13 @@ export const TaskLogContent = ({ error, isLoading, logError, parsedLogs, wrap }:
104155
))}
105156
</VStack>
106157
</Code>
158+
159+
{showScrollButtons ? (
160+
<>
161+
<ScrollToButton direction="top" onClick={() => handleScrollTo("top")} />
162+
<ScrollToButton direction="bottom" onClick={() => handleScrollTo("bottom")} />
163+
</>
164+
) : undefined}
107165
</Box>
108166
);
109167
};

0 commit comments

Comments
 (0)