[CodeEditor/UrlDrilldown] Add fitToContent support, autoresize the url template editor (#175561)

## Summary

This PR fixes the paper cut where the URL template editor in URL
drilldown is unusably small. It now can expand as you type longer URLs
fix https://github.com/elastic/kibana/issues/132513

The input box now expands from 5 to 15 lines.
This commit is contained in:
Anton Dosov 2024-02-06 15:38:50 +01:00 committed by GitHub
parent 43d93baf29
commit 86e8bc197b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 121 additions and 34 deletions

View file

@ -1064,7 +1064,6 @@
"react-popper-tooltip": "^3.1.1",
"react-redux": "^7.2.8",
"react-resizable": "^3.0.4",
"react-resize-detector": "^7.1.1",
"react-reverse-portal": "^2.1.0",
"react-router": "^5.3.4",
"react-router-config": "^5.1.1",

View file

@ -25,7 +25,6 @@ BUNDLER_DEPS = [
"@npm//react",
"@npm//tslib",
"@npm//react-monaco-editor",
"@npm//react-resize-detector",
]
js_library(

View file

@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
import React from 'react';
import React, { useState } from 'react';
import { action } from '@storybook/addon-actions';
import { monaco as monacoEditor } from '@kbn/monaco';
@ -32,7 +32,13 @@ const argTypes = mock.getArgumentTypes();
export const Basic = (params: CodeEditorStorybookParams) => {
return (
<CodeEditor {...params} languageId="plainText" onChange={action('on change')} value="Hello!" />
<CodeEditor
{...params}
languageId="plainText"
onChange={action('on change')}
value="Hello!"
height={200}
/>
);
};
@ -199,3 +205,39 @@ export const HoverProvider = () => {
</div>
);
};
export const AutomaticResize = (params: CodeEditorStorybookParams) => {
return (
<div style={{ height: `calc(100vh - 30px)` }}>
<CodeEditor
{...params}
languageId="plainText"
onChange={action('on change')}
value="Hello!"
height={'100%'}
options={{ automaticLayout: true }}
/>
</div>
);
};
AutomaticResize.argTypes = argTypes;
export const FitToContent = (params: CodeEditorStorybookParams) => {
const [value, setValue] = useState('hello');
return (
<CodeEditor
{...params}
languageId="plainText"
onChange={(newValue) => {
setValue(newValue);
action('on change');
}}
value={value}
fitToContent={{ minLines: 3, maxLines: 5 }}
options={{ automaticLayout: true }}
/>
);
};
FitToContent.argTypes = argTypes;

View file

@ -7,7 +7,6 @@
*/
import React, { useState, useRef, useCallback, useMemo, useEffect, KeyboardEvent } from 'react';
import { useResizeDetector } from 'react-resize-detector';
import ReactMonacoEditor, {
type MonacoEditorProps as ReactMonacoEditorProps,
} from 'react-monaco-editor';
@ -140,6 +139,15 @@ export interface CodeEditorProps {
* Alternate text to display, when an attempt is made to edit read only content. (Defaults to "Cannot edit in read-only editor")
*/
readOnlyMessage?: string;
/**
* Enables the editor to grow vertically to fit its content.
* This option overrides the `height` option.
*/
fitToContent?: {
minLines?: number;
maxLines?: number;
};
}
export const CodeEditor: React.FC<CodeEditorProps> = ({
@ -168,6 +176,7 @@ export const CodeEditor: React.FC<CodeEditorProps> = ({
readOnlyMessage = i18n.translate('sharedUXPackages.codeEditor.readOnlyMessage', {
defaultMessage: 'Cannot edit in read-only editor',
}),
fitToContent,
}) => {
const { colorMode, euiTheme } = useEuiTheme();
const useDarkTheme = useDarkThemeProp ?? colorMode === 'DARK';
@ -189,7 +198,7 @@ export const CodeEditor: React.FC<CodeEditorProps> = ({
const isReadOnly = options?.readOnly ?? false;
const _editor = useRef<monaco.editor.IStandaloneCodeEditor | null>(null);
const [_editor, setEditor] = useState<monaco.editor.IStandaloneCodeEditor | null>(null);
const _placeholderWidget = useRef<PlaceholderWidget | null>(null);
const isSuggestionMenuOpen = useRef(false);
const editorHint = useRef<HTMLDivElement>(null);
@ -197,21 +206,10 @@ export const CodeEditor: React.FC<CodeEditorProps> = ({
const [isHintActive, setIsHintActive] = useState(true);
const _updateDimensions = useCallback(() => {
_editor.current?.layout();
}, []);
useResizeDetector({
handleWidth: true,
handleHeight: true,
onResize: _updateDimensions,
refreshMode: 'debounce',
});
const startEditing = useCallback(() => {
setIsHintActive(false);
_editor.current?.focus();
}, []);
_editor?.focus();
}, [_editor]);
const stopEditing = useCallback(() => {
setIsHintActive(true);
@ -391,8 +389,6 @@ export const CodeEditor: React.FC<CodeEditorProps> = ({
remeasureFonts();
_editor.current = editor;
const textbox = editor.getDomNode()?.getElementsByTagName('textarea')[0];
if (textbox) {
// Make sure the textarea is not directly accessible with TAB
@ -435,6 +431,7 @@ export const CodeEditor: React.FC<CodeEditorProps> = ({
}
editorDidMount?.(editor);
setEditor(editor);
},
[editorDidMount, onBlurMonaco, onKeydownMonaco, readOnlyMessage]
);
@ -454,16 +451,18 @@ export const CodeEditor: React.FC<CodeEditorProps> = ({
}, []);
useEffect(() => {
if (placeholder && !value && _editor.current) {
if (placeholder && !value && _editor) {
// Mounts editor inside constructor
_placeholderWidget.current = new PlaceholderWidget(placeholder, euiTheme, _editor.current);
_placeholderWidget.current = new PlaceholderWidget(placeholder, euiTheme, _editor);
}
return () => {
_placeholderWidget.current?.dispose();
_placeholderWidget.current = null;
};
}, [placeholder, value, euiTheme]);
}, [placeholder, value, euiTheme, _editor]);
useFitToContent({ editor: _editor, fitToContent, isFullScreen });
const { CopyButton } = useCopy({ isCopyable, value });
@ -512,7 +511,7 @@ export const CodeEditor: React.FC<CodeEditorProps> = ({
value={value}
onChange={onChange}
width={isFullScreen ? '100vw' : width}
height={isFullScreen ? '100vh' : height}
height={isFullScreen ? '100vh' : fitToContent ? undefined : height}
editorWillMount={_editorWillMount}
editorDidMount={_editorDidMount}
editorWillUnmount={_editorWillUnmount}
@ -640,3 +639,40 @@ const useCopy = ({ isCopyable, value }: { isCopyable: boolean; value: string })
return { showCopyButton, CopyButton };
};
const useFitToContent = ({
editor,
fitToContent,
isFullScreen,
}: {
editor: monaco.editor.IStandaloneCodeEditor | null;
isFullScreen: boolean;
fitToContent?: { minLines?: number; maxLines?: number };
}) => {
const isFitToContent = !!fitToContent;
const minLines = fitToContent?.minLines;
const maxLines = fitToContent?.maxLines;
useEffect(() => {
if (!editor) return;
if (isFullScreen) return;
if (!isFitToContent) return;
const updateHeight = () => {
const contentHeight = editor.getContentHeight();
const lineHeight = editor.getOption(monaco.editor.EditorOption.lineHeight);
const minHeight = (minLines ?? 1) * lineHeight;
let maxHeight = maxLines ? maxLines * lineHeight : contentHeight;
maxHeight = Math.max(minHeight, maxHeight);
editor.layout({
height: Math.min(maxHeight, Math.max(minHeight, contentHeight)),
width: editor.getLayoutInfo().width,
});
};
updateHeight();
const disposable = editor.onDidContentSizeChange(updateHeight);
return () => {
disposable.dispose();
editor.layout(); // reset the layout that was controlled by the fitToContent
};
}, [editor, isFitToContent, minLines, maxLines, isFullScreen]);
};

View file

@ -106,7 +106,9 @@ export const MockedMonacoEditor = ({
className?: string;
['data-test-subj']?: string;
}) => {
editorWillMount?.(monaco);
useComponentWillMount(() => {
editorWillMount?.(monaco);
});
useEffect(() => {
editorDidMount?.(
@ -133,3 +135,11 @@ export const MockedMonacoEditor = ({
</div>
);
};
const useComponentWillMount = (cb: Function) => {
const willMount = React.useRef(true);
if (willMount.current) cb();
willMount.current = false;
};

View file

@ -1,5 +1,5 @@
.urlTemplateEditor__container {
.monaco-editor .lines-content.monaco-editor-background {
margin: $euiSizeS;
margin: 0 $euiSizeS;
}
}

View file

@ -22,6 +22,7 @@ export interface UrlTemplateEditorVariable {
export interface UrlTemplateEditorProps {
value: string;
height?: CodeEditorProps['height'];
fitToContent?: CodeEditorProps['fitToContent'];
variables?: UrlTemplateEditorVariable[];
onChange: CodeEditorProps['onChange'];
onEditor?: (editor: monaco.editor.IStandaloneCodeEditor) => void;
@ -31,6 +32,7 @@ export interface UrlTemplateEditorProps {
export const UrlTemplateEditor: React.FC<UrlTemplateEditorProps> = ({
height = 105,
fitToContent,
value,
variables,
onChange,
@ -127,6 +129,7 @@ export const UrlTemplateEditor: React.FC<UrlTemplateEditorProps> = ({
<Editor
languageId={HandlebarsLang}
height={height}
fitToContent={fitToContent}
value={value}
onChange={onChange}
editorDidMount={handleEditor}
@ -152,6 +155,10 @@ export const UrlTemplateEditor: React.FC<UrlTemplateEditorProps> = ({
},
wordWrap: 'on',
wrappingIndent: 'none',
automaticLayout: true,
scrollBeyondLastLine: false,
overviewRulerLanes: 0,
padding: { top: 8, bottom: 8 },
}}
/>
</div>

View file

@ -88,6 +88,7 @@ export const UrlDrilldownCollectConfig: React.FC<UrlDrilldownCollectConfigProps>
labelAppend={variablesDropdown}
>
<UrlTemplateEditor
fitToContent={{ minLines: 5, maxLines: 15 }}
variables={variables}
value={urlTemplate}
placeholder={exampleUrl}

View file

@ -26016,13 +26016,6 @@ react-resizable@^3.0.4:
prop-types "15.x"
react-draggable "^4.0.3"
react-resize-detector@^7.1.1:
version "7.1.1"
resolved "https://registry.yarnpkg.com/react-resize-detector/-/react-resize-detector-7.1.1.tgz#18d5b84909d5ab13abe0a68ddf0fb8e80c553dfc"
integrity sha512-rU54VTstNzFLZAmMNHqt8xINjDWP7SQR05A2HUW0OGvl4vcrXzgaxrrqAY5tZMfkLkoYm5u0i0qGqCjdc2jyAA==
dependencies:
lodash "^4.17.21"
react-reverse-portal@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/react-reverse-portal/-/react-reverse-portal-2.1.0.tgz#3c572e1c0d9e49b8febf4bf2fd43b9819ce6f508"