mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[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:
parent
43d93baf29
commit
86e8bc197b
9 changed files with 121 additions and 34 deletions
|
@ -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",
|
||||
|
|
|
@ -25,7 +25,6 @@ BUNDLER_DEPS = [
|
|||
"@npm//react",
|
||||
"@npm//tslib",
|
||||
"@npm//react-monaco-editor",
|
||||
"@npm//react-resize-detector",
|
||||
]
|
||||
|
||||
js_library(
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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]);
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
.urlTemplateEditor__container {
|
||||
.monaco-editor .lines-content.monaco-editor-background {
|
||||
margin: $euiSizeS;
|
||||
margin: 0 $euiSizeS;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -88,6 +88,7 @@ export const UrlDrilldownCollectConfig: React.FC<UrlDrilldownCollectConfigProps>
|
|||
labelAppend={variablesDropdown}
|
||||
>
|
||||
<UrlTemplateEditor
|
||||
fitToContent={{ minLines: 5, maxLines: 15 }}
|
||||
variables={variables}
|
||||
value={urlTemplate}
|
||||
placeholder={exampleUrl}
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue