diff --git a/airflow/api_fastapi/core_api/datamodels/xcom.py b/airflow/api_fastapi/core_api/datamodels/xcom.py
index 4e3c6f54a7a4f..3a819b317d760 100644
--- a/airflow/api_fastapi/core_api/datamodels/xcom.py
+++ b/airflow/api_fastapi/core_api/datamodels/xcom.py
@@ -33,6 +33,7 @@ class XComResponse(BaseModel):
map_index: int
task_id: str
dag_id: str
+ run_id: str
class XComResponseNative(XComResponse):
diff --git a/airflow/api_fastapi/core_api/openapi/v1-generated.yaml b/airflow/api_fastapi/core_api/openapi/v1-generated.yaml
index a36e45d4241a9..a10960a2c4634 100644
--- a/airflow/api_fastapi/core_api/openapi/v1-generated.yaml
+++ b/airflow/api_fastapi/core_api/openapi/v1-generated.yaml
@@ -9481,6 +9481,9 @@ components:
dag_id:
type: string
title: Dag Id
+ run_id:
+ type: string
+ title: Run Id
type: object
required:
- key
@@ -9489,6 +9492,7 @@ components:
- map_index
- task_id
- dag_id
+ - run_id
title: XComResponse
description: Serializer for a xcom item.
XComResponseNative:
@@ -9513,6 +9517,9 @@ components:
dag_id:
type: string
title: Dag Id
+ run_id:
+ type: string
+ title: Run Id
value:
title: Value
type: object
@@ -9523,6 +9530,7 @@ components:
- map_index
- task_id
- dag_id
+ - run_id
- value
title: XComResponseNative
description: XCom response serializer with native return type.
@@ -9548,6 +9556,9 @@ components:
dag_id:
type: string
title: Dag Id
+ run_id:
+ type: string
+ title: Run Id
value:
anyOf:
- type: string
@@ -9561,6 +9572,7 @@ components:
- map_index
- task_id
- dag_id
+ - run_id
- value
title: XComResponseString
description: XCom response serializer with string return type.
diff --git a/airflow/ui/openapi-gen/requests/schemas.gen.ts b/airflow/ui/openapi-gen/requests/schemas.gen.ts
index 41e309d694c9b..c8bf77fde2328 100644
--- a/airflow/ui/openapi-gen/requests/schemas.gen.ts
+++ b/airflow/ui/openapi-gen/requests/schemas.gen.ts
@@ -5380,6 +5380,10 @@ export const $XComResponse = {
type: "string",
title: "Dag Id",
},
+ run_id: {
+ type: "string",
+ title: "Run Id",
+ },
},
type: "object",
required: [
@@ -5389,6 +5393,7 @@ export const $XComResponse = {
"map_index",
"task_id",
"dag_id",
+ "run_id",
],
title: "XComResponse",
description: "Serializer for a xcom item.",
@@ -5422,6 +5427,10 @@ export const $XComResponseNative = {
type: "string",
title: "Dag Id",
},
+ run_id: {
+ type: "string",
+ title: "Run Id",
+ },
value: {
title: "Value",
},
@@ -5434,6 +5443,7 @@ export const $XComResponseNative = {
"map_index",
"task_id",
"dag_id",
+ "run_id",
"value",
],
title: "XComResponseNative",
@@ -5468,6 +5478,10 @@ export const $XComResponseString = {
type: "string",
title: "Dag Id",
},
+ run_id: {
+ type: "string",
+ title: "Run Id",
+ },
value: {
anyOf: [
{
@@ -5488,6 +5502,7 @@ export const $XComResponseString = {
"map_index",
"task_id",
"dag_id",
+ "run_id",
"value",
],
title: "XComResponseString",
diff --git a/airflow/ui/openapi-gen/requests/types.gen.ts b/airflow/ui/openapi-gen/requests/types.gen.ts
index c178f78608e30..bac010ef3d28c 100644
--- a/airflow/ui/openapi-gen/requests/types.gen.ts
+++ b/airflow/ui/openapi-gen/requests/types.gen.ts
@@ -1286,6 +1286,7 @@ export type XComResponse = {
map_index: number;
task_id: string;
dag_id: string;
+ run_id: string;
};
/**
@@ -1298,6 +1299,7 @@ export type XComResponseNative = {
map_index: number;
task_id: string;
dag_id: string;
+ run_id: string;
value: unknown;
};
@@ -1311,6 +1313,7 @@ export type XComResponseString = {
map_index: number;
task_id: string;
dag_id: string;
+ run_id: string;
value: string | null;
};
diff --git a/airflow/ui/src/components/ui/Clipboard.tsx b/airflow/ui/src/components/ui/Clipboard.tsx
new file mode 100644
index 0000000000000..639d437200bb9
--- /dev/null
+++ b/airflow/ui/src/components/ui/Clipboard.tsx
@@ -0,0 +1,112 @@
+/*!
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import type { ButtonProps, InputProps } from "@chakra-ui/react";
+import {
+ Button,
+ Clipboard as ChakraClipboard,
+ IconButton,
+ Input,
+} from "@chakra-ui/react";
+import * as React from "react";
+import { LuCheck, LuClipboard, LuLink } from "react-icons/lu";
+
+const ClipboardIcon = React.forwardRef<
+ HTMLDivElement,
+ ChakraClipboard.IndicatorProps
+>((props, ref) => (
+ } {...props} ref={ref}>
+
+
+));
+
+const ClipboardCopyText = React.forwardRef<
+ HTMLDivElement,
+ ChakraClipboard.IndicatorProps
+>((props, ref) => (
+
+ Copy
+
+));
+
+export const ClipboardLabel = React.forwardRef<
+ HTMLLabelElement,
+ ChakraClipboard.LabelProps
+>((props, ref) => (
+
+));
+
+export const ClipboardButton = React.forwardRef(
+ (props, ref) => (
+
+
+
+ ),
+);
+
+export const ClipboardLink = React.forwardRef(
+ (props, ref) => (
+
+
+
+ ),
+);
+
+export const ClipboardIconButton = React.forwardRef<
+ HTMLButtonElement,
+ ButtonProps
+>((props, ref) => (
+
+
+
+
+
+
+));
+
+export const ClipboardInput = React.forwardRef(
+ (props, ref) => (
+
+
+
+ ),
+);
+
+export const ClipboardRoot = ChakraClipboard.Root;
diff --git a/airflow/ui/src/components/ui/index.ts b/airflow/ui/src/components/ui/index.ts
index add5c4b355038..950be1e910d73 100644
--- a/airflow/ui/src/components/ui/index.ts
+++ b/airflow/ui/src/components/ui/index.ts
@@ -35,3 +35,4 @@ export * from "./Status";
export * from "./Button";
export * from "./Toaster";
export * from "./Breadcrumb";
+export * from "./Clipboard";
diff --git a/airflow/ui/src/layouts/Nav/BrowseButton.tsx b/airflow/ui/src/layouts/Nav/BrowseButton.tsx
index de58ecff2f3f2..3c50ed6b8a27b 100644
--- a/airflow/ui/src/layouts/Nav/BrowseButton.tsx
+++ b/airflow/ui/src/layouts/Nav/BrowseButton.tsx
@@ -28,6 +28,10 @@ const links = [
href: "/events",
title: "Events",
},
+ {
+ href: "/xcoms",
+ title: "XComs",
+ },
];
export const BrowseButton = () => (
diff --git a/airflow/ui/src/pages/XCom/XCom.tsx b/airflow/ui/src/pages/XCom/XCom.tsx
new file mode 100644
index 0000000000000..13fa99b1021d3
--- /dev/null
+++ b/airflow/ui/src/pages/XCom/XCom.tsx
@@ -0,0 +1,87 @@
+/*!
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { Box } from "@chakra-ui/react";
+import type { ColumnDef } from "@tanstack/react-table";
+import { useParams, useSearchParams } from "react-router-dom";
+
+import { useXcomServiceGetXcomEntries } from "openapi/queries";
+import type { XComResponse } from "openapi/requests/types.gen";
+import { DataTable } from "src/components/DataTable";
+import { useTableURLState } from "src/components/DataTable/useTableUrlState";
+import { ErrorAlert } from "src/components/ErrorAlert";
+
+import { XComEntry } from "./XComEntry";
+
+const columns: Array> = [
+ {
+ accessorKey: "key",
+ enableSorting: false,
+ header: "Key",
+ },
+ {
+ cell: ({ row: { original } }) => (
+
+ ),
+ enableSorting: false,
+ header: "Value",
+ },
+];
+
+export const XCom = () => {
+ const { dagId = "~", runId = "~", taskId = "~" } = useParams();
+ const [searchParams] = useSearchParams();
+ const mapIndexParam = searchParams.get("map_index");
+ const mapIndex = parseInt(mapIndexParam ?? "-1", 10);
+
+ const { setTableURLState, tableURLState } = useTableURLState();
+ const { pagination } = tableURLState;
+
+ const { data, error, isFetching, isLoading } = useXcomServiceGetXcomEntries({
+ dagId,
+ dagRunId: runId,
+ limit: pagination.pageSize,
+ mapIndex,
+ offset: pagination.pageIndex * pagination.pageSize,
+ taskId,
+ });
+
+ return (
+
+
+
+
+ );
+};
diff --git a/airflow/ui/src/pages/XCom/XComEntry.tsx b/airflow/ui/src/pages/XCom/XComEntry.tsx
new file mode 100644
index 0000000000000..13247d284204a
--- /dev/null
+++ b/airflow/ui/src/pages/XCom/XComEntry.tsx
@@ -0,0 +1,66 @@
+/*!
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { Skeleton, HStack, Text } from "@chakra-ui/react";
+
+import { useXcomServiceGetXcomEntry } from "openapi/queries";
+import type { XComResponseString } from "openapi/requests/types.gen";
+import { ClipboardIconButton, ClipboardRoot } from "src/components/ui";
+
+type XComEntryProps = {
+ readonly dagId: string;
+ readonly mapIndex: number;
+ readonly runId: string;
+ readonly taskId: string;
+ readonly xcomKey: string;
+};
+
+export const XComEntry = ({
+ dagId,
+ mapIndex,
+ runId,
+ taskId,
+ xcomKey,
+}: XComEntryProps) => {
+ const { data, isLoading } = useXcomServiceGetXcomEntry({
+ dagId,
+ dagRunId: runId,
+ mapIndex,
+ stringify: true,
+ taskId,
+ xcomKey,
+ });
+
+ return isLoading ? (
+
+ ) : (data?.value?.length ?? 0) > 0 ? (
+
+ {data?.value}
+ {Boolean(data?.value) ? (
+
+
+
+ ) : undefined}
+
+ ) : undefined;
+};
diff --git a/airflow/ui/src/pages/XCom/index.ts b/airflow/ui/src/pages/XCom/index.ts
new file mode 100644
index 0000000000000..1430290d8c56c
--- /dev/null
+++ b/airflow/ui/src/pages/XCom/index.ts
@@ -0,0 +1,20 @@
+/*!
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+export { XCom } from "./XCom";
diff --git a/airflow/ui/src/router.tsx b/airflow/ui/src/router.tsx
index cd8da392dbc6f..d1f66c15e4cd6 100644
--- a/airflow/ui/src/router.tsx
+++ b/airflow/ui/src/router.tsx
@@ -32,6 +32,7 @@ import { Run } from "src/pages/Run";
import { TaskInstances } from "src/pages/Run/TaskInstances";
import { Task, Instances } from "src/pages/Task";
import { TaskInstance } from "src/pages/TaskInstance";
+import { XCom } from "src/pages/XCom";
import { Variables } from "./pages/Variables";
@@ -51,6 +52,10 @@ export const router = createBrowserRouter(
element: ,
path: "events",
},
+ {
+ element: ,
+ path: "xcoms",
+ },
{
element: ,
path: "variables",
@@ -79,7 +84,7 @@ export const router = createBrowserRouter(
children: [
{ element: Logs
, index: true },
{ element: , path: "events" },
- { element: Xcom
, path: "xcom" },
+ { element: , path: "xcom" },
{ element:
, path: "code" },
{ element: Details
, path: "details" },
],
diff --git a/tests/api_fastapi/core_api/routes/public/test_xcom.py b/tests/api_fastapi/core_api/routes/public/test_xcom.py
index 022987125fb7a..b61b22a239898 100644
--- a/tests/api_fastapi/core_api/routes/public/test_xcom.py
+++ b/tests/api_fastapi/core_api/routes/public/test_xcom.py
@@ -118,6 +118,7 @@ def test_should_respond_200_stringify(self, test_client):
assert current_data == {
"dag_id": TEST_DAG_ID,
"logical_date": logical_date_parsed.strftime("%Y-%m-%dT%H:%M:%SZ"),
+ "run_id": run_id,
"key": TEST_XCOM_KEY,
"task_id": TEST_TASK_ID,
"map_index": -1,
@@ -136,6 +137,7 @@ def test_should_respond_200_native(self, test_client):
assert current_data == {
"dag_id": TEST_DAG_ID,
"logical_date": logical_date_parsed.strftime("%Y-%m-%dT%H:%M:%SZ"),
+ "run_id": run_id,
"key": TEST_XCOM_KEY,
"task_id": TEST_TASK_ID,
"map_index": -1,
@@ -230,6 +232,7 @@ def test_should_respond_200(self, test_client):
{
"dag_id": TEST_DAG_ID,
"logical_date": logical_date_formatted,
+ "run_id": run_id,
"key": f"{TEST_XCOM_KEY}-0",
"task_id": TEST_TASK_ID,
"timestamp": "TIMESTAMP",
@@ -238,6 +241,7 @@ def test_should_respond_200(self, test_client):
{
"dag_id": TEST_DAG_ID,
"logical_date": logical_date_formatted,
+ "run_id": run_id,
"key": f"{TEST_XCOM_KEY}-1",
"task_id": TEST_TASK_ID,
"timestamp": "TIMESTAMP",
@@ -263,6 +267,7 @@ def test_should_respond_200_with_tilde(self, test_client):
{
"dag_id": TEST_DAG_ID,
"logical_date": logical_date_formatted,
+ "run_id": run_id,
"key": f"{TEST_XCOM_KEY}-0",
"task_id": TEST_TASK_ID,
"timestamp": "TIMESTAMP",
@@ -271,6 +276,7 @@ def test_should_respond_200_with_tilde(self, test_client):
{
"dag_id": TEST_DAG_ID,
"logical_date": logical_date_formatted,
+ "run_id": run_id,
"key": f"{TEST_XCOM_KEY}-1",
"task_id": TEST_TASK_ID,
"timestamp": "TIMESTAMP",
@@ -279,6 +285,7 @@ def test_should_respond_200_with_tilde(self, test_client):
{
"dag_id": TEST_DAG_ID_2,
"logical_date": logical_date_formatted,
+ "run_id": run_id,
"key": f"{TEST_XCOM_KEY}-0",
"task_id": TEST_TASK_ID_2,
"timestamp": "TIMESTAMP",
@@ -287,6 +294,7 @@ def test_should_respond_200_with_tilde(self, test_client):
{
"dag_id": TEST_DAG_ID_2,
"logical_date": logical_date_formatted,
+ "run_id": run_id,
"key": f"{TEST_XCOM_KEY}-1",
"task_id": TEST_TASK_ID_2,
"timestamp": "TIMESTAMP",
@@ -313,6 +321,7 @@ def test_should_respond_200_with_map_index(self, map_index, test_client):
{
"dag_id": TEST_DAG_ID,
"logical_date": logical_date_formatted,
+ "run_id": run_id,
"key": TEST_XCOM_KEY,
"task_id": TEST_TASK_ID,
"timestamp": "TIMESTAMP",
@@ -325,6 +334,7 @@ def test_should_respond_200_with_map_index(self, map_index, test_client):
{
"dag_id": TEST_DAG_ID,
"logical_date": logical_date_formatted,
+ "run_id": run_id,
"key": TEST_XCOM_KEY,
"task_id": TEST_TASK_ID,
"timestamp": "TIMESTAMP",
@@ -347,6 +357,7 @@ def test_should_respond_200_with_map_index(self, map_index, test_client):
{
"dag_id": TEST_DAG_ID,
"logical_date": logical_date_formatted,
+ "run_id": run_id,
"key": TEST_XCOM_KEY,
"task_id": TEST_TASK_ID,
"timestamp": "TIMESTAMP",
@@ -355,6 +366,7 @@ def test_should_respond_200_with_map_index(self, map_index, test_client):
{
"dag_id": TEST_DAG_ID,
"logical_date": logical_date_formatted,
+ "run_id": run_id,
"key": TEST_XCOM_KEY,
"task_id": TEST_TASK_ID,
"timestamp": "TIMESTAMP",