This is an automated email from the ASF dual-hosted git repository.

bbovenzi 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 352feb2b615 Add base react plugin destination (#62530)
352feb2b615 is described below

commit 352feb2b615a68549e21d5504d5d5494120e0e4c
Author: Brent Bovenzi <[email protected]>
AuthorDate: Fri Feb 27 09:29:42 2026 -0500

    Add base react plugin destination (#62530)
---
 .../docs/administration-and-deployment/plugins.rst |  5 +--
 .../api_fastapi/core_api/datamodels/plugins.py     |  2 +-
 .../core_api/openapi/v2-rest-api-generated.yaml    |  2 ++
 .../airflow/ui/openapi-gen/requests/schemas.gen.ts |  4 +--
 .../airflow/ui/openapi-gen/requests/types.gen.ts   |  8 ++---
 .../src/airflow/ui/src/layouts/BaseLayout.tsx      | 39 +++++++++++++++-------
 .../src/airflowctl/api/datamodels/generated.py     |  2 ++
 7 files changed, 41 insertions(+), 21 deletions(-)

diff --git a/airflow-core/docs/administration-and-deployment/plugins.rst 
b/airflow-core/docs/administration-and-deployment/plugins.rst
index 010a3d51cd5..79deee2b4fe 100644
--- a/airflow-core/docs/administration-and-deployment/plugins.rst
+++ b/airflow-core/docs/administration-and-deployment/plugins.rst
@@ -235,7 +235,7 @@ definitions in Airflow.
         # 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}/{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".
+        # Supported locations are Literal["nav", "dag", "dag_run", "task", 
"task_instance", "base"], default to "nav".
         "destination": "dag_run",
         # Optional icon, url to an svg file.
         "icon": "https://example.com/icon.svg";,
@@ -258,9 +258,10 @@ definitions in Airflow.
         # 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".
+        # Supported locations are Literal["nav", "dag", "dag_run", "task", 
"task_instance", "base"], default to "nav".
         # It can also be put inside of an existing page, the supported views 
are ["dashboard", "dag_overview", "task_overview"]. You can position
         # element in the existing page via the css `order` rule which will 
determine the flex order.
+        # Use "base" to mount the app in the base layout (e.g. a toolbar 
strip); the host uses a flex container so you can set ``order`` in your root 
JSX to control position.
         "destination": "dag_run",
         # Optional icon, url to an svg file.
         "icon": "https://example.com/icon.svg";,
diff --git 
a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/plugins.py 
b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/plugins.py
index 8a2873e6745..e7fa0fe276a 100644
--- a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/plugins.py
+++ b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/plugins.py
@@ -69,7 +69,7 @@ class AppBuilderMenuItemResponse(BaseModel):
     category: str | None = None
 
 
-BaseDestinationLiteral = Literal["nav", "dag", "dag_run", "task", 
"task_instance"]
+BaseDestinationLiteral = Literal["nav", "dag", "dag_run", "task", 
"task_instance", "base"]
 
 
 class BaseUIResponse(BaseModel):
diff --git 
a/airflow-core/src/airflow/api_fastapi/core_api/openapi/v2-rest-api-generated.yaml
 
b/airflow-core/src/airflow/api_fastapi/core_api/openapi/v2-rest-api-generated.yaml
index c24e1fc2666..f50499bdb85 100644
--- 
a/airflow-core/src/airflow/api_fastapi/core_api/openapi/v2-rest-api-generated.yaml
+++ 
b/airflow-core/src/airflow/api_fastapi/core_api/openapi/v2-rest-api-generated.yaml
@@ -11472,6 +11472,7 @@ components:
           - dag_run
           - task
           - task_instance
+          - base
           title: Destination
           default: nav
       additionalProperties: true
@@ -12350,6 +12351,7 @@ components:
           - dag_run
           - task
           - task_instance
+          - base
           - dashboard
           title: Destination
           default: nav
diff --git a/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts 
b/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts
index 3ddffda1b15..a82a6598449 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts
@@ -3766,7 +3766,7 @@ export const $ExternalViewResponse = {
         },
         destination: {
             type: 'string',
-            enum: ['nav', 'dag', 'dag_run', 'task', 'task_instance'],
+            enum: ['nav', 'dag', 'dag_run', 'task', 'task_instance', 'base'],
             title: 'Destination',
             default: 'nav'
         }
@@ -5016,7 +5016,7 @@ export const $ReactAppResponse = {
         },
         destination: {
             type: 'string',
-            enum: ['nav', 'dag', 'dag_run', 'task', 'task_instance', 
'dashboard'],
+            enum: ['nav', 'dag', 'dag_run', 'task', 'task_instance', 'base', 
'dashboard'],
             title: 'Destination',
             default: 'nav'
         }
