You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@airflow.apache.org by bb...@apache.org on 2022/11/17 20:10:47 UTC
[airflow] branch main updated: reset commits, clean submodules (#27560)
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 65bfea2a20 reset commits, clean submodules (#27560)
65bfea2a20 is described below
commit 65bfea2a20830baa10d2e1e8328c07a7a11bbb0c
Author: Brent Bovenzi <br...@astronomer.io>
AuthorDate: Thu Nov 17 14:10:35 2022 -0600
reset commits, clean submodules (#27560)
---
airflow/www/static/js/dag/Main.tsx | 18 +-
airflow/www/static/js/dag/details/Dag.tsx | 137 +++++++-------
airflow/www/static/js/dag/details/dagRun/index.tsx | 169 +++++++++--------
airflow/www/static/js/dag/details/index.tsx | 10 +-
.../js/dag/details/taskInstance/Logs/index.tsx | 207 +++++++++++----------
.../www/static/js/dag/details/taskInstance/Nav.tsx | 20 +-
.../static/js/dag/details/taskInstance/index.tsx | 27 ++-
airflow/www/static/js/dag/grid/index.tsx | 16 +-
airflow/www/static/js/datasets/index.tsx | 7 +-
.../{useContentHeight.ts => useOffsetHeight.tsx} | 36 ++--
10 files changed, 359 insertions(+), 288 deletions(-)
diff --git a/airflow/www/static/js/dag/Main.tsx b/airflow/www/static/js/dag/Main.tsx
index e9822ea794..26f7e22346 100644
--- a/airflow/www/static/js/dag/Main.tsx
+++ b/airflow/www/static/js/dag/Main.tsx
@@ -34,7 +34,6 @@ import { isEmpty, debounce } from 'lodash';
import useSelection from 'src/dag/useSelection';
import { useGridData } from 'src/api';
import { hoverDelay } from 'src/utils';
-import useContentHeight from 'src/utils/useContentHeight';
import Details from './details';
import Grid from './grid';
@@ -44,11 +43,14 @@ import LegendRow from './nav/LegendRow';
const detailsPanelKey = 'hideDetailsPanel';
const minPanelWidth = 300;
+const gridWidthKey = 'grid-width';
+const saveWidth = debounce((w) => localStorage.setItem(gridWidthKey, w), hoverDelay);
+
const Main = () => {
const { data: { groups }, isLoading } = useGridData();
const resizeRef = useRef<HTMLDivElement>(null);
const gridRef = useRef<HTMLDivElement>(null);
- const contentRef = useRef<HTMLDivElement>(null);
+ const detailsRef = useRef<HTMLDivElement>(null);
const isPanelOpen = localStorage.getItem(detailsPanelKey) !== 'true';
const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: isPanelOpen });
const { clearSelection } = useSelection();
@@ -62,6 +64,8 @@ const Main = () => {
onStatusHover.cancel();
};
+ const gridWidth = localStorage.getItem(gridWidthKey) || undefined;
+
const onPanelToggle = () => {
if (!isOpen) {
localStorage.setItem(detailsPanelKey, 'false');
@@ -72,12 +76,12 @@ const Main = () => {
onToggle();
};
- useContentHeight(contentRef);
-
const resize = useCallback((e: MouseEvent) => {
const gridEl = gridRef.current;
if (gridEl && e.x > minPanelWidth && e.x < window.innerWidth - minPanelWidth) {
- gridEl.style.width = `${e.x}px`;
+ const width = `${e.x}px`;
+ gridEl.style.width = width;
+ saveWidth(width);
}
}, [gridRef]);
@@ -106,7 +110,7 @@ const Main = () => {
<FilterBar />
<LegendRow onStatusHover={onStatusHover} onStatusLeave={onStatusLeave} />
<Divider mb={5} borderBottomWidth={2} />
- <Flex ref={contentRef} overflow="hidden">
+ <Flex>
{isLoading || isEmpty(groups)
? (<Spinner />)
: (
@@ -116,6 +120,7 @@ const Main = () => {
flex={isOpen ? undefined : 1}
ref={gridRef}
height="100%"
+ width={gridWidth}
>
<Grid
isPanelOpen={isOpen}
@@ -138,6 +143,7 @@ const Main = () => {
zIndex={1}
bg="white"
height="100%"
+ ref={detailsRef}
>
<Details />
</Box>
diff --git a/airflow/www/static/js/dag/details/Dag.tsx b/airflow/www/static/js/dag/details/Dag.tsx
index 4973555f42..c45d48adb5 100644
--- a/airflow/www/static/js/dag/details/Dag.tsx
+++ b/airflow/www/static/js/dag/details/Dag.tsx
@@ -17,7 +17,7 @@
* under the License.
*/
-import React, { ReactNode } from 'react';
+import React, { ReactNode, useRef } from 'react';
import {
Table,
Tbody,
@@ -28,6 +28,7 @@ import {
Flex,
Heading,
Text,
+ Box,
} from '@chakra-ui/react';
import { mean } from 'lodash';
@@ -35,6 +36,7 @@ import { getDuration, formatDuration } from 'src/datetime_utils';
import { finalStatesMap, getMetaValue, getTaskSummary } from 'src/utils';
import { useGridData } from 'src/api';
import Time from 'src/components/Time';
+import useOffsetHeight from 'src/utils/useOffsetHeight';
import type { TaskState } from 'src/types';
import { SimpleStatus } from '../StatusBox';
@@ -43,6 +45,8 @@ const dagDetailsUrl = getMetaValue('dag_details_url');
const Dag = () => {
const { data: { dagRuns, groups } } = useGridData();
+ const detailsRef = useRef<HTMLDivElement>(null);
+ const offsetHeight = useOffsetHeight(detailsRef);
const taskSummary = getTaskSummary({ task: groups });
const numMap = finalStatesMap();
@@ -89,84 +93,91 @@ const Dag = () => {
<Button as={Link} variant="ghost" colorScheme="blue" href={dagDetailsUrl}>
DAG Details
</Button>
- <Table variant="striped">
- <Tbody>
- {durations.length > 0 && (
- <>
- <Tr borderBottomWidth={2} borderBottomColor="gray.300">
- <Td><Heading size="sm">DAG Runs Summary</Heading></Td>
- <Td />
- </Tr>
- <Tr>
- <Td>Total Runs Displayed</Td>
- <Td>
- {durations.length}
- </Td>
- </Tr>
- {stateSummary}
- {firstStart && (
+ <Box
+ height="100%"
+ maxHeight={offsetHeight}
+ ref={detailsRef}
+ overflowY="auto"
+ >
+ <Table variant="striped">
+ <Tbody>
+ {durations.length > 0 && (
+ <>
+ <Tr borderBottomWidth={2} borderBottomColor="gray.300">
+ <Td><Heading size="sm">DAG Runs Summary</Heading></Td>
+ <Td />
+ </Tr>
<Tr>
- <Td>First Run Start</Td>
+ <Td>Total Runs Displayed</Td>
<Td>
- <Time dateTime={firstStart} />
+ {durations.length}
+ </Td>
+ </Tr>
+ {stateSummary}
+ {firstStart && (
+ <Tr>
+ <Td>First Run Start</Td>
+ <Td>
+ <Time dateTime={firstStart} />
+ </Td>
+ </Tr>
+ )}
+ {lastStart && (
+ <Tr>
+ <Td>Last Run Start</Td>
+ <Td>
+ <Time dateTime={lastStart} />
+ </Td>
+ </Tr>
+ )}
+ <Tr>
+ <Td>Max Run Duration</Td>
+ <Td>
+ {formatDuration(max)}
</Td>
</Tr>
- )}
- {lastStart && (
<Tr>
- <Td>Last Run Start</Td>
+ <Td>Mean Run Duration</Td>
<Td>
- <Time dateTime={lastStart} />
+ {formatDuration(avg)}
</Td>
</Tr>
+ <Tr>
+ <Td>Min Run Duration</Td>
+ <Td>
+ {formatDuration(min)}
+ </Td>
+ </Tr>
+ </>
)}
- <Tr>
- <Td>Max Run Duration</Td>
+ <Tr borderBottomWidth={2} borderBottomColor="gray.300">
<Td>
- {formatDuration(max)}
+ <Heading size="sm">DAG Summary</Heading>
</Td>
+ <Td />
</Tr>
<Tr>
- <Td>Mean Run Duration</Td>
- <Td>
- {formatDuration(avg)}
- </Td>
+ <Td>Total Tasks</Td>
+ <Td>{taskSummary.taskCount}</Td>
</Tr>
+ {!!taskSummary.groupCount && (
<Tr>
- <Td>Min Run Duration</Td>
- <Td>
- {formatDuration(min)}
- </Td>
+ <Td>Total Task Groups</Td>
+ <Td>{taskSummary.groupCount}</Td>
</Tr>
- </>
- )}
- <Tr borderBottomWidth={2} borderBottomColor="gray.300">
- <Td>
- <Heading size="sm">DAG Summary</Heading>
- </Td>
- <Td />
- </Tr>
- <Tr>
- <Td>Total Tasks</Td>
- <Td>{taskSummary.taskCount}</Td>
- </Tr>
- {!!taskSummary.groupCount && (
- <Tr>
- <Td>Total Task Groups</Td>
- <Td>{taskSummary.groupCount}</Td>
- </Tr>
- )}
- {Object.entries(taskSummary.operators).map(([key, value]) => (
- <Tr key={key}>
- <Td>
- {key}
- {value > 1 && 's'}
- </Td>
- <Td>{value}</Td>
- </Tr>
- ))}
- </Tbody>
- </Table>
+ )}
+ {Object.entries(taskSummary.operators).map(([key, value]) => (
+ <Tr key={key}>
+ <Td>
+ {key}
+ {value > 1 && 's'}
+ </Td>
+ <Td>{value}</Td>
+ </Tr>
+ ))}
+ </Tbody>
+ </Table>
+ </Box>
</>
);
};
diff --git a/airflow/www/static/js/dag/details/dagRun/index.tsx b/airflow/www/static/js/dag/details/dagRun/index.tsx
index 728b8b8e07..ccfc0aa4c6 100644
--- a/airflow/www/static/js/dag/details/dagRun/index.tsx
+++ b/airflow/www/static/js/dag/details/dagRun/index.tsx
@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
-import React from 'react';
+import React, { useRef } from 'react';
import {
Flex,
Text,
@@ -43,6 +43,7 @@ import { ClipboardText } from 'src/components/Clipboard';
import { formatDuration, getDuration } from 'src/datetime_utils';
import Time from 'src/components/Time';
import RunTypeIcon from 'src/components/RunTypeIcon';
+import useOffsetHeight from 'src/utils/useOffsetHeight';
import URLSearchParamsWrapper from 'src/utils/URLSearchParamWrapper';
import NotesAccordion from 'src/dag/details/NotesAccordion';
@@ -62,6 +63,8 @@ interface Props {
const DagRun = ({ runId }: Props) => {
const { data: { dagRuns } } = useGridData();
+ const detailsRef = useRef<HTMLDivElement>(null);
+ const offsetHeight = useOffsetHeight(detailsRef);
const run = dagRuns.find((dr) => dr.runId === runId);
const { onCopy, hasCopied } = useClipboard(run?.conf || '');
if (!run) return null;
@@ -103,100 +106,107 @@ const DagRun = ({ runId }: Props) => {
</Flex>
<Divider my={3} />
</Box>
- <Box px={4}>
- <NotesAccordion
- dagId={dagId}
- runId={runId}
- initialValue={notes}
- key={dagId + runId}
- />
- </Box>
- <Divider my={0} />
- <Table variant="striped">
- <Tbody>
- <Tr>
- <Td>Status</Td>
- <Td>
- <Flex>
- <SimpleStatus state={state} mx={2} />
- {state || 'no status'}
- </Flex>
- </Td>
- </Tr>
- <Tr>
- <Td>Run ID</Td>
- <Td><ClipboardText value={runId} /></Td>
- </Tr>
- <Tr>
- <Td>Run type</Td>
- <Td>
- <RunTypeIcon runType={runType} />
- {runType}
- </Td>
- </Tr>
- <Tr>
- <Td>Run duration</Td>
- <Td>
- {formatDuration(getDuration(startDate, endDate))}
- </Td>
- </Tr>
- {lastSchedulingDecision && (
+ <Box
+ height="100%"
+ maxHeight={offsetHeight}
+ ref={detailsRef}
+ overflowY="auto"
+ pb={4}
+ >
+ <Box px={4}>
+ <NotesAccordion
+ dagId={dagId}
+ runId={runId}
+ initialValue={notes}
+ key={dagId + runId}
+ />
+ </Box>
+ <Divider my={0} />
+ <Table variant="striped">
+ <Tbody>
<Tr>
- <Td>Last scheduling decision</Td>
+ <Td>Status</Td>
<Td>
- <Time dateTime={lastSchedulingDecision} />
+ <Flex>
+ <SimpleStatus state={state} mx={2} />
+ {state || 'no status'}
+ </Flex>
</Td>
</Tr>
- )}
- {queuedAt && (
<Tr>
- <Td>Queued at</Td>
- <Td>
- <Time dateTime={queuedAt} />
- </Td>
+ <Td>Run ID</Td>
+ <Td><ClipboardText value={runId} /></Td>
</Tr>
- )}
- {startDate && (
<Tr>
- <Td>Started</Td>
+ <Td>Run type</Td>
<Td>
- <Time dateTime={startDate} />
+ <RunTypeIcon runType={runType} />
+ {runType}
</Td>
</Tr>
- )}
- {endDate && (
<Tr>
- <Td>Ended</Td>
+ <Td>Run duration</Td>
<Td>
- <Time dateTime={endDate} />
+ {formatDuration(getDuration(startDate, endDate))}
</Td>
</Tr>
- )}
- {dataIntervalStart && dataIntervalEnd && (
- <>
+ {lastSchedulingDecision && (
+ <Tr>
+ <Td>Last scheduling decision</Td>
+ <Td>
+ <Time dateTime={lastSchedulingDecision} />
+ </Td>
+ </Tr>
+ )}
+ {queuedAt && (
+ <Tr>
+ <Td>Queued at</Td>
+ <Td>
+ <Time dateTime={queuedAt} />
+ </Td>
+ </Tr>
+ )}
+ {startDate && (
<Tr>
- <Td>Data interval start</Td>
+ <Td>Started</Td>
<Td>
- <Time dateTime={dataIntervalStart} />
+ <Time dateTime={startDate} />
</Td>
</Tr>
+ )}
+ {endDate && (
<Tr>
- <Td>Data interval end</Td>
+ <Td>Ended</Td>
<Td>
- <Time dateTime={dataIntervalEnd} />
+ <Time dateTime={endDate} />
</Td>
</Tr>
- </>
- )}
- <Tr>
- <Td>Externally triggered</Td>
- <Td>
- {externalTrigger ? 'True' : 'False'}
- </Td>
- </Tr>
- <Tr>
- <Td>Run config</Td>
- {
+ )}
+ {dataIntervalStart && dataIntervalEnd && (
+ <>
+ <Tr>
+ <Td>Data interval start</Td>
+ <Td>
+ <Time dateTime={dataIntervalStart} />
+ </Td>
+ </Tr>
+ <Tr>
+ <Td>Data interval end</Td>
+ <Td>
+ <Time dateTime={dataIntervalEnd} />
+ </Td>
+ </Tr>
+ </>
+ )}
+ <Tr>
+ <Td>Externally triggered</Td>
+ <Td>
+ {externalTrigger ? 'True' : 'False'}
+ </Td>
+ </Tr>
+ <Tr>
+ <Td>Run config</Td>
+ {
confIsJson
? (
<Td>
@@ -218,12 +228,13 @@ const DagRun = ({ runId }: Props) => {
)
: <Td>{conf ?? 'None'}</Td>
}
- </Tr>
- </Tbody>
- </Table>
- {runType === 'dataset_triggered' && (
- <DatasetTriggerEvents runId={runId} />
- )}
+ </Tr>
+ </Tbody>
+ </Table>
+ {runType === 'dataset_triggered' && (
+ <DatasetTriggerEvents runId={runId} />
+ )}
+ </Box>
</>
);
};
diff --git a/airflow/www/static/js/dag/details/index.tsx b/airflow/www/static/js/dag/details/index.tsx
index b4eaac4db3..ee73378acd 100644
--- a/airflow/www/static/js/dag/details/index.tsx
+++ b/airflow/www/static/js/dag/details/index.tsx
@@ -33,11 +33,17 @@ import DagContent from './Dag';
const Details = () => {
const { selected: { runId, taskId, mapIndex }, onSelect } = useSelection();
+
return (
- <Flex flexDirection="column" pl={3} mr={3} height="100%">
+ <Flex
+ flexDirection="column"
+ pl={3}
+ mr={3}
+ height="100%"
+ >
<Header />
<Divider my={2} />
- <Box overflowY="scroll">
+ <Box height="100%">
{!runId && !taskId && <DagContent />}
{runId && !taskId && (
<DagRunContent runId={runId} />
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 ec6249cb27..308446678a 100644
--- a/airflow/www/static/js/dag/details/taskInstance/Logs/index.tsx
+++ b/airflow/www/static/js/dag/details/taskInstance/Logs/index.tsx
@@ -36,8 +36,10 @@ 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';
@@ -106,6 +108,9 @@ const Logs = ({
const [logLevelFilters, setLogLevelFilters] = useState<Array<LogLevelOption>>([]);
const [fileSourceFilters, setFileSourceFilters] = useState<Array<FileSourceOption>>([]);
const { timezone } = useTimezone();
+ const logBoxRef = useRef<HTMLPreElement>(null);
+
+ const offsetHeight = useOffsetHeight(logBoxRef);
const taskTryNumber = selectedTryNumber || tryNumber || 1;
const { data, isSuccess } = useTaskLog({
@@ -140,7 +145,7 @@ const Logs = ({
const codeBlockBottomDiv = useRef<HTMLDivElement>(null);
useEffect(() => {
- if (codeBlockBottomDiv.current) {
+ if (codeBlockBottomDiv.current && parsedLogs) {
codeBlockBottomDiv.current.scrollIntoView({ block: 'nearest', inline: 'nearest' });
}
}, [wrap, parsedLogs]);
@@ -165,109 +170,113 @@ const Logs = ({
<>
{tryNumber !== undefined && (
<>
- <Text as="span"> (by attempts)</Text>
- <Flex my={1} justifyContent="space-between">
- <Flex flexWrap="wrap">
- {internalIndexes.map((index) => (
- <Button
- key={index}
- variant={taskTryNumber === index ? 'solid' : 'ghost'}
- colorScheme="blue"
- onClick={() => setSelectedTryNumber(index)}
- data-testid={`log-attempt-select-button-${index}`}
- >
- {index}
- </Button>
- ))}
+ <Box>
+ <Text as="span"> (by attempts)</Text>
+ <Flex my={1} justifyContent="space-between">
+ <Flex flexWrap="wrap">
+ {internalIndexes.map((index) => (
+ <Button
+ key={index}
+ variant={taskTryNumber === index ? 'solid' : 'ghost'}
+ colorScheme="blue"
+ onClick={() => setSelectedTryNumber(index)}
+ data-testid={`log-attempt-select-button-${index}`}
+ >
+ {index}
+ </Button>
+ ))}
+ </Flex>
</Flex>
- </Flex>
- <Flex my={1} justifyContent="space-between">
- <Flex alignItems="center" flexGrow={1} mr={10}>
- <Box width="100%" mr={2}>
- <MultiSelect
- size="sm"
- isMulti
- options={logLevelOptions}
- placeholder="All Levels"
- value={logLevelFilters}
- onChange={(options) => setLogLevelFilters([...options])}
- chakraStyles={{
- multiValue: (provided, ...rest) => ({
- ...provided,
- backgroundColor: rest[0].data.color,
- }),
- option: (provided, ...rest) => ({
- ...provided,
- borderLeft: 'solid 4px black',
- borderColor: rest[0].data.color,
- mt: 2,
- }),
- }}
- />
- </Box>
- <Box width="100%">
- <MultiSelect
- size="sm"
- isMulti
- options={fileSources.map((fileSource) => ({
- label: fileSource,
- value: fileSource,
- }))}
- placeholder="All File Sources"
- value={fileSourceFilters}
- onChange={(options) => setFileSourceFilters([...options])}
+ <Flex my={1} justifyContent="space-between" flexWrap="wrap">
+ <Flex alignItems="center" flexGrow={1} mr={10}>
+ <Box width="100%" mr={2}>
+ <MultiSelect
+ size="sm"
+ isMulti
+ options={logLevelOptions}
+ placeholder="All Levels"
+ value={logLevelFilters}
+ onChange={(options) => setLogLevelFilters([...options])}
+ chakraStyles={{
+ multiValue: (provided, ...rest) => ({
+ ...provided,
+ backgroundColor: rest[0].data.color,
+ }),
+ option: (provided, ...rest) => ({
+ ...provided,
+ borderLeft: 'solid 4px black',
+ borderColor: rest[0].data.color,
+ mt: 2,
+ }),
+ }}
+ />
+ </Box>
+ <Box width="100%">
+ <MultiSelect
+ size="sm"
+ isMulti
+ options={fileSources.map((fileSource) => ({
+ label: fileSource,
+ value: fileSource,
+ }))}
+ placeholder="All File Sources"
+ value={fileSourceFilters}
+ onChange={(options) => setFileSourceFilters([...options])}
+ />
+ </Box>
+ </Flex>
+ <Flex alignItems="center" flexWrap="wrap">
+ <Checkbox
+ isChecked={wrap}
+ onChange={() => setWrap((previousState) => !previousState)}
+ px={4}
+ data-testid="wrap-checkbox"
+ >
+ <Text as="strong">Wrap</Text>
+ </Checkbox>
+ <Checkbox
+ onChange={() => setShouldRequestFullContent((previousState) => !previousState)}
+ px={4}
+ data-testid="full-content-checkbox"
+ >
+ <Text as="strong" whiteSpace="nowrap">Full Logs</Text>
+ </Checkbox>
+ <LogLink
+ dagId={dagId}
+ taskId={taskId}
+ executionDate={executionDate}
+ isInternal
+ tryNumber={tryNumber}
+ mapIndex={mapIndex}
/>
- </Box>
- </Flex>
- <Flex alignItems="center">
- <Checkbox
- isChecked={wrap}
- onChange={() => setWrap((previousState) => !previousState)}
- px={4}
- data-testid="wrap-checkbox"
- >
- <Text as="strong">Wrap</Text>
- </Checkbox>
- <Checkbox
- onChange={() => setShouldRequestFullContent((previousState) => !previousState)}
- px={4}
- data-testid="full-content-checkbox"
- >
- <Text as="strong" whiteSpace="nowrap">Full Logs</Text>
- </Checkbox>
- <LogLink
- dagId={dagId}
- taskId={taskId}
- executionDate={executionDate}
- isInternal
- tryNumber={tryNumber}
- mapIndex={mapIndex}
- />
- <LinkButton
- href={`${logUrl}&${params.toString()}`}
- >
- See More
- </LinkButton>
+ <LinkButton
+ href={`${logUrl}&${params.toString()}`}
+ >
+ See More
+ </LinkButton>
+ </Flex>
</Flex>
- </Flex>
- {
- isSuccess && (
- <Code
- height={350}
- overflowY="scroll"
- p={3}
- pb={0}
- display="block"
- whiteSpace={wrap ? 'pre-wrap' : 'pre'}
- border="1px solid"
- borderRadius={3}
- borderColor="blue.500"
- >
+ </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>
- )
- }
+ </>
+ )}
+ </Code>
</>
)}
{externalLogName && externalIndexes.length > 0 && (
diff --git a/airflow/www/static/js/dag/details/taskInstance/Nav.tsx b/airflow/www/static/js/dag/details/taskInstance/Nav.tsx
index 833a7b3169..5f2695b96d 100644
--- a/airflow/www/static/js/dag/details/taskInstance/Nav.tsx
+++ b/airflow/www/static/js/dag/details/taskInstance/Nav.tsx
@@ -17,7 +17,7 @@
* under the License.
*/
-import React from 'react';
+import React, { forwardRef } from 'react';
import {
Flex,
Divider,
@@ -50,9 +50,17 @@ interface Props {
mapIndex?: number;
}
-const Nav = ({
- runId, taskId, executionDate, operator, isMapped = false, mapIndex,
-}: Props) => {
+const Nav = forwardRef<HTMLDivElement, Props>((
+ {
+ runId,
+ taskId,
+ executionDate,
+ operator,
+ isMapped = false,
+ mapIndex,
+ },
+ ref,
+) => {
if (!taskId) return null;
const params = new URLSearchParamsWrapper({
task_id: taskId,
@@ -92,7 +100,7 @@ const Nav = ({
return (
<>
- <Flex flexWrap="wrap">
+ <Flex flexWrap="wrap" ref={ref}>
{(!isMapped || mapIndex !== undefined) && (
<>
<LinkButton href={detailsLink}>Task Instance Details</LinkButton>
@@ -113,6 +121,6 @@ const Nav = ({
<Divider mt={3} />
</>
);
-};
+});
export default Nav;
diff --git a/airflow/www/static/js/dag/details/taskInstance/index.tsx b/airflow/www/static/js/dag/details/taskInstance/index.tsx
index 0fe53ec000..560ef28f49 100644
--- a/airflow/www/static/js/dag/details/taskInstance/index.tsx
+++ b/airflow/www/static/js/dag/details/taskInstance/index.tsx
@@ -19,7 +19,7 @@
/* global localStorage */
-import React, { useState } from 'react';
+import React, { useRef, useState } from 'react';
import {
Box,
Text,
@@ -32,8 +32,8 @@ import {
import { useGridData, useTaskInstance } from 'src/api';
import { getMetaValue, getTask } from 'src/utils';
+import useOffsetHeight from 'src/utils/useOffsetHeight';
import type { DagRun, TaskInstance as TaskInstanceType } from 'src/types';
-
import type { SelectionProps } from 'src/dag/useSelection';
import NotesAccordion from 'src/dag/details/NotesAccordion';
@@ -62,6 +62,8 @@ const TaskInstance = ({
const isMapIndexDefined = !(mapIndex === undefined);
const actionsMapIndexes = isMapIndexDefined ? [mapIndex] : [];
const { data: { dagRuns, groups } } = useGridData();
+ const detailsRef = useRef<HTMLDivElement>(null);
+ const offsetHeight = useOffsetHeight(detailsRef);
const storageTabIndex = parseInt(localStorage.getItem(detailsPanelActiveTabIndex) || '0', 10);
const [preferedTabIndex, setPreferedTabIndex] = useState(storageTabIndex);
@@ -115,7 +117,7 @@ const TaskInstance = ({
}
return (
- <Box py="4px">
+ <Box py="4px" height="100%">
{!isGroup && (
<TaskNav
taskId={taskId}
@@ -126,7 +128,13 @@ const TaskInstance = ({
operator={operator}
/>
)}
- <Tabs size="lg" index={selectedTabIndex} onChange={handleTabsChange} isLazy>
+ <Tabs
+ size="lg"
+ index={selectedTabIndex}
+ onChange={handleTabsChange}
+ isLazy
+ height="100%"
+ >
<TabList>
<Tab>
<Text as="strong">Details</Text>
@@ -149,9 +157,16 @@ const TaskInstance = ({
/>
<TabPanels>
-
{/* Details Tab */}
- <TabPanel pt={isMapIndexDefined ? '0px' : undefined}>
+ <TabPanel
+ pt={isMapIndexDefined ? '0px' : undefined}
+ height="100%"
+ maxHeight={offsetHeight}
+ ref={detailsRef}
+ overflowY="auto"
+ py="4px"
+ pb={4}
+ >
<Box py="4px">
{!isGroupOrMappedTaskSummary && (
<NotesAccordion
diff --git a/airflow/www/static/js/dag/grid/index.tsx b/airflow/www/static/js/dag/grid/index.tsx
index b21e95bf8d..e2545125c1 100644
--- a/airflow/www/static/js/dag/grid/index.tsx
+++ b/airflow/www/static/js/dag/grid/index.tsx
@@ -27,7 +27,6 @@ import {
Thead,
Flex,
IconButton,
- useDimensions,
} from '@chakra-ui/react';
import { MdReadMore } from 'react-icons/md';
@@ -35,6 +34,7 @@ import { MdReadMore } from 'react-icons/md';
import { useGridData } from 'src/api';
import { getMetaValue } from 'src/utils';
import AutoRefresh from 'src/components/AutoRefresh';
+import useOffsetHeight from 'src/utils/useOffsetHeight';
import renderTaskRows from './renderTaskRows';
import ResetRoot from './ResetRoot';
@@ -49,11 +49,14 @@ interface Props {
hoveredTaskState?: string | null;
}
-const Grid = ({ isPanelOpen = false, onPanelToggle, hoveredTaskState }: Props) => {
+const Grid = ({
+ isPanelOpen = false,
+ onPanelToggle,
+ hoveredTaskState,
+}: Props) => {
const scrollRef = useRef<HTMLDivElement>(null);
const tableRef = useRef<HTMLTableSectionElement>(null);
- const buttonsRef = useRef<HTMLDivElement>(null);
- const dimensions = useDimensions(buttonsRef);
+ const offsetHeight = useOffsetHeight(scrollRef);
const { data: { groups, dagRuns } } = useGridData();
const dagRunIds = dagRuns.map((dr) => dr.runId);
@@ -103,7 +106,6 @@ const Grid = ({ isPanelOpen = false, onPanelToggle, hoveredTaskState }: Props) =
p={1}
pb={2}
backgroundColor="white"
- ref={buttonsRef}
>
<Flex alignItems="center">
<AutoRefresh />
@@ -125,11 +127,13 @@ const Grid = ({ isPanelOpen = false, onPanelToggle, hoveredTaskState }: Props) =
/>
</Flex>
<Box
- height={`calc(100% - ${dimensions?.borderBox.height || 0}px)`}
+ height="100%"
+ maxHeight={offsetHeight}
ref={scrollRef}
overflow="auto"
position="relative"
pr={4}
+ pb={4}
>
<Table pr="10px">
<Thead>
diff --git a/airflow/www/static/js/datasets/index.tsx b/airflow/www/static/js/datasets/index.tsx
index 9a993c0562..ba738a0d10 100644
--- a/airflow/www/static/js/datasets/index.tsx
+++ b/airflow/www/static/js/datasets/index.tsx
@@ -26,7 +26,6 @@ import { useSearchParams } from 'react-router-dom';
import { Flex, Box, useDimensions } from '@chakra-ui/react';
import App from 'src/App';
-import useContentHeight from 'src/utils/useContentHeight';
import DatasetsList from './List';
import DatasetDetails from './Details';
@@ -61,16 +60,14 @@ const Datasets = () => {
const datasetUri = decodeURIComponent(searchParams.get(DATASET_URI) || '');
- useContentHeight(contentRef);
-
return (
<Flex alignItems="flex-start" justifyContent="space-between" ref={contentRef}>
- <Box minWidth="450px" height="100%" overflowY="scroll">
+ <Box minWidth="450px" height="100%" overflowY="auto">
{datasetUri
? <DatasetDetails uri={datasetUri} onBack={onBack} />
: <DatasetsList onSelect={onSelect} />}
</Box>
- <Box flex={1} ref={graphRef} height="100%" borderColor="gray.200" borderWidth={1}>
+ <Box flex={1} ref={graphRef} height="calc(100vh - 68px)" borderColor="gray.200" borderWidth={1}>
<Graph
selectedUri={datasetUri}
onSelect={onSelect}
diff --git a/airflow/www/static/js/utils/useContentHeight.ts b/airflow/www/static/js/utils/useOffsetHeight.tsx
similarity index 59%
rename from airflow/www/static/js/utils/useContentHeight.ts
rename to airflow/www/static/js/utils/useOffsetHeight.tsx
index 9eb585899a..b0d39a89d5 100644
--- a/airflow/www/static/js/utils/useContentHeight.ts
+++ b/airflow/www/static/js/utils/useOffsetHeight.tsx
@@ -17,26 +17,28 @@
* under the License.
*/
-/* global document */
+/* global document, window */
-import React, { useEffect } from 'react';
+import { debounce } from 'lodash';
+import React, { useEffect, useState } from 'react';
+
+const footerHeight = parseInt(getComputedStyle(document.getElementsByTagName('body')[0]).paddingBottom.replace('px', ''), 10) || 0;
+
+// For an html element, keep it within view height by calculating the top offset and footer height
+const useOffsetHeight = (
+ contentRef: React.RefObject<HTMLDivElement | HTMLPreElement>,
+ minHeight: number = 300,
+) => {
+ const [height, setHeight] = useState(0);
-const useContentHeight = (contentRef: React.RefObject<HTMLDivElement>) => {
useEffect(() => {
- const calculateHeight = () => {
+ const calculateHeight = debounce(() => {
if (contentRef.current) {
const topOffset = contentRef.current.offsetTop;
- const footerHeight = parseInt(getComputedStyle(document.getElementsByTagName('body')[0]).paddingBottom.replace('px', ''), 10) || 0;
- const newHeight = window.innerHeight - topOffset - footerHeight;
- const newHeightPx = `${newHeight}px`;
-
- // only set a new height if it has changed
- if (newHeightPx !== contentRef.current.style.height) {
- // keep a minimum usable height of 300px
- contentRef.current.style.height = newHeight > 300 ? newHeightPx : '300px';
- }
+ const newHeight = window.innerHeight - (topOffset + footerHeight);
+ setHeight(newHeight > minHeight ? newHeight : minHeight);
}
- };
+ }, 25);
// set height on load
calculateHeight();
@@ -45,7 +47,9 @@ const useContentHeight = (contentRef: React.RefObject<HTMLDivElement>) => {
return () => {
window.removeEventListener('resize', calculateHeight);
};
- }, [contentRef]);
+ }, [contentRef, minHeight]);
+
+ return height;
};
-export default useContentHeight;
+export default useOffsetHeight;