Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 12 commits into from
Feb 6, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -1063,7 +1063,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",
Expand Down
1 change: 0 additions & 1 deletion packages/shared-ux/code_editor/impl/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ BUNDLER_DEPS = [
"@npm//react",
"@npm//tslib",
"@npm//react-monaco-editor",
"@npm//react-resize-detector",
]

js_library(
Expand Down
2 changes: 0 additions & 2 deletions packages/shared-ux/code_editor/impl/README.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,4 @@ This editor component allows easy access to:
* Function signature widget
* [Hover widget](https://microsoft.github.io/monaco-editor/playground.html#extending-language-services-hover-provider-example)

The Monaco editor doesn't automatically resize the editor area on window or container resize so this component includes a [resize detector](https://github.com/maslianok/react-resize-detector) to cause the Monaco editor to re-layout and adjust its size when the window or container size changes

## API
46 changes: 44 additions & 2 deletions packages/shared-ux/code_editor/impl/code_editor.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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}
/>
);
};

Expand Down Expand Up @@ -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;
77 changes: 56 additions & 21 deletions packages/shared-ux/code_editor/impl/code_editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -133,6 +132,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> = ({
Expand Down Expand Up @@ -160,6 +168,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';
Expand All @@ -181,29 +190,18 @@ export const CodeEditor: React.FC<CodeEditorProps> = ({

const isReadOnly = options?.readOnly ?? false;

const _editor = useRef<monaco.editor.IStandaloneCodeEditor | null>(null);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

had to move editor to setState so that I could properly register the callback that would react to props changes

const [_editor, setEditor] = useState<monaco.editor.IStandaloneCodeEditor | null>(null);
const _placeholderWidget = useRef<PlaceholderWidget | null>(null);
const isSuggestionMenuOpen = useRef(false);
const editorHint = useRef<HTMLDivElement>(null);
const textboxMutationObserver = useRef<MutationObserver | null>(null);

const [isHintActive, setIsHintActive] = useState(true);

const _updateDimensions = useCallback(() => {
_editor.current?.layout();
}, []);

useResizeDetector({
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seemed to be unused. The resize detection is working where it is needed using built in automaticLayout option. I showed it in a new story I created

handleWidth: true,
handleHeight: true,
onResize: _updateDimensions,
refreshMode: 'debounce',
});

const startEditing = useCallback(() => {
setIsHintActive(false);
_editor.current?.focus();
}, []);
_editor?.focus();
}, [_editor]);

const stopEditing = useCallback(() => {
setIsHintActive(true);
Expand Down Expand Up @@ -378,8 +376,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
Expand Down Expand Up @@ -422,6 +418,7 @@ export const CodeEditor: React.FC<CodeEditorProps> = ({
}

editorDidMount?.(editor);
setEditor(editor);
},
[editorDidMount, onBlurMonaco, onKeydownMonaco, readOnlyMessage]
);
Expand All @@ -441,16 +438,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 });

Expand Down Expand Up @@ -499,7 +498,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}
Expand Down Expand Up @@ -627,3 +626,39 @@ 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 };
}) => {
useEffect(() => {
if (!editor) return;

const updateHeight = () => {
if (fitToContent && !isFullScreen) {
const contentHeight = editor.getContentHeight();
const lineHeight = editor.getOption(monaco.editor.EditorOption.lineHeight);
const minHeight = (fitToContent.minLines ?? 1) * lineHeight;
const maxHeight = fitToContent.maxLines
? fitToContent.maxLines * lineHeight
: contentHeight;
editor.layout({
height: Math.min(maxHeight, Math.max(minHeight, contentHeight)),
width: editor.getLayoutInfo().width,
});
} else {
editor.layout();
}
};
updateHeight();
const disposable = editor.onDidContentSizeChange(updateHeight);
return () => {
disposable.dispose();
};
}, [editor, fitToContent, isFullScreen]);
};
12 changes: 11 additions & 1 deletion packages/shared-ux/code_editor/mocks/monaco_mock/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,9 @@ export const MockedMonacoEditor = ({
className?: string;
['data-test-subj']?: string;
}) => {
editorWillMount?.(monaco);
useComponentWillMount(() => {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixing willMount in the mock because now it re-renders twice due to setState(editor) (this is fine, just the tests expectation got broken because of incomplete mock)

editorWillMount?.(monaco);
});

useEffect(() => {
editorDidMount?.(
Expand All @@ -133,3 +135,11 @@ export const MockedMonacoEditor = ({
</div>
);
};

const useComponentWillMount = (cb: Function) => {
const willMount = React.useRef(true);

if (willMount.current) cb();

willMount.current = false;
};
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
.urlTemplateEditor__container {
.monaco-editor .lines-content.monaco-editor-background {
margin: $euiSizeS;
margin: 0 $euiSizeS;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the vertical margin here broke the height calculation. Moved it to padding as part of monaco options

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -31,6 +32,7 @@ export interface UrlTemplateEditorProps {

export const UrlTemplateEditor: React.FC<UrlTemplateEditorProps> = ({
height = 105,
fitToContent,
value,
variables,
onChange,
Expand Down Expand Up @@ -127,6 +129,7 @@ export const UrlTemplateEditor: React.FC<UrlTemplateEditorProps> = ({
<Editor
languageId={HandlebarsLang}
height={height}
fitToContent={fitToContent}
value={value}
onChange={onChange}
editorDidMount={handleEditor}
Expand All @@ -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>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ export const UrlDrilldownCollectConfig: React.FC<UrlDrilldownCollectConfigProps>
labelAppend={variablesDropdown}
>
<UrlTemplateEditor
fitToContent={{ minLines: 5, maxLines: 15 }}
variables={variables}
value={urlTemplate}
placeholder={exampleUrl}
Expand Down
7 changes: 0 additions & 7 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -26009,13 +26009,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"
Expand Down