This is an automated email from the ASF dual-hosted git repository. kaxilnaik pushed a commit to branch v3-1-test in repository https://gitbox.apache.org/repos/asf/airflow.git
commit 6f222c1235c41c5cb1a57c37189dbe7d30f1e80a Author: LI,JHE-CHEN <[email protected]> AuthorDate: Wed Sep 17 10:49:49 2025 -0400 UI fixes for calendar (#55476) * fix: modify calendar UI for non-selected year cells * style: UI design roll back * feat(ui): enhance calendar tooltip with rich content and filter support * fix(i18n): Revert new translation key to use fallback * style: use semantic values for colors * feat(ui): implement Portal-based calendar tooltips with theme support (cherry picked from commit c76e3dd336109f82b8b2baa30aea18299f4903e6) --- .../src/airflow/ui/src/components/HoverTooltip.tsx | 63 ++++++++++ .../airflow/ui/src/pages/Dag/Calendar/Calendar.tsx | 10 +- .../ui/src/pages/Dag/Calendar/CalendarCell.tsx | 60 ++++++---- .../ui/src/pages/Dag/Calendar/CalendarLegend.tsx | 2 +- .../ui/src/pages/Dag/Calendar/CalendarTooltip.tsx | 132 ++++++++++++++++----- .../pages/Dag/Calendar/CalendarTooltipContent.tsx | 68 ----------- .../src/pages/Dag/Calendar/DailyCalendarView.tsx | 25 ++-- .../src/pages/Dag/Calendar/HourlyCalendarView.tsx | 26 +++- .../ui/src/pages/Dag/Calendar/calendarUtils.ts | 22 +--- .../ui/src/pages/Dag/Calendar/richTooltipUtils.ts | 50 -------- .../ui/src/pages/Dag/Calendar/useDelayedTooltip.ts | 60 ---------- 11 files changed, 251 insertions(+), 267 deletions(-) diff --git a/airflow-core/src/airflow/ui/src/components/HoverTooltip.tsx b/airflow-core/src/airflow/ui/src/components/HoverTooltip.tsx new file mode 100644 index 00000000000..46466858cae --- /dev/null +++ b/airflow-core/src/airflow/ui/src/components/HoverTooltip.tsx @@ -0,0 +1,63 @@ +/*! + * 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. + */ +import { Portal } from "@chakra-ui/react"; +import { useState, useRef, useCallback, cloneElement } from "react"; +import type { ReactElement, ReactNode, RefObject } from "react"; + +type Props = { + readonly children: ReactElement; + readonly delayMs?: number; + readonly tooltip: (triggerRef: RefObject<HTMLElement>) => ReactNode; +}; + +export const HoverTooltip = ({ children, delayMs = 200, tooltip }: Props) => { + const triggerRef = useRef<HTMLElement>(null); + const [isOpen, setIsOpen] = useState(false); + const timeoutRef = useRef<NodeJS.Timeout>(); + + const handleMouseEnter = useCallback(() => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + timeoutRef.current = setTimeout(() => { + setIsOpen(true); + }, delayMs); + }, [delayMs]); + + const handleMouseLeave = useCallback(() => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + timeoutRef.current = undefined; + } + setIsOpen(false); + }, []); + + const trigger = cloneElement(children, { + onMouseEnter: handleMouseEnter, + onMouseLeave: handleMouseLeave, + ref: triggerRef, + }); + + return ( + <> + {trigger} + {Boolean(isOpen) && <Portal>{tooltip(triggerRef)}</Portal>} + </> + ); +}; diff --git a/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/Calendar.tsx b/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/Calendar.tsx index 72678265165..634ff609e35 100644 --- a/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/Calendar.tsx +++ b/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/Calendar.tsx @@ -237,7 +237,7 @@ export const Calendar = () => { <Box animation={`${spin} 1s linear infinite`} border="3px solid" - borderColor={{ _dark: "border.emphasized", _light: "brand.100" }} + borderColor={{ _dark: "none.600", _light: "brand.100" }} borderRadius="50%" borderTopColor="brand.500" height="24px" @@ -248,7 +248,12 @@ export const Calendar = () => { ) : undefined} {granularity === "daily" ? ( <> - <DailyCalendarView data={data?.dag_runs ?? []} scale={scale} selectedYear={selectedDate.year()} /> + <DailyCalendarView + data={data?.dag_runs ?? []} + scale={scale} + selectedYear={selectedDate.year()} + viewMode={viewMode} + /> <CalendarLegend scale={scale} viewMode={viewMode} /> </> ) : ( @@ -259,6 +264,7 @@ export const Calendar = () => { scale={scale} selectedMonth={selectedDate.month()} selectedYear={selectedDate.year()} + viewMode={viewMode} /> </Box> <Box display="flex" flex="1" justifyContent="center" pt={16}> diff --git a/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/CalendarCell.tsx b/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/CalendarCell.tsx index fe06ea93776..da6973810ea 100644 --- a/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/CalendarCell.tsx +++ b/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/CalendarCell.tsx @@ -17,38 +17,56 @@ * under the License. */ import { Box } from "@chakra-ui/react"; +import type React from "react"; + +import { HoverTooltip } from "src/components/HoverTooltip"; import { CalendarTooltip } from "./CalendarTooltip"; -import { CalendarTooltipContent } from "./CalendarTooltipContent"; -import type { CalendarCellData } from "./types"; -import { useDelayedTooltip } from "./useDelayedTooltip"; +import type { CalendarCellData, CalendarColorMode } from "./types"; type Props = { readonly backgroundColor: Record<string, string> | string; - readonly cellData?: CalendarCellData; // For rich tooltip content + readonly cellData: CalendarCellData | undefined; readonly index?: number; readonly marginRight?: string; + readonly viewMode?: CalendarColorMode; }; -export const CalendarCell = ({ backgroundColor, cellData, index, marginRight }: Props) => { - const { handleMouseEnter, handleMouseLeave } = useDelayedTooltip(); +const renderTooltip = + (cellData: CalendarCellData | undefined, viewMode: CalendarColorMode) => + (triggerRef: React.RefObject<HTMLElement>) => ( + <CalendarTooltip cellData={cellData} triggerRef={triggerRef} viewMode={viewMode} /> + ); +export const CalendarCell = ({ + backgroundColor, + cellData, + index, + marginRight, + viewMode = "total", +}: Props) => { const computedMarginRight = marginRight ?? (index !== undefined && index % 7 === 6 ? "8px" : "0"); - return ( - <Box onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave} position="relative"> - <Box - _hover={{ transform: "scale(1.1)" }} - bg={backgroundColor} - border="1px solid" - borderColor="border.emphasized" - borderRadius="2px" - cursor="pointer" - height="14px" - marginRight={computedMarginRight} - width="14px" - /> - <CalendarTooltip content={cellData ? <CalendarTooltipContent cellData={cellData} /> : ""} /> - </Box> + const relevantCount = + viewMode === "failed" ? (cellData?.counts.failed ?? 0) : (cellData?.counts.total ?? 0); + const hasData = Boolean(cellData && relevantCount > 0); + const hasTooltip = Boolean(cellData); + + const cellBox = ( + <Box + _hover={hasData ? { transform: "scale(1.1)" } : {}} + bg={backgroundColor} + borderRadius="2px" + cursor={hasData ? "pointer" : "default"} + height="14px" + marginRight={computedMarginRight} + width="14px" + /> ); + + if (!hasTooltip) { + return cellBox; + } + + return <HoverTooltip tooltip={renderTooltip(cellData, viewMode)}>{cellBox}</HoverTooltip>; }; diff --git a/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/CalendarLegend.tsx b/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/CalendarLegend.tsx index afb963e6c96..86ed307f8d8 100644 --- a/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/CalendarLegend.tsx +++ b/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/CalendarLegend.tsx @@ -84,7 +84,7 @@ export const CalendarLegend = ({ scale, vertical = false, viewMode }: Props) => <HStack gap={4} justify="center" wrap="wrap"> <HStack gap={2}> <Box - bg={{ _dark: "scheduled.600", _light: "scheduled.200" }} + bg={{ _dark: "stone.600", _light: "stone.200" }} borderRadius="2px" boxShadow="sm" height="14px" diff --git a/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/CalendarTooltip.tsx b/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/CalendarTooltip.tsx index ac8a03679a4..d4c919436bd 100644 --- a/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/CalendarTooltip.tsx +++ b/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/CalendarTooltip.tsx @@ -16,49 +16,58 @@ * specific language governing permissions and limitations * under the License. */ +import { Box, HStack, Text, VStack } from "@chakra-ui/react"; import { useMemo } from "react"; -import type { ReactNode } from "react"; +import type { RefObject } from "react"; +import { useTranslation } from "react-i18next"; -const TOOLTIP_MIN_WIDTH = "200px"; -const TOOLTIP_MAX_WIDTH = "350px"; -const TOOLTIP_MAX_HEIGHT = "200px"; -const TOOLTIP_PADDING = "12px"; +import type { CalendarCellData, CalendarColorMode } from "./types"; + +const SQUARE_SIZE = "12px"; +const SQUARE_BORDER_RADIUS = "2px"; type Props = { - readonly content: ReactNode; + readonly cellData: CalendarCellData | undefined; + readonly triggerRef: RefObject<HTMLElement>; + readonly viewMode?: CalendarColorMode; }; -export const CalendarTooltip = ({ content }: Props) => { - const tooltipStyle = useMemo( - () => ({ - backgroundColor: "var(--chakra-colors-bg-muted)", - border: "1px solid var(--chakra-colors-border-emphasized)", +const stateColorMap = { + failed: "failed.solid", + planned: "stone.solid", + running: "running.solid", + success: "success.solid", +}; + +export const CalendarTooltip = ({ cellData, triggerRef, viewMode = "total" }: Props) => { + const { t: translate } = useTranslation(["dag", "common"]); + + const tooltipStyle = useMemo(() => { + if (!triggerRef.current) { + return { display: "none" }; + } + + const rect = triggerRef.current.getBoundingClientRect(); + + return { + backgroundColor: "var(--chakra-colors-bg-inverted)", borderRadius: "4px", - color: "fg", + color: "var(--chakra-colors-fg-inverted)", fontSize: "14px", - left: "50%", - maxHeight: TOOLTIP_MAX_HEIGHT, - maxWidth: TOOLTIP_MAX_WIDTH, - minWidth: TOOLTIP_MIN_WIDTH, - opacity: 0, - overflowY: "auto" as const, - padding: TOOLTIP_PADDING, - pointerEvents: "none" as const, + left: `${rect.left + globalThis.scrollX + rect.width / 2}px`, + minWidth: "200px", + padding: "8px", position: "absolute" as const, - top: "22px", + top: `${rect.bottom + globalThis.scrollY + 8}px`, transform: "translateX(-50%)", - transition: "opacity 0.2s, visibility 0.2s", - visibility: "hidden" as const, - whiteSpace: "normal" as const, - width: "auto", + whiteSpace: "nowrap" as const, zIndex: 1000, - }), - [], - ); + }; + }, [triggerRef]); const arrowStyle = useMemo( () => ({ - borderBottom: "4px solid var(--chakra-colors-bg-muted)", + borderBottom: "4px solid var(--chakra-colors-bg-inverted)", borderLeft: "4px solid transparent", borderRight: "4px solid transparent", content: '""', @@ -72,10 +81,71 @@ export const CalendarTooltip = ({ content }: Props) => { [], ); + if (!cellData) { + return undefined; + } + + const { counts, date } = cellData; + + const relevantCount = viewMode === "failed" ? counts.failed : counts.total; + const hasRuns = relevantCount > 0; + + // In failed mode, only show failed runs; in total mode, show all non-zero states + const states = Object.entries(counts) + .filter(([key, value]) => { + if (key === "total") { + return false; + } + if (value === 0) { + return false; + } + if (viewMode === "failed") { + return key === "failed"; + } + + return true; + }) + .map(([state, count]) => ({ + color: stateColorMap[state as keyof typeof stateColorMap] || "gray.500", + count, + state: translate(`common:states.${state}`), + })); + return ( - <div data-tooltip style={tooltipStyle}> + <div style={tooltipStyle}> <div style={arrowStyle} /> - {content} + {hasRuns ? ( + <VStack align="start" gap={2}> + <Text fontSize="sm" fontWeight="medium"> + {date} + </Text> + <VStack align="start" gap={1.5}> + {states.map(({ color, count, state }) => ( + <HStack gap={3} key={state}> + <Box + bg={color} + border="1px solid" + borderColor="border.emphasized" + borderRadius={SQUARE_BORDER_RADIUS} + height={SQUARE_SIZE} + width={SQUARE_SIZE} + /> + <Text fontSize="xs"> + {count} {state} + </Text> + </HStack> + ))} + </VStack> + </VStack> + ) : ( + <Text fontSize="sm"> + {/* To do: remove fallback translations */} + {date}:{" "} + {viewMode === "failed" + ? translate("calendar.noFailedRuns", "No failed runs") + : translate("calendar.noRuns", "No runs")} + </Text> + )} </div> ); }; diff --git a/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/CalendarTooltipContent.tsx b/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/CalendarTooltipContent.tsx deleted file mode 100644 index ebc483c40a4..00000000000 --- a/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/CalendarTooltipContent.tsx +++ /dev/null @@ -1,68 +0,0 @@ -/*! - * 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. - */ -import { Box, HStack, Text, VStack } from "@chakra-ui/react"; -import { useTranslation } from "react-i18next"; - -import { createRichTooltipContent } from "./richTooltipUtils"; -import type { CalendarCellData } from "./types"; - -const SQUARE_SIZE = "12px"; -const SQUARE_BORDER_RADIUS = "2px"; - -type Props = { - readonly cellData: CalendarCellData; -}; - -export const CalendarTooltipContent = ({ cellData }: Props) => { - const { t: translate } = useTranslation("dag"); - const { date, hasRuns, states, total } = createRichTooltipContent(cellData); - - if (!hasRuns) { - return ( - <Text fontSize="sm"> - {date}: {translate("calendar.noRuns")} - </Text> - ); - } - - return ( - <VStack align="start" gap={2}> - <Text fontSize="sm" fontWeight="medium"> - {date}: {total} {translate("calendar.runs")} - </Text> - <VStack align="start" gap={1.5}> - {states.map(({ color, count, state }) => ( - <HStack gap={3} key={state}> - <Box - bg={color} - border="1px solid" - borderColor="border.emphasized" - borderRadius={SQUARE_BORDER_RADIUS} - height={SQUARE_SIZE} - width={SQUARE_SIZE} - /> - <Text fontSize="xs"> - {count} {state} - </Text> - </HStack> - ))} - </VStack> - </VStack> - ); -}; diff --git a/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/DailyCalendarView.tsx b/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/DailyCalendarView.tsx index c67b91a4726..c34a22c8b78 100644 --- a/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/DailyCalendarView.tsx +++ b/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/DailyCalendarView.tsx @@ -43,15 +43,16 @@ import type { CalendarTimeRangeResponse } from "openapi/requests/types.gen"; import { CalendarCell } from "./CalendarCell"; import { generateDailyCalendarData } from "./calendarUtils"; -import type { CalendarScale } from "./types"; +import type { CalendarScale, CalendarColorMode } from "./types"; type Props = { readonly data: Array<CalendarTimeRangeResponse>; readonly scale: CalendarScale; readonly selectedYear: number; + readonly viewMode?: CalendarColorMode; }; -export const DailyCalendarView = ({ data, scale, selectedYear }: Props) => { +export const DailyCalendarView = ({ data, scale, selectedYear, viewMode = "total" }: Props) => { const { t: translate } = useTranslation("dag"); const dailyData = generateDailyCalendarData(data, selectedYear); @@ -107,19 +108,23 @@ export const DailyCalendarView = ({ data, scale, selectedYear }: Props) => { const isInSelectedYear = dayDate.year() === selectedYear; if (!isInSelectedYear) { - const emptyCellData = { - counts: { failed: 0, planned: 0, queued: 0, running: 0, success: 0, total: 0 }, - date: day.date, - runs: [], - }; - return ( - <CalendarCell backgroundColor="transparent" cellData={emptyCellData} key={day.date} /> + <CalendarCell + backgroundColor="transparent" + cellData={undefined} + key={day.date} + viewMode={viewMode} + /> ); } return ( - <CalendarCell backgroundColor={scale.getColor(day.counts)} cellData={day} key={day.date} /> + <CalendarCell + backgroundColor={scale.getColor(day.counts)} + cellData={day} + key={day.date} + viewMode={viewMode} + /> ); })} </Box> diff --git a/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/HourlyCalendarView.tsx b/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/HourlyCalendarView.tsx index 38bb50c50fc..0979f5efcb6 100644 --- a/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/HourlyCalendarView.tsx +++ b/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/HourlyCalendarView.tsx @@ -44,7 +44,7 @@ import type { CalendarTimeRangeResponse } from "openapi/requests/types.gen"; import { CalendarCell } from "./CalendarCell"; import { generateHourlyCalendarData } from "./calendarUtils"; -import type { CalendarScale } from "./types"; +import type { CalendarScale, CalendarColorMode } from "./types"; dayjs.extend(isSameOrBefore); @@ -53,9 +53,16 @@ type Props = { readonly scale: CalendarScale; readonly selectedMonth: number; readonly selectedYear: number; + readonly viewMode?: CalendarColorMode; }; -export const HourlyCalendarView = ({ data, scale, selectedMonth, selectedYear }: Props) => { +export const HourlyCalendarView = ({ + data, + scale, + selectedMonth, + selectedYear, + viewMode = "total", +}: Props) => { const { t: translate } = useTranslation("dag"); const hourlyData = generateHourlyCalendarData(data, selectedYear, selectedMonth); @@ -159,28 +166,35 @@ export const HourlyCalendarView = ({ data, scale, selectedMonth, selectedYear }: if (!hourData) { const emptyCounts = { failed: 0, planned: 0, queued: 0, running: 0, success: 0, total: 0 }; - const emptyCellData = { + const emptyData = { counts: emptyCounts, - date: `${day.day}T${hour.toString().padStart(2, "0")}:00:00`, + date: `${dayjs(day.day).format("MMM DD")}, ${hour.toString().padStart(2, "0")}:00`, runs: [], }; return ( <CalendarCell backgroundColor={scale.getColor(emptyCounts)} - cellData={emptyCellData} + cellData={emptyData} index={index} key={`${day.day}-${hour}`} + viewMode={viewMode} /> ); } + const formattedHourData = { + ...hourData, + date: `${dayjs(day.day).format("MMM DD")}, ${hour.toString().padStart(2, "0")}:00`, + }; + return ( <CalendarCell backgroundColor={scale.getColor(hourData.counts)} - cellData={hourData} + cellData={formattedHourData} index={index} key={`${day.day}-${hour}`} + viewMode={viewMode} /> ); })} diff --git a/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/calendarUtils.ts b/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/calendarUtils.ts index 7f145f41c22..4ea24f89f81 100644 --- a/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/calendarUtils.ts +++ b/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/calendarUtils.ts @@ -25,7 +25,6 @@ import type { RunCounts, DailyCalendarData, HourlyCalendarData, - CalendarCellData, CalendarColorMode, CalendarGranularity, CalendarScale, @@ -36,7 +35,7 @@ dayjs.extend(isSameOrBefore); // Calendar color constants const EMPTY_COLOR = { _dark: "gray.700", _light: "gray.100" }; -const PLANNED_COLOR = { _dark: "scheduled.600", _light: "scheduled.200" }; +const PLANNED_COLOR = { _dark: "stone.600", _light: "stone.200" }; const TOTAL_COLOR_INTENSITIES = [ EMPTY_COLOR, // 0 @@ -220,7 +219,8 @@ export const createCalendarScale = ( // Handle single value case if (minCount === maxCount) { - const singleColor = viewMode === "total" ? TOTAL_COLOR_INTENSITIES[2]! : FAILURE_COLOR_INTENSITIES[2]!; + const singleColor = + (viewMode === "total" ? TOTAL_COLOR_INTENSITIES[2] : FAILURE_COLOR_INTENSITIES[2]) ?? EMPTY_COLOR; return { getColor: (counts: RunCounts) => { @@ -294,7 +294,7 @@ export const createCalendarScale = ( label = `${threshold}-${nextThreshold - 1}`; } - const color = colorScheme[Math.min(index, colorScheme.length - 1)]!; + const color = colorScheme[Math.min(index, colorScheme.length - 1)] ?? EMPTY_COLOR; legendItems.push({ color, @@ -308,17 +308,3 @@ export const createCalendarScale = ( type: "gradient", }; }; - -export const createTooltipContent = (cellData: CalendarCellData): string => { - const { counts, date } = cellData; - - if (counts.total === 0) { - return `${date}: No runs`; - } - - const parts = Object.entries(counts) - .filter(([key, value]) => key !== "total" && value > 0) - .map(([state, count]) => `${count} ${state}`); - - return `${date}: ${counts.total} runs (${parts.join(", ")})`; -}; diff --git a/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/richTooltipUtils.ts b/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/richTooltipUtils.ts deleted file mode 100644 index 717ee73e03c..00000000000 --- a/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/richTooltipUtils.ts +++ /dev/null @@ -1,50 +0,0 @@ -/*! - * 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. - */ -import dayjs from "dayjs"; - -import type { CalendarCellData } from "./types"; - -export const createRichTooltipContent = (cellData: CalendarCellData) => { - const { counts, date } = cellData; - const hasRuns = counts.total > 0; - - if (!hasRuns) { - return { - date: dayjs(date).format("MMM DD, YYYY"), - hasRuns: false, - states: [], - total: 0, - }; - } - - const states = Object.entries(counts) - .filter(([key, value]) => key !== "total" && value > 0) - .map(([state, count]) => ({ - color: `var(--chakra-colors-${state}-solid)`, - count, - state, - })); - - return { - date: dayjs(date).format("MMM DD, YYYY"), - hasRuns: true, - states, - total: counts.total, - }; -}; diff --git a/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/useDelayedTooltip.ts b/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/useDelayedTooltip.ts deleted file mode 100644 index b2e0d17c0ea..00000000000 --- a/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/useDelayedTooltip.ts +++ /dev/null @@ -1,60 +0,0 @@ -/*! - * 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. - */ -import { useRef } from "react"; - -export const useDelayedTooltip = (delayMs: number = 200) => { - const debounceTimeoutRef = useRef<NodeJS.Timeout | undefined>(undefined); - const activeTooltipRef = useRef<HTMLElement | undefined>(undefined); - - const handleMouseEnter = (event: React.MouseEvent<HTMLDivElement>) => { - if (debounceTimeoutRef.current) { - clearTimeout(debounceTimeoutRef.current); - } - - const tooltipElement = event.currentTarget.querySelector("[data-tooltip]"); - - if (tooltipElement) { - activeTooltipRef.current = tooltipElement as HTMLElement; - debounceTimeoutRef.current = setTimeout(() => { - if (activeTooltipRef.current) { - activeTooltipRef.current.style.opacity = "1"; - activeTooltipRef.current.style.visibility = "visible"; - } - }, delayMs); - } - }; - - const handleMouseLeave = () => { - if (debounceTimeoutRef.current) { - clearTimeout(debounceTimeoutRef.current); - debounceTimeoutRef.current = undefined; - } - - if (activeTooltipRef.current) { - activeTooltipRef.current.style.opacity = "0"; - activeTooltipRef.current.style.visibility = "hidden"; - activeTooltipRef.current = undefined; - } - }; - - return { - handleMouseEnter, - handleMouseLeave, - }; -};
