This is an automated email from the ASF dual-hosted git repository. ephraimanierobi pushed a commit to branch v2-3-test in repository https://gitbox.apache.org/repos/asf/airflow.git
commit b73c38cc358b36e797eca4dc2a9847740244c913 Author: Brent Bovenzi <brent.bove...@gmail.com> AuthorDate: Tue Jun 28 14:50:02 2022 -0400 Debounce status highlighting in Grid view (#24710) * Add delay / debounce to not always highlight tasks * fix linting * single delay variable at 200ms (cherry picked from commit c7feb31786c7744da91d319f499d9f6015d82454) --- airflow/www/static/js/grid/Grid.tsx | 3 +-- .../{LegendRow.test.jsx => LegendRow.test.tsx} | 15 +++++++----- .../js/grid/{LegendRow.jsx => LegendRow.tsx} | 27 ++++++++++++++++------ airflow/www/static/js/grid/Main.tsx | 15 +++++++++--- .../www/static/js/grid/components/Clipboard.jsx | 1 - .../www/static/js/grid/components/StatusBox.tsx | 3 ++- airflow/www/static/js/grid/dagRuns/Bar.tsx | 3 ++- airflow/www/static/js/grid/renderTaskRows.tsx | 4 ++-- airflow/www/static/js/grid/utils/index.ts | 26 +++++++++++++++++++++ 9 files changed, 74 insertions(+), 23 deletions(-) diff --git a/airflow/www/static/js/grid/Grid.tsx b/airflow/www/static/js/grid/Grid.tsx index c32d8230ce..6de3d0ecc2 100644 --- a/airflow/www/static/js/grid/Grid.tsx +++ b/airflow/www/static/js/grid/Grid.tsx @@ -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) => { @@ -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, diff --git a/airflow/www/static/js/grid/LegendRow.test.jsx b/airflow/www/static/js/grid/LegendRow.test.tsx similarity index 76% rename from airflow/www/static/js/grid/LegendRow.test.jsx rename to airflow/www/static/js/grid/LegendRow.test.tsx index bd1c64d0c3..7a3ba18593 100644 --- a/airflow/www/static/js/grid/LegendRow.test.jsx +++ b/airflow/www/static/js/grid/LegendRow.test.tsx @@ -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) => { @@ -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()); }, ); }); diff --git a/airflow/www/static/js/grid/LegendRow.jsx b/airflow/www/static/js/grid/LegendRow.tsx similarity index 71% rename from airflow/www/static/js/grid/LegendRow.jsx rename to airflow/www/static/js/grid/LegendRow.tsx index eff503403c..ade94b0592 100644 --- a/airflow/www/static/js/grid/LegendRow.jsx +++ b/airflow/www/static/js/grid/LegendRow.tsx @@ -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"> { @@ -51,7 +62,8 @@ const LegendRow = ({ setHoveredTaskState }) => ( key={state} state={state} stateColor={stateColor} - setHoveredTaskState={setHoveredTaskState} + onStatusHover={onStatusHover} + onStatusLeave={onStatusLeave} /> )) } @@ -60,7 +72,8 @@ const LegendRow = ({ setHoveredTaskState }) => ( displayValue="no_status" state={null} stateColor="white" - setHoveredTaskState={setHoveredTaskState} + onStatusHover={onStatusHover} + onStatusLeave={onStatusLeave} /> </HStack> </Flex> diff --git a/airflow/www/static/js/grid/Main.tsx b/airflow/www/static/js/grid/Main.tsx index 8779b547ec..bde36c2984 100644 --- a/airflow/www/static/js/grid/Main.tsx +++ b/airflow/www/static/js/grid/Main.tsx @@ -27,7 +27,7 @@ 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'; @@ -35,6 +35,7 @@ import Grid from './Grid'; import FilterBar from './FilterBar'; import LegendRow from './LegendRow'; import { useGridData } from './api'; +import { hoverDelay } from './utils'; const detailsPanelKey = 'hideDetailsPanel'; @@ -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) { @@ -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) diff --git a/airflow/www/static/js/grid/components/Clipboard.jsx b/airflow/www/static/js/grid/components/Clipboard.jsx index 794e363fa0..a4e0acca28 100644 --- a/airflow/www/static/js/grid/components/Clipboard.jsx +++ b/airflow/www/static/js/grid/components/Clipboard.jsx @@ -60,7 +60,6 @@ export const ClipboardButton = forwardRef( label="Copied" isOpen={hasCopied} isDisabled={!hasCopied} - closeDelay={500} placement="top" portalProps={{ containerRef }} > diff --git a/airflow/www/static/js/grid/components/StatusBox.tsx b/airflow/www/static/js/grid/components/StatusBox.tsx index f9acf57402..dd8dfabfb1 100644 --- a/airflow/www/static/js/grid/components/StatusBox.tsx +++ b/airflow/www/static/js/grid/components/StatusBox.tsx @@ -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`; @@ -92,7 +93,7 @@ const StatusBox = ({ portalProps={{ containerRef }} hasArrow placement="top" - openDelay={400} + openDelay={hoverDelay} > <Box> <SimpleStatus diff --git a/airflow/www/static/js/grid/dagRuns/Bar.tsx b/airflow/www/static/js/grid/dagRuns/Bar.tsx index 18399ab885..00c5214b83 100644 --- a/airflow/www/static/js/grid/dagRuns/Bar.tsx +++ b/airflow/www/static/js/grid/dagRuns/Bar.tsx @@ -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; @@ -95,7 +96,7 @@ const DagRunBar = ({ hasArrow portalProps={{ containerRef }} placement="top" - openDelay={100} + openDelay={hoverDelay} > <Flex width="10px" diff --git a/airflow/www/static/js/grid/renderTaskRows.tsx b/airflow/www/static/js/grid/renderTaskRows.tsx index cfbae119da..f35d1a2c9e 100644 --- a/airflow/www/static/js/grid/renderTaskRows.tsx +++ b/airflow/www/static/js/grid/renderTaskRows.tsx @@ -43,7 +43,7 @@ interface RowProps { openParentCount?: number; openGroupIds?: string[]; onToggleGroups?: (groupIds: string[]) => void; - hoveredTaskState?: string; + hoveredTaskState?: string | null; } const renderTaskRows = ({ @@ -67,7 +67,7 @@ interface TaskInstancesProps { dagRunIds: string[]; selectedRunId?: string | null; onSelect: (selection: SelectionProps) => void; - hoveredTaskState?: string; + hoveredTaskState?: string | null; } const TaskInstances = ({ diff --git a/airflow/www/static/js/grid/utils/index.ts b/airflow/www/static/js/grid/utils/index.ts new file mode 100644 index 0000000000..165de55616 --- /dev/null +++ b/airflow/www/static/js/grid/utils/index.ts @@ -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, +};