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 fb235e6926a Preserve newlines in XCom tab string values (#74112)
fb235e6926a is described below
commit fb235e6926a67d839d1a009df13b2a15fea73eda
Author: Basant_Jamwal <[email protected]>
AuthorDate: Thu Oct 8 23:07:48 2026 +0530
Preserve newlines in XCom tab string values (#74112)
* Preserve newlines in XCom tab string values
* Fix end-of-file and line-ending issues in XComEntry.test.tsx
* Fix line endings in XComEntry test
* Fix formatting in XComEntry
---
.../airflow/ui/src/pages/XCom/XComEntry.test.tsx | 28 +++++++++++++++++++++-
.../src/airflow/ui/src/pages/XCom/XComEntry.tsx | 19 ++++++++++-----
2 files changed, 40 insertions(+), 7 deletions(-)
diff --git a/airflow-core/src/airflow/ui/src/pages/XCom/XComEntry.test.tsx
b/airflow-core/src/airflow/ui/src/pages/XCom/XComEntry.test.tsx
index 72138d3a487..e82fd314bdd 100644
--- a/airflow-core/src/airflow/ui/src/pages/XCom/XComEntry.test.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/XCom/XComEntry.test.tsx
@@ -38,6 +38,19 @@ beforeAll(() => server.listen({ onUnhandledFrame: "bypass"
}));
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
+const renderValue = (value: string) => {
+ server.use(http.get(entryUrl, () => HttpResponse.json({ key: "return_value",
value })));
+
+ const queryClient = new QueryClient({ defaultOptions: { queries: { retry:
false } } });
+
+ render(
+ <QueryClientProvider client={queryClient}>
+ <XComEntry {...entryProps} xcomKey="return_value" />
+ </QueryClientProvider>,
+ { wrapper: Wrapper },
+ );
+};
+
describe("XComEntry", () => {
it.each([
{ detail: "XCom entry was deleted", status: 404 },
@@ -213,4 +226,17 @@ describe("XComEntry", () => {
expect(within(dialog).getByTestId("error-alert")).toHaveTextContent(/network
error/iu);
});
-});
+
+ it("keeps newlines in string values", async () => {
+ renderValue("a\nb");
+
+ expect((await
screen.findByTestId("xcom-value")).textContent).toContain("a\nb");
+ });
+
+ it("keeps newlines and still renders links", async () => {
+ renderValue("Line 1\nSee https://airflow.apache.org\nLine 3");
+
+ expect(await screen.findByRole("link")).toHaveAttribute("href",
"https://airflow.apache.org");
+ expect(screen.getByTestId("xcom-value").textContent).toContain("Line
1\nSee ");
+ });
+});
\ No newline at end of file
diff --git a/airflow-core/src/airflow/ui/src/pages/XCom/XComEntry.tsx
b/airflow-core/src/airflow/ui/src/pages/XCom/XComEntry.tsx
index aad31bc6aff..9a8675ce893 100644
--- a/airflow-core/src/airflow/ui/src/pages/XCom/XComEntry.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/XCom/XComEntry.tsx
@@ -44,13 +44,11 @@ type XComEntryProps = {
const renderTextWithLinks = (text: string) => {
const urls = text.match(urlRegex);
- const parts = text.split(/\s+/u);
+ const parts = text.split(/(\s+)/u);
return (
<>
- {parts.map((part, index) => {
- const isLastPart = index === parts.length - 1;
-
+ {parts.map((part) => {
if (urls?.includes(part)) {
return (
<Link
@@ -66,7 +64,7 @@ const renderTextWithLinks = (text: string) => {
);
}
- return `${part}${isLastPart ? "" : " "}`;
+ return part;
})}
</>
);
@@ -127,6 +125,7 @@ export const XComEntry = ({ dagId, mapIndex, open = false,
runId, taskId, xcomKe
const xcomValue = data?.value;
const isObjectOrArray = Array.isArray(xcomValue) || (xcomValue !== null &&
typeof xcomValue === "object");
const valueFormatted = typeof xcomValue === "string" ? xcomValue :
JSON.stringify(xcomValue, undefined, 4);
+ const hasNewline = typeof xcomValue === "string" && xcomValue.includes("\n");
return isLoading ? (
<Skeleton
@@ -140,7 +139,15 @@ export const XComEntry = ({ dagId, mapIndex, open = false,
runId, taskId, xcomKe
{isObjectOrArray ? (
<RenderedJsonField collapsed={!open} content={xcomValue as object}
enableClipboard={false} />
) : (
- <Text>{renderTextWithLinks(valueFormatted)}</Text>
+ <Text
+ maxHeight="200px"
+ minWidth={hasNewline ? "250px" : undefined}
+ overflowWrap="anywhere"
+ overflowY="auto"
+ whiteSpace="pre-wrap"
+ >
+ {renderTextWithLinks(valueFormatted)}
+ </Text>
)}
{xcomValue === undefined || xcomValue === null ? undefined : (
<ClipboardRoot value={valueFormatted}>