Skip to content

Commit 02160cd

Browse files
github-actions[bot]RoyLee1224
authored andcommitted
[v3-1-test] Fix task instance and runs tooltips in Grid view (#58359) (#59013)
* refactor: replace hovertooltip with basictooltip * feat: implement basic tooltip for grid TI * feat: implement basic tooltip for grid run * fix: use get duration util * refactor: simplify BasicTooltip and add auto-flip for viewport overflow * fix(i18n): add translation for tooltip states * refactor: improve BasicTooltip positioning and remove duration * fix: use Chakra semantic tokens * refactor: use dynamic height for BasicTooltip positioning (cherry picked from commit 4970ea2) Co-authored-by: LI,JHE-CHEN <103923510+RoyLee1224@users.noreply.github.com>
1 parent 34e0e9e commit 02160cd

8 files changed

Lines changed: 241 additions & 206 deletions

File tree

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
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+
import { Box, Portal } from "@chakra-ui/react";
20+
import type { ReactElement, ReactNode } from "react";
21+
import { cloneElement, useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
22+
23+
type Props = {
24+
readonly children: ReactElement;
25+
readonly content: ReactNode;
26+
};
27+
28+
const offset = 8;
29+
30+
export const BasicTooltip = ({ children, content }: Props): ReactElement => {
31+
const triggerRef = useRef<HTMLElement>(null);
32+
const tooltipRef = useRef<HTMLDivElement>(null);
33+
const [isOpen, setIsOpen] = useState(false);
34+
const [showOnTop, setShowOnTop] = useState(false);
35+
const timeoutRef = useRef<NodeJS.Timeout>();
36+
37+
const handleMouseEnter = useCallback(() => {
38+
if (timeoutRef.current) {
39+
clearTimeout(timeoutRef.current);
40+
}
41+
timeoutRef.current = setTimeout(() => {
42+
setIsOpen(true);
43+
}, 500);
44+
}, []);
45+
46+
const handleMouseLeave = useCallback(() => {
47+
if (timeoutRef.current) {
48+
clearTimeout(timeoutRef.current);
49+
timeoutRef.current = undefined;
50+
}
51+
setIsOpen(false);
52+
}, []);
53+
54+
// Calculate position based on actual tooltip height before paint
55+
useLayoutEffect(() => {
56+
if (isOpen && triggerRef.current && tooltipRef.current) {
57+
const triggerRect = triggerRef.current.getBoundingClientRect();
58+
const tooltipHeight = tooltipRef.current.clientHeight;
59+
const wouldOverflow = triggerRect.bottom + offset + tooltipHeight > globalThis.innerHeight;
60+
61+
setShowOnTop(wouldOverflow);
62+
}
63+
}, [isOpen]);
64+
65+
// Cleanup on unmount
66+
useEffect(
67+
() => () => {
68+
if (timeoutRef.current) {
69+
clearTimeout(timeoutRef.current);
70+
}
71+
},
72+
[],
73+
);
74+
75+
// Clone children and attach event handlers + ref
76+
const trigger = cloneElement(children, {
77+
onMouseEnter: handleMouseEnter,
78+
onMouseLeave: handleMouseLeave,
79+
ref: triggerRef,
80+
});
81+
82+
if (!isOpen || !triggerRef.current) {
83+
return trigger;
84+
}
85+
86+
const rect = triggerRef.current.getBoundingClientRect();
87+
const { scrollX, scrollY } = globalThis;
88+
89+
return (
90+
<>
91+
{trigger}
92+
<Portal>
93+
<Box
94+
bg="bg.inverted"
95+
borderRadius="md"
96+
boxShadow="md"
97+
color="fg.inverted"
98+
fontSize="sm"
99+
left={`${rect.left + scrollX + rect.width / 2}px`}
100+
paddingX="3"
101+
paddingY="2"
102+
pointerEvents="none"
103+
position="absolute"
104+
ref={tooltipRef}
105+
top={showOnTop ? `${rect.top + scrollY - offset}px` : `${rect.bottom + scrollY + offset}px`}
106+
transform={showOnTop ? "translate(-50%, -100%)" : "translateX(-50%)"}
107+
whiteSpace="nowrap"
108+
zIndex="popover"
109+
>
110+
<Box
111+
borderLeft="4px solid transparent"
112+
borderRight="4px solid transparent"
113+
height={0}
114+
left="50%"
115+
position="absolute"
116+
transform="translateX(-50%)"
117+
width={0}
118+
{...(showOnTop
119+
? { borderTop: "4px solid var(--chakra-colors-bg-inverted)", bottom: "-4px" }
120+
: { borderBottom: "4px solid var(--chakra-colors-bg-inverted)", top: "-4px" })}
121+
/>
122+
{content}
123+
</Box>
124+
</Portal>
125+
</>
126+
);
127+
};

airflow-core/src/airflow/ui/src/components/DagRunInfo.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ const DagRunInfo = ({ endDate, logicalDate, runAfter, startDate, state }: Props)
4242
<VStack align="left" gap={0}>
4343
{state === undefined ? undefined : (
4444
<Text>
45-
{translate("state")}: {state}
45+
{translate("state")}: {translate(`common:states.${state}`)}
4646
</Text>
4747
)}
4848
{Boolean(logicalDate) ? (

airflow-core/src/airflow/ui/src/components/HoverTooltip.tsx

Lines changed: 0 additions & 63 deletions
This file was deleted.

airflow-core/src/airflow/ui/src/components/TaskInstanceTooltip.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,10 @@ const TaskInstanceTooltip = ({ children, positioning, taskInstance, ...rest }: P
4343
content={
4444
<Box>
4545
<Text>
46-
{translate("state")}: {taskInstance.state}
46+
{translate("state")}:{" "}
47+
{taskInstance.state
48+
? translate(`common:states.${taskInstance.state}`)
49+
: translate("common:states.no_status")}
4750
</Text>
4851
{"dag_run_id" in taskInstance ? (
4952
<Text>

airflow-core/src/airflow/ui/src/layouts/Details/Grid/GridButton.tsx

Lines changed: 41 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,11 @@
1717
* under the License.
1818
*/
1919
import { Flex, type FlexProps } from "@chakra-ui/react";
20+
import { useTranslation } from "react-i18next";
2021
import { Link } from "react-router-dom";
2122

2223
import type { DagRunState, TaskInstanceState } from "openapi/requests/types.gen";
24+
import { BasicTooltip } from "src/components/BasicTooltip";
2325

2426
type Props = {
2527
readonly dagId: string;
@@ -41,39 +43,53 @@ export const GridButton = ({
4143
state,
4244
taskId,
4345
...rest
44-
}: Props) =>
45-
isGroup ? (
46-
<Flex
47-
background={`${state}.solid`}
48-
borderRadius={2}
49-
height="10px"
50-
minW="14px"
51-
pb="2px"
52-
px="2px"
53-
title={`${label}\n${state}`}
54-
{...rest}
55-
>
56-
{children}
57-
</Flex>
58-
) : (
59-
<Link
60-
replace
61-
to={{
62-
pathname: `/dags/${dagId}/runs/${runId}/${taskId === undefined ? "" : `tasks/${taskId}`}`,
63-
search: searchParams.toString(),
64-
}}
65-
>
46+
}: Props) => {
47+
const { t: translate } = useTranslation();
48+
49+
const tooltipContent = (
50+
<>
51+
{label}
52+
<br />
53+
{translate("state")}:{" "}
54+
{state ? translate(`common:states.${state}`) : translate("common:states.no_status")}
55+
</>
56+
);
57+
58+
return isGroup ? (
59+
<BasicTooltip content={tooltipContent}>
6660
<Flex
6761
background={`${state}.solid`}
6862
borderRadius={2}
6963
height="10px"
64+
minW="14px"
7065
pb="2px"
7166
px="2px"
72-
title={`${label}\n${state}`}
73-
width="14px"
7467
{...rest}
7568
>
7669
{children}
7770
</Flex>
78-
</Link>
71+
</BasicTooltip>
72+
) : (
73+
<BasicTooltip content={tooltipContent}>
74+
<Link
75+
replace
76+
to={{
77+
pathname: `/dags/${dagId}/runs/${runId}/${taskId === undefined ? "" : `tasks/${taskId}`}`,
78+
search: searchParams.toString(),
79+
}}
80+
>
81+
<Flex
82+
background={`${state}.solid`}
83+
borderRadius={2}
84+
height="10px"
85+
pb="2px"
86+
px="2px"
87+
width="14px"
88+
{...rest}
89+
>
90+
{children}
91+
</Flex>
92+
</Link>
93+
</BasicTooltip>
7994
);
95+
};

airflow-core/src/airflow/ui/src/layouts/Details/Grid/GridTI.tsx

Lines changed: 34 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,9 @@ import { useTranslation } from "react-i18next";
2323
import { Link, useLocation, useParams, useSearchParams } from "react-router-dom";
2424

2525
import type { LightGridTaskInstanceSummary } from "openapi/requests/types.gen";
26+
import { BasicTooltip } from "src/components/BasicTooltip";
2627
import { StateIcon } from "src/components/StateIcon";
2728
import Time from "src/components/Time";
28-
import { Tooltip } from "src/components/ui";
2929
import { type HoverContextType, useHover } from "src/context/hover";
3030
import { buildTaskInstanceUrl } from "src/utils/links";
3131

@@ -106,35 +106,38 @@ const Instance = ({ dagId, instance, isGroup, isMapped, onClick, runId, taskId }
106106
py={0}
107107
transition="background-color 0.2s"
108108
>
109-
<Link
110-
id={`grid-${runId}-${taskId}`}
111-
onClick={onClick}
112-
replace
113-
to={{
114-
pathname: getTaskUrl(),
115-
search: redirectionSearch,
116-
}}
109+
<BasicTooltip
110+
content={
111+
<>
112+
{translate("taskId")}: {taskId}
113+
<br />
114+
{translate("state")}:{" "}
115+
{instance.state
116+
? translate(`common:states.${instance.state}`)
117+
: translate("common:states.no_status")}
118+
{instance.min_start_date !== null && (
119+
<>
120+
<br />
121+
{translate("startDate")}: <Time datetime={instance.min_start_date} />
122+
</>
123+
)}
124+
{instance.max_end_date !== null && (
125+
<>
126+
<br />
127+
{translate("endDate")}: <Time datetime={instance.max_end_date} />
128+
</>
129+
)}
130+
</>
131+
}
117132
>
118-
<Tooltip
119-
content={
120-
<>
121-
{translate("taskId")}: {taskId}
122-
<br />
123-
{translate("state")}: {instance.state}
124-
{instance.min_start_date !== null && (
125-
<>
126-
<br />
127-
{translate("startDate")}: <Time datetime={instance.min_start_date} />
128-
</>
129-
)}
130-
{instance.max_end_date !== null && (
131-
<>
132-
<br />
133-
{translate("endDate")}: <Time datetime={instance.max_end_date} />
134-
</>
135-
)}
136-
</>
137-
}
133+
<Link
134+
id={`grid-${runId}-${taskId}`}
135+
onClick={onClick}
136+
replace
137+
to={{
138+
pathname: getTaskUrl(),
139+
search: redirectionSearch,
140+
}}
138141
>
139142
<Badge
140143
alignItems="center"
@@ -150,8 +153,8 @@ const Instance = ({ dagId, instance, isGroup, isMapped, onClick, runId, taskId }
150153
>
151154
<StateIcon size={10} state={instance.state} />
152155
</Badge>
153-
</Tooltip>
154-
</Link>
156+
</Link>
157+
</BasicTooltip>
155158
</Flex>
156159
);
157160
};

0 commit comments

Comments
 (0)