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 1429091d7c Fix Grid vertical scrolling (#24684)
1429091d7c is described below
commit 1429091d7cf8e16e16efc7a0a3cc00f6ae5716a1
Author: Brent Bovenzi <[email protected]>
AuthorDate: Tue Jun 28 11:43:28 2022 -0400
Fix Grid vertical scrolling (#24684)
* fix vertical scrolling
* fix flex grow for panel open/close
* add type checking
* add duration axis component
* remove details/grid width changes
this should be done in a separate PR
---
airflow/www/static/js/grid/{Grid.jsx => Grid.tsx} | 73 +++++++++---------
.../static/js/grid/dagRuns/{Bar.jsx => Bar.tsx} | 29 +++++---
airflow/www/static/js/grid/dagRuns/index.tsx | 87 +++++++++++-----------
airflow/www/static/js/grid/renderTaskRows.tsx | 1 -
4 files changed, 103 insertions(+), 87 deletions(-)
diff --git a/airflow/www/static/js/grid/Grid.jsx
b/airflow/www/static/js/grid/Grid.tsx
similarity index 73%
rename from airflow/www/static/js/grid/Grid.jsx
rename to airflow/www/static/js/grid/Grid.tsx
index 17101cef4f..c32d8230ce 100644
--- a/airflow/www/static/js/grid/Grid.jsx
+++ b/airflow/www/static/js/grid/Grid.tsx
@@ -40,18 +40,24 @@ import AutoRefresh from './AutoRefresh';
const dagId = getMetaValue('dag_id');
-const Grid = ({ isPanelOpen = false, onPanelToggle, hoveredTaskState }) => {
- const scrollRef = useRef();
- const tableRef = useRef();
+interface Props {
+ isPanelOpen?: boolean;
+ onPanelToggle: () => void;
+ hoveredTaskState?: string;
+}
+
+const Grid = ({ isPanelOpen = false, onPanelToggle, hoveredTaskState }: Props)
=> {
+ const scrollRef = useRef<HTMLDivElement>(null);
+ const tableRef = useRef<HTMLTableSectionElement>(null);
const { data: { groups, dagRuns } } = useGridData();
const dagRunIds = dagRuns.map((dr) => dr.runId);
const openGroupsKey = `${dagId}/open-groups`;
- const storedGroups = JSON.parse(localStorage.getItem(openGroupsKey)) || [];
+ const storedGroups = JSON.parse(localStorage.getItem(openGroupsKey) || '[]');
const [openGroupIds, setOpenGroupIds] = useState(storedGroups);
- const onToggleGroups = (groupIds) => {
+ const onToggleGroups = (groupIds: string[]) => {
localStorage.setItem(openGroupsKey, JSON.stringify(groupIds));
setOpenGroupIds(groupIds);
};
@@ -60,7 +66,11 @@ const Grid = ({ isPanelOpen = false, onPanelToggle,
hoveredTaskState }) => {
const scrollOnResize = new ResizeObserver(() => {
const runsContainer = scrollRef.current;
// Set scroll to top right if it is scrollable
- if (runsContainer && runsContainer.scrollWidth >
runsContainer.clientWidth) {
+ if (
+ tableRef?.current
+ && runsContainer
+ && runsContainer.scrollWidth > runsContainer.clientWidth
+ ) {
runsContainer.scrollBy(tableRef.current.offsetWidth, 0);
}
});
@@ -74,26 +84,21 @@ const Grid = ({ isPanelOpen = false, onPanelToggle,
hoveredTaskState }) => {
};
}
return () => {};
- }, [tableRef]);
+ }, [tableRef, isPanelOpen]);
return (
<Box
- position="relative"
+ minWidth={isPanelOpen ? '350px' : undefined}
+ flexGrow={1}
m={3}
mt={0}
- overflow="auto"
- ref={scrollRef}
- flexGrow={1}
- minWidth={isPanelOpen && '350px'}
>
<Flex
alignItems="center"
justifyContent="space-between"
- position="sticky"
- top={0}
- left={0}
mb={2}
p={1}
+ backgroundColor="white"
>
<Flex alignItems="center">
<AutoRefresh />
@@ -110,28 +115,28 @@ const Grid = ({ isPanelOpen = false, onPanelToggle,
hoveredTaskState }) => {
title={`${isPanelOpen ? 'Hide ' : 'Show '} Details Panel`}
aria-label={isPanelOpen ? 'Show Details' : 'Hide Details'}
icon={<MdReadMore />}
- transform={!isPanelOpen && 'rotateZ(180deg)'}
+ transform={!isPanelOpen ? 'rotateZ(180deg)' : undefined}
transitionProperty="none"
/>
</Flex>
- <Table>
- <Thead display="block" pr="10px" position="sticky" top={0} zIndex={2}
bg="white">
- <DagRuns />
- </Thead>
- {/* TODO: remove hardcoded values. 665px is roughly the total
heade+footer height */}
- <Tbody
- display="block"
- width="100%"
- maxHeight="calc(100vh - 665px)"
- minHeight="500px"
- ref={tableRef}
- pr="10px"
- >
- {renderTaskRows({
- task: groups, dagRunIds, openGroupIds, onToggleGroups,
hoveredTaskState,
- })}
- </Tbody>
- </Table>
+ <Box
+ overflow="auto"
+ ref={scrollRef}
+ maxHeight="900px"
+ position="relative"
+ >
+ <Table pr="10px">
+ <Thead>
+ <DagRuns />
+ </Thead>
+ {/* TODO: remove hardcoded values. 665px is roughly the total
heade+footer height */}
+ <Tbody ref={tableRef}>
+ {renderTaskRows({
+ task: groups, dagRunIds, openGroupIds, onToggleGroups,
hoveredTaskState,
+ })}
+ </Tbody>
+ </Table>
+ </Box>
</Box>
);
};
diff --git a/airflow/www/static/js/grid/dagRuns/Bar.jsx
b/airflow/www/static/js/grid/dagRuns/Bar.tsx
similarity index 82%
rename from airflow/www/static/js/grid/dagRuns/Bar.jsx
rename to airflow/www/static/js/grid/dagRuns/Bar.tsx
index 22163a44da..18399ab885 100644
--- a/airflow/www/static/js/grid/dagRuns/Bar.jsx
+++ b/airflow/www/static/js/grid/dagRuns/Bar.tsx
@@ -35,12 +35,23 @@ import { RiArrowGoBackFill } from 'react-icons/ri';
import DagRunTooltip from './Tooltip';
import { useContainerRef } from '../context/containerRef';
import Time from '../components/Time';
+import type { SelectionProps } from '../utils/useSelection';
+import type { RunWithDuration } from '.';
const BAR_HEIGHT = 100;
+interface Props {
+ run: RunWithDuration
+ max: number;
+ index: number;
+ totalRuns: number;
+ isSelected: boolean;
+ onSelect: (props: SelectionProps) => void;
+}
+
const DagRunBar = ({
run, max, index, totalRuns, isSelected, onSelect,
-}) => {
+}: Props) => {
const containerRef = useContainerRef();
const { colors } = useTheme();
const hoverBlue = `${colors.blue[100]}50`;
@@ -48,20 +59,20 @@ const DagRunBar = ({
// Fetch the corresponding column element and set its background color when
hovering
const onMouseEnter = () => {
if (!isSelected) {
- [...containerRef.current.getElementsByClassName(`js-${run.runId}`)]
- .forEach((e) => { e.style.backgroundColor = hoverBlue; });
+ const els =
Array.from(containerRef?.current?.getElementsByClassName(`js-${run.runId}`) as
HTMLCollectionOf<HTMLElement>);
+ els.forEach((e) => { e.style.backgroundColor = hoverBlue; });
}
};
const onMouseLeave = () => {
- [...containerRef.current.getElementsByClassName(`js-${run.runId}`)]
- .forEach((e) => { e.style.backgroundColor = null; });
+ const els =
Array.from(containerRef?.current?.getElementsByClassName(`js-${run.runId}`) as
HTMLCollectionOf<HTMLElement>);
+ els.forEach((e) => { e.style.backgroundColor = ''; });
};
return (
<Box
className={`js-${run.runId}`}
data-selected={isSelected}
- bg={isSelected && 'blue.100'}
+ bg={isSelected ? 'blue.100' : undefined}
transition="background-color 0.2s"
px="1px"
pb="2px"
@@ -106,7 +117,7 @@ const DagRunBar = ({
</Flex>
</Tooltip>
</Flex>
- {index < totalRuns - 3 && index % 10 === 0 && (
+ {(index === totalRuns - 4 || (index + 4) % 10 === 0) && (
<VStack position="absolute" top="0" left="8px" spacing={0} zIndex={0}
width={0}>
<Text fontSize="sm" color="gray.400" whiteSpace="nowrap"
transform="rotate(-30deg) translateX(28px)" mt="-23px !important">
<Time dateTime={run.executionDate} format="MMM DD, HH:mm" />
@@ -121,8 +132,8 @@ const DagRunBar = ({
// The default equality function is a shallow comparison and json objects will
return false
// This custom compare function allows us to do a deeper comparison
const compareProps = (
- prevProps,
- nextProps,
+ prevProps: Props,
+ nextProps: Props,
) => (
isEqual(prevProps.run, nextProps.run)
&& prevProps.max === nextProps.max
diff --git a/airflow/www/static/js/grid/dagRuns/index.tsx
b/airflow/www/static/js/grid/dagRuns/index.tsx
index 679b7db3a5..6b779175f8 100644
--- a/airflow/www/static/js/grid/dagRuns/index.tsx
+++ b/airflow/www/static/js/grid/dagRuns/index.tsx
@@ -23,8 +23,10 @@ import {
Td,
Text,
Box,
- Flex,
TextProps,
+ TableCellProps,
+ Flex,
+ BoxProps,
} from '@chakra-ui/react';
import { useGridData } from '../api';
@@ -33,17 +35,29 @@ import { getDuration, formatDuration } from
'../../datetime_utils';
import useSelection from '../utils/useSelection';
import type { DagRun } from '../types';
+const DurationAxis = (props: BoxProps) => (
+ <Box position="absolute" borderBottomWidth={1} zIndex={0} opacity={0.7}
width="100%" {...props} />
+);
+
const DurationTick = ({ children, ...rest }: TextProps) => (
<Text fontSize="sm" color="gray.400" right={1} position="absolute"
whiteSpace="nowrap" {...rest}>
{children}
</Text>
);
+const Th = (props: TableCellProps) => (
+ <Td position="sticky" top={0} zIndex={1} p={0} height="155px" bg="white"
{...props} />
+);
+
+export interface RunWithDuration extends DagRun {
+ duration: number;
+}
+
const DagRuns = () => {
const { data: { dagRuns } } = useGridData();
const { selected, onSelect } = useSelection();
const durations: number[] = [];
- const runs = dagRuns.map((dagRun) => {
+ const runs: RunWithDuration[] = dagRuns.map((dagRun) => {
const duration = getDuration(dagRun.startDate, dagRun.endDate);
durations.push(duration);
return {
@@ -54,58 +68,45 @@ const DagRuns = () => {
// calculate dag run bar heights relative to max
const max = Math.max.apply(null, durations);
- const tickWidth = `${runs.length * 16}px`;
return (
- <Tr
- borderBottomWidth={3}
- position="relative"
- >
- <Td
- height="155px"
- p={0}
- position="sticky"
- left={0}
- backgroundColor="white"
- zIndex={2}
- borderBottom={0}
- width="100%"
- >
- {!!runs.length && (
- <>
- <DurationTick bottom="120px">Duration</DurationTick>
- <DurationTick bottom="96px">
- {formatDuration(max)}
- </DurationTick>
- <DurationTick bottom="46px">
- {formatDuration(max / 2)}
- </DurationTick>
- <DurationTick bottom={0}>
- 00:00:00
- </DurationTick>
- </>
- )}
- </Td>
- <Td p={0} borderBottom={0}>
- <Box position="absolute" bottom="100px" borderBottomWidth={1}
zIndex={0} opacity={0.7} width={tickWidth} />
- <Box position="absolute" bottom="50px" borderBottomWidth={1}
zIndex={0} opacity={0.7} width={tickWidth} />
- <Box position="absolute" bottom="4px" borderBottomWidth={1} zIndex={0}
opacity={0.7} width={tickWidth} />
- </Td>
- <Td p={0} align="right" verticalAlign="bottom" borderBottom={0}
width={`${runs.length * 16}px`}>
- <Flex justifyContent="flex-end">
- {runs.map((run: DagRun, i: number) => (
+ <Tr>
+ <Th left={0} zIndex={2}>
+ <Box borderBottomWidth={3} position="relative" height="100%"
width="100%">
+ {!!runs.length && (
+ <>
+ <DurationTick bottom="120px">Duration</DurationTick>
+ <DurationTick bottom="96px">
+ {formatDuration(max)}
+ </DurationTick>
+ <DurationTick bottom="46px">
+ {formatDuration(max / 2)}
+ </DurationTick>
+ <DurationTick bottom={0}>
+ 00:00:00
+ </DurationTick>
+ </>
+ )}
+ </Box>
+ </Th>
+ <Th align="right" verticalAlign="bottom">
+ <Flex justifyContent="flex-end" borderBottomWidth={3}
position="relative">
+ {runs.map((run: RunWithDuration, index) => (
<DagRunBar
key={run.runId}
run={run}
- max={max}
- index={i}
+ index={index}
totalRuns={runs.length}
+ max={max}
isSelected={run.runId === selected.runId}
onSelect={onSelect}
/>
))}
+ <DurationAxis bottom="100px" />
+ <DurationAxis bottom="50px" />
+ <DurationAxis bottom="4px" />
</Flex>
- </Td>
+ </Th>
</Tr>
);
};
diff --git a/airflow/www/static/js/grid/renderTaskRows.tsx
b/airflow/www/static/js/grid/renderTaskRows.tsx
index 11658e873f..cfbae119da 100644
--- a/airflow/www/static/js/grid/renderTaskRows.tsx
+++ b/airflow/www/static/js/grid/renderTaskRows.tsx
@@ -173,7 +173,6 @@ const Row = (props: RowProps) => {
level={level}
/>
</Td>
- <Td width={0} p={0} borderBottom={0} />
<Td
p={0}
align="right"