Skip to content

Add React Apps to plugin #52255

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 26, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 34 additions & 8 deletions airflow-core/docs/administration-and-deployment/plugins.rst
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,10 @@ looks like:
fastapi_apps = []
# A list of dictionaries containing FastAPI middleware factory objects and some metadata. See the example below.
fastapi_root_middlewares = []
# A list of dictionaries containing iframe views and some metadata. See the example below.
# A list of dictionaries containing external views and some metadata. See the example below.
external_views = []
# A list of dictionaries containing react apps and some metadata. See the example below.
react_apps = []
# A callback to perform actions when Airflow starts and the plugin is loaded.
# NOTE: Ensure your plugin has *args, and **kwargs in the method definition
Expand Down Expand Up @@ -194,27 +196,50 @@ definitions in Airflow.
"name": "Name of the Middleware",
}
# Creating a iframe view that will be rendered in the Airflow UI.
# Creating an external view that will be rendered in the Airflow UI.
external_view_with_metadata = {
"name": "Name of the Iframe View as displayed in the UI",
# Name of the external view, this will be displayed in the UI.
"name": "Name of the External View",
# Source URL of the external view. This URL can be templated using context variables, depending on the location where the external view is rendered
# the context variables available will be different, i.e a subset of (DAG_ID, RUN_ID, TASK_ID, MAP_INDEX).
"href": "https://example.com/{DAG_ID}/{RUN_ID}/{TASK_ID}",
# Destination of the iframe view. This is used to determine where the iframe will be loaded in the UI.
"href": "https://example.com/{DAG_ID}/{RUN_ID}/{TASK_ID}/{MAP_INDEX}",
# Destination of the external view. This is used to determine where the view will be loaded in the UI.
# Supported locations are Literal["nav", "dag", "dag_run", "task", "task_instance"], default to "nav".
"destination": "dag_run",
# Optional icon, url to an svg file.
"icon": "https://example.com/icon.svg",
# Optional dark icon for the dark theme, url to an svg file. If not provided, "icon" will be used for both light and dark themes.
"icon_dark_mode": "https://example.com/dark_icon.svg",
# Optional parameters, relative URL location for the iframe rendering. If not provided, external view will be rendeded as an external link. Should
# not contain a leading slash.
"url_route": "my_iframe_view",
# Optional parameters, relative URL location for the External View rendering. If not provided, external view will be rendeded as an external link. If provided
# will be rendered inside an Iframe in the UI. Should not contain a leading slash.
"url_route": "my_external_view",
# Optional category, only relevant for destination "nav". This is used to group the external links in the navigation bar. We will match the existing
# menus of ["browse", "docs", "admin", "user"] and if there's no match then create a new menu.
"category": "browse",
}
react_app_with_metadata = {
# Name of the React app, this will be displayed in the UI.
"name": "Name of the React App",
# Bundle URL of the React app. This is the URL where the React app is served from. It can be a static file or a CDN.
# This URL can be templated using context variables, depending on the location where the external view is rendered
# the context variables available will be different, i.e a subset of (DAG_ID, RUN_ID, TASK_ID, MAP_INDEX).
"bundle_url": "https://example.com/static/js/my_react_app.js",
# Destination of the react app. This is used to determine where the app will be loaded in the UI.
# Supported locations are Literal["nav", "dag", "dag_run", "task", "task_instance"], default to "nav".
# It can also be put inside of an existing page, the supported views are ["dashboard", "dag_overview", "task_overview"]
"destination": "dag_run",
# Optional icon, url to an svg file.
"icon": "https://example.com/icon.svg",
# Optional dark icon for the dark theme, url to an svg file. If not provided, "icon" will be used for both light and dark themes.
"icon_dark_mode": "https://example.com/dark_icon.svg",
# URL route for the React app, relative to the Airflow UI base URL. Should not contain a leading slash.
"url_route": "my_react_app",
# Optional category, only relevant for destination "nav". This is used to group the react apps in the navigation bar. We will match the existing
# menus of ["browse", "docs", "admin", "user"] and if there's no match then create a new menu.
"category": "browse",
}
# Defining the plugin class
class AirflowTestPlugin(AirflowPlugin):
Expand All @@ -223,6 +248,7 @@ definitions in Airflow.
fastapi_apps = [app_with_metadata]
fastapi_root_middlewares = [middleware_with_metadata]
external_views = [external_view_with_metadata]
react_apps = [react_app_with_metadata]
.. seealso:: :doc:`/howto/define-extra-link`

