This is an automated email from the ASF dual-hosted git repository.
bbovenzi 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 5c80d985a3 Maintain manual scroll position in task logs (#28386)
5c80d985a3 is described below
commit 5c80d985a3102a46f198aec1c57a255e00784c51
Author: Brent Bovenzi <[email protected]>
AuthorDate: Sun Dec 18 19:00:34 2022 -0600
Maintain manual scroll position in task logs (#28386)
---
airflow/www/static/js/api/useTaskLog.ts | 20 ++++-
.../js/dag/details/taskInstance/Logs/LogBlock.tsx | 90 ++++++++++++++++++++++
.../js/dag/details/taskInstance/Logs/index.tsx | 45 +++--------
3 files changed, 117 insertions(+), 38 deletions(-)
diff --git a/airflow/www/static/js/api/useTaskLog.ts
b/airflow/www/static/js/api/useTaskLog.ts
index 580c5e0ab4..bbb6395878 100644
--- a/airflow/www/static/js/api/useTaskLog.ts
+++ b/airflow/www/static/js/api/useTaskLog.ts
@@ -17,6 +17,7 @@
* under the License.
*/
+import { useState } from 'react';
import axios, { AxiosResponse } from 'axios';
import { useQuery } from 'react-query';
import { useAutoRefresh } from 'src/context/autorefresh';
@@ -34,6 +35,7 @@ const useTaskLog = ({
dagId, dagRunId, taskId, taskTryNumber, mapIndex, fullContent, state,
}: Props) => {
let url: string = '';
+ const [isPreviousStatePending, setPrevState] = useState(true);
if (taskLogApi) {
url = taskLogApi.replace('_DAG_RUN_ID_', dagRunId).replace('_TASK_ID_',
taskId).replace(/-1$/, taskTryNumber.toString());
}
@@ -49,12 +51,24 @@ const useTaskLog = ({
|| state === 'queued'
|| state === 'restarting';
+ // We also want to get the last log when the task was finished
+ const expectingLogs = isStatePending || isPreviousStatePending;
+
return useQuery(
- ['taskLogs', dagId, dagRunId, taskId, mapIndex, taskTryNumber,
fullContent, state],
- () => axios.get<AxiosResponse, string>(url, { headers: { Accept:
'text/plain' }, params: { map_index: mapIndex, full_content: fullContent } }),
+ ['taskLogs', dagId, dagRunId, taskId, mapIndex, taskTryNumber,
fullContent],
+ () => {
+ setPrevState(isStatePending);
+ return axios.get<AxiosResponse, string>(
+ url,
+ {
+ headers: { Accept: 'text/plain' },
+ params: { map_index: mapIndex, full_content: fullContent },
+ },
+ );
+ },
{
placeholderData: '',
- refetchInterval: isStatePending && isRefreshOn && (autoRefreshInterval
|| 1) * 1000,
+ refetchInterval: expectingLogs && isRefreshOn && (autoRefreshInterval ||
1) * 1000,
},
);
};
diff --git a/airflow/www/static/js/dag/details/taskInstance/Logs/LogBlock.tsx
b/airflow/www/static/js/dag/details/taskInstance/Logs/LogBlock.tsx
new file mode 100644
index 0000000000..0ffa76e21f
--- /dev/null
+++ b/airflow/www/static/js/dag/details/taskInstance/Logs/LogBlock.tsx
@@ -0,0 +1,90 @@
+/*!
+ * 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 React, {
+ useRef, useEffect, useState,
+} from 'react';
+import {
+ Code,
+} from '@chakra-ui/react';
+
+import useOffsetHeight from 'src/utils/useOffsetHeight';
+
+interface Props {
+ parsedLogs: string;
+ wrap: boolean;
+ tryNumber: number;
+}
+
+const LogBlock = ({
+ parsedLogs,
+ wrap,
+ tryNumber,
+}: Props) => {
+ const [autoScroll, setAutoScroll] = useState(true);
+ const logBoxRef = useRef<HTMLPreElement>(null);
+
+ const maxHeight = useOffsetHeight(logBoxRef, parsedLogs);
+
+ const codeBlockBottomDiv = useRef<HTMLDivElement>(null);
+
+ const scrollToBottom = () => {
+ codeBlockBottomDiv.current?.scrollIntoView({ block: 'nearest', inline:
'nearest' });
+ };
+
+ useEffect(() => {
+ // Always scroll to bottom when wrap or tryNumber change
+ scrollToBottom();
+ }, [wrap, tryNumber]);
+
+ useEffect(() => {
+ // When logs change, only scroll if autoScroll is enabled
+ if (autoScroll) scrollToBottom();
+ }, [parsedLogs, autoScroll]);
+
+ const onScroll = (e: React.UIEvent<HTMLDivElement>) => {
+ if (e.currentTarget) {
+ const { scrollTop, offsetHeight, scrollHeight } = e.currentTarget;
+ // Enable autoscroll if we've scrolled to the bottom of the logs
+ setAutoScroll(scrollTop + offsetHeight >= scrollHeight);
+ }
+ };
+
+ return (
+ <Code
+ ref={logBoxRef}
+ onScroll={onScroll}
+ height="100%"
+ maxHeight={maxHeight}
+ overflowY="auto"
+ p={3}
+ pb={0}
+ display="block"
+ whiteSpace={wrap ? 'pre-wrap' : 'pre'}
+ border="1px solid"
+ borderRadius={3}
+ borderColor="blue.500"
+ >
+ {parsedLogs}
+ <div ref={codeBlockBottomDiv} />
+ </Code>
+ );
+};
+
+export default LogBlock;
diff --git a/airflow/www/static/js/dag/details/taskInstance/Logs/index.tsx
b/airflow/www/static/js/dag/details/taskInstance/Logs/index.tsx
index 43976a7b52..0139d8e223 100644
--- a/airflow/www/static/js/dag/details/taskInstance/Logs/index.tsx
+++ b/airflow/www/static/js/dag/details/taskInstance/Logs/index.tsx
@@ -18,14 +18,13 @@
*/
import React, {
- useRef, useState, useEffect, useMemo,
+ useState, useEffect, useMemo,
} from 'react';
import {
Text,
Box,
Flex,
Divider,
- Code,
Button,
Checkbox,
} from '@chakra-ui/react';
@@ -36,12 +35,12 @@ import LinkButton from 'src/components/LinkButton';
import { useTimezone } from 'src/context/timezone';
import type { Dag, DagRun, TaskInstance } from 'src/types';
import MultiSelect from 'src/components/MultiSelect';
-import useOffsetHeight from 'src/utils/useOffsetHeight';
import URLSearchParamsWrapper from 'src/utils/URLSearchParamWrapper';
import LogLink from './LogLink';
import { LogLevel, logLevelColorMapping, parseLogs } from './utils';
+import LogBlock from './LogBlock';
interface LogLevelOption {
label: LogLevel;
@@ -108,10 +107,9 @@ const Logs = ({
const [logLevelFilters, setLogLevelFilters] =
useState<Array<LogLevelOption>>([]);
const [fileSourceFilters, setFileSourceFilters] =
useState<Array<FileSourceOption>>([]);
const { timezone } = useTimezone();
- const logBoxRef = useRef<HTMLPreElement>(null);
const taskTryNumber = selectedTryNumber || tryNumber || 1;
- const { data, isSuccess } = useTaskLog({
+ const { data } = useTaskLog({
dagId,
dagRunId,
taskId,
@@ -121,8 +119,6 @@ const Logs = ({
state,
});
- const offsetHeight = useOffsetHeight(logBoxRef, data);
-
const params = new URLSearchParamsWrapper({
task_id: taskId,
execution_date: executionDate,
@@ -142,14 +138,6 @@ const Logs = ({
[data, fileSourceFilters, logLevelFilters, timezone],
);
- const codeBlockBottomDiv = useRef<HTMLDivElement>(null);
-
- useEffect(() => {
- if (codeBlockBottomDiv.current && parsedLogs) {
- codeBlockBottomDiv.current.scrollIntoView({ block: 'nearest', inline:
'nearest' });
- }
- }, [wrap, parsedLogs]);
-
useEffect(() => {
// Reset fileSourceFilters and selected attempt when changing to
// a task that do not have those filters anymore.
@@ -257,26 +245,13 @@ const Logs = ({
</Flex>
</Flex>
</Box>
- <Code
- ref={logBoxRef}
- height="100%"
- maxHeight={offsetHeight}
- overflowY="auto"
- p={3}
- pb={0}
- display="block"
- whiteSpace={wrap ? 'pre-wrap' : 'pre'}
- border="1px solid"
- borderRadius={3}
- borderColor="blue.500"
- >
- {isSuccess && (
- <>
- {parsedLogs}
- <div ref={codeBlockBottomDiv} />
- </>
- )}
- </Code>
+ {!!parsedLogs && (
+ <LogBlock
+ parsedLogs={parsedLogs}
+ wrap={wrap}
+ tryNumber={taskTryNumber}
+ />
+ )}
</>
)}
{externalLogName && externalIndexes.length > 0 && (