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"