Skip to content

Commit 778c688

Browse files
[v3-2-test] UI: Show deactivated state for stale DAGs (#65214) (#65218)
When a DAG becomes stale or deactivated, the UI still shows active-oriented controls like the pause toggle, parse action, and next-run information. This change makes the deactivated state explicit by showing a badge and hiding controls that imply the DAG is still schedulable. - Add reusable DagDeactivatedBadge component - Show badge instead of pause toggle for stale DAGs in header and breadcrumb - Hide next-run stat and parse action for stale DAGs - Add regression test for stale DAG header behavior Fixes #63800 (cherry picked from commit 2d42641) Co-authored-by: Pierre Jeambrun <pierrejbrun@gmail.com>
1 parent ea24232 commit 778c688

6 files changed

Lines changed: 125 additions & 16 deletions

File tree

airflow-core/src/airflow/ui/public/i18n/locales/en/dag.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@
5252
"buttons": {
5353
"advanced": "Advanced",
5454
"dagDocs": "Dag Docs"
55+
},
56+
"status": {
57+
"deactivated": "Deactivated"
5558
}
5659
},
5760
"logs": {
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/*!
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
import { Badge } from "@chakra-ui/react";
20+
import { useTranslation } from "react-i18next";
21+
22+
export const DagDeactivatedBadge = () => {
23+
const { t: translate } = useTranslation("dag");
24+
25+
return <Badge colorPalette="orange">{translate("header.status.deactivated")}</Badge>;
26+
};

airflow-core/src/airflow/ui/src/layouts/Details/DagBreadcrumb.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727
useTaskServiceGetTask,
2828
} from "openapi/queries";
2929
import { BreadcrumbStats } from "src/components/BreadcrumbStats";
30+
import { DagDeactivatedBadge } from "src/components/DagDeactivatedBadge";
3031
import { StateBadge } from "src/components/StateBadge";
3132
import { TogglePause } from "src/components/TogglePause";
3233
import { isStatePending, useAutoRefresh } from "src/utils";
@@ -65,7 +66,9 @@ export const DagBreadcrumb = () => {
6566
[
6667
{
6768
label: dag?.dag_display_name ?? dagId,
68-
labelExtra: (
69+
labelExtra: dag?.is_stale ? (
70+
<DagDeactivatedBadge />
71+
) : (
6972
<TogglePause
7073
dagDisplayName={dag?.dag_display_name}
7174
dagId={dagId}

airflow-core/src/airflow/ui/src/layouts/Details/DetailsLayout.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ export const DetailsLayout = ({ children, error, isLoading, tabs }: Props) => {
7878
const [defaultDagView] = useLocalStorage<"graph" | "grid">(DEFAULT_DAG_VIEW_KEY, "grid");
7979
const panelGroupRef = useRef<ImperativePanelGroupHandle | null>(null);
8080
const [dagView, setDagView] = useLocalStorage<"graph" | "grid">(dagViewKey(dagId), defaultDagView);
81-
const [limit, setLimit] = useLocalStorage<number>(dagRunsLimitKey(dagId), 10);
81+
const [limit, setLimit] = useLocalStorage(dagRunsLimitKey(dagId), 10);
8282
const [runAfterGte, setRunAfterGte] = useLocalStorage<string | undefined>(runAfterGteKey(dagId), undefined);
8383
const [runAfterLte, setRunAfterLte] = useLocalStorage<string | undefined>(runAfterLteKey(dagId), undefined);
8484
const [runTypeFilter, setRunTypeFilter] = useLocalStorage<DagRunType | undefined>(
@@ -94,9 +94,9 @@ export const DetailsLayout = ({ children, error, isLoading, tabs }: Props) => {
9494
undefined,
9595
);
9696

97-
const [showGantt, setShowGantt] = useLocalStorage<boolean>(showGanttKey(dagId), false);
97+
const [showGantt, setShowGantt] = useLocalStorage(showGanttKey(dagId), false);
9898
// Global setting: applies to all Dags (intentionally not scoped to dagId)
99-
const [showVersionIndicatorMode, setShowVersionIndicatorMode] = useLocalStorage<VersionIndicatorOptions>(
99+
const [showVersionIndicatorMode, setShowVersionIndicatorMode] = useLocalStorage(
100100
`version_indicator_display_mode`,
101101
VersionIndicatorOptions.ALL,
102102
);
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/*!
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
import "@testing-library/jest-dom";
20+
import { render, screen } from "@testing-library/react";
21+
import type { DAGDetailsResponse } from "openapi-gen/requests/types.gen";
22+
import { describe, expect, it } from "vitest";
23+
24+
import i18n from "src/i18n/config";
25+
import { MOCK_DAG } from "src/mocks/handlers/dag";
26+
import { Wrapper } from "src/utils/Wrapper";
27+
28+
import { Header } from "./Header";
29+
30+
const mockDag = {
31+
...MOCK_DAG,
32+
active_runs_count: 0,
33+
allowed_run_types: [],
34+
bundle_name: "dags-folder",
35+
bundle_version: "1",
36+
default_args: {},
37+
fileloc: "/files/dags/stale_dag.py",
38+
is_favorite: false,
39+
is_stale: true,
40+
last_parse_duration: 0.23,
41+
// `null` matches the API response shape for DAGs without version metadata.
42+
// eslint-disable-next-line unicorn/no-null
43+
latest_dag_version: null,
44+
next_dagrun_logical_date: "2024-08-22T00:00:00+00:00",
45+
next_dagrun_run_after: "2024-08-22T19:00:00+00:00",
46+
owner_links: {},
47+
relative_fileloc: "stale_dag.py",
48+
tags: [],
49+
timetable_partitioned: false,
50+
timetable_summary: "* * * * *",
51+
} as unknown as DAGDetailsResponse;
52+
53+
describe("Header", () => {
54+
it("shows a deactivated badge and hides stale-only next actions for stale dags", () => {
55+
render(
56+
<Wrapper>
57+
<Header dag={mockDag} />
58+
</Wrapper>,
59+
);
60+
61+
expect(screen.getByText(i18n.t("dag:header.status.deactivated"))).toBeInTheDocument();
62+
expect(screen.queryByText(i18n.t("dag:dagDetails.nextRun"))).not.toBeInTheDocument();
63+
expect(screen.queryByRole("button", { name: "Reparse Dag" })).not.toBeInTheDocument();
64+
});
65+
});

airflow-core/src/airflow/ui/src/pages/Dag/Header.tsx

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import { DagIcon } from "src/assets/DagIcon";
2626
import { DeleteDagButton } from "src/components/DagActions/DeleteDagButton";
2727
import { FavoriteDagButton } from "src/components/DagActions/FavoriteDagButton";
2828
import { ParseDagButton } from "src/components/DagActions/ParseDagButton";
29+
import { DagDeactivatedBadge } from "src/components/DagDeactivatedBadge";
2930
import DagRunInfo from "src/components/DagRunInfo";
3031
import { DagVersion } from "src/components/DagVersion";
3132
import DisplayMarkdownButton from "src/components/DisplayMarkdownButton";
@@ -56,6 +57,21 @@ export const Header = ({
5657
const { t: translate } = useTranslation(["common", "dag"]);
5758
// We would still like to show the dagId even if the dag object hasn't loaded yet
5859
const { dagId } = useParams();
60+
const isStale = dag?.is_stale;
61+
62+
const nextRunStat = isStale
63+
? []
64+
: [
65+
{
66+
label: translate("dagDetails.nextRun"),
67+
value: Boolean(dag?.next_dagrun_run_after) ? (
68+
<DagRunInfo
69+
logicalDate={dag?.next_dagrun_logical_date}
70+
runAfter={dag?.next_dagrun_run_after as string}
71+
/>
72+
) : undefined,
73+
},
74+
];
5975

6076
const stats = [
6177
{
@@ -88,15 +104,7 @@ export const Header = ({
88104
</Link>
89105
) : undefined,
90106
},
91-
{
92-
label: translate("dagDetails.nextRun"),
93-
value: Boolean(dag?.next_dagrun_run_after) ? (
94-
<DagRunInfo
95-
logicalDate={dag?.next_dagrun_logical_date}
96-
runAfter={dag?.next_dagrun_run_after as string}
97-
/>
98-
) : undefined,
99-
},
107+
...nextRunStat,
100108
{
101109
label: translate("dagDetails.maxActiveRuns"),
102110
value:
@@ -132,16 +140,20 @@ export const Header = ({
132140
/>
133141
)}
134142
<FavoriteDagButton dagId={dag.dag_id} isFavorite={dag.is_favorite} />
135-
<ParseDagButton dagId={dag.dag_id} fileToken={dag.file_token} />
143+
{isStale ? undefined : <ParseDagButton dagId={dag.dag_id} fileToken={dag.file_token} />}
136144
<DeleteDagButton dagDisplayName={dag.dag_display_name} dagId={dag.dag_id} />
137145
</>
138146
)
139147
}
140148
icon={<DagIcon />}
141149
stats={stats}
142150
subTitle={
143-
dag !== undefined && (
144-
<TogglePause dagDisplayName={dag.dag_display_name} dagId={dag.dag_id} isPaused={dag.is_paused} />
151+
isStale ? (
152+
<DagDeactivatedBadge />
153+
) : (
154+
dag !== undefined && (
155+
<TogglePause dagDisplayName={dag.dag_display_name} dagId={dag.dag_id} isPaused={dag.is_paused} />
156+
)
145157
)
146158
}
147159
title={dag?.dag_display_name ?? dagId}

0 commit comments

Comments
 (0)