This is an automated email from the ASF dual-hosted git repository.
bbovenzi pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/main by this push:
new 637a8b8af1 Highlight task states by hovering on legend row (#23678)
637a8b8af1 is described below
commit 637a8b8af132e6231756160a3f6ce7ed789abaf6
Author: pierrejeambrun <[email protected]>
AuthorDate: Mon May 23 15:41:46 2022 +0800
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
---
airflow/settings.py | 10 ++--
airflow/www/static/js/grid/FilterBar.jsx | 23 ++-------
airflow/www/static/js/grid/Grid.jsx | 4 +-
airflow/www/static/js/grid/Grid.test.jsx | 32 +++++++++++++
airflow/www/static/js/grid/LegendRow.jsx | 45 +++++++++++++----
airflow/www/static/js/grid/LegendRow.test.jsx | 56 ++++++++++++++++++++++
airflow/www/static/js/grid/Main.jsx | 7 +--
.../www/static/js/grid/components/StatusBox.jsx | 7 ++-
airflow/www/static/js/grid/renderTaskRows.jsx | 5 +-
airflow/www/static/js/grid/utils/useFilters.js | 17 ++-----
.../www/static/js/grid/utils/useFilters.test.jsx | 5 +-
11 files changed, 151 insertions(+), 60 deletions(-)
diff --git a/airflow/settings.py b/airflow/settings.py
index 8b50bde0a1..374960ab3e 100644
--- a/airflow/settings.py
+++ b/airflow/settings.py
@@ -88,16 +88,16 @@ json = json
# 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",
}
diff --git a/airflow/www/static/js/grid/FilterBar.jsx
b/airflow/www/static/js/grid/FilterBar.jsx
index 26d99f1d2e..8776be4c7f 100644
--- a/airflow/www/static/js/grid/FilterBar.jsx
+++ b/airflow/www/static/js/grid/FilterBar.jsx
@@ -39,7 +39,6 @@ const FilterBar = () => {
onNumRunsChange,
onRunTypeChange,
onRunStateChange,
- onTaskStateChange,
clearFilters,
} = useFilters();
@@ -50,13 +49,13 @@ 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}>
@@ -64,7 +63,7 @@ const FilterBar = () => {
{...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>
@@ -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) => (
@@ -88,7 +87,7 @@ 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) => (
@@ -96,18 +95,6 @@ const FilterBar = () => {
))}
</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"
diff --git a/airflow/www/static/js/grid/Grid.jsx
b/airflow/www/static/js/grid/Grid.jsx
index 19ba50bc60..2df059dd35 100644
--- a/airflow/www/static/js/grid/Grid.jsx
+++ b/airflow/www/static/js/grid/Grid.jsx
@@ -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();
@@ -107,7 +107,7 @@ const Grid = ({ isPanelOpen = false }) => {
pr="10px"
>
{renderTaskRows({
- task: groups, dagRunIds, openGroupIds, onToggleGroups,
+ task: groups, dagRunIds, openGroupIds, onToggleGroups,
hoveredTaskState,
})}
</Tbody>
</Table>
diff --git a/airflow/www/static/js/grid/Grid.test.jsx
b/airflow/www/static/js/grid/Grid.test.jsx
index 12a38d1223..9f16fe53d6 100644
--- a/airflow/www/static/js/grid/Grid.test.jsx
+++ b/airflow/www/static/js/grid/Grid.test.jsx
@@ -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');
+ });
+ });
});
diff --git a/airflow/www/static/js/grid/LegendRow.jsx
b/airflow/www/static/js/grid/LegendRow.jsx
index b155c1ba22..3193435701 100644
--- a/airflow/www/static/js/grid/LegendRow.jsx
+++ b/airflow/www/static/js/grid/LegendRow.jsx
@@ -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>
);
diff --git a/airflow/www/static/js/grid/LegendRow.test.jsx
b/airflow/www/static/js/grid/LegendRow.test.jsx
new file mode 100644
index 0000000000..63ea2dfcb4
--- /dev/null
+++ b/airflow/www/static/js/grid/LegendRow.test.jsx
@@ -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();
+ });
+});
diff --git a/airflow/www/static/js/grid/Main.jsx
b/airflow/www/static/js/grid/Main.jsx
index 7f128195b3..8668c1379d 100644
--- a/airflow/www/static/js/grid/Main.jsx
+++ b/airflow/www/static/js/grid/Main.jsx
@@ -19,7 +19,7 @@
/* global localStorage */
-import React from 'react';
+import React, { useState } from 'react';
import {
Box,
Flex,
@@ -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) {
@@ -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"
diff --git a/airflow/www/static/js/grid/components/StatusBox.jsx
b/airflow/www/static/js/grid/components/StatusBox.jsx
index 2868e8727c..316529930f 100644
--- a/airflow/www/static/js/grid/components/StatusBox.jsx
+++ b/airflow/www/static/js/grid/components/StatusBox.jsx
@@ -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`;
@@ -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 = () => {
@@ -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>
@@ -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);
diff --git a/airflow/www/static/js/grid/renderTaskRows.jsx
b/airflow/www/static/js/grid/renderTaskRows.jsx
index 4f3abb8594..94369b4181 100644
--- a/airflow/www/static/js/grid/renderTaskRows.jsx
+++ b/airflow/www/static/js/grid/renderTaskRows.jsx
@@ -48,7 +48,7 @@ const renderTaskRows = ({
));
const TaskInstances = ({
- task, dagRunIds, selectedRunId, onSelect,
+ task, dagRunIds, selectedRunId, onSelect, activeTaskState,
}) => (
<Flex justifyContent="flex-end">
{dagRunIds.map((runId) => {
@@ -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" />}
@@ -88,6 +89,7 @@ const Row = (props) => {
openParentCount = 0,
openGroupIds = [],
onToggleGroups = () => {},
+ hoveredTaskState,
} = props;
const { colors } = useTheme();
const { selected, onSelect } = useSelection();
@@ -162,6 +164,7 @@ const Row = (props) => {
task={task}
selectedRunId={selected.runId}
onSelect={onSelect}
+ activeTaskState={hoveredTaskState}
/>
</Collapse>
</Td>
diff --git a/airflow/www/static/js/grid/utils/useFilters.js
b/airflow/www/static/js/grid/utils/useFilters.js
index f253d32ba5..aa207e2353 100644
--- a/airflow/www/static/js/grid/utils/useFilters.js
+++ b/airflow/www/static/js/grid/utils/useFilters.js
@@ -26,7 +26,6 @@ export const BASE_DATE_PARAM = 'base_date';
export const NUM_RUNS_PARAM = 'num_runs';
export const RUN_TYPE_PARAM = 'run_type';
export const RUN_STATE_PARAM = 'run_state';
-export const TASK_STATE_PARAM = 'task_state';
const date = new Date();
date.setMilliseconds(0);
@@ -40,18 +39,12 @@ const useFilters = () => {
const numRuns = searchParams.get(NUM_RUNS_PARAM) ||
defaultDagRunDisplayNumber;
const runType = searchParams.get(RUN_TYPE_PARAM);
const runState = searchParams.get(RUN_STATE_PARAM);
- // taskState is only used to change the opacity of the tasks,
- // it is not send to the api for filtering.
- const taskState = searchParams.get(TASK_STATE_PARAM);
- const makeOnChangeFn = (paramName, formatFn) => (e) => {
- let { value } = e.target;
- if (formatFn) {
- value = formatFn(value);
- }
+ const makeOnChangeFn = (paramName, formatFn) => (value) => {
+ const formattedValue = formatFn ? formatFn(value) : value;
const params = new URLSearchParams(searchParams);
- if (value) params.set(paramName, value);
+ if (formattedValue) params.set(paramName, formattedValue);
else params.delete(paramName);
setSearchParams(params);
@@ -62,14 +55,12 @@ const useFilters = () => {
const onNumRunsChange = makeOnChangeFn(NUM_RUNS_PARAM);
const onRunTypeChange = makeOnChangeFn(RUN_TYPE_PARAM);
const onRunStateChange = makeOnChangeFn(RUN_STATE_PARAM);
- const onTaskStateChange = makeOnChangeFn(TASK_STATE_PARAM);
const clearFilters = () => {
searchParams.delete(BASE_DATE_PARAM);
searchParams.delete(NUM_RUNS_PARAM);
searchParams.delete(RUN_TYPE_PARAM);
searchParams.delete(RUN_STATE_PARAM);
- searchParams.delete(TASK_STATE_PARAM);
setSearchParams(searchParams);
};
@@ -79,13 +70,11 @@ const useFilters = () => {
numRuns,
runType,
runState,
- taskState,
},
onBaseDateChange,
onNumRunsChange,
onRunTypeChange,
onRunStateChange,
- onTaskStateChange,
clearFilters,
};
};
diff --git a/airflow/www/static/js/grid/utils/useFilters.test.jsx
b/airflow/www/static/js/grid/utils/useFilters.test.jsx
index cbf5824519..f23846efd5 100644
--- a/airflow/www/static/js/grid/utils/useFilters.test.jsx
+++ b/airflow/www/static/js/grid/utils/useFilters.test.jsx
@@ -38,7 +38,6 @@ describe('Test useFilters hook', () => {
numRuns,
runType,
runState,
- taskState,
},
} = result.current;
@@ -46,7 +45,6 @@ describe('Test useFilters hook', () => {
expect(numRuns).toBe(global.defaultDagRunDisplayNumber);
expect(runType).toBeNull();
expect(runState).toBeNull();
- expect(taskState).toBeNull();
});
test.each([
@@ -54,12 +52,11 @@ describe('Test useFilters hook', () => {
{ fnName: 'onNumRunsChange', paramName: 'numRuns', paramValue: '10' },
{ fnName: 'onRunTypeChange', paramName: 'runType', paramValue: 'manual' },
{ fnName: 'onRunStateChange', paramName: 'runState', paramValue: 'success'
},
- { fnName: 'onTaskStateChange', paramName: 'taskState', paramValue:
'deferred' },
])('Test $fnName functions', async ({ fnName, paramName, paramValue }) => {
const { result } = renderHook(() => useFilters(), { wrapper: RouterWrapper
});
await act(async () => {
- result.current[fnName]({ target: { value: paramValue } });
+ result.current[fnName](paramValue);
});
expect(result.current.filters[paramName]).toBe(paramValue);