Skip to content

Commit bd6ce9c

Browse files
authored
Merge pull request #357 from RedisInsight/feature/2441-dedicated-cypher-editor
Feature/2441 dedicated cypher editor
2 parents 06acb6f + 9fe958c commit bd6ce9c

File tree

29 files changed

+1627
-69
lines changed

29 files changed

+1627
-69
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,7 @@
251251
"react-jsx-parser": "^1.28.4",
252252
"react-monaco-editor": "^0.44.0",
253253
"react-redux": "^7.2.2",
254+
"react-rnd": "^10.3.5",
254255
"react-router-dom": "^5.2.0",
255256
"react-virtualized": "^9.22.2",
256257
"rehype-stringify": "^9.0.2",
Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
import React, { useEffect, useRef } from 'react'
2+
import { compact, findIndex } from 'lodash'
3+
import * as monacoEditor from 'monaco-editor/esm/vs/editor/editor.api'
4+
import MonacoEditor, { monaco } from 'react-monaco-editor'
5+
import { Rnd } from 'react-rnd'
6+
import cx from 'classnames'
7+
import { EuiButtonIcon } from '@elastic/eui'
8+
9+
import {
10+
DSL,
11+
DSLNaming,
12+
MonacoLanguage,
13+
MonacoSyntaxLang,
14+
} from 'uiSrc/constants'
15+
import {
16+
decoration,
17+
getMonacoAction,
18+
MonacoAction,
19+
Nullable,
20+
toModelDeltaDecoration
21+
} from 'uiSrc/utils'
22+
import { IEditorMount, ISnippetController } from 'uiSrc/pages/workbench/interfaces'
23+
import { getCypherCompletionProvider } from 'uiSrc/utils/monaco/cypher/completionProvider'
24+
import {
25+
cypherLanguageConfiguration,
26+
} from 'uiSrc/constants/monaco/cypher'
27+
import { getCypherMonarchTokensProvider } from 'uiSrc/utils/monaco/cypher/monarchTokensProvider'
28+
29+
import styles from './styles.module.scss'
30+
31+
export interface Props {
32+
value: string
33+
lang: string
34+
onSubmit: (query?: string) => void
35+
onCancel: () => void
36+
onKeyDown?: (e: React.KeyboardEvent, script: string) => void
37+
width: number
38+
}
39+
40+
const langs: MonacoSyntaxLang = {
41+
[DSL.cypher]: {
42+
name: DSLNaming[DSL.cypher],
43+
id: MonacoLanguage.Cypher,
44+
config: cypherLanguageConfiguration,
45+
completionProvider: getCypherCompletionProvider,
46+
tokensProvider: getCypherMonarchTokensProvider
47+
}
48+
}
49+
let decorations: string[] = []
50+
51+
const DedicatedEditor = (props: Props) => {
52+
const { width, value = '', lang, onCancel, onSubmit } = props
53+
const selectedLang = langs[lang]
54+
let contribution: Nullable<ISnippetController> = null
55+
const monacoObjects = useRef<Nullable<IEditorMount>>(null)
56+
let disposeCompletionItemProvider = () => {}
57+
58+
useEffect(() =>
59+
// componentWillUnmount
60+
() => {
61+
contribution?.dispose?.()
62+
disposeCompletionItemProvider()
63+
},
64+
[])
65+
66+
useEffect(() => {
67+
if (!monacoObjects.current) return
68+
const commands = value.split('\n')
69+
const { monaco, editor } = monacoObjects.current
70+
const notCommandRegEx = /^\s|\/\//
71+
72+
const newDecorations = compact(commands.map((command, index) => {
73+
if (!command || notCommandRegEx.test(command)) return null
74+
const lineNumber = index + 1
75+
76+
return toModelDeltaDecoration(
77+
decoration(monaco, `decoration_${lineNumber}`, lineNumber, 1, lineNumber, 1)
78+
)
79+
}))
80+
81+
decorations = editor.deltaDecorations(
82+
decorations,
83+
newDecorations
84+
)
85+
}, [value])
86+
87+
const handleKeyDown = (e: React.KeyboardEvent) => {
88+
if (e.key === 'Escape') {
89+
onCancel()
90+
}
91+
}
92+
93+
const handleSubmit = () => {
94+
const { editor } = monacoObjects?.current || {}
95+
onSubmit(editor?.getValue() || '')
96+
}
97+
98+
const onKeyDownMonaco = (e: monacoEditor.IKeyboardEvent) => {
99+
// trigger parameter hints
100+
if (e.keyCode === monaco.KeyCode.Enter || e.keyCode === monaco.KeyCode.Space) {
101+
onExitSnippetMode()
102+
}
103+
}
104+
105+
const onExitSnippetMode = () => {
106+
if (!monacoObjects.current) return
107+
const { editor } = monacoObjects?.current
108+
109+
if (contribution?.isInSnippet?.()) {
110+
const { lineNumber = 0, column = 0 } = editor?.getPosition() ?? {}
111+
editor.setSelection(new monaco.Selection(lineNumber, column, lineNumber, column))
112+
contribution?.cancel?.()
113+
}
114+
}
115+
116+
const editorDidMount = (
117+
editor: monacoEditor.editor.IStandaloneCodeEditor,
118+
monaco: typeof monacoEditor
119+
) => {
120+
monacoObjects.current = { editor, monaco }
121+
122+
// hack for exit from snippet mode after click Enter until no answer from monaco authors
123+
// https://github.com/microsoft/monaco-editor/issues/2756
124+
contribution = editor.getContribution<ISnippetController>('snippetController2')
125+
126+
editor.focus()
127+
128+
editor.onKeyDown(onKeyDownMonaco)
129+
130+
setupMonacoLang(monaco)
131+
editor.addAction(
132+
getMonacoAction(MonacoAction.Submit, () => handleSubmit(), monaco)
133+
)
134+
}
135+
136+
const setupMonacoLang = (monaco: typeof monacoEditor) => {
137+
const languages = monaco.languages.getLanguages()
138+
139+
const selectedLang = langs[lang]
140+
if (!selectedLang) return
141+
142+
const isLangRegistered = findIndex(languages, { id: selectedLang.id }) > -1
143+
if (!isLangRegistered) {
144+
monaco.languages.register({ id: selectedLang.id })
145+
}
146+
147+
monaco.languages.setLanguageConfiguration(selectedLang.id, selectedLang.config)
148+
149+
disposeCompletionItemProvider = monaco.languages.registerCompletionItemProvider(
150+
selectedLang.id,
151+
selectedLang.completionProvider()
152+
).dispose
153+
154+
monaco.languages.setMonarchTokensProvider(
155+
selectedLang.id,
156+
selectedLang.tokensProvider()
157+
)
158+
}
159+
160+
const options: monacoEditor.editor.IStandaloneEditorConstructionOptions = {
161+
tabCompletion: 'on',
162+
wordWrap: 'on',
163+
padding: { top: 10 },
164+
automaticLayout: true,
165+
formatOnPaste: false,
166+
suggest: {
167+
preview: false,
168+
showStatusBar: false,
169+
showIcons: true,
170+
},
171+
minimap: {
172+
enabled: false
173+
},
174+
overviewRulerLanes: 0,
175+
hideCursorInOverviewRuler: true,
176+
overviewRulerBorder: false,
177+
lineNumbersMinChars: 4
178+
}
179+
180+
return (
181+
<Rnd
182+
default={{
183+
x: 17,
184+
y: 80,
185+
width,
186+
height: 240
187+
}}
188+
className={styles.rnd}
189+
dragHandleClassName="draggable-area"
190+
>
191+
<div className={styles.container} onKeyDown={handleKeyDown} role="textbox" tabIndex={0}>
192+
<div className="draggable-area" />
193+
<div className={styles.input} data-testid="query-input-container">
194+
<MonacoEditor
195+
language={selectedLang.id || MonacoLanguage.Cypher}
196+
value={value}
197+
options={options}
198+
className={`${lang}-editor`}
199+
editorDidMount={editorDidMount}
200+
/>
201+
</div>
202+
<div className={cx(styles.actions)}>
203+
<span>{ selectedLang.name }</span>
204+
<div>
205+
<EuiButtonIcon
206+
iconSize="m"
207+
iconType="cross"
208+
color="primary"
209+
aria-label="Cancel editing"
210+
className={styles.declineBtn}
211+
onClick={onCancel}
212+
data-testid="cancel-btn"
213+
/>
214+
<EuiButtonIcon
215+
iconSize="m"
216+
iconType="check"
217+
color="primary"
218+
type="submit"
219+
aria-label="Apply"
220+
onClick={handleSubmit}
221+
className={styles.applyBtn}
222+
data-testid="apply-btn"
223+
/>
224+
</div>
225+
</div>
226+
</div>
227+
</Rnd>
228+
)
229+
}
230+
231+
export default React.memo(DedicatedEditor)
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
.rnd {
2+
position: fixed;
3+
z-index: 100;
4+
}
5+
.container {
6+
height: 100%;
7+
word-break: break-word;
8+
text-align: left;
9+
letter-spacing: 0;
10+
background-color: var(--monacoBgColor);
11+
color: var(--euiTextSubduedColor) !important;
12+
border: 1px solid var(--euiColorPrimary);
13+
border-radius: 4px;
14+
padding-left: 6px;
15+
padding-right: 6px;
16+
box-shadow: 0 5px 15px var(--controlsBoxShadowColor);
17+
}
18+
19+
.containerPlaceholder {
20+
display: flex;
21+
background-color: var(--monacoBgColor);
22+
color: var(--euiTextSubduedColor) !important;
23+
border: 1px solid var(--euiColorLightShade);
24+
border-radius: 4px;
25+
overflow: hidden;
26+
> div {
27+
border: 1px solid var(--euiColorLightShade);
28+
background-color: var(--euiColorEmptyShade);
29+
padding: 8px 20px;
30+
width: 100%;
31+
}
32+
}
33+
34+
.input {
35+
height: calc(100% - 46px);
36+
width: 100%;
37+
background-color: var(--rsInputColor);
38+
}
39+
40+
#script {
41+
font: normal normal bold 14px/17px Inconsolata !important;
42+
color: var(--textColorShade);
43+
caret-color: var(--euiColorFullShade);
44+
min-width: 5px;
45+
display: inline;
46+
}
47+
48+
:global(.draggable-area) {
49+
height: 20px;
50+
width: 100%;
51+
cursor: grab;
52+
background-color: var(--monacoBgColor);
53+
border-radius: 4px 4px 0 0;
54+
}
55+
56+
.actions {
57+
height: 26px;
58+
width: 100%;
59+
display: flex;
60+
align-items: center;
61+
padding: 6px 12px;
62+
background-color: var(--monacoBgColor);
63+
border-radius: 0 0 4px 4px;
64+
justify-content: space-between;
65+
}
66+
67+
.declineBtn:hover {
68+
color: var(--euiColorColorDanger) !important;
69+
}
70+
71+
.applyBtn {
72+
margin-left: 6px;
73+
&:hover {
74+
color: var(--euiColorPrimary) !important;
75+
}
76+
}
77+
78+
.submitButton {
79+
color: var(--rsSubmitBtn) !important;
80+
width: 44px !important;
81+
height: 44px !important;
82+
83+
svg {
84+
width: 24px;
85+
height: 24px;
86+
}
87+
}

0 commit comments

Comments
 (0)