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 b9acffa81b Fix for TaskGroup toggles for duplicated labels (#34072)
b9acffa81b is described below
commit b9acffa81bf61dcf0c5553942c52629c7f75ebe2
Author: Usiel Riedl <[email protected]>
AuthorDate: Wed Sep 6 12:23:20 2023 +0200
Fix for TaskGroup toggles for duplicated labels (#34072)
* Fix for TaskGroup toggles for duplicated labels
We have been using `_.label` so far to remember which task groups are
opened/closed. Unfortunately, this leads to issues when the same label is used
twice, which can happen as described in the linked issue.
With this change we switch to use `_.id` instead, which must be unique
(otherwise parsing the DAG would lead to `DuplicateTaskIdFound`).
Fixes #34066
* Fix toggle for Gantt as well
---
airflow/www/static/js/dag/details/gantt/Row.tsx | 2 +-
airflow/www/static/js/dag/details/graph/utils.ts | 4 +-
airflow/www/static/js/dag/grid/TaskName.test.tsx | 12 ++-
airflow/www/static/js/dag/grid/TaskName.tsx | 3 +
airflow/www/static/js/dag/grid/ToggleGroups.tsx | 2 +-
airflow/www/static/js/dag/grid/index.test.tsx | 117 ++++++++++++++++++----
airflow/www/static/js/dag/grid/renderTaskRows.tsx | 11 +-
airflow/www/static/js/utils/graph.ts | 4 +-
8 files changed, 123 insertions(+), 32 deletions(-)
diff --git a/airflow/www/static/js/dag/details/gantt/Row.tsx
b/airflow/www/static/js/dag/details/gantt/Row.tsx
index 304fb83e22..bf1ce7ac60 100644
--- a/airflow/www/static/js/dag/details/gantt/Row.tsx
+++ b/airflow/www/static/js/dag/details/gantt/Row.tsx
@@ -53,7 +53,7 @@ const Row = ({
const instance = task.instances.find((ti) => ti.runId === runId);
const isSelected = taskId === instance?.taskId;
const hasQueuedDttm = !!instance?.queuedDttm;
- const isOpen = openGroupIds.includes(task.label || "");
+ const isOpen = openGroupIds.includes(task.id || "");
// Calculate durations in ms
const taskDuration = getDuration(instance?.startDate, instance?.endDate);
diff --git a/airflow/www/static/js/dag/details/graph/utils.ts
b/airflow/www/static/js/dag/details/graph/utils.ts
index 3d85053619..ac940d46c7 100644
--- a/airflow/www/static/js/dag/details/graph/utils.ts
+++ b/airflow/www/static/js/dag/details/graph/utils.ts
@@ -79,9 +79,9 @@ export const flattenNodes = ({
onToggleCollapse: () => {
let newGroupIds = [];
if (!node.value.isOpen) {
- newGroupIds = [...openGroupIds, node.value.label];
+ newGroupIds = [...openGroupIds, node.id];
} else {
- newGroupIds = openGroupIds.filter((g) => g !== node.value.label);
+ newGroupIds = openGroupIds.filter((g) => g !== node.id);
}
onToggleGroups(newGroupIds);
},
diff --git a/airflow/www/static/js/dag/grid/TaskName.test.tsx
b/airflow/www/static/js/dag/grid/TaskName.test.tsx
index f7a4c7267d..8002ab78ca 100644
--- a/airflow/www/static/js/dag/grid/TaskName.test.tsx
+++ b/airflow/www/static/js/dag/grid/TaskName.test.tsx
@@ -29,7 +29,7 @@ import TaskName from "./TaskName";
describe("Test TaskName", () => {
test("Displays a normal task name", () => {
const { getByText } = render(
- <TaskName label="test" onToggle={() => {}} />,
+ <TaskName label="test" id="test" onToggle={() => {}} />,
{ wrapper: ChakraWrapper }
);
@@ -38,7 +38,13 @@ describe("Test TaskName", () => {
test("Displays a mapped task name", () => {
const { getByText } = render(
- <TaskName level={0} label="test" isMapped onToggle={() => {}} />,
+ <TaskName
+ level={0}
+ label="test"
+ id="test"
+ isMapped
+ onToggle={() => {}}
+ />,
{ wrapper: ChakraWrapper }
);
@@ -47,7 +53,7 @@ describe("Test TaskName", () => {
test("Displays a group task name", () => {
const { getByText, getByTestId } = render(
- <TaskName level={0} label="test" isGroup onToggle={() => {}} />,
+ <TaskName level={0} label="test" id="test" isGroup onToggle={() => {}}
/>,
{ wrapper: ChakraWrapper }
);
diff --git a/airflow/www/static/js/dag/grid/TaskName.tsx
b/airflow/www/static/js/dag/grid/TaskName.tsx
index 030767212f..ce2de10bf6 100644
--- a/airflow/www/static/js/dag/grid/TaskName.tsx
+++ b/airflow/www/static/js/dag/grid/TaskName.tsx
@@ -28,6 +28,7 @@ interface Props {
isOpen?: boolean;
level?: number;
label: string;
+ id: string;
}
const TaskName = ({
@@ -37,11 +38,13 @@ const TaskName = ({
isOpen = false,
level = 0,
label,
+ id,
}: Props) => (
<Flex
as={isGroup ? "button" : "div"}
onClick={onToggle}
aria-label={label}
+ data-testid={id}
title={label}
mr={4}
width="100%"
diff --git a/airflow/www/static/js/dag/grid/ToggleGroups.tsx
b/airflow/www/static/js/dag/grid/ToggleGroups.tsx
index 1473dd13aa..730e2e61a4 100644
--- a/airflow/www/static/js/dag/grid/ToggleGroups.tsx
+++ b/airflow/www/static/js/dag/grid/ToggleGroups.tsx
@@ -28,7 +28,7 @@ const getGroupIds = (groups: Task[]) => {
const checkTasks = (tasks: Task[]) =>
tasks.forEach((task) => {
if (task.children) {
- groupIds.push(task.label!);
+ groupIds.push(task.id!);
checkTasks(task.children);
}
});
diff --git a/airflow/www/static/js/dag/grid/index.test.tsx
b/airflow/www/static/js/dag/grid/index.test.tsx
index 66e1cafaeb..5cc6e71788 100644
--- a/airflow/www/static/js/dag/grid/index.test.tsx
+++ b/airflow/www/static/js/dag/grid/index.test.tsx
@@ -86,6 +86,55 @@ const mockGridData = {
},
],
},
+ {
+ id: "group_2",
+ label: "group_2",
+ instances: [
+ {
+ endDate: "2021-10-26T15:42:03.391939+00:00",
+ runId: "run1",
+ startDate: "2021-10-26T15:42:03.391917+00:00",
+ state: "success",
+ taskId: "group_1",
+ tryNumber: 1,
+ },
+ ],
+ children: [
+ {
+ id: "group_2.task_1",
+ operator: "DummyOperator",
+ label: "task_1",
+ instances: [
+ {
+ endDate: "2021-10-26T15:42:03.391939+00:00",
+ runId: "run1",
+ startDate: "2021-10-26T15:42:03.391917+00:00",
+ state: "success",
+ taskId: "group_1.task_1",
+ tryNumber: 1,
+ },
+ ],
+ children: [
+ {
+ id: "group_2.task_1.sub_task_1",
+ label: "sub_task_1",
+ extraLinks: [],
+ operator: "DummyOperator",
+ instances: [
+ {
+ endDate: "2021-10-26T15:42:03.391939+00:00",
+ runId: "run1",
+ startDate: "2021-10-26T15:42:03.391917+00:00",
+ state: "success",
+ taskId: "group_2.task_1.sub_task_1",
+ tryNumber: 1,
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
],
instances: [],
},
@@ -144,16 +193,18 @@ describe("Test ToggleGroups", () => {
});
test("Group defaults to closed", () => {
- const { getByTestId, getByText, getAllByTestId } = render(
+ const { queryAllByTestId, getByText, getAllByTestId } = render(
<Grid openGroupIds={[]} onToggleGroups={() => {}} />,
{ wrapper: Wrapper }
);
- const groupName = getByText("group_1");
+ const groupName1 = getByText("group_1");
+ const groupName2 = getByText("group_2");
- expect(getAllByTestId("task-instance")).toHaveLength(1);
- expect(groupName).toBeInTheDocument();
- expect(getByTestId("open-group")).toBeInTheDocument();
+ expect(getAllByTestId("task-instance")).toHaveLength(2);
+ expect(groupName1).toBeInTheDocument();
+ expect(groupName2).toBeInTheDocument();
+ expect(queryAllByTestId("open-group")).toHaveLength(2);
});
test("Buttons are disabled if all groups are expanded or collapsed", () => {
@@ -168,12 +219,12 @@ describe("Test ToggleGroups", () => {
expect(collapseButton).toBeDisabled();
const taskElements = queryAllByTestId("task-instance");
- expect(taskElements).toHaveLength(1);
+ expect(taskElements).toHaveLength(2);
fireEvent.click(expandButton);
const newTaskElements = queryAllByTestId("task-instance");
- expect(newTaskElements).toHaveLength(3);
+ expect(newTaskElements).toHaveLength(6);
expect(collapseButton).toBeEnabled();
expect(expandButton).toBeDisabled();
@@ -188,41 +239,71 @@ describe("Test ToggleGroups", () => {
const expandButton = getByTitle(EXPAND);
const collapseButton = getByTitle(COLLAPSE);
- const groupName = getByText("group_1");
+ const groupName1 = getByText("group_1");
+ const groupName2 = getByText("group_2");
- expect(queryAllByTestId("task-instance")).toHaveLength(3);
- expect(groupName).toBeInTheDocument();
+ expect(queryAllByTestId("task-instance")).toHaveLength(6);
+ expect(groupName1).toBeInTheDocument();
+ expect(groupName2).toBeInTheDocument();
- expect(queryAllByTestId("close-group")).toHaveLength(2);
+ expect(queryAllByTestId("close-group")).toHaveLength(4);
expect(queryAllByTestId("open-group")).toHaveLength(0);
fireEvent.click(collapseButton);
await waitFor(() =>
- expect(queryAllByTestId("task-instance")).toHaveLength(1)
+ expect(queryAllByTestId("task-instance")).toHaveLength(2)
);
expect(queryAllByTestId("close-group")).toHaveLength(0);
- // Since the groups are nested, only the parent row is rendered
- expect(queryAllByTestId("open-group")).toHaveLength(1);
+ // Since the groups are nested, only the parent rows is rendered
+ expect(queryAllByTestId("open-group")).toHaveLength(2);
+
+ fireEvent.click(expandButton);
+
+ await waitFor(() =>
+ expect(queryAllByTestId("task-instance")).toHaveLength(6)
+ );
+ expect(queryAllByTestId("close-group")).toHaveLength(4);
+ expect(queryAllByTestId("open-group")).toHaveLength(0);
+ });
+
+ test("Toggling a group does not affect groups with the same label", async ()
=> {
+ const { getByTitle, getByTestId, queryAllByTestId } = render(
+ <GridWithGroups />,
+ { wrapper: Wrapper }
+ );
+
+ const expandButton = getByTitle(EXPAND);
fireEvent.click(expandButton);
await waitFor(() =>
- expect(queryAllByTestId("task-instance")).toHaveLength(3)
+ expect(queryAllByTestId("task-instance")).toHaveLength(6)
);
- expect(queryAllByTestId("close-group")).toHaveLength(2);
+ expect(queryAllByTestId("close-group")).toHaveLength(4);
expect(queryAllByTestId("open-group")).toHaveLength(0);
+
+ const group2Task1 = getByTestId("group_2.task_1");
+
+ fireEvent.click(group2Task1);
+
+ // We only expect group_2.task_1 to be affected, not group_1.task_1
+ await waitFor(() =>
+ expect(queryAllByTestId("task-instance")).toHaveLength(5)
+ );
+ expect(queryAllByTestId("close-group")).toHaveLength(3);
+ expect(queryAllByTestId("open-group")).toHaveLength(1);
});
test("Hovered effect on task state", async () => {
- const openGroupIds = ["group_1", "task_1"];
+ const openGroupIds = ["group_1", "group_1.task_1"];
const { rerender, queryAllByTestId } = render(
<Grid openGroupIds={openGroupIds} onToggleGroups={() => {}} />,
{ wrapper: Wrapper }
);
const taskElements = queryAllByTestId("task-instance");
- expect(taskElements).toHaveLength(3);
+ expect(taskElements).toHaveLength(4);
taskElements.forEach((taskElement) => {
expect(taskElement).toHaveStyle("opacity: 1");
diff --git a/airflow/www/static/js/dag/grid/renderTaskRows.tsx
b/airflow/www/static/js/dag/grid/renderTaskRows.tsx
index c661eba677..181186b172 100644
--- a/airflow/www/static/js/dag/grid/renderTaskRows.tsx
+++ b/airflow/www/static/js/dag/grid/renderTaskRows.tsx
@@ -122,20 +122,20 @@ const Row = (props: RowProps) => {
const isGroup = !!task.children;
const isSelected = selected.taskId === task.id;
- const isOpen = openGroupIds.some((g) => g === task.label);
+ const isOpen = openGroupIds.some((g) => g === task.id);
// assure the function is the same across renders
const memoizedToggle = useCallback(() => {
- if (isGroup && task.label) {
+ if (isGroup && task.id) {
let newGroupIds = [];
if (!isOpen) {
- newGroupIds = [...openGroupIds, task.label];
+ newGroupIds = [...openGroupIds, task.id];
} else {
- newGroupIds = openGroupIds.filter((g) => g !== task.label);
+ newGroupIds = openGroupIds.filter((g) => g !== task.id);
}
onToggleGroups(newGroupIds);
}
- }, [isGroup, isOpen, task.label, openGroupIds, onToggleGroups]);
+ }, [isGroup, isOpen, task.id, openGroupIds, onToggleGroups]);
// check if the group's parents are all open, if not, return null
if (level !== openParentCount) return null;
@@ -168,6 +168,7 @@ const Row = (props: RowProps) => {
isGroup={isGroup}
isMapped={task.isMapped && !isParentMapped}
label={task.label || task.id || ""}
+ id={task.id || ""}
isOpen={isOpen}
level={level}
/>
diff --git a/airflow/www/static/js/utils/graph.ts
b/airflow/www/static/js/utils/graph.ts
index 08d5415833..1f77990930 100644
--- a/airflow/www/static/js/utils/graph.ts
+++ b/airflow/www/static/js/utils/graph.ts
@@ -117,7 +117,7 @@ const generateGraph = ({
height?: number;
} => {
const { id, value, children } = node;
- const isOpen = openGroupIds?.includes(value.label);
+ const isOpen = openGroupIds?.includes(id);
const childCount =
children?.filter((c: DepNode) => !c.id.includes("join_id")).length || 0;
const childIds = children?.length ? getNestedChildIds(children) : [];
@@ -170,7 +170,7 @@ const generateGraph = ({
sourceId: childIds.indexOf(e.sourceId) > -1 ? node.id : e.sourceId,
targetId: childIds.indexOf(e.targetId) > -1 ? node.id : e.targetId,
}));
- closedGroupIds.push(value.label);
+ closedGroupIds.push(id);
}
return {
id,