Skip to content

Commit

Permalink
Fix Grid vertical scrolling (#24684)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
bbovenzi authored Jun 28, 2022
1 parent 5326da4 commit 1429091
Show file tree
Hide file tree
Showing 4 changed files with 103 additions and 87 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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);
};
Expand All @@ -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);
}
});
Expand All @@ -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 />
Expand All @@ -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>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,33 +35,44 @@ 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`;

// 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"
Expand Down Expand Up @@ -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" />
Expand All @@ -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
Expand Down
87 changes: 44 additions & 43 deletions airflow/www/static/js/grid/dagRuns/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,10 @@ import {
Td,
Text,
Box,
Flex,
TextProps,
TableCellProps,
Flex,
BoxProps,
} from '@chakra-ui/react';

import { useGridData } from '../api';
Expand All @@ -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 {
Expand All @@ -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>
);
};
Expand Down
1 change: 0 additions & 1 deletion airflow/www/static/js/grid/renderTaskRows.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,6 @@ const Row = (props: RowProps) => {
level={level}
/>
</Td>
<Td width={0} p={0} borderBottom={0} />
<Td
p={0}
align="right"
Expand Down

0 comments on commit 1429091

Please sign in to comment.