Add SQL auto complete in druid console (#7244)

* Add SQL auto complete in druid console

* Add comment in sql.md to alert user to change create-sql-function-doc if sql.md format gets changed
This commit is contained in:
Qi Shu 2019-03-16 01:45:53 -07:00 committed by Clint Wylie
parent 892d1d35d6
commit 5406aaa49d
7 changed files with 255 additions and 6 deletions

View File

@ -22,6 +22,13 @@ title: "SQL"
~ under the License. ~ under the License.
--> -->
<!--
The format of the tables that describe the functions and operators
should not be changed without updating the script create-sql-function-doc
in web-console/script/create-sql-function-doc, because the script detects
patterns in this markdown file and parse it to TypeScript file for web console
-->
# SQL # SQL
<div class="note caution"> <div class="note caution">

View File

@ -9,6 +9,8 @@ coordinator-console/
pages/ pages/
index.html index.html
lib/sql-function-doc.ts
.tscache .tscache
tscommand-*.tmp.txt tscommand-*.tmp.txt

View File

@ -27,6 +27,9 @@ echo "Copying blueprint assets in..."
sed 's|url("assets|url("/assets|g' "./node_modules/@blueprintjs/core/dist/blueprint.css" > lib/blueprint.css sed 's|url("assets|url("/assets|g' "./node_modules/@blueprintjs/core/dist/blueprint.css" > lib/blueprint.css
cp -r "./node_modules/@blueprintjs/core/dist/assets" . cp -r "./node_modules/@blueprintjs/core/dist/assets" .
echo "Adding SQL function doc..."
PATH="./target/node:$PATH" ./script/create-sql-function-doc
echo "Transpiling ReactTable CSS..." echo "Transpiling ReactTable CSS..."
PATH="./target/node:$PATH" ./node_modules/.bin/stylus lib/react-table.styl -o lib/react-table.css PATH="./target/node:$PATH" ./node_modules/.bin/stylus lib/react-table.styl -o lib/react-table.css

View File

@ -0,0 +1,50 @@
#!/bin/bash
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
readfile='../docs/content/querying/sql.md'
writefile='lib/sql-function-doc.ts'
> "$writefile"
echo -e "// This file is auto generated and should not be modified\n" > "$writefile"
echo -e 'export const SQLFunctionDoc: any[] = [' >> "$writefile"
isFunction=false
while read -r line; do
if [[ $line =~ ^###.*functions$ ]]; then
isFunction=true
elif [[ $line =~ ^## ]] ; then
isFunction=false
elif [[ $isFunction == true ]]; then
if [[ $line =~ \|\`.*\`\|.*\| ]]; then
syntax=$(echo $line | grep -o '|`.*`|')
syntax=${syntax:2:${#syntax}-4}
syntax=${syntax//\\/}
description=$(echo $line | grep -o '`|.*.|')
description=${description//\"/\'}
description=${description:2:${#description}-4}
echo -e " {" >> "$writefile"
echo -e " syntax: \"$syntax\"," >> "$writefile"
echo -e " description: \"$description\"" >> "$writefile"
echo -e " }," >> "$writefile"
fi
fi
done < "$readfile"
echo -e ']' >> "$writefile"

View File

@ -0,0 +1,61 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
.sql-control {
.ace_scroller {
background-color: #232C35;
}
.ace_gutter-layer {
background-color: #27313c;
}
.buttons {
button{
margin-right: 15px;
}
}
}
.auto-complete-checkbox {
margin:10px;
min-width: 120px;
}
.ace_tooltip {
padding: 10px;
background-color: #333D47;
color: #C1CCD5;
width: 500px;
display: block;
height: auto;
white-space: initial;
hr {
margin: 10px 0;
}
.function-doc-name {
font-size: 18px;
}
}

View File

@ -17,6 +17,8 @@
*/ */
import * as React from 'react'; import * as React from 'react';
import * as ReactDOMServer from 'react-dom/server';
import axios from "axios";
import * as classNames from 'classnames'; import * as classNames from 'classnames';
import * as ace from 'brace' import * as ace from 'brace'
import AceEditor from "react-ace"; import AceEditor from "react-ace";
@ -24,8 +26,13 @@ import 'brace/mode/sql';
import 'brace/mode/hjson'; import 'brace/mode/hjson';
import 'brace/theme/solarized_dark'; import 'brace/theme/solarized_dark';
import 'brace/ext/language_tools'; import 'brace/ext/language_tools';
import { Intent, Button } from "@blueprintjs/core"; import {Intent, Button, Popover, Checkbox, Classes, Position} from "@blueprintjs/core";
import { SQLFunctionDoc } from "../../lib/sql-function-doc";
import { IconNames } from './filler'; import { IconNames } from './filler';
import './sql-control.scss'
import {AppToaster} from "../singletons/toaster";
const langTools = ace.acequire('ace/ext/language_tools');
export interface SqlControlProps extends React.Props<any> { export interface SqlControlProps extends React.Props<any> {
initSql: string | null; initSql: string | null;
@ -35,6 +42,7 @@ export interface SqlControlProps extends React.Props<any> {
export interface SqlControlState { export interface SqlControlState {
query: string; query: string;
autoCompleteOn: boolean; autoCompleteOn: boolean;
autoCompleteLoading: boolean;
} }
export class SqlControl extends React.Component<SqlControlProps, SqlControlState> { export class SqlControl extends React.Component<SqlControlProps, SqlControlState> {
@ -42,10 +50,113 @@ export class SqlControl extends React.Component<SqlControlProps, SqlControlState
super(props, context); super(props, context);
this.state = { this.state = {
query: props.initSql || '', query: props.initSql || '',
autoCompleteOn: true autoCompleteOn: true,
autoCompleteLoading: false
}; };
} }
private addDatasourceAutoCompleter = async (): Promise<any> =>{
const datasourceResp = await axios.post("/druid/v2/sql", { query: `SELECT datasource FROM sys.segments GROUP BY 1`})
const datasourceList: any[] = datasourceResp.data.map((d: any) => {
const datasourceName: string = d.datasource;
return {
value: datasourceName,
score: 50,
meta: "datasource"
};
});
const completer = {
getCompletions: (editor:any , session: any, pos: any, prefix: any, callback: any) => {
callback(null, datasourceList);
}
};
langTools.addCompleter(completer);
}
private addColumnNameAutoCompleter = async (): Promise<any> => {
const columnNameResp = await axios.post("/druid/v2/sql", {query: `SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = 'druid'`})
const columnNameList: any[] = columnNameResp.data.map((d: any) => {
const columnName: string = d.COLUMN_NAME;
return {
value: columnName,
score: 50,
meta: "column"
};
});
const completer = {
getCompletions: (editor:any , session: any, pos: any, prefix: any, callback: any) => {
callback(null, columnNameList);
}
};
langTools.addCompleter(completer);
}
private addFunctionAutoCompleter = (): void => {
const functionList: any[]= SQLFunctionDoc.map((entry: any) => {
let funcName: string = entry.syntax.replace(/\(.*\)/,"()");
if (!funcName.includes("(")) funcName = funcName.substr(0,10);
return {
value: funcName,
score: 80,
meta: "function",
syntax: entry.syntax,
description: entry.description,
completer: {
insertMatch: (editor:any, data:any) => {
editor.completer.insertMatch({value: data.caption});
const pos = editor.getCursorPosition();
editor.gotoLine(pos.row+1, pos.column-1);
}
}
};
});
const completer = {
getCompletions: (editor:any , session: any, pos: any, prefix: any, callback: any) => {
callback(null, functionList);
},
getDocTooltip: (item: any) => {
if (item.meta === "function") {
const functionName = item.caption.slice(0,-2);
item.docHTML = ReactDOMServer.renderToStaticMarkup((
<div className={"function-doc"}>
<div className={"function-doc-name"}><b>{functionName}</b></div>
<hr/>
<div><b>Syntax:</b></div>
<div>{item.syntax}</div>
<br/>
<div><b>Description:</b></div>
<div>{item.description}</div>
</div>
))
}
}
};
langTools.addCompleter(completer);
}
private addCompleters = async () => {
try {
this.addFunctionAutoCompleter();
await this.addDatasourceAutoCompleter();
await this.addColumnNameAutoCompleter();
} catch (e) {
AppToaster.show({
message: "Failed to load SQL auto completer",
intent: Intent.DANGER
});
}
}
componentDidMount(): void {
this.addCompleters();
}
private handleChange = (newValue: string): void => { private handleChange = (newValue: string): void => {
this.setState({ this.setState({
query: newValue query: newValue
@ -58,6 +169,15 @@ export class SqlControl extends React.Component<SqlControlProps, SqlControlState
const isRune = query.trim().startsWith('{'); const isRune = query.trim().startsWith('{');
const autoCompletePopover = <div className={"auto-complete-checkbox"}>
<Checkbox
checked={isRune ? false : autoCompleteOn}
disabled={isRune}
label={"Auto complete"}
onChange={() => this.setState({autoCompleteOn: !autoCompleteOn})}
/>
</div>;
// Set the key in the AceEditor to force a rebind and prevent an error that happens otherwise // Set the key in the AceEditor to force a rebind and prevent an error that happens otherwise
return <div className="sql-control"> return <div className="sql-control">
<AceEditor <AceEditor
@ -83,9 +203,14 @@ export class SqlControl extends React.Component<SqlControlProps, SqlControlState
}} }}
/> />
<div className="buttons"> <div className="buttons">
<Button rightIconName={IconNames.CARET_RIGHT} onClick={() => onRun(query)}>{isRune ? 'Rune' : 'Run'}</Button> <Button rightIconName={IconNames.CARET_RIGHT} onClick={() => onRun(query)}>
</div> {isRune ? 'Rune' : 'Run'}
</Button>
<Popover position={Position.BOTTOM_LEFT} content={autoCompletePopover} inline>
<Button className={`${Classes.MINIMAL} pt-icon-more`}/>
</Popover>
</div> </div>
</div>;
} }
} }

View File

@ -15,14 +15,15 @@
"moduleResolution": "node", "moduleResolution": "node",
"lib": ["dom", "es2016"], "lib": ["dom", "es2016"],
"jsx": "react", "jsx": "react",
"rootDir": "src", "rootDirs": ["lib","src"],
"outDir": "build" "outDir": "build"
}, },
"include": [ "include": [
"src/**/*.ts", "src/**/*.ts",
"src/**/*.tsx" "src/**/*.tsx",
"lib/sql-function-doc.ts"
], ],
"exclude": [ "exclude": [
"**/*.test.ts" "**/*.test.ts"