Skip to content

Commit d450194

Browse files
davemarcohoophalab
andauthored
feat(webui): Integrate Monaco editor for future Presto SQL query input. (#1108)
Co-authored-by: hoophalab <[email protected]>
1 parent c3f4c2b commit d450194

File tree

14 files changed

+333
-6
lines changed

14 files changed

+333
-6
lines changed

components/webui/client/package-lock.json

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

components/webui/client/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"@ant-design/v5-patch-for-react-19": "^1.0.3",
1717
"@emotion/react": "^11.14.0",
1818
"@emotion/styled": "^11.14.0",
19+
"@monaco-editor/react": "^4.7.0",
1920
"@mui/joy": "^5.0.0-beta.51",
2021
"@sinclair/typebox": "^0.34.25",
2122
"@tanstack/react-query": "^5.81.5",
@@ -26,6 +27,7 @@
2627
"chartjs-adapter-dayjs-4": "^1.0.4",
2728
"chartjs-plugin-zoom": "^2.2.0",
2829
"dayjs": "^1.11.13",
30+
"monaco-editor": "^0.52.2",
2931
"react": "^19.0.0",
3032
"react-chartjs-2": "^5.3.0",
3133
"react-dom": "^19.0.0",

components/webui/client/public/settings.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{
22
"ClpStorageEngine": "clp",
3+
"ClpQueryEngine": "native",
34
"MongoDbSearchResultsMetadataCollectionName": "results-metadata",
45
"SqlDbClpArchivesTableName": "clp_archives",
56
"SqlDbClpDatasetsTableName": "clp_datasets",
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import {
2+
useCallback,
3+
useEffect,
4+
useState,
5+
} from "react";
6+
7+
import {
8+
Editor,
9+
EditorProps,
10+
useMonaco,
11+
} from "@monaco-editor/react";
12+
import {language as sqlLanguage} from "monaco-editor/esm/vs/basic-languages/sql/sql.js";
13+
import * as monaco from "monaco-editor/esm/vs/editor/editor.api.js";
14+
15+
import "./monaco-loader";
16+
17+
18+
const MAX_VISIBLE_LINES: number = 5;
19+
20+
type SqlEditorProps = Omit<EditorProps, "language">;
21+
22+
/**
23+
* Monaco editor with highlighting and autocomplete for SQL syntax.
24+
*
25+
* @param props
26+
* @return
27+
*/
28+
const SqlEditor = (props: SqlEditorProps) => {
29+
const monacoEditor = useMonaco();
30+
31+
useEffect(() => {
32+
if (null === monacoEditor) {
33+
return () => {
34+
};
35+
}
36+
37+
// Adds autocomplete suggestions for SQL keywords on editor load
38+
const provider = monacoEditor.languages.registerCompletionItemProvider("sql", {
39+
provideCompletionItems: (model, position) => {
40+
const word = model.getWordUntilPosition(position);
41+
const range = {
42+
startLineNumber: position.lineNumber,
43+
endLineNumber: position.lineNumber,
44+
startColumn: word.startColumn,
45+
endColumn: word.endColumn,
46+
};
47+
const suggestions = sqlLanguage.keywords.map((keyword: string) => ({
48+
detail: "Presto SQL (CLP)",
49+
insertText: `${keyword} `,
50+
kind: monacoEditor.languages.CompletionItemKind.Keyword,
51+
label: keyword,
52+
range: range,
53+
}));
54+
55+
// When SQL keyword suggestions appear (e.g., after "SELECT a"), hitting Enter
56+
// accepts the first suggestion. To prevent accidental auto-completion
57+
// in multi-line queries and to allow users to dismiss suggestions more easily,
58+
// we make the current input the first suggestion.
59+
// Users can then use arrow keys to select a keyword if needed.
60+
const typedWord = model.getValueInRange(range);
61+
if (0 < typedWord.length) {
62+
suggestions.push({
63+
detail: "Current",
64+
insertText: `${typedWord}\n`,
65+
kind: monaco.languages.CompletionItemKind.Text,
66+
label: typedWord,
67+
range: range,
68+
});
69+
}
70+
71+
return {
72+
suggestions: suggestions,
73+
incomplete: true,
74+
};
75+
},
76+
triggerCharacters: [
77+
" ",
78+
"\n",
79+
],
80+
});
81+
82+
return () => {
83+
provider.dispose();
84+
};
85+
}, [monacoEditor]);
86+
87+
const [isContentMultiline, setIsContentMultiline] = useState<boolean>(false);
88+
89+
const handleMonacoMount = useCallback((editor: monaco.editor.IStandaloneCodeEditor) => {
90+
editor.onDidContentSizeChange((ev) => {
91+
if (false === ev.contentHeightChanged) {
92+
return;
93+
}
94+
if (null === monacoEditor) {
95+
throw new Error("Unexpected null Monaco instance");
96+
}
97+
const domNode = editor.getDomNode();
98+
if (null === domNode) {
99+
throw new Error("Unexpected null editor DOM node");
100+
}
101+
const model = editor.getModel();
102+
if (null === model) {
103+
throw new Error("Unexpected null editor model");
104+
}
105+
const lineHeight = editor.getOption(monacoEditor.editor.EditorOption.lineHeight);
106+
const contentHeight = editor.getContentHeight();
107+
const approxWrappedLines = Math.round(contentHeight / lineHeight);
108+
setIsContentMultiline(1 < approxWrappedLines);
109+
if (MAX_VISIBLE_LINES >= approxWrappedLines) {
110+
domNode.style.height = `${contentHeight}px`;
111+
} else {
112+
domNode.style.height = `${lineHeight * MAX_VISIBLE_LINES}px`;
113+
}
114+
});
115+
}, [monacoEditor]);
116+
117+
return (
118+
<Editor
119+
language={"sql"}
120+
121+
// Use white background while loading (default is grey) so transition to editor with
122+
// white background is less jarring.
123+
loading={<div style={{backgroundColor: "white", height: "100%", width: "100%"}}/>}
124+
options={{
125+
automaticLayout: true,
126+
folding: isContentMultiline,
127+
fontSize: 20,
128+
lineHeight: 30,
129+
lineNumbers: isContentMultiline ?
130+
"on" :
131+
"off",
132+
lineNumbersMinChars: 2,
133+
minimap: {enabled: false},
134+
overviewRulerBorder: false,
135+
placeholder: "Enter your SQL query",
136+
renderLineHighlightOnlyWhenFocus: true,
137+
scrollBeyondLastLine: false,
138+
wordWrap: "on",
139+
}}
140+
onMount={handleMonacoMount}
141+
{...props}/>
142+
);
143+
};
144+
145+
export default SqlEditor;
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/* eslint-disable import/default, @stylistic/max-len */
2+
import {loader} from "@monaco-editor/react";
3+
import * as monaco from "monaco-editor/esm/vs/editor/editor.api";
4+
import EditorWorker from "monaco-editor/esm/vs/editor/editor.worker?worker";
5+
6+
import "monaco-editor/esm/vs/basic-languages/sql/sql.contribution.js";
7+
import "monaco-editor/esm/vs/editor/contrib/clipboard/browser/clipboard.js";
8+
import "monaco-editor/esm/vs/editor/contrib/contextmenu/browser/contextmenu.js";
9+
import "monaco-editor/esm/vs/editor/contrib/find/browser/findController.js";
10+
import "monaco-editor/esm/vs/editor/contrib/wordHighlighter/browser/wordHighlighter.js";
11+
import "monaco-editor/esm/vs/editor/contrib/suggest/browser/suggestController.js";
12+
import "monaco-editor/esm/vs/editor/contrib/placeholderText/browser/placeholderText.contribution.js";
13+
14+
15+
/* eslint-enable import/default, @stylistic/max-len */
16+
17+
18+
self.MonacoEnvironment = {
19+
/**
20+
* Creates a web worker for Monaco Editor.
21+
*
22+
* @return
23+
*/
24+
getWorker () {
25+
return new EditorWorker();
26+
},
27+
};
28+
29+
loader.config({monaco});
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
declare module "monaco-editor/esm/vs/basic-languages/sql/sql.js" {
2+
import {languages} from "monaco-editor/esm/vs/editor/editor.api";
3+
4+
5+
interface SqlLanguageDefinition extends languages.IMonarchLanguage {
6+
keywords: string[];
7+
}
8+
9+
export const language: SqlLanguageDefinition;
10+
}

components/webui/client/src/config/index.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,16 @@ enum CLP_STORAGE_ENGINES {
99
CLP_S = "clp-s",
1010
}
1111

12+
/**
13+
* Query engine options.
14+
*/
15+
enum CLP_QUERY_ENGINES {
16+
NATIVE = "native",
17+
PRESTO = "presto",
18+
}
19+
1220
const SETTINGS_STORAGE_ENGINE = settings.ClpStorageEngine as CLP_STORAGE_ENGINES;
21+
const SETTINGS_QUERY_ENGINE = settings.ClpQueryEngine as CLP_QUERY_ENGINES;
1322

1423
/**
1524
* Stream type based on the storage engine.
@@ -19,7 +28,9 @@ const STREAM_TYPE = CLP_STORAGE_ENGINES.CLP === SETTINGS_STORAGE_ENGINE ?
1928
"json";
2029

2130
export {
31+
CLP_QUERY_ENGINES,
2232
CLP_STORAGE_ENGINES,
33+
SETTINGS_QUERY_ENGINE,
2334
SETTINGS_STORAGE_ENGINE,
2435
STREAM_TYPE,
2536
};
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import {CaretRightOutlined} from "@ant-design/icons";
2+
import {
3+
Button,
4+
Tooltip,
5+
} from "antd";
6+
7+
import useSearchStore from "../../../SearchState/index";
8+
9+
10+
/**
11+
* Renders a button to run the SQL query.
12+
*
13+
* @return
14+
*/
15+
const RunButton = () => {
16+
const queryString = useSearchStore((state) => state.queryString);
17+
18+
const isQueryStringEmpty = "" === queryString;
19+
const tooltipTitle = isQueryStringEmpty ?
20+
"Enter SQL query to run" :
21+
"";
22+
23+
return (
24+
<Tooltip title={tooltipTitle}>
25+
<Button
26+
color={"green"}
27+
disabled={isQueryStringEmpty}
28+
icon={<CaretRightOutlined/>}
29+
size={"large"}
30+
variant={"solid"}
31+
>
32+
Run
33+
</Button>
34+
</Tooltip>
35+
);
36+
};
37+
38+
export default RunButton;
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
/* Allows the editor to shrink when page width decreases */
2+
.input {
3+
width: 100%;
4+
min-width: 0;
5+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import {useCallback} from "react";
2+
3+
import SqlEditor from "../../../../../components/SqlEditor";
4+
import useSearchStore from "../../../SearchState/index";
5+
import styles from "./index.module.css";
6+
7+
8+
/**
9+
* Renders SQL query input.
10+
*
11+
* @return
12+
*/
13+
const SqlQueryInput = () => {
14+
const handleChange = useCallback((value: string | undefined) => {
15+
const {updateQueryString} = useSearchStore.getState();
16+
updateQueryString(value || "");
17+
}, []);
18+
19+
return (
20+
<div className={styles["input"] || ""}>
21+
<SqlEditor onChange={handleChange}/>
22+
</div>
23+
);
24+
};
25+
26+
export default SqlQueryInput;

0 commit comments

Comments
 (0)