Skip to content

Commit 0cbce7d

Browse files
authored
Merge pull request #35 from carlosms/issue-20
Add CodeMirror editor with sql syntax and autocomplete
2 parents 2bb41f9 + 1fc27ce commit 0cbce7d

File tree

8 files changed

+184
-17
lines changed

8 files changed

+184
-17
lines changed

frontend/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@
44
"private": true,
55
"dependencies": {
66
"bootstrap": "3",
7+
"codemirror": "^5.37.0",
78
"prop-types": "^15.6.1",
89
"react": "^16.3.2",
910
"react-bootstrap": "^0.32.1",
11+
"react-codemirror2": "^5.0.1",
1012
"react-dom": "^16.3.2",
1113
"react-helmet": "^5.2.0",
1214
"react-scripts": "1.1.4",

frontend/src/App.js

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ class App extends Component {
1717
INNER JOIN commits c ON YEAR(committer_when) = 2018 AND history_idx(refs.commit_hash, c.commit_hash) >= 0
1818
) as t
1919
GROUP BY committer_email, month, repo_id`,
20-
results: new Map()
20+
results: new Map(),
21+
schema: undefined
2122
};
2223

2324
this.handleTextChange = this.handleTextChange.bind(this);
@@ -54,12 +55,40 @@ GROUP BY committer_email, month, repo_id`,
5455

5556
api
5657
.query(sql)
57-
.then(response => this.setResult(key, { sql, response }))
58+
.then(response => {
59+
this.setResult(key, { sql, response });
60+
61+
if (!this.state.schema) {
62+
// The schema was not loaded for some reason, and we know we just
63+
// did a successful call to the backend. Let's retry.
64+
this.loadSchema();
65+
}
66+
})
5867
.catch(msgArr =>
5968
this.setResult(key, { sql, errorMsg: msgArr.join('; ') })
6069
);
6170
}
6271

