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