You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@airflow.apache.org by pi...@apache.org on 2025/07/09 14:37:42 UTC
(airflow) branch main updated: AIP-68 Add loading validation to React App and External Views (#52972)
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 0a047bd8af2 AIP-68 Add loading validation to React App and External Views (#52972)
0a047bd8af2 is described below
commit 0a047bd8af25d1690f84727ade83a4efa9bee522
Author: Pierre Jeambrun <pi...@gmail.com>
AuthorDate: Wed Jul 9 16:37:34 2025 +0200
AIP-68 Add loading validation to React App and External Views (#52972)
* AIP-68 Add loading validation to React App and External Views
* Small adjustment
---
airflow-core/src/airflow/api_fastapi/app.py | 2 ++
.../src/airflow/api_fastapi/core_api/app.py | 7 ++++
airflow-core/src/airflow/plugins_manager.py | 38 ++++++++++++++++++--
.../tests/unit/plugins/test_plugins_manager.py | 42 ++++++++++++++++++++++
4 files changed, 87 insertions(+), 2 deletions(-)
diff --git a/airflow-core/src/airflow/api_fastapi/app.py b/airflow-core/src/airflow/api_fastapi/app.py
index 36b59630d0b..f515be6992c 100644
--- a/airflow-core/src/airflow/api_fastapi/app.py
+++ b/airflow-core/src/airflow/api_fastapi/app.py
@@ -30,6 +30,7 @@ from airflow.api_fastapi.core_api.app import (
init_error_handlers,
init_flask_plugins,
init_middlewares,
+ init_ui_plugins,
init_views,
)
from airflow.api_fastapi.execution_api.app import create_task_execution_api_app
@@ -93,6 +94,7 @@ def create_app(apps: str = "all") -> FastAPI:
init_plugins(app)
init_auth_manager(app)
init_flask_plugins(app)
+ init_ui_plugins(app)
init_views(app) # Core views need to be the last routes added - it has a catch all route
init_error_handlers(app)
init_middlewares(app)
diff --git a/airflow-core/src/airflow/api_fastapi/core_api/app.py b/airflow-core/src/airflow/api_fastapi/core_api/app.py
index ad8938de994..bafa3add823 100644
--- a/airflow-core/src/airflow/api_fastapi/core_api/app.py
+++ b/airflow-core/src/airflow/api_fastapi/core_api/app.py
@@ -181,3 +181,10 @@ def init_middlewares(app: FastAPI) -> None:
from airflow.api_fastapi.auth.managers.simple.middleware import SimpleAllAdminMiddleware
app.add_middleware(SimpleAllAdminMiddleware)
+
+
+def init_ui_plugins(app: FastAPI) -> None:
+ """Initialize UI plugins."""
+ from airflow import plugins_manager
+
+ plugins_manager.initialize_ui_plugins()
diff --git a/airflow-core/src/airflow/plugins_manager.py b/airflow-core/src/airflow/plugins_manager.py
index 2b0b324e9e3..d750ff7c405 100644
--- a/airflow-core/src/airflow/plugins_manager.py
+++ b/airflow-core/src/airflow/plugins_manager.py
@@ -387,12 +387,46 @@ def initialize_ui_plugins():
log.debug("Initialize UI plugin")
+ seen_url_route = {}
external_views = []
react_apps = []
for plugin in plugins:
- external_views.extend(plugin.external_views)
- react_apps.extend(plugin.react_apps)
+ for external_view in plugin.external_views:
+ url_route = external_view["url_route"]
+ if url_route is not None and url_route in seen_url_route:
+ log.warning(
+ "Plugin '%s' has an external view with an URL route '%s' "
+ "that conflicts with another plugin '%s'. The view will not be loaded.",
+ plugin.name,
+ url_route,
+ seen_url_route[url_route],
+ )
+ # Mutate in place the plugin's external views to remove the conflicting view
+ # because some function still access the plugin's external views and not the
+ # global `external_views` variable. (get_plugin_info, for example)
+ plugin.external_views.remove(external_view)
+ continue
+ external_views.append(external_view)
+ seen_url_route[url_route] = plugin.name
+
+ for react_app in plugin.react_apps:
+ url_route = react_app["url_route"]
+ if url_route is not None and url_route in seen_url_route:
+ log.warning(
+ "Plugin '%s' has a React App with an URL route '%s' "
+ "that conflicts with another plugin '%s'. The React App will not be loaded.",
+ plugin.name,
+ url_route,
+ seen_url_route[url_route],
+ )
+ # Mutate in place the plugin's React Apps to remove the conflicting app
+ # because some function still access the plugin's React Apps and not the
+ # global `react_apps` variable. (get_plugin_info, for example)
+ plugin.react_apps.remove(react_app)
+ continue
+ react_apps.append(react_app)
+ seen_url_route[url_route] = plugin.name
def initialize_flask_plugins():
diff --git a/airflow-core/tests/unit/plugins/test_plugins_manager.py b/airflow-core/tests/unit/plugins/test_plugins_manager.py
index addcc4745a2..ae65bb2e790 100644
--- a/airflow-core/tests/unit/plugins/test_plugins_manager.py
+++ b/airflow-core/tests/unit/plugins/test_plugins_manager.py
@@ -151,6 +151,48 @@ class TestPluginsManager:
),
]
+ def test_should_warning_about_conflicting_url_route(self, caplog):
+ class TestPluginA(AirflowPlugin):
+ name = "test_plugin_a"
+
+ external_views = [{"url_route": "/test_route"}]
+
+ class TestPluginB(AirflowPlugin):
+ name = "test_plugin_b"
+
+ external_views = [{"url_route": "/test_route"}]
+ react_apps = [{"url_route": "/test_route"}]
+
+ with (
+ mock_plugin_manager(plugins=[TestPluginA(), TestPluginB()]),
+ caplog.at_level(logging.WARNING, logger="airflow.plugins_manager"),
+ ):
+ from airflow import plugins_manager
+
+ plugins_manager.initialize_ui_plugins()
+
+ # Verify that the conflicting external view and react app are not loaded
+ plugin_b = next(plugin for plugin in plugins_manager.plugins if plugin.name == "test_plugin_b")
+ assert plugin_b.external_views == []
+ assert plugin_b.react_apps == []
+ assert len(plugins_manager.external_views) == 1
+ assert len(plugins_manager.react_apps) == 0
+
+ assert caplog.record_tuples == [
+ (
+ "airflow.plugins_manager",
+ logging.WARNING,
+ "Plugin 'test_plugin_b' has an external view with an URL route '/test_route' "
+ "that conflicts with another plugin 'test_plugin_a'. The view will not be loaded.",
+ ),
+ (
+ "airflow.plugins_manager",
+ logging.WARNING,
+ "Plugin 'test_plugin_b' has a React App with an URL route '/test_route' "
+ "that conflicts with another plugin 'test_plugin_a'. The React App will not be loaded.",
+ ),
+ ]
+
def test_should_not_warning_about_fab_plugins(self, caplog):
class AirflowAdminViewsPlugin(AirflowPlugin):
name = "test_admin_views_plugin"