72+
loadSchema() {
73+
api
74+
.schema()
75+
.then(schema => {
76+
if (JSON.stringify(schema) !== JSON.stringify(this.state.schema)) {
77+
this.setState({ schema });
78+
}
79+
})
80+
.catch(msgArr => {
81+
// TODO (@carlosms): left as console message for now, we may decide to
82+
// show it in the interface somehow when we have to populate the sidebar
83+
// eslint-disable-next-line no-console
84+
console.error(`Error while loading schema: ${msgArr}`);
85+
});
86+
}
87+
88+
componentDidMount() {
89+
this.loadSchema();
90+
}
91+
6392
handleRemoveResult(key) {
6493
const newResults = new Map(this.state.results);
6594
newResults.delete(key);
@@ -93,6 +122,7 @@ GROUP BY committer_email, month, repo_id`,
93122
<Col xs={12}>
94123
<QueryBox
95124
sql={this.state.sql}
125+
schema={this.state.schema}
96126
handleTextChange={this.handleTextChange}
97127
handleSubmit={this.handleSubmit}
98128
/>

frontend/src/api.js

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,42 @@ function query(sql) {
8787
});
8888
}
8989

90+
function tables() {
91+
return apiCall(`/tables`);
92+
}
93+
94+
/* Returns an array in the form:
95+
[
96+
{
97+
"table": "refs",
98+
"columns": [
99+
{
100+
"name": "repository_id",
101+
"type": "TEXT"
102+
},
103+
...
104+
]
105+
},
106+
...
107+
]
108+
*/
109+
function schema() {
110+
return tables()
111+
.then(res =>
112+
Promise.all(
113+
res.data.map(e =>
114+
query(`DESCRIBE TABLE ${e.table}`).then(tableRes => ({
115+
table: e.table,
116+
columns: tableRes.data
117+
}))
118+
)
119+
)
120+
)
121+
.catch(err => Promise.reject(normalizeErrors(err)));
122+
}
123+
90124
export default {
91-
query
125+
query,
126+
tables,
127+
schema
92128
};

frontend/src/components/QueryBox.js

Lines changed: 78 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,82 @@
11
import React, { Component } from 'react';
22
import PropTypes from 'prop-types';
33
import { Row, Col, Button } from 'react-bootstrap';
4+
import { Controlled as CodeMirror } from 'react-codemirror2';
5+
6+
import 'codemirror/lib/codemirror.css';
7+
import 'codemirror/mode/sql/sql';
8+
import 'codemirror/addon/display/placeholder';
9+
import 'codemirror/addon/edit/matchbrackets';
10+
import 'codemirror/addon/hint/show-hint.css';
11+
import 'codemirror/addon/hint/show-hint';
12+
import 'codemirror/addon/hint/sql-hint';
13+
414
import './QueryBox.less';
515

616
class QueryBox extends Component {
17+
constructor(props) {
18+
super(props);
19+
20+
this.state = {
21+
schema: undefined,
22+
codeMirrorTables: {}
23+
};
24+
}
25+
26+
static getDerivedStateFromProps(nextProps, prevState) {
27+
if (nextProps.schema === prevState.schema) {
28+
return null;
29+
}
30+
31+
return {
32+
schema: nextProps.schema,
33+
codeMirrorTables: QueryBox.schemaToCodeMirror(nextProps.schema)
34+
};
35+
}
36+
37+
static schemaToCodeMirror(schema) {
38+
if (!schema) {
39+
return {};
40+
}
41+
42+
return schema.reduce(
43+
(prevVal, currVal) => ({
44+
...prevVal,
45+
[currVal.table]: currVal.columns.map(col => col.name)
46+
}),
47+
{}
48+
);
49+
}
50+
751
render() {
52+
const { codeMirrorTables } = this.state;
53+
54+
const options = {
55+
mode: 'text/x-mariadb',
56+
smartIndent: true,
57+
lineNumbers: true,
58+
matchBrackets: true,
59+
autofocus: true,
60+
placeholder: 'Enter an SQL query',
61+
extraKeys: {
62+
'Ctrl-Space': 'autocomplete',
63+
'Ctrl-Enter': () => this.props.handleSubmit()
64+
},
65+
hintOptions: {
66+
tables: codeMirrorTables
67+
}
68+
};
69+
870
return (
971
<div className="query-box">
1072
<Row>
1173
<Col xs={12}>
12-
<textarea
13-
autoFocus="true"
14-
rows="7"
15-
placeholder="Enter an SQL query"
74+
<CodeMirror
1675
value={this.props.sql}
17-
onChange={e => this.props.handleTextChange(e.target.value)}
76+
options={options}
77+
onBeforeChange={(editor, data, value) => {
78+
this.props.handleTextChange(value);
79+
}}
1880
/>
1981
</Col>
2082
</Row>
@@ -36,6 +98,17 @@ class QueryBox extends Component {
3698

3799
QueryBox.propTypes = {
38100
sql: PropTypes.string.isRequired,
101+
schema: PropTypes.arrayOf(
102+
PropTypes.shape({
103+
table: PropTypes.string.isRequired,
104+
columns: PropTypes.arrayOf(
105+
PropTypes.shape({
106+
name: PropTypes.string.isRequired,
107+
type: PropTypes.string.isRequired
108+
})
109+
).isRequired
110+
})
111+
),
39112
enabled: PropTypes.bool,
40113
handleTextChange: PropTypes.func.isRequired,
41114
handleSubmit: PropTypes.func.isRequired

frontend/src/components/QueryBox.less

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,14 @@
22
width: 100%;
33
}
44

5-
textarea {
5+
button {
66
width: 100%;
7-
font-family: monospace;
87
}
98

10-
button {
11-
width: 100%;
9+
.CodeMirror {
10+
height: 150px;
11+
}
12+
13+
.CodeMirror-empty {
14+
color: dimgrey;
1215
}

frontend/src/components/TabbedResults.less

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
12
.close {
23
width: auto;
34
margin-left: 1ex;
@@ -7,11 +8,6 @@
78
margin-top: 2em;
89
}
910

10-
pre {
11-
float: left;
12-
max-width: 85%;
13-
}
14-
1511
.query-row {
1612
margin-bottom: 2em;
1713

frontend/src/setupTests.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,22 @@ global.localStorage = new LocalStorage(
1111

1212
global.window = document.defaultView;
1313
global.window.localStorage = global.localStorage;
14+
15+
// CodeMirror needs all of this in order to work.
16+
// see: https://discuss.codemirror.net/t/working-in-jsdom-or-node-js-natively/138/5
17+
global.document.body.createTextRange = function() {
18+
return {
19+
setEnd() {},
20+
setStart() {},
21+
getBoundingClientRect() {
22+
return { right: 0 };
23+
},
24+
getClientRects() {
25+
return {
26+
length: 0,
27+
left: 0,
28+
right: 0
29+
};
30+
}
31+
};
32+
};

frontend/yarn.lock

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1524,6 +1524,10 @@ code-point-at@^1.0.0:
15241524
version "1.1.0"
15251525
resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"
15261526

1527+
codemirror@^5.37.0:
1528+
version "5.37.0"
1529+
resolved "https://registry.yarnpkg.com/codemirror/-/codemirror-5.37.0.tgz#c349b584e158f590277f26d37c2469a6bc538036"
1530+
15271531
collection-visit@^1.0.0:
15281532
version "1.0.0"
15291533
resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0"
@@ -5747,6 +5751,10 @@ react-bootstrap@^0.32.1:
57475751
uncontrollable "^4.1.0"
57485752
warning "^3.0.0"
57495753

5754+
react-codemirror2@^5.0.1:
5755+
version "5.0.1"
5756+
resolved "https://registry.yarnpkg.com/react-codemirror2/-/react-codemirror2-5.0.1.tgz#81eb8e17bfe859633a6855a9ce40307914d42891"
5757+
57505758
react-dev-utils@^5.0.1:
57515759
version "5.0.1"
57525760
resolved "https://registry.yarnpkg.com/react-dev-utils/-/react-dev-utils-5.0.1.tgz#1f396e161fe44b595db1b186a40067289bf06613"

0 commit comments

Comments
 (0)