Skip to content

Adjustment external views #51889

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 6 commits into from
Jun 23, 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
24 changes: 15 additions & 9 deletions airflow-core/docs/administration-and-deployment/plugins.rst
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ looks like:
# 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.
iframe_views = []
external_views = []

# 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 @@ -195,18 +195,24 @@ definitions in Airflow.
}

# Creating a iframe view that will be rendered in the Airflow UI.
iframe_view_with_metadata = {
external_view_with_metadata = {
"name": "Name of the Iframe View as displayed in the UI",
# Source URL of the iframe. This URL can be templated using context variables, depending on the location where the iframe is rendered
# the context variables available will be different, i.e a subset of (DAG_ID, RUN_ID, TASK_ID, MAP_INDEX)
"src": "https://example.com/{DAG_ID}/{RUN_ID}/{TASK_ID}",
# 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.
# Supported locations are Literal["nav", "dag", "dag_run", "task", "task_instance"]
# 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 parameters, relative URL location when opening the iframe
"url_route": "/my_iframe_view",
# 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 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",
}


Expand All @@ -216,7 +222,7 @@ definitions in Airflow.
macros = [plugin_macro]
fastapi_apps = [app_with_metadata]
fastapi_root_middlewares = [middleware_with_metadata]
iframe_views = [iframe_view_with_metadata]
external_views = [external_view_with_metadata]

.. seealso:: :doc:`/howto/define-extra-link`

Expand Down
8 changes: 4 additions & 4 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 iframe_views.
They are fastapi_apps, fastapi_root_middlewares and external_views.

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 @@ -37,12 +37,12 @@ functionality to the entire FastAPI application, including core endpoints.
In this object reference, the list of dictionaries with Middleware factories object,
initialization parameters and some metadata information like the name are passed on.

Using iframe_views in Airflow plugin, allows to register custom views that are rendered in iframes in
the Airflow UI. This is useful for integrating external applications or custom dashboards into the Airflow UI.
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
optional parameters like the icon and url_route are passed on.

Information and code samples to register ``fastapi_apps``, ``fastapi_root_middlewares`` and ``iframe_views`` are
Information and code samples to register ``fastapi_apps``, ``fastapi_root_middlewares`` and ``external_views`` 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 @@ -19,7 +19,7 @@

from typing import Annotated, Any, Literal

from pydantic import BeforeValidator, ConfigDict, field_validator
from pydantic import BeforeValidator, ConfigDict, Field, field_validator, model_validator

from airflow.api_fastapi.core_api.base import BaseModel
from airflow.plugins_manager import AirflowPluginSource
Expand Down Expand Up @@ -65,20 +65,22 @@ class AppBuilderMenuItemResponse(BaseModel):
model_config = ConfigDict(extra="allow")

name: str
href: str | None = None
href: str
category: str | None = None


class IFrameViewsResponse(BaseModel):
class ExternalViewResponse(BaseModel):
"""Serializer for IFrame Plugin responses."""

model_config = ConfigDict(extra="allow")

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


class PluginResponse(BaseModel):
Expand All @@ -89,9 +91,13 @@ class PluginResponse(BaseModel):
flask_blueprints: list[str]
fastapi_apps: list[FastAPIAppResponse]
fastapi_root_middlewares: list[FastAPIRootMiddlewareResponse]
iframe_views: list[IFrameViewsResponse]
external_views: list[ExternalViewResponse] = Field(
description="Aggregate all external views. Both 'external_views' and 'appbuilder_menu_items' are included here."
)
appbuilder_views: list[AppBuilderViewResponse]
appbuilder_menu_items: list[AppBuilderMenuItemResponse]
appbuilder_menu_items: list[AppBuilderMenuItemResponse] = Field(
deprecated="Kept for backward compatibility, use `external_views` instead.",
)
global_operator_extra_links: list[str]
operator_extra_links: list[str]
source: Annotated[str, BeforeValidator(coerce_to_string)]
Expand All @@ -105,6 +111,12 @@ def convert_source(cls, data: Any) -> Any:
return str(data)
return data

@model_validator(mode="before")
@classmethod
def convert_external_views(cls, data: Any) -> Any:
data["external_views"] = [*data["external_views"], *data.get("appbuilder_menu_items", [])]
return data


class PluginCollectionResponse(BaseModel):
"""Plugin Collection serializer."""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7029,9 +7029,7 @@ components:
type: string
title: Name
href:
anyOf:
- type: string
- type: 'null'
type: string
title: Href
category:
anyOf:
Expand All @@ -7042,6 +7040,7 @@ components:
type: object
required:
- name
- href
title: AppBuilderMenuItemResponse
description: Serializer for AppBuilder Menu Item responses.
AppBuilderViewResponse:
Expand Down Expand Up @@ -9293,6 +9292,51 @@ components:
- url
title: ExternalLogUrlResponse
description: Response for the external log URL endpoint.
ExternalViewResponse:
properties:
name:
type: string
title: Name
href:
type: string
title: Href
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
additionalProperties: true
type: object
required:
- name
- href
title: ExternalViewResponse
description: Serializer for IFrame Plugin responses.
ExtraLinkCollectionResponse:
properties:
extra_links:
Expand Down Expand Up @@ -9386,42 +9430,6 @@ components:
- triggerer
title: HealthInfoResponse
description: Health serializer for responses.
IFrameViewsResponse:
properties:
name:
type: string
title: Name
src:
type: string
title: Src
icon:
anyOf:
- type: string
- type: 'null'
title: Icon
url_route:
anyOf:
- type: string
- type: 'null'
title: Url Route
destination:
anyOf:
- type: string
enum:
- nav
- dag
- dag_run
- task
- task_instance
- type: 'null'
title: Destination
additionalProperties: true
type: object
required:
- name
- src
title: IFrameViewsResponse
description: Serializer for IFrame Plugin responses.
ImportErrorCollectionResponse:
properties:
import_errors:
Expand Down Expand Up @@ -9676,11 +9684,13 @@ components:
$ref: '#/components/schemas/FastAPIRootMiddlewareResponse'
type: array
title: Fastapi Root Middlewares
iframe_views:
external_views:
items:
$ref: '#/components/schemas/IFrameViewsResponse'
$ref: '#/components/schemas/ExternalViewResponse'
type: array
title: Iframe Views
title: External Views
description: Aggregate all external views. Both 'external_views' and 'appbuilder_menu_items'
are included here.
appbuilder_views:
items:
$ref: '#/components/schemas/AppBuilderViewResponse'
Expand All @@ -9691,6 +9701,7 @@ components:
$ref: '#/components/schemas/AppBuilderMenuItemResponse'
type: array
title: Appbuilder Menu Items
deprecated: true
global_operator_extra_links:
items:
type: string
Expand Down Expand Up @@ -9721,7 +9732,7 @@ components:
- flask_blueprints
- fastapi_apps
- fastapi_root_middlewares
- iframe_views
- external_views
- appbuilder_views
- appbuilder_menu_items
- global_operator_extra_links
Expand Down
14 changes: 7 additions & 7 deletions airflow-core/src/airflow/plugins_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@
flask_blueprints: list[Any] | None = None
fastapi_apps: list[Any] | None = None
fastapi_root_middlewares: list[Any] | None = None
iframe_views: list[Any] | None = None
external_views: 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 @@ -91,7 +91,7 @@
"flask_blueprints",
"fastapi_apps",
"fastapi_root_middlewares",
"iframe_views",
"external_views",
"menu_links",
"appbuilder_views",
"appbuilder_menu_items",
Expand Down Expand Up @@ -156,7 +156,7 @@ class AirflowPlugin:
flask_blueprints: list[Any] = []
fastapi_apps: list[Any] = []
fastapi_root_middlewares: list[Any] = []
iframe_views: list[Any] = []
external_views: list[Any] = []
menu_links: list[Any] = []
appbuilder_views: list[Any] = []
appbuilder_menu_items: list[Any] = []
Expand Down Expand Up @@ -371,9 +371,9 @@ def ensure_plugins_loaded():
def initialize_ui_plugins():
"""Collect extension points for the UI."""
global plugins
global iframe_views
global external_views

if iframe_views is not None:
if external_views is not None:
return

ensure_plugins_loaded()
Expand All @@ -383,10 +383,10 @@ def initialize_ui_plugins():

log.debug("Initialize UI plugin")

iframe_views = []
external_views = []

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


def initialize_flask_plugins():
Expand Down
Loading