diff --git a/ui/package.json b/ui/package.json
index dd456cdb1dc1..4e672a23d037 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -24,6 +24,7 @@
"cronstrue": "^2.50.0",
"dagre": "^0.8.5",
"history": "^4.10.1",
+ "linkify-it": "^5.0.0",
"moment": "^2.30.1",
"monaco-editor": "^0.45.0",
"prop-types": "^15.8.1",
@@ -48,6 +49,7 @@
"@types/dagre": "^0.7.52",
"@types/history": "^4.6.2",
"@types/jest": "^26.0.15",
+ "@types/linkify-it": "^5.0.0",
"@types/prop-types": "^15.7.11",
"@types/react": "^16.8.5",
"@types/react-autocomplete": "^1.8.10",
diff --git a/ui/src/app/shared/components/linkified-text.tsx b/ui/src/app/shared/components/linkified-text.tsx
new file mode 100644
index 000000000000..233bbd6ba5a7
--- /dev/null
+++ b/ui/src/app/shared/components/linkified-text.tsx
@@ -0,0 +1,37 @@
+import LinkifyIt from 'linkify-it';
+import React from 'react';
+
+interface Props {
+ text: string;
+}
+
+const linkify = new LinkifyIt();
+
+export default function LinkifiedText({text}: Props) {
+ const matches = linkify.match(text);
+
+ if (!matches) {
+ return <>{text}>;
+ }
+
+ const parts = [];
+ let lastIndex = 0;
+
+ matches.forEach(match => {
+ if (match.index > lastIndex) {
+ parts.push({text.slice(lastIndex, match.index)});
+ }
+ parts.push(
+
+ {match.text}
+
+ );
+ lastIndex = match.lastIndex;
+ });
+
+ if (lastIndex < text.length) {
+ parts.push({text.slice(lastIndex)});
+ }
+
+ return <>{parts}>;
+}
diff --git a/ui/src/app/workflows/components/workflow-node-info/workflow-node-info.tsx b/ui/src/app/workflows/components/workflow-node-info/workflow-node-info.tsx
index 66b4b92e578d..874f219b31ef 100644
--- a/ui/src/app/workflows/components/workflow-node-info/workflow-node-info.tsx
+++ b/ui/src/app/workflows/components/workflow-node-info/workflow-node-info.tsx
@@ -12,6 +12,7 @@ import {Button} from '../../../shared/components/button';
import {ClipboardText} from '../../../shared/components/clipboard-text';
import {DurationPanel} from '../../../shared/components/duration-panel';
import {InlineTable} from '../../../shared/components/inline-table/inline-table';
+import LinkifiedText from '../../../shared/components/linkified-text';
import {Links} from '../../../shared/components/links';
import {Phase} from '../../../shared/components/phase';
import {Timestamp} from '../../../shared/components/timestamp';
@@ -19,9 +20,9 @@ import {getPodName} from '../../../shared/pod-name';
import {ResourcesDuration} from '../../../shared/resources-duration';
import {services} from '../../../shared/services';
import {getResolvedTemplates} from '../../../shared/template-resolution';
+import {TIMESTAMP_KEYS} from '../../../shared/use-timestamp';
import './workflow-node-info.scss';
-import {TIMESTAMP_KEYS} from '../../../shared/use-timestamp';
function nodeDuration(node: models.NodeStatus, now: moment.Moment) {
const endTime = node.finishedAt ? moment(node.finishedAt) : now;
@@ -70,13 +71,13 @@ interface Props {
onRetryNode?: () => void;
}
-const AttributeRow = (attr: {title: string; value: any}) => (
+const AttributeRow = (attr: {title: string; value: string | React.JSX.Element}) => (