query - basic print workflow - messages on client

This commit is contained in:
Jan Prochazka 2020-04-05 20:48:04 +02:00
parent 3df4e9b7dc
commit 72375ec635
13 changed files with 315 additions and 24 deletions

View File

@ -49,7 +49,7 @@ module.exports = {
},
/** @param {import('@dbgate/types').OpenedDatabaseConnection} conn */
async sendRequest(conn, message) {
sendRequest(conn, message) {
const msgid = uuidv1();
const promise = new Promise((resolve, reject) => {
this.requests[msgid] = [resolve, reject];

View File

@ -0,0 +1,68 @@
const _ = require('lodash');
const uuidv1 = require('uuid/v1');
const connections = require('./connections');
const socket = require('../utility/socket');
const { fork } = require('child_process');
const DatabaseAnalyser = require('@dbgate/engines/default/DatabaseAnalyser');
module.exports = {
/** @type {import('@dbgate/types').OpenedSession[]} */
opened: [],
handle_error(sesid, props) {
const { error } = props;
console.log(`Error in database session ${sesid}: ${error}`);
},
// handle_row(sesid, props) {
// const { row } = props;
// socket.emit('sessionRow', row);
// },
handle_info(sesid, props) {
const { info } = props;
socket.emit(`session-info-${sesid}`, info);
},
create_meta: 'post',
async create({ conid, database }) {
const sesid = uuidv1();
const connection = await connections.get({ conid });
const subprocess = fork(process.argv[1], ['sessionProcess']);
const newOpened = {
conid,
database,
subprocess,
connection,
sesid,
};
this.opened.push(newOpened);
// @ts-ignore
subprocess.on('message', ({ msgtype, ...message }) => {
this[`handle_${msgtype}`](sesid, message);
});
subprocess.send({ msgtype: 'connect', ...connection, database });
return newOpened;
},
executeQuery_meta: 'post',
async executeQuery({ sesid, sql }) {
const session = this.opened.find((x) => x.sesid == sesid);
if (!session) {
throw new Error('Invalid session');
}
console.log(`Processing query, sesid=${sesid}, sql=${sql}`);
session.subprocess.send({ msgtype: 'executeQuery', sql });
return { state: 'ok' };
},
// runCommand_meta: 'post',
// async runCommand({ conid, database, sql }) {
// console.log(`Running SQL command , conid=${conid}, database=${database}, sql=${sql}`);
// const opened = await this.ensureOpened(conid, database);
// const res = await this.sendRequest(opened, { msgtype: 'queryData', sql });
// return res;
// },
};

View File

@ -10,6 +10,7 @@ const connections = require('./controllers/connections');
const serverConnections = require('./controllers/serverConnections');
const databaseConnections = require('./controllers/databaseConnections');
const tables = require('./controllers/tables');
const sessions = require('./controllers/sessions');
const socket = require('./utility/socket');
function start() {
@ -27,6 +28,7 @@ function start() {
useController(app, '/server-connections', serverConnections);
useController(app, '/database-connections', databaseConnections);
useController(app, '/tables', tables);
useController(app, '/sessions', sessions);
if (fs.existsSync('/home/dbgate-docker/build')) {
// server static files inside docker container

View File

@ -1,9 +1,11 @@
const connectProcess = require('./connectProcess');
const databaseConnectionProcess = require('./databaseConnectionProcess');
const serverConnectionProcess = require('./serverConnectionProcess');
const sessionProcess = require('./sessionProcess');
module.exports = {
connectProcess,
databaseConnectionProcess,
serverConnectionProcess,
sessionProcess,
};

View File

@ -0,0 +1,69 @@
const engines = require('@dbgate/engines');
const driverConnect = require('../utility/driverConnect');
let systemConnection;
let storedConnection;
let afterConnectCallbacks = [];
async function handleConnect(connection) {
storedConnection = connection;
const driver = engines(storedConnection);
systemConnection = await driverConnect(driver, storedConnection);
for (const [resolve, reject] of afterConnectCallbacks) {
resolve();
}
afterConnectCallbacks = [];
}
function waitConnected() {
if (systemConnection) return Promise.resolve();
return new Promise((resolve, reject) => {
afterConnectCallbacks.push([resolve, reject]);
});
}
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 messageHandlers = {
connect: handleConnect,
executeQuery: handleExecuteQuery,
};
async function handleMessage({ msgtype, ...other }) {
const handler = messageHandlers[msgtype];
await handler(other);
}
function start() {
process.on('message', async (message) => {
try {
await handleMessage(message);
} catch (e) {
process.send({ msgtype: 'error', error: e.message });
}
});
}
module.exports = { start };

View File

@ -44,6 +44,35 @@ const driver = {
}
return res;
},
async stream(pool, sql, options) {
const request = await pool.request();
const handleInfo = (info) => {
const { message, lineNumber, procName } = info;
options.info({
message,
line: lineNumber,
procedure: procName,
time: new Date(),
});
};
const handleDone = (result) => {
console.log('RESULT', result);
};
const handleRow = (row) => {
console.log('ROW', row);
};
request.stream = true;
request.on('recordset', options.recordset);
request.on('row', handleRow);
request.on('error', options.error);
request.on('done', handleDone);
request.on('info', handleInfo);
request.query(sql);
},
async getVersion(pool) {
const { version } = (await this.query(pool, 'SELECT @@VERSION AS version')).rows[0];
return { version };

View File

@ -1,12 +1,21 @@
import { QueryResult } from "./query";
import { SqlDialect } from "./dialect";
import { SqlDumper } from "./dumper";
import { DatabaseInfo } from "./dbinfo";
import { QueryResult } from './query';
import { SqlDialect } from './dialect';
import { SqlDumper } from './dumper';
import { DatabaseInfo } from './dbinfo';
export interface StreamOptions {
recordset: (columns) => void;
row: (row) => void;
error: (error) => void;
done: (result) => void;
info: (info) => void;
}
export interface EngineDriver {
engine: string;
connect(nativeModules, { server, port, user, password, database }): any;
query(pool: any, sql: string): Promise<QueryResult>;
stream(pool: any, sql: string, options: StreamOptions);
getVersion(pool: any): Promise<{ version: string }>;
listDatabases(
pool: any

View File

@ -7,6 +7,13 @@ export interface OpenedDatabaseConnection {
subprocess: ChildProcess;
}
export interface OpenedSession {
sesid: string;
conid: string;
database: string;
subprocess: ChildProcess;
}
export interface StoredConnection {
engine: string;
server: string;

View File

@ -0,0 +1,24 @@
import React from 'react';
export default function MessagesView({ items }) {
return (
<table>
<tr>
<th>Number</th>
<th>Message</th>
<th>Time</th>
<th>Procedure</th>
<th>Line</th>
</tr>
{items.map((row, index) => (
<tr key={index}>
<td>{index + 1}</td>
<td>{row.message}</td>
<td>{row.time}</td>
<td>{row.procedure}</td>
<td>{row.line}</td>
</tr>
))}
</table>
);
}

View File

@ -1,8 +1,10 @@
import React from 'react'
import ToolbarButton from '../widgets/ToolbarButton'
import React from 'react';
import ToolbarButton from '../widgets/ToolbarButton';
export default function QueryToolbar() {
return <>
<ToolbarButton onClick={()=>{}}>Execute</ToolbarButton>
export default function QueryToolbar({ execute,isDatabaseDefined }) {
return (
<>
<ToolbarButton disabled={!isDatabaseDefined} onClick={execute}>Execute</ToolbarButton>
</>
}
);
}

View File

@ -0,0 +1,21 @@
import React from 'react';
import MessagesView from './MessagesView';
import useSocket from '../utility/SocketProvider';
export default function SessionMessagesView({ sessionId }) {
const [messages, setMessages] = React.useState([]);
const socket = useSocket();
const handleInfo = React.useCallback((info) => setMessages((items) => [...items, info]), []);
React.useEffect(() => {
if (sessionId && socket) {
socket.on(`session-info-${sessionId}`, handleInfo);
return () => {
socket.off(`session-info-${sessionId}`, handleInfo);
};
}
}, [sessionId, socket]);
return <MessagesView items={messages} />;
}

View File

@ -1,17 +1,30 @@
import React from 'react';
import ReactDOM from 'react-dom';
import _ from 'lodash';
import axios from '../utility/axios';
import engines from '@dbgate/engines';
import useTableInfo from '../utility/useTableInfo';
import useConnectionInfo from '../utility/useConnectionInfo';
import SqlEditor from '../sqleditor/SqlEditor';
import { useUpdateDatabaseForTab } from '../utility/globalState';
import QueryToolbar from '../query/QueryToolbar';
import styled from 'styled-components';
import SessionMessagesView from '../query/SessionMessagesView';
const MainContainer = styled.div``;
const EditorContainer = styled.div`
height: 600px;
position: relative;
`;
const MessagesContainer = styled.div``;
export default function QueryTab({ tabid, conid, database, tabVisible, toolbarPortalRef }) {
const localStorageKey = `sql_${tabid}`;
const [queryText, setQueryText] = React.useState(() => localStorage.getItem(localStorageKey) || '');
const queryTextRef = React.useRef(queryText);
const [sessionId, setSessionId] = React.useState(null);
const saveToStorage = React.useCallback(() => localStorage.setItem(localStorageKey, queryTextRef.current), [
localStorageKey,
@ -22,26 +35,57 @@ export default function QueryTab({ tabid, conid, database, tabVisible, toolbarPo
React.useEffect(() => {
window.addEventListener('beforeunload', saveToStorage);
return () => {
saveToStorage();
window.removeEventListener('beforeunload', saveToStorage);
};
}, []);
useUpdateDatabaseForTab(tabVisible, conid, database);
const connection = useConnectionInfo(conid);
const handleChange = text => {
const handleChange = (text) => {
if (text != null) queryTextRef.current = text;
setQueryText(text);
saveToStorageDebounced();
};
return (
<>
<SqlEditor value={queryText} onChange={handleChange} tabVisible={tabVisible} />
const handleExecute = async () => {
let sesid = sessionId;
if (!sesid) {
const resp = await axios.post('sessions/create', {
conid,
database,
});
sesid = resp.data.sesid;
setSessionId(sesid);
}
const resp2 = await axios.post('sessions/execute-query', {
sesid,
sql: queryText,
});
};
{toolbarPortalRef &&
toolbarPortalRef.current &&
tabVisible &&
ReactDOM.createPortal(<QueryToolbar />, toolbarPortalRef.current)}
</>
return (
<MainContainer>
<EditorContainer>
<SqlEditor
value={queryText}
onChange={handleChange}
tabVisible={tabVisible}
engine={connection && connection.engine}
/>
{toolbarPortalRef &&
toolbarPortalRef.current &&
tabVisible &&
ReactDOM.createPortal(
<QueryToolbar isDatabaseDefined={conid && database} execute={handleExecute} />,
toolbarPortalRef.current
)}
</EditorContainer>
<MessagesContainer>
<SessionMessagesView sessionId={sessionId} />
</MessagesContainer>
</MainContainer>
);
}

View File

@ -16,9 +16,9 @@ export default function useFetch({
const [loadCounter, setLoadCounter] = React.useState(0);
const socket = useSocket();
const handleReload = () => {
setLoadCounter(loadCounter + 1);
};
const handleReload = React.useCallback(() => {
setLoadCounter((counter) => counter + 1);
}, []);
const indicators = [url, stableStringify(data), stableStringify(params), loadCounter];
@ -32,15 +32,29 @@ export default function useFetch({
});
setValue([resp.data, loadedIndicators]);
}
// React.useEffect(() => {
// loadValue(indicators);
// if (reloadTrigger && socket) {
// socket.on(reloadTrigger, handleReload);
// return () => {
// socket.off(reloadTrigger, handleReload);
// };
// }
// }, [...indicators, socket]);
React.useEffect(() => {
loadValue(indicators);
}, [...indicators]);
React.useEffect(() => {
if (reloadTrigger && socket) {
socket.on(reloadTrigger, handleReload);
return () => {
socket.off(reloadTrigger, handleReload);
};
}
}, [...indicators, socket]);
}, [socket, reloadTrigger]);
const [returnValue, loadedIndicators] = value;
if (_.isEqual(indicators, loadedIndicators)) return returnValue;