mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
# Backport This will backport the following commits from `main` to `8.x`: - [[Search profiler] Migrate ace to monaco (#195343)](https://github.com/elastic/kibana/pull/195343) <!--- Backport version: 9.4.3 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) <!--BACKPORT [{"author":{"name":"Ignacio Rivas","email":"rivasign@gmail.com"},"sourceCommit":{"committedDate":"2024-10-09T15:40:10Z","message":"[Search profiler] Migrate ace to monaco (#195343)","sha":"f2b9348f976b96c296b3dc89949115a10c9b19f9","branchLabelMapping":{"^v9.0.0$":"main","^v8.16.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["Team:Kibana Management","release_note:skip","Feature:Search Profiler","v9.0.0","backport:prev-minor"],"title":"[Search profiler] Migrate ace to monaco","number":195343,"url":"https://github.com/elastic/kibana/pull/195343","mergeCommit":{"message":"[Search profiler] Migrate ace to monaco (#195343)","sha":"f2b9348f976b96c296b3dc89949115a10c9b19f9"}},"sourceBranch":"main","suggestedTargetBranches":[],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","branchLabelMappingKey":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/195343","number":195343,"mergeCommit":{"message":"[Search profiler] Migrate ace to monaco (#195343)","sha":"f2b9348f976b96c296b3dc89949115a10c9b19f9"}}]}] BACKPORT--> Co-authored-by: Ignacio Rivas <rivasign@gmail.com>
This commit is contained in:
parent
8bd34c49e5
commit
7647d242bb
13 changed files with 76 additions and 162 deletions
|
@ -3,5 +3,4 @@ $badgeSize: $euiSize * 5.5;
|
|||
@import 'highlight_details_flyout/highlight_details_flyout';
|
||||
@import 'license_warning_notice/license_warning_notice';
|
||||
@import 'percentage_badge/percentage_badge';
|
||||
@import 'profile_query_editor/profile_query_editor';
|
||||
@import 'profile_tree/index';
|
||||
|
|
|
@ -1,25 +0,0 @@
|
|||
|
||||
.prfDevTool__sense {
|
||||
order: 1;
|
||||
// To anchor ace editor
|
||||
position: relative;
|
||||
|
||||
// Ace Editor overrides
|
||||
.ace_editor {
|
||||
min-height: $euiSize * 10;
|
||||
flex-grow: 1;
|
||||
margin-bottom: $euiSize;
|
||||
margin-top: $euiSize;
|
||||
outline: solid 1px $euiColorLightShade;
|
||||
}
|
||||
|
||||
.errorMarker {
|
||||
position: absolute;
|
||||
background: rgba($euiColorDanger, .5);
|
||||
z-index: 20;
|
||||
}
|
||||
}
|
||||
|
||||
.prfDevTool__profileButtonContainer {
|
||||
flex-shrink: 1;
|
||||
}
|
|
@ -5,20 +5,14 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import 'brace';
|
||||
import 'brace/mode/json';
|
||||
|
||||
import { coreMock } from '@kbn/core/public/mocks';
|
||||
import { registerTestBed } from '@kbn/test-jest-helpers';
|
||||
import { Editor, Props } from './editor';
|
||||
|
||||
const coreStart = coreMock.createStart();
|
||||
|
||||
describe('Editor Component', () => {
|
||||
it('renders', async () => {
|
||||
const props: Props = {
|
||||
...coreStart,
|
||||
initialValue: '',
|
||||
editorValue: '',
|
||||
setEditorValue: () => {},
|
||||
licenseEnabled: true,
|
||||
onEditorReady: (e: any) => {},
|
||||
};
|
||||
|
|
|
@ -5,67 +5,37 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { memo, useRef, useEffect, useState } from 'react';
|
||||
import React, { memo, useCallback } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiScreenReaderOnly } from '@elastic/eui';
|
||||
import { Editor as AceEditor } from 'brace';
|
||||
import { EuiScreenReaderOnly, EuiSpacer } from '@elastic/eui';
|
||||
import { CodeEditor } from '@kbn/code-editor';
|
||||
import { monaco, XJsonLang } from '@kbn/monaco';
|
||||
|
||||
import { SearchProfilerStartServices } from '../../../../types';
|
||||
import { ace } from '../../../../shared_imports';
|
||||
import { initializeEditor } from './init_editor';
|
||||
|
||||
const { useUIAceKeyboardMode } = ace;
|
||||
|
||||
type EditorShim = ReturnType<typeof createEditorShim>;
|
||||
|
||||
export type EditorInstance = EditorShim;
|
||||
|
||||
export interface Props extends SearchProfilerStartServices {
|
||||
export interface Props {
|
||||
licenseEnabled: boolean;
|
||||
initialValue: string;
|
||||
onEditorReady: (editor: EditorShim) => void;
|
||||
editorValue: string;
|
||||
setEditorValue: (value: string) => void;
|
||||
onEditorReady: (props: EditorProps) => void;
|
||||
}
|
||||
|
||||
const createEditorShim = (aceEditor: AceEditor) => {
|
||||
return {
|
||||
getValue() {
|
||||
return aceEditor.getValue();
|
||||
},
|
||||
focus() {
|
||||
aceEditor.focus();
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const EDITOR_INPUT_ID = 'SearchProfilerTextArea';
|
||||
|
||||
export interface EditorProps {
|
||||
focus: () => void;
|
||||
}
|
||||
|
||||
export const Editor = memo(
|
||||
({ licenseEnabled, initialValue, onEditorReady, ...startServices }: Props) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null as any);
|
||||
const editorInstanceRef = useRef<AceEditor>(null as any);
|
||||
|
||||
const [textArea, setTextArea] = useState<HTMLTextAreaElement | null>(null);
|
||||
|
||||
useUIAceKeyboardMode(textArea, startServices);
|
||||
|
||||
useEffect(() => {
|
||||
const divEl = containerRef.current;
|
||||
editorInstanceRef.current = initializeEditor({ el: divEl, licenseEnabled });
|
||||
editorInstanceRef.current.setValue(initialValue, 1);
|
||||
const textarea = divEl.querySelector<HTMLTextAreaElement>('textarea');
|
||||
if (textarea) {
|
||||
textarea.setAttribute('id', EDITOR_INPUT_ID);
|
||||
}
|
||||
setTextArea(licenseEnabled ? containerRef.current!.querySelector('textarea') : null);
|
||||
|
||||
onEditorReady(createEditorShim(editorInstanceRef.current));
|
||||
|
||||
return () => {
|
||||
if (editorInstanceRef.current) {
|
||||
editorInstanceRef.current.destroy();
|
||||
}
|
||||
};
|
||||
}, [initialValue, onEditorReady, licenseEnabled]);
|
||||
({ licenseEnabled, editorValue, setEditorValue, onEditorReady }: Props) => {
|
||||
const editorDidMountCallback = useCallback(
|
||||
(editor: monaco.editor.IStandaloneCodeEditor) => {
|
||||
onEditorReady({
|
||||
focus: () => {
|
||||
editor.focus();
|
||||
},
|
||||
} as EditorProps);
|
||||
},
|
||||
[onEditorReady]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -76,7 +46,26 @@ export const Editor = memo(
|
|||
})}
|
||||
</label>
|
||||
</EuiScreenReaderOnly>
|
||||
<div data-test-subj="searchProfilerEditor" ref={containerRef} />
|
||||
|
||||
<EuiSpacer size="m" />
|
||||
<CodeEditor
|
||||
languageId={XJsonLang.ID}
|
||||
dataTestSubj="searchProfilerEditor"
|
||||
value={editorValue}
|
||||
editorDidMount={editorDidMountCallback}
|
||||
options={{
|
||||
readOnly: !licenseEnabled,
|
||||
lineNumbers: 'on',
|
||||
tabSize: 2,
|
||||
automaticLayout: true,
|
||||
overviewRulerLanes: 0,
|
||||
}}
|
||||
aria-label={i18n.translate('xpack.searchProfiler.editor.queryEditor', {
|
||||
defaultMessage: 'Query editor',
|
||||
})}
|
||||
onChange={setEditorValue}
|
||||
/>
|
||||
<EuiSpacer size="m" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -5,5 +5,4 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
export type { EditorInstance } from './editor';
|
||||
export { Editor } from './editor';
|
||||
export { Editor, type EditorProps } from './editor';
|
||||
|
|
|
@ -1,36 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import ace from 'brace';
|
||||
import { installXJsonMode } from '@kbn/ace';
|
||||
|
||||
export function initializeEditor({
|
||||
el,
|
||||
licenseEnabled,
|
||||
}: {
|
||||
el: HTMLDivElement;
|
||||
licenseEnabled: boolean;
|
||||
}) {
|
||||
const editor: ace.Editor = ace.acequire('ace/ace').edit(el);
|
||||
|
||||
installXJsonMode(editor);
|
||||
editor.$blockScrolling = Infinity;
|
||||
|
||||
if (!licenseEnabled) {
|
||||
editor.setReadOnly(true);
|
||||
editor.container.style.pointerEvents = 'none';
|
||||
editor.container.style.opacity = '0.5';
|
||||
const textArea = editor.container.querySelector('textarea');
|
||||
if (textArea) {
|
||||
textArea.setAttribute('tabindex', '-1');
|
||||
}
|
||||
editor.renderer.setStyle('disabled');
|
||||
editor.blur();
|
||||
}
|
||||
|
||||
return editor;
|
||||
}
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useRef, memo, useCallback } from 'react';
|
||||
import React, { useRef, memo, useCallback, useState } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
EuiForm,
|
||||
|
@ -23,7 +23,7 @@ import { decompressFromEncodedURIComponent } from 'lz-string';
|
|||
import { useRequestProfile } from '../../hooks';
|
||||
import { useAppContext } from '../../contexts/app_context';
|
||||
import { useProfilerActionContext } from '../../contexts/profiler_context';
|
||||
import { Editor, EditorInstance } from './editor';
|
||||
import { Editor, type EditorProps } from './editor';
|
||||
|
||||
const DEFAULT_INDEX_VALUE = '_all';
|
||||
|
||||
|
@ -39,33 +39,36 @@ const INITIAL_EDITOR_VALUE = `{
|
|||
* Drives state changes for mine via profiler action context.
|
||||
*/
|
||||
export const ProfileQueryEditor = memo(() => {
|
||||
const editorRef = useRef<EditorInstance>(null as any);
|
||||
const editorPropsRef = useRef<EditorProps>(null as any);
|
||||
const indexInputRef = useRef<HTMLInputElement>(null as any);
|
||||
|
||||
const dispatch = useProfilerActionContext();
|
||||
|
||||
const { getLicenseStatus, notifications, location, ...startServices } = useAppContext();
|
||||
const { getLicenseStatus, notifications, location } = useAppContext();
|
||||
|
||||
const queryParams = new URLSearchParams(location.search);
|
||||
const indexName = queryParams.get('index');
|
||||
const searchProfilerQueryURI = queryParams.get('load_from');
|
||||
|
||||
const searchProfilerQuery =
|
||||
searchProfilerQueryURI &&
|
||||
decompressFromEncodedURIComponent(searchProfilerQueryURI.replace(/^data:text\/plain,/, ''));
|
||||
const [editorValue, setEditorValue] = useState(
|
||||
searchProfilerQuery ? searchProfilerQuery : INITIAL_EDITOR_VALUE
|
||||
);
|
||||
|
||||
const requestProfile = useRequestProfile();
|
||||
|
||||
const handleProfileClick = async () => {
|
||||
dispatch({ type: 'setProfiling', value: true });
|
||||
try {
|
||||
const { current: editor } = editorRef;
|
||||
const { data: result, error } = await requestProfile({
|
||||
query: editorRef.current.getValue(),
|
||||
query: editorValue,
|
||||
index: indexInputRef.current.value,
|
||||
});
|
||||
if (error) {
|
||||
notifications.addDanger(error);
|
||||
editor.focus();
|
||||
editorPropsRef.current.focus();
|
||||
return;
|
||||
}
|
||||
if (result === null) {
|
||||
|
@ -78,18 +81,13 @@ export const ProfileQueryEditor = memo(() => {
|
|||
};
|
||||
|
||||
const onEditorReady = useCallback(
|
||||
(editorInstance: any) => (editorRef.current = editorInstance),
|
||||
(editorPropsInstance: EditorProps) => (editorPropsRef.current = editorPropsInstance),
|
||||
[]
|
||||
);
|
||||
const licenseEnabled = getLicenseStatus().valid;
|
||||
|
||||
return (
|
||||
<EuiFlexGroup
|
||||
responsive={false}
|
||||
className="prfDevTool__sense"
|
||||
gutterSize="none"
|
||||
direction="column"
|
||||
>
|
||||
<EuiFlexGroup responsive={false} gutterSize="none" direction="column">
|
||||
{/* Form */}
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiForm>
|
||||
|
@ -120,9 +118,9 @@ export const ProfileQueryEditor = memo(() => {
|
|||
<EuiFlexItem grow={10}>
|
||||
<Editor
|
||||
onEditorReady={onEditorReady}
|
||||
setEditorValue={setEditorValue}
|
||||
editorValue={editorValue}
|
||||
licenseEnabled={licenseEnabled}
|
||||
initialValue={searchProfilerQuery ? searchProfilerQuery : INITIAL_EDITOR_VALUE}
|
||||
{...startServices}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
|
||||
|
|
|
@ -5,6 +5,4 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
export { ace } from '@kbn/es-ui-shared-plugin/public';
|
||||
|
||||
export { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render';
|
||||
|
|
|
@ -20,9 +20,10 @@
|
|||
"@kbn/expect",
|
||||
"@kbn/test-jest-helpers",
|
||||
"@kbn/i18n-react",
|
||||
"@kbn/ace",
|
||||
"@kbn/config-schema",
|
||||
"@kbn/react-kibana-context-render",
|
||||
"@kbn/code-editor",
|
||||
"@kbn/monaco",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
|
|
|
@ -11,7 +11,7 @@ import { FtrProviderContext } from '../../ftr_provider_context';
|
|||
export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
||||
const PageObjects = getPageObjects(['common', 'security']);
|
||||
const testSubjects = getService('testSubjects');
|
||||
const aceEditor = getService('aceEditor');
|
||||
const monacoEditor = getService('monacoEditor');
|
||||
const a11y = getService('a11y');
|
||||
const esArchiver = getService('esArchiver');
|
||||
|
||||
|
@ -27,7 +27,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
await esArchiver.unload('x-pack/test/functional/es_archives/logstash_functional');
|
||||
});
|
||||
|
||||
it('input the JSON in the aceeditor', async () => {
|
||||
it('input the JSON in the editor', async () => {
|
||||
const input = {
|
||||
query: {
|
||||
bool: {
|
||||
|
@ -54,7 +54,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
},
|
||||
};
|
||||
|
||||
await aceEditor.setValue('searchProfilerEditor', JSON.stringify(input));
|
||||
await monacoEditor.setCodeEditorValue(JSON.stringify(input), 0);
|
||||
await a11y.testAppSnapshot();
|
||||
});
|
||||
|
||||
|
|
|
@ -67,7 +67,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
|
|||
`parser errors to match expectation: HAS ${expectation ? 'ERRORS' : 'NO ERRORS'}`,
|
||||
async () => {
|
||||
const actual = await PageObjects.searchProfiler.editorHasParseErrors();
|
||||
return expectation === actual;
|
||||
return expectation === actual?.length > 0;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@ import { FtrProviderContext } from '../ftr_provider_context';
|
|||
export function SearchProfilerPageProvider({ getService }: FtrProviderContext) {
|
||||
const find = getService('find');
|
||||
const testSubjects = getService('testSubjects');
|
||||
const aceEditor = getService('aceEditor');
|
||||
const monacoEditor = getService('monacoEditor');
|
||||
const editorTestSubjectSelector = 'searchProfilerEditor';
|
||||
|
||||
return {
|
||||
|
@ -19,10 +19,10 @@ export function SearchProfilerPageProvider({ getService }: FtrProviderContext) {
|
|||
return await testSubjects.exists(editorTestSubjectSelector);
|
||||
},
|
||||
async setQuery(query: any) {
|
||||
await aceEditor.setValue(editorTestSubjectSelector, JSON.stringify(query));
|
||||
await monacoEditor.setCodeEditorValue(JSON.stringify(query), 0);
|
||||
},
|
||||
async getQuery() {
|
||||
return JSON.parse(await aceEditor.getValue(editorTestSubjectSelector));
|
||||
return JSON.parse(await monacoEditor.getCodeEditorValue(0));
|
||||
},
|
||||
async setIndexName(indexName: string) {
|
||||
await testSubjects.setValue('indexName', indexName);
|
||||
|
@ -36,6 +36,7 @@ export function SearchProfilerPageProvider({ getService }: FtrProviderContext) {
|
|||
},
|
||||
async getProfileContent() {
|
||||
const profileTree = await find.byClassName('prfDevTool__main__profiletree');
|
||||
// const profileTree = await find.byClassName('prfDevTool__page');
|
||||
return profileTree.getVisibleText();
|
||||
},
|
||||
getUrlWithIndexAndQuery({ indexName, query }: { indexName: string; query: any }) {
|
||||
|
@ -43,7 +44,7 @@ export function SearchProfilerPageProvider({ getService }: FtrProviderContext) {
|
|||
return `/searchprofiler?index=${indexName}&load_from=${searchQueryURI}`;
|
||||
},
|
||||
async editorHasParseErrors() {
|
||||
return await aceEditor.hasParseErrors(editorTestSubjectSelector);
|
||||
return await monacoEditor.getCurrentMarkers(editorTestSubjectSelector);
|
||||
},
|
||||
async editorHasErrorNotification() {
|
||||
const notification = await testSubjects.find('noShardsNotification');
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
import expect from '@kbn/expect';
|
||||
import { FtrProviderContext } from '../../../ftr_provider_context';
|
||||
|
||||
const testIndex = 'test-index';
|
||||
const indexName = 'my_index';
|
||||
const testQuery = {
|
||||
query: {
|
||||
match_all: {},
|
||||
|
@ -53,10 +53,6 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
|
|||
},
|
||||
};
|
||||
|
||||
// Since we're not actually running the query in the test,
|
||||
// this index name is just an input placeholder and does not exist
|
||||
const indexName = 'my_index';
|
||||
|
||||
await PageObjects.common.navigateToUrl(
|
||||
'searchProfiler',
|
||||
PageObjects.searchProfiler.getUrlWithIndexAndQuery({ indexName, query }),
|
||||
|
@ -77,21 +73,21 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
|
|||
|
||||
describe('With a test index', () => {
|
||||
before(async () => {
|
||||
await es.indices.create({ index: testIndex });
|
||||
await es.indices.create({ index: indexName });
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await es.indices.delete({ index: testIndex });
|
||||
await es.indices.delete({ index: indexName });
|
||||
});
|
||||
|
||||
it('profiles a simple query', async () => {
|
||||
await PageObjects.searchProfiler.setIndexName(testIndex);
|
||||
await PageObjects.searchProfiler.setIndexName(indexName);
|
||||
await PageObjects.searchProfiler.setQuery(testQuery);
|
||||
|
||||
await PageObjects.searchProfiler.clickProfileButton();
|
||||
|
||||
const content = await PageObjects.searchProfiler.getProfileContent();
|
||||
expect(content).to.contain(testIndex);
|
||||
expect(content).to.contain(indexName);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue