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

pierrejeambrun 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 14ab6a03ca7 Add Deadline in Dashboard Page (#68038)
14ab6a03ca7 is described below

commit 14ab6a03ca7e0fc6e07c94023eb7992f17416c5a
Author: Richard <[email protected]>
AuthorDate: Mon Jun 15 04:11:36 2026 -0700

    Add Deadline in Dashboard Page (#68038)
    
    * Add dashboard deadline monitoring
    
    * Center align "Show More" link in DeadlineSection component
    
    * Reorder Dashboard components for improved layout
    
    * Enhance Dashboard with time range selection for deadlines and historical 
metrics
    
    - Introduced TimeRangeSelector component to allow users to specify start 
and end dates for deadlines.
    - Updated DashboardDeadlines and HistoricalMetrics components to accept 
startDate and endDate props.
    - Refactored useDashboardDeadlines to utilize new date parameters for 
fetching deadlines.
    - Added DeadlineItem and DeadlineSection components for better organization 
of deadline display.
    - Improved localization for recently missed deadlines in dashboard.json.
    
    * fix import error
    
    * Enhance Dashboard Deadlines UI with loading states
    
    - Added loading state handling for pending and missed deadlines in the 
DashboardDeadlines component.
    - Updated DeadlineSection to display skeleton loaders when data is being 
fetched.
    - Removed unused subtitle prop from DeadlineSection for cleaner code.
    
    * Remove duplicate missed deadlines stats card
    
    * UI: Remove mouse focus outline from dashboard deadline links
    
    * move time range selctor
    
    * emoving unused translation keys
---
 .../ui/public/i18n/locales/en/dashboard.json       |  12 +++
 .../airflow/ui/src/pages/Dashboard/Dashboard.tsx   |  24 ++++-
 .../Dashboard/Deadlines/DashboardDeadlines.tsx     | 114 +++++++++++++++++++++
 .../src/pages/Dashboard/Deadlines/DeadlineItem.tsx |  53 ++++++++++
 .../pages/Dashboard/Deadlines/DeadlineSection.tsx  |  85 +++++++++++++++
 .../ui/src/pages/Dashboard/Deadlines/index.ts      |  20 ++++
 .../HistoricalMetrics/HistoricalMetrics.tsx        |  19 +---
 .../src/pages/Dashboard/useDashboardDeadlines.ts   |  69 +++++++++++++
 8 files changed, 381 insertions(+), 15 deletions(-)

diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/en/dashboard.json 
b/airflow-core/src/airflow/ui/public/i18n/locales/en/dashboard.json
index 224bd8d8575..9fe1e0f21bd 100644
--- a/airflow-core/src/airflow/ui/public/i18n/locales/en/dashboard.json
+++ b/airflow-core/src/airflow/ui/public/i18n/locales/en/dashboard.json
@@ -1,4 +1,16 @@
 {
+  "deadlines": {
+    "pending": {
+      "empty": "No upcoming deadlines",
+      "title": "Upcoming"
+    },
+    "recentlyMissed": {
+      "empty": "No missed deadlines",
+      "title": "Missed Deadlines"
+    },
+    "showMore": "Show more",
+    "title": "Deadlines"
+  },
   "deferredSlotsNotCounted": "Deferred not counted in slots: {{count}}",
   "deferredSlotsNotCountedTooltip": "Deferred tasks shown in the bar are 
counted against pool slots. Deferred tasks shown below the bar are from pools 
that do not count deferred tasks against slots.",
   "favorite": {
diff --git a/airflow-core/src/airflow/ui/src/pages/Dashboard/Dashboard.tsx 
b/airflow-core/src/airflow/ui/src/pages/Dashboard/Dashboard.tsx
index e04c4fb8f77..969d9e8787b 100644
--- a/airflow-core/src/airflow/ui/src/pages/Dashboard/Dashboard.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Dashboard/Dashboard.tsx
@@ -17,26 +17,36 @@
  * under the License.
  */
 import { Box, Heading, VStack } from "@chakra-ui/react";
+import dayjs from "dayjs";
+import { useState } from "react";
 import { useTranslation } from "react-i18next";
 
 import { usePluginServiceGetPlugins } from "openapi/queries";
 import type { ReactAppResponse, UIAlert } from "openapi/requests/types.gen";
 import ReactMarkdown from "src/components/ReactMarkdown";
+import TimeRangeSelector from "src/components/TimeRangeSelector";
 import { Accordion, Alert } from "src/components/ui";
 import { useConfig } from "src/queries/useConfig";
 
 import { ReactPlugin } from "../ReactPlugin";
+import { DashboardDeadlines } from "./Deadlines";
 import { FavoriteDags } from "./FavoriteDags";
 import { Health } from "./Health";
 import { HistoricalMetrics } from "./HistoricalMetrics";
 import { PoolSummary } from "./PoolSummary";
 import { Stats } from "./Stats";
 
+const defaultHour = "24";
+
 export const Dashboard = () => {
   const alerts = useConfig("dashboard_alert") as Array<UIAlert>;
   const { t: translate } = useTranslation("dashboard");
   const instanceName = useConfig("instance_name");
 
+  const now = dayjs();
+  const [startDate, setStartDate] = useState(now.subtract(Number(defaultHour), 
"hour").toISOString());
+  const [endDate, setEndDate] = useState(now.toISOString());
+
   const { data: pluginData } = usePluginServiceGetPlugins();
 
   const dashboardReactPlugins =
@@ -86,7 +96,19 @@ export const Dashboard = () => {
           <PoolSummary />
         </Box>
         <Box order={6}>
-          <HistoricalMetrics />
+          <TimeRangeSelector
+            defaultValue={defaultHour}
+            endDate={endDate}
+            setEndDate={setEndDate}
+            setStartDate={setStartDate}
+            startDate={startDate}
+          />
+        </Box>
+        <Box order={7}>
+          <DashboardDeadlines endDate={endDate} startDate={startDate} />
+        </Box>
+        <Box order={8}>
+          <HistoricalMetrics endDate={endDate} startDate={startDate} />
         </Box>
         {dashboardReactPlugins.map((plugin) => (
           <ReactPlugin key={plugin.name} reactApp={plugin} />
diff --git 
a/airflow-core/src/airflow/ui/src/pages/Dashboard/Deadlines/DashboardDeadlines.tsx
 
b/airflow-core/src/airflow/ui/src/pages/Dashboard/Deadlines/DashboardDeadlines.tsx
new file mode 100644
index 00000000000..4c5a52c0e24
--- /dev/null
+++ 
b/airflow-core/src/airflow/ui/src/pages/Dashboard/Deadlines/DashboardDeadlines.tsx
@@ -0,0 +1,114 @@
+/*!
+ * 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, Flex, Heading } from "@chakra-ui/react";
+import dayjs from "dayjs";
+import { useTranslation } from "react-i18next";
+import { FiClock } from "react-icons/fi";
+
+import { ErrorAlert } from "src/components/ErrorAlert";
+import { SearchParamsKeys } from "src/constants/searchParams";
+import { useAutoRefresh } from "src/utils";
+
+import { useDashboardMissedDeadlines, useDashboardPendingDeadlines } from 
"../useDashboardDeadlines";
+import { DeadlineSection } from "./DeadlineSection";
+
+type DashboardDeadlinesProps = {
+  readonly endDate: string;
+  readonly startDate: string;
+};
+
+const buildDeadlinesPath = (params: Record<string, string>) => {
+  const searchParams = new URLSearchParams(params);
+
+  return `/deadlines?${searchParams.toString()}`;
+};
+
+export const DashboardDeadlines = ({ endDate, startDate }: 
DashboardDeadlinesProps) => {
+  const { t: translate } = useTranslation("dashboard");
+  const refetchInterval = useAutoRefresh({ checkPendingRuns: true });
+
+  const {
+    data: pendingData,
+    error: pendingError,
+    isLoading: isPendingLoading,
+  } = useDashboardPendingDeadlines({ refetchInterval });
+  const {
+    data: missedData,
+    error: missedError,
+    isLoading: isMissedLoading,
+  } = useDashboardMissedDeadlines({ endDate, refetchInterval, startDate });
+
+  const pendingDeadlines = pendingData?.deadlines ?? [];
+  const missedDeadlines = missedData?.deadlines ?? [];
+
+  const now = dayjs().toISOString();
+  const pendingDeadlinesPath = buildDeadlinesPath({
+    [SearchParamsKeys.DEADLINE_TIME_GTE]: now,
+    [SearchParamsKeys.MISSED]: "false",
+  });
+  const missedDeadlinesPath = buildDeadlinesPath({
+    [SearchParamsKeys.DEADLINE_TIME_GTE]: startDate,
+    [SearchParamsKeys.DEADLINE_TIME_LTE]: endDate,
+    [SearchParamsKeys.MISSED]: "true",
+  });
+
+  const hasNoDeadlines =
+    !Boolean(pendingError) &&
+    !Boolean(missedError) &&
+    !isPendingLoading &&
+    !isMissedLoading &&
+    (pendingData?.total_entries ?? 0) === 0 &&
+    (missedData?.total_entries ?? 0) === 0;
+
+  if (hasNoDeadlines) {
+    return undefined;
+  }
+
+  return (
+    <Box>
+      <Flex alignItems="center" color="fg.muted" my={2}>
+        <FiClock />
+        <Heading ml={1} size="xs">
+          {translate("deadlines.title")}
+        </Heading>
+      </Flex>
+      <ErrorAlert error={pendingError ?? missedError} />
+      <Flex flexDirection={{ base: "column", md: "row" }} gap={4}>
+        <DeadlineSection
+          deadlines={pendingDeadlines}
+          emptyLabel={translate("deadlines.pending.empty")}
+          isLoading={isPendingLoading}
+          showMoreLabel={translate("deadlines.showMore")}
+          showMoreTo={pendingDeadlinesPath}
+          title={translate("deadlines.pending.title")}
+          totalEntries={pendingData?.total_entries ?? 0}
+        />
+        <DeadlineSection
+          deadlines={missedDeadlines}
+          emptyLabel={translate("deadlines.recentlyMissed.empty")}
+          isLoading={isMissedLoading}
+          showMoreLabel={translate("deadlines.showMore")}
+          showMoreTo={missedDeadlinesPath}
+          title={translate("deadlines.recentlyMissed.title")}
+          totalEntries={missedData?.total_entries ?? 0}
+        />
+      </Flex>
+    </Box>
+  );
+};
diff --git 
a/airflow-core/src/airflow/ui/src/pages/Dashboard/Deadlines/DeadlineItem.tsx 
b/airflow-core/src/airflow/ui/src/pages/Dashboard/Deadlines/DeadlineItem.tsx
new file mode 100644
index 00000000000..d1395f97243
--- /dev/null
+++ b/airflow-core/src/airflow/ui/src/pages/Dashboard/Deadlines/DeadlineItem.tsx
@@ -0,0 +1,53 @@
+/*!
+ * 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, HStack, Link, 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";
+import { TruncatedText } from "src/components/TruncatedText";
+
+type DeadlineItemProps = {
+  readonly deadline: DeadlineResponse;
+};
+
+const focusStyles = {
+  _focus: { outline: "none" },
+  _focusVisible: { outline: "2px solid", outlineColor: "brand.focusRing", 
outlineOffset: "2px" },
+};
+
+export const DeadlineItem = ({ deadline }: DeadlineItemProps) => (
+  <HStack justify="space-between" px={3} py={2} width="100%">
+    <VStack align="start" gap={0} minWidth={0} overflow="hidden">
+      <Link asChild color="fg.info" fontSize="sm" fontWeight="medium" 
{...focusStyles}>
+        <RouterLink to={`/dags/${deadline.dag_id}`}>
+          <TruncatedText text={deadline.dag_id} />
+        </RouterLink>
+      </Link>
+      <Link asChild color="fg.muted" fontSize="xs" {...focusStyles}>
+        <RouterLink 
to={`/dags/${deadline.dag_id}/runs/${deadline.dag_run_id}`}>
+          <TruncatedText text={deadline.dag_run_id} />
+        </RouterLink>
+      </Link>
+    </VStack>
+    <Box color="fg.muted" flexShrink={0} fontSize="xs">
+      <Time datetime={deadline.deadline_time} />
+    </Box>
+  </HStack>
+);
diff --git 
a/airflow-core/src/airflow/ui/src/pages/Dashboard/Deadlines/DeadlineSection.tsx 
b/airflow-core/src/airflow/ui/src/pages/Dashboard/Deadlines/DeadlineSection.tsx
new file mode 100644
index 00000000000..aab67617399
--- /dev/null
+++ 
b/airflow-core/src/airflow/ui/src/pages/Dashboard/Deadlines/DeadlineSection.tsx
@@ -0,0 +1,85 @@
+/*!
+ * 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, Flex, Heading, HStack, Link, Separator, Skeleton, Text, VStack } 
from "@chakra-ui/react";
+import { Link as RouterLink } from "react-router-dom";
+
+import type { DeadlineResponse } from "openapi/requests/types.gen";
+
+import { DeadlineItem } from "./DeadlineItem";
+
+type DeadlineSectionProps = {
+  readonly deadlines: Array<DeadlineResponse>;
+  readonly emptyLabel: string;
+  readonly isLoading: boolean;
+  readonly showMoreLabel: string;
+  readonly showMoreTo: string;
+  readonly title: string;
+  readonly totalEntries: number;
+};
+
+export const DeadlineSection = ({
+  deadlines,
+  emptyLabel,
+  isLoading,
+  showMoreLabel,
+  showMoreTo,
+  title,
+  totalEntries,
+}: DeadlineSectionProps) => {
+  const hasMoreDeadlines = totalEntries > deadlines.length;
+
+  return (
+    <Box flex={1} minWidth={0}>
+      <Box borderRadius="md" borderWidth="1px" overflow="hidden">
+        <HStack justify="space-between" px={3} py={2}>
+          <Heading color="fg.muted" size="xs">
+            {title}
+          </Heading>
+        </HStack>
+        <Separator />
+        {isLoading ? (
+          <VStack align="stretch" data-testid="deadline-section-skeleton" 
gap={2} px={3} py={3}>
+            <Skeleton height={4} width="85%" />
+            <Skeleton height={4} width="65%" />
+          </VStack>
+        ) : deadlines.length === 0 ? (
+          <Text color="fg.muted" fontSize="sm" px={3} py={3} 
textAlign="center">
+            {emptyLabel}
+          </Text>
+        ) : (
+          <VStack align="stretch" gap={0} separator={<Separator />}>
+            {deadlines.map((deadline) => (
+              <DeadlineItem deadline={deadline} key={deadline.id} />
+            ))}
+          </VStack>
+        )}
+        {hasMoreDeadlines ? (
+          <>
+            <Separator />
+            <Flex justify="center" px={3} py={2}>
+              <Link asChild color="fg.info" fontSize="xs" fontWeight="medium">
+                <RouterLink to={showMoreTo}>{showMoreLabel}</RouterLink>
+              </Link>
+            </Flex>
+          </>
+        ) : undefined}
+      </Box>
+    </Box>
+  );
+};
diff --git a/airflow-core/src/airflow/ui/src/pages/Dashboard/Deadlines/index.ts 
b/airflow-core/src/airflow/ui/src/pages/Dashboard/Deadlines/index.ts
new file mode 100644
index 00000000000..b45551a0c25
--- /dev/null
+++ b/airflow-core/src/airflow/ui/src/pages/Dashboard/Deadlines/index.ts
@@ -0,0 +1,20 @@
+/*!
+ * 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.
+ */
+
+export { DashboardDeadlines } from "./DashboardDeadlines";
diff --git 
a/airflow-core/src/airflow/ui/src/pages/Dashboard/HistoricalMetrics/HistoricalMetrics.tsx
 
b/airflow-core/src/airflow/ui/src/pages/Dashboard/HistoricalMetrics/HistoricalMetrics.tsx
index 6609bc89da2..d193522beeb 100644
--- 
a/airflow-core/src/airflow/ui/src/pages/Dashboard/HistoricalMetrics/HistoricalMetrics.tsx
+++ 
b/airflow-core/src/airflow/ui/src/pages/Dashboard/HistoricalMetrics/HistoricalMetrics.tsx
@@ -17,7 +17,6 @@
  * under the License.
  */
 import { Box, VStack, SimpleGrid, GridItem, Flex, Heading } from 
"@chakra-ui/react";
-import dayjs from "dayjs";
 import { useState } from "react";
 import { useTranslation } from "react-i18next";
 import { PiBooks } from "react-icons/pi";
@@ -25,20 +24,19 @@ import { PiBooks } from "react-icons/pi";
 import { useAssetServiceGetAssetEvents, useDashboardServiceHistoricalMetrics } 
from "openapi/queries";
 import { AssetEvents } from "src/components/Assets/AssetEvents";
 import { ErrorAlert } from "src/components/ErrorAlert";
-import TimeRangeSelector from "src/components/TimeRangeSelector";
 import { useAutoRefresh } from "src/utils";
 
 import { DagRunMetrics } from "./DagRunMetrics";
 import { MetricSectionSkeleton } from "./MetricSectionSkeleton";
 import { TaskInstanceMetrics } from "./TaskInstanceMetrics";
 
-const defaultHour = "24";
+type HistoricalMetricsProps = {
+  readonly endDate: string;
+  readonly startDate: string;
+};
 
-export const HistoricalMetrics = () => {
+export const HistoricalMetrics = ({ endDate, startDate }: 
HistoricalMetricsProps) => {
   const { t: translate } = useTranslation("dashboard");
-  const now = dayjs();
-  const [startDate, setStartDate] = useState(now.subtract(Number(defaultHour), 
"hour").toISOString());
-  const [endDate, setEndDate] = useState(now.toISOString());
   const [assetSortBy, setAssetSortBy] = useState("-timestamp");
 
   const refetchInterval = useAutoRefresh({ checkPendingRuns: true });
@@ -70,13 +68,6 @@ export const HistoricalMetrics = () => {
       </Flex>
       <ErrorAlert error={error} />
       <VStack alignItems="left" gap={2}>
-        <TimeRangeSelector
-          defaultValue={defaultHour}
-          endDate={endDate}
-          setEndDate={setEndDate}
-          setStartDate={setStartDate}
-          startDate={startDate}
-        />
         <SimpleGrid columns={{ base: 1, lg: 10 }} gap={2}>
           <GridItem colSpan={{ base: 1, lg: 7 }}>
             {isLoading ? <MetricSectionSkeleton /> : undefined}
diff --git 
a/airflow-core/src/airflow/ui/src/pages/Dashboard/useDashboardDeadlines.ts 
b/airflow-core/src/airflow/ui/src/pages/Dashboard/useDashboardDeadlines.ts
new file mode 100644
index 00000000000..cda154a49d1
--- /dev/null
+++ b/airflow-core/src/airflow/ui/src/pages/Dashboard/useDashboardDeadlines.ts
@@ -0,0 +1,69 @@
+/*!
+ * 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 dayjs from "dayjs";
+
+import { useDeadlinesServiceGetDeadlines } from "openapi/queries";
+
+export const DASHBOARD_DEADLINE_LIMIT = 5;
+
+type DashboardPendingDeadlinesQueryOptions = {
+  readonly refetchInterval: number | false;
+};
+
+type DashboardMissedDeadlinesQueryOptions = {
+  readonly endDate: string;
+  readonly refetchInterval: number | false;
+  readonly startDate: string;
+};
+
+export const useDashboardPendingDeadlines = ({ refetchInterval }: 
DashboardPendingDeadlinesQueryOptions) => {
+  const now = dayjs().startOf("minute").toISOString();
+
+  return useDeadlinesServiceGetDeadlines(
+    {
+      dagId: "~",
+      dagRunId: "~",
+      deadlineTimeGt: now,
+      limit: DASHBOARD_DEADLINE_LIMIT,
+      missed: false,
+      orderBy: ["deadline_time"],
+    },
+    undefined,
+    { refetchInterval },
+  );
+};
+
+export const useDashboardMissedDeadlines = ({
+  endDate,
+  refetchInterval,
+  startDate,
+}: DashboardMissedDeadlinesQueryOptions) =>
+  useDeadlinesServiceGetDeadlines(
+    {
+      dagId: "~",
+      dagRunId: "~",
+      deadlineTimeGte: startDate,
+      deadlineTimeLt: endDate,
+      limit: DASHBOARD_DEADLINE_LIMIT,
+      missed: true,
+      orderBy: ["-deadline_time"],
+    },
+    undefined,
+    { refetchInterval },
+  );

Reply via email to