diff options
| -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 |