Skip to content

Commit ade8b62

Browse files
authored
feat: added Monaco Editor for query input (#3)
1 parent c603652 commit ade8b62

File tree

4 files changed

+287
-35
lines changed

4 files changed

+287
-35
lines changed

package-lock.json

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@
6262
"@grafana/data": "9.3.8",
6363
"@grafana/runtime": "9.3.8",
6464
"@grafana/ui": "9.3.8",
65+
"monaco-editor": "^0.34.0",
6566
"react": "17.0.2",
6667
"react-dom": "17.0.2",
6768
"tslib": "2.5.0"

src/components/QueryEditor.tsx

Lines changed: 36 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
import React, { useEffect, useState } from 'react';
2-
import { InlineLabel, QueryField, Select, Switch } from '@grafana/ui';
1+
import React, { useEffect, useRef, useState } from 'react';
2+
import { InlineLabel, Select, Switch } from '@grafana/ui';
33
import { QueryEditorProps } from '@grafana/data';
44
import { DataSource } from '../datasource';
55
import { MyDataSourceOptions, MyQuery } from '../types';
66
import { getStreams } from '../services/streams';
77
import { getOrganizations } from '../services/organizations';
88
import { css } from '@emotion/css';
9+
import { ZincEditor } from './ZincEditor';
910

1011
type Props = QueryEditorProps<DataSource, MyQuery, MyDataSourceOptions>;
1112

@@ -26,7 +27,6 @@ export function QueryEditor({ query, onChange, onRunQuery, datasource }: Props)
2627
setupStreams(orgs.data[0].name).then((streams: any) => {
2728
onChange({
2829
...query,
29-
query: '',
3030
stream: streams[0].name,
3131
organization: orgs.data[0].name,
3232
streamFields: streams[0].schema,
@@ -95,7 +95,9 @@ export function QueryEditor({ query, onChange, onRunQuery, datasource }: Props)
9595
};
9696

9797
const onChangeQuery = (queryText: string) => {
98-
onChange({ ...query, query: queryText, queryType: 'logs' });
98+
if (query.query !== queryText) {
99+
onChange({ ...query, query: queryText, queryType: 'logs' });
100+
}
99101
};
100102

101103
const streamUpdated = (stream: any) => {
@@ -135,22 +137,6 @@ export function QueryEditor({ query, onChange, onRunQuery, datasource }: Props)
135137

136138
return (
137139
<div>
138-
<div
139-
className={css`
140-
display: flex;
141-
align-items: center;
142-
`}
143-
>
144-
<InlineLabel
145-
className={css`
146-
width: fit-content;
147-
`}
148-
transparent={true}
149-
>
150-
SQL Mode
151-
</InlineLabel>
152-
<Switch value={!!query.sqlMode} onChange={toggleSqlMode} />
153-
</div>
154140
<div
155141
className={css`
156142
display: flex;
@@ -170,16 +156,16 @@ export function QueryEditor({ query, onChange, onRunQuery, datasource }: Props)
170156
`}
171157
transparent
172158
>
173-
Select Stream
159+
Select Organization
174160
</InlineLabel>
175161
<Select
176162
className={css`
177163
width: 200px !important;
178164
margin: 8px 0px;
179165
`}
180-
options={streamOptions}
181-
value={query.stream}
182-
onChange={streamUpdated}
166+
options={orgOptions}
167+
value={query.organization}
168+
onChange={orgUpdated}
183169
/>
184170
</div>
185171
<div
@@ -194,28 +180,43 @@ export function QueryEditor({ query, onChange, onRunQuery, datasource }: Props)
194180
`}
195181
transparent
196182
>
197-
Select Organization
183+
Select Stream
198184
</InlineLabel>
199185
<Select
200186
className={css`
201187
width: 200px !important;
202188
margin: 8px 0px;
203189
`}
204-
options={orgOptions}
205-
value={query.organization}
206-
onChange={orgUpdated}
190+
options={streamOptions}
191+
value={query.stream}
192+
onChange={streamUpdated}
207193
/>
208194
</div>
209195
</div>
210-
<QueryField
196+
<div
197+
className={css`
198+
display: flex;
199+
align-items: center;
200+
padding-bottom: 0.5rem;
201+
`}
202+
>
203+
<InlineLabel
204+
className={css`
205+
width: fit-content;
206+
`}
207+
transparent={true}
208+
>
209+
SQL Mode
210+
</InlineLabel>
211+
<Switch value={!!query.sqlMode} onChange={toggleSqlMode} />
212+
</div>
213+
<ZincEditor
211214
query={query.query}
212-
// By default QueryField calls onChange if onBlur is not defined, this will trigger a rerender
213-
// And slate will claim the focus, making it impossible to leave the field.
214-
onBlur={() => {}}
215-
onChange={onChangeQuery}
215+
onChange={() => onChangeQuery}
216216
placeholder="Enter a zinc query"
217-
portalOrigin="zincObserve"
218-
/>
217+
fields={query.streamFields || []}
218+
runQuery={onRunQuery}
219+
></ZincEditor>
219220
</div>
220221
);
221222
}

src/components/ZincEditor.tsx

Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
import React, { useRef } from 'react';
2+
import * as monaco from 'monaco-editor';
3+
import { css } from '@emotion/css';
4+
import { ReactMonacoEditor, monacoTypes } from '@grafana/ui';
5+
6+
interface Props {
7+
query: string;
8+
onChange: (queryText: string) => void;
9+
placeholder: string;
10+
fields: any[];
11+
runQuery: () => void;
12+
mode?: boolean;
13+
}
14+
15+
export function ZincEditor({ query, onChange, placeholder, fields, runQuery }: Props) {
16+
const reactMonacoEditorRef = useRef(null);
17+
const options: monacoTypes.editor.IStandaloneEditorConstructionOptions = {
18+
wordWrap: 'on',
19+
lineNumbers: 'on',
20+
lineNumbersMinChars: 0,
21+
overviewRulerLanes: 0,
22+
fixedOverflowWidgets: false,
23+
overviewRulerBorder: false,
24+
lineDecorationsWidth: 3,
25+
hideCursorInOverviewRuler: true,
26+
renderLineHighlight: 'none',
27+
glyphMargin: false,
28+
folding: false,
29+
scrollBeyondLastColumn: 0,
30+
scrollBeyondLastLine: true,
31+
scrollbar: { horizontal: 'auto', vertical: 'visible' },
32+
find: {
33+
addExtraSpaceOnTop: false,
34+
autoFindInSelection: 'never',
35+
seedSearchStringFromSelection: 'never',
36+
},
37+
minimap: { enabled: false },
38+
};
39+
40+
const createDependencyProposals = (range: any) => {
41+
const keywords = [
42+
{
43+
label: 'and',
44+
kind: monaco.languages.CompletionItemKind.Keyword,
45+
insertText: 'and ',
46+
range: range,
47+
},
48+
{
49+
label: 'or',
50+
kind: monaco.languages.CompletionItemKind.Keyword,
51+
insertText: 'or ',
52+
range: range,
53+
},
54+
{
55+
label: 'like',
56+
kind: monaco.languages.CompletionItemKind.Keyword,
57+
insertText: "like '%${1:params}%' ",
58+
range: range,
59+
},
60+
{
61+
label: 'in',
62+
kind: monaco.languages.CompletionItemKind.Keyword,
63+
insertText: "in ('${1:params}') ",
64+
range: range,
65+
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
66+
},
67+
{
68+
label: 'not in',
69+
kind: monaco.languages.CompletionItemKind.Keyword,
70+
insertText: "not in ('${1:params}') ",
71+
range: range,
72+
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
73+
},
74+
{
75+
label: 'between',
76+
kind: monaco.languages.CompletionItemKind.Keyword,
77+
insertText: "between '${1:params}' and '${1:params}' ",
78+
range: range,
79+
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
80+
},
81+
{
82+
label: 'not between',
83+
kind: monaco.languages.CompletionItemKind.Keyword,
84+
insertText: "not between '${1:params}' and '${1:params}' ",
85+
range: range,
86+
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
87+
},
88+
{
89+
label: 'is null',
90+
kind: monaco.languages.CompletionItemKind.Keyword,
91+
insertText: 'is null ',
92+
range: range,
93+
},
94+
{
95+
label: 'is not null',
96+
kind: monaco.languages.CompletionItemKind.Keyword,
97+
insertText: 'is not null ',
98+
range: range,
99+
},
100+
{
101+
label: '>',
102+
kind: monaco.languages.CompletionItemKind.Operator,
103+
insertText: '> ',
104+
range: range,
105+
},
106+
{
107+
label: '<',
108+
kind: monaco.languages.CompletionItemKind.Operator,
109+
insertText: '< ',
110+
range: range,
111+
},
112+
{
113+
label: '>=',
114+
kind: monaco.languages.CompletionItemKind.Operator,
115+
insertText: '>= ',
116+
range: range,
117+
},
118+
{
119+
label: '<=',
120+
kind: monaco.languages.CompletionItemKind.Operator,
121+
insertText: '<= ',
122+
range: range,
123+
},
124+
{
125+
label: '<>',
126+
kind: monaco.languages.CompletionItemKind.Operator,
127+
insertText: '<> ',
128+
range: range,
129+
},
130+
{
131+
label: '=',
132+
kind: monaco.languages.CompletionItemKind.Operator,
133+
insertText: '= ',
134+
range: range,
135+
},
136+
{
137+
label: '!=',
138+
kind: monaco.languages.CompletionItemKind.Operator,
139+
insertText: '!= ',
140+
range: range,
141+
},
142+
{
143+
label: '()',
144+
kind: monaco.languages.CompletionItemKind.Keyword,
145+
insertText: '(${1:condition}) ',
146+
range: range,
147+
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
148+
},
149+
];
150+
151+
fields.forEach((field: any) => {
152+
if (field.name === '_timestamp') {
153+
return;
154+
}
155+
let itemObj = {
156+
label: field.name,
157+
kind: monaco.languages.CompletionItemKind.Text,
158+
insertText: field.name,
159+
range: range,
160+
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
161+
};
162+
keywords.push(itemObj);
163+
});
164+
165+
return keywords;
166+
};
167+
168+
const onEditorMount = (editor: any, monaco: any) => {
169+
console.log(reactMonacoEditorRef.current);
170+
monaco.languages.registerCompletionItemProvider('sql', {
171+
provideCompletionItems: function (model, position) {
172+
// find out if we are completing a property in the 'dependencies' object.
173+
let textUntilPosition = model.getValueInRange({
174+
startLineNumber: 1,
175+
startColumn: 1,
176+
endLineNumber: position.lineNumber,
177+
endColumn: position.column,
178+
});
179+
180+
let word = model.getWordUntilPosition(position);
181+
let range = {
182+
startLineNumber: position.lineNumber,
183+
endLineNumber: position.lineNumber,
184+
startColumn: word.startColumn,
185+
endColumn: word.endColumn,
186+
};
187+
188+
let arr = textUntilPosition.trim().split(' ');
189+
let filteredSuggestions = [];
190+
filteredSuggestions = createDependencyProposals(range);
191+
filteredSuggestions = filteredSuggestions.filter((item) => {
192+
return item.label.toLowerCase().includes(word.word.toLowerCase());
193+
});
194+
195+
// if (filteredSuggestions.length == 0) {
196+
const lastElement = arr.pop();
197+
198+
filteredSuggestions.push({
199+
label: `match_all('${lastElement}')`,
200+
kind: monaco.languages.CompletionItemKind.Text,
201+
insertText: `match_all('${lastElement}')`,
202+
range: range,
203+
});
204+
filteredSuggestions.push({
205+
label: `match_all_ignore_case('${lastElement}')`,
206+
kind: monaco.languages.CompletionItemKind.Text,
207+
insertText: `match_all_ignore_case('${lastElement}')`,
208+
range: range,
209+
});
210+
211+
return {
212+
suggestions: filteredSuggestions,
213+
};
214+
},
215+
});
216+
217+
editor.onDidChangeModelContent((e: any) => {
218+
onChange(editor.getValue());
219+
});
220+
221+
editor.createContextKey('ctrlenter', true);
222+
editor.addCommand(
223+
monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter,
224+
function () {
225+
runQuery();
226+
},
227+
'ctrlenter'
228+
);
229+
230+
window.addEventListener('click', () => {
231+
editor.layout();
232+
});
233+
};
234+
235+
return (
236+
<>
237+
<ReactMonacoEditor
238+
options={options}
239+
onMount={onEditorMount}
240+
value={query}
241+
language="sql"
242+
className={css`
243+
height: 100px;
244+
max-height: 200px;
245+
`}
246+
></ReactMonacoEditor>
247+
</>
248+
);
249+
}

0 commit comments

Comments
 (0)