Skip to content

Commit

Permalink
Open in dev tools button for request inspector (#109923)
Browse files Browse the repository at this point in the history
Add a "Open in Dev Tools" link to the request inspector.

Allow the dev tools to open data uris that are lz-string encoded (the same method used by TypeScript Playground, which are a lot shorter than a base64 encoded string.)
  • Loading branch information
smith authored Aug 25, 2021
1 parent 4fced99 commit 406df4d
Show file tree
Hide file tree
Showing 13 changed files with 200 additions and 62 deletions.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -375,8 +375,8 @@
"redux-saga": "^1.1.3",
"redux-thunk": "^2.3.0",
"redux-thunks": "^1.0.0",
"remark-stringify": "^9.0.0",
"regenerator-runtime": "^0.13.3",
"remark-stringify": "^9.0.0",
"request": "^2.88.0",
"require-in-the-middle": "^5.0.2",
"reselect": "^4.0.0",
Expand Down Expand Up @@ -570,6 +570,7 @@
"@types/loader-utils": "^1.1.3",
"@types/lodash": "^4.14.159",
"@types/lru-cache": "^5.1.0",
"@types/lz-string": "^1.3.34",
"@types/mapbox-gl": "1.13.1",
"@types/markdown-it": "^0.0.7",
"@types/md5": "^2.2.0",
Expand Down
15 changes: 14 additions & 1 deletion src/plugins/console/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,17 @@

## About

Console provides the user with tools for storing and executing requests against Elasticsearch.
Console provides the user with tools for storing and executing requests against Elasticsearch.

## Features

### `load_from` query parameter

The `load_from` query parameter enables opening Console with prepopulated reuqests in two ways: from the elastic.co docs and from within other parts of Kibana.

Plugins can open requests in Kibana by assigning this parameter a `data:text/plain` [lz-string](https://pieroxy.net/blog/pages/lz-string/index.html) encoded value. For example, navigating to `/dev_tools#/console?load_from=data:text/plain,OIUQKgBA+gzgpgQwE4GMAWAoA3gIgI4CucSAnjgFy4C2CALulAgDZMVYC+nQA` will prepopulate Console with the following request:

```
GET _search
{"query":{"match_all":{}}}
```
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiScreenReaderOnly, EuiToolTip } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { debounce } from 'lodash';
import { decompressFromEncodedURIComponent } from 'lz-string';
import { parse } from 'query-string';
import React, { CSSProperties, useCallback, useEffect, useRef, useState } from 'react';
import { ace } from '../../../../../../../es_ui_shared/public';
Expand Down Expand Up @@ -96,6 +97,8 @@ function EditorUI({ initialTextValue }: EditorProps) {
};

const loadBufferFromRemote = (url: string) => {
const coreEditor = editor.getCoreEditor();

if (/^https?:\/\//.test(url)) {
const loadFrom: Record<string, any> = {
url,
Expand All @@ -111,14 +114,35 @@ function EditorUI({ initialTextValue }: EditorProps) {

// Fire and forget.
$.ajax(loadFrom).done(async (data) => {
const coreEditor = editor.getCoreEditor();
await editor.update(data, true);
editor.moveToNextRequestEdge(false);
coreEditor.clearSelection();
editor.highlightCurrentRequestsAndUpdateActionBar();
coreEditor.getContainer().focus();
});
}

// If we have a data URI instead of HTTP, LZ-decode it. This enables
// opening requests in Console from anywhere in Kibana.
if (/^data:/.test(url)) {
const data = decompressFromEncodedURIComponent(url.replace(/^data:text\/plain,/, ''));

// Show a toast if we have a failure
if (data === null || data === '') {
notifications.toasts.addWarning(
i18n.translate('console.loadFromDataUriErrorMessage', {
defaultMessage: 'Unable to load data from the load_from query parameter in the URL',
})
);
return;
}

editor.update(data, true);
editor.moveToNextRequestEdge(false);
coreEditor.clearSelection();
editor.highlightCurrentRequestsAndUpdateActionBar();
coreEditor.getContainer().focus();
}
};

// Support for loading a console snippet from a remote source, like support docs.
Expand Down Expand Up @@ -176,7 +200,14 @@ function EditorUI({ initialTextValue }: EditorProps) {
editorInstanceRef.current.getCoreEditor().destroy();
}
};
}, [saveCurrentTextObject, initialTextValue, history, setInputEditor, settingsService]);
}, [
notifications.toasts,
saveCurrentTextObject,
initialTextValue,
history,
setInputEditor,
settingsService,
]);

useEffect(() => {
const { current: editor } = editorInstanceRef;
Expand Down
6 changes: 5 additions & 1 deletion src/plugins/inspector/public/plugin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,11 @@ export class InspectorPublicPlugin implements Plugin<Setup, Start> {
adapters={adapters}
title={options.title}
options={options.options}
dependencies={{ uiSettings: core.uiSettings }}
dependencies={{
application: core.application,
http: core.http,
uiSettings: core.uiSettings,
}}
/>
),
{
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 8 additions & 4 deletions src/plugins/inspector/public/ui/inspector_panel.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,16 @@ import { mountWithIntl } from '@kbn/test/jest';
import { InspectorPanel } from './inspector_panel';
import { InspectorViewDescription } from '../types';
import { Adapters } from '../../common';
import type { IUiSettingsClient } from 'kibana/public';
import type { ApplicationStart, HttpSetup, IUiSettingsClient } from 'kibana/public';

describe('InspectorPanel', () => {
let adapters: Adapters;
let views: InspectorViewDescription[];
const uiSettings: IUiSettingsClient = {} as IUiSettingsClient;
const dependencies = {
application: {},
http: {},
uiSettings: {},
} as { application: ApplicationStart; http: HttpSetup; uiSettings: IUiSettingsClient };

beforeEach(() => {
adapters = {
Expand Down Expand Up @@ -54,14 +58,14 @@ describe('InspectorPanel', () => {

it('should render as expected', () => {
const component = mountWithIntl(
<InspectorPanel adapters={adapters} views={views} dependencies={{ uiSettings }} />
<InspectorPanel adapters={adapters} views={views} dependencies={dependencies} />
);
expect(component).toMatchSnapshot();
});

it('should not allow updating adapters', () => {
const component = mountWithIntl(
<InspectorPanel adapters={adapters} views={views} dependencies={{ uiSettings }} />
<InspectorPanel adapters={adapters} views={views} dependencies={dependencies} />
);
adapters.notAllowed = {};
expect(() => component.setProps({ adapters })).toThrow();
Expand Down
4 changes: 3 additions & 1 deletion src/plugins/inspector/public/ui/inspector_panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {
EuiFlyoutBody,
EuiLoadingSpinner,
} from '@elastic/eui';
import { IUiSettingsClient } from 'kibana/public';
import { ApplicationStart, HttpStart, IUiSettingsClient } from 'kibana/public';
import { InspectorViewDescription } from '../types';
import { Adapters } from '../../common';
import { InspectorViewChooser } from './inspector_view_chooser';
Expand All @@ -41,6 +41,8 @@ interface InspectorPanelProps {
options?: unknown;
views: InspectorViewDescription[];
dependencies: {
application: ApplicationStart;
http: HttpStart;
uiSettings: IUiSettingsClient;
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,68 +6,111 @@
* Side Public License, v 1.
*/

import React from 'react';
// Since we're not using `RedirectAppLinks`, we need to use `navigateToUrl` when
// handling the click of the Open in Dev Tools link. We want to have both an
// `onClick` handler and an `href` attribute so it will work on click without a
// page reload, and on right-click to open in new tab.
/* eslint-disable @elastic/eui/href-or-on-click */

import { EuiButtonEmpty, EuiCopy, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { XJsonLang } from '@kbn/monaco';
import { EuiFlexItem, EuiFlexGroup, EuiCopy, EuiButtonEmpty, EuiSpacer } from '@elastic/eui';

import { CodeEditor } from '../../../../../../kibana_react/public';
import { compressToEncodedURIComponent } from 'lz-string';
import React, { MouseEvent, useCallback } from 'react';
import { CodeEditor, useKibana } from '../../../../../../kibana_react/public';

interface RequestCodeViewerProps {
indexPattern?: string;
json: string;
}

const copyToClipboardLabel = i18n.translate('inspector.requests.copyToClipboardLabel', {
defaultMessage: 'Copy to clipboard',
});

const openInDevToolsLabel = i18n.translate('inspector.requests.openInDevToolsLabel', {
defaultMessage: 'Open in Dev Tools',
});

/**
* @internal
*/
export const RequestCodeViewer = ({ json }: RequestCodeViewerProps) => (
<EuiFlexGroup
direction="column"
gutterSize="s"
wrap={false}
responsive={false}
className="insRequestCodeViewer"
>
<EuiFlexItem grow={false}>
<EuiSpacer size="s" />
<div className="eui-textRight">
<EuiCopy textToCopy={json}>
{(copy) => (
export const RequestCodeViewer = ({ indexPattern, json }: RequestCodeViewerProps) => {
const { services } = useKibana();
const prepend = services.http?.basePath?.prepend;
const navigateToUrl = services.application?.navigateToUrl;
const canShowDevTools = services.application?.capabilities?.dev_tools.show;
const devToolsDataUri = compressToEncodedURIComponent(`GET ${indexPattern}/_search\n${json}`);
const devToolsUrl = `/app/dev_tools#/console?load_from=data:text/plain,${devToolsDataUri}`;
const shouldShowDevToolsLink = !!(indexPattern && canShowDevTools);

const handleDevToolsLinkClick = useCallback(
(event: MouseEvent<HTMLAnchorElement>) => {
event.preventDefault();
if (navigateToUrl && prepend) {
navigateToUrl(prepend(devToolsUrl));
}
},
[devToolsUrl, navigateToUrl, prepend]
);

return (
<EuiFlexGroup
direction="column"
gutterSize="s"
wrap={false}
responsive={false}
className="insRequestCodeViewer"
>
<EuiFlexItem grow={false}>
<EuiSpacer size="s" />
<div className="eui-textRight">
<EuiCopy textToCopy={json}>
{(copy) => (
<EuiButtonEmpty
size="xs"
flush="right"
iconType="copyClipboard"
onClick={copy}
data-test-subj="inspectorRequestCopyClipboardButton"
>
{copyToClipboardLabel}
</EuiButtonEmpty>
)}
</EuiCopy>
{shouldShowDevToolsLink && (
<EuiButtonEmpty
size="xs"
flush="right"
iconType="copyClipboard"
onClick={copy}
data-test-subj="inspectorRequestCopyClipboardButton"
iconType="wrench"
href={prepend && prepend(devToolsUrl)}
onClick={handleDevToolsLinkClick}
data-test-subj="inspectorRequestOpenInDevToolsButton"
>
{copyToClipboardLabel}
{openInDevToolsLabel}
</EuiButtonEmpty>
)}
</EuiCopy>
</div>
</EuiFlexItem>
<EuiFlexItem grow={true}>
<CodeEditor
languageId={XJsonLang.ID}
value={json}
options={{
readOnly: true,
lineNumbers: 'off',
fontSize: 12,
minimap: {
enabled: false,
},
folding: true,
scrollBeyondLastLine: false,
wordWrap: 'on',
wrappingIndent: 'indent',
automaticLayout: true,
}}
/>
</EuiFlexItem>
</EuiFlexGroup>
);
</div>
</EuiFlexItem>
<EuiFlexItem grow={true}>
<CodeEditor
languageId={XJsonLang.ID}
value={json}
options={{
readOnly: true,
lineNumbers: 'off',
fontSize: 12,
minimap: {
enabled: false,
},
folding: true,
scrollBeyondLastLine: false,
wordWrap: 'on',
wrappingIndent: 'indent',
automaticLayout: true,
}}
/>
</EuiFlexItem>
</EuiFlexGroup>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ export class RequestDetailsRequest extends Component<RequestDetailsProps> {
return null;
}

return <RequestCodeViewer json={JSON.stringify(json, null, 2)} />;
return (
<RequestCodeViewer
indexPattern={this.props.request.stats?.indexPattern?.value}
json={JSON.stringify(json, null, 2)}
/>
);
}
}
Loading

0 comments on commit 406df4d

Please sign in to comment.