From d4c5a98affcb90a588c0ebd7265103230a498987 Mon Sep 17 00:00:00 2001 From: Jens Scheffler <95105677+jscheffl@users.noreply.github.com> Date: Tue, 20 Aug 2024 10:45:08 +0200 Subject: [PATCH] Fix UI rendering when XCom is INT, FLOAT, BOOL or NULL (#41516) (#41605) --- airflow/api_connexion/openapi/v1.yaml | 1 + .../js/components/RenderedJsonField.tsx | 21 +---- .../www/static/js/components/utils.test.ts | 84 +++++++++++++++++++ airflow/www/static/js/components/utils.ts | 38 +++++++++ .../details/taskInstance/Xcom/XcomEntry.tsx | 9 +- airflow/www/static/js/types/api-generated.ts | 2 +- 6 files changed, 134 insertions(+), 21 deletions(-) create mode 100644 airflow/www/static/js/components/utils.test.ts create mode 100644 airflow/www/static/js/components/utils.ts diff --git a/airflow/api_connexion/openapi/v1.yaml b/airflow/api_connexion/openapi/v1.yaml index de99cccaa92259..d91186ff9d01cf 100644 --- a/airflow/api_connexion/openapi/v1.yaml +++ b/airflow/api_connexion/openapi/v1.yaml @@ -4143,6 +4143,7 @@ components: - type: array items: {} - type: object + nullable: true description: The value(s), # Python objects diff --git a/airflow/www/static/js/components/RenderedJsonField.tsx b/airflow/www/static/js/components/RenderedJsonField.tsx index 7000dc17ad3c54..a0356f0a3a8335 100644 --- a/airflow/www/static/js/components/RenderedJsonField.tsx +++ b/airflow/www/static/js/components/RenderedJsonField.tsx @@ -30,32 +30,15 @@ import { useTheme, FlexProps, } from "@chakra-ui/react"; +import jsonParse from "./utils"; interface Props extends FlexProps { content: string | object; jsonProps?: Omit; } -const JsonParse = (content: string | object) => { - let contentJson = null; - let contentFormatted = ""; - let isJson = false; - try { - if (typeof content === "string") { - contentJson = JSON.parse(content); - } else { - contentJson = content; - } - contentFormatted = JSON.stringify(contentJson, null, 4); - isJson = true; - } catch (e) { - // skip - } - return [isJson, contentJson, contentFormatted]; -}; - const RenderedJsonField = ({ content, jsonProps, ...rest }: Props) => { - const [isJson, contentJson, contentFormatted] = JsonParse(content); + const [isJson, contentJson, contentFormatted] = jsonParse(content); const { onCopy, hasCopied } = useClipboard(contentFormatted); const theme = useTheme(); diff --git a/airflow/www/static/js/components/utils.test.ts b/airflow/www/static/js/components/utils.test.ts new file mode 100644 index 00000000000000..8df59388144fdc --- /dev/null +++ b/airflow/www/static/js/components/utils.test.ts @@ -0,0 +1,84 @@ +/*! + * 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 jsonParse from "./utils"; + +/* global describe, test, expect */ + +describe("JSON Parsing.", () => { + test.each([ + { + testName: "null", + testContent: null, + expectedIsJson: false, + }, + { + testName: "boolean", + testContent: true, + expectedIsJson: false, + }, + { + testName: "int", + testContent: 42, + expectedIsJson: false, + }, + { + testName: "float", + testContent: 3.1415, + expectedIsJson: false, + }, + { + testName: "string", + testContent: "hello world", + expectedIsJson: false, + }, + { + testName: "array", + testContent: ["hello world", 42, 3.1515], + expectedIsJson: true, + }, + { + testName: "array as string", + testContent: JSON.stringify(["hello world", 42, 3.1515]), + expectedIsJson: true, + }, + { + testName: "dict", + testContent: { key: 42 }, + expectedIsJson: true, + }, + { + testName: "dict as string", + testContent: JSON.stringify({ key: 42 }), + expectedIsJson: true, + }, + ])( + "Input value is $testName", + ({ testName, testContent, expectedIsJson }) => { + const [isJson, contentJson, contentFormatted] = jsonParse(testContent); + + expect(testName).not.toBeNull(); + expect(isJson).toEqual(expectedIsJson); + if (expectedIsJson) { + expect(contentJson).not.toBeNull(); + expect(contentFormatted.length).toBeGreaterThan(0); + } + } + ); +}); diff --git a/airflow/www/static/js/components/utils.ts b/airflow/www/static/js/components/utils.ts new file mode 100644 index 00000000000000..7ba2ee057337b3 --- /dev/null +++ b/airflow/www/static/js/components/utils.ts @@ -0,0 +1,38 @@ +/*! + * 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. + */ + +const jsonParse = (content: any) => { + let contentJson = null; + let contentFormatted = ""; + let isJson = false; + try { + if (typeof content === "string") { + contentJson = JSON.parse(content); + } else { + contentJson = content; + } + contentFormatted = JSON.stringify(contentJson, null, 4); + isJson = contentJson != null && typeof contentJson === "object"; // ensure numbers/bool are not treated as JSON + } catch (e) { + // skip + } + return [isJson, contentJson, contentFormatted]; +}; + +export default jsonParse; diff --git a/airflow/www/static/js/dag/details/taskInstance/Xcom/XcomEntry.tsx b/airflow/www/static/js/dag/details/taskInstance/Xcom/XcomEntry.tsx index 2e9ba769ae099b..806209bccbf32d 100644 --- a/airflow/www/static/js/dag/details/taskInstance/Xcom/XcomEntry.tsx +++ b/airflow/www/static/js/dag/details/taskInstance/Xcom/XcomEntry.tsx @@ -60,13 +60,20 @@ const XcomEntry = ({ content = ; } else if (error) { content = ; - } else if (!xcom || !xcom.value) { + } else if (!xcom) { content = ( No value found for XCom key ); + } else if (!xcom.value) { + content = ( + + + Value is NULL + + ); } else { let xcomString = ""; if (typeof xcom.value !== "string") { diff --git a/airflow/www/static/js/types/api-generated.ts b/airflow/www/static/js/types/api-generated.ts index a892e327ace077..c6ca3a409b946d 100644 --- a/airflow/www/static/js/types/api-generated.ts +++ b/airflow/www/static/js/types/api-generated.ts @@ -1662,7 +1662,7 @@ export interface components { Partial & Partial & Partial & - Partial<{ [key: string]: unknown }>; + Partial<{ [key: string]: unknown } | null>; }; /** * @description DAG details.