Skip to content
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
7e6cac1
Make the links clickable within Dev Console
saikatsarkar056 Nov 5, 2025
0a1da76
Merge branch 'main' of github.com:elastic/kibana into clickable-links…
saikatsarkar056 Nov 5, 2025
8ba2336
Make the links clickable within Dev Console
saikatsarkar056 Nov 5, 2025
a694a38
Merge branch 'main' of github.com:elastic/kibana into clickable-links…
saikatsarkar056 Nov 5, 2025
9956b79
Make the links clickable within Dev Console
saikatsarkar056 Nov 5, 2025
6b63c51
Added FTR test
saikatsarkar056 Nov 5, 2025
4105824
Added FTR test
saikatsarkar056 Nov 5, 2025
6b98c1c
Merge branch 'main' of github.com:elastic/kibana into clickable-links…
saikatsarkar056 Nov 6, 2025
7187942
Disable the links by default for all editors
saikatsarkar056 Nov 6, 2025
34f594f
Enable the links option for console only
saikatsarkar056 Nov 6, 2025
b48ebe8
Merge branch 'main' of github.com:elastic/kibana into clickable-links…
saikatsarkar056 Nov 6, 2025
d44ddd6
Update the FTR tests
saikatsarkar056 Nov 6, 2025
97af2d3
Update the FTR tests
saikatsarkar056 Nov 6, 2025
55fb519
Update the FTR tests
saikatsarkar056 Nov 6, 2025
a1a5042
Update the FTR tests
saikatsarkar056 Nov 6, 2025
53df6b8
Update the FTR tests
saikatsarkar056 Nov 6, 2025
5afa5cc
Update the FTR tests
saikatsarkar056 Nov 6, 2025
510df50
Update the FTR tests
saikatsarkar056 Nov 6, 2025
c0a847e
Update the FTR tests
saikatsarkar056 Nov 6, 2025
d2f8c73
Update the FTR tests
saikatsarkar056 Nov 7, 2025
ffdc7d7
Update the FTR tests
saikatsarkar056 Nov 7, 2025
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 @@ -23,6 +23,7 @@ import 'monaco-editor/esm/vs/editor/contrib/suggest/browser/suggestController.js
import 'monaco-editor/esm/vs/editor/contrib/hover/browser/hover.js'; // Needed for hover
import 'monaco-editor/esm/vs/editor/contrib/parameterHints/browser/parameterHints.js'; // Needed for signature
import 'monaco-editor/esm/vs/editor/contrib/bracketMatching/browser/bracketMatching.js'; // Needed for brackets matching highlight
import 'monaco-editor/esm/vs/editor/contrib/links/browser/links.js'; // Needed for clickable links with Cmd/Ctrl+Click

