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(" ");