query - result tabs

This commit is contained in:
Jan Prochazka 2020-04-07 21:37:00 +02:00
parent 72375ec635
commit 949985769c
15 changed files with 316 additions and 48 deletions

View File

@ -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",

View File

@ -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);
},

View File

@ -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;
},
};

View File

@ -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();

View File

@ -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 = {

View File

@ -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;
};

View File

@ -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,
};

View File

@ -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);

View File

@ -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;
}

View File

@ -0,0 +1,8 @@
import React from 'react';
import DataGrid from '../datagrid/DataGrid';
export default function JslDataGrid({ jslid }) {
return <div>{jslid}</div>;
// const display=React.useMemo(()=>)
// return <DataGrid />;
}

View File

@ -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 (
<TabControl>
{children}
{resultIds.map((jslid, index) => (
<TabPage label={`Result ${index + 1}`} key={index}>
<JslDataGrid jslid={jslid} />
</TabPage>
))}
</TabControl>
);
}

View File

@ -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 (
<Wrapper ref={containerRef}>
<AceEditor

View File

@ -10,6 +10,9 @@ import { useUpdateDatabaseForTab } from '../utility/globalState';
import QueryToolbar from '../query/QueryToolbar';
import styled from 'styled-components';
import SessionMessagesView from '../query/SessionMessagesView';
import { TabPage, TabControl } from '../widgets/TabControl';
import getResultTabs from '../sqleditor/ResultTabs';
import ResultTabs from '../sqleditor/ResultTabs';
const MainContainer = styled.div``;
@ -65,6 +68,8 @@ export default function QueryTab({ tabid, conid, database, tabVisible, toolbarPo
});
};
const handleKeyDown = (e) => {};
return (
<MainContainer>
<EditorContainer>
@ -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
)}
</EditorContainer>
<MessagesContainer>
<SessionMessagesView sessionId={sessionId} />
<ResultTabs sessionId={sessionId}>
<TabPage label="Messages">
<SessionMessagesView sessionId={sessionId} />
</TabPage>
</ResultTabs>
</MessagesContainer>
</MainContainer>
);

View File

@ -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 (
<div>
<TabsContainer>
{childrenArray
.filter((x) => x.props)
.map((tab, index) => (
// @ts-ignore
<TabItem key={index} onClick={() => setValue(index)} selected={value == index}>
<TabNameWrapper>{tab.props.label}</TabNameWrapper>
</TabItem>
))}
</TabsContainer>
{<TabContainer key={value}>{childrenArray[value] && childrenArray[value].props.children}</TabContainer>}
</div>
);
}

View File

@ -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"