mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[Console] Implement documentation action button (#181057)
## Summary
Closes https://github.com/elastic/kibana/issues/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](https://github.com/elastic/kibana/issues/180911))
### Screen recording
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>
This commit is contained in:
parent
753e8c7917
commit
e18d19fafc
5 changed files with 158 additions and 3 deletions
|
@ -37,6 +37,7 @@ export const MonacoEditor = ({ initialTextValue }: EditorProps) => {
|
|||
settings: settingsService,
|
||||
autocompleteInfo,
|
||||
},
|
||||
docLinkVersion,
|
||||
} = useServicesContext();
|
||||
const { toasts } = notifications;
|
||||
const { settings } = useEditorReadContext();
|
||||
|
@ -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]);
|
||||
|
@ -103,9 +108,7 @@ export const MonacoEditor = ({ initialTextValue }: EditorProps) => {
|
|||
<EuiFlexItem>
|
||||
<ConsoleMenu
|
||||
getCurl={getCurlCallback}
|
||||
getDocumentation={() => {
|
||||
return Promise.resolve(null);
|
||||
}}
|
||||
getDocumentation={getDocumenationLink}
|
||||
autoIndent={() => {}}
|
||||
notifications={notifications}
|
||||
/>
|
||||
|
|
|
@ -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 {
|
||||
|
@ -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';
|
||||
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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';
|
||||
|
@ -27,6 +30,8 @@ import {
|
|||
replaceRequestVariables,
|
||||
getCurlRequest,
|
||||
trackSentRequests,
|
||||
tokenizeRequestUrl,
|
||||
getDocumentationLinkFromAutocompleteContext,
|
||||
} from './utils';
|
||||
|
||||
const selectedRequestsClass = 'console__monaco_editor__selectedRequests';
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 = [
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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';
|
||||
|
@ -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;
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue