Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@
"private": true,
"dependencies": {
"bootstrap": "3",
"codemirror": "^5.37.0",
"prop-types": "^15.6.1",
"react": "^16.3.2",
"react-bootstrap": "^0.32.1",
"react-codemirror2": "^5.0.1",
"react-dom": "^16.3.2",
"react-helmet": "^5.2.0",
"react-scripts": "1.1.4",
Expand Down
34 changes: 32 additions & 2 deletions frontend/src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ class App extends Component {
INNER JOIN commits c ON YEAR(committer_when) = 2018 AND history_idx(refs.commit_hash, c.commit_hash) >= 0
) as t
GROUP BY committer_email, month, repo_id`,
results: new Map()
results: new Map(),
schema: undefined
};

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

api
.query(sql)
.then(response => this.setResult(key, { sql, response }))
.then(response => {
this.setResult(key, { sql, response });

if (!this.state.schema) {
// The schema was not loaded for some reason, and we know we just
// did a successful call to the backend. Let's retry.
this.loadSchema();
}
})
.catch(msgArr =>
this.setResult(key, { sql, errorMsg: msgArr.join('; ') })
);
}

loadSchema() {
api
.schema()
.then(schema => {
if (JSON.stringify(schema) !== JSON.stringify(this.state.schema)) {
this.setState({ schema });
}
})
.catch(msgArr => {
// TODO (@carlosms): left as console message for now, we may decide to
// show it in the interface somehow when we have to populate the sidebar
// eslint-disable-next-line no-console
console.error(`Error while loading schema: ${msgArr}`);
});
}

componentDidMount() {
this.loadSchema();
}

handleRemoveResult(key) {
const newResults = new Map(this.state.results);
newResults.delete(key);
Expand Down Expand Up @@ -93,6 +122,7 @@ GROUP BY committer_email, month, repo_id`,
<Col xs={12}>
<QueryBox
sql={this.state.sql}
schema={this.state.schema}
handleTextChange={this.handleTextChange}
handleSubmit={this.handleSubmit}
/>
Expand Down
38 changes: 37 additions & 1 deletion frontend/src/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,42 @@ function query(sql) {
});
}

function tables() {
return apiCall(`/tables`);
}

/* Returns an array in the form:
[
{
"table": "refs",
"columns": [
{
"name": "repository_id",
"type": "TEXT"
},
...
]
},
...
]
*/
function schema() {
return tables()
.then(res =>
Promise.all(
res.data.map(e =>
query(`DESCRIBE TABLE ${e.table}`).then(tableRes => ({
Copy link
Contributor

@bzz bzz May 22, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@carlosms sorry for digging this up, but I have just realized that we need to change this logic eventually.

Instead for

  • API call /tables
  • then, for each table call DESCRIBE TABLE from the frontend

We should have

  • API call to /tables, so that the backend returns all this information in a single call

on top of per improvements (less HTTP calls), one of the purposes of having separate API endpoint on the backend was to abstract the details of how meta-information (schema) is collected.

Current approach, for a case if it is not a DESCRIBE TABLE - would require us to change both, backend and frontend. The one suggested above would requite only the backend change.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with you comment. I actually though of doing it that way in this PR, but didn't want to get too ahead of myself and went for the quicker approach of doing it in the frontend.

I've created a new issue with your request: #40

table: e.table,
columns: tableRes.data
}))
)
)
)
.catch(err => Promise.reject(normalizeErrors(err)));
}

export default {
query
query,
tables,
schema
};
83 changes: 78 additions & 5 deletions frontend/src/components/QueryBox.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,82 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { Row, Col, Button } from 'react-bootstrap';
import { Controlled as CodeMirror } from 'react-codemirror2';

import 'codemirror/lib/codemirror.css';
import 'codemirror/mode/sql/sql';
import 'codemirror/addon/display/placeholder';
import 'codemirror/addon/edit/matchbrackets';
import 'codemirror/addon/hint/show-hint.css';
import 'codemirror/addon/hint/show-hint';
import 'codemirror/addon/hint/sql-hint';

import './QueryBox.less';

class QueryBox extends Component {
constructor(props) {
super(props);

this.state = {
schema: undefined,
codeMirrorTables: {}
};
}

static getDerivedStateFromProps(nextProps, prevState) {
if (nextProps.schema === prevState.schema) {
return null;
}

return {
schema: nextProps.schema,
codeMirrorTables: QueryBox.schemaToCodeMirror(nextProps.schema)
};
}

static schemaToCodeMirror(schema) {
if (!schema) {
return {};
}

return schema.reduce(
(prevVal, currVal) => ({
...prevVal,
[currVal.table]: currVal.columns.map(col => col.name)
}),
{}
);
}

render() {
const { codeMirrorTables } = this.state;

const options = {
mode: 'text/x-mariadb',
smartIndent: true,
lineNumbers: true,
matchBrackets: true,
autofocus: true,
placeholder: 'Enter an SQL query',
extraKeys: {
'Ctrl-Space': 'autocomplete',
'Ctrl-Enter': () => this.props.handleSubmit()
},
hintOptions: {
tables: codeMirrorTables
}
};

return (
<div className="query-box">
<Row>
<Col xs={12}>
<textarea
autoFocus="true"
rows="7"
placeholder="Enter an SQL query"
<CodeMirror
value={this.props.sql}
onChange={e => this.props.handleTextChange(e.target.value)}
options={options}
onBeforeChange={(editor, data, value) => {
this.props.handleTextChange(value);
}}
/>
</Col>
</Row>
Expand All @@ -36,6 +98,17 @@ class QueryBox extends Component {

QueryBox.propTypes = {
sql: PropTypes.string.isRequired,
schema: PropTypes.arrayOf(
PropTypes.shape({
table: PropTypes.string.isRequired,
columns: PropTypes.arrayOf(
PropTypes.shape({
name: PropTypes.string.isRequired,
type: PropTypes.string.isRequired
})
).isRequired
})
),
enabled: PropTypes.bool,
handleTextChange: PropTypes.func.isRequired,
handleSubmit: PropTypes.func.isRequired
Expand Down
11 changes: 7 additions & 4 deletions frontend/src/components/QueryBox.less
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@
width: 100%;
}

textarea {
button {
width: 100%;
font-family: monospace;
}

button {
width: 100%;
.CodeMirror {
height: 150px;
}

.CodeMirror-empty {
color: dimgrey;
}
6 changes: 1 addition & 5 deletions frontend/src/components/TabbedResults.less
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@

.close {
width: auto;
margin-left: 1ex;
Expand All @@ -7,11 +8,6 @@
margin-top: 2em;
}

pre {
float: left;
max-width: 85%;
}

.query-row {
margin-bottom: 2em;

Expand Down
19 changes: 19 additions & 0 deletions frontend/src/setupTests.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,22 @@ global.localStorage = new LocalStorage(

global.window = document.defaultView;
global.window.localStorage = global.localStorage;

// CodeMirror needs all of this in order to work.
// see: https://discuss.codemirror.net/t/working-in-jsdom-or-node-js-natively/138/5
global.document.body.createTextRange = function() {
return {
setEnd() {},
setStart() {},
getBoundingClientRect() {
return { right: 0 };
},
getClientRects() {
return {
length: 0,
left: 0,
right: 0
};
}
};
};
8 changes: 8 additions & 0 deletions frontend/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1524,6 +1524,10 @@ code-point-at@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"

codemirror@^5.37.0:
version "5.37.0"
resolved "https://registry.yarnpkg.com/codemirror/-/codemirror-5.37.0.tgz#c349b584e158f590277f26d37c2469a6bc538036"

collection-visit@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0"
Expand Down Expand Up @@ -5747,6 +5751,10 @@ react-bootstrap@^0.32.1:
uncontrollable "^4.1.0"
warning "^3.0.0"

react-codemirror2@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/react-codemirror2/-/react-codemirror2-5.0.1.tgz#81eb8e17bfe859633a6855a9ce40307914d42891"

react-dev-utils@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/react-dev-utils/-/react-dev-utils-5.0.1.tgz#1f396e161fe44b595db1b186a40067289bf06613"
Expand Down