Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Highlight task states by hovering on legend row #23678

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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