From cd9ed63ebc0c7174cc29ba887afd7a621f29dd87 Mon Sep 17 00:00:00 2001 From: Jesse Swidler Date: Fri, 16 Jun 2017 13:06:07 -0700 Subject: [PATCH] Add persistence to each view The window will remember the last query entered. The backend is keeping track of up to 20 queries per view, but only the most recent is expsed in this commit. The configuration is saved in a simple JSON file. --- .gitignore | 2 +- Dockerfile | 4 +-- api/app.js | 2 +- api/routes/api.js | 84 +++++++++++++++++++++++++++++++++++++++------- docker-compose.yml | 2 ++ nginx/nginx.conf | 2 +- ui/package.json | 1 + ui/src/App.js | 36 ++++++++++++++++++-- ui/src/index.js | 2 +- 9 files changed, 114 insertions(+), 21 deletions(-) diff --git a/.gitignore b/.gitignore index 2a26e2e..70d9d90 100644 --- a/.gitignore +++ b/.gitignore @@ -40,7 +40,7 @@ jspm_packages /coverage # production -/build +ui/build # misc .DS_Store diff --git a/Dockerfile b/Dockerfile index fe9b3f6..2a42469 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,10 +5,10 @@ RUN apt-get install nginx -y RUN npm install -g serve -# Create api and ui directory +# Create directories RUN mkdir -p /root/sqlwidget/api RUN mkdir -p /root/sqlwidget/ui - +RUN mkdir -p /opt/sqlwidget/ # Install app dependencies COPY api/package.json /root/sqlwidget/api diff --git a/api/app.js b/api/app.js index 832de12..8e68008 100644 --- a/api/app.js +++ b/api/app.js @@ -32,7 +32,7 @@ app.use(bodyParser.urlencoded({ extended: false })); app.use(function(req,res,next) { req.db = db; - req.query = Promise.promisify(db.query, {context: db}); + req.querydb = Promise.promisify(db.query, {context: db}); next(); }); diff --git a/api/routes/api.js b/api/routes/api.js index 0a13799..4d30be8 100644 --- a/api/routes/api.js +++ b/api/routes/api.js @@ -1,11 +1,18 @@ -var express = require('express'); -var fs = require('fs'); -var router = express.Router(); +const express = require('express'); +const router = express.Router(); +const Promise = require('bluebird'); +const fs = require('fs'); -var parch = fs.readFileSync('dbs/parch.sql').toString(); +const readFile = Promise.promisify(fs.readFile); +const writeFile = Promise.promisify(fs.writeFile); + +const parch = fs.readFileSync('dbs/parch.sql').toString(); + +const configFile = '/opt/sqlwidget/config'; +let config = {}; router.all('/tables', function(req, res, next) { - req.query("SELECT table_name FROM information_schema.tables WHERE table_schema='public' AND table_type='BASE TABLE';") + req.querydb("SELECT table_name FROM information_schema.tables WHERE table_schema='public' AND table_type='BASE TABLE';") .then(result => { res.json({tables: result.rows.map(row => row.table_name)}); }).catch(err => { @@ -16,9 +23,9 @@ router.all('/tables', function(req, res, next) { router.all('/initdb', function(req, res, next) { // In the future, posting the desired db might be required somehow; ie {"db":"parch"} - req.query("drop owned by temp cascade;") + req.querydb("drop owned by temp cascade;") .then(function(result) { - return req.query(parch); + return req.querydb(parch); }).then(function(result) { res.json({ok:1}); }).catch(err => { @@ -27,14 +34,67 @@ router.all('/initdb', function(req, res, next) { }); }); +router.get('/history', function(req, res, next) { + let viewId = req.query.viewId; + getConfig() + .then(config => { + if (config[viewId] && config[viewId].history) { + res.json({query:config[viewId].history[0].query}); + } else { + res.json({query: ""}); + } + }) + .catch(err => { + res.json({query: ""}); + }) +}); + +router.post('/history', function(req, res, next) { + saveHistory(req.body.viewId, req.body.query, false) + .then(() => res.json({ok:1})) + .catch(err => { + err.status = 500; + next(err); + }); +}); + router.post('/query', function(req, res, next) { - req.query(req.body.query) - .then(result => { - res.json(result); - }).catch(err => { - err.status = 400; + req.querydb(req.body.query) + .tap(() => saveHistory(req.body.viewId, req.body.query, true) + .catch(e=>console.log("Failed to save config:", e))) + .then(result => res.json(result)) + .catch(err => { + if (!err.status) { + err.status = 400; + } next(err); }); }); +function getConfig() { + return new Promise(resolve => { + readFile(configFile) + .then(data => resolve(JSON.parse(data))) + .catch(err => resolve({})); + }); +} + +function saveHistory(viewId, query, executed) { + getConfig() + .then(config => { + config[viewId] = config[viewId] || { }; + config[viewId].history = config[viewId].history || [ ]; + let history = config[viewId].history; + let newRecord = {query, executed}; + if (history[0] && !history[0].executed) { + history[0] = newRecord; + } else { + history.unshift(newRecord); + history.length > 20 && history.pop(); + } + return Promise.resolve(config); + }) + .then(config => writeFile(configFile, JSON.stringify(config))); +} + module.exports = router; diff --git a/docker-compose.yml b/docker-compose.yml index 7cc9fe9..79f7b61 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -14,3 +14,5 @@ services: - db links: - db + volumes: + - /workspace/history:/opt/sqlwidget diff --git a/nginx/nginx.conf b/nginx/nginx.conf index d111d69..586c9b4 100644 --- a/nginx/nginx.conf +++ b/nginx/nginx.conf @@ -15,7 +15,7 @@ http { } location / { - return 301 /sql/ui/; + return 302 " /sql/ui/"; } } } diff --git a/ui/package.json b/ui/package.json index aa222e4..5da8a10 100644 --- a/ui/package.json +++ b/ui/package.json @@ -5,6 +5,7 @@ "homepage": ".", "dependencies": { "axios": "^0.16.1", + "lodash": "^4.17.4", "react": "^15.5.4", "react-codemirror": "^0.3.0", "react-dom": "^15.5.4" diff --git a/ui/src/App.js b/ui/src/App.js index 474a797..7f0bd5c 100644 --- a/ui/src/App.js +++ b/ui/src/App.js @@ -4,25 +4,54 @@ import SQLOutput from './SQLOutput'; import SQLText from './SqlText'; import logo from './udacity_logo.png'; import axios from 'axios'; +const _ = require('lodash'); import './App.css'; import '../node_modules/codemirror/lib/codemirror.css'; class App extends Component { + /* Props: query, viewId, useHeader + */ constructor(props) { super(props); - this.state = { - query: props.query || "-- Enter your SQL below, for instance:\nSELECT id, name,website from accounts ORDER BY name ASC;" + query: props.query } this.execQuery = this.execQuery.bind(this); this.setQuery = this.setQuery.bind(this); this.getQuery = this.getQuery.bind(this); + this.setHistory = _.debounce(this.setHistory.bind(this), 3000, {maxWait:30000}); + } + + componentDidMount() { + if (this.props.viewId) { + this.getHistoricQuery(this.props.viewId); + } + } + + getHistoricQuery(viewId) { + axios.get("/sql/api/history?viewId="+viewId) + .then(response => { + if (response && response.data && response.data.query !== undefined) { // allow empty strings which are falsey + this.setState({query: response.data.query}); + return Promise.resolve(); + } else { + return Promise.reject({message:'failed to get historic query'}); + } + }); } setQuery(query) { this.setState({query: query}); + this.setHistory(); + } + + setHistory() { + axios.post("/sql/api/history", { + query: this.state.query, + viewId: this.props.viewId + }) } getQuery() { @@ -31,7 +60,8 @@ class App extends Component { execQuery() { axios.post("/sql/api/query", { - query: this.state.query + query: this.state.query, + viewId: this.props.viewId }).then(response => { if (response.data) { this.setState({queryResult: response.data, queryError: undefined}); diff --git a/ui/src/index.js b/ui/src/index.js index ece9815..aea4128 100644 --- a/ui/src/index.js +++ b/ui/src/index.js @@ -13,6 +13,6 @@ function getParameterByName(name, url) { } ReactDOM.render( - , + , document.getElementById('root') );