import 'monaco-editor/esm/vs/editor/contrib/codeAction/browser/codeAction.js';
import 'monaco-editor/esm/vs/editor/contrib/codeAction/browser/codeActionCommands.js';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,12 @@ export interface CodeEditorProps {
*/
htmlId?: string;

/**
* Enables clickable links in the editor. URLs will be underlined and can be opened
* in a new tab using Cmd/Ctrl+Click. Disabled by default.
*/
links?: boolean;

/**
* Callbacks for when editor is focused/blurred
*/
Expand Down Expand Up @@ -239,6 +245,7 @@ export const CodeEditor: React.FC<CodeEditorProps> = ({
enableCustomContextMenu = false,
customContextMenuActions = [],
htmlId,
links = false,
onFocus,
onBlur,
}) => {
Expand Down Expand Up @@ -649,6 +656,8 @@ export const CodeEditor: React.FC<CodeEditorProps> = ({
// @ts-expect-error, see https://github.com/microsoft/monaco-editor/issues/3829
'bracketPairColorization.enabled': false,
...options,
// Explicit links prop always takes precedence over any value passed in options
links,
}}
/>
</UseBug223981FixRepositionSuggestWidget>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,7 @@ export const MonacoEditor = ({
accessibilityOverlayEnabled={settings.isAccessibilityOverlayEnabled}
editorDidMount={editorDidMountCallback}
editorWillUnmount={editorWillUnmountCallback}
links={true}
options={{
fontSize: settings.fontSize,
wordWrap: settings.wrapMode === true ? 'on' : 'off',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const retry = getService('retry');
const browser = getService('browser');
const PageObjects = getPageObjects(['common', 'console', 'header']);
const testSubjects = getService('testSubjects');
const toasts = getService('toasts');

describe('misc console behavior', function testMiscConsoleBehavior() {
Expand Down Expand Up @@ -261,6 +262,90 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
});

describe('clickable links', () => {
let initialWindowHandles: string[];

beforeEach(async () => {
initialWindowHandles = await browser.getAllWindowHandles();
});

afterEach(async () => {
// Close any new tabs that were opened
const currentHandles = await browser.getAllWindowHandles();
if (currentHandles.length > initialWindowHandles.length) {
// Close all new tabs
for (let i = initialWindowHandles.length; i < currentHandles.length; i++) {
await browser.switchToWindow(currentHandles[i]);
await browser.closeCurrentWindow();
}
// Switch back to the original tab
await browser.switchToWindow(initialWindowHandles[0]);
}
});

it('should open URL in new tab when clicking on a link in the input editor', async () => {
await PageObjects.console.clearEditorText();

// Enter a URL that will be detected as a link
await PageObjects.console.enterText('# https://www.elastic.co');

// Wait for Monaco to detect and decorate the link
await retry.waitFor('link to be detected by Monaco', async () => {
const inputEditor = await testSubjects.find('consoleMonacoEditor');
const detectedLinks = await inputEditor.findAllByCssSelector('.detected-link');
return detectedLinks.length > 0;
});

// Get the input editor container
const inputEditor = await testSubjects.find('consoleMonacoEditor');

// Find the detected link element that Monaco created
const detectedLink = await inputEditor.findByCssSelector('.detected-link');

// Perform Cmd/Ctrl+Click on the detected link
// Following the pattern from ES|QL tests where we hover to show tooltip, then click the option
const modifierKey = browser.keys[process.platform === 'darwin' ? 'COMMAND' : 'CONTROL'];

await browser.getActions().keyDown(modifierKey).perform();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we do this given that later we hover over the link and click the link?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are two ways to open the link:

  1. Cmd + Click
  2. Hover over the link to display a tooltip, then click “Follow the link” to open the page in a new tab.

I’ve updated the test so that it now uses only the Cmd + Click method.


// Hover over the detected link to show the tooltip
await detectedLink.moveMouseTo();

// Wait for Monaco hover tooltip to appear
// Monaco shows a tooltip with "Follow link" when hovering over a URL with Cmd/Ctrl held
await retry.waitFor('Monaco hover tooltip to appear', async () => {
const links = await inputEditor.findAllByCssSelector(
'.monaco-hover .rendered-markdown a'
);
return links.length > 0;
});

// Click the "Follow link" anchor in the hover tooltip
// The HTML structure is: .monaco-hover > .hover-contents > .rendered-markdown > a
const followLinkElement = await inputEditor.findByCssSelector(
'.monaco-hover .rendered-markdown a'
);
await followLinkElement.click();

await browser.getActions().keyUp(modifierKey).perform();

// Wait for a new tab to open
await retry.waitFor('new tab to open after clicking link', async () => {
const handles = await browser.getAllWindowHandles();
return handles.length > initialWindowHandles.length;
});

// Verify a new tab was opened
const windowHandles = await browser.getAllWindowHandles();
expect(windowHandles.length).to.be.greaterThan(initialWindowHandles.length);

// Switch to the new tab and verify the URL
await browser.switchToWindow(windowHandles[windowHandles.length - 1]);
const currentUrl = await browser.getCurrentUrl();
expect(currentUrl).to.contain('elastic.co');
});
});

it('should work fine with a large content', async () => {
await PageObjects.console.clearEditorText();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
const security = getService('security');
const es = getService('es');
const log = getService('log');
const testSubjects = getService('testSubjects');
const browser = getService('browser');

describe('Search Profiler Editor', () => {
before(async () => {
Expand Down Expand Up @@ -185,5 +187,46 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
});
});
});

describe('links', () => {
it('does not enable clickable links', async () => {
const linksEnabled = await browser.execute(() => {
const monaco = (window as any).MonacoEnvironment?.monaco;
if (!monaco) return null;

const editors = monaco.editor.getEditors();
const editor = editors.find((e: any) => {
const container = e.getContainerDomNode();
return container?.closest('[data-test-subj="searchProfilerEditor"]');
});

if (editor) {
editor.setValue('# https://www.elastic.co');
return editor.getOptions().get(monaco.editor.EditorOption.links);
}
return null;
});

expect(linksEnabled).to.be(false);

await PageObjects.common.sleep(500);

const editor = await testSubjects.find('searchProfilerEditor');
const detectedLinks = await editor.findAllByCssSelector('.detected-link');
expect(detectedLinks.length).to.be(0);

const modifierKey = browser.keys[process.platform === 'darwin' ? 'COMMAND' : 'CONTROL'];
await browser.getActions().keyDown(modifierKey).perform();

const editorLines = await editor.findAllByClassName('view-line');
await editorLines[0].moveMouseTo();
await PageObjects.common.sleep(500);

const followLinks = await editor.findAllByCssSelector('.monaco-hover .rendered-markdown a');
expect(followLinks.length).to.be(0);

await browser.getActions().keyUp(modifierKey).perform();
});
});
});
}