Skip to content

Commit

Permalink
[Console] Implement documentation action button (#181057)
Browse files Browse the repository at this point in the history
## Summary
Closes #180209 

This PR implements the "view documentation" button in the new Monaco
editor in Console. The code re-use the existing autocomplete
functionality and gets the documentation link for the current request
from autocomplete definitions. The current request is the 1st request of
the user selection in the editor. The link is opened in the new tab and
if no link is available or the request is unknown, then nothing happens
(existing functionality, we might want to hide the button in that case
in a [follow up work](#180911))

### Screen recording 



https://github.com/elastic/kibana/assets/6585477/56ea016c-02b6-4134-97b7-914204557d61

### How to test
1. Add `console.dev.enableMonaco: true` to the `config/kibana.dev.yml`
file
2. Start Kibana and ES locally
3. Navigate to the Dev tools Console and try using the "view
documentation" button for various requests

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
  • Loading branch information
2 people authored and pull[bot] committed Jul 15, 2024
1 parent 860b268 commit 779b7f3
Show file tree
Hide file tree
Showing 5 changed files with 158 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export const MonacoEditor = ({ initialTextValue }: EditorProps) => {
settings: settingsService,
autocompleteInfo,
},
docLinkVersion,
} = useServicesContext();
const { toasts } = notifications;
const { settings } = useEditorReadContext();
Expand All @@ -53,6 +54,10 @@ export const MonacoEditor = ({ initialTextValue }: EditorProps) => {
return curl ?? '';
}, [esHostService]);

const getDocumenationLink = useCallback(async () => {
return actionsProvider.current!.getDocumentationLink(docLinkVersion);
}, [docLinkVersion]);

const sendRequestsCallback = useCallback(async () => {
await actionsProvider.current?.sendRequests(toasts, dispatch, trackUiMetric, http);
}, [dispatch, http, toasts, trackUiMetric]);
Expand Down Expand Up @@ -103,9 +108,7 @@ export const MonacoEditor = ({ initialTextValue }: EditorProps) => {
<EuiFlexItem>
<ConsoleMenu
getCurl={getCurlCallback}
getDocumentation={() => {
return Promise.resolve(null);
}}
getDocumentation={getDocumenationLink}
autoIndent={() => {}}
notifications={notifications}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@
* Mock kbn/monaco to provide the console parser code directly without a web worker
*/
const mockGetParsedRequests = jest.fn();

/*
* Mock the function "populateContext" that accesses the autocomplete definitions
*/
const mockPopulateContext = jest.fn();

jest.mock('@kbn/monaco', () => {
const original = jest.requireActual('@kbn/monaco');
return {
Expand All @@ -33,6 +39,14 @@ jest.mock('../../../../services', () => {
};
});

jest.mock('../../../../lib/autocomplete/engine', () => {
return {
populateContext: (...args: any) => {
mockPopulateContext(args);
},
};
});

import { MonacoEditorActionsProvider } from './monaco_editor_actions_provider';
import { monaco } from '@kbn/monaco';

Expand Down Expand Up @@ -101,4 +115,36 @@ describe('Editor actions provider', () => {
expect(curl).toBe('curl -XGET "http://localhost/_search" -H "kbn-xsrf: reporting"');
});
});

describe('getDocumentationLink', () => {
const docLinkVersion = '8.13';
const docsLink = 'http://elastic.co/_search';
// mock the populateContext function that finds the correct autocomplete endpoint object and puts it into the context object
mockPopulateContext.mockImplementation((...args) => {
const context = args[0][1];
context.endpoint = {
documentation: docsLink,
};
});
it('returns null if no requests', async () => {
mockGetParsedRequests.mockResolvedValue([]);
const link = await editorActionsProvider.getDocumentationLink(docLinkVersion);
expect(link).toBe(null);
});

it('returns null if there is a request but not in the selection range', async () => {
editor.getSelection.mockReturnValue({
// the request is on line 1, the user selected line 2
startLineNumber: 2,
endLineNumber: 2,
} as unknown as monaco.Selection);
const link = await editorActionsProvider.getDocumentationLink(docLinkVersion);
expect(link).toBe(null);
});

it('returns the correct link if there is a request in the selection range', async () => {
const link = await editorActionsProvider.getDocumentationLink(docLinkVersion);
expect(link).toBe(docsLink);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,11 @@ import {
import { IToasts } from '@kbn/core-notifications-browser';
import { i18n } from '@kbn/i18n';
import type { HttpSetup } from '@kbn/core-http-browser';
import { AutoCompleteContext } from '../../../../lib/autocomplete/types';
import { populateContext } from '../../../../lib/autocomplete/engine';
import { DEFAULT_VARIABLES } from '../../../../../common/constants';
import { getStorage, StorageKeys } from '../../../../services';
import { getTopLevelUrlCompleteComponents } from '../../../../lib/kb';
import { sendRequest } from '../../../hooks/use_send_current_request/send_request';
import { MetricsTracker } from '../../../../types';
import { Actions } from '../../../stores/request';
Expand All @@ -27,6 +30,8 @@ import {
replaceRequestVariables,
getCurlRequest,
trackSentRequests,
tokenizeRequestUrl,
getDocumentationLinkFromAutocompleteContext,
} from './utils';

const selectedRequestsClass = 'console__monaco_editor__selectedRequests';
Expand Down Expand Up @@ -235,4 +240,29 @@ export class MonacoEditorActionsProvider {
}
}
}

public async getDocumentationLink(docLinkVersion: string): Promise<string | null> {
const requests = await this.getRequests();
if (requests.length < 1) {
return null;
}
const request = requests[0];

// get autocomplete components for the request method
const components = getTopLevelUrlCompleteComponents(request.method);
// get the url parts from the request url
const urlTokens = tokenizeRequestUrl(request.url);

// this object will contain the information later, it needs to be initialized with some data
// similar to the old ace editor context
const context: AutoCompleteContext = {
method: request.method,
urlTokenPath: urlTokens,
};

// this function uses the autocomplete info and the url tokens to find the correct endpoint
populateContext(urlTokens, context, undefined, true, components);

return getDocumentationLinkFromAutocompleteContext(context, docLinkVersion);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,15 @@

import {
getCurlRequest,
getDocumentationLinkFromAutocompleteContext,
removeTrailingWhitespaces,
replaceRequestVariables,
stringifyRequest,
tokenizeRequestUrl,
trackSentRequests,
} from './utils';
import { MetricsTracker } from '../../../../types';
import { AutoCompleteContext } from '../../../../lib/autocomplete/types';

describe('monaco editor utils', () => {
const dataObjects = [
Expand Down Expand Up @@ -179,4 +182,46 @@ describe('monaco editor utils', () => {
expect(mockMetricsTracker.count).toHaveBeenNthCalledWith(2, 'POST__test');
});
});

describe('tokenizeRequestUrl', () => {
it('returns the url if it has only 1 part', () => {
const url = '_search';
const urlTokens = tokenizeRequestUrl(url);
expect(urlTokens).toEqual(['_search', '__url_path_end__']);
});

it('returns correct url tokens', () => {
const url = '_search/test';
const urlTokens = tokenizeRequestUrl(url);
expect(urlTokens).toEqual(['_search', 'test', '__url_path_end__']);
});
});

describe('getDocumentationLinkFromAutocompleteContext', () => {
const version = '8.13';
const expectedLink = 'http://elastic.co/8.13/_search';
it('correctly replaces {branch} with the version', () => {
const endpoint = {
documentation: 'http://elastic.co/{branch}/_search',
} as AutoCompleteContext['endpoint'];
const link = getDocumentationLinkFromAutocompleteContext({ endpoint }, version);
expect(link).toBe(expectedLink);
});

it('correctly replaces /master/ with the version', () => {
const endpoint = {
documentation: 'http://elastic.co/master/_search',
} as AutoCompleteContext['endpoint'];
const link = getDocumentationLinkFromAutocompleteContext({ endpoint }, version);
expect(link).toBe(expectedLink);
});

it('correctly replaces /current/ with the version', () => {
const endpoint = {
documentation: 'http://elastic.co/current/_search',
} as AutoCompleteContext['endpoint'];
const link = getDocumentationLinkFromAutocompleteContext({ endpoint }, version);
expect(link).toBe(expectedLink);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
*/

import { ParsedRequest } from '@kbn/monaco';
import { AutoCompleteContext } from '../../../../lib/autocomplete/types';
import { constructUrl } from '../../../../lib/es';
import type { DevToolsVariable } from '../../../components';
import { EditorRequest } from './monaco_editor_actions_provider';
Expand Down Expand Up @@ -74,3 +75,33 @@ export const trackSentRequests = (
trackUiMetric.count(eventName);
});
};

/*
* This function takes a request url as a string and returns it parts,
* for example '_search/test' => ['_search', 'test']
*/
const urlPartsSeparatorRegex = /\//;
const endOfUrlToken = '__url_path_end__';
export const tokenizeRequestUrl = (url: string): string[] => {
const parts = url.split(urlPartsSeparatorRegex);
// this special token is used to mark the end of the url
parts.push(endOfUrlToken);
return parts;
};

/*
* This function returns a documentation link from the autocomplete endpoint object
* and replaces the branch in the url with the current version "docLinkVersion"
*/
export const getDocumentationLinkFromAutocompleteContext = (
{ endpoint }: AutoCompleteContext,
docLinkVersion: string
): string | null => {
if (endpoint && endpoint.documentation && endpoint.documentation.indexOf('http') !== -1) {
return endpoint.documentation
.replace('/master/', `/${docLinkVersion}/`)
.replace('/current/', `/${docLinkVersion}/`)
.replace('/{branch}/', `/${docLinkVersion}/`);
}
return null;
};

0 comments on commit 779b7f3

Please sign in to comment.