This is an automated email from the ASF dual-hosted git repository.

shahar1 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/main by this push:
     new 78b88db5aba Regenerate UI client and fix deadline alerts with no fixed 
interval (#70637)
78b88db5aba is described below

commit 78b88db5aba7e0eb972680bc503df04e52e42d3a
Author: Shahar Epstein <[email protected]>
AuthorDate: Wed Jul 29 08:39:39 2026 +0300

    Regenerate UI client and fix deadline alerts with no fixed interval (#70637)
    
    The deadline-alert response now reports a null interval when the alert
    has no fixed number of seconds, but the committed TypeScript client
    still described it as a required number. That stale client hid a real
    type error in every component rendering the completion rule, so
    `ts-compile-lint-ui` fails on a clean checkout and blocks unrelated PRs.
    
    The null is wrong on screen today as well: dayjs humanizes it as "a few
    seconds", so such an alert claims the run must complete within a few
    seconds of its reference point. There is no duration to name there, so
    the rule now names the reference point alone. English is the source and
    fallback locale, so the wording is added there alone and the other
    locales fall back to it until translated.
---
 .../ui/openapi-gen/queries/ensureQueryData.ts      |  2 +-
 .../src/airflow/ui/openapi-gen/queries/prefetch.ts |  2 +-
 .../src/airflow/ui/openapi-gen/queries/queries.ts  |  2 +-
 .../src/airflow/ui/openapi-gen/queries/suspense.ts |  2 +-
 .../airflow/ui/openapi-gen/requests/schemas.gen.ts | 13 +++-
 .../ui/openapi-gen/requests/services.gen.ts        |  2 +-
 .../airflow/ui/openapi-gen/requests/types.gen.ts   |  6 +-
 .../src/airflow/ui/public/i18n/locales/en/dag.json |  1 +
 .../ui/src/pages/Dag/DeadlineAlertsBadge.test.tsx  | 89 ++++++++++++++++++++++
 .../ui/src/pages/Dag/DeadlineAlertsBadge.tsx       | 13 +---
 .../ui/src/pages/Dag/Overview/DeadlineRow.tsx      | 20 ++---
 .../airflow/ui/src/pages/Run/DeadlineStatus.tsx    | 23 ++----
 .../ui/src/pages/Run/DeadlineStatusModal.tsx       | 16 +---
 .../src/airflow/ui/src/utils/datetimeUtils.test.ts | 17 +++++
 .../src/airflow/ui/src/utils/datetimeUtils.ts      |  7 ++
 .../src/airflow/ui/src/utils/deadlines.test.ts     | 52 +++++++++++++
 airflow-core/src/airflow/ui/src/utils/deadlines.ts | 44 +++++++++++
 17 files changed, 244 insertions(+), 67 deletions(-)

diff --git a/airflow-core/src/airflow/ui/openapi-gen/queries/ensureQueryData.ts 
b/airflow-core/src/airflow/ui/openapi-gen/queries/ensureQueryData.ts
index f435ab90302..d25f9977981 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/queries/ensureQueryData.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/queries/ensureQueryData.ts
@@ -1872,7 +1872,7 @@ export const ensureUseDeadlinesServiceGetDeadlinesData = 
(queryClient: QueryClie
 * @param data.dagId
 * @param data.limit
 * @param data.offset
-* @param data.orderBy Attributes to order by, multi criteria sort is 
supported. Prefix with `-` for descending order. Supported attributes: `id, 
created_at, name, interval`
+* @param data.orderBy Attributes to order by, multi criteria sort is 
supported. Prefix with `-` for descending order. Supported attributes: `id, 
created_at, name`
 * @returns DeadlineAlertCollectionResponse Successful Response
 * @throws ApiError
 */
diff --git a/airflow-core/src/airflow/ui/openapi-gen/queries/prefetch.ts 
b/airflow-core/src/airflow/ui/openapi-gen/queries/prefetch.ts
index a9e1144261a..a15d7bfd4ed 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/queries/prefetch.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/queries/prefetch.ts
@@ -1872,7 +1872,7 @@ export const prefetchUseDeadlinesServiceGetDeadlines = 
(queryClient: QueryClient
 * @param data.dagId
 * @param data.limit
 * @param data.offset
-* @param data.orderBy Attributes to order by, multi criteria sort is 
supported. Prefix with `-` for descending order. Supported attributes: `id, 
created_at, name, interval`
+* @param data.orderBy Attributes to order by, multi criteria sort is 
supported. Prefix with `-` for descending order. Supported attributes: `id, 
created_at, name`
 * @returns DeadlineAlertCollectionResponse Successful Response
 * @throws ApiError
 */
diff --git a/airflow-core/src/airflow/ui/openapi-gen/queries/queries.ts 
b/airflow-core/src/airflow/ui/openapi-gen/queries/queries.ts
index 57a9970cdb7..1eb20ba00fa 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/queries/queries.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/queries/queries.ts
@@ -1872,7 +1872,7 @@ export const useDeadlinesServiceGetDeadlines = <TData = 
Common.DeadlinesServiceG
 * @param data.dagId
 * @param data.limit
 * @param data.offset
-* @param data.orderBy Attributes to order by, multi criteria sort is 
supported. Prefix with `-` for descending order. Supported attributes: `id, 
created_at, name, interval`
+* @param data.orderBy Attributes to order by, multi criteria sort is 
supported. Prefix with `-` for descending order. Supported attributes: `id, 
created_at, name`
 * @returns DeadlineAlertCollectionResponse Successful Response
 * @throws ApiError
 */
diff --git a/airflow-core/src/airflow/ui/openapi-gen/queries/suspense.ts 
b/airflow-core/src/airflow/ui/openapi-gen/queries/suspense.ts
index acd2583f742..dd209e6fb01 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/queries/suspense.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/queries/suspense.ts
@@ -1872,7 +1872,7 @@ export const useDeadlinesServiceGetDeadlinesSuspense = 
<TData = Common.Deadlines
 * @param data.dagId
 * @param data.limit
 * @param data.offset
-* @param data.orderBy Attributes to order by, multi criteria sort is 
supported. Prefix with `-` for descending order. Supported attributes: `id, 
created_at, name, interval`
+* @param data.orderBy Attributes to order by, multi criteria sort is 
supported. Prefix with `-` for descending order. Supported attributes: `id, 
created_at, name`
 * @returns DeadlineAlertCollectionResponse Successful Response
 * @throws ApiError
 */
diff --git a/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts 
b/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts
index 39ef5285f6d..7795ef0983f 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts
@@ -9601,9 +9601,16 @@ export const $DeadlineAlertResponse = {
             title: 'Reference Type'
         },
         interval: {
-            type: 'number',
+            anyOf: [
+                {
+                    type: 'number'
+                },
+                {
+                    type: 'null'
+                }
+            ],
             title: 'Interval',
-            description: 'Interval in seconds between deadline evaluations.'
+            description: 'Interval in seconds between the reference time and 
the deadline. Null for a dynamic interval (e.g. a VariableInterval) whose value 
is only resolved at scheduler evaluation time.'
         },
         created_at: {
             type: 'string',
@@ -9612,7 +9619,7 @@ export const $DeadlineAlertResponse = {
         }
     },
     type: 'object',
-    required: ['id', 'reference_type', 'interval', 'created_at'],
+    required: ['id', 'reference_type', 'created_at'],
     title: 'DeadlineAlertResponse',
     description: 'DeadlineAlert serializer for responses.'
 } as const;
diff --git a/airflow-core/src/airflow/ui/openapi-gen/requests/services.gen.ts 
b/airflow-core/src/airflow/ui/openapi-gen/requests/services.gen.ts
index 94215ebd52e..f353ba627b4 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/requests/services.gen.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/requests/services.gen.ts
@@ -4839,7 +4839,7 @@ export class DeadlinesService {
      * @param data.dagId
      * @param data.limit
      * @param data.offset
-     * @param data.orderBy Attributes to order by, multi criteria sort is 
supported. Prefix with `-` for descending order. Supported attributes: `id, 
created_at, name, interval`
+     * @param data.orderBy Attributes to order by, multi criteria sort is 
supported. Prefix with `-` for descending order. Supported attributes: `id, 
created_at, name`
      * @returns DeadlineAlertCollectionResponse Successful Response
      * @throws ApiError
      */
diff --git a/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts 
b/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts
index 8455306b581..e26a2abdee7 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts
@@ -2433,9 +2433,9 @@ export type DeadlineAlertResponse = {
     name?: string | null;
     reference_type: string;
     /**
-     * Interval in seconds between deadline evaluations.
+     * Interval in seconds between the reference time and the deadline. Null 
for a dynamic interval (e.g. a VariableInterval) whose value is only resolved 
at scheduler evaluation time.
      */
-    interval: number;
+    interval?: number | null;
     created_at: string;
 };
 
@@ -4604,7 +4604,7 @@ export type GetDagDeadlineAlertsData = {
     limit?: number;
     offset?: number;
     /**
-     * Attributes to order by, multi criteria sort is supported. Prefix with 
`-` for descending order. Supported attributes: `id, created_at, name, interval`
+     * Attributes to order by, multi criteria sort is supported. Prefix with 
`-` for descending order. Supported attributes: `id, created_at, name`
      */
     orderBy?: Array<(string)>;
 };
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 907fba9e5ea..f203c21595f 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
@@ -42,6 +42,7 @@
   },
   "deadlineAlerts": {
     "completionRule": "Must complete within {{interval}} of {{reference}}",
+    "completionRuleDynamic": "Must complete within a variable interval 
measured from {{reference}}",
     "count_one": "{{count}} deadline",
     "count_other": "{{count}} deadlines",
     "referenceType": {
diff --git 
a/airflow-core/src/airflow/ui/src/pages/Dag/DeadlineAlertsBadge.test.tsx 
b/airflow-core/src/airflow/ui/src/pages/Dag/DeadlineAlertsBadge.test.tsx
new file mode 100644
index 00000000000..cd1885f3f1a
--- /dev/null
+++ b/airflow-core/src/airflow/ui/src/pages/Dag/DeadlineAlertsBadge.test.tsx
@@ -0,0 +1,89 @@
+/*!
+ * 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 "@testing-library/jest-dom";
+import { fireEvent, render, screen } from "@testing-library/react";
+import { describe, expect, it, vi } from "vitest";
+
+import type * as OpenapiQueries from "openapi/queries";
+import type { DeadlineAlertResponse } from "openapi/requests/types.gen";
+import { Wrapper } from "src/utils/Wrapper";
+
+import { DeadlineAlertsBadge } from "./DeadlineAlertsBadge";
+
+vi.mock("react-i18next", () => ({
+  useTranslation: () => ({
+    // eslint-disable-next-line id-length
+    t: (key: string, options?: { interval?: string; reference?: string }) => {
+      if (options?.reference === undefined) {
+        return key;
+      }
+
+      return options.interval === undefined
+        ? `${key}:${options.reference}`
+        : `${key}:${options.interval}:${options.reference}`;
+    },
+  }),
+}));
+
+vi.mock("openapi/queries", async (importOriginal) => {
+  const actual = await importOriginal<typeof OpenapiQueries>();
+
+  return {
+    ...actual,
+    useDeadlinesServiceGetDagDeadlineAlerts: vi.fn(),
+  };
+});
+
+const { useDeadlinesServiceGetDagDeadlineAlerts } = await 
import("openapi/queries");
+
+// Defaults to a VariableInterval alert, whose interval only the scheduler 
resolves at evaluation
+// time. Without a rule of its own, dayjs humanizes that null interval as "a 
few seconds" and the
+// popover claims the run must complete within a few seconds of its logical 
date.
+const baseAlert: DeadlineAlertResponse = {
+  created_at: "2025-01-01T00:00:00Z",
+  id: "alert-1",
+  interval: null,
+  name: null,
+  reference_type: "DagRunLogicalDateDeadline",
+};
+
+const REFERENCE = "deadlineAlerts.referenceType.DagRunLogicalDateDeadline";
+const DYNAMIC_RULE = `deadlineAlerts.completionRuleDynamic:${REFERENCE}`;
+const FIXED_RULE = `deadlineAlerts.completionRule:an hour:${REFERENCE}`;
+
+describe("DeadlineAlertsBadge", () => {
+  it.each([
+    { absent: FIXED_RULE, expected: DYNAMIC_RULE, interval: null },
+    { absent: DYNAMIC_RULE, expected: FIXED_RULE, interval: 3600 },
+  ])(
+    "states the completion rule for an alert with interval $interval",
+    async ({ absent, expected, interval }) => {
+      vi.mocked(useDeadlinesServiceGetDagDeadlineAlerts).mockReturnValue({
+        data: { deadline_alerts: [{ ...baseAlert, interval }], total_entries: 
1 },
+      } as ReturnType<typeof useDeadlinesServiceGetDagDeadlineAlerts>);
+
+      render(<DeadlineAlertsBadge dagId="test_dag" />, { wrapper: Wrapper });
+
+      fireEvent.click(screen.getByRole("button"));
+
+      expect(await screen.findByText(expected)).toBeInTheDocument();
+      expect(screen.queryByText(absent)).not.toBeInTheDocument();
+    },
+  );
+});
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 5ee03b8aff8..549ee76a14c 100644
--- a/airflow-core/src/airflow/ui/src/pages/Dag/DeadlineAlertsBadge.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Dag/DeadlineAlertsBadge.tsx
@@ -17,30 +17,21 @@
  * 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);
+import { translateCompletionRule } from "src/utils/deadlines";
 
 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 (
     <Box py={2} width="100%">
       <Text color="fg.muted" fontSize="xs">
-        {translate("deadlineAlerts.completionRule", { interval, reference })}
+        {translateCompletionRule(translate, alert)}
         {Boolean(alert.name) && (
           <Text as="span" color="fg.subtle" fontSize="xs">
             {" "}
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
index 3e47861c90d..dff4053b3c1 100644
--- a/airflow-core/src/airflow/ui/src/pages/Dag/Overview/DeadlineRow.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Dag/Overview/DeadlineRow.tsx
@@ -17,18 +17,13 @@
  * under the License.
  */
 import { Badge, HStack, 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 type { DeadlineAlertResponse, DeadlineResponse } from 
"openapi/requests/types.gen";
 import Time from "src/components/Time";
 import { RouterLink } from "src/components/ui";
-
-dayjs.extend(duration);
-dayjs.extend(relativeTime);
+import { translateCompletionRule } from "src/utils/deadlines";
 
 type DeadlineRowProps = {
   readonly alert?: DeadlineAlertResponse;
@@ -38,12 +33,7 @@ type DeadlineRowProps = {
 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;
+  const completionRule = translateCompletionRule(translate, alert);
 
   return (
     <HStack justifyContent="space-between" px={2} py={1.5} width="100%">
@@ -61,11 +51,11 @@ export const DeadlineRow = ({ alert, deadline }: 
DeadlineRowProps) => {
             {deadline.dag_run_id}
           </RouterLink>
         </HStack>
-        {reference !== undefined && interval !== undefined ? (
+        {completionRule === undefined ? undefined : (
           <Text color="fg.muted" fontSize="xs">
-            {translate("deadlineAlerts.completionRule", { interval, reference 
})}
+            {completionRule}
           </Text>
-        ) : undefined}
+        )}
       </VStack>
       <HStack gap={1}>
         <Text color="fg.muted" fontSize="xs">
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 eff38d4a78e..0a38e2d99aa 100644
--- a/airflow-core/src/airflow/ui/src/pages/Run/DeadlineStatus.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Run/DeadlineStatus.tsx
@@ -18,8 +18,6 @@
  */
 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";
@@ -29,12 +27,10 @@ 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 { translateCompletionRule } from "src/utils/deadlines";
 
 import { DeadlineStatusModal } from "./DeadlineStatusModal";
 
-dayjs.extend(duration);
-dayjs.extend(relativeTime);
-
 type DeadlineStatusProps = {
   readonly dagId: string;
   readonly dagRunId: string;
@@ -84,12 +80,7 @@ export const DeadlineStatus = ({ dagId, dagRunId, endDate }: 
DeadlineStatusProps
       <VStack alignItems="flex-start" gap={0.5}>
         {(alertData?.deadline_alerts ?? []).map((deadlineAlert) => (
           <Text fontSize="xs" key={deadlineAlert.id}>
-            {translate("deadlineAlerts.completionRule", {
-              interval: dayjs.duration(deadlineAlert.interval, 
"seconds").humanize(),
-              reference: 
translate(`deadlineAlerts.referenceType.${deadlineAlert.reference_type}`, {
-                defaultValue: deadlineAlert.reference_type,
-              }),
-            })}
+            {translateCompletionRule(translate, deadlineAlert)}
           </Text>
         ))}
       </VStack>
@@ -156,6 +147,7 @@ export const DeadlineStatus = ({ dagId, dagRunId, endDate 
}: DeadlineStatusProps
   }
 
   const alert = dl.alert_id !== undefined && dl.alert_id !== null ? 
alertMap.get(dl.alert_id) : undefined;
+  const completionRule = translateCompletionRule(translate, alert);
   const deadlineTime = dayjs(dl.deadline_time);
 
   let actualDurationLabel: string | undefined;
@@ -185,14 +177,9 @@ export const DeadlineStatus = ({ dagId, dagRunId, endDate 
}: DeadlineStatusProps
           </Text>
         )}
       </HStack>
-      {alert === undefined ? undefined : (
+      {completionRule === undefined ? undefined : (
         <Text color="fg.muted" fontSize="xs">
-          {translate("deadlineAlerts.completionRule", {
-            interval: dayjs.duration(alert.interval, "seconds").humanize(),
-            reference: 
translate(`deadlineAlerts.referenceType.${alert.reference_type}`, {
-              defaultValue: alert.reference_type,
-            }),
-          })}
+          {completionRule}
         </Text>
       )}
       <HStack gap={1}>
diff --git a/airflow-core/src/airflow/ui/src/pages/Run/DeadlineStatusModal.tsx 
b/airflow-core/src/airflow/ui/src/pages/Run/DeadlineStatusModal.tsx
index 968f1210d9e..c5afe117eb4 100644
--- a/airflow-core/src/airflow/ui/src/pages/Run/DeadlineStatusModal.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Run/DeadlineStatusModal.tsx
@@ -18,8 +18,6 @@
  */
 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";
@@ -31,9 +29,7 @@ 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);
+import { translateCompletionRule } from "src/utils/deadlines";
 
 const PAGE_LIMIT = 10;
 
@@ -99,6 +95,7 @@ export const DeadlineStatusModal = ({
               {deadlines.map((dl) => {
                 const alert =
                   dl.alert_id !== undefined && dl.alert_id !== null ? 
alertMap.get(dl.alert_id) : undefined;
+                const completionRule = translateCompletionRule(translate, 
alert);
                 const deadlineTime = dayjs(dl.deadline_time);
 
                 let actualDurationLabel: string | undefined;
@@ -128,14 +125,9 @@ export const DeadlineStatusModal = ({
                         </Text>
                       )}
                     </HStack>
-                    {alert === undefined ? undefined : (
+                    {completionRule === undefined ? undefined : (
                       <Text color="fg.muted" fontSize="xs">
-                        {translate("deadlineAlerts.completionRule", {
-                          interval: dayjs.duration(alert.interval, 
"seconds").humanize(),
-                          reference: 
translate(`deadlineAlerts.referenceType.${alert.reference_type}`, {
-                            defaultValue: alert.reference_type,
-                          }),
-                        })}
+                        {completionRule}
                       </Text>
                     )}
                     <HStack gap={1}>
diff --git a/airflow-core/src/airflow/ui/src/utils/datetimeUtils.test.ts 
b/airflow-core/src/airflow/ui/src/utils/datetimeUtils.test.ts
index 2919167a33f..c5c766af95a 100644
--- a/airflow-core/src/airflow/ui/src/utils/datetimeUtils.test.ts
+++ b/airflow-core/src/airflow/ui/src/utils/datetimeUtils.test.ts
@@ -23,6 +23,7 @@ import { describe, it, expect, vi, beforeAll, afterAll } from 
"vitest";
 import {
   getDuration,
   getDurationTickStep,
+  humanizeSeconds,
   renderCompactDuration,
   renderDuration,
   getRelativeTime,
@@ -170,3 +171,19 @@ describe("getDurationTickStep", () => {
     expect(getDurationTickStep(10_000_000)).toBe(1_250_000);
   });
 });
+
+describe("humanizeSeconds", () => {
+  it.each([
+    [3600, "an hour"],
+    [86_400, "a day"],
+  ])("humanizes %s seconds as %s", (seconds, expected) => {
+    expect(humanizeSeconds(seconds)).toBe(expected);
+  });
+
+  it.each([[null], [undefined], [Number.NaN], [Number.POSITIVE_INFINITY]])(
+    "returns undefined without a finite interval (%s)",
+    (seconds) => {
+      expect(humanizeSeconds(seconds)).toBeUndefined();
+    },
+  );
+});
diff --git a/airflow-core/src/airflow/ui/src/utils/datetimeUtils.ts 
b/airflow-core/src/airflow/ui/src/utils/datetimeUtils.ts
index 3a73c29459f..20e129d0645 100644
--- a/airflow-core/src/airflow/ui/src/utils/datetimeUtils.ts
+++ b/airflow-core/src/airflow/ui/src/utils/datetimeUtils.ts
@@ -55,6 +55,13 @@ export const renderDuration = (
   return duration.asSeconds() < 86_400 ? duration.format("HH:mm:ss") : 
duration.format("D[d]HH:mm:ss");
 };
 
+// dayjs humanizes a missing or non-finite input as "a few seconds", so 
callers with no duration
+// to name get undefined instead of a made-up one.
+export const humanizeSeconds = (seconds: number | null | undefined): string | 
undefined =>
+  typeof seconds === "number" && Number.isFinite(seconds)
+    ? dayjs.duration(seconds, "seconds").humanize()
+    : undefined;
+
 // Chart axes need whole units at a glance; HH:mm:ss forces the reader to 
decode
 // every tick to work out the magnitude.
 export const renderCompactDuration = (durationSeconds: number): string => {
diff --git a/airflow-core/src/airflow/ui/src/utils/deadlines.test.ts 
b/airflow-core/src/airflow/ui/src/utils/deadlines.test.ts
new file mode 100644
index 00000000000..fdd65885bb3
--- /dev/null
+++ b/airflow-core/src/airflow/ui/src/utils/deadlines.test.ts
@@ -0,0 +1,52 @@
+/*!
+ * 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 { TFunction } from "i18next";
+import { describe, expect, it } from "vitest";
+
+import type { DeadlineAlertResponse } from "openapi/requests/types.gen";
+
+import { translateCompletionRule } from "./deadlines";
+
+const translate = ((key: string, options?: { interval?: string; reference?: 
string }) =>
+  options?.reference === undefined
+    ? key
+    : `${key}:${options.interval ?? ""}:${options.reference}`) as unknown as 
TFunction;
+
+const baseAlert: DeadlineAlertResponse = {
+  created_at: "2025-01-01T00:00:00Z",
+  id: "alert-1",
+  interval: null,
+  name: null,
+  reference_type: "DagRunLogicalDateDeadline",
+};
+
+const REFERENCE = "deadlineAlerts.referenceType.DagRunLogicalDateDeadline";
+
+describe("translateCompletionRule", () => {
+  it.each([
+    [3600, `deadlineAlerts.completionRule:an hour:${REFERENCE}`],
+    [null, `deadlineAlerts.completionRuleDynamic::${REFERENCE}`],
+  ])("names the rule for an interval of %s seconds", (interval, expected) => {
+    expect(translateCompletionRule(translate, { ...baseAlert, interval 
})).toBe(expected);
+  });
+
+  it("has no rule to name without an alert", () => {
+    expect(translateCompletionRule(translate, undefined)).toBeUndefined();
+  });
+});
diff --git a/airflow-core/src/airflow/ui/src/utils/deadlines.ts 
b/airflow-core/src/airflow/ui/src/utils/deadlines.ts
new file mode 100644
index 00000000000..96c86065d7c
--- /dev/null
+++ b/airflow-core/src/airflow/ui/src/utils/deadlines.ts
@@ -0,0 +1,44 @@
+/*!
+ * 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 { TFunction } from "i18next";
+
+import type { DeadlineAlertResponse } from "openapi/requests/types.gen";
+import { humanizeSeconds } from "src/utils/datetimeUtils";
+
+// The API sends a null interval when the alert has no fixed number of seconds 
— a variable
+// interval the scheduler only resolves per run, or a stored value it could 
not read as a
+// duration. Naming a length there would be a lie, so the rule only names the 
reference point.
+// `translate` must be bound to the "dag" namespace.
+export const translateCompletionRule = (
+  translate: TFunction,
+  alert: DeadlineAlertResponse | undefined,
+): string | undefined => {
+  if (alert === undefined) {
+    return undefined;
+  }
+
+  const reference = 
translate(`deadlineAlerts.referenceType.${alert.reference_type}`, {
+    defaultValue: alert.reference_type,
+  });
+  const interval = humanizeSeconds(alert.interval);
+
+  return interval === undefined
+    ? translate("deadlineAlerts.completionRuleDynamic", { reference })
+    : translate("deadlineAlerts.completionRule", { interval, reference });
+};

Reply via email to