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..822e8d7e663d6 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"; @@ -65,7 +66,9 @@ export const DagBreadcrumb = () => { [ { label: dag?.dag_display_name ?? dagId, - labelExtra: ( + labelExtra: dag?.is_stale ? ( + + ) : ( { 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, ); 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 new file mode 100644 index 0000000000000..9d68c7b8e851c --- /dev/null +++ b/airflow-core/src/airflow/ui/src/pages/Dag/Header.test.tsx @@ -0,0 +1,65 @@ +/*! + * 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 { render, screen } from "@testing-library/react"; +import type { DAGDetailsResponse } from "openapi-gen/requests/types.gen"; +import { describe, expect, it } from "vitest"; + +import i18n from "src/i18n/config"; +import { MOCK_DAG } from "src/mocks/handlers/dag"; +import { Wrapper } from "src/utils/Wrapper"; + +import { Header } from "./Header"; + +const mockDag = { + ...MOCK_DAG, + active_runs_count: 0, + allowed_run_types: [], + bundle_name: "dags-folder", + bundle_version: "1", + default_args: {}, + fileloc: "/files/dags/stale_dag.py", + is_favorite: false, + is_stale: true, + last_parse_duration: 0.23, + // `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: {}, + relative_fileloc: "stale_dag.py", + tags: [], + timetable_partitioned: false, + timetable_summary: "* * * * *", +} as unknown as DAGDetailsResponse; + +describe("Header", () => { + it("shows a deactivated badge and hides stale-only next actions for stale dags", () => { + render( + +
+ , + ); + + 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 6af9129a31641..67be5da796fdc 100644 --- a/airflow-core/src/airflow/ui/src/pages/Dag/Header.tsx +++ b/airflow-core/src/airflow/ui/src/pages/Dag/Header.tsx @@ -26,6 +26,7 @@ import { DagIcon } from "src/assets/DagIcon"; import { DeleteDagButton } from "src/components/DagActions/DeleteDagButton"; import { FavoriteDagButton } from "src/components/DagActions/FavoriteDagButton"; import { ParseDagButton } from "src/components/DagActions/ParseDagButton"; +import { DagDeactivatedBadge } from "src/components/DagDeactivatedBadge"; import DagRunInfo from "src/components/DagRunInfo"; import { DagVersion } from "src/components/DagVersion"; import DisplayMarkdownButton from "src/components/DisplayMarkdownButton"; @@ -56,6 +57,22 @@ 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 = + isStale + ? [] + : [ + { + label: translate("dagDetails.nextRun"), + value: Boolean(dag?.next_dagrun_run_after) ? ( + + ) : undefined, + }, + ]; const stats = [ { @@ -88,15 +105,7 @@ export const Header = ({ ) : undefined, }, - { - label: translate("dagDetails.nextRun"), - value: Boolean(dag?.next_dagrun_run_after) ? ( - - ) : undefined, - }, + ...nextRunStat, { label: translate("dagDetails.maxActiveRuns"), value: @@ -132,7 +141,7 @@ export const Header = ({ /> )} - + {isStale ? undefined : } ) @@ -140,8 +149,12 @@ export const Header = ({ icon={} stats={stats} subTitle={ - dag !== undefined && ( - + isStale ? ( + + ) : ( + dag !== undefined && ( + + ) ) } title={dag?.dag_display_name ?? dagId}