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