Skip to content

Commit 637a8b8

Browse files
Highlight task states by hovering on legend row (#23678)
* 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
1 parent a71e4b7 commit 637a8b8

File tree

11 files changed

+151
-60
lines changed

11 files changed

+151
-60
lines changed

airflow/settings.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -88,16 +88,16 @@
8888
# Dictionary containing State and colors associated to each state to
8989
# display on the Webserver
9090
STATE_COLORS = {
91+
"deferred": "mediumpurple",
92+
"failed": "red",
9193
"queued": "gray",
9294
"running": "lime",
95+
"scheduled": "tan",
96+
"skipped": "hotpink",
9397
"success": "green",
94-
"failed": "red",
95-
"up_for_retry": "gold",
9698
"up_for_reschedule": "turquoise",
99+
"up_for_retry": "gold",
97100
"upstream_failed": "orange",
98-
"skipped": "hotpink",
99-
"scheduled": "tan",
100-
"deferred": "mediumpurple",
101101
}
102102

103103

airflow/www/static/js/grid/FilterBar.jsx

Lines changed: 5 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@ const FilterBar = () => {
3939
onNumRunsChange,
4040
onRunTypeChange,
4141
onRunStateChange,
42-
onTaskStateChange,
4342
clearFilters,
4443
} = useFilters();
4544

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

5251
return (
53-
<Flex backgroundColor="#f0f0f0" mt={0} mb={2} p={4}>
52+
<Flex backgroundColor="#f0f0f0" mt={4} p={4}>
5453
<Box px={2}>
5554
<Input
5655
{...inputStyles}
5756
type="datetime-local"
5857
value={formattedTime || ''}
59-
onChange={onBaseDateChange}
58+
onChange={(e) => onBaseDateChange(e.target.value)}
6059
/>
6160
</Box>
6261
<Box px={2}>
6362
<Select
6463
{...inputStyles}
6564
placeholder="Runs"
6665
value={filters.numRuns || ''}
67-
onChange={onNumRunsChange}
66+
onChange={(e) => onNumRunsChange(e.target.value)}
6867
>
6968
{filtersOptions.numRuns.map((value) => (
7069
<option value={value} key={value}>{value}</option>
@@ -75,7 +74,7 @@ const FilterBar = () => {
7574
<Select
7675
{...inputStyles}
7776
value={filters.runType || ''}
78-
onChange={onRunTypeChange}
77+
onChange={(e) => onRunTypeChange(e.target.value)}
7978
>
8079
<option value="" key="all">All Run Types</option>
8180
{filtersOptions.runTypes.map((value) => (
@@ -88,26 +87,14 @@ const FilterBar = () => {
8887
<Select
8988
{...inputStyles}
9089
value={filters.runState || ''}
91-
onChange={onRunStateChange}
90+
onChange={(e) => onRunStateChange(e.target.value)}
9291
>
9392
<option value="" key="all">All Run States</option>
9493
{filtersOptions.dagStates.map((value) => (
9594
<option value={value} key={value}>{value}</option>
9695
))}
9796
</Select>
9897
</Box>
99-
<Box px={2}>
100-
<Select
101-
{...inputStyles}
102-
value={filters.taskState || ''}
103-
onChange={onTaskStateChange}
104-
>
105-
<option value="" key="all">All Task States</option>
106-
{filtersOptions.taskStates.map((value) => (
107-
<option value={value} key={value}>{value}</option>
108-
))}
109-
</Select>
110-
</Box>
11198
<Box px={2}>
11299
<Button
113100
colorScheme="cyan"

airflow/www/static/js/grid/Grid.jsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ import AutoRefresh from './AutoRefresh';
3838

3939
const dagId = getMetaValue('dag_id');
4040

41-
const Grid = ({ isPanelOpen = false }) => {
41+
const Grid = ({ isPanelOpen = false, hoveredTaskState }) => {
4242
const scrollRef = useRef();
4343
const tableRef = useRef();
4444

@@ -107,7 +107,7 @@ const Grid = ({ isPanelOpen = false }) => {
107107
pr="10px"
108108
>
109109
{renderTaskRows({
110-
task: groups, dagRunIds, openGroupIds, onToggleGroups,
110+
task: groups, dagRunIds, openGroupIds, onToggleGroups, hoveredTaskState,
111111
})}
112112
</Tbody>
113113
</Table>

airflow/www/static/js/grid/Grid.test.jsx

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,4 +194,36 @@ describe('Test ToggleGroups', () => {
194194
expect(queryAllByTestId('open-group')).toHaveLength(2);
195195
expect(queryAllByTestId('closed-group')).toHaveLength(0);
196196
});
197+
198+
test('Hovered effect on task state', async () => {
199+
const { rerender, queryAllByTestId } = render(
200+
<Grid />,
201+
{ wrapper: Wrapper },
202+
);
203+
204+
const taskElements = queryAllByTestId('task-instance');
205+
expect(taskElements).toHaveLength(3);
206+
207+
taskElements.forEach((taskElement) => {
208+
expect(taskElement).toHaveStyle('opacity: 1');
209+
});
210+
211+
rerender(
212+
<Grid hoveredTaskState="success" />,
213+
{ wrapper: Wrapper },
214+
);
215+
216+
taskElements.forEach((taskElement) => {
217+
expect(taskElement).toHaveStyle('opacity: 1');
218+
});
219+
220+
rerender(
221+
<Grid hoveredTaskState="failed" />,
222+
{ wrapper: Wrapper },
223+
);
224+
225+
taskElements.forEach((taskElement) => {
226+
expect(taskElement).toHaveStyle('opacity: 0.3');
227+
});
228+
});
197229
});

airflow/www/static/js/grid/LegendRow.jsx

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,20 +22,47 @@
2222
import {
2323
Flex,
2424
Text,
25+
HStack,
2526
} from '@chakra-ui/react';
2627
import React from 'react';
27-
import { SimpleStatus } from './components/StatusBox';
2828

29-
const LegendRow = () => (
30-
<Flex mt={0} mb={2} p={4} flexWrap="wrap">
31-
{
29+
const StatusBadge = ({
30+
state, stateColor, setHoveredTaskState, displayValue,
31+
}) => (
32+
<Text
33+
borderRadius={4}
34+
border={`solid 2px ${stateColor}`}
35+
px={1}
36+
cursor="pointer"
37+
fontSize="11px"
38+
onMouseEnter={() => setHoveredTaskState(state)}
39+
onMouseLeave={() => setHoveredTaskState()}
40+
>
41+
{displayValue || state }
42+
</Text>
43+
);
44+
45+
const LegendRow = ({ setHoveredTaskState }) => (
46+
<Flex p={4} flexWrap="wrap" justifyContent="end">
47+
<HStack spacing={2}>
48+
{
3249
Object.entries(stateColors).map(([state, stateColor]) => (
33-
<Flex alignItems="center" mr={3} key={stateColor}>
34-
<SimpleStatus mr={1} state={state} />
35-
<Text fontSize="md">{state}</Text>
36-
</Flex>
50+
<StatusBadge
51+
key={state}
52+
state={state}
53+
stateColor={stateColor}
54+
setHoveredTaskState={setHoveredTaskState}
55+
/>
3756
))
38-
}
57+
}
58+
<StatusBadge
59+
key="no_status"
60+
displayValue="no_status"
61+
state={null}
62+
stateColor="white"
63+
setHoveredTaskState={setHoveredTaskState}
64+
/>
65+
</HStack>
3966
</Flex>
4067
);
4168

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/*!
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
/* global describe, test, expect, stateColors, jest */
21+
22+
import React from 'react';
23+
import { render, fireEvent } from '@testing-library/react';
24+
25+
import LegendRow from './LegendRow';
26+
27+
describe('Test LegendRow', () => {
28+
test('Render displays correctly the different task states', () => {
29+
const { getByText } = render(
30+
<LegendRow />,
31+
);
32+
33+
Object.keys(stateColors).forEach((taskState) => {
34+
expect(getByText(taskState)).toBeInTheDocument();
35+
});
36+
37+
expect(getByText('no_status')).toBeInTheDocument();
38+
});
39+
40+
test.each([
41+
{ state: 'success', expectedSetValue: 'success' },
42+
{ state: 'failed', expectedSetValue: 'failed' },
43+
{ state: 'no_status', expectedSetValue: null },
44+
])('Hovering $state badge should trigger setHoverdTaskState function with $expectedSetValue',
45+
async ({ state, expectedSetValue }) => {
46+
const setHoveredTaskState = jest.fn();
47+
const { getByText } = render(
48+
<LegendRow setHoveredTaskState={setHoveredTaskState} />,
49+
);
50+
const successElement = getByText(state);
51+
fireEvent.mouseEnter(successElement);
52+
expect(setHoveredTaskState).toHaveBeenCalledWith(expectedSetValue);
53+
fireEvent.mouseLeave(successElement);
54+
expect(setHoveredTaskState).toHaveBeenLastCalledWith();
55+
});
56+
});

airflow/www/static/js/grid/Main.jsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919

2020
/* global localStorage */
2121

22-
import React from 'react';
22+
import React, { useState } from 'react';
2323
import {
2424
Box,
2525
Flex,
@@ -40,6 +40,7 @@ const Main = () => {
4040
const isPanelOpen = localStorage.getItem(detailsPanelKey) !== 'true';
4141
const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: isPanelOpen });
4242
const { clearSelection } = useSelection();
43+
const [hoveredTaskState, setHoveredTaskState] = useState();
4344

4445
const toggleDetailsPanel = () => {
4546
if (!isOpen) {
@@ -54,10 +55,10 @@ const Main = () => {
5455
return (
5556
<Box>
5657
<FilterBar />
57-
<LegendRow />
58+
<LegendRow setHoveredTaskState={setHoveredTaskState} />
5859
<Divider mb={5} borderBottomWidth={2} />
5960
<Flex flexDirection="row" justifyContent="space-between">
60-
<Grid isPanelOpen={isOpen} />
61+
<Grid isPanelOpen={isOpen} hoveredTaskState={hoveredTaskState} />
6162
<Box borderLeftWidth={isOpen ? 1 : 0} position="relative">
6263
<Button
6364
position="absolute"

airflow/www/static/js/grid/components/StatusBox.jsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ import {
2929

3030
import InstanceTooltip from './InstanceTooltip';
3131
import { useContainerRef } from '../context/containerRef';
32-
import useFilters from '../utils/useFilters';
3332

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

4847
const StatusBox = ({
49-
group, instance, onSelect,
48+
group, instance, onSelect, isActive,
5049
}) => {
5150
const containerRef = useContainerRef();
5251
const { runId, taskId } = instance;
5352
const { colors } = useTheme();
5453
const hoverBlue = `${colors.blue[100]}50`;
55-
const { filters } = useFilters();
5654

5755
// Fetch the corresponding column element and set its background color when hovering
5856
const onMouseEnter = () => {
@@ -89,7 +87,7 @@ const StatusBox = ({
8987
zIndex={1}
9088
onMouseEnter={onMouseEnter}
9189
onMouseLeave={onMouseLeave}
92-
opacity={(filters.taskState && filters.taskState !== instance.state) ? 0.30 : 1}
90+
opacity={isActive ? 1 : 0.3}
9391
/>
9492
</Box>
9593
</Tooltip>
@@ -104,6 +102,7 @@ const compareProps = (
104102
) => (
105103
isEqual(prevProps.group, nextProps.group)
106104
&& isEqual(prevProps.instance, nextProps.instance)
105+
&& isEqual(prevProps.isActive, nextProps.isActive)
107106
);
108107

109108
export default React.memo(StatusBox, compareProps);

airflow/www/static/js/grid/renderTaskRows.jsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ const renderTaskRows = ({
4848
));
4949

5050
const TaskInstances = ({
51-
task, dagRunIds, selectedRunId, onSelect,
51+
task, dagRunIds, selectedRunId, onSelect, activeTaskState,
5252
}) => (
5353
<Flex justifyContent="flex-end">
5454
{dagRunIds.map((runId) => {
@@ -71,6 +71,7 @@ const TaskInstances = ({
7171
instance={instance}
7272
group={task}
7373
onSelect={onSelect}
74+
isActive={activeTaskState === undefined || activeTaskState === instance.state}
7475
/>
7576
)
7677
: <Box width={boxSizePx} data-testid="blank-task" />}
@@ -88,6 +89,7 @@ const Row = (props) => {
8889
openParentCount = 0,
8990
openGroupIds = [],
9091
onToggleGroups = () => {},
92+
hoveredTaskState,
9193
} = props;
9294
const { colors } = useTheme();
9395
const { selected, onSelect } = useSelection();
@@ -162,6 +164,7 @@ const Row = (props) => {
162164
task={task}
163165
selectedRunId={selected.runId}
164166
onSelect={onSelect}
167+
activeTaskState={hoveredTaskState}
165168
/>
166169
</Collapse>
167170
</Td>

0 commit comments

Comments
 (0)