diff --git a/packages/bruno-app/src/components/Devtools/Console/StyledWrapper.js b/packages/bruno-app/src/components/Devtools/Console/StyledWrapper.js index ef297687f6a..43166b2f824 100644 --- a/packages/bruno-app/src/components/Devtools/Console/StyledWrapper.js +++ b/packages/bruno-app/src/components/Devtools/Console/StyledWrapper.js @@ -107,6 +107,13 @@ const StyledWrapper = styled.div` overflow-y: auto; background: ${(props) => props.theme.console.contentBg}; min-height: 0; + + &.cmd-ctrl-pressed .log-link:hover, + &.cmd-ctrl-pressed .log-link:focus-visible { + cursor: pointer; + color: ${(props) => props.theme.textLink}; + text-decoration: underline; + } } .network-with-details { @@ -346,6 +353,20 @@ const StyledWrapper = styled.div` white-space: pre-wrap; word-break: break-word; flex: 1; + + .log-link { + cursor: text; + + &:hover { + text-decoration: underline; + } + + &:focus-visible { + outline: 1px solid ${(props) => props.theme.textLink}; + outline-offset: 1px; + border-radius: 2px; + } + } .log-object { margin: 4px 0; diff --git a/packages/bruno-app/src/components/Devtools/Console/index.js b/packages/bruno-app/src/components/Devtools/Console/index.js index e0d55654d73..acff287742a 100644 --- a/packages/bruno-app/src/components/Devtools/Console/index.js +++ b/packages/bruno-app/src/components/Devtools/Console/index.js @@ -32,6 +32,7 @@ import ErrorDetailsPanel from './ErrorDetailsPanel'; import Performance from '../Performance'; import StyledWrapper from './StyledWrapper'; import { useResizablePanel } from 'hooks/useResizablePanel'; +import { isMacOS } from 'utils/common/platform'; const MIN_DETAILS_PANEL_WIDTH = 280; const DETAILS_PANEL_MAX_RATIO = 0.7; @@ -132,6 +133,38 @@ const getBrunoTypeMetadata = (obj) => { return {}; }; +// Turns any http(s) URL inside a plain string into a marked, hoverable span. + +const getLinkHint = () => (isMacOS() ? 'Hold Cmd and click to open link' : 'Hold Ctrl and click to open link'); + +const linkifyText = (text, key) => { + if (!text || typeof text !== 'string') return text; + const urlRegex = /(https?:\/\/[^\s"'()]+(?:\([^\s"'()]*\)[^\s"'()]*)*)/g; + if (!text.match(urlRegex)) return text; + const parts = text.split(urlRegex); + const linkHint = getLinkHint(); + return ( + + {parts.map((part, index) => + part.match(urlRegex) ? ( + + {part} + + ) : ( + part + ) + )} + + ); +}; + const LogMessage = ({ message, args }) => { const { displayedTheme } = useTheme(); @@ -172,10 +205,10 @@ const LogMessage = ({ message, args }) => { ); } - return String(arg); + return linkifyText(String(arg), index); }); } - return msg; + return linkifyText(msg, 'msg'); }; const formattedMessage = formatMessage(message, args); @@ -192,6 +225,7 @@ const LogMessage = ({ message, args }) => { const ConsoleTab = ({ logs, filters, logCounts, onFilterToggle, onToggleAll, onClearLogs }) => { const logsEndRef = useRef(null); const prevLogsCountRef = useRef(0); + const contentAreaRef = useRef(null); useEffect(() => { // Only scroll when new logs are added, not when switching tabs @@ -201,11 +235,71 @@ const ConsoleTab = ({ logs, filters, logCounts, onFilterToggle, onToggleAll, onC prevLogsCountRef.current = logs.length; }, [logs]); + // Toggle a CSS-only class on the container while Cmd/Ctrl is held, so + // links visually become clickable + + useEffect(() => { + const isCmdOrCtrlPressed = (event) => (isMacOS() ? event.metaKey : event.ctrlKey); + const updateCmdCtrlClass = (event) => { + const el = contentAreaRef.current; + if (!el) return; + el.classList.toggle('cmd-ctrl-pressed', isCmdOrCtrlPressed(event)); + }; + // If the window loses focus while the modifier is held (e.g. Cmd+Tab + // to another app), this component never receives the matching keyup, + // so the class would otherwise stay stuck on — clear it explicitly. + const clearCmdCtrlClass = () => { + contentAreaRef.current?.classList.remove('cmd-ctrl-pressed'); + }; + window.addEventListener('keydown', updateCmdCtrlClass); + window.addEventListener('keyup', updateCmdCtrlClass); + window.addEventListener('blur', clearCmdCtrlClass); + return () => { + window.removeEventListener('keydown', updateCmdCtrlClass); + window.removeEventListener('keyup', updateCmdCtrlClass); + window.removeEventListener('blur', clearCmdCtrlClass); + }; + }, []); + + // Single delegated click handler for every .log-link, shared by mouse and + // keyboard activation, rather than one listener per link. Only opens the URL + // when the modifier is held a plain click/Enter is left alone. + + const activateLogLink = (event, linkEl) => { + const modifierPressed = isMacOS() ? event.metaKey : event.ctrlKey; + if (!modifierPressed) return; + + event.preventDefault(); + event.stopPropagation(); + const url = linkEl.getAttribute('data-url'); + if (url) window?.ipcRenderer?.openExternal(url); + }; + + const handleContentAreaClick = (event) => { + const linkEl = event.target.closest?.('.log-link'); + if (!linkEl) return; + activateLogLink(event, linkEl); + }; + + // Keyboard equivalent of modifier+click: focus the link (Tab), hold the + // same modifier, press Enter. + const handleContentAreaKeyDown = (event) => { + if (event.key !== 'Enter') return; + const linkEl = event.target.closest?.('.log-link'); + if (!linkEl) return; + activateLogLink(event, linkEl); + }; + const filteredLogs = logs.filter((log) => filters[log.type]); return (
-
+
{filteredLogs.length === 0 ? (