diff --git a/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts 
b/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts
index 93365a66c0e..53c9e259d64 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts
@@ -943,11 +943,11 @@ export type ExternalViewResponse = {
     url_route?: string | null;
     category?: string | null;
     href: string;
-    destination?: 'nav' | 'dag' | 'dag_run' | 'task' | 'task_instance';
+    destination?: 'nav' | 'dag' | 'dag_run' | 'task' | 'task_instance' | 
'base';
     [key: string]: unknown | string;
 };
 
-export type destination = 'nav' | 'dag' | 'dag_run' | 'task' | 'task_instance';
+export type destination = 'nav' | 'dag' | 'dag_run' | 'task' | 'task_instance' 
| 'base';
 
 /**
  * Extra Links Response.
@@ -1292,11 +1292,11 @@ export type ReactAppResponse = {
     url_route?: string | null;
     category?: string | null;
     bundle_url: string;
-    destination?: 'nav' | 'dag' | 'dag_run' | 'task' | 'task_instance' | 
'dashboard';
+    destination?: 'nav' | 'dag' | 'dag_run' | 'task' | 'task_instance' | 
'base' | 'dashboard';
     [key: string]: unknown | string;
 };
 
-export type destination2 = 'nav' | 'dag' | 'dag_run' | 'task' | 
'task_instance' | 'dashboard';
+export type destination2 = 'nav' | 'dag' | 'dag_run' | 'task' | 
'task_instance' | 'base' | 'dashboard';
 
 /**
  * Internal enum for setting reprocess behavior in a backfill.
diff --git a/airflow-core/src/airflow/ui/src/layouts/BaseLayout.tsx 
b/airflow-core/src/airflow/ui/src/layouts/BaseLayout.tsx
index 351501028dd..cb645babc82 100644
--- a/airflow-core/src/airflow/ui/src/layouts/BaseLayout.tsx
+++ b/airflow-core/src/airflow/ui/src/layouts/BaseLayout.tsx
@@ -21,6 +21,9 @@ import { useEffect, type PropsWithChildren } from "react";
 import { useTranslation } from "react-i18next";
 import { Outlet } from "react-router-dom";
 
+import { usePluginServiceGetPlugins } from "openapi/queries";
+import type { ReactAppResponse } from "openapi/requests/types.gen";
+import { ReactPlugin } from "src/pages/ReactPlugin";
 import { useConfig } from "src/queries/useConfig";
 
 import { Nav } from "./Nav";
@@ -28,6 +31,12 @@ import { Nav } from "./Nav";
 export const BaseLayout = ({ children }: PropsWithChildren) => {
   const instanceName = useConfig("instance_name");
   const { i18n } = useTranslation();
+  const { data: pluginData } = usePluginServiceGetPlugins();
+
+  const baseReactPlugins =
+    pluginData?.plugins
+      .flatMap((plugin) => plugin.react_apps)
+      .filter((reactApp: ReactAppResponse) => reactApp.destination === "base") 
?? [];
 
   if (typeof instanceName === "string") {
     document.title = instanceName;
@@ -52,18 +61,24 @@ export const BaseLayout = ({ children }: PropsWithChildren) 
=> {
 
   return (
     <LocaleProvider locale={i18n.language || "en"}>
-      <Nav />
-      <Box
-        _ltr={{ ml: 16 }}
-        _rtl={{ mr: 16 }}
-        data-testid="main-content"
-        display="flex"
-        flexDirection="column"
-        h="100vh"
-        overflowY="auto"
-        p={3}
-      >
-        {children ?? <Outlet />}
+      <Box display="flex" flexDirection="column" h="100vh">
+        <Nav />
+        <Box
+          _ltr={{ ml: 16 }}
+          _rtl={{ mr: 16 }}
+          data-testid="main-content"
+          display="flex"
+          flex={1}
+          flexDirection="column"
+          minH={0}
+          overflowY="auto"
+          p={3}
+        >
+          {baseReactPlugins.map((plugin) => (
+            <ReactPlugin key={plugin.name} reactApp={plugin} />
+          ))}
+          {children ?? <Outlet />}
+        </Box>
       </Box>
     </LocaleProvider>
   );
diff --git a/airflow-ctl/src/airflowctl/api/datamodels/generated.py 
b/airflow-ctl/src/airflowctl/api/datamodels/generated.py
index 51b234ab9e3..0ca5629b532 100644
--- a/airflow-ctl/src/airflowctl/api/datamodels/generated.py
+++ b/airflow-ctl/src/airflowctl/api/datamodels/generated.py
@@ -502,6 +502,7 @@ class Destination(str, Enum):
     DAG_RUN = "dag_run"
     TASK = "task"
     TASK_INSTANCE = "task_instance"
+    BASE = "base"
 
 
 class ExternalViewResponse(BaseModel):
@@ -708,6 +709,7 @@ class Destination1(str, Enum):
     DAG_RUN = "dag_run"
     TASK = "task"
     TASK_INSTANCE = "task_instance"
+    BASE = "base"
     DASHBOARD = "dashboard"
 
 

Reply via email to