Skip to content

Commit

Permalink
Highlight task states by hovering on legend row (#23678)
Browse files Browse the repository at this point in the history
* Rework the legend row and add the hover effect.

* Move horevedTaskState to state and fix merge conflicts.

* Add tests.

* Order of item in the LegendRow, add no_status support
  • Loading branch information
pierrejeambrun authored May 23, 2022
1 parent a71e4b7 commit 637a8b8
Show file tree
Hide file tree
Showing 11 changed files with 151 additions and 60 deletions.
10 changes: 5 additions & 5 deletions airflow/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,16 +88,16 @@
# Dictionary containing State and colors associated to each state to
# display on the Webserver
STATE_COLORS = {
"deferred": "mediumpurple",
"failed": "red",
"queued": "gray",
"running": "lime",
"scheduled": "tan",
"skipped": "hotpink",
"success": "green",
"failed": "red",
"up_for_retry": "gold",
"up_for_reschedule": "turquoise",
"up_for_retry": "gold",
"upstream_failed": "orange",
"skipped": "hotpink",
"scheduled": "tan",
"deferred": "mediumpurple",
}


Expand Down
23 changes: 5 additions & 18 deletions airflow/www/static/js/grid/FilterBar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ const FilterBar = () => {
onNumRunsChange,
onRunTypeChange,
onRunStateChange,
onTaskStateChange,
clearFilters,
} = useFilters();

Expand All @@ -50,21 +49,21 @@ const FilterBar = () => {
const inputStyles = { backgroundColor: 'white', size: 'lg' };

return (
<Flex backgroundColor="#f0f0f0" mt={0} mb={2} p={4}>
<Flex backgroundColor="#f0f0f0" mt={4} p={4}>
<Box px={2}>
<Input
{...inputStyles}
type="datetime-local"
value={formattedTime || ''}
onChange={onBaseDateChange}
onChange={(e) => onBaseDateChange(e.target.value)}
/>
</Box>
<Box px={2}>
<Select
{...inputStyles}
placeholder="Runs"
value={filters.numRuns || ''}
onChange={onNumRunsChange}
onChange={(e) => onNumRunsChange(e.target.value)}
>
{filtersOptions.numRuns.map((value) => (
<option value={value} key={value}>{value}</option>
Expand All @@ -75,7 +74,7 @@ const FilterBar = () => {
<Select
{...inputStyles}
value={filters.runType || ''}
onChange={onRunTypeChange}
onChange={(e) => onRunTypeChange(e.target.value)}
>
<option value="" key="all">All Run Types</option>
{filtersOptions.runTypes.map((value) => (
Expand All @@ -88,26 +87,14 @@ const FilterBar = () => {
<Select
{...inputStyles}
value={filters.runState || ''}
onChange={onRunStateChange}
onChange={(e) => onRunStateChange(e.target.value)}
>
<option value="" key="all">All Run States</option>
{filtersOptions.dagStates.map((value) => (
<option value={value} key={value}>{value}</option>
))}
</Select>
</Box>
<Box px={2}>
<Select
{...inputStyles}
value={filters.taskState || ''}
onChange={onTaskStateChange}
>
<option value="" key="all">All Task States</option>
{filtersOptions.taskStates.map((value) => (
<option value={value} key={value}>{value}</option>
))}
</Select>
</Box>
<Box px={2}>
<Button
colorScheme="cyan"
Expand Down
4 changes: 2 additions & 2 deletions airflow/www/static/js/grid/Grid.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ import AutoRefresh from './AutoRefresh';

const dagId = getMetaValue('dag_id');

const Grid = ({ isPanelOpen = false }) => {
const Grid = ({ isPanelOpen = false, hoveredTaskState }) => {
const scrollRef = useRef();
const tableRef = useRef();

Expand Down Expand Up @@ -107,7 +107,7 @@ const Grid = ({ isPanelOpen = false }) => {
pr="10px"
>
{renderTaskRows({
task: groups, dagRunIds, openGroupIds, onToggleGroups,
task: groups, dagRunIds, openGroupIds, onToggleGroups, hoveredTaskState,
})}
</Tbody>
</Table>
Expand Down
32 changes: 32 additions & 0 deletions airflow/www/static/js/grid/Grid.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -194,4 +194,36 @@ describe('Test ToggleGroups', () => {
expect(queryAllByTestId('open-group')).toHaveLength(2);
expect(queryAllByTestId('closed-group')).toHaveLength(0);
});

test('Hovered effect on task state', async () => {
const { rerender, queryAllByTestId } = render(
<Grid />,
{ wrapper: Wrapper },
);

const taskElements = queryAllByTestId('task-instance');
expect(taskElements).toHaveLength(3);

taskElements.forEach((taskElement) => {
expect(taskElement).toHaveStyle('opacity: 1');
});

rerender(
<Grid hoveredTaskState="success" />,
{ wrapper: Wrapper },
);

taskElements.forEach((taskElement) => {
expect(taskElement).toHaveStyle('opacity: 1');
});

rerender(
<Grid hoveredTaskState="failed" />,
{ wrapper: Wrapper },
);

taskElements.forEach((taskElement) => {
expect(taskElement).toHaveStyle('opacity: 0.3');
});
});
});
45 changes: 36 additions & 9 deletions airflow/www/static/js/grid/LegendRow.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,20 +22,47 @@
import {
Flex,
Text,
HStack,
} from '@chakra-ui/react';
import React from 'react';
import { SimpleStatus } from './components/StatusBox';

const LegendRow = () => (
<Flex mt={0} mb={2} p={4} flexWrap="wrap">
{
const StatusBadge = ({
state, stateColor, setHoveredTaskState, displayValue,
}) => (
<Text
borderRadius={4}
border={`solid 2px ${stateColor}`}
px={1}
cursor="pointer"
fontSize="11px"
onMouseEnter={() => setHoveredTaskState(state)}
onMouseLeave={() => setHoveredTaskState()}
>
{displayValue || state }
</Text>
);

const LegendRow = ({ setHoveredTaskState }) => (
<Flex p={4} flexWrap="wrap" justifyContent="end">
<HStack spacing={2}>
{
Object.entries(stateColors).map(([state, stateColor]) => (
<Flex alignItems="center" mr={3} key={stateColor}>
<SimpleStatus mr={1} state={state} />
<Text fontSize="md">{state}</Text>
</Flex>
<StatusBadge
key={state}
state={state}
stateColor={stateColor}
setHoveredTaskState={setHoveredTaskState}
/>
))
}
}
<StatusBadge
key="no_status"
displayValue="no_status"
state={null}
stateColor="white"
setHoveredTaskState={setHoveredTaskState}
/>
</HStack>
</Flex>
);

Expand Down
56 changes: 56 additions & 0 deletions airflow/www/static/js/grid/LegendRow.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*!
* 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.
*/

/* global describe, test, expect, stateColors, jest */

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

import LegendRow from './LegendRow';

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

Object.keys(stateColors).forEach((taskState) => {
expect(getByText(taskState)).toBeInTheDocument();
});

expect(getByText('no_status')).toBeInTheDocument();
});

test.each([
{ state: 'success', expectedSetValue: 'success' },
{ state: 'failed', expectedSetValue: 'failed' },
{ state: 'no_status', expectedSetValue: null },
])('Hovering $state badge should trigger setHoverdTaskState function with $expectedSetValue',
async ({ state, expectedSetValue }) => {
const setHoveredTaskState = jest.fn();
const { getByText } = render(
<LegendRow setHoveredTaskState={setHoveredTaskState} />,
);
const successElement = getByText(state);
fireEvent.mouseEnter(successElement);
expect(setHoveredTaskState).toHaveBeenCalledWith(expectedSetValue);
fireEvent.mouseLeave(successElement);
expect(setHoveredTaskState).toHaveBeenLastCalledWith();
});
});
7 changes: 4 additions & 3 deletions airflow/www/static/js/grid/Main.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

/* global localStorage */

import React from 'react';
import React, { useState } from 'react';
import {
Box,
Flex,
Expand All @@ -40,6 +40,7 @@ const Main = () => {
const isPanelOpen = localStorage.getItem(detailsPanelKey) !== 'true';
const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: isPanelOpen });
const { clearSelection } = useSelection();
const [hoveredTaskState, setHoveredTaskState] = useState();

const toggleDetailsPanel = () => {
if (!isOpen) {
Expand All @@ -54,10 +55,10 @@ const Main = () => {
return (
<Box>
<FilterBar />
<LegendRow />
<LegendRow setHoveredTaskState={setHoveredTaskState} />
<Divider mb={5} borderBottomWidth={2} />
<Flex flexDirection="row" justifyContent="space-between">
<Grid isPanelOpen={isOpen} />
<Grid isPanelOpen={isOpen} hoveredTaskState={hoveredTaskState} />
<Box borderLeftWidth={isOpen ? 1 : 0} position="relative">
<Button
position="absolute"
Expand Down
7 changes: 3 additions & 4 deletions airflow/www/static/js/grid/components/StatusBox.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ import {

import InstanceTooltip from './InstanceTooltip';
import { useContainerRef } from '../context/containerRef';
import useFilters from '../utils/useFilters';

export const boxSize = 10;
export const boxSizePx = `${boxSize}px`;
Expand All @@ -46,13 +45,12 @@ export const SimpleStatus = ({ state, ...rest }) => (
);

const StatusBox = ({
group, instance, onSelect,
group, instance, onSelect, isActive,
}) => {
const containerRef = useContainerRef();
const { runId, taskId } = instance;
const { colors } = useTheme();
const hoverBlue = `${colors.blue[100]}50`;
const { filters } = useFilters();

// Fetch the corresponding column element and set its background color when hovering
const onMouseEnter = () => {
Expand Down Expand Up @@ -89,7 +87,7 @@ const StatusBox = ({
zIndex={1}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
opacity={(filters.taskState && filters.taskState !== instance.state) ? 0.30 : 1}
opacity={isActive ? 1 : 0.3}
/>
</Box>
</Tooltip>
Expand All @@ -104,6 +102,7 @@ const compareProps = (
) => (
isEqual(prevProps.group, nextProps.group)
&& isEqual(prevProps.instance, nextProps.instance)
&& isEqual(prevProps.isActive, nextProps.isActive)
);

export default React.memo(StatusBox, compareProps);
5 changes: 4 additions & 1 deletion airflow/www/static/js/grid/renderTaskRows.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ const renderTaskRows = ({
));

const TaskInstances = ({
task, dagRunIds, selectedRunId, onSelect,
task, dagRunIds, selectedRunId, onSelect, activeTaskState,
}) => (
<Flex justifyContent="flex-end">
{dagRunIds.map((runId) => {
Expand All @@ -71,6 +71,7 @@ const TaskInstances = ({
instance={instance}
group={task}
onSelect={onSelect}
isActive={activeTaskState === undefined || activeTaskState === instance.state}
/>
)
: <Box width={boxSizePx} data-testid="blank-task" />}
Expand All @@ -88,6 +89,7 @@ const Row = (props) => {
openParentCount = 0,
openGroupIds = [],
onToggleGroups = () => {},
hoveredTaskState,
} = props;
const { colors } = useTheme();
const { selected, onSelect } = useSelection();
Expand Down Expand Up @@ -162,6 +164,7 @@ const Row = (props) => {
task={task}
selectedRunId={selected.runId}
onSelect={onSelect}
activeTaskState={hoveredTaskState}
/>
</Collapse>
</Td>
Expand Down
Loading

0 comments on commit 637a8b8

Please sign in to comment.