From bcbe7222cd50af0db7c95ce883c9d807ea912f46 Mon Sep 17 00:00:00 2001 From: Deepak Jain Date: Sat, 28 Mar 2026 10:20:11 -0700 Subject: [PATCH 1/5] Show deactivated DAG state in UI (#63800) --- .../ui/public/i18n/locales/en/dag.json | 3 + .../ui/src/components/DagDeactivatedBadge.tsx | 26 ++++++++ .../ui/src/layouts/Details/DagBreadcrumb.tsx | 17 +++-- .../ui/src/layouts/Details/DetailsLayout.tsx | 8 +-- .../airflow/ui/src/pages/Dag/Header.test.tsx | 62 +++++++++++++++++++ .../src/airflow/ui/src/pages/Dag/Header.tsx | 28 ++++++--- 6 files changed, 124 insertions(+), 20 deletions(-) create mode 100644 airflow-core/src/airflow/ui/src/components/DagDeactivatedBadge.tsx create mode 100644 airflow-core/src/airflow/ui/src/pages/Dag/Header.test.tsx 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 58626c2494248..9477d1f981af3 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 @@ -52,6 +52,9 @@ "buttons": { "advanced": "Advanced", "dagDocs": "Dag Docs" + }, + "status": { + "deactivated": "Deactivated" } }, "logs": { diff --git a/airflow-core/src/airflow/ui/src/components/DagDeactivatedBadge.tsx b/airflow-core/src/airflow/ui/src/components/DagDeactivatedBadge.tsx new file mode 100644 index 0000000000000..ad915b256bee5 --- /dev/null +++ b/airflow-core/src/airflow/ui/src/components/DagDeactivatedBadge.tsx @@ -0,0 +1,26 @@ +/*! + * 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 } from "@chakra-ui/react"; +import { useTranslation } from "react-i18next"; + +export const DagDeactivatedBadge = () => { + const { t: translate } = useTranslation("dag"); + + return {translate("header.status.deactivated")}; +}; diff --git a/airflow-core/src/airflow/ui/src/layouts/Details/DagBreadcrumb.tsx b/airflow-core/src/airflow/ui/src/layouts/Details/DagBreadcrumb.tsx index d877b4daa1b9b..326ef695b83d4 100644 --- a/airflow-core/src/airflow/ui/src/layouts/Details/DagBreadcrumb.tsx +++ b/airflow-core/src/airflow/ui/src/layouts/Details/DagBreadcrumb.tsx @@ -27,6 +27,7 @@ import { useTaskServiceGetTask, } from "openapi/queries"; import { BreadcrumbStats } from "src/components/BreadcrumbStats"; +import { DagDeactivatedBadge } from "src/components/DagDeactivatedBadge"; import { StateBadge } from "src/components/StateBadge"; import { TogglePause } from "src/components/TogglePause"; import { isStatePending, useAutoRefresh } from "src/utils"; @@ -66,12 +67,16 @@ export const DagBreadcrumb = () => { { label: dag?.dag_display_name ?? dagId, labelExtra: ( - + dag?.is_stale ? ( + + ) : ( + + ) ), title: translate("dag_one"), value: `/dags/${dagId}`, diff --git a/airflow-core/src/airflow/ui/src/layouts/Details/DetailsLayout.tsx b/airflow-core/src/airflow/ui/src/layouts/Details/DetailsLayout.tsx index dd559e0ac5e85..57aeb166bdc1a 100644 --- a/airflow-core/src/airflow/ui/src/layouts/Details/DetailsLayout.tsx +++ b/airflow-core/src/airflow/ui/src/layouts/Details/DetailsLayout.tsx @@ -78,7 +78,7 @@ export const DetailsLayout = ({ children, error, isLoading, tabs }: Props) => { const [defaultDagView] = useLocalStorage<"graph" | "grid">(DEFAULT_DAG_VIEW_KEY, "grid"); const panelGroupRef = useRef(null); const [dagView, setDagView] = useLocalStorage<"graph" | "grid">(dagViewKey(dagId), defaultDagView); - const [limit, setLimit] = useLocalStorage(dagRunsLimitKey(dagId), 10); + const [limit, setLimit] = useLocalStorage(dagRunsLimitKey(dagId), 10); const [runAfterGte, setRunAfterGte] = useLocalStorage(runAfterGteKey(dagId), undefined); const [runAfterLte, setRunAfterLte] = useLocalStorage(runAfterLteKey(dagId), undefined); const [runTypeFilter, setRunTypeFilter] = useLocalStorage( @@ -94,9 +94,9 @@ export const DetailsLayout = ({ children, error, isLoading, tabs }: Props) => { undefined, ); - const [showGantt, setShowGantt] = useLocalStorage(showGanttKey(dagId), false); + const [showGantt, setShowGantt] = useLocalStorage(showGanttKey(dagId), false); // Global setting: applies to all Dags (intentionally not scoped to dagId) - const [showVersionIndicatorMode, setShowVersionIndicatorMode] = useLocalStorage( + const [showVersionIndicatorMode, setShowVersionIndicatorMode] = useLocalStorage( `version_indicator_display_mode`, VersionIndicatorOptions.ALL, ); @@ -114,7 +114,7 @@ export const DetailsLayout = ({ children, error, isLoading, tabs }: Props) => { - {dag === undefined ? undefined : ( + {dag === undefined || dag.is_stale ? undefined : ( { + it("shows a deactivated badge and hides the next run stat for stale dags", () => { + render( + +
+ , + ); + + expect(screen.getByText("header.status.deactivated")).toBeInTheDocument(); + expect(screen.queryByText("dagDetails.nextRun")).not.toBeInTheDocument(); + }); +}); 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 6af9129a31641..7ac406b3e342f 100644 --- a/airflow-core/src/airflow/ui/src/pages/Dag/Header.tsx +++ b/airflow-core/src/airflow/ui/src/pages/Dag/Header.tsx @@ -24,6 +24,7 @@ import { useParams, Link as RouterLink } from "react-router-dom"; import type { DAGDetailsResponse, DagRunState } from "openapi/requests/types.gen"; import { DagIcon } from "src/assets/DagIcon"; import { DeleteDagButton } from "src/components/DagActions/DeleteDagButton"; +import { DagDeactivatedBadge } from "src/components/DagDeactivatedBadge"; import { FavoriteDagButton } from "src/components/DagActions/FavoriteDagButton"; import { ParseDagButton } from "src/components/DagActions/ParseDagButton"; import DagRunInfo from "src/components/DagRunInfo"; @@ -88,15 +89,6 @@ export const Header = ({ ) : undefined, }, - { - label: translate("dagDetails.nextRun"), - value: Boolean(dag?.next_dagrun_run_after) ? ( - - ) : undefined, - }, { label: translate("dagDetails.maxActiveRuns"), value: @@ -118,6 +110,18 @@ export const Header = ({ }, ]; + if (!dag?.is_stale) { + stats.splice(2, 0, { + label: translate("dagDetails.nextRun"), + value: Boolean(dag?.next_dagrun_run_after) ? ( + + ) : undefined, + }); + } + return ( } stats={stats} subTitle={ - dag !== undefined && ( + dag?.is_stale ? ( + + ) : ( + dag !== undefined && ( + ) ) } title={dag?.dag_display_name ?? dagId} From eaf9c908d1f3e5f0471b8d71bc6dcb36d06b6426 Mon Sep 17 00:00:00 2001 From: dejain Date: Sat, 28 Mar 2026 18:38:14 -0700 Subject: [PATCH 2/5] Fix DAG header test typing after rebase (#63800) --- airflow-core/src/airflow/ui/src/pages/Dag/Header.test.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/airflow-core/src/airflow/ui/src/pages/Dag/Header.test.tsx b/airflow-core/src/airflow/ui/src/pages/Dag/Header.test.tsx index 657c2f8d6051b..a79004bbb8a58 100644 --- a/airflow-core/src/airflow/ui/src/pages/Dag/Header.test.tsx +++ b/airflow-core/src/airflow/ui/src/pages/Dag/Header.test.tsx @@ -38,7 +38,9 @@ const mockDag = { is_favorite: false, is_stale: true, last_parse_duration: 0.23, - latest_dag_version: undefined, + // `null` matches the API response shape for DAGs without version metadata. + // eslint-disable-next-line unicorn/no-null + latest_dag_version: null, next_dagrun_logical_date: "2024-08-22T00:00:00+00:00", next_dagrun_run_after: "2024-08-22T19:00:00+00:00", owner_links: {}, @@ -46,7 +48,7 @@ const mockDag = { tags: [], timetable_partitioned: false, timetable_summary: "* * * * *", -} satisfies DAGDetailsResponse; +} as unknown as DAGDetailsResponse; describe("Header", () => { it("shows a deactivated badge and hides the next run stat for stale dags", () => { From 30f068a48651867f06f9309befcd7a8e50d3d9cc Mon Sep 17 00:00:00 2001 From: dejain Date: Sat, 28 Mar 2026 22:53:28 -0700 Subject: [PATCH 3/5] Fix stale DAG details actions in the UI (#63800) --- .../ui/src/layouts/Details/DagBreadcrumb.tsx | 20 +++++++++---------- .../ui/src/layouts/Details/DetailsLayout.tsx | 2 +- .../src/airflow/ui/src/pages/Dag/Header.tsx | 4 ++-- 3 files changed, 12 insertions(+), 14 deletions(-) diff --git a/airflow-core/src/airflow/ui/src/layouts/Details/DagBreadcrumb.tsx b/airflow-core/src/airflow/ui/src/layouts/Details/DagBreadcrumb.tsx index 326ef695b83d4..822e8d7e663d6 100644 --- a/airflow-core/src/airflow/ui/src/layouts/Details/DagBreadcrumb.tsx +++ b/airflow-core/src/airflow/ui/src/layouts/Details/DagBreadcrumb.tsx @@ -66,17 +66,15 @@ export const DagBreadcrumb = () => { [ { label: dag?.dag_display_name ?? dagId, - labelExtra: ( - dag?.is_stale ? ( - - ) : ( - - ) + labelExtra: dag?.is_stale ? ( + + ) : ( + ), title: translate("dag_one"), value: `/dags/${dagId}`, diff --git a/airflow-core/src/airflow/ui/src/layouts/Details/DetailsLayout.tsx b/airflow-core/src/airflow/ui/src/layouts/Details/DetailsLayout.tsx index 57aeb166bdc1a..dacd6171f5dda 100644 --- a/airflow-core/src/airflow/ui/src/layouts/Details/DetailsLayout.tsx +++ b/airflow-core/src/airflow/ui/src/layouts/Details/DetailsLayout.tsx @@ -114,7 +114,7 @@ export const DetailsLayout = ({ children, error, isLoading, tabs }: Props) => { - {dag === undefined || dag.is_stale ? undefined : ( + {dag === undefined ? undefined : ( ) : ( dag !== undefined && ( - + ) ) } From 747cace9930aabdb3072979a31cfe4a6c155654d Mon Sep 17 00:00:00 2001 From: dejain Date: Wed, 1 Apr 2026 11:06:41 -0700 Subject: [PATCH 4/5] Refine stale DAG header actions --- .../ui/src/layouts/Details/DetailsLayout.tsx | 10 ------- .../airflow/ui/src/pages/Dag/Header.test.tsx | 3 +- .../src/airflow/ui/src/pages/Dag/Header.tsx | 30 +++++++++++-------- 3 files changed, 19 insertions(+), 24 deletions(-) diff --git a/airflow-core/src/airflow/ui/src/layouts/Details/DetailsLayout.tsx b/airflow-core/src/airflow/ui/src/layouts/Details/DetailsLayout.tsx index dacd6171f5dda..3b26d20e62fd7 100644 --- a/airflow-core/src/airflow/ui/src/layouts/Details/DetailsLayout.tsx +++ b/airflow-core/src/airflow/ui/src/layouts/Details/DetailsLayout.tsx @@ -48,8 +48,6 @@ import { dagRunStateFilterKey, dagViewKey, DEFAULT_DAG_VIEW_KEY, - runAfterGteKey, - runAfterLteKey, runTypeFilterKey, showGanttKey, triggeringUserFilterKey, @@ -79,8 +77,6 @@ export const DetailsLayout = ({ children, error, isLoading, tabs }: Props) => { const panelGroupRef = useRef(null); const [dagView, setDagView] = useLocalStorage<"graph" | "grid">(dagViewKey(dagId), defaultDagView); const [limit, setLimit] = useLocalStorage(dagRunsLimitKey(dagId), 10); - const [runAfterGte, setRunAfterGte] = useLocalStorage(runAfterGteKey(dagId), undefined); - const [runAfterLte, setRunAfterLte] = useLocalStorage(runAfterLteKey(dagId), undefined); const [runTypeFilter, setRunTypeFilter] = useLocalStorage( runTypeFilterKey(dagId), undefined, @@ -167,14 +163,10 @@ export const DetailsLayout = ({ children, error, isLoading, tabs }: Props) => { dagView={dagView} limit={limit} panelGroupRef={panelGroupRef} - runAfterGte={runAfterGte} - runAfterLte={runAfterLte} runTypeFilter={runTypeFilter} setDagRunStateFilter={setDagRunStateFilter} setDagView={setDagView} setLimit={setLimit} - setRunAfterGte={setRunAfterGte} - setRunAfterLte={setRunAfterLte} setRunTypeFilter={setRunTypeFilter} setShowGantt={setShowGantt} setShowVersionIndicatorMode={setShowVersionIndicatorMode} @@ -190,8 +182,6 @@ export const DetailsLayout = ({ children, error, isLoading, tabs }: Props) => { { - it("shows a deactivated badge and hides the next run stat for stale dags", () => { + it("shows a deactivated badge and hides stale-only next actions for stale dags", () => { render(
@@ -60,5 +60,6 @@ describe("Header", () => { expect(screen.getByText("header.status.deactivated")).toBeInTheDocument(); expect(screen.queryByText("dagDetails.nextRun")).not.toBeInTheDocument(); + expect(screen.queryByRole("button", { name: "Reparse Dag" })).not.toBeInTheDocument(); }); }); 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 32b36c436d283..8bd8ca2861775 100644 --- a/airflow-core/src/airflow/ui/src/pages/Dag/Header.tsx +++ b/airflow-core/src/airflow/ui/src/pages/Dag/Header.tsx @@ -58,6 +58,21 @@ export const Header = ({ // We would still like to show the dagId even if the dag object hasn't loaded yet const { dagId } = useParams(); + const nextRunStat = + dag?.is_stale === true + ? [] + : [ + { + label: translate("dagDetails.nextRun"), + value: Boolean(dag?.next_dagrun_run_after) ? ( + + ) : undefined, + }, + ]; + const stats = [ { label: translate("dagDetails.schedule"), @@ -89,6 +104,7 @@ export const Header = ({ ) : undefined, }, + ...nextRunStat, { label: translate("dagDetails.maxActiveRuns"), value: @@ -110,18 +126,6 @@ export const Header = ({ }, ]; - if (!dag?.is_stale) { - stats.splice(2, 0, { - label: translate("dagDetails.nextRun"), - value: Boolean(dag?.next_dagrun_run_after) ? ( - - ) : undefined, - }); - } - return ( )} - + {dag.is_stale ? undefined : } ) From 4469eb728458ef807fdda902bc743c31b1250973 Mon Sep 17 00:00:00 2001 From: Deepak Jain Date: Wed, 1 Apr 2026 19:11:29 -0700 Subject: [PATCH 5/5] Fix stale DAG UI review follow-ups (#63800) --- .../airflow/ui/src/layouts/Details/DetailsLayout.tsx | 10 ++++++++++ .../src/airflow/ui/src/pages/Dag/Header.test.tsx | 6 +++--- airflow-core/src/airflow/ui/src/pages/Dag/Header.tsx | 7 ++++--- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/airflow-core/src/airflow/ui/src/layouts/Details/DetailsLayout.tsx b/airflow-core/src/airflow/ui/src/layouts/Details/DetailsLayout.tsx index 3b26d20e62fd7..dacd6171f5dda 100644 --- a/airflow-core/src/airflow/ui/src/layouts/Details/DetailsLayout.tsx +++ b/airflow-core/src/airflow/ui/src/layouts/Details/DetailsLayout.tsx @@ -48,6 +48,8 @@ import { dagRunStateFilterKey, dagViewKey, DEFAULT_DAG_VIEW_KEY, + runAfterGteKey, + runAfterLteKey, runTypeFilterKey, showGanttKey, triggeringUserFilterKey, @@ -77,6 +79,8 @@ export const DetailsLayout = ({ children, error, isLoading, tabs }: Props) => { const panelGroupRef = useRef(null); const [dagView, setDagView] = useLocalStorage<"graph" | "grid">(dagViewKey(dagId), defaultDagView); const [limit, setLimit] = useLocalStorage(dagRunsLimitKey(dagId), 10); + const [runAfterGte, setRunAfterGte] = useLocalStorage(runAfterGteKey(dagId), undefined); + const [runAfterLte, setRunAfterLte] = useLocalStorage(runAfterLteKey(dagId), undefined); const [runTypeFilter, setRunTypeFilter] = useLocalStorage( runTypeFilterKey(dagId), undefined, @@ -163,10 +167,14 @@ export const DetailsLayout = ({ children, error, isLoading, tabs }: Props) => { dagView={dagView} limit={limit} panelGroupRef={panelGroupRef} + runAfterGte={runAfterGte} + runAfterLte={runAfterLte} runTypeFilter={runTypeFilter} setDagRunStateFilter={setDagRunStateFilter} setDagView={setDagView} setLimit={setLimit} + setRunAfterGte={setRunAfterGte} + setRunAfterLte={setRunAfterLte} setRunTypeFilter={setRunTypeFilter} setShowGantt={setShowGantt} setShowVersionIndicatorMode={setShowVersionIndicatorMode} @@ -182,6 +190,8 @@ export const DetailsLayout = ({ children, error, isLoading, tabs }: Props) => { { , ); - expect(screen.getByText("header.status.deactivated")).toBeInTheDocument(); - expect(screen.queryByText("dagDetails.nextRun")).not.toBeInTheDocument(); + expect(screen.getByText(i18n.t("dag:header.status.deactivated"))).toBeInTheDocument(); + expect(screen.queryByText(i18n.t("dag:dagDetails.nextRun"))).not.toBeInTheDocument(); expect(screen.queryByRole("button", { name: "Reparse Dag" })).not.toBeInTheDocument(); }); }); 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 8bd8ca2861775..67be5da796fdc 100644 --- a/airflow-core/src/airflow/ui/src/pages/Dag/Header.tsx +++ b/airflow-core/src/airflow/ui/src/pages/Dag/Header.tsx @@ -57,9 +57,10 @@ export const Header = ({ const { t: translate } = useTranslation(["common", "dag"]); // We would still like to show the dagId even if the dag object hasn't loaded yet const { dagId } = useParams(); + const isStale = dag?.is_stale; const nextRunStat = - dag?.is_stale === true + isStale ? [] : [ { @@ -140,7 +141,7 @@ export const Header = ({ /> )} - {dag.is_stale ? undefined : } + {isStale ? undefined : } ) @@ -148,7 +149,7 @@ export const Header = ({ icon={} stats={stats} subTitle={ - dag?.is_stale ? ( + isStale ? ( ) : ( dag !== undefined && (