Skip to content

Commit

Permalink
Debounce status highlighting in Grid view (#24710)
Browse files Browse the repository at this point in the history
* Add delay / debounce to not always highlight tasks

* fix linting

* single delay variable at 200ms
  • Loading branch information
bbovenzi authored Jun 28, 2022
1 parent 2fbd750 commit c7feb31
Show file tree
Hide file tree
Showing 9 changed files with 74 additions and 23 deletions.
3 changes: 1 addition & 2 deletions airflow/www/static/js/grid/Grid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ const dagId = getMetaValue('dag_id');
interface Props {
isPanelOpen?: boolean;
onPanelToggle: () => void;
hoveredTaskState?: string;
hoveredTaskState?: string | null;
}

const Grid = ({ isPanelOpen = false, onPanelToggle, hoveredTaskState }: Props) => {
Expand Down Expand Up @@ -129,7 +129,6 @@ const Grid = ({ isPanelOpen = false, onPanelToggle, hoveredTaskState }: Props) =
<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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,16 @@
/* global describe, test, expect, stateColors, jest */

import React from 'react';
import { render, fireEvent } from '@testing-library/react';
import { render, fireEvent, waitFor } from '@testing-library/react';

import LegendRow from './LegendRow';

describe('Test LegendRow', () => {
test('Render displays correctly the different task states', () => {
const onStatusHover = jest.fn();
const onStatusLeave = jest.fn();
const { getByText } = render(
<LegendRow />,
<LegendRow onStatusHover={onStatusHover} onStatusLeave={onStatusLeave} />,
);

Object.keys(stateColors).forEach((taskState) => {
Expand All @@ -44,15 +46,16 @@ describe('Test LegendRow', () => {
])(
'Hovering $state badge should trigger setHoverdTaskState function with $expectedSetValue',
async ({ state, expectedSetValue }) => {
const setHoveredTaskState = jest.fn();
const onStatusHover = jest.fn();
const onStatusLeave = jest.fn();
const { getByText } = render(
<LegendRow setHoveredTaskState={setHoveredTaskState} />,
<LegendRow onStatusHover={onStatusHover} onStatusLeave={onStatusLeave} />,
);
const successElement = getByText(state);
fireEvent.mouseEnter(successElement);
expect(setHoveredTaskState).toHaveBeenCalledWith(expectedSetValue);
await waitFor(() => expect(onStatusHover).toHaveBeenCalledWith(expectedSetValue));
fireEvent.mouseLeave(successElement);
expect(setHoveredTaskState).toHaveBeenLastCalledWith();
await waitFor(() => expect(onStatusLeave).toHaveBeenLastCalledWith());
},
);
});
Original file line number Diff line number Diff line change
Expand Up @@ -26,23 +26,34 @@ import {
} from '@chakra-ui/react';
import React from 'react';

interface LegendProps {
onStatusHover: (status: string | null) => void;
onStatusLeave: () => void;
}

interface BadgeProps extends LegendProps {
state: string | null;
stateColor: string;
displayValue?: string;
}

const StatusBadge = ({
state, stateColor, setHoveredTaskState, displayValue,
}) => (
state, stateColor, onStatusHover, onStatusLeave, displayValue,
}: BadgeProps) => (
<Text
borderRadius={4}
border={`solid 2px ${stateColor}`}
px={1}
cursor="pointer"
fontSize="11px"
onMouseEnter={() => setHoveredTaskState(state)}
onMouseLeave={() => setHoveredTaskState()}
onMouseEnter={() => onStatusHover(state)}
onMouseLeave={() => onStatusLeave()}
>
{displayValue || state }
</Text>
);

const LegendRow = ({ setHoveredTaskState }) => (
const LegendRow = ({ onStatusHover, onStatusLeave }: LegendProps) => (
<Flex p={4} flexWrap="wrap" justifyContent="end">
<HStack spacing={2} wrap="wrap">
{
Expand All @@ -51,7 +62,8 @@ const LegendRow = ({ setHoveredTaskState }) => (
key={state}
state={state}
stateColor={stateColor}
setHoveredTaskState={setHoveredTaskState}
onStatusHover={onStatusHover}
onStatusLeave={onStatusLeave}
/>
))
}
Expand All @@ -60,7 +72,8 @@ const LegendRow = ({ setHoveredTaskState }) => (
displayValue="no_status"
state={null}
stateColor="white"
setHoveredTaskState={setHoveredTaskState}
onStatusHover={onStatusHover}
onStatusLeave={onStatusLeave}
/>
</HStack>
</Flex>
Expand Down
15 changes: 12 additions & 3 deletions airflow/www/static/js/grid/Main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,15 @@ import {
Divider,
Spinner,
} from '@chakra-ui/react';
import { isEmpty } from 'lodash';
import { isEmpty, debounce } from 'lodash';

import Details from './details';
import useSelection from './utils/useSelection';
import Grid from './Grid';
import FilterBar from './FilterBar';
import LegendRow from './LegendRow';
import { useGridData } from './api';
import { hoverDelay } from './utils';

const detailsPanelKey = 'hideDetailsPanel';

Expand All @@ -43,7 +44,15 @@ const Main = () => {
const isPanelOpen = localStorage.getItem(detailsPanelKey) !== 'true';
const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: isPanelOpen });
const { clearSelection } = useSelection();
const [hoveredTaskState, setHoveredTaskState] = useState();
const [hoveredTaskState, setHoveredTaskState] = useState<string | null | undefined>();

// Add a debounced delay to not constantly trigger highlighting certain task states
const onStatusHover = debounce((state) => setHoveredTaskState(state), hoverDelay);

const onStatusLeave = () => {
setHoveredTaskState(undefined);
onStatusHover.cancel();
};

const onPanelToggle = () => {
if (!isOpen) {
Expand All @@ -58,7 +67,7 @@ const Main = () => {
return (
<Box>
<FilterBar />
<LegendRow setHoveredTaskState={setHoveredTaskState} />
<LegendRow onStatusHover={onStatusHover} onStatusLeave={onStatusLeave} />
<Divider mb={5} borderBottomWidth={2} />
<Flex justifyContent="space-between">
{isLoading || isEmpty(groups)
Expand Down
1 change: 0 additions & 1 deletion airflow/www/static/js/grid/components/Clipboard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,6 @@ export const ClipboardButton = forwardRef(
label="Copied"
isOpen={hasCopied}
isDisabled={!hasCopied}
closeDelay={500}
placement="top"
portalProps={{ containerRef }}
>
Expand Down
3 changes: 2 additions & 1 deletion airflow/www/static/js/grid/components/StatusBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import InstanceTooltip from './InstanceTooltip';
import { useContainerRef } from '../context/containerRef';
import type { Task, TaskInstance, TaskState } from '../types';
import type { SelectionProps } from '../utils/useSelection';
import { hoverDelay } from '../utils';

export const boxSize = 10;
export const boxSizePx = `${boxSize}px`;
Expand Down Expand Up @@ -92,7 +93,7 @@ const StatusBox = ({
portalProps={{ containerRef }}
hasArrow
placement="top"
openDelay={400}
openDelay={hoverDelay}
>
<Box>
<SimpleStatus
Expand Down
3 changes: 2 additions & 1 deletion airflow/www/static/js/grid/dagRuns/Bar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import { useContainerRef } from '../context/containerRef';
import Time from '../components/Time';
import type { SelectionProps } from '../utils/useSelection';
import type { RunWithDuration } from '.';
import { hoverDelay } from '../utils';

const BAR_HEIGHT = 100;

Expand Down Expand Up @@ -95,7 +96,7 @@ const DagRunBar = ({
hasArrow
portalProps={{ containerRef }}
placement="top"
openDelay={100}
openDelay={hoverDelay}
>
<Flex
width="10px"
Expand Down
4 changes: 2 additions & 2 deletions airflow/www/static/js/grid/renderTaskRows.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ interface RowProps {
openParentCount?: number;
openGroupIds?: string[];
onToggleGroups?: (groupIds: string[]) => void;
hoveredTaskState?: string;
hoveredTaskState?: string | null;
}

const renderTaskRows = ({
Expand All @@ -67,7 +67,7 @@ interface TaskInstancesProps {
dagRunIds: string[];
selectedRunId?: string | null;
onSelect: (selection: SelectionProps) => void;
hoveredTaskState?: string;
hoveredTaskState?: string | null;
}

const TaskInstances = ({
Expand Down
26 changes: 26 additions & 0 deletions airflow/www/static/js/grid/utils/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/* eslint-disable import/prefer-default-export */
/*!
* 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.
*/

// Delay in ms for various hover actions
const hoverDelay = 200;

export {
hoverDelay,
};

0 comments on commit c7feb31

Please sign in to comment.