Expand Down
12 changes: 9 additions & 3 deletions airflow-core/docs/howto/custom-view-plugin.rst
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ core UI using the Plugin manager.

Plugins integrate with the Airflow core RestAPI. In this plugin,
three object references are derived from the base class ``airflow.plugins_manager.AirflowPlugin``.
They are fastapi_apps, fastapi_root_middlewares and external_views.
They are fastapi_apps, fastapi_root_middlewares, external_views and react_apps.

Using fastapi_apps in Airflow plugin, the core RestAPI can be extended
to support extra endpoints to serve custom static file or any other json/application responses.
Expand All @@ -39,10 +39,16 @@ initialization parameters and some metadata information like the name are passed

Using external_views in Airflow plugin, allows to register custom views that are rendered in iframes or external link
in the Airflow UI. This is useful for integrating external applications or custom dashboards into the Airflow UI.
In this object reference, the list of dictionaries with the view name, iframe src (templatable), destination and
In this object reference, the list of dictionaries with the view name, href (templatable), destination and
optional parameters like the icon and url_route are passed on.

Information and code samples to register ``fastapi_apps``, ``fastapi_root_middlewares`` and ``external_views`` are
Using react_apps in Airflow plugin, allows to register custom React applications that can be rendered
in the Airflow UI. This is useful for integrating custom React components or applications into the Airflow UI.
In this object reference, the list of dictionaries with the app name, bundle_url (where to load the js assets, templatable), destination and
optional parameters like the icon and url_route are passed on.


Information and code samples to register ``fastapi_apps``, ``fastapi_root_middlewares``, ``external_views`` and ``react_apps`` are
available in :doc:`plugin </administration-and-deployment/plugins>`.

Support for Airflow 2 plugins
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,20 +69,35 @@ class AppBuilderMenuItemResponse(BaseModel):
category: str | None = None


class ExternalViewResponse(BaseModel):
"""Serializer for IFrame Plugin responses."""
class BaseUIResponse(BaseModel):
"""Base serializer for UI Plugin responses."""

model_config = ConfigDict(extra="allow")

name: str
href: str
icon: str | None = None
icon_dark_mode: str | None = None
url_route: str | None = None
category: str | None = None
destination: Literal["nav", "dag", "dag_run", "task", "task_instance"] = "nav"


class ExternalViewResponse(BaseUIResponse):
"""Serializer for External View Plugin responses."""

model_config = ConfigDict(extra="allow")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why actually do we allow extras here?

Copy link
Member Author

@pierrejeambrun pierrejeambrun Jun 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is how plugin attributes are built, every members of the Plugin allow extra attributes and are serialized and returned. This was the behavior in 2.x, so we kept the same parttern in 3.x. Newly added attributes (react_apps, fastapi_apps, eternal_views) also follow the same pattern.

Meaning that you can attach some extra metadata to your plugin members, and it will be returned by the API.


href: str


class ReactAppResponse(BaseUIResponse):
"""Serializer for React App Plugin responses."""

model_config = ConfigDict(extra="allow")

bundle_url: str


class PluginResponse(BaseModel):
"""Plugin serializer."""

