diff --git a/src/components/CodeEditor/HeightControllerBar.tsx b/src/components/CodeEditor/HeightControllerBar.tsx new file mode 100644 index 0000000..3837593 --- /dev/null +++ b/src/components/CodeEditor/HeightControllerBar.tsx @@ -0,0 +1,118 @@ +import React, { type RefObject } from 'react'; +import { css } from '@emotion/css'; +import { IconButton, Text, Tooltip, useStyles2 } from '@grafana/ui'; +import type { GrafanaTheme2 } from '@grafana/data'; + +function getShrinkIcon( + editorHeight: number | undefined, + containerHeight: number | undefined, + viewHeight33InPx: number +) { + if (editorHeight === undefined || containerHeight === undefined) { + return 'arrow-up'; + } + if (editorHeight === 64) { + return 'arrow-left'; + } + if (editorHeight < 64) { + return 'arrow-down'; + } + if (viewHeight33InPx < Math.round(containerHeight)) { + return 'angle-double-up'; + } + return 'arrow-up'; +} + +function getExpandIcon( + editorHeight: number | undefined, + containerHeight: number | undefined, + viewHeight33InPx: number +) { + if (editorHeight === undefined || containerHeight === undefined) { + return 'arrow-down'; + } + if (viewHeight33InPx === Math.round(containerHeight)) { + return 'arrow-left'; + } + if (viewHeight33InPx < Math.round(containerHeight)) { + return 'arrow-up'; + } + if (editorHeight < 64) { + return 'angle-double-down'; + } + return 'arrow-down'; +} + +export function HeightControllerBar({ + containerRef, + editorHeight, + containerHeight, +}: { + containerRef: RefObject; + editorHeight: number | undefined; + containerHeight: number | undefined; +}) { + const styles = useStyles2(getStyles); + + const viewHeight33InPx = Math.round(window.innerHeight * 0.33); + const editorHeightOffset = (containerHeight && editorHeight && containerHeight - editorHeight) ?? 0; + + const shrinkIcon = getShrinkIcon(editorHeight, containerHeight, viewHeight33InPx); + const expandIcon = getExpandIcon(editorHeight, containerHeight, viewHeight33InPx); + + const actuallySetContainerHeight = (height: string) => { + if (!containerRef.current) { + return; + } + containerRef.current.style.height = height; + }; + + return ( +
+ +
+ + {editorHeight === 5 ? '(min) ' : ''} + {editorHeight === 64 ? '(default) ' : ''} + {editorHeight !== undefined ? editorHeight.toFixed() + 'px' : ''} + +
+
+
+ actuallySetContainerHeight(`${64 + editorHeightOffset}px`)} + /> + actuallySetContainerHeight('33vh')} + /> +
+
+ ); +} + +function getStyles(theme: GrafanaTheme2) { + return { + bar: css` + display: flex; + align-items: center; + justify-content: end; + padding-right: 14px; + gap: 8px; + border: 1px solid ${theme.colors.border.weak}; + border-top: none; + `, + heightText: css` + height: 24px; + display: flex; + gap: 4px; + `, + }; +} diff --git a/src/components/CodeEditor/index.tsx b/src/components/CodeEditor/index.tsx index 5b7b9b6..279dcd4 100644 --- a/src/components/CodeEditor/index.tsx +++ b/src/components/CodeEditor/index.tsx @@ -1,7 +1,9 @@ -import React, { FC, useEffect, useState } from 'react'; +import React, { FC, useEffect, useLayoutEffect, useState, useRef, type RefObject } from 'react'; import { CodeEditorOptionSettings, OptionsInterface } from 'types'; -import { CodeEditor as GrafanaCodeEditor, Monaco, MonacoEditor } from '@grafana/ui'; -import { StandardEditorContext } from '@grafana/data'; +import { CodeEditor as GrafanaCodeEditor, Monaco, MonacoEditor, useStyles2 } from '@grafana/ui'; +import { StandardEditorContext, type GrafanaTheme2 } from '@grafana/data'; +import { HeightControllerBar } from './HeightControllerBar'; +import { css } from '@emotion/css'; interface Props { settings?: CodeEditorOptionSettings; @@ -10,24 +12,63 @@ interface Props { onChange: (value: string) => void; } -export const CodeEditor: FC = ({ settings, value, context, onChange }) => { - const [monaco, setMonaco] = useState(); +const EDITOR_BORDER_SIZE = 2; // Grafana has a 1px border on the editor container +const BAR_HEIGHT = 24; +const BAR_BORDER_SIZE = 1; // The bar only has a bottom border +const EDITOR_HEIGHT_OFFSET = EDITOR_BORDER_SIZE + BAR_HEIGHT + BAR_BORDER_SIZE; // 2px + 24px + 1px = 27px - const editorDidMount = async (_: MonacoEditor, m: Monaco) => { - setMonaco(m); - }; +function useSizeController(containerRef: RefObject, editor: MonacoEditor | undefined) { + const [containerSize, setContainerSize] = useState<{ height: number; width: number }>(); + const [editorSize, setEditorSize] = useState<{ height: number; width: number }>(); + + useLayoutEffect(() => { + if (!containerRef.current) { + return; + } + + const resizeObserver = new ResizeObserver((entries) => { + if (entries.length !== 1) { + return; + } + const entry = entries[0]; + + const editorHeight = entry.contentRect.height - EDITOR_HEIGHT_OFFSET; + const editorWidth = entry.contentRect.width - EDITOR_BORDER_SIZE; + + setContainerSize({ height: entry.contentRect.height, width: entry.contentRect.width }); + setEditorSize({ height: editorHeight, width: editorWidth }); + + if (editor) { + editor.layout({ height: editorHeight, width: editorWidth }); + } + }); + + resizeObserver.observe(containerRef.current); + + return () => { + resizeObserver.disconnect(); + }; + }, [containerRef, editor]); + + return { containerSize, editorSize }; +} +function useTypeDeclarationUpdater( + monaco: Monaco | undefined, + htmlGraphicsDeclarationState: CodeEditorOptionSettings['htmlGraphicsDeclarationState'] | undefined, + codeData: string | undefined +) { useEffect(() => { - if (!monaco || !settings?.htmlGraphicsDeclarationState?.enabled) { + if (!monaco || !htmlGraphicsDeclarationState?.enabled) { return; } - if (settings.htmlGraphicsDeclarationState.declarationsLoaded) { + if (htmlGraphicsDeclarationState.declarationsLoaded) { // onInit and onRender would normally both load the declarations, // but this makes only one of them load the declarations return; } else { - settings.htmlGraphicsDeclarationState.declarationsLoaded = true; + htmlGraphicsDeclarationState.declarationsLoaded = true; } const reqDecl = require.context('./declarations', true, /\..*\.d\.ts$/); @@ -53,17 +94,17 @@ export const CodeEditor: FC = ({ settings, value, context, onChange }) => monaco.languages.typescript.javascriptDefaults.addExtraLib(d[i], truncatedPath); }); }); - }, [monaco, settings?.htmlGraphicsDeclarationState]); + }, [monaco, htmlGraphicsDeclarationState]); useEffect(() => { - if (!monaco || context.options?.codeData === undefined || !settings?.htmlGraphicsDeclarationState?.enabled) { + if (!monaco || codeData === undefined || !htmlGraphicsDeclarationState?.enabled) { return; } - if (settings.htmlGraphicsDeclarationState.handlingCustomPropertiesUpdate) { + if (htmlGraphicsDeclarationState.handlingCustomPropertiesUpdate) { return; } else { - settings.htmlGraphicsDeclarationState.handlingCustomPropertiesUpdate = true; + htmlGraphicsDeclarationState.handlingCustomPropertiesUpdate = true; } const createCustomPropertiesType = (json: string) => { @@ -78,28 +119,67 @@ export const CodeEditor: FC = ({ settings, value, context, onChange }) => } }; - const content = createCustomPropertiesType(context.options.codeData); + const content = createCustomPropertiesType(codeData); monaco.languages.typescript.javascriptDefaults.addExtraLib(content, 'customProperties.d.ts'); return () => { - if (!settings.htmlGraphicsDeclarationState) { + if (!htmlGraphicsDeclarationState) { return; } - settings.htmlGraphicsDeclarationState.handlingCustomPropertiesUpdate = false; + htmlGraphicsDeclarationState.handlingCustomPropertiesUpdate = false; }; - }, [monaco, settings?.htmlGraphicsDeclarationState, context.options?.codeData]); + }, [monaco, htmlGraphicsDeclarationState, codeData]); +} + +export const CodeEditor: FC = ({ settings, value, context, onChange }) => { + const styles = useStyles2( + getStyles, + `${64 + EDITOR_HEIGHT_OFFSET}px` // // 64px + 27px = 91px + ); + + const [monaco, setMonaco] = useState(); + const [editor, setEditor] = useState(); + + const containerRef = useRef(null); + + const { containerSize, editorSize } = useSizeController(containerRef, editor); + useTypeDeclarationUpdater(monaco, settings?.htmlGraphicsDeclarationState, context.options?.codeData); + + const editorDidMount = async (e: MonacoEditor, m: Monaco) => { + e.layout(); + setMonaco(m); + setEditor(e); + }; return ( -
+
+
); }; + +function getStyles(_theme: GrafanaTheme2, defaultHeight: string) { + return { + container: css` + resize: vertical; + overflow: hidden; + height: ${defaultHeight}; + min-height: 32px; + display: flex; + flex-direction: column; + justify-content: space-between; + `, + }; +}