Skip to content

Commit b1c77cd

Browse files
committed
feature(ui): Make entire task box clickable to select the task in Airflow 3 graph view
1 parent 90dbd24 commit b1c77cd

File tree

2 files changed

+111
-89
lines changed

2 files changed

+111
-89
lines changed

airflow-core/src/airflow/ui/src/components/Graph/TaskLink.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,15 @@ import { TaskName, type TaskNameProps } from "src/components/TaskName";
2323

2424
type Props = {
2525
readonly id: string;
26+
readonly isLinkDisabled?: boolean;
2627
} & TaskNameProps;
2728

28-
export const TaskLink = ({ id, isGroup, isMapped, ...rest }: Props) => {
29+
export const TaskLink = ({ id, isGroup, isLinkDisabled, isMapped, ...rest }: Props) => {
2930
const { dagId = "", runId, taskId } = useParams();
3031
const [searchParams] = useSearchParams();
3132

3233
// We don't have a task group details page to link to
33-
if (isGroup) {
34+
if (isGroup ?? isLinkDisabled) {
3435
return <TaskName isGroup={true} isMapped={isMapped} {...rest} />;
3536
}
3637

airflow-core/src/airflow/ui/src/components/Graph/TaskNode.tsx

Lines changed: 108 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import { Box, Button, Flex, HStack, Text } from "@chakra-ui/react";
2020
import type { NodeProps, Node as NodeType } from "@xyflow/react";
2121
import { CgRedo } from "react-icons/cg";
22+
import { useParams, useSearchParams, Link as RouterLink } from "react-router-dom";
2223

2324
import { StateBadge } from "src/components/StateBadge";
2425
import TaskInstanceTooltip from "src/components/TaskInstanceTooltip";
@@ -46,104 +47,124 @@ export const TaskNode = ({
4647
},
4748
id,
4849
}: NodeProps<NodeType<CustomNodeProps, "task">>) => {
50+
const { dagId = "", runId, taskId } = useParams();
51+
const [searchParams] = useSearchParams();
4952
const { toggleGroupId } = useOpenGroups();
5053
const onClick = () => {
5154
if (isGroup) {
5255
toggleGroupId(id);
5356
}
5457
};
5558

59+
const renderTaskContent = () => (
60+
<Flex alignItems="center" cursor="default" flexDirection="column">
61+
<TaskInstanceTooltip
62+
openDelay={500}
63+
positioning={{
64+
placement: "top-start",
65+
}}
66+
taskInstance={taskInstance}
67+
>
68+
<Flex
69+
// Alternate background color for nested open groups
70+
bg={isOpen && depth !== undefined && depth % 2 === 0 ? "bg.muted" : "bg"}
71+
borderColor={
72+
taskInstance?.state ? `${taskInstance.state}.solid` : isSelected ? "border.inverted" : "border"
73+
}
74+
borderRadius={5}
75+
borderWidth={isSelected ? 6 : 2}
76+
cursor={isGroup ? "default" : "pointer"}
77+
height={`${height + (isSelected ? 4 : 0)}px`}
78+
justifyContent="space-between"
79+
px={isSelected ? 1 : 2}
80+
py={isSelected ? 0 : 1}
81+
width={`${width + (isSelected ? 4 : 0)}px`}
82+
>
83+
<Box>
84+
<TaskLink
85+
childCount={taskInstance?.task_count}
86+
id={id}
87+
isGroup={isGroup}
88+
isLinkDisabled={true}
89+
isMapped={isMapped}
90+
isOpen={isOpen}
91+
label={label}
92+
setupTeardownType={setupTeardownType}
93+
/>
94+
<Text color="fg.muted" fontSize="sm" textTransform="capitalize">
95+
{isGroup ? "Task Group" : operator}
96+
</Text>
97+
{taskInstance === undefined ? undefined : (
98+
<HStack>
99+
<StateBadge fontSize="xs" state={taskInstance.state}>
100+
{taskInstance.state}
101+
</StateBadge>
102+
{taskInstance.try_number > 1 ? <CgRedo /> : undefined}
103+
</HStack>
104+
)}
105+
</Box>
106+
<Box>
107+
{isGroup ? (
108+
<Button
109+
colorPalette="blue"
110+
cursor="pointer"
111+
height="inherit"
112+
onClick={onClick}
113+
pb={2}
114+
pr={0}
115+
variant="plain"
116+
>
117+
{isOpen ? "- " : "+ "}
118+
{pluralize("task", childCount, undefined, false)}
119+
</Button>
120+
) : undefined}
121+
</Box>
122+
</Flex>
123+
</TaskInstanceTooltip>
124+
{Boolean(isMapped) || Boolean(isGroup && !isOpen) ? (
125+
<>
126+
<Box
127+
bg="bg.subtle"
128+
borderBottomLeftRadius={5}
129+
borderBottomRightRadius={5}
130+
borderBottomWidth={1}
131+
borderColor="fg"
132+
borderLeftWidth={1}
133+
borderRightWidth={1}
134+
height={1}
135+
width={`${width - 10}px`}
136+
/>
137+
<Box
138+
bg="bg.subtle"
139+
borderBottomLeftRadius={5}
140+
borderBottomRightRadius={5}
141+
borderBottomWidth={1}
142+
borderColor="fg"
143+
borderLeftWidth={1}
144+
borderRightWidth={1}
145+
height={1}
146+
width={`${width - 20}px`}
147+
/>
148+
</>
149+
) : undefined}
150+
</Flex>
151+
);
152+
56153
return (
57154
<NodeWrapper>
58-
<Flex alignItems="center" cursor="default" flexDirection="column">
59-
<TaskInstanceTooltip
60-
openDelay={500}
61-
positioning={{
62-
placement: "top-start",
155+
{isGroup ? (
156+
renderTaskContent()
157+
) : (
158+
<RouterLink
159+
style={{ cursor: "pointer", textDecoration: "none" }}
160+
to={{
161+
pathname: `/dags/${dagId}/${runId === undefined ? "" : `runs/${runId}/`}${taskId === id ? "" : `tasks/${id}`}${isMapped && taskId !== id && runId !== undefined ? "/mapped" : ""}`,
162+
search: searchParams.toString(),
63163
}}
64-
taskInstance={taskInstance}
65164
>
66-
<Flex
67-
// Alternate background color for nested open groups
68-
bg={isOpen && depth !== undefined && depth % 2 === 0 ? "bg.muted" : "bg"}
69-
borderColor={
70-
taskInstance?.state ? `${taskInstance.state}.solid` : isSelected ? "border.inverted" : "border"
71-
}
72-
borderRadius={5}
73-
borderWidth={isSelected ? 6 : 2}
74-
height={`${height + (isSelected ? 4 : 0)}px`}
75-
justifyContent="space-between"
76-
px={isSelected ? 1 : 2}
77-
py={isSelected ? 0 : 1}
78-
width={`${width + (isSelected ? 4 : 0)}px`}
79-
>
80-
<Box>
81-
<TaskLink
82-
childCount={taskInstance?.task_count}
83-
id={id}
84-
isGroup={isGroup}
85-
isMapped={isMapped}
86-
isOpen={isOpen}
87-
label={label}
88-
setupTeardownType={setupTeardownType}
89-
/>
90-
<Text color="fg.muted" fontSize="sm" textTransform="capitalize">
91-
{isGroup ? "Task Group" : operator}
92-
</Text>
93-
{taskInstance === undefined ? undefined : (
94-
<HStack>
95-
<StateBadge fontSize="xs" state={taskInstance.state}>
96-
{taskInstance.state}
97-
</StateBadge>
98-
{taskInstance.try_number > 1 ? <CgRedo /> : undefined}
99-
</HStack>
100-
)}
101-
</Box>
102-
<Box>
103-
{isGroup ? (
104-
<Button
105-
colorPalette="blue"
106-
cursor="pointer"
107-
height="inherit"
108-
onClick={onClick}
109-
pb={2}
110-
pr={0}
111-
variant="plain"
112-
>
113-
{isOpen ? "- " : "+ "}
114-
{pluralize("task", childCount, undefined, false)}
115-
</Button>
116-
) : undefined}
117-
</Box>
118-
</Flex>
119-
</TaskInstanceTooltip>
120-
{Boolean(isMapped) || Boolean(isGroup && !isOpen) ? (
121-
<>
122-
<Box
123-
bg="bg.subtle"
124-
borderBottomLeftRadius={5}
125-
borderBottomRightRadius={5}
126-
borderBottomWidth={1}
127-
borderColor="fg"
128-
borderLeftWidth={1}
129-
borderRightWidth={1}
130-
height={1}
131-
width={`${width - 10}px`}
132-
/>
133-
<Box
134-
bg="bg.subtle"
135-
borderBottomLeftRadius={5}
136-
borderBottomRightRadius={5}
137-
borderBottomWidth={1}
138-
borderColor="fg"
139-
borderLeftWidth={1}
140-
borderRightWidth={1}
141-
height={1}
142-
width={`${width - 20}px`}
143-
/>
144-
</>
145-
) : undefined}
146-
</Flex>
165+
{renderTaskContent()}
166+
</RouterLink>
167+
)}
147168
</NodeWrapper>
148169
);
149170
};

0 commit comments

Comments
 (0)