From e593169ec463482045d16898ec3be226bf6278fc Mon Sep 17 00:00:00 2001 From: imrichardwu Date: Fri, 13 Feb 2026 16:55:52 -0700 Subject: [PATCH 01/14] Add FailedIcon component and DurationChart failed icon plugin This commit introduces a new `FailedIcon` component for representing failure states in the UI, along with a plugin for the DurationChart that visually indicates failed indices. The `FailedIcon` is created using Chakra UI's icon system, while the plugin draws a custom icon on the chart for failed data points. --- .../src/airflow/ui/src/assets/FailedIcon.tsx | 41 ++++++++++ .../DurationChart/failedIconPlugin.ts | 74 +++++++++++++++++++ 2 files changed, 115 insertions(+) create mode 100644 airflow-core/src/airflow/ui/src/assets/FailedIcon.tsx create mode 100644 airflow-core/src/airflow/ui/src/components/DurationChart/failedIconPlugin.ts diff --git a/airflow-core/src/airflow/ui/src/assets/FailedIcon.tsx b/airflow-core/src/airflow/ui/src/assets/FailedIcon.tsx new file mode 100644 index 0000000000000..5bc0b4e9bfb48 --- /dev/null +++ b/airflow-core/src/airflow/ui/src/assets/FailedIcon.tsx @@ -0,0 +1,41 @@ +/*! + * 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 { createIcon } from "@chakra-ui/react"; + +/** + * Warning/error icon (triangle with exclamation) for failed state. + * Use in UI components; for Canvas (e.g. Chart.js) use canvas drawing or an image. + */ +export const FailedIcon = createIcon({ + defaultProps: { + height: "1em", + width: "1em", + }, + displayName: "Failed Icon", + path: ( + + + + ), + viewBox: "0 0 16 16", +}); diff --git a/airflow-core/src/airflow/ui/src/components/DurationChart/failedIconPlugin.ts b/airflow-core/src/airflow/ui/src/components/DurationChart/failedIconPlugin.ts new file mode 100644 index 0000000000000..87f38e06c40da --- /dev/null +++ b/airflow-core/src/airflow/ui/src/components/DurationChart/failedIconPlugin.ts @@ -0,0 +1,74 @@ +/*! + * 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 type { Chart } from "chart.js"; + +const FAILED_ICON_PLUGIN_ID = "durationChartFailedIcon"; +const ICON_SIZE = 14; +const ICON_OFFSET = 4; + +export const createFailedIconPlugin = (failedIndices: Array, failedIconColor: string) => ({ + afterDatasetsDraw(chart: Chart) { + if (failedIndices.length === 0) { + return; + } + + const { ctx } = chart; + const meta = chart.getDatasetMeta(1); + + if (meta.data.length === 0) { + return; + } + + failedIndices.forEach((index) => { + const element = meta.data[index]; + + if (!element) { + return; + } + + const { x, y } = element.getProps(["x", "y"], true) as { x: number; y: number }; + const iconX = x; + const iconY = y - ICON_OFFSET - ICON_SIZE; + + ctx.save(); + + const half = ICON_SIZE / 2; + + ctx.beginPath(); + ctx.moveTo(iconX, iconY + ICON_SIZE); + ctx.lineTo(iconX - half, iconY); + ctx.lineTo(iconX + half, iconY); + ctx.closePath(); + ctx.fillStyle = failedIconColor; + ctx.fill(); + ctx.strokeStyle = failedIconColor; + ctx.lineWidth = 1; + ctx.stroke(); + + ctx.fillStyle = "white"; + ctx.font = `bold ${ICON_SIZE * 0.6}px sans-serif`; + ctx.textAlign = "center"; + ctx.textBaseline = "middle"; + ctx.fillText("!", iconX, iconY + ICON_SIZE * 0.55); + + ctx.restore(); + }); + }, + id: FAILED_ICON_PLUGIN_ID, +}); From 6c32415de128ca90af31e21841d2c2bb53bd1813 Mon Sep 17 00:00:00 2001 From: imrichardwu Date: Thu, 2 Apr 2026 10:13:42 -0600 Subject: [PATCH 02/14] Add initial plugin files for AI provider with license information --- .../providers/common/ai/plugins/__init__.py | 16 ++++++++++++++++ .../providers/common/ai/plugins/www/__init__.py | 16 ++++++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 providers/common/ai/src/airflow/providers/common/ai/plugins/__init__.py create mode 100644 providers/common/ai/src/airflow/providers/common/ai/plugins/www/__init__.py diff --git a/providers/common/ai/src/airflow/providers/common/ai/plugins/__init__.py b/providers/common/ai/src/airflow/providers/common/ai/plugins/__init__.py new file mode 100644 index 0000000000000..13a83393a9124 --- /dev/null +++ b/providers/common/ai/src/airflow/providers/common/ai/plugins/__init__.py @@ -0,0 +1,16 @@ +# 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. diff --git a/providers/common/ai/src/airflow/providers/common/ai/plugins/www/__init__.py b/providers/common/ai/src/airflow/providers/common/ai/plugins/www/__init__.py new file mode 100644 index 0000000000000..13a83393a9124 --- /dev/null +++ b/providers/common/ai/src/airflow/providers/common/ai/plugins/www/__init__.py @@ -0,0 +1,16 @@ +# 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. From 5c7fd912561421f5dc7831a7f003acf3d099d67f Mon Sep 17 00:00:00 2001 From: imrichardwu Date: Thu, 2 Apr 2026 10:39:43 -0600 Subject: [PATCH 03/14] Add deadline management features to DAG UI - Introduce CalendarDeadlines component to display deadlines in the calendar view. - Implement DeadlineAlertsBadge to show alerts related to deadlines in the DAG header. - Create DagDeadlines component for overview page to list upcoming and missed deadlines. - Add DeadlineStatus component to show the status of deadlines in the run details. - Enhance i18n support for new deadline-related strings in dag.json. --- .../ui/public/i18n/locales/en/dag.json | 24 +++ .../src/airflow/ui/src/assets/FailedIcon.tsx | 41 ----- .../DurationChart/failedIconPlugin.ts | 74 -------- .../ui/src/pages/Dag/Calendar/Calendar.tsx | 2 + .../pages/Dag/Calendar/CalendarDeadlines.tsx | 122 +++++++++++++ .../ui/src/pages/Dag/DeadlineAlertsBadge.tsx | 101 ++++++++++ .../src/airflow/ui/src/pages/Dag/Header.tsx | 2 + .../src/pages/Dag/Overview/DagDeadlines.tsx | 172 ++++++++++++++++++ .../ui/src/pages/Dag/Overview/Overview.tsx | 3 + .../ui/src/pages/Run/DeadlineStatus.tsx | 103 +++++++++++ .../src/airflow/ui/src/pages/Run/Header.tsx | 6 + 11 files changed, 535 insertions(+), 115 deletions(-) delete mode 100644 airflow-core/src/airflow/ui/src/assets/FailedIcon.tsx delete mode 100644 airflow-core/src/airflow/ui/src/components/DurationChart/failedIconPlugin.ts create mode 100644 airflow-core/src/airflow/ui/src/pages/Dag/Calendar/CalendarDeadlines.tsx create mode 100644 airflow-core/src/airflow/ui/src/pages/Dag/DeadlineAlertsBadge.tsx create mode 100644 airflow-core/src/airflow/ui/src/pages/Dag/Overview/DagDeadlines.tsx create mode 100644 airflow-core/src/airflow/ui/src/pages/Run/DeadlineStatus.tsx diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/en/dag.json b/airflow-core/src/airflow/ui/public/i18n/locales/en/dag.json index 58626c2494248..d9eeb09cd64ca 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/en/dag.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/en/dag.json @@ -7,6 +7,11 @@ }, "calendar": { "daily": "Daily", + "deadlines": { + "missed": "Missed", + "pending": "Pending", + "title": "Deadlines in Period" + }, "hourly": "Hourly", "legend": { "less": "Less", @@ -40,6 +45,20 @@ "parseDuration": "Parse Duration:", "parsedAt": "Parsed at:" }, + "deadlineAlerts": { + "count_one": "{{count}} deadline", + "count_other": "{{count}} deadlines", + "interval": "Interval", + "referenceType": "Calculated from", + "title": "Deadline Alerts", + "unnamed": "Unnamed Alert" + }, + "deadlineStatus": { + "label": "Deadline", + "met": "Met", + "missed": "Missed", + "upcoming": "Upcoming" + }, "extraLinks": "Extra Links", "grid": { "buttons": { @@ -93,6 +112,11 @@ "assetEvent_one": "Created Asset Event", "assetEvent_other": "Created Asset Events" }, + "deadlines": { + "pending": "Pending Deadlines", + "recentlyMissed": "Recently Missed Deadlines", + "title": "Deadlines" + }, "failedLogs": { "hideLogs": "Hide Logs", "showLogs": "Show Logs", diff --git a/airflow-core/src/airflow/ui/src/assets/FailedIcon.tsx b/airflow-core/src/airflow/ui/src/assets/FailedIcon.tsx deleted file mode 100644 index 5bc0b4e9bfb48..0000000000000 --- a/airflow-core/src/airflow/ui/src/assets/FailedIcon.tsx +++ /dev/null @@ -1,41 +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 { createIcon } from "@chakra-ui/react"; - -/** - * Warning/error icon (triangle with exclamation) for failed state. - * Use in UI components; for Canvas (e.g. Chart.js) use canvas drawing or an image. - */ -export const FailedIcon = createIcon({ - defaultProps: { - height: "1em", - width: "1em", - }, - displayName: "Failed Icon", - path: ( - - - - ), - viewBox: "0 0 16 16", -}); diff --git a/airflow-core/src/airflow/ui/src/components/DurationChart/failedIconPlugin.ts b/airflow-core/src/airflow/ui/src/components/DurationChart/failedIconPlugin.ts deleted file mode 100644 index 87f38e06c40da..0000000000000 --- a/airflow-core/src/airflow/ui/src/components/DurationChart/failedIconPlugin.ts +++ /dev/null @@ -1,74 +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 type { Chart } from "chart.js"; - -const FAILED_ICON_PLUGIN_ID = "durationChartFailedIcon"; -const ICON_SIZE = 14; -const ICON_OFFSET = 4; - -export const createFailedIconPlugin = (failedIndices: Array, failedIconColor: string) => ({ - afterDatasetsDraw(chart: Chart) { - if (failedIndices.length === 0) { - return; - } - - const { ctx } = chart; - const meta = chart.getDatasetMeta(1); - - if (meta.data.length === 0) { - return; - } - - failedIndices.forEach((index) => { - const element = meta.data[index]; - - if (!element) { - return; - } - - const { x, y } = element.getProps(["x", "y"], true) as { x: number; y: number }; - const iconX = x; - const iconY = y - ICON_OFFSET - ICON_SIZE; - - ctx.save(); - - const half = ICON_SIZE / 2; - - ctx.beginPath(); - ctx.moveTo(iconX, iconY + ICON_SIZE); - ctx.lineTo(iconX - half, iconY); - ctx.lineTo(iconX + half, iconY); - ctx.closePath(); - ctx.fillStyle = failedIconColor; - ctx.fill(); - ctx.strokeStyle = failedIconColor; - ctx.lineWidth = 1; - ctx.stroke(); - - ctx.fillStyle = "white"; - ctx.font = `bold ${ICON_SIZE * 0.6}px sans-serif`; - ctx.textAlign = "center"; - ctx.textBaseline = "middle"; - ctx.fillText("!", iconX, iconY + ICON_SIZE * 0.55); - - ctx.restore(); - }); - }, - id: FAILED_ICON_PLUGIN_ID, -}); 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 807c3a9376991..4e7c7f50a0cc5 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 @@ -30,6 +30,7 @@ import { ErrorAlert } from "src/components/ErrorAlert"; import { ButtonGroupToggle } from "src/components/ui/ButtonGroupToggle"; import { CALENDAR_GRANULARITY_KEY, CALENDAR_VIEW_MODE_KEY } from "src/constants/localStorage"; +import { CalendarDeadlines } from "./CalendarDeadlines"; import { CalendarLegend } from "./CalendarLegend"; import { DailyCalendarView } from "./DailyCalendarView"; import { HourlyCalendarView } from "./HourlyCalendarView"; @@ -254,6 +255,7 @@ export const Calendar = () => { )} + ); }; diff --git a/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/CalendarDeadlines.tsx b/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/CalendarDeadlines.tsx new file mode 100644 index 0000000000000..bd45ffad491b9 --- /dev/null +++ b/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/CalendarDeadlines.tsx @@ -0,0 +1,122 @@ +/*! + * 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 { Badge, Box, Heading, HStack, Link, Separator, Text, VStack } from "@chakra-ui/react"; +import { useTranslation } from "react-i18next"; +import { FiAlertTriangle, FiClock } from "react-icons/fi"; +import { Link as RouterLink } from "react-router-dom"; + +import { useDeadlinesServiceGetDeadlines } from "openapi/queries"; +import type { DeadlineResponse } from "openapi/requests/types.gen"; +import Time from "src/components/Time"; + +const DeadlineRow = ({ + deadline, + translate, +}: { + readonly deadline: DeadlineResponse; + readonly translate: (key: string) => string; +}) => ( + + + + {deadline.missed ? ( + + ) : ( + + )} + + {deadline.missed ? translate("calendar.deadlines.missed") : translate("calendar.deadlines.pending")} + + + + {deadline.dag_run_id} + + + + {deadline.alert_name !== undefined && deadline.alert_name !== null && deadline.alert_name !== "" ? ( + + {deadline.alert_name} + + ) : undefined} + + +); + +type CalendarDeadlinesProps = { + readonly dagId: string; + readonly endDate: string; + readonly startDate: string; +}; + +export const CalendarDeadlines = ({ dagId, endDate, startDate }: CalendarDeadlinesProps) => { + const { t: translate } = useTranslation("dag"); + + const { data: pendingData } = useDeadlinesServiceGetDeadlines({ + dagId, + dagRunId: "~", + deadlineTimeGte: startDate, + deadlineTimeLte: endDate, + limit: 20, + missed: false, + orderBy: ["deadline_time"], + }); + + const { data: missedData } = useDeadlinesServiceGetDeadlines({ + dagId, + dagRunId: "~", + deadlineTimeGte: startDate, + deadlineTimeLte: endDate, + limit: 20, + missed: true, + orderBy: ["-deadline_time"], + }); + + const pendingDeadlines = pendingData?.deadlines ?? []; + const missedDeadlines = missedData?.deadlines ?? []; + const allDeadlines = [...missedDeadlines, ...pendingDeadlines]; + + if (allDeadlines.length === 0) { + return undefined; + } + + return ( + + + + {translate("calendar.deadlines.title")} + {missedDeadlines.length > 0 ? ( + + {missedDeadlines.length} {translate("calendar.deadlines.missed")} + + ) : undefined} + {pendingDeadlines.length > 0 ? ( + + {pendingDeadlines.length} {translate("calendar.deadlines.pending")} + + ) : undefined} + + }> + {allDeadlines.map((dl) => ( + + ))} + + + ); +}; diff --git a/airflow-core/src/airflow/ui/src/pages/Dag/DeadlineAlertsBadge.tsx b/airflow-core/src/airflow/ui/src/pages/Dag/DeadlineAlertsBadge.tsx new file mode 100644 index 0000000000000..452f0052e3e39 --- /dev/null +++ b/airflow-core/src/airflow/ui/src/pages/Dag/DeadlineAlertsBadge.tsx @@ -0,0 +1,101 @@ +/*! + * 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, Button, HStack, Separator, Text, VStack } from "@chakra-ui/react"; +import { useTranslation } from "react-i18next"; +import { FiClock } from "react-icons/fi"; + +import { useDeadlinesServiceGetDagDeadlineAlerts } from "openapi/queries"; +import type { DeadlineAlertResponse } from "openapi/requests/types.gen"; +import { Popover } from "src/components/ui"; +import { renderDuration } from "src/utils"; + +const referenceTypeLabels: Record = { + AverageRuntimeDeadline: "Average Runtime", + DagRunLogicalDateDeadline: "Logical Date", + DagRunQueuedAtDeadline: "Queued At", + FixedDatetimeDeadline: "Fixed Datetime", +}; + +const formatReferenceType = (referenceType: string): string => + referenceTypeLabels[referenceType] ?? referenceType; + +const AlertRow = ({ alert }: { readonly alert: DeadlineAlertResponse }) => { + const { t: translate } = useTranslation("dag"); + + return ( + + + {alert.name !== undefined && alert.name !== null && alert.name !== "" + ? alert.name + : translate("deadlineAlerts.unnamed")} + + {alert.description !== undefined && alert.description !== null && alert.description !== "" ? ( + + {alert.description} + + ) : undefined} + + + {translate("deadlineAlerts.referenceType")}: {formatReferenceType(alert.reference_type)} + + + {translate("deadlineAlerts.interval")}:{" "} + {renderDuration(alert.interval, false) ?? `${alert.interval}s`} + + + + ); +}; + +export const DeadlineAlertsBadge = ({ dagId }: { readonly dagId: string }) => { + const { t: translate } = useTranslation("dag"); + + const { data } = useDeadlinesServiceGetDagDeadlineAlerts({ dagId }); + + const alerts = data?.deadline_alerts ?? []; + + if (alerts.length === 0) { + return undefined; + } + + return ( + // eslint-disable-next-line jsx-a11y/no-autofocus + + + + + + + + + {translate("deadlineAlerts.title")} + + }> + {alerts.map((alert) => ( + + ))} + + + + + ); +}; diff --git a/airflow-core/src/airflow/ui/src/pages/Dag/Header.tsx b/airflow-core/src/airflow/ui/src/pages/Dag/Header.tsx index 6af9129a31641..05c436dc9478f 100644 --- a/airflow-core/src/airflow/ui/src/pages/Dag/Header.tsx +++ b/airflow-core/src/airflow/ui/src/pages/Dag/Header.tsx @@ -35,6 +35,7 @@ import { TogglePause } from "src/components/TogglePause"; import { DagOwners } from "../DagsList/DagOwners"; import { DagTags } from "../DagsList/DagTags"; import { Schedule } from "../DagsList/Schedule"; +import { DeadlineAlertsBadge } from "./DeadlineAlertsBadge"; type LatestRunInfo = { dag_id: string; @@ -123,6 +124,7 @@ export const Header = ({ actions={ dag === undefined ? undefined : ( <> + {dag.doc_md === null ? undefined : ( ( + + + + + {deadline.dag_run_id} + + + {deadline.alert_name !== undefined && deadline.alert_name !== null && deadline.alert_name !== "" ? ( + + {deadline.alert_name} + + ) : undefined} + + +); + +export const DagDeadlines = ({ dagId }: { readonly dagId: string }) => { + const { t: translate } = useTranslation("dag"); + const refetchInterval = useAutoRefresh({ dagId }); + const now = dayjs().toISOString(); + + const { + data: pendingData, + error: pendingError, + isLoading: isPendingLoading, + } = useDeadlinesServiceGetDeadlines( + { + dagId, + dagRunId: "~", + deadlineTimeGte: now, + limit: LIMIT, + missed: false, + orderBy: ["deadline_time"], + }, + undefined, + { refetchInterval }, + ); + + const last24h = dayjs().subtract(24, "hour").toISOString(); + + const { + data: missedData, + error: missedError, + isLoading: isMissedLoading, + } = useDeadlinesServiceGetDeadlines( + { + dagId, + dagRunId: "~", + lastUpdatedAtGte: last24h, + limit: LIMIT, + missed: true, + orderBy: ["-last_updated_at"], + }, + undefined, + { refetchInterval }, + ); + + const pendingDeadlines = pendingData?.deadlines ?? []; + const missedDeadlines = missedData?.deadlines ?? []; + + if ( + !isPendingLoading && + !isMissedLoading && + pendingDeadlines.length === 0 && + missedDeadlines.length === 0 + ) { + return undefined; + } + + return ( + + + + + {translate("overview.deadlines.title")} + + + + + {isPendingLoading || pendingDeadlines.length > 0 ? ( + + + + {translate("overview.deadlines.pending")} + {pendingData ? ( + + {pendingData.total_entries} + + ) : undefined} + + {isPendingLoading ? ( + + {Array.from({ length: 3 }).map((_, idx) => ( + // eslint-disable-next-line react/no-array-index-key + + ))} + + ) : ( + }> + {pendingDeadlines.map((dl) => ( + + ))} + + )} + + ) : undefined} + + {isMissedLoading || missedDeadlines.length > 0 ? ( + + + + {translate("overview.deadlines.recentlyMissed")} + {missedData ? ( + + {missedData.total_entries} + + ) : undefined} + + {isMissedLoading ? ( + + {Array.from({ length: 3 }).map((_, idx) => ( + // eslint-disable-next-line react/no-array-index-key + + ))} + + ) : ( + }> + {missedDeadlines.map((dl) => ( + + ))} + + )} + + ) : undefined} + + + ); +}; diff --git a/airflow-core/src/airflow/ui/src/pages/Dag/Overview/Overview.tsx b/airflow-core/src/airflow/ui/src/pages/Dag/Overview/Overview.tsx index 7ea155fe84761..a98edc16e223d 100644 --- a/airflow-core/src/airflow/ui/src/pages/Dag/Overview/Overview.tsx +++ b/airflow-core/src/airflow/ui/src/pages/Dag/Overview/Overview.tsx @@ -38,6 +38,8 @@ import { SearchParamsKeys } from "src/constants/searchParams"; import { useGridRuns } from "src/queries/useGridRuns.ts"; import { isStatePending, useAutoRefresh } from "src/utils"; +import { DagDeadlines } from "./DagDeadlines"; + const FailedLogs = lazy(() => import("./FailedLogs")); const defaultHour = "24"; @@ -146,6 +148,7 @@ export const Overview = () => { /> ) : undefined} + {dagId === undefined ? undefined : } }> diff --git a/airflow-core/src/airflow/ui/src/pages/Run/DeadlineStatus.tsx b/airflow-core/src/airflow/ui/src/pages/Run/DeadlineStatus.tsx new file mode 100644 index 0000000000000..ae777c36ccd8e --- /dev/null +++ b/airflow-core/src/airflow/ui/src/pages/Run/DeadlineStatus.tsx @@ -0,0 +1,103 @@ +/*! + * 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 { Badge, HStack, Text, VStack } from "@chakra-ui/react"; +import { useTranslation } from "react-i18next"; +import { FiAlertTriangle, FiCheck, FiClock } from "react-icons/fi"; + +import { useDeadlinesServiceGetDagDeadlineAlerts, useDeadlinesServiceGetDeadlines } from "openapi/queries"; +import Time from "src/components/Time"; + +type DeadlineStatusProps = { + readonly dagId: string; + readonly dagRunId: string; +}; + +export const DeadlineStatus = ({ dagId, dagRunId }: DeadlineStatusProps) => { + const { t: translate } = useTranslation("dag"); + + const { data: deadlineData } = useDeadlinesServiceGetDeadlines({ + dagId, + dagRunId, + limit: 10, + orderBy: ["deadline_time"], + }); + + const { data: alertData } = useDeadlinesServiceGetDagDeadlineAlerts({ dagId }); + + const deadlines = deadlineData?.deadlines ?? []; + const hasAlerts = (alertData?.total_entries ?? 0) > 0; + + if (deadlines.length === 0 && !hasAlerts) { + return undefined; + } + + if (deadlines.length === 0 && hasAlerts) { + return ( + + + + {translate("deadlineStatus.met")} + + + ); + } + + const missedDeadlines = deadlines.filter((dl) => dl.missed); + const upcomingDeadlines = deadlines.filter((dl) => !dl.missed); + + if (missedDeadlines.length > 0) { + return ( + + {missedDeadlines.map((dl) => ( + + + + {translate("deadlineStatus.missed")} + + + ))} + + ); + } + + return ( + + {upcomingDeadlines.map((dl) => ( + + + + {translate("deadlineStatus.upcoming")} + + + ))} + + ); +}; diff --git a/airflow-core/src/airflow/ui/src/pages/Run/Header.tsx b/airflow-core/src/airflow/ui/src/pages/Run/Header.tsx index a5a0c4a26c2fa..d4c581ce59c46 100644 --- a/airflow-core/src/airflow/ui/src/pages/Run/Header.tsx +++ b/airflow-core/src/airflow/ui/src/pages/Run/Header.tsx @@ -36,6 +36,8 @@ import DeleteRunButton from "src/pages/DeleteRunButton"; import { usePatchDagRun } from "src/queries/usePatchDagRun"; import { getDuration } from "src/utils"; +import { DeadlineStatus } from "./DeadlineStatus"; + export const Header = ({ dagRun }: { readonly dagRun: DAGRunResponse }) => { const { t: translate } = useTranslation(); const [note, setNote] = useState(dagRun.note); @@ -139,6 +141,10 @@ export const Header = ({ dagRun }: { readonly dagRun: DAGRunResponse }) => { /> ), }, + { + label: translate("dag:deadlineStatus.label"), + value: , + }, ]} title={dagRun.dag_run_id} /> From 9b2fd64335fc953284eb11f7a204838b98b9e4a3 Mon Sep 17 00:00:00 2001 From: imrichardwu Date: Thu, 2 Apr 2026 11:01:15 -0600 Subject: [PATCH 04/14] Add name and description fields to DeadlineAlert and related serialization --- airflow-core/src/airflow/models/serialized_dag.py | 2 ++ airflow-core/src/airflow/serialization/decoders.py | 2 ++ .../src/airflow/serialization/definitions/deadline.py | 4 ++++ airflow-core/src/airflow/serialization/encoders.py | 2 ++ task-sdk/src/airflow/sdk/definitions/deadline.py | 4 ++++ 5 files changed, 14 insertions(+) diff --git a/airflow-core/src/airflow/models/serialized_dag.py b/airflow-core/src/airflow/models/serialized_dag.py index 23b93f2dae5aa..0883250ff3992 100644 --- a/airflow-core/src/airflow/models/serialized_dag.py +++ b/airflow-core/src/airflow/models/serialized_dag.py @@ -502,6 +502,8 @@ def _create_deadline_alert_records( for uuid_str, deadline_data in uuid_mapping.items(): alert = DeadlineAlertModel( id=UUID(uuid_str), + name=deadline_data.get(DeadlineAlertFields.NAME), + description=deadline_data.get(DeadlineAlertFields.DESCRIPTION), reference=deadline_data[DeadlineAlertFields.REFERENCE], interval=deadline_data[DeadlineAlertFields.INTERVAL], callback_def=deadline_data[DeadlineAlertFields.CALLBACK], diff --git a/airflow-core/src/airflow/serialization/decoders.py b/airflow-core/src/airflow/serialization/decoders.py index a25068443e4c0..2eb0b959c384e 100644 --- a/airflow-core/src/airflow/serialization/decoders.py +++ b/airflow-core/src/airflow/serialization/decoders.py @@ -166,6 +166,8 @@ def decode_deadline_alert(encoded_data: dict): reference=reference, interval=datetime.timedelta(seconds=data[DeadlineAlertFields.INTERVAL]), callback=deserialize(data[DeadlineAlertFields.CALLBACK]), + name=data.get(DeadlineAlertFields.NAME), + description=data.get(DeadlineAlertFields.DESCRIPTION), ) diff --git a/airflow-core/src/airflow/serialization/definitions/deadline.py b/airflow-core/src/airflow/serialization/definitions/deadline.py index 93af9ef19e7cc..39f701ee2c467 100644 --- a/airflow-core/src/airflow/serialization/definitions/deadline.py +++ b/airflow-core/src/airflow/serialization/definitions/deadline.py @@ -49,6 +49,8 @@ class DeadlineAlertFields: serializing DeadlineAlert instances to and from their dictionary representation. """ + NAME = "name" + DESCRIPTION = "description" REFERENCE = "reference" INTERVAL = "interval" CALLBACK = "callback" @@ -367,3 +369,5 @@ class SerializedDeadlineAlert: reference: SerializedReferenceModels.SerializedBaseDeadlineReference interval: timedelta callback: Any + name: str | None = None + description: str | None = None diff --git a/airflow-core/src/airflow/serialization/encoders.py b/airflow-core/src/airflow/serialization/encoders.py index dcb064dcde06b..7aee02242374b 100644 --- a/airflow-core/src/airflow/serialization/encoders.py +++ b/airflow-core/src/airflow/serialization/encoders.py @@ -203,6 +203,8 @@ def encode_deadline_alert(d: DeadlineAlert | SerializedDeadlineAlert) -> dict[st from airflow.sdk.serde import serialize return { + "name": getattr(d, "name", None), + "description": getattr(d, "description", None), "reference": encode_deadline_reference(d.reference), "interval": d.interval.total_seconds(), "callback": serialize(d.callback), diff --git a/task-sdk/src/airflow/sdk/definitions/deadline.py b/task-sdk/src/airflow/sdk/definitions/deadline.py index 2fe220e789d02..dee4b8828307c 100644 --- a/task-sdk/src/airflow/sdk/definitions/deadline.py +++ b/task-sdk/src/airflow/sdk/definitions/deadline.py @@ -145,9 +145,13 @@ def __init__( reference: DeadlineReferenceType, interval: timedelta, callback: Callback, + name: str | None = None, + description: str | None = None, ): self.reference = reference self.interval = interval + self.name = name + self.description = description if not isinstance(callback, (AsyncCallback, SyncCallback)): raise ValueError(f"Callbacks of type {type(callback).__name__} are not currently supported") From d42cd59f70a902508443102104595908c8f2613c Mon Sep 17 00:00:00 2001 From: imrichardwu Date: Thu, 2 Apr 2026 14:37:31 -0600 Subject: [PATCH 05/14] Refactor deadline management in DAG UI: update DeadlineAlert fields, remove CalendarDeadlines component, and enhance deadline display with new modal and row components. --- .../src/airflow/models/serialized_dag.py | 14 +- .../ui/src/pages/Dag/Calendar/Calendar.tsx | 2 - .../pages/Dag/Calendar/CalendarDeadlines.tsx | 122 ------------------ .../ui/src/pages/Dag/DeadlineAlertsBadge.tsx | 17 ++- .../pages/Dag/Overview/AllDeadlinesModal.tsx | 102 +++++++++++++++ .../src/pages/Dag/Overview/DagDeadlines.tsx | 84 ++++++++---- .../ui/src/pages/Dag/Overview/DeadlineRow.tsx | 41 ++++++ .../ui/src/pages/Dag/Overview/Overview.tsx | 4 +- .../ui/src/pages/Run/DeadlineStatus.tsx | 10 +- .../src/airflow/ui/src/pages/Run/Header.tsx | 16 ++- 10 files changed, 247 insertions(+), 165 deletions(-) delete mode 100644 airflow-core/src/airflow/ui/src/pages/Dag/Calendar/CalendarDeadlines.tsx create mode 100644 airflow-core/src/airflow/ui/src/pages/Dag/Overview/AllDeadlinesModal.tsx create mode 100644 airflow-core/src/airflow/ui/src/pages/Dag/Overview/DeadlineRow.tsx diff --git a/airflow-core/src/airflow/models/serialized_dag.py b/airflow-core/src/airflow/models/serialized_dag.py index 0883250ff3992..4349c0bd9ebfd 100644 --- a/airflow-core/src/airflow/models/serialized_dag.py +++ b/airflow-core/src/airflow/models/serialized_dag.py @@ -579,8 +579,18 @@ def write_dag( if deadline_uuid_mapping is not None: # All deadlines matched — reuse the UUIDs to preserve hash. - # Clear the mapping since the alert rows already exist in the DB; - # no need to delete and recreate identical records. + # Update name/description in case they changed (they don't affect + # the definition match or the hash, so existing rows won't be + # recreated, but they must stay current in the DB). + for uuid_str, deadline_data in deadline_uuid_mapping.items(): + session.execute( + update(DeadlineAlertModel) + .where(DeadlineAlertModel.id == UUID(uuid_str)) + .values( + name=deadline_data.get(DeadlineAlertFields.NAME), + description=deadline_data.get(DeadlineAlertFields.DESCRIPTION), + ) + ) dag.data["dag"]["deadline"] = existing_deadline_uuids deadline_uuid_mapping = {} else: 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 4e7c7f50a0cc5..807c3a9376991 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 @@ -30,7 +30,6 @@ import { ErrorAlert } from "src/components/ErrorAlert"; import { ButtonGroupToggle } from "src/components/ui/ButtonGroupToggle"; import { CALENDAR_GRANULARITY_KEY, CALENDAR_VIEW_MODE_KEY } from "src/constants/localStorage"; -import { CalendarDeadlines } from "./CalendarDeadlines"; import { CalendarLegend } from "./CalendarLegend"; import { DailyCalendarView } from "./DailyCalendarView"; import { HourlyCalendarView } from "./HourlyCalendarView"; @@ -255,7 +254,6 @@ export const Calendar = () => { )} - ); }; diff --git a/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/CalendarDeadlines.tsx b/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/CalendarDeadlines.tsx deleted file mode 100644 index bd45ffad491b9..0000000000000 --- a/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/CalendarDeadlines.tsx +++ /dev/null @@ -1,122 +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 { Badge, Box, Heading, HStack, Link, Separator, Text, VStack } from "@chakra-ui/react"; -import { useTranslation } from "react-i18next"; -import { FiAlertTriangle, FiClock } from "react-icons/fi"; -import { Link as RouterLink } from "react-router-dom"; - -import { useDeadlinesServiceGetDeadlines } from "openapi/queries"; -import type { DeadlineResponse } from "openapi/requests/types.gen"; -import Time from "src/components/Time"; - -const DeadlineRow = ({ - deadline, - translate, -}: { - readonly deadline: DeadlineResponse; - readonly translate: (key: string) => string; -}) => ( - - - - {deadline.missed ? ( - - ) : ( - - )} - - {deadline.missed ? translate("calendar.deadlines.missed") : translate("calendar.deadlines.pending")} - - - - {deadline.dag_run_id} - - - - {deadline.alert_name !== undefined && deadline.alert_name !== null && deadline.alert_name !== "" ? ( - - {deadline.alert_name} - - ) : undefined} - - -); - -type CalendarDeadlinesProps = { - readonly dagId: string; - readonly endDate: string; - readonly startDate: string; -}; - -export const CalendarDeadlines = ({ dagId, endDate, startDate }: CalendarDeadlinesProps) => { - const { t: translate } = useTranslation("dag"); - - const { data: pendingData } = useDeadlinesServiceGetDeadlines({ - dagId, - dagRunId: "~", - deadlineTimeGte: startDate, - deadlineTimeLte: endDate, - limit: 20, - missed: false, - orderBy: ["deadline_time"], - }); - - const { data: missedData } = useDeadlinesServiceGetDeadlines({ - dagId, - dagRunId: "~", - deadlineTimeGte: startDate, - deadlineTimeLte: endDate, - limit: 20, - missed: true, - orderBy: ["-deadline_time"], - }); - - const pendingDeadlines = pendingData?.deadlines ?? []; - const missedDeadlines = missedData?.deadlines ?? []; - const allDeadlines = [...missedDeadlines, ...pendingDeadlines]; - - if (allDeadlines.length === 0) { - return undefined; - } - - return ( - - - - {translate("calendar.deadlines.title")} - {missedDeadlines.length > 0 ? ( - - {missedDeadlines.length} {translate("calendar.deadlines.missed")} - - ) : undefined} - {pendingDeadlines.length > 0 ? ( - - {pendingDeadlines.length} {translate("calendar.deadlines.pending")} - - ) : undefined} - - }> - {allDeadlines.map((dl) => ( - - ))} - - - ); -}; diff --git a/airflow-core/src/airflow/ui/src/pages/Dag/DeadlineAlertsBadge.tsx b/airflow-core/src/airflow/ui/src/pages/Dag/DeadlineAlertsBadge.tsx index 452f0052e3e39..740eab7bd4702 100644 --- a/airflow-core/src/airflow/ui/src/pages/Dag/DeadlineAlertsBadge.tsx +++ b/airflow-core/src/airflow/ui/src/pages/Dag/DeadlineAlertsBadge.tsx @@ -17,10 +17,11 @@ * under the License. */ import { Box, Button, HStack, Separator, Text, VStack } from "@chakra-ui/react"; +import dayjs from "dayjs"; import { useTranslation } from "react-i18next"; import { FiClock } from "react-icons/fi"; -import { useDeadlinesServiceGetDagDeadlineAlerts } from "openapi/queries"; +import { useDeadlinesServiceGetDeadlines, useDeadlinesServiceGetDagDeadlineAlerts } from "openapi/queries"; import type { DeadlineAlertResponse } from "openapi/requests/types.gen"; import { Popover } from "src/components/ui"; import { renderDuration } from "src/utils"; @@ -67,10 +68,18 @@ export const DeadlineAlertsBadge = ({ dagId }: { readonly dagId: string }) => { const { t: translate } = useTranslation("dag"); const { data } = useDeadlinesServiceGetDagDeadlineAlerts({ dagId }); + const { data: missedData } = useDeadlinesServiceGetDeadlines({ + dagId, + dagRunId: "~", + lastUpdatedAtGte: dayjs().subtract(24, "hour").toISOString(), + limit: 1, + missed: true, + }); - const alerts = data?.deadline_alerts ?? []; + const alerts = (data?.total_entries ?? 0) > 0 ? data?.deadline_alerts : []; + const hasMissed = (missedData?.total_entries ?? 0) > 0; - if (alerts.length === 0) { + if (!alerts || alerts.length === 0) { return undefined; } @@ -78,7 +87,7 @@ export const DeadlineAlertsBadge = ({ dagId }: { readonly dagId: string }) => { // eslint-disable-next-line jsx-a11y/no-autofocus - diff --git a/airflow-core/src/airflow/ui/src/pages/Dag/Overview/AllDeadlinesModal.tsx b/airflow-core/src/airflow/ui/src/pages/Dag/Overview/AllDeadlinesModal.tsx new file mode 100644 index 0000000000000..39ff0a085d66a --- /dev/null +++ b/airflow-core/src/airflow/ui/src/pages/Dag/Overview/AllDeadlinesModal.tsx @@ -0,0 +1,102 @@ +/*! + * 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 { Heading, Separator, Skeleton, VStack } from "@chakra-ui/react"; + +import { useDeadlinesServiceGetDeadlines } from "openapi/queries"; +import { ErrorAlert } from "src/components/ErrorAlert"; +import { Dialog } from "src/components/ui"; + +import { DeadlineRow } from "./DeadlineRow"; + +const MODAL_LIMIT = 100; + +type AllDeadlinesModalProps = { + readonly dagId: string; + readonly endDate: string; + readonly missed: boolean; + readonly onClose: () => void; + readonly open: boolean; + readonly refetchInterval: number | false; + readonly startDate: string; + readonly title: string; +}; + +export const AllDeadlinesModal = ({ + dagId, + endDate, + missed, + onClose, + open, + refetchInterval, + startDate, + title, +}: AllDeadlinesModalProps) => { + const { data, error, isLoading } = useDeadlinesServiceGetDeadlines( + missed + ? { + dagId, + dagRunId: "~", + lastUpdatedAtGte: startDate, + lastUpdatedAtLte: endDate, + limit: MODAL_LIMIT, + missed: true, + orderBy: ["-last_updated_at"], + } + : { + dagId, + dagRunId: "~", + deadlineTimeGte: endDate, + limit: MODAL_LIMIT, + missed: false, + orderBy: ["deadline_time"], + }, + undefined, + { enabled: open, refetchInterval }, + ); + + const deadlines = data?.deadlines ?? []; + + return ( + + + + {title} + + + + + {isLoading ? ( + + {Array.from({ length: 5 }).map((_, idx) => ( + // eslint-disable-next-line react/no-array-index-key + + ))} + + ) : deadlines.length === 0 ? undefined : ( + }> + {deadlines.map((dl) => ( + + ))} + + )} + + + + ); +}; diff --git a/airflow-core/src/airflow/ui/src/pages/Dag/Overview/DagDeadlines.tsx b/airflow-core/src/airflow/ui/src/pages/Dag/Overview/DagDeadlines.tsx index 7e98690f87f8c..2ee24d4290b26 100644 --- a/airflow-core/src/airflow/ui/src/pages/Dag/Overview/DagDeadlines.tsx +++ b/airflow-core/src/airflow/ui/src/pages/Dag/Overview/DagDeadlines.tsx @@ -16,42 +16,30 @@ * specific language governing permissions and limitations * under the License. */ -import { Badge, Box, Flex, Heading, HStack, Link, Separator, Skeleton, Text, VStack } from "@chakra-ui/react"; -import dayjs from "dayjs"; +import { Badge, Box, Button, Flex, Heading, HStack, Separator, Skeleton, VStack } from "@chakra-ui/react"; +import { useState } from "react"; import { useTranslation } from "react-i18next"; import { FiAlertTriangle, FiClock } from "react-icons/fi"; -import { Link as RouterLink } from "react-router-dom"; import { useDeadlinesServiceGetDeadlines } from "openapi/queries"; -import type { DeadlineResponse } from "openapi/requests/types.gen"; import { ErrorAlert } from "src/components/ErrorAlert"; -import Time from "src/components/Time"; import { useAutoRefresh } from "src/utils"; +import { AllDeadlinesModal } from "./AllDeadlinesModal"; +import { DeadlineRow } from "./DeadlineRow"; + const LIMIT = 5; -const DeadlineRow = ({ deadline }: { readonly deadline: DeadlineResponse }) => ( - - - - - {deadline.dag_run_id} - - - {deadline.alert_name !== undefined && deadline.alert_name !== null && deadline.alert_name !== "" ? ( - - {deadline.alert_name} - - ) : undefined} - - -); +type DagDeadlinesProps = { + readonly dagId: string; + readonly endDate: string; + readonly startDate: string; +}; -export const DagDeadlines = ({ dagId }: { readonly dagId: string }) => { +export const DagDeadlines = ({ dagId, endDate, startDate }: DagDeadlinesProps) => { const { t: translate } = useTranslation("dag"); const refetchInterval = useAutoRefresh({ dagId }); - const now = dayjs().toISOString(); + const [modalOpen, setModalOpen] = useState<"missed" | "pending" | null>(null); const { data: pendingData, @@ -61,7 +49,7 @@ export const DagDeadlines = ({ dagId }: { readonly dagId: string }) => { { dagId, dagRunId: "~", - deadlineTimeGte: now, + deadlineTimeGte: endDate, limit: LIMIT, missed: false, orderBy: ["deadline_time"], @@ -70,8 +58,6 @@ export const DagDeadlines = ({ dagId }: { readonly dagId: string }) => { { refetchInterval }, ); - const last24h = dayjs().subtract(24, "hour").toISOString(); - const { data: missedData, error: missedError, @@ -80,7 +66,8 @@ export const DagDeadlines = ({ dagId }: { readonly dagId: string }) => { { dagId, dagRunId: "~", - lastUpdatedAtGte: last24h, + lastUpdatedAtGte: startDate, + lastUpdatedAtLte: endDate, limit: LIMIT, missed: true, orderBy: ["-last_updated_at"], @@ -134,6 +121,19 @@ export const DagDeadlines = ({ dagId }: { readonly dagId: string }) => { {pendingDeadlines.map((dl) => ( ))} + {(pendingData?.total_entries ?? 0) > LIMIT ? ( + + ) : undefined} )} @@ -162,11 +162,39 @@ export const DagDeadlines = ({ dagId }: { readonly dagId: string }) => { {missedDeadlines.map((dl) => ( ))} + {(missedData?.total_entries ?? 0) > LIMIT ? ( + + ) : undefined} )} ) : undefined} + + setModalOpen(null)} + open={modalOpen !== null} + refetchInterval={refetchInterval} + startDate={startDate} + title={ + modalOpen === "missed" + ? translate("overview.deadlines.recentlyMissed") + : translate("overview.deadlines.pending") + } + /> ); }; diff --git a/airflow-core/src/airflow/ui/src/pages/Dag/Overview/DeadlineRow.tsx b/airflow-core/src/airflow/ui/src/pages/Dag/Overview/DeadlineRow.tsx new file mode 100644 index 0000000000000..3eba76c0fb0ca --- /dev/null +++ b/airflow-core/src/airflow/ui/src/pages/Dag/Overview/DeadlineRow.tsx @@ -0,0 +1,41 @@ +/*! + * 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 { HStack, Link, Text, VStack } from "@chakra-ui/react"; +import { Link as RouterLink } from "react-router-dom"; + +import type { DeadlineResponse } from "openapi/requests/types.gen"; +import Time from "src/components/Time"; + +export const DeadlineRow = ({ deadline }: { readonly deadline: DeadlineResponse }) => ( + + + + + {deadline.dag_run_id} + + + {deadline.alert_name !== undefined && deadline.alert_name !== null && deadline.alert_name !== "" ? ( + + {deadline.alert_name} + + ) : undefined} + + +); diff --git a/airflow-core/src/airflow/ui/src/pages/Dag/Overview/Overview.tsx b/airflow-core/src/airflow/ui/src/pages/Dag/Overview/Overview.tsx index a98edc16e223d..7627695f2c45f 100644 --- a/airflow-core/src/airflow/ui/src/pages/Dag/Overview/Overview.tsx +++ b/airflow-core/src/airflow/ui/src/pages/Dag/Overview/Overview.tsx @@ -148,7 +148,9 @@ export const Overview = () => { /> ) : undefined} - {dagId === undefined ? undefined : } + {dagId === undefined ? undefined : ( + + )} }> diff --git a/airflow-core/src/airflow/ui/src/pages/Run/DeadlineStatus.tsx b/airflow-core/src/airflow/ui/src/pages/Run/DeadlineStatus.tsx index ae777c36ccd8e..9c66925716d64 100644 --- a/airflow-core/src/airflow/ui/src/pages/Run/DeadlineStatus.tsx +++ b/airflow-core/src/airflow/ui/src/pages/Run/DeadlineStatus.tsx @@ -31,14 +31,20 @@ type DeadlineStatusProps = { export const DeadlineStatus = ({ dagId, dagRunId }: DeadlineStatusProps) => { const { t: translate } = useTranslation("dag"); - const { data: deadlineData } = useDeadlinesServiceGetDeadlines({ + const { data: deadlineData, isLoading: isLoadingDeadlines } = useDeadlinesServiceGetDeadlines({ dagId, dagRunId, limit: 10, orderBy: ["deadline_time"], }); - const { data: alertData } = useDeadlinesServiceGetDagDeadlineAlerts({ dagId }); + const { data: alertData, isLoading: isLoadingAlerts } = useDeadlinesServiceGetDagDeadlineAlerts({ + dagId, + }); + + if (isLoadingDeadlines || isLoadingAlerts) { + return undefined; + } const deadlines = deadlineData?.deadlines ?? []; const hasAlerts = (alertData?.total_entries ?? 0) > 0; diff --git a/airflow-core/src/airflow/ui/src/pages/Run/Header.tsx b/airflow-core/src/airflow/ui/src/pages/Run/Header.tsx index d4c581ce59c46..506c33e686445 100644 --- a/airflow-core/src/airflow/ui/src/pages/Run/Header.tsx +++ b/airflow-core/src/airflow/ui/src/pages/Run/Header.tsx @@ -22,6 +22,7 @@ import { useTranslation } from "react-i18next"; import { FiBarChart } from "react-icons/fi"; import { Link as RouterLink } from "react-router-dom"; +import { useDeadlinesServiceGetDagDeadlineAlerts } from "openapi/queries"; import type { DAGRunResponse } from "openapi/requests/types.gen"; import { ClearRunButton } from "src/components/Clear"; import { DagVersion } from "src/components/DagVersion"; @@ -45,6 +46,9 @@ export const Header = ({ dagRun }: { readonly dagRun: DAGRunResponse }) => { const dagId = dagRun.dag_id; const dagRunId = dagRun.dag_run_id; + const { data: alertData } = useDeadlinesServiceGetDagDeadlineAlerts({ dagId }); + const hasDeadlineAlerts = (alertData?.total_entries ?? 0) > 0; + const { isPending, mutate } = usePatchDagRun({ dagId, dagRunId, @@ -141,10 +145,14 @@ export const Header = ({ dagRun }: { readonly dagRun: DAGRunResponse }) => { /> ), }, - { - label: translate("dag:deadlineStatus.label"), - value: , - }, + ...(hasDeadlineAlerts + ? [ + { + label: translate("dag:deadlineStatus.label"), + value: , + }, + ] + : []), ]} title={dagRun.dag_run_id} /> From af27e2f708b94e90846d470144e9e95abd28904d Mon Sep 17 00:00:00 2001 From: imrichardwu Date: Thu, 2 Apr 2026 14:54:34 -0600 Subject: [PATCH 06/14] Enhance AllDeadlinesModal: add pagination support and update deadline limit handling --- .../ui/public/i18n/locales/en/dag.json | 3 +- .../pages/Dag/Overview/AllDeadlinesModal.tsx | 44 +++++++++++++++---- 2 files changed, 38 insertions(+), 9 deletions(-) diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/en/dag.json b/airflow-core/src/airflow/ui/public/i18n/locales/en/dag.json index d9eeb09cd64ca..78fe826e390e9 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/en/dag.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/en/dag.json @@ -115,7 +115,8 @@ "deadlines": { "pending": "Pending Deadlines", "recentlyMissed": "Recently Missed Deadlines", - "title": "Deadlines" + "title": "Deadlines", + "viewAll": "View all {{count}}" }, "failedLogs": { "hideLogs": "Hide Logs", diff --git a/airflow-core/src/airflow/ui/src/pages/Dag/Overview/AllDeadlinesModal.tsx b/airflow-core/src/airflow/ui/src/pages/Dag/Overview/AllDeadlinesModal.tsx index 39ff0a085d66a..1a1c1edbc71da 100644 --- a/airflow-core/src/airflow/ui/src/pages/Dag/Overview/AllDeadlinesModal.tsx +++ b/airflow-core/src/airflow/ui/src/pages/Dag/Overview/AllDeadlinesModal.tsx @@ -16,15 +16,17 @@ * specific language governing permissions and limitations * under the License. */ -import { Heading, Separator, Skeleton, VStack } from "@chakra-ui/react"; +import { Heading, HStack, Separator, Skeleton, VStack } from "@chakra-ui/react"; +import { useState } from "react"; import { useDeadlinesServiceGetDeadlines } from "openapi/queries"; import { ErrorAlert } from "src/components/ErrorAlert"; import { Dialog } from "src/components/ui"; +import { Pagination } from "src/components/ui/Pagination"; import { DeadlineRow } from "./DeadlineRow"; -const MODAL_LIMIT = 100; +const PAGE_LIMIT = 10; type AllDeadlinesModalProps = { readonly dagId: string; @@ -47,6 +49,9 @@ export const AllDeadlinesModal = ({ startDate, title, }: AllDeadlinesModalProps) => { + const [page, setPage] = useState(1); + const offset = (page - 1) * PAGE_LIMIT; + const { data, error, isLoading } = useDeadlinesServiceGetDeadlines( missed ? { @@ -54,16 +59,18 @@ export const AllDeadlinesModal = ({ dagRunId: "~", lastUpdatedAtGte: startDate, lastUpdatedAtLte: endDate, - limit: MODAL_LIMIT, + limit: PAGE_LIMIT, missed: true, + offset, orderBy: ["-last_updated_at"], } : { dagId, dagRunId: "~", deadlineTimeGte: endDate, - limit: MODAL_LIMIT, + limit: PAGE_LIMIT, missed: false, + offset, orderBy: ["deadline_time"], }, undefined, @@ -71,24 +78,30 @@ export const AllDeadlinesModal = ({ ); const deadlines = data?.deadlines ?? []; + const totalEntries = data?.total_entries ?? 0; + + const onOpenChange = () => { + setPage(1); + onClose(); + }; return ( - + {title} - + {isLoading ? ( - {Array.from({ length: 5 }).map((_, idx) => ( + {Array.from({ length: PAGE_LIMIT }).map((_, idx) => ( // eslint-disable-next-line react/no-array-index-key ))} - ) : deadlines.length === 0 ? undefined : ( + ) : ( }> {deadlines.map((dl) => ( @@ -96,6 +109,21 @@ export const AllDeadlinesModal = ({ )} + {totalEntries > PAGE_LIMIT ? ( + setPage(event.page)} + p={3} + page={page} + pageSize={PAGE_LIMIT} + > + + + + + + + ) : undefined} ); From 83b4e028c7fef2c324f7cdeaae0226558fefb0bf Mon Sep 17 00:00:00 2001 From: imrichardwu Date: Thu, 2 Apr 2026 15:07:18 -0600 Subject: [PATCH 07/14] Enhance AllDeadlinesModal: increase dialog size and add backdrop padding --- .../airflow/ui/src/pages/Dag/Overview/AllDeadlinesModal.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/airflow-core/src/airflow/ui/src/pages/Dag/Overview/AllDeadlinesModal.tsx b/airflow-core/src/airflow/ui/src/pages/Dag/Overview/AllDeadlinesModal.tsx index 1a1c1edbc71da..a8b81013ee03b 100644 --- a/airflow-core/src/airflow/ui/src/pages/Dag/Overview/AllDeadlinesModal.tsx +++ b/airflow-core/src/airflow/ui/src/pages/Dag/Overview/AllDeadlinesModal.tsx @@ -86,8 +86,8 @@ export const AllDeadlinesModal = ({ }; return ( - - + + {title} From c040737d8426740b49724172095226d6e2f28296 Mon Sep 17 00:00:00 2001 From: imrichardwu Date: Mon, 6 Apr 2026 13:01:33 -0600 Subject: [PATCH 08/14] Enhance DeadlineStatus: update icon colors for better visibility --- .../src/airflow/ui/src/pages/Run/DeadlineStatus.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/airflow-core/src/airflow/ui/src/pages/Run/DeadlineStatus.tsx b/airflow-core/src/airflow/ui/src/pages/Run/DeadlineStatus.tsx index 9c66925716d64..7e60fd666780e 100644 --- a/airflow-core/src/airflow/ui/src/pages/Run/DeadlineStatus.tsx +++ b/airflow-core/src/airflow/ui/src/pages/Run/DeadlineStatus.tsx @@ -56,8 +56,8 @@ export const DeadlineStatus = ({ dagId, dagRunId }: DeadlineStatusProps) => { if (deadlines.length === 0 && hasAlerts) { return ( - + {translate("deadlineStatus.met")} @@ -72,8 +72,8 @@ export const DeadlineStatus = ({ dagId, dagRunId }: DeadlineStatusProps) => { {missedDeadlines.map((dl) => ( - + {translate("deadlineStatus.missed")}