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 e0bf03c3d33 [AIP-68] Support pluginv2 views (#52582)
e0bf03c3d33 is described below

commit e0bf03c3d337ce85df7afb58b5fb41b87ff30b52
Author: Pierre Jeambrun <[email protected]>
AuthorDate: Wed Jul 2 14:28:46 2025 +0200

    [AIP-68] Support pluginv2 views (#52582)
    
    * Support v2 fab views
    
    * Upate following code review
    
    * Fix version compat
---
 .../airflow/ui/public/i18n/locales/en/common.json  |  1 +
 .../airflow/ui/src/layouts/Nav/PluginMenuItem.tsx  |  3 ++
 .../src/airflow/ui/src/layouts/Nav/PluginMenus.tsx | 29 +++++++++++++++---
 airflow-core/src/airflow/ui/src/pages/Iframe.tsx   | 11 +++++--
 .../src/airflow/providers/fab/version_compat.py    | 35 ++++++++++++++++++++++
 .../providers/fab/www/extensions/init_views.py     | 14 +++++----
 providers/fab/www-hash.txt                         |  2 +-
 7 files changed, 82 insertions(+), 13 deletions(-)

diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/en/common.json 
b/airflow-core/src/airflow/ui/public/i18n/locales/en/common.json
index 9e0b78500f6..1a3709ce9ed 100644
--- a/airflow-core/src/airflow/ui/public/i18n/locales/en/common.json
+++ b/airflow-core/src/airflow/ui/public/i18n/locales/en/common.json
@@ -103,6 +103,7 @@
     "dags": "Dags",
     "docs": "Docs",
     "home": "Home",
+    "legacyFabViews": "Legacy Views",
     "plugins": "Plugins",
     "security": "Security"
   },
diff --git a/airflow-core/src/airflow/ui/src/layouts/Nav/PluginMenuItem.tsx 
b/airflow-core/src/airflow/ui/src/layouts/Nav/PluginMenuItem.tsx
index 28b77757050..eb298ded06c 100644
--- a/airflow-core/src/airflow/ui/src/layouts/Nav/PluginMenuItem.tsx
+++ b/airflow-core/src/airflow/ui/src/layouts/Nav/PluginMenuItem.tsx
@@ -18,6 +18,7 @@
  */
 import { Box, Link, Image } from "@chakra-ui/react";
 import { LuPlug } from "react-icons/lu";
+import { RiArchiveStackLine } from "react-icons/ri";
 import { Link as RouterLink } from "react-router-dom";
 
 import type { ExternalViewResponse } from "openapi/requests/types.gen";
@@ -82,6 +83,8 @@ export const PluginMenuItem = ({ href, icon, name, topLevel = 
false, url_route:
       <Box alignItems="center" display="flex" fontSize="sm" gap={2} px={2} 
py="6px">
         {typeof icon === "string" ? (
           <Image height="1.25rem" src={icon} width="1.25rem" />
+        ) : urlRoute === "legacy-fab-views" ? (
+          <RiArchiveStackLine size="1.25rem" />
         ) : (
           <LuPlug size="1.25rem" />
         )}
diff --git a/airflow-core/src/airflow/ui/src/layouts/Nav/PluginMenus.tsx 
b/airflow-core/src/airflow/ui/src/layouts/Nav/PluginMenus.tsx
index 13fde6fa663..d8850f9ec1e 100644
--- a/airflow-core/src/airflow/ui/src/layouts/Nav/PluginMenus.tsx
+++ b/airflow-core/src/airflow/ui/src/layouts/Nav/PluginMenus.tsx
@@ -32,18 +32,39 @@ export const PluginMenus = () => {
   const { t: translate } = useTranslation("common");
   const { data } = usePluginServiceGetPlugins();
 
-  const menuPlugins =
+  let menuPlugins =
     data?.plugins.flatMap((plugin) => plugin.external_views).filter((view) => 
view.destination === "nav") ??
     [];
 
-  // Only show external plugins in menu if there are more than 2
-  const menuExternalViews = menuPlugins.length > 2 ? menuPlugins : [];
-  const directExternalViews = menuPlugins.length <= 2 ? menuPlugins : [];
+  const hasLegacyViews =
+    (
+      data?.plugins
+        .flatMap((plugin) => plugin.appbuilder_views)
+        // Only include legacy views that have a visible link in the menu. No 
menu items views
+        // are accessible via direct URLs.
+        .filter((view) => view.name !== undefined && view.name !== null) ?? []
+    ).length >= 1;
+
+  if (hasLegacyViews) {
+    menuPlugins = [
+      ...menuPlugins,
+      {
+        destination: "nav",
+        href: "/pluginsv2",
+        name: translate("nav.legacyFabViews"),
+        url_route: "legacy-fab-views",
+      },
+    ];
+  }
 
   if (data === undefined || menuPlugins.length === 0) {
     return undefined;
   }
 
+  // Only show external plugins in menu if there are more than 2
+  const menuExternalViews = menuPlugins.length > 2 ? menuPlugins : [];
+  const directExternalViews = menuPlugins.length <= 2 ? menuPlugins : [];
+
   const categories: Record<string, Array<ExternalViewResponse>> = {};
   const buttons: Array<ExternalViewResponse> = [];
 
diff --git a/airflow-core/src/airflow/ui/src/pages/Iframe.tsx 
b/airflow-core/src/airflow/ui/src/pages/Iframe.tsx
index ae67820700f..c544668b2ae 100644
--- a/airflow-core/src/airflow/ui/src/pages/Iframe.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Iframe.tsx
@@ -17,6 +17,7 @@
  * under the License.
  */
 import { Box } from "@chakra-ui/react";
+import { useTranslation } from "react-i18next";
 import { useParams } from "react-router-dom";
 
 import { usePluginServiceGetPlugins } from "openapi/queries";
@@ -25,12 +26,16 @@ import { ProgressBar } from "src/components/ui";
 import { ErrorPage } from "./Error";
 
 export const Iframe = ({ sandbox = "allow-same-origin allow-forms" }: { 
readonly sandbox: string }) => {
+  const { t: translate } = useTranslation("common");
   const { page } = useParams();
   const { data: pluginData, isLoading } = usePluginServiceGetPlugins();
 
-  const iframeView = pluginData?.plugins
-    .flatMap((plugin) => plugin.external_views)
-    .find((view) => (view.url_route ?? view.name.toLowerCase().replace(" ", 
"-")) === page);
+  const iframeView =
+    page === "legacy-fab-views"
+      ? { href: "/pluginsv2/", name: translate("nav.legacyFabViews") }
+      : pluginData?.plugins
+          .flatMap((plugin) => plugin.external_views)
+          .find((view) => (view.url_route ?? view.name.toLowerCase().replace(" 
", "-")) === page);
 
   if (!iframeView) {
     if (isLoading) {
diff --git a/providers/fab/src/airflow/providers/fab/version_compat.py 
b/providers/fab/src/airflow/providers/fab/version_compat.py
new file mode 100644
index 00000000000..423a778376e
--- /dev/null
+++ b/providers/fab/src/airflow/providers/fab/version_compat.py
@@ -0,0 +1,35 @@
+# 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.
+#
+# NOTE! THIS FILE IS COPIED MANUALLY IN OTHER PROVIDERS DELIBERATELY TO AVOID 
ADDING UNNECESSARY
+# DEPENDENCIES BETWEEN PROVIDERS. IF YOU WANT TO ADD CONDITIONAL CODE IN YOUR 
PROVIDER THAT DEPENDS
+# ON AIRFLOW VERSION, PLEASE COPY THIS FILE TO THE ROOT PACKAGE OF YOUR 
PROVIDER AND IMPORT
+# THOSE CONSTANTS FROM IT RATHER THAN IMPORTING THEM FROM ANOTHER PROVIDER OR 
TEST CODE
+#
+from __future__ import annotations
+
+
+def get_base_airflow_version_tuple() -> tuple[int, int, int]:
+    from packaging.version import Version
+
+    from airflow import __version__
+
+    airflow_version = Version(__version__)
+    return airflow_version.major, airflow_version.minor, airflow_version.micro
+
+
+AIRFLOW_V_3_1_PLUS = get_base_airflow_version_tuple() >= (3, 1, 0)
diff --git 
a/providers/fab/src/airflow/providers/fab/www/extensions/init_views.py 
b/providers/fab/src/airflow/providers/fab/www/extensions/init_views.py
index 76883ae3ad8..e7623e8217e 100644
--- a/providers/fab/src/airflow/providers/fab/www/extensions/init_views.py
+++ b/providers/fab/src/airflow/providers/fab/www/extensions/init_views.py
@@ -26,6 +26,7 @@ from connexion.exceptions import BadRequestProblem, 
ProblemException
 from flask import request
 
 from airflow.api_fastapi.app import get_auth_manager
+from airflow.providers.fab.version_compat import AIRFLOW_V_3_1_PLUS
 from airflow.providers.fab.www.api_connexion.exceptions import 
common_error_handler
 
 if TYPE_CHECKING:
@@ -120,11 +121,14 @@ def init_plugins(app):
             log.debug("Adding view %s without menu", str(type(view["view"])))
             appbuilder.add_view_no_menu(view["view"])
 
-    for menu_link in sorted(
-        plugins_manager.flask_appbuilder_menu_links, key=lambda x: 
(x.get("category", ""), x["name"])
-    ):
-        log.debug("Adding menu link %s to %s", menu_link["name"], 
menu_link["href"])
-        appbuilder.add_link(**menu_link)
+    # Since Airflow 3.1 flask_appbuilder_menu_links are added to the Airflow 3 
UI
+    # navbar..
+    if not AIRFLOW_V_3_1_PLUS:
+        for menu_link in sorted(
+            plugins_manager.flask_appbuilder_menu_links, key=lambda x: 
(x.get("category", ""), x["name"])
+        ):
+            log.debug("Adding menu link %s to %s", menu_link["name"], 
menu_link["href"])
+            appbuilder.add_link(**menu_link)
 
     for blue_print in plugins_manager.flask_blueprints:
         log.debug("Adding blueprint %s:%s", blue_print["name"], 
blue_print["blueprint"].import_name)
diff --git a/providers/fab/www-hash.txt b/providers/fab/www-hash.txt
index 3cc44fe60b2..7ae68df6839 100644
--- a/providers/fab/www-hash.txt
+++ b/providers/fab/www-hash.txt
@@ -1 +1 @@
-1ad167892c3365364f9bdf3940d713e8e50f6d3aedd333f1a7dfb6455d1e2525
+29b02f974d33cfc83d71984325e4f21e43b4889240c9e26715f925e91750a2be

Reply via email to