mirror of
https://github.com/dbgate/dbgate
synced 2024-11-07 20:26:23 +00:00
query - basic print workflow - messages on client
This commit is contained in:
parent
3df4e9b7dc
commit
72375ec635
@ -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];
|
||||
|
68
packages/api/src/controllers/sessions.js
Normal file
68
packages/api/src/controllers/sessions.js
Normal 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;
|
||||
// },
|
||||
};
|
@ -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
|
||||
|
@ -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,
|
||||
};
|
||||
|
69
packages/api/src/proc/sessionProcess.js
Normal file
69
packages/api/src/proc/sessionProcess.js
Normal 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 };
|
@ -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 };
|
||||
|
17
packages/types/engines.d.ts
vendored
17
packages/types/engines.d.ts
vendored
@ -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
|
||||
|
7
packages/types/index.d.ts
vendored
7
packages/types/index.d.ts
vendored
@ -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;
|
||||
|
24
packages/web/src/query/MessagesView.js
Normal file
24
packages/web/src/query/MessagesView.js
Normal 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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
</>
|
||||
}
|
||||
);
|
||||
}
|
||||
|
21
packages/web/src/query/SessionMessagesView.js
Normal file
21
packages/web/src/query/SessionMessagesView.js
Normal 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} />;
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user