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 7e3ad3d65fbf7..428de89205a53 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
@@ -40,6 +40,32 @@
"parseDuration": "Parse Duration:",
"parsedAt": "Parsed at:"
},
+ "deadlineAlerts": {
+ "completionRule": "Must complete within {{interval}} of {{reference}}",
+ "count_one": "{{count}} deadline",
+ "count_other": "{{count}} deadlines",
+ "referenceType": {
+ "AverageRuntimeDeadline": "average runtime",
+ "DagRunLogicalDateDeadline": "logical date",
+ "DagRunQueuedAtDeadline": "queue time"
+ }
+ },
+ "deadlineStatus": {
+ "actual": "Actual",
+ "expected": "Expected",
+ "finishedEarly": "Finished {{duration}} before deadline",
+ "finishedLate": "Finished {{duration}} after deadline",
+ "label": "Deadline",
+ "met": "Met",
+ "missed": "Missed",
+ "missedCount_one": "{{count}} Missed Deadline",
+ "missedCount_other": "{{count}} Missed Deadlines",
+ "mixedCount": "{{missedCount}} Missed, {{upcomingCount}} Upcoming",
+ "stillRunning": "Still running",
+ "upcoming": "Upcoming",
+ "upcomingCount_one": "{{count}} Upcoming Deadline",
+ "upcomingCount_other": "{{count}} Upcoming Deadlines"
+ },
"extraLinks": "Extra Links",
"grid": {
"buttons": {
@@ -103,6 +129,10 @@
"assetEvent_one": "Created Asset Event",
"assetEvent_other": "Created Asset Events"
},
+ "deadlines": {
+ "showAll": "Show All",
+ "title": "Deadlines"
+ },
"failedLogs": {
"hideLogs": "Hide Logs",
"showLogs": "Show Logs",
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..5ee03b8aff855
--- /dev/null
+++ b/airflow-core/src/airflow/ui/src/pages/Dag/DeadlineAlertsBadge.tsx
@@ -0,0 +1,87 @@
+/*!
+ * 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, Separator, Text, VStack } from "@chakra-ui/react";
+import dayjs from "dayjs";
+import duration from "dayjs/plugin/duration";
+import relativeTime from "dayjs/plugin/relativeTime";
+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";
+
+dayjs.extend(duration);
+dayjs.extend(relativeTime);
+
+const AlertRow = ({ alert }: { readonly alert: DeadlineAlertResponse }) => {
+ const { t: translate } = useTranslation("dag");
+ const reference = translate(`deadlineAlerts.referenceType.${alert.reference_type}`, {
+ defaultValue: alert.reference_type,
+ });
+ const interval = dayjs.duration(alert.interval, "seconds").humanize();
+
+ return (
+
+
+ {translate("deadlineAlerts.completionRule", { interval, reference })}
+ {Boolean(alert.name) && (
+
+ {" "}
+ ({alert.name})
+
+ )}
+
+
+ );
+};
+
+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
+
+
+
+
+
+
+
+ }>
+ {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 99acda13f05b5..651bb2ad79bcb 100644
--- a/airflow-core/src/airflow/ui/src/pages/Dag/Header.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Dag/Header.tsx
@@ -36,6 +36,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;
@@ -131,6 +132,7 @@ export const Header = ({
actions={
dag === undefined ? undefined : (
<>
+
{dag.doc_md === null ? undefined : (
;
+ readonly dagId: string;
+ readonly onClose: () => void;
+ readonly open: boolean;
+};
+
+export const AllDeadlinesModal = ({ alertMap, dagId, onClose, open }: AllDeadlinesModalProps) => {
+ const { t: translate } = useTranslation("dag");
+ const [page, setPage] = useState(1);
+ const offset = (page - 1) * PAGE_LIMIT;
+
+ const { data, error, isLoading } = useDeadlines({
+ dagId,
+ enabled: open,
+ limit: PAGE_LIMIT,
+ offset,
+ });
+
+ const deadlines = data?.deadlines ?? [];
+ const totalEntries = data?.total_entries ?? 0;
+
+ const getAlert = (alertId?: string | null) =>
+ alertId !== undefined && alertId !== null ? alertMap.get(alertId) : undefined;
+
+ const onOpenChange = () => {
+ setPage(1);
+ onClose();
+ };
+
+ return (
+
+
+
+ {translate("overview.deadlines.title")}
+
+
+
+
+ {isLoading ? (
+
+ {Array.from({ length: PAGE_LIMIT }).map((_, idx) => (
+ // eslint-disable-next-line react/no-array-index-key
+
+ ))}
+
+ ) : (
+ }>
+ {deadlines.map((deadline) => (
+
+ ))}
+
+ )}
+
+ {totalEntries > PAGE_LIMIT ? (
+ setPage(event.page)}
+ p={3}
+ page={page}
+ pageSize={PAGE_LIMIT}
+ >
+
+
+
+
+
+
+ ) : undefined}
+
+
+ );
+};
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
new file mode 100644
index 0000000000000..d844c0002deae
--- /dev/null
+++ b/airflow-core/src/airflow/ui/src/pages/Dag/Overview/DagDeadlines.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 { Box, Button, Flex, Heading, Separator, Skeleton, VStack } from "@chakra-ui/react";
+import { useState } from "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 { ErrorAlert } from "src/components/ErrorAlert";
+import { useDeadlines } from "src/queries/useDeadlines";
+
+import { AllDeadlinesModal } from "./AllDeadlinesModal";
+import { DeadlineRow } from "./DeadlineRow";
+
+const LIMIT = 10;
+
+type DagDeadlinesProps = {
+ readonly dagId: string;
+};
+
+export const DagDeadlines = ({ dagId }: DagDeadlinesProps) => {
+ const { t: translate } = useTranslation("dag");
+ const [modalOpen, setModalOpen] = useState(false);
+
+ const { data, error, isLoading } = useDeadlines({ dagId, limit: LIMIT });
+
+ const { data: alertData } = useDeadlinesServiceGetDagDeadlineAlerts({ dagId, limit: 100 });
+
+ const alertMap = new Map();
+
+ for (const alert of alertData?.deadline_alerts ?? []) {
+ alertMap.set(alert.id, alert);
+ }
+
+ const deadlines = data?.deadlines ?? [];
+ const totalEntries = data?.total_entries ?? 0;
+ const hasMore = totalEntries > LIMIT;
+
+ if (!isLoading && error === null && deadlines.length === 0) {
+ return undefined;
+ }
+
+ const getAlert = (alertId?: string | null) =>
+ alertId !== undefined && alertId !== null ? alertMap.get(alertId) : undefined;
+
+ return (
+
+
+
+
+ {translate("overview.deadlines.title")}
+
+
+
+
+ {isLoading ? (
+
+ {Array.from({ length: 3 }).map((_, idx) => (
+ // eslint-disable-next-line react/no-array-index-key
+
+ ))}
+
+ ) : (
+ }>
+ {deadlines.map((deadline) => (
+
+ ))}
+
+ )}
+ {hasMore ? (
+
+ ) : undefined}
+
+
+ setModalOpen(false)}
+ open={modalOpen}
+ />
+
+ );
+};
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..011530f082cc6
--- /dev/null
+++ b/airflow-core/src/airflow/ui/src/pages/Dag/Overview/DeadlineRow.tsx
@@ -0,0 +1,76 @@
+/*!
+ * 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, Link, Text, VStack } from "@chakra-ui/react";
+import dayjs from "dayjs";
+import duration from "dayjs/plugin/duration";
+import relativeTime from "dayjs/plugin/relativeTime";
+import { useTranslation } from "react-i18next";
+import { FiAlertTriangle, FiClock } from "react-icons/fi";
+import { Link as RouterLink } from "react-router-dom";
+
+import type { DeadlineAlertResponse, DeadlineResponse } from "openapi/requests/types.gen";
+import Time from "src/components/Time";
+
+dayjs.extend(duration);
+dayjs.extend(relativeTime);
+
+type DeadlineRowProps = {
+ readonly alert?: DeadlineAlertResponse;
+ readonly deadline: DeadlineResponse;
+};
+
+export const DeadlineRow = ({ alert, deadline }: DeadlineRowProps) => {
+ const { t: translate } = useTranslation("dag");
+
+ const reference = alert
+ ? translate(`deadlineAlerts.referenceType.${alert.reference_type}`, {
+ defaultValue: alert.reference_type,
+ })
+ : undefined;
+ const interval = alert ? dayjs.duration(alert.interval, "seconds").humanize() : undefined;
+
+ return (
+
+
+
+
+ {deadline.missed ? : }
+ {translate(deadline.missed ? "deadlineStatus.missed" : "deadlineStatus.upcoming")}
+
+
+
+ {deadline.dag_run_id}
+
+
+
+ {reference !== undefined && interval !== undefined ? (
+
+ {translate("deadlineAlerts.completionRule", { interval, reference })}
+
+ ) : undefined}
+
+
+
+ {translate("deadlineStatus.expected")}:
+
+
+
+
+ );
+};
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 e3f220eaded4f..0d81a88803166 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";
@@ -149,6 +151,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..cadec7483f056
--- /dev/null
+++ b/airflow-core/src/airflow/ui/src/pages/Run/DeadlineStatus.tsx
@@ -0,0 +1,223 @@
+/*!
+ * 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, Button, HStack, Text, VStack } from "@chakra-ui/react";
+import dayjs from "dayjs";
+import duration from "dayjs/plugin/duration";
+import relativeTime from "dayjs/plugin/relativeTime";
+import { useState } from "react";
+import { useTranslation } from "react-i18next";
+import { FiAlertTriangle, FiCheck, FiClock } from "react-icons/fi";
+
+import { useDeadlinesServiceGetDagDeadlineAlerts, useDeadlinesServiceGetDeadlines } from "openapi/queries";
+import type { DeadlineAlertResponse } from "openapi/requests/types.gen";
+import Time from "src/components/Time";
+import { Tooltip } from "src/components/ui/Tooltip";
+import { renderDuration } from "src/utils/datetimeUtils";
+
+import { DeadlineStatusModal } from "./DeadlineStatusModal";
+
+dayjs.extend(duration);
+dayjs.extend(relativeTime);
+
+type DeadlineStatusProps = {
+ readonly dagId: string;
+ readonly dagRunId: string;
+ readonly endDate: string | null;
+};
+
+export const DeadlineStatus = ({ dagId, dagRunId, endDate }: DeadlineStatusProps) => {
+ const { t: translate } = useTranslation("dag");
+ const [isModalOpen, setIsModalOpen] = useState(false);
+
+ const { data: deadlineData, isLoading: isLoadingDeadlines } = useDeadlinesServiceGetDeadlines({
+ dagId,
+ dagRunId,
+ limit: 10,
+ orderBy: ["deadline_time"],
+ });
+
+ // Used to detect whether the DAG has any deadline alerts at all, so we can show "Met" when there are alerts configured but no active deadline instances.
+ const { data: alertData, isLoading: isLoadingAlerts } = useDeadlinesServiceGetDagDeadlineAlerts({
+ dagId,
+ limit: 100,
+ });
+
+ const alertMap = new Map();
+
+ for (const deadlineAlert of alertData?.deadline_alerts ?? []) {
+ alertMap.set(deadlineAlert.id, deadlineAlert);
+ }
+
+ if (isLoadingDeadlines || isLoadingAlerts) {
+ return undefined;
+ }
+
+ // Active instances for this run; hasAlerts = DAG has deadline alerts configured.
+ const deadlines = deadlineData?.deadlines ?? [];
+ const hasAlerts = (alertData?.total_entries ?? 0) > 0;
+
+ // No deadline alerts configured on the DAG at all
+ if (deadlines.length === 0 && !hasAlerts) {
+ return undefined;
+ }
+
+ // Alerts are configured but no active deadline instances exist therefore all deadlines were met.
+ // Show a tooltip listing each alert's completion rule.
+ if (deadlines.length === 0 && hasAlerts) {
+ const tooltipContent = (
+
+ {(alertData?.deadline_alerts ?? []).map((deadlineAlert) => (
+
+ {translate("deadlineAlerts.completionRule", {
+ interval: dayjs.duration(deadlineAlert.interval, "seconds").humanize(),
+ reference: translate(`deadlineAlerts.referenceType.${deadlineAlert.reference_type}`, {
+ defaultValue: deadlineAlert.reference_type,
+ }),
+ })}
+
+ ))}
+
+ );
+
+ return (
+
+
+
+ {translate("deadlineStatus.met")}
+
+
+ );
+ }
+
+ const totalEntries = deadlineData?.total_entries ?? 0;
+ const runEndDate = endDate ?? undefined;
+
+ // When there are multiple deadline instances, collapse into a single compact badge to
+ // avoid stretching the run header. Clicking opens the modal with full details.
+ // Counts are derived from the loaded page (limit: 10); a single run virtually never has more
+ // than that, but the modal shows the authoritative paginated list either way.
+ if (totalEntries > 1) {
+ const missedCount = deadlines.filter((entry) => entry.missed).length;
+ const upcomingCount = deadlines.length - missedCount;
+ const hasMissed = missedCount > 0;
+ const hasUpcoming = upcomingCount > 0;
+
+ let label: string;
+
+ if (hasMissed && hasUpcoming) {
+ label = translate("deadlineStatus.mixedCount", { missedCount, upcomingCount });
+ } else if (hasMissed) {
+ label = translate("deadlineStatus.missedCount", { count: missedCount });
+ } else {
+ label = translate("deadlineStatus.upcomingCount", { count: upcomingCount });
+ }
+
+ return (
+ <>
+
+ setIsModalOpen(false)}
+ open={isModalOpen}
+ runEndDate={runEndDate}
+ />
+ >
+ );
+ }
+
+ // Single deadline — show inline with Expected / Actual dates and precise duration.
+ const [dl] = deadlines;
+
+ if (dl === undefined) {
+ return undefined;
+ }
+
+ const alert = dl.alert_id !== undefined && dl.alert_id !== null ? alertMap.get(dl.alert_id) : undefined;
+ const deadlineTime = dayjs(dl.deadline_time);
+
+ let actualDurationLabel: string | undefined;
+
+ if (dl.missed && runEndDate !== undefined) {
+ const diff = dayjs(runEndDate).diff(deadlineTime);
+ const dur = renderDuration(Math.abs(diff) / 1000, false);
+
+ if (dur !== undefined) {
+ actualDurationLabel =
+ diff >= 0
+ ? translate("deadlineStatus.finishedLate", { duration: dur })
+ : translate("deadlineStatus.finishedEarly", { duration: dur });
+ }
+ }
+
+ return (
+
+
+
+ {dl.missed ? : }
+ {translate(dl.missed ? "deadlineStatus.missed" : "deadlineStatus.upcoming")}
+
+ {Boolean(dl.alert_name) && (
+
+ ({dl.alert_name})
+
+ )}
+
+ {alert === undefined ? undefined : (
+
+ {translate("deadlineAlerts.completionRule", {
+ interval: dayjs.duration(alert.interval, "seconds").humanize(),
+ reference: translate(`deadlineAlerts.referenceType.${alert.reference_type}`, {
+ defaultValue: alert.reference_type,
+ }),
+ })}
+
+ )}
+
+
+ {translate("deadlineStatus.expected")}:
+
+
+
+
+
+ {translate("deadlineStatus.actual")}:
+
+ {runEndDate === undefined ? (
+
+ {translate("deadlineStatus.stillRunning")}
+
+ ) : (
+
+ )}
+
+ {actualDurationLabel === undefined ? undefined : (
+
+ {actualDurationLabel}
+
+ )}
+
+ );
+};
diff --git a/airflow-core/src/airflow/ui/src/pages/Run/DeadlineStatusModal.tsx b/airflow-core/src/airflow/ui/src/pages/Run/DeadlineStatusModal.tsx
new file mode 100644
index 0000000000000..968f1210d9e82
--- /dev/null
+++ b/airflow-core/src/airflow/ui/src/pages/Run/DeadlineStatusModal.tsx
@@ -0,0 +1,188 @@
+/*!
+ * 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, Heading, HStack, Separator, Skeleton, Text, VStack } from "@chakra-ui/react";
+import dayjs from "dayjs";
+import duration from "dayjs/plugin/duration";
+import relativeTime from "dayjs/plugin/relativeTime";
+import { useState } from "react";
+import { useTranslation } from "react-i18next";
+import { FiAlertTriangle, FiClock } from "react-icons/fi";
+
+import { useDeadlinesServiceGetDeadlines } from "openapi/queries";
+import type { DeadlineAlertResponse } from "openapi/requests/types.gen";
+import { ErrorAlert } from "src/components/ErrorAlert";
+import Time from "src/components/Time";
+import { Dialog } from "src/components/ui";
+import { Pagination } from "src/components/ui/Pagination";
+import { renderDuration } from "src/utils/datetimeUtils";
+
+dayjs.extend(duration);
+dayjs.extend(relativeTime);
+
+const PAGE_LIMIT = 10;
+
+type DeadlineStatusModalProps = {
+ readonly alertMap: Map;
+ readonly dagId: string;
+ readonly dagRunId: string;
+ readonly onClose: () => void;
+ readonly open: boolean;
+ readonly runEndDate: string | undefined;
+};
+
+export const DeadlineStatusModal = ({
+ alertMap,
+ dagId,
+ dagRunId,
+ onClose,
+ open,
+ runEndDate,
+}: DeadlineStatusModalProps) => {
+ const { t: translate } = useTranslation("dag");
+ const [page, setPage] = useState(1);
+ const offset = (page - 1) * PAGE_LIMIT;
+
+ const { data, error, isLoading } = useDeadlinesServiceGetDeadlines(
+ {
+ dagId,
+ dagRunId,
+ limit: PAGE_LIMIT,
+ offset,
+ orderBy: ["deadline_time"],
+ },
+ undefined,
+ { enabled: open },
+ );
+
+ const deadlines = data?.deadlines ?? [];
+ const totalEntries = data?.total_entries ?? 0;
+
+ const onOpenChange = () => {
+ setPage(1);
+ onClose();
+ };
+
+ return (
+
+
+
+ {translate("deadlineStatus.label")}
+
+
+
+
+ {isLoading ? (
+
+ {Array.from({ length: PAGE_LIMIT }).map((_, idx) => (
+ // eslint-disable-next-line react/no-array-index-key
+
+ ))}
+
+ ) : (
+ }>
+ {deadlines.map((dl) => {
+ const alert =
+ dl.alert_id !== undefined && dl.alert_id !== null ? alertMap.get(dl.alert_id) : undefined;
+ const deadlineTime = dayjs(dl.deadline_time);
+
+ let actualDurationLabel: string | undefined;
+
+ if (dl.missed && runEndDate !== undefined) {
+ const diff = dayjs(runEndDate).diff(deadlineTime);
+ const dur = renderDuration(Math.abs(diff) / 1000, false);
+
+ if (dur !== undefined) {
+ actualDurationLabel =
+ diff >= 0
+ ? translate("deadlineStatus.finishedLate", { duration: dur })
+ : translate("deadlineStatus.finishedEarly", { duration: dur });
+ }
+ }
+
+ return (
+
+
+
+ {dl.missed ? : }
+ {translate(dl.missed ? "deadlineStatus.missed" : "deadlineStatus.upcoming")}
+
+ {Boolean(dl.alert_name) && (
+
+ {dl.alert_name}
+
+ )}
+
+ {alert === undefined ? undefined : (
+
+ {translate("deadlineAlerts.completionRule", {
+ interval: dayjs.duration(alert.interval, "seconds").humanize(),
+ reference: translate(`deadlineAlerts.referenceType.${alert.reference_type}`, {
+ defaultValue: alert.reference_type,
+ }),
+ })}
+
+ )}
+
+
+ {translate("deadlineStatus.expected")}:
+
+
+
+
+
+ {translate("deadlineStatus.actual")}:
+
+ {runEndDate === undefined ? (
+
+ {translate("deadlineStatus.stillRunning")}
+
+ ) : (
+
+ )}
+
+ {actualDurationLabel === undefined ? undefined : (
+
+ {actualDurationLabel}
+
+ )}
+
+ );
+ })}
+
+ )}
+
+ {totalEntries > PAGE_LIMIT ? (
+ setPage(event.page)}
+ p={3}
+ page={page}
+ pageSize={PAGE_LIMIT}
+ >
+
+
+
+
+
+
+ ) : undefined}
+
+
+ );
+};
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..6b29ba5187330 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";
@@ -36,6 +37,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);
@@ -43,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,
@@ -139,6 +145,14 @@ export const Header = ({ dagRun }: { readonly dagRun: DAGRunResponse }) => {
/>
),
},
+ ...(hasDeadlineAlerts
+ ? [
+ {
+ label: translate("dag:deadlineStatus.label"),
+ value: ,
+ },
+ ]
+ : []),
]}
title={dagRun.dag_run_id}
/>
diff --git a/airflow-core/src/airflow/ui/src/queries/useDeadlines.ts b/airflow-core/src/airflow/ui/src/queries/useDeadlines.ts
new file mode 100644
index 0000000000000..e7f4b1750d0af
--- /dev/null
+++ b/airflow-core/src/airflow/ui/src/queries/useDeadlines.ts
@@ -0,0 +1,54 @@
+/*!
+ * 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 { useDeadlinesServiceGetDeadlines } from "openapi/queries";
+import { useAutoRefresh } from "src/utils";
+
+type UseDeadlinesParams = {
+ readonly dagId: string;
+ readonly enabled?: boolean;
+ readonly limit: number;
+ readonly offset?: number;
+};
+
+export const useDeadlines = ({ dagId, enabled, limit, offset = 0 }: UseDeadlinesParams) => {
+ const refetchInterval = useAutoRefresh({ dagId });
+
+ return useDeadlinesServiceGetDeadlines(
+ {
+ dagId,
+ dagRunId: "~",
+ limit,
+ offset,
+ orderBy: ["deadline_time"],
+ },
+ undefined,
+ {
+ enabled,
+ refetchInterval: ({ state: { data } }) => {
+ if (data === undefined) {
+ return refetchInterval;
+ }
+ // Stop polling only when every deadline in the full result set is missed
+ const allMissed = data.total_entries > 0 && data.deadlines.every((deadline) => deadline.missed);
+
+ return allMissed ? false : refetchInterval;
+ },
+ },
+ );
+};