Expand All @@ -94,6 +109,7 @@ class PluginResponse(BaseModel):
external_views: list[ExternalViewResponse] = Field(
description="Aggregate all external views. Both 'external_views' and 'appbuilder_menu_items' are included here."
)
react_apps: list[ReactAppResponse]
appbuilder_views: list[AppBuilderViewResponse]
appbuilder_menu_items: list[AppBuilderMenuItemResponse] = Field(
deprecated="Kept for backward compatibility, use `external_views` instead.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9297,9 +9297,6 @@ components:
name:
type: string
title: Name
href:
type: string
title: Href
icon:
anyOf:
- type: string
Expand Down Expand Up @@ -9330,13 +9327,16 @@ components:
- task_instance
title: Destination
default: nav
href:
type: string
title: Href
additionalProperties: true
type: object
required:
- name
- href
title: ExternalViewResponse
description: Serializer for IFrame Plugin responses.
description: Serializer for External View Plugin responses.
ExtraLinkCollectionResponse:
properties:
extra_links:
Expand Down Expand Up @@ -9691,6 +9691,11 @@ components:
title: External Views
description: Aggregate all external views. Both 'external_views' and 'appbuilder_menu_items'
are included here.
react_apps:
items:
$ref: '#/components/schemas/ReactAppResponse'
type: array
title: React Apps
appbuilder_views:
items:
$ref: '#/components/schemas/AppBuilderViewResponse'
Expand Down Expand Up @@ -9733,6 +9738,7 @@ components:
- fastapi_apps
- fastapi_root_middlewares
- external_views
- react_apps
- appbuilder_views
- appbuilder_menu_items
- global_operator_extra_links
Expand Down Expand Up @@ -9930,6 +9936,51 @@ components:
- dag_display_name
title: QueuedEventResponse
description: Queued Event serializer for responses..
ReactAppResponse:
properties:
name:
type: string
title: Name
icon:
anyOf:
- type: string
- type: 'null'
title: Icon
icon_dark_mode:
anyOf:
- type: string
- type: 'null'
title: Icon Dark Mode
url_route:
anyOf:
- type: string
- type: 'null'
title: Url Route
category:
anyOf:
- type: string
- type: 'null'
title: Category
destination:
type: string
enum:
- nav
- dag
- dag_run
- task
- task_instance
title: Destination
default: nav
bundle_url:
type: string
title: Bundle Url
additionalProperties: true
type: object
required:
- name
- bundle_url
title: ReactAppResponse
description: Serializer for React App Plugin responses.
ReprocessBehavior:
type: string
enum:
Expand Down
8 changes: 7 additions & 1 deletion airflow-core/src/airflow/plugins_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@
fastapi_apps: list[Any] | None = None
fastapi_root_middlewares: list[Any] | None = None
external_views: list[Any] | None = None
react_apps: list[Any] | None = None
menu_links: list[Any] | None = None
flask_appbuilder_views: list[Any] | None = None
flask_appbuilder_menu_links: list[Any] | None = None
Expand All @@ -92,6 +93,7 @@
"fastapi_apps",
"fastapi_root_middlewares",
"external_views",
"react_apps",
"menu_links",
"appbuilder_views",
"appbuilder_menu_items",
Expand Down Expand Up @@ -157,6 +159,7 @@ class AirflowPlugin:
fastapi_apps: list[Any] = []
fastapi_root_middlewares: list[Any] = []
external_views: list[Any] = []
react_apps: list[Any] = []
menu_links: list[Any] = []
appbuilder_views: list[Any] = []
appbuilder_menu_items: list[Any] = []
Expand Down Expand Up @@ -372,8 +375,9 @@ def initialize_ui_plugins():
"""Collect extension points for the UI."""
global plugins
global external_views
global react_apps

if external_views is not None:
if external_views is not None and react_apps is not None:
return

ensure_plugins_loaded()
Expand All @@ -384,9 +388,11 @@ def initialize_ui_plugins():
log.debug("Initialize UI plugin")

external_views = []
react_apps = []

for plugin in plugins:
external_views.extend(plugin.external_views)
react_apps.extend(plugin.react_apps)


def initialize_flask_plugins():
Expand Down
Loading
Loading