diff --git a/packages/api/package.json b/packages/api/package.json index 6be13819e..4f02db9e5 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -14,6 +14,7 @@ "express": "^4.17.1", "fs-extra": "^8.1.0", "http": "^0.0.0", + "line-reader": "^0.4.0", "mssql": "^6.0.1", "mysql": "^2.17.1", "nedb-promises": "^4.0.1", diff --git a/packages/api/src/controllers/connections.js b/packages/api/src/controllers/connections.js index 19b4c06d7..3a06afbcc 100644 --- a/packages/api/src/controllers/connections.js +++ b/packages/api/src/controllers/connections.js @@ -1,10 +1,9 @@ - const path = require('path'); const { fork } = require('child_process'); const _ = require('lodash'); const nedb = require('nedb-promises'); -const datadir = require('../utility/datadir'); +const { datadir } = require('../utility/directories'); const socket = require('../utility/socket'); module.exports = { @@ -28,7 +27,7 @@ module.exports = { }, test(req, res) { const subprocess = fork(process.argv[1], ['connectProcess']); - subprocess.on('message', resp => res.json(resp)); + subprocess.on('message', (resp) => res.json(resp)); subprocess.send(req.body); }, diff --git a/packages/api/src/controllers/jsldata.js b/packages/api/src/controllers/jsldata.js new file mode 100644 index 000000000..e2a89434b --- /dev/null +++ b/packages/api/src/controllers/jsldata.js @@ -0,0 +1,77 @@ +const _ = require('lodash'); +const path = require('path'); +const fs = require('fs'); +const lineReader = require('line-reader'); +const { jsldir } = require('../utility/directories'); + +module.exports = { + openedReaders: {}, + + closeReader(jslid) { + if (!this.openedReaders[jslid]) return Promise.resolve(); + return new Promise((resolve, reject) => { + this.openedReaders[jslid].reader.close((err) => { + if (err) reject(err); + resolve(); + delete this.openedReaders[jslid]; + }); + }); + }, + + readLine(jslid) { + if (!this.openedReaders[jslid]) return Promise.reject(); + return new Promise((resolve, reject) => { + const { reader } = this.openedReaders[jslid]; + if (!reader.hasNextLine()) return Promise.resolve(null); + reader.nextLine((err, line) => { + this.openedReaders[jslid].readedCount += 1; + if (err) reject(err); + resolve(line); + }); + }); + }, + + openReader(jslid) { + const file = path.join(jsldir(), `${jslid}.jsonl`); + return new Promise((resolve, reject) => + lineReader.open(file, function (err, reader) { + if (err) reject(err); + resolve(); + this.openedReaders[jslid] = { + reader, + readedCount: 0, + }; + }) + ); + }, + + async ensureReader(jslid, offset) { + if (this.openedReaders[jslid] && this.openedReaders[jslid].readedCount > offset) { + await this.closeReader(); + } + if (!this.openedReaders[jslid]) { + await this.openReader(); + } + while (this.openedReaders[jslid].readedCount < offset) { + await this.readLine(jslid); + } + }, + + getInfo_meta: 'get', + getInfo(jslid) { + const file = path.join(jsldir(), `${jslid}.jsonl.info`); + return JSON.parse(fs.readFileSync(file, 'utf-8')); + }, + + getRows_meta: 'get', + async getRows(jslid, offset, limit) { + await this.ensureReader(jslid, offset); + const res = []; + for (let i = 0; i < limit; i += 1) { + const line = await this.readLine(jslid); + if (line == null) break; + res.push(JSON.parse(line)); + } + return res; + }, +}; diff --git a/packages/api/src/controllers/sessions.js b/packages/api/src/controllers/sessions.js index 8216b17c4..bb74d20b1 100644 --- a/packages/api/src/controllers/sessions.js +++ b/packages/api/src/controllers/sessions.js @@ -24,6 +24,15 @@ module.exports = { socket.emit(`session-info-${sesid}`, info); }, + handle_done(sesid) { + socket.emit(`session-done-${sesid}`); + }, + + handle_recordset(sesid, props) { + const { jslid } = props; + socket.emit(`session-recordset-${sesid}`, { jslid }); + }, + create_meta: 'post', async create({ conid, database }) { const sesid = uuidv1(); diff --git a/packages/api/src/proc/sessionProcess.js b/packages/api/src/proc/sessionProcess.js index 6ad0f49ed..6d3b26ce0 100644 --- a/packages/api/src/proc/sessionProcess.js +++ b/packages/api/src/proc/sessionProcess.js @@ -1,10 +1,55 @@ const engines = require('@dbgate/engines'); +const uuidv1 = require('uuid/v1'); +const path = require('path'); +const fs = require('fs'); + const driverConnect = require('../utility/driverConnect'); +const { jsldir } = require('../utility/directories'); let systemConnection; let storedConnection; let afterConnectCallbacks = []; +class StreamHandler { + constructor() { + this.recordset = this.recordset.bind(this); + this.row = this.row.bind(this); + this.error = this.error.bind(this); + this.done = this.done.bind(this); + this.info = this.info.bind(this); + } + + closeCurrentStream() { + if (this.currentStream) { + this.currentStream.end(); + this.currentStream = null; + } + } + + recordset(columns) { + this.closeCurrentStream(); + this.jslid = uuidv1(); + this.currentFile = path.join(jsldir(), `${this.jslid}.jsonl`); + this.currentStream = fs.createWriteStream(this.currentFile); + fs.writeFileSync(`${this.currentFile}.info`, JSON.stringify(columns)); + process.send({ msgtype: 'recordset', jslid: this.jslid }); + } + row(row) { + // console.log('ACCEPT ROW', row); + this.currentStream.write(JSON.stringify(row) + '\n'); + } + error(error) { + process.send({ msgtype: 'error', error }); + } + done(result) { + this.closeCurrentStream(); + process.send({ msgtype: 'done', result }); + } + info(info) { + process.send({ msgtype: 'info', info }); + } +} + async function handleConnect(connection) { storedConnection = connection; @@ -27,23 +72,8 @@ async function handleExecuteQuery({ sql }) { await waitConnected(); const driver = engines(storedConnection); - await driver.stream(systemConnection, sql, { - recordset: (columns) => { - process.send({ msgtype: 'recordset', columns }); - }, - row: (row) => { - process.send({ msgtype: 'row', row }); - }, - error: (error) => { - process.send({ msgtype: 'error', error }); - }, - done: (result) => { - process.send({ msgtype: 'done', result }); - }, - info: (info) => { - process.send({ msgtype: 'info', info }); - }, - }); + const handler = new StreamHandler(); + await driver.stream(systemConnection, sql, handler); } const messageHandlers = { diff --git a/packages/api/src/utility/datadir.js b/packages/api/src/utility/datadir.js deleted file mode 100644 index 88a77ad4b..000000000 --- a/packages/api/src/utility/datadir.js +++ /dev/null @@ -1,18 +0,0 @@ -const os = require('os'); -const path = require('path'); -const fs = require('fs'); - -let created = false; - -module.exports = function datadir() { - const dir = path.join(os.homedir(), 'dbgate-data'); - if (!created) { - if (!fs.existsSync(dir)) { - console.log(`Creating data directory ${dir}`) - fs.mkdirSync(dir); - } - created = true; - } - - return dir; -}; diff --git a/packages/api/src/utility/directories.js b/packages/api/src/utility/directories.js new file mode 100644 index 000000000..044b6e4e8 --- /dev/null +++ b/packages/api/src/utility/directories.js @@ -0,0 +1,37 @@ +const os = require('os'); +const path = require('path'); +const fs = require('fs'); + +let createdDatadir = false; +let createdJsldir = false; + +function datadir() { + const dir = path.join(os.homedir(), 'dbgate-data'); + if (!createdDatadir) { + if (!fs.existsSync(dir)) { + console.log(`Creating data directory ${dir}`); + fs.mkdirSync(dir); + } + createdDatadir = true; + } + + return dir; +} + +function jsldir() { + const dir = path.join(datadir(), 'jsl'); + if (!createdJsldir) { + if (!fs.existsSync(dir)) { + console.log(`Creating jsl directory ${dir}`); + fs.mkdirSync(dir); + } + createdJsldir = true; + } + + return dir; +} + +module.exports = { + datadir, + jsldir, +}; diff --git a/packages/engines/mssql/index.js b/packages/engines/mssql/index.js index f026ba130..fb67fb873 100644 --- a/packages/engines/mssql/index.js +++ b/packages/engines/mssql/index.js @@ -13,6 +13,10 @@ const dialect = { }, }; +function extractColumns(columns) { + return _.sortBy(_.values(columns), 'index') +} + /** @type {import('@dbgate/types').EngineDriver} */ const driver = { async connect(nativeModules, { server, port, user, password, database }) { @@ -36,7 +40,7 @@ const driver = { const res = {}; if (resp.recordset) { - res.columns = _.sortBy(_.values(resp.recordset.columns), 'index'); + res.columns = extractColumns(resp.recordset.columns); res.rows = resp.recordset; } if (resp.rowsAffected) { @@ -58,15 +62,20 @@ const driver = { }; const handleDone = (result) => { - console.log('RESULT', result); + // console.log('RESULT', result); + options.done(result); }; const handleRow = (row) => { - console.log('ROW', row); + options.row(row); + }; + + const handleRecordset = (columns) => { + options.recordset(extractColumns(columns)); }; request.stream = true; - request.on('recordset', options.recordset); + request.on('recordset', handleRecordset); request.on('row', handleRow); request.on('error', options.error); request.on('done', handleDone); diff --git a/packages/web/src/datagrid/types.ts b/packages/web/src/datagrid/types.ts index db7ba7ecf..fd4c8e804 100644 --- a/packages/web/src/datagrid/types.ts +++ b/packages/web/src/datagrid/types.ts @@ -1,11 +1,11 @@ import { GridDisplay, ChangeSet } from '@dbgate/datalib'; export interface DataGridProps { - conid: number; - database: string; + conid?: number; + database?: string; display: GridDisplay; tabVisible?: boolean; - changeSetState: { value: ChangeSet }; - dispatchChangeSet: Function; - toolbarPortalRef: any; + changeSetState?: { value: ChangeSet }; + dispatchChangeSet?: Function; + toolbarPortalRef?: any; } diff --git a/packages/web/src/sqleditor/JslDataGrid.js b/packages/web/src/sqleditor/JslDataGrid.js new file mode 100644 index 000000000..0d97e23e2 --- /dev/null +++ b/packages/web/src/sqleditor/JslDataGrid.js @@ -0,0 +1,8 @@ +import React from 'react'; +import DataGrid from '../datagrid/DataGrid'; + +export default function JslDataGrid({ jslid }) { + return
{jslid}
; + // const display=React.useMemo(()=>) + // return ; +} diff --git a/packages/web/src/sqleditor/ResultTabs.js b/packages/web/src/sqleditor/ResultTabs.js new file mode 100644 index 000000000..5f7332393 --- /dev/null +++ b/packages/web/src/sqleditor/ResultTabs.js @@ -0,0 +1,34 @@ +import React from 'react'; +import { TabPage, TabControl } from '../widgets/TabControl'; +import useSocket from '../utility/SocketProvider'; +import JslDataGrid from './JslDataGrid'; + +export default function ResultTabs({ children, sessionId }) { + const socket = useSocket(); + const [resultIds, setResultIds] = React.useState([]); + + const handleResultSet = (props) => { + const { jslid } = props; + setResultIds((ids) => [...ids, jslid]); + }; + + React.useEffect(() => { + if (sessionId && socket) { + socket.on(`session-recordset-${sessionId}`, handleResultSet); + return () => { + socket.off(`session-recordset-${sessionId}`, handleResultSet); + }; + } + }, [sessionId, socket]); + + return ( + + {children} + {resultIds.map((jslid, index) => ( + + + + ))} + + ); +} diff --git a/packages/web/src/sqleditor/SqlEditor.js b/packages/web/src/sqleditor/SqlEditor.js index da8454e74..1f2a6cac3 100644 --- a/packages/web/src/sqleditor/SqlEditor.js +++ b/packages/web/src/sqleditor/SqlEditor.js @@ -28,6 +28,7 @@ export default function SqlEditor({ readOnly = false, onChange = undefined, tabVisible = false, + onKeyDown = undefined, }) { const [containerRef, { height, width }] = useDimensions(); const editorRef = React.useRef(null); @@ -35,6 +36,16 @@ export default function SqlEditor({ React.useEffect(() => { if (tabVisible && editorRef.current && editorRef.current.editor) editorRef.current.editor.focus(); }, [tabVisible]); + + React.useEffect(() => { + if (onKeyDown && editorRef.current) { + editorRef.current.editor.keyBinding.addKeyboardHandler(onKeyDown); + } + return () => { + editorRef.current.editor.keyBinding.removeKeyboardHandler(onKeyDown); + }; + }, [onKeyDown]); + return ( {}; + return ( @@ -73,6 +78,7 @@ export default function QueryTab({ tabid, conid, database, tabVisible, toolbarPo onChange={handleChange} tabVisible={tabVisible} engine={connection && connection.engine} + onKeyDown={handleKeyDown} /> {toolbarPortalRef && @@ -84,7 +90,11 @@ export default function QueryTab({ tabid, conid, database, tabVisible, toolbarPo )} - + + + + + ); diff --git a/packages/web/src/widgets/TabControl.js b/packages/web/src/widgets/TabControl.js new file mode 100644 index 000000000..8f75bfb8f --- /dev/null +++ b/packages/web/src/widgets/TabControl.js @@ -0,0 +1,56 @@ +import React from 'react'; +import _ from 'lodash'; +import styled from 'styled-components'; +import theme from '../theme'; + +const TabItem = styled.div` + border-right: 1px solid white; + padding-left: 15px; + padding-right: 15px; + display: flex; + align-items: center; + cursor: pointer; + &:hover { + color: ${theme.tabsPanel.hoverFont}; + } + background-color: ${(props) => + // @ts-ignore + props.selected ? theme.mainArea.background : 'inherit'}; +`; + +const TabNameWrapper = styled.span` + margin-left: 5px; +`; + +const TabContainer = styled.div``; + +const TabsContainer = styled.div` + display: flex; + height: ${theme.tabsPanel.height}px; + right: 0; + background-color: ${theme.tabsPanel.background}; +`; + +export function TabPage({ label = undefined, children }) { + return children; +} + +export function TabControl({ children }) { + const [value, setValue] = React.useState(0); + const childrenArray = (_.isArray(children) ? _.flatten(children) : [children]).filter((x) => x); + return ( +
+ + {childrenArray + .filter((x) => x.props) + .map((tab, index) => ( + // @ts-ignore + setValue(index)} selected={value == index}> + {tab.props.label} + + ))} + + {{childrenArray[value] && childrenArray[value].props.children}} +
+ ); +} diff --git a/yarn.lock b/yarn.lock index 4d7b4d293..1397f47b3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6978,6 +6978,11 @@ lie@3.1.1: dependencies: immediate "~3.0.5" +line-reader@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/line-reader/-/line-reader-0.4.0.tgz#17e44818da0ac335675ba300954f94ef670e66fd" + integrity sha1-F+RIGNoKwzVnW6MAlU+U72cOZv0= + lines-and-columns@^1.1.6: version "1.1.6" resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00"