Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 28 additions & 4 deletions airflow-core/src/airflow/ui/src/pages/Dag/Calendar/Calendar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,11 @@ import { FiChevronLeft, FiChevronRight } from "react-icons/fi";
import { useParams } from "react-router-dom";
import { useLocalStorage } from "usehooks-ts";

import { useCalendarServiceGetCalendar, useDagServiceGetDagDetails } from "openapi/queries";
import {
useCalendarServiceGetCalendar,
useCalendarServiceGetCalendarDeadlines,
useDagServiceGetDagDetails,
} from "openapi/queries";
import { ErrorAlert } from "src/components/ErrorAlert";
import { IconButton } from "src/components/ui";
import { ButtonGroupToggle } from "src/components/ui/ButtonGroupToggle";
Expand All @@ -37,7 +41,7 @@ import { useTimezone } from "src/context/timezone";
import { CalendarLegend } from "./CalendarLegend";
import { DailyCalendarView } from "./DailyCalendarView";
import { HourlyCalendarView } from "./HourlyCalendarView";
import { createCalendarScale } from "./calendarUtils";
import { buildDeadlineDateMap, createCalendarScale } from "./calendarUtils";

const spin = keyframes`
from { transform: rotate(0deg); }
Expand Down Expand Up @@ -84,6 +88,19 @@ export const Calendar = () => {
{ enabled: Boolean(dagId) },
);

const { data: deadlineData } = useCalendarServiceGetCalendarDeadlines(
{
dagId,
deadlineTimeGte: gte,
deadlineTimeLte: lte,
granularity,
},
undefined,
{ enabled: Boolean(dagId) },
);

const deadlineMap = buildDeadlineDateMap(deadlineData?.deadlines ?? [], selectedTimezone, granularity);

const scale = createCalendarScale(data?.dag_runs ?? [], {
granularity,
timezone: selectedTimezone,
Expand Down Expand Up @@ -238,18 +255,20 @@ export const Calendar = () => {
<DailyCalendarView
data={data?.dag_runs ?? []}
data-testid="calendar-daily-view"
deadlineMap={deadlineMap}
scale={scale}
selectedYear={selectedDate.year()}
timezone={selectedTimezone}
viewMode={viewMode}
/>
<CalendarLegend scale={scale} viewMode={viewMode} />
<CalendarLegend hasDeadlines={deadlineMap.size > 0} scale={scale} viewMode={viewMode} />
</>
) : (
<HStack align="start" gap={2}>
<Box>
<HourlyCalendarView
data={data?.dag_runs ?? []}
deadlineMap={deadlineMap}
scale={scale}
selectedMonth={selectedDate.month()}
selectedYear={selectedDate.year()}
Expand All @@ -258,7 +277,12 @@ export const Calendar = () => {
/>
</Box>
<Box display="flex" flex="1" justifyContent="center" pt={16}>
<CalendarLegend scale={scale} vertical viewMode={viewMode} />
<CalendarLegend
hasDeadlines={deadlineMap.size > 0}
scale={scale}
vertical
viewMode={viewMode}
/>
</Box>
</HStack>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
* under the License.
*/
import { Box } from "@chakra-ui/react";
import { FiAlertTriangle, FiClock } from "react-icons/fi";

import { BasicTooltip } from "src/components/BasicTooltip";

Expand Down Expand Up @@ -66,6 +67,30 @@ export const CalendarCell = ({
const isMixedState =
typeof backgroundColor === "object" && "secondary" in backgroundColor && "primary" in backgroundColor;

const { deadlineCounts } = cellData ?? {};
const hasMissedDeadline = (deadlineCounts?.missed ?? 0) > 0;
const hasPendingDeadline = (deadlineCounts?.pending ?? 0) > 0;
const hasDeadline = hasMissedDeadline || hasPendingDeadline;
const DeadlineIcon = hasMissedDeadline ? FiAlertTriangle : FiClock;

const deadlineIndicator = hasDeadline ? (
<Box
alignItems="center"
data-testid="deadline-indicator"
display="flex"
fontSize="10px"
height="100%"
justifyContent="center"
left="0"
lineHeight={1}
position="absolute"
top="0"
width="100%"
>
<DeadlineIcon />
</Box>
) : undefined;

const cellBox = isMixedState ? (
<Box
_hover={hasData ? { transform: "scale(1.1)" } : {}}
Expand Down Expand Up @@ -95,6 +120,7 @@ export const CalendarCell = ({
position="absolute"
width="100%"
/>
{deadlineIndicator}
</Box>
) : (
<Box
Expand All @@ -108,8 +134,11 @@ export const CalendarCell = ({
data-view-mode={viewMode}
height="14px"
marginRight={computedMarginRight}
position="relative"
width="14px"
/>
>
{deadlineIndicator}
</Box>
);

if (!hasTooltip) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,60 +18,23 @@
*/
import { Box, HStack, Text, VStack } from "@chakra-ui/react";
import { useTranslation } from "react-i18next";
import { FiAlertTriangle, FiClock } from "react-icons/fi";

import { Tooltip } from "src/components/ui";

import { LegendIcon } from "./LegendIcon";
import { PLANNED_COLOR } from "./calendarUtils";
import type { CalendarScale, CalendarColorMode } from "./types";

type Props = {
readonly hasDeadlines?: boolean;
readonly scale: CalendarScale;
readonly vertical?: boolean;
readonly viewMode: CalendarColorMode;
};

type LegendColorType =
| Record<string, string>
| string
| { primary: Record<string, string> | string; secondary: Record<string, string> | string };

const LegendIcon = ({ color, cursor }: { readonly color: LegendColorType; readonly cursor?: string }) => {
const isMixedState = typeof color === "object" && "primary" in color && "secondary" in color;

if (isMixedState) {
return (
<Box
borderRadius="2px"
boxShadow="sm"
cursor={cursor}
height="14px"
overflow="hidden"
position="relative"
width="14px"
>
<Box
bg={color.secondary}
clipPath="polygon(0 100%, 100% 100%, 0 0)"
height="100%"
position="absolute"
width="100%"
/>
<Box
bg={color.primary}
clipPath="polygon(100% 0, 100% 100%, 0 0)"
height="100%"
position="absolute"
width="100%"
/>
</Box>
);
}

return <Box bg={color} borderRadius="2px" boxShadow="sm" cursor={cursor} height="14px" width="14px" />;
};

export const CalendarLegend = ({ scale, vertical = false, viewMode }: Props) => {
const { t: translate } = useTranslation(["dag", "common"]);
export const CalendarLegend = ({ hasDeadlines = false, scale, vertical = false, viewMode }: Props) => {
const { t: translate } = useTranslation("dag");

const legendTitle =
viewMode === "failed" ? translate("overview.buttons.failedRun_other") : translate("calendar.totalRuns");
Expand Down Expand Up @@ -174,6 +137,32 @@ export const CalendarLegend = ({ scale, vertical = false, viewMode }: Props) =>
</HStack>
</HStack>
</Box>

{Boolean(hasDeadlines) && (
<Box mt={4}>
<Text color="fg.muted" fontSize="sm" fontWeight="medium" mb={3} textAlign="center">
{translate("overview.deadlines.title")}
</Text>
<HStack gap={4} justify="center" wrap="wrap">
<HStack gap={2}>
<Box fontSize="sm" lineHeight={1}>
<FiClock />
</Box>
<Text color="fg.muted" fontSize="xs">
{translate("deadlineStatus.upcoming")}
</Text>
</HStack>
<HStack gap={2}>
<Box fontSize="sm" lineHeight={1}>
<FiAlertTriangle />
</Box>
<Text color="fg.muted" fontSize="xs">
{translate("deadlineStatus.missed")}
</Text>
</HStack>
</HStack>
</Box>
)}
</Box>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
*/
import { Box, HStack, Text, VStack } from "@chakra-ui/react";
import { useTranslation } from "react-i18next";
import { FiAlertTriangle, FiClock } from "react-icons/fi";

import type { CalendarCellData, CalendarColorMode } from "./types";

Expand All @@ -44,11 +45,15 @@ export const CalendarTooltip = ({ cellData, viewMode = "total" }: Props) => {
return undefined;
}

const { counts, date } = cellData;
const { counts, date, deadlineCounts } = cellData;

const relevantCount = viewMode === "failed" ? counts.failed : counts.total;
const hasRuns = relevantCount > 0;

const missedDeadlines = deadlineCounts?.missed ?? 0;
const pendingDeadlines = deadlineCounts?.pending ?? 0;
const hasDeadlines = missedDeadlines > 0 || pendingDeadlines > 0;

// In failed mode, only show failed runs; in total mode, show all non-zero states
const states = Object.entries(counts)
.filter(([key, value]) => {
Expand All @@ -70,28 +75,57 @@ export const CalendarTooltip = ({ cellData, viewMode = "total" }: Props) => {
state: translate(`common:states.${state}`),
}));

return hasRuns ? (
return hasRuns || hasDeadlines ? (
<VStack align="start" data-testid="calendar-tooltip" data-view-mode={viewMode} gap={2}>
<Text fontSize="sm" fontWeight="medium">
{date}
</Text>
<VStack align="start" gap={1.5}>
{states.map(({ color, count, state }) => (
<HStack data-testid={`calendar-tooltip-state-${state.toLowerCase()}`} 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>
{hasRuns ? (
<VStack align="start" gap={1.5}>
{states.map(({ color, count, state }) => (
<HStack data-testid={`calendar-tooltip-state-${state.toLowerCase()}`} 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>
) : null}
{hasDeadlines ? (
<VStack align="start" gap={1.5}>
<Text color="fg.muted" fontSize="xs" fontWeight="medium">
{translate("dag:overview.deadlines.title")}
</Text>
{missedDeadlines > 0 && (
<HStack data-testid="calendar-tooltip-deadline-missed" gap={3}>
<Box fontSize="16px" lineHeight={1}>
<FiAlertTriangle />
</Box>
<Text fontSize="xs">
{translate("dag:deadlineStatus.missedCount", { count: missedDeadlines })}
</Text>
</HStack>
)}
{pendingDeadlines > 0 && (
<HStack data-testid="calendar-tooltip-deadline-pending" gap={3}>
<Box fontSize="16px" lineHeight={1}>
<FiClock />
</Box>
<Text fontSize="xs">
{translate("dag:deadlineStatus.upcomingCount", { count: pendingDeadlines })}
</Text>
</HStack>
)}
</VStack>
) : null}
</VStack>
) : (
<Text fontSize="sm">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,19 +43,27 @@ import type { CalendarTimeRangeResponse } from "openapi/requests/types.gen";

import { CalendarCell } from "./CalendarCell";
import { generateDailyCalendarData } from "./calendarUtils";
import type { CalendarScale, CalendarColorMode } from "./types";
import type { CalendarScale, CalendarColorMode, DeadlineCounts } from "./types";

type Props = {
readonly data: Array<CalendarTimeRangeResponse>;
readonly deadlineMap?: Map<string, DeadlineCounts>;
readonly scale: CalendarScale;
readonly selectedYear: number;
readonly timezone: string;
readonly viewMode?: CalendarColorMode;
};

export const DailyCalendarView = ({ data, scale, selectedYear, timezone, viewMode = "total" }: Props) => {
export const DailyCalendarView = ({
data,
deadlineMap,
scale,
selectedYear,
timezone,
viewMode = "total",
}: Props) => {
const { t: translate } = useTranslation("dag");
const dailyData = generateDailyCalendarData(data, selectedYear, timezone);
const dailyData = generateDailyCalendarData(data, { deadlineMap, selectedYear, timezone });

const weekdays = [
translate("calendar.weekdays.sunday"),
Expand Down
Loading
Loading