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",