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 8480c9ddd19 Add hyperlinks to urls in log messages (#48878)
8480c9ddd19 is described below

commit 8480c9ddd198c1cca8e05007d6bcd8223506215a
Author: Brent Bovenzi <[email protected]>
AuthorDate: Tue Apr 8 12:12:26 2025 -0400

    Add hyperlinks to urls in log messages (#48878)
    
    * Add hyperlinks to urls in log messages
    
    * Move url regex to a constant
    
    * Refactor to work with log groups and tests
---
 .../ui/src/components/renderStructuredLog.tsx      | 54 +++++++++++++++++++---
 .../src/airflow/ui/src/constants/urlRegex.ts       | 20 ++++++++
 .../src/airflow/ui/src/pages/Providers.tsx         |  2 +-
 3 files changed, 69 insertions(+), 7 deletions(-)

diff --git a/airflow-core/src/airflow/ui/src/components/renderStructuredLog.tsx 
b/airflow-core/src/airflow/ui/src/components/renderStructuredLog.tsx
index b5c7ce91fd8..122c68d3a1f 100644
--- a/airflow-core/src/airflow/ui/src/components/renderStructuredLog.tsx
+++ b/airflow-core/src/airflow/ui/src/components/renderStructuredLog.tsx
@@ -16,11 +16,12 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { chakra, Code } from "@chakra-ui/react";
-import { Link } from "react-router-dom";
+import { chakra, Code, Link } from "@chakra-ui/react";
+import { Link as RouterLink } from "react-router-dom";
 
 import type { StructuredLogMessage } from "openapi/requests/types.gen";
 import Time from "src/components/Time";
+import { urlRegex } from "src/constants/urlRegex";
 import { LogLevel, logLevelColorMapping } from "src/utils/logs";
 
 type Frame = {
@@ -46,6 +47,47 @@ type RenderStructuredLogProps = {
   sourceFilters?: Array<string>;
 };
 
+const addLinks = (line: string) => {
+  const matches = [...line.matchAll(urlRegex)];
+  let currentIndex = 0;
+  const elements: Array<JSX.Element | string> = [];
+
+  if (!matches.length) {
+    return line;
+  }
+
+  matches.forEach((match) => {
+    const startIndex = match.index;
+
+    // Add text before the URL
+    if (startIndex > currentIndex) {
+      elements.push(line.slice(currentIndex, startIndex));
+    }
+
+    elements.push(
+      <Link
+        color="fg.info"
+        href={match[0]}
+        key={match[0]}
+        rel="noopener noreferrer"
+        target="_blank"
+        textDecoration="underline"
+      >
+        {match[0]}
+      </Link>,
+    );
+
+    currentIndex = startIndex + match[0].length;
+  });
+
+  // Add remaining text after the last URL
+  if (currentIndex < line.length) {
+    elements.push(line.slice(currentIndex));
+  }
+
+  return elements;
+};
+
 export const renderStructuredLog = ({
   index,
   logLevelFilters,
@@ -56,7 +98,7 @@ export const renderStructuredLog = ({
   if (typeof logMessage === "string") {
     return (
       <chakra.span key={index} lineHeight={1.5}>
-        {logMessage}
+        {addLinks(logMessage)}
       </chakra.span>
     );
   }
@@ -83,7 +125,7 @@ export const renderStructuredLog = ({
   }
 
   elements.push(
-    <Link
+    <RouterLink
       id={index.toString()}
       key={`line_${index}`}
       style={{
@@ -98,7 +140,7 @@ export const renderStructuredLog = ({
       to={`${logLink}#${index}`}
     >
       {index}
-    </Link>,
+    </RouterLink>,
   );
 
   if (Boolean(timestamp)) {
@@ -147,7 +189,7 @@ export const renderStructuredLog = ({
 
   elements.push(
     <chakra.span className="event" key={2} style={{ whiteSpace: "pre-wrap" }}>
-      {event}
+      {addLinks(event)}
     </chakra.span>,
   );
 
diff --git a/airflow-core/src/airflow/ui/src/constants/urlRegex.ts 
b/airflow-core/src/airflow/ui/src/constants/urlRegex.ts
new file mode 100644
index 00000000000..f0c100a135d
--- /dev/null
+++ b/airflow-core/src/airflow/ui/src/constants/urlRegex.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 const urlRegex = 
/https?:\/\/[\w.-]+(?:\.?:[\w.-]+)*(?:[#/?][\w!#$%&'()*+,./:;=?@[\]~-]*)?/gu;
diff --git a/airflow-core/src/airflow/ui/src/pages/Providers.tsx 
b/airflow-core/src/airflow/ui/src/pages/Providers.tsx
index 76a900b7b53..b0a6cd5523b 100644
--- a/airflow-core/src/airflow/ui/src/pages/Providers.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Providers.tsx
@@ -23,6 +23,7 @@ import { useProviderServiceGetProviders } from 
"openapi/queries";
 import type { ProviderResponse } from "openapi/requests/types.gen";
 import { DataTable } from "src/components/DataTable";
 import { ErrorAlert } from "src/components/ErrorAlert";
+import { urlRegex } from "src/constants/urlRegex";
 
 const columns: Array<ColumnDef<ProviderResponse>> = [
   {
@@ -50,7 +51,6 @@ const columns: Array<ColumnDef<ProviderResponse>> = [
   {
     accessorKey: "description",
     cell: ({ row: { original } }) => {
-      const urlRegex = 
/https?:\/\/[\w.-]+(?:\.?:[\w.-]+)*(?:[#/?][\w!#$%&'()*+,./:;=?@[\]~-]*)?/gu;
       const urls = original.description.match(urlRegex);
       const cleanText = original.description.replaceAll(/\n(?:and)?/gu, " 
").split(" ");
 

Reply via email to