[Search profiler] Migrate ace to monaco (#195343)

This commit is contained in:
Ignacio Rivas 2024-10-09 17:40:10 +02:00 committed by GitHub
parent 5a71d8445d
commit f2b9348f97
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 76 additions and 162 deletions

View file

@ -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';

View file

@ -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;
}

View file

@ -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) => {},
};

View file

@ -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" />
</>
);
}

View file

@ -5,5 +5,4 @@
* 2.0.
*/
export type { EditorInstance } from './editor';
export { Editor } from './editor';
export { Editor, type EditorProps } from './editor';

View file

@ -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;
}

View file

@ -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>

View file

@ -5,6 +5,4 @@
* 2.0.
*/
export { ace } from '@kbn/es-ui-shared-plugin/public';
export { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render';

View file

@ -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/**/*",

View file

@ -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();
});

View file

@ -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;
}
);
}

View file

@ -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');

View file

@ -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);
});
});
});