diff options
| author | James Barnett <james.barnett@fivium.co.uk> | 2018-02-21 21:42:55 +0000 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2018-02-21 21:42:55 +0000 |
| commit | 11e98009906651acb110cd3b1625a771b1e2f472 (patch) | |
| tree | de0a90c201f1949e701c969b2a3a94d7015f4006 | |
| parent | 6e21916395fac6861783c2b930b47242ac0f4c09 (diff) | |
| parent | 703f78867653ed9c53a745f9808eb96ae8a89dc7 (diff) | |
| download | sql-plus-plus-11e98009906651acb110cd3b1625a771b1e2f472.tar.xz sql-plus-plus-11e98009906651acb110cd3b1625a771b1e2f472.zip | |
Merge branch tabbed-interface into master
Support multiple, configurable connections
Each connection is displayed in its own tab in the main UI, and gets its own query executor.
New connections can be added dynamically through the new connection dialog.
| -rw-r--r-- | editor-instance.html | 45 | ||||
| -rw-r--r-- | editor-instance.js (renamed from renderer.js) | 65 | ||||
| -rw-r--r-- | index.html | 36 | ||||
| -rw-r--r-- | instance-manager.js | 40 | ||||
| -rw-r--r-- | main.js | 106 | ||||
| -rw-r--r-- | new-connection.html | 72 | ||||
| -rw-r--r-- | new-connection.js | 46 | ||||
| -rw-r--r-- | package-lock.json | 12 | ||||
| -rw-r--r-- | package.json | 5 | ||||
| -rw-r--r-- | query-executor.js | 45 | ||||
| -rw-r--r-- | styles/editor-instance.css (renamed from styles/style.css) | 0 | ||||
| -rw-r--r-- | styles/instance-manager.css | 35 | ||||
| -rw-r--r-- | styles/new-connection.css | 3 |
13 files changed, 425 insertions, 85 deletions
diff --git a/editor-instance.html b/editor-instance.html new file mode 100644 index 0000000..454c6ce --- /dev/null +++ b/editor-instance.html @@ -0,0 +1,45 @@ +<!DOCTYPE html> +<html> + +<head> + <meta charset="UTF-8"> + <title>SQL++</title> + + <link rel="stylesheet" href="node_modules/codemirror/lib/codemirror.css"></link> + <link rel="stylesheet" href="node_modules/codemirror/theme/dracula.css"></link> + <link rel="stylesheet" href="node_modules/codemirror/addon/hint/show-hint.css"></link> + <link rel="stylesheet" href="node_modules/jquery.tabulator/dist/css/tabulator.css"></link> + <link rel="stylesheet" type="text/css" href="./styles/editor-instance.css"></link> + <link rel="stylesheet" type="text/css" href="./styles/table-style.css"></link> + +</head> + +<body> + + <div class="flex-wrapper"> + <div class="row header"> + <input id="run-query" type="button" value="Run"></input> + </div> + + + <div class="row content"> + <div id="editor" class="editor-row split-row"></div> + + <div class="results-row split-row"> + <div id="result-table"></div> + <div id="result-error"></div> + </div> + </div> + + <div class="row footer"> + <div id="execution-status" class="exec-idle">Idle</div> + <div id="execution-time"></div> + <div id="cursor-coords"></div> + </div> + </div> + <script> + require("./editor-instance.js"); + </script> +</body> + +</html>
\ No newline at end of file diff --git a/renderer.js b/editor-instance.js index 9fe1be7..8ed6674 100644 --- a/renderer.js +++ b/editor-instance.js @@ -6,10 +6,14 @@ require("jquery-ui"); require("jquery.tabulator"); const cm = require("codemirror"); require("codemirror/mode/sql/sql"); -require("codemirror/addon/hint/show-hint.js") -require("codemirror/addon/hint/sql-hint.js") +require("codemirror/addon/hint/show-hint.js"); +require("codemirror/addon/hint/sql-hint.js"); const Split = require("split.js"); +const editorInstanceId = require('uuid/v1')(); + +let queryExecutorId; + const editorContext = cm(document.getElementById("editor"), { value: "select *\nfrom information_schema.tables\n/\nselect now()\n/\nselect *\nfrom foo", mode: "text/x-sql", @@ -24,7 +28,20 @@ editorContext.on("cursorActivity", (instance) => { $("#cursor-coords").text("Ln " + (parseInt(coords.line) + 1) + ", Col " + (parseInt(coords.ch) + 1)); }); -ipcRenderer.send("queryExecutor.queryTableMetadata"); +const statementDelimiter = "/"; + +let dataTable; +let execStartTime; +let execTimerInterval; +let execElapsedTime; +let queryMark; + +ipcRenderer.on("editorInstance.registerQueryExecutor", (event, payload) => { + queryExecutorId = payload; + console.log(queryExecutorId); + ipcRenderer.send("queryExecutor.queryTableMetadata", _generateIpcPayload()); +}) + ipcRenderer.on("queryExecutor.queryTableMetadataComplete", (event, response) => { console.log(response); cm.commands.autocomplete = function (cmInstance) { @@ -34,22 +51,15 @@ ipcRenderer.on("queryExecutor.queryTableMetadataComplete", (event, response) => } }); -const statementDelimiter = "/"; - -let dataTable; -let execStartTime; -let execTimerInterval; -let execElapsedTime; -let queryMark; - function runQuery() { _setExecutionStatusIndicator("RUNNING"); _startExecTimer(); - let query = findQuery(); + let payload = _generateIpcPayload(); + payload.query = findQuery(); - ipcRenderer.send("queryExecutor.runQuery", query); + ipcRenderer.send("queryExecutor.runQuery", payload); } /** @@ -126,15 +136,26 @@ function _clearQueryMarks() { editorContext.clearGutter("statement-pointer"); } -ipcRenderer.on("queryExecutor.runQueryComplete", (event, response) => { - _stopExecTimer(); - if (response.error === undefined) { - handleResult(response.result); - } - else { - handleError(response.error); +function _generateIpcPayload() { + return { + editorInstanceId: editorInstanceId, + queryExecutorId: queryExecutorId } +} +ipcRenderer.on("queryExecutor.runQueryComplete", (event, response) => { + // TODO - new instances should register with the instance manager, and the manager should proxy IPC messages only to + // the webview which sent the message. Rather than sending to all instances and the instance having to check + // if they were the intended recipient + if (response.editorInstanceId === editorInstanceId) { + _stopExecTimer(); + if (response.error === undefined) { + handleResult(response.result); + } + else { + handleError(response.error); + } + } }); function _startExecTimer() { @@ -154,7 +175,7 @@ function _stopExecTimer() { function handleError(err) { _stopExecTimer(); _destroyDataTable(); - $("#result-error").removeAttr("style").text("Error (" + err.code + ") - " + err.message); + $("#result-error").removeAttr("style").text("Error (" + err.code + ") - " + err.cause); _setExecutionStatusIndicator("ERROR"); $("#execution-time").text("failed after " + execElapsedTime + " ms"); } @@ -202,7 +223,7 @@ function _setExecutionStatusIndicator(status) { } function _destroyDataTable() { - if(dataTable) { + if (dataTable) { _resultTable().tabulator("destroy"); _resultTable().removeAttr("style").empty(); dataTable = undefined; @@ -4,37 +4,23 @@ <meta charset="UTF-8"> <title>SQL++</title> - <link rel="stylesheet" href="node_modules/codemirror/lib/codemirror.css"></link> - <link rel="stylesheet" href="node_modules/codemirror/theme/dracula.css"></link> - <link rel="stylesheet" href="node_modules/codemirror/addon/hint/show-hint.css"></link> - <link rel="stylesheet" href="node_modules/jquery.tabulator/dist/css/tabulator.css"></link> - <link rel="stylesheet" type="text/css" href="./styles/style.css"></link> - <link rel="stylesheet" type="text/css" href="./styles/table-style.css"></link> + <link rel="stylesheet" href="node_modules/electron-tabs/electron-tabs.css"></link> + <link rel="stylesheet" href="./styles/instance-manager.css"></link> + </head> <body> - <div class="flex-wrapper"> - <div class="row header"> - <input id="run-query" type="button" value="Run"></input> - </div> - - <div class="row content"> - <div id="editor" class="editor-row split-row"></div> - - <div class="results-row split-row"> - <div id="result-table"></div> - <div id="result-error"></div> - </div> - </div> - - <div class="row footer"> - <div id="execution-status" class="exec-idle">Idle</div> - <div id="execution-time"></div> - <div id="cursor-coords"></div> + <div class="etabs-tabgroup"> + <div class="etabs-tabs"></div> + <div class="etabs-buttons"> + <button id="new-connection">New connection</button> </div> </div> + <div class="etabs-tabgroup-footer"></div> + <div class="etabs-views"></div> + <script> - require("./renderer.js"); + require("./instance-manager.js"); </script> </body> </html>
\ No newline at end of file diff --git a/instance-manager.js b/instance-manager.js new file mode 100644 index 0000000..7d27a7e --- /dev/null +++ b/instance-manager.js @@ -0,0 +1,40 @@ +"use strict"; + +const { ipcRenderer } = require('electron'); +const TabGroup = require("electron-tabs"); +const $ = window.jQuery = require("jquery"); + +const tabGroup = new TabGroup(); + +function createNewConnection() { + ipcRenderer.send("instanceManager.openNewConnectionDialog"); +} + +function registerNewInstance(payload) { + tabGroup.addTab({ + title: payload.connectionName, + src: "file://" + __dirname + "/editor-instance.html", + visible: true, + active: true, + webviewAttributes: {"nodeintegration":true}, + ready: tab => { + let webview = tab.webview; + if (!!webview) { + webview.addEventListener("dom-ready", () => { + webview.send("editorInstance.registerQueryExecutor", payload.assignedQueryExecutorId); + }) + } + } + }); +} + +ipcRenderer.on("instanceManager.registerNewInstance", (event, payload) => { + registerNewInstance(payload); +}); + + +$(document).ready(() => { + $("#new-connection").click(() => { + createNewConnection(); + }) +})
\ No newline at end of file @@ -4,7 +4,8 @@ const path = require("path"); const url = require("url"); let uiWindow; -let queryExecutorProcess; +let newConnectionDialog; +let queryExecutors = []; function createMainWindow() { uiWindow = new BrowserWindow({ @@ -23,26 +24,8 @@ function createMainWindow() { }); } -function createQueryExecutorProcess() { - queryExecutorProcess = new BrowserWindow({ - show: false - }); - - queryExecutorProcess.loadURL(url.format({ - pathname: path.join(__dirname, "./query-executor-wrapper.html"), - protocol: "file:", - slashes: true - })); - - queryExecutorProcess.on("closed", () => { - queryExecutorProcess = null; - }); - -} - app.on("ready", () => { createMainWindow(); - createQueryExecutorProcess(); }); app.on("window-all-closed", () => { @@ -57,8 +40,85 @@ app.on("activate", () => { } }); -ipcMain.on("queryExecutor.runQueryComplete", (event, payload) => uiWindow.webContents.send("queryExecutor.runQueryComplete", payload)); -ipcMain.on("queryExecutor.runQuery", (event, payload) => queryExecutorProcess.webContents.send("queryExecutor.runQuery", payload)); +function createNewConnectionDialog() { + newConnectionDialog = new BrowserWindow({ + width: 400, + height: 540 + }); + newConnectionDialog.loadURL(url.format({ + pathname: path.join(__dirname, "new-connection.html"), + protocol: "file:", + slashes: true + })); + + newConnectionDialog.on("closed", () => { + newConnectionDialog = null; + }); +} + +ipcMain.on("instanceManager.openNewConnectionDialog", (event, payload) => { + createNewConnectionDialog(); +}); + +function createQueryExecutor(payload) { + let executor = new BrowserWindow({ + show: false, + }); + + executor.connectionConfig = payload; + + executor.loadURL(url.format({ + pathname: path.join(__dirname, "./query-executor-wrapper.html"), + protocol: "file:", + slashes: true + })); + + queryExecutors.push(executor); + + return executor; +} + +ipcMain.on("newConnection.createConnection", (event, payload) => { + createQueryExecutor(payload); +}); + + +ipcMain.on("queryExecutor.initialiseConnectionCallback", (event, executorId) => { + // TODO - handle connection initialisation errors + + // Bit of a hack, cant guarantee this was the executor which just got initalised.executor + // Should pass it back from the executor via the payload + let connectionName = queryExecutors[queryExecutors.length -1].connectionConfig.name; + + uiWindow.webContents.send("instanceManager.registerNewInstance", {assignedQueryExecutorId: executorId, connectionName: connectionName}); + newConnectionDialog.close(); +}); + + +const { webContents } = require('electron'); + +// TODO - only send messages to instance manager which will route request to correct webView, rather than +// sending to all webViews +ipcMain.on("queryExecutor.runQueryComplete", (event, payload) => { + webContents.getAllWebContents().forEach((w) => { + w.send("queryExecutor.runQueryComplete", payload); + }) +}); -ipcMain.on("queryExecutor.queryTableMetadataComplete", (event, payload) => uiWindow.webContents.send("queryExecutor.queryTableMetadataComplete", payload)); -ipcMain.on("queryExecutor.queryTableMetadata", (event, payload) => queryExecutorProcess.webContents.send("queryExecutor.queryTableMetadata", payload));
\ No newline at end of file +ipcMain.on("queryExecutor.runQuery", (event, payload) => { + queryExecutors.forEach((executor) => { + executor.webContents.send("queryExecutor.runQuery", payload); + }); +}); + +ipcMain.on("queryExecutor.queryTableMetadataComplete", (event, payload) => { + webContents.getAllWebContents().forEach((w) => { + w.send("queryExecutor.queryTableMetadataComplete", payload); + }) +}); +; +ipcMain.on("queryExecutor.queryTableMetadata", (event, payload) => { + queryExecutors.forEach((executor) => { + executor.webContents.send("queryExecutor.queryTableMetadata", payload); + }); +});
\ No newline at end of file diff --git a/new-connection.html b/new-connection.html new file mode 100644 index 0000000..1ac6c89 --- /dev/null +++ b/new-connection.html @@ -0,0 +1,72 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="UTF-8"> + <title>SQL++ - New connection</title> + <link rel="stylesheet" href="node_modules/bulma/css/bulma.css"></link> + <link rel="stylesheet" href="./styles/new-connection.css"></link> + </head> + <body> + + <h4 class="title is-4" >Add new connection</h4> + <form> + <div class="field"> + <label class="label is-small" for="name">Connection name</label> + <div class="control"> + <input name="name" class="input is-small" type="text"/> + </div> + <p class="help">This is shown as the title of the connection tab</p> + </div> + <div class="field"> + <label class="label is-small" for="vendor">Vendor</label> + <div class="control"> + <div class="select is-small"> + <select name="vendor"> + <option value="postgres">Postgres</option> + <option value="mysql">MySQL/MariaDB</option> + <option value="oracle">Oracle</option> + </select> + </div> + </div> + </div> + <div class="field"> + <label class="label is-small" for="hostname">Hostname</label> + <div class="control"> + <input name="hostname" class="input is-small" type="text"/> + </div> + </div> + <div class="field"> + <label class="label is-small" for="port">Port</label> + <div class="control"> + <input name="port" class="input is-small" type="text" placeholder="5432"/> + </div> + </div> + <div class="field"> + <label class="label is-small" for="username">Username</label> + <div class="control"> + <input name="username" class="input is-small" type="text"/> + </div> + </div> + <div class="field"> + <label class="label is-small" for="password">Password</label> + <div class="control"> + <input name="password" class="input is-small" type="text"/> + </div> + </div> + <div class="field is-grouped"> + <div class="control"> + <input id="create-connection" type="button" class="button is-link is-small" value="Add"></input> + </div> + <div class="control"> + <input id="test-connection" type="button" class="button is-small" value="Test connection"></input> + </div> + <div class="control"> + <input id="cancel" type="button" class="button is-text is-small" value="Cancel"></input> + </div> + </div> + </form> + </body> + <script> + require("./new-connection.js"); + </script> +</html>
\ No newline at end of file diff --git a/new-connection.js b/new-connection.js new file mode 100644 index 0000000..2493c25 --- /dev/null +++ b/new-connection.js @@ -0,0 +1,46 @@ +"use strict"; + +const { remote, ipcRenderer } = require("electron"); +const $ = window.jQuery = require("jquery"); + +function cancel() { + let confirm = remote.dialog.showMessageBox(remote.getCurrentWindow(), { + type: "question", + buttons: ["Yes", "No"], + title: "Confirm", + message: "Remove this connection?" + }); + + if (confirm === 0) { + remote.getCurrentWindow().close(); + } +} + +function parseForm() { + let formData = {}; + + $("form").serializeArray().forEach((input) => { + formData[input.name] = input.value; + }); + + return formData; +} + +function createConnection() { + let connectionProps = parseForm(); + ipcRenderer.send("newConnection.createConnection", connectionProps); +} + +$(document).ready(() => { + $("#create-connection").click(() => { + createConnection(); + }); + + $("#test-connection").click(() => { + //TODO + }); + + $("#cancel").click(() => { + cancel(); + }); +});
\ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 04cec7a..b05f744 100644 --- a/package-lock.json +++ b/package-lock.json @@ -111,6 +111,12 @@ "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", "dev": true }, + "bulma": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/bulma/-/bulma-0.6.2.tgz", + "integrity": "sha1-9LHRHVrMUaeWROsKKwsQZJ09cfU=", + "dev": true + }, "camelcase": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz", @@ -318,6 +324,12 @@ "sumchecker": "1.3.1" } }, + "electron-tabs": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/electron-tabs/-/electron-tabs-0.9.0.tgz", + "integrity": "sha1-kb+L66dftJjp0vjf5WHhH6cLm34=", + "dev": true + }, "error-ex": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.1.tgz", diff --git a/package.json b/package.json index ae1958c..5dd96cb 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,9 @@ "pg": "7.4.1", "split.js": "1.3.5", "jquery-ui": "1.12.1", - "jquery.tabulator": "3.4.2" + "jquery.tabulator": "3.4.2", + "electron-tabs": "0.9.0", + "uuid": "3.2.1", + "bulma": "0.6.2" } } diff --git a/query-executor.js b/query-executor.js index 7bd836d..88df7e1 100644 --- a/query-executor.js +++ b/query-executor.js @@ -1,29 +1,45 @@ "use strict"; -const { ipcRenderer } = require("electron"); +const { remote, ipcRenderer } = require("electron"); const { Pool } = require("pg"); +const executorId = require("uuid/v1")(); + +const connectionConfig = remote.getCurrentWindow().connectionConfig; + const connectionPool = new Pool({ - user: "postgres", - host: "localhost", + user: connectionConfig.username, + host: connectionConfig.host, database: "postgres", - password: "", - port: 5432 + password: connectionConfig.password, + port: connectionConfig.port }); +// Initialisation completed +ipcRenderer.send("queryExecutor.initialiseConnectionCallback", executorId); + ipcRenderer.on("queryExecutor.runQuery", (event, payload) => { - connectionPool.query(payload, (err, res) => { + if(payload.queryExecutorId === executorId) { + connectionPool.query(payload.query, (err, res) => { - console.log(err, res) + console.log(err, res); + + if(err !== undefined) { + // The IPC payload is serialised to JSON beofre transmisison, so the protoype chain is lost. + // So store the nicely formatted error message as a property. + err.cause = err.toString(); + } - ipcRenderer.send("queryExecutor.runQueryComplete", { - "error": err, - "result": res + ipcRenderer.send("queryExecutor.runQueryComplete", { + "error": err, + "result": res, + "editorInstanceId": payload.editorInstanceId + }); + }); - - }); - + } + }); ipcRenderer.on("queryExecutor.queryTableMetadata", (event, payload) => { @@ -50,7 +66,8 @@ ipcRenderer.on("queryExecutor.queryTableMetadata", (event, payload) => { ipcRenderer.send("queryExecutor.queryTableMetadataComplete", { "error": err, - "result": tableMetadata + "result": tableMetadata, + "editorInstanceId": payload.editorInstanceId }); }); diff --git a/styles/style.css b/styles/editor-instance.css index 7ae5727..7ae5727 100644 --- a/styles/style.css +++ b/styles/editor-instance.css diff --git a/styles/instance-manager.css b/styles/instance-manager.css new file mode 100644 index 0000000..c49c8a3 --- /dev/null +++ b/styles/instance-manager.css @@ -0,0 +1,35 @@ +body { + margin: 0; +} + +.etabs-tabgroup { + background-color: #282a36; +} + +.etabs-tab { + color: white; + background: none; + background-color: #474A5E; + border: none; + font-weight: bold; +} + +.etabs-tab.active { + background: #6D8A88; +} + +.etabs-views { + border: none; + height: calc(100vh - 52px); +} + +.etabs-buttons button { + width: inherit; + padding: 4px 6px; + background: #6D8A88; +} + +.etabs-tabgroup-footer { + background: #6D8A88; + height: 20px; +}
\ No newline at end of file diff --git a/styles/new-connection.css b/styles/new-connection.css new file mode 100644 index 0000000..97aaae6 --- /dev/null +++ b/styles/new-connection.css @@ -0,0 +1,3 @@ +html, body { + padding: 8px; +}
\ No newline at end of file |