This is an automated email from the ASF dual-hosted git repository.
potiuk 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 a0e0ae1d068 Reduce DAG card tooltip controller overhead (#69681)
a0e0ae1d068 is described below
commit a0e0ae1d0685466c70c259fee889dff097ec1a63
Author: Shivam Rastogi <[email protected]>
AuthorDate: Sun Jul 12 09:09:53 2026 -0700
Reduce DAG card tooltip controller overhead (#69681)
---
.../airflow/ui/src/pages/DagsList/DagCard.test.tsx | 32 ++++-
.../airflow/ui/src/pages/DagsList/RecentRuns.tsx | 135 ++++++++++++---------
2 files changed, 109 insertions(+), 58 deletions(-)
diff --git a/airflow-core/src/airflow/ui/src/pages/DagsList/DagCard.test.tsx
b/airflow-core/src/airflow/ui/src/pages/DagsList/DagCard.test.tsx
index ad163ae19f1..95a35080aed 100644
--- a/airflow-core/src/airflow/ui/src/pages/DagsList/DagCard.test.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/DagsList/DagCard.test.tsx
@@ -17,7 +17,7 @@
* under the License.
*/
import "@testing-library/jest-dom/vitest";
-import { render, screen } from "@testing-library/react";
+import { act, fireEvent, render, screen } from "@testing-library/react";
import i18n from "i18next";
import type { DagTagResponse, DAGWithLatestDagRunsResponse } from
"openapi-gen/requests/types.gen";
import type { PropsWithChildren } from "react";
@@ -209,6 +209,36 @@ describe("DagCard", () => {
expect(latestRunElement).toHaveTextContent("2025-09-19 19:22:00");
});
+ it("DagCard should share one tooltip controller across recent runs", async
() => {
+ vi.useFakeTimers();
+ renderCard(mockDag);
+
+ const recentRuns = screen.getAllByTestId("recent-run");
+ const tooltipOwners = new Set(recentRuns.map((run) =>
run.getAttribute("data-ownedby")));
+ const secondRecentRun = recentRuns.at(1);
+
+ try {
+ expect(recentRuns).toHaveLength(mockDag.latest_dag_runs.length);
+ expect(tooltipOwners.size).toBe(1);
+ expect(tooltipOwners.has(null)).toBe(false);
+ expect(secondRecentRun).toBeDefined();
+
+ if (secondRecentRun === undefined) {
+ throw new Error("Expected at least two recent runs");
+ }
+
+ await act(async () => {
+ fireEvent.focus(secondRecentRun);
+ fireEvent.pointerEnter(secondRecentRun);
+ await vi.advanceTimersByTimeAsync(500);
+ });
+
+ expect(screen.getByRole("tooltip")).toHaveTextContent("2025-09-19
19:21:00");
+ } finally {
+ vi.useRealTimers();
+ }
+ });
+
it("DagCard should render next run section with timestamp", () => {
renderCard(mockDag);
const nextRunElement = screen.getByTestId("next-run");
diff --git a/airflow-core/src/airflow/ui/src/pages/DagsList/RecentRuns.tsx
b/airflow-core/src/airflow/ui/src/pages/DagsList/RecentRuns.tsx
index 1bcd6eb663b..f327006f2d3 100644
--- a/airflow-core/src/airflow/ui/src/pages/DagsList/RecentRuns.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/DagsList/RecentRuns.tsx
@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
-import { Flex, Box, Text } from "@chakra-ui/react";
+import { Flex, Box, Portal, Text, Tooltip } from "@chakra-ui/react";
import dayjs from "dayjs";
import duration from "dayjs/plugin/duration";
import { useTranslation } from "react-i18next";
@@ -25,20 +25,47 @@ import { Link } from "react-router-dom";
import type { DAGWithLatestDagRunsResponse } from "openapi/requests/types.gen";
import { StateIcon } from "src/components/StateIcon";
import Time from "src/components/Time";
-import { Tooltip } from "src/components/ui";
import { renderDuration } from "src/utils";
dayjs.extend(duration);
const BAR_HEIGHT = 65;
+type LatestRun = DAGWithLatestDagRunsResponse["latest_dag_runs"][number];
+
+const RecentRunTooltipContent = ({ run }: { readonly run: LatestRun }) => {
+ const { t: translate } = useTranslation();
+
+ return (
+ <Box>
+ <Text>
+ {translate("state")}: {translate(`common:states.${run.state}`)}
+ </Text>
+ <Text>
+ {translate("dagRun.runAfter")}: <Time datetime={run.run_after} />
+ </Text>
+ {run.start_date === null ? undefined : (
+ <Text>
+ {translate("startDate")}: <Time datetime={run.start_date} />
+ </Text>
+ )}
+ {run.end_date === null ? undefined : (
+ <Text>
+ {translate("endDate")}: <Time datetime={run.end_date} />
+ </Text>
+ )}
+ <Text>
+ {translate("duration")}: {renderDuration(run.duration)}
+ </Text>
+ </Box>
+ );
+};
+
export const RecentRuns = ({
latestRuns,
}: {
readonly latestRuns: DAGWithLatestDagRunsResponse["latest_dag_runs"];
}) => {
- const { t: translate } = useTranslation();
-
if (!latestRuns.length) {
return undefined;
}
@@ -49,58 +76,52 @@ export const RecentRuns = ({
);
return (
- <Flex alignItems="flex-end" flexDirection="row-reverse" gap={[0.5, 0.5,
0.5, 1]} pb={1}>
- {latestRuns.map((run) => (
- <Tooltip
- content={
- <Box>
- <Text>
- {translate("state")}: {translate(`common:states.${run.state}`)}
- </Text>
- <Text>
- {translate("dagRun.runAfter")}: <Time datetime={run.run_after}
/>
- </Text>
- {run.start_date === null ? undefined : (
- <Text>
- {translate("startDate")}: <Time datetime={run.start_date} />
- </Text>
- )}
- {run.end_date === null ? undefined : (
- <Text>
- {translate("endDate")}: <Time datetime={run.end_date} />
- </Text>
- )}
- <Text>
- {translate("duration")}: {renderDuration(run.duration)}
- </Text>
- </Box>
- }
- key={run.run_id}
- positioning={{
- offset: {
- crossAxis: 5,
- mainAxis: 5,
- },
- placement: "bottom-start",
- }}
- >
- <Link to={`/dags/${run.dag_id}/runs/${run.run_id}/`}>
- <Flex
- alignItems="center"
- bg={`${run.state}.solid`}
- borderRadius="4px"
- flexDir="column"
- fontSize="12px"
- height={`${run.duration === null ? 1 : (run.duration / max) *
BAR_HEIGHT}px`}
- justifyContent="flex-end"
- minHeight="12px"
- width="12px"
- >
- <StateIcon color="white" state={run.state} />
- </Flex>
- </Link>
- </Tooltip>
- ))}
- </Flex>
+ <Tooltip.Root
+ positioning={{
+ offset: {
+ crossAxis: 5,
+ mainAxis: 5,
+ },
+ placement: "bottom-start",
+ }}
+ >
+ <Flex alignItems="flex-end" flexDirection="row-reverse" gap={[0.5, 0.5,
0.5, 1]} pb={1}>
+ {latestRuns.map((run) => (
+ <Tooltip.Trigger asChild key={run.run_id} value={run.run_id}>
+ <Link data-testid="recent-run"
to={`/dags/${run.dag_id}/runs/${run.run_id}/`}>
+ <Flex
+ alignItems="center"
+ bg={`${run.state}.solid`}
+ borderRadius="4px"
+ flexDir="column"
+ fontSize="12px"
+ height={`${run.duration === null ? 1 : (run.duration / max) *
BAR_HEIGHT}px`}
+ justifyContent="flex-end"
+ minHeight="12px"
+ width="12px"
+ >
+ <StateIcon color="white" state={run.state} />
+ </Flex>
+ </Link>
+ </Tooltip.Trigger>
+ ))}
+ </Flex>
+ <Portal disabled>
+ <Tooltip.Positioner>
+ <Tooltip.Content>
+ <Tooltip.Arrow>
+ <Tooltip.ArrowTip />
+ </Tooltip.Arrow>
+ <Tooltip.Context>
+ {({ triggerValue }) => {
+ const run = latestRuns.find(({ run_id: runId }) => runId ===
triggerValue);
+
+ return run === undefined ? undefined :
<RecentRunTooltipContent run={run} />;
+ }}
+ </Tooltip.Context>
+ </Tooltip.Content>
+ </Tooltip.Positioner>
+ </Portal>
+ </Tooltip.Root>
);
};