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; + }, + }, + ); +};