-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(hog): refactor code editors, hog validation (#23054)
- Loading branch information
1 parent
cdccce9
commit 0a7cf88
Showing
35 changed files
with
608 additions
and
460 deletions.
There are no files selected for viewing
Binary file modified
BIN
+19 Bytes
(100%)
frontend/__snapshots__/scenes-app-insights--user-paths-edit--dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file was deleted.
Oops, something went wrong.
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,111 @@ | ||
import './CodeEditor.scss' | ||
|
||
import MonacoEditor, { type EditorProps, Monaco } from '@monaco-editor/react' | ||
import { useMountedLogic, useValues } from 'kea' | ||
import { Spinner } from 'lib/lemon-ui/Spinner' | ||
import { codeEditorLogic } from 'lib/monaco/codeEditorLogic' | ||
import { hogQLAutocompleteProvider } from 'lib/monaco/hogQLAutocompleteProvider' | ||
import { hogQLMetadataProvider } from 'lib/monaco/hogQLMetadataProvider' | ||
import * as hog from 'lib/monaco/languages/hog' | ||
import * as hogQL from 'lib/monaco/languages/hogql' | ||
import { inStorybookTestRunner } from 'lib/utils' | ||
import type { editor as importedEditor, IDisposable } from 'monaco-editor' | ||
import { useEffect, useRef, useState } from 'react' | ||
|
||
import { themeLogic } from '~/layout/navigation-3000/themeLogic' | ||
|
||
export interface CodeEditorProps extends Omit<EditorProps, 'loading' | 'theme'> { | ||
queryKey?: string | ||
} | ||
let codeEditorIndex = 0 | ||
|
||
export function CodeEditor({ queryKey, options, onMount, value, ...editorProps }: CodeEditorProps): JSX.Element { | ||
const { isDarkModeOn } = useValues(themeLogic) | ||
const scrollbarRendering = !inStorybookTestRunner() ? 'auto' : 'hidden' | ||
const [monacoAndEditor, setMonacoAndEditor] = useState( | ||
null as [Monaco, importedEditor.IStandaloneCodeEditor] | null | ||
) | ||
const [monaco, editor] = monacoAndEditor ?? [] | ||
|
||
const [realKey] = useState(() => codeEditorIndex++) | ||
const builtCodeEditorLogic = codeEditorLogic({ | ||
key: queryKey ?? `new/${realKey}`, | ||
query: value ?? '', | ||
language: editorProps.language, | ||
monaco: monaco, | ||
editor: editor, | ||
}) | ||
useMountedLogic(builtCodeEditorLogic) | ||
|
||
// Using useRef, not useState, as we don't want to reload the component when this changes. | ||
const monacoDisposables = useRef([] as IDisposable[]) | ||
useEffect(() => { | ||
return () => { | ||
monacoDisposables.current.forEach((d) => d?.dispose()) | ||
} | ||
}, []) | ||
|
||
return ( | ||
<MonacoEditor // eslint-disable-line react/forbid-elements | ||
theme={isDarkModeOn ? 'vs-dark' : 'vs-light'} | ||
loading={<Spinner />} | ||
options={{ | ||
// :TRICKY: We need to declare all options here, as omitting something will carry its value from one <CodeEditor> to another. | ||
minimap: { | ||
enabled: false, | ||
}, | ||
scrollBeyondLastLine: false, | ||
automaticLayout: true, | ||
fixedOverflowWidgets: true, | ||
wordWrap: 'off', | ||
lineNumbers: 'on', | ||
...options, | ||
padding: { bottom: 8, top: 8 }, | ||
scrollbar: { | ||
vertical: scrollbarRendering, | ||
horizontal: scrollbarRendering, | ||
...options?.scrollbar, | ||
}, | ||
}} | ||
value={value} | ||
{...editorProps} | ||
onMount={(editor, monaco) => { | ||
setMonacoAndEditor([monaco, editor]) | ||
if (editorProps?.language === 'hog') { | ||
if (!monaco.languages.getLanguages().some(({ id }) => id === 'hog')) { | ||
monaco.languages.register({ id: 'hog', extensions: ['.hog'], mimetypes: ['application/hog'] }) | ||
monaco.languages.setLanguageConfiguration('hog', hog.conf) | ||
monaco.languages.setMonarchTokensProvider('hog', hog.language) | ||
} | ||
monacoDisposables.current.push( | ||
monaco.languages.registerCodeActionProvider('hog', hogQLMetadataProvider(builtCodeEditorLogic)) | ||
) | ||
} | ||
if (editorProps?.language === 'hogql') { | ||
if (!monaco.languages.getLanguages().some(({ id }) => id === 'hogql')) { | ||
monaco.languages.register({ | ||
id: 'hogql', | ||
extensions: ['.sql', '.hogql'], | ||
mimetypes: ['application/hog+ql'], | ||
}) | ||
monaco.languages.setLanguageConfiguration('hogql', hogQL.conf) | ||
monaco.languages.setMonarchTokensProvider('hogql', hogQL.language) | ||
} | ||
monacoDisposables.current.push( | ||
monaco.languages.registerCompletionItemProvider( | ||
'hogql', | ||
hogQLAutocompleteProvider(builtCodeEditorLogic) | ||
) | ||
) | ||
monacoDisposables.current.push( | ||
monaco.languages.registerCodeActionProvider( | ||
'hogql', | ||
hogQLMetadataProvider(builtCodeEditorLogic) | ||
) | ||
) | ||
} | ||
onMount?.(editor, monaco) | ||
}} | ||
/> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
import { CodeEditor, CodeEditorProps } from 'lib/monaco/CodeEditor' | ||
import { useEffect, useRef, useState } from 'react' | ||
import AutoSizer from 'react-virtualized/dist/es/AutoSizer' | ||
|
||
export function CodeEditorResizeable({ | ||
height: defaultHeight, | ||
minHeight = '5rem', | ||
maxHeight = '90vh', | ||
...props | ||
}: Omit<CodeEditorProps, 'height'> & { | ||
height?: number | ||
minHeight?: string | number | ||
maxHeight?: string | number | ||
}): JSX.Element { | ||
const [height, setHeight] = useState(defaultHeight) | ||
const [manualHeight, setManualHeight] = useState<number | undefined>(defaultHeight) | ||
|
||
const ref = useRef<HTMLDivElement | null>(null) | ||
|
||
useEffect(() => { | ||
const value = typeof props.value !== 'string' ? JSON.stringify(props.value, null, 2) : props.value | ||
const lineCount = (value?.split('\n').length ?? 1) + 1 | ||
const lineHeight = 18 | ||
setHeight(lineHeight * lineCount) | ||
}, [props.value]) | ||
|
||
return ( | ||
<div | ||
ref={ref} | ||
className="CodeEditorResizeable relative border rounded" | ||
// eslint-disable-next-line react/forbid-dom-props | ||
style={{ | ||
minHeight, | ||
maxHeight, | ||
height: manualHeight ?? height, | ||
}} | ||
> | ||
<AutoSizer disableWidth> | ||
{({ height }) => ( | ||
<CodeEditor | ||
{...props} | ||
height={height - 2} // Account for border | ||
/> | ||
)} | ||
</AutoSizer> | ||
|
||
{/* Using a standard resize css means we need overflow-hidden which hides parts of the editor unnecessarily */} | ||
<div | ||
className="absolute bottom-0 right-0 z-10 resize-y h-5 w-5 cursor-s-resize overflow-hidden" | ||
onMouseDown={(e) => { | ||
const startY = e.clientY | ||
const startHeight = ref.current?.clientHeight ?? 0 | ||
const onMouseMove = (event: MouseEvent): void => { | ||
setManualHeight(startHeight + event.clientY - startY) | ||
} | ||
const onMouseUp = (): void => { | ||
window.removeEventListener('mousemove', onMouseMove) | ||
window.removeEventListener('mouseup', onMouseUp) | ||
} | ||
window.addEventListener('mousemove', onMouseMove) | ||
window.addEventListener('mouseup', onMouseUp) | ||
}} | ||
/> | ||
</div> | ||
) | ||
} |
Oops, something went wrong.