Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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;
Expand Down
100 changes: 97 additions & 3 deletions packages/bruno-app/src/components/Devtools/Console/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 (
<React.Fragment key={key}>
{parts.map((part, index) =>
part.match(urlRegex) ? (
<span
key={index}
className="log-link"
data-url={part}
title={linkHint}
role="link"
tabIndex={0}
>
{part}
</span>
) : (
part
)
)}
</React.Fragment>
);
};

const LogMessage = ({ message, args }) => {
const { displayedTheme } = useTheme();

Expand Down Expand Up @@ -172,10 +205,10 @@ const LogMessage = ({ message, args }) => {
</div>
);
}
return String(arg);
return linkifyText(String(arg), index);
});
}
return msg;
return linkifyText(msg, 'msg');
};

const formattedMessage = formatMessage(message, args);
Expand All @@ -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
Expand All @@ -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);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
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 (
<div className="tab-content">
<div className="tab-content-area">
<div
className="tab-content-area"
ref={contentAreaRef}
onClick={handleContentAreaClick}
onKeyDown={handleContentAreaKeyDown}
>
{filteredLogs.length === 0 ? (
<div className="console-empty">
<IconTerminal2 size={48} strokeWidth={1} />
